diff --git a/.coveragerc b/.coveragerc
index c692dfbba5e618..6e3db6555efb6e 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -23,6 +23,8 @@ omit =
homeassistant/components/adguard/sensor.py
homeassistant/components/adguard/switch.py
homeassistant/components/ads/*
+ homeassistant/components/aemet/abstract_aemet_sensor.py
+ homeassistant/components/aemet/weather_update_coordinator.py
homeassistant/components/aftership/sensor.py
homeassistant/components/agent_dvr/__init__.py
homeassistant/components/agent_dvr/alarm_control_panel.py
@@ -67,11 +69,14 @@ omit =
homeassistant/components/arwn/sensor.py
homeassistant/components/asterisk_cdr/mailbox.py
homeassistant/components/asterisk_mbox/*
+ homeassistant/components/asuswrt/__init__.py
+ homeassistant/components/asuswrt/router.py
homeassistant/components/aten_pe/*
homeassistant/components/atome/*
homeassistant/components/aurora/__init__.py
homeassistant/components/aurora/binary_sensor.py
homeassistant/components/aurora/const.py
+ homeassistant/components/aurora/sensor.py
homeassistant/components/aurora_abb_powerone/sensor.py
homeassistant/components/avea/light.py
homeassistant/components/avion/light.py
@@ -156,7 +161,6 @@ omit =
homeassistant/components/coolmaster/const.py
homeassistant/components/cppm_tracker/device_tracker.py
homeassistant/components/cpuspeed/sensor.py
- homeassistant/components/crimereports/sensor.py
homeassistant/components/cups/sensor.py
homeassistant/components/currencylayer/sensor.py
homeassistant/components/daikin/*
@@ -169,6 +173,7 @@ omit =
homeassistant/components/deluge/sensor.py
homeassistant/components/deluge/switch.py
homeassistant/components/denon/media_player.py
+ homeassistant/components/denonavr/__init__.py
homeassistant/components/denonavr/media_player.py
homeassistant/components/denonavr/receiver.py
homeassistant/components/deutsche_bahn/sensor.py
@@ -216,6 +221,7 @@ omit =
homeassistant/components/ecobee/weather.py
homeassistant/components/econet/__init__.py
homeassistant/components/econet/binary_sensor.py
+ homeassistant/components/econet/climate.py
homeassistant/components/econet/const.py
homeassistant/components/econet/sensor.py
homeassistant/components/econet/water_heater.py
@@ -231,6 +237,8 @@ omit =
homeassistant/components/emby/media_player.py
homeassistant/components/emoncms/sensor.py
homeassistant/components/emoncms_history/*
+ homeassistant/components/emonitor/__init__.py
+ homeassistant/components/emonitor/sensor.py
homeassistant/components/enigma2/media_player.py
homeassistant/components/enocean/__init__.py
homeassistant/components/enocean/binary_sensor.py
@@ -240,6 +248,7 @@ omit =
homeassistant/components/enocean/light.py
homeassistant/components/enocean/sensor.py
homeassistant/components/enocean/switch.py
+ homeassistant/components/enphase_envoy/__init__.py
homeassistant/components/enphase_envoy/sensor.py
homeassistant/components/entur_public_transport/*
homeassistant/components/environment_canada/*
@@ -265,8 +274,16 @@ omit =
homeassistant/components/eufy/*
homeassistant/components/everlights/light.py
homeassistant/components/evohome/*
- homeassistant/components/ezviz/*
+ homeassistant/components/ezviz/__init__.py
+ homeassistant/components/ezviz/camera.py
+ homeassistant/components/ezviz/coordinator.py
+ homeassistant/components/ezviz/const.py
+ homeassistant/components/ezviz/binary_sensor.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/fastdotcom/*
homeassistant/components/ffmpeg/camera.py
homeassistant/components/fibaro/*
@@ -307,7 +324,6 @@ omit =
homeassistant/components/foscam/camera.py
homeassistant/components/foursquare/*
homeassistant/components/free_mobile/notify.py
- homeassistant/components/freebox/__init__.py
homeassistant/components/freebox/device_tracker.py
homeassistant/components/freebox/router.py
homeassistant/components/freebox/sensor.py
@@ -328,7 +344,6 @@ omit =
homeassistant/components/garmin_connect/alarm_util.py
homeassistant/components/gc100/*
homeassistant/components/geniushub/*
- homeassistant/components/geizhals/sensor.py
homeassistant/components/github/sensor.py
homeassistant/components/gitlab_ci/sensor.py
homeassistant/components/gitter/sensor.py
@@ -342,6 +357,8 @@ omit =
homeassistant/components/google/*
homeassistant/components/google_cloud/tts.py
homeassistant/components/google_maps/device_tracker.py
+ homeassistant/components/google_travel_time/__init__.py
+ homeassistant/components/google_travel_time/helpers.py
homeassistant/components/google_travel_time/sensor.py
homeassistant/components/gpmdp/media_player.py
homeassistant/components/gpsd/sensor.py
@@ -357,7 +374,9 @@ omit =
homeassistant/components/guardian/sensor.py
homeassistant/components/guardian/switch.py
homeassistant/components/guardian/util.py
- homeassistant/components/habitica/*
+ homeassistant/components/habitica/__init__.py
+ homeassistant/components/habitica/const.py
+ homeassistant/components/habitica/sensor.py
homeassistant/components/hangouts/*
homeassistant/components/hangouts/__init__.py
homeassistant/components/hangouts/const.py
@@ -368,6 +387,9 @@ omit =
homeassistant/components/harmony/data.py
homeassistant/components/harmony/remote.py
homeassistant/components/harmony/util.py
+ homeassistant/components/hassio/binary_sensor.py
+ homeassistant/components/hassio/entity.py
+ homeassistant/components/hassio/sensor.py
homeassistant/components/haveibeenpwned/sensor.py
homeassistant/components/hdmi_cec/*
homeassistant/components/heatmiser/climate.py
@@ -375,7 +397,13 @@ omit =
homeassistant/components/hikvisioncam/switch.py
homeassistant/components/hisense_aehw4a1/*
homeassistant/components/hitron_coda/device_tracker.py
- homeassistant/components/hive/*
+ homeassistant/components/hive/__init__.py
+ homeassistant/components/hive/climate.py
+ homeassistant/components/hive/binary_sensor.py
+ homeassistant/components/hive/light.py
+ homeassistant/components/hive/sensor.py
+ homeassistant/components/hive/switch.py
+ homeassistant/components/hive/water_heater.py
homeassistant/components/hlk_sw16/__init__.py
homeassistant/components/hlk_sw16/switch.py
homeassistant/components/home_connect/*
@@ -383,6 +411,9 @@ omit =
homeassistant/components/homematic/climate.py
homeassistant/components/homematic/cover.py
homeassistant/components/homematic/notify.py
+ homeassistant/components/home_plus_control/api.py
+ homeassistant/components/home_plus_control/helpers.py
+ homeassistant/components/home_plus_control/switch.py
homeassistant/components/homeworks/*
homeassistant/components/honeywell/climate.py
homeassistant/components/horizon/media_player.py
@@ -462,7 +493,11 @@ omit =
homeassistant/components/kaiterra/*
homeassistant/components/kankun/switch.py
homeassistant/components/keba/*
+ homeassistant/components/keenetic_ndms2/__init__.py
+ homeassistant/components/keenetic_ndms2/binary_sensor.py
+ homeassistant/components/keenetic_ndms2/const.py
homeassistant/components/keenetic_ndms2/device_tracker.py
+ homeassistant/components/keenetic_ndms2/router.py
homeassistant/components/kef/*
homeassistant/components/keyboard/*
homeassistant/components/keyboard_remote/*
@@ -477,6 +512,10 @@ omit =
homeassistant/components/kodi/media_player.py
homeassistant/components/kodi/notify.py
homeassistant/components/konnected/*
+ homeassistant/components/kostal_plenticore/__init__.py
+ homeassistant/components/kostal_plenticore/const.py
+ homeassistant/components/kostal_plenticore/helper.py
+ homeassistant/components/kostal_plenticore/sensor.py
homeassistant/components/kwb/sensor.py
homeassistant/components/lacrosse/sensor.py
homeassistant/components/lametric/*
@@ -484,7 +523,18 @@ omit =
homeassistant/components/lastfm/sensor.py
homeassistant/components/launch_library/const.py
homeassistant/components/launch_library/sensor.py
- homeassistant/components/lcn/*
+ homeassistant/components/lcn/__init__.py
+ homeassistant/components/lcn/binary_sensor.py
+ homeassistant/components/lcn/climate.py
+ homeassistant/components/lcn/const.py
+ homeassistant/components/lcn/cover.py
+ homeassistant/components/lcn/helpers.py
+ homeassistant/components/lcn/light.py
+ homeassistant/components/lcn/scene.py
+ homeassistant/components/lcn/schemas.py
+ homeassistant/components/lcn/sensor.py
+ homeassistant/components/lcn/services.py
+ homeassistant/components/lcn/switch.py
homeassistant/components/lg_netcast/media_player.py
homeassistant/components/lg_soundbar/media_player.py
homeassistant/components/life360/*
@@ -518,12 +568,15 @@ omit =
homeassistant/components/lutron_caseta/switch.py
homeassistant/components/lw12wifi/light.py
homeassistant/components/lyft/sensor.py
+ homeassistant/components/lyric/__init__.py
+ homeassistant/components/lyric/api.py
+ homeassistant/components/lyric/climate.py
+ homeassistant/components/lyric/sensor.py
homeassistant/components/magicseaweed/sensor.py
homeassistant/components/mailgun/notify.py
homeassistant/components/map/*
homeassistant/components/mastodon/notify.py
homeassistant/components/matrix/*
- homeassistant/components/maxcube/*
homeassistant/components/mcp23017/*
homeassistant/components/media_extractor/*
homeassistant/components/mediaroom/media_player.py
@@ -534,6 +587,8 @@ omit =
homeassistant/components/melcloud/water_heater.py
homeassistant/components/message_bird/notify.py
homeassistant/components/met/weather.py
+ homeassistant/components/met_eireann/__init__.py
+ homeassistant/components/met_eireann/weather.py
homeassistant/components/meteo_france/__init__.py
homeassistant/components/meteo_france/const.py
homeassistant/components/meteo_france/sensor.py
@@ -559,6 +614,7 @@ omit =
homeassistant/components/mochad/*
homeassistant/components/modbus/climate.py
homeassistant/components/modbus/cover.py
+ homeassistant/components/modbus/modbus.py
homeassistant/components/modbus/switch.py
homeassistant/components/modbus/sensor.py
homeassistant/components/modem_callerid/sensor.py
@@ -570,11 +626,27 @@ omit =
homeassistant/components/mpd/media_player.py
homeassistant/components/mqtt_room/sensor.py
homeassistant/components/msteams/notify.py
+ homeassistant/components/mullvad/__init__.py
+ homeassistant/components/mullvad/binary_sensor.py
+ homeassistant/components/nest/const.py
homeassistant/components/mvglive/sensor.py
homeassistant/components/mychevy/*
homeassistant/components/mycroft/*
homeassistant/components/mycroft/notify.py
- homeassistant/components/mysensors/*
+ homeassistant/components/mysensors/__init__.py
+ homeassistant/components/mysensors/binary_sensor.py
+ homeassistant/components/mysensors/climate.py
+ homeassistant/components/mysensors/const.py
+ homeassistant/components/mysensors/cover.py
+ homeassistant/components/mysensors/device.py
+ homeassistant/components/mysensors/device_tracker.py
+ homeassistant/components/mysensors/gateway.py
+ homeassistant/components/mysensors/handler.py
+ homeassistant/components/mysensors/helpers.py
+ homeassistant/components/mysensors/light.py
+ homeassistant/components/mysensors/notify.py
+ homeassistant/components/mysensors/sensor.py
+ homeassistant/components/mysensors/switch.py
homeassistant/components/mystrom/binary_sensor.py
homeassistant/components/mystrom/light.py
homeassistant/components/mystrom/switch.py
@@ -590,17 +662,6 @@ omit =
homeassistant/components/nederlandse_spoorwegen/sensor.py
homeassistant/components/nello/lock.py
homeassistant/components/nest/legacy/*
- homeassistant/components/netatmo/__init__.py
- homeassistant/components/netatmo/api.py
- homeassistant/components/netatmo/camera.py
- homeassistant/components/netatmo/climate.py
- homeassistant/components/netatmo/const.py
- homeassistant/components/netatmo/data_handler.py
- homeassistant/components/netatmo/helper.py
- homeassistant/components/netatmo/light.py
- homeassistant/components/netatmo/netatmo_entity_base.py
- homeassistant/components/netatmo/sensor.py
- homeassistant/components/netatmo/webhook.py
homeassistant/components/netdata/sensor.py
homeassistant/components/netgear/device_tracker.py
homeassistant/components/netgear_lte/*
@@ -620,9 +681,9 @@ omit =
homeassistant/components/norway_air/air_quality.py
homeassistant/components/notify_events/notify.py
homeassistant/components/nsw_fuel_station/sensor.py
- homeassistant/components/nuimo_controller/*
homeassistant/components/nuki/__init__.py
homeassistant/components/nuki/const.py
+ homeassistant/components/nuki/binary_sensor.py
homeassistant/components/nuki/lock.py
homeassistant/components/nut/sensor.py
homeassistant/components/nx584/alarm_control_panel.py
@@ -688,19 +749,26 @@ omit =
homeassistant/components/pandora/media_player.py
homeassistant/components/pcal9535a/*
homeassistant/components/pencom/switch.py
+ homeassistant/components/philips_js/__init__.py
homeassistant/components/philips_js/media_player.py
+ homeassistant/components/philips_js/remote.py
homeassistant/components/pi_hole/sensor.py
homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py
homeassistant/components/pi4ioe5v9xxxx/switch.py
homeassistant/components/picotts/tts.py
homeassistant/components/piglow/light.py
homeassistant/components/pilight/*
+ homeassistant/components/ping/__init__.py
homeassistant/components/ping/const.py
homeassistant/components/ping/binary_sensor.py
homeassistant/components/ping/device_tracker.py
homeassistant/components/pioneer/media_player.py
homeassistant/components/pjlink/media_player.py
- homeassistant/components/plaato/*
+ homeassistant/components/plaato/__init__.py
+ homeassistant/components/plaato/binary_sensor.py
+ homeassistant/components/plaato/const.py
+ homeassistant/components/plaato/entity.py
+ homeassistant/components/plaato/sensor.py
homeassistant/components/plex/media_player.py
homeassistant/components/plum_lightpad/light.py
homeassistant/components/pocketcasts/sensor.py
@@ -744,6 +812,7 @@ omit =
homeassistant/components/raspyrfm/*
homeassistant/components/recollect_waste/__init__.py
homeassistant/components/recollect_waste/sensor.py
+ homeassistant/components/recorder/repack.py
homeassistant/components/recswitch/switch.py
homeassistant/components/reddit/*
homeassistant/components/rejseplanen/sensor.py
@@ -755,6 +824,8 @@ omit =
homeassistant/components/rest/switch.py
homeassistant/components/ring/camera.py
homeassistant/components/ripple/sensor.py
+ homeassistant/components/rituals_perfume_genie/switch.py
+ homeassistant/components/rituals_perfume_genie/__init__.py
homeassistant/components/rocketchat/notify.py
homeassistant/components/roomba/binary_sensor.py
homeassistant/components/roomba/braava.py
@@ -783,6 +854,11 @@ omit =
homeassistant/components/satel_integra/*
homeassistant/components/schluter/*
homeassistant/components/scrape/sensor.py
+ homeassistant/components/screenlogic/__init__.py
+ homeassistant/components/screenlogic/binary_sensor.py
+ homeassistant/components/screenlogic/climate.py
+ homeassistant/components/screenlogic/sensor.py
+ homeassistant/components/screenlogic/switch.py
homeassistant/components/scsgate/*
homeassistant/components/scsgate/cover.py
homeassistant/components/sendgrid/notify.py
@@ -834,7 +910,6 @@ omit =
homeassistant/components/snapcast/*
homeassistant/components/snmp/*
homeassistant/components/sochain/sensor.py
- homeassistant/components/socialblade/sensor.py
homeassistant/components/solaredge/__init__.py
homeassistant/components/solaredge/sensor.py
homeassistant/components/solaredge_local/sensor.py
@@ -877,7 +952,6 @@ omit =
homeassistant/components/switcher_kis/switch.py
homeassistant/components/switchmate/switch.py
homeassistant/components/syncthru/sensor.py
- homeassistant/components/synology/camera.py
homeassistant/components/synology_chat/notify.py
homeassistant/components/synology_dsm/__init__.py
homeassistant/components/synology_dsm/binary_sensor.py
@@ -945,7 +1019,10 @@ omit =
homeassistant/components/toon/sensor.py
homeassistant/components/toon/switch.py
homeassistant/components/torque/sensor.py
- homeassistant/components/totalconnect/*
+ homeassistant/components/totalconnect/__init__.py
+ homeassistant/components/totalconnect/alarm_control_panel.py
+ homeassistant/components/totalconnect/binary_sensor.py
+ homeassistant/components/totalconnect/const.py
homeassistant/components/touchline/climate.py
homeassistant/components/tplink/common.py
homeassistant/components/tplink/switch.py
@@ -964,7 +1041,14 @@ omit =
homeassistant/components/transmission/const.py
homeassistant/components/transmission/errors.py
homeassistant/components/travisci/sensor.py
- homeassistant/components/tuya/*
+ homeassistant/components/tuya/__init__.py
+ homeassistant/components/tuya/climate.py
+ homeassistant/components/tuya/const.py
+ homeassistant/components/tuya/cover.py
+ homeassistant/components/tuya/fan.py
+ homeassistant/components/tuya/light.py
+ homeassistant/components/tuya/scene.py
+ homeassistant/components/tuya/switch.py
homeassistant/components/twentemilieu/const.py
homeassistant/components/twentemilieu/sensor.py
homeassistant/components/twilio_call/notify.py
@@ -996,12 +1080,20 @@ omit =
homeassistant/components/velbus/switch.py
homeassistant/components/velux/*
homeassistant/components/venstar/climate.py
- homeassistant/components/verisure/*
+ homeassistant/components/verisure/__init__.py
+ homeassistant/components/verisure/alarm_control_panel.py
+ homeassistant/components/verisure/binary_sensor.py
+ homeassistant/components/verisure/camera.py
+ homeassistant/components/verisure/coordinator.py
+ homeassistant/components/verisure/lock.py
+ homeassistant/components/verisure/sensor.py
+ homeassistant/components/verisure/switch.py
homeassistant/components/versasense/*
homeassistant/components/vesync/__init__.py
homeassistant/components/vesync/common.py
homeassistant/components/vesync/const.py
homeassistant/components/vesync/fan.py
+ homeassistant/components/vesync/light.py
homeassistant/components/vesync/switch.py
homeassistant/components/viaggiatreno/sensor.py
homeassistant/components/vicare/*
@@ -1021,6 +1113,8 @@ omit =
homeassistant/components/waterfurnace/*
homeassistant/components/watson_iot/*
homeassistant/components/watson_tts/tts.py
+ homeassistant/components/waze_travel_time/__init__.py
+ homeassistant/components/waze_travel_time/helpers.py
homeassistant/components/waze_travel_time/sensor.py
homeassistant/components/webostv/*
homeassistant/components/whois/sensor.py
@@ -1044,7 +1138,6 @@ omit =
homeassistant/components/xbox/sensor.py
homeassistant/components/xbox_live/sensor.py
homeassistant/components/xeoma/camera.py
- homeassistant/components/xfinity/device_tracker.py
homeassistant/components/xiaomi/camera.py
homeassistant/components/xiaomi_aqara/__init__.py
homeassistant/components/xiaomi_aqara/binary_sensor.py
@@ -1057,6 +1150,7 @@ omit =
homeassistant/components/xiaomi_miio/__init__.py
homeassistant/components/xiaomi_miio/air_quality.py
homeassistant/components/xiaomi_miio/alarm_control_panel.py
+ homeassistant/components/xiaomi_miio/device.py
homeassistant/components/xiaomi_miio/device_tracker.py
homeassistant/components/xiaomi_miio/fan.py
homeassistant/components/xiaomi_miio/gateway.py
@@ -1114,3 +1208,6 @@ exclude_lines =
# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError
+
+ # TYPE_CHECKING block is never executed during pytest run
+ if TYPE_CHECKING:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 26e4b2e78adaab..efcc0380748116 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -2,7 +2,9 @@
"name": "Home Assistant Dev",
"context": "..",
"dockerFile": "../Dockerfile.dev",
- "postCreateCommand": "mkdir -p config && pip3 install -e .",
+ "postCreateCommand": "script/setup",
+ "postStartCommand": "script/bootstrap",
+ "containerEnv": { "DEVCONTAINER": "1" },
"appPort": 8123,
"runArgs": ["-e", "GIT_EDITOR=code --wait"],
"extensions": [
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 00000000000000..ad3205c51c8fad
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+custom: https://www.nabucasa.com
+github: balloob
diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md
deleted file mode 100644
index bdadc5678ff72e..00000000000000
--- a/.github/ISSUE_TEMPLATE/BUG_REPORT.md
+++ /dev/null
@@ -1,53 +0,0 @@
----
-name: Report a bug with Home Assistant Core
-about: Report an issue with Home Assistant Core
----
-
-## The problem
-
-
-
-## Environment
-
-
-- Home Assistant Core release with the issue:
-- Last working Home Assistant Core release (if known):
-- Operating environment (OS/Container/Supervised/Core):
-- Integration causing this issue:
-- Link to integration documentation on our website:
-
-## Problem-relevant `configuration.yaml`
-
-
-```yaml
-
-```
-
-## Traceback/Error logs
-
-
-```txt
-
-```
-
-## Additional information
-
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 00000000000000..aa81d6e4df71c9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,97 @@
+name: Report an issue with Home Assistant Core
+description: Report an issue with Home Assistant Core.
+title: ""
+issue_body: true
+body:
+ - type: markdown
+ attributes:
+ value: |
+ This issue form is for reporting bugs only!
+
+ If you have a feature or enhancement request, please use the [feature request][fr] section of our [Community Forum][fr].
+
+ [fr]: https://community.home-assistant.io/c/feature-requests
+ - type: textarea
+ validations:
+ required: true
+ attributes:
+ label: The problem
+ description: >-
+ Describe the issue you are experiencing here to communicate to the
+ maintainers. Tell us what you were trying to do and what happened.
+
+ Provide a clear and concise description of what the problem is.
+ - type: markdown
+ attributes:
+ value: |
+ ## Environment
+ - type: input
+ id: version
+ validations:
+ required: true
+ attributes:
+ label: What is version of Home Assistant Core has the issue?
+ placeholder: core-
+ description: >
+ Can be found in the Configuration panel -> Info.
+ - type: input
+ attributes:
+ label: What was the last working version of Home Assistant Core?
+ placeholder: core-
+ description: >
+ If known, otherwise leave blank.
+ - type: dropdown
+ validations:
+ required: true
+ attributes:
+ label: What type of installation are you running?
+ description: >
+ If you don't know, you can find it in: Configuration panel -> Info.
+ options:
+ - Home Assistant OS
+ - Home Assistant Container
+ - Home Assistant Supervised
+ - Home Assistant Core
+ - type: input
+ id: integration_name
+ attributes:
+ label: Integration causing the issue
+ description: >
+ The name of the integration, for example, Automation or Philips Hue.
+ - type: input
+ id: integration_link
+ attributes:
+ label: Link to integration documentation on our website
+ placeholder: "https://www.home-assistant.io/integrations/..."
+ description: |
+ Providing a link [to the documentation][docs] help us categorizing the
+ issue, while providing a useful reference at the same time.
+
+ [docs]: https://www.home-assistant.io/integrations
+
+ - type: markdown
+ attributes:
+ value: |
+ # Details
+ - type: textarea
+ attributes:
+ label: Example YAML snippet
+ description: |
+ If this issue has an example piece of YAML that can help reproducing this problem, please provide.
+ This can be an piece of YAML from, e.g., an automation, script, scene or configuration.
+ render: yaml
+ - type: textarea
+ attributes:
+ label: Anything in the logs that might be useful for us?
+ description: For example, error message, or stack traces.
+ render: txt
+ - type: markdown
+ attributes:
+ value: |
+ ## Additional information
+ - type: markdown
+ attributes:
+ value: >
+ If you have any additional information for us, use the field below.
+ Please note, you can attach screenshots or screen recordings here, by
+ dragging and dropping files in the field below.
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 6356803cbef7b6..5d43b124c49539 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -12,7 +12,7 @@ on:
env:
CACHE_VERSION: 1
DEFAULT_PYTHON: 3.8
- PRE_COMMIT_HOME: ~/.cache/pre-commit
+ PRE_COMMIT_CACHE: ~/.cache/pre-commit
jobs:
# Separate job to pre-populate the base dependency cache
@@ -20,6 +20,9 @@ jobs:
prepare-base:
name: Prepare base dependencies
runs-on: ubuntu-latest
+ outputs:
+ python-key: ${{ steps.generate-python-key.outputs.key }}
+ pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v2
@@ -28,21 +31,25 @@ jobs:
uses: actions/setup-python@v2.2.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
+ - name: Generate partial Python venv restore key
+ id: generate-python-key
+ run: >-
+ echo "::set-output name=key::base-venv-${{ env.CACHE_VERSION }}-${{
+ hashFiles('requirements.txt') }}-${{
+ hashFiles('requirements_test.txt') }}-${{
+ hashFiles('homeassistant/package_constraints.txt') }}"
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
path: venv
key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
- steps.python.outputs.python-version }}-${{
- hashFiles('requirements.txt') }}-${{
- hashFiles('requirements_test.txt') }}-${{
- hashFiles('homeassistant/package_constraints.txt') }}
+ ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
+ steps.generate-python-key.outputs.key }}
restore-keys: |
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-
+ ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-
+ ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-
+ ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -50,15 +57,20 @@ jobs:
. venv/bin/activate
pip install -U "pip<20.3" setuptools
pip install -r requirements.txt -r requirements_test.txt
+ - name: Generate partial pre-commit restore key
+ id: generate-pre-commit-key
+ run: >-
+ echo "::set-output name=key::pre-commit-${{ env.CACHE_VERSION }}-${{
+ hashFiles('.pre-commit-config.yaml') }}"
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
- path: ${{ env.PRE_COMMIT_HOME }}
- key: |
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
+ path: ${{ env.PRE_COMMIT_CACHE }}
+ key: >-
+ ${{ runner.os }}-${{ steps.generate-pre-commit-key.outputs.key }}
restore-keys: |
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-
+ ${{ runner.os }}-pre-commit-${{ env.CACHE_VERSION }}-
- name: Install pre-commit dependencies
if: steps.cache-precommit.outputs.cache-hit != 'true'
run: |
@@ -79,15 +91,11 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
path: venv
- key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
- steps.python.outputs.python-version }}-${{
- hashFiles('requirements.txt') }}-${{
- hashFiles('requirements_test.txt') }}-${{
- hashFiles('homeassistant/package_constraints.txt') }}
+ key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
+ needs.prepare-base.outputs.python-key }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -95,15 +103,14 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
- path: ${{ env.PRE_COMMIT_HOME }}
- key: |
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- - name: Fail job if cache restore failed
- if: steps.cache-venv.outputs.cache-hit != 'true'
+ path: ${{ env.PRE_COMMIT_CACHE }}
+ key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
+ - name: Fail job if pre-commit cache restore failed
+ if: steps.cache-precommit.outputs.cache-hit != 'true'
run: |
- echo "Failed to restore Python virtual environment from cache"
+ echo "Failed to restore pre-commit environment from cache"
exit 1
- name: Run bandit
run: |
@@ -124,15 +131,11 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
path: venv
- key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
- steps.python.outputs.python-version }}-${{
- hashFiles('requirements.txt') }}-${{
- hashFiles('requirements_test.txt') }}-${{
- hashFiles('homeassistant/package_constraints.txt') }}
+ key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
+ needs.prepare-base.outputs.python-key }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -140,15 +143,14 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
- path: ${{ env.PRE_COMMIT_HOME }}
- key: |
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- - name: Fail job if cache restore failed
- if: steps.cache-venv.outputs.cache-hit != 'true'
+ path: ${{ env.PRE_COMMIT_CACHE }}
+ key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
+ - name: Fail job if pre-commit cache restore failed
+ if: steps.cache-precommit.outputs.cache-hit != 'true'
run: |
- echo "Failed to restore Python virtual environment from cache"
+ echo "Failed to restore pre-commit environment from cache"
exit 1
- name: Run black
run: |
@@ -169,15 +171,11 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
path: venv
- key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
- steps.python.outputs.python-version }}-${{
- hashFiles('requirements.txt') }}-${{
- hashFiles('requirements_test.txt') }}-${{
- hashFiles('homeassistant/package_constraints.txt') }}
+ key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
+ needs.prepare-base.outputs.python-key }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -185,15 +183,14 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
- path: ${{ env.PRE_COMMIT_HOME }}
- key: |
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- - name: Fail job if cache restore failed
- if: steps.cache-venv.outputs.cache-hit != 'true'
+ path: ${{ env.PRE_COMMIT_CACHE }}
+ key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
+ - name: Fail job if pre-commit cache restore failed
+ if: steps.cache-precommit.outputs.cache-hit != 'true'
run: |
- echo "Failed to restore Python virtual environment from cache"
+ echo "Failed to restore pre-commit environment from cache"
exit 1
- name: Register codespell problem matcher
run: |
@@ -236,15 +233,11 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
path: venv
- key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
- steps.python.outputs.python-version }}-${{
- hashFiles('requirements.txt') }}-${{
- hashFiles('requirements_test.txt') }}-${{
- hashFiles('homeassistant/package_constraints.txt') }}
+ key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
+ needs.prepare-base.outputs.python-key }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -252,15 +245,14 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
- path: ${{ env.PRE_COMMIT_HOME }}
- key: |
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- - name: Fail job if cache restore failed
- if: steps.cache-venv.outputs.cache-hit != 'true'
+ path: ${{ env.PRE_COMMIT_CACHE }}
+ key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
+ - name: Fail job if pre-commit cache restore failed
+ if: steps.cache-precommit.outputs.cache-hit != 'true'
run: |
- echo "Failed to restore Python virtual environment from cache"
+ echo "Failed to restore pre-commit environment from cache"
exit 1
- name: Register check executables problem matcher
run: |
@@ -284,15 +276,11 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
path: venv
- key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
- steps.python.outputs.python-version }}-${{
- hashFiles('requirements.txt') }}-${{
- hashFiles('requirements_test.txt') }}-${{
- hashFiles('homeassistant/package_constraints.txt') }}
+ key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
+ needs.prepare-base.outputs.python-key }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -300,15 +288,14 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
- path: ${{ env.PRE_COMMIT_HOME }}
- key: |
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- - name: Fail job if cache restore failed
- if: steps.cache-venv.outputs.cache-hit != 'true'
+ path: ${{ env.PRE_COMMIT_CACHE }}
+ key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
+ - name: Fail job if pre-commit cache restore failed
+ if: steps.cache-precommit.outputs.cache-hit != 'true'
run: |
- echo "Failed to restore Python virtual environment from cache"
+ echo "Failed to restore pre-commit environment from cache"
exit 1
- name: Register flake8 problem matcher
run: |
@@ -332,15 +319,11 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
path: venv
- key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
- steps.python.outputs.python-version }}-${{
- hashFiles('requirements.txt') }}-${{
- hashFiles('requirements_test.txt') }}-${{
- hashFiles('homeassistant/package_constraints.txt') }}
+ key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
+ needs.prepare-base.outputs.python-key }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -348,15 +331,14 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
- path: ${{ env.PRE_COMMIT_HOME }}
- key: |
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- - name: Fail job if cache restore failed
- if: steps.cache-venv.outputs.cache-hit != 'true'
+ path: ${{ env.PRE_COMMIT_CACHE }}
+ key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
+ - name: Fail job if pre-commit cache restore failed
+ if: steps.cache-precommit.outputs.cache-hit != 'true'
run: |
- echo "Failed to restore Python virtual environment from cache"
+ echo "Failed to restore pre-commit environment from cache"
exit 1
- name: Run isort
run: |
@@ -377,15 +359,11 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
path: venv
- key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
- steps.python.outputs.python-version }}-${{
- hashFiles('requirements.txt') }}-${{
- hashFiles('requirements_test.txt') }}-${{
- hashFiles('homeassistant/package_constraints.txt') }}
+ key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
+ needs.prepare-base.outputs.python-key }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -393,15 +371,14 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
- path: ${{ env.PRE_COMMIT_HOME }}
- key: |
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- - name: Fail job if cache restore failed
- if: steps.cache-venv.outputs.cache-hit != 'true'
+ path: ${{ env.PRE_COMMIT_CACHE }}
+ key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
+ - name: Fail job if pre-commit cache restore failed
+ if: steps.cache-precommit.outputs.cache-hit != 'true'
run: |
- echo "Failed to restore Python virtual environment from cache"
+ echo "Failed to restore pre-commit environment from cache"
exit 1
- name: Register check-json problem matcher
run: |
@@ -425,15 +402,11 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
path: venv
- key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
- steps.python.outputs.python-version }}-${{
- hashFiles('requirements.txt') }}-${{
- hashFiles('requirements_test.txt') }}-${{
- hashFiles('homeassistant/package_constraints.txt') }}
+ key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
+ needs.prepare-base.outputs.python-key }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -441,15 +414,14 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
- path: ${{ env.PRE_COMMIT_HOME }}
- key: |
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- - name: Fail job if cache restore failed
- if: steps.cache-venv.outputs.cache-hit != 'true'
+ path: ${{ env.PRE_COMMIT_CACHE }}
+ key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
+ - name: Fail job if pre-commit cache restore failed
+ if: steps.cache-precommit.outputs.cache-hit != 'true'
run: |
- echo "Failed to restore Python virtual environment from cache"
+ echo "Failed to restore pre-commit environment from cache"
exit 1
- name: Run pyupgrade
run: |
@@ -481,15 +453,11 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
path: venv
- key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
- steps.python.outputs.python-version }}-${{
- hashFiles('requirements.txt') }}-${{
- hashFiles('requirements_test.txt') }}-${{
- hashFiles('homeassistant/package_constraints.txt') }}
+ key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
+ needs.prepare-base.outputs.python-key }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -497,15 +465,14 @@ jobs:
exit 1
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
- path: ${{ env.PRE_COMMIT_HOME }}
- key: |
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- - name: Fail job if cache restore failed
- if: steps.cache-venv.outputs.cache-hit != 'true'
+ path: ${{ env.PRE_COMMIT_CACHE }}
+ key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
+ - name: Fail job if pre-commit cache restore failed
+ if: steps.cache-precommit.outputs.cache-hit != 'true'
run: |
- echo "Failed to restore Python virtual environment from cache"
+ echo "Failed to restore pre-commit environment from cache"
exit 1
- name: Register yamllint problem matcher
run: |
@@ -528,14 +495,11 @@ jobs:
uses: actions/checkout@v2
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
path: venv
- key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{
- matrix.python-version }}-${{ hashFiles('requirements_test.txt')
- }}-${{ hashFiles('requirements_all.txt') }}-${{
- hashFiles('homeassistant/package_constraints.txt') }}
+ key: ${{ runner.os }}-${{ matrix.python-version }}-${{
+ needs.prepare-tests.outputs.python-key }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -560,15 +524,11 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
path: venv
- key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
- steps.python.outputs.python-version }}-${{
- hashFiles('requirements.txt') }}-${{
- hashFiles('requirements_test.txt') }}-${{
- hashFiles('homeassistant/package_constraints.txt') }}
+ key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
+ needs.prepare-base.outputs.python-key }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -585,24 +545,31 @@ jobs:
strategy:
matrix:
python-version: [3.8, 3.9]
+ outputs:
+ python-key: ${{ steps.generate-python-key.outputs.key }}
container: homeassistant/ci-azure:${{ matrix.python-version }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v2
+ - name: Generate partial Python venv restore key
+ id: generate-python-key
+ run: >-
+ echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{
+ hashFiles('requirements_test.txt') }}-${{
+ hashFiles('requirements_all.txt') }}-${{
+ hashFiles('homeassistant/package_constraints.txt') }}"
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
path: venv
key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{
- matrix.python-version }}-${{ hashFiles('requirements_test.txt')
- }}-${{ hashFiles('requirements_all.txt') }}-${{
- hashFiles('homeassistant/package_constraints.txt') }}
+ ${{ runner.os }}-${{ matrix.python-version }}-${{
+ steps.generate-python-key.outputs.key }}
restore-keys: |
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }}
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-
+ ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}-
+ ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-
+ ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-
- name: Create full Python ${{ matrix.python-version }} virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -630,14 +597,11 @@ jobs:
uses: actions/checkout@v2
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
path: venv
- key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{
- matrix.python-version }}-${{ hashFiles('requirements_test.txt')
- }}-${{ hashFiles('requirements_all.txt') }}-${{
- hashFiles('homeassistant/package_constraints.txt') }}
+ key: ${{ runner.os }}-${{ matrix.python-version }}-${{
+ needs.prepare-tests.outputs.python-key }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -664,14 +628,11 @@ jobs:
uses: actions/checkout@v2
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
path: venv
- key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{
- matrix.python-version }}-${{ hashFiles('requirements_test.txt')
- }}-${{ hashFiles('requirements_all.txt') }}-${{
- hashFiles('homeassistant/package_constraints.txt') }}
+ key: ${{ runner.os }}-${{ matrix.python-version }}-${{
+ needs.prepare-tests.outputs.python-key }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -689,6 +650,7 @@ jobs:
runs-on: ubuntu-latest
needs: prepare-tests
strategy:
+ fail-fast: false
matrix:
group: [1, 2, 3, 4]
python-version: [3.8, 3.9]
@@ -700,14 +662,11 @@ jobs:
uses: actions/checkout@v2
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
path: venv
- key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{
- matrix.python-version }}-${{ hashFiles('requirements_test.txt')
- }}-${{ hashFiles('requirements_all.txt') }}-${{
- hashFiles('homeassistant/package_constraints.txt') }}
+ key: ${{ runner.os }}-${{ matrix.python-version }}-${{
+ needs.prepare-tests.outputs.python-key }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -735,11 +694,12 @@ jobs:
--test-group-count 4 \
--test-group=${{ matrix.group }} \
--cov homeassistant \
+ --cov-report= \
-o console_output_style=count \
-p no:sugar \
tests
- name: Upload coverage artifact
- uses: actions/upload-artifact@v2.2.2
+ uses: actions/upload-artifact@v2.2.3
with:
name: coverage-${{ matrix.python-version }}-group${{ matrix.group }}
path: .coverage
@@ -750,7 +710,7 @@ jobs:
coverage:
name: Process test coverage
runs-on: ubuntu-latest
- needs: pytest
+ needs: ["prepare-tests", "pytest"]
strategy:
matrix:
python-version: [3.8]
@@ -760,14 +720,11 @@ jobs:
uses: actions/checkout@v2
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.4
with:
path: venv
- key: >-
- ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{
- matrix.python-version }}-${{ hashFiles('requirements_test.txt')
- }}-${{ hashFiles('requirements_all.txt') }}-${{
- hashFiles('homeassistant/package_constraints.txt') }}
+ key: ${{ runner.os }}-${{ matrix.python-version }}-${{
+ needs.prepare-tests.outputs.python-key }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
@@ -782,4 +739,4 @@ jobs:
coverage report --fail-under=94
coverage xml
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@v1.2.1
+ uses: codecov/codecov-action@v1.3.2
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 6daeccc4aca6c3..3ff0f47cedc147 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -16,7 +16,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@v3.0.15
+ uses: actions/stale@v3.0.18
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90
@@ -53,7 +53,7 @@ jobs:
# - No PRs marked as no-stale or new-integrations
# - No issues (-1)
- name: 30 days stale PRs policy
- uses: actions/stale@v3.0.15
+ uses: actions/stale@v3.0.18
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 30
@@ -78,7 +78,7 @@ jobs:
# - No Issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: Needs more information stale issues policy
- uses: actions/stale@v3.0.15
+ uses: actions/stale@v3.0.18
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "needs-more-information"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8944a69d9ed195..2f4aea74ae9c3c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/asottile/pyupgrade
- rev: v2.7.2
+ rev: v2.11.0
hooks:
- id: pyupgrade
args: [--py38-plus]
@@ -23,12 +23,16 @@ repos:
exclude_types: [csv, json]
exclude: ^tests/fixtures/
- repo: https://gitlab.com/pycqa/flake8
- rev: 3.8.4
+ rev: 3.9.0
hooks:
- id: flake8
additional_dependencies:
- - flake8-docstrings==1.5.0
- - pydocstyle==5.1.1
+ - pycodestyle==2.7.0
+ - pyflakes==2.3.1
+ - flake8-docstrings==1.6.0
+ - pydocstyle==6.0.0
+ - flake8-comprehensions==3.4.0
+ - flake8-noqa==1.1.0
files: ^(homeassistant|script|tests)/.+\.py$
- repo: https://github.com/PyCQA/bandit
rev: 1.7.0
@@ -40,7 +44,7 @@ repos:
- --configfile=tests/bandit.yaml
files: ^(homeassistant|script|tests)/.+\.py$
- repo: https://github.com/PyCQA/isort
- rev: 5.5.3
+ rev: 5.7.0
hooks:
- id: isort
- repo: https://github.com/pre-commit/pre-commit-hooks
@@ -59,11 +63,24 @@ repos:
rev: v1.24.2
hooks:
- id: yamllint
- - repo: https://github.com/prettier/prettier
- rev: 2.0.4
+ - repo: https://github.com/pre-commit/mirrors-prettier
+ rev: v2.2.1
hooks:
- id: prettier
stages: [manual]
+ - repo: https://github.com/cdce8p/python-typing-update
+ rev: v0.3.2
+ hooks:
+ # Run `python-typing-update` hook manually from time to time
+ # to update python typing syntax.
+ # Will require manual work, before submitting changes!
+ - id: python-typing-update
+ stages: [manual]
+ args:
+ - --py38-plus
+ - --force
+ - --keep-updates
+ files: ^(homeassistant|tests|script)/.+\.py$
- repo: local
hooks:
# Run mypy through our wrapper script in order to get the possible
@@ -90,4 +107,4 @@ repos:
pass_filenames: false
language: script
types: [text]
- files: ^(homeassistant/.+/(manifest|strings)\.json|\.coveragerc)$
+ files: ^(homeassistant/.+/(manifest|strings)\.json|\.coveragerc|homeassistant/.+/services\.yaml)$
diff --git a/CODEOWNERS b/CODEOWNERS
index 73d42f4efcffeb..5f2fd6588a6272 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -24,6 +24,7 @@ homeassistant/components/accuweather/* @bieniu
homeassistant/components/acmeda/* @atmurray
homeassistant/components/adguard/* @frenck
homeassistant/components/advantage_air/* @Bre77
+homeassistant/components/aemet/* @noltari
homeassistant/components/agent_dvr/* @ispysoftware
homeassistant/components/airly/* @bieniu
homeassistant/components/airnow/* @asymworks
@@ -35,6 +36,7 @@ homeassistant/components/alpha_vantage/* @fabaff
homeassistant/components/ambiclimate/* @danielhiversen
homeassistant/components/ambient_station/* @bachya
homeassistant/components/amcrest/* @pnbruckner
+homeassistant/components/analytics/* @home-assistant/core @ludeeus
homeassistant/components/androidtv/* @JeffLIrion
homeassistant/components/apache_kafka/* @bachya
homeassistant/components/api/* @home-assistant/core
@@ -45,7 +47,7 @@ homeassistant/components/arcam_fmj/* @elupus
homeassistant/components/arduino/* @fabaff
homeassistant/components/arest/* @fabaff
homeassistant/components/arris_tg2492lg/* @vanbalken
-homeassistant/components/asuswrt/* @kennedyshead
+homeassistant/components/asuswrt/* @kennedyshead @ollo69
homeassistant/components/atag/* @MatsNL
homeassistant/components/aten_pe/* @mtdcr
homeassistant/components/atome/* @baqs
@@ -82,10 +84,12 @@ homeassistant/components/circuit/* @braam
homeassistant/components/cisco_ios/* @fbradyirl
homeassistant/components/cisco_mobility_express/* @fbradyirl
homeassistant/components/cisco_webex_teams/* @fbradyirl
+homeassistant/components/climacell/* @raman325
homeassistant/components/cloud/* @home-assistant/cloud
homeassistant/components/cloudflare/* @ludeeus @ctalkington
homeassistant/components/color_extractor/* @GenericStudent
homeassistant/components/comfoconnect/* @michaelarnauts
+homeassistant/components/compensation/* @Petro31
homeassistant/components/config/* @home-assistant/core
homeassistant/components/configurator/* @home-assistant/core
homeassistant/components/control4/* @lawtancool
@@ -130,6 +134,7 @@ homeassistant/components/elkm1/* @gwww @bdraco
homeassistant/components/elv/* @majuss
homeassistant/components/emby/* @mezz64
homeassistant/components/emoncms/* @borpin
+homeassistant/components/emonitor/* @bdraco
homeassistant/components/emulated_kasa/* @kbickar
homeassistant/components/enigma2/* @fbradyirl
homeassistant/components/enocean/* @bdurrer
@@ -143,7 +148,8 @@ homeassistant/components/eq3btsmart/* @rytilahti
homeassistant/components/esphome/* @OttoWinter
homeassistant/components/essent/* @TheLastProject
homeassistant/components/evohome/* @zxdavb
-homeassistant/components/ezviz/* @baqs
+homeassistant/components/ezviz/* @RenierM26 @baqs
+homeassistant/components/faa_delays/* @ntilley905
homeassistant/components/fastdotcom/* @rohankapoorcom
homeassistant/components/file/* @fabaff
homeassistant/components/filter/* @dgomes
@@ -158,7 +164,7 @@ homeassistant/components/flunearyou/* @bachya
homeassistant/components/forked_daapd/* @uvjustin
homeassistant/components/fortios/* @kimfrellsen
homeassistant/components/foscam/* @skgsergio
-homeassistant/components/freebox/* @snoof85 @Quentame
+homeassistant/components/freebox/* @hacf-fr @Quentame
homeassistant/components/fronius/* @nielstron
homeassistant/components/frontend/* @home-assistant/frontend
homeassistant/components/garmin_connect/* @cyberjunky
@@ -177,13 +183,12 @@ homeassistant/components/google_cloud/* @lufton
homeassistant/components/gpsd/* @fabaff
homeassistant/components/gree/* @cmroche
homeassistant/components/greeneye_monitor/* @jkeljo
-homeassistant/components/griddy/* @bdraco
homeassistant/components/group/* @home-assistant/core
homeassistant/components/growatt_server/* @indykoning
homeassistant/components/guardian/* @bachya
+homeassistant/components/habitica/* @ASMfreaK @leikoilja
homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey
homeassistant/components/hassio/* @home-assistant/supervisor
-homeassistant/components/hdmi_cec/* @newAM
homeassistant/components/heatmiser/* @andylockran
homeassistant/components/heos/* @andrewsayre
homeassistant/components/here_travel_time/* @eifinger
@@ -194,11 +199,11 @@ homeassistant/components/history/* @home-assistant/core
homeassistant/components/hive/* @Rendili @KJonline
homeassistant/components/hlk_sw16/* @jameshilliard
homeassistant/components/home_connect/* @DavidMStraub
+homeassistant/components/home_plus_control/* @chemaaa
homeassistant/components/homeassistant/* @home-assistant/core
homeassistant/components/homekit/* @bdraco
homeassistant/components/homekit_controller/* @Jc2k
homeassistant/components/homematic/* @pvizeli @danielperna84
-homeassistant/components/homematicip_cloud/* @SukramJ
homeassistant/components/http/* @home-assistant/core
homeassistant/components/huawei_lte/* @scop @fphammerle
homeassistant/components/huawei_router/* @abmantis
@@ -211,7 +216,7 @@ homeassistant/components/hydrawise/* @ptcryan
homeassistant/components/hyperion/* @dermotduffy
homeassistant/components/iammeter/* @lewei50
homeassistant/components/iaqualink/* @flz
-homeassistant/components/icloud/* @Quentame
+homeassistant/components/icloud/* @Quentame @nzapponi
homeassistant/components/ign_sismologia/* @exxamalte
homeassistant/components/image/* @home-assistant/core
homeassistant/components/incomfort/* @zxdavb
@@ -241,15 +246,19 @@ homeassistant/components/keba/* @dannerph
homeassistant/components/keenetic_ndms2/* @foxel
homeassistant/components/kef/* @basnijholt
homeassistant/components/keyboard_remote/* @bendavid
+homeassistant/components/kmtronic/* @dgomes
homeassistant/components/knx/* @Julius2342 @farmio @marvin-w
homeassistant/components/kodi/* @OnFreund @cgtobi
homeassistant/components/konnected/* @heythisisnate @kit-klein
+homeassistant/components/kostal_plenticore/* @stegm
homeassistant/components/kulersky/* @emlove
homeassistant/components/lametric/* @robbiet480
homeassistant/components/launch_library/* @ludeeus
homeassistant/components/lcn/* @alengwenus
homeassistant/components/life360/* @pnbruckner
homeassistant/components/linux_battery/* @fabaff
+homeassistant/components/litejet/* @joncar
+homeassistant/components/litterrobot/* @natekspencer
homeassistant/components/local_ip/* @issacg
homeassistant/components/logger/* @home-assistant/core
homeassistant/components/logi_circle/* @evanjd
@@ -260,14 +269,17 @@ homeassistant/components/luftdaten/* @fabaff
homeassistant/components/lupusec/* @majuss
homeassistant/components/lutron/* @JonGilmore
homeassistant/components/lutron_caseta/* @swails @bdraco
+homeassistant/components/lyric/* @timmo001
homeassistant/components/mastodon/* @fabaff
homeassistant/components/matrix/* @tinloaf
+homeassistant/components/mazda/* @bdr99
homeassistant/components/mcp23017/* @jardiamj
homeassistant/components/media_source/* @hunterjm
homeassistant/components/mediaroom/* @dgomes
homeassistant/components/melcloud/* @vilppuvuorinen
homeassistant/components/melissa/* @kennedyshead
homeassistant/components/met/* @danielhiversen @thimic
+homeassistant/components/met_eireann/* @DylanGore
homeassistant/components/meteo_france/* @hacf-fr @oncleben31 @Quentame
homeassistant/components/meteoalarm/* @rolfberkenbosch
homeassistant/components/metoffice/* @MrHarcombe
@@ -283,10 +295,12 @@ homeassistant/components/monoprice/* @etsinko @OnFreund
homeassistant/components/moon/* @fabaff
homeassistant/components/motion_blinds/* @starkillerOG
homeassistant/components/mpd/* @fabaff
-homeassistant/components/mqtt/* @home-assistant/core @emontnemery
+homeassistant/components/mqtt/* @emontnemery
homeassistant/components/msteams/* @peroyvind
+homeassistant/components/mullvad/* @meichthys
+homeassistant/components/my/* @home-assistant/core
homeassistant/components/myq/* @bdraco
-homeassistant/components/mysensors/* @MartinHjelmare
+homeassistant/components/mysensors/* @MartinHjelmare @functionpointer
homeassistant/components/mystrom/* @fabaff
homeassistant/components/neato/* @dshokouhi @Santobert
homeassistant/components/nederlandse_spoorwegen/* @YarmoM
@@ -374,9 +388,11 @@ homeassistant/components/random/* @fabaff
homeassistant/components/recollect_waste/* @bachya
homeassistant/components/rejseplanen/* @DarkFox
homeassistant/components/repetier/* @MTrab
+homeassistant/components/rflink/* @javicalle
homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221
homeassistant/components/ring/* @balloob
homeassistant/components/risco/* @OnFreund
+homeassistant/components/rituals_perfume_genie/* @milanmeu
homeassistant/components/rmvtransport/* @cgtobi
homeassistant/components/roku/* @ctalkington
homeassistant/components/roomba/* @pschmitt @cyr-ius @shenxn
@@ -390,6 +406,7 @@ homeassistant/components/samsungtv/* @escoand
homeassistant/components/scene/* @home-assistant/core
homeassistant/components/schluter/* @prairieapps
homeassistant/components/scrape/* @fabaff
+homeassistant/components/screenlogic/* @dieselrabbit
homeassistant/components/script/* @home-assistant/core
homeassistant/components/search/* @home-assistant/core
homeassistant/components/sense/* @kbickar
@@ -416,15 +433,16 @@ homeassistant/components/smappee/* @bsmappee
homeassistant/components/smart_meter_texas/* @grahamwetzler
homeassistant/components/smarthab/* @outadoc
homeassistant/components/smartthings/* @andrewsayre
+homeassistant/components/smarttub/* @mdz
homeassistant/components/smarty/* @z0mbieprocess
homeassistant/components/sms/* @ocalvo
homeassistant/components/smtp/* @fabaff
+homeassistant/components/solaredge/* @frenck
homeassistant/components/solaredge_local/* @drobtravels @scheric
homeassistant/components/solarlog/* @Ernst79
homeassistant/components/solax/* @squishykid
homeassistant/components/soma/* @ratsept
homeassistant/components/somfy/* @tetienne
-homeassistant/components/somfy_mylink/* @bdraco
homeassistant/components/sonarr/* @ctalkington
homeassistant/components/songpal/* @rytilahti @shenxn
homeassistant/components/sonos/* @cgtobi
@@ -440,8 +458,9 @@ homeassistant/components/starline/* @anonym-tsk
homeassistant/components/statistics/* @fabaff
homeassistant/components/stiebel_eltron/* @fucm
homeassistant/components/stookalert/* @fwestenberg
-homeassistant/components/stream/* @hunterjm @uvjustin
+homeassistant/components/stream/* @hunterjm @uvjustin @allenporter
homeassistant/components/stt/* @pvizeli
+homeassistant/components/subaru/* @G-Two
homeassistant/components/suez_water/* @ooii
homeassistant/components/sun/* @Swamp-Ig
homeassistant/components/supla/* @mwegrzynek
@@ -455,7 +474,7 @@ homeassistant/components/syncthru/* @nielstron
homeassistant/components/synology_dsm/* @hacf-fr @Quentame @mib1185
homeassistant/components/synology_srm/* @aerialls
homeassistant/components/syslog/* @fabaff
-homeassistant/components/tado/* @michaelarnauts @bdraco
+homeassistant/components/tado/* @michaelarnauts @bdraco @noltari
homeassistant/components/tag/* @balloob @dmulcahey
homeassistant/components/tahoma/* @philklei
homeassistant/components/tankerkoenig/* @guillempages
@@ -477,7 +496,7 @@ homeassistant/components/toon/* @frenck
homeassistant/components/totalconnect/* @austinmroczek
homeassistant/components/tplink/* @rytilahti @thegardenmonkey
homeassistant/components/traccar/* @ludeeus
-homeassistant/components/tradfri/* @ggravlingen
+homeassistant/components/trace/* @home-assistant/core
homeassistant/components/trafikverket_train/* @endor-force
homeassistant/components/trafikverket_weatherstation/* @endor-force
homeassistant/components/transmission/* @engrbm87 @JPHutchins
@@ -485,6 +504,7 @@ homeassistant/components/tts/* @pvizeli
homeassistant/components/tuya/* @ollo69
homeassistant/components/twentemilieu/* @frenck
homeassistant/components/twinkly/* @dr1rrb
+homeassistant/components/ubus/* @noltari
homeassistant/components/unifi/* @Kane610
homeassistant/components/unifiled/* @florisvdk
homeassistant/components/upb/* @gwww
@@ -497,7 +517,7 @@ homeassistant/components/usgs_earthquakes_feed/* @exxamalte
homeassistant/components/utility_meter/* @dgomes
homeassistant/components/velbus/* @Cereal2nd @brefra
homeassistant/components/velux/* @Julius2342
-homeassistant/components/vera/* @vangorra
+homeassistant/components/vera/* @pavoni
homeassistant/components/verisure/* @frenck
homeassistant/components/versasense/* @flamm3blemuff1n
homeassistant/components/version/* @fabaff @ludeeus
@@ -506,14 +526,16 @@ homeassistant/components/vicare/* @oischinger
homeassistant/components/vilfo/* @ManneW
homeassistant/components/vivotek/* @HarlemSquirrel
homeassistant/components/vizio/* @raman325
-homeassistant/components/vlc_telnet/* @rodripf
+homeassistant/components/vlc_telnet/* @rodripf @dmcc
homeassistant/components/volkszaehler/* @fabaff
homeassistant/components/volumio/* @OnFreund
+homeassistant/components/wake_on_lan/* @ntilley905
homeassistant/components/waqi/* @andrey-git
homeassistant/components/watson_tts/* @rutkai
homeassistant/components/weather/* @fabaff
homeassistant/components/webostv/* @bendavid
homeassistant/components/websocket_api/* @home-assistant/core
+homeassistant/components/wemo/* @esev
homeassistant/components/wiffi/* @mampfes
homeassistant/components/wilight/* @leofig-rj
homeassistant/components/withings/* @vangorra
@@ -523,7 +545,6 @@ homeassistant/components/workday/* @fabaff
homeassistant/components/worldclock/* @fabaff
homeassistant/components/xbox/* @hunterjm
homeassistant/components/xbox_live/* @MartinHjelmare
-homeassistant/components/xfinity/* @cisasteelersfan
homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
homeassistant/components/xiaomi_miio/* @rytilahti @syssi @starkillerOG
homeassistant/components/xiaomi_tv/* @simse
diff --git a/Dockerfile.dev b/Dockerfile.dev
index d72ebcaed016e3..68188f16f012ae 100644
--- a/Dockerfile.dev
+++ b/Dockerfile.dev
@@ -1,7 +1,11 @@
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.8
+SHELL ["/bin/bash", "-o", "pipefail", "-c"]
+
RUN \
- apt-get update && apt-get install -y --no-install-recommends \
+ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
+ && apt-get update \
+ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
libudev-dev \
libavformat-dev \
libavcodec-dev \
@@ -10,6 +14,7 @@ RUN \
libswscale-dev \
libswresample-dev \
libavfilter-dev \
+ libpcap-dev \
git \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
@@ -23,10 +28,11 @@ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
WORKDIR /workspaces
# Install Python dependencies from requirements
-COPY requirements_test.txt requirements_test_pre_commit.txt ./
+COPY requirements.txt requirements_test.txt requirements_test_pre_commit.txt ./
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
-RUN pip3 install -r requirements_test.txt \
- && rm -rf requirements_test.txt requirements_test_pre_commit.txt homeassistant/
+RUN pip3 install -r requirements.txt \
+ && pip3 install -r requirements_test.txt \
+ && rm -rf requirements.txt requirements_test.txt requirements_test_pre_commit.txt homeassistant/
# Set the default shell to bash instead of sh
ENV SHELL /bin/bash
diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml
index 418fdf5b26c5bf..74aa05e58f33a7 100644
--- a/azure-pipelines-release.yml
+++ b/azure-pipelines-release.yml
@@ -14,7 +14,7 @@ schedules:
always: true
variables:
- name: versionBuilder
- value: '2020.11.0'
+ value: '2021.02.0'
- group: docker
- group: github
- group: twine
@@ -114,10 +114,12 @@ stages:
pool:
vmImage: 'ubuntu-latest'
strategy:
- maxParallel: 15
+ maxParallel: 17
matrix:
qemux86-64:
buildMachine: 'qemux86-64'
+ generic-x86-64:
+ buildMachine: 'generic-x86-64'
intel-nuc:
buildMachine: 'intel-nuc'
qemux86:
diff --git a/build.json b/build.json
index 1cf4217146dbfa..0183b61c67c35b 100644
--- a/build.json
+++ b/build.json
@@ -1,11 +1,11 @@
{
"image": "homeassistant/{arch}-homeassistant",
"build_from": {
- "aarch64": "homeassistant/aarch64-homeassistant-base:2021.01.1",
- "armhf": "homeassistant/armhf-homeassistant-base:2021.01.1",
- "armv7": "homeassistant/armv7-homeassistant-base:2021.01.1",
- "amd64": "homeassistant/amd64-homeassistant-base:2021.01.1",
- "i386": "homeassistant/i386-homeassistant-base:2021.01.1"
+ "aarch64": "homeassistant/aarch64-homeassistant-base:2021.02.0",
+ "armhf": "homeassistant/armhf-homeassistant-base:2021.02.0",
+ "armv7": "homeassistant/armv7-homeassistant-base:2021.02.0",
+ "amd64": "homeassistant/amd64-homeassistant-base:2021.02.0",
+ "i386": "homeassistant/i386-homeassistant-base:2021.02.0"
},
"labels": {
"io.hass.type": "core"
diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py
index 840e0bed24d5f1..d8256e2ef92aa0 100644
--- a/homeassistant/__main__.py
+++ b/homeassistant/__main__.py
@@ -1,11 +1,12 @@
"""Start Home Assistant."""
+from __future__ import annotations
+
import argparse
import os
import platform
import subprocess
import sys
import threading
-from typing import List
from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
@@ -206,7 +207,7 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
pass
-def cmdline() -> List[str]:
+def cmdline() -> list[str]:
"""Collect path and arguments to re-execute the current hass instance."""
if os.path.basename(sys.argv[0]) == "__main__.py":
modulepath = os.path.dirname(sys.argv[0])
diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py
index 531e36ff0b309b..3830419c537545 100644
--- a/homeassistant/auth/__init__.py
+++ b/homeassistant/auth/__init__.py
@@ -1,8 +1,10 @@
"""Provide an authentication layer for Home Assistant."""
+from __future__ import annotations
+
import asyncio
from collections import OrderedDict
from datetime import timedelta
-from typing import Any, Dict, List, Optional, Tuple, cast
+from typing import Any, Dict, Optional, Tuple, cast
import jwt
@@ -34,9 +36,9 @@ class InvalidProvider(Exception):
async def auth_manager_from_config(
hass: HomeAssistant,
- provider_configs: List[Dict[str, Any]],
- module_configs: List[Dict[str, Any]],
-) -> "AuthManager":
+ provider_configs: list[dict[str, Any]],
+ module_configs: list[dict[str, Any]],
+) -> AuthManager:
"""Initialize an auth manager from config.
CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or
@@ -76,7 +78,7 @@ async def auth_manager_from_config(
class AuthManagerFlowManager(data_entry_flow.FlowManager):
"""Manage authentication flows."""
- def __init__(self, hass: HomeAssistant, auth_manager: "AuthManager"):
+ def __init__(self, hass: HomeAssistant, auth_manager: AuthManager):
"""Init auth manager flows."""
super().__init__(hass)
self.auth_manager = auth_manager
@@ -85,8 +87,8 @@ async def async_create_flow(
self,
handler_key: Any,
*,
- context: Optional[Dict[str, Any]] = None,
- data: Optional[Dict[str, Any]] = None,
+ context: dict[str, Any] | None = None,
+ data: dict[str, Any] | None = None,
) -> data_entry_flow.FlowHandler:
"""Create a login flow."""
auth_provider = self.auth_manager.get_auth_provider(*handler_key)
@@ -95,8 +97,8 @@ async def async_create_flow(
return await auth_provider.async_login_flow(context)
async def async_finish_flow(
- self, flow: data_entry_flow.FlowHandler, result: Dict[str, Any]
- ) -> Dict[str, Any]:
+ self, flow: data_entry_flow.FlowHandler, result: dict[str, Any]
+ ) -> dict[str, Any]:
"""Return a user as result of login flow."""
flow = cast(LoginFlow, flow)
@@ -155,22 +157,22 @@ def __init__(
self.login_flow = AuthManagerFlowManager(hass, self)
@property
- def auth_providers(self) -> List[AuthProvider]:
+ def auth_providers(self) -> list[AuthProvider]:
"""Return a list of available auth providers."""
return list(self._providers.values())
@property
- def auth_mfa_modules(self) -> List[MultiFactorAuthModule]:
+ def auth_mfa_modules(self) -> list[MultiFactorAuthModule]:
"""Return a list of available auth modules."""
return list(self._mfa_modules.values())
def get_auth_provider(
- self, provider_type: str, provider_id: Optional[str]
- ) -> Optional[AuthProvider]:
+ self, provider_type: str, provider_id: str | None
+ ) -> AuthProvider | None:
"""Return an auth provider, None if not found."""
return self._providers.get((provider_type, provider_id))
- def get_auth_providers(self, provider_type: str) -> List[AuthProvider]:
+ def get_auth_providers(self, provider_type: str) -> list[AuthProvider]:
"""Return a List of auth provider of one type, Empty if not found."""
return [
provider
@@ -178,30 +180,30 @@ def get_auth_providers(self, provider_type: str) -> List[AuthProvider]:
if p_type == provider_type
]
- def get_auth_mfa_module(self, module_id: str) -> Optional[MultiFactorAuthModule]:
+ def get_auth_mfa_module(self, module_id: str) -> MultiFactorAuthModule | None:
"""Return a multi-factor auth module, None if not found."""
return self._mfa_modules.get(module_id)
- async def async_get_users(self) -> List[models.User]:
+ async def async_get_users(self) -> list[models.User]:
"""Retrieve all users."""
return await self._store.async_get_users()
- async def async_get_user(self, user_id: str) -> Optional[models.User]:
+ async def async_get_user(self, user_id: str) -> models.User | None:
"""Retrieve a user."""
return await self._store.async_get_user(user_id)
- async def async_get_owner(self) -> Optional[models.User]:
+ async def async_get_owner(self) -> models.User | None:
"""Retrieve the owner."""
users = await self.async_get_users()
return next((user for user in users if user.is_owner), None)
- async def async_get_group(self, group_id: str) -> Optional[models.Group]:
+ async def async_get_group(self, group_id: str) -> models.Group | None:
"""Retrieve all groups."""
return await self._store.async_get_group(group_id)
async def async_get_user_by_credentials(
self, credentials: models.Credentials
- ) -> Optional[models.User]:
+ ) -> models.User | None:
"""Get a user by credential, return None if not found."""
for user in await self.async_get_users():
for creds in user.credentials:
@@ -211,7 +213,7 @@ async def async_get_user_by_credentials(
return None
async def async_create_system_user(
- self, name: str, group_ids: Optional[List[str]] = None
+ self, name: str, group_ids: list[str] | None = None
) -> models.User:
"""Create a system user."""
user = await self._store.async_create_user(
@@ -223,10 +225,10 @@ async def async_create_system_user(
return user
async def async_create_user(
- self, name: str, group_ids: Optional[List[str]] = None
+ self, name: str, group_ids: list[str] | None = None
) -> models.User:
"""Create a user."""
- kwargs: Dict[str, Any] = {
+ kwargs: dict[str, Any] = {
"name": name,
"is_active": True,
"group_ids": group_ids or [],
@@ -292,12 +294,12 @@ async def async_remove_user(self, user: models.User) -> None:
async def async_update_user(
self,
user: models.User,
- name: Optional[str] = None,
- is_active: Optional[bool] = None,
- group_ids: Optional[List[str]] = None,
+ name: str | None = None,
+ is_active: bool | None = None,
+ group_ids: list[str] | None = None,
) -> None:
"""Update a user."""
- kwargs: Dict[str, Any] = {}
+ kwargs: dict[str, Any] = {}
if name is not None:
kwargs["name"] = name
if group_ids is not None:
@@ -360,9 +362,9 @@ async def async_disable_user_mfa(
await module.async_depose_user(user.id)
- async def async_get_enabled_mfa(self, user: models.User) -> Dict[str, str]:
+ async def async_get_enabled_mfa(self, user: models.User) -> dict[str, str]:
"""List enabled mfa modules for user."""
- modules: Dict[str, str] = OrderedDict()
+ modules: dict[str, str] = OrderedDict()
for module_id, module in self._mfa_modules.items():
if await module.async_is_user_setup(user.id):
modules[module_id] = module.name
@@ -371,12 +373,12 @@ async def async_get_enabled_mfa(self, user: models.User) -> Dict[str, str]:
async def async_create_refresh_token(
self,
user: models.User,
- client_id: Optional[str] = None,
- client_name: Optional[str] = None,
- client_icon: Optional[str] = None,
- token_type: Optional[str] = None,
+ client_id: str | None = None,
+ client_name: str | None = None,
+ client_icon: str | None = None,
+ token_type: str | None = None,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
- credential: Optional[models.Credentials] = None,
+ credential: models.Credentials | None = None,
) -> models.RefreshToken:
"""Create a new refresh token for a user."""
if not user.is_active:
@@ -430,13 +432,13 @@ async def async_create_refresh_token(
async def async_get_refresh_token(
self, token_id: str
- ) -> Optional[models.RefreshToken]:
+ ) -> models.RefreshToken | None:
"""Get refresh token by id."""
return await self._store.async_get_refresh_token(token_id)
async def async_get_refresh_token_by_token(
self, token: str
- ) -> Optional[models.RefreshToken]:
+ ) -> models.RefreshToken | None:
"""Get refresh token by token."""
return await self._store.async_get_refresh_token_by_token(token)
@@ -448,7 +450,7 @@ async def async_remove_refresh_token(
@callback
def async_create_access_token(
- self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
+ self, refresh_token: models.RefreshToken, remote_ip: str | None = None
) -> str:
"""Create a new access token."""
self.async_validate_refresh_token(refresh_token, remote_ip)
@@ -469,7 +471,7 @@ def async_create_access_token(
@callback
def _async_resolve_provider(
self, refresh_token: models.RefreshToken
- ) -> Optional[AuthProvider]:
+ ) -> AuthProvider | None:
"""Get the auth provider for the given refresh token.
Raises an exception if the expected provider is no longer available or return
@@ -490,7 +492,7 @@ def _async_resolve_provider(
@callback
def async_validate_refresh_token(
- self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
+ self, refresh_token: models.RefreshToken, remote_ip: str | None = None
) -> None:
"""Validate that a refresh token is usable.
@@ -502,7 +504,7 @@ def async_validate_refresh_token(
async def async_validate_access_token(
self, token: str
- ) -> Optional[models.RefreshToken]:
+ ) -> models.RefreshToken | None:
"""Return refresh token if an access token is valid."""
try:
unverif_claims = jwt.decode(token, verify=False)
@@ -533,7 +535,7 @@ async def async_validate_access_token(
@callback
def _async_get_auth_provider(
self, credentials: models.Credentials
- ) -> Optional[AuthProvider]:
+ ) -> AuthProvider | None:
"""Get auth provider from a set of credentials."""
auth_provider_key = (
credentials.auth_provider_type,
diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py
index 724f1c86722572..0b360668ad4504 100644
--- a/homeassistant/auth/auth_store.py
+++ b/homeassistant/auth/auth_store.py
@@ -1,10 +1,12 @@
"""Storage for auth models."""
+from __future__ import annotations
+
import asyncio
from collections import OrderedDict
from datetime import timedelta
import hmac
from logging import getLogger
-from typing import Any, Dict, List, Optional
+from typing import Any
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
from homeassistant.core import HomeAssistant, callback
@@ -34,15 +36,15 @@ class AuthStore:
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the auth store."""
self.hass = hass
- self._users: Optional[Dict[str, models.User]] = None
- self._groups: Optional[Dict[str, models.Group]] = None
- self._perm_lookup: Optional[PermissionLookup] = None
+ self._users: dict[str, models.User] | None = None
+ self._groups: dict[str, models.Group] | None = None
+ self._perm_lookup: PermissionLookup | None = None
self._store = hass.helpers.storage.Store(
STORAGE_VERSION, STORAGE_KEY, private=True
)
self._lock = asyncio.Lock()
- async def async_get_groups(self) -> List[models.Group]:
+ async def async_get_groups(self) -> list[models.Group]:
"""Retrieve all users."""
if self._groups is None:
await self._async_load()
@@ -50,7 +52,7 @@ async def async_get_groups(self) -> List[models.Group]:
return list(self._groups.values())
- async def async_get_group(self, group_id: str) -> Optional[models.Group]:
+ async def async_get_group(self, group_id: str) -> models.Group | None:
"""Retrieve all users."""
if self._groups is None:
await self._async_load()
@@ -58,7 +60,7 @@ async def async_get_group(self, group_id: str) -> Optional[models.Group]:
return self._groups.get(group_id)
- async def async_get_users(self) -> List[models.User]:
+ async def async_get_users(self) -> list[models.User]:
"""Retrieve all users."""
if self._users is None:
await self._async_load()
@@ -66,7 +68,7 @@ async def async_get_users(self) -> List[models.User]:
return list(self._users.values())
- async def async_get_user(self, user_id: str) -> Optional[models.User]:
+ async def async_get_user(self, user_id: str) -> models.User | None:
"""Retrieve a user by id."""
if self._users is None:
await self._async_load()
@@ -76,12 +78,12 @@ async def async_get_user(self, user_id: str) -> Optional[models.User]:
async def async_create_user(
self,
- name: Optional[str],
- is_owner: Optional[bool] = None,
- is_active: Optional[bool] = None,
- system_generated: Optional[bool] = None,
- credentials: Optional[models.Credentials] = None,
- group_ids: Optional[List[str]] = None,
+ name: str | None,
+ is_owner: bool | None = None,
+ is_active: bool | None = None,
+ system_generated: bool | None = None,
+ credentials: models.Credentials | None = None,
+ group_ids: list[str] | None = None,
) -> models.User:
"""Create a new user."""
if self._users is None:
@@ -97,7 +99,7 @@ async def async_create_user(
raise ValueError(f"Invalid group specified {group_id}")
groups.append(group)
- kwargs: Dict[str, Any] = {
+ kwargs: dict[str, Any] = {
"name": name,
# Until we get group management, we just put everyone in the
# same group.
@@ -146,9 +148,9 @@ async def async_remove_user(self, user: models.User) -> None:
async def async_update_user(
self,
user: models.User,
- name: Optional[str] = None,
- is_active: Optional[bool] = None,
- group_ids: Optional[List[str]] = None,
+ name: str | None = None,
+ is_active: bool | None = None,
+ group_ids: list[str] | None = None,
) -> None:
"""Update a user."""
assert self._groups is not None
@@ -203,15 +205,15 @@ async def async_remove_credentials(self, credentials: models.Credentials) -> Non
async def async_create_refresh_token(
self,
user: models.User,
- client_id: Optional[str] = None,
- client_name: Optional[str] = None,
- client_icon: Optional[str] = None,
+ client_id: str | None = None,
+ client_name: str | None = None,
+ client_icon: str | None = None,
token_type: str = models.TOKEN_TYPE_NORMAL,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
- credential: Optional[models.Credentials] = None,
+ credential: models.Credentials | None = None,
) -> models.RefreshToken:
"""Create a new token for a user."""
- kwargs: Dict[str, Any] = {
+ kwargs: dict[str, Any] = {
"user": user,
"client_id": client_id,
"token_type": token_type,
@@ -244,7 +246,7 @@ async def async_remove_refresh_token(
async def async_get_refresh_token(
self, token_id: str
- ) -> Optional[models.RefreshToken]:
+ ) -> models.RefreshToken | None:
"""Get refresh token by id."""
if self._users is None:
await self._async_load()
@@ -259,7 +261,7 @@ async def async_get_refresh_token(
async def async_get_refresh_token_by_token(
self, token: str
- ) -> Optional[models.RefreshToken]:
+ ) -> models.RefreshToken | None:
"""Get refresh token by token."""
if self._users is None:
await self._async_load()
@@ -276,7 +278,7 @@ async def async_get_refresh_token_by_token(
@callback
def async_log_refresh_token_usage(
- self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
+ self, refresh_token: models.RefreshToken, remote_ip: str | None = None
) -> None:
"""Update refresh token last used information."""
refresh_token.last_used_at = dt_util.utcnow()
@@ -309,9 +311,9 @@ async def _async_load_task(self) -> None:
self._set_defaults()
return
- users: Dict[str, models.User] = OrderedDict()
- groups: Dict[str, models.Group] = OrderedDict()
- credentials: Dict[str, models.Credentials] = OrderedDict()
+ users: dict[str, models.User] = OrderedDict()
+ groups: dict[str, models.Group] = OrderedDict()
+ credentials: dict[str, models.Credentials] = OrderedDict()
# Soft-migrating data as we load. We are going to make sure we have a
# read only group and an admin group. There are two states that we can
@@ -328,7 +330,7 @@ async def _async_load_task(self) -> None:
# was added.
for group_dict in data.get("groups", []):
- policy: Optional[PolicyType] = None
+ policy: PolicyType | None = None
if group_dict["id"] == GROUP_ID_ADMIN:
has_admin_group = True
@@ -489,7 +491,7 @@ def _async_schedule_save(self) -> None:
self._store.async_delay_save(self._data_to_save, 1)
@callback
- def _data_to_save(self) -> Dict:
+ def _data_to_save(self) -> dict:
"""Return the data to store."""
assert self._users is not None
assert self._groups is not None
@@ -508,7 +510,7 @@ def _data_to_save(self) -> Dict:
groups = []
for group in self._groups.values():
- g_dict: Dict[str, Any] = {
+ g_dict: dict[str, Any] = {
"id": group.id,
# Name not read for sys groups. Kept here for backwards compat
"name": group.name,
@@ -567,7 +569,7 @@ def _set_defaults(self) -> None:
"""Set default values for auth store."""
self._users = OrderedDict()
- groups: Dict[str, models.Group] = OrderedDict()
+ groups: dict[str, models.Group] = OrderedDict()
admin_group = _system_admin_group()
groups[admin_group.id] = admin_group
user_group = _system_user_group()
diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py
index 6e4b189bf74338..d6989b6416fc9a 100644
--- a/homeassistant/auth/mfa_modules/__init__.py
+++ b/homeassistant/auth/mfa_modules/__init__.py
@@ -1,8 +1,10 @@
"""Pluggable auth modules for Home Assistant."""
+from __future__ import annotations
+
import importlib
import logging
import types
-from typing import Any, Dict, Optional
+from typing import Any
import voluptuous as vol
from voluptuous.humanize import humanize_error
@@ -36,7 +38,7 @@ class MultiFactorAuthModule:
DEFAULT_TITLE = "Unnamed auth module"
MAX_RETRY_TIME = 3
- def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
+ def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None:
"""Initialize an auth module."""
self.hass = hass
self.config = config
@@ -66,7 +68,7 @@ def input_schema(self) -> vol.Schema:
"""Return a voluptuous schema to define mfa auth module's input."""
raise NotImplementedError
- async def async_setup_flow(self, user_id: str) -> "SetupFlow":
+ async def async_setup_flow(self, user_id: str) -> SetupFlow:
"""Return a data entry flow handler for setup module.
Mfa module should extend SetupFlow
@@ -85,7 +87,7 @@ async def async_is_user_setup(self, user_id: str) -> bool:
"""Return whether user is setup."""
raise NotImplementedError
- async def async_validate(self, user_id: str, user_input: Dict[str, Any]) -> bool:
+ async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool:
"""Return True if validation passed."""
raise NotImplementedError
@@ -102,14 +104,14 @@ def __init__(
self._user_id = user_id
async def async_step_init(
- self, user_input: Optional[Dict[str, str]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, str] | None = None
+ ) -> dict[str, Any]:
"""Handle the first step of setup flow.
Return self.async_show_form(step_id='init') if user_input is None.
Return self.async_create_entry(data={'result': result}) if finish.
"""
- errors: Dict[str, str] = {}
+ errors: dict[str, str] = {}
if user_input:
result = await self._auth_module.async_setup_user(self._user_id, user_input)
@@ -123,7 +125,7 @@ async def async_step_init(
async def auth_mfa_module_from_config(
- hass: HomeAssistant, config: Dict[str, Any]
+ hass: HomeAssistant, config: dict[str, Any]
) -> MultiFactorAuthModule:
"""Initialize an auth module from a config."""
module_name = config[CONF_TYPE]
diff --git a/homeassistant/auth/mfa_modules/insecure_example.py b/homeassistant/auth/mfa_modules/insecure_example.py
index ddceeaae826740..1d40339417bd37 100644
--- a/homeassistant/auth/mfa_modules/insecure_example.py
+++ b/homeassistant/auth/mfa_modules/insecure_example.py
@@ -1,5 +1,7 @@
"""Example auth module."""
-from typing import Any, Dict
+from __future__ import annotations
+
+from typing import Any
import voluptuous as vol
@@ -28,7 +30,7 @@ class InsecureExampleModule(MultiFactorAuthModule):
DEFAULT_TITLE = "Insecure Personal Identify Number"
- def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
+ def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None:
"""Initialize the user data store."""
super().__init__(hass, config)
self._data = config["data"]
@@ -75,17 +77,11 @@ async def async_depose_user(self, user_id: str) -> None:
async def async_is_user_setup(self, user_id: str) -> bool:
"""Return whether user is setup."""
- for data in self._data:
- if data["user_id"] == user_id:
- return True
- return False
+ return any(data["user_id"] == user_id for data in self._data)
- async def async_validate(self, user_id: str, user_input: Dict[str, Any]) -> bool:
+ async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool:
"""Return True if validation passed."""
- for data in self._data:
- if data["user_id"] == user_id:
- # user_input has been validate in caller
- if data["pin"] == user_input["pin"]:
- return True
-
- return False
+ return any(
+ data["user_id"] == user_id and data["pin"] == user_input["pin"]
+ for data in self._data
+ )
diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py
index c4e5800821e5be..76a5676d562c87 100644
--- a/homeassistant/auth/mfa_modules/notify.py
+++ b/homeassistant/auth/mfa_modules/notify.py
@@ -2,10 +2,12 @@
Sending HOTP through notify service
"""
+from __future__ import annotations
+
import asyncio
from collections import OrderedDict
import logging
-from typing import Any, Dict, List, Optional
+from typing import Any, Dict
import attr
import voluptuous as vol
@@ -79,8 +81,8 @@ class NotifySetting:
secret: str = attr.ib(factory=_generate_secret) # not persistent
counter: int = attr.ib(factory=_generate_random) # not persistent
- notify_service: Optional[str] = attr.ib(default=None)
- target: Optional[str] = attr.ib(default=None)
+ notify_service: str | None = attr.ib(default=None)
+ target: str | None = attr.ib(default=None)
_UsersDict = Dict[str, NotifySetting]
@@ -92,10 +94,10 @@ class NotifyAuthModule(MultiFactorAuthModule):
DEFAULT_TITLE = "Notify One-Time Password"
- def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
+ def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None:
"""Initialize the user data store."""
super().__init__(hass, config)
- self._user_settings: Optional[_UsersDict] = None
+ self._user_settings: _UsersDict | None = None
self._user_store = hass.helpers.storage.Store(
STORAGE_VERSION, STORAGE_KEY, private=True
)
@@ -146,7 +148,7 @@ async def _async_save(self) -> None:
)
@callback
- def aync_get_available_notify_services(self) -> List[str]:
+ def aync_get_available_notify_services(self) -> list[str]:
"""Return list of notify services."""
unordered_services = set()
@@ -198,7 +200,7 @@ async def async_is_user_setup(self, user_id: str) -> bool:
return user_id in self._user_settings
- async def async_validate(self, user_id: str, user_input: Dict[str, Any]) -> bool:
+ async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool:
"""Return True if validation passed."""
if self._user_settings is None:
await self._async_load()
@@ -258,7 +260,7 @@ async def async_notify_user(self, user_id: str, code: str) -> None:
)
async def async_notify(
- self, code: str, notify_service: str, target: Optional[str] = None
+ self, code: str, notify_service: str, target: str | None = None
) -> None:
"""Send code by notify service."""
data = {"message": self._message_template.format(code)}
@@ -276,23 +278,23 @@ def __init__(
auth_module: NotifyAuthModule,
setup_schema: vol.Schema,
user_id: str,
- available_notify_services: List[str],
+ available_notify_services: list[str],
) -> None:
"""Initialize the setup flow."""
super().__init__(auth_module, setup_schema, user_id)
# to fix typing complaint
self._auth_module: NotifyAuthModule = auth_module
self._available_notify_services = available_notify_services
- self._secret: Optional[str] = None
- self._count: Optional[int] = None
- self._notify_service: Optional[str] = None
- self._target: Optional[str] = None
+ self._secret: str | None = None
+ self._count: int | None = None
+ self._notify_service: str | None = None
+ self._target: str | None = None
async def async_step_init(
- self, user_input: Optional[Dict[str, str]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, str] | None = None
+ ) -> dict[str, Any]:
"""Let user select available notify services."""
- errors: Dict[str, str] = {}
+ errors: dict[str, str] = {}
hass = self._auth_module.hass
if user_input:
@@ -306,7 +308,7 @@ async def async_step_init(
if not self._available_notify_services:
return self.async_abort(reason="no_available_service")
- schema: Dict[str, Any] = OrderedDict()
+ schema: dict[str, Any] = OrderedDict()
schema["notify_service"] = vol.In(self._available_notify_services)
schema["target"] = vol.Optional(str)
@@ -315,10 +317,10 @@ async def async_step_init(
)
async def async_step_setup(
- self, user_input: Optional[Dict[str, str]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, str] | None = None
+ ) -> dict[str, Any]:
"""Verify user can receive one-time password."""
- errors: Dict[str, str] = {}
+ errors: dict[str, str] = {}
hass = self._auth_module.hass
if user_input:
diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py
index 359f79ce6ce1b3..d20c84655463af 100644
--- a/homeassistant/auth/mfa_modules/totp.py
+++ b/homeassistant/auth/mfa_modules/totp.py
@@ -1,7 +1,9 @@
"""Time-based One Time Password auth module."""
+from __future__ import annotations
+
import asyncio
from io import BytesIO
-from typing import Any, Dict, Optional, Tuple
+from typing import Any
import voluptuous as vol
@@ -50,7 +52,7 @@ def _generate_qr_code(data: str) -> str:
)
-def _generate_secret_and_qr_code(username: str) -> Tuple[str, str, str]:
+def _generate_secret_and_qr_code(username: str) -> tuple[str, str, str]:
"""Generate a secret, url, and QR code."""
import pyotp # pylint: disable=import-outside-toplevel
@@ -69,10 +71,10 @@ class TotpAuthModule(MultiFactorAuthModule):
DEFAULT_TITLE = "Time-based One Time Password"
MAX_RETRY_TIME = 5
- def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
+ def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None:
"""Initialize the user data store."""
super().__init__(hass, config)
- self._users: Optional[Dict[str, str]] = None
+ self._users: dict[str, str] | None = None
self._user_store = hass.helpers.storage.Store(
STORAGE_VERSION, STORAGE_KEY, private=True
)
@@ -100,7 +102,7 @@ async def _async_save(self) -> None:
"""Save data."""
await self._user_store.async_save({STORAGE_USERS: self._users})
- def _add_ota_secret(self, user_id: str, secret: Optional[str] = None) -> str:
+ def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str:
"""Create a ota_secret for user."""
import pyotp # pylint: disable=import-outside-toplevel
@@ -145,7 +147,7 @@ async def async_is_user_setup(self, user_id: str) -> bool:
return user_id in self._users # type: ignore
- async def async_validate(self, user_id: str, user_input: Dict[str, Any]) -> bool:
+ async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool:
"""Return True if validation passed."""
if self._users is None:
await self._async_load()
@@ -181,13 +183,13 @@ def __init__(
# to fix typing complaint
self._auth_module: TotpAuthModule = auth_module
self._user = user
- self._ota_secret: Optional[str] = None
+ self._ota_secret: str | None = None
self._url = None # type Optional[str]
self._image = None # type Optional[str]
async def async_step_init(
- self, user_input: Optional[Dict[str, str]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, str] | None = None
+ ) -> dict[str, Any]:
"""Handle the first step of setup flow.
Return self.async_show_form(step_id='init') if user_input is None.
@@ -195,10 +197,10 @@ async def async_step_init(
"""
import pyotp # pylint: disable=import-outside-toplevel
- errors: Dict[str, str] = {}
+ errors: dict[str, str] = {}
if user_input:
- verified = await self.hass.async_add_executor_job( # type: ignore
+ verified = await self.hass.async_add_executor_job(
pyotp.TOTP(self._ota_secret).verify, user_input["code"]
)
if verified:
diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py
index 4cc67b2ebd4b71..758bbdb78e212d 100644
--- a/homeassistant/auth/models.py
+++ b/homeassistant/auth/models.py
@@ -1,7 +1,9 @@
"""Auth models."""
+from __future__ import annotations
+
from datetime import datetime, timedelta
import secrets
-from typing import Dict, List, NamedTuple, Optional
+from typing import NamedTuple
import uuid
import attr
@@ -21,7 +23,7 @@
class Group:
"""A group."""
- name: Optional[str] = attr.ib()
+ name: str | None = attr.ib()
policy: perm_mdl.PolicyType = attr.ib()
id: str = attr.ib(factory=lambda: uuid.uuid4().hex)
system_generated: bool = attr.ib(default=False)
@@ -31,24 +33,24 @@ class Group:
class User:
"""A user."""
- name: Optional[str] = attr.ib()
+ name: str | None = attr.ib()
perm_lookup: perm_mdl.PermissionLookup = attr.ib(eq=False, order=False)
id: str = attr.ib(factory=lambda: uuid.uuid4().hex)
is_owner: bool = attr.ib(default=False)
is_active: bool = attr.ib(default=False)
system_generated: bool = attr.ib(default=False)
- groups: List[Group] = attr.ib(factory=list, eq=False, order=False)
+ groups: list[Group] = attr.ib(factory=list, eq=False, order=False)
# List of credentials of a user.
- credentials: List["Credentials"] = attr.ib(factory=list, eq=False, order=False)
+ credentials: list[Credentials] = attr.ib(factory=list, eq=False, order=False)
# Tokens associated with a user.
- refresh_tokens: Dict[str, "RefreshToken"] = attr.ib(
+ refresh_tokens: dict[str, RefreshToken] = attr.ib(
factory=dict, eq=False, order=False
)
- _permissions: Optional[perm_mdl.PolicyPermissions] = attr.ib(
+ _permissions: perm_mdl.PolicyPermissions | None = attr.ib(
init=False,
eq=False,
order=False,
@@ -89,10 +91,10 @@ class RefreshToken:
"""RefreshToken for a user to grant new access tokens."""
user: User = attr.ib()
- client_id: Optional[str] = attr.ib()
+ client_id: str | None = attr.ib()
access_token_expiration: timedelta = attr.ib()
- client_name: Optional[str] = attr.ib(default=None)
- client_icon: Optional[str] = attr.ib(default=None)
+ client_name: str | None = attr.ib(default=None)
+ client_icon: str | None = attr.ib(default=None)
token_type: str = attr.ib(
default=TOKEN_TYPE_NORMAL,
validator=attr.validators.in_(
@@ -104,12 +106,12 @@ class RefreshToken:
token: str = attr.ib(factory=lambda: secrets.token_hex(64))
jwt_key: str = attr.ib(factory=lambda: secrets.token_hex(64))
- last_used_at: Optional[datetime] = attr.ib(default=None)
- last_used_ip: Optional[str] = attr.ib(default=None)
+ last_used_at: datetime | None = attr.ib(default=None)
+ last_used_ip: str | None = attr.ib(default=None)
- credential: Optional["Credentials"] = attr.ib(default=None)
+ credential: Credentials | None = attr.ib(default=None)
- version: Optional[str] = attr.ib(default=__version__)
+ version: str | None = attr.ib(default=__version__)
@attr.s(slots=True)
@@ -117,7 +119,7 @@ class Credentials:
"""Credentials for a user on an auth provider."""
auth_provider_type: str = attr.ib()
- auth_provider_id: Optional[str] = attr.ib()
+ auth_provider_id: str | None = attr.ib()
# Allow the auth provider to store data to represent their auth.
data: dict = attr.ib()
@@ -129,5 +131,5 @@ class Credentials:
class UserMeta(NamedTuple):
"""User metadata."""
- name: Optional[str]
+ name: str | None
is_active: bool
diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py
index 2f887d21b02543..28ff3f638d409c 100644
--- a/homeassistant/auth/permissions/__init__.py
+++ b/homeassistant/auth/permissions/__init__.py
@@ -1,6 +1,8 @@
"""Permissions for Home Assistant."""
+from __future__ import annotations
+
import logging
-from typing import Any, Callable, Optional
+from typing import Any, Callable
import voluptuous as vol
@@ -19,7 +21,7 @@
class AbstractPermissions:
"""Default permissions class."""
- _cached_entity_func: Optional[Callable[[str, str], bool]] = None
+ _cached_entity_func: Callable[[str, str], bool] | None = None
def _entity_func(self) -> Callable[[str, str], bool]:
"""Return a function that can test entity access."""
diff --git a/homeassistant/auth/permissions/entities.py b/homeassistant/auth/permissions/entities.py
index be30c7bf69aef5..f19590a6349de1 100644
--- a/homeassistant/auth/permissions/entities.py
+++ b/homeassistant/auth/permissions/entities.py
@@ -1,6 +1,8 @@
"""Entity permissions."""
+from __future__ import annotations
+
from collections import OrderedDict
-from typing import Callable, Optional
+from typing import Callable
import voluptuous as vol
@@ -43,14 +45,14 @@
def _lookup_domain(
perm_lookup: PermissionLookup, domains_dict: SubCategoryDict, entity_id: str
-) -> Optional[ValueType]:
+) -> ValueType | None:
"""Look up entity permissions by domain."""
return domains_dict.get(entity_id.split(".", 1)[0])
def _lookup_area(
perm_lookup: PermissionLookup, area_dict: SubCategoryDict, entity_id: str
-) -> Optional[ValueType]:
+) -> ValueType | None:
"""Look up entity permissions by area."""
entity_entry = perm_lookup.entity_registry.async_get(entity_id)
@@ -67,7 +69,7 @@ def _lookup_area(
def _lookup_device(
perm_lookup: PermissionLookup, devices_dict: SubCategoryDict, entity_id: str
-) -> Optional[ValueType]:
+) -> ValueType | None:
"""Look up entity permissions by device."""
entity_entry = perm_lookup.entity_registry.async_get(entity_id)
@@ -79,7 +81,7 @@ def _lookup_device(
def _lookup_entity_id(
perm_lookup: PermissionLookup, entities_dict: SubCategoryDict, entity_id: str
-) -> Optional[ValueType]:
+) -> ValueType | None:
"""Look up entity permission by entity id."""
return entities_dict.get(entity_id)
diff --git a/homeassistant/auth/permissions/merge.py b/homeassistant/auth/permissions/merge.py
index fad98b3f22a3f7..121d87f78484cb 100644
--- a/homeassistant/auth/permissions/merge.py
+++ b/homeassistant/auth/permissions/merge.py
@@ -1,13 +1,15 @@
"""Merging of policies."""
-from typing import Dict, List, Set, cast
+from __future__ import annotations
+
+from typing import cast
from .types import CategoryType, PolicyType
-def merge_policies(policies: List[PolicyType]) -> PolicyType:
+def merge_policies(policies: list[PolicyType]) -> PolicyType:
"""Merge policies."""
- new_policy: Dict[str, CategoryType] = {}
- seen: Set[str] = set()
+ new_policy: dict[str, CategoryType] = {}
+ seen: set[str] = set()
for policy in policies:
for category in policy:
if category in seen:
@@ -20,7 +22,7 @@ def merge_policies(policies: List[PolicyType]) -> PolicyType:
return new_policy
-def _merge_policies(sources: List[CategoryType]) -> CategoryType:
+def _merge_policies(sources: list[CategoryType]) -> CategoryType:
"""Merge a policy."""
# When merging policies, the most permissive wins.
# This means we order it like this:
@@ -34,7 +36,7 @@ def _merge_policies(sources: List[CategoryType]) -> CategoryType:
# merge each key in the source.
policy: CategoryType = None
- seen: Set[str] = set()
+ seen: set[str] = set()
for source in sources:
if source is None:
continue
diff --git a/homeassistant/auth/permissions/models.py b/homeassistant/auth/permissions/models.py
index 2542be14cc613d..aa1a777ced26eb 100644
--- a/homeassistant/auth/permissions/models.py
+++ b/homeassistant/auth/permissions/models.py
@@ -1,11 +1,12 @@
"""Models for permissions."""
+from __future__ import annotations
+
from typing import TYPE_CHECKING
import attr
if TYPE_CHECKING:
- # pylint: disable=unused-import
- from homeassistant.helpers import ( # noqa: F401
+ from homeassistant.helpers import (
device_registry as dev_reg,
entity_registry as ent_reg,
)
@@ -15,5 +16,5 @@
class PermissionLookup:
"""Class to hold data for permission lookups."""
- entity_registry: "ent_reg.EntityRegistry" = attr.ib()
- device_registry: "dev_reg.DeviceRegistry" = attr.ib()
+ entity_registry: ent_reg.EntityRegistry = attr.ib()
+ device_registry: dev_reg.DeviceRegistry = attr.ib()
diff --git a/homeassistant/auth/permissions/util.py b/homeassistant/auth/permissions/util.py
index 11bbd878eb23d1..e95e0080b50a83 100644
--- a/homeassistant/auth/permissions/util.py
+++ b/homeassistant/auth/permissions/util.py
@@ -1,6 +1,8 @@
"""Helpers to deal with permissions."""
+from __future__ import annotations
+
from functools import wraps
-from typing import Callable, Dict, List, Optional, cast
+from typing import Callable, Dict, Optional, cast
from .const import SUBCAT_ALL
from .models import PermissionLookup
@@ -45,7 +47,7 @@ def apply_policy_allow_all(entity_id: str, key: str) -> bool:
assert isinstance(policy, dict)
- funcs: List[Callable[[str, str], Optional[bool]]] = []
+ funcs: list[Callable[[str, str], bool | None]] = []
for key, lookup_func in subcategories.items():
lookup_value = policy.get(key)
@@ -80,10 +82,10 @@ def apply_policy_funcs(object_id: str, key: str) -> bool:
def _gen_dict_test_func(
perm_lookup: PermissionLookup, lookup_func: LookupFunc, lookup_dict: SubCategoryDict
-) -> Callable[[str, str], Optional[bool]]:
+) -> Callable[[str, str], bool | None]:
"""Generate a lookup function."""
- def test_value(object_id: str, key: str) -> Optional[bool]:
+ def test_value(object_id: str, key: str) -> bool | None:
"""Test if permission is allowed based on the keys."""
schema: ValueType = lookup_func(perm_lookup, lookup_dict, object_id)
diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py
index e766083edc382c..6e188be1ffc940 100644
--- a/homeassistant/auth/providers/__init__.py
+++ b/homeassistant/auth/providers/__init__.py
@@ -1,8 +1,10 @@
"""Auth providers for Home Assistant."""
+from __future__ import annotations
+
import importlib
import logging
import types
-from typing import Any, Dict, List, Optional
+from typing import Any
import voluptuous as vol
from voluptuous.humanize import humanize_error
@@ -40,7 +42,7 @@ class AuthProvider:
DEFAULT_TITLE = "Unnamed auth provider"
def __init__(
- self, hass: HomeAssistant, store: AuthStore, config: Dict[str, Any]
+ self, hass: HomeAssistant, store: AuthStore, config: dict[str, Any]
) -> None:
"""Initialize an auth provider."""
self.hass = hass
@@ -48,7 +50,7 @@ def __init__(
self.config = config
@property
- def id(self) -> Optional[str]:
+ def id(self) -> str | None:
"""Return id of the auth provider.
Optional, can be None.
@@ -70,7 +72,7 @@ def support_mfa(self) -> bool:
"""Return whether multi-factor auth supported by the auth provider."""
return True
- async def async_credentials(self) -> List[Credentials]:
+ async def async_credentials(self) -> list[Credentials]:
"""Return all credentials of this provider."""
users = await self.store.async_get_users()
return [
@@ -84,7 +86,7 @@ async def async_credentials(self) -> List[Credentials]:
]
@callback
- def async_create_credentials(self, data: Dict[str, str]) -> Credentials:
+ def async_create_credentials(self, data: dict[str, str]) -> Credentials:
"""Create credentials."""
return Credentials(
auth_provider_type=self.type, auth_provider_id=self.id, data=data
@@ -92,7 +94,7 @@ def async_create_credentials(self, data: Dict[str, str]) -> Credentials:
# Implement by extending class
- async def async_login_flow(self, context: Optional[Dict]) -> "LoginFlow":
+ async def async_login_flow(self, context: dict | None) -> LoginFlow:
"""Return the data flow for logging in with auth provider.
Auth provider should extend LoginFlow and return an instance.
@@ -100,7 +102,7 @@ async def async_login_flow(self, context: Optional[Dict]) -> "LoginFlow":
raise NotImplementedError
async def async_get_or_create_credentials(
- self, flow_result: Dict[str, str]
+ self, flow_result: dict[str, str]
) -> Credentials:
"""Get credentials based on the flow result."""
raise NotImplementedError
@@ -119,7 +121,7 @@ async def async_initialize(self) -> None:
@callback
def async_validate_refresh_token(
- self, refresh_token: RefreshToken, remote_ip: Optional[str] = None
+ self, refresh_token: RefreshToken, remote_ip: str | None = None
) -> None:
"""Verify a refresh token is still valid.
@@ -129,7 +131,7 @@ def async_validate_refresh_token(
async def auth_provider_from_config(
- hass: HomeAssistant, store: AuthStore, config: Dict[str, Any]
+ hass: HomeAssistant, store: AuthStore, config: dict[str, Any]
) -> AuthProvider:
"""Initialize an auth provider from a config."""
provider_name = config[CONF_TYPE]
@@ -186,17 +188,17 @@ class LoginFlow(data_entry_flow.FlowHandler):
def __init__(self, auth_provider: AuthProvider) -> None:
"""Initialize the login flow."""
self._auth_provider = auth_provider
- self._auth_module_id: Optional[str] = None
+ self._auth_module_id: str | None = None
self._auth_manager = auth_provider.hass.auth
- self.available_mfa_modules: Dict[str, str] = {}
+ self.available_mfa_modules: dict[str, str] = {}
self.created_at = dt_util.utcnow()
self.invalid_mfa_times = 0
- self.user: Optional[User] = None
- self.credential: Optional[Credentials] = None
+ self.user: User | None = None
+ self.credential: Credentials | None = None
async def async_step_init(
- self, user_input: Optional[Dict[str, str]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, str] | None = None
+ ) -> dict[str, Any]:
"""Handle the first step of login flow.
Return self.async_show_form(step_id='init') if user_input is None.
@@ -205,8 +207,8 @@ async def async_step_init(
raise NotImplementedError
async def async_step_select_mfa_module(
- self, user_input: Optional[Dict[str, str]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, str] | None = None
+ ) -> dict[str, Any]:
"""Handle the step of select mfa module."""
errors = {}
@@ -230,8 +232,8 @@ async def async_step_select_mfa_module(
)
async def async_step_mfa(
- self, user_input: Optional[Dict[str, str]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, str] | None = None
+ ) -> dict[str, Any]:
"""Handle the step of mfa validation."""
assert self.credential
assert self.user
@@ -271,7 +273,7 @@ async def async_step_mfa(
if not errors:
return await self.async_finish(self.credential)
- description_placeholders: Dict[str, Optional[str]] = {
+ description_placeholders: dict[str, str | None] = {
"mfa_module_name": auth_module.name,
"mfa_module_id": auth_module.id,
}
@@ -283,6 +285,6 @@ async def async_step_mfa(
errors=errors,
)
- async def async_finish(self, flow_result: Any) -> Dict:
+ async def async_finish(self, flow_result: Any) -> dict:
"""Handle the pass of login flow."""
return self.async_create_entry(title=self._auth_provider.name, data=flow_result)
diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py
index d194d8119d1d25..47a56d87097c42 100644
--- a/homeassistant/auth/providers/command_line.py
+++ b/homeassistant/auth/providers/command_line.py
@@ -1,19 +1,20 @@
"""Auth provider that validates credentials via an external command."""
+from __future__ import annotations
import asyncio.subprocess
import collections
import logging
import os
-from typing import Any, Dict, Optional, cast
+from typing import Any, cast
import voluptuous as vol
+from homeassistant.const import CONF_COMMAND
from homeassistant.exceptions import HomeAssistantError
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
from ..models import Credentials, UserMeta
-CONF_COMMAND = "command"
CONF_ARGS = "args"
CONF_META = "meta"
@@ -51,9 +52,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
attributes provided by external programs.
"""
super().__init__(*args, **kwargs)
- self._user_meta: Dict[str, Dict[str, Any]] = {}
+ self._user_meta: dict[str, dict[str, Any]] = {}
- async def async_login_flow(self, context: Optional[dict]) -> LoginFlow:
+ async def async_login_flow(self, context: dict | None) -> LoginFlow:
"""Return a flow to login."""
return CommandLineLoginFlow(self)
@@ -82,7 +83,7 @@ async def async_validate_login(self, username: str, password: str) -> None:
raise InvalidAuthError
if self.config[CONF_META]:
- meta: Dict[str, str] = {}
+ meta: dict[str, str] = {}
for _line in stdout.splitlines():
try:
line = _line.decode().lstrip()
@@ -99,7 +100,7 @@ async def async_validate_login(self, username: str, password: str) -> None:
self._user_meta[username] = meta
async def async_get_or_create_credentials(
- self, flow_result: Dict[str, str]
+ self, flow_result: dict[str, str]
) -> Credentials:
"""Get credentials based on the flow result."""
username = flow_result["username"]
@@ -125,8 +126,8 @@ class CommandLineLoginFlow(LoginFlow):
"""Handler for the login flow."""
async def async_step_init(
- self, user_input: Optional[Dict[str, str]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, str] | None = None
+ ) -> dict[str, Any]:
"""Handle the step of the form."""
errors = {}
@@ -143,7 +144,7 @@ async def async_step_init(
user_input.pop("password")
return await self.async_finish(user_input)
- schema: Dict[str, type] = collections.OrderedDict()
+ schema: dict[str, type] = collections.OrderedDict()
schema["username"] = str
schema["password"] = str
diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py
index 70e2f5403cdefa..54d82013a7580e 100644
--- a/homeassistant/auth/providers/homeassistant.py
+++ b/homeassistant/auth/providers/homeassistant.py
@@ -1,9 +1,11 @@
"""Home Assistant auth provider."""
+from __future__ import annotations
+
import asyncio
import base64
from collections import OrderedDict
import logging
-from typing import Any, Dict, List, Optional, Set, cast
+from typing import Any, cast
import bcrypt
import voluptuous as vol
@@ -19,7 +21,7 @@
STORAGE_KEY = "auth_provider.homeassistant"
-def _disallow_id(conf: Dict[str, Any]) -> Dict[str, Any]:
+def _disallow_id(conf: dict[str, Any]) -> dict[str, Any]:
"""Disallow ID in config."""
if CONF_ID in conf:
raise vol.Invalid("ID is not allowed for the homeassistant auth provider.")
@@ -31,7 +33,7 @@ def _disallow_id(conf: Dict[str, Any]) -> Dict[str, Any]:
@callback
-def async_get_provider(hass: HomeAssistant) -> "HassAuthProvider":
+def async_get_provider(hass: HomeAssistant) -> HassAuthProvider:
"""Get the provider."""
for prv in hass.auth.auth_providers:
if prv.type == "homeassistant":
@@ -60,7 +62,7 @@ def __init__(self, hass: HomeAssistant) -> None:
self._store = hass.helpers.storage.Store(
STORAGE_VERSION, STORAGE_KEY, private=True
)
- self._data: Optional[Dict[str, Any]] = None
+ self._data: dict[str, Any] | None = None
# Legacy mode will allow usernames to start/end with whitespace
# and will compare usernames case-insensitive.
# Remove in 2020 or when we launch 1.0.
@@ -81,7 +83,7 @@ async def async_load(self) -> None:
if data is None:
data = {"users": []}
- seen: Set[str] = set()
+ seen: set[str] = set()
for user in data["users"]:
username = user["username"]
@@ -119,7 +121,7 @@ async def async_load(self) -> None:
self._data = data
@property
- def users(self) -> List[Dict[str, str]]:
+ def users(self) -> list[dict[str, str]]:
"""Return users."""
return self._data["users"] # type: ignore
@@ -218,7 +220,7 @@ class HassAuthProvider(AuthProvider):
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize an Home Assistant auth provider."""
super().__init__(*args, **kwargs)
- self.data: Optional[Data] = None
+ self.data: Data | None = None
self._init_lock = asyncio.Lock()
async def async_initialize(self) -> None:
@@ -231,7 +233,7 @@ async def async_initialize(self) -> None:
await data.async_load()
self.data = data
- async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
+ async def async_login_flow(self, context: dict | None) -> LoginFlow:
"""Return a flow to login."""
return HassLoginFlow(self)
@@ -275,7 +277,7 @@ async def async_change_password(self, username: str, new_password: str) -> None:
await self.data.async_save()
async def async_get_or_create_credentials(
- self, flow_result: Dict[str, str]
+ self, flow_result: dict[str, str]
) -> Credentials:
"""Get credentials based on the flow result."""
if self.data is None:
@@ -316,8 +318,8 @@ class HassLoginFlow(LoginFlow):
"""Handler for the login flow."""
async def async_step_init(
- self, user_input: Optional[Dict[str, str]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, str] | None = None
+ ) -> dict[str, Any]:
"""Handle the step of the form."""
errors = {}
@@ -333,7 +335,7 @@ async def async_step_init(
user_input.pop("password")
return await self.async_finish(user_input)
- schema: Dict[str, type] = OrderedDict()
+ schema: dict[str, type] = OrderedDict()
schema["username"] = str
schema["password"] = str
diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py
index 70014a236cdae2..c938a6fac81539 100644
--- a/homeassistant/auth/providers/insecure_example.py
+++ b/homeassistant/auth/providers/insecure_example.py
@@ -1,7 +1,9 @@
"""Example auth provider."""
+from __future__ import annotations
+
from collections import OrderedDict
import hmac
-from typing import Any, Dict, Optional, cast
+from typing import Any, cast
import voluptuous as vol
@@ -33,7 +35,7 @@ class InvalidAuthError(HomeAssistantError):
class ExampleAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords."""
- async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
+ async def async_login_flow(self, context: dict | None) -> LoginFlow:
"""Return a flow to login."""
return ExampleLoginFlow(self)
@@ -60,7 +62,7 @@ def async_validate_login(self, username: str, password: str) -> None:
raise InvalidAuthError
async def async_get_or_create_credentials(
- self, flow_result: Dict[str, str]
+ self, flow_result: dict[str, str]
) -> Credentials:
"""Get credentials based on the flow result."""
username = flow_result["username"]
@@ -94,8 +96,8 @@ class ExampleLoginFlow(LoginFlow):
"""Handler for the login flow."""
async def async_step_init(
- self, user_input: Optional[Dict[str, str]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, str] | None = None
+ ) -> dict[str, Any]:
"""Handle the step of the form."""
errors = {}
@@ -111,7 +113,7 @@ async def async_step_init(
user_input.pop("password")
return await self.async_finish(user_input)
- schema: Dict[str, type] = OrderedDict()
+ schema: dict[str, type] = OrderedDict()
schema["username"] = str
schema["password"] = str
diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py
index ba96fa285f1213..522751c70d6988 100644
--- a/homeassistant/auth/providers/legacy_api_password.py
+++ b/homeassistant/auth/providers/legacy_api_password.py
@@ -3,8 +3,10 @@
It will be removed when auth system production ready
"""
+from __future__ import annotations
+
import hmac
-from typing import Any, Dict, Optional, cast
+from typing import Any, cast
import voluptuous as vol
@@ -40,7 +42,7 @@ def api_password(self) -> str:
"""Return api_password."""
return str(self.config[CONF_API_PASSWORD])
- async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
+ async def async_login_flow(self, context: dict | None) -> LoginFlow:
"""Return a flow to login."""
return LegacyLoginFlow(self)
@@ -55,7 +57,7 @@ def async_validate_login(self, password: str) -> None:
raise InvalidAuthError
async def async_get_or_create_credentials(
- self, flow_result: Dict[str, str]
+ self, flow_result: dict[str, str]
) -> Credentials:
"""Return credentials for this login."""
credentials = await self.async_credentials()
@@ -79,8 +81,8 @@ class LegacyLoginFlow(LoginFlow):
"""Handler for the login flow."""
async def async_step_init(
- self, user_input: Optional[Dict[str, str]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, str] | None = None
+ ) -> dict[str, Any]:
"""Handle the step of the form."""
errors = {}
diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py
index 2afdbf98196dd1..85b43d89f3fb40 100644
--- a/homeassistant/auth/providers/trusted_networks.py
+++ b/homeassistant/auth/providers/trusted_networks.py
@@ -3,6 +3,8 @@
It shows list of users if access from trusted network.
Abort login flow if not access from trusted network.
"""
+from __future__ import annotations
+
from ipaddress import (
IPv4Address,
IPv4Network,
@@ -11,7 +13,7 @@
ip_address,
ip_network,
)
-from typing import Any, Dict, List, Optional, Union, cast
+from typing import Any, Dict, List, Union, cast
import voluptuous as vol
@@ -68,12 +70,12 @@ class TrustedNetworksAuthProvider(AuthProvider):
DEFAULT_TITLE = "Trusted Networks"
@property
- def trusted_networks(self) -> List[IPNetwork]:
+ def trusted_networks(self) -> list[IPNetwork]:
"""Return trusted networks."""
return cast(List[IPNetwork], self.config[CONF_TRUSTED_NETWORKS])
@property
- def trusted_users(self) -> Dict[IPNetwork, Any]:
+ def trusted_users(self) -> dict[IPNetwork, Any]:
"""Return trusted users per network."""
return cast(Dict[IPNetwork, Any], self.config[CONF_TRUSTED_USERS])
@@ -82,7 +84,7 @@ def support_mfa(self) -> bool:
"""Trusted Networks auth provider does not support MFA."""
return False
- async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
+ async def async_login_flow(self, context: dict | None) -> LoginFlow:
"""Return a flow to login."""
assert context is not None
ip_addr = cast(IPAddress, context.get("ip_address"))
@@ -111,7 +113,7 @@ async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
if (
user.id in user_list
or any(
- [group.id in flattened_group_list for group in user.groups]
+ group.id in flattened_group_list for group in user.groups
)
)
]
@@ -125,7 +127,7 @@ async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
)
async def async_get_or_create_credentials(
- self, flow_result: Dict[str, str]
+ self, flow_result: dict[str, str]
) -> Credentials:
"""Get credentials based on the flow result."""
user_id = flow_result["user"]
@@ -169,7 +171,7 @@ def async_validate_access(self, ip_addr: IPAddress) -> None:
@callback
def async_validate_refresh_token(
- self, refresh_token: RefreshToken, remote_ip: Optional[str] = None
+ self, refresh_token: RefreshToken, remote_ip: str | None = None
) -> None:
"""Verify a refresh token is still valid."""
if remote_ip is None:
@@ -186,7 +188,7 @@ def __init__(
self,
auth_provider: TrustedNetworksAuthProvider,
ip_addr: IPAddress,
- available_users: Dict[str, Optional[str]],
+ available_users: dict[str, str | None],
allow_bypass_login: bool,
) -> None:
"""Initialize the login flow."""
@@ -196,8 +198,8 @@ def __init__(
self._allow_bypass_login = allow_bypass_login
async def async_step_init(
- self, user_input: Optional[Dict[str, str]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, str] | None = None
+ ) -> dict[str, Any]:
"""Handle the step of the form."""
try:
cast(
diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py
index 0f5bda7fbf24d7..fc12ec065a9bd8 100644
--- a/homeassistant/bootstrap.py
+++ b/homeassistant/bootstrap.py
@@ -1,4 +1,6 @@
"""Provide methods to bootstrap a Home Assistant instance."""
+from __future__ import annotations
+
import asyncio
import contextlib
from datetime import datetime
@@ -8,7 +10,7 @@
import sys
import threading
from time import monotonic
-from typing import TYPE_CHECKING, Any, Dict, Optional, Set
+from typing import TYPE_CHECKING, Any
import voluptuous as vol
import yarl
@@ -17,17 +19,20 @@
from homeassistant.components import http
from homeassistant.const import REQUIRED_NEXT_PYTHON_DATE, REQUIRED_NEXT_PYTHON_VER
from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers import area_registry, device_registry, entity_registry
+from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import (
DATA_SETUP,
DATA_SETUP_STARTED,
+ DATA_SETUP_TIME,
async_set_domains_to_be_loaded,
async_setup_component,
)
from homeassistant.util.async_ import gather_with_concurrency
+import homeassistant.util.dt as dt_util
from homeassistant.util.logging import async_activate_log_queue_handler
from homeassistant.util.package import async_get_user_site, is_virtual_env
-from homeassistant.util.yaml import clear_secret_cache
if TYPE_CHECKING:
from .runner import RuntimeConfig
@@ -40,6 +45,8 @@
DATA_LOGGING = "logging"
LOG_SLOW_STARTUP_INTERVAL = 60
+SLOW_STARTUP_CHECK_INTERVAL = 1
+SIGNAL_BOOTSTRAP_INTEGRATONS = "bootstrap_integrations"
STAGE_1_TIMEOUT = 120
STAGE_2_TIMEOUT = 300
@@ -74,8 +81,8 @@
async def async_setup_hass(
- runtime_config: "RuntimeConfig",
-) -> Optional[core.HomeAssistant]:
+ runtime_config: RuntimeConfig,
+) -> core.HomeAssistant | None:
"""Set up Home Assistant."""
hass = core.HomeAssistant()
hass.config.config_dir = runtime_config.config_dir
@@ -121,8 +128,6 @@ async def async_setup_hass(
basic_setup_success = (
await async_from_config_dict(config_dict, hass) is not None
)
- finally:
- clear_secret_cache()
if config_dict is None:
safe_mode = True
@@ -190,7 +195,7 @@ def open_hass_ui(hass: core.HomeAssistant) -> None:
async def async_from_config_dict(
config: ConfigType, hass: core.HomeAssistant
-) -> Optional[core.HomeAssistant]:
+) -> core.HomeAssistant | None:
"""Try to configure Home Assistant from a configuration dictionary.
Dynamically loads required components and its dependencies.
@@ -257,8 +262,8 @@ async def async_from_config_dict(
def async_enable_logging(
hass: core.HomeAssistant,
verbose: bool = False,
- log_rotate_days: Optional[int] = None,
- log_file: Optional[str] = None,
+ log_rotate_days: int | None = None,
+ log_file: str | None = None,
log_no_color: bool = False,
) -> None:
"""Set up the logging.
@@ -364,7 +369,7 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
@core.callback
-def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]:
+def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
"""Get domains of components to set up."""
# Filter out the repeating and common config section [homeassistant]
domains = {key.split(" ")[0] for key in config if key != core.DOMAIN}
@@ -380,38 +385,43 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]:
return domains
-async def _async_log_pending_setups(
- hass: core.HomeAssistant, domains: Set[str], setup_started: Dict[str, datetime]
-) -> None:
+async def _async_watch_pending_setups(hass: core.HomeAssistant) -> None:
"""Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL."""
+ loop_count = 0
+ setup_started: dict[str, datetime] = hass.data[DATA_SETUP_STARTED]
while True:
- await asyncio.sleep(LOG_SLOW_STARTUP_INTERVAL)
- remaining = [domain for domain in domains if domain in setup_started]
+ now = dt_util.utcnow()
+ remaining_with_setup_started = {
+ domain: (now - setup_started[domain]).total_seconds()
+ for domain in setup_started
+ }
+ _LOGGER.debug("Integration remaining: %s", remaining_with_setup_started)
+ async_dispatcher_send(
+ hass, SIGNAL_BOOTSTRAP_INTEGRATONS, remaining_with_setup_started
+ )
+ await asyncio.sleep(SLOW_STARTUP_CHECK_INTERVAL)
+ loop_count += SLOW_STARTUP_CHECK_INTERVAL
- if remaining:
+ if loop_count >= LOG_SLOW_STARTUP_INTERVAL and setup_started:
_LOGGER.warning(
"Waiting on integrations to complete setup: %s",
- ", ".join(remaining),
+ ", ".join(setup_started),
)
+ loop_count = 0
_LOGGER.debug("Running timeout Zones: %s", hass.timeout.zones)
async def async_setup_multi_components(
hass: core.HomeAssistant,
- domains: Set[str],
- config: Dict[str, Any],
- setup_started: Dict[str, datetime],
+ domains: set[str],
+ config: dict[str, Any],
) -> None:
"""Set up multiple domains. Log on failure."""
futures = {
domain: hass.async_create_task(async_setup_component(hass, domain, config))
for domain in domains
}
- log_task = asyncio.create_task(
- _async_log_pending_setups(hass, domains, setup_started)
- )
await asyncio.wait(futures.values())
- log_task.cancel()
errors = [domain for domain in domains if futures[domain].exception()]
for domain in errors:
exception = futures[domain].exception()
@@ -424,15 +434,19 @@ async def async_setup_multi_components(
async def _async_set_up_integrations(
- hass: core.HomeAssistant, config: Dict[str, Any]
+ hass: core.HomeAssistant, config: dict[str, Any]
) -> None:
"""Set up all the integrations."""
- setup_started = hass.data[DATA_SETUP_STARTED] = {}
+ hass.data[DATA_SETUP_STARTED] = {}
+ setup_time = hass.data[DATA_SETUP_TIME] = {}
+
+ watch_task = asyncio.create_task(_async_watch_pending_setups(hass))
+
domains_to_setup = _get_domains(hass, config)
# Resolve all dependencies so we know all integrations
# that will have to be loaded and start rightaway
- integration_cache: Dict[str, loader.Integration] = {}
+ integration_cache: dict[str, loader.Integration] = {}
to_resolve = domains_to_setup
while to_resolve:
old_to_resolve = to_resolve
@@ -476,14 +490,14 @@ async def _async_set_up_integrations(
# Load logging as soon as possible
if logging_domains:
_LOGGER.info("Setting up logging: %s", logging_domains)
- await async_setup_multi_components(hass, logging_domains, config, setup_started)
+ await async_setup_multi_components(hass, logging_domains, config)
# Start up debuggers. Start these first in case they want to wait.
debuggers = domains_to_setup & DEBUGGER_INTEGRATIONS
if debuggers:
_LOGGER.debug("Setting up debuggers: %s", debuggers)
- await async_setup_multi_components(hass, debuggers, config, setup_started)
+ await async_setup_multi_components(hass, debuggers, config)
# calculate what components to setup in what stage
stage_1_domains = set()
@@ -510,10 +524,12 @@ async def _async_set_up_integrations(
stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains
- # Kick off loading the registries. They don't need to be awaited.
- asyncio.create_task(hass.helpers.device_registry.async_get_registry())
- asyncio.create_task(hass.helpers.entity_registry.async_get_registry())
- asyncio.create_task(hass.helpers.area_registry.async_get_registry())
+ # Load the registries
+ await asyncio.gather(
+ device_registry.async_load(hass),
+ entity_registry.async_load(hass),
+ area_registry.async_load(hass),
+ )
# Start setup
if stage_1_domains:
@@ -522,9 +538,7 @@ async def _async_set_up_integrations(
async with hass.timeout.async_timeout(
STAGE_1_TIMEOUT, cool_down=COOLDOWN_TIME
):
- await async_setup_multi_components(
- hass, stage_1_domains, config, setup_started
- )
+ await async_setup_multi_components(hass, stage_1_domains, config)
except asyncio.TimeoutError:
_LOGGER.warning("Setup timed out for stage 1 - moving forward")
@@ -537,12 +551,23 @@ async def _async_set_up_integrations(
async with hass.timeout.async_timeout(
STAGE_2_TIMEOUT, cool_down=COOLDOWN_TIME
):
- await async_setup_multi_components(
- hass, stage_2_domains, config, setup_started
- )
+ await async_setup_multi_components(hass, stage_2_domains, config)
except asyncio.TimeoutError:
_LOGGER.warning("Setup timed out for stage 2 - moving forward")
+ watch_task.cancel()
+ async_dispatcher_send(hass, SIGNAL_BOOTSTRAP_INTEGRATONS, {})
+
+ _LOGGER.debug(
+ "Integration setup times: %s",
+ {
+ integration: timedelta.total_seconds()
+ for integration, timedelta in sorted(
+ setup_time.items(), key=lambda item: item[1].total_seconds() # type: ignore
+ )
+ },
+ )
+
# Wrap up startup
_LOGGER.debug("Waiting for startup to wrap up")
try:
diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py
index 529e3ff7189b01..c1c89951c3f8f5 100644
--- a/homeassistant/components/abode/__init__.py
+++ b/homeassistant/components/abode/__init__.py
@@ -13,6 +13,7 @@
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_DATE,
+ ATTR_DEVICE_ID,
ATTR_ENTITY_ID,
ATTR_TIME,
CONF_PASSWORD,
@@ -32,7 +33,6 @@
SERVICE_CAPTURE_IMAGE = "capture_image"
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
-ATTR_DEVICE_ID = "device_id"
ATTR_DEVICE_NAME = "device_name"
ATTR_DEVICE_TYPE = "device_type"
ATTR_EVENT_CODE = "event_code"
@@ -66,7 +66,7 @@
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
-ABODE_PLATFORMS = [
+PLATFORMS = [
"alarm_control_panel",
"binary_sensor",
"lock",
@@ -138,7 +138,7 @@ async def async_setup_entry(hass, config_entry):
hass.data[DOMAIN] = AbodeSystem(abode, polling)
- for platform in ABODE_PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
@@ -158,7 +158,7 @@ async def async_unload_entry(hass, config_entry):
tasks = []
- for platform in ABODE_PLATFORMS:
+ for platform in PLATFORMS:
tasks.append(
hass.config_entries.async_forward_entry_unload(config_entry, platform)
)
@@ -363,7 +363,7 @@ def name(self):
return self._device.name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
@@ -411,7 +411,7 @@ def name(self):
return self._automation.name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION, "type": "CUE automation"}
diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py
index c508d0f02408d0..6d0c030e3e1e04 100644
--- a/homeassistant/components/abode/alarm_control_panel.py
+++ b/homeassistant/components/abode/alarm_control_panel.py
@@ -69,7 +69,7 @@ def alarm_arm_away(self, code=None):
self._device.set_away()
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py
index 76c23f7f705ac5..4e6a6f4c904cbf 100644
--- a/homeassistant/components/abode/config_flow.py
+++ b/homeassistant/components/abode/config_flow.py
@@ -8,7 +8,7 @@
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_BAD_REQUEST
-from .const import DEFAULT_CACHEDB, DOMAIN, LOGGER # pylint: disable=unused-import
+from .const import DEFAULT_CACHEDB, DOMAIN, LOGGER
CONF_MFA = "mfa_code"
CONF_POLLING = "polling"
@@ -163,7 +163,7 @@ async def async_step_reauth_confirm(self, user_input=None):
async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml."""
if self._async_current_entries():
- LOGGER.warning("Already configured. Only a single configuration possible.")
+ LOGGER.warning("Already configured; Only a single configuration possible")
return self.async_abort(reason="single_instance_allowed")
self._polling = import_config.get(CONF_POLLING, False)
diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py
index 6ecc5c871cd090..e3ececb62d96ab 100644
--- a/homeassistant/components/abode/sensor.py
+++ b/homeassistant/components/abode/sensor.py
@@ -1,6 +1,7 @@
"""Support for Abode Security System sensors."""
import abodepy.helpers.constants as CONST
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
@@ -33,7 +34,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities)
-class AbodeSensor(AbodeDevice):
+class AbodeSensor(AbodeDevice, SensorEntity):
"""A sensor implementation for Abode devices."""
def __init__(self, data, device, sensor_type):
diff --git a/homeassistant/components/abode/translations/de.json b/homeassistant/components/abode/translations/de.json
index 43d6ba21ca565e..307f5f45065d42 100644
--- a/homeassistant/components/abode/translations/de.json
+++ b/homeassistant/components/abode/translations/de.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"reauth_successful": "Die erneute Authentifizierung war erfolgreich",
- "single_instance_allowed": "Es ist nur eine einzige Konfiguration von Abode erlaubt."
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"error": {
"cannot_connect": "Verbindung fehlgeschlagen",
diff --git a/homeassistant/components/abode/translations/es.json b/homeassistant/components/abode/translations/es.json
index 9fa8cd8b06b7af..66cb5d13f22318 100644
--- a/homeassistant/components/abode/translations/es.json
+++ b/homeassistant/components/abode/translations/es.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "reauth_successful": "La reautenticaci\u00f3n fue exitosa",
+ "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente",
"single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n."
},
"error": {
@@ -19,7 +19,7 @@
"reauth_confirm": {
"data": {
"password": "Contrase\u00f1a",
- "username": "Correo electronico"
+ "username": "Correo electr\u00f3nico"
},
"title": "Rellene su informaci\u00f3n de inicio de sesi\u00f3n de Abode"
},
diff --git a/homeassistant/components/abode/translations/fa.json b/homeassistant/components/abode/translations/fa.json
new file mode 100644
index 00000000000000..4ceaaf32a132fe
--- /dev/null
+++ b/homeassistant/components/abode/translations/fa.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u06a9\u0644\u0645\u0647 \u0639\u0628\u0648\u0631",
+ "username": "\u0627\u06cc\u0645\u06cc\u0644"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/translations/fr.json b/homeassistant/components/abode/translations/fr.json
index 87be79571a4ac3..2ab158cca57f1c 100644
--- a/homeassistant/components/abode/translations/fr.json
+++ b/homeassistant/components/abode/translations/fr.json
@@ -1,17 +1,32 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Une seule configuration d'Abode est autoris\u00e9e."
+ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi",
+ "single_instance_allowed": "D\u00e9ja configur\u00e9. Une seule configuration possible."
},
"error": {
"cannot_connect": "\u00c9chec de connexion",
- "invalid_auth": "Authentification invalide"
+ "invalid_auth": "Authentification invalide",
+ "invalid_mfa_code": "Code MFA non valide"
},
"step": {
+ "mfa": {
+ "data": {
+ "mfa_code": "Code MFA (6 chiffres)"
+ },
+ "title": "Entrez votre code MFA pour Abode"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "Mot de passe",
+ "username": "Email"
+ },
+ "title": "Remplissez vos informations de connexion Abode"
+ },
"user": {
"data": {
"password": "Mot de passe",
- "username": "Adresse e-mail"
+ "username": "Email"
},
"title": "Remplissez vos informations de connexion Abode"
}
diff --git a/homeassistant/components/abode/translations/he.json b/homeassistant/components/abode/translations/he.json
new file mode 100644
index 00000000000000..6f4191da70d538
--- /dev/null
+++ b/homeassistant/components/abode/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/translations/hu.json b/homeassistant/components/abode/translations/hu.json
index 5df508d0f33bfd..260416b07bba48 100644
--- a/homeassistant/components/abode/translations/hu.json
+++ b/homeassistant/components/abode/translations/hu.json
@@ -1,16 +1,25 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Csak egyetlen Abode konfigur\u00e1ci\u00f3 enged\u00e9lyezett."
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"invalid_mfa_code": "\u00c9rv\u00e9nytelen MFA k\u00f3d"
},
"step": {
"mfa": {
"data": {
"mfa_code": "MFA k\u00f3d (6 jegy\u0171)"
+ },
+ "title": "Add meg az Abode MFA k\u00f3dj\u00e1t"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "E-mail"
}
},
"user": {
diff --git a/homeassistant/components/abode/translations/id.json b/homeassistant/components/abode/translations/id.json
new file mode 100644
index 00000000000000..2dc79c833b2457
--- /dev/null
+++ b/homeassistant/components/abode/translations/id.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Autentikasi ulang berhasil",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "invalid_mfa_code": "Kode MFA tidak valid"
+ },
+ "step": {
+ "mfa": {
+ "data": {
+ "mfa_code": "Kode MFA (6 digit)"
+ },
+ "title": "Masukkan kode MFA Anda untuk Abode"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Email"
+ },
+ "title": "Masukkan informasi masuk Abode Anda"
+ },
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Email"
+ },
+ "title": "Masukkan informasi masuk Abode Anda"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/translations/it.json b/homeassistant/components/abode/translations/it.json
index a3e5aa4d7a81b0..6cb571df8e5e9d 100644
--- a/homeassistant/components/abode/translations/it.json
+++ b/homeassistant/components/abode/translations/it.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "reauth_successful": "La riautenticazione ha avuto successo",
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente",
"single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"error": {
diff --git a/homeassistant/components/abode/translations/ko.json b/homeassistant/components/abode/translations/ko.json
index 06c301550b4fea..85d3ef81aeba26 100644
--- a/homeassistant/components/abode/translations/ko.json
+++ b/homeassistant/components/abode/translations/ko.json
@@ -1,9 +1,28 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\ud558\ub098\uc758 Abode \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_mfa_code": "MFA \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
+ "mfa": {
+ "data": {
+ "mfa_code": "MFA \ucf54\ub4dc (6\uc790\ub9ac)"
+ },
+ "title": "Abode\uc5d0 \ub300\ud55c MFA \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc774\uba54\uc77c"
+ },
+ "title": "Abode \ub85c\uadf8\uc778 \uc815\ubcf4 \uc785\ub825\ud558\uae30"
+ },
"user": {
"data": {
"password": "\ube44\ubc00\ubc88\ud638",
diff --git a/homeassistant/components/abode/translations/nl.json b/homeassistant/components/abode/translations/nl.json
index 9177b1deb7c346..7b6a8b5aace727 100644
--- a/homeassistant/components/abode/translations/nl.json
+++ b/homeassistant/components/abode/translations/nl.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "reauth_successful": "Herauthenticatie was succesvol",
"single_instance_allowed": "Slechts een enkele configuratie van Abode is toegestaan."
},
"error": {
@@ -12,9 +13,14 @@
"mfa": {
"data": {
"mfa_code": "MFA-code (6-cijfers)"
- }
+ },
+ "title": "Voer uw MFA-code voor Abode in"
},
"reauth_confirm": {
+ "data": {
+ "password": "Wachtwoord",
+ "username": "E-mail"
+ },
"title": "Vul uw Abode-inloggegevens in"
},
"user": {
diff --git a/homeassistant/components/abode/translations/ru.json b/homeassistant/components/abode/translations/ru.json
index 04efaa6e5194a5..f3804a840ab2e3 100644
--- a/homeassistant/components/abode/translations/ru.json
+++ b/homeassistant/components/abode/translations/ru.json
@@ -6,7 +6,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"invalid_mfa_code": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434 MFA."
},
"step": {
diff --git a/homeassistant/components/abode/translations/tr.json b/homeassistant/components/abode/translations/tr.json
new file mode 100644
index 00000000000000..d469e43f1f42ae
--- /dev/null
+++ b/homeassistant/components/abode/translations/tr.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu",
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "invalid_mfa_code": "Ge\u00e7ersiz MFA kodu"
+ },
+ "step": {
+ "mfa": {
+ "data": {
+ "mfa_code": "MFA kodu (6 basamakl\u0131)"
+ },
+ "title": "Abode i\u00e7in MFA kodunuzu girin"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "Parola",
+ "username": "E-posta"
+ },
+ "title": "Abode giri\u015f bilgilerinizi doldurun"
+ },
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "E-posta"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/abode/translations/uk.json b/homeassistant/components/abode/translations/uk.json
new file mode 100644
index 00000000000000..7ad57a0ec68a69
--- /dev/null
+++ b/homeassistant/components/abode/translations/uk.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "invalid_mfa_code": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439 \u043a\u043e\u0434 MFA."
+ },
+ "step": {
+ "mfa": {
+ "data": {
+ "mfa_code": "\u041a\u043e\u0434 MFA (6 \u0446\u0438\u0444\u0440)"
+ },
+ "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 MFA \u0434\u043b\u044f Abode"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438"
+ },
+ "title": "Abode"
+ },
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438"
+ },
+ "title": "Abode"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py
index 27dbae7a41fdef..4ed471a50f592d 100644
--- a/homeassistant/components/accuweather/__init__.py
+++ b/homeassistant/components/accuweather/__init__.py
@@ -8,8 +8,6 @@
from async_timeout import timeout
from homeassistant.const import CONF_API_KEY
-from homeassistant.core import Config, HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -26,12 +24,6 @@
PLATFORMS = ["sensor", "weather"]
-async def async_setup(hass: HomeAssistant, config: Config) -> bool:
- """Set up configured AccuWeather."""
- hass.data.setdefault(DOMAIN, {})
- return True
-
-
async def async_setup_entry(hass, config_entry) -> bool:
"""Set up AccuWeather as config entry."""
api_key = config_entry.data[CONF_API_KEY]
@@ -45,21 +37,18 @@ async def async_setup_entry(hass, config_entry) -> bool:
coordinator = AccuWeatherDataUpdateCoordinator(
hass, websession, api_key, location_key, forecast
)
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
undo_listener = config_entry.add_update_listener(update_listener)
- hass.data[DOMAIN][config_entry.entry_id] = {
+ hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = {
COORDINATOR: coordinator,
UNDO_UPDATE_LISTENER: undo_listener,
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return True
@@ -70,8 +59,8 @@ async def async_unload_entry(hass, config_entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/accuweather/config_flow.py b/homeassistant/components/accuweather/config_flow.py
index 03d6f40181cef9..6dac2aee286f8a 100644
--- a/homeassistant/components/accuweather/config_flow.py
+++ b/homeassistant/components/accuweather/config_flow.py
@@ -13,7 +13,7 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from .const import CONF_FORECAST, DOMAIN # pylint:disable=unused-import
+from .const import CONF_FORECAST, DOMAIN
class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py
index e8dbe921d7707f..60fdd48c8f4c02 100644
--- a/homeassistant/components/accuweather/const.py
+++ b/homeassistant/components/accuweather/const.py
@@ -17,6 +17,7 @@
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
+ ATTR_ICON,
CONCENTRATION_PARTS_PER_CUBIC_METER,
DEVICE_CLASS_TEMPERATURE,
LENGTH_FEET,
@@ -33,7 +34,6 @@
)
ATTRIBUTION = "Data provided by AccuWeather"
-ATTR_ICON = "icon"
ATTR_FORECAST = CONF_FORECAST = "forecast"
ATTR_LABEL = "label"
ATTR_UNIT_IMPERIAL = "Imperial"
diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json
index 6ccd6a4f10b1f5..fd91f62ae33a65 100644
--- a/homeassistant/components/accuweather/manifest.json
+++ b/homeassistant/components/accuweather/manifest.json
@@ -2,7 +2,7 @@
"domain": "accuweather",
"name": "AccuWeather",
"documentation": "https://www.home-assistant.io/integrations/accuweather/",
- "requirements": ["accuweather==0.0.11"],
+ "requirements": ["accuweather==0.1.1"],
"codeowners": ["@bieniu"],
"config_flow": true,
"quality_scale": "platinum"
diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py
index 90058e254dcd56..722dd8869be88f 100644
--- a/homeassistant/components/accuweather/sensor.py
+++ b/homeassistant/components/accuweather/sensor.py
@@ -1,4 +1,5 @@
"""Support for the AccuWeather service."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
@@ -48,7 +49,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors, False)
-class AccuWeatherSensor(CoordinatorEntity):
+class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
"""Define an AccuWeather entity."""
def __init__(self, name, kind, coordinator, forecast_day=None):
@@ -141,7 +142,7 @@ def unit_of_measurement(self):
return SENSOR_TYPES[self.kind][self._unit_system]
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self.forecast_day is not None:
if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]:
diff --git a/homeassistant/components/accuweather/translations/ca.json b/homeassistant/components/accuweather/translations/ca.json
index 9c33637baa85dc..8178a5caef0907 100644
--- a/homeassistant/components/accuweather/translations/ca.json
+++ b/homeassistant/components/accuweather/translations/ca.json
@@ -27,7 +27,7 @@
"data": {
"forecast": "Previsi\u00f3 meteorol\u00f2gica"
},
- "description": "Per culpa de les limitacions de la versi\u00f3 gratu\u00efta l'API d'AccuWeather, quan habilitis la previsi\u00f3 meteorol\u00f2gica, les actualitzacions es realitzaran cada 64 minuts en comptes de 32.",
+ "description": "Per culpa de les limitacions de la versi\u00f3 gratu\u00efta l'API d'AccuWeather, quan habilitis la previsi\u00f3 meteorol\u00f2gica, les actualitzacions de dades es faran cada 80 minuts en comptes de cada 40.",
"title": "Opcions d'AccuWeather"
}
}
diff --git a/homeassistant/components/accuweather/translations/cs.json b/homeassistant/components/accuweather/translations/cs.json
index ea954b9f0dbc87..1cf34a42695440 100644
--- a/homeassistant/components/accuweather/translations/cs.json
+++ b/homeassistant/components/accuweather/translations/cs.json
@@ -27,7 +27,7 @@
"data": {
"forecast": "P\u0159edpov\u011b\u010f po\u010das\u00ed"
},
- "description": "Kdy\u017e povol\u00edte p\u0159edpov\u011b\u010f po\u010das\u00ed, budou aktualizace dat prov\u00e1d\u011bny ka\u017ed\u00fdch 64 minut nam\u00edsto 32 minut z d\u016fvodu omezen\u00ed bezplatn\u00e9 verze AccuWeather.",
+ "description": "Kdy\u017e povol\u00edte p\u0159edpov\u011b\u010f po\u010das\u00ed, budou aktualizace dat prov\u00e1d\u011bny ka\u017ed\u00fdch 80 minut nam\u00edsto 40 minut z d\u016fvodu omezen\u00ed bezplatn\u00e9 verze AccuWeather.",
"title": "Mo\u017enosti AccuWeather"
}
}
diff --git a/homeassistant/components/accuweather/translations/de.json b/homeassistant/components/accuweather/translations/de.json
index fe0319764a7cf7..330f2850d26e81 100644
--- a/homeassistant/components/accuweather/translations/de.json
+++ b/homeassistant/components/accuweather/translations/de.json
@@ -1,11 +1,17 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel",
+ "requests_exceeded": "Die zul\u00e4ssige Anzahl von Anforderungen an die Accuweather-API wurde \u00fcberschritten. Sie m\u00fcssen warten oder den API-Schl\u00fcssel \u00e4ndern."
},
"step": {
"user": {
"data": {
+ "api_key": "API-Schl\u00fcssel",
"latitude": "Breitengrad",
"longitude": "L\u00e4ngengrad",
"name": "Name"
@@ -25,7 +31,7 @@
},
"system_health": {
"info": {
- "can_reach_server": "AccuWeather Server erreichen",
+ "can_reach_server": "AccuWeather-Server erreichen",
"remaining_requests": "Verbleibende erlaubte Anfragen"
}
}
diff --git a/homeassistant/components/accuweather/translations/en.json b/homeassistant/components/accuweather/translations/en.json
index b737c420a2d9dc..8f2261b93c7de0 100644
--- a/homeassistant/components/accuweather/translations/en.json
+++ b/homeassistant/components/accuweather/translations/en.json
@@ -27,7 +27,7 @@
"data": {
"forecast": "Weather forecast"
},
- "description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 64 minutes instead of every 32 minutes.",
+ "description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 80 minutes instead of every 40 minutes.",
"title": "AccuWeather Options"
}
}
diff --git a/homeassistant/components/accuweather/translations/et.json b/homeassistant/components/accuweather/translations/et.json
index bed28b62975910..6e2dc1ffd96f1f 100644
--- a/homeassistant/components/accuweather/translations/et.json
+++ b/homeassistant/components/accuweather/translations/et.json
@@ -27,7 +27,7 @@
"data": {
"forecast": "Ilmateade"
},
- "description": "AccuWeather API tasuta versioonis toimub ilmaennustuse lubamisel andmete v\u00e4rskendamine iga 32 minuti asemel iga 64 minuti j\u00e4rel.",
+ "description": "AccuWeather API tasuta versioonis toimub ilmaennustuse lubamisel andmete v\u00e4rskendamine iga 80 minuti j\u00e4rel (muidu 40 minutit).",
"title": "AccuWeatheri valikud"
}
}
diff --git a/homeassistant/components/accuweather/translations/fr.json b/homeassistant/components/accuweather/translations/fr.json
index 8e63820541701b..a083ed09bdf6eb 100644
--- a/homeassistant/components/accuweather/translations/fr.json
+++ b/homeassistant/components/accuweather/translations/fr.json
@@ -34,7 +34,8 @@
},
"system_health": {
"info": {
- "can_reach_server": "Acc\u00e8s au serveur AccuWeather"
+ "can_reach_server": "Acc\u00e8s au serveur AccuWeather",
+ "remaining_requests": "Demandes restantes autoris\u00e9es"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/translations/he.json b/homeassistant/components/accuweather/translations/he.json
new file mode 100644
index 00000000000000..4c49313d97741a
--- /dev/null
+++ b/homeassistant/components/accuweather/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/translations/hu.json b/homeassistant/components/accuweather/translations/hu.json
new file mode 100644
index 00000000000000..8a0f7f5a198f95
--- /dev/null
+++ b/homeassistant/components/accuweather/translations/hu.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API kulcs",
+ "latitude": "Sz\u00e9less\u00e9g",
+ "longitude": "Hossz\u00fas\u00e1g",
+ "name": "N\u00e9v"
+ },
+ "title": "AccuWeather"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "title": "AccuWeather be\u00e1ll\u00edt\u00e1sok"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/translations/id.json b/homeassistant/components/accuweather/translations/id.json
new file mode 100644
index 00000000000000..970b3a026b7bf9
--- /dev/null
+++ b/homeassistant/components/accuweather/translations/id.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_api_key": "Kunci API tidak valid",
+ "requests_exceeded": "Jumlah permintaan yang diizinkan ke API Accuweather telah terlampaui. Anda harus menunggu atau mengubah Kunci API."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kunci API",
+ "latitude": "Lintang",
+ "longitude": "Bujur",
+ "name": "Nama"
+ },
+ "description": "Jika Anda memerlukan bantuan tentang konfigurasi, baca di sini: https://www.home-assistant.io/integrations/accuweather/\n\nBeberapa sensor tidak diaktifkan secara default. Anda dapat mengaktifkannya di registri entitas setelah konfigurasi integrasi.\nPrakiraan cuaca tidak diaktifkan secara default. Anda dapat mengaktifkannya di opsi integrasi.",
+ "title": "AccuWeather"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "forecast": "Prakiraan cuaca"
+ },
+ "description": "Karena keterbatasan versi gratis kunci API AccuWeather, ketika Anda mengaktifkan prakiraan cuaca, pembaruan data akan dilakukan setiap 80 menit, bukan setiap 40 menit.",
+ "title": "Opsi AccuWeather"
+ }
+ }
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "Keterjangkauan server AccuWeather",
+ "remaining_requests": "Sisa permintaan yang diizinkan"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/translations/it.json b/homeassistant/components/accuweather/translations/it.json
index 86aaa213a15fcd..8a1f9b964630bf 100644
--- a/homeassistant/components/accuweather/translations/it.json
+++ b/homeassistant/components/accuweather/translations/it.json
@@ -27,7 +27,7 @@
"data": {
"forecast": "Previsioni meteo"
},
- "description": "A causa delle limitazioni della versione gratuita della chiave API AccuWeather, quando si abilitano le previsioni del tempo, gli aggiornamenti dei dati verranno eseguiti ogni 64 minuti invece che ogni 32 minuti.",
+ "description": "A causa delle limitazioni della versione gratuita della chiave API AccuWeather, quando si abilitano le previsioni del tempo, gli aggiornamenti dei dati verranno eseguiti ogni 80 minuti invece che ogni 40.",
"title": "Opzioni AccuWeather"
}
}
diff --git a/homeassistant/components/accuweather/translations/ko.json b/homeassistant/components/accuweather/translations/ko.json
index b04778c8cb2aa9..d992d0bfdd4300 100644
--- a/homeassistant/components/accuweather/translations/ko.json
+++ b/homeassistant/components/accuweather/translations/ko.json
@@ -1,9 +1,41 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "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",
+ "requests_exceeded": "Accuweather API\uc5d0 \ud5c8\uc6a9\ub41c \uc694\uccad \uc218\uac00 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uae30\ub2e4\ub9ac\uac70\ub098 API \ud0a4\ub97c \ubcc0\uacbd\ud574\uc57c \ud569\ub2c8\ub2e4."
+ },
"step": {
"user": {
- "description": "\uad6c\uc131\uc5d0 \ub300\ud55c \ub3c4\uc6c0\uc774 \ud544\uc694\ud55c \uacbd\uc6b0 \ub2e4\uc74c\uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694:\nhttps://www.home-assistant.io/integrations/accuweather/\n\n\uc77c\ubd80 \uc13c\uc11c\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc5f0\ub3d9 \uad6c\uc131 \ud6c4 \uad6c\uc131\uc694\uc18c \ub808\uc9c0\uc2a4\ud2b8\ub9ac\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n\uc77c\uae30\uc608\ubcf4\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc5f0\ub3d9 \uc635\uc158\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ "data": {
+ "api_key": "API \ud0a4",
+ "latitude": "\uc704\ub3c4",
+ "longitude": "\uacbd\ub3c4",
+ "name": "\uc774\ub984"
+ },
+ "description": "\uad6c\uc131\uc5d0 \ub300\ud55c \ub3c4\uc6c0\uc774 \ud544\uc694\ud55c \uacbd\uc6b0 \ub2e4\uc74c\uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694: https://www.home-assistant.io/integrations/accuweather/\n\n\uc77c\ubd80 \uc13c\uc11c\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uad6c\uc131 \ud6c4 \uad6c\uc131\uc694\uc18c \ub808\uc9c0\uc2a4\ud2b8\ub9ac\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n\uc77c\uae30\uc608\ubcf4\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc635\uc158\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "title": "AccuWeather"
}
}
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "forecast": "\ub0a0\uc528 \uc608\ubcf4"
+ },
+ "description": "\ubb34\ub8cc \ubc84\uc804\uc758 AccuWeather API \ud0a4\ub85c \uc77c\uae30\uc608\ubcf4\ub97c \ud65c\uc131\ud654\ud55c \uacbd\uc6b0 \uc81c\ud55c\uc0ac\ud56d\uc73c\ub85c \uc778\ud574 \uc5c5\ub370\uc774\ud2b8\ub294 40 \ubd84\uc774 \uc544\ub2cc 80 \ubd84\ub9c8\ub2e4 \uc218\ud589\ub429\ub2c8\ub2e4.",
+ "title": "AccuWeather \uc635\uc158"
+ }
+ }
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "AccuWeather \uc11c\ubc84 \uc5f0\uacb0",
+ "remaining_requests": "\ub0a8\uc740 \ud5c8\uc6a9 \uc694\uccad \ud69f\uc218"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/translations/nl.json b/homeassistant/components/accuweather/translations/nl.json
index ff0d81f94d38b5..f04d93b5921f72 100644
--- a/homeassistant/components/accuweather/translations/nl.json
+++ b/homeassistant/components/accuweather/translations/nl.json
@@ -1,6 +1,10 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
+ },
"error": {
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_api_key": "API-sleutel",
"requests_exceeded": "Het toegestane aantal verzoeken aan de Accuweather API is overschreden. U moet wachten of de API-sleutel wijzigen."
},
@@ -27,5 +31,11 @@
"title": "AccuWeather-opties"
}
}
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "Kan AccuWeather server bereiken",
+ "remaining_requests": "Resterende toegestane verzoeken"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/translations/no.json b/homeassistant/components/accuweather/translations/no.json
index 50482cb3e61618..be87b1ab2447b9 100644
--- a/homeassistant/components/accuweather/translations/no.json
+++ b/homeassistant/components/accuweather/translations/no.json
@@ -27,7 +27,7 @@
"data": {
"forecast": "V\u00e6rmelding"
},
- "description": "P\u00e5 grunn av begrensningene i gratisversjonen av AccuWeather API-n\u00f8kkelen, n\u00e5r du aktiverer v\u00e6rmelding, vil dataoppdateringer bli utf\u00f8rt hvert 64. minutt i stedet for hvert 32. minutt.",
+ "description": "P\u00e5 grunn av begrensningene i den gratis versjonen av AccuWeather API-n\u00f8kkelen, vil dataoppdateringer utf\u00f8res hvert 80. minutt i stedet for hvert 40. minutt n\u00e5r du aktiverer v\u00e6rmelding.",
"title": "AccuWeather-alternativer"
}
}
diff --git a/homeassistant/components/accuweather/translations/pl.json b/homeassistant/components/accuweather/translations/pl.json
index c6e4fb3ba8242d..2794bc8b7b6c69 100644
--- a/homeassistant/components/accuweather/translations/pl.json
+++ b/homeassistant/components/accuweather/translations/pl.json
@@ -27,7 +27,7 @@
"data": {
"forecast": "Prognoza pogody"
},
- "description": "Ze wzgl\u0119du na ograniczenia darmowej wersji klucza API AccuWeather po w\u0142\u0105czeniu prognozy pogody aktualizacje danych b\u0119d\u0105 wykonywane co 64 minuty zamiast co 32 minuty.",
+ "description": "Ze wzgl\u0119du na ograniczenia darmowej wersji klucza API AccuWeather po w\u0142\u0105czeniu prognozy pogody aktualizacje danych b\u0119d\u0105 wykonywane co 80 minut zamiast co 40 minut.",
"title": "Opcje AccuWeather"
}
}
diff --git a/homeassistant/components/accuweather/translations/ru.json b/homeassistant/components/accuweather/translations/ru.json
index 6a675c1724854d..7bc767b1baf73a 100644
--- a/homeassistant/components/accuweather/translations/ru.json
+++ b/homeassistant/components/accuweather/translations/ru.json
@@ -27,7 +27,7 @@
"data": {
"forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b"
},
- "description": "\u0412 \u0441\u0432\u044f\u0437\u0438 \u0441 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u043f\u043e\u0433\u043e\u0434\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442\u044c \u043a\u0430\u0436\u0434\u044b\u0435 64 \u043c\u0438\u043d\u0443\u0442\u044b, \u0430 \u043d\u0435 \u043a\u0430\u0436\u0434\u044b\u0435 32 \u043c\u0438\u043d\u0443\u0442\u044b.",
+ "description": "\u0412 \u0441\u0432\u044f\u0437\u0438 \u0441 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u043f\u043e\u0433\u043e\u0434\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442\u044c \u043a\u0430\u0436\u0434\u044b\u0435 80 \u043c\u0438\u043d\u0443\u0442, \u0430 \u043d\u0435 \u043a\u0430\u0436\u0434\u044b\u0435 40 \u043c\u0438\u043d\u0443\u0442.",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AccuWeather"
}
}
diff --git a/homeassistant/components/accuweather/translations/sensor.hu.json b/homeassistant/components/accuweather/translations/sensor.hu.json
new file mode 100644
index 00000000000000..49f2fe41ab30a0
--- /dev/null
+++ b/homeassistant/components/accuweather/translations/sensor.hu.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "accuweather__pressure_tendency": {
+ "falling": "Cs\u00f6kken\u0151",
+ "rising": "Emelked\u0151",
+ "steady": "\u00c1lland\u00f3"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/translations/sensor.id.json b/homeassistant/components/accuweather/translations/sensor.id.json
new file mode 100644
index 00000000000000..8ce99bbc8c330a
--- /dev/null
+++ b/homeassistant/components/accuweather/translations/sensor.id.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "accuweather__pressure_tendency": {
+ "falling": "Turun",
+ "rising": "Naik",
+ "steady": "Tetap"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/translations/sensor.ko.json b/homeassistant/components/accuweather/translations/sensor.ko.json
new file mode 100644
index 00000000000000..287974fa3fdad6
--- /dev/null
+++ b/homeassistant/components/accuweather/translations/sensor.ko.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "accuweather__pressure_tendency": {
+ "falling": "\ud558\uac15",
+ "rising": "\uc0c1\uc2b9",
+ "steady": "\uc548\uc815"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/translations/sensor.uk.json b/homeassistant/components/accuweather/translations/sensor.uk.json
new file mode 100644
index 00000000000000..81243e0b05da2b
--- /dev/null
+++ b/homeassistant/components/accuweather/translations/sensor.uk.json
@@ -0,0 +1,9 @@
+{
+ "state": {
+ "accuweather__pressure_tendency": {
+ "falling": "\u0417\u043d\u0438\u0436\u0435\u043d\u043d\u044f",
+ "rising": "\u0417\u0440\u043e\u0441\u0442\u0430\u043d\u043d\u044f",
+ "steady": "\u0421\u0442\u0430\u0431\u0456\u043b\u044c\u043d\u0438\u0439"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/translations/tr.json b/homeassistant/components/accuweather/translations/tr.json
new file mode 100644
index 00000000000000..f79f9a0e3270e8
--- /dev/null
+++ b/homeassistant/components/accuweather/translations/tr.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Anahtar\u0131",
+ "latitude": "Enlem",
+ "longitude": "Boylam",
+ "name": "Ad"
+ },
+ "title": "AccuWeather"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "forecast": "Hava Durumu tahmini"
+ },
+ "title": "AccuWeather Se\u00e7enekleri"
+ }
+ }
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "AccuWeather sunucusuna ula\u015f\u0131n",
+ "remaining_requests": "Kalan izin verilen istekler"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/translations/uk.json b/homeassistant/components/accuweather/translations/uk.json
index 8c3f282b35070d..7432d0df484355 100644
--- a/homeassistant/components/accuweather/translations/uk.json
+++ b/homeassistant/components/accuweather/translations/uk.json
@@ -1,15 +1,22 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
"error": {
- "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API"
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API",
+ "requests_exceeded": "\u041f\u0435\u0440\u0435\u0432\u0438\u0449\u0435\u043d\u043e \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u0443 \u043a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0437\u0430\u043f\u0438\u0442\u0456\u0432 \u0434\u043e API Accuweather. \u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043f\u043e\u0447\u0435\u043a\u0430\u0442\u0438 \u0430\u0431\u043e \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u043a\u043b\u044e\u0447 API."
},
"step": {
"user": {
"data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
"longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430",
- "name": "\u041d\u0430\u0437\u0432\u0430 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457"
+ "name": "\u041d\u0430\u0437\u0432\u0430"
},
+ "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438, \u044f\u043a\u0449\u043e \u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u0430 \u0437 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c:\n https://www.home-assistant.io/integrations/accuweather/ \n\n\u0417\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0434\u0435\u044f\u043a\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 \u043f\u0440\u0438\u0445\u043e\u0432\u0430\u043d\u0456 \u0456 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u0438. \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0430\u043a\u0442\u0438\u0432\u0443\u0432\u0430\u0442\u0438 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0438\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432 \u0432 \u0440\u0435\u0454\u0441\u0442\u0440\u0456 \u043e\u0431'\u0454\u043a\u0442\u0456\u0432 \u0456 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u0438 \u0432 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u0445 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457.",
"title": "AccuWeather"
}
}
@@ -19,8 +26,16 @@
"user": {
"data": {
"forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u0438"
- }
+ },
+ "description": "\u0423 \u0437\u0432'\u044f\u0437\u043a\u0443 \u0437 \u043e\u0431\u043c\u0435\u0436\u0435\u043d\u043d\u044f\u043c\u0438 \u0431\u0435\u0437\u043a\u043e\u0448\u0442\u043e\u0432\u043d\u043e\u0457 \u0432\u0435\u0440\u0441\u0456\u0457 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0443 \u043f\u043e\u0433\u043e\u0434\u0438 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0434\u0430\u043d\u0438\u0445 \u0431\u0443\u0434\u0435 \u0432\u0456\u0434\u0431\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u043a\u043e\u0436\u043d\u0456 64 \u0445\u0432\u0438\u043b\u0438\u043d\u0438, \u0430 \u043d\u0435 \u043a\u043e\u0436\u043d\u0456 32 \u0445\u0432\u0438\u043b\u0438\u043d\u0438.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AccuWeather"
}
}
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 AccuWeather",
+ "remaining_requests": "\u0417\u0430\u043f\u0438\u0442\u0456\u0432 \u0437\u0430\u043b\u0438\u0448\u0438\u043b\u043e\u0441\u044c"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/translations/zh-Hant.json b/homeassistant/components/accuweather/translations/zh-Hant.json
index ed5fa26f0c002b..eb3729fd2c495a 100644
--- a/homeassistant/components/accuweather/translations/zh-Hant.json
+++ b/homeassistant/components/accuweather/translations/zh-Hant.json
@@ -27,7 +27,7 @@
"data": {
"forecast": "\u5929\u6c23\u9810\u5831"
},
- "description": "\u7531\u65bc AccuWeather API \u5bc6\u9470\u514d\u8cbb\u7248\u672c\u9650\u5236\uff0c\u7576\u958b\u555f\u5929\u6c23\u9810\u5831\u6642\u3001\u6578\u64da\u6703\u6bcf 64 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\uff0c\u800c\u975e 32 \u5206\u9418\u3002",
+ "description": "\u7531\u65bc AccuWeather API \u5bc6\u9470\u514d\u8cbb\u7248\u672c\u9650\u5236\uff0c\u7576\u958b\u555f\u5929\u6c23\u9810\u5831\u6642\u3001\u6578\u64da\u6703\u6bcf 80 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\uff0c\u800c\u975e 40 \u5206\u9418\u3002",
"title": "AccuWeather \u9078\u9805"
}
}
diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py
index f947f3fe0c080e..4a61ec793dbb8d 100644
--- a/homeassistant/components/acer_projector/switch.py
+++ b/homeassistant/components/acer_projector/switch.py
@@ -9,6 +9,7 @@
from homeassistant.const import (
CONF_FILENAME,
CONF_NAME,
+ CONF_TIMEOUT,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
@@ -17,7 +18,6 @@
_LOGGER = logging.getLogger(__name__)
-CONF_TIMEOUT = "timeout"
CONF_WRITE_TIMEOUT = "write_timeout"
DEFAULT_NAME = "Acer Projector"
@@ -74,7 +74,6 @@ class AcerSwitch(SwitchEntity):
def __init__(self, serial_port, name, timeout, write_timeout, **kwargs):
"""Init of the Acer projector."""
-
self.ser = serial.Serial(
port=serial_port, timeout=timeout, write_timeout=write_timeout, **kwargs
)
@@ -90,7 +89,6 @@ def __init__(self, serial_port, name, timeout, write_timeout, **kwargs):
def _write_read(self, msg):
"""Write to the projector and read the return."""
-
ret = ""
# Sometimes the projector won't answer for no reason or the projector
# was disconnected during runtime.
@@ -134,7 +132,7 @@ def is_on(self):
return self._state
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return state attributes."""
return self._attributes
diff --git a/homeassistant/components/acmeda/__init__.py b/homeassistant/components/acmeda/__init__.py
index 3b4f135a6fd937..926208fba40d5e 100644
--- a/homeassistant/components/acmeda/__init__.py
+++ b/homeassistant/components/acmeda/__init__.py
@@ -11,11 +11,6 @@
PLATFORMS = ["cover", "sensor"]
-async def async_setup(hass: core.HomeAssistant, config: dict):
- """Set up the Rollease Acmeda Automate component."""
- return True
-
-
async def async_setup_entry(
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
):
@@ -28,9 +23,9 @@ async def async_setup_entry(
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = hub
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return True
@@ -45,8 +40,8 @@ async def async_unload_entry(
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/acmeda/base.py b/homeassistant/components/acmeda/base.py
index b325e2c944afaf..15f9716db47ef8 100644
--- a/homeassistant/components/acmeda/base.py
+++ b/homeassistant/components/acmeda/base.py
@@ -32,7 +32,7 @@ async def async_remove_and_unregister(self):
device.id, remove_config_entry_id=self.registry_entry.config_entry_id
)
- await self.async_remove()
+ await self.async_remove(force_remove=True)
async def async_added_to_hass(self):
"""Entity has been added to hass."""
diff --git a/homeassistant/components/acmeda/config_flow.py b/homeassistant/components/acmeda/config_flow.py
index f421fa9ca25329..70935b7086961b 100644
--- a/homeassistant/components/acmeda/config_flow.py
+++ b/homeassistant/components/acmeda/config_flow.py
@@ -1,6 +1,8 @@
"""Config flow for Rollease Acmeda Automate Pulse Hub."""
+from __future__ import annotations
+
import asyncio
-from typing import Dict, Optional
+from contextlib import suppress
import aiopulse
import async_timeout
@@ -8,7 +10,7 @@
from homeassistant import config_entries
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import DOMAIN
class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@@ -19,7 +21,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self):
"""Initialize the config flow."""
- self.discovered_hubs: Optional[Dict[str, aiopulse.Hub]] = None
+ self.discovered_hubs: dict[str, aiopulse.Hub] | None = None
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
@@ -36,15 +38,13 @@ async def async_step_user(self, user_input=None):
}
hubs = []
- try:
- with async_timeout.timeout(5):
+ with suppress(asyncio.TimeoutError):
+ async with async_timeout.timeout(5):
async for hub in aiopulse.Hub.discover():
if hub.id not in already_configured:
hubs.append(hub)
- except asyncio.TimeoutError:
- pass
- if len(hubs) == 0:
+ if not hubs:
return self.async_abort(reason="no_devices_found")
if len(hubs) == 1:
diff --git a/homeassistant/components/acmeda/hub.py b/homeassistant/components/acmeda/hub.py
index 0b74b874dcc838..e156ee5cb7812e 100644
--- a/homeassistant/components/acmeda/hub.py
+++ b/homeassistant/components/acmeda/hub.py
@@ -1,6 +1,7 @@
"""Code to handle a Pulse Hub."""
+from __future__ import annotations
+
import asyncio
-from typing import Optional
import aiopulse
@@ -17,7 +18,7 @@ def __init__(self, hass, config_entry):
"""Initialize the system."""
self.config_entry = config_entry
self.hass = hass
- self.api: Optional[aiopulse.Hub] = None
+ self.api: aiopulse.Hub | None = None
self.tasks = []
self.current_rollers = {}
self.cleanup_callbacks = []
diff --git a/homeassistant/components/acmeda/sensor.py b/homeassistant/components/acmeda/sensor.py
index f427548ab94f95..4f617c5726fdba 100644
--- a/homeassistant/components/acmeda/sensor.py
+++ b/homeassistant/components/acmeda/sensor.py
@@ -1,4 +1,5 @@
"""Support for Acmeda Roller Blind Batteries."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -29,7 +30,7 @@ def async_add_acmeda_sensors():
)
-class AcmedaBattery(AcmedaBase):
+class AcmedaBattery(AcmedaBase, SensorEntity):
"""Representation of a Acmeda cover device."""
device_class = DEVICE_CLASS_BATTERY
diff --git a/homeassistant/components/acmeda/translations/de.json b/homeassistant/components/acmeda/translations/de.json
index 86b22e47cda95a..94834cde42768a 100644
--- a/homeassistant/components/acmeda/translations/de.json
+++ b/homeassistant/components/acmeda/translations/de.json
@@ -1,11 +1,14 @@
{
"config": {
+ "abort": {
+ "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden"
+ },
"step": {
"user": {
"data": {
"id": "Host-ID"
},
- "title": "W\u00e4hlen Sie einen Hub zum Hinzuf\u00fcgen aus"
+ "title": "W\u00e4hle einen Hub zum Hinzuf\u00fcgen aus"
}
}
}
diff --git a/homeassistant/components/acmeda/translations/hu.json b/homeassistant/components/acmeda/translations/hu.json
new file mode 100644
index 00000000000000..6105977de80fd4
--- /dev/null
+++ b/homeassistant/components/acmeda/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/acmeda/translations/id.json b/homeassistant/components/acmeda/translations/id.json
new file mode 100644
index 00000000000000..6e80d134f5a325
--- /dev/null
+++ b/homeassistant/components/acmeda/translations/id.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "ID Host"
+ },
+ "title": "Pilih hub untuk ditambahkan"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/acmeda/translations/ko.json b/homeassistant/components/acmeda/translations/ko.json
index 345628eef02a62..098d3a952f5516 100644
--- a/homeassistant/components/acmeda/translations/ko.json
+++ b/homeassistant/components/acmeda/translations/ko.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/acmeda/translations/nl.json b/homeassistant/components/acmeda/translations/nl.json
index 470e0f8f6982f2..aac926ec0481ce 100644
--- a/homeassistant/components/acmeda/translations/nl.json
+++ b/homeassistant/components/acmeda/translations/nl.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "no_devices_found": "Geen apparaten gevonden op het netwerk"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/acmeda/translations/tr.json b/homeassistant/components/acmeda/translations/tr.json
new file mode 100644
index 00000000000000..aea81abdcba0ca
--- /dev/null
+++ b/homeassistant/components/acmeda/translations/tr.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "id": "Ana bilgisayar kimli\u011fi"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/acmeda/translations/uk.json b/homeassistant/components/acmeda/translations/uk.json
new file mode 100644
index 00000000000000..245428e9c732ba
--- /dev/null
+++ b/homeassistant/components/acmeda/translations/uk.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0445\u043e\u0441\u0442\u0430"
+ },
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0445\u0430\u0431, \u044f\u043a\u0438\u0439 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0434\u043e\u0434\u0430\u0442\u0438"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/actiontec/device_tracker.py b/homeassistant/components/actiontec/device_tracker.py
index e3fdeaf35f28fe..c88ed546b9d060 100644
--- a/homeassistant/components/actiontec/device_tracker.py
+++ b/homeassistant/components/actiontec/device_tracker.py
@@ -53,7 +53,7 @@ def __init__(self, config):
self.last_results = []
data = self.get_actiontec_data()
self.success_init = data is not None
- _LOGGER.info("canner initialized")
+ _LOGGER.info("Scanner initialized")
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py
index 71dff2ab6eed86..0f10d20ec593f4 100644
--- a/homeassistant/components/adguard/__init__.py
+++ b/homeassistant/components/adguard/__init__.py
@@ -1,6 +1,8 @@
"""Support for AdGuard Home."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict
+from typing import Any
from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError
import voluptuous as vol
@@ -27,11 +29,11 @@
CONF_USERNAME,
CONF_VERIFY_SSL,
)
+from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.typing import ConfigType, HomeAssistantType
_LOGGER = logging.getLogger(__name__)
@@ -43,13 +45,10 @@
{vol.Optional(CONF_FORCE, default=False): cv.boolean}
)
-
-async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
- """Set up the AdGuard Home components."""
- return True
+PLATFORMS = ["sensor", "switch"]
-async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up AdGuard Home from a config entry."""
session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL])
adguard = AdGuardHome(
@@ -69,32 +68,36 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
except AdGuardHomeConnectionError as exception:
raise ConfigEntryNotReady from exception
- for component in "sensor", "switch":
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
async def add_url(call) -> None:
"""Service call to add a new filter subscription to AdGuard Home."""
await adguard.filtering.add_url(
- call.data.get(CONF_NAME), call.data.get(CONF_URL)
+ allowlist=False, name=call.data.get(CONF_NAME), url=call.data.get(CONF_URL)
)
async def remove_url(call) -> None:
"""Service call to remove a filter subscription from AdGuard Home."""
- await adguard.filtering.remove_url(call.data.get(CONF_URL))
+ await adguard.filtering.remove_url(allowlist=False, url=call.data.get(CONF_URL))
async def enable_url(call) -> None:
"""Service call to enable a filter subscription in AdGuard Home."""
- await adguard.filtering.enable_url(call.data.get(CONF_URL))
+ await adguard.filtering.enable_url(allowlist=False, url=call.data.get(CONF_URL))
async def disable_url(call) -> None:
"""Service call to disable a filter subscription in AdGuard Home."""
- await adguard.filtering.disable_url(call.data.get(CONF_URL))
+ await adguard.filtering.disable_url(
+ allowlist=False, url=call.data.get(CONF_URL)
+ )
async def refresh(call) -> None:
"""Service call to refresh the filter subscriptions in AdGuard Home."""
- await adguard.filtering.refresh(call.data.get(CONF_FORCE))
+ await adguard.filtering.refresh(
+ allowlist=False, force=call.data.get(CONF_FORCE)
+ )
hass.services.async_register(
DOMAIN, SERVICE_ADD_URL, add_url, schema=SERVICE_ADD_URL_SCHEMA
@@ -115,7 +118,7 @@ async def refresh(call) -> None:
return True
-async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool:
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload AdGuard Home config entry."""
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL)
@@ -123,8 +126,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool
hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL)
hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
- for component in "sensor", "switch":
- await hass.config_entries.async_forward_entry_unload(entry, component)
+ for platform in PLATFORMS:
+ await hass.config_entries.async_forward_entry_unload(entry, platform)
del hass.data[DOMAIN]
@@ -189,7 +192,7 @@ class AdGuardHomeDeviceEntity(AdGuardHomeEntity):
"""Defines a AdGuard Home device entity."""
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about this AdGuard Home instance."""
return {
"identifiers": {
diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py
index d728eed3003c4a..d5ec79d788f4a5 100644
--- a/homeassistant/components/adguard/config_flow.py
+++ b/homeassistant/components/adguard/config_flow.py
@@ -1,9 +1,12 @@
"""Config flow to configure the AdGuard Home integration."""
+from __future__ import annotations
+
+from typing import Any
+
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
import voluptuous as vol
from homeassistant import config_entries
-from homeassistant.components.adguard.const import DOMAIN
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import (
CONF_HOST,
@@ -15,9 +18,10 @@
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from .const import DOMAIN
+
-@config_entries.HANDLERS.register(DOMAIN)
-class AdGuardHomeFlowHandler(ConfigFlow):
+class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a AdGuard Home config flow."""
VERSION = 1
@@ -25,7 +29,9 @@ class AdGuardHomeFlowHandler(ConfigFlow):
_hassio_discovery = None
- async def _show_setup_form(self, errors=None):
+ async def _show_setup_form(
+ self, errors: dict[str, str] | None = None
+ ) -> dict[str, Any]:
"""Show the setup form to the user."""
return self.async_show_form(
step_id="user",
@@ -42,7 +48,9 @@ async def _show_setup_form(self, errors=None):
errors=errors or {},
)
- async def _show_hassio_form(self, errors=None):
+ async def _show_hassio_form(
+ self, errors: dict[str, str] | None = None
+ ) -> dict[str, Any]:
"""Show the Hass.io confirmation form to the user."""
return self.async_show_form(
step_id="hassio_confirm",
@@ -51,7 +59,9 @@ async def _show_hassio_form(self, errors=None):
errors=errors or {},
)
- async def async_step_user(self, user_input=None):
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initiated by the user."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
@@ -91,7 +101,7 @@ async def async_step_user(self, user_input=None):
},
)
- async def async_step_hassio(self, discovery_info):
+ async def async_step_hassio(self, discovery_info: dict[str, Any]) -> dict[str, Any]:
"""Prepare configuration for a Hass.io AdGuard Home add-on.
This flow is triggered by the discovery component.
@@ -100,6 +110,7 @@ async def async_step_hassio(self, discovery_info):
if not entries:
self._hassio_discovery = discovery_info
+ await self._async_handle_discovery_without_unique_id()
return await self.async_step_hassio_confirm()
cur_entry = entries[0]
@@ -129,7 +140,9 @@ async def async_step_hassio(self, discovery_info):
return self.async_abort(reason="existing_instance_updated")
- async def async_step_hassio_confirm(self, user_input=None):
+ async def async_step_hassio_confirm(
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Confirm Hass.io discovery."""
if user_input is None:
return await self._show_hassio_form()
diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json
index 0bcd25569a5a9c..dd23e56136403a 100644
--- a/homeassistant/components/adguard/manifest.json
+++ b/homeassistant/components/adguard/manifest.json
@@ -3,6 +3,6 @@
"name": "AdGuard Home",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/adguard",
- "requirements": ["adguardhome==0.4.2"],
+ "requirements": ["adguardhome==0.5.0"],
"codeowners": ["@frenck"]
}
diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py
index 05e23ba8b80dfa..dd0400b6592608 100644
--- a/homeassistant/components/adguard/sensor.py
+++ b/homeassistant/components/adguard/sensor.py
@@ -1,25 +1,29 @@
"""Support for AdGuard Home sensors."""
+from __future__ import annotations
+
from datetime import timedelta
+from typing import Callable
-from adguardhome import AdGuardHomeConnectionError
+from adguardhome import AdGuardHome, AdGuardHomeConnectionError
-from homeassistant.components.adguard import AdGuardHomeDeviceEntity
-from homeassistant.components.adguard.const import (
- DATA_ADGUARD_CLIENT,
- DATA_ADGUARD_VERION,
- DOMAIN,
-)
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, TIME_MILLISECONDS
+from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.entity import Entity
+
+from . import AdGuardHomeDeviceEntity
+from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN
SCAN_INTERVAL = timedelta(seconds=300)
PARALLEL_UPDATES = 4
async def async_setup_entry(
- hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up AdGuard Home sensor based on a config entry."""
adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT]
@@ -45,12 +49,12 @@ async def async_setup_entry(
async_add_entities(sensors, True)
-class AdGuardHomeSensor(AdGuardHomeDeviceEntity):
+class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity):
"""Defines a AdGuard Home sensor."""
def __init__(
self,
- adguard,
+ adguard: AdGuardHome,
name: str,
icon: str,
measurement: str,
@@ -78,12 +82,12 @@ def unique_id(self) -> str:
)
@property
- def state(self):
+ def state(self) -> str | None:
"""Return the state of the sensor."""
return self._state
@property
- def unit_of_measurement(self) -> str:
+ def unit_of_measurement(self) -> str | None:
"""Return the unit this state is expressed in."""
return self._unit_of_measurement
@@ -91,7 +95,7 @@ def unit_of_measurement(self) -> str:
class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor):
"""Defines a AdGuard Home DNS Queries sensor."""
- def __init__(self, adguard):
+ def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home sensor."""
super().__init__(
adguard, "AdGuard DNS Queries", "mdi:magnify", "dns_queries", "queries"
@@ -105,7 +109,7 @@ async def _adguard_update(self) -> None:
class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor):
"""Defines a AdGuard Home blocked by filtering sensor."""
- def __init__(self, adguard):
+ def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home sensor."""
super().__init__(
adguard,
@@ -124,7 +128,7 @@ async def _adguard_update(self) -> None:
class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor):
"""Defines a AdGuard Home blocked percentage sensor."""
- def __init__(self, adguard):
+ def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home sensor."""
super().__init__(
adguard,
@@ -143,7 +147,7 @@ async def _adguard_update(self) -> None:
class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor):
"""Defines a AdGuard Home replaced by parental control sensor."""
- def __init__(self, adguard):
+ def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home sensor."""
super().__init__(
adguard,
@@ -161,7 +165,7 @@ async def _adguard_update(self) -> None:
class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor):
"""Defines a AdGuard Home replaced by safe browsing sensor."""
- def __init__(self, adguard):
+ def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home sensor."""
super().__init__(
adguard,
@@ -179,7 +183,7 @@ async def _adguard_update(self) -> None:
class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor):
"""Defines a AdGuard Home replaced by safe search sensor."""
- def __init__(self, adguard):
+ def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home sensor."""
super().__init__(
adguard,
@@ -197,7 +201,7 @@ async def _adguard_update(self) -> None:
class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor):
"""Defines a AdGuard Home average processing time sensor."""
- def __init__(self, adguard):
+ def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home sensor."""
super().__init__(
adguard,
@@ -216,7 +220,7 @@ async def _adguard_update(self) -> None:
class AdGuardHomeRulesCountSensor(AdGuardHomeSensor):
"""Defines a AdGuard Home rules count sensor."""
- def __init__(self, adguard):
+ def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home sensor."""
super().__init__(
adguard,
@@ -229,4 +233,4 @@ def __init__(self, adguard):
async def _adguard_update(self) -> None:
"""Update AdGuard Home entity."""
- self._state = await self.adguard.filtering.rules_count()
+ self._state = await self.adguard.filtering.rules_count(allowlist=False)
diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json
index 2d4bb49304fbe5..4e6a63cfd3a258 100644
--- a/homeassistant/components/adguard/strings.json
+++ b/homeassistant/components/adguard/strings.json
@@ -13,8 +13,8 @@
}
},
"hassio_confirm": {
- "title": "AdGuard Home via Hass.io add-on",
- "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the Hass.io add-on: {addon}?"
+ "title": "AdGuard Home via Home Assistant add-on",
+ "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the add-on: {addon}?"
}
},
"error": {
diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py
index 44aab11573d04a..0b127a280cfad5 100644
--- a/homeassistant/components/adguard/switch.py
+++ b/homeassistant/components/adguard/switch.py
@@ -1,19 +1,20 @@
"""Support for AdGuard Home switches."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
+from typing import Callable
-from adguardhome import AdGuardHomeConnectionError, AdGuardHomeError
+from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError
-from homeassistant.components.adguard import AdGuardHomeDeviceEntity
-from homeassistant.components.adguard.const import (
- DATA_ADGUARD_CLIENT,
- DATA_ADGUARD_VERION,
- DOMAIN,
-)
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.entity import Entity
+
+from . import AdGuardHomeDeviceEntity
+from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -22,7 +23,9 @@
async def async_setup_entry(
- hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up AdGuard Home switch based on a config entry."""
adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT]
@@ -49,8 +52,13 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity):
"""Defines a AdGuard Home switch."""
def __init__(
- self, adguard, name: str, icon: str, key: str, enabled_default: bool = True
- ):
+ self,
+ adguard: AdGuardHome,
+ name: str,
+ icon: str,
+ key: str,
+ enabled_default: bool = True,
+ ) -> None:
"""Initialize AdGuard Home switch."""
self._state = False
self._key = key
@@ -96,7 +104,7 @@ async def _adguard_turn_on(self) -> None:
class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch):
"""Defines a AdGuard Home protection switch."""
- def __init__(self, adguard) -> None:
+ def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home switch."""
super().__init__(
adguard, "AdGuard Protection", "mdi:shield-check", "protection"
@@ -118,7 +126,7 @@ async def _adguard_update(self) -> None:
class AdGuardHomeParentalSwitch(AdGuardHomeSwitch):
"""Defines a AdGuard Home parental control switch."""
- def __init__(self, adguard) -> None:
+ def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home switch."""
super().__init__(
adguard, "AdGuard Parental Control", "mdi:shield-check", "parental"
@@ -140,7 +148,7 @@ async def _adguard_update(self) -> None:
class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch):
"""Defines a AdGuard Home safe search switch."""
- def __init__(self, adguard) -> None:
+ def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home switch."""
super().__init__(
adguard, "AdGuard Safe Search", "mdi:shield-check", "safesearch"
@@ -162,7 +170,7 @@ async def _adguard_update(self) -> None:
class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch):
"""Defines a AdGuard Home safe search switch."""
- def __init__(self, adguard) -> None:
+ def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home switch."""
super().__init__(
adguard, "AdGuard Safe Browsing", "mdi:shield-check", "safebrowsing"
@@ -184,7 +192,7 @@ async def _adguard_update(self) -> None:
class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch):
"""Defines a AdGuard Home filtering switch."""
- def __init__(self, adguard) -> None:
+ def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home switch."""
super().__init__(adguard, "AdGuard Filtering", "mdi:shield-check", "filtering")
@@ -204,7 +212,7 @@ async def _adguard_update(self) -> None:
class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch):
"""Defines a AdGuard Home query log switch."""
- def __init__(self, adguard) -> None:
+ def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home switch."""
super().__init__(
adguard,
diff --git a/homeassistant/components/adguard/translations/bg.json b/homeassistant/components/adguard/translations/bg.json
index 1edfc9d8da610f..82c658e7c15b30 100644
--- a/homeassistant/components/adguard/translations/bg.json
+++ b/homeassistant/components/adguard/translations/bg.json
@@ -6,8 +6,8 @@
},
"step": {
"hassio_confirm": {
- "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0437\u0432\u0430 \u0441 AdGuard Home, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 Hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430: {addon} ?",
- "title": "AdGuard Home \u0447\u0440\u0435\u0437 Hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430"
+ "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0437\u0432\u0430 \u0441 AdGuard Home, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430: {addon} ?",
+ "title": "AdGuard Home \u0447\u0440\u0435\u0437 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430"
},
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/ca.json b/homeassistant/components/adguard/translations/ca.json
index 422fde9479a70b..0c7057a67eef47 100644
--- a/homeassistant/components/adguard/translations/ca.json
+++ b/homeassistant/components/adguard/translations/ca.json
@@ -9,8 +9,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb l'AdGuard Home proporcionat pel complement de Hass.io: {addon}?",
- "title": "AdGuard Home (complement de Hass.io)"
+ "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb l'AdGuard Home proporcionat pel complement: {addon}?",
+ "title": "AdGuard Home via complement de Home Assistant"
},
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/cs.json b/homeassistant/components/adguard/translations/cs.json
index 27b9d291fc26f7..00531088a08f12 100644
--- a/homeassistant/components/adguard/translations/cs.json
+++ b/homeassistant/components/adguard/translations/cs.json
@@ -9,8 +9,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k AddGuard pomoc\u00ed hass.io {addon}?",
- "title": "AdGuard prost\u0159ednictv\u00edm dopl\u0148ku Hass.io"
+ "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k AddGuard pomoc\u00ed Supervisor {addon}?",
+ "title": "AdGuard prost\u0159ednictv\u00edm dopl\u0148ku Supervisor"
},
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/da.json b/homeassistant/components/adguard/translations/da.json
index 927fd03d50a88e..79a1937eba8263 100644
--- a/homeassistant/components/adguard/translations/da.json
+++ b/homeassistant/components/adguard/translations/da.json
@@ -6,8 +6,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home leveret af Hass.io-tilf\u00f8jelsen: {addon}?",
- "title": "AdGuard Home via Hass.io-tilf\u00f8jelse"
+ "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home leveret af Supervisor-tilf\u00f8jelsen: {addon}?",
+ "title": "AdGuard Home via Supervisor-tilf\u00f8jelse"
},
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/de.json b/homeassistant/components/adguard/translations/de.json
index a02601759be9ab..2731f3f7ebae4b 100644
--- a/homeassistant/components/adguard/translations/de.json
+++ b/homeassistant/components/adguard/translations/de.json
@@ -2,15 +2,15 @@
"config": {
"abort": {
"existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert.",
- "single_instance_allowed": "Es ist nur eine einzige Konfiguration von AdGuard Home zul\u00e4ssig."
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"step": {
"hassio_confirm": {
- "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit AdGuard Home als Hass.io-Add-On hergestellt wird: {addon}?",
- "title": "AdGuard Home \u00fcber das Hass.io Add-on"
+ "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit AdGuard Home als Supervisor-Add-On hergestellt wird: {addon}?",
+ "title": "AdGuard Home \u00fcber das Supervisor Add-on"
},
"user": {
"data": {
@@ -19,7 +19,7 @@
"port": "Port",
"ssl": "AdGuard Home verwendet ein SSL-Zertifikat",
"username": "Benutzername",
- "verify_ssl": "AdGuard Home verwendet ein richtiges Zertifikat"
+ "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen"
},
"description": "Richte deine AdGuard Home-Instanz ein um sie zu \u00dcberwachen und zu Steuern."
}
diff --git a/homeassistant/components/adguard/translations/en.json b/homeassistant/components/adguard/translations/en.json
index 6c1ad2008cea6e..5e09b42b9f2cc9 100644
--- a/homeassistant/components/adguard/translations/en.json
+++ b/homeassistant/components/adguard/translations/en.json
@@ -9,8 +9,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the Hass.io add-on: {addon}?",
- "title": "AdGuard Home via Hass.io add-on"
+ "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the add-on: {addon}?",
+ "title": "AdGuard Home via Home Assistant add-on"
},
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/es-419.json b/homeassistant/components/adguard/translations/es-419.json
index 5efdfae180204f..8fac53b61ab178 100644
--- a/homeassistant/components/adguard/translations/es-419.json
+++ b/homeassistant/components/adguard/translations/es-419.json
@@ -6,8 +6,8 @@
},
"step": {
"hassio_confirm": {
- "description": "\u00bfDesea configurar Home Assistant para conectarse a la p\u00e1gina principal de AdGuard proporcionada por el complemento Hass.io: {addon}?",
- "title": "AdGuard Home a trav\u00e9s del complemento Hass.io"
+ "description": "\u00bfDesea configurar Home Assistant para conectarse a la p\u00e1gina principal de AdGuard proporcionada por el complemento Supervisor: {addon}?",
+ "title": "AdGuard Home a trav\u00e9s del complemento Supervisor"
},
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/es.json b/homeassistant/components/adguard/translations/es.json
index a165a9b1c09e7f..3ffdb6b9eb0b89 100644
--- a/homeassistant/components/adguard/translations/es.json
+++ b/homeassistant/components/adguard/translations/es.json
@@ -9,8 +9,8 @@
},
"step": {
"hassio_confirm": {
- "description": "\u00bfDesea configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento Hass.io: {addon} ?",
- "title": "AdGuard Home a trav\u00e9s del complemento Hass.io"
+ "description": "\u00bfDesea configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento Supervisor: {addon} ?",
+ "title": "AdGuard Home a trav\u00e9s del complemento Supervisor"
},
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/et.json b/homeassistant/components/adguard/translations/et.json
index 3408d752522293..18e67dedb36584 100644
--- a/homeassistant/components/adguard/translations/et.json
+++ b/homeassistant/components/adguard/translations/et.json
@@ -9,8 +9,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Kas soovid seadistada Home Assistant-i \u00fchenduse AdGuard Home'iga mida pakub Hass.io lisandmoodul: {addon} ?",
- "title": "AdGuard Home Hass.io pistikprogrammi kaudu"
+ "description": "Kas soovid seadistada Home Assistant-i \u00fchenduse AdGuard Home'iga mida pakub lisandmoodul: {addon} ?",
+ "title": "AdGuard Home Home Assistanti lisandmooduli abil"
},
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/he.json b/homeassistant/components/adguard/translations/he.json
index 49c18fac88c046..1471fd6603b017 100644
--- a/homeassistant/components/adguard/translations/he.json
+++ b/homeassistant/components/adguard/translations/he.json
@@ -4,6 +4,7 @@
"user": {
"data": {
"host": "Host",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
"port": "\u05e4\u05d5\u05e8\u05d8"
}
}
diff --git a/homeassistant/components/adguard/translations/hu.json b/homeassistant/components/adguard/translations/hu.json
index 3f67c7658507cb..3813fae8f3c4c4 100644
--- a/homeassistant/components/adguard/translations/hu.json
+++ b/homeassistant/components/adguard/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
@@ -9,7 +12,9 @@
"host": "Hoszt",
"password": "Jelsz\u00f3",
"port": "Port",
- "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v",
+ "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se"
}
}
}
diff --git a/homeassistant/components/adguard/translations/id.json b/homeassistant/components/adguard/translations/id.json
index c5d61d91df0498..d2e36cfe5b9938 100644
--- a/homeassistant/components/adguard/translations/id.json
+++ b/homeassistant/components/adguard/translations/id.json
@@ -1,10 +1,27 @@
{
"config": {
+ "abort": {
+ "existing_instance_updated": "Memperbarui konfigurasi yang ada.",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
"step": {
+ "hassio_confirm": {
+ "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke AdGuard Home yang disediakan oleh add-on Supervisor {addon}?",
+ "title": "AdGuard Home melalui add-on Home Assistant"
+ },
"user": {
"data": {
- "password": "Kata sandi"
- }
+ "host": "Host",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "ssl": "Menggunakan sertifikat SSL",
+ "username": "Nama Pengguna",
+ "verify_ssl": "Verifikasi sertifikat SSL"
+ },
+ "description": "Siapkan instans AdGuard Home Anda untuk pemantauan dan kontrol."
}
}
}
diff --git a/homeassistant/components/adguard/translations/it.json b/homeassistant/components/adguard/translations/it.json
index 3df01316aa15da..cbafb68a834506 100644
--- a/homeassistant/components/adguard/translations/it.json
+++ b/homeassistant/components/adguard/translations/it.json
@@ -9,8 +9,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Vuoi configurare Home Assistant per connettersi alla AdGuard Home fornita dal componente aggiuntivo di Hass.io: {addon}?",
- "title": "AdGuard Home tramite il componente aggiuntivo di Hass.io"
+ "description": "Vuoi configurare Home Assistant per connettersi ad AdGuard Home fornito dal componente aggiuntivo: {addon}?",
+ "title": "AdGuard Home tramite il componente aggiuntivo di Home Assistant"
},
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/ko.json b/homeassistant/components/adguard/translations/ko.json
index e17bca1f0a2509..6b1917cf73b2c2 100644
--- a/homeassistant/components/adguard/translations/ko.json
+++ b/homeassistant/components/adguard/translations/ko.json
@@ -2,21 +2,24 @@
"config": {
"abort": {
"existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.",
- "single_instance_allowed": "\ud558\ub098\uc758 AdGuard Home \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"hassio_confirm": {
- "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c AdGuard Home \uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
- "title": "Hass.io \uc560\ub4dc\uc628\uc758 AdGuard Home"
+ "description": "{addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 AdGuard Home\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "Home Assistant \uc560\ub4dc\uc628\uc758 AdGuard Home"
},
"user": {
"data": {
"host": "\ud638\uc2a4\ud2b8",
"password": "\ube44\ubc00\ubc88\ud638",
"port": "\ud3ec\ud2b8",
- "ssl": "AdGuard Home \uc740 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4",
+ "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9",
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984",
- "verify_ssl": "AdGuard Home \uc740 \uc62c\ubc14\ub978 \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4"
+ "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778"
},
"description": "\ubaa8\ub2c8\ud130\ub9c1 \ubc0f \uc81c\uc5b4\uac00 \uac00\ub2a5\ud558\ub3c4\ub85d AdGuard Home \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694."
}
diff --git a/homeassistant/components/adguard/translations/lb.json b/homeassistant/components/adguard/translations/lb.json
index 135451c061f2d1..ae7e6ad99be648 100644
--- a/homeassistant/components/adguard/translations/lb.json
+++ b/homeassistant/components/adguard/translations/lb.json
@@ -9,8 +9,8 @@
},
"step": {
"hassio_confirm": {
- "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam AdGuard Home ze verbannen dee vum hass.io add-on {addon} bereet gestallt g\u00ebtt?",
- "title": "AdGuard Home via Hass.io add-on"
+ "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam AdGuard Home ze verbannen dee vum Supervisor add-on {addon} bereet gestallt g\u00ebtt?",
+ "title": "AdGuard Home via Supervisor add-on"
},
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/nl.json b/homeassistant/components/adguard/translations/nl.json
index 4c735333932aab..a1bfaad6e05a13 100644
--- a/homeassistant/components/adguard/translations/nl.json
+++ b/homeassistant/components/adguard/translations/nl.json
@@ -9,8 +9,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Wilt u Home Assistant configureren om verbinding te maken met AdGuard Home van de Hass.io-add-on: {addon}?",
- "title": "AdGuard Home via Hass.io add-on"
+ "description": "Wilt u Home Assistant configureren om verbinding te maken met AdGuard Home van de Home Assistant add-on: {addon}?",
+ "title": "AdGuard Home via Home Assistant add-on"
},
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/no.json b/homeassistant/components/adguard/translations/no.json
index f5aeea990c30c0..a35bfb181d6248 100644
--- a/homeassistant/components/adguard/translations/no.json
+++ b/homeassistant/components/adguard/translations/no.json
@@ -9,8 +9,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Vil du konfigurere Home Assistant til \u00e5 koble til AdGuard Hjem gitt av hass.io tillegget {addon}?",
- "title": "AdGuard Hjem via Hass.io tillegg"
+ "description": "Vil du konfigurere Home Assistant til \u00e5 koble til AdGuard Home levert av tillegget: {addon} ?",
+ "title": "AdGuard Home via Home Assistant-tillegg"
},
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/pl.json b/homeassistant/components/adguard/translations/pl.json
index 41cb2c019dd3d6..50f442d793718f 100644
--- a/homeassistant/components/adguard/translations/pl.json
+++ b/homeassistant/components/adguard/translations/pl.json
@@ -9,8 +9,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek Hass.io {addon}?",
- "title": "AdGuard Home przez dodatek Hass.io"
+ "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek {addon}?",
+ "title": "AdGuard Home przez dodatek Home Assistant"
},
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/pt-BR.json b/homeassistant/components/adguard/translations/pt-BR.json
index ae8899bb1a775d..959c7ba3638e10 100644
--- a/homeassistant/components/adguard/translations/pt-BR.json
+++ b/homeassistant/components/adguard/translations/pt-BR.json
@@ -6,8 +6,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Deseja configurar o Home Assistant para se conectar ao AdGuard Home fornecido pelo complemento Hass.io: {addon} ?",
- "title": "AdGuard Home via add-on Hass.io"
+ "description": "Deseja configurar o Home Assistant para se conectar ao AdGuard Home fornecido pelo complemento Supervisor: {addon} ?",
+ "title": "AdGuard Home via add-on Supervisor"
},
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/pt.json b/homeassistant/components/adguard/translations/pt.json
index 5d8abfc9f56f2f..a7e494936b86be 100644
--- a/homeassistant/components/adguard/translations/pt.json
+++ b/homeassistant/components/adguard/translations/pt.json
@@ -8,7 +8,7 @@
},
"step": {
"hassio_confirm": {
- "title": "AdGuard Home via Hass.io add-on"
+ "title": "AdGuard Home via Supervisor add-on"
},
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/ru.json b/homeassistant/components/adguard/translations/ru.json
index 34c56342b5bcd9..480204da0a185e 100644
--- a/homeassistant/components/adguard/translations/ru.json
+++ b/homeassistant/components/adguard/translations/ru.json
@@ -9,8 +9,8 @@
},
"step": {
"hassio_confirm": {
- "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?",
- "title": "AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)"
+ "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?",
+ "title": "AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)"
},
"user": {
"data": {
@@ -18,7 +18,7 @@
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
"ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
- "username": "\u041b\u043e\u0433\u0438\u043d",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f",
"verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL"
},
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f AdGuard Home."
diff --git a/homeassistant/components/adguard/translations/sl.json b/homeassistant/components/adguard/translations/sl.json
index 06ad40a17d64ec..34b03263cebfdb 100644
--- a/homeassistant/components/adguard/translations/sl.json
+++ b/homeassistant/components/adguard/translations/sl.json
@@ -6,8 +6,8 @@
},
"step": {
"hassio_confirm": {
- "description": "\u017delite konfigurirati Home Assistant-a za povezavo z AdGuard Home, ki ga ponuja Hass.io add-on {addon} ?",
- "title": "AdGuard Home preko dodatka Hass.io"
+ "description": "\u017delite konfigurirati Home Assistant-a za povezavo z AdGuard Home, ki ga ponuja Supervisor add-on {addon} ?",
+ "title": "AdGuard Home preko dodatka Supervisor"
},
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/sv.json b/homeassistant/components/adguard/translations/sv.json
index 5b9a0f9a969bda..ca6158eaf32b69 100644
--- a/homeassistant/components/adguard/translations/sv.json
+++ b/homeassistant/components/adguard/translations/sv.json
@@ -6,8 +6,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till AdGuard Home som tillhandah\u00e5lls av Hass.io Add-on: {addon}?",
- "title": "AdGuard Home via Hass.io-till\u00e4gget"
+ "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till AdGuard Home som tillhandah\u00e5lls av Supervisor Add-on: {addon}?",
+ "title": "AdGuard Home via Supervisor-till\u00e4gget"
},
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/tr.json b/homeassistant/components/adguard/translations/tr.json
new file mode 100644
index 00000000000000..26bef46408a1e0
--- /dev/null
+++ b/homeassistant/components/adguard/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "password": "Parola",
+ "port": "Port",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/translations/uk.json b/homeassistant/components/adguard/translations/uk.json
new file mode 100644
index 00000000000000..28d02f25b7e8fa
--- /dev/null
+++ b/homeassistant/components/adguard/translations/uk.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u0430.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "hassio_confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e AdGuard Home (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor \"{addon}\")?",
+ "title": "AdGuard Home (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor)"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430",
+ "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443 \u0456 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044e AdGuard Home."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/translations/zh-Hant.json b/homeassistant/components/adguard/translations/zh-Hant.json
index 8306b2daf70a7f..69d24d1fa7f358 100644
--- a/homeassistant/components/adguard/translations/zh-Hant.json
+++ b/homeassistant/components/adguard/translations/zh-Hant.json
@@ -9,8 +9,8 @@
},
"step": {
"hassio_confirm": {
- "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6\uff1a{addon} \u9023\u7dda\u81f3 AdGuard Home\uff1f",
- "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6 AdGuard Home"
+ "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 AdGuard Home\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f",
+ "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 AdGuard Home"
},
"user": {
"data": {
diff --git a/homeassistant/components/ads/sensor.py b/homeassistant/components/ads/sensor.py
index 2f411b277236e0..933950dcf1b34c 100644
--- a/homeassistant/components/ads/sensor.py
+++ b/homeassistant/components/ads/sensor.py
@@ -2,7 +2,7 @@
import voluptuous as vol
from homeassistant.components import ads
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT
import homeassistant.helpers.config_validation as cv
@@ -43,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([entity])
-class AdsSensor(AdsEntity):
+class AdsSensor(AdsEntity, SensorEntity):
"""Representation of an ADS sensor entity."""
def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, factor):
diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py
index 7b270f1f33532c..98c6c401810e7a 100644
--- a/homeassistant/components/advantage_air/__init__.py
+++ b/homeassistant/components/advantage_air/__init__.py
@@ -7,24 +7,17 @@
from advantage_air import ApiError, advantage_air
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ADVANTAGE_AIR_RETRY, DOMAIN
ADVANTAGE_AIR_SYNC_INTERVAL = 15
-ADVANTAGE_AIR_PLATFORMS = ["climate", "cover", "binary_sensor", "sensor", "switch"]
+PLATFORMS = ["climate", "cover", "binary_sensor", "sensor", "switch"]
_LOGGER = logging.getLogger(__name__)
-async def async_setup(hass, config):
- """Set up Advantage Air integration."""
- hass.data[DOMAIN] = {}
- return True
-
-
async def async_setup_entry(hass, entry):
"""Set up Advantage Air config."""
ip_address = entry.data[CONF_IP_ADDRESS]
@@ -57,17 +50,15 @@ async def async_change(change):
except ApiError as err:
_LOGGER.warning(err)
- await coordinator.async_refresh()
-
- if not coordinator.data:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
+ hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
"coordinator": coordinator,
"async_change": async_change,
}
- for platform in ADVANTAGE_AIR_PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)
@@ -80,8 +71,8 @@ async def async_unload_entry(hass, entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in ADVANTAGE_AIR_PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py
index d25ce4888fccdf..19ac584ac2f85b 100644
--- a/homeassistant/components/advantage_air/sensor.py
+++ b/homeassistant/components/advantage_air/sensor.py
@@ -1,6 +1,7 @@
"""Sensor platform for Advantage Air integration."""
import voluptuous as vol
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import PERCENTAGE
from homeassistant.helpers import config_validation as cv, entity_platform
@@ -40,7 +41,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
-class AdvantageAirTimeTo(AdvantageAirEntity):
+class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity):
"""Representation of Advantage Air timer control."""
def __init__(self, instance, ac_key, action):
@@ -82,7 +83,7 @@ async def set_time_to(self, **kwargs):
await self.async_change({self.ac_key: {"info": {self._time_key: value}}})
-class AdvantageAirZoneVent(AdvantageAirEntity):
+class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity):
"""Representation of Advantage Air Zone Vent Sensor."""
@property
@@ -115,7 +116,7 @@ def icon(self):
return "mdi:fan-off"
-class AdvantageAirZoneSignal(AdvantageAirEntity):
+class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity):
"""Representation of Advantage Air Zone wireless signal sensor."""
@property
diff --git a/homeassistant/components/advantage_air/translations/de.json b/homeassistant/components/advantage_air/translations/de.json
index 0d8a0052406daa..3b4066996ebf2f 100644
--- a/homeassistant/components/advantage_air/translations/de.json
+++ b/homeassistant/components/advantage_air/translations/de.json
@@ -1,7 +1,10 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"step": {
"user": {
diff --git a/homeassistant/components/advantage_air/translations/hu.json b/homeassistant/components/advantage_air/translations/hu.json
index 0da6d0d5304447..e82e88da8d217a 100644
--- a/homeassistant/components/advantage_air/translations/hu.json
+++ b/homeassistant/components/advantage_air/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
diff --git a/homeassistant/components/advantage_air/translations/id.json b/homeassistant/components/advantage_air/translations/id.json
new file mode 100644
index 00000000000000..7993fa3be1d886
--- /dev/null
+++ b/homeassistant/components/advantage_air/translations/id.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "Alamat IP",
+ "port": "Port"
+ },
+ "description": "Hubungkan ke API tablet dinding Advantage Air.",
+ "title": "Hubungkan"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/advantage_air/translations/ko.json b/homeassistant/components/advantage_air/translations/ko.json
new file mode 100644
index 00000000000000..9a28cc499bc1eb
--- /dev/null
+++ b/homeassistant/components/advantage_air/translations/ko.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "IP \uc8fc\uc18c",
+ "port": "\ud3ec\ud2b8"
+ },
+ "description": "\ubcbd\uc5d0 \ubd80\ucc29\ub41c Advantage Air \ud0dc\ube14\ub9bf\uc758 API\uc5d0 \uc5f0\uacb0\ud558\uae30",
+ "title": "\uc5f0\uacb0\ud558\uae30"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/advantage_air/translations/nl.json b/homeassistant/components/advantage_air/translations/nl.json
index 95395d24bcaf60..3206c7a3165560 100644
--- a/homeassistant/components/advantage_air/translations/nl.json
+++ b/homeassistant/components/advantage_air/translations/nl.json
@@ -3,9 +3,13 @@
"abort": {
"already_configured": "Apparaat is al geconfigureerd"
},
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken"
+ },
"step": {
"user": {
"data": {
+ "ip_address": "IP-adres",
"port": "Poort"
},
"description": "Maak verbinding met de API van uw Advantage Air-tablet voor wandmontage.",
diff --git a/homeassistant/components/advantage_air/translations/tr.json b/homeassistant/components/advantage_air/translations/tr.json
new file mode 100644
index 00000000000000..db639c593764b0
--- /dev/null
+++ b/homeassistant/components/advantage_air/translations/tr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "\u0130p Adresi",
+ "port": "Port"
+ },
+ "title": "Ba\u011flan"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/advantage_air/translations/uk.json b/homeassistant/components/advantage_air/translations/uk.json
new file mode 100644
index 00000000000000..14ac18395e25e3
--- /dev/null
+++ b/homeassistant/components/advantage_air/translations/uk.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "description": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e API \u0412\u0430\u0448\u043e\u0433\u043e \u043d\u0430\u0441\u0442\u0456\u043d\u043d\u043e\u0433\u043e \u043f\u043b\u0430\u043d\u0448\u0435\u0442\u0430 Advantage Air.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py
new file mode 100644
index 00000000000000..4c1315d187df2b
--- /dev/null
+++ b/homeassistant/components/aemet/__init__.py
@@ -0,0 +1,56 @@
+"""The AEMET OpenData component."""
+import asyncio
+import logging
+
+from aemet_opendata.interface import AEMET
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+from homeassistant.core import HomeAssistant
+
+from .const import DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, PLATFORMS
+from .weather_update_coordinator import WeatherUpdateCoordinator
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
+ """Set up AEMET OpenData as config entry."""
+ name = config_entry.data[CONF_NAME]
+ api_key = config_entry.data[CONF_API_KEY]
+ latitude = config_entry.data[CONF_LATITUDE]
+ longitude = config_entry.data[CONF_LONGITUDE]
+
+ aemet = AEMET(api_key)
+ weather_coordinator = WeatherUpdateCoordinator(hass, aemet, latitude, longitude)
+
+ await weather_coordinator.async_config_entry_first_refresh()
+
+ hass.data.setdefault(DOMAIN, {})
+ hass.data[DOMAIN][config_entry.entry_id] = {
+ ENTRY_NAME: name,
+ ENTRY_WEATHER_COORDINATOR: weather_coordinator,
+ }
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(config_entry.entry_id)
+
+ return unload_ok
diff --git a/homeassistant/components/aemet/abstract_aemet_sensor.py b/homeassistant/components/aemet/abstract_aemet_sensor.py
new file mode 100644
index 00000000000000..8847a5d094d480
--- /dev/null
+++ b/homeassistant/components/aemet/abstract_aemet_sensor.py
@@ -0,0 +1,58 @@
+"""Abstraction form AEMET OpenData sensors."""
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.const import ATTR_ATTRIBUTION
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import ATTRIBUTION, SENSOR_DEVICE_CLASS, SENSOR_NAME, SENSOR_UNIT
+from .weather_update_coordinator import WeatherUpdateCoordinator
+
+
+class AbstractAemetSensor(CoordinatorEntity, SensorEntity):
+ """Abstract class for an AEMET OpenData sensor."""
+
+ def __init__(
+ self,
+ name,
+ unique_id,
+ sensor_type,
+ sensor_configuration,
+ coordinator: WeatherUpdateCoordinator,
+ ):
+ """Initialize the sensor."""
+ super().__init__(coordinator)
+ self._name = name
+ self._unique_id = unique_id
+ self._sensor_type = sensor_type
+ self._sensor_name = sensor_configuration[SENSOR_NAME]
+ self._unit_of_measurement = sensor_configuration.get(SENSOR_UNIT)
+ self._device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS)
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return f"{self._name} {self._sensor_name}"
+
+ @property
+ def unique_id(self):
+ """Return a unique_id for this entity."""
+ return self._unique_id
+
+ @property
+ def attribution(self):
+ """Return the attribution."""
+ return ATTRIBUTION
+
+ @property
+ def device_class(self):
+ """Return the device_class."""
+ return self._device_class
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of this entity, if any."""
+ return self._unit_of_measurement
+
+ @property
+ def extra_state_attributes(self):
+ """Return the state attributes."""
+ return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/aemet/config_flow.py b/homeassistant/components/aemet/config_flow.py
new file mode 100644
index 00000000000000..2e36896c1ebde7
--- /dev/null
+++ b/homeassistant/components/aemet/config_flow.py
@@ -0,0 +1,57 @@
+"""Config flow for AEMET OpenData."""
+from aemet_opendata import AEMET
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+import homeassistant.helpers.config_validation as cv
+
+from .const import DEFAULT_NAME, DOMAIN
+
+
+class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Config flow for AEMET OpenData."""
+
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ errors = {}
+
+ if user_input is not None:
+ latitude = user_input[CONF_LATITUDE]
+ longitude = user_input[CONF_LONGITUDE]
+
+ await self.async_set_unique_id(f"{latitude}-{longitude}")
+ self._abort_if_unique_id_configured()
+
+ api_online = await _is_aemet_api_online(self.hass, user_input[CONF_API_KEY])
+ if not api_online:
+ errors["base"] = "invalid_api_key"
+
+ if not errors:
+ return self.async_create_entry(
+ title=user_input[CONF_NAME], data=user_input
+ )
+
+ schema = vol.Schema(
+ {
+ vol.Required(CONF_API_KEY): str,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
+ vol.Optional(
+ CONF_LATITUDE, default=self.hass.config.latitude
+ ): cv.latitude,
+ vol.Optional(
+ CONF_LONGITUDE, default=self.hass.config.longitude
+ ): cv.longitude,
+ }
+ )
+
+ return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
+
+
+async def _is_aemet_api_online(hass, api_key):
+ aemet = AEMET(api_key)
+ return await hass.async_add_executor_job(
+ aemet.get_conventional_observation_stations, False
+ )
diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py
new file mode 100644
index 00000000000000..390ccb860034ad
--- /dev/null
+++ b/homeassistant/components/aemet/const.py
@@ -0,0 +1,326 @@
+"""Constant values for the AEMET OpenData component."""
+
+from homeassistant.components.weather import (
+ ATTR_CONDITION_CLEAR_NIGHT,
+ ATTR_CONDITION_CLOUDY,
+ ATTR_CONDITION_FOG,
+ ATTR_CONDITION_LIGHTNING,
+ ATTR_CONDITION_LIGHTNING_RAINY,
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_POURING,
+ ATTR_CONDITION_RAINY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_CONDITION_SUNNY,
+ ATTR_FORECAST_CONDITION,
+ ATTR_FORECAST_PRECIPITATION,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY,
+ ATTR_FORECAST_TEMP,
+ ATTR_FORECAST_TEMP_LOW,
+ ATTR_FORECAST_TIME,
+ ATTR_FORECAST_WIND_BEARING,
+ ATTR_FORECAST_WIND_SPEED,
+)
+from homeassistant.const import (
+ DEGREE,
+ DEVICE_CLASS_HUMIDITY,
+ DEVICE_CLASS_PRESSURE,
+ DEVICE_CLASS_TEMPERATURE,
+ DEVICE_CLASS_TIMESTAMP,
+ PERCENTAGE,
+ PRECIPITATION_MILLIMETERS_PER_HOUR,
+ PRESSURE_HPA,
+ SPEED_KILOMETERS_PER_HOUR,
+ TEMP_CELSIUS,
+)
+
+ATTRIBUTION = "Powered by AEMET OpenData"
+PLATFORMS = ["sensor", "weather"]
+DEFAULT_NAME = "AEMET"
+DOMAIN = "aemet"
+ENTRY_NAME = "name"
+ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
+UPDATE_LISTENER = "update_listener"
+SENSOR_NAME = "sensor_name"
+SENSOR_UNIT = "sensor_unit"
+SENSOR_DEVICE_CLASS = "sensor_device_class"
+
+ATTR_API_CONDITION = "condition"
+ATTR_API_FORECAST_DAILY = "forecast-daily"
+ATTR_API_FORECAST_HOURLY = "forecast-hourly"
+ATTR_API_HUMIDITY = "humidity"
+ATTR_API_PRESSURE = "pressure"
+ATTR_API_RAIN = "rain"
+ATTR_API_RAIN_PROB = "rain-probability"
+ATTR_API_SNOW = "snow"
+ATTR_API_SNOW_PROB = "snow-probability"
+ATTR_API_STATION_ID = "station-id"
+ATTR_API_STATION_NAME = "station-name"
+ATTR_API_STATION_TIMESTAMP = "station-timestamp"
+ATTR_API_STORM_PROB = "storm-probability"
+ATTR_API_TEMPERATURE = "temperature"
+ATTR_API_TEMPERATURE_FEELING = "temperature-feeling"
+ATTR_API_TOWN_ID = "town-id"
+ATTR_API_TOWN_NAME = "town-name"
+ATTR_API_TOWN_TIMESTAMP = "town-timestamp"
+ATTR_API_WIND_BEARING = "wind-bearing"
+ATTR_API_WIND_MAX_SPEED = "wind-max-speed"
+ATTR_API_WIND_SPEED = "wind-speed"
+
+CONDITIONS_MAP = {
+ ATTR_CONDITION_CLEAR_NIGHT: {
+ "11n", # Despejado (de noche)
+ },
+ ATTR_CONDITION_CLOUDY: {
+ "14", # Nuboso
+ "14n", # Nuboso (de noche)
+ "15", # Muy nuboso
+ "15n", # Muy nuboso (de noche)
+ "16", # Cubierto
+ "16n", # Cubierto (de noche)
+ "17", # Nubes altas
+ "17n", # Nubes altas (de noche)
+ },
+ ATTR_CONDITION_FOG: {
+ "81", # Niebla
+ "81n", # Niebla (de noche)
+ "82", # Bruma - Neblina
+ "82n", # Bruma - Neblina (de noche)
+ },
+ ATTR_CONDITION_LIGHTNING: {
+ "51", # Intervalos nubosos con tormenta
+ "51n", # Intervalos nubosos con tormenta (de noche)
+ "52", # Nuboso con tormenta
+ "52n", # Nuboso con tormenta (de noche)
+ "53", # Muy nuboso con tormenta
+ "53n", # Muy nuboso con tormenta (de noche)
+ "54", # Cubierto con tormenta
+ "54n", # Cubierto con tormenta (de noche)
+ },
+ ATTR_CONDITION_LIGHTNING_RAINY: {
+ "61", # Intervalos nubosos con tormenta y lluvia escasa
+ "61n", # Intervalos nubosos con tormenta y lluvia escasa (de noche)
+ "62", # Nuboso con tormenta y lluvia escasa
+ "62n", # Nuboso con tormenta y lluvia escasa (de noche)
+ "63", # Muy nuboso con tormenta y lluvia escasa
+ "63n", # Muy nuboso con tormenta y lluvia escasa (de noche)
+ "64", # Cubierto con tormenta y lluvia escasa
+ "64n", # Cubierto con tormenta y lluvia escasa (de noche)
+ },
+ ATTR_CONDITION_PARTLYCLOUDY: {
+ "12", # Poco nuboso
+ "12n", # Poco nuboso (de noche)
+ "13", # Intervalos nubosos
+ "13n", # Intervalos nubosos (de noche)
+ },
+ ATTR_CONDITION_POURING: {
+ "27", # Chubascos
+ "27n", # Chubascos (de noche)
+ },
+ ATTR_CONDITION_RAINY: {
+ "23", # Intervalos nubosos con lluvia
+ "23n", # Intervalos nubosos con lluvia (de noche)
+ "24", # Nuboso con lluvia
+ "24n", # Nuboso con lluvia (de noche)
+ "25", # Muy nuboso con lluvia
+ "25n", # Muy nuboso con lluvia (de noche)
+ "26", # Cubierto con lluvia
+ "26n", # Cubierto con lluvia (de noche)
+ "43", # Intervalos nubosos con lluvia escasa
+ "43n", # Intervalos nubosos con lluvia escasa (de noche)
+ "44", # Nuboso con lluvia escasa
+ "44n", # Nuboso con lluvia escasa (de noche)
+ "45", # Muy nuboso con lluvia escasa
+ "45n", # Muy nuboso con lluvia escasa (de noche)
+ "46", # Cubierto con lluvia escasa
+ "46n", # Cubierto con lluvia escasa (de noche)
+ },
+ ATTR_CONDITION_SNOWY: {
+ "33", # Intervalos nubosos con nieve
+ "33n", # Intervalos nubosos con nieve (de noche)
+ "34", # Nuboso con nieve
+ "34n", # Nuboso con nieve (de noche)
+ "35", # Muy nuboso con nieve
+ "35n", # Muy nuboso con nieve (de noche)
+ "36", # Cubierto con nieve
+ "36n", # Cubierto con nieve (de noche)
+ "71", # Intervalos nubosos con nieve escasa
+ "71n", # Intervalos nubosos con nieve escasa (de noche)
+ "72", # Nuboso con nieve escasa
+ "72n", # Nuboso con nieve escasa (de noche)
+ "73", # Muy nuboso con nieve escasa
+ "73n", # Muy nuboso con nieve escasa (de noche)
+ "74", # Cubierto con nieve escasa
+ "74n", # Cubierto con nieve escasa (de noche)
+ },
+ ATTR_CONDITION_SUNNY: {
+ "11", # Despejado
+ },
+}
+
+FORECAST_MONITORED_CONDITIONS = [
+ ATTR_FORECAST_CONDITION,
+ ATTR_FORECAST_PRECIPITATION,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY,
+ ATTR_FORECAST_TEMP,
+ ATTR_FORECAST_TEMP_LOW,
+ ATTR_FORECAST_TIME,
+ ATTR_FORECAST_WIND_BEARING,
+ ATTR_FORECAST_WIND_SPEED,
+]
+MONITORED_CONDITIONS = [
+ ATTR_API_CONDITION,
+ ATTR_API_HUMIDITY,
+ ATTR_API_PRESSURE,
+ ATTR_API_RAIN,
+ ATTR_API_RAIN_PROB,
+ ATTR_API_SNOW,
+ ATTR_API_SNOW_PROB,
+ ATTR_API_STATION_ID,
+ ATTR_API_STATION_NAME,
+ ATTR_API_STATION_TIMESTAMP,
+ ATTR_API_STORM_PROB,
+ ATTR_API_TEMPERATURE,
+ ATTR_API_TEMPERATURE_FEELING,
+ ATTR_API_TOWN_ID,
+ ATTR_API_TOWN_NAME,
+ ATTR_API_TOWN_TIMESTAMP,
+ ATTR_API_WIND_BEARING,
+ ATTR_API_WIND_MAX_SPEED,
+ ATTR_API_WIND_SPEED,
+]
+
+FORECAST_MODE_DAILY = "daily"
+FORECAST_MODE_HOURLY = "hourly"
+FORECAST_MODES = [
+ FORECAST_MODE_DAILY,
+ FORECAST_MODE_HOURLY,
+]
+FORECAST_MODE_ATTR_API = {
+ FORECAST_MODE_DAILY: ATTR_API_FORECAST_DAILY,
+ FORECAST_MODE_HOURLY: ATTR_API_FORECAST_HOURLY,
+}
+
+FORECAST_SENSOR_TYPES = {
+ ATTR_FORECAST_CONDITION: {
+ SENSOR_NAME: "Condition",
+ },
+ ATTR_FORECAST_PRECIPITATION: {
+ SENSOR_NAME: "Precipitation",
+ SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
+ },
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: {
+ SENSOR_NAME: "Precipitation probability",
+ SENSOR_UNIT: PERCENTAGE,
+ },
+ ATTR_FORECAST_TEMP: {
+ SENSOR_NAME: "Temperature",
+ SENSOR_UNIT: TEMP_CELSIUS,
+ SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
+ },
+ ATTR_FORECAST_TEMP_LOW: {
+ SENSOR_NAME: "Temperature Low",
+ SENSOR_UNIT: TEMP_CELSIUS,
+ SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
+ },
+ ATTR_FORECAST_TIME: {
+ SENSOR_NAME: "Time",
+ SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
+ },
+ ATTR_FORECAST_WIND_BEARING: {
+ SENSOR_NAME: "Wind bearing",
+ SENSOR_UNIT: DEGREE,
+ },
+ ATTR_FORECAST_WIND_SPEED: {
+ SENSOR_NAME: "Wind speed",
+ SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
+ },
+}
+WEATHER_SENSOR_TYPES = {
+ ATTR_API_CONDITION: {
+ SENSOR_NAME: "Condition",
+ },
+ ATTR_API_HUMIDITY: {
+ SENSOR_NAME: "Humidity",
+ SENSOR_UNIT: PERCENTAGE,
+ SENSOR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
+ },
+ ATTR_API_PRESSURE: {
+ SENSOR_NAME: "Pressure",
+ SENSOR_UNIT: PRESSURE_HPA,
+ SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
+ },
+ ATTR_API_RAIN: {
+ SENSOR_NAME: "Rain",
+ SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
+ },
+ ATTR_API_RAIN_PROB: {
+ SENSOR_NAME: "Rain probability",
+ SENSOR_UNIT: PERCENTAGE,
+ },
+ ATTR_API_SNOW: {
+ SENSOR_NAME: "Snow",
+ SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
+ },
+ ATTR_API_SNOW_PROB: {
+ SENSOR_NAME: "Snow probability",
+ SENSOR_UNIT: PERCENTAGE,
+ },
+ ATTR_API_STATION_ID: {
+ SENSOR_NAME: "Station ID",
+ },
+ ATTR_API_STATION_NAME: {
+ SENSOR_NAME: "Station name",
+ },
+ ATTR_API_STATION_TIMESTAMP: {
+ SENSOR_NAME: "Station timestamp",
+ SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
+ },
+ ATTR_API_STORM_PROB: {
+ SENSOR_NAME: "Storm probability",
+ SENSOR_UNIT: PERCENTAGE,
+ },
+ ATTR_API_TEMPERATURE: {
+ SENSOR_NAME: "Temperature",
+ SENSOR_UNIT: TEMP_CELSIUS,
+ SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
+ },
+ ATTR_API_TEMPERATURE_FEELING: {
+ SENSOR_NAME: "Temperature feeling",
+ SENSOR_UNIT: TEMP_CELSIUS,
+ SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
+ },
+ ATTR_API_TOWN_ID: {
+ SENSOR_NAME: "Town ID",
+ },
+ ATTR_API_TOWN_NAME: {
+ SENSOR_NAME: "Town name",
+ },
+ ATTR_API_TOWN_TIMESTAMP: {
+ SENSOR_NAME: "Town timestamp",
+ SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
+ },
+ ATTR_API_WIND_BEARING: {
+ SENSOR_NAME: "Wind bearing",
+ SENSOR_UNIT: DEGREE,
+ },
+ ATTR_API_WIND_MAX_SPEED: {
+ SENSOR_NAME: "Wind max speed",
+ SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
+ },
+ ATTR_API_WIND_SPEED: {
+ SENSOR_NAME: "Wind speed",
+ SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
+ },
+}
+
+WIND_BEARING_MAP = {
+ "C": None,
+ "N": 0.0,
+ "NE": 45.0,
+ "E": 90.0,
+ "SE": 135.0,
+ "S": 180.0,
+ "SO": 225.0,
+ "O": 270.0,
+ "NO": 315.0,
+}
diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json
new file mode 100644
index 00000000000000..eb5dc295f297ed
--- /dev/null
+++ b/homeassistant/components/aemet/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "aemet",
+ "name": "AEMET OpenData",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/aemet",
+ "requirements": ["AEMET-OpenData==0.1.8"],
+ "codeowners": ["@noltari"]
+}
diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py
new file mode 100644
index 00000000000000..6f43d66e011a52
--- /dev/null
+++ b/homeassistant/components/aemet/sensor.py
@@ -0,0 +1,115 @@
+"""Support for the AEMET OpenData service."""
+from .abstract_aemet_sensor import AbstractAemetSensor
+from .const import (
+ DOMAIN,
+ ENTRY_NAME,
+ ENTRY_WEATHER_COORDINATOR,
+ FORECAST_MODE_ATTR_API,
+ FORECAST_MODE_DAILY,
+ FORECAST_MODES,
+ FORECAST_MONITORED_CONDITIONS,
+ FORECAST_SENSOR_TYPES,
+ MONITORED_CONDITIONS,
+ WEATHER_SENSOR_TYPES,
+)
+from .weather_update_coordinator import WeatherUpdateCoordinator
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up AEMET OpenData sensor entities based on a config entry."""
+ domain_data = hass.data[DOMAIN][config_entry.entry_id]
+ name = domain_data[ENTRY_NAME]
+ weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
+
+ weather_sensor_types = WEATHER_SENSOR_TYPES
+ forecast_sensor_types = FORECAST_SENSOR_TYPES
+
+ entities = []
+ for sensor_type in MONITORED_CONDITIONS:
+ unique_id = f"{config_entry.unique_id}-{sensor_type}"
+ entities.append(
+ AemetSensor(
+ name,
+ unique_id,
+ sensor_type,
+ weather_sensor_types[sensor_type],
+ weather_coordinator,
+ )
+ )
+
+ for mode in FORECAST_MODES:
+ name = f"{domain_data[ENTRY_NAME]} {mode}"
+
+ for sensor_type in FORECAST_MONITORED_CONDITIONS:
+ unique_id = f"{config_entry.unique_id}-forecast-{mode}-{sensor_type}"
+ entities.append(
+ AemetForecastSensor(
+ f"{name} Forecast",
+ unique_id,
+ sensor_type,
+ forecast_sensor_types[sensor_type],
+ weather_coordinator,
+ mode,
+ )
+ )
+
+ async_add_entities(entities)
+
+
+class AemetSensor(AbstractAemetSensor):
+ """Implementation of an AEMET OpenData sensor."""
+
+ def __init__(
+ self,
+ name,
+ unique_id,
+ sensor_type,
+ sensor_configuration,
+ weather_coordinator: WeatherUpdateCoordinator,
+ ):
+ """Initialize the sensor."""
+ super().__init__(
+ name, unique_id, sensor_type, sensor_configuration, weather_coordinator
+ )
+ self._weather_coordinator = weather_coordinator
+
+ @property
+ def state(self):
+ """Return the state of the device."""
+ return self._weather_coordinator.data.get(self._sensor_type)
+
+
+class AemetForecastSensor(AbstractAemetSensor):
+ """Implementation of an AEMET OpenData forecast sensor."""
+
+ def __init__(
+ self,
+ name,
+ unique_id,
+ sensor_type,
+ sensor_configuration,
+ weather_coordinator: WeatherUpdateCoordinator,
+ forecast_mode,
+ ):
+ """Initialize the sensor."""
+ super().__init__(
+ name, unique_id, sensor_type, sensor_configuration, weather_coordinator
+ )
+ self._weather_coordinator = weather_coordinator
+ self._forecast_mode = forecast_mode
+
+ @property
+ def entity_registry_enabled_default(self) -> bool:
+ """Return if the entity should be enabled when first added to the entity registry."""
+ return self._forecast_mode == FORECAST_MODE_DAILY
+
+ @property
+ def state(self):
+ """Return the state of the device."""
+ forecast = None
+ forecasts = self._weather_coordinator.data.get(
+ FORECAST_MODE_ATTR_API[self._forecast_mode]
+ )
+ if forecasts:
+ forecast = forecasts[0].get(self._sensor_type)
+ return forecast
diff --git a/homeassistant/components/aemet/strings.json b/homeassistant/components/aemet/strings.json
new file mode 100644
index 00000000000000..a25a503bade514
--- /dev/null
+++ b/homeassistant/components/aemet/strings.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_location%]"
+ },
+ "error": {
+ "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]"
+ },
+ "step": {
+ "user": {
+ "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": "Name of the integration"
+ },
+ "description": "Set up AEMET OpenData integration. To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario",
+ "title": "AEMET OpenData"
+ }
+ }
+ }
+}
diff --git a/homeassistant/components/aemet/translations/ca.json b/homeassistant/components/aemet/translations/ca.json
new file mode 100644
index 00000000000000..85b22e72d76f9c
--- /dev/null
+++ b/homeassistant/components/aemet/translations/ca.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada"
+ },
+ "error": {
+ "invalid_api_key": "Clau API inv\u00e0lida"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Clau API",
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "name": "Nom de la integraci\u00f3"
+ },
+ "description": "Configura la integraci\u00f3 d'AEMET OpenData. Per generar la clau API, v\u00e9s a https://opendata.aemet.es/centrodedescargas/altaUsuario",
+ "title": "AEMET OpenData"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/cs.json b/homeassistant/components/aemet/translations/cs.json
new file mode 100644
index 00000000000000..d892d4c6dc37ad
--- /dev/null
+++ b/homeassistant/components/aemet/translations/cs.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno"
+ },
+ "error": {
+ "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kl\u00ed\u010d API",
+ "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka",
+ "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka",
+ "name": "N\u00e1zev integrace"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/de.json b/homeassistant/components/aemet/translations/de.json
new file mode 100644
index 00000000000000..d5312805722964
--- /dev/null
+++ b/homeassistant/components/aemet/translations/de.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Standort ist bereits konfiguriert"
+ },
+ "error": {
+ "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-Schl\u00fcssel",
+ "latitude": "Breitengrad",
+ "longitude": "L\u00e4ngengrad",
+ "name": "Name der Integration"
+ },
+ "description": "Richte die AEMET OpenData Integration ein. Um den API-Schl\u00fcssel zu generieren, besuche https://opendata.aemet.es/centrodedescargas/altaUsuario",
+ "title": "[void]"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/en.json b/homeassistant/components/aemet/translations/en.json
new file mode 100644
index 00000000000000..60e7f5f2ec22cb
--- /dev/null
+++ b/homeassistant/components/aemet/translations/en.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Location is already configured"
+ },
+ "error": {
+ "invalid_api_key": "Invalid API key"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Key",
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "name": "Name of the integration"
+ },
+ "description": "Set up AEMET OpenData integration. To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario",
+ "title": "AEMET OpenData"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/es.json b/homeassistant/components/aemet/translations/es.json
new file mode 100644
index 00000000000000..ffe4d524754c05
--- /dev/null
+++ b/homeassistant/components/aemet/translations/es.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada"
+ },
+ "error": {
+ "invalid_api_key": "Clave API no v\u00e1lida"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Clave API",
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "name": "Nombre de la integraci\u00f3n"
+ },
+ "description": "Configurar la integraci\u00f3n de AEMET OpenData. Para generar la clave API, ve a https://opendata.aemet.es/centrodedescargas/altaUsuario",
+ "title": "AEMET OpenData"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/et.json b/homeassistant/components/aemet/translations/et.json
new file mode 100644
index 00000000000000..bc0a26179d56a9
--- /dev/null
+++ b/homeassistant/components/aemet/translations/et.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Asukoht on juba m\u00e4\u00e4ratud"
+ },
+ "error": {
+ "invalid_api_key": "Vale API v\u00f5ti"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API v\u00f5ti",
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad",
+ "name": "Sidumise nimi"
+ },
+ "description": "Seadista AEMET OpenData sidumine. API v\u00f5tme loomiseks mine aadressile https://opendata.aemet.es/centrodedescargas/altaUsuario",
+ "title": "AEMET OpenData"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/fr.json b/homeassistant/components/aemet/translations/fr.json
new file mode 100644
index 00000000000000..bb1e792aa5e504
--- /dev/null
+++ b/homeassistant/components/aemet/translations/fr.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "invalid_api_key": "Cl\u00e9 API invalide"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Cl\u00e9 d'API",
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "name": "Nom de l'int\u00e9gration"
+ },
+ "description": "Configurez l'int\u00e9gration AEMET OpenData. Pour g\u00e9n\u00e9rer la cl\u00e9 API, acc\u00e9dez \u00e0 https://opendata.aemet.es/centrodedescargas/altaUsuario",
+ "title": "AEMET OpenData"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/hu.json b/homeassistant/components/aemet/translations/hu.json
new file mode 100644
index 00000000000000..d810691046e54a
--- /dev/null
+++ b/homeassistant/components/aemet/translations/hu.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A hely m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API kulcs",
+ "latitude": "Sz\u00e9less\u00e9g",
+ "longitude": "Hossz\u00fas\u00e1g",
+ "name": "Az integr\u00e1ci\u00f3 neve"
+ },
+ "title": "AEMET OpenData"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/id.json b/homeassistant/components/aemet/translations/id.json
new file mode 100644
index 00000000000000..fa678cbbbe0b25
--- /dev/null
+++ b/homeassistant/components/aemet/translations/id.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Lokasi sudah dikonfigurasi"
+ },
+ "error": {
+ "invalid_api_key": "Kunci API tidak valid"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kunci API",
+ "latitude": "Lintang",
+ "longitude": "Bujur",
+ "name": "Nama integrasi"
+ },
+ "description": "Siapkan integrasi AEMET OpenData. Untuk menghasilkan kunci API, buka https://opendata.aemet.es/centrodedescargas/altaUsuario",
+ "title": "AEMET OpenData"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/it.json b/homeassistant/components/aemet/translations/it.json
new file mode 100644
index 00000000000000..112630028b9687
--- /dev/null
+++ b/homeassistant/components/aemet/translations/it.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La posizione \u00e8 gi\u00e0 configurata"
+ },
+ "error": {
+ "invalid_api_key": "Chiave API non valida"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Chiave API",
+ "latitude": "Latitudine",
+ "longitude": "Logitudine",
+ "name": "Nome dell'integrazione"
+ },
+ "description": "Imposta l'integrazione di AEMET OpenData. Per generare la chiave API, vai su https://opendata.aemet.es/centrodedescargas/altaUsuario",
+ "title": "AEMET OpenData"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/ko.json b/homeassistant/components/aemet/translations/ko.json
new file mode 100644
index 00000000000000..95c11b018fef02
--- /dev/null
+++ b/homeassistant/components/aemet/translations/ko.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "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": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc774\ub984"
+ },
+ "description": "AEMET OpenData \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://opendata.aemet.es/centrodedescargas/altaUsuario \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694",
+ "title": "AEMET OpenData"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/lb.json b/homeassistant/components/aemet/translations/lb.json
new file mode 100644
index 00000000000000..8e83c0e86d3288
--- /dev/null
+++ b/homeassistant/components/aemet/translations/lb.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Schl\u00ebssel",
+ "latitude": "L\u00e4ngegrad",
+ "longitude": "Breedegrad",
+ "name": "Numm vun der Integratioun"
+ },
+ "title": "AEMET OpenData"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/nl.json b/homeassistant/components/aemet/translations/nl.json
new file mode 100644
index 00000000000000..77589e20490712
--- /dev/null
+++ b/homeassistant/components/aemet/translations/nl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Locatie is al geconfigureerd."
+ },
+ "error": {
+ "invalid_api_key": "Ongeldige API-sleutel"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-sleutel",
+ "latitude": "Breedtegraad",
+ "longitude": "Lengtegraad",
+ "name": "Naam van de integratie"
+ },
+ "description": "Stel AEMET OpenData-integratie in. Ga naar https://opendata.aemet.es/centrodedescargas/altaUsuario om een API-sleutel te genereren",
+ "title": "AEMET OpenData"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/no.json b/homeassistant/components/aemet/translations/no.json
new file mode 100644
index 00000000000000..48cbc9916caed8
--- /dev/null
+++ b/homeassistant/components/aemet/translations/no.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Plasseringen er allerede konfigurert"
+ },
+ "error": {
+ "invalid_api_key": "Ugyldig API-n\u00f8kkel"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-n\u00f8kkel",
+ "latitude": "Breddegrad",
+ "longitude": "Lengdegrad",
+ "name": "Navnet p\u00e5 integrasjonen"
+ },
+ "description": "Sett opp AEMET OpenData-integrasjon. For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://opendata.aemet.es/centrodedescargas/altaUsuario",
+ "title": "AEMET OpenData"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/pl.json b/homeassistant/components/aemet/translations/pl.json
new file mode 100644
index 00000000000000..2c5c24fae2aa6e
--- /dev/null
+++ b/homeassistant/components/aemet/translations/pl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Lokalizacja jest ju\u017c skonfigurowana"
+ },
+ "error": {
+ "invalid_api_key": "Nieprawid\u0142owy klucz API"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Klucz API",
+ "latitude": "Szeroko\u015b\u0107 geograficzna",
+ "longitude": "D\u0142ugo\u015b\u0107 geograficzna",
+ "name": "Nazwa integracji"
+ },
+ "description": "Skonfiguruj integracj\u0119 AEMET OpenData. Aby wygenerowa\u0107 klucz API, przejd\u017a do https://opendata.aemet.es/centrodedescargas/altaUsuario",
+ "title": "AEMET OpenData"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/ru.json b/homeassistant/components/aemet/translations/ru.json
new file mode 100644
index 00000000000000..4da9a032d2b56b
--- /dev/null
+++ b/homeassistant/components/aemet/translations/ru.json
@@ -0,0 +1,22 @@
+{
+ "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": {
+ "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "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\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 AEMET OpenData. \u0427\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://opendata.aemet.es/centrodedescargas/altaUsuario.",
+ "title": "AEMET OpenData"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/translations/zh-Hant.json b/homeassistant/components/aemet/translations/zh-Hant.json
new file mode 100644
index 00000000000000..75b251ae2ff5f8
--- /dev/null
+++ b/homeassistant/components/aemet/translations/zh-Hant.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "invalid_api_key": "API \u5bc6\u9470\u7121\u6548"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API \u5bc6\u9470",
+ "latitude": "\u7def\u5ea6",
+ "longitude": "\u7d93\u5ea6",
+ "name": "\u6574\u5408\u540d\u7a31"
+ },
+ "description": "\u6b32\u8a2d\u5b9a AEMET OpenData \u6574\u5408\u3002\u8acb\u81f3 https://opendata.aemet.es/centrodedescargas/altaUsuario \u7522\u751f API \u5bc6\u9470",
+ "title": "AEMET OpenData"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py
new file mode 100644
index 00000000000000..e54a297cc091e3
--- /dev/null
+++ b/homeassistant/components/aemet/weather.py
@@ -0,0 +1,113 @@
+"""Support for the AEMET OpenData service."""
+from homeassistant.components.weather import WeatherEntity
+from homeassistant.const import TEMP_CELSIUS
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import (
+ ATTR_API_CONDITION,
+ ATTR_API_HUMIDITY,
+ ATTR_API_PRESSURE,
+ ATTR_API_TEMPERATURE,
+ ATTR_API_WIND_BEARING,
+ ATTR_API_WIND_SPEED,
+ ATTRIBUTION,
+ DOMAIN,
+ ENTRY_NAME,
+ ENTRY_WEATHER_COORDINATOR,
+ FORECAST_MODE_ATTR_API,
+ FORECAST_MODE_DAILY,
+ FORECAST_MODES,
+)
+from .weather_update_coordinator import WeatherUpdateCoordinator
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up AEMET OpenData weather entity based on a config entry."""
+ domain_data = hass.data[DOMAIN][config_entry.entry_id]
+ weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
+
+ entities = []
+ for mode in FORECAST_MODES:
+ name = f"{domain_data[ENTRY_NAME]} {mode}"
+ unique_id = f"{config_entry.unique_id} {mode}"
+ entities.append(AemetWeather(name, unique_id, weather_coordinator, mode))
+
+ if entities:
+ async_add_entities(entities, False)
+
+
+class AemetWeather(CoordinatorEntity, WeatherEntity):
+ """Implementation of an AEMET OpenData sensor."""
+
+ def __init__(
+ self,
+ name,
+ unique_id,
+ coordinator: WeatherUpdateCoordinator,
+ forecast_mode,
+ ):
+ """Initialize the sensor."""
+ super().__init__(coordinator)
+ self._name = name
+ self._unique_id = unique_id
+ self._forecast_mode = forecast_mode
+
+ @property
+ def attribution(self):
+ """Return the attribution."""
+ return ATTRIBUTION
+
+ @property
+ def condition(self):
+ """Return the current condition."""
+ return self.coordinator.data[ATTR_API_CONDITION]
+
+ @property
+ def entity_registry_enabled_default(self) -> bool:
+ """Return if the entity should be enabled when first added to the entity registry."""
+ return self._forecast_mode == FORECAST_MODE_DAILY
+
+ @property
+ def forecast(self):
+ """Return the forecast array."""
+ return self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]]
+
+ @property
+ def humidity(self):
+ """Return the humidity."""
+ return self.coordinator.data[ATTR_API_HUMIDITY]
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def pressure(self):
+ """Return the pressure."""
+ return self.coordinator.data[ATTR_API_PRESSURE]
+
+ @property
+ def temperature(self):
+ """Return the temperature."""
+ return self.coordinator.data[ATTR_API_TEMPERATURE]
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement."""
+ return TEMP_CELSIUS
+
+ @property
+ def unique_id(self):
+ """Return a unique_id for this entity."""
+ return self._unique_id
+
+ @property
+ def wind_bearing(self):
+ """Return the temperature."""
+ return self.coordinator.data[ATTR_API_WIND_BEARING]
+
+ @property
+ def wind_speed(self):
+ """Return the temperature."""
+ return self.coordinator.data[ATTR_API_WIND_SPEED]
diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py
new file mode 100644
index 00000000000000..7aab23488b56de
--- /dev/null
+++ b/homeassistant/components/aemet/weather_update_coordinator.py
@@ -0,0 +1,642 @@
+"""Weather data coordinator for the AEMET OpenData service."""
+from dataclasses import dataclass, field
+from datetime import timedelta
+import logging
+
+from aemet_opendata.const import (
+ AEMET_ATTR_DATE,
+ AEMET_ATTR_DAY,
+ AEMET_ATTR_DIRECTION,
+ AEMET_ATTR_ELABORATED,
+ AEMET_ATTR_FORECAST,
+ AEMET_ATTR_HUMIDITY,
+ AEMET_ATTR_ID,
+ AEMET_ATTR_IDEMA,
+ AEMET_ATTR_MAX,
+ AEMET_ATTR_MIN,
+ AEMET_ATTR_NAME,
+ AEMET_ATTR_PRECIPITATION,
+ AEMET_ATTR_PRECIPITATION_PROBABILITY,
+ AEMET_ATTR_SKY_STATE,
+ AEMET_ATTR_SNOW,
+ AEMET_ATTR_SNOW_PROBABILITY,
+ AEMET_ATTR_SPEED,
+ AEMET_ATTR_STATION_DATE,
+ AEMET_ATTR_STATION_HUMIDITY,
+ AEMET_ATTR_STATION_LOCATION,
+ AEMET_ATTR_STATION_PRESSURE_SEA,
+ AEMET_ATTR_STATION_TEMPERATURE,
+ AEMET_ATTR_STORM_PROBABILITY,
+ AEMET_ATTR_TEMPERATURE,
+ AEMET_ATTR_TEMPERATURE_FEELING,
+ AEMET_ATTR_WIND,
+ AEMET_ATTR_WIND_GUST,
+ ATTR_DATA,
+)
+from aemet_opendata.helpers import (
+ get_forecast_day_value,
+ get_forecast_hour_value,
+ get_forecast_interval_value,
+)
+import async_timeout
+
+from homeassistant.components.weather import (
+ ATTR_FORECAST_CONDITION,
+ ATTR_FORECAST_PRECIPITATION,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY,
+ ATTR_FORECAST_TEMP,
+ ATTR_FORECAST_TEMP_LOW,
+ ATTR_FORECAST_TIME,
+ ATTR_FORECAST_WIND_BEARING,
+ ATTR_FORECAST_WIND_SPEED,
+)
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+from homeassistant.util import dt as dt_util
+
+from .const import (
+ ATTR_API_CONDITION,
+ ATTR_API_FORECAST_DAILY,
+ ATTR_API_FORECAST_HOURLY,
+ ATTR_API_HUMIDITY,
+ ATTR_API_PRESSURE,
+ ATTR_API_RAIN,
+ ATTR_API_RAIN_PROB,
+ ATTR_API_SNOW,
+ ATTR_API_SNOW_PROB,
+ ATTR_API_STATION_ID,
+ ATTR_API_STATION_NAME,
+ ATTR_API_STATION_TIMESTAMP,
+ ATTR_API_STORM_PROB,
+ ATTR_API_TEMPERATURE,
+ ATTR_API_TEMPERATURE_FEELING,
+ ATTR_API_TOWN_ID,
+ ATTR_API_TOWN_NAME,
+ ATTR_API_TOWN_TIMESTAMP,
+ ATTR_API_WIND_BEARING,
+ ATTR_API_WIND_MAX_SPEED,
+ ATTR_API_WIND_SPEED,
+ CONDITIONS_MAP,
+ DOMAIN,
+ WIND_BEARING_MAP,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+STATION_MAX_DELTA = timedelta(hours=2)
+WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
+
+
+def format_condition(condition: str) -> str:
+ """Return condition from dict CONDITIONS_MAP."""
+ for key, value in CONDITIONS_MAP.items():
+ if condition in value:
+ return key
+ _LOGGER.error('Condition "%s" not found in CONDITIONS_MAP', condition)
+ return condition
+
+
+def format_float(value) -> float:
+ """Try converting string to float."""
+ try:
+ return float(value)
+ except (TypeError, ValueError):
+ return None
+
+
+def format_int(value) -> int:
+ """Try converting string to int."""
+ try:
+ return int(value)
+ except (TypeError, ValueError):
+ return None
+
+
+class TownNotFound(UpdateFailed):
+ """Raised when town is not found."""
+
+
+class WeatherUpdateCoordinator(DataUpdateCoordinator):
+ """Weather data update coordinator."""
+
+ def __init__(self, hass, aemet, latitude, longitude):
+ """Initialize coordinator."""
+ super().__init__(
+ hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL
+ )
+
+ self._aemet = aemet
+ self._station = None
+ self._town = None
+ self._latitude = latitude
+ self._longitude = longitude
+ self._data = {
+ "daily": None,
+ "hourly": None,
+ "station": None,
+ }
+
+ async def _async_update_data(self):
+ data = {}
+ with async_timeout.timeout(120):
+ weather_response = await self._get_aemet_weather()
+ data = self._convert_weather_response(weather_response)
+ return data
+
+ async def _get_aemet_weather(self):
+ """Poll weather data from AEMET OpenData."""
+ weather = await self.hass.async_add_executor_job(self._get_weather_and_forecast)
+ return weather
+
+ def _get_weather_station(self):
+ if not self._station:
+ self._station = (
+ self._aemet.get_conventional_observation_station_by_coordinates(
+ self._latitude, self._longitude
+ )
+ )
+ if self._station:
+ _LOGGER.debug(
+ "station found for coordinates [%s, %s]: %s",
+ self._latitude,
+ self._longitude,
+ self._station,
+ )
+ if not self._station:
+ _LOGGER.debug(
+ "station not found for coordinates [%s, %s]",
+ self._latitude,
+ self._longitude,
+ )
+ return self._station
+
+ def _get_weather_town(self):
+ if not self._town:
+ self._town = self._aemet.get_town_by_coordinates(
+ self._latitude, self._longitude
+ )
+ if self._town:
+ _LOGGER.debug(
+ "Town found for coordinates [%s, %s]: %s",
+ self._latitude,
+ self._longitude,
+ self._town,
+ )
+ if not self._town:
+ _LOGGER.error(
+ "Town not found for coordinates [%s, %s]",
+ self._latitude,
+ self._longitude,
+ )
+ raise TownNotFound
+ return self._town
+
+ def _get_weather_and_forecast(self):
+ """Get weather and forecast data from AEMET OpenData."""
+
+ self._get_weather_town()
+
+ daily = self._aemet.get_specific_forecast_town_daily(self._town[AEMET_ATTR_ID])
+ if not daily:
+ _LOGGER.error(
+ 'Error fetching daily data for town "%s"', self._town[AEMET_ATTR_ID]
+ )
+
+ hourly = self._aemet.get_specific_forecast_town_hourly(
+ self._town[AEMET_ATTR_ID]
+ )
+ if not hourly:
+ _LOGGER.error(
+ 'Error fetching hourly data for town "%s"', self._town[AEMET_ATTR_ID]
+ )
+
+ station = None
+ if self._get_weather_station():
+ station = self._aemet.get_conventional_observation_station_data(
+ self._station[AEMET_ATTR_IDEMA]
+ )
+ if not station:
+ _LOGGER.error(
+ 'Error fetching data for station "%s"',
+ self._station[AEMET_ATTR_IDEMA],
+ )
+
+ if daily:
+ self._data["daily"] = daily
+ if hourly:
+ self._data["hourly"] = hourly
+ if station:
+ self._data["station"] = station
+
+ return AemetWeather(
+ self._data["daily"],
+ self._data["hourly"],
+ self._data["station"],
+ )
+
+ def _convert_weather_response(self, weather_response):
+ """Format the weather response correctly."""
+ if not weather_response or not weather_response.hourly:
+ return None
+
+ elaborated = dt_util.parse_datetime(
+ weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_ELABORATED] + "Z"
+ )
+ now = dt_util.now()
+ now_utc = dt_util.utcnow()
+ hour = now.hour
+
+ # Get current day
+ day = None
+ for cur_day in weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_FORECAST][
+ AEMET_ATTR_DAY
+ ]:
+ cur_day_date = dt_util.parse_datetime(cur_day[AEMET_ATTR_DATE])
+ if now.date() == cur_day_date.date():
+ day = cur_day
+ break
+
+ # Get latest station data
+ station_data = None
+ station_dt = None
+ if weather_response.station:
+ for _station_data in weather_response.station[ATTR_DATA]:
+ if AEMET_ATTR_STATION_DATE in _station_data:
+ _station_dt = dt_util.parse_datetime(
+ _station_data[AEMET_ATTR_STATION_DATE] + "Z"
+ )
+ if not station_dt or _station_dt > station_dt:
+ station_data = _station_data
+ station_dt = _station_dt
+
+ condition = None
+ humidity = None
+ pressure = None
+ rain = None
+ rain_prob = None
+ snow = None
+ snow_prob = None
+ station_id = None
+ station_name = None
+ station_timestamp = None
+ storm_prob = None
+ temperature = None
+ temperature_feeling = None
+ town_id = None
+ town_name = None
+ town_timestamp = dt_util.as_utc(elaborated).isoformat()
+ wind_bearing = None
+ wind_max_speed = None
+ wind_speed = None
+
+ # Get weather values
+ if day:
+ condition = self._get_condition(day, hour)
+ humidity = self._get_humidity(day, hour)
+ rain = self._get_rain(day, hour)
+ rain_prob = self._get_rain_prob(day, hour)
+ snow = self._get_snow(day, hour)
+ snow_prob = self._get_snow_prob(day, hour)
+ station_id = self._get_station_id()
+ station_name = self._get_station_name()
+ storm_prob = self._get_storm_prob(day, hour)
+ temperature = self._get_temperature(day, hour)
+ temperature_feeling = self._get_temperature_feeling(day, hour)
+ town_id = self._get_town_id()
+ town_name = self._get_town_name()
+ wind_bearing = self._get_wind_bearing(day, hour)
+ wind_max_speed = self._get_wind_max_speed(day, hour)
+ wind_speed = self._get_wind_speed(day, hour)
+
+ # Overwrite weather values with closest station data (if present)
+ if station_data:
+ station_timestamp = dt_util.as_utc(station_dt).isoformat()
+ if (now_utc - station_dt) <= STATION_MAX_DELTA:
+ if AEMET_ATTR_STATION_HUMIDITY in station_data:
+ humidity = format_float(station_data[AEMET_ATTR_STATION_HUMIDITY])
+ if AEMET_ATTR_STATION_PRESSURE_SEA in station_data:
+ pressure = format_float(
+ station_data[AEMET_ATTR_STATION_PRESSURE_SEA]
+ )
+ if AEMET_ATTR_STATION_TEMPERATURE in station_data:
+ temperature = format_float(
+ station_data[AEMET_ATTR_STATION_TEMPERATURE]
+ )
+ else:
+ _LOGGER.warning("Station data is outdated")
+
+ # Get forecast from weather data
+ forecast_daily = self._get_daily_forecast_from_weather_response(
+ weather_response, now
+ )
+ forecast_hourly = self._get_hourly_forecast_from_weather_response(
+ weather_response, now
+ )
+
+ return {
+ ATTR_API_CONDITION: condition,
+ ATTR_API_FORECAST_DAILY: forecast_daily,
+ ATTR_API_FORECAST_HOURLY: forecast_hourly,
+ ATTR_API_HUMIDITY: humidity,
+ ATTR_API_TEMPERATURE: temperature,
+ ATTR_API_TEMPERATURE_FEELING: temperature_feeling,
+ ATTR_API_PRESSURE: pressure,
+ ATTR_API_RAIN: rain,
+ ATTR_API_RAIN_PROB: rain_prob,
+ ATTR_API_SNOW: snow,
+ ATTR_API_SNOW_PROB: snow_prob,
+ ATTR_API_STATION_ID: station_id,
+ ATTR_API_STATION_NAME: station_name,
+ ATTR_API_STATION_TIMESTAMP: station_timestamp,
+ ATTR_API_STORM_PROB: storm_prob,
+ ATTR_API_TOWN_ID: town_id,
+ ATTR_API_TOWN_NAME: town_name,
+ ATTR_API_TOWN_TIMESTAMP: town_timestamp,
+ ATTR_API_WIND_BEARING: wind_bearing,
+ ATTR_API_WIND_MAX_SPEED: wind_max_speed,
+ ATTR_API_WIND_SPEED: wind_speed,
+ }
+
+ def _get_daily_forecast_from_weather_response(self, weather_response, now):
+ if weather_response.daily:
+ parse = False
+ forecast = []
+ for day in weather_response.daily[ATTR_DATA][0][AEMET_ATTR_FORECAST][
+ AEMET_ATTR_DAY
+ ]:
+ day_date = dt_util.parse_datetime(day[AEMET_ATTR_DATE])
+ if now.date() == day_date.date():
+ parse = True
+ if parse:
+ cur_forecast = self._convert_forecast_day(day_date, day)
+ if cur_forecast:
+ forecast.append(cur_forecast)
+ return forecast
+ return None
+
+ def _get_hourly_forecast_from_weather_response(self, weather_response, now):
+ if weather_response.hourly:
+ parse = False
+ hour = now.hour
+ forecast = []
+ for day in weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_FORECAST][
+ AEMET_ATTR_DAY
+ ]:
+ day_date = dt_util.parse_datetime(day[AEMET_ATTR_DATE])
+ hour_start = 0
+ if now.date() == day_date.date():
+ parse = True
+ hour_start = now.hour
+ if parse:
+ for hour in range(hour_start, 24):
+ cur_forecast = self._convert_forecast_hour(day_date, day, hour)
+ if cur_forecast:
+ forecast.append(cur_forecast)
+ return forecast
+ return None
+
+ def _convert_forecast_day(self, date, day):
+ condition = self._get_condition_day(day)
+ if not condition:
+ return None
+
+ return {
+ ATTR_FORECAST_CONDITION: condition,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day(
+ day
+ ),
+ ATTR_FORECAST_TEMP: self._get_temperature_day(day),
+ ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day),
+ ATTR_FORECAST_TIME: dt_util.as_utc(date).isoformat(),
+ ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day),
+ ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day),
+ }
+
+ def _convert_forecast_hour(self, date, day, hour):
+ condition = self._get_condition(day, hour)
+ if not condition:
+ return None
+
+ forecast_dt = date.replace(hour=hour, minute=0, second=0)
+
+ return {
+ ATTR_FORECAST_CONDITION: condition,
+ ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour),
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob(
+ day, hour
+ ),
+ ATTR_FORECAST_TEMP: self._get_temperature(day, hour),
+ ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(),
+ ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour),
+ ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour),
+ }
+
+ def _calc_precipitation(self, day, hour):
+ """Calculate the precipitation."""
+ rain_value = self._get_rain(day, hour)
+ if not rain_value:
+ rain_value = 0
+
+ snow_value = self._get_snow(day, hour)
+ if not snow_value:
+ snow_value = 0
+
+ if round(rain_value + snow_value, 1) == 0:
+ return None
+ return round(rain_value + snow_value, 1)
+
+ def _calc_precipitation_prob(self, day, hour):
+ """Calculate the precipitation probability (hour)."""
+ rain_value = self._get_rain_prob(day, hour)
+ if not rain_value:
+ rain_value = 0
+
+ snow_value = self._get_snow_prob(day, hour)
+ if not snow_value:
+ snow_value = 0
+
+ if rain_value == 0 and snow_value == 0:
+ return None
+ return max(rain_value, snow_value)
+
+ @staticmethod
+ def _get_condition(day_data, hour):
+ """Get weather condition (hour) from weather data."""
+ val = get_forecast_hour_value(day_data[AEMET_ATTR_SKY_STATE], hour)
+ if val:
+ return format_condition(val)
+ return None
+
+ @staticmethod
+ def _get_condition_day(day_data):
+ """Get weather condition (day) from weather data."""
+ val = get_forecast_day_value(day_data[AEMET_ATTR_SKY_STATE])
+ if val:
+ return format_condition(val)
+ return None
+
+ @staticmethod
+ def _get_humidity(day_data, hour):
+ """Get humidity from weather data."""
+ val = get_forecast_hour_value(day_data[AEMET_ATTR_HUMIDITY], hour)
+ if val:
+ return format_int(val)
+ return None
+
+ @staticmethod
+ def _get_precipitation_prob_day(day_data):
+ """Get humidity from weather data."""
+ val = get_forecast_day_value(day_data[AEMET_ATTR_PRECIPITATION_PROBABILITY])
+ if val:
+ return format_int(val)
+ return None
+
+ @staticmethod
+ def _get_rain(day_data, hour):
+ """Get rain from weather data."""
+ val = get_forecast_hour_value(day_data[AEMET_ATTR_PRECIPITATION], hour)
+ if val:
+ return format_float(val)
+ return None
+
+ @staticmethod
+ def _get_rain_prob(day_data, hour):
+ """Get rain probability from weather data."""
+ val = get_forecast_interval_value(
+ day_data[AEMET_ATTR_PRECIPITATION_PROBABILITY], hour
+ )
+ if val:
+ return format_int(val)
+ return None
+
+ @staticmethod
+ def _get_snow(day_data, hour):
+ """Get snow from weather data."""
+ val = get_forecast_hour_value(day_data[AEMET_ATTR_SNOW], hour)
+ if val:
+ return format_float(val)
+ return None
+
+ @staticmethod
+ def _get_snow_prob(day_data, hour):
+ """Get snow probability from weather data."""
+ val = get_forecast_interval_value(day_data[AEMET_ATTR_SNOW_PROBABILITY], hour)
+ if val:
+ return format_int(val)
+ return None
+
+ def _get_station_id(self):
+ """Get station ID from weather data."""
+ if self._station:
+ return self._station[AEMET_ATTR_IDEMA]
+ return None
+
+ def _get_station_name(self):
+ """Get station name from weather data."""
+ if self._station:
+ return self._station[AEMET_ATTR_STATION_LOCATION]
+ return None
+
+ @staticmethod
+ def _get_storm_prob(day_data, hour):
+ """Get storm probability from weather data."""
+ val = get_forecast_interval_value(day_data[AEMET_ATTR_STORM_PROBABILITY], hour)
+ if val:
+ return format_int(val)
+ return None
+
+ @staticmethod
+ def _get_temperature(day_data, hour):
+ """Get temperature (hour) from weather data."""
+ val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE], hour)
+ return format_int(val)
+
+ @staticmethod
+ def _get_temperature_day(day_data):
+ """Get temperature (day) from weather data."""
+ val = get_forecast_day_value(
+ day_data[AEMET_ATTR_TEMPERATURE], key=AEMET_ATTR_MAX
+ )
+ return format_int(val)
+
+ @staticmethod
+ def _get_temperature_low_day(day_data):
+ """Get temperature (day) from weather data."""
+ val = get_forecast_day_value(
+ day_data[AEMET_ATTR_TEMPERATURE], key=AEMET_ATTR_MIN
+ )
+ return format_int(val)
+
+ @staticmethod
+ def _get_temperature_feeling(day_data, hour):
+ """Get temperature from weather data."""
+ val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE_FEELING], hour)
+ return format_int(val)
+
+ def _get_town_id(self):
+ """Get town ID from weather data."""
+ if self._town:
+ return self._town[AEMET_ATTR_ID]
+ return None
+
+ def _get_town_name(self):
+ """Get town name from weather data."""
+ if self._town:
+ return self._town[AEMET_ATTR_NAME]
+ return None
+
+ @staticmethod
+ def _get_wind_bearing(day_data, hour):
+ """Get wind bearing (hour) from weather data."""
+ val = get_forecast_hour_value(
+ day_data[AEMET_ATTR_WIND_GUST], hour, key=AEMET_ATTR_DIRECTION
+ )[0]
+ if val in WIND_BEARING_MAP:
+ return WIND_BEARING_MAP[val]
+ _LOGGER.error("%s not found in Wind Bearing map", val)
+ return None
+
+ @staticmethod
+ def _get_wind_bearing_day(day_data):
+ """Get wind bearing (day) from weather data."""
+ val = get_forecast_day_value(
+ day_data[AEMET_ATTR_WIND], key=AEMET_ATTR_DIRECTION
+ )
+ if val in WIND_BEARING_MAP:
+ return WIND_BEARING_MAP[val]
+ _LOGGER.error("%s not found in Wind Bearing map", val)
+ return None
+
+ @staticmethod
+ def _get_wind_max_speed(day_data, hour):
+ """Get wind max speed from weather data."""
+ val = get_forecast_hour_value(day_data[AEMET_ATTR_WIND_GUST], hour)
+ if val:
+ return format_int(val)
+ return None
+
+ @staticmethod
+ def _get_wind_speed(day_data, hour):
+ """Get wind speed (hour) from weather data."""
+ val = get_forecast_hour_value(
+ day_data[AEMET_ATTR_WIND_GUST], hour, key=AEMET_ATTR_SPEED
+ )[0]
+ if val:
+ return format_int(val)
+ return None
+
+ @staticmethod
+ def _get_wind_speed_day(day_data):
+ """Get wind speed (day) from weather data."""
+ val = get_forecast_day_value(day_data[AEMET_ATTR_WIND], key=AEMET_ATTR_SPEED)
+ if val:
+ return format_int(val)
+ return None
+
+
+@dataclass
+class AemetWeather:
+ """Class to harmonize weather data model."""
+
+ daily: dict = field(default_factory=dict)
+ hourly: dict = field(default_factory=dict)
+ station: dict = field(default_factory=dict)
diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py
index 2d9021f80095c8..a5ffc511a26c0e 100644
--- a/homeassistant/components/aftership/sensor.py
+++ b/homeassistant/components/aftership/sensor.py
@@ -5,12 +5,11 @@
from pyaftership.tracker import Tracking
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME, HTTP_OK
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from .const import DOMAIN
@@ -108,7 +107,7 @@ async def handle_remove_tracking(call):
)
-class AfterShipSensor(Entity):
+class AfterShipSensor(SensorEntity):
"""Representation of a AfterShip sensor."""
def __init__(self, aftership, name):
@@ -134,7 +133,7 @@ def unit_of_measurement(self):
return "packages"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return attributes for the sensor."""
return self._attributes
diff --git a/homeassistant/components/agent_dvr/__init__.py b/homeassistant/components/agent_dvr/__init__.py
index fc52ee0ef366d5..3623f4f702a907 100644
--- a/homeassistant/components/agent_dvr/__init__.py
+++ b/homeassistant/components/agent_dvr/__init__.py
@@ -16,11 +16,6 @@
FORWARDS = ["alarm_control_panel", "camera"]
-async def async_setup(hass, config):
- """Old way to set up integrations."""
- return True
-
-
async def async_setup_entry(hass, config_entry):
"""Set up the Agent component."""
hass.data.setdefault(AGENT_DOMAIN, {})
diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py
index 571b5239de7ce5..24cd5dbb92cec6 100644
--- a/homeassistant/components/agent_dvr/camera.py
+++ b/homeassistant/components/agent_dvr/camera.py
@@ -102,13 +102,13 @@ async def async_update(self):
_LOGGER.debug("%s reacquired", self._name)
self._removed = False
except AgentError:
- if self.device.client.is_available: # server still available - camera error
- if not self._removed:
- _LOGGER.error("%s lost", self._name)
- self._removed = True
+ # server still available - camera error
+ if self.device.client.is_available and not self._removed:
+ _LOGGER.error("%s lost", self._name)
+ self._removed = True
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the Agent DVR camera state attributes."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/agent_dvr/config_flow.py b/homeassistant/components/agent_dvr/config_flow.py
index 9448b8d3123564..a21e6855337d2c 100644
--- a/homeassistant/components/agent_dvr/config_flow.py
+++ b/homeassistant/components/agent_dvr/config_flow.py
@@ -1,6 +1,4 @@
"""Config flow to configure Agent devices."""
-import logging
-
from agent import AgentConnectionError, AgentError
from agent.a import Agent
import voluptuous as vol
@@ -9,11 +7,10 @@
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from .const import DOMAIN, SERVER_URL # pylint:disable=unused-import
+from .const import DOMAIN, SERVER_URL
from .helpers import generate_url
DEFAULT_PORT = 8090
-_LOGGER = logging.getLogger(__name__)
class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/agent_dvr/translations/de.json b/homeassistant/components/agent_dvr/translations/de.json
index 6ea40d0fd007f0..10a8307ada1d16 100644
--- a/homeassistant/components/agent_dvr/translations/de.json
+++ b/homeassistant/components/agent_dvr/translations/de.json
@@ -4,8 +4,8 @@
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
- "already_in_progress": "Der Konfigurationsfluss f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.",
- "cannot_connect": "Verbindungsfehler"
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"step": {
"user": {
diff --git a/homeassistant/components/agent_dvr/translations/hu.json b/homeassistant/components/agent_dvr/translations/hu.json
index 1d28556ba1ad5a..49968ceea75ed9 100644
--- a/homeassistant/components/agent_dvr/translations/hu.json
+++ b/homeassistant/components/agent_dvr/translations/hu.json
@@ -1,6 +1,10 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
"step": {
diff --git a/homeassistant/components/agent_dvr/translations/id.json b/homeassistant/components/agent_dvr/translations/id.json
new file mode 100644
index 00000000000000..f2ee1cc662264b
--- /dev/null
+++ b/homeassistant/components/agent_dvr/translations/id.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ },
+ "title": "Siapkan Agent DVR"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/agent_dvr/translations/ko.json b/homeassistant/components/agent_dvr/translations/ko.json
index 30b96c00b636b9..add0b91710089b 100644
--- a/homeassistant/components/agent_dvr/translations/ko.json
+++ b/homeassistant/components/agent_dvr/translations/ko.json
@@ -4,7 +4,8 @@
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4."
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
diff --git a/homeassistant/components/agent_dvr/translations/nl.json b/homeassistant/components/agent_dvr/translations/nl.json
index ad625c169c8372..7c679f66c11936 100644
--- a/homeassistant/components/agent_dvr/translations/nl.json
+++ b/homeassistant/components/agent_dvr/translations/nl.json
@@ -4,6 +4,7 @@
"already_configured": "Apparaat is al geconfigureerd"
},
"error": {
+ "already_in_progress": "De configuratiestroom is al aan de gang",
"cannot_connect": "Kan geen verbinding maken"
},
"step": {
diff --git a/homeassistant/components/agent_dvr/translations/tr.json b/homeassistant/components/agent_dvr/translations/tr.json
new file mode 100644
index 00000000000000..31dddab779548b
--- /dev/null
+++ b/homeassistant/components/agent_dvr/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor",
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ },
+ "title": "Agent DVR'\u0131 kurun"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/agent_dvr/translations/uk.json b/homeassistant/components/agent_dvr/translations/uk.json
new file mode 100644
index 00000000000000..fef8d45d5a4927
--- /dev/null
+++ b/homeassistant/components/agent_dvr/translations/uk.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "title": "Agent DVR"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py
index 48423d08e69f68..d69a02f83bd798 100644
--- a/homeassistant/components/air_quality/__init__.py
+++ b/homeassistant/components/air_quality/__init__.py
@@ -1,8 +1,12 @@
"""Component for handling Air Quality data for your location."""
from datetime import timedelta
import logging
+from typing import final
-from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
+from homeassistant.const import (
+ ATTR_ATTRIBUTION,
+ CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+)
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
@@ -13,7 +17,6 @@
_LOGGER = logging.getLogger(__name__)
ATTR_AQI = "air_quality_index"
-ATTR_ATTRIBUTION = "attribution"
ATTR_CO2 = "carbon_dioxide"
ATTR_CO = "carbon_monoxide"
ATTR_N2O = "nitrogen_oxide"
@@ -129,6 +132,7 @@ def nitrogen_dioxide(self):
"""Return the NO2 (nitrogen dioxide) level."""
return None
+ @final
@property
def state_attributes(self):
"""Return the state attributes."""
diff --git a/homeassistant/components/air_quality/group.py b/homeassistant/components/air_quality/group.py
index 4741f8a3b548cd..2ac081496cdd90 100644
--- a/homeassistant/components/air_quality/group.py
+++ b/homeassistant/components/air_quality/group.py
@@ -2,13 +2,12 @@
from homeassistant.components.group import GroupIntegrationRegistry
-from homeassistant.core import callback
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import HomeAssistant, callback
@callback
def async_describe_on_off_states(
- hass: HomeAssistantType, registry: GroupIntegrationRegistry
+ hass: HomeAssistant, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.exclude_domain()
diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py
index 9d6b46f82e5dfa..41a7c03e636a29 100644
--- a/homeassistant/components/airly/__init__.py
+++ b/homeassistant/components/airly/__init__.py
@@ -1,4 +1,4 @@
-"""The Airly component."""
+"""The Airly integration."""
import asyncio
from datetime import timedelta
import logging
@@ -10,8 +10,6 @@
import async_timeout
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
-from homeassistant.core import Config, HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -44,11 +42,6 @@ def set_update_interval(hass, instances):
return interval
-async def async_setup(hass: HomeAssistant, config: Config) -> bool:
- """Set up configured Airly."""
- return True
-
-
async def async_setup_entry(hass, config_entry):
"""Set up Airly as config entry."""
api_key = config_entry.data[CONF_API_KEY]
@@ -71,17 +64,14 @@ async def async_setup_entry(hass, config_entry):
coordinator = AirlyDataUpdateCoordinator(
hass, websession, api_key, latitude, longitude, update_interval, use_nearest
)
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = coordinator
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return True
@@ -92,8 +82,8 @@ async def async_unload_entry(hass, config_entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -144,6 +134,12 @@ async def _async_update_data(self):
except (AirlyError, ClientConnectorError) as error:
raise UpdateFailed(error) from error
+ _LOGGER.debug(
+ "Requests remaining: %s/%s",
+ self.airly.requests_remaining,
+ self.airly.requests_per_day,
+ )
+
values = measurements.current["values"]
index = measurements.current["indexes"][0]
standards = measurements.current["standards"]
diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py
index e43a76b34187fe..f89a804ab3bc11 100644
--- a/homeassistant/components/airly/air_quality.py
+++ b/homeassistant/components/airly/air_quality.py
@@ -19,14 +19,13 @@
ATTR_API_PM25,
ATTR_API_PM25_LIMIT,
ATTR_API_PM25_PERCENT,
+ ATTRIBUTION,
DEFAULT_NAME,
DOMAIN,
+ LABEL_ADVICE,
MANUFACTURER,
)
-ATTRIBUTION = "Data provided by Airly"
-
-LABEL_ADVICE = "advice"
LABEL_AQI_DESCRIPTION = f"{ATTR_AQI}_description"
LABEL_AQI_LEVEL = f"{ATTR_AQI}_level"
LABEL_PM_2_5_LIMIT = f"{ATTR_PM_2_5}_limit"
@@ -118,7 +117,7 @@ def device_info(self):
}
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attrs = {
LABEL_AQI_DESCRIPTION: self.coordinator.data[ATTR_API_CAQI_DESCRIPTION],
diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py
index d7636d1db335e2..3b1432f77ba406 100644
--- a/homeassistant/components/airly/config_flow.py
+++ b/homeassistant/components/airly/config_flow.py
@@ -16,11 +16,7 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from .const import ( # pylint:disable=unused-import
- CONF_USE_NEAREST,
- DOMAIN,
- NO_AIRLY_SENSORS,
-)
+from .const import CONF_USE_NEAREST, DOMAIN, NO_AIRLY_SENSORS
class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py
index b4711b50dd2309..b8d2270c3c426a 100644
--- a/homeassistant/components/airly/const.py
+++ b/homeassistant/components/airly/const.py
@@ -1,4 +1,5 @@
"""Constants for Airly integration."""
+
ATTR_API_ADVICE = "ADVICE"
ATTR_API_CAQI = "CAQI"
ATTR_API_CAQI_DESCRIPTION = "DESCRIPTION"
@@ -13,9 +14,15 @@
ATTR_API_PM25_PERCENT = "PM25_PERCENT"
ATTR_API_PRESSURE = "PRESSURE"
ATTR_API_TEMPERATURE = "TEMPERATURE"
+
+ATTR_LABEL = "label"
+ATTR_UNIT = "unit"
+
+ATTRIBUTION = "Data provided by Airly"
CONF_USE_NEAREST = "use_nearest"
DEFAULT_NAME = "Airly"
DOMAIN = "airly"
+LABEL_ADVICE = "advice"
MANUFACTURER = "Airly sp. z o.o."
MAX_REQUESTS_PER_DAY = 100
NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet."
diff --git a/homeassistant/components/airly/manifest.json b/homeassistant/components/airly/manifest.json
index 77de843ffce631..a5ff485d1d0aa1 100644
--- a/homeassistant/components/airly/manifest.json
+++ b/homeassistant/components/airly/manifest.json
@@ -3,7 +3,7 @@
"name": "Airly",
"documentation": "https://www.home-assistant.io/integrations/airly",
"codeowners": ["@bieniu"],
- "requirements": ["airly==1.0.0"],
+ "requirements": ["airly==1.1.0"],
"config_flow": true,
"quality_scale": "platinum"
}
diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py
index 420d11a5963139..2a52050db15b5a 100644
--- a/homeassistant/components/airly/sensor.py
+++ b/homeassistant/components/airly/sensor.py
@@ -1,7 +1,9 @@
"""Support for the Airly sensor service."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
+ ATTR_ICON,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONF_NAME,
DEVICE_CLASS_HUMIDITY,
@@ -18,17 +20,14 @@
ATTR_API_PM1,
ATTR_API_PRESSURE,
ATTR_API_TEMPERATURE,
+ ATTR_LABEL,
+ ATTR_UNIT,
+ ATTRIBUTION,
DEFAULT_NAME,
DOMAIN,
MANUFACTURER,
)
-ATTRIBUTION = "Data provided by Airly"
-
-ATTR_ICON = "icon"
-ATTR_LABEL = "label"
-ATTR_UNIT = "unit"
-
PARALLEL_UPDATES = 1
SENSOR_TYPES = {
@@ -74,7 +73,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors, False)
-class AirlySensor(CoordinatorEntity):
+class AirlySensor(CoordinatorEntity, SensorEntity):
"""Define an Airly sensor."""
def __init__(self, coordinator, name, kind):
@@ -104,7 +103,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attrs
diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json
index afda73ae887986..c6b6f1e6a41223 100644
--- a/homeassistant/components/airly/strings.json
+++ b/homeassistant/components/airly/strings.json
@@ -22,7 +22,9 @@
},
"system_health": {
"info": {
- "can_reach_server": "Reach Airly server"
+ "can_reach_server": "Reach Airly server",
+ "requests_remaining": "Remaining allowed requests",
+ "requests_per_day": "Allowed requests per day"
}
}
}
diff --git a/homeassistant/components/airly/system_health.py b/homeassistant/components/airly/system_health.py
index 6b683518ebdead..3f2ed8e8d65cd8 100644
--- a/homeassistant/components/airly/system_health.py
+++ b/homeassistant/components/airly/system_health.py
@@ -4,6 +4,8 @@
from homeassistant.components import system_health
from homeassistant.core import HomeAssistant, callback
+from .const import DOMAIN
+
@callback
def async_register(
@@ -15,8 +17,13 @@ def async_register(
async def system_health_info(hass):
"""Get info for the info page."""
+ requests_remaining = list(hass.data[DOMAIN].values())[0].airly.requests_remaining
+ requests_per_day = list(hass.data[DOMAIN].values())[0].airly.requests_per_day
+
return {
"can_reach_server": system_health.async_check_can_reach_url(
hass, Airly.AIRLY_API_URL
- )
+ ),
+ "requests_remaining": requests_remaining,
+ "requests_per_day": requests_per_day,
}
diff --git a/homeassistant/components/airly/translations/ca.json b/homeassistant/components/airly/translations/ca.json
index 95400de23b438b..e76cec94f4ccdc 100644
--- a/homeassistant/components/airly/translations/ca.json
+++ b/homeassistant/components/airly/translations/ca.json
@@ -22,7 +22,9 @@
},
"system_health": {
"info": {
- "can_reach_server": "Servidor d'Airly accessible"
+ "can_reach_server": "Servidor d'Airly accessible",
+ "requests_per_day": "Sol\u00b7licituds per dia permeses",
+ "requests_remaining": "Sol\u00b7licituds permeses restants"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/de.json b/homeassistant/components/airly/translations/de.json
index 743a68a010ef63..b13798c0ae3d26 100644
--- a/homeassistant/components/airly/translations/de.json
+++ b/homeassistant/components/airly/translations/de.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Die Airly-Integration ist f\u00fcr diese Koordinaten bereits konfiguriert."
+ "already_configured": "Standort ist bereits konfiguriert"
},
"error": {
+ "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel",
"wrong_location": "Keine Airly Luftmessstation an diesem Ort"
},
"step": {
@@ -12,7 +13,7 @@
"api_key": "API-Schl\u00fcssel",
"latitude": "Breitengrad",
"longitude": "L\u00e4ngengrad",
- "name": "Name der Integration"
+ "name": "Name"
},
"description": "Einrichtung der Airly-Luftqualit\u00e4t Integration. Um einen API-Schl\u00fcssel zu generieren, registriere dich auf https://developer.airly.eu/register",
"title": "Airly"
@@ -21,7 +22,9 @@
},
"system_health": {
"info": {
- "can_reach_server": "Airly Server erreichen"
+ "can_reach_server": "Airly-Server erreichen",
+ "requests_per_day": "Erlaubte Anfragen pro Tag",
+ "requests_remaining": "Verbleibende erlaubte Anfragen"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/en.json b/homeassistant/components/airly/translations/en.json
index 720f68f8349c9d..0a5426c87d845a 100644
--- a/homeassistant/components/airly/translations/en.json
+++ b/homeassistant/components/airly/translations/en.json
@@ -22,7 +22,9 @@
},
"system_health": {
"info": {
- "can_reach_server": "Reach Airly server"
+ "can_reach_server": "Reach Airly server",
+ "requests_per_day": "Allowed requests per day",
+ "requests_remaining": "Remaining allowed requests"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/es.json b/homeassistant/components/airly/translations/es.json
index a0ed36a71691c7..a96a2f62293b41 100644
--- a/homeassistant/components/airly/translations/es.json
+++ b/homeassistant/components/airly/translations/es.json
@@ -22,7 +22,9 @@
},
"system_health": {
"info": {
- "can_reach_server": "Alcanzar el servidor Airly"
+ "can_reach_server": "Alcanzar el servidor Airly",
+ "requests_per_day": "Solicitudes permitidas por d\u00eda",
+ "requests_remaining": "Solicitudes permitidas restantes"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/et.json b/homeassistant/components/airly/translations/et.json
index 8cbfd138257a91..6730e131ac2c8d 100644
--- a/homeassistant/components/airly/translations/et.json
+++ b/homeassistant/components/airly/translations/et.json
@@ -22,7 +22,9 @@
},
"system_health": {
"info": {
- "can_reach_server": "\u00dchendus Airly serveriga"
+ "can_reach_server": "\u00dchendus Airly serveriga",
+ "requests_per_day": "Lubatud p\u00e4ringuid p\u00e4evas",
+ "requests_remaining": "Lubatud p\u00e4ringute arv"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/fr.json b/homeassistant/components/airly/translations/fr.json
index 98407155f17bfd..a23f455e0b82ce 100644
--- a/homeassistant/components/airly/translations/fr.json
+++ b/homeassistant/components/airly/translations/fr.json
@@ -22,7 +22,9 @@
},
"system_health": {
"info": {
- "can_reach_server": "Acc\u00e8s au serveur Airly"
+ "can_reach_server": "Acc\u00e8s au serveur Airly",
+ "requests_per_day": "Demandes autoris\u00e9es par jour",
+ "requests_remaining": "Demandes autoris\u00e9es restantes"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/he.json b/homeassistant/components/airly/translations/he.json
new file mode 100644
index 00000000000000..4c49313d97741a
--- /dev/null
+++ b/homeassistant/components/airly/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/hu.json b/homeassistant/components/airly/translations/hu.json
index b96734935a0245..b9fd0c9e05cc5a 100644
--- a/homeassistant/components/airly/translations/hu.json
+++ b/homeassistant/components/airly/translations/hu.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Ezen koordin\u00e1t\u00e1k Airly integr\u00e1ci\u00f3ja m\u00e1r konfigur\u00e1lva van."
+ "already_configured": "A hely m\u00e1r konfigur\u00e1lva van"
},
"error": {
+ "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs",
"wrong_location": "Ezen a ter\u00fcleten nincs Airly m\u00e9r\u0151\u00e1llom\u00e1s."
},
"step": {
@@ -12,11 +13,17 @@
"api_key": "API kulcs",
"latitude": "Sz\u00e9less\u00e9g",
"longitude": "Hossz\u00fas\u00e1g",
- "name": "Az integr\u00e1ci\u00f3 neve"
+ "name": "N\u00e9v"
},
"description": "Az Airly leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Api-kulcs l\u00e9trehoz\u00e1s\u00e1hoz nyissa meg a k\u00f6vetkez\u0151 weboldalt: https://developer.airly.eu/register",
"title": "Airly"
}
}
+ },
+ "system_health": {
+ "info": {
+ "requests_per_day": "Enged\u00e9lyezett k\u00e9r\u00e9sek naponta",
+ "requests_remaining": "Fennmarad\u00f3 enged\u00e9lyezett k\u00e9r\u00e9sek"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/id.json b/homeassistant/components/airly/translations/id.json
new file mode 100644
index 00000000000000..57b4c0d95f9eab
--- /dev/null
+++ b/homeassistant/components/airly/translations/id.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Lokasi sudah dikonfigurasi"
+ },
+ "error": {
+ "invalid_api_key": "Kunci API tidak valid",
+ "wrong_location": "Tidak ada stasiun pengukur Airly di daerah ini."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kunci API",
+ "latitude": "Lintang",
+ "longitude": "Bujur",
+ "name": "Nama"
+ },
+ "description": "Siapkan integrasi kualitas udara Airly. Untuk membuat kunci API, buka https://developer.airly.eu/register",
+ "title": "Airly"
+ }
+ }
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "Keterjangkauan server Airly",
+ "requests_per_day": "Permintaan yang diizinkan per hari",
+ "requests_remaining": "Sisa permintaan yang diizinkan"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/it.json b/homeassistant/components/airly/translations/it.json
index bf6d7a461ceb0e..385b8117437b2c 100644
--- a/homeassistant/components/airly/translations/it.json
+++ b/homeassistant/components/airly/translations/it.json
@@ -22,7 +22,9 @@
},
"system_health": {
"info": {
- "can_reach_server": "Raggiungi il server Airly"
+ "can_reach_server": "Raggiungi il server Airly",
+ "requests_per_day": "Richieste consentite al giorno",
+ "requests_remaining": "Richieste consentite rimanenti"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/ko.json b/homeassistant/components/airly/translations/ko.json
index a0b20ed8c443b2..1f9db4a592ed1d 100644
--- a/homeassistant/components/airly/translations/ko.json
+++ b/homeassistant/components/airly/translations/ko.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 \uc88c\ud45c\uc5d0 \ub300\ud55c Airly \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
+ "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"wrong_location": "\uc774 \uc9c0\uc5ed\uc5d0\ub294 Airly \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc774 \uc5c6\uc2b5\ub2c8\ub2e4."
},
"step": {
@@ -12,11 +13,18 @@
"api_key": "API \ud0a4",
"latitude": "\uc704\ub3c4",
"longitude": "\uacbd\ub3c4",
- "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984"
+ "name": "\uc774\ub984"
},
"description": "Airly \uacf5\uae30 \ud488\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://developer.airly.eu/register \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694",
"title": "Airly"
}
}
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "Airly \uc11c\ubc84 \uc5f0\uacb0",
+ "requests_per_day": "\uc77c\uc77c \ud5c8\uc6a9 \uc694\uccad \ud69f\uc218",
+ "requests_remaining": "\ub0a8\uc740 \ud5c8\uc6a9 \uc694\uccad \ud69f\uc218"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/nl.json b/homeassistant/components/airly/translations/nl.json
index a7bc9966f637e2..14cbaf1711e922 100644
--- a/homeassistant/components/airly/translations/nl.json
+++ b/homeassistant/components/airly/translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Airly-integratie voor deze co\u00f6rdinaten is al geconfigureerd."
+ "already_configured": "Locatie is al geconfigureerd."
},
"error": {
"invalid_api_key": "Ongeldige API-sleutel",
@@ -10,14 +10,21 @@
"step": {
"user": {
"data": {
- "api_key": "Airly API-sleutel",
+ "api_key": "API-sleutel",
"latitude": "Breedtegraad",
"longitude": "Lengtegraad",
- "name": "Naam van de integratie"
+ "name": "Naam"
},
"description": "Airly-integratie van luchtkwaliteit instellen. Ga naar https://developer.airly.eu/register om de API-sleutel te genereren",
"title": "Airly"
}
}
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "Kan Airly server bereiken",
+ "requests_per_day": "Toegestane verzoeken per dag",
+ "requests_remaining": "Resterende toegestane verzoeken"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/no.json b/homeassistant/components/airly/translations/no.json
index b38568210ad07b..4c81422d93c90a 100644
--- a/homeassistant/components/airly/translations/no.json
+++ b/homeassistant/components/airly/translations/no.json
@@ -22,7 +22,9 @@
},
"system_health": {
"info": {
- "can_reach_server": "N\u00e5 Airly-serveren"
+ "can_reach_server": "N\u00e5 Airly-serveren",
+ "requests_per_day": "Tillatte foresp\u00f8rsler per dag",
+ "requests_remaining": "Gjenv\u00e6rende tillatte foresp\u00f8rsler"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/pl.json b/homeassistant/components/airly/translations/pl.json
index e36e6f86ec72df..f205a569474497 100644
--- a/homeassistant/components/airly/translations/pl.json
+++ b/homeassistant/components/airly/translations/pl.json
@@ -22,7 +22,9 @@
},
"system_health": {
"info": {
- "can_reach_server": "Dost\u0119p do serwera Airly"
+ "can_reach_server": "Dost\u0119p do serwera Airly",
+ "requests_per_day": "Dozwolone dzienne \u017c\u0105dania",
+ "requests_remaining": "Pozosta\u0142o dozwolonych \u017c\u0105da\u0144"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/ru.json b/homeassistant/components/airly/translations/ru.json
index b1469af787e617..41ca90a8c027d1 100644
--- a/homeassistant/components/airly/translations/ru.json
+++ b/homeassistant/components/airly/translations/ru.json
@@ -22,7 +22,9 @@
},
"system_health": {
"info": {
- "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Airly"
+ "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Airly",
+ "requests_per_day": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0432 \u0434\u0435\u043d\u044c",
+ "requests_remaining": "\u0421\u0447\u0451\u0442\u0447\u0438\u043a \u043e\u0441\u0442\u0430\u0432\u0448\u0438\u0445\u0441\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/tr.json b/homeassistant/components/airly/translations/tr.json
index 1b6e9caa24c4f5..144acc1e1aedd6 100644
--- a/homeassistant/components/airly/translations/tr.json
+++ b/homeassistant/components/airly/translations/tr.json
@@ -1,4 +1,21 @@
{
+ "config": {
+ "abort": {
+ "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Anahtar\u0131",
+ "latitude": "Enlem",
+ "longitude": "Boylam"
+ }
+ }
+ }
+ },
"system_health": {
"info": {
"can_reach_server": "Airly sunucusuna eri\u015fin"
diff --git a/homeassistant/components/airly/translations/uk.json b/homeassistant/components/airly/translations/uk.json
new file mode 100644
index 00000000000000..51bcf5195dfd2d
--- /dev/null
+++ b/homeassistant/components/airly/translations/uk.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435."
+ },
+ "error": {
+ "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API",
+ "wrong_location": "\u0423 \u0446\u0456\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0456 \u043d\u0435\u043c\u0430\u0454 \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043b\u044c\u043d\u0438\u0445 \u0441\u0442\u0430\u043d\u0446\u0456\u0439 Airly."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
+ "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430",
+ "name": "\u041d\u0430\u0437\u0432\u0430"
+ },
+ "description": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0441\u0435\u0440\u0432\u0456\u0441\u0443 \u0437 \u0430\u043d\u0430\u043b\u0456\u0437\u0443 \u044f\u043a\u043e\u0441\u0442\u0456 \u043f\u043e\u0432\u0456\u0442\u0440\u044f Airly. \u0429\u043e\u0431 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c https://developer.airly.eu/register.",
+ "title": "Airly"
+ }
+ }
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Airly"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/zh-Hans.json b/homeassistant/components/airly/translations/zh-Hans.json
index 1a57bfbadf9c0a..0f3c5137d31b43 100644
--- a/homeassistant/components/airly/translations/zh-Hans.json
+++ b/homeassistant/components/airly/translations/zh-Hans.json
@@ -13,7 +13,8 @@
},
"system_health": {
"info": {
- "can_reach_server": "\u53ef\u8bbf\u95ee Airly \u670d\u52a1\u5668"
+ "can_reach_server": "\u53ef\u8bbf\u95ee Airly \u670d\u52a1\u5668",
+ "requests_per_day": "\u5141\u8bb8\u6bcf\u5929\u8bf7\u6c42"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/zh-Hant.json b/homeassistant/components/airly/translations/zh-Hant.json
index 4d60b158c4c276..19ef2ae7532cde 100644
--- a/homeassistant/components/airly/translations/zh-Hant.json
+++ b/homeassistant/components/airly/translations/zh-Hant.json
@@ -22,7 +22,9 @@
},
"system_health": {
"info": {
- "can_reach_server": "\u9023\u7dda Airly \u4f3a\u670d\u5668"
+ "can_reach_server": "\u9023\u7dda Airly \u4f3a\u670d\u5668",
+ "requests_per_day": "\u6bcf\u65e5\u5141\u8a31\u7684\u8acb\u6c42",
+ "requests_remaining": "\u5176\u9918\u5141\u8a31\u7684\u8acb\u6c42"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py
index 5cbc87947f92de..b1770dcbde788c 100644
--- a/homeassistant/components/airnow/__init__.py
+++ b/homeassistant/components/airnow/__init__.py
@@ -11,7 +11,6 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -38,11 +37,6 @@
PLATFORMS = ["sensor"]
-async def async_setup(hass: HomeAssistant, config: dict):
- """Set up the AirNow component."""
- return True
-
-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up AirNow from a config entry."""
api_key = entry.data[CONF_API_KEY]
@@ -60,17 +54,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
)
# Sync with Coordinator
- await coordinator.async_refresh()
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
# Store Entity and Initialize Platforms
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -81,8 +73,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/airnow/config_flow.py b/homeassistant/components/airnow/config_flow.py
index 6d53ac133ee4f9..b4de58808da361 100644
--- a/homeassistant/components/airnow/config_flow.py
+++ b/homeassistant/components/airnow/config_flow.py
@@ -10,7 +10,7 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py
index fed6def2b36e62..2d3adc8d1e2ae7 100644
--- a/homeassistant/components/airnow/sensor.py
+++ b/homeassistant/components/airnow/sensor.py
@@ -1,7 +1,9 @@
"""Support for the AirNow sensor service."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
+ ATTR_ICON,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
)
@@ -20,7 +22,6 @@
ATTRIBUTION = "Data provided by AirNow"
-ATTR_ICON = "icon"
ATTR_LABEL = "label"
ATTR_UNIT = "unit"
@@ -59,7 +60,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors, False)
-class AirNowSensor(CoordinatorEntity):
+class AirNowSensor(CoordinatorEntity, SensorEntity):
"""Define an AirNow sensor."""
def __init__(self, coordinator, kind):
@@ -84,7 +85,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self.kind == ATTR_API_AQI:
self._attrs[SENSOR_AQI_ATTR_DESCR] = self.coordinator.data[
diff --git a/homeassistant/components/airnow/translations/bg.json b/homeassistant/components/airnow/translations/bg.json
new file mode 100644
index 00000000000000..5d274ec2b73c77
--- /dev/null
+++ b/homeassistant/components/airnow/translations/bg.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "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/airnow/translations/ca.json b/homeassistant/components/airnow/translations/ca.json
new file mode 100644
index 00000000000000..2db3cfad563d9f
--- /dev/null
+++ b/homeassistant/components/airnow/translations/ca.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "invalid_location": "No s'ha trobat cap resultat per a aquesta ubicaci\u00f3",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Clau API",
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "radius": "Radi de l'estaci\u00f3 (milles; opcional)"
+ },
+ "description": "Configura la integraci\u00f3 de qualitat d'aire AirNow. Per generar la clau API, v\u00e9s a https://docs.airnowapi.org/account/request/",
+ "title": "AirNow"
+ }
+ }
+ },
+ "title": "AirNow"
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/cs.json b/homeassistant/components/airnow/translations/cs.json
new file mode 100644
index 00000000000000..d978e44c70af64
--- /dev/null
+++ b/homeassistant/components/airnow/translations/cs.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno"
+ },
+ "error": {
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
+ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
+ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kl\u00ed\u010d API",
+ "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka",
+ "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka"
+ },
+ "title": "AirNow"
+ }
+ }
+ },
+ "title": "AirNow"
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/de.json b/homeassistant/components/airnow/translations/de.json
new file mode 100644
index 00000000000000..8c2b47c1bd4962
--- /dev/null
+++ b/homeassistant/components/airnow/translations/de.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "invalid_location": "F\u00fcr diesen Standort wurden keine Ergebnisse gefunden",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-Schl\u00fcssel",
+ "latitude": "Breitengrad",
+ "longitude": "L\u00e4ngengrad"
+ },
+ "description": "Richten Sie die AirNow-Luftqualit\u00e4tsintegration ein. Um den API-Schl\u00fcssel zu generieren, besuchen Sie https://docs.airnowapi.org/account/request/.",
+ "title": "AirNow"
+ }
+ }
+ },
+ "title": "AirNow"
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/en.json b/homeassistant/components/airnow/translations/en.json
index 5c5259c74e229d..371bb270ac170a 100644
--- a/homeassistant/components/airnow/translations/en.json
+++ b/homeassistant/components/airnow/translations/en.json
@@ -1,13 +1,13 @@
{
"config": {
"abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ "already_configured": "Device is already configured"
},
"error": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
"invalid_location": "No results found for that location",
- "unknown": "[%key:common::config_flow::error::unknown%]"
+ "unknown": "Unexpected error"
},
"step": {
"user": {
@@ -15,7 +15,6 @@
"api_key": "API Key",
"latitude": "Latitude",
"longitude": "Longitude",
- "name": "Name of the Entity",
"radius": "Station Radius (miles; optional)"
},
"description": "Set up AirNow air quality integration. To generate API key go to https://docs.airnowapi.org/account/request/",
diff --git a/homeassistant/components/airnow/translations/es.json b/homeassistant/components/airnow/translations/es.json
new file mode 100644
index 00000000000000..d6a228a6e27784
--- /dev/null
+++ b/homeassistant/components/airnow/translations/es.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositivo ya est\u00e1 configurado"
+ },
+ "error": {
+ "cannot_connect": "No se pudo conectar",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
+ "invalid_location": "No se han encontrado resultados para esa ubicaci\u00f3n",
+ "unknown": "Error inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Clave API",
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "radius": "Radio de la estaci\u00f3n (millas; opcional)"
+ },
+ "description": "Configurar la integraci\u00f3n de calidad del aire de AirNow. Para generar una clave API, ve a https://docs.airnowapi.org/account/request/",
+ "title": "AirNow"
+ }
+ }
+ },
+ "title": "AirNow"
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/et.json b/homeassistant/components/airnow/translations/et.json
new file mode 100644
index 00000000000000..52b2bb618e0246
--- /dev/null
+++ b/homeassistant/components/airnow/translations/et.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_auth": "Vigane autentimine",
+ "invalid_location": "Selle asukoha jaoks ei leitud andmeid",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API v\u00f5ti",
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad",
+ "radius": "Jaama raadius (miilid; valikuline)"
+ },
+ "description": "Seadista AirNow \u00f5hukvaliteedi sidumine. API-v\u00f5tme loomiseks mine aadressile https://docs.airnowapi.org/account/request/",
+ "title": ""
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/fr.json b/homeassistant/components/airnow/translations/fr.json
new file mode 100644
index 00000000000000..ff85d9318e9408
--- /dev/null
+++ b/homeassistant/components/airnow/translations/fr.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec \u00e0 la connexion",
+ "invalid_auth": "Authentification invalide",
+ "invalid_location": "Aucun r\u00e9sultat trouv\u00e9 pour cet emplacement",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Cl\u00e9 API",
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "radius": "Rayon d'action de la station (en miles, facultatif)"
+ },
+ "description": "Configurez l'int\u00e9gration de la qualit\u00e9 de l'air AirNow. Pour g\u00e9n\u00e9rer la cl\u00e9 API, acc\u00e9dez \u00e0 https://docs.airnowapi.org/account/request/",
+ "title": "AirNow"
+ }
+ }
+ },
+ "title": "AirNow"
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/hu.json b/homeassistant/components/airnow/translations/hu.json
new file mode 100644
index 00000000000000..418450f24198c7
--- /dev/null
+++ b/homeassistant/components/airnow/translations/hu.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "invalid_location": "Erre a helyre nem tal\u00e1lhat\u00f3 eredm\u00e9ny",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API kulcs",
+ "latitude": "Sz\u00e9less\u00e9g",
+ "longitude": "Hossz\u00fas\u00e1g"
+ },
+ "title": "AirNow"
+ }
+ }
+ },
+ "title": "AirNow"
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/id.json b/homeassistant/components/airnow/translations/id.json
new file mode 100644
index 00000000000000..66fdff72fae3b2
--- /dev/null
+++ b/homeassistant/components/airnow/translations/id.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "invalid_location": "Tidak ada hasil yang ditemukan untuk lokasi tersebut",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kunci API",
+ "latitude": "Lintang",
+ "longitude": "Bujur",
+ "radius": "Radius Stasiun (mil; opsional)"
+ },
+ "description": "Siapkan integrasi kualitas udara AirNow. Untuk membuat kunci API buka https://docs.airnowapi.org/account/request/",
+ "title": "AirNow"
+ }
+ }
+ },
+ "title": "AirNow"
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/it.json b/homeassistant/components/airnow/translations/it.json
new file mode 100644
index 00000000000000..9dda15dfbd205b
--- /dev/null
+++ b/homeassistant/components/airnow/translations/it.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
+ "invalid_location": "Nessun risultato trovato per quella localit\u00e0",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Chiave API",
+ "latitude": "Latitudine",
+ "longitude": "Logitudine",
+ "radius": "Raggio stazione (miglia; opzionale)"
+ },
+ "description": "Configura l'integrazione per la qualit\u00e0 dell'aria AirNow. Per generare la chiave API, vai su https://docs.airnowapi.org/account/request/",
+ "title": "AirNow"
+ }
+ }
+ },
+ "title": "AirNow"
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/ko.json b/homeassistant/components/airnow/translations/ko.json
new file mode 100644
index 00000000000000..adfbf0be8ed5e3
--- /dev/null
+++ b/homeassistant/components/airnow/translations/ko.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_location": "\ud574\ub2f9 \uc704\uce58\uc5d0 \uacb0\uacfc\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API \ud0a4",
+ "latitude": "\uc704\ub3c4",
+ "longitude": "\uacbd\ub3c4",
+ "radius": "\uce21\uc815 \uc2a4\ud14c\uc774\uc158 \ubc18\uacbd (\ub9c8\uc77c; \uc120\ud0dd \uc0ac\ud56d)"
+ },
+ "description": "AirNow \uacf5\uae30 \ud488\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://docs.airnowapi.org/account/request \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694",
+ "title": "AirNow"
+ }
+ }
+ },
+ "title": "AirNow"
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/lb.json b/homeassistant/components/airnow/translations/lb.json
new file mode 100644
index 00000000000000..a62bd0bf478e4a
--- /dev/null
+++ b/homeassistant/components/airnow/translations/lb.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparat ass scho konfigur\u00e9iert"
+ },
+ "error": {
+ "cannot_connect": "Feeler beim verbannen",
+ "invalid_auth": "Ong\u00eblteg Authentifikatioun",
+ "invalid_location": "Keng Resultater fonnt fir d\u00ebse Standuert",
+ "unknown": "Onerwaarte Feeler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Schl\u00ebssel",
+ "latitude": "L\u00e4ngegrad",
+ "longitude": "Breedegrag"
+ },
+ "title": "AirNow"
+ }
+ }
+ },
+ "title": "AirNow"
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/nl.json b/homeassistant/components/airnow/translations/nl.json
new file mode 100644
index 00000000000000..090a5363823cf0
--- /dev/null
+++ b/homeassistant/components/airnow/translations/nl.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
+ "invalid_location": "Geen resultaten gevonden voor die locatie",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-sleutel",
+ "latitude": "Breedtegraad",
+ "longitude": "Lengtegraad",
+ "radius": "Stationsradius (mijl; optioneel)"
+ },
+ "description": "AirNow luchtkwaliteit integratie opzetten. Om een API sleutel te genereren ga naar https://docs.airnowapi.org/account/request/",
+ "title": "AirNow"
+ }
+ }
+ },
+ "title": "AirNow"
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/no.json b/homeassistant/components/airnow/translations/no.json
new file mode 100644
index 00000000000000..19fa7e12207b27
--- /dev/null
+++ b/homeassistant/components/airnow/translations/no.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_auth": "Ugyldig godkjenning",
+ "invalid_location": "Ingen resultater funnet for den plasseringen",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-n\u00f8kkel",
+ "latitude": "Breddegrad",
+ "longitude": "Lengdegrad",
+ "radius": "Stasjonsradius (miles; valgfritt)"
+ },
+ "description": "Konfigurer integrering av luftkvalitet i AirNow. For \u00e5 generere en API-n\u00f8kkel, g\u00e5r du til https://docs.airnowapi.org/account/request/",
+ "title": ""
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/pl.json b/homeassistant/components/airnow/translations/pl.json
new file mode 100644
index 00000000000000..fe4310607b9422
--- /dev/null
+++ b/homeassistant/components/airnow/translations/pl.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "invalid_location": "Brak wynik\u00f3w dla tej lokalizacji",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Klucz API",
+ "latitude": "Szeroko\u015b\u0107 geograficzna",
+ "longitude": "D\u0142ugo\u015b\u0107 geograficzna",
+ "radius": "Promie\u0144 od stacji (w milach; opcjonalnie)"
+ },
+ "description": "Konfiguracja integracji jako\u015bci powietrza AirNow. Aby wygenerowa\u0107 klucz API, przejd\u017a do https://docs.airnowapi.org/account/request/",
+ "title": "AirNow"
+ }
+ }
+ },
+ "title": "AirNow"
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/pt.json b/homeassistant/components/airnow/translations/pt.json
new file mode 100644
index 00000000000000..3aa509dd6e814b
--- /dev/null
+++ b/homeassistant/components/airnow/translations/pt.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado"
+ },
+ "error": {
+ "cannot_connect": "Falha na liga\u00e7\u00e3o",
+ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida",
+ "unknown": "Erro inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Key",
+ "latitude": "Latitude",
+ "longitude": "Longitude"
+ },
+ "title": ""
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/ru.json b/homeassistant/components/airnow/translations/ru.json
new file mode 100644
index 00000000000000..9667accb7c4c61
--- /dev/null
+++ b/homeassistant/components/airnow/translations/ru.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "invalid_location": "\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u0432 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
+ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430",
+ "radius": "\u0420\u0430\u0434\u0438\u0443\u0441 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 (\u0432 \u043c\u0438\u043b\u044f\u0445; \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)"
+ },
+ "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043f\u043e \u0430\u043d\u0430\u043b\u0438\u0437\u0443 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 AirNow. \u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://docs.airnowapi.org/account/request/.",
+ "title": "AirNow"
+ }
+ }
+ },
+ "title": "AirNow"
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/tr.json b/homeassistant/components/airnow/translations/tr.json
new file mode 100644
index 00000000000000..06af714dc87426
--- /dev/null
+++ b/homeassistant/components/airnow/translations/tr.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "invalid_location": "Bu konum i\u00e7in hi\u00e7bir sonu\u00e7 bulunamad\u0131",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Anahtar\u0131",
+ "latitude": "Enlem",
+ "longitude": "Boylam",
+ "radius": "\u0130stasyon Yar\u0131\u00e7ap\u0131 (mil; iste\u011fe ba\u011fl\u0131)"
+ },
+ "title": "AirNow"
+ }
+ }
+ },
+ "title": "AirNow"
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/uk.json b/homeassistant/components/airnow/translations/uk.json
new file mode 100644
index 00000000000000..bb872123f54945
--- /dev/null
+++ b/homeassistant/components/airnow/translations/uk.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "invalid_location": "\u041d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0456\u0432 \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
+ "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430",
+ "radius": "\u0420\u0430\u0434\u0456\u0443\u0441 \u0441\u0442\u0430\u043d\u0446\u0456\u0457 (\u043c\u0438\u043b\u0456; \u043d\u0435\u043e\u0431\u043e\u0432\u2019\u044f\u0437\u043a\u043e\u0432\u043e)"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u044f\u043a\u043e\u0441\u0442\u0456 \u043f\u043e\u0432\u0456\u0442\u0440\u044f AirNow. \u0429\u043e\u0431 \u0437\u0433\u0435\u043d\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043d\u0430 \u0441\u0442\u043e\u0440\u0456\u043d\u043a\u0443 https://docs.airnowapi.org/account/request/",
+ "title": "AirNow"
+ }
+ }
+ },
+ "title": "AirNow"
+}
\ No newline at end of file
diff --git a/homeassistant/components/airnow/translations/zh-Hant.json b/homeassistant/components/airnow/translations/zh-Hant.json
new file mode 100644
index 00000000000000..0f6008e75a600b
--- /dev/null
+++ b/homeassistant/components/airnow/translations/zh-Hant.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "invalid_location": "\u627e\u4e0d\u5230\u8a72\u4f4d\u7f6e\u7684\u7d50\u679c",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API \u5bc6\u9470",
+ "latitude": "\u7def\u5ea6",
+ "longitude": "\u7d93\u5ea6",
+ "radius": "\u89c0\u6e2c\u7ad9\u534a\u5f91\uff08\u82f1\u91cc\uff1b\u9078\u9805\uff09"
+ },
+ "description": "\u6b32\u8a2d\u5b9a AirNow \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://docs.airnowapi.org/account/request/ \u7522\u751f API \u5bc6\u9470",
+ "title": "AirNow"
+ }
+ }
+ },
+ "title": "AirNow"
+}
\ No newline at end of file
diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py
index 956b168a665d55..f02020d25b4de1 100644
--- a/homeassistant/components/airvisual/__init__.py
+++ b/homeassistant/components/airvisual/__init__.py
@@ -37,7 +37,8 @@
CONF_INTEGRATION_TYPE,
DATA_COORDINATOR,
DOMAIN,
- INTEGRATION_TYPE_GEOGRAPHY,
+ INTEGRATION_TYPE_GEOGRAPHY_COORDS,
+ INTEGRATION_TYPE_GEOGRAPHY_NAME,
INTEGRATION_TYPE_NODE_PRO,
LOGGER,
)
@@ -78,9 +79,9 @@ def async_get_cloud_api_update_interval(hass, api_key, num_consumers):
This will shift based on the number of active consumers, thus keeping the user
under the monthly API limit.
"""
- # Assuming 10,000 calls per month and a "smallest possible month" of 28 days; note
+ # Assuming 10,000 calls per month and a "largest possible month" of 31 days; note
# that we give a buffer of 1500 API calls for any drift, restarts, etc.:
- minutes_between_api_calls = ceil(1 / (8500 / 28 / 24 / 60 / num_consumers))
+ minutes_between_api_calls = ceil(num_consumers * 31 * 24 * 60 / 8500)
LOGGER.debug(
"Leveling API key usage (%s): %s consumers, %s minutes between updates",
@@ -141,12 +142,21 @@ def _standardize_geography_config_entry(hass, config_entry):
if not config_entry.options:
# If the config entry doesn't already have any options set, set defaults:
entry_updates["options"] = {CONF_SHOW_ON_MAP: True}
- if CONF_INTEGRATION_TYPE not in config_entry.data:
- # If the config entry data doesn't contain the integration type, add it:
- entry_updates["data"] = {
- **config_entry.data,
- CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY,
- }
+ if config_entry.data.get(CONF_INTEGRATION_TYPE) not in [
+ INTEGRATION_TYPE_GEOGRAPHY_COORDS,
+ INTEGRATION_TYPE_GEOGRAPHY_NAME,
+ ]:
+ # If the config entry data doesn't contain an integration type that we know
+ # about, infer it from the data we have:
+ entry_updates["data"] = {**config_entry.data}
+ if CONF_CITY in config_entry.data:
+ entry_updates["data"][
+ CONF_INTEGRATION_TYPE
+ ] = INTEGRATION_TYPE_GEOGRAPHY_NAME
+ else:
+ entry_updates["data"][
+ CONF_INTEGRATION_TYPE
+ ] = INTEGRATION_TYPE_GEOGRAPHY_COORDS
if not entry_updates:
return
@@ -232,11 +242,6 @@ async def async_update_data():
update_method=async_update_data,
)
- hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
- async_sync_geo_coordinator_update_intervals(
- hass, config_entry.data[CONF_API_KEY]
- )
-
# Only geography-based entries have options:
hass.data[DOMAIN][DATA_LISTENER][
config_entry.entry_id
@@ -262,13 +267,19 @@ async def async_update_data():
update_method=async_update_data,
)
- hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
+ await coordinator.async_config_entry_first_refresh()
- await coordinator.async_refresh()
+ hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
- for component in PLATFORMS:
+ # Reassess the interval between 2 server requests
+ if CONF_API_KEY in config_entry.data:
+ async_sync_geo_coordinator_update_intervals(
+ hass, config_entry.data[CONF_API_KEY]
+ )
+
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return True
@@ -299,10 +310,14 @@ async def async_migrate_entry(hass, config_entry):
# For any geographies that remain, create a new config entry for each one:
for geography in geographies:
+ if CONF_LATITUDE in geography:
+ source = "geography_by_coords"
+ else:
+ source = "geography_by_name"
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
- context={"source": "geography"},
+ context={"source": source},
data={CONF_API_KEY: config_entry.data[CONF_API_KEY], **geography},
)
)
@@ -317,8 +332,8 @@ async def async_unload_entry(hass, config_entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -327,7 +342,7 @@ async def async_unload_entry(hass, config_entry):
remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id)
remove_listener()
- if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY:
+ if CONF_API_KEY in config_entry.data:
# Re-calculate the update interval period for any remaining consumers of
# this API key:
async_sync_geo_coordinator_update_intervals(
@@ -353,7 +368,7 @@ def __init__(self, coordinator):
self._unit = None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
return self._attrs
diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py
index b086aeefc2762d..128a06b740008e 100644
--- a/homeassistant/components/airvisual/config_flow.py
+++ b/homeassistant/components/airvisual/config_flow.py
@@ -2,7 +2,12 @@
import asyncio
from pyairvisual import CloudAPI, NodeSamba
-from pyairvisual.errors import InvalidKeyError, NodeProError
+from pyairvisual.errors import (
+ AirVisualError,
+ InvalidKeyError,
+ NodeProError,
+ NotFoundError,
+)
import voluptuous as vol
from homeassistant import config_entries
@@ -13,20 +18,46 @@
CONF_LONGITUDE,
CONF_PASSWORD,
CONF_SHOW_ON_MAP,
+ CONF_STATE,
)
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client, config_validation as cv
from . import async_get_geography_id
-from .const import ( # pylint: disable=unused-import
- CONF_GEOGRAPHIES,
+from .const import (
+ CONF_CITY,
+ CONF_COUNTRY,
CONF_INTEGRATION_TYPE,
DOMAIN,
- INTEGRATION_TYPE_GEOGRAPHY,
+ INTEGRATION_TYPE_GEOGRAPHY_COORDS,
+ INTEGRATION_TYPE_GEOGRAPHY_NAME,
INTEGRATION_TYPE_NODE_PRO,
LOGGER,
)
+API_KEY_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): cv.string})
+GEOGRAPHY_NAME_SCHEMA = API_KEY_DATA_SCHEMA.extend(
+ {
+ vol.Required(CONF_CITY): cv.string,
+ vol.Required(CONF_STATE): cv.string,
+ vol.Required(CONF_COUNTRY): cv.string,
+ }
+)
+NODE_PRO_SCHEMA = vol.Schema(
+ {vol.Required(CONF_IP_ADDRESS): str, vol.Required(CONF_PASSWORD): cv.string}
+)
+PICK_INTEGRATION_TYPE_SCHEMA = vol.Schema(
+ {
+ vol.Required("type"): vol.In(
+ [
+ INTEGRATION_TYPE_GEOGRAPHY_COORDS,
+ INTEGRATION_TYPE_GEOGRAPHY_NAME,
+ INTEGRATION_TYPE_NODE_PRO,
+ ]
+ )
+ }
+)
+
class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle an AirVisual config flow."""
@@ -36,16 +67,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self):
"""Initialize the config flow."""
+ self._entry_data_for_reauth = None
self._geo_id = None
- self._latitude = None
- self._longitude = None
-
- self.api_key_data_schema = vol.Schema({vol.Required(CONF_API_KEY): str})
@property
- def geography_schema(self):
+ def geography_coords_schema(self):
"""Return the data schema for the cloud API."""
- return self.api_key_data_schema.extend(
+ return API_KEY_DATA_SCHEMA.extend(
{
vol.Required(
CONF_LATITUDE, default=self.hass.config.latitude
@@ -56,62 +84,7 @@ def geography_schema(self):
}
)
- @property
- def pick_integration_type_schema(self):
- """Return the data schema for picking the integration type."""
- return vol.Schema(
- {
- vol.Required("type"): vol.In(
- [INTEGRATION_TYPE_GEOGRAPHY, INTEGRATION_TYPE_NODE_PRO]
- )
- }
- )
-
- @property
- def node_pro_schema(self):
- """Return the data schema for a Node/Pro."""
- return vol.Schema(
- {vol.Required(CONF_IP_ADDRESS): str, vol.Required(CONF_PASSWORD): str}
- )
-
- async def _async_set_unique_id(self, unique_id):
- """Set the unique ID of the config flow and abort if it already exists."""
- await self.async_set_unique_id(unique_id)
- self._abort_if_unique_id_configured()
-
- @staticmethod
- @callback
- def async_get_options_flow(config_entry):
- """Define the config flow to handle options."""
- return AirVisualOptionsFlowHandler(config_entry)
-
- async def async_step_geography(self, user_input=None):
- """Handle the initialization of the integration via the cloud API."""
- if not user_input:
- return self.async_show_form(
- step_id="geography", data_schema=self.geography_schema
- )
-
- self._geo_id = async_get_geography_id(user_input)
- await self._async_set_unique_id(self._geo_id)
- self._abort_if_unique_id_configured()
-
- # Find older config entries without unique ID:
- for entry in self._async_current_entries():
- if entry.version != 1:
- continue
-
- if any(
- self._geo_id == async_get_geography_id(geography)
- for geography in entry.data[CONF_GEOGRAPHIES]
- ):
- return self.async_abort(reason="already_configured")
-
- return await self.async_step_geography_finish(
- user_input, "geography", self.geography_schema
- )
-
- async def async_step_geography_finish(self, user_input, error_step, error_schema):
+ async def _async_finish_geography(self, user_input, integration_type):
"""Validate a Cloud API key."""
websession = aiohttp_client.async_get_clientsession(self.hass)
cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession)
@@ -123,16 +96,40 @@ async def async_step_geography_finish(self, user_input, error_step, error_schema
"airvisual_checked_api_keys_lock", asyncio.Lock()
)
+ if integration_type == INTEGRATION_TYPE_GEOGRAPHY_COORDS:
+ coro = cloud_api.air_quality.nearest_city()
+ error_schema = self.geography_coords_schema
+ error_step = "geography_by_coords"
+ else:
+ coro = cloud_api.air_quality.city(
+ user_input[CONF_CITY], user_input[CONF_STATE], user_input[CONF_COUNTRY]
+ )
+ error_schema = GEOGRAPHY_NAME_SCHEMA
+ error_step = "geography_by_name"
+
async with valid_keys_lock:
if user_input[CONF_API_KEY] not in valid_keys:
try:
- await cloud_api.air_quality.nearest_city()
+ await coro
except InvalidKeyError:
return self.async_show_form(
step_id=error_step,
data_schema=error_schema,
errors={CONF_API_KEY: "invalid_api_key"},
)
+ except NotFoundError:
+ return self.async_show_form(
+ step_id=error_step,
+ data_schema=error_schema,
+ errors={CONF_CITY: "location_not_found"},
+ )
+ except AirVisualError as err:
+ LOGGER.error(err)
+ return self.async_show_form(
+ step_id=error_step,
+ data_schema=error_schema,
+ errors={"base": "unknown"},
+ )
valid_keys.add(user_input[CONF_API_KEY])
@@ -143,16 +140,54 @@ async def async_step_geography_finish(self, user_input, error_step, error_schema
return self.async_create_entry(
title=f"Cloud API ({self._geo_id})",
- data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY},
+ data={**user_input, CONF_INTEGRATION_TYPE: integration_type},
)
- async def async_step_node_pro(self, user_input=None):
- """Handle the initialization of the integration with a Node/Pro."""
+ async def _async_init_geography(self, user_input, integration_type):
+ """Handle the initialization of the integration via the cloud API."""
+ self._geo_id = async_get_geography_id(user_input)
+ await self._async_set_unique_id(self._geo_id)
+ self._abort_if_unique_id_configured()
+ return await self._async_finish_geography(user_input, integration_type)
+
+ async def _async_set_unique_id(self, unique_id):
+ """Set the unique ID of the config flow and abort if it already exists."""
+ await self.async_set_unique_id(unique_id)
+ self._abort_if_unique_id_configured()
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Define the config flow to handle options."""
+ return AirVisualOptionsFlowHandler(config_entry)
+
+ async def async_step_geography_by_coords(self, user_input=None):
+ """Handle the initialization of the cloud API based on latitude/longitude."""
+ if not user_input:
+ return self.async_show_form(
+ step_id="geography_by_coords", data_schema=self.geography_coords_schema
+ )
+
+ return await self._async_init_geography(
+ user_input, INTEGRATION_TYPE_GEOGRAPHY_COORDS
+ )
+
+ async def async_step_geography_by_name(self, user_input=None):
+ """Handle the initialization of the cloud API based on city/state/country."""
if not user_input:
return self.async_show_form(
- step_id="node_pro", data_schema=self.node_pro_schema
+ step_id="geography_by_name", data_schema=GEOGRAPHY_NAME_SCHEMA
)
+ return await self._async_init_geography(
+ user_input, INTEGRATION_TYPE_GEOGRAPHY_NAME
+ )
+
+ async def async_step_node_pro(self, user_input=None):
+ """Handle the initialization of the integration with a Node/Pro."""
+ if not user_input:
+ return self.async_show_form(step_id="node_pro", data_schema=NODE_PRO_SCHEMA)
+
await self._async_set_unique_id(user_input[CONF_IP_ADDRESS])
node = NodeSamba(user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD])
@@ -163,7 +198,7 @@ async def async_step_node_pro(self, user_input=None):
LOGGER.error("Error connecting to Node/Pro unit: %s", err)
return self.async_show_form(
step_id="node_pro",
- data_schema=self.node_pro_schema,
+ data_schema=NODE_PRO_SCHEMA,
errors={CONF_IP_ADDRESS: "cannot_connect"},
)
@@ -176,39 +211,34 @@ async def async_step_node_pro(self, user_input=None):
async def async_step_reauth(self, data):
"""Handle configuration by re-auth."""
+ self._entry_data_for_reauth = data
self._geo_id = async_get_geography_id(data)
- self._latitude = data[CONF_LATITUDE]
- self._longitude = data[CONF_LONGITUDE]
-
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(self, user_input=None):
"""Handle re-auth completion."""
if not user_input:
return self.async_show_form(
- step_id="reauth_confirm", data_schema=self.api_key_data_schema
+ step_id="reauth_confirm", data_schema=API_KEY_DATA_SCHEMA
)
- conf = {
- CONF_API_KEY: user_input[CONF_API_KEY],
- CONF_LATITUDE: self._latitude,
- CONF_LONGITUDE: self._longitude,
- CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY,
- }
+ conf = {CONF_API_KEY: user_input[CONF_API_KEY], **self._entry_data_for_reauth}
- return await self.async_step_geography_finish(
- conf, "reauth_confirm", self.api_key_data_schema
+ return await self._async_finish_geography(
+ conf, self._entry_data_for_reauth[CONF_INTEGRATION_TYPE]
)
async def async_step_user(self, user_input=None):
"""Handle the start of the config flow."""
if not user_input:
return self.async_show_form(
- step_id="user", data_schema=self.pick_integration_type_schema
+ step_id="user", data_schema=PICK_INTEGRATION_TYPE_SCHEMA
)
- if user_input["type"] == INTEGRATION_TYPE_GEOGRAPHY:
- return await self.async_step_geography()
+ if user_input["type"] == INTEGRATION_TYPE_GEOGRAPHY_COORDS:
+ return await self.async_step_geography_by_coords()
+ if user_input["type"] == INTEGRATION_TYPE_GEOGRAPHY_NAME:
+ return await self.async_step_geography_by_name()
return await self.async_step_node_pro()
diff --git a/homeassistant/components/airvisual/const.py b/homeassistant/components/airvisual/const.py
index a98a899b762420..510ada2b68c0f9 100644
--- a/homeassistant/components/airvisual/const.py
+++ b/homeassistant/components/airvisual/const.py
@@ -4,7 +4,8 @@
DOMAIN = "airvisual"
LOGGER = logging.getLogger(__package__)
-INTEGRATION_TYPE_GEOGRAPHY = "Geographical Location"
+INTEGRATION_TYPE_GEOGRAPHY_COORDS = "Geographical Location by Latitude/Longitude"
+INTEGRATION_TYPE_GEOGRAPHY_NAME = "Geographical Location by Name"
INTEGRATION_TYPE_NODE_PRO = "AirVisual Node/Pro"
CONF_CITY = "city"
diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py
index ae9995f36c32c4..1febcec68f4be8 100644
--- a/homeassistant/components/airvisual/sensor.py
+++ b/homeassistant/components/airvisual/sensor.py
@@ -1,6 +1,5 @@
"""Support for AirVisual air quality sensors."""
-from logging import getLogger
-
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_LATITUDE,
ATTR_LONGITUDE,
@@ -27,11 +26,10 @@
CONF_INTEGRATION_TYPE,
DATA_COORDINATOR,
DOMAIN,
- INTEGRATION_TYPE_GEOGRAPHY,
+ INTEGRATION_TYPE_GEOGRAPHY_COORDS,
+ INTEGRATION_TYPE_GEOGRAPHY_NAME,
)
-_LOGGER = getLogger(__name__)
-
ATTR_CITY = "city"
ATTR_COUNTRY = "country"
ATTR_POLLUTANT_SYMBOL = "pollutant_symbol"
@@ -115,7 +113,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up AirVisual sensors based on a config entry."""
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
- if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY:
+ if config_entry.data[CONF_INTEGRATION_TYPE] in [
+ INTEGRATION_TYPE_GEOGRAPHY_COORDS,
+ INTEGRATION_TYPE_GEOGRAPHY_NAME,
+ ]:
sensors = [
AirVisualGeographySensor(
coordinator,
@@ -138,7 +139,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors, True)
-class AirVisualGeographySensor(AirVisualEntity):
+class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
"""Define an AirVisual sensor related to geography data via the Cloud API."""
def __init__(self, coordinator, config_entry, kind, name, icon, unit, locale):
@@ -208,20 +209,35 @@ def update_from_latest_data(self):
}
)
- if CONF_LATITUDE in self._config_entry.data:
- if self._config_entry.options[CONF_SHOW_ON_MAP]:
- self._attrs[ATTR_LATITUDE] = self._config_entry.data[CONF_LATITUDE]
- self._attrs[ATTR_LONGITUDE] = self._config_entry.data[CONF_LONGITUDE]
- self._attrs.pop("lati", None)
- self._attrs.pop("long", None)
- else:
- self._attrs["lati"] = self._config_entry.data[CONF_LATITUDE]
- self._attrs["long"] = self._config_entry.data[CONF_LONGITUDE]
- self._attrs.pop(ATTR_LATITUDE, None)
- self._attrs.pop(ATTR_LONGITUDE, None)
+ # Displaying the geography on the map relies upon putting the latitude/longitude
+ # in the entity attributes with "latitude" and "longitude" as the keys.
+ # Conversely, we can hide the location on the map by using other keys, like
+ # "lati" and "long".
+ #
+ # We use any coordinates in the config entry and, in the case of a geography by
+ # name, we fall back to the latitude longitude provided in the coordinator data:
+ latitude = self._config_entry.data.get(
+ CONF_LATITUDE,
+ self.coordinator.data["location"]["coordinates"][1],
+ )
+ longitude = self._config_entry.data.get(
+ CONF_LONGITUDE,
+ self.coordinator.data["location"]["coordinates"][0],
+ )
+
+ if self._config_entry.options[CONF_SHOW_ON_MAP]:
+ self._attrs[ATTR_LATITUDE] = latitude
+ self._attrs[ATTR_LONGITUDE] = longitude
+ self._attrs.pop("lati", None)
+ self._attrs.pop("long", None)
+ else:
+ self._attrs["lati"] = latitude
+ self._attrs["long"] = longitude
+ self._attrs.pop(ATTR_LATITUDE, None)
+ self._attrs.pop(ATTR_LONGITUDE, None)
-class AirVisualNodeProSensor(AirVisualEntity):
+class AirVisualNodeProSensor(AirVisualEntity, SensorEntity):
"""Define an AirVisual sensor related to a Node/Pro unit."""
def __init__(self, coordinator, kind, name, device_class, unit):
diff --git a/homeassistant/components/airvisual/strings.json b/homeassistant/components/airvisual/strings.json
index 22f9c80f313a89..8d2dce85a17c79 100644
--- a/homeassistant/components/airvisual/strings.json
+++ b/homeassistant/components/airvisual/strings.json
@@ -1,15 +1,25 @@
{
"config": {
"step": {
- "geography": {
+ "geography_by_coords": {
"title": "Configure a Geography",
- "description": "Use the AirVisual cloud API to monitor a geographical location.",
+ "description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]"
}
},
+ "geography_by_name": {
+ "title": "Configure a Geography",
+ "description": "Use the AirVisual cloud API to monitor a city/state/country.",
+ "data": {
+ "api_key": "[%key:common::config_flow::data::api_key%]",
+ "city": "City",
+ "country": "Country",
+ "state": "state"
+ }
+ },
"node_pro": {
"title": "Configure an AirVisual Node/Pro",
"description": "Monitor a personal AirVisual unit. The password can be retrieved from the unit's UI.",
@@ -26,17 +36,13 @@
},
"user": {
"title": "Configure AirVisual",
- "description": "Pick what type of AirVisual data you want to monitor.",
- "data": {
- "cloud_api": "Geographical Location",
- "node_pro": "AirVisual Node Pro",
- "type": "Integration Type"
- }
+ "description": "Pick what type of AirVisual data you want to monitor."
}
},
"error": {
"general_error": "[%key:common::config_flow::error::unknown%]",
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
+ "location_not_found": "Location not found",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
diff --git a/homeassistant/components/airvisual/translations/ar.json b/homeassistant/components/airvisual/translations/ar.json
new file mode 100644
index 00000000000000..771d88e84340ac
--- /dev/null
+++ b/homeassistant/components/airvisual/translations/ar.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "geography_by_name": {
+ "data": {
+ "country": "\u0627\u0644\u062f\u0648\u0644\u0629"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airvisual/translations/bg.json b/homeassistant/components/airvisual/translations/bg.json
new file mode 100644
index 00000000000000..7e463418576e50
--- /dev/null
+++ b/homeassistant/components/airvisual/translations/bg.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "geography_by_name": {
+ "data": {
+ "city": "\u0413\u0440\u0430\u0434",
+ "country": "\u0421\u0442\u0440\u0430\u043d\u0430"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airvisual/translations/ca.json b/homeassistant/components/airvisual/translations/ca.json
index d7a0ec2bd99362..29df3dc7ca2726 100644
--- a/homeassistant/components/airvisual/translations/ca.json
+++ b/homeassistant/components/airvisual/translations/ca.json
@@ -7,7 +7,8 @@
"error": {
"cannot_connect": "Ha fallat la connexi\u00f3",
"general_error": "Error inesperat",
- "invalid_api_key": "Clau API inv\u00e0lida"
+ "invalid_api_key": "Clau API inv\u00e0lida",
+ "location_not_found": "No s'ha trobat la ubicaci\u00f3"
},
"step": {
"geography": {
@@ -17,7 +18,26 @@
"longitude": "Longitud"
},
"description": "Utilitza l'API d'AirVisual per monitoritzar una ubicaci\u00f3 geogr\u00e0fica.",
- "title": "Configuraci\u00f3 localitzaci\u00f3 geogr\u00e0fica"
+ "title": "Configura una ubicaci\u00f3 geogr\u00e0fica"
+ },
+ "geography_by_coords": {
+ "data": {
+ "api_key": "Clau API",
+ "latitude": "Latitud",
+ "longitude": "Longitud"
+ },
+ "description": "Utilitza l'API d'AirVisual per monitoritzar una latitud/longitud.",
+ "title": "Configura una ubicaci\u00f3 geogr\u00e0fica"
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "Clau API",
+ "city": "Ciutat",
+ "country": "Pa\u00eds",
+ "state": "Estat"
+ },
+ "description": "Utilitza l'API d'AirVisual per monitoritzar un/a ciutat/estat/pa\u00eds",
+ "title": "Configura una ubicaci\u00f3 geogr\u00e0fica"
},
"node_pro": {
"data": {
diff --git a/homeassistant/components/airvisual/translations/cs.json b/homeassistant/components/airvisual/translations/cs.json
index 5c26dcf98efe21..75720b0f30b7dd 100644
--- a/homeassistant/components/airvisual/translations/cs.json
+++ b/homeassistant/components/airvisual/translations/cs.json
@@ -17,6 +17,20 @@
"longitude": "Zem\u011bpisn\u00e1 d\u00e9lka"
}
},
+ "geography_by_coords": {
+ "data": {
+ "api_key": "Kl\u00ed\u010d API",
+ "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka",
+ "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka"
+ }
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "Kl\u00ed\u010d API",
+ "city": "M\u011bsto",
+ "country": "Zem\u011b"
+ }
+ },
"node_pro": {
"data": {
"ip_address": "Hostitel",
diff --git a/homeassistant/components/airvisual/translations/de.json b/homeassistant/components/airvisual/translations/de.json
index 63012e23da1fbf..6e2a5f60c6feda 100644
--- a/homeassistant/components/airvisual/translations/de.json
+++ b/homeassistant/components/airvisual/translations/de.json
@@ -1,12 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "Diese Koordinaten oder Node/Pro ID sind bereits registriert."
+ "already_configured": "Diese Koordinaten oder Node/Pro ID sind bereits registriert.",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
},
"error": {
- "cannot_connect": "Verbindungsfehler",
- "general_error": "Es gab einen unbekannten Fehler.",
- "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel bereitgestellt."
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "general_error": "Unerwarteter Fehler",
+ "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel",
+ "location_not_found": "Standort nicht gefunden"
},
"step": {
"geography": {
@@ -15,11 +17,31 @@
"latitude": "Breitengrad",
"longitude": "L\u00e4ngengrad"
},
+ "description": "Verwende die AirVisual Cloud API, um einen geografischen Standort zu \u00fcberwachen.",
"title": "Konfigurieren Sie eine Geografie"
},
+ "geography_by_coords": {
+ "data": {
+ "api_key": "API-Schl\u00fcssel",
+ "latitude": "Breitengrad",
+ "longitude": "L\u00e4ngengrad"
+ },
+ "description": "Verwende die AirVisual Cloud API, um einen L\u00e4ngengrad/Breitengrad zu \u00fcberwachen.",
+ "title": "Konfiguriere einen Standort"
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "API-Schl\u00fcssel",
+ "city": "Stadt",
+ "country": "Land",
+ "state": "Bundesland"
+ },
+ "description": "Verwende die AirVisual Cloud API, um ein(e) Stadt/Bundesland/Land zu \u00fcberwachen.",
+ "title": "Konfiguriere einen Standort"
+ },
"node_pro": {
"data": {
- "ip_address": "IP-Adresse/Hostname des Ger\u00e4ts",
+ "ip_address": "Host",
"password": "Passwort"
},
"description": "\u00dcberwachen Sie eine pers\u00f6nliche AirVisual-Einheit. Das Passwort kann von der Benutzeroberfl\u00e4che des Ger\u00e4ts abgerufen werden.",
@@ -28,7 +50,8 @@
"reauth_confirm": {
"data": {
"api_key": "API-Key"
- }
+ },
+ "title": "AirVisual erneut authentifizieren"
},
"user": {
"data": {
diff --git a/homeassistant/components/airvisual/translations/en.json b/homeassistant/components/airvisual/translations/en.json
index 129abcc29e5598..64eb11f902c8ab 100644
--- a/homeassistant/components/airvisual/translations/en.json
+++ b/homeassistant/components/airvisual/translations/en.json
@@ -7,7 +7,8 @@
"error": {
"cannot_connect": "Failed to connect",
"general_error": "Unexpected error",
- "invalid_api_key": "Invalid API key"
+ "invalid_api_key": "Invalid API key",
+ "location_not_found": "Location not found"
},
"step": {
"geography": {
@@ -19,6 +20,25 @@
"description": "Use the AirVisual cloud API to monitor a geographical location.",
"title": "Configure a Geography"
},
+ "geography_by_coords": {
+ "data": {
+ "api_key": "API Key",
+ "latitude": "Latitude",
+ "longitude": "Longitude"
+ },
+ "description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
+ "title": "Configure a Geography"
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "API Key",
+ "city": "City",
+ "country": "Country",
+ "state": "state"
+ },
+ "description": "Use the AirVisual cloud API to monitor a city/state/country.",
+ "title": "Configure a Geography"
+ },
"node_pro": {
"data": {
"ip_address": "Host",
diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json
index 4325f1561b9e43..53768f679be687 100644
--- a/homeassistant/components/airvisual/translations/es.json
+++ b/homeassistant/components/airvisual/translations/es.json
@@ -7,7 +7,8 @@
"error": {
"cannot_connect": "No se pudo conectar",
"general_error": "Se ha producido un error desconocido.",
- "invalid_api_key": "Se proporciona una clave API no v\u00e1lida."
+ "invalid_api_key": "Se proporciona una clave API no v\u00e1lida.",
+ "location_not_found": "Ubicaci\u00f3n no encontrada"
},
"step": {
"geography": {
@@ -19,6 +20,25 @@
"description": "Utilizar la API en la nube de AirVisual para monitorizar una ubicaci\u00f3n geogr\u00e1fica.",
"title": "Configurar una Geograf\u00eda"
},
+ "geography_by_coords": {
+ "data": {
+ "api_key": "Clave API",
+ "latitude": "Latitud",
+ "longitude": "Longitud"
+ },
+ "description": "Utilice la API de la nube de AirVisual para supervisar una latitud/longitud.",
+ "title": "Configurar una geograf\u00eda"
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "Clave API",
+ "city": "Ciudad",
+ "country": "Pa\u00eds",
+ "state": "estado"
+ },
+ "description": "Utilice la API de la nube de AirVisual para supervisar una ciudad/estado/pa\u00eds.",
+ "title": "Configurar una geograf\u00eda"
+ },
"node_pro": {
"data": {
"ip_address": "Direcci\u00f3n IP/Nombre de host de la Unidad",
diff --git a/homeassistant/components/airvisual/translations/et.json b/homeassistant/components/airvisual/translations/et.json
index 4bbf04817f961a..0fae2bcc57bb65 100644
--- a/homeassistant/components/airvisual/translations/et.json
+++ b/homeassistant/components/airvisual/translations/et.json
@@ -7,7 +7,8 @@
"error": {
"cannot_connect": "\u00dchendamine nurjus",
"general_error": "Tundmatu viga",
- "invalid_api_key": "Vale API v\u00f5ti"
+ "invalid_api_key": "Vale API v\u00f5ti",
+ "location_not_found": "Asukohta ei leitud"
},
"step": {
"geography": {
@@ -16,9 +17,28 @@
"latitude": "Laiuskraad",
"longitude": "Pikkuskraad"
},
- "description": "Kasutage AirVisual pilve API-t geograafilise asukoha j\u00e4lgimiseks.",
+ "description": "Kasuta AirVisual pilve API-t geograafilise asukoha j\u00e4lgimiseks.",
"title": "Seadista Geography"
},
+ "geography_by_coords": {
+ "data": {
+ "api_key": "API v\u00f5ti",
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad"
+ },
+ "description": "Kasuta AirVisual pilve API-t pikkus/laiuskraadi j\u00e4lgimiseks.",
+ "title": "Seadista Geography sidumine"
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "API v\u00f5ti",
+ "city": "Linn",
+ "country": "Riik",
+ "state": "olek"
+ },
+ "description": "Kasuta AirVisual pilve API-t linna/osariigi/riigi j\u00e4lgimiseks.",
+ "title": "Seadista Geography sidumine"
+ },
"node_pro": {
"data": {
"ip_address": "\u00dcksuse IP-aadress / hostinimi",
diff --git a/homeassistant/components/airvisual/translations/fr.json b/homeassistant/components/airvisual/translations/fr.json
index 90857d826eac1b..62c144e075dde3 100644
--- a/homeassistant/components/airvisual/translations/fr.json
+++ b/homeassistant/components/airvisual/translations/fr.json
@@ -6,8 +6,9 @@
},
"error": {
"cannot_connect": "\u00c9chec de connexion",
- "general_error": "Une erreur inconnue est survenue.",
- "invalid_api_key": "La cl\u00e9 API fournie n'est pas valide."
+ "general_error": "Erreur inattendue",
+ "invalid_api_key": "Cl\u00e9 API invalide",
+ "location_not_found": "Emplacement introuvable"
},
"step": {
"geography": {
@@ -19,13 +20,32 @@
"description": "Utilisez l'API cloud AirVisual pour surveiller une position g\u00e9ographique.",
"title": "Configurer une g\u00e9ographie"
},
+ "geography_by_coords": {
+ "data": {
+ "api_key": "Clef d'API",
+ "latitude": "Latitude",
+ "longitude": "Longitude"
+ },
+ "description": "Utilisez l'API cloud AirVisual pour surveiller une latitude / longitude.",
+ "title": "Configurer un lieu g\u00e9ographique"
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "Clef d'API",
+ "city": "Ville",
+ "country": "Pays",
+ "state": "Etat"
+ },
+ "description": "Utilisez l'API cloud AirVisual pour surveiller une ville / un \u00e9tat / un pays.",
+ "title": "Configurer un lieu g\u00e9ographique"
+ },
"node_pro": {
"data": {
- "ip_address": "Adresse IP / nom d'h\u00f4te de l'unit\u00e9",
+ "ip_address": "H\u00f4te",
"password": "Mot de passe"
},
- "description": "Surveillez une unit\u00e9 AirVisual personnelle. Le mot de passe peut \u00eatre r\u00e9cup\u00e9r\u00e9 dans l'interface utilisateur de l'unit\u00e9.",
- "title": "Configurer un AirVisual Node/Pro"
+ "description": "Surveillez une unit\u00e9 personnelle AirVisual. Le mot de passe peut \u00eatre r\u00e9cup\u00e9r\u00e9 dans l'interface utilisateur de l'unit\u00e9.",
+ "title": "Configurer un noeud AirVisual Pro"
},
"reauth_confirm": {
"data": {
diff --git a/homeassistant/components/airvisual/translations/he.json b/homeassistant/components/airvisual/translations/he.json
index 7d1a1c696ed07c..e32efda96dcb6a 100644
--- a/homeassistant/components/airvisual/translations/he.json
+++ b/homeassistant/components/airvisual/translations/he.json
@@ -6,7 +6,13 @@
"step": {
"geography": {
"data": {
- "api_key": "\u05de\u05e4\u05ea\u05d7 API"
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
+ }
+ },
+ "node_pro": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
}
}
}
diff --git a/homeassistant/components/airvisual/translations/hu.json b/homeassistant/components/airvisual/translations/hu.json
index 53ab734e5059b0..89584cd128b7b0 100644
--- a/homeassistant/components/airvisual/translations/hu.json
+++ b/homeassistant/components/airvisual/translations/hu.json
@@ -1,8 +1,13 @@
{
"config": {
+ "abort": {
+ "already_configured": "A hely m\u00e1r konfigur\u00e1lva van vagy a Node/Pro azonos\u00edt\u00f3 m\u00e1r regisztr\u00e1lva van.",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
+ },
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
- "general_error": "Ismeretlen hiba t\u00f6rt\u00e9nt."
+ "general_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt",
+ "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs"
},
"step": {
"geography": {
@@ -12,8 +17,23 @@
"longitude": "Hossz\u00fas\u00e1g"
}
},
+ "geography_by_coords": {
+ "data": {
+ "api_key": "API kulcs",
+ "latitude": "Sz\u00e9less\u00e9g",
+ "longitude": "Hossz\u00fas\u00e1g"
+ }
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "API kulcs",
+ "city": "V\u00e1ros",
+ "country": "Orsz\u00e1g"
+ }
+ },
"node_pro": {
"data": {
+ "ip_address": "Hoszt",
"password": "Jelsz\u00f3"
}
},
@@ -21,6 +41,11 @@
"data": {
"api_key": "API kulcs"
}
+ },
+ "user": {
+ "data": {
+ "type": "Integr\u00e1ci\u00f3 t\u00edpusa"
+ }
}
}
}
diff --git a/homeassistant/components/airvisual/translations/id.json b/homeassistant/components/airvisual/translations/id.json
new file mode 100644
index 00000000000000..3c689338d9f3e7
--- /dev/null
+++ b/homeassistant/components/airvisual/translations/id.json
@@ -0,0 +1,77 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Lokasi sudah dikonfigurasi atau ID Node/Pro sudah terdaftar.",
+ "reauth_successful": "Autentikasi ulang berhasil"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "general_error": "Kesalahan yang tidak diharapkan",
+ "invalid_api_key": "Kunci API tidak valid",
+ "location_not_found": "Lokasi tidak ditemukan"
+ },
+ "step": {
+ "geography": {
+ "data": {
+ "api_key": "Kunci API",
+ "latitude": "Lintang",
+ "longitude": "Bujur"
+ },
+ "description": "Gunakan API cloud AirVisual untuk memantau lokasi geografis.",
+ "title": "Konfigurasikan Lokasi Geografi"
+ },
+ "geography_by_coords": {
+ "data": {
+ "api_key": "Kunci API",
+ "latitude": "Lintang",
+ "longitude": "Bujur"
+ },
+ "description": "Gunakan API cloud AirVisual untuk memantau satu pasang lintang/bujur.",
+ "title": "Konfigurasikan Lokasi Geografi"
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "Kunci API",
+ "city": "Kota",
+ "country": "Negara",
+ "state": "negara bagian"
+ },
+ "description": "Gunakan API cloud AirVisual untuk memantau kota/negara bagian/negara.",
+ "title": "Konfigurasikan Lokasi Geografi"
+ },
+ "node_pro": {
+ "data": {
+ "ip_address": "Host",
+ "password": "Kata Sandi"
+ },
+ "description": "Pantau unit AirVisual pribadi. Kata sandi dapat diambil dari antarmuka unit.",
+ "title": "Konfigurasikan AirVisual Node/Pro"
+ },
+ "reauth_confirm": {
+ "data": {
+ "api_key": "Kunci API"
+ },
+ "title": "Autentikasi Ulang AirVisual"
+ },
+ "user": {
+ "data": {
+ "cloud_api": "Lokasi Geografis",
+ "node_pro": "AirVisual Node Pro",
+ "type": "Jenis Integrasi"
+ },
+ "description": "Pilih jenis data AirVisual yang ingin dipantau.",
+ "title": "Konfigurasikan AirVisual"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "show_on_map": "Tampilkan lokasi geografi yang dipantau pada peta"
+ },
+ "title": "Konfigurasikan AirVisual"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airvisual/translations/it.json b/homeassistant/components/airvisual/translations/it.json
index 7a4062fbe7604c..3ce45ff1342d0b 100644
--- a/homeassistant/components/airvisual/translations/it.json
+++ b/homeassistant/components/airvisual/translations/it.json
@@ -2,12 +2,13 @@
"config": {
"abort": {
"already_configured": "La posizione \u00e8 gi\u00e0 configurata o Node/Pro ID sono gi\u00e0 registrati.",
- "reauth_successful": "La riautenticazione ha avuto successo"
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
},
"error": {
"cannot_connect": "Impossibile connettersi",
"general_error": "Errore imprevisto",
- "invalid_api_key": "Chiave API non valida"
+ "invalid_api_key": "Chiave API non valida",
+ "location_not_found": "Posizione non trovata"
},
"step": {
"geography": {
@@ -19,6 +20,25 @@
"description": "Utilizzare l'API di AirVisual cloud per monitorare una posizione geografica.",
"title": "Configurare una Geografia"
},
+ "geography_by_coords": {
+ "data": {
+ "api_key": "Chiave API",
+ "latitude": "Latitudine",
+ "longitude": "Logitudine"
+ },
+ "description": "Usa l'API cloud di AirVisual per monitorare una latitudine/longitudine.",
+ "title": "Configurare un'area geografica"
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "Chiave API",
+ "city": "Citt\u00e0",
+ "country": "Nazione",
+ "state": "Stato"
+ },
+ "description": "Usa l'API cloud di AirVisual per monitorare una citt\u00e0/stato/paese.",
+ "title": "Configurare un'area geografica"
+ },
"node_pro": {
"data": {
"ip_address": "Host",
diff --git a/homeassistant/components/airvisual/translations/ko.json b/homeassistant/components/airvisual/translations/ko.json
index d25df4c213f4ec..3ab3dbcf2867d0 100644
--- a/homeassistant/components/airvisual/translations/ko.json
+++ b/homeassistant/components/airvisual/translations/ko.json
@@ -1,11 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "\uc88c\ud45c\uac12 \ub610\ub294 Node/Pro ID \uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "Node/Pro ID\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uac70\ub098 \uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
- "general_error": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
- "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "general_error": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "location_not_found": "\uc704\uce58\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
},
"step": {
"geography": {
@@ -17,14 +20,39 @@
"description": "AirVisual \ud074\ub77c\uc6b0\ub4dc API \ub97c \uc0ac\uc6a9\ud558\uc5ec \uc9c0\ub9ac\uc801 \uc704\uce58\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.",
"title": "\uc9c0\ub9ac\uc801 \uc704\uce58 \uad6c\uc131\ud558\uae30"
},
+ "geography_by_coords": {
+ "data": {
+ "api_key": "API \ud0a4",
+ "latitude": "\uc704\ub3c4",
+ "longitude": "\uacbd\ub3c4"
+ },
+ "description": "AirVisual \ud074\ub77c\uc6b0\ub4dc API\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc704\ub3c4/\uacbd\ub3c4\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.",
+ "title": "\uc9c0\ub9ac\uc801 \uc704\uce58 \uad6c\uc131\ud558\uae30"
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "API \ud0a4",
+ "city": "\ub3c4\uc2dc",
+ "country": "\uad6d\uac00",
+ "state": "\uc8fc"
+ },
+ "description": "AirVisual \ud074\ub77c\uc6b0\ub4dc API\ub97c \uc0ac\uc6a9\ud558\uc5ec \ub3c4\uc2dc/\uc8fc/\uad6d\uac00\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.",
+ "title": "\uc9c0\ub9ac\uc801 \uc704\uce58 \uad6c\uc131\ud558\uae30"
+ },
"node_pro": {
"data": {
- "ip_address": "\uae30\uae30 IP \uc8fc\uc18c/\ud638\uc2a4\ud2b8 \uc774\ub984",
+ "ip_address": "\ud638\uc2a4\ud2b8",
"password": "\ube44\ubc00\ubc88\ud638"
},
"description": "\uc0ac\uc6a9\uc790\uc758 AirVisual \uae30\uae30\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4. \uae30\uae30\uc758 UI \uc5d0\uc11c \ube44\ubc00\ubc88\ud638\ub97c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
"title": "AirVisual Node/Pro \uad6c\uc131\ud558\uae30"
},
+ "reauth_confirm": {
+ "data": {
+ "api_key": "API \ud0a4"
+ },
+ "title": "AirVisual \uc7ac\uc778\uc99d\ud558\uae30"
+ },
"user": {
"data": {
"cloud_api": "\uc9c0\ub9ac\uc801 \uc704\uce58",
diff --git a/homeassistant/components/airvisual/translations/lb.json b/homeassistant/components/airvisual/translations/lb.json
index 5e45098c11d837..d6799ba6e37b96 100644
--- a/homeassistant/components/airvisual/translations/lb.json
+++ b/homeassistant/components/airvisual/translations/lb.json
@@ -7,7 +7,8 @@
"error": {
"cannot_connect": "Feeler beim verbannen",
"general_error": "Onerwaarte Feeler",
- "invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel"
+ "invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel",
+ "location_not_found": "Standuert net fonnt."
},
"step": {
"geography": {
@@ -19,6 +20,12 @@
"description": "Benotz Airvisual cloud API fir eng geografescher Lag z'iwwerwaachen.",
"title": "Geografie ariichten"
},
+ "geography_by_name": {
+ "data": {
+ "city": "Stad",
+ "country": "Land"
+ }
+ },
"node_pro": {
"data": {
"ip_address": "Host",
diff --git a/homeassistant/components/airvisual/translations/nl.json b/homeassistant/components/airvisual/translations/nl.json
index 85f8be5f8e00cb..ed81d6568edfff 100644
--- a/homeassistant/components/airvisual/translations/nl.json
+++ b/homeassistant/components/airvisual/translations/nl.json
@@ -1,12 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "Deze co\u00f6rdinaten of Node / Pro ID zijn al geregistreerd."
+ "already_configured": "Locatie is al geconfigureerd. of Node/Pro IDis al geregistreerd.",
+ "reauth_successful": "Herauthenticatie was succesvol"
},
"error": {
"cannot_connect": "Kan geen verbinding maken",
"general_error": "Er is een onbekende fout opgetreden.",
- "invalid_api_key": "Ongeldige API-sleutel"
+ "invalid_api_key": "Ongeldige API-sleutel",
+ "location_not_found": "Locatie niet gevonden"
},
"step": {
"geography": {
@@ -18,6 +20,25 @@
"description": "Gebruik de AirVisual cloud API om een geografische locatie te bewaken.",
"title": "Configureer een geografie"
},
+ "geography_by_coords": {
+ "data": {
+ "api_key": "API-sleutel",
+ "latitude": "Breedtegraad",
+ "longitude": "Lengtegraad"
+ },
+ "description": "Gebruik de AirVisual-cloud-API om een lengte- / breedtegraad te bewaken.",
+ "title": "Configureer een geografie"
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "API-sleutel",
+ "city": "Stad",
+ "country": "Land",
+ "state": "staat"
+ },
+ "description": "Gebruik de AirVisual-cloud-API om een stad/staat/land te bewaken.",
+ "title": "Configureer een geografie"
+ },
"node_pro": {
"data": {
"ip_address": "IP adres/hostname van component",
diff --git a/homeassistant/components/airvisual/translations/no.json b/homeassistant/components/airvisual/translations/no.json
index abf4a9f62e42ef..7c5b0333652387 100644
--- a/homeassistant/components/airvisual/translations/no.json
+++ b/homeassistant/components/airvisual/translations/no.json
@@ -7,7 +7,8 @@
"error": {
"cannot_connect": "Tilkobling mislyktes",
"general_error": "Uventet feil",
- "invalid_api_key": "Ugyldig API-n\u00f8kkel"
+ "invalid_api_key": "Ugyldig API-n\u00f8kkel",
+ "location_not_found": "Stedet ble ikke funnet"
},
"step": {
"geography": {
@@ -19,6 +20,25 @@
"description": "Bruk AirVisual cloud API til \u00e5 overv\u00e5ke en geografisk plassering.",
"title": "Konfigurer en Geography"
},
+ "geography_by_coords": {
+ "data": {
+ "api_key": "API-n\u00f8kkel",
+ "latitude": "Breddegrad",
+ "longitude": "Lengdegrad"
+ },
+ "description": "Bruk AirVisual cloud API til \u00e5 overv\u00e5ke en breddegrad/lengdegrad.",
+ "title": "Konfigurer en Geography"
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "API-n\u00f8kkel",
+ "city": "By",
+ "country": "Land",
+ "state": "stat"
+ },
+ "description": "Bruk AirVisual cloud API til \u00e5 overv\u00e5ke en by/stat/land.",
+ "title": "Konfigurer en Geography"
+ },
"node_pro": {
"data": {
"ip_address": "Vert",
diff --git a/homeassistant/components/airvisual/translations/pl.json b/homeassistant/components/airvisual/translations/pl.json
index 10af1fc2ee08cf..5590a951641175 100644
--- a/homeassistant/components/airvisual/translations/pl.json
+++ b/homeassistant/components/airvisual/translations/pl.json
@@ -7,7 +7,8 @@
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"general_error": "Nieoczekiwany b\u0142\u0105d",
- "invalid_api_key": "Nieprawid\u0142owy klucz API"
+ "invalid_api_key": "Nieprawid\u0142owy klucz API",
+ "location_not_found": "Nie znaleziono lokalizacji"
},
"step": {
"geography": {
@@ -19,6 +20,25 @@
"description": "U\u017cyj interfejsu API chmury AirVisual do monitorowania lokalizacji geograficznej.",
"title": "Konfiguracja Geography"
},
+ "geography_by_coords": {
+ "data": {
+ "api_key": "Klucz API",
+ "latitude": "Szeroko\u015b\u0107 geograficzna",
+ "longitude": "D\u0142ugo\u015b\u0107 geograficzna"
+ },
+ "description": "U\u017cyj API chmury AirVisual do monitorowania szeroko\u015bci/d\u0142ugo\u015bci geograficznej.",
+ "title": "Konfiguracja Geography"
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "Klucz API",
+ "city": "Miasto",
+ "country": "Kraj",
+ "state": "Stan"
+ },
+ "description": "U\u017cyj API chmury AirVisual do monitorowania miasta/stanu/kraju.",
+ "title": "Konfiguracja Geography"
+ },
"node_pro": {
"data": {
"ip_address": "Nazwa hosta lub adres IP",
diff --git a/homeassistant/components/airvisual/translations/ru.json b/homeassistant/components/airvisual/translations/ru.json
index de9e5a730fecee..bc648c84bfe72a 100644
--- a/homeassistant/components/airvisual/translations/ru.json
+++ b/homeassistant/components/airvisual/translations/ru.json
@@ -7,7 +7,8 @@
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"general_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.",
- "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API."
+ "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.",
+ "location_not_found": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e."
},
"step": {
"geography": {
@@ -19,6 +20,25 @@
"description": "\u041c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e API AirVisual.",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
},
+ "geography_by_coords": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
+ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430"
+ },
+ "description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0448\u0438\u0440\u043e\u0442\u044b/\u0434\u043e\u043b\u0433\u043e\u0442\u044b.",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "city": "\u0413\u043e\u0440\u043e\u0434",
+ "country": "\u0421\u0442\u0440\u0430\u043d\u0430",
+ "state": "\u0448\u0442\u0430\u0442"
+ },
+ "description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0433\u043e\u0440\u043e\u0434\u0430/\u0448\u0442\u0430\u0442\u0430/\u0441\u0442\u0440\u0430\u043d\u044b.",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
+ },
"node_pro": {
"data": {
"ip_address": "\u0425\u043e\u0441\u0442",
diff --git a/homeassistant/components/airvisual/translations/sv.json b/homeassistant/components/airvisual/translations/sv.json
index 4c4e1271d72464..ecc1c397ec46d7 100644
--- a/homeassistant/components/airvisual/translations/sv.json
+++ b/homeassistant/components/airvisual/translations/sv.json
@@ -1,7 +1,8 @@
{
"config": {
"error": {
- "general_error": "Ett ok\u00e4nt fel intr\u00e4ffade."
+ "general_error": "Ett ok\u00e4nt fel intr\u00e4ffade.",
+ "invalid_api_key": "Ogiltig API-nyckel"
},
"step": {
"geography": {
@@ -20,7 +21,8 @@
"data": {
"cloud_api": "Geografisk Plats",
"type": "Integrationstyp"
- }
+ },
+ "title": "Konfigurera AirVisual"
}
}
}
diff --git a/homeassistant/components/airvisual/translations/tr.json b/homeassistant/components/airvisual/translations/tr.json
new file mode 100644
index 00000000000000..3d20c8ea9fc83e
--- /dev/null
+++ b/homeassistant/components/airvisual/translations/tr.json
@@ -0,0 +1,59 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "general_error": "Beklenmeyen hata",
+ "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131",
+ "location_not_found": "Konum bulunamad\u0131"
+ },
+ "step": {
+ "geography": {
+ "data": {
+ "api_key": "API Anahtar\u0131",
+ "latitude": "Enlem",
+ "longitude": "Boylam"
+ }
+ },
+ "geography_by_coords": {
+ "data": {
+ "api_key": "API Anahtar\u0131",
+ "latitude": "Enlem",
+ "longitude": "Boylam"
+ },
+ "description": "Bir enlem / boylam\u0131 izlemek i\u00e7in AirVisual bulut API'sini kullan\u0131n.",
+ "title": "Bir Co\u011frafyay\u0131 Yap\u0131land\u0131rma"
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "API Anahtar\u0131",
+ "city": "\u015eehir",
+ "country": "\u00dclke",
+ "state": "durum"
+ },
+ "title": "Bir Co\u011frafyay\u0131 Yap\u0131land\u0131rma"
+ },
+ "node_pro": {
+ "data": {
+ "ip_address": "Ana Bilgisayar",
+ "password": "Parola"
+ },
+ "description": "Ki\u015fisel bir AirVisual \u00fcnitesini izleyin. Parola, \u00fcnitenin kullan\u0131c\u0131 aray\u00fcz\u00fcnden al\u0131nabilir."
+ },
+ "reauth_confirm": {
+ "data": {
+ "api_key": "API Anahtar\u0131"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "AirVisual'\u0131 yap\u0131land\u0131r\u0131n"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airvisual/translations/uk.json b/homeassistant/components/airvisual/translations/uk.json
new file mode 100644
index 00000000000000..d99c58de7c07ae
--- /dev/null
+++ b/homeassistant/components/airvisual/translations/uk.json
@@ -0,0 +1,57 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435. \u0410\u0431\u043e \u0446\u0435\u0439 Node / Pro ID \u0432\u0436\u0435 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u0438\u0439.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "general_error": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430",
+ "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API"
+ },
+ "step": {
+ "geography": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
+ "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430"
+ },
+ "description": "\u041c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433 \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0445\u043c\u0430\u0440\u043d\u043e\u0433\u043e API AirVisual.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f"
+ },
+ "node_pro": {
+ "data": {
+ "ip_address": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "description": "\u041c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e AirVisual. \u041f\u0430\u0440\u043e\u043b\u044c \u043c\u043e\u0436\u043d\u0430 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AirVisual Node / Pro"
+ },
+ "reauth_confirm": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API"
+ },
+ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0444\u0456\u043b\u044e"
+ },
+ "user": {
+ "data": {
+ "cloud_api": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f",
+ "node_pro": "AirVisual Node Pro",
+ "type": "\u0422\u0438\u043f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u0434\u0430\u043d\u0438\u0445 AirVisual, \u044f\u043a\u0438\u0439 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u0442\u0438.",
+ "title": "AirVisual"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u043d\u0443 \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0456"
+ },
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AirVisual"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airvisual/translations/zh-Hant.json b/homeassistant/components/airvisual/translations/zh-Hant.json
index 4bdc2959047da1..3767d41b519e12 100644
--- a/homeassistant/components/airvisual/translations/zh-Hant.json
+++ b/homeassistant/components/airvisual/translations/zh-Hant.json
@@ -7,7 +7,8 @@
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557",
"general_error": "\u672a\u9810\u671f\u932f\u8aa4",
- "invalid_api_key": "API \u5bc6\u9470\u7121\u6548"
+ "invalid_api_key": "API \u5bc6\u9470\u7121\u6548",
+ "location_not_found": "\u627e\u4e0d\u5230\u5730\u9ede"
},
"step": {
"geography": {
@@ -19,6 +20,25 @@
"description": "\u4f7f\u7528 AirVisual \u96f2\u7aef API \u4ee5\u76e3\u63a7\u5730\u7406\u5ea7\u6a19\u3002",
"title": "\u8a2d\u5b9a\u5730\u7406\u5ea7\u6a19"
},
+ "geography_by_coords": {
+ "data": {
+ "api_key": "API \u5bc6\u9470",
+ "latitude": "\u7def\u5ea6",
+ "longitude": "\u7d93\u5ea6"
+ },
+ "description": "\u4f7f\u7528 AirVisual \u96f2\u7aef API \u4ee5\u76e3\u63a7\u7d93\u5ea6/\u7def\u5ea6\u3002",
+ "title": "\u8a2d\u5b9a\u5730\u7406\u5ea7\u6a19"
+ },
+ "geography_by_name": {
+ "data": {
+ "api_key": "API \u5bc6\u9470",
+ "city": "\u57ce\u5e02",
+ "country": "\u570b\u5bb6",
+ "state": "\u5dde"
+ },
+ "description": "\u4f7f\u7528 AirVisual \u96f2\u7aef API \u4ee5\u76e3\u63a7\u57ce\u5e02/\u5dde/\u570b\u5bb6\u3002",
+ "title": "\u8a2d\u5b9a\u5730\u7406\u5ea7\u6a19"
+ },
"node_pro": {
"data": {
"ip_address": "\u4e3b\u6a5f\u7aef",
diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py
index 114abfa9cd6e4c..7d9e47fbcbecdf 100644
--- a/homeassistant/components/alarm_control_panel/__init__.py
+++ b/homeassistant/components/alarm_control_panel/__init__.py
@@ -2,6 +2,7 @@
from abc import abstractmethod
from datetime import timedelta
import logging
+from typing import final
import voluptuous as vol
@@ -172,6 +173,7 @@ async def async_alarm_arm_custom_bypass(self, code=None):
def supported_features(self) -> int:
"""Return the list of supported features."""
+ @final
@property
def state_attributes(self):
"""Return the state attributes."""
diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py
index 0dc16fdcf42275..9a55998e929142 100644
--- a/homeassistant/components/alarm_control_panel/device_action.py
+++ b/homeassistant/components/alarm_control_panel/device_action.py
@@ -1,5 +1,5 @@
"""Provides device automations for Alarm control panel."""
-from typing import List, Optional
+from __future__ import annotations
import voluptuous as vol
@@ -41,7 +41,7 @@
)
-async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device actions for Alarm control panel devices."""
registry = await entity_registry.async_get_registry(hass)
actions = []
@@ -109,11 +109,9 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
async def async_call_action_from_config(
- hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context]
+ hass: HomeAssistant, config: dict, variables: dict, context: Context | None
) -> None:
"""Execute a device action."""
- config = ACTION_SCHEMA(config)
-
service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]}
if CONF_CODE in config:
service_data[ATTR_CODE] = config[CONF_CODE]
diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py
index e5b3ec6aeee960..3817cf37b451fb 100644
--- a/homeassistant/components/alarm_control_panel/device_condition.py
+++ b/homeassistant/components/alarm_control_panel/device_condition.py
@@ -1,5 +1,5 @@
"""Provide the device automations for Alarm control panel."""
-from typing import Dict, List
+from __future__ import annotations
import voluptuous as vol
@@ -58,7 +58,7 @@
async def async_get_conditions(
hass: HomeAssistant, device_id: str
-) -> List[Dict[str, str]]:
+) -> list[dict[str, str]]:
"""List device conditions for Alarm control panel devices."""
registry = await entity_registry.async_get_registry(hass)
conditions = []
diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py
index bb5d82c52b177c..b24716bb43e859 100644
--- a/homeassistant/components/alarm_control_panel/device_trigger.py
+++ b/homeassistant/components/alarm_control_panel/device_trigger.py
@@ -1,5 +1,5 @@
"""Provides device automations for Alarm control panel."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -16,6 +16,7 @@
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
+ CONF_FOR,
CONF_PLATFORM,
CONF_TYPE,
STATE_ALARM_ARMED_AWAY,
@@ -31,24 +32,19 @@
from . import DOMAIN
-TRIGGER_TYPES = {
- "triggered",
- "disarmed",
- "arming",
- "armed_home",
- "armed_away",
- "armed_night",
-}
+BASIC_TRIGGER_TYPES = {"triggered", "disarmed", "arming"}
+TRIGGER_TYPES = BASIC_TRIGGER_TYPES | {"armed_home", "armed_away", "armed_night"}
TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
+ vol.Optional(CONF_FOR): cv.positive_time_period_dict,
}
)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers for Alarm control panel devices."""
registry = await entity_registry.async_get_registry(hass)
triggers = []
@@ -67,56 +63,38 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
supported_features = entity_state.attributes[ATTR_SUPPORTED_FEATURES]
# Add triggers for each entity that belongs to this integration
+ base_trigger = {
+ CONF_PLATFORM: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+
triggers += [
{
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "disarmed",
- },
- {
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "triggered",
- },
- {
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "arming",
- },
+ **base_trigger,
+ CONF_TYPE: trigger,
+ }
+ for trigger in BASIC_TRIGGER_TYPES
]
if supported_features & SUPPORT_ALARM_ARM_HOME:
triggers.append(
{
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
+ **base_trigger,
CONF_TYPE: "armed_home",
}
)
if supported_features & SUPPORT_ALARM_ARM_AWAY:
triggers.append(
{
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
+ **base_trigger,
CONF_TYPE: "armed_away",
}
)
if supported_features & SUPPORT_ALARM_ARM_NIGHT:
triggers.append(
{
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
+ **base_trigger,
CONF_TYPE: "armed_night",
}
)
@@ -124,6 +102,15 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
return triggers
+async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict:
+ """List trigger capabilities."""
+ return {
+ "extra_fields": vol.Schema(
+ {vol.Optional(CONF_FOR): cv.positive_time_period_dict}
+ )
+ }
+
+
async def async_attach_trigger(
hass: HomeAssistant,
config: ConfigType,
@@ -131,15 +118,11 @@ async def async_attach_trigger(
automation_info: dict,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
- config = TRIGGER_SCHEMA(config)
- from_state = None
-
if config[CONF_TYPE] == "triggered":
to_state = STATE_ALARM_TRIGGERED
elif config[CONF_TYPE] == "disarmed":
to_state = STATE_ALARM_DISARMED
elif config[CONF_TYPE] == "arming":
- from_state = STATE_ALARM_DISARMED
to_state = STATE_ALARM_ARMING
elif config[CONF_TYPE] == "armed_home":
to_state = STATE_ALARM_ARMED_HOME
@@ -153,8 +136,8 @@ async def async_attach_trigger(
CONF_ENTITY_ID: config[CONF_ENTITY_ID],
state_trigger.CONF_TO: to_state,
}
- if from_state:
- state_config[state_trigger.CONF_FROM] = from_state
+ if CONF_FOR in config:
+ state_config[CONF_FOR] = config[CONF_FOR]
state_config = state_trigger.TRIGGER_SCHEMA(state_config)
return await state_trigger.async_attach_trigger(
hass, state_config, action, automation_info, platform_type="device"
diff --git a/homeassistant/components/alarm_control_panel/group.py b/homeassistant/components/alarm_control_panel/group.py
index 6645f12245d257..4bfb14868140a5 100644
--- a/homeassistant/components/alarm_control_panel/group.py
+++ b/homeassistant/components/alarm_control_panel/group.py
@@ -10,13 +10,12 @@
STATE_ALARM_TRIGGERED,
STATE_OFF,
)
-from homeassistant.core import callback
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import HomeAssistant, callback
@callback
def async_describe_on_off_states(
- hass: HomeAssistantType, registry: GroupIntegrationRegistry
+ hass: HomeAssistant, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states(
diff --git a/homeassistant/components/alarm_control_panel/reproduce_state.py b/homeassistant/components/alarm_control_panel/reproduce_state.py
index 9e7d8e6f1a76a1..90979d97dd04a0 100644
--- a/homeassistant/components/alarm_control_panel/reproduce_state.py
+++ b/homeassistant/components/alarm_control_panel/reproduce_state.py
@@ -1,7 +1,9 @@
"""Reproduce an Alarm control panel state."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -18,8 +20,7 @@
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
)
-from homeassistant.core import Context, State
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import Context, HomeAssistant, State
from . import DOMAIN
@@ -36,11 +37,11 @@
async def _async_reproduce_state(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -80,11 +81,11 @@ async def _async_reproduce_state(
async def async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Alarm control panel states."""
await asyncio.gather(
diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml
index ee1e8c1fcf6993..b18f1cfb78270a 100644
--- a/homeassistant/components/alarm_control_panel/services.yaml
+++ b/homeassistant/components/alarm_control_panel/services.yaml
@@ -1,61 +1,74 @@
# Describes the format for available alarm control panel services
alarm_disarm:
+ name: Disarm
description: Send the alarm the command for disarm.
+ target:
fields:
- entity_id:
- description: Name of alarm control panel to disarm.
- example: "alarm_control_panel.downstairs"
code:
+ name: Code
description: An optional code to disarm the alarm control panel with.
example: "1234"
+ selector:
+ text:
alarm_arm_custom_bypass:
+ name: Arm with custom bypass
description: Send arm custom bypass command.
+ target:
fields:
- entity_id:
- description: Name of alarm control panel to arm custom bypass.
- example: "alarm_control_panel.downstairs"
code:
- description: An optional code to arm custom bypass the alarm control panel with.
+ name: Code
+ description:
+ An optional code to arm custom bypass the alarm control panel with.
example: "1234"
+ selector:
+ text:
alarm_arm_home:
+ name: Arm home
description: Send the alarm the command for arm home.
+ target:
fields:
- entity_id:
- description: Name of alarm control panel to arm home.
- example: "alarm_control_panel.downstairs"
code:
+ name: Code
description: An optional code to arm home the alarm control panel with.
example: "1234"
+ selector:
+ text:
alarm_arm_away:
+ name: Arm away
description: Send the alarm the command for arm away.
+ target:
fields:
- entity_id:
- description: Name of alarm control panel to arm away.
- example: "alarm_control_panel.downstairs"
code:
+ name: Code
description: An optional code to arm away the alarm control panel with.
example: "1234"
+ selector:
+ text:
alarm_arm_night:
+ name: Arm night
description: Send the alarm the command for arm night.
+ target:
fields:
- entity_id:
- description: Name of alarm control panel to arm night.
- example: "alarm_control_panel.downstairs"
code:
+ name: Code
description: An optional code to arm night the alarm control panel with.
example: "1234"
+ selector:
+ text:
alarm_trigger:
+ name: Trigger
description: Send the alarm the command for trigger.
+ target:
fields:
- entity_id:
- description: Name of alarm control panel to trigger.
- example: "alarm_control_panel.downstairs"
code:
+ name: Code
description: An optional code to trigger the alarm control panel with.
example: "1234"
+ selector:
+ text:
diff --git a/homeassistant/components/alarm_control_panel/translations/id.json b/homeassistant/components/alarm_control_panel/translations/id.json
index cbc3d31370c9f6..f1676ce8c7561c 100644
--- a/homeassistant/components/alarm_control_panel/translations/id.json
+++ b/homeassistant/components/alarm_control_panel/translations/id.json
@@ -1,14 +1,37 @@
{
+ "device_automation": {
+ "action_type": {
+ "arm_away": "Aktifkan {entity_name} untuk keluar",
+ "arm_home": "Aktifkan {entity_name} untuk di rumah",
+ "arm_night": "Aktifkan {entity_name} untuk malam",
+ "disarm": "Nonaktifkan {entity_name}",
+ "trigger": "Picu {entity_name}"
+ },
+ "condition_type": {
+ "is_armed_away": "{entity_name} diaktifkan untuk keluar",
+ "is_armed_home": "{entity_name} diaktifkan untuk di rumah",
+ "is_armed_night": "{entity_name} diaktifkan untuk malam",
+ "is_disarmed": "{entity_name} dinonaktifkan",
+ "is_triggered": "{entity_name} dipicu"
+ },
+ "trigger_type": {
+ "armed_away": "{entity_name} diaktifkan untuk keluar",
+ "armed_home": "{entity_name} diaktifkan untuk di rumah",
+ "armed_night": "{entity_name} diaktifkan untuk malam",
+ "disarmed": "{entity_name} dinonaktifkan",
+ "triggered": "{entity_name} dipicu"
+ }
+ },
"state": {
"_": {
- "armed": "Bersenjata",
- "armed_away": "Armed away",
- "armed_custom_bypass": "Armed custom bypass",
- "armed_home": "Armed home",
- "armed_night": "Armed night",
- "arming": "Mempersenjatai",
- "disarmed": "Dilucuti",
- "disarming": "Melucuti",
+ "armed": "Diaktifkan",
+ "armed_away": "Diaktifkan untuk keluar",
+ "armed_custom_bypass": "Diaktifkan khusus",
+ "armed_home": "Diaktifkan untuk di rumah",
+ "armed_night": "Diaktifkan untuk malam",
+ "arming": "Mengaktifkan",
+ "disarmed": "Dinonaktifkan",
+ "disarming": "Dinonaktifkan",
"pending": "Tertunda",
"triggered": "Terpicu"
}
diff --git a/homeassistant/components/alarm_control_panel/translations/ko.json b/homeassistant/components/alarm_control_panel/translations/ko.json
index f6adb68fe66093..0fd766ba0b9a60 100644
--- a/homeassistant/components/alarm_control_panel/translations/ko.json
+++ b/homeassistant/components/alarm_control_panel/translations/ko.json
@@ -1,35 +1,35 @@
{
"device_automation": {
"action_type": {
- "arm_away": "{entity_name} \uc678\ucd9c\uacbd\ube44",
- "arm_home": "{entity_name} \uc7ac\uc2e4\uacbd\ube44",
- "arm_night": "{entity_name} \uc57c\uac04\uacbd\ube44",
- "disarm": "{entity_name} \uacbd\ube44\ud574\uc81c",
- "trigger": "{entity_name} \ud2b8\ub9ac\uac70"
+ "arm_away": "{entity_name}\uc744(\ub97c) \uc678\ucd9c\uacbd\ube44\ub85c \uc124\uc815\ud558\uae30",
+ "arm_home": "{entity_name}\uc744(\ub97c) \uc7ac\uc2e4\uacbd\ube44\ub85c \uc124\uc815\ud558\uae30",
+ "arm_night": "{entity_name}\uc744(\ub97c) \uc57c\uac04\uacbd\ube44\ub85c \uc124\uc815\ud558\uae30",
+ "disarm": "{entity_name}\uc744(\ub97c) \uacbd\ube44\ud574\uc81c\ub85c \uc124\uc815\ud558\uae30",
+ "trigger": "{entity_name}\uc744(\ub97c) \ud2b8\ub9ac\uac70\ud558\uae30"
},
"condition_type": {
- "is_armed_away": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74",
- "is_armed_home": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74",
- "is_armed_night": "{entity_name} \uc774(\uac00) \uc57c\uac04 \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74",
- "is_disarmed": "{entity_name} \uc774(\uac00) \ud574\uc81c \uc0c1\ud0dc\uc774\uba74",
- "is_triggered": "{entity_name} \uc774(\uac00) \ud2b8\ub9ac\uac70\ub418\uc5c8\uc73c\uba74"
+ "is_armed_away": "{entity_name}\uc774(\uac00) \uc678\ucd9c \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74",
+ "is_armed_home": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74",
+ "is_armed_night": "{entity_name}\uc774(\uac00) \uc57c\uac04 \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74",
+ "is_disarmed": "{entity_name}\uc774(\uac00) \ud574\uc81c \uc0c1\ud0dc\uc774\uba74",
+ "is_triggered": "{entity_name}\uc774(\uac00) \ud2b8\ub9ac\uac70 \ub418\uc5c8\uc73c\uba74"
},
"trigger_type": {
- "armed_away": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub420 \ub54c",
- "armed_home": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub420 \ub54c",
- "armed_night": "{entity_name} \uc774(\uac00) \uc57c\uac04 \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub420 \ub54c",
- "disarmed": "{entity_name} \uc774(\uac00) \ud574\uc81c\ub420 \ub54c",
- "triggered": "{entity_name} \uc774(\uac00) \ud2b8\ub9ac\uac70\ub420 \ub54c"
+ "armed_away": "{entity_name}\uc774(\uac00) \uc678\ucd9c \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5c8\uc744 \ub54c",
+ "armed_home": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5c8\uc744 \ub54c",
+ "armed_night": "{entity_name}\uc774(\uac00) \uc57c\uac04 \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5c8\uc744 \ub54c",
+ "disarmed": "{entity_name}\uc774(\uac00) \ud574\uc81c\ub418\uc5c8\uc744 \ub54c",
+ "triggered": "{entity_name}\uc774(\uac00) \ud2b8\ub9ac\uac70\ub418\uc5c8\uc744 \ub54c"
}
},
"state": {
"_": {
- "armed": "\uacbd\ube44\uc911",
- "armed_away": "\uacbd\ube44\uc911(\uc678\ucd9c)",
- "armed_custom_bypass": "\uacbd\ube44\uc911(\uc0ac\uc6a9\uc790 \uc6b0\ud68c)",
- "armed_home": "\uacbd\ube44\uc911(\uc7ac\uc2e4)",
- "armed_night": "\uacbd\ube44\uc911(\uc57c\uac04)",
- "arming": "\uacbd\ube44\uc911",
+ "armed": "\uacbd\ube44 \uc911",
+ "armed_away": "\uacbd\ube44 \uc911(\uc678\ucd9c)",
+ "armed_custom_bypass": "\uacbd\ube44 \uc911 (\uc0ac\uc6a9\uc790 \uc6b0\ud68c)",
+ "armed_home": "\uacbd\ube44 \uc911(\uc7ac\uc2e4)",
+ "armed_night": "\uacbd\ube44 \uc911(\uc57c\uac04)",
+ "arming": "\uacbd\ube44 \uc911",
"disarmed": "\ud574\uc81c\ub428",
"disarming": "\ud574\uc81c\uc911",
"pending": "\ubcf4\ub958\uc911",
diff --git a/homeassistant/components/alarm_control_panel/translations/tr.json b/homeassistant/components/alarm_control_panel/translations/tr.json
index ebbcf568338883..cc509430436673 100644
--- a/homeassistant/components/alarm_control_panel/translations/tr.json
+++ b/homeassistant/components/alarm_control_panel/translations/tr.json
@@ -1,4 +1,10 @@
{
+ "device_automation": {
+ "trigger_type": {
+ "disarmed": "{entity_name} b\u0131rak\u0131ld\u0131",
+ "triggered": "{entity_name} tetiklendi"
+ }
+ },
"state": {
"_": {
"armed": "Etkin",
diff --git a/homeassistant/components/alarm_control_panel/translations/uk.json b/homeassistant/components/alarm_control_panel/translations/uk.json
index e618e297019558..b50fd9f459d714 100644
--- a/homeassistant/components/alarm_control_panel/translations/uk.json
+++ b/homeassistant/components/alarm_control_panel/translations/uk.json
@@ -1,13 +1,36 @@
{
+ "device_automation": {
+ "action_type": {
+ "arm_away": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}",
+ "arm_home": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u0412\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}",
+ "arm_night": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0456\u0447\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}",
+ "disarm": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043e\u0445\u043e\u0440\u043e\u043d\u0443 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}",
+ "trigger": "{entity_name} \u0441\u043f\u0440\u0430\u0446\u044c\u043e\u0432\u0443\u0454"
+ },
+ "condition_type": {
+ "is_armed_away": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}",
+ "is_armed_home": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u0412\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}",
+ "is_armed_night": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0456\u0447\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}",
+ "is_disarmed": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}",
+ "is_triggered": "{entity_name} \u0441\u043f\u0440\u0430\u0446\u044c\u043e\u0432\u0443\u0454"
+ },
+ "trigger_type": {
+ "armed_away": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}",
+ "armed_home": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u0412\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}",
+ "armed_night": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0456\u0447\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}",
+ "disarmed": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}",
+ "triggered": "{entity_name} \u0441\u043f\u0440\u0430\u0446\u044c\u043e\u0432\u0443\u0454"
+ }
+ },
"state": {
"_": {
"armed": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430",
- "armed_away": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u043d\u0435 \u0432\u0434\u043e\u043c\u0430)",
+ "armed_away": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u041d\u0435 \u0432\u0434\u043e\u043c\u0430)",
"armed_custom_bypass": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 \u0437 \u0432\u0438\u043d\u044f\u0442\u043a\u0430\u043c\u0438",
- "armed_home": "\u0411\u0443\u0434\u0438\u043d\u043a\u043e\u0432\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430",
+ "armed_home": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u0412\u0434\u043e\u043c\u0430)",
"armed_night": "\u041d\u0456\u0447\u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430",
"arming": "\u0421\u0442\u0430\u0432\u043b\u044e \u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0443",
- "disarmed": "\u0417\u043d\u044f\u0442\u043e",
+ "disarmed": "\u0417\u043d\u044f\u0442\u043e \u0437 \u043e\u0445\u043e\u0440\u043e\u043d\u0438",
"disarming": "\u0417\u043d\u044f\u0442\u0442\u044f",
"pending": "\u041e\u0447\u0456\u043a\u0443\u044e",
"triggered": "\u0422\u0440\u0438\u0432\u043e\u0433\u0430"
diff --git a/homeassistant/components/alarm_control_panel/translations/zh-Hans.json b/homeassistant/components/alarm_control_panel/translations/zh-Hans.json
index 749674e8e6e62d..fa819e71b4921a 100644
--- a/homeassistant/components/alarm_control_panel/translations/zh-Hans.json
+++ b/homeassistant/components/alarm_control_panel/translations/zh-Hans.json
@@ -1,4 +1,27 @@
{
+ "device_automation": {
+ "action_type": {
+ "arm_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212",
+ "arm_home": "{entity_name} \u5728\u5bb6\u8b66\u6212",
+ "arm_night": "{entity_name} \u591c\u95f4\u8b66\u6212",
+ "disarm": "\u89e3\u9664 {entity_name} \u8b66\u6212",
+ "trigger": "\u89e6\u53d1 {entity_name}"
+ },
+ "condition_type": {
+ "is_armed_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212",
+ "is_armed_home": "{entity_name} \u5728\u5bb6\u8b66\u6212",
+ "is_armed_night": "{entity_name} \u591c\u95f4\u8b66\u6212",
+ "is_disarmed": "{entity_name} \u8b66\u6212\u5df2\u89e3\u9664",
+ "is_triggered": "{entity_name} \u8b66\u62a5\u5df2\u89e6\u53d1"
+ },
+ "trigger_type": {
+ "armed_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212",
+ "armed_home": "{entity_name} \u5728\u5bb6\u8b66\u6212",
+ "armed_night": "{entity_name} \u591c\u95f4\u8b66\u6212",
+ "disarmed": "{entity_name} \u8b66\u6212\u89e3\u9664",
+ "triggered": "{entity_name} \u89e6\u53d1\u8b66\u62a5"
+ }
+ },
"state": {
"_": {
"armed": "\u8b66\u6212",
diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py
index 8dd704f133345a..849aae9b3cc11a 100644
--- a/homeassistant/components/alarmdecoder/__init__.py
+++ b/homeassistant/components/alarmdecoder/__init__.py
@@ -39,11 +39,6 @@
PLATFORMS = ["alarm_control_panel", "sensor", "binary_sensor"]
-async def async_setup(hass, config):
- """Set up for the AlarmDecoder devices."""
- return True
-
-
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
"""Set up AlarmDecoder config flow."""
undo_listener = entry.add_update_listener(_update_listener)
@@ -130,9 +125,9 @@ def handle_rel_message(sender, message):
await open_connection()
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -144,8 +139,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py
index 5e3118b1d3df25..9cab2afa43cbcb 100644
--- a/homeassistant/components/alarmdecoder/alarm_control_panel.py
+++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py
@@ -163,7 +163,7 @@ def code_arm_required(self):
return self._code_arm_required
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
"ac_power": self._ac_power,
diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py
index 55bf13d7fef0b5..4cc3bb6b5cf8bd 100644
--- a/homeassistant/components/alarmdecoder/binary_sensor.py
+++ b/homeassistant/components/alarmdecoder/binary_sensor.py
@@ -118,7 +118,7 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attr = {CONF_ZONE_NUMBER: self._zone_number}
if self._rfid and self._rfstate is not None:
diff --git a/homeassistant/components/alarmdecoder/config_flow.py b/homeassistant/components/alarmdecoder/config_flow.py
index a82b84b60d1611..137795c684fe8a 100644
--- a/homeassistant/components/alarmdecoder/config_flow.py
+++ b/homeassistant/components/alarmdecoder/config_flow.py
@@ -11,7 +11,7 @@
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL
from homeassistant.core import callback
-from .const import ( # pylint: disable=unused-import
+from .const import (
CONF_ALT_NIGHT_MODE,
CONF_AUTO_BYPASS,
CONF_CODE_ARM_REQUIRED,
@@ -349,12 +349,18 @@ def _device_already_added(current_entries, user_input, protocol):
entry_path = entry.data.get(CONF_DEVICE_PATH)
entry_baud = entry.data.get(CONF_DEVICE_BAUD)
- if protocol == PROTOCOL_SOCKET:
- if user_host == entry_host and user_port == entry_port:
- return True
-
- if protocol == PROTOCOL_SERIAL:
- if user_baud == entry_baud and user_path == entry_path:
- return True
+ if (
+ protocol == PROTOCOL_SOCKET
+ and user_host == entry_host
+ and user_port == entry_port
+ ):
+ return True
+
+ if (
+ protocol == PROTOCOL_SERIAL
+ and user_baud == entry_baud
+ and user_path == entry_path
+ ):
+ return True
return False
diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json
index 1697858718d273..c3e72e407c2620 100644
--- a/homeassistant/components/alarmdecoder/manifest.json
+++ b/homeassistant/components/alarmdecoder/manifest.json
@@ -2,7 +2,7 @@
"domain": "alarmdecoder",
"name": "AlarmDecoder",
"documentation": "https://www.home-assistant.io/integrations/alarmdecoder",
- "requirements": ["adext==0.3"],
+ "requirements": ["adext==0.4.1"],
"codeowners": ["@ajschmidt8"],
"config_flow": true
}
diff --git a/homeassistant/components/alarmdecoder/sensor.py b/homeassistant/components/alarmdecoder/sensor.py
index a8aed8dac73c1c..80b9c1261a3b6f 100644
--- a/homeassistant/components/alarmdecoder/sensor.py
+++ b/homeassistant/components/alarmdecoder/sensor.py
@@ -1,6 +1,6 @@
"""Support for AlarmDecoder sensors (Shows Panel Display)."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
from .const import SIGNAL_PANEL_MESSAGE
@@ -16,7 +16,7 @@ async def async_setup_entry(
return True
-class AlarmDecoderSensor(Entity):
+class AlarmDecoderSensor(SensorEntity):
"""Representation of an AlarmDecoder keypad."""
def __init__(self):
diff --git a/homeassistant/components/alarmdecoder/translations/de.json b/homeassistant/components/alarmdecoder/translations/de.json
index 3f1b7ef816ed31..c37fb7b43906f3 100644
--- a/homeassistant/components/alarmdecoder/translations/de.json
+++ b/homeassistant/components/alarmdecoder/translations/de.json
@@ -1,7 +1,10 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"step": {
"protocol": {
@@ -42,7 +45,7 @@
"data": {
"zone_number": "Zonennummer"
},
- "description": "Geben Sie die Zonennummer ein, die Sie hinzuf\u00fcgen, bearbeiten oder entfernen m\u00f6chten."
+ "description": "Gib die die Zonennummer ein, die du hinzuf\u00fcgen, bearbeiten oder entfernen m\u00f6chtest."
}
}
}
diff --git a/homeassistant/components/alarmdecoder/translations/hu.json b/homeassistant/components/alarmdecoder/translations/hu.json
index 2d5f91cf3737ff..8c80adcb3c0f54 100644
--- a/homeassistant/components/alarmdecoder/translations/hu.json
+++ b/homeassistant/components/alarmdecoder/translations/hu.json
@@ -1,10 +1,40 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
"create_entry": {
"default": "Sikeres csatlakoz\u00e1s az AlarmDecoderhez."
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "host": "Hoszt",
+ "port": "Port"
+ }
+ },
+ "user": {
+ "data": {
+ "protocol": "Protokoll"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "edit_select": "Szerkeszt\u00e9s"
+ }
+ },
+ "zone_details": {
+ "data": {
+ "zone_name": "Z\u00f3na neve"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/id.json b/homeassistant/components/alarmdecoder/translations/id.json
new file mode 100644
index 00000000000000..39c8282b36fc18
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/id.json
@@ -0,0 +1,74 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "create_entry": {
+ "default": "Berhasil terhubung ke AlarmDecoder."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "device_baudrate": "Laju Baud Perangkat",
+ "device_path": "Jalur Perangkat",
+ "host": "Host",
+ "port": "Port"
+ },
+ "title": "Konfigurasikan pengaturan koneksi"
+ },
+ "user": {
+ "data": {
+ "protocol": "Protokol"
+ },
+ "title": "Pilih Protokol AlarmDecoder"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "int": "Bidang di bawah ini harus berupa bilangan bulat.",
+ "loop_range": "RF Loop harus merupakan bilangan bulat antara 1 dan 4.",
+ "loop_rfid": "RF Loop tidak dapat digunakan tanpa RF Serial.",
+ "relay_inclusive": "Relay Address dan Relay Channel saling tergantung dan harus disertakan bersama-sama."
+ },
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "Mode Malam Alternatif",
+ "auto_bypass": "Diaktifkan Secara Otomatis",
+ "code_arm_required": "Kode Diperlukan untuk Mengaktifkan"
+ },
+ "title": "Konfigurasikan AlarmDecoder"
+ },
+ "init": {
+ "data": {
+ "edit_select": "Edit"
+ },
+ "description": "Apa yang ingin diedit?",
+ "title": "Konfigurasikan AlarmDecoder"
+ },
+ "zone_details": {
+ "data": {
+ "zone_loop": "RF Loop",
+ "zone_name": "Nama Zona",
+ "zone_relayaddr": "Relay Address",
+ "zone_relaychan": "Relay Channel",
+ "zone_rfid": "RF Serial",
+ "zone_type": "Jenis Zona"
+ },
+ "description": "Masukkan detail untuk zona {zone_number}. Untuk menghapus zona {zone_number}, kosongkan Nama Zona.",
+ "title": "Konfigurasikan AlarmDecoder"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "Nomor Zona"
+ },
+ "description": "Masukkan nomor zona yang ingin ditambahkan, diedit, atau dihapus.",
+ "title": "Konfigurasikan AlarmDecoder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/ko.json b/homeassistant/components/alarmdecoder/translations/ko.json
index ed29a3260efd22..cdb63a01bfb4a3 100644
--- a/homeassistant/components/alarmdecoder/translations/ko.json
+++ b/homeassistant/components/alarmdecoder/translations/ko.json
@@ -1,70 +1,73 @@
{
"config": {
"abort": {
- "already_configured": "\uc7a5\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"create_entry": {
"default": "AlarmDecoder\uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
},
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
"step": {
"protocol": {
"data": {
- "device_baudrate": "\uc7a5\uce58 \uc804\uc1a1 \uc18d\ub3c4",
- "device_path": "\uc7a5\uce58 \uacbd\ub85c",
+ "device_baudrate": "\uae30\uae30 \uc804\uc1a1 \uc18d\ub3c4",
+ "device_path": "\uae30\uae30 \uacbd\ub85c",
"host": "\ud638\uc2a4\ud2b8",
"port": "\ud3ec\ud2b8"
},
- "title": "\uc5f0\uacb0 \uc124\uc815 \uad6c\uc131"
+ "title": "\uc5f0\uacb0 \uc124\uc815 \uad6c\uc131\ud558\uae30"
},
"user": {
"data": {
"protocol": "\ud504\ub85c\ud1a0\ucf5c"
},
- "title": "AlarmDecoder \ud504\ub85c\ud1a0\ucf5c \uc120\ud0dd"
+ "title": "AlarmDecoder \ud504\ub85c\ud1a0\ucf5c \uc120\ud0dd\ud558\uae30"
}
}
},
"options": {
"error": {
"int": "\uc544\ub798 \ud544\ub4dc\ub294 \uc815\uc218\uc5ec\uc57c \ud569\ub2c8\ub2e4.",
- "loop_range": "RF \ub8e8\ud504\ub294 1\uc5d0\uc11c 4 \uc0ac\uc774\uc758 \uc815\uc218\uc5ec\uc57c \ud569\ub2c8\ub2e4.",
- "loop_rfid": "RF \ub8e8\ud504\ub294 RF \uc2dc\ub9ac\uc5bc\uc5c6\uc774 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
- "relay_inclusive": "\ub9b4\ub808\uc774 \uc8fc\uc18c\uc640 \ub9b4\ub808\uc774 \ucc44\ub110\uc740 \uc11c\ub85c \uc758\uc874\uc801\uc774\uba70 \ud568\uaed8 \ud3ec\ud568\ub418\uc5b4\uc57c\ud569\ub2c8\ub2e4."
+ "loop_range": "RF \ub8e8\ud504\ub294 1\uacfc 4 \uc0ac\uc774\uc758 \uc815\uc218\uc5ec\uc57c \ud569\ub2c8\ub2e4.",
+ "loop_rfid": "RF \ub8e8\ud504\ub294 RF \uc2dc\ub9ac\uc5bc \uc5c6\uc73c\uba74 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
+ "relay_inclusive": "\ub9b4\ub808\uc774 \uc8fc\uc18c\uc640 \ub9b4\ub808\uc774 \ucc44\ub110\uc740 \uc0c1\ud638 \uc758\uc874\uc801\uc774\uae30 \ub54c\ubb38\uc5d0 \ud568\uaed8 \ud3ec\ud568\ub418\uc5b4\uc57c \ud569\ub2c8\ub2e4."
},
"step": {
"arm_settings": {
"data": {
"alt_night_mode": "\ub300\uccb4 \uc57c\uac04 \ubaa8\ub4dc",
- "auto_bypass": "\uacbd\ube44\uc911 \uc790\ub3d9 \uc6b0\ud68c",
+ "auto_bypass": "\uacbd\ube44 \uc911 \uc790\ub3d9 \uc6b0\ud68c",
"code_arm_required": "\uacbd\ube44\uc5d0 \ud544\uc694\ud55c \ucf54\ub4dc"
},
- "title": "AlarmDecoder \uad6c\uc131"
+ "title": "AlarmDecoder \uad6c\uc131\ud558\uae30"
},
"init": {
"data": {
- "edit_select": "\ud3b8\uc9d1"
+ "edit_select": "\ud3b8\uc9d1\ud558\uae30"
},
- "description": "\ubb34\uc5c7\uc744 \ud3b8\uc9d1 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
- "title": "AlarmDecoder \uad6c\uc131"
+ "description": "\ubb34\uc5c7\uc744 \ud3b8\uc9d1\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "AlarmDecoder \uad6c\uc131\ud558\uae30"
},
"zone_details": {
"data": {
"zone_loop": "RF \ub8e8\ud504",
- "zone_name": "\uc601\uc5ed \uc774\ub984",
+ "zone_name": "\uad6c\uc5ed \uc774\ub984",
"zone_relayaddr": "\ub9b4\ub808\uc774 \uc8fc\uc18c",
"zone_relaychan": "\ub9b4\ub808\uc774 \ucc44\ub110",
"zone_rfid": "RF \uc2dc\ub9ac\uc5bc",
- "zone_type": "\uc601\uc5ed \uc720\ud615"
+ "zone_type": "\uad6c\uc5ed \uc720\ud615"
},
- "description": "{zone_number} \uc601\uc5ed\uc5d0 \ub300\ud55c \uc138\ubd80 \uc815\ubcf4\ub97c \uc785\ub825\ud569\ub2c8\ub2e4. {zone_number} \uc601\uc5ed\uc744 \uc0ad\uc81c\ud558\ub824\uba74 \uc601\uc5ed \uc774\ub984\uc744 \ube44\uc6cc \ub461\ub2c8\ub2e4.",
- "title": "AlarmDecoder \uad6c\uc131"
+ "description": "\uad6c\uc5ed {zone_number}\uc5d0 \ub300\ud55c \uc138\ubd80 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \uad6c\uc5ed {zone_number}\uc744(\ub97c) \uc0ad\uc81c\ud558\ub824\uba74 \uad6c\uc5ed \uc774\ub984\uc744 \ube44\uc6cc\ub450\uc138\uc694.",
+ "title": "AlarmDecoder \uad6c\uc131\ud558\uae30"
},
"zone_select": {
"data": {
"zone_number": "\uad6c\uc5ed \ubc88\ud638"
},
- "description": "\ucd94\uac00, \ud3b8\uc9d1 \ub610\ub294 \uc81c\uac70\ud560 \uc601\uc5ed \ubc88\ud638\ub97c \uc785\ub825\ud569\ub2c8\ub2e4.",
- "title": "AlarmDecoder \uad6c\uc131"
+ "description": "\ucd94\uac00, \ud3b8\uc9d1 \ub610\ub294 \uc81c\uac70\ud560 \uad6c\uc5ed \ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "AlarmDecoder \uad6c\uc131\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/alarmdecoder/translations/nl.json b/homeassistant/components/alarmdecoder/translations/nl.json
index 1af1e8d803c9c2..1ea9cb98b56a69 100644
--- a/homeassistant/components/alarmdecoder/translations/nl.json
+++ b/homeassistant/components/alarmdecoder/translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "AlarmDecoder-apparaat is al geconfigureerd."
+ "already_configured": "Apparaat is al geconfigureerd"
},
"create_entry": {
"default": "Succesvol verbonden met AlarmDecoder."
diff --git a/homeassistant/components/alarmdecoder/translations/tr.json b/homeassistant/components/alarmdecoder/translations/tr.json
new file mode 100644
index 00000000000000..276b733b31fd5f
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/tr.json
@@ -0,0 +1,48 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "relay_inclusive": "R\u00f6le Adresi ve R\u00f6le Kanal\u0131 birbirine ba\u011fl\u0131d\u0131r ve birlikte eklenmelidir."
+ },
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "Alternatif Gece Modu"
+ }
+ },
+ "init": {
+ "data": {
+ "edit_select": "D\u00fczenle"
+ }
+ },
+ "zone_details": {
+ "data": {
+ "zone_name": "B\u00f6lge Ad\u0131",
+ "zone_relayaddr": "R\u00f6le Adresi",
+ "zone_relaychan": "R\u00f6le Kanal\u0131"
+ }
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "B\u00f6lge Numaras\u0131"
+ },
+ "title": "AlarmDecoder'\u0131 yap\u0131land\u0131r\u0131n"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/uk.json b/homeassistant/components/alarmdecoder/translations/uk.json
new file mode 100644
index 00000000000000..c19d00c0ecad1f
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/uk.json
@@ -0,0 +1,74 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "create_entry": {
+ "default": "\u0423\u0441\u043f\u0456\u0448\u043d\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e AlarmDecoder."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "device_baudrate": "\u0428\u0432\u0438\u0434\u043a\u0456\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0456 \u0434\u0430\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e",
+ "device_path": "\u0428\u043b\u044f\u0445 \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e",
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f"
+ },
+ "user": {
+ "data": {
+ "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b"
+ },
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b AlarmDecoder"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "int": "\u041f\u043e\u043b\u0435 \u043d\u0438\u0436\u0447\u0435 \u043c\u0430\u0454 \u0431\u0443\u0442\u0438 \u0446\u0456\u043b\u0438\u043c \u0447\u0438\u0441\u043b\u043e\u043c.",
+ "loop_range": "RF Loop \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0446\u0456\u043b\u0438\u043c \u0447\u0438\u0441\u043b\u043e\u043c \u0432\u0456\u0434 1 \u0434\u043e 4.",
+ "loop_rfid": "RF Loop \u043d\u0435 \u043c\u043e\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0431\u0435\u0437 RF Serial.",
+ "relay_inclusive": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0440\u0435\u043b\u0435 \u0456 \u043a\u0430\u043d\u0430\u043b \u0440\u0435\u043b\u0435 \u0432\u0437\u0430\u0454\u043c\u043e\u0437\u0430\u043b\u0435\u0436\u043d\u0456 \u0456 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0456 \u0440\u0430\u0437\u043e\u043c."
+ },
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u043d\u0456\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c",
+ "auto_bypass": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0438\u0439 \u0432\u043a\u043b\u044e\u0447\u0430\u0442\u0438 \u0432\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438 \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u0446\u0456 \u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0443",
+ "code_arm_required": "\u041a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0438\u0439 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0443"
+ },
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AlarmDecoder"
+ },
+ "init": {
+ "data": {
+ "edit_select": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438"
+ },
+ "description": "\u0429\u043e \u0431 \u0412\u0438 \u0445\u043e\u0442\u0456\u043b\u0438 \u0440\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u0442\u0438?",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AlarmDecoder"
+ },
+ "zone_details": {
+ "data": {
+ "zone_loop": "RF Loop",
+ "zone_name": "\u041d\u0430\u0437\u0432\u0430 \u0437\u043e\u043d\u0438",
+ "zone_relayaddr": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0440\u0435\u043b\u0435",
+ "zone_relaychan": "\u041a\u0430\u043d\u0430\u043b \u0440\u0435\u043b\u0435",
+ "zone_rfid": "RF Serial",
+ "zone_type": "\u0422\u0438\u043f \u0437\u043e\u043d\u0438"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u0430\u043d\u0456 \u0434\u043b\u044f \u0437\u043e\u043d\u0438 {zone_number}. \u0429\u043e\u0431 \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u0437\u043e\u043d\u0443 {zone_number}, \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u043b\u0435 \"\u041d\u0430\u0437\u0432\u0430 \u0437\u043e\u043d\u0438\" \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AlarmDecoder"
+ },
+ "zone_select": {
+ "data": {
+ "zone_number": "\u041d\u043e\u043c\u0435\u0440 \u0437\u043e\u043d\u0438"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043d\u043e\u043c\u0435\u0440 \u0437\u043e\u043d\u0438, \u044f\u043a\u0443 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438, \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u0430\u0431\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AlarmDecoder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py
index 53b1a1248dce05..73bea193394c22 100644
--- a/homeassistant/components/alert/__init__.py
+++ b/homeassistant/components/alert/__init__.py
@@ -14,6 +14,7 @@
ATTR_ENTITY_ID,
CONF_ENTITY_ID,
CONF_NAME,
+ CONF_REPEAT,
CONF_STATE,
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
@@ -33,7 +34,6 @@
CONF_CAN_ACK = "can_acknowledge"
CONF_NOTIFIERS = "notifiers"
-CONF_REPEAT = "repeat"
CONF_SKIP_FIRST = "skip_first"
CONF_ALERT_MESSAGE = "message"
CONF_DONE_MESSAGE = "done_message"
diff --git a/homeassistant/components/alert/reproduce_state.py b/homeassistant/components/alert/reproduce_state.py
index 7645b642d59500..dfe51df7531819 100644
--- a/homeassistant/components/alert/reproduce_state.py
+++ b/homeassistant/components/alert/reproduce_state.py
@@ -1,7 +1,9 @@
"""Reproduce an Alert state."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -10,8 +12,7 @@
STATE_OFF,
STATE_ON,
)
-from homeassistant.core import Context, State
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import Context, HomeAssistant, State
from . import DOMAIN
@@ -21,11 +22,11 @@
async def _async_reproduce_state(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -58,11 +59,11 @@ async def _async_reproduce_state(
async def async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Alert states."""
# Reproduce states in parallel.
diff --git a/homeassistant/components/alert/services.yaml b/homeassistant/components/alert/services.yaml
index 995302005460e7..5800d642b93116 100644
--- a/homeassistant/components/alert/services.yaml
+++ b/homeassistant/components/alert/services.yaml
@@ -1,18 +1,14 @@
toggle:
+ name: Toggle
description: Toggle alert's notifications.
- fields:
- entity_id:
- description: Name of the alert to toggle.
- example: alert.garage_door_open
+ target:
+
turn_off:
+ name: Turn off
description: Silence alert's notifications.
- fields:
- entity_id:
- description: Name of the alert to silence.
- example: alert.garage_door_open
+ target:
+
turn_on:
+ name: Turn on
description: Reset alert's notifications.
- fields:
- entity_id:
- description: Name of the alert to reset.
- example: alert.garage_door_open
+ target:
diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py
index 5180a8d55b63bb..d388a22983fcaa 100644
--- a/homeassistant/components/alexa/__init__.py
+++ b/homeassistant/components/alexa/__init__.py
@@ -1,20 +1,24 @@
"""Support for Alexa skill service end point."""
import voluptuous as vol
-from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_NAME
+from homeassistant.const import (
+ CONF_CLIENT_ID,
+ CONF_CLIENT_SECRET,
+ CONF_DESCRIPTION,
+ CONF_NAME,
+ CONF_PASSWORD,
+)
from homeassistant.helpers import config_validation as cv, entityfilter
from . import flash_briefings, intent, smart_home_http
from .const import (
CONF_AUDIO,
- CONF_DESCRIPTION,
CONF_DISPLAY_CATEGORIES,
CONF_DISPLAY_URL,
CONF_ENDPOINT,
CONF_ENTITY_CONFIG,
CONF_FILTER,
CONF_LOCALE,
- CONF_PASSWORD,
CONF_SUPPORTED_LOCALES,
CONF_TEXT,
CONF_TITLE,
diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py
index 008870c8dd93b0..69acf95e207333 100644
--- a/homeassistant/components/alexa/capabilities.py
+++ b/homeassistant/components/alexa/capabilities.py
@@ -1,6 +1,7 @@
"""Alexa capabilities."""
+from __future__ import annotations
+
import logging
-from typing import List, Optional
from homeassistant.components import (
cover,
@@ -46,7 +47,6 @@
API_THERMOSTAT_MODES,
API_THERMOSTAT_PRESETS,
DATE_FORMAT,
- PERCENTAGE_FAN_MAP,
Inputs,
)
from .errors import UnsupportedProperty
@@ -73,7 +73,7 @@ class AlexaCapability:
supported_locales = {"en-US"}
- def __init__(self, entity: State, instance: Optional[str] = None):
+ def __init__(self, entity: State, instance: str | None = None):
"""Initialize an Alexa capability."""
self.entity = entity
self.instance = instance
@@ -83,7 +83,7 @@ def name(self) -> str:
raise NotImplementedError
@staticmethod
- def properties_supported() -> List[dict]:
+ def properties_supported() -> list[dict]:
"""Return what properties this entity supports."""
return []
@@ -668,9 +668,7 @@ def get_property(self, name):
raise UnsupportedProperty(name)
if self.entity.domain == fan.DOMAIN:
- speed = self.entity.attributes.get(fan.ATTR_SPEED)
-
- return PERCENTAGE_FAN_MAP.get(speed, 0)
+ return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
if self.entity.domain == cover.DOMAIN:
return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION, 0)
@@ -1155,9 +1153,7 @@ def get_property(self, name):
raise UnsupportedProperty(name)
if self.entity.domain == fan.DOMAIN:
- speed = self.entity.attributes.get(fan.ATTR_SPEED)
-
- return PERCENTAGE_FAN_MAP.get(speed)
+ return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
return None
diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py
index a5a1cde2e15dcb..de8a4a6fdc4307 100644
--- a/homeassistant/components/alexa/const.py
+++ b/homeassistant/components/alexa/const.py
@@ -1,7 +1,6 @@
"""Constants for the Alexa integration."""
from collections import OrderedDict
-from homeassistant.components import fan
from homeassistant.components.climate import const as climate
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
@@ -19,7 +18,6 @@
CONF_ENTITY_CONFIG = "entity_config"
CONF_ENDPOINT = "endpoint"
CONF_LOCALE = "locale"
-CONF_PASSWORD = "password"
ATTR_UID = "uid"
ATTR_UPDATE_DATE = "updateDate"
@@ -42,7 +40,6 @@
API_CHANGE = "change"
API_PASSWORD = "password"
-CONF_DESCRIPTION = "description"
CONF_DISPLAY_CATEGORIES = "display_categories"
CONF_SUPPORTED_LOCALES = (
"de-DE",
@@ -53,10 +50,13 @@
"en-US",
"es-ES",
"es-MX",
+ "es-US",
"fr-CA",
"fr-FR",
+ "hi-IN",
"it-IT",
"ja-JP",
+ "pt-BR",
)
API_TEMP_UNITS = {TEMP_FAHRENHEIT: "FAHRENHEIT", TEMP_CELSIUS: "CELSIUS"}
@@ -78,13 +78,6 @@
API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"}
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
-PERCENTAGE_FAN_MAP = {
- fan.SPEED_OFF: 0,
- fan.SPEED_LOW: 33,
- fan.SPEED_MEDIUM: 66,
- fan.SPEED_HIGH: 100,
-}
-
class Cause:
"""Possible causes for property changes.
diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py
index c05d9641b9ae63..e7eaeb4a1cbe43 100644
--- a/homeassistant/components/alexa/entities.py
+++ b/homeassistant/components/alexa/entities.py
@@ -1,6 +1,8 @@
"""Alexa entity adapters."""
+from __future__ import annotations
+
import logging
-from typing import TYPE_CHECKING, List
+from typing import TYPE_CHECKING
from homeassistant.components import (
alarm_control_panel,
@@ -30,6 +32,7 @@
ATTR_SUPPORTED_FEATURES,
ATTR_UNIT_OF_MEASUREMENT,
CLOUD_NEVER_EXPOSED_ENTITIES,
+ CONF_DESCRIPTION,
CONF_NAME,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
@@ -72,7 +75,7 @@
AlexaTimeHoldController,
AlexaToggleController,
)
-from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES
+from .const import CONF_DISPLAY_CATEGORIES
if TYPE_CHECKING:
from .config import AbstractConfig
@@ -251,7 +254,7 @@ class AlexaEntity:
The API handlers should manipulate entities only through this interface.
"""
- def __init__(self, hass: HomeAssistant, config: "AbstractConfig", entity: State):
+ def __init__(self, hass: HomeAssistant, config: AbstractConfig, entity: State):
"""Initialize Alexa Entity."""
self.hass = hass
self.config = config
@@ -300,7 +303,7 @@ def get_interface(self, capability) -> AlexaCapability:
Raises _UnsupportedInterface.
"""
- def interfaces(self) -> List[AlexaCapability]:
+ def interfaces(self) -> list[AlexaCapability]:
"""Return a list of supported interfaces.
Used for discovery. The list should contain AlexaInterface instances.
@@ -353,7 +356,7 @@ def serialize_discovery(self):
@callback
-def async_get_entities(hass, config) -> List[AlexaEntity]:
+def async_get_entities(hass, config) -> list[AlexaEntity]:
"""Return all entities that are supported by Alexa."""
entities = []
for state in hass.states.async_all():
diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py
index b8f78705e10be5..50463810bbfb77 100644
--- a/homeassistant/components/alexa/flash_briefings.py
+++ b/homeassistant/components/alexa/flash_briefings.py
@@ -5,7 +5,7 @@
import uuid
from homeassistant.components import http
-from homeassistant.const import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED
+from homeassistant.const import CONF_PASSWORD, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED
from homeassistant.core import callback
from homeassistant.helpers import template
import homeassistant.util.dt as dt_util
@@ -20,7 +20,6 @@
ATTR_UPDATE_DATE,
CONF_AUDIO,
CONF_DISPLAY_URL,
- CONF_PASSWORD,
CONF_TEXT,
CONF_TITLE,
CONF_UID,
diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py
index 8837210b6ad726..cee4cda562d8cd 100644
--- a/homeassistant/components/alexa/handlers.py
+++ b/homeassistant/components/alexa/handlers.py
@@ -54,7 +54,6 @@
API_THERMOSTAT_MODES,
API_THERMOSTAT_MODES_CUSTOM,
API_THERMOSTAT_PRESETS,
- PERCENTAGE_FAN_MAP,
Cause,
Inputs,
)
@@ -360,17 +359,9 @@ async def async_api_set_percentage(hass, config, directive, context):
data = {ATTR_ENTITY_ID: entity.entity_id}
if entity.domain == fan.DOMAIN:
- service = fan.SERVICE_SET_SPEED
- speed = "off"
-
+ service = fan.SERVICE_SET_PERCENTAGE
percentage = int(directive.payload["percentage"])
- if percentage <= 33:
- speed = "low"
- elif percentage <= 66:
- speed = "medium"
- elif percentage <= 100:
- speed = "high"
- data[fan.ATTR_SPEED] = speed
+ data[fan.ATTR_PERCENTAGE] = percentage
await hass.services.async_call(
entity.domain, service, data, blocking=False, context=context
@@ -388,22 +379,12 @@ async def async_api_adjust_percentage(hass, config, directive, context):
data = {ATTR_ENTITY_ID: entity.entity_id}
if entity.domain == fan.DOMAIN:
- service = fan.SERVICE_SET_SPEED
- speed = entity.attributes.get(fan.ATTR_SPEED)
- current = PERCENTAGE_FAN_MAP.get(speed, 100)
+ service = fan.SERVICE_SET_PERCENTAGE
+ current = entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
# set percentage
- percentage = max(0, percentage_delta + current)
- speed = "off"
-
- if percentage <= 33:
- speed = "low"
- elif percentage <= 66:
- speed = "medium"
- elif percentage <= 100:
- speed = "high"
-
- data[fan.ATTR_SPEED] = speed
+ percentage = min(100, max(0, percentage_delta + current))
+ data[fan.ATTR_PERCENTAGE] = percentage
await hass.services.async_call(
entity.domain, service, data, blocking=False, context=context
@@ -672,10 +653,9 @@ def temperature_from_object(hass, temp_obj, interval=False):
if temp_obj["scale"] == "FAHRENHEIT":
from_unit = TEMP_FAHRENHEIT
- elif temp_obj["scale"] == "KELVIN":
+ elif temp_obj["scale"] == "KELVIN" and not interval:
# convert to Celsius if absolute temperature
- if not interval:
- temp -= 273.15
+ temp -= 273.15
return convert_temperature(temp, from_unit, to_unit, interval)
@@ -854,18 +834,9 @@ async def async_api_set_power_level(hass, config, directive, context):
data = {ATTR_ENTITY_ID: entity.entity_id}
if entity.domain == fan.DOMAIN:
- service = fan.SERVICE_SET_SPEED
- speed = "off"
-
+ service = fan.SERVICE_SET_PERCENTAGE
percentage = int(directive.payload["powerLevel"])
- if percentage <= 33:
- speed = "low"
- elif percentage <= 66:
- speed = "medium"
- else:
- speed = "high"
-
- data[fan.ATTR_SPEED] = speed
+ data[fan.ATTR_PERCENTAGE] = percentage
await hass.services.async_call(
entity.domain, service, data, blocking=False, context=context
@@ -883,22 +854,12 @@ async def async_api_adjust_power_level(hass, config, directive, context):
data = {ATTR_ENTITY_ID: entity.entity_id}
if entity.domain == fan.DOMAIN:
- service = fan.SERVICE_SET_SPEED
- speed = entity.attributes.get(fan.ATTR_SPEED)
- current = PERCENTAGE_FAN_MAP.get(speed, 100)
+ service = fan.SERVICE_SET_PERCENTAGE
+ current = entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
# set percentage
- percentage = max(0, percentage_delta + current)
- speed = "off"
-
- if percentage <= 33:
- speed = "low"
- elif percentage <= 66:
- speed = "medium"
- else:
- speed = "high"
-
- data[fan.ATTR_SPEED] = speed
+ percentage = min(100, max(0, percentage_delta + current))
+ data[fan.ATTR_PERCENTAGE] = percentage
await hass.services.async_call(
entity.domain, service, data, blocking=False, context=context
diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py
index aa4110ea68698c..712a08ac6b9ea7 100644
--- a/homeassistant/components/alexa/state_report.py
+++ b/homeassistant/components/alexa/state_report.py
@@ -1,14 +1,15 @@
"""Alexa state report code."""
+from __future__ import annotations
+
import asyncio
import json
import logging
-from typing import Optional
import aiohttp
import async_timeout
from homeassistant.const import HTTP_ACCEPTED, MATCH_ALL, STATE_ON
-from homeassistant.core import State
+from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers.significant_change import create_checker
import homeassistant.util.dt as dt_util
@@ -28,12 +29,25 @@ async def async_enable_proactive_mode(hass, smart_home_config):
# Validate we can get access token.
await smart_home_config.async_get_access_token()
- checker = await create_checker(hass, DOMAIN)
+ @callback
+ def extra_significant_check(
+ hass: HomeAssistant,
+ old_state: str,
+ old_attrs: dict,
+ old_extra_arg: dict,
+ new_state: str,
+ new_attrs: dict,
+ new_extra_arg: dict,
+ ):
+ """Check if the serialized data has changed."""
+ return old_extra_arg is not None and old_extra_arg != new_extra_arg
+
+ checker = await create_checker(hass, DOMAIN, extra_significant_check)
async def async_entity_state_listener(
changed_entity: str,
- old_state: Optional[State],
- new_state: Optional[State],
+ old_state: State | None,
+ new_state: State | None,
):
if not hass.is_running:
return
@@ -60,31 +74,30 @@ async def async_entity_state_listener(
if not should_report and interface.properties_proactively_reported():
should_report = True
- if (
- interface.name() == "Alexa.DoorbellEventSource"
- and new_state.state == STATE_ON
- ):
+ if interface.name() == "Alexa.DoorbellEventSource":
should_doorbell = True
break
if not should_report and not should_doorbell:
return
- if not checker.async_is_significant_change(new_state):
+ if should_doorbell:
+ if new_state.state == STATE_ON:
+ await async_send_doorbell_event_message(
+ hass, smart_home_config, alexa_changed_entity
+ )
return
- if should_doorbell:
- should_report = False
+ alexa_properties = list(alexa_changed_entity.serialize_properties())
- if should_report:
- await async_send_changereport_message(
- hass, smart_home_config, alexa_changed_entity
- )
+ if not checker.async_is_significant_change(
+ new_state, extra_arg=alexa_properties
+ ):
+ return
- elif should_doorbell:
- await async_send_doorbell_event_message(
- hass, smart_home_config, alexa_changed_entity
- )
+ await async_send_changereport_message(
+ hass, smart_home_config, alexa_changed_entity, alexa_properties
+ )
return hass.helpers.event.async_track_state_change(
MATCH_ALL, async_entity_state_listener
@@ -92,7 +105,7 @@ async def async_entity_state_listener(
async def async_send_changereport_message(
- hass, config, alexa_entity, *, invalidate_access_token=True
+ hass, config, alexa_entity, alexa_properties, *, invalidate_access_token=True
):
"""Send a ChangeReport message for an Alexa entity.
@@ -107,7 +120,7 @@ async def async_send_changereport_message(
payload = {
API_CHANGE: {
"cause": {"type": Cause.APP_INTERACTION},
- "properties": list(alexa_entity.serialize_properties()),
+ "properties": alexa_properties,
}
}
@@ -146,7 +159,7 @@ async def async_send_changereport_message(
):
config.async_invalidate_access_token()
return await async_send_changereport_message(
- hass, config, alexa_entity, invalidate_access_token=False
+ hass, config, alexa_entity, alexa_properties, invalidate_access_token=False
)
_LOGGER.error(
@@ -226,7 +239,7 @@ async def async_send_delete_message(hass, config, entity_ids):
async def async_send_doorbell_event_message(hass, config, alexa_entity):
"""Send a DoorbellPress event message for an Alexa entity.
- https://developer.amazon.com/docs/smarthome/send-events-to-the-alexa-event-gateway.html
+ https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-doorbelleventsource.html
"""
token = await config.async_get_access_token()
diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py
index b9f75ff8c6b4fc..554a4aa47bcf8a 100644
--- a/homeassistant/components/almond/__init__.py
+++ b/homeassistant/components/almond/__init__.py
@@ -1,9 +1,10 @@
"""Support for Almond."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
import logging
import time
-from typing import Optional
from aiohttp import ClientError, ClientSession
import async_timeout
@@ -281,7 +282,7 @@ async def async_set_onboarding(self, shown):
return True
async def async_process(
- self, text: str, context: Context, conversation_id: Optional[str] = None
+ self, text: str, context: Context, conversation_id: str | None = None
) -> intent.IntentResponse:
"""Process a sentence."""
response = await self.api.async_converse_text(text, conversation_id)
diff --git a/homeassistant/components/almond/strings.json b/homeassistant/components/almond/strings.json
index 8eb5478ecef756..548471a664c0cb 100644
--- a/homeassistant/components/almond/strings.json
+++ b/homeassistant/components/almond/strings.json
@@ -5,8 +5,8 @@
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
},
"hassio_confirm": {
- "title": "Almond via Hass.io add-on",
- "description": "Do you want to configure Home Assistant to connect to Almond provided by the Hass.io add-on: {addon}?"
+ "title": "Almond via Home Assistant add-on",
+ "description": "Do you want to configure Home Assistant to connect to Almond provided by the add-on: {addon}?"
}
},
"abort": {
diff --git a/homeassistant/components/almond/translations/ca.json b/homeassistant/components/almond/translations/ca.json
index 8ba96e603f5475..c4dcc2e38e2c40 100644
--- a/homeassistant/components/almond/translations/ca.json
+++ b/homeassistant/components/almond/translations/ca.json
@@ -8,8 +8,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb Almond proporcionat pel complement de Hass.io: {addon}?",
- "title": "Almond (complement de Hass.io)"
+ "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb Almond proporcionat pel complement: {addon}?",
+ "title": "Almond via complement de Home Assistant"
},
"pick_implementation": {
"title": "Selecciona el m\u00e8tode d'autenticaci\u00f3"
diff --git a/homeassistant/components/almond/translations/cs.json b/homeassistant/components/almond/translations/cs.json
index 1c667c9d55ea26..dc981403ad23ad 100644
--- a/homeassistant/components/almond/translations/cs.json
+++ b/homeassistant/components/almond/translations/cs.json
@@ -8,8 +8,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k Almond pomoc\u00ed hass.io {addon}?",
- "title": "Almond prost\u0159ednictv\u00edm dopl\u0148ku Hass.io"
+ "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k Almond pomoc\u00ed Supervisor {addon}?",
+ "title": "Almond prost\u0159ednictv\u00edm dopl\u0148ku Supervisor"
},
"pick_implementation": {
"title": "Vyberte metodu ov\u011b\u0159en\u00ed"
diff --git a/homeassistant/components/almond/translations/da.json b/homeassistant/components/almond/translations/da.json
index 37c66ea8efd28a..0e7a804acc6f65 100644
--- a/homeassistant/components/almond/translations/da.json
+++ b/homeassistant/components/almond/translations/da.json
@@ -6,8 +6,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til Almond leveret af Hass.io-tilf\u00f8jelsen: {addon}?",
- "title": "Almond via Hass.io-tilf\u00f8jelse"
+ "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til Almond leveret af Supervisor-tilf\u00f8jelsen: {addon}?",
+ "title": "Almond via Supervisor-tilf\u00f8jelse"
},
"pick_implementation": {
"title": "V\u00e6lg godkendelsesmetode"
diff --git a/homeassistant/components/almond/translations/de.json b/homeassistant/components/almond/translations/de.json
index e3a61026774b85..1f69b1c09e4197 100644
--- a/homeassistant/components/almond/translations/de.json
+++ b/homeassistant/components/almond/translations/de.json
@@ -1,13 +1,15 @@
{
"config": {
"abort": {
- "cannot_connect": "Verbindung zum Almond-Server nicht m\u00f6glich.",
- "missing_configuration": "Bitte \u00fcberpr\u00fcfe die Dokumentation zur Einrichtung von Almond."
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.",
+ "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).",
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"step": {
"hassio_confirm": {
- "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit Almond als Hass.io-Add-On hergestellt wird: {addon}?",
- "title": "Almond \u00fcber das Hass.io Add-on"
+ "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit Almond als Supervisor-Add-On hergestellt wird: {addon}?",
+ "title": "Almond \u00fcber das Supervisor Add-on"
},
"pick_implementation": {
"title": "W\u00e4hle die Authentifizierungsmethode"
diff --git a/homeassistant/components/almond/translations/en.json b/homeassistant/components/almond/translations/en.json
index b7f76e8933b1ca..fb7d41273524fc 100644
--- a/homeassistant/components/almond/translations/en.json
+++ b/homeassistant/components/almond/translations/en.json
@@ -8,8 +8,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Do you want to configure Home Assistant to connect to Almond provided by the Hass.io add-on: {addon}?",
- "title": "Almond via Hass.io add-on"
+ "description": "Do you want to configure Home Assistant to connect to Almond provided by the add-on: {addon}?",
+ "title": "Almond via Home Assistant add-on"
},
"pick_implementation": {
"title": "Pick Authentication Method"
diff --git a/homeassistant/components/almond/translations/es-419.json b/homeassistant/components/almond/translations/es-419.json
index 50a43d67b6d95c..ce1d655d69e7fa 100644
--- a/homeassistant/components/almond/translations/es-419.json
+++ b/homeassistant/components/almond/translations/es-419.json
@@ -6,8 +6,8 @@
},
"step": {
"hassio_confirm": {
- "description": "\u00bfDesea configurar Home Assistant para conectarse a Almond proporcionado por el complemento Hass.io: {addon}?",
- "title": "Almond a trav\u00e9s del complemento Hass.io"
+ "description": "\u00bfDesea configurar Home Assistant para conectarse a Almond proporcionado por el complemento Supervisor: {addon}?",
+ "title": "Almond a trav\u00e9s del complemento Supervisor"
},
"pick_implementation": {
"title": "Seleccione el m\u00e9todo de autenticaci\u00f3n"
diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json
index f14d3cd04ee397..4dc5e4ee1c0dcd 100644
--- a/homeassistant/components/almond/translations/es.json
+++ b/homeassistant/components/almond/translations/es.json
@@ -8,8 +8,8 @@
},
"step": {
"hassio_confirm": {
- "description": "\u00bfDesea configurar Home Assistant para conectarse a Almond proporcionado por el complemento Hass.io: {addon} ?",
- "title": "Almond a trav\u00e9s del complemento Hass.io"
+ "description": "\u00bfDesea configurar Home Assistant para conectarse a Almond proporcionado por el complemento Supervisor: {addon} ?",
+ "title": "Almond a trav\u00e9s del complemento Supervisor"
},
"pick_implementation": {
"title": "Seleccione el m\u00e9todo de autenticaci\u00f3n"
diff --git a/homeassistant/components/almond/translations/et.json b/homeassistant/components/almond/translations/et.json
index 55ab29b2a819d0..5b15d9328cce77 100644
--- a/homeassistant/components/almond/translations/et.json
+++ b/homeassistant/components/almond/translations/et.json
@@ -8,8 +8,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Kas soovid seadistada Home Assistant-i \u00fchendust Almondiga mida pakub Hass.io lisandmoodul: {addon} ?",
- "title": "Almond Hass.io lisandmooduli kaudu"
+ "description": "Kas soovid seadistada Home Assistant-i \u00fchendust Almondiga mida pakub lisandmoodul: {addon} ?",
+ "title": "Almond Home Assistanti lisandmooduli abil"
},
"pick_implementation": {
"title": "Vali tuvastusmeetod"
diff --git a/homeassistant/components/almond/translations/fr.json b/homeassistant/components/almond/translations/fr.json
index e4fb8610cd0f18..0e6a8e0be3f6c6 100644
--- a/homeassistant/components/almond/translations/fr.json
+++ b/homeassistant/components/almond/translations/fr.json
@@ -8,8 +8,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Voulez-vous configurer Home Assistant pour se connecter \u00e0 Almond fourni par le module compl\u00e9mentaire Hass.io: {addon} ?",
- "title": "Almonf via le module compl\u00e9mentaire Hass.io"
+ "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 Almond fourni par le module compl\u00e9mentaire Hass.io: {addon} ?",
+ "title": "Amande via le module compl\u00e9mentaire Hass.io"
},
"pick_implementation": {
"title": "S\u00e9lectionner une m\u00e9thode d'authentification"
diff --git a/homeassistant/components/almond/translations/hu.json b/homeassistant/components/almond/translations/hu.json
index 7654f66bc28812..568cd7270de260 100644
--- a/homeassistant/components/almond/translations/hu.json
+++ b/homeassistant/components/almond/translations/hu.json
@@ -1,16 +1,18 @@
{
"config": {
"abort": {
- "cannot_connect": "Nem lehet csatlakozni az Almond szerverhez.",
- "missing_configuration": "K\u00e9rj\u00fck, ellen\u0151rizze az Almond be\u00e1ll\u00edt\u00e1s\u00e1nak dokument\u00e1ci\u00f3j\u00e1t."
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.",
+ "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
},
"step": {
"hassio_confirm": {
- "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant alkalmaz\u00e1st az Almondhoz val\u00f3 csatlakoz\u00e1shoz, amelyet a Hass.io kieg\u00e9sz\u00edt\u0151 biztos\u00edt: {addon} ?",
- "title": "Almond a Hass.io kieg\u00e9sz\u00edt\u0151n kereszt\u00fcl"
+ "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant alkalmaz\u00e1st az Almondhoz val\u00f3 csatlakoz\u00e1shoz, amelyet a Supervisor kieg\u00e9sz\u00edt\u0151 biztos\u00edt: {addon} ?",
+ "title": "Almond a Supervisor kieg\u00e9sz\u00edt\u0151n kereszt\u00fcl"
},
"pick_implementation": {
- "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert"
+ "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert"
}
}
}
diff --git a/homeassistant/components/almond/translations/id.json b/homeassistant/components/almond/translations/id.json
new file mode 100644
index 00000000000000..21a627132c4723
--- /dev/null
+++ b/homeassistant/components/almond/translations/id.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Gagal terhubung",
+ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.",
+ "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "step": {
+ "hassio_confirm": {
+ "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke Almond yang disediakan oleh add-on Supervisor {addon}?",
+ "title": "Almond melalui add-on Home Assistant"
+ },
+ "pick_implementation": {
+ "title": "Pilih Metode Autentikasi"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/almond/translations/it.json b/homeassistant/components/almond/translations/it.json
index ab5d7f86f196c1..58eadad0d803d8 100644
--- a/homeassistant/components/almond/translations/it.json
+++ b/homeassistant/components/almond/translations/it.json
@@ -8,8 +8,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Vuoi configurare Home Assistant a connettersi ad Almond tramite il componente aggiuntivo Hass.io: {addon} ?",
- "title": "Almond tramite il componente aggiuntivo di Hass.io"
+ "description": "Vuoi configurare Home Assistant per connettersi a Almond fornito dal componente aggiuntivo: {addon}?",
+ "title": "Almond tramite il componente aggiuntivo di Home Assistant"
},
"pick_implementation": {
"title": "Scegli il metodo di autenticazione"
diff --git a/homeassistant/components/almond/translations/ko.json b/homeassistant/components/almond/translations/ko.json
index eff796699e3578..d18f5c914cccb4 100644
--- a/homeassistant/components/almond/translations/ko.json
+++ b/homeassistant/components/almond/translations/ko.json
@@ -1,14 +1,15 @@
{
"config": {
"abort": {
- "cannot_connect": "Almond \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
- "missing_configuration": "Almond \uc124\uc815 \ubc29\ubc95\uc5d0 \ub300\ud55c \uc124\uba85\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694.",
- "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})"
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
+ "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"step": {
"hassio_confirm": {
- "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c Almond \uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
- "title": "Hass.io \uc560\ub4dc\uc628\uc758 Almond"
+ "description": "{addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 Almond\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "Home Assistant \uc560\ub4dc\uc628\uc758 Almond"
},
"pick_implementation": {
"title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30"
diff --git a/homeassistant/components/almond/translations/lb.json b/homeassistant/components/almond/translations/lb.json
index 5a59645cdaa222..0e29d69bbed628 100644
--- a/homeassistant/components/almond/translations/lb.json
+++ b/homeassistant/components/almond/translations/lb.json
@@ -8,8 +8,8 @@
},
"step": {
"hassio_confirm": {
- "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam Almond ze verbannen dee vun der hass.io Erweiderung {addon} bereet gestallt g\u00ebtt?",
- "title": "Almond via Hass.io Erweiderung"
+ "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam Almond ze verbannen dee vun der Supervisor Erweiderung {addon} bereet gestallt g\u00ebtt?",
+ "title": "Almond via Supervisor Erweiderung"
},
"pick_implementation": {
"title": "Wiel Authentifikatiouns Method aus"
diff --git a/homeassistant/components/almond/translations/nl.json b/homeassistant/components/almond/translations/nl.json
index 26bbc8dea876a3..dbf4c485d345d0 100644
--- a/homeassistant/components/almond/translations/nl.json
+++ b/homeassistant/components/almond/translations/nl.json
@@ -1,18 +1,18 @@
{
"config": {
"abort": {
- "cannot_connect": "Kan geen verbinding maken met de Almond-server.",
- "missing_configuration": "Raadpleeg de documentatie over het instellen van Almond.",
+ "cannot_connect": "Kan geen verbinding maken",
+ "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.",
"no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})",
"single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk."
},
"step": {
"hassio_confirm": {
- "description": "Wilt u Home Assistant configureren om verbinding te maken met Almond die wordt aangeboden door de hass.io add-on {addon} ?",
- "title": "Almond via Hass.io add-on"
+ "description": "Wilt u Home Assistant configureren om verbinding te maken met Almond die wordt aangeboden door de add-on {addon} ?",
+ "title": "Almond via Home Assistant add-on"
},
"pick_implementation": {
- "title": "Kies de authenticatie methode"
+ "title": "Kies een authenticatie methode"
}
}
}
diff --git a/homeassistant/components/almond/translations/no.json b/homeassistant/components/almond/translations/no.json
index 1b0f03b80182a5..098184ff7af407 100644
--- a/homeassistant/components/almond/translations/no.json
+++ b/homeassistant/components/almond/translations/no.json
@@ -8,8 +8,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Hass.io tillegget: {addon}?",
- "title": "Almond via Hass.io tillegg"
+ "description": "Vil du konfigurere Home Assistant for \u00e5 koble til Almond levert av tillegget: {addon} ?",
+ "title": "Almond via Home Assistant-tillegg"
},
"pick_implementation": {
"title": "Velg godkjenningsmetode"
diff --git a/homeassistant/components/almond/translations/pl.json b/homeassistant/components/almond/translations/pl.json
index 110ab5a6a397f0..88fd6cda01c8dc 100644
--- a/homeassistant/components/almond/translations/pl.json
+++ b/homeassistant/components/almond/translations/pl.json
@@ -8,8 +8,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby \u0142\u0105czy\u0142 si\u0119 z Almond dostarczonym przez dodatek Hass.io: {addon}?",
- "title": "Almond poprzez dodatek Hass.io"
+ "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby \u0142\u0105czy\u0142 si\u0119 z Almond dostarczonym przez dodatek {addon}?",
+ "title": "Almond poprzez dodatek Home Assistant"
},
"pick_implementation": {
"title": "Wybierz metod\u0119 uwierzytelniania"
diff --git a/homeassistant/components/almond/translations/ru.json b/homeassistant/components/almond/translations/ru.json
index 27870a46e95ec7..62b5df122a1764 100644
--- a/homeassistant/components/almond/translations/ru.json
+++ b/homeassistant/components/almond/translations/ru.json
@@ -8,8 +8,8 @@
},
"step": {
"hassio_confirm": {
- "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?",
- "title": "Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)"
+ "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?",
+ "title": "Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)"
},
"pick_implementation": {
"title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438"
diff --git a/homeassistant/components/almond/translations/sl.json b/homeassistant/components/almond/translations/sl.json
index 573df43876f151..cb20393201f1dc 100644
--- a/homeassistant/components/almond/translations/sl.json
+++ b/homeassistant/components/almond/translations/sl.json
@@ -6,8 +6,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo z Almondom, ki ga ponuja dodatek Hass.io: {addon} ?",
- "title": "Almond prek dodatka Hass.io"
+ "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo z Almondom, ki ga ponuja dodatek Supervisor: {addon} ?",
+ "title": "Almond prek dodatka Supervisor"
},
"pick_implementation": {
"title": "Izberite na\u010din preverjanja pristnosti"
diff --git a/homeassistant/components/almond/translations/sv.json b/homeassistant/components/almond/translations/sv.json
index 6a7dfdb970c07e..8b20512df9b3f6 100644
--- a/homeassistant/components/almond/translations/sv.json
+++ b/homeassistant/components/almond/translations/sv.json
@@ -6,8 +6,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till Almond som tillhandah\u00e5lls av Hass.io-till\u00e4gget: {addon} ?",
- "title": "Almond via Hass.io-till\u00e4gget"
+ "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till Almond som tillhandah\u00e5lls av Supervisor-till\u00e4gget: {addon} ?",
+ "title": "Almond via Supervisor-till\u00e4gget"
},
"pick_implementation": {
"title": "V\u00e4lj autentiseringsmetod"
diff --git a/homeassistant/components/almond/translations/tr.json b/homeassistant/components/almond/translations/tr.json
new file mode 100644
index 00000000000000..dc270099fcd292
--- /dev/null
+++ b/homeassistant/components/almond/translations/tr.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/almond/translations/uk.json b/homeassistant/components/almond/translations/uk.json
new file mode 100644
index 00000000000000..db96ef3d0a3aec
--- /dev/null
+++ b/homeassistant/components/almond/translations/uk.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.",
+ "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "step": {
+ "hassio_confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Almond (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor \"{addon}\")?",
+ "title": "Almond (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor)"
+ },
+ "pick_implementation": {
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/almond/translations/zh-Hant.json b/homeassistant/components/almond/translations/zh-Hant.json
index 6312d4ecd1895d..9606a440aab9ae 100644
--- a/homeassistant/components/almond/translations/zh-Hant.json
+++ b/homeassistant/components/almond/translations/zh-Hant.json
@@ -8,8 +8,8 @@
},
"step": {
"hassio_confirm": {
- "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6\uff1a{addon} \u9023\u7dda\u81f3 Almond\uff1f",
- "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6 Almond"
+ "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Almond\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f",
+ "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 Almond"
},
"pick_implementation": {
"title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f"
diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py
index 0d0aec47915e9f..0788772a45b58a 100644
--- a/homeassistant/components/alpha_vantage/sensor.py
+++ b/homeassistant/components/alpha_vantage/sensor.py
@@ -6,10 +6,9 @@
from alpha_vantage.timeseries import TimeSeries
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -105,7 +104,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_LOGGER.debug("Setup completed")
-class AlphaVantageSensor(Entity):
+class AlphaVantageSensor(SensorEntity):
"""Representation of a Alpha Vantage sensor."""
def __init__(self, timeseries, symbol):
@@ -133,7 +132,7 @@ def state(self):
return self.values["1. open"]
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self.values is not None:
return {
@@ -156,7 +155,7 @@ def update(self):
_LOGGER.debug("Received new values for symbol %s", self._symbol)
-class AlphaVantageForeignExchange(Entity):
+class AlphaVantageForeignExchange(SensorEntity):
"""Sensor for foreign exchange rates."""
def __init__(self, foreign_exchange, config):
@@ -193,7 +192,7 @@ def icon(self):
return self._icon
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self.values is not None:
return {
diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py
index fb9560832ca552..bdb46abda9a1b4 100644
--- a/homeassistant/components/amazon_polly/tts.py
+++ b/homeassistant/components/amazon_polly/tts.py
@@ -6,6 +6,7 @@
import voluptuous as vol
from homeassistant.components.tts import PLATFORM_SCHEMA, Provider
+from homeassistant.const import ATTR_CREDENTIALS, CONF_PROFILE_NAME
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -13,8 +14,6 @@
CONF_REGION = "region_name"
CONF_ACCESS_KEY_ID = "aws_access_key_id"
CONF_SECRET_ACCESS_KEY = "aws_secret_access_key"
-CONF_PROFILE_NAME = "profile_name"
-ATTR_CREDENTIALS = "credentials"
DEFAULT_REGION = "us-east-1"
SUPPORTED_REGIONS = [
diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py
index 8550497148912a..a6dbe60a761376 100644
--- a/homeassistant/components/ambiclimate/config_flow.py
+++ b/homeassistant/components/ambiclimate/config_flow.py
@@ -38,7 +38,7 @@ def register_flow_implementation(hass, client_id, client_secret):
}
-@config_entries.HANDLERS.register("ambiclimate")
+@config_entries.HANDLERS.register(DOMAIN)
class AmbiclimateFlowHandler(config_entries.ConfigFlow):
"""Handle a config flow."""
diff --git a/homeassistant/components/ambiclimate/strings.json b/homeassistant/components/ambiclimate/strings.json
index b08b8919da2dcf..c51c25a2f61cbe 100644
--- a/homeassistant/components/ambiclimate/strings.json
+++ b/homeassistant/components/ambiclimate/strings.json
@@ -3,7 +3,7 @@
"step": {
"auth": {
"title": "Authenticate Ambiclimate",
- "description": "Please follow this [link]({authorization_url}) and **Allow** access to your Ambiclimate account, then come back and press **Submit** below.\n(Make sure the specified callback url is {cb_url})"
+ "description": "Please follow this [link]({authorization_url}) and **Allow** access to your Ambiclimate account, then come back and press **Submit** below.\n(Make sure the specified callback URL is {cb_url})"
}
},
"create_entry": {
diff --git a/homeassistant/components/ambiclimate/translations/ca.json b/homeassistant/components/ambiclimate/translations/ca.json
index b635d877ffe90c..8e54a222217516 100644
--- a/homeassistant/components/ambiclimate/translations/ca.json
+++ b/homeassistant/components/ambiclimate/translations/ca.json
@@ -14,7 +14,7 @@
},
"step": {
"auth": {
- "description": "V\u00e9s a l'[enlla\u00e7]({authorization_url}) i **Permet** l'acc\u00e9s al teu compte de Ambiclimate, despr\u00e9s torna i prem **Envia** (a sota).\n(Assegura't que l'enlla\u00e7 de retorn \u00e9s el seg\u00fcent {cb_url})",
+ "description": "V\u00e9s a l'[enlla\u00e7]({authorization_url}) i **Permet** l'acc\u00e9s al teu compte de Ambiclimate, despr\u00e9s torna i prem **Envia** a sota.\n(Assegura't que l'enlla\u00e7 de retorn \u00e9s el seg\u00fcent {cb_url})",
"title": "Autenticaci\u00f3 amb Ambi Climate"
}
}
diff --git a/homeassistant/components/ambiclimate/translations/de.json b/homeassistant/components/ambiclimate/translations/de.json
index e5988f761037a8..d91fc15f37d9e6 100644
--- a/homeassistant/components/ambiclimate/translations/de.json
+++ b/homeassistant/components/ambiclimate/translations/de.json
@@ -1,7 +1,9 @@
{
"config": {
"abort": {
- "access_token": "Unbekannter Fehler beim Generieren eines Zugriffstokens."
+ "access_token": "Unbekannter Fehler beim Generieren eines Zugriffstokens.",
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen."
},
"create_entry": {
"default": "Erfolgreiche Authentifizierung mit Ambiclimate"
diff --git a/homeassistant/components/ambiclimate/translations/en.json b/homeassistant/components/ambiclimate/translations/en.json
index 01c52875250f63..8621b0e247c7ab 100644
--- a/homeassistant/components/ambiclimate/translations/en.json
+++ b/homeassistant/components/ambiclimate/translations/en.json
@@ -14,7 +14,7 @@
},
"step": {
"auth": {
- "description": "Please follow this [link]({authorization_url}) and **Allow** access to your Ambiclimate account, then come back and press **Submit** below.\n(Make sure the specified callback url is {cb_url})",
+ "description": "Please follow this [link]({authorization_url}) and **Allow** access to your Ambiclimate account, then come back and press **Submit** below.\n(Make sure the specified callback URL is {cb_url})",
"title": "Authenticate Ambiclimate"
}
}
diff --git a/homeassistant/components/ambiclimate/translations/et.json b/homeassistant/components/ambiclimate/translations/et.json
index f9da8f8f7cd910..ff2264c3e0ebf3 100644
--- a/homeassistant/components/ambiclimate/translations/et.json
+++ b/homeassistant/components/ambiclimate/translations/et.json
@@ -9,7 +9,7 @@
"default": "Ambiclimate autentimine \u00f5nnestus"
},
"error": {
- "follow_link": "Enne Esita nupu vajutamist j\u00e4rgige linki ja autentige",
+ "follow_link": "Enne Esita nupu vajutamist j\u00e4rgi linki ja autendi",
"no_token": "Ambiclimate ei ole autenditud"
},
"step": {
diff --git a/homeassistant/components/ambiclimate/translations/fr.json b/homeassistant/components/ambiclimate/translations/fr.json
index bdbfaea20efbe3..37ef95496860de 100644
--- a/homeassistant/components/ambiclimate/translations/fr.json
+++ b/homeassistant/components/ambiclimate/translations/fr.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"access_token": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'un jeton d'acc\u00e8s.",
- "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9",
+ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9",
"missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation."
},
"create_entry": {
diff --git a/homeassistant/components/ambiclimate/translations/hu.json b/homeassistant/components/ambiclimate/translations/hu.json
index 19f706be1c8180..04035f04ccae6a 100644
--- a/homeassistant/components/ambiclimate/translations/hu.json
+++ b/homeassistant/components/ambiclimate/translations/hu.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t."
+ },
"create_entry": {
- "default": "Sikeres autentik\u00e1ci\u00f3"
+ "default": "Sikeres hiteles\u00edt\u00e9s"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ambiclimate/translations/id.json b/homeassistant/components/ambiclimate/translations/id.json
new file mode 100644
index 00000000000000..66c30afcb09386
--- /dev/null
+++ b/homeassistant/components/ambiclimate/translations/id.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "access_token": "Terjadi kesalahan yang tidak diketahui saat membuat token akses.",
+ "already_configured": "Akun sudah dikonfigurasi",
+ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi."
+ },
+ "create_entry": {
+ "default": "Berhasil diautentikasi"
+ },
+ "error": {
+ "follow_link": "Buka tautan dan autentikasi sebelum menekan Kirim",
+ "no_token": "Tidak diautentikasi dengan Ambiclimate"
+ },
+ "step": {
+ "auth": {
+ "description": "Buka [tautan ini] ({authorization_url}) dan **Izinkan** akses ke akun Ambiclimate Anda, lalu kembali dan tekan **Kirim** di bawah ini.\n(Pastikan URL panggil balik yang ditentukan adalah {cb_url})",
+ "title": "Autentikasi Ambiclimate"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambiclimate/translations/ko.json b/homeassistant/components/ambiclimate/translations/ko.json
index 2a5e9280aa722c..e47c5041728522 100644
--- a/homeassistant/components/ambiclimate/translations/ko.json
+++ b/homeassistant/components/ambiclimate/translations/ko.json
@@ -1,18 +1,20 @@
{
"config": {
"abort": {
- "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070 \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4."
+ "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070 \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694."
},
"create_entry": {
- "default": "Ambi Climate \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
"follow_link": "\ud655\uc778\uc744 \ud074\ub9ad\ud558\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694",
- "no_token": "Ambi Climate \ub85c \uc778\uc99d\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4"
+ "no_token": "Ambi Climate\ub85c \uc778\uc99d\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4"
},
"step": {
"auth": {
- "description": "[\ub9c1\ud06c]({authorization_url}) \ub97c \ud074\ub9ad\ud558\uc5ec Ambi Climate \uacc4\uc815\uc5d0 \ub300\ud574 **\ud5c8\uc6a9**\ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 **\ud655\uc778**\uc744 \ud074\ub9ad\ud574\uc8fc\uc138\uc694. \n(\ucf5c\ubc31 url \uc744 {cb_url} \ub85c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694)",
+ "description": "[\ub9c1\ud06c]({authorization_url})(\uc744)\ub97c \ud074\ub9ad\ud558\uc5ec Ambiclimate \uacc4\uc815\uc5d0 \ub300\ud574 **\ud5c8\uc6a9**\ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 **\ud655\uc778**\uc744 \ud074\ub9ad\ud574\uc8fc\uc138\uc694.\n(\ucf5c\ubc31 URL\uc774 {cb_url}(\uc73c)\ub85c \uc9c0\uc815\ub418\uc5c8\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694)",
"title": "Ambi Climate \uc778\uc99d\ud558\uae30"
}
}
diff --git a/homeassistant/components/ambiclimate/translations/nl.json b/homeassistant/components/ambiclimate/translations/nl.json
index 52f8cfc40d37e4..4e6c5ebb202d78 100644
--- a/homeassistant/components/ambiclimate/translations/nl.json
+++ b/homeassistant/components/ambiclimate/translations/nl.json
@@ -2,10 +2,11 @@
"config": {
"abort": {
"access_token": "Onbekende fout bij het genereren van een toegangstoken.",
- "already_configured": "Account is al geconfigureerd"
+ "already_configured": "Account is al geconfigureerd",
+ "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen."
},
"create_entry": {
- "default": "Succesvol geverifieerd met Ambiclimate"
+ "default": "Succesvol geauthenticeerd"
},
"error": {
"follow_link": "Gelieve de link te volgen en te verifi\u00ebren voordat u op Verzenden drukt.",
diff --git a/homeassistant/components/ambiclimate/translations/no.json b/homeassistant/components/ambiclimate/translations/no.json
index c39aa7637f8b57..6feaabadacc5b2 100644
--- a/homeassistant/components/ambiclimate/translations/no.json
+++ b/homeassistant/components/ambiclimate/translations/no.json
@@ -14,7 +14,7 @@
},
"step": {
"auth": {
- "description": "Vennligst f\u00f8lg denne [linken]({authorization_url}) og **Tillat** tilgang til din Ambiclimate konto, kom deretter tilbake og trykk **Send** nedenfor.\n(Kontroller at den angitte URL-adressen for tilbakeringing er {cb_url})",
+ "description": "F\u00f8lg denne [linken]({authorization_url}) og **Tillat** tilgang til Ambiclimate-kontoen din, og kom deretter tilbake og trykk **Send** nedenfor.\n(Kontroller at den angitte url-adressen for tilbakeringing er {cb_url})",
"title": "Godkjenn Ambiclimate"
}
}
diff --git a/homeassistant/components/ambiclimate/translations/ru.json b/homeassistant/components/ambiclimate/translations/ru.json
index 8c8863c0eec34f..a1948c45d0f677 100644
--- a/homeassistant/components/ambiclimate/translations/ru.json
+++ b/homeassistant/components/ambiclimate/translations/ru.json
@@ -14,7 +14,7 @@
},
"step": {
"auth": {
- "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 **\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435** \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Ambi Climate, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**. \n(\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 {cb_url})",
+ "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 **\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435** \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Ambi Climate, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**. \n(\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441 \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 {cb_url})",
"title": "Ambi Climate"
}
}
diff --git a/homeassistant/components/ambiclimate/translations/tr.json b/homeassistant/components/ambiclimate/translations/tr.json
new file mode 100644
index 00000000000000..bcaeba84558752
--- /dev/null
+++ b/homeassistant/components/ambiclimate/translations/tr.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambiclimate/translations/uk.json b/homeassistant/components/ambiclimate/translations/uk.json
new file mode 100644
index 00000000000000..398665ab667e3a
--- /dev/null
+++ b/homeassistant/components/ambiclimate/translations/uk.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "access_token": "\u041f\u0440\u0438 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u0456 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0441\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430.",
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438."
+ },
+ "create_entry": {
+ "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e."
+ },
+ "error": {
+ "follow_link": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c \u0456 \u043f\u0440\u043e\u0439\u0434\u0456\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e, \u043f\u0435\u0440\u0448 \u043d\u0456\u0436 \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0438 \"\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438\".",
+ "no_token": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043d\u0435 \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430."
+ },
+ "step": {
+ "auth": {
+ "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043f\u043e [\u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c]({authorization_url}) \u0456 ** \u0414\u043e\u0437\u0432\u043e\u043b\u044c\u0442\u0435 ** \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0432\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Ambi Climate, \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0435\u0440\u043d\u0456\u0442\u044c\u0441\u044f \u0441\u044e\u0434\u0438 \u0456 \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **.\n(\u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u0439 URL \u0437\u0432\u043e\u0440\u043e\u0442\u043d\u043e\u0433\u043e \u0432\u0438\u043a\u043b\u0438\u043a\u0443 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0454 {cb_url} )",
+ "title": "Ambi Climate"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambiclimate/translations/zh-Hant.json b/homeassistant/components/ambiclimate/translations/zh-Hant.json
index f91c38dd36e6f7..e50accd732763e 100644
--- a/homeassistant/components/ambiclimate/translations/zh-Hant.json
+++ b/homeassistant/components/ambiclimate/translations/zh-Hant.json
@@ -14,7 +14,7 @@
},
"step": {
"auth": {
- "description": "\u8acb\u4f7f\u7528\u6b64[\u9023\u7d50]\uff08{authorization_url}\uff09\u4e26\u9ede\u9078 **\u5141\u8a31** \u4ee5\u5b58\u53d6 Ambiclimate \u5e33\u865f\uff0c\u7136\u5f8c\u8fd4\u56de\u6b64\u9801\u9762\u4e26\u9ede\u9078\u4e0b\u65b9\u7684 **\u50b3\u9001**\u3002\n\uff08\u78ba\u5b9a Callback url \u70ba {cb_url}\uff09",
+ "description": "\u8acb\u4f7f\u7528\u6b64 [\u9023\u7d50]\uff08{authorization_url}\uff09\u4e26\u9ede\u9078**\u5141\u8a31**\u4ee5\u5b58\u53d6 Ambiclimate \u5e33\u865f\uff0c\u7136\u5f8c\u8fd4\u56de\u6b64\u9801\u9762\u4e26\u9ede\u9078\u4e0b\u65b9\u7684**\u50b3\u9001**\u3002\n\uff08\u78ba\u5b9a\u6307\u5b9a Callback URL \u70ba {cb_url}\uff09",
"title": "\u8a8d\u8b49 Ambiclimate"
}
}
diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py
index 4a5558c5963648..9d3359ca98183e 100644
--- a/homeassistant/components/ambient_station/__init__.py
+++ b/homeassistant/components/ambient_station/__init__.py
@@ -5,7 +5,11 @@
from aioambient.errors import WebsocketError
import voluptuous as vol
-from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
+ DOMAIN as BINARY_SENSOR,
+)
+from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_LOCATION,
@@ -14,6 +18,13 @@
CONCENTRATION_PARTS_PER_MILLION,
CONF_API_KEY,
DEGREE,
+ DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_CO2,
+ DEVICE_CLASS_HUMIDITY,
+ DEVICE_CLASS_ILLUMINANCE,
+ DEVICE_CLASS_PRESSURE,
+ DEVICE_CLASS_TEMPERATURE,
+ DEVICE_CLASS_TIMESTAMP,
EVENT_HOMEASSISTANT_STOP,
IRRADIATION_WATTS_PER_SQUARE_METER,
LIGHT_LUX,
@@ -39,10 +50,10 @@
DATA_CLIENT,
DOMAIN,
LOGGER,
- TYPE_BINARY_SENSOR,
- TYPE_SENSOR,
)
+PLATFORMS = [BINARY_SENSOR, SENSOR]
+
DATA_CONFIG = "config"
DEFAULT_SOCKET_MIN_RETRY = 15
@@ -60,6 +71,7 @@
TYPE_BATT7 = "batt7"
TYPE_BATT8 = "batt8"
TYPE_BATT9 = "batt9"
+TYPE_BATT_CO2 = "batt_co2"
TYPE_BATTOUT = "battout"
TYPE_CO2 = "co2"
TYPE_DAILYRAININ = "dailyrainin"
@@ -82,6 +94,12 @@
TYPE_LASTRAIN = "lastRain"
TYPE_MAXDAILYGUST = "maxdailygust"
TYPE_MONTHLYRAININ = "monthlyrainin"
+TYPE_PM25 = "pm25"
+TYPE_PM25_24H = "pm25_24h"
+TYPE_PM25_BATT = "batt_25"
+TYPE_PM25_IN = "pm25_in"
+TYPE_PM25_IN_24H = "pm25_in_24h"
+TYPE_PM25IN_BATT = "batt_25in"
TYPE_RELAY1 = "relay1"
TYPE_RELAY10 = "relay10"
TYPE_RELAY2 = "relay2"
@@ -128,8 +146,6 @@
TYPE_TEMPINF = "tempinf"
TYPE_TOTALRAININ = "totalrainin"
TYPE_UV = "uv"
-TYPE_PM25 = "pm25"
-TYPE_PM25_24H = "pm25_24h"
TYPE_WEEKLYRAININ = "weeklyrainin"
TYPE_WINDDIR = "winddir"
TYPE_WINDDIR_AVG10M = "winddir_avg10m"
@@ -141,109 +157,139 @@
TYPE_WINDSPEEDMPH = "windspeedmph"
TYPE_YEARLYRAININ = "yearlyrainin"
SENSOR_TYPES = {
- TYPE_24HOURRAININ: ("24 Hr Rain", "in", TYPE_SENSOR, None),
- TYPE_BAROMABSIN: ("Abs Pressure", PRESSURE_INHG, TYPE_SENSOR, "pressure"),
- TYPE_BAROMRELIN: ("Rel Pressure", PRESSURE_INHG, TYPE_SENSOR, "pressure"),
- TYPE_BATT10: ("Battery 10", None, TYPE_BINARY_SENSOR, "battery"),
- TYPE_BATT1: ("Battery 1", None, TYPE_BINARY_SENSOR, "battery"),
- TYPE_BATT2: ("Battery 2", None, TYPE_BINARY_SENSOR, "battery"),
- TYPE_BATT3: ("Battery 3", None, TYPE_BINARY_SENSOR, "battery"),
- TYPE_BATT4: ("Battery 4", None, TYPE_BINARY_SENSOR, "battery"),
- TYPE_BATT5: ("Battery 5", None, TYPE_BINARY_SENSOR, "battery"),
- TYPE_BATT6: ("Battery 6", None, TYPE_BINARY_SENSOR, "battery"),
- TYPE_BATT7: ("Battery 7", None, TYPE_BINARY_SENSOR, "battery"),
- TYPE_BATT8: ("Battery 8", None, TYPE_BINARY_SENSOR, "battery"),
- TYPE_BATT9: ("Battery 9", None, TYPE_BINARY_SENSOR, "battery"),
- TYPE_BATTOUT: ("Battery", None, TYPE_BINARY_SENSOR, "battery"),
- TYPE_CO2: ("co2", CONCENTRATION_PARTS_PER_MILLION, TYPE_SENSOR, None),
- TYPE_DAILYRAININ: ("Daily Rain", "in", TYPE_SENSOR, None),
- TYPE_DEWPOINT: ("Dew Point", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_EVENTRAININ: ("Event Rain", "in", TYPE_SENSOR, None),
- TYPE_FEELSLIKE: ("Feels Like", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_HOURLYRAININ: ("Hourly Rain Rate", "in/hr", TYPE_SENSOR, None),
- TYPE_HUMIDITY10: ("Humidity 10", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_HUMIDITY1: ("Humidity 1", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_HUMIDITY2: ("Humidity 2", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_HUMIDITY3: ("Humidity 3", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_HUMIDITY4: ("Humidity 4", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_HUMIDITY5: ("Humidity 5", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_HUMIDITY6: ("Humidity 6", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_HUMIDITY7: ("Humidity 7", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_HUMIDITY8: ("Humidity 8", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_HUMIDITY9: ("Humidity 9", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_HUMIDITY: ("Humidity", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_HUMIDITYIN: ("Humidity In", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_LASTRAIN: ("Last Rain", None, TYPE_SENSOR, "timestamp"),
- TYPE_MAXDAILYGUST: ("Max Gust", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None),
- TYPE_MONTHLYRAININ: ("Monthly Rain", "in", TYPE_SENSOR, None),
- TYPE_RELAY10: ("Relay 10", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
- TYPE_RELAY1: ("Relay 1", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
- TYPE_RELAY2: ("Relay 2", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
- TYPE_RELAY3: ("Relay 3", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
- TYPE_RELAY4: ("Relay 4", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
- TYPE_RELAY5: ("Relay 5", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
- TYPE_RELAY6: ("Relay 6", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
- TYPE_RELAY7: ("Relay 7", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
- TYPE_RELAY8: ("Relay 8", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
- TYPE_RELAY9: ("Relay 9", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
- TYPE_SOILHUM10: ("Soil Humidity 10", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_SOILHUM1: ("Soil Humidity 1", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_SOILHUM2: ("Soil Humidity 2", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_SOILHUM3: ("Soil Humidity 3", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_SOILHUM4: ("Soil Humidity 4", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_SOILHUM5: ("Soil Humidity 5", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_SOILHUM6: ("Soil Humidity 6", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_SOILHUM7: ("Soil Humidity 7", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_SOILHUM8: ("Soil Humidity 8", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_SOILHUM9: ("Soil Humidity 9", PERCENTAGE, TYPE_SENSOR, "humidity"),
- TYPE_SOILTEMP10F: ("Soil Temp 10", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_SOILTEMP1F: ("Soil Temp 1", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_SOILTEMP2F: ("Soil Temp 2", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_SOILTEMP3F: ("Soil Temp 3", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_SOILTEMP4F: ("Soil Temp 4", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_SOILTEMP5F: ("Soil Temp 5", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_SOILTEMP6F: ("Soil Temp 6", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_SOILTEMP7F: ("Soil Temp 7", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_SOILTEMP8F: ("Soil Temp 8", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_SOILTEMP9F: ("Soil Temp 9", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
+ TYPE_24HOURRAININ: ("24 Hr Rain", "in", SENSOR, None),
+ TYPE_BAROMABSIN: ("Abs Pressure", PRESSURE_INHG, SENSOR, DEVICE_CLASS_PRESSURE),
+ TYPE_BAROMRELIN: ("Rel Pressure", PRESSURE_INHG, SENSOR, DEVICE_CLASS_PRESSURE),
+ TYPE_BATT10: ("Battery 10", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
+ TYPE_BATT1: ("Battery 1", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
+ TYPE_BATT2: ("Battery 2", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
+ TYPE_BATT3: ("Battery 3", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
+ TYPE_BATT4: ("Battery 4", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
+ TYPE_BATT5: ("Battery 5", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
+ TYPE_BATT6: ("Battery 6", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
+ TYPE_BATT7: ("Battery 7", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
+ TYPE_BATT8: ("Battery 8", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
+ TYPE_BATT9: ("Battery 9", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
+ TYPE_BATTOUT: ("Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
+ TYPE_BATT_CO2: ("CO2 Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
+ TYPE_CO2: ("co2", CONCENTRATION_PARTS_PER_MILLION, SENSOR, DEVICE_CLASS_CO2),
+ TYPE_DAILYRAININ: ("Daily Rain", "in", SENSOR, None),
+ TYPE_DEWPOINT: ("Dew Point", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_EVENTRAININ: ("Event Rain", "in", SENSOR, None),
+ TYPE_FEELSLIKE: ("Feels Like", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_HOURLYRAININ: ("Hourly Rain Rate", "in/hr", SENSOR, None),
+ TYPE_HUMIDITY10: ("Humidity 10", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_HUMIDITY1: ("Humidity 1", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_HUMIDITY2: ("Humidity 2", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_HUMIDITY3: ("Humidity 3", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_HUMIDITY4: ("Humidity 4", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_HUMIDITY5: ("Humidity 5", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_HUMIDITY6: ("Humidity 6", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_HUMIDITY7: ("Humidity 7", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_HUMIDITY8: ("Humidity 8", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_HUMIDITY9: ("Humidity 9", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_HUMIDITY: ("Humidity", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_HUMIDITYIN: ("Humidity In", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_LASTRAIN: ("Last Rain", None, SENSOR, DEVICE_CLASS_TIMESTAMP),
+ TYPE_MAXDAILYGUST: ("Max Gust", SPEED_MILES_PER_HOUR, SENSOR, None),
+ TYPE_MONTHLYRAININ: ("Monthly Rain", "in", SENSOR, None),
+ TYPE_PM25_24H: (
+ "PM25 24h Avg",
+ CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ SENSOR,
+ None,
+ ),
+ TYPE_PM25_BATT: ("PM25 Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
+ TYPE_PM25_IN: (
+ "PM25 Indoor",
+ CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ SENSOR,
+ None,
+ ),
+ TYPE_PM25_IN_24H: (
+ "PM25 Indoor 24h Avg",
+ CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ SENSOR,
+ None,
+ ),
+ TYPE_PM25: ("PM25", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, SENSOR, None),
+ TYPE_PM25IN_BATT: (
+ "PM25 Indoor Battery",
+ None,
+ BINARY_SENSOR,
+ DEVICE_CLASS_BATTERY,
+ ),
+ TYPE_RELAY10: ("Relay 10", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY1: ("Relay 1", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY2: ("Relay 2", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY3: ("Relay 3", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY4: ("Relay 4", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY5: ("Relay 5", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY6: ("Relay 6", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY7: ("Relay 7", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY8: ("Relay 8", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_RELAY9: ("Relay 9", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
+ TYPE_SOILHUM10: ("Soil Humidity 10", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_SOILHUM1: ("Soil Humidity 1", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_SOILHUM2: ("Soil Humidity 2", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_SOILHUM3: ("Soil Humidity 3", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_SOILHUM4: ("Soil Humidity 4", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_SOILHUM5: ("Soil Humidity 5", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_SOILHUM6: ("Soil Humidity 6", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_SOILHUM7: ("Soil Humidity 7", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_SOILHUM8: ("Soil Humidity 8", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_SOILHUM9: ("Soil Humidity 9", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
+ TYPE_SOILTEMP10F: (
+ "Soil Temp 10",
+ TEMP_FAHRENHEIT,
+ SENSOR,
+ DEVICE_CLASS_TEMPERATURE,
+ ),
+ TYPE_SOILTEMP1F: ("Soil Temp 1", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_SOILTEMP2F: ("Soil Temp 2", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_SOILTEMP3F: ("Soil Temp 3", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_SOILTEMP4F: ("Soil Temp 4", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_SOILTEMP5F: ("Soil Temp 5", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_SOILTEMP6F: ("Soil Temp 6", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_SOILTEMP7F: ("Soil Temp 7", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_SOILTEMP8F: ("Soil Temp 8", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_SOILTEMP9F: ("Soil Temp 9", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
TYPE_SOLARRADIATION: (
"Solar Rad",
IRRADIATION_WATTS_PER_SQUARE_METER,
- TYPE_SENSOR,
+ SENSOR,
None,
),
- TYPE_SOLARRADIATION_LX: ("Solar Rad (lx)", LIGHT_LUX, TYPE_SENSOR, "illuminance"),
- TYPE_TEMP10F: ("Temp 10", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_TEMP1F: ("Temp 1", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_TEMP2F: ("Temp 2", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_TEMP3F: ("Temp 3", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_TEMP4F: ("Temp 4", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_TEMP5F: ("Temp 5", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_TEMP6F: ("Temp 6", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_TEMP7F: ("Temp 7", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_TEMP8F: ("Temp 8", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_TEMP9F: ("Temp 9", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_TEMPF: ("Temp", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_TEMPINF: ("Inside Temp", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
- TYPE_TOTALRAININ: ("Lifetime Rain", "in", TYPE_SENSOR, None),
- TYPE_UV: ("uv", "Index", TYPE_SENSOR, None),
- TYPE_PM25: ("PM25", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, TYPE_SENSOR, None),
- TYPE_PM25_24H: (
- "PM25 24h Avg",
- CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
- TYPE_SENSOR,
- None,
+ TYPE_SOLARRADIATION_LX: (
+ "Solar Rad (lx)",
+ LIGHT_LUX,
+ SENSOR,
+ DEVICE_CLASS_ILLUMINANCE,
),
- TYPE_WEEKLYRAININ: ("Weekly Rain", "in", TYPE_SENSOR, None),
- TYPE_WINDDIR: ("Wind Dir", DEGREE, TYPE_SENSOR, None),
- TYPE_WINDDIR_AVG10M: ("Wind Dir Avg 10m", DEGREE, TYPE_SENSOR, None),
- TYPE_WINDDIR_AVG2M: ("Wind Dir Avg 2m", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None),
- TYPE_WINDGUSTDIR: ("Gust Dir", DEGREE, TYPE_SENSOR, None),
- TYPE_WINDGUSTMPH: ("Wind Gust", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None),
- TYPE_WINDSPDMPH_AVG10M: ("Wind Avg 10m", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None),
- TYPE_WINDSPDMPH_AVG2M: ("Wind Avg 2m", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None),
- TYPE_WINDSPEEDMPH: ("Wind Speed", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None),
- TYPE_YEARLYRAININ: ("Yearly Rain", "in", TYPE_SENSOR, None),
+ TYPE_TEMP10F: ("Temp 10", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_TEMP1F: ("Temp 1", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_TEMP2F: ("Temp 2", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_TEMP3F: ("Temp 3", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_TEMP4F: ("Temp 4", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_TEMP5F: ("Temp 5", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_TEMP6F: ("Temp 6", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_TEMP7F: ("Temp 7", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_TEMP8F: ("Temp 8", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_TEMP9F: ("Temp 9", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_TEMPF: ("Temp", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_TEMPINF: ("Inside Temp", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
+ TYPE_TOTALRAININ: ("Lifetime Rain", "in", SENSOR, None),
+ TYPE_UV: ("uv", "Index", SENSOR, None),
+ TYPE_WEEKLYRAININ: ("Weekly Rain", "in", SENSOR, None),
+ TYPE_WINDDIR: ("Wind Dir", DEGREE, SENSOR, None),
+ TYPE_WINDDIR_AVG10M: ("Wind Dir Avg 10m", DEGREE, SENSOR, None),
+ TYPE_WINDDIR_AVG2M: ("Wind Dir Avg 2m", SPEED_MILES_PER_HOUR, SENSOR, None),
+ TYPE_WINDGUSTDIR: ("Gust Dir", DEGREE, SENSOR, None),
+ TYPE_WINDGUSTMPH: ("Wind Gust", SPEED_MILES_PER_HOUR, SENSOR, None),
+ TYPE_WINDSPDMPH_AVG10M: ("Wind Avg 10m", SPEED_MILES_PER_HOUR, SENSOR, None),
+ TYPE_WINDSPDMPH_AVG2M: ("Wind Avg 2m", SPEED_MILES_PER_HOUR, SENSOR, None),
+ TYPE_WINDSPEEDMPH: ("Wind Speed", SPEED_MILES_PER_HOUR, SENSOR, None),
+ TYPE_YEARLYRAININ: ("Yearly Rain", "in", SENSOR, None),
}
CONFIG_SCHEMA = vol.Schema(
@@ -260,13 +306,12 @@
async def async_setup(hass, config):
- """Set up the Ambient PWS component."""
+ """Set up the Ambient PWS integration."""
hass.data[DOMAIN] = {}
hass.data[DOMAIN][DATA_CLIENT] = {}
if DOMAIN not in config:
return True
-
conf = config[DOMAIN]
# Store config for use during entry setup:
@@ -289,7 +334,6 @@ async def async_setup_entry(hass, config_entry):
hass.config_entries.async_update_entry(
config_entry, unique_id=config_entry.data[CONF_APP_KEY]
)
-
session = aiohttp_client.async_get_clientsession(hass)
try:
@@ -322,8 +366,8 @@ async def async_unload_entry(hass, config_entry):
hass.async_create_task(ambient.ws_disconnect())
tasks = [
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in ("binary_sensor", "sensor")
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
await asyncio.gather(*tasks)
@@ -347,7 +391,6 @@ async def async_migrate_entry(hass, config_entry):
version = config_entry.version = 2
hass.config_entries.async_update_entry(config_entry)
-
LOGGER.info("Migration to version %s successful", version)
return True
@@ -405,7 +448,6 @@ def on_subscribed(data):
for station in data["devices"]:
if station["macAddress"] in self.stations:
continue
-
LOGGER.debug("New station subscription: %s", data)
# Only create entities based on the data coming through the socket.
@@ -416,7 +458,6 @@ def on_subscribed(data):
]
if TYPE_SOLARRADIATION in monitored_conditions:
monitored_conditions.append(TYPE_SOLARRADIATION_LX)
-
self.stations[station["macAddress"]] = {
ATTR_LAST_DATA: station["lastData"],
ATTR_LOCATION: station.get("info", {}).get("location"),
@@ -425,20 +466,18 @@ def on_subscribed(data):
"name", station["macAddress"]
),
}
-
# If the websocket disconnects and reconnects, the on_subscribed
# handler will get called again; in that case, we don't want to
# attempt forward setup of the config entry (because it will have
# already been done):
if not self._entry_setup_complete:
- for component in ("binary_sensor", "sensor"):
+ for platform in PLATFORMS:
self._hass.async_create_task(
self._hass.config_entries.async_forward_entry_setup(
- self._config_entry, component
+ self._config_entry, platform
)
)
self._entry_setup_complete = True
-
self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
self.client.websocket.on_connect(on_connect)
diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py
index 0a3a91e515c919..c2e5ad8b4f4cbb 100644
--- a/homeassistant/components/ambient_station/binary_sensor.py
+++ b/homeassistant/components/ambient_station/binary_sensor.py
@@ -1,5 +1,8 @@
"""Support for Ambient Weather Station binary sensors."""
-from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.components.binary_sensor import (
+ DOMAIN as BINARY_SENSOR,
+ BinarySensorEntity,
+)
from homeassistant.const import ATTR_NAME
from homeassistant.core import callback
@@ -15,16 +18,13 @@
TYPE_BATT8,
TYPE_BATT9,
TYPE_BATT10,
+ TYPE_BATT_CO2,
TYPE_BATTOUT,
+ TYPE_PM25_BATT,
+ TYPE_PM25IN_BATT,
AmbientWeatherEntity,
)
-from .const import (
- ATTR_LAST_DATA,
- ATTR_MONITORED_CONDITIONS,
- DATA_CLIENT,
- DOMAIN,
- TYPE_BINARY_SENSOR,
-)
+from .const import ATTR_LAST_DATA, ATTR_MONITORED_CONDITIONS, DATA_CLIENT, DOMAIN
async def async_setup_entry(hass, entry, async_add_entities):
@@ -35,7 +35,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
for mac_address, station in ambient.stations.items():
for condition in station[ATTR_MONITORED_CONDITIONS]:
name, _, kind, device_class = SENSOR_TYPES[condition]
- if kind == TYPE_BINARY_SENSOR:
+ if kind == BINARY_SENSOR:
binary_sensor_list.append(
AmbientWeatherBinarySensor(
ambient,
@@ -67,7 +67,10 @@ def is_on(self):
TYPE_BATT7,
TYPE_BATT8,
TYPE_BATT9,
+ TYPE_BATT_CO2,
TYPE_BATTOUT,
+ TYPE_PM25_BATT,
+ TYPE_PM25IN_BATT,
):
return self._state == 0
diff --git a/homeassistant/components/ambient_station/config_flow.py b/homeassistant/components/ambient_station/config_flow.py
index a4c0a6aa44f241..30548bbe31ba96 100644
--- a/homeassistant/components/ambient_station/config_flow.py
+++ b/homeassistant/components/ambient_station/config_flow.py
@@ -7,7 +7,7 @@
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import aiohttp_client
-from .const import CONF_APP_KEY, DOMAIN # pylint: disable=unused-import
+from .const import CONF_APP_KEY, DOMAIN
class AmbientStationFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/ambient_station/const.py b/homeassistant/components/ambient_station/const.py
index e59f926eac3f94..87b5ff61877527 100644
--- a/homeassistant/components/ambient_station/const.py
+++ b/homeassistant/components/ambient_station/const.py
@@ -10,6 +10,3 @@
CONF_APP_KEY = "app_key"
DATA_CLIENT = "data_client"
-
-TYPE_BINARY_SENSOR = "binary_sensor"
-TYPE_SENSOR = "sensor"
diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py
index 540e2facd4d34d..7c60d1da9bc7dc 100644
--- a/homeassistant/components/ambient_station/sensor.py
+++ b/homeassistant/components/ambient_station/sensor.py
@@ -1,4 +1,5 @@
"""Support for Ambient Weather Station sensors."""
+from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity
from homeassistant.const import ATTR_NAME
from homeassistant.core import callback
@@ -8,13 +9,7 @@
TYPE_SOLARRADIATION_LX,
AmbientWeatherEntity,
)
-from .const import (
- ATTR_LAST_DATA,
- ATTR_MONITORED_CONDITIONS,
- DATA_CLIENT,
- DOMAIN,
- TYPE_SENSOR,
-)
+from .const import ATTR_LAST_DATA, ATTR_MONITORED_CONDITIONS, DATA_CLIENT, DOMAIN
async def async_setup_entry(hass, entry, async_add_entities):
@@ -25,7 +20,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
for mac_address, station in ambient.stations.items():
for condition in station[ATTR_MONITORED_CONDITIONS]:
name, unit, kind, device_class = SENSOR_TYPES[condition]
- if kind == TYPE_SENSOR:
+ if kind == SENSOR:
sensor_list.append(
AmbientWeatherSensor(
ambient,
@@ -41,7 +36,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
async_add_entities(sensor_list, True)
-class AmbientWeatherSensor(AmbientWeatherEntity):
+class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity):
"""Define an Ambient sensor."""
def __init__(
diff --git a/homeassistant/components/ambient_station/translations/de.json b/homeassistant/components/ambient_station/translations/de.json
index 53e6b1f69d6100..c6570fee0e352f 100644
--- a/homeassistant/components/ambient_station/translations/de.json
+++ b/homeassistant/components/ambient_station/translations/de.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Dieser App-Schl\u00fcssel wird bereits verwendet."
+ "already_configured": "Der Dienst ist bereits konfiguriert"
},
"error": {
- "invalid_key": "Ung\u00fcltiger API Key und / oder Anwendungsschl\u00fcssel",
+ "invalid_key": "Ung\u00fcltiger API-Schl\u00fcssel",
"no_devices": "Keine Ger\u00e4te im Konto gefunden"
},
"step": {
diff --git a/homeassistant/components/ambient_station/translations/hu.json b/homeassistant/components/ambient_station/translations/hu.json
index e6b95634827da4..7c7e3a658b90ad 100644
--- a/homeassistant/components/ambient_station/translations/hu.json
+++ b/homeassistant/components/ambient_station/translations/hu.json
@@ -1,7 +1,10 @@
{
"config": {
+ "abort": {
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
- "invalid_key": "\u00c9rv\u00e9nytelen API kulcs \u00e9s / vagy alkalmaz\u00e1skulcs",
+ "invalid_key": "\u00c9rv\u00e9nytelen API kulcs",
"no_devices": "Nincs a fi\u00f3kodban tal\u00e1lhat\u00f3 eszk\u00f6z"
},
"step": {
diff --git a/homeassistant/components/ambient_station/translations/id.json b/homeassistant/components/ambient_station/translations/id.json
new file mode 100644
index 00000000000000..1b5a1dd0b21eec
--- /dev/null
+++ b/homeassistant/components/ambient_station/translations/id.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi"
+ },
+ "error": {
+ "invalid_key": "Kunci API tidak valid",
+ "no_devices": "Tidak ada perangkat yang ditemukan dalam akun"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kunci API",
+ "app_key": "Kunci Aplikasi"
+ },
+ "title": "Isi informasi Anda"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambient_station/translations/ko.json b/homeassistant/components/ambient_station/translations/ko.json
index d4e227656c296b..6fc8f4b17fc72d 100644
--- a/homeassistant/components/ambient_station/translations/ko.json
+++ b/homeassistant/components/ambient_station/translations/ko.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 \uc571 \ud0a4\ub294 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4."
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "invalid_key": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"no_devices": "\uacc4\uc815\uc5d0 \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4"
},
"step": {
diff --git a/homeassistant/components/ambient_station/translations/nl.json b/homeassistant/components/ambient_station/translations/nl.json
index 02c8f0727f8b52..008bc10e084d5f 100644
--- a/homeassistant/components/ambient_station/translations/nl.json
+++ b/homeassistant/components/ambient_station/translations/nl.json
@@ -4,7 +4,7 @@
"already_configured": "Service is al geconfigureerd"
},
"error": {
- "invalid_key": "Ongeldige API-sleutel en/of applicatiesleutel",
+ "invalid_key": "Ongeldige API-sleutel",
"no_devices": "Geen apparaten gevonden in account"
},
"step": {
diff --git a/homeassistant/components/ambient_station/translations/tr.json b/homeassistant/components/ambient_station/translations/tr.json
new file mode 100644
index 00000000000000..908d97f5758bfb
--- /dev/null
+++ b/homeassistant/components/ambient_station/translations/tr.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "invalid_key": "Ge\u00e7ersiz API anahtar\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Anahtar\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambient_station/translations/uk.json b/homeassistant/components/ambient_station/translations/uk.json
new file mode 100644
index 00000000000000..722cf99af7e648
--- /dev/null
+++ b/homeassistant/components/ambient_station/translations/uk.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant."
+ },
+ "error": {
+ "invalid_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API",
+ "no_devices": "\u0412 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u0456 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "app_key": "\u041a\u043b\u044e\u0447 \u0434\u043e\u0434\u0430\u0442\u043a\u0443"
+ },
+ "title": "Ambient PWS"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py
index 3baad1ac88e1d6..71c277e578ca37 100644
--- a/homeassistant/components/amcrest/__init__.py
+++ b/homeassistant/components/amcrest/__init__.py
@@ -1,4 +1,5 @@
"""Support for Amcrest IP cameras."""
+from contextlib import suppress
from datetime import timedelta
import logging
import threading
@@ -191,10 +192,8 @@ def command(self, *args, **kwargs):
def _wrap_test_online(self, now):
"""Test if camera is back online."""
_LOGGER.debug("Testing if %s back online", self._wrap_name)
- try:
- self.current_time
- except AmcrestError:
- pass
+ with suppress(AmcrestError):
+ self.current_time # pylint: disable=pointless-statement
def _monitor_events(hass, name, api, event_codes):
diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py
index 649258c42c7ab5..0add382b81f0d0 100644
--- a/homeassistant/components/amcrest/binary_sensor.py
+++ b/homeassistant/components/amcrest/binary_sensor.py
@@ -1,4 +1,5 @@
"""Support for Amcrest IP camera binary sensors."""
+from contextlib import suppress
from datetime import timedelta
import logging
@@ -38,6 +39,8 @@
BINARY_SENSOR_MOTION_DETECTED = "motion_detected"
BINARY_SENSOR_MOTION_DETECTED_POLLED = "motion_detected_polled"
BINARY_SENSOR_ONLINE = "online"
+BINARY_SENSOR_CROSSLINE_DETECTED = "crossline_detected"
+BINARY_SENSOR_CROSSLINE_DETECTED_POLLED = "crossline_detected_polled"
BINARY_POLLED_SENSORS = [
BINARY_SENSOR_AUDIO_DETECTED_POLLED,
BINARY_SENSOR_MOTION_DETECTED_POLLED,
@@ -45,11 +48,18 @@
]
_AUDIO_DETECTED_PARAMS = ("Audio Detected", DEVICE_CLASS_SOUND, "AudioMutation")
_MOTION_DETECTED_PARAMS = ("Motion Detected", DEVICE_CLASS_MOTION, "VideoMotion")
+_CROSSLINE_DETECTED_PARAMS = (
+ "CrossLine Detected",
+ DEVICE_CLASS_MOTION,
+ "CrossLineDetection",
+)
BINARY_SENSORS = {
BINARY_SENSOR_AUDIO_DETECTED: _AUDIO_DETECTED_PARAMS,
BINARY_SENSOR_AUDIO_DETECTED_POLLED: _AUDIO_DETECTED_PARAMS,
BINARY_SENSOR_MOTION_DETECTED: _MOTION_DETECTED_PARAMS,
BINARY_SENSOR_MOTION_DETECTED_POLLED: _MOTION_DETECTED_PARAMS,
+ BINARY_SENSOR_CROSSLINE_DETECTED: _CROSSLINE_DETECTED_PARAMS,
+ BINARY_SENSOR_CROSSLINE_DETECTED_POLLED: _CROSSLINE_DETECTED_PARAMS,
BINARY_SENSOR_ONLINE: ("Online", DEVICE_CLASS_CONNECTIVITY, None),
}
BINARY_SENSORS = {
@@ -58,6 +68,7 @@
}
_EXCLUSIVE_OPTIONS = [
{BINARY_SENSOR_MOTION_DETECTED, BINARY_SENSOR_MOTION_DETECTED_POLLED},
+ {BINARY_SENSOR_CROSSLINE_DETECTED, BINARY_SENSOR_CROSSLINE_DETECTED_POLLED},
]
_UPDATE_MSG = "Updating %s binary sensor"
@@ -144,10 +155,8 @@ def _update_online(self):
# Send a command to the camera to test if we can still communicate with it.
# Override of Http.command() in __init__.py will set self._api.available
# accordingly.
- try:
- self._api.current_time
- except AmcrestError:
- pass
+ with suppress(AmcrestError):
+ self._api.current_time # pylint: disable=pointless-statement
self._state = self._api.available
def _update_others(self):
diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py
index a8aabc233d179f..f57b9e62bae4f3 100644
--- a/homeassistant/components/amcrest/camera.py
+++ b/homeassistant/components/amcrest/camera.py
@@ -197,7 +197,7 @@ async def async_camera_image(self):
# and before initiating shapshot.
while self._snapshot_task:
self._check_snapshot_ok()
- _LOGGER.debug("Waiting for previous snapshot from %s ...", self._name)
+ _LOGGER.debug("Waiting for previous snapshot from %s", self._name)
await self._snapshot_task
self._check_snapshot_ok()
# Run snapshot command in separate Task that can't be cancelled so
@@ -266,7 +266,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the Amcrest-specific camera state attributes."""
attr = {}
if self._audio_enabled is not None:
diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json
index 0b6fbbdc09a02a..0b7a59edb79c44 100644
--- a/homeassistant/components/amcrest/manifest.json
+++ b/homeassistant/components/amcrest/manifest.json
@@ -2,7 +2,7 @@
"domain": "amcrest",
"name": "Amcrest",
"documentation": "https://www.home-assistant.io/integrations/amcrest",
- "requirements": ["amcrest==1.7.0"],
+ "requirements": ["amcrest==1.7.1"],
"dependencies": ["ffmpeg"],
"codeowners": ["@pnbruckner"]
}
diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py
index 44ebcfcdb95643..a30de62494e52e 100644
--- a/homeassistant/components/amcrest/sensor.py
+++ b/homeassistant/components/amcrest/sensor.py
@@ -4,9 +4,9 @@
from amcrest import AmcrestError
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_NAME, CONF_SENSORS, PERCENTAGE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from .const import DATA_AMCREST, DEVICES, SENSOR_SCAN_INTERVAL_SECS, SERVICE_UPDATE
from .helpers import log_update_error, service_signal
@@ -40,7 +40,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
-class AmcrestSensor(Entity):
+class AmcrestSensor(SensorEntity):
"""A sensor implementation for Amcrest IP camera."""
def __init__(self, name, device, sensor_type):
@@ -66,7 +66,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attrs
diff --git a/homeassistant/components/analytics/__init__.py b/homeassistant/components/analytics/__init__.py
new file mode 100644
index 00000000000000..c1187af7f17307
--- /dev/null
+++ b/homeassistant/components/analytics/__init__.py
@@ -0,0 +1,76 @@
+"""Send instance and usage analytics."""
+import voluptuous as vol
+
+from homeassistant.components import websocket_api
+from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.event import async_call_later, async_track_time_interval
+
+from .analytics import Analytics
+from .const import ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
+
+
+async def async_setup(hass: HomeAssistant, _):
+ """Set up the analytics integration."""
+ analytics = Analytics(hass)
+
+ # Load stored data
+ await analytics.load()
+
+ async def start_schedule(_event):
+ """Start the send schedule after the started event."""
+ # Wait 15 min after started
+ async_call_later(hass, 900, analytics.send_analytics)
+
+ # Send every day
+ async_track_time_interval(hass, analytics.send_analytics, INTERVAL)
+
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule)
+
+ websocket_api.async_register_command(hass, websocket_analytics)
+ websocket_api.async_register_command(hass, websocket_analytics_preferences)
+
+ hass.data[DOMAIN] = analytics
+ return True
+
+
+@websocket_api.require_admin
+@websocket_api.async_response
+@websocket_api.websocket_command({vol.Required("type"): "analytics"})
+async def websocket_analytics(
+ hass: HomeAssistant,
+ connection: websocket_api.connection.ActiveConnection,
+ msg: dict,
+) -> None:
+ """Return analytics preferences."""
+ analytics: Analytics = hass.data[DOMAIN]
+ connection.send_result(
+ msg["id"],
+ {ATTR_PREFERENCES: analytics.preferences},
+ )
+
+
+@websocket_api.require_admin
+@websocket_api.async_response
+@websocket_api.websocket_command(
+ {
+ vol.Required("type"): "analytics/preferences",
+ vol.Required("preferences", default={}): PREFERENCE_SCHEMA,
+ }
+)
+async def websocket_analytics_preferences(
+ hass: HomeAssistant,
+ connection: websocket_api.connection.ActiveConnection,
+ msg: dict,
+) -> None:
+ """Update analytics preferences."""
+ preferences = msg[ATTR_PREFERENCES]
+ analytics: Analytics = hass.data[DOMAIN]
+
+ await analytics.save_preferences(preferences)
+ await analytics.send_analytics()
+
+ connection.send_result(
+ msg["id"],
+ {ATTR_PREFERENCES: analytics.preferences},
+ )
diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py
new file mode 100644
index 00000000000000..e6e8678cc10945
--- /dev/null
+++ b/homeassistant/components/analytics/analytics.py
@@ -0,0 +1,240 @@
+"""Analytics helper class for the analytics integration."""
+import asyncio
+import uuid
+
+import aiohttp
+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.const import ATTR_DOMAIN, __version__ as HA_VERSION
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.storage import Store
+from homeassistant.helpers.system_info import async_get_system_info
+from homeassistant.loader import IntegrationNotFound, async_get_integration
+from homeassistant.setup import async_get_loaded_integrations
+
+from .const import (
+ ANALYTICS_ENDPOINT_URL,
+ ATTR_ADDON_COUNT,
+ ATTR_ADDONS,
+ ATTR_AUTO_UPDATE,
+ ATTR_AUTOMATION_COUNT,
+ ATTR_BASE,
+ ATTR_CUSTOM_INTEGRATIONS,
+ ATTR_DIAGNOSTICS,
+ ATTR_HEALTHY,
+ ATTR_INTEGRATION_COUNT,
+ ATTR_INTEGRATIONS,
+ ATTR_ONBOARDED,
+ ATTR_PREFERENCES,
+ ATTR_PROTECTED,
+ ATTR_SLUG,
+ ATTR_STATE_COUNT,
+ ATTR_STATISTICS,
+ ATTR_SUPERVISOR,
+ ATTR_SUPPORTED,
+ ATTR_USAGE,
+ ATTR_USER_COUNT,
+ ATTR_UUID,
+ ATTR_VERSION,
+ LOGGER,
+ PREFERENCE_SCHEMA,
+ STORAGE_KEY,
+ STORAGE_VERSION,
+)
+
+
+class Analytics:
+ """Analytics helper class for the analytics integration."""
+
+ def __init__(self, hass: HomeAssistant) -> None:
+ """Initialize the Analytics class."""
+ self.hass: HomeAssistant = hass
+ self.session = async_get_clientsession(hass)
+ self._data = {ATTR_PREFERENCES: {}, ATTR_ONBOARDED: False, ATTR_UUID: None}
+ self._store: Store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
+
+ @property
+ def preferences(self) -> dict:
+ """Return the current active preferences."""
+ preferences = self._data[ATTR_PREFERENCES]
+ return {
+ ATTR_BASE: preferences.get(ATTR_BASE, False),
+ ATTR_DIAGNOSTICS: preferences.get(ATTR_DIAGNOSTICS, False),
+ ATTR_USAGE: preferences.get(ATTR_USAGE, False),
+ ATTR_STATISTICS: preferences.get(ATTR_STATISTICS, False),
+ }
+
+ @property
+ def onboarded(self) -> bool:
+ """Return bool if the user has made a choice."""
+ return self._data[ATTR_ONBOARDED]
+
+ @property
+ def uuid(self) -> bool:
+ """Return the uuid for the analytics integration."""
+ return self._data[ATTR_UUID]
+
+ @property
+ def supervisor(self) -> bool:
+ """Return bool if a supervisor is present."""
+ return hassio.is_hassio(self.hass)
+
+ async def load(self) -> None:
+ """Load preferences."""
+ stored = await self._store.async_load()
+ if stored:
+ self._data = stored
+
+ if self.supervisor:
+ supervisor_info = hassio.get_supervisor_info(self.hass)
+ if not self.onboarded:
+ # User have not configured analytics, get this setting from the supervisor
+ if supervisor_info[ATTR_DIAGNOSTICS] and not self.preferences.get(
+ ATTR_DIAGNOSTICS, False
+ ):
+ self._data[ATTR_PREFERENCES][ATTR_DIAGNOSTICS] = True
+ elif not supervisor_info[ATTR_DIAGNOSTICS] and self.preferences.get(
+ ATTR_DIAGNOSTICS, False
+ ):
+ self._data[ATTR_PREFERENCES][ATTR_DIAGNOSTICS] = False
+
+ async def save_preferences(self, preferences: dict) -> None:
+ """Save preferences."""
+ preferences = PREFERENCE_SCHEMA(preferences)
+ self._data[ATTR_PREFERENCES].update(preferences)
+ self._data[ATTR_ONBOARDED] = True
+
+ await self._store.async_save(self._data)
+
+ if self.supervisor:
+ await hassio.async_update_diagnostics(
+ self.hass, self.preferences.get(ATTR_DIAGNOSTICS, False)
+ )
+
+ async def send_analytics(self, _=None) -> None:
+ """Send analytics."""
+ supervisor_info = None
+
+ if not self.onboarded or not self.preferences.get(ATTR_BASE, False):
+ LOGGER.debug("Nothing to submit")
+ return
+
+ if self._data.get(ATTR_UUID) is None:
+ self._data[ATTR_UUID] = uuid.uuid4().hex
+ await self._store.async_save(self._data)
+
+ if self.supervisor:
+ supervisor_info = hassio.get_supervisor_info(self.hass)
+
+ system_info = await async_get_system_info(self.hass)
+ integrations = []
+ custom_integrations = []
+ addons = []
+ payload: dict = {
+ ATTR_UUID: self.uuid,
+ ATTR_VERSION: HA_VERSION,
+ ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE],
+ }
+
+ if supervisor_info is not None:
+ payload[ATTR_SUPERVISOR] = {
+ ATTR_HEALTHY: supervisor_info[ATTR_HEALTHY],
+ ATTR_SUPPORTED: supervisor_info[ATTR_SUPPORTED],
+ }
+
+ if self.preferences.get(ATTR_USAGE, False) or self.preferences.get(
+ ATTR_STATISTICS, False
+ ):
+ configured_integrations = await asyncio.gather(
+ *[
+ async_get_integration(self.hass, domain)
+ for domain in async_get_loaded_integrations(self.hass)
+ ],
+ return_exceptions=True,
+ )
+
+ for integration in configured_integrations:
+ if isinstance(integration, IntegrationNotFound):
+ continue
+
+ if isinstance(integration, BaseException):
+ raise integration
+
+ if integration.disabled:
+ continue
+
+ if not integration.is_built_in:
+ custom_integrations.append(
+ {
+ ATTR_DOMAIN: integration.domain,
+ ATTR_VERSION: integration.version,
+ }
+ )
+ continue
+
+ integrations.append(integration.domain)
+
+ if supervisor_info is not None:
+ installed_addons = await asyncio.gather(
+ *[
+ hassio.async_get_addon_info(self.hass, addon[ATTR_SLUG])
+ for addon in supervisor_info[ATTR_ADDONS]
+ ]
+ )
+ for addon in installed_addons:
+ addons.append(
+ {
+ ATTR_SLUG: addon[ATTR_SLUG],
+ ATTR_PROTECTED: addon[ATTR_PROTECTED],
+ ATTR_VERSION: addon[ATTR_VERSION],
+ ATTR_AUTO_UPDATE: addon[ATTR_AUTO_UPDATE],
+ }
+ )
+
+ if self.preferences.get(ATTR_USAGE, False):
+ payload[ATTR_INTEGRATIONS] = integrations
+ payload[ATTR_CUSTOM_INTEGRATIONS] = custom_integrations
+ if supervisor_info is not None:
+ payload[ATTR_ADDONS] = addons
+
+ if self.preferences.get(ATTR_STATISTICS, False):
+ payload[ATTR_STATE_COUNT] = len(self.hass.states.async_all())
+ payload[ATTR_AUTOMATION_COUNT] = len(
+ self.hass.states.async_all(AUTOMATION_DOMAIN)
+ )
+ payload[ATTR_INTEGRATION_COUNT] = len(integrations)
+ if supervisor_info is not None:
+ payload[ATTR_ADDON_COUNT] = len(addons)
+ payload[ATTR_USER_COUNT] = len(
+ [
+ user
+ for user in await self.hass.auth.async_get_users()
+ if not user.system_generated
+ ]
+ )
+
+ try:
+ with async_timeout.timeout(30):
+ response = await self.session.post(ANALYTICS_ENDPOINT_URL, json=payload)
+ if response.status == 200:
+ LOGGER.info(
+ (
+ "Submitted analytics to Home Assistant servers. "
+ "Information submitted includes %s"
+ ),
+ payload,
+ )
+ else:
+ LOGGER.warning(
+ "Sending analytics failed with statuscode %s", response.status
+ )
+ except asyncio.TimeoutError:
+ LOGGER.error("Timeout sending analytics to %s", ANALYTICS_ENDPOINT_URL)
+ except aiohttp.ClientError as err:
+ LOGGER.error(
+ "Error sending analytics to %s: %r", ANALYTICS_ENDPOINT_URL, err
+ )
diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py
new file mode 100644
index 00000000000000..a6fe91b5a44b1e
--- /dev/null
+++ b/homeassistant/components/analytics/const.py
@@ -0,0 +1,48 @@
+"""Constants for the analytics integration."""
+from datetime import timedelta
+import logging
+
+import voluptuous as vol
+
+ANALYTICS_ENDPOINT_URL = "https://analytics-api.home-assistant.io/v1"
+DOMAIN = "analytics"
+INTERVAL = timedelta(days=1)
+STORAGE_KEY = "core.analytics"
+STORAGE_VERSION = 1
+
+
+LOGGER: logging.Logger = logging.getLogger(__package__)
+
+ATTR_ADDON_COUNT = "addon_count"
+ATTR_ADDONS = "addons"
+ATTR_AUTO_UPDATE = "auto_update"
+ATTR_AUTOMATION_COUNT = "automation_count"
+ATTR_BASE = "base"
+ATTR_CUSTOM_INTEGRATIONS = "custom_integrations"
+ATTR_DIAGNOSTICS = "diagnostics"
+ATTR_HEALTHY = "healthy"
+ATTR_INSTALLATION_TYPE = "installation_type"
+ATTR_INTEGRATION_COUNT = "integration_count"
+ATTR_INTEGRATIONS = "integrations"
+ATTR_ONBOARDED = "onboarded"
+ATTR_PREFERENCES = "preferences"
+ATTR_PROTECTED = "protected"
+ATTR_SLUG = "slug"
+ATTR_STATE_COUNT = "state_count"
+ATTR_STATISTICS = "statistics"
+ATTR_SUPERVISOR = "supervisor"
+ATTR_SUPPORTED = "supported"
+ATTR_USAGE = "usage"
+ATTR_USER_COUNT = "user_count"
+ATTR_UUID = "uuid"
+ATTR_VERSION = "version"
+
+
+PREFERENCE_SCHEMA = vol.Schema(
+ {
+ vol.Optional(ATTR_BASE): bool,
+ vol.Optional(ATTR_DIAGNOSTICS): bool,
+ vol.Optional(ATTR_STATISTICS): bool,
+ vol.Optional(ATTR_USAGE): bool,
+ }
+)
diff --git a/homeassistant/components/analytics/manifest.json b/homeassistant/components/analytics/manifest.json
new file mode 100644
index 00000000000000..db795501fa666b
--- /dev/null
+++ b/homeassistant/components/analytics/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "analytics",
+ "name": "Analytics",
+ "documentation": "https://www.home-assistant.io/integrations/analytics",
+ "codeowners": ["@home-assistant/core", "@ludeeus"],
+ "dependencies": ["api", "websocket_api"],
+ "quality_scale": "internal"
+}
diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py
index 905d0262862720..54a281feacd1fb 100644
--- a/homeassistant/components/android_ip_webcam/__init__.py
+++ b/homeassistant/components/android_ip_webcam/__init__.py
@@ -321,7 +321,7 @@ def available(self):
return self._ipcam.available
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
state_attr = {ATTR_HOST: self._host}
if self._ipcam.status_data is None:
diff --git a/homeassistant/components/android_ip_webcam/sensor.py b/homeassistant/components/android_ip_webcam/sensor.py
index 05c1fe16c61c2d..adedb297cd1fb8 100644
--- a/homeassistant/components/android_ip_webcam/sensor.py
+++ b/homeassistant/components/android_ip_webcam/sensor.py
@@ -1,4 +1,5 @@
"""Support for Android IP Webcam sensors."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.helpers.icon import icon_for_battery_level
from . import (
@@ -30,7 +31,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(all_sensors, True)
-class IPWebcamSensor(AndroidIPCamEntity):
+class IPWebcamSensor(AndroidIPCamEntity, SensorEntity):
"""Representation of a IP Webcam sensor."""
def __init__(self, name, host, ipcam, sensor):
diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py
index 4d17b4ecdad4d4..b2a8ceffc9f1df 100644
--- a/homeassistant/components/androidtv/media_player.py
+++ b/homeassistant/components/androidtv/media_player.py
@@ -469,7 +469,7 @@ def available(self):
return self._available
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Provide the last ADB command's response and the device's HDMI input as attributes."""
return {
"adb_response": self._adb_response,
diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py
index 55e6d8c7a569a9..36dc1155b7fd7b 100644
--- a/homeassistant/components/apcupsd/sensor.py
+++ b/homeassistant/components/apcupsd/sensor.py
@@ -4,7 +4,7 @@
from apcaccess.status import ALL_UNITS
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_RESOURCES,
ELECTRICAL_CURRENT_AMPERE,
@@ -18,7 +18,6 @@
VOLT,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from . import DOMAIN
@@ -156,7 +155,7 @@ def infer_unit(value):
return value, None
-class APCUPSdSensor(Entity):
+class APCUPSdSensor(SensorEntity):
"""Representation of a sensor entity for APCUPSd status values."""
def __init__(self, data, sensor_type):
diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py
index f383f982abc87b..a91d85402866c4 100644
--- a/homeassistant/components/api/__init__.py
+++ b/homeassistant/components/api/__init__.py
@@ -1,5 +1,6 @@
"""Rest API for Home Assistant."""
import asyncio
+from contextlib import suppress
import json
import logging
@@ -37,7 +38,6 @@
from homeassistant.helpers.json import JSONEncoder
from homeassistant.helpers.network import NoURLAvailableError, get_url
from homeassistant.helpers.service import async_get_all_descriptions
-from homeassistant.helpers.state import AsyncTrackStates
from homeassistant.helpers.system_info import async_get_system_info
_LOGGER = logging.getLogger(__name__)
@@ -197,15 +197,11 @@ async def get(self, request):
ATTR_VERSION: __version__,
}
- try:
+ with suppress(NoURLAvailableError):
data["external_url"] = get_url(hass, allow_internal=False)
- except NoURLAvailableError:
- pass
- try:
+ with suppress(NoURLAvailableError):
data["internal_url"] = get_url(hass, allow_external=False)
- except NoURLAvailableError:
- pass
# Set old base URL based on external or internal
data["base_url"] = data["external_url"] or data["internal_url"]
@@ -367,20 +363,27 @@ async def post(self, request, domain, service):
Returns a list of changed states.
"""
- hass = request.app["hass"]
+ hass: ha.HomeAssistant = request.app["hass"]
body = await request.text()
try:
data = json.loads(body) if body else None
except ValueError:
return self.json_message("Data should be valid JSON.", HTTP_BAD_REQUEST)
- with AsyncTrackStates(hass) as changed_states:
- try:
- await hass.services.async_call(
- domain, service, data, True, self.context(request)
- )
- except (vol.Invalid, ServiceNotFound) as ex:
- raise HTTPBadRequest() from ex
+ context = self.context(request)
+
+ try:
+ await hass.services.async_call(
+ domain, service, data, blocking=True, context=context
+ )
+ except (vol.Invalid, ServiceNotFound) as ex:
+ raise HTTPBadRequest() from ex
+
+ changed_states = []
+
+ for state in hass.states.async_all():
+ if state.context is context:
+ changed_states.append(state)
return self.json(changed_states)
diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py
index 6f8de7e9c847fa..c9e12a2086359a 100644
--- a/homeassistant/components/apns/notify.py
+++ b/homeassistant/components/apns/notify.py
@@ -1,4 +1,5 @@
"""APNS Notification platform."""
+from contextlib import suppress
import logging
from apns2.client import APNsClient
@@ -155,7 +156,7 @@ def __init__(self, hass, app_name, topic, sandbox, cert_file):
self.device_states = {}
self.topic = topic
- try:
+ with suppress(FileNotFoundError):
self.devices = {
str(key): ApnsDevice(
str(key),
@@ -165,8 +166,6 @@ def __init__(self, hass, app_name, topic, sandbox, cert_file):
)
for (key, value) in load_yaml_config_file(self.yaml_path).items()
}
- except FileNotFoundError:
- pass
tracking_ids = [
device.full_tracking_device_id
diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py
index eca5e91ddeb80b..b4e0e1be666341 100644
--- a/homeassistant/components/apple_tv/__init__.py
+++ b/homeassistant/components/apple_tv/__init__.py
@@ -8,6 +8,7 @@
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN
+from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.const import (
CONF_ADDRESS,
CONF_NAME,
@@ -34,19 +35,12 @@
NOTIFICATION_TITLE = "Apple TV Notification"
NOTIFICATION_ID = "apple_tv_notification"
-SOURCE_REAUTH = "reauth"
-
SIGNAL_CONNECTED = "apple_tv_connected"
SIGNAL_DISCONNECTED = "apple_tv_disconnected"
PLATFORMS = [MP_DOMAIN, REMOTE_DOMAIN]
-async def async_setup(hass, config):
- """Set up the Apple TV integration."""
- return True
-
-
async def async_setup_entry(hass, entry):
"""Set up a config entry for Apple TV."""
manager = AppleTVManager(hass, entry)
@@ -62,8 +56,8 @@ async def setup_platforms():
"""Set up platforms and initiate connection."""
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_setup(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ for platform in PLATFORMS
]
)
await manager.init()
@@ -151,6 +145,13 @@ def should_poll(self):
"""No polling needed for Apple TV."""
return False
+ @property
+ def device_info(self):
+ """Return the device info."""
+ return {
+ "identifiers": {(DOMAIN, self._identifier)},
+ }
+
class AppleTVManager:
"""Connection and power manager for an Apple TV.
@@ -180,20 +181,23 @@ def connection_lost(self, _):
This is a callback function from pyatv.interface.DeviceListener.
"""
- _LOGGER.warning('Connection lost to Apple TV "%s"', self.atv.name)
- if self.atv:
- self.atv.close()
- self.atv = None
+ _LOGGER.warning(
+ 'Connection lost to Apple TV "%s"', self.config_entry.data[CONF_NAME]
+ )
self._connection_was_lost = True
- self._dispatch_send(SIGNAL_DISCONNECTED)
- self._start_connect_loop()
+ self._handle_disconnect()
def connection_closed(self):
"""Device connection was (intentionally) closed.
This is a callback function from pyatv.interface.DeviceListener.
"""
+ self._handle_disconnect()
+
+ def _handle_disconnect(self):
+ """Handle that the device disconnected and restart connect loop."""
if self.atv:
+ self.atv.listener = None
self.atv.close()
self.atv = None
self._dispatch_send(SIGNAL_DISCONNECTED)
@@ -265,7 +269,7 @@ def _auth_problem(self):
"""Problem to authenticate occurred that needs intervention."""
_LOGGER.debug("Authentication error, reconfigure integration")
- name = self.config_entry.data.get(CONF_NAME)
+ name = self.config_entry.data[CONF_NAME]
identifier = self.config_entry.unique_id
self.hass.components.persistent_notification.create(
@@ -334,7 +338,8 @@ async def _connect(self, conf):
self._connection_attempts = 0
if self._connection_was_lost:
_LOGGER.info(
- 'Connection was re-established to Apple TV "%s"', self.atv.service.name
+ 'Connection was re-established to Apple TV "%s"',
+ self.config_entry.data[CONF_NAME],
)
self._connection_was_lost = False
@@ -345,10 +350,16 @@ async def _async_setup_device_registry(self):
"name": self.config_entry.data[CONF_NAME],
}
+ area = attrs["name"]
+ name_trailer = f" {DEFAULT_NAME}"
+ if area.endswith(name_trailer):
+ area = area[: -len(name_trailer)]
+ attrs["suggested_area"] = area
+
if self.atv:
dev_info = self.atv.device_info
- attrs["model"] = "Apple TV " + dev_info.model.name.replace("Gen", "")
+ attrs["model"] = DEFAULT_NAME + " " + dev_info.model.name.replace("Gen", "")
attrs["sw_version"] = dev_info.version
if dev_info.mac:
diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py
index 9c2f25b6d5343f..ab4ced1547f2f8 100644
--- a/homeassistant/components/apple_tv/config_flow.py
+++ b/homeassistant/components/apple_tv/config_flow.py
@@ -21,8 +21,7 @@
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -44,7 +43,7 @@ def _filter_device(dev):
return True
if identifier == dev.name:
return True
- return any([service.identifier == identifier for service in dev.services])
+ return any(service.identifier == identifier for service in dev.services)
def _host_filter():
try:
@@ -101,10 +100,7 @@ async def async_step_reauth(self, info):
await self.async_set_unique_id(info[CONF_IDENTIFIER])
self.target_device = info[CONF_IDENTIFIER]
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {"name": info[CONF_NAME]}
-
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["identifier"] = self.unique_id
return await self.async_step_reconfigure()
@@ -170,7 +166,6 @@ async def async_step_zeroconf(self, discovery_info):
await self.async_set_unique_id(identifier)
self._abort_if_unique_id_configured()
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["identifier"] = self.unique_id
self.context["title_placeholders"] = {"name": name}
self.target_device = identifier
diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json
index 21b2df308d3f33..a60c5db3a06309 100644
--- a/homeassistant/components/apple_tv/manifest.json
+++ b/homeassistant/components/apple_tv/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"requirements": [
- "pyatv==0.7.5"
+ "pyatv==0.7.7"
],
"zeroconf": [
"_mediaremotetv._tcp.local.",
diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py
index 81bb79dc50b585..a855fc6b53e03c 100644
--- a/homeassistant/components/apple_tv/media_player.py
+++ b/homeassistant/components/apple_tv/media_player.py
@@ -1,22 +1,35 @@
"""Support for Apple TV media player."""
import logging
-from pyatv.const import DeviceState, FeatureName, FeatureState, MediaType
+from pyatv.const import (
+ DeviceState,
+ FeatureName,
+ FeatureState,
+ MediaType,
+ RepeatState,
+ ShuffleState,
+)
from homeassistant.components.media_player import MediaPlayerEntity
from homeassistant.components.media_player.const import (
MEDIA_TYPE_MUSIC,
MEDIA_TYPE_TVSHOW,
MEDIA_TYPE_VIDEO,
+ REPEAT_MODE_ALL,
+ REPEAT_MODE_OFF,
+ REPEAT_MODE_ONE,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY,
SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK,
+ SUPPORT_REPEAT_SET,
SUPPORT_SEEK,
+ SUPPORT_SHUFFLE_SET,
SUPPORT_STOP,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
+ SUPPORT_VOLUME_STEP,
)
from homeassistant.const import (
CONF_NAME,
@@ -46,6 +59,9 @@
| SUPPORT_STOP
| SUPPORT_NEXT_TRACK
| SUPPORT_PREVIOUS_TRACK
+ | SUPPORT_VOLUME_STEP
+ | SUPPORT_REPEAT_SET
+ | SUPPORT_SHUFFLE_SET
)
@@ -110,17 +126,15 @@ def playstatus_error(self, _, exception):
@property
def app_id(self):
"""ID of the current running app."""
- if self.atv:
- if self.atv.features.in_state(FeatureState.Available, FeatureName.App):
- return self.atv.metadata.app.identifier
+ if self._is_feature_available(FeatureName.App):
+ return self.atv.metadata.app.identifier
return None
@property
def app_name(self):
"""Name of the current running app."""
- if self.atv:
- if self.atv.features.in_state(FeatureState.Available, FeatureName.App):
- return self.atv.metadata.app.name
+ if self._is_feature_available(FeatureName.App):
+ return self.atv.metadata.app.name
return None
@property
@@ -198,6 +212,23 @@ def media_album_name(self):
return self._playing.album
return None
+ @property
+ def repeat(self):
+ """Return current repeat mode."""
+ if self._is_feature_available(FeatureName.Repeat):
+ return {
+ RepeatState.Track: REPEAT_MODE_ONE,
+ RepeatState.All: REPEAT_MODE_ALL,
+ }.get(self._playing.repeat, REPEAT_MODE_OFF)
+ return None
+
+ @property
+ def shuffle(self):
+ """Boolean if shuffle is enabled."""
+ if self._is_feature_available(FeatureName.Shuffle):
+ return self._playing.shuffle != ShuffleState.Off
+ return None
+
@property
def supported_features(self):
"""Flag media player features that are supported."""
@@ -221,39 +252,61 @@ async def async_turn_off(self):
async def async_media_play_pause(self):
"""Pause media on media player."""
if self._playing:
- state = self.state
- if state == STATE_PAUSED:
- await self.atv.remote_control.play()
- elif state == STATE_PLAYING:
- await self.atv.remote_control.pause()
+ await self.atv.remote_control.play_pause()
return None
async def async_media_play(self):
"""Play media."""
- if self._playing:
+ if self.atv:
await self.atv.remote_control.play()
async def async_media_stop(self):
"""Stop the media player."""
- if self._playing:
+ if self.atv:
await self.atv.remote_control.stop()
async def async_media_pause(self):
"""Pause the media player."""
- if self._playing:
+ if self.atv:
await self.atv.remote_control.pause()
async def async_media_next_track(self):
"""Send next track command."""
- if self._playing:
+ if self.atv:
await self.atv.remote_control.next()
async def async_media_previous_track(self):
"""Send previous track command."""
- if self._playing:
+ if self.atv:
await self.atv.remote_control.previous()
async def async_media_seek(self, position):
"""Send seek command."""
- if self._playing:
+ if self.atv:
await self.atv.remote_control.set_position(position)
+
+ async def async_volume_up(self):
+ """Turn volume up for media player."""
+ if self.atv:
+ await self.atv.remote_control.volume_up()
+
+ async def async_volume_down(self):
+ """Turn volume down for media player."""
+ if self.atv:
+ await self.atv.remote_control.volume_down()
+
+ async def async_set_repeat(self, repeat):
+ """Set repeat mode."""
+ if self.atv:
+ mode = {
+ REPEAT_MODE_ONE: RepeatState.Track,
+ REPEAT_MODE_ALL: RepeatState.All,
+ }.get(repeat, RepeatState.Off)
+ await self.atv.remote_control.set_repeat(mode)
+
+ async def async_set_shuffle(self, shuffle):
+ """Enable/disable shuffle mode."""
+ if self.atv:
+ await self.atv.remote_control.set_shuffle(
+ ShuffleState.Songs if shuffle else ShuffleState.Off
+ )
diff --git a/homeassistant/components/apple_tv/remote.py b/homeassistant/components/apple_tv/remote.py
index a76c4c6a20882c..3d88bddcbc9546 100644
--- a/homeassistant/components/apple_tv/remote.py
+++ b/homeassistant/components/apple_tv/remote.py
@@ -1,8 +1,14 @@
"""Remote control support for Apple TV."""
+import asyncio
import logging
-from homeassistant.components.remote import RemoteEntity
+from homeassistant.components.remote import (
+ ATTR_DELAY_SECS,
+ ATTR_NUM_REPEATS,
+ DEFAULT_DELAY_SECS,
+ RemoteEntity,
+)
from homeassistant.const import CONF_NAME
from . import AppleTVEntity
@@ -43,12 +49,19 @@ async def async_turn_off(self, **kwargs):
async def async_send_command(self, command, **kwargs):
"""Send a command to one device."""
+ num_repeats = kwargs[ATTR_NUM_REPEATS]
+ delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
+
if not self.is_on:
_LOGGER.error("Unable to send commands, not connected to %s", self._name)
return
- for single_command in command:
- if not hasattr(self.atv.remote_control, single_command):
- continue
+ for _ in range(num_repeats):
+ for single_command in command:
+ attr_value = getattr(self.atv.remote_control, single_command, None)
+ if not attr_value:
+ raise ValueError("Command not found. Exiting sequence")
- await getattr(self.atv.remote_control, single_command)()
+ _LOGGER.info("Sending command %s", single_command)
+ await attr_value()
+ await asyncio.sleep(delay)
diff --git a/homeassistant/components/apple_tv/translations/fr.json b/homeassistant/components/apple_tv/translations/fr.json
index a55d37ed588cc3..e1a719b31c90b3 100644
--- a/homeassistant/components/apple_tv/translations/fr.json
+++ b/homeassistant/components/apple_tv/translations/fr.json
@@ -1,7 +1,17 @@
{
"config": {
+ "abort": {
+ "already_configured_device": "Le p\u00e9riph\u00e9rique est d\u00e9j\u00e0 configur\u00e9",
+ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours",
+ "backoff": "L'appareil n'accepte pas les demandes d'appariement pour le moment (vous avez peut-\u00eatre saisi un code PIN non valide trop de fois), r\u00e9essayez plus tard.",
+ "device_did_not_pair": "Aucune tentative pour terminer l'appairage n'a \u00e9t\u00e9 effectu\u00e9e \u00e0 partir de l'appareil.",
+ "invalid_config": "La configuration de cet appareil est incompl\u00e8te. Veuillez r\u00e9essayer de l'ajouter.",
+ "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau",
+ "unknown": "Erreur inattendue"
+ },
"error": {
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
+ "invalid_auth": "Autentification invalide",
"no_devices_found": "Aucun appareil d\u00e9tect\u00e9 sur le r\u00e9seau",
"no_usable_service": "Un dispositif a \u00e9t\u00e9 trouv\u00e9, mais aucun moyen d\u2019\u00e9tablir un lien avec lui. Si vous continuez \u00e0 voir ce message, essayez de sp\u00e9cifier son adresse IP ou de red\u00e9marrer votre Apple TV.",
"unknown": "Erreur innatendue"
@@ -19,9 +29,12 @@
"pair_with_pin": {
"data": {
"pin": "Code PIN"
- }
+ },
+ "description": "L'appairage est requis pour le protocole `{protocol}`. Veuillez saisir le code PIN affich\u00e9 \u00e0 l'\u00e9cran. Les z\u00e9ros doivent \u00eatre omis, c'est-\u00e0-dire entrer 123 si le code affich\u00e9 est 0123.",
+ "title": "Appairage"
},
"reconfigure": {
+ "description": "Cette Apple TV rencontre des difficult\u00e9s de connexion et doit \u00eatre reconfigur\u00e9e.",
"title": "Reconfiguration de l'appareil"
},
"service_problem": {
diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json
index 26c02fabbb4d85..63bf29a73f1aa2 100644
--- a/homeassistant/components/apple_tv/translations/hu.json
+++ b/homeassistant/components/apple_tv/translations/hu.json
@@ -1,15 +1,18 @@
{
"config": {
"abort": {
- "no_devices_found": "Nincs eszk\u00f6z a h\u00e1l\u00f3zaton",
- "unknown": "V\u00e1ratlan hiba"
+ "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
+ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"error": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
- "invalid_auth": "Azonos\u00edt\u00e1s nem siker\u00fclt",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton",
- "unknown": "V\u00e1ratlan hiba"
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
+ "flow_title": "Apple TV: {name}",
"step": {
"confirm": {
"title": "Apple TV sikeresen hozz\u00e1adva"
@@ -19,7 +22,7 @@
},
"pair_with_pin": {
"data": {
- "pin": "PIN K\u00f3d"
+ "pin": "PIN-k\u00f3d"
},
"title": "P\u00e1ros\u00edt\u00e1s"
},
diff --git a/homeassistant/components/apple_tv/translations/id.json b/homeassistant/components/apple_tv/translations/id.json
new file mode 100644
index 00000000000000..5646b4982422c4
--- /dev/null
+++ b/homeassistant/components/apple_tv/translations/id.json
@@ -0,0 +1,64 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_device": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "backoff": "Perangkat tidak bisa menerima permintaan pemasangan saat ini (Anda mungkin telah berulang kali memasukkan kode PIN yang salah). Coba lagi nanti.",
+ "device_did_not_pair": "Tidak ada upaya untuk menyelesaikan proses pemasangan dari sisi perangkat.",
+ "invalid_config": "Konfigurasi untuk perangkat ini tidak lengkap. Coba tambahkan lagi.",
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "error": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "invalid_auth": "Autentikasi tidak valid",
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan",
+ "no_usable_service": "Perangkat ditemukan tetapi kami tidak dapat mengidentifikasi berbagai cara untuk membuat koneksi ke perangkat tersebut. Jika Anda terus melihat pesan ini, coba tentukan alamat IP-nya atau mulai ulang Apple TV Anda.",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "flow_title": "Apple TV: {name}",
+ "step": {
+ "confirm": {
+ "description": "Anda akan menambahkan Apple TV bernama `{name}` ke Home Assistant.\n\n** Untuk menyelesaikan proses, Anda mungkin harus memasukkan kode PIN beberapa kali.**\n\nPerhatikan bahwa Anda *tidak* akan dapat mematikan Apple TV dengan integrasi ini. Hanya pemutar media di Home Assistant yang akan dimatikan!",
+ "title": "Konfirmasikan menambahkan Apple TV"
+ },
+ "pair_no_pin": {
+ "description": "Pemasangan diperlukan untuk layanan `{protocol}`. Masukkan PIN {pin} di Apple TV untuk melanjutkan.",
+ "title": "Memasangkan"
+ },
+ "pair_with_pin": {
+ "data": {
+ "pin": "Kode PIN"
+ },
+ "description": "Pemasangan diperlukan untuk protokol `{protocol}`. Masukkan kode PIN yang ditampilkan pada layar. Angka nol di awal harus dihilangkan. Misalnya, masukkan 123 jika kode yang ditampilkan adalah 0123.",
+ "title": "Memasangkan"
+ },
+ "reconfigure": {
+ "description": "Apple TV ini mengalami masalah koneksi dan harus dikonfigurasi ulang.",
+ "title": "Konfigurasi ulang perangkat"
+ },
+ "service_problem": {
+ "description": "Terjadi masalah saat protokol pemasangan `{protocol}`. Masalah ini akan diabaikan.",
+ "title": "Gagal menambahkan layanan"
+ },
+ "user": {
+ "data": {
+ "device_input": "Perangkat"
+ },
+ "description": "Mulai dengan memasukkan nama perangkat (misalnya Dapur atau Kamar Tidur) atau alamat IP Apple TV yang ingin ditambahkan. Jika ada perangkat yang ditemukan secara otomatis di jaringan Anda, perangkat tersebut akan ditampilkan di bawah ini.\n\nJika Anda tidak dapat melihat perangkat atau mengalami masalah, coba tentukan alamat IP perangkat.\n\n{devices}",
+ "title": "Siapkan Apple TV baru"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "start_off": "Jangan nyalakan perangkat saat memulai Home Assistant"
+ },
+ "description": "Konfigurasikan pengaturan umum perangkat"
+ }
+ }
+ },
+ "title": "Apple TV"
+}
\ No newline at end of file
diff --git a/homeassistant/components/apple_tv/translations/ko.json b/homeassistant/components/apple_tv/translations/ko.json
new file mode 100644
index 00000000000000..278dbec04e40bb
--- /dev/null
+++ b/homeassistant/components/apple_tv/translations/ko.json
@@ -0,0 +1,64 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
+ "backoff": "\uae30\uae30\uac00 \ud604\uc7ac \ud398\uc5b4\ub9c1 \uc694\uccad\uc744 \uc218\ub77d\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4(\uc798\ubabb\ub41c PIN \ucf54\ub4dc\ub97c \ub108\ubb34 \ub9ce\uc774 \uc785\ub825\ud588\uc744 \uc218 \uc788\uc74c). \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "device_did_not_pair": "\uae30\uae30\uc5d0\uc11c \ud398\uc5b4\ub9c1 \ud504\ub85c\uc138\uc2a4\ub97c \uc644\ub8cc\ud558\ub824\uace0 \uc2dc\ub3c4\ud558\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
+ "invalid_config": "\uc774 \uae30\uae30\uc5d0 \ub300\ud55c \uad6c\uc131\uc774 \ubd88\uc644\uc804\ud569\ub2c8\ub2e4. \ub2e4\uc2dc \ucd94\uac00\ud574\uc8fc\uc138\uc694.",
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "no_usable_service": "\uae30\uae30\ub97c \ucc3e\uc558\uc9c0\ub9cc \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\ub294 \ubc29\ubc95\uc744 \uc2dd\ubcc4\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uba54\uc2dc\uc9c0\uac00 \uacc4\uc18d \ud45c\uc2dc\ub418\uba74 \ud574\ub2f9 IP \uc8fc\uc18c\ub97c \uc9c1\uc811 \uc9c0\uc815\ud574\uc8fc\uc2dc\uac70\ub098 Apple TV\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud574\uc8fc\uc138\uc694.",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "flow_title": "Apple TV: {name}",
+ "step": {
+ "confirm": {
+ "description": "Apple TV `{name}`\uc744(\ub97c) Home Assistant\uc5d0 \ucd94\uac00\ud558\ub824\uace0 \ud569\ub2c8\ub2e4.\n\n**\ud504\ub85c\uc138\uc2a4\ub97c \uc644\ub8cc\ud558\ub824\uba74 \uc5ec\ub7ec \uac1c\uc758 PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc57c \ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.**\n\n\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \ud1b5\ud574 Apple TV\uc758 \uc804\uc6d0\uc740 *\ub04c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4*. Home Assistant\uc758 \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\ub9cc \uaebc\uc9d1\ub2c8\ub2e4!",
+ "title": "Apple TV \ucd94\uac00 \ud655\uc778\ud558\uae30"
+ },
+ "pair_no_pin": {
+ "description": "`{protocol}` \uc11c\ube44\uc2a4\uc5d0 \ub300\ud55c \ud398\uc5b4\ub9c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uacc4\uc18d\ud558\ub824\uba74 Apple TV\uc5d0 PIN {pin}\uc744(\ub97c) \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "\ud398\uc5b4\ub9c1\ud558\uae30"
+ },
+ "pair_with_pin": {
+ "data": {
+ "pin": "PIN \ucf54\ub4dc"
+ },
+ "description": "`{protocol}` \ud504\ub85c\ud1a0\ucf5c\uc5d0 \ub300\ud55c \ud398\uc5b4\ub9c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \ud654\uba74\uc5d0 \ud45c\uc2dc\ub418\ub294 PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc55e\uc790\ub9ac\uc758 0\uc740 \uc0dd\ub7b5\ub418\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc989, \ud45c\uc2dc\ub41c \ucf54\ub4dc\uac00 0123\uc774\uba74 123\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "\ud398\uc5b4\ub9c1\ud558\uae30"
+ },
+ "reconfigure": {
+ "description": "\uc774 Apple TV\uc5d0 \uc5f0\uacb0 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud558\uc5ec \ub2e4\uc2dc \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4.",
+ "title": "\uae30\uae30 \uc7ac\uad6c\uc131"
+ },
+ "service_problem": {
+ "description": "\ud504\ub85c\ud1a0\ucf5c `{protocol}`\uc744(\ub97c) \ud398\uc5b4\ub9c1\ud558\ub294 \ub3d9\uc548 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\ub294 \ubb34\uc2dc\ub429\ub2c8\ub2e4.",
+ "title": "\uc11c\ube44\uc2a4\ub97c \ucd94\uac00\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "user": {
+ "data": {
+ "device_input": "\uae30\uae30"
+ },
+ "description": "\uba3c\uc800 \ucd94\uac00\ud560 Apple TV\uc758 \uae30\uae30 \uc774\ub984(\uc608: \uc8fc\ubc29 \ub610\ub294 \uce68\uc2e4) \ub610\ub294 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uc7a5\uce58\uac00 \uc790\ub3d9\uc73c\ub85c \ubc1c\uacac\ub41c \uacbd\uc6b0 \ub2e4\uc74c\uacfc \uac19\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4.\n\n\uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uac70\ub098 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud55c \uacbd\uc6b0 \uae30\uae30 IP \uc8fc\uc18c\ub97c \uc9c1\uc811 \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\n{devices}",
+ "title": "\uc0c8\ub85c\uc6b4 Apple TV \uc124\uc815\ud558\uae30"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "start_off": "Home Assistant\ub97c \uc2dc\uc791\ud560 \ub54c \uae30\uae30\ub97c \ucf1c\uc9c0 \ub9d0\uc544 \uc8fc\uc138\uc694"
+ },
+ "description": "\uc77c\ubc18 \uae30\uae30 \uc124\uc815 \uad6c\uc131"
+ }
+ }
+ },
+ "title": "Apple TV"
+}
\ No newline at end of file
diff --git a/homeassistant/components/apple_tv/translations/lb.json b/homeassistant/components/apple_tv/translations/lb.json
index 945f467c4cf84f..2354033b577050 100644
--- a/homeassistant/components/apple_tv/translations/lb.json
+++ b/homeassistant/components/apple_tv/translations/lb.json
@@ -3,9 +3,14 @@
"abort": {
"already_configured_device": "Apparat ass scho konfigur\u00e9iert",
"already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang",
+ "invalid_config": "Konfiguratioun fir d\u00ebsen Apparat ass net komplett. Prob\u00e9ier fir et nach emol dob\u00e4i ze setzen.",
+ "no_devices_found": "Keng Apparater am Netzwierk fonnt",
"unknown": "Onerwaarte Feeler"
},
"error": {
+ "already_configured": "Apparat ass scho konfigur\u00e9iert",
+ "invalid_auth": "Ong\u00eblteg Authentifikatioun",
+ "no_devices_found": "Keng Apparater am Netzwierk fonnt",
"unknown": "Onerwaarte Feeler"
},
"flow_title": "Apple TV: {name}",
@@ -29,6 +34,9 @@
"description": "D\u00ebsen Apple TV huet e puer Verbindungsschwieregkeeten a muss nei konfigur\u00e9iert ginn.",
"title": "Apparat Rekonfiguratioun"
},
+ "service_problem": {
+ "title": "Feeler beim dob\u00e4isetze vum Service"
+ },
"user": {
"data": {
"device_input": "Apparat"
diff --git a/homeassistant/components/apple_tv/translations/nl.json b/homeassistant/components/apple_tv/translations/nl.json
index a11488ebca979e..e313e972188798 100644
--- a/homeassistant/components/apple_tv/translations/nl.json
+++ b/homeassistant/components/apple_tv/translations/nl.json
@@ -1,9 +1,64 @@
{
"config": {
"abort": {
+ "already_configured_device": "Apparaat is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
"backoff": "Het apparaat accepteert op dit moment geen koppelingsverzoeken (u heeft mogelijk te vaak een ongeldige pincode ingevoerd), probeer het later opnieuw.",
"device_did_not_pair": "Er is geen poging gedaan om het koppelingsproces te voltooien vanaf het apparaat.",
- "invalid_config": "De configuratie voor dit apparaat is onvolledig. Probeer het opnieuw toe te voegen."
+ "invalid_config": "De configuratie voor dit apparaat is onvolledig. Probeer het opnieuw toe te voegen.",
+ "no_devices_found": "Geen apparaten gevonden op het netwerk",
+ "unknown": "Onverwachte fout"
+ },
+ "error": {
+ "already_configured": "Apparaat is al geconfigureerd",
+ "invalid_auth": "Ongeldige authenticatie",
+ "no_devices_found": "Geen apparaten gevonden op het netwerk",
+ "no_usable_service": "Er is een apparaat gevonden, maar er kon geen manier worden gevonden om er verbinding mee te maken. Als u dit bericht blijft zien, probeert u het IP-adres in te voeren of uw Apple TV opnieuw op te starten.",
+ "unknown": "Onverwachte fout"
+ },
+ "flow_title": "Apple TV: {name}",
+ "step": {
+ "confirm": {
+ "description": "U staat op het punt om de Apple TV met de naam `{name}` toe te voegen aan Home Assistant.\n\n**Om het proces te voltooien, moet u mogelijk meerdere PIN-codes invoeren.**\n\nLet op: u kunt uw Apple TV *niet* uitschakelen met deze integratie. Alleen de mediaspeler in Home Assistant wordt uitgeschakeld!",
+ "title": "Bevestig het toevoegen van Apple TV"
+ },
+ "pair_no_pin": {
+ "description": "Koppeling is vereist voor de `{protocol}` service. Voer de PIN {pin} in op uw Apple TV om verder te gaan.",
+ "title": "Koppelen"
+ },
+ "pair_with_pin": {
+ "data": {
+ "pin": "PIN-code"
+ },
+ "description": "Koppelen is vereist voor het `{protocol}` protocol. Voer de PIN-code in die op het scherm wordt getoond. Beginnende nullen moeten worden weggelaten, d.w.z. voer 123 in als de getoonde code 0123 is.",
+ "title": "Koppelen"
+ },
+ "reconfigure": {
+ "description": "Deze Apple TV ondervindt verbindingsproblemen en moet opnieuw worden geconfigureerd.",
+ "title": "Apparaat herconfiguratie"
+ },
+ "service_problem": {
+ "description": "Er is een probleem opgetreden tijdens het koppelen van protocol `{protocol}`. Dit wordt genegeerd.",
+ "title": "Dienst toevoegen mislukt"
+ },
+ "user": {
+ "data": {
+ "device_input": "Apparaat"
+ },
+ "description": "Begin met het invoeren van de apparaatnaam (bijv. Keuken of Slaapkamer) of het IP-adres van de Apple TV die u wilt toevoegen. Als er automatisch apparaten in uw netwerk zijn gevonden, worden deze hieronder weergegeven.\n\nAls u het apparaat niet kunt zien of problemen ondervindt, probeer dan het IP-adres van het apparaat in te voeren.\n\n{devices}",
+ "title": "Stel een nieuwe Apple TV in"
+ }
}
- }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "start_off": "Schakel het apparaat niet in wanneer u Home Assistant start"
+ },
+ "description": "Algemene apparaatinstellingen configureren"
+ }
+ }
+ },
+ "title": "Apple TV"
}
\ No newline at end of file
diff --git a/homeassistant/components/apple_tv/translations/ru.json b/homeassistant/components/apple_tv/translations/ru.json
index e3f5804cebef5c..4ad9b9f52c762d 100644
--- a/homeassistant/components/apple_tv/translations/ru.json
+++ b/homeassistant/components/apple_tv/translations/ru.json
@@ -11,7 +11,7 @@
},
"error": {
"already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.",
"no_usable_service": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u0415\u0441\u043b\u0438 \u0412\u044b \u0443\u0436\u0435 \u0432\u0438\u0434\u0435\u043b\u0438 \u044d\u0442\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u0435\u0433\u043e.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
diff --git a/homeassistant/components/apple_tv/translations/tr.json b/homeassistant/components/apple_tv/translations/tr.json
index 0ddc466a6f7662..f33e3998af6279 100644
--- a/homeassistant/components/apple_tv/translations/tr.json
+++ b/homeassistant/components/apple_tv/translations/tr.json
@@ -1,6 +1,8 @@
{
"config": {
"abort": {
+ "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor",
"invalid_config": "Bu ayg\u0131t\u0131n yap\u0131land\u0131rmas\u0131 tamamlanmad\u0131. L\u00fctfen tekrar eklemeyi deneyin.",
"no_devices_found": "A\u011fda cihaz bulunamad\u0131",
"unknown": "Beklenmeyen hata"
diff --git a/homeassistant/components/apple_tv/translations/uk.json b/homeassistant/components/apple_tv/translations/uk.json
new file mode 100644
index 00000000000000..a1ae2259ada604
--- /dev/null
+++ b/homeassistant/components/apple_tv/translations/uk.json
@@ -0,0 +1,64 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_device": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "backoff": "\u0412 \u0434\u0430\u043d\u0438\u0439 \u0447\u0430\u0441 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0440\u0438\u0439\u043c\u0430\u0454 \u0437\u0430\u043f\u0438\u0442\u0438 \u043d\u0430 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438 (\u043c\u043e\u0436\u043b\u0438\u0432\u043e, \u0412\u0438 \u0437\u0430\u043d\u0430\u0434\u0442\u043e \u0431\u0430\u0433\u0430\u0442\u043e \u0440\u0430\u0437 \u0432\u0432\u043e\u0434\u0438\u043b\u0438 \u043d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 PIN-\u043a\u043e\u0434), \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437 \u043f\u0456\u0437\u043d\u0456\u0448\u0435.",
+ "device_did_not_pair": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043d\u0430\u043c\u0430\u0433\u0430\u0432\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043f\u0440\u043e\u0446\u0435\u0441 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438.",
+ "invalid_config": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430. \u0421\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u0439\u043e\u0433\u043e \u0449\u0435 \u0440\u0430\u0437.",
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "error": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.",
+ "no_usable_service": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 \u0441\u043f\u043e\u0441\u0456\u0431 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \u042f\u043a\u0449\u043e \u0412\u0438 \u0432\u0436\u0435 \u0431\u0430\u0447\u0438\u043b\u0438 \u0446\u0435 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0432\u043a\u0430\u0437\u0430\u0442\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0430\u0431\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c \u0439\u043e\u0433\u043e.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "flow_title": "Apple TV: {name}",
+ "step": {
+ "confirm": {
+ "description": "\u0412\u0438 \u0437\u0431\u0438\u0440\u0430\u0454\u0442\u0435\u0441\u044f \u0434\u043e\u0434\u0430\u0442\u0438 Apple TV `{name}` \u0432 Home Assistant. \n\n ** \u0414\u043b\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044f \u043f\u0440\u043e\u0446\u0435\u0441\u0443 \u0412\u0430\u043c \u043c\u043e\u0436\u0435 \u0437\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0438\u0441\u044f \u0432\u0432\u0435\u0441\u0442\u0438 \u043a\u0456\u043b\u044c\u043a\u0430 PIN-\u043a\u043e\u0434\u0456\u0432. ** \n\n\u0417\u0432\u0435\u0440\u043d\u0456\u0442\u044c \u0443\u0432\u0430\u0433\u0443, \u0449\u043e \u0412\u0438 *\u043d\u0435* \u0437\u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u0438\u043c\u0438\u043a\u0430\u0442\u0438 Apple TV \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0446\u0456\u0454\u0457 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457. \u0412 Home Assistant \u043c\u043e\u0436\u043b\u0438\u0432\u043e \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438 \u0442\u0456\u043b\u044c\u043a\u0438 \u043c\u0435\u0434\u0456\u0430\u043f\u0440\u043e\u0433\u0440\u0430\u0432\u0430\u0447!",
+ "title": "\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0456\u0442\u044c \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f Apple TV"
+ },
+ "pair_no_pin": {
+ "description": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0434\u043b\u044f \u0441\u043b\u0443\u0436\u0431\u0438 `{protocol}`. \u0414\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u0432\u0436\u0435\u043d\u043d\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434 {pin} \u043d\u0430 \u0412\u0430\u0448\u043e\u043c\u0443 Apple TV.",
+ "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f"
+ },
+ "pair_with_pin": {
+ "data": {
+ "pin": "PIN-\u043a\u043e\u0434"
+ },
+ "description": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 `{protocol}`. \u0412\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434, \u044f\u043a\u0438\u0439 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456. \u041f\u0435\u0440\u0448\u0456 \u043d\u0443\u043b\u0456 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u043e\u043f\u0443\u0449\u0435\u043d\u0456, \u0442\u043e\u0431\u0442\u043e \u0432\u0432\u0435\u0434\u0456\u0442\u044c 123, \u044f\u043a\u0449\u043e \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043a\u043e\u0434 0123.",
+ "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f"
+ },
+ "reconfigure": {
+ "description": "\u0423 \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Apple TV \u0432\u0438\u043d\u0438\u043a\u0430\u044e\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u043f\u0440\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456, \u0439\u043e\u0433\u043e \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043f\u0435\u0440\u0435\u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438.",
+ "title": "\u041f\u0435\u0440\u0435\u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ },
+ "service_problem": {
+ "description": "\u0412\u0438\u043d\u0438\u043a\u043b\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043f\u0440\u0438 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u0456 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 `{protocol}`. \u0426\u0435 \u0431\u0443\u0434\u0435 \u043f\u0440\u043e\u0456\u0433\u043d\u043e\u0440\u043e\u0432\u0430\u043d\u043e.",
+ "title": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0434\u043e\u0434\u0430\u0442\u0438 \u0441\u043b\u0443\u0436\u0431\u0443"
+ },
+ "user": {
+ "data": {
+ "device_input": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439"
+ },
+ "description": "\u041f\u043e\u0447\u043d\u0456\u0442\u044c \u0437 \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044f \u043d\u0430\u0437\u0432\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434, \u041a\u0443\u0445\u043d\u044f \u0430\u0431\u043e \u0421\u043f\u0430\u043b\u044c\u043d\u044f) \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0438 Apple TV, \u044f\u043a\u0443 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438. \u042f\u043a\u0449\u043e \u0431\u0443\u0434\u044c-\u044f\u043a\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0431\u0443\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u0456 \u0443 \u0412\u0430\u0448\u0456\u0439 \u043c\u0435\u0440\u0435\u0436\u0456, \u0432\u043e\u043d\u0438 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0456 \u043d\u0438\u0436\u0447\u0435. \n\n \u042f\u043a\u0449\u043e \u0412\u0438 \u043d\u0435 \u0431\u0430\u0447\u0438\u0442\u0435 \u0441\u0432\u0456\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0430\u0431\u043e \u0432\u0438\u043d\u0438\u043a\u0430\u044e\u0442\u044c \u0431\u0443\u0434\u044c-\u044f\u043a\u0456 \u0456\u043d\u0448\u0456 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u043f\u0456\u0434 \u0447\u0430\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0432\u043a\u0430\u0437\u0430\u0442\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \n\n {devices}",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043d\u043e\u0432\u043e\u0433\u043e Apple TV"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "start_off": "\u041d\u0435 \u0432\u043c\u0438\u043a\u0430\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0443 Home Assistant"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ }
+ }
+ },
+ "title": "Apple TV"
+}
\ No newline at end of file
diff --git a/homeassistant/components/apple_tv/translations/zh-Hans.json b/homeassistant/components/apple_tv/translations/zh-Hans.json
index bb1f8e025cad7a..54095a0a633679 100644
--- a/homeassistant/components/apple_tv/translations/zh-Hans.json
+++ b/homeassistant/components/apple_tv/translations/zh-Hans.json
@@ -1,6 +1,9 @@
{
"config": {
"step": {
+ "confirm": {
+ "description": "\u60a8\u5373\u5c06\u6dfb\u52a0 Apple TV (\u540d\u79f0\u4e3a\u201c{name}\u201d)\u5230 Home Assistant\u3002 \n\n **\u8981\u5b8c\u6210\u6b64\u8fc7\u7a0b\uff0c\u53ef\u80fd\u9700\u8981\u8f93\u5165\u591a\u4e2a PIN \u7801\u3002** \n\n\u8bf7\u6ce8\u610f\uff0c\u6b64\u96c6\u6210*\u4e0d\u80fd*\u5173\u95ed Apple TV \u7684\u7535\u6e90\uff0c\u53ea\u4f1a\u5173\u95ed Home Assistant \u4e2d\u7684\u5a92\u4f53\u64ad\u653e\u5668\uff01"
+ },
"pair_no_pin": {
"title": "\u914d\u5bf9\u4e2d"
},
@@ -8,6 +11,20 @@
"data": {
"pin": "PIN\u7801"
}
+ },
+ "user": {
+ "description": "\u8981\u5f00\u59cb\uff0c\u8bf7\u8f93\u5165\u8981\u6dfb\u52a0\u7684 Apple TV \u7684\u8bbe\u5907\u540d\u79f0\u6216 IP \u5730\u5740\u3002\u5728\u7f51\u7edc\u4e0a\u81ea\u52a8\u53d1\u73b0\u7684\u8bbe\u5907\u4f1a\u663e\u793a\u5728\u4e0b\u65b9\u3002 \n\n\u5982\u679c\u6ca1\u6709\u53d1\u73b0\u8bbe\u5907\u6216\u9047\u5230\u4efb\u4f55\u95ee\u9898\uff0c\u8bf7\u5c1d\u8bd5\u6307\u5b9a\u8bbe\u5907 IP \u5730\u5740\u3002 \n\n {devices}",
+ "title": "\u8bbe\u7f6e\u65b0\u7684 Apple TV"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "start_off": "\u542f\u52a8 Home Assistant \u65f6\u4e0d\u6253\u5f00\u8bbe\u5907"
+ },
+ "description": "\u914d\u7f6e\u8bbe\u5907\u901a\u7528\u8bbe\u7f6e"
}
}
},
diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py
index 95bf11ddc097ae..5f4a6b666430ff 100644
--- a/homeassistant/components/apprise/notify.py
+++ b/homeassistant/components/apprise/notify.py
@@ -42,11 +42,10 @@ def get_service(hass, config, discovery_info=None):
_LOGGER.error("Invalid Apprise config url provided")
return None
- if config.get(CONF_URL):
- # Ordered list of URLs
- if not a_obj.add(config[CONF_URL]):
- _LOGGER.error("Invalid Apprise URL(s) supplied")
- return None
+ # Ordered list of URLs
+ if config.get(CONF_URL) and not a_obj.add(config[CONF_URL]):
+ _LOGGER.error("Invalid Apprise URL(s) supplied")
+ return None
return AppriseNotificationService(a_obj)
diff --git a/homeassistant/components/aqualogic/manifest.json b/homeassistant/components/aqualogic/manifest.json
index 2a8e2a78cacdba..5a753342b2bcb6 100644
--- a/homeassistant/components/aqualogic/manifest.json
+++ b/homeassistant/components/aqualogic/manifest.json
@@ -2,6 +2,6 @@
"domain": "aqualogic",
"name": "AquaLogic",
"documentation": "https://www.home-assistant.io/integrations/aqualogic",
- "requirements": ["aqualogic==1.0"],
+ "requirements": ["aqualogic==2.6"],
"codeowners": []
}
diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py
index f92cd11c0118f8..315b039f778af9 100644
--- a/homeassistant/components/aqualogic/sensor.py
+++ b/homeassistant/components/aqualogic/sensor.py
@@ -2,7 +2,7 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_MONITORED_CONDITIONS,
PERCENTAGE,
@@ -12,7 +12,6 @@
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from . import DOMAIN, UPDATE_TOPIC
@@ -56,7 +55,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(sensors)
-class AquaLogicSensor(Entity):
+class AquaLogicSensor(SensorEntity):
"""Sensor implementation for the AquaLogic component."""
def __init__(self, processor, sensor_type):
diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py
index 686e7c2de16d48..fe62c41c061c17 100644
--- a/homeassistant/components/arcam_fmj/__init__.py
+++ b/homeassistant/components/arcam_fmj/__init__.py
@@ -1,5 +1,6 @@
"""Arcam component."""
import asyncio
+from contextlib import suppress
import logging
from arcam.fmj import ConnectionFailed
@@ -28,10 +29,8 @@
async def _await_cancel(task):
task.cancel()
- try:
+ with suppress(asyncio.CancelledError):
await task
- except asyncio.CancelledError:
- pass
async def async_setup(hass: HomeAssistantType, config: ConfigType):
diff --git a/homeassistant/components/arcam_fmj/config_flow.py b/homeassistant/components/arcam_fmj/config_flow.py
index d270af9295b43c..31735a0a037a8e 100644
--- a/homeassistant/components/arcam_fmj/config_flow.py
+++ b/homeassistant/components/arcam_fmj/config_flow.py
@@ -71,7 +71,7 @@ async def async_step_user(self, user_input=None):
async def async_step_confirm(self, user_input=None):
"""Handle user-confirmation of discovered node."""
- context = self.context # pylint: disable=no-member
+ context = self.context
placeholders = {
"host": context[CONF_HOST],
}
@@ -94,7 +94,7 @@ async def async_step_ssdp(self, discovery_info):
await self._async_set_unique_id_and_update(host, port, uuid)
- context = self.context # pylint: disable=no-member
+ context = self.context
context[CONF_HOST] = host
context[CONF_PORT] = DEFAULT_PORT
return await self.async_step_confirm()
diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py
index c03a082c149f44..4ae34abb2c2a6a 100644
--- a/homeassistant/components/arcam_fmj/device_trigger.py
+++ b/homeassistant/components/arcam_fmj/device_trigger.py
@@ -1,5 +1,5 @@
"""Provides device automations for Arcam FMJ Receiver control."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -28,7 +28,7 @@
)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers for Arcam FMJ Receiver control devices."""
registry = await entity_registry.async_get_registry(hass)
triggers = []
@@ -56,7 +56,7 @@ async def async_attach_trigger(
automation_info: dict,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
- config = TRIGGER_SCHEMA(config)
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
job = HassJob(action)
if config[CONF_TYPE] == "turn_on":
@@ -67,7 +67,13 @@ def _handle_event(event: Event):
if event.data[ATTR_ENTITY_ID] == entity_id:
hass.async_run_hass_job(
job,
- {"trigger": {**config, "description": f"{DOMAIN} - {entity_id}"}},
+ {
+ "trigger": {
+ **config,
+ "description": f"{DOMAIN} - {entity_id}",
+ "id": trigger_id,
+ }
+ },
event.context,
)
diff --git a/homeassistant/components/arcam_fmj/translations/de.json b/homeassistant/components/arcam_fmj/translations/de.json
index 92ad0e22663046..1f67a8d30a94ea 100644
--- a/homeassistant/components/arcam_fmj/translations/de.json
+++ b/homeassistant/components/arcam_fmj/translations/de.json
@@ -2,9 +2,14 @@
"config": {
"abort": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
- "cannot_connect": "Verbindungsfehler"
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
+ "flow_title": "Arcam FMJ auf {host}",
"step": {
+ "confirm": {
+ "description": "M\u00f6chtest du Arcam FMJ auf `{host}` zum Home Assistant hinzuf\u00fcgen?"
+ },
"user": {
"data": {
"host": "Host",
diff --git a/homeassistant/components/arcam_fmj/translations/hu.json b/homeassistant/components/arcam_fmj/translations/hu.json
index 563ede561557c5..4af1181a265b2b 100644
--- a/homeassistant/components/arcam_fmj/translations/hu.json
+++ b/homeassistant/components/arcam_fmj/translations/hu.json
@@ -1,7 +1,17 @@
{
"config": {
"abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "port": "Port"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/arcam_fmj/translations/id.json b/homeassistant/components/arcam_fmj/translations/id.json
new file mode 100644
index 00000000000000..96b10140948bc5
--- /dev/null
+++ b/homeassistant/components/arcam_fmj/translations/id.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "cannot_connect": "Gagal terhubung"
+ },
+ "flow_title": "Arcam FMJ di {host}",
+ "step": {
+ "confirm": {
+ "description": "Ingin menambahkan Arcam FMJ `{host}` ke Home Assistant?"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ },
+ "description": "Masukkan nama host atau alamat IP perangkat."
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "{entity_name} diminta untuk dinyalakan"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/arcam_fmj/translations/ko.json b/homeassistant/components/arcam_fmj/translations/ko.json
index 62b5a54928ef35..29a2887f7e2e8d 100644
--- a/homeassistant/components/arcam_fmj/translations/ko.json
+++ b/homeassistant/components/arcam_fmj/translations/ko.json
@@ -2,12 +2,13 @@
"config": {
"abort": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4."
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
"flow_title": "Arcam FMJ: {host}",
"step": {
"confirm": {
- "description": "Home Assistant \uc5d0 Arcam FMJ `{host}` \uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ "description": "Home Assistant\uc5d0 Arcam FMJ `{host}`\uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
},
"user": {
"data": {
@@ -20,7 +21,7 @@
},
"device_automation": {
"trigger_type": {
- "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c0\ub3c4\ub85d \uc694\uccad\ub418\uc5c8\uc744 \ub54c"
+ "turn_on": "{entity_name}\uc774(\uac00) \ucf1c\uc9c0\ub3c4\ub85d \uc694\uccad\ub418\uc5c8\uc744 \ub54c"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/arcam_fmj/translations/nl.json b/homeassistant/components/arcam_fmj/translations/nl.json
index 5607b426cc988f..03465d5c53df4e 100644
--- a/homeassistant/components/arcam_fmj/translations/nl.json
+++ b/homeassistant/components/arcam_fmj/translations/nl.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "Apparaat is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
"cannot_connect": "Kan geen verbinding maken"
},
"error": {
diff --git a/homeassistant/components/arcam_fmj/translations/pt.json b/homeassistant/components/arcam_fmj/translations/pt.json
index 097e3d086d6494..af72dfe96e2c34 100644
--- a/homeassistant/components/arcam_fmj/translations/pt.json
+++ b/homeassistant/components/arcam_fmj/translations/pt.json
@@ -1,6 +1,8 @@
{
"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",
"cannot_connect": "Falha na liga\u00e7\u00e3o"
},
"error": {
diff --git a/homeassistant/components/arcam_fmj/translations/ro.json b/homeassistant/components/arcam_fmj/translations/ro.json
new file mode 100644
index 00000000000000..a8008f1e8bcd5c
--- /dev/null
+++ b/homeassistant/components/arcam_fmj/translations/ro.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "error": {
+ "few": "Pu\u021bine",
+ "one": "Unul",
+ "other": "Altele"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/arcam_fmj/translations/ru.json b/homeassistant/components/arcam_fmj/translations/ru.json
index bdd59b39067e63..8b3c3092745536 100644
--- a/homeassistant/components/arcam_fmj/translations/ru.json
+++ b/homeassistant/components/arcam_fmj/translations/ru.json
@@ -21,7 +21,7 @@
},
"device_automation": {
"trigger_type": {
- "turn_on": "\u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}"
+ "turn_on": "\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/arcam_fmj/translations/tr.json b/homeassistant/components/arcam_fmj/translations/tr.json
new file mode 100644
index 00000000000000..dd15f57212caa4
--- /dev/null
+++ b/homeassistant/components/arcam_fmj/translations/tr.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor",
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/arcam_fmj/translations/uk.json b/homeassistant/components/arcam_fmj/translations/uk.json
new file mode 100644
index 00000000000000..4d33a5bc0d9733
--- /dev/null
+++ b/homeassistant/components/arcam_fmj/translations/uk.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "flow_title": "Arcam FMJ {host}",
+ "step": {
+ "confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 Arcam FMJ `{host}`?"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e."
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "\u0437\u0430\u043f\u0438\u0442\u0430\u043d\u043e \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/arduino/sensor.py b/homeassistant/components/arduino/sensor.py
index f31272b3a2014e..588a652660aad0 100644
--- a/homeassistant/components/arduino/sensor.py
+++ b/homeassistant/components/arduino/sensor.py
@@ -1,10 +1,9 @@
"""Support for getting information from Arduino pins."""
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from . import DOMAIN
@@ -30,7 +29,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors)
-class ArduinoSensor(Entity):
+class ArduinoSensor(SensorEntity):
"""Representation of an Arduino Sensor."""
def __init__(self, name, pin, pin_type, board):
diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py
index d213a3d2903e15..061c15eafb04bb 100644
--- a/homeassistant/components/arest/sensor.py
+++ b/homeassistant/components/arest/sensor.py
@@ -5,7 +5,7 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_MONITORED_VARIABLES,
CONF_NAME,
@@ -16,7 +16,6 @@
)
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -124,7 +123,7 @@ def _render(value):
add_entities(dev, True)
-class ArestSensor(Entity):
+class ArestSensor(SensorEntity):
"""Implementation of an aREST sensor for exposed variables."""
def __init__(
diff --git a/homeassistant/components/arlo/alarm_control_panel.py b/homeassistant/components/arlo/alarm_control_panel.py
index 47328d5cbc2234..dd899cbd04ff27 100644
--- a/homeassistant/components/arlo/alarm_control_panel.py
+++ b/homeassistant/components/arlo/alarm_control_panel.py
@@ -136,7 +136,7 @@ def name(self):
return self._base_station.name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py
index 36e321817022ac..c1848661429888 100644
--- a/homeassistant/components/arlo/camera.py
+++ b/homeassistant/components/arlo/camera.py
@@ -108,7 +108,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
name: value
diff --git a/homeassistant/components/arlo/sensor.py b/homeassistant/components/arlo/sensor.py
index d5d583de22c9c4..c794bf1ef5e751 100644
--- a/homeassistant/components/arlo/sensor.py
+++ b/homeassistant/components/arlo/sensor.py
@@ -3,7 +3,7 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONCENTRATION_PARTS_PER_MILLION,
@@ -16,7 +16,6 @@
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level
from . import ATTRIBUTION, DATA_ARLO, DEFAULT_BRAND, SIGNAL_UPDATE_ARLO
@@ -73,7 +72,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class ArloSensor(Entity):
+class ArloSensor(SensorEntity):
"""An implementation of a Netgear Arlo IP sensor."""
def __init__(self, name, device, sensor_type):
@@ -183,7 +182,7 @@ def update(self):
self._state = None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
attrs = {}
diff --git a/homeassistant/components/arris_tg2492lg/device_tracker.py b/homeassistant/components/arris_tg2492lg/device_tracker.py
index e63bef9c108564..1011d76f8aa128 100644
--- a/homeassistant/components/arris_tg2492lg/device_tracker.py
+++ b/homeassistant/components/arris_tg2492lg/device_tracker.py
@@ -1,5 +1,5 @@
"""Support for Arris TG2492LG router."""
-from typing import List
+from __future__ import annotations
from arris_tg2492lg import ConnectBox, Device
import voluptuous as vol
@@ -36,7 +36,7 @@ class ArrisDeviceScanner(DeviceScanner):
def __init__(self, connect_box: ConnectBox):
"""Initialize the scanner."""
self.connect_box = connect_box
- self.last_results: List[Device] = []
+ self.last_results: list[Device] = []
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py
index 18186e8b87192d..ba9166d1af585e 100644
--- a/homeassistant/components/arwn/sensor.py
+++ b/homeassistant/components/arwn/sensor.py
@@ -3,9 +3,9 @@
import logging
from homeassistant.components import mqtt
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEGREE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import callback
-from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__)
@@ -114,7 +114,7 @@ def async_sensor_event_received(msg):
return True
-class ArwnSensor(Entity):
+class ArwnSensor(SensorEntity):
"""Representation of an ARWN sensor."""
def __init__(self, topic, name, state_key, units, icon=None):
@@ -154,7 +154,7 @@ def unique_id(self):
return self._uid
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return all the state attributes."""
return self.event
diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py
index 1829d00a353cb2..25a78f6a523ee6 100644
--- a/homeassistant/components/asuswrt/__init__.py
+++ b/homeassistant/components/asuswrt/__init__.py
@@ -1,120 +1,174 @@
"""Support for ASUSWRT devices."""
-import logging
+import asyncio
-from aioasuswrt.asuswrt import AsusWrt
import voluptuous as vol
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_MODE,
CONF_PASSWORD,
CONF_PORT,
CONF_PROTOCOL,
+ CONF_SENSORS,
CONF_USERNAME,
+ EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.discovery import async_load_platform
-from homeassistant.helpers.event import async_call_later
+from homeassistant.helpers.typing import HomeAssistantType
+
+from .const import (
+ CONF_DNSMASQ,
+ CONF_INTERFACE,
+ CONF_REQUIRE_IP,
+ CONF_SSH_KEY,
+ DATA_ASUSWRT,
+ DEFAULT_DNSMASQ,
+ DEFAULT_INTERFACE,
+ DEFAULT_SSH_PORT,
+ DOMAIN,
+ MODE_AP,
+ MODE_ROUTER,
+ PROTOCOL_SSH,
+ PROTOCOL_TELNET,
+)
+from .router import AsusWrtRouter
-_LOGGER = logging.getLogger(__name__)
+PLATFORMS = ["device_tracker", "sensor"]
-CONF_DNSMASQ = "dnsmasq"
-CONF_INTERFACE = "interface"
CONF_PUB_KEY = "pub_key"
-CONF_REQUIRE_IP = "require_ip"
-CONF_SENSORS = "sensors"
-CONF_SSH_KEY = "ssh_key"
-
-DOMAIN = "asuswrt"
-DATA_ASUSWRT = DOMAIN
-
-DEFAULT_SSH_PORT = 22
-DEFAULT_INTERFACE = "eth0"
-DEFAULT_DNSMASQ = "/var/lib/misc"
-
-FIRST_RETRY_TIME = 60
-MAX_RETRY_TIME = 900
-
SECRET_GROUP = "Password or SSH Key"
SENSOR_TYPES = ["devices", "upload_speed", "download_speed", "download", "upload"]
CONFIG_SCHEMA = vol.Schema(
- {
- DOMAIN: vol.Schema(
- {
- vol.Required(CONF_HOST): cv.string,
- vol.Required(CONF_USERNAME): cv.string,
- vol.Optional(CONF_PROTOCOL, default="ssh"): vol.In(["ssh", "telnet"]),
- vol.Optional(CONF_MODE, default="router"): vol.In(["router", "ap"]),
- vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port,
- vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean,
- vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string,
- vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile,
- vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile,
- vol.Optional(CONF_SENSORS): vol.All(
- cv.ensure_list, [vol.In(SENSOR_TYPES)]
- ),
- vol.Optional(CONF_INTERFACE, default=DEFAULT_INTERFACE): cv.string,
- vol.Optional(CONF_DNSMASQ, default=DEFAULT_DNSMASQ): cv.string,
- }
- )
- },
+ vol.All(
+ cv.deprecated(DOMAIN),
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Required(CONF_HOST): cv.string,
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Optional(CONF_PROTOCOL, default=PROTOCOL_SSH): vol.In(
+ [PROTOCOL_SSH, PROTOCOL_TELNET]
+ ),
+ vol.Optional(CONF_MODE, default=MODE_ROUTER): vol.In(
+ [MODE_ROUTER, MODE_AP]
+ ),
+ vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port,
+ vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean,
+ vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string,
+ vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile,
+ vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile,
+ vol.Optional(CONF_SENSORS): vol.All(
+ cv.ensure_list, [vol.In(SENSOR_TYPES)]
+ ),
+ vol.Optional(CONF_INTERFACE, default=DEFAULT_INTERFACE): cv.string,
+ vol.Optional(CONF_DNSMASQ, default=DEFAULT_DNSMASQ): cv.string,
+ }
+ )
+ },
+ ),
extra=vol.ALLOW_EXTRA,
)
-async def async_setup(hass, config, retry_delay=FIRST_RETRY_TIME):
- """Set up the asuswrt component."""
+async def async_setup(hass, config):
+ """Set up the AsusWrt integration."""
+ conf = config.get(DOMAIN)
+ if conf is None:
+ return True
+
+ # save the options from config yaml
+ options = {}
+ mode = conf.get(CONF_MODE, MODE_ROUTER)
+ for name, value in conf.items():
+ if name in ([CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]):
+ if name == CONF_REQUIRE_IP and mode != MODE_AP:
+ continue
+ options[name] = value
+ hass.data[DOMAIN] = {"yaml_options": options}
+
+ # check if already configured
+ domains_list = hass.config_entries.async_domains()
+ if DOMAIN in domains_list:
+ return True
- conf = config[DOMAIN]
+ # remove not required config keys
+ pub_key = conf.pop(CONF_PUB_KEY, "")
+ if pub_key:
+ conf[CONF_SSH_KEY] = pub_key
- api = AsusWrt(
- conf[CONF_HOST],
- conf[CONF_PORT],
- conf[CONF_PROTOCOL] == "telnet",
- conf[CONF_USERNAME],
- conf.get(CONF_PASSWORD, ""),
- conf.get("ssh_key", conf.get("pub_key", "")),
- conf[CONF_MODE],
- conf[CONF_REQUIRE_IP],
- interface=conf[CONF_INTERFACE],
- dnsmasq=conf[CONF_DNSMASQ],
- )
+ conf.pop(CONF_REQUIRE_IP, True)
+ conf.pop(CONF_SENSORS, {})
+ conf.pop(CONF_INTERFACE, "")
+ conf.pop(CONF_DNSMASQ, "")
- try:
- await api.connection.async_connect()
- except OSError as ex:
- _LOGGER.warning(
- "Error [%s] connecting %s to %s. Will retry in %s seconds...",
- str(ex),
- DOMAIN,
- conf[CONF_HOST],
- retry_delay,
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
)
+ )
- async def retry_setup(now):
- """Retry setup if a error happens on asuswrt API."""
- await async_setup(
- hass, config, retry_delay=min(2 * retry_delay, MAX_RETRY_TIME)
- )
+ return True
- async_call_later(hass, retry_delay, retry_setup)
- return True
+async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
+ """Set up AsusWrt platform."""
- if not api.is_connected:
- _LOGGER.error("Error connecting %s to %s", DOMAIN, conf[CONF_HOST])
- return False
+ # import options from yaml if empty
+ yaml_options = hass.data.get(DOMAIN, {}).pop("yaml_options", {})
+ if not entry.options and yaml_options:
+ hass.config_entries.async_update_entry(entry, options=yaml_options)
- hass.data[DATA_ASUSWRT] = api
+ router = AsusWrtRouter(hass, entry)
+ await router.setup()
- hass.async_create_task(
- async_load_platform(
- hass, "sensor", DOMAIN, config[DOMAIN].get(CONF_SENSORS), config
+ router.async_on_close(entry.add_update_listener(update_listener))
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
- )
- hass.async_create_task(
- async_load_platform(hass, "device_tracker", DOMAIN, {}, config)
+
+ async def async_close_connection(event):
+ """Close AsusWrt connection on HA Stop."""
+ await router.close()
+
+ stop_listener = hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_STOP, async_close_connection
)
+ hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
+ DATA_ASUSWRT: router,
+ "stop_listener": stop_listener,
+ }
+
return True
+
+
+async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN][entry.entry_id]["stop_listener"]()
+ router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT]
+ await router.close()
+
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
+
+
+async def update_listener(hass: HomeAssistantType, entry: ConfigEntry):
+ """Update when config_entry options update."""
+ router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT]
+
+ if router.update_options(entry.options):
+ await hass.config_entries.async_reload(entry.entry_id)
diff --git a/homeassistant/components/asuswrt/config_flow.py b/homeassistant/components/asuswrt/config_flow.py
new file mode 100644
index 00000000000000..af778dbe972ea0
--- /dev/null
+++ b/homeassistant/components/asuswrt/config_flow.py
@@ -0,0 +1,237 @@
+"""Config flow to configure the AsusWrt integration."""
+import logging
+import os
+import socket
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.components.device_tracker.const import (
+ CONF_CONSIDER_HOME,
+ DEFAULT_CONSIDER_HOME,
+)
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_MODE,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_PROTOCOL,
+ CONF_USERNAME,
+)
+from homeassistant.core import callback
+from homeassistant.helpers import config_validation as cv
+
+from .const import (
+ CONF_DNSMASQ,
+ CONF_INTERFACE,
+ CONF_REQUIRE_IP,
+ CONF_SSH_KEY,
+ CONF_TRACK_UNKNOWN,
+ DEFAULT_DNSMASQ,
+ DEFAULT_INTERFACE,
+ DEFAULT_SSH_PORT,
+ DEFAULT_TRACK_UNKNOWN,
+ DOMAIN,
+ MODE_AP,
+ MODE_ROUTER,
+ PROTOCOL_SSH,
+ PROTOCOL_TELNET,
+)
+from .router import get_api
+
+RESULT_CONN_ERROR = "cannot_connect"
+RESULT_UNKNOWN = "unknown"
+RESULT_SUCCESS = "success"
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def _is_file(value) -> bool:
+ """Validate that the value is an existing file."""
+ file_in = os.path.expanduser(str(value))
+
+ if not os.path.isfile(file_in):
+ return False
+ if not os.access(file_in, os.R_OK):
+ return False
+ return True
+
+
+def _get_ip(host):
+ """Get the ip address from the host name."""
+ try:
+ return socket.gethostbyname(host)
+ except socket.gaierror:
+ return None
+
+
+class AsusWrtFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ def __init__(self):
+ """Initialize AsusWrt config flow."""
+ self._host = None
+
+ @callback
+ def _show_setup_form(self, user_input=None, errors=None):
+ """Show the setup form to the user."""
+
+ if user_input is None:
+ user_input = {}
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str,
+ vol.Required(
+ CONF_USERNAME, default=user_input.get(CONF_USERNAME, "")
+ ): str,
+ vol.Optional(CONF_PASSWORD): str,
+ vol.Optional(CONF_SSH_KEY): str,
+ vol.Required(CONF_PROTOCOL, default=PROTOCOL_SSH): vol.In(
+ {PROTOCOL_SSH: "SSH", PROTOCOL_TELNET: "Telnet"}
+ ),
+ vol.Required(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port,
+ vol.Required(CONF_MODE, default=MODE_ROUTER): vol.In(
+ {MODE_ROUTER: "Router", MODE_AP: "Access Point"}
+ ),
+ }
+ ),
+ errors=errors or {},
+ )
+
+ async def _async_check_connection(self, user_input):
+ """Attempt to connect the AsusWrt router."""
+
+ api = get_api(user_input)
+ try:
+ await api.connection.async_connect()
+
+ except OSError:
+ _LOGGER.error("Error connecting to the AsusWrt router at %s", self._host)
+ return RESULT_CONN_ERROR
+
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception(
+ "Unknown error connecting with AsusWrt router at %s", self._host
+ )
+ return RESULT_UNKNOWN
+
+ if not api.is_connected:
+ _LOGGER.error("Error connecting to the AsusWrt router at %s", self._host)
+ return RESULT_CONN_ERROR
+
+ conf_protocol = user_input[CONF_PROTOCOL]
+ if conf_protocol == PROTOCOL_TELNET:
+ api.connection.disconnect()
+ return RESULT_SUCCESS
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initiated by the user."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
+ if user_input is None:
+ return self._show_setup_form(user_input)
+
+ errors = {}
+ self._host = user_input[CONF_HOST]
+ pwd = user_input.get(CONF_PASSWORD)
+ ssh = user_input.get(CONF_SSH_KEY)
+
+ if not (pwd or ssh):
+ errors["base"] = "pwd_or_ssh"
+ elif ssh:
+ if pwd:
+ errors["base"] = "pwd_and_ssh"
+ else:
+ isfile = await self.hass.async_add_executor_job(_is_file, ssh)
+ if not isfile:
+ errors["base"] = "ssh_not_file"
+
+ if not errors:
+ ip_address = await self.hass.async_add_executor_job(_get_ip, self._host)
+ if not ip_address:
+ errors["base"] = "invalid_host"
+
+ if not errors:
+ result = await self._async_check_connection(user_input)
+ if result != RESULT_SUCCESS:
+ errors["base"] = result
+
+ if errors:
+ return self._show_setup_form(user_input, errors)
+
+ return self.async_create_entry(
+ title=self._host,
+ data=user_input,
+ )
+
+ async def async_step_import(self, user_input=None):
+ """Import a config entry."""
+ return await self.async_step_user(user_input)
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for this handler."""
+ return OptionsFlowHandler(config_entry)
+
+
+class OptionsFlowHandler(config_entries.OptionsFlow):
+ """Handle a option flow for AsusWrt."""
+
+ def __init__(self, config_entry: config_entries.ConfigEntry):
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(self, user_input=None):
+ """Handle options flow."""
+ if user_input is not None:
+ return self.async_create_entry(title="", data=user_input)
+
+ data_schema = vol.Schema(
+ {
+ vol.Optional(
+ CONF_CONSIDER_HOME,
+ default=self.config_entry.options.get(
+ CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds()
+ ),
+ ): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900)),
+ vol.Optional(
+ CONF_TRACK_UNKNOWN,
+ default=self.config_entry.options.get(
+ CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN
+ ),
+ ): bool,
+ vol.Required(
+ CONF_INTERFACE,
+ default=self.config_entry.options.get(
+ CONF_INTERFACE, DEFAULT_INTERFACE
+ ),
+ ): str,
+ vol.Required(
+ CONF_DNSMASQ,
+ default=self.config_entry.options.get(
+ CONF_DNSMASQ, DEFAULT_DNSMASQ
+ ),
+ ): str,
+ }
+ )
+
+ conf_mode = self.config_entry.data[CONF_MODE]
+ if conf_mode == MODE_AP:
+ data_schema = data_schema.extend(
+ {
+ vol.Optional(
+ CONF_REQUIRE_IP,
+ default=self.config_entry.options.get(CONF_REQUIRE_IP, True),
+ ): bool,
+ }
+ )
+
+ return self.async_show_form(step_id="init", data_schema=data_schema)
diff --git a/homeassistant/components/asuswrt/const.py b/homeassistant/components/asuswrt/const.py
new file mode 100644
index 00000000000000..a8977a77ea8e11
--- /dev/null
+++ b/homeassistant/components/asuswrt/const.py
@@ -0,0 +1,28 @@
+"""AsusWrt component constants."""
+DOMAIN = "asuswrt"
+
+CONF_DNSMASQ = "dnsmasq"
+CONF_INTERFACE = "interface"
+CONF_REQUIRE_IP = "require_ip"
+CONF_SSH_KEY = "ssh_key"
+CONF_TRACK_UNKNOWN = "track_unknown"
+
+DATA_ASUSWRT = DOMAIN
+
+DEFAULT_DNSMASQ = "/var/lib/misc"
+DEFAULT_INTERFACE = "eth0"
+DEFAULT_SSH_PORT = 22
+DEFAULT_TRACK_UNKNOWN = False
+
+MODE_AP = "ap"
+MODE_ROUTER = "router"
+
+PROTOCOL_SSH = "ssh"
+PROTOCOL_TELNET = "telnet"
+
+# Sensors
+SENSOR_CONNECTED_DEVICE = "sensor_connected_device"
+SENSOR_RX_BYTES = "sensor_rx_bytes"
+SENSOR_TX_BYTES = "sensor_tx_bytes"
+SENSOR_RX_RATES = "sensor_rx_rates"
+SENSOR_TX_RATES = "sensor_tx_rates"
diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py
index a3545183d2e597..0db5dba0b17a51 100644
--- a/homeassistant/components/asuswrt/device_tracker.py
+++ b/homeassistant/components/asuswrt/device_tracker.py
@@ -1,64 +1,131 @@
"""Support for ASUSWRT routers."""
-import logging
+from __future__ import annotations
-from homeassistant.components.device_tracker import DeviceScanner
+from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER
+from homeassistant.components.device_tracker.config_entry import ScannerEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import callback
+from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.typing import HomeAssistantType
-from . import DATA_ASUSWRT
+from .const import DATA_ASUSWRT, DOMAIN
+from .router import AsusWrtRouter
-_LOGGER = logging.getLogger(__name__)
+DEFAULT_DEVICE_NAME = "Unknown device"
-async def async_get_scanner(hass, config):
- """Validate the configuration and return an ASUS-WRT scanner."""
- scanner = AsusWrtDeviceScanner(hass.data[DATA_ASUSWRT])
- await scanner.async_connect()
- return scanner if scanner.success_init else None
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up device tracker for AsusWrt component."""
+ router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT]
+ tracked = set()
+ @callback
+ def update_router():
+ """Update the values of the router."""
+ add_entities(router, async_add_entities, tracked)
-class AsusWrtDeviceScanner(DeviceScanner):
- """This class queries a router running ASUSWRT firmware."""
+ router.async_on_close(
+ async_dispatcher_connect(hass, router.signal_device_new, update_router)
+ )
- # Eighth attribute needed for mode (AP mode vs router mode)
- def __init__(self, api):
- """Initialize the scanner."""
- self.last_results = {}
- self.success_init = False
- self.connection = api
- self._connect_error = False
+ update_router()
- async def async_connect(self):
- """Initialize connection to the router."""
- # Test the router is accessible.
- data = await self.connection.async_get_connected_devices()
- self.success_init = data is not None
- async def async_scan_devices(self):
- """Scan for new devices and return a list with found device IDs."""
- await self.async_update_info()
- return list(self.last_results)
+@callback
+def add_entities(router, async_add_entities, tracked):
+ """Add new tracker entities from the router."""
+ new_tracked = []
- async def async_get_device_name(self, device):
- """Return the name of the given device or None if we don't know."""
- if device not in self.last_results:
- return None
- return self.last_results[device].name
+ for mac, device in router.devices.items():
+ if mac in tracked:
+ continue
- async def async_update_info(self):
- """Ensure the information from the ASUSWRT router is up to date.
+ new_tracked.append(AsusWrtDevice(router, device))
+ tracked.add(mac)
- Return boolean if scanning successful.
- """
- _LOGGER.debug("Checking Devices")
+ if new_tracked:
+ async_add_entities(new_tracked)
- try:
- self.last_results = await self.connection.async_get_connected_devices()
- if self._connect_error:
- self._connect_error = False
- _LOGGER.info("Reconnected to ASUS router for device update")
- except OSError as err:
- if not self._connect_error:
- self._connect_error = True
- _LOGGER.error(
- "Error connecting to ASUS router for device update: %s", err
- )
+class AsusWrtDevice(ScannerEntity):
+ """Representation of a AsusWrt device."""
+
+ def __init__(self, router: AsusWrtRouter, device) -> None:
+ """Initialize a AsusWrt device."""
+ self._router = router
+ self._device = device
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique ID."""
+ return self._device.mac
+
+ @property
+ def name(self) -> str:
+ """Return the name."""
+ return self._device.name or DEFAULT_DEVICE_NAME
+
+ @property
+ def is_connected(self):
+ """Return true if the device is connected to the network."""
+ return self._device.is_connected
+
+ @property
+ def source_type(self) -> str:
+ """Return the source type."""
+ return SOURCE_TYPE_ROUTER
+
+ @property
+ def extra_state_attributes(self) -> dict[str, any]:
+ """Return the attributes."""
+ attrs = {}
+ if self._device.last_activity:
+ attrs["last_time_reachable"] = self._device.last_activity.isoformat(
+ timespec="seconds"
+ )
+ return attrs
+
+ @property
+ def ip_address(self) -> str:
+ """Return the primary ip address of the device."""
+ return self._device.ip_address
+
+ @property
+ def mac_address(self) -> str:
+ """Return the mac address of the device."""
+ return self._device.mac
+
+ @property
+ def device_info(self) -> dict[str, any]:
+ """Return the device information."""
+ data = {
+ "connections": {(CONNECTION_NETWORK_MAC, self._device.mac)},
+ }
+ if self._device.name:
+ data["default_name"] = self._device.name
+
+ return data
+
+ @property
+ def should_poll(self) -> bool:
+ """No polling needed."""
+ return False
+
+ @callback
+ def async_on_demand_update(self):
+ """Update state."""
+ self._device = self._router.devices[self._device.mac]
+ self.async_write_ha_state()
+
+ async def async_added_to_hass(self):
+ """Register state update callback."""
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ self._router.signal_device_update,
+ self.async_on_demand_update,
+ )
+ )
diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json
index 9afb7849f8cebd..ab739f1c7ec47a 100644
--- a/homeassistant/components/asuswrt/manifest.json
+++ b/homeassistant/components/asuswrt/manifest.json
@@ -1,7 +1,8 @@
{
"domain": "asuswrt",
"name": "ASUSWRT",
+ "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/asuswrt",
"requirements": ["aioasuswrt==1.3.1"],
- "codeowners": ["@kennedyshead"]
+ "codeowners": ["@kennedyshead", "@ollo69"]
}
diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py
new file mode 100644
index 00000000000000..c5880ea11bb635
--- /dev/null
+++ b/homeassistant/components/asuswrt/router.py
@@ -0,0 +1,426 @@
+"""Represent the AsusWrt router."""
+from __future__ import annotations
+
+from datetime import datetime, timedelta
+import logging
+from typing import Any
+
+from aioasuswrt.asuswrt import AsusWrt
+
+from homeassistant.components.device_tracker.const import (
+ CONF_CONSIDER_HOME,
+ DEFAULT_CONSIDER_HOME,
+ DOMAIN as TRACKER_DOMAIN,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_MODE,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_PROTOCOL,
+ CONF_USERNAME,
+)
+from homeassistant.core import CALLBACK_TYPE, callback
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+from homeassistant.util import dt as dt_util
+
+from .const import (
+ CONF_DNSMASQ,
+ CONF_INTERFACE,
+ CONF_REQUIRE_IP,
+ CONF_SSH_KEY,
+ CONF_TRACK_UNKNOWN,
+ DEFAULT_DNSMASQ,
+ DEFAULT_INTERFACE,
+ DEFAULT_TRACK_UNKNOWN,
+ DOMAIN,
+ PROTOCOL_TELNET,
+ SENSOR_CONNECTED_DEVICE,
+ SENSOR_RX_BYTES,
+ SENSOR_RX_RATES,
+ SENSOR_TX_BYTES,
+ SENSOR_TX_RATES,
+)
+
+CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]
+
+KEY_COORDINATOR = "coordinator"
+KEY_SENSORS = "sensors"
+
+SCAN_INTERVAL = timedelta(seconds=30)
+
+SENSORS_TYPE_BYTES = "sensors_bytes"
+SENSORS_TYPE_COUNT = "sensors_count"
+SENSORS_TYPE_RATES = "sensors_rates"
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class AsusWrtSensorDataHandler:
+ """Data handler for AsusWrt sensor."""
+
+ def __init__(self, hass, api):
+ """Initialize a AsusWrt sensor data handler."""
+ self._hass = hass
+ self._api = api
+ self._connected_devices = 0
+
+ async def _get_connected_devices(self):
+ """Return number of connected devices."""
+ return {SENSOR_CONNECTED_DEVICE: self._connected_devices}
+
+ async def _get_bytes(self):
+ """Fetch byte information from the router."""
+ ret_dict: dict[str, Any] = {}
+ try:
+ datas = await self._api.async_get_bytes_total()
+ except OSError as exc:
+ raise UpdateFailed from exc
+
+ ret_dict[SENSOR_RX_BYTES] = datas[0]
+ ret_dict[SENSOR_TX_BYTES] = datas[1]
+
+ return ret_dict
+
+ async def _get_rates(self):
+ """Fetch rates information from the router."""
+ ret_dict: dict[str, Any] = {}
+ try:
+ rates = await self._api.async_get_current_transfer_rates()
+ except OSError as exc:
+ raise UpdateFailed from exc
+
+ ret_dict[SENSOR_RX_RATES] = rates[0]
+ ret_dict[SENSOR_TX_RATES] = rates[1]
+
+ return ret_dict
+
+ def update_device_count(self, conn_devices: int):
+ """Update connected devices attribute."""
+ if self._connected_devices == conn_devices:
+ return False
+ self._connected_devices = conn_devices
+ return True
+
+ async def get_coordinator(self, sensor_type: str, should_poll=True):
+ """Get the coordinator for a specific sensor type."""
+ if sensor_type == SENSORS_TYPE_COUNT:
+ method = self._get_connected_devices
+ elif sensor_type == SENSORS_TYPE_BYTES:
+ method = self._get_bytes
+ elif sensor_type == SENSORS_TYPE_RATES:
+ method = self._get_rates
+ else:
+ raise RuntimeError(f"Invalid sensor type: {sensor_type}")
+
+ coordinator = DataUpdateCoordinator(
+ self._hass,
+ _LOGGER,
+ name=sensor_type,
+ update_method=method,
+ # Polling interval. Will only be polled if there are subscribers.
+ update_interval=SCAN_INTERVAL if should_poll else None,
+ )
+ await coordinator.async_refresh()
+
+ return coordinator
+
+
+class AsusWrtDevInfo:
+ """Representation of a AsusWrt device info."""
+
+ def __init__(self, mac, name=None):
+ """Initialize a AsusWrt device info."""
+ self._mac = mac
+ self._name = name
+ self._ip_address = None
+ self._last_activity = None
+ self._connected = False
+
+ def update(self, dev_info=None, consider_home=0):
+ """Update AsusWrt device info."""
+ utc_point_in_time = dt_util.utcnow()
+ if dev_info:
+ if not self._name:
+ self._name = dev_info.name or self._mac.replace(":", "_")
+ self._ip_address = dev_info.ip
+ self._last_activity = utc_point_in_time
+ self._connected = True
+
+ elif self._connected:
+ self._connected = (
+ utc_point_in_time - self._last_activity
+ ).total_seconds() < consider_home
+ self._ip_address = None
+
+ @property
+ def is_connected(self):
+ """Return connected status."""
+ return self._connected
+
+ @property
+ def mac(self):
+ """Return device mac address."""
+ return self._mac
+
+ @property
+ def name(self):
+ """Return device name."""
+ return self._name
+
+ @property
+ def ip_address(self):
+ """Return device ip address."""
+ return self._ip_address
+
+ @property
+ def last_activity(self):
+ """Return device last activity."""
+ return self._last_activity
+
+
+class AsusWrtRouter:
+ """Representation of a AsusWrt router."""
+
+ def __init__(self, hass: HomeAssistantType, entry: ConfigEntry) -> None:
+ """Initialize a AsusWrt router."""
+ self.hass = hass
+ self._entry = entry
+
+ self._api: AsusWrt = None
+ self._protocol = entry.data[CONF_PROTOCOL]
+ self._host = entry.data[CONF_HOST]
+
+ self._devices: dict[str, Any] = {}
+ self._connected_devices = 0
+ self._connect_error = False
+
+ self._sensors_data_handler: AsusWrtSensorDataHandler = None
+ self._sensors_coordinator: dict[str, Any] = {}
+
+ self._on_close = []
+
+ self._options = {
+ CONF_DNSMASQ: DEFAULT_DNSMASQ,
+ CONF_INTERFACE: DEFAULT_INTERFACE,
+ CONF_REQUIRE_IP: True,
+ }
+ self._options.update(entry.options)
+
+ async def setup(self) -> None:
+ """Set up a AsusWrt router."""
+ self._api = get_api(self._entry.data, self._options)
+
+ try:
+ await self._api.connection.async_connect()
+ except OSError as exp:
+ raise ConfigEntryNotReady from exp
+
+ if not self._api.is_connected:
+ raise ConfigEntryNotReady
+
+ # Load tracked entities from registry
+ entity_registry = await self.hass.helpers.entity_registry.async_get_registry()
+ track_entries = (
+ self.hass.helpers.entity_registry.async_entries_for_config_entry(
+ entity_registry, self._entry.entry_id
+ )
+ )
+ for entry in track_entries:
+ if entry.domain == TRACKER_DOMAIN:
+ self._devices[entry.unique_id] = AsusWrtDevInfo(
+ entry.unique_id, entry.original_name
+ )
+
+ # Update devices
+ await self.update_devices()
+
+ # Init Sensors
+ await self.init_sensors_coordinator()
+
+ self.async_on_close(
+ async_track_time_interval(self.hass, self.update_all, SCAN_INTERVAL)
+ )
+
+ async def update_all(self, now: datetime | None = None) -> None:
+ """Update all AsusWrt platforms."""
+ await self.update_devices()
+
+ async def update_devices(self) -> None:
+ """Update AsusWrt devices tracker."""
+ new_device = False
+ _LOGGER.debug("Checking devices for ASUS router %s", self._host)
+ try:
+ wrt_devices = await self._api.async_get_connected_devices()
+ except OSError as exc:
+ if not self._connect_error:
+ self._connect_error = True
+ _LOGGER.error(
+ "Error connecting to ASUS router %s for device update: %s",
+ self._host,
+ exc,
+ )
+ return
+
+ if self._connect_error:
+ self._connect_error = False
+ _LOGGER.info("Reconnected to ASUS router %s", self._host)
+
+ consider_home = self._options.get(
+ CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds()
+ )
+ track_unknown = self._options.get(CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN)
+
+ for device_mac in self._devices:
+ dev_info = wrt_devices.get(device_mac)
+ self._devices[device_mac].update(dev_info, consider_home)
+
+ for device_mac, dev_info in wrt_devices.items():
+ if device_mac in self._devices:
+ continue
+ if not track_unknown and not dev_info.name:
+ continue
+ new_device = True
+ device = AsusWrtDevInfo(device_mac)
+ device.update(dev_info)
+ self._devices[device_mac] = device
+
+ async_dispatcher_send(self.hass, self.signal_device_update)
+ if new_device:
+ async_dispatcher_send(self.hass, self.signal_device_new)
+
+ self._connected_devices = len(wrt_devices)
+ await self._update_unpolled_sensors()
+
+ async def init_sensors_coordinator(self) -> None:
+ """Init AsusWrt sensors coordinators."""
+ if self._sensors_data_handler:
+ return
+
+ self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api)
+ self._sensors_data_handler.update_device_count(self._connected_devices)
+
+ conn_dev_coordinator = await self._sensors_data_handler.get_coordinator(
+ SENSORS_TYPE_COUNT, False
+ )
+ self._sensors_coordinator[SENSORS_TYPE_COUNT] = {
+ KEY_COORDINATOR: conn_dev_coordinator,
+ KEY_SENSORS: [SENSOR_CONNECTED_DEVICE],
+ }
+
+ bytes_coordinator = await self._sensors_data_handler.get_coordinator(
+ SENSORS_TYPE_BYTES
+ )
+ self._sensors_coordinator[SENSORS_TYPE_BYTES] = {
+ KEY_COORDINATOR: bytes_coordinator,
+ KEY_SENSORS: [SENSOR_RX_BYTES, SENSOR_TX_BYTES],
+ }
+
+ rates_coordinator = await self._sensors_data_handler.get_coordinator(
+ SENSORS_TYPE_RATES
+ )
+ self._sensors_coordinator[SENSORS_TYPE_RATES] = {
+ KEY_COORDINATOR: rates_coordinator,
+ KEY_SENSORS: [SENSOR_RX_RATES, SENSOR_TX_RATES],
+ }
+
+ async def _update_unpolled_sensors(self) -> None:
+ """Request refresh for AsusWrt unpolled sensors."""
+ if not self._sensors_data_handler:
+ return
+
+ if SENSORS_TYPE_COUNT in self._sensors_coordinator:
+ coordinator = self._sensors_coordinator[SENSORS_TYPE_COUNT][KEY_COORDINATOR]
+ if self._sensors_data_handler.update_device_count(self._connected_devices):
+ await coordinator.async_refresh()
+
+ async def close(self) -> None:
+ """Close the connection."""
+ if self._api is not None and self._protocol == PROTOCOL_TELNET:
+ self._api.connection.disconnect()
+ self._api = None
+
+ for func in self._on_close:
+ func()
+ self._on_close.clear()
+
+ @callback
+ def async_on_close(self, func: CALLBACK_TYPE) -> None:
+ """Add a function to call when router is closed."""
+ self._on_close.append(func)
+
+ def update_options(self, new_options: dict) -> bool:
+ """Update router options."""
+ req_reload = False
+ for name, new_opt in new_options.items():
+ if name in (CONF_REQ_RELOAD):
+ old_opt = self._options.get(name)
+ if not old_opt or old_opt != new_opt:
+ req_reload = True
+ break
+
+ self._options.update(new_options)
+ return req_reload
+
+ @property
+ def device_info(self) -> dict[str, Any]:
+ """Return the device information."""
+ return {
+ "identifiers": {(DOMAIN, "AsusWRT")},
+ "name": self._host,
+ "model": "Asus Router",
+ "manufacturer": "Asus",
+ }
+
+ @property
+ def signal_device_new(self) -> str:
+ """Event specific per AsusWrt entry to signal new device."""
+ return f"{DOMAIN}-device-new"
+
+ @property
+ def signal_device_update(self) -> str:
+ """Event specific per AsusWrt entry to signal updates in devices."""
+ return f"{DOMAIN}-device-update"
+
+ @property
+ def host(self) -> str:
+ """Return router hostname."""
+ return self._host
+
+ @property
+ def devices(self) -> dict[str, Any]:
+ """Return devices."""
+ return self._devices
+
+ @property
+ def sensors_coordinator(self) -> dict[str, Any]:
+ """Return sensors coordinators."""
+ return self._sensors_coordinator
+
+ @property
+ def api(self) -> AsusWrt:
+ """Return router API."""
+ return self._api
+
+
+def get_api(conf: dict, options: dict | None = None) -> AsusWrt:
+ """Get the AsusWrt API."""
+ opt = options or {}
+
+ return AsusWrt(
+ conf[CONF_HOST],
+ conf[CONF_PORT],
+ conf[CONF_PROTOCOL] == PROTOCOL_TELNET,
+ conf[CONF_USERNAME],
+ conf.get(CONF_PASSWORD, ""),
+ conf.get(CONF_SSH_KEY, ""),
+ conf[CONF_MODE],
+ opt.get(CONF_REQUIRE_IP, True),
+ interface=opt.get(CONF_INTERFACE, DEFAULT_INTERFACE),
+ dnsmasq=opt.get(CONF_DNSMASQ, DEFAULT_DNSMASQ),
+ )
diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py
index aa13bee81d0946..7e38243e3d6ebb 100644
--- a/homeassistant/components/asuswrt/sensor.py
+++ b/homeassistant/components/asuswrt/sensor.py
@@ -1,168 +1,171 @@
"""Asuswrt status sensors."""
-from datetime import timedelta
-import enum
-import logging
-from typing import Any, Dict, List, Optional
+from __future__ import annotations
-from aioasuswrt.asuswrt import AsusWrt
+import logging
+from numbers import Number
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND
+from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
-from . import DATA_ASUSWRT
-
-UPLOAD_ICON = "mdi:upload-network"
-DOWNLOAD_ICON = "mdi:download-network"
+from .const import (
+ DATA_ASUSWRT,
+ DOMAIN,
+ SENSOR_CONNECTED_DEVICE,
+ SENSOR_RX_BYTES,
+ SENSOR_RX_RATES,
+ SENSOR_TX_BYTES,
+ SENSOR_TX_RATES,
+)
+from .router import KEY_COORDINATOR, KEY_SENSORS, AsusWrtRouter
+
+DEFAULT_PREFIX = "Asuswrt"
+
+SENSOR_DEVICE_CLASS = "device_class"
+SENSOR_ICON = "icon"
+SENSOR_NAME = "name"
+SENSOR_UNIT = "unit"
+SENSOR_FACTOR = "factor"
+SENSOR_DEFAULT_ENABLED = "default_enabled"
+
+UNIT_DEVICES = "Devices"
+
+CONNECTION_SENSORS = {
+ SENSOR_CONNECTED_DEVICE: {
+ SENSOR_NAME: "Devices Connected",
+ SENSOR_UNIT: UNIT_DEVICES,
+ SENSOR_FACTOR: 0,
+ SENSOR_ICON: "mdi:router-network",
+ SENSOR_DEVICE_CLASS: None,
+ SENSOR_DEFAULT_ENABLED: True,
+ },
+ SENSOR_RX_RATES: {
+ SENSOR_NAME: "Download Speed",
+ SENSOR_UNIT: DATA_RATE_MEGABITS_PER_SECOND,
+ SENSOR_FACTOR: 125000,
+ SENSOR_ICON: "mdi:download-network",
+ SENSOR_DEVICE_CLASS: None,
+ },
+ SENSOR_TX_RATES: {
+ SENSOR_NAME: "Upload Speed",
+ SENSOR_UNIT: DATA_RATE_MEGABITS_PER_SECOND,
+ SENSOR_FACTOR: 125000,
+ SENSOR_ICON: "mdi:upload-network",
+ SENSOR_DEVICE_CLASS: None,
+ },
+ SENSOR_RX_BYTES: {
+ SENSOR_NAME: "Download",
+ SENSOR_UNIT: DATA_GIGABYTES,
+ SENSOR_FACTOR: 1000000000,
+ SENSOR_ICON: "mdi:download",
+ SENSOR_DEVICE_CLASS: None,
+ },
+ SENSOR_TX_BYTES: {
+ SENSOR_NAME: "Upload",
+ SENSOR_UNIT: DATA_GIGABYTES,
+ SENSOR_FACTOR: 1000000000,
+ SENSOR_ICON: "mdi:upload",
+ SENSOR_DEVICE_CLASS: None,
+ },
+}
_LOGGER = logging.getLogger(__name__)
-@enum.unique
-class _SensorTypes(enum.Enum):
- DEVICES = "devices"
- UPLOAD = "upload"
- DOWNLOAD = "download"
- DOWNLOAD_SPEED = "download_speed"
- UPLOAD_SPEED = "upload_speed"
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up the sensors."""
+ router: AsusWrtRouter = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT]
+ entities = []
+
+ for sensor_data in router.sensors_coordinator.values():
+ coordinator = sensor_data[KEY_COORDINATOR]
+ sensors = sensor_data[KEY_SENSORS]
+ for sensor_key in sensors:
+ if sensor_key in CONNECTION_SENSORS:
+ entities.append(
+ AsusWrtSensor(
+ coordinator, router, sensor_key, CONNECTION_SENSORS[sensor_key]
+ )
+ )
+
+ async_add_entities(entities, True)
+
+
+class AsusWrtSensor(CoordinatorEntity, SensorEntity):
+ """Representation of a AsusWrt sensor."""
+
+ def __init__(
+ self,
+ coordinator: DataUpdateCoordinator,
+ router: AsusWrtRouter,
+ sensor_type: str,
+ sensor: dict[str, any],
+ ) -> None:
+ """Initialize a AsusWrt sensor."""
+ super().__init__(coordinator)
+ self._router = router
+ self._sensor_type = sensor_type
+ self._name = f"{DEFAULT_PREFIX} {sensor[SENSOR_NAME]}"
+ self._unique_id = f"{DOMAIN} {self._name}"
+ self._unit = sensor[SENSOR_UNIT]
+ self._factor = sensor[SENSOR_FACTOR]
+ self._icon = sensor[SENSOR_ICON]
+ self._device_class = sensor[SENSOR_DEVICE_CLASS]
+ self._default_enabled = sensor.get(SENSOR_DEFAULT_ENABLED, False)
@property
- def unit_of_measurement(self) -> Optional[str]:
- """Return a string with the unit of the sensortype."""
- if self in (_SensorTypes.UPLOAD, _SensorTypes.DOWNLOAD):
- return DATA_GIGABYTES
- if self in (_SensorTypes.UPLOAD_SPEED, _SensorTypes.DOWNLOAD_SPEED):
- return DATA_RATE_MEGABITS_PER_SECOND
- return None
+ def entity_registry_enabled_default(self) -> bool:
+ """Return if the entity should be enabled when first added to the entity registry."""
+ return self._default_enabled
@property
- def icon(self) -> Optional[str]:
- """Return the expected icon for the sensortype."""
- if self in (_SensorTypes.UPLOAD, _SensorTypes.UPLOAD_SPEED):
- return UPLOAD_ICON
- if self in (_SensorTypes.DOWNLOAD, _SensorTypes.DOWNLOAD_SPEED):
- return DOWNLOAD_ICON
- return None
+ def state(self) -> str:
+ """Return current state."""
+ state = self.coordinator.data.get(self._sensor_type)
+ if state is None:
+ return None
+ if self._factor and isinstance(state, Number):
+ return round(state / self._factor, 2)
+ return state
@property
- def sensor_name(self) -> Optional[str]:
- """Return the name of the sensor."""
- if self is _SensorTypes.DEVICES:
- return "Asuswrt Devices Connected"
- if self is _SensorTypes.UPLOAD:
- return "Asuswrt Upload"
- if self is _SensorTypes.DOWNLOAD:
- return "Asuswrt Download"
- if self is _SensorTypes.UPLOAD_SPEED:
- return "Asuswrt Upload Speed"
- if self is _SensorTypes.DOWNLOAD_SPEED:
- return "Asuswrt Download Speed"
- return None
+ def unique_id(self) -> str:
+ """Return a unique ID."""
+ return self._unique_id
@property
- def is_speed(self) -> bool:
- """Return True if the type is an upload/download speed."""
- return self in (_SensorTypes.UPLOAD_SPEED, _SensorTypes.DOWNLOAD_SPEED)
+ def name(self) -> str:
+ """Return the name."""
+ return self._name
@property
- def is_size(self) -> bool:
- """Return True if the type is the total upload/download size."""
- return self in (_SensorTypes.UPLOAD, _SensorTypes.DOWNLOAD)
-
-
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the asuswrt sensors."""
- if discovery_info is None:
- return
-
- api: AsusWrt = hass.data[DATA_ASUSWRT]
-
- # Let's discover the valid sensor types.
- sensors = [_SensorTypes(x) for x in discovery_info]
-
- data_handler = AsuswrtDataHandler(sensors, api)
- coordinator = DataUpdateCoordinator(
- hass,
- _LOGGER,
- name="sensor",
- update_method=data_handler.update_data,
- # Polling interval. Will only be polled if there are subscribers.
- update_interval=timedelta(seconds=30),
- )
-
- await coordinator.async_refresh()
- async_add_entities([AsuswrtSensor(coordinator, x) for x in sensors])
-
-
-class AsuswrtDataHandler:
- """Class handling the API updates."""
-
- def __init__(self, sensors: List[_SensorTypes], api: AsusWrt):
- """Initialize the handler class."""
- self._api = api
- self._sensors = sensors
- self._connected = True
-
- async def update_data(self) -> Dict[_SensorTypes, Any]:
- """Fetch the relevant data from the router."""
- ret_dict: Dict[_SensorTypes, Any] = {}
- try:
- if _SensorTypes.DEVICES in self._sensors:
- # Let's check the nr of devices.
- devices = await self._api.async_get_connected_devices()
- ret_dict[_SensorTypes.DEVICES] = len(devices)
-
- if any(x.is_speed for x in self._sensors):
- # Let's check the upload and download speed
- speed = await self._api.async_get_current_transfer_rates()
- ret_dict[_SensorTypes.DOWNLOAD_SPEED] = round(speed[0] / 125000, 2)
- ret_dict[_SensorTypes.UPLOAD_SPEED] = round(speed[1] / 125000, 2)
-
- if any(x.is_size for x in self._sensors):
- rates = await self._api.async_get_bytes_total()
- ret_dict[_SensorTypes.DOWNLOAD] = round(rates[0] / 1000000000, 1)
- ret_dict[_SensorTypes.UPLOAD] = round(rates[1] / 1000000000, 1)
-
- if not self._connected:
- # Log a successful reconnect
- self._connected = True
- _LOGGER.warning("Successfully reconnected to ASUS router")
-
- except OSError as err:
- if self._connected:
- # Log the first time connection was lost
- _LOGGER.warning("Lost connection to router error due to: '%s'", err)
- self._connected = False
-
- return ret_dict
-
-
-class AsuswrtSensor(CoordinatorEntity):
- """The asuswrt specific sensor class."""
-
- def __init__(self, coordinator: DataUpdateCoordinator, sensor_type: _SensorTypes):
- """Initialize the sensor class."""
- super().__init__(coordinator)
- self._type = sensor_type
+ def unit_of_measurement(self) -> str:
+ """Return the unit."""
+ return self._unit
@property
- def state(self):
- """Return the state of the sensor."""
- return self.coordinator.data.get(self._type)
+ def icon(self) -> str:
+ """Return the icon."""
+ return self._icon
@property
- def name(self) -> str:
- """Return the name of the sensor."""
- return self._type.sensor_name
+ def device_class(self) -> str:
+ """Return the device_class."""
+ return self._device_class
@property
- def icon(self) -> Optional[str]:
- """Return the icon to use in the frontend."""
- return self._type.icon
+ def extra_state_attributes(self) -> dict[str, any]:
+ """Return the attributes."""
+ return {"hostname": self._router.host}
@property
- def unit_of_measurement(self) -> Optional[str]:
- """Return the unit of measurement of this entity, if any."""
- return self._type.unit_of_measurement
+ def device_info(self) -> dict[str, any]:
+ """Return the device information."""
+ return self._router.device_info
diff --git a/homeassistant/components/asuswrt/strings.json b/homeassistant/components/asuswrt/strings.json
new file mode 100644
index 00000000000000..079ee35bf95a98
--- /dev/null
+++ b/homeassistant/components/asuswrt/strings.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "AsusWRT",
+ "description": "Set required parameter to connect to your router",
+ "data": {
+ "host": "[%key:common::config_flow::data::host%]",
+ "name": "[%key:common::config_flow::data::name%]",
+ "username": "[%key:common::config_flow::data::username%]",
+ "password": "[%key:common::config_flow::data::password%]",
+ "ssh_key": "Path to your SSH key file (instead of password)",
+ "protocol": "Communication protocol to use",
+ "port": "[%key:common::config_flow::data::port%]",
+ "mode": "[%key:common::config_flow::data::mode%]"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_host": "[%key:common::config_flow::error::invalid_host%]",
+ "pwd_and_ssh": "Only provide password or SSH key file",
+ "pwd_or_ssh": "Please provide password or SSH key file",
+ "ssh_not_file": "SSH key file not found",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "AsusWRT Options",
+ "data": {
+ "consider_home": "Seconds to wait before considering a device away",
+ "track_unknown": "Track unknown / unamed devices",
+ "interface": "The interface that you want statistics from (e.g. eth0,eth1 etc)",
+ "dnsmasq": "The location in the router of the dnsmasq.leases files",
+ "require_ip": "Devices must have IP (for access point mode)"
+ }
+ }
+ }
+ }
+}
diff --git a/homeassistant/components/asuswrt/translations/bg.json b/homeassistant/components/asuswrt/translations/bg.json
new file mode 100644
index 00000000000000..dbb5f415f92913
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/bg.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "\u0418\u043c\u0435",
+ "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/asuswrt/translations/ca.json b/homeassistant/components/asuswrt/translations/ca.json
new file mode 100644
index 00000000000000..2b15199a092b7e
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/ca.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids",
+ "pwd_and_ssh": "Proporciona, nom\u00e9s, la contrasenya o el fitxer de claus SSH",
+ "pwd_or_ssh": "Proporciona la contrasenya o el fitxer de claus SSH",
+ "ssh_not_file": "No s'ha trobat el fitxer de claus SSH",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "mode": "Mode",
+ "name": "Nom",
+ "password": "Contrasenya",
+ "port": "Port",
+ "protocol": "Protocol de comunicacions a utilitzar",
+ "ssh_key": "Ruta al fitxer de claus SSH (en lloc de la contrasenya)",
+ "username": "Nom d'usuari"
+ },
+ "description": "Introdueix el par\u00e0metre necessari per connectar-te al router",
+ "title": "AsusWRT"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "Segons d'espera abans de considerar un dispositiu a fora",
+ "dnsmasq": "La ubicaci\u00f3 dins el router dels fitxers dnsmasq.leases",
+ "interface": "La interf\u00edcie de la qual obtenir les estad\u00edstiques (per exemple, eth0, eth1, etc.)",
+ "require_ip": "Els dispositius han de tenir una IP (per al mode de punt d'acc\u00e9s)",
+ "track_unknown": "Segueix dispositius desconeguts/sense nom"
+ },
+ "title": "Opcions d'AsusWRT"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/asuswrt/translations/cs.json b/homeassistant/components/asuswrt/translations/cs.json
new file mode 100644
index 00000000000000..d9766e9a6d0b1e
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/cs.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace."
+ },
+ "error": {
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
+ "invalid_host": "Neplatn\u00fd hostitel nebo IP adresa",
+ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hostitel",
+ "mode": "Re\u017eim",
+ "name": "Jm\u00e9no",
+ "password": "Heslo",
+ "port": "Port",
+ "username": "U\u017eivatelsk\u00e9 jm\u00e9no"
+ },
+ "title": "AsusWRT"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/asuswrt/translations/de.json b/homeassistant/components/asuswrt/translations/de.json
new file mode 100644
index 00000000000000..36699d9575379a
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/de.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse",
+ "pwd_and_ssh": "Nur Passwort oder SSH-Schl\u00fcsseldatei angeben",
+ "pwd_or_ssh": "Bitte Passwort oder SSH-Schl\u00fcsseldatei angeben",
+ "ssh_not_file": "SSH-Schl\u00fcsseldatei nicht gefunden",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "mode": "Modus",
+ "name": "Name",
+ "password": "Passwort",
+ "port": "Port",
+ "protocol": "Zu verwendendes Kommunikationsprotokoll",
+ "ssh_key": "Pfad zu deiner SSH-Schl\u00fcsseldatei (anstelle des Passworts)",
+ "username": "Benutzername"
+ },
+ "title": ""
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "interface": "Schnittstelle, von der du Statistiken haben m\u00f6chtest (z.B. eth0, eth1 usw.)",
+ "require_ip": "Ger\u00e4te m\u00fcssen IP haben (f\u00fcr Zugangspunkt-Modus)"
+ },
+ "title": "AsusWRT Optionen"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/asuswrt/translations/en.json b/homeassistant/components/asuswrt/translations/en.json
new file mode 100644
index 00000000000000..5ac87e277f4e66
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/en.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_host": "Invalid hostname or IP address",
+ "pwd_and_ssh": "Only provide password or SSH key file",
+ "pwd_or_ssh": "Please provide password or SSH key file",
+ "ssh_not_file": "SSH key file not found",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "mode": "Mode",
+ "name": "Name",
+ "password": "Password",
+ "port": "Port",
+ "protocol": "Communication protocol to use",
+ "ssh_key": "Path to your SSH key file (instead of password)",
+ "username": "Username"
+ },
+ "description": "Set required parameter to connect to your router",
+ "title": "AsusWRT"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "Seconds to wait before considering a device away",
+ "dnsmasq": "The location in the router of the dnsmasq.leases files",
+ "interface": "The interface that you want statistics from (e.g. eth0,eth1 etc)",
+ "require_ip": "Devices must have IP (for access point mode)",
+ "track_unknown": "Track unknown / unamed devices"
+ },
+ "title": "AsusWRT Options"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/asuswrt/translations/es.json b/homeassistant/components/asuswrt/translations/es.json
new file mode 100644
index 00000000000000..c5792babf004d2
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/es.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n."
+ },
+ "error": {
+ "cannot_connect": "No se pudo conectar",
+ "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos",
+ "pwd_and_ssh": "S\u00f3lo proporcionar la contrase\u00f1a o el archivo de clave SSH",
+ "pwd_or_ssh": "Por favor, proporcione la contrase\u00f1a o el archivo de clave SSH",
+ "ssh_not_file": "Archivo de clave SSH no encontrado",
+ "unknown": "Error inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "mode": "Modo",
+ "name": "Nombre",
+ "password": "Contrase\u00f1a",
+ "port": "Puerto",
+ "protocol": "Protocolo de comunicaci\u00f3n a utilizar",
+ "ssh_key": "Ruta de acceso a su archivo de clave SSH (en lugar de la contrase\u00f1a)",
+ "username": "Nombre de usuario"
+ },
+ "description": "Establezca los par\u00e1metros necesarios para conectarse a su router",
+ "title": "AsusWRT"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "Segundos de espera antes de considerar un dispositivo ausente",
+ "dnsmasq": "La ubicaci\u00f3n en el router de los archivos dnsmasq.leases",
+ "interface": "La interfaz de la que desea obtener estad\u00edsticas (por ejemplo, eth0, eth1, etc.)",
+ "require_ip": "Los dispositivos deben tener IP (para el modo de punto de acceso)",
+ "track_unknown": "Seguimiento de dispositivos desconocidos / sin nombre"
+ },
+ "title": "Opciones de AsusWRT"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/asuswrt/translations/et.json b/homeassistant/components/asuswrt/translations/et.json
new file mode 100644
index 00000000000000..8cc14b7353b66b
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/et.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_host": "Sobimatu hostinimi v\u00f5i IP-aadress",
+ "pwd_and_ssh": "Sisesta ainult parooli v\u00f5i SSH v\u00f5tmefail",
+ "pwd_or_ssh": "Sisesta parool v\u00f5i SSH v\u00f5tmefail",
+ "ssh_not_file": "SSH v\u00f5tmefaili ei leitud",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "mode": "Re\u017eiim",
+ "name": "Nimi",
+ "password": "Salas\u00f5na",
+ "port": "Port",
+ "protocol": "Kasutatav sideprotokoll",
+ "ssh_key": "Rada SSH v\u00f5tmefailini (parooli asemel)",
+ "username": "Kasutajanimi"
+ },
+ "description": "M\u00e4\u00e4ra ruuteriga \u00fchenduse loomiseks vajalik parameeter",
+ "title": "AsusWRT"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "Mitu sekundit oodata, enne kui lugeda seade eemal olevaks",
+ "dnsmasq": "Dnsmasq.leases failide asukoht ruuteris",
+ "interface": "Liides kust soovite statistikat (n\u00e4iteks eth0, eth1 jne.)",
+ "require_ip": "Seadmetel peab olema IP (p\u00e4\u00e4supunkti re\u017eiimi jaoks)",
+ "track_unknown": "J\u00e4lgi tundmatuid / nimetamata seadmeid"
+ },
+ "title": "AsusWRT valikud"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/asuswrt/translations/fr.json b/homeassistant/components/asuswrt/translations/fr.json
new file mode 100644
index 00000000000000..0d53f3f24cfc79
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/fr.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide",
+ "pwd_and_ssh": "Fournissez uniquement le mot de passe ou le fichier de cl\u00e9 SSH",
+ "pwd_or_ssh": "Veuillez fournir un mot de passe ou un fichier de cl\u00e9 SSH",
+ "ssh_not_file": "Fichier cl\u00e9 SSH non trouv\u00e9",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "H\u00f4te",
+ "mode": "Mode",
+ "name": "Nom",
+ "password": "Mot de passe",
+ "port": "Port",
+ "protocol": "Protocole de communication \u00e0 utiliser",
+ "ssh_key": "Chemin d'acc\u00e8s \u00e0 votre fichier de cl\u00e9s SSH (au lieu du mot de passe)",
+ "username": "Nom d'utilisateur"
+ },
+ "description": "D\u00e9finissez les param\u00e8tres n\u00e9cessaires pour vous connecter \u00e0 votre routeur",
+ "title": "AsusWRT"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "Quelques secondes d'attente avant d'envisager l'abandon d'un appareil",
+ "dnsmasq": "L\u2019emplacement dans le routeur des fichiers dnsmasq.leases",
+ "interface": "L'interface \u00e0 partir de laquelle vous souhaitez obtenir des statistiques (e.g. eth0,eth1 etc)",
+ "require_ip": "Les appareils doivent avoir une IP (pour le mode point d'acc\u00e8s)",
+ "track_unknown": "Traquer les appareils inconnus / non identifi\u00e9s"
+ },
+ "title": "Options AsusWRT"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/asuswrt/translations/hu.json b/homeassistant/components/asuswrt/translations/hu.json
new file mode 100644
index 00000000000000..4f47781a15c56a
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/hu.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm",
+ "ssh_not_file": "Az SSH kulcsf\u00e1jl nem tal\u00e1lhat\u00f3",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "mode": "M\u00f3d",
+ "name": "N\u00e9v",
+ "password": "Jelsz\u00f3",
+ "port": "Port",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "title": "AsusWRT"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "AsusWRT Be\u00e1ll\u00edt\u00e1sok"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/asuswrt/translations/id.json b/homeassistant/components/asuswrt/translations/id.json
new file mode 100644
index 00000000000000..aa4eebd1f86c28
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/id.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_host": "Nama host atau alamat IP tidak valid",
+ "pwd_and_ssh": "Hanya berikan kata sandi atau file kunci SSH",
+ "pwd_or_ssh": "Harap berikan kata sandi atau file kunci SSH",
+ "ssh_not_file": "File kunci SSH tidak ditemukan",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "mode": "Mode",
+ "name": "Nama",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "protocol": "Protokol komunikasi yang akan digunakan",
+ "ssh_key": "Jalur ke file kunci SSH Anda (bukan kata sandi)",
+ "username": "Nama Pengguna"
+ },
+ "description": "Tetapkan parameter yang diperlukan untuk terhubung ke router Anda",
+ "title": "AsusWRT"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "Tenggang waktu dalam detik untuk perangkat dianggap sebagai keluar",
+ "dnsmasq": "Lokasi dnsmasq.leases di router file",
+ "interface": "Antarmuka statistik yang diinginkan (mis. eth0, eth1, dll)",
+ "require_ip": "Perangkat harus memiliki IP (untuk mode titik akses)",
+ "track_unknown": "Lacak perangkat yang tidak dikenal/tidak bernama"
+ },
+ "title": "Opsi AsusWRT"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/asuswrt/translations/it.json b/homeassistant/components/asuswrt/translations/it.json
new file mode 100644
index 00000000000000..d266cabbed4f14
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/it.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_host": "Nome host o indirizzo IP non valido",
+ "pwd_and_ssh": "Fornire solo la password o il file della chiave SSH",
+ "pwd_or_ssh": "Si prega di fornire la password o il file della chiave SSH",
+ "ssh_not_file": "File chiave SSH non trovato",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "mode": "Modalit\u00e0",
+ "name": "Nome",
+ "password": "Password",
+ "port": "Porta",
+ "protocol": "Protocollo di comunicazione da utilizzare",
+ "ssh_key": "Percorso del file della chiave SSH (invece della password)",
+ "username": "Nome utente"
+ },
+ "description": "Imposta il parametro richiesto per collegarti al tuo router",
+ "title": "AsusWRT"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "Secondi di attesa prima di considerare un dispositivo lontano",
+ "dnsmasq": "La posizione nel router dei file dnsmasq.leases",
+ "interface": "L'interfaccia da cui si desidera ottenere statistiche (ad esempio eth0, eth1, ecc.)",
+ "require_ip": "I dispositivi devono avere un IP (per la modalit\u00e0 punto di accesso)",
+ "track_unknown": "Tieni traccia dei dispositivi sconosciuti / non denominati"
+ },
+ "title": "Opzioni AsusWRT"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/asuswrt/translations/ko.json b/homeassistant/components/asuswrt/translations/ko.json
new file mode 100644
index 00000000000000..4e60a0d15b08ad
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/ko.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "pwd_and_ssh": "\ube44\ubc00\ubc88\ud638 \ub610\ub294 SSH \ud0a4 \ud30c\uc77c\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4",
+ "pwd_or_ssh": "\ube44\ubc00\ubc88\ud638 \ub610\ub294 SSH \ud0a4 \ud30c\uc77c\uc744 \ub123\uc5b4\uc8fc\uc138\uc694",
+ "ssh_not_file": "SSH \ud0a4 \ud30c\uc77c\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "mode": "\ubaa8\ub4dc",
+ "name": "\uc774\ub984",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "port": "\ud3ec\ud2b8",
+ "protocol": "\uc0ac\uc6a9\ud560 \ud1b5\uc2e0 \ud504\ub85c\ud1a0\ucf5c",
+ "ssh_key": "SSH \ud0a4 \ud30c\uc77c\uc758 \uacbd\ub85c (\ube44\ubc00\ubc88\ud638 \ub300\uccb4)",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ },
+ "description": "\ub77c\uc6b0\ud130\uc5d0 \uc5f0\uacb0\ud558\ub294 \ub370 \ud544\uc694\ud55c \ub9e4\uac1c \ubcc0\uc218\ub97c \uc124\uc815\ud558\uae30",
+ "title": "AsusWRT"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "\uae30\uae30\uac00 \uc678\ucd9c \uc0c1\ud0dc\ub85c \uac04\uc8fc\ub418\uae30 \uc804\uc5d0 \uae30\ub2e4\ub9ac\ub294 \uc2dc\uac04 (\ucd08)",
+ "dnsmasq": "dnsmasq.lease \ud30c\uc77c\uc758 \ub77c\uc6b0\ud130 \uc704\uce58",
+ "interface": "\ud1b5\uacc4\ub97c \uc6d0\ud558\ub294 \uc778\ud130\ud398\uc774\uc2a4 (\uc608: eth0, eth1 \ub4f1)",
+ "require_ip": "\uae30\uae30\uc5d0\ub294 IP\uac00 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4 (\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 \ubaa8\ub4dc\uc778 \uacbd\uc6b0)",
+ "track_unknown": "\uc54c \uc218 \uc5c6\uac70\ub098 \uc774\ub984\uc774 \uc5c6\ub294 \uae30\uae30 \ucd94\uc801\ud558\uae30"
+ },
+ "title": "AsusWRT \uc635\uc158"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/asuswrt/translations/nl.json b/homeassistant/components/asuswrt/translations/nl.json
new file mode 100644
index 00000000000000..f6f347f771ff0f
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/nl.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_host": "Ongeldige hostnaam of IP-adres",
+ "pwd_and_ssh": "Geef alleen wachtwoord of SSH-sleutelbestand op",
+ "pwd_or_ssh": "Geef een wachtwoord of SSH-sleutelbestand op",
+ "ssh_not_file": "SSH-sleutelbestand niet gevonden",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "mode": "Mode",
+ "name": "Naam",
+ "password": "Wachtwoord",
+ "port": "Poort",
+ "protocol": "Te gebruiken communicatieprotocol",
+ "ssh_key": "Pad naar uw SSH-sleutelbestand (in plaats van wachtwoord)",
+ "username": "Gebruikersnaam"
+ },
+ "description": "Stel de vereiste parameter in om verbinding te maken met uw router",
+ "title": "AsusWRT"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "Aantal seconden dat wordt gewacht voordat een apparaat als afwezig wordt beschouwd",
+ "dnsmasq": "De locatie in de router van de dnsmasq.leases-bestanden",
+ "interface": "De interface waarvan u statistieken wilt (bijv. Eth0, eth1 enz.)",
+ "require_ip": "Apparaten moeten een IP-adres hebben (voor toegangspuntmodus)",
+ "track_unknown": "Volg onbekende / naamloze apparaten"
+ },
+ "title": "AsusWRT-opties"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/asuswrt/translations/no.json b/homeassistant/components/asuswrt/translations/no.json
new file mode 100644
index 00000000000000..42c9798d49578c
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/no.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_host": "Ugyldig vertsnavn eller IP-adresse",
+ "pwd_and_ssh": "Oppgi bare passord eller SSH-n\u00f8kkelfil",
+ "pwd_or_ssh": "Oppgi passord eller SSH-n\u00f8kkelfil",
+ "ssh_not_file": "Finner ikke SSH-n\u00f8kkelfilen",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Vert",
+ "mode": "Modus",
+ "name": "Navn",
+ "password": "Passord",
+ "port": "Port",
+ "protocol": "Kommunikasjonsprotokoll som skal brukes",
+ "ssh_key": "Bane til SSH-n\u00f8kkelfilen (i stedet for passord)",
+ "username": "Brukernavn"
+ },
+ "description": "Sett \u00f8nsket parameter for \u00e5 koble til ruteren",
+ "title": "AsusWRT"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "Sekunder \u00e5 vente f\u00f8r du vurderer en enhet borte",
+ "dnsmasq": "Plasseringen i ruteren til dnsmasq.leases-filene",
+ "interface": "Grensesnittet du vil ha statistikk fra (f.eks. Eth0, eth1 osv.)",
+ "require_ip": "Enheter m\u00e5 ha IP (for tilgangspunktmodus)",
+ "track_unknown": "Spor ukjente / ikke-navngitte enheter"
+ },
+ "title": "AsusWRT-alternativer"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/asuswrt/translations/pl.json b/homeassistant/components/asuswrt/translations/pl.json
new file mode 100644
index 00000000000000..9fd5d00b1c4545
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/pl.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_host": "Nieprawid\u0142owa nazwa hosta lub adres IP",
+ "pwd_and_ssh": "Podaj tylko has\u0142o lub plik z kluczem SSH",
+ "pwd_or_ssh": "Podaj has\u0142o lub plik z kluczem SSH",
+ "ssh_not_file": "Nie znaleziono pliku z kluczem SSH",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP",
+ "mode": "Tryb",
+ "name": "Nazwa",
+ "password": "Has\u0142o",
+ "port": "Port",
+ "protocol": "Wybierz protok\u00f3\u0142 komunikacyjny",
+ "ssh_key": "\u015acie\u017cka do pliku z kluczem SSH (zamiast has\u0142a)",
+ "username": "Nazwa u\u017cytkownika"
+ },
+ "description": "Ustaw wymagany parametr, aby po\u0142\u0105czy\u0107 si\u0119 z routerem",
+ "title": "AsusWRT"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "Czas w sekundach, zanim urz\u0105dzenie utrzyma stan \"poza domem\"",
+ "dnsmasq": "Lokalizacja w routerze plik\u00f3w dnsmasq.leases",
+ "interface": "Interfejs, z kt\u00f3rego chcesz uzyska\u0107 statystyki (np. eth0, eth1 itp.)",
+ "require_ip": "Urz\u0105dzenia musz\u0105 mie\u0107 adres IP (w trybie punktu dost\u0119pu)",
+ "track_unknown": "\u015aled\u017a nieznane / nienazwane urz\u0105dzenia"
+ },
+ "title": "Opcje AsusWRT"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/asuswrt/translations/ru.json b/homeassistant/components/asuswrt/translations/ru.json
new file mode 100644
index 00000000000000..a2090b1faf6374
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/ru.json
@@ -0,0 +1,45 @@
+{
+ "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."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.",
+ "pwd_and_ssh": "\u041d\u0443\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0430\u0440\u043e\u043b\u044c \u0438\u043b\u0438 \u0442\u043e\u043b\u044c\u043a\u043e \u0444\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 SSH.",
+ "pwd_or_ssh": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0438\u043b\u0438 \u0444\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 SSH.",
+ "ssh_not_file": "\u0424\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 SSH \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "mode": "\u0420\u0435\u0436\u0438\u043c",
+ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0441\u0432\u044f\u0437\u0438",
+ "ssh_key": "\u041f\u0443\u0442\u044c \u0444\u0430\u0439\u043b\u0443 \u043a\u043b\u044e\u0447\u0435\u0439 SSH (\u0432\u043c\u0435\u0441\u0442\u043e \u043f\u0430\u0440\u043e\u043b\u044f)",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0440\u043e\u0443\u0442\u0435\u0440\u0443.",
+ "title": "AsusWRT"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "\u0412\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0434\u043e\u043c\u0430\"",
+ "dnsmasq": "\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0432 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0435 \u0444\u0430\u0439\u043b\u043e\u0432 dnsmasq.leases",
+ "interface": "\u0418\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441, \u0441 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0443 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, eth0, eth1 \u0438 \u0442. \u0434.)",
+ "require_ip": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0441 IP-\u0430\u0434\u0440\u0435\u0441\u043e\u043c (\u0434\u043b\u044f \u0440\u0435\u0436\u0438\u043c\u0430 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430)",
+ "track_unknown": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0435/\u0431\u0435\u0437\u044b\u043c\u044f\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430"
+ },
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AsusWRT"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/asuswrt/translations/zh-Hant.json b/homeassistant/components/asuswrt/translations/zh-Hant.json
new file mode 100644
index 00000000000000..8caddacd23e159
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/zh-Hant.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740",
+ "pwd_and_ssh": "\u50c5\u63d0\u4f9b\u5bc6\u78bc\u6216 SSH \u5bc6\u9470\u6a94\u6848",
+ "pwd_or_ssh": "\u8acb\u8f38\u5165\u5bc6\u78bc\u6216 SSH \u5bc6\u9470\u6a94\u6848",
+ "ssh_not_file": "\u627e\u4e0d\u5230 SSH \u5bc6\u9470\u6a94\u6848",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "mode": "\u6a21\u5f0f",
+ "name": "\u540d\u7a31",
+ "password": "\u5bc6\u78bc",
+ "port": "\u901a\u8a0a\u57e0",
+ "protocol": "\u4f7f\u7528\u901a\u8a0a\u606f\u5354\u5b9a",
+ "ssh_key": "SSH \u5bc6\u9470\u6a94\u6848\u8def\u5f91\uff08\u975e\u5bc6\u78bc\uff09",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ },
+ "description": "\u8a2d\u5b9a\u6240\u9700\u53c3\u6578\u4ee5\u9023\u7dda\u81f3\u8def\u7531\u5668",
+ "title": "AsusWRT"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "consider_home": "\u8996\u70ba\u96e2\u958b\u7684\u7b49\u5019\u79d2\u6578",
+ "dnsmasq": "dnsmasq.leases \u6a94\u6848\u65bc\u8def\u7531\u5668\u4e2d\u6240\u5728\u4f4d\u7f6e",
+ "interface": "\u6240\u8981\u9032\u884c\u7d71\u8a08\u7684\u4ecb\u9762\u53e3\uff08\u4f8b\u5982 eth0\u3001eth1 \u7b49\uff09",
+ "require_ip": "\u88dd\u7f6e\u5fc5\u9808\u5177\u6709 IP\uff08\u7528\u65bc AP \u6a21\u5f0f\uff09",
+ "track_unknown": "\u8ffd\u8e64\u672a\u77e5 / \u672a\u547d\u540d\u88dd\u7f6e"
+ },
+ "title": "AsusWRT \u9078\u9805"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py
index 7489ada3341a1c..017e9968d1ec8d 100644
--- a/homeassistant/components/atag/__init__.py
+++ b/homeassistant/components/atag/__init__.py
@@ -10,7 +10,6 @@
from homeassistant.components.water_heater import DOMAIN as WATER_HEATER
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, asyncio
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
@@ -24,24 +23,34 @@
PLATFORMS = [CLIMATE, WATER_HEATER, SENSOR]
-async def async_setup(hass: HomeAssistant, config):
- """Set up the Atag component."""
- return True
-
-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Atag integration from a config entry."""
- session = async_get_clientsession(hass)
- coordinator = AtagDataUpdateCoordinator(hass, session, entry)
- await coordinator.async_refresh()
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ async def _async_update_data():
+ """Update data via library."""
+ with async_timeout.timeout(20):
+ try:
+ await atag.update()
+ except AtagException as err:
+ raise UpdateFailed(err) from err
+ return atag
+
+ atag = AtagOne(
+ session=async_get_clientsession(hass), **entry.data, device=entry.unique_id
+ )
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name=DOMAIN.title(),
+ update_method=_async_update_data,
+ update_interval=timedelta(seconds=60),
+ )
+
+ await coordinator.async_config_entry_first_refresh()
- hass.data.setdefault(DOMAIN, {})
- hass.data[DOMAIN][entry.entry_id] = coordinator
+ hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
if entry.unique_id is None:
- hass.config_entries.async_update_entry(entry, unique_id=coordinator.atag.id)
+ hass.config_entries.async_update_entry(entry, unique_id=atag.id)
for platform in PLATFORMS:
hass.async_create_task(
@@ -51,35 +60,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
return True
-class AtagDataUpdateCoordinator(DataUpdateCoordinator):
- """Define an object to hold Atag data."""
-
- def __init__(self, hass, session, entry):
- """Initialize."""
- self.atag = AtagOne(session=session, **entry.data)
-
- super().__init__(
- hass, _LOGGER, name=DOMAIN, update_interval=timedelta(seconds=30)
- )
-
- async def _async_update_data(self):
- """Update data via library."""
- with async_timeout.timeout(20):
- try:
- if not await self.atag.update():
- raise UpdateFailed("No data received")
- except AtagException as error:
- raise UpdateFailed(error) from error
- return self.atag.report
-
-
async def async_unload_entry(hass, entry):
"""Unload Atag config entry."""
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -91,7 +78,7 @@ async def async_unload_entry(hass, entry):
class AtagEntity(CoordinatorEntity):
"""Defines a base Atag entity."""
- def __init__(self, coordinator: AtagDataUpdateCoordinator, atag_id: str) -> None:
+ def __init__(self, coordinator: DataUpdateCoordinator, atag_id: str) -> None:
"""Initialize the Atag entity."""
super().__init__(coordinator)
@@ -101,8 +88,8 @@ def __init__(self, coordinator: AtagDataUpdateCoordinator, atag_id: str) -> None
@property
def device_info(self) -> dict:
"""Return info for device registry."""
- device = self.coordinator.atag.id
- version = self.coordinator.atag.apiversion
+ device = self.coordinator.data.id
+ version = self.coordinator.data.apiversion
return {
"identifiers": {(DOMAIN, device)},
"name": "Atag Thermostat",
@@ -119,4 +106,4 @@ def name(self) -> str:
@property
def unique_id(self):
"""Return a unique ID to use for this entity."""
- return f"{self.coordinator.atag.id}-{self._id}"
+ return f"{self.coordinator.data.id}-{self._id}"
diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py
index ad46fefe8c2bec..da7e6a14a73ef6 100644
--- a/homeassistant/components/atag/climate.py
+++ b/homeassistant/components/atag/climate.py
@@ -1,5 +1,5 @@
"""Initialization of ATAG One climate platform."""
-from typing import List, Optional
+from __future__ import annotations
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
@@ -16,16 +16,14 @@
from . import CLIMATE, DOMAIN, AtagEntity
-PRESET_SCHEDULE = "Auto"
-PRESET_MANUAL = "Manual"
-PRESET_EXTEND = "Extend"
-SUPPORT_PRESET = [
- PRESET_MANUAL,
- PRESET_SCHEDULE,
- PRESET_EXTEND,
- PRESET_AWAY,
- PRESET_BOOST,
-]
+PRESET_MAP = {
+ "Manual": "manual",
+ "Auto": "automatic",
+ "Extend": "extend",
+ PRESET_AWAY: "vacation",
+ PRESET_BOOST: "fireplace",
+}
+PRESET_INVERTED = {v: k for k, v in PRESET_MAP.items()}
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
HVAC_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT]
@@ -45,60 +43,60 @@ def supported_features(self):
return SUPPORT_FLAGS
@property
- def hvac_mode(self) -> Optional[str]:
+ def hvac_mode(self) -> str | None:
"""Return hvac operation ie. heat, cool mode."""
- if self.coordinator.atag.climate.hvac_mode in HVAC_MODES:
- return self.coordinator.atag.climate.hvac_mode
+ if self.coordinator.data.climate.hvac_mode in HVAC_MODES:
+ return self.coordinator.data.climate.hvac_mode
return None
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes."""
return HVAC_MODES
@property
- def hvac_action(self) -> Optional[str]:
+ def hvac_action(self) -> str | None:
"""Return the current running hvac operation."""
- if self.coordinator.atag.climate.status:
- return CURRENT_HVAC_HEAT
- return CURRENT_HVAC_IDLE
+ is_active = self.coordinator.data.climate.status
+ return CURRENT_HVAC_HEAT if is_active else CURRENT_HVAC_IDLE
@property
- def temperature_unit(self):
+ def temperature_unit(self) -> str | None:
"""Return the unit of measurement."""
- return self.coordinator.atag.climate.temp_unit
+ return self.coordinator.data.climate.temp_unit
@property
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
- return self.coordinator.atag.climate.temperature
+ return self.coordinator.data.climate.temperature
@property
- def target_temperature(self) -> Optional[float]:
+ def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
- return self.coordinator.atag.climate.target_temperature
+ return self.coordinator.data.climate.target_temperature
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., auto, manual, fireplace, extend, etc."""
- return self.coordinator.atag.climate.preset_mode
+ preset = self.coordinator.data.climate.preset_mode
+ return PRESET_INVERTED.get(preset)
@property
- def preset_modes(self) -> Optional[List[str]]:
+ def preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes."""
- return SUPPORT_PRESET
+ return list(PRESET_MAP.keys())
async def async_set_temperature(self, **kwargs) -> None:
"""Set new target temperature."""
- await self.coordinator.atag.climate.set_temp(kwargs.get(ATTR_TEMPERATURE))
+ await self.coordinator.data.climate.set_temp(kwargs.get(ATTR_TEMPERATURE))
self.async_write_ha_state()
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set new target hvac mode."""
- await self.coordinator.atag.climate.set_hvac_mode(hvac_mode)
+ await self.coordinator.data.climate.set_hvac_mode(hvac_mode)
self.async_write_ha_state()
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
- await self.coordinator.atag.climate.set_preset_mode(preset_mode)
+ await self.coordinator.data.climate.set_preset_mode(PRESET_MAP[preset_mode])
self.async_write_ha_state()
diff --git a/homeassistant/components/atag/config_flow.py b/homeassistant/components/atag/config_flow.py
index 865159aa65806c..20055bd8a9aa22 100644
--- a/homeassistant/components/atag/config_flow.py
+++ b/homeassistant/components/atag/config_flow.py
@@ -3,14 +3,13 @@
import voluptuous as vol
from homeassistant import config_entries
-from homeassistant.const import CONF_EMAIL, CONF_HOST, CONF_PORT
+from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from . import DOMAIN # pylint: disable=unused-import
+from . import DOMAIN
DATA_SCHEMA = {
vol.Required(CONF_HOST): str,
- vol.Optional(CONF_EMAIL): str,
vol.Required(CONF_PORT, default=pyatag.const.DEFAULT_PORT): vol.Coerce(int),
}
@@ -26,15 +25,14 @@ async def async_step_user(self, user_input=None):
if not user_input:
return await self._show_form()
- session = async_get_clientsession(self.hass)
+
+ atag = pyatag.AtagOne(session=async_get_clientsession(self.hass), **user_input)
try:
- atag = pyatag.AtagOne(session=session, **user_input)
- await atag.authorize()
- await atag.update(force=True)
+ await atag.update()
- except pyatag.errors.Unauthorized:
+ except pyatag.Unauthorized:
return await self._show_form({"base": "unauthorized"})
- except pyatag.errors.AtagException:
+ except pyatag.AtagException:
return await self._show_form({"base": "cannot_connect"})
await self.async_set_unique_id(atag.id)
diff --git a/homeassistant/components/atag/manifest.json b/homeassistant/components/atag/manifest.json
index 5e94afb06d31cb..1154a120f9137e 100644
--- a/homeassistant/components/atag/manifest.json
+++ b/homeassistant/components/atag/manifest.json
@@ -3,6 +3,6 @@
"name": "Atag",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/atag/",
- "requirements": ["pyatag==0.3.4.4"],
+ "requirements": ["pyatag==0.3.5.3"],
"codeowners": ["@MatsNL"]
}
diff --git a/homeassistant/components/atag/sensor.py b/homeassistant/components/atag/sensor.py
index d6abe16ffdb7b3..88ccbdc899ff29 100644
--- a/homeassistant/components/atag/sensor.py
+++ b/homeassistant/components/atag/sensor.py
@@ -1,4 +1,5 @@
"""Initialization of ATAG One sensor platform."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
@@ -26,13 +27,10 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Initialize sensor platform from config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
- entities = []
- for sensor in SENSORS:
- entities.append(AtagSensor(coordinator, sensor))
- async_add_entities(entities)
+ async_add_entities([AtagSensor(coordinator, sensor) for sensor in SENSORS])
-class AtagSensor(AtagEntity):
+class AtagSensor(AtagEntity, SensorEntity):
"""Representation of a AtagOne Sensor."""
def __init__(self, coordinator, sensor):
@@ -43,32 +41,32 @@ def __init__(self, coordinator, sensor):
@property
def state(self):
"""Return the state of the sensor."""
- return self.coordinator.data[self._id].state
+ return self.coordinator.data.report[self._id].state
@property
def icon(self):
"""Return icon."""
- return self.coordinator.data[self._id].icon
+ return self.coordinator.data.report[self._id].icon
@property
def device_class(self):
"""Return deviceclass."""
- if self.coordinator.data[self._id].sensorclass in [
+ if self.coordinator.data.report[self._id].sensorclass in [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
]:
- return self.coordinator.data[self._id].sensorclass
+ return self.coordinator.data.report[self._id].sensorclass
return None
@property
def unit_of_measurement(self):
"""Return measure."""
- if self.coordinator.data[self._id].measure in [
+ if self.coordinator.data.report[self._id].measure in [
PRESSURE_BAR,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
PERCENTAGE,
TIME_HOURS,
]:
- return self.coordinator.data[self._id].measure
+ return self.coordinator.data.report[self._id].measure
return None
diff --git a/homeassistant/components/atag/strings.json b/homeassistant/components/atag/strings.json
index b06e9188b5b9b4..39ed972524dfc1 100644
--- a/homeassistant/components/atag/strings.json
+++ b/homeassistant/components/atag/strings.json
@@ -5,7 +5,6 @@
"title": "Connect to the device",
"data": {
"host": "[%key:common::config_flow::data::host%]",
- "email": "[%key:common::config_flow::data::email%]",
"port": "[%key:common::config_flow::data::port%]"
}
}
diff --git a/homeassistant/components/atag/translations/de.json b/homeassistant/components/atag/translations/de.json
index 2ced7577fdf937..8b2b7ce4dff990 100644
--- a/homeassistant/components/atag/translations/de.json
+++ b/homeassistant/components/atag/translations/de.json
@@ -1,15 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "Dieses Ger\u00e4t wurde bereits zu HomeAssistant hinzugef\u00fcgt"
+ "already_configured": "Dieses Ger\u00e4t wurde bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "unauthorized": "Pairing verweigert, Ger\u00e4t auf Authentifizierungsanforderung pr\u00fcfen"
},
"step": {
"user": {
"data": {
- "email": "Email (Optional)",
+ "email": "E-Mail",
"host": "Host",
"port": "Port"
},
diff --git a/homeassistant/components/atag/translations/et.json b/homeassistant/components/atag/translations/et.json
index 2a4094806ed81b..fd0651a219c628 100644
--- a/homeassistant/components/atag/translations/et.json
+++ b/homeassistant/components/atag/translations/et.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u00dchendamine nurjus",
- "unauthorized": "Sidumine on keelatud, kontrollige seadme tuvastamistaotlust"
+ "unauthorized": "Sidumine on keelatud, kontrolli seadme tuvastamistaotlust"
},
"step": {
"user": {
diff --git a/homeassistant/components/atag/translations/hu.json b/homeassistant/components/atag/translations/hu.json
index 1d28556ba1ad5a..98e947ae64387c 100644
--- a/homeassistant/components/atag/translations/hu.json
+++ b/homeassistant/components/atag/translations/hu.json
@@ -1,11 +1,15 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
"step": {
"user": {
"data": {
+ "email": "E-mail",
"host": "Hoszt",
"port": "Port"
}
diff --git a/homeassistant/components/atag/translations/id.json b/homeassistant/components/atag/translations/id.json
new file mode 100644
index 00000000000000..24732f8c235bda
--- /dev/null
+++ b/homeassistant/components/atag/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "unauthorized": "Pemasangan ditolak, periksa perangkat untuk permintaan autentikasi"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Email",
+ "host": "Host",
+ "port": "Port"
+ },
+ "title": "Hubungkan ke perangkat"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/atag/translations/ko.json b/homeassistant/components/atag/translations/ko.json
index c09b4f7b249432..9b0c1ea1b3665e 100644
--- a/homeassistant/components/atag/translations/ko.json
+++ b/homeassistant/components/atag/translations/ko.json
@@ -1,15 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 HomeAssistant \uc5d0 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"unauthorized": "\ud398\uc5b4\ub9c1\uc774 \uac70\ubd80\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc778\uc99d \uc694\uccad \uae30\uae30\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694"
},
"step": {
"user": {
"data": {
- "email": "\uc774\uba54\uc77c (\uc120\ud0dd \uc0ac\ud56d)",
+ "email": "\uc774\uba54\uc77c",
"host": "\ud638\uc2a4\ud2b8",
"port": "\ud3ec\ud2b8"
},
diff --git a/homeassistant/components/atag/translations/tr.json b/homeassistant/components/atag/translations/tr.json
new file mode 100644
index 00000000000000..f7c94d0a976461
--- /dev/null
+++ b/homeassistant/components/atag/translations/tr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unauthorized": "E\u015fle\u015ftirme reddedildi, kimlik do\u011frulama iste\u011fi i\u00e7in cihaz\u0131 kontrol edin"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-posta",
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ },
+ "title": "Cihaza ba\u011flan\u0131n"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/atag/translations/uk.json b/homeassistant/components/atag/translations/uk.json
new file mode 100644
index 00000000000000..ee0a077d900852
--- /dev/null
+++ b/homeassistant/components/atag/translations/uk.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "unauthorized": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0437\u0430\u0431\u043e\u0440\u043e\u043d\u0435\u043d\u043e, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0430 \u0437\u0430\u043f\u0438\u0442 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438",
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/atag/water_heater.py b/homeassistant/components/atag/water_heater.py
index f9c2a4625bb7d1..dac56edf89d599 100644
--- a/homeassistant/components/atag/water_heater.py
+++ b/homeassistant/components/atag/water_heater.py
@@ -35,12 +35,12 @@ def temperature_unit(self):
@property
def current_temperature(self):
"""Return the current temperature."""
- return self.coordinator.atag.dhw.temperature
+ return self.coordinator.data.dhw.temperature
@property
def current_operation(self):
"""Return current operation."""
- operation = self.coordinator.atag.dhw.current_operation
+ operation = self.coordinator.data.dhw.current_operation
return operation if operation in self.operation_list else STATE_OFF
@property
@@ -50,20 +50,20 @@ def operation_list(self):
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
- if await self.coordinator.atag.dhw.set_temp(kwargs.get(ATTR_TEMPERATURE)):
+ if await self.coordinator.data.dhw.set_temp(kwargs.get(ATTR_TEMPERATURE)):
self.async_write_ha_state()
@property
def target_temperature(self):
"""Return the setpoint if water demand, otherwise return base temp (comfort level)."""
- return self.coordinator.atag.dhw.target_temperature
+ return self.coordinator.data.dhw.target_temperature
@property
def max_temp(self):
"""Return the maximum temperature."""
- return self.coordinator.atag.dhw.max_temp
+ return self.coordinator.data.dhw.max_temp
@property
def min_temp(self):
"""Return the minimum temperature."""
- return self.coordinator.atag.dhw.min_temp
+ return self.coordinator.data.dhw.min_temp
diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py
index 1a8585653fe088..d10024f64c244f 100644
--- a/homeassistant/components/atome/sensor.py
+++ b/homeassistant/components/atome/sensor.py
@@ -5,7 +5,7 @@
from pyatome.client import AtomeClient, PyAtomeError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_NAME,
CONF_PASSWORD,
@@ -15,7 +15,6 @@
POWER_WATT,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -215,7 +214,7 @@ def update_year_usage(self):
_LOGGER.error("Missing last value in values: %s: %s", values, error)
-class AtomeSensor(Entity):
+class AtomeSensor(SensorEntity):
"""Representation of a sensor entity for Atome."""
def __init__(self, data, name, sensor_type):
@@ -243,7 +242,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attributes
diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py
index 6f16f7d5b312d2..46acd1132d9f1c 100644
--- a/homeassistant/components/august/__init__.py
+++ b/homeassistant/components/august/__init__.py
@@ -1,172 +1,37 @@
"""Support for August devices."""
import asyncio
-import itertools
+from itertools import chain
import logging
from aiohttp import ClientError, ClientResponseError
-from august.authenticator import ValidationResult
-from august.exceptions import AugustApiAIOHTTPError
-import voluptuous as vol
-
-from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
-from homeassistant.const import (
- CONF_PASSWORD,
- CONF_TIMEOUT,
- CONF_USERNAME,
- HTTP_UNAUTHORIZED,
-)
-from homeassistant.core import HomeAssistant
+from yalexs.exceptions import AugustApiAIOHTTPError
+from yalexs.pubnub_activity import activities_from_pubnub_message
+from yalexs.pubnub_async import AugustPubNub, async_create_pubnub
+
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
+from homeassistant.const import CONF_PASSWORD, HTTP_UNAUTHORIZED
+from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
-import homeassistant.helpers.config_validation as cv
from .activity import ActivityStream
-from .const import (
- AUGUST_COMPONENTS,
- CONF_ACCESS_TOKEN_CACHE_FILE,
- CONF_INSTALL_ID,
- CONF_LOGIN_METHOD,
- DATA_AUGUST,
- DEFAULT_AUGUST_CONFIG_FILE,
- DEFAULT_NAME,
- DEFAULT_TIMEOUT,
- DOMAIN,
- LOGIN_METHODS,
- MIN_TIME_BETWEEN_DETAIL_UPDATES,
- VERIFICATION_CODE_KEY,
-)
+from .const import DATA_AUGUST, DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS
from .exceptions import CannotConnect, InvalidAuth, RequireValidation
from .gateway import AugustGateway
from .subscriber import AugustSubscriberMixin
_LOGGER = logging.getLogger(__name__)
-TWO_FA_REVALIDATE = "verify_configurator"
-
-CONFIG_SCHEMA = vol.Schema(
- vol.All(
- cv.deprecated(DOMAIN),
- {
- DOMAIN: vol.Schema(
- {
- vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS),
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_INSTALL_ID): cv.string,
- vol.Optional(
- CONF_TIMEOUT, default=DEFAULT_TIMEOUT
- ): cv.positive_int,
- }
- )
- },
- ),
- extra=vol.ALLOW_EXTRA,
+API_CACHED_ATTRS = (
+ "door_state",
+ "door_state_datetime",
+ "lock_status",
+ "lock_status_datetime",
)
-async def async_request_validation(hass, config_entry, august_gateway):
- """Request a new verification code from the user."""
-
- #
- # In the future this should start a new config flow
- # instead of using the legacy configurator
- #
- _LOGGER.error("Access token is no longer valid")
- configurator = hass.components.configurator
- entry_id = config_entry.entry_id
-
- async def async_august_configuration_validation_callback(data):
- code = data.get(VERIFICATION_CODE_KEY)
- result = await august_gateway.authenticator.async_validate_verification_code(
- code
- )
-
- if result == ValidationResult.INVALID_VERIFICATION_CODE:
- configurator.async_notify_errors(
- hass.data[DOMAIN][entry_id][TWO_FA_REVALIDATE],
- "Invalid verification code, please make sure you are using the latest code and try again.",
- )
- elif result == ValidationResult.VALIDATED:
- return await async_setup_august(hass, config_entry, august_gateway)
-
- return False
-
- if TWO_FA_REVALIDATE not in hass.data[DOMAIN][entry_id]:
- await august_gateway.authenticator.async_send_verification_code()
-
- entry_data = config_entry.data
- login_method = entry_data.get(CONF_LOGIN_METHOD)
- username = entry_data.get(CONF_USERNAME)
-
- hass.data[DOMAIN][entry_id][TWO_FA_REVALIDATE] = configurator.async_request_config(
- f"{DEFAULT_NAME} ({username})",
- async_august_configuration_validation_callback,
- description=(
- "August must be re-verified. "
- f"Please check your {login_method} ({username}) "
- "and enter the verification code below"
- ),
- submit_caption="Verify",
- fields=[
- {"id": VERIFICATION_CODE_KEY, "name": "Verification code", "type": "string"}
- ],
- )
- return
-
-
-async def async_setup_august(hass, config_entry, august_gateway):
- """Set up the August component."""
-
- entry_id = config_entry.entry_id
- hass.data[DOMAIN].setdefault(entry_id, {})
-
- try:
- await august_gateway.async_authenticate()
- except RequireValidation:
- await async_request_validation(hass, config_entry, august_gateway)
- raise
-
- # We still use the configurator to get a new 2fa code
- # when needed since config_flow doesn't have a way
- # to re-request if it expires
- if TWO_FA_REVALIDATE in hass.data[DOMAIN][entry_id]:
- hass.components.configurator.async_request_done(
- hass.data[DOMAIN][entry_id].pop(TWO_FA_REVALIDATE)
- )
-
- hass.data[DOMAIN][entry_id][DATA_AUGUST] = AugustData(hass, august_gateway)
-
- await hass.data[DOMAIN][entry_id][DATA_AUGUST].async_setup()
-
- for component in AUGUST_COMPONENTS:
- hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
- )
-
- return True
-
-
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the August component from YAML."""
-
- conf = config.get(DOMAIN)
hass.data.setdefault(DOMAIN, {})
-
- if not conf:
- return True
-
- hass.async_create_task(
- hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": SOURCE_IMPORT},
- data={
- CONF_LOGIN_METHOD: conf.get(CONF_LOGIN_METHOD),
- CONF_USERNAME: conf.get(CONF_USERNAME),
- CONF_PASSWORD: conf.get(CONF_PASSWORD),
- CONF_INSTALL_ID: conf.get(CONF_INSTALL_ID),
- CONF_ACCESS_TOKEN_CACHE_FILE: DEFAULT_AUGUST_CONFIG_FILE,
- },
- )
- )
return True
@@ -184,11 +49,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
return False
raise ConfigEntryNotReady from err
- except InvalidAuth:
+ except (RequireValidation, InvalidAuth):
_async_start_reauth(hass, entry)
return False
- except RequireValidation:
- return False
except (CannotConnect, asyncio.TimeoutError) as err:
raise ConfigEntryNotReady from err
@@ -197,7 +60,7 @@ def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
- context={"source": "reauth"},
+ context={"source": SOURCE_REAUTH},
data=entry.data,
)
)
@@ -206,11 +69,14 @@ def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry):
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
+
+ hass.data[DOMAIN][entry.entry_id][DATA_AUGUST].async_stop()
+
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in AUGUST_COMPONENTS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -221,6 +87,31 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
return unload_ok
+async def async_setup_august(hass, config_entry, august_gateway):
+ """Set up the August component."""
+
+ if CONF_PASSWORD in config_entry.data:
+ # We no longer need to store passwords since we do not
+ # support YAML anymore
+ config_data = config_entry.data.copy()
+ del config_data[CONF_PASSWORD]
+ hass.config_entries.async_update_entry(config_entry, data=config_data)
+
+ await august_gateway.async_authenticate()
+
+ data = hass.data[DOMAIN][config_entry.entry_id] = {
+ DATA_AUGUST: AugustData(hass, august_gateway)
+ }
+ await data[DATA_AUGUST].async_setup()
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ )
+
+ return True
+
+
class AugustData(AugustSubscriberMixin):
"""August data object."""
@@ -235,25 +126,27 @@ def __init__(self, hass, august_gateway):
self._doorbells_by_id = {}
self._locks_by_id = {}
self._house_ids = set()
+ self._pubnub_unsub = None
async def async_setup(self):
"""Async setup of august device data and activities."""
- locks = (
- await self._api.async_get_operable_locks(self._august_gateway.access_token)
- or []
- )
- doorbells = (
- await self._api.async_get_doorbells(self._august_gateway.access_token) or []
+ token = self._august_gateway.access_token
+ user_data, locks, doorbells = await asyncio.gather(
+ self._api.async_get_user(token),
+ self._api.async_get_operable_locks(token),
+ self._api.async_get_doorbells(token),
)
+ if not doorbells:
+ doorbells = []
+ if not locks:
+ locks = []
self._doorbells_by_id = {device.device_id: device for device in doorbells}
self._locks_by_id = {device.device_id: device for device in locks}
- self._house_ids = {
- device.house_id for device in itertools.chain(locks, doorbells)
- }
+ self._house_ids = {device.house_id for device in chain(locks, doorbells)}
await self._async_refresh_device_detail_by_ids(
- [device.device_id for device in itertools.chain(locks, doorbells)]
+ [device.device_id for device in chain(locks, doorbells)]
)
# We remove all devices that we are missing
@@ -263,10 +156,32 @@ async def async_setup(self):
self._remove_inoperative_locks()
self._remove_inoperative_doorbells()
+ pubnub = AugustPubNub()
+ for device in self._device_detail_by_id.values():
+ pubnub.register_device(device)
+
self.activity_stream = ActivityStream(
- self._hass, self._api, self._august_gateway, self._house_ids
+ self._hass, self._api, self._august_gateway, self._house_ids, pubnub
)
await self.activity_stream.async_setup()
+ pubnub.subscribe(self.async_pubnub_message)
+ self._pubnub_unsub = async_create_pubnub(user_data["UserID"], pubnub)
+
+ @callback
+ def async_pubnub_message(self, device_id, date_time, message):
+ """Process a pubnub message."""
+ device = self.get_device_detail(device_id)
+ activities = activities_from_pubnub_message(device, date_time, message)
+ if activities:
+ self.activity_stream.async_process_newer_device_activities(activities)
+ self.async_signal_device_id_update(device.device_id)
+ self.activity_stream.async_schedule_house_id_refresh(device.house_id)
+
+ @callback
+ def async_stop(self):
+ """Stop the subscriptions."""
+ self._pubnub_unsub()
+ self.activity_stream.async_stop()
@property
def doorbells(self):
@@ -286,27 +201,38 @@ async def _async_refresh(self, time):
await self._async_refresh_device_detail_by_ids(self._subscriptions.keys())
async def _async_refresh_device_detail_by_ids(self, device_ids_list):
- for device_id in device_ids_list:
- if device_id in self._locks_by_id:
- await self._async_update_device_detail(
- self._locks_by_id[device_id], self._api.async_get_lock_detail
- )
- # keypads are always attached to locks
- if (
- device_id in self._device_detail_by_id
- and self._device_detail_by_id[device_id].keypad is not None
- ):
- keypad = self._device_detail_by_id[device_id].keypad
- self._device_detail_by_id[keypad.device_id] = keypad
- elif device_id in self._doorbells_by_id:
- await self._async_update_device_detail(
- self._doorbells_by_id[device_id],
- self._api.async_get_doorbell_detail,
- )
- _LOGGER.debug(
- "async_signal_device_id_update (from detail updates): %s", device_id
+ await asyncio.gather(
+ *[
+ self._async_refresh_device_detail_by_id(device_id)
+ for device_id in device_ids_list
+ ]
+ )
+
+ async def _async_refresh_device_detail_by_id(self, device_id):
+ if device_id in self._locks_by_id:
+ if self.activity_stream and self.activity_stream.pubnub.connected:
+ saved_attrs = _save_live_attrs(self._device_detail_by_id[device_id])
+ await self._async_update_device_detail(
+ self._locks_by_id[device_id], self._api.async_get_lock_detail
+ )
+ if self.activity_stream and self.activity_stream.pubnub.connected:
+ _restore_live_attrs(self._device_detail_by_id[device_id], saved_attrs)
+ # keypads are always attached to locks
+ if (
+ device_id in self._device_detail_by_id
+ and self._device_detail_by_id[device_id].keypad is not None
+ ):
+ keypad = self._device_detail_by_id[device_id].keypad
+ self._device_detail_by_id[keypad.device_id] = keypad
+ elif device_id in self._doorbells_by_id:
+ await self._async_update_device_detail(
+ self._doorbells_by_id[device_id],
+ self._api.async_get_doorbell_detail,
)
- self.async_signal_device_id_update(device_id)
+ _LOGGER.debug(
+ "async_signal_device_id_update (from detail updates): %s", device_id
+ )
+ self.async_signal_device_id_update(device_id)
async def _async_update_device_detail(self, device, api_call):
_LOGGER.debug(
@@ -334,9 +260,9 @@ async def _async_update_device_detail(self, device, api_call):
def _get_device_name(self, device_id):
"""Return doorbell or lock name as August has it stored."""
- if self._locks_by_id.get(device_id):
+ if device_id in self._locks_by_id:
return self._locks_by_id[device_id].device_name
- if self._doorbells_by_id.get(device_id):
+ if device_id in self._doorbells_by_id:
return self._doorbells_by_id[device_id].device_name
async def async_lock(self, device_id):
@@ -373,8 +299,7 @@ async def _async_call_api_op_requires_bridge(
return ret
def _remove_inoperative_doorbells(self):
- doorbells = list(self.doorbells)
- for doorbell in doorbells:
+ for doorbell in list(self.doorbells):
device_id = doorbell.device_id
doorbell_is_operative = False
doorbell_detail = self._device_detail_by_id.get(device_id)
@@ -394,9 +319,7 @@ def _remove_inoperative_locks(self):
# Remove non-operative locks as there must
# be a bridge (August Connect) for them to
# be usable
- locks = list(self.locks)
-
- for lock in locks:
+ for lock in list(self.locks):
device_id = lock.device_id
lock_is_operative = False
lock_detail = self._device_detail_by_id.get(device_id)
@@ -410,14 +333,27 @@ def _remove_inoperative_locks(self):
"The lock %s could not be setup because it does not have a bridge (Connect)",
lock.device_name,
)
- elif not lock_detail.bridge.operative:
- _LOGGER.info(
- "The lock %s could not be setup because the bridge (Connect) is not operative",
- lock.device_name,
- )
+ # Bridge may come back online later so we still add the device since we will
+ # have a pubnub subscription to tell use when it recovers
else:
lock_is_operative = True
if not lock_is_operative:
del self._locks_by_id[device_id]
del self._device_detail_by_id[device_id]
+
+
+def _save_live_attrs(lock_detail):
+ """Store the attributes that the lock detail api may have an invalid cache for.
+
+ Since we are connected to pubnub we may have more current data
+ then the api so we want to restore the most current data after
+ updating battery state etc.
+ """
+ return {attr: getattr(lock_detail, attr) for attr in API_CACHED_ATTRS}
+
+
+def _restore_live_attrs(lock_detail, attrs):
+ """Restore the non-cache attributes after a cached update."""
+ for attr, value in attrs.items():
+ setattr(lock_detail, attr, value)
diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py
index d972fbf52817e0..18f390b4f8f5ec 100644
--- a/homeassistant/components/august/activity.py
+++ b/homeassistant/components/august/activity.py
@@ -1,8 +1,12 @@
"""Consume the august activity stream."""
+import asyncio
import logging
from aiohttp import ClientError
+from homeassistant.core import callback
+from homeassistant.helpers.debounce import Debouncer
+from homeassistant.helpers.event import async_call_later
from homeassistant.util.dt import utcnow
from .const import ACTIVITY_UPDATE_INTERVAL
@@ -17,27 +21,58 @@
class ActivityStream(AugustSubscriberMixin):
"""August activity stream handler."""
- def __init__(self, hass, api, august_gateway, house_ids):
+ def __init__(self, hass, api, august_gateway, house_ids, pubnub):
"""Init August activity stream object."""
super().__init__(hass, ACTIVITY_UPDATE_INTERVAL)
self._hass = hass
+ self._schedule_updates = {}
self._august_gateway = august_gateway
self._api = api
self._house_ids = house_ids
- self._latest_activities_by_id_type = {}
+ self._latest_activities = {}
self._last_update_time = None
self._abort_async_track_time_interval = None
+ self.pubnub = pubnub
+ self._update_debounce = {}
async def async_setup(self):
"""Token refresh check and catch up the activity stream."""
- await self._async_refresh(utcnow)
+ for house_id in self._house_ids:
+ self._update_debounce[house_id] = self._async_create_debouncer(house_id)
+
+ await self._async_refresh(utcnow())
+
+ @callback
+ def _async_create_debouncer(self, house_id):
+ """Create a debouncer for the house id."""
+
+ async def _async_update_house_id():
+ await self._async_update_house_id(house_id)
+
+ return Debouncer(
+ self._hass,
+ _LOGGER,
+ cooldown=ACTIVITY_UPDATE_INTERVAL.seconds,
+ immediate=True,
+ function=_async_update_house_id,
+ )
+
+ @callback
+ def async_stop(self):
+ """Cleanup any debounces."""
+ for debouncer in self._update_debounce.values():
+ debouncer.async_cancel()
+ for house_id in self._schedule_updates:
+ if self._schedule_updates[house_id] is not None:
+ self._schedule_updates[house_id]()
+ self._schedule_updates[house_id] = None
def get_latest_device_activity(self, device_id, activity_types):
"""Return latest activity that is one of the acitivty_types."""
- if device_id not in self._latest_activities_by_id_type:
+ if device_id not in self._latest_activities:
return None
- latest_device_activities = self._latest_activities_by_id_type[device_id]
+ latest_device_activities = self._latest_activities[device_id]
latest_activity = None
for activity_type in activity_types:
@@ -54,62 +89,86 @@ def get_latest_device_activity(self, device_id, activity_types):
async def _async_refresh(self, time):
"""Update the activity stream from August."""
-
# This is the only place we refresh the api token
await self._august_gateway.async_refresh_access_token_if_needed()
+ if self.pubnub.connected:
+ _LOGGER.debug("Skipping update because pubnub is connected")
+ return
await self._async_update_device_activities(time)
async def _async_update_device_activities(self, time):
_LOGGER.debug("Start retrieving device activities")
-
- limit = (
- ACTIVITY_STREAM_FETCH_LIMIT
- if self._last_update_time
- else ACTIVITY_CATCH_UP_FETCH_LIMIT
+ await asyncio.gather(
+ *[
+ self._update_debounce[house_id].async_call()
+ for house_id in self._house_ids
+ ]
)
+ self._last_update_time = time
- for house_id in self._house_ids:
- _LOGGER.debug("Updating device activity for house id %s", house_id)
- try:
- activities = await self._api.async_get_house_activities(
- self._august_gateway.access_token, house_id, limit=limit
- )
- except ClientError as ex:
- _LOGGER.error(
- "Request error trying to retrieve activity for house id %s: %s",
- house_id,
- ex,
- )
- # Make sure we process the next house if one of them fails
- continue
+ @callback
+ def async_schedule_house_id_refresh(self, house_id):
+ """Update for a house activities now and once in the future."""
+ if self._schedule_updates.get(house_id):
+ self._schedule_updates[house_id]()
+ self._schedule_updates[house_id] = None
+
+ async def _update_house_activities(_):
+ await self._update_debounce[house_id].async_call()
+
+ self._hass.async_create_task(self._update_debounce[house_id].async_call())
+ # Schedule an update past the debounce to ensure
+ # we catch the case where the lock operator is
+ # not updated or the lock failed
+ self._schedule_updates[house_id] = async_call_later(
+ self._hass, ACTIVITY_UPDATE_INTERVAL.seconds + 1, _update_house_activities
+ )
- _LOGGER.debug(
- "Completed retrieving device activities for house id %s", house_id
+ async def _async_update_house_id(self, house_id):
+ """Update device activities for a house."""
+ if self._last_update_time:
+ limit = ACTIVITY_STREAM_FETCH_LIMIT
+ else:
+ limit = ACTIVITY_CATCH_UP_FETCH_LIMIT
+
+ _LOGGER.debug("Updating device activity for house id %s", house_id)
+ try:
+ activities = await self._api.async_get_house_activities(
+ self._august_gateway.access_token, house_id, limit=limit
+ )
+ except ClientError as ex:
+ _LOGGER.error(
+ "Request error trying to retrieve activity for house id %s: %s",
+ house_id,
+ ex,
)
+ # Make sure we process the next house if one of them fails
+ return
- updated_device_ids = self._process_newer_device_activities(activities)
+ _LOGGER.debug(
+ "Completed retrieving device activities for house id %s", house_id
+ )
- if updated_device_ids:
- for device_id in updated_device_ids:
- _LOGGER.debug(
- "async_signal_device_id_update (from activity stream): %s",
- device_id,
- )
- self.async_signal_device_id_update(device_id)
+ updated_device_ids = self.async_process_newer_device_activities(activities)
- self._last_update_time = time
+ if not updated_device_ids:
+ return
- def _process_newer_device_activities(self, activities):
+ for device_id in updated_device_ids:
+ _LOGGER.debug(
+ "async_signal_device_id_update (from activity stream): %s",
+ device_id,
+ )
+ self.async_signal_device_id_update(device_id)
+
+ def async_process_newer_device_activities(self, activities):
+ """Process activities if they are newer than the last one."""
updated_device_ids = set()
for activity in activities:
device_id = activity.device_id
activity_type = activity.activity_type
-
- self._latest_activities_by_id_type.setdefault(device_id, {})
-
- lastest_activity = self._latest_activities_by_id_type[device_id].get(
- activity_type
- )
+ device_activities = self._latest_activities.setdefault(device_id, {})
+ lastest_activity = device_activities.get(activity_type)
# Ignore activities that are older than the latest one
if (
@@ -118,7 +177,7 @@ def _process_newer_device_activities(self, activities):
):
continue
- self._latest_activities_by_id_type[device_id][activity_type] = activity
+ device_activities[activity_type] = activity
updated_device_ids.add(device_id)
diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py
index 226cbf655f996d..6dccec57a09a86 100644
--- a/homeassistant/components/august/binary_sensor.py
+++ b/homeassistant/components/august/binary_sensor.py
@@ -2,9 +2,9 @@
from datetime import datetime, timedelta
import logging
-from august.activity import ActivityType
-from august.lock import LockDoorStatus
-from august.util import update_lock_detail_from_activity
+from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, ActivityType
+from yalexs.lock import LockDoorStatus
+from yalexs.util import update_lock_detail_from_activity
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_CONNECTIVITY,
@@ -14,15 +14,15 @@
BinarySensorEntity,
)
from homeassistant.core import callback
-from homeassistant.helpers.event import async_track_point_in_utc_time
-from homeassistant.util.dt import utcnow
+from homeassistant.helpers.event import async_call_later
-from .const import DATA_AUGUST, DOMAIN
+from .const import ACTIVITY_UPDATE_INTERVAL, DATA_AUGUST, DOMAIN
from .entity import AugustEntityMixin
_LOGGER = logging.getLogger(__name__)
-TIME_TO_DECLARE_DETECTION = timedelta(seconds=60)
+TIME_TO_DECLARE_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.seconds)
+TIME_TO_RECHECK_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.seconds * 3)
def _retrieve_online_state(data, detail):
@@ -35,30 +35,43 @@ def _retrieve_online_state(data, detail):
def _retrieve_motion_state(data, detail):
-
- return _activity_time_based_state(
- data,
- detail.device_id,
- [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING],
+ latest = data.activity_stream.get_latest_device_activity(
+ detail.device_id, {ActivityType.DOORBELL_MOTION}
)
+ if latest is None:
+ return False
+
+ return _activity_time_based_state(latest)
-def _retrieve_ding_state(data, detail):
- return _activity_time_based_state(
- data, detail.device_id, [ActivityType.DOORBELL_DING]
+def _retrieve_ding_state(data, detail):
+ latest = data.activity_stream.get_latest_device_activity(
+ detail.device_id, {ActivityType.DOORBELL_DING}
)
+ if latest is None:
+ return False
+
+ if (
+ data.activity_stream.pubnub.connected
+ and latest.action == ACTION_DOORBELL_CALL_MISSED
+ ):
+ return False
-def _activity_time_based_state(data, device_id, activity_types):
+ return _activity_time_based_state(latest)
+
+
+def _activity_time_based_state(latest):
"""Get the latest state of the sensor."""
- latest = data.activity_stream.get_latest_device_activity(device_id, activity_types)
+ start = latest.activity_start_time
+ end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
+ return start <= _native_datetime() <= end
- if latest is not None:
- start = latest.activity_start_time
- end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
- return start <= datetime.now() <= end
- return None
+
+def _native_datetime():
+ """Return time in the format august uses without timezone."""
+ return datetime.now()
SENSOR_NAME = 0
@@ -143,12 +156,19 @@ def name(self):
def _update_from_data(self):
"""Get the latest state of the sensor and update activity."""
door_activity = self._data.activity_stream.get_latest_device_activity(
- self._device_id, [ActivityType.DOOR_OPERATION]
+ self._device_id, {ActivityType.DOOR_OPERATION}
)
if door_activity is not None:
update_lock_detail_from_activity(self._detail, door_activity)
+ bridge_activity = self._data.activity_stream.get_latest_device_activity(
+ self._device_id, {ActivityType.BRIDGE_OPERATION}
+ )
+
+ if bridge_activity is not None:
+ update_lock_detail_from_activity(self._detail, bridge_activity)
+
@property
def unique_id(self) -> str:
"""Get the unique of the door open binary sensor."""
@@ -179,25 +199,30 @@ def is_on(self):
"""Return true if the binary sensor is on."""
return self._state
+ @property
+ def _sensor_config(self):
+ """Return the config for the sensor."""
+ return SENSOR_TYPES_DOORBELL[self._sensor_type]
+
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
- return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_DEVICE_CLASS]
+ return self._sensor_config[SENSOR_DEVICE_CLASS]
@property
def name(self):
"""Return the name of the binary sensor."""
- return f"{self._device.device_name} {SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME]}"
+ return f"{self._device.device_name} {self._sensor_config[SENSOR_NAME]}"
@property
def _state_provider(self):
"""Return the state provider for the binary sensor."""
- return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_STATE_PROVIDER]
+ return self._sensor_config[SENSOR_STATE_PROVIDER]
@property
def _is_time_based(self):
"""Return true of false if the sensor is time based."""
- return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_STATE_IS_TIME_BASED]
+ return self._sensor_config[SENSOR_STATE_IS_TIME_BASED]
@callback
def _update_from_data(self):
@@ -228,17 +253,20 @@ def _scheduled_update(now):
"""Timer callback for sensor update."""
self._check_for_off_update_listener = None
self._update_from_data()
+ if not self._state:
+ self.async_write_ha_state()
- self._check_for_off_update_listener = async_track_point_in_utc_time(
- self.hass, _scheduled_update, utcnow() + TIME_TO_DECLARE_DETECTION
+ self._check_for_off_update_listener = async_call_later(
+ self.hass, TIME_TO_RECHECK_DETECTION.seconds, _scheduled_update
)
def _cancel_any_pending_updates(self):
"""Cancel any updates to recheck a sensor to see if it is ready to turn off."""
- if self._check_for_off_update_listener:
- _LOGGER.debug("%s: canceled pending update", self.entity_id)
- self._check_for_off_update_listener()
- self._check_for_off_update_listener = None
+ if not self._check_for_off_update_listener:
+ return
+ _LOGGER.debug("%s: canceled pending update", self.entity_id)
+ self._check_for_off_update_listener()
+ self._check_for_off_update_listener = None
async def async_added_to_hass(self):
"""Call the mixin to subscribe and setup an async_track_point_in_utc_time to turn off the sensor if needed."""
@@ -248,7 +276,4 @@ async def async_added_to_hass(self):
@property
def unique_id(self) -> str:
"""Get the unique id of the doorbell sensor."""
- return (
- f"{self._device_id}_"
- f"{SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME].lower()}"
- )
+ return f"{self._device_id}_{self._sensor_config[SENSOR_NAME].lower()}"
diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py
index 4037489fa229a3..e002e0b25176dc 100644
--- a/homeassistant/components/august/camera.py
+++ b/homeassistant/components/august/camera.py
@@ -1,7 +1,7 @@
"""Support for August doorbell camera."""
-from august.activity import ActivityType
-from august.util import update_doorbell_image_from_activity
+from yalexs.activity import ActivityType
+from yalexs.util import update_doorbell_image_from_activity
from homeassistant.components.camera import Camera
from homeassistant.core import callback
@@ -63,7 +63,7 @@ def model(self):
def _update_from_data(self):
"""Get the latest state of the sensor."""
doorbell_activity = self._data.activity_stream.get_latest_device_activity(
- self._device_id, [ActivityType.DOORBELL_MOTION]
+ self._device_id, {ActivityType.DOORBELL_MOTION}
)
if doorbell_activity is not None:
diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py
index f595479c0cfd65..0138b438a1e2d9 100644
--- a/homeassistant/components/august/config_flow.py
+++ b/homeassistant/components/august/config_flow.py
@@ -1,19 +1,13 @@
"""Config flow for August integration."""
import logging
-from august.authenticator import ValidationResult
import voluptuous as vol
+from yalexs.authenticator import ValidationResult
from homeassistant import config_entries
-from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
-
-from .const import (
- CONF_LOGIN_METHOD,
- DEFAULT_TIMEOUT,
- LOGIN_METHODS,
- VERIFICATION_CODE_KEY,
-)
-from .const import DOMAIN # pylint:disable=unused-import
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+
+from .const import CONF_LOGIN_METHOD, DOMAIN, LOGIN_METHODS, VERIFICATION_CODE_KEY
from .exceptions import CannotConnect, InvalidAuth, RequireValidation
from .gateway import AugustGateway
@@ -68,61 +62,48 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self):
"""Store an AugustGateway()."""
self._august_gateway = None
- self.user_auth_details = {}
+ self._user_auth_details = {}
self._needs_reset = False
+ self._mode = None
super().__init__()
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
- if self._august_gateway is None:
- self._august_gateway = AugustGateway(self.hass)
+ self._august_gateway = AugustGateway(self.hass)
+ return await self.async_step_user_validate()
+
+ async def async_step_user_validate(self, user_input=None):
+ """Handle authentication."""
errors = {}
if user_input is not None:
- combined_inputs = {**self.user_auth_details, **user_input}
- await self._august_gateway.async_setup(combined_inputs)
- if self._needs_reset:
- self._needs_reset = False
- await self._august_gateway.async_reset_authentication()
-
- try:
- info = await async_validate_input(
- combined_inputs,
- self._august_gateway,
- )
- except CannotConnect:
- errors["base"] = "cannot_connect"
- except InvalidAuth:
- errors["base"] = "invalid_auth"
- except RequireValidation:
- self.user_auth_details.update(user_input)
-
- return await self.async_step_validation()
- except Exception: # pylint: disable=broad-except
- _LOGGER.exception("Unexpected exception")
- errors["base"] = "unknown"
-
- if not errors:
- self.user_auth_details.update(user_input)
-
- existing_entry = await self.async_set_unique_id(
- combined_inputs[CONF_USERNAME]
- )
- if existing_entry:
- self.hass.config_entries.async_update_entry(
- existing_entry, data=info["data"]
- )
- await self.hass.config_entries.async_reload(existing_entry.entry_id)
- return self.async_abort(reason="reauth_successful")
- return self.async_create_entry(title=info["title"], data=info["data"])
+ result = await self._async_auth_or_validate(user_input, errors)
+ if result is not None:
+ return result
return self.async_show_form(
- step_id="user", data_schema=self._async_build_schema(), errors=errors
+ step_id="user_validate",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ CONF_LOGIN_METHOD,
+ default=self._user_auth_details.get(CONF_LOGIN_METHOD, "phone"),
+ ): vol.In(LOGIN_METHODS),
+ vol.Required(
+ CONF_USERNAME,
+ default=self._user_auth_details.get(CONF_USERNAME),
+ ): str,
+ vol.Required(CONF_PASSWORD): str,
+ }
+ ),
+ errors=errors,
)
async def async_step_validation(self, user_input=None):
"""Handle validation (2fa) step."""
if user_input:
- return await self.async_step_user({**self.user_auth_details, **user_input})
+ if self._mode == "reauth":
+ return await self.async_step_reauth_validate(user_input)
+ return await self.async_step_user_validate(user_input)
return self.async_show_form(
step_id="validation",
@@ -130,34 +111,70 @@ async def async_step_validation(self, user_input=None):
{vol.Required(VERIFICATION_CODE_KEY): vol.All(str, vol.Strip)}
),
description_placeholders={
- CONF_USERNAME: self.user_auth_details.get(CONF_USERNAME),
- CONF_LOGIN_METHOD: self.user_auth_details.get(CONF_LOGIN_METHOD),
+ CONF_USERNAME: self._user_auth_details[CONF_USERNAME],
+ CONF_LOGIN_METHOD: self._user_auth_details[CONF_LOGIN_METHOD],
},
)
- async def async_step_import(self, user_input):
- """Handle import."""
- await self.async_set_unique_id(user_input[CONF_USERNAME])
- self._abort_if_unique_id_configured()
-
- return await self.async_step_user(user_input)
-
async def async_step_reauth(self, data):
"""Handle configuration by re-auth."""
- self.user_auth_details = dict(data)
+ self._user_auth_details = dict(data)
+ self._mode = "reauth"
self._needs_reset = True
- return await self.async_step_user()
-
- def _async_build_schema(self):
- """Generate the config flow schema."""
- base_schema = {
- vol.Required(CONF_LOGIN_METHOD, default="phone"): vol.In(LOGIN_METHODS),
- vol.Required(CONF_USERNAME): str,
- vol.Required(CONF_PASSWORD): str,
- vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
- }
- for key in self.user_auth_details:
- if key == CONF_PASSWORD or key not in base_schema:
- continue
- del base_schema[key]
- return vol.Schema(base_schema)
+ self._august_gateway = AugustGateway(self.hass)
+ return await self.async_step_reauth_validate()
+
+ async def async_step_reauth_validate(self, user_input=None):
+ """Handle reauth and validation."""
+ errors = {}
+ if user_input is not None:
+ result = await self._async_auth_or_validate(user_input, errors)
+ if result is not None:
+ return result
+
+ return self.async_show_form(
+ step_id="reauth_validate",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_PASSWORD): str,
+ }
+ ),
+ errors=errors,
+ description_placeholders={
+ CONF_USERNAME: self._user_auth_details[CONF_USERNAME],
+ },
+ )
+
+ async def _async_auth_or_validate(self, user_input, errors):
+ self._user_auth_details.update(user_input)
+ await self._august_gateway.async_setup(self._user_auth_details)
+ if self._needs_reset:
+ self._needs_reset = False
+ await self._august_gateway.async_reset_authentication()
+ try:
+ info = await async_validate_input(
+ self._user_auth_details,
+ self._august_gateway,
+ )
+ except CannotConnect:
+ errors["base"] = "cannot_connect"
+ except InvalidAuth:
+ errors["base"] = "invalid_auth"
+ except RequireValidation:
+ return await self.async_step_validation()
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+
+ if errors:
+ return None
+
+ existing_entry = await self.async_set_unique_id(
+ self._user_auth_details[CONF_USERNAME]
+ )
+ if not existing_entry:
+ return self.async_create_entry(title=info["title"], data=info["data"])
+
+ self.hass.config_entries.async_update_entry(existing_entry, data=info["data"])
+ await self.hass.config_entries.async_reload(existing_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
diff --git a/homeassistant/components/august/const.py b/homeassistant/components/august/const.py
index a32e187647a1cf..57e0d5a7fb7cb3 100644
--- a/homeassistant/components/august/const.py
+++ b/homeassistant/components/august/const.py
@@ -43,4 +43,4 @@
LOGIN_METHODS = ["phone", "email"]
-AUGUST_COMPONENTS = ["camera", "binary_sensor", "lock", "sensor"]
+PLATFORMS = ["camera", "binary_sensor", "lock", "sensor"]
diff --git a/homeassistant/components/august/entity.py b/homeassistant/components/august/entity.py
index b6c677a63b6f9a..b2a93948449003 100644
--- a/homeassistant/components/august/entity.py
+++ b/homeassistant/components/august/entity.py
@@ -5,6 +5,8 @@
from . import DOMAIN
from .const import MANUFACTURER
+DEVICE_TYPES = ["keypad", "lock", "camera", "doorbell", "door", "bell"]
+
class AugustEntityMixin(Entity):
"""Base implementation for August device."""
@@ -31,12 +33,14 @@ def _detail(self):
@property
def device_info(self):
"""Return the device_info of the device."""
+ name = self._device.device_name
return {
"identifiers": {(DOMAIN, self._device_id)},
- "name": self._device.device_name,
+ "name": name,
"manufacturer": MANUFACTURER,
"sw_version": self._detail.firmware_version,
"model": self._detail.model,
+ "suggested_area": _remove_device_types(name, DEVICE_TYPES),
}
@callback
@@ -56,3 +60,19 @@ async def async_added_to_hass(self):
self._device_id, self._update_from_data_and_write_state
)
)
+
+
+def _remove_device_types(name, device_types):
+ """Strip device types from a string.
+
+ August stores the name as Master Bed Lock
+ or Master Bed Door. We can come up with a
+ reasonable suggestion by removing the supported
+ device types from the string.
+ """
+ lower_name = name.lower()
+ for device_type in device_types:
+ device_type_with_space = f" {device_type}"
+ if lower_name.endswith(device_type_with_space):
+ lower_name = lower_name[: -len(device_type_with_space)]
+ return name[: len(lower_name)]
diff --git a/homeassistant/components/august/gateway.py b/homeassistant/components/august/gateway.py
index b72bb52e710925..5499246a1871ec 100644
--- a/homeassistant/components/august/gateway.py
+++ b/homeassistant/components/august/gateway.py
@@ -5,8 +5,8 @@
import os
from aiohttp import ClientError, ClientResponseError
-from august.api_async import ApiAsync
-from august.authenticator_async import AuthenticationState, AuthenticatorAsync
+from yalexs.api_async import ApiAsync
+from yalexs.authenticator_async import AuthenticationState, AuthenticatorAsync
from homeassistant.const import (
CONF_PASSWORD,
@@ -21,6 +21,7 @@
CONF_INSTALL_ID,
CONF_LOGIN_METHOD,
DEFAULT_AUGUST_CONFIG_FILE,
+ DEFAULT_TIMEOUT,
VERIFICATION_CODE_KEY,
)
from .exceptions import CannotConnect, InvalidAuth, RequireValidation
@@ -52,9 +53,7 @@ def config_entry(self):
return {
CONF_LOGIN_METHOD: self._config[CONF_LOGIN_METHOD],
CONF_USERNAME: self._config[CONF_USERNAME],
- CONF_PASSWORD: self._config[CONF_PASSWORD],
CONF_INSTALL_ID: self._config.get(CONF_INSTALL_ID),
- CONF_TIMEOUT: self._config.get(CONF_TIMEOUT),
CONF_ACCESS_TOKEN_CACHE_FILE: self._access_token_cache_file,
}
@@ -70,14 +69,15 @@ async def async_setup(self, conf):
self._config = conf
self.api = ApiAsync(
- self._aiohttp_session, timeout=self._config.get(CONF_TIMEOUT)
+ self._aiohttp_session,
+ timeout=self._config.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
)
self.authenticator = AuthenticatorAsync(
self.api,
self._config[CONF_LOGIN_METHOD],
self._config[CONF_USERNAME],
- self._config[CONF_PASSWORD],
+ self._config.get(CONF_PASSWORD, ""),
install_id=self._config.get(CONF_INSTALL_ID),
access_token_cache_file=self._hass.config.path(
self._access_token_cache_file
@@ -128,14 +128,15 @@ def _reset_authentication(self):
async def async_refresh_access_token_if_needed(self):
"""Refresh the august access token if needed."""
- if self.authenticator.should_refresh():
- async with self._token_refresh_lock:
- refreshed_authentication = (
- await self.authenticator.async_refresh_access_token(force=False)
- )
- _LOGGER.info(
- "Refreshed august access token. The old token expired at %s, and the new token expires at %s",
- self.authentication.access_token_expires,
- refreshed_authentication.access_token_expires,
- )
- self.authentication = refreshed_authentication
+ if not self.authenticator.should_refresh():
+ return
+ async with self._token_refresh_lock:
+ refreshed_authentication = (
+ await self.authenticator.async_refresh_access_token(force=False)
+ )
+ _LOGGER.info(
+ "Refreshed august access token. The old token expired at %s, and the new token expires at %s",
+ self.authentication.access_token_expires,
+ refreshed_authentication.access_token_expires,
+ )
+ self.authentication = refreshed_authentication
diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py
index e16c603d919ea7..59c97190d7fe1b 100644
--- a/homeassistant/components/august/lock.py
+++ b/homeassistant/components/august/lock.py
@@ -1,9 +1,9 @@
"""Support for August lock."""
import logging
-from august.activity import ActivityType
-from august.lock import LockStatus
-from august.util import update_lock_detail_from_activity
+from yalexs.activity import ActivityType
+from yalexs.lock import LockStatus
+from yalexs.util import update_lock_detail_from_activity
from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity
from homeassistant.const import ATTR_BATTERY_LEVEL
@@ -73,13 +73,21 @@ def _update_lock_status_from_detail(self):
def _update_from_data(self):
"""Get the latest state of the sensor and update activity."""
lock_activity = self._data.activity_stream.get_latest_device_activity(
- self._device_id, [ActivityType.LOCK_OPERATION]
+ self._device_id,
+ {ActivityType.LOCK_OPERATION, ActivityType.LOCK_OPERATION_WITHOUT_OPERATOR},
)
if lock_activity is not None:
self._changed_by = lock_activity.operated_by
update_lock_detail_from_activity(self._detail, lock_activity)
+ bridge_activity = self._data.activity_stream.get_latest_device_activity(
+ self._device_id, {ActivityType.BRIDGE_OPERATION}
+ )
+
+ if bridge_activity is not None:
+ update_lock_detail_from_activity(self._detail, bridge_activity)
+
self._update_lock_status_from_detail()
@property
@@ -105,7 +113,7 @@ def changed_by(self):
return self._changed_by
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
attributes = {ATTR_BATTERY_LEVEL: self._detail.battery_level}
diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json
index dcdfb0a0497a65..fb4ff1a3484dae 100644
--- a/homeassistant/components/august/manifest.json
+++ b/homeassistant/components/august/manifest.json
@@ -2,12 +2,12 @@
"domain": "august",
"name": "August",
"documentation": "https://www.home-assistant.io/integrations/august",
- "requirements": ["py-august==0.25.2"],
- "dependencies": ["configurator"],
+ "requirements": ["yalexs==1.1.10"],
"codeowners": ["@bdraco"],
"dhcp": [
{"hostname":"connect","macaddress":"D86162*"},
- {"hostname":"connect","macaddress":"B8B7F1*"}
+ {"hostname":"connect","macaddress":"B8B7F1*"},
+ {"hostname":"august*","macaddress":"E076D0*"}
],
"config_flow": true
}
diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py
index 6004a07f605ab3..44597a6485ead0 100644
--- a/homeassistant/components/august/sensor.py
+++ b/homeassistant/components/august/sensor.py
@@ -1,12 +1,11 @@
"""Support for August sensors."""
import logging
-from august.activity import ActivityType
+from yalexs.activity import ActivityType
-from homeassistant.components.sensor import DEVICE_CLASS_BATTERY
+from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, SensorEntity
from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE, STATE_UNAVAILABLE
from homeassistant.core import callback
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_registry import async_get_registry
from homeassistant.helpers.restore_state import RestoreEntity
@@ -118,7 +117,7 @@ async def _async_migrate_old_unique_ids(hass, devices):
registry.async_update_entity(old_entity_id, new_unique_id=device.unique_id)
-class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, Entity):
+class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity):
"""Representation of an August lock operation sensor."""
def __init__(self, data, device):
@@ -154,7 +153,7 @@ def name(self):
def _update_from_data(self):
"""Get the latest state of the sensor and update activity."""
lock_activity = self._data.activity_stream.get_latest_device_activity(
- self._device_id, [ActivityType.LOCK_OPERATION]
+ self._device_id, {ActivityType.LOCK_OPERATION}
)
self._available = True
@@ -166,7 +165,7 @@ def _update_from_data(self):
self._entity_picture = lock_activity.operator_thumbnail_url
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
attributes = {}
@@ -217,7 +216,7 @@ def unique_id(self) -> str:
return f"{self._device_id}_lock_operator"
-class AugustBatterySensor(AugustEntityMixin, Entity):
+class AugustBatterySensor(AugustEntityMixin, SensorEntity):
"""Representation of an August sensor."""
def __init__(self, data, sensor_type, device, old_device):
diff --git a/homeassistant/components/august/strings.json b/homeassistant/components/august/strings.json
index 998d870e629e03..7939fb1d25f3b3 100644
--- a/homeassistant/components/august/strings.json
+++ b/homeassistant/components/august/strings.json
@@ -17,15 +17,21 @@
},
"description": "Please check your {login_method} ({username}) and enter the verification code below"
},
- "user": {
+ "user_validate": {
"description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.",
"data": {
- "timeout": "Timeout (seconds)",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]",
"login_method": "Login Method"
},
"title": "Setup an August account"
+ },
+ "reauth_validate": {
+ "description": "Enter the password for {username}.",
+ "data": {
+ "password": "[%key:common::config_flow::data::password%]"
+ },
+ "title": "Reauthenticate an August account"
}
}
}
diff --git a/homeassistant/components/august/translations/ca.json b/homeassistant/components/august/translations/ca.json
index 9d3e3535e46c6d..8faa12e27573f3 100644
--- a/homeassistant/components/august/translations/ca.json
+++ b/homeassistant/components/august/translations/ca.json
@@ -10,6 +10,13 @@
"unknown": "Error inesperat"
},
"step": {
+ "reauth_validate": {
+ "data": {
+ "password": "Contrasenya"
+ },
+ "description": "Introdueix la contrasenya per a {username}.",
+ "title": "Torna a autenticar compte d'August"
+ },
"user": {
"data": {
"login_method": "M\u00e8tode d'inici de sessi\u00f3",
@@ -20,12 +27,21 @@
"description": "Si el m\u00e8tode d'inici de sessi\u00f3 \u00e9s 'email', el nom d'usuari \u00e9s l'adre\u00e7a de correu electr\u00f2nic. Si el m\u00e8tode d'inici de sessi\u00f3 \u00e9s 'phone', el nom d'usuari \u00e9s el n\u00famero de tel\u00e8fon en el format \"+NNNNNNNNN\".",
"title": "Configuraci\u00f3 de compte August"
},
+ "user_validate": {
+ "data": {
+ "login_method": "M\u00e8tode d'inici de sessi\u00f3",
+ "password": "Contrasenya",
+ "username": "Nom d'usuari"
+ },
+ "description": "Si el m\u00e8tode d'inici de sessi\u00f3 \u00e9s 'email', el nom d'usuari \u00e9s l'adre\u00e7a de correu electr\u00f2nic. Si el m\u00e8tode d'inici de sessi\u00f3 \u00e9s 'phone', el nom d'usuari \u00e9s el n\u00famero de tel\u00e8fon en format \"+NNNNNNNNN\".",
+ "title": "Configuraci\u00f3 de compte August"
+ },
"validation": {
"data": {
"code": "Codi de verificaci\u00f3"
},
"description": "Comprova el teu {login_method} ({username}) i introdueix el codi de verificaci\u00f3 a continuaci\u00f3",
- "title": "Autenticaci\u00f3 de dos factors"
+ "title": "Verificaci\u00f3 en dos passos"
}
}
}
diff --git a/homeassistant/components/august/translations/cs.json b/homeassistant/components/august/translations/cs.json
index 4100014abb6cb8..4176da8f1bf7eb 100644
--- a/homeassistant/components/august/translations/cs.json
+++ b/homeassistant/components/august/translations/cs.json
@@ -10,6 +10,11 @@
"unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
},
"step": {
+ "reauth_validate": {
+ "data": {
+ "password": "Heslo"
+ }
+ },
"user": {
"data": {
"login_method": "Zp\u016fsob p\u0159ihl\u00e1\u0161en\u00ed",
@@ -20,6 +25,12 @@
"description": "Pokud je metoda p\u0159ihl\u00e1\u0161en\u00ed \"e-mail\", je e-mailovou adresou u\u017eivatelsk\u00e9 jm\u00e9no. Pokud je p\u0159ihla\u0161ovac\u00ed metoda \"telefon\", u\u017eivatelsk\u00e9 jm\u00e9no je telefonn\u00ed \u010d\u00edslo ve form\u00e1tu \"+NNNNNNNNN\".",
"title": "Nastavte \u00fa\u010det August"
},
+ "user_validate": {
+ "data": {
+ "password": "Heslo",
+ "username": "U\u017eivatelsk\u00e9 jm\u00e9no"
+ }
+ },
"validation": {
"data": {
"code": "Ov\u011b\u0159ovac\u00ed k\u00f3d"
diff --git a/homeassistant/components/august/translations/de.json b/homeassistant/components/august/translations/de.json
index d46be650e2c5f1..ef525fb665df3a 100644
--- a/homeassistant/components/august/translations/de.json
+++ b/homeassistant/components/august/translations/de.json
@@ -1,14 +1,22 @@
{
"config": {
"abort": {
- "already_configured": "Konto ist bereits konfiguriert"
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"step": {
+ "reauth_validate": {
+ "data": {
+ "password": "Passwort"
+ },
+ "description": "Gib das Passwort f\u00fcr {username} ein.",
+ "title": "August-Konto erneut authentifizieren"
+ },
"user": {
"data": {
"login_method": "Anmeldemethode",
@@ -19,6 +27,15 @@
"description": "Wenn die Anmeldemethode \"E-Mail\" lautet, ist Benutzername die E-Mail-Adresse. Wenn die Anmeldemethode \"Telefon\" ist, ist Benutzername die Telefonnummer im Format \"+ NNNNNNNNN\".",
"title": "Richten Sie ein August-Konto ein"
},
+ "user_validate": {
+ "data": {
+ "login_method": "Anmeldemethode",
+ "password": "Passwort",
+ "username": "Benutzername"
+ },
+ "description": "Wenn die Anmeldemethode \"E-Mail\" lautet, ist Benutzername die E-Mail-Adresse. Wenn die Anmeldemethode \"Telefon\" ist, ist Benutzername die Telefonnummer im Format \"+ NNNNNNNNN\".",
+ "title": "Richte ein August-Konto ein"
+ },
"validation": {
"data": {
"code": "Verifizierungs-Code"
diff --git a/homeassistant/components/august/translations/el.json b/homeassistant/components/august/translations/el.json
new file mode 100644
index 00000000000000..8a976d451be060
--- /dev/null
+++ b/homeassistant/components/august/translations/el.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "step": {
+ "reauth_validate": {
+ "data": {
+ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2"
+ },
+ "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {username} .",
+ "title": "\u0395\u03c0\u03b9\u03ba\u03c5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03ad\u03bd\u03b1\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc August"
+ },
+ "user_validate": {
+ "data": {
+ "login_method": "\u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 \u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2",
+ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2",
+ "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7"
+ },
+ "description": "\u0395\u03ac\u03bd \u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \"\u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf\", \u03c4\u03bf \u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf\u03c5. \u0395\u03ac\u03bd \u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \"\u03c4\u03b7\u03bb\u03ad\u03c6\u03c9\u03bd\u03bf\", \u03c4\u03bf \u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bf \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03b7\u03bb\u03b5\u03c6\u03ce\u03bd\u03bf\u03c5 \u03bc\u03b5 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae '+NNNNNNNNNN'.",
+ "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc August"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/august/translations/en.json b/homeassistant/components/august/translations/en.json
index c6c19321d8a5a0..0b8d1511244f3a 100644
--- a/homeassistant/components/august/translations/en.json
+++ b/homeassistant/components/august/translations/en.json
@@ -10,6 +10,13 @@
"unknown": "Unexpected error"
},
"step": {
+ "reauth_validate": {
+ "data": {
+ "password": "Password"
+ },
+ "description": "Enter the password for {username}.",
+ "title": "Reauthenticate an August account"
+ },
"user": {
"data": {
"login_method": "Login Method",
@@ -20,6 +27,15 @@
"description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.",
"title": "Setup an August account"
},
+ "user_validate": {
+ "data": {
+ "login_method": "Login Method",
+ "password": "Password",
+ "username": "Username"
+ },
+ "description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.",
+ "title": "Setup an August account"
+ },
"validation": {
"data": {
"code": "Verification code"
diff --git a/homeassistant/components/august/translations/es.json b/homeassistant/components/august/translations/es.json
index a8f7bc6af23a6f..bb343e6da97d8a 100644
--- a/homeassistant/components/august/translations/es.json
+++ b/homeassistant/components/august/translations/es.json
@@ -10,6 +10,10 @@
"unknown": "Error inesperado"
},
"step": {
+ "reauth_validate": {
+ "description": "Introduzca la contrase\u00f1a de {username}.",
+ "title": "Reautorizar una cuenta de August"
+ },
"user": {
"data": {
"login_method": "M\u00e9todo de inicio de sesi\u00f3n",
@@ -20,6 +24,13 @@
"description": "Si el M\u00e9todo de Inicio de Sesi\u00f3n es 'correo electr\u00f3nico', Usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el M\u00e9todo de Inicio de Sesi\u00f3n es 'tel\u00e9fono', Usuario es el n\u00famero de tel\u00e9fono en formato '+NNNNNNNNN'.",
"title": "Configurar una cuenta de August"
},
+ "user_validate": {
+ "data": {
+ "login_method": "M\u00e9todo de inicio de sesi\u00f3n"
+ },
+ "description": "Si el m\u00e9todo de inicio de sesi\u00f3n es \"correo electr\u00f3nico\", el nombre de usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el m\u00e9todo de inicio de sesi\u00f3n es \"tel\u00e9fono\", el nombre de usuario es el n\u00famero de tel\u00e9fono en el formato \"+NNNNNNN\".",
+ "title": "Configurar una cuenta de August"
+ },
"validation": {
"data": {
"code": "C\u00f3digo de verificaci\u00f3n"
diff --git a/homeassistant/components/august/translations/et.json b/homeassistant/components/august/translations/et.json
index 0b455b06d00552..69cd9e66ce380b 100644
--- a/homeassistant/components/august/translations/et.json
+++ b/homeassistant/components/august/translations/et.json
@@ -10,6 +10,13 @@
"unknown": "Tundmatu viga"
},
"step": {
+ "reauth_validate": {
+ "data": {
+ "password": "Salas\u00f5na"
+ },
+ "description": "Sisesta kasutaja {username} salas\u00f5na.",
+ "title": "Autendi Augusti konto uuesti"
+ },
"user": {
"data": {
"login_method": "Sisselogimismeetod",
@@ -20,6 +27,15 @@
"description": "Kui sisselogimismeetod on \"e-post\" on kasutajanimi e-posti aadress. Kui sisselogimismeetod on \"telefon\" on kasutajanimi telefoninumber vormingus \"+NNNNNNNNN\".",
"title": "Seadista Augusti sidumise konto"
},
+ "user_validate": {
+ "data": {
+ "login_method": "Sisselogimismeetod",
+ "password": "Salas\u00f5na",
+ "username": "Kasutajanimi"
+ },
+ "description": "Kui sisselogimismeetod on \"e-post\" on kasutajanimi e-posti aadress. Kui sisselogimismeetod on \"telefon\" on kasutajanimi telefoninumber vormingus \"+NNNNNNNNN\".",
+ "title": "Seadista Augusti sidumise konto"
+ },
"validation": {
"data": {
"code": "Kinnituskood"
diff --git a/homeassistant/components/august/translations/fr.json b/homeassistant/components/august/translations/fr.json
index 82568b681fdf48..967fb249d971cf 100644
--- a/homeassistant/components/august/translations/fr.json
+++ b/homeassistant/components/august/translations/fr.json
@@ -10,6 +10,13 @@
"unknown": "Erreur inattendue"
},
"step": {
+ "reauth_validate": {
+ "data": {
+ "password": "Mot de passe"
+ },
+ "description": "Saisissez le mot de passe de {username} .",
+ "title": "R\u00e9authentifier un compte August"
+ },
"user": {
"data": {
"login_method": "M\u00e9thode de connexion",
@@ -20,6 +27,15 @@
"description": "Si la m\u00e9thode de connexion est \u00abe-mail\u00bb, le nom d'utilisateur est l'adresse e-mail. Si la m\u00e9thode de connexion est \u00abt\u00e9l\u00e9phone\u00bb, le nom d'utilisateur est le num\u00e9ro de t\u00e9l\u00e9phone au format \u00ab+ NNNNNNNNN\u00bb.",
"title": "Configurer un compte August"
},
+ "user_validate": {
+ "data": {
+ "login_method": "M\u00e9thode de connexion",
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur"
+ },
+ "description": "Si la m\u00e9thode de connexion est \u00abemail\u00bb, le nom d'utilisateur est l'adresse e-mail. Si la m\u00e9thode de connexion est \u00abt\u00e9l\u00e9phone\u00bb, le nom d'utilisateur est le num\u00e9ro de t\u00e9l\u00e9phone au format \u00ab+ NNNNNNNNN\u00bb.",
+ "title": "Cr\u00e9er un compte August"
+ },
"validation": {
"data": {
"code": "Code de v\u00e9rification"
diff --git a/homeassistant/components/august/translations/he.json b/homeassistant/components/august/translations/he.json
new file mode 100644
index 00000000000000..ac90b3264eab33
--- /dev/null
+++ b/homeassistant/components/august/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/august/translations/hu.json b/homeassistant/components/august/translations/hu.json
index dee4ed9ee0fa4d..1bced4e1036cd1 100644
--- a/homeassistant/components/august/translations/hu.json
+++ b/homeassistant/components/august/translations/hu.json
@@ -1,11 +1,42 @@
{
"config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k 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"
+ },
"step": {
+ "reauth_validate": {
+ "data": {
+ "password": "Jelsz\u00f3"
+ },
+ "description": "Add meg a(z) {username} jelszav\u00e1t.",
+ "title": "August fi\u00f3k \u00fajrahiteles\u00edt\u00e9se"
+ },
"user": {
"data": {
"password": "Jelsz\u00f3",
+ "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s (m\u00e1sodperc)",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
}
+ },
+ "user_validate": {
+ "data": {
+ "login_method": "Bejelentkez\u00e9si m\u00f3d",
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "title": "August fi\u00f3k be\u00e1ll\u00edt\u00e1sa"
+ },
+ "validation": {
+ "data": {
+ "code": "Ellen\u0151rz\u0151 k\u00f3d"
+ },
+ "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s"
}
}
}
diff --git a/homeassistant/components/august/translations/id.json b/homeassistant/components/august/translations/id.json
new file mode 100644
index 00000000000000..a66c43ce05755f
--- /dev/null
+++ b/homeassistant/components/august/translations/id.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi",
+ "reauth_successful": "Autentikasi ulang berhasil"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "login_method": "Metode Masuk",
+ "password": "Kata Sandi",
+ "timeout": "Tenggang waktu (detik)",
+ "username": "Nama Pengguna"
+ },
+ "description": "Jika Metode Masuk adalah 'email', Nama Pengguna adalah alamat email. Jika Metode Masuk adalah 'telepon', Nama Pengguna adalah nomor telepon dalam format '+NNNNNNNNN'.",
+ "title": "Siapkan akun August"
+ },
+ "validation": {
+ "data": {
+ "code": "Kode verifikasi"
+ },
+ "description": "Periksa {login_method} ({username}) Anda dan masukkan kode verifikasi di bawah ini",
+ "title": "Autentikasi dua faktor"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/august/translations/it.json b/homeassistant/components/august/translations/it.json
index 08332c29d7ecdf..c20f95b90adc85 100644
--- a/homeassistant/components/august/translations/it.json
+++ b/homeassistant/components/august/translations/it.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "L'account \u00e8 gi\u00e0 configurato",
- "reauth_successful": "La riautenticazione ha avuto successo"
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
},
"error": {
"cannot_connect": "Impossibile connettersi",
@@ -10,6 +10,13 @@
"unknown": "Errore imprevisto"
},
"step": {
+ "reauth_validate": {
+ "data": {
+ "password": "Password"
+ },
+ "description": "Inserisci la password per {username}.",
+ "title": "Riautentica un account di August"
+ },
"user": {
"data": {
"login_method": "Metodo di accesso",
@@ -20,6 +27,15 @@
"description": "Se il metodo di accesso \u00e8 \"e-mail\", il nome utente \u00e8 l'indirizzo e-mail. Se il metodo di accesso \u00e8 \"telefono\", il nome utente \u00e8 il numero di telefono nel formato \"+NNNNNNNNN\".",
"title": "Configura un account di August"
},
+ "user_validate": {
+ "data": {
+ "login_method": "Metodo di accesso",
+ "password": "Password",
+ "username": "Nome utente"
+ },
+ "description": "Se il metodo di accesso \u00e8 'email', il nome utente \u00e8 l'indirizzo email. Se il metodo di accesso \u00e8 'phone', il nome utente \u00e8 il numero di telefono nel formato '+NNNNNNNNNN'.",
+ "title": "Configura un account di August"
+ },
"validation": {
"data": {
"code": "Codice di verifica"
diff --git a/homeassistant/components/august/translations/ko.json b/homeassistant/components/august/translations/ko.json
index 52f939c45a0a76..f3bc64a706c67e 100644
--- a/homeassistant/components/august/translations/ko.json
+++ b/homeassistant/components/august/translations/ko.json
@@ -1,14 +1,22 @@
{
"config": {
"abort": {
- "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
+ "reauth_validate": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638"
+ },
+ "description": "{username}\uc758 \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "August \uacc4\uc815 \uc7ac\uc778\uc99d\ud558\uae30"
+ },
"user": {
"data": {
"login_method": "\ub85c\uadf8\uc778 \ubc29\ubc95",
@@ -19,6 +27,15 @@
"description": "\ub85c\uadf8\uc778 \ubc29\ubc95\uc774 '\uc774\uba54\uc77c'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 \uc774\uba54\uc77c \uc8fc\uc18c\uc785\ub2c8\ub2e4. \ub85c\uadf8\uc778 \ubc29\ubc95\uc774 '\uc804\ud654\ubc88\ud638'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 '+NNNNNNNNN' \ud615\uc2dd\uc758 \uc804\ud654\ubc88\ud638\uc785\ub2c8\ub2e4.",
"title": "August \uacc4\uc815 \uc124\uc815\ud558\uae30"
},
+ "user_validate": {
+ "data": {
+ "login_method": "\ub85c\uadf8\uc778 \ubc29\ubc95",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ },
+ "description": "\ub85c\uadf8\uc778 \ubc29\ubc95\uc774 '\uc774\uba54\uc77c'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 \uc774\uba54\uc77c \uc8fc\uc18c\uc785\ub2c8\ub2e4. \ub85c\uadf8\uc778 \ubc29\ubc95\uc774 '\uc804\ud654\ubc88\ud638'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 '+NNNNNNNNN' \ud615\uc2dd\uc758 \uc804\ud654\ubc88\ud638\uc785\ub2c8\ub2e4.",
+ "title": "August \uacc4\uc815 \uc124\uc815\ud558\uae30"
+ },
"validation": {
"data": {
"code": "\uc778\uc99d \ucf54\ub4dc"
diff --git a/homeassistant/components/august/translations/nl.json b/homeassistant/components/august/translations/nl.json
index 1697f634d9a82e..05a5a4c52651d4 100644
--- a/homeassistant/components/august/translations/nl.json
+++ b/homeassistant/components/august/translations/nl.json
@@ -1,14 +1,22 @@
{
"config": {
"abort": {
- "already_configured": "Account al geconfigureerd"
+ "already_configured": "Account is al geconfigureerd",
+ "reauth_successful": "Herauthenticatie was succesvol"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
"step": {
+ "reauth_validate": {
+ "data": {
+ "password": "Wachtwoord"
+ },
+ "description": "Voer het wachtwoord in voor {username} .",
+ "title": "Verifieer een August-account opnieuw"
+ },
"user": {
"data": {
"login_method": "Aanmeldmethode",
@@ -19,6 +27,15 @@
"description": "Als de aanmeldingsmethode 'e-mail' is, is gebruikersnaam het e-mailadres. Als de aanmeldingsmethode 'telefoon' is, is gebruikersnaam het telefoonnummer in de indeling '+ NNNNNNNNN'.",
"title": "Stel een augustus-account in"
},
+ "user_validate": {
+ "data": {
+ "login_method": "Inlogmethode",
+ "password": "Wachtwoord",
+ "username": "Gebruikersnaam"
+ },
+ "description": "Als de aanmeldingsmethode 'e-mail' is, is de gebruikersnaam het e-mailadres. Als de aanmeldingsmethode 'telefoon' is, is de gebruikersnaam het telefoonnummer in de indeling '+ NNNNNNNNN'.",
+ "title": "Stel een August account in"
+ },
"validation": {
"data": {
"code": "Verificatiecode"
diff --git a/homeassistant/components/august/translations/no.json b/homeassistant/components/august/translations/no.json
index ae314897e74d3f..d90e7f8080a1a7 100644
--- a/homeassistant/components/august/translations/no.json
+++ b/homeassistant/components/august/translations/no.json
@@ -10,6 +10,13 @@
"unknown": "Uventet feil"
},
"step": {
+ "reauth_validate": {
+ "data": {
+ "password": "Passord"
+ },
+ "description": "Skriv inn passordet for {username} .",
+ "title": "Godkjenn en August-konto p\u00e5 nytt"
+ },
"user": {
"data": {
"login_method": "P\u00e5loggingsmetode",
@@ -20,6 +27,15 @@
"description": "Hvis p\u00e5loggingsmetoden er 'e-post', er brukernavnet e-postadressen. Hvis p\u00e5loggingsmetoden er 'telefon', er brukernavn telefonnummeret i formatet '+ NNNNNNNNN'.",
"title": "Sett opp en August konto"
},
+ "user_validate": {
+ "data": {
+ "login_method": "P\u00e5loggingsmetode",
+ "password": "Passord",
+ "username": "Brukernavn"
+ },
+ "description": "Hvis p\u00e5loggingsmetoden er 'e-post', er brukernavnet e-postadressen. Hvis p\u00e5loggingsmetoden er 'telefon', er brukernavn telefonnummeret i formatet '+ NNNNNNNNN'.",
+ "title": "Sett opp en August konto"
+ },
"validation": {
"data": {
"code": "Bekreftelseskode"
diff --git a/homeassistant/components/august/translations/pl.json b/homeassistant/components/august/translations/pl.json
index e76b663c3078de..a5539bea93ab5e 100644
--- a/homeassistant/components/august/translations/pl.json
+++ b/homeassistant/components/august/translations/pl.json
@@ -10,6 +10,13 @@
"unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
+ "reauth_validate": {
+ "data": {
+ "password": "Has\u0142o"
+ },
+ "description": "Wprowad\u017a has\u0142o dla {username}",
+ "title": "Ponownie uwierzytelnij konto August"
+ },
"user": {
"data": {
"login_method": "Metoda logowania",
@@ -20,6 +27,15 @@
"description": "Je\u015bli metod\u0105 logowania jest 'e-mail', nazw\u0105 u\u017cytkownika b\u0119dzie adres e-mail. Je\u015bli metod\u0105 logowania jest 'telefon', nazw\u0105 u\u017cytkownika b\u0119dzie numer telefonu w formacie '+NNNNNNNNN'.",
"title": "Konfiguracja konta August"
},
+ "user_validate": {
+ "data": {
+ "login_method": "Metoda logowania",
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ },
+ "description": "Je\u015bli metod\u0105 logowania jest 'e-mail', nazw\u0105 u\u017cytkownika b\u0119dzie adres e-mail. Je\u015bli metod\u0105 logowania jest 'telefon', nazw\u0105 u\u017cytkownika b\u0119dzie numer telefonu w formacie '+NNNNNNNNN'.",
+ "title": "Konfiguracja konta August"
+ },
"validation": {
"data": {
"code": "Kod weryfikacyjny"
diff --git a/homeassistant/components/august/translations/ru.json b/homeassistant/components/august/translations/ru.json
index 9ea0b531bf8b0c..0263ef6ee18a57 100644
--- a/homeassistant/components/august/translations/ru.json
+++ b/homeassistant/components/august/translations/ru.json
@@ -6,16 +6,32 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
+ "reauth_validate": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {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 \u043f\u0440\u043e\u0444\u0438\u043b\u044f"
+ },
"user": {
"data": {
"login_method": "\u0421\u043f\u043e\u0441\u043e\u0431 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ },
+ "description": "\u0415\u0441\u043b\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'email', \u0442\u043e \u043b\u043e\u0433\u0438\u043d\u043e\u043c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b. \u0415\u0441\u043b\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'phone', \u0442\u043e \u043b\u043e\u0433\u0438\u043d\u043e\u043c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 '+NNNNNNNNN'.",
+ "title": "August"
+ },
+ "user_validate": {
+ "data": {
+ "login_method": "\u0421\u043f\u043e\u0441\u043e\u0431 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u0415\u0441\u043b\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'email', \u0442\u043e \u043b\u043e\u0433\u0438\u043d\u043e\u043c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b. \u0415\u0441\u043b\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'phone', \u0442\u043e \u043b\u043e\u0433\u0438\u043d\u043e\u043c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 '+NNNNNNNNN'.",
"title": "August"
diff --git a/homeassistant/components/august/translations/sv.json b/homeassistant/components/august/translations/sv.json
index df72f5daaf33fe..a3a0b891bc6391 100644
--- a/homeassistant/components/august/translations/sv.json
+++ b/homeassistant/components/august/translations/sv.json
@@ -13,10 +13,16 @@
"data": {
"login_method": "Inloggningsmetod",
"password": "L\u00f6senord",
+ "timeout": "Timeout (sekunder)",
"username": "Anv\u00e4ndarnamn"
- }
+ },
+ "description": "Om inloggningsmetoden \u00e4r \"e-post\" \u00e4r anv\u00e4ndarnamnet e-postadressen. Om inloggningsmetoden \u00e4r \"telefon\" \u00e4r anv\u00e4ndarnamnet telefonnumret i formatet \"+ NNNNNNNN\".",
+ "title": "St\u00e4ll in ett August-konto"
},
"validation": {
+ "data": {
+ "code": "Verifieringskod"
+ },
"title": "Tv\u00e5faktorsautentisering"
}
}
diff --git a/homeassistant/components/august/translations/tr.json b/homeassistant/components/august/translations/tr.json
new file mode 100644
index 00000000000000..ccb9e200c820cf
--- /dev/null
+++ b/homeassistant/components/august/translations/tr.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "login_method": "Giri\u015f Y\u00f6ntemi",
+ "password": "Parola",
+ "timeout": "Zaman a\u015f\u0131m\u0131 (saniye)",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ },
+ "description": "Giri\u015f Y\u00f6ntemi 'e-posta' ise, Kullan\u0131c\u0131 Ad\u0131 e-posta adresidir. Giri\u015f Y\u00f6ntemi 'telefon' ise, Kullan\u0131c\u0131 Ad\u0131 '+ NNNNNNNNN' bi\u00e7imindeki telefon numaras\u0131d\u0131r."
+ },
+ "validation": {
+ "data": {
+ "code": "Do\u011frulama kodu"
+ },
+ "description": "L\u00fctfen {login_method} ( {username} ) bilgilerinizi kontrol edin ve a\u015fa\u011f\u0131ya do\u011frulama kodunu girin",
+ "title": "\u0130ki fakt\u00f6rl\u00fc kimlik do\u011frulama"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/august/translations/uk.json b/homeassistant/components/august/translations/uk.json
new file mode 100644
index 00000000000000..e06c5347d73364
--- /dev/null
+++ b/homeassistant/components/august/translations/uk.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "login_method": "\u0421\u043f\u043e\u0441\u0456\u0431 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u042f\u043a\u0449\u043e \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457 \u0432\u0438\u0431\u0440\u0430\u043d\u043e 'email', \u0442\u043e \u043b\u043e\u0433\u0456\u043d\u043e\u043c \u0454 \u0430\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438. \u042f\u043a\u0449\u043e \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457 \u0432\u0438\u0431\u0440\u0430\u043d\u043e 'phone', \u0442\u043e \u043b\u043e\u0433\u0456\u043d\u043e\u043c \u0454 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0443 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 '+ NNNNNNNNN'.",
+ "title": "August"
+ },
+ "validation": {
+ "data": {
+ "code": "\u041a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f"
+ },
+ "description": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 {login_method} ({username}) \u0456 \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 \u043a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f.",
+ "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/august/translations/zh-Hant.json b/homeassistant/components/august/translations/zh-Hant.json
index 667d881465962f..ab157e3da3c08a 100644
--- a/homeassistant/components/august/translations/zh-Hant.json
+++ b/homeassistant/components/august/translations/zh-Hant.json
@@ -10,6 +10,13 @@
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"step": {
+ "reauth_validate": {
+ "data": {
+ "password": "\u5bc6\u78bc"
+ },
+ "description": "\u8f38\u5165{username} \u5bc6\u78bc",
+ "title": "\u91cd\u65b0\u8a8d\u8b49 August \u5e33\u865f"
+ },
"user": {
"data": {
"login_method": "\u767b\u5165\u65b9\u5f0f",
@@ -20,12 +27,21 @@
"description": "\u5047\u5982\u767b\u5165\u65b9\u5f0f\u70ba\u90f5\u4ef6\u300cemail\u300d\u3001\u4f7f\u7528\u8005\u540d\u7a31\u70ba\u96fb\u5b50\u90f5\u4ef6\u4f4d\u5740\u3002\u5047\u5982\u767b\u5165\u65b9\u5f0f\u70ba\u96fb\u8a71\u300cphone\u300d\u3001\u5247\u4f7f\u7528\u8005\u540d\u7a31\u70ba\u5305\u542b\u570b\u78bc\u4e4b\u96fb\u8a71\u865f\u78bc\uff0c\u5982\u300c+NNNNNNNNN\u300d\u3002",
"title": "\u8a2d\u5b9a August \u5e33\u865f"
},
+ "user_validate": {
+ "data": {
+ "login_method": "\u767b\u5165\u65b9\u5f0f",
+ "password": "\u5bc6\u78bc",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ },
+ "description": "\u5047\u5982\u767b\u5165\u65b9\u5f0f\u70ba\u90f5\u4ef6\u300cemail\u300d\u3001\u4f7f\u7528\u8005\u540d\u7a31\u70ba\u96fb\u5b50\u90f5\u4ef6\u4f4d\u5740\u3002\u5047\u5982\u767b\u5165\u65b9\u5f0f\u70ba\u96fb\u8a71\u300cphone\u300d\u3001\u5247\u4f7f\u7528\u8005\u540d\u7a31\u70ba\u5305\u542b\u570b\u78bc\u4e4b\u96fb\u8a71\u865f\u78bc\uff0c\u5982\u300c+NNNNNNNNN\u300d\u3002",
+ "title": "\u8a2d\u5b9a August \u5e33\u865f"
+ },
"validation": {
"data": {
"code": "\u9a57\u8b49\u78bc"
},
"description": "\u8acb\u78ba\u8a8d {login_method} ({username}) \u4e26\u65bc\u4e0b\u65b9\u8f38\u5165\u9a57\u8b49\u78bc",
- "title": "\u5169\u6b65\u9a5f\u9a57\u8b49"
+ "title": "\u96d9\u91cd\u8a8d\u8b49"
}
}
}
diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py
index 260a3bd735d463..8823cf1c8ec3e3 100644
--- a/homeassistant/components/aurora/__init__.py
+++ b/homeassistant/components/aurora/__init__.py
@@ -4,16 +4,25 @@
from datetime import timedelta
import logging
+from aiohttp import ClientError
from auroranoaa import AuroraForecast
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+from homeassistant.const import ATTR_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+ UpdateFailed,
+)
from .const import (
+ ATTR_ENTRY_TYPE,
+ ATTR_IDENTIFIERS,
+ ATTR_MANUFACTURER,
+ ATTR_MODEL,
+ ATTRIBUTION,
AURORA_API,
CONF_THRESHOLD,
COORDINATOR,
@@ -24,14 +33,7 @@
_LOGGER = logging.getLogger(__name__)
-PLATFORMS = ["binary_sensor"]
-
-
-async def async_setup(hass: HomeAssistant, config: dict):
- """Set up the Aurora component."""
- hass.data.setdefault(DOMAIN, {})
-
- return True
+PLATFORMS = ["binary_sensor", "sensor"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
@@ -59,19 +61,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
threshold=threshold,
)
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
+ hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
COORDINATOR: coordinator,
AURORA_API: api,
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -82,8 +82,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -126,5 +126,54 @@ async def _async_update_data(self):
try:
return await self.api.get_forecast_data(self.longitude, self.latitude)
- except ConnectionError as error:
+ except ClientError as error:
raise UpdateFailed(f"Error updating from NOAA: {error}") from error
+
+
+class AuroraEntity(CoordinatorEntity):
+ """Implementation of the base Aurora Entity."""
+
+ def __init__(
+ self,
+ coordinator: AuroraDataUpdateCoordinator,
+ name: str,
+ icon: str,
+ ):
+ """Initialize the Aurora Entity."""
+
+ super().__init__(coordinator=coordinator)
+
+ self._name = name
+ self._unique_id = f"{self.coordinator.latitude}_{self.coordinator.longitude}"
+ self._icon = icon
+
+ @property
+ def unique_id(self):
+ """Define the unique id based on the latitude and longitude."""
+ return self._unique_id
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def extra_state_attributes(self):
+ """Return the state attributes."""
+ return {"attribution": ATTRIBUTION}
+
+ @property
+ def icon(self):
+ """Return the icon for the sensor."""
+ return self._icon
+
+ @property
+ def device_info(self):
+ """Define the device based on name."""
+ return {
+ ATTR_IDENTIFIERS: {(DOMAIN, self._unique_id)},
+ ATTR_NAME: self.coordinator.name,
+ ATTR_MANUFACTURER: "NOAA",
+ ATTR_MODEL: "Aurora Visibility Sensor",
+ ATTR_ENTRY_TYPE: "service",
+ }
diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py
index 82be366ce6ddde..a6d5a1817b293b 100644
--- a/homeassistant/components/aurora/binary_sensor.py
+++ b/homeassistant/components/aurora/binary_sensor.py
@@ -1,75 +1,24 @@
-"""Support for aurora forecast data sensor."""
-import logging
-
+"""Support for Aurora Forecast binary sensor."""
from homeassistant.components.binary_sensor import BinarySensorEntity
-from homeassistant.const import ATTR_NAME
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-
-from . import AuroraDataUpdateCoordinator
-from .const import (
- ATTR_IDENTIFIERS,
- ATTR_MANUFACTURER,
- ATTR_MODEL,
- ATTRIBUTION,
- COORDINATOR,
- DOMAIN,
-)
-_LOGGER = logging.getLogger(__name__)
+from . import AuroraEntity
+from .const import COORDINATOR, DOMAIN
async def async_setup_entry(hass, entry, async_add_entries):
"""Set up the binary_sensor platform."""
coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]
- name = coordinator.name
+ name = f"{coordinator.name} Aurora Visibility Alert"
- entity = AuroraSensor(coordinator, name)
+ entity = AuroraSensor(coordinator=coordinator, name=name, icon="mdi:hazard-lights")
async_add_entries([entity])
-class AuroraSensor(CoordinatorEntity, BinarySensorEntity):
+class AuroraSensor(AuroraEntity, BinarySensorEntity):
"""Implementation of an aurora sensor."""
- def __init__(self, coordinator: AuroraDataUpdateCoordinator, name):
- """Define the binary sensor for the Aurora integration."""
- super().__init__(coordinator=coordinator)
-
- self._name = name
- self.coordinator = coordinator
- self._unique_id = f"{self.coordinator.latitude}_{self.coordinator.longitude}"
-
- @property
- def unique_id(self):
- """Define the unique id based on the latitude and longitude."""
- return self._unique_id
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
@property
def is_on(self):
"""Return true if aurora is visible."""
return self.coordinator.data > self.coordinator.threshold
-
- @property
- def device_state_attributes(self):
- """Return the state attributes."""
- return {"attribution": ATTRIBUTION}
-
- @property
- def icon(self):
- """Return the icon for the sensor."""
- return "mdi:hazard-lights"
-
- @property
- def device_info(self):
- """Define the device based on name."""
- return {
- ATTR_IDENTIFIERS: {(DOMAIN, self._unique_id)},
- ATTR_NAME: self.coordinator.name,
- ATTR_MANUFACTURER: "NOAA",
- ATTR_MODEL: "Aurora Visibility Sensor",
- }
diff --git a/homeassistant/components/aurora/config_flow.py b/homeassistant/components/aurora/config_flow.py
index 37885cc87cf3d2..6e2396d3f5c4d2 100644
--- a/homeassistant/components/aurora/config_flow.py
+++ b/homeassistant/components/aurora/config_flow.py
@@ -1,6 +1,7 @@
"""Config flow for SpaceX Launches and Starman."""
import logging
+from aiohttp import ClientError
from auroranoaa import AuroraForecast
import voluptuous as vol
@@ -40,14 +41,14 @@ async def async_step_user(self, user_input=None):
try:
await api.get_forecast_data(longitude, latitude)
- except ConnectionError:
+ except ClientError:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(
- f"{DOMAIN}_{user_input[CONF_LONGITUDE]}_{user_input[CONF_LATITUDE]}"
+ f"{user_input[CONF_LONGITUDE]}_{user_input[CONF_LATITUDE]}"
)
self._abort_if_unique_id_configured()
return self.async_create_entry(
diff --git a/homeassistant/components/aurora/const.py b/homeassistant/components/aurora/const.py
index f4451de863d171..cd6f54a3d0cd0a 100644
--- a/homeassistant/components/aurora/const.py
+++ b/homeassistant/components/aurora/const.py
@@ -6,6 +6,7 @@
ATTR_IDENTIFIERS = "identifiers"
ATTR_MANUFACTURER = "manufacturer"
ATTR_MODEL = "model"
+ATTR_ENTRY_TYPE = "entry_type"
DEFAULT_POLLING_INTERVAL = 5
CONF_THRESHOLD = "forecast_threshold"
DEFAULT_THRESHOLD = 75
diff --git a/homeassistant/components/aurora/sensor.py b/homeassistant/components/aurora/sensor.py
new file mode 100644
index 00000000000000..d7024cc630a439
--- /dev/null
+++ b/homeassistant/components/aurora/sensor.py
@@ -0,0 +1,33 @@
+"""Support for Aurora Forecast sensor."""
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.const import PERCENTAGE
+
+from . import AuroraEntity
+from .const import COORDINATOR, DOMAIN
+
+
+async def async_setup_entry(hass, entry, async_add_entries):
+ """Set up the sensor platform."""
+ coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]
+
+ entity = AuroraSensor(
+ coordinator=coordinator,
+ name=f"{coordinator.name} Aurora Visibility %",
+ icon="mdi:gauge",
+ )
+
+ async_add_entries([entity])
+
+
+class AuroraSensor(AuroraEntity, SensorEntity):
+ """Implementation of an aurora sensor."""
+
+ @property
+ def state(self):
+ """Return % chance the aurora is visible."""
+ return self.coordinator.data
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measure."""
+ return PERCENTAGE
diff --git a/homeassistant/components/aurora/translations/de.json b/homeassistant/components/aurora/translations/de.json
index 95312fe7943d09..838673e8d60000 100644
--- a/homeassistant/components/aurora/translations/de.json
+++ b/homeassistant/components/aurora/translations/de.json
@@ -1,7 +1,16 @@
{
"config": {
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Breitengrad",
+ "longitude": "L\u00e4ngengrad",
+ "name": "Name"
+ }
+ }
}
},
"options": {
@@ -12,5 +21,6 @@
}
}
}
- }
+ },
+ "title": "NOAA Aurora-Sensor"
}
\ No newline at end of file
diff --git a/homeassistant/components/aurora/translations/fr.json b/homeassistant/components/aurora/translations/fr.json
new file mode 100644
index 00000000000000..473ecefdbd9693
--- /dev/null
+++ b/homeassistant/components/aurora/translations/fr.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00c9chec \u00e0 la connexion"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "name": "Nom"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "threshold": "Seuil (%)"
+ }
+ }
+ }
+ },
+ "title": "Capteur NOAA Aurora"
+}
\ No newline at end of file
diff --git a/homeassistant/components/aurora/translations/hu.json b/homeassistant/components/aurora/translations/hu.json
index d5363860cbd0f3..292ed55223547e 100644
--- a/homeassistant/components/aurora/translations/hu.json
+++ b/homeassistant/components/aurora/translations/hu.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "cannot_connect": "Nem siker\u00fclt csatlakozni"
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
"step": {
"user": {
diff --git a/homeassistant/components/aurora/translations/id.json b/homeassistant/components/aurora/translations/id.json
new file mode 100644
index 00000000000000..66cf534b7ae22f
--- /dev/null
+++ b/homeassistant/components/aurora/translations/id.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Lintang",
+ "longitude": "Bujur",
+ "name": "Nama"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "threshold": "Ambang (%)"
+ }
+ }
+ }
+ },
+ "title": "Sensor Aurora NOAA"
+}
\ No newline at end of file
diff --git a/homeassistant/components/aurora/translations/ko.json b/homeassistant/components/aurora/translations/ko.json
new file mode 100644
index 00000000000000..39f3c4d42818c7
--- /dev/null
+++ b/homeassistant/components/aurora/translations/ko.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "\uc704\ub3c4",
+ "longitude": "\uacbd\ub3c4",
+ "name": "\uc774\ub984"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "threshold": "\uc784\uacc4\uac12 (%)"
+ }
+ }
+ }
+ },
+ "title": "NOAA Aurora \uc13c\uc11c"
+}
\ No newline at end of file
diff --git a/homeassistant/components/aurora/translations/nl.json b/homeassistant/components/aurora/translations/nl.json
new file mode 100644
index 00000000000000..fe7b4809f1320c
--- /dev/null
+++ b/homeassistant/components/aurora/translations/nl.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Breedtegraad",
+ "longitude": "Lengtegraad",
+ "name": "Naam"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "threshold": "Drempel (%)"
+ }
+ }
+ }
+ },
+ "title": "NOAA Aurora Sensor"
+}
\ No newline at end of file
diff --git a/homeassistant/components/aurora/translations/tr.json b/homeassistant/components/aurora/translations/tr.json
new file mode 100644
index 00000000000000..0c3bb75ed6e913
--- /dev/null
+++ b/homeassistant/components/aurora/translations/tr.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Enlem",
+ "longitude": "Boylam",
+ "name": "Ad"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aurora/translations/uk.json b/homeassistant/components/aurora/translations/uk.json
new file mode 100644
index 00000000000000..0cb3c4fcbceb43
--- /dev/null
+++ b/homeassistant/components/aurora/translations/uk.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
+ "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430",
+ "name": "\u041d\u0430\u0437\u0432\u0430"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "threshold": "\u041f\u043e\u0440\u0456\u0433 (%)"
+ }
+ }
+ }
+ },
+ "title": "NOAA Aurora Sensor"
+}
\ No newline at end of file
diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py
index 69a513dd8fb5ed..f4640e7c014229 100644
--- a/homeassistant/components/aurora_abb_powerone/sensor.py
+++ b/homeassistant/components/aurora_abb_powerone/sensor.py
@@ -5,7 +5,7 @@
from aurorapy.client import AuroraError, AuroraSerialClient
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_ADDRESS,
CONF_DEVICE,
@@ -14,7 +14,6 @@
POWER_WATT,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -44,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(devices, True)
-class AuroraABBSolarPVMonitorSensor(Entity):
+class AuroraABBSolarPVMonitorSensor(SensorEntity):
"""Representation of a Sensor."""
def __init__(self, client, name, typename):
diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py
index 4ddf82cc022108..7381be5e9de613 100644
--- a/homeassistant/components/auth/__init__.py
+++ b/homeassistant/components/auth/__init__.py
@@ -114,8 +114,9 @@
}
"""
+from __future__ import annotations
+
from datetime import timedelta
-from typing import Union
import uuid
from aiohttp import web
@@ -183,7 +184,7 @@
@bind_hass
def create_auth_code(
- hass, client_id: str, credential_or_user: Union[Credentials, User]
+ hass, client_id: str, credential_or_user: Credentials | User
) -> str:
"""Create an authorization code to fetch tokens."""
return hass.data[DOMAIN](client_id, credential_or_user)
diff --git a/homeassistant/components/auth/translations/de.json b/homeassistant/components/auth/translations/de.json
index 06da3cde1a132b..93cbf1073ccbe5 100644
--- a/homeassistant/components/auth/translations/de.json
+++ b/homeassistant/components/auth/translations/de.json
@@ -25,7 +25,7 @@
},
"step": {
"init": {
- "description": "Um die Zwei-Faktor-Authentifizierung mit zeitbasierten Einmalpassw\u00f6rtern zu aktivieren, scanne den QR-Code mit Ihrer Authentifizierungs-App. Wenn du keine hast, empfehlen wir entweder [Google Authenticator] (https://support.google.com/accounts/answer/1066447) oder [Authy] (https://authy.com/). \n\n {qr_code} \n \nNachdem du den Code gescannt hast, gebe den sechsstelligen Code aus der App ein, um das Setup zu \u00fcberpr\u00fcfen. Wenn es Probleme beim Scannen des QR-Codes gibt, f\u00fchre ein manuelles Setup mit dem Code ** ` {code} ` ** durch.",
+ "description": "Um die Zwei-Faktor-Authentifizierung mit zeitbasierten Einmalpassw\u00f6rtern zu aktivieren, scanne den QR-Code mit deiner Authentifizierungs-App. Wenn du keine hast, empfehlen wir entweder [Google Authenticator] (https://support.google.com/accounts/answer/1066447) oder [Authy] (https://authy.com/). \n\n {qr_code} \n \nNachdem du den Code gescannt hast, gibst du den sechsstelligen Code aus der App ein, um das Setup zu \u00fcberpr\u00fcfen. Wenn es Probleme beim Scannen des QR-Codes gibt, f\u00fchre ein manuelles Setup mit dem Code ** ` {code} ` ** durch.",
"title": "Richte die Zwei-Faktor-Authentifizierung mit TOTP ein"
}
},
diff --git a/homeassistant/components/auth/translations/et.json b/homeassistant/components/auth/translations/et.json
index 03fc42f38e2ac2..9b22951e7fa6f7 100644
--- a/homeassistant/components/auth/translations/et.json
+++ b/homeassistant/components/auth/translations/et.json
@@ -10,7 +10,7 @@
"step": {
"init": {
"description": "Vali \u00fcks teavitusteenustest:",
- "title": "Seadistage Notify poolt edastatud \u00fchekordne parool"
+ "title": "Seadista Notify poolt edastatud \u00fchekordne parool"
},
"setup": {
"description": "\u00dchekordne parool on saadetud **notify. {notify_service}**. Palun sisesta see allpool:",
@@ -26,7 +26,7 @@
"step": {
"init": {
"description": "Kahefaktorilise autentimise aktiveerimiseks ajap\u00f5histe \u00fchekordsete paroolide abil skanni QR-kood oma autentimisrakendusega. Kui seda pole, soovitame kas [Google Authenticator] (https://support.google.com/accounts/answer/1066447) v\u00f5i [Authy] (https://authy.com/).\n\n {qr_code}\n\n P\u00e4rast koodi skannimist sisesta seadistuse kinnitamiseks rakenduse kuuekohaline kood. Kui on probleeme QR-koodi skannimisega, tehke koodiga **' {code}' ** k\u00e4sitsi seadistamine.",
- "title": "Seadistage TOTP-ga kaheastmeline autentimine"
+ "title": "Seadista TOTP-ga kaheastmeline autentimine"
}
},
"title": ""
diff --git a/homeassistant/components/auth/translations/id.json b/homeassistant/components/auth/translations/id.json
index f6a22386f99bfb..ed7bede5fff458 100644
--- a/homeassistant/components/auth/translations/id.json
+++ b/homeassistant/components/auth/translations/id.json
@@ -1,13 +1,32 @@
{
"mfa_setup": {
+ "notify": {
+ "abort": {
+ "no_available_service": "Tidak ada layanan notifikasi yang tersedia."
+ },
+ "error": {
+ "invalid_code": "Kode tifak valid, coba lagi."
+ },
+ "step": {
+ "init": {
+ "description": "Pilih salah satu layanan notifikasi:",
+ "title": "Siapkan kata sandi sekali pakai yang dikirimkan oleh komponen notify"
+ },
+ "setup": {
+ "description": "Kata sandi sekali pakai telah dikirim melalui **notify.{notify_service}**. Masukkan di bawah ini:",
+ "title": "Verifikasi penyiapan"
+ }
+ },
+ "title": "Kata Sandi Sekali Pakai Notifikasi"
+ },
"totp": {
"error": {
- "invalid_code": "Kode salah, coba lagi. Jika Anda mendapatkan kesalahan ini secara konsisten, pastikan jam pada sistem Home Assistant anda akurat."
+ "invalid_code": "Kode tidak valid, coba lagi. Jika Anda terus mendapatkan kesalahan yang sama, pastikan jam pada sistem Home Assistant Anda sudah akurat."
},
"step": {
"init": {
- "description": "Untuk mengaktifkan otentikasi dua faktor menggunakan password satu kali berbasis waktu, pindai kode QR dengan aplikasi otentikasi Anda. Jika Anda tidak memilikinya, kami menyarankan [Google Authenticator] (https://support.google.com/accounts/answer/1066447) atau [Authy] (https://authy.com/). \n\n {qr_code} \n \n Setelah memindai kode, masukkan kode enam digit dari aplikasi Anda untuk memverifikasi pengaturan. Jika Anda mengalami masalah saat memindai kode QR, lakukan pengaturan manual dengan kode ** ` {code} ` **.",
- "title": "Siapkan otentikasi dua faktor menggunakan TOTP"
+ "description": "Untuk mengaktifkan autentikasi dua faktor menggunakan kata sandi sekali pakai berbasis waktu, pindai kode QR dengan aplikasi autentikasi Anda. Jika tidak punya, kami menyarankan aplikasi [Google Authenticator] (https://support.google.com/accounts/answer/1066447) atau [Authy] (https://authy.com/). \n\n {qr_code} \n \nSetelah memindai kode, masukkan kode enam digit dari aplikasi Anda untuk memverifikasi penyiapan. Jika mengalami masalah saat memindai kode QR, lakukan penyiapan manual dengan kode **`{code}`**.",
+ "title": "Siapkan autentikasi dua faktor menggunakan TOTP"
}
},
"title": "TOTP"
diff --git a/homeassistant/components/auth/translations/ko.json b/homeassistant/components/auth/translations/ko.json
index 80850bb58b4c64..09af8eb89bfa7e 100644
--- a/homeassistant/components/auth/translations/ko.json
+++ b/homeassistant/components/auth/translations/ko.json
@@ -5,7 +5,7 @@
"no_available_service": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c \uc54c\ub9bc \uc11c\ube44\uc2a4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4."
},
"error": {
- "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694."
+ "invalid_code": "\ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694."
},
"step": {
"init": {
@@ -13,7 +13,7 @@
"title": "\uc54c\ub9bc \uad6c\uc131\uc694\uc18c\uac00 \uc81c\uacf5\ud558\ub294 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc124\uc815\ud558\uae30"
},
"setup": {
- "description": "**notify.{notify_service}** \uc5d0\uc11c \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4. \uc544\ub798\uc758 \uacf5\ub780\uc5d0 \uc785\ub825\ud574\uc8fc\uc138\uc694:",
+ "description": "**notify.{notify_service}**\uc5d0\uc11c \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4. \uc544\ub798\uc758 \uacf5\ub780\uc5d0 \uc785\ub825\ud574\uc8fc\uc138\uc694:",
"title": "\uc124\uc815 \ud655\uc778\ud558\uae30"
}
},
@@ -21,11 +21,11 @@
},
"totp": {
"error": {
- "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant \uc758 \uc2dc\uac04\uc124\uc815\uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694."
+ "invalid_code": "\ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant\uc758 \uc2dc\uac04\uc124\uc815\uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694."
},
"step": {
"init": {
- "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \uad6c\uc131\ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud558\uc5ec \uc124\uc815\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\uc8fc\uc138\uc694.",
+ "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \uad6c\uc131\ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/)\ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud558\uc5ec \uc124\uc815\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\uc8fc\uc138\uc694.",
"title": "TOTP 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131\ud558\uae30"
}
},
diff --git a/homeassistant/components/auth/translations/tr.json b/homeassistant/components/auth/translations/tr.json
new file mode 100644
index 00000000000000..7d273214574684
--- /dev/null
+++ b/homeassistant/components/auth/translations/tr.json
@@ -0,0 +1,22 @@
+{
+ "mfa_setup": {
+ "notify": {
+ "step": {
+ "init": {
+ "title": "Bilgilendirme bile\u015feni taraf\u0131ndan verilen tek seferlik parolay\u0131 ayarlay\u0131n"
+ },
+ "setup": {
+ "description": "**bildirim yoluyla tek seferlik bir parola g\u00f6nderildi. {notify_service}**. L\u00fctfen a\u015fa\u011f\u0131da girin:"
+ }
+ },
+ "title": "Tek Seferlik Parolay\u0131 Bildir"
+ },
+ "totp": {
+ "step": {
+ "init": {
+ "description": "Zamana dayal\u0131 tek seferlik parolalar\u0131 kullanarak iki fakt\u00f6rl\u00fc kimlik do\u011frulamay\u0131 etkinle\u015ftirmek i\u00e7in kimlik do\u011frulama uygulaman\u0131zla QR kodunu taray\u0131n. Hesab\u0131n\u0131z yoksa, [Google Authenticator] (https://support.google.com/accounts/answer/1066447) veya [Authy] (https://authy.com/) \u00f6neririz. \n\n {qr_code}\n\n Kodu tarad\u0131ktan sonra, kurulumu do\u011frulamak i\u00e7in uygulaman\u0131zdan alt\u0131 haneli kodu girin. QR kodunu taramayla ilgili sorun ya\u015f\u0131yorsan\u0131z, ** ` {code} ` manuel kurulum yap\u0131n."
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/auth/translations/uk.json b/homeassistant/components/auth/translations/uk.json
index f826075078e7b5..eeb8f1ee7c7c2e 100644
--- a/homeassistant/components/auth/translations/uk.json
+++ b/homeassistant/components/auth/translations/uk.json
@@ -1,14 +1,35 @@
{
"mfa_setup": {
"notify": {
+ "abort": {
+ "no_available_service": "\u041d\u0435\u043c\u0430\u0454 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u0441\u043b\u0443\u0436\u0431 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u044c."
+ },
"error": {
"invalid_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437."
},
"step": {
+ "init": {
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0434\u043d\u0443 \u0456\u0437 \u0441\u043b\u0443\u0436\u0431 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u044c:",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0438 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u0438\u0445 \u043f\u0430\u0440\u043e\u043b\u0456\u0432 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u044c"
+ },
"setup": {
+ "description": "\u041e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u0438\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0439 \u0447\u0435\u0440\u0435\u0437 ** notify.{notify_service} **. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0439\u043e\u0433\u043e \u043d\u0438\u0436\u0447\u0435:",
"title": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f"
}
- }
+ },
+ "title": "\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u0438\u0445 \u043f\u0430\u0440\u043e\u043b\u0456\u0432"
+ },
+ "totp": {
+ "error": {
+ "invalid_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443. \u042f\u043a\u0449\u043e \u0412\u0438 \u043f\u043e\u0441\u0442\u0456\u0439\u043d\u043e \u043e\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u0435 \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0433\u043e\u0434\u0438\u043d\u043d\u0438\u043a \u0443 \u0412\u0430\u0448\u0456\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0456 Home Assistant \u043f\u043e\u043a\u0430\u0437\u0443\u0454 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u0447\u0430\u0441."
+ },
+ "step": {
+ "init": {
+ "description": "\u0429\u043e\u0431 \u0430\u043a\u0442\u0438\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u0438\u0445 \u043f\u0430\u0440\u043e\u043b\u0456\u0432, \u0437\u0430\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u0445 \u043d\u0430 \u0447\u0430\u0441\u0456, \u0432\u0456\u0434\u0441\u043a\u0430\u043d\u0443\u0439\u0442\u0435 QR-\u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0438 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0447\u043d\u043e\u0441\u0442\u0456. \u042f\u043a\u0449\u043e \u0443 \u0412\u0430\u0441 \u0457\u0457 \u043d\u0435\u043c\u0430\u0454, \u043c\u0438 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0454\u043c\u043e \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0430\u0431\u043e [Google Authenticator](https://support.google.com/accounts/answer/1066447), \u0430\u0431\u043e [Authy](https://authy.com/). \n\n{qr_code}\n\n\u041f\u0456\u0441\u043b\u044f \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f QR-\u043a\u043e\u0434\u0443 \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0448\u0435\u0441\u0442\u0438\u0437\u043d\u0430\u0447\u043d\u0438\u0439 \u043a\u043e\u0434 \u0437 \u0412\u0430\u0448\u043e\u0433\u043e \u0437\u0430\u0441\u0442\u043e\u0441\u0443\u0432\u0430\u043d\u043d\u044f, \u0449\u043e\u0431 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u042f\u043a\u0449\u043e \u0443 \u0412\u0430\u0441 \u0454 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0437\u0456 \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f\u043c QR-\u043a\u043e\u0434\u0443, \u0432\u0438\u043a\u043e\u043d\u0430\u0439\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u043a\u043e\u0434\u0443 ** `{code}` **.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c TOTP"
+ }
+ },
+ "title": "TOTP"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/auth/translations/zh-Hant.json b/homeassistant/components/auth/translations/zh-Hant.json
index 96e7f21ac998c1..8e769cb59830cf 100644
--- a/homeassistant/components/auth/translations/zh-Hant.json
+++ b/homeassistant/components/auth/translations/zh-Hant.json
@@ -25,8 +25,8 @@
},
"step": {
"init": {
- "description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u96d9\u91cd\u9a57\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u641c\u5c0b\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u641c\u5c0b\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002",
- "title": "\u4f7f\u7528 TOTP \u8a2d\u5b9a\u96d9\u91cd\u9a57\u8b49"
+ "description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u96d9\u91cd\u8a8d\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u641c\u5c0b\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u641c\u5c0b\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002",
+ "title": "\u4f7f\u7528 TOTP \u8a2d\u5b9a\u96d9\u91cd\u8a8d\u8b49"
}
},
"title": "TOTP"
diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py
index 201eeb5c456505..36b7f1688f856f 100644
--- a/homeassistant/components/automation/__init__.py
+++ b/homeassistant/components/automation/__init__.py
@@ -1,6 +1,8 @@
"""Allow to set up simple automation rules via the config file."""
+from __future__ import annotations
+
import logging
-from typing import Any, Awaitable, Callable, Dict, List, Optional, Set, Union, cast
+from typing import Any, Awaitable, Callable, Dict, cast
import voluptuous as vol
from voluptuous.humanize import humanize_error
@@ -8,8 +10,10 @@
from homeassistant.components import blueprint
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_MODE,
ATTR_NAME,
CONF_ALIAS,
+ CONF_CONDITION,
CONF_DEVICE_ID,
CONF_ENTITY_ID,
CONF_ID,
@@ -31,7 +35,12 @@
callback,
split_entity_id,
)
-from homeassistant.exceptions import HomeAssistantError
+from homeassistant.exceptions import (
+ ConditionError,
+ ConditionErrorContainer,
+ ConditionErrorIndex,
+ HomeAssistantError,
+)
from homeassistant.helpers import condition, extract_domain_configs, template
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import ToggleEntity
@@ -40,31 +49,39 @@
from homeassistant.helpers.script import (
ATTR_CUR,
ATTR_MAX,
- ATTR_MODE,
CONF_MAX,
CONF_MAX_EXCEEDED,
Script,
)
from homeassistant.helpers.script_variables import ScriptVariables
from homeassistant.helpers.service import async_register_admin_service
+from homeassistant.helpers.trace import (
+ TraceElement,
+ script_execution_set,
+ trace_append_element,
+ trace_get,
+ trace_path,
+)
from homeassistant.helpers.trigger import async_initialize_triggers
from homeassistant.helpers.typing import TemplateVarsType
from homeassistant.loader import bind_hass
from homeassistant.util.dt import parse_datetime
+from .config import AutomationConfig, async_validate_config_item
+
# Not used except by packages to check config structure
-from .config import PLATFORM_SCHEMA # noqa
-from .config import async_validate_config_item
+from .config import PLATFORM_SCHEMA # noqa: F401
from .const import (
CONF_ACTION,
- CONF_CONDITION,
CONF_INITIAL_STATE,
CONF_TRIGGER,
+ CONF_TRIGGER_VARIABLES,
DEFAULT_INITIAL_STATE,
DOMAIN,
LOGGER,
)
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
@@ -84,6 +101,7 @@
ATTR_VARIABLES = "variables"
SERVICE_TRIGGER = "trigger"
+_LOGGER = logging.getLogger(__name__)
AutomationActionType = Callable[[HomeAssistant, TemplateVarsType], Awaitable[None]]
@@ -98,7 +116,7 @@ def is_on(hass, entity_id):
@callback
-def automations_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]:
+def automations_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]:
"""Return all automations that reference the entity."""
if DOMAIN not in hass.data:
return []
@@ -113,7 +131,7 @@ def automations_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]:
@callback
-def entities_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]:
+def entities_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]:
"""Return all entities in a scene."""
if DOMAIN not in hass.data:
return []
@@ -129,7 +147,7 @@ def entities_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]:
@callback
-def automations_with_device(hass: HomeAssistant, device_id: str) -> List[str]:
+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 []
@@ -144,7 +162,7 @@ def automations_with_device(hass: HomeAssistant, device_id: str) -> List[str]:
@callback
-def devices_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]:
+def devices_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]:
"""Return all devices in a scene."""
if DOMAIN not in hass.data:
return []
@@ -159,8 +177,40 @@ def devices_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]:
return list(automation_entity.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
+ ]
+
+
+@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]
+
+ automation_entity = component.get_entity(entity_id)
+
+ if automation_entity is None:
+ return []
+
+ return list(automation_entity.referenced_areas)
+
+
async def async_setup(hass, config):
- """Set up the automation."""
+ """Set up all automations."""
+ # Local import to avoid circular import
hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass)
# To register the automation blueprints
@@ -170,9 +220,9 @@ async def async_setup(hass, config):
await async_get_blueprints(hass).async_populate()
async def trigger_service_handler(entity, service_call):
- """Handle automation triggers."""
+ """Handle forced automation trigger, e.g. from frontend."""
await entity.async_trigger(
- service_call.data[ATTR_VARIABLES],
+ {**service_call.data[ATTR_VARIABLES], "trigger": {"platform": None}},
skip_condition=service_call.data[CONF_SKIP_CONDITION],
context=service_call.context,
)
@@ -221,6 +271,9 @@ def __init__(
action_script,
initial_state,
variables,
+ trigger_variables,
+ raw_config,
+ blueprint_inputs,
):
"""Initialize an automation entity."""
self._id = automation_id
@@ -232,10 +285,13 @@ def __init__(
self.action_script.change_listener = self.async_write_ha_state
self._initial_state = initial_state
self._is_enabled = False
- self._referenced_entities: Optional[Set[str]] = None
- self._referenced_devices: Optional[Set[str]] = None
+ 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._blueprint_inputs = blueprint_inputs
@property
def name(self):
@@ -253,7 +309,7 @@ def should_poll(self):
return False
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the entity state attributes."""
attrs = {
ATTR_LAST_TRIGGERED: self.action_script.last_triggered,
@@ -262,6 +318,8 @@ def state_attributes(self):
}
if self.action_script.supports_max:
attrs[ATTR_MAX] = self.action_script.max_runs
+ if self._id is not None:
+ attrs[CONF_ID] = self._id
return attrs
@property
@@ -269,6 +327,11 @@ def is_on(self) -> bool:
"""Return True if entity is on."""
return self._async_detach_triggers is not None or self._is_enabled
+ @property
+ def referenced_areas(self):
+ """Return a set of referenced areas."""
+ return self.action_script.referenced_areas
+
@property
def referenced_devices(self):
"""Return a set of referenced devices."""
@@ -366,52 +429,87 @@ async def async_trigger(self, run_variables, context=None, skip_condition=False)
This method is a coroutine.
"""
- if self._variables:
- try:
- variables = self._variables.async_render(self.hass, run_variables)
- except template.TemplateError as err:
- self._logger.error("Error rendering variables: %s", err)
- return
- else:
- variables = run_variables
-
- if (
- not skip_condition
- and self._cond_func is not None
- and not self._cond_func(variables)
- ):
- return
+ reason = ""
+ if "trigger" in run_variables and "description" in run_variables["trigger"]:
+ reason = f' by {run_variables["trigger"]["description"]}'
+ self._logger.debug("Automation triggered%s", reason)
# Create a new context referring to the old context.
parent_id = None if context is None else context.id
trigger_context = Context(parent_id=parent_id)
- self.async_set_context(trigger_context)
- event_data = {
- ATTR_NAME: self._name,
- ATTR_ENTITY_ID: self.entity_id,
- }
- if "trigger" in variables and "description" in variables["trigger"]:
- event_data[ATTR_SOURCE] = variables["trigger"]["description"]
+ with trace_automation(
+ self.hass,
+ self.unique_id,
+ self._raw_config,
+ self._blueprint_inputs,
+ trigger_context,
+ ) as automation_trace:
+ if self._variables:
+ try:
+ variables = self._variables.async_render(self.hass, run_variables)
+ except template.TemplateError as err:
+ self._logger.error("Error rendering variables: %s", err)
+ automation_trace.set_error(err)
+ return
+ else:
+ variables = run_variables
+ # Prepare tracing the automation
+ automation_trace.set_trace(trace_get())
- @callback
- def started_action():
- self.hass.bus.async_fire(
- EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context
- )
+ # Set trigger reason
+ trigger_description = variables.get("trigger", {}).get("description")
+ automation_trace.set_trigger_description(trigger_description)
- try:
- await self.action_script.async_run(
- variables, trigger_context, started_action
- )
- except (vol.Invalid, HomeAssistantError) as err:
- self._logger.error(
- "Error while executing automation %s: %s",
- self.entity_id,
- err,
- )
- except Exception: # pylint: disable=broad-except
- self._logger.exception("While executing automation %s", self.entity_id)
+ # Add initial variables as the trigger step
+ if "trigger" in variables and "id" in variables["trigger"]:
+ trigger_path = f"trigger/{variables['trigger']['id']}"
+ else:
+ trigger_path = "trigger"
+ trace_element = TraceElement(variables, trigger_path)
+ trace_append_element(trace_element)
+
+ if (
+ not skip_condition
+ and self._cond_func is not None
+ and not self._cond_func(variables)
+ ):
+ self._logger.debug(
+ "Conditions not met, aborting automation. Condition summary: %s",
+ trace_get(clear=False),
+ )
+ script_execution_set("failed_conditions")
+ return
+
+ self.async_set_context(trigger_context)
+ event_data = {
+ ATTR_NAME: self._name,
+ ATTR_ENTITY_ID: self.entity_id,
+ }
+ if "trigger" in variables and "description" in variables["trigger"]:
+ event_data[ATTR_SOURCE] = variables["trigger"]["description"]
+
+ @callback
+ def started_action():
+ self.hass.bus.async_fire(
+ EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context
+ )
+
+ try:
+ with trace_path("action"):
+ await self.action_script.async_run(
+ variables, trigger_context, started_action
+ )
+ except (vol.Invalid, HomeAssistantError) as err:
+ self._logger.error(
+ "Error while executing automation %s: %s",
+ self.entity_id,
+ err,
+ )
+ automation_trace.set_error(err)
+ except Exception as err: # pylint: disable=broad-except
+ self._logger.exception("While executing automation %s", self.entity_id)
+ automation_trace.set_error(err)
async def async_will_remove_from_hass(self):
"""Remove listeners when removing automation from Home Assistant."""
@@ -465,34 +563,37 @@ async def async_disable(self, stop_actions=DEFAULT_STOP_ACTIONS):
async def _async_attach_triggers(
self, home_assistant_start: bool
- ) -> Optional[Callable[[], None]]:
+ ) -> Callable[[], None] | None:
"""Set up the triggers."""
def log_cb(level, msg, **kwargs):
self._logger.log(level, "%s %s", msg, self._name, **kwargs)
+ variables = None
+ if self._trigger_variables:
+ try:
+ variables = self._trigger_variables.async_render(
+ self.hass, None, limited=True
+ )
+ except template.TemplateError as err:
+ self._logger.error("Error rendering trigger variables: %s", err)
+ return None
+
return await async_initialize_triggers(
- cast(HomeAssistant, self.hass),
+ self.hass,
self._trigger_config,
self.async_trigger,
DOMAIN,
self._name,
log_cb,
home_assistant_start,
+ variables,
)
- @property
- def device_state_attributes(self):
- """Return automation attributes."""
- if self._id is None:
- return None
-
- return {CONF_ID: self._id}
-
async def _async_process_config(
hass: HomeAssistant,
- config: Dict[str, Any],
+ config: dict[str, Any],
component: EntityComponent,
) -> bool:
"""Process config and add automations.
@@ -503,21 +604,23 @@ async def _async_process_config(
blueprints_used = False
for config_key in extract_domain_configs(config, DOMAIN):
- conf: List[Union[Dict[str, Any], blueprint.BlueprintInputs]] = config[ # type: ignore
+ conf: list[dict[str, Any] | blueprint.BlueprintInputs] = config[ # type: ignore
config_key
]
for list_no, config_block in enumerate(conf):
+ raw_blueprint_inputs = None
+ raw_config = None
if isinstance(config_block, blueprint.BlueprintInputs): # type: ignore
blueprints_used = True
blueprint_inputs = config_block
+ raw_blueprint_inputs = blueprint_inputs.config_with_inputs
try:
+ raw_config = blueprint_inputs.async_substitute()
config_block = cast(
Dict[str, Any],
- await async_validate_config_item(
- hass, blueprint_inputs.async_substitute()
- ),
+ await async_validate_config_item(hass, raw_config),
)
except vol.Invalid as err:
LOGGER.error(
@@ -527,6 +630,8 @@ async def _async_process_config(
humanize_error(config_block, err),
)
continue
+ 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}"
@@ -549,13 +654,25 @@ async def _async_process_config(
)
if CONF_CONDITION in config_block:
- cond_func = await _async_process_if(hass, config, config_block)
+ cond_func = await _async_process_if(hass, name, config, config_block)
if cond_func is None:
continue
else:
cond_func = None
+ # Add trigger variables to variables
+ variables = None
+ if CONF_TRIGGER_VARIABLES in config_block:
+ variables = ScriptVariables(
+ dict(config_block[CONF_TRIGGER_VARIABLES].as_dict())
+ )
+ if CONF_VARIABLES in config_block:
+ if variables:
+ variables.variables.update(config_block[CONF_VARIABLES].as_dict())
+ else:
+ variables = config_block[CONF_VARIABLES]
+
entity = AutomationEntity(
automation_id,
name,
@@ -563,7 +680,10 @@ async def _async_process_config(
cond_func,
action_script,
initial_state,
- config_block.get(CONF_VARIABLES),
+ variables,
+ config_block.get(CONF_TRIGGER_VARIABLES),
+ raw_config,
+ raw_blueprint_inputs,
)
entities.append(entity)
@@ -574,7 +694,7 @@ async def _async_process_config(
return blueprints_used
-async def _async_process_if(hass, config, p_config):
+async def _async_process_if(hass, name, config, p_config):
"""Process if checks."""
if_configs = p_config[CONF_CONDITION]
@@ -588,7 +708,28 @@ async def _async_process_if(hass, config, p_config):
def if_action(variables=None):
"""AND all conditions."""
- return all(check(hass, variables) for check in checks)
+ errors = []
+ for index, check in enumerate(checks):
+ try:
+ with trace_path(["condition", str(index)]):
+ if not check(hass, variables):
+ return False
+ except ConditionError as ex:
+ errors.append(
+ ConditionErrorIndex(
+ "condition", index=index, total=len(checks), error=ex
+ )
+ )
+
+ if errors:
+ LOGGER.warning(
+ "Error evaluating condition in '%s':\n%s",
+ name,
+ ConditionErrorContainer("condition", errors=errors),
+ )
+ return False
+
+ return True
if_action.config = if_configs
@@ -596,7 +737,7 @@ def if_action(variables=None):
@callback
-def _trigger_extract_device(trigger_conf: dict) -> Optional[str]:
+def _trigger_extract_device(trigger_conf: dict) -> str | None:
"""Extract devices from a trigger config."""
if trigger_conf[CONF_PLATFORM] != "device":
return None
@@ -605,7 +746,7 @@ def _trigger_extract_device(trigger_conf: dict) -> Optional[str]:
@callback
-def _trigger_extract_entities(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]
diff --git a/homeassistant/components/automation/blueprints/motion_light.yaml b/homeassistant/components/automation/blueprints/motion_light.yaml
index c11d22d974eb9e..54a4a4f0643a05 100644
--- a/homeassistant/components/automation/blueprints/motion_light.yaml
+++ b/homeassistant/components/automation/blueprints/motion_light.yaml
@@ -38,13 +38,17 @@ trigger:
to: "on"
action:
- - service: light.turn_on
+ - alias: "Turn on the light"
+ service: light.turn_on
target: !input light_target
- - wait_for_trigger:
+ - alias: "Wait until there is no motion from device"
+ wait_for_trigger:
platform: state
entity_id: !input motion_entity
from: "on"
to: "off"
- - delay: !input no_motion_wait
- - service: light.turn_off
+ - alias: "Wait the number of seconds that has been set"
+ delay: !input no_motion_wait
+ - alias: "Turn off the light"
+ service: light.turn_off
target: !input light_target
diff --git a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml
index d3a70d773eef7a..71abf8f865cd63 100644
--- a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml
+++ b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml
@@ -37,7 +37,8 @@ condition:
value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}"
action:
- domain: mobile_app
- type: notify
- device_id: !input notify_device
- message: "{{ person_name }} has left {{ zone_state }}"
+ - alias: "Notify that a person has left the zone"
+ domain: mobile_app
+ type: notify
+ device_id: !input notify_device
+ message: "{{ person_name }} has left {{ zone_state }}"
diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py
index 9c26f3552aa9c3..b4b8b49fa3e5b1 100644
--- a/homeassistant/components/automation/config.py
+++ b/homeassistant/components/automation/config.py
@@ -1,5 +1,6 @@
"""Config validation helper for the automation integration."""
import asyncio
+from contextlib import suppress
import voluptuous as vol
@@ -8,7 +9,13 @@
InvalidDeviceAutomationConfig,
)
from homeassistant.config import async_log_exception, config_without_domain
-from homeassistant.const import CONF_ALIAS, CONF_ID, CONF_VARIABLES
+from homeassistant.const import (
+ CONF_ALIAS,
+ CONF_CONDITION,
+ CONF_DESCRIPTION,
+ CONF_ID,
+ CONF_VARIABLES,
+)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform, config_validation as cv, script
from homeassistant.helpers.condition import async_validate_condition_config
@@ -17,11 +24,10 @@
from .const import (
CONF_ACTION,
- CONF_CONDITION,
- CONF_DESCRIPTION,
CONF_HIDE_ENTITY,
CONF_INITIAL_STATE,
CONF_TRIGGER,
+ CONF_TRIGGER_VARIABLES,
DOMAIN,
)
from .helpers import async_get_blueprints
@@ -44,6 +50,7 @@
vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
+ vol.Optional(CONF_TRIGGER_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
},
script.SCRIPT_MODE_SINGLE,
@@ -78,8 +85,18 @@ async def async_validate_config_item(hass, config, full_config=None):
return config
+class AutomationConfig(dict):
+ """Dummy class to allow adding attributes."""
+
+ raw_config = None
+
+
async def _try_async_validate_config_item(hass, config, full_config=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)
except (
@@ -91,6 +108,11 @@ 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
+
+ config = AutomationConfig(config)
+ config.raw_config = raw_config
return config
diff --git a/homeassistant/components/automation/const.py b/homeassistant/components/automation/const.py
index c8db3aa01e56fe..d6f34ddfeb642f 100644
--- a/homeassistant/components/automation/const.py
+++ b/homeassistant/components/automation/const.py
@@ -1,12 +1,11 @@
"""Constants for the automation integration."""
import logging
-CONF_CONDITION = "condition"
CONF_ACTION = "action"
CONF_TRIGGER = "trigger"
+CONF_TRIGGER_VARIABLES = "trigger_variables"
DOMAIN = "automation"
-CONF_DESCRIPTION = "description"
CONF_HIDE_ENTITY = "hide_entity"
CONF_CONDITION_TYPE = "condition_type"
diff --git a/homeassistant/components/automation/logbook.py b/homeassistant/components/automation/logbook.py
index 3c9671af18f24e..b5dbada1b7a098 100644
--- a/homeassistant/components/automation/logbook.py
+++ b/homeassistant/components/automation/logbook.py
@@ -1,26 +1,29 @@
"""Describe logbook events."""
+from homeassistant.components.logbook import LazyEventPartialState
from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
from . import ATTR_SOURCE, DOMAIN, EVENT_AUTOMATION_TRIGGERED
@callback
-def async_describe_events(hass, async_describe_event): # type: ignore
+def async_describe_events(hass: HomeAssistant, async_describe_event): # type: ignore
"""Describe logbook events."""
@callback
- def async_describe_logbook_event(event): # type: ignore
+ def async_describe_logbook_event(event: LazyEventPartialState): # type: ignore
"""Describe a logbook event."""
data = event.data
message = "has been triggered"
if ATTR_SOURCE in data:
message = f"{message} by {data[ATTR_SOURCE]}"
+
return {
"name": data.get(ATTR_NAME),
"message": message,
"source": data.get(ATTR_SOURCE),
"entity_id": data.get(ATTR_ENTITY_ID),
+ "context_id": event.context_id,
}
async_describe_event(
diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json
index 2db56eb597faa7..2483f57de8eece 100644
--- a/homeassistant/components/automation/manifest.json
+++ b/homeassistant/components/automation/manifest.json
@@ -2,7 +2,7 @@
"domain": "automation",
"name": "Automation",
"documentation": "https://www.home-assistant.io/integrations/automation",
- "dependencies": ["blueprint"],
+ "dependencies": ["blueprint", "trace"],
"after_dependencies": [
"device_automation",
"webhook"
diff --git a/homeassistant/components/automation/reproduce_state.py b/homeassistant/components/automation/reproduce_state.py
index bcd0cc4e58570a..ff716f3a83bf9a 100644
--- a/homeassistant/components/automation/reproduce_state.py
+++ b/homeassistant/components/automation/reproduce_state.py
@@ -1,7 +1,9 @@
"""Reproduce an Automation state."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -10,8 +12,7 @@
STATE_OFF,
STATE_ON,
)
-from homeassistant.core import Context, State
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import Context, HomeAssistant, State
from . import DOMAIN
@@ -21,11 +22,11 @@
async def _async_reproduce_state(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -57,11 +58,11 @@ async def _async_reproduce_state(
async def async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Automation states."""
await asyncio.gather(
diff --git a/homeassistant/components/automation/services.yaml b/homeassistant/components/automation/services.yaml
index 2f5b0a231e4908..5d399fb253efc6 100644
--- a/homeassistant/components/automation/services.yaml
+++ b/homeassistant/components/automation/services.yaml
@@ -1,37 +1,40 @@
# Describes the format for available automation services
turn_on:
+ name: Turn on
description: Enable an automation.
- fields:
- entity_id:
- description: Name of the automation to turn on.
- example: "automation.notify_home"
+ target:
turn_off:
+ name: Turn off
description: Disable an automation.
+ target:
fields:
- entity_id:
- description: Name of the automation to turn off.
- example: "automation.notify_home"
stop_actions:
- description: Stop currently running actions (defaults to true).
- example: false
+ name: Stop actions
+ description: Stop currently running actions.
+ default: true
+ example: true
+ selector:
+ boolean:
toggle:
- description: Toggle an automation.
- fields:
- entity_id:
- description: Name of the automation to toggle on/off.
- example: "automation.notify_home"
+ name: Toggle
+ description: Toggle (enable / disable) an automation.
+ target:
trigger:
- description: Trigger the action of an automation.
+ name: Trigger
+ description: Trigger the actions of an automation.
+ target:
fields:
- entity_id:
- description: Name of the automation to trigger.
- example: "automation.notify_home"
skip_condition:
- description: Whether or not the condition will be skipped (defaults to true).
+ name: Skip conditions
+ description: Whether or not the conditions will be skipped.
+ default: true
example: true
+ selector:
+ boolean:
reload:
+ name: Reload
description: Reload the automation configuration.
diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py
new file mode 100644
index 00000000000000..cfdbe02056b927
--- /dev/null
+++ b/homeassistant/components/automation/trace.py
@@ -0,0 +1,54 @@
+"""Trace support for automation."""
+from __future__ import annotations
+
+from contextlib import contextmanager
+from typing import Any
+
+from homeassistant.components.trace import ActionTrace, async_store_trace
+from homeassistant.core import Context
+
+# mypy: allow-untyped-calls, allow-untyped-defs
+# mypy: no-check-untyped-defs, no-warn-return-any
+
+
+class AutomationTrace(ActionTrace):
+ """Container for automation trace."""
+
+ def __init__(
+ self,
+ item_id: str,
+ config: dict[str, Any],
+ blueprint_inputs: dict[str, Any],
+ context: Context,
+ ):
+ """Container for automation trace."""
+ key = ("automation", item_id)
+ super().__init__(key, config, blueprint_inputs, context)
+ self._trigger_description: str | None = None
+
+ def set_trigger_description(self, trigger: str) -> None:
+ """Set trigger description."""
+ self._trigger_description = trigger
+
+ def as_short_dict(self) -> dict[str, Any]:
+ """Return a brief dictionary version of this AutomationTrace."""
+ result = super().as_short_dict()
+ result["trigger"] = self._trigger_description
+ return result
+
+
+@contextmanager
+def trace_automation(hass, automation_id, config, blueprint_inputs, context):
+ """Trace action execution of automation with automation_id."""
+ trace = AutomationTrace(automation_id, config, blueprint_inputs, context)
+ async_store_trace(hass, trace)
+
+ try:
+ yield trace
+ except Exception as ex:
+ if automation_id:
+ trace.set_error(ex)
+ raise ex
+ finally:
+ if automation_id:
+ trace.finished()
diff --git a/homeassistant/components/automation/translations/id.json b/homeassistant/components/automation/translations/id.json
index eabfe0b64aac4b..58e8497c8b9c0f 100644
--- a/homeassistant/components/automation/translations/id.json
+++ b/homeassistant/components/automation/translations/id.json
@@ -1,8 +1,8 @@
{
"state": {
"_": {
- "off": "Off",
- "on": "On"
+ "off": "Mati",
+ "on": "Nyala"
}
},
"title": "Otomasi"
diff --git a/homeassistant/components/avion/light.py b/homeassistant/components/avion/light.py
index 0af2f15b34e78e..0d242b952dd3f9 100644
--- a/homeassistant/components/avion/light.py
+++ b/homeassistant/components/avion/light.py
@@ -41,7 +41,6 @@
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up an Avion switch."""
- # pylint: disable=no-member
avion = importlib.import_module("avion")
lights = []
@@ -111,7 +110,6 @@ def assumed_state(self):
def set_state(self, brightness):
"""Set the state of this lamp to the provided brightness."""
- # pylint: disable=no-member
avion = importlib.import_module("avion")
# Bluetooth LE is unreliable, and the connection may drop at any
diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py
index 56af5d2b66293e..bfb95fd91fccba 100644
--- a/homeassistant/components/awair/__init__.py
+++ b/homeassistant/components/awair/__init__.py
@@ -1,15 +1,15 @@
"""The awair component."""
+from __future__ import annotations
from asyncio import gather
-from typing import Any, Optional
+from typing import Any
from async_timeout import timeout
from python_awair import Awair
from python_awair.exceptions import AuthError
+from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.const import CONF_ACCESS_TOKEN
-from homeassistant.core import Config, HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -18,20 +18,12 @@
PLATFORMS = ["sensor"]
-async def async_setup(hass: HomeAssistant, config: Config) -> bool:
- """Set up Awair integration."""
- return True
-
-
async def async_setup_entry(hass, config_entry) -> bool:
"""Set up Awair integration from a config entry."""
session = async_get_clientsession(hass)
coordinator = AwairDataUpdateCoordinator(hass, config_entry, session)
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = coordinator
@@ -70,7 +62,7 @@ def __init__(self, hass, config_entry, session) -> None:
super().__init__(hass, LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL)
- async def _async_update_data(self) -> Optional[Any]:
+ async def _async_update_data(self) -> Any | None:
"""Update data via Awair client library."""
with timeout(API_TIMEOUT):
try:
@@ -83,7 +75,7 @@ async def _async_update_data(self) -> Optional[Any]:
return {result.device.uuid: result for result in results}
except AuthError as err:
flow_context = {
- "source": "reauth",
+ "source": SOURCE_REAUTH,
"unique_id": self._config_entry.unique_id,
}
diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py
index a4768014f96a52..76c7cbca3a9b70 100644
--- a/homeassistant/components/awair/config_flow.py
+++ b/homeassistant/components/awair/config_flow.py
@@ -1,6 +1,5 @@
"""Config flow for Awair."""
-
-from typing import Optional
+from __future__ import annotations
from python_awair import Awair
from python_awair.exceptions import AuthError, AwairError
@@ -10,7 +9,7 @@
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from .const import DOMAIN, LOGGER # pylint: disable=unused-import
+from .const import DOMAIN, LOGGER
class AwairFlowHandler(ConfigFlow, domain=DOMAIN):
@@ -36,7 +35,7 @@ async def async_step_import(self, conf: dict):
data={CONF_ACCESS_TOKEN: conf[CONF_ACCESS_TOKEN]},
)
- async def async_step_user(self, user_input: Optional[dict] = None):
+ async def async_step_user(self, user_input: dict | None = None):
"""Handle a flow initialized by the user."""
errors = {}
@@ -61,7 +60,7 @@ async def async_step_user(self, user_input: Optional[dict] = None):
errors=errors,
)
- async def async_step_reauth(self, user_input: Optional[dict] = None):
+ async def async_step_reauth(self, user_input: dict | None = None):
"""Handle re-auth if token invalid."""
errors = {}
diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py
index b262fdec572086..2853ef9dd6c81e 100644
--- a/homeassistant/components/awair/const.py
+++ b/homeassistant/components/awair/const.py
@@ -8,9 +8,11 @@
from homeassistant.const import (
ATTR_DEVICE_CLASS,
+ ATTR_ICON,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
+ DEVICE_CLASS_CO2,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
@@ -33,7 +35,6 @@
ATTRIBUTION = "Awair air quality sensor"
-ATTR_ICON = "icon"
ATTR_LABEL = "label"
ATTR_UNIT = "unit"
ATTR_UNIQUE_ID = "unique_id"
@@ -104,7 +105,7 @@
ATTR_UNIQUE_ID: "PM10", # matches legacy format
},
API_CO2: {
- ATTR_DEVICE_CLASS: None,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_CO2,
ATTR_ICON: "mdi:cloud",
ATTR_UNIT: CONCENTRATION_PARTS_PER_MILLION,
ATTR_LABEL: "Carbon dioxide",
diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py
index 421fa3d8a26e6e..502fa3dc62605c 100644
--- a/homeassistant/components/awair/sensor.py
+++ b/homeassistant/components/awair/sensor.py
@@ -1,12 +1,13 @@
"""Support for Awair sensors."""
+from __future__ import annotations
-from typing import Callable, List, Optional
+from typing import Callable
from python_awair.devices import AwairDevice
import voluptuous as vol
from homeassistant.components.awair import AwairDataUpdateCoordinator, AwairResult
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, CONF_ACCESS_TOKEN
from homeassistant.helpers import device_registry as dr
@@ -41,7 +42,7 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Import Awair configuration from YAML."""
LOGGER.warning(
- "Loading Awair via platform setup is deprecated. Please remove it from your configuration."
+ "Loading Awair via platform setup is deprecated; Please remove it from your configuration"
)
hass.async_create_task(
hass.config_entries.flow.async_init(
@@ -55,13 +56,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(
hass: HomeAssistantType,
config_entry: ConfigType,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
):
"""Set up Awair sensor entity based on a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
sensors = []
- data: List[AwairResult] = coordinator.data.values()
+ data: list[AwairResult] = coordinator.data.values()
for result in data:
if result.air_data:
sensors.append(AwairSensor(API_SCORE, result.device, coordinator))
@@ -83,7 +84,7 @@ async def async_setup_entry(
async_add_entities(sensors)
-class AwairSensor(CoordinatorEntity):
+class AwairSensor(CoordinatorEntity, SensorEntity):
"""Defines an Awair sensor entity."""
def __init__(
@@ -179,7 +180,7 @@ def unit_of_measurement(self) -> str:
return SENSOR_TYPES[self._kind][ATTR_UNIT]
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return the Awair Index alongside state attributes.
The Awair Index is a subjective score ranging from 0-4 (inclusive) that
@@ -228,9 +229,9 @@ def device_info(self) -> dict:
return info
@property
- def _air_data(self) -> Optional[AwairResult]:
+ def _air_data(self) -> AwairResult | None:
"""Return the latest data for our device, or None."""
- result: Optional[AwairResult] = self.coordinator.data.get(self._device.uuid)
+ result: AwairResult | None = self.coordinator.data.get(self._device.uuid)
if result:
return result.air_data
diff --git a/homeassistant/components/awair/translations/de.json b/homeassistant/components/awair/translations/de.json
index fcdcd0190e3ba2..1dacaf099dc763 100644
--- a/homeassistant/components/awair/translations/de.json
+++ b/homeassistant/components/awair/translations/de.json
@@ -1,19 +1,28 @@
{
"config": {
+ "abort": {
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
+ },
"error": {
- "unknown": "Unbekannter Awair-API-Fehler."
+ "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token",
+ "unknown": "Unerwarteter Fehler"
},
"step": {
"reauth": {
"data": {
+ "access_token": "Zugangstoken",
"email": "E-Mail"
},
- "description": "Bitte geben Sie Ihr Awair-Entwicklerzugriffstoken erneut ein."
+ "description": "Bitte gib dein Awair-Entwicklerzugriffstoken erneut ein."
},
"user": {
"data": {
+ "access_token": "Zugangstoken",
"email": "E-Mail"
- }
+ },
+ "description": "Du musst dich f\u00fcr ein Awair Entwickler-Zugangs-Token registrieren unter: https://developer.getawair.com/onboard/login"
}
}
}
diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json
index 436e8b1fb7dd75..53827adf344e78 100644
--- a/homeassistant/components/awair/translations/hu.json
+++ b/homeassistant/components/awair/translations/hu.json
@@ -1,7 +1,28 @@
{
"config": {
"abort": {
- "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
+ },
+ "error": {
+ "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "access_token": "Hozz\u00e1f\u00e9r\u00e9si token",
+ "email": "E-mail"
+ },
+ "description": "Add meg \u00fajra az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokent."
+ },
+ "user": {
+ "data": {
+ "access_token": "Hozz\u00e1f\u00e9r\u00e9si token",
+ "email": "E-mail"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/awair/translations/id.json b/homeassistant/components/awair/translations/id.json
new file mode 100644
index 00000000000000..2c6fab90909932
--- /dev/null
+++ b/homeassistant/components/awair/translations/id.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi",
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan",
+ "reauth_successful": "Autentikasi ulang berhasil"
+ },
+ "error": {
+ "invalid_access_token": "Token akses tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "access_token": "Token Akses",
+ "email": "Email"
+ },
+ "description": "Masukkan kembali token akses pengembang Awair Anda."
+ },
+ "user": {
+ "data": {
+ "access_token": "Token Akses",
+ "email": "Email"
+ },
+ "description": "Anda harus mendaftar untuk mendapatkan token akses pengembang Awair di: https://developer.getawair.com/onboard/login"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/awair/translations/it.json b/homeassistant/components/awair/translations/it.json
index 085796f9263ec1..cad2b8555a8f04 100644
--- a/homeassistant/components/awair/translations/it.json
+++ b/homeassistant/components/awair/translations/it.json
@@ -3,7 +3,7 @@
"abort": {
"already_configured": "L'account \u00e8 gi\u00e0 configurato",
"no_devices_found": "Nessun dispositivo trovato sulla rete",
- "reauth_successful": "La riautenticazione ha avuto successo"
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
},
"error": {
"invalid_access_token": "Token di accesso non valido",
diff --git a/homeassistant/components/awair/translations/ko.json b/homeassistant/components/awair/translations/ko.json
index 977532de45defa..22677f8ab45fbe 100644
--- a/homeassistant/components/awair/translations/ko.json
+++ b/homeassistant/components/awair/translations/ko.json
@@ -1,11 +1,13 @@
{
"config": {
"abort": {
- "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "reauth_successful": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc131\uacf5\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
- "unknown": "\uc54c \uc218 \uc5c6\ub294 Awair API \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4."
+ "invalid_access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"reauth": {
diff --git a/homeassistant/components/awair/translations/nl.json b/homeassistant/components/awair/translations/nl.json
index 08a30a52250ac7..d41b85cc09b1fb 100644
--- a/homeassistant/components/awair/translations/nl.json
+++ b/homeassistant/components/awair/translations/nl.json
@@ -2,14 +2,27 @@
"config": {
"abort": {
"already_configured": "Account is al geconfigureerd",
- "no_devices_found": "Geen apparaten op het netwerk gevonden"
+ "no_devices_found": "Geen apparaten op het netwerk gevonden",
+ "reauth_successful": "Herauthenticatie was succesvol"
},
"error": {
+ "invalid_access_token": "Ongeldig toegangstoken",
"unknown": "Onverwachte fout"
},
"step": {
"reauth": {
+ "data": {
+ "access_token": "Toegangstoken",
+ "email": "E-mail"
+ },
"description": "Voer uw Awair-ontwikkelaarstoegangstoken opnieuw in."
+ },
+ "user": {
+ "data": {
+ "access_token": "Toegangstoken",
+ "email": "E-mail"
+ },
+ "description": "U moet zich registreren voor een Awair-toegangstoken voor ontwikkelaars op: https://developer.getawair.com/onboard/login"
}
}
}
diff --git a/homeassistant/components/awair/translations/tr.json b/homeassistant/components/awair/translations/tr.json
new file mode 100644
index 00000000000000..84da92b97d39f3
--- /dev/null
+++ b/homeassistant/components/awair/translations/tr.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu"
+ },
+ "error": {
+ "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "access_token": "Eri\u015fim Belirteci",
+ "email": "E-posta"
+ }
+ },
+ "user": {
+ "data": {
+ "access_token": "Eri\u015fim Belirteci",
+ "email": "E-posta"
+ },
+ "description": "Awair geli\u015ftirici eri\u015fim belirteci i\u00e7in \u015fu adresten kaydolmal\u0131s\u0131n\u0131z: https://developer.getawair.com/onboard/login"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/awair/translations/uk.json b/homeassistant/components/awair/translations/uk.json
new file mode 100644
index 00000000000000..f8150ad7faf53f
--- /dev/null
+++ b/homeassistant/components/awair/translations/uk.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e"
+ },
+ "error": {
+ "invalid_access_token": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443",
+ "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438"
+ },
+ "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443."
+ },
+ "user": {
+ "data": {
+ "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443",
+ "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438"
+ },
+ "description": "\u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0434\u043e Awair \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e: https://developer.getawair.com/onboard/login"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/awair/translations/zh-Hant.json b/homeassistant/components/awair/translations/zh-Hant.json
index 11fe9ff88b39aa..0bd7749c65fb20 100644
--- a/homeassistant/components/awair/translations/zh-Hant.json
+++ b/homeassistant/components/awair/translations/zh-Hant.json
@@ -6,23 +6,23 @@
"reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f"
},
"error": {
- "invalid_access_token": "\u5b58\u53d6\u5bc6\u9470\u7121\u6548",
+ "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"step": {
"reauth": {
"data": {
- "access_token": "\u5b58\u53d6\u5bc6\u9470",
+ "access_token": "\u5b58\u53d6\u6b0a\u6756",
"email": "\u96fb\u5b50\u90f5\u4ef6"
},
- "description": "\u8acb\u91cd\u65b0\u8f38\u5165 Awair \u958b\u767c\u8005\u5b58\u53d6\u5bc6\u9470\u3002"
+ "description": "\u8acb\u91cd\u65b0\u8f38\u5165 Awair \u958b\u767c\u8005\u5b58\u53d6\u6b0a\u6756\u3002"
},
"user": {
"data": {
- "access_token": "\u5b58\u53d6\u5bc6\u9470",
+ "access_token": "\u5b58\u53d6\u6b0a\u6756",
"email": "\u96fb\u5b50\u90f5\u4ef6"
},
- "description": "\u5fc5\u9808\u5148\u8a3b\u518a Awair \u958b\u767c\u8005\u5b58\u53d6\u5bc6\u9470\uff1ahttps://developer.getawair.com/onboard/login"
+ "description": "\u5fc5\u9808\u5148\u8a3b\u518a Awair \u958b\u767c\u8005\u5b58\u53d6\u6b0a\u6756\uff1ahttps://developer.getawair.com/onboard/login"
}
}
}
diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py
index 600874b0d25d34..da8c27d74455d2 100644
--- a/homeassistant/components/aws/__init__.py
+++ b/homeassistant/components/aws/__init__.py
@@ -7,11 +7,15 @@
import voluptuous as vol
from homeassistant import config_entries
-from homeassistant.const import ATTR_CREDENTIALS, CONF_NAME, CONF_PROFILE_NAME
+from homeassistant.const import (
+ ATTR_CREDENTIALS,
+ CONF_NAME,
+ CONF_PROFILE_NAME,
+ CONF_SERVICE,
+)
from homeassistant.helpers import config_validation as cv, discovery
# Loading the config flow file will register the flow
-from . import config_flow # noqa: F401
from .const import (
CONF_ACCESS_KEY_ID,
CONF_CONTEXT,
@@ -20,7 +24,6 @@
CONF_NOTIFY,
CONF_REGION,
CONF_SECRET_ACCESS_KEY,
- CONF_SERVICE,
CONF_VALIDATE,
DATA_CONFIG,
DATA_HASS_CONFIG,
@@ -152,7 +155,6 @@ async def async_setup_entry(hass, entry):
async def _validate_aws_credentials(hass, credential):
"""Validate AWS credential config."""
-
aws_config = credential.copy()
del aws_config[CONF_NAME]
del aws_config[CONF_VALIDATE]
diff --git a/homeassistant/components/aws/const.py b/homeassistant/components/aws/const.py
index 499f4413596d39..8be6afec7ff65e 100644
--- a/homeassistant/components/aws/const.py
+++ b/homeassistant/components/aws/const.py
@@ -10,8 +10,6 @@
CONF_CREDENTIAL_NAME = "credential_name"
CONF_CREDENTIALS = "credentials"
CONF_NOTIFY = "notify"
-CONF_PROFILE_NAME = "profile_name"
CONF_REGION = "region_name"
CONF_SECRET_ACCESS_KEY = "aws_secret_access_key"
-CONF_SERVICE = "service"
CONF_VALIDATE = "validate"
diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py
index 13fa189a318235..f487bc7aab3f32 100644
--- a/homeassistant/components/aws/notify.py
+++ b/homeassistant/components/aws/notify.py
@@ -12,24 +12,21 @@
ATTR_TITLE_DEFAULT,
BaseNotificationService,
)
-from homeassistant.const import CONF_NAME, CONF_PLATFORM
-from homeassistant.helpers.json import JSONEncoder
-
-from .const import (
- CONF_CONTEXT,
- CONF_CREDENTIAL_NAME,
+from homeassistant.const import (
+ CONF_NAME,
+ CONF_PLATFORM,
CONF_PROFILE_NAME,
- CONF_REGION,
CONF_SERVICE,
- DATA_SESSIONS,
)
+from homeassistant.helpers.json import JSONEncoder
+
+from .const import CONF_CONTEXT, CONF_CREDENTIAL_NAME, CONF_REGION, DATA_SESSIONS
_LOGGER = logging.getLogger(__name__)
async def get_available_regions(hass, service):
"""Get available regions for a service."""
-
session = aiobotocore.get_session()
# get_available_regions is not a coroutine since it does not perform
# network I/O. But it still perform file I/O heavily, so put it into
diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py
index c467359c17e6ff..acbdc2ca782544 100644
--- a/homeassistant/components/axis/__init__.py
+++ b/homeassistant/components/axis/__init__.py
@@ -13,11 +13,6 @@
_LOGGER = logging.getLogger(__name__)
-async def async_setup(hass, config):
- """Old way to set up Axis devices."""
- return True
-
-
async def async_setup_entry(hass, config_entry):
"""Set up the Axis component."""
hass.data.setdefault(AXIS_DOMAIN, {})
@@ -31,7 +26,9 @@ async def async_setup_entry(hass, config_entry):
await device.async_update_device_registry()
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown)
+ device.listeners.append(
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown)
+ )
return True
@@ -48,8 +45,11 @@ async def async_migrate_entry(hass, config_entry):
# Flatten configuration but keep old data if user rollbacks HASS prior to 0.106
if config_entry.version == 1:
- config_entry.data = {**config_entry.data, **config_entry.data[CONF_DEVICE]}
- config_entry.unique_id = config_entry.data[CONF_MAC]
+ unique_id = config_entry.data[CONF_MAC]
+ data = {**config_entry.data, **config_entry.data[CONF_DEVICE]}
+ hass.config_entries.async_update_entry(
+ config_entry, unique_id=unique_id, data=data
+ )
config_entry.version = 2
# Normalise MAC address of device which also affects entity unique IDs
@@ -66,10 +66,12 @@ def update_unique_id(entity_entry):
)
}
- await async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
+ if old_unique_id != new_unique_id:
+ await async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
- config_entry.unique_id = new_unique_id
- config_entry.version = 3
+ hass.config_entries.async_update_entry(
+ config_entry, unique_id=new_unique_id
+ )
_LOGGER.info("Migration to version %s successful", config_entry.version)
diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py
index d99c5329e3230b..c65f663f2b9915 100644
--- a/homeassistant/components/axis/config_flow.py
+++ b/homeassistant/components/axis/config_flow.py
@@ -138,7 +138,6 @@ async def _create_entry(self):
async def async_step_reauth(self, device_config: dict):
"""Trigger a reauthentication flow."""
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {
CONF_NAME: device_config[CONF_NAME],
CONF_HOST: device_config[CONF_HOST],
@@ -204,7 +203,6 @@ async def _process_discovered_device(self, device: dict):
}
)
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {
CONF_NAME: device[CONF_NAME],
CONF_HOST: device[CONF_HOST],
diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py
index bd7b5e442ad498..93b63b64122410 100644
--- a/homeassistant/components/axis/device.py
+++ b/homeassistant/components/axis/device.py
@@ -263,9 +263,7 @@ async def start_platforms():
def disconnect_from_stream(self):
"""Stop stream."""
if self.api.stream.state != STATE_STOPPED:
- self.api.stream.connection_status_callback.remove(
- self.async_connection_status_callback
- )
+ self.api.stream.connection_status_callback.clear()
self.api.stream.stop()
async def shutdown(self, event):
@@ -304,13 +302,13 @@ async def get_device(hass, host, port, username, password):
)
try:
- with async_timeout.timeout(15):
+ with async_timeout.timeout(30):
await device.vapix.initialize()
return device
except axis.Unauthorized as err:
- LOGGER.warning("Connected to device at %s but not registered.", host)
+ LOGGER.warning("Connected to device at %s but not registered", host)
raise AuthenticationRequired from err
except (asyncio.TimeoutError, axis.RequestError) as err:
diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json
index a78d916da9e9bc..b709ac35da2b3d 100644
--- a/homeassistant/components/axis/manifest.json
+++ b/homeassistant/components/axis/manifest.json
@@ -3,7 +3,7 @@
"name": "Axis",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/axis",
- "requirements": ["axis==43"],
+ "requirements": ["axis==44"],
"dhcp": [
{ "hostname": "axis-00408c*", "macaddress": "00408C*" },
{ "hostname": "axis-accc8e*", "macaddress": "ACCC8E*" },
diff --git a/homeassistant/components/axis/translations/ca.json b/homeassistant/components/axis/translations/ca.json
index 26da6057dc1c41..3e104c1005e101 100644
--- a/homeassistant/components/axis/translations/ca.json
+++ b/homeassistant/components/axis/translations/ca.json
@@ -11,7 +11,7 @@
"cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida"
},
- "flow_title": "Dispositiu d'eix: {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/translations/cs.json b/homeassistant/components/axis/translations/cs.json
index fd99c68ab351ab..4f7c30162354f9 100644
--- a/homeassistant/components/axis/translations/cs.json
+++ b/homeassistant/components/axis/translations/cs.json
@@ -11,7 +11,7 @@
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
"invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed"
},
- "flow_title": "Za\u0159\u00edzen\u00ed Axis: {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/translations/de.json b/homeassistant/components/axis/translations/de.json
index 4706350cdb3edb..ed95dea6fc12be 100644
--- a/homeassistant/components/axis/translations/de.json
+++ b/homeassistant/components/axis/translations/de.json
@@ -7,8 +7,9 @@
},
"error": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
- "already_in_progress": "Der Konfigurationsablauf f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.",
- "cannot_connect": "Verbindungsfehler"
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
},
"flow_title": "Achsenger\u00e4t: {name} ({host})",
"step": {
@@ -22,5 +23,15 @@
"title": "Axis Ger\u00e4t einrichten"
}
}
+ },
+ "options": {
+ "step": {
+ "configure_stream": {
+ "data": {
+ "stream_profile": "Zu verwendendes Stream-Profil ausw\u00e4hlen"
+ },
+ "title": "Optionen des Axis Videostream-Ger\u00e4ts"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/axis/translations/en.json b/homeassistant/components/axis/translations/en.json
index 6b01533aefa856..f71e91f6280784 100644
--- a/homeassistant/components/axis/translations/en.json
+++ b/homeassistant/components/axis/translations/en.json
@@ -11,7 +11,7 @@
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication"
},
- "flow_title": "Axis device: {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/translations/et.json b/homeassistant/components/axis/translations/et.json
index 6a27e74b28705a..f6f9a523cb6fba 100644
--- a/homeassistant/components/axis/translations/et.json
+++ b/homeassistant/components/axis/translations/et.json
@@ -11,7 +11,7 @@
"cannot_connect": "\u00dchendamine nurjus",
"invalid_auth": "Tuvastamise viga"
},
- "flow_title": "Axise seade: {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/translations/he.json b/homeassistant/components/axis/translations/he.json
new file mode 100644
index 00000000000000..3007c0e968c1dc
--- /dev/null
+++ b/homeassistant/components/axis/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/axis/translations/hu.json b/homeassistant/components/axis/translations/hu.json
index 659c50e49e7988..972690ede9724a 100644
--- a/homeassistant/components/axis/translations/hu.json
+++ b/homeassistant/components/axis/translations/hu.json
@@ -1,8 +1,13 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
- "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk",
- "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
},
"flow_title": "Axis eszk\u00f6z: {name} ({host})",
"step": {
diff --git a/homeassistant/components/axis/translations/id.json b/homeassistant/components/axis/translations/id.json
new file mode 100644
index 00000000000000..cdd498a8e6cd3b
--- /dev/null
+++ b/homeassistant/components/axis/translations/id.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "link_local_address": "Tautan alamat lokal tidak didukung",
+ "not_axis_device": "Perangkat yang ditemukan bukan perangkat Axis"
+ },
+ "error": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "flow_title": "{name} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "username": "Nama Pengguna"
+ },
+ "title": "Siapkan perangkat Axis"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "configure_stream": {
+ "data": {
+ "stream_profile": "Pilih profil streaming yang akan digunakan"
+ },
+ "title": "Opsi streaming video perangkat Axis"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/axis/translations/it.json b/homeassistant/components/axis/translations/it.json
index 6461b2a6619dc8..7e7aeb1d1b2612 100644
--- a/homeassistant/components/axis/translations/it.json
+++ b/homeassistant/components/axis/translations/it.json
@@ -11,7 +11,7 @@
"cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida"
},
- "flow_title": "Dispositivo Axis: {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/translations/ko.json b/homeassistant/components/axis/translations/ko.json
index f73d467fbf774f..d9e0114a97a9aa 100644
--- a/homeassistant/components/axis/translations/ko.json
+++ b/homeassistant/components/axis/translations/ko.json
@@ -7,9 +7,11 @@
},
"error": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4."
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
- "flow_title": "Axis \uae30\uae30: {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/translations/nl.json b/homeassistant/components/axis/translations/nl.json
index 483acefec15e66..3b41c1184ba915 100644
--- a/homeassistant/components/axis/translations/nl.json
+++ b/homeassistant/components/axis/translations/nl.json
@@ -7,10 +7,11 @@
},
"error": {
"already_configured": "Apparaat is al geconfigureerd",
- "already_in_progress": "De configuratiestroom voor het apparaat is al in volle gang.",
- "cannot_connect": "Kan geen verbinding maken"
+ "already_in_progress": "De configuratiestroom is al aan de gang",
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie"
},
- "flow_title": "Axis apparaat: {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
@@ -22,5 +23,15 @@
"title": "Stel het Axis-apparaat in"
}
}
+ },
+ "options": {
+ "step": {
+ "configure_stream": {
+ "data": {
+ "stream_profile": "Selecteer stream profiel om te gebruiken"
+ },
+ "title": "Opties voor videostreams van Axis-apparaten"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/axis/translations/no.json b/homeassistant/components/axis/translations/no.json
index 984d522eba93b8..1fc0640eb9b667 100644
--- a/homeassistant/components/axis/translations/no.json
+++ b/homeassistant/components/axis/translations/no.json
@@ -11,7 +11,7 @@
"cannot_connect": "Tilkobling mislyktes",
"invalid_auth": "Ugyldig godkjenning"
},
- "flow_title": "Axis enhet: {name} ({host})",
+ "flow_title": "{name} ( {host} )",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/translations/pl.json b/homeassistant/components/axis/translations/pl.json
index 84af845ab31dc3..e44816bc2ea93a 100644
--- a/homeassistant/components/axis/translations/pl.json
+++ b/homeassistant/components/axis/translations/pl.json
@@ -11,7 +11,7 @@
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"invalid_auth": "Niepoprawne uwierzytelnienie"
},
- "flow_title": "Urz\u0105dzenie Axis: {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/axis/translations/ru.json b/homeassistant/components/axis/translations/ru.json
index ee1dc8494f3b45..f5c5e79a32fe8a 100644
--- a/homeassistant/components/axis/translations/ru.json
+++ b/homeassistant/components/axis/translations/ru.json
@@ -9,16 +9,16 @@
"already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
"already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
- "flow_title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Axis {name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
"host": "\u0425\u043e\u0441\u0442",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "Axis"
}
diff --git a/homeassistant/components/axis/translations/tr.json b/homeassistant/components/axis/translations/tr.json
new file mode 100644
index 00000000000000..b2d609747d1429
--- /dev/null
+++ b/homeassistant/components/axis/translations/tr.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor",
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "password": "Parola",
+ "port": "Port",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/axis/translations/uk.json b/homeassistant/components/axis/translations/uk.json
new file mode 100644
index 00000000000000..35b849ce9689ba
--- /dev/null
+++ b/homeassistant/components/axis/translations/uk.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "link_local_address": "\u041f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0456 \u0430\u0434\u0440\u0435\u0441\u0438 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.",
+ "not_axis_device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0454 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c Axis."
+ },
+ "error": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "flow_title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Axis {name} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "Axis"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "configure_stream": {
+ "data": {
+ "stream_profile": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u043e\u0444\u0456\u043b\u044c \u043f\u043e\u0442\u043e\u043a\u0443 \u0434\u043b\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f"
+ },
+ "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0432\u0456\u0434\u0435\u043e\u043f\u043e\u0442\u043e\u043a\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Axis"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/axis/translations/zh-Hant.json b/homeassistant/components/axis/translations/zh-Hant.json
index 1d7aaa7c74ed5a..293f08c5f05961 100644
--- a/homeassistant/components/axis/translations/zh-Hant.json
+++ b/homeassistant/components/axis/translations/zh-Hant.json
@@ -11,7 +11,7 @@
"cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548"
},
- "flow_title": "Axis \u88dd\u7f6e\uff1a{name} ({host})",
+ "flow_title": "{name} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py
index f72a4c44918d48..3db74679d9abb6 100644
--- a/homeassistant/components/azure_devops/__init__.py
+++ b/homeassistant/components/azure_devops/__init__.py
@@ -1,6 +1,8 @@
"""Support for Azure DevOps."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict
+from typing import Any
from aioazuredevops.client import DevOpsClient
import aiohttp
@@ -12,7 +14,7 @@
DATA_AZURE_DEVOPS_CLIENT,
DOMAIN,
)
-from homeassistant.config_entries import ConfigEntry
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
@@ -20,11 +22,6 @@
_LOGGER = logging.getLogger(__name__)
-async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
- """Set up the Azure DevOps components."""
- return True
-
-
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
"""Set up Azure DevOps from a config entry."""
client = DevOpsClient()
@@ -39,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
- context={"source": "reauth"},
+ context={"source": SOURCE_REAUTH},
data=entry.data,
)
)
@@ -100,7 +97,7 @@ async def async_update(self) -> None:
else:
if self._available:
_LOGGER.debug(
- "An error occurred while updating Azure DevOps sensor.",
+ "An error occurred while updating Azure DevOps sensor",
exc_info=True,
)
self._available = False
@@ -114,7 +111,7 @@ class AzureDevOpsDeviceEntity(AzureDevOpsEntity):
"""Defines a Azure DevOps device entity."""
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about this Azure DevOps instance."""
return {
"identifiers": {
diff --git a/homeassistant/components/azure_devops/config_flow.py b/homeassistant/components/azure_devops/config_flow.py
index e1e7d8339264df..138ea67e788ea8 100644
--- a/homeassistant/components/azure_devops/config_flow.py
+++ b/homeassistant/components/azure_devops/config_flow.py
@@ -4,7 +4,7 @@
import voluptuous as vol
from homeassistant import config_entries
-from homeassistant.components.azure_devops.const import ( # pylint:disable=unused-import
+from homeassistant.components.azure_devops.const import (
CONF_ORG,
CONF_PAT,
CONF_PROJECT,
@@ -95,7 +95,6 @@ async def async_step_reauth(self, user_input):
self._project = user_input[CONF_PROJECT]
self._pat = user_input[CONF_PAT]
- # pylint: disable=no-member
self.context["title_placeholders"] = {
"project_url": f"{self._organization}/{self._project}",
}
diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py
index 6f259afb9a978c..01018d34c78ebc 100644
--- a/homeassistant/components/azure_devops/sensor.py
+++ b/homeassistant/components/azure_devops/sensor.py
@@ -1,7 +1,8 @@
"""Support for Azure DevOps sensors."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import List
from aioazuredevops.builds import DevOpsBuild
from aioazuredevops.client import DevOpsClient
@@ -16,6 +17,7 @@
DATA_PROJECT,
DOMAIN,
)
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.typing import HomeAssistantType
@@ -39,7 +41,7 @@ async def async_setup_entry(
sensors = []
try:
- builds: List[DevOpsBuild] = await client.get_builds(
+ builds: list[DevOpsBuild] = await client.get_builds(
organization, project, BUILDS_QUERY
)
except aiohttp.ClientError as exception:
@@ -54,7 +56,7 @@ async def async_setup_entry(
async_add_entities(sensors, True)
-class AzureDevOpsSensor(AzureDevOpsDeviceEntity):
+class AzureDevOpsSensor(AzureDevOpsDeviceEntity, SensorEntity):
"""Defines a Azure DevOps sensor."""
def __init__(
@@ -92,7 +94,7 @@ def state(self) -> str:
return self._state
@property
- def device_state_attributes(self) -> object:
+ def extra_state_attributes(self) -> object:
"""Return the attributes of the sensor."""
return self._attributes
diff --git a/homeassistant/components/azure_devops/translations/de.json b/homeassistant/components/azure_devops/translations/de.json
index 1c940ea7a359a5..e7d9e073ec617c 100644
--- a/homeassistant/components/azure_devops/translations/de.json
+++ b/homeassistant/components/azure_devops/translations/de.json
@@ -1,7 +1,12 @@
{
"config": {
+ "abort": {
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
+ },
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
},
"step": {
"reauth": {
diff --git a/homeassistant/components/azure_devops/translations/fr.json b/homeassistant/components/azure_devops/translations/fr.json
index edcf3dda517de0..5e62d54ec1d44f 100644
--- a/homeassistant/components/azure_devops/translations/fr.json
+++ b/homeassistant/components/azure_devops/translations/fr.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9",
+ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9",
"reauth_successful": "Jeton d'acc\u00e8s mis \u00e0 jour avec succ\u00e8s"
},
"error": {
diff --git a/homeassistant/components/azure_devops/translations/hu.json b/homeassistant/components/azure_devops/translations/hu.json
index 6bd42409877c9b..460b6132048515 100644
--- a/homeassistant/components/azure_devops/translations/hu.json
+++ b/homeassistant/components/azure_devops/translations/hu.json
@@ -1,11 +1,19 @@
{
"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",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
},
"step": {
"reauth": {
"description": "A(z) {project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait."
+ },
+ "user": {
+ "title": "Azure DevOps Project hozz\u00e1ad\u00e1sa"
}
}
}
diff --git a/homeassistant/components/azure_devops/translations/id.json b/homeassistant/components/azure_devops/translations/id.json
new file mode 100644
index 00000000000000..42292805b0882d
--- /dev/null
+++ b/homeassistant/components/azure_devops/translations/id.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi",
+ "reauth_successful": "Autentikasi ulang berhasil"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "project_error": "Tidak bisa mendapatkan info proyek."
+ },
+ "flow_title": "Azure DevOps: {project_url}",
+ "step": {
+ "reauth": {
+ "data": {
+ "personal_access_token": "Token Akses Pribadi (PAT)"
+ },
+ "description": "Autentikasi gagal untuk {project_url} . Masukkan kredensial Anda saat ini.",
+ "title": "Autentikasi ulang"
+ },
+ "user": {
+ "data": {
+ "organization": "Organisasi",
+ "personal_access_token": "Token Akses Pribadi (PAT)",
+ "project": "Proyek"
+ },
+ "description": "Siapkan instans Azure DevOps untuk mengakses proyek Anda. Token Akses Pribadi hanya diperlukan untuk proyek pribadi.",
+ "title": "Tambahkan Proyek Azure DevOps"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/azure_devops/translations/it.json b/homeassistant/components/azure_devops/translations/it.json
index 849e65b933fa9f..4b2f5e0efae8d8 100644
--- a/homeassistant/components/azure_devops/translations/it.json
+++ b/homeassistant/components/azure_devops/translations/it.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "L'account \u00e8 gi\u00e0 configurato",
- "reauth_successful": "La riautenticazione ha avuto successo"
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
},
"error": {
"cannot_connect": "Impossibile connettersi",
diff --git a/homeassistant/components/azure_devops/translations/ko.json b/homeassistant/components/azure_devops/translations/ko.json
new file mode 100644
index 00000000000000..cdb67cf77dfd61
--- /dev/null
+++ b/homeassistant/components/azure_devops/translations/ko.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "project_error": "\ud504\ub85c\uc81d\ud2b8 \uc815\ubcf4\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
+ },
+ "flow_title": "Azure DevOps: {project_url}",
+ "step": {
+ "reauth": {
+ "data": {
+ "personal_access_token": "\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070 (PAT)"
+ },
+ "description": "{project_url} \uc5d0 \ub300\ud55c \uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \ud604\uc7ac \uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "\uc7ac\uc778\uc99d"
+ },
+ "user": {
+ "data": {
+ "organization": "\uc870\uc9c1",
+ "personal_access_token": "\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070 (PAT)",
+ "project": "\ud504\ub85c\uc81d\ud2b8"
+ },
+ "description": "\ud504\ub85c\uc81d\ud2b8\uc5d0 \uc811\uadfc\ud560 Azure DevOps \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694. \uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070\uc740 \uac1c\uc778 \ud504\ub85c\uc81d\ud2b8\uc5d0\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4.",
+ "title": "Azure DevOps \ud504\ub85c\uc81d\ud2b8 \ucd94\uac00\ud558\uae30"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/azure_devops/translations/nl.json b/homeassistant/components/azure_devops/translations/nl.json
index 9abecd187fef4d..971af5b8d588b7 100644
--- a/homeassistant/components/azure_devops/translations/nl.json
+++ b/homeassistant/components/azure_devops/translations/nl.json
@@ -1,10 +1,32 @@
{
"config": {
"abort": {
- "already_configured": "Account is al geconfigureerd"
+ "already_configured": "Account is al geconfigureerd",
+ "reauth_successful": "Herauthenticatie was succesvol"
},
"error": {
- "invalid_auth": "Ongeldige authenticatie"
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
+ "project_error": "Kon geen projectinformatie ophalen."
+ },
+ "flow_title": "Azure DevOps: {project_url}",
+ "step": {
+ "reauth": {
+ "data": {
+ "personal_access_token": "Persoonlijk toegangstoken (PAT)"
+ },
+ "description": "Authenticatie mislukt voor {project_url}. Voer uw huidige inloggegevens in.",
+ "title": "Herauthenticatie"
+ },
+ "user": {
+ "data": {
+ "organization": "Organisatie",
+ "personal_access_token": "Persoonlijk toegangstoken (PAT)",
+ "project": "Project"
+ },
+ "description": "Stel een Azure DevOps instantie in om toegang te krijgen tot uw project. Een persoonlijke toegangstoken is alleen nodig voor een priv\u00e9project.",
+ "title": "Azure DevOps-project toevoegen"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/azure_devops/translations/ru.json b/homeassistant/components/azure_devops/translations/ru.json
index 84e0fc93b466aa..4e59af2dd11a73 100644
--- a/homeassistant/components/azure_devops/translations/ru.json
+++ b/homeassistant/components/azure_devops/translations/ru.json
@@ -6,7 +6,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"project_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0435."
},
"flow_title": "Azure DevOps: {project_url}",
diff --git a/homeassistant/components/azure_devops/translations/tr.json b/homeassistant/components/azure_devops/translations/tr.json
new file mode 100644
index 00000000000000..11a15956f635b3
--- /dev/null
+++ b/homeassistant/components/azure_devops/translations/tr.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "project_error": "Proje bilgileri al\u0131namad\u0131."
+ },
+ "flow_title": "Azure DevOps: {project_url}",
+ "step": {
+ "reauth": {
+ "title": "Yeniden kimlik do\u011frulama"
+ },
+ "user": {
+ "data": {
+ "organization": "Organizasyon",
+ "personal_access_token": "Ki\u015fisel Eri\u015fim Belirteci (PAT)",
+ "project": "Proje"
+ },
+ "description": "Projenize eri\u015fmek i\u00e7in bir Azure DevOps \u00f6rne\u011fi ayarlay\u0131n. Ki\u015fisel Eri\u015fim Jetonu yaln\u0131zca \u00f6zel bir proje i\u00e7in gereklidir.",
+ "title": "Azure DevOps Projesi Ekle"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/azure_devops/translations/uk.json b/homeassistant/components/azure_devops/translations/uk.json
index 4a42fd17fc3933..848528f444e1c0 100644
--- a/homeassistant/components/azure_devops/translations/uk.json
+++ b/homeassistant/components/azure_devops/translations/uk.json
@@ -1,16 +1,30 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "project_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u043f\u0440\u043e\u0435\u043a\u0442."
+ },
"flow_title": "Azure DevOps: {project_url}",
"step": {
"reauth": {
+ "data": {
+ "personal_access_token": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 (PAT)"
+ },
+ "description": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 {project_url} . \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.",
"title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f"
},
"user": {
"data": {
"organization": "\u041e\u0440\u0433\u0430\u043d\u0456\u0437\u0430\u0446\u0456\u044f",
"personal_access_token": "\u0422\u043e\u043a\u0435\u043d \u043e\u0441\u043e\u0431\u0438\u0441\u0442\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0443 (PAT)",
- "project": "\u041f\u0440\u043e\u0454\u043a\u0442"
+ "project": "\u041f\u0440\u043e\u0435\u043a\u0442"
},
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u043c Azure DevOps. \u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0432\u0432\u043e\u0434\u0438\u0442\u0438 \u043b\u0438\u0448\u0435 \u0434\u043b\u044f \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u0438\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u0456\u0432.",
"title": "\u0414\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u043e\u0435\u043a\u0442 Azure DevOps"
}
}
diff --git a/homeassistant/components/azure_event_hub/__init__.py b/homeassistant/components/azure_event_hub/__init__.py
index 3b44c6423be779..0473c4ff5a7559 100644
--- a/homeassistant/components/azure_event_hub/__init__.py
+++ b/homeassistant/components/azure_event_hub/__init__.py
@@ -1,9 +1,11 @@
"""Support for Azure Event Hubs."""
+from __future__ import annotations
+
import asyncio
import json
import logging
import time
-from typing import Any, Dict
+from typing import Any
from azure.eventhub import EventData
from azure.eventhub.aio import EventHubProducerClient, EventHubSharedKeyCredential
@@ -95,7 +97,7 @@ class AzureEventHub:
def __init__(
self,
hass: HomeAssistant,
- client_args: Dict[str, Any],
+ client_args: dict[str, Any],
conn_str_client: bool,
entities_filter: vol.Schema,
send_interval: int,
diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py
index 4768b3f4fe6261..6879e278bab03f 100644
--- a/homeassistant/components/bayesian/binary_sensor.py
+++ b/homeassistant/components/bayesian/binary_sensor.py
@@ -17,7 +17,7 @@
STATE_UNKNOWN,
)
from homeassistant.core import callback
-from homeassistant.exceptions import TemplateError
+from homeassistant.exceptions import ConditionError, TemplateError
from homeassistant.helpers import condition
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import (
@@ -340,20 +340,28 @@ def _process_numeric_state(self, entity_observation):
"""Return True if numeric condition is met."""
entity = entity_observation["entity_id"]
- return condition.async_numeric_state(
- self.hass,
- entity,
- entity_observation.get("below"),
- entity_observation.get("above"),
- None,
- entity_observation,
- )
+ try:
+ return condition.async_numeric_state(
+ self.hass,
+ entity,
+ entity_observation.get("below"),
+ entity_observation.get("above"),
+ None,
+ entity_observation,
+ )
+ except ConditionError:
+ return False
def _process_state(self, entity_observation):
"""Return True if state conditions are met."""
entity = entity_observation["entity_id"]
- return condition.state(self.hass, entity, entity_observation.get("to_state"))
+ try:
+ return condition.state(
+ self.hass, entity, entity_observation.get("to_state")
+ )
+ except ConditionError:
+ return False
@property
def name(self):
@@ -376,7 +384,7 @@ def device_class(self):
return self._device_class
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
attr_observations_list = [
diff --git a/homeassistant/components/bayesian/services.yaml b/homeassistant/components/bayesian/services.yaml
index ec7313a86301d3..2fe3a4f7c9b260 100644
--- a/homeassistant/components/bayesian/services.yaml
+++ b/homeassistant/components/bayesian/services.yaml
@@ -1,2 +1,2 @@
reload:
- description: Reload all bayesian entities.
+ description: Reload all bayesian entities
diff --git a/homeassistant/components/bbb_gpio/__init__.py b/homeassistant/components/bbb_gpio/__init__.py
index 61d98c7413ea2c..f7d146e073edc9 100644
--- a/homeassistant/components/bbb_gpio/__init__.py
+++ b/homeassistant/components/bbb_gpio/__init__.py
@@ -8,7 +8,6 @@
def setup(hass, config):
"""Set up the BeagleBone Black GPIO component."""
- # pylint: disable=import-error
def cleanup_gpio(event):
"""Stuff to do before stopping."""
diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py
index 8097c11eb89b92..9dac635dd2f748 100644
--- a/homeassistant/components/bbox/device_tracker.py
+++ b/homeassistant/components/bbox/device_tracker.py
@@ -1,8 +1,9 @@
"""Support for French FAI Bouygues Bbox routers."""
+from __future__ import annotations
+
from collections import namedtuple
from datetime import timedelta
import logging
-from typing import List
import pybbox
import voluptuous as vol
@@ -47,7 +48,7 @@ def __init__(self, config):
self.host = config[CONF_HOST]
"""Initialize the scanner."""
- self.last_results: List[Device] = []
+ self.last_results: list[Device] = []
self.success_init = self._update_info()
_LOGGER.info("Scanner initialized")
@@ -74,7 +75,7 @@ def _update_info(self):
Returns boolean if scanning successful.
"""
- _LOGGER.info("Scanning...")
+ _LOGGER.info("Scanning")
box = pybbox.Bbox(ip=self.host)
result = box.get_all_connected_devices()
diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py
index 13c8f5bb03f099..5256c2a61a0787 100644
--- a/homeassistant/components/bbox/sensor.py
+++ b/homeassistant/components/bbox/sensor.py
@@ -6,7 +6,7 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_MONITORED_VARIABLES,
@@ -15,7 +15,6 @@
DEVICE_CLASS_TIMESTAMP,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from homeassistant.util.dt import utcnow
@@ -86,7 +85,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class BboxUptimeSensor(Entity):
+class BboxUptimeSensor(SensorEntity):
"""Bbox uptime sensor."""
def __init__(self, bbox_data, sensor_type, name):
@@ -115,7 +114,7 @@ def icon(self):
return self._icon
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
@@ -133,7 +132,7 @@ def update(self):
self._state = uptime.replace(microsecond=0).isoformat()
-class BboxSensor(Entity):
+class BboxSensor(SensorEntity):
"""Implementation of a Bbox sensor."""
def __init__(self, bbox_data, sensor_type, name):
@@ -167,7 +166,7 @@ def icon(self):
return self._icon
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py
index e8a37d51be4a1d..9bf935f3c4f5c4 100644
--- a/homeassistant/components/beewi_smartclim/sensor.py
+++ b/homeassistant/components/beewi_smartclim/sensor.py
@@ -2,7 +2,7 @@
from beewi_smartclim import BeewiSmartClimPoller # pylint: disable=import-error
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_MAC,
CONF_NAME,
@@ -13,7 +13,6 @@
TEMP_CELSIUS,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
# Default values
DEFAULT_NAME = "BeeWi SmartClim"
@@ -56,7 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors)
-class BeewiSmartclimSensor(Entity):
+class BeewiSmartclimSensor(SensorEntity):
"""Representation of a Sensor."""
def __init__(self, poller, name, mac, device, unit):
diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py
index 7680b8b09ade68..5b708ae2630747 100644
--- a/homeassistant/components/bh1750/sensor.py
+++ b/homeassistant/components/bh1750/sensor.py
@@ -3,13 +3,12 @@
import logging
from i2csense.bh1750 import BH1750 # pylint: disable=import-error
-import smbus # pylint: disable=import-error
+import smbus
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -94,7 +93,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(dev, True)
-class BH1750Sensor(Entity):
+class BH1750Sensor(SensorEntity):
"""Implementation of the BH1750 sensor."""
def __init__(self, bh1750_sensor, name, unit, multiplier=1.0):
diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py
index 999a62b3a80e31..8c5066342005aa 100644
--- a/homeassistant/components/binary_sensor/device_condition.py
+++ b/homeassistant/components/binary_sensor/device_condition.py
@@ -1,5 +1,5 @@
"""Implement device conditions for binary sensor."""
-from typing import Dict, List
+from __future__ import annotations
import voluptuous as vol
@@ -205,9 +205,9 @@
async def async_get_conditions(
hass: HomeAssistant, device_id: str
-) -> List[Dict[str, str]]:
+) -> list[dict[str, str]]:
"""List device conditions."""
- conditions: List[Dict[str, str]] = []
+ conditions: list[dict[str, str]] = []
entity_registry = await async_get_registry(hass)
entries = [
entry
diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py
index f7f0c53a6988a8..b87a761a7a1138 100644
--- a/homeassistant/components/binary_sensor/device_trigger.py
+++ b/homeassistant/components/binary_sensor/device_trigger.py
@@ -190,16 +190,13 @@ async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
trigger_type = config[CONF_TYPE]
if trigger_type in TURNED_ON:
- from_state = "off"
to_state = "on"
else:
- from_state = "on"
to_state = "off"
state_config = {
state_trigger.CONF_PLATFORM: "state",
state_trigger.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
- state_trigger.CONF_FROM: from_state,
state_trigger.CONF_TO: to_state,
}
if CONF_FOR in config:
diff --git a/homeassistant/components/binary_sensor/group.py b/homeassistant/components/binary_sensor/group.py
index 1636054663dc69..234883ffd5a041 100644
--- a/homeassistant/components/binary_sensor/group.py
+++ b/homeassistant/components/binary_sensor/group.py
@@ -3,13 +3,12 @@
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OFF, STATE_ON
-from homeassistant.core import callback
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import HomeAssistant, callback
@callback
def async_describe_on_off_states(
- hass: HomeAssistantType, registry: GroupIntegrationRegistry
+ hass: HomeAssistant, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_ON}, STATE_OFF)
diff --git a/homeassistant/components/binary_sensor/significant_change.py b/homeassistant/components/binary_sensor/significant_change.py
new file mode 100644
index 00000000000000..8421483ba0cb8f
--- /dev/null
+++ b/homeassistant/components/binary_sensor/significant_change.py
@@ -0,0 +1,22 @@
+"""Helper to test significant Binary Sensor state changes."""
+from __future__ import annotations
+
+from typing import Any
+
+from homeassistant.core import HomeAssistant, callback
+
+
+@callback
+def async_check_significant_change(
+ hass: HomeAssistant,
+ old_state: str,
+ old_attrs: dict,
+ new_state: str,
+ new_attrs: dict,
+ **kwargs: Any,
+) -> bool | None:
+ """Test if state significantly changed."""
+ if old_state != new_state:
+ return True
+
+ return False
diff --git a/homeassistant/components/binary_sensor/translations/de.json b/homeassistant/components/binary_sensor/translations/de.json
index 3687536eb5b663..a78befb7965e4a 100644
--- a/homeassistant/components/binary_sensor/translations/de.json
+++ b/homeassistant/components/binary_sensor/translations/de.json
@@ -98,6 +98,10 @@
"off": "Normal",
"on": "Schwach"
},
+ "battery_charging": {
+ "off": "L\u00e4dt nicht",
+ "on": "L\u00e4dt"
+ },
"cold": {
"off": "Normal",
"on": "Kalt"
@@ -122,6 +126,10 @@
"off": "Normal",
"on": "Hei\u00df"
},
+ "light": {
+ "off": "Kein Licht",
+ "on": "Licht erkannt"
+ },
"lock": {
"off": "Verriegelt",
"on": "Entriegelt"
@@ -134,6 +142,10 @@
"off": "Ruhig",
"on": "Bewegung erkannt"
},
+ "moving": {
+ "off": "Bewegt sich nicht",
+ "on": "Bewegt sich"
+ },
"occupancy": {
"off": "Frei",
"on": "Belegt"
@@ -142,6 +154,10 @@
"off": "Geschlossen",
"on": "Offen"
},
+ "plug": {
+ "off": "Ausgesteckt",
+ "on": "Eingesteckt"
+ },
"presence": {
"off": "Abwesend",
"on": "Zu Hause"
diff --git a/homeassistant/components/binary_sensor/translations/id.json b/homeassistant/components/binary_sensor/translations/id.json
index 4ca757da6e5e0f..ac880aa28fa38b 100644
--- a/homeassistant/components/binary_sensor/translations/id.json
+++ b/homeassistant/components/binary_sensor/translations/id.json
@@ -1,13 +1,107 @@
{
+ "device_automation": {
+ "condition_type": {
+ "is_bat_low": "Baterai {entity_name} hampir habis",
+ "is_cold": "{entity_name} dingin",
+ "is_connected": "{entity_name} terhubung",
+ "is_gas": "{entity_name} mendeteksi gas",
+ "is_hot": "{entity_name} panas",
+ "is_light": "{entity_name} mendeteksi cahaya",
+ "is_locked": "{entity_name} terkunci",
+ "is_moist": "{entity_name} lembab",
+ "is_motion": "{entity_name} mendeteksi gerakan",
+ "is_moving": "{entity_name} bergerak",
+ "is_no_gas": "{entity_name} tidak mendeteksi gas",
+ "is_no_light": "{entity_name} tidak mendeteksi cahaya",
+ "is_no_motion": "{entity_name} tidak mendeteksi gerakan",
+ "is_no_problem": "{entity_name} tidak mendeteksi masalah",
+ "is_no_smoke": "{entity_name} tidak mendeteksi asap",
+ "is_no_sound": "{entity_name} tidak mendeteksi suara",
+ "is_no_vibration": "{entity_name} tidak mendeteksi getaran",
+ "is_not_bat_low": "Baterai {entity_name} normal",
+ "is_not_cold": "{entity_name} tidak dingin",
+ "is_not_connected": "{entity_name} terputus",
+ "is_not_hot": "{entity_name} tidak panas",
+ "is_not_locked": "{entity_name} tidak terkunci",
+ "is_not_moist": "{entity_name} kering",
+ "is_not_moving": "{entity_name} tidak bergerak",
+ "is_not_occupied": "{entity_name} tidak ditempati",
+ "is_not_open": "{entity_name} tertutup",
+ "is_not_plugged_in": "{entity_name} dicabut",
+ "is_not_powered": "{entity_name} tidak ditenagai",
+ "is_not_present": "{entity_name} tidak ada",
+ "is_not_unsafe": "{entity_name} aman",
+ "is_occupied": "{entity_name} ditempati",
+ "is_off": "{entity_name} mati",
+ "is_on": "{entity_name} nyala",
+ "is_open": "{entity_name} terbuka",
+ "is_plugged_in": "{entity_name} dicolokkan",
+ "is_powered": "{entity_name} ditenagai",
+ "is_present": "{entity_name} ada",
+ "is_problem": "{entity_name} mendeteksi masalah",
+ "is_smoke": "{entity_name} mendeteksi asap",
+ "is_sound": "{entity_name} mendeteksi suara",
+ "is_unsafe": "{entity_name} tidak aman",
+ "is_vibration": "{entity_name} mendeteksi getaran"
+ },
+ "trigger_type": {
+ "bat_low": "Baterai {entity_name} hampir habis",
+ "cold": "{entity_name} menjadi dingin",
+ "connected": "{entity_name} terhubung",
+ "gas": "{entity_name} mulai mendeteksi gas",
+ "hot": "{entity_name} menjadi panas",
+ "light": "{entity_name} mulai mendeteksi cahaya",
+ "locked": "{entity_name} terkunci",
+ "moist": "{entity_name} menjadi lembab",
+ "motion": "{entity_name} mulai mendeteksi gerakan",
+ "moving": "{entity_name} mulai bergerak",
+ "no_gas": "{entity_name} berhenti mendeteksi gas",
+ "no_light": "{entity_name} berhenti mendeteksi cahaya",
+ "no_motion": "{entity_name} berhenti mendeteksi gerakan",
+ "no_problem": "{entity_name} berhenti mendeteksi masalah",
+ "no_smoke": "{entity_name} berhenti mendeteksi asap",
+ "no_sound": "{entity_name} berhenti mendeteksi suara",
+ "no_vibration": "{entity_name} berhenti mendeteksi getaran",
+ "not_bat_low": "Baterai {entity_name} normal",
+ "not_cold": "{entity_name} menjadi tidak dingin",
+ "not_connected": "{entity_name} terputus",
+ "not_hot": "{entity_name} menjadi tidak panas",
+ "not_locked": "{entity_name} tidak terkunci",
+ "not_moist": "{entity_name} menjadi kering",
+ "not_moving": "{entity_name} berhenti bergerak",
+ "not_occupied": "{entity_name} menjadi tidak ditempati",
+ "not_opened": "{entity_name} tertutup",
+ "not_plugged_in": "{entity_name} dicabut",
+ "not_powered": "{entity_name} tidak ditenagai",
+ "not_present": "{entity_name} tidak ada",
+ "not_unsafe": "{entity_name} menjadi aman",
+ "occupied": "{entity_name} menjadi ditempati",
+ "opened": "{entity_name} terbuka",
+ "plugged_in": "{entity_name} dicolokkan",
+ "powered": "{entity_name} ditenagai",
+ "present": "{entity_name} ada",
+ "problem": "{entity_name} mulai mendeteksi masalah",
+ "smoke": "{entity_name} mulai mendeteksi asap",
+ "sound": "{entity_name} mulai mendeteksi suara",
+ "turned_off": "{entity_name} dimatikan",
+ "turned_on": "{entity_name} dinyalakan",
+ "unsafe": "{entity_name} menjadi tidak aman",
+ "vibration": "{entity_name} mulai mendeteksi getaran"
+ }
+ },
"state": {
"_": {
- "off": "Off",
- "on": "On"
+ "off": "Mati",
+ "on": "Nyala"
},
"battery": {
"off": "Normal",
"on": "Rendah"
},
+ "battery_charging": {
+ "off": "Tidak mengisi daya",
+ "on": "Mengisi daya"
+ },
"cold": {
"off": "Normal",
"on": "Dingin"
@@ -25,13 +119,17 @@
"on": "Terbuka"
},
"gas": {
- "off": "Kosong",
+ "off": "Tidak ada",
"on": "Terdeteksi"
},
"heat": {
"off": "Normal",
"on": "Panas"
},
+ "light": {
+ "off": "Tidak ada cahaya",
+ "on": "Cahaya terdeteksi"
+ },
"lock": {
"off": "Terkunci",
"on": "Terbuka"
@@ -44,6 +142,10 @@
"off": "Tidak ada",
"on": "Terdeteksi"
},
+ "moving": {
+ "off": "Tidak bergerak",
+ "on": "Bergerak"
+ },
"occupancy": {
"off": "Tidak ada",
"on": "Terdeteksi"
@@ -52,13 +154,17 @@
"off": "Tertutup",
"on": "Terbuka"
},
+ "plug": {
+ "off": "Dicabut",
+ "on": "Dicolokkan"
+ },
"presence": {
"off": "Keluar",
- "on": "Rumah"
+ "on": "Di Rumah"
},
"problem": {
"off": "Oke",
- "on": "Masalah"
+ "on": "Bermasalah"
},
"safety": {
"off": "Aman",
diff --git a/homeassistant/components/binary_sensor/translations/ko.json b/homeassistant/components/binary_sensor/translations/ko.json
index 0b8ef0b73d5f4a..7a725fc67199b7 100644
--- a/homeassistant/components/binary_sensor/translations/ko.json
+++ b/homeassistant/components/binary_sensor/translations/ko.json
@@ -1,92 +1,92 @@
{
"device_automation": {
"condition_type": {
- "is_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud558\uba74",
- "is_cold": "{entity_name} \uc628\ub3c4\uac00 \ub0ae\uc73c\uba74",
- "is_connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub418\uc5b4 \uc788\uc73c\uba74",
- "is_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uba74",
- "is_hot": "{entity_name} \uc628\ub3c4\uac00 \ub192\uc73c\uba74",
- "is_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uba74",
- "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc73c\uba74",
- "is_moist": "{entity_name} \uc774(\uac00) \uc2b5\ud558\uba74",
- "is_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uba74",
- "is_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uba74",
- "is_no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74",
- "is_no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74",
- "is_no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74",
- "is_no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74",
- "is_no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74",
- "is_no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74",
- "is_no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74",
- "is_not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc774\uba74",
- "is_not_cold": "{entity_name} \uc628\ub3c4\uac00 \ub0ae\uc9c0 \uc54a\uc73c\uba74",
- "is_not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc838 \uc788\ub2e4\uba74",
- "is_not_hot": "{entity_name} \uc628\ub3c4\uac00 \ub192\uc9c0 \uc54a\uc73c\uba74",
- "is_not_locked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc9c0 \uc54a\uc73c\uba74",
- "is_not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud558\uba74",
- "is_not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc73c\uba74",
- "is_not_occupied": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \uc544\ub2c8\uba74",
- "is_not_open": "{entity_name} \uc774(\uac00) \ub2eb\ud600 \uc788\uc73c\uba74",
- "is_not_plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \ubf51\ud600 \uc788\uc73c\uba74",
- "is_not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc73c\uba74",
- "is_not_present": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uc911\uc774\uba74",
- "is_not_unsafe": "{entity_name} \uc774(\uac00) \uc548\uc804\ud558\uba74",
- "is_occupied": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uc774\uba74",
- "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74",
- "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74",
- "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub824 \uc788\uc73c\uba74",
- "is_plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \uaf3d\ud600 \uc788\uc73c\uba74",
- "is_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uace0 \uc788\uc73c\uba74",
- "is_present": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc911\uc774\uba74",
- "is_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uba74",
- "is_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uba74",
- "is_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uba74",
- "is_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc73c\uba74",
- "is_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uba74"
+ "is_bat_low": "{entity_name}\uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud558\uba74",
+ "is_cold": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub0ae\uc73c\uba74",
+ "is_connected": "{entity_name}\uc774(\uac00) \uc5f0\uacb0\ub418\uc5b4 \uc788\uc73c\uba74",
+ "is_gas": "{entity_name}\uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74",
+ "is_hot": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub192\uc73c\uba74",
+ "is_light": "{entity_name}\uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74",
+ "is_locked": "{entity_name}\uc774(\uac00) \uc7a0\uaca8\uc788\uc73c\uba74",
+ "is_moist": "{entity_name}\uc774(\uac00) \uc2b5\ud558\uba74",
+ "is_motion": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74",
+ "is_moving": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc774\uace0 \uc788\uc73c\uba74",
+ "is_no_gas": "{entity_name}\uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74",
+ "is_no_light": "{entity_name}\uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74",
+ "is_no_motion": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74",
+ "is_no_problem": "{entity_name}\uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74",
+ "is_no_smoke": "{entity_name}\uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74",
+ "is_no_sound": "{entity_name}\uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74",
+ "is_no_vibration": "{entity_name}\uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74",
+ "is_not_bat_low": "{entity_name}\uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc774\uba74",
+ "is_not_cold": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub0ae\uc9c0 \uc54a\uc73c\uba74",
+ "is_not_connected": "{entity_name}\uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc838 \uc788\uc73c\uba74",
+ "is_not_hot": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub192\uc9c0 \uc54a\uc73c\uba74",
+ "is_not_locked": "{entity_name}\uc774(\uac00) \uc7a0\uaca8\uc788\uc9c0 \uc54a\uc73c\uba74",
+ "is_not_moist": "{entity_name}\uc774(\uac00) \uac74\uc870\ud558\uba74",
+ "is_not_moving": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc774\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74",
+ "is_not_occupied": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \uc544\ub2c8\uba74",
+ "is_not_open": "{entity_name}\uc774(\uac00) \ub2eb\ud600 \uc788\uc73c\uba74",
+ "is_not_plugged_in": "{entity_name}\uc758 \ud50c\ub7ec\uadf8\uac00 \ubf51\ud600 \uc788\uc73c\uba74",
+ "is_not_powered": "{entity_name}\uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74",
+ "is_not_present": "{entity_name}\uc774(\uac00) \uc678\ucd9c \uc911\uc774\uba74",
+ "is_not_unsafe": "{entity_name}\uc774(\uac00) \uc548\uc804\ud558\uba74",
+ "is_occupied": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uc774\uba74",
+ "is_off": "{entity_name}\uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74",
+ "is_on": "{entity_name}\uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74",
+ "is_open": "{entity_name}\uc774(\uac00) \uc5f4\ub824 \uc788\uc73c\uba74",
+ "is_plugged_in": "{entity_name}\uc758 \ud50c\ub7ec\uadf8\uac00 \uaf3d\ud600 \uc788\uc73c\uba74",
+ "is_powered": "{entity_name}\uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uace0 \uc788\uc73c\uba74",
+ "is_present": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc911\uc774\uba74",
+ "is_problem": "{entity_name}\uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74",
+ "is_smoke": "{entity_name}\uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74",
+ "is_sound": "{entity_name}\uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74",
+ "is_unsafe": "{entity_name}\uc774(\uac00) \uc548\uc804\ud558\uc9c0 \uc54a\uc73c\uba74",
+ "is_vibration": "{entity_name}\uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74"
},
"trigger_type": {
- "bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud574\uc9c8 \ub54c",
- "cold": "{entity_name} \uc628\ub3c4\uac00 \ub0ae\uc544\uc84c\uc744 \ub54c",
- "connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub420 \ub54c",
- "gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud560 \ub54c",
- "hot": "{entity_name} \uc628\ub3c4\uac00 \ub192\uc544\uc84c\uc744 \ub54c",
- "light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud560 \ub54c",
- "locked": "{entity_name} \uc774(\uac00) \uc7a0\uae38 \ub54c",
- "moist": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9c8 \ub54c",
- "motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud560 \ub54c",
- "moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc77c \ub54c",
- "no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c",
- "no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c",
- "no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c",
- "no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c",
- "no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c",
- "no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c",
- "no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c",
- "not_bat_low": "{entity_name} \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc774 \ub420 \ub54c",
- "not_cold": "{entity_name} \uc628\ub3c4\uac00 \ub0ae\uc9c0 \uc54a\uac8c \ub410\uc744 \ub54c",
- "not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc9c8 \ub54c",
- "not_hot": "{entity_name} \uc628\ub3c4\uac00 \ub192\uc9c0 \uc54a\uac8c \ub410\uc744 \ub54c",
- "not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub420 \ub54c",
- "not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud574\uc9c8 \ub54c",
- "not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc744 \ub54c",
- "not_occupied": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \uc544\ub2c8\uac8c \ub420 \ub54c",
- "not_opened": "{entity_name} \uc774(\uac00) \ub2eb\ud790 \ub54c",
- "not_plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \ubf51\ud790 \ub54c",
- "not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc744 \ub54c",
- "not_present": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uc0c1\ud0dc\uac00 \ub420 \ub54c",
- "not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud574\uc9c8 \ub54c",
- "occupied": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \ub420 \ub54c",
- "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9b4 \ub54c",
- "plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \uaf3d\ud790 \ub54c",
- "powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub420 \ub54c",
- "present": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \ub420 \ub54c",
- "problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud560 \ub54c",
- "smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud560 \ub54c",
- "sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud560 \ub54c",
- "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c",
- "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c",
- "unsafe": "{entity_name} \uc774(\uac00) \uc548\uc804\ud558\uc9c0 \uc54a\uc744 \ub54c",
- "vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud560 \ub54c"
+ "bat_low": "{entity_name}\uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud574\uc84c\uc744 \ub54c",
+ "cold": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub0ae\uc544\uc84c\uc744 \ub54c",
+ "connected": "{entity_name}\uc774(\uac00) \uc5f0\uacb0\ub418\uc5c8\uc744 \ub54c",
+ "gas": "{entity_name}\uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c",
+ "hot": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub192\uc544\uc84c\uc744 \ub54c",
+ "light": "{entity_name}\uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c",
+ "locked": "{entity_name}\uc774(\uac00) \uc7a0\uacbc\uc744 \ub54c",
+ "moist": "{entity_name}\uc774(\uac00) \uc2b5\ud574\uc84c\uc744 \ub54c",
+ "motion": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c",
+ "moving": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc774\uae30 \uc2dc\uc791\ud588\uc744 \ub54c",
+ "no_gas": "{entity_name}\uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c",
+ "no_light": "{entity_name}\uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c",
+ "no_motion": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c",
+ "no_problem": "{entity_name}\uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c",
+ "no_smoke": "{entity_name}\uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c",
+ "no_sound": "{entity_name}\uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c",
+ "no_vibration": "{entity_name}\uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c",
+ "not_bat_low": "{entity_name}\uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc774 \ub418\uc5c8\uc744 \ub54c",
+ "not_cold": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub0ae\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c",
+ "not_connected": "{entity_name}\uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc84c\uc744 \ub54c",
+ "not_hot": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub192\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c",
+ "not_locked": "{entity_name}\uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc744 \ub54c",
+ "not_moist": "{entity_name}\uc774(\uac00) \uac74\uc870\ud574\uc84c\uc744 \ub54c",
+ "not_moving": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c",
+ "not_occupied": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \uc544\ub2c8\uac8c \ub418\uc5c8\uc744 \ub54c",
+ "not_opened": "{entity_name}\uc774(\uac00) \ub2eb\ud614\uc744 \ub54c",
+ "not_plugged_in": "{entity_name}\uc758 \ud50c\ub7ec\uadf8\uac00 \ubf51\ud614\uc744 \ub54c",
+ "not_powered": "{entity_name}\uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c",
+ "not_present": "{entity_name}\uc774(\uac00) \uc678\ucd9c \uc0c1\ud0dc\uac00 \ub418\uc5c8\uc744 \ub54c",
+ "not_unsafe": "{entity_name}\uc774(\uac00) \uc548\uc804\ud574\uc84c\uc744 \ub54c",
+ "occupied": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \ub418\uc5c8\uc744 \ub54c",
+ "opened": "{entity_name}\uc774(\uac00) \uc5f4\ub838\uc744 \ub54c",
+ "plugged_in": "{entity_name}\uc758 \ud50c\ub7ec\uadf8\uac00 \uaf3d\ud614\uc744 \ub54c",
+ "powered": "{entity_name}\uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc5c8\uc744 \ub54c",
+ "present": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \ub418\uc5c8\uc744 \ub54c",
+ "problem": "{entity_name}\uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c",
+ "smoke": "{entity_name}\uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c",
+ "sound": "{entity_name}\uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c",
+ "turned_off": "{entity_name}\uc774(\uac00) \uaebc\uc84c\uc744 \ub54c",
+ "turned_on": "{entity_name}\uc774(\uac00) \ucf1c\uc84c\uc744 \ub54c",
+ "unsafe": "{entity_name}\uc774(\uac00) \uc548\uc804\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c",
+ "vibration": "{entity_name}\uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c"
}
},
"state": {
@@ -98,6 +98,10 @@
"off": "\ubcf4\ud1b5",
"on": "\ub0ae\uc74c"
},
+ "battery_charging": {
+ "off": "\ucda9\uc804 \uc911\uc774 \uc544\ub2d8",
+ "on": "\ucda9\uc804 \uc911"
+ },
"cold": {
"off": "\ubcf4\ud1b5",
"on": "\uc800\uc628"
@@ -122,6 +126,10 @@
"off": "\ubcf4\ud1b5",
"on": "\uace0\uc628"
},
+ "light": {
+ "off": "\ube5b\uc774 \uc5c6\uc2b4",
+ "on": "\ube5b\uc744 \uac10\uc9c0\ud568"
+ },
"lock": {
"off": "\uc7a0\uae40",
"on": "\ud574\uc81c"
@@ -134,6 +142,10 @@
"off": "\uc774\uc0c1\uc5c6\uc74c",
"on": "\uac10\uc9c0\ub428"
},
+ "moving": {
+ "off": "\uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc74c",
+ "on": "\uc6c0\uc9c1\uc784"
+ },
"occupancy": {
"off": "\uc774\uc0c1\uc5c6\uc74c",
"on": "\uac10\uc9c0\ub428"
@@ -142,6 +154,10 @@
"off": "\ub2eb\ud798",
"on": "\uc5f4\ub9bc"
},
+ "plug": {
+ "off": "\ud50c\ub7ec\uadf8\uac00 \ubf51\ud798",
+ "on": "\ud50c\ub7ec\uadf8\uac00 \uaf3d\ud798"
+ },
"presence": {
"off": "\uc678\ucd9c",
"on": "\uc7ac\uc2e4"
diff --git a/homeassistant/components/binary_sensor/translations/pl.json b/homeassistant/components/binary_sensor/translations/pl.json
index 7d6e8eab4ba96c..726765aea0255d 100644
--- a/homeassistant/components/binary_sensor/translations/pl.json
+++ b/homeassistant/components/binary_sensor/translations/pl.json
@@ -99,7 +99,7 @@
"on": "roz\u0142adowana"
},
"battery_charging": {
- "off": "nie \u0142aduje",
+ "off": "roz\u0142adowywanie",
"on": "\u0142adowanie"
},
"cold": {
diff --git a/homeassistant/components/binary_sensor/translations/tr.json b/homeassistant/components/binary_sensor/translations/tr.json
index 3c5cfaeeacf07c..daf44cc967b554 100644
--- a/homeassistant/components/binary_sensor/translations/tr.json
+++ b/homeassistant/components/binary_sensor/translations/tr.json
@@ -1,4 +1,10 @@
{
+ "device_automation": {
+ "trigger_type": {
+ "moist": "{entity_name} nemli oldu",
+ "not_opened": "{entity_name} kapat\u0131ld\u0131"
+ }
+ },
"state": {
"_": {
"off": "Kapal\u0131",
@@ -8,6 +14,10 @@
"off": "Normal",
"on": "D\u00fc\u015f\u00fck"
},
+ "battery_charging": {
+ "off": "\u015earj olmuyor",
+ "on": "\u015earj Oluyor"
+ },
"cold": {
"off": "Normal",
"on": "So\u011fuk"
@@ -32,6 +42,10 @@
"off": "Normal",
"on": "S\u0131cak"
},
+ "light": {
+ "off": "I\u015f\u0131k yok",
+ "on": "I\u015f\u0131k alg\u0131land\u0131"
+ },
"lock": {
"off": "Kilit kapal\u0131",
"on": "Kilit a\u00e7\u0131k"
@@ -44,6 +58,10 @@
"off": "Temiz",
"on": "Alg\u0131land\u0131"
},
+ "moving": {
+ "off": "Hareket etmiyor",
+ "on": "Hareketli"
+ },
"occupancy": {
"off": "Temiz",
"on": "Alg\u0131land\u0131"
@@ -52,9 +70,13 @@
"off": "Kapal\u0131",
"on": "A\u00e7\u0131k"
},
+ "plug": {
+ "off": "Fi\u015fi \u00e7ekildi",
+ "on": "Tak\u0131l\u0131"
+ },
"presence": {
- "off": "[%key:common::state::evde_degil%]",
- "on": "[%key:common::state::evde%]"
+ "off": "D\u0131\u015farda",
+ "on": "Evde"
},
"problem": {
"off": "Tamam",
diff --git a/homeassistant/components/binary_sensor/translations/uk.json b/homeassistant/components/binary_sensor/translations/uk.json
index 29767f6d6d62e5..0f8d92749c4cb1 100644
--- a/homeassistant/components/binary_sensor/translations/uk.json
+++ b/homeassistant/components/binary_sensor/translations/uk.json
@@ -2,15 +2,91 @@
"device_automation": {
"condition_type": {
"is_bat_low": "{entity_name} \u043d\u0438\u0437\u044c\u043a\u0438\u0439 \u0440\u0456\u0432\u0435\u043d\u044c \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430",
- "is_not_bat_low": "{entity_name} \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u043c \u0437\u0430\u0440\u044f\u0434 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430"
+ "is_cold": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f",
+ "is_connected": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f",
+ "is_gas": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0433\u0430\u0437",
+ "is_hot": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043d\u0430\u0433\u0440\u0456\u0432",
+ "is_light": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0441\u0432\u0456\u0442\u043b\u043e",
+ "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_moist": "{entity_name} \u0432 \u0441\u0442\u0430\u043d\u0456 \"\u0412\u043e\u043b\u043e\u0433\u043e\"",
+ "is_motion": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0440\u0443\u0445",
+ "is_moving": "{entity_name} \u043f\u0435\u0440\u0435\u043c\u0456\u0449\u0443\u0454\u0442\u044c\u0441\u044f",
+ "is_no_gas": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0433\u0430\u0437",
+ "is_no_light": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0441\u0432\u0456\u0442\u043b\u043e",
+ "is_no_motion": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0440\u0443\u0445",
+ "is_no_problem": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u043e\u0431\u043b\u0435\u043c",
+ "is_no_smoke": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0434\u0438\u043c",
+ "is_no_sound": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0437\u0432\u0443\u043a",
+ "is_no_vibration": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044e",
+ "is_not_bat_low": "{entity_name} \u0432 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_not_cold": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f",
+ "is_not_connected": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f",
+ "is_not_hot": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043d\u0430\u0433\u0440\u0456\u0432",
+ "is_not_locked": "{entity_name} \u0432 \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_not_moist": "{entity_name} \u0432 \u0441\u0442\u0430\u043d\u0456 \"\u0421\u0443\u0445\u043e\"",
+ "is_not_moving": "{entity_name} \u043d\u0435 \u0440\u0443\u0445\u0430\u0454\u0442\u044c\u0441\u044f",
+ "is_not_occupied": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c",
+ "is_not_open": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u0438\u0442\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_not_plugged_in": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f",
+ "is_not_powered": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f",
+ "is_not_present": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c",
+ "is_not_unsafe": "{entity_name} \u0432 \u0431\u0435\u0437\u043f\u0435\u0447\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_occupied": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c",
+ "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_open": "{entity_name} \u0443 \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_plugged_in": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f",
+ "is_powered": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f",
+ "is_present": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c",
+ "is_problem": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443",
+ "is_smoke": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0434\u0438\u043c",
+ "is_sound": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0437\u0432\u0443\u043a",
+ "is_unsafe": "{entity_name} \u0432 \u043d\u0435\u0431\u0435\u0437\u043f\u0435\u0447\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_vibration": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044e"
},
"trigger_type": {
- "bat_low": "{entity_name} \u043d\u0438\u0437\u044c\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430",
- "not_bat_low": "{entity_name} \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u0439 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440",
+ "bat_low": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043d\u0438\u0437\u044c\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434",
+ "cold": "{entity_name} \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0443\u0454\u0442\u044c\u0441\u044f",
+ "connected": "{entity_name} \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0430\u0454\u0442\u044c\u0441\u044f",
+ "gas": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0433\u0430\u0437",
+ "hot": "{entity_name} \u043d\u0430\u0433\u0440\u0456\u0432\u0430\u0454\u0442\u044c\u0441\u044f",
+ "light": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0441\u0432\u0456\u0442\u043b\u043e",
+ "locked": "{entity_name} \u0431\u043b\u043e\u043a\u0443\u0454\u0442\u044c\u0441\u044f",
+ "moist": "{entity_name} \u0441\u0442\u0430\u0454 \u0432\u043e\u043b\u043e\u0433\u0438\u043c",
+ "motion": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0440\u0443\u0445",
+ "moving": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u0440\u0443\u0445\u0430\u0442\u0438\u0441\u044f",
+ "no_gas": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0433\u0430\u0437",
+ "no_light": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0441\u0432\u0456\u0442\u043b\u043e",
+ "no_motion": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0440\u0443\u0445",
+ "no_problem": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443",
+ "no_smoke": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0434\u0438\u043c",
+ "no_sound": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0437\u0432\u0443\u043a",
+ "no_vibration": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044e",
+ "not_bat_low": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u0439 \u0437\u0430\u0440\u044f\u0434",
+ "not_cold": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0443\u0432\u0430\u0442\u0438\u0441\u044f",
+ "not_connected": "{entity_name} \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0430\u0454\u0442\u044c\u0441\u044f",
+ "not_hot": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u043d\u0430\u0433\u0440\u0456\u0432\u0430\u0442\u0438\u0441\u044f",
+ "not_locked": "{entity_name} \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0454\u0442\u044c\u0441\u044f",
+ "not_moist": "{entity_name} \u0441\u0442\u0430\u0454 \u0441\u0443\u0445\u0438\u043c",
+ "not_moving": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u043f\u0435\u0440\u0435\u043c\u0456\u0449\u0435\u043d\u043d\u044f",
+ "not_occupied": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c",
"not_opened": "{entity_name} \u0437\u0430\u043a\u0440\u0438\u0442\u043e",
+ "not_plugged_in": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f",
+ "not_powered": "{entity_name} \u043d\u0435 \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043d\u0430\u044f\u0432\u043d\u0456\u0441\u0442\u044c \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f",
+ "not_present": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u0432\u0456\u0434\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c",
+ "not_unsafe": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u0431\u0435\u0437\u043f\u0435\u043a\u0443",
+ "occupied": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c",
"opened": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e",
+ "plugged_in": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f",
+ "powered": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043d\u0430\u044f\u0432\u043d\u0456\u0441\u0442\u044c \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f",
+ "present": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c",
+ "problem": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443",
+ "smoke": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0434\u0438\u043c",
+ "sound": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0437\u0432\u0443\u043a",
"turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
- "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e"
+ "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e",
+ "unsafe": "{entity_name} \u043d\u0435 \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u0431\u0435\u0437\u043f\u0435\u043a\u0443",
+ "vibration": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044e"
}
},
"state": {
@@ -22,6 +98,10 @@
"off": "\u041d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u0439",
"on": "\u041d\u0438\u0437\u044c\u043a\u0438\u0439"
},
+ "battery_charging": {
+ "off": "\u041d\u0435 \u0437\u0430\u0440\u044f\u0434\u0436\u0430\u0454\u0442\u044c\u0441\u044f",
+ "on": "\u0417\u0430\u0440\u044f\u0434\u0436\u0430\u043d\u043d\u044f"
+ },
"cold": {
"off": "\u041d\u043e\u0440\u043c\u0430",
"on": "\u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f"
@@ -46,6 +126,10 @@
"off": "\u041d\u043e\u0440\u043c\u0430",
"on": "\u041d\u0430\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f"
},
+ "light": {
+ "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
+ "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e"
+ },
"lock": {
"off": "\u0417\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e",
"on": "\u0420\u043e\u0437\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e"
@@ -58,13 +142,21 @@
"off": "\u041d\u0435\u043c\u0430\u0454 \u0440\u0443\u0445\u0443",
"on": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0440\u0443\u0445"
},
+ "moving": {
+ "off": "\u0420\u0443\u0445\u0443 \u043d\u0435\u043c\u0430\u0454",
+ "on": "\u0420\u0443\u0445\u0430\u0454\u0442\u044c\u0441\u044f"
+ },
"occupancy": {
"off": "\u0427\u0438\u0441\u0442\u043e",
"on": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c"
},
"opening": {
- "off": "\u0417\u0430\u043a\u0440\u0438\u0442\u043e",
- "on": "\u0412\u0456\u0434\u043a\u0440\u0438\u0442\u0438\u0439"
+ "off": "\u0417\u0430\u0447\u0438\u043d\u0435\u043d\u043e",
+ "on": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u043e"
+ },
+ "plug": {
+ "off": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e",
+ "on": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e"
},
"presence": {
"off": "\u041d\u0435 \u0432\u0434\u043e\u043c\u0430",
@@ -91,8 +183,8 @@
"on": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u0430 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044f"
},
"window": {
- "off": "\u0417\u0430\u0447\u0438\u043d\u0435\u043d\u0435",
- "on": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u0435"
+ "off": "\u0417\u0430\u0447\u0438\u043d\u0435\u043d\u043e",
+ "on": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u043e"
}
},
"title": "\u0411\u0456\u043d\u0430\u0440\u043d\u0438\u0439 \u0434\u0430\u0442\u0447\u0438\u043a"
diff --git a/homeassistant/components/binary_sensor/translations/zh-Hans.json b/homeassistant/components/binary_sensor/translations/zh-Hans.json
index 9254f667a48d92..82cd0d3ccfec98 100644
--- a/homeassistant/components/binary_sensor/translations/zh-Hans.json
+++ b/homeassistant/components/binary_sensor/translations/zh-Hans.json
@@ -25,13 +25,13 @@
"is_not_locked": "{entity_name} \u5df2\u89e3\u9501",
"is_not_moist": "{entity_name} \u5e72\u71e5",
"is_not_moving": "{entity_name} \u9759\u6b62",
- "is_not_occupied": "{entity_name}\u6ca1\u6709\u4eba",
+ "is_not_occupied": "{entity_name} \u65e0\u4eba",
"is_not_open": "{entity_name} \u5df2\u5173\u95ed",
"is_not_plugged_in": "{entity_name} \u672a\u63d2\u5165",
"is_not_powered": "{entity_name} \u672a\u901a\u7535",
"is_not_present": "{entity_name} \u4e0d\u5728\u5bb6",
"is_not_unsafe": "{entity_name} \u5b89\u5168",
- "is_occupied": "{entity_name}\u6709\u4eba",
+ "is_occupied": "{entity_name} \u6709\u4eba",
"is_off": "{entity_name} \u5df2\u5173\u95ed",
"is_on": "{entity_name} \u5df2\u5f00\u542f",
"is_open": "{entity_name} \u5df2\u6253\u5f00",
@@ -51,15 +51,42 @@
"gas": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u71c3\u6c14\u6cc4\u6f0f",
"hot": "{entity_name} \u53d8\u70ed",
"light": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u5149\u7ebf",
- "locked": "{entity_name}\u5df2\u4e0a\u9501",
+ "locked": "{entity_name} \u88ab\u9501\u5b9a",
+ "moist": "{entity_name} \u53d8\u6e7f",
"motion": "{entity_name} \u68c0\u6d4b\u5230\u6709\u4eba",
- "moving": "{entity_name}\u5f00\u59cb\u79fb\u52a8",
- "no_motion": "{entity_name} \u672a\u68c0\u6d4b\u5230\u6709\u4eba",
- "not_bat_low": "{entity_name}\u7535\u91cf\u6b63\u5e38",
- "not_locked": "{entity_name}\u5df2\u89e3\u9501",
- "not_opened": "{entity_name}\u5df2\u5173\u95ed",
+ "moving": "{entity_name} \u5f00\u59cb\u79fb\u52a8",
+ "no_gas": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u71c3\u6c14\u6cc4\u6f0f",
+ "no_light": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u5149\u7ebf",
+ "no_motion": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u6709\u4eba",
+ "no_problem": "{entity_name} \u95ee\u9898\u89e3\u9664",
+ "no_smoke": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u70df\u96fe",
+ "no_sound": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u58f0\u97f3",
+ "no_vibration": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u632f\u52a8",
+ "not_bat_low": "{entity_name} \u7535\u6c60\u7535\u91cf\u6b63\u5e38",
+ "not_cold": "{entity_name} \u4e0d\u51b7\u4e86",
+ "not_connected": "{entity_name} \u65ad\u5f00",
+ "not_hot": "{entity_name} \u4e0d\u70ed\u4e86",
+ "not_locked": "{entity_name} \u89e3\u9501",
+ "not_moist": "{entity_name} \u53d8\u5e72",
+ "not_moving": "{entity_name} \u505c\u6b62\u79fb\u52a8",
+ "not_occupied": "{entity_name} \u4e0d\u518d\u6709\u4eba",
+ "not_opened": "{entity_name} \u5df2\u5173\u95ed",
+ "not_plugged_in": "{entity_name} \u88ab\u62d4\u51fa",
+ "not_powered": "{entity_name} \u6389\u7535",
+ "not_present": "{entity_name} \u4e0d\u5728\u5bb6",
+ "not_unsafe": "{entity_name} \u5b89\u5168\u4e86",
+ "occupied": "{entity_name} \u6709\u4eba",
+ "opened": "{entity_name} \u88ab\u6253\u5f00",
+ "plugged_in": "{entity_name} \u88ab\u63d2\u5165",
+ "powered": "{entity_name} \u4e0a\u7535",
+ "present": "{entity_name} \u5728\u5bb6",
+ "problem": "{entity_name} \u53d1\u73b0\u95ee\u9898",
+ "smoke": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u70df\u96fe",
+ "sound": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u58f0\u97f3",
"turned_off": "{entity_name} \u88ab\u5173\u95ed",
- "turned_on": "{entity_name} \u88ab\u6253\u5f00"
+ "turned_on": "{entity_name} \u88ab\u6253\u5f00",
+ "unsafe": "{entity_name} \u4e0d\u518d\u5b89\u5168",
+ "vibration": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u632f\u52a8"
}
},
"state": {
@@ -100,8 +127,8 @@
"on": "\u8fc7\u70ed"
},
"light": {
- "off": "\u6ca1\u6709\u5149\u7ebf",
- "on": "\u68c0\u6d4b\u5230\u5149\u7ebf"
+ "off": "\u65e0\u5149",
+ "on": "\u6709\u5149"
},
"lock": {
"off": "\u4e0a\u9501",
@@ -120,8 +147,8 @@
"on": "\u6b63\u5728\u79fb\u52a8"
},
"occupancy": {
- "off": "\u672a\u89e6\u53d1",
- "on": "\u5df2\u89e6\u53d1"
+ "off": "\u65e0\u4eba",
+ "on": "\u6709\u4eba"
},
"opening": {
"off": "\u5173\u95ed",
diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py
index c748b2f72f94e6..4acce03d6faaf1 100644
--- a/homeassistant/components/bitcoin/sensor.py
+++ b/homeassistant/components/bitcoin/sensor.py
@@ -5,7 +5,7 @@
from blockchain import exchangerates, statistics
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_CURRENCY,
@@ -14,7 +14,6 @@
TIME_SECONDS,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -77,7 +76,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(dev, True)
-class BitcoinSensor(Entity):
+class BitcoinSensor(SensorEntity):
"""Representation of a Bitcoin sensor."""
def __init__(self, data, option_type, currency):
@@ -110,7 +109,7 @@ def icon(self):
return ICON
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/bizkaibus/sensor.py b/homeassistant/components/bizkaibus/sensor.py
index c1cee98208c936..d0cade31a72402 100644
--- a/homeassistant/components/bizkaibus/sensor.py
+++ b/homeassistant/components/bizkaibus/sensor.py
@@ -1,11 +1,12 @@
"""Support for Bizkaibus, Biscay (Basque Country, Spain) Bus service."""
+from contextlib import suppress
+
from bizkaibus.bizkaibus import BizkaibusData
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, TIME_MINUTES
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
ATTR_DUE_IN = "Due in"
@@ -33,7 +34,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([BizkaibusSensor(data, stop, route, name)], True)
-class BizkaibusSensor(Entity):
+class BizkaibusSensor(SensorEntity):
"""The class for handling the data."""
def __init__(self, data, stop, route, name):
@@ -62,10 +63,8 @@ def unit_of_measurement(self):
def update(self):
"""Get the latest data from the webservice."""
self.data.update()
- try:
+ with suppress(TypeError):
self._state = self.data.info[0][ATTR_DUE_IN]
- except TypeError:
- pass
class Bizkaibus:
diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py
index 3d3c997596af7d..c5f723b68588ef 100644
--- a/homeassistant/components/blebox/__init__.py
+++ b/homeassistant/components/blebox/__init__.py
@@ -22,11 +22,6 @@
PARALLEL_UPDATES = 0
-async def async_setup(hass: HomeAssistant, config: dict):
- """Set up the BleBox devices component."""
- return True
-
-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up BleBox devices from a config entry."""
@@ -48,9 +43,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
domain_entry = domain.setdefault(entry.entry_id, {})
product = domain_entry.setdefault(PRODUCT, product)
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py
index 84f4c19371df41..c1b9d8501c10f8 100644
--- a/homeassistant/components/blebox/sensor.py
+++ b/homeassistant/components/blebox/sensor.py
@@ -1,6 +1,6 @@
"""BleBox sensor entities."""
-from homeassistant.helpers.entity import Entity
+from homeassistant.components.sensor import SensorEntity
from . import BleBoxEntity, create_blebox_entities
from .const import BLEBOX_TO_HASS_DEVICE_CLASSES, BLEBOX_TO_UNIT_MAP
@@ -14,7 +14,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
-class BleBoxSensorEntity(BleBoxEntity, Entity):
+class BleBoxSensorEntity(BleBoxEntity, SensorEntity):
"""Representation of a BleBox sensor feature."""
@property
diff --git a/homeassistant/components/blebox/translations/de.json b/homeassistant/components/blebox/translations/de.json
index baf14ba4897c0f..37c8dde54e5e4d 100644
--- a/homeassistant/components/blebox/translations/de.json
+++ b/homeassistant/components/blebox/translations/de.json
@@ -2,11 +2,11 @@
"config": {
"abort": {
"address_already_configured": "Ein BleBox-Ger\u00e4t ist bereits unter {address} konfiguriert.",
- "already_configured": "Dieses BleBox-Ger\u00e4t ist bereits konfiguriert."
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung mit dem BleBox-Ger\u00e4t nicht m\u00f6glich. (\u00dcberpr\u00fcfen Sie die Protokolle auf Fehler).",
- "unknown": "Unbekannter Fehler beim Anschlie\u00dfen an das BleBox-Ger\u00e4t. (Pr\u00fcfen Sie die Protokolle auf Fehler).",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "unknown": "Unerwarteter Fehler",
"unsupported_version": "Das BleBox-Ger\u00e4t hat eine veraltete Firmware. Bitte aktualisieren Sie es zuerst."
},
"flow_title": "BleBox-Ger\u00e4t: {name} ( {host} )",
diff --git a/homeassistant/components/blebox/translations/hu.json b/homeassistant/components/blebox/translations/hu.json
index 9b0bf1c0ddf0bd..9649d70d976ddf 100644
--- a/homeassistant/components/blebox/translations/hu.json
+++ b/homeassistant/components/blebox/translations/hu.json
@@ -1,7 +1,12 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
- "unsupported_version": "A BleBox eszk\u00f6z elavult firmware-vel rendelkezik. El\u0151sz\u00f6r friss\u00edtse."
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt",
+ "unsupported_version": "A BleBox eszk\u00f6z elavult firmware-rel rendelkezik. El\u0151sz\u00f6r friss\u00edtsd."
},
"flow_title": "BleBox eszk\u00f6z: {name} ({host})",
"step": {
diff --git a/homeassistant/components/blebox/translations/id.json b/homeassistant/components/blebox/translations/id.json
new file mode 100644
index 00000000000000..2ef604d1bff6fd
--- /dev/null
+++ b/homeassistant/components/blebox/translations/id.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "address_already_configured": "Perangkat BleBox sudah dikonfigurasi di {address}.",
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "unknown": "Kesalahan yang tidak diharapkan",
+ "unsupported_version": "Firmware Perangkat BleBox sudah usang. Tingkatkan terlebih dulu."
+ },
+ "flow_title": "Perangkat BleBox: {name} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Alamat IP",
+ "port": "Port"
+ },
+ "description": "Siapkan BleBox Anda untuk diintegrasikan dengan Home Assistant.",
+ "title": "Siapkan perangkat BleBox Anda"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/blebox/translations/ko.json b/homeassistant/components/blebox/translations/ko.json
index ff3fa740092a9f..1032e873ae9524 100644
--- a/homeassistant/components/blebox/translations/ko.json
+++ b/homeassistant/components/blebox/translations/ko.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "address_already_configured": "BleBox \uae30\uae30\uac00 {address} \ub85c \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "already_configured": "BleBox \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "address_already_configured": "BleBox \uae30\uae30\uac00 {address}(\uc73c)\ub85c \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "BleBox \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. (\ub85c\uadf8\uc5d0\uc11c \uc624\ub958 \ub0b4\uc6a9\uc744 \ud655\uc778\ud574\ubcf4\uc138\uc694.)",
- "unknown": "BleBox \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. (\ub85c\uadf8\uc5d0\uc11c \uc624\ub958 \ub0b4\uc6a9\uc744 \ud655\uc778\ud574\ubcf4\uc138\uc694.)",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4",
"unsupported_version": "BleBox \uae30\uae30 \ud38c\uc6e8\uc5b4\uac00 \uc624\ub798\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uba3c\uc800 \uc5c5\uadf8\ub808\uc774\ub4dc\ud574\uc8fc\uc138\uc694."
},
"flow_title": "BleBox \uae30\uae30: {name} ({host})",
@@ -16,7 +16,7 @@
"host": "IP \uc8fc\uc18c",
"port": "\ud3ec\ud2b8"
},
- "description": "Home Assistant \uc5d0 BleBox \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.",
+ "description": "Home Assistant\uc5d0 BleBox \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.",
"title": "BleBox \uae30\uae30 \uc124\uc815\ud558\uae30"
}
}
diff --git a/homeassistant/components/blebox/translations/tr.json b/homeassistant/components/blebox/translations/tr.json
new file mode 100644
index 00000000000000..31df3fb5e3074e
--- /dev/null
+++ b/homeassistant/components/blebox/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "address_already_configured": "Bir BleBox cihaz\u0131 zaten {address} yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r.",
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0130p Adresi",
+ "port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/blebox/translations/uk.json b/homeassistant/components/blebox/translations/uk.json
new file mode 100644
index 00000000000000..fb10807acff132
--- /dev/null
+++ b/homeassistant/components/blebox/translations/uk.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "address_already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 \u0430\u0434\u0440\u0435\u0441\u043e\u044e {address} \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435.",
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430",
+ "unsupported_version": "\u041c\u0456\u043a\u0440\u043e\u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0430 \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437\u0430\u0441\u0442\u0430\u0440\u0456\u043b\u0430. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u043d\u043e\u0432\u0456\u0442\u044c \u0457\u0457."
+ },
+ "flow_title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 BleBox: {name} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 BleBox.",
+ "title": "BleBox"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py
index 0809018522ea07..9c73ee6f995368 100644
--- a/homeassistant/components/blink/__init__.py
+++ b/homeassistant/components/blink/__init__.py
@@ -16,6 +16,7 @@
SERVICE_SAVE_VIDEO,
SERVICE_SEND_PIN,
)
+from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.const import CONF_FILENAME, CONF_NAME, CONF_PIN, CONF_SCAN_INTERVAL
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
@@ -49,7 +50,7 @@ def _reauth_flow_wrapper(hass, data):
"""Reauth flow wrapper."""
hass.add_job(
hass.config_entries.flow.async_init(
- DOMAIN, context={"source": "reauth"}, data=data
+ DOMAIN, context={"source": SOURCE_REAUTH}, data=data
)
)
persistent_notification.async_create(
@@ -59,26 +60,25 @@ def _reauth_flow_wrapper(hass, data):
)
-async def async_setup(hass, config):
- """Set up a Blink component."""
- hass.data[DOMAIN] = {}
- return True
-
-
async def async_migrate_entry(hass, entry):
"""Handle migration of a previous version config entry."""
+ _LOGGER.debug("Migrating from version %s", entry.version)
data = {**entry.data}
if entry.version == 1:
data.pop("login_response", None)
await hass.async_add_executor_job(_reauth_flow_wrapper, hass, data)
return False
+ if entry.version == 2:
+ await hass.async_add_executor_job(_reauth_flow_wrapper, hass, data)
+ return False
return True
async def async_setup_entry(hass, entry):
"""Set up Blink via config entry."""
- _async_import_options_from_data_if_missing(hass, entry)
+ hass.data.setdefault(DOMAIN, {})
+ _async_import_options_from_data_if_missing(hass, entry)
hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job(
_blink_startup_wrapper, hass, entry
)
@@ -86,9 +86,9 @@ async def async_setup_entry(hass, entry):
if not hass.data[DOMAIN][entry.entry_id].available:
raise ConfigEntryNotReady
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
def blink_refresh(event_time=None):
@@ -133,8 +133,8 @@ async def async_unload_entry(hass, entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -148,7 +148,7 @@ async def async_unload_entry(hass, entry):
return True
hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
- hass.services.async_remove(DOMAIN, SERVICE_SAVE_VIDEO_SCHEMA)
+ hass.services.async_remove(DOMAIN, SERVICE_SAVE_VIDEO)
hass.services.async_remove(DOMAIN, SERVICE_SEND_PIN)
return True
diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py
index dbcb6d30143a2e..ed2b46acaa1b21 100644
--- a/homeassistant/components/blink/alarm_control_panel.py
+++ b/homeassistant/components/blink/alarm_control_panel.py
@@ -62,7 +62,7 @@ def name(self):
return f"{DOMAIN} {self._name}"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attr = self.sync.attributes
attr["network_info"] = self.data.networks
diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py
index d4282bed606964..a25b978ee7b012 100644
--- a/homeassistant/components/blink/camera.py
+++ b/homeassistant/components/blink/camera.py
@@ -60,7 +60,7 @@ def unique_id(self):
return self._unique_id
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the camera attributes."""
return self._camera.attributes
diff --git a/homeassistant/components/blink/config_flow.py b/homeassistant/components/blink/config_flow.py
index 5c77add31185e4..c6c3f0b27bee63 100644
--- a/homeassistant/components/blink/config_flow.py
+++ b/homeassistant/components/blink/config_flow.py
@@ -44,7 +44,7 @@ def _send_blink_2fa_pin(auth, pin):
class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a Blink config flow."""
- VERSION = 2
+ VERSION = 3
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json
index 17d737bcaf3418..c88e13cdde717f 100644
--- a/homeassistant/components/blink/manifest.json
+++ b/homeassistant/components/blink/manifest.json
@@ -2,7 +2,8 @@
"domain": "blink",
"name": "Blink",
"documentation": "https://www.home-assistant.io/integrations/blink",
- "requirements": ["blinkpy==0.16.4"],
+ "requirements": ["blinkpy==0.17.0"],
"codeowners": ["@fronzbot"],
+ "dhcp": [{"hostname":"blink*","macaddress":"B85F98*"}],
"config_flow": true
}
diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py
index 3c3adf6d990e6d..1ec61900091d42 100644
--- a/homeassistant/components/blink/sensor.py
+++ b/homeassistant/components/blink/sensor.py
@@ -1,13 +1,13 @@
"""Support for Blink system camera sensors."""
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_TEMPERATURE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
TEMP_FAHRENHEIT,
)
-from homeassistant.helpers.entity import Entity
from .const import DOMAIN, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH
@@ -34,7 +34,7 @@ async def async_setup_entry(hass, config, async_add_entities):
async_add_entities(entities)
-class BlinkSensor(Entity):
+class BlinkSensor(SensorEntity):
"""A Blink camera sensor."""
def __init__(self, data, camera, sensor_type):
diff --git a/homeassistant/components/blink/services.yaml b/homeassistant/components/blink/services.yaml
index dc6491e2139393..6ea4e2aa9ac029 100644
--- a/homeassistant/components/blink/services.yaml
+++ b/homeassistant/components/blink/services.yaml
@@ -21,8 +21,8 @@ save_video:
example: "/tmp/video.mp4"
send_pin:
- description: Send a new pin to blink for 2FA.
+ description: Send a new PIN to blink for 2FA.
fields:
pin:
- description: Pin received from blink. Leave empty if you only received a verification email.
+ description: PIN received from blink. Leave empty if you only received a verification email.
example: "abc123"
diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json
index db9bdf96273a10..6e438b585906c8 100644
--- a/homeassistant/components/blink/strings.json
+++ b/homeassistant/components/blink/strings.json
@@ -11,7 +11,7 @@
"2fa": {
"title": "Two-factor authentication",
"data": { "2fa": "Two-factor code" },
- "description": "Enter the pin sent to your email"
+ "description": "Enter the PIN sent to your email"
}
},
"error": {
diff --git a/homeassistant/components/blink/translations/de.json b/homeassistant/components/blink/translations/de.json
index f5116110a09896..86fa2b609ad6b3 100644
--- a/homeassistant/components/blink/translations/de.json
+++ b/homeassistant/components/blink/translations/de.json
@@ -4,7 +4,8 @@
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
@@ -13,7 +14,7 @@
"data": {
"2fa": "Zwei-Faktor Authentifizierungscode"
},
- "description": "Geben Sie die an Ihre E-Mail gesendete Pin ein. Wenn die E-Mail keine PIN enth\u00e4lt, lassen Sie das Feld leer.",
+ "description": "Gib die an deine E-Mail gesendete Pin ein. Wenn die E-Mail keine PIN enth\u00e4lt, lass das Feld leer.",
"title": "Zwei-Faktor-Authentifizierung"
},
"user": {
@@ -24,5 +25,16 @@
"title": "Anmelden mit Blink-Konto"
}
}
+ },
+ "options": {
+ "step": {
+ "simple_options": {
+ "data": {
+ "scan_interval": "Scanintervall (Sekunden)"
+ },
+ "description": "Blink-Integration konfigurieren",
+ "title": "Blink Optionen"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/blink/translations/en.json b/homeassistant/components/blink/translations/en.json
index 9a0a2636d3e900..c8c154418dffd0 100644
--- a/homeassistant/components/blink/translations/en.json
+++ b/homeassistant/components/blink/translations/en.json
@@ -14,7 +14,7 @@
"data": {
"2fa": "Two-factor code"
},
- "description": "Enter the pin sent to your email",
+ "description": "Enter the PIN sent to your email",
"title": "Two-factor authentication"
},
"user": {
diff --git a/homeassistant/components/blink/translations/et.json b/homeassistant/components/blink/translations/et.json
index 24de0ccbefd2a2..a5cae0eaae2164 100644
--- a/homeassistant/components/blink/translations/et.json
+++ b/homeassistant/components/blink/translations/et.json
@@ -14,7 +14,7 @@
"data": {
"2fa": "2FA kood"
},
- "description": "Sisesta E-posti aadressile saadetud PIN-kood",
+ "description": "Sisesta e-posti aadressile saadetud PIN kood",
"title": "Kaheastmeline tuvastamine (2FA)"
},
"user": {
diff --git a/homeassistant/components/blink/translations/fr.json b/homeassistant/components/blink/translations/fr.json
index 83aaad902a151f..23bb7fb91dd8f6 100644
--- a/homeassistant/components/blink/translations/fr.json
+++ b/homeassistant/components/blink/translations/fr.json
@@ -14,7 +14,7 @@
"data": {
"2fa": "Code \u00e0 deux facteurs"
},
- "description": "Entrez le code PIN envoy\u00e9 \u00e0 votre e-mail",
+ "description": "Entrez le NIP envoy\u00e9 \u00e0 votre e-mail",
"title": "Authentification \u00e0 deux facteurs"
},
"user": {
diff --git a/homeassistant/components/blink/translations/hu.json b/homeassistant/components/blink/translations/hu.json
index 1150cda9ea93e5..e56b142a5b03d2 100644
--- a/homeassistant/components/blink/translations/hu.json
+++ b/homeassistant/components/blink/translations/hu.json
@@ -4,10 +4,19 @@
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
},
"error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token",
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
- "unknown": "V\u00e1ratlan hiba"
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"step": {
+ "2fa": {
+ "data": {
+ "2fa": "K\u00e9tfaktoros k\u00f3d"
+ },
+ "description": "Add meg az e-mail c\u00edmedre k\u00fcld\u00f6tt pint",
+ "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s"
+ },
"user": {
"data": {
"password": "Jelsz\u00f3",
diff --git a/homeassistant/components/blink/translations/id.json b/homeassistant/components/blink/translations/id.json
new file mode 100644
index 00000000000000..bdbc406bda7001
--- /dev/null
+++ b/homeassistant/components/blink/translations/id.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_access_token": "Token akses tidak valid",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "Kode autentikasi dua faktor"
+ },
+ "description": "Masukkan PIN yang dikirimkan ke email Anda",
+ "title": "Autentikasi dua faktor"
+ },
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "title": "Masuk dengan akun Blink"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "simple_options": {
+ "data": {
+ "scan_interval": "Interval Pindai (detik)"
+ },
+ "description": "Konfigurasikan integrasi Blink",
+ "title": "Opsi Blink"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/blink/translations/it.json b/homeassistant/components/blink/translations/it.json
index bdb0ba3f6b4350..6dee5d9c02fe52 100644
--- a/homeassistant/components/blink/translations/it.json
+++ b/homeassistant/components/blink/translations/it.json
@@ -14,7 +14,7 @@
"data": {
"2fa": "Codice a due fattori"
},
- "description": "Inserisci il pin inviato alla tua email",
+ "description": "Inserisci il PIN inviato alla tua email",
"title": "Autenticazione a due fattori"
},
"user": {
diff --git a/homeassistant/components/blink/translations/ko.json b/homeassistant/components/blink/translations/ko.json
index ac8c96e4f2d7a5..6d42cdbb2e921e 100644
--- a/homeassistant/components/blink/translations/ko.json
+++ b/homeassistant/components/blink/translations/ko.json
@@ -4,8 +4,8 @@
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
- "invalid_access_token": "\uc798\ubabb\ub41c \uc778\uc99d",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
@@ -14,7 +14,7 @@
"data": {
"2fa": "2\ub2e8\uacc4 \uc778\uc99d \ucf54\ub4dc"
},
- "description": "\uc774\uba54\uc77c\ub85c \ubcf4\ub0b4\ub4dc\ub9b0 PIN \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "description": "\uc774\uba54\uc77c\ub85c \ubcf4\ub0b4\ub4dc\ub9b0 PIN\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694",
"title": "2\ub2e8\uacc4 \uc778\uc99d"
},
"user": {
diff --git a/homeassistant/components/blink/translations/nl.json b/homeassistant/components/blink/translations/nl.json
index c1ab971dbf04f8..bce18bfda47561 100644
--- a/homeassistant/components/blink/translations/nl.json
+++ b/homeassistant/components/blink/translations/nl.json
@@ -4,6 +4,8 @@
"already_configured": "Apparaat is al geconfigureerd"
},
"error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_access_token": "Ongeldig toegangstoken",
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
@@ -12,7 +14,7 @@
"data": {
"2fa": "Twee-factor code"
},
- "description": "Voer de pincode in die naar uw e-mail is gestuurd. Als de e-mail geen pincode bevat, laat u dit leeg",
+ "description": "Voer de pincode in die naar uw e-mail is gestuurd.",
"title": "Tweestapsverificatie"
},
"user": {
@@ -23,5 +25,16 @@
"title": "Aanmelden met Blink account"
}
}
+ },
+ "options": {
+ "step": {
+ "simple_options": {
+ "data": {
+ "scan_interval": "Scaninterval (seconden)"
+ },
+ "description": "Configureer Blink-integratie",
+ "title": "Blink opties"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/blink/translations/no.json b/homeassistant/components/blink/translations/no.json
index 0b99005c382ed1..90f8fcaa06bfb5 100644
--- a/homeassistant/components/blink/translations/no.json
+++ b/homeassistant/components/blink/translations/no.json
@@ -14,7 +14,7 @@
"data": {
"2fa": "Totrinnsbekreftelse kode"
},
- "description": "Skriv inn pin-koden som ble sendt til din e-posten",
+ "description": "Skriv inn PIN-koden som er sendt til e-posten din",
"title": "Totrinnsbekreftelse"
},
"user": {
diff --git a/homeassistant/components/blink/translations/pl.json b/homeassistant/components/blink/translations/pl.json
index 13fe2f1fc934b0..72b6c32e5be748 100644
--- a/homeassistant/components/blink/translations/pl.json
+++ b/homeassistant/components/blink/translations/pl.json
@@ -14,7 +14,7 @@
"data": {
"2fa": "Kod uwierzytelniania dwusk\u0142adnikowego"
},
- "description": "Wpisz kod PIN wys\u0142any na Tw\u00f3j adres e-mail. Je\u015bli.",
+ "description": "Wprowad\u017a kod PIN wys\u0142any na Tw\u00f3j adres e-mail.",
"title": "Uwierzytelnianie dwusk\u0142adnikowe"
},
"user": {
diff --git a/homeassistant/components/blink/translations/ru.json b/homeassistant/components/blink/translations/ru.json
index 0e55fa716b92a7..fa68ee2dad46fa 100644
--- a/homeassistant/components/blink/translations/ru.json
+++ b/homeassistant/components/blink/translations/ru.json
@@ -6,7 +6,7 @@
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
@@ -20,7 +20,7 @@
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "Blink"
}
diff --git a/homeassistant/components/blink/translations/tr.json b/homeassistant/components/blink/translations/tr.json
new file mode 100644
index 00000000000000..8193ff9d8bee78
--- /dev/null
+++ b/homeassistant/components/blink/translations/tr.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "2fa": {
+ "description": "E-postan\u0131za g\u00f6nderilen PIN kodunu girin"
+ },
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/blink/translations/uk.json b/homeassistant/components/blink/translations/uk.json
new file mode 100644
index 00000000000000..c45bf7b66517e2
--- /dev/null
+++ b/homeassistant/components/blink/translations/uk.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_access_token": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443.",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "\u041a\u043e\u0434 \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434, \u043d\u0430\u0434\u0456\u0441\u043b\u0430\u043d\u0438\u0439 \u043d\u0430 \u0412\u0430\u0448\u0443 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0443 \u043f\u043e\u0448\u0442\u0443",
+ "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f"
+ },
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "Blink"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "simple_options": {
+ "data": {
+ "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 Blink",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Blink"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/blink/translations/zh-Hant.json b/homeassistant/components/blink/translations/zh-Hant.json
index 3d05dc82abcbbb..6874efb6e316c9 100644
--- a/homeassistant/components/blink/translations/zh-Hant.json
+++ b/homeassistant/components/blink/translations/zh-Hant.json
@@ -5,17 +5,17 @@
},
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557",
- "invalid_access_token": "\u5b58\u53d6\u5bc6\u9470\u7121\u6548",
+ "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"step": {
"2fa": {
"data": {
- "2fa": "\u96d9\u91cd\u9a57\u8b49\u78bc"
+ "2fa": "\u96d9\u91cd\u8a8d\u8b49\u78bc"
},
"description": "\u8f38\u5165\u90f5\u4ef6\u6240\u6536\u5230 PIN \u78bc",
- "title": "\u96d9\u91cd\u9a57\u8b49"
+ "title": "\u96d9\u91cd\u8a8d\u8b49"
},
"user": {
"data": {
diff --git a/homeassistant/components/blinkt/light.py b/homeassistant/components/blinkt/light.py
index 0d1aff7b8260f6..bb9bbf315e49ed 100644
--- a/homeassistant/components/blinkt/light.py
+++ b/homeassistant/components/blinkt/light.py
@@ -26,7 +26,6 @@
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Blinkt Light platform."""
- # pylint: disable=no-member
blinkt = importlib.import_module("blinkt")
# ensure that the lights are off when exiting
diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py
index feb9d582cff0f9..3ecf4bee3190a1 100644
--- a/homeassistant/components/blockchain/sensor.py
+++ b/homeassistant/components/blockchain/sensor.py
@@ -5,10 +5,9 @@
from pyblockchain import get_balance, validate_address
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -44,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([BlockchainSensor(name, addresses)], True)
-class BlockchainSensor(Entity):
+class BlockchainSensor(SensorEntity):
"""Representation of a Blockchain.com sensor."""
def __init__(self, name, addresses):
@@ -75,7 +74,7 @@ def icon(self):
return ICON
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py
index cd993e0332a366..fa8d3160dc8d51 100644
--- a/homeassistant/components/bloomsky/__init__.py
+++ b/homeassistant/components/bloomsky/__init__.py
@@ -18,7 +18,7 @@
_LOGGER = logging.getLogger(__name__)
-BLOOMSKY_TYPE = ["camera", "binary_sensor", "sensor"]
+PLATFORMS = ["camera", "binary_sensor", "sensor"]
DOMAIN = "bloomsky"
@@ -32,7 +32,7 @@
def setup(hass, config):
- """Set up the BloomSky component."""
+ """Set up the BloomSky integration."""
api_key = config[DOMAIN][CONF_API_KEY]
try:
@@ -42,8 +42,8 @@ def setup(hass, config):
hass.data[DOMAIN] = bloomsky
- for component in BLOOMSKY_TYPE:
- discovery.load_platform(hass, component, DOMAIN, {}, config)
+ for platform in PLATFORMS:
+ discovery.load_platform(hass, platform, DOMAIN, {}, config)
return True
@@ -60,7 +60,7 @@ def __init__(self, api_key, is_metric):
self._endpoint_argument = "unit=intl" if is_metric else ""
self.devices = {}
self.is_metric = is_metric
- _LOGGER.debug("Initial BloomSky device load...")
+ _LOGGER.debug("Initial BloomSky device load")
self.refresh_devices()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py
index df06e39db7e96e..4dc52e1a85cf5f 100644
--- a/homeassistant/components/bloomsky/sensor.py
+++ b/homeassistant/components/bloomsky/sensor.py
@@ -1,7 +1,7 @@
"""Support the sensor of a BloomSky weather station."""
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
AREA_SQUARE_METERS,
CONF_MONITORED_CONDITIONS,
@@ -12,7 +12,6 @@
TEMP_FAHRENHEIT,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from . import DOMAIN
@@ -70,7 +69,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([BloomSkySensor(bloomsky, device, variable)], True)
-class BloomSkySensor(Entity):
+class BloomSkySensor(SensorEntity):
"""Representation of a single sensor in a BloomSky device."""
def __init__(self, bs, device, sensor_name):
diff --git a/homeassistant/components/blueprint/__init__.py b/homeassistant/components/blueprint/__init__.py
index 9e8b1260eff6b7..309365710ad394 100644
--- a/homeassistant/components/blueprint/__init__.py
+++ b/homeassistant/components/blueprint/__init__.py
@@ -1,7 +1,7 @@
"""The blueprint integration."""
from . import websocket_api
-from .const import DOMAIN # noqa
-from .errors import ( # noqa
+from .const import DOMAIN # noqa: F401
+from .errors import ( # noqa: F401
BlueprintException,
BlueprintWithNameException,
FailedToLoad,
@@ -9,8 +9,8 @@
InvalidBlueprintInputs,
MissingInput,
)
-from .models import Blueprint, BlueprintInputs, DomainBlueprints # noqa
-from .schemas import is_blueprint_instance_config # noqa
+from .models import Blueprint, BlueprintInputs, DomainBlueprints # noqa: F401
+from .schemas import is_blueprint_instance_config # noqa: F401
async def async_setup(hass, config):
diff --git a/homeassistant/components/blueprint/const.py b/homeassistant/components/blueprint/const.py
index 60df20dda36107..a91d30199c90be 100644
--- a/homeassistant/components/blueprint/const.py
+++ b/homeassistant/components/blueprint/const.py
@@ -5,7 +5,6 @@
CONF_USE_BLUEPRINT = "use_blueprint"
CONF_INPUT = "input"
CONF_SOURCE_URL = "source_url"
-CONF_DESCRIPTION = "description"
CONF_HOMEASSISTANT = "homeassistant"
CONF_MIN_VERSION = "min_version"
diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py
index 217851df980ade..99dffb114e1149 100644
--- a/homeassistant/components/blueprint/importer.py
+++ b/homeassistant/components/blueprint/importer.py
@@ -1,8 +1,9 @@
"""Import logic for blueprint."""
+from __future__ import annotations
+
from dataclasses import dataclass
import html
import re
-from typing import Optional
import voluptuous as vol
import yarl
@@ -93,7 +94,7 @@ def _get_community_post_import_url(url: str) -> str:
def _extract_blueprint_from_community_topic(
url: str,
topic: dict,
-) -> Optional[ImportedBlueprint]:
+) -> ImportedBlueprint | None:
"""Extract a blueprint from a community post JSON.
Async friendly.
@@ -136,7 +137,7 @@ def _extract_blueprint_from_community_topic(
async def fetch_blueprint_from_community_post(
hass: HomeAssistant, url: str
-) -> Optional[ImportedBlueprint]:
+) -> ImportedBlueprint | None:
"""Get blueprints from a community post url.
Method can raise aiohttp client exceptions, vol.Invalid.
diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py
index 32fc30b60b97be..797f9bd1512b95 100644
--- a/homeassistant/components/blueprint/models.py
+++ b/homeassistant/components/blueprint/models.py
@@ -1,11 +1,13 @@
"""Blueprint models."""
+from __future__ import annotations
+
import asyncio
import logging
import pathlib
import shutil
-from typing import Any, Dict, List, Optional, Union
+from typing import Any
-from pkg_resources import parse_version
+from awesomeversion import AwesomeVersion
import voluptuous as vol
from voluptuous.humanize import humanize_error
@@ -49,8 +51,8 @@ def __init__(
self,
data: dict,
*,
- path: Optional[str] = None,
- expected_domain: Optional[str] = None,
+ path: str | None = None,
+ expected_domain: str | None = None,
) -> None:
"""Initialize a blueprint."""
try:
@@ -95,7 +97,7 @@ def metadata(self) -> dict:
"""Return blueprint metadata."""
return self.data[CONF_BLUEPRINT]
- def update_metadata(self, *, source_url: Optional[str] = None) -> None:
+ def update_metadata(self, *, source_url: str | None = None) -> None:
"""Update metadata."""
if source_url is not None:
self.data[CONF_BLUEPRINT][CONF_SOURCE_URL] = source_url
@@ -105,7 +107,7 @@ def yaml(self) -> str:
return yaml.dump(self.data)
@callback
- def validate(self) -> Optional[List[str]]:
+ def validate(self) -> list[str] | None:
"""Test if the Home Assistant installation supports this blueprint.
Return list of errors if not valid.
@@ -114,7 +116,7 @@ def validate(self) -> Optional[List[str]]:
metadata = self.metadata
min_version = metadata.get(CONF_HOMEASSISTANT, {}).get(CONF_MIN_VERSION)
- if min_version is not None and parse_version(__version__) < parse_version(
+ if min_version is not None and AwesomeVersion(__version__) < AwesomeVersion(
min_version
):
errors.append(f"Requires at least Home Assistant {min_version}")
@@ -126,7 +128,7 @@ class BlueprintInputs:
"""Inputs for a blueprint."""
def __init__(
- self, blueprint: Blueprint, config_with_inputs: Dict[str, Any]
+ self, blueprint: Blueprint, config_with_inputs: dict[str, Any]
) -> None:
"""Instantiate a blueprint inputs object."""
self.blueprint = blueprint
@@ -218,7 +220,7 @@ def _load_blueprint(self, blueprint_path) -> Blueprint:
blueprint_data, expected_domain=self.domain, path=blueprint_path
)
- def _load_blueprints(self) -> Dict[str, Union[Blueprint, BlueprintException]]:
+ def _load_blueprints(self) -> dict[str, Blueprint | BlueprintException]:
"""Load all the blueprints."""
blueprint_folder = pathlib.Path(
self.hass.config.path(BLUEPRINT_FOLDER, self.domain)
@@ -243,7 +245,7 @@ def _load_blueprints(self) -> Dict[str, Union[Blueprint, BlueprintException]]:
async def async_get_blueprints(
self,
- ) -> Dict[str, Union[Blueprint, BlueprintException]]:
+ ) -> dict[str, Blueprint | BlueprintException]:
"""Get all the blueprints."""
async with self._load_lock:
return await self.hass.async_add_executor_job(self._load_blueprints)
diff --git a/homeassistant/components/blueprint/schemas.py b/homeassistant/components/blueprint/schemas.py
index 07d8e8b0128397..f16598afac2763 100644
--- a/homeassistant/components/blueprint/schemas.py
+++ b/homeassistant/components/blueprint/schemas.py
@@ -5,6 +5,7 @@
from homeassistant.const import (
CONF_DEFAULT,
+ CONF_DESCRIPTION,
CONF_DOMAIN,
CONF_NAME,
CONF_PATH,
@@ -15,7 +16,6 @@
from .const import (
CONF_BLUEPRINT,
- CONF_DESCRIPTION,
CONF_HOMEASSISTANT,
CONF_INPUT,
CONF_MIN_VERSION,
diff --git a/homeassistant/components/blueprint/websocket_api.py b/homeassistant/components/blueprint/websocket_api.py
index 6968d4530cdf35..b8a4c214a2e5d4 100644
--- a/homeassistant/components/blueprint/websocket_api.py
+++ b/homeassistant/components/blueprint/websocket_api.py
@@ -1,6 +1,5 @@
"""Websocket API for blueprint."""
-import logging
-from typing import Dict, Optional
+from __future__ import annotations
import async_timeout
import voluptuous as vol
@@ -15,8 +14,6 @@
from .const import DOMAIN
from .errors import FileAlreadyExists
-_LOGGER = logging.getLogger(__package__)
-
@callback
def async_setup(hass: HomeAssistant):
@@ -36,7 +33,7 @@ def async_setup(hass: HomeAssistant):
)
async def ws_list_blueprints(hass, connection, msg):
"""List available blueprints."""
- domain_blueprints: Optional[Dict[str, models.DomainBlueprints]] = hass.data.get(
+ domain_blueprints: dict[str, models.DomainBlueprints] | None = hass.data.get(
DOMAIN, {}
)
results = {}
@@ -105,7 +102,7 @@ async def ws_save_blueprint(hass, connection, msg):
path = msg["path"]
domain = msg["domain"]
- domain_blueprints: Optional[Dict[str, models.DomainBlueprints]] = hass.data.get(
+ domain_blueprints: dict[str, models.DomainBlueprints] | None = hass.data.get(
DOMAIN, {}
)
@@ -152,7 +149,7 @@ async def ws_delete_blueprint(hass, connection, msg):
path = msg["path"]
domain = msg["domain"]
- domain_blueprints: Optional[Dict[str, models.DomainBlueprints]] = hass.data.get(
+ domain_blueprints: dict[str, models.DomainBlueprints] | None = hass.data.get(
DOMAIN, {}
)
diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py
index 0ace6f5679a242..dff45ca68bd701 100644
--- a/homeassistant/components/bluesound/media_player.py
+++ b/homeassistant/components/bluesound/media_player.py
@@ -850,7 +850,7 @@ async def async_join(self, master):
_LOGGER.error("Master not found %s", master_device)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""List members in group."""
attributes = {}
if self._group_list:
diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py
index 76df4e65ac7e98..9ac79afde2cfbc 100644
--- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py
+++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py
@@ -4,7 +4,7 @@
import logging
from uuid import UUID
-import pygatt # pylint: disable=import-error
+import pygatt
import voluptuous as vol
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py
index af49266bef4b0d..f00bd672892c03 100644
--- a/homeassistant/components/bluetooth_tracker/device_tracker.py
+++ b/homeassistant/components/bluetooth_tracker/device_tracker.py
@@ -1,10 +1,10 @@
"""Tracking for bluetooth devices."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import List, Optional, Set, Tuple
-# pylint: disable=import-error
-import bluetooth
+import bluetooth # pylint: disable=import-error
from bt_proximity import BluetoothRSSI
import voluptuous as vol
@@ -20,6 +20,7 @@
YAML_DEVICES,
async_load_config,
)
+from homeassistant.const import CONF_DEVICE_ID
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import HomeAssistantType
@@ -32,8 +33,6 @@
CONF_REQUEST_RSSI = "request_rssi"
-CONF_DEVICE_ID = "device_id"
-
DEFAULT_DEVICE_ID = -1
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
@@ -52,7 +51,7 @@ def is_bluetooth_device(device) -> bool:
return device.mac and device.mac[:3].upper() == BT_PREFIX
-def discover_devices(device_id: int) -> List[Tuple[str, str]]:
+def discover_devices(device_id: int) -> list[tuple[str, str]]:
"""Discover Bluetooth devices."""
result = bluetooth.discover_devices(
duration=8,
@@ -81,7 +80,7 @@ async def see_device(
)
-async def get_tracking_devices(hass: HomeAssistantType) -> Tuple[Set[str], Set[str]]:
+async def get_tracking_devices(hass: HomeAssistantType) -> tuple[set[str], set[str]]:
"""
Load all known devices.
@@ -92,17 +91,17 @@ async def get_tracking_devices(hass: HomeAssistantType) -> Tuple[Set[str], Set[s
devices = await async_load_config(yaml_path, hass, 0)
bluetooth_devices = [device for device in devices if is_bluetooth_device(device)]
- devices_to_track: Set[str] = {
+ devices_to_track: set[str] = {
device.mac[3:] for device in bluetooth_devices if device.track
}
- devices_to_not_track: Set[str] = {
+ devices_to_not_track: set[str] = {
device.mac[3:] for device in bluetooth_devices if not device.track
}
return devices_to_track, devices_to_not_track
-def lookup_name(mac: str) -> Optional[str]:
+def lookup_name(mac: str) -> str | None:
"""Lookup a Bluetooth device name."""
_LOGGER.debug("Scanning %s", mac)
return bluetooth.lookup_name(mac, timeout=5)
@@ -131,7 +130,6 @@ async def async_setup_scanner(
async def perform_bluetooth_update():
"""Discover Bluetooth devices and update status."""
-
_LOGGER.debug("Performing Bluetooth devices discovery and update")
tasks = []
@@ -164,7 +162,6 @@ async def perform_bluetooth_update():
async def update_bluetooth(now=None):
"""Lookup Bluetooth devices and update status."""
-
# If an update is in progress, we don't do anything
if update_bluetooth_lock.locked():
_LOGGER.debug(
@@ -178,7 +175,6 @@ async def update_bluetooth(now=None):
async def handle_manual_update_bluetooth(call):
"""Update bluetooth devices on demand."""
-
await update_bluetooth()
hass.async_create_task(update_bluetooth())
diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py
index 265ec01b6dbc8b..2c3ab0303b05b8 100644
--- a/homeassistant/components/bme280/sensor.py
+++ b/homeassistant/components/bme280/sensor.py
@@ -1,13 +1,14 @@
"""Support for BME280 temperature, humidity and pressure sensor."""
+from contextlib import suppress
from datetime import timedelta
from functools import partial
import logging
from i2csense.bme280 import BME280 # pylint: disable=import-error
-import smbus # pylint: disable=import-error
+import smbus
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_MONITORED_CONDITIONS,
CONF_NAME,
@@ -15,7 +16,6 @@
TEMP_FAHRENHEIT,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from homeassistant.util.temperature import celsius_to_fahrenheit
@@ -111,13 +111,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
sensor_handler = await hass.async_add_executor_job(BME280Handler, sensor)
dev = []
- try:
+ with suppress(KeyError):
for variable in config[CONF_MONITORED_CONDITIONS]:
dev.append(
BME280Sensor(sensor_handler, variable, SENSOR_TYPES[variable][1], name)
)
- except KeyError:
- pass
async_add_entities(dev, True)
@@ -136,7 +134,7 @@ def update(self, first_reading=False):
self.sensor.update(first_reading)
-class BME280Sensor(Entity):
+class BME280Sensor(SensorEntity):
"""Implementation of the BME280 sensor."""
def __init__(self, bme280_client, sensor_type, temp_unit, name):
diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py
index 49d95bbc53be7d..f3d6b9428ea359 100644
--- a/homeassistant/components/bme680/sensor.py
+++ b/homeassistant/components/bme680/sensor.py
@@ -4,10 +4,10 @@
from time import monotonic, sleep
import bme680 # pylint: disable=import-error
-from smbus import SMBus # pylint: disable=import-error
+from smbus import SMBus
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_MONITORED_CONDITIONS,
CONF_NAME,
@@ -15,7 +15,6 @@
TEMP_FAHRENHEIT,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util.temperature import celsius_to_fahrenheit
_LOGGER = logging.getLogger(__name__)
@@ -131,7 +130,6 @@ def _setup_bme680(config):
sensor_handler = None
sensor = None
try:
- # pylint: disable=no-member
i2c_address = config[CONF_I2C_ADDRESS]
bus = SMBus(config[CONF_I2C_BUS])
sensor = bme680.BME680(i2c_address, bus)
@@ -317,7 +315,7 @@ def _calculate_aq_score(self):
return hum_score + gas_score
-class BME680Sensor(Entity):
+class BME680Sensor(SensorEntity):
"""Implementation of the BME680 sensor."""
def __init__(self, bme680_client, sensor_type, temp_unit, name):
diff --git a/homeassistant/components/bmp280/manifest.json b/homeassistant/components/bmp280/manifest.json
index dbd7989671896e..e22c275ed76ac6 100644
--- a/homeassistant/components/bmp280/manifest.json
+++ b/homeassistant/components/bmp280/manifest.json
@@ -3,6 +3,6 @@
"name": "Bosch BMP280 Environmental Sensor",
"documentation": "https://www.home-assistant.io/integrations/bmp280",
"codeowners": ["@belidzs"],
- "requirements": ["adafruit-circuitpython-bmp280==3.1.1", "RPi.GPIO==0.7.0"],
+ "requirements": ["adafruit-circuitpython-bmp280==3.1.1", "RPi.GPIO==0.7.1a4"],
"quality_scale": "silver"
}
diff --git a/homeassistant/components/bmp280/sensor.py b/homeassistant/components/bmp280/sensor.py
index 3c34408cb62662..60cbdb75d41fef 100644
--- a/homeassistant/components/bmp280/sensor.py
+++ b/homeassistant/components/bmp280/sensor.py
@@ -11,11 +11,11 @@
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
PLATFORM_SCHEMA,
+ SensorEntity,
)
from homeassistant.const import CONF_NAME, PRESSURE_HPA, TEMP_CELSIUS
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -65,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
)
-class Bmp280Sensor(Entity):
+class Bmp280Sensor(SensorEntity):
"""Base class for BMP280 entities."""
def __init__(
diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py
index e9f6a0d7f6f68f..ebf1fd6f74ea82 100644
--- a/homeassistant/components/bmw_connected_drive/__init__.py
+++ b/homeassistant/components/bmw_connected_drive/__init__.py
@@ -1,4 +1,6 @@
"""Reads vehicle status from BMW connected drive portal."""
+from __future__ import annotations
+
import asyncio
import logging
@@ -12,6 +14,7 @@
ATTR_ATTRIBUTION,
CONF_NAME,
CONF_PASSWORD,
+ CONF_REGION,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant, callback
@@ -28,7 +31,6 @@
CONF_ACCOUNT,
CONF_ALLOWED_REGIONS,
CONF_READ_ONLY,
- CONF_REGION,
CONF_USE_LOCATION,
DATA_ENTRIES,
DATA_HASS_CONFIG,
@@ -57,7 +59,7 @@
CONF_USE_LOCATION: False,
}
-BMW_PLATFORMS = ["binary_sensor", "device_tracker", "lock", "notify", "sensor"]
+PLATFORMS = ["binary_sensor", "device_tracker", "lock", "notify", "sensor"]
UPDATE_INTERVAL = 5 # in minutes
SERVICE_UPDATE_STATE = "update_state"
@@ -120,7 +122,7 @@ async def _async_update_all(service_call=None):
def _update_all() -> None:
"""Update all BMW accounts."""
- for entry in hass.data[DOMAIN][DATA_ENTRIES].values():
+ for entry in hass.data[DOMAIN][DATA_ENTRIES].copy().values():
entry[CONF_ACCOUNT].update()
# Add update listener for config entry changes (options)
@@ -136,13 +138,13 @@ def _update_all() -> None:
await _async_update_all()
- for platform in BMW_PLATFORMS:
+ for platform in PLATFORMS:
if platform != NOTIFY_DOMAIN:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)
- # set up notify platform, no entry support for notify component yet,
+ # set up notify platform, no entry support for notify platform yet,
# have to use discovery to load platform.
hass.async_create_task(
discovery.async_load_platform(
@@ -162,9 +164,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in BMW_PLATFORMS
- if component != NOTIFY_DOMAIN
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ if platform != NOTIFY_DOMAIN
]
)
)
@@ -195,7 +197,7 @@ async def update_listener(hass, config_entry):
await hass.config_entries.async_reload(config_entry.entry_id)
-def setup_account(entry: ConfigEntry, hass, name: str) -> "BMWConnectedDriveAccount":
+def setup_account(entry: ConfigEntry, hass, name: str) -> BMWConnectedDriveAccount:
"""Set up a new BMWConnectedDriveAccount based on the config."""
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
@@ -332,7 +334,7 @@ def device_info(self) -> dict:
}
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return self._attrs
diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py
index cad5426d548345..bebb55bbde0845 100644
--- a/homeassistant/components/bmw_connected_drive/binary_sensor.py
+++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py
@@ -109,7 +109,7 @@ def is_on(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the binary sensor."""
vehicle_state = self._vehicle.state
result = self._attrs.copy()
diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py
index a6081d5ccc1cca..7798dc4e7e8e65 100644
--- a/homeassistant/components/bmw_connected_drive/config_flow.py
+++ b/homeassistant/components/bmw_connected_drive/config_flow.py
@@ -1,19 +1,14 @@
"""Config flow for BMW ConnectedDrive integration."""
-import logging
-
from bimmer_connected.account import ConnectedDriveAccount
from bimmer_connected.country_selector import get_region_from_name
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
-from homeassistant.const import CONF_PASSWORD, CONF_SOURCE, CONF_USERNAME
+from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_SOURCE, CONF_USERNAME
from homeassistant.core import callback
-from . import DOMAIN # pylint: disable=unused-import
-from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY, CONF_REGION, CONF_USE_LOCATION
-
-_LOGGER = logging.getLogger(__name__)
-
+from . import DOMAIN
+from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY, CONF_USE_LOCATION
DATA_SCHEMA = vol.Schema(
{
diff --git a/homeassistant/components/bmw_connected_drive/const.py b/homeassistant/components/bmw_connected_drive/const.py
index 65dc7fde595910..7af24496838c54 100644
--- a/homeassistant/components/bmw_connected_drive/const.py
+++ b/homeassistant/components/bmw_connected_drive/const.py
@@ -1,7 +1,6 @@
"""Const file for the BMW Connected Drive integration."""
ATTRIBUTION = "Data provided by BMW Connected Drive"
-CONF_REGION = "region"
CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"]
CONF_READ_ONLY = "read_only"
CONF_USE_LOCATION = "use_location"
diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py
index 7f069e741b8ecd..25adf6cb09f946 100644
--- a/homeassistant/components/bmw_connected_drive/device_tracker.py
+++ b/homeassistant/components/bmw_connected_drive/device_tracker.py
@@ -42,12 +42,12 @@ def __init__(self, account, vehicle):
@property
def latitude(self):
"""Return latitude value of the device."""
- return self._location[0]
+ return self._location[0] if self._location else None
@property
def longitude(self):
"""Return longitude value of the device."""
- return self._location[1]
+ return self._location[1] if self._location else None
@property
def name(self):
diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py
index 0d281e78f14707..97c9be7216b36c 100644
--- a/homeassistant/components/bmw_connected_drive/lock.py
+++ b/homeassistant/components/bmw_connected_drive/lock.py
@@ -52,7 +52,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the lock."""
vehicle_state = self._vehicle.state
result = self._attrs.copy()
diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json
index c1d90f713f4adf..bbff139187e8b9 100644
--- a/homeassistant/components/bmw_connected_drive/manifest.json
+++ b/homeassistant/components/bmw_connected_drive/manifest.json
@@ -2,7 +2,7 @@
"domain": "bmw_connected_drive",
"name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
- "requirements": ["bimmer_connected==0.7.14"],
+ "requirements": ["bimmer_connected==0.7.15"],
"codeowners": ["@gerard33", "@rikroe"],
"config_flow": true
}
diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py
index 480aac34eb3e26..38415d0006fdb0 100644
--- a/homeassistant/components/bmw_connected_drive/sensor.py
+++ b/homeassistant/components/bmw_connected_drive/sensor.py
@@ -3,6 +3,7 @@
from bimmer_connected.state import ChargingState
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
CONF_UNIT_SYSTEM_IMPERIAL,
LENGTH_KILOMETERS,
@@ -12,7 +13,6 @@
VOLUME_GALLONS,
VOLUME_LITERS,
)
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level
from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity
@@ -67,7 +67,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities, True)
-class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, Entity):
+class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity):
"""Representation of a BMW vehicle sensor."""
def __init__(self, account, vehicle, attribute: str, attribute_info):
diff --git a/homeassistant/components/bmw_connected_drive/translations/bg.json b/homeassistant/components/bmw_connected_drive/translations/bg.json
new file mode 100644
index 00000000000000..67a484573aa0c1
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/bg.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "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/bmw_connected_drive/translations/ca.json b/homeassistant/components/bmw_connected_drive/translations/ca.json
new file mode 100644
index 00000000000000..d6bd70064c3271
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/ca.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El compte ja ha estat configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrasenya",
+ "region": "Regi\u00f3 de ConnectedDrive",
+ "username": "Nom d'usuari"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "account_options": {
+ "data": {
+ "read_only": "Nom\u00e9s de lectura (nom\u00e9s sensors i notificacions, sense execuci\u00f3 de serveis, sense bloqueig)",
+ "use_location": "Utilitza la ubicaci\u00f3 de Home Assistant per a les crides de localitzaci\u00f3 del cotxe (obligatori per a vehicles que no siguin i3/i8 produ\u00efts abans del 7/2014)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/cs.json b/homeassistant/components/bmw_connected_drive/translations/cs.json
new file mode 100644
index 00000000000000..665dccd443db71
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/cs.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u00da\u010det je ji\u017e nastaven"
+ },
+ "error": {
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
+ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Heslo",
+ "username": "U\u017eivatelsk\u00e9 jm\u00e9no"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/de.json b/homeassistant/components/bmw_connected_drive/translations/de.json
new file mode 100644
index 00000000000000..d274719d7d0d0d
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/de.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto wurde bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "region": "ConnectedDrive Region",
+ "username": "Benutzername"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/en.json b/homeassistant/components/bmw_connected_drive/translations/en.json
index f194c8a344448a..dedd84d070b5c1 100644
--- a/homeassistant/components/bmw_connected_drive/translations/en.json
+++ b/homeassistant/components/bmw_connected_drive/translations/en.json
@@ -11,7 +11,6 @@
"user": {
"data": {
"password": "Password",
- "read_only": "Read-only",
"region": "ConnectedDrive Region",
"username": "Username"
}
diff --git a/homeassistant/components/bmw_connected_drive/translations/es.json b/homeassistant/components/bmw_connected_drive/translations/es.json
new file mode 100644
index 00000000000000..65ed9643f890f2
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/es.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La cuenta ya est\u00e1 configurada"
+ },
+ "error": {
+ "cannot_connect": "No se pudo conectar",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrase\u00f1a",
+ "region": "Regi\u00f3n de ConnectedDrive",
+ "username": "Nombre de usuario"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "account_options": {
+ "data": {
+ "read_only": "S\u00f3lo lectura (s\u00f3lo sensores y notificaci\u00f3n, sin ejecuci\u00f3n de servicios, sin bloqueo)",
+ "use_location": "Usar la ubicaci\u00f3n de Home Assistant para las encuestas de localizaci\u00f3n de autom\u00f3viles (necesario para los veh\u00edculos no i3/i8 producidos antes del 7/2014)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/et.json b/homeassistant/components/bmw_connected_drive/translations/et.json
new file mode 100644
index 00000000000000..f28209a1e7a3fb
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/et.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kasutaja on juba seadistatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_auth": "Vigane autentimine"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Salas\u00f5na",
+ "region": "ConnectedDrive'i piirkond",
+ "username": "Kasutajanimi"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "account_options": {
+ "data": {
+ "read_only": "Kirjutuskaitstud (ainult andurid ja teavitused, ei k\u00e4ivita teenuseid, kood puudub)",
+ "use_location": "Kasuta HA asukohta auto asukoha k\u00fcsitluste jaoks (n\u00f5utav enne 7/2014 toodetud muude kui i3 / i8 s\u00f5idukite jaoks)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/fr.json b/homeassistant/components/bmw_connected_drive/translations/fr.json
new file mode 100644
index 00000000000000..900b352ecb6c20
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/fr.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec \u00e0 la connexion",
+ "invalid_auth": "Authentification invalide"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Mot de passe",
+ "region": "R\u00e9gion ConnectedDrive",
+ "username": "Nom d'utilisateur"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "account_options": {
+ "data": {
+ "read_only": "Lecture seule (uniquement capteurs et notification, pas d'ex\u00e9cution de services, pas de verrouillage)",
+ "use_location": "Utilisez la localisation de Home Assistant pour les sondages de localisation de voiture (obligatoire pour les v\u00e9hicules non i3 / i8 produits avant 7/2014)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/hu.json b/homeassistant/components/bmw_connected_drive/translations/hu.json
new file mode 100644
index 00000000000000..8724f525626c78
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/hu.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "region": "ConnectedDrive R\u00e9gi\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/id.json b/homeassistant/components/bmw_connected_drive/translations/id.json
new file mode 100644
index 00000000000000..e49e9202dbe2e5
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/id.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "region": "Wilayah ConnectedDrive",
+ "username": "Nama Pengguna"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "account_options": {
+ "data": {
+ "read_only": "Hanya baca (hanya sensor dan notifikasi, tidak ada eksekusi layanan, tidak ada fitur penguncian)",
+ "use_location": "Gunakan lokasi Asisten Rumah untuk polling lokasi mobil (diperlukan untuk kendaraan non i3/i8 yang diproduksi sebelum Juli 2014)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/it.json b/homeassistant/components/bmw_connected_drive/translations/it.json
new file mode 100644
index 00000000000000..277ed189c43ced
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/it.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password",
+ "region": "Regione ConnectedDrive",
+ "username": "Nome utente"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "account_options": {
+ "data": {
+ "read_only": "Sola lettura (solo sensori e notifica, nessuna esecuzione di servizi, nessun blocco)",
+ "use_location": "Usa la posizione di Home Assistant per richieste sulla posizione dell'auto (richiesto per veicoli non i3/i8 prodotti prima del 7/2014)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/ko.json b/homeassistant/components/bmw_connected_drive/translations/ko.json
new file mode 100644
index 00000000000000..4c9872573bee1a
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/ko.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "region": "ConnectedDrive \uc9c0\uc5ed",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "account_options": {
+ "data": {
+ "read_only": "\uc77d\uae30 \uc804\uc6a9(\uc13c\uc11c \ubc0f \uc54c\ub9bc\ub9cc \uac00\ub2a5, \uc11c\ube44\uc2a4 \uc2e4\ud589 \ubc0f \uc7a0\uae08 \uc5c6\uc74c)",
+ "use_location": "\ucc28\ub7c9 \uc704\uce58 \ud3f4\ub9c1\uc5d0 Home Assistant \uc704\uce58\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4(2014\ub144 7\uc6d4 \uc774\uc804\uc5d0 \uc0dd\uc0b0\ub41c i3/i8\uc774 \uc544\ub2cc \ucc28\ub7c9\uc758 \uacbd\uc6b0 \ud544\uc694)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/lb.json b/homeassistant/components/bmw_connected_drive/translations/lb.json
new file mode 100644
index 00000000000000..9ebbe919f8badf
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/lb.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kont ass scho konfigur\u00e9iert"
+ },
+ "error": {
+ "cannot_connect": "Feeler beim verbannen",
+ "invalid_auth": "Ong\u00eblteg Authentifikatioun"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwuert",
+ "region": "ConnectedDrive Regioun"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/nl.json b/homeassistant/components/bmw_connected_drive/translations/nl.json
new file mode 100644
index 00000000000000..8fa6c839112f7d
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/nl.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Account is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Wachtwoord",
+ "region": "ConnectedDrive-regio",
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "account_options": {
+ "data": {
+ "read_only": "Alleen-lezen (alleen sensoren en notificatie, geen uitvoering van diensten, geen vergrendeling)",
+ "use_location": "Gebruik Home Assistant locatie voor auto locatie peilingen (vereist voor niet i3/i8 voertuigen geproduceerd voor 7/2014)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/no.json b/homeassistant/components/bmw_connected_drive/translations/no.json
new file mode 100644
index 00000000000000..f1715c550db4dc
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/no.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kontoen er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_auth": "Ugyldig godkjenning"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passord",
+ "region": "ConnectedDrive-region",
+ "username": "Brukernavn"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "account_options": {
+ "data": {
+ "read_only": "Skrivebeskyttet (bare sensorer og varsler, ingen utf\u00f8relse av tjenester, ingen l\u00e5s)",
+ "use_location": "Bruk Home Assistant plassering for avstemningssteder for biler (p\u00e5krevd for ikke i3 / i8-kj\u00f8ret\u00f8y produsert f\u00f8r 7/2014)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/pl.json b/homeassistant/components/bmw_connected_drive/translations/pl.json
new file mode 100644
index 00000000000000..70467c6f9b93ed
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/pl.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Has\u0142o",
+ "region": "Region ConnectedDrive",
+ "username": "Nazwa u\u017cytkownika"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "account_options": {
+ "data": {
+ "read_only": "Tylko odczyt (tylko czujniki i powiadomienia, brak wykonywania us\u0142ug, brak blokady)",
+ "use_location": "U\u017cyj lokalizacji Home Assistant do sondowania lokalizacji samochodu (wymagane w przypadku pojazd\u00f3w innych ni\u017c i3/i8 wyprodukowanych przed 7/2014)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/pt.json b/homeassistant/components/bmw_connected_drive/translations/pt.json
new file mode 100644
index 00000000000000..3814c892bd16d5
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/pt.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Conta j\u00e1 configurada"
+ },
+ "error": {
+ "cannot_connect": "Falha na liga\u00e7\u00e3o",
+ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Palavra-passe",
+ "username": "Nome de Utilizador"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/ru.json b/homeassistant/components/bmw_connected_drive/translations/ru.json
new file mode 100644
index 00000000000000..8ab4e4e1207000
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/ru.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "region": "\u0420\u0435\u0433\u0438\u043e\u043d ConnectedDrive",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "account_options": {
+ "data": {
+ "read_only": "\u0422\u043e\u043b\u044c\u043a\u043e \u0447\u0442\u0435\u043d\u0438\u0435 (\u0442\u043e\u043b\u044c\u043a\u043e \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f, \u0431\u0435\u0437 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0441\u043b\u0443\u0436\u0431, \u0431\u0435\u0437 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438)",
+ "use_location": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Home Assistant \u0434\u043b\u044f \u043e\u043f\u0440\u043e\u0441\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u0435\u0439 (\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u0435\u0439 \u043d\u0435 i3/i8, \u0432\u044b\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u0445 \u0434\u043e 7/2014)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/tr.json b/homeassistant/components/bmw_connected_drive/translations/tr.json
new file mode 100644
index 00000000000000..153aa4126b0668
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/tr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/uk.json b/homeassistant/components/bmw_connected_drive/translations/uk.json
new file mode 100644
index 00000000000000..68cdee2a66f9df
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/uk.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "region": "ConnectedDrive Region",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "account_options": {
+ "data": {
+ "read_only": "\u041b\u0438\u0448\u0435 \u0434\u043b\u044f \u0447\u0438\u0442\u0430\u043d\u043d\u044f (\u043b\u0438\u0448\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0442\u0430 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f, \u0431\u0435\u0437 \u0437\u0430\u043f\u0443\u0441\u043a\u0443 \u0441\u0435\u0440\u0432\u0456\u0441\u0456\u0432, \u0431\u0435\u0437 \u0431\u043b\u043e\u043a\u0443\u0432\u0430\u043d\u043d\u044f)",
+ "use_location": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f Home Assistant \u0434\u043b\u044f \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u044c \u043c\u0456\u0441\u0446\u044f \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0456\u043b\u0456\u0432 (\u043e\u0431\u043e\u0432\u2019\u044f\u0437\u043a\u043e\u0432\u043e \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0456\u043b\u0456\u0432, \u0449\u043e \u043d\u0435 \u043d\u0430\u043b\u0435\u0436\u0430\u0442\u044c \u0434\u043e i3/i8, \u0432\u0438\u0433\u043e\u0442\u043e\u0432\u043b\u0435\u043d\u0438\u0445 \u0434\u043e 7/2014)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json b/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json
new file mode 100644
index 00000000000000..fde5e1e3c94e05
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u5bc6\u78bc",
+ "region": "ConnectedDrive \u5340\u57df",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "account_options": {
+ "data": {
+ "read_only": "\u552f\u8b80\uff08\u50c5\u652f\u63f4\u50b3\u611f\u5668\u8207\u901a\u77e5\uff0c\u4e0d\n\u5305\u542b\u670d\u52d9\u8207\u9396\u5b9a\uff09",
+ "use_location": "\u4f7f\u7528 Home Assistant \u4f4d\u7f6e\u53d6\u5f97\u6c7d\u8eca\u4f4d\u7f6e\uff08\u9700\u8981\u70ba2014/7 \u524d\u751f\u7522\u7684\u975ei3/i8 \u8eca\u6b3e\uff09"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py
index 9af92f6e7e7a81..800e130251742a 100644
--- a/homeassistant/components/bond/__init__.py
+++ b/homeassistant/components/bond/__init__.py
@@ -3,58 +3,72 @@
from asyncio import TimeoutError as AsyncIOTimeoutError
from aiohttp import ClientError, ClientTimeout
-from bond_api import Bond
+from bond_api import Bond, BPUPSubscriptions, start_bpup
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
-from homeassistant.core import HomeAssistant
+from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import SLOW_UPDATE_WARNING
-from .const import DOMAIN
+from .const import BPUP_STOP, BPUP_SUBS, BRIDGE_MAKE, DOMAIN, HUB
from .utils import BondHub
PLATFORMS = ["cover", "fan", "light", "switch"]
_API_TIMEOUT = SLOW_UPDATE_WARNING - 1
-async def async_setup(hass: HomeAssistant, config: dict):
- """Set up the Bond component."""
- hass.data.setdefault(DOMAIN, {})
- return True
-
-
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Bond from a config entry."""
host = entry.data[CONF_HOST]
token = entry.data[CONF_ACCESS_TOKEN]
+ config_entry_id = entry.entry_id
- bond = Bond(host=host, token=token, timeout=ClientTimeout(total=_API_TIMEOUT))
+ bond = Bond(
+ host=host,
+ token=token,
+ timeout=ClientTimeout(total=_API_TIMEOUT),
+ session=async_get_clientsession(hass),
+ )
hub = BondHub(bond)
try:
await hub.setup()
except (ClientError, AsyncIOTimeoutError, OSError) as error:
raise ConfigEntryNotReady from error
- hass.data[DOMAIN][entry.entry_id] = hub
+ bpup_subs = BPUPSubscriptions()
+ stop_bpup = await start_bpup(host, bpup_subs)
+
+ hass.data.setdefault(DOMAIN, {})
+ hass.data[DOMAIN][entry.entry_id] = {
+ HUB: hub,
+ BPUP_SUBS: bpup_subs,
+ BPUP_STOP: stop_bpup,
+ }
if not entry.unique_id:
hass.config_entries.async_update_entry(entry, unique_id=hub.bond_id)
+ assert hub.bond_id is not None
+ hub_name = hub.name or hub.bond_id
device_registry = await dr.async_get_registry(hass)
device_registry.async_get_or_create(
- config_entry_id=entry.entry_id,
+ config_entry_id=config_entry_id,
identifiers={(DOMAIN, hub.bond_id)},
- manufacturer="Olibra",
- name=hub.bond_id,
+ manufacturer=BRIDGE_MAKE,
+ name=hub_name,
model=hub.target,
sw_version=hub.fw_ver,
+ suggested_area=hub.location,
)
- for component in PLATFORMS:
+ _async_remove_old_device_identifiers(config_entry_id, device_registry, hub)
+
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -65,13 +79,30 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
+ data = hass.data[DOMAIN][entry.entry_id]
+ if BPUP_STOP in data:
+ data[BPUP_STOP]()
+
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
+
+
+@callback
+def _async_remove_old_device_identifiers(
+ config_entry_id: str, device_registry: dr.DeviceRegistry, hub: BondHub
+) -> None:
+ """Remove the non-unique device registry entries."""
+ for device in hub.devices:
+ dev = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)})
+ if dev is None:
+ continue
+ if config_entry_id in dev.config_entries:
+ device_registry.async_remove_device(dev.id)
diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py
index 6666cd57ca357e..2e1f106193efe4 100644
--- a/homeassistant/components/bond/config_flow.py
+++ b/homeassistant/components/bond/config_flow.py
@@ -1,6 +1,8 @@
"""Config flow for Bond integration."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from aiohttp import ClientConnectionError, ClientResponseError
from bond_api import Bond
@@ -13,26 +15,32 @@
CONF_NAME,
HTTP_UNAUTHORIZED,
)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.typing import DiscoveryInfoType
-from .const import CONF_BOND_ID
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import DOMAIN
+from .utils import BondHub
_LOGGER = logging.getLogger(__name__)
-DATA_SCHEMA_USER = vol.Schema(
+
+USER_SCHEMA = vol.Schema(
{vol.Required(CONF_HOST): str, vol.Required(CONF_ACCESS_TOKEN): str}
)
-DATA_SCHEMA_DISCOVERY = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
+DISCOVERY_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
+TOKEN_SCHEMA = vol.Schema({})
-async def _validate_input(data: Dict[str, Any]) -> str:
+async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[str, str]:
"""Validate the user input allows us to connect."""
+ bond = Bond(
+ data[CONF_HOST], data[CONF_ACCESS_TOKEN], session=async_get_clientsession(hass)
+ )
try:
- bond = Bond(data[CONF_HOST], data[CONF_ACCESS_TOKEN])
- version = await bond.version()
- # call to non-version API is needed to validate authentication
- await bond.devices()
+ hub = BondHub(bond)
+ await hub.setup(max_devices=1)
except ClientConnectionError as error:
raise InputValidationError("cannot_connect") from error
except ClientResponseError as error:
@@ -44,24 +52,46 @@ async def _validate_input(data: Dict[str, Any]) -> str:
raise InputValidationError("unknown") from error
# Return unique ID from the hub to be stored in the config entry.
- bond_id = version.get("bondid")
- if not bond_id:
+ if not hub.bond_id:
raise InputValidationError("old_firmware")
- return bond_id
+ return hub.bond_id, hub.name
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Bond."""
VERSION = 1
- CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
+
+ def __init__(self) -> None:
+ """Initialize config flow."""
+ self._discovered: dict[str, str] = {}
+
+ async def _async_try_automatic_configure(self) -> None:
+ """Try to auto configure the device.
+
+ Failure is acceptable here since the device may have been
+ online longer then the allowed setup period, and we will
+ instead ask them to manually enter the token.
+ """
+ bond = Bond(
+ self._discovered[CONF_HOST], "", session=async_get_clientsession(self.hass)
+ )
+ try:
+ response = await bond.token()
+ except ClientConnectionError:
+ return
- _discovered: dict = None
+ token = response.get("token")
+ if token is None:
+ return
- async def async_step_zeroconf(
- self, discovery_info: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self._discovered[CONF_ACCESS_TOKEN] = token
+ _, hub_name = await _validate_input(self.hass, self._discovered)
+ self._discovered[CONF_NAME] = hub_name
+
+ async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType) -> dict[str, Any]: # type: ignore
"""Handle a flow initialized by zeroconf discovery."""
name: str = discovery_info[CONF_NAME]
host: str = discovery_info[CONF_HOST]
@@ -69,56 +99,80 @@ async def async_step_zeroconf(
await self.async_set_unique_id(bond_id)
self._abort_if_unique_id_configured({CONF_HOST: host})
- self._discovered = {
- CONF_HOST: host,
- CONF_BOND_ID: bond_id,
- }
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
- self.context.update({"title_placeholders": self._discovered})
+ self._discovered = {CONF_HOST: host, CONF_NAME: bond_id}
+ await self._async_try_automatic_configure()
+
+ self.context.update(
+ {
+ "title_placeholders": {
+ CONF_HOST: self._discovered[CONF_HOST],
+ CONF_NAME: self._discovered[CONF_NAME],
+ }
+ }
+ )
return await self.async_step_confirm()
async def async_step_confirm(
- self, user_input: Dict[str, Any] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle confirmation flow for discovered bond hub."""
errors = {}
if user_input is not None:
- data = user_input.copy()
- data[CONF_HOST] = self._discovered[CONF_HOST]
+ if CONF_ACCESS_TOKEN in self._discovered:
+ return self.async_create_entry(
+ title=self._discovered[CONF_NAME],
+ data={
+ CONF_ACCESS_TOKEN: self._discovered[CONF_ACCESS_TOKEN],
+ CONF_HOST: self._discovered[CONF_HOST],
+ },
+ )
+
+ data = {
+ CONF_ACCESS_TOKEN: user_input[CONF_ACCESS_TOKEN],
+ CONF_HOST: self._discovered[CONF_HOST],
+ }
try:
- return await self._try_create_entry(data)
+ _, hub_name = await _validate_input(self.hass, data)
except InputValidationError as error:
errors["base"] = error.base
+ else:
+ return self.async_create_entry(
+ title=hub_name,
+ data=data,
+ )
+
+ if CONF_ACCESS_TOKEN in self._discovered:
+ data_schema = TOKEN_SCHEMA
+ else:
+ data_schema = DISCOVERY_SCHEMA
return self.async_show_form(
step_id="confirm",
- data_schema=DATA_SCHEMA_DISCOVERY,
+ data_schema=data_schema,
errors=errors,
description_placeholders=self._discovered,
)
async def async_step_user(
- self, user_input: Dict[str, Any] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initialized by the user."""
errors = {}
if user_input is not None:
try:
- return await self._try_create_entry(user_input)
+ bond_id, hub_name = await _validate_input(self.hass, user_input)
except InputValidationError as error:
errors["base"] = error.base
+ else:
+ await self.async_set_unique_id(bond_id)
+ self._abort_if_unique_id_configured()
+ return self.async_create_entry(title=hub_name, data=user_input)
return self.async_show_form(
- step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors
+ step_id="user", data_schema=USER_SCHEMA, errors=errors
)
- async def _try_create_entry(self, data: Dict[str, Any]) -> Dict[str, Any]:
- bond_id = await _validate_input(data)
- await self.async_set_unique_id(bond_id)
- self._abort_if_unique_id_configured()
- return self.async_create_entry(title=bond_id, data=data)
-
class InputValidationError(exceptions.HomeAssistantError):
"""Error to indicate we cannot proceed due to invalid input."""
diff --git a/homeassistant/components/bond/const.py b/homeassistant/components/bond/const.py
index 843c3f9f1dc651..818288a5764ca6 100644
--- a/homeassistant/components/bond/const.py
+++ b/homeassistant/components/bond/const.py
@@ -1,5 +1,12 @@
"""Constants for the Bond integration."""
+BRIDGE_MAKE = "Olibra"
+
DOMAIN = "bond"
CONF_BOND_ID: str = "bond_id"
+
+
+HUB = "hub"
+BPUP_SUBS = "bpup_subs"
+BPUP_STOP = "bpup_stop"
diff --git a/homeassistant/components/bond/cover.py b/homeassistant/components/bond/cover.py
index dc0fc6d500c026..60dcc4ec1f0803 100644
--- a/homeassistant/components/bond/cover.py
+++ b/homeassistant/components/bond/cover.py
@@ -1,14 +1,16 @@
"""Support for Bond covers."""
-from typing import Any, Callable, List, Optional
+from __future__ import annotations
-from bond_api import Action, DeviceType
+from typing import Any, Callable
+
+from bond_api import Action, BPUPSubscriptions, DeviceType
from homeassistant.components.cover import DEVICE_CLASS_SHADE, CoverEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
-from .const import DOMAIN
+from .const import BPUP_SUBS, DOMAIN, HUB
from .entity import BondEntity
from .utils import BondDevice, BondHub
@@ -16,13 +18,15 @@
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up Bond cover devices."""
- hub: BondHub = hass.data[DOMAIN][entry.entry_id]
+ data = hass.data[DOMAIN][entry.entry_id]
+ hub: BondHub = data[HUB]
+ bpup_subs: BPUPSubscriptions = data[BPUP_SUBS]
- covers = [
- BondCover(hub, device)
+ covers: list[Entity] = [
+ BondCover(hub, device, bpup_subs)
for device in hub.devices
if device.type == DeviceType.MOTORIZED_SHADES
]
@@ -33,23 +37,25 @@ async def async_setup_entry(
class BondCover(BondEntity, CoverEntity):
"""Representation of a Bond cover."""
- def __init__(self, hub: BondHub, device: BondDevice):
+ def __init__(
+ self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions
+ ) -> None:
"""Create HA entity representing Bond cover."""
- super().__init__(hub, device)
+ super().__init__(hub, device, bpup_subs)
- self._closed: Optional[bool] = None
+ self._closed: bool | None = None
- def _apply_state(self, state: dict):
+ def _apply_state(self, state: dict) -> None:
cover_open = state.get("open")
self._closed = True if cover_open == 0 else False if cover_open == 1 else None
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Get device class."""
return DEVICE_CLASS_SHADE
@property
- def is_closed(self):
+ def is_closed(self) -> bool | None:
"""Return if the cover is closed or not."""
return self._closed
@@ -61,6 +67,6 @@ async def async_close_cover(self, **kwargs: Any) -> None:
"""Close cover."""
await self._hub.bond.action(self._device.device_id, Action.close())
- async def async_stop_cover(self, **kwargs):
+ async def async_stop_cover(self, **kwargs: Any) -> None:
"""Hold cover."""
await self._hub.bond.action(self._device.device_id, Action.hold())
diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py
index 501d65749600b2..a676d99e9ad9d6 100644
--- a/homeassistant/components/bond/entity.py
+++ b/homeassistant/components/bond/entity.py
@@ -1,53 +1,92 @@
"""An abstract class common to all Bond entities."""
+from __future__ import annotations
+
from abc import abstractmethod
-from asyncio import TimeoutError as AsyncIOTimeoutError
+from asyncio import Lock, TimeoutError as AsyncIOTimeoutError
+from datetime import timedelta
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from aiohttp import ClientError
+from bond_api import BPUPSubscriptions
from homeassistant.const import ATTR_NAME
+from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.event import async_track_time_interval
from .const import DOMAIN
from .utils import BondDevice, BondHub
_LOGGER = logging.getLogger(__name__)
+_FALLBACK_SCAN_INTERVAL = timedelta(seconds=10)
+
class BondEntity(Entity):
"""Generic Bond entity encapsulating common features of any Bond controlled device."""
def __init__(
- self, hub: BondHub, device: BondDevice, sub_device: Optional[str] = None
+ self,
+ hub: BondHub,
+ device: BondDevice,
+ bpup_subs: BPUPSubscriptions,
+ sub_device: str | None = None,
):
"""Initialize entity with API and device info."""
self._hub = hub
self._device = device
+ self._device_id = device.device_id
self._sub_device = sub_device
self._available = True
+ self._bpup_subs = bpup_subs
+ self._update_lock: Lock | None = None
+ self._initialized = False
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Get unique ID for the entity."""
hub_id = self._hub.bond_id
- device_id = self._device.device_id
+ device_id = self._device_id
sub_device_id: str = f"_{self._sub_device}" if self._sub_device else ""
return f"{hub_id}_{device_id}{sub_device_id}"
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Get entity name."""
+ if self._sub_device:
+ sub_device_name = self._sub_device.replace("_", " ").title()
+ return f"{self._device.name} {sub_device_name}"
return self._device.name
@property
- def device_info(self) -> Optional[Dict[str, Any]]:
+ def should_poll(self) -> bool:
+ """No polling needed."""
+ return False
+
+ @property
+ def device_info(self) -> dict[str, Any] | None:
"""Get a an HA device representing this Bond controlled device."""
- return {
+ device_info = {
ATTR_NAME: self.name,
- "identifiers": {(DOMAIN, self._device.device_id)},
+ "manufacturer": self._hub.make,
+ "identifiers": {(DOMAIN, self._hub.bond_id, self._device.device_id)},
+ "suggested_area": self._device.location,
"via_device": (DOMAIN, self._hub.bond_id),
}
+ if not self._hub.is_bridge:
+ device_info["model"] = self._hub.model
+ device_info["sw_version"] = self._hub.fw_ver
+ else:
+ model_data = []
+ if self._device.branding_profile:
+ model_data.append(self._device.branding_profile)
+ if self._device.template:
+ model_data.append(self._device.template)
+ if model_data:
+ device_info["model"] = " ".join(model_data)
+
+ return device_info
@property
def assumed_state(self) -> bool:
@@ -59,10 +98,32 @@ def available(self) -> bool:
"""Report availability of this entity based on last API call results."""
return self._available
- async def async_update(self):
+ async def async_update(self) -> None:
"""Fetch assumed state of the cover from the hub using API."""
+ await self._async_update_from_api()
+
+ async def _async_update_if_bpup_not_alive(self, *_: Any) -> None:
+ """Fetch via the API if BPUP is not alive."""
+ if self._bpup_subs.alive and self._initialized and self._available:
+ return
+
+ assert self._update_lock is not None
+ if self._update_lock.locked():
+ _LOGGER.warning(
+ "Updating %s took longer than the scheduled update interval %s",
+ self.entity_id,
+ _FALLBACK_SCAN_INTERVAL,
+ )
+ return
+
+ async with self._update_lock:
+ await self._async_update_from_api()
+ self.async_write_ha_state()
+
+ async def _async_update_from_api(self) -> None:
+ """Fetch via the API."""
try:
- state: dict = await self._hub.bond.device_state(self._device.device_id)
+ state: dict = await self._hub.bond.device_state(self._device_id)
except (ClientError, AsyncIOTimeoutError, OSError) as error:
if self._available:
_LOGGER.warning(
@@ -70,12 +131,42 @@ async def async_update(self):
)
self._available = False
else:
- _LOGGER.debug("Device state for %s is:\n%s", self.entity_id, state)
- if not self._available:
- _LOGGER.info("Entity %s has come back", self.entity_id)
- self._available = True
- self._apply_state(state)
+ self._async_state_callback(state)
@abstractmethod
- def _apply_state(self, state: dict):
+ def _apply_state(self, state: dict) -> None:
raise NotImplementedError
+
+ @callback
+ def _async_state_callback(self, state: dict) -> None:
+ """Process a state change."""
+ self._initialized = True
+ if not self._available:
+ _LOGGER.info("Entity %s has come back", self.entity_id)
+ self._available = True
+ _LOGGER.debug(
+ "Device state for %s (%s) is:\n%s", self.name, self.entity_id, state
+ )
+ self._apply_state(state)
+
+ @callback
+ def _async_bpup_callback(self, state: dict) -> None:
+ """Process a state change from BPUP."""
+ self._async_state_callback(state)
+ self.async_write_ha_state()
+
+ async def async_added_to_hass(self) -> None:
+ """Subscribe to BPUP and start polling."""
+ await super().async_added_to_hass()
+ self._update_lock = Lock()
+ self._bpup_subs.subscribe(self._device_id, self._async_bpup_callback)
+ self.async_on_remove(
+ async_track_time_interval(
+ self.hass, self._async_update_if_bpup_not_alive, _FALLBACK_SCAN_INTERVAL
+ )
+ )
+
+ async def async_will_remove_from_hass(self) -> None:
+ """Unsubscribe from BPUP data on remove."""
+ await super().async_will_remove_from_hass()
+ self._bpup_subs.unsubscribe(self._device_id, self._async_bpup_callback)
diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py
index 18eeb912ed8192..817cf0f99a2c25 100644
--- a/homeassistant/components/bond/fan.py
+++ b/homeassistant/components/bond/fan.py
@@ -1,9 +1,11 @@
"""Support for Bond fans."""
+from __future__ import annotations
+
import logging
import math
-from typing import Any, Callable, List, Optional, Tuple
+from typing import Any, Callable
-from bond_api import Action, DeviceType, Direction
+from bond_api import Action, BPUPSubscriptions, DeviceType, Direction
from homeassistant.components.fan import (
DIRECTION_FORWARD,
@@ -16,11 +18,12 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.util.percentage import (
+ int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
-from .const import DOMAIN
+from .const import BPUP_SUBS, DOMAIN, HUB
from .entity import BondEntity
from .utils import BondDevice, BondHub
@@ -30,13 +33,17 @@
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up Bond fan devices."""
- hub: BondHub = hass.data[DOMAIN][entry.entry_id]
-
- fans = [
- BondFan(hub, device) for device in hub.devices if DeviceType.is_fan(device.type)
+ data = hass.data[DOMAIN][entry.entry_id]
+ hub: BondHub = data[HUB]
+ bpup_subs: BPUPSubscriptions = data[BPUP_SUBS]
+
+ fans: list[Entity] = [
+ BondFan(hub, device, bpup_subs)
+ for device in hub.devices
+ if DeviceType.is_fan(device.type)
]
async_add_entities(fans, True)
@@ -45,15 +52,15 @@ async def async_setup_entry(
class BondFan(BondEntity, FanEntity):
"""Representation of a Bond fan."""
- def __init__(self, hub: BondHub, device: BondDevice):
+ def __init__(self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions):
"""Create HA entity representing Bond fan."""
- super().__init__(hub, device)
+ super().__init__(hub, device, bpup_subs)
- self._power: Optional[bool] = None
- self._speed: Optional[int] = None
- self._direction: Optional[int] = None
+ self._power: bool | None = None
+ self._speed: int | None = None
+ self._direction: int | None = None
- def _apply_state(self, state: dict):
+ def _apply_state(self, state: dict) -> None:
self._power = state.get("power")
self._speed = state.get("speed")
self._direction = state.get("direction")
@@ -70,19 +77,24 @@ def supported_features(self) -> int:
return features
@property
- def _speed_range(self) -> Tuple[int, int]:
+ def _speed_range(self) -> tuple[int, int]:
"""Return the range of speeds."""
return (1, self._device.props.get("max_speed", 3))
@property
- def percentage(self) -> Optional[str]:
+ def percentage(self) -> int:
"""Return the current speed percentage for the fan."""
if not self._speed or not self._power:
return 0
return ranged_value_to_percentage(self._speed_range, self._speed)
@property
- def current_direction(self) -> Optional[str]:
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return int_states_in_range(self._speed_range)
+
+ @property
+ def current_direction(self) -> str | None:
"""Return fan rotation direction."""
direction = None
if self._direction == Direction.FORWARD:
@@ -115,10 +127,10 @@ async def async_set_percentage(self, percentage: int) -> None:
async def async_turn_on(
self,
- speed: Optional[str] = None,
- percentage: Optional[int] = None,
- preset_mode: Optional[str] = None,
- **kwargs,
+ speed: str | None = None,
+ percentage: int | None = None,
+ preset_mode: str | None = None,
+ **kwargs: Any,
) -> None:
"""Turn on the fan."""
_LOGGER.debug("Fan async_turn_on called with percentage %s", percentage)
@@ -132,7 +144,7 @@ async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the fan off."""
await self._hub.bond.action(self._device.device_id, Action.turn_off())
- async def async_set_direction(self, direction: str):
+ async def async_set_direction(self, direction: str) -> None:
"""Set fan rotation direction."""
bond_direction = (
Direction.REVERSE if direction == DIRECTION_REVERSE else Direction.FORWARD
diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py
index 77771167e147b1..8faab26f785715 100644
--- a/homeassistant/components/bond/light.py
+++ b/homeassistant/components/bond/light.py
@@ -1,8 +1,10 @@
"""Support for Bond lights."""
+from __future__ import annotations
+
import logging
-from typing import Any, Callable, List, Optional
+from typing import Any, Callable
-from bond_api import Action, DeviceType
+from bond_api import Action, BPUPSubscriptions, DeviceType
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -14,7 +16,7 @@
from homeassistant.helpers.entity import Entity
from . import BondHub
-from .const import DOMAIN
+from .const import BPUP_SUBS, DOMAIN, HUB
from .entity import BondEntity
from .utils import BondDevice
@@ -24,61 +26,109 @@
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up Bond light devices."""
- hub: BondHub = hass.data[DOMAIN][entry.entry_id]
+ data = hass.data[DOMAIN][entry.entry_id]
+ hub: BondHub = data[HUB]
+ bpup_subs: BPUPSubscriptions = data[BPUP_SUBS]
+
+ fan_lights: list[Entity] = [
+ BondLight(hub, device, bpup_subs)
+ for device in hub.devices
+ if DeviceType.is_fan(device.type)
+ and device.supports_light()
+ and not (device.supports_up_light() and device.supports_down_light())
+ ]
+
+ fan_up_lights: list[Entity] = [
+ BondUpLight(hub, device, bpup_subs, "up_light")
+ for device in hub.devices
+ if DeviceType.is_fan(device.type) and device.supports_up_light()
+ ]
- fan_lights: List[Entity] = [
- BondLight(hub, device)
+ fan_down_lights: list[Entity] = [
+ BondDownLight(hub, device, bpup_subs, "down_light")
for device in hub.devices
- if DeviceType.is_fan(device.type) and device.supports_light()
+ if DeviceType.is_fan(device.type) and device.supports_down_light()
]
- fireplaces: List[Entity] = [
- BondFireplace(hub, device)
+ fireplaces: list[Entity] = [
+ BondFireplace(hub, device, bpup_subs)
for device in hub.devices
if DeviceType.is_fireplace(device.type)
]
- fp_lights: List[Entity] = [
- BondLight(hub, device, "light")
+ fp_lights: list[Entity] = [
+ BondLight(hub, device, bpup_subs, "light")
for device in hub.devices
if DeviceType.is_fireplace(device.type) and device.supports_light()
]
- async_add_entities(fan_lights + fireplaces + fp_lights, True)
+ lights: list[Entity] = [
+ BondLight(hub, device, bpup_subs)
+ for device in hub.devices
+ if DeviceType.is_light(device.type)
+ ]
+ async_add_entities(
+ fan_lights + fan_up_lights + fan_down_lights + fireplaces + fp_lights + lights,
+ True,
+ )
-class BondLight(BondEntity, LightEntity):
+
+class BondBaseLight(BondEntity, LightEntity):
"""Representation of a Bond light."""
def __init__(
- self, hub: BondHub, device: BondDevice, sub_device: Optional[str] = None
+ self,
+ hub: BondHub,
+ device: BondDevice,
+ bpup_subs: BPUPSubscriptions,
+ sub_device: str | None = None,
):
- """Create HA entity representing Bond fan."""
- super().__init__(hub, device, sub_device)
- self._brightness: Optional[int] = None
- self._light: Optional[int] = None
+ """Create HA entity representing Bond light."""
+ super().__init__(hub, device, bpup_subs, sub_device)
+ self._light: int | None = None
- def _apply_state(self, state: dict):
+ @property
+ def is_on(self) -> bool:
+ """Return if light is currently on."""
+ return self._light == 1
+
+ @property
+ def supported_features(self) -> int:
+ """Flag supported features."""
+ return 0
+
+
+class BondLight(BondBaseLight, BondEntity, LightEntity):
+ """Representation of a Bond light."""
+
+ def __init__(
+ self,
+ hub: BondHub,
+ device: BondDevice,
+ bpup_subs: BPUPSubscriptions,
+ sub_device: str | None = None,
+ ):
+ """Create HA entity representing Bond light."""
+ super().__init__(hub, device, bpup_subs, sub_device)
+ self._brightness: int | None = None
+
+ def _apply_state(self, state: dict) -> None:
self._light = state.get("light")
self._brightness = state.get("brightness")
@property
- def supported_features(self) -> Optional[int]:
+ def supported_features(self) -> int:
"""Flag supported features."""
if self._device.supports_set_brightness():
return SUPPORT_BRIGHTNESS
return 0
@property
- def is_on(self) -> bool:
- """Return if light is currently on."""
- return self._light == 1
-
- @property
- def brightness(self) -> int:
+ def brightness(self) -> int | None:
"""Return the brightness of this light between 1..255."""
brightness_value = (
round(self._brightness * 255 / 100) if self._brightness else None
@@ -101,23 +151,61 @@ async def async_turn_off(self, **kwargs: Any) -> None:
await self._hub.bond.action(self._device.device_id, Action.turn_light_off())
+class BondDownLight(BondBaseLight, BondEntity, LightEntity):
+ """Representation of a Bond light."""
+
+ def _apply_state(self, state: dict) -> None:
+ self._light = state.get("down_light") and state.get("light")
+
+ async def async_turn_on(self, **kwargs: Any) -> None:
+ """Turn on the light."""
+ await self._hub.bond.action(
+ self._device.device_id, Action(Action.TURN_DOWN_LIGHT_ON)
+ )
+
+ async def async_turn_off(self, **kwargs: Any) -> None:
+ """Turn off the light."""
+ await self._hub.bond.action(
+ self._device.device_id, Action(Action.TURN_DOWN_LIGHT_OFF)
+ )
+
+
+class BondUpLight(BondBaseLight, BondEntity, LightEntity):
+ """Representation of a Bond light."""
+
+ def _apply_state(self, state: dict) -> None:
+ self._light = state.get("up_light") and state.get("light")
+
+ async def async_turn_on(self, **kwargs: Any) -> None:
+ """Turn on the light."""
+ await self._hub.bond.action(
+ self._device.device_id, Action(Action.TURN_UP_LIGHT_ON)
+ )
+
+ async def async_turn_off(self, **kwargs: Any) -> None:
+ """Turn off the light."""
+ await self._hub.bond.action(
+ self._device.device_id, Action(Action.TURN_UP_LIGHT_OFF)
+ )
+
+
class BondFireplace(BondEntity, LightEntity):
"""Representation of a Bond-controlled fireplace."""
- def __init__(self, hub: BondHub, device: BondDevice):
+ def __init__(self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions):
"""Create HA entity representing Bond fireplace."""
- super().__init__(hub, device)
+ super().__init__(hub, device, bpup_subs)
- self._power: Optional[bool] = None
+ self._power: bool | None = None
# Bond flame level, 0-100
- self._flame: Optional[int] = None
+ self._flame: int | None = None
- def _apply_state(self, state: dict):
+ def _apply_state(self, state: dict) -> None:
self._power = state.get("power")
self._flame = state.get("flame")
@property
- def supported_features(self) -> Optional[int]:
+ def supported_features(self) -> int:
"""Flag brightness as supported feature to represent flame level."""
return SUPPORT_BRIGHTNESS
@@ -144,11 +232,11 @@ async def async_turn_off(self, **kwargs: Any) -> None:
await self._hub.bond.action(self._device.device_id, Action.turn_off())
@property
- def brightness(self):
+ def brightness(self) -> int | None:
"""Return the flame of this fireplace converted to HA brightness between 0..255."""
return round(self._flame * 255 / 100) if self._flame else None
@property
- def icon(self) -> Optional[str]:
+ def icon(self) -> str | None:
"""Show fireplace icon for the entity."""
return "mdi:fireplace" if self._power == 1 else "mdi:fireplace-off"
diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json
index 3f62403dba786e..7204ac7e91df11 100644
--- a/homeassistant/components/bond/manifest.json
+++ b/homeassistant/components/bond/manifest.json
@@ -3,7 +3,7 @@
"name": "Bond",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bond",
- "requirements": ["bond-api==0.1.8"],
+ "requirements": ["bond-api==0.1.12"],
"zeroconf": ["_bond._tcp.local."],
"codeowners": ["@prystupa"],
"quality_scale": "platinum"
diff --git a/homeassistant/components/bond/strings.json b/homeassistant/components/bond/strings.json
index 5ca2278a3e59f8..f8eff6ddd9e335 100644
--- a/homeassistant/components/bond/strings.json
+++ b/homeassistant/components/bond/strings.json
@@ -1,9 +1,9 @@
{
"config": {
- "flow_title": "Bond: {bond_id} ({host})",
+ "flow_title": "Bond: {name} ({host})",
"step": {
"confirm": {
- "description": "Do you want to set up {bond_id}?",
+ "description": "Do you want to set up {name}?",
"data": {
"access_token": "[%key:common::config_flow::data::access_token%]"
}
diff --git a/homeassistant/components/bond/switch.py b/homeassistant/components/bond/switch.py
index d2f1797225d4bf..23e99d6af30aaf 100644
--- a/homeassistant/components/bond/switch.py
+++ b/homeassistant/components/bond/switch.py
@@ -1,14 +1,16 @@
"""Support for Bond generic devices."""
-from typing import Any, Callable, List, Optional
+from __future__ import annotations
-from bond_api import Action, DeviceType
+from typing import Any, Callable
+
+from bond_api import Action, BPUPSubscriptions, DeviceType
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
-from .const import DOMAIN
+from .const import BPUP_SUBS, DOMAIN, HUB
from .entity import BondEntity
from .utils import BondDevice, BondHub
@@ -16,13 +18,15 @@
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up Bond generic devices."""
- hub: BondHub = hass.data[DOMAIN][entry.entry_id]
+ data = hass.data[DOMAIN][entry.entry_id]
+ hub: BondHub = data[HUB]
+ bpup_subs: BPUPSubscriptions = data[BPUP_SUBS]
- switches = [
- BondSwitch(hub, device)
+ switches: list[Entity] = [
+ BondSwitch(hub, device, bpup_subs)
for device in hub.devices
if DeviceType.is_generic(device.type)
]
@@ -33,13 +37,13 @@ async def async_setup_entry(
class BondSwitch(BondEntity, SwitchEntity):
"""Representation of a Bond generic device."""
- def __init__(self, hub: BondHub, device: BondDevice):
+ def __init__(self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions):
"""Create HA entity representing Bond generic device (switch)."""
- super().__init__(hub, device)
+ super().__init__(hub, device, bpup_subs)
- self._power: Optional[bool] = None
+ self._power: bool | None = None
- def _apply_state(self, state: dict):
+ def _apply_state(self, state: dict) -> None:
self._power = state.get("power")
@property
diff --git a/homeassistant/components/bond/translations/ca.json b/homeassistant/components/bond/translations/ca.json
index 3903ea77c34166..1d1df91563057b 100644
--- a/homeassistant/components/bond/translations/ca.json
+++ b/homeassistant/components/bond/translations/ca.json
@@ -9,13 +9,13 @@
"old_firmware": "Hi ha un programari antic i no compatible al dispositiu Bond - actualitza'l abans de continuar",
"unknown": "Error inesperat"
},
- "flow_title": "Bond: {bond_id} ({host})",
+ "flow_title": "Bond: {name} ({host})",
"step": {
"confirm": {
"data": {
"access_token": "Token d'acc\u00e9s"
},
- "description": "Vols configurar {bond_id}?"
+ "description": "Vols configurar {name}?"
},
"user": {
"data": {
diff --git a/homeassistant/components/bond/translations/cs.json b/homeassistant/components/bond/translations/cs.json
index 677c7e80236996..13135dbf53e592 100644
--- a/homeassistant/components/bond/translations/cs.json
+++ b/homeassistant/components/bond/translations/cs.json
@@ -15,7 +15,7 @@
"data": {
"access_token": "P\u0159\u00edstupov\u00fd token"
},
- "description": "Chcete nastavit {bond_id} ?"
+ "description": "Chcete nastavit {name}?"
},
"user": {
"data": {
diff --git a/homeassistant/components/bond/translations/de.json b/homeassistant/components/bond/translations/de.json
index 393232025ddc1f..1c1c7375a28b06 100644
--- a/homeassistant/components/bond/translations/de.json
+++ b/homeassistant/components/bond/translations/de.json
@@ -1,10 +1,19 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
"error": {
- "cannot_connect": "Verbindung nicht m\u00f6glich",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"step": {
+ "confirm": {
+ "data": {
+ "access_token": "Zugangstoken"
+ }
+ },
"user": {
"data": {
"access_token": "Zugriffstoken",
diff --git a/homeassistant/components/bond/translations/en.json b/homeassistant/components/bond/translations/en.json
index 945b09b8186cb3..d9ce8ab0fe4b92 100644
--- a/homeassistant/components/bond/translations/en.json
+++ b/homeassistant/components/bond/translations/en.json
@@ -9,13 +9,13 @@
"old_firmware": "Unsupported old firmware on the Bond device - please upgrade before continuing",
"unknown": "Unexpected error"
},
- "flow_title": "Bond: {bond_id} ({host})",
+ "flow_title": "Bond: {name} ({host})",
"step": {
"confirm": {
"data": {
"access_token": "Access Token"
},
- "description": "Do you want to set up {bond_id}?"
+ "description": "Do you want to set up {name}?"
},
"user": {
"data": {
diff --git a/homeassistant/components/bond/translations/et.json b/homeassistant/components/bond/translations/et.json
index dc6a8414bce9ef..5e9a8e4493f44b 100644
--- a/homeassistant/components/bond/translations/et.json
+++ b/homeassistant/components/bond/translations/et.json
@@ -9,13 +9,13 @@
"old_firmware": "Bondi seadme ei toeta vana p\u00fcsivara - uuenda enne j\u00e4tkamist",
"unknown": "Tundmatu viga"
},
- "flow_title": "Bond: {bond_id} ( {host} )",
+ "flow_title": "Bond: {name} ( {host} )",
"step": {
"confirm": {
"data": {
"access_token": "Juurdep\u00e4\u00e4sut\u00f5end"
},
- "description": "Kas soovid seadistada teenuse {bond_id} ?"
+ "description": "Kas soovid seadistada teenust {name} ?"
},
"user": {
"data": {
diff --git a/homeassistant/components/bond/translations/fr.json b/homeassistant/components/bond/translations/fr.json
index 496a21339cbf24..d9eb14b1a620cc 100644
--- a/homeassistant/components/bond/translations/fr.json
+++ b/homeassistant/components/bond/translations/fr.json
@@ -9,13 +9,13 @@
"old_firmware": "Ancien micrologiciel non pris en charge sur l'appareil Bond - veuillez mettre \u00e0 niveau avant de continuer",
"unknown": "Erreur inattendue"
},
- "flow_title": "Bond : {bond_id} ({h\u00f4te})",
+ "flow_title": "Lien : {name} ({host})",
"step": {
"confirm": {
"data": {
"access_token": "Jeton d'acc\u00e8s"
},
- "description": "Voulez-vous configurer {bond_id} ?"
+ "description": "Voulez-vous configurer {name}?"
},
"user": {
"data": {
diff --git a/homeassistant/components/bond/translations/hu.json b/homeassistant/components/bond/translations/hu.json
index 3b2d79a34a77e2..868ef455f5ddc4 100644
--- a/homeassistant/components/bond/translations/hu.json
+++ b/homeassistant/components/bond/translations/hu.json
@@ -2,6 +2,27 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "old_firmware": "Nem t\u00e1mogatott r\u00e9gi firmware a Bond eszk\u00f6z\u00f6n - k\u00e9rj\u00fck friss\u00edtsd, miel\u0151tt folytatn\u00e1d",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "flow_title": "Bond: {name} ({host})",
+ "step": {
+ "confirm": {
+ "data": {
+ "access_token": "Hozz\u00e1f\u00e9r\u00e9si token"
+ },
+ "description": "Szeretn\u00e9d be\u00e1ll\u00edtani a(z) {name}-t?"
+ },
+ "user": {
+ "data": {
+ "access_token": "Hozz\u00e1f\u00e9r\u00e9si token",
+ "host": "Hoszt"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/bond/translations/id.json b/homeassistant/components/bond/translations/id.json
new file mode 100644
index 00000000000000..56c633cf31c7de
--- /dev/null
+++ b/homeassistant/components/bond/translations/id.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "old_firmware": "Firmware lama yang tidak didukung pada perangkat Bond - tingkatkan versi sebelum melanjutkan",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "flow_title": "Bond: {name} ({host})",
+ "step": {
+ "confirm": {
+ "data": {
+ "access_token": "Token Akses"
+ },
+ "description": "Ingin menyiapkan {name}?"
+ },
+ "user": {
+ "data": {
+ "access_token": "Token Akses",
+ "host": "Host"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bond/translations/it.json b/homeassistant/components/bond/translations/it.json
index d3ac1ab6b49094..e22ad82e1fd1a1 100644
--- a/homeassistant/components/bond/translations/it.json
+++ b/homeassistant/components/bond/translations/it.json
@@ -9,13 +9,13 @@
"old_firmware": "Firmware precedente non supportato sul dispositivo Bond - si prega di aggiornare prima di continuare",
"unknown": "Errore imprevisto"
},
- "flow_title": "Bond: {bond_id} ({host})",
+ "flow_title": "Bond: {name} ({host})",
"step": {
"confirm": {
"data": {
"access_token": "Token di accesso"
},
- "description": "Vuoi configurare {bond_id}?"
+ "description": "Vuoi configurare {name}?"
},
"user": {
"data": {
diff --git a/homeassistant/components/bond/translations/ko.json b/homeassistant/components/bond/translations/ko.json
index 61576d7043166b..33a56559f79c8c 100644
--- a/homeassistant/components/bond/translations/ko.json
+++ b/homeassistant/components/bond/translations/ko.json
@@ -1,14 +1,21 @@
{
"config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
"error": {
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "old_firmware": "Bond \uae30\uae30\uc5d0\uc11c \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \uc624\ub798\ub41c \ud38c\uc6e8\uc5b4\uc785\ub2c8\ub2e4. \uacc4\uc18d\ud558\uae30 \uc804\uc5d0 \uc5c5\uadf8\ub808\uc774\ub4dc\ud574\uc8fc\uc138\uc694.",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
- "flow_title": "\ubcf8\ub4dc : {bond_id} ( {host} )",
+ "flow_title": "Bond: {name} ({host})",
"step": {
"confirm": {
- "description": "{bond_id} \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ "data": {
+ "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070"
+ },
+ "description": "{name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
},
"user": {
"data": {
diff --git a/homeassistant/components/bond/translations/nl.json b/homeassistant/components/bond/translations/nl.json
index 8010dfc2e78645..67812678082a5b 100644
--- a/homeassistant/components/bond/translations/nl.json
+++ b/homeassistant/components/bond/translations/nl.json
@@ -6,12 +6,20 @@
"error": {
"cannot_connect": "Kon niet verbinden",
"invalid_auth": "Ongeldige authenticatie",
+ "old_firmware": "Niet-ondersteunde oude firmware op het Bond-apparaat - voer een upgrade uit voordat u doorgaat",
"unknown": "Onverwachte fout"
},
- "flow_title": "Bond: {bond_id} ({host})",
+ "flow_title": "Bond: {name} ({host})",
"step": {
+ "confirm": {
+ "data": {
+ "access_token": "Toegangstoken"
+ },
+ "description": "Wilt u {name} instellen?"
+ },
"user": {
"data": {
+ "access_token": "Toegangstoken",
"host": "Host"
}
}
diff --git a/homeassistant/components/bond/translations/no.json b/homeassistant/components/bond/translations/no.json
index 01ff745eed36c7..c09b7a1763533b 100644
--- a/homeassistant/components/bond/translations/no.json
+++ b/homeassistant/components/bond/translations/no.json
@@ -9,13 +9,13 @@
"old_firmware": "Gammel fastvare som ikke st\u00f8ttes p\u00e5 Bond-enheten \u2013 vennligst oppgrader f\u00f8r du fortsetter",
"unknown": "Uventet feil"
},
- "flow_title": "",
+ "flow_title": "Obligasjon: {name} ({host})",
"step": {
"confirm": {
"data": {
"access_token": "Tilgangstoken"
},
- "description": "Vil du konfigurere {bond_id}?"
+ "description": "Vil du konfigurere {name}?"
},
"user": {
"data": {
diff --git a/homeassistant/components/bond/translations/pl.json b/homeassistant/components/bond/translations/pl.json
index c50c270b74cd50..6f5f2d276ff391 100644
--- a/homeassistant/components/bond/translations/pl.json
+++ b/homeassistant/components/bond/translations/pl.json
@@ -9,13 +9,13 @@
"old_firmware": "Stare, nieobs\u0142ugiwane oprogramowanie na urz\u0105dzeniu Bond - zaktualizuj przed kontynuowaniem",
"unknown": "Nieoczekiwany b\u0142\u0105d"
},
- "flow_title": "Bond: {bond_id} ({host})",
+ "flow_title": "Bond: {name} ({host})",
"step": {
"confirm": {
"data": {
"access_token": "Token dost\u0119pu"
},
- "description": "Czy chcesz skonfigurowa\u0107 {bond_id}?"
+ "description": "Czy chcesz skonfigurowa\u0107 {name}?"
},
"user": {
"data": {
diff --git a/homeassistant/components/bond/translations/ru.json b/homeassistant/components/bond/translations/ru.json
index 493b8e141ce01e..cdc37fc27f7021 100644
--- a/homeassistant/components/bond/translations/ru.json
+++ b/homeassistant/components/bond/translations/ru.json
@@ -5,17 +5,17 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"old_firmware": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0443\u0441\u0442\u0430\u0440\u0435\u043b\u0430 \u0438 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
- "flow_title": "Bond {bond_id} ({host})",
+ "flow_title": "Bond: {name} ({host})",
"step": {
"confirm": {
"data": {
"access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430"
},
- "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 {bond_id}?"
+ "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?"
},
"user": {
"data": {
diff --git a/homeassistant/components/bond/translations/tr.json b/homeassistant/components/bond/translations/tr.json
new file mode 100644
index 00000000000000..3488480a21845a
--- /dev/null
+++ b/homeassistant/components/bond/translations/tr.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "confirm": {
+ "data": {
+ "access_token": "Eri\u015fim Belirteci"
+ }
+ },
+ "user": {
+ "data": {
+ "access_token": "Eri\u015fim Belirteci",
+ "host": "Ana Bilgisayar"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bond/translations/uk.json b/homeassistant/components/bond/translations/uk.json
index d7da60ea1786af..95ede3d43298ef 100644
--- a/homeassistant/components/bond/translations/uk.json
+++ b/homeassistant/components/bond/translations/uk.json
@@ -1,7 +1,28 @@
{
"config": {
"abort": {
- "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e"
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "old_firmware": "\u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043e\u043d\u043e\u0432\u0438\u0442\u0438 \u043c\u0456\u043a\u0440\u043e\u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u043d\u0430 \u0432\u0435\u0440\u0441\u0456\u044f \u0437\u0430\u0441\u0442\u0430\u0440\u0456\u043b\u0430 \u0456 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0454\u044e.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "flow_title": "Bond {bond_id} ({host})",
+ "step": {
+ "confirm": {
+ "data": {
+ "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443"
+ },
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {bond_id}?"
+ },
+ "user": {
+ "data": {
+ "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443",
+ "host": "\u0425\u043e\u0441\u0442"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/bond/translations/zh-Hant.json b/homeassistant/components/bond/translations/zh-Hant.json
index af652c54509efa..8bb8e178869aa9 100644
--- a/homeassistant/components/bond/translations/zh-Hant.json
+++ b/homeassistant/components/bond/translations/zh-Hant.json
@@ -9,17 +9,17 @@
"old_firmware": "Bond \u88dd\u7f6e\u4f7f\u7528\u4e0d\u652f\u63f4\u7684\u820a\u7248\u672c\u97cc\u9ad4 - \u8acb\u66f4\u65b0\u5f8c\u518d\u7e7c\u7e8c",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
- "flow_title": "Bond\uff1a{bond_id} ({host})",
+ "flow_title": "Bond\uff1a{name} ({host})",
"step": {
"confirm": {
"data": {
- "access_token": "\u5b58\u53d6\u5bc6\u9470"
+ "access_token": "\u5b58\u53d6\u6b0a\u6756"
},
- "description": "\u662f\u5426\u8981\u8a2d\u5b9a {bond_id}\uff1f"
+ "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f"
},
"user": {
"data": {
- "access_token": "\u5b58\u53d6\u5bc6\u9470",
+ "access_token": "\u5b58\u53d6\u6b0a\u6756",
"host": "\u4e3b\u6a5f\u7aef"
}
}
diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py
index 5a9fff692faec6..916da69a06cada 100644
--- a/homeassistant/components/bond/utils.py
+++ b/homeassistant/components/bond/utils.py
@@ -1,22 +1,33 @@
"""Reusable utilities for the Bond component."""
+from __future__ import annotations
+
import logging
-from typing import List, Optional
+from typing import Any, cast
+from aiohttp import ClientResponseError
from bond_api import Action, Bond
+from homeassistant.util.async_ import gather_with_concurrency
+
+from .const import BRIDGE_MAKE
+
+MAX_REQUESTS = 6
+
_LOGGER = logging.getLogger(__name__)
class BondDevice:
"""Helper device class to hold ID and attributes together."""
- def __init__(self, device_id: str, attrs: dict, props: dict):
+ def __init__(
+ self, device_id: str, attrs: dict[str, Any], props: dict[str, Any]
+ ) -> None:
"""Create a helper device from ID and attributes returned by API."""
self.device_id = device_id
self.props = props
self._attrs = attrs
- def __repr__(self):
+ def __repr__(self) -> str:
"""Return readable representation of a bond device."""
return {
"device_id": self.device_id,
@@ -27,43 +38,66 @@ def __repr__(self):
@property
def name(self) -> str:
"""Get the name of this device."""
- return self._attrs["name"]
+ return cast(str, self._attrs["name"])
@property
def type(self) -> str:
"""Get the type of this device."""
- return self._attrs["type"]
+ return cast(str, self._attrs["type"])
+
+ @property
+ def location(self) -> str | None:
+ """Get the location of this device."""
+ return self._attrs.get("location")
+
+ @property
+ def template(self) -> str | None:
+ """Return this model template."""
+ return self._attrs.get("template")
+
+ @property
+ def branding_profile(self) -> str | None:
+ """Return this branding profile."""
+ return self.props.get("branding_profile")
@property
def trust_state(self) -> bool:
"""Check if Trust State is turned on."""
return self.props.get("trust_state", False)
+ def _has_any_action(self, actions: set[str]) -> bool:
+ """Check to see if the device supports any of the actions."""
+ supported_actions: list[str] = self._attrs["actions"]
+ for action in supported_actions:
+ if action in actions:
+ return True
+ return False
+
def supports_speed(self) -> bool:
"""Return True if this device supports any of the speed related commands."""
- actions: List[str] = self._attrs["actions"]
- return bool([action for action in actions if action in [Action.SET_SPEED]])
+ return self._has_any_action({Action.SET_SPEED})
def supports_direction(self) -> bool:
"""Return True if this device supports any of the direction related commands."""
- actions: List[str] = self._attrs["actions"]
- return bool([action for action in actions if action in [Action.SET_DIRECTION]])
+ return self._has_any_action({Action.SET_DIRECTION})
def supports_light(self) -> bool:
"""Return True if this device supports any of the light related commands."""
- actions: List[str] = self._attrs["actions"]
- return bool(
- [
- action
- for action in actions
- if action in [Action.TURN_LIGHT_ON, Action.TURN_LIGHT_OFF]
- ]
+ return self._has_any_action({Action.TURN_LIGHT_ON, Action.TURN_LIGHT_OFF})
+
+ def supports_up_light(self) -> bool:
+ """Return true if the device has an up light."""
+ return self._has_any_action({Action.TURN_UP_LIGHT_ON, Action.TURN_UP_LIGHT_OFF})
+
+ def supports_down_light(self) -> bool:
+ """Return true if the device has a down light."""
+ return self._has_any_action(
+ {Action.TURN_DOWN_LIGHT_ON, Action.TURN_DOWN_LIGHT_OFF}
)
def supports_set_brightness(self) -> bool:
"""Return True if this device supports setting a light brightness."""
- actions: List[str] = self._attrs["actions"]
- return bool([action for action in actions if action in [Action.SET_BRIGHTNESS]])
+ return self._has_any_action({Action.SET_BRIGHTNESS})
class BondHub:
@@ -72,49 +106,91 @@ class BondHub:
def __init__(self, bond: Bond):
"""Initialize Bond Hub."""
self.bond: Bond = bond
- self._version: Optional[dict] = None
- self._devices: Optional[List[BondDevice]] = None
+ self._bridge: dict[str, Any] = {}
+ self._version: dict[str, Any] = {}
+ self._devices: list[BondDevice] = []
- async def setup(self):
+ async def setup(self, max_devices: int | None = None) -> None:
"""Read hub version information."""
self._version = await self.bond.version()
_LOGGER.debug("Bond reported the following version info: %s", self._version)
-
# Fetch all available devices using Bond API.
device_ids = await self.bond.devices()
- self._devices = [
- BondDevice(
- device_id,
- await self.bond.device(device_id),
- await self.bond.device_properties(device_id),
+ self._devices = []
+ setup_device_ids = []
+ tasks = []
+ for idx, device_id in enumerate(device_ids):
+ if max_devices is not None and idx >= max_devices:
+ break
+ setup_device_ids.append(device_id)
+ tasks.extend(
+ [self.bond.device(device_id), self.bond.device_properties(device_id)]
)
- for device_id in device_ids
- ]
+
+ responses = await gather_with_concurrency(MAX_REQUESTS, *tasks)
+ response_idx = 0
+ for device_id in setup_device_ids:
+ self._devices.append(
+ BondDevice(
+ device_id, responses[response_idx], responses[response_idx + 1]
+ )
+ )
+ response_idx += 2
_LOGGER.debug("Discovered Bond devices: %s", self._devices)
+ try:
+ # Smart by bond devices do not have a bridge api call
+ self._bridge = await self.bond.bridge()
+ except ClientResponseError:
+ self._bridge = {}
+ _LOGGER.debug("Bond reported the following bridge info: %s", self._bridge)
@property
- def bond_id(self) -> str:
+ def bond_id(self) -> str | None:
"""Return unique Bond ID for this hub."""
- return self._version["bondid"]
+ # Old firmwares are missing the bondid
+ return self._version.get("bondid")
@property
- def target(self) -> str:
- """Return this hub model."""
+ def target(self) -> str | None:
+ """Return this hub target."""
return self._version.get("target")
@property
- def fw_ver(self) -> str:
+ def model(self) -> str | None:
+ """Return this hub model."""
+ return self._version.get("model")
+
+ @property
+ def make(self) -> str:
+ """Return this hub make."""
+ return self._version.get("make", BRIDGE_MAKE)
+
+ @property
+ def name(self) -> str:
+ """Get the name of this bridge."""
+ if not self.is_bridge and self._devices:
+ return self._devices[0].name
+ return cast(str, self._bridge["name"])
+
+ @property
+ def location(self) -> str | None:
+ """Get the location of this bridge."""
+ if not self.is_bridge and self._devices:
+ return self._devices[0].location
+ return self._bridge.get("location")
+
+ @property
+ def fw_ver(self) -> str | None:
"""Return this hub firmware version."""
return self._version.get("fw_ver")
@property
- def devices(self) -> List[BondDevice]:
+ def devices(self) -> list[BondDevice]:
"""Return a list of all devices controlled by this hub."""
return self._devices
@property
def is_bridge(self) -> bool:
"""Return if the Bond is a Bond Bridge."""
- # If False, it means that it is a Smart by Bond product. Assumes that it is if the model is not available.
- return self._version.get("model", "BD-").startswith("BD-")
+ return bool(self._bridge)
diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py
index 46fd8675358df4..d8f6d64b15f188 100644
--- a/homeassistant/components/braviatv/__init__.py
+++ b/homeassistant/components/braviatv/__init__.py
@@ -10,11 +10,6 @@
PLATFORMS = ["media_player"]
-async def async_setup(hass, config):
- """Set up the Bravia TV component."""
- return True
-
-
async def async_setup_entry(hass, config_entry):
"""Set up a config entry."""
host = config_entry.data[CONF_HOST]
@@ -28,9 +23,9 @@ async def async_setup_entry(hass, config_entry):
UNDO_UPDATE_LISTENER: undo_listener,
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return True
@@ -41,8 +36,8 @@ async def async_unload_entry(hass, config_entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py
index d8831dd14941b6..1ac31972f33337 100644
--- a/homeassistant/components/braviatv/config_flow.py
+++ b/homeassistant/components/braviatv/config_flow.py
@@ -12,7 +12,7 @@
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from .const import ( # pylint:disable=unused-import
+from .const import (
ATTR_CID,
ATTR_MAC,
ATTR_MODEL,
diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json
index b17d42ffaed465..7dfff8a1b440a0 100644
--- a/homeassistant/components/braviatv/translations/de.json
+++ b/homeassistant/components/braviatv/translations/de.json
@@ -1,15 +1,19 @@
{
"config": {
"abort": {
- "already_configured": "Dieser Fernseher ist bereits konfiguriert."
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "no_ip_control": "IP-Steuerung ist auf deinen Fernseher deaktiviert oder der Fernseher wird nicht unterst\u00fctzt."
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, ung\u00fcltiger Host- oder PIN-Code.",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse",
"unsupported_model": "Ihr TV-Modell wird nicht unterst\u00fctzt."
},
"step": {
"authorize": {
+ "data": {
+ "pin": "PIN-Code"
+ },
"description": "Geben Sie den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, m\u00fcssen Sie die Registrierung von Home Assistant auf Ihrem Fernseher aufheben, gehen Sie daf\u00fcr zu: Einstellungen -> Netzwerk -> Remote - Ger\u00e4teeinstellungen -> Registrierung des entfernten Ger\u00e4ts aufheben.",
"title": "Autorisieren Sie Sony Bravia TV"
},
diff --git a/homeassistant/components/braviatv/translations/et.json b/homeassistant/components/braviatv/translations/et.json
index b69844ee839b1b..6930186aeba9e0 100644
--- a/homeassistant/components/braviatv/translations/et.json
+++ b/homeassistant/components/braviatv/translations/et.json
@@ -21,7 +21,7 @@
"data": {
"host": ""
},
- "description": "Seadista Sony Bravia TV sidumine. Kuion probleeme seadetega mine: https://www.home-assistant.io/integrations/braviatv \n\nVeenduge, et teler on sisse l\u00fclitatud.",
+ "description": "Seadista Sony Bravia TV sidumine. Kuion probleeme seadetega mine: https://www.home-assistant.io/integrations/braviatv \n\nVeendu, et teler on sisse l\u00fclitatud.",
"title": ""
}
}
diff --git a/homeassistant/components/braviatv/translations/hu.json b/homeassistant/components/braviatv/translations/hu.json
index a87786df1e8fe1..fbb23fdee0432f 100644
--- a/homeassistant/components/braviatv/translations/hu.json
+++ b/homeassistant/components/braviatv/translations/hu.json
@@ -1,15 +1,36 @@
{
"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."
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "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"
+ },
+ "title": "Sony Bravia TV enged\u00e9lyez\u00e9se"
},
"user": {
"data": {
"host": "Hoszt"
- }
+ },
+ "title": "Sony Bravia TV"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "ignored_sources": "Figyelmen k\u00edv\u00fcl hagyott forr\u00e1sok list\u00e1ja"
+ },
+ "title": "A Sony Bravia TV be\u00e1ll\u00edt\u00e1sai"
}
}
}
diff --git a/homeassistant/components/braviatv/translations/id.json b/homeassistant/components/braviatv/translations/id.json
new file mode 100644
index 00000000000000..def84dacdbb217
--- /dev/null
+++ b/homeassistant/components/braviatv/translations/id.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "no_ip_control": "Kontrol IP dinonaktifkan di TV Anda atau TV tidak didukung."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_host": "Nama host atau alamat IP tidak valid",
+ "unsupported_model": "Model TV Anda tidak didukung."
+ },
+ "step": {
+ "authorize": {
+ "data": {
+ "pin": "Kode PIN"
+ },
+ "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia. \n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.",
+ "title": "Otorisasi TV Sony Bravia"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Siapkan integrasi TV Sony Bravia. Jika Anda memiliki masalah dengan konfigurasi, buka: https://www.home-assistant.io/integrations/braviatv \n\nPastikan TV Anda dinyalakan.",
+ "title": "TV Sony Bravia"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "ignored_sources": "Daftar sumber yang diabaikan"
+ },
+ "title": "Pilihan untuk TV Sony Bravia"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/braviatv/translations/ko.json b/homeassistant/components/braviatv/translations/ko.json
index 3a82c38f90448d..7bad0f0047c558 100644
--- a/homeassistant/components/braviatv/translations/ko.json
+++ b/homeassistant/components/braviatv/translations/ko.json
@@ -1,24 +1,27 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 TV \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "no_ip_control": "TV \uc5d0\uc11c IP \uc81c\uc5b4\uac00 \ube44\ud65c\uc131\ud654\ub418\uc5c8\uac70\ub098 TV \uac00 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "no_ip_control": "TV\uc5d0\uc11c IP \uc81c\uc5b4\uac00 \ube44\ud65c\uc131\ud654\ub418\uc5c8\uac70\ub098 TV\uac00 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4."
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8 \ub610\ub294 PIN \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "invalid_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unsupported_model": "\uc774 TV \ubaa8\ub378\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4."
},
"step": {
"authorize": {
- "description": "Sony Bravia TV \uc5d0 \ud45c\uc2dc\ub41c PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\nPIN \ucf54\ub4dc\uac00 \ud45c\uc2dc\ub418\uc9c0 \uc54a\uc73c\uba74 TV \uc5d0\uc11c Home Assistant \ub97c \ub4f1\ub85d \ud574\uc81c\ud558\uc5ec\uc57c \ud569\ub2c8\ub2e4. Settings -> Network -> Remote device settings -> Unregister remote device \ub85c \uc774\ub3d9\ud558\uc5ec \ub4f1\ub85d\uc744 \ud574\uc81c\ud574\uc8fc\uc138\uc694.",
+ "data": {
+ "pin": "PIN \ucf54\ub4dc"
+ },
+ "description": "Sony Bravia TV\uc5d0 \ud45c\uc2dc\ub41c PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\nPIN \ucf54\ub4dc\uac00 \ud45c\uc2dc\ub418\uc9c0 \uc54a\uc73c\uba74 TV\uc5d0\uc11c Home Assistant\ub97c \ub4f1\ub85d \ud574\uc81c\ud558\uc5ec\uc57c \ud569\ub2c8\ub2e4. Settings -> Network -> Remote device settings -> Unregister remote device\ub85c \uc774\ub3d9\ud558\uc5ec \ub4f1\ub85d\uc744 \ud574\uc81c\ud574\uc8fc\uc138\uc694.",
"title": "Sony Bravia TV \uc2b9\uc778\ud558\uae30"
},
"user": {
"data": {
"host": "\ud638\uc2a4\ud2b8"
},
- "description": "Sony Bravia TV \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694. \uad6c\uc131\uc5d0 \ubb38\uc81c\uac00 \uc788\ub294 \uacbd\uc6b0 https://www.home-assistant.io/integrations/braviatv \ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.\n\nTV \uac00 \ucf1c\uc838 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
+ "description": "Sony Bravia TV \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694. \uad6c\uc131\uc5d0 \ubb38\uc81c\uac00 \uc788\ub294 \uacbd\uc6b0 https://www.home-assistant.io/integrations/braviatv \ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.\n\nTV\uac00 \ucf1c\uc838 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
"title": "Sony Bravia TV"
}
}
diff --git a/homeassistant/components/braviatv/translations/nl.json b/homeassistant/components/braviatv/translations/nl.json
index b35d7de45cfed7..5354f5761ecca2 100644
--- a/homeassistant/components/braviatv/translations/nl.json
+++ b/homeassistant/components/braviatv/translations/nl.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Deze tv is al geconfigureerd.",
+ "already_configured": "Apparaat is al geconfigureerd",
"no_ip_control": "IP-besturing is uitgeschakeld op uw tv of de tv wordt niet ondersteund."
},
"error": {
- "cannot_connect": "Geen verbinding, ongeldige host of PIN-code.",
- "invalid_host": "Ongeldige hostnaam of IP-adres.",
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_host": "Ongeldige hostnaam of IP-adres",
"unsupported_model": "Uw tv-model wordt niet ondersteund."
},
"step": {
@@ -19,7 +19,7 @@
},
"user": {
"data": {
- "host": "Hostnaam of IP-adres van tv"
+ "host": "Host"
},
"description": "Stel Sony Bravia TV-integratie in. Als je problemen hebt met de configuratie ga dan naar: https://www.home-assistant.io/integrations/braviatv \n\nZorg ervoor dat uw tv is ingeschakeld.",
"title": "Sony Bravia TV"
diff --git a/homeassistant/components/braviatv/translations/tr.json b/homeassistant/components/braviatv/translations/tr.json
new file mode 100644
index 00000000000000..0853c8028fcb96
--- /dev/null
+++ b/homeassistant/components/braviatv/translations/tr.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unsupported_model": "TV modeliniz desteklenmiyor."
+ },
+ "step": {
+ "authorize": {
+ "title": "Sony Bravia TV'yi yetkilendirin"
+ },
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ },
+ "title": "Sony Bravia TV"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "title": "Sony Bravia TV i\u00e7in se\u00e7enekler"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/braviatv/translations/uk.json b/homeassistant/components/braviatv/translations/uk.json
new file mode 100644
index 00000000000000..7f66329c57ec6b
--- /dev/null
+++ b/homeassistant/components/braviatv/translations/uk.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "no_ip_control": "\u041d\u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0456 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a\u0435\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e IP, \u0430\u0431\u043e \u0446\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430.",
+ "unsupported_model": "\u0426\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f."
+ },
+ "step": {
+ "authorize": {
+ "data": {
+ "pin": "PIN-\u043a\u043e\u0434"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434, \u044f\u043a\u0438\u0439 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Sony Bravia. \n\n\u042f\u043a\u0449\u043e \u0412\u0438 \u043d\u0435 \u0431\u0430\u0447\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0441\u043a\u0430\u0441\u0443\u0432\u0430\u0442\u0438 \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u044e Home Assistant \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0456. \u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 - > \u041c\u0435\u0440\u0435\u0436\u0430 - > \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e - > \u0421\u043a\u0430\u0441\u0443\u0432\u0430\u0442\u0438 \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u044e \u0432\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e.",
+ "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Sony Bravia"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0454\u044e \u043f\u043e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457:\nhttps://www.home-assistant.io/integrations/braviatv",
+ "title": "\u0422\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Sony Bravia"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "ignored_sources": "\u0421\u043f\u0438\u0441\u043e\u043a \u0456\u0433\u043d\u043e\u0440\u043e\u0432\u0430\u043d\u0438\u0445 \u0434\u0436\u0435\u0440\u0435\u043b"
+ },
+ "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Sony Bravia"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py
index a2e770d6c4fed3..158f3a27113106 100644
--- a/homeassistant/components/broadlink/config_flow.py
+++ b/homeassistant/components/broadlink/config_flow.py
@@ -13,15 +13,12 @@
import voluptuous as vol
from homeassistant import config_entries, data_entry_flow
+from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS
+from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE
from homeassistant.helpers import config_validation as cv
-from .const import ( # pylint: disable=unused-import
- DEFAULT_PORT,
- DEFAULT_TIMEOUT,
- DOMAIN,
- DOMAINS_AND_TYPES,
-)
+from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN, DOMAINS_AND_TYPES
from .helpers import format_mac
_LOGGER = logging.getLogger(__name__)
@@ -39,11 +36,7 @@ def __init__(self):
async def async_set_device(self, device, raise_on_progress=True):
"""Define a device for the config flow."""
- supported_types = {
- device_type
- for device_types in DOMAINS_AND_TYPES
- for device_type in device_types[1]
- }
+ supported_types = set.union(*DOMAINS_AND_TYPES.values())
if device.type not in supported_types:
_LOGGER.error(
"Unsupported device: %s. If it worked before, please open "
@@ -57,13 +50,34 @@ async def async_set_device(self, device, raise_on_progress=True):
)
self.device = device
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {
"name": device.name,
"model": device.model,
"host": device.host[0],
}
+ async def async_step_dhcp(self, dhcp_discovery):
+ """Handle dhcp discovery."""
+ host = dhcp_discovery[IP_ADDRESS]
+ unique_id = dhcp_discovery[MAC_ADDRESS].lower().replace(":", "")
+ await self.async_set_unique_id(unique_id)
+ self._abort_if_unique_id_configured(updates={CONF_HOST: host})
+ try:
+ hello = partial(blk.discover, discover_ip_address=host)
+ device = (await self.hass.async_add_executor_job(hello))[0]
+ except IndexError:
+ return self.async_abort(reason="cannot_connect")
+ except OSError as err:
+ if err.errno == errno.ENETUNREACH:
+ return self.async_abort(reason="cannot_connect")
+ return self.async_abort(reason="invalid_host")
+ except Exception as ex: # pylint: disable=broad-except
+ _LOGGER.error("Failed to connect to the device at %s", host, exc_info=ex)
+ return self.async_abort(reason="unknown")
+
+ await self.async_set_device(device)
+ return await self.async_step_auth()
+
async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
errors = {}
@@ -94,7 +108,7 @@ async def async_step_user(self, user_input=None):
else:
device.timeout = timeout
- if self.source != "reauth":
+ if self.source != SOURCE_REAUTH:
await self.async_set_device(device)
self._abort_if_unique_id_configured(
updates={CONF_HOST: device.host[0], CONF_TIMEOUT: timeout}
diff --git a/homeassistant/components/broadlink/const.py b/homeassistant/components/broadlink/const.py
index b10f7e74ba7622..fd060d23b35cb8 100644
--- a/homeassistant/components/broadlink/const.py
+++ b/homeassistant/components/broadlink/const.py
@@ -5,11 +5,26 @@
DOMAIN = "broadlink"
-DOMAINS_AND_TYPES = (
- (REMOTE_DOMAIN, ("RM2", "RM4")),
- (SENSOR_DOMAIN, ("A1", "RM2", "RM4")),
- (SWITCH_DOMAIN, ("BG1", "MP1", "RM2", "RM4", "SP1", "SP2", "SP4", "SP4B")),
-)
+DOMAINS_AND_TYPES = {
+ REMOTE_DOMAIN: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"},
+ SENSOR_DOMAIN: {"A1", "RM4MINI", "RM4PRO", "RMPRO"},
+ SWITCH_DOMAIN: {
+ "BG1",
+ "MP1",
+ "RM4MINI",
+ "RM4PRO",
+ "RMMINI",
+ "RMMINIB",
+ "RMPRO",
+ "SP1",
+ "SP2",
+ "SP2S",
+ "SP3",
+ "SP3S",
+ "SP4",
+ "SP4B",
+ },
+}
DEFAULT_PORT = 80
DEFAULT_TIMEOUT = 5
diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py
index be9c7626ac176d..5b42205993c3a1 100644
--- a/homeassistant/components/broadlink/device.py
+++ b/homeassistant/components/broadlink/device.py
@@ -1,5 +1,6 @@
"""Support for Broadlink devices."""
import asyncio
+from contextlib import suppress
from functools import partial
import logging
@@ -12,6 +13,7 @@
NetworkTimeoutError,
)
+from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
@@ -22,9 +24,9 @@
_LOGGER = logging.getLogger(__name__)
-def get_domains(device_type):
+def get_domains(dev_type):
"""Return the domains available for a device type."""
- return {domain for domain, types in DOMAINS_AND_TYPES if device_type in types}
+ return {d for d, t in DOMAINS_AND_TYPES.items() if dev_type in t}
class BroadlinkDevice:
@@ -94,18 +96,14 @@ async def async_setup(self):
update_manager = get_update_manager(self)
coordinator = update_manager.coordinator
- await coordinator.async_refresh()
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady()
+ await coordinator.async_config_entry_first_refresh()
self.update_manager = update_manager
self.hass.data[DOMAIN].devices[config.entry_id] = self
self.reset_jobs.append(config.add_update_listener(self.async_update))
- try:
+ with suppress(BroadlinkException, OSError):
self.fw_version = await self.hass.async_add_executor_job(api.get_fwversion)
- except (BroadlinkException, OSError):
- pass
# Forward entry setup to related domains.
tasks = (
@@ -174,7 +172,7 @@ async def _async_handle_auth_error(self):
self.hass.async_create_task(
self.hass.config_entries.flow.async_init(
DOMAIN,
- context={"source": "reauth"},
+ context={"source": SOURCE_REAUTH},
data={CONF_NAME: self.name, **self.config.data},
)
)
diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json
index 0562bc306a5116..a1437521cb6452 100644
--- a/homeassistant/components/broadlink/manifest.json
+++ b/homeassistant/components/broadlink/manifest.json
@@ -2,7 +2,13 @@
"domain": "broadlink",
"name": "Broadlink",
"documentation": "https://www.home-assistant.io/integrations/broadlink",
- "requirements": ["broadlink==0.16.0"],
+ "requirements": ["broadlink==0.17.0"],
"codeowners": ["@danielhiversen", "@felipediel"],
- "config_flow": true
+ "config_flow": true,
+ "dhcp": [
+ {"macaddress": "34EA34*"},
+ {"macaddress": "24DFA7*"},
+ {"macaddress": "A043B0*"},
+ {"macaddress": "B4430D*"}
+ ]
}
diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py
index 116c97aeb312fd..dff7ba6b2fdb41 100644
--- a/homeassistant/components/broadlink/remote.py
+++ b/homeassistant/components/broadlink/remote.py
@@ -26,13 +26,14 @@
DOMAIN as RM_DOMAIN,
PLATFORM_SCHEMA,
SERVICE_DELETE_COMMAND,
+ SERVICE_LEARN_COMMAND,
+ SERVICE_SEND_COMMAND,
SUPPORT_DELETE_COMMAND,
SUPPORT_LEARN_COMMAND,
RemoteEntity,
)
from homeassistant.const import CONF_HOST, STATE_OFF
from homeassistant.core import callback
-from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.storage import Store
@@ -108,12 +109,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
Store(hass, CODE_STORAGE_VERSION, f"broadlink_remote_{device.unique_id}_codes"),
Store(hass, FLAG_STORAGE_VERSION, f"broadlink_remote_{device.unique_id}_flags"),
)
-
- loaded = await remote.async_load_storage_files()
- if not loaded:
- _LOGGER.error("Failed to create '%s Remote' entity: Storage error", device.name)
- return
-
async_add_entities([remote], False)
@@ -126,9 +121,11 @@ def __init__(self, device, codes, flags):
self._coordinator = device.update_manager.coordinator
self._code_storage = codes
self._flag_storage = flags
+ self._storage_loaded = False
self._codes = {}
self._flags = defaultdict(int)
self._state = True
+ self._lock = asyncio.Lock()
@property
def name(self):
@@ -171,47 +168,52 @@ def device_info(self):
"sw_version": self._device.fw_version,
}
- def get_code(self, command, device):
- """Return a code and a boolean indicating a toggle command.
+ def _extract_codes(self, commands, device=None):
+ """Extract a list of codes.
If the command starts with `b64:`, extract the code from it.
- Otherwise, extract the code from the dictionary, using the device
- and command as keys.
+ Otherwise, extract the code from storage, using the command and
+ device as keys.
- You need to change the flag whenever a toggle command is sent
- successfully. Use `self._flags[device] ^= 1`.
+ The codes are returned in sublists. For toggle commands, the
+ sublist contains two codes that must be sent alternately with
+ each call.
"""
- if command.startswith("b64:"):
- code, is_toggle_cmd = command[4:], False
-
- else:
- if device is None:
- raise KeyError("You need to specify a device")
+ code_list = []
+ for cmd in commands:
+ if cmd.startswith("b64:"):
+ codes = [cmd[4:]]
- try:
- code = self._codes[device][command]
- except KeyError as err:
- raise KeyError("Command not found") from err
-
- # For toggle commands, alternate between codes in a list.
- if isinstance(code, list):
- code = code[self._flags[device]]
- is_toggle_cmd = True
else:
- is_toggle_cmd = False
+ if device is None:
+ raise ValueError("You need to specify a device")
- try:
- return data_packet(code), is_toggle_cmd
- except ValueError as err:
- raise ValueError("Invalid code") from err
+ try:
+ codes = self._codes[device][cmd]
+ except KeyError as err:
+ raise ValueError(f"Command not found: {repr(cmd)}") from err
+
+ if isinstance(codes, list):
+ codes = codes[:]
+ else:
+ codes = [codes]
+
+ for idx, code in enumerate(codes):
+ try:
+ codes[idx] = data_packet(code)
+ except ValueError as err:
+ raise ValueError(f"Invalid code: {repr(code)}") from err
+
+ code_list.append(codes)
+ return code_list
@callback
- def get_codes(self):
+ def _get_codes(self):
"""Return a dictionary of codes."""
return self._codes
@callback
- def get_flags(self):
+ def _get_flags(self):
"""Return a dictionary of toggle flags.
A toggle flag indicates whether the remote should send an
@@ -242,16 +244,13 @@ async def async_turn_off(self, **kwargs):
self._state = False
self.async_write_ha_state()
- async def async_load_storage_files(self):
- """Load codes and toggle flags from storage files."""
- try:
- self._codes.update(await self._code_storage.async_load() or {})
- self._flags.update(await self._flag_storage.async_load() or {})
-
- except HomeAssistantError:
- return False
-
- return True
+ async def _async_load_storage(self):
+ """Load code and flag storage from disk."""
+ # Exception is intentionally not trapped to
+ # provide feedback if something fails.
+ self._codes.update(await self._code_storage.async_load() or {})
+ self._flags.update(await self._flag_storage.async_load() or {})
+ self._storage_loaded = True
async def async_send_command(self, command, **kwargs):
"""Send a list of commands to a device."""
@@ -261,44 +260,53 @@ async def async_send_command(self, command, **kwargs):
device = kwargs.get(ATTR_DEVICE)
repeat = kwargs[ATTR_NUM_REPEATS]
delay = kwargs[ATTR_DELAY_SECS]
+ service = f"{RM_DOMAIN}.{SERVICE_SEND_COMMAND}"
if not self._state:
_LOGGER.warning(
- "remote.send_command canceled: %s entity is turned off", self.entity_id
+ "%s canceled: %s entity is turned off", service, self.entity_id
)
return
- should_delay = False
+ if not self._storage_loaded:
+ await self._async_load_storage()
- for _, cmd in product(range(repeat), commands):
- if should_delay:
- await asyncio.sleep(delay)
+ try:
+ code_list = self._extract_codes(commands, device)
+ except ValueError as err:
+ _LOGGER.error("Failed to call %s: %s", service, err)
+ raise
- try:
- code, is_toggle_cmd = self.get_code(cmd, device)
+ rf_flags = {0xB2, 0xD7}
+ if not hasattr(self._device.api, "sweep_frequency") and any(
+ c[0] in rf_flags for codes in code_list for c in codes
+ ):
+ err_msg = f"{self.entity_id} doesn't support sending RF commands"
+ _LOGGER.error("Failed to call %s: %s", service, err_msg)
+ raise ValueError(err_msg)
+
+ at_least_one_sent = False
+ for _, codes in product(range(repeat), code_list):
+ if at_least_one_sent:
+ await asyncio.sleep(delay)
- except (KeyError, ValueError) as err:
- _LOGGER.error("Failed to send '%s': %s", cmd, err)
- should_delay = False
- continue
+ if len(codes) > 1:
+ code = codes[self._flags[device]]
+ else:
+ code = codes[0]
try:
await self._device.async_request(self._device.api.send_data, code)
-
- except (AuthorizationError, NetworkTimeoutError, OSError) as err:
- _LOGGER.error("Failed to send '%s': %s", cmd, err)
+ except (BroadlinkException, OSError) as err:
+ _LOGGER.error("Error during %s: %s", service, err)
break
- except BroadlinkException as err:
- _LOGGER.error("Failed to send '%s': %s", cmd, err)
- should_delay = False
- continue
-
- should_delay = True
- if is_toggle_cmd:
+ if len(codes) > 1:
self._flags[device] ^= 1
+ at_least_one_sent = True
- self._flag_storage.async_delay_save(self.get_flags, FLAG_SAVE_DELAY)
+ if at_least_one_sent:
+ self._flag_storage.async_delay_save(self._get_flags, FLAG_SAVE_DELAY)
async def async_learn_command(self, **kwargs):
"""Learn a list of commands from a remote."""
@@ -307,39 +315,50 @@ async def async_learn_command(self, **kwargs):
command_type = kwargs[ATTR_COMMAND_TYPE]
device = kwargs[ATTR_DEVICE]
toggle = kwargs[ATTR_ALTERNATIVE]
+ service = f"{RM_DOMAIN}.{SERVICE_LEARN_COMMAND}"
if not self._state:
_LOGGER.warning(
- "remote.learn_command canceled: %s entity is turned off", self.entity_id
+ "%s canceled: %s entity is turned off", service, self.entity_id
)
return
- if command_type == COMMAND_TYPE_IR:
- learn_command = self._async_learn_ir_command
- else:
- learn_command = self._async_learn_rf_command
+ if not self._storage_loaded:
+ await self._async_load_storage()
- should_store = False
+ async with self._lock:
+ if command_type == COMMAND_TYPE_IR:
+ learn_command = self._async_learn_ir_command
- for command in commands:
- try:
- code = await learn_command(command)
- if toggle:
- code = [code, await learn_command(command)]
+ elif hasattr(self._device.api, "sweep_frequency"):
+ learn_command = self._async_learn_rf_command
- except (AuthorizationError, NetworkTimeoutError, OSError) as err:
- _LOGGER.error("Failed to learn '%s': %s", command, err)
- break
+ else:
+ err_msg = f"{self.entity_id} doesn't support learning RF commands"
+ _LOGGER.error("Failed to call %s: %s", service, err_msg)
+ raise ValueError(err_msg)
+
+ should_store = False
+
+ for command in commands:
+ try:
+ code = await learn_command(command)
+ if toggle:
+ code = [code, await learn_command(command)]
- except BroadlinkException as err:
- _LOGGER.error("Failed to learn '%s': %s", command, err)
- continue
+ except (AuthorizationError, NetworkTimeoutError, OSError) as err:
+ _LOGGER.error("Failed to learn '%s': %s", command, err)
+ break
+
+ except BroadlinkException as err:
+ _LOGGER.error("Failed to learn '%s': %s", command, err)
+ continue
- self._codes.setdefault(device, {}).update({command: code})
- should_store = True
+ self._codes.setdefault(device, {}).update({command: code})
+ should_store = True
- if should_store:
- await self._code_storage.async_save(self._codes)
+ if should_store:
+ await self._code_storage.async_save(self._codes)
async def _async_learn_ir_command(self, command):
"""Learn an infrared command."""
@@ -464,6 +483,9 @@ async def async_delete_command(self, **kwargs):
)
return
+ if not self._storage_loaded:
+ await self._async_load_storage()
+
try:
codes = self._codes[device]
except KeyError as err:
@@ -494,6 +516,6 @@ async def async_delete_command(self, **kwargs):
if not codes:
del self._codes[device]
if self._flags.pop(device, None) is not None:
- self._flag_storage.async_delay_save(self.get_flags, FLAG_SAVE_DELAY)
+ self._flag_storage.async_delay_save(self._get_flags, FLAG_SAVE_DELAY)
- self._code_storage.async_delay_save(self.get_codes, CODE_SAVE_DELAY)
+ self._code_storage.async_delay_save(self._get_codes, CODE_SAVE_DELAY)
diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py
index af350329e8c026..0e42d8c438f652 100644
--- a/homeassistant/components/broadlink/sensor.py
+++ b/homeassistant/components/broadlink/sensor.py
@@ -8,11 +8,11 @@
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
PLATFORM_SCHEMA,
+ SensorEntity,
)
from homeassistant.const import CONF_HOST, PERCENTAGE, TEMP_CELSIUS
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.entity import Entity
from .const import DOMAIN
from .helpers import import_device
@@ -56,7 +56,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors)
-class BroadlinkSensor(Entity):
+class BroadlinkSensor(SensorEntity):
"""Representation of a Broadlink sensor."""
def __init__(self, device, monitored_condition):
diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py
index b4cd43ac493b39..0a98530c8069dd 100644
--- a/homeassistant/components/broadlink/switch.py
+++ b/homeassistant/components/broadlink/switch.py
@@ -109,7 +109,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Broadlink switch."""
device = hass.data[DOMAIN].devices[config_entry.entry_id]
- if device.api.type in {"RM2", "RM4"}:
+ if device.api.type in {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}:
platform_data = hass.data[DOMAIN].platforms.get(SWITCH_DOMAIN, {})
user_defined_switches = platform_data.get(device.api.mac, {})
switches = [
@@ -119,12 +119,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
elif device.api.type == "SP1":
switches = [BroadlinkSP1Switch(device)]
- elif device.api.type == "SP2":
+ elif device.api.type in {"SP2", "SP2S", "SP3", "SP3S", "SP4", "SP4B"}:
switches = [BroadlinkSP2Switch(device)]
- elif device.api.type in {"SP4", "SP4B"}:
- switches = [BroadlinkSP4Switch(device)]
-
elif device.api.type == "BG1":
switches = [BroadlinkBG1Slot(device, slot) for slot in range(1, 3)]
@@ -143,7 +140,6 @@ def __init__(self, device, command_on, command_off):
self._command_on = command_on
self._command_off = command_off
self._coordinator = device.update_manager.coordinator
- self._device_class = None
self._state = None
@property
@@ -174,7 +170,7 @@ def should_poll(self):
@property
def device_class(self):
"""Return device class."""
- return self._device_class
+ return DEVICE_CLASS_SWITCH
@property
def device_info(self):
@@ -254,7 +250,6 @@ class BroadlinkSP1Switch(BroadlinkSwitch):
def __init__(self, device):
"""Initialize the switch."""
super().__init__(device, 1, 0)
- self._device_class = DEVICE_CLASS_OUTLET
@property
def unique_id(self):
@@ -277,10 +272,8 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch):
def __init__(self, device, *args, **kwargs):
"""Initialize the switch."""
super().__init__(device, *args, **kwargs)
- self._state = self._coordinator.data["state"]
- self._load_power = self._coordinator.data["load_power"]
- if device.api.model == "SC1":
- self._device_class = DEVICE_CLASS_SWITCH
+ self._state = self._coordinator.data["pwr"]
+ self._load_power = self._coordinator.data.get("power")
@property
def assumed_state(self):
@@ -292,33 +285,12 @@ def current_power_w(self):
"""Return the current power usage in Watt."""
return self._load_power
- @callback
- def update_data(self):
- """Update data."""
- if self._coordinator.last_update_success:
- self._state = self._coordinator.data["state"]
- self._load_power = self._coordinator.data["load_power"]
- self.async_write_ha_state()
-
-
-class BroadlinkSP4Switch(BroadlinkSP1Switch):
- """Representation of a Broadlink SP4 switch."""
-
- def __init__(self, device, *args, **kwargs):
- """Initialize the switch."""
- super().__init__(device, *args, **kwargs)
- self._state = self._coordinator.data["pwr"]
-
- @property
- def assumed_state(self):
- """Return True if unable to access real state of the switch."""
- return False
-
@callback
def update_data(self):
"""Update data."""
if self._coordinator.last_update_success:
self._state = self._coordinator.data["pwr"]
+ self._load_power = self._coordinator.data.get("power")
self.async_write_ha_state()
@@ -330,7 +302,6 @@ def __init__(self, device, slot):
super().__init__(device, 1, 0)
self._slot = slot
self._state = self._coordinator.data[f"s{slot}"]
- self._device_class = DEVICE_CLASS_OUTLET
@property
def unique_id(self):
@@ -374,7 +345,6 @@ def __init__(self, device, slot):
super().__init__(device, 1, 0)
self._slot = slot
self._state = self._coordinator.data[f"pwr{slot}"]
- self._device_class = DEVICE_CLASS_OUTLET
@property
def unique_id(self):
@@ -391,6 +361,11 @@ def assumed_state(self):
"""Return True if unable to access real state of the switch."""
return False
+ @property
+ def device_class(self):
+ """Return device class."""
+ return DEVICE_CLASS_OUTLET
+
@callback
def update_data(self):
"""Update data."""
diff --git a/homeassistant/components/broadlink/translations/de.json b/homeassistant/components/broadlink/translations/de.json
index f915040635f53a..5704efe37c623f 100644
--- a/homeassistant/components/broadlink/translations/de.json
+++ b/homeassistant/components/broadlink/translations/de.json
@@ -1,13 +1,15 @@
{
"config": {
"abort": {
- "cannot_connect": "Verbindungsfehler",
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse",
"not_supported": "Ger\u00e4t nicht unterst\u00fctzt",
"unknown": "Unerwarteter Fehler"
},
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse",
"unknown": "Unerwarteter Fehler"
},
diff --git a/homeassistant/components/broadlink/translations/hu.json b/homeassistant/components/broadlink/translations/hu.json
index 3b2d79a34a77e2..90213e99aecfcc 100644
--- a/homeassistant/components/broadlink/translations/hu.json
+++ b/homeassistant/components/broadlink/translations/hu.json
@@ -1,7 +1,46 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "flow_title": "{name} ({model} a {host} c\u00edmen)",
+ "step": {
+ "auth": {
+ "title": "Hiteles\u00edt\u00e9s az eszk\u00f6z\u00f6n"
+ },
+ "finish": {
+ "data": {
+ "name": "N\u00e9v"
+ },
+ "title": "V\u00e1lassz egy nevet az eszk\u00f6znek"
+ },
+ "reset": {
+ "description": "{name} ({model} a {host} c\u00edmen) z\u00e1rolva van. A hiteles\u00edt\u00e9shez \u00e9s a konfigur\u00e1ci\u00f3 befejez\u00e9s\u00e9hez fel kell oldani az eszk\u00f6z z\u00e1rol\u00e1s\u00e1t. Utas\u00edt\u00e1sok:\n 1. Nyisd meg a Broadlink alkalmaz\u00e1st.\n 2. Kattints az eszk\u00f6zre.\n 3. Kattints a jobb fels\u0151 sarokban tal\u00e1lhat\u00f3 `...` gombra.\n 4. G\u00f6rgess az oldal alj\u00e1ra.\n 5. Kapcsold ki a z\u00e1rol\u00e1s\u00e1t.",
+ "title": "Az eszk\u00f6z felold\u00e1sa"
+ },
+ "unlock": {
+ "data": {
+ "unlock": "Igen, csin\u00e1ld."
+ },
+ "description": "{name} ({model} a {host} c\u00edmen) z\u00e1rolva van. Ez hiteles\u00edt\u00e9si probl\u00e9m\u00e1khoz vezethet a Home Assistantban. Szeretn\u00e9d feloldani?",
+ "title": "Az eszk\u00f6z felold\u00e1sa (opcion\u00e1lis)"
+ },
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s"
+ },
+ "title": "Csatlakoz\u00e1s az eszk\u00f6zh\u00f6z"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/broadlink/translations/id.json b/homeassistant/components/broadlink/translations/id.json
new file mode 100644
index 00000000000000..89d9b17a800710
--- /dev/null
+++ b/homeassistant/components/broadlink/translations/id.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "cannot_connect": "Gagal terhubung",
+ "invalid_host": "Nama host atau alamat IP tidak valid",
+ "not_supported": "Perangkat tidak didukung",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_host": "Nama host atau alamat IP tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "flow_title": "{name} ({model} di {host})",
+ "step": {
+ "auth": {
+ "title": "Autentikasi ke perangkat"
+ },
+ "finish": {
+ "data": {
+ "name": "Nama"
+ },
+ "title": "Pilih nama untuk perangkat"
+ },
+ "reset": {
+ "description": "{name} ({model} di {host}) dikunci. Anda perlu membuka kunci perangkat untuk mengautentikasi dan menyelesaikan konfigurasi. Ikuti petunjuk berikut:\n1. Buka aplikasi Broadlink.\n2. Klik pada perangkat.\n3. Klik `\u2026` di pojok kanan atas.\n4. Gulir ke bagian bawah halaman.\n5. Nonaktifkan kunci.",
+ "title": "Buka kunci perangkat"
+ },
+ "unlock": {
+ "data": {
+ "unlock": "Ya, lakukan."
+ },
+ "description": "{name} ({model} di {host}) dikunci. Hal ini dapat menyebabkan masalah autentikasi di Home Assistant. Apakah Anda ingin membukanya?",
+ "title": "Buka kunci perangkat (opsional)"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "timeout": "Tenggang waktu"
+ },
+ "title": "Hubungkan ke perangkat"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/broadlink/translations/ko.json b/homeassistant/components/broadlink/translations/ko.json
index b27391e11001bf..ac3ada0b8314fb 100644
--- a/homeassistant/components/broadlink/translations/ko.json
+++ b/homeassistant/components/broadlink/translations/ko.json
@@ -1,40 +1,46 @@
{
"config": {
"abort": {
- "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "already_in_progress": "\uc774 \uae30\uae30\uc5d0 \ub300\ud574 \uc774\ubbf8 \uc9c4\ud589\uc911\uc778 \uad6c\uc131\uc774 \uc788\uc2b5\ub2c8\ub2e4.",
- "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
- "invalid_host": "\uc798\ubabb\ub41c \ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c",
- "not_supported": "\uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \uc7a5\uce58",
- "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "not_supported": "\uae30\uae30\uac00 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
- "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
- "flow_title": "{name} ({host} \uc758 {model})",
+ "flow_title": "{name} ({host}\uc758 {model})",
"step": {
"auth": {
- "title": "\uc7a5\uce58\uc5d0 \uc778\uc99d"
+ "title": "\uae30\uae30\uc5d0 \uc778\uc99d\ud558\uae30"
},
"finish": {
- "title": "\uc7a5\uce58 \uc774\ub984\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624"
+ "data": {
+ "name": "\uc774\ub984"
+ },
+ "title": "\uae30\uae30\uc5d0 \ub300\ud55c \uc774\ub984 \uc120\ud0dd\ud558\uae30"
},
"reset": {
- "title": "\uc7a5\uce58 \uc7a0\uae08 \ud574\uc81c"
+ "description": "{name} ({host}\uc758 {model})\uc774(\uac00) \uc7a0\uaca8 \uc788\uc2b5\ub2c8\ub2e4. \uad6c\uc131\uc744 \uc778\uc99d\ud558\uace0 \uc644\ub8cc\ud558\ub824\uba74 \uae30\uae30\uc758 \uc7a0\uae08\uc744 \ud574\uc81c\ud574\uc57c \ud569\ub2c8\ub2e4. \ub2e4\uc74c\uc744 \ub530\ub77c\uc8fc\uc138\uc694:\n1. Broadlink \uc571\uc744 \uc5f4\uc5b4\uc8fc\uc138\uc694.\n2. \uae30\uae30\ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694.\n3. \uc624\ub978\ucabd \uc0c1\ub2e8\uc758 `...`\ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694.\n4. \ud398\uc774\uc9c0 \ub9e8 \uc544\ub798\ub85c \uc2a4\ud06c\ub864\ud574\uc8fc\uc138\uc694.\n5. \uc7a0\uae08\uc744 \ud574\uc81c\ud574\uc8fc\uc138\uc694.",
+ "title": "\uae30\uae30 \uc7a0\uae08 \ud574\uc81c\ud558\uae30"
},
"unlock": {
"data": {
- "unlock": "\uc608"
+ "unlock": "\uc608, \uc7a0\uae08 \ud574\uc81c\ud558\uaca0\uc2b5\ub2c8\ub2e4."
},
- "description": "\uc7a5\uce58\uac00 \uc7a0\uaca8 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub85c \uc778\ud574 Home Assistant\uc5d0\uc11c \uc778\uc99d \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc7a0\uae08\uc744 \ud574\uc81c \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
- "title": "\uc7a5\uce58 \uc7a0\uae08 \ud574\uc81c (\uc635\uc158)"
+ "description": "{name} ({host}\uc758 {model})\uc774(\uac00) \uc7a0\uaca8 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub85c \uc778\ud574 Home Assistant\uc5d0\uc11c \uc778\uc99d \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc7a0\uae08\uc744 \ud574\uc81c\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "\uae30\uae30 \uc7a0\uae08 \ud574\uc81c\ud558\uae30 (\uc120\ud0dd \uc0ac\ud56d)"
},
"user": {
"data": {
+ "host": "\ud638\uc2a4\ud2b8",
"timeout": "\uc81c\ud55c \uc2dc\uac04"
},
- "title": "\uc7a5\uce58\uc5d0 \uc5f0\uacb0"
+ "title": "\uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/broadlink/translations/nl.json b/homeassistant/components/broadlink/translations/nl.json
index 7205512d3681dc..da75118d5b1ccf 100644
--- a/homeassistant/components/broadlink/translations/nl.json
+++ b/homeassistant/components/broadlink/translations/nl.json
@@ -2,24 +2,43 @@
"config": {
"abort": {
"already_configured": "Apparaat is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
"cannot_connect": "Kon niet verbinden",
+ "invalid_host": "Ongeldige hostnaam of IP-adres",
"not_supported": "Apparaat wordt niet ondersteund",
"unknown": "Onverwachte fout"
},
"error": {
"cannot_connect": "Kon niet verbinden",
+ "invalid_host": "Ongeldige hostnaam of IP-adres",
"unknown": "Onverwachte fout"
},
"flow_title": "{name} ({model} bij {host})",
"step": {
+ "auth": {
+ "title": "Authenticeer naar het apparaat"
+ },
"finish": {
"data": {
"name": "Naam"
- }
+ },
+ "title": "Kies een naam voor het apparaat"
+ },
+ "reset": {
+ "description": "{name} ({model} op {host}) is vergrendeld. U moet het apparaat ontgrendelen om te verifi\u00ebren en de configuratie te voltooien. Instructies:\n 1. Open de Broadlink-app.\n 2. Klik op het apparaat.\n 3. Klik op '...' in de rechterbovenhoek.\n 4. Scrol naar de onderkant van de pagina.\n 5. Schakel het slot uit.",
+ "title": "Ontgrendel het apparaat"
+ },
+ "unlock": {
+ "data": {
+ "unlock": "Ja, doe het."
+ },
+ "description": "{name} ({model} op {host}) is vergrendeld. Dit kan leiden tot authenticatieproblemen in Home Assistant. Wilt u deze ontgrendelen?",
+ "title": "Ontgrendel het apparaat (optioneel)"
},
"user": {
"data": {
- "host": "Host"
+ "host": "Host",
+ "timeout": "Time-out"
},
"title": "Verbinding maken met het apparaat"
}
diff --git a/homeassistant/components/broadlink/translations/tr.json b/homeassistant/components/broadlink/translations/tr.json
new file mode 100644
index 00000000000000..d37a3203476704
--- /dev/null
+++ b/homeassistant/components/broadlink/translations/tr.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor",
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "not_supported": "Cihaz desteklenmiyor",
+ "unknown": "Beklenmeyen hata"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "auth": {
+ "title": "Cihaza kimlik do\u011frulama"
+ },
+ "finish": {
+ "title": "Cihaz i\u00e7in bir isim se\u00e7in"
+ },
+ "reset": {
+ "title": "Cihaz\u0131n kilidini a\u00e7\u0131n"
+ },
+ "unlock": {
+ "data": {
+ "unlock": "Evet, yap."
+ },
+ "title": "Cihaz\u0131n kilidini a\u00e7\u0131n (iste\u011fe ba\u011fl\u0131)"
+ },
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "timeout": "Zaman a\u015f\u0131m\u0131"
+ },
+ "title": "Cihaza ba\u011flan\u0131n"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/broadlink/translations/uk.json b/homeassistant/components/broadlink/translations/uk.json
new file mode 100644
index 00000000000000..ea3e3e75cd6007
--- /dev/null
+++ b/homeassistant/components/broadlink/translations/uk.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430.",
+ "not_supported": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "flow_title": "{name} ({model}, {host})",
+ "step": {
+ "auth": {
+ "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457"
+ },
+ "finish": {
+ "data": {
+ "name": "\u041d\u0430\u0437\u0432\u0430"
+ },
+ "title": "\u0412\u043a\u0430\u0436\u0456\u0442\u044c \u043d\u0430\u0437\u0432\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ },
+ "reset": {
+ "description": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 {name} ({model}, {host}) \u0437\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e. \u0414\u043e\u0442\u0440\u0438\u043c\u0443\u0439\u0442\u0435\u0441\u044f \u0446\u0438\u0445 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0439, \u0449\u043e\u0431 \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u0442\u0438 \u0439\u043e\u0433\u043e:\n 1. \u0412\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0443 Broadlink.\n 2. \u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439.\n 3. \u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c `...` \u0432 \u043f\u0440\u0430\u0432\u043e\u043c\u0443 \u0432\u0435\u0440\u0445\u043d\u044c\u043e\u043c\u0443 \u043a\u0443\u0442\u0456.\n 4. \u041f\u0440\u043e\u043a\u0440\u0443\u0442\u0456\u0442\u044c \u0441\u0442\u043e\u0440\u0456\u043d\u043a\u0443 \u0432\u043d\u0438\u0437.\n 5. \u0412\u0438\u043c\u043a\u043d\u0456\u0442\u044c \u0431\u043b\u043e\u043a\u0443\u0432\u0430\u043d\u043d\u044f.",
+ "title": "\u0420\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ },
+ "unlock": {
+ "data": {
+ "unlock": "\u0422\u0430\u043a, \u0437\u0440\u043e\u0431\u0438\u0442\u0438 \u0446\u0435."
+ },
+ "description": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 {name} ({model}, {host}) \u0437\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e. \u0426\u0435 \u043c\u043e\u0436\u0435 \u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u0434\u043e \u043f\u0440\u043e\u0431\u043b\u0435\u043c \u0437 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0454\u044e \u0432 Home Assistant. \u0425\u043e\u0447\u0435\u0442\u0435 \u0439\u043e\u0433\u043e \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u0442\u0438?",
+ "title": "\u0420\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442"
+ },
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py
index c9b273218b59bf..8401dba8c0d840 100644
--- a/homeassistant/components/broadlink/updater.py
+++ b/homeassistant/components/broadlink/updater.py
@@ -1,17 +1,9 @@
"""Support for fetching data from Broadlink devices."""
from abc import ABC, abstractmethod
from datetime import timedelta
-from functools import partial
import logging
-import broadlink as blk
-from broadlink.exceptions import (
- AuthorizationError,
- BroadlinkException,
- CommandNotSupportedError,
- NetworkTimeoutError,
- StorageError,
-)
+from broadlink.exceptions import AuthorizationError, BroadlinkException
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt
@@ -21,17 +13,20 @@
def get_update_manager(device):
"""Return an update manager for a given Broadlink device."""
- if device.api.model.startswith("RM mini"):
- return BroadlinkRMMini3UpdateManager(device)
-
update_managers = {
"A1": BroadlinkA1UpdateManager,
"BG1": BroadlinkBG1UpdateManager,
"MP1": BroadlinkMP1UpdateManager,
- "RM2": BroadlinkRMUpdateManager,
- "RM4": BroadlinkRMUpdateManager,
+ "RM4MINI": BroadlinkRMUpdateManager,
+ "RM4PRO": BroadlinkRMUpdateManager,
+ "RMMINI": BroadlinkRMUpdateManager,
+ "RMMINIB": BroadlinkRMUpdateManager,
+ "RMPRO": BroadlinkRMUpdateManager,
"SP1": BroadlinkSP1UpdateManager,
"SP2": BroadlinkSP2UpdateManager,
+ "SP2S": BroadlinkSP2UpdateManager,
+ "SP3": BroadlinkSP2UpdateManager,
+ "SP3S": BroadlinkSP2UpdateManager,
"SP4": BroadlinkSP4UpdateManager,
"SP4B": BroadlinkSP4UpdateManager,
}
@@ -114,28 +109,18 @@ async def async_fetch_data(self):
return await self.device.async_request(self.device.api.check_power)
-class BroadlinkRMMini3UpdateManager(BroadlinkUpdateManager):
- """Manages updates for Broadlink RM mini 3 devices."""
+class BroadlinkRMUpdateManager(BroadlinkUpdateManager):
+ """Manages updates for Broadlink remotes."""
async def async_fetch_data(self):
"""Fetch data from the device."""
- hello = partial(
- blk.discover,
- discover_ip_address=self.device.api.host[0],
- timeout=self.device.api.timeout,
- )
- devices = await self.device.hass.async_add_executor_job(hello)
- if not devices:
- raise NetworkTimeoutError("The device is offline")
- return {}
+ device = self.device
+ if hasattr(device.api, "check_sensors"):
+ return await device.async_request(device.api.check_sensors)
-class BroadlinkRMUpdateManager(BroadlinkUpdateManager):
- """Manages updates for Broadlink RM2 and RM4 devices."""
-
- async def async_fetch_data(self):
- """Fetch data from the device."""
- return await self.device.async_request(self.device.api.check_sensors)
+ await device.async_request(device.api.update)
+ return {}
class BroadlinkSP1UpdateManager(BroadlinkUpdateManager):
@@ -151,14 +136,14 @@ class BroadlinkSP2UpdateManager(BroadlinkUpdateManager):
async def async_fetch_data(self):
"""Fetch data from the device."""
+ device = self.device
+
data = {}
- data["state"] = await self.device.async_request(self.device.api.check_power)
- try:
- data["load_power"] = await self.device.async_request(
- self.device.api.get_energy
- )
- except (CommandNotSupportedError, StorageError):
- data["load_power"] = None
+ data["pwr"] = await device.async_request(device.api.check_power)
+
+ if hasattr(device.api, "get_energy"):
+ data["power"] = await device.async_request(device.api.get_energy)
+
return data
diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py
index 8c5cdb2d7edd35..cd5a8b444b38d2 100644
--- a/homeassistant/components/brother/__init__.py
+++ b/homeassistant/components/brother/__init__.py
@@ -6,12 +6,12 @@
from brother import Brother, SnmpError, UnsupportedModel
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_HOST, CONF_TYPE, EVENT_HOMEASSISTANT_STOP
-from homeassistant.core import Config, HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.const import CONF_HOST, CONF_TYPE
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-from .const import DOMAIN
+from .const import DATA_CONFIG_ENTRY, DOMAIN, SNMP
+from .utils import get_snmp_engine
PLATFORMS = ["sensor"]
@@ -20,29 +20,26 @@
_LOGGER = logging.getLogger(__name__)
-async def async_setup(hass: HomeAssistant, config: Config):
- """Set up the Brother component."""
- return True
-
-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Brother from a config entry."""
host = entry.data[CONF_HOST]
kind = entry.data[CONF_TYPE]
- coordinator = BrotherDataUpdateCoordinator(hass, host=host, kind=kind)
- await coordinator.async_refresh()
+ snmp_engine = get_snmp_engine(hass)
- if not coordinator.last_update_success:
- coordinator.shutdown()
- raise ConfigEntryNotReady
+ coordinator = BrotherDataUpdateCoordinator(
+ hass, host=host, kind=kind, snmp_engine=snmp_engine
+ )
+ await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
- hass.data[DOMAIN][entry.entry_id] = coordinator
+ hass.data[DOMAIN].setdefault(DATA_CONFIG_ENTRY, {})
+ hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = coordinator
+ hass.data[DOMAIN][SNMP] = snmp_engine
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -53,13 +50,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
if unload_ok:
- hass.data[DOMAIN].pop(entry.entry_id).shutdown()
+ hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id)
+ if not hass.data[DOMAIN][DATA_CONFIG_ENTRY]:
+ hass.data[DOMAIN].pop(SNMP)
+ hass.data[DOMAIN].pop(DATA_CONFIG_ENTRY)
return unload_ok
@@ -67,12 +67,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
class BrotherDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching Brother data from the printer."""
- def __init__(self, hass, host, kind):
+ def __init__(self, hass, host, kind, snmp_engine):
"""Initialize."""
- self.brother = Brother(host, kind=kind)
- self._unsub_stop = hass.bus.async_listen(
- EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop
- )
+ self.brother = Brother(host, kind=kind, snmp_engine=snmp_engine)
super().__init__(
hass,
@@ -83,22 +80,8 @@ def __init__(self, hass, host, kind):
async def _async_update_data(self):
"""Update data via library."""
- # Race condition on shutdown. Stop all the fetches.
- if self._unsub_stop is None:
- return None
-
try:
await self.brother.async_update()
except (ConnectionError, SnmpError, UnsupportedModel) as error:
raise UpdateFailed(error) from error
return self.brother.data
-
- def shutdown(self):
- """Shutdown the Brother coordinator."""
- self._unsub_stop()
- self._unsub_stop = None
- self.brother.shutdown()
-
- def _handle_ha_stop(self, _):
- """Handle Home Assistant stopping."""
- self.shutdown()
diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py
index aa9d7ce53a3e40..1d635984b7223f 100644
--- a/homeassistant/components/brother/config_flow.py
+++ b/homeassistant/components/brother/config_flow.py
@@ -8,7 +8,8 @@
from homeassistant import config_entries, exceptions
from homeassistant.const import CONF_HOST, CONF_TYPE
-from .const import DOMAIN, PRINTER_TYPES # pylint:disable=unused-import
+from .const import DOMAIN, PRINTER_TYPES
+from .utils import get_snmp_engine
DATA_SCHEMA = vol.Schema(
{
@@ -48,9 +49,10 @@ async def async_step_user(self, user_input=None):
if not host_valid(user_input[CONF_HOST]):
raise InvalidHost()
- brother = Brother(user_input[CONF_HOST])
+ snmp_engine = get_snmp_engine(self.hass)
+
+ brother = Brother(user_input[CONF_HOST], snmp_engine=snmp_engine)
await brother.async_update()
- brother.shutdown()
await self.async_set_unique_id(brother.serial.lower())
self._abort_if_unique_id_configured()
@@ -83,7 +85,9 @@ async def async_step_zeroconf(self, discovery_info):
# Hostname is format: brother.local.
self.host = discovery_info["hostname"].rstrip(".")
- self.brother = Brother(self.host)
+ snmp_engine = get_snmp_engine(self.hass)
+
+ self.brother = Brother(self.host, snmp_engine=snmp_engine)
try:
await self.brother.async_update()
except (ConnectionError, SnmpError, UnsupportedModel):
@@ -93,7 +97,6 @@ async def async_step_zeroconf(self, discovery_info):
await self.async_set_unique_id(self.brother.serial.lower())
self._abort_if_unique_id_configured()
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update(
{
"title_placeholders": {
@@ -108,7 +111,6 @@ async def async_step_zeroconf_confirm(self, user_input=None):
"""Handle a flow initiated by zeroconf."""
if user_input is not None:
title = f"{self.brother.model} {self.brother.serial}"
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
return self.async_create_entry(
title=title,
data={CONF_HOST: self.host, CONF_TYPE: user_input[CONF_TYPE]},
diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py
index 5aecde16327d53..07843b0f3d0b1c 100644
--- a/homeassistant/components/brother/const.py
+++ b/homeassistant/components/brother/const.py
@@ -1,5 +1,5 @@
"""Constants for Brother integration."""
-from homeassistant.const import PERCENTAGE
+from homeassistant.const import ATTR_ICON, PERCENTAGE
ATTR_BELT_UNIT_REMAINING_LIFE = "belt_unit_remaining_life"
ATTR_BLACK_DRUM_COUNTER = "black_drum_counter"
@@ -20,7 +20,6 @@
ATTR_DUPLEX_COUNTER = "duplex_unit_pages_counter"
ATTR_ENABLED = "enabled"
ATTR_FUSER_REMAINING_LIFE = "fuser_remaining_life"
-ATTR_ICON = "icon"
ATTR_LABEL = "label"
ATTR_LASER_REMAINING_LIFE = "laser_remaining_life"
ATTR_MAGENTA_DRUM_COUNTER = "magenta_drum_counter"
@@ -41,12 +40,16 @@
ATTR_YELLOW_INK_REMAINING = "yellow_ink_remaining"
ATTR_YELLOW_TONER_REMAINING = "yellow_toner_remaining"
+DATA_CONFIG_ENTRY = "config_entry"
+
DOMAIN = "brother"
UNIT_PAGES = "p"
PRINTER_TYPES = ["laser", "ink"]
+SNMP = "snmp"
+
SENSOR_TYPES = {
ATTR_STATUS: {
ATTR_ICON: "mdi:printer",
diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json
index 9bb9ba00261c83..13933b7bf60460 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==0.1.20"],
+ "requirements": ["brother==0.2.2"],
"zeroconf": [{ "type": "_printer._tcp.local.", "name": "brother*" }],
"config_flow": true,
"quality_scale": "platinum"
diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py
index 40e2deae67dc71..0b614ffa5825e0 100644
--- a/homeassistant/components/brother/sensor.py
+++ b/homeassistant/components/brother/sensor.py
@@ -1,4 +1,5 @@
"""Support for the Brother service."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_TIMESTAMP
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -24,6 +25,7 @@
ATTR_YELLOW_DRUM_COUNTER,
ATTR_YELLOW_DRUM_REMAINING_LIFE,
ATTR_YELLOW_DRUM_REMAINING_PAGES,
+ DATA_CONFIG_ENTRY,
DOMAIN,
SENSOR_TYPES,
)
@@ -37,7 +39,7 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Add Brother entities from a config_entry."""
- coordinator = hass.data[DOMAIN][config_entry.entry_id]
+ coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id]
sensors = []
@@ -55,7 +57,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors, False)
-class BrotherPrinterSensor(CoordinatorEntity):
+class BrotherPrinterSensor(CoordinatorEntity, SensorEntity):
"""Define an Brother Printer sensor."""
def __init__(self, coordinator, kind, device_info):
@@ -87,7 +89,7 @@ def device_class(self):
return None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
remaining_pages = None
drum_counter = None
diff --git a/homeassistant/components/brother/translations/de.json b/homeassistant/components/brother/translations/de.json
index 72bd052cc1d507..c2a7ae8ec76b77 100644
--- a/homeassistant/components/brother/translations/de.json
+++ b/homeassistant/components/brother/translations/de.json
@@ -5,7 +5,7 @@
"unsupported_model": "Dieses Druckermodell wird nicht unterst\u00fctzt."
},
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"snmp_error": "SNMP-Server deaktiviert oder Drucker nicht unterst\u00fctzt.",
"wrong_host": " Ung\u00fcltiger Hostname oder IP-Adresse"
},
diff --git a/homeassistant/components/brother/translations/et.json b/homeassistant/components/brother/translations/et.json
index 190db6ed7687c3..7b2b7c1b4a5abc 100644
--- a/homeassistant/components/brother/translations/et.json
+++ b/homeassistant/components/brother/translations/et.json
@@ -16,7 +16,7 @@
"host": "Host",
"type": "Printeri t\u00fc\u00fcp"
},
- "description": "Seadistage Brotheri printeri sidumine. Kui teil on seadistamisega probleeme minge aadressile https://www.home-assistant.io/integrations/brother"
+ "description": "Seadista Brotheri printeri sidumine. Kui seadistamisega on probleeme mine aadressile https://www.home-assistant.io/integrations/brother"
},
"zeroconf_confirm": {
"data": {
diff --git a/homeassistant/components/brother/translations/hu.json b/homeassistant/components/brother/translations/hu.json
index dd5711cc516928..ae950f58f7271d 100644
--- a/homeassistant/components/brother/translations/hu.json
+++ b/homeassistant/components/brother/translations/hu.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Ez a nyomtat\u00f3 m\u00e1r konfigur\u00e1lva van.",
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
"unsupported_model": "Ez a nyomtat\u00f3modell nem t\u00e1mogatott."
},
"error": {
diff --git a/homeassistant/components/brother/translations/id.json b/homeassistant/components/brother/translations/id.json
new file mode 100644
index 00000000000000..5e0b562017ceb8
--- /dev/null
+++ b/homeassistant/components/brother/translations/id.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "unsupported_model": "Model printer ini tidak didukung."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "snmp_error": "Server SNMP dimatikan atau printer tidak didukung.",
+ "wrong_host": "Nama host atau alamat IP tidak valid."
+ },
+ "flow_title": "Printer Brother: {model} {serial_number}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "type": "Jenis printer"
+ },
+ "description": "Siapkan integrasi printer Brother. Jika Anda memiliki masalah dengan konfigurasi, buka: https://www.home-assistant.io/integrations/brother"
+ },
+ "zeroconf_confirm": {
+ "data": {
+ "type": "Jenis printer"
+ },
+ "description": "Ingin menambahkan Printer Brother {model} dengan nomor seri `{serial_number}` ke Home Assistant?",
+ "title": "Perangkat Printer Brother yang Ditemukan"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/brother/translations/ko.json b/homeassistant/components/brother/translations/ko.json
index a54aea7f108e21..2d3b587e475108 100644
--- a/homeassistant/components/brother/translations/ko.json
+++ b/homeassistant/components/brother/translations/ko.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 \ud504\ub9b0\ud130\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unsupported_model": "\uc774 \ud504\ub9b0\ud130 \ubaa8\ub378\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4."
},
"error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"snmp_error": "SNMP \uc11c\ubc84\uac00 \uaebc\uc838 \uc788\uac70\ub098 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \ud504\ub9b0\ud130\uc785\ub2c8\ub2e4.",
"wrong_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
},
@@ -21,7 +22,7 @@
"data": {
"type": "\ud504\ub9b0\ud130\uc758 \uc885\ub958"
},
- "description": "\uc2dc\ub9ac\uc5bc \ubc88\ud638 `{serial_number}` \ub85c \ube0c\ub77c\ub354 \ud504\ub9b0\ud130 {model} \uc744(\ub97c) Home Assistant \uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "description": "\uc2dc\ub9ac\uc5bc \ubc88\ud638 `{serial_number}`\uc758 {model} \ube0c\ub77c\ub354 \ud504\ub9b0\ud130\ub97c Home Assistant\uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "\ubc1c\uacac\ub41c \ube0c\ub77c\ub354 \ud504\ub9b0\ud130"
}
}
diff --git a/homeassistant/components/brother/translations/nl.json b/homeassistant/components/brother/translations/nl.json
index d754b2df9c1bf0..531038d827b8c5 100644
--- a/homeassistant/components/brother/translations/nl.json
+++ b/homeassistant/components/brother/translations/nl.json
@@ -13,7 +13,7 @@
"step": {
"user": {
"data": {
- "host": "Printerhostnaam of IP-adres",
+ "host": "Host",
"type": "Type printer"
},
"description": "Zet Brother printerintegratie op. Als u problemen heeft met de configuratie ga dan naar: https://www.home-assistant.io/integrations/brother"
diff --git a/homeassistant/components/brother/translations/tr.json b/homeassistant/components/brother/translations/tr.json
index 160a5ecc7b78a2..cd91a4852527a3 100644
--- a/homeassistant/components/brother/translations/tr.json
+++ b/homeassistant/components/brother/translations/tr.json
@@ -1,6 +1,20 @@
{
"config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "unsupported_model": "Bu yaz\u0131c\u0131 modeli desteklenmiyor."
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "flow_title": "Brother Yaz\u0131c\u0131: {model} {serial_number}",
"step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "type": "Yaz\u0131c\u0131n\u0131n t\u00fcr\u00fc"
+ }
+ },
"zeroconf_confirm": {
"title": "Ke\u015ffedilen Brother Yaz\u0131c\u0131"
}
diff --git a/homeassistant/components/brother/translations/uk.json b/homeassistant/components/brother/translations/uk.json
new file mode 100644
index 00000000000000..ac5943aa85cc15
--- /dev/null
+++ b/homeassistant/components/brother/translations/uk.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "unsupported_model": "\u0426\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "snmp_error": "\u0421\u0435\u0440\u0432\u0435\u0440 SNMP \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0430\u0431\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.",
+ "wrong_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430."
+ },
+ "flow_title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 Brother: {model} {serial_number}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "type": "\u0422\u0438\u043f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430"
+ },
+ "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0454\u044e \u043f\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457: https://www.home-assistant.io/integrations/brother."
+ },
+ "zeroconf_confirm": {
+ "data": {
+ "type": "\u0422\u0438\u043f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430"
+ },
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 Brother {model} \u0437 \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serial_number}`?",
+ "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440 Brother"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/brother/utils.py b/homeassistant/components/brother/utils.py
new file mode 100644
index 00000000000000..3a53f4c04a2d60
--- /dev/null
+++ b/homeassistant/components/brother/utils.py
@@ -0,0 +1,30 @@
+"""Brother helpers functions."""
+import logging
+
+import pysnmp.hlapi.asyncio as hlapi
+from pysnmp.hlapi.asyncio.cmdgen import lcd
+
+from homeassistant.const import EVENT_HOMEASSISTANT_STOP
+from homeassistant.core import callback
+from homeassistant.helpers import singleton
+
+from .const import DOMAIN, SNMP
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@singleton.singleton("snmp_engine")
+def get_snmp_engine(hass):
+ """Get SNMP engine."""
+ _LOGGER.debug("Creating SNMP engine")
+ snmp_engine = hlapi.SnmpEngine()
+
+ @callback
+ def shutdown_listener(ev):
+ if hass.data.get(DOMAIN):
+ _LOGGER.debug("Unconfiguring SNMP engine")
+ lcd.unconfigure(hass.data[DOMAIN][SNMP], None)
+
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_listener)
+
+ return snmp_engine
diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py
index 7b2c3e585e309f..32af96dfe604a1 100644
--- a/homeassistant/components/brottsplatskartan/sensor.py
+++ b/homeassistant/components/brottsplatskartan/sensor.py
@@ -7,7 +7,7 @@
import brottsplatskartan
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_LATITUDE,
@@ -15,7 +15,6 @@
CONF_NAME,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -78,7 +77,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([BrottsplatskartanSensor(bpk, name)], True)
-class BrottsplatskartanSensor(Entity):
+class BrottsplatskartanSensor(SensorEntity):
"""Representation of a Brottsplatskartan Sensor."""
def __init__(self, bpk, name):
@@ -99,7 +98,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attributes
diff --git a/homeassistant/components/browser/services.yaml b/homeassistant/components/browser/services.yaml
index 460def22dc1445..1014e50db21204 100644
--- a/homeassistant/components/browser/services.yaml
+++ b/homeassistant/components/browser/services.yaml
@@ -1,6 +1,12 @@
browse_url:
- description: Open a URL in the default browser on the host machine of Home Assistant.
+ name: Browse
+ description:
+ Open a URL in the default browser on the host machine of Home Assistant.
fields:
url:
+ name: URL
description: The URL to open.
+ required: true
example: "https://www.home-assistant.io"
+ selector:
+ text:
diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py
index ceb56ba03fa750..9c539fe51fe1c9 100644
--- a/homeassistant/components/brunt/cover.py
+++ b/homeassistant/components/brunt/cover.py
@@ -131,7 +131,7 @@ def is_closing(self):
return self.move_state == 2
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the detailed device state attributes."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py
index bab4af29422a2c..f452451050b63a 100644
--- a/homeassistant/components/bsblan/__init__.py
+++ b/homeassistant/components/bsblan/__init__.py
@@ -9,18 +9,12 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.typing import ConfigType
from .const import CONF_PASSKEY, DATA_BSBLAN_CLIENT, DOMAIN
SCAN_INTERVAL = timedelta(seconds=30)
-async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
- """Set up the BSB-Lan component."""
- return True
-
-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up BSB-Lan from a config entry."""
diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py
index a97c13c3424098..4d83fb04dbe872 100644
--- a/homeassistant/components/bsblan/climate.py
+++ b/homeassistant/components/bsblan/climate.py
@@ -1,7 +1,9 @@
"""BSBLAN platform to control a compatible Climate Device."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Any, Callable, Dict, List, Optional
+from typing import Any, Callable
from bsblan import BSBLan, BSBLanError, Info, State
@@ -74,7 +76,7 @@
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up BSBLan device based on a config entry."""
bsblan: BSBLan = hass.data[DOMAIN][entry.entry_id][DATA_BSBLAN_CLIENT]
@@ -92,10 +94,10 @@ def __init__(
info: Info,
):
"""Initialize BSBLan climate device."""
- self._current_temperature: Optional[float] = None
+ self._current_temperature: float | None = None
self._available = True
- self._hvac_mode: Optional[str] = None
- self._target_temperature: Optional[float] = None
+ self._hvac_mode: str | None = None
+ self._target_temperature: float | None = None
self._temperature_unit = None
self._preset_mode = None
self._store_hvac_mode = None
@@ -229,7 +231,7 @@ async def async_update(self) -> None:
self._temperature_unit = state.current_temperature.unit
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about this BSBLan device."""
return {
ATTR_IDENTIFIERS: {(DOMAIN, self._info.device_identification)},
diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py
index dee04e6ef85056..f5df1df043728f 100644
--- a/homeassistant/components/bsblan/config_flow.py
+++ b/homeassistant/components/bsblan/config_flow.py
@@ -1,6 +1,8 @@
"""Config flow for BSB-Lan integration."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from bsblan import BSBLan, BSBLanError, Info
import voluptuous as vol
@@ -10,11 +12,7 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
-from .const import ( # pylint:disable=unused-import
- CONF_DEVICE_IDENT,
- CONF_PASSKEY,
- DOMAIN,
-)
+from .const import CONF_DEVICE_IDENT, CONF_PASSKEY, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -26,8 +24,8 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN):
CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL
async def async_step_user(
- self, user_input: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, user_input: ConfigType | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initiated by the user."""
if user_input is None:
return self._show_setup_form()
@@ -59,7 +57,7 @@ async def async_step_user(
},
)
- def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
+ def _show_setup_form(self, errors: dict | None = None) -> dict[str, Any]:
"""Show the setup form to the user."""
return self.async_show_form(
step_id="user",
@@ -78,9 +76,9 @@ def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
async def _get_bsblan_info(
self,
host: str,
- username: Optional[str],
- password: Optional[str],
- passkey: Optional[str],
+ username: str | None,
+ password: str | None,
+ passkey: str | None,
port: int,
) -> Info:
"""Get device information from an BSBLan device."""
diff --git a/homeassistant/components/bsblan/translations/de.json b/homeassistant/components/bsblan/translations/de.json
index 5fd61c0bfedb6c..d1400529b0b84d 100644
--- a/homeassistant/components/bsblan/translations/de.json
+++ b/homeassistant/components/bsblan/translations/de.json
@@ -4,16 +4,20 @@
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
+ "flow_title": "BSB-Lan: {name}",
"step": {
"user": {
"data": {
"host": "Host",
+ "passkey": "Passkey String",
"password": "Passwort",
"port": "Port Nummer",
"username": "Benutzername"
- }
+ },
+ "description": "Richte dein BSB-Lan Ger\u00e4t f\u00fcr die Integration mit dem Home Assistant ein.",
+ "title": "Verbinden mit dem BSB-Lan Ger\u00e4t"
}
}
}
diff --git a/homeassistant/components/bsblan/translations/hu.json b/homeassistant/components/bsblan/translations/hu.json
index 1d28556ba1ad5a..50d250cc384dd5 100644
--- a/homeassistant/components/bsblan/translations/hu.json
+++ b/homeassistant/components/bsblan/translations/hu.json
@@ -1,13 +1,19 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
+ "flow_title": "BSB-Lan: {name}",
"step": {
"user": {
"data": {
"host": "Hoszt",
- "port": "Port"
+ "password": "Jelsz\u00f3",
+ "port": "Port",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
}
}
}
diff --git a/homeassistant/components/bsblan/translations/id.json b/homeassistant/components/bsblan/translations/id.json
new file mode 100644
index 00000000000000..6e8ac0bd4cbbf9
--- /dev/null
+++ b/homeassistant/components/bsblan/translations/id.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "flow_title": "BSB-Lan: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "passkey": "String kunci sandi",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "username": "Nama Pengguna"
+ },
+ "description": "Siapkan perangkat BSB-Lan Anda untuk diintegrasikan dengan Home Assistant.",
+ "title": "Hubungkan ke perangkat BSB-Lan"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bsblan/translations/ko.json b/homeassistant/components/bsblan/translations/ko.json
index 41b421ff8174cb..65843a258339af 100644
--- a/homeassistant/components/bsblan/translations/ko.json
+++ b/homeassistant/components/bsblan/translations/ko.json
@@ -3,15 +3,20 @@
"abort": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
"flow_title": "BSB-Lan: {name}",
"step": {
"user": {
"data": {
"host": "\ud638\uc2a4\ud2b8",
"passkey": "\ud328\uc2a4\ud0a4 \ubb38\uc790\uc5f4",
- "port": "\ud3ec\ud2b8"
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "port": "\ud3ec\ud2b8",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
},
- "description": "Home Assistant \uc5d0 BSB-Lan \uae30\uae30 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.",
+ "description": "Home Assistant\uc5d0 BSB-Lan \uae30\uae30 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.",
"title": "BSB-Lan \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
diff --git a/homeassistant/components/bsblan/translations/nl.json b/homeassistant/components/bsblan/translations/nl.json
index 850f942df2e7e6..415cd759a8a886 100644
--- a/homeassistant/components/bsblan/translations/nl.json
+++ b/homeassistant/components/bsblan/translations/nl.json
@@ -12,7 +12,9 @@
"data": {
"host": "Host",
"passkey": "Passkey-tekenreeks",
- "port": "Poort"
+ "password": "Wachtwoord",
+ "port": "Poort",
+ "username": "Gebruikersnaam"
},
"description": "Stel uw BSB-Lan-apparaat in om te integreren met Home Assistant.",
"title": "Maak verbinding met het BSB-Lan-apparaat"
diff --git a/homeassistant/components/bsblan/translations/ru.json b/homeassistant/components/bsblan/translations/ru.json
index 76aa715a9de7c7..8291a20d3078a0 100644
--- a/homeassistant/components/bsblan/translations/ru.json
+++ b/homeassistant/components/bsblan/translations/ru.json
@@ -14,7 +14,7 @@
"passkey": "\u041f\u0430\u0440\u043e\u043b\u044c",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 BSB-Lan.",
"title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443"
diff --git a/homeassistant/components/bsblan/translations/tr.json b/homeassistant/components/bsblan/translations/tr.json
index 94acde2d0a3e46..803b5102a073c6 100644
--- a/homeassistant/components/bsblan/translations/tr.json
+++ b/homeassistant/components/bsblan/translations/tr.json
@@ -1,9 +1,17 @@
{
"config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
"step": {
"user": {
"data": {
+ "host": "Ana Bilgisayar",
"password": "\u015eifre",
+ "port": "Port",
"username": "Kullan\u0131c\u0131 ad\u0131"
}
}
diff --git a/homeassistant/components/bsblan/translations/uk.json b/homeassistant/components/bsblan/translations/uk.json
new file mode 100644
index 00000000000000..619f7c8e8a580a
--- /dev/null
+++ b/homeassistant/components/bsblan/translations/uk.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "flow_title": "BSB-Lan: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "passkey": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 BSB-Lan.",
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bt_smarthub/device_tracker.py b/homeassistant/components/bt_smarthub/device_tracker.py
index 383f724decd632..107eb5598d9d2c 100644
--- a/homeassistant/components/bt_smarthub/device_tracker.py
+++ b/homeassistant/components/bt_smarthub/device_tracker.py
@@ -1,4 +1,5 @@
"""Support for BT Smart Hub (Sometimes referred to as BT Home Hub 6)."""
+from collections import namedtuple
import logging
from btsmarthub_devicelist import BTSmartHub
@@ -31,19 +32,30 @@ def get_scanner(hass, config):
smarthub_client = BTSmartHub(
router_ip=info[CONF_HOST], smarthub_model=info.get(CONF_SMARTHUB_MODEL)
)
-
scanner = BTSmartHubScanner(smarthub_client)
-
return scanner if scanner.success_init else None
+def _create_device(data):
+ """Create new device from the dict."""
+ ip_address = data.get("IPAddress")
+ mac = data.get("PhysAddress")
+ host = data.get("UserHostName")
+ status = data.get("Active")
+ name = data.get("name")
+ return _Device(ip_address, mac, host, status, name)
+
+
+_Device = namedtuple("_Device", ["ip_address", "mac", "host", "status", "name"])
+
+
class BTSmartHubScanner(DeviceScanner):
"""This class queries a BT Smart Hub."""
def __init__(self, smarthub_client):
"""Initialise the scanner."""
self.smarthub = smarthub_client
- self.last_results = {}
+ self.last_results = []
self.success_init = False
# Test the router is accessible
@@ -56,15 +68,15 @@ def __init__(self, smarthub_client):
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
- return [client["mac"] for client in self.last_results]
+ return [device.mac for device in self.last_results]
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
if not self.last_results:
return None
- for client in self.last_results:
- if client["mac"] == device:
- return client["host"]
+ for result_device in self.last_results:
+ if result_device.mac == device:
+ return result_device.name or result_device.host
return None
def _update_info(self):
@@ -77,26 +89,10 @@ def _update_info(self):
if not data:
_LOGGER.warning("Error scanning devices")
return
-
- clients = list(data.values())
- self.last_results = clients
+ self.last_results = data
def get_bt_smarthub_data(self):
"""Retrieve data from BT Smart Hub and return parsed result."""
-
# Request data from bt smarthub into a list of dicts.
data = self.smarthub.get_devicelist(only_active_devices=True)
-
- # Renaming keys from parsed result.
- devices = {}
- for device in data:
- try:
- devices[device["UserHostName"]] = {
- "ip": device["IPAddress"],
- "mac": device["PhysAddress"],
- "host": device["UserHostName"],
- "status": device["Active"],
- }
- except KeyError:
- pass
- return devices
+ return [_create_device(d) for d in data if d.get("PhysAddress")]
diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py
index 44f86589b2707f..92f25b7ffc6798 100644
--- a/homeassistant/components/buienradar/camera.py
+++ b/homeassistant/components/buienradar/camera.py
@@ -1,8 +1,9 @@
"""Provide animated GIF loops of Buienradar imagery."""
+from __future__ import annotations
+
import asyncio
from datetime import datetime, timedelta
import logging
-from typing import Optional
import aiohttp
import voluptuous as vol
@@ -85,13 +86,13 @@ def __init__(self, name: str, dimension: int, delta: float, country: str):
# invariant: this condition is private to and owned by this instance.
self._condition = asyncio.Condition()
- self._last_image: Optional[bytes] = None
+ self._last_image: bytes | None = None
# value of the last seen last modified header
- self._last_modified: Optional[str] = None
+ self._last_modified: str | None = None
# loading status
self._loading = False
# deadline for image refresh - self.delta after last successful load
- self._deadline: Optional[datetime] = None
+ self._deadline: datetime | None = None
self._unique_id = f"{self._dimension}_{self._country}"
@@ -140,7 +141,7 @@ async def __retrieve_radar_image(self) -> bool:
_LOGGER.error("Failed to fetch image, %s", type(err))
return False
- async def async_camera_image(self) -> Optional[bytes]:
+ async def async_camera_image(self) -> bytes | None:
"""
Return a still image response from the camera.
diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py
index 4e35542b581254..170493969f8d7c 100644
--- a/homeassistant/components/buienradar/sensor.py
+++ b/homeassistant/components/buienradar/sensor.py
@@ -20,7 +20,7 @@
)
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_LATITUDE,
@@ -39,7 +39,6 @@
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import dt as dt_util
from .const import DEFAULT_TIMEFRAME
@@ -236,7 +235,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
await data.schedule_update(1)
-class BrSensor(Entity):
+class BrSensor(SensorEntity):
"""Representation of an Buienradar sensor."""
def __init__(self, sensor_type, client_name, coordinates):
@@ -309,7 +308,7 @@ def _load_data(self, data):
try:
condition = data.get(FORECAST)[fcday].get(CONDITION)
except IndexError:
- _LOGGER.warning("No forecast for fcday=%s...", fcday)
+ _LOGGER.warning("No forecast for fcday=%s", fcday)
return False
if condition:
@@ -339,7 +338,7 @@ def _load_data(self, data):
self._state = round(self._state * 3.6, 1)
return True
except IndexError:
- _LOGGER.warning("No forecast for fcday=%s...", fcday)
+ _LOGGER.warning("No forecast for fcday=%s", fcday)
return False
# update all other sensors
@@ -347,7 +346,7 @@ def _load_data(self, data):
self._state = data.get(FORECAST)[fcday].get(self.type[:-3])
return True
except IndexError:
- _LOGGER.warning("No forecast for fcday=%s...", fcday)
+ _LOGGER.warning("No forecast for fcday=%s", fcday)
return False
if self.type == SYMBOL or self.type.startswith(CONDITION):
@@ -430,7 +429,7 @@ def entity_picture(self):
return self._entity_picture
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self.type.startswith(PRECIPITATION_FORECAST):
result = {ATTR_ATTRIBUTION: self._attribution}
diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py
index b4f2314eee5b5f..83c511713d0164 100644
--- a/homeassistant/components/buienradar/util.py
+++ b/homeassistant/components/buienradar/util.py
@@ -82,7 +82,7 @@ async def schedule_update(self, minute=1):
async def get_data(self, url):
"""Load data from specified url."""
- _LOGGER.debug("Calling url: %s...", url)
+ _LOGGER.debug("Calling url: %s", url)
result = {SUCCESS: False, MESSAGE: None}
resp = None
try:
diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py
index 4b0391c3190cad..2ff638a2550914 100644
--- a/homeassistant/components/buienradar/weather.py
+++ b/homeassistant/components/buienradar/weather.py
@@ -201,7 +201,7 @@ def forecast(self):
# keys understood by the weather component:
condcode = data_in.get(CONDITION, []).get(CONDCODE)
data_out = {
- ATTR_FORECAST_TIME: data_in.get(DATETIME),
+ ATTR_FORECAST_TIME: data_in.get(DATETIME).isoformat(),
ATTR_FORECAST_CONDITION: cond[condcode],
ATTR_FORECAST_TEMP_LOW: data_in.get(MIN_TEMP),
ATTR_FORECAST_TEMP: data_in.get(MAX_TEMP),
diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py
index 66b3c974306e8b..62be361df3b5d8 100644
--- a/homeassistant/components/caldav/calendar.py
+++ b/homeassistant/components/caldav/calendar.py
@@ -123,7 +123,7 @@ def __init__(self, name, calendar, entity_id, days, all_day=False, search=None):
self._offset_reached = False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
return {"offset_reached": self._offset_reached}
diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py
index 478dafb3423653..11a6916ba83ff1 100644
--- a/homeassistant/components/calendar/__init__.py
+++ b/homeassistant/components/calendar/__init__.py
@@ -1,8 +1,10 @@
"""Support for Google Calendar event device sensors."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
import re
-from typing import Dict, List, cast
+from typing import cast, final
from aiohttp import web
@@ -127,13 +129,14 @@ def is_offset_reached(event):
class CalendarEventDevice(Entity):
- """A calendar event device."""
+ """Base class for calendar event entities."""
@property
def event(self):
"""Return the next upcoming event."""
raise NotImplementedError()
+ @final
@property
def state_attributes(self):
"""Return the entity state attributes."""
@@ -218,7 +221,7 @@ def __init__(self, component: EntityComponent) -> None:
async def get(self, request: web.Request) -> web.Response:
"""Retrieve calendar list."""
hass = request.app["hass"]
- calendar_list: List[Dict[str, str]] = []
+ calendar_list: list[dict[str, str]] = []
for entity in self.component.entities:
state = hass.states.get(entity.entity_id)
diff --git a/homeassistant/components/calendar/translations/id.json b/homeassistant/components/calendar/translations/id.json
index 383a6ba77a13bf..e48c6e69b98d59 100644
--- a/homeassistant/components/calendar/translations/id.json
+++ b/homeassistant/components/calendar/translations/id.json
@@ -1,8 +1,8 @@
{
"state": {
"_": {
- "off": "Off",
- "on": "On"
+ "off": "Mati",
+ "on": "Nyala"
}
},
"title": "Kalender"
diff --git a/homeassistant/components/calendar/translations/ko.json b/homeassistant/components/calendar/translations/ko.json
index af8622be7d772b..fd1672fef56ec7 100644
--- a/homeassistant/components/calendar/translations/ko.json
+++ b/homeassistant/components/calendar/translations/ko.json
@@ -5,5 +5,5 @@
"on": "\ucf1c\uc9d0"
}
},
- "title": "\uc77c\uc815"
+ "title": "\uce98\ub9b0\ub354"
}
\ No newline at end of file
diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py
index 25505800709284..707398575878f7 100644
--- a/homeassistant/components/camera/__init__.py
+++ b/homeassistant/components/camera/__init__.py
@@ -8,6 +8,7 @@
import logging
import os
from random import SystemRandom
+from typing import final
from aiohttp import web
import async_timeout
@@ -23,16 +24,8 @@
DOMAIN as DOMAIN_MP,
SERVICE_PLAY_MEDIA,
)
-from homeassistant.components.stream import request_stream
-from homeassistant.components.stream.const import (
- CONF_DURATION,
- CONF_LOOKBACK,
- CONF_STREAM_SOURCE,
- DOMAIN as DOMAIN_STREAM,
- FORMAT_CONTENT_TYPE,
- OUTPUT_FORMATS,
- SERVICE_RECORD,
-)
+from homeassistant.components.stream import Stream, create_stream
+from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE, OUTPUT_FORMATS
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_FILENAME,
@@ -53,7 +46,15 @@
from homeassistant.helpers.network import get_url
from homeassistant.loader import bind_hass
-from .const import DATA_CAMERA_PREFS, DOMAIN
+from .const import (
+ CAMERA_IMAGE_TIMEOUT,
+ CAMERA_STREAM_SOURCE_TIMEOUT,
+ CONF_DURATION,
+ CONF_LOOKBACK,
+ DATA_CAMERA_PREFS,
+ DOMAIN,
+ SERVICE_RECORD,
+)
from .prefs import CameraPreferences
# mypy: allow-untyped-calls, allow-untyped-defs
@@ -130,23 +131,7 @@ class Image:
async def async_request_stream(hass, entity_id, fmt):
"""Request a stream for a camera entity."""
camera = _get_camera_from_entity_id(hass, entity_id)
- camera_prefs = hass.data[DATA_CAMERA_PREFS].get(entity_id)
-
- async with async_timeout.timeout(10):
- source = await camera.stream_source()
-
- if not source:
- raise HomeAssistantError(
- f"{camera.entity_id} does not support play stream service"
- )
-
- return request_stream(
- hass,
- source,
- fmt=fmt,
- keepalive=camera_prefs.preload_stream,
- options=camera.stream_options,
- )
+ return await _async_stream_endpoint_url(hass, camera, fmt)
@bind_hass
@@ -267,14 +252,12 @@ async def preload_stream(_):
camera_prefs = prefs.get(camera.entity_id)
if not camera_prefs.preload_stream:
continue
-
- async with async_timeout.timeout(10):
- source = await camera.stream_source()
-
- if not source:
+ stream = await camera.create_stream()
+ if not stream:
continue
-
- request_stream(hass, source, keepalive=True, options=camera.stream_options)
+ stream.keepalive = True
+ stream.add_provider("hls")
+ stream.start()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, preload_stream)
@@ -330,6 +313,7 @@ class Camera(Entity):
def __init__(self):
"""Initialize a camera."""
self.is_streaming = False
+ self.stream = None
self.stream_options = {}
self.content_type = DEFAULT_CONTENT_TYPE
self.access_tokens: collections.deque = collections.deque([], 2)
@@ -375,6 +359,17 @@ def frame_interval(self):
"""Return the interval between frames of the mjpeg stream."""
return 0.5
+ async def create_stream(self) -> Stream:
+ """Create a Stream for stream_source."""
+ # There is at most one stream (a decode worker) per camera
+ if not self.stream:
+ async with async_timeout.timeout(CAMERA_STREAM_SOURCE_TIMEOUT):
+ source = await self.stream_source()
+ if not source:
+ return None
+ self.stream = create_stream(self.hass, source, options=self.stream_options)
+ return self.stream
+
async def stream_source(self):
"""Return the source of the stream."""
return None
@@ -447,6 +442,7 @@ async def async_disable_motion_detection(self):
"""Call the job and disable motion detection."""
await self.hass.async_add_executor_job(self.disable_motion_detection)
+ @final
@property
def state_attributes(self):
"""Return the camera state attributes."""
@@ -515,7 +511,7 @@ class CameraImageView(CameraView):
async def handle(self, request: web.Request, camera: Camera) -> web.Response:
"""Serve camera image."""
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
- async with async_timeout.timeout(10):
+ async with async_timeout.timeout(CAMERA_IMAGE_TIMEOUT):
image = await camera.async_camera_image()
if image:
@@ -586,24 +582,7 @@ async def ws_camera_stream(hass, connection, msg):
try:
entity_id = msg["entity_id"]
camera = _get_camera_from_entity_id(hass, entity_id)
- camera_prefs = hass.data[DATA_CAMERA_PREFS].get(entity_id)
-
- async with async_timeout.timeout(10):
- source = await camera.stream_source()
-
- if not source:
- raise HomeAssistantError(
- f"{camera.entity_id} does not support play stream service"
- )
-
- fmt = msg["format"]
- url = request_stream(
- hass,
- source,
- fmt=fmt,
- keepalive=camera_prefs.preload_stream,
- options=camera.stream_options,
- )
+ url = await _async_stream_endpoint_url(hass, camera, fmt=msg["format"])
connection.send_result(msg["id"], {"url": url})
except HomeAssistantError as ex:
_LOGGER.error("Error requesting stream: %s", ex)
@@ -676,32 +655,17 @@ def _write_image(to_file, image_data):
async def async_handle_play_stream_service(camera, service_call):
"""Handle play stream services calls."""
- async with async_timeout.timeout(10):
- source = await camera.stream_source()
-
- if not source:
- raise HomeAssistantError(
- f"{camera.entity_id} does not support play stream service"
- )
-
- hass = camera.hass
- camera_prefs = hass.data[DATA_CAMERA_PREFS].get(camera.entity_id)
fmt = service_call.data[ATTR_FORMAT]
- entity_ids = service_call.data[ATTR_MEDIA_PLAYER]
+ url = await _async_stream_endpoint_url(camera.hass, camera, fmt)
- url = request_stream(
- hass,
- source,
- fmt=fmt,
- keepalive=camera_prefs.preload_stream,
- options=camera.stream_options,
- )
+ hass = camera.hass
data = {
ATTR_MEDIA_CONTENT_ID: f"{get_url(hass)}{url}",
ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt],
}
# It is required to send a different payload for cast media players
+ entity_ids = service_call.data[ATTR_MEDIA_PLAYER]
cast_entity_ids = [
entity
for entity, source in entity_sources(hass).items()
@@ -740,12 +704,27 @@ async def async_handle_play_stream_service(camera, service_call):
)
+async def _async_stream_endpoint_url(hass, camera, fmt):
+ stream = await camera.create_stream()
+ if not stream:
+ raise HomeAssistantError(
+ f"{camera.entity_id} does not support play stream service"
+ )
+
+ # Update keepalive setting which manages idle shutdown
+ camera_prefs = hass.data[DATA_CAMERA_PREFS].get(camera.entity_id)
+ stream.keepalive = camera_prefs.preload_stream
+
+ stream.add_provider(fmt)
+ stream.start()
+ return stream.endpoint_url(fmt)
+
+
async def async_handle_record_service(camera, call):
"""Handle stream recording service calls."""
- async with async_timeout.timeout(10):
- source = await camera.stream_source()
+ stream = await camera.create_stream()
- if not source:
+ if not stream:
raise HomeAssistantError(f"{camera.entity_id} does not support record service")
hass = camera.hass
@@ -753,13 +732,6 @@ async def async_handle_record_service(camera, call):
filename.hass = hass
video_path = filename.async_render(variables={ATTR_ENTITY_ID: camera})
- data = {
- CONF_STREAM_SOURCE: source,
- CONF_FILENAME: video_path,
- CONF_DURATION: call.data[CONF_DURATION],
- CONF_LOOKBACK: call.data[CONF_LOOKBACK],
- }
-
- await hass.services.async_call(
- DOMAIN_STREAM, SERVICE_RECORD, data, blocking=True, context=call.context
+ await stream.async_record(
+ video_path, duration=call.data[CONF_DURATION], lookback=call.data[CONF_LOOKBACK]
)
diff --git a/homeassistant/components/camera/const.py b/homeassistant/components/camera/const.py
index 563f0554f0fa68..7218b19f8fe845 100644
--- a/homeassistant/components/camera/const.py
+++ b/homeassistant/components/camera/const.py
@@ -4,3 +4,11 @@
DATA_CAMERA_PREFS = "camera_prefs"
PREF_PRELOAD_STREAM = "preload_stream"
+
+SERVICE_RECORD = "record"
+
+CONF_LOOKBACK = "lookback"
+CONF_DURATION = "duration"
+
+CAMERA_STREAM_SOURCE_TIMEOUT = 10
+CAMERA_IMAGE_TIMEOUT = 10
diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml
index 70d33da884c047..3c8e99f001ba26 100644
--- a/homeassistant/components/camera/services.yaml
+++ b/homeassistant/components/camera/services.yaml
@@ -1,69 +1,96 @@
# Describes the format for available camera services
turn_off:
+ name: Turn off
description: Turn off camera.
- fields:
- entity_id:
- description: Entity id.
- example: "camera.living_room"
+ target:
turn_on:
+ name: Turn on
description: Turn on camera.
- fields:
- entity_id:
- description: Entity id.
- example: "camera.living_room"
+ target:
enable_motion_detection:
+ name: Enable motion detection
description: Enable the motion detection in a camera.
- fields:
- entity_id:
- description: Name(s) of entities to enable motion detection.
- example: "camera.living_room_camera"
+ target:
disable_motion_detection:
+ name: Disable motion detection
description: Disable the motion detection in a camera.
- fields:
- entity_id:
- description: Name(s) of entities to disable motion detection.
- example: "camera.living_room_camera"
+ target:
snapshot:
+ name: Take snapshot
description: Take a snapshot from a camera.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to create snapshots from.
- example: "camera.living_room_camera"
filename:
+ name: Filename
description: Template of a Filename. Variable is entity_id.
+ required: true
example: "/tmp/snapshot_{{ entity_id.name }}.jpg"
+ selector:
+ text:
play_stream:
+ name: Play stream
description: Play camera stream on supported media player.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to stream from.
- example: "camera.living_room_camera"
media_player:
+ name: Media Player
description: Name(s) of media player to stream to.
+ required: true
example: "media_player.living_room_tv"
+ selector:
+ entity:
+ domain: media_player
format:
- description: (Optional) Stream format supported by media player.
+ name: Format
+ description: Stream format supported by media player.
+ default: "hls"
example: "hls"
+ selector:
+ select:
+ options:
+ - "hls"
record:
+ name: Record
description: Record live camera feed.
+ target:
fields:
- entity_id:
- description: Name of entities to record.
- example: "camera.living_room_camera"
filename:
- description: Template of a Filename. Variable is entity_id. Must be mp4.
+ name: Filename
+ description: Template of a Filename. Variable is entity_id. Must be mp4.
+ required: true
example: "/tmp/snapshot_{{ entity_id.name }}.mp4"
+ selector:
+ text:
duration:
- description: (Optional) Target recording length (in seconds).
+ name: Duration
+ description: Target recording length.
default: 30
example: 30
+ selector:
+ number:
+ min: 1
+ max: 3600
+ step: 1
+ unit_of_measurement: seconds
+ mode: slider
lookback:
- description: (Optional) Target lookback period (in seconds) to include in addition to duration. Only available if there is currently an active HLS stream.
+ name: Lookback
+ description:
+ Target lookback period to include in addition to duration. Only
+ available if there is currently an active HLS stream.
+ default: 0
example: 4
+ selector:
+ number:
+ min: 0
+ max: 300
+ step: 1
+ unit_of_measurement: seconds
+ mode: slider
diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py
index 64f2d00735eabe..ca6c4118753cfd 100644
--- a/homeassistant/components/canary/__init__.py
+++ b/homeassistant/components/canary/__init__.py
@@ -95,10 +95,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
raise ConfigEntryNotReady from error
coordinator = CanaryDataUpdateCoordinator(hass, api=canary_api)
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
undo_listener = entry.add_update_listener(_async_update_listener)
@@ -107,9 +104,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
DATA_UNDO_UPDATE_LISTENER: undo_listener,
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -120,8 +117,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py
index e4dda0f9f33636..933e6708e22f7f 100644
--- a/homeassistant/components/canary/alarm_control_panel.py
+++ b/homeassistant/components/canary/alarm_control_panel.py
@@ -1,5 +1,7 @@
"""Support for Canary alarm."""
-from typing import Callable, List
+from __future__ import annotations
+
+from typing import Callable
from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT
@@ -27,7 +29,7 @@
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up Canary alarm control panels based on a config entry."""
coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
@@ -87,7 +89,7 @@ def supported_features(self) -> int:
return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {"private": self.location.is_private}
diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py
index 0493a964cc45fb..703ae2edc8a2c4 100644
--- a/homeassistant/components/canary/camera.py
+++ b/homeassistant/components/canary/camera.py
@@ -1,7 +1,9 @@
"""Support for Canary camera."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
-from typing import Callable, List
+from typing import Callable
from haffmpeg.camera import CameraMjpeg
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
@@ -44,7 +46,7 @@
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up Canary sensors based on a config entry."""
coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
diff --git a/homeassistant/components/canary/config_flow.py b/homeassistant/components/canary/config_flow.py
index dc2822d836a2ae..2d324e09cc8d64 100644
--- a/homeassistant/components/canary/config_flow.py
+++ b/homeassistant/components/canary/config_flow.py
@@ -1,6 +1,8 @@
"""Config flow for Canary."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from canary.api import Api
from requests import ConnectTimeout, HTTPError
@@ -11,13 +13,17 @@
from homeassistant.core import callback
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
-from .const import CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS, DEFAULT_TIMEOUT
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import (
+ CONF_FFMPEG_ARGUMENTS,
+ DEFAULT_FFMPEG_ARGUMENTS,
+ DEFAULT_TIMEOUT,
+ DOMAIN,
+)
_LOGGER = logging.getLogger(__name__)
-def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]:
+def validate_input(hass: HomeAssistantType, data: dict) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
@@ -45,14 +51,14 @@ def async_get_options_flow(config_entry):
return CanaryOptionsFlowHandler(config_entry)
async def async_step_import(
- self, user_input: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, user_input: ConfigType | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initiated by configuration file."""
return await self.async_step_user(user_input)
async def async_step_user(
- self, user_input: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, user_input: ConfigType | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initiated by the user."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
@@ -100,7 +106,7 @@ def __init__(self, config_entry):
"""Initialize options flow."""
self.config_entry = config_entry
- async def async_step_init(self, user_input: Optional[ConfigType] = None):
+ async def async_step_init(self, user_input: ConfigType | None = None):
"""Manage Canary options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py
index 99dcdf48fceddb..d7a6648857a503 100644
--- a/homeassistant/components/canary/sensor.py
+++ b/homeassistant/components/canary/sensor.py
@@ -1,8 +1,11 @@
"""Support for Canary sensors."""
-from typing import Callable, List
+from __future__ import annotations
+
+from typing import Callable
from canary.api import SensorType
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
DEVICE_CLASS_BATTERY,
@@ -54,7 +57,7 @@
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up Canary sensors based on a config entry."""
coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
@@ -75,7 +78,7 @@ async def async_setup_entry(
async_add_entities(sensors, True)
-class CanarySensor(CoordinatorEntity, Entity):
+class CanarySensor(CoordinatorEntity, SensorEntity):
"""Representation of a Canary sensor."""
def __init__(self, coordinator, sensor_type, location, device):
@@ -163,7 +166,7 @@ def icon(self):
return self._sensor_type[2]
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
reading = self.reading
diff --git a/homeassistant/components/canary/translations/de.json b/homeassistant/components/canary/translations/de.json
index eebc9bd5fc3fd8..bdd746c314988c 100644
--- a/homeassistant/components/canary/translations/de.json
+++ b/homeassistant/components/canary/translations/de.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.",
"unknown": "Unerwarteter Fehler"
},
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"flow_title": "Canary: {name}",
"step": {
diff --git a/homeassistant/components/canary/translations/hu.json b/homeassistant/components/canary/translations/hu.json
new file mode 100644
index 00000000000000..c2c70fdbf22aee
--- /dev/null
+++ b/homeassistant/components/canary/translations/hu.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "flow_title": "Canary: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "title": "Csatlakoz\u00e1s a Canary-hoz"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s (opcion\u00e1lis)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/id.json b/homeassistant/components/canary/translations/id.json
new file mode 100644
index 00000000000000..5f092847b4d499
--- /dev/null
+++ b/homeassistant/components/canary/translations/id.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "flow_title": "Canary: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "title": "Hubungkan ke Canary"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "ffmpeg_arguments": "Argumen yang diteruskan ke ffmpeg untuk kamera",
+ "timeout": "Tenggang Waktu Permintaan (detik)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/ko.json b/homeassistant/components/canary/translations/ko.json
index 0b1d82bb20a418..c96049f16ffb6b 100644
--- a/homeassistant/components/canary/translations/ko.json
+++ b/homeassistant/components/canary/translations/ko.json
@@ -1,20 +1,20 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c. \ud558\ub098\uc758 \uc124\uc815\ub9cc \uac00\ub2a5\ud568.",
- "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec \ubc1c\uc0dd"
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328"
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
"flow_title": "Canary: {name}",
"step": {
"user": {
"data": {
- "password": "\uc554\ud638",
- "username": "\uc0ac\uc6a9\uc790\uba85"
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
},
- "title": "Canary\uc5d0 \uc5f0\uacb0"
+ "title": "Canary\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
},
@@ -22,7 +22,7 @@
"step": {
"init": {
"data": {
- "ffmpeg_arguments": "\uce74\uba54\ub77c ffmpeg\uc5d0 \uc804\ub2ec \ub41c \uc778\uc218",
+ "ffmpeg_arguments": "\uce74\uba54\ub77c\uc5d0 \ub300\ud55c ffmpeg \uc804\ub2ec \uc778\uc218",
"timeout": "\uc694\uccad \uc81c\ud55c \uc2dc\uac04 (\ucd08)"
}
}
diff --git a/homeassistant/components/canary/translations/nl.json b/homeassistant/components/canary/translations/nl.json
index 9681bcd7c371fe..fbe642bbc96024 100644
--- a/homeassistant/components/canary/translations/nl.json
+++ b/homeassistant/components/canary/translations/nl.json
@@ -22,6 +22,7 @@
"step": {
"init": {
"data": {
+ "ffmpeg_arguments": "Argumenten doorgegeven aan ffmpeg voor camera's",
"timeout": "Time-out verzoek (seconden)"
}
}
diff --git a/homeassistant/components/canary/translations/ru.json b/homeassistant/components/canary/translations/ru.json
index 146863cf768ba2..51052d0d68d4ba 100644
--- a/homeassistant/components/canary/translations/ru.json
+++ b/homeassistant/components/canary/translations/ru.json
@@ -12,7 +12,7 @@
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Canary"
}
diff --git a/homeassistant/components/canary/translations/tr.json b/homeassistant/components/canary/translations/tr.json
new file mode 100644
index 00000000000000..6d18629b067921
--- /dev/null
+++ b/homeassistant/components/canary/translations/tr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.",
+ "unknown": "Beklenmeyen hata"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/canary/translations/uk.json b/homeassistant/components/canary/translations/uk.json
new file mode 100644
index 00000000000000..74327f3ebd672b
--- /dev/null
+++ b/homeassistant/components/canary/translations/uk.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "flow_title": "Canary: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Canary"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "ffmpeg_arguments": "\u0410\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u0438, \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u0456 \u0432 ffmpeg \u0434\u043b\u044f \u043a\u0430\u043c\u0435\u0440",
+ "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u0437\u0430\u043f\u0438\u0442\u0443 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py
index 49cec207764477..43b6b77ebd298f 100644
--- a/homeassistant/components/cast/__init__.py
+++ b/homeassistant/components/cast/__init__.py
@@ -1,20 +1,42 @@
"""Component to embed Google Cast."""
+import logging
+
+import voluptuous as vol
+
from homeassistant import config_entries
+from homeassistant.helpers import config_validation as cv
from . import home_assistant_cast
from .const import DOMAIN
+from .media_player import ENTITY_SCHEMA
+
+# Deprecated from 2021.4, remove in 2021.6
+CONFIG_SCHEMA = cv.deprecated(DOMAIN)
+
+_LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config):
"""Set up the Cast component."""
conf = config.get(DOMAIN)
- hass.data[DOMAIN] = conf or {}
-
if conf is not None:
+ media_player_config_validated = []
+ media_player_config = conf.get("media_player", {})
+ if not isinstance(media_player_config, list):
+ media_player_config = [media_player_config]
+ for cfg in media_player_config:
+ try:
+ cfg = ENTITY_SCHEMA(cfg)
+ media_player_config_validated.append(cfg)
+ except vol.Error as ex:
+ _LOGGER.warning("Invalid config '%s': %s", cfg, ex)
+
hass.async_create_task(
hass.config_entries.flow.async_init(
- DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data=media_player_config_validated,
)
)
diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py
index e00048a7589077..464283e07f30cc 100644
--- a/homeassistant/components/cast/config_flow.py
+++ b/homeassistant/components/cast/config_flow.py
@@ -1,35 +1,180 @@
"""Config flow for Cast."""
-import functools
-
-from pychromecast.discovery import discover_chromecasts, stop_discovery
+import voluptuous as vol
from homeassistant import config_entries
-from homeassistant.components import zeroconf
-from homeassistant.helpers import config_entry_flow
+from homeassistant.helpers import config_validation as cv
+
+from .const import CONF_IGNORE_CEC, CONF_KNOWN_HOSTS, CONF_UUID, DOMAIN
+
+IGNORE_CEC_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string]))
+KNOWN_HOSTS_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string]))
+WANTED_UUID_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string]))
+
+
+class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
+
+ def __init__(self):
+ """Initialize flow."""
+ self._ignore_cec = set()
+ self._known_hosts = set()
+ self._wanted_uuid = set()
+
+ @staticmethod
+ def async_get_options_flow(config_entry):
+ """Get the options flow for this handler."""
+ return CastOptionsFlowHandler(config_entry)
+
+ async def async_step_import(self, import_data=None):
+ """Import data."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
+ media_player_config = import_data or []
+ for cfg in media_player_config:
+ if CONF_IGNORE_CEC in cfg:
+ self._ignore_cec.update(set(cfg[CONF_IGNORE_CEC]))
+ if CONF_UUID in cfg:
+ self._wanted_uuid.add(cfg[CONF_UUID])
+
+ data = self._get_data()
+ return self.async_create_entry(title="Google Cast", data=data)
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
+ return await self.async_step_config()
+
+ async def async_step_zeroconf(self, discovery_info):
+ """Handle a flow initialized by zeroconf discovery."""
+ if self._async_in_progress() or self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
+ await self.async_set_unique_id(DOMAIN)
+
+ return await self.async_step_confirm()
+
+ async def async_step_config(self, user_input=None):
+ """Confirm the setup."""
+ errors = {}
+ data = {CONF_KNOWN_HOSTS: self._known_hosts}
+
+ if user_input is not None:
+ bad_hosts = False
+ known_hosts = user_input[CONF_KNOWN_HOSTS]
+ known_hosts = [x.strip() for x in known_hosts.split(",") if x.strip()]
+ try:
+ known_hosts = KNOWN_HOSTS_SCHEMA(known_hosts)
+ except vol.Invalid:
+ errors["base"] = "invalid_known_hosts"
+ bad_hosts = True
+ else:
+ self._known_hosts = known_hosts
+ data = self._get_data()
+ if not bad_hosts:
+ return self.async_create_entry(title="Google Cast", data=data)
+
+ fields = {}
+ fields[vol.Optional(CONF_KNOWN_HOSTS, default="")] = str
+
+ return self.async_show_form(
+ step_id="config", data_schema=vol.Schema(fields), errors=errors
+ )
+
+ async def async_step_confirm(self, user_input=None):
+ """Confirm the setup."""
+
+ data = self._get_data()
+
+ if user_input is not None:
+ return self.async_create_entry(title="Google Cast", data=data)
+
+ return self.async_show_form(step_id="confirm")
+
+ def _get_data(self):
+ return {
+ CONF_IGNORE_CEC: list(self._ignore_cec),
+ CONF_KNOWN_HOSTS: list(self._known_hosts),
+ CONF_UUID: list(self._wanted_uuid),
+ }
+
+
+class CastOptionsFlowHandler(config_entries.OptionsFlow):
+ """Handle Google Cast options."""
+
+ def __init__(self, config_entry):
+ """Initialize MQTT options flow."""
+ self.config_entry = config_entry
+ self.broker_config = {}
+ self.options = dict(config_entry.options)
+
+ async def async_step_init(self, user_input=None):
+ """Manage the Cast options."""
+ return await self.async_step_options()
+
+ async def async_step_options(self, user_input=None):
+ """Manage the MQTT options."""
+ errors = {}
+ current_config = self.config_entry.data
+ if user_input is not None:
+ bad_cec, ignore_cec = _string_to_list(
+ user_input.get(CONF_IGNORE_CEC, ""), IGNORE_CEC_SCHEMA
+ )
+ bad_hosts, known_hosts = _string_to_list(
+ user_input.get(CONF_KNOWN_HOSTS, ""), KNOWN_HOSTS_SCHEMA
+ )
+ bad_uuid, wanted_uuid = _string_to_list(
+ user_input.get(CONF_UUID, ""), WANTED_UUID_SCHEMA
+ )
+
+ if not bad_cec and not bad_hosts and not bad_uuid:
+ updated_config = {}
+ updated_config[CONF_IGNORE_CEC] = ignore_cec
+ updated_config[CONF_KNOWN_HOSTS] = known_hosts
+ updated_config[CONF_UUID] = wanted_uuid
+ self.hass.config_entries.async_update_entry(
+ self.config_entry, data=updated_config
+ )
+ return self.async_create_entry(title="", data=None)
+
+ fields = {}
+ suggested_value = _list_to_string(current_config.get(CONF_KNOWN_HOSTS))
+ _add_with_suggestion(fields, CONF_KNOWN_HOSTS, suggested_value)
+ if self.show_advanced_options:
+ suggested_value = _list_to_string(current_config.get(CONF_UUID))
+ _add_with_suggestion(fields, CONF_UUID, suggested_value)
+ suggested_value = _list_to_string(current_config.get(CONF_IGNORE_CEC))
+ _add_with_suggestion(fields, CONF_IGNORE_CEC, suggested_value)
-from .const import DOMAIN
-from .helpers import ChromeCastZeroconf
+ return self.async_show_form(
+ step_id="options",
+ data_schema=vol.Schema(fields),
+ errors=errors,
+ )
-async def _async_has_devices(hass):
- """
- Return if there are devices that can be discovered.
+def _list_to_string(items):
+ comma_separated_string = ""
+ if items:
+ comma_separated_string = ",".join(items)
+ return comma_separated_string
- This function will be called if no devices are already found through the zeroconf
- integration.
- """
- zeroconf_instance = ChromeCastZeroconf.get_zeroconf()
- if zeroconf_instance is None:
- zeroconf_instance = await zeroconf.async_get_instance(hass)
+def _string_to_list(string, schema):
+ invalid = False
+ items = [x.strip() for x in string.split(",") if x.strip()]
+ try:
+ items = schema(items)
+ except vol.Invalid:
+ invalid = True
- casts, browser = await hass.async_add_executor_job(
- functools.partial(discover_chromecasts, zeroconf_instance=zeroconf_instance)
- )
- stop_discovery(browser)
- return casts
+ return invalid, items
-config_entry_flow.register_discovery_flow(
- DOMAIN, "Google Cast", _async_has_devices, config_entries.CONN_CLASS_LOCAL_PUSH
-)
+def _add_with_suggestion(fields, key, suggested_value):
+ fields[vol.Optional(key, description={"suggested_value": suggested_value})] = str
diff --git a/homeassistant/components/cast/const.py b/homeassistant/components/cast/const.py
index c6164484dbbb14..03ffdfbd15c99c 100644
--- a/homeassistant/components/cast/const.py
+++ b/homeassistant/components/cast/const.py
@@ -5,14 +5,13 @@
# Stores a threading.Lock that is held by the internal pychromecast discovery.
INTERNAL_DISCOVERY_RUNNING_KEY = "cast_discovery_running"
-# Stores all ChromecastInfo we encountered through discovery or config as a set
-# If we find a chromecast with a new host, the old one will be removed again.
-KNOWN_CHROMECAST_INFO_KEY = "cast_known_chromecasts"
# Stores UUIDs of cast devices that were added as entities. Doesn't store
# None UUIDs.
ADDED_CAST_DEVICES_KEY = "cast_added_cast_devices"
# Stores an audio group manager.
CAST_MULTIZONE_MANAGER_KEY = "cast_multizone_manager"
+# Store a CastBrowser
+CAST_BROWSER_KEY = "cast_browser"
# Dispatcher signal fired with a ChromecastInfo every time we discover a new
# Chromecast or receive it through configuration
@@ -24,3 +23,7 @@
# Dispatcher signal fired when a Chromecast should show a Home Assistant Cast view.
SIGNAL_HASS_CAST_SHOW_VIEW = "cast_show_view"
+
+CONF_IGNORE_CEC = "ignore_cec"
+CONF_KNOWN_HOSTS = "known_hosts"
+CONF_UUID = "uuid"
diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py
index 4858d37f732777..a5ac4c02047850 100644
--- a/homeassistant/components/cast/discovery.py
+++ b/homeassistant/components/cast/discovery.py
@@ -9,8 +9,10 @@
from homeassistant.helpers.dispatcher import dispatcher_send
from .const import (
+ CAST_BROWSER_KEY,
+ CONF_KNOWN_HOSTS,
+ DEFAULT_PORT,
INTERNAL_DISCOVERY_RUNNING_KEY,
- KNOWN_CHROMECAST_INFO_KEY,
SIGNAL_CAST_DISCOVERED,
SIGNAL_CAST_REMOVED,
)
@@ -19,19 +21,24 @@
_LOGGER = logging.getLogger(__name__)
-def discover_chromecast(hass: HomeAssistant, info: ChromecastInfo):
+def discover_chromecast(hass: HomeAssistant, device_info):
"""Discover a Chromecast."""
+
+ info = ChromecastInfo(
+ services=device_info.services,
+ uuid=device_info.uuid,
+ model_name=device_info.model_name,
+ friendly_name=device_info.friendly_name,
+ is_audio_group=device_info.port != DEFAULT_PORT,
+ )
+
if info.uuid is None:
_LOGGER.error("Discovered chromecast without uuid %s", info)
return
info = info.fill_out_missing_chromecast_info()
- if info.uuid in hass.data[KNOWN_CHROMECAST_INFO_KEY]:
- _LOGGER.debug("Discovered update for known chromecast %s", info)
- else:
- _LOGGER.debug("Discovered chromecast %s", info)
+ _LOGGER.debug("Discovered new or updated chromecast %s", info)
- hass.data[KNOWN_CHROMECAST_INFO_KEY][info.uuid] = info
dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info)
@@ -42,7 +49,7 @@ def _remove_chromecast(hass: HomeAssistant, info: ChromecastInfo):
dispatcher_send(hass, SIGNAL_CAST_REMOVED, info)
-def setup_internal_discovery(hass: HomeAssistant) -> None:
+def setup_internal_discovery(hass: HomeAssistant, config_entry) -> None:
"""Set up the pychromecast internal discovery."""
if INTERNAL_DISCOVERY_RUNNING_KEY not in hass.data:
hass.data[INTERNAL_DISCOVERY_RUNNING_KEY] = threading.Lock()
@@ -51,72 +58,50 @@ def setup_internal_discovery(hass: HomeAssistant) -> None:
# Internal discovery is already running
return
- def internal_add_update_callback(uuid, service_name):
- """Handle zeroconf discovery of a new or updated chromecast."""
- service = listener.services[uuid]
-
- # For support of deprecated IP based white listing
- zconf = ChromeCastZeroconf.get_zeroconf()
- service_info = None
- tries = 0
- while service_info is None and tries < 4:
- try:
- service_info = zconf.get_service_info(
- "_googlecast._tcp.local.", service_name
- )
- except OSError:
- # If the zeroconf fails to receive the necessary data we abort
- # adding the service
- break
- tries += 1
-
- if not service_info:
- _LOGGER.warning(
- "setup_internal_discovery failed to get info for %s, %s",
- uuid,
- service_name,
+ class CastListener(pychromecast.discovery.AbstractCastListener):
+ """Listener for discovering chromecasts."""
+
+ def add_cast(self, uuid, _):
+ """Handle zeroconf discovery of a new chromecast."""
+ discover_chromecast(hass, browser.devices[uuid])
+
+ def update_cast(self, uuid, _):
+ """Handle zeroconf discovery of an updated chromecast."""
+ discover_chromecast(hass, browser.devices[uuid])
+
+ def remove_cast(self, uuid, service, cast_info):
+ """Handle zeroconf discovery of a removed chromecast."""
+ _remove_chromecast(
+ hass,
+ ChromecastInfo(
+ services=cast_info.services,
+ uuid=cast_info.uuid,
+ model_name=cast_info.model_name,
+ friendly_name=cast_info.friendly_name,
+ ),
)
- return
-
- addresses = service_info.parsed_addresses()
- host = addresses[0] if addresses else service_info.server
-
- discover_chromecast(
- hass,
- ChromecastInfo(
- services=service[0],
- uuid=service[1],
- model_name=service[2],
- friendly_name=service[3],
- host=host,
- port=service_info.port,
- ),
- )
-
- def internal_remove_callback(uuid, service_name, service):
- """Handle zeroconf discovery of a removed chromecast."""
- _remove_chromecast(
- hass,
- ChromecastInfo(
- services=service[0],
- uuid=service[1],
- model_name=service[2],
- friendly_name=service[3],
- ),
- )
_LOGGER.debug("Starting internal pychromecast discovery")
- listener = pychromecast.CastListener(
- internal_add_update_callback,
- internal_remove_callback,
- internal_add_update_callback,
+ browser = pychromecast.discovery.CastBrowser(
+ CastListener(),
+ ChromeCastZeroconf.get_zeroconf(),
+ config_entry.data.get(CONF_KNOWN_HOSTS),
)
- browser = pychromecast.start_discovery(listener, ChromeCastZeroconf.get_zeroconf())
+ hass.data[CAST_BROWSER_KEY] = browser
+ browser.start_discovery()
def stop_discovery(event):
"""Stop discovery of new chromecasts."""
_LOGGER.debug("Stopping internal pychromecast discovery")
- pychromecast.discovery.stop_discovery(browser)
+ browser.stop_discovery()
hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_discovery)
+
+ config_entry.add_update_listener(config_entry_updated)
+
+
+async def config_entry_updated(hass, config_entry):
+ """Handle config entry being updated."""
+ browser = hass.data[CAST_BROWSER_KEY]
+ browser.host_browser.update_hosts(config_entry.data.get(CONF_KNOWN_HOSTS))
diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py
index e7db380406b0e4..71caa6490d852e 100644
--- a/homeassistant/components/cast/helpers.py
+++ b/homeassistant/components/cast/helpers.py
@@ -1,12 +1,12 @@
"""Helpers to deal with Cast devices."""
+from __future__ import annotations
+
from typing import Optional
import attr
from pychromecast import dial
from pychromecast.const import CAST_MANUFACTURERS
-from .const import DEFAULT_PORT
-
@attr.s(slots=True, frozen=True)
class ChromecastInfo:
@@ -15,22 +15,16 @@ class ChromecastInfo:
This also has the same attributes as the mDNS fields by zeroconf.
"""
- services: Optional[set] = attr.ib()
- host: Optional[str] = attr.ib(default=None)
- port: Optional[int] = attr.ib(default=0)
- uuid: Optional[str] = attr.ib(
+ services: set | None = attr.ib()
+ uuid: str | None = attr.ib(
converter=attr.converters.optional(str), default=None
) # always convert UUID to string if not None
_manufacturer = attr.ib(type=Optional[str], default=None)
model_name: str = attr.ib(default="")
- friendly_name: Optional[str] = attr.ib(default=None)
+ friendly_name: str | None = attr.ib(default=None)
+ is_audio_group = attr.ib(type=Optional[bool], default=False)
is_dynamic_group = attr.ib(type=Optional[bool], default=None)
- @property
- def is_audio_group(self) -> bool:
- """Return if this is an audio group."""
- return self.port != DEFAULT_PORT
-
@property
def is_information_complete(self) -> bool:
"""Return if all information is filled out."""
@@ -57,7 +51,7 @@ def manufacturer(self) -> str:
return None
return CAST_MANUFACTURERS.get(self.model_name.lower(), "Google Inc.")
- def fill_out_missing_chromecast_info(self) -> "ChromecastInfo":
+ def fill_out_missing_chromecast_info(self) -> ChromecastInfo:
"""Return a new ChromecastInfo object with missing attributes filled in.
Uses blocking HTTP / HTTPS.
@@ -72,7 +66,7 @@ def fill_out_missing_chromecast_info(self) -> "ChromecastInfo":
http_group_status = None
if self.uuid:
http_group_status = dial.get_multizone_status(
- self.host,
+ None,
services=self.services,
zconf=ChromeCastZeroconf.get_zeroconf(),
)
@@ -84,17 +78,16 @@ def fill_out_missing_chromecast_info(self) -> "ChromecastInfo":
return ChromecastInfo(
services=self.services,
- host=self.host,
- port=self.port,
uuid=self.uuid,
friendly_name=self.friendly_name,
model_name=self.model_name,
+ is_audio_group=True,
is_dynamic_group=is_dynamic_group,
)
# Fill out some missing information (friendly_name, uuid) via HTTP dial.
http_device_status = dial.get_device_status(
- self.host, services=self.services, zconf=ChromeCastZeroconf.get_zeroconf()
+ None, services=self.services, zconf=ChromeCastZeroconf.get_zeroconf()
)
if http_device_status is None:
# HTTP dial didn't give us any new information.
@@ -102,8 +95,6 @@ def fill_out_missing_chromecast_info(self) -> "ChromecastInfo":
return ChromecastInfo(
services=self.services,
- host=self.host,
- port=self.port,
uuid=(self.uuid or http_device_status.uuid),
friendly_name=(self.friendly_name or http_device_status.friendly_name),
manufacturer=(self.manufacturer or http_device_status.manufacturer),
diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py
index 3edc1ce2cde59d..bb0354bb68e17f 100644
--- a/homeassistant/components/cast/home_assistant_cast.py
+++ b/homeassistant/components/cast/home_assistant_cast.py
@@ -1,5 +1,5 @@
"""Home Assistant Cast integration for Cast."""
-from typing import Optional
+from __future__ import annotations
from pychromecast.controllers.homeassistant import HomeAssistantController
import voluptuous as vol
@@ -20,8 +20,8 @@ async def async_setup_ha_cast(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
):
"""Set up Home Assistant Cast."""
- user_id: Optional[str] = entry.data.get("user_id")
- user: Optional[auth.models.User] = None
+ user_id: str | None = entry.data.get("user_id")
+ user: auth.models.User | None = None
if user_id is not None:
user = await hass.auth.async_get_user(user_id)
@@ -78,7 +78,7 @@ async def async_remove_user(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
):
"""Remove Home Assistant Cast user."""
- user_id: Optional[str] = entry.data.get("user_id")
+ user_id: str | None = entry.data.get("user_id")
if user_id is not None:
user = await hass.auth.async_get_user(user_id)
diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json
index 5f3deb365522ed..3f30bc450fdd44 100644
--- a/homeassistant/components/cast/manifest.json
+++ b/homeassistant/components/cast/manifest.json
@@ -3,7 +3,7 @@
"name": "Google Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast",
- "requirements": ["pychromecast==7.7.2"],
+ "requirements": ["pychromecast==9.1.2"],
"after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"],
"zeroconf": ["_googlecast._tcp.local."],
"codeowners": ["@emontnemery"]
diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py
index 6bedae1cac58c2..b6ca8dd07286b8 100644
--- a/homeassistant/components/cast/media_player.py
+++ b/homeassistant/components/cast/media_player.py
@@ -1,15 +1,18 @@
"""Provide functionality to interact with Cast devices on the network."""
+from __future__ import annotations
+
import asyncio
+from contextlib import suppress
from datetime import timedelta
import functools as ft
import json
import logging
-from typing import Optional
import pychromecast
from pychromecast.controllers.homeassistant import HomeAssistantController
from pychromecast.controllers.multizone import MultizoneManager
from pychromecast.controllers.plex import PlexController
+from pychromecast.controllers.receiver import VOLUME_CONTROL_TYPE_FIXED
from pychromecast.quick_play import quick_play
from pychromecast.socket_client import (
CONNECTION_STATUS_CONNECTED,
@@ -49,19 +52,19 @@
STATE_PLAYING,
)
from homeassistant.core import callback
-from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.network import NoURLAvailableError, get_url
-from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+from homeassistant.helpers.typing import HomeAssistantType
import homeassistant.util.dt as dt_util
from homeassistant.util.logging import async_create_catching_coro
from .const import (
ADDED_CAST_DEVICES_KEY,
CAST_MULTIZONE_MANAGER_KEY,
+ CONF_IGNORE_CEC,
+ CONF_UUID,
DOMAIN as CAST_DOMAIN,
- KNOWN_CHROMECAST_INFO_KEY,
SIGNAL_CAST_DISCOVERED,
SIGNAL_CAST_REMOVED,
SIGNAL_HASS_CAST_SHOW_VIEW,
@@ -71,8 +74,6 @@
_LOGGER = logging.getLogger(__name__)
-CONF_IGNORE_CEC = "ignore_cec"
-CONF_UUID = "uuid"
CAST_SPLASH = "https://www.home-assistant.io/images/cast/splash.png"
SUPPORT_CAST = (
@@ -82,8 +83,6 @@
| SUPPORT_STOP
| SUPPORT_TURN_OFF
| SUPPORT_TURN_ON
- | SUPPORT_VOLUME_MUTE
- | SUPPORT_VOLUME_SET
)
@@ -128,43 +127,20 @@ def _async_create_cast_device(hass: HomeAssistantType, info: ChromecastInfo):
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Cast from a config entry."""
- config = hass.data[CAST_DOMAIN].get("media_player") or {}
- if not isinstance(config, list):
- config = [config]
-
- # no pending task
- done, _ = await asyncio.wait(
- [
- _async_setup_platform(hass, ENTITY_SCHEMA(cfg), async_add_entities)
- for cfg in config
- ]
- )
- if any(task.exception() for task in done):
- exceptions = [task.exception() for task in done]
- for exception in exceptions:
- _LOGGER.debug("Failed to setup chromecast", exc_info=exception)
- raise PlatformNotReady
-
-
-async def _async_setup_platform(
- hass: HomeAssistantType, config: ConfigType, async_add_entities
-):
- """Set up the cast platform."""
- # Import CEC IGNORE attributes
- pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, [])
hass.data.setdefault(ADDED_CAST_DEVICES_KEY, set())
- hass.data.setdefault(KNOWN_CHROMECAST_INFO_KEY, {})
- info = None
- if CONF_UUID in config:
- info = ChromecastInfo(uuid=config[CONF_UUID], services=None)
+ # Import CEC IGNORE attributes
+ pychromecast.IGNORE_CEC += config_entry.data.get(CONF_IGNORE_CEC) or []
+
+ wanted_uuids = config_entry.data.get(CONF_UUID) or None
@callback
def async_cast_discovered(discover: ChromecastInfo) -> None:
"""Handle discovery of a new chromecast."""
- # If info is set, we're handling a specific cast device identified by UUID
- if info is not None and (info.uuid is not None and info.uuid != discover.uuid):
- # UUID not matching, this is not it.
+ # If wanted_uuids is set, we're only accepting specific cast devices identified
+ # by UUID
+ if wanted_uuids is not None and discover.uuid not in wanted_uuids:
+ # UUID not matching, ignore.
return
cast_device = _async_create_cast_device(hass, discover)
@@ -172,13 +148,8 @@ def async_cast_discovered(discover: ChromecastInfo) -> None:
async_add_entities([cast_device])
async_dispatcher_connect(hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered)
- # Re-play the callback for all past chromecasts, store the objects in
- # a list to avoid concurrent modification resulting in exception.
- for chromecast in hass.data[KNOWN_CHROMECAST_INFO_KEY].values():
- async_cast_discovered(chromecast)
-
ChromeCastZeroconf.set_zeroconf(await zeroconf.async_get_instance(hass))
- hass.async_add_executor_job(setup_internal_discovery, hass)
+ hass.async_add_executor_job(setup_internal_discovery, hass, config_entry)
class CastDevice(MediaPlayerEntity):
@@ -194,7 +165,7 @@ def __init__(self, cast_info: ChromecastInfo):
self._cast_info = cast_info
self.services = cast_info.services
- self._chromecast: Optional[pychromecast.Chromecast] = None
+ self._chromecast: pychromecast.Chromecast | None = None
self.cast_status = None
self.media_status = None
self.media_status_received = None
@@ -202,8 +173,8 @@ def __init__(self, cast_info: ChromecastInfo):
self.mz_media_status_received = {}
self.mz_mgr = None
self._available = False
- self._status_listener: Optional[CastStatusListener] = None
- self._hass_cast_controller: Optional[HomeAssistantController] = None
+ self._status_listener: CastStatusListener | None = None
+ self._hass_cast_controller: HomeAssistantController | None = None
self._add_remove_handler = None
self._cast_view_remove_handler = None
@@ -215,7 +186,9 @@ async def async_added_to_hass(self):
)
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop)
self.async_set_cast_info(self._cast_info)
- self.hass.async_create_task(
+ # asyncio.create_task is used to avoid delaying startup wrapup if the device
+ # is discovered already during startup but then fails to respond
+ asyncio.create_task(
async_create_catching_coro(self.async_connect_to_chromecast())
)
@@ -252,8 +225,8 @@ async def async_connect_to_chromecast(self):
self.services,
)
chromecast = await self.hass.async_add_executor_job(
- pychromecast.get_chromecast_from_service,
- (
+ pychromecast.get_chromecast_from_cast_info,
+ pychromecast.discovery.CastInfo(
self.services,
self._cast_info.uuid,
self._cast_info.model_name,
@@ -328,21 +301,14 @@ def new_media_status(self, media_status):
tts_base_url = None
url_description = ""
if "tts" in self.hass.config.components:
- try:
+ with suppress(KeyError): # base_url not configured
tts_base_url = self.hass.components.tts.get_base_url(self.hass)
- except KeyError:
- # base_url not configured, ignore
- pass
- try:
+
+ with suppress(NoURLAvailableError): # external_url not configured
external_url = get_url(self.hass, allow_internal=False)
- except NoURLAvailableError:
- # external_url not configured, ignore
- pass
- try:
+
+ with suppress(NoURLAvailableError): # internal_url not configured
internal_url = get_url(self.hass, allow_external=False)
- except NoURLAvailableError:
- # internal_url not configured, ignore
- pass
if media_status.content_id:
if tts_base_url and media_status.content_id.startswith(tts_base_url):
@@ -743,11 +709,15 @@ def supported_features(self):
support = SUPPORT_CAST
media_status = self._media_status()[0]
+ if (
+ self.cast_status
+ and self.cast_status.volume_control_type != VOLUME_CONTROL_TYPE_FIXED
+ ):
+ support |= SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET
+
if media_status:
if media_status.supports_queue_next:
- support |= SUPPORT_PREVIOUS_TRACK
- if media_status.supports_queue_next:
- support |= SUPPORT_NEXT_TRACK
+ support |= SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
if media_status.supports_seek:
support |= SUPPORT_SEEK
@@ -778,7 +748,7 @@ def media_position_updated_at(self):
return media_status_recevied
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID."""
return self._cast_info.uuid
@@ -800,7 +770,7 @@ def _handle_signal_show_view(
controller: HomeAssistantController,
entity_id: str,
view_path: str,
- url_path: Optional[str],
+ url_path: str | None,
):
"""Handle a show view signal."""
if entity_id != self.entity_id:
@@ -822,9 +792,9 @@ def __init__(self, hass, cast_info: ChromecastInfo):
self.hass = hass
self._cast_info = cast_info
self.services = cast_info.services
- self._chromecast: Optional[pychromecast.Chromecast] = None
+ self._chromecast: pychromecast.Chromecast | None = None
self.mz_mgr = None
- self._status_listener: Optional[CastStatusListener] = None
+ self._status_listener: CastStatusListener | None = None
self._add_remove_handler = None
self._del_remove_handler = None
@@ -872,8 +842,8 @@ async def async_connect_to_chromecast(self):
self.services,
)
chromecast = await self.hass.async_add_executor_job(
- pychromecast.get_chromecast_from_service,
- (
+ pychromecast.get_chromecast_from_cast_info,
+ pychromecast.discovery.CastInfo(
self.services,
self._cast_info.uuid,
self._cast_info.model_name,
diff --git a/homeassistant/components/cast/services.yaml b/homeassistant/components/cast/services.yaml
index d1c29281aad690..8e4466c349ce4f 100644
--- a/homeassistant/components/cast/services.yaml
+++ b/homeassistant/components/cast/services.yaml
@@ -5,7 +5,7 @@ show_lovelace_view:
description: Media Player entity to show the Lovelace view on.
example: "media_player.kitchen"
dashboard_path:
- description: The url path of the Lovelace dashboard to show.
+ description: The URL path of the Lovelace dashboard to show.
example: lovelace-cast
view_path:
description: The path of the Lovelace view to show.
diff --git a/homeassistant/components/cast/strings.json b/homeassistant/components/cast/strings.json
index ad8f0f41ae7b29..33ce4b6941e450 100644
--- a/homeassistant/components/cast/strings.json
+++ b/homeassistant/components/cast/strings.json
@@ -3,11 +3,35 @@
"step": {
"confirm": {
"description": "[%key:common::config_flow::description::confirm_setup%]"
+ },
+ "config": {
+ "title": "Google Cast",
+ "description": "Please enter the Google Cast configuration.",
+ "data": {
+ "known_hosts": "Optional list of known hosts if mDNS discovery is not working."
+ }
}
},
"abort": {
- "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
- "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
+ },
+ "error": {
+ "invalid_known_hosts": "Known hosts must be a comma separated list of hosts."
+ }
+ },
+ "options": {
+ "step": {
+ "options": {
+ "description": "Please enter the Google Cast configuration.",
+ "data": {
+ "ignore_cec": "Optional list which will be passed to pychromecast.IGNORE_CEC.",
+ "known_hosts": "Optional list of known hosts if mDNS discovery is not working.",
+ "uuid": "Optional list of UUIDs. Casts not listed will not be added."
+ }
+ }
+ },
+ "error": {
+ "invalid_known_hosts": "Known hosts must be a comma separated list of hosts."
}
}
}
diff --git a/homeassistant/components/cast/translations/ca.json b/homeassistant/components/cast/translations/ca.json
index dc21c371e60fa0..6a5d16aa6bb8c2 100644
--- a/homeassistant/components/cast/translations/ca.json
+++ b/homeassistant/components/cast/translations/ca.json
@@ -4,10 +4,35 @@
"no_devices_found": "No s'han trobat dispositius a la xarxa",
"single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
+ "error": {
+ "invalid_known_hosts": "Els amfitrions coneguts han de ser una llista d'amfitrions separats per comes."
+ },
"step": {
+ "config": {
+ "data": {
+ "known_hosts": "Llista opcional d'amfitrions coneguts per si el descobriment mDNS deixa de funcionar."
+ },
+ "description": "Introdueix la configuraci\u00f3 de Google Cast.",
+ "title": "Google Cast"
+ },
"confirm": {
"description": "Vols comen\u00e7ar la configuraci\u00f3?"
}
}
+ },
+ "options": {
+ "error": {
+ "invalid_known_hosts": "Els amfitrions coneguts han de ser una llista d'amfitrions separats per comes."
+ },
+ "step": {
+ "options": {
+ "data": {
+ "ignore_cec": "Llista opcional que es passar\u00e0 a pychromecast.IGNORE_CEC.",
+ "known_hosts": "Llista opcional d'amfitrions coneguts per si el descobriment mDNS deixa de funcionar.",
+ "uuid": "Llista opcional d'UUIDs. No s'afegiran 'casts' que no siguin a la llista."
+ },
+ "description": "Introdueix la configuraci\u00f3 de Google Cast."
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cast/translations/de.json b/homeassistant/components/cast/translations/de.json
index 87f8e7cb2bc9d2..a59b3421c109a5 100644
--- a/homeassistant/components/cast/translations/de.json
+++ b/homeassistant/components/cast/translations/de.json
@@ -1,13 +1,36 @@
{
"config": {
"abort": {
- "no_devices_found": "Keine Google Cast Ger\u00e4te im Netzwerk gefunden.",
- "single_instance_allowed": "Nur eine einzige Konfiguration von Google Cast ist notwendig."
+ "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden.",
+ "single_instance_allowed": "Bereits konfiguriert. Es ist nur eine Konfiguration m\u00f6glich."
+ },
+ "error": {
+ "invalid_known_hosts": "Bekannte Hosts m\u00fcssen eine durch Kommata getrennte Liste von Hosts sein."
},
"step": {
+ "config": {
+ "data": {
+ "known_hosts": "Optionale Liste bekannter Hosts, wenn die mDNS-Erkennung nicht funktioniert."
+ },
+ "description": "Bitte die Google Cast-Konfiguration eingeben.",
+ "title": "Google Cast"
+ },
"confirm": {
"description": "M\u00f6chtest du Google Cast einrichten?"
}
}
+ },
+ "options": {
+ "error": {
+ "invalid_known_hosts": "Bekannte Hosts m\u00fcssen eine durch Kommata getrennte Liste von Hosts sein."
+ },
+ "step": {
+ "options": {
+ "data": {
+ "known_hosts": "Optionale Liste bekannter Hosts, wenn die mDNS-Erkennung nicht funktioniert."
+ },
+ "description": "Bitte die Google Cast-Konfiguration eingeben."
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cast/translations/en.json b/homeassistant/components/cast/translations/en.json
index f05becffed3131..c2c2460cc9c7f8 100644
--- a/homeassistant/components/cast/translations/en.json
+++ b/homeassistant/components/cast/translations/en.json
@@ -4,10 +4,35 @@
"no_devices_found": "No devices found on the network",
"single_instance_allowed": "Already configured. Only a single configuration possible."
},
+ "error": {
+ "invalid_known_hosts": "Known hosts must be a comma separated list of hosts."
+ },
"step": {
+ "config": {
+ "data": {
+ "known_hosts": "Optional list of known hosts if mDNS discovery is not working."
+ },
+ "description": "Please enter the Google Cast configuration.",
+ "title": "Google Cast"
+ },
"confirm": {
"description": "Do you want to start set up?"
}
}
+ },
+ "options": {
+ "error": {
+ "invalid_known_hosts": "Known hosts must be a comma separated list of hosts."
+ },
+ "step": {
+ "options": {
+ "data": {
+ "ignore_cec": "Optional list which will be passed to pychromecast.IGNORE_CEC.",
+ "known_hosts": "Optional list of known hosts if mDNS discovery is not working.",
+ "uuid": "Optional list of UUIDs. Casts not listed will not be added."
+ },
+ "description": "Please enter the Google Cast configuration."
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cast/translations/es-419.json b/homeassistant/components/cast/translations/es-419.json
index c62ece17721ce1..ee30ef16b4697d 100644
--- a/homeassistant/components/cast/translations/es-419.json
+++ b/homeassistant/components/cast/translations/es-419.json
@@ -9,5 +9,15 @@
"description": "\u00bfDesea configurar Google Cast?"
}
}
+ },
+ "options": {
+ "step": {
+ "options": {
+ "data": {
+ "ignore_cec": "Lista opcional que se pasar\u00e1 a pychromecast.IGNORE_CEC.",
+ "uuid": "Lista opcional de UUID. No se agregar\u00e1n los dispositivos Cast que no est\u00e9n listados."
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cast/translations/es.json b/homeassistant/components/cast/translations/es.json
index 520df7ee4cdb26..07b090634e0a68 100644
--- a/homeassistant/components/cast/translations/es.json
+++ b/homeassistant/components/cast/translations/es.json
@@ -4,10 +4,33 @@
"no_devices_found": "No se encontraron dispositivos en la red",
"single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n."
},
+ "error": {
+ "invalid_known_hosts": "Los hosts conocidos deben ser una lista de hosts separados por comas."
+ },
"step": {
+ "config": {
+ "data": {
+ "known_hosts": "Lista opcional de hosts conocidos si el descubrimiento mDNS no funciona."
+ },
+ "description": "Introduce la configuraci\u00f3n de Google Cast.",
+ "title": "Google Cast"
+ },
"confirm": {
"description": "\u00bfQuieres iniciar la configuraci\u00f3n?"
}
}
+ },
+ "options": {
+ "error": {
+ "invalid_known_hosts": "Los hosts conocidos deben ser una lista de hosts separados por comas."
+ },
+ "step": {
+ "options": {
+ "data": {
+ "known_hosts": "Lista opcional de hosts conocidos si el descubrimiento mDNS no funciona."
+ },
+ "description": "Introduce la configuraci\u00f3n de Google Cast."
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cast/translations/et.json b/homeassistant/components/cast/translations/et.json
index 05287b5a52b9a2..6397951272a059 100644
--- a/homeassistant/components/cast/translations/et.json
+++ b/homeassistant/components/cast/translations/et.json
@@ -4,10 +4,35 @@
"no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi Google Casti seadet.",
"single_instance_allowed": "Vajalik on ainult \u00fcks Google Casti konfiguratsioon."
},
+ "error": {
+ "invalid_known_hosts": "Teadaolevad hostid peab olema komaeraldusega hostide loend."
+ },
"step": {
+ "config": {
+ "data": {
+ "known_hosts": "Valikuline loend teadaolevatest hostidest kui mDNS-i tuvastamine ei t\u00f6\u00f6ta."
+ },
+ "description": "Sisesta Google Casti andmed.",
+ "title": "Google Cast"
+ },
"confirm": {
"description": "Kas soovid seadistada Google Casti?"
}
}
+ },
+ "options": {
+ "error": {
+ "invalid_known_hosts": "Teadaolevad hostid peab olema komaeraldusega hostide loend."
+ },
+ "step": {
+ "options": {
+ "data": {
+ "ignore_cec": "Valikuline nimekiri mis edastatakse pychromecast.IGNORE_CEC-ile.",
+ "known_hosts": "Valikuline loend teadaolevatest hostidest kui mDNS-i tuvastamine ei t\u00f6\u00f6ta.",
+ "uuid": "Valikuline UUIDide loend. Loetlemata cast-e ei lisata."
+ },
+ "description": "Sisesta Google Casti andmed."
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cast/translations/fr.json b/homeassistant/components/cast/translations/fr.json
index afa4b094fad414..0acfd327e3e2e7 100644
--- a/homeassistant/components/cast/translations/fr.json
+++ b/homeassistant/components/cast/translations/fr.json
@@ -4,10 +4,33 @@
"no_devices_found": "Aucun appareil Google Cast trouv\u00e9 sur le r\u00e9seau.",
"single_instance_allowed": "Une seule configuration de Google Cast est n\u00e9cessaire."
},
+ "error": {
+ "invalid_known_hosts": "Les h\u00f4tes connus doivent \u00eatre une liste d'h\u00f4tes s\u00e9par\u00e9s par des virgules."
+ },
"step": {
+ "config": {
+ "data": {
+ "known_hosts": "Liste facultative des h\u00f4tes connus si la d\u00e9couverte mDNS ne fonctionne pas."
+ },
+ "description": "Veuillez saisir la configuration de Google Cast.",
+ "title": "Google Cast"
+ },
"confirm": {
"description": "Voulez-vous configurer Google Cast?"
}
}
+ },
+ "options": {
+ "error": {
+ "invalid_known_hosts": "Les h\u00f4tes connus doivent \u00eatre une liste d'h\u00f4tes s\u00e9par\u00e9s par des virgules."
+ },
+ "step": {
+ "options": {
+ "data": {
+ "known_hosts": "Liste facultative des h\u00f4tes connus si la d\u00e9couverte mDNS ne fonctionne pas."
+ },
+ "description": "Veuillez saisir la configuration de Google Cast."
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cast/translations/hu.json b/homeassistant/components/cast/translations/hu.json
index dc55cd224f8c43..7e5625c925d5fc 100644
--- a/homeassistant/components/cast/translations/hu.json
+++ b/homeassistant/components/cast/translations/hu.json
@@ -1,12 +1,36 @@
{
"config": {
"abort": {
- "no_devices_found": "Nem tal\u00e1lhat\u00f3k Google Cast eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton.",
- "single_instance_allowed": "Csak egyetlen Google Cast konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges."
+ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
+ "error": {
+ "invalid_known_hosts": "Az ismert hosztoknak vessz\u0151vel elv\u00e1lasztott hosztok list\u00e1j\u00e1nak kell lennie."
},
"step": {
+ "config": {
+ "data": {
+ "known_hosts": "Opcion\u00e1lis lista az ismert hosztokr\u00f3l, ha az mDNS felder\u00edt\u00e9s nem m\u0171k\u00f6dik."
+ },
+ "description": "K\u00e9rj\u00fck, add meg a Google Cast konfigur\u00e1ci\u00f3t.",
+ "title": "Google Cast"
+ },
"confirm": {
- "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Google Cast szolg\u00e1ltat\u00e1st?"
+ "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "invalid_known_hosts": "Az ismert hosztoknak vessz\u0151vel elv\u00e1lasztott hosztok list\u00e1j\u00e1nak kell lennie."
+ },
+ "step": {
+ "options": {
+ "data": {
+ "known_hosts": "Opcion\u00e1lis lista az ismert hosztokr\u00f3l, ha az mDNS felder\u00edt\u00e9s nem m\u0171k\u00f6dik.",
+ "uuid": "Az UUID-k opcion\u00e1lis list\u00e1ja. A felsorol\u00e1sban nem szerepl\u0151 szerepl\u0151g\u00e1rd\u00e1k nem ker\u00fclnek hozz\u00e1ad\u00e1sra."
+ },
+ "description": "K\u00e9rj\u00fck, add meg a Google Cast konfigur\u00e1ci\u00f3t."
}
}
}
diff --git a/homeassistant/components/cast/translations/id.json b/homeassistant/components/cast/translations/id.json
index d3e2bb5f360ff9..240ee853609761 100644
--- a/homeassistant/components/cast/translations/id.json
+++ b/homeassistant/components/cast/translations/id.json
@@ -1,12 +1,35 @@
{
"config": {
"abort": {
- "no_devices_found": "Tidak ada perangkat Google Cast yang ditemukan pada jaringan.",
- "single_instance_allowed": "Hanya satu konfigurasi Google Cast yang diperlukan."
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "invalid_known_hosts": "Host yang diketahui harus berupa daftar host yang dipisahkan koma."
},
"step": {
+ "config": {
+ "data": {
+ "known_hosts": "Daftar opsional host yang diketahui jika penemuan mDNS tidak berfungsi."
+ },
+ "description": "Masukkan konfigurasi Google Cast.",
+ "title": "Google Cast"
+ },
"confirm": {
- "description": "Apakah Anda ingin menyiapkan Google Cast?"
+ "description": "Ingin memulai penyiapan?"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "invalid_known_hosts": "Host yang diketahui harus berupa daftar host yang dipisahkan koma."
+ },
+ "step": {
+ "options": {
+ "data": {
+ "known_hosts": "Daftar opsional host yang diketahui jika penemuan mDNS tidak berfungsi."
+ },
+ "description": "Masukkan konfigurasi Google Cast."
}
}
}
diff --git a/homeassistant/components/cast/translations/it.json b/homeassistant/components/cast/translations/it.json
index 0278fe07bfeff9..83586bf9f2cfd1 100644
--- a/homeassistant/components/cast/translations/it.json
+++ b/homeassistant/components/cast/translations/it.json
@@ -4,10 +4,35 @@
"no_devices_found": "Nessun dispositivo trovato sulla rete",
"single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
+ "error": {
+ "invalid_known_hosts": "Gli host noti devono essere un elenco di host separato da virgole."
+ },
"step": {
+ "config": {
+ "data": {
+ "known_hosts": "Elenco facoltativo di host noti se l'individuazione di mDNS non funziona."
+ },
+ "description": "Inserisci la configurazione di Google Cast.",
+ "title": "Google Cast"
+ },
"confirm": {
"description": "Vuoi iniziare la configurazione?"
}
}
+ },
+ "options": {
+ "error": {
+ "invalid_known_hosts": "Gli host noti devono essere indicati sotto forma di un elenco di host separati da virgole."
+ },
+ "step": {
+ "options": {
+ "data": {
+ "ignore_cec": "Elenco opzionale che sar\u00e0 passato a pychromecast.IGNORE_CEC.",
+ "known_hosts": "Elenco facoltativo di host noti se l'individuazione di mDNS non funziona.",
+ "uuid": "Elenco opzionale di UUID. I cast non elencati non saranno aggiunti."
+ },
+ "description": "Inserisci la configurazione di Google Cast."
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cast/translations/ko.json b/homeassistant/components/cast/translations/ko.json
index e57fceb77052ca..b0bfd3271c9a96 100644
--- a/homeassistant/components/cast/translations/ko.json
+++ b/homeassistant/components/cast/translations/ko.json
@@ -1,12 +1,37 @@
{
"config": {
"abort": {
- "no_devices_found": "Google \uce90\uc2a4\ud2b8 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
- "single_instance_allowed": "\ud558\ub098\uc758 Google \uce90\uc2a4\ud2b8\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "invalid_known_hosts": "\uc54c\ub824\uc9c4 \ud638\uc2a4\ud2b8\ub294 \uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \ud638\uc2a4\ud2b8 \ubaa9\ub85d\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4."
},
"step": {
+ "config": {
+ "data": {
+ "known_hosts": "mDNS \uac80\uc0c9\uc774 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0 \uc54c\ub824\uc9c4 \ud638\uc2a4\ud2b8\uc758 \uc120\ud0dd\uc801 \ubaa9\ub85d\uc785\ub2c8\ub2e4."
+ },
+ "description": "Google Cast \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "Google Cast"
+ },
"confirm": {
- "description": "Google \uce90\uc2a4\ud2b8\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "invalid_known_hosts": "\uc54c\ub824\uc9c4 \ud638\uc2a4\ud2b8\ub294 \uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \ud638\uc2a4\ud2b8 \ubaa9\ub85d\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4."
+ },
+ "step": {
+ "options": {
+ "data": {
+ "ignore_cec": "pychromecast.IGNORE_CEC\uc5d0 \uc804\ub2ec\ub420 \uc120\ud0dd\uc801 \ubaa9\ub85d\uc785\ub2c8\ub2e4.",
+ "known_hosts": "mDNS \uac80\uc0c9\uc774 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0 \uc54c\ub824\uc9c4 \ud638\uc2a4\ud2b8\uc758 \uc120\ud0dd\uc801 \ubaa9\ub85d\uc785\ub2c8\ub2e4.",
+ "uuid": "UUID\uc758 \uc120\ud0dd\uc801 \ubaa9\ub85d\uc785\ub2c8\ub2e4. \ubaa9\ub85d\uc5d0 \uc5c6\ub294 \uce90\uc2a4\ud2b8\ub294 \ucd94\uac00\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4."
+ },
+ "description": "Google Cast \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694."
}
}
}
diff --git a/homeassistant/components/cast/translations/nl.json b/homeassistant/components/cast/translations/nl.json
index d42ef3e850c6bb..02bf7514761b30 100644
--- a/homeassistant/components/cast/translations/nl.json
+++ b/homeassistant/components/cast/translations/nl.json
@@ -1,12 +1,37 @@
{
"config": {
"abort": {
- "no_devices_found": "Geen Google Cast-apparaten gevonden op het netwerk.",
- "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van Google Cast nodig."
+ "no_devices_found": "Geen apparaten gevonden op het netwerk",
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
+ },
+ "error": {
+ "invalid_known_hosts": "Bekende hosts moet een door komma's gescheiden lijst van hosts zijn."
},
"step": {
+ "config": {
+ "data": {
+ "known_hosts": "Optionele lijst van bekende hosts indien mDNS discovery niet werkt."
+ },
+ "description": "Voer de Google Cast configuratie in.",
+ "title": "Google Cast"
+ },
"confirm": {
- "description": "Wilt u Google Cast instellen?"
+ "description": "Wil je beginnen met instellen?"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "invalid_known_hosts": "Bekende hosts moet een door komma's gescheiden lijst van hosts zijn."
+ },
+ "step": {
+ "options": {
+ "data": {
+ "ignore_cec": "Optionele lijst die zal worden doorgegeven aan pychromecast.IGNORE_CEC.",
+ "known_hosts": "Optionele lijst van bekende hosts indien mDNS discovery niet werkt.",
+ "uuid": "Optionele lijst van UUID's. Casts die niet in de lijst staan, worden niet toegevoegd."
+ },
+ "description": "Voer de Google Cast configuratie in."
}
}
}
diff --git a/homeassistant/components/cast/translations/no.json b/homeassistant/components/cast/translations/no.json
index b3d6b5d782e97a..0c9b3d93dce2d3 100644
--- a/homeassistant/components/cast/translations/no.json
+++ b/homeassistant/components/cast/translations/no.json
@@ -4,10 +4,35 @@
"no_devices_found": "Ingen enheter funnet p\u00e5 nettverket",
"single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
+ "error": {
+ "invalid_known_hosts": "Kjente verter m\u00e5 v\u00e6re en kommaseparert liste over verter."
+ },
"step": {
+ "config": {
+ "data": {
+ "known_hosts": "Valgfri liste over kjente verter hvis mDNS-oppdagelse ikke fungerer."
+ },
+ "description": "Angi Google Cast-konfigurasjonen.",
+ "title": "Google Cast"
+ },
"confirm": {
"description": "Vil du starte oppsettet?"
}
}
+ },
+ "options": {
+ "error": {
+ "invalid_known_hosts": "Kjente verter m\u00e5 v\u00e6re en kommaseparert liste over verter."
+ },
+ "step": {
+ "options": {
+ "data": {
+ "ignore_cec": "Valgfri liste som sendes til pychromecast.IGNORE_CEC.",
+ "known_hosts": "Valgfri liste over kjente verter hvis mDNS-oppdagelse ikke fungerer.",
+ "uuid": "Valgfri liste over UUIDer. Medvirkende som ikke er oppf\u00f8rt, blir ikke lagt til."
+ },
+ "description": "Angi Google Cast-konfigurasjonen."
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cast/translations/pl.json b/homeassistant/components/cast/translations/pl.json
index a8ee3fa57ac860..5802bda502abd4 100644
--- a/homeassistant/components/cast/translations/pl.json
+++ b/homeassistant/components/cast/translations/pl.json
@@ -4,10 +4,35 @@
"no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci",
"single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
},
+ "error": {
+ "invalid_known_hosts": "Znane hosty musz\u0105 by\u0107 list\u0105 host\u00f3w oddzielonych przecinkami."
+ },
"step": {
+ "config": {
+ "data": {
+ "known_hosts": "Opcjonalna lista znanych host\u00f3w, je\u015bli wykrywanie mDNS nie dzia\u0142a."
+ },
+ "description": "Wprowad\u017a konfiguracj\u0119 Google Cast.",
+ "title": "Google Cast"
+ },
"confirm": {
"description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?"
}
}
+ },
+ "options": {
+ "error": {
+ "invalid_known_hosts": "Znane hosty musz\u0105 by\u0107 list\u0105 host\u00f3w oddzielonych przecinkami."
+ },
+ "step": {
+ "options": {
+ "data": {
+ "ignore_cec": "Opcjonalna lista, kt\u00f3ra zostanie przekazana do pychromecast.IGNORE_CEC.",
+ "known_hosts": "Opcjonalna lista znanych host\u00f3w, je\u015bli wykrywanie mDNS nie dzia\u0142a.",
+ "uuid": "Opcjonalna lista identyfikator\u00f3w UUID. Casty nie wymienione na li\u015bcie nie zostan\u0105 dodane."
+ },
+ "description": "Wprowad\u017a konfiguracj\u0119 Google Cast."
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cast/translations/pt.json b/homeassistant/components/cast/translations/pt.json
index 9dd9a69a94c50b..2a5b62a9de190d 100644
--- a/homeassistant/components/cast/translations/pt.json
+++ b/homeassistant/components/cast/translations/pt.json
@@ -5,9 +5,19 @@
"single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do Google Cast \u00e9 necess\u00e1ria."
},
"step": {
+ "config": {
+ "title": "Google Cast"
+ },
"confirm": {
"description": "Deseja configurar o Google Cast?"
}
}
+ },
+ "options": {
+ "step": {
+ "options": {
+ "description": "Por favor introduza a configura\u00e7\u00e3o do Google Cast"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cast/translations/ru.json b/homeassistant/components/cast/translations/ru.json
index 85a42bf1be546c..7c412476151865 100644
--- a/homeassistant/components/cast/translations/ru.json
+++ b/homeassistant/components/cast/translations/ru.json
@@ -4,10 +4,35 @@
"no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.",
"single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e."
},
+ "error": {
+ "invalid_known_hosts": "\u0418\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0435 \u0445\u043e\u0441\u0442\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u044b \u0441\u043f\u0438\u0441\u043a\u043e\u043c, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u043c \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438."
+ },
"step": {
+ "config": {
+ "data": {
+ "known_hosts": "\u041d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0445 \u0445\u043e\u0441\u0442\u043e\u0432, \u0435\u0441\u043b\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 mDNS \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442."
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Google Cast.",
+ "title": "Google Cast"
+ },
"confirm": {
"description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?"
}
}
+ },
+ "options": {
+ "error": {
+ "invalid_known_hosts": "\u0418\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0435 \u0445\u043e\u0441\u0442\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u044b \u0441\u043f\u0438\u0441\u043a\u043e\u043c, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u043c \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438."
+ },
+ "step": {
+ "options": {
+ "data": {
+ "ignore_cec": "\u041d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d \u0432 pychromecast.IGNORE_CEC.",
+ "known_hosts": "\u041d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0445 \u0445\u043e\u0441\u0442\u043e\u0432, \u0435\u0441\u043b\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 mDNS \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442.",
+ "uuid": "\u041d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a UUID. \u041d\u0435 \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u043d\u044b\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043d\u0435 \u0431\u0443\u0434\u0443\u0442."
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Google Cast."
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cast/translations/tr.json b/homeassistant/components/cast/translations/tr.json
new file mode 100644
index 00000000000000..8de4663957ea85
--- /dev/null
+++ b/homeassistant/components/cast/translations/tr.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "step": {
+ "confirm": {
+ "description": "Kuruluma ba\u015flamak ister misiniz?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cast/translations/uk.json b/homeassistant/components/cast/translations/uk.json
index 783defdca258a3..292861e9129dbd 100644
--- a/homeassistant/components/cast/translations/uk.json
+++ b/homeassistant/components/cast/translations/uk.json
@@ -1,8 +1,12 @@
{
"config": {
+ "abort": {
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
"step": {
"confirm": {
- "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Google Cast?"
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?"
}
}
}
diff --git a/homeassistant/components/cast/translations/zh-Hans.json b/homeassistant/components/cast/translations/zh-Hans.json
index 1c2024f8b8114f..0feaac564400ab 100644
--- a/homeassistant/components/cast/translations/zh-Hans.json
+++ b/homeassistant/components/cast/translations/zh-Hans.json
@@ -5,9 +5,20 @@
"single_instance_allowed": "Google Cast \u53ea\u9700\u8981\u914d\u7f6e\u4e00\u6b21\u3002"
},
"step": {
+ "config": {
+ "description": "\u8bf7\u786e\u8ba4Goole Cast\u7684\u914d\u7f6e",
+ "title": "Google Cast"
+ },
"confirm": {
"description": "\u60a8\u60f3\u8981\u914d\u7f6e Google Cast \u5417\uff1f"
}
}
+ },
+ "options": {
+ "step": {
+ "options": {
+ "description": "\u8bf7\u786e\u8ba4Goole Cast\u7684\u914d\u7f6e"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cast/translations/zh-Hant.json b/homeassistant/components/cast/translations/zh-Hant.json
index 90c98e491dfea4..00810ade520413 100644
--- a/homeassistant/components/cast/translations/zh-Hant.json
+++ b/homeassistant/components/cast/translations/zh-Hant.json
@@ -4,10 +4,35 @@
"no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e",
"single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002"
},
+ "error": {
+ "invalid_known_hosts": "\u5df2\u77e5\u4e3b\u6a5f\u5fc5\u9808\u4ee5\u9017\u865f\u5206\u4e3b\u6a5f\u5217\u8868\u3002"
+ },
"step": {
+ "config": {
+ "data": {
+ "known_hosts": "\u5047\u5982 mDNS \u63a2\u7d22\u7121\u6cd5\u4f5c\u7528\uff0c\u5247\u70ba\u5df2\u77e5\u4e3b\u6a5f\u7684\u9078\u9805\u5217\u8868\u3002"
+ },
+ "description": "\u8acb\u8f38\u5165 Google Cast \u8a2d\u5b9a\u3002",
+ "title": "Google Cast"
+ },
"confirm": {
"description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f"
}
}
+ },
+ "options": {
+ "error": {
+ "invalid_known_hosts": "\u5df2\u77e5\u4e3b\u6a5f\u5fc5\u9808\u4ee5\u9017\u865f\u5206\u4e3b\u6a5f\u5217\u8868\u3002"
+ },
+ "step": {
+ "options": {
+ "data": {
+ "ignore_cec": "\u9078\u9805\u5217\u8868\u5c07\u50b3\u905e\u81f3 pychromecast.IGNORE_CEC\u3002",
+ "known_hosts": "\u5047\u5982 mDNS \u63a2\u7d22\u7121\u6cd5\u4f5c\u7528\uff0c\u5247\u70ba\u5df2\u77e5\u4e3b\u6a5f\u7684\u9078\u9805\u5217\u8868\u3002",
+ "uuid": "UUID \u9078\u9805\u5217\u8868\u3002\u672a\u5217\u51fa\u7684 Cast \u88dd\u7f6e\u5c07\u4e0d\u6703\u9032\u884c\u65b0\u589e\u3002"
+ },
+ "description": "\u8acb\u8f38\u5165 Google Cast \u8a2d\u5b9a\u3002"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py
index d01e38a2e2cbb3..22b3ce561298a1 100644
--- a/homeassistant/components/cert_expiry/__init__.py
+++ b/homeassistant/components/cert_expiry/__init__.py
@@ -1,11 +1,11 @@
"""The cert_expiry component."""
+from __future__ import annotations
+
from datetime import datetime, timedelta
import logging
-from typing import Optional
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -18,21 +18,13 @@
SCAN_INTERVAL = timedelta(hours=12)
-async def async_setup(hass, config):
- """Platform setup, do nothing."""
- return True
-
-
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""Load the saved entities."""
host = entry.data[CONF_HOST]
port = entry.data[CONF_PORT]
coordinator = CertExpiryDataUpdateCoordinator(hass, host, port)
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
@@ -71,7 +63,7 @@ def __init__(self, hass, host, port):
update_interval=SCAN_INTERVAL,
)
- async def _async_update_data(self) -> Optional[datetime]:
+ async def _async_update_data(self) -> datetime | None:
"""Fetch certificate."""
try:
timestamp = await get_cert_expiry_timestamp(self.hass, self.host, self.port)
diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py
index 7953a7bb8cfc7e..bfa5f46190b280 100644
--- a/homeassistant/components/cert_expiry/config_flow.py
+++ b/homeassistant/components/cert_expiry/config_flow.py
@@ -6,7 +6,7 @@
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PORT
-from .const import DEFAULT_PORT, DOMAIN # pylint: disable=unused-import
+from .const import DEFAULT_PORT, DOMAIN
from .errors import (
ConnectionRefused,
ConnectionTimeout,
@@ -63,9 +63,7 @@ async def async_step_user(self, user_input=None):
title=title,
data={CONF_HOST: host, CONF_PORT: port},
)
- if ( # pylint: disable=no-member
- self.context["source"] == config_entries.SOURCE_IMPORT
- ):
+ if self.context["source"] == config_entries.SOURCE_IMPORT:
_LOGGER.error("Config import failed for %s", user_input[CONF_HOST])
return self.async_abort(reason="import_failed")
else:
diff --git a/homeassistant/components/cert_expiry/helper.py b/homeassistant/components/cert_expiry/helper.py
index 6c49e9e26b9c6d..c00a99c8e86dab 100644
--- a/homeassistant/components/cert_expiry/helper.py
+++ b/homeassistant/components/cert_expiry/helper.py
@@ -19,8 +19,7 @@ def get_cert(host, port):
address = (host, port)
with socket.create_connection(address, timeout=TIMEOUT) as sock:
with ctx.wrap_socket(sock, server_hostname=address[0]) as ssock:
- # pylint disable: https://github.com/PyCQA/pylint/issues/3166
- cert = ssock.getpeercert() # pylint: disable=no-member
+ cert = ssock.getpeercert()
return cert
diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py
index 0e329b1898f507..a05acdb5d77397 100644
--- a/homeassistant/components/cert_expiry/sensor.py
+++ b/homeassistant/components/cert_expiry/sensor.py
@@ -3,7 +3,7 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
CONF_HOST,
@@ -68,7 +68,7 @@ def icon(self):
return "mdi:certificate"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return additional sensor state attributes."""
return {
"is_valid": self.coordinator.is_cert_valid,
@@ -76,7 +76,7 @@ def device_state_attributes(self):
}
-class SSLCertificateTimestamp(CertExpiryEntity):
+class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity):
"""Implementation of the Cert Expiry timestamp sensor."""
@property
diff --git a/homeassistant/components/cert_expiry/translations/hu.json b/homeassistant/components/cert_expiry/translations/hu.json
index 5bad24ecb6a1a0..2ae516565e3a34 100644
--- a/homeassistant/components/cert_expiry/translations/hu.json
+++ b/homeassistant/components/cert_expiry/translations/hu.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van",
"import_failed": "Nem siker\u00fclt import\u00e1lni a konfigur\u00e1ci\u00f3t"
},
"step": {
diff --git a/homeassistant/components/cert_expiry/translations/id.json b/homeassistant/components/cert_expiry/translations/id.json
new file mode 100644
index 00000000000000..9fac285fe824d7
--- /dev/null
+++ b/homeassistant/components/cert_expiry/translations/id.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi",
+ "import_failed": "Impor dari konfigurasi gagal"
+ },
+ "error": {
+ "connection_refused": "Sambungan ditolak saat menghubungkan ke host",
+ "connection_timeout": "Tenggang waktu terhubung ke host ini habis",
+ "resolve_failed": "Host ini tidak dapat ditemukan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nama sertifikat",
+ "port": "Port"
+ },
+ "title": "Tentukan sertifikat yang akan diuji"
+ }
+ }
+ },
+ "title": "Informasi Kedaluwarsa Sertifikat"
+}
\ No newline at end of file
diff --git a/homeassistant/components/cert_expiry/translations/ko.json b/homeassistant/components/cert_expiry/translations/ko.json
index ee912a3369532b..8782776977195a 100644
--- a/homeassistant/components/cert_expiry/translations/ko.json
+++ b/homeassistant/components/cert_expiry/translations/ko.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"import_failed": "\uad6c\uc131\uc5d0\uc11c \uac00\uc838\uc624\uae30 \uc2e4\ud328"
},
"error": {
diff --git a/homeassistant/components/cert_expiry/translations/nl.json b/homeassistant/components/cert_expiry/translations/nl.json
index d844d28e62fe8a..e3cd3d7983beb0 100644
--- a/homeassistant/components/cert_expiry/translations/nl.json
+++ b/homeassistant/components/cert_expiry/translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Deze combinatie van host en poort is al geconfigureerd",
+ "already_configured": "Service is al geconfigureerd",
"import_failed": "Importeren vanuit configuratie is mislukt"
},
"error": {
@@ -12,9 +12,9 @@
"step": {
"user": {
"data": {
- "host": "De hostnaam van het certificaat",
+ "host": "Host",
"name": "De naam van het certificaat",
- "port": "De poort van het certificaat"
+ "port": "Poort"
},
"title": "Het certificaat defini\u00ebren dat moet worden getest"
}
diff --git a/homeassistant/components/cert_expiry/translations/sv.json b/homeassistant/components/cert_expiry/translations/sv.json
index 23703f11e5bedf..f00fc236d093c8 100644
--- a/homeassistant/components/cert_expiry/translations/sv.json
+++ b/homeassistant/components/cert_expiry/translations/sv.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Tj\u00e4nsten har redan konfigurerats"
+ },
"error": {
"connection_refused": "Anslutningen blev tillbakavisad under anslutning till v\u00e4rd.",
"connection_timeout": "Timeout vid anslutning till den h\u00e4r v\u00e4rden",
diff --git a/homeassistant/components/cert_expiry/translations/tr.json b/homeassistant/components/cert_expiry/translations/tr.json
new file mode 100644
index 00000000000000..6c05bef3a65f64
--- /dev/null
+++ b/homeassistant/components/cert_expiry/translations/tr.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cert_expiry/translations/uk.json b/homeassistant/components/cert_expiry/translations/uk.json
new file mode 100644
index 00000000000000..997e12a8cb29a9
--- /dev/null
+++ b/homeassistant/components/cert_expiry/translations/uk.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.",
+ "import_failed": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0456\u043c\u043f\u043e\u0440\u0442\u0443 \u0437 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457."
+ },
+ "error": {
+ "connection_refused": "\u041f\u0440\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456 \u0434\u043e \u0445\u043e\u0441\u0442\u0443 \u0431\u0443\u043b\u043e \u0432\u0456\u0434\u043c\u043e\u0432\u043b\u0435\u043d\u043e \u0432 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u0456.",
+ "connection_timeout": "\u0427\u0430\u0441 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0445\u043e\u0441\u0442\u0430 \u043c\u0438\u043d\u0443\u0432.",
+ "resolve_failed": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044c \u0434\u043e \u0445\u043e\u0441\u0442\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "title": "\u0422\u0435\u0440\u043c\u0456\u043d \u0434\u0456\u0457 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430"
+ }
+ }
+ },
+ "title": "\u0422\u0435\u0440\u043c\u0456\u043d \u0434\u0456\u0457 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430"
+}
\ No newline at end of file
diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py
index 481cdd7ecad3d5..5376dc3fe97848 100644
--- a/homeassistant/components/channels/media_player.py
+++ b/homeassistant/components/channels/media_player.py
@@ -18,6 +18,7 @@
SUPPORT_VOLUME_MUTE,
)
from homeassistant.const import (
+ ATTR_SECONDS,
CONF_HOST,
CONF_NAME,
CONF_PORT,
@@ -53,10 +54,6 @@
)
-# Service call validation schemas
-ATTR_SECONDS = "seconds"
-
-
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Channels platform."""
device = ChannelsPlayer(config[CONF_NAME], config[CONF_HOST], config[CONF_PORT])
diff --git a/homeassistant/components/cisco_ios/device_tracker.py b/homeassistant/components/cisco_ios/device_tracker.py
index 8bf2b77fa2502e..0c77fc6fd7ee12 100644
--- a/homeassistant/components/cisco_ios/device_tracker.py
+++ b/homeassistant/components/cisco_ios/device_tracker.py
@@ -47,7 +47,7 @@ def __init__(self, config):
self.last_results = {}
self.success_init = self._update_info()
- _LOGGER.info("cisco_ios scanner initialized")
+ _LOGGER.info("Initialized cisco_ios scanner")
def get_device_name(self, device):
"""Get the firmware doesn't save the name of the wireless device."""
@@ -131,8 +131,7 @@ def _get_arp_data(self):
return devices_result.decode("utf-8")
except pxssh.ExceptionPxssh as px_e:
- _LOGGER.error("pxssh failed on login")
- _LOGGER.error(px_e)
+ _LOGGER.error("Failed to login via pxssh: %s", px_e)
return None
diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py
index 924ef2fa6b4dc3..bc323a51151e0d 100644
--- a/homeassistant/components/citybikes/sensor.py
+++ b/homeassistant/components/citybikes/sensor.py
@@ -7,7 +7,11 @@
import async_timeout
import voluptuous as vol
-from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA
+from homeassistant.components.sensor import (
+ ENTITY_ID_FORMAT,
+ PLATFORM_SCHEMA,
+ SensorEntity,
+)
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_ID,
@@ -25,7 +29,7 @@
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity, async_generate_entity_id
+from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util import distance, location
@@ -258,7 +262,7 @@ async def async_refresh(self, now=None):
raise PlatformNotReady from err
-class CityBikesStation(Entity):
+class CityBikesStation(SensorEntity):
"""CityBikes API Sensor."""
def __init__(self, network, station_id, entity_id):
@@ -286,7 +290,7 @@ async def async_update(self):
break
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._station_data:
return {
diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py
new file mode 100644
index 00000000000000..8095f7991bde5e
--- /dev/null
+++ b/homeassistant/components/climacell/__init__.py
@@ -0,0 +1,370 @@
+"""The ClimaCell integration."""
+from __future__ import annotations
+
+import asyncio
+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.weather import DOMAIN as WEATHER_DOMAIN
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ CONF_API_KEY,
+ CONF_API_VERSION,
+ CONF_LATITUDE,
+ CONF_LONGITUDE,
+ CONF_NAME,
+)
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+ UpdateFailed,
+)
+
+from .const import (
+ ATTRIBUTION,
+ CC_ATTR_CONDITION,
+ CC_ATTR_HUMIDITY,
+ CC_ATTR_OZONE,
+ CC_ATTR_PRECIPITATION,
+ CC_ATTR_PRECIPITATION_PROBABILITY,
+ CC_ATTR_PRESSURE,
+ CC_ATTR_TEMPERATURE,
+ CC_ATTR_TEMPERATURE_HIGH,
+ CC_ATTR_TEMPERATURE_LOW,
+ CC_ATTR_VISIBILITY,
+ CC_ATTR_WIND_DIRECTION,
+ CC_ATTR_WIND_SPEED,
+ 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_PRESSURE,
+ CC_V3_ATTR_TEMPERATURE,
+ CC_V3_ATTR_VISIBILITY,
+ CC_V3_ATTR_WIND_DIRECTION,
+ CC_V3_ATTR_WIND_SPEED,
+ CONF_TIMESTEP,
+ DEFAULT_FORECAST_TYPE,
+ DEFAULT_TIMESTEP,
+ DOMAIN,
+ MAX_REQUESTS_PER_DAY,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORMS = [WEATHER_DOMAIN]
+
+
+def _set_update_interval(
+ hass: HomeAssistantType, 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: HomeAssistantType, config_entry: ConfigEntry) -> bool:
+ """Set up ClimaCell API from a config entry."""
+ hass.data.setdefault(DOMAIN, {})
+
+ params = {}
+ # If config entry options not set up, set them up
+ if not config_entry.options:
+ params["options"] = {
+ CONF_TIMESTEP: DEFAULT_TIMESTEP,
+ }
+ else:
+ # Use valid timestep if it's invalid
+ timestep = config_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 = config_entry.options.copy()
+ new_options[CONF_TIMESTEP] = timestep
+ params["options"] = new_options
+ # Add API version if not found
+ if CONF_API_VERSION not in config_entry.data:
+ new_data = config_entry.data.copy()
+ new_data[CONF_API_VERSION] = 3
+ params["data"] = new_data
+
+ if params:
+ hass.config_entries.async_update_entry(config_entry, **params)
+
+ api_class = ClimaCellV3 if config_entry.data[CONF_API_VERSION] == 3 else ClimaCellV4
+ api = api_class(
+ config_entry.data[CONF_API_KEY],
+ config_entry.data.get(CONF_LATITUDE, hass.config.latitude),
+ config_entry.data.get(CONF_LONGITUDE, hass.config.longitude),
+ session=async_get_clientsession(hass),
+ )
+
+ coordinator = ClimaCellDataUpdateCoordinator(
+ hass,
+ config_entry,
+ api,
+ _set_update_interval(hass, config_entry),
+ )
+
+ await coordinator.async_config_entry_first_refresh()
+
+ hass.data[DOMAIN][config_entry.entry_id] = coordinator
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry
+) -> bool:
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+
+ hass.data[DOMAIN].pop(config_entry.entry_id)
+ 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: HomeAssistantType,
+ 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 = {FORECASTS: {}}
+ try:
+ if self._api_version == 3:
+ 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,
+ ]
+ )
+ 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],
+ )
+ else:
+ return await self._api.realtime_and_all_forecasts(
+ [
+ CC_ATTR_TEMPERATURE,
+ CC_ATTR_HUMIDITY,
+ CC_ATTR_PRESSURE,
+ CC_ATTR_WIND_SPEED,
+ CC_ATTR_WIND_DIRECTION,
+ CC_ATTR_CONDITION,
+ CC_ATTR_VISIBILITY,
+ CC_ATTR_OZONE,
+ ],
+ [
+ CC_ATTR_TEMPERATURE_LOW,
+ CC_ATTR_TEMPERATURE_HIGH,
+ CC_ATTR_WIND_SPEED,
+ CC_ATTR_WIND_DIRECTION,
+ CC_ATTR_CONDITION,
+ CC_ATTR_PRECIPITATION,
+ CC_ATTR_PRECIPITATION_PROBABILITY,
+ ],
+ )
+ except (
+ CantConnectException,
+ InvalidAPIKeyException,
+ RateLimitedException,
+ UnknownException,
+ ) as error:
+ raise UpdateFailed from error
+
+ return data
+
+
+class ClimaCellEntity(CoordinatorEntity):
+ """Base ClimaCell Entity."""
+
+ def __init__(
+ self,
+ config_entry: ConfigEntry,
+ coordinator: ClimaCellDataUpdateCoordinator,
+ forecast_type: str,
+ api_version: int,
+ ) -> None:
+ """Initialize ClimaCell Entity."""
+ super().__init__(coordinator)
+ self.api_version = api_version
+ self.forecast_type = forecast_type
+ 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."""
+ 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 entity_registry_enabled_default(self) -> bool:
+ """Return if the entity should be enabled when first added to the entity registry."""
+ if self.forecast_type == DEFAULT_FORECAST_TYPE:
+ return True
+
+ return False
+
+ @property
+ def name(self) -> str:
+ """Return the name of the entity."""
+ return f"{self._config_entry.data[CONF_NAME]} - {self.forecast_type.title()}"
+
+ @property
+ def unique_id(self) -> str:
+ """Return the unique id of the entity."""
+ return f"{self._config_entry.unique_id}_{self.forecast_type}"
+
+ @property
+ def attribution(self):
+ """Return the attribution."""
+ return ATTRIBUTION
+
+ @property
+ def device_info(self) -> dict[str, Any]:
+ """Return device registry information."""
+ return {
+ "identifiers": {(DOMAIN, self._config_entry.data[CONF_API_KEY])},
+ "name": "ClimaCell",
+ "manufacturer": "ClimaCell",
+ "sw_version": "v3",
+ "entry_type": "service",
+ }
diff --git a/homeassistant/components/climacell/config_flow.py b/homeassistant/components/climacell/config_flow.py
new file mode 100644
index 00000000000000..1457479e62aca0
--- /dev/null
+++ b/homeassistant/components/climacell/config_flow.py
@@ -0,0 +1,166 @@
+"""Config flow for ClimaCell integration."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from pyclimacell import ClimaCellV3
+from pyclimacell.exceptions import (
+ CantConnectException,
+ InvalidAPIKeyException,
+ RateLimitedException,
+)
+from pyclimacell.pyclimacell import ClimaCellV4
+import voluptuous as vol
+
+from homeassistant import config_entries, core
+from homeassistant.const import (
+ CONF_API_KEY,
+ CONF_API_VERSION,
+ CONF_LATITUDE,
+ CONF_LONGITUDE,
+ CONF_NAME,
+)
+from homeassistant.core import callback
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.typing import HomeAssistantType
+
+from .const import (
+ CC_ATTR_TEMPERATURE,
+ CC_V3_ATTR_TEMPERATURE,
+ CONF_TIMESTEP,
+ DEFAULT_NAME,
+ DEFAULT_TIMESTEP,
+ DOMAIN,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def _get_config_schema(
+ hass: core.HomeAssistant, input_dict: dict[str, Any] = None
+) -> vol.Schema:
+ """
+ Return schema defaults for init step based on user input/config dict.
+
+ Retain info already provided for future form views by setting them as
+ defaults in schema.
+ """
+ if input_dict is None:
+ input_dict = {}
+
+ return vol.Schema(
+ {
+ vol.Required(
+ CONF_NAME, default=input_dict.get(CONF_NAME, DEFAULT_NAME)
+ ): str,
+ vol.Required(CONF_API_KEY, default=input_dict.get(CONF_API_KEY)): str,
+ vol.Required(CONF_API_VERSION, default=4): vol.In([3, 4]),
+ vol.Inclusive(
+ CONF_LATITUDE,
+ "location",
+ default=input_dict.get(CONF_LATITUDE, hass.config.latitude),
+ ): cv.latitude,
+ vol.Inclusive(
+ CONF_LONGITUDE,
+ "location",
+ default=input_dict.get(CONF_LONGITUDE, hass.config.longitude),
+ ): cv.longitude,
+ },
+ extra=vol.REMOVE_EXTRA,
+ )
+
+
+def _get_unique_id(hass: HomeAssistantType, input_dict: dict[str, Any]):
+ """Return unique ID from config data."""
+ return (
+ f"{input_dict[CONF_API_KEY]}"
+ f"_{input_dict.get(CONF_LATITUDE, hass.config.latitude)}"
+ f"_{input_dict.get(CONF_LONGITUDE, hass.config.longitude)}"
+ )
+
+
+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
+ ) -> dict[str, Any]:
+ """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
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(
+ config_entry: config_entries.ConfigEntry,
+ ) -> ClimaCellOptionsConfigFlow:
+ """Get the options flow for this handler."""
+ return ClimaCellOptionsConfigFlow(config_entry)
+
+ async def async_step_user(
+ self, user_input: dict[str, Any] = None
+ ) -> dict[str, Any]:
+ """Handle the initial step."""
+ errors = {}
+ if user_input is not None:
+ await self.async_set_unique_id(
+ unique_id=_get_unique_id(self.hass, user_input)
+ )
+ self._abort_if_unique_id_configured()
+
+ try:
+ if user_input[CONF_API_VERSION] == 3:
+ api_class = ClimaCellV3
+ field = CC_V3_ATTR_TEMPERATURE
+ else:
+ api_class = ClimaCellV4
+ field = CC_ATTR_TEMPERATURE
+ await api_class(
+ user_input[CONF_API_KEY],
+ str(user_input.get(CONF_LATITUDE, self.hass.config.latitude)),
+ str(user_input.get(CONF_LONGITUDE, self.hass.config.longitude)),
+ session=async_get_clientsession(self.hass),
+ ).realtime([field])
+
+ return self.async_create_entry(
+ title=user_input[CONF_NAME], data=user_input
+ )
+ except CantConnectException:
+ errors["base"] = "cannot_connect"
+ except InvalidAPIKeyException:
+ errors[CONF_API_KEY] = "invalid_api_key"
+ except RateLimitedException:
+ errors[CONF_API_KEY] = "rate_limited"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=_get_config_schema(self.hass, user_input),
+ errors=errors,
+ )
diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py
new file mode 100644
index 00000000000000..01d85dcc161366
--- /dev/null
+++ b/homeassistant/components/climacell/const.py
@@ -0,0 +1,119 @@
+"""Constants for the ClimaCell integration."""
+from pyclimacell.const import DAILY, HOURLY, NOWCAST, WeatherCode
+
+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,
+)
+
+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 = 1000
+
+CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY}
+
+MAX_FORECASTS = {
+ DAILY: 14,
+ HOURLY: 24,
+ NOWCAST: 30,
+}
+
+# V4 constants
+CONDITIONS = {
+ WeatherCode.WIND: ATTR_CONDITION_WINDY,
+ WeatherCode.LIGHT_WIND: ATTR_CONDITION_WINDY,
+ WeatherCode.STRONG_WIND: ATTR_CONDITION_WINDY,
+ WeatherCode.FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY,
+ WeatherCode.HEAVY_FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY,
+ WeatherCode.LIGHT_FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY,
+ WeatherCode.FREEZING_DRIZZLE: ATTR_CONDITION_SNOWY_RAINY,
+ WeatherCode.ICE_PELLETS: ATTR_CONDITION_HAIL,
+ WeatherCode.HEAVY_ICE_PELLETS: ATTR_CONDITION_HAIL,
+ WeatherCode.LIGHT_ICE_PELLETS: ATTR_CONDITION_HAIL,
+ WeatherCode.SNOW: ATTR_CONDITION_SNOWY,
+ WeatherCode.HEAVY_SNOW: ATTR_CONDITION_SNOWY,
+ WeatherCode.LIGHT_SNOW: ATTR_CONDITION_SNOWY,
+ WeatherCode.FLURRIES: ATTR_CONDITION_SNOWY,
+ WeatherCode.THUNDERSTORM: ATTR_CONDITION_LIGHTNING,
+ WeatherCode.RAIN: ATTR_CONDITION_POURING,
+ WeatherCode.HEAVY_RAIN: ATTR_CONDITION_RAINY,
+ WeatherCode.LIGHT_RAIN: ATTR_CONDITION_RAINY,
+ WeatherCode.DRIZZLE: ATTR_CONDITION_RAINY,
+ WeatherCode.FOG: ATTR_CONDITION_FOG,
+ WeatherCode.LIGHT_FOG: ATTR_CONDITION_FOG,
+ WeatherCode.CLOUDY: ATTR_CONDITION_CLOUDY,
+ WeatherCode.MOSTLY_CLOUDY: ATTR_CONDITION_CLOUDY,
+ WeatherCode.PARTLY_CLOUDY: ATTR_CONDITION_PARTLYCLOUDY,
+}
+
+CC_ATTR_TIMESTAMP = "startTime"
+CC_ATTR_TEMPERATURE = "temperature"
+CC_ATTR_TEMPERATURE_HIGH = "temperatureMax"
+CC_ATTR_TEMPERATURE_LOW = "temperatureMin"
+CC_ATTR_PRESSURE = "pressureSeaLevel"
+CC_ATTR_HUMIDITY = "humidity"
+CC_ATTR_WIND_SPEED = "windSpeed"
+CC_ATTR_WIND_DIRECTION = "windDirection"
+CC_ATTR_OZONE = "pollutantO3"
+CC_ATTR_CONDITION = "weatherCode"
+CC_ATTR_VISIBILITY = "visibility"
+CC_ATTR_PRECIPITATION = "precipitationIntensityAvg"
+CC_ATTR_PRECIPITATION_PROBABILITY = "precipitationProbability"
+
+# 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,
+}
+
+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"
diff --git a/homeassistant/components/climacell/manifest.json b/homeassistant/components/climacell/manifest.json
new file mode 100644
index 00000000000000..1df0b3613bba5e
--- /dev/null
+++ b/homeassistant/components/climacell/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "climacell",
+ "name": "ClimaCell",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/climacell",
+ "requirements": ["pyclimacell==0.18.0"],
+ "codeowners": ["@raman325"]
+}
diff --git a/homeassistant/components/climacell/strings.json b/homeassistant/components/climacell/strings.json
new file mode 100644
index 00000000000000..f4347d254b704a
--- /dev/null
+++ b/homeassistant/components/climacell/strings.json
@@ -0,0 +1,34 @@
+{
+ "title": "ClimaCell",
+ "config": {
+ "step": {
+ "user": {
+ "description": "If [%key:common::config_flow::data::latitude%] and [%key:common::config_flow::data::longitude%] are not provided, the default values in the Home Assistant configuration will be used. An entity will be created for each forecast type but only the ones you select will be enabled by default.",
+ "data": {
+ "name": "[%key:common::config_flow::data::name%]",
+ "api_key": "[%key:common::config_flow::data::api_key%]",
+ "api_version": "API Version",
+ "latitude": "[%key:common::config_flow::data::latitude%]",
+ "longitude": "[%key:common::config_flow::data::longitude%]"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]",
+ "rate_limited": "Currently rate limited, please try again later."
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "Update [%key:component::climacell::title%] 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/translations/af.json b/homeassistant/components/climacell/translations/af.json
new file mode 100644
index 00000000000000..b62fc7023a47b1
--- /dev/null
+++ b/homeassistant/components/climacell/translations/af.json
@@ -0,0 +1,10 @@
+{
+ "options": {
+ "step": {
+ "init": {
+ "title": "Update ClimaCell opties"
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/bg.json b/homeassistant/components/climacell/translations/bg.json
new file mode 100644
index 00000000000000..6b1e4d3cba27ab
--- /dev/null
+++ b/homeassistant/components/climacell/translations/bg.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "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/climacell/translations/ca.json b/homeassistant/components/climacell/translations/ca.json
new file mode 100644
index 00000000000000..3f215b63234356
--- /dev/null
+++ b/homeassistant/components/climacell/translations/ca.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_api_key": "Clau API inv\u00e0lida",
+ "rate_limited": "Freq\u00fc\u00e8ncia limitada temporalment, torna-ho a provar m\u00e9s tard.",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Clau API",
+ "api_version": "Versi\u00f3 de l'API",
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "name": "Nom"
+ },
+ "description": "Si no es proporcionen la Latitud i Longitud, s'utilitzaran els valors per defecte de la configuraci\u00f3 de Home Assistant. Es crear\u00e0 una entitat per a cada tipus de previsi\u00f3, per\u00f2 nom\u00e9s s'habilitaran les que seleccionis."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "forecast_types": "Tipus de previsi\u00f3",
+ "timestep": "Minuts entre previsions NowCast"
+ },
+ "description": "Si decideixes activar l'entitat de predicci\u00f3 \"nowcast\", podr\u00e0s configurar l'interval en minuts entre cada previsi\u00f3. El nombre de previsions proporcionades dep\u00e8n d'aquest interval de minuts.",
+ "title": "Actualitzaci\u00f3 de les opcions de ClimaCell"
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/cs.json b/homeassistant/components/climacell/translations/cs.json
new file mode 100644
index 00000000000000..1ae29deb08c1fb
--- /dev/null
+++ b/homeassistant/components/climacell/translations/cs.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
+ "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API",
+ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kl\u00ed\u010d API",
+ "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka",
+ "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka",
+ "name": "Jm\u00e9no"
+ }
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/de.json b/homeassistant/components/climacell/translations/de.json
new file mode 100644
index 00000000000000..e53b96d8e731c3
--- /dev/null
+++ b/homeassistant/components/climacell/translations/de.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel",
+ "rate_limited": "Aktuelle Aktualisierungsrate gedrosselt, bitte versuche es sp\u00e4ter erneut.",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-Schl\u00fcssel",
+ "api_version": "API Version",
+ "latitude": "Breitengrad",
+ "longitude": "L\u00e4ngengrad",
+ "name": "Name"
+ },
+ "description": "Wenn Breitengrad und L\u00e4ngengrad nicht angegeben werden, werden die Standardwerte in der Home Assistant-Konfiguration verwendet. F\u00fcr jeden Vorhersagetyp wird eine Entit\u00e4t erstellt, aber nur die von Ihnen ausgew\u00e4hlten werden standardm\u00e4\u00dfig aktiviert."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "forecast_types": "Vorhersage Arten",
+ "timestep": "Minuten zwischen den Kurzvorhersagen"
+ },
+ "description": "Wenn du die Vorhersage-Entitit\u00e4t \"Kurzvorhersage\" aktivierst, kannst du die Anzahl der Minuten zwischen den einzelnen Vorhersagen konfigurieren. Die Anzahl der bereitgestellten Vorhersagen h\u00e4ngt von der Anzahl der zwischen den Vorhersagen gew\u00e4hlten Minuten ab.",
+ "title": "Aktualisiere ClimaCell-Optionen"
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/el.json b/homeassistant/components/climacell/translations/el.json
new file mode 100644
index 00000000000000..45ed5d8a722b4f
--- /dev/null
+++ b/homeassistant/components/climacell/translations/el.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "error": {
+ "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API",
+ "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03a0\u03bb\u03ac\u03c4\u03bf\u03c2",
+ "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u039c\u03ae\u03ba\u03bf\u03c2",
+ "name": "\u038c\u03bd\u03bf\u03bc\u03b1"
+ },
+ "description": "\u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c4\u03bf \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2 \u03ba\u03b1\u03b9 \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2, \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd \u03bf\u03b9 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c4\u03b9\u03bc\u03ad\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c4\u03bf\u03c5 Home Assistant. \u0398\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c4\u03cd\u03c0\u03bf \u03b4\u03b5\u03bb\u03c4\u03af\u03bf\u03c5, \u03b1\u03bb\u03bb\u03ac \u03bc\u03cc\u03bd\u03bf \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03bb\u03ad\u03b3\u03b5\u03c4\u03b5 \u03b8\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd \u03b1\u03c0\u03cc \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "forecast_types": "\u0395\u03af\u03b4\u03bf\u03c2/\u03b7 \u0394\u03b5\u03bb\u03c4\u03af\u03bf\u03c5/\u03c9\u03bd",
+ "timestep": "\u039b\u03b5\u03c0\u03c4\u03ac \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd NowCast"
+ },
+ "description": "\u0395\u03ac\u03bd \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd 'nowcast', \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03ba\u03ac\u03b8\u03b5 \u03b4\u03b5\u03bb\u03c4\u03af\u03bf\u03c5. \u039f \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b5\u03be\u03b1\u03c1\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03bb\u03ad\u03b3\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd.",
+ "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b5 \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 ClimaCell"
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/en.json b/homeassistant/components/climacell/translations/en.json
new file mode 100644
index 00000000000000..c126cf170b1314
--- /dev/null
+++ b/homeassistant/components/climacell/translations/en.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_api_key": "Invalid API key",
+ "rate_limited": "Currently rate limited, please try again later.",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Key",
+ "api_version": "API Version",
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "name": "Name"
+ },
+ "description": "If Latitude and Longitude are not provided, the default values in the Home Assistant configuration will be used. An entity will be created for each forecast type but only the ones you select will be enabled by default."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "forecast_types": "Forecast Type(s)",
+ "timestep": "Min. Between NowCast Forecasts"
+ },
+ "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.",
+ "title": "Update ClimaCell Options"
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/es.json b/homeassistant/components/climacell/translations/es.json
new file mode 100644
index 00000000000000..52fd5d21166da9
--- /dev/null
+++ b/homeassistant/components/climacell/translations/es.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Fallo al conectar",
+ "rate_limited": "Actualmente la tarifa est\u00e1 limitada, por favor int\u00e9ntelo m\u00e1s tarde.",
+ "unknown": "Error inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "name": "Nombre"
+ },
+ "description": "Si no se proporcionan Latitud y Longitud , se utilizar\u00e1n los valores predeterminados en la configuraci\u00f3n de Home Assistant. Se crear\u00e1 una entidad para cada tipo de pron\u00f3stico, pero solo las que seleccione estar\u00e1n habilitadas de forma predeterminada."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "forecast_types": "Tipo(s) de pron\u00f3stico",
+ "timestep": "Min. Entre pron\u00f3sticos de NowCast"
+ },
+ "description": "Si elige habilitar la entidad de pron\u00f3stico \"nowcast\", puede configurar el n\u00famero de minutos entre cada pron\u00f3stico. El n\u00famero de pron\u00f3sticos proporcionados depende del n\u00famero de minutos elegidos entre los pron\u00f3sticos.",
+ "title": "Actualizar las opciones ClimaCell"
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/et.json b/homeassistant/components/climacell/translations/et.json
new file mode 100644
index 00000000000000..de44f9d70d1e47
--- /dev/null
+++ b/homeassistant/components/climacell/translations/et.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_api_key": "Vale API v\u00f5ti",
+ "rate_limited": "Hetkel on p\u00e4ringud piiratud, proovi hiljem uuesti.",
+ "unknown": "Tundmatu t\u00f5rge"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API v\u00f5ti",
+ "api_version": "API versioon",
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad",
+ "name": "Nimi"
+ },
+ "description": "Kui [%key:component::climacell::config::step::user::d ata::latitude%] ja [%key:component::climacell::config::step::user::d ata::longitude%] andmed pole sisestatud kasutatakse Home Assistanti vaikev\u00e4\u00e4rtusi. Olem luuakse iga prognoosit\u00fc\u00fcbi jaoks kuid vaikimisi lubatakse ainult need, mille valid."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "forecast_types": "Prognoosi t\u00fc\u00fcp (t\u00fc\u00fcbid)",
+ "timestep": "Minuteid NowCasti prognooside vahel"
+ },
+ "description": "Kui otsustad lubada \"nowcast\" prognoosi\u00fcksuse, saad seadistada minutite arvu iga prognoosi vahel. Esitatavate prognooside arv s\u00f5ltub prognooside vahel valitud minutite arvust.",
+ "title": "V\u00e4rskenda ClimaCell suvandeid"
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/fr.json b/homeassistant/components/climacell/translations/fr.json
new file mode 100644
index 00000000000000..3b3aa3d18babfb
--- /dev/null
+++ b/homeassistant/components/climacell/translations/fr.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "invalid_api_key": "Cl\u00e9 API invalide",
+ "rate_limited": "Nombre maximal de tentatives de connexion d\u00e9pass\u00e9, veuillez r\u00e9essayer ult\u00e9rieurement",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Cl\u00e9 d'API",
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "name": "Nom"
+ },
+ "description": "Si Latitude et Longitude ne sont pas fournis, les valeurs par d\u00e9faut de la configuration de Home Assistant seront utilis\u00e9es. Une entit\u00e9 sera cr\u00e9\u00e9e pour chaque type de pr\u00e9vision, mais seules celles que vous s\u00e9lectionnez seront activ\u00e9es par d\u00e9faut."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "forecast_types": "Type(s) de pr\u00e9vision",
+ "timestep": "Min. Entre les pr\u00e9visions NowCast"
+ },
+ "description": "Si vous choisissez d'activer l'entit\u00e9 de pr\u00e9vision \u00abnowcast\u00bb, vous pouvez configurer le nombre de minutes entre chaque pr\u00e9vision. Le nombre de pr\u00e9visions fournies d\u00e9pend du nombre de minutes choisies entre les pr\u00e9visions.",
+ "title": "Mettre \u00e0 jour les options de ClimaCell"
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/he.json b/homeassistant/components/climacell/translations/he.json
new file mode 100644
index 00000000000000..81a4b5c1fce612
--- /dev/null
+++ b/homeassistant/components/climacell/translations/he.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+ "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/climacell/translations/hu.json b/homeassistant/components/climacell/translations/hu.json
new file mode 100644
index 00000000000000..6d97a51b530d6c
--- /dev/null
+++ b/homeassistant/components/climacell/translations/hu.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API kulcs",
+ "api_version": "API Verzi\u00f3",
+ "latitude": "Sz\u00e9less\u00e9g",
+ "longitude": "Hossz\u00fas\u00e1g",
+ "name": "N\u00e9v"
+ },
+ "description": "Ha a Sz\u00e9less\u00e9g \u00e9s Hossz\u00fas\u00e1g nincs megadva, akkor a Home Assistant konfigur\u00e1ci\u00f3j\u00e1ban l\u00e9v\u0151 alap\u00e9rtelmezett \u00e9rt\u00e9keket fogjuk haszn\u00e1lni. Minden el\u0151rejelz\u00e9si t\u00edpushoz l\u00e9trej\u00f6n egy entit\u00e1s, de alap\u00e9rtelmez\u00e9s szerint csak az \u00e1ltalad kiv\u00e1lasztottak lesznek enged\u00e9lyezve."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "Friss\u00edtse a ClimaCell be\u00e1ll\u00edt\u00e1sokat"
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/id.json b/homeassistant/components/climacell/translations/id.json
new file mode 100644
index 00000000000000..132f4dcfcb7d3d
--- /dev/null
+++ b/homeassistant/components/climacell/translations/id.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_api_key": "Kunci API tidak valid",
+ "rate_limited": "Saat ini tingkatnya dibatasi, coba lagi nanti.",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kunci API",
+ "latitude": "Lintang",
+ "longitude": "Bujur",
+ "name": "Nama"
+ },
+ "description": "Jika Lintang dan Bujur tidak tersedia, nilai default dalam konfigurasi Home Assistant akan digunakan. Entitas akan dibuat untuk setiap jenis prakiraan tetapi hanya yang Anda pilih yang akan diaktifkan secara default."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "forecast_types": "Jenis Prakiraan",
+ "timestep": "Jarak Interval Prakiraan NowCast dalam Menit"
+ },
+ "description": "Jika Anda memilih untuk mengaktifkan entitas prakiraan 'nowcast', Anda dapat mengonfigurasi jarak interval prakiraan dalam menit. Jumlah prakiraan yang diberikan tergantung pada nilai interval yang dipilih.",
+ "title": "Perbarui Opsi ClimaCell"
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/it.json b/homeassistant/components/climacell/translations/it.json
new file mode 100644
index 00000000000000..bbd8e33d30502d
--- /dev/null
+++ b/homeassistant/components/climacell/translations/it.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_api_key": "Chiave API non valida",
+ "rate_limited": "Al momento la tariffa \u00e8 limitata, riprova pi\u00f9 tardi.",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Chiave API",
+ "api_version": "Versione API",
+ "latitude": "Latitudine",
+ "longitude": "Logitudine",
+ "name": "Nome"
+ },
+ "description": "Se Latitudine e Logitudine non vengono forniti, verranno utilizzati i valori predefiniti nella configurazione di Home Assistant. Verr\u00e0 creata un'entit\u00e0 per ogni tipo di previsione, ma solo quelli selezionati saranno abilitati per impostazione predefinita."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "forecast_types": "Tipo(i) di previsione",
+ "timestep": "Minuti tra le previsioni di NowCast"
+ },
+ "description": "Se scegli di abilitare l'entit\u00e0 di previsione `nowcast`, puoi configurare il numero di minuti tra ogni previsione. Il numero di previsioni fornite dipende dal numero di minuti scelti tra le previsioni.",
+ "title": "Aggiorna le opzioni di ClimaCell"
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/ko.json b/homeassistant/components/climacell/translations/ko.json
new file mode 100644
index 00000000000000..901fd429b1a046
--- /dev/null
+++ b/homeassistant/components/climacell/translations/ko.json
@@ -0,0 +1,35 @@
+{
+ "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",
+ "rate_limited": "\ud604\uc7ac \uc0ac\uc6a9 \ud69f\uc218\ub97c \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API \ud0a4",
+ "api_version": "API \ubc84\uc804",
+ "latitude": "\uc704\ub3c4",
+ "longitude": "\uacbd\ub3c4",
+ "name": "\uc774\ub984"
+ },
+ "description": "\uc704\ub3c4 \ubc0f \uacbd\ub3c4\uac00 \uc81c\uacf5\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 Home Assistant \uad6c\uc131\uc758 \uae30\ubcf8\uac12\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4. \uac01 \uc77c\uae30\uc608\ubcf4 \uc720\ud615\uc5d0 \ub300\ud574 \uad6c\uc131\uc694\uc18c\uac00 \uc0dd\uc131\ub418\uc9c0\ub9cc \uae30\ubcf8\uc801\uc73c\ub85c \uc120\ud0dd\ud55c \uad6c\uc131\uc694\uc18c\ub9cc \ud65c\uc131\ud654\ub429\ub2c8\ub2e4."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "forecast_types": "\uc77c\uae30\uc608\ubcf4 \uc720\ud615",
+ "timestep": "\ub2e8\uae30\uc608\uce21 \uc77c\uae30\uc608\ubcf4 \uac04 \ucd5c\uc18c \uc2dc\uac04"
+ },
+ "description": "`nowcast` \uc77c\uae30\uc608\ubcf4 \uad6c\uc131\uc694\uc18c\ub97c \uc0ac\uc6a9\ud558\ub3c4\ub85d \uc120\ud0dd\ud55c \uacbd\uc6b0 \uac01 \uc77c\uae30\uc608\ubcf4 \uc0ac\uc774\uc758 \uc2dc\uac04(\ubd84)\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc81c\uacf5\ub41c \uc77c\uae30\uc608\ubcf4 \ud69f\uc218\ub294 \uc608\uce21 \uac04 \uc120\ud0dd\ud55c \uc2dc\uac04(\ubd84)\uc5d0 \ub530\ub77c \ub2ec\ub77c\uc9d1\ub2c8\ub2e4.",
+ "title": "ClimaCell \uc635\uc158 \uc5c5\ub370\uc774\ud2b8\ud558\uae30"
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/nl.json b/homeassistant/components/climacell/translations/nl.json
new file mode 100644
index 00000000000000..925300c089dfc0
--- /dev/null
+++ b/homeassistant/components/climacell/translations/nl.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_api_key": "Ongeldige API-sleutel",
+ "rate_limited": "Momenteel is een beperkt aantal aanvragen mogelijk, probeer later opnieuw.",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-sleutel",
+ "api_version": "API-versie",
+ "latitude": "Breedtegraad",
+ "longitude": "Lengtegraad",
+ "name": "Naam"
+ },
+ "description": "Indien Breedtegraad en Lengtegraad niet worden opgegeven, worden de standaardwaarden in de Home Assistant-configuratie gebruikt. Er wordt een entiteit gemaakt voor elk voorspellingstype, maar alleen degenen die u selecteert worden standaard ingeschakeld."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "forecast_types": "Voorspellingstype(n)",
+ "timestep": "Min. Tussen NowCast-voorspellingen"
+ },
+ "description": "Als u ervoor kiest om de `nowcast` voorspellingsentiteit in te schakelen, kan u het aantal minuten tussen elke voorspelling configureren. Het aantal voorspellingen hangt af van het aantal gekozen minuten tussen de voorspellingen.",
+ "title": "Update ClimaCell Opties"
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/no.json b/homeassistant/components/climacell/translations/no.json
new file mode 100644
index 00000000000000..2aad790060717f
--- /dev/null
+++ b/homeassistant/components/climacell/translations/no.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_api_key": "Ugyldig API-n\u00f8kkel",
+ "rate_limited": "Prisen er for \u00f8yeblikket begrenset. Pr\u00f8v igjen senere.",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-n\u00f8kkel",
+ "api_version": "API-versjon",
+ "latitude": "Breddegrad",
+ "longitude": "Lengdegrad",
+ "name": "Navn"
+ },
+ "description": "Hvis Breddegrad og Lengdegrad ikke er oppgitt, vil standardverdiene i Home Assistant-konfigurasjonen bli brukt. Det blir opprettet en enhet for hver prognosetype, men bare de du velger blir aktivert som standard."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "forecast_types": "Prognosetype(r)",
+ "timestep": "Min. mellom NowCast prognoser"
+ },
+ "description": "Hvis du velger \u00e5 aktivere \u00abnowcast\u00bb -varselenheten, kan du konfigurere antall minutter mellom hver prognose. Antall angitte prognoser avhenger av antall minutter som er valgt mellom prognosene.",
+ "title": "Oppdater ClimaCell Alternativer"
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/pl.json b/homeassistant/components/climacell/translations/pl.json
new file mode 100644
index 00000000000000..6c8bad0f57a40b
--- /dev/null
+++ b/homeassistant/components/climacell/translations/pl.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_api_key": "Nieprawid\u0142owy klucz API",
+ "rate_limited": "Przekroczono limit, spr\u00f3buj ponownie p\u00f3\u017aniej.",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Klucz API",
+ "api_version": "Wersja API",
+ "latitude": "Szeroko\u015b\u0107 geograficzna",
+ "longitude": "D\u0142ugo\u015b\u0107 geograficzna",
+ "name": "Nazwa"
+ },
+ "description": "Je\u015bli szeroko\u015b\u0107 i d\u0142ugo\u015b\u0107 geograficzna nie zostan\u0105 podane, zostan\u0105 u\u017cyte domy\u015blne warto\u015bci z konfiguracji Home Assistanta. Zostanie utworzona encja dla ka\u017cdego typu prognozy, ale domy\u015blnie w\u0142\u0105czone bed\u0105 tylko te, kt\u00f3re wybierzesz."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "forecast_types": "Typ(y) prognozy",
+ "timestep": "Minuty pomi\u0119dzy prognozami"
+ },
+ "description": "Je\u015bli zdecydujesz si\u0119 w\u0142\u0105czy\u0107 encj\u0119 prognozy \u201enowcast\u201d, mo\u017cesz skonfigurowa\u0107 liczb\u0119 minut mi\u0119dzy ka\u017cd\u0105 prognoz\u0105. Liczba dostarczonych prognoz zale\u017cy od liczby minut wybranych mi\u0119dzy prognozami.",
+ "title": "Opcje aktualizacji ClimaCell"
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/pt.json b/homeassistant/components/climacell/translations/pt.json
new file mode 100644
index 00000000000000..8e05df2f1b5476
--- /dev/null
+++ b/homeassistant/components/climacell/translations/pt.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Falha na liga\u00e7\u00e3o",
+ "invalid_api_key": "Chave de API inv\u00e1lida",
+ "unknown": "Erro inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Key",
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "name": "Nome"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/ru.json b/homeassistant/components/climacell/translations/ru.json
new file mode 100644
index 00000000000000..7e40c61911204d
--- /dev/null
+++ b/homeassistant/components/climacell/translations/ru.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.",
+ "rate_limited": "\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a, \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."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "api_version": "\u0412\u0435\u0440\u0441\u0438\u044f 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": "\u0415\u0441\u043b\u0438 \u0428\u0438\u0440\u043e\u0442\u0430 \u0438 \u0414\u043e\u043b\u0433\u043e\u0442\u0430 \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u044b, \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0431\u0443\u0434\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 Home Assistant. \u041e\u0431\u044a\u0435\u043a\u0442\u044b \u0431\u0443\u0434\u0443\u0442 \u0441\u043e\u0437\u0434\u0430\u043d\u044b \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430, \u043d\u043e \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0431\u0443\u0434\u0443\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0435 \u0412\u0430\u043c\u0438."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "forecast_types": "\u0422\u0438\u043f(\u044b) \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430",
+ "timestep": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)"
+ },
+ "description": "\u0415\u0441\u043b\u0438 \u0412\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0435\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 'nowcast', \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430.",
+ "title": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 ClimaCell"
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/zh-Hans.json b/homeassistant/components/climacell/translations/zh-Hans.json
new file mode 100644
index 00000000000000..315d060bc69a26
--- /dev/null
+++ b/homeassistant/components/climacell/translations/zh-Hans.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Key",
+ "name": "\u540d\u5b57"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/translations/zh-Hant.json b/homeassistant/components/climacell/translations/zh-Hant.json
new file mode 100644
index 00000000000000..710759b954cbc6
--- /dev/null
+++ b/homeassistant/components/climacell/translations/zh-Hant.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_api_key": "API \u5bc6\u9470\u7121\u6548",
+ "rate_limited": "\u9054\u5230\u9650\u5236\u983b\u7387\u3001\u8acb\u7a0d\u5019\u518d\u8a66\u3002",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API \u5bc6\u9470",
+ "api_version": "API \u7248\u672c",
+ "latitude": "\u7def\u5ea6",
+ "longitude": "\u7d93\u5ea6",
+ "name": "\u540d\u7a31"
+ },
+ "description": "\u5047\u5982\u672a\u63d0\u4f9b\u7def\u5ea6\u8207\u7d93\u5ea6\uff0c\u5c07\u6703\u4f7f\u7528 Home Assistant \u8a2d\u5b9a\u4f5c\u70ba\u9810\u8a2d\u503c\u3002\u6bcf\u4e00\u500b\u9810\u5831\u985e\u578b\u90fd\u6703\u7522\u751f\u4e00\u7d44\u5be6\u9ad4\uff0c\u6216\u8005\u9810\u8a2d\u70ba\u6240\u9078\u64c7\u555f\u7528\u7684\u9810\u5831\u3002"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "forecast_types": "\u9810\u5831\u985e\u578b",
+ "timestep": "NowCast \u9810\u5831\u9593\u9694\u5206\u9418"
+ },
+ "description": "\u5047\u5982\u9078\u64c7\u958b\u555f `nowcast` \u9810\u5831\u5be6\u9ad4\u3001\u5c07\u53ef\u4ee5\u8a2d\u5b9a\u9810\u5831\u983b\u7387\u9593\u9694\u5206\u9418\u6578\u3002\u6839\u64da\u6240\u8f38\u5165\u7684\u9593\u9694\u6642\u9593\u5c07\u6c7a\u5b9a\u9810\u5831\u7684\u6578\u76ee\u3002",
+ "title": "\u66f4\u65b0 ClimaCell \u9078\u9805"
+ }
+ }
+ },
+ "title": "ClimaCell"
+}
\ No newline at end of file
diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py
new file mode 100644
index 00000000000000..012f987171ed33
--- /dev/null
+++ b/homeassistant/components/climacell/weather.py
@@ -0,0 +1,451 @@
+"""Weather component that handles meteorological data for your location."""
+from __future__ import annotations
+
+from datetime import datetime
+import logging
+from typing import Any, Callable
+
+from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST, WeatherCode
+
+from homeassistant.components.weather import (
+ ATTR_FORECAST_CONDITION,
+ ATTR_FORECAST_PRECIPITATION,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY,
+ ATTR_FORECAST_TEMP,
+ ATTR_FORECAST_TEMP_LOW,
+ ATTR_FORECAST_TIME,
+ ATTR_FORECAST_WIND_BEARING,
+ ATTR_FORECAST_WIND_SPEED,
+ WeatherEntity,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ CONF_API_VERSION,
+ LENGTH_FEET,
+ LENGTH_KILOMETERS,
+ LENGTH_METERS,
+ LENGTH_MILES,
+ PRESSURE_HPA,
+ PRESSURE_INHG,
+ TEMP_FAHRENHEIT,
+)
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.sun import is_up
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.util import dt as dt_util
+from homeassistant.util.distance import convert as distance_convert
+from homeassistant.util.pressure import convert as pressure_convert
+
+from . import ClimaCellEntity
+from .const import (
+ CC_ATTR_CONDITION,
+ CC_ATTR_HUMIDITY,
+ CC_ATTR_OZONE,
+ CC_ATTR_PRECIPITATION,
+ CC_ATTR_PRECIPITATION_PROBABILITY,
+ CC_ATTR_PRESSURE,
+ CC_ATTR_TEMPERATURE,
+ CC_ATTR_TEMPERATURE_HIGH,
+ CC_ATTR_TEMPERATURE_LOW,
+ CC_ATTR_TIMESTAMP,
+ CC_ATTR_VISIBILITY,
+ CC_ATTR_WIND_DIRECTION,
+ CC_ATTR_WIND_SPEED,
+ 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_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_SPEED,
+ CLEAR_CONDITIONS,
+ CONDITIONS,
+ CONDITIONS_V3,
+ CONF_TIMESTEP,
+ DOMAIN,
+ MAX_FORECASTS,
+)
+
+# mypy: allow-untyped-defs, no-check-untyped-defs
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType,
+ config_entry: ConfigEntry,
+ async_add_entities: Callable[[list[Entity], bool], None],
+) -> None:
+ """Set up a config entry."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]
+ api_version = config_entry.data[CONF_API_VERSION]
+
+ api_class = ClimaCellV3WeatherEntity if api_version == 3 else ClimaCellWeatherEntity
+ entities = [
+ api_class(config_entry, coordinator, forecast_type, api_version)
+ for forecast_type in [DAILY, HOURLY, NOWCAST]
+ ]
+ async_add_entities(entities)
+
+
+class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
+ """Base ClimaCell weather entity."""
+
+ @staticmethod
+ def _translate_condition(
+ condition: int | None, sun_is_up: bool = True
+ ) -> str | None:
+ """Translate ClimaCell condition into an HA condition."""
+ raise NotImplementedError()
+
+ def _forecast_dict(
+ self,
+ forecast_dt: datetime,
+ use_datetime: bool,
+ condition: 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)
+
+ if self.hass.config.units.is_metric:
+ if precipitation:
+ precipitation = (
+ distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS)
+ * 1000
+ )
+ if wind_speed:
+ wind_speed = distance_convert(
+ wind_speed, LENGTH_MILES, LENGTH_KILOMETERS
+ )
+
+ data = {
+ ATTR_FORECAST_TIME: forecast_dt.isoformat(),
+ ATTR_FORECAST_CONDITION: translated_condition,
+ ATTR_FORECAST_PRECIPITATION: precipitation,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability,
+ ATTR_FORECAST_TEMP: temp,
+ ATTR_FORECAST_TEMP_LOW: temp_low,
+ ATTR_FORECAST_WIND_BEARING: wind_direction,
+ ATTR_FORECAST_WIND_SPEED: wind_speed,
+ }
+
+ return {k: v for k, v in data.items() if v is not None}
+
+
+class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity):
+ """Entity that talks to ClimaCell v4 API to retrieve weather data."""
+
+ @staticmethod
+ def _translate_condition(
+ condition: int | None, sun_is_up: bool = True
+ ) -> str | None:
+ """Translate ClimaCell condition into an HA condition."""
+ if condition is None:
+ return None
+ # We won't guard here, instead we will fail hard
+ condition = WeatherCode(condition)
+ if condition in (WeatherCode.CLEAR, WeatherCode.MOSTLY_CLEAR):
+ if sun_is_up:
+ return CLEAR_CONDITIONS["day"]
+ return CLEAR_CONDITIONS["night"]
+ return CONDITIONS[condition]
+
+ def _get_current_property(self, property_name: str) -> int | str | float | None:
+ """Get property from current conditions."""
+ return self.coordinator.data.get(CURRENT, {}).get(property_name)
+
+ @property
+ def temperature(self):
+ """Return the platform temperature."""
+ return self._get_current_property(CC_ATTR_TEMPERATURE)
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement."""
+ return TEMP_FAHRENHEIT
+
+ @property
+ def pressure(self):
+ """Return the pressure."""
+ pressure = self._get_current_property(CC_ATTR_PRESSURE)
+ if self.hass.config.units.is_metric and pressure:
+ return pressure_convert(pressure, PRESSURE_INHG, PRESSURE_HPA)
+ return pressure
+
+ @property
+ def humidity(self):
+ """Return the humidity."""
+ return self._get_current_property(CC_ATTR_HUMIDITY)
+
+ @property
+ def wind_speed(self):
+ """Return the wind speed."""
+ wind_speed = self._get_current_property(CC_ATTR_WIND_SPEED)
+ if self.hass.config.units.is_metric and wind_speed:
+ return distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS)
+ return wind_speed
+
+ @property
+ def wind_bearing(self):
+ """Return the wind bearing."""
+ return self._get_current_property(CC_ATTR_WIND_DIRECTION)
+
+ @property
+ def ozone(self):
+ """Return the O3 (ozone) level."""
+ return self._get_current_property(CC_ATTR_OZONE)
+
+ @property
+ def condition(self):
+ """Return the condition."""
+ return self._translate_condition(
+ self._get_current_property(CC_ATTR_CONDITION),
+ is_up(self.hass),
+ )
+
+ @property
+ def visibility(self):
+ """Return the visibility."""
+ visibility = self._get_current_property(CC_ATTR_VISIBILITY)
+ if self.hass.config.units.is_metric and visibility:
+ return distance_convert(visibility, LENGTH_MILES, LENGTH_KILOMETERS)
+ return 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 = []
+ max_forecasts = MAX_FORECASTS[self.forecast_type]
+ forecast_count = 0
+
+ # 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(forecast[CC_ATTR_TIMESTAMP])
+
+ # Throw out past data
+ if forecast_dt.date() < dt_util.utcnow().date():
+ continue
+
+ values = forecast["values"]
+ use_datetime = True
+
+ condition = values.get(CC_ATTR_CONDITION)
+ precipitation = values.get(CC_ATTR_PRECIPITATION)
+ precipitation_probability = values.get(CC_ATTR_PRECIPITATION_PROBABILITY)
+
+ temp = values.get(CC_ATTR_TEMPERATURE_HIGH)
+ temp_low = values.get(CC_ATTR_TEMPERATURE_LOW)
+ wind_direction = values.get(CC_ATTR_WIND_DIRECTION)
+ wind_speed = values.get(CC_ATTR_WIND_SPEED)
+
+ if self.forecast_type == DAILY:
+ use_datetime = False
+ if precipitation:
+ precipitation = precipitation * 24
+ elif self.forecast_type == NOWCAST:
+ # Precipitation is forecasted in CONF_TIMESTEP increments but in a
+ # per hour rate, so value needs to be converted to an amount.
+ if precipitation:
+ 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,
+ )
+ )
+
+ forecast_count += 1
+ if forecast_count == max_forecasts:
+ break
+
+ return forecasts
+
+
+class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity):
+ """Entity that talks to ClimaCell v3 API to retrieve weather data."""
+
+ @staticmethod
+ def _translate_condition(
+ condition: str | None, sun_is_up: bool = True
+ ) -> str | None:
+ """Translate ClimaCell condition into an HA condition."""
+ if not condition:
+ return None
+ if "clear" in condition.lower():
+ if sun_is_up:
+ return CLEAR_CONDITIONS["day"]
+ return CLEAR_CONDITIONS["night"]
+ return CONDITIONS_V3[condition]
+
+ @property
+ def temperature(self):
+ """Return the platform temperature."""
+ return self._get_cc_value(
+ self.coordinator.data[CURRENT], CC_V3_ATTR_TEMPERATURE
+ )
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement."""
+ return TEMP_FAHRENHEIT
+
+ @property
+ def pressure(self):
+ """Return the pressure."""
+ pressure = self._get_cc_value(
+ self.coordinator.data[CURRENT], CC_V3_ATTR_PRESSURE
+ )
+ if self.hass.config.units.is_metric and pressure:
+ return pressure_convert(pressure, PRESSURE_INHG, PRESSURE_HPA)
+ return pressure
+
+ @property
+ def humidity(self):
+ """Return the humidity."""
+ return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_HUMIDITY)
+
+ @property
+ def wind_speed(self):
+ """Return the wind speed."""
+ wind_speed = self._get_cc_value(
+ self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_SPEED
+ )
+ if self.hass.config.units.is_metric and wind_speed:
+ return distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS)
+ return 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 visibility."""
+ visibility = self._get_cc_value(
+ self.coordinator.data[CURRENT], CC_V3_ATTR_VISIBILITY
+ )
+ if self.hass.config.units.is_metric and visibility:
+ return distance_convert(visibility, LENGTH_MILES, LENGTH_KILOMETERS)
+ return 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 32dfaa0e8fb4a5..30842f1fe23cc7 100644
--- a/homeassistant/components/climate/__init__.py
+++ b/homeassistant/components/climate/__init__.py
@@ -1,9 +1,11 @@
"""Provides functionality to interact with climate devices."""
+from __future__ import annotations
+
from abc import abstractmethod
from datetime import timedelta
import functools as ft
import logging
-from typing import Any, Dict, List, Optional
+from typing import Any, final
import voluptuous as vol
@@ -17,6 +19,7 @@
STATE_ON,
TEMP_CELSIUS,
)
+from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
@@ -26,7 +29,7 @@
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.temperature import display_temp as show_temp
-from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType
+from homeassistant.helpers.typing import ConfigType, ServiceDataType
from homeassistant.util.temperature import convert as convert_temperature
from .const import (
@@ -100,7 +103,7 @@
)
-async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up climate entities."""
component = hass.data[DOMAIN] = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
@@ -154,18 +157,18 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
return True
-async def async_setup_entry(hass: HomeAssistantType, entry):
+async def async_setup_entry(hass: HomeAssistant, entry):
"""Set up a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)
-async def async_unload_entry(hass: HomeAssistantType, entry):
+async def async_unload_entry(hass: HomeAssistant, entry):
"""Unload a config entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)
class ClimateEntity(Entity):
- """Representation of a climate entity."""
+ """Base class for climate entities."""
@property
def state(self) -> str:
@@ -180,7 +183,7 @@ def precision(self) -> float:
return PRECISION_WHOLE
@property
- def capability_attributes(self) -> Optional[Dict[str, Any]]:
+ def capability_attributes(self) -> dict[str, Any] | None:
"""Return the capability attributes."""
supported_features = self.supported_features
data = {
@@ -211,8 +214,9 @@ def capability_attributes(self) -> Optional[Dict[str, Any]]:
return data
+ @final
@property
- def state_attributes(self) -> Dict[str, Any]:
+ def state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
supported_features = self.supported_features
data = {
@@ -275,12 +279,12 @@ def temperature_unit(self) -> str:
raise NotImplementedError()
@property
- def current_humidity(self) -> Optional[int]:
+ def current_humidity(self) -> int | None:
"""Return the current humidity."""
return None
@property
- def target_humidity(self) -> Optional[int]:
+ def target_humidity(self) -> int | None:
"""Return the humidity we try to reach."""
return None
@@ -294,14 +298,14 @@ def hvac_mode(self) -> str:
@property
@abstractmethod
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes.
Need to be a subset of HVAC_MODES.
"""
@property
- def hvac_action(self) -> Optional[str]:
+ def hvac_action(self) -> str | None:
"""Return the current running hvac operation if supported.
Need to be one of CURRENT_HVAC_*.
@@ -309,22 +313,22 @@ def hvac_action(self) -> Optional[str]:
return None
@property
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
return None
@property
- def target_temperature(self) -> Optional[float]:
+ def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
return None
@property
- def target_temperature_step(self) -> Optional[float]:
+ def target_temperature_step(self) -> float | None:
"""Return the supported step of target temperature."""
return None
@property
- def target_temperature_high(self) -> Optional[float]:
+ def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach.
Requires SUPPORT_TARGET_TEMPERATURE_RANGE.
@@ -332,7 +336,7 @@ def target_temperature_high(self) -> Optional[float]:
raise NotImplementedError
@property
- def target_temperature_low(self) -> Optional[float]:
+ def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach.
Requires SUPPORT_TARGET_TEMPERATURE_RANGE.
@@ -340,7 +344,7 @@ def target_temperature_low(self) -> Optional[float]:
raise NotImplementedError
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp.
Requires SUPPORT_PRESET_MODE.
@@ -348,7 +352,7 @@ def preset_mode(self) -> Optional[str]:
raise NotImplementedError
@property
- def preset_modes(self) -> Optional[List[str]]:
+ def preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes.
Requires SUPPORT_PRESET_MODE.
@@ -356,7 +360,7 @@ def preset_modes(self) -> Optional[List[str]]:
raise NotImplementedError
@property
- def is_aux_heat(self) -> Optional[bool]:
+ def is_aux_heat(self) -> bool | None:
"""Return true if aux heater.
Requires SUPPORT_AUX_HEAT.
@@ -364,7 +368,7 @@ def is_aux_heat(self) -> Optional[bool]:
raise NotImplementedError
@property
- def fan_mode(self) -> Optional[str]:
+ def fan_mode(self) -> str | None:
"""Return the fan setting.
Requires SUPPORT_FAN_MODE.
@@ -372,7 +376,7 @@ def fan_mode(self) -> Optional[str]:
raise NotImplementedError
@property
- def fan_modes(self) -> Optional[List[str]]:
+ def fan_modes(self) -> list[str] | None:
"""Return the list of available fan modes.
Requires SUPPORT_FAN_MODE.
@@ -380,7 +384,7 @@ def fan_modes(self) -> Optional[List[str]]:
raise NotImplementedError
@property
- def swing_mode(self) -> Optional[str]:
+ def swing_mode(self) -> str | None:
"""Return the swing setting.
Requires SUPPORT_SWING_MODE.
@@ -388,7 +392,7 @@ def swing_mode(self) -> Optional[str]:
raise NotImplementedError
@property
- def swing_modes(self) -> Optional[List[str]]:
+ def swing_modes(self) -> list[str] | None:
"""Return the list of available swing modes.
Requires SUPPORT_SWING_MODE.
diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py
index 3f2b8dc23f2c89..02474a47f96ea8 100644
--- a/homeassistant/components/climate/device_action.py
+++ b/homeassistant/components/climate/device_action.py
@@ -1,5 +1,5 @@
"""Provides device automations for Climate."""
-from typing import List, Optional
+from __future__ import annotations
import voluptuous as vol
@@ -38,7 +38,7 @@
ACTION_SCHEMA = vol.Any(SET_HVAC_MODE_SCHEMA, SET_PRESET_MODE_SCHEMA)
-async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device actions for Climate devices."""
registry = await entity_registry.async_get_registry(hass)
actions = []
@@ -76,11 +76,9 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
async def async_call_action_from_config(
- hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context]
+ hass: HomeAssistant, config: dict, variables: dict, context: Context | None
) -> None:
"""Execute a device action."""
- config = ACTION_SCHEMA(config)
-
service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]}
if config[CONF_TYPE] == "set_hvac_mode":
diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py
index 423efdf8196c4d..d20c202e93bc67 100644
--- a/homeassistant/components/climate/device_condition.py
+++ b/homeassistant/components/climate/device_condition.py
@@ -1,5 +1,5 @@
"""Provide the device automations for Climate."""
-from typing import Dict, List
+from __future__ import annotations
import voluptuous as vol
@@ -42,7 +42,7 @@
async def async_get_conditions(
hass: HomeAssistant, device_id: str
-) -> List[Dict[str, str]]:
+) -> list[dict[str, str]]:
"""List device conditions for Climate devices."""
registry = await entity_registry.async_get_registry(hass)
conditions = []
@@ -119,6 +119,6 @@ async def async_get_condition_capabilities(hass, config):
else:
preset_modes = []
- fields[vol.Required(const.ATTR_PRESET_MODES)] = vol.In(preset_modes)
+ fields[vol.Required(const.ATTR_PRESET_MODE)] = vol.In(preset_modes)
return {"extra_fields": vol.Schema(fields)}
diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py
index 84a7c35162a792..df925463d4c071 100644
--- a/homeassistant/components/climate/device_trigger.py
+++ b/homeassistant/components/climate/device_trigger.py
@@ -1,5 +1,5 @@
"""Provides device automations for Climate."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -58,7 +58,7 @@
TRIGGER_SCHEMA = vol.Any(HVAC_MODE_TRIGGER_SCHEMA, CURRENT_TRIGGER_SCHEMA)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers for Climate devices."""
registry = await entity_registry.async_get_registry(hass)
triggers = []
@@ -71,12 +71,16 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
state = hass.states.get(entry.entity_id)
# Add triggers for each entity that belongs to this integration
+ base_trigger = {
+ CONF_PLATFORM: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+
triggers.append(
{
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
+ **base_trigger,
CONF_TYPE: "hvac_mode_changed",
}
)
@@ -84,10 +88,7 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
if state and const.ATTR_CURRENT_TEMPERATURE in state.attributes:
triggers.append(
{
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
+ **base_trigger,
CONF_TYPE: "current_temperature_changed",
}
)
@@ -95,10 +96,7 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
if state and const.ATTR_CURRENT_HUMIDITY in state.attributes:
triggers.append(
{
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
+ **base_trigger,
CONF_TYPE: "current_humidity_changed",
}
)
@@ -113,7 +111,6 @@ async def async_attach_trigger(
automation_info: dict,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
- config = TRIGGER_SCHEMA(config)
trigger_type = config[CONF_TYPE]
if trigger_type == "hvac_mode_changed":
diff --git a/homeassistant/components/climate/group.py b/homeassistant/components/climate/group.py
index 87674da414bee3..3603e37d9703d6 100644
--- a/homeassistant/components/climate/group.py
+++ b/homeassistant/components/climate/group.py
@@ -3,15 +3,14 @@
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OFF
-from homeassistant.core import callback
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import HomeAssistant, callback
from .const import HVAC_MODE_OFF, HVAC_MODES
@callback
def async_describe_on_off_states(
- hass: HomeAssistantType, registry: GroupIntegrationRegistry
+ hass: HomeAssistant, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states(
diff --git a/homeassistant/components/climate/reproduce_state.py b/homeassistant/components/climate/reproduce_state.py
index 1217d5fde4cd08..be52138e3e56ac 100644
--- a/homeassistant/components/climate/reproduce_state.py
+++ b/homeassistant/components/climate/reproduce_state.py
@@ -1,10 +1,11 @@
"""Module that groups code required to handle state restore for component."""
+from __future__ import annotations
+
import asyncio
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import ATTR_TEMPERATURE
-from homeassistant.core import Context, State
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import Context, HomeAssistant, State
from .const import (
ATTR_AUX_HEAT,
@@ -26,11 +27,11 @@
async def _async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce component states."""
@@ -73,11 +74,11 @@ async def call_service(service: str, keys: Iterable, data=None):
async def async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce component states."""
await asyncio.gather(
diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml
index 999640812779d3..ca88896c6c251b 100644
--- a/homeassistant/components/climate/services.yaml
+++ b/homeassistant/components/climate/services.yaml
@@ -1,93 +1,153 @@
# Describes the format for available climate services
set_aux_heat:
+ name: Turn on/off auxiliary heater
description: Turn auxiliary heater on/off for climate device.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to change.
- example: "climate.kitchen"
aux_heat:
- description: New value of axillary heater.
+ name: Auxiliary heating
+ description: New value of auxiliary heater.
+ required: true
example: true
+ selector:
+ boolean:
set_preset_mode:
+ name: Set preset mode
description: Set preset mode for climate device.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to change.
- example: "climate.kitchen"
preset_mode:
- description: New value of preset mode
+ name: Preset mode
+ description: New value of preset mode.
+ required: true
example: "away"
+ selector:
+ text:
set_temperature:
+ name: Set temperature
description: Set target temperature of climate device.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to change.
- example: "climate.kitchen"
temperature:
+ name: Temperature
description: New target temperature for HVAC.
example: 25
+ selector:
+ number:
+ min: 0
+ max: 250
+ step: 0.1
+ mode: box
target_temp_high:
+ name: Target temperature high
description: New target high temperature for HVAC.
+ advanced: true
example: 26
+ selector:
+ number:
+ min: 0
+ max: 250
+ step: 0.1
+ mode: box
target_temp_low:
+ name: Target temperature low
description: New target low temperature for HVAC.
+ advanced: true
example: 20
+ selector:
+ number:
+ min: 0
+ max: 250
+ step: 0.1
+ mode: box
hvac_mode:
+ name: HVAC mode
description: HVAC operation mode to set temperature to.
example: "heat"
+ selector:
+ select:
+ options:
+ - "off"
+ - "auto"
+ - "cool"
+ - "dry"
+ - "fan_only"
+ - "heat_cool"
+ - "heat"
set_humidity:
+ name: Set target humidity
description: Set target humidity of climate device.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to change.
- example: "climate.kitchen"
humidity:
+ name: Humidity
description: New target humidity for climate device.
+ required: true
example: 60
+ selector:
+ number:
+ min: 30
+ max: 99
+ step: 1
+ unit_of_measurement: "%"
+ mode: slider
set_fan_mode:
+ name: Set fan mode
description: Set fan operation for climate device.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to change.
- example: "climate.nest"
fan_mode:
+ name: Fan mode
description: New value of fan mode.
- example: On Low
+ required: true
+ example: "low"
+ selector:
+ text:
set_hvac_mode:
+ name: Set HVAC mode
description: Set HVAC operation mode for climate device.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to change.
- example: "climate.nest"
hvac_mode:
+ name: HVAC mode
description: New value of operation mode.
- example: heat
+ example: "heat"
+ selector:
+ select:
+ options:
+ - "off"
+ - "auto"
+ - "cool"
+ - "dry"
+ - "fan_only"
+ - "heat_cool"
+ - "heat"
set_swing_mode:
+ name: Set swing mode
description: Set swing operation for climate device.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to change.
- example: "climate.nest"
swing_mode:
+ name: Swing mode
description: New value of swing mode.
+ required: true
+ example: "horizontal"
+ selector:
+ text:
turn_on:
+ name: Turn on
description: Turn climate device on.
- fields:
- entity_id:
- description: Name(s) of entities to change.
- example: "climate.kitchen"
+ target:
turn_off:
+ name: Turn off
description: Turn climate device off.
- fields:
- entity_id:
- description: Name(s) of entities to change.
- example: "climate.kitchen"
+ target:
diff --git a/homeassistant/components/climate/translations/cs.json b/homeassistant/components/climate/translations/cs.json
index caeb255264e35a..3740a7b423eec8 100644
--- a/homeassistant/components/climate/translations/cs.json
+++ b/homeassistant/components/climate/translations/cs.json
@@ -16,7 +16,7 @@
},
"state": {
"_": {
- "auto": "Automatika",
+ "auto": "Auto",
"cool": "Chlazen\u00ed",
"dry": "Vysou\u0161en\u00ed",
"fan_only": "Pouze ventil\u00e1tor",
diff --git a/homeassistant/components/climate/translations/id.json b/homeassistant/components/climate/translations/id.json
index 4f1ec02379b4f0..bdae7d60067525 100644
--- a/homeassistant/components/climate/translations/id.json
+++ b/homeassistant/components/climate/translations/id.json
@@ -1,13 +1,28 @@
{
+ "device_automation": {
+ "action_type": {
+ "set_hvac_mode": "Ubah mode HVAC di {entity_name}",
+ "set_preset_mode": "Ubah prasetel di {entity_name}"
+ },
+ "condition_type": {
+ "is_hvac_mode": "{entity_name} disetel ke mode HVAC tertentu",
+ "is_preset_mode": "{entity_name} disetel ke mode prasetel tertentu"
+ },
+ "trigger_type": {
+ "current_humidity_changed": "Kelembaban terukur {entity_name} berubah",
+ "current_temperature_changed": "Suhu terukur {entity_name} berubah",
+ "hvac_mode_changed": "Mode HVAC {entity_name} berubah"
+ }
+ },
"state": {
"_": {
- "auto": "Auto",
- "cool": "Sejuk",
+ "auto": "Otomatis",
+ "cool": "Dingin",
"dry": "Kering",
"fan_only": "Hanya kipas",
"heat": "Panas",
"heat_cool": "Panas/Dingin",
- "off": "Off"
+ "off": "Mati"
}
},
"title": "Cuaca"
diff --git a/homeassistant/components/climate/translations/ko.json b/homeassistant/components/climate/translations/ko.json
index 0923d16604055a..7c7342ef95ccb2 100644
--- a/homeassistant/components/climate/translations/ko.json
+++ b/homeassistant/components/climate/translations/ko.json
@@ -1,17 +1,17 @@
{
"device_automation": {
"action_type": {
- "set_hvac_mode": "{entity_name} \uc758 HVAC \ubaa8\ub4dc \ubcc0\uacbd",
- "set_preset_mode": "{entity_name} \uc758 \ud504\ub9ac\uc14b \ubcc0\uacbd"
+ "set_hvac_mode": "{entity_name}\uc758 HVAC \ubaa8\ub4dc \ubcc0\uacbd\ud558\uae30",
+ "set_preset_mode": "{entity_name}\uc758 \ud504\ub9ac\uc14b \ubcc0\uacbd\ud558\uae30"
},
"condition_type": {
- "is_hvac_mode": "{entity_name} \uc774(\uac00) \ud2b9\uc815 HVAC \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74",
- "is_preset_mode": "{entity_name} \uc774(\uac00) \ud2b9\uc815 \ud504\ub9ac\uc14b \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74"
+ "is_hvac_mode": "{entity_name}\uc774(\uac00) \ud2b9\uc815 HVAC \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74",
+ "is_preset_mode": "{entity_name}\uc774(\uac00) \ud2b9\uc815 \ud504\ub9ac\uc14b \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74"
},
"trigger_type": {
- "current_humidity_changed": "{entity_name} \uc774(\uac00) \uc2b5\ub3c4 \ubcc0\ud654\ub97c \uac10\uc9c0\ud560 \ub54c",
- "current_temperature_changed": "{entity_name} \uc774(\uac00) \uc628\ub3c4 \ubcc0\ud654\ub97c \uac10\uc9c0\ud560 \ub54c",
- "hvac_mode_changed": "{entity_name} HVAC \ubaa8\ub4dc\uac00 \ubcc0\uacbd\ub420 \ub54c"
+ "current_humidity_changed": "{entity_name}\uc774(\uac00) \uc2b5\ub3c4 \ubcc0\ud654\ub97c \uac10\uc9c0\ud588\uc744 \ub54c",
+ "current_temperature_changed": "{entity_name}\uc774(\uac00) \uc628\ub3c4 \ubcc0\ud654\ub97c \uac10\uc9c0\ud588\uc744 \ub54c",
+ "hvac_mode_changed": "{entity_name}\uc758 HVAC \ubaa8\ub4dc\uac00 \ubcc0\uacbd\ub418\uc5c8\uc744 \ub54c"
}
},
"state": {
diff --git a/homeassistant/components/climate/translations/tr.json b/homeassistant/components/climate/translations/tr.json
index 0b027dbd87faa2..201fec4c4b648d 100644
--- a/homeassistant/components/climate/translations/tr.json
+++ b/homeassistant/components/climate/translations/tr.json
@@ -1,4 +1,10 @@
{
+ "device_automation": {
+ "action_type": {
+ "set_hvac_mode": "{entity_name} \u00fczerinde HVAC modunu de\u011fi\u015ftir",
+ "set_preset_mode": "{entity_name} \u00fczerindeki \u00f6n ayar\u0131 de\u011fi\u015ftir"
+ }
+ },
"state": {
"_": {
"auto": "Otomatik",
diff --git a/homeassistant/components/climate/translations/uk.json b/homeassistant/components/climate/translations/uk.json
index 8d636c386e5479..de6baff021cec4 100644
--- a/homeassistant/components/climate/translations/uk.json
+++ b/homeassistant/components/climate/translations/uk.json
@@ -1,17 +1,17 @@
{
"device_automation": {
"action_type": {
- "set_hvac_mode": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438 \u0440\u0435\u0436\u0438\u043c HVAC \u043d\u0430 {entity_name}",
- "set_preset_mode": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438 \u043f\u043e\u043f\u0435\u0440\u0435\u0434\u043d\u044c\u043e \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043d\u0430 {entity_name}"
+ "set_hvac_mode": "{entity_name}: \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u0440\u043e\u0431\u043e\u0442\u0438",
+ "set_preset_mode": "{entity_name}: \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u043f\u0440\u0435\u0441\u0435\u0442"
},
"condition_type": {
- "is_hvac_mode": "{entity_name} \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u0432 \u043f\u0435\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c HVAC",
+ "is_hvac_mode": "{entity_name} \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432 \u0437\u0430\u0434\u0430\u043d\u043e\u043c\u0443 \u0440\u0435\u0436\u0438\u043c\u0456 \u0440\u043e\u0431\u043e\u0442\u0438",
"is_preset_mode": "{entity_name} \u043d\u0430\u0441\u0442\u0440\u043e\u0454\u043d\u043e \u043d\u0430 \u043f\u0435\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c"
},
"trigger_type": {
- "current_humidity_changed": "{entity_name} \u0432\u0438\u043c\u0456\u0440\u044f\u043d\u0430 \u0432\u043e\u043b\u043e\u0433\u0456\u0441\u0442\u044c \u0437\u043c\u0456\u043d\u0435\u043d\u0430",
- "current_temperature_changed": "{entity_name} \u0432\u0438\u043c\u0456\u0440\u044f\u043d\u0443 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0443 \u0437\u043c\u0456\u043d\u0435\u043d\u043e",
- "hvac_mode_changed": "{entity_name} \u0420\u0435\u0436\u0438\u043c HVAC \u0437\u043c\u0456\u043d\u0435\u043d\u043e"
+ "current_humidity_changed": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0432\u0438\u043c\u0456\u0440\u044f\u043d\u043e\u0457 \u0432\u043e\u043b\u043e\u0433\u043e\u0441\u0442\u0456",
+ "current_temperature_changed": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0432\u0438\u043c\u0456\u0440\u044f\u043d\u043e\u0457 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438",
+ "hvac_mode_changed": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0440\u0435\u0436\u0438\u043c \u0440\u043e\u0431\u043e\u0442\u0438"
}
},
"state": {
@@ -21,7 +21,7 @@
"dry": "\u041e\u0441\u0443\u0448\u0435\u043d\u043d\u044f",
"fan_only": "\u041b\u0438\u0448\u0435 \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0442\u043e\u0440",
"heat": "\u041d\u0430\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f",
- "heat_cool": "\u041d\u0430\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f/\u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f",
+ "heat_cool": "\u041d\u0430\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f / \u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f",
"off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e"
}
},
diff --git a/homeassistant/components/climate/translations/zh-Hans.json b/homeassistant/components/climate/translations/zh-Hans.json
index 9927cd679ae259..a93125525e1feb 100644
--- a/homeassistant/components/climate/translations/zh-Hans.json
+++ b/homeassistant/components/climate/translations/zh-Hans.json
@@ -5,8 +5,8 @@
"set_preset_mode": "\u66f4\u6539 {entity_name} \u9884\u8bbe\u6a21\u5f0f"
},
"condition_type": {
- "is_hvac_mode": "{entity_name} \u88ab\u8bbe\u4e3a\u6307\u5b9a\u7684\u7a7a\u8c03\u6a21\u5f0f",
- "is_preset_mode": "{entity_name} \u88ab\u8bbe\u4e3a\u6307\u5b9a\u7684\u9884\u8bbe\u6a21\u5f0f"
+ "is_hvac_mode": "{entity_name} \u5df2\u8bbe\u4e3a\u6307\u5b9a\u7684\u7a7a\u8c03\u6a21\u5f0f",
+ "is_preset_mode": "{entity_name} \u5df2\u8bbe\u4e3a\u6307\u5b9a\u7684\u9884\u8bbe\u6a21\u5f0f"
},
"trigger_type": {
"current_humidity_changed": "{entity_name} \u6d4b\u91cf\u7684\u5ba4\u5185\u6e7f\u5ea6\u53d8\u5316",
diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py
index b1ad55c0b04a70..038bc227fcdec0 100644
--- a/homeassistant/components/cloud/__init__.py
+++ b/homeassistant/components/cloud/__init__.py
@@ -5,6 +5,7 @@
from homeassistant.components.alexa import const as alexa_const
from homeassistant.components.google_assistant import const as ga_c
from homeassistant.const import (
+ CONF_DESCRIPTION,
CONF_MODE,
CONF_NAME,
CONF_REGION,
@@ -49,7 +50,7 @@
ALEXA_ENTITY_SCHEMA = vol.Schema(
{
- vol.Optional(alexa_const.CONF_DESCRIPTION): cv.string,
+ vol.Optional(CONF_DESCRIPTION): cv.string,
vol.Optional(alexa_const.CONF_DISPLAY_CATEGORIES): cv.string,
vol.Optional(CONF_NAME): cv.string,
}
diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py
index 7abbefe85ff5bb..138b2db0b8c756 100644
--- a/homeassistant/components/cloud/alexa_config.py
+++ b/homeassistant/components/cloud/alexa_config.py
@@ -1,5 +1,6 @@
"""Alexa configuration for Home Assistant Cloud."""
import asyncio
+from contextlib import suppress
from datetime import timedelta
import logging
@@ -62,7 +63,11 @@ def __init__(
@property
def enabled(self):
"""Return if Alexa is enabled."""
- return self._prefs.alexa_enabled
+ return (
+ self._cloud.is_logged_in
+ and not self._cloud.subscription_expired
+ and self._prefs.alexa_enabled
+ )
@property
def supports_auth(self):
@@ -318,7 +323,5 @@ async def _handle_entity_registry_updated(self, event):
if "old_entity_id" in event.data:
to_remove.append(event.data["old_entity_id"])
- try:
+ with suppress(alexa_errors.NoTokenAvailable):
await self._sync_helper(to_update, to_remove)
- except alexa_errors.NoTokenAvailable:
- pass
diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py
index 155a39e49b6aa4..f451a4faddb10f 100644
--- a/homeassistant/components/cloud/client.py
+++ b/homeassistant/components/cloud/client.py
@@ -1,8 +1,10 @@
"""Interface implementation for cloud client."""
+from __future__ import annotations
+
import asyncio
import logging
from pathlib import Path
-from typing import Any, Dict
+from typing import Any
import aiohttp
from hass_nabucasa.client import CloudClient as Interface
@@ -13,10 +15,9 @@
)
from homeassistant.components.google_assistant import const as gc, smart_home as ga
from homeassistant.const import HTTP_OK
-from homeassistant.core import Context, callback
+from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_call_later
-from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.aiohttp import MockRequest
from . import alexa_config, google_config, utils
@@ -29,11 +30,11 @@ class CloudClient(Interface):
def __init__(
self,
- hass: HomeAssistantType,
+ hass: HomeAssistant,
prefs: CloudPreferences,
websession: aiohttp.ClientSession,
- alexa_user_config: Dict[str, Any],
- google_user_config: Dict[str, Any],
+ alexa_user_config: dict[str, Any],
+ google_user_config: dict[str, Any],
):
"""Initialize client interface to Cloud."""
self._hass = hass
@@ -70,7 +71,7 @@ def aiohttp_runner(self) -> aiohttp.web.AppRunner:
return self._hass.http.runner
@property
- def cloudhooks(self) -> Dict[str, Dict[str, str]]:
+ def cloudhooks(self) -> dict[str, dict[str, str]]:
"""Return list of cloudhooks."""
return self._prefs.cloudhooks
@@ -108,7 +109,7 @@ async def get_google_config(self) -> google_config.CloudGoogleConfig:
async def logged_in(self) -> None:
"""When user logs in."""
- await self.prefs.async_set_username(self.cloud.username)
+ is_new_user = await self.prefs.async_set_username(self.cloud.username)
async def enable_alexa(_):
"""Enable Alexa."""
@@ -134,6 +135,9 @@ async def enable_google(_):
if gconf.should_report_state:
gconf.async_enable_report_state()
+ if is_new_user:
+ await gconf.async_sync_entities(gconf.agent_user_id)
+
tasks = []
if self._prefs.alexa_enabled and self._prefs.alexa_report_state:
@@ -164,7 +168,7 @@ def dispatcher_message(self, identifier: str, data: Any = None) -> None:
if identifier.startswith("remote_"):
async_dispatcher_send(self._hass, DISPATCHER_REMOTE_UPDATE, data)
- async def async_alexa_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]:
+ async def async_alexa_message(self, payload: dict[Any, Any]) -> dict[Any, Any]:
"""Process cloud alexa message to client."""
cloud_user = await self._prefs.get_cloud_user()
aconfig = await self.get_alexa_config()
@@ -176,7 +180,7 @@ async def async_alexa_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]:
enabled=self._prefs.alexa_enabled,
)
- async def async_google_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]:
+ async def async_google_message(self, payload: dict[Any, Any]) -> dict[Any, Any]:
"""Process cloud google message to client."""
if not self._prefs.google_enabled:
return ga.turned_off_response(payload)
@@ -187,7 +191,7 @@ async def async_google_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]:
self._hass, gconf, gconf.cloud_user, payload, gc.SOURCE_CLOUD
)
- async def async_webhook_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]:
+ async def async_webhook_message(self, payload: dict[Any, Any]) -> dict[Any, Any]:
"""Process cloud webhook message to client."""
cloudhook_id = payload["cloudhook_id"]
@@ -221,6 +225,6 @@ async def async_webhook_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]
"headers": {"Content-Type": response.content_type},
}
- async def async_cloudhooks_update(self, data: Dict[str, Dict[str, str]]) -> None:
+ async def async_cloudhooks_update(self, data: dict[str, dict[str, str]]) -> None:
"""Update local list of cloudhooks."""
await self._prefs.async_update(cloudhooks=data)
diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py
index 2ac0bc40252978..62ca1b15a71546 100644
--- a/homeassistant/components/cloud/google_config.py
+++ b/homeassistant/components/cloud/google_config.py
@@ -2,15 +2,11 @@
import asyncio
import logging
-from hass_nabucasa import cloud_api
+from hass_nabucasa import Cloud, cloud_api
from hass_nabucasa.google_report_state import ErrorResponse
from homeassistant.components.google_assistant.helpers import AbstractConfig
-from homeassistant.const import (
- CLOUD_NEVER_EXPOSED_ENTITIES,
- EVENT_HOMEASSISTANT_STARTED,
- HTTP_OK,
-)
+from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_OK
from homeassistant.core import CoreState, split_entity_id
from homeassistant.helpers import entity_registry
@@ -28,7 +24,9 @@
class CloudGoogleConfig(AbstractConfig):
"""HA Cloud Configuration for Google Assistant."""
- def __init__(self, hass, config, cloud_user: str, prefs: CloudPreferences, cloud):
+ def __init__(
+ self, hass, config, cloud_user: str, prefs: CloudPreferences, cloud: Cloud
+ ):
"""Initialize the Google config."""
super().__init__(hass)
self._config = config
@@ -43,7 +41,11 @@ def __init__(self, hass, config, cloud_user: str, prefs: CloudPreferences, cloud
@property
def enabled(self):
"""Return if Google is enabled."""
- return self._cloud.is_logged_in and self._prefs.google_enabled
+ return (
+ self._cloud.is_logged_in
+ and not self._cloud.subscription_expired
+ and self._prefs.google_enabled
+ )
@property
def entity_config(self):
@@ -81,8 +83,15 @@ def cloud_user(self):
async def async_initialize(self):
"""Perform async initialization of config."""
await super().async_initialize()
- # Remove bad data that was there until 0.103.6 - Jan 6, 2020
- self._store.pop_agent_user_id(self._user)
+
+ # Remove old/wrong user agent ids
+ remove_agent_user_ids = []
+ for agent_user_id in self._store.agent_user_ids:
+ if agent_user_id != self.agent_user_id:
+ remove_agent_user_ids.append(agent_user_id)
+
+ for agent_user_id in remove_agent_user_ids:
+ await self.async_disconnect_agent_user(agent_user_id)
self._prefs.async_listen_updates(self._async_prefs_updated)
@@ -122,6 +131,11 @@ def agent_user_id(self):
"""Return Agent User Id to use for query responses."""
return self._cloud.username
+ @property
+ def has_registered_user_agent(self):
+ """Return if we have a Agent User Id registered."""
+ return len(self._store.agent_user_ids) > 0
+
def get_agent_user_id(self, context):
"""Get agent user ID making request."""
return self.agent_user_id
@@ -192,17 +206,7 @@ async def _handle_entity_registry_updated(self, event):
if not self._should_expose_entity_id(entity_id):
return
- if self.hass.state == CoreState.running:
- self.async_schedule_google_sync_all()
- return
-
- if self._sync_on_started:
+ if self.hass.state != CoreState.running:
return
- self._sync_on_started = True
-
- async def sync_google(_):
- """Sync entities to Google."""
- await self.async_sync_entities_all()
-
- self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, sync_google)
+ self.async_schedule_google_sync_all()
diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py
index 2bcc37fec05d41..e9771012379ca6 100644
--- a/homeassistant/components/cloud/http_api.py
+++ b/homeassistant/components/cloud/http_api.py
@@ -27,7 +27,6 @@
HTTP_OK,
HTTP_UNAUTHORIZED,
)
-from homeassistant.core import callback
from .const import (
DOMAIN,
@@ -225,6 +224,7 @@ async def post(self, request, data):
hass = request.app["hass"]
cloud = hass.data[DOMAIN]
await cloud.login(data["email"], data["password"])
+
return self.json({"success": True})
@@ -310,15 +310,15 @@ async def post(self, request, data):
return self.json_message("ok")
-@callback
-def websocket_cloud_status(hass, connection, msg):
+@websocket_api.async_response
+async def websocket_cloud_status(hass, connection, msg):
"""Handle request for account info.
Async friendly.
"""
cloud = hass.data[DOMAIN]
connection.send_message(
- websocket_api.result_message(msg["id"], _account_data(cloud))
+ websocket_api.result_message(msg["id"], await _account_data(cloud))
)
@@ -446,7 +446,7 @@ async def websocket_hook_delete(hass, connection, msg):
connection.send_message(websocket_api.result_message(msg["id"]))
-def _account_data(cloud):
+async def _account_data(cloud):
"""Generate the auth data JSON response."""
if not cloud.is_logged_in:
@@ -456,6 +456,8 @@ def _account_data(cloud):
client = cloud.client
remote = cloud.remote
+ gconf = await client.get_google_config()
+
# Load remote certificate
if remote.certificate:
certificate = attr.asdict(remote.certificate)
@@ -467,6 +469,7 @@ def _account_data(cloud):
"email": claims["email"],
"cloud": cloud.iot.state,
"prefs": client.prefs.as_dict(),
+ "google_registered": gconf.has_registered_user_agent,
"google_entities": client.google_user_config["filter"].config,
"alexa_entities": client.alexa_user_config["filter"].config,
"remote_domain": remote.instance_domain,
@@ -485,7 +488,7 @@ async def websocket_remote_connect(hass, connection, msg):
cloud = hass.data[DOMAIN]
await cloud.client.prefs.async_update(remote_enabled=True)
await cloud.remote.connect()
- connection.send_result(msg["id"], _account_data(cloud))
+ connection.send_result(msg["id"], await _account_data(cloud))
@websocket_api.require_admin
@@ -498,7 +501,7 @@ async def websocket_remote_disconnect(hass, connection, msg):
cloud = hass.data[DOMAIN]
await cloud.client.prefs.async_update(remote_enabled=False)
await cloud.remote.disconnect()
- connection.send_result(msg["id"], _account_data(cloud))
+ connection.send_result(msg["id"], await _account_data(cloud))
@websocket_api.require_admin
diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json
index 9d27de133097ce..b854cb4578dcda 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.41.0"],
+ "requirements": ["hass-nabucasa==0.42.0"],
"dependencies": ["http", "webhook", "alexa"],
"after_dependencies": ["google_assistant"],
"codeowners": ["@home-assistant/cloud"]
diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py
index a15eafc4d081d8..c51d527873096d 100644
--- a/homeassistant/components/cloud/prefs.py
+++ b/homeassistant/components/cloud/prefs.py
@@ -1,6 +1,7 @@
"""Preference management for cloud."""
+from __future__ import annotations
+
from ipaddress import ip_address
-from typing import List, Optional
from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.auth.models import User
@@ -172,7 +173,7 @@ async def async_update_alexa_entity_config(
updated_entities = {**entities, entity_id: updated_entity}
await self.async_update(alexa_entity_configs=updated_entities)
- async def async_set_username(self, username):
+ async def async_set_username(self, username) -> bool:
"""Set the username that is logged in."""
# Logging out.
if username is None:
@@ -181,18 +182,20 @@ async def async_set_username(self, username):
if user is not None:
await self._hass.auth.async_remove_user(user)
await self._save_prefs({**self._prefs, PREF_CLOUD_USER: None})
- return
+ return False
cur_username = self._prefs.get(PREF_USERNAME)
if cur_username == username:
- return
+ return False
if cur_username is None:
await self._save_prefs({**self._prefs, PREF_USERNAME: username})
else:
await self._save_prefs(self._empty_config(username))
+ return True
+
def as_dict(self):
"""Return dictionary version."""
return {
@@ -234,7 +237,7 @@ def alexa_report_state(self):
return self._prefs.get(PREF_ALEXA_REPORT_STATE, DEFAULT_ALEXA_REPORT_STATE)
@property
- def alexa_default_expose(self) -> Optional[List[str]]:
+ def alexa_default_expose(self) -> list[str] | None:
"""Return array of entity domains that are exposed by default to Alexa.
Can return None, in which case for backwards should be interpreted as allow all domains.
@@ -272,7 +275,7 @@ def google_local_webhook_id(self):
return self._prefs[PREF_GOOGLE_LOCAL_WEBHOOK_ID]
@property
- def google_default_expose(self) -> Optional[List[str]]:
+ def google_default_expose(self) -> list[str] | None:
"""Return array of entity domains that are exposed by default to Google.
Can return None, in which case for backwards should be interpreted as allow all domains.
@@ -302,7 +305,7 @@ async def get_cloud_user(self) -> str:
await self.async_update(cloud_user=user.id)
return user.id
- async def _load_cloud_user(self) -> Optional[User]:
+ async def _load_cloud_user(self) -> User | None:
"""Load cloud user if available."""
user_id = self._prefs.get(PREF_CLOUD_USER)
diff --git a/homeassistant/components/cloud/services.yaml b/homeassistant/components/cloud/services.yaml
index 20c25225ce2450..a7fb6b2f21b56e 100644
--- a/homeassistant/components/cloud/services.yaml
+++ b/homeassistant/components/cloud/services.yaml
@@ -1,7 +1,7 @@
# Describes the format for available cloud services
remote_connect:
- description: Make instance UI available outside over NabuCasa cloud.
+ description: Make instance UI available outside over NabuCasa cloud
remote_disconnect:
- description: Disconnect UI from NabuCasa cloud.
+ description: Disconnect UI from NabuCasa cloud
diff --git a/homeassistant/components/cloud/stt.py b/homeassistant/components/cloud/stt.py
index 6c069ce16d777d..80578a8d721de6 100644
--- a/homeassistant/components/cloud/stt.py
+++ b/homeassistant/components/cloud/stt.py
@@ -1,5 +1,5 @@
"""Support for the cloud for speech to text service."""
-from typing import List
+from __future__ import annotations
from aiohttp import StreamReader
from hass_nabucasa import Cloud
@@ -56,32 +56,32 @@ def __init__(self, cloud: Cloud) -> None:
self.cloud = cloud
@property
- def supported_languages(self) -> List[str]:
+ def supported_languages(self) -> list[str]:
"""Return a list of supported languages."""
return SUPPORT_LANGUAGES
@property
- def supported_formats(self) -> List[AudioFormats]:
+ def supported_formats(self) -> list[AudioFormats]:
"""Return a list of supported formats."""
return [AudioFormats.WAV, AudioFormats.OGG]
@property
- def supported_codecs(self) -> List[AudioCodecs]:
+ def supported_codecs(self) -> list[AudioCodecs]:
"""Return a list of supported codecs."""
return [AudioCodecs.PCM, AudioCodecs.OPUS]
@property
- def supported_bit_rates(self) -> List[AudioBitRates]:
+ def supported_bit_rates(self) -> list[AudioBitRates]:
"""Return a list of supported bitrates."""
return [AudioBitRates.BITRATE_16]
@property
- def supported_sample_rates(self) -> List[AudioSampleRates]:
+ def supported_sample_rates(self) -> list[AudioSampleRates]:
"""Return a list of supported samplerates."""
return [AudioSampleRates.SAMPLERATE_16000]
@property
- def supported_channels(self) -> List[AudioChannels]:
+ def supported_channels(self) -> list[AudioChannels]:
"""Return a list of supported channels."""
return [AudioChannels.CHANNEL_MONO]
diff --git a/homeassistant/components/cloud/translations/ca.json b/homeassistant/components/cloud/translations/ca.json
index fede749c7dd935..4e6a14cd2f021e 100644
--- a/homeassistant/components/cloud/translations/ca.json
+++ b/homeassistant/components/cloud/translations/ca.json
@@ -2,9 +2,9 @@
"system_health": {
"info": {
"alexa_enabled": "Alexa activada",
- "can_reach_cert_server": "Servidor de certificaci\u00f3 accessible",
- "can_reach_cloud": "Home Assistant Cloud accessible",
- "can_reach_cloud_auth": "Servidor d'autenticaci\u00f3 accessible",
+ "can_reach_cert_server": "Acc\u00e9s al servidor de certificaci\u00f3",
+ "can_reach_cloud": "Acc\u00e9s a Home Assistant Cloud",
+ "can_reach_cloud_auth": "Acc\u00e9s al servidor d'autenticaci\u00f3",
"google_enabled": "Google activat",
"logged_in": "Sessi\u00f3 iniciada",
"relayer_connected": "Encaminador connectat",
diff --git a/homeassistant/components/cloud/translations/de.json b/homeassistant/components/cloud/translations/de.json
new file mode 100644
index 00000000000000..443a5e3aa72dde
--- /dev/null
+++ b/homeassistant/components/cloud/translations/de.json
@@ -0,0 +1,15 @@
+{
+ "system_health": {
+ "info": {
+ "alexa_enabled": "Alexa aktiviert",
+ "can_reach_cert_server": "Zertifikatsserver erreichbar",
+ "can_reach_cloud": "Home Assistant Cloud erreichbar",
+ "can_reach_cloud_auth": "Authentifizierungsserver erreichbar",
+ "google_enabled": "Google aktiviert",
+ "logged_in": "Angemeldet",
+ "remote_connected": "Remote verbunden",
+ "remote_enabled": "Remote aktiviert",
+ "subscription_expiration": "Ablauf des Abonnements"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloud/translations/fr.json b/homeassistant/components/cloud/translations/fr.json
new file mode 100644
index 00000000000000..9bb4029fce0532
--- /dev/null
+++ b/homeassistant/components/cloud/translations/fr.json
@@ -0,0 +1,16 @@
+{
+ "system_health": {
+ "info": {
+ "alexa_enabled": "Alexa activ\u00e9",
+ "can_reach_cert_server": "Acc\u00e9der au serveur de certificats",
+ "can_reach_cloud": "Acc\u00e9der \u00e0 Home Assistant Cloud",
+ "can_reach_cloud_auth": "Acc\u00e9der au serveur d'authentification",
+ "google_enabled": "Google activ\u00e9",
+ "logged_in": "Connect\u00e9",
+ "relayer_connected": "Relais connect\u00e9",
+ "remote_connected": "Contr\u00f4le \u00e0 distance connect\u00e9",
+ "remote_enabled": "Contr\u00f4le \u00e0 distance activ\u00e9",
+ "subscription_expiration": "Expiration de l'abonnement"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloud/translations/hu.json b/homeassistant/components/cloud/translations/hu.json
index a2bea167b5ea00..8301806831b53a 100644
--- a/homeassistant/components/cloud/translations/hu.json
+++ b/homeassistant/components/cloud/translations/hu.json
@@ -2,6 +2,8 @@
"system_health": {
"info": {
"alexa_enabled": "Alexa enged\u00e9lyezve",
+ "can_reach_cert_server": "Tan\u00fas\u00edtv\u00e1nykiszolg\u00e1l\u00f3 el\u00e9r\u00e9se",
+ "can_reach_cloud": "Home Assistant Cloud el\u00e9r\u00e9se",
"can_reach_cloud_auth": "Hiteles\u00edt\u00e9si kiszolg\u00e1l\u00f3 el\u00e9r\u00e9se",
"google_enabled": "Google enged\u00e9lyezve",
"logged_in": "Bejelentkezve",
diff --git a/homeassistant/components/cloud/translations/id.json b/homeassistant/components/cloud/translations/id.json
new file mode 100644
index 00000000000000..1cff542796c3c9
--- /dev/null
+++ b/homeassistant/components/cloud/translations/id.json
@@ -0,0 +1,16 @@
+{
+ "system_health": {
+ "info": {
+ "alexa_enabled": "Alexa Diaktifkan",
+ "can_reach_cert_server": "Keterjangkauan Server Sertifikat",
+ "can_reach_cloud": "Keterjangkauan Home Assistant Cloud",
+ "can_reach_cloud_auth": "Keterjangkauan Server Autentikasi",
+ "google_enabled": "Google Diaktifkan",
+ "logged_in": "Masuk",
+ "relayer_connected": "Relayer Terhubung",
+ "remote_connected": "Terhubung Jarak Jauh",
+ "remote_enabled": "Kontrol Jarak Jauh Diaktifkan",
+ "subscription_expiration": "Masa Kedaluwarsa Langganan"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloud/translations/ko.json b/homeassistant/components/cloud/translations/ko.json
new file mode 100644
index 00000000000000..269afab2ce911e
--- /dev/null
+++ b/homeassistant/components/cloud/translations/ko.json
@@ -0,0 +1,16 @@
+{
+ "system_health": {
+ "info": {
+ "alexa_enabled": "Alexa \ud65c\uc131\ud654",
+ "can_reach_cert_server": "\uc778\uc99d\uc11c \uc11c\ubc84 \uc5f0\uacb0",
+ "can_reach_cloud": "Home Assistant Cloud \uc5f0\uacb0",
+ "can_reach_cloud_auth": "\uc778\uc99d \uc11c\ubc84 \uc5f0\uacb0",
+ "google_enabled": "Google Assistant \ud65c\uc131\ud654",
+ "logged_in": "\ub85c\uadf8\uc778",
+ "relayer_connected": "\uc911\uacc4\uae30 \uc5f0\uacb0",
+ "remote_connected": "\uc6d0\uaca9 \uc81c\uc5b4 \uc5f0\uacb0",
+ "remote_enabled": "\uc6d0\uaca9 \uc81c\uc5b4 \ud65c\uc131\ud654",
+ "subscription_expiration": "\uad6c\ub3c5 \ub9cc\ub8cc"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloud/translations/nl.json b/homeassistant/components/cloud/translations/nl.json
new file mode 100644
index 00000000000000..7d02a04cd01e06
--- /dev/null
+++ b/homeassistant/components/cloud/translations/nl.json
@@ -0,0 +1,16 @@
+{
+ "system_health": {
+ "info": {
+ "alexa_enabled": "Alexa ingeschakeld",
+ "can_reach_cert_server": "Certificaatserver bereikbaar",
+ "can_reach_cloud": "Home Assistant Cloud bereikbaar",
+ "can_reach_cloud_auth": "Authenticatieserver bereikbaar",
+ "google_enabled": "Google ingeschakeld",
+ "logged_in": "Ingelogd",
+ "relayer_connected": "Relayer verbonden",
+ "remote_connected": "Op afstand verbonden",
+ "remote_enabled": "Op afstand ingeschakeld",
+ "subscription_expiration": "Afloop abonnement"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloud/translations/pl.json b/homeassistant/components/cloud/translations/pl.json
index 30aaeeb77d14be..1df32a14d8e0e2 100644
--- a/homeassistant/components/cloud/translations/pl.json
+++ b/homeassistant/components/cloud/translations/pl.json
@@ -4,7 +4,7 @@
"alexa_enabled": "Alexa w\u0142\u0105czona",
"can_reach_cert_server": "Dost\u0119p do serwera certyfikat\u00f3w",
"can_reach_cloud": "Dost\u0119p do chmury Home Assistant",
- "can_reach_cloud_auth": "Dost\u0119p do serwera uwierzytelniania",
+ "can_reach_cloud_auth": "Dost\u0119p do serwera certyfikat\u00f3w",
"google_enabled": "Asystent Google w\u0142\u0105czony",
"logged_in": "Zalogowany",
"relayer_connected": "Relayer pod\u0142\u0105czony",
diff --git a/homeassistant/components/cloud/translations/tr.json b/homeassistant/components/cloud/translations/tr.json
index 0acb1e6a9a6632..75d1c768bebda4 100644
--- a/homeassistant/components/cloud/translations/tr.json
+++ b/homeassistant/components/cloud/translations/tr.json
@@ -1,6 +1,9 @@
{
"system_health": {
"info": {
+ "alexa_enabled": "Alexa Etkin",
+ "can_reach_cloud": "Home Assistant Cloud'a ula\u015f\u0131n",
+ "google_enabled": "Google Etkin",
"logged_in": "Giri\u015f Yapt\u0131",
"relayer_connected": "Yeniden Katman ba\u011fl\u0131",
"remote_connected": "Uzaktan Ba\u011fl\u0131",
diff --git a/homeassistant/components/cloud/translations/uk.json b/homeassistant/components/cloud/translations/uk.json
new file mode 100644
index 00000000000000..a2e68b911e58f0
--- /dev/null
+++ b/homeassistant/components/cloud/translations/uk.json
@@ -0,0 +1,16 @@
+{
+ "system_health": {
+ "info": {
+ "alexa_enabled": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0437 Alexa",
+ "can_reach_cert_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0456\u0432",
+ "can_reach_cloud": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e Home Assistant Cloud",
+ "can_reach_cloud_auth": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457",
+ "google_enabled": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0437 Google",
+ "logged_in": "\u0412\u0445\u0456\u0434 \u0443 \u0441\u0438\u0441\u0442\u0435\u043c\u0443",
+ "relayer_connected": "Relayer \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439",
+ "remote_connected": "\u0412\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u0438\u0439 \u0434\u043e\u0441\u0442\u0443\u043f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439",
+ "remote_enabled": "\u0412\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u0438\u0439 \u0434\u043e\u0441\u0442\u0443\u043f \u0430\u043a\u0442\u0438\u0432\u043e\u0432\u0430\u043d\u0438\u0439",
+ "subscription_expiration": "\u0422\u0435\u0440\u043c\u0456\u043d \u0434\u0456\u0457 \u043f\u0435\u0440\u0435\u0434\u043f\u043b\u0430\u0442\u0438"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloud/utils.py b/homeassistant/components/cloud/utils.py
index 36599b42ad336a..57f84b057f78d5 100644
--- a/homeassistant/components/cloud/utils.py
+++ b/homeassistant/components/cloud/utils.py
@@ -1,10 +1,12 @@
"""Helper functions for cloud components."""
-from typing import Any, Dict
+from __future__ import annotations
+
+from typing import Any
from aiohttp import payload, web
-def aiohttp_serialize_response(response: web.Response) -> Dict[str, Any]:
+def aiohttp_serialize_response(response: web.Response) -> dict[str, Any]:
"""Serialize an aiohttp response to a dictionary."""
body = response.body
diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py
index 446890887c10cb..abef32a4c5c1a6 100644
--- a/homeassistant/components/cloudflare/__init__.py
+++ b/homeassistant/components/cloudflare/__init__.py
@@ -1,7 +1,8 @@
"""Update the IP addresses of your Cloudflare DNS records."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Dict
from pycfdns import CloudflareUpdater
from pycfdns.exceptions import (
@@ -51,7 +52,7 @@
)
-async def async_setup(hass: HomeAssistant, config: Dict) -> bool:
+async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up the component."""
hass.data.setdefault(DOMAIN, {})
diff --git a/homeassistant/components/cloudflare/config_flow.py b/homeassistant/components/cloudflare/config_flow.py
index c96e6455ce9da8..0e3468903af716 100644
--- a/homeassistant/components/cloudflare/config_flow.py
+++ b/homeassistant/components/cloudflare/config_flow.py
@@ -1,6 +1,7 @@
"""Config flow for Cloudflare integration."""
+from __future__ import annotations
+
import logging
-from typing import Dict, List, Optional
from pycfdns import CloudflareUpdater
from pycfdns.exceptions import (
@@ -18,8 +19,7 @@
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from .const import CONF_RECORDS
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import CONF_RECORDS, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -30,7 +30,7 @@
)
-def _zone_schema(zones: Optional[List] = None):
+def _zone_schema(zones: list | None = None):
"""Zone selection schema."""
zones_list = []
@@ -40,7 +40,7 @@ def _zone_schema(zones: Optional[List] = None):
return vol.Schema({vol.Required(CONF_ZONE): vol.In(zones_list)})
-def _records_schema(records: Optional[List] = None):
+def _records_schema(records: list | None = None):
"""Zone records selection schema."""
records_dict = {}
@@ -50,7 +50,7 @@ def _records_schema(records: Optional[List] = None):
return vol.Schema({vol.Required(CONF_RECORDS): cv.multi_select(records_dict)})
-async def validate_input(hass: HomeAssistant, data: Dict):
+async def validate_input(hass: HomeAssistant, data: dict):
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
@@ -92,12 +92,11 @@ def __init__(self):
self.zones = None
self.records = None
- async def async_step_user(self, user_input: Optional[Dict] = None):
+ async def async_step_user(self, user_input: dict | None = None):
"""Handle a flow initiated by the user."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
- assert self.hass
persistent_notification.async_dismiss(self.hass, "cloudflare_setup")
errors = {}
@@ -114,7 +113,7 @@ async def async_step_user(self, user_input: Optional[Dict] = None):
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
- async def async_step_zone(self, user_input: Optional[Dict] = None):
+ async def async_step_zone(self, user_input: dict | None = None):
"""Handle the picking the zone."""
errors = {}
@@ -134,7 +133,7 @@ async def async_step_zone(self, user_input: Optional[Dict] = None):
errors=errors,
)
- async def async_step_records(self, user_input: Optional[Dict] = None):
+ async def async_step_records(self, user_input: dict | None = None):
"""Handle the picking the zone records."""
errors = {}
diff --git a/homeassistant/components/cloudflare/services.yaml b/homeassistant/components/cloudflare/services.yaml
index 23ffdd14d5f03d..80165700dbb034 100644
--- a/homeassistant/components/cloudflare/services.yaml
+++ b/homeassistant/components/cloudflare/services.yaml
@@ -1,2 +1,2 @@
update_records:
- description: Manually trigger update to Cloudflare records.
+ description: Manually trigger update to Cloudflare records
diff --git a/homeassistant/components/cloudflare/translations/de.json b/homeassistant/components/cloudflare/translations/de.json
index 809dad5da46e6c..21118e106bf22e 100644
--- a/homeassistant/components/cloudflare/translations/de.json
+++ b/homeassistant/components/cloudflare/translations/de.json
@@ -1,23 +1,34 @@
{
"config": {
"abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.",
"unknown": "Unerwarteter Fehler"
},
"error": {
"cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"invalid_zone": "Ung\u00fcltige Zone"
},
+ "flow_title": "Cloudflare: {name}",
"step": {
"records": {
"data": {
"records": "Datens\u00e4tze"
- }
+ },
+ "title": "W\u00e4hle die Records, die aktualisiert werden sollen"
},
"user": {
"data": {
"api_token": "API Token"
},
+ "description": "F\u00fcr diese Integration ist ein API-Token erforderlich, der mit Zone: Zone: Lesen und Zone: DNS: Bearbeiten f\u00fcr alle Zonen in deinem Konto erstellt wurde.",
"title": "Mit Cloudflare verbinden"
+ },
+ "zone": {
+ "data": {
+ "zone": "Zone"
+ },
+ "title": "W\u00e4hle die Zone, die aktualisiert werden soll"
}
}
}
diff --git a/homeassistant/components/cloudflare/translations/hu.json b/homeassistant/components/cloudflare/translations/hu.json
index fa13d00617f0d0..fed6f22d536c78 100644
--- a/homeassistant/components/cloudflare/translations/hu.json
+++ b/homeassistant/components/cloudflare/translations/hu.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "unknown": "V\u00e1ratlan hiba"
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
- "invalid_auth": "\u00c9rv\u00e9nytelen autentik\u00e1ci\u00f3",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"invalid_zone": "\u00c9rv\u00e9nytelen z\u00f3na"
},
"flow_title": "Cloudflare: {name}",
diff --git a/homeassistant/components/cloudflare/translations/id.json b/homeassistant/components/cloudflare/translations/id.json
new file mode 100644
index 00000000000000..98286398ea8e79
--- /dev/null
+++ b/homeassistant/components/cloudflare/translations/id.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "invalid_zone": "Zona tidak valid"
+ },
+ "flow_title": "Cloudflare: {name}",
+ "step": {
+ "records": {
+ "data": {
+ "records": "Catatan"
+ },
+ "title": "Pilih Catatan untuk Diperbarui"
+ },
+ "user": {
+ "data": {
+ "api_token": "Token API"
+ },
+ "description": "Integrasi ini memerlukan Token API yang dibuat dengan izin Zone:Zone:Read and Zone:DNS:Edit untuk semua zona di akun Anda.",
+ "title": "Hubungkan ke Cloudflare"
+ },
+ "zone": {
+ "data": {
+ "zone": "Zona"
+ },
+ "title": "Pilih Zona yang akan Diperbarui"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloudflare/translations/ko.json b/homeassistant/components/cloudflare/translations/ko.json
new file mode 100644
index 00000000000000..4dbe263a138aed
--- /dev/null
+++ b/homeassistant/components/cloudflare/translations/ko.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_zone": "\uc601\uc5ed\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "flow_title": "Cloudflare: {name}",
+ "step": {
+ "records": {
+ "data": {
+ "records": "\ub808\ucf54\ub4dc"
+ },
+ "title": "\uc5c5\ub370\uc774\ud2b8\ud560 \ub808\ucf54\ub4dc \uc120\ud0dd\ud558\uae30"
+ },
+ "user": {
+ "data": {
+ "api_token": "API \ud1a0\ud070"
+ },
+ "description": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \uacc4\uc815\uc758 \ubaa8\ub4e0 \uc601\uc5ed\uc5d0 \ub300\ud574 Zone:Zone:Read \ubc0f Zone:DNS:Edit \uad8c\ud55c\uc73c\ub85c \uc0dd\uc131\ub41c API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.",
+ "title": "Cloudflare\uc5d0 \uc5f0\uacb0\ud558\uae30"
+ },
+ "zone": {
+ "data": {
+ "zone": "\uc601\uc5ed"
+ },
+ "title": "\uc5c5\ub370\uc774\ud2b8\ud560 \uc601\uc5ed \uc120\ud0dd\ud558\uae30"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloudflare/translations/nl.json b/homeassistant/components/cloudflare/translations/nl.json
index 37162761d86f3e..94697419ff1e90 100644
--- a/homeassistant/components/cloudflare/translations/nl.json
+++ b/homeassistant/components/cloudflare/translations/nl.json
@@ -1,7 +1,35 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.",
+ "unknown": "Onverwachte fout"
+ },
"error": {
- "invalid_auth": "Ongeldige authenticatie"
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
+ "invalid_zone": "Ongeldige zone"
+ },
+ "flow_title": "Cloudflare: {name}",
+ "step": {
+ "records": {
+ "data": {
+ "records": "Records"
+ },
+ "title": "Kies de records die u wilt bijwerken"
+ },
+ "user": {
+ "data": {
+ "api_token": "API-token"
+ },
+ "description": "Voor deze integratie is een API-token vereist dat is gemaakt met Zone:Zone:Lezen en Zone:DNS:Bewerk machtigingen voor alle zones in uw account.",
+ "title": "Verbinden met Cloudflare"
+ },
+ "zone": {
+ "data": {
+ "zone": "Zone"
+ },
+ "title": "Kies de zone die u wilt bijwerken"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cloudflare/translations/ru.json b/homeassistant/components/cloudflare/translations/ru.json
index fa4819d8c836b2..7c397faa37e7f8 100644
--- a/homeassistant/components/cloudflare/translations/ru.json
+++ b/homeassistant/components/cloudflare/translations/ru.json
@@ -6,7 +6,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"invalid_zone": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0437\u043e\u043d\u0430"
},
"flow_title": "Cloudflare: {name}",
diff --git a/homeassistant/components/cloudflare/translations/tr.json b/homeassistant/components/cloudflare/translations/tr.json
index b7c7b438804b0d..5d1180961f6295 100644
--- a/homeassistant/components/cloudflare/translations/tr.json
+++ b/homeassistant/components/cloudflare/translations/tr.json
@@ -1,6 +1,12 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.",
+ "unknown": "Beklenmeyen hata"
+ },
"error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
"invalid_zone": "Ge\u00e7ersiz b\u00f6lge"
},
"flow_title": "Cloudflare: {name}",
@@ -12,6 +18,9 @@
"title": "G\u00fcncellenecek Kay\u0131tlar\u0131 Se\u00e7in"
},
"user": {
+ "data": {
+ "api_token": "API Belirteci"
+ },
"title": "Cloudflare'ye ba\u011flan\u0131n"
},
"zone": {
diff --git a/homeassistant/components/cloudflare/translations/uk.json b/homeassistant/components/cloudflare/translations/uk.json
new file mode 100644
index 00000000000000..425ec2733b8f66
--- /dev/null
+++ b/homeassistant/components/cloudflare/translations/uk.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "invalid_zone": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0437\u043e\u043d\u0430"
+ },
+ "flow_title": "Cloudflare: {name}",
+ "step": {
+ "records": {
+ "data": {
+ "records": "\u0417\u0430\u043f\u0438\u0441\u0438"
+ },
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0437\u0430\u043f\u0438\u0441\u0438 \u0434\u043b\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f"
+ },
+ "user": {
+ "data": {
+ "api_token": "\u0422\u043e\u043a\u0435\u043d API"
+ },
+ "description": "\u0414\u043b\u044f \u0446\u0456\u0454\u0457 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0442\u043e\u043a\u0435\u043d API, \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u0438\u0439 \u0437 \u0434\u043e\u0437\u0432\u043e\u043b\u0430\u043c\u0438 Zone: Zone: Read \u0456 Zone: DNS: Edit \u0434\u043b\u044f \u0432\u0441\u0456\u0445 \u0437\u043e\u043d \u0443 \u0432\u0430\u0448\u043e\u043c\u0443 \u043f\u0440\u043e\u0444\u0456\u043b\u0456.",
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Cloudflare"
+ },
+ "zone": {
+ "data": {
+ "zone": "\u0417\u043e\u043d\u0430"
+ },
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0437\u043e\u043d\u0443 \u0434\u043b\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloudflare/translations/zh-Hant.json b/homeassistant/components/cloudflare/translations/zh-Hant.json
index 1be70def0344e0..d9a05269748f5f 100644
--- a/homeassistant/components/cloudflare/translations/zh-Hant.json
+++ b/homeassistant/components/cloudflare/translations/zh-Hant.json
@@ -19,9 +19,9 @@
},
"user": {
"data": {
- "api_token": "API \u5bc6\u9470"
+ "api_token": "API \u6b0a\u6756"
},
- "description": "\u6b64\u6574\u5408\u9700\u8981\u5e33\u865f\u4e2d\u6240\u6709\u5340\u57df Zone:Zone:Read \u8207 Zone:DNS:Edit \u6b0a\u9650 API \u5bc6\u9470\u3002",
+ "description": "\u6b64\u6574\u5408\u9700\u8981\u5e33\u865f\u4e2d\u6240\u6709\u5340\u57df Zone:Zone:Read \u8207 Zone:DNS:Edit \u6b0a\u9650 API \u6b0a\u6756\u3002",
"title": "\u9023\u7dda\u81f3 Cloudflare"
},
"zone": {
diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py
index 73a55fda8e3e82..3968ebbe9d7fc6 100644
--- a/homeassistant/components/cmus/media_player.py
+++ b/homeassistant/components/cmus/media_player.py
@@ -64,37 +64,65 @@ def setup_platform(hass, config, add_entities, discover_info=None):
port = config[CONF_PORT]
name = config[CONF_NAME]
- try:
- cmus_remote = CmusDevice(host, password, port, name)
- except exceptions.InvalidPassword:
- _LOGGER.error("The provided password was rejected by cmus")
- return False
- add_entities([cmus_remote], True)
+ cmus_remote = CmusRemote(server=host, port=port, password=password)
+ cmus_remote.connect()
+
+ if cmus_remote.cmus is None:
+ return
+
+ add_entities([CmusDevice(device=cmus_remote, name=name, server=host)], True)
+
+
+class CmusRemote:
+ """Representation of a cmus connection."""
+
+ def __init__(self, server, port, password):
+ """Initialize the cmus remote."""
+
+ self._server = server
+ self._port = port
+ self._password = password
+ self.cmus = None
+
+ def connect(self):
+ """Connect to the cmus server."""
+
+ try:
+ self.cmus = remote.PyCmus(
+ server=self._server, port=self._port, password=self._password
+ )
+ except exceptions.InvalidPassword:
+ _LOGGER.error("The provided password was rejected by cmus")
class CmusDevice(MediaPlayerEntity):
"""Representation of a running cmus."""
- # pylint: disable=no-member
- def __init__(self, server, password, port, name):
+ def __init__(self, device, name, server):
"""Initialize the CMUS device."""
+ self._remote = device
if server:
- self.cmus = remote.PyCmus(server=server, password=password, port=port)
auto_name = f"cmus-{server}"
else:
- self.cmus = remote.PyCmus()
auto_name = "cmus-local"
self._name = name or auto_name
self.status = {}
def update(self):
"""Get the latest data and update the state."""
- status = self.cmus.get_status_dict()
- if not status:
- _LOGGER.warning("Received no status from cmus")
+ try:
+ status = self._remote.cmus.get_status_dict()
+ except BrokenPipeError:
+ self._remote.connect()
+ except exceptions.ConfigurationError:
+ _LOGGER.warning("A configuration error occurred")
+ self._remote.connect()
else:
self.status = status
+ return
+
+ _LOGGER.warning("Received no status from cmus")
@property
def name(self):
@@ -168,15 +196,15 @@ def supported_features(self):
def turn_off(self):
"""Service to send the CMUS the command to stop playing."""
- self.cmus.player_stop()
+ self._remote.cmus.player_stop()
def turn_on(self):
"""Service to send the CMUS the command to start playing."""
- self.cmus.player_play()
+ self._remote.cmus.player_play()
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
- self.cmus.set_volume(int(volume * 100))
+ self._remote.cmus.set_volume(int(volume * 100))
def volume_up(self):
"""Set the volume up."""
@@ -188,7 +216,7 @@ def volume_up(self):
current_volume = left
if current_volume <= 100:
- self.cmus.set_volume(int(current_volume) + 5)
+ self._remote.cmus.set_volume(int(current_volume) + 5)
def volume_down(self):
"""Set the volume down."""
@@ -200,12 +228,12 @@ def volume_down(self):
current_volume = left
if current_volume <= 100:
- self.cmus.set_volume(int(current_volume) - 5)
+ self._remote.cmus.set_volume(int(current_volume) - 5)
def play_media(self, media_type, media_id, **kwargs):
"""Send the play command."""
if media_type in [MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST]:
- self.cmus.player_play_file(media_id)
+ self._remote.cmus.player_play_file(media_id)
else:
_LOGGER.error(
"Invalid media type %s. Only %s and %s are supported",
@@ -216,24 +244,24 @@ def play_media(self, media_type, media_id, **kwargs):
def media_pause(self):
"""Send the pause command."""
- self.cmus.player_pause()
+ self._remote.cmus.player_pause()
def media_next_track(self):
"""Send next track command."""
- self.cmus.player_next()
+ self._remote.cmus.player_next()
def media_previous_track(self):
"""Send next track command."""
- self.cmus.player_prev()
+ self._remote.cmus.player_prev()
def media_seek(self, position):
"""Send seek command."""
- self.cmus.seek(position)
+ self._remote.cmus.seek(position)
def media_play(self):
"""Send the play command."""
- self.cmus.player_play()
+ self._remote.cmus.player_play()
def media_stop(self):
"""Send the stop command."""
- self.cmus.stop()
+ self._remote.cmus.stop()
diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py
index a61615d0e23507..c7d2a64d6b0b5a 100644
--- a/homeassistant/components/co2signal/sensor.py
+++ b/homeassistant/components/co2signal/sensor.py
@@ -4,7 +4,7 @@
import CO2Signal
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_LATITUDE,
@@ -13,7 +13,6 @@
ENERGY_KILO_WATT_HOUR,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
CONF_COUNTRY_CODE = "country_code"
@@ -52,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(devs, True)
-class CO2Sensor(Entity):
+class CO2Sensor(SensorEntity):
"""Implementation of the CO2Signal sensor."""
def __init__(self, token, country_code, lat, lon):
@@ -91,7 +90,7 @@ def unit_of_measurement(self):
return CO2_INTENSITY_UNIT
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the last update."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py
index f191bb778f4969..e4e4e719c9e25d 100644
--- a/homeassistant/components/coinbase/sensor.py
+++ b/homeassistant/components/coinbase/sensor.py
@@ -1,6 +1,6 @@
"""Support for Coinbase sensors."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION
-from homeassistant.helpers.entity import Entity
ATTR_NATIVE_BALANCE = "Balance in native currency"
@@ -38,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([sensor], True)
-class AccountSensor(Entity):
+class AccountSensor(SensorEntity):
"""Representation of a Coinbase.com sensor."""
def __init__(self, coinbase_data, name, currency):
@@ -71,7 +71,7 @@ def icon(self):
return CURRENCY_ICONS.get(self._unit_of_measurement, DEFAULT_COIN_ICON)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
@@ -88,7 +88,7 @@ def update(self):
self._native_currency = account["native_balance"]["currency"]
-class ExchangeRateSensor(Entity):
+class ExchangeRateSensor(SensorEntity):
"""Representation of a Coinbase.com sensor."""
def __init__(self, coinbase_data, exchange_currency, native_currency):
@@ -120,7 +120,7 @@ def icon(self):
return CURRENCY_ICONS.get(self.currency, DEFAULT_COIN_ICON)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/coinmarketcap/__init__.py b/homeassistant/components/coinmarketcap/__init__.py
deleted file mode 100644
index 0cdb5a16a4aec5..00000000000000
--- a/homeassistant/components/coinmarketcap/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The coinmarketcap component."""
diff --git a/homeassistant/components/coinmarketcap/manifest.json b/homeassistant/components/coinmarketcap/manifest.json
deleted file mode 100644
index e3f827f2718ba5..00000000000000
--- a/homeassistant/components/coinmarketcap/manifest.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "domain": "coinmarketcap",
- "name": "CoinMarketCap",
- "documentation": "https://www.home-assistant.io/integrations/coinmarketcap",
- "requirements": ["coinmarketcap==5.0.3"],
- "codeowners": []
-}
diff --git a/homeassistant/components/coinmarketcap/sensor.py b/homeassistant/components/coinmarketcap/sensor.py
deleted file mode 100644
index f3fe240c0bca37..00000000000000
--- a/homeassistant/components/coinmarketcap/sensor.py
+++ /dev/null
@@ -1,164 +0,0 @@
-"""Details about crypto currencies from CoinMarketCap."""
-from datetime import timedelta
-import logging
-from urllib.error import HTTPError
-
-from coinmarketcap import Market
-import voluptuous as vol
-
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_DISPLAY_CURRENCY
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
-
-_LOGGER = logging.getLogger(__name__)
-
-ATTR_VOLUME_24H = "volume_24h"
-ATTR_AVAILABLE_SUPPLY = "available_supply"
-ATTR_CIRCULATING_SUPPLY = "circulating_supply"
-ATTR_MARKET_CAP = "market_cap"
-ATTR_PERCENT_CHANGE_24H = "percent_change_24h"
-ATTR_PERCENT_CHANGE_7D = "percent_change_7d"
-ATTR_PERCENT_CHANGE_1H = "percent_change_1h"
-ATTR_PRICE = "price"
-ATTR_RANK = "rank"
-ATTR_SYMBOL = "symbol"
-ATTR_TOTAL_SUPPLY = "total_supply"
-
-ATTRIBUTION = "Data provided by CoinMarketCap"
-
-CONF_CURRENCY_ID = "currency_id"
-CONF_DISPLAY_CURRENCY_DECIMALS = "display_currency_decimals"
-
-DEFAULT_CURRENCY_ID = 1
-DEFAULT_DISPLAY_CURRENCY = "USD"
-DEFAULT_DISPLAY_CURRENCY_DECIMALS = 2
-
-ICON = "mdi:currency-usd"
-
-SCAN_INTERVAL = timedelta(minutes=15)
-
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_CURRENCY_ID, default=DEFAULT_CURRENCY_ID): cv.positive_int,
- vol.Optional(CONF_DISPLAY_CURRENCY, default=DEFAULT_DISPLAY_CURRENCY): vol.All(
- cv.string, vol.Upper
- ),
- vol.Optional(
- CONF_DISPLAY_CURRENCY_DECIMALS, default=DEFAULT_DISPLAY_CURRENCY_DECIMALS
- ): vol.All(vol.Coerce(int), vol.Range(min=1)),
- }
-)
-
-
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the CoinMarketCap sensor."""
- currency_id = config[CONF_CURRENCY_ID]
- display_currency = config[CONF_DISPLAY_CURRENCY]
- display_currency_decimals = config[CONF_DISPLAY_CURRENCY_DECIMALS]
-
- try:
- CoinMarketCapData(currency_id, display_currency).update()
- except HTTPError:
- _LOGGER.warning(
- "Currency ID %s or display currency %s "
- "is not available. Using 1 (bitcoin) "
- "and USD",
- currency_id,
- display_currency,
- )
- currency_id = DEFAULT_CURRENCY_ID
- display_currency = DEFAULT_DISPLAY_CURRENCY
-
- add_entities(
- [
- CoinMarketCapSensor(
- CoinMarketCapData(currency_id, display_currency),
- display_currency_decimals,
- )
- ],
- True,
- )
-
-
-class CoinMarketCapSensor(Entity):
- """Representation of a CoinMarketCap sensor."""
-
- def __init__(self, data, display_currency_decimals):
- """Initialize the sensor."""
- self.data = data
- self.display_currency_decimals = display_currency_decimals
- self._ticker = None
- self._unit_of_measurement = self.data.display_currency
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._ticker.get("name")
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return round(
- float(
- self._ticker.get("quotes").get(self.data.display_currency).get("price")
- ),
- self.display_currency_decimals,
- )
-
- @property
- def unit_of_measurement(self):
- """Return the unit the value is expressed in."""
- return self._unit_of_measurement
-
- @property
- def icon(self):
- """Return the icon to use in the frontend, if any."""
- return ICON
-
- @property
- def device_state_attributes(self):
- """Return the state attributes of the sensor."""
- return {
- ATTR_VOLUME_24H: self._ticker.get("quotes")
- .get(self.data.display_currency)
- .get("volume_24h"),
- ATTR_ATTRIBUTION: ATTRIBUTION,
- ATTR_CIRCULATING_SUPPLY: self._ticker.get("circulating_supply"),
- ATTR_MARKET_CAP: self._ticker.get("quotes")
- .get(self.data.display_currency)
- .get("market_cap"),
- ATTR_PERCENT_CHANGE_24H: self._ticker.get("quotes")
- .get(self.data.display_currency)
- .get("percent_change_24h"),
- ATTR_PERCENT_CHANGE_7D: self._ticker.get("quotes")
- .get(self.data.display_currency)
- .get("percent_change_7d"),
- ATTR_PERCENT_CHANGE_1H: self._ticker.get("quotes")
- .get(self.data.display_currency)
- .get("percent_change_1h"),
- ATTR_RANK: self._ticker.get("rank"),
- ATTR_SYMBOL: self._ticker.get("symbol"),
- ATTR_TOTAL_SUPPLY: self._ticker.get("total_supply"),
- }
-
- def update(self):
- """Get the latest data and updates the states."""
- self.data.update()
- self._ticker = self.data.ticker.get("data")
-
-
-class CoinMarketCapData:
- """Get the latest data and update the states."""
-
- def __init__(self, currency_id, display_currency):
- """Initialize the data object."""
- self.currency_id = currency_id
- self.display_currency = display_currency
- self.ticker = None
-
- def update(self):
- """Get the latest data from coinmarketcap.com."""
-
- self.ticker = Market().ticker(self.currency_id, convert=self.display_currency)
diff --git a/homeassistant/components/color_extractor/services.yaml b/homeassistant/components/color_extractor/services.yaml
index fa97dacf3d13c0..00438dc9aa17f0 100644
--- a/homeassistant/components/color_extractor/services.yaml
+++ b/homeassistant/components/color_extractor/services.yaml
@@ -1,12 +1,23 @@
turn_on:
- description: Set the light RGB to the predominant color found in the image provided by url or file path.
+ name: Turn on
+ description:
+ Set the light RGB to the predominant color found in the image provided by
+ URL or file path.
+ target:
fields:
color_extract_url:
- description: The URL of the image we want to extract RGB values from. Must be allowed in allowlist_external_urls.
+ name: URL
+ description:
+ The URL of the image we want to extract RGB values from. Must be allowed
+ in allowlist_external_urls.
example: https://www.example.com/images/logo.png
+ selector:
+ text:
color_extract_path:
- description: The full system path to the image we want to extract RGB values from. Must be allowed in allowlist_external_dirs.
+ name: Path
+ description:
+ The full system path to the image we want to extract RGB values from.
+ Must be allowed in allowlist_external_dirs.
example: /opt/images/logo.png
- entity_id:
- description: The entity we want to set our RGB color on.
- example: "light.living_room_shelves"
+ selector:
+ text:
diff --git a/homeassistant/components/comed_hourly_pricing/sensor.py b/homeassistant/components/comed_hourly_pricing/sensor.py
index 90830d5223672c..5d4ec6eec1337b 100644
--- a/homeassistant/components/comed_hourly_pricing/sensor.py
+++ b/homeassistant/components/comed_hourly_pricing/sensor.py
@@ -8,11 +8,10 @@
import async_timeout
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_OFFSET
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
_RESOURCE = "https://hourlypricing.comed.com/api"
@@ -65,7 +64,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(dev, True)
-class ComedHourlyPricingSensor(Entity):
+class ComedHourlyPricingSensor(SensorEntity):
"""Implementation of a ComEd Hourly Pricing sensor."""
def __init__(self, loop, websession, sensor_type, offset, name):
@@ -97,7 +96,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py
index 1d7b9c1c0af791..53bc242ba2ff03 100644
--- a/homeassistant/components/comfoconnect/fan.py
+++ b/homeassistant/components/comfoconnect/fan.py
@@ -1,4 +1,6 @@
"""Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit."""
+from __future__ import annotations
+
import logging
import math
@@ -13,6 +15,7 @@
from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import (
+ int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@@ -94,13 +97,18 @@ def supported_features(self) -> int:
return SUPPORT_SET_SPEED
@property
- def percentage(self) -> str:
+ def percentage(self) -> int | None:
"""Return the current speed percentage."""
- speed = self._ccb.data[SENSOR_FAN_SPEED_MODE]
+ speed = self._ccb.data.get(SENSOR_FAN_SPEED_MODE)
if speed is None:
return None
return ranged_value_to_percentage(SPEED_RANGE, speed)
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return int_states_in_range(SPEED_RANGE)
+
def turn_on(
self, speed: str = None, percentage=None, preset_mode=None, **kwargs
) -> None:
diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py
index 53075beecaf643..728bc13b76bdbb 100644
--- a/homeassistant/components/comfoconnect/sensor.py
+++ b/homeassistant/components/comfoconnect/sensor.py
@@ -26,9 +26,11 @@
)
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_DEVICE_CLASS,
+ ATTR_ICON,
+ ATTR_ID,
CONF_RESOURCES,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_HUMIDITY,
@@ -43,7 +45,6 @@
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from . import DOMAIN, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, ComfoConnectBridge
@@ -71,8 +72,6 @@
_LOGGER = logging.getLogger(__name__)
-ATTR_ICON = "icon"
-ATTR_ID = "id"
ATTR_LABEL = "label"
ATTR_MULTIPLIER = "multiplier"
ATTR_UNIT = "unit"
@@ -258,7 +257,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class ComfoConnectSensor(Entity):
+class ComfoConnectSensor(SensorEntity):
"""Representation of a ComfoConnect sensor."""
def __init__(self, name, ccb: ComfoConnectBridge, sensor_type) -> None:
diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py
index 05d2b9634f20f6..961d9a31f4e89d 100644
--- a/homeassistant/components/command_line/cover.py
+++ b/homeassistant/components/command_line/cover.py
@@ -107,11 +107,6 @@ def _move_cover(self, command):
return success
- def _query_state_value(self, command):
- """Execute state command for return value."""
- _LOGGER.info("Running state value command: %s", command)
- return check_output_or_log(command, self._timeout)
-
@property
def should_poll(self):
"""Only poll if we have state command."""
@@ -138,10 +133,8 @@ def current_cover_position(self):
def _query_state(self):
"""Query for the state."""
- if not self._command_state:
- _LOGGER.error("No state command specified")
- return
- return self._query_state_value(self._command_state)
+ _LOGGER.info("Running state value command: %s", self._command_state)
+ return check_output_or_log(self._command_state, self._timeout)
def update(self):
"""Update device state."""
diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py
index 35f7c5a4811b6e..10c5a16f60b37f 100644
--- a/homeassistant/components/command_line/sensor.py
+++ b/homeassistant/components/command_line/sensor.py
@@ -6,7 +6,7 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_COMMAND,
CONF_NAME,
@@ -17,7 +17,6 @@
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.reload import setup_reload_service
from . import check_output_or_log
@@ -63,7 +62,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
)
-class CommandSensor(Entity):
+class CommandSensor(SensorEntity):
"""Representation of a sensor that is using shell commands."""
def __init__(
@@ -95,7 +94,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attributes
@@ -145,19 +144,14 @@ def __init__(self, hass, command, command_timeout):
def update(self):
"""Get the latest data with a shell command."""
command = self.command
- cache = {}
- if command in cache:
- prog, args, args_compiled = cache[command]
- elif " " not in command:
+ if " " not in command:
prog = command
args = None
args_compiled = None
- cache[command] = (prog, args, args_compiled)
else:
prog, args = command.split(" ", 1)
args_compiled = template.Template(args, self.hass)
- cache[command] = (prog, args, args_compiled)
if args_compiled:
try:
diff --git a/homeassistant/components/command_line/services.yaml b/homeassistant/components/command_line/services.yaml
index 8876e8dc925184..de010ba8b850d7 100644
--- a/homeassistant/components/command_line/services.yaml
+++ b/homeassistant/components/command_line/services.yaml
@@ -1,2 +1,2 @@
reload:
- description: Reload all command_line entities.
+ description: Reload all command_line entities
diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py
index ce46cd4f2cd731..ae6c1c0c925f92 100644
--- a/homeassistant/components/command_line/switch.py
+++ b/homeassistant/components/command_line/switch.py
@@ -144,9 +144,6 @@ def assumed_state(self):
def _query_state(self):
"""Query for state."""
- if not self._command_state:
- _LOGGER.error("No state command specified")
- return
if self._value_template:
return self._query_state_value(self._command_state)
return self._query_state_code(self._command_state)
diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py
new file mode 100644
index 00000000000000..7d96905efa0aef
--- /dev/null
+++ b/homeassistant/components/compensation/__init__.py
@@ -0,0 +1,120 @@
+"""The Compensation integration."""
+import logging
+import warnings
+
+import numpy as np
+import voluptuous as vol
+
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.const import (
+ CONF_ATTRIBUTE,
+ CONF_SOURCE,
+ CONF_UNIQUE_ID,
+ CONF_UNIT_OF_MEASUREMENT,
+)
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.discovery import async_load_platform
+
+from .const import (
+ CONF_COMPENSATION,
+ CONF_DATAPOINTS,
+ CONF_DEGREE,
+ CONF_POLYNOMIAL,
+ CONF_PRECISION,
+ DATA_COMPENSATION,
+ DEFAULT_DEGREE,
+ DEFAULT_PRECISION,
+ DOMAIN,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def datapoints_greater_than_degree(value: dict) -> dict:
+ """Validate data point list is greater than polynomial degrees."""
+ if len(value[CONF_DATAPOINTS]) <= value[CONF_DEGREE]:
+ raise vol.Invalid(
+ f"{CONF_DATAPOINTS} must have at least {value[CONF_DEGREE]+1} {CONF_DATAPOINTS}"
+ )
+
+ return value
+
+
+COMPENSATION_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_SOURCE): cv.entity_id,
+ vol.Required(CONF_DATAPOINTS): [
+ vol.ExactSequence([vol.Coerce(float), vol.Coerce(float)])
+ ],
+ vol.Optional(CONF_UNIQUE_ID): cv.string,
+ vol.Optional(CONF_ATTRIBUTE): cv.string,
+ vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int,
+ vol.Optional(CONF_DEGREE, default=DEFAULT_DEGREE): vol.All(
+ vol.Coerce(int),
+ vol.Range(min=1, max=7),
+ ),
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
+ }
+)
+
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.Schema(
+ {cv.slug: vol.All(COMPENSATION_SCHEMA, datapoints_greater_than_degree)}
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+async def async_setup(hass, config):
+ """Set up the Compensation sensor."""
+ hass.data[DATA_COMPENSATION] = {}
+
+ for compensation, conf in config.get(DOMAIN).items():
+ _LOGGER.debug("Setup %s.%s", DOMAIN, compensation)
+
+ degree = conf[CONF_DEGREE]
+
+ # get x values and y values from the x,y point pairs
+ x_values, y_values = zip(*conf[CONF_DATAPOINTS])
+
+ # try to get valid coefficients for a polynomial
+ coefficients = None
+ with np.errstate(all="raise"):
+ with warnings.catch_warnings(record=True) as all_warnings:
+ warnings.simplefilter("always")
+ try:
+ coefficients = np.polyfit(x_values, y_values, degree)
+ except FloatingPointError as error:
+ _LOGGER.error(
+ "Setup of %s encountered an error, %s",
+ compensation,
+ error,
+ )
+ for warning in all_warnings:
+ _LOGGER.warning(
+ "Setup of %s encountered a warning, %s",
+ compensation,
+ str(warning.message).lower(),
+ )
+
+ if coefficients is not None:
+ data = {
+ k: v for k, v in conf.items() if k not in [CONF_DEGREE, CONF_DATAPOINTS]
+ }
+ data[CONF_POLYNOMIAL] = np.poly1d(coefficients)
+
+ hass.data[DATA_COMPENSATION][compensation] = data
+
+ hass.async_create_task(
+ async_load_platform(
+ hass,
+ SENSOR_DOMAIN,
+ DOMAIN,
+ {CONF_COMPENSATION: compensation},
+ config,
+ )
+ )
+
+ return True
diff --git a/homeassistant/components/compensation/const.py b/homeassistant/components/compensation/const.py
new file mode 100644
index 00000000000000..f116725883ecdb
--- /dev/null
+++ b/homeassistant/components/compensation/const.py
@@ -0,0 +1,16 @@
+"""Compensation constants."""
+DOMAIN = "compensation"
+
+SENSOR = "compensation"
+
+CONF_COMPENSATION = "compensation"
+CONF_DATAPOINTS = "data_points"
+CONF_DEGREE = "degree"
+CONF_PRECISION = "precision"
+CONF_POLYNOMIAL = "polynomial"
+
+DATA_COMPENSATION = "compensation_data"
+
+DEFAULT_DEGREE = 1
+DEFAULT_NAME = "Compensation"
+DEFAULT_PRECISION = 2
diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json
new file mode 100644
index 00000000000000..86efbce72c87bd
--- /dev/null
+++ b/homeassistant/components/compensation/manifest.json
@@ -0,0 +1,7 @@
+{
+ "domain": "compensation",
+ "name": "Compensation",
+ "documentation": "https://www.home-assistant.io/integrations/compensation",
+ "requirements": ["numpy==1.20.2"],
+ "codeowners": ["@Petro31"]
+}
diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py
new file mode 100644
index 00000000000000..35ca07ce52215b
--- /dev/null
+++ b/homeassistant/components/compensation/sensor.py
@@ -0,0 +1,162 @@
+"""Support for compensation sensor."""
+import logging
+
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.const import (
+ ATTR_UNIT_OF_MEASUREMENT,
+ CONF_ATTRIBUTE,
+ CONF_SOURCE,
+ CONF_UNIQUE_ID,
+ CONF_UNIT_OF_MEASUREMENT,
+ STATE_UNKNOWN,
+)
+from homeassistant.core import callback
+from homeassistant.helpers.event import async_track_state_change_event
+
+from .const import (
+ CONF_COMPENSATION,
+ CONF_POLYNOMIAL,
+ CONF_PRECISION,
+ DATA_COMPENSATION,
+ DEFAULT_NAME,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_COEFFICIENTS = "coefficients"
+ATTR_SOURCE = "source"
+ATTR_SOURCE_ATTRIBUTE = "source_attribute"
+
+
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Set up the Compensation sensor."""
+ if discovery_info is None:
+ return
+
+ compensation = discovery_info[CONF_COMPENSATION]
+ conf = hass.data[DATA_COMPENSATION][compensation]
+
+ source = conf[CONF_SOURCE]
+ attribute = conf.get(CONF_ATTRIBUTE)
+ name = f"{DEFAULT_NAME} {source}"
+ if attribute is not None:
+ name = f"{name} {attribute}"
+
+ async_add_entities(
+ [
+ CompensationSensor(
+ conf.get(CONF_UNIQUE_ID),
+ name,
+ source,
+ attribute,
+ conf[CONF_PRECISION],
+ conf[CONF_POLYNOMIAL],
+ conf.get(CONF_UNIT_OF_MEASUREMENT),
+ )
+ ]
+ )
+
+
+class CompensationSensor(SensorEntity):
+ """Representation of a Compensation sensor."""
+
+ def __init__(
+ self,
+ unique_id,
+ name,
+ source,
+ attribute,
+ precision,
+ polynomial,
+ unit_of_measurement,
+ ):
+ """Initialize the Compensation sensor."""
+ self._source_entity_id = source
+ self._precision = precision
+ self._source_attribute = attribute
+ self._unit_of_measurement = unit_of_measurement
+ self._poly = polynomial
+ self._coefficients = polynomial.coefficients.tolist()
+ self._state = None
+ self._unique_id = unique_id
+ self._name = name
+
+ async def async_added_to_hass(self):
+ """Handle added to Hass."""
+ self.async_on_remove(
+ async_track_state_change_event(
+ self.hass,
+ [self._source_entity_id],
+ self._async_compensation_sensor_state_listener,
+ )
+ )
+
+ @property
+ def unique_id(self):
+ """Return the unique id of this sensor."""
+ return self._unique_id
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def should_poll(self):
+ """No polling needed."""
+ return False
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def extra_state_attributes(self):
+ """Return the state attributes of the sensor."""
+ ret = {
+ ATTR_SOURCE: self._source_entity_id,
+ ATTR_COEFFICIENTS: self._coefficients,
+ }
+ if self._source_attribute:
+ ret[ATTR_SOURCE_ATTRIBUTE] = self._source_attribute
+ return ret
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit the value is expressed in."""
+ return self._unit_of_measurement
+
+ @callback
+ def _async_compensation_sensor_state_listener(self, event):
+ """Handle sensor state changes."""
+ new_state = event.data.get("new_state")
+ if new_state is None:
+ return
+
+ if self._unit_of_measurement is None and self._source_attribute is None:
+ self._unit_of_measurement = new_state.attributes.get(
+ ATTR_UNIT_OF_MEASUREMENT
+ )
+
+ try:
+ if self._source_attribute:
+ value = float(new_state.attributes.get(self._source_attribute))
+ else:
+ value = (
+ None if new_state.state == STATE_UNKNOWN else float(new_state.state)
+ )
+ self._state = round(self._poly(value), self._precision)
+
+ except (ValueError, TypeError):
+ self._state = None
+ if self._source_attribute:
+ _LOGGER.warning(
+ "%s attribute %s is not numerical",
+ self._source_entity_id,
+ self._source_attribute,
+ )
+ else:
+ _LOGGER.warning("%s state is not numerical", self._source_entity_id)
+
+ self.async_write_ha_state()
diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py
index 1098594a04c642..7d07710a4d00ce 100644
--- a/homeassistant/components/config/__init__.py
+++ b/homeassistant/components/config/__init__.py
@@ -65,11 +65,11 @@ def component_loaded(event):
hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded)
- tasks = [setup_panel(panel_name) for panel_name in SECTIONS]
+ tasks = [asyncio.create_task(setup_panel(panel_name)) for panel_name in SECTIONS]
for panel_name in ON_DEMAND:
if panel_name in hass.config.components:
- tasks.append(setup_panel(panel_name))
+ tasks.append(asyncio.create_task(setup_panel(panel_name)))
if tasks:
await asyncio.wait(tasks)
diff --git a/homeassistant/components/config/area_registry.py b/homeassistant/components/config/area_registry.py
index 81daf35339e881..f40ed7834e3d6f 100644
--- a/homeassistant/components/config/area_registry.py
+++ b/homeassistant/components/config/area_registry.py
@@ -90,7 +90,7 @@ async def websocket_delete_area(hass, connection, msg):
registry = await async_get_registry(hass)
try:
- await registry.async_delete(msg["area_id"])
+ registry.async_delete(msg["area_id"])
except KeyError:
connection.send_message(
websocket_api.error_message(
diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py
index b8d9944d7af34e..edf94268741350 100644
--- a/homeassistant/components/config/config_entries.py
+++ b/homeassistant/components/config/config_entries.py
@@ -1,7 +1,6 @@
"""Http views to control the config manager."""
import aiohttp.web_exceptions
import voluptuous as vol
-import voluptuous_serialize
from homeassistant import config_entries, data_entry_flow
from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT
@@ -10,7 +9,6 @@
from homeassistant.const import HTTP_FORBIDDEN, HTTP_NOT_FOUND
from homeassistant.core import callback
from homeassistant.exceptions import Unauthorized
-import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView,
FlowManagerResourceView,
@@ -30,6 +28,7 @@ async def async_setup(hass):
hass.http.register_view(OptionManagerFlowIndexView(hass.config_entries.options))
hass.http.register_view(OptionManagerFlowResourceView(hass.config_entries.options))
+ hass.components.websocket_api.async_register_command(config_entry_disable)
hass.components.websocket_api.async_register_command(config_entry_update)
hass.components.websocket_api.async_register_command(config_entries_progress)
hass.components.websocket_api.async_register_command(system_options_list)
@@ -39,24 +38,6 @@ async def async_setup(hass):
return True
-def _prepare_json(result):
- """Convert result for JSON."""
- if result["type"] != data_entry_flow.RESULT_TYPE_FORM:
- return result
-
- data = result.copy()
-
- schema = data["data_schema"]
- if schema is None:
- data["data_schema"] = []
- else:
- data["data_schema"] = voluptuous_serialize.convert(
- schema, custom_serializer=cv.custom_serializer
- )
-
- return data
-
-
class ConfigManagerEntryIndexView(HomeAssistantView):
"""View to get available config entries."""
@@ -116,6 +97,17 @@ async def post(self, request, entry_id):
return self.json({"require_restart": not result})
+def _prepare_config_flow_result_json(result, prepare_result_json):
+ """Convert result to JSON."""
+ if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
+ return prepare_result_json(result)
+
+ data = result.copy()
+ data["result"] = entry_json(result["result"])
+ data.pop("data")
+ return data
+
+
class ConfigManagerFlowIndexView(FlowManagerIndexView):
"""View to create config flows."""
@@ -137,13 +129,7 @@ async def post(self, request):
def _prepare_result_json(self, result):
"""Convert result to JSON."""
- if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
- return super()._prepare_result_json(result)
-
- data = result.copy()
- data["result"] = data["result"].entry_id
- data.pop("data")
- return data
+ return _prepare_config_flow_result_json(result, super()._prepare_result_json)
class ConfigManagerFlowResourceView(FlowManagerResourceView):
@@ -170,13 +156,7 @@ async def post(self, request, flow_id):
def _prepare_result_json(self, result):
"""Convert result to JSON."""
- if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
- return super()._prepare_result_json(result)
-
- data = result.copy()
- data["result"] = data["result"].entry_id
- data.pop("data")
- return data
+ return _prepare_config_flow_result_json(result, super()._prepare_result_json)
class ConfigManagerAvailableFlowView(HomeAssistantView):
@@ -265,6 +245,21 @@ async def system_options_list(hass, connection, msg):
connection.send_result(msg["id"], entry.system_options.as_dict())
+def send_entry_not_found(connection, msg_id):
+ """Send Config entry not found error."""
+ connection.send_error(
+ msg_id, websocket_api.const.ERR_NOT_FOUND, "Config entry not found"
+ )
+
+
+def get_entry(hass, connection, entry_id, msg_id):
+ """Get entry, send error message if it doesn't exist."""
+ entry = hass.config_entries.async_get_entry(entry_id)
+ if entry is None:
+ send_entry_not_found(connection, msg_id)
+ return entry
+
+
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command(
@@ -279,13 +274,10 @@ async def system_options_update(hass, connection, msg):
changes = dict(msg)
changes.pop("id")
changes.pop("type")
- entry_id = changes.pop("entry_id")
- entry = hass.config_entries.async_get_entry(entry_id)
+ changes.pop("entry_id")
+ entry = get_entry(hass, connection, msg["entry_id"], msg["id"])
if entry is None:
- connection.send_error(
- msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config entry not found"
- )
return
hass.config_entries.async_update_entry(entry, system_options=changes)
@@ -302,20 +294,47 @@ async def config_entry_update(hass, connection, msg):
changes = dict(msg)
changes.pop("id")
changes.pop("type")
- entry_id = changes.pop("entry_id")
-
- entry = hass.config_entries.async_get_entry(entry_id)
+ changes.pop("entry_id")
+ entry = get_entry(hass, connection, msg["entry_id"], msg["id"])
if entry is None:
- connection.send_error(
- msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config entry not found"
- )
return
hass.config_entries.async_update_entry(entry, **changes)
connection.send_result(msg["id"], entry_json(entry))
+@websocket_api.require_admin
+@websocket_api.async_response
+@websocket_api.websocket_command(
+ {
+ "type": "config_entries/disable",
+ "entry_id": str,
+ # We only allow setting disabled_by user via API.
+ "disabled_by": vol.Any("user", None),
+ }
+)
+async def config_entry_disable(hass, connection, msg):
+ """Disable config entry."""
+ disabled_by = msg["disabled_by"]
+
+ result = False
+ try:
+ result = await hass.config_entries.async_set_disabled_by(
+ msg["entry_id"], disabled_by
+ )
+ except config_entries.OperationNotAllowed:
+ # Failed to unload the config entry
+ pass
+ except config_entries.UnknownEntry:
+ send_entry_not_found(connection, msg["id"])
+ return
+
+ result = {"require_restart": not result}
+
+ connection.send_result(msg["id"], result)
+
+
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command(
@@ -333,9 +352,7 @@ async def ignore_config_flow(hass, connection, msg):
)
if flow is None:
- connection.send_error(
- msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config entry not found"
- )
+ send_entry_not_found(connection, msg["id"])
return
if "unique_id" not in flow["context"]:
@@ -357,7 +374,7 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict:
"""Return JSON value of a config entry."""
handler = config_entries.HANDLERS.get(entry.domain)
supports_options = (
- # Guard in case handler is no longer registered (custom compnoent etc)
+ # Guard in case handler is no longer registered (custom component etc)
handler is not None
# pylint: disable=comparison-with-callable
and handler.async_get_options_flow
@@ -372,4 +389,5 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict:
"connection_class": entry.connection_class,
"supports_options": supports_options,
"supports_unload": entry.supports_unload,
+ "disabled_by": entry.disabled_by,
}
diff --git a/homeassistant/components/config/group.py b/homeassistant/components/config/group.py
index 22a9bf4f02ab8c..8319816eb8a211 100644
--- a/homeassistant/components/config/group.py
+++ b/homeassistant/components/config/group.py
@@ -6,9 +6,8 @@
)
from homeassistant.config import GROUP_CONFIG_PATH
from homeassistant.const import SERVICE_RELOAD
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.typing import HomeAssistantType
from . import EditKeyBasedConfigView
@@ -35,7 +34,7 @@ async def hook(action, config_key):
@callback
def async_describe_on_off_states(
- hass: HomeAssistantType, registry: GroupIntegrationRegistry
+ hass: HomeAssistant, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
return
diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py
index 9ddd9d76ed9c65..e988e58f76bdfd 100644
--- a/homeassistant/components/configurator/__init__.py
+++ b/homeassistant/components/configurator/__init__.py
@@ -6,6 +6,7 @@
A callback has to be provided to `request_config` which will be called when
the user has submitted configuration information.
"""
+from contextlib import suppress
import functools as ft
from homeassistant.const import (
@@ -96,11 +97,8 @@ def request_config(hass, *args, **kwargs):
@async_callback
def async_notify_errors(hass, request_id, error):
"""Add errors to a config request."""
- try:
+ with suppress(KeyError): # If request_id does not exist
hass.data[DATA_REQUESTS][request_id].async_notify_errors(request_id, error)
- except KeyError:
- # If request_id does not exist
- pass
@bind_hass
@@ -115,11 +113,8 @@ def notify_errors(hass, request_id, error):
@async_callback
def async_request_done(hass, request_id):
"""Mark a configuration request as done."""
- try:
+ with suppress(KeyError): # If request_id does not exist
hass.data[DATA_REQUESTS].pop(request_id).async_request_done(request_id)
- except KeyError:
- # If request_id does not exist
- pass
@bind_hass
diff --git a/homeassistant/components/configurator/translations/id.json b/homeassistant/components/configurator/translations/id.json
index 759af513228a3a..f345a39417b754 100644
--- a/homeassistant/components/configurator/translations/id.json
+++ b/homeassistant/components/configurator/translations/id.json
@@ -1,7 +1,7 @@
{
"state": {
"_": {
- "configure": "Konfigurasi",
+ "configure": "Konfigurasikan",
"configured": "Terkonfigurasi"
}
},
diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py
index a45cdc4006a0f6..d7f8ec52f7a434 100644
--- a/homeassistant/components/control4/__init__.py
+++ b/homeassistant/components/control4/__init__.py
@@ -42,17 +42,9 @@
PLATFORMS = ["light"]
-async def async_setup(hass: HomeAssistant, entry: ConfigEntry) -> bool:
- """Stub to allow setting up this component.
-
- Configuration through YAML is not supported at this time.
- """
- hass.data.setdefault(DOMAIN, {})
- return True
-
-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Control4 from a config entry."""
+ hass.data.setdefault(DOMAIN, {})
entry_data = hass.data[DOMAIN].setdefault(entry.entry_id, {})
account_session = aiohttp_client.async_get_clientsession(hass)
@@ -115,9 +107,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
entry_data[CONF_CONFIG_LISTENER] = entry.add_update_listener(update_listener)
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -134,8 +126,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/control4/config_flow.py b/homeassistant/components/control4/config_flow.py
index 03183edbfda3ed..1456c440dfa24c 100644
--- a/homeassistant/components/control4/config_flow.py
+++ b/homeassistant/components/control4/config_flow.py
@@ -19,8 +19,12 @@
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.device_registry import format_mac
-from .const import CONF_CONTROLLER_UNIQUE_ID, DEFAULT_SCAN_INTERVAL, MIN_SCAN_INTERVAL
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import (
+ CONF_CONTROLLER_UNIQUE_ID,
+ DEFAULT_SCAN_INTERVAL,
+ DOMAIN,
+ MIN_SCAN_INTERVAL,
+)
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/control4/translations/de.json b/homeassistant/components/control4/translations/de.json
index f9a5783cd9117e..e50e2499320d4b 100644
--- a/homeassistant/components/control4/translations/de.json
+++ b/homeassistant/components/control4/translations/de.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"step": {
@@ -10,6 +14,16 @@
"host": "IP-Addresse",
"password": "Passwort",
"username": "Benutzername"
+ },
+ "description": "Bitte gib deine Control4-Kontodaten und die IP-Adresse deiner lokalen Steuerung ein."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Sekunden zwischen Updates"
}
}
}
diff --git a/homeassistant/components/control4/translations/hu.json b/homeassistant/components/control4/translations/hu.json
index 3b2d79a34a77e2..68cb4fe23a9d35 100644
--- a/homeassistant/components/control4/translations/hu.json
+++ b/homeassistant/components/control4/translations/hu.json
@@ -2,6 +2,20 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "IP c\u00edm",
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/control4/translations/id.json b/homeassistant/components/control4/translations/id.json
new file mode 100644
index 00000000000000..4b8033c0873f62
--- /dev/null
+++ b/homeassistant/components/control4/translations/id.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Alamat IP",
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "description": "Masukkan detail akun Control4 Anda dan alamat IP pengontrol lokal Anda."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Interval pembaruan dalam detik"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/control4/translations/ko.json b/homeassistant/components/control4/translations/ko.json
index ca36da40c18fe6..245fe666eab1cc 100644
--- a/homeassistant/components/control4/translations/ko.json
+++ b/homeassistant/components/control4/translations/ko.json
@@ -23,7 +23,7 @@
"step": {
"init": {
"data": {
- "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9(\ucd08)"
+ "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 (\ucd08)"
}
}
}
diff --git a/homeassistant/components/control4/translations/nl.json b/homeassistant/components/control4/translations/nl.json
index 1c4e7de05c995a..f13dd5e7f64c7c 100644
--- a/homeassistant/components/control4/translations/nl.json
+++ b/homeassistant/components/control4/translations/nl.json
@@ -4,6 +4,7 @@
"already_configured": "Apparaat is al geconfigureerd"
},
"error": {
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
@@ -13,6 +14,16 @@
"host": "IP-adres",
"password": "Wachtwoord",
"username": "Gebruikersnaam"
+ },
+ "description": "Voer uw Control4-accountgegevens en het IP-adres van uw lokale controller in."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Seconden tussen updates"
}
}
}
diff --git a/homeassistant/components/control4/translations/ru.json b/homeassistant/components/control4/translations/ru.json
index 4f51641992b102..41a033c037643b 100644
--- a/homeassistant/components/control4/translations/ru.json
+++ b/homeassistant/components/control4/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
@@ -13,7 +13,7 @@
"data": {
"host": "IP-\u0430\u0434\u0440\u0435\u0441",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Control4 \u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0412\u0430\u0448\u0435\u0433\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430."
}
diff --git a/homeassistant/components/control4/translations/tr.json b/homeassistant/components/control4/translations/tr.json
new file mode 100644
index 00000000000000..aed7e564a760b5
--- /dev/null
+++ b/homeassistant/components/control4/translations/tr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0130p Adresi",
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/control4/translations/uk.json b/homeassistant/components/control4/translations/uk.json
index 6c0426eba8fb3c..682d86c5deb4da 100644
--- a/homeassistant/components/control4/translations/uk.json
+++ b/homeassistant/components/control4/translations/uk.json
@@ -1,10 +1,21 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
"step": {
"user": {
"data": {
+ "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
- }
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u0430\u043d\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Control4 \u0456 IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u0412\u0430\u0448\u043e\u0433\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430."
}
}
},
@@ -12,7 +23,7 @@
"step": {
"init": {
"data": {
- "scan_interval": "\u0421\u0435\u043a\u0443\u043d\u0434 \u043c\u0456\u0436 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f\u043c\u0438"
+ "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
}
}
}
diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py
index c9c2ab46cf9fd3..56cf4aecdeaebc 100644
--- a/homeassistant/components/conversation/agent.py
+++ b/homeassistant/components/conversation/agent.py
@@ -1,6 +1,7 @@
"""Agent foundation for conversation integration."""
+from __future__ import annotations
+
from abc import ABC, abstractmethod
-from typing import Optional
from homeassistant.core import Context
from homeassistant.helpers import intent
@@ -24,6 +25,6 @@ async def async_set_onboarding(self, shown):
@abstractmethod
async def async_process(
- self, text: str, context: Context, conversation_id: Optional[str] = None
+ self, text: str, context: Context, conversation_id: str | None = None
) -> intent.IntentResponse:
"""Process a sentence."""
diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py
index d2dcf13a62aece..a98f685ea1d447 100644
--- a/homeassistant/components/conversation/default_agent.py
+++ b/homeassistant/components/conversation/default_agent.py
@@ -1,6 +1,7 @@
"""Standard conversastion implementation for Home Assistant."""
+from __future__ import annotations
+
import re
-from typing import Optional
from homeassistant import core, setup
from homeassistant.components.cover.intent import INTENT_CLOSE_COVER, INTENT_OPEN_COVER
@@ -112,7 +113,7 @@ def register_utterances(self, component):
async_register(self.hass, intent_type, sentences)
async def async_process(
- self, text: str, context: core.Context, conversation_id: Optional[str] = None
+ self, text: str, context: core.Context, conversation_id: str | None = None
) -> intent.IntentResponse:
"""Process a sentence."""
intents = self.hass.data[DOMAIN]
diff --git a/homeassistant/components/conversation/services.yaml b/homeassistant/components/conversation/services.yaml
index 032edba8db18f9..edba9ffb0b9fee 100644
--- a/homeassistant/components/conversation/services.yaml
+++ b/homeassistant/components/conversation/services.yaml
@@ -1,7 +1,11 @@
# Describes the format for available component services
process:
+ name: Process
description: Launch a conversation from a transcribed text.
fields:
text:
+ name: Text
description: Transcribed text
example: Turn all lights on
+ selector:
+ text:
diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py
index 14165bd93b3984..2b092935bb04ac 100644
--- a/homeassistant/components/coolmaster/__init__.py
+++ b/homeassistant/components/coolmaster/__init__.py
@@ -13,12 +13,6 @@
_LOGGER = logging.getLogger(__name__)
-async def async_setup(hass, config):
- """Set up Coolmaster components."""
- hass.data.setdefault(DOMAIN, {})
- return True
-
-
async def async_setup_entry(hass, entry):
"""Set up Coolmaster from a config entry."""
host = entry.data[CONF_HOST]
@@ -31,7 +25,8 @@ async def async_setup_entry(hass, entry):
except (OSError, ConnectionRefusedError, TimeoutError) as error:
raise ConfigEntryNotReady() from error
coordinator = CoolmasterDataUpdateCoordinator(hass, coolmaster)
- await coordinator.async_refresh()
+ hass.data.setdefault(DOMAIN, {})
+ await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = {
DATA_INFO: info,
DATA_COORDINATOR: coordinator,
diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py
index 7c9b4d5d065b6b..04ee995d25c681 100644
--- a/homeassistant/components/coolmaster/config_flow.py
+++ b/homeassistant/components/coolmaster/config_flow.py
@@ -6,7 +6,6 @@
from homeassistant import config_entries, core
from homeassistant.const import CONF_HOST, CONF_PORT
-# pylint: disable=unused-import
from .const import AVAILABLE_MODES, CONF_SUPPORTED_MODES, DEFAULT_PORT, DOMAIN
MODES_SCHEMA = {vol.Required(mode, default=True): bool for mode in AVAILABLE_MODES}
diff --git a/homeassistant/components/coolmaster/translations/de.json b/homeassistant/components/coolmaster/translations/de.json
index 908dfaa448c1af..4e58b1ed964cd8 100644
--- a/homeassistant/components/coolmaster/translations/de.json
+++ b/homeassistant/components/coolmaster/translations/de.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"no_units": "Es wurden keine HVAC-Ger\u00e4te im CoolMasterNet-Host gefunden."
},
"step": {
diff --git a/homeassistant/components/coolmaster/translations/id.json b/homeassistant/components/coolmaster/translations/id.json
new file mode 100644
index 00000000000000..d12c10da25a25d
--- /dev/null
+++ b/homeassistant/components/coolmaster/translations/id.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "no_units": "Tidak dapat menemukan perangkat HVAC di host CoolMasterNet."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "cool": "Mendukung mode dingin",
+ "dry": "Mendukung mode kering",
+ "fan_only": "Mendukung mode kipas saja",
+ "heat": "Mendukung mode panas",
+ "heat_cool": "Mendukung mode panas/dingin otomatis",
+ "host": "Host",
+ "off": "Bisa dimatikan"
+ },
+ "title": "Siapkan detail koneksi CoolMasterNet Anda."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coolmaster/translations/ko.json b/homeassistant/components/coolmaster/translations/ko.json
index 5d0636bddcd245..82b88394431c97 100644
--- a/homeassistant/components/coolmaster/translations/ko.json
+++ b/homeassistant/components/coolmaster/translations/ko.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"no_units": "CoolMasterNet \ud638\uc2a4\ud2b8\uc5d0\uc11c HVAC \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
},
"step": {
diff --git a/homeassistant/components/coolmaster/translations/tr.json b/homeassistant/components/coolmaster/translations/tr.json
new file mode 100644
index 00000000000000..4848a34362cc3b
--- /dev/null
+++ b/homeassistant/components/coolmaster/translations/tr.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "off": "Kapat\u0131labilir"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coolmaster/translations/uk.json b/homeassistant/components/coolmaster/translations/uk.json
new file mode 100644
index 00000000000000..038a7bc48f0011
--- /dev/null
+++ b/homeassistant/components/coolmaster/translations/uk.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "no_units": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u043d\u0430\u0439\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043e\u043f\u0430\u043b\u0435\u043d\u043d\u044f, \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0446\u0456\u0457 \u0442\u0430 \u043a\u043e\u043d\u0434\u0438\u0446\u0456\u043e\u043d\u0443\u0432\u0430\u043d\u043d\u044f."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "cool": "\u0420\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f",
+ "dry": "\u0420\u0435\u0436\u0438\u043c \u043e\u0441\u0443\u0448\u0435\u043d\u043d\u044f",
+ "fan_only": "\u0420\u0435\u0436\u0438\u043c \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0446\u0456\u0457",
+ "heat": "\u0420\u0435\u0436\u0438\u043c \u043e\u0431\u0456\u0433\u0440\u0456\u0432\u0443",
+ "heat_cool": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c",
+ "host": "\u0425\u043e\u0441\u0442",
+ "off": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f"
+ },
+ "title": "CoolMasterNet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coronavirus/__init__.py b/homeassistant/components/coronavirus/__init__.py
index fa8efebe154a81..d05c4cef862ba7 100644
--- a/homeassistant/components/coronavirus/__init__.py
+++ b/homeassistant/components/coronavirus/__init__.py
@@ -44,9 +44,9 @@ def _async_migrator(entity_entry: entity_registry.RegistryEntry):
if not entry.unique_id:
hass.config_entries.async_update_entry(entry, unique_id=entry.data["country"])
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -57,8 +57,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/coronavirus/config_flow.py b/homeassistant/components/coronavirus/config_flow.py
index ccf2e7f2c75706..6d2776c7ecc5eb 100644
--- a/homeassistant/components/coronavirus/config_flow.py
+++ b/homeassistant/components/coronavirus/config_flow.py
@@ -4,7 +4,7 @@
from homeassistant import config_entries
from . import get_coordinator
-from .const import DOMAIN, OPTION_WORLDWIDE # pylint:disable=unused-import
+from .const import DOMAIN, OPTION_WORLDWIDE
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/coronavirus/sensor.py b/homeassistant/components/coronavirus/sensor.py
index 7f0e0c230e6cfe..472b8bc8d1ce3b 100644
--- a/homeassistant/components/coronavirus/sensor.py
+++ b/homeassistant/components/coronavirus/sensor.py
@@ -1,4 +1,5 @@
"""Sensor platform for the Corona virus."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -23,7 +24,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
-class CoronavirusSensor(CoordinatorEntity):
+class CoronavirusSensor(CoordinatorEntity, SensorEntity):
"""Sensor representing corona virus data."""
name = None
@@ -73,6 +74,6 @@ def unit_of_measurement(self):
return "people"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/coronavirus/translations/hu.json b/homeassistant/components/coronavirus/translations/hu.json
index fcee85c40e8297..631454ec04582a 100644
--- a/homeassistant/components/coronavirus/translations/hu.json
+++ b/homeassistant/components/coronavirus/translations/hu.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Ez az orsz\u00e1g m\u00e1r konfigur\u00e1lva van."
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
},
"step": {
"user": {
diff --git a/homeassistant/components/coronavirus/translations/id.json b/homeassistant/components/coronavirus/translations/id.json
new file mode 100644
index 00000000000000..e2626d16abb95d
--- /dev/null
+++ b/homeassistant/components/coronavirus/translations/id.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "country": "Negara"
+ },
+ "title": "Pilih negara untuk dipantau"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coronavirus/translations/ko.json b/homeassistant/components/coronavirus/translations/ko.json
index 65eec9e8bb757f..873aca88e30af9 100644
--- a/homeassistant/components/coronavirus/translations/ko.json
+++ b/homeassistant/components/coronavirus/translations/ko.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 \uad6d\uac00\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
diff --git a/homeassistant/components/coronavirus/translations/nl.json b/homeassistant/components/coronavirus/translations/nl.json
index d306894f7d03a3..fed3101b38e401 100644
--- a/homeassistant/components/coronavirus/translations/nl.json
+++ b/homeassistant/components/coronavirus/translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Dit land is al geconfigureerd."
+ "already_configured": "Service is al geconfigureerd"
},
"step": {
"user": {
diff --git a/homeassistant/components/coronavirus/translations/tr.json b/homeassistant/components/coronavirus/translations/tr.json
new file mode 100644
index 00000000000000..b608d60f824060
--- /dev/null
+++ b/homeassistant/components/coronavirus/translations/tr.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "country": "\u00dclke"
+ },
+ "title": "\u0130zlemek i\u00e7in bir \u00fclke se\u00e7in"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coronavirus/translations/uk.json b/homeassistant/components/coronavirus/translations/uk.json
new file mode 100644
index 00000000000000..151e7b14d3f0ca
--- /dev/null
+++ b/homeassistant/components/coronavirus/translations/uk.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "country": "\u041a\u0440\u0430\u0457\u043d\u0430"
+ },
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043a\u0440\u0430\u0457\u043d\u0443 \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py
index ad5e400011687e..ecb405a81cd0c7 100644
--- a/homeassistant/components/counter/__init__.py
+++ b/homeassistant/components/counter/__init__.py
@@ -1,6 +1,7 @@
"""Component to count within automations."""
+from __future__ import annotations
+
import logging
-from typing import Dict, Optional
import voluptuous as vol
@@ -12,13 +13,13 @@
CONF_MINIMUM,
CONF_NAME,
)
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import collection
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.storage import Store
-from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -100,7 +101,7 @@ def _none_to_empty_dict(value):
)
-async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the counters."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
id_manager = collection.IDManager()
@@ -108,8 +109,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
yaml_collection = collection.YamlCollection(
logging.getLogger(f"{__name__}.yaml_collection"), id_manager
)
- collection.attach_entity_component_collection(
- component, yaml_collection, Counter.from_yaml
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, component, yaml_collection, Counter.from_yaml
)
storage_collection = CounterStorageCollection(
@@ -117,8 +118,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
logging.getLogger(f"{__name__}.storage_collection"),
id_manager,
)
- collection.attach_entity_component_collection(
- component, storage_collection, Counter
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, component, storage_collection, Counter
)
await yaml_collection.async_load(
@@ -130,9 +131,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
).async_setup(hass)
- collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection)
- collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection)
-
component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment")
component.async_register_entity_service(SERVICE_DECREMENT, {}, "async_decrement")
component.async_register_entity_service(SERVICE_RESET, {}, "async_reset")
@@ -157,16 +155,16 @@ class CounterStorageCollection(collection.StorageCollection):
CREATE_SCHEMA = vol.Schema(CREATE_FIELDS)
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
- async def _process_create_data(self, data: Dict) -> Dict:
+ async def _process_create_data(self, data: dict) -> dict:
"""Validate the config is valid."""
return self.CREATE_SCHEMA(data)
@callback
- def _get_suggested_id(self, info: Dict) -> str:
+ def _get_suggested_id(self, info: dict) -> str:
"""Suggest an ID based on the config."""
return info[CONF_NAME]
- async def _update_data(self, data: dict, update_data: Dict) -> Dict:
+ async def _update_data(self, data: dict, update_data: dict) -> dict:
"""Return a new updated data object."""
update_data = self.UPDATE_SCHEMA(update_data)
return {**data, **update_data}
@@ -175,14 +173,14 @@ async def _update_data(self, data: dict, update_data: Dict) -> Dict:
class Counter(RestoreEntity):
"""Representation of a counter."""
- def __init__(self, config: Dict):
+ def __init__(self, config: dict):
"""Initialize a counter."""
- self._config: Dict = config
- self._state: Optional[int] = config[CONF_INITIAL]
+ self._config: dict = config
+ self._state: int | None = config[CONF_INITIAL]
self.editable: bool = True
@classmethod
- def from_yaml(cls, config: Dict) -> "Counter":
+ def from_yaml(cls, config: dict) -> Counter:
"""Create counter instance from yaml config."""
counter = cls(config)
counter.editable = False
@@ -195,22 +193,22 @@ def should_poll(self) -> bool:
return False
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return name of the counter."""
return self._config.get(CONF_NAME)
@property
- def icon(self) -> Optional[str]:
+ def icon(self) -> str | None:
"""Return the icon to be used for this entity."""
return self._config.get(CONF_ICON)
@property
- def state(self) -> Optional[int]:
+ def state(self) -> int | None:
"""Return the current value of the counter."""
return self._state
@property
- def state_attributes(self) -> Dict:
+ def extra_state_attributes(self) -> dict:
"""Return the state attributes."""
ret = {
ATTR_EDITABLE: self.editable,
@@ -224,7 +222,7 @@ def state_attributes(self) -> Dict:
return ret
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return unique id of the entity."""
return self._config[CONF_ID]
@@ -277,7 +275,7 @@ def async_configure(self, **kwargs) -> None:
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: dict) -> None:
"""Change the counter's settings WS CRUD."""
self._config = config
self._state = self.compute_next_state(self._state)
diff --git a/homeassistant/components/counter/reproduce_state.py b/homeassistant/components/counter/reproduce_state.py
index b2dd63adedc912..8fb15bd84e8544 100644
--- a/homeassistant/components/counter/reproduce_state.py
+++ b/homeassistant/components/counter/reproduce_state.py
@@ -1,11 +1,12 @@
"""Reproduce an Counter state."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import ATTR_ENTITY_ID
-from homeassistant.core import Context, State
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import Context, HomeAssistant, State
from . import (
ATTR_INITIAL,
@@ -21,11 +22,11 @@
async def _async_reproduce_state(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -67,11 +68,11 @@ async def _async_reproduce_state(
async def async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Counter states."""
await asyncio.gather(
diff --git a/homeassistant/components/counter/services.yaml b/homeassistant/components/counter/services.yaml
index 960424df0ca2c5..4dd427c1fa11bd 100644
--- a/homeassistant/components/counter/services.yaml
+++ b/homeassistant/components/counter/services.yaml
@@ -1,41 +1,67 @@
# Describes the format for available counter services
decrement:
+ name: Decrement
description: Decrement a counter.
- fields:
- entity_id:
- description: Entity id of the counter to decrement.
- example: "counter.count0"
+ target:
+
increment:
+ name: Increment
description: Increment a counter.
- fields:
- entity_id:
- description: Entity id of the counter to increment.
- example: "counter.count0"
+ target:
+
reset:
+ name: Reset
description: Reset a counter.
- fields:
- entity_id:
- description: Entity id of the counter to reset.
- example: "counter.count0"
+ target:
+
configure:
- description: Change counter parameters
+ name: Configure
+ description: Change counter parameters.
+ target:
fields:
- entity_id:
- description: Entity id of the counter to change.
- example: "counter.count0"
minimum:
- description: New minimum value for the counter or None to remove minimum
+ name: Minimum
+ description: New minimum value for the counter or None to remove minimum.
example: 0
+ selector:
+ number:
+ min: -9223372036854775807
+ max: 9223372036854775807
+ mode: box
maximum:
- description: New maximum value for the counter or None to remove maximum
+ name: Maximum
+ description: New maximum value for the counter or None to remove maximum.
example: 100
+ selector:
+ number:
+ min: -9223372036854775807
+ max: 9223372036854775807
+ mode: box
step:
- description: New value for step
+ name: Step
+ description: New value for step.
example: 2
+ selector:
+ number:
+ min: 1
+ max: 9223372036854775807
+ mode: box
initial:
- description: New value for initial
+ name: Initial
+ description: New value for initial.
example: 6
+ selector:
+ number:
+ min: 0
+ max: 9223372036854775807
+ mode: box
value:
- description: New state value
+ name: Value
+ description: New state value.
example: 3
+ selector:
+ number:
+ min: 0
+ max: 9223372036854775807
+ mode: box
diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py
index c63963c87dcea7..034beb7f9db411 100644
--- a/homeassistant/components/cover/__init__.py
+++ b/homeassistant/components/cover/__init__.py
@@ -2,7 +2,7 @@
from datetime import timedelta
import functools as ft
import logging
-from typing import Any
+from typing import Any, final
import voluptuous as vol
@@ -165,7 +165,7 @@ async def async_unload_entry(hass, entry):
class CoverEntity(Entity):
- """Representation of a cover."""
+ """Base class for cover entities."""
@property
def current_cover_position(self):
@@ -196,6 +196,7 @@ def state(self):
return STATE_CLOSED if closed else STATE_OPEN
+ @final
@property
def state_attributes(self):
"""Return the state attributes."""
diff --git a/homeassistant/components/cover/device_action.py b/homeassistant/components/cover/device_action.py
index 29dd97909e30da..74eef8102dfd66 100644
--- a/homeassistant/components/cover/device_action.py
+++ b/homeassistant/components/cover/device_action.py
@@ -1,5 +1,5 @@
"""Provides device automations for Cover."""
-from typing import List, Optional
+from __future__ import annotations
import voluptuous as vol
@@ -49,14 +49,16 @@
{
vol.Required(CONF_TYPE): vol.In(POSITION_ACTION_TYPES),
vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN),
- vol.Required("position"): vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
+ vol.Optional("position", default=0): vol.All(
+ vol.Coerce(int), vol.Range(min=0, max=100)
+ ),
}
)
ACTION_SCHEMA = vol.Any(CMD_ACTION_SCHEMA, POSITION_ACTION_SCHEMA)
-async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device actions for Cover devices."""
registry = await entity_registry.async_get_registry(hass)
actions = []
@@ -151,7 +153,7 @@ async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> di
return {
"extra_fields": vol.Schema(
{
- vol.Optional("position", default=0): vol.All(
+ vol.Optional(ATTR_POSITION, default=0): vol.All(
vol.Coerce(int), vol.Range(min=0, max=100)
)
}
@@ -160,11 +162,9 @@ async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> di
async def async_call_action_from_config(
- hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context]
+ hass: HomeAssistant, config: dict, variables: dict, context: Context | None
) -> None:
"""Execute a device action."""
- config = ACTION_SCHEMA(config)
-
service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]}
if config[CONF_TYPE] == "open":
diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py
index 0bcec2a6e43d0e..2943f589f7b8d8 100644
--- a/homeassistant/components/cover/device_condition.py
+++ b/homeassistant/components/cover/device_condition.py
@@ -1,5 +1,7 @@
"""Provides device automations for Cover."""
-from typing import Any, Dict, List
+from __future__ import annotations
+
+from typing import Any
import voluptuous as vol
@@ -65,10 +67,10 @@
CONDITION_SCHEMA = vol.Any(POSITION_CONDITION_SCHEMA, STATE_CONDITION_SCHEMA)
-async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_conditions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device conditions for Cover devices."""
registry = await entity_registry.async_get_registry(hass)
- conditions: List[Dict[str, Any]] = []
+ conditions: list[dict[str, Any]] = []
# 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/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py
index 764cc173e5fa13..9b94833bb29641 100644
--- a/homeassistant/components/cover/device_trigger.py
+++ b/homeassistant/components/cover/device_trigger.py
@@ -1,5 +1,5 @@
"""Provides device automations for Cover."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -16,6 +16,7 @@
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
+ CONF_FOR,
CONF_PLATFORM,
CONF_TYPE,
CONF_VALUE_TEMPLATE,
@@ -59,13 +60,14 @@
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(STATE_TRIGGER_TYPES),
+ vol.Optional(CONF_FOR): cv.positive_time_period_dict,
}
)
TRIGGER_SCHEMA = vol.Any(POSITION_TRIGGER_SCHEMA, STATE_TRIGGER_SCHEMA)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers for Cover devices."""
registry = await entity_registry.async_get_registry(hass)
triggers = []
@@ -83,60 +85,32 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
supports_open_close = supported_features & (SUPPORT_OPEN | SUPPORT_CLOSE)
# Add triggers for each entity that belongs to this integration
+ base_trigger = {
+ CONF_PLATFORM: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ }
+
if supports_open_close:
- triggers.append(
- {
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "opened",
- }
- )
- triggers.append(
+ triggers += [
{
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "closed",
+ **base_trigger,
+ CONF_TYPE: trigger,
}
- )
- triggers.append(
- {
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "opening",
- }
- )
- triggers.append(
- {
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "closing",
- }
- )
+ for trigger in STATE_TRIGGER_TYPES
+ ]
if supported_features & SUPPORT_SET_POSITION:
triggers.append(
{
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
+ **base_trigger,
CONF_TYPE: "position",
}
)
if supported_features & SUPPORT_SET_TILT_POSITION:
triggers.append(
{
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
+ **base_trigger,
CONF_TYPE: "tilt_position",
}
)
@@ -146,8 +120,12 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict:
"""List trigger capabilities."""
- if config[CONF_TYPE] not in ["position", "tilt_position"]:
- return {}
+ if config[CONF_TYPE] not in POSITION_TRIGGER_TYPES:
+ return {
+ "extra_fields": vol.Schema(
+ {vol.Optional(CONF_FOR): cv.positive_time_period_dict}
+ )
+ }
return {
"extra_fields": vol.Schema(
@@ -170,8 +148,6 @@ async def async_attach_trigger(
automation_info: dict,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
- config = TRIGGER_SCHEMA(config)
-
if config[CONF_TYPE] in STATE_TRIGGER_TYPES:
if config[CONF_TYPE] == "opened":
to_state = STATE_OPEN
@@ -187,6 +163,8 @@ async def async_attach_trigger(
CONF_ENTITY_ID: config[CONF_ENTITY_ID],
state_trigger.CONF_TO: to_state,
}
+ if CONF_FOR in config:
+ state_config[CONF_FOR] = config[CONF_FOR]
state_config = state_trigger.TRIGGER_SCHEMA(state_config)
return await state_trigger.async_attach_trigger(
hass, state_config, action, automation_info, platform_type="device"
diff --git a/homeassistant/components/cover/group.py b/homeassistant/components/cover/group.py
index d031b7cf693c29..28a1dc530fedd3 100644
--- a/homeassistant/components/cover/group.py
+++ b/homeassistant/components/cover/group.py
@@ -3,13 +3,12 @@
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_CLOSED, STATE_OPEN
-from homeassistant.core import callback
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import HomeAssistant, callback
@callback
def async_describe_on_off_states(
- hass: HomeAssistantType, registry: GroupIntegrationRegistry
+ hass: HomeAssistant, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
# On means open, Off means closed
diff --git a/homeassistant/components/cover/reproduce_state.py b/homeassistant/components/cover/reproduce_state.py
index 2a12172bdab882..3b82596a21c25e 100644
--- a/homeassistant/components/cover/reproduce_state.py
+++ b/homeassistant/components/cover/reproduce_state.py
@@ -1,7 +1,9 @@
"""Reproduce an Cover state."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.components.cover import (
ATTR_CURRENT_POSITION,
@@ -22,8 +24,7 @@
STATE_OPEN,
STATE_OPENING,
)
-from homeassistant.core import Context, State
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import Context, HomeAssistant, State
from . import DOMAIN
@@ -33,11 +34,11 @@
async def _async_reproduce_state(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -114,11 +115,11 @@ async def _async_reproduce_state(
async def async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Cover states."""
# Reproduce states in parallel.
diff --git a/homeassistant/components/cover/services.yaml b/homeassistant/components/cover/services.yaml
index 604955aa1992ed..1419a5f48edea9 100644
--- a/homeassistant/components/cover/services.yaml
+++ b/homeassistant/components/cover/services.yaml
@@ -1,77 +1,77 @@
# Describes the format for available cover services
open_cover:
+ name: Open
description: Open all or specified cover.
- fields:
- entity_id:
- description: Name(s) of cover(s) to open.
- example: "cover.living_room"
+ target:
close_cover:
+ name: Close
description: Close all or specified cover.
- fields:
- entity_id:
- description: Name(s) of cover(s) to close.
- example: "cover.living_room"
+ target:
toggle:
- description: Toggles a cover open/closed.
- fields:
- entity_id:
- description: Name(s) of cover(s) to toggle.
- example: "cover.garage_door"
+ name: Toggle
+ description: Toggle a cover open/closed.
+ target:
set_cover_position:
+ name: Set position
description: Move to specific position all or specified cover.
+ target:
fields:
- entity_id:
- description: Name(s) of cover(s) to set cover position.
- example: "cover.living_room"
position:
- description: Position of the cover (0 to 100).
+ name: Position
+ description: Position of the cover
+ required: true
example: 30
+ selector:
+ number:
+ min: 0
+ max: 100
+ step: 1
+ unit_of_measurement: "%"
+ mode: slider
stop_cover:
+ name: Stop
description: Stop all or specified cover.
- fields:
- entity_id:
- description: Name(s) of cover(s) to stop.
- example: "cover.living_room"
+ target:
open_cover_tilt:
+ name: Open tilt
description: Open all or specified cover tilt.
- fields:
- entity_id:
- description: Name(s) of cover(s) tilt to open.
- example: "cover.living_room_blinds"
+ target:
close_cover_tilt:
+ name: Close tilt
description: Close all or specified cover tilt.
- fields:
- entity_id:
- description: Name(s) of cover(s) to close tilt.
- example: "cover.living_room_blinds"
+ target:
toggle_cover_tilt:
- description: Toggles a cover tilt open/closed.
- fields:
- entity_id:
- description: Name(s) of cover(s) to toggle tilt.
- example: "cover.living_room_blinds"
+ name: Toggle tilt
+ description: Toggle a cover tilt open/closed.
+ target:
set_cover_tilt_position:
+ name: Set tilt position
description: Move to specific position all or specified cover tilt.
+ target:
fields:
- entity_id:
- description: Name(s) of cover(s) to set cover tilt position.
- example: "cover.living_room_blinds"
tilt_position:
- description: Tilt position of the cover (0 to 100).
+ name: Tilt position
+ description: Tilt position of the cover.
+ required: true
example: 30
+ selector:
+ number:
+ min: 0
+ max: 100
+ step: 1
+ unit_of_measurement: "%"
+ mode: slider
stop_cover_tilt:
+ name: Stop tilt
description: Stop all or specified cover.
- fields:
- entity_id:
- description: Name(s) of cover(s) to stop.
- example: "cover.living_room_blinds"
+ target:
diff --git a/homeassistant/components/cover/translations/de.json b/homeassistant/components/cover/translations/de.json
index a90ec822adc36a..bf320e07f9e5a4 100644
--- a/homeassistant/components/cover/translations/de.json
+++ b/homeassistant/components/cover/translations/de.json
@@ -6,7 +6,8 @@
"open": "\u00d6ffne {entity_name}",
"open_tilt": "{entity_name} gekippt \u00f6ffnen",
"set_position": "Position von {entity_name} setzen",
- "set_tilt_position": "Neigeposition von {entity_name} einstellen"
+ "set_tilt_position": "Neigeposition von {entity_name} einstellen",
+ "stop": "Stoppen {entity_name}"
},
"condition_type": {
"is_closed": "{entity_name} ist geschlossen",
diff --git a/homeassistant/components/cover/translations/hu.json b/homeassistant/components/cover/translations/hu.json
index 6d48cca1251304..87bd1c241c667e 100644
--- a/homeassistant/components/cover/translations/hu.json
+++ b/homeassistant/components/cover/translations/hu.json
@@ -6,7 +6,8 @@
"open": "{entity_name} nyit\u00e1sa",
"open_tilt": "{entity_name} d\u00f6nt\u00e9s nyit\u00e1sa",
"set_position": "{entity_name} poz\u00edci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa",
- "set_tilt_position": "{entity_name} d\u00f6nt\u00e9si poz\u00edci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa"
+ "set_tilt_position": "{entity_name} d\u00f6nt\u00e9si poz\u00edci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa",
+ "stop": "{entity_name} meg\u00e1ll\u00edt\u00e1sa"
},
"condition_type": {
"is_closed": "{entity_name} z\u00e1rva van",
diff --git a/homeassistant/components/cover/translations/id.json b/homeassistant/components/cover/translations/id.json
index b38fcf86a174e0..d07f2f23ad2992 100644
--- a/homeassistant/components/cover/translations/id.json
+++ b/homeassistant/components/cover/translations/id.json
@@ -1,9 +1,36 @@
{
+ "device_automation": {
+ "action_type": {
+ "close": "Tutup {entity_name}",
+ "close_tilt": "Tutup miring {entity_name}",
+ "open": "Buka {entity_name}",
+ "open_tilt": "Buka miring {entity_name}",
+ "set_position": "Tetapkan posisi {entity_name}",
+ "set_tilt_position": "Setel posisi miring {entity_name}",
+ "stop": "Hentikan {entity_name}"
+ },
+ "condition_type": {
+ "is_closed": "{entity_name} tertutup",
+ "is_closing": "{entity_name} menutup",
+ "is_open": "{entity_name} terbuka",
+ "is_opening": "{entity_name} membuka",
+ "is_position": "Posisi {entity_name} saat ini adalah",
+ "is_tilt_position": "Posisi miring {entity_name} saat ini adalah"
+ },
+ "trigger_type": {
+ "closed": "{entity_name} tertutup",
+ "closing": "{entity_name} menutup",
+ "opened": "{entity_name} terbuka",
+ "opening": "{entity_name} membuka",
+ "position": "Perubahan posisi {entity_name}",
+ "tilt_position": "Perubahan posisi kemiringan {entity_name}"
+ }
+ },
"state": {
"_": {
"closed": "Tertutup",
"closing": "Menutup",
- "open": "Buka",
+ "open": "Terbuka",
"opening": "Membuka",
"stopped": "Terhenti"
}
diff --git a/homeassistant/components/cover/translations/ko.json b/homeassistant/components/cover/translations/ko.json
index 0a666a8bd82d67..71a48bd532d4bb 100644
--- a/homeassistant/components/cover/translations/ko.json
+++ b/homeassistant/components/cover/translations/ko.json
@@ -1,28 +1,29 @@
{
"device_automation": {
"action_type": {
- "close": "{entity_name} \ub2eb\uae30",
- "close_tilt": "{entity_name} \ub2eb\uae30",
- "open": "{entity_name} \uc5f4\uae30",
- "open_tilt": "{entity_name} \uc5f4\uae30",
- "set_position": "{entity_name} \uac1c\ud3d0 \uc704\uce58 \uc124\uc815\ud558\uae30",
- "set_tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30 \uc124\uc815\ud558\uae30"
+ "close": "{entity_name}\uc744(\ub97c) \ub2eb\uae30",
+ "close_tilt": "{entity_name}\uc744(\ub97c) \ub2eb\uae30",
+ "open": "{entity_name}\uc744(\ub97c) \uc5f4\uae30",
+ "open_tilt": "{entity_name}\uc744(\ub97c) \uc5f4\uae30",
+ "set_position": "{entity_name}\uc758 \uac1c\ud3d0 \uc704\uce58 \uc124\uc815\ud558\uae30",
+ "set_tilt_position": "{entity_name}\uc758 \uac1c\ud3d0 \uae30\uc6b8\uae30 \uc124\uc815\ud558\uae30",
+ "stop": "{entity_name}\uc744(\ub97c) \uc815\uc9c0\ud558\uae30"
},
"condition_type": {
- "is_closed": "{entity_name} \uc774(\uac00) \ub2eb\ud600 \uc788\uc73c\uba74",
- "is_closing": "{entity_name} \uc774(\uac00) \ub2eb\ud788\ub294 \uc911\uc774\uba74",
- "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub824 \uc788\uc73c\uba74",
- "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9ac\ub294 \uc911\uc774\uba74",
- "is_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uc704\uce58\uac00 ~ \uc774\uba74",
- "is_tilt_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30\uac00 ~ \uc774\uba74"
+ "is_closed": "{entity_name}\uc774(\uac00) \ub2eb\ud600 \uc788\uc73c\uba74",
+ "is_closing": "{entity_name}\uc774(\uac00) \ub2eb\ud788\ub294 \uc911\uc774\uba74",
+ "is_open": "{entity_name}\uc774(\uac00) \uc5f4\ub824 \uc788\uc73c\uba74",
+ "is_opening": "{entity_name}\uc774(\uac00) \uc5f4\ub9ac\ub294 \uc911\uc774\uba74",
+ "is_position": "\ud604\uc7ac {entity_name}\uc758 \uac1c\ud3d0 \uc704\uce58\uac00 ~ \uc774\uba74",
+ "is_tilt_position": "\ud604\uc7ac {entity_name}\uc758 \uac1c\ud3d0 \uae30\uc6b8\uae30\uac00 ~ \uc774\uba74"
},
"trigger_type": {
- "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud790 \ub54c",
- "closing": "{entity_name} \uc774(\uac00) \ub2eb\ud788\ub294 \uc911\uc77c \ub54c",
- "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9b4 \ub54c",
- "opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9ac\ub294 \uc911\uc77c \ub54c",
- "position": "{entity_name} \uac1c\ud3d0 \uc704\uce58\uac00 \ubcc0\ud560 \ub54c",
- "tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30\uac00 \ubcc0\ud560 \ub54c"
+ "closed": "{entity_name}\uc774(\uac00) \ub2eb\ud614\uc744 \ub54c",
+ "closing": "{entity_name}\uc774(\uac00) \ub2eb\ud788\ub294 \uc911\uc77c \ub54c",
+ "opened": "{entity_name}\uc774(\uac00) \uc5f4\ub838\uc744 \ub54c",
+ "opening": "{entity_name}\uc774(\uac00) \uc5f4\ub9ac\ub294 \uc911\uc77c \ub54c",
+ "position": "{entity_name}\uc758 \uac1c\ud3d0 \uc704\uce58\uac00 \ubcc0\ud560 \ub54c",
+ "tilt_position": "{entity_name}\uc758 \uac1c\ud3d0 \uae30\uc6b8\uae30\uac00 \ubcc0\ud560 \ub54c"
}
},
"state": {
diff --git a/homeassistant/components/cover/translations/nl.json b/homeassistant/components/cover/translations/nl.json
index 679d9360a821c4..8b1ca3c3500f7c 100644
--- a/homeassistant/components/cover/translations/nl.json
+++ b/homeassistant/components/cover/translations/nl.json
@@ -6,7 +6,8 @@
"open": "Open {entity_name}",
"open_tilt": "Open de kanteling {entity_name}",
"set_position": "Stel de positie van {entity_name} in",
- "set_tilt_position": "Stel de {entity_name} kantelpositie in"
+ "set_tilt_position": "Stel de {entity_name} kantelpositie in",
+ "stop": "Stop {entity_name}"
},
"condition_type": {
"is_closed": "{entity_name} is gesloten",
diff --git a/homeassistant/components/cover/translations/sv.json b/homeassistant/components/cover/translations/sv.json
index 0a8dbecf124427..a950974033099b 100644
--- a/homeassistant/components/cover/translations/sv.json
+++ b/homeassistant/components/cover/translations/sv.json
@@ -1,5 +1,9 @@
{
"device_automation": {
+ "action_type": {
+ "close": "St\u00e4ng {entity_name}",
+ "open": "\u00d6ppna {entity_name}"
+ },
"condition_type": {
"is_closed": "{entity_name} \u00e4r st\u00e4ngd",
"is_closing": "{entity_name} st\u00e4ngs",
diff --git a/homeassistant/components/cover/translations/tr.json b/homeassistant/components/cover/translations/tr.json
index 98bc8cdb18d7fa..f042233a6d1289 100644
--- a/homeassistant/components/cover/translations/tr.json
+++ b/homeassistant/components/cover/translations/tr.json
@@ -1,4 +1,10 @@
{
+ "device_automation": {
+ "action_type": {
+ "close": "{entity_name} kapat",
+ "open": "{entity_name} a\u00e7\u0131n"
+ }
+ },
"state": {
"_": {
"closed": "Kapal\u0131",
diff --git a/homeassistant/components/cover/translations/uk.json b/homeassistant/components/cover/translations/uk.json
index 66cd0c77c73c97..ceb49fff3e949d 100644
--- a/homeassistant/components/cover/translations/uk.json
+++ b/homeassistant/components/cover/translations/uk.json
@@ -1,10 +1,29 @@
{
"device_automation": {
"action_type": {
+ "close": "{entity_name}: \u0437\u0430\u043a\u0440\u0438\u0442\u0438",
+ "close_tilt": "{entity_name}: \u0437\u0430\u043a\u0440\u0438\u0442\u0438 \u043b\u0430\u043c\u0435\u043b\u0456",
+ "open": "{entity_name}: \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u0438",
+ "open_tilt": "{entity_name}: \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u0438 \u043b\u0430\u043c\u0435\u043b\u0456",
+ "set_position": "{entity_name}: \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0438 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u043d\u044f",
+ "set_tilt_position": "{entity_name}: \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0438 \u043d\u0430\u0445\u0438\u043b \u043b\u0430\u043c\u0435\u043b\u0435\u0439",
"stop": "\u0417\u0443\u043f\u0438\u043d\u0438\u0442\u0438 {entity_name}"
},
+ "condition_type": {
+ "is_closed": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u0438\u0442\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_closing": "{entity_name} \u0437\u0430\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f",
+ "is_open": "{entity_name} \u0443 \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_opening": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f",
+ "is_position": "{entity_name} \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u043d\u0456",
+ "is_tilt_position": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \"{entity_name}\" \u043c\u0430\u0454 \u043d\u0430\u0445\u0438\u043b \u043b\u0430\u043c\u0435\u043b\u0435\u0439"
+ },
"trigger_type": {
- "opened": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e"
+ "closed": "{entity_name} \u0437\u0430\u043a\u0440\u0438\u0442\u043e",
+ "closing": "{entity_name} \u0437\u0430\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f",
+ "opened": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e",
+ "opening": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f",
+ "position": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u043d\u044f",
+ "tilt_position": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u043d\u0430\u0445\u0438\u043b \u043b\u0430\u043c\u0435\u043b\u0435\u0439"
}
},
"state": {
diff --git a/homeassistant/components/cover/translations/zh-Hans.json b/homeassistant/components/cover/translations/zh-Hans.json
index 04b25ad7cb8a97..765fcbeebe022f 100644
--- a/homeassistant/components/cover/translations/zh-Hans.json
+++ b/homeassistant/components/cover/translations/zh-Hans.json
@@ -2,8 +2,11 @@
"device_automation": {
"action_type": {
"close": "\u5173\u95ed {entity_name}",
+ "close_tilt": "\u5173\u95ed {entity_name}",
"open": "\u6253\u5f00 {entity_name}",
+ "open_tilt": "\u65cb\u5f00 {entity_name}",
"set_position": "\u8bbe\u7f6e {entity_name} \u7684\u4f4d\u7f6e",
+ "set_tilt_position": "\u8bbe\u7f6e {entity_name} \u7684\u503e\u659c\u4f4d\u7f6e",
"stop": "\u505c\u6b62 {entity_name}"
},
"condition_type": {
@@ -19,7 +22,8 @@
"closing": "{entity_name} \u6b63\u5728\u5173\u95ed",
"opened": "{entity_name} \u5df2\u6253\u5f00",
"opening": "{entity_name} \u6b63\u5728\u6253\u5f00",
- "position": "{entity_name} \u7684\u4f4d\u7f6e\u53d8\u5316"
+ "position": "{entity_name} \u7684\u4f4d\u7f6e\u53d8\u5316",
+ "tilt_position": "{entity_name} \u7684\u503e\u659c\u4f4d\u7f6e\u53d8\u5316"
}
},
"state": {
diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py
index df1a224bccefc8..019383446944fa 100644
--- a/homeassistant/components/cpuspeed/sensor.py
+++ b/homeassistant/components/cpuspeed/sensor.py
@@ -2,10 +2,9 @@
from cpuinfo import cpuinfo
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, FREQUENCY_GIGAHERTZ
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
ATTR_BRAND = "brand"
ATTR_HZ = "ghz_advertised"
@@ -29,7 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([CpuSpeedSensor(name)], True)
-class CpuSpeedSensor(Entity):
+class CpuSpeedSensor(SensorEntity):
"""Representation of a CPU sensor."""
def __init__(self, name):
@@ -54,7 +53,7 @@ def unit_of_measurement(self):
return FREQUENCY_GIGAHERTZ
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self.info is not None:
attrs = {
diff --git a/homeassistant/components/crimereports/__init__.py b/homeassistant/components/crimereports/__init__.py
deleted file mode 100644
index 57af9df4dbfe6d..00000000000000
--- a/homeassistant/components/crimereports/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The crimereports component."""
diff --git a/homeassistant/components/crimereports/manifest.json b/homeassistant/components/crimereports/manifest.json
deleted file mode 100644
index 624d812f5f334e..00000000000000
--- a/homeassistant/components/crimereports/manifest.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "domain": "crimereports",
- "name": "Crime Reports",
- "documentation": "https://www.home-assistant.io/integrations/crimereports",
- "requirements": ["crimereports==1.0.1"],
- "codeowners": []
-}
diff --git a/homeassistant/components/crimereports/sensor.py b/homeassistant/components/crimereports/sensor.py
deleted file mode 100644
index 8919b2d09b127e..00000000000000
--- a/homeassistant/components/crimereports/sensor.py
+++ /dev/null
@@ -1,127 +0,0 @@
-"""Sensor for Crime Reports."""
-from collections import defaultdict
-from datetime import timedelta
-
-import crimereports
-import voluptuous as vol
-
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (
- ATTR_ATTRIBUTION,
- ATTR_LATITUDE,
- ATTR_LONGITUDE,
- CONF_EXCLUDE,
- CONF_INCLUDE,
- CONF_LATITUDE,
- CONF_LONGITUDE,
- CONF_NAME,
- CONF_RADIUS,
- LENGTH_KILOMETERS,
- LENGTH_METERS,
-)
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
-from homeassistant.util import slugify
-from homeassistant.util.distance import convert
-from homeassistant.util.dt import now
-
-DOMAIN = "crimereports"
-
-EVENT_INCIDENT = f"{DOMAIN}_incident"
-
-SCAN_INTERVAL = timedelta(minutes=30)
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_NAME): cv.string,
- vol.Required(CONF_RADIUS): vol.Coerce(float),
- vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude,
- vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude,
- vol.Optional(CONF_INCLUDE): vol.All(cv.ensure_list, [cv.string]),
- vol.Optional(CONF_EXCLUDE): vol.All(cv.ensure_list, [cv.string]),
- }
-)
-
-
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Crime Reports platform."""
- latitude = config.get(CONF_LATITUDE, hass.config.latitude)
- longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
- name = config[CONF_NAME]
- radius = config[CONF_RADIUS]
- include = config.get(CONF_INCLUDE)
- exclude = config.get(CONF_EXCLUDE)
-
- add_entities(
- [CrimeReportsSensor(hass, name, latitude, longitude, radius, include, exclude)],
- True,
- )
-
-
-class CrimeReportsSensor(Entity):
- """Representation of a Crime Reports Sensor."""
-
- def __init__(self, hass, name, latitude, longitude, radius, include, exclude):
- """Initialize the Crime Reports sensor."""
- self._hass = hass
- self._name = name
- self._include = include
- self._exclude = exclude
- radius_kilometers = convert(radius, LENGTH_METERS, LENGTH_KILOMETERS)
- self._crimereports = crimereports.CrimeReports(
- (latitude, longitude), radius_kilometers
- )
- self._attributes = None
- self._state = None
- self._previous_incidents = set()
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- @property
- def device_state_attributes(self):
- """Return the state attributes."""
- return self._attributes
-
- def _incident_event(self, incident):
- """Fire if an event occurs."""
- data = {
- "type": incident.get("type"),
- "description": incident.get("friendly_description"),
- "timestamp": incident.get("timestamp"),
- "location": incident.get("location"),
- }
- if incident.get("coordinates"):
- data.update(
- {
- ATTR_LATITUDE: incident.get("coordinates")[0],
- ATTR_LONGITUDE: incident.get("coordinates")[1],
- }
- )
- self._hass.bus.fire(EVENT_INCIDENT, data)
-
- def update(self):
- """Update device state."""
- incident_counts = defaultdict(int)
- incidents = self._crimereports.get_incidents(
- now().date(), include=self._include, exclude=self._exclude
- )
- fire_events = len(self._previous_incidents) > 0
- if len(incidents) < len(self._previous_incidents):
- self._previous_incidents = set()
- for incident in incidents:
- incident_type = slugify(incident.get("type"))
- incident_counts[incident_type] += 1
- if fire_events and incident.get("id") not in self._previous_incidents:
- self._incident_event(incident)
- self._previous_incidents.add(incident.get("id"))
- self._attributes = {ATTR_ATTRIBUTION: crimereports.ATTRIBUTION}
- self._attributes.update(incident_counts)
- self._state = len(incidents)
diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py
index 72d2aa62ae079d..6a3fc7b4215fc0 100644
--- a/homeassistant/components/cups/sensor.py
+++ b/homeassistant/components/cups/sensor.py
@@ -5,11 +5,10 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_HOST, CONF_PORT, PERCENTAGE
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -96,7 +95,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(dev, True)
-class CupsSensor(Entity):
+class CupsSensor(SensorEntity):
"""Representation of a CUPS sensor."""
def __init__(self, data, printer):
@@ -131,7 +130,7 @@ def icon(self):
return ICON_PRINTER
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
if self._printer is None:
return None
@@ -155,7 +154,7 @@ def update(self):
self._available = self.data.available
-class IPPSensor(Entity):
+class IPPSensor(SensorEntity):
"""Implementation of the IPPSensor.
This sensor represents the status of the printer.
@@ -193,7 +192,7 @@ def state(self):
return PRINTER_STATES.get(key, key)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
if self._attributes is None:
return None
@@ -232,7 +231,7 @@ def update(self):
self._available = self.data.available
-class MarkerSensor(Entity):
+class MarkerSensor(SensorEntity):
"""Implementation of the MarkerSensor.
This sensor represents the percentage of ink or toner.
@@ -271,7 +270,7 @@ def unit_of_measurement(self):
return PERCENTAGE
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
if self._attributes is None:
return None
diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py
index f2cb29515b0381..f42534f509b6c4 100644
--- a/homeassistant/components/currencylayer/sensor.py
+++ b/homeassistant/components/currencylayer/sensor.py
@@ -5,7 +5,7 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_API_KEY,
@@ -14,7 +14,6 @@
CONF_QUOTE,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
_RESOURCE = "http://apilayer.net/api/live"
@@ -55,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class CurrencylayerSensor(Entity):
+class CurrencylayerSensor(SensorEntity):
"""Implementing the Currencylayer sensor."""
def __init__(self, rest, base, quote):
@@ -86,7 +85,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py
index b4950b8b05bcd3..092bbf8866d40c 100644
--- a/homeassistant/components/daikin/__init__.py
+++ b/homeassistant/components/daikin/__init__.py
@@ -16,17 +16,14 @@
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import Throttle
-from . import config_flow # noqa: F401
-from .const import CONF_UUID, KEY_MAC, TIMEOUT
+from .const import CONF_UUID, DOMAIN, KEY_MAC, TIMEOUT
_LOGGER = logging.getLogger(__name__)
-DOMAIN = "daikin"
-
PARALLEL_UPDATES = 0
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
-COMPONENT_TYPES = ["climate", "sensor", "switch"]
+PLATFORMS = ["climate", "sensor", "switch"]
CONFIG_SCHEMA = vol.Schema(
vol.All(
@@ -84,9 +81,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
if not daikin_api:
return False
hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api})
- for component in COMPONENT_TYPES:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -95,8 +92,8 @@ async def async_unload_entry(hass, config_entry):
"""Unload a config entry."""
await asyncio.wait(
[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in COMPONENT_TYPES
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
hass.data[DOMAIN].pop(config_entry.entry_id)
diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py
index b9956a87af0b23..619f9c8d1d8b4e 100644
--- a/homeassistant/components/daikin/config_flow.py
+++ b/homeassistant/components/daikin/config_flow.py
@@ -12,12 +12,12 @@
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD
-from .const import CONF_UUID, KEY_IP, KEY_MAC, TIMEOUT
+from .const import CONF_UUID, DOMAIN, KEY_MAC, TIMEOUT
_LOGGER = logging.getLogger(__name__)
-@config_entries.HANDLERS.register("daikin")
+@config_entries.HANDLERS.register(DOMAIN)
class FlowHandler(config_entries.ConfigFlow):
"""Handle a config flow."""
@@ -124,14 +124,6 @@ async def async_step_import(self, user_input):
return await self.async_step_user()
return await self._create_device(host)
- async def async_step_discovery(self, discovery_info):
- """Initialize step from discovery."""
- _LOGGER.debug("Discovered device: %s", discovery_info)
- await self.async_set_unique_id(discovery_info[KEY_MAC])
- self._abort_if_unique_id_configured()
- self.host = discovery_info[KEY_IP]
- return await self.async_step_user()
-
async def async_step_zeroconf(self, discovery_info):
"""Prepare configuration for a discovered Daikin device."""
_LOGGER.debug("Zeroconf user_input: %s", discovery_info)
diff --git a/homeassistant/components/daikin/const.py b/homeassistant/components/daikin/const.py
index 00bbbefd051381..5b4bdd28331b29 100644
--- a/homeassistant/components/daikin/const.py
+++ b/homeassistant/components/daikin/const.py
@@ -14,6 +14,8 @@
TEMP_CELSIUS,
)
+DOMAIN = "daikin"
+
ATTR_TARGET_TEMPERATURE = "target_temperature"
ATTR_INSIDE_TEMPERATURE = "inside_temperature"
ATTR_OUTSIDE_TEMPERATURE = "outside_temperature"
diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py
index 80de4ed34a5fe0..a5b515ea918946 100644
--- a/homeassistant/components/daikin/sensor.py
+++ b/homeassistant/components/daikin/sensor.py
@@ -1,4 +1,5 @@
"""Support for Daikin AC sensors."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_ICON,
@@ -6,7 +7,6 @@
CONF_TYPE,
CONF_UNIT_OF_MEASUREMENT,
)
-from homeassistant.helpers.entity import Entity
from . import DOMAIN as DAIKIN_DOMAIN, DaikinApi
from .const import (
@@ -49,7 +49,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
async_add_entities([DaikinSensor.factory(daikin_api, sensor) for sensor in sensors])
-class DaikinSensor(Entity):
+class DaikinSensor(SensorEntity):
"""Representation of a Sensor."""
@staticmethod
diff --git a/homeassistant/components/daikin/translations/de.json b/homeassistant/components/daikin/translations/de.json
index bbac113eb44487..dcec53c15690b1 100644
--- a/homeassistant/components/daikin/translations/de.json
+++ b/homeassistant/components/daikin/translations/de.json
@@ -2,15 +2,17 @@
"config": {
"abort": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"step": {
"user": {
"data": {
+ "api_key": "API-Schl\u00fcssel",
"host": "Host",
"password": "Passwort"
},
diff --git a/homeassistant/components/daikin/translations/hu.json b/homeassistant/components/daikin/translations/hu.json
index ef589eb7f6d869..f1cb7eab8f68c6 100644
--- a/homeassistant/components/daikin/translations/hu.json
+++ b/homeassistant/components/daikin/translations/hu.json
@@ -1,11 +1,13 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
- "unknown": "V\u00e1ratlan hiba"
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"step": {
"user": {
diff --git a/homeassistant/components/daikin/translations/id.json b/homeassistant/components/daikin/translations/id.json
new file mode 100644
index 00000000000000..8b7cfb5460eb1b
--- /dev/null
+++ b/homeassistant/components/daikin/translations/id.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kunci API",
+ "host": "Host",
+ "password": "Kata Sandi"
+ },
+ "description": "Masukkan Alamat IP perangkat AC Daikin Anda. \n\nPerhatikan bahwa Kunci API dan Kata Sandi hanya digunakan untuk perangkat BRP072Cxx dan SKYFi.",
+ "title": "Konfigurasi AC Daikin"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/daikin/translations/ko.json b/homeassistant/components/daikin/translations/ko.json
index 9c4a6c8d50c191..e87db9f29d34ee 100644
--- a/homeassistant/components/daikin/translations/ko.json
+++ b/homeassistant/components/daikin/translations/ko.json
@@ -4,13 +4,19 @@
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
"step": {
"user": {
"data": {
+ "api_key": "API \ud0a4",
"host": "\ud638\uc2a4\ud2b8",
"password": "\ube44\ubc00\ubc88\ud638"
},
- "description": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8\uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\nAPI \ud0a4 \ubc0f \ube44\ubc00\ubc88\ud638\ub294 BRP072Cxx \uc640 SKYFi \uae30\uae30\uc5d0\uc11c \uc0ac\uc6a9\ub41c\ub2e4\ub294 \uc810\uc5d0 \uc720\uc758\ud558\uc138\uc694.",
+ "description": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8\uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\nAPI \ud0a4 \ubc0f \ube44\ubc00\ubc88\ud638\ub294 BRP072Cxx \uc640 SKYFi \uae30\uae30\uc5d0\uc11c \uc0ac\uc6a9\ub41c\ub2e4\ub294 \uc810\uc5d0 \uc720\uc758\ud574\uc8fc\uc138\uc694.",
"title": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8 \uad6c\uc131\ud558\uae30"
}
}
diff --git a/homeassistant/components/daikin/translations/nl.json b/homeassistant/components/daikin/translations/nl.json
index 2d1e1edbdbbbc2..706a81b5f7fa13 100644
--- a/homeassistant/components/daikin/translations/nl.json
+++ b/homeassistant/components/daikin/translations/nl.json
@@ -4,6 +4,11 @@
"already_configured": "Apparaat is al geconfigureerd",
"cannot_connect": "Kon niet verbinden"
},
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
+ },
"step": {
"user": {
"data": {
@@ -11,7 +16,7 @@
"host": "Host",
"password": "Wachtwoord"
},
- "description": "Voer het IP-adres van uw Daikin AC in.",
+ "description": "Voer IP-adres van uw Daikin AC in.\n\nLet op dat API-sleutel en Wachtwoord alleen worden gebruikt door respectievelijk BRP072Cxx en SKYFi apparaten.",
"title": "Daikin AC instellen"
}
}
diff --git a/homeassistant/components/daikin/translations/pt.json b/homeassistant/components/daikin/translations/pt.json
index dd9b538ae8b08a..d4188fb10f960f 100644
--- a/homeassistant/components/daikin/translations/pt.json
+++ b/homeassistant/components/daikin/translations/pt.json
@@ -16,7 +16,7 @@
"host": "Servidor",
"password": "Palavra-passe"
},
- "description": "Introduza o endere\u00e7o IP do seu Daikin AC.",
+ "description": "Introduza Endere\u00e7o IP do seu Daikin AC.\n\nAten\u00e7\u00e3o que [%chave:common::config_flow::data::api_key%] e Palavra-passe s\u00f3 s\u00e3o utilizador pelos dispositivos BRP072Cxx e SKYFi, respectivamente.",
"title": "Configurar o Daikin AC"
}
}
diff --git a/homeassistant/components/daikin/translations/ru.json b/homeassistant/components/daikin/translations/ru.json
index df7d9fb07dc8d0..7365bb0e7bb37f 100644
--- a/homeassistant/components/daikin/translations/ru.json
+++ b/homeassistant/components/daikin/translations/ru.json
@@ -6,7 +6,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
diff --git a/homeassistant/components/daikin/translations/tr.json b/homeassistant/components/daikin/translations/tr.json
new file mode 100644
index 00000000000000..4148bf2b9f1c51
--- /dev/null
+++ b/homeassistant/components/daikin/translations/tr.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Anahtar\u0131",
+ "host": "Ana Bilgisayar",
+ "password": "Parola"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/daikin/translations/uk.json b/homeassistant/components/daikin/translations/uk.json
new file mode 100644
index 00000000000000..648d68d7a810e5
--- /dev/null
+++ b/homeassistant/components/daikin/translations/uk.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u0412\u0430\u0448\u043e\u0433\u043e Daikin AC. \n\n\u0417\u0432\u0435\u0440\u043d\u0456\u0442\u044c \u0443\u0432\u0430\u0433\u0443, \u0449\u043e \u041a\u043b\u044e\u0447 API \u0456 \u041f\u0430\u0440\u043e\u043b\u044c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044f\u043c\u0438 BRP072Cxx \u0456 SKYFi \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u043d\u043e.",
+ "title": "Daikin AC"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/danfoss_air/__init__.py b/homeassistant/components/danfoss_air/__init__.py
index b1dbf890eb970f..18780c10310a91 100644
--- a/homeassistant/components/danfoss_air/__init__.py
+++ b/homeassistant/components/danfoss_air/__init__.py
@@ -13,7 +13,7 @@
_LOGGER = logging.getLogger(__name__)
-DANFOSS_AIR_PLATFORMS = ["sensor", "binary_sensor", "switch"]
+PLATFORMS = ["sensor", "binary_sensor", "switch"]
DOMAIN = "danfoss_air"
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
@@ -29,7 +29,7 @@ def setup(hass, config):
hass.data[DOMAIN] = DanfossAir(conf[CONF_HOST])
- for platform in DANFOSS_AIR_PLATFORMS:
+ for platform in PLATFORMS:
discovery.load_platform(hass, platform, DOMAIN, {}, config)
return True
diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py
index 251c9692021dde..792a95e8ac46f1 100644
--- a/homeassistant/components/danfoss_air/sensor.py
+++ b/homeassistant/components/danfoss_air/sensor.py
@@ -3,6 +3,7 @@
from pydanfossair.commands import ReadCommand
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY,
@@ -10,7 +11,6 @@
PERCENTAGE,
TEMP_CELSIUS,
)
-from homeassistant.helpers.entity import Entity
from . import DOMAIN as DANFOSS_AIR_DOMAIN
@@ -77,7 +77,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(dev, True)
-class DanfossAir(Entity):
+class DanfossAir(SensorEntity):
"""Representation of a Sensor."""
def __init__(self, data, name, sensor_unit, sensor_type, device_class):
diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py
index 0bafdbb3d819ae..058969d96f9574 100644
--- a/homeassistant/components/darksky/sensor.py
+++ b/homeassistant/components/darksky/sensor.py
@@ -6,7 +6,11 @@
from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout
import voluptuous as vol
-from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE, PLATFORM_SCHEMA
+from homeassistant.components.sensor import (
+ DEVICE_CLASS_TEMPERATURE,
+ PLATFORM_SCHEMA,
+ SensorEntity,
+)
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_API_KEY,
@@ -29,7 +33,6 @@
UV_INDEX,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -544,7 +547,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class DarkSkySensor(Entity):
+class DarkSkySensor(SensorEntity):
"""Implementation of a Dark Sky sensor."""
def __init__(
@@ -620,7 +623,7 @@ def device_class(self):
return None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
@@ -708,7 +711,7 @@ def get_state(self, data):
return state
-class DarkSkyAlertSensor(Entity):
+class DarkSkyAlertSensor(SensorEntity):
"""Implementation of a Dark Sky sensor."""
def __init__(self, forecast_data, sensor_type, name):
@@ -739,7 +742,7 @@ def icon(self):
return "mdi:alert-circle-outline"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._alerts
diff --git a/homeassistant/components/debugpy/__init__.py b/homeassistant/components/debugpy/__init__.py
index caa691b23695af..98f08827c239e0 100644
--- a/homeassistant/components/debugpy/__init__.py
+++ b/homeassistant/components/debugpy/__init__.py
@@ -1,8 +1,9 @@
"""The Remote Python Debugger integration."""
+from __future__ import annotations
+
from asyncio import Event
import logging
from threading import Thread
-from typing import Optional
import debugpy
import voluptuous as vol
@@ -40,7 +41,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
conf = config[DOMAIN]
async def debug_start(
- call: Optional[ServiceCall] = None, *, wait: bool = True
+ call: ServiceCall | None = None, *, wait: bool = True
) -> None:
"""Start the debugger."""
debugpy.listen((conf[CONF_HOST], conf[CONF_PORT]))
diff --git a/homeassistant/components/debugpy/services.yaml b/homeassistant/components/debugpy/services.yaml
index 4e3c19dd0d7ea1..6bf9ad67288448 100644
--- a/homeassistant/components/debugpy/services.yaml
+++ b/homeassistant/components/debugpy/services.yaml
@@ -1,3 +1,3 @@
# Describes the format for available Remote Python Debugger services
start:
- description: Start the Remote Python Debugger.
+ description: Start the Remote Python Debugger
diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py
index fec7b82e365b27..8b609fe312696d 100644
--- a/homeassistant/components/deconz/__init__.py
+++ b/homeassistant/components/deconz/__init__.py
@@ -1,11 +1,17 @@
"""Support for deCONZ devices."""
import voluptuous as vol
-from homeassistant.const import EVENT_HOMEASSISTANT_STOP
-from homeassistant.helpers.typing import UNDEFINED
+from homeassistant.const import (
+ CONF_API_KEY,
+ CONF_HOST,
+ CONF_PORT,
+ EVENT_HOMEASSISTANT_STOP,
+)
+from homeassistant.core import callback
+from homeassistant.helpers.entity_registry import async_migrate_entries
from .config_flow import get_master_gateway
-from .const import CONF_BRIDGE_ID, CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN
+from .const import CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN
from .gateway import DeconzGateway
from .services import async_setup_services, async_unload_services
@@ -14,11 +20,6 @@
)
-async def async_setup(hass, config):
- """Old way of setting up deCONZ integrations."""
- return True
-
-
async def async_setup_entry(hass, config_entry):
"""Set up a deCONZ bridge for a config entry.
@@ -28,6 +29,8 @@ async def async_setup_entry(hass, config_entry):
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
+ await async_update_group_unique_id(hass, config_entry)
+
if not config_entry.options:
await async_update_master_gateway(hass, config_entry)
@@ -36,18 +39,6 @@ async def async_setup_entry(hass, config_entry):
if not await gateway.async_setup():
return False
- # 0.104 introduced config entry unique id, this makes upgrading possible
- if config_entry.unique_id is None:
-
- new_data = UNDEFINED
- if CONF_BRIDGE_ID in config_entry.data:
- new_data = dict(config_entry.data)
- new_data[CONF_GROUP_ID_BASE] = config_entry.data[CONF_BRIDGE_ID]
-
- hass.config_entries.async_update_entry(
- config_entry, unique_id=gateway.api.config.bridgeid, data=new_data
- )
-
hass.data[DOMAIN][config_entry.unique_id] = gateway
await gateway.async_update_device_registry()
@@ -84,3 +75,30 @@ async def async_update_master_gateway(hass, config_entry):
options = {**config_entry.options, CONF_MASTER_GATEWAY: master}
hass.config_entries.async_update_entry(config_entry, options=options)
+
+
+async def async_update_group_unique_id(hass, config_entry) -> None:
+ """Update unique ID entities based on deCONZ groups."""
+ if not (old_unique_id := config_entry.data.get(CONF_GROUP_ID_BASE)):
+ return
+
+ new_unique_id: str = config_entry.unique_id
+
+ @callback
+ def update_unique_id(entity_entry):
+ """Update unique ID of entity entry."""
+ if f"{old_unique_id}-" not in entity_entry.unique_id:
+ return None
+ return {
+ "new_unique_id": entity_entry.unique_id.replace(
+ old_unique_id, new_unique_id
+ )
+ }
+
+ await async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
+ data = {
+ CONF_API_KEY: config_entry.data[CONF_API_KEY],
+ CONF_HOST: config_entry.data[CONF_HOST],
+ CONF_PORT: config_entry.data[CONF_PORT],
+ }
+ hass.config_entries.async_update_entry(config_entry, data=data)
diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py
index 616206949edd32..99f559eec3d837 100644
--- a/homeassistant/components/deconz/binary_sensor.py
+++ b/homeassistant/components/deconz/binary_sensor.py
@@ -92,7 +92,7 @@ def device_class(self):
return DEVICE_CLASS.get(type(self._device))
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
attr = {}
diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py
index 98e3864e191b9c..49f0cc4d1499bd 100644
--- a/homeassistant/components/deconz/climate.py
+++ b/homeassistant/components/deconz/climate.py
@@ -1,5 +1,5 @@
"""Support for deCONZ climate devices."""
-from typing import Optional
+from __future__ import annotations
from pydeconz.sensor import Thermostat
@@ -26,7 +26,7 @@
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from .const import ATTR_OFFSET, ATTR_VALVE, NEW_SENSOR
+from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE, NEW_SENSOR
from .deconz_device import DeconzDevice
from .gateway import get_gateway_from_config_entry
@@ -195,7 +195,7 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None:
# Preset control
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return preset mode."""
return DECONZ_TO_PRESET_MODE.get(self._device.preset)
@@ -244,7 +244,7 @@ def temperature_unit(self):
return TEMP_CELSIUS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the thermostat."""
attr = {}
@@ -254,4 +254,7 @@ def device_state_attributes(self):
if self._device.valve is not None:
attr[ATTR_VALVE] = self._device.valve
+ if self._device.locked is not None:
+ attr[ATTR_LOCKED] = self._device.locked
+
return attr
diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py
index bc14af9ff11015..d1ea3826e2fa6f 100644
--- a/homeassistant/components/deconz/config_flow.py
+++ b/homeassistant/components/deconz/config_flow.py
@@ -176,7 +176,6 @@ async def _create_entry(self):
async def async_step_reauth(self, config: dict):
"""Trigger a reauthentication flow."""
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {CONF_HOST: config[CONF_HOST]}
self.deconz_config = {
@@ -207,7 +206,6 @@ async def async_step_ssdp(self, discovery_info):
updates={CONF_HOST: parsed_url.hostname, CONF_PORT: parsed_url.port}
)
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {"host": parsed_url.hostname}
self.deconz_config = {
diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py
index cbad37b1b87eca..5ed1def66c284d 100644
--- a/homeassistant/components/deconz/const.py
+++ b/homeassistant/components/deconz/const.py
@@ -28,7 +28,7 @@
CONF_ALLOW_NEW_DEVICES = "allow_new_devices"
CONF_MASTER_GATEWAY = "master"
-SUPPORTED_PLATFORMS = [
+PLATFORMS = [
BINARY_SENSOR_DOMAIN,
CLIMATE_DOMAIN,
COVER_DOMAIN,
@@ -46,6 +46,7 @@
NEW_SENSOR = "sensors"
ATTR_DARK = "dark"
+ATTR_LOCKED = "locked"
ATTR_OFFSET = "offset"
ATTR_ON = "on"
ATTR_VALVE = "valve"
@@ -59,7 +60,7 @@
FANS = ["Fan"]
# Locks
-LOCKS = ["Door Lock"]
+LOCKS = ["Door Lock", "ZHADoorLock"]
LOCK_TYPES = LOCKS
# Switches
diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py
index 6e57d08302a831..301d17535917e6 100644
--- a/homeassistant/components/deconz/cover.py
+++ b/homeassistant/components/deconz/cover.py
@@ -2,7 +2,8 @@
from homeassistant.components.cover import (
ATTR_POSITION,
ATTR_TILT_POSITION,
- DEVICE_CLASS_WINDOW,
+ DEVICE_CLASS_DAMPER,
+ DEVICE_CLASS_SHADE,
DOMAIN,
SUPPORT_CLOSE,
SUPPORT_CLOSE_TILT,
@@ -80,9 +81,9 @@ def supported_features(self):
def device_class(self):
"""Return the class of the cover."""
if self._device.type in DAMPERS:
- return "damper"
+ return DEVICE_CLASS_DAMPER
if self._device.type in WINDOW_COVERS:
- return DEVICE_CLASS_WINDOW
+ return DEVICE_CLASS_SHADE
@property
def current_cover_position(self):
diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py
index 81d3aa94d31771..706850477d8a15 100644
--- a/homeassistant/components/deconz/deconz_event.py
+++ b/homeassistant/components/deconz/deconz_event.py
@@ -106,8 +106,11 @@ def async_update_callback(self, force_update=False):
self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data)
- async def async_update_device_registry(self):
+ async def async_update_device_registry(self) -> None:
"""Update device registry."""
+ if not self.device_info:
+ return
+
device_registry = (
await self.gateway.hass.helpers.device_registry.async_get_registry()
)
diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py
index 5ee0a00f04fb8e..2703adbc139523 100644
--- a/homeassistant/components/deconz/device_trigger.py
+++ b/homeassistant/components/deconz/device_trigger.py
@@ -16,7 +16,6 @@
)
from . import DOMAIN
-from .const import LOGGER
from .deconz_event import CONF_DECONZ_EVENT, CONF_GESTURE
CONF_SUBTYPE = "subtype"
@@ -65,6 +64,10 @@
CONF_BUTTON_2 = "button_2"
CONF_BUTTON_3 = "button_3"
CONF_BUTTON_4 = "button_4"
+CONF_BUTTON_5 = "button_5"
+CONF_BUTTON_6 = "button_6"
+CONF_BUTTON_7 = "button_7"
+CONF_BUTTON_8 = "button_8"
CONF_SIDE_1 = "side_1"
CONF_SIDE_2 = "side_2"
CONF_SIDE_3 = "side_3"
@@ -139,6 +142,22 @@
(CONF_LONG_RELEASE, CONF_BOTTOM_BUTTONS): {CONF_EVENT: 6003},
}
+STYRBAR_REMOTE_MODEL = "Remote Control N2"
+STYRBAR_REMOTE = {
+ (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002},
+ (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001},
+ (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003},
+ (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002},
+ (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 2001},
+ (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003},
+ (CONF_SHORT_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3002},
+ (CONF_LONG_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 3001},
+ (CONF_LONG_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3003},
+ (CONF_SHORT_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4002},
+ (CONF_LONG_PRESS, CONF_TURN_OFF): {CONF_EVENT: 4001},
+ (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4003},
+}
+
SYMFONISK_SOUND_CONTROLLER_MODEL = "SYMFONISK Sound Controller"
SYMFONISK_SOUND_CONTROLLER = {
(CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1002},
@@ -271,6 +290,21 @@
(CONF_SHORT_PRESS, CONF_BOTH_BUTTONS): {CONF_EVENT: 3002},
}
+AQARA_DOUBLE_WALL_SWITCH_QBKG12LM_MODEL = "lumi.ctrl_ln2.aq1"
+AQARA_DOUBLE_WALL_SWITCH_QBKG12LM = {
+ (CONF_SHORT_PRESS, CONF_LEFT): {CONF_EVENT: 1002},
+ (CONF_DOUBLE_PRESS, CONF_LEFT): {CONF_EVENT: 1004},
+ (CONF_SHORT_PRESS, CONF_RIGHT): {CONF_EVENT: 2002},
+ (CONF_DOUBLE_PRESS, CONF_RIGHT): {CONF_EVENT: 2004},
+ (CONF_SHORT_PRESS, CONF_BOTH_BUTTONS): {CONF_EVENT: 3002},
+}
+
+AQARA_SINGLE_WALL_SWITCH_QBKG11LM_MODEL = "lumi.ctrl_ln1.aq1"
+AQARA_SINGLE_WALL_SWITCH_QBKG11LM = {
+ (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1002},
+ (CONF_DOUBLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1004},
+}
+
AQARA_SINGLE_WALL_SWITCH_WXKG03LM_MODEL = "lumi.remote.b186acn01"
AQARA_SINGLE_WALL_SWITCH_WXKG06LM_MODEL = "lumi.remote.b186acn02"
AQARA_SINGLE_WALL_SWITCH = {
@@ -287,6 +321,7 @@
(CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003},
}
+
AQARA_ROUND_SWITCH_MODEL = "lumi.sensor_switch"
AQARA_ROUND_SWITCH = {
(CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1000},
@@ -360,6 +395,133 @@
(CONF_TRIPLE_PRESS, CONF_RIGHT): {CONF_EVENT: 6005},
}
+DRESDEN_ELEKTRONIK_LIGHTING_SWITCH_MODEL = "Lighting Switch"
+DRESDEN_ELEKTRONIK_LIGHTING_SWITCH = {
+ (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002},
+ (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001},
+ (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003},
+ (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002},
+ (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 2001},
+ (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003},
+ (CONF_SHORT_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3002},
+ (CONF_LONG_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 3001},
+ (CONF_LONG_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3003},
+ (CONF_SHORT_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4002},
+ (CONF_LONG_PRESS, CONF_TURN_OFF): {CONF_EVENT: 4001},
+ (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4003},
+}
+
+DRESDEN_ELEKTRONIK_SCENE_SWITCH_MODEL = "Scene Switch"
+DRESDEN_ELEKTRONIK_SCENE_SWITCH = {
+ (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002},
+ (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001},
+ (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003},
+ (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002},
+ (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 2001},
+ (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 3002},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 4002},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 5002},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 6002},
+}
+
+GIRA_JUNG_SWITCH_MODEL = "HS_4f_GJ_1"
+GIRA_SWITCH_MODEL = "WS_4f_J_1"
+JUNG_SWITCH_MODEL = "WS_3f_G_1"
+GIRA_JUNG_SWITCH = {
+ (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002},
+ (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001},
+ (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003},
+ (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002},
+ (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 2001},
+ (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3002},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4002},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_5): {CONF_EVENT: 5002},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_6): {CONF_EVENT: 6002},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_7): {CONF_EVENT: 7002},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_8): {CONF_EVENT: 8002},
+}
+
+LIGHTIFIY_FOUR_BUTTON_REMOTE_MODEL = "Switch-LIGHTIFY"
+LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_MODEL = "Switch 4x-LIGHTIFY"
+LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_EU_MODEL = "Switch 4x EU-LIGHTIFY"
+LIGHTIFIY_FOUR_BUTTON_REMOTE = {
+ (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002},
+ (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001},
+ (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003},
+ (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002},
+ (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 2001},
+ (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003},
+ (CONF_SHORT_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3002},
+ (CONF_LONG_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 3001},
+ (CONF_LONG_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3003},
+ (CONF_SHORT_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4002},
+ (CONF_LONG_PRESS, CONF_TURN_OFF): {CONF_EVENT: 4001},
+ (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4003},
+}
+
+BUSCH_JAEGER_REMOTE_1_MODEL = "RB01"
+BUSCH_JAEGER_REMOTE_2_MODEL = "RM01"
+BUSCH_JAEGER_REMOTE = {
+ (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1002},
+ (CONF_LONG_PRESS, CONF_BUTTON_1): {CONF_EVENT: 1001},
+ (CONF_LONG_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1003},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2002},
+ (CONF_LONG_PRESS, CONF_BUTTON_2): {CONF_EVENT: 2001},
+ (CONF_LONG_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2003},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3002},
+ (CONF_LONG_PRESS, CONF_BUTTON_3): {CONF_EVENT: 3001},
+ (CONF_LONG_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3003},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4002},
+ (CONF_LONG_PRESS, CONF_BUTTON_4): {CONF_EVENT: 4001},
+ (CONF_LONG_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4003},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_5): {CONF_EVENT: 5002},
+ (CONF_LONG_PRESS, CONF_BUTTON_5): {CONF_EVENT: 5001},
+ (CONF_LONG_RELEASE, CONF_BUTTON_5): {CONF_EVENT: 5003},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_6): {CONF_EVENT: 6002},
+ (CONF_LONG_PRESS, CONF_BUTTON_6): {CONF_EVENT: 6001},
+ (CONF_LONG_RELEASE, CONF_BUTTON_6): {CONF_EVENT: 6003},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_7): {CONF_EVENT: 7002},
+ (CONF_LONG_PRESS, CONF_BUTTON_7): {CONF_EVENT: 7001},
+ (CONF_LONG_RELEASE, CONF_BUTTON_7): {CONF_EVENT: 7003},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_8): {CONF_EVENT: 8002},
+ (CONF_LONG_PRESS, CONF_BUTTON_8): {CONF_EVENT: 8001},
+ (CONF_LONG_RELEASE, CONF_BUTTON_8): {CONF_EVENT: 8003},
+}
+
+TRUST_ZYCT_202_MODEL = "ZYCT-202"
+TRUST_ZYCT_202_ZLL_MODEL = "ZLL-NonColorController"
+TRUST_ZYCT_202 = {
+ (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1002},
+ (CONF_LONG_PRESS, CONF_BUTTON_2): {CONF_EVENT: 2001},
+ (CONF_LONG_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2003},
+ (CONF_LONG_PRESS, CONF_BUTTON_3): {CONF_EVENT: 3001},
+ (CONF_LONG_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3003},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4002},
+}
+
+UBISYS_POWER_SWITCH_S2_MODEL = "S2"
+UBISYS_POWER_SWITCH_S2 = {
+ (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1002},
+ (CONF_LONG_PRESS, CONF_BUTTON_1): {CONF_EVENT: 1001},
+ (CONF_LONG_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1003},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2002},
+ (CONF_LONG_PRESS, CONF_BUTTON_2): {CONF_EVENT: 2001},
+ (CONF_LONG_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2003},
+}
+
+UBISYS_CONTROL_UNIT_C4_MODEL = "C4"
+UBISYS_CONTROL_UNIT_C4 = {
+ **UBISYS_POWER_SWITCH_S2,
+ (CONF_SHORT_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3002},
+ (CONF_LONG_PRESS, CONF_BUTTON_3): {CONF_EVENT: 3001},
+ (CONF_LONG_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3003},
+ (CONF_SHORT_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4002},
+ (CONF_LONG_PRESS, CONF_BUTTON_4): {CONF_EVENT: 4001},
+ (CONF_LONG_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4003},
+}
+
REMOTES = {
HUE_DIMMER_REMOTE_MODEL_GEN1: HUE_DIMMER_REMOTE,
HUE_DIMMER_REMOTE_MODEL_GEN2: HUE_DIMMER_REMOTE,
@@ -367,6 +529,7 @@
HUE_BUTTON_REMOTE_MODEL: HUE_BUTTON_REMOTE,
HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE,
FRIENDS_OF_HUE_SWITCH_MODEL: FRIENDS_OF_HUE_SWITCH,
+ STYRBAR_REMOTE_MODEL: STYRBAR_REMOTE,
SYMFONISK_SOUND_CONTROLLER_MODEL: SYMFONISK_SOUND_CONTROLLER,
TRADFRI_ON_OFF_SWITCH_MODEL: TRADFRI_ON_OFF_SWITCH,
TRADFRI_OPEN_CLOSE_REMOTE_MODEL: TRADFRI_OPEN_CLOSE_REMOTE,
@@ -377,6 +540,8 @@
AQARA_DOUBLE_WALL_SWITCH_MODEL: AQARA_DOUBLE_WALL_SWITCH,
AQARA_DOUBLE_WALL_SWITCH_MODEL_2020: AQARA_DOUBLE_WALL_SWITCH,
AQARA_DOUBLE_WALL_SWITCH_WXKG02LM_MODEL: AQARA_DOUBLE_WALL_SWITCH_WXKG02LM,
+ AQARA_DOUBLE_WALL_SWITCH_QBKG12LM_MODEL: AQARA_DOUBLE_WALL_SWITCH_QBKG12LM,
+ AQARA_SINGLE_WALL_SWITCH_QBKG11LM_MODEL: AQARA_SINGLE_WALL_SWITCH_QBKG11LM,
AQARA_SINGLE_WALL_SWITCH_WXKG03LM_MODEL: AQARA_SINGLE_WALL_SWITCH,
AQARA_SINGLE_WALL_SWITCH_WXKG06LM_MODEL: AQARA_SINGLE_WALL_SWITCH,
AQARA_MINI_SWITCH_MODEL: AQARA_MINI_SWITCH,
@@ -386,6 +551,20 @@
AQARA_OPPLE_2_BUTTONS_MODEL: AQARA_OPPLE_2_BUTTONS,
AQARA_OPPLE_4_BUTTONS_MODEL: AQARA_OPPLE_4_BUTTONS,
AQARA_OPPLE_6_BUTTONS_MODEL: AQARA_OPPLE_6_BUTTONS,
+ DRESDEN_ELEKTRONIK_LIGHTING_SWITCH_MODEL: DRESDEN_ELEKTRONIK_LIGHTING_SWITCH,
+ DRESDEN_ELEKTRONIK_SCENE_SWITCH_MODEL: DRESDEN_ELEKTRONIK_SCENE_SWITCH,
+ GIRA_JUNG_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL,
+ GIRA_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL,
+ JUNG_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL,
+ LIGHTIFIY_FOUR_BUTTON_REMOTE_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE,
+ LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE,
+ LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_EU_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE,
+ BUSCH_JAEGER_REMOTE_1_MODEL: BUSCH_JAEGER_REMOTE,
+ BUSCH_JAEGER_REMOTE_2_MODEL: BUSCH_JAEGER_REMOTE,
+ TRUST_ZYCT_202_MODEL: TRUST_ZYCT_202,
+ TRUST_ZYCT_202_ZLL_MODEL: TRUST_ZYCT_202,
+ UBISYS_POWER_SWITCH_S2_MODEL: UBISYS_POWER_SWITCH_S2,
+ UBISYS_CONTROL_UNIT_C4_MODEL: UBISYS_CONTROL_UNIT_C4,
}
TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
@@ -414,16 +593,13 @@ async def async_validate_trigger_config(hass, config):
trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
- if (
- not device
- or device.model not in REMOTES
- or trigger not in REMOTES[device.model]
- ):
- if not device:
- raise InvalidDeviceAutomationConfig(
- f"deCONZ trigger {trigger} device with id "
- f"{config[CONF_DEVICE_ID]} not found"
- )
+ if not device:
+ raise InvalidDeviceAutomationConfig(
+ f"deCONZ trigger {trigger} device with ID "
+ f"{config[CONF_DEVICE_ID]} not found"
+ )
+
+ if device.model not in REMOTES or trigger not in REMOTES[device.model]:
raise InvalidDeviceAutomationConfig(
f"deCONZ trigger {trigger} is not valid for device "
f"{device} ({config[CONF_DEVICE_ID]})"
@@ -443,8 +619,9 @@ async def async_attach_trigger(hass, config, action, automation_info):
deconz_event = _get_deconz_event_from_device_id(hass, device.id)
if deconz_event is None:
- LOGGER.error("No deconz_event tied to device %s found", device.name)
- raise InvalidDeviceAutomationConfig
+ raise InvalidDeviceAutomationConfig(
+ f'No deconz_event tied to device "{device.name}" found'
+ )
event_id = deconz_event.serial
diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py
index 1ca4c8ff9c2845..aca92f893c7bd1 100644
--- a/homeassistant/components/deconz/fan.py
+++ b/homeassistant/components/deconz/fan.py
@@ -1,4 +1,6 @@
-"""Support for deCONZ switches."""
+"""Support for deCONZ fans."""
+from __future__ import annotations
+
from homeassistant.components.fan import (
DOMAIN,
SPEED_HIGH,
@@ -10,25 +12,19 @@
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.util.percentage import (
+ ordered_list_item_to_percentage,
+ percentage_to_ordered_list_item,
+)
from .const import FANS, NEW_LIGHT
from .deconz_device import DeconzDevice
from .gateway import get_gateway_from_config_entry
-SPEEDS = {SPEED_OFF: 0, SPEED_LOW: 1, SPEED_MEDIUM: 2, SPEED_HIGH: 4}
-SUPPORTED_ON_SPEEDS = {1: SPEED_LOW, 2: SPEED_MEDIUM, 4: SPEED_HIGH}
-
+ORDERED_NAMED_FAN_SPEEDS = [1, 2, 3, 4]
-def convert_speed(speed: int) -> str:
- """Convert speed from deCONZ to HASS.
-
- Fallback to medium speed if unsupported by HASS fan platform.
- """
- if speed in SPEEDS.values():
- for hass_speed, deconz_speed in SPEEDS.items():
- if speed == deconz_speed:
- return hass_speed
- return SPEED_MEDIUM
+LEGACY_SPEED_TO_DECONZ = {SPEED_OFF: 0, SPEED_LOW: 1, SPEED_MEDIUM: 2, SPEED_HIGH: 4}
+LEGACY_DECONZ_TO_SPEED = {0: SPEED_OFF, 1: SPEED_LOW, 2: SPEED_MEDIUM, 4: SPEED_HIGH}
async def async_setup_entry(hass, config_entry, async_add_entities) -> None:
@@ -67,8 +63,8 @@ def __init__(self, device, gateway) -> None:
"""Set up fan."""
super().__init__(device, gateway)
- self._default_on_speed = SPEEDS[SPEED_MEDIUM]
- if self.speed != SPEED_OFF:
+ self._default_on_speed = 2
+ if self._device.speed in ORDERED_NAMED_FAN_SPEEDS:
self._default_on_speed = self._device.speed
self._features = SUPPORT_SET_SPEED
@@ -76,17 +72,58 @@ def __init__(self, device, gateway) -> None:
@property
def is_on(self) -> bool:
"""Return true if fan is on."""
- return self.speed != SPEED_OFF
+ return self._device.speed != 0
@property
- def speed(self) -> int:
- """Return the current speed."""
- return convert_speed(self._device.speed)
+ def percentage(self) -> int | None:
+ """Return the current speed percentage."""
+ if self._device.speed == 0:
+ return 0
+ if self._device.speed not in ORDERED_NAMED_FAN_SPEEDS:
+ return None
+ return ordered_list_item_to_percentage(
+ ORDERED_NAMED_FAN_SPEEDS, self._device.speed
+ )
+
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return len(ORDERED_NAMED_FAN_SPEEDS)
@property
def speed_list(self) -> list:
- """Get the list of available speeds."""
- return list(SPEEDS)
+ """Get the list of available speeds.
+
+ Legacy fan support.
+ """
+ return list(LEGACY_SPEED_TO_DECONZ)
+
+ def speed_to_percentage(self, speed: str) -> int:
+ """Convert speed to percentage.
+
+ Legacy fan support.
+ """
+ if speed == SPEED_OFF:
+ return 0
+
+ if speed not in LEGACY_SPEED_TO_DECONZ:
+ speed = SPEED_MEDIUM
+
+ return ordered_list_item_to_percentage(
+ ORDERED_NAMED_FAN_SPEEDS, LEGACY_SPEED_TO_DECONZ[speed]
+ )
+
+ def percentage_to_speed(self, percentage: int) -> str:
+ """Convert percentage to speed.
+
+ Legacy fan support.
+ """
+ if percentage == 0:
+ return SPEED_OFF
+ return LEGACY_DECONZ_TO_SPEED.get(
+ percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage),
+ SPEED_MEDIUM,
+ )
@property
def supported_features(self) -> int:
@@ -96,24 +133,26 @@ def supported_features(self) -> int:
@callback
def async_update_callback(self, force_update=False) -> None:
"""Store latest configured speed from the device."""
- if self.speed != SPEED_OFF and self._device.speed != self._default_on_speed:
+ if self._device.speed in ORDERED_NAMED_FAN_SPEEDS:
self._default_on_speed = self._device.speed
super().async_update_callback(force_update)
+ async def async_set_percentage(self, percentage: int) -> None:
+ """Set the speed percentage of the fan."""
+ await self._device.set_speed(
+ percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage)
+ )
+
async def async_set_speed(self, speed: str) -> None:
- """Set the speed of the fan."""
- if speed not in SPEEDS:
+ """Set the speed of the fan.
+
+ Legacy fan support.
+ """
+ if speed not in LEGACY_SPEED_TO_DECONZ:
raise ValueError(f"Unsupported speed {speed}")
- await self._device.set_speed(SPEEDS[speed])
+ await self._device.set_speed(LEGACY_SPEED_TO_DECONZ[speed])
- #
- # The fan entity model has changed to use percentages and preset_modes
- # instead of speeds.
- #
- # Please review
- # https://developers.home-assistant.io/docs/core/entity/fan/
- #
async def async_turn_on(
self,
speed: str = None,
@@ -122,10 +161,15 @@ async def async_turn_on(
**kwargs,
) -> None:
"""Turn on fan."""
- if not speed:
- speed = convert_speed(self._default_on_speed)
- await self.async_set_speed(speed)
+ new_speed = self._default_on_speed
+
+ if percentage is not None:
+ new_speed = percentage_to_ordered_list_item(
+ ORDERED_NAMED_FAN_SPEEDS, percentage
+ )
+
+ await self._device.set_speed(new_speed)
async def async_turn_off(self, **kwargs) -> None:
"""Turn off fan."""
- await self.async_set_speed(SPEED_OFF)
+ await self._device.set_speed(0)
diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py
index a6cbb2acef9582..2b38f6956beb7b 100644
--- a/homeassistant/components/deconz/gateway.py
+++ b/homeassistant/components/deconz/gateway.py
@@ -26,7 +26,7 @@
NEW_LIGHT,
NEW_SCENE,
NEW_SENSOR,
- SUPPORTED_PLATFORMS,
+ PLATFORMS,
)
from .deconz_event import async_setup_events, async_unload_events
from .errors import AuthenticationRequired, CannotConnect
@@ -184,10 +184,10 @@ async def async_setup(self) -> bool:
)
return False
- for component in SUPPORTED_PLATFORMS:
+ for platform in PLATFORMS:
self.hass.async_create_task(
self.hass.config_entries.async_forward_entry_setup(
- self.config_entry, component
+ self.config_entry, platform
)
)
@@ -259,9 +259,9 @@ async def async_reset(self):
self.api.async_connection_status_callback = None
self.api.close()
- for component in SUPPORTED_PLATFORMS:
+ for platform in PLATFORMS:
await self.hass.config_entries.async_forward_entry_unload(
- self.config_entry, component
+ self.config_entry, platform
)
for unsub_dispatcher in self.listeners:
diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py
index 9080160c76fe19..f7ae45781acc85 100644
--- a/homeassistant/components/deconz/light.py
+++ b/homeassistant/components/deconz/light.py
@@ -26,7 +26,6 @@
import homeassistant.util.color as color_util
from .const import (
- CONF_GROUP_ID_BASE,
COVER_TYPES,
DOMAIN as DECONZ_DOMAIN,
LOCK_TYPES,
@@ -224,7 +223,7 @@ async def async_turn_off(self, **kwargs):
await self._device.async_set_state(data)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
return {"is_deconz_group": self._device.type == "LightGroup"}
@@ -248,10 +247,7 @@ class DeconzGroup(DeconzBaseLight):
def __init__(self, device, gateway):
"""Set up group and create an unique id."""
- group_id_base = gateway.config_entry.unique_id
- if CONF_GROUP_ID_BASE in gateway.config_entry.data:
- group_id_base = gateway.config_entry.data[CONF_GROUP_ID_BASE]
- self._unique_id = f"{group_id_base}-{device.deconz_id}"
+ self._unique_id = f"{gateway.bridgeid}-{device.deconz_id}"
super().__init__(device, gateway)
@@ -279,9 +275,9 @@ def device_info(self):
}
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
- attributes = dict(super().device_state_attributes)
+ attributes = dict(super().extra_state_attributes)
attributes["all_on"] = self._device.all_on
return attributes
diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py
index 4d428af36734dd..4b6da1e0b97198 100644
--- a/homeassistant/components/deconz/lock.py
+++ b/homeassistant/components/deconz/lock.py
@@ -3,7 +3,7 @@
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from .const import LOCKS, NEW_LIGHT
+from .const import LOCKS, NEW_LIGHT, NEW_SENSOR
from .deconz_device import DeconzDevice
from .gateway import get_gateway_from_config_entry
@@ -14,7 +14,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
gateway.entities[DOMAIN] = set()
@callback
- def async_add_lock(lights=gateway.api.lights.values()):
+ def async_add_lock_from_light(lights=gateway.api.lights.values()):
"""Add lock from deCONZ."""
entities = []
@@ -28,11 +28,33 @@ def async_add_lock(lights=gateway.api.lights.values()):
gateway.listeners.append(
async_dispatcher_connect(
- hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_lock
+ hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_lock_from_light
)
)
- async_add_lock()
+ @callback
+ def async_add_lock_from_sensor(sensors=gateway.api.sensors.values()):
+ """Add lock from deCONZ."""
+ entities = []
+
+ for sensor in sensors:
+
+ if sensor.type in LOCKS and sensor.uniqueid not in gateway.entities[DOMAIN]:
+ entities.append(DeconzLock(sensor, gateway))
+
+ if entities:
+ async_add_entities(entities)
+
+ gateway.listeners.append(
+ async_dispatcher_connect(
+ hass,
+ gateway.async_signal_new_device(NEW_SENSOR),
+ async_add_lock_from_sensor,
+ )
+ )
+
+ async_add_lock_from_light()
+ async_add_lock_from_sensor()
class DeconzLock(DeconzDevice, LockEntity):
diff --git a/homeassistant/components/deconz/logbook.py b/homeassistant/components/deconz/logbook.py
index 73c157ac8f67ff..e6f3e9362cd3b3 100644
--- a/homeassistant/components/deconz/logbook.py
+++ b/homeassistant/components/deconz/logbook.py
@@ -1,12 +1,13 @@
"""Describe deCONZ logbook events."""
+from __future__ import annotations
-from typing import Callable, Optional
+from typing import Callable
from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.event import Event
-from .const import DOMAIN as DECONZ_DOMAIN
+from .const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN
from .deconz_event import CONF_DECONZ_EVENT, DeconzEvent
from .device_trigger import (
CONF_BOTH_BUTTONS,
@@ -107,8 +108,12 @@ def _get_device_event_description(modelid: str, event: str) -> tuple:
device_event_descriptions: dict = REMOTES[modelid]
for event_type_tuple, event_dict in device_event_descriptions.items():
- if event == event_dict[CONF_EVENT]:
+ if event == event_dict.get(CONF_EVENT):
return event_type_tuple
+ if event == event_dict.get(CONF_GESTURE):
+ return event_type_tuple
+
+ return (None, None)
@callback
@@ -121,19 +126,39 @@ def async_describe_events(
@callback
def async_describe_deconz_event(event: Event) -> dict:
"""Describe deCONZ logbook event."""
- deconz_event: Optional[DeconzEvent] = _get_deconz_event_from_device_id(
+ deconz_event: DeconzEvent | None = _get_deconz_event_from_device_id(
hass, event.data[ATTR_DEVICE_ID]
)
- if deconz_event.device.modelid not in REMOTES:
+ action = None
+ interface = None
+ data = event.data.get(CONF_EVENT) or event.data.get(CONF_GESTURE, "")
+
+ if data and deconz_event.device.modelid in REMOTES:
+ action, interface = _get_device_event_description(
+ deconz_event.device.modelid, data
+ )
+
+ # Unknown event
+ if not data:
return {
"name": f"{deconz_event.device.name}",
- "message": f"fired event '{event.data[CONF_EVENT]}'.",
+ "message": "fired an unknown event.",
}
- action, interface = _get_device_event_description(
- deconz_event.device.modelid, event.data[CONF_EVENT]
- )
+ # No device event match
+ if not action:
+ return {
+ "name": f"{deconz_event.device.name}",
+ "message": f"fired event '{data}'.",
+ }
+
+ # Gesture event
+ if not interface:
+ return {
+ "name": f"{deconz_event.device.name}",
+ "message": f"fired event '{ACTIONS[action]}'.",
+ }
return {
"name": f"{deconz_event.device.name}",
diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json
index 22711b84b9d0ec..5cce88589104b2 100644
--- a/homeassistant/components/deconz/manifest.json
+++ b/homeassistant/components/deconz/manifest.json
@@ -3,7 +3,7 @@
"name": "deCONZ",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/deconz",
- "requirements": ["pydeconz==77"],
+ "requirements": ["pydeconz==78"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics"
diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py
index 9d71fd0a9f9870..a38b7cb20aa3a7 100644
--- a/homeassistant/components/deconz/sensor.py
+++ b/homeassistant/components/deconz/sensor.py
@@ -3,6 +3,7 @@
Battery,
Consumption,
Daylight,
+ DoorLock,
Humidity,
LightLevel,
Power,
@@ -12,7 +13,7 @@
Thermostat,
)
-from homeassistant.components.sensor import DOMAIN
+from homeassistant.components.sensor import DOMAIN, SensorEntity
from homeassistant.const import (
ATTR_TEMPERATURE,
ATTR_VOLTAGE,
@@ -103,7 +104,10 @@ def async_add_sensor(sensors=gateway.api.sensors.values()):
if (
not sensor.BINARY
and sensor.type
- not in Battery.ZHATYPE + Switch.ZHATYPE + Thermostat.ZHATYPE
+ not in Battery.ZHATYPE
+ + DoorLock.ZHATYPE
+ + Switch.ZHATYPE
+ + Thermostat.ZHATYPE
and sensor.uniqueid not in gateway.entities[DOMAIN]
):
entities.append(DeconzSensor(sensor, gateway))
@@ -122,7 +126,7 @@ def async_add_sensor(sensors=gateway.api.sensors.values()):
)
-class DeconzSensor(DeconzDevice):
+class DeconzSensor(DeconzDevice, SensorEntity):
"""Representation of a deCONZ sensor."""
TYPE = DOMAIN
@@ -155,7 +159,7 @@ def unit_of_measurement(self):
return UNIT_OF_MEASUREMENT.get(type(self._device))
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
attr = {}
@@ -186,7 +190,7 @@ def device_state_attributes(self):
return attr
-class DeconzBattery(DeconzDevice):
+class DeconzBattery(DeconzDevice, SensorEntity):
"""Battery class for when a device is only represented as an event."""
TYPE = DOMAIN
@@ -200,7 +204,18 @@ def async_update_callback(self, force_update=False):
@property
def unique_id(self):
- """Return a unique identifier for this device."""
+ """Return a unique identifier for this device.
+
+ Normally there should only be one battery sensor per device from deCONZ.
+ With specific Danfoss devices each endpoint can report its own battery state.
+ """
+ if self._device.manufacturer == "Danfoss" and self._device.modelid in [
+ "0x8030",
+ "0x8031",
+ "0x8034",
+ "0x8035",
+ ]:
+ return f"{super().unique_id}-battery"
return f"{self.serial}-battery"
@property
@@ -224,7 +239,7 @@ def unit_of_measurement(self):
return PERCENTAGE
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the battery."""
attr = {}
diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml
index 9d85e76d8d3a53..3bce097f7d3912 100644
--- a/homeassistant/components/deconz/services.yaml
+++ b/homeassistant/components/deconz/services.yaml
@@ -1,32 +1,66 @@
configure:
- description: Set attribute of device in deCONZ. See https://home-assistant.io/integrations/deconz/#device-services for details.
+ name: Configure
+ description: >-
+ Configure attributes of either a device endpoint in deCONZ
+ or the deCONZ service itself.
fields:
entity:
- description: Entity id representing a specific device in deCONZ.
+ name: Entity
+ description: Represents a specific device endpoint in deCONZ.
example: "light.rgb_light"
+ selector:
+ entity:
+ integration: deconz
field:
+ name: Path
+ selector:
+ text:
description: >-
- Field is a string representing a full path to deCONZ endpoint (when
+ String representing a full path to deCONZ endpoint (when
entity is not specified) or a subpath of the device path for the
entity (when entity is specified).
example: '"/lights/1/state" or "/state"'
data:
- description: Data is a json object with what data you want to alter.
+ name: Configuration payload
+ required: true
+ selector:
+ object:
+ description: JSON object with what data you want to alter.
example: '{"on": true}'
bridgeid:
- description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name.
+ name: Bridge identifier
+ selector:
+ text:
+ description: >-
+ Unique string for each deCONZ hardware.
+ It can be found as part of the integration name.
+ Useful if you run multiple deCONZ integrations.
example: "00212EFFFF012345"
device_refresh:
- description: Refresh device lists from deCONZ.
+ name: Device refresh
+ description: Refresh available devices from deCONZ.
fields:
bridgeid:
- description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name.
+ name: Bridge identifier
+ selector:
+ text:
+ description: >-
+ Unique string for each deCONZ hardware.
+ It can be found as part of the integration name.
+ Useful if you run multiple deCONZ integrations.
example: "00212EFFFF012345"
remove_orphaned_entries:
+ name: Remove orphaned entries
description: Clean up device and entity registry entries orphaned by deCONZ.
fields:
bridgeid:
- description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name.
+ name: Bridge identifier
+ selector:
+ text:
+ description: >-
+ Unique string for each deCONZ hardware.
+ It can be found as part of the integration name.
+ Useful if you run multiple deCONZ integrations.
example: "00212EFFFF012345"
diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json
index bbda4e8c0cb532..fbb321959c16c3 100644
--- a/homeassistant/components/deconz/strings.json
+++ b/homeassistant/components/deconz/strings.json
@@ -18,8 +18,8 @@
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button"
},
"hassio_confirm": {
- "title": "deCONZ Zigbee gateway via Hass.io add-on",
- "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?"
+ "title": "deCONZ Zigbee gateway via Home Assistant add-on",
+ "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the add-on {addon}?"
}
},
"error": {
@@ -94,6 +94,10 @@
"button_2": "Second button",
"button_3": "Third button",
"button_4": "Fourth button",
+ "button_5": "Fifth button",
+ "button_6": "Sixth button",
+ "button_7": "Seventh button",
+ "button_8": "Eighth button",
"side_1": "Side 1",
"side_2": "Side 2",
"side_3": "Side 3",
diff --git a/homeassistant/components/deconz/translations/bg.json b/homeassistant/components/deconz/translations/bg.json
index 0aef2e3ec988fe..24e36ecbe55824 100644
--- a/homeassistant/components/deconz/translations/bg.json
+++ b/homeassistant/components/deconz/translations/bg.json
@@ -13,8 +13,8 @@
"flow_title": "deCONZ Zigbee \u0448\u043b\u044e\u0437 ({host})",
"step": {
"hassio_confirm": {
- "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0437\u0432\u0430 \u0441 deCONZ \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 \u0437\u0430 hass.io {addon}?",
- "title": "deCONZ Zigbee \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f \u0447\u0440\u0435\u0437 Hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430"
+ "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0437\u0432\u0430 \u0441 deCONZ \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 \u0437\u0430 Supervisor {addon}?",
+ "title": "deCONZ Zigbee \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f \u0447\u0440\u0435\u0437 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430"
},
"link": {
"description": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438 deCONZ \u0448\u043b\u044e\u0437\u0430 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u0441 Home Assistant.\n\n1. \u041e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 deCONZ Settings -> Gateway -> Advanced\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Authenticate app\"",
diff --git a/homeassistant/components/deconz/translations/ca.json b/homeassistant/components/deconz/translations/ca.json
index 49374ee123feb0..60d91a83db8b3c 100644
--- a/homeassistant/components/deconz/translations/ca.json
+++ b/homeassistant/components/deconz/translations/ca.json
@@ -14,8 +14,8 @@
"flow_title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee ({host})",
"step": {
"hassio_confirm": {
- "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb la passarel\u00b7la deCONZ proporcionada pel complement de Hass.io: {addon}?",
- "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee (complement de Hass.io)"
+ "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb la passarel\u00b7la deCONZ proporcionada pel complement {addon}?",
+ "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee via complement de Home Assistant"
},
"link": {
"description": "Desbloqueja la teva passarel\u00b7la d'enlla\u00e7 deCONZ per a registrar-te amb Home Assistant.\n\n1. V\u00e9s a la configuraci\u00f3 del sistema deCONZ -> Passarel\u00b7la -> Avan\u00e7at\n2. Prem el bot\u00f3 \"Autenticar applicaci\u00f3\"",
@@ -42,6 +42,10 @@
"button_2": "Segon bot\u00f3",
"button_3": "Tercer bot\u00f3",
"button_4": "Quart bot\u00f3",
+ "button_5": "Cinqu\u00e8 bot\u00f3",
+ "button_6": "Sis\u00e8 bot\u00f3",
+ "button_7": "Set\u00e8 bot\u00f3",
+ "button_8": "Vuit\u00e8 bot\u00f3",
"close": "Tanca",
"dim_down": "Atenua la brillantor",
"dim_up": "Augmenta la brillantor",
diff --git a/homeassistant/components/deconz/translations/cs.json b/homeassistant/components/deconz/translations/cs.json
index 3ad72dc9ac9c65..c198068e07edfd 100644
--- a/homeassistant/components/deconz/translations/cs.json
+++ b/homeassistant/components/deconz/translations/cs.json
@@ -14,8 +14,8 @@
"flow_title": "Br\u00e1na deCONZ ZigBee ({host})",
"step": {
"hassio_confirm": {
- "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k deCONZ br\u00e1n\u011b pomoc\u00ed hass.io {addon}?",
- "title": "deCONZ Zigbee br\u00e1na prost\u0159ednictv\u00edm dopl\u0148ku Hass.io"
+ "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k deCONZ br\u00e1n\u011b pomoc\u00ed dopl\u0148ku {addon}?",
+ "title": "deCONZ Zigbee br\u00e1na prost\u0159ednictv\u00edm Home Assistant dopl\u0148ku"
},
"link": {
"description": "Odemkn\u011bte br\u00e1nu deCONZ pro registraci v Home Assistant.\n\n 1. P\u0159ejd\u011bte na Nastaven\u00ed deCONZ - > Br\u00e1na - > Pokro\u010dil\u00e9\n 2. Stiskn\u011bte tla\u010d\u00edtko \"Ov\u011b\u0159it aplikaci\"",
@@ -60,14 +60,15 @@
},
"trigger_type": {
"remote_awakened": "Za\u0159\u00edzen\u00ed probuzeno",
- "remote_button_double_press": "Dvakr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"",
+ "remote_button_double_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto dvakr\u00e1t",
+ "remote_button_long_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto dlouze",
"remote_button_long_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\" po dlouh\u00e9m stisku",
- "remote_button_quadruple_press": "\u010cty\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"",
- "remote_button_quintuple_press": "P\u011btkr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"",
+ "remote_button_quadruple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t",
+ "remote_button_quintuple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto p\u011btkr\u00e1t",
"remote_button_rotation_stopped": "Oto\u010den\u00ed tla\u010d\u00edtka \"{subtype}\" bylo zastaveno",
- "remote_button_short_press": "Stiknuto tla\u010d\u00edtko \"{subtype}\"",
+ "remote_button_short_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto",
"remote_button_short_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\"",
- "remote_button_triple_press": "T\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"",
+ "remote_button_triple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto t\u0159ikr\u00e1t",
"remote_double_tap": "Dvakr\u00e1t poklep\u00e1no na za\u0159\u00edzen\u00ed \"{subtype}\"",
"remote_double_tap_any_side": "Za\u0159\u00edzen\u00ed bylo poklep\u00e1no 2x na libovolnou stranu",
"remote_flip_180_degrees": "Za\u0159\u00edzen\u00ed p\u0159evr\u00e1ceno o 180 stup\u0148\u016f",
diff --git a/homeassistant/components/deconz/translations/da.json b/homeassistant/components/deconz/translations/da.json
index 50cdd242ad01e1..be165a206bf6d6 100644
--- a/homeassistant/components/deconz/translations/da.json
+++ b/homeassistant/components/deconz/translations/da.json
@@ -13,8 +13,8 @@
"flow_title": "deCONZ Zigbee gateway ({host})",
"step": {
"hassio_confirm": {
- "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til deCONZ-gateway'en leveret af Hass.io-tilf\u00f8jelsen {addon}?",
- "title": "deCONZ Zigbee-gateway via Hass.io-tilf\u00f8jelse"
+ "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til deCONZ-gateway'en leveret af Supervisor-tilf\u00f8jelsen {addon}?",
+ "title": "deCONZ Zigbee-gateway via Supervisor-tilf\u00f8jelse"
},
"link": {
"description": "L\u00e5s din deCONZ-gateway op for at registrere dig med Home Assistant. \n\n 1. G\u00e5 til deCONZ settings -> Gateway -> Advanced\n 2. Tryk p\u00e5 knappen \"Authenticate app\"",
diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json
index f9448705c5dc67..a8575d212d649e 100644
--- a/homeassistant/components/deconz/translations/de.json
+++ b/homeassistant/components/deconz/translations/de.json
@@ -2,8 +2,9 @@
"config": {
"abort": {
"already_configured": "Bridge ist bereits konfiguriert",
- "already_in_progress": "Der Konfigurationsablauf f\u00fcr die Bridge wird bereits ausgef\u00fchrt.",
- "no_bridges": "Keine deCON-Bridges entdeckt",
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
+ "no_bridges": "Keine deCONZ-Bridges entdeckt",
+ "no_hardware_available": "Keine Funkhardware an deCONZ angeschlossen",
"not_deconz_bridge": "Keine deCONZ Bridge entdeckt",
"updated_instance": "deCONZ-Instanz mit neuer Host-Adresse aktualisiert"
},
@@ -13,8 +14,8 @@
"flow_title": "deCONZ Zigbee Gateway",
"step": {
"hassio_confirm": {
- "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem deCONZ Gateway herstellt, der vom Add-on hass.io {addon} bereitgestellt wird?",
- "title": "deCONZ Zigbee Gateway \u00fcber das Hass.io Add-on"
+ "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem deCONZ Gateway herstellt, der vom Supervisor Add-on {addon} bereitgestellt wird?",
+ "title": "deCONZ Zigbee Gateway \u00fcber das Supervisor Add-on"
},
"link": {
"description": "Entsperre dein deCONZ-Gateway, um es bei Home Assistant zu registrieren. \n\n 1. Gehe in die deCONZ-Systemeinstellungen \n 2. Dr\u00fccke die Taste \"Gateway entsperren\"",
@@ -28,7 +29,7 @@
},
"user": {
"data": {
- "host": "W\u00e4hlen Sie das erkannte deCONZ-Gateway aus"
+ "host": "W\u00e4hle das erkannte deCONZ-Gateway aus"
}
}
}
@@ -65,6 +66,7 @@
"remote_button_quadruple_press": "\"{subtype}\" Taste vierfach geklickt",
"remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach geklickt",
"remote_button_rotated": "Button gedreht \"{subtype}\".",
+ "remote_button_rotated_fast": "Button schnell gedreht \"{subtype}\"",
"remote_button_rotation_stopped": "Die Tastendrehung \"{subtype}\" wurde gestoppt",
"remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt",
"remote_button_short_release": "\"{subtype}\" Taste losgelassen",
@@ -92,7 +94,8 @@
"deconz_devices": {
"data": {
"allow_clip_sensor": "deCONZ CLIP-Sensoren zulassen",
- "allow_deconz_groups": "deCONZ-Lichtgruppen zulassen"
+ "allow_deconz_groups": "deCONZ-Lichtgruppen zulassen",
+ "allow_new_devices": "Automatisches Hinzuf\u00fcgen von neuen Ger\u00e4ten zulassen"
},
"description": "Sichtbarkeit der deCONZ-Ger\u00e4tetypen konfigurieren",
"title": "deCONZ-Optionen"
diff --git a/homeassistant/components/deconz/translations/en.json b/homeassistant/components/deconz/translations/en.json
index 014280d8cc4cde..14ddb6890d4298 100644
--- a/homeassistant/components/deconz/translations/en.json
+++ b/homeassistant/components/deconz/translations/en.json
@@ -14,8 +14,8 @@
"flow_title": "deCONZ Zigbee gateway ({host})",
"step": {
"hassio_confirm": {
- "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?",
- "title": "deCONZ Zigbee gateway via Hass.io add-on"
+ "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the add-on {addon}?",
+ "title": "deCONZ Zigbee gateway via Home Assistant add-on"
},
"link": {
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button",
@@ -42,6 +42,10 @@
"button_2": "Second button",
"button_3": "Third button",
"button_4": "Fourth button",
+ "button_5": "Fifth button",
+ "button_6": "Sixth button",
+ "button_7": "Seventh button",
+ "button_8": "Eighth button",
"close": "Close",
"dim_down": "Dim down",
"dim_up": "Dim up",
diff --git a/homeassistant/components/deconz/translations/es-419.json b/homeassistant/components/deconz/translations/es-419.json
index e454d080ad7196..e439d1da949506 100644
--- a/homeassistant/components/deconz/translations/es-419.json
+++ b/homeassistant/components/deconz/translations/es-419.json
@@ -13,8 +13,8 @@
"flow_title": "Puerta de enlace Zigbee deCONZ ({host})",
"step": {
"hassio_confirm": {
- "description": "\u00bfDesea configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento hass.io {addon}?",
- "title": "deCONZ Zigbee gateway a trav\u00e9s del complemento Hass.io"
+ "description": "\u00bfDesea configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento Supervisor {addon}?",
+ "title": "deCONZ Zigbee gateway a trav\u00e9s del complemento Supervisor"
},
"link": {
"description": "Desbloquee su puerta de enlace deCONZ para registrarse con Home Assistant. \n\n 1. Vaya a Configuraci\u00f3n deCONZ - > Gateway - > Avanzado \n 2. Presione el bot\u00f3n \"Autenticar aplicaci\u00f3n\"",
diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json
index 62ab509e268ede..b237d84fafc1ef 100644
--- a/homeassistant/components/deconz/translations/es.json
+++ b/homeassistant/components/deconz/translations/es.json
@@ -14,8 +14,8 @@
"flow_title": "pasarela deCONZ Zigbee ({host})",
"step": {
"hassio_confirm": {
- "description": "\u00bfQuieres configurar Home Assistant para que se conecte al gateway de deCONZ proporcionado por el add-on {addon} de hass.io?",
- "title": "Add-on deCONZ Zigbee v\u00eda Hass.io"
+ "description": "\u00bfQuieres configurar Home Assistant para que se conecte al gateway de deCONZ proporcionado por el add-on {addon} de Supervisor?",
+ "title": "Add-on deCONZ Zigbee v\u00eda Supervisor"
},
"link": {
"description": "Desbloquea tu gateway de deCONZ para registrarte con Home Assistant.\n\n1. Dir\u00edgete a deCONZ Settings -> Gateway -> Advanced\n2. Pulsa el bot\u00f3n \"Authenticate app\"",
diff --git a/homeassistant/components/deconz/translations/et.json b/homeassistant/components/deconz/translations/et.json
index ad5c07b6607519..e52b54166a14bb 100644
--- a/homeassistant/components/deconz/translations/et.json
+++ b/homeassistant/components/deconz/translations/et.json
@@ -14,11 +14,11 @@
"flow_title": "deCONZ Zigbee l\u00fc\u00fcs ( {host} )",
"step": {
"hassio_confirm": {
- "description": "Kas soovid seadistada Home Assistant-i \u00fchenduse deCONZ-l\u00fc\u00fcsiga, mida pakub Hass.io lisandmoodul {addon} ?",
- "title": "deCONZ Zigbee v\u00e4rav Hass.io pistikprogrammi kaudu"
+ "description": "Kas soovid seadistada Home Assistant-i \u00fchenduse deCONZ-l\u00fc\u00fcsiga, mida pakub lisandmoodul {addon} ?",
+ "title": "deCONZ Zigbee l\u00fc\u00fcs Home Assistanti lisandmooduli abil"
},
"link": {
- "description": "Home Assistanti registreerumiseks ava deCONZ-i l\u00fc\u00fcs.\n\n 1. Minge deCONZ Settings - > Gateway - > Advanced\n 2. Vajutage nuppu \"Authenticate app\"",
+ "description": "Home Assistanti registreerumiseks ava deCONZ-i l\u00fc\u00fcs.\n\n 1. Mine deCONZ Settings - > Gateway - > Advanced\n 2. Vajuta nuppu \"Authenticate app\"",
"title": "\u00dchenda deCONZ-iga"
},
"manual_input": {
@@ -42,6 +42,10 @@
"button_2": "Teine nupp",
"button_3": "Kolmas nupp",
"button_4": "Neljas nupp",
+ "button_5": "Viies nupp",
+ "button_6": "Kuues nupp",
+ "button_7": "Seitsmes nupp",
+ "button_8": "Kaheksas nupp",
"close": "Sulge",
"dim_down": "H\u00e4marda",
"dim_up": "Tee heledamaks",
diff --git a/homeassistant/components/deconz/translations/fr.json b/homeassistant/components/deconz/translations/fr.json
index b64819471a0a0f..d24b592ac10297 100644
--- a/homeassistant/components/deconz/translations/fr.json
+++ b/homeassistant/components/deconz/translations/fr.json
@@ -14,8 +14,8 @@
"flow_title": "Passerelle deCONZ Zigbee ({host})",
"step": {
"hassio_confirm": {
- "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 la passerelle deCONZ fournie par l'add-on hass.io {addon} ?",
- "title": "Passerelle deCONZ Zigbee via l'add-on Hass.io"
+ "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 la passerelle deCONZ fournie par le module compl\u00e9mentaire Hass.io {addon} ?",
+ "title": "Passerelle deCONZ Zigbee via le module compl\u00e9mentaire Hass.io"
},
"link": {
"description": "D\u00e9verrouillez votre passerelle deCONZ pour vous enregistrer avec Home Assistant. \n\n 1. Acc\u00e9dez aux param\u00e8tres avanc\u00e9s du syst\u00e8me deCONZ \n 2. Cliquez sur \"D\u00e9verrouiller la passerelle\"",
diff --git a/homeassistant/components/deconz/translations/hu.json b/homeassistant/components/deconz/translations/hu.json
index 4651a7c08e002e..0463463c0b3f3b 100644
--- a/homeassistant/components/deconz/translations/hu.json
+++ b/homeassistant/components/deconz/translations/hu.json
@@ -2,8 +2,9 @@
"config": {
"abort": {
"already_configured": "A bridge m\u00e1r konfigur\u00e1lva van",
- "already_in_progress": "Az \u00e1tj\u00e1r\u00f3 konfigur\u00e1ci\u00f3s folyamata m\u00e1r folyamatban van.",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
"no_bridges": "Nem tal\u00e1ltam deCONZ bridget",
+ "no_hardware_available": "Nincs deCONZ-hoz csatlakoztatott r\u00e1di\u00f3hardver",
"not_deconz_bridge": "Nem egy deCONZ \u00e1tj\u00e1r\u00f3",
"updated_instance": "A deCONZ-p\u00e9ld\u00e1ny \u00faj \u00e1llom\u00e1sc\u00edmmel friss\u00edtve"
},
@@ -13,7 +14,7 @@
"flow_title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 ({host})",
"step": {
"hassio_confirm": {
- "title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 a Hass.io kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel"
+ "title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 a Supervisor kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel"
},
"link": {
"description": "Oldja fel a deCONZ \u00e1tj\u00e1r\u00f3t a Home Assistant-ban val\u00f3 regisztr\u00e1l\u00e1shoz.\n\n1. Menjen a deCONZ rendszer be\u00e1ll\u00edt\u00e1sokhoz\n2. Nyomja meg az \"\u00c1tj\u00e1r\u00f3 felold\u00e1sa\" gombot",
@@ -34,6 +35,10 @@
"button_2": "M\u00e1sodik gomb",
"button_3": "Harmadik gomb",
"button_4": "Negyedik gomb",
+ "button_5": "\u00d6t\u00f6dik gomb",
+ "button_6": "Hatodik gomb",
+ "button_7": "Hetedik gomb",
+ "button_8": "Nyolcadik gomb",
"close": "Bez\u00e1r\u00e1s",
"dim_down": "S\u00f6t\u00e9t\u00edt",
"dim_up": "Vil\u00e1gos\u00edt",
@@ -51,30 +56,30 @@
},
"trigger_type": {
"remote_awakened": "A k\u00e9sz\u00fcl\u00e9k fel\u00e9bredt",
- "remote_button_double_press": "\" {subtype} \" gombra k\u00e9tszer kattintottak",
- "remote_button_long_press": "A \" {subtype} \" gomb folyamatosan lenyomva",
- "remote_button_long_release": "A \" {subtype} \" gomb hossz\u00fa megnyom\u00e1s ut\u00e1n elengedve",
- "remote_button_quadruple_press": "\" {subtype} \" gombra n\u00e9gyszer kattintottak",
- "remote_button_quintuple_press": "\" {subtype} \" gombra \u00f6tsz\u00f6r kattintottak",
- "remote_button_rotated": "A gomb elforgatva: \" {subtype} \"",
- "remote_button_rotation_stopped": "A (z) \" {subtype} \" gomb forg\u00e1sa le\u00e1llt",
- "remote_button_short_press": "\" {subtype} \" gomb lenyomva",
- "remote_button_short_release": "\"{alt\u00edpus}\" gomb elengedve",
- "remote_button_triple_press": "\" {subtype} \" gombra h\u00e1romszor kattintottak",
- "remote_double_tap": "Az \" {subtype} \" eszk\u00f6z dupla kattint\u00e1sa",
+ "remote_button_double_press": "\"{subtype}\" gombra k\u00e9tszer kattintottak",
+ "remote_button_long_press": "A \"{subtype}\" gomb folyamatosan lenyomva",
+ "remote_button_long_release": "A \"{subtype}\" gomb hossz\u00fa megnyom\u00e1s ut\u00e1n elengedve",
+ "remote_button_quadruple_press": "\"{subtype}\" gombra n\u00e9gyszer kattintottak",
+ "remote_button_quintuple_press": "\"{subtype}\" gombra \u00f6tsz\u00f6r kattintottak",
+ "remote_button_rotated": "A gomb elforgatva: \"{subtype}\"",
+ "remote_button_rotation_stopped": "A (z) \"{subtype}\" gomb forg\u00e1sa le\u00e1llt",
+ "remote_button_short_press": "\"{subtype}\" gomb lenyomva",
+ "remote_button_short_release": "\"{subtype}\" gomb elengedve",
+ "remote_button_triple_press": "\"{subtype}\" gombra h\u00e1romszor kattintottak",
+ "remote_double_tap": "Az \"{subtype}\" eszk\u00f6z dupla kattint\u00e1sa",
"remote_double_tap_any_side": "A k\u00e9sz\u00fcl\u00e9k b\u00e1rmelyik oldal\u00e1n dupl\u00e1n koppint.",
"remote_falling": "K\u00e9sz\u00fcl\u00e9k szabades\u00e9sben",
"remote_flip_180_degrees": "180 fokkal megd\u00f6nt\u00f6tt eszk\u00f6z",
"remote_flip_90_degrees": "90 fokkal megd\u00f6nt\u00f6tt eszk\u00f6z",
"remote_gyro_activated": "A k\u00e9sz\u00fcl\u00e9k meg lett r\u00e1zva",
- "remote_moved": "Az eszk\u00f6z a \" {subtype} \"-lal felfel\u00e9 mozgatva",
+ "remote_moved": "Az eszk\u00f6z a \"{subtype}\"-lal felfel\u00e9 mozgatva",
"remote_moved_any_side": "A k\u00e9sz\u00fcl\u00e9k valamelyik oldal\u00e1val felfel\u00e9 mozogott",
- "remote_rotate_from_side_1": "Az eszk\u00f6z a \"1. oldalr\u00f3l\" a \" {subtype} \" -ra fordult",
- "remote_rotate_from_side_2": "Az eszk\u00f6z a \"2. oldalr\u00f3l\" a \" {subtype} \" -ra fordult",
- "remote_rotate_from_side_3": "Az eszk\u00f6z a \"3. oldalr\u00f3l\" a \" {subtype} \" -ra fordult",
- "remote_rotate_from_side_4": "Az eszk\u00f6z a \"4. oldalr\u00f3l\" a \" {subtype} \" -ra fordult",
- "remote_rotate_from_side_5": "Az eszk\u00f6z a \"5. oldalr\u00f3l\" a \" {subtype} \" -ra fordult",
- "remote_rotate_from_side_6": "Az eszk\u00f6z a \"6. oldalr\u00f3l\" a \" {subtype} \" -ra fordult",
+ "remote_rotate_from_side_1": "Az eszk\u00f6z az \"1. oldalr\u00f3l\" a \"{subtype}\"-ra fordult",
+ "remote_rotate_from_side_2": "Az eszk\u00f6z a \"2. oldalr\u00f3l\" a \"{subtype}\"-ra fordult",
+ "remote_rotate_from_side_3": "Az eszk\u00f6z a \"3. oldalr\u00f3l\" a \"{subtype}\"-ra fordult",
+ "remote_rotate_from_side_4": "Az eszk\u00f6z a \"4. oldalr\u00f3l\" a \"{subtype}\"-ra fordult",
+ "remote_rotate_from_side_5": "Az eszk\u00f6z az \"5. oldalr\u00f3l\" a \"{subtype}\"-ra fordult",
+ "remote_rotate_from_side_6": "Az eszk\u00f6z a \"6. oldalr\u00f3l\" a \"{subtype}\"-ra fordult",
"remote_turned_clockwise": "A k\u00e9sz\u00fcl\u00e9k az \u00f3ramutat\u00f3 j\u00e1r\u00e1s\u00e1val megegyez\u0151en fordult",
"remote_turned_counter_clockwise": "A k\u00e9sz\u00fcl\u00e9k az \u00f3ramutat\u00f3 j\u00e1r\u00e1s\u00e1val ellent\u00e9tes ir\u00e1nyban fordult"
}
diff --git a/homeassistant/components/deconz/translations/id.json b/homeassistant/components/deconz/translations/id.json
index 0d46cf7c176b36..d7fb26f8d52081 100644
--- a/homeassistant/components/deconz/translations/id.json
+++ b/homeassistant/components/deconz/translations/id.json
@@ -2,15 +2,103 @@
"config": {
"abort": {
"already_configured": "Bridge sudah dikonfigurasi",
- "no_bridges": "deCONZ bridges tidak ditemukan"
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "no_bridges": "deCONZ bridge tidak ditemukan",
+ "no_hardware_available": "Tidak ada perangkat keras radio yang terhubung ke deCONZ",
+ "not_deconz_bridge": "Bukan bridge deCONZ",
+ "updated_instance": "Instans deCONZ yang diperbarui dengan alamat host baru"
},
"error": {
"no_key": "Tidak bisa mendapatkan kunci API"
},
+ "flow_title": "Gateway Zigbee deCONZ ({host})",
"step": {
+ "hassio_confirm": {
+ "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke gateway deCONZ yang disediakan oleh add-on Supervisor {addon}?",
+ "title": "Gateway Zigbee deCONZ melalui add-on Supervisor"
+ },
"link": {
- "description": "Buka gerbang deCONZ Anda untuk mendaftar dengan Home Assistant. \n\n 1. Pergi ke pengaturan sistem deCONZ \n 2. Tekan tombol \"Buka Kunci Gateway\"",
- "title": "Tautan dengan deCONZ"
+ "description": "Buka gateway deCONZ Anda untuk mendaftarkan ke Home Assistant. \n\n1. Buka pengaturan sistem deCONZ \n2. Tekan tombol \"Authenticate app\"",
+ "title": "Tautkan dengan deCONZ"
+ },
+ "manual_input": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Pilih gateway deCONZ yang ditemukan"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "Kedua tombol",
+ "bottom_buttons": "Tombol bawah",
+ "button_1": "Tombol pertama",
+ "button_2": "Tombol kedua",
+ "button_3": "Tombol ketiga",
+ "button_4": "Tombol keempat",
+ "close": "Tutup",
+ "dim_down": "Redupkan",
+ "dim_up": "Terangkan",
+ "left": "Kiri",
+ "open": "Buka",
+ "right": "Kanan",
+ "side_1": "Sisi 1",
+ "side_2": "Sisi 2",
+ "side_3": "Sisi 3",
+ "side_4": "Sisi 4",
+ "side_5": "Sisi 5",
+ "side_6": "Sisi 6",
+ "top_buttons": "Tombol atas",
+ "turn_off": "Matikan",
+ "turn_on": "Nyalakan"
+ },
+ "trigger_type": {
+ "remote_awakened": "Perangkat terbangun",
+ "remote_button_double_press": "Tombol \"{subtype}\" diklik dua kali",
+ "remote_button_long_press": "Tombol \"{subtype}\" terus ditekan",
+ "remote_button_long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama",
+ "remote_button_quadruple_press": "Tombol \"{subtype}\" diklik empat kali",
+ "remote_button_quintuple_press": "Tombol \"{subtype}\" diklik lima kali",
+ "remote_button_rotated": "Tombol diputar \"{subtype}\"",
+ "remote_button_rotated_fast": "Tombol diputar cepat \"{subtype}\"",
+ "remote_button_rotation_stopped": "Pemutaran tombol \"{subtype}\" berhenti",
+ "remote_button_short_press": "Tombol \"{subtype}\" ditekan",
+ "remote_button_short_release": "Tombol \"{subtype}\" dilepaskan",
+ "remote_button_triple_press": "Tombol \"{subtype}\" diklik tiga kali",
+ "remote_double_tap": "Perangkat \"{subtype}\" diketuk dua kali",
+ "remote_double_tap_any_side": "Perangkat diketuk dua kali di sisi mana pun",
+ "remote_falling": "Perangkat jatuh bebas",
+ "remote_flip_180_degrees": "Perangkat dibalik 180 derajat",
+ "remote_flip_90_degrees": "Perangkat dibalik 90 derajat",
+ "remote_gyro_activated": "Perangkat diguncangkan",
+ "remote_moved": "Perangkat dipindahkan dengan \"{subtype}\" ke atas",
+ "remote_moved_any_side": "Perangkat dipindahkan dengan sisi mana pun menghadap ke atas",
+ "remote_rotate_from_side_1": "Perangkat diputar dari \"sisi 1\" ke \"{subtype}\"",
+ "remote_rotate_from_side_2": "Perangkat diputar dari \"sisi 2\" ke \"{subtype}\"",
+ "remote_rotate_from_side_3": "Perangkat diputar dari \"sisi 3\" ke \"{subtype}\"",
+ "remote_rotate_from_side_4": "Perangkat diputar dari \"sisi 4\" ke \"{subtype}\"",
+ "remote_rotate_from_side_5": "Perangkat diputar dari \"sisi 5\" ke \"{subtype}\"",
+ "remote_rotate_from_side_6": "Perangkat diputar dari \"sisi 6\" ke \"{subtype}\"",
+ "remote_turned_clockwise": "Perangkat diputar searah jarum jam",
+ "remote_turned_counter_clockwise": "Perangkat diputar berlawanan arah jarum jam"
+ }
+ },
+ "options": {
+ "step": {
+ "deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "Izinkan sensor CLIP deCONZ",
+ "allow_deconz_groups": "Izinkan grup lampu deCONZ",
+ "allow_new_devices": "Izinkan penambahan otomatis perangkat baru"
+ },
+ "description": "Konfigurasikan visibilitas jenis perangkat deCONZ",
+ "title": "Opsi deCONZ"
}
}
}
diff --git a/homeassistant/components/deconz/translations/it.json b/homeassistant/components/deconz/translations/it.json
index 00ecd316da761a..cb445ac4f764b0 100644
--- a/homeassistant/components/deconz/translations/it.json
+++ b/homeassistant/components/deconz/translations/it.json
@@ -14,8 +14,8 @@
"flow_title": "Gateway Zigbee deCONZ ({host})",
"step": {
"hassio_confirm": {
- "description": "Vuoi configurare Home Assistant per connettersi al gateway deCONZ fornito dal componente aggiuntivo di Hass.io: {addon}?",
- "title": "Gateway Pigmee deCONZ tramite il componente aggiuntivo di Hass.io"
+ "description": "Vuoi configurare Home Assistant per connettersi al gateway deCONZ fornito dal componente aggiuntivo: {addon}?",
+ "title": "Gateway deCONZ Zigbee tramite il componente aggiuntivo di Home Assistant"
},
"link": {
"description": "Sblocca il tuo gateway deCONZ per registrarti con Home Assistant.\n\n1. Vai a Impostazioni deCONZ -> Gateway -> Avanzate\n2. Premere il pulsante \"Autentica app\"",
@@ -42,6 +42,10 @@
"button_2": "Secondo pulsante",
"button_3": "Terzo pulsante",
"button_4": "Quarto pulsante",
+ "button_5": "Quinto pulsante",
+ "button_6": "Sesto pulsante",
+ "button_7": "Settimo pulsante",
+ "button_8": "Ottavo pulsante",
"close": "Chiudere",
"dim_down": "Diminuire luminosit\u00e0",
"dim_up": "Aumentare luminosit\u00e0",
diff --git a/homeassistant/components/deconz/translations/ko.json b/homeassistant/components/deconz/translations/ko.json
index 6c7dde04e31488..5158d557106f8c 100644
--- a/homeassistant/components/deconz/translations/ko.json
+++ b/homeassistant/components/deconz/translations/ko.json
@@ -2,8 +2,9 @@
"config": {
"abort": {
"already_configured": "\ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "already_in_progress": "\ube0c\ub9ac\uc9c0 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
"no_bridges": "\ubc1c\uacac\ub41c deCONZ \ube0c\ub9ac\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "no_hardware_available": "deCONZ\uc5d0 \uc5f0\uacb0\ub41c \ubb34\uc120 \ud558\ub4dc\uc6e8\uc5b4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4",
"not_deconz_bridge": "deCONZ \ube0c\ub9ac\uc9c0\uac00 \uc544\ub2d9\ub2c8\ub2e4",
"updated_instance": "deCONZ \uc778\uc2a4\ud134\uc2a4\ub97c \uc0c8\ub85c\uc6b4 \ud638\uc2a4\ud2b8 \uc8fc\uc18c\ub85c \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4"
},
@@ -13,11 +14,11 @@
"flow_title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774 ({host})",
"step": {
"hassio_confirm": {
- "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ub41c deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
- "title": "Hass.io \uc560\ub4dc\uc628\uc758 deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774"
+ "description": "{addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "Home Assistant \uc560\ub4dc\uc628\uc758 deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774"
},
"link": {
- "description": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc5b8\ub77d\ud558\uc5ec Home Assistant \uc5d0 \uc5f0\uacb0\ud558\uae30.\n\n1. deCONZ \uc2dc\uc2a4\ud15c \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc138\uc694\n2. \"Authenticate app\" \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694",
+ "description": "Home Assistant\uc5d0 \ub4f1\ub85d\ud558\ub824\uba74 deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc7a0\uae08 \ud574\uc81c\ud574\uc8fc\uc138\uc694.\n\n 1. deCONZ \uc124\uc815 -> \uac8c\uc774\ud2b8\uc6e8\uc774 -> \uace0\uae09\uc73c\ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694\n 2. \"\uc571 \uc778\uc99d\ud558\uae30\" \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694",
"title": "deCONZ \uc5f0\uacb0\ud558\uae30"
},
"manual_input": {
@@ -41,6 +42,10 @@
"button_2": "\ub450 \ubc88\uc9f8",
"button_3": "\uc138 \ubc88\uc9f8",
"button_4": "\ub124 \ubc88\uc9f8",
+ "button_5": "\ub2e4\uc12f \ubc88\uc9f8 \ubc84\ud2bc",
+ "button_6": "\uc5ec\uc12f \ubc88\uc9f8 \ubc84\ud2bc",
+ "button_7": "\uc77c\uacf1 \ubc88\uc9f8 \ubc84\ud2bc",
+ "button_8": "\uc5ec\ub35f \ubc88\uc9f8 \ubc84\ud2bc",
"close": "\ub2eb\uae30",
"dim_down": "\uc5b4\ub461\uac8c \ud558\uae30",
"dim_up": "\ubc1d\uac8c \ud558\uae30",
@@ -58,33 +63,34 @@
"turn_on": "\ucf1c\uae30"
},
"trigger_type": {
- "remote_awakened": "\uae30\uae30 \uc808\uc804 \ubaa8\ub4dc \ud574\uc81c\ub420 \ub54c",
- "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub9b4 \ub54c",
- "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub824\uc9c8 \ub54c",
- "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c",
- "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub124 \ubc88 \ub20c\ub9b4 \ub54c",
- "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub2e4\uc12f \ubc88 \ub20c\ub9b4 \ub54c",
- "remote_button_rotated": "\"{subtype}\" \ub85c \ubc84\ud2bc\uc774 \ud68c\uc804\ub420 \ub54c",
- "remote_button_rotation_stopped": "\"{subtype}\" \ub85c \ubc84\ud2bc\uc774 \ud68c\uc804\uc744 \uba48\ucd9c \ub54c",
- "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub9b4 \ub54c",
- "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c",
- "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c",
- "remote_double_tap": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \ub354\ube14 \ud0ed \ub420 \ub54c",
- "remote_double_tap_any_side": "\uae30\uae30\uc758 \uc544\ubb34 \uba74\uc774\ub098 \ub354\ube14 \ud0ed \ub420 \ub54c",
+ "remote_awakened": "\uae30\uae30\uc758 \uc808\uc804 \ubaa8\ub4dc\uac00 \ud574\uc81c\ub418\uc5c8\uc744 \ub54c",
+ "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub838\uc744 \ub54c",
+ "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub838\uc744 \ub54c",
+ "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \ub5bc\uc600\uc744 \ub54c",
+ "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub124 \ubc88 \ub20c\ub838\uc744 \ub54c",
+ "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub2e4\uc12f \ubc88 \ub20c\ub838\uc744 \ub54c",
+ "remote_button_rotated": "\"{subtype}\"(\uc73c)\ub85c \ubc84\ud2bc\uc774 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c",
+ "remote_button_rotated_fast": "\"{subtype}\"(\uc73c)\ub85c \ubc84\ud2bc\uc774 \ube60\ub974\uac8c \ud68c\uc804\ub418\uc5c8\uc744 \ub54c",
+ "remote_button_rotation_stopped": "\"{subtype}\"(\uc73c)\ub85c \ubc84\ud2bc\ud68c\uc804\uc774 \uba48\ucd94\uc5c8\uc744 \ub54c",
+ "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub838\uc744 \ub54c",
+ "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5bc\uc5c8\uc744 \ub54c",
+ "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub838\uc744 \ub54c",
+ "remote_double_tap": "\uae30\uae30\uc758 \"{subtype}\"\uc774(\uac00) \ub354\ube14 \ud0ed \ub418\uc5c8\uc744 \ub54c",
+ "remote_double_tap_any_side": "\uae30\uae30\uc758 \uc544\ubb34 \uba74\uc774\ub098 \ub354\ube14 \ud0ed \ub418\uc5c8\uc744 \ub54c",
"remote_falling": "\uae30\uae30\uac00 \ub5a8\uc5b4\uc9c8 \ub54c",
- "remote_flip_180_degrees": "\uae30\uae30\uac00 180\ub3c4\ub85c \ub4a4\uc9d1\uc5b4\uc9c8 \ub54c",
- "remote_flip_90_degrees": "\uae30\uae30\uac00 90\ub3c4\ub85c \ub4a4\uc9d1\uc5b4\uc9c8 \ub54c",
- "remote_gyro_activated": "\uae30\uae30\uac00 \ud754\ub4e4\ub9b4 \ub54c",
- "remote_moved": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \uc704\ub85c \ud5a5\ud55c\ucc44\ub85c \uc6c0\uc9c1\uc77c \ub54c",
- "remote_moved_any_side": "\uae30\uae30\uc758 \uc544\ubb34 \uba74\uc774\ub098 \uc704\ub85c \ud5a5\ud55c\ucc44\ub85c \uc6c0\uc9c1\uc77c \ub54c",
- "remote_rotate_from_side_1": "\"\uba74 1\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c",
- "remote_rotate_from_side_2": "\"\uba74 2\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c",
- "remote_rotate_from_side_3": "\"\uba74 3\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c",
- "remote_rotate_from_side_4": "\"\uba74 4\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c",
- "remote_rotate_from_side_5": "\"\uba74 5\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c",
- "remote_rotate_from_side_6": "\"\uba74 6\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c",
- "remote_turned_clockwise": "\uc2dc\uacc4 \ubc29\ud5a5\uc73c\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c",
- "remote_turned_counter_clockwise": "\ubc18\uc2dc\uacc4 \ubc29\ud5a5\uc73c\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c"
+ "remote_flip_180_degrees": "\uae30\uae30\uac00 180\ub3c4\ub85c \ub4a4\uc9d1\uc5b4\uc84c\uc744 \ub54c",
+ "remote_flip_90_degrees": "\uae30\uae30\uac00 90\ub3c4\ub85c \ub4a4\uc9d1\uc5b4\uc84c\uc744 \ub54c",
+ "remote_gyro_activated": "\uae30\uae30\uac00 \ud754\ub4e4\ub838\uc744 \ub54c",
+ "remote_moved": "\uae30\uae30\uc758 \"{subtype}\"\uc774(\uac00) \uc704\ub85c \ud5a5\ud55c \ucc44\ub85c \uc6c0\uc9c1\uc600\uc744 \ub54c",
+ "remote_moved_any_side": "\uae30\uae30\uc758 \uc544\ubb34 \uba74\uc774\ub098 \uc704\ub85c \ud5a5\ud55c\ucc44\ub85c \uc6c0\uc9c1\uc600\uc744 \ub54c",
+ "remote_rotate_from_side_1": "\"\uba74 1\" \uc5d0\uc11c \"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c",
+ "remote_rotate_from_side_2": "\"\uba74 2\" \uc5d0\uc11c \"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c",
+ "remote_rotate_from_side_3": "\"\uba74 3\" \uc5d0\uc11c \"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c",
+ "remote_rotate_from_side_4": "\"\uba74 4\" \uc5d0\uc11c \"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c",
+ "remote_rotate_from_side_5": "\"\uba74 5\" \uc5d0\uc11c \"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c",
+ "remote_rotate_from_side_6": "\"\uba74 6\" \uc5d0\uc11c \"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c",
+ "remote_turned_clockwise": "\uc2dc\uacc4 \ubc29\ud5a5\uc73c\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c",
+ "remote_turned_counter_clockwise": "\ubc18\uc2dc\uacc4 \ubc29\ud5a5\uc73c\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c"
}
},
"options": {
@@ -92,7 +98,8 @@
"deconz_devices": {
"data": {
"allow_clip_sensor": "deCONZ CLIP \uc13c\uc11c \ud5c8\uc6a9",
- "allow_deconz_groups": "deCONZ \ub77c\uc774\ud2b8 \uadf8\ub8f9 \ud5c8\uc6a9"
+ "allow_deconz_groups": "deCONZ \ub77c\uc774\ud2b8 \uadf8\ub8f9 \ud5c8\uc6a9",
+ "allow_new_devices": "\uc0c8\ub85c\uc6b4 \uae30\uae30\uc758 \uc790\ub3d9 \ucd94\uac00 \ud5c8\uc6a9\ud558\uae30"
},
"description": "deCONZ \uae30\uae30 \uc720\ud615\uc758 \ud45c\uc2dc \uc5ec\ubd80 \uad6c\uc131",
"title": "deCONZ \uc635\uc158"
diff --git a/homeassistant/components/deconz/translations/lb.json b/homeassistant/components/deconz/translations/lb.json
index bb556842194c84..06b8dbacdc53b1 100644
--- a/homeassistant/components/deconz/translations/lb.json
+++ b/homeassistant/components/deconz/translations/lb.json
@@ -14,8 +14,8 @@
"flow_title": "deCONZ Zigbee gateway ({host})",
"step": {
"hassio_confirm": {
- "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mat der deCONZ gateway ze verbannen d\u00e9i vum hass.io add-on {addon} bereet gestallt g\u00ebtt?",
- "title": "deCONZ Zigbee gateway via Hass.io add-on"
+ "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mat der deCONZ gateway ze verbannen d\u00e9i vum Supervisor add-on {addon} bereet gestallt g\u00ebtt?",
+ "title": "deCONZ Zigbee gateway via Supervisor add-on"
},
"link": {
"description": "Entsperrt \u00e4r deCONZ gateway fir se mat Home Assistant ze registr\u00e9ieren.\n\n1. Gidd op deCONZ System Astellungen\n2. Dr\u00e9ckt \"Unlock\" Gateway Kn\u00e4ppchen",
diff --git a/homeassistant/components/deconz/translations/nl.json b/homeassistant/components/deconz/translations/nl.json
index 2d43ca63bfef62..0d0a745bc1b26a 100644
--- a/homeassistant/components/deconz/translations/nl.json
+++ b/homeassistant/components/deconz/translations/nl.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Bridge is al geconfigureerd",
- "already_in_progress": "Configuratiestroom voor bridge wordt al ingesteld.",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
"no_bridges": "Geen deCONZ apparaten ontdekt",
"no_hardware_available": "Geen radiohardware aangesloten op deCONZ",
"not_deconz_bridge": "Dit is geen deCONZ bridge",
@@ -14,8 +14,8 @@
"flow_title": "deCONZ Zigbee gateway ( {host} )",
"step": {
"hassio_confirm": {
- "description": "Wilt u de Home Assistant configureren om verbinding te maken met de deCONZ gateway van de hass.io add-on {addon}?",
- "title": "deCONZ Zigbee Gateway via Hass.io add-on"
+ "description": "Wilt u Home Assistant configureren om verbinding te maken met de deCONZ gateway van de Home Assistant add-on {addon}?",
+ "title": "deCONZ Zigbee Gateway via Home Assistant add-on"
},
"link": {
"description": "Ontgrendel je deCONZ gateway om te registreren met Home Assistant.\n\n1. Ga naar deCONZ systeeminstellingen (Instellingen -> Gateway -> Geavanceerd)\n2. Druk op de knop \"Gateway ontgrendelen\"",
@@ -42,6 +42,10 @@
"button_2": "Tweede knop",
"button_3": "Derde knop",
"button_4": "Vierde knop",
+ "button_5": "Vijfde knop",
+ "button_6": "Zesde knop",
+ "button_7": "Zevende knop",
+ "button_8": "Achtste knop",
"close": "Sluiten",
"dim_down": "Dim omlaag",
"dim_up": "Dim omhoog",
diff --git a/homeassistant/components/deconz/translations/no.json b/homeassistant/components/deconz/translations/no.json
index 487163794833f5..f27e7235f40689 100644
--- a/homeassistant/components/deconz/translations/no.json
+++ b/homeassistant/components/deconz/translations/no.json
@@ -14,8 +14,8 @@
"flow_title": "",
"step": {
"hassio_confirm": {
- "description": "Vil du konfigurere Home Assistant til \u00e5 koble seg til deCONZ-gateway levert av Hass.io-tillegget {addon} ?",
- "title": "deCONZ Zigbee gateway via Hass.io tillegg"
+ "description": "Vil du konfigurere Home Assistant til \u00e5 koble til deCONZ gateway levert av tillegget {addon} ?",
+ "title": "deCONZ Zigbee-gateway via Home Assistant-tillegget"
},
"link": {
"description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger -> Gateway -> Avansert \n 2. Trykk p\u00e5 \"Autentiser app\" knappen",
@@ -42,6 +42,10 @@
"button_2": "Andre knapp",
"button_3": "Tredje knapp",
"button_4": "Fjerde knapp",
+ "button_5": "Femte knapp",
+ "button_6": "Sjette knapp",
+ "button_7": "Syvende knapp",
+ "button_8": "\u00c5ttende knapp",
"close": "Lukk",
"dim_down": "Dimm ned",
"dim_up": "Dimm opp",
diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json
index 24a3ba61706696..d2352bdb9731b0 100644
--- a/homeassistant/components/deconz/translations/pl.json
+++ b/homeassistant/components/deconz/translations/pl.json
@@ -14,8 +14,8 @@
"flow_title": "Bramka deCONZ Zigbee ({host})",
"step": {
"hassio_confirm": {
- "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z bramk\u0105 deCONZ dostarczon\u0105 przez dodatek Hass.io {addon}?",
- "title": "Bramka deCONZ Zigbee przez dodatek Hass.io"
+ "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z bramk\u0105 deCONZ dostarczon\u0105 przez dodatek {addon}?",
+ "title": "Bramka deCONZ Zigbee przez dodatek Home Assistant"
},
"link": {
"description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistantem. \n\n 1. Przejd\u017a do ustawienia deCONZ > bramka > Zaawansowane\n 2. Naci\u015bnij przycisk \"Uwierzytelnij aplikacj\u0119\"",
@@ -38,10 +38,14 @@
"trigger_subtype": {
"both_buttons": "oba przyciski",
"bottom_buttons": "dolne przyciski",
- "button_1": "pierwszy przycisk",
- "button_2": "drugi przycisk",
- "button_3": "trzeci przycisk",
+ "button_1": "pierwszy",
+ "button_2": "drugi",
+ "button_3": "trzeci",
"button_4": "czwarty",
+ "button_5": "pi\u0105ty",
+ "button_6": "sz\u00f3sty",
+ "button_7": "si\u00f3dmy",
+ "button_8": "\u00f3smy",
"close": "zamknij",
"dim_down": "zmniejszenie jasno\u015bci",
"dim_up": "zwi\u0119kszenie jasno\u015bci",
diff --git a/homeassistant/components/deconz/translations/pt-BR.json b/homeassistant/components/deconz/translations/pt-BR.json
index a13c94d82d7939..450fa7707d15a7 100644
--- a/homeassistant/components/deconz/translations/pt-BR.json
+++ b/homeassistant/components/deconz/translations/pt-BR.json
@@ -12,8 +12,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Deseja configurar o Home Assistant para conectar-se ao gateway deCONZ fornecido pelo add-on hass.io {addon} ?",
- "title": "Gateway deCONZ Zigbee via add-on Hass.io"
+ "description": "Deseja configurar o Home Assistant para conectar-se ao gateway deCONZ fornecido pelo add-on Supervisor {addon} ?",
+ "title": "Gateway deCONZ Zigbee via add-on Supervisor"
},
"link": {
"description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"",
diff --git a/homeassistant/components/deconz/translations/pt.json b/homeassistant/components/deconz/translations/pt.json
index 725ce07a1b65c8..cc8b4ab19f27d7 100644
--- a/homeassistant/components/deconz/translations/pt.json
+++ b/homeassistant/components/deconz/translations/pt.json
@@ -11,8 +11,8 @@
},
"step": {
"hassio_confirm": {
- "description": "Deseja configurar o Home Assistant para se conectar ao gateway deCONZ fornecido pelo addon Hass.io {addon} ?",
- "title": "Gateway Zigbee deCONZ via addon Hass.io"
+ "description": "Deseja configurar o Home Assistant para se conectar ao gateway deCONZ fornecido pelo addon Supervisor {addon} ?",
+ "title": "Gateway Zigbee deCONZ via addon Supervisor"
},
"link": {
"description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"",
diff --git a/homeassistant/components/deconz/translations/ru.json b/homeassistant/components/deconz/translations/ru.json
index a6bc0daaa3e271..de97d799381cd2 100644
--- a/homeassistant/components/deconz/translations/ru.json
+++ b/homeassistant/components/deconz/translations/ru.json
@@ -14,8 +14,8 @@
"flow_title": "\u0428\u043b\u044e\u0437 Zigbee deCONZ ({host})",
"step": {
"hassio_confirm": {
- "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?",
- "title": "Zigbee \u0448\u043b\u044e\u0437 deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)"
+ "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?",
+ "title": "Zigbee \u0448\u043b\u044e\u0437 deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)"
},
"link": {
"description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced.\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb.",
@@ -42,6 +42,10 @@
"button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
"button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
"button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_5": "\u041f\u044f\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_6": "\u0428\u0435\u0441\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_7": "\u0421\u0435\u0434\u044c\u043c\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_8": "\u0412\u043e\u0441\u044c\u043c\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
"close": "\u0417\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f",
"dim_down": "\u0423\u043c\u0435\u043d\u044c\u0448\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c",
"dim_up": "\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c",
diff --git a/homeassistant/components/deconz/translations/sl.json b/homeassistant/components/deconz/translations/sl.json
index cf5600c20e4bf6..9e8ed42c07ee66 100644
--- a/homeassistant/components/deconz/translations/sl.json
+++ b/homeassistant/components/deconz/translations/sl.json
@@ -13,8 +13,8 @@
"flow_title": "deCONZ Zigbee prehod ({host})",
"step": {
"hassio_confirm": {
- "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo s prehodom deCONZ, ki ga ponuja dodatek Hass.io {addon} ?",
- "title": "deCONZ Zigbee prehod preko dodatka Hass.io"
+ "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo s prehodom deCONZ, ki ga ponuja dodatek Supervisor {addon} ?",
+ "title": "deCONZ Zigbee prehod preko dodatka Supervisor"
},
"link": {
"description": "Odklenite va\u0161 deCONZ gateway za registracijo s Home Assistant-om. \n1. Pojdite v deCONZ sistemske nastavitve\n2. Pritisnite tipko \"odkleni prehod\"",
diff --git a/homeassistant/components/deconz/translations/sv.json b/homeassistant/components/deconz/translations/sv.json
index 225b9f0b4e3a1a..c9814734af09f6 100644
--- a/homeassistant/components/deconz/translations/sv.json
+++ b/homeassistant/components/deconz/translations/sv.json
@@ -13,8 +13,8 @@
"flow_title": "deCONZ Zigbee gateway ({host})",
"step": {
"hassio_confirm": {
- "description": "Vill du konfigurera Home Assistant att ansluta till den deCONZ-gateway som tillhandah\u00e5lls av Hass.io-till\u00e4gget {addon}?",
- "title": "deCONZ Zigbee gateway via Hass.io till\u00e4gg"
+ "description": "Vill du konfigurera Home Assistant att ansluta till den deCONZ-gateway som tillhandah\u00e5lls av Supervisor-till\u00e4gget {addon}?",
+ "title": "deCONZ Zigbee gateway via Supervisor till\u00e4gg"
},
"link": {
"description": "L\u00e5s upp din deCONZ-gateway f\u00f6r att registrera dig med Home Assistant. \n\n 1. G\u00e5 till deCONZ-systeminst\u00e4llningarna \n 2. Tryck p\u00e5 \"L\u00e5s upp gateway\"-knappen",
@@ -87,7 +87,8 @@
"allow_clip_sensor": "Till\u00e5t deCONZ CLIP-sensorer",
"allow_deconz_groups": "Till\u00e5t deCONZ ljusgrupper"
},
- "description": "Konfigurera synlighet f\u00f6r deCONZ-enhetstyper"
+ "description": "Konfigurera synlighet f\u00f6r deCONZ-enhetstyper",
+ "title": "deCONZ-inst\u00e4llningar"
}
}
}
diff --git a/homeassistant/components/deconz/translations/tr.json b/homeassistant/components/deconz/translations/tr.json
index e73703043f3ce0..22eea1278d744c 100644
--- a/homeassistant/components/deconz/translations/tr.json
+++ b/homeassistant/components/deconz/translations/tr.json
@@ -1,4 +1,47 @@
{
+ "config": {
+ "abort": {
+ "already_configured": "K\u00f6pr\u00fc zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor"
+ },
+ "step": {
+ "manual_input": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Ke\u015ffedilen deCONZ a\u011f ge\u00e7idini se\u00e7in"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "side_4": "Yan 4",
+ "side_5": "Yan 5",
+ "side_6": "Yan 6"
+ },
+ "trigger_type": {
+ "remote_awakened": "Cihaz uyand\u0131",
+ "remote_double_tap": "\" {subtype} \" cihaz\u0131na iki kez hafif\u00e7e vuruldu",
+ "remote_double_tap_any_side": "Cihaz herhangi bir tarafta \u00e7ift dokundu",
+ "remote_falling": "Serbest d\u00fc\u015f\u00fc\u015fte cihaz",
+ "remote_flip_180_degrees": "Cihaz 180 derece d\u00f6nd\u00fcr\u00fcld\u00fc",
+ "remote_flip_90_degrees": "Cihaz 90 derece d\u00f6nd\u00fcr\u00fcld\u00fc",
+ "remote_moved": "Cihaz \" {subtype} \" yukar\u0131 ta\u015f\u0131nd\u0131",
+ "remote_moved_any_side": "Cihaz herhangi bir taraf\u0131 yukar\u0131 gelecek \u015fekilde ta\u015f\u0131nd\u0131",
+ "remote_rotate_from_side_1": "Cihaz, \"1. taraftan\" \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc",
+ "remote_rotate_from_side_2": "Cihaz, \"2. taraftan\" \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc",
+ "remote_rotate_from_side_3": "Cihaz \"3. taraftan\" \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc",
+ "remote_rotate_from_side_4": "Cihaz, \"4. taraf\" dan \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc",
+ "remote_rotate_from_side_5": "Cihaz, \"5. taraf\" dan \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc",
+ "remote_turned_clockwise": "Cihaz saat y\u00f6n\u00fcnde d\u00f6nd\u00fc",
+ "remote_turned_counter_clockwise": "Cihaz saat y\u00f6n\u00fcn\u00fcn tersine d\u00f6nd\u00fc"
+ }
+ },
"options": {
"step": {
"deconz_devices": {
diff --git a/homeassistant/components/deconz/translations/uk.json b/homeassistant/components/deconz/translations/uk.json
new file mode 100644
index 00000000000000..3b09a517385393
--- /dev/null
+++ b/homeassistant/components/deconz/translations/uk.json
@@ -0,0 +1,105 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u043e.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "no_bridges": "\u0428\u043b\u044e\u0437\u0438 deCONZ \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456.",
+ "no_hardware_available": "\u0420\u0430\u0434\u0456\u043e\u043e\u0431\u043b\u0430\u0434\u043d\u0430\u043d\u043d\u044f \u043d\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e deCONZ.",
+ "not_deconz_bridge": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0454 \u0448\u043b\u044e\u0437\u043e\u043c deCONZ.",
+ "updated_instance": "\u0410\u0434\u0440\u0435\u0441\u0443 \u0445\u043e\u0441\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043e."
+ },
+ "error": {
+ "no_key": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u043a\u043b\u044e\u0447 API."
+ },
+ "flow_title": "\u0428\u043b\u044e\u0437 Zigbee deCONZ ({host})",
+ "step": {
+ "hassio_confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e deCONZ (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor \"{addon}\")?",
+ "title": "Zigbee \u0448\u043b\u044e\u0437 deCONZ (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor)"
+ },
+ "link": {
+ "description": "\u0420\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u0457 \u0432 Home Assistant: \n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0434\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u044c \u0441\u0438\u0441\u0442\u0435\u043c\u0438 deCONZ - > Gateway - > Advanced.\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb.",
+ "title": "\u0417\u0432'\u044f\u0437\u043e\u043a \u0437 deCONZ"
+ },
+ "manual_input": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u0438\u0439 \u0448\u043b\u044e\u0437 deCONZ"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "\u041e\u0431\u0438\u0434\u0432\u0456 \u043a\u043d\u043e\u043f\u043a\u0438",
+ "bottom_buttons": "\u041d\u0438\u0436\u043d\u0456 \u043a\u043d\u043e\u043f\u043a\u0438",
+ "button_1": "\u041f\u0435\u0440\u0448\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_2": "\u0414\u0440\u0443\u0433\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_3": "\u0422\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "close": "\u0417\u0430\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f",
+ "dim_down": "\u0417\u043c\u0435\u043d\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c",
+ "dim_up": "\u0417\u0431\u0456\u043b\u044c\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c",
+ "left": "\u041b\u0456\u0432\u043e\u0440\u0443\u0447",
+ "open": "\u0412\u0456\u0434\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f",
+ "right": "\u041f\u0440\u0430\u0432\u043e\u0440\u0443\u0447",
+ "side_1": "\u0413\u0440\u0430\u043d\u044c 1",
+ "side_2": "\u0413\u0440\u0430\u043d\u044c 2",
+ "side_3": "\u0413\u0440\u0430\u043d\u044c 3",
+ "side_4": "\u0413\u0440\u0430\u043d\u044c 4",
+ "side_5": "\u0413\u0440\u0430\u043d\u044c 5",
+ "side_6": "\u0413\u0440\u0430\u043d\u044c 6",
+ "top_buttons": "\u0412\u0435\u0440\u0445\u043d\u0456 \u043a\u043d\u043e\u043f\u043a\u0438",
+ "turn_off": "\u0412\u0438\u043c\u043a\u043d\u0443\u0442\u0438",
+ "turn_on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438"
+ },
+ "trigger_type": {
+ "remote_awakened": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0440\u043e\u0437\u0431\u0443\u0434\u0438\u043b\u0438",
+ "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0438",
+ "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u0434\u043e\u0432\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430",
+ "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f",
+ "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0447\u043e\u0442\u0438\u0440\u0438 \u0440\u0430\u0437\u0438",
+ "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u043f'\u044f\u0442\u044c \u0440\u0430\u0437\u0456\u0432",
+ "remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u0430",
+ "remote_button_rotated_fast": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u0430 \u0448\u0432\u0438\u0434\u043a\u043e",
+ "remote_button_rotation_stopped": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u0440\u0438\u043f\u0438\u043d\u0438\u043b\u0430 \u043e\u0431\u0435\u0440\u0442\u0430\u043d\u043d\u044f",
+ "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430",
+ "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f",
+ "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0438",
+ "remote_double_tap": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c {subtype} \u043f\u043e\u0441\u0442\u0443\u043a\u0430\u043b\u0438 \u0434\u0432\u0456\u0447\u0456",
+ "remote_double_tap_any_side": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c \u043f\u043e\u0441\u0442\u0443\u043a\u0430\u043b\u0438 \u0434\u0432\u0456\u0447\u0456",
+ "remote_falling": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0443 \u0432\u0456\u043b\u044c\u043d\u043e\u043c\u0443 \u043f\u0430\u0434\u0456\u043d\u043d\u0456",
+ "remote_flip_180_degrees": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043d\u0430 180 \u0433\u0440\u0430\u0434\u0443\u0441\u0456\u0432",
+ "remote_flip_90_degrees": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043d\u0430 90 \u0433\u0440\u0430\u0434\u0443\u0441\u0456\u0432",
+ "remote_gyro_activated": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0442\u0440\u044f\u0441\u043b\u0438",
+ "remote_moved": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0437\u0440\u0443\u0448\u0438\u043b\u0438, \u043a\u043e\u043b\u0438 {subtype} \u0437\u0432\u0435\u0440\u0445\u0443",
+ "remote_moved_any_side": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u043c\u0456\u0441\u0442\u0438\u043b\u0438",
+ "remote_rotate_from_side_1": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 1 \u043d\u0430 {subtype}",
+ "remote_rotate_from_side_2": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 2 \u043d\u0430 {subtype}",
+ "remote_rotate_from_side_3": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 3 \u043d\u0430 {subtype}",
+ "remote_rotate_from_side_4": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 4 \u043d\u0430 {subtype}",
+ "remote_rotate_from_side_5": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 5 \u043d\u0430 {subtype}",
+ "remote_rotate_from_side_6": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 6 \u043d\u0430 {subtype}",
+ "remote_turned_clockwise": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437\u0430 \u0433\u043e\u0434\u0438\u043d\u043d\u0438\u043a\u043e\u0432\u043e\u044e \u0441\u0442\u0440\u0456\u043b\u043a\u043e\u044e",
+ "remote_turned_counter_clockwise": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043f\u0440\u043e\u0442\u0438 \u0433\u043e\u0434\u0438\u043d\u043d\u0438\u043a\u043e\u0432\u043e\u0457 \u0441\u0442\u0440\u0456\u043b\u043a\u0438"
+ }
+ },
+ "options": {
+ "step": {
+ "deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "\u0412\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 deCONZ CLIP",
+ "allow_deconz_groups": "\u0412\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u0438 \u0433\u0440\u0443\u043f\u0438 \u043e\u0441\u0432\u0456\u0442\u043b\u0435\u043d\u043d\u044f deCONZ",
+ "allow_new_devices": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f \u043d\u043e\u0432\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0456 \u0442\u0438\u043f\u0456\u0432 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 deCONZ",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f deCONZ"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/translations/zh-Hans.json b/homeassistant/components/deconz/translations/zh-Hans.json
index a85ed6d72ca190..dfe8209fa1c93b 100644
--- a/homeassistant/components/deconz/translations/zh-Hans.json
+++ b/homeassistant/components/deconz/translations/zh-Hans.json
@@ -21,11 +21,11 @@
"side_3": "\u7b2c 3 \u9762",
"side_4": "\u7b2c 4 \u9762",
"side_5": "\u7b2c 5 \u9762",
- "side_6": "\u7b2c 6 \u9762",
- "turn_off": "\u5173\u95ed"
+ "side_6": "\u7b2c 6 \u9762"
},
"trigger_type": {
"remote_awakened": "\u8bbe\u5907\u5524\u9192",
+ "remote_button_rotation_stopped": "\u6309\u94ae \"{subtype}\" \u505c\u6b62\u65cb\u8f6c",
"remote_double_tap": "\u8bbe\u5907\u7684\u201c{subtype}\u201d\u88ab\u8f7b\u6572\u4e24\u6b21",
"remote_falling": "\u8bbe\u5907\u81ea\u7531\u843d\u4f53",
"remote_gyro_activated": "\u8bbe\u5907\u6447\u6643",
diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json
index 335aa73a67cfb4..a80afaf46954f5 100644
--- a/homeassistant/components/deconz/translations/zh-Hant.json
+++ b/homeassistant/components/deconz/translations/zh-Hant.json
@@ -14,8 +14,8 @@
"flow_title": "deCONZ Zigbee \u9598\u9053\u5668\uff08{host}\uff09",
"step": {
"hassio_confirm": {
- "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Hass.io \u9644\u52a0\u6574\u5408 {addon} \u4e4b deCONZ \u9598\u9053\u5668\uff1f",
- "title": "\u900f\u904e Hass.io \u9644\u52a0\u7d44\u4ef6 deCONZ Zigbee \u9598\u9053\u5668"
+ "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 deCONZ \u9598\u9053\u5668\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f",
+ "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 deCONZ Zigbee \u9598\u9053\u5668"
},
"link": {
"description": "\u89e3\u9664 deCONZ \u9598\u9053\u5668\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a -> \u9598\u9053\u5668 -> \u9032\u968e\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u8a8d\u8b49\u7a0b\u5f0f\uff08Authenticate app\uff09\u300d\u6309\u9215",
@@ -42,6 +42,10 @@
"button_2": "\u7b2c\u4e8c\u500b\u6309\u9215",
"button_3": "\u7b2c\u4e09\u500b\u6309\u9215",
"button_4": "\u7b2c\u56db\u500b\u6309\u9215",
+ "button_5": "\u7b2c\u4e94\u500b\u6309\u9215",
+ "button_6": "\u7b2c\u516d\u500b\u6309\u9215",
+ "button_7": "\u7b2c\u4e03\u500b\u6309\u9215",
+ "button_8": "\u7b2c\u516b\u500b\u6309\u9215",
"close": "\u95dc\u9589",
"dim_down": "\u8abf\u6697",
"dim_up": "\u8abf\u4eae",
diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py
index 2fc436eda20e1e..45c42c4bb1c2e2 100644
--- a/homeassistant/components/decora/light.py
+++ b/homeassistant/components/decora/light.py
@@ -4,10 +4,8 @@
import logging
import time
-from bluepy.btle import ( # pylint: disable=import-error, no-member, no-name-in-module
- BTLEException,
-)
-import decora # pylint: disable=import-error, no-member
+from bluepy.btle import BTLEException # pylint: disable=import-error
+import decora # pylint: disable=import-error
import voluptuous as vol
from homeassistant.components.light import (
@@ -64,7 +62,7 @@ def wrapper_retry(device, *args, **kwargs):
return method(device, *args, **kwargs)
except (decora.decoraException, AttributeError, BTLEException):
_LOGGER.warning(
- "Decora connect error for device %s. Reconnecting...",
+ "Decora connect error for device %s. Reconnecting",
device.name,
)
# pylint: disable=protected-access
diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json
index f8be3c9fe2aedc..0f4b940cc36811 100644
--- a/homeassistant/components/default_config/manifest.json
+++ b/homeassistant/components/default_config/manifest.json
@@ -18,6 +18,7 @@
"map",
"media_source",
"mobile_app",
+ "my",
"person",
"scene",
"script",
diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py
index 8c73fecf26ee6e..cff93c899541e4 100644
--- a/homeassistant/components/delijn/sensor.py
+++ b/homeassistant/components/delijn/sensor.py
@@ -5,11 +5,10 @@
from pydelijn.common import HttpException
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, DEVICE_CLASS_TIMESTAMP
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -17,7 +16,6 @@
CONF_NEXT_DEPARTURE = "next_departure"
CONF_STOP_ID = "stop_id"
-CONF_API_KEY = "api_key"
CONF_NUMBER_OF_DEPARTURES = "number_of_departures"
DEFAULT_NAME = "De Lijn"
@@ -59,7 +57,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(sensors, True)
-class DeLijnPublicTransportSensor(Entity):
+class DeLijnPublicTransportSensor(SensorEntity):
"""Representation of a Ruter sensor."""
def __init__(self, line):
@@ -127,6 +125,6 @@ def icon(self):
return "mdi:bus"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return attributes for the sensor."""
return self._attributes
diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py
index 5e8df89c20d748..0c79e6f835e8c3 100644
--- a/homeassistant/components/deluge/sensor.py
+++ b/homeassistant/components/deluge/sensor.py
@@ -4,7 +4,7 @@
from deluge_client import DelugeRPCClient, FailedToReconnectException
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_HOST,
CONF_MONITORED_VARIABLES,
@@ -17,7 +17,6 @@
)
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
_THROTTLED_REFRESH = None
@@ -68,7 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(dev)
-class DelugeSensor(Entity):
+class DelugeSensor(SensorEntity):
"""Representation of a Deluge sensor."""
def __init__(self, sensor_type, deluge_client, client_name):
diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py
index 09c3d27a1bc1cf..b32537ae44e01a 100644
--- a/homeassistant/components/demo/__init__.py
+++ b/homeassistant/components/demo/__init__.py
@@ -50,9 +50,9 @@ async def async_setup(hass, config):
)
# Set up demo platforms
- for component in COMPONENTS_WITH_DEMO_PLATFORM:
+ for platform in COMPONENTS_WITH_DEMO_PLATFORM:
hass.async_create_task(
- hass.helpers.discovery.async_load_platform(component, DOMAIN, {}, config)
+ hass.helpers.discovery.async_load_platform(platform, DOMAIN, {}, config)
)
config.setdefault(ha.DOMAIN, {})
@@ -146,9 +146,9 @@ async def demo_start_listener(_event):
async def async_setup_entry(hass, config_entry):
"""Set the config entry up."""
# Set up demo platforms with config entry
- for component in COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM:
+ for platform in COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return True
diff --git a/homeassistant/components/demo/config_flow.py b/homeassistant/components/demo/config_flow.py
index 35f72e9e0cd6d4..f99693bfeb25f3 100644
--- a/homeassistant/components/demo/config_flow.py
+++ b/homeassistant/components/demo/config_flow.py
@@ -5,7 +5,6 @@
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-# pylint: disable=unused-import
from . import DOMAIN
CONF_STRING = "string"
diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py
index bd6661b6c2b6ae..c406ffdb2143ec 100644
--- a/homeassistant/components/demo/fan.py
+++ b/homeassistant/components/demo/fan.py
@@ -1,5 +1,5 @@
"""Demo fan platform that has a fake fan."""
-from typing import List, Optional
+from __future__ import annotations
from homeassistant.components.fan import (
SPEED_HIGH,
@@ -15,6 +15,8 @@
PRESET_MODE_AUTO = "auto"
PRESET_MODE_SMART = "smart"
+PRESET_MODE_SLEEP = "sleep"
+PRESET_MODE_ON = "on"
FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION
LIMITED_SUPPORT = SUPPORT_SET_SPEED
@@ -38,6 +40,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
SPEED_HIGH,
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
+ PRESET_MODE_SLEEP,
+ PRESET_MODE_ON,
],
),
DemoFan(
@@ -54,7 +58,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"fan3",
"Percentage Full Fan",
FULL_SUPPORT,
- [PRESET_MODE_AUTO, PRESET_MODE_SMART],
+ [
+ PRESET_MODE_AUTO,
+ PRESET_MODE_SMART,
+ PRESET_MODE_SLEEP,
+ PRESET_MODE_ON,
+ ],
None,
),
DemoPercentageFan(
@@ -62,7 +71,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"fan4",
"Percentage Limited Fan",
LIMITED_SUPPORT,
- [PRESET_MODE_AUTO, PRESET_MODE_SMART],
+ [
+ PRESET_MODE_AUTO,
+ PRESET_MODE_SMART,
+ PRESET_MODE_SLEEP,
+ PRESET_MODE_ON,
+ ],
None,
),
AsyncDemoPercentageFan(
@@ -70,7 +84,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"fan5",
"Preset Only Limited Fan",
SUPPORT_PRESET_MODE,
- [PRESET_MODE_AUTO, PRESET_MODE_SMART],
+ [
+ PRESET_MODE_AUTO,
+ PRESET_MODE_SMART,
+ PRESET_MODE_SLEEP,
+ PRESET_MODE_ON,
+ ],
[],
),
]
@@ -91,15 +110,15 @@ def __init__(
unique_id: str,
name: str,
supported_features: int,
- preset_modes: Optional[List[str]],
- speed_list: Optional[List[str]],
+ preset_modes: list[str] | None,
+ speed_list: list[str] | None,
) -> None:
"""Initialize the entity."""
self.hass = hass
self._unique_id = unique_id
self._supported_features = supported_features
self._speed = SPEED_OFF
- self._percentage = 0
+ self._percentage = None
self._speed_list = speed_list
self._preset_modes = preset_modes
self._preset_mode = None
@@ -192,10 +211,15 @@ class DemoPercentageFan(BaseDemoFan, FanEntity):
"""A demonstration fan component that uses percentages."""
@property
- def percentage(self) -> str:
+ def percentage(self) -> int | None:
"""Return the current speed."""
return self._percentage
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return 3
+
def set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage."""
self._percentage = percentage
@@ -203,12 +227,12 @@ def set_percentage(self, percentage: int) -> None:
self.schedule_update_ha_state()
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., auto, smart, interval, favorite."""
return self._preset_mode
@property
- def preset_modes(self) -> Optional[List[str]]:
+ def preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes."""
return self._preset_modes
@@ -247,10 +271,15 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity):
"""An async demonstration fan component that uses percentages."""
@property
- def percentage(self) -> str:
+ def percentage(self) -> int | None:
"""Return the current speed."""
return self._percentage
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return 3
+
async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage."""
self._percentage = percentage
@@ -258,12 +287,12 @@ async def async_set_percentage(self, percentage: int) -> None:
self.async_write_ha_state()
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., auto, smart, interval, favorite."""
return self._preset_mode
@property
- def preset_modes(self) -> Optional[List[str]]:
+ def preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes."""
return self._preset_modes
diff --git a/homeassistant/components/demo/geo_location.py b/homeassistant/components/demo/geo_location.py
index 76dea52e846671..f288a2fd340b88 100644
--- a/homeassistant/components/demo/geo_location.py
+++ b/homeassistant/components/demo/geo_location.py
@@ -1,9 +1,10 @@
"""Demo platform for the geolocation component."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
from math import cos, pi, radians, sin
import random
-from typing import Optional
from homeassistant.components.geo_location import GeolocationEvent
from homeassistant.const import LENGTH_KILOMETERS
@@ -117,7 +118,7 @@ def source(self) -> str:
return SOURCE
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the event."""
return self._name
@@ -127,17 +128,17 @@ def should_poll(self):
return False
@property
- def distance(self) -> Optional[float]:
+ def distance(self) -> float | None:
"""Return distance value of this external event."""
return self._distance
@property
- def latitude(self) -> Optional[float]:
+ def latitude(self) -> float | None:
"""Return latitude value of this external event."""
return self._latitude
@property
- def longitude(self) -> Optional[float]:
+ def longitude(self) -> float | None:
"""Return longitude value of this external event."""
return self._longitude
diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py
index 2ea360ebb81c4c..ea315707deae93 100644
--- a/homeassistant/components/demo/media_player.py
+++ b/homeassistant/components/demo/media_player.py
@@ -6,6 +6,7 @@
MEDIA_TYPE_TVSHOW,
REPEAT_MODE_OFF,
SUPPORT_CLEAR_PLAYLIST,
+ SUPPORT_GROUPING,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY,
@@ -40,6 +41,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"Bedroom", "kxopViU98Xo", "Epic sax guy 10 hours", 360000
),
DemoMusicPlayer(),
+ DemoMusicPlayer("Kitchen"),
DemoTVShowPlayer(),
]
)
@@ -73,6 +75,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
| SUPPORT_TURN_ON
| SUPPORT_TURN_OFF
| SUPPORT_CLEAR_PLAYLIST
+ | SUPPORT_GROUPING
| SUPPORT_PLAY
| SUPPORT_SHUFFLE_SET
| SUPPORT_REPEAT_SET
@@ -291,7 +294,7 @@ def media_pause(self):
class DemoMusicPlayer(AbstractDemoPlayer):
- """A Demo media player that only supports YouTube."""
+ """A Demo media player."""
# We only implement the methods that we support
@@ -318,12 +321,18 @@ class DemoMusicPlayer(AbstractDemoPlayer):
),
]
- def __init__(self):
+ def __init__(self, name="Walkman"):
"""Initialize the demo device."""
- super().__init__("Walkman")
+ super().__init__(name)
self._cur_track = 0
+ self._group_members = []
self._repeat = REPEAT_MODE_OFF
+ @property
+ def group_members(self):
+ """List of players which are currently grouped together."""
+ return self._group_members
+
@property
def media_content_id(self):
"""Return the content ID of current playing media."""
@@ -398,6 +407,18 @@ def set_repeat(self, repeat):
self._repeat = repeat
self.schedule_update_ha_state()
+ def join_players(self, group_members):
+ """Join `group_members` as a player group with the current player."""
+ self._group_members = [
+ self.entity_id,
+ ] + group_members
+ self.schedule_update_ha_state()
+
+ def unjoin_player(self):
+ """Remove this player from any group."""
+ self._group_members = []
+ self.schedule_update_ha_state()
+
class DemoTVShowPlayer(AbstractDemoPlayer):
"""A Demo media player that only supports YouTube."""
diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py
index 5a6ce5f5c64954..f3fd815f621b2c 100644
--- a/homeassistant/components/demo/number.py
+++ b/homeassistant/components/demo/number.py
@@ -21,7 +21,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
DemoNumber(
"pwm1",
"PWM 1",
- 42.0,
+ 0.42,
"mdi:square-wave",
False,
0.0,
diff --git a/homeassistant/components/demo/remote.py b/homeassistant/components/demo/remote.py
index 9d12621fef1341..98e949f38c33a7 100644
--- a/homeassistant/components/demo/remote.py
+++ b/homeassistant/components/demo/remote.py
@@ -49,7 +49,7 @@ def is_on(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device state attributes."""
if self._last_command_sent is not None:
return {"last_command_sent": self._last_command_sent}
diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py
index 99aadf356d7882..7607bad4e1ce72 100644
--- a/homeassistant/components/demo/sensor.py
+++ b/homeassistant/components/demo/sensor.py
@@ -1,12 +1,15 @@
"""Demo platform that has a couple of fake sensors."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
+ CONCENTRATION_PARTS_PER_MILLION,
+ DEVICE_CLASS_CO,
+ DEVICE_CLASS_CO2,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
PERCENTAGE,
TEMP_CELSIUS,
)
-from homeassistant.helpers.entity import Entity
from . import DOMAIN
@@ -31,6 +34,22 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
PERCENTAGE,
None,
),
+ DemoSensor(
+ "sensor_3",
+ "Carbon monoxide",
+ 54,
+ DEVICE_CLASS_CO,
+ CONCENTRATION_PARTS_PER_MILLION,
+ None,
+ ),
+ DemoSensor(
+ "sensor_4",
+ "Carbon dioxide",
+ 54,
+ DEVICE_CLASS_CO2,
+ CONCENTRATION_PARTS_PER_MILLION,
+ 14,
+ ),
]
)
@@ -40,7 +59,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
await async_setup_platform(hass, {}, async_add_entities)
-class DemoSensor(Entity):
+class DemoSensor(SensorEntity):
"""Representation of a Demo sensor."""
def __init__(
@@ -96,7 +115,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._battery:
return {ATTR_BATTERY_LEVEL: self._battery}
diff --git a/homeassistant/components/demo/stt.py b/homeassistant/components/demo/stt.py
index e0367fad6a920c..0497e2335d3d02 100644
--- a/homeassistant/components/demo/stt.py
+++ b/homeassistant/components/demo/stt.py
@@ -1,5 +1,5 @@
"""Support for the demo for speech to text service."""
-from typing import List
+from __future__ import annotations
from aiohttp import StreamReader
@@ -25,32 +25,32 @@ class DemoProvider(Provider):
"""Demo speech API provider."""
@property
- def supported_languages(self) -> List[str]:
+ def supported_languages(self) -> list[str]:
"""Return a list of supported languages."""
return SUPPORT_LANGUAGES
@property
- def supported_formats(self) -> List[AudioFormats]:
+ def supported_formats(self) -> list[AudioFormats]:
"""Return a list of supported formats."""
return [AudioFormats.WAV]
@property
- def supported_codecs(self) -> List[AudioCodecs]:
+ def supported_codecs(self) -> list[AudioCodecs]:
"""Return a list of supported codecs."""
return [AudioCodecs.PCM]
@property
- def supported_bit_rates(self) -> List[AudioBitRates]:
+ def supported_bit_rates(self) -> list[AudioBitRates]:
"""Return a list of supported bit rates."""
return [AudioBitRates.BITRATE_16]
@property
- def supported_sample_rates(self) -> List[AudioSampleRates]:
+ def supported_sample_rates(self) -> list[AudioSampleRates]:
"""Return a list of supported sample rates."""
return [AudioSampleRates.SAMPLERATE_16000, AudioSampleRates.SAMPLERATE_44100]
@property
- def supported_channels(self) -> List[AudioChannels]:
+ def supported_channels(self) -> list[AudioChannels]:
"""Return a list of supported channels."""
return [AudioChannels.CHANNEL_STEREO]
diff --git a/homeassistant/components/demo/translations/id.json b/homeassistant/components/demo/translations/id.json
new file mode 100644
index 00000000000000..8adbeb3e3c4ed8
--- /dev/null
+++ b/homeassistant/components/demo/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "options": {
+ "step": {
+ "options_1": {
+ "data": {
+ "bool": "Boolean opsional",
+ "constant": "Konstanta",
+ "int": "Input numerik"
+ }
+ },
+ "options_2": {
+ "data": {
+ "multi": "Pilihan ganda",
+ "select": "Pilih salah satu opsi",
+ "string": "Nilai string"
+ }
+ }
+ }
+ },
+ "title": "Demo"
+}
\ No newline at end of file
diff --git a/homeassistant/components/demo/translations/nl.json b/homeassistant/components/demo/translations/nl.json
index ac10172933f14e..8e7c97f7c3f1b8 100644
--- a/homeassistant/components/demo/translations/nl.json
+++ b/homeassistant/components/demo/translations/nl.json
@@ -10,6 +10,7 @@
"options_1": {
"data": {
"bool": "Optioneel Boolean",
+ "constant": "Constant",
"int": "Numerieke invoer"
}
},
diff --git a/homeassistant/components/demo/translations/tr.json b/homeassistant/components/demo/translations/tr.json
new file mode 100644
index 00000000000000..1ca389b0b979b9
--- /dev/null
+++ b/homeassistant/components/demo/translations/tr.json
@@ -0,0 +1,12 @@
+{
+ "options": {
+ "step": {
+ "options_1": {
+ "data": {
+ "constant": "Sabit"
+ }
+ }
+ }
+ },
+ "title": "Demo"
+}
\ No newline at end of file
diff --git a/homeassistant/components/demo/translations/uk.json b/homeassistant/components/demo/translations/uk.json
new file mode 100644
index 00000000000000..5ac1ac74708e18
--- /dev/null
+++ b/homeassistant/components/demo/translations/uk.json
@@ -0,0 +1,21 @@
+{
+ "options": {
+ "step": {
+ "options_1": {
+ "data": {
+ "bool": "\u041b\u043e\u0433\u0456\u0447\u043d\u0438\u0439",
+ "constant": "\u041f\u043e\u0441\u0442\u0456\u0439\u043d\u0430",
+ "int": "\u0427\u0438\u0441\u043b\u043e\u0432\u0438\u0439"
+ }
+ },
+ "options_2": {
+ "data": {
+ "multi": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0434\u0435\u043a\u0456\u043b\u044c\u043a\u0430",
+ "select": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u043f\u0446\u0456\u044e",
+ "string": "\u0421\u0442\u0440\u043e\u043a\u043e\u0432\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f"
+ }
+ }
+ }
+ },
+ "title": "\u0414\u0435\u043c\u043e"
+}
\ No newline at end of file
diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py
index ebb6b57ce14ec5..39413a1b9f7e9a 100644
--- a/homeassistant/components/demo/vacuum.py
+++ b/homeassistant/components/demo/vacuum.py
@@ -140,7 +140,7 @@ def battery_level(self):
return max(0, min(100, self._battery_level))
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device state attributes."""
return {ATTR_CLEANED_AREA: round(self._cleaned_area, 2)}
@@ -288,7 +288,7 @@ def fan_speed_list(self):
return FAN_SPEEDS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device state attributes."""
return {ATTR_CLEANED_AREA: round(self._cleaned_area, 2)}
diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py
index b909dc7c07008c..82427f8aa16e33 100644
--- a/homeassistant/components/denon/media_player.py
+++ b/homeassistant/components/denon/media_player.py
@@ -230,7 +230,7 @@ def is_volume_muted(self):
@property
def source_list(self):
"""Return the list of available input sources."""
- return sorted(list(self._source_list))
+ return sorted(self._source_list)
@property
def media_title(self):
diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py
index 89c6413d1462a5..fa4d1612697314 100644
--- a/homeassistant/components/denonavr/__init__.py
+++ b/homeassistant/components/denonavr/__init__.py
@@ -1,13 +1,13 @@
"""The denonavr component."""
import logging
-import voluptuous as vol
+from denonavr.exceptions import AvrNetworkError, AvrTimoutError
from homeassistant import config_entries, core
-from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST
+from homeassistant.const import CONF_HOST
from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers import config_validation as cv, entity_registry as er
-from homeassistant.helpers.dispatcher import dispatcher_send
+from homeassistant.helpers import entity_registry as er
+from homeassistant.helpers.httpx_client import get_async_client
from .config_flow import (
CONF_SHOW_ALL_SOURCES,
@@ -23,35 +23,9 @@
CONF_RECEIVER = "receiver"
UNDO_UPDATE_LISTENER = "undo_update_listener"
-SERVICE_GET_COMMAND = "get_command"
-ATTR_COMMAND = "command"
_LOGGER = logging.getLogger(__name__)
-CALL_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids})
-
-GET_COMMAND_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_COMMAND): cv.string})
-
-SERVICE_TO_METHOD = {
- SERVICE_GET_COMMAND: {"method": "get_command", "schema": GET_COMMAND_SCHEMA}
-}
-
-
-def setup(hass: core.HomeAssistant, config: dict):
- """Set up the denonavr platform."""
-
- def service_handler(service):
- method = SERVICE_TO_METHOD.get(service.service)
- data = service.data.copy()
- data["method"] = method["method"]
- dispatcher_send(hass, DOMAIN, data)
-
- for service in SERVICE_TO_METHOD:
- schema = SERVICE_TO_METHOD[service]["schema"]
- hass.services.register(DOMAIN, service, service_handler, schema=schema)
-
- return True
-
async def async_setup_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
@@ -61,15 +35,18 @@ async def async_setup_entry(
# Connect to receiver
connect_denonavr = ConnectDenonAVR(
- hass,
entry.data[CONF_HOST],
DEFAULT_TIMEOUT,
entry.options.get(CONF_SHOW_ALL_SOURCES, DEFAULT_SHOW_SOURCES),
entry.options.get(CONF_ZONE2, DEFAULT_ZONE2),
entry.options.get(CONF_ZONE3, DEFAULT_ZONE3),
+ lambda: get_async_client(hass),
+ entry.state,
)
- if not await connect_denonavr.async_connect_receiver():
- raise ConfigEntryNotReady
+ try:
+ await connect_denonavr.async_connect_receiver()
+ except (AvrNetworkError, AvrTimoutError) as ex:
+ raise ConfigEntryNotReady from ex
receiver = connect_denonavr.receiver
undo_listener = entry.add_update_listener(update_listener)
@@ -99,8 +76,9 @@ async def async_unload_entry(
# Remove zone2 and zone3 entities if needed
entity_registry = await er.async_get_registry(hass)
entries = er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)
- zone2_id = f"{config_entry.unique_id}-Zone2"
- zone3_id = f"{config_entry.unique_id}-Zone3"
+ unique_id = config_entry.unique_id or config_entry.entry_id
+ zone2_id = f"{unique_id}-Zone2"
+ zone3_id = f"{unique_id}-Zone3"
for entry in entries:
if entry.unique_id == zone2_id and not config_entry.options.get(CONF_ZONE2):
entity_registry.async_remove(entry.entity_id)
diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py
index 21c8da01783588..f2c37d9fc75679 100644
--- a/homeassistant/components/denonavr/config_flow.py
+++ b/homeassistant/components/denonavr/config_flow.py
@@ -1,17 +1,17 @@
"""Config flow to configure Denon AVR receivers using their HTTP interface."""
-from functools import partial
import logging
+from typing import Any, Dict, Optional
from urllib.parse import urlparse
import denonavr
-from getmac import get_mac_address
+from denonavr.exceptions import AvrNetworkError, AvrTimoutError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import ssdp
-from homeassistant.const import CONF_HOST, CONF_MAC
+from homeassistant.const import CONF_HOST, CONF_TYPE
from homeassistant.core import callback
-from homeassistant.helpers.device_registry import format_mac
+from homeassistant.helpers.httpx_client import get_async_client
from .receiver import ConnectDenonAVR
@@ -25,7 +25,6 @@
CONF_SHOW_ALL_SOURCES = "show_all_sources"
CONF_ZONE2 = "zone2"
CONF_ZONE3 = "zone3"
-CONF_TYPE = "type"
CONF_MODEL = "model"
CONF_MANUFACTURER = "manufacturer"
CONF_SERIAL_NUMBER = "serial_number"
@@ -45,7 +44,7 @@ def __init__(self, config_entry: config_entries.ConfigEntry):
"""Init object."""
self.config_entry = config_entry
- async def async_step_init(self, user_input=None):
+ async def async_step_init(self, user_input: Optional[Dict[str, Any]] = None):
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
@@ -91,11 +90,13 @@ def __init__(self):
@staticmethod
@callback
- def async_get_options_flow(config_entry) -> OptionsFlowHandler:
+ def async_get_options_flow(
+ config_entry: config_entries.ConfigEntry,
+ ) -> OptionsFlowHandler:
"""Get the options flow."""
return OptionsFlowHandler(config_entry)
- async def async_step_user(self, user_input=None):
+ async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None):
"""Handle a flow initialized by the user."""
errors = {}
if user_input is not None:
@@ -106,7 +107,7 @@ async def async_step_user(self, user_input=None):
return await self.async_step_connect()
# discovery using denonavr library
- self.d_receivers = await self.hass.async_add_executor_job(denonavr.discover)
+ self.d_receivers = await denonavr.async_discover()
# More than one receiver could be discovered by that method
if len(self.d_receivers) == 1:
self.host = self.d_receivers[0]["host"]
@@ -121,7 +122,9 @@ async def async_step_user(self, user_input=None):
step_id="user", data_schema=CONFIG_SCHEMA, errors=errors
)
- async def async_step_select(self, user_input=None):
+ async def async_step_select(
+ self, user_input: Optional[Dict[str, Any]] = None
+ ) -> Dict[str, Any]:
"""Handle multiple receivers found."""
errors = {}
if user_input is not None:
@@ -140,29 +143,37 @@ async def async_step_select(self, user_input=None):
step_id="select", data_schema=select_scheme, errors=errors
)
- async def async_step_confirm(self, user_input=None):
+ async def async_step_confirm(
+ self, user_input: Optional[Dict[str, Any]] = None
+ ) -> Dict[str, Any]:
"""Allow the user to confirm adding the device."""
if user_input is not None:
return await self.async_step_connect()
+ self._set_confirm_only()
return self.async_show_form(step_id="confirm")
- async def async_step_connect(self, user_input=None):
+ async def async_step_connect(
+ self, user_input: Optional[Dict[str, Any]] = None
+ ) -> Dict[str, Any]:
"""Connect to the receiver."""
connect_denonavr = ConnectDenonAVR(
- self.hass,
self.host,
self.timeout,
self.show_all_sources,
self.zone2,
self.zone3,
+ lambda: get_async_client(self.hass),
)
- if not await connect_denonavr.async_connect_receiver():
+
+ try:
+ success = await connect_denonavr.async_connect_receiver()
+ except (AvrNetworkError, AvrTimoutError):
+ success = False
+ if not success:
return self.async_abort(reason="cannot_connect")
receiver = connect_denonavr.receiver
- mac_address = await self.async_get_mac(self.host)
-
if not self.serial_number:
self.serial_number = receiver.serial_number
if not self.model_name:
@@ -186,7 +197,6 @@ async def async_step_connect(self, user_input=None):
title=receiver.name,
data={
CONF_HOST: self.host,
- CONF_MAC: mac_address,
CONF_TYPE: receiver.receiver_type,
CONF_MODEL: self.model_name,
CONF_MANUFACTURER: receiver.manufacturer,
@@ -194,7 +204,7 @@ async def async_step_connect(self, user_input=None):
},
)
- async def async_step_ssdp(self, discovery_info):
+ async def async_step_ssdp(self, discovery_info: Dict[str, Any]) -> Dict[str, Any]:
"""Handle a discovered Denon AVR.
This flow is triggered by the SSDP component. It will check if the
@@ -225,7 +235,6 @@ async def async_step_ssdp(self, discovery_info):
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured({CONF_HOST: self.host})
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update(
{
"title_placeholders": {
@@ -237,24 +246,6 @@ async def async_step_ssdp(self, discovery_info):
return await self.async_step_confirm()
@staticmethod
- def construct_unique_id(model_name, serial_number):
+ def construct_unique_id(model_name: str, serial_number: str) -> str:
"""Construct the unique id from the ssdp discovery or user_step."""
return f"{model_name}-{serial_number}"
-
- async def async_get_mac(self, host):
- """Get the mac address of the DenonAVR receiver."""
- try:
- mac_address = await self.hass.async_add_executor_job(
- partial(get_mac_address, **{"ip": host})
- )
- if not mac_address:
- mac_address = await self.hass.async_add_executor_job(
- partial(get_mac_address, **{"hostname": host})
- )
- except Exception as err: # pylint: disable=broad-except
- _LOGGER.error("Unable to get mac address: %s", err)
- mac_address = None
-
- if mac_address is not None:
- mac_address = format_mac(mac_address)
- return mac_address
diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json
index 8d2052181f8720..e4cdaa03724032 100644
--- a/homeassistant/components/denonavr/manifest.json
+++ b/homeassistant/components/denonavr/manifest.json
@@ -3,7 +3,7 @@
"name": "Denon AVR Network Receivers",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/denonavr",
- "requirements": ["denonavr==0.9.10", "getmac==0.8.2"],
+ "requirements": ["denonavr==0.10.5"],
"codeowners": ["@scarface-4711", "@starkillerOG"],
"ssdp": [
{
diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py
index 73fe0f2152d464..799f07ed71b6ac 100644
--- a/homeassistant/components/denonavr/media_player.py
+++ b/homeassistant/components/denonavr/media_player.py
@@ -1,7 +1,22 @@
"""Support for Denon AVR receivers using their HTTP interface."""
+from datetime import timedelta
+from functools import wraps
import logging
+from typing import Coroutine
+
+from denonavr import DenonAVR
+from denonavr.const import POWER_ON
+from denonavr.exceptions import (
+ AvrCommandError,
+ AvrForbiddenError,
+ AvrNetworkError,
+ AvrTimoutError,
+ DenonAvrError,
+)
+import voluptuous as vol
+from homeassistant import config_entries
from homeassistant.components.media_player import MediaPlayerEntity
from homeassistant.components.media_player.const import (
MEDIA_TYPE_CHANNEL,
@@ -19,18 +34,9 @@
SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP,
)
-from homeassistant.const import (
- ATTR_ENTITY_ID,
- CONF_MAC,
- ENTITY_MATCH_ALL,
- ENTITY_MATCH_NONE,
- STATE_OFF,
- STATE_ON,
- STATE_PAUSED,
- STATE_PLAYING,
-)
-from homeassistant.helpers import device_registry as dr
-from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.const import ATTR_COMMAND, STATE_PAUSED, STATE_PLAYING
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import config_validation as cv, entity_platform
from . import CONF_RECEIVER
from .config_flow import (
@@ -63,8 +69,18 @@
| SUPPORT_PLAY
)
+SCAN_INTERVAL = timedelta(seconds=10)
+PARALLEL_UPDATES = 1
+
+# Services
+SERVICE_GET_COMMAND = "get_command"
-async def async_setup_entry(hass, config_entry, async_add_entities):
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: config_entries.ConfigEntry,
+ async_add_entities: entity_platform.EntityPlatform.async_add_entities,
+):
"""Set up the DenonAVR receiver from a config entry."""
entities = []
receiver = hass.data[DOMAIN][config_entry.entry_id][CONF_RECEIVER]
@@ -72,93 +88,116 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
if config_entry.data[CONF_SERIAL_NUMBER] is not None:
unique_id = f"{config_entry.unique_id}-{receiver_zone.zone}"
else:
- unique_id = None
+ unique_id = f"{config_entry.entry_id}-{receiver_zone.zone}"
+ await receiver_zone.async_setup()
entities.append(DenonDevice(receiver_zone, unique_id, config_entry))
_LOGGER.debug(
"%s receiver at host %s initialized", receiver.manufacturer, receiver.host
)
- async_add_entities(entities)
+
+ # Register additional services
+ platform = entity_platform.current_platform.get()
+ platform.async_register_entity_service(
+ SERVICE_GET_COMMAND,
+ {vol.Required(ATTR_COMMAND): cv.string},
+ f"async_{SERVICE_GET_COMMAND}",
+ )
+
+ async_add_entities(entities, update_before_add=True)
class DenonDevice(MediaPlayerEntity):
"""Representation of a Denon Media Player Device."""
- def __init__(self, receiver, unique_id, config_entry):
+ def __init__(
+ self,
+ receiver: DenonAVR,
+ unique_id: str,
+ config_entry: config_entries.ConfigEntry,
+ ):
"""Initialize the device."""
self._receiver = receiver
- self._name = self._receiver.name
self._unique_id = unique_id
self._config_entry = config_entry
- self._muted = self._receiver.muted
- self._volume = self._receiver.volume
- self._current_source = self._receiver.input_func
- self._source_list = self._receiver.input_func_list
- self._state = self._receiver.state
- self._power = self._receiver.power
- self._media_image_url = self._receiver.image_url
- self._title = self._receiver.title
- self._artist = self._receiver.artist
- self._album = self._receiver.album
- self._band = self._receiver.band
- self._frequency = self._receiver.frequency
- self._station = self._receiver.station
-
- self._sound_mode_support = self._receiver.support_sound_mode
- if self._sound_mode_support:
- self._sound_mode = self._receiver.sound_mode
- self._sound_mode_raw = self._receiver.sound_mode_raw
- self._sound_mode_list = self._receiver.sound_mode_list
- else:
- self._sound_mode = None
- self._sound_mode_raw = None
- self._sound_mode_list = None
self._supported_features_base = SUPPORT_DENON
self._supported_features_base |= (
- self._sound_mode_support and SUPPORT_SELECT_SOUND_MODE
+ self._receiver.support_sound_mode and SUPPORT_SELECT_SOUND_MODE
)
-
- async def async_added_to_hass(self):
- """Register signal handler."""
- self.async_on_remove(
- async_dispatcher_connect(self.hass, DOMAIN, self.signal_handler)
- )
-
- def signal_handler(self, data):
- """Handle domain-specific signal by calling appropriate method."""
- entity_ids = data[ATTR_ENTITY_ID]
-
- if entity_ids == ENTITY_MATCH_NONE:
- return
-
- if entity_ids == ENTITY_MATCH_ALL or self.entity_id in entity_ids:
- params = {
- key: value
- for key, value in data.items()
- if key not in ["entity_id", "method"]
- }
- getattr(self, data["method"])(**params)
-
- def update(self):
+ self._available = True
+
+ def async_log_errors( # pylint: disable=no-self-argument
+ func: Coroutine,
+ ) -> Coroutine:
+ """
+ Log errors occurred when calling a Denon AVR receiver.
+
+ Decorates methods of DenonDevice class.
+ Declaration of staticmethod for this method is at the end of this class.
+ """
+
+ @wraps(func)
+ async def wrapper(self, *args, **kwargs):
+ # pylint: disable=protected-access
+ available = True
+ try:
+ return await func(self, *args, **kwargs) # pylint: disable=not-callable
+ except AvrTimoutError:
+ available = False
+ if self._available is True:
+ _LOGGER.warning(
+ "Timeout connecting to Denon AVR receiver at host %s. Device is unavailable",
+ self._receiver.host,
+ )
+ self._available = False
+ except AvrNetworkError:
+ available = False
+ if self._available is True:
+ _LOGGER.warning(
+ "Network error connecting to Denon AVR receiver at host %s. Device is unavailable",
+ self._receiver.host,
+ )
+ self._available = False
+ except AvrForbiddenError:
+ available = False
+ if self._available is True:
+ _LOGGER.warning(
+ "Denon AVR receiver at host %s responded with HTTP 403 error. Device is unavailable. Please consider power cycling your receiver",
+ self._receiver.host,
+ )
+ self._available = False
+ except AvrCommandError as err:
+ _LOGGER.error(
+ "Command %s failed with error: %s",
+ func.__name__,
+ err,
+ )
+ except DenonAvrError as err:
+ _LOGGER.error(
+ "Error %s occurred in method %s for Denon AVR receiver",
+ err,
+ func.__name__, # pylint: disable=no-member
+ exc_info=True,
+ )
+ finally:
+ if available is True and self._available is False:
+ _LOGGER.info(
+ "Denon AVR receiver at host %s is available again",
+ self._receiver.host,
+ )
+ self._available = True
+
+ return wrapper
+
+ @async_log_errors
+ async def async_update(self) -> None:
"""Get the latest status information from device."""
- self._receiver.update()
- self._name = self._receiver.name
- self._muted = self._receiver.muted
- self._volume = self._receiver.volume
- self._current_source = self._receiver.input_func
- self._source_list = self._receiver.input_func_list
- self._state = self._receiver.state
- self._power = self._receiver.power
- self._media_image_url = self._receiver.image_url
- self._title = self._receiver.title
- self._artist = self._receiver.artist
- self._album = self._receiver.album
- self._band = self._receiver.band
- self._frequency = self._receiver.frequency
- self._station = self._receiver.station
- if self._sound_mode_support:
- self._sound_mode = self._receiver.sound_mode
- self._sound_mode_raw = self._receiver.sound_mode_raw
+ await self._receiver.async_update()
+
+ @property
+ def available(self):
+ """Return True if entity is available."""
+ return self._available
@property
def unique_id(self):
@@ -176,60 +215,59 @@ def device_info(self):
"manufacturer": self._config_entry.data[CONF_MANUFACTURER],
"name": self._config_entry.title,
"model": f"{self._config_entry.data[CONF_MODEL]}-{self._config_entry.data[CONF_TYPE]}",
+ "serial_number": self._config_entry.data[CONF_SERIAL_NUMBER],
}
- if self._config_entry.data[CONF_MAC] is not None:
- device_info["connections"] = {
- (dr.CONNECTION_NETWORK_MAC, self._config_entry.data[CONF_MAC])
- }
return device_info
@property
def name(self):
"""Return the name of the device."""
- return self._name
+ return self._receiver.name
@property
def state(self):
"""Return the state of the device."""
- return self._state
+ return self._receiver.state
@property
def is_volume_muted(self):
"""Return boolean if volume is currently muted."""
- return self._muted
+ return self._receiver.muted
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
# Volume is sent in a format like -50.0. Minimum is -80.0,
# maximum is 18.0
- return (float(self._volume) + 80) / 100
+ if self._receiver.volume is None:
+ return None
+ return (float(self._receiver.volume) + 80) / 100
@property
def source(self):
"""Return the current input source."""
- return self._current_source
+ return self._receiver.input_func
@property
def source_list(self):
"""Return a list of available input sources."""
- return self._source_list
+ return self._receiver.input_func_list
@property
def sound_mode(self):
"""Return the current matched sound mode."""
- return self._sound_mode
+ return self._receiver.sound_mode
@property
def sound_mode_list(self):
"""Return a list of available sound modes."""
- return self._sound_mode_list
+ return self._receiver.sound_mode_list
@property
def supported_features(self):
"""Flag media player features that are supported."""
- if self._current_source in self._receiver.netaudio_func_list:
+ if self._receiver.input_func in self._receiver.netaudio_func_list:
return self._supported_features_base | SUPPORT_MEDIA_MODES
return self._supported_features_base
@@ -241,7 +279,10 @@ def media_content_id(self):
@property
def media_content_type(self):
"""Content type of current playing media."""
- if self._state == STATE_PLAYING or self._state == STATE_PAUSED:
+ if (
+ self._receiver.state == STATE_PLAYING
+ or self._receiver.state == STATE_PAUSED
+ ):
return MEDIA_TYPE_MUSIC
return MEDIA_TYPE_CHANNEL
@@ -253,32 +294,32 @@ def media_duration(self):
@property
def media_image_url(self):
"""Image url of current playing media."""
- if self._current_source in self._receiver.playing_func_list:
- return self._media_image_url
+ if self._receiver.input_func in self._receiver.playing_func_list:
+ return self._receiver.image_url
return None
@property
def media_title(self):
"""Title of current playing media."""
- if self._current_source not in self._receiver.playing_func_list:
- return self._current_source
- if self._title is not None:
- return self._title
- return self._frequency
+ if self._receiver.input_func not in self._receiver.playing_func_list:
+ return self._receiver.input_func
+ if self._receiver.title is not None:
+ return self._receiver.title
+ return self._receiver.frequency
@property
def media_artist(self):
"""Artist of current playing media, music track only."""
- if self._artist is not None:
- return self._artist
- return self._band
+ if self._receiver.artist is not None:
+ return self._receiver.artist
+ return self._receiver.band
@property
def media_album_name(self):
"""Album name of current playing media, music track only."""
- if self._album is not None:
- return self._album
- return self._station
+ if self._receiver.album is not None:
+ return self._receiver.album
+ return self._receiver.station
@property
def media_album_artist(self):
@@ -306,82 +347,95 @@ def media_episode(self):
return None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
if (
- self._sound_mode_raw is not None
- and self._sound_mode_support
- and self._power == "ON"
+ self._receiver.sound_mode_raw is not None
+ and self._receiver.support_sound_mode
+ and self._receiver.power == POWER_ON
):
- return {ATTR_SOUND_MODE_RAW: self._sound_mode_raw}
+ return {ATTR_SOUND_MODE_RAW: self._receiver.sound_mode_raw}
return {}
- def media_play_pause(self):
+ @async_log_errors
+ async def async_media_play_pause(self):
"""Play or pause the media player."""
- return self._receiver.toggle_play_pause()
+ await self._receiver.async_toggle_play_pause()
- def media_play(self):
+ @async_log_errors
+ async def async_media_play(self):
"""Send play command."""
- return self._receiver.play()
+ await self._receiver.async_play()
- def media_pause(self):
+ @async_log_errors
+ async def async_media_pause(self):
"""Send pause command."""
- return self._receiver.pause()
+ await self._receiver.async_pause()
- def media_previous_track(self):
+ @async_log_errors
+ async def async_media_previous_track(self):
"""Send previous track command."""
- return self._receiver.previous_track()
+ await self._receiver.async_previous_track()
- def media_next_track(self):
+ @async_log_errors
+ async def async_media_next_track(self):
"""Send next track command."""
- return self._receiver.next_track()
+ await self._receiver.async_next_track()
- def select_source(self, source):
+ @async_log_errors
+ async def async_select_source(self, source: str):
"""Select input source."""
# Ensure that the AVR is turned on, which is necessary for input
# switch to work.
- self.turn_on()
- return self._receiver.set_input_func(source)
+ await self.async_turn_on()
+ await self._receiver.async_set_input_func(source)
- def select_sound_mode(self, sound_mode):
+ @async_log_errors
+ async def async_select_sound_mode(self, sound_mode: str):
"""Select sound mode."""
- return self._receiver.set_sound_mode(sound_mode)
+ await self._receiver.async_set_sound_mode(sound_mode)
- def turn_on(self):
+ @async_log_errors
+ async def async_turn_on(self):
"""Turn on media player."""
- if self._receiver.power_on():
- self._state = STATE_ON
+ await self._receiver.async_power_on()
- def turn_off(self):
+ @async_log_errors
+ async def async_turn_off(self):
"""Turn off media player."""
- if self._receiver.power_off():
- self._state = STATE_OFF
+ await self._receiver.async_power_off()
- def volume_up(self):
+ @async_log_errors
+ async def async_volume_up(self):
"""Volume up the media player."""
- return self._receiver.volume_up()
+ await self._receiver.async_volume_up()
- def volume_down(self):
+ @async_log_errors
+ async def async_volume_down(self):
"""Volume down media player."""
- return self._receiver.volume_down()
+ await self._receiver.async_volume_down()
- def set_volume_level(self, volume):
+ @async_log_errors
+ async def async_set_volume_level(self, volume: int):
"""Set volume level, range 0..1."""
# Volume has to be sent in a format like -50.0. Minimum is -80.0,
# maximum is 18.0
volume_denon = float((volume * 100) - 80)
if volume_denon > 18:
volume_denon = float(18)
- try:
- if self._receiver.set_volume(volume_denon):
- self._volume = volume_denon
- except ValueError:
- pass
+ await self._receiver.async_set_volume(volume_denon)
- def mute_volume(self, mute):
+ @async_log_errors
+ async def async_mute_volume(self, mute: bool):
"""Send mute command."""
- return self._receiver.mute(mute)
+ await self._receiver.async_mute(mute)
- def get_command(self, command, **kwargs):
+ @async_log_errors
+ async def async_get_command(self, command: str, **kwargs):
"""Send generic command."""
- self._receiver.send_get_command(command)
+ return await self._receiver.async_get_command(command)
+
+ # Decorator defined before is a staticmethod
+ async_log_errors = staticmethod( # pylint: disable=no-staticmethod-decorator
+ async_log_errors
+ )
diff --git a/homeassistant/components/denonavr/receiver.py b/homeassistant/components/denonavr/receiver.py
index f30469961dfe89..31d91c0a9baabc 100644
--- a/homeassistant/components/denonavr/receiver.py
+++ b/homeassistant/components/denonavr/receiver.py
@@ -1,7 +1,8 @@
"""Code to handle a DenonAVR receiver."""
import logging
+from typing import Callable, Optional
-import denonavr
+from denonavr import DenonAVR
_LOGGER = logging.getLogger(__name__)
@@ -9,13 +10,23 @@
class ConnectDenonAVR:
"""Class to async connect to a DenonAVR receiver."""
- def __init__(self, hass, host, timeout, show_all_inputs, zone2, zone3):
+ def __init__(
+ self,
+ host: str,
+ timeout: float,
+ show_all_inputs: bool,
+ zone2: bool,
+ zone3: bool,
+ async_client_getter: Callable,
+ entry_state: Optional[str] = None,
+ ):
"""Initialize the class."""
- self._hass = hass
+ self._async_client_getter = async_client_getter
self._receiver = None
self._host = host
self._show_all_inputs = show_all_inputs
self._timeout = timeout
+ self._entry_state = entry_state
self._zones = {}
if zone2:
@@ -24,14 +35,13 @@ def __init__(self, hass, host, timeout, show_all_inputs, zone2, zone3):
self._zones["Zone3"] = None
@property
- def receiver(self):
+ def receiver(self) -> Optional[DenonAVR]:
"""Return the class containing all connections to the receiver."""
return self._receiver
- async def async_connect_receiver(self):
+ async def async_connect_receiver(self) -> bool:
"""Connect to the DenonAVR receiver."""
- if not await self._hass.async_add_executor_job(self.init_receiver_class):
- return False
+ await self.async_init_receiver_class()
if (
self._receiver.manufacturer is None
@@ -60,19 +70,16 @@ async def async_connect_receiver(self):
return True
- def init_receiver_class(self):
- """Initialize the DenonAVR class in a way that can called by async_add_executor_job."""
- try:
- self._receiver = denonavr.DenonAVR(
- host=self._host,
- show_all_inputs=self._show_all_inputs,
- timeout=self._timeout,
- add_zones=self._zones,
- )
- except ConnectionError:
- _LOGGER.error(
- "ConnectionError during setup of denonavr with host %s", self._host
- )
- return False
+ async def async_init_receiver_class(self) -> bool:
+ """Initialize the DenonAVR class asynchronously."""
+ receiver = DenonAVR(
+ host=self._host,
+ show_all_inputs=self._show_all_inputs,
+ timeout=self._timeout,
+ add_zones=self._zones,
+ )
+ # Use httpx.AsyncClient getter provided by Home Assistant
+ receiver.set_async_client_getter(self._async_client_getter)
+ await receiver.async_setup()
- return True
+ self._receiver = receiver
diff --git a/homeassistant/components/denonavr/services.yaml b/homeassistant/components/denonavr/services.yaml
index c9831a68aa5294..62157426bb2213 100644
--- a/homeassistant/components/denonavr/services.yaml
+++ b/homeassistant/components/denonavr/services.yaml
@@ -1,7 +1,7 @@
-# Describes the format for available webostv services
+# Describes the format for available denonavr services
get_command:
- description: "Send a generic http get command."
+ description: "Send a generic HTTP get command."
fields:
entity_id:
description: Name(s) of the denonavr entities where to run the API method.
diff --git a/homeassistant/components/denonavr/translations/de.json b/homeassistant/components/denonavr/translations/de.json
index 5af7d3393e243b..6bd9f1613dc5f0 100644
--- a/homeassistant/components/denonavr/translations/de.json
+++ b/homeassistant/components/denonavr/translations/de.json
@@ -1,15 +1,45 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
+ "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuchen Sie es noch einmal. Trennen Sie ggf. Strom- und Ethernetkabel und verbinden Sie diese erneut.",
+ "not_denonavr_manufacturer": "Kein Denon AVR-Netzwerkempf\u00e4nger, entdeckter Hersteller stimmte nicht \u00fcberein",
+ "not_denonavr_missing": "Kein Denon AVR-Netzwerk-Receiver, Erkennungsinformationen nicht vollst\u00e4ndig"
+ },
+ "error": {
+ "discovery_error": "Denon AVR-Netzwerk-Receiver konnte nicht gefunden werden"
+ },
+ "flow_title": "Denon AVR-Netzwerk-Receiver: {name}",
"step": {
+ "confirm": {
+ "description": "Bitte best\u00e4tige das Hinzuf\u00fcgen des Receivers",
+ "title": "Denon AVR-Netzwerk-Receiver"
+ },
"select": {
"data": {
"select_host": "IP-Adresse des Empf\u00e4ngers"
- }
+ },
+ "description": "F\u00fchre das Setup erneut aus, wenn du weitere Receiver verbinden m\u00f6chten",
+ "title": "W\u00e4hle den Receiver, den du verbinden m\u00f6chtest"
},
"user": {
"data": {
"host": "IP-Adresse"
- }
+ },
+ "description": "Verbinde dich mit deinem Receiver, wenn die IP-Adresse nicht eingestellt ist, wird die automatische Erkennung verwendet",
+ "title": "Denon AVR-Netzwerk-Receiver"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "show_all_sources": "Alle Quellen anzeigen"
+ },
+ "description": "Optionale Einstellungen festlegen",
+ "title": "Denon AVR-Netzwerk-Receiver"
}
}
}
diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json
index aa56cb477417ab..41a1910bd56d01 100644
--- a/homeassistant/components/denonavr/translations/hu.json
+++ b/homeassistant/components/denonavr/translations/hu.json
@@ -2,10 +2,18 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
"cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra. A h\u00e1l\u00f3zati \u00e9s Ethernet k\u00e1belek kih\u00faz\u00e1sa \u00e9s \u00fajracsatlakoztat\u00e1sa seg\u00edthet"
},
"error": {
"discovery_error": "Nem siker\u00fclt megtal\u00e1lni a Denon AVR h\u00e1l\u00f3zati er\u0151s\u00edt\u0151t"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "IP c\u00edm"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/denonavr/translations/id.json b/homeassistant/components/denonavr/translations/id.json
new file mode 100644
index 00000000000000..d78f547ef35930
--- /dev/null
+++ b/homeassistant/components/denonavr/translations/id.json
@@ -0,0 +1,48 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "cannot_connect": "Gagal menyambungkan, coba lagi. Pemutusan sambungan daya listrik dan kabel ethernet lalu menyambungkannya kembali mungkin dapat membantu",
+ "not_denonavr_manufacturer": "Bukan Network Receiver Denon AVR, pabrikan yang ditemukan tidak sesuai",
+ "not_denonavr_missing": "Bukan Network Receiver AVR Denon, informasi penemuan tidak lengkap"
+ },
+ "error": {
+ "discovery_error": "Gagal menemukan Network Receiver AVR Denon"
+ },
+ "flow_title": "Network Receiver Denon AVR: {name}",
+ "step": {
+ "confirm": {
+ "description": "Konfirmasikan penambahan Receiver",
+ "title": "Network Receiver Denon AVR"
+ },
+ "select": {
+ "data": {
+ "select_host": "Alamat IP Receiver"
+ },
+ "description": "Jalankan penyiapan lagi jika ingin menghubungkan Receiver lainnya",
+ "title": "Pilih Receiver yang ingin dihubungkan"
+ },
+ "user": {
+ "data": {
+ "host": "Alamat IP"
+ },
+ "description": "Hubungkan ke Receiver Anda. Jika alamat IP tidak ditentukan, penemuan otomatis akan digunakan",
+ "title": "Network Receiver Denon AVR"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "show_all_sources": "Tampilkan semua sumber",
+ "zone2": "Siapkan Zona 2",
+ "zone3": "Siapkan Zona 3"
+ },
+ "description": "Tentukan pengaturan opsional",
+ "title": "Network Receiver Denon AVR"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/denonavr/translations/ko.json b/homeassistant/components/denonavr/translations/ko.json
index f995b852a57fe9..c0121a1e2ca885 100644
--- a/homeassistant/components/denonavr/translations/ko.json
+++ b/homeassistant/components/denonavr/translations/ko.json
@@ -2,7 +2,8 @@
"config": {
"abort": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "already_in_progress": "Denon AVR \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc8fc\uc804\uc6d0 \ubc0f \uc774\ub354\ub137 \ucf00\uc774\ube14\uc744 \ubd84\ub9ac\ud55c \ud6c4 \ub2e4\uc2dc \uc5f0\uacb0\ud558\uba74 \ub3c4\uc6c0\uc774 \ub420 \uc218 \uc788\uc2b5\ub2c8\ub2e4",
"not_denonavr_manufacturer": "Denon AVR \ub124\ud2b8\uc6cc\ud06c \ub9ac\uc2dc\ubc84\uac00 \uc544\ub2d9\ub2c8\ub2e4. \ubc1c\uacac\ub41c \uc81c\uc870\uc0ac\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4",
"not_denonavr_missing": "Denon AVR \ub124\ud2b8\uc6cc\ud06c \ub9ac\uc2dc\ubc84\uac00 \uc544\ub2d9\ub2c8\ub2e4. \uac80\uc0c9 \uc815\ubcf4\uac00 \uc644\uc804\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4"
},
@@ -17,7 +18,7 @@
},
"select": {
"data": {
- "select_host": "\ub9ac\uc2dc\ubc84 IP"
+ "select_host": "\ub9ac\uc2dc\ubc84 IP \uc8fc\uc18c"
},
"description": "\ub9ac\uc2dc\ubc84 \uc5f0\uacb0\uc744 \ucd94\uac00\ud558\ub824\uba74 \uc124\uc815\uc744 \ub2e4\uc2dc \uc2e4\ud589\ud574\uc8fc\uc138\uc694",
"title": "\uc5f0\uacb0\ud560 \ub9ac\uc2dc\ubc84\ub97c \uc120\ud0dd\ud558\uae30"
diff --git a/homeassistant/components/denonavr/translations/nl.json b/homeassistant/components/denonavr/translations/nl.json
index 9f79aebeb600fc..04f067c2f2a9d6 100644
--- a/homeassistant/components/denonavr/translations/nl.json
+++ b/homeassistant/components/denonavr/translations/nl.json
@@ -1,11 +1,19 @@
{
"config": {
"abort": {
- "already_configured": "Apparaat is al geconfigureerd"
+ "already_configured": "Apparaat is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
+ "cannot_connect": "Verbinding mislukt, probeer het opnieuw, de stroom- en ethernetkabels loskoppelen en opnieuw aansluiten kan helpen",
+ "not_denonavr_manufacturer": "Geen Denon AVR Netwerk Receiver, ontdekte fabrikant komt niet overeen",
+ "not_denonavr_missing": "Geen Denon AVR netwerkontvanger, zoekinformatie niet compleet"
+ },
+ "error": {
+ "discovery_error": "Kan een Denon AVR netwerkontvanger niet vinden"
},
"flow_title": "Denon AVR Network Receiver: {name}",
"step": {
"confirm": {
+ "description": "Bevestig het toevoegen van de ontvanger",
"title": "Denon AVR Network Receivers"
},
"select": {
@@ -19,13 +27,20 @@
"data": {
"host": "IP-adres"
},
- "description": "Maak verbinding met uw ontvanger. Als het IP-adres niet is ingesteld, wordt automatische detectie gebruikt"
+ "description": "Maak verbinding met uw ontvanger. Als het IP-adres niet is ingesteld, wordt automatische detectie gebruikt",
+ "title": "Denon AVR Netwerk Ontvangers"
}
}
},
"options": {
"step": {
"init": {
+ "data": {
+ "show_all_sources": "Toon alle bronnen",
+ "zone2": "Stel Zone 2 in",
+ "zone3": "Stel Zone 3 in"
+ },
+ "description": "Optionele instellingen opgeven",
"title": "Denon AVR Network Receivers"
}
}
diff --git a/homeassistant/components/denonavr/translations/tr.json b/homeassistant/components/denonavr/translations/tr.json
new file mode 100644
index 00000000000000..f618d3a3038474
--- /dev/null
+++ b/homeassistant/components/denonavr/translations/tr.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor",
+ "cannot_connect": "Ba\u011flan\u0131lamad\u0131, l\u00fctfen tekrar deneyin, ana g\u00fc\u00e7 ve ethernet kablolar\u0131n\u0131n ba\u011flant\u0131s\u0131n\u0131 kesip yeniden ba\u011flamak yard\u0131mc\u0131 olabilir"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0130p Adresi"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/denonavr/translations/uk.json b/homeassistant/components/denonavr/translations/uk.json
new file mode 100644
index 00000000000000..efb4cb417779fd
--- /dev/null
+++ b/homeassistant/components/denonavr/translations/uk.json
@@ -0,0 +1,48 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "cannot_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437. \u042f\u043a\u0449\u043e \u0446\u0435 \u043d\u0435 \u0441\u043f\u0440\u0430\u0446\u044e\u0432\u0430\u043b\u043e, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043a\u0430\u0431\u0435\u043b\u044c Ethernet \u0456 \u043a\u0430\u0431\u0435\u043b\u044c \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f.",
+ "not_denonavr_manufacturer": "\u0426\u0435 \u043d\u0435 \u0440\u0435\u0441\u0438\u0432\u0435\u0440 Denon. \u0412\u0438\u0440\u043e\u0431\u043d\u0438\u043a \u043d\u0435 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0454.",
+ "not_denonavr_missing": "\u041d\u0435\u043f\u043e\u0432\u043d\u0430 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044f \u0434\u043b\u044f \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e."
+ },
+ "error": {
+ "discovery_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u043d\u0430\u0439\u0442\u0438 \u0440\u0435\u0441\u0438\u0432\u0435\u0440 Denon."
+ },
+ "flow_title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon: {name}",
+ "step": {
+ "confirm": {
+ "description": "\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0456\u0442\u044c \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f \u0440\u0435\u0441\u0438\u0432\u0435\u0440\u0430",
+ "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon"
+ },
+ "select": {
+ "data": {
+ "select_host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430"
+ },
+ "description": "\u041f\u043e\u0447\u043d\u0456\u0442\u044c \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u043d\u043e\u0432\u0443, \u044f\u043a\u0449\u043e \u0432\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u0456\u043d\u0448\u0438\u0439 \u0440\u0435\u0441\u0438\u0432\u0435\u0440",
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0440\u0435\u0441\u0438\u0432\u0435\u0440, \u044f\u043a\u0438\u0439 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438"
+ },
+ "user": {
+ "data": {
+ "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430"
+ },
+ "description": "\u042f\u043a\u0449\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u0432\u043a\u0430\u0437\u0430\u043d\u0430, \u0431\u0443\u0434\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f",
+ "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "show_all_sources": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u0432\u0441\u0456 \u0434\u0436\u0435\u0440\u0435\u043b\u0430",
+ "zone2": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u043e\u043d\u0438 2",
+ "zone3": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u043e\u043d\u0438 3"
+ },
+ "description": "\u0414\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f",
+ "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py
index 7c2cc444a5a25f..12fc4ddd7ba2b4 100644
--- a/homeassistant/components/derivative/sensor.py
+++ b/homeassistant/components/derivative/sensor.py
@@ -4,7 +4,7 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
CONF_NAME,
@@ -86,7 +86,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([derivative])
-class DerivativeSensor(RestoreEntity):
+class DerivativeSensor(RestoreEntity, SensorEntity):
"""Representation of an derivative sensor."""
def __init__(
@@ -211,7 +211,7 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {ATTR_SOURCE_ID: self._sensor_source_id}
diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py
index 01752f0373f1de..33fd9a8224fab0 100644
--- a/homeassistant/components/deutsche_bahn/sensor.py
+++ b/homeassistant/components/deutsche_bahn/sensor.py
@@ -4,14 +4,13 @@
import schiene
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.const import CONF_OFFSET
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
CONF_DESTINATION = "to"
CONF_START = "from"
-CONF_OFFSET = "offset"
DEFAULT_OFFSET = timedelta(minutes=0)
CONF_ONLY_DIRECT = "only_direct"
DEFAULT_ONLY_DIRECT = False
@@ -40,7 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([DeutscheBahnSensor(start, destination, offset, only_direct)], True)
-class DeutscheBahnSensor(Entity):
+class DeutscheBahnSensor(SensorEntity):
"""Implementation of a Deutsche Bahn sensor."""
def __init__(self, start, goal, offset, only_direct):
@@ -65,7 +64,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
connections = self.data.connections[0]
if len(self.data.connections) > 1:
@@ -87,7 +86,6 @@ class SchieneData:
def __init__(self, start, goal, offset, only_direct):
"""Initialize the sensor."""
-
self.start = start
self.goal = goal
self.offset = offset
diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py
index bba02147c71173..4741dbdb7f5572 100644
--- a/homeassistant/components/device_automation/__init__.py
+++ b/homeassistant/components/device_automation/__init__.py
@@ -1,8 +1,10 @@
"""Helpers for device automations."""
+from __future__ import annotations
+
import asyncio
from functools import wraps
from types import ModuleType
-from typing import Any, List, MutableMapping
+from typing import Any, MutableMapping
import voluptuous as vol
import voluptuous_serialize
@@ -116,7 +118,7 @@ async def _async_get_device_automations(hass, automation_type, device_id):
)
domains = set()
- automations: List[MutableMapping[str, Any]] = []
+ automations: list[MutableMapping[str, Any]] = []
device = device_registry.async_get(device_id)
if device is None:
diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py
index 61c50da68681ce..72fcc9790b2d70 100644
--- a/homeassistant/components/device_automation/toggle_entity.py
+++ b/homeassistant/components/device_automation/toggle_entity.py
@@ -1,5 +1,7 @@
"""Device automation helpers for toggle entity."""
-from typing import Any, Dict, List
+from __future__ import annotations
+
+from typing import Any
import voluptuous as vol
@@ -149,15 +151,12 @@ async def async_attach_trigger(
"""Listen for state changes based on configuration."""
trigger_type = config[CONF_TYPE]
if trigger_type == CONF_TURNED_ON:
- from_state = "off"
to_state = "on"
else:
- from_state = "on"
to_state = "off"
state_config = {
CONF_PLATFORM: "state",
state_trigger.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
- state_trigger.CONF_FROM: from_state,
state_trigger.CONF_TO: to_state,
}
if CONF_FOR in config:
@@ -170,10 +169,10 @@ async def async_attach_trigger(
async def _async_get_automations(
- hass: HomeAssistant, device_id: str, automation_templates: List[dict], domain: str
-) -> List[dict]:
+ hass: HomeAssistant, device_id: str, automation_templates: list[dict], domain: str
+) -> list[dict]:
"""List device automations."""
- automations: List[Dict[str, Any]] = []
+ automations: list[dict[str, Any]] = []
entity_registry = await hass.helpers.entity_registry.async_get_registry()
entries = [
@@ -198,21 +197,21 @@ async def _async_get_automations(
async def async_get_actions(
hass: HomeAssistant, device_id: str, domain: str
-) -> List[dict]:
+) -> list[dict]:
"""List device actions."""
return await _async_get_automations(hass, device_id, ENTITY_ACTIONS, domain)
async def async_get_conditions(
hass: HomeAssistant, device_id: str, domain: str
-) -> List[Dict[str, str]]:
+) -> list[dict[str, str]]:
"""List device conditions."""
return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS, domain)
async def async_get_triggers(
hass: HomeAssistant, device_id: str, domain: str
-) -> List[dict]:
+) -> list[dict]:
"""List device triggers."""
return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain)
diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py
index d785ee826e86b3..dfdfd678c0f4a9 100644
--- a/homeassistant/components/device_tracker/__init__.py
+++ b/homeassistant/components/device_tracker/__init__.py
@@ -1,16 +1,11 @@
"""Provide functionality to keep track of devices."""
-from homeassistant.const import ( # noqa: F401 pylint: disable=unused-import
- ATTR_GPS_ACCURACY,
- STATE_HOME,
-)
-from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME # noqa: F401
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
-from .config_entry import ( # noqa: F401 pylint: disable=unused-import
- async_setup_entry,
- async_unload_entry,
-)
-from .const import ( # noqa: F401 pylint: disable=unused-import
+from .config_entry import async_setup_entry, async_unload_entry # noqa: F401
+from .const import ( # noqa: F401
ATTR_ATTRIBUTES,
ATTR_BATTERY,
ATTR_DEV_ID,
@@ -29,7 +24,7 @@
SOURCE_TYPE_GPS,
SOURCE_TYPE_ROUTER,
)
-from .legacy import ( # noqa: F401 pylint: disable=unused-import
+from .legacy import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
SERVICE_SEE,
@@ -42,12 +37,12 @@
@bind_hass
-def is_on(hass: HomeAssistantType, entity_id: str):
+def is_on(hass: HomeAssistant, entity_id: str):
"""Return the state if any or a specified device is home."""
return hass.states.is_state(entity_id, STATE_HOME)
-async def async_setup(hass: HomeAssistantType, config: ConfigType):
+async def async_setup(hass: HomeAssistant, config: ConfigType):
"""Set up the device tracker."""
await async_setup_legacy_integration(hass, config)
diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py
index 16e7d022c92c54..05fa4b4a60d7fa 100644
--- a/homeassistant/components/device_tracker/config_entry.py
+++ b/homeassistant/components/device_tracker/config_entry.py
@@ -1,5 +1,7 @@
"""Code to set up a device tracker platform using a config entry."""
-from typing import Optional
+from __future__ import annotations
+
+from typing import final
from homeassistant.components import zone
from homeassistant.const import (
@@ -18,7 +20,7 @@
async def async_setup_entry(hass, entry):
"""Set up an entry."""
- component: Optional[EntityComponent] = hass.data.get(DOMAIN)
+ component: EntityComponent | None = hass.data.get(DOMAIN)
if component is None:
component = hass.data[DOMAIN] = EntityComponent(LOGGER, DOMAIN, hass)
@@ -59,7 +61,7 @@ def state_attributes(self):
class TrackerEntity(BaseTrackerEntity):
- """Represent a tracked device."""
+ """Base class for a tracked device."""
@property
def should_poll(self):
@@ -114,6 +116,7 @@ def state(self):
return None
+ @final
@property
def state_attributes(self):
"""Return the device state attributes."""
@@ -128,7 +131,7 @@ def state_attributes(self):
class ScannerEntity(BaseTrackerEntity):
- """Represent a tracked device that is on a scanned network."""
+ """Base class for a tracked device that is on a scanned network."""
@property
def ip_address(self) -> str:
@@ -157,6 +160,7 @@ def is_connected(self):
"""Return true if the device is connected to the network."""
raise NotImplementedError
+ @final
@property
def state_attributes(self):
"""Return the device state attributes."""
diff --git a/homeassistant/components/device_tracker/device_condition.py b/homeassistant/components/device_tracker/device_condition.py
index 9c102bfa745e9b..0260a4bbd3abf0 100644
--- a/homeassistant/components/device_tracker/device_condition.py
+++ b/homeassistant/components/device_tracker/device_condition.py
@@ -1,5 +1,5 @@
"""Provides device automations for Device tracker."""
-from typing import Dict, List
+from __future__ import annotations
import voluptuous as vol
@@ -32,7 +32,7 @@
async def async_get_conditions(
hass: HomeAssistant, device_id: str
-) -> List[Dict[str, str]]:
+) -> list[dict[str, str]]:
"""List device conditions for Device tracker devices."""
registry = await entity_registry.async_get_registry(hass)
conditions = []
diff --git a/homeassistant/components/device_tracker/device_trigger.py b/homeassistant/components/device_tracker/device_trigger.py
index 2c92304a246827..81a16545c74e8e 100644
--- a/homeassistant/components/device_tracker/device_trigger.py
+++ b/homeassistant/components/device_tracker/device_trigger.py
@@ -1,5 +1,5 @@
"""Provides device automations for Device Tracker."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -32,7 +32,7 @@
)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers for Device Tracker devices."""
registry = await entity_registry.async_get_registry(hass)
triggers = []
@@ -71,8 +71,6 @@ async def async_attach_trigger(
automation_info: dict,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
- config = TRIGGER_SCHEMA(config)
-
if config[CONF_TYPE] == "enters":
event = zone.EVENT_ENTER
else:
diff --git a/homeassistant/components/device_tracker/group.py b/homeassistant/components/device_tracker/group.py
index 07ec2cfe985ed7..9bd2c991678c34 100644
--- a/homeassistant/components/device_tracker/group.py
+++ b/homeassistant/components/device_tracker/group.py
@@ -3,13 +3,12 @@
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
-from homeassistant.core import callback
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import HomeAssistant, callback
@callback
def async_describe_on_off_states(
- hass: HomeAssistantType, registry: GroupIntegrationRegistry
+ hass: HomeAssistant, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_HOME}, STATE_NOT_HOME)
diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py
index 5f60d84f406b7b..a90d92944a4d82 100644
--- a/homeassistant/components/device_tracker/legacy.py
+++ b/homeassistant/components/device_tracker/legacy.py
@@ -1,9 +1,11 @@
"""Legacy device tracker classes."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
import hashlib
from types import ModuleType
-from typing import Any, Callable, Dict, List, Optional, Sequence
+from typing import Any, Callable, Sequence, final
import attr
import voluptuous as vol
@@ -25,7 +27,7 @@
STATE_HOME,
STATE_NOT_HOME,
)
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform, discovery
import homeassistant.helpers.config_validation as cv
@@ -35,8 +37,8 @@
async_track_utc_time_change,
)
from homeassistant.helpers.restore_state import RestoreEntity
-from homeassistant.helpers.typing import ConfigType, GPSType, HomeAssistantType
-from homeassistant.setup import async_prepare_setup_platform
+from homeassistant.helpers.typing import ConfigType, GPSType
+from homeassistant.setup import async_prepare_setup_platform, async_start_setup
from homeassistant.util import dt as dt_util
from homeassistant.util.yaml import dump
@@ -117,7 +119,7 @@
def see(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
mac: str = None,
dev_id: str = None,
host_name: str = None,
@@ -146,14 +148,14 @@ def see(
hass.services.call(DOMAIN, SERVICE_SEE, data)
-async def async_setup_integration(hass: HomeAssistantType, config: ConfigType) -> None:
+async def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> None:
"""Set up the legacy integration."""
tracker = await get_tracker(hass, config)
legacy_platforms = await async_extract_config(hass, config)
setup_tasks = [
- legacy_platform.async_setup_legacy(hass, tracker)
+ asyncio.create_task(legacy_platform.async_setup_legacy(hass, tracker))
for legacy_platform in legacy_platforms
]
@@ -205,7 +207,7 @@ class DeviceTrackerPlatform:
name: str = attr.ib()
platform: ModuleType = attr.ib()
- config: Dict = attr.ib()
+ config: dict = attr.ib()
@property
def type(self):
@@ -219,45 +221,54 @@ def type(self):
async def async_setup_legacy(self, hass, tracker, discovery_info=None):
"""Set up a legacy platform."""
- LOGGER.info("Setting up %s.%s", DOMAIN, self.type)
- try:
- scanner = None
- setup = None
- if hasattr(self.platform, "async_get_scanner"):
- scanner = await self.platform.async_get_scanner(
- hass, {DOMAIN: self.config}
- )
- elif hasattr(self.platform, "get_scanner"):
- scanner = await hass.async_add_executor_job(
- self.platform.get_scanner, hass, {DOMAIN: self.config}
- )
- elif hasattr(self.platform, "async_setup_scanner"):
- setup = await self.platform.async_setup_scanner(
- hass, self.config, tracker.async_see, discovery_info
- )
- elif hasattr(self.platform, "setup_scanner"):
- setup = await hass.async_add_executor_job(
- self.platform.setup_scanner,
- hass,
- self.config,
- tracker.see,
- discovery_info,
- )
- else:
- raise HomeAssistantError("Invalid legacy device_tracker platform.")
-
- if scanner:
- async_setup_scanner_platform(
- hass, self.config, scanner, tracker.async_see, self.type
+ full_name = f"{DOMAIN}.{self.name}"
+ LOGGER.info("Setting up %s", full_name)
+ with async_start_setup(hass, [full_name]):
+ try:
+ scanner = None
+ setup = None
+ if hasattr(self.platform, "async_get_scanner"):
+ scanner = await self.platform.async_get_scanner(
+ hass, {DOMAIN: self.config}
+ )
+ elif hasattr(self.platform, "get_scanner"):
+ scanner = await hass.async_add_executor_job(
+ self.platform.get_scanner, hass, {DOMAIN: self.config}
+ )
+ elif hasattr(self.platform, "async_setup_scanner"):
+ setup = await self.platform.async_setup_scanner(
+ hass, self.config, tracker.async_see, discovery_info
+ )
+ elif hasattr(self.platform, "setup_scanner"):
+ setup = await hass.async_add_executor_job(
+ self.platform.setup_scanner,
+ hass,
+ self.config,
+ tracker.see,
+ discovery_info,
+ )
+ else:
+ raise HomeAssistantError("Invalid legacy device_tracker platform.")
+
+ if setup:
+ hass.config.components.add(full_name)
+
+ if scanner:
+ async_setup_scanner_platform(
+ hass, self.config, scanner, tracker.async_see, self.type
+ )
+ return
+
+ if not setup:
+ LOGGER.error(
+ "Error setting up platform %s %s", self.type, self.name
+ )
+ return
+
+ except Exception: # pylint: disable=broad-except
+ LOGGER.exception(
+ "Error setting up platform %s %s", self.type, self.name
)
- return
-
- if not setup:
- LOGGER.error("Error setting up platform %s", self.type)
- return
-
- except Exception: # pylint: disable=broad-except
- LOGGER.exception("Error setting up platform %s", self.type)
async def async_extract_config(hass, config):
@@ -285,7 +296,7 @@ async def async_extract_config(hass, config):
async def async_create_platform_type(
hass, config, p_type, p_config
-) -> Optional[DeviceTrackerPlatform]:
+) -> DeviceTrackerPlatform | None:
"""Determine type of platform."""
platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type)
@@ -297,7 +308,7 @@ async def async_create_platform_type(
@callback
def async_setup_scanner_platform(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
config: ConfigType,
scanner: Any,
async_see_device: Callable,
@@ -387,7 +398,7 @@ class DeviceTracker:
def __init__(
self,
- hass: HomeAssistantType,
+ hass: HomeAssistant,
consider_home: timedelta,
track_new: bool,
defaults: dict,
@@ -586,7 +597,7 @@ async def async_init_single_device(dev):
class Device(RestoreEntity):
- """Represent a tracked device."""
+ """Base class for a tracked device."""
host_name: str = None
location_name: str = None
@@ -604,7 +615,7 @@ class Device(RestoreEntity):
def __init__(
self,
- hass: HomeAssistantType,
+ hass: HomeAssistant,
consider_home: timedelta,
track: bool,
dev_id: str,
@@ -659,6 +670,7 @@ def entity_picture(self):
"""Return the picture of the device."""
return self.config_picture
+ @final
@property
def state_attributes(self):
"""Return the device state attributes."""
@@ -675,7 +687,7 @@ def state_attributes(self):
return attributes
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device state attributes."""
return self._attributes
@@ -784,9 +796,9 @@ async def async_added_to_hass(self):
class DeviceScanner:
"""Device scanner object."""
- hass: HomeAssistantType = None
+ hass: HomeAssistant = None
- def scan_devices(self) -> List[str]:
+ def scan_devices(self) -> list[str]:
"""Scan for devices."""
raise NotImplementedError()
@@ -811,9 +823,7 @@ async def async_get_extra_attributes(self, device: str) -> Any:
return await self.hass.async_add_executor_job(self.get_extra_attributes, device)
-async def async_load_config(
- path: str, hass: HomeAssistantType, consider_home: timedelta
-):
+async def async_load_config(path: str, hass: HomeAssistant, consider_home: timedelta):
"""Load devices from YAML configuration file.
This method is a coroutine.
diff --git a/homeassistant/components/device_tracker/translations/de.json b/homeassistant/components/device_tracker/translations/de.json
index 651805dcb14b86..fe59183e67a808 100644
--- a/homeassistant/components/device_tracker/translations/de.json
+++ b/homeassistant/components/device_tracker/translations/de.json
@@ -1,8 +1,12 @@
{
"device_automation": {
"condition_type": {
- "is_home": "{entity_name} ist Zuhause",
- "is_not_home": "{entity_name} ist nicht zu Hause"
+ "is_home": "{entity_name} ist zuhause",
+ "is_not_home": "{entity_name} ist nicht zuhause"
+ },
+ "trigger_type": {
+ "enters": "{entity_name} betritt einen Bereich",
+ "leaves": "{entity_name} verl\u00e4sst einen Bereich"
}
},
"state": {
diff --git a/homeassistant/components/device_tracker/translations/id.json b/homeassistant/components/device_tracker/translations/id.json
index 99baa5e1a76c54..be5c7e932cea2e 100644
--- a/homeassistant/components/device_tracker/translations/id.json
+++ b/homeassistant/components/device_tracker/translations/id.json
@@ -1,7 +1,17 @@
{
+ "device_automation": {
+ "condition_type": {
+ "is_home": "{entity_name} ada di rumah",
+ "is_not_home": "{entity_name} tidak ada di rumah"
+ },
+ "trigger_type": {
+ "enters": "{entity_name} memasuki zona",
+ "leaves": "{entity_name} meninggalkan zona"
+ }
+ },
"state": {
"_": {
- "home": "Rumah",
+ "home": "Di Rumah",
"not_home": "Keluar"
}
},
diff --git a/homeassistant/components/device_tracker/translations/it.json b/homeassistant/components/device_tracker/translations/it.json
index 92ccce1c1c56f2..646f0732cd8264 100644
--- a/homeassistant/components/device_tracker/translations/it.json
+++ b/homeassistant/components/device_tracker/translations/it.json
@@ -15,5 +15,5 @@
"not_home": "Fuori casa"
}
},
- "title": "Tracciatore dispositivo"
+ "title": "Localizzatore di dispositivi"
}
\ No newline at end of file
diff --git a/homeassistant/components/device_tracker/translations/ko.json b/homeassistant/components/device_tracker/translations/ko.json
index e3e72d49c89e01..538db691b4ff21 100644
--- a/homeassistant/components/device_tracker/translations/ko.json
+++ b/homeassistant/components/device_tracker/translations/ko.json
@@ -1,8 +1,12 @@
{
"device_automation": {
"condition_type": {
- "is_home": "{entity_name} \uc774(\uac00) \uc9d1\uc5d0 \uc788\uc73c\uba74",
- "is_not_home": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uc911\uc774\uba74"
+ "is_home": "{entity_name}\uc774(\uac00) \uc9d1\uc5d0 \uc788\uc73c\uba74",
+ "is_not_home": "{entity_name}\uc774(\uac00) \uc678\ucd9c \uc911\uc774\uba74"
+ },
+ "trigger_type": {
+ "enters": "{entity_name}\uc774(\uac00) \uc9c0\uc5ed\uc5d0 \ub4e4\uc5b4\uac08 \ub54c",
+ "leaves": "{entity_name}\uc774(\uac00) \uc9c0\uc5ed\uc5d0\uc11c \ub098\uc62c \ub54c"
}
},
"state": {
diff --git a/homeassistant/components/device_tracker/translations/nl.json b/homeassistant/components/device_tracker/translations/nl.json
index a28c8bdbbb8773..a3f7d6cd31f12c 100644
--- a/homeassistant/components/device_tracker/translations/nl.json
+++ b/homeassistant/components/device_tracker/translations/nl.json
@@ -15,5 +15,5 @@
"not_home": "Afwezig"
}
},
- "title": "Apparaat tracker"
+ "title": "Apparaattracker"
}
\ No newline at end of file
diff --git a/homeassistant/components/device_tracker/translations/uk.json b/homeassistant/components/device_tracker/translations/uk.json
index f49c7acc0e3933..87945d2a19a8dd 100644
--- a/homeassistant/components/device_tracker/translations/uk.json
+++ b/homeassistant/components/device_tracker/translations/uk.json
@@ -1,8 +1,18 @@
{
+ "device_automation": {
+ "condition_type": {
+ "is_home": "{entity_name} \u0432\u0434\u043e\u043c\u0430",
+ "is_not_home": "{entity_name} \u043d\u0435 \u0432\u0434\u043e\u043c\u0430"
+ },
+ "trigger_type": {
+ "enters": "{entity_name} \u0432\u0445\u043e\u0434\u0438\u0442\u044c \u0432 \u0437\u043e\u043d\u0443",
+ "leaves": "{entity_name} \u043f\u043e\u043a\u0438\u0434\u0430\u0454 \u0437\u043e\u043d\u0443"
+ }
+ },
"state": {
"_": {
"home": "\u0412\u0434\u043e\u043c\u0430",
- "not_home": "\u0412\u0456\u0434\u0441\u0443\u0442\u043d\u0456\u0439"
+ "not_home": "\u041d\u0435 \u0432\u0434\u043e\u043c\u0430"
}
},
"title": "\u0422\u0440\u0435\u043a\u0435\u0440 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
diff --git a/homeassistant/components/device_tracker/translations/zh-Hans.json b/homeassistant/components/device_tracker/translations/zh-Hans.json
index c019a3dcda854d..5d56de9b855633 100644
--- a/homeassistant/components/device_tracker/translations/zh-Hans.json
+++ b/homeassistant/components/device_tracker/translations/zh-Hans.json
@@ -5,8 +5,8 @@
"is_not_home": "{entity_name} \u4e0d\u5728\u5bb6"
},
"trigger_type": {
- "enters": "{entity_name} \u8fdb\u5165\u533a\u57df",
- "leaves": "{entity_name} \u79bb\u5f00\u533a\u57df"
+ "enters": "{entity_name} \u8fdb\u5165\u6307\u5b9a\u533a\u57df",
+ "leaves": "{entity_name} \u79bb\u5f00\u6307\u5b9a\u533a\u57df"
}
},
"state": {
diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py
index e5ee9029302b8e..2fb31c6291c186 100644
--- a/homeassistant/components/devolo_home_control/__init__.py
+++ b/homeassistant/components/devolo_home_control/__init__.py
@@ -15,11 +15,6 @@
from .const import CONF_MYDEVOLO, DOMAIN, GATEWAY_SERIAL_PATTERN, PLATFORMS
-async def async_setup(hass, config):
- """Get all devices and add them to hass."""
- return True
-
-
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
"""Set up the devolo account from a config entry."""
hass.data.setdefault(DOMAIN, {})
@@ -54,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
)
)
)
- except (ConnectionError, GatewayOfflineError) as err:
+ except GatewayOfflineError as err:
raise ConfigEntryNotReady from err
for platform in PLATFORMS:
diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py
index d333a5c7609350..7ad375bf44de41 100644
--- a/homeassistant/components/devolo_home_control/climate.py
+++ b/homeassistant/components/devolo_home_control/climate.py
@@ -1,5 +1,5 @@
"""Platform for climate integration."""
-from typing import List, Optional
+from __future__ import annotations
from homeassistant.components.climate import (
ATTR_TEMPERATURE,
@@ -45,7 +45,7 @@ class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntit
"""Representation of a climate/thermostat device within devolo Home Control."""
@property
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
if hasattr(self._device_instance, "multi_level_sensor_property"):
return next(
@@ -60,7 +60,7 @@ def current_temperature(self) -> Optional[float]:
return None
@property
- def target_temperature(self) -> Optional[float]:
+ def target_temperature(self) -> float | None:
"""Return the target temperature."""
return self._value
@@ -75,7 +75,7 @@ def hvac_mode(self) -> str:
return HVAC_MODE_HEAT
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes."""
return [HVAC_MODE_HEAT]
diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py
index 3f51a9c0884307..d6dbd331d5fec9 100644
--- a/homeassistant/components/devolo_home_control/config_flow.py
+++ b/homeassistant/components/devolo_home_control/config_flow.py
@@ -8,11 +8,7 @@
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
-from .const import ( # pylint:disable=unused-import
- CONF_MYDEVOLO,
- DEFAULT_MYDEVOLO,
- DOMAIN,
-)
+from .const import CONF_MYDEVOLO, DEFAULT_MYDEVOLO, DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py
index d0be9543bf4189..6aef842ffff185 100644
--- a/homeassistant/components/devolo_home_control/devolo_device.py
+++ b/homeassistant/components/devolo_home_control/devolo_device.py
@@ -18,6 +18,7 @@ def __init__(self, homecontrol, device_instance, element_uid):
self._unique_id = element_uid
self._homecontrol = homecontrol
self._name = device_instance.settings_property["general_device_settings"].name
+ self._area = device_instance.settings_property["general_device_settings"].zone
self._device_class = None
self._value = None
self._unit = None
@@ -59,6 +60,7 @@ def device_info(self):
"name": self._name,
"manufacturer": self._brand,
"model": self._model,
+ "suggested_area": self._area,
}
@property
diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json
index 0ffa991493cca4..93cf4be5d350db 100644
--- a/homeassistant/components/devolo_home_control/manifest.json
+++ b/homeassistant/components/devolo_home_control/manifest.json
@@ -2,7 +2,7 @@
"domain": "devolo_home_control",
"name": "devolo Home Control",
"documentation": "https://www.home-assistant.io/integrations/devolo_home_control",
- "requirements": ["devolo-home-control-api==0.16.0"],
+ "requirements": ["devolo-home-control-api==0.17.1"],
"after_dependencies": ["zeroconf"],
"config_flow": true,
"codeowners": ["@2Fake", "@Shutgun"],
diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py
index e78b4eabeacc78..e3c16670dfd540 100644
--- a/homeassistant/components/devolo_home_control/sensor.py
+++ b/homeassistant/components/devolo_home_control/sensor.py
@@ -5,6 +5,8 @@
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
+ DEVICE_CLASS_VOLTAGE,
+ SensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE
@@ -20,6 +22,7 @@
"humidity": DEVICE_CLASS_HUMIDITY,
"current": DEVICE_CLASS_POWER,
"total": DEVICE_CLASS_POWER,
+ "voltage": DEVICE_CLASS_VOLTAGE,
}
@@ -63,7 +66,7 @@ async def async_setup_entry(
async_add_entities(entities, False)
-class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity):
+class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity, SensorEntity):
"""Abstract representation of a multi level sensor within devolo Home Control."""
@property
diff --git a/homeassistant/components/devolo_home_control/translations/de.json b/homeassistant/components/devolo_home_control/translations/de.json
index 112daf582b3629..6cf7ed3c82115b 100644
--- a/homeassistant/components/devolo_home_control/translations/de.json
+++ b/homeassistant/components/devolo_home_control/translations/de.json
@@ -1,7 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Diese Home Control Zentral wird bereits verwendet."
+ "already_configured": "Konto wurde bereits konfiguriert"
+ },
+ "error": {
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
},
"step": {
"user": {
@@ -9,7 +12,7 @@
"home_control_url": "Home Control URL",
"mydevolo_url": "mydevolo URL",
"password": "Passwort",
- "username": "E-Mail-Adresse / devolo ID"
+ "username": "E-Mail / devolo ID"
}
}
}
diff --git a/homeassistant/components/devolo_home_control/translations/he.json b/homeassistant/components/devolo_home_control/translations/he.json
index 3007c0e968c1dc..ac90b3264eab33 100644
--- a/homeassistant/components/devolo_home_control/translations/he.json
+++ b/homeassistant/components/devolo_home_control/translations/he.json
@@ -3,7 +3,8 @@
"step": {
"user": {
"data": {
- "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
}
}
}
diff --git a/homeassistant/components/devolo_home_control/translations/hu.json b/homeassistant/components/devolo_home_control/translations/hu.json
index ff2c2fc87b5159..45b07f0adcb502 100644
--- a/homeassistant/components/devolo_home_control/translations/hu.json
+++ b/homeassistant/components/devolo_home_control/translations/hu.json
@@ -1,9 +1,18 @@
{
"config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
+ },
"step": {
"user": {
"data": {
- "password": "Jelsz\u00f3"
+ "home_control_url": "Home Control URL",
+ "mydevolo_url": "mydevolo URL",
+ "password": "Jelsz\u00f3",
+ "username": "E-mail / devolo ID"
}
}
}
diff --git a/homeassistant/components/devolo_home_control/translations/id.json b/homeassistant/components/devolo_home_control/translations/id.json
new file mode 100644
index 00000000000000..8b7ce0171d557b
--- /dev/null
+++ b/homeassistant/components/devolo_home_control/translations/id.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi"
+ },
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "home_control_url": "URL Home Control",
+ "mydevolo_url": "URL mydevolo",
+ "password": "Kata Sandi",
+ "username": "Email/ID devolo"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/devolo_home_control/translations/ko.json b/homeassistant/components/devolo_home_control/translations/ko.json
index 17d4fe28a569f0..f21122bff70d82 100644
--- a/homeassistant/components/devolo_home_control/translations/ko.json
+++ b/homeassistant/components/devolo_home_control/translations/ko.json
@@ -1,15 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 Home Control Central \uc720\ub2db\uc740 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4."
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
"data": {
- "home_control_url": "Home Control URL",
- "mydevolo_url": "mydevolo URL",
+ "home_control_url": "Home Control URL \uc8fc\uc18c",
+ "mydevolo_url": "mydevolo URL \uc8fc\uc18c",
"password": "\ube44\ubc00\ubc88\ud638",
- "username": "\uc774\uba54\uc77c \uc8fc\uc18c / devolo ID"
+ "username": "\uc774\uba54\uc77c / devolo ID"
}
}
}
diff --git a/homeassistant/components/devolo_home_control/translations/nl.json b/homeassistant/components/devolo_home_control/translations/nl.json
index d61f9183cc5d30..5d79d2ec9e9c31 100644
--- a/homeassistant/components/devolo_home_control/translations/nl.json
+++ b/homeassistant/components/devolo_home_control/translations/nl.json
@@ -3,9 +3,14 @@
"abort": {
"already_configured": "Account is al geconfigureerd"
},
+ "error": {
+ "invalid_auth": "Ongeldige authenticatie"
+ },
"step": {
"user": {
"data": {
+ "home_control_url": "Home Control URL",
+ "mydevolo_url": "mydevolo URL",
"password": "Wachtwoord",
"username": "E-mail adres / devolo ID"
}
diff --git a/homeassistant/components/devolo_home_control/translations/ru.json b/homeassistant/components/devolo_home_control/translations/ru.json
index d4cf639ffd5882..b2e82f1355b750 100644
--- a/homeassistant/components/devolo_home_control/translations/ru.json
+++ b/homeassistant/components/devolo_home_control/translations/ru.json
@@ -4,7 +4,7 @@
"already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant."
},
"error": {
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"step": {
"user": {
diff --git a/homeassistant/components/devolo_home_control/translations/tr.json b/homeassistant/components/devolo_home_control/translations/tr.json
new file mode 100644
index 00000000000000..4c6b158f6946b9
--- /dev/null
+++ b/homeassistant/components/devolo_home_control/translations/tr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "E-posta / devolo ID"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/devolo_home_control/translations/uk.json b/homeassistant/components/devolo_home_control/translations/uk.json
new file mode 100644
index 00000000000000..d230d1918f5e09
--- /dev/null
+++ b/homeassistant/components/devolo_home_control/translations/uk.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "home_control_url": "Home Control URL-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "mydevolo_url": "mydevolo URL-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438 / devolo ID"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py
index c2eb9bd466d917..1630d4b9dfdb5b 100644
--- a/homeassistant/components/dexcom/__init__.py
+++ b/homeassistant/components/dexcom/__init__.py
@@ -26,12 +26,6 @@
SCAN_INTERVAL = timedelta(seconds=180)
-async def async_setup(hass: HomeAssistant, config: dict):
- """Set up configured Dexcom."""
- hass.data[DOMAIN] = {}
- return True
-
-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Dexcom from a config entry."""
try:
@@ -57,6 +51,7 @@ async def async_update_data():
except SessionError as error:
raise UpdateFailed(error) from error
+ hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
COORDINATOR: DataUpdateCoordinator(
hass,
@@ -68,11 +63,13 @@ async def async_update_data():
UNDO_UPDATE_LISTENER: entry.add_update_listener(update_listener),
}
- await hass.data[DOMAIN][entry.entry_id][COORDINATOR].async_refresh()
+ await hass.data[DOMAIN][entry.entry_id][
+ COORDINATOR
+ ].async_config_entry_first_refresh()
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -83,8 +80,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/dexcom/config_flow.py b/homeassistant/components/dexcom/config_flow.py
index 1d6d52fa0c9bc7..0f3aad6f813afd 100644
--- a/homeassistant/components/dexcom/config_flow.py
+++ b/homeassistant/components/dexcom/config_flow.py
@@ -6,14 +6,7 @@
from homeassistant.const import CONF_PASSWORD, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME
from homeassistant.core import callback
-from .const import ( # pylint:disable=unused-import
- CONF_SERVER,
- DOMAIN,
- MG_DL,
- MMOL_L,
- SERVER_OUS,
- SERVER_US,
-)
+from .const import CONF_SERVER, DOMAIN, MG_DL, MMOL_L, SERVER_OUS, SERVER_US
DATA_SCHEMA = vol.Schema(
{
diff --git a/homeassistant/components/dexcom/sensor.py b/homeassistant/components/dexcom/sensor.py
index afb3feeec4d050..730a1824e1aad5 100644
--- a/homeassistant/components/dexcom/sensor.py
+++ b/homeassistant/components/dexcom/sensor.py
@@ -1,4 +1,5 @@
"""Support for Dexcom sensors."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -16,7 +17,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors, False)
-class DexcomGlucoseValueSensor(CoordinatorEntity):
+class DexcomGlucoseValueSensor(CoordinatorEntity, SensorEntity):
"""Representation of a Dexcom glucose value sensor."""
def __init__(self, coordinator, username, unit_of_measurement):
@@ -58,7 +59,7 @@ def unique_id(self):
return self._unique_id
-class DexcomGlucoseTrendSensor(CoordinatorEntity):
+class DexcomGlucoseTrendSensor(CoordinatorEntity, SensorEntity):
"""Representation of a Dexcom glucose trend sensor."""
def __init__(self, coordinator, username):
diff --git a/homeassistant/components/dexcom/translations/de.json b/homeassistant/components/dexcom/translations/de.json
index fadb459a3d35ca..20e5ee22751f2f 100644
--- a/homeassistant/components/dexcom/translations/de.json
+++ b/homeassistant/components/dexcom/translations/de.json
@@ -4,15 +4,19 @@
"already_configured": "Konto ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"step": {
"user": {
"data": {
"password": "Passwort",
+ "server": "Server",
"username": "Benutzername"
- }
+ },
+ "description": "Anmeldedaten f\u00fcr Dexcom Share eingeben",
+ "title": "Dexcom-Integration einrichten"
}
}
},
diff --git a/homeassistant/components/dexcom/translations/fr.json b/homeassistant/components/dexcom/translations/fr.json
index d10643a3c1eac7..095c769a1be4e2 100644
--- a/homeassistant/components/dexcom/translations/fr.json
+++ b/homeassistant/components/dexcom/translations/fr.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9"
+ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9"
},
"error": {
"cannot_connect": "\u00c9chec de connexion",
diff --git a/homeassistant/components/dexcom/translations/hu.json b/homeassistant/components/dexcom/translations/hu.json
index 7a67a978ae15f7..45f38b22a84a0f 100644
--- a/homeassistant/components/dexcom/translations/hu.json
+++ b/homeassistant/components/dexcom/translations/hu.json
@@ -4,7 +4,27 @@
"already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
},
"error": {
- "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "server": "Szerver",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "unit_of_measurement": "M\u00e9rt\u00e9kegys\u00e9g"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/dexcom/translations/id.json b/homeassistant/components/dexcom/translations/id.json
new file mode 100644
index 00000000000000..2802216e782d76
--- /dev/null
+++ b/homeassistant/components/dexcom/translations/id.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "server": "Server",
+ "username": "Nama Pengguna"
+ },
+ "description": "Masukkan kredensial Dexcom Share",
+ "title": "Siapkan integrasi Dexcom"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "unit_of_measurement": "Satuan pengukuran"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dexcom/translations/ko.json b/homeassistant/components/dexcom/translations/ko.json
index 35129cbfbdee9f..c3daac033564ee 100644
--- a/homeassistant/components/dexcom/translations/ko.json
+++ b/homeassistant/components/dexcom/translations/ko.json
@@ -1,6 +1,11 @@
{
"config": {
+ "abort": {
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
"error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
diff --git a/homeassistant/components/dexcom/translations/nl.json b/homeassistant/components/dexcom/translations/nl.json
index 1dd597d28b4f49..9be09aff0c8269 100644
--- a/homeassistant/components/dexcom/translations/nl.json
+++ b/homeassistant/components/dexcom/translations/nl.json
@@ -12,7 +12,19 @@
"user": {
"data": {
"password": "Wachtwoord",
+ "server": "Server",
"username": "Gebruikersnaam"
+ },
+ "description": "Voer Dexcom Share-gegevens in",
+ "title": "Dexcom integratie instellen"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "unit_of_measurement": "Meeteenheid"
}
}
}
diff --git a/homeassistant/components/dexcom/translations/ru.json b/homeassistant/components/dexcom/translations/ru.json
index 5b6b3ab24b11a0..08543cf103ed04 100644
--- a/homeassistant/components/dexcom/translations/ru.json
+++ b/homeassistant/components/dexcom/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
@@ -13,7 +13,7 @@
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"server": "\u0421\u0435\u0440\u0432\u0435\u0440",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
"title": "Dexcom"
diff --git a/homeassistant/components/dexcom/translations/tr.json b/homeassistant/components/dexcom/translations/tr.json
index 80638d181b2e58..ec93dc078afb9c 100644
--- a/homeassistant/components/dexcom/translations/tr.json
+++ b/homeassistant/components/dexcom/translations/tr.json
@@ -2,6 +2,28 @@
"config": {
"abort": {
"already_configured": "Hesap zaten konfig\u00fcre edilmi\u015fi durumda"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "unit_of_measurement": "\u00d6l\u00e7\u00fc birimi"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/dexcom/translations/uk.json b/homeassistant/components/dexcom/translations/uk.json
new file mode 100644
index 00000000000000..66727af90d19af
--- /dev/null
+++ b/homeassistant/components/dexcom/translations/uk.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "server": "\u0421\u0435\u0440\u0432\u0435\u0440",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.",
+ "title": "Dexcom"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "unit_of_measurement": "\u041e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u0443"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py
index a71db430da41cd..e21b6cf88dce89 100644
--- a/homeassistant/components/dhcp/__init__.py
+++ b/homeassistant/components/dhcp/__init__.py
@@ -1,15 +1,24 @@
"""The dhcp integration."""
from abc import abstractmethod
+from datetime import timedelta
import fnmatch
from ipaddress import ip_address as make_ip_address
import logging
import os
import threading
+from aiodiscover import DiscoverHosts
+from aiodiscover.discovery import (
+ HOSTNAME as DISCOVERY_HOSTNAME,
+ IP_ADDRESS as DISCOVERY_IP_ADDRESS,
+ MAC_ADDRESS as DISCOVERY_MAC_ADDRESS,
+)
+from scapy.arch.common import compile_filter
from scapy.config import conf
from scapy.error import Scapy_Exception
from scapy.layers.dhcp import DHCP
+from scapy.layers.inet import IP
from scapy.layers.l2 import Ether
from scapy.sendrecv import AsyncSniffer
@@ -28,9 +37,12 @@
)
from homeassistant.core import Event, HomeAssistant, State, callback
from homeassistant.helpers.device_registry import format_mac
-from homeassistant.helpers.event import async_track_state_added_domain
+from homeassistant.helpers.event import (
+ async_track_state_added_domain,
+ async_track_time_interval,
+)
from homeassistant.loader import async_get_dhcp
-from homeassistant.util.network import is_link_local
+from homeassistant.util.network import is_invalid, is_link_local, is_loopback
from .const import DOMAIN
@@ -41,6 +53,7 @@
MAC_ADDRESS = "macaddress"
IP_ADDRESS = "ip"
DHCP_REQUEST = 3
+SCAN_INTERVAL = timedelta(minutes=60)
_LOGGER = logging.getLogger(__name__)
@@ -53,7 +66,7 @@ async def _initialize(_):
integration_matchers = await async_get_dhcp(hass)
watchers = []
- for cls in (DHCPWatcher, DeviceTrackerWatcher):
+ for cls in (DHCPWatcher, DeviceTrackerWatcher, NetworkWatcher):
watcher = cls(hass, address_data, integration_matchers)
await watcher.async_start()
watchers.append(watcher)
@@ -81,13 +94,23 @@ def __init__(self, hass, address_data, integration_matchers):
def process_client(self, ip_address, hostname, mac_address):
"""Process a client."""
- if is_link_local(make_ip_address(ip_address)):
- # Ignore self assigned addresses
+ made_ip_address = make_ip_address(ip_address)
+
+ if (
+ is_link_local(made_ip_address)
+ or is_loopback(made_ip_address)
+ or is_invalid(made_ip_address)
+ ):
+ # Ignore self assigned addresses, loopback, invalid
return
data = self._address_data.get(ip_address)
- if data and data[MAC_ADDRESS] == mac_address and data[HOSTNAME] == hostname:
+ if (
+ data
+ and data[MAC_ADDRESS] == mac_address
+ and data[HOSTNAME].startswith(hostname)
+ ):
# If the address data is the same no need
# to process it
return
@@ -125,7 +148,11 @@ def process_updated_address_data(self, ip_address, data):
self.hass.config_entries.flow.async_init(
entry["domain"],
context={"source": DOMAIN},
- data={IP_ADDRESS: ip_address, **data},
+ data={
+ IP_ADDRESS: ip_address,
+ HOSTNAME: lowercase_hostname,
+ MAC_ADDRESS: data[MAC_ADDRESS],
+ },
)
)
@@ -134,6 +161,54 @@ def create_task(self, task):
"""Pass a task to async_add_task based on which context we are in."""
+class NetworkWatcher(WatcherBase):
+ """Class to query ptr records routers."""
+
+ def __init__(self, hass, address_data, integration_matchers):
+ """Initialize class."""
+ super().__init__(hass, address_data, integration_matchers)
+ self._unsub = None
+ self._discover_hosts = None
+ self._discover_task = None
+
+ async def async_stop(self):
+ """Stop scanning for new devices on the network."""
+ if self._unsub:
+ self._unsub()
+ self._unsub = None
+ if self._discover_task:
+ self._discover_task.cancel()
+ self._discover_task = None
+
+ async def async_start(self):
+ """Start scanning for new devices on the network."""
+ self._discover_hosts = DiscoverHosts()
+ self._unsub = async_track_time_interval(
+ self.hass, self.async_start_discover, SCAN_INTERVAL
+ )
+ self.async_start_discover()
+
+ @callback
+ def async_start_discover(self, *_):
+ """Start a new discovery task if one is not running."""
+ if self._discover_task and not self._discover_task.done():
+ return
+ self._discover_task = self.create_task(self.async_discover())
+
+ async def async_discover(self):
+ """Process discovery."""
+ for host in await self._discover_hosts.async_discover():
+ self.process_client(
+ host[DISCOVERY_IP_ADDRESS],
+ host[DISCOVERY_HOSTNAME],
+ _format_mac(host[DISCOVERY_MAC_ADDRESS]),
+ )
+
+ def create_task(self, task):
+ """Pass a task to async_create_task since we are in async context."""
+ return self.hass.async_create_task(task)
+
+
class DeviceTrackerWatcher(WatcherBase):
"""Class to watch dhcp data from routers."""
@@ -183,7 +258,7 @@ def _async_process_device_state(self, state: State):
def create_task(self, task):
"""Pass a task to async_create_task since we are in async context."""
- self.hass.async_create_task(task)
+ return self.hass.async_create_task(task)
class DHCPWatcher(WatcherBase):
@@ -206,8 +281,11 @@ def _stop(self):
async def async_start(self):
"""Start watching for dhcp packets."""
+ # disable scapy promiscuous mode as we do not need it
+ conf.sniff_promisc = 0
+
try:
- _verify_l2socket_creation_permission()
+ await self.hass.async_add_executor_job(_verify_l2socket_setup, FILTER)
except (Scapy_Exception, OSError) as ex:
if os.geteuid() == 0:
_LOGGER.error("Cannot watch for dhcp packets: %s", ex)
@@ -217,12 +295,22 @@ async def async_start(self):
)
return
+ try:
+ await self.hass.async_add_executor_job(_verify_working_pcap, FILTER)
+ except (Scapy_Exception, ImportError) as ex:
+ _LOGGER.error(
+ "Cannot watch for dhcp packets without a functional packet filter: %s",
+ ex,
+ )
+ return
+
self._sniffer = AsyncSniffer(
filter=FILTER,
started_callback=self._started.set,
prn=self.handle_dhcp_packet,
store=0,
)
+
self._sniffer.start()
def handle_dhcp_packet(self, packet):
@@ -237,7 +325,7 @@ def handle_dhcp_packet(self, packet):
# DHCP request
return
- ip_address = _decode_dhcp_option(options, REQUESTED_ADDR)
+ ip_address = _decode_dhcp_option(options, REQUESTED_ADDR) or packet[IP].src
hostname = _decode_dhcp_option(options, HOSTNAME)
mac_address = _format_mac(packet[Ether].src)
@@ -248,7 +336,7 @@ def handle_dhcp_packet(self, packet):
def create_task(self, task):
"""Pass a task to hass.add_job since we are in a thread."""
- self.hass.add_job(task)
+ return self.hass.add_job(task)
def _decode_dhcp_option(dhcp_options, key):
@@ -273,7 +361,7 @@ def _format_mac(mac_address):
return format_mac(mac_address).replace(":", "")
-def _verify_l2socket_creation_permission():
+def _verify_l2socket_setup(cap_filter):
"""Create a socket using the scapy configured l2socket.
Try to create the socket
@@ -282,4 +370,13 @@ def _verify_l2socket_creation_permission():
thread so we will not be able to capture
any permission or bind errors.
"""
- conf.L2socket()
+ conf.L2socket(filter=cap_filter)
+
+
+def _verify_working_pcap(cap_filter):
+ """Verify we can create a packet filter.
+
+ If we cannot create a filter we will be listening for
+ all traffic which is too intensive.
+ """
+ compile_filter(cap_filter)
diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json
index eda229ebec746d..80cc6b116c96ed 100644
--- a/homeassistant/components/dhcp/manifest.json
+++ b/homeassistant/components/dhcp/manifest.json
@@ -3,7 +3,7 @@
"name": "DHCP Discovery",
"documentation": "https://www.home-assistant.io/integrations/dhcp",
"requirements": [
- "scapy==2.4.4"
+ "scapy==2.4.4", "aiodiscover==1.3.3"
],
"codeowners": [
"@bdraco"
diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py
index 57e12d03ffecbe..602a0f2b76fdc8 100644
--- a/homeassistant/components/dht/sensor.py
+++ b/homeassistant/components/dht/sensor.py
@@ -1,25 +1,25 @@
"""Support for Adafruit DHT temperature and humidity sensor."""
+from contextlib import suppress
from datetime import timedelta
import logging
import Adafruit_DHT # pylint: disable=import-error
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_MONITORED_CONDITIONS,
CONF_NAME,
+ CONF_PIN,
PERCENTAGE,
TEMP_FAHRENHEIT,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from homeassistant.util.temperature import celsius_to_fahrenheit
_LOGGER = logging.getLogger(__name__)
-CONF_PIN = "pin"
CONF_SENSOR = "sensor"
CONF_HUMIDITY_OFFSET = "humidity_offset"
CONF_TEMPERATURE_OFFSET = "temperature_offset"
@@ -56,7 +56,6 @@
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the DHT sensor."""
-
SENSOR_TYPES[SENSOR_TEMPERATURE][1] = hass.config.units.temperature_unit
available_sensors = {
"AM2302": Adafruit_DHT.AM2302,
@@ -76,7 +75,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
dev = []
name = config[CONF_NAME]
- try:
+ with suppress(KeyError):
for variable in config[CONF_MONITORED_CONDITIONS]:
dev.append(
DHTSensor(
@@ -88,13 +87,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
humidity_offset,
)
)
- except KeyError:
- pass
add_entities(dev, True)
-class DHTSensor(Entity):
+class DHTSensor(SensorEntity):
"""Implementation of the DHT sensor."""
def __init__(
diff --git a/homeassistant/components/dialogflow/__init__.py b/homeassistant/components/dialogflow/__init__.py
index e623919f09999f..6003f17c9e9a78 100644
--- a/homeassistant/components/dialogflow/__init__.py
+++ b/homeassistant/components/dialogflow/__init__.py
@@ -24,11 +24,6 @@ class DialogFlowError(HomeAssistantError):
"""Raised when a DialogFlow error happens."""
-async def async_setup(hass, config):
- """Set up the Dialogflow component."""
- return True
-
-
async def handle_webhook(hass, webhook_id, request):
"""Handle incoming webhook with Dialogflow requests."""
message = await request.json()
diff --git a/homeassistant/components/dialogflow/translations/de.json b/homeassistant/components/dialogflow/translations/de.json
index f1853107cc2374..2035b818b44d10 100644
--- a/homeassistant/components/dialogflow/translations/de.json
+++ b/homeassistant/components/dialogflow/translations/de.json
@@ -1,5 +1,9 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.",
+ "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen."
+ },
"create_entry": {
"default": "Um Ereignisse an den Home Assistant zu senden, musst du [Webhook-Integration von Dialogflow]({dialogflow_url}) einrichten. \n\nF\u00fclle die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / json \n\nWeitere Informationen findest du in der [Dokumentation]({docs_url})."
},
diff --git a/homeassistant/components/dialogflow/translations/et.json b/homeassistant/components/dialogflow/translations/et.json
index 989db1c25648d6..8ffe23497efe89 100644
--- a/homeassistant/components/dialogflow/translations/et.json
+++ b/homeassistant/components/dialogflow/translations/et.json
@@ -10,7 +10,7 @@
"step": {
"user": {
"description": "Kas oled kindel, et soovid seadistada Dialogflow?",
- "title": "Seadistage Dialogflow veebihaak"
+ "title": "Seadista Dialogflow veebihaak"
}
}
}
diff --git a/homeassistant/components/dialogflow/translations/hu.json b/homeassistant/components/dialogflow/translations/hu.json
index 04427a1efed1c6..17f38b0262f6e0 100644
--- a/homeassistant/components/dialogflow/translations/hu.json
+++ b/homeassistant/components/dialogflow/translations/hu.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.",
+ "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz."
+ },
"create_entry": {
- "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Dialogflow webhook integr\u00e1ci\u00f3j\u00e1t] ( {dialogflow_url} ). \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} )."
+ "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Dialogflow webhook integr\u00e1ci\u00f3j\u00e1t]({dialogflow_url}). \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t]({docs_url})."
},
"step": {
"user": {
diff --git a/homeassistant/components/dialogflow/translations/id.json b/homeassistant/components/dialogflow/translations/id.json
new file mode 100644
index 00000000000000..046a04b1dc49bf
--- /dev/null
+++ b/homeassistant/components/dialogflow/translations/id.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.",
+ "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook."
+ },
+ "create_entry": {
+ "default": "Untuk mengirim event ke Home Assistant, Anda harus menyiapkan [integrasi webhook dengan Dialogflow]({dialogflow_url}).\n\nIsi info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nBaca [dokumentasi]({docs_url}) tentang detail lebih lanjut."
+ },
+ "step": {
+ "user": {
+ "description": "Yakin ingin menyiapkan Dialogflow?",
+ "title": "Siapkan Dialogflow Webhook"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dialogflow/translations/ko.json b/homeassistant/components/dialogflow/translations/ko.json
index 7afeb6da74c63d..e0414018787777 100644
--- a/homeassistant/components/dialogflow/translations/ko.json
+++ b/homeassistant/components/dialogflow/translations/ko.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4."
+ },
"create_entry": {
- "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Dialogflow \uc6f9 \ud6c5]({dialogflow_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
+ "default": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Dialogflow \uc6f9 \ud6c5]({dialogflow_url})\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
},
"step": {
"user": {
diff --git a/homeassistant/components/dialogflow/translations/nl.json b/homeassistant/components/dialogflow/translations/nl.json
index 7cccf8ecb9b2bd..82fe7daea00150 100644
--- a/homeassistant/components/dialogflow/translations/nl.json
+++ b/homeassistant/components/dialogflow/translations/nl.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk."
+ "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.",
+ "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen."
},
"create_entry": {
"default": "Om evenementen naar de Home Assistant te verzenden, moet u [webhookintegratie van Dialogflow]({dialogflow_url}) instellen. \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nZie [de documentatie]({docs_url}) voor verdere informatie."
diff --git a/homeassistant/components/dialogflow/translations/tr.json b/homeassistant/components/dialogflow/translations/tr.json
new file mode 100644
index 00000000000000..84adcdf8225c43
--- /dev/null
+++ b/homeassistant/components/dialogflow/translations/tr.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.",
+ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dialogflow/translations/uk.json b/homeassistant/components/dialogflow/translations/uk.json
new file mode 100644
index 00000000000000..625d2db78dcb01
--- /dev/null
+++ b/homeassistant/components/dialogflow/translations/uk.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.",
+ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c."
+ },
+ "create_entry": {
+ "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f [Dialogflow]({dialogflow_url}). \n\n\u0414\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457."
+ },
+ "step": {
+ "user": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Dialogflow?",
+ "title": "Dialogflow"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py
index eb1345df45c011..9a9f82c36d2d51 100644
--- a/homeassistant/components/digital_ocean/binary_sensor.py
+++ b/homeassistant/components/digital_ocean/binary_sensor.py
@@ -79,7 +79,7 @@ def device_class(self):
return DEVICE_CLASS_MOVING
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the Digital Ocean droplet."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/digital_ocean/switch.py b/homeassistant/components/digital_ocean/switch.py
index 811b844e35cdf5..0678b9ab1a157e 100644
--- a/homeassistant/components/digital_ocean/switch.py
+++ b/homeassistant/components/digital_ocean/switch.py
@@ -71,7 +71,7 @@ def is_on(self):
return self.data.status == "active"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the Digital Ocean droplet."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py
index 22a97b9e82e85b..f1f05e815a88b4 100644
--- a/homeassistant/components/directv/__init__.py
+++ b/homeassistant/components/directv/__init__.py
@@ -1,7 +1,9 @@
"""The DirecTV integration."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
-from typing import Any, Dict
+from typing import Any
from directv import DIRECTV, DIRECTVError
@@ -28,12 +30,6 @@
SCAN_INTERVAL = timedelta(seconds=30)
-async def async_setup(hass: HomeAssistant, config: Dict) -> bool:
- """Set up the DirecTV component."""
- hass.data.setdefault(DOMAIN, {})
- return True
-
-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up DirecTV from a config entry."""
dtv = DIRECTV(entry.data[CONF_HOST], session=async_get_clientsession(hass))
@@ -43,11 +39,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except DIRECTVError as err:
raise ConfigEntryNotReady from err
+ hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = dtv
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -58,8 +55,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -87,7 +84,7 @@ def name(self) -> str:
return self._name
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about this DirecTV receiver."""
return {
ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)},
diff --git a/homeassistant/components/directv/config_flow.py b/homeassistant/components/directv/config_flow.py
index cae0e62b1bed88..71a8e052c47466 100644
--- a/homeassistant/components/directv/config_flow.py
+++ b/homeassistant/components/directv/config_flow.py
@@ -1,6 +1,8 @@
"""Config flow for DirecTV."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from urllib.parse import urlparse
from directv import DIRECTV, DIRECTVError
@@ -16,8 +18,7 @@
HomeAssistantType,
)
-from .const import CONF_RECEIVER_ID
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import CONF_RECEIVER_ID, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -25,7 +26,7 @@
ERROR_UNKNOWN = "unknown"
-async def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]:
+async def validate_input(hass: HomeAssistantType, data: dict) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
@@ -48,8 +49,8 @@ def __init__(self):
self.discovery_info = {}
async def async_step_user(
- self, user_input: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, user_input: ConfigType | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initiated by the user."""
if user_input is None:
return self._show_setup_form()
@@ -71,7 +72,7 @@ async def async_step_user(
async def async_step_ssdp(
self, discovery_info: DiscoveryInfoType
- ) -> Dict[str, Any]:
+ ) -> dict[str, Any]:
"""Handle SSDP discovery."""
host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname
receiver_id = None
@@ -79,7 +80,6 @@ async def async_step_ssdp(
if discovery_info.get(ATTR_UPNP_SERIAL):
receiver_id = discovery_info[ATTR_UPNP_SERIAL][4:] # strips off RID-
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update({"title_placeholders": {"name": host}})
self.discovery_info.update(
@@ -105,7 +105,7 @@ async def async_step_ssdp(
async def async_step_ssdp_confirm(
self, user_input: ConfigType = None
- ) -> Dict[str, Any]:
+ ) -> dict[str, Any]:
"""Handle a confirmation flow initiated by SSDP."""
if user_input is None:
return self.async_show_form(
@@ -119,7 +119,7 @@ async def async_step_ssdp_confirm(
data=self.discovery_info,
)
- def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
+ def _show_setup_form(self, errors: dict | None = None) -> dict[str, Any]:
"""Show the setup form to the user."""
return self.async_show_form(
step_id="user",
diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py
index dfd88ca885b5ff..4004592e5dcc0e 100644
--- a/homeassistant/components/directv/media_player.py
+++ b/homeassistant/components/directv/media_player.py
@@ -1,6 +1,8 @@
"""Support for the DirecTV receivers."""
+from __future__ import annotations
+
import logging
-from typing import Callable, List, Optional
+from typing import Callable
from directv import DIRECTV
@@ -64,7 +66,7 @@
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
- async_add_entities: Callable[[List, bool], None],
+ async_add_entities: Callable[[list, bool], None],
) -> bool:
"""Set up the DirecTV config entry."""
dtv = hass.data[DOMAIN][entry.entry_id]
@@ -124,7 +126,7 @@ async def async_update(self):
self._assumed_state = self._is_recorded
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
if self._is_standby:
return {}
@@ -141,7 +143,7 @@ def name(self):
return self._name
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the class of this device."""
return DEVICE_CLASS_RECEIVER
diff --git a/homeassistant/components/directv/remote.py b/homeassistant/components/directv/remote.py
index 64695ae3813592..b35580928acb55 100644
--- a/homeassistant/components/directv/remote.py
+++ b/homeassistant/components/directv/remote.py
@@ -1,7 +1,9 @@
"""Support for the DIRECTV remote."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Any, Callable, Iterable, List
+from typing import Any, Callable, Iterable
from directv import DIRECTV, DIRECTVError
@@ -20,7 +22,7 @@
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
- async_add_entities: Callable[[List, bool], None],
+ async_add_entities: Callable[[list, bool], None],
) -> bool:
"""Load DirecTV remote based on a config entry."""
dtv = hass.data[DOMAIN][entry.entry_id]
diff --git a/homeassistant/components/directv/translations/hu.json b/homeassistant/components/directv/translations/hu.json
index 5d8fc929b920f2..0309eb358814c0 100644
--- a/homeassistant/components/directv/translations/hu.json
+++ b/homeassistant/components/directv/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",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
diff --git a/homeassistant/components/directv/translations/id.json b/homeassistant/components/directv/translations/id.json
new file mode 100644
index 00000000000000..74f778d6cee28b
--- /dev/null
+++ b/homeassistant/components/directv/translations/id.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "flow_title": "DirecTV: {name}",
+ "step": {
+ "ssdp_confirm": {
+ "description": "Ingin menyiapkan {name}?"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/directv/translations/ko.json b/homeassistant/components/directv/translations/ko.json
index f2526418d08292..ecbde981160fc3 100644
--- a/homeassistant/components/directv/translations/ko.json
+++ b/homeassistant/components/directv/translations/ko.json
@@ -10,7 +10,7 @@
"flow_title": "DirecTV: {name}",
"step": {
"ssdp_confirm": {
- "description": "{name} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ "description": "{name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
},
"user": {
"data": {
diff --git a/homeassistant/components/directv/translations/nl.json b/homeassistant/components/directv/translations/nl.json
index 2024368daf6d91..957095712342a2 100644
--- a/homeassistant/components/directv/translations/nl.json
+++ b/homeassistant/components/directv/translations/nl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "DirecTV-ontvanger is al geconfigureerd",
+ "already_configured": "Apparaat is al geconfigureerd",
"unknown": "Onverwachte fout"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw"
+ "cannot_connect": "Kan geen verbinding maken"
},
"flow_title": "DirecTV: {name}",
"step": {
@@ -18,7 +18,7 @@
},
"user": {
"data": {
- "host": "Host- of IP-adres"
+ "host": "Host"
}
}
}
diff --git a/homeassistant/components/directv/translations/tr.json b/homeassistant/components/directv/translations/tr.json
new file mode 100644
index 00000000000000..daca8f1ef6246b
--- /dev/null
+++ b/homeassistant/components/directv/translations/tr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "unknown": "Beklenmeyen hata"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "ssdp_confirm": {
+ "description": "{name} kurmak istiyor musunuz?"
+ },
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/directv/translations/uk.json b/homeassistant/components/directv/translations/uk.json
new file mode 100644
index 00000000000000..5371f638e3d8a4
--- /dev/null
+++ b/homeassistant/components/directv/translations/uk.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "flow_title": "DirecTV: {name}",
+ "step": {
+ "ssdp_confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name}?"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py
index 4d78b540a0c7b0..81beec0e60ebd4 100644
--- a/homeassistant/components/discogs/sensor.py
+++ b/homeassistant/components/discogs/sensor.py
@@ -6,7 +6,7 @@
import discogs_client
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_MONITORED_CONDITIONS,
@@ -15,7 +15,6 @@
)
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -89,7 +88,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class DiscogsSensor(Entity):
+class DiscogsSensor(SensorEntity):
"""Create a new Discogs sensor for a specific type."""
def __init__(self, discogs_data, name, sensor_type):
@@ -121,7 +120,7 @@ def unit_of_measurement(self):
return SENSORS[self._type]["unit_of_measurement"]
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes of the sensor."""
if self._state is None or self._attrs is None:
return None
diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py
index b97202033bde1a..883958226d8d09 100644
--- a/homeassistant/components/discovery/__init__.py
+++ b/homeassistant/components/discovery/__init__.py
@@ -1,11 +1,4 @@
-"""
-Starts a service to scan in intervals for new devices.
-
-Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered.
-
-Knows which components handle certain types, will make sure they are
-loaded before the EVENT_PLATFORM_DISCOVERED is fired.
-"""
+"""Starts a service to scan in intervals for new devices."""
from datetime import timedelta
import json
import logging
@@ -29,7 +22,6 @@
SERVICE_DAIKIN = "daikin"
SERVICE_DLNA_DMR = "dlna_dmr"
SERVICE_ENIGMA2 = "enigma2"
-SERVICE_FREEBOX = "freebox"
SERVICE_HASS_IOS_APP = "hass_ios"
SERVICE_HASSIO = "hassio"
SERVICE_HEOS = "heos"
@@ -45,25 +37,17 @@
SERVICE_WINK = "wink"
SERVICE_XIAOMI_GW = "xiaomi_gw"
+# These have custom protocols
CONFIG_ENTRY_HANDLERS = {
- SERVICE_DAIKIN: "daikin",
SERVICE_TELLDUSLIVE: "tellduslive",
"logitech_mediaserver": "squeezebox",
}
+# These have no config flows
SERVICE_HANDLERS = {
- SERVICE_MOBILE_APP: ("mobile_app", None),
- SERVICE_HASS_IOS_APP: ("ios", None),
SERVICE_NETGEAR: ("device_tracker", None),
- SERVICE_HASSIO: ("hassio", None),
- SERVICE_APPLE_TV: ("apple_tv", None),
SERVICE_ENIGMA2: ("media_player", "enigma2"),
- SERVICE_WINK: ("wink", None),
SERVICE_SABNZBD: ("sabnzbd", None),
- SERVICE_SAMSUNG_PRINTER: ("sensor", None),
- SERVICE_KONNECTED: ("konnected", None),
- SERVICE_OCTOPRINT: ("octoprint", None),
- SERVICE_FREEBOX: ("freebox", None),
"yamaha": ("media_player", "yamaha"),
"frontier_silicon": ("media_player", "frontier_silicon"),
"openhome": ("media_player", "openhome"),
@@ -76,20 +60,29 @@
OPTIONAL_SERVICE_HANDLERS = {SERVICE_DLNA_DMR: ("media_player", "dlna_dmr")}
MIGRATED_SERVICE_HANDLERS = [
+ SERVICE_APPLE_TV,
"axis",
"deconz",
+ SERVICE_DAIKIN,
"denonavr",
"esphome",
"google_cast",
+ SERVICE_HASS_IOS_APP,
+ SERVICE_HASSIO,
SERVICE_HEOS,
"harmony",
"homekit",
"ikea_tradfri",
"kodi",
+ SERVICE_KONNECTED,
+ SERVICE_MOBILE_APP,
+ SERVICE_OCTOPRINT,
"philips_hue",
+ SERVICE_SAMSUNG_PRINTER,
"sonos",
"songpal",
SERVICE_WEMO,
+ SERVICE_WINK,
SERVICE_XIAOMI_GW,
"volumio",
SERVICE_YEELIGHT,
diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py
index c2bec855b9b965..2a5e7662d454a0 100644
--- a/homeassistant/components/dlib_face_detect/image_processing.py
+++ b/homeassistant/components/dlib_face_detect/image_processing.py
@@ -9,6 +9,7 @@
CONF_SOURCE,
ImageProcessingFaceEntity,
)
+from homeassistant.const import ATTR_LOCATION
from homeassistant.core import split_entity_id
# pylint: disable=unused-import
@@ -16,8 +17,6 @@
PLATFORM_SCHEMA,
)
-ATTR_LOCATION = "location"
-
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Dlib Face detection platform."""
diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py
index 32c2aa5868c009..f9db607c2986e8 100644
--- a/homeassistant/components/dlib_face_identify/image_processing.py
+++ b/homeassistant/components/dlib_face_identify/image_processing.py
@@ -14,12 +14,12 @@
PLATFORM_SCHEMA,
ImageProcessingFaceEntity,
)
+from homeassistant.const import ATTR_NAME
from homeassistant.core import split_entity_id
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
-ATTR_NAME = "name"
CONF_FACES = "faces"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py
index c173c879ad1808..432bc7ec0b366d 100644
--- a/homeassistant/components/dlink/switch.py
+++ b/homeassistant/components/dlink/switch.py
@@ -71,7 +71,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
try:
ui_temp = self.units.temperature(int(self.data.temperature), TEMP_CELSIUS)
diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json
index ac7a4b22e58802..094a9adc43a769 100644
--- a/homeassistant/components/dlna_dmr/manifest.json
+++ b/homeassistant/components/dlna_dmr/manifest.json
@@ -2,6 +2,6 @@
"domain": "dlna_dmr",
"name": "DLNA Digital Media Renderer",
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
- "requirements": ["async-upnp-client==0.14.13"],
+ "requirements": ["async-upnp-client==0.16.0"],
"codeowners": []
}
diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py
index f8af118caedbe6..c208e1eb2ffaea 100644
--- a/homeassistant/components/dlna_dmr/media_player.py
+++ b/homeassistant/components/dlna_dmr/media_player.py
@@ -1,9 +1,10 @@
"""Support for DLNA DMR (Device Media Renderer)."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
import functools
import logging
-from typing import Optional
import aiohttp
from async_upnp_client import UpnpFactory
@@ -13,14 +14,6 @@
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
from homeassistant.components.media_player.const import (
- MEDIA_TYPE_CHANNEL,
- MEDIA_TYPE_EPISODE,
- MEDIA_TYPE_IMAGE,
- MEDIA_TYPE_MOVIE,
- MEDIA_TYPE_MUSIC,
- MEDIA_TYPE_PLAYLIST,
- MEDIA_TYPE_TVSHOW,
- MEDIA_TYPE_VIDEO,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY,
@@ -69,28 +62,6 @@
}
)
-HOME_ASSISTANT_UPNP_CLASS_MAPPING = {
- MEDIA_TYPE_MUSIC: "object.item.audioItem",
- MEDIA_TYPE_TVSHOW: "object.item.videoItem",
- MEDIA_TYPE_MOVIE: "object.item.videoItem",
- MEDIA_TYPE_VIDEO: "object.item.videoItem",
- MEDIA_TYPE_EPISODE: "object.item.videoItem",
- MEDIA_TYPE_CHANNEL: "object.item.videoItem",
- MEDIA_TYPE_IMAGE: "object.item.imageItem",
- MEDIA_TYPE_PLAYLIST: "object.item.playlistItem",
-}
-UPNP_CLASS_DEFAULT = "object.item"
-HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING = {
- MEDIA_TYPE_MUSIC: "audio/*",
- MEDIA_TYPE_TVSHOW: "video/*",
- MEDIA_TYPE_MOVIE: "video/*",
- MEDIA_TYPE_VIDEO: "video/*",
- MEDIA_TYPE_EPISODE: "video/*",
- MEDIA_TYPE_CHANNEL: "video/*",
- MEDIA_TYPE_IMAGE: "image/*",
- MEDIA_TYPE_PLAYLIST: "playlist/*",
-}
-
def catch_request_errors():
"""Catch asyncio.TimeoutError, aiohttp.ClientError errors."""
@@ -116,7 +87,7 @@ async def async_start_event_handler(
server_host: str,
server_port: int,
requester,
- callback_url_override: Optional[str] = None,
+ callback_url_override: str | None = None,
):
"""Register notify view."""
hass_data = hass.data[DLNA_DMR_DATA]
@@ -341,20 +312,15 @@ async def async_media_seek(self, position):
@catch_request_errors()
async def async_play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
+ _LOGGER.debug("Playing media: %s, %s, %s", media_type, media_id, kwargs)
title = "Home Assistant"
- mime_type = HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING.get(media_type, media_type)
- upnp_class = HOME_ASSISTANT_UPNP_CLASS_MAPPING.get(
- media_type, UPNP_CLASS_DEFAULT
- )
# Stop current playing media
if self._device.can_stop:
await self.async_media_stop()
# Queue media
- await self._device.async_set_transport_uri(
- media_id, title, mime_type, upnp_class
- )
+ await self._device.async_set_transport_uri(media_id, title)
await self._device.async_wait_for_can_play()
# If already playing, no need to call Play
diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py
index b202ff8485c794..01d6e2f4f2aa90 100644
--- a/homeassistant/components/dnsip/sensor.py
+++ b/homeassistant/components/dnsip/sensor.py
@@ -6,10 +6,9 @@
from aiodns.error import DNSError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -55,7 +54,7 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=N
async_add_devices([WanIpSensor(hass, name, hostname, resolver, ipv6)], True)
-class WanIpSensor(Entity):
+class WanIpSensor(SensorEntity):
"""Implementation of a DNS IP sensor."""
def __init__(self, hass, name, hostname, resolver, ipv6):
diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py
index f4180ffcffa5d1..3e41c1871bfe1d 100644
--- a/homeassistant/components/doods/image_processing.py
+++ b/homeassistant/components/doods/image_processing.py
@@ -16,7 +16,7 @@
PLATFORM_SCHEMA,
ImageProcessingEntity,
)
-from homeassistant.const import CONF_TIMEOUT
+from homeassistant.const import CONF_COVERS, CONF_TIMEOUT, CONF_URL
from homeassistant.core import split_entity_id
from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv
@@ -29,12 +29,10 @@
ATTR_TOTAL_MATCHES = "total_matches"
ATTR_PROCESS_TIME = "process_time"
-CONF_URL = "url"
CONF_AUTH_KEY = "auth_key"
CONF_DETECTOR = "detector"
CONF_LABELS = "labels"
CONF_AREA = "area"
-CONF_COVERS = "covers"
CONF_TOP = "top"
CONF_BOTTOM = "bottom"
CONF_RIGHT = "right"
@@ -223,7 +221,7 @@ def state(self):
return self._total_matches
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
return {
ATTR_MATCHES: self._matches,
diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json
index ecbcd8563a78fd..6f6fcb0d6b3b55 100644
--- a/homeassistant/components/doods/manifest.json
+++ b/homeassistant/components/doods/manifest.json
@@ -2,6 +2,6 @@
"domain": "doods",
"name": "DOODS - Dedicated Open Object Detection Service",
"documentation": "https://www.home-assistant.io/integrations/doods",
- "requirements": ["pydoods==1.0.2", "pillow==8.1.0"],
+ "requirements": ["pydoods==1.0.2", "pillow==8.1.2"],
"codeowners": []
}
diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py
index 1dc5bf56c8638c..3e8e59df203f23 100644
--- a/homeassistant/components/doorbird/__init__.py
+++ b/homeassistant/components/doorbird/__init__.py
@@ -1,11 +1,10 @@
"""Support for DoorBird devices."""
import asyncio
import logging
-import urllib
-from urllib.error import HTTPError
from aiohttp import web
from doorbirdpy import DoorBird
+import requests
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
@@ -34,6 +33,7 @@
DOOR_STATION_EVENT_ENTITY_IDS,
DOOR_STATION_INFO,
PLATFORMS,
+ UNDO_UPDATE_LISTENER,
)
from .util import get_doorstation_by_token
@@ -128,10 +128,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
device = DoorBird(device_ip, username, password)
try:
- status = await hass.async_add_executor_job(device.ready)
- info = await hass.async_add_executor_job(device.info)
- except urllib.error.HTTPError as err:
- if err.code == HTTP_UNAUTHORIZED:
+ status, info = await hass.async_add_executor_job(_init_doorbird_device, device)
+ except requests.exceptions.HTTPError as err:
+ if err.response.status_code == HTTP_UNAUTHORIZED:
_LOGGER.error(
"Authorization rejected by DoorBird for %s@%s", username, device_ip
)
@@ -154,34 +153,42 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
custom_url = doorstation_config.get(CONF_CUSTOM_URL)
name = doorstation_config.get(CONF_NAME)
events = doorstation_options.get(CONF_EVENTS, [])
- doorstation = ConfiguredDoorBird(device, name, events, custom_url, token)
+ doorstation = ConfiguredDoorBird(device, name, custom_url, token)
+ doorstation.update_events(events)
# Subscribe to doorbell or motion events
if not await _async_register_events(hass, doorstation):
raise ConfigEntryNotReady
+ undo_listener = entry.add_update_listener(_update_listener)
+
hass.data[DOMAIN][config_entry_id] = {
DOOR_STATION: doorstation,
DOOR_STATION_INFO: info,
+ UNDO_UPDATE_LISTENER: undo_listener,
}
- entry.add_update_listener(_update_listener)
-
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
+def _init_doorbird_device(device):
+ return device.ready(), device.info()
+
+
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
+ hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
+
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -194,8 +201,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
async def _async_register_events(hass, doorstation):
try:
await hass.async_add_executor_job(doorstation.register_events, hass)
- except HTTPError:
- hass.components.persistent_notification.create(
+ except requests.exceptions.HTTPError:
+ hass.components.persistent_notification.async_create(
"Doorbird configuration failed. Please verify that API "
"Operator permission is enabled for the Doorbird user. "
"A restart will be required once permissions have been "
@@ -212,8 +219,7 @@ async def _update_listener(hass: HomeAssistant, entry: ConfigEntry):
"""Handle options update."""
config_entry_id = entry.entry_id
doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION]
-
- doorstation.events = entry.options[CONF_EVENTS]
+ doorstation.update_events(entry.options[CONF_EVENTS])
# Subscribe to doorbell or motion events
await _async_register_events(hass, doorstation)
@@ -234,14 +240,19 @@ def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: Confi
class ConfiguredDoorBird:
"""Attach additional information to pass along with configured device."""
- def __init__(self, device, name, events, custom_url, token):
+ def __init__(self, device, name, custom_url, token):
"""Initialize configured device."""
self._name = name
self._device = device
self._custom_url = custom_url
+ self.events = None
+ self.doorstation_events = None
+ self._token = token
+
+ def update_events(self, events):
+ """Update the doorbird events."""
self.events = events
self.doorstation_events = [self._get_event_name(event) for event in self.events]
- self._token = token
@property
def name(self):
@@ -305,16 +316,7 @@ def _register_event(self, hass_url, event):
def webhook_is_registered(self, url, favs=None) -> bool:
"""Return whether the given URL is registered as a device favorite."""
- favs = favs if favs else self.device.favorites()
-
- if "http" not in favs:
- return False
-
- for fav in favs["http"].values():
- if fav["value"] == url:
- return True
-
- return False
+ return self.get_webhook_id(url, favs) is not None
def get_webhook_id(self, url, favs=None) -> str or None:
"""
diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py
index 8e3f661254df15..f69b38c7a7a407 100644
--- a/homeassistant/components/doorbird/config_flow.py
+++ b/homeassistant/components/doorbird/config_flow.py
@@ -1,9 +1,9 @@
"""Config flow for DoorBird integration."""
from ipaddress import ip_address
import logging
-import urllib
from doorbirdpy import DoorBird
+import requests
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
@@ -17,8 +17,7 @@
from homeassistant.core import callback
from homeassistant.util.network import is_link_local
-from .const import CONF_EVENTS, DOORBIRD_OUI
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import CONF_EVENTS, DOMAIN, DOORBIRD_OUI
from .util import get_mac_address_from_doorstation_info
_LOGGER = logging.getLogger(__name__)
@@ -35,17 +34,18 @@ def _schema_with_defaults(host=None, name=None):
)
-async def validate_input(hass: core.HomeAssistant, data):
- """Validate the user input allows us to connect.
+def _check_device(device):
+ """Verify we can connect to the device and return the status."""
+ return device.ready(), device.info()
+
- Data has the keys from DATA_SCHEMA with values provided by the user.
- """
+async def validate_input(hass: core.HomeAssistant, data):
+ """Validate the user input allows us to connect."""
device = DoorBird(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD])
try:
- status = await hass.async_add_executor_job(device.ready)
- info = await hass.async_add_executor_job(device.info)
- except urllib.error.HTTPError as err:
- if err.code == HTTP_UNAUTHORIZED:
+ status, info = await hass.async_add_executor_job(_check_device, device)
+ except requests.exceptions.HTTPError as err:
+ if err.response.status_code == HTTP_UNAUTHORIZED:
raise InvalidAuth from err
raise CannotConnect from err
except OSError as err:
@@ -60,6 +60,19 @@ async def validate_input(hass: core.HomeAssistant, data):
return {"title": data[CONF_HOST], "mac_addr": mac_addr}
+async def async_verify_supported_device(hass, host):
+ """Verify the doorbell state endpoint returns a 401."""
+ device = DoorBird(host, "", "")
+ try:
+ await hass.async_add_executor_job(device.doorbell_state)
+ except requests.exceptions.HTTPError as err:
+ if err.response.status_code == HTTP_UNAUTHORIZED:
+ return True
+ except OSError:
+ return False
+ return False
+
+
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for DoorBird."""
@@ -86,31 +99,29 @@ async def async_step_user(self, user_input=None):
async def async_step_zeroconf(self, discovery_info):
"""Prepare configuration for a discovered doorbird device."""
macaddress = discovery_info["properties"]["macaddress"]
+ host = discovery_info[CONF_HOST]
if macaddress[:6] != DOORBIRD_OUI:
return self.async_abort(reason="not_doorbird_device")
- if is_link_local(ip_address(discovery_info[CONF_HOST])):
+ if is_link_local(ip_address(host)):
return self.async_abort(reason="link_local_address")
+ if not await async_verify_supported_device(self.hass, host):
+ return self.async_abort(reason="not_doorbird_device")
await self.async_set_unique_id(macaddress)
- self._abort_if_unique_id_configured(
- updates={CONF_HOST: discovery_info[CONF_HOST]}
- )
+ self._abort_if_unique_id_configured(updates={CONF_HOST: host})
chop_ending = "._axis-video._tcp.local."
friendly_hostname = discovery_info["name"]
if friendly_hostname.endswith(chop_ending):
friendly_hostname = friendly_hostname[: -len(chop_ending)]
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {
CONF_NAME: friendly_hostname,
- CONF_HOST: discovery_info[CONF_HOST],
+ CONF_HOST: host,
}
- self.discovery_schema = _schema_with_defaults(
- host=discovery_info[CONF_HOST], name=friendly_hostname
- )
+ self.discovery_schema = _schema_with_defaults(host=host, name=friendly_hostname)
return await self.async_step_user()
diff --git a/homeassistant/components/doorbird/const.py b/homeassistant/components/doorbird/const.py
index af847dac673a26..46a95f0d5009ff 100644
--- a/homeassistant/components/doorbird/const.py
+++ b/homeassistant/components/doorbird/const.py
@@ -17,3 +17,5 @@
DOORBIRD_INFO_KEY_RELAYS = "RELAYS"
DOORBIRD_INFO_KEY_PRIMARY_MAC_ADDR = "PRIMARY_MAC_ADDR"
DOORBIRD_INFO_KEY_WIFI_MAC_ADDR = "WIFI_MAC_ADDR"
+
+UNDO_UPDATE_LISTENER = "undo_update_listener"
diff --git a/homeassistant/components/doorbird/switch.py b/homeassistant/components/doorbird/switch.py
index f1f146aebb96fe..424bb79092f40b 100644
--- a/homeassistant/components/doorbird/switch.py
+++ b/homeassistant/components/doorbird/switch.py
@@ -17,8 +17,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities = []
config_entry_id = config_entry.entry_id
- doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION]
- doorstation_info = hass.data[DOMAIN][config_entry_id][DOOR_STATION_INFO]
+ data = hass.data[DOMAIN][config_entry_id]
+ doorstation = data[DOOR_STATION]
+ doorstation_info = data[DOOR_STATION_INFO]
relays = doorstation_info["RELAYS"]
relays.append(IR_RELAY)
diff --git a/homeassistant/components/doorbird/translations/de.json b/homeassistant/components/doorbird/translations/de.json
index 62bb11d6a8cb0c..0d6bef7a63fb4f 100644
--- a/homeassistant/components/doorbird/translations/de.json
+++ b/homeassistant/components/doorbird/translations/de.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Dieser DoorBird ist bereits konfiguriert",
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
"link_local_address": "Lokale Linkadressen werden nicht unterst\u00fctzt",
"not_doorbird_device": "Dieses Ger\u00e4t ist kein DoorBird"
},
diff --git a/homeassistant/components/doorbird/translations/hu.json b/homeassistant/components/doorbird/translations/hu.json
index 618368433acc67..3f74783b7ac781 100644
--- a/homeassistant/components/doorbird/translations/hu.json
+++ b/homeassistant/components/doorbird/translations/hu.json
@@ -1,14 +1,19 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
- "unknown": "V\u00e1ratlan hiba"
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
+ "flow_title": "DoorBird {name} ({host})",
"step": {
"user": {
"data": {
"host": "Hoszt",
+ "name": "Eszk\u00f6z neve",
"password": "Jelsz\u00f3",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
}
diff --git a/homeassistant/components/doorbird/translations/id.json b/homeassistant/components/doorbird/translations/id.json
new file mode 100644
index 00000000000000..f708780ce311f6
--- /dev/null
+++ b/homeassistant/components/doorbird/translations/id.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "link_local_address": "Tautan alamat lokal tidak didukung",
+ "not_doorbird_device": "Perangkat ini bukan DoorBird"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "flow_title": "DoorBird {name} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nama Perangkat",
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "title": "Hubungkan ke DoorBird"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "events": "Daftar event yang dipisahkan koma."
+ },
+ "description": "Tambahkan nama event yang dipisahkan koma untuk setiap event yang ingin dilacak. Setelah memasukkannya di sini, gunakan aplikasi DoorBird untuk menetapkannya ke event tertentu. Baca dokumentasi di https://www.home-assistant.io/integrations/doorbird/#events. Contoh: somebody_pressed_the_button, motion"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/doorbird/translations/ko.json b/homeassistant/components/doorbird/translations/ko.json
index 74057a94d26784..85d00317c2d63b 100644
--- a/homeassistant/components/doorbird/translations/ko.json
+++ b/homeassistant/components/doorbird/translations/ko.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 DoorBird \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"link_local_address": "\ub85c\uceec \uc8fc\uc18c \uc5f0\uacb0\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4",
"not_doorbird_device": "\uc774 \uae30\uae30\ub294 DoorBird \uac00 \uc544\ub2d9\ub2c8\ub2e4"
},
@@ -19,7 +19,7 @@
"password": "\ube44\ubc00\ubc88\ud638",
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
},
- "title": "DoorBird \uc5d0 \uc5f0\uacb0\ud558\uae30"
+ "title": "DoorBird\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
},
@@ -29,7 +29,7 @@
"data": {
"events": "\uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \uc774\ubca4\ud2b8 \ubaa9\ub85d."
},
- "description": "\ucd94\uc801\ud558\ub824\ub294 \uac01 \uc774\ubca4\ud2b8\uc5d0 \ub300\ud574 \uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \uc774\ubca4\ud2b8 \uc774\ub984\uc744 \ucd94\uac00\ud574\uc8fc\uc138\uc694. \uc5ec\uae30\uc5d0 \uc785\ub825\ud55c \ud6c4 DoorBird \uc571\uc744 \uc0ac\uc6a9\ud558\uc5ec \ud2b9\uc815 \uc774\ubca4\ud2b8\uc5d0 \ud560\ub2f9\ud574\uc8fc\uc138\uc694. \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 https://www.home-assistant.io/integrations/doorbird/#event \uc124\uba85\uc11c\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694. \uc608: someone_pressed_the_button, motion"
+ "description": "\ucd94\uc801\ud558\ub824\ub294 \uac01 \uc774\ubca4\ud2b8\uc5d0 \ub300\ud574 \uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \uc774\ubca4\ud2b8 \uc774\ub984\uc744 \ucd94\uac00\ud574\uc8fc\uc138\uc694. \uc5ec\uae30\uc5d0 \uc785\ub825\ud55c \ud6c4 DoorBird \uc571\uc744 \uc0ac\uc6a9\ud558\uc5ec \ud2b9\uc815 \uc774\ubca4\ud2b8\uc5d0 \ud560\ub2f9\ud574\uc8fc\uc138\uc694. \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 https://www.home-assistant.io/integrations/doorbird/#event \uc124\uba85\uc11c\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694. \uc608: somebody_pressed_the_button, motion"
}
}
}
diff --git a/homeassistant/components/doorbird/translations/nl.json b/homeassistant/components/doorbird/translations/nl.json
index 625367484b0ac0..1c43ee2d9c28c5 100644
--- a/homeassistant/components/doorbird/translations/nl.json
+++ b/homeassistant/components/doorbird/translations/nl.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Deze DoorBird is al geconfigureerd",
+ "already_configured": "Apparaat is al geconfigureerd",
"link_local_address": "Link-lokale adressen worden niet ondersteund",
"not_doorbird_device": "Dit apparaat is geen DoorBird"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
@@ -14,7 +14,7 @@
"step": {
"user": {
"data": {
- "host": "Host (IP-adres)",
+ "host": "Host",
"name": "Apparaatnaam",
"password": "Wachtwoord",
"username": "Gebruikersnaam"
diff --git a/homeassistant/components/doorbird/translations/ru.json b/homeassistant/components/doorbird/translations/ru.json
index 274b88a8b473f1..4d5695a3ab2367 100644
--- a/homeassistant/components/doorbird/translations/ru.json
+++ b/homeassistant/components/doorbird/translations/ru.json
@@ -7,7 +7,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"flow_title": "DoorBird {name} ({host})",
@@ -17,7 +17,7 @@
"host": "\u0425\u043e\u0441\u0442",
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a DoorBird"
}
diff --git a/homeassistant/components/doorbird/translations/sv.json b/homeassistant/components/doorbird/translations/sv.json
index b2a809a576ef0d..546535fb93736b 100644
--- a/homeassistant/components/doorbird/translations/sv.json
+++ b/homeassistant/components/doorbird/translations/sv.json
@@ -9,7 +9,8 @@
"host": "V\u00e4rd (IP-adress)",
"name": "Enhetsnamn",
"username": "Anv\u00e4ndarnamn"
- }
+ },
+ "title": "Anslut till DoorBird"
}
}
}
diff --git a/homeassistant/components/doorbird/translations/tr.json b/homeassistant/components/doorbird/translations/tr.json
new file mode 100644
index 00000000000000..d7a1ca8a93a9f0
--- /dev/null
+++ b/homeassistant/components/doorbird/translations/tr.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "name": "Cihaz ad\u0131",
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/doorbird/translations/uk.json b/homeassistant/components/doorbird/translations/uk.json
new file mode 100644
index 00000000000000..07bbdfacafe69e
--- /dev/null
+++ b/homeassistant/components/doorbird/translations/uk.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "link_local_address": "\u041f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0456 \u0430\u0434\u0440\u0435\u0441\u0438 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.",
+ "not_doorbird_device": "\u0426\u0435 \u043d\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 DoorBird."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "flow_title": "DoorBird {name} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e DoorBird"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "events": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u0434\u0456\u0439 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u0443."
+ },
+ "description": "\u0414\u043e\u0434\u0430\u0439\u0442\u0435 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u0443 \u043d\u0430\u0437\u0432\u0438 \u043f\u043e\u0434\u0456\u0439, \u044f\u043a\u0435 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0456\u0434\u0441\u043b\u0456\u0434\u043a\u043e\u0432\u0443\u0432\u0430\u0442\u0438. \u041f\u0456\u0441\u043b\u044f \u0446\u044c\u043e\u0433\u043e, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a DoorBird, \u0449\u043e\u0431 \u043f\u0440\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 \u0457\u0445 \u0434\u043e \u043f\u0435\u0432\u043d\u043e\u0457 \u043f\u043e\u0434\u0456\u0457. \u041f\u0440\u0438\u043a\u043b\u0430\u0434: somebody_pressed_the_button, motion. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457: https://www.home-assistant.io/integrations/doorbird/#events."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py
index 46ff402788ec22..e7b3dbdd363084 100644
--- a/homeassistant/components/dovado/sensor.py
+++ b/homeassistant/components/dovado/sensor.py
@@ -4,10 +4,9 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_SENSORS, DATA_GIGABYTES, PERCENTAGE
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from . import DOMAIN as DOVADO_DOMAIN
@@ -53,7 +52,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(entities)
-class DovadoSensor(Entity):
+class DovadoSensor(SensorEntity):
"""Representation of a Dovado sensor."""
def __init__(self, data, sensor):
@@ -105,6 +104,6 @@ def unit_of_measurement(self):
return SENSORS[self._sensor][2]
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {k: v for k, v in self._data.state.items() if k not in ["date", "time"]}
diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py
index 94617ce43aa63b..89aa4a465cfeff 100644
--- a/homeassistant/components/downloader/__init__.py
+++ b/homeassistant/components/downloader/__init__.py
@@ -70,8 +70,9 @@ def do_download():
overwrite = service.data.get(ATTR_OVERWRITE)
- # Check the path
- raise_if_invalid_path(subdir)
+ if subdir:
+ # Check the path
+ raise_if_invalid_path(subdir)
final_path = None
@@ -79,7 +80,7 @@ def do_download():
if req.status_code != HTTP_OK:
_LOGGER.warning(
- "downloading '%s' failed, status_code=%d", url, req.status_code
+ "Downloading '%s' failed, status_code=%d", url, req.status_code
)
hass.bus.fire(
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
diff --git a/homeassistant/components/downloader/services.yaml b/homeassistant/components/downloader/services.yaml
index 6e16e00432fdb7..cecb3804227a77 100644
--- a/homeassistant/components/downloader/services.yaml
+++ b/homeassistant/components/downloader/services.yaml
@@ -1,15 +1,29 @@
download_file:
- description: Downloads a file to the download location.
+ name: Download file
+ description: Download a file to the download location.
fields:
url:
+ name: URL
description: The URL of the file to download.
+ required: true
example: "http://example.org/myfile"
+ selector:
+ text:
subdir:
+ name: Subdirectory
description: Download into subdirectory.
example: "download_dir"
+ selector:
+ text:
filename:
+ name: Filename
description: Determine the filename.
example: "my_file_name"
+ selector:
+ text:
overwrite:
+ name: Overwrite
description: Whether to overwrite the file or not.
- example: "false"
+ default: false
+ selector:
+ boolean:
diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py
index 50823bb1d29293..f130f500545d61 100644
--- a/homeassistant/components/dsmr/__init__.py
+++ b/homeassistant/components/dsmr/__init__.py
@@ -1,6 +1,7 @@
"""The dsmr component."""
import asyncio
from asyncio import CancelledError
+from contextlib import suppress
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -8,11 +9,6 @@
from .const import DATA_LISTENER, DATA_TASK, DOMAIN, PLATFORMS
-async def async_setup(hass, config: dict):
- """Set up the DSMR platform."""
- return True
-
-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up DSMR from a config entry."""
hass.data.setdefault(DOMAIN, {})
@@ -36,16 +32,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
# Cancel the reconnect task
task.cancel()
- try:
+ with suppress(CancelledError):
await task
- except CancelledError:
- pass
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py
index f0899598351f1e..5a03a80ff5295a 100644
--- a/homeassistant/components/dsmr/config_flow.py
+++ b/homeassistant/components/dsmr/config_flow.py
@@ -1,8 +1,10 @@
"""Config flow for DSMR integration."""
+from __future__ import annotations
+
import asyncio
from functools import partial
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from async_timeout import timeout
from dsmr_parser import obis_references as obis_ref
@@ -14,7 +16,7 @@
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import callback
-from .const import ( # pylint:disable=unused-import
+from .const import (
CONF_DSMR_VERSION,
CONF_SERIAL_ID,
CONF_SERIAL_ID_GAS,
@@ -133,7 +135,7 @@ def _abort_if_host_port_configured(
self,
port: str,
host: str = None,
- updates: Optional[Dict[Any, Any]] = None,
+ updates: dict[Any, Any] | None = None,
reload_on_update: bool = True,
):
"""Test if host and port are already configured."""
diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json
index c3f6aa4dea33ad..c442130bb9fa81 100644
--- a/homeassistant/components/dsmr/manifest.json
+++ b/homeassistant/components/dsmr/manifest.json
@@ -2,7 +2,7 @@
"domain": "dsmr",
"name": "DSMR Slimme Meter",
"documentation": "https://www.home-assistant.io/integrations/dsmr",
- "requirements": ["dsmr_parser==0.25"],
+ "requirements": ["dsmr_parser==0.28"],
"codeowners": ["@Robbie1221"],
"config_flow": false
}
diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py
index 78cd317bb3eb2c..d17c3b780e42de 100644
--- a/homeassistant/components/dsmr/sensor.py
+++ b/homeassistant/components/dsmr/sensor.py
@@ -1,17 +1,19 @@
"""Support for Dutch Smart Meter (also known as Smartmeter or P1 port)."""
+from __future__ import annotations
+
import asyncio
from asyncio import CancelledError
+from contextlib import suppress
from datetime import timedelta
from functools import partial
import logging
-from typing import Dict
from dsmr_parser import obis_references as obis_ref
from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader
import serial
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_HOST,
@@ -21,7 +23,6 @@
)
from homeassistant.core import CoreState, callback
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import Throttle
@@ -80,35 +81,59 @@ async def async_setup_entry(
dsmr_version = config[CONF_DSMR_VERSION]
- # Define list of name,obis mappings to generate entities
+ # Define list of name,obis,force_update mappings to generate entities
obis_mapping = [
- ["Power Consumption", obis_ref.CURRENT_ELECTRICITY_USAGE],
- ["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY],
- ["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF],
- ["Energy Consumption (tarif 1)", obis_ref.ELECTRICITY_USED_TARIFF_1],
- ["Energy Consumption (tarif 2)", obis_ref.ELECTRICITY_USED_TARIFF_2],
- ["Energy Production (tarif 1)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1],
- ["Energy Production (tarif 2)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_2],
- ["Power Consumption Phase L1", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE],
- ["Power Consumption Phase L2", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE],
- ["Power Consumption Phase L3", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE],
- ["Power Production Phase L1", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE],
- ["Power Production Phase L2", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE],
- ["Power Production Phase L3", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE],
- ["Short Power Failure Count", obis_ref.SHORT_POWER_FAILURE_COUNT],
- ["Long Power Failure Count", obis_ref.LONG_POWER_FAILURE_COUNT],
- ["Voltage Sags Phase L1", obis_ref.VOLTAGE_SAG_L1_COUNT],
- ["Voltage Sags Phase L2", obis_ref.VOLTAGE_SAG_L2_COUNT],
- ["Voltage Sags Phase L3", obis_ref.VOLTAGE_SAG_L3_COUNT],
- ["Voltage Swells Phase L1", obis_ref.VOLTAGE_SWELL_L1_COUNT],
- ["Voltage Swells Phase L2", obis_ref.VOLTAGE_SWELL_L2_COUNT],
- ["Voltage Swells Phase L3", obis_ref.VOLTAGE_SWELL_L3_COUNT],
- ["Voltage Phase L1", obis_ref.INSTANTANEOUS_VOLTAGE_L1],
- ["Voltage Phase L2", obis_ref.INSTANTANEOUS_VOLTAGE_L2],
- ["Voltage Phase L3", obis_ref.INSTANTANEOUS_VOLTAGE_L3],
- ["Current Phase L1", obis_ref.INSTANTANEOUS_CURRENT_L1],
- ["Current Phase L2", obis_ref.INSTANTANEOUS_CURRENT_L2],
- ["Current Phase L3", obis_ref.INSTANTANEOUS_CURRENT_L3],
+ ["Power Consumption", obis_ref.CURRENT_ELECTRICITY_USAGE, True],
+ ["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY, True],
+ ["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF, False],
+ ["Energy Consumption (tarif 1)", obis_ref.ELECTRICITY_USED_TARIFF_1, True],
+ ["Energy Consumption (tarif 2)", obis_ref.ELECTRICITY_USED_TARIFF_2, True],
+ ["Energy Production (tarif 1)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1, True],
+ ["Energy Production (tarif 2)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_2, True],
+ [
+ "Power Consumption Phase L1",
+ obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE,
+ False,
+ ],
+ [
+ "Power Consumption Phase L2",
+ obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE,
+ False,
+ ],
+ [
+ "Power Consumption Phase L3",
+ obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE,
+ False,
+ ],
+ [
+ "Power Production Phase L1",
+ obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE,
+ False,
+ ],
+ [
+ "Power Production Phase L2",
+ obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE,
+ False,
+ ],
+ [
+ "Power Production Phase L3",
+ obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE,
+ False,
+ ],
+ ["Short Power Failure Count", obis_ref.SHORT_POWER_FAILURE_COUNT, False],
+ ["Long Power Failure Count", obis_ref.LONG_POWER_FAILURE_COUNT, False],
+ ["Voltage Sags Phase L1", obis_ref.VOLTAGE_SAG_L1_COUNT, False],
+ ["Voltage Sags Phase L2", obis_ref.VOLTAGE_SAG_L2_COUNT, False],
+ ["Voltage Sags Phase L3", obis_ref.VOLTAGE_SAG_L3_COUNT, False],
+ ["Voltage Swells Phase L1", obis_ref.VOLTAGE_SWELL_L1_COUNT, False],
+ ["Voltage Swells Phase L2", obis_ref.VOLTAGE_SWELL_L2_COUNT, False],
+ ["Voltage Swells Phase L3", obis_ref.VOLTAGE_SWELL_L3_COUNT, False],
+ ["Voltage Phase L1", obis_ref.INSTANTANEOUS_VOLTAGE_L1, False],
+ ["Voltage Phase L2", obis_ref.INSTANTANEOUS_VOLTAGE_L2, False],
+ ["Voltage Phase L3", obis_ref.INSTANTANEOUS_VOLTAGE_L3, False],
+ ["Current Phase L1", obis_ref.INSTANTANEOUS_CURRENT_L1, False],
+ ["Current Phase L2", obis_ref.INSTANTANEOUS_CURRENT_L2, False],
+ ["Current Phase L3", obis_ref.INSTANTANEOUS_CURRENT_L3, False],
]
if dsmr_version == "5L":
@@ -117,22 +142,26 @@ async def async_setup_entry(
[
"Energy Consumption (total)",
obis_ref.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL,
+ True,
],
[
"Energy Production (total)",
obis_ref.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL,
+ True,
],
]
)
else:
obis_mapping.extend(
- [["Energy Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL]]
+ [["Energy Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL, True]]
)
# Generate device entities
devices = [
- DSMREntity(name, DEVICE_NAME_ENERGY, config[CONF_SERIAL_ID], obis, config)
- for name, obis in obis_mapping
+ DSMREntity(
+ name, DEVICE_NAME_ENERGY, config[CONF_SERIAL_ID], obis, config, force_update
+ )
+ for name, obis, force_update in obis_mapping
]
# Protocol version specific obis
@@ -152,6 +181,7 @@ async def async_setup_entry(
config[CONF_SERIAL_ID_GAS],
gas_obis,
config,
+ True,
),
DerivativeDSMREntity(
"Hourly Gas Consumption",
@@ -159,6 +189,7 @@ async def async_setup_entry(
config[CONF_SERIAL_ID_GAS],
gas_obis,
config,
+ False,
),
]
@@ -185,6 +216,7 @@ def update_entities_telegram(telegram):
config[CONF_DSMR_VERSION],
update_entities_telegram,
loop=hass.loop,
+ keep_alive_interval=60,
)
else:
reader_factory = partial(
@@ -254,10 +286,10 @@ async def connect_and_reconnect():
hass.data[DOMAIN][entry.entry_id][DATA_TASK] = task
-class DSMREntity(Entity):
+class DSMREntity(SensorEntity):
"""Entity reading values from DSMR telegram."""
- def __init__(self, name, device_name, device_serial, obis, config):
+ def __init__(self, name, device_name, device_serial, obis, config, force_update):
"""Initialize entity."""
self._name = name
self._obis = obis
@@ -266,6 +298,7 @@ def __init__(self, name, device_name, device_serial, obis, config):
self._device_name = device_name
self._device_serial = device_serial
+ self._force_update = force_update
self._unique_id = f"{device_serial}_{name}".replace(" ", "_")
@callback
@@ -310,10 +343,8 @@ def state(self):
if self._obis == obis_ref.ELECTRICITY_ACTIVE_TARIFF:
return self.translate_tariff(value, self._config[CONF_DSMR_VERSION])
- try:
+ with suppress(TypeError):
value = round(float(value), self._config[CONF_PRECISION])
- except TypeError:
- pass
if value is not None:
return value
@@ -331,7 +362,7 @@ def unique_id(self) -> str:
return self._unique_id
@property
- def device_info(self) -> Dict[str, any]:
+ def device_info(self) -> dict[str, any]:
"""Return the device information."""
return {
"identifiers": {(DOMAIN, self._device_serial)},
@@ -341,7 +372,7 @@ def device_info(self) -> Dict[str, any]:
@property
def force_update(self):
"""Force update."""
- return True
+ return self._force_update
@property
def should_poll(self):
diff --git a/homeassistant/components/dsmr/translations/de.json b/homeassistant/components/dsmr/translations/de.json
new file mode 100644
index 00000000000000..da1d200c2a2099
--- /dev/null
+++ b/homeassistant/components/dsmr/translations/de.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr/translations/fr.json b/homeassistant/components/dsmr/translations/fr.json
index ea382532a71327..d156aee8ca0aab 100644
--- a/homeassistant/components/dsmr/translations/fr.json
+++ b/homeassistant/components/dsmr/translations/fr.json
@@ -3,9 +3,23 @@
"abort": {
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
},
+ "error": {
+ "one": "Vide",
+ "other": "Vide"
+ },
"step": {
"one": "",
"other": "Autre"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "time_between_update": "Temps minimum entre les mises \u00e0 jour des entit\u00e9s"
+ },
+ "title": "Options DSMR"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr/translations/id.json b/homeassistant/components/dsmr/translations/id.json
new file mode 100644
index 00000000000000..fd8299d61eddf8
--- /dev/null
+++ b/homeassistant/components/dsmr/translations/id.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "time_between_update": "Interval minimum pembaruan entitas (dalam detik)"
+ },
+ "title": "Opsi DSMR"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr/translations/ko.json b/homeassistant/components/dsmr/translations/ko.json
index 9c8fbbe80a99cb..73837e1b4f2843 100644
--- a/homeassistant/components/dsmr/translations/ko.json
+++ b/homeassistant/components/dsmr/translations/ko.json
@@ -1,7 +1,17 @@
{
"config": {
"abort": {
- "already_configured": "\uc7a5\uce58\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "time_between_update": "\uad6c\uc131\uc694\uc18c \uc5c6\ub370\uc774\ud2b8 \uac04 \ucd5c\uc18c \uc2dc\uac04 (\ucd08)"
+ },
+ "title": "DSMR \uc635\uc158"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr/translations/tr.json b/homeassistant/components/dsmr/translations/tr.json
index 94c31d0e1563d5..0857160dc51b34 100644
--- a/homeassistant/components/dsmr/translations/tr.json
+++ b/homeassistant/components/dsmr/translations/tr.json
@@ -1,4 +1,9 @@
{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ }
+ },
"options": {
"step": {
"init": {
diff --git a/homeassistant/components/dsmr/translations/uk.json b/homeassistant/components/dsmr/translations/uk.json
new file mode 100644
index 00000000000000..9bca6b00c74eda
--- /dev/null
+++ b/homeassistant/components/dsmr/translations/uk.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "time_between_update": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
+ },
+ "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 DSMR"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py
index 309f0d297ec55f..daf6b9eb950a82 100644
--- a/homeassistant/components/dsmr_reader/definitions.py
+++ b/homeassistant/components/dsmr_reader/definitions.py
@@ -325,4 +325,160 @@ def tariff_transform(value):
"enable_default": True,
"icon": "mdi:flash",
},
+ "dsmr/current-month/electricity1": {
+ "name": "Current month low tariff usage",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
+ "unit": ENERGY_KILO_WATT_HOUR,
+ },
+ "dsmr/current-month/electricity2": {
+ "name": "Current month high tariff usage",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
+ "unit": ENERGY_KILO_WATT_HOUR,
+ },
+ "dsmr/current-month/electricity1_returned": {
+ "name": "Current month low tariff returned",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
+ "unit": ENERGY_KILO_WATT_HOUR,
+ },
+ "dsmr/current-month/electricity2_returned": {
+ "name": "Current month high tariff returned",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
+ "unit": ENERGY_KILO_WATT_HOUR,
+ },
+ "dsmr/current-month/electricity_merged": {
+ "name": "Current month power usage total",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
+ "unit": ENERGY_KILO_WATT_HOUR,
+ },
+ "dsmr/current-month/electricity_returned_merged": {
+ "name": "Current month power return total",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
+ "unit": ENERGY_KILO_WATT_HOUR,
+ },
+ "dsmr/current-month/electricity1_cost": {
+ "name": "Current month low tariff cost",
+ "enable_default": True,
+ "icon": "mdi:currency-eur",
+ "unit": CURRENCY_EURO,
+ },
+ "dsmr/current-month/electricity2_cost": {
+ "name": "Current month high tariff cost",
+ "enable_default": True,
+ "icon": "mdi:currency-eur",
+ "unit": CURRENCY_EURO,
+ },
+ "dsmr/current-month/electricity_cost_merged": {
+ "name": "Current month power total cost",
+ "enable_default": True,
+ "icon": "mdi:currency-eur",
+ "unit": CURRENCY_EURO,
+ },
+ "dsmr/current-month/gas": {
+ "name": "Current month gas usage",
+ "enable_default": True,
+ "icon": "mdi:counter",
+ "unit": VOLUME_CUBIC_METERS,
+ },
+ "dsmr/current-month/gas_cost": {
+ "name": "Current month gas cost",
+ "enable_default": True,
+ "icon": "mdi:currency-eur",
+ "unit": CURRENCY_EURO,
+ },
+ "dsmr/current-month/fixed_cost": {
+ "name": "Current month fixed cost",
+ "enable_default": True,
+ "icon": "mdi:currency-eur",
+ "unit": CURRENCY_EURO,
+ },
+ "dsmr/current-month/total_cost": {
+ "name": "Current month total cost",
+ "enable_default": True,
+ "icon": "mdi:currency-eur",
+ "unit": CURRENCY_EURO,
+ },
+ "dsmr/current-year/electricity1": {
+ "name": "Current year low tariff usage",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
+ "unit": ENERGY_KILO_WATT_HOUR,
+ },
+ "dsmr/current-year/electricity2": {
+ "name": "Current year high tariff usage",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
+ "unit": ENERGY_KILO_WATT_HOUR,
+ },
+ "dsmr/current-year/electricity1_returned": {
+ "name": "Current year low tariff returned",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
+ "unit": ENERGY_KILO_WATT_HOUR,
+ },
+ "dsmr/current-year/electricity2_returned": {
+ "name": "Current year high tariff usage",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
+ "unit": ENERGY_KILO_WATT_HOUR,
+ },
+ "dsmr/current-year/electricity_merged": {
+ "name": "Current year power usage total",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
+ "unit": ENERGY_KILO_WATT_HOUR,
+ },
+ "dsmr/current-year/electricity_returned_merged": {
+ "name": "Current year power returned total",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
+ "unit": ENERGY_KILO_WATT_HOUR,
+ },
+ "dsmr/current-year/electricity1_cost": {
+ "name": "Current year low tariff cost",
+ "enable_default": True,
+ "icon": "mdi:currency-eur",
+ "unit": CURRENCY_EURO,
+ },
+ "dsmr/current-year/electricity2_cost": {
+ "name": "Current year high tariff cost",
+ "enable_default": True,
+ "icon": "mdi:currency-eur",
+ "unit": CURRENCY_EURO,
+ },
+ "dsmr/current-year/electricity_cost_merged": {
+ "name": "Current year power total cost",
+ "enable_default": True,
+ "icon": "mdi:currency-eur",
+ "unit": CURRENCY_EURO,
+ },
+ "dsmr/current-year/gas": {
+ "name": "Current year gas usage",
+ "enable_default": True,
+ "icon": "mdi:counter",
+ "unit": VOLUME_CUBIC_METERS,
+ },
+ "dsmr/current-year/gas_cost": {
+ "name": "Current year gas cost",
+ "enable_default": True,
+ "icon": "mdi:currency-eur",
+ "unit": CURRENCY_EURO,
+ },
+ "dsmr/current-year/fixed_cost": {
+ "name": "Current year fixed cost",
+ "enable_default": True,
+ "icon": "mdi:currency-eur",
+ "unit": CURRENCY_EURO,
+ },
+ "dsmr/current-year/total_cost": {
+ "name": "Current year total cost",
+ "enable_default": True,
+ "icon": "mdi:currency-eur",
+ "unit": CURRENCY_EURO,
+ },
}
diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py
index 14234b49dbe585..0ee5932c1bbd17 100644
--- a/homeassistant/components/dsmr_reader/sensor.py
+++ b/homeassistant/components/dsmr_reader/sensor.py
@@ -1,7 +1,7 @@
"""Support for DSMR Reader through MQTT."""
from homeassistant.components import mqtt
+from homeassistant.components.sensor import SensorEntity
from homeassistant.core import callback
-from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify
from .definitions import DEFINITIONS
@@ -19,7 +19,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(sensors)
-class DSMRSensor(Entity):
+class DSMRSensor(SensorEntity):
"""Representation of a DSMR sensor that is updated via MQTT."""
def __init__(self, topic):
diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py
index efd00b3da1ef89..27475990de024d 100644
--- a/homeassistant/components/dte_energy_bridge/sensor.py
+++ b/homeassistant/components/dte_energy_bridge/sensor.py
@@ -4,10 +4,9 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, HTTP_OK
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -39,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([DteEnergyBridgeSensor(ip_address, name, version)], True)
-class DteEnergyBridgeSensor(Entity):
+class DteEnergyBridgeSensor(SensorEntity):
"""Implementation of the DTE Energy Bridge sensors."""
def __init__(self, ip_address, name, version):
diff --git a/homeassistant/components/dublin_bus_transport/sensor.py b/homeassistant/components/dublin_bus_transport/sensor.py
index 96e485a94e6721..dbe1d10b553600 100644
--- a/homeassistant/components/dublin_bus_transport/sensor.py
+++ b/homeassistant/components/dublin_bus_transport/sensor.py
@@ -4,15 +4,15 @@
For more info on the API see :
https://data.gov.ie/dataset/real-time-passenger-information-rtpi-for-dublin-bus-bus-eireann-luas-and-irish-rail/resource/4b9f2c4f-6bf5-4958-a43a-f12dab04cf61
"""
+from contextlib import suppress
from datetime import datetime, timedelta
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, HTTP_OK, TIME_MINUTES
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
_RESOURCE = "https://data.dublinked.ie/cgi-bin/rtpi/realtimebusinformation"
@@ -65,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([DublinPublicTransportSensor(data, stop, route, name)], True)
-class DublinPublicTransportSensor(Entity):
+class DublinPublicTransportSensor(SensorEntity):
"""Implementation of an Dublin public transport sensor."""
def __init__(self, data, stop, route, name):
@@ -87,7 +87,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._times is not None:
next_up = "None"
@@ -118,10 +118,8 @@ def update(self):
"""Get the latest data from opendata.ch and update the states."""
self.data.update()
self._times = self.data.info
- try:
+ with suppress(TypeError):
self._state = self._times[0][ATTR_DUE_IN]
- except TypeError:
- pass
class PublicTransportData:
diff --git a/homeassistant/components/duckdns/__init__.py b/homeassistant/components/duckdns/__init__.py
index 3e08d0e7b349e5..76353415d4f5f3 100644
--- a/homeassistant/components/duckdns/__init__.py
+++ b/homeassistant/components/duckdns/__init__.py
@@ -103,7 +103,7 @@ async def _update_duckdns(session, domain, token, *, txt=_SENTINEL, clear=False)
def async_track_time_interval_backoff(hass, action, intervals) -> CALLBACK_TYPE:
"""Add a listener that fires repetitively at every timedelta interval."""
if not iscoroutinefunction:
- _LOGGER.error("action needs to be a coroutine and return True/False")
+ _LOGGER.error("Action needs to be a coroutine and return True/False")
return
if not isinstance(intervals, (list, tuple)):
diff --git a/homeassistant/components/dunehd/__init__.py b/homeassistant/components/dunehd/__init__.py
index a1fa456aa093c2..10c66c3bfb06f8 100644
--- a/homeassistant/components/dunehd/__init__.py
+++ b/homeassistant/components/dunehd/__init__.py
@@ -10,11 +10,6 @@
PLATFORMS = ["media_player"]
-async def async_setup(hass, config):
- """Set up the Dune HD component."""
- return True
-
-
async def async_setup_entry(hass, config_entry):
"""Set up a config entry."""
host = config_entry.data[CONF_HOST]
@@ -24,9 +19,9 @@ async def async_setup_entry(hass, config_entry):
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = player
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return True
@@ -37,8 +32,8 @@ async def async_unload_entry(hass, config_entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/dunehd/config_flow.py b/homeassistant/components/dunehd/config_flow.py
index 1f0efa668f9784..998ff408f3668d 100644
--- a/homeassistant/components/dunehd/config_flow.py
+++ b/homeassistant/components/dunehd/config_flow.py
@@ -9,7 +9,7 @@
from homeassistant import config_entries, exceptions
from homeassistant.const import CONF_HOST
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/dunehd/translations/de.json b/homeassistant/components/dunehd/translations/de.json
index 57856b68421a79..aa87de530b8115 100644
--- a/homeassistant/components/dunehd/translations/de.json
+++ b/homeassistant/components/dunehd/translations/de.json
@@ -13,6 +13,7 @@
"data": {
"host": "Host"
},
+ "description": "Richte die Dune HD-Integration ein. Wenn du Probleme mit der Konfiguration hast, gehe zu: https://www.home-assistant.io/integrations/dunehd \n\nStelle sicher, dass dein Player eingeschaltet ist.",
"title": "Dune HD"
}
}
diff --git a/homeassistant/components/dunehd/translations/hu.json b/homeassistant/components/dunehd/translations/hu.json
index 44b4442dc31c9e..cf0b593d5460d0 100644
--- a/homeassistant/components/dunehd/translations/hu.json
+++ b/homeassistant/components/dunehd/translations/hu.json
@@ -4,7 +4,17 @@
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
},
"error": {
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt"
+ },
+ "title": "Dune HD"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/dunehd/translations/id.json b/homeassistant/components/dunehd/translations/id.json
new file mode 100644
index 00000000000000..25cb96bedea610
--- /dev/null
+++ b/homeassistant/components/dunehd/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung",
+ "invalid_host": "Nama host atau alamat IP tidak valid"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Siapkan integrasi Dune HD. Jika Anda memiliki masalah dengan konfigurasi, buka: https://www.home-assistant.io/integrations/dunehd \n\nPastikan pemutar Anda dinyalakan.",
+ "title": "Dune HD"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dunehd/translations/ko.json b/homeassistant/components/dunehd/translations/ko.json
index 1ddcadf8350341..5c7feb27f7eda3 100644
--- a/homeassistant/components/dunehd/translations/ko.json
+++ b/homeassistant/components/dunehd/translations/ko.json
@@ -6,7 +6,7 @@
"error": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
- "invalid_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "invalid_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
diff --git a/homeassistant/components/dunehd/translations/nl.json b/homeassistant/components/dunehd/translations/nl.json
index c8e16770db2407..bb3dd7def476e3 100644
--- a/homeassistant/components/dunehd/translations/nl.json
+++ b/homeassistant/components/dunehd/translations/nl.json
@@ -12,7 +12,9 @@
"user": {
"data": {
"host": "Host"
- }
+ },
+ "description": "Stel Dune HD integratie in. Als u problemen heeft met de configuratie ga dan naar: https://www.home-assistant.io/integrations/dunehd \n\nZorg ervoor dat uw speler is ingeschakeld.",
+ "title": "Dune HD"
}
}
}
diff --git a/homeassistant/components/dunehd/translations/tr.json b/homeassistant/components/dunehd/translations/tr.json
new file mode 100644
index 00000000000000..0f8c17228fdd1f
--- /dev/null
+++ b/homeassistant/components/dunehd/translations/tr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ },
+ "title": "Dune HD"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dunehd/translations/uk.json b/homeassistant/components/dunehd/translations/uk.json
new file mode 100644
index 00000000000000..d2f4eadbdcb7f9
--- /dev/null
+++ b/homeassistant/components/dunehd/translations/uk.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Dune HD. \u042f\u043a\u0449\u043e \u0443 \u0412\u0430\u0441 \u0432\u0438\u043d\u0438\u043a\u043b\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0437 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438 \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e: https://www.home-assistant.io/integrations/dunehd \n\n \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0412\u0430\u0448 \u043f\u043b\u0435\u0454\u0440 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439.",
+ "title": "Dune HD"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py
index 79beebb005d121..78fa9bd85522d7 100644
--- a/homeassistant/components/dwd_weather_warnings/sensor.py
+++ b/homeassistant/components/dwd_weather_warnings/sensor.py
@@ -15,10 +15,9 @@
from dwdwfsapi import DwdWeatherWarningsAPI
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -87,7 +86,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class DwdWeatherWarningsSensor(Entity):
+class DwdWeatherWarningsSensor(SensorEntity):
"""Representation of a DWD-Weather-Warnings sensor."""
def __init__(self, api, name, sensor_type):
@@ -119,7 +118,7 @@ def state(self):
return self._api.api.expected_warning_level
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the DWD-Weather-Warnings."""
data = {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/dweet/sensor.py b/homeassistant/components/dweet/sensor.py
index f3f604ff3692bf..f1243cd540771b 100644
--- a/homeassistant/components/dweet/sensor.py
+++ b/homeassistant/components/dweet/sensor.py
@@ -6,7 +6,7 @@
import dweepy
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_DEVICE,
CONF_NAME,
@@ -14,7 +14,6 @@
CONF_VALUE_TEMPLATE,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -56,7 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([DweetSensor(hass, dweet, name, value_template, unit)], True)
-class DweetSensor(Entity):
+class DweetSensor(SensorEntity):
"""Representation of a Dweet sensor."""
def __init__(self, hass, dweet, name, value_template, unit_of_measurement):
diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py
index c131ebec3dadd4..92392e4b51a89d 100644
--- a/homeassistant/components/dynalite/__init__.py
+++ b/homeassistant/components/dynalite/__init__.py
@@ -1,14 +1,15 @@
"""Support for the Dynalite networks."""
+from __future__ import annotations
import asyncio
-from typing import Any, Dict, Union
+from typing import Any
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.cover import DEVICE_CLASSES_SCHEMA
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE
+from homeassistant.const import CONF_DEFAULT, CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
@@ -29,7 +30,6 @@
CONF_CHANNEL,
CONF_CHANNEL_COVER,
CONF_CLOSE_PRESET,
- CONF_DEFAULT,
CONF_DEVICE_CLASS,
CONF_DURATION,
CONF_FADE,
@@ -48,14 +48,14 @@
DEFAULT_PORT,
DEFAULT_TEMPLATES,
DOMAIN,
- ENTITY_PLATFORMS,
LOGGER,
+ PLATFORMS,
SERVICE_REQUEST_AREA_PRESET,
SERVICE_REQUEST_CHANNEL_LEVEL,
)
-def num_string(value: Union[int, str]) -> str:
+def num_string(value: int | str) -> str:
"""Test if value is a string of digits, aka an integer."""
new_value = str(value)
if new_value.isdigit():
@@ -106,7 +106,7 @@ def num_string(value: Union[int, str]) -> str:
TEMPLATE_SCHEMA = vol.Schema({str: TEMPLATE_DATA_SCHEMA})
-def validate_area(config: Dict[str, Any]) -> Dict[str, Any]:
+def validate_area(config: dict[str, Any]) -> dict[str, Any]:
"""Validate that template parameters are only used if area is using the relevant template."""
conf_set = set()
for template in DEFAULT_TEMPLATES:
@@ -179,9 +179,8 @@ def validate_area(config: Dict[str, Any]) -> Dict[str, Any]:
)
-async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
+async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up the Dynalite platform."""
-
conf = config.get(DOMAIN)
LOGGER.debug("Setting up dynalite component config = %s", conf)
@@ -269,14 +268,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# need to do it before the listener
hass.data[DOMAIN][entry.entry_id] = bridge
entry.add_update_listener(async_entry_changed)
+
if not await bridge.async_setup():
LOGGER.error("Could not set up bridge for entry %s", entry.data)
hass.data[DOMAIN][entry.entry_id] = None
raise ConfigEntryNotReady
- for platform in ENTITY_PLATFORMS:
+
+ for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)
+
return True
@@ -286,7 +288,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN].pop(entry.entry_id)
tasks = [
hass.config_entries.async_forward_entry_unload(entry, platform)
- for platform in ENTITY_PLATFORMS
+ for platform in PLATFORMS
]
results = await asyncio.gather(*tasks)
return False not in results
diff --git a/homeassistant/components/dynalite/bridge.py b/homeassistant/components/dynalite/bridge.py
index 5bf21801b3d1db..71cecee8d43011 100644
--- a/homeassistant/components/dynalite/bridge.py
+++ b/homeassistant/components/dynalite/bridge.py
@@ -1,6 +1,7 @@
"""Code to handle a Dynalite bridge."""
+from __future__ import annotations
-from typing import Any, Callable, Dict, List, Optional
+from typing import Any, Callable
from dynalite_devices_lib.dynalite_devices import (
CONF_AREA as dyn_CONF_AREA,
@@ -16,21 +17,14 @@
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
-from .const import (
- ATTR_AREA,
- ATTR_HOST,
- ATTR_PACKET,
- ATTR_PRESET,
- ENTITY_PLATFORMS,
- LOGGER,
-)
+from .const import ATTR_AREA, ATTR_HOST, ATTR_PACKET, ATTR_PRESET, LOGGER, PLATFORMS
from .convert_config import convert_config
class DynaliteBridge:
"""Manages a single Dynalite bridge."""
- def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
+ def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None:
"""Initialize the system based on host parameter."""
self.hass = hass
self.area = {}
@@ -51,12 +45,12 @@ async def async_setup(self) -> bool:
LOGGER.debug("Setting up bridge - host %s", self.host)
return await self.dynalite_devices.async_setup()
- def reload_config(self, config: Dict[str, Any]) -> None:
+ def reload_config(self, config: dict[str, Any]) -> None:
"""Reconfigure a bridge when config changes."""
LOGGER.debug("Reloading bridge - host %s, config %s", self.host, config)
self.dynalite_devices.configure(convert_config(config))
- def update_signal(self, device: Optional[DynaliteBaseDevice] = None) -> str:
+ def update_signal(self, device: DynaliteBaseDevice | None = None) -> str:
"""Create signal to use to trigger entity update."""
if device:
signal = f"dynalite-update-{self.host}-{device.unique_id}"
@@ -65,7 +59,7 @@ def update_signal(self, device: Optional[DynaliteBaseDevice] = None) -> str:
return signal
@callback
- def update_device(self, device: Optional[DynaliteBaseDevice] = None) -> None:
+ def update_device(self, device: DynaliteBaseDevice | None = None) -> None:
"""Call when a device or all devices should be updated."""
if not device:
# This is used to signal connection or disconnection, so all devices may become available or not.
@@ -105,9 +99,9 @@ def register_add_devices(self, platform: str, async_add_devices: Callable) -> No
if platform in self.waiting_devices:
self.async_add_devices[platform](self.waiting_devices[platform])
- def add_devices_when_registered(self, devices: List[DynaliteBaseDevice]) -> None:
+ def add_devices_when_registered(self, devices: list[DynaliteBaseDevice]) -> None:
"""Add the devices to HA if the add devices callback was registered, otherwise queue until it is."""
- for platform in ENTITY_PLATFORMS:
+ for platform in PLATFORMS:
platform_devices = [
device for device in devices if device.category == platform
]
diff --git a/homeassistant/components/dynalite/config_flow.py b/homeassistant/components/dynalite/config_flow.py
index 4c5b2ceb7d88a4..2bd2b142252dbf 100644
--- a/homeassistant/components/dynalite/config_flow.py
+++ b/homeassistant/components/dynalite/config_flow.py
@@ -1,5 +1,7 @@
"""Config flow to configure Dynalite hub."""
-from typing import Any, Dict
+from __future__ import annotations
+
+from typing import Any
from homeassistant import config_entries
from homeassistant.const import CONF_HOST
@@ -18,7 +20,7 @@ def __init__(self) -> None:
"""Initialize the Dynalite flow."""
self.host = None
- async def async_step_import(self, import_info: Dict[str, Any]) -> Any:
+ async def async_step_import(self, import_info: dict[str, Any]) -> Any:
"""Import a new bridge as a config entry."""
LOGGER.debug("Starting async_step_import - %s", import_info)
host = import_info[CONF_HOST]
diff --git a/homeassistant/components/dynalite/const.py b/homeassistant/components/dynalite/const.py
index 4159c98f073283..eda43305461d70 100644
--- a/homeassistant/components/dynalite/const.py
+++ b/homeassistant/components/dynalite/const.py
@@ -6,7 +6,7 @@
LOGGER = logging.getLogger(__package__)
DOMAIN = "dynalite"
-ENTITY_PLATFORMS = ["light", "switch", "cover"]
+PLATFORMS = ["light", "switch", "cover"]
CONF_ACTIVE = "active"
@@ -19,7 +19,6 @@
CONF_CHANNEL = "channel"
CONF_CHANNEL_COVER = "channel_cover"
CONF_CLOSE_PRESET = "close"
-CONF_DEFAULT = "default"
CONF_DEVICE_CLASS = "class"
CONF_DURATION = "duration"
CONF_FADE = "fade"
diff --git a/homeassistant/components/dynalite/convert_config.py b/homeassistant/components/dynalite/convert_config.py
index b84450c807dd9c..89a7f32b47ab07 100644
--- a/homeassistant/components/dynalite/convert_config.py
+++ b/homeassistant/components/dynalite/convert_config.py
@@ -1,10 +1,18 @@
"""Convert the HA config to the dynalite config."""
+from __future__ import annotations
-from typing import Any, Dict
+from typing import Any
from dynalite_devices_lib import const as dyn_const
-from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_ROOM, CONF_TYPE
+from homeassistant.const import (
+ CONF_DEFAULT,
+ CONF_HOST,
+ CONF_NAME,
+ CONF_PORT,
+ CONF_ROOM,
+ CONF_TYPE,
+)
from .const import (
ACTIVE_INIT,
@@ -16,7 +24,6 @@
CONF_CHANNEL,
CONF_CHANNEL_COVER,
CONF_CLOSE_PRESET,
- CONF_DEFAULT,
CONF_DEVICE_CLASS,
CONF_DURATION,
CONF_FADE,
@@ -56,7 +63,7 @@ def convert_with_map(config, conf_map):
return result
-def convert_channel(config: Dict[str, Any]) -> Dict[str, Any]:
+def convert_channel(config: dict[str, Any]) -> dict[str, Any]:
"""Convert the config for a channel."""
my_map = {
CONF_NAME: dyn_const.CONF_NAME,
@@ -66,7 +73,7 @@ def convert_channel(config: Dict[str, Any]) -> Dict[str, Any]:
return convert_with_map(config, my_map)
-def convert_preset(config: Dict[str, Any]) -> Dict[str, Any]:
+def convert_preset(config: dict[str, Any]) -> dict[str, Any]:
"""Convert the config for a preset."""
my_map = {
CONF_NAME: dyn_const.CONF_NAME,
@@ -76,7 +83,7 @@ def convert_preset(config: Dict[str, Any]) -> Dict[str, Any]:
return convert_with_map(config, my_map)
-def convert_area(config: Dict[str, Any]) -> Dict[str, Any]:
+def convert_area(config: dict[str, Any]) -> dict[str, Any]:
"""Convert the config for an area."""
my_map = {
CONF_NAME: dyn_const.CONF_NAME,
@@ -108,12 +115,12 @@ def convert_area(config: Dict[str, Any]) -> Dict[str, Any]:
return result
-def convert_default(config: Dict[str, Any]) -> Dict[str, Any]:
+def convert_default(config: dict[str, Any]) -> dict[str, Any]:
"""Convert the config for the platform defaults."""
return convert_with_map(config, {CONF_FADE: dyn_const.CONF_FADE})
-def convert_template(config: Dict[str, Any]) -> Dict[str, Any]:
+def convert_template(config: dict[str, Any]) -> dict[str, Any]:
"""Convert the config for a template."""
my_map = {
CONF_ROOM_ON: dyn_const.CONF_ROOM_ON,
@@ -129,7 +136,7 @@ def convert_template(config: Dict[str, Any]) -> Dict[str, Any]:
return convert_with_map(config, my_map)
-def convert_config(config: Dict[str, Any]) -> Dict[str, Any]:
+def convert_config(config: dict[str, Any]) -> dict[str, Any]:
"""Convert a config dict by replacing component consts with library consts."""
my_map = {
CONF_NAME: dyn_const.CONF_NAME,
diff --git a/homeassistant/components/dynalite/dynalitebase.py b/homeassistant/components/dynalite/dynalitebase.py
index 31879c5c118f6b..2cc28002a2c145 100644
--- a/homeassistant/components/dynalite/dynalitebase.py
+++ b/homeassistant/components/dynalite/dynalitebase.py
@@ -1,5 +1,7 @@
"""Support for the Dynalite devices as entities."""
-from typing import Any, Callable, Dict
+from __future__ import annotations
+
+from typing import Any, Callable
from homeassistant.components.dynalite.bridge import DynaliteBridge
from homeassistant.config_entries import ConfigEntry
@@ -58,7 +60,7 @@ def available(self) -> bool:
return self._device.available
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Device info for this entity."""
return {
"identifiers": {(DOMAIN, self._device.unique_id)},
diff --git a/homeassistant/components/dynalite/light.py b/homeassistant/components/dynalite/light.py
index 5e7069ab50b7e8..bb9569358be42e 100644
--- a/homeassistant/components/dynalite/light.py
+++ b/homeassistant/components/dynalite/light.py
@@ -12,7 +12,6 @@ async def async_setup_entry(
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable
) -> None:
"""Record the async_add_entities function to add them later when received from Dynalite."""
-
async_setup_entry_base(
hass, config_entry, async_add_entities, "light", DynaliteLight
)
diff --git a/homeassistant/components/dynalite/switch.py b/homeassistant/components/dynalite/switch.py
index d106d976d689d9..a482228183ccb8 100644
--- a/homeassistant/components/dynalite/switch.py
+++ b/homeassistant/components/dynalite/switch.py
@@ -12,7 +12,6 @@ async def async_setup_entry(
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable
) -> None:
"""Record the async_add_entities function to add them later when received from Dynalite."""
-
async_setup_entry_base(
hass, config_entry, async_add_entities, "switch", DynaliteSwitch
)
diff --git a/homeassistant/components/dyson/__init__.py b/homeassistant/components/dyson/__init__.py
index b39af2a2fd1599..d8023b42973265 100644
--- a/homeassistant/components/dyson/__init__.py
+++ b/homeassistant/components/dyson/__init__.py
@@ -17,7 +17,7 @@
DEFAULT_TIMEOUT = 5
DEFAULT_RETRY = 10
DYSON_DEVICES = "dyson_devices"
-DYSON_PLATFORMS = ["sensor", "fan", "vacuum", "climate", "air_quality"]
+PLATFORMS = ["sensor", "fan", "vacuum", "climate", "air_quality"]
DOMAIN = "dyson"
@@ -105,7 +105,7 @@ def setup(hass, config):
# Start fan/sensors components
if hass.data[DYSON_DEVICES]:
_LOGGER.debug("Starting sensor/fan components")
- for platform in DYSON_PLATFORMS:
+ for platform in PLATFORMS:
discovery.load_platform(hass, platform, DOMAIN, {}, config)
return True
diff --git a/homeassistant/components/dyson/air_quality.py b/homeassistant/components/dyson/air_quality.py
index d23b2b1ef883c9..48b66fe7683586 100644
--- a/homeassistant/components/dyson/air_quality.py
+++ b/homeassistant/components/dyson/air_quality.py
@@ -1,6 +1,4 @@
"""Support for Dyson Pure Cool Air Quality Sensors."""
-import logging
-
from libpurecool.dyson_pure_cool import DysonPureCool
from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State
@@ -10,8 +8,6 @@
ATTRIBUTION = "Dyson purifier air quality sensor"
-_LOGGER = logging.getLogger(__name__)
-
DYSON_AIQ_DEVICES = "dyson_aiq_devices"
ATTR_VOC = "volatile_organic_compounds"
@@ -92,6 +88,6 @@ def volatile_organic_compounds(self):
return int(self._device.environmental_state.volatile_organic_compounds)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
return {ATTR_VOC: self.volatile_organic_compounds}
diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py
index 7a57a75523ea98..e646babb944c32 100644
--- a/homeassistant/components/dyson/fan.py
+++ b/homeassistant/components/dyson/fan.py
@@ -1,6 +1,8 @@
"""Support for Dyson Pure Cool link fan."""
+from __future__ import annotations
+
import logging
-from typing import Optional
+import math
from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation
from libpurecool.dyson_pure_cool import DysonPureCool
@@ -9,15 +11,13 @@
from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State
import voluptuous as vol
-from homeassistant.components.fan import (
- SPEED_HIGH,
- SPEED_LOW,
- SPEED_MEDIUM,
- SUPPORT_OSCILLATE,
- SUPPORT_SET_SPEED,
- FanEntity,
-)
+from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity
from homeassistant.helpers import config_validation as cv, entity_platform
+from homeassistant.util.percentage import (
+ int_states_in_range,
+ percentage_to_ranged_value,
+ ranged_value_to_percentage,
+)
from . import DYSON_DEVICES, DysonEntity
@@ -70,40 +70,30 @@
}
-SPEED_LIST_HA = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
-
-SPEED_LIST_DYSON = [
- int(FanSpeed.FAN_SPEED_1.value),
- int(FanSpeed.FAN_SPEED_2.value),
- int(FanSpeed.FAN_SPEED_3.value),
- int(FanSpeed.FAN_SPEED_4.value),
- int(FanSpeed.FAN_SPEED_5.value),
- int(FanSpeed.FAN_SPEED_6.value),
- int(FanSpeed.FAN_SPEED_7.value),
- int(FanSpeed.FAN_SPEED_8.value),
- int(FanSpeed.FAN_SPEED_9.value),
- int(FanSpeed.FAN_SPEED_10.value),
+PRESET_MODE_AUTO = "auto"
+PRESET_MODES = [PRESET_MODE_AUTO]
+
+ORDERED_DYSON_SPEEDS = [
+ FanSpeed.FAN_SPEED_1,
+ FanSpeed.FAN_SPEED_2,
+ FanSpeed.FAN_SPEED_3,
+ FanSpeed.FAN_SPEED_4,
+ FanSpeed.FAN_SPEED_5,
+ FanSpeed.FAN_SPEED_6,
+ FanSpeed.FAN_SPEED_7,
+ FanSpeed.FAN_SPEED_8,
+ FanSpeed.FAN_SPEED_9,
+ FanSpeed.FAN_SPEED_10,
]
+DYSON_SPEED_TO_INT_VALUE = {k: int(k.value) for k in ORDERED_DYSON_SPEEDS}
+INT_VALUE_TO_DYSON_SPEED = {v: k for k, v in DYSON_SPEED_TO_INT_VALUE.items()}
-SPEED_DYSON_TO_HA = {
- FanSpeed.FAN_SPEED_1.value: SPEED_LOW,
- FanSpeed.FAN_SPEED_2.value: SPEED_LOW,
- FanSpeed.FAN_SPEED_3.value: SPEED_LOW,
- FanSpeed.FAN_SPEED_4.value: SPEED_LOW,
- FanSpeed.FAN_SPEED_AUTO.value: SPEED_MEDIUM,
- FanSpeed.FAN_SPEED_5.value: SPEED_MEDIUM,
- FanSpeed.FAN_SPEED_6.value: SPEED_MEDIUM,
- FanSpeed.FAN_SPEED_7.value: SPEED_MEDIUM,
- FanSpeed.FAN_SPEED_8.value: SPEED_HIGH,
- FanSpeed.FAN_SPEED_9.value: SPEED_HIGH,
- FanSpeed.FAN_SPEED_10.value: SPEED_HIGH,
-}
+SPEED_LIST_DYSON = list(DYSON_SPEED_TO_INT_VALUE.values())
-SPEED_HA_TO_DYSON = {
- SPEED_LOW: FanSpeed.FAN_SPEED_4,
- SPEED_MEDIUM: FanSpeed.FAN_SPEED_7,
- SPEED_HIGH: FanSpeed.FAN_SPEED_10,
-}
+SPEED_RANGE = (
+ SPEED_LIST_DYSON[0],
+ SPEED_LIST_DYSON[-1],
+) # off is not included
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
@@ -160,14 +150,28 @@ class DysonFanEntity(DysonEntity, FanEntity):
"""Representation of a Dyson fan."""
@property
- def speed(self):
- """Return the current speed."""
- return SPEED_DYSON_TO_HA[self._device.state.speed]
+ def percentage(self):
+ """Return the current speed percentage."""
+ if self.auto_mode:
+ return None
+ return ranged_value_to_percentage(SPEED_RANGE, int(self._device.state.speed))
+
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return int_states_in_range(SPEED_RANGE)
@property
- def speed_list(self) -> list:
- """Get the list of available speeds."""
- return SPEED_LIST_HA
+ def preset_modes(self):
+ """Return the available preset modes."""
+ return PRESET_MODES
+
+ @property
+ def preset_mode(self):
+ """Return the current preset mode."""
+ if self.auto_mode:
+ return PRESET_MODE_AUTO
+ return None
@property
def dyson_speed(self):
@@ -197,7 +201,7 @@ def supported_features(self) -> int:
return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return optional state attributes."""
return {
ATTR_NIGHT_MODE: self.night_mode,
@@ -206,12 +210,25 @@ def device_state_attributes(self) -> dict:
ATTR_DYSON_SPEED_LIST: self.dyson_speed_list,
}
- def set_speed(self, speed: str) -> None:
- """Set the speed of the fan."""
- if speed not in SPEED_LIST_HA:
- raise ValueError(f'"{speed}" is not a valid speed')
- _LOGGER.debug("Set fan speed to: %s", speed)
- self.set_dyson_speed(SPEED_HA_TO_DYSON[speed])
+ def set_auto_mode(self, auto_mode: bool) -> None:
+ """Set auto mode."""
+ raise NotImplementedError
+
+ def set_percentage(self, percentage: int) -> None:
+ """Set the speed percentage of the fan."""
+ if percentage == 0:
+ self.turn_off()
+ return
+ dyson_speed = INT_VALUE_TO_DYSON_SPEED[
+ math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
+ ]
+ self.set_dyson_speed(dyson_speed)
+
+ def set_preset_mode(self, preset_mode: str) -> None:
+ """Set a preset mode on the fan."""
+ self._valid_preset_mode_or_raise(preset_mode)
+ # There currently is only one
+ self.set_auto_mode(True)
def set_dyson_speed(self, speed: FanSpeed) -> None:
"""Set the exact speed of the fan."""
@@ -225,6 +242,23 @@ def service_set_dyson_speed(self, dyson_speed: str) -> None:
speed = FanSpeed(f"{int(dyson_speed):04d}")
self.set_dyson_speed(speed)
+ def turn_on(
+ self,
+ speed: str | None = None,
+ percentage: int | None = None,
+ preset_mode: str | None = None,
+ **kwargs,
+ ) -> None:
+ """Turn on the fan."""
+ _LOGGER.debug("Turn on fan %s with percentage %s", self.name, percentage)
+ if preset_mode:
+ self.set_preset_mode(preset_mode)
+ elif percentage is None:
+ # percentage not set, just turn on
+ self._device.set_configuration(fan_mode=FanMode.FAN)
+ else:
+ self.set_percentage(percentage)
+
class DysonPureCoolLinkEntity(DysonFanEntity):
"""Representation of a Dyson fan."""
@@ -233,28 +267,6 @@ def __init__(self, device):
"""Initialize the fan."""
super().__init__(device, DysonPureCoolState)
- #
- # The fan entity model has changed to use percentages and preset_modes
- # instead of speeds.
- #
- # Please review
- # https://developers.home-assistant.io/docs/core/entity/fan/
- #
- def turn_on(
- self,
- speed: Optional[str] = None,
- percentage: Optional[int] = None,
- preset_mode: Optional[str] = None,
- **kwargs,
- ) -> None:
- """Turn on the fan."""
- _LOGGER.debug("Turn on fan %s with speed %s", self.name, speed)
- if speed is not None:
- self.set_speed(speed)
- else:
- # Speed not set, just turn on
- self._device.set_configuration(fan_mode=FanMode.FAN)
-
def turn_off(self, **kwargs) -> None:
"""Turn off the fan."""
_LOGGER.debug("Turn off fan %s", self.name)
@@ -312,27 +324,22 @@ def __init__(self, device):
"""Initialize the fan."""
super().__init__(device, DysonPureCoolV2State)
- #
- # The fan entity model has changed to use percentages and preset_modes
- # instead of speeds.
- #
- # Please review
- # https://developers.home-assistant.io/docs/core/entity/fan/
- #
def turn_on(
self,
- speed: Optional[str] = None,
- percentage: Optional[int] = None,
- preset_mode: Optional[str] = None,
+ speed: str | None = None,
+ percentage: int | None = None,
+ preset_mode: str | None = None,
**kwargs,
) -> None:
"""Turn on the fan."""
- _LOGGER.debug("Turn on fan %s", self.name)
-
- if speed is not None:
- self.set_speed(speed)
- else:
+ _LOGGER.debug("Turn on fan %s with percentage %s", self.name, percentage)
+ if preset_mode:
+ self.set_preset_mode(preset_mode)
+ elif percentage is None:
+ # percentage not set, just turn on
self._device.turn_on()
+ else:
+ self.set_percentage(percentage)
def turn_off(self, **kwargs):
"""Turn off the fan."""
@@ -449,10 +456,10 @@ def carbon_filter(self):
return int(self._device.state.carbon_filter_state)
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return optional state attributes."""
return {
- **super().device_state_attributes,
+ **super().extra_state_attributes,
ATTR_ANGLE_LOW: self.angle_low,
ATTR_ANGLE_HIGH: self.angle_high,
ATTR_FLOW_DIRECTION_FRONT: self.flow_direction_front,
diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py
index f1198188b5ccd6..cff4b8f5501366 100644
--- a/homeassistant/components/dyson/sensor.py
+++ b/homeassistant/components/dyson/sensor.py
@@ -1,9 +1,8 @@
"""Support for Dyson Pure Cool Link Sensors."""
-import logging
-
from libpurecool.dyson_pure_cool import DysonPureCool
from libpurecool.dyson_pure_cool_link import DysonPureCoolLink
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ICON,
@@ -15,7 +14,6 @@
TEMP_CELSIUS,
TIME_HOURS,
)
-from homeassistant.helpers.entity import Entity
from . import DYSON_DEVICES, DysonEntity
@@ -58,8 +56,6 @@
DYSON_SENSOR_DEVICES = "dyson_sensor_devices"
-_LOGGER = logging.getLogger(__name__)
-
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Dyson Sensors."""
@@ -105,7 +101,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(devices)
-class DysonSensor(DysonEntity, Entity):
+class DysonSensor(DysonEntity, SensorEntity):
"""Representation of a generic Dyson sensor."""
def __init__(self, device, sensor_type):
diff --git a/homeassistant/components/dyson/services.yaml b/homeassistant/components/dyson/services.yaml
index 73f7bc7587449a..f96aa9315c1c2d 100644
--- a/homeassistant/components/dyson/services.yaml
+++ b/homeassistant/components/dyson/services.yaml
@@ -33,7 +33,7 @@ set_angle:
description: The angle at which the oscillation should end
example: 255
-flow_direction_front:
+set_flow_direction_front:
description: Set the fan flow direction.
fields:
entity_id:
diff --git a/homeassistant/components/dyson/vacuum.py b/homeassistant/components/dyson/vacuum.py
index 466b409c342ead..f4035d33cf3af5 100644
--- a/homeassistant/components/dyson/vacuum.py
+++ b/homeassistant/components/dyson/vacuum.py
@@ -95,7 +95,7 @@ def fan_speed_list(self):
return ["Quiet", "Max"]
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the specific state attributes of this vacuum cleaner."""
return {ATTR_POSITION: str(self._device.state.position)}
diff --git a/homeassistant/components/eafm/config_flow.py b/homeassistant/components/eafm/config_flow.py
index 19b10c3e6c5863..98e9158e2df855 100644
--- a/homeassistant/components/eafm/config_flow.py
+++ b/homeassistant/components/eafm/config_flow.py
@@ -5,7 +5,6 @@
from homeassistant import config_entries
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-# pylint: disable=unused-import
from .const import DOMAIN
diff --git a/homeassistant/components/eafm/sensor.py b/homeassistant/components/eafm/sensor.py
index 746f6f34abcf7d..b3d726f9cd3c47 100644
--- a/homeassistant/components/eafm/sensor.py
+++ b/homeassistant/components/eafm/sensor.py
@@ -5,6 +5,7 @@
from aioeafm import get_station
import async_timeout
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_METERS
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import (
@@ -77,7 +78,7 @@ async def async_update_data():
await coordinator.async_refresh()
-class Measurement(CoordinatorEntity):
+class Measurement(CoordinatorEntity, SensorEntity):
"""A gauge at a flood monitoring station."""
attribution = "This uses Environment Agency flood and river level data from the real-time data API"
@@ -156,7 +157,7 @@ def unit_of_measurement(self):
return UNIT_MAPPING.get(measure["unit"], measure["unitName"])
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the sensor specific state attributes."""
return {ATTR_ATTRIBUTION: self.attribution}
diff --git a/homeassistant/components/eafm/translations/de.json b/homeassistant/components/eafm/translations/de.json
new file mode 100644
index 00000000000000..da1d200c2a2099
--- /dev/null
+++ b/homeassistant/components/eafm/translations/de.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/eafm/translations/id.json b/homeassistant/components/eafm/translations/id.json
new file mode 100644
index 00000000000000..656c9eb51ea404
--- /dev/null
+++ b/homeassistant/components/eafm/translations/id.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "no_stations": "Tidak ditemukan stasiun pemantau banjir."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "station": "Stasiun"
+ },
+ "description": "Pilih stasiun yang ingin dipantau",
+ "title": "Lacak stasiun pemantauan banjir"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/eafm/translations/ko.json b/homeassistant/components/eafm/translations/ko.json
index 4e7bfc9dc9363d..5c6c0a8aa33c04 100644
--- a/homeassistant/components/eafm/translations/ko.json
+++ b/homeassistant/components/eafm/translations/ko.json
@@ -1,15 +1,16 @@
{
"config": {
"abort": {
- "no_stations": "\ud64d\uc218 \ubaa8\ub2c8\ud130\ub9c1 \uc2a4\ud14c\uc774\uc158\uc774 \uc5c6\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "no_stations": "\ud64d\uc218 \ubaa8\ub2c8\ud130\ub9c1 \uc2a4\ud14c\uc774\uc158\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
},
"step": {
"user": {
"data": {
"station": "\uc2a4\ud14c\uc774\uc158"
},
- "description": "\ubaa8\ub2c8\ud130\ub9c1\ud560 \uc2a4\ud14c\uc774\uc158 \uc120\ud0dd",
- "title": "\ud64d\uc218 \ubaa8\ub2c8\ud130\ub9c1 \uc2a4\ud14c\uc774\uc158 \ucd94\uc801"
+ "description": "\ubaa8\ub2c8\ud130\ub9c1\ud560 \uc2a4\ud14c\uc774\uc158 \uc120\ud0dd\ud558\uae30",
+ "title": "\ud64d\uc218 \ubaa8\ub2c8\ud130\ub9c1 \uc2a4\ud14c\uc774\uc158 \ucd94\uc801\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/eafm/translations/nl.json b/homeassistant/components/eafm/translations/nl.json
index 8b2702b6708dcb..ed67ed8f982c8e 100644
--- a/homeassistant/components/eafm/translations/nl.json
+++ b/homeassistant/components/eafm/translations/nl.json
@@ -1,7 +1,17 @@
{
"config": {
"abort": {
- "already_configured": "Apparaat is al geconfigureerd"
+ "already_configured": "Apparaat is al geconfigureerd",
+ "no_stations": "Geen meetstations voor overstromingen gevonden."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "station": "Station"
+ },
+ "description": "Selecteer het station dat u wilt monitoren",
+ "title": "Volg een station voor overstromingen"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/eafm/translations/tr.json b/homeassistant/components/eafm/translations/tr.json
new file mode 100644
index 00000000000000..4ed0f406e57ce6
--- /dev/null
+++ b/homeassistant/components/eafm/translations/tr.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "no_stations": "Ak\u0131\u015f izleme istasyonu bulunamad\u0131."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "station": "\u0130stasyon"
+ },
+ "title": "Ak\u0131\u015f izleme istasyonunu takip edin"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/eafm/translations/uk.json b/homeassistant/components/eafm/translations/uk.json
new file mode 100644
index 00000000000000..4f84eb92722948
--- /dev/null
+++ b/homeassistant/components/eafm/translations/uk.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "no_stations": "\u0421\u0442\u0430\u043d\u0446\u0456\u0457 \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443 \u043f\u043e\u0432\u0435\u043d\u0435\u0439 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "station": "\u0421\u0442\u0430\u043d\u0446\u0456\u044f"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0456\u044e \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443",
+ "title": "\u0421\u0442\u0430\u043d\u0446\u0456\u0457 \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443 \u043f\u043e\u0432\u0435\u043d\u0435\u0439"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py
index aa6945bac99dcf..72d169f389eabe 100644
--- a/homeassistant/components/ebox/sensor.py
+++ b/homeassistant/components/ebox/sensor.py
@@ -10,7 +10,7 @@
from pyebox.client import PyEboxError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_MONITORED_VARIABLES,
CONF_NAME,
@@ -22,7 +22,6 @@
)
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -90,7 +89,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(sensors, True)
-class EBoxSensor(Entity):
+class EBoxSensor(SensorEntity):
"""Implementation of a EBox sensor."""
def __init__(self, ebox_data, sensor_type, name):
diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py
index 00c40344d6ec28..beb8abd6289016 100644
--- a/homeassistant/components/ebusd/__init__.py
+++ b/homeassistant/components/ebusd/__init__.py
@@ -115,8 +115,7 @@ def write(self, call):
try:
_LOGGER.debug("Opening socket to ebusd %s", name)
command_result = ebusdpy.write(self._address, self._circuit, name, value)
- if command_result is not None:
- if "done" not in command_result:
- _LOGGER.warning("Write command failed: %s", name)
+ if command_result is not None and "done" not in command_result:
+ _LOGGER.warning("Write command failed: %s", name)
except RuntimeError as err:
_LOGGER.error(err)
diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py
index badb94a6f8541f..00f6a6b2b3e468 100644
--- a/homeassistant/components/ebusd/sensor.py
+++ b/homeassistant/components/ebusd/sensor.py
@@ -2,7 +2,7 @@
import datetime
import logging
-from homeassistant.helpers.entity import Entity
+from homeassistant.components.sensor import SensorEntity
from homeassistant.util import Throttle
import homeassistant.util.dt as dt_util
@@ -34,7 +34,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(dev, True)
-class EbusdSensor(Entity):
+class EbusdSensor(SensorEntity):
"""Ebusd component sensor methods definition."""
def __init__(self, data, sensor, name):
@@ -55,7 +55,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
if self._type == 1 and self._state is not None:
schedule = {
diff --git a/homeassistant/components/ebusd/translations/id.json b/homeassistant/components/ebusd/translations/id.json
new file mode 100644
index 00000000000000..6b2aaa6e789354
--- /dev/null
+++ b/homeassistant/components/ebusd/translations/id.json
@@ -0,0 +1,6 @@
+{
+ "state": {
+ "day": "Siang",
+ "night": "Malam"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ecoal_boiler/sensor.py b/homeassistant/components/ecoal_boiler/sensor.py
index 963f547283fd68..e1c9308b5a950e 100644
--- a/homeassistant/components/ecoal_boiler/sensor.py
+++ b/homeassistant/components/ecoal_boiler/sensor.py
@@ -1,6 +1,6 @@
"""Allows reading temperatures from ecoal/esterownik.pl controller."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import TEMP_CELSIUS
-from homeassistant.helpers.entity import Entity
from . import AVAILABLE_SENSORS, DATA_ECOAL_BOILER
@@ -17,7 +17,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(devices, True)
-class EcoalTempSensor(Entity):
+class EcoalTempSensor(SensorEntity):
"""Representation of a temperature sensor using ecoal status data."""
def __init__(self, ecoal_contr, name, status_attr):
diff --git a/homeassistant/components/ecoal_boiler/switch.py b/homeassistant/components/ecoal_boiler/switch.py
index 57a8d420c437c6..995a49554e6486 100644
--- a/homeassistant/components/ecoal_boiler/switch.py
+++ b/homeassistant/components/ecoal_boiler/switch.py
@@ -1,5 +1,5 @@
"""Allows to configuration ecoal (esterownik.pl) pumps as switches."""
-from typing import Optional
+from __future__ import annotations
from homeassistant.components.switch import SwitchEntity
@@ -40,7 +40,7 @@ def __init__(self, ecoal_contr, name, state_attr):
self._state = None
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the switch."""
return self._name
diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py
index 26bfbe5b3dadc1..015ee1fbf6cab0 100644
--- a/homeassistant/components/ecobee/__init__.py
+++ b/homeassistant/components/ecobee/__init__.py
@@ -10,13 +10,7 @@
from homeassistant.helpers import config_validation as cv
from homeassistant.util import Throttle
-from .const import (
- _LOGGER,
- CONF_REFRESH_TOKEN,
- DATA_ECOBEE_CONFIG,
- DOMAIN,
- ECOBEE_PLATFORMS,
-)
+from .const import _LOGGER, CONF_REFRESH_TOKEN, DATA_ECOBEE_CONFIG, DOMAIN, PLATFORMS
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180)
@@ -32,7 +26,7 @@ async def async_setup(hass, config):
But, an "ecobee:" entry in configuration.yaml will trigger an import flow
if a config entry doesn't already exist. If ecobee.conf exists, the import
flow will attempt to import it and create a config entry, to assist users
- migrating from the old ecobee component. Otherwise, the user will have to
+ migrating from the old ecobee integration. Otherwise, the user will have to
continue setting up the integration via the config flow.
"""
hass.data[DATA_ECOBEE_CONFIG] = config.get(DOMAIN, {})
@@ -66,9 +60,9 @@ async def async_setup_entry(hass, entry):
hass.data[DOMAIN] = data
- for component in ECOBEE_PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -120,7 +114,7 @@ async def async_unload_entry(hass, config_entry):
hass.data.pop(DOMAIN)
tasks = []
- for platform in ECOBEE_PLATFORMS:
+ for platform in PLATFORMS:
tasks.append(
hass.config_entries.async_forward_entry_unload(config_entry, platform)
)
diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py
index c61428cbc78cc9..47c2ff969ecf73 100644
--- a/homeassistant/components/ecobee/climate.py
+++ b/homeassistant/components/ecobee/climate.py
@@ -1,6 +1,7 @@
"""Support for Ecobee Thermostats."""
+from __future__ import annotations
+
import collections
-from typing import Optional
import voluptuous as vol
@@ -406,7 +407,7 @@ def has_humidifier_control(self):
)
@property
- def target_humidity(self) -> Optional[int]:
+ def target_humidity(self) -> int | None:
"""Return the desired humidity set point."""
if self.has_humidifier_control:
return self.thermostat["runtime"]["desiredHumidity"]
@@ -484,7 +485,7 @@ def hvac_modes(self):
return self._operation_list
@property
- def current_humidity(self) -> Optional[int]:
+ def current_humidity(self) -> int | None:
"""Return the current humidity."""
return self.thermostat["runtime"]["actualHumidity"]
@@ -519,7 +520,7 @@ def hvac_action(self):
return CURRENT_HVAC_IDLE
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
status = self.thermostat["equipmentStatus"]
return {
@@ -559,7 +560,7 @@ def set_preset_mode(self, preset_mode):
if preset_mode == PRESET_AWAY:
self.data.ecobee.set_climate_hold(
- self.thermostat_index, "away", "indefinite"
+ self.thermostat_index, "away", "indefinite", self.hold_hours()
)
elif preset_mode == PRESET_TEMPERATURE:
@@ -570,6 +571,7 @@ def set_preset_mode(self, preset_mode):
self.thermostat_index,
PRESET_TO_ECOBEE_HOLD[preset_mode],
self.hold_preference(),
+ self.hold_hours(),
)
elif preset_mode == PRESET_NONE:
@@ -585,14 +587,20 @@ def set_preset_mode(self, preset_mode):
if climate_ref is not None:
self.data.ecobee.set_climate_hold(
- self.thermostat_index, climate_ref, self.hold_preference()
+ self.thermostat_index,
+ climate_ref,
+ self.hold_preference(),
+ self.hold_hours(),
)
else:
_LOGGER.warning("Received unknown preset mode: %s", preset_mode)
else:
self.data.ecobee.set_climate_hold(
- self.thermostat_index, preset_mode, self.hold_preference()
+ self.thermostat_index,
+ preset_mode,
+ self.hold_preference(),
+ self.hold_hours(),
)
@property
@@ -743,7 +751,7 @@ def hold_hours(self):
"useEndTime2hour": 2,
"useEndTime4hour": 4,
}
- return hold_hours_map.get(device_preference, 0)
+ return hold_hours_map.get(device_preference)
def create_vacation(self, service_data):
"""Create a vacation with user-specified parameters."""
diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py
index 5ec3a0fcf96981..44abafe83804d6 100644
--- a/homeassistant/components/ecobee/const.py
+++ b/homeassistant/components/ecobee/const.py
@@ -37,7 +37,7 @@
"vulcanSmart": "ecobee4 Smart",
}
-ECOBEE_PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"]
+PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"]
MANUFACTURER = "ecobee"
diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json
index 040744b27aa939..de7a7d325b3d2d 100644
--- a/homeassistant/components/ecobee/manifest.json
+++ b/homeassistant/components/ecobee/manifest.json
@@ -3,6 +3,6 @@
"name": "ecobee",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ecobee",
- "requirements": ["python-ecobee-api==0.2.8"],
+ "requirements": ["python-ecobee-api==0.2.10"],
"codeowners": ["@marthoc"]
}
diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py
index cfbaa7a45161b0..5abe809e59d4bd 100644
--- a/homeassistant/components/ecobee/sensor.py
+++ b/homeassistant/components/ecobee/sensor.py
@@ -1,13 +1,13 @@
"""Support for Ecobee sensors."""
from pyecobee.const import ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
PERCENTAGE,
TEMP_FAHRENHEIT,
)
-from homeassistant.helpers.entity import Entity
from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER
@@ -32,7 +32,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(dev, True)
-class EcobeeSensor(Entity):
+class EcobeeSensor(SensorEntity):
"""Representation of an Ecobee sensor."""
def __init__(self, data, sensor_name, sensor_type, sensor_index):
diff --git a/homeassistant/components/ecobee/strings.json b/homeassistant/components/ecobee/strings.json
index 78f0708134c8f6..19f379de7d953a 100644
--- a/homeassistant/components/ecobee/strings.json
+++ b/homeassistant/components/ecobee/strings.json
@@ -10,7 +10,7 @@
},
"authorize": {
"title": "Authorize app on ecobee.com",
- "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with pin code:\n\n{pin}\n\nThen, press Submit."
+ "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with PIN code:\n\n{pin}\n\nThen, press Submit."
}
},
"error": {
diff --git a/homeassistant/components/ecobee/translations/ca.json b/homeassistant/components/ecobee/translations/ca.json
index 46d42d0774bbae..99b3f234df23ea 100644
--- a/homeassistant/components/ecobee/translations/ca.json
+++ b/homeassistant/components/ecobee/translations/ca.json
@@ -9,7 +9,7 @@
},
"step": {
"authorize": {
- "description": "Autoritza aquesta aplicaci\u00f3 a https://www.ecobee.com/consumerportal/index.html amb el codi pin seg\u00fcent: \n\n {pin} \n \n A continuaci\u00f3, prem Enviar.",
+ "description": "Autoritza aquesta aplicaci\u00f3 a https://www.ecobee.com/consumerportal/index.html amb el codi PIN: \n\n {pin} \n \n A continuaci\u00f3, prem Envia.",
"title": "Autoritzaci\u00f3 de l'aplicaci\u00f3 a ecobee.com"
},
"user": {
diff --git a/homeassistant/components/ecobee/translations/de.json b/homeassistant/components/ecobee/translations/de.json
index bc65fddebdd047..0c89a696b2c412 100644
--- a/homeassistant/components/ecobee/translations/de.json
+++ b/homeassistant/components/ecobee/translations/de.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits eingerichtet. Es ist nur eine Konfiguration m\u00f6glich."
+ },
"error": {
"pin_request_failed": "Fehler beim Anfordern der PIN von ecobee; Bitte \u00fcberpr\u00fcfe, ob der API-Schl\u00fcssel korrekt ist.",
"token_request_failed": "Fehler beim Anfordern eines Token von ecobee; Bitte versuche es erneut."
diff --git a/homeassistant/components/ecobee/translations/en.json b/homeassistant/components/ecobee/translations/en.json
index 1dfcc2f6a191f0..8a8beeb63c445f 100644
--- a/homeassistant/components/ecobee/translations/en.json
+++ b/homeassistant/components/ecobee/translations/en.json
@@ -9,7 +9,7 @@
},
"step": {
"authorize": {
- "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with pin code:\n\n{pin}\n\nThen, press Submit.",
+ "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with PIN code:\n\n{pin}\n\nThen, press Submit.",
"title": "Authorize app on ecobee.com"
},
"user": {
diff --git a/homeassistant/components/ecobee/translations/et.json b/homeassistant/components/ecobee/translations/et.json
index 46c332a5356803..452cbd578fab84 100644
--- a/homeassistant/components/ecobee/translations/et.json
+++ b/homeassistant/components/ecobee/translations/et.json
@@ -9,7 +9,7 @@
},
"step": {
"authorize": {
- "description": "Tuvasta see rakendus aadressil https://www.ecobee.com/consumerportal/index.html koos PIN-koodiga:\n\n {pin}\n\n Seej\u00e4rel vajuta Esita.",
+ "description": "kinnita see rakendus aadressil https://www.ecobee.com/consumerportal/index.html PIN koodiga:\n\n {pin}\n\n Seej\u00e4rel vajuta Esita.",
"title": "Rakenduse tuvastamine saidil ecobee.com"
},
"user": {
diff --git a/homeassistant/components/ecobee/translations/fr.json b/homeassistant/components/ecobee/translations/fr.json
index cfb307053da59d..acbc909d881144 100644
--- a/homeassistant/components/ecobee/translations/fr.json
+++ b/homeassistant/components/ecobee/translations/fr.json
@@ -9,7 +9,7 @@
},
"step": {
"authorize": {
- "description": "Veuillez autoriser cette application \u00e0 https://www.ecobee.com/consumerportal/index.html avec un code PIN :\n\n{pin}\n\nEnsuite, appuyez sur Soumettre.",
+ "description": "Veuillez autoriser cette application \u00e0 https://www.ecobee.com/consumerportal/index.html avec le code NIP :\n\n{pin}\n\nEnsuite, appuyez sur Soumettre.",
"title": "Autoriser l'application sur ecobee.com"
},
"user": {
diff --git a/homeassistant/components/ecobee/translations/hu.json b/homeassistant/components/ecobee/translations/hu.json
index bd620bc9685030..a91478ff03813b 100644
--- a/homeassistant/components/ecobee/translations/hu.json
+++ b/homeassistant/components/ecobee/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
"error": {
"pin_request_failed": "Hiba t\u00f6rt\u00e9nt a PIN-k\u00f3d ecobee-t\u0151l t\u00f6rt\u00e9n\u0151 k\u00e9r\u00e9sekor; ellen\u0151rizze, hogy az API-kulcs helyes-e.",
"token_request_failed": "Hiba t\u00f6rt\u00e9nt a tokenek ecobee-t\u0151l t\u00f6rt\u00e9n\u0151 ig\u00e9nyl\u00e9se k\u00f6zben; pr\u00f3b\u00e1lkozzon \u00fajra."
diff --git a/homeassistant/components/ecobee/translations/id.json b/homeassistant/components/ecobee/translations/id.json
new file mode 100644
index 00000000000000..7d23b0ca14165e
--- /dev/null
+++ b/homeassistant/components/ecobee/translations/id.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "pin_request_failed": "Terjadi kesalahan saat meminta PIN dari ecobee. Verifikasi apakah kunci API sudah benar.",
+ "token_request_failed": "Kesalahan saat meminta token dari ecobee. Coba lagi"
+ },
+ "step": {
+ "authorize": {
+ "description": "Otorisasi aplikasi ini di https://www.ecobee.com/consumerportal/index.html dengan kode PIN:\n\n{pin}\n\nKemudian, tekan Kirim.",
+ "title": "Otorisasi aplikasi di ecobee.com"
+ },
+ "user": {
+ "data": {
+ "api_key": "Kunci API"
+ },
+ "description": "Masukkan kunci API yang diperoleh dari ecobee.com.",
+ "title": "Kunci API ecobee"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ecobee/translations/ko.json b/homeassistant/components/ecobee/translations/ko.json
index 8be4c28bfbbdc2..45406df54b648c 100644
--- a/homeassistant/components/ecobee/translations/ko.json
+++ b/homeassistant/components/ecobee/translations/ko.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
"error": {
"pin_request_failed": "ecobee \ub85c\ubd80\ud130 PIN \uc694\uccad\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4; API \ud0a4\uac00 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
"token_request_failed": "ecobee \ub85c\ubd80\ud130 \ud1a0\ud070 \uc694\uccad\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4; \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694."
diff --git a/homeassistant/components/ecobee/translations/nl.json b/homeassistant/components/ecobee/translations/nl.json
index 62405b05ff1959..957d2f8244d29c 100644
--- a/homeassistant/components/ecobee/translations/nl.json
+++ b/homeassistant/components/ecobee/translations/nl.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
+ },
"error": {
"pin_request_failed": "Fout bij het aanvragen van pincode bij ecobee; Controleer of de API-sleutel correct is.",
"token_request_failed": "Fout bij het aanvragen van tokens bij ecobee; probeer het opnieuw."
diff --git a/homeassistant/components/ecobee/translations/no.json b/homeassistant/components/ecobee/translations/no.json
index f3c2eceee44f38..0492ff76cc6180 100644
--- a/homeassistant/components/ecobee/translations/no.json
+++ b/homeassistant/components/ecobee/translations/no.json
@@ -9,7 +9,7 @@
},
"step": {
"authorize": {
- "description": "Vennligst godkjenn denne appen p\u00e5 [https://www.ecobee.com/consumerportal](https://www.ecobee.com/consumerportal) med pin-kode:\n\n{pin}\n\nTrykk deretter p\u00e5 send.",
+ "description": "Autoriser denne appen p\u00e5 https://www.ecobee.com/consumerportal/index.html med PIN-kode: \n\n {pin}\n\n Trykk deretter p\u00e5 Send.",
"title": "Godkjenn app p\u00e5 ecobee.com"
},
"user": {
diff --git a/homeassistant/components/ecobee/translations/tr.json b/homeassistant/components/ecobee/translations/tr.json
new file mode 100644
index 00000000000000..23ece38682d11b
--- /dev/null
+++ b/homeassistant/components/ecobee/translations/tr.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Anahtar\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ecobee/translations/uk.json b/homeassistant/components/ecobee/translations/uk.json
new file mode 100644
index 00000000000000..7cf7df534296f6
--- /dev/null
+++ b/homeassistant/components/ecobee/translations/uk.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "pin_request_failed": "\u0421\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0456\u0434 \u0447\u0430\u0441 \u0437\u0430\u043f\u0438\u0442\u0443 PIN-\u043a\u043e\u0434\u0443 \u0443 ecobee; \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0456\u0441\u0442\u044c \u043a\u043b\u044e\u0447\u0430 API.",
+ "token_request_failed": "\u0421\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0456\u0434 \u0447\u0430\u0441 \u0437\u0430\u043f\u0438\u0442\u0443 \u0442\u043e\u043a\u0435\u043d\u0456\u0432 \u0443 ecobee; \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437."
+ },
+ "step": {
+ "authorize": {
+ "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0440\u043e\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e https://www.ecobee.com/consumerportal/index.html \u0456 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e PIN-\u043a\u043e\u0434\u0443: \n\n{pin}\n\n\u041f\u043e\u0442\u0456\u043c \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u041d\u0430\u0434\u0456\u0441\u043b\u0430\u0442\u0438.",
+ "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u0430 \u043d\u0430 ecobee.com"
+ },
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 \u0432\u0456\u0434 ecobee.com.",
+ "title": "ecobee"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py
index dce4550eb1bc68..e605b16a2378be 100644
--- a/homeassistant/components/econet/__init__.py
+++ b/homeassistant/components/econet/__init__.py
@@ -13,7 +13,7 @@
PyeconetError,
)
-from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
+from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, TEMP_FAHRENHEIT
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import dispatcher_send
@@ -24,7 +24,7 @@
_LOGGER = logging.getLogger(__name__)
-PLATFORMS = ["binary_sensor", "sensor", "water_heater"]
+PLATFORMS = ["climate", "binary_sensor", "sensor", "water_heater"]
PUSH_UPDATE = "econet.push_update"
INTERVAL = timedelta(minutes=60)
@@ -54,15 +54,17 @@ async def async_setup_entry(hass, config_entry):
raise ConfigEntryNotReady from err
try:
- equipment = await api.get_equipment_by_type([EquipmentType.WATER_HEATER])
+ equipment = await api.get_equipment_by_type(
+ [EquipmentType.WATER_HEATER, EquipmentType.THERMOSTAT]
+ )
except (ClientError, GenericHTTPError, InvalidResponseFormat) as err:
raise ConfigEntryNotReady from err
hass.data[DOMAIN][API_CLIENT][config_entry.entry_id] = api
hass.data[DOMAIN][EQUIPMENT][config_entry.entry_id] = equipment
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
api.subscribe()
@@ -74,6 +76,9 @@ def update_published():
for _eqip in equipment[EquipmentType.WATER_HEATER]:
_eqip.set_update_callback(update_published)
+ for _eqip in equipment[EquipmentType.THERMOSTAT]:
+ _eqip.set_update_callback(update_published)
+
async def resubscribe(now):
"""Resubscribe to the MQTT updates."""
await hass.async_add_executor_job(api.unsubscribe)
@@ -92,8 +97,8 @@ async def fetch_update(now):
async def async_unload_entry(hass, entry):
"""Unload a EcoNet config entry."""
tasks = [
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
await asyncio.gather(*tasks)
@@ -149,6 +154,11 @@ def unique_id(self):
"""Return the unique ID of the entity."""
return f"{self._econet.device_id}_{self._econet.device_name}"
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement."""
+ return TEMP_FAHRENHEIT
+
@property
def should_poll(self) -> bool:
"""Return True if entity has to be polled for state.
diff --git a/homeassistant/components/econet/binary_sensor.py b/homeassistant/components/econet/binary_sensor.py
index ec8131c510560f..116b1243ee0899 100644
--- a/homeassistant/components/econet/binary_sensor.py
+++ b/homeassistant/components/econet/binary_sensor.py
@@ -1,42 +1,53 @@
"""Support for Rheem EcoNet water heaters."""
-import logging
-
from pyeconet.equipment import EquipmentType
from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_LOCK,
DEVICE_CLASS_OPENING,
DEVICE_CLASS_POWER,
+ DEVICE_CLASS_SOUND,
BinarySensorEntity,
)
from . import EcoNetEntity
from .const import DOMAIN, EQUIPMENT
-_LOGGER = logging.getLogger(__name__)
-
SENSOR_NAME_RUNNING = "running"
SENSOR_NAME_SHUTOFF_VALVE = "shutoff_valve"
-SENSOR_NAME_VACATION = "vacation"
+SENSOR_NAME_RUNNING = "running"
+SENSOR_NAME_SCREEN_LOCKED = "screen_locked"
+SENSOR_NAME_BEEP_ENABLED = "beep_enabled"
+
+ATTR = "attr"
+DEVICE_CLASS = "device_class"
+SENSORS = {
+ SENSOR_NAME_SHUTOFF_VALVE: {
+ ATTR: "shutoff_valve_open",
+ DEVICE_CLASS: DEVICE_CLASS_OPENING,
+ },
+ SENSOR_NAME_RUNNING: {ATTR: "running", DEVICE_CLASS: DEVICE_CLASS_POWER},
+ SENSOR_NAME_SCREEN_LOCKED: {
+ ATTR: "screen_locked",
+ DEVICE_CLASS: DEVICE_CLASS_LOCK,
+ },
+ SENSOR_NAME_BEEP_ENABLED: {
+ ATTR: "beep_enabled",
+ DEVICE_CLASS: DEVICE_CLASS_SOUND,
+ },
+}
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up EcoNet binary sensor based on a config entry."""
equipment = hass.data[DOMAIN][EQUIPMENT][entry.entry_id]
binary_sensors = []
- for water_heater in equipment[EquipmentType.WATER_HEATER]:
- if water_heater.has_shutoff_valve:
- binary_sensors.append(
- EcoNetBinarySensor(
- water_heater,
- SENSOR_NAME_SHUTOFF_VALVE,
- )
- )
- if water_heater.running is not None:
- binary_sensors.append(EcoNetBinarySensor(water_heater, SENSOR_NAME_RUNNING))
- if water_heater.vacation is not None:
- binary_sensors.append(
- EcoNetBinarySensor(water_heater, SENSOR_NAME_VACATION)
- )
+ all_equipment = equipment[EquipmentType.WATER_HEATER].copy()
+ all_equipment.extend(equipment[EquipmentType.THERMOSTAT].copy())
+ for _equip in all_equipment:
+ for sensor_name, sensor in SENSORS.items():
+ if getattr(_equip, sensor[ATTR], None) is not None:
+ binary_sensors.append(EcoNetBinarySensor(_equip, sensor_name))
+
async_add_entities(binary_sensors)
@@ -52,22 +63,12 @@ def __init__(self, econet_device, device_name):
@property
def is_on(self):
"""Return true if the binary sensor is on."""
- if self._device_name == SENSOR_NAME_SHUTOFF_VALVE:
- return self._econet.shutoff_valve_open
- if self._device_name == SENSOR_NAME_RUNNING:
- return self._econet.running
- if self._device_name == SENSOR_NAME_VACATION:
- return self._econet.vacation
- return False
+ return getattr(self._econet, SENSORS[self._device_name][ATTR])
@property
def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES."""
- if self._device_name == SENSOR_NAME_SHUTOFF_VALVE:
- return DEVICE_CLASS_OPENING
- if self._device_name == SENSOR_NAME_RUNNING:
- return DEVICE_CLASS_POWER
- return None
+ return SENSORS[self._device_name][DEVICE_CLASS]
@property
def name(self):
diff --git a/homeassistant/components/econet/climate.py b/homeassistant/components/econet/climate.py
new file mode 100644
index 00000000000000..fe50855d559366
--- /dev/null
+++ b/homeassistant/components/econet/climate.py
@@ -0,0 +1,241 @@
+"""Support for Rheem EcoNet thermostats."""
+import logging
+
+from pyeconet.equipment import EquipmentType
+from pyeconet.equipment.thermostat import ThermostatFanMode, ThermostatOperationMode
+
+from homeassistant.components.climate import ClimateEntity
+from homeassistant.components.climate.const import (
+ ATTR_TARGET_TEMP_HIGH,
+ ATTR_TARGET_TEMP_LOW,
+ FAN_AUTO,
+ FAN_HIGH,
+ FAN_LOW,
+ FAN_MEDIUM,
+ HVAC_MODE_COOL,
+ HVAC_MODE_FAN_ONLY,
+ HVAC_MODE_HEAT,
+ HVAC_MODE_HEAT_COOL,
+ HVAC_MODE_OFF,
+ SUPPORT_AUX_HEAT,
+ SUPPORT_FAN_MODE,
+ SUPPORT_TARGET_HUMIDITY,
+ SUPPORT_TARGET_TEMPERATURE,
+ SUPPORT_TARGET_TEMPERATURE_RANGE,
+)
+from homeassistant.const import ATTR_TEMPERATURE
+
+from . import EcoNetEntity
+from .const import DOMAIN, EQUIPMENT
+
+_LOGGER = logging.getLogger(__name__)
+
+ECONET_STATE_TO_HA = {
+ ThermostatOperationMode.HEATING: HVAC_MODE_HEAT,
+ ThermostatOperationMode.COOLING: HVAC_MODE_COOL,
+ ThermostatOperationMode.OFF: HVAC_MODE_OFF,
+ ThermostatOperationMode.AUTO: HVAC_MODE_HEAT_COOL,
+ ThermostatOperationMode.FAN_ONLY: HVAC_MODE_FAN_ONLY,
+}
+HA_STATE_TO_ECONET = {value: key for key, value in ECONET_STATE_TO_HA.items()}
+
+ECONET_FAN_STATE_TO_HA = {
+ ThermostatFanMode.AUTO: FAN_AUTO,
+ ThermostatFanMode.LOW: FAN_LOW,
+ ThermostatFanMode.MEDIUM: FAN_MEDIUM,
+ ThermostatFanMode.HIGH: FAN_HIGH,
+}
+HA_FAN_STATE_TO_ECONET = {value: key for key, value in ECONET_FAN_STATE_TO_HA.items()}
+
+SUPPORT_FLAGS_THERMOSTAT = (
+ SUPPORT_TARGET_TEMPERATURE
+ | SUPPORT_TARGET_TEMPERATURE_RANGE
+ | SUPPORT_FAN_MODE
+ | SUPPORT_AUX_HEAT
+)
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up EcoNet thermostat based on a config entry."""
+ equipment = hass.data[DOMAIN][EQUIPMENT][entry.entry_id]
+ async_add_entities(
+ [
+ EcoNetThermostat(thermostat)
+ for thermostat in equipment[EquipmentType.THERMOSTAT]
+ ],
+ )
+
+
+class EcoNetThermostat(EcoNetEntity, ClimateEntity):
+ """Define a Econet thermostat."""
+
+ def __init__(self, thermostat):
+ """Initialize."""
+ super().__init__(thermostat)
+ self._running = thermostat.running
+ self._poll = True
+ self.econet_state_to_ha = {}
+ self.ha_state_to_econet = {}
+ self.op_list = []
+ for mode in self._econet.modes:
+ if mode not in [
+ ThermostatOperationMode.UNKNOWN,
+ ThermostatOperationMode.EMERGENCY_HEAT,
+ ]:
+ ha_mode = ECONET_STATE_TO_HA[mode]
+ self.op_list.append(ha_mode)
+
+ @property
+ def supported_features(self):
+ """Return the list of supported features."""
+ if self._econet.supports_humidifier:
+ return SUPPORT_FLAGS_THERMOSTAT | SUPPORT_TARGET_HUMIDITY
+ return SUPPORT_FLAGS_THERMOSTAT
+
+ @property
+ def current_temperature(self):
+ """Return the current temperature."""
+ return self._econet.set_point
+
+ @property
+ def current_humidity(self):
+ """Return the current humidity."""
+ return self._econet.humidity
+
+ @property
+ def target_humidity(self):
+ """Return the humidity we try to reach."""
+ if self._econet.supports_humidifier:
+ return self._econet.dehumidifier_set_point
+ return None
+
+ @property
+ def target_temperature(self):
+ """Return the temperature we try to reach."""
+ if self.hvac_mode == HVAC_MODE_COOL:
+ return self._econet.cool_set_point
+ if self.hvac_mode == HVAC_MODE_HEAT:
+ return self._econet.heat_set_point
+ return None
+
+ @property
+ def target_temperature_low(self):
+ """Return the lower bound temperature we try to reach."""
+ if self.hvac_mode == HVAC_MODE_HEAT_COOL:
+ return self._econet.heat_set_point
+ return None
+
+ @property
+ def target_temperature_high(self):
+ """Return the higher bound temperature we try to reach."""
+ if self.hvac_mode == HVAC_MODE_HEAT_COOL:
+ return self._econet.cool_set_point
+ return None
+
+ def set_temperature(self, **kwargs):
+ """Set new target temperature."""
+ target_temp = kwargs.get(ATTR_TEMPERATURE)
+ target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
+ target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
+ if target_temp:
+ self._econet.set_set_point(target_temp, None, None)
+ if target_temp_low or target_temp_high:
+ self._econet.set_set_point(None, target_temp_high, target_temp_low)
+
+ @property
+ def is_aux_heat(self):
+ """Return true if aux heater."""
+ return self._econet.mode == ThermostatOperationMode.EMERGENCY_HEAT
+
+ @property
+ def hvac_modes(self):
+ """Return hvac operation ie. heat, cool mode.
+
+ Needs to be one of HVAC_MODE_*.
+ """
+ return self.op_list
+
+ @property
+ def hvac_mode(self) -> str:
+ """Return hvac operation ie. heat, cool, mode.
+
+ Needs to be one of HVAC_MODE_*.
+ """
+ econet_mode = self._econet.mode
+ _current_op = HVAC_MODE_OFF
+ if econet_mode is not None:
+ _current_op = ECONET_STATE_TO_HA[econet_mode]
+
+ return _current_op
+
+ def set_hvac_mode(self, hvac_mode):
+ """Set new target hvac mode."""
+ hvac_mode_to_set = HA_STATE_TO_ECONET.get(hvac_mode)
+ if hvac_mode_to_set is None:
+ raise ValueError(f"{hvac_mode} is not a valid mode.")
+ self._econet.set_mode(hvac_mode_to_set)
+
+ def set_humidity(self, humidity: int):
+ """Set new target humidity."""
+ self._econet.set_dehumidifier_set_point(humidity)
+
+ @property
+ def fan_mode(self):
+ """Return the current fan mode."""
+ econet_fan_mode = self._econet.fan_mode
+
+ # Remove this after we figure out how to handle med lo and med hi
+ if econet_fan_mode in [ThermostatFanMode.MEDHI, ThermostatFanMode.MEDLO]:
+ econet_fan_mode = ThermostatFanMode.MEDIUM
+
+ _current_fan_mode = FAN_AUTO
+ if econet_fan_mode is not None:
+ _current_fan_mode = ECONET_FAN_STATE_TO_HA[econet_fan_mode]
+ return _current_fan_mode
+
+ @property
+ def fan_modes(self):
+ """Return the fan modes."""
+ econet_fan_modes = self._econet.fan_modes
+ fan_list = []
+ for mode in econet_fan_modes:
+ # Remove the MEDLO MEDHI once we figure out how to handle it
+ if mode not in [
+ ThermostatFanMode.UNKNOWN,
+ ThermostatFanMode.MEDLO,
+ ThermostatFanMode.MEDHI,
+ ]:
+ fan_list.append(ECONET_FAN_STATE_TO_HA[mode])
+ return fan_list
+
+ def set_fan_mode(self, fan_mode):
+ """Set the fan mode."""
+ self._econet.set_fan_mode(HA_FAN_STATE_TO_ECONET[fan_mode])
+
+ def turn_aux_heat_on(self):
+ """Turn auxiliary heater on."""
+ self._econet.set_mode(ThermostatOperationMode.EMERGENCY_HEAT)
+
+ def turn_aux_heat_off(self):
+ """Turn auxiliary heater off."""
+ self._econet.set_mode(ThermostatOperationMode.HEATING)
+
+ @property
+ def min_temp(self):
+ """Return the minimum temperature."""
+ return self._econet.set_point_limits[0]
+
+ @property
+ def max_temp(self):
+ """Return the maximum temperature."""
+ return self._econet.set_point_limits[1]
+
+ @property
+ def min_humidity(self) -> int:
+ """Return the minimum humidity."""
+ return self._econet.dehumidifier_set_point_limits[0]
+
+ @property
+ def max_humidity(self) -> int:
+ """Return the maximum humidity."""
+ return self._econet.dehumidifier_set_point_limits[1]
diff --git a/homeassistant/components/econet/config_flow.py b/homeassistant/components/econet/config_flow.py
index 78aff2eac8ff39..739606088f68f5 100644
--- a/homeassistant/components/econet/config_flow.py
+++ b/homeassistant/components/econet/config_flow.py
@@ -6,7 +6,7 @@
from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import DOMAIN
class EcoNetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json
index 7e4cf0106ba224..c658542295e86d 100644
--- a/homeassistant/components/econet/manifest.json
+++ b/homeassistant/components/econet/manifest.json
@@ -4,6 +4,6 @@
"name": "Rheem EcoNet Products",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/econet",
- "requirements": ["pyeconet==0.1.12"],
+ "requirements": ["pyeconet==0.1.13"],
"codeowners": ["@vangorra", "@w1ll1am23"]
}
\ No newline at end of file
diff --git a/homeassistant/components/econet/sensor.py b/homeassistant/components/econet/sensor.py
index 6ae14d18aa1243..0dfe8df7fb39c1 100644
--- a/homeassistant/components/econet/sensor.py
+++ b/homeassistant/components/econet/sensor.py
@@ -1,12 +1,11 @@
"""Support for Rheem EcoNet water heaters."""
-import logging
-
from pyeconet.equipment import EquipmentType
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
+ DEVICE_CLASS_SIGNAL_STRENGTH,
ENERGY_KILO_WATT_HOUR,
PERCENTAGE,
- SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
VOLUME_GALLONS,
)
@@ -14,6 +13,7 @@
from .const import DOMAIN, EQUIPMENT
ENERGY_KILO_BRITISH_THERMAL_UNIT = "kBtu"
+
TANK_HEALTH = "tank_health"
AVAILIBLE_HOT_WATER = "availible_hot_water"
COMPRESSOR_HEALTH = "compressor_health"
@@ -24,35 +24,55 @@
WIFI_SIGNAL = "wifi_signal"
RUNNING_STATE = "running_state"
+SENSOR_NAMES_TO_ATTRIBUTES = {
+ TANK_HEALTH: "tank_health",
+ AVAILIBLE_HOT_WATER: "tank_hot_water_availability",
+ COMPRESSOR_HEALTH: "compressor_health",
+ OVERRIDE_STATUS: "override_status",
+ WATER_USAGE_TODAY: "todays_water_usage",
+ POWER_USAGE_TODAY: "todays_energy_usage",
+ ALERT_COUNT: "alert_count",
+ WIFI_SIGNAL: "wifi_signal",
+ RUNNING_STATE: "running_state",
+}
-_LOGGER = logging.getLogger(__name__)
+SENSOR_NAMES_TO_UNIT_OF_MEASUREMENT = {
+ TANK_HEALTH: PERCENTAGE,
+ AVAILIBLE_HOT_WATER: PERCENTAGE,
+ COMPRESSOR_HEALTH: PERCENTAGE,
+ OVERRIDE_STATUS: None,
+ WATER_USAGE_TODAY: VOLUME_GALLONS,
+ POWER_USAGE_TODAY: None, # Depends on unit type
+ ALERT_COUNT: None,
+ WIFI_SIGNAL: DEVICE_CLASS_SIGNAL_STRENGTH,
+ RUNNING_STATE: None, # This is just a string
+}
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up EcoNet sensor based on a config entry."""
+
equipment = hass.data[DOMAIN][EQUIPMENT][entry.entry_id]
sensors = []
+ all_equipment = equipment[EquipmentType.WATER_HEATER].copy()
+ all_equipment.extend(equipment[EquipmentType.THERMOSTAT].copy())
+
+ for _equip in all_equipment:
+ for name, attribute in SENSOR_NAMES_TO_ATTRIBUTES.items():
+ if getattr(_equip, attribute, None) is not None:
+ sensors.append(EcoNetSensor(_equip, name))
+ # This is None to start with and all device have it
+ sensors.append(EcoNetSensor(_equip, WIFI_SIGNAL))
+
for water_heater in equipment[EquipmentType.WATER_HEATER]:
- if water_heater.tank_hot_water_availability is not None:
- sensors.append(EcoNetSensor(water_heater, AVAILIBLE_HOT_WATER))
- if water_heater.tank_health is not None:
- sensors.append(EcoNetSensor(water_heater, TANK_HEALTH))
- if water_heater.compressor_health is not None:
- sensors.append(EcoNetSensor(water_heater, COMPRESSOR_HEALTH))
- if water_heater.override_status:
- sensors.append(EcoNetSensor(water_heater, OVERRIDE_STATUS))
- if water_heater.running_state is not None:
- sensors.append(EcoNetSensor(water_heater, RUNNING_STATE))
- # All units have this
- sensors.append(EcoNetSensor(water_heater, ALERT_COUNT))
# These aren't part of the device and start off as None in pyeconet so always add them
sensors.append(EcoNetSensor(water_heater, WATER_USAGE_TODAY))
sensors.append(EcoNetSensor(water_heater, POWER_USAGE_TODAY))
- sensors.append(EcoNetSensor(water_heater, WIFI_SIGNAL))
+
async_add_entities(sensors)
-class EcoNetSensor(EcoNetEntity):
+class EcoNetSensor(EcoNetEntity, SensorEntity):
"""Define a Econet sensor."""
def __init__(self, econet_device, device_name):
@@ -64,50 +84,21 @@ def __init__(self, econet_device, device_name):
@property
def state(self):
"""Return sensors state."""
- if self._device_name == AVAILIBLE_HOT_WATER:
- return self._econet.tank_hot_water_availability
- if self._device_name == TANK_HEALTH:
- return self._econet.tank_health
- if self._device_name == COMPRESSOR_HEALTH:
- return self._econet.compressor_health
- if self._device_name == OVERRIDE_STATUS:
- return self._econet.oveerride_status
- if self._device_name == WATER_USAGE_TODAY:
- if self._econet.todays_water_usage:
- return round(self._econet.todays_water_usage, 2)
- return None
- if self._device_name == POWER_USAGE_TODAY:
- if self._econet.todays_energy_usage:
- return round(self._econet.todays_energy_usage, 2)
- return None
- if self._device_name == WIFI_SIGNAL:
- if self._econet.wifi_signal:
- return self._econet.wifi_signal
- return None
- if self._device_name == ALERT_COUNT:
- return self._econet.alert_count
- if self._device_name == RUNNING_STATE:
- return self._econet.running_state
- return None
+ value = getattr(self._econet, SENSOR_NAMES_TO_ATTRIBUTES[self._device_name])
+ if isinstance(value, float):
+ value = round(value, 2)
+ return value
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
- if self._device_name == AVAILIBLE_HOT_WATER:
- return PERCENTAGE
- if self._device_name == TANK_HEALTH:
- return PERCENTAGE
- if self._device_name == COMPRESSOR_HEALTH:
- return PERCENTAGE
- if self._device_name == WATER_USAGE_TODAY:
- return VOLUME_GALLONS
+ unit_of_measurement = SENSOR_NAMES_TO_UNIT_OF_MEASUREMENT[self._device_name]
if self._device_name == POWER_USAGE_TODAY:
if self._econet.energy_type == ENERGY_KILO_BRITISH_THERMAL_UNIT.upper():
- return ENERGY_KILO_BRITISH_THERMAL_UNIT
- return ENERGY_KILO_WATT_HOUR
- if self._device_name == WIFI_SIGNAL:
- return SIGNAL_STRENGTH_DECIBELS_MILLIWATT
- return None
+ unit_of_measurement = ENERGY_KILO_BRITISH_THERMAL_UNIT
+ else:
+ unit_of_measurement = ENERGY_KILO_WATT_HOUR
+ return unit_of_measurement
@property
def name(self):
diff --git a/homeassistant/components/econet/translations/bg.json b/homeassistant/components/econet/translations/bg.json
new file mode 100644
index 00000000000000..cef3726d759676
--- /dev/null
+++ b/homeassistant/components/econet/translations/bg.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u0430"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/translations/ca.json b/homeassistant/components/econet/translations/ca.json
new file mode 100644
index 00000000000000..c53914f8cb990e
--- /dev/null
+++ b/homeassistant/components/econet/translations/ca.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Correu electr\u00f2nic",
+ "password": "Contrasenya"
+ },
+ "title": "Configuraci\u00f3 del compte Rheem EcoNet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/translations/cs.json b/homeassistant/components/econet/translations/cs.json
new file mode 100644
index 00000000000000..bd8b079962851c
--- /dev/null
+++ b/homeassistant/components/econet/translations/cs.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "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"
+ },
+ "error": {
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
+ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-mail",
+ "password": "Heslo"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/translations/de.json b/homeassistant/components/econet/translations/de.json
new file mode 100644
index 00000000000000..854d61f1790b48
--- /dev/null
+++ b/homeassistant/components/econet/translations/de.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-Mail",
+ "password": "Passwort"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/translations/en.json b/homeassistant/components/econet/translations/en.json
index 4061c094c1ff83..ad499b0e37c739 100644
--- a/homeassistant/components/econet/translations/en.json
+++ b/homeassistant/components/econet/translations/en.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "already_configured": "Device is already configured",
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication"
},
diff --git a/homeassistant/components/econet/translations/es.json b/homeassistant/components/econet/translations/es.json
new file mode 100644
index 00000000000000..8634be9413fcc2
--- /dev/null
+++ b/homeassistant/components/econet/translations/es.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositivo ya est\u00e1 configurado",
+ "cannot_connect": "No se pudo conectar",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida"
+ },
+ "error": {
+ "cannot_connect": "No se pudo conectar",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Correo electr\u00f3nico",
+ "password": "Contrase\u00f1a"
+ },
+ "title": "Configurar la cuenta Rheem EcoNet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/translations/et.json b/homeassistant/components/econet/translations/et.json
new file mode 100644
index 00000000000000..349a4d21111587
--- /dev/null
+++ b/homeassistant/components/econet/translations/et.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_auth": "Vigane autentimine"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_auth": "Vigane autentimine"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-posti aadress",
+ "password": "Salas\u00f5na"
+ },
+ "title": "Seadista Rheem EcoNeti konto"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/translations/fr.json b/homeassistant/components/econet/translations/fr.json
new file mode 100644
index 00000000000000..64fd39c852ad51
--- /dev/null
+++ b/homeassistant/components/econet/translations/fr.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ",
+ "cannot_connect": "\u00c9chec de la connexion ",
+ "invalid_auth": "Authentification invalide "
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de la connexion",
+ "invalid_auth": "Authentification invalide "
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Email",
+ "password": "Mot de passe"
+ },
+ "title": "Configurer le compte Rheem EcoNet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/translations/hu.json b/homeassistant/components/econet/translations/hu.json
new file mode 100644
index 00000000000000..065c648d4a041f
--- /dev/null
+++ b/homeassistant/components/econet/translations/hu.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-mail",
+ "password": "Jelsz\u00f3"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/translations/id.json b/homeassistant/components/econet/translations/id.json
new file mode 100644
index 00000000000000..467b58a8d27f50
--- /dev/null
+++ b/homeassistant/components/econet/translations/id.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Email",
+ "password": "Kata Sandi"
+ },
+ "title": "Siapkan Akun Rheem EcoNet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/translations/it.json b/homeassistant/components/econet/translations/it.json
new file mode 100644
index 00000000000000..3074c72b083953
--- /dev/null
+++ b/homeassistant/components/econet/translations/it.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-mail",
+ "password": "Password"
+ },
+ "title": "Imposta account Rheem EcoNet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/translations/ko.json b/homeassistant/components/econet/translations/ko.json
new file mode 100644
index 00000000000000..40735cdb6d05f9
--- /dev/null
+++ b/homeassistant/components/econet/translations/ko.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "\uc774\uba54\uc77c",
+ "password": "\ube44\ubc00\ubc88\ud638"
+ },
+ "title": "Rheem EcoNet \uacc4\uc815 \uc124\uc815\ud558\uae30"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/translations/nl.json b/homeassistant/components/econet/translations/nl.json
new file mode 100644
index 00000000000000..226c1611e2b71d
--- /dev/null
+++ b/homeassistant/components/econet/translations/nl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd",
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-mail",
+ "password": "Wachtwoord"
+ },
+ "title": "Stel Rheem EcoNet-account in"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/translations/no.json b/homeassistant/components/econet/translations/no.json
new file mode 100644
index 00000000000000..f54cedffda84ed
--- /dev/null
+++ b/homeassistant/components/econet/translations/no.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert",
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_auth": "Ugyldig godkjenning"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_auth": "Ugyldig godkjenning"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-post",
+ "password": "Passord"
+ },
+ "title": "Konfigurer Rheem EcoNet-konto"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/translations/pl.json b/homeassistant/components/econet/translations/pl.json
new file mode 100644
index 00000000000000..e5d74de590d54d
--- /dev/null
+++ b/homeassistant/components/econet/translations/pl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Adres e-mail",
+ "password": "Has\u0142o"
+ },
+ "title": "Konfiguracja konta Rheem EcoNet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/translations/ru.json b/homeassistant/components/econet/translations/ru.json
new file mode 100644
index 00000000000000..1b0d79ac396e13
--- /dev/null
+++ b/homeassistant/components/econet/translations/ru.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "title": "Rheem EcoNet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/translations/tr.json b/homeassistant/components/econet/translations/tr.json
new file mode 100644
index 00000000000000..237a87d02685eb
--- /dev/null
+++ b/homeassistant/components/econet/translations/tr.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Email",
+ "password": "\u015eifre"
+ },
+ "title": "Rheem EcoNet Hesab\u0131n\u0131 Kur"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/translations/zh-Hant.json b/homeassistant/components/econet/translations/zh-Hant.json
new file mode 100644
index 00000000000000..50824c198145a4
--- /dev/null
+++ b/homeassistant/components/econet/translations/zh-Hant.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "\u96fb\u5b50\u90f5\u4ef6",
+ "password": "\u5bc6\u78bc"
+ },
+ "title": "\u8a2d\u5b9a Rheem EcoNet \u5e33\u865f"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py
index af3399b53afb3a..ed31e78af7c2ac 100644
--- a/homeassistant/components/econet/water_heater.py
+++ b/homeassistant/components/econet/water_heater.py
@@ -18,7 +18,6 @@
SUPPORT_TARGET_TEMPERATURE,
WaterHeaterEntity,
)
-from homeassistant.const import TEMP_FAHRENHEIT
from homeassistant.core import callback
from . import EcoNetEntity
@@ -77,11 +76,6 @@ def is_away_mode_on(self):
"""Return true if away mode is on."""
return self._econet.away
- @property
- def temperature_unit(self):
- """Return the unit of measurement."""
- return TEMP_FAHRENHEIT
-
@property
def current_operation(self):
"""Return current operation."""
@@ -160,6 +154,7 @@ async def async_update(self):
"""Get the latest energy usage."""
await self.water_heater.get_energy_usage()
await self.water_heater.get_water_usage()
+ self.async_write_ha_state()
self._poll = False
def turn_away_mode_on(self):
diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py
index 6ad51e6c4741c0..934833c0f952b4 100644
--- a/homeassistant/components/ecovacs/vacuum.py
+++ b/homeassistant/components/ecovacs/vacuum.py
@@ -189,7 +189,7 @@ def send_command(self, command, params=None, **kwargs):
self.device.run(sucks.VacBotCommand(command, params))
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device-specific state attributes of this vacuum."""
data = {}
data[ATTR_ERROR] = self._error
diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py
index 1d6ff61bf59c98..28711821f507d5 100644
--- a/homeassistant/components/eddystone_temperature/sensor.py
+++ b/homeassistant/components/eddystone_temperature/sensor.py
@@ -10,7 +10,7 @@
from beacontools import BeaconScanner, EddystoneFilter, EddystoneTLMFrame
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_NAME,
EVENT_HOMEASSISTANT_START,
@@ -19,7 +19,6 @@
TEMP_CELSIUS,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -97,7 +96,7 @@ def get_from_conf(config, config_key, length):
return string
-class EddystoneTemp(Entity):
+class EddystoneTemp(SensorEntity):
"""Representation of a temperature sensor."""
def __init__(self, name, namespace, instance):
@@ -171,15 +170,18 @@ def process_packet(self, namespace, instance, temperature):
)
for dev in self.devices:
- if dev.namespace == namespace and dev.instance == instance:
- if dev.temperature != temperature:
- dev.temperature = temperature
- dev.schedule_update_ha_state()
+ if (
+ dev.namespace == namespace
+ and dev.instance == instance
+ and dev.temperature != temperature
+ ):
+ dev.temperature = temperature
+ dev.schedule_update_ha_state()
def stop(self):
"""Signal runner to stop and join thread."""
if self.scanning:
- _LOGGER.debug("Stopping...")
+ _LOGGER.debug("Stopping")
self.scanner.stop()
_LOGGER.debug("Stopped")
self.scanning = False
diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py
index dc0f51abe61f88..090b2780ec4681 100644
--- a/homeassistant/components/edl21/sensor.py
+++ b/homeassistant/components/edl21/sensor.py
@@ -7,7 +7,7 @@
from sml.asyncio import SmlProtocol
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
@@ -15,7 +15,6 @@
async_dispatcher_connect,
async_dispatcher_send,
)
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_registry import async_get_registry
from homeassistant.helpers.typing import Optional
from homeassistant.util.dt import utcnow
@@ -193,7 +192,7 @@ async def add_entities(self, new_entities) -> None:
self._async_add_entities(new_entities, update_before_add=True)
-class EDL21Entity(Entity):
+class EDL21Entity(SensorEntity):
"""Entity reading values from EDL21 telegram."""
def __init__(self, electricity_id, obis, name, telegram):
@@ -269,7 +268,7 @@ def state(self) -> str:
return self._telegram.get("value")
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Enumerate supported attributes."""
return {
self._state_attrs[k]: v
diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py
index 8c16317beda402..6e2ac1c01c7f49 100644
--- a/homeassistant/components/efergy/sensor.py
+++ b/homeassistant/components/efergy/sensor.py
@@ -4,18 +4,21 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_CURRENCY, ENERGY_KILO_WATT_HOUR, POWER_WATT
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.const import (
+ CONF_CURRENCY,
+ CONF_MONITORED_VARIABLES,
+ CONF_TYPE,
+ ENERGY_KILO_WATT_HOUR,
+ POWER_WATT,
+)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
_RESOURCE = "https://engage.efergy.com/mobile_proxy/"
CONF_APPTOKEN = "app_token"
CONF_UTC_OFFSET = "utc_offset"
-CONF_MONITORED_VARIABLES = "monitored_variables"
-CONF_SENSOR_TYPE = "type"
CONF_PERIOD = "period"
@@ -40,7 +43,7 @@
SENSORS_SCHEMA = vol.Schema(
{
- vol.Required(CONF_SENSOR_TYPE): TYPES_SCHEMA,
+ vol.Required(CONF_TYPE): TYPES_SCHEMA,
vol.Optional(CONF_CURRENCY, default=""): cv.string,
vol.Optional(CONF_PERIOD, default=DEFAULT_PERIOD): cv.string,
}
@@ -62,14 +65,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
dev = []
for variable in config[CONF_MONITORED_VARIABLES]:
- if variable[CONF_SENSOR_TYPE] == CONF_CURRENT_VALUES:
+ if variable[CONF_TYPE] == CONF_CURRENT_VALUES:
url_string = f"{_RESOURCE}getCurrentValuesSummary?token={app_token}"
response = requests.get(url_string, timeout=10)
for sensor in response.json():
sid = sensor["sid"]
dev.append(
EfergySensor(
- variable[CONF_SENSOR_TYPE],
+ variable[CONF_TYPE],
app_token,
utc_offset,
variable[CONF_PERIOD],
@@ -79,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
)
dev.append(
EfergySensor(
- variable[CONF_SENSOR_TYPE],
+ variable[CONF_TYPE],
app_token,
utc_offset,
variable[CONF_PERIOD],
@@ -90,7 +93,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(dev, True)
-class EfergySensor(Entity):
+class EfergySensor(SensorEntity):
"""Implementation of an Efergy sensor."""
def __init__(self, sensor_type, app_token, utc_offset, period, currency, sid=None):
diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py
index 43bcb4c2f0930f..ae0854ec244526 100644
--- a/homeassistant/components/eight_sleep/sensor.py
+++ b/homeassistant/components/eight_sleep/sensor.py
@@ -1,6 +1,7 @@
"""Support for Eight Sleep sensors."""
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from . import (
@@ -66,7 +67,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(all_sensors, True)
-class EightHeatSensor(EightSleepHeatEntity):
+class EightHeatSensor(EightSleepHeatEntity, SensorEntity):
"""Representation of an eight sleep heat-based sensor."""
def __init__(self, name, eight, sensor):
@@ -110,7 +111,7 @@ async def async_update(self):
self._state = self._usrobj.heating_level
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device state attributes."""
return {
ATTR_TARGET_HEAT: self._usrobj.target_heating_level,
@@ -119,7 +120,7 @@ def device_state_attributes(self):
}
-class EightUserSensor(EightSleepUserEntity):
+class EightUserSensor(EightSleepUserEntity, SensorEntity):
"""Representation of an eight sleep user-based sensor."""
def __init__(self, name, eight, sensor, units):
@@ -202,7 +203,7 @@ async def async_update(self):
self._state = self._usrobj.current_values["stage"]
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device state attributes."""
if self._attr is None:
# Skip attributes if sensor type doesn't support
@@ -289,7 +290,7 @@ def device_state_attributes(self):
return state_attr
-class EightRoomSensor(EightSleepUserEntity):
+class EightRoomSensor(EightSleepUserEntity, SensorEntity):
"""Representation of an eight sleep room sensor."""
def __init__(self, name, eight, sensor, units):
diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py
index d6c849a4c5a351..22d6040678008e 100644
--- a/homeassistant/components/elgato/__init__.py
+++ b/homeassistant/components/elgato/__init__.py
@@ -1,4 +1,6 @@
"""Support for Elgato Key Lights."""
+import logging
+
from elgato import Elgato, ElgatoConnectionError
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
@@ -7,16 +9,10 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.typing import ConfigType
from .const import DATA_ELGATO_CLIENT, DOMAIN
-async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
- """Set up the Elgato Key Light components."""
- return True
-
-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Elgato Key Light from a config entry."""
session = async_get_clientsession(hass)
@@ -30,6 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try:
await elgato.info()
except ElgatoConnectionError as exception:
+ logging.getLogger(__name__).debug("Unable to connect: %s", exception)
raise ConfigEntryNotReady from exception
hass.data.setdefault(DOMAIN, {})
diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py
index a6f25b8827c24d..afdbe7e1cdc47f 100644
--- a/homeassistant/components/elgato/config_flow.py
+++ b/homeassistant/components/elgato/config_flow.py
@@ -1,15 +1,17 @@
"""Config flow to configure the Elgato Key Light integration."""
-from typing import Any, Dict, Optional
+from __future__ import annotations
-from elgato import Elgato, ElgatoError, Info
+from typing import Any
+
+from elgato import Elgato, ElgatoError
import voluptuous as vol
from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow
from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.typing import ConfigType
-from .const import CONF_SERIAL_NUMBER, DOMAIN # pylint: disable=unused-import
+from .const import CONF_SERIAL_NUMBER, DOMAIN
class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN):
@@ -18,93 +20,55 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL
+ host: str
+ port: int
+ serial_number: str
+
async def async_step_user(
- self, user_input: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initiated by the user."""
if user_input is None:
- return self._show_setup_form()
+ return self._async_show_setup_form()
+
+ self.host = user_input[CONF_HOST]
+ self.port = user_input[CONF_PORT]
try:
- info = await self._get_elgato_info(
- user_input[CONF_HOST], user_input[CONF_PORT]
- )
+ await self._get_elgato_serial_number(raise_on_progress=False)
except ElgatoError:
- return self._show_setup_form({"base": "cannot_connect"})
-
- # Check if already configured
- await self.async_set_unique_id(info.serial_number)
- self._abort_if_unique_id_configured()
+ return self._async_show_setup_form({"base": "cannot_connect"})
- return self.async_create_entry(
- title=info.serial_number,
- data={
- CONF_HOST: user_input[CONF_HOST],
- CONF_PORT: user_input[CONF_PORT],
- CONF_SERIAL_NUMBER: info.serial_number,
- },
- )
+ return self._async_create_entry()
async def async_step_zeroconf(
- self, user_input: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, discovery_info: dict[str, Any]
+ ) -> dict[str, Any]:
"""Handle zeroconf discovery."""
- if user_input is None:
- return self.async_abort(reason="cannot_connect")
+ self.host = discovery_info[CONF_HOST]
+ self.port = discovery_info[CONF_PORT]
try:
- info = await self._get_elgato_info(
- user_input[CONF_HOST], user_input[CONF_PORT]
- )
+ await self._get_elgato_serial_number()
except ElgatoError:
return self.async_abort(reason="cannot_connect")
- # Check if already configured
- await self.async_set_unique_id(info.serial_number)
- self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]})
-
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
- self.context.update(
- {
- CONF_HOST: user_input[CONF_HOST],
- CONF_PORT: user_input[CONF_PORT],
- CONF_SERIAL_NUMBER: info.serial_number,
- "title_placeholders": {"serial_number": info.serial_number},
- }
+ self._set_confirm_only()
+ return self.async_show_form(
+ step_id="zeroconf_confirm",
+ description_placeholders={"serial_number": self.serial_number},
)
- # Prepare configuration flow
- return self._show_confirm_dialog()
-
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
async def async_step_zeroconf_confirm(
- self, user_input: ConfigType = None
- ) -> Dict[str, Any]:
+ self, _: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initiated by zeroconf."""
- if user_input is None:
- return self._show_confirm_dialog()
+ return self._async_create_entry()
- try:
- info = await self._get_elgato_info(
- self.context.get(CONF_HOST), self.context.get(CONF_PORT)
- )
- except ElgatoError:
- return self.async_abort(reason="cannot_connect")
-
- # Check if already configured
- await self.async_set_unique_id(info.serial_number)
- self._abort_if_unique_id_configured()
-
- return self.async_create_entry(
- title=self.context.get(CONF_SERIAL_NUMBER),
- data={
- CONF_HOST: self.context.get(CONF_HOST),
- CONF_PORT: self.context.get(CONF_PORT),
- CONF_SERIAL_NUMBER: self.context.get(CONF_SERIAL_NUMBER),
- },
- )
-
- def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
+ @callback
+ def _async_show_setup_form(
+ self, errors: dict[str, str] | None = None
+ ) -> dict[str, Any]:
"""Show the setup form to the user."""
return self.async_show_form(
step_id="user",
@@ -117,21 +81,33 @@ def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
errors=errors or {},
)
- def _show_confirm_dialog(self) -> Dict[str, Any]:
- """Show the confirm dialog to the user."""
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
- serial_number = self.context.get(CONF_SERIAL_NUMBER)
- return self.async_show_form(
- step_id="zeroconf_confirm",
- description_placeholders={"serial_number": serial_number},
+ @callback
+ def _async_create_entry(self) -> dict[str, Any]:
+ return self.async_create_entry(
+ title=self.serial_number,
+ data={
+ CONF_HOST: self.host,
+ CONF_PORT: self.port,
+ CONF_SERIAL_NUMBER: self.serial_number,
+ },
)
- async def _get_elgato_info(self, host: str, port: int) -> Info:
+ async def _get_elgato_serial_number(self, raise_on_progress: bool = True) -> None:
"""Get device information from an Elgato Key Light device."""
session = async_get_clientsession(self.hass)
elgato = Elgato(
- host,
- port=port,
+ host=self.host,
+ port=self.port,
session=session,
)
- return await elgato.info()
+ info = await elgato.info()
+
+ # Check if already configured
+ await self.async_set_unique_id(
+ info.serial_number, raise_on_progress=raise_on_progress
+ )
+ self._abort_if_unique_id_configured(
+ updates={CONF_HOST: self.host, CONF_PORT: self.port}
+ )
+
+ self.serial_number = info.serial_number
diff --git a/homeassistant/components/elgato/const.py b/homeassistant/components/elgato/const.py
index 2b6caa37a8f9c8..b2535ce0e4f986 100644
--- a/homeassistant/components/elgato/const.py
+++ b/homeassistant/components/elgato/const.py
@@ -12,6 +12,5 @@
ATTR_MODEL = "model"
ATTR_ON = "on"
ATTR_SOFTWARE_VERSION = "sw_version"
-ATTR_TEMPERATURE = "temperature"
CONF_SERIAL_NUMBER = "serial_number"
diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py
index 313b5600248a07..ae3d8274281425 100644
--- a/homeassistant/components/elgato/light.py
+++ b/homeassistant/components/elgato/light.py
@@ -1,7 +1,9 @@
"""Support for LED lights."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Any, Callable, Dict, List, Optional
+from typing import Any, Callable
from elgato import Elgato, ElgatoError, Info, State
@@ -13,7 +15,7 @@
LightEntity,
)
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_NAME
+from homeassistant.const import ATTR_NAME, ATTR_TEMPERATURE
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
@@ -23,7 +25,6 @@
ATTR_MODEL,
ATTR_ON,
ATTR_SOFTWARE_VERSION,
- ATTR_TEMPERATURE,
DATA_ELGATO_CLIENT,
DOMAIN,
)
@@ -37,12 +38,12 @@
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up Elgato Key Light based on a config entry."""
elgato: Elgato = hass.data[DOMAIN][entry.entry_id][DATA_ELGATO_CLIENT]
info = await elgato.info()
- async_add_entities([ElgatoLight(entry.entry_id, elgato, info)], True)
+ async_add_entities([ElgatoLight(elgato, info)], True)
class ElgatoLight(LightEntity):
@@ -50,30 +51,24 @@ class ElgatoLight(LightEntity):
def __init__(
self,
- entry_id: str,
elgato: Elgato,
info: Info,
):
"""Initialize Elgato Key Light."""
- self._brightness: Optional[int] = None
self._info: Info = info
- self._state: Optional[bool] = None
- self._temperature: Optional[int] = None
- self._available = True
+ self._state: State | None = None
self.elgato = elgato
@property
def name(self) -> str:
"""Return the name of the entity."""
# Return the product name, if display name is not set
- if not self._info.display_name:
- return self._info.product_name
- return self._info.display_name
+ return self._info.display_name or self._info.product_name
@property
def available(self) -> bool:
"""Return True if entity is available."""
- return self._available
+ return self._state is not None
@property
def unique_id(self) -> str:
@@ -81,22 +76,24 @@ def unique_id(self) -> str:
return self._info.serial_number
@property
- def brightness(self) -> Optional[int]:
+ def brightness(self) -> int | None:
"""Return the brightness of this light between 1..255."""
- return self._brightness
+ assert self._state is not None
+ return round((self._state.brightness * 255) / 100)
@property
- def color_temp(self):
+ def color_temp(self) -> int | None:
"""Return the CT color value in mireds."""
- return self._temperature
+ assert self._state is not None
+ return self._state.temperature
@property
- def min_mireds(self):
+ def min_mireds(self) -> int:
"""Return the coldest color_temp that this light supports."""
return 143
@property
- def max_mireds(self):
+ def max_mireds(self) -> int:
"""Return the warmest color_temp that this light supports."""
return 344
@@ -108,7 +105,8 @@ def supported_features(self) -> int:
@property
def is_on(self) -> bool:
"""Return the state of the light."""
- return bool(self._state)
+ assert self._state is not None
+ return self._state.on
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light."""
@@ -116,9 +114,8 @@ async def async_turn_off(self, **kwargs: Any) -> None:
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the light."""
- data = {}
+ data: dict[str, bool | int] = {ATTR_ON: True}
- data[ATTR_ON] = True
if ATTR_ON in kwargs:
data[ATTR_ON] = kwargs[ATTR_ON]
@@ -132,25 +129,22 @@ async def async_turn_on(self, **kwargs: Any) -> None:
await self.elgato.light(**data)
except ElgatoError:
_LOGGER.error("An error occurred while updating the Elgato Key Light")
- self._available = False
+ self._state = None
async def async_update(self) -> None:
"""Update Elgato entity."""
+ restoring = self._state is None
try:
- state: State = await self.elgato.state()
- except ElgatoError:
- if self._available:
- _LOGGER.error("An error occurred while updating the Elgato Key Light")
- self._available = False
- return
-
- self._available = True
- self._brightness = round((state.brightness * 255) / 100)
- self._state = state.on
- self._temperature = state.temperature
+ self._state: State = await self.elgato.state()
+ if restoring:
+ _LOGGER.info("Connection restored")
+ except ElgatoError as err:
+ meth = _LOGGER.error if self._state else _LOGGER.debug
+ meth("An error occurred while updating the Elgato Key Light: %s", err)
+ self._state = None
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about this Elgato Key Light."""
return {
ATTR_IDENTIFIERS: {(DOMAIN, self._info.serial_number)},
diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json
index 1da98a4121170d..9a166b86b8e7d3 100644
--- a/homeassistant/components/elgato/manifest.json
+++ b/homeassistant/components/elgato/manifest.json
@@ -3,7 +3,7 @@
"name": "Elgato Key Light",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/elgato",
- "requirements": ["elgato==1.0.0"],
+ "requirements": ["elgato==2.0.1"],
"zeroconf": ["_elg._tcp.local."],
"codeowners": ["@frenck"],
"quality_scale": "platinum"
diff --git a/homeassistant/components/elgato/translations/de.json b/homeassistant/components/elgato/translations/de.json
index 7497460445337c..1df8f91ecd6a2e 100644
--- a/homeassistant/components/elgato/translations/de.json
+++ b/homeassistant/components/elgato/translations/de.json
@@ -2,10 +2,10 @@
"config": {
"abort": {
"already_configured": "Dieses Elgato Key Light-Ger\u00e4t ist bereits konfiguriert.",
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"flow_title": "Elgato Key Light: {serial_number}",
"step": {
diff --git a/homeassistant/components/elgato/translations/hu.json b/homeassistant/components/elgato/translations/hu.json
index 3c69fd4562a2e4..ef6404bd92d5c0 100644
--- a/homeassistant/components/elgato/translations/hu.json
+++ b/homeassistant/components/elgato/translations/hu.json
@@ -1,12 +1,13 @@
{
"config": {
"abort": {
- "already_configured": "Ez az Elgato Key Light eszk\u00f6z m\u00e1r konfigur\u00e1lva van.",
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
+ "flow_title": "Elgato Key Light: {serial_number}",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/elgato/translations/id.json b/homeassistant/components/elgato/translations/id.json
new file mode 100644
index 00000000000000..b06691b9453cdd
--- /dev/null
+++ b/homeassistant/components/elgato/translations/id.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "flow_title": "Elgato Key Light: {serial_number}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ },
+ "description": "Siapkan Elgato Key Light Anda untuk diintegrasikan dengan Home Assistant."
+ },
+ "zeroconf_confirm": {
+ "description": "Ingin menambahkan Elgato Key Light dengan nomor seri `{serial_number}` ke Home Assistant?",
+ "title": "Perangkat Elgato Key Light yang ditemukan"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/elgato/translations/ko.json b/homeassistant/components/elgato/translations/ko.json
index d11b106e28b4a2..2d3c7111b2e543 100644
--- a/homeassistant/components/elgato/translations/ko.json
+++ b/homeassistant/components/elgato/translations/ko.json
@@ -1,7 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Elgato Key Light \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
"flow_title": "Elgato Key Light: {serial_number}",
"step": {
@@ -10,10 +14,10 @@
"host": "\ud638\uc2a4\ud2b8",
"port": "\ud3ec\ud2b8"
},
- "description": "Home Assistant \uc5d0 Elgato Key Light \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4."
+ "description": "Home Assistant\uc5d0 Elgato Key Light \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4."
},
"zeroconf_confirm": {
- "description": "Elgato Key Light \uc2dc\ub9ac\uc5bc \ubc88\ud638 `{serial_number}` \uc744(\ub97c) Home Assistant \uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "description": "\uc2dc\ub9ac\uc5bc \ubc88\ud638 `{serial_number}`\uc758 Elgato Key Light\ub97c Home Assistant\uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "\ubc1c\uacac\ub41c Elgato Key Light \uae30\uae30"
}
}
diff --git a/homeassistant/components/elgato/translations/nl.json b/homeassistant/components/elgato/translations/nl.json
index 81035cc898ffbf..fcda6a7ca84113 100644
--- a/homeassistant/components/elgato/translations/nl.json
+++ b/homeassistant/components/elgato/translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Dit Elgato Key Light apparaat is al geconfigureerd.",
+ "already_configured": "Apparaat is al geconfigureerd",
"cannot_connect": "Kan geen verbinding maken"
},
"error": {
@@ -11,8 +11,8 @@
"step": {
"user": {
"data": {
- "host": "Hostnaam of IP-adres",
- "port": "Poortnummer"
+ "host": "Host",
+ "port": "Poort"
},
"description": "Stel uw Elgato Key Light in om te integreren met Home Assistant."
},
diff --git a/homeassistant/components/elgato/translations/tr.json b/homeassistant/components/elgato/translations/tr.json
new file mode 100644
index 00000000000000..b2d1753fd68b32
--- /dev/null
+++ b/homeassistant/components/elgato/translations/tr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/elgato/translations/uk.json b/homeassistant/components/elgato/translations/uk.json
new file mode 100644
index 00000000000000..978ff1a310088f
--- /dev/null
+++ b/homeassistant/components/elgato/translations/uk.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "flow_title": "Elgato Key Light: {serial_number}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Elgato Key Light \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Home Assistant."
+ },
+ "zeroconf_confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 Elgato Key Light \u0437 \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serial_number}`?",
+ "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Elgato Key Light"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py
index b3d56e42325e0d..a4d812850f70bd 100644
--- a/homeassistant/components/eliqonline/sensor.py
+++ b/homeassistant/components/eliqonline/sensor.py
@@ -6,11 +6,10 @@
import eliqonline
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, POWER_WATT
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -52,7 +51,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([EliqSensor(api, channel_id, name)], True)
-class EliqSensor(Entity):
+class EliqSensor(SensorEntity):
"""Implementation of an ELIQ Online sensor."""
def __init__(self, api, channel_id, name):
diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py
index e33e1722edfce4..568b3109227cf7 100644
--- a/homeassistant/components/elkm1/__init__.py
+++ b/homeassistant/components/elkm1/__init__.py
@@ -13,6 +13,7 @@
CONF_HOST,
CONF_INCLUDE,
CONF_PASSWORD,
+ CONF_PREFIX,
CONF_TEMPERATURE_UNIT,
CONF_USERNAME,
TEMP_CELSIUS,
@@ -38,7 +39,6 @@
CONF_KEYPAD,
CONF_OUTPUT,
CONF_PLC,
- CONF_PREFIX,
CONF_SETTING,
CONF_TASK,
CONF_THERMOSTAT,
@@ -52,7 +52,7 @@
_LOGGER = logging.getLogger(__name__)
-SUPPORTED_DOMAINS = [
+PLATFORMS = [
"alarm_control_panel",
"climate",
"light",
@@ -197,7 +197,6 @@ def _async_find_matching_config_entry(hass, prefix):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Elk-M1 Control from a config entry."""
-
conf = entry.data
_LOGGER.debug("Setting up elkm1 %s", conf["host"])
@@ -263,9 +262,9 @@ def _element_changed(element, changeset):
"keypads": {},
}
- for component in SUPPORTED_DOMAINS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -290,8 +289,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in SUPPORTED_DOMAINS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -430,7 +429,7 @@ def should_poll(self) -> bool:
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the default attributes of the element."""
return {**self._element.as_dict(), **self.initial_attrs()}
diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py
index 8f752cd9adf32e..756166c86a60df 100644
--- a/homeassistant/components/elkm1/alarm_control_panel.py
+++ b/homeassistant/components/elkm1/alarm_control_panel.py
@@ -173,7 +173,7 @@ def supported_features(self) -> int:
return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Attributes of the area."""
attrs = self.initial_attrs()
elmt = self._element
diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py
index 0248025795b3e4..86117824767525 100644
--- a/homeassistant/components/elkm1/config_flow.py
+++ b/homeassistant/components/elkm1/config_flow.py
@@ -11,6 +11,7 @@
CONF_ADDRESS,
CONF_HOST,
CONF_PASSWORD,
+ CONF_PREFIX,
CONF_PROTOCOL,
CONF_TEMPERATURE_UNIT,
CONF_USERNAME,
@@ -20,8 +21,7 @@
from homeassistant.util import slugify
from . import async_wait_for_elk_to_sync
-from .const import CONF_AUTO_CONFIGURE, CONF_PREFIX
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import CONF_AUTO_CONFIGURE, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -50,7 +50,6 @@ async def validate_input(data):
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
-
userid = data.get(CONF_USERNAME)
password = data.get(CONF_PASSWORD)
diff --git a/homeassistant/components/elkm1/const.py b/homeassistant/components/elkm1/const.py
index 71646582c99732..4d2dac4b1de1cf 100644
--- a/homeassistant/components/elkm1/const.py
+++ b/homeassistant/components/elkm1/const.py
@@ -3,7 +3,7 @@
from elkm1_lib.const import Max
import voluptuous as vol
-from homeassistant.const import ATTR_CODE
+from homeassistant.const import ATTR_CODE, CONF_ZONE
DOMAIN = "elkm1"
@@ -17,8 +17,6 @@
CONF_SETTING = "setting"
CONF_TASK = "task"
CONF_THERMOSTAT = "thermostat"
-CONF_ZONE = "zone"
-CONF_PREFIX = "prefix"
BARE_TEMP_FAHRENHEIT = "F"
diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py
index c6442af2e44c91..e33196a08c07b2 100644
--- a/homeassistant/components/elkm1/sensor.py
+++ b/homeassistant/components/elkm1/sensor.py
@@ -8,6 +8,7 @@
from elkm1_lib.util import pretty_const, username
import voluptuous as vol
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import VOLT
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform
@@ -67,7 +68,7 @@ def temperature_to_state(temperature, undefined_temperature):
return temperature if temperature > undefined_temperature else None
-class ElkSensor(ElkAttachedEntity):
+class ElkSensor(ElkAttachedEntity, SensorEntity):
"""Base representation of Elk-M1 sensor."""
def __init__(self, element, elk, elk_data):
@@ -136,7 +137,7 @@ def icon(self):
return "mdi:thermometer-lines"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Attributes of the sensor."""
attrs = self.initial_attrs()
attrs["area"] = self._element.area + 1
@@ -163,7 +164,7 @@ def icon(self):
return "mdi:home"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Attributes of the sensor."""
attrs = self.initial_attrs()
attrs["system_trouble_status"] = self._element.system_trouble_status
@@ -190,7 +191,7 @@ def _element_changed(self, element, changeset):
self._state = self._element.value
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Attributes of the sensor."""
attrs = self.initial_attrs()
attrs["value_format"] = SettingFormat(self._element.value_format).name.lower()
@@ -227,7 +228,7 @@ def icon(self):
return f"mdi:{zone_icons.get(self._element.definition, 'alarm-bell')}"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Attributes of the sensor."""
attrs = self.initial_attrs()
attrs["physical_status"] = ZonePhysicalStatus(
diff --git a/homeassistant/components/elkm1/translations/de.json b/homeassistant/components/elkm1/translations/de.json
index 8c562a7502659c..8157a061d82d5f 100644
--- a/homeassistant/components/elkm1/translations/de.json
+++ b/homeassistant/components/elkm1/translations/de.json
@@ -5,7 +5,7 @@
"already_configured": "Ein ElkM1 mit diesem Pr\u00e4fix ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
diff --git a/homeassistant/components/elkm1/translations/he.json b/homeassistant/components/elkm1/translations/he.json
new file mode 100644
index 00000000000000..ac90b3264eab33
--- /dev/null
+++ b/homeassistant/components/elkm1/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/elkm1/translations/hu.json b/homeassistant/components/elkm1/translations/hu.json
index dee4ed9ee0fa4d..83862dfb75f243 100644
--- a/homeassistant/components/elkm1/translations/hu.json
+++ b/homeassistant/components/elkm1/translations/hu.json
@@ -1,9 +1,15 @@
{
"config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
"step": {
"user": {
"data": {
"password": "Jelsz\u00f3",
+ "protocol": "Protokoll",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
}
}
diff --git a/homeassistant/components/elkm1/translations/id.json b/homeassistant/components/elkm1/translations/id.json
new file mode 100644
index 00000000000000..e7ddd3cf9ee4e7
--- /dev/null
+++ b/homeassistant/components/elkm1/translations/id.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "address_already_configured": "ElkM1 dengan alamat ini sudah dikonfigurasi",
+ "already_configured": "ElkM1 dengan prefiks ini sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "address": "Alamat IP atau domain atau port serial jika terhubung melalui serial.",
+ "password": "Kata Sandi",
+ "prefix": "Prefiks unik (kosongkan jika hanya ada satu ElkM1).",
+ "protocol": "Protokol",
+ "temperature_unit": "Unit suhu yang digunakan ElkM1.",
+ "username": "Nama Pengguna"
+ },
+ "description": "String alamat harus dalam format 'alamat[:port]' untuk 'aman' dan 'tidak aman'. Misalnya, '192.168.1.1'. Port bersifat opsional dan nilai baku adalah 2101 untuk 'tidak aman' dan 2601 untuk 'aman'. Untuk protokol serial, alamat harus dalam format 'tty[:baud]'. Misalnya, '/dev/ttyS1'. Baud bersifat opsional dan nilai bakunya adalah 115200.",
+ "title": "Hubungkan ke Kontrol Elk-M1"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/elkm1/translations/ko.json b/homeassistant/components/elkm1/translations/ko.json
index d074946b7ad067..507f741676a35e 100644
--- a/homeassistant/components/elkm1/translations/ko.json
+++ b/homeassistant/components/elkm1/translations/ko.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "address_already_configured": "\uc774 \uc8fc\uc18c\ub85c ElkM1 \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "already_configured": "\uc774 \uc811\ub450\uc0ac\ub85c ElkM1 \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "address_already_configured": "\uc774 \uc8fc\uc18c\ub85c ElkM1\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_configured": "\uc774 \uc811\ub450\uc0ac\ub85c ElkM1\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
@@ -19,7 +19,7 @@
"temperature_unit": "ElkM1 \uc774 \uc0ac\uc6a9\ud558\ub294 \uc628\ub3c4 \ub2e8\uc704.",
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
},
- "description": "\uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 '\ubcf4\uc548' \ubc0f '\ube44\ubcf4\uc548'\uc5d0 \ub300\ud574 'address[:port]' \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: '192.168.1.1'. \ud3ec\ud2b8\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 '\ube44\ubcf4\uc548' \uc758 \uacbd\uc6b0 2101 \uc774\uace0 '\ubcf4\uc548' \uc758 \uacbd\uc6b0 2601 \uc785\ub2c8\ub2e4. \uc2dc\ub9ac\uc5bc \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 \uc8fc\uc18c\ub294 'tty[:baud]' \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: '/dev/ttyS1'. \ud1b5\uc2e0\uc18d\ub3c4 \ubc14\uc6b0\ub4dc\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 115200 \uc785\ub2c8\ub2e4.",
+ "description": "\uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 '\ubcf4\uc548' \ubc0f '\ube44\ubcf4\uc548'\uc5d0 \ub300\ud574 'address[:port]' \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: '192.168.1.1'. \ud3ec\ud2b8\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 '\ube44\ubcf4\uc548' \uc758 \uacbd\uc6b0 2101 \uc774\uace0 '\ubcf4\uc548' \uc758 \uacbd\uc6b0 2601 \uc785\ub2c8\ub2e4. \uc2dc\ub9ac\uc5bc \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 \uc8fc\uc18c\ub294 'tty[:baud]' \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: '/dev/ttyS1'. \uc804\uc1a1 \uc18d\ub3c4\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 115200 \uc785\ub2c8\ub2e4.",
"title": "Elk-M1 \uc81c\uc5b4\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
diff --git a/homeassistant/components/elkm1/translations/nl.json b/homeassistant/components/elkm1/translations/nl.json
index 9e7adf71c4b14e..de51e67b2062c9 100644
--- a/homeassistant/components/elkm1/translations/nl.json
+++ b/homeassistant/components/elkm1/translations/nl.json
@@ -5,7 +5,7 @@
"already_configured": "Een ElkM1 met dit voorvoegsel is al geconfigureerd"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
@@ -13,11 +13,11 @@
"user": {
"data": {
"address": "Het IP-adres of domein of seri\u00eble poort bij verbinding via serieel.",
- "password": "Wachtwoord (alleen beveiligd).",
+ "password": "Wachtwoord",
"prefix": "Een uniek voorvoegsel (laat dit leeg als u maar \u00e9\u00e9n ElkM1 heeft).",
"protocol": "Protocol",
"temperature_unit": "De temperatuureenheid die ElkM1 gebruikt.",
- "username": "Gebruikersnaam (alleen beveiligd)."
+ "username": "Gebruikersnaam"
},
"description": "De adresreeks moet de vorm 'adres [: poort]' hebben voor 'veilig' en 'niet-beveiligd'. Voorbeeld: '192.168.1.1'. De poort is optioneel en is standaard 2101 voor 'niet beveiligd' en 2601 voor 'beveiligd'. Voor het seri\u00eble protocol moet het adres de vorm 'tty [: baud]' hebben. Voorbeeld: '/ dev / ttyS1'. De baud is optioneel en is standaard ingesteld op 115200.",
"title": "Maak verbinding met Elk-M1 Control"
diff --git a/homeassistant/components/elkm1/translations/ru.json b/homeassistant/components/elkm1/translations/ru.json
index 3c84b98b0cade9..954722ecf5267e 100644
--- a/homeassistant/components/elkm1/translations/ru.json
+++ b/homeassistant/components/elkm1/translations/ru.json
@@ -6,7 +6,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
@@ -17,7 +17,7 @@
"prefix": "\u0423\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u0435\u0444\u0438\u043a\u0441 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0435\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d ElkM1)",
"protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b",
"temperature_unit": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u0421\u0442\u0440\u043e\u043a\u0430 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'addres[:port]' \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u043e\u0432 'secure' \u0438 'non-secure' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: '192.168.1.1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'port' \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u043d \u0440\u0430\u0432\u0435\u043d 2101 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'non-secure' \u0438 2601 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'secure'. \u0414\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'serial' \u0430\u0434\u0440\u0435\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'tty[:baud]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: '/dev/ttyS1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'baud' \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u043d \u0440\u0430\u0432\u0435\u043d 115200.",
"title": "Elk-M1 Control"
diff --git a/homeassistant/components/elkm1/translations/tr.json b/homeassistant/components/elkm1/translations/tr.json
new file mode 100644
index 00000000000000..9259220985bb1b
--- /dev/null
+++ b/homeassistant/components/elkm1/translations/tr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "address_already_configured": "Bu adrese sahip bir ElkM1 zaten yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r",
+ "already_configured": "Bu \u00f6nek ile bir ElkM1 zaten yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/elkm1/translations/uk.json b/homeassistant/components/elkm1/translations/uk.json
new file mode 100644
index 00000000000000..a8e711a4590822
--- /dev/null
+++ b/homeassistant/components/elkm1/translations/uk.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "address_already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 \u0446\u0456\u0454\u044e \u0430\u0434\u0440\u0435\u0441\u043e\u044e \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435.",
+ "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 \u0446\u0438\u043c \u043f\u0440\u0435\u0444\u0456\u043a\u0441\u043e\u043c \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430, \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e \u043f\u043e\u0441\u043b\u0456\u0434\u043e\u0432\u043d\u0438\u0439 \u043f\u043e\u0440\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "prefix": "\u0423\u043d\u0456\u043a\u0430\u043b\u044c\u043d\u0438\u0439 \u043f\u0440\u0435\u0444\u0456\u043a\u0441 (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c, \u044f\u043a\u0449\u043e \u0443 \u0412\u0430\u0441 \u0442\u0456\u043b\u044c\u043a\u0438 \u043e\u0434\u0438\u043d ElkM1)",
+ "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b",
+ "temperature_unit": "\u041e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u0443 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u0420\u044f\u0434\u043e\u043a \u0430\u0434\u0440\u0435\u0441\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0430 \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'addres[:port]' \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0456\u0432 'secure' \u0456 'non-secure' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: '192.168.1.1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'port' \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u043d \u0434\u043e\u0440\u0456\u0432\u043d\u044e\u0454 2101 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'non-secure' \u0456 2601 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'secure'. \u0414\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'serial' \u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u043e\u0432\u0438\u043d\u043d\u0430 \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'tty[:baud]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: '/dev/ttyS1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'baud' \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u043d \u0434\u043e\u0440\u0456\u0432\u043d\u044e\u0454 115200.",
+ "title": "Elk-M1 Control"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/elv/switch.py b/homeassistant/components/elv/switch.py
index 12b21c23d1a75f..48eb9675277b4e 100644
--- a/homeassistant/components/elv/switch.py
+++ b/homeassistant/components/elv/switch.py
@@ -74,7 +74,7 @@ def turn_off(self, **kwargs):
self._pca.turn_off(self._device_id)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
return self._emeter_params
diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py
index 1cbc893f98bca7..5656a1f14868e3 100644
--- a/homeassistant/components/emby/media_player.py
+++ b/homeassistant/components/emby/media_player.py
@@ -96,12 +96,13 @@ def device_update_callback(data):
active_emby_devices[dev_id] = new
new_devices.append(new)
- elif dev_id in inactive_emby_devices:
- if emby.devices[dev_id].state != "Off":
- add = inactive_emby_devices.pop(dev_id)
- active_emby_devices[dev_id] = add
- _LOGGER.debug("Showing %s, item: %s", dev_id, add)
- add.set_available(True)
+ elif (
+ dev_id in inactive_emby_devices and emby.devices[dev_id].state != "Off"
+ ):
+ add = inactive_emby_devices.pop(dev_id)
+ active_emby_devices[dev_id] = add
+ _LOGGER.debug("Showing %s, item: %s", dev_id, add)
+ add.set_available(True)
if new_devices:
_LOGGER.debug("Adding new devices: %s", new_devices)
diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py
index dca9c870022e1b..bfc86db387ef3f 100644
--- a/homeassistant/components/emoncms/sensor.py
+++ b/homeassistant/components/emoncms/sensor.py
@@ -5,7 +5,7 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_API_KEY,
CONF_ID,
@@ -19,7 +19,6 @@
)
from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -93,13 +92,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for elem in data.data:
- if exclude_feeds is not None:
- if int(elem["id"]) in exclude_feeds:
- continue
+ if exclude_feeds is not None and int(elem["id"]) in exclude_feeds:
+ continue
- if include_only_feeds is not None:
- if int(elem["id"]) not in include_only_feeds:
- continue
+ if include_only_feeds is not None and int(elem["id"]) not in include_only_feeds:
+ continue
name = None
if sensor_names is not None:
@@ -125,7 +122,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors)
-class EmonCmsSensor(Entity):
+class EmonCmsSensor(SensorEntity):
"""Implementation of an Emoncms sensor."""
def __init__(
@@ -175,7 +172,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the attributes of the sensor."""
return {
ATTR_FEEDID: self._elem["id"],
diff --git a/homeassistant/components/emonitor/__init__.py b/homeassistant/components/emonitor/__init__.py
new file mode 100644
index 00000000000000..74630a193a406b
--- /dev/null
+++ b/homeassistant/components/emonitor/__init__.py
@@ -0,0 +1,67 @@
+"""The SiteSage Emonitor integration."""
+import asyncio
+from datetime import timedelta
+import logging
+
+from aioemonitor import Emonitor
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import aiohttp_client
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+DEFAULT_UPDATE_RATE = 60
+
+PLATFORMS = ["sensor"]
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up SiteSage Emonitor from a config entry."""
+
+ session = aiohttp_client.async_get_clientsession(hass)
+ emonitor = Emonitor(entry.data[CONF_HOST], session)
+
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name=entry.title,
+ update_method=emonitor.async_get_status,
+ update_interval=timedelta(seconds=DEFAULT_UPDATE_RATE),
+ )
+
+ await coordinator.async_config_entry_first_refresh()
+
+ hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
+
+
+def name_short_mac(short_mac):
+ """Name from short mac."""
+ return f"Emonitor {short_mac}"
diff --git a/homeassistant/components/emonitor/config_flow.py b/homeassistant/components/emonitor/config_flow.py
new file mode 100644
index 00000000000000..bd5650d28cd190
--- /dev/null
+++ b/homeassistant/components/emonitor/config_flow.py
@@ -0,0 +1,105 @@
+"""Config flow for SiteSage Emonitor integration."""
+import logging
+
+from aioemonitor import Emonitor
+import aiohttp
+import voluptuous as vol
+
+from homeassistant import config_entries, core
+from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS
+from homeassistant.const import CONF_HOST, CONF_NAME
+from homeassistant.helpers import aiohttp_client
+from homeassistant.helpers.device_registry import format_mac
+
+from . import name_short_mac
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def fetch_mac_and_title(hass: core.HomeAssistant, host):
+ """Validate the user input allows us to connect."""
+ session = aiohttp_client.async_get_clientsession(hass)
+ emonitor = Emonitor(host, session)
+ status = await emonitor.async_get_status()
+ mac_address = status.network.mac_address
+ return {"title": name_short_mac(mac_address[-6:]), "mac_address": mac_address}
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for SiteSage Emonitor."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ def __init__(self):
+ """Initialize Emonitor ConfigFlow."""
+ self.discovered_ip = None
+ self.discovered_info = None
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ errors = {}
+ if user_input is not None:
+ try:
+ info = await fetch_mac_and_title(self.hass, user_input[CONF_HOST])
+ except aiohttp.ClientError:
+ errors[CONF_HOST] = "cannot_connect"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+ else:
+ await self.async_set_unique_id(
+ format_mac(info["mac_address"]), raise_on_progress=False
+ )
+ self._abort_if_unique_id_configured()
+ return self.async_create_entry(title=info["title"], data=user_input)
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {vol.Required("host", default=self.discovered_ip): str}
+ ),
+ errors=errors,
+ )
+
+ async def async_step_dhcp(self, dhcp_discovery):
+ """Handle dhcp discovery."""
+ self.discovered_ip = dhcp_discovery[IP_ADDRESS]
+ await self.async_set_unique_id(format_mac(dhcp_discovery[MAC_ADDRESS]))
+ self._abort_if_unique_id_configured(updates={CONF_HOST: self.discovered_ip})
+ name = name_short_mac(short_mac(dhcp_discovery[MAC_ADDRESS]))
+ self.context["title_placeholders"] = {"name": name}
+ try:
+ self.discovered_info = await fetch_mac_and_title(
+ self.hass, self.discovered_ip
+ )
+ except Exception as ex: # pylint: disable=broad-except
+ _LOGGER.debug(
+ "Unable to fetch status, falling back to manual entry", exc_info=ex
+ )
+ return await self.async_step_user()
+ return await self.async_step_confirm()
+
+ async def async_step_confirm(self, user_input=None):
+ """Attempt to confim."""
+ if user_input is not None:
+ return self.async_create_entry(
+ title=self.discovered_info["title"],
+ data={CONF_HOST: self.discovered_ip},
+ )
+
+ self._set_confirm_only()
+ self.context["title_placeholders"] = {"name": self.discovered_info["title"]}
+ return self.async_show_form(
+ step_id="confirm",
+ description_placeholders={
+ CONF_HOST: self.discovered_ip,
+ CONF_NAME: self.discovered_info["title"],
+ },
+ )
+
+
+def short_mac(mac):
+ """Short version of the mac."""
+ return "".join(mac.split(":")[3:]).upper()
diff --git a/homeassistant/components/emonitor/const.py b/homeassistant/components/emonitor/const.py
new file mode 100644
index 00000000000000..e39aea462843bf
--- /dev/null
+++ b/homeassistant/components/emonitor/const.py
@@ -0,0 +1,3 @@
+"""Constants for the SiteSage Emonitor integration."""
+
+DOMAIN = "emonitor"
diff --git a/homeassistant/components/emonitor/manifest.json b/homeassistant/components/emonitor/manifest.json
new file mode 100644
index 00000000000000..b6cf3526bd8edb
--- /dev/null
+++ b/homeassistant/components/emonitor/manifest.json
@@ -0,0 +1,13 @@
+{
+ "domain": "emonitor",
+ "name": "SiteSage Emonitor",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/emonitor",
+ "requirements": [
+ "aioemonitor==1.0.5"
+ ],
+ "dhcp": [{"hostname":"emonitor*","macaddress":"0090C2*"}],
+ "codeowners": [
+ "@bdraco"
+ ]
+}
\ No newline at end of file
diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py
new file mode 100644
index 00000000000000..3b075f7cbaacf0
--- /dev/null
+++ b/homeassistant/components/emonitor/sensor.py
@@ -0,0 +1,108 @@
+"""Support for a Emonitor channel sensor."""
+
+from aioemonitor.monitor import EmonitorChannel
+
+from homeassistant.components.sensor import DEVICE_CLASS_POWER, SensorEntity
+from homeassistant.const import POWER_WATT
+from homeassistant.helpers import device_registry as dr
+from homeassistant.helpers.typing import StateType
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+)
+
+from . import name_short_mac
+from .const import DOMAIN
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up entry."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]
+ channels = coordinator.data.channels
+ entities = []
+ seen_channels = set()
+ for channel_number, channel in channels.items():
+ seen_channels.add(channel_number)
+ if not channel.active:
+ continue
+ if channel.paired_with_channel in seen_channels:
+ continue
+
+ entities.append(EmonitorPowerSensor(coordinator, channel_number))
+
+ async_add_entities(entities)
+
+
+class EmonitorPowerSensor(CoordinatorEntity, SensorEntity):
+ """Representation of an Emonitor power sensor entity."""
+
+ def __init__(self, coordinator: DataUpdateCoordinator, channel_number: int):
+ """Initialize the channel sensor."""
+ self.channel_number = channel_number
+ super().__init__(coordinator)
+
+ @property
+ def unique_id(self) -> str:
+ """Channel unique id."""
+ return f"{self.mac_address}_{self.channel_number}"
+
+ @property
+ def channel_data(self) -> EmonitorChannel:
+ """Channel data."""
+ return self.coordinator.data.channels[self.channel_number]
+
+ @property
+ def paired_channel_data(self) -> EmonitorChannel:
+ """Channel data."""
+ return self.coordinator.data.channels[self.channel_data.paired_with_channel]
+
+ @property
+ def name(self) -> str:
+ """Name of the sensor."""
+ return self.channel_data.label
+
+ @property
+ def unit_of_measurement(self) -> str:
+ """Return the unit of measurement."""
+ return POWER_WATT
+
+ @property
+ def device_class(self) -> str:
+ """Device class of the sensor."""
+ return DEVICE_CLASS_POWER
+
+ def _paired_attr(self, attr_name: str) -> float:
+ """Cumulative attributes for channel and paired channel."""
+ attr_val = getattr(self.channel_data, attr_name)
+ if self.channel_data.paired_with_channel:
+ attr_val += getattr(self.paired_channel_data, attr_name)
+ return attr_val
+
+ @property
+ def state(self) -> StateType:
+ """State of the sensor."""
+ return self._paired_attr("inst_power")
+
+ @property
+ def extra_state_attributes(self) -> dict:
+ """Return the device specific state attributes."""
+ return {
+ "channel": self.channel_number,
+ "avg_power": self._paired_attr("avg_power"),
+ "max_power": self._paired_attr("max_power"),
+ }
+
+ @property
+ def mac_address(self) -> str:
+ """Return the mac address of the device."""
+ return self.coordinator.data.network.mac_address
+
+ @property
+ def device_info(self) -> dict:
+ """Return info about the emonitor device."""
+ return {
+ "name": name_short_mac(self.mac_address[-6:]),
+ "connections": {(dr.CONNECTION_NETWORK_MAC, self.mac_address)},
+ "manufacturer": "Powerhouse Dynamics, Inc.",
+ "sw_version": self.coordinator.data.hardware.firmware_version,
+ }
diff --git a/homeassistant/components/emonitor/strings.json b/homeassistant/components/emonitor/strings.json
new file mode 100644
index 00000000000000..aac15dfaae2575
--- /dev/null
+++ b/homeassistant/components/emonitor/strings.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "flow_title": "SiteSage {name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "[%key:common::config_flow::data::host%]"
+ }
+ },
+ "confirm": {
+ "title": "Setup SiteSage Emonitor",
+ "description": "Do you want to setup {name} ({host})?"
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ }
+ }
+}
diff --git a/homeassistant/components/emonitor/translations/ca.json b/homeassistant/components/emonitor/translations/ca.json
new file mode 100644
index 00000000000000..b6fd1f99c849d3
--- /dev/null
+++ b/homeassistant/components/emonitor/translations/ca.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "unknown": "Error inesperat"
+ },
+ "flow_title": "SiteSage {name}",
+ "step": {
+ "confirm": {
+ "description": "Vols configurar {name} ({host})?",
+ "title": "Configura SiteSage Emonitor"
+ },
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/emonitor/translations/de.json b/homeassistant/components/emonitor/translations/de.json
new file mode 100644
index 00000000000000..6abbe1b2b276ab
--- /dev/null
+++ b/homeassistant/components/emonitor/translations/de.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/emonitor/translations/en.json b/homeassistant/components/emonitor/translations/en.json
new file mode 100644
index 00000000000000..6e24bbac7a3b97
--- /dev/null
+++ b/homeassistant/components/emonitor/translations/en.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "unknown": "Unexpected error"
+ },
+ "flow_title": "SiteSage {name}",
+ "step": {
+ "confirm": {
+ "description": "Do you want to setup {name} ({host})?",
+ "title": "Setup SiteSage Emonitor"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/emonitor/translations/et.json b/homeassistant/components/emonitor/translations/et.json
new file mode 100644
index 00000000000000..bea6607a9cad2c
--- /dev/null
+++ b/homeassistant/components/emonitor/translations/et.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "unknown": "Tundmatu viga"
+ },
+ "flow_title": "SiteSage {name}",
+ "step": {
+ "confirm": {
+ "description": "Kas soovid seadistada {name}({host})?",
+ "title": "SiteSage Emonitori seadistamine"
+ },
+ "user": {
+ "data": {
+ "host": ""
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/emonitor/translations/hu.json b/homeassistant/components/emonitor/translations/hu.json
new file mode 100644
index 00000000000000..2d7d4218e7de8a
--- /dev/null
+++ b/homeassistant/components/emonitor/translations/hu.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "flow_title": "SiteSage {name}",
+ "step": {
+ "confirm": {
+ "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?",
+ "title": "A SiteSage Emonitor be\u00e1ll\u00edt\u00e1sa"
+ },
+ "user": {
+ "data": {
+ "host": "Hoszt"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/emonitor/translations/it.json b/homeassistant/components/emonitor/translations/it.json
new file mode 100644
index 00000000000000..7a194a301a54bd
--- /dev/null
+++ b/homeassistant/components/emonitor/translations/it.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "unknown": "Errore imprevisto"
+ },
+ "flow_title": "SiteSage {name}",
+ "step": {
+ "confirm": {
+ "description": "Vuoi impostare {name} ({host})?",
+ "title": "Imposta SiteSage Emonitor"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/emonitor/translations/ko.json b/homeassistant/components/emonitor/translations/ko.json
new file mode 100644
index 00000000000000..36e9fa7a04c7cd
--- /dev/null
+++ b/homeassistant/components/emonitor/translations/ko.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "flow_title": "SiteSage {name}",
+ "step": {
+ "confirm": {
+ "description": "{name} ({host})\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "SiteSage eMonitor \uc124\uc815\ud558\uae30"
+ },
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/emonitor/translations/nl.json b/homeassistant/components/emonitor/translations/nl.json
new file mode 100644
index 00000000000000..742656c8e9259f
--- /dev/null
+++ b/homeassistant/components/emonitor/translations/nl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "unknown": "Onverwachte fout"
+ },
+ "flow_title": "SiteSage {name}",
+ "step": {
+ "confirm": {
+ "description": "Wilt u {name} ( {host} ) instellen?",
+ "title": "SiteSage Emonitor instellen"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/emonitor/translations/no.json b/homeassistant/components/emonitor/translations/no.json
new file mode 100644
index 00000000000000..866602d854b30e
--- /dev/null
+++ b/homeassistant/components/emonitor/translations/no.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "unknown": "Uventet feil"
+ },
+ "flow_title": "SiteSage {name}",
+ "step": {
+ "confirm": {
+ "description": "Vil du konfigurere {name} ({host})?",
+ "title": "Konfigurer SiteSage Emonitor"
+ },
+ "user": {
+ "data": {
+ "host": "Vert"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/emonitor/translations/pl.json b/homeassistant/components/emonitor/translations/pl.json
new file mode 100644
index 00000000000000..a5b250c3f4d779
--- /dev/null
+++ b/homeassistant/components/emonitor/translations/pl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "flow_title": "SiteSage {name}",
+ "step": {
+ "confirm": {
+ "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?",
+ "title": "Konfiguracja SiteSage Emonitor"
+ },
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/emonitor/translations/ru.json b/homeassistant/components/emonitor/translations/ru.json
new file mode 100644
index 00000000000000..e9ae6b12e86ae1
--- /dev/null
+++ b/homeassistant/components/emonitor/translations/ru.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "flow_title": "SiteSage {name}",
+ "step": {
+ "confirm": {
+ "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?",
+ "title": "SiteSage Emonitor"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/emonitor/translations/zh-Hant.json b/homeassistant/components/emonitor/translations/zh-Hant.json
new file mode 100644
index 00000000000000..371cf7575423f2
--- /dev/null
+++ b/homeassistant/components/emonitor/translations/zh-Hant.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "flow_title": "SiteSage {name}",
+ "step": {
+ "confirm": {
+ "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f",
+ "title": "\u8a2d\u5b9a SiteSage Emonitor"
+ },
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py
index b4a49c7efcdc14..3864a2651f8d43 100644
--- a/homeassistant/components/emulated_hue/__init__.py
+++ b/homeassistant/components/emulated_hue/__init__.py
@@ -1,11 +1,17 @@
"""Support for local control of entities by emulating a Philips Hue bridge."""
+from contextlib import suppress
import logging
from aiohttp import web
import voluptuous as vol
from homeassistant import util
-from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
+from homeassistant.const import (
+ CONF_ENTITIES,
+ CONF_TYPE,
+ EVENT_HOMEASSISTANT_START,
+ EVENT_HOMEASSISTANT_STOP,
+)
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json, save_json
@@ -31,7 +37,6 @@
CONF_ADVERTISE_IP = "advertise_ip"
CONF_ADVERTISE_PORT = "advertise_port"
-CONF_ENTITIES = "entities"
CONF_ENTITY_HIDDEN = "hidden"
CONF_ENTITY_NAME = "name"
CONF_EXPOSE_BY_DEFAULT = "expose_by_default"
@@ -40,7 +45,6 @@
CONF_LIGHTS_ALL_DIMMABLE = "lights_all_dimmable"
CONF_LISTEN_PORT = "listen_port"
CONF_OFF_MAPS_TO_ON_DOMAINS = "off_maps_to_on_domains"
-CONF_TYPE = "type"
CONF_UPNP_BIND_MULTICAST = "upnp_bind_multicast"
TYPE_ALEXA = "alexa"
@@ -338,8 +342,6 @@ def _is_entity_exposed(self, entity):
def _load_json(filename):
"""Load JSON, handling invalid syntax."""
- try:
+ with suppress(HomeAssistantError):
return load_json(filename)
- except HomeAssistantError:
- pass
return {}
diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py
index 51580a28adf710..f97636a46c038f 100644
--- a/homeassistant/components/emulated_hue/hue_api.py
+++ b/homeassistant/components/emulated_hue/hue_api.py
@@ -43,9 +43,12 @@
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
+ ATTR_TRANSITION,
+ ATTR_XY_COLOR,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
+ SUPPORT_TRANSITION,
)
from homeassistant.components.media_player.const import (
ATTR_MEDIA_VOLUME_LEVEL,
@@ -82,6 +85,8 @@
STATE_HUE = "hue"
STATE_SATURATION = "sat"
STATE_COLOR_TEMP = "ct"
+STATE_TRANSITON = "tt"
+STATE_XY = "xy"
# Hue API states, defined separately in case they change
HUE_API_STATE_ON = "on"
@@ -90,7 +95,9 @@
HUE_API_STATE_HUE = "hue"
HUE_API_STATE_SAT = "sat"
HUE_API_STATE_CT = "ct"
+HUE_API_STATE_XY = "xy"
HUE_API_STATE_EFFECT = "effect"
+HUE_API_STATE_TRANSITION = "transitiontime"
# Hue API min/max values - https://developers.meethue.com/develop/hue-api/lights-api/
HUE_API_STATE_BRI_MIN = 1 # Brightness
@@ -357,6 +364,8 @@ async def put(self, request, username, entity_number):
STATE_HUE: None,
STATE_SATURATION: None,
STATE_COLOR_TEMP: None,
+ STATE_XY: None,
+ STATE_TRANSITON: None,
}
if HUE_API_STATE_ON in request_json:
@@ -372,6 +381,7 @@ async def put(self, request, username, entity_number):
(HUE_API_STATE_HUE, STATE_HUE),
(HUE_API_STATE_SAT, STATE_SATURATION),
(HUE_API_STATE_CT, STATE_COLOR_TEMP),
+ (HUE_API_STATE_TRANSITION, STATE_TRANSITON),
):
if key in request_json:
try:
@@ -379,6 +389,15 @@ async def put(self, request, username, entity_number):
except ValueError:
_LOGGER.error("Unable to parse data (2): %s", request_json)
return self.json_message("Bad request", HTTP_BAD_REQUEST)
+ if HUE_API_STATE_XY in request_json:
+ try:
+ parsed[STATE_XY] = (
+ float(request_json[HUE_API_STATE_XY][0]),
+ float(request_json[HUE_API_STATE_XY][1]),
+ )
+ except ValueError:
+ _LOGGER.error("Unable to parse data (2): %s", request_json)
+ return self.json_message("Bad request", HTTP_BAD_REQUEST)
if HUE_API_STATE_BRI in request_json:
if entity.domain == light.DOMAIN:
@@ -420,11 +439,13 @@ async def put(self, request, username, entity_number):
# saturation and color temp
if entity.domain == light.DOMAIN:
if parsed[STATE_ON]:
- if entity_features & SUPPORT_BRIGHTNESS:
- if parsed[STATE_BRIGHTNESS] is not None:
- data[ATTR_BRIGHTNESS] = hue_brightness_to_hass(
- parsed[STATE_BRIGHTNESS]
- )
+ if (
+ entity_features & SUPPORT_BRIGHTNESS
+ and parsed[STATE_BRIGHTNESS] is not None
+ ):
+ data[ATTR_BRIGHTNESS] = hue_brightness_to_hass(
+ parsed[STATE_BRIGHTNESS]
+ )
if entity_features & SUPPORT_COLOR:
if any((parsed[STATE_HUE], parsed[STATE_SATURATION])):
@@ -444,9 +465,20 @@ async def put(self, request, username, entity_number):
data[ATTR_HS_COLOR] = (hue, sat)
- if entity_features & SUPPORT_COLOR_TEMP:
- if parsed[STATE_COLOR_TEMP] is not None:
- data[ATTR_COLOR_TEMP] = parsed[STATE_COLOR_TEMP]
+ if parsed[STATE_XY] is not None:
+ data[ATTR_XY_COLOR] = parsed[STATE_XY]
+
+ if (
+ entity_features & SUPPORT_COLOR_TEMP
+ and parsed[STATE_COLOR_TEMP] is not None
+ ):
+ data[ATTR_COLOR_TEMP] = parsed[STATE_COLOR_TEMP]
+
+ if (
+ entity_features & SUPPORT_TRANSITION
+ and parsed[STATE_TRANSITON] is not None
+ ):
+ data[ATTR_TRANSITION] = parsed[STATE_TRANSITON] / 10
# If the requested entity is a script, add some variables
elif entity.domain == script.DOMAIN:
@@ -463,11 +495,13 @@ async def put(self, request, username, entity_number):
# only setting the temperature
service = None
- if entity_features & SUPPORT_TARGET_TEMPERATURE:
- if parsed[STATE_BRIGHTNESS] is not None:
- domain = entity.domain
- service = SERVICE_SET_TEMPERATURE
- data[ATTR_TEMPERATURE] = parsed[STATE_BRIGHTNESS]
+ if (
+ entity_features & SUPPORT_TARGET_TEMPERATURE
+ and parsed[STATE_BRIGHTNESS] is not None
+ ):
+ domain = entity.domain
+ service = SERVICE_SET_TEMPERATURE
+ data[ATTR_TEMPERATURE] = parsed[STATE_BRIGHTNESS]
# If the requested entity is a humidifier, set the humidity
elif entity.domain == humidifier.DOMAIN:
@@ -479,43 +513,48 @@ async def put(self, request, username, entity_number):
# If the requested entity is a media player, convert to volume
elif entity.domain == media_player.DOMAIN:
- if entity_features & SUPPORT_VOLUME_SET:
- if parsed[STATE_BRIGHTNESS] is not None:
- turn_on_needed = True
- domain = entity.domain
- service = SERVICE_VOLUME_SET
- # Convert 0-100 to 0.0-1.0
- data[ATTR_MEDIA_VOLUME_LEVEL] = parsed[STATE_BRIGHTNESS] / 100.0
+ if (
+ entity_features & SUPPORT_VOLUME_SET
+ and parsed[STATE_BRIGHTNESS] is not None
+ ):
+ turn_on_needed = True
+ domain = entity.domain
+ service = SERVICE_VOLUME_SET
+ # Convert 0-100 to 0.0-1.0
+ data[ATTR_MEDIA_VOLUME_LEVEL] = parsed[STATE_BRIGHTNESS] / 100.0
# If the requested entity is a cover, convert to open_cover/close_cover
elif entity.domain == cover.DOMAIN:
domain = entity.domain
+ service = SERVICE_CLOSE_COVER
if service == SERVICE_TURN_ON:
service = SERVICE_OPEN_COVER
- else:
- service = SERVICE_CLOSE_COVER
- if entity_features & SUPPORT_SET_POSITION:
- if parsed[STATE_BRIGHTNESS] is not None:
- domain = entity.domain
- service = SERVICE_SET_COVER_POSITION
- data[ATTR_POSITION] = parsed[STATE_BRIGHTNESS]
+ if (
+ entity_features & SUPPORT_SET_POSITION
+ and parsed[STATE_BRIGHTNESS] is not None
+ ):
+ domain = entity.domain
+ service = SERVICE_SET_COVER_POSITION
+ data[ATTR_POSITION] = parsed[STATE_BRIGHTNESS]
# If the requested entity is a fan, convert to speed
- elif entity.domain == fan.DOMAIN:
- if entity_features & SUPPORT_SET_SPEED:
- if parsed[STATE_BRIGHTNESS] is not None:
- domain = entity.domain
- # Convert 0-100 to a fan speed
- brightness = parsed[STATE_BRIGHTNESS]
- if brightness == 0:
- data[ATTR_SPEED] = SPEED_OFF
- elif 0 < brightness <= 33.3:
- data[ATTR_SPEED] = SPEED_LOW
- elif 33.3 < brightness <= 66.6:
- data[ATTR_SPEED] = SPEED_MEDIUM
- elif 66.6 < brightness <= 100:
- data[ATTR_SPEED] = SPEED_HIGH
+ elif (
+ entity.domain == fan.DOMAIN
+ and entity_features & SUPPORT_SET_SPEED
+ and parsed[STATE_BRIGHTNESS] is not None
+ ):
+ domain = entity.domain
+ # Convert 0-100 to a fan speed
+ brightness = parsed[STATE_BRIGHTNESS]
+ if brightness == 0:
+ data[ATTR_SPEED] = SPEED_OFF
+ elif 0 < brightness <= 33.3:
+ data[ATTR_SPEED] = SPEED_LOW
+ elif 33.3 < brightness <= 66.6:
+ data[ATTR_SPEED] = SPEED_MEDIUM
+ elif 66.6 < brightness <= 100:
+ data[ATTR_SPEED] = SPEED_HIGH
# Map the off command to on
if entity.domain in config.off_maps_to_on_domains:
@@ -557,6 +596,8 @@ async def put(self, request, username, entity_number):
(STATE_HUE, HUE_API_STATE_HUE),
(STATE_SATURATION, HUE_API_STATE_SAT),
(STATE_COLOR_TEMP, HUE_API_STATE_CT),
+ (STATE_XY, HUE_API_STATE_XY),
+ (STATE_TRANSITON, HUE_API_STATE_TRANSITION),
):
if parsed[key] is not None:
json_response.append(
diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py
index 58f964d4984dc5..8ff7eb85b39ae3 100644
--- a/homeassistant/components/emulated_hue/upnp.py
+++ b/homeassistant/components/emulated_hue/upnp.py
@@ -63,7 +63,6 @@ def create_upnp_datagram_endpoint(
advertise_port,
):
"""Create the UPNP socket and protocol."""
-
# Listen for UDP port 1900 packets sent to SSDP multicast address
ssdp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ssdp_socket.setblocking(False)
diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json
index c3458093943c63..bb292b2e7b5886 100644
--- a/homeassistant/components/emulated_kasa/manifest.json
+++ b/homeassistant/components/emulated_kasa/manifest.json
@@ -2,7 +2,7 @@
"domain": "emulated_kasa",
"name": "Emulated Kasa",
"documentation": "https://www.home-assistant.io/integrations/emulated_kasa",
- "requirements": ["sense_energy==0.8.1"],
+ "requirements": ["sense_energy==0.9.0"],
"codeowners": ["@kbickar"],
"quality_scale": "internal"
}
diff --git a/homeassistant/components/emulated_roku/translations/de.json b/homeassistant/components/emulated_roku/translations/de.json
index a0bfd9f83aa440..39c8da5197fa1f 100644
--- a/homeassistant/components/emulated_roku/translations/de.json
+++ b/homeassistant/components/emulated_roku/translations/de.json
@@ -8,7 +8,7 @@
"data": {
"advertise_ip": "IP Adresse annoncieren",
"advertise_port": "Port annoncieren",
- "host_ip": "Host-IP",
+ "host_ip": "Host-IP-Adresse",
"listen_port": "Listen-Port",
"name": "Name",
"upnp_bind_multicast": "Multicast binden (True/False)"
diff --git a/homeassistant/components/emulated_roku/translations/hu.json b/homeassistant/components/emulated_roku/translations/hu.json
index 3d490ddbfeb125..bccfe3bdcab0d6 100644
--- a/homeassistant/components/emulated_roku/translations/hu.json
+++ b/homeassistant/components/emulated_roku/translations/hu.json
@@ -1,9 +1,12 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
"step": {
"user": {
"data": {
- "host_ip": "Hoszt IP",
+ "host_ip": "Hoszt IP c\u00edm",
"listen_port": "Port figyel\u00e9se",
"name": "N\u00e9v"
},
@@ -11,5 +14,5 @@
}
}
},
- "title": "EmulatedRoku"
+ "title": "Emulated Roku"
}
\ No newline at end of file
diff --git a/homeassistant/components/emulated_roku/translations/id.json b/homeassistant/components/emulated_roku/translations/id.json
new file mode 100644
index 00000000000000..9ffcedf5d19791
--- /dev/null
+++ b/homeassistant/components/emulated_roku/translations/id.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "advertise_ip": "Umumkan Alamat IP",
+ "advertise_port": "Umumkan Port",
+ "host_ip": "Alamat IP Host",
+ "name": "Nama",
+ "upnp_bind_multicast": "Bind multicast (True/False)"
+ },
+ "title": "Tentukan konfigurasi server"
+ }
+ }
+ },
+ "title": "Emulasi Roku"
+}
\ No newline at end of file
diff --git a/homeassistant/components/emulated_roku/translations/ko.json b/homeassistant/components/emulated_roku/translations/ko.json
index e9d1d134af2b0c..32b4202bc78d05 100644
--- a/homeassistant/components/emulated_roku/translations/ko.json
+++ b/homeassistant/components/emulated_roku/translations/ko.json
@@ -1,11 +1,14 @@
{
"config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
"step": {
"user": {
"data": {
- "advertise_ip": "\uad11\uace0 IP",
+ "advertise_ip": "\uad11\uace0 IP \uc8fc\uc18c",
"advertise_port": "\uad11\uace0 \ud3ec\ud2b8",
- "host_ip": "\ud638\uc2a4\ud2b8 IP",
+ "host_ip": "\ud638\uc2a4\ud2b8 IP \uc8fc\uc18c",
"listen_port": "\uc218\uc2e0 \ud3ec\ud2b8",
"name": "\uc774\ub984",
"upnp_bind_multicast": "\uba40\ud2f0 \uce90\uc2a4\ud2b8 \ud560\ub2f9 (\ucc38/\uac70\uc9d3)"
diff --git a/homeassistant/components/emulated_roku/translations/nl.json b/homeassistant/components/emulated_roku/translations/nl.json
index 54d544faee852c..d9510824ecf620 100644
--- a/homeassistant/components/emulated_roku/translations/nl.json
+++ b/homeassistant/components/emulated_roku/translations/nl.json
@@ -6,8 +6,8 @@
"step": {
"user": {
"data": {
- "advertise_ip": "Adverteer IP",
- "advertise_port": "Adverterenpoort",
+ "advertise_ip": "Toegekend IP-adres",
+ "advertise_port": "Toegekende Poort",
"host_ip": "Host IP",
"listen_port": "Luisterpoort",
"name": "Naam",
@@ -17,5 +17,5 @@
}
}
},
- "title": "EmulatedRoku"
+ "title": "Emulated Roku"
}
\ No newline at end of file
diff --git a/homeassistant/components/emulated_roku/translations/ru.json b/homeassistant/components/emulated_roku/translations/ru.json
index 2d4c4a7d935331..f0094930f839a3 100644
--- a/homeassistant/components/emulated_roku/translations/ru.json
+++ b/homeassistant/components/emulated_roku/translations/ru.json
@@ -13,9 +13,9 @@
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
"upnp_bind_multicast": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c multicast (True/False)"
},
- "title": "EmulatedRoku"
+ "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u0430"
}
}
},
- "title": "Emulated Roku"
+ "title": "\u042d\u043c\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 Roku"
}
\ No newline at end of file
diff --git a/homeassistant/components/emulated_roku/translations/tr.json b/homeassistant/components/emulated_roku/translations/tr.json
new file mode 100644
index 00000000000000..5307276a71d3a3
--- /dev/null
+++ b/homeassistant/components/emulated_roku/translations/tr.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/emulated_roku/translations/uk.json b/homeassistant/components/emulated_roku/translations/uk.json
new file mode 100644
index 00000000000000..a299f3a5ebc6d4
--- /dev/null
+++ b/homeassistant/components/emulated_roku/translations/uk.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "advertise_ip": "\u041e\u0433\u043e\u043b\u043e\u0448\u0443\u0432\u0430\u0442\u0438 IP",
+ "advertise_port": "\u041e\u0433\u043e\u043b\u043e\u0448\u0443\u0432\u0430\u0442\u0438 \u043f\u043e\u0440\u0442",
+ "host_ip": "\u0425\u043e\u0441\u0442",
+ "listen_port": "\u041f\u043e\u0440\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "upnp_bind_multicast": "\u041f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 multicast (True / False)"
+ },
+ "title": "EmulatedRoku"
+ }
+ }
+ },
+ "title": "Emulated Roku"
+}
\ No newline at end of file
diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py
index 4baa6aaf047cf6..1c32b1ab805272 100644
--- a/homeassistant/components/enigma2/media_player.py
+++ b/homeassistant/components/enigma2/media_player.py
@@ -251,7 +251,7 @@ def update(self):
self.e2_box.update()
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes.
isRecording: Is the box currently recording.
diff --git a/homeassistant/components/enocean/config_flow.py b/homeassistant/components/enocean/config_flow.py
index 7fce66d54e5b29..3f4309e3507556 100644
--- a/homeassistant/components/enocean/config_flow.py
+++ b/homeassistant/components/enocean/config_flow.py
@@ -7,8 +7,7 @@
from homeassistant.const import CONF_DEVICE
from . import dongle
-from .const import DOMAIN # pylint:disable=unused-import
-from .const import ERROR_INVALID_DONGLE_PATH, LOGGER
+from .const import DOMAIN, ERROR_INVALID_DONGLE_PATH, LOGGER
class EnOceanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py
index 011dcdafdb65b0..1814efb9c87bf5 100644
--- a/homeassistant/components/enocean/sensor.py
+++ b/homeassistant/components/enocean/sensor.py
@@ -1,7 +1,7 @@
"""Support for EnOcean sensors."""
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_ID,
@@ -101,7 +101,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([EnOceanWindowHandle(dev_id, dev_name)])
-class EnOceanSensor(EnOceanEntity, RestoreEntity):
+class EnOceanSensor(EnOceanEntity, RestoreEntity, SensorEntity):
"""Representation of an EnOcean sensor device such as a power meter."""
def __init__(self, dev_id, dev_name, sensor_type):
diff --git a/homeassistant/components/enocean/translations/hu.json b/homeassistant/components/enocean/translations/hu.json
new file mode 100644
index 00000000000000..065747fb39df50
--- /dev/null
+++ b/homeassistant/components/enocean/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enocean/translations/id.json b/homeassistant/components/enocean/translations/id.json
new file mode 100644
index 00000000000000..ccadfe55982ad7
--- /dev/null
+++ b/homeassistant/components/enocean/translations/id.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "invalid_dongle_path": "Jalur dongle tidak valid",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "invalid_dongle_path": "Tidak ada dongle valid yang ditemukan untuk jalur ini"
+ },
+ "step": {
+ "detect": {
+ "data": {
+ "path": "Jalur dongle USB"
+ },
+ "title": "Pilih jalur ke dongle ENOcean Anda"
+ },
+ "manual": {
+ "data": {
+ "path": "Jalur dongle USB"
+ },
+ "title": "Masukkan jalur ke dongle ENOcean Anda"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enocean/translations/ko.json b/homeassistant/components/enocean/translations/ko.json
index ba109ed58c04ec..a7480a72b3b80b 100644
--- a/homeassistant/components/enocean/translations/ko.json
+++ b/homeassistant/components/enocean/translations/ko.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"invalid_dongle_path": "\ub3d9\uae00 \uacbd\ub85c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4."
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"error": {
"invalid_dongle_path": "\uc774 \uacbd\ub85c\uc5d0 \uc720\ud6a8\ud55c \ub3d9\uae00\uc774 \uc5c6\uc2b5\ub2c8\ub2e4"
@@ -18,7 +18,7 @@
"data": {
"path": "USB \ub3d9\uae00 \uacbd\ub85c"
},
- "title": "ENOcean \ub3d9\uae00 \uacbd\ub85c \uc785\ub825\ud558\uae30"
+ "title": "ENOcean \ub3d9\uae00 \uacbd\ub85c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694"
}
}
}
diff --git a/homeassistant/components/enocean/translations/nl.json b/homeassistant/components/enocean/translations/nl.json
new file mode 100644
index 00000000000000..79e0ab6dfec135
--- /dev/null
+++ b/homeassistant/components/enocean/translations/nl.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "invalid_dongle_path": "Ongeldig dongle-pad",
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
+ },
+ "error": {
+ "invalid_dongle_path": "Geen geldige dongle gevonden voor dit pad"
+ },
+ "step": {
+ "detect": {
+ "data": {
+ "path": "USB dongle pad"
+ },
+ "title": "Selecteer het pad naar uw ENOcean-dongle"
+ },
+ "manual": {
+ "data": {
+ "path": "USB-dongle pad"
+ },
+ "title": "Voer het pad naar uw ENOcean dongle in"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enocean/translations/tr.json b/homeassistant/components/enocean/translations/tr.json
new file mode 100644
index 00000000000000..b4e6be555ff467
--- /dev/null
+++ b/homeassistant/components/enocean/translations/tr.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "invalid_dongle_path": "Ge\u00e7ersiz dongle yolu",
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "error": {
+ "invalid_dongle_path": "Bu yol i\u00e7in ge\u00e7erli bir dongle bulunamad\u0131"
+ },
+ "step": {
+ "detect": {
+ "data": {
+ "path": "USB dongle yolu"
+ }
+ },
+ "manual": {
+ "data": {
+ "path": "USB dongle yolu"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enocean/translations/uk.json b/homeassistant/components/enocean/translations/uk.json
new file mode 100644
index 00000000000000..5c3e2d6eb6ec97
--- /dev/null
+++ b/homeassistant/components/enocean/translations/uk.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "invalid_dongle_path": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u0448\u043b\u044f\u0445 \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "invalid_dongle_path": "\u041d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u0437\u0430 \u0446\u0438\u043c \u0448\u043b\u044f\u0445\u043e\u043c."
+ },
+ "step": {
+ "detect": {
+ "data": {
+ "path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ },
+ "title": "ENOcean"
+ },
+ "manual": {
+ "data": {
+ "path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ },
+ "title": "ENOcean"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py
index c4101fbcdf23fa..1b8d09b1f1d817 100644
--- a/homeassistant/components/enphase_envoy/__init__.py
+++ b/homeassistant/components/enphase_envoy/__init__.py
@@ -1 +1,100 @@
-"""The enphase_envoy component."""
+"""The Enphase Envoy integration."""
+from __future__ import annotations
+
+import asyncio
+from datetime import timedelta
+import logging
+
+import async_timeout
+from envoy_reader.envoy_reader import EnvoyReader
+import httpx
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import COORDINATOR, DOMAIN, NAME, PLATFORMS, SENSORS
+
+SCAN_INTERVAL = timedelta(seconds=60)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Set up Enphase Envoy from a config entry."""
+
+ config = entry.data
+ name = config[CONF_NAME]
+ envoy_reader = EnvoyReader(
+ config[CONF_HOST], config[CONF_USERNAME], config[CONF_PASSWORD]
+ )
+
+ try:
+ await envoy_reader.getData()
+ except httpx.HTTPStatusError as err:
+ _LOGGER.error("Authentication failure during setup: %s", err)
+ return
+ except (AttributeError, httpx.HTTPError) as err:
+ raise ConfigEntryNotReady from err
+
+ async def async_update_data():
+ """Fetch data from API endpoint."""
+ data = {}
+ async with async_timeout.timeout(30):
+ try:
+ await envoy_reader.getData()
+ except httpx.HTTPError as err:
+ raise UpdateFailed(f"Error communicating with API: {err}") from err
+
+ for condition in SENSORS:
+ if condition != "inverters":
+ data[condition] = await getattr(envoy_reader, condition)()
+ else:
+ data[
+ "inverters_production"
+ ] = await envoy_reader.inverters_production()
+
+ _LOGGER.debug("Retrieved data from API: %s", data)
+
+ return data
+
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name="envoy {name}",
+ update_method=async_update_data,
+ update_interval=SCAN_INTERVAL,
+ )
+
+ envoy_reader.get_inverters = True
+ await coordinator.async_config_entry_first_refresh()
+
+ hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
+ COORDINATOR: coordinator,
+ NAME: name,
+ }
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py
new file mode 100644
index 00000000000000..41d72c09a31b7f
--- /dev/null
+++ b/homeassistant/components/enphase_envoy/config_flow.py
@@ -0,0 +1,162 @@
+"""Config flow for Enphase Envoy integration."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from envoy_reader.envoy_reader import EnvoyReader
+import httpx
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_IP_ADDRESS,
+ CONF_NAME,
+ CONF_PASSWORD,
+ CONF_USERNAME,
+)
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.exceptions import HomeAssistantError
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+ENVOY = "Envoy"
+
+CONF_SERIAL = "serial"
+
+
+async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
+ """Validate the user input allows us to connect."""
+ envoy_reader = EnvoyReader(
+ data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], inverters=True
+ )
+
+ try:
+ await envoy_reader.getData()
+ except httpx.HTTPStatusError as err:
+ raise InvalidAuth from err
+ except (AttributeError, httpx.HTTPError) as err:
+ raise CannotConnect from err
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Enphase Envoy."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ def __init__(self):
+ """Initialize an envoy flow."""
+ self.ip_address = None
+ self.name = None
+ self.username = None
+ self.serial = None
+
+ @callback
+ def _async_generate_schema(self):
+ """Generate schema."""
+ schema = {}
+
+ if self.ip_address:
+ schema[vol.Required(CONF_HOST, default=self.ip_address)] = vol.In(
+ [self.ip_address]
+ )
+ else:
+ schema[vol.Required(CONF_HOST)] = str
+
+ schema[vol.Optional(CONF_USERNAME, default=self.username or "envoy")] = str
+ schema[vol.Optional(CONF_PASSWORD, default="")] = str
+ return vol.Schema(schema)
+
+ async def async_step_import(self, import_config):
+ """Handle a flow import."""
+ self.ip_address = import_config[CONF_IP_ADDRESS]
+ self.username = import_config[CONF_USERNAME]
+ self.name = import_config[CONF_NAME]
+ return await self.async_step_user(
+ {
+ CONF_HOST: import_config[CONF_IP_ADDRESS],
+ CONF_USERNAME: import_config[CONF_USERNAME],
+ CONF_PASSWORD: import_config[CONF_PASSWORD],
+ }
+ )
+
+ @callback
+ def _async_current_hosts(self):
+ """Return a set of hosts."""
+ return {
+ entry.data[CONF_HOST]
+ for entry in self._async_current_entries(include_ignore=False)
+ if CONF_HOST in entry.data
+ }
+
+ async def async_step_zeroconf(self, discovery_info):
+ """Handle a flow initialized by zeroconf discovery."""
+ self.serial = discovery_info["properties"]["serialnum"]
+ await self.async_set_unique_id(self.serial)
+ self.ip_address = discovery_info[CONF_HOST]
+ self._abort_if_unique_id_configured({CONF_HOST: self.ip_address})
+ for entry in self._async_current_entries(include_ignore=False):
+ if (
+ entry.unique_id is None
+ and CONF_HOST in entry.data
+ and entry.data[CONF_HOST] == self.ip_address
+ ):
+ title = f"{ENVOY} {self.serial}" if entry.title == ENVOY else ENVOY
+ self.hass.config_entries.async_update_entry(
+ entry, title=title, unique_id=self.serial
+ )
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(entry.entry_id)
+ )
+ return self.async_abort(reason="already_configured")
+
+ return await self.async_step_user()
+
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
+ """Handle the initial step."""
+ errors = {}
+
+ if user_input is not None:
+ if user_input[CONF_HOST] in self._async_current_hosts():
+ return self.async_abort(reason="already_configured")
+ try:
+ await validate_input(self.hass, user_input)
+ except CannotConnect:
+ errors["base"] = "cannot_connect"
+ except InvalidAuth:
+ errors["base"] = "invalid_auth"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+ else:
+ data = user_input.copy()
+ if self.serial:
+ data[CONF_NAME] = f"{ENVOY} {self.serial}"
+ else:
+ data[CONF_NAME] = self.name or ENVOY
+ return self.async_create_entry(title=data[CONF_NAME], data=data)
+
+ if self.serial:
+ self.context["title_placeholders"] = {
+ CONF_SERIAL: self.serial,
+ CONF_HOST: self.ip_address,
+ }
+ return self.async_show_form(
+ step_id="user",
+ data_schema=self._async_generate_schema(),
+ errors=errors,
+ )
+
+
+class CannotConnect(HomeAssistantError):
+ """Error to indicate we cannot connect."""
+
+
+class InvalidAuth(HomeAssistantError):
+ """Error to indicate there is invalid auth."""
diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py
new file mode 100644
index 00000000000000..89803d32351c99
--- /dev/null
+++ b/homeassistant/components/enphase_envoy/const.py
@@ -0,0 +1,30 @@
+"""The enphase_envoy component."""
+
+
+from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT
+
+DOMAIN = "enphase_envoy"
+
+PLATFORMS = ["sensor"]
+
+
+COORDINATOR = "coordinator"
+NAME = "name"
+
+SENSORS = {
+ "production": ("Current Energy Production", POWER_WATT),
+ "daily_production": ("Today's Energy Production", ENERGY_WATT_HOUR),
+ "seven_days_production": (
+ "Last Seven Days Energy Production",
+ ENERGY_WATT_HOUR,
+ ),
+ "lifetime_production": ("Lifetime Energy Production", ENERGY_WATT_HOUR),
+ "consumption": ("Current Energy Consumption", POWER_WATT),
+ "daily_consumption": ("Today's Energy Consumption", ENERGY_WATT_HOUR),
+ "seven_days_consumption": (
+ "Last Seven Days Energy Consumption",
+ ENERGY_WATT_HOUR,
+ ),
+ "lifetime_consumption": ("Lifetime Energy Consumption", ENERGY_WATT_HOUR),
+ "inverters": ("Inverter", POWER_WATT),
+}
diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json
index 9e9760560d5ab7..236010607372bf 100644
--- a/homeassistant/components/enphase_envoy/manifest.json
+++ b/homeassistant/components/enphase_envoy/manifest.json
@@ -2,8 +2,12 @@
"domain": "enphase_envoy",
"name": "Enphase Envoy",
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
- "requirements": ["envoy_reader==0.18.3"],
+ "requirements": [
+ "envoy_reader==0.18.3"
+ ],
"codeowners": [
"@gtdiehl"
- ]
-}
+ ],
+ "config_flow": true,
+ "zeroconf": [{ "type": "_enphase-envoy._tcp.local."}]
+}
\ No newline at end of file
diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py
index 64b4fdf66ad909..050a497f69e4f3 100644
--- a/homeassistant/components/enphase_envoy/sensor.py
+++ b/homeassistant/components/enphase_envoy/sensor.py
@@ -1,55 +1,27 @@
"""Support for Enphase Envoy solar energy monitor."""
-from datetime import timedelta
import logging
-import async_timeout
-from envoy_reader.envoy_reader import EnvoyReader
-import httpx
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
CONF_IP_ADDRESS,
CONF_MONITORED_CONDITIONS,
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
- ENERGY_WATT_HOUR,
- POWER_WATT,
)
-from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.update_coordinator import (
- CoordinatorEntity,
- DataUpdateCoordinator,
- UpdateFailed,
-)
-
-_LOGGER = logging.getLogger(__name__)
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
-SENSORS = {
- "production": ("Envoy Current Energy Production", POWER_WATT),
- "daily_production": ("Envoy Today's Energy Production", ENERGY_WATT_HOUR),
- "seven_days_production": (
- "Envoy Last Seven Days Energy Production",
- ENERGY_WATT_HOUR,
- ),
- "lifetime_production": ("Envoy Lifetime Energy Production", ENERGY_WATT_HOUR),
- "consumption": ("Envoy Current Energy Consumption", POWER_WATT),
- "daily_consumption": ("Envoy Today's Energy Consumption", ENERGY_WATT_HOUR),
- "seven_days_consumption": (
- "Envoy Last Seven Days Energy Consumption",
- ENERGY_WATT_HOUR,
- ),
- "lifetime_consumption": ("Envoy Lifetime Energy Consumption", ENERGY_WATT_HOUR),
- "inverters": ("Envoy Inverter", POWER_WATT),
-}
+from .const import COORDINATOR, DOMAIN, NAME, SENSORS
ICON = "mdi:flash"
CONST_DEFAULT_HOST = "envoy"
+_LOGGER = logging.getLogger(__name__)
-SCAN_INTERVAL = timedelta(seconds=60)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -64,89 +36,59 @@
)
-async def async_setup_platform(
- homeassistant, config, async_add_entities, discovery_info=None
-):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Enphase Envoy sensor."""
- ip_address = config[CONF_IP_ADDRESS]
- monitored_conditions = config[CONF_MONITORED_CONDITIONS]
- name = config[CONF_NAME]
- username = config[CONF_USERNAME]
- password = config[CONF_PASSWORD]
-
- if "inverters" in monitored_conditions:
- envoy_reader = EnvoyReader(ip_address, username, password, inverters=True)
- else:
- envoy_reader = EnvoyReader(ip_address, username, password)
-
- try:
- await envoy_reader.getData()
- except httpx.HTTPStatusError as err:
- _LOGGER.error("Authentication failure during setup: %s", err)
- return
- except httpx.HTTPError as err:
- raise PlatformNotReady from err
-
- async def async_update_data():
- """Fetch data from API endpoint."""
- data = {}
- async with async_timeout.timeout(30):
- try:
- await envoy_reader.getData()
- except httpx.HTTPError as err:
- raise UpdateFailed(f"Error communicating with API: {err}") from err
-
- for condition in monitored_conditions:
- if condition != "inverters":
- data[condition] = await getattr(envoy_reader, condition)()
- else:
- data["inverters_production"] = await getattr(
- envoy_reader, "inverters_production"
- )()
-
- _LOGGER.debug("Retrieved data from API: %s", data)
-
- return data
-
- coordinator = DataUpdateCoordinator(
- homeassistant,
- _LOGGER,
- name="sensor",
- update_method=async_update_data,
- update_interval=SCAN_INTERVAL,
+ _LOGGER.warning(
+ "Loading enphase_envoy via platform config is deprecated; The configuration"
+ " has been migrated to a config entry and can be safely removed"
+ )
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=config
+ )
)
- await coordinator.async_refresh()
- if coordinator.data is None:
- raise PlatformNotReady
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up envoy sensor platform."""
+ data = hass.data[DOMAIN][config_entry.entry_id]
+ coordinator = data[COORDINATOR]
+ name = data[NAME]
entities = []
- for condition in monitored_conditions:
+ for condition in SENSORS:
entity_name = ""
if (
condition == "inverters"
and coordinator.data.get("inverters_production") is not None
):
for inverter in coordinator.data["inverters_production"]:
- entity_name = f"{name}{SENSORS[condition][0]} {inverter}"
+ entity_name = f"{name} {SENSORS[condition][0]} {inverter}"
split_name = entity_name.split(" ")
serial_number = split_name[-1]
entities.append(
Envoy(
condition,
entity_name,
+ name,
+ config_entry.unique_id,
serial_number,
SENSORS[condition][1],
coordinator,
)
)
elif condition != "inverters":
- entity_name = f"{name}{SENSORS[condition][0]}"
+ data = coordinator.data.get(condition)
+ if isinstance(data, str) and "not available" in data:
+ continue
+
+ entity_name = f"{name} {SENSORS[condition][0]}"
entities.append(
Envoy(
condition,
entity_name,
+ name,
+ config_entry.unique_id,
None,
SENSORS[condition][1],
coordinator,
@@ -156,14 +98,25 @@ async def async_update_data():
async_add_entities(entities)
-class Envoy(CoordinatorEntity):
+class Envoy(CoordinatorEntity, SensorEntity):
"""Envoy entity."""
- def __init__(self, sensor_type, name, serial_number, unit, coordinator):
+ def __init__(
+ self,
+ sensor_type,
+ name,
+ device_name,
+ device_serial_number,
+ serial_number,
+ unit,
+ coordinator,
+ ):
"""Initialize Envoy entity."""
self._type = sensor_type
self._name = name
self._serial_number = serial_number
+ self._device_name = device_name
+ self._device_serial_number = device_serial_number
self._unit_of_measurement = unit
super().__init__(coordinator)
@@ -173,6 +126,14 @@ def name(self):
"""Return the name of the sensor."""
return self._name
+ @property
+ def unique_id(self):
+ """Return the unique id of the sensor."""
+ if self._serial_number:
+ return self._serial_number
+ if self._device_serial_number:
+ return f"{self._device_serial_number}_{self._type}"
+
@property
def state(self):
"""Return the state of the sensor."""
@@ -202,7 +163,7 @@ def icon(self):
return ICON
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if (
self._type == "inverters"
@@ -214,3 +175,15 @@ def device_state_attributes(self):
return {"last_reported": value}
return None
+
+ @property
+ def device_info(self):
+ """Return the device_info of the device."""
+ if not self._device_serial_number:
+ return None
+ return {
+ "identifiers": {(DOMAIN, str(self._device_serial_number))},
+ "name": self._device_name,
+ "model": "Envoy",
+ "manufacturer": "Enphase",
+ }
diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json
new file mode 100644
index 00000000000000..399358659d70e7
--- /dev/null
+++ b/homeassistant/components/enphase_envoy/strings.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "flow_title": "Envoy {serial} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "[%key:common::config_flow::data::host%]",
+ "username": "[%key:common::config_flow::data::username%]",
+ "password": "[%key:common::config_flow::data::password%]"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enphase_envoy/translations/ca.json b/homeassistant/components/enphase_envoy/translations/ca.json
new file mode 100644
index 00000000000000..f388abca5b8085
--- /dev/null
+++ b/homeassistant/components/enphase_envoy/translations/ca.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "flow_title": "Envoy {serial} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "password": "Contrasenya",
+ "username": "Nom d'usuari"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enphase_envoy/translations/de.json b/homeassistant/components/enphase_envoy/translations/de.json
new file mode 100644
index 00000000000000..c3c916f31f709a
--- /dev/null
+++ b/homeassistant/components/enphase_envoy/translations/de.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "flow_title": "Envoy {serial} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enphase_envoy/translations/en.json b/homeassistant/components/enphase_envoy/translations/en.json
new file mode 100644
index 00000000000000..7c138727cd72de
--- /dev/null
+++ b/homeassistant/components/enphase_envoy/translations/en.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "flow_title": "Envoy {serial} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Password",
+ "username": "Username"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enphase_envoy/translations/et.json b/homeassistant/components/enphase_envoy/translations/et.json
new file mode 100644
index 00000000000000..34f052809df9af
--- /dev/null
+++ b/homeassistant/components/enphase_envoy/translations/et.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_auth": "Tuvastamise viga",
+ "unknown": "Tundmatu viga"
+ },
+ "flow_title": "Envoy {serial} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Salas\u00f5na",
+ "username": "Kasutajanimi"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enphase_envoy/translations/hu.json b/homeassistant/components/enphase_envoy/translations/hu.json
new file mode 100644
index 00000000000000..caef6a32c86467
--- /dev/null
+++ b/homeassistant/components/enphase_envoy/translations/hu.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "flow_title": "Envoy {serial} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enphase_envoy/translations/it.json b/homeassistant/components/enphase_envoy/translations/it.json
new file mode 100644
index 00000000000000..18eab778b340ff
--- /dev/null
+++ b/homeassistant/components/enphase_envoy/translations/it.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "flow_title": "Envoy {serial} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Password",
+ "username": "Nome utente"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enphase_envoy/translations/ko.json b/homeassistant/components/enphase_envoy/translations/ko.json
new file mode 100644
index 00000000000000..74ec68256be47a
--- /dev/null
+++ b/homeassistant/components/enphase_envoy/translations/ko.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "flow_title": "Envoy {serial} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enphase_envoy/translations/nl.json b/homeassistant/components/enphase_envoy/translations/nl.json
new file mode 100644
index 00000000000000..1679e5ce0f417f
--- /dev/null
+++ b/homeassistant/components/enphase_envoy/translations/nl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
+ },
+ "flow_title": "Envoy {serial} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Wachtwoord",
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enphase_envoy/translations/no.json b/homeassistant/components/enphase_envoy/translations/no.json
new file mode 100644
index 00000000000000..b059bbf6be0321
--- /dev/null
+++ b/homeassistant/components/enphase_envoy/translations/no.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_auth": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "flow_title": "Utsending {serial} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Vert",
+ "password": "Passord",
+ "username": "Brukernavn"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enphase_envoy/translations/pl.json b/homeassistant/components/enphase_envoy/translations/pl.json
new file mode 100644
index 00000000000000..de961875c56c62
--- /dev/null
+++ b/homeassistant/components/enphase_envoy/translations/pl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "flow_title": "Envoy {serial} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP",
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enphase_envoy/translations/ru.json b/homeassistant/components/enphase_envoy/translations/ru.json
new file mode 100644
index 00000000000000..f10538617398f7
--- /dev/null
+++ b/homeassistant/components/enphase_envoy/translations/ru.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "flow_title": "Envoy {serial} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enphase_envoy/translations/zh-Hant.json b/homeassistant/components/enphase_envoy/translations/zh-Hant.json
new file mode 100644
index 00000000000000..bf901948b244b0
--- /dev/null
+++ b/homeassistant/components/enphase_envoy/translations/zh-Hant.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "flow_title": "Envoy {serial} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "password": "\u5bc6\u78bc",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/entur_public_transport/sensor.py b/homeassistant/components/entur_public_transport/sensor.py
index 883b5c43d7eca9..c9c530c6b08715 100644
--- a/homeassistant/components/entur_public_transport/sensor.py
+++ b/homeassistant/components/entur_public_transport/sensor.py
@@ -4,7 +4,7 @@
from enturclient import EnturPublicTransportData
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_LATITUDE,
@@ -15,7 +15,6 @@
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
import homeassistant.util.dt as dt_util
@@ -148,7 +147,7 @@ def get_stop_info(self, stop_id: str) -> dict:
return self._api.get_stop_info(stop_id)
-class EnturPublicTransportSensor(Entity):
+class EnturPublicTransportSensor(SensorEntity):
"""Implementation of a Entur public transport sensor."""
def __init__(self, api: EnturProxy, name: str, stop: str, show_on_map: bool):
@@ -172,7 +171,7 @@ def state(self) -> str:
return self._state
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return the state attributes."""
self._attributes[ATTR_ATTRIBUTION] = ATTRIBUTION
self._attributes[ATTR_STOP_ID] = self._stop
diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py
index 66079ac73ff83b..019dcb1aee5a24 100644
--- a/homeassistant/components/environment_canada/camera.py
+++ b/homeassistant/components/environment_canada/camera.py
@@ -1,7 +1,7 @@
"""Support for the Environment Canada radar imagery."""
import datetime
-from env_canada import ECRadar # pylint: disable=import-error
+from env_canada import ECRadar
import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
@@ -81,7 +81,7 @@ def name(self):
return "Environment Canada Radar"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
return {ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_UPDATED: self.timestamp}
diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py
index a8772909f68374..0f0fb04fd00cb2 100644
--- a/homeassistant/components/environment_canada/sensor.py
+++ b/homeassistant/components/environment_canada/sensor.py
@@ -3,10 +3,10 @@
import logging
import re
-from env_canada import ECData # pylint: disable=import-error
+from env_canada import ECData
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_LOCATION,
@@ -15,7 +15,6 @@
TEMP_CELSIUS,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -65,7 +64,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([ECSensor(sensor_type, ec_data) for sensor_type in sensor_list], True)
-class ECSensor(Entity):
+class ECSensor(SensorEntity):
"""Implementation of an Environment Canada sensor."""
def __init__(self, sensor_type, ec_data):
@@ -95,7 +94,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
return self._attr
diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py
index f4fa96b52d6ca0..9abbc33bc93376 100644
--- a/homeassistant/components/environment_canada/weather.py
+++ b/homeassistant/components/environment_canada/weather.py
@@ -2,7 +2,7 @@
import datetime
import re
-from env_canada import ECData # pylint: disable=import-error
+from env_canada import ECData
import voluptuous as vol
from homeassistant.components.weather import (
@@ -184,23 +184,34 @@ def get_forecast(ec_data, forecast_type):
if forecast_type == "daily":
half_days = ec_data.daily_forecasts
+
+ today = {
+ ATTR_FORECAST_TIME: dt.now().isoformat(),
+ ATTR_FORECAST_CONDITION: icon_code_to_condition(
+ int(half_days[0]["icon_code"])
+ ),
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: int(
+ half_days[0]["precip_probability"]
+ ),
+ }
+
if half_days[0]["temperature_class"] == "high":
- forecast_array.append(
+ today.update(
{
- ATTR_FORECAST_TIME: dt.now().isoformat(),
ATTR_FORECAST_TEMP: int(half_days[0]["temperature"]),
ATTR_FORECAST_TEMP_LOW: int(half_days[1]["temperature"]),
- ATTR_FORECAST_CONDITION: icon_code_to_condition(
- int(half_days[0]["icon_code"])
- ),
- ATTR_FORECAST_PRECIPITATION_PROBABILITY: int(
- half_days[0]["precip_probability"]
- ),
}
)
- half_days = half_days[2:]
else:
- half_days = half_days[1:]
+ today.update(
+ {
+ ATTR_FORECAST_TEMP_LOW: int(half_days[0]["temperature"]),
+ ATTR_FORECAST_TEMP: int(half_days[1]["temperature"]),
+ }
+ )
+
+ forecast_array.append(today)
+ half_days = half_days[2:]
for day, high, low in zip(range(1, 6), range(0, 9, 2), range(1, 10, 2)):
forecast_array.append(
diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py
index 873d9935ee8443..137d6aee853fda 100644
--- a/homeassistant/components/envirophat/sensor.py
+++ b/homeassistant/components/envirophat/sensor.py
@@ -5,7 +5,7 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_DISPLAY_OPTIONS,
CONF_NAME,
@@ -14,7 +14,6 @@
VOLT,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -71,7 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(dev, True)
-class EnvirophatSensor(Entity):
+class EnvirophatSensor(SensorEntity):
"""Representation of an Enviro pHAT sensor."""
def __init__(self, data, sensor_types):
diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py
index 636cf0c19df44b..75d4bff3dd183b 100644
--- a/homeassistant/components/envisalink/__init__.py
+++ b/homeassistant/components/envisalink/__init__.py
@@ -5,7 +5,12 @@
from pyenvisalink import EnvisalinkAlarmPanel
import voluptuous as vol
-from homeassistant.const import CONF_HOST, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP
+from homeassistant.const import (
+ CONF_CODE,
+ CONF_HOST,
+ CONF_TIMEOUT,
+ EVENT_HOMEASSISTANT_STOP,
+)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
@@ -18,7 +23,6 @@
DATA_EVL = "envisalink"
-CONF_CODE = "code"
CONF_EVL_KEEPALIVE = "keepalive_interval"
CONF_EVL_PORT = "port"
CONF_EVL_VERSION = "evl_version"
@@ -99,7 +103,6 @@
async def async_setup(hass, config):
"""Set up for Envisalink devices."""
-
conf = config.get(DOMAIN)
host = conf.get(CONF_HOST)
@@ -141,9 +144,7 @@ def login_fail_callback(data):
@callback
def connection_fail_callback(data):
"""Network failure callback."""
- _LOGGER.error(
- "Could not establish a connection with the Envisalink- retrying..."
- )
+ _LOGGER.error("Could not establish a connection with the Envisalink- retrying")
if not sync_connect.done():
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink)
sync_connect.set_result(True)
@@ -159,13 +160,13 @@ def connection_success_callback(data):
@callback
def zones_updated_callback(data):
"""Handle zone timer updates."""
- _LOGGER.debug("Envisalink sent a zone update event. Updating zones...")
+ _LOGGER.debug("Envisalink sent a zone update event. Updating zones")
async_dispatcher_send(hass, SIGNAL_ZONE_UPDATE, data)
@callback
def alarm_data_updated_callback(data):
"""Handle non-alarm based info updates."""
- _LOGGER.debug("Envisalink sent new alarm info. Updating alarms...")
+ _LOGGER.debug("Envisalink sent new alarm info. Updating alarms")
async_dispatcher_send(hass, SIGNAL_KEYPAD_UPDATE, data)
@callback
diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py
index 670dc78392f4d8..dff434a68ee931 100644
--- a/homeassistant/components/envisalink/alarm_control_panel.py
+++ b/homeassistant/components/envisalink/alarm_control_panel.py
@@ -15,6 +15,7 @@
)
from homeassistant.const import (
ATTR_ENTITY_ID,
+ CONF_CODE,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
@@ -28,7 +29,6 @@
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import (
- CONF_CODE,
CONF_PANIC,
CONF_PARTITIONNAME,
DATA_EVL,
diff --git a/homeassistant/components/envisalink/binary_sensor.py b/homeassistant/components/envisalink/binary_sensor.py
index 54445660484540..22089ee79073b0 100644
--- a/homeassistant/components/envisalink/binary_sensor.py
+++ b/homeassistant/components/envisalink/binary_sensor.py
@@ -56,7 +56,7 @@ async def async_added_to_hass(self):
async_dispatcher_connect(self.hass, SIGNAL_ZONE_UPDATE, self._update_callback)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attr = {}
diff --git a/homeassistant/components/envisalink/sensor.py b/homeassistant/components/envisalink/sensor.py
index 3f3711b2e40a27..6fd7f32c6fe669 100644
--- a/homeassistant/components/envisalink/sensor.py
+++ b/homeassistant/components/envisalink/sensor.py
@@ -1,9 +1,9 @@
"""Support for Envisalink sensors (shows panel info)."""
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from . import (
CONF_PARTITIONNAME,
@@ -37,7 +37,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(devices)
-class EnvisalinkSensor(EnvisalinkDevice, Entity):
+class EnvisalinkSensor(EnvisalinkDevice, SensorEntity):
"""Representation of an Envisalink keypad."""
def __init__(self, hass, partition_name, partition_number, info, controller):
@@ -66,7 +66,7 @@ def state(self):
return self._info["status"]["alpha"]
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._info["status"]
diff --git a/homeassistant/components/epson/__init__.py b/homeassistant/components/epson/__init__.py
index c7278ed2cc9bd8..51d464dacb5c79 100644
--- a/homeassistant/components/epson/__init__.py
+++ b/homeassistant/components/epson/__init__.py
@@ -48,9 +48,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
_LOGGER.warning("Cannot connect to projector %s", entry.data[CONF_HOST])
return False
hass.data[DOMAIN][entry.entry_id] = projector
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -60,8 +60,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py
index 24ed62067f91a9..6115cdd6ef4978 100644
--- a/homeassistant/components/epson/media_player.py
+++ b/homeassistant/components/epson/media_player.py
@@ -215,7 +215,7 @@ async def async_media_previous_track(self):
await self._projector.send_command(BACK)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
if self._cmode is None:
return {}
diff --git a/homeassistant/components/epson/translations/de.json b/homeassistant/components/epson/translations/de.json
index c03615a39ff5d5..a91e3831cdb39c 100644
--- a/homeassistant/components/epson/translations/de.json
+++ b/homeassistant/components/epson/translations/de.json
@@ -1,12 +1,14 @@
{
"config": {
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"step": {
"user": {
"data": {
- "name": "Name"
+ "host": "Host",
+ "name": "Name",
+ "port": "Port"
}
}
}
diff --git a/homeassistant/components/epson/translations/hu.json b/homeassistant/components/epson/translations/hu.json
index 5ff60755bfd723..f2a380903ecf04 100644
--- a/homeassistant/components/epson/translations/hu.json
+++ b/homeassistant/components/epson/translations/hu.json
@@ -6,7 +6,7 @@
"step": {
"user": {
"data": {
- "host": "Gazdag\u00e9p",
+ "host": "Hoszt",
"name": "N\u00e9v",
"port": "Port"
}
diff --git a/homeassistant/components/epson/translations/id.json b/homeassistant/components/epson/translations/id.json
new file mode 100644
index 00000000000000..ba2d36424f951d
--- /dev/null
+++ b/homeassistant/components/epson/translations/id.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nama",
+ "port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/epson/translations/ko.json b/homeassistant/components/epson/translations/ko.json
new file mode 100644
index 00000000000000..1ee9afdcf75ce3
--- /dev/null
+++ b/homeassistant/components/epson/translations/ko.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "name": "\uc774\ub984",
+ "port": "\ud3ec\ud2b8"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/epson/translations/nl.json b/homeassistant/components/epson/translations/nl.json
new file mode 100644
index 00000000000000..d5ae90c0e38274
--- /dev/null
+++ b/homeassistant/components/epson/translations/nl.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Naam",
+ "port": "Poort"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/epson/translations/tr.json b/homeassistant/components/epson/translations/tr.json
index aafc2e2b30345a..9ffd77fc50f6c8 100644
--- a/homeassistant/components/epson/translations/tr.json
+++ b/homeassistant/components/epson/translations/tr.json
@@ -1,5 +1,8 @@
{
"config": {
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/epson/translations/uk.json b/homeassistant/components/epson/translations/uk.json
new file mode 100644
index 00000000000000..65566a8f4aa5cb
--- /dev/null
+++ b/homeassistant/components/epson/translations/uk.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "port": "\u041f\u043e\u0440\u0442"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py
index 7c6042c89594c8..22f74e1c0b132c 100644
--- a/homeassistant/components/epsonworkforce/sensor.py
+++ b/homeassistant/components/epsonworkforce/sensor.py
@@ -4,11 +4,10 @@
from epsonprinter_pkg.epsonprinterapi import EpsonPrinterAPI
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_HOST, CONF_MONITORED_CONDITIONS, PERCENTAGE
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
MONITORED_CONDITIONS = {
"black": ["Ink level Black", PERCENTAGE, "mdi:water"],
@@ -45,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(sensors, True)
-class EpsonPrinterCartridge(Entity):
+class EpsonPrinterCartridge(SensorEntity):
"""Representation of a cartridge sensor."""
def __init__(self, api, cartridgeidx):
diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py
index 737b3fe357a4b7..f803c9c0bd5c3a 100644
--- a/homeassistant/components/eq3btsmart/climate.py
+++ b/homeassistant/components/eq3btsmart/climate.py
@@ -1,7 +1,7 @@
"""Support for eQ-3 Bluetooth Smart thermostats."""
import logging
-from bluepy.btle import BTLEException # pylint: disable=import-error, no-name-in-module
+from bluepy.btle import BTLEException # pylint: disable=import-error
import eq3bt as eq3 # pylint: disable=import-error
import voluptuous as vol
@@ -155,7 +155,7 @@ def max_temp(self):
return self._thermostat.max_temp
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
dev_specific = {
ATTR_STATE_AWAY_END: self._thermostat.away_end,
diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py
index c0c3d02ec56825..0caf00af8ef0d5 100644
--- a/homeassistant/components/esphome/__init__.py
+++ b/homeassistant/components/esphome/__init__.py
@@ -1,8 +1,11 @@
"""Support for esphome devices."""
+from __future__ import annotations
+
import asyncio
+import functools
import logging
import math
-from typing import Any, Callable, Dict, List, Optional
+from typing import Any, Callable
from aioesphomeapi import (
APIClient,
@@ -15,12 +18,14 @@
UserServiceArgType,
)
import voluptuous as vol
+from zeroconf import DNSPointer, DNSRecord, RecordUpdateListener, Zeroconf
from homeassistant import const
from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST,
+ CONF_MODE,
CONF_PASSWORD,
CONF_PORT,
EVENT_HOMEASSISTANT_STOP,
@@ -34,13 +39,13 @@
from homeassistant.helpers.entity import Entity
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 homeassistant.helpers.typing import ConfigType, HomeAssistantType
+from homeassistant.helpers.typing import HomeAssistantType
# Import config flow so that it's added to the registry
-from .config_flow import EsphomeFlowHandler # noqa: F401
-from .entry_data import DATA_KEY, RuntimeEntryData
+from .entry_data import RuntimeEntryData
DOMAIN = "esphome"
_LOGGER = logging.getLogger(__name__)
@@ -51,17 +56,9 @@
CONFIG_SCHEMA = vol.Schema({}, extra=vol.ALLOW_EXTRA)
-async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
- """Stub to allow setting up this component.
-
- Configuration through YAML is not supported at this time.
- """
- return True
-
-
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
"""Set up the esphome component."""
- hass.data.setdefault(DATA_KEY, {})
+ hass.data.setdefault(DOMAIN, {})
host = entry.data[CONF_HOST]
port = entry.data[CONF_PORT]
@@ -83,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
store = Store(
hass, STORAGE_VERSION, f"esphome.{entry.entry_id}", encoder=JSONEncoder
)
- entry_data = hass.data[DATA_KEY][entry.entry_id] = RuntimeEntryData(
+ entry_data = hass.data[DOMAIN][entry.entry_id] = RuntimeEntryData(
client=cli, entry_id=entry.entry_id, store=store
)
@@ -156,7 +153,7 @@ async def send_home_assistant_state_event(event: Event) -> None:
await cli.send_home_assistant_state(entity_id, new_state.state)
async def _send_home_assistant_state(
- entity_id: str, new_state: Optional[State]
+ entity_id: str, new_state: State | None
) -> None:
"""Forward Home Assistant states to ESPHome."""
await cli.send_home_assistant_state(entity_id, new_state.state)
@@ -198,7 +195,9 @@ async def on_login() -> None:
# Re-connection logic will trigger after this
await cli.disconnect()
- try_connect = await _setup_auto_reconnect_logic(hass, cli, entry, host, on_login)
+ reconnect_logic = ReconnectLogic(
+ hass, cli, entry, host, on_login, zeroconf_instance
+ )
async def complete_setup() -> None:
"""Complete the config entry setup."""
@@ -206,81 +205,250 @@ async def complete_setup() -> None:
await entry_data.async_update_static_infos(hass, entry, infos)
await _setup_services(hass, entry_data, services)
- # Create connection attempt outside of HA's tracked task in order
- # not to delay startup.
- hass.loop.create_task(try_connect(is_disconnect=False))
+ await reconnect_logic.start()
+ entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback)
hass.async_create_task(complete_setup())
return True
-async def _setup_auto_reconnect_logic(
- hass: HomeAssistantType, cli: APIClient, entry: ConfigEntry, host: str, on_login
-):
- """Set up the re-connect logic for the API client."""
+class ReconnectLogic(RecordUpdateListener):
+ """Reconnectiong logic handler for ESPHome config entries.
- async def try_connect(tries: int = 0, is_disconnect: bool = True) -> None:
- """Try connecting to the API client. Will retry if not successful."""
- if entry.entry_id not in hass.data[DOMAIN]:
- # When removing/disconnecting manually
- return
+ Contains two reconnect strategies:
+ - Connect with increasing time between connection attempts.
+ - Listen to zeroconf mDNS records, if any records are found for this device, try reconnecting immediately.
+ """
- device_registry = await hass.helpers.device_registry.async_get_registry()
- devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
- for device in devices:
- # There is only one device in ESPHome
- if device.disabled:
- # Don't attempt to connect if it's disabled
- return
+ def __init__(
+ self,
+ hass: HomeAssistantType,
+ cli: APIClient,
+ entry: ConfigEntry,
+ host: str,
+ on_login,
+ zc: Zeroconf,
+ ):
+ """Initialize ReconnectingLogic."""
+ self._hass = hass
+ self._cli = cli
+ self._entry = entry
+ self._host = host
+ self._on_login = on_login
+ self._zc = zc
+ # Flag to check if the device is connected
+ self._connected = True
+ self._connected_lock = asyncio.Lock()
+ self._zc_lock = asyncio.Lock()
+ self._zc_listening = False
+ # Event the different strategies use for issuing a reconnect attempt.
+ self._reconnect_event = asyncio.Event()
+ # The task containing the infinite reconnect loop while running
+ self._loop_task: asyncio.Task | None = None
+ # How many reconnect attempts have there been already, used for exponential wait time
+ self._tries = 0
+ self._tries_lock = asyncio.Lock()
+ # Track the wait task to cancel it on HA shutdown
+ self._wait_task: asyncio.Task | None = None
+ self._wait_task_lock = asyncio.Lock()
- data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id]
- for disconnect_cb in data.disconnect_callbacks:
+ @property
+ def _entry_data(self) -> RuntimeEntryData | None:
+ return self._hass.data[DOMAIN].get(self._entry.entry_id)
+
+ async def _on_disconnect(self):
+ """Log and issue callbacks when disconnecting."""
+ if self._entry_data is None:
+ return
+ # This can happen often depending on WiFi signal strength.
+ # So therefore all these connection warnings are logged
+ # as infos. The "unavailable" logic will still trigger so the
+ # user knows if the device is not connected.
+ _LOGGER.info("Disconnected from ESPHome API for %s", self._host)
+
+ # Run disconnect hooks
+ for disconnect_cb in self._entry_data.disconnect_callbacks:
disconnect_cb()
- data.disconnect_callbacks = []
- data.available = False
- data.async_update_device_state(hass)
-
- if is_disconnect:
- # This can happen often depending on WiFi signal strength.
- # So therefore all these connection warnings are logged
- # as infos. The "unavailable" logic will still trigger so the
- # user knows if the device is not connected.
- _LOGGER.info("Disconnected from ESPHome API for %s", host)
-
- if tries != 0:
- # If not first re-try, wait and print message
- # Cap wait time at 1 minute. This is because while working on the
- # device (e.g. soldering stuff), users don't want to have to wait
- # a long time for their device to show up in HA again (this was
- # mentioned a lot in early feedback)
- #
- # In the future another API will be set up so that the ESP can
- # notify HA of connectivity directly, but for new we'll use a
- # really short reconnect interval.
- tries = min(tries, 10) # prevent OverflowError
- wait_time = int(round(min(1.8 ** tries, 60.0)))
- _LOGGER.info("Trying to reconnect to %s in %s seconds", host, wait_time)
- await asyncio.sleep(wait_time)
+ self._entry_data.disconnect_callbacks = []
+ self._entry_data.available = False
+ self._entry_data.async_update_device_state(self._hass)
+ await self._start_zc_listen()
+
+ # Reset tries
+ async with self._tries_lock:
+ self._tries = 0
+ # Connected needs to be reset before the reconnect event (opposite order of check)
+ async with self._connected_lock:
+ self._connected = False
+ self._reconnect_event.set()
+
+ async def _wait_and_start_reconnect(self):
+ """Wait for exponentially increasing time to issue next reconnect event."""
+ async with self._tries_lock:
+ tries = self._tries
+ # If not first re-try, wait and print message
+ # Cap wait time at 1 minute. This is because while working on the
+ # device (e.g. soldering stuff), users don't want to have to wait
+ # a long time for their device to show up in HA again (this was
+ # mentioned a lot in early feedback)
+ tries = min(tries, 10) # prevent OverflowError
+ wait_time = int(round(min(1.8 ** tries, 60.0)))
+ if tries == 1:
+ _LOGGER.info("Trying to reconnect to %s in the background", self._host)
+ _LOGGER.debug("Retrying %s in %d seconds", self._host, wait_time)
+ await asyncio.sleep(wait_time)
+ async with self._wait_task_lock:
+ self._wait_task = None
+ self._reconnect_event.set()
+
+ async def _try_connect(self):
+ """Try connecting to the API client."""
+ async with self._tries_lock:
+ tries = self._tries
+ self._tries += 1
try:
- await cli.connect(on_stop=try_connect, login=True)
+ await self._cli.connect(on_stop=self._on_disconnect, login=True)
except APIConnectionError as error:
- _LOGGER.info(
+ level = logging.WARNING if tries == 0 else logging.DEBUG
+ _LOGGER.log(
+ level,
"Can't connect to ESPHome API for %s (%s): %s",
- entry.unique_id,
- host,
+ self._entry.unique_id,
+ self._host,
error,
)
+ await self._start_zc_listen()
# Schedule re-connect in event loop in order not to delay HA
# startup. First connect is scheduled in tracked tasks.
- data.reconnect_task = hass.loop.create_task(
- try_connect(tries + 1, is_disconnect=False)
- )
+ async with self._wait_task_lock:
+ # Allow only one wait task at a time
+ # can happen if mDNS record received while waiting, then use existing wait task
+ if self._wait_task is not None:
+ return
+
+ self._wait_task = self._hass.loop.create_task(
+ self._wait_and_start_reconnect()
+ )
else:
- _LOGGER.info("Successfully connected to %s", host)
- hass.async_create_task(on_login())
+ _LOGGER.info("Successfully connected to %s", self._host)
+ async with self._tries_lock:
+ self._tries = 0
+ async with self._connected_lock:
+ self._connected = True
+ await self._stop_zc_listen()
+ self._hass.async_create_task(self._on_login())
+
+ async def _reconnect_once(self):
+ # Wait and clear reconnection event
+ await self._reconnect_event.wait()
+ self._reconnect_event.clear()
+
+ # If in connected state, do not try to connect again.
+ async with self._connected_lock:
+ if self._connected:
+ return False
+
+ # Check if the entry got removed or disabled, in which case we shouldn't reconnect
+ if self._entry.entry_id not in self._hass.data[DOMAIN]:
+ # When removing/disconnecting manually
+ return
+
+ device_registry = self._hass.helpers.device_registry.async_get(self._hass)
+ devices = dr.async_entries_for_config_entry(
+ device_registry, self._entry.entry_id
+ )
+ for device in devices:
+ # There is only one device in ESPHome
+ if device.disabled:
+ # Don't attempt to connect if it's disabled
+ return
+
+ await self._try_connect()
+
+ async def _reconnect_loop(self):
+ while True:
+ try:
+ await self._reconnect_once()
+ except asyncio.CancelledError: # pylint: disable=try-except-raise
+ raise
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.error("Caught exception while reconnecting", exc_info=True)
+
+ async def start(self):
+ """Start the reconnecting logic background task."""
+ # Create reconnection loop outside of HA's tracked tasks in order
+ # not to delay startup.
+ self._loop_task = self._hass.loop.create_task(self._reconnect_loop())
+
+ async with self._connected_lock:
+ self._connected = False
+ self._reconnect_event.set()
+
+ async def stop(self):
+ """Stop the reconnecting logic background task. Does not disconnect the client."""
+ if self._loop_task is not None:
+ self._loop_task.cancel()
+ self._loop_task = None
+ async with self._wait_task_lock:
+ if self._wait_task is not None:
+ self._wait_task.cancel()
+ self._wait_task = None
+ await self._stop_zc_listen()
+
+ async def _start_zc_listen(self):
+ """Listen for mDNS records.
+
+ This listener allows us to schedule a reconnect as soon as a
+ received mDNS record indicates the node is up again.
+ """
+ async with self._zc_lock:
+ if not self._zc_listening:
+ await self._hass.async_add_executor_job(
+ self._zc.add_listener, self, None
+ )
+ self._zc_listening = True
+
+ async def _stop_zc_listen(self):
+ """Stop listening for zeroconf updates."""
+ async with self._zc_lock:
+ if self._zc_listening:
+ await self._hass.async_add_executor_job(self._zc.remove_listener, self)
+ self._zc_listening = False
+
+ @callback
+ def stop_callback(self):
+ """Stop as an async callback function."""
+ self._hass.async_create_task(self.stop())
- return try_connect
+ @callback
+ def _set_reconnect(self):
+ self._reconnect_event.set()
+
+ def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None:
+ """Listen to zeroconf updated mDNS records."""
+ if not isinstance(record, DNSPointer):
+ # We only consider PTR records and match using the alias name
+ return
+ if self._entry_data is None or self._entry_data.device_info is None:
+ # Either the entry was already teared down or we haven't received device info yet
+ return
+ filter_alias = f"{self._entry_data.device_info.name}._esphomelib._tcp.local."
+ if record.alias != filter_alias:
+ return
+
+ # This is a mDNS record from the device and could mean it just woke up
+ # Check if already connected, no lock needed for this access
+ if self._connected:
+ return
+
+ # Tell reconnection logic to retry connection attempt now (even before reconnect timer finishes)
+ _LOGGER.debug(
+ "%s: Triggering reconnect because of received mDNS record %s",
+ self._host,
+ record,
+ )
+ self._hass.add_job(self._set_reconnect)
async def _async_setup_device_registry(
@@ -307,17 +475,63 @@ async def _register_service(
):
service_name = f"{entry_data.device_info.name}_{service.name}"
schema = {}
+ fields = {}
+
for arg in service.args:
- schema[vol.Required(arg.name)] = {
- UserServiceArgType.BOOL: cv.boolean,
- UserServiceArgType.INT: vol.Coerce(int),
- UserServiceArgType.FLOAT: vol.Coerce(float),
- UserServiceArgType.STRING: cv.string,
- UserServiceArgType.BOOL_ARRAY: [cv.boolean],
- UserServiceArgType.INT_ARRAY: [vol.Coerce(int)],
- UserServiceArgType.FLOAT_ARRAY: [vol.Coerce(float)],
- UserServiceArgType.STRING_ARRAY: [cv.string],
+ metadata = {
+ UserServiceArgType.BOOL: {
+ "validator": cv.boolean,
+ "example": "False",
+ "selector": {"boolean": None},
+ },
+ UserServiceArgType.INT: {
+ "validator": vol.Coerce(int),
+ "example": "42",
+ "selector": {"number": {CONF_MODE: "box"}},
+ },
+ UserServiceArgType.FLOAT: {
+ "validator": vol.Coerce(float),
+ "example": "12.3",
+ "selector": {"number": {CONF_MODE: "box", "step": 1e-3}},
+ },
+ UserServiceArgType.STRING: {
+ "validator": cv.string,
+ "example": "Example text",
+ "selector": {"text": None},
+ },
+ UserServiceArgType.BOOL_ARRAY: {
+ "validator": [cv.boolean],
+ "description": "A list of boolean values.",
+ "example": "[True, False]",
+ "selector": {"object": {}},
+ },
+ UserServiceArgType.INT_ARRAY: {
+ "validator": [vol.Coerce(int)],
+ "description": "A list of integer values.",
+ "example": "[42, 34]",
+ "selector": {"object": {}},
+ },
+ UserServiceArgType.FLOAT_ARRAY: {
+ "validator": [vol.Coerce(float)],
+ "description": "A list of floating point numbers.",
+ "example": "[ 12.3, 34.5 ]",
+ "selector": {"object": {}},
+ },
+ UserServiceArgType.STRING_ARRAY: {
+ "validator": [cv.string],
+ "description": "A list of strings.",
+ "example": "['Example text', 'Another example']",
+ "selector": {"object": {}},
+ },
}[arg.type_]
+ schema[vol.Required(arg.name)] = metadata["validator"]
+ fields[arg.name] = {
+ "name": arg.name,
+ "required": True,
+ "description": metadata.get("description"),
+ "example": metadata["example"],
+ "selector": metadata["selector"],
+ }
async def execute_service(call):
await entry_data.client.execute_service(service, call.data)
@@ -326,9 +540,16 @@ async def execute_service(call):
DOMAIN, service_name, execute_service, vol.Schema(schema)
)
+ service_desc = {
+ "description": f"Calls the service {service.name} of the node {entry_data.device_info.name}",
+ "fields": fields,
+ }
+
+ async_set_service_schema(hass, DOMAIN, service_name, service_desc)
+
async def _setup_services(
- hass: HomeAssistantType, entry_data: RuntimeEntryData, services: List[UserService]
+ hass: HomeAssistantType, entry_data: RuntimeEntryData, services: list[UserService]
):
old_services = entry_data.services.copy()
to_unregister = []
@@ -362,9 +583,7 @@ async def _cleanup_instance(
hass: HomeAssistantType, entry: ConfigEntry
) -> RuntimeEntryData:
"""Cleanup the esphome client if it exists."""
- data: RuntimeEntryData = hass.data[DATA_KEY].pop(entry.entry_id)
- if data.reconnect_task is not None:
- data.reconnect_task.cancel()
+ data: RuntimeEntryData = hass.data[DOMAIN].pop(entry.entry_id)
for disconnect_cb in data.disconnect_callbacks:
disconnect_cb()
for cleanup_callback in data.cleanup_callbacks:
@@ -405,7 +624,7 @@ async def platform_async_setup_entry(
entry_data.state[component_key] = {}
@callback
- def async_list_entities(infos: List[EntityInfo]):
+ def async_list_entities(infos: list[EntityInfo]):
"""Update entities of this platform when entities are listed."""
old_infos = entry_data.info[component_key]
new_infos = {}
@@ -479,7 +698,7 @@ def _wrapper(self):
class EsphomeEnumMapper:
"""Helper class to convert between hass and esphome enum values."""
- def __init__(self, func: Callable[[], Dict[int, str]]):
+ def __init__(self, func: Callable[[], dict[int, str]]):
"""Construct a EsphomeEnumMapper."""
self._func = func
@@ -493,7 +712,7 @@ def from_hass(self, value: str) -> int:
return inverse[value]
-def esphome_map_enum(func: Callable[[], Dict[int, str]]):
+def esphome_map_enum(func: Callable[[], dict[int, str]]):
"""Map esphome int enum values to hass string constants.
This class has to be used as a decorator. This ensures the aioesphomeapi
@@ -520,7 +739,7 @@ async def async_added_to_hass(self) -> None:
f"esphome_{self._entry_id}_remove_"
f"{self._component_key}_{self._key}"
),
- self.async_remove,
+ functools.partial(self.async_remove, force_remove=True),
)
)
@@ -544,7 +763,7 @@ def _on_device_update(self) -> None:
@property
def _entry_data(self) -> RuntimeEntryData:
- return self.hass.data[DATA_KEY][self._entry_id]
+ return self.hass.data[DOMAIN][self._entry_id]
@property
def _static_info(self) -> EntityInfo:
@@ -565,7 +784,7 @@ def _client(self) -> APIClient:
return self._entry_data.client
@property
- def _state(self) -> Optional[EntityState]:
+ def _state(self) -> EntityState | None:
try:
return self._entry_data.state[self._component_key][self._key]
except KeyError:
@@ -584,14 +803,14 @@ def available(self) -> bool:
return self._entry_data.available
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique id identifying the entity."""
if not self._static_info.unique_id:
return None
return self._static_info.unique_id
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device registry information for this entity."""
return {
"connections": {(dr.CONNECTION_NETWORK_MAC, self._device_info.mac_address)}
diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py
index d605a48410b89f..28cc47691f56b8 100644
--- a/homeassistant/components/esphome/binary_sensor.py
+++ b/homeassistant/components/esphome/binary_sensor.py
@@ -1,5 +1,5 @@
"""Support for ESPHome binary sensors."""
-from typing import Optional
+from __future__ import annotations
from aioesphomeapi import BinarySensorInfo, BinarySensorState
@@ -29,11 +29,11 @@ def _static_info(self) -> BinarySensorInfo:
return super()._static_info
@property
- def _state(self) -> Optional[BinarySensorState]:
+ def _state(self) -> BinarySensorState | None:
return super()._state
@property
- def is_on(self) -> Optional[bool]:
+ def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
if self._static_info.is_status_binary_sensor:
# Status binary sensors indicated connected state.
diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py
index 5b8f4f0d7e6633..c868d7b320a735 100644
--- a/homeassistant/components/esphome/camera.py
+++ b/homeassistant/components/esphome/camera.py
@@ -1,6 +1,7 @@
"""Support for ESPHome cameras."""
+from __future__ import annotations
+
import asyncio
-from typing import Optional
from aioesphomeapi import CameraInfo, CameraState
@@ -42,7 +43,7 @@ def _static_info(self) -> CameraInfo:
return super()._static_info
@property
- def _state(self) -> Optional[CameraState]:
+ def _state(self) -> CameraState | None:
return super()._state
async def async_added_to_hass(self) -> None:
@@ -67,7 +68,7 @@ async def _on_state_update(self) -> None:
async with self._image_cond:
self._image_cond.notify_all()
- async def async_camera_image(self) -> Optional[bytes]:
+ async def async_camera_image(self) -> bytes | None:
"""Return single camera image bytes."""
if not self.available:
return None
@@ -78,7 +79,7 @@ async def async_camera_image(self) -> Optional[bytes]:
return None
return self._state.image[:]
- async def _async_camera_stream_image(self) -> Optional[bytes]:
+ async def _async_camera_stream_image(self) -> bytes | None:
"""Return a single camera image in a stream."""
if not self.available:
return None
diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py
index fe171d24fb938a..5d21d495ec2cdb 100644
--- a/homeassistant/components/esphome/climate.py
+++ b/homeassistant/components/esphome/climate.py
@@ -1,5 +1,5 @@
"""Support for ESPHome climate devices."""
-from typing import List, Optional
+from __future__ import annotations
from aioesphomeapi import (
ClimateAction,
@@ -134,7 +134,7 @@ def _static_info(self) -> ClimateInfo:
return super()._static_info
@property
- def _state(self) -> Optional[ClimateState]:
+ def _state(self) -> ClimateState | None:
return super()._state
@property
@@ -153,7 +153,7 @@ def temperature_unit(self) -> str:
return TEMP_CELSIUS
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available operation modes."""
return [
_climate_modes.from_esphome(mode)
@@ -217,12 +217,12 @@ def supported_features(self) -> int:
# pylint: disable=invalid-overridden-method
@esphome_state_property
- def hvac_mode(self) -> Optional[str]:
+ def hvac_mode(self) -> str | None:
"""Return current operation ie. heat, cool, idle."""
return _climate_modes.from_esphome(self._state.mode)
@esphome_state_property
- def hvac_action(self) -> Optional[str]:
+ def hvac_action(self) -> str | None:
"""Return current action."""
# HA has no support feature field for hvac_action
if not self._static_info.supports_action:
@@ -245,22 +245,22 @@ def swing_mode(self):
return _swing_modes.from_esphome(self._state.swing_mode)
@esphome_state_property
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self._state.current_temperature
@esphome_state_property
- def target_temperature(self) -> Optional[float]:
+ def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
return self._state.target_temperature
@esphome_state_property
- def target_temperature_low(self) -> Optional[float]:
+ def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach."""
return self._state.target_temperature_low
@esphome_state_property
- def target_temperature_high(self) -> Optional[float]:
+ def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach."""
return self._state.target_temperature_high
diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py
index 1d1fc421bb8e6e..45a33a0dc24fca 100644
--- a/homeassistant/components/esphome/config_flow.py
+++ b/homeassistant/components/esphome/config_flow.py
@@ -1,6 +1,7 @@
"""Config flow to configure esphome component."""
+from __future__ import annotations
+
from collections import OrderedDict
-from typing import Optional
from aioesphomeapi import APIClient, APIConnectionError
import voluptuous as vol
@@ -11,9 +12,8 @@
from homeassistant.core import callback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
-from .entry_data import DATA_KEY, RuntimeEntryData
-
-DOMAIN = "esphome"
+from . import DOMAIN
+from .entry_data import RuntimeEntryData
class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
@@ -24,12 +24,12 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
def __init__(self):
"""Initialize flow."""
- self._host: Optional[str] = None
- self._port: Optional[int] = None
- self._password: Optional[str] = None
+ self._host: str | None = None
+ self._port: int | None = None
+ self._password: str | None = None
async def async_step_user(
- self, user_input: Optional[ConfigType] = None, error: Optional[str] = None
+ self, user_input: ConfigType | None = None, error: str | None = None
): # pylint: disable=arguments-differ
"""Handle a flow initialized by the user."""
if user_input is not None:
@@ -49,12 +49,10 @@ async def async_step_user(
@property
def _name(self):
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
return self.context.get(CONF_NAME)
@_name.setter
def _name(self, value):
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context[CONF_NAME] = value
self.context["title_placeholders"] = {"name": self._name}
@@ -107,9 +105,9 @@ async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType):
]:
# Is this address or IP address already configured?
already_configured = True
- elif entry.entry_id in self.hass.data.get(DATA_KEY, {}):
+ elif entry.entry_id in self.hass.data.get(DOMAIN, {}):
# Does a config entry with this name already exist?
- data: RuntimeEntryData = self.hass.data[DATA_KEY][entry.entry_id]
+ data: RuntimeEntryData = self.hass.data[DOMAIN][entry.entry_id]
# Node names are unique in the network
if data.device_info is not None:
diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py
index 4c42fe0d522a1a..294689d075a199 100644
--- a/homeassistant/components/esphome/cover.py
+++ b/homeassistant/components/esphome/cover.py
@@ -1,5 +1,5 @@
"""Support for ESPHome covers."""
-from typing import Optional
+from __future__ import annotations
from aioesphomeapi import CoverInfo, CoverOperation, CoverState
@@ -64,14 +64,14 @@ def assumed_state(self) -> bool:
return self._static_info.assumed_state
@property
- def _state(self) -> Optional[CoverState]:
+ def _state(self) -> CoverState | None:
return super()._state
# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
# pylint: disable=invalid-overridden-method
@esphome_state_property
- def is_closed(self) -> Optional[bool]:
+ 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._client.api_version)
@@ -87,14 +87,14 @@ def is_closing(self) -> bool:
return self._state.current_operation == CoverOperation.IS_CLOSING
@esphome_state_property
- def current_cover_position(self) -> Optional[int]:
+ def current_cover_position(self) -> int | None:
"""Return current position of cover. 0 is closed, 100 is open."""
if not self._static_info.supports_position:
return None
return round(self._state.position * 100.0)
@esphome_state_property
- def current_cover_tilt_position(self) -> Optional[float]:
+ def current_cover_tilt_position(self) -> float | None:
"""Return current position of cover tilt. 0 is closed, 100 is open."""
if not self._static_info.supports_tilt:
return None
diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py
index 54da1ed5562db2..34ed6ffee4634f 100644
--- a/homeassistant/components/esphome/entry_data.py
+++ b/homeassistant/components/esphome/entry_data.py
@@ -1,6 +1,8 @@
"""Runtime entry data for ESPHome stored in hass.data."""
+from __future__ import annotations
+
import asyncio
-from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple
+from typing import TYPE_CHECKING, Any, Callable
from aioesphomeapi import (
COMPONENT_TYPE_TO_INFO,
@@ -29,8 +31,6 @@
if TYPE_CHECKING:
from . import APIClient
-DATA_KEY = "esphome"
-
SAVE_DELAY = 120
# Mapping from ESPHome info type to HA platform
@@ -52,24 +52,23 @@ class RuntimeEntryData:
"""Store runtime data for esphome config entries."""
entry_id: str = attr.ib()
- client: "APIClient" = attr.ib()
+ client: APIClient = attr.ib()
store: Store = attr.ib()
- reconnect_task: Optional[asyncio.Task] = attr.ib(default=None)
- state: Dict[str, Dict[str, Any]] = attr.ib(factory=dict)
- info: Dict[str, Dict[str, Any]] = attr.ib(factory=dict)
+ state: dict[str, dict[str, Any]] = attr.ib(factory=dict)
+ info: dict[str, dict[str, Any]] = attr.ib(factory=dict)
# A second list of EntityInfo objects
# This is necessary for when an entity is being removed. HA requires
# some static info to be accessible during removal (unique_id, maybe others)
# If an entity can't find anything in the info array, it will look for info here.
- old_info: Dict[str, Dict[str, Any]] = attr.ib(factory=dict)
+ old_info: dict[str, dict[str, Any]] = attr.ib(factory=dict)
- services: Dict[int, "UserService"] = attr.ib(factory=dict)
+ services: dict[int, UserService] = attr.ib(factory=dict)
available: bool = attr.ib(default=False)
- device_info: Optional[DeviceInfo] = attr.ib(default=None)
- cleanup_callbacks: List[Callable[[], None]] = attr.ib(factory=list)
- disconnect_callbacks: List[Callable[[], None]] = attr.ib(factory=list)
- loaded_platforms: Set[str] = attr.ib(factory=set)
+ device_info: DeviceInfo | None = attr.ib(default=None)
+ cleanup_callbacks: list[Callable[[], None]] = attr.ib(factory=list)
+ disconnect_callbacks: list[Callable[[], None]] = attr.ib(factory=list)
+ loaded_platforms: set[str] = attr.ib(factory=set)
platform_load_lock: asyncio.Lock = attr.ib(factory=asyncio.Lock)
@callback
@@ -89,7 +88,7 @@ def async_remove_entity(
async_dispatcher_send(hass, signal)
async def _ensure_platforms_loaded(
- self, hass: HomeAssistantType, entry: ConfigEntry, platforms: Set[str]
+ self, hass: HomeAssistantType, entry: ConfigEntry, platforms: set[str]
):
async with self.platform_load_lock:
needed = platforms - self.loaded_platforms
@@ -103,7 +102,7 @@ async def _ensure_platforms_loaded(
self.loaded_platforms |= needed
async def async_update_static_infos(
- self, hass: HomeAssistantType, entry: ConfigEntry, infos: List[EntityInfo]
+ self, hass: HomeAssistantType, entry: ConfigEntry, infos: list[EntityInfo]
) -> None:
"""Distribute an update of static infos to all platforms."""
# First, load all platforms
@@ -131,7 +130,7 @@ def async_update_device_state(self, hass: HomeAssistantType) -> None:
signal = f"esphome_{self.entry_id}_on_device_update"
async_dispatcher_send(hass, signal)
- async def async_load_from_store(self) -> Tuple[List[EntityInfo], List[UserService]]:
+ async def async_load_from_store(self) -> tuple[list[EntityInfo], list[UserService]]:
"""Load the retained data from store and return de-serialized data."""
restored = await self.store.async_load()
if restored is None:
diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py
index 8da52b8d584da6..5d7cf24f2c5183 100644
--- a/homeassistant/components/esphome/fan.py
+++ b/homeassistant/components/esphome/fan.py
@@ -1,5 +1,7 @@
"""Support for ESPHome fans."""
-from typing import Optional
+from __future__ import annotations
+
+import math
from aioesphomeapi import FanDirection, FanInfo, FanSpeed, FanState
@@ -16,6 +18,8 @@
from homeassistant.util.percentage import (
ordered_list_item_to_percentage,
percentage_to_ordered_list_item,
+ percentage_to_ranged_value,
+ ranged_value_to_percentage,
)
from . import (
@@ -59,9 +63,14 @@ def _static_info(self) -> FanInfo:
return super()._static_info
@property
- def _state(self) -> Optional[FanState]:
+ def _state(self) -> FanState | None:
return super()._state
+ @property
+ def _supports_speed_levels(self) -> bool:
+ api_version = self._client.api_version
+ return api_version.major == 1 and api_version.minor > 3
+
async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed percentage of the fan."""
if percentage == 0:
@@ -70,17 +79,24 @@ async def async_set_percentage(self, percentage: int) -> None:
data = {"key": self._static_info.key, "state": True}
if percentage is not None:
- named_speed = percentage_to_ordered_list_item(
- ORDERED_NAMED_FAN_SPEEDS, percentage
- )
- data["speed"] = named_speed
+ if self._supports_speed_levels:
+ data["speed_level"] = math.ceil(
+ percentage_to_ranged_value(
+ (1, self._static_info.supported_speed_levels), percentage
+ )
+ )
+ else:
+ named_speed = percentage_to_ordered_list_item(
+ ORDERED_NAMED_FAN_SPEEDS, percentage
+ )
+ data["speed"] = named_speed
await self._client.fan_command(**data)
async def async_turn_on(
self,
- speed: Optional[str] = None,
- percentage: Optional[int] = None,
- preset_mode: Optional[str] = None,
+ speed: str | None = None,
+ percentage: int | None = None,
+ preset_mode: str | None = None,
**kwargs,
) -> None:
"""Turn on the fan."""
@@ -106,19 +122,32 @@ async def async_set_direction(self, direction: str):
# pylint: disable=invalid-overridden-method
@esphome_state_property
- def is_on(self) -> Optional[bool]:
+ def is_on(self) -> bool | None:
"""Return true if the entity is on."""
return self._state.state
@esphome_state_property
- def percentage(self) -> Optional[str]:
+ def percentage(self) -> int | None:
"""Return the current speed percentage."""
if not self._static_info.supports_speed:
return None
- return ordered_list_item_to_percentage(
- ORDERED_NAMED_FAN_SPEEDS, self._state.speed
+
+ if not self._supports_speed_levels:
+ return ordered_list_item_to_percentage(
+ ORDERED_NAMED_FAN_SPEEDS, self._state.speed
+ )
+
+ return ranged_value_to_percentage(
+ (1, self._static_info.supported_speed_levels), self._state.speed_level
)
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ if not self._supports_speed_levels:
+ return len(ORDERED_NAMED_FAN_SPEEDS)
+ return self._static_info.supported_speed_levels
+
@esphome_state_property
def oscillating(self) -> None:
"""Return the oscillation state."""
diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py
index adbb36188fd9d1..29fd969d479e44 100644
--- a/homeassistant/components/esphome/light.py
+++ b/homeassistant/components/esphome/light.py
@@ -1,5 +1,5 @@
"""Support for ESPHome lights."""
-from typing import List, Optional, Tuple
+from __future__ import annotations
from aioesphomeapi import LightInfo, LightState
@@ -54,14 +54,14 @@ def _static_info(self) -> LightInfo:
return super()._static_info
@property
- def _state(self) -> Optional[LightState]:
+ def _state(self) -> LightState | None:
return super()._state
# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
# pylint: disable=invalid-overridden-method
@esphome_state_property
- def is_on(self) -> Optional[bool]:
+ def is_on(self) -> bool | None:
"""Return true if the switch is on."""
return self._state.state
@@ -96,29 +96,29 @@ async def async_turn_off(self, **kwargs) -> None:
await self._client.light_command(**data)
@esphome_state_property
- def brightness(self) -> Optional[int]:
+ def brightness(self) -> int | None:
"""Return the brightness of this light between 0..255."""
return round(self._state.brightness * 255)
@esphome_state_property
- def hs_color(self) -> Optional[Tuple[float, float]]:
+ def hs_color(self) -> tuple[float, float] | None:
"""Return the hue and saturation color value [float, float]."""
return color_util.color_RGB_to_hs(
self._state.red * 255, self._state.green * 255, self._state.blue * 255
)
@esphome_state_property
- def color_temp(self) -> Optional[float]:
+ def color_temp(self) -> float | None:
"""Return the CT color value in mireds."""
return self._state.color_temperature
@esphome_state_property
- def white_value(self) -> Optional[int]:
+ def white_value(self) -> int | None:
"""Return the white value of this light between 0..255."""
return round(self._state.white * 255)
@esphome_state_property
- def effect(self) -> Optional[str]:
+ def effect(self) -> str | None:
"""Return the current effect."""
return self._state.effect
@@ -140,7 +140,7 @@ def supported_features(self) -> int:
return flags
@property
- def effect_list(self) -> List[str]:
+ def effect_list(self) -> list[str]:
"""Return the list of supported effects."""
return self._static_info.effects
diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json
index c69f4f4d8c62c4..e3c609c9fad522 100644
--- a/homeassistant/components/esphome/manifest.json
+++ b/homeassistant/components/esphome/manifest.json
@@ -3,7 +3,7 @@
"name": "ESPHome",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/esphome",
- "requirements": ["aioesphomeapi==2.6.4"],
+ "requirements": ["aioesphomeapi==2.6.6"],
"zeroconf": ["_esphomelib._tcp.local."],
"codeowners": ["@OttoWinter"],
"after_dependencies": ["zeroconf", "tag"]
diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py
index fbf3925953b01f..d751109c159752 100644
--- a/homeassistant/components/esphome/sensor.py
+++ b/homeassistant/components/esphome/sensor.py
@@ -1,10 +1,14 @@
"""Support for esphome sensors."""
+from __future__ import annotations
+
import math
-from typing import Optional
from aioesphomeapi import SensorInfo, SensorState, TextSensorInfo, TextSensorState
+import voluptuous as vol
+from homeassistant.components.sensor import DEVICE_CLASSES, SensorEntity
from homeassistant.config_entries import ConfigEntry
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType
from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry
@@ -38,7 +42,7 @@ async def async_setup_entry(
# pylint: disable=invalid-overridden-method
-class EsphomeSensor(EsphomeEntity):
+class EsphomeSensor(EsphomeEntity, SensorEntity):
"""A sensor implementation for esphome."""
@property
@@ -46,13 +50,15 @@ def _static_info(self) -> SensorInfo:
return super()._static_info
@property
- def _state(self) -> Optional[SensorState]:
+ def _state(self) -> SensorState | None:
return super()._state
@property
def icon(self) -> str:
"""Return the icon."""
- return self._static_info.icon
+ if not self._static_info.icon or self._static_info.device_class:
+ return None
+ return vol.Schema(cv.icon)(self._static_info.icon)
@property
def force_update(self) -> bool:
@@ -60,7 +66,7 @@ def force_update(self) -> bool:
return self._static_info.force_update
@esphome_state_property
- def state(self) -> Optional[str]:
+ def state(self) -> str | None:
"""Return the state of the entity."""
if math.isnan(self._state.state):
return None
@@ -71,18 +77,27 @@ def state(self) -> Optional[str]:
@property
def unit_of_measurement(self) -> str:
"""Return the unit the value is expressed in."""
+ if not self._static_info.unit_of_measurement:
+ return None
return self._static_info.unit_of_measurement
+ @property
+ def device_class(self) -> str:
+ """Return the class of this device, from component DEVICE_CLASSES."""
+ if self._static_info.device_class not in DEVICE_CLASSES:
+ return None
+ return self._static_info.device_class
+
-class EsphomeTextSensor(EsphomeEntity):
+class EsphomeTextSensor(EsphomeEntity, SensorEntity):
"""A text sensor implementation for ESPHome."""
@property
- def _static_info(self) -> "TextSensorInfo":
+ def _static_info(self) -> TextSensorInfo:
return super()._static_info
@property
- def _state(self) -> Optional["TextSensorState"]:
+ def _state(self) -> TextSensorState | None:
return super()._state
@property
@@ -91,7 +106,7 @@ def icon(self) -> str:
return self._static_info.icon
@esphome_state_property
- def state(self) -> Optional[str]:
+ def state(self) -> str | None:
"""Return the state of the entity."""
if self._state.missing_state:
return None
diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py
index 3f8ca90d6c5a34..992f014e829f4f 100644
--- a/homeassistant/components/esphome/switch.py
+++ b/homeassistant/components/esphome/switch.py
@@ -1,5 +1,5 @@
"""Support for ESPHome switches."""
-from typing import Optional
+from __future__ import annotations
from aioesphomeapi import SwitchInfo, SwitchState
@@ -33,7 +33,7 @@ def _static_info(self) -> SwitchInfo:
return super()._static_info
@property
- def _state(self) -> Optional[SwitchState]:
+ def _state(self) -> SwitchState | None:
return super()._state
@property
@@ -49,7 +49,7 @@ def assumed_state(self) -> bool:
# https://github.com/PyCQA/pylint/issues/3150 for @esphome_state_property
# pylint: disable=invalid-overridden-method
@esphome_state_property
- def is_on(self) -> Optional[bool]:
+ def is_on(self) -> bool | None:
"""Return true if the switch is on."""
return self._state.state
diff --git a/homeassistant/components/esphome/translations/de.json b/homeassistant/components/esphome/translations/de.json
index 826574cb7e089d..fdaea452c4560a 100644
--- a/homeassistant/components/esphome/translations/de.json
+++ b/homeassistant/components/esphome/translations/de.json
@@ -2,10 +2,11 @@
"config": {
"abort": {
"already_configured": "ESP ist bereits konfiguriert",
- "already_in_progress": "Die ESP-Konfiguration wird bereits ausgef\u00fchrt"
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt"
},
"error": {
"connection_error": "Keine Verbindung zum ESP m\u00f6glich. Achte darauf, dass deine YAML-Datei eine Zeile 'api:' enth\u00e4lt.",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"resolve_error": "Adresse des ESP kann nicht aufgel\u00f6st werden. Wenn dieser Fehler weiterhin besteht, lege eine statische IP-Adresse fest: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
"flow_title": "ESPHome: {name}",
diff --git a/homeassistant/components/esphome/translations/he.json b/homeassistant/components/esphome/translations/he.json
new file mode 100644
index 00000000000000..648d007cc469fc
--- /dev/null
+++ b/homeassistant/components/esphome/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "authenticate": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json
index 46cb3228edb830..6c4586fbd558b1 100644
--- a/homeassistant/components/esphome/translations/hu.json
+++ b/homeassistant/components/esphome/translations/hu.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Az ESP-t m\u00e1r konfigur\u00e1ltad."
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van."
},
"error": {
"connection_error": "Nem lehet csatlakozni az ESP-hez. K\u00e9rlek gy\u0151z\u0151dj meg r\u00f3la, hogy a YAML f\u00e1jl tartalmaz egy \"api:\" sort.",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"resolve_error": "Az ESP c\u00edme nem oldhat\u00f3 fel. Ha a hiba tov\u00e1bbra is fenn\u00e1ll, k\u00e9rlek, \u00e1ll\u00edts be egy statikus IP-c\u00edmet: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
"flow_title": "ESPHome: {name}",
diff --git a/homeassistant/components/esphome/translations/id.json b/homeassistant/components/esphome/translations/id.json
index 9f6cd01294942b..a39a19e12db3b9 100644
--- a/homeassistant/components/esphome/translations/id.json
+++ b/homeassistant/components/esphome/translations/id.json
@@ -1,14 +1,32 @@
{
"config": {
"abort": {
- "already_configured": "ESP sudah dikonfigurasi"
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung"
},
+ "error": {
+ "connection_error": "Tidak dapat terhubung ke ESP. Pastikan file YAML Anda mengandung baris 'api:'.",
+ "invalid_auth": "Autentikasi tidak valid",
+ "resolve_error": "Tidak dapat menemukan alamat ESP. Jika kesalahan ini terus terjadi, atur alamat IP statis: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
+ },
+ "flow_title": "ESPHome: {name}",
"step": {
"authenticate": {
"data": {
- "password": "Kata kunci"
+ "password": "Kata Sandi"
+ },
+ "description": "Masukkan kata sandi yang ditetapkan di konfigurasi Anda untuk {name}."
+ },
+ "discovery_confirm": {
+ "description": "Ingin menambahkan node ESPHome `{name}` ke Home Assistant?",
+ "title": "Perangkat node ESPHome yang ditemukan"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
},
- "description": "Silakan masukkan kata kunci yang Anda atur di konfigurasi Anda."
+ "description": "Masukkan pengaturan koneksi node [ESPHome](https://esphomelib.com/)."
}
}
}
diff --git a/homeassistant/components/esphome/translations/ko.json b/homeassistant/components/esphome/translations/ko.json
index 82b9490757b7b8..3e98bdabeb5cbf 100644
--- a/homeassistant/components/esphome/translations/ko.json
+++ b/homeassistant/components/esphome/translations/ko.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "ESP \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "already_in_progress": "ESP \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4"
},
"error": {
- "connection_error": "ESP \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. YAML \ud30c\uc77c\uc5d0 'api:' \ub97c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
+ "connection_error": "ESP\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. YAML \ud30c\uc77c\uc5d0 'api:'\ub97c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"resolve_error": "ESP \uc758 \uc8fc\uc18c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uac00 \uacc4\uc18d \ubc1c\uc0dd\ud558\uba74 \uace0\uc815 IP \uc8fc\uc18c\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
"flow_title": "ESPHome: {name}",
@@ -17,7 +18,7 @@
"description": "{name} \uc758 \uad6c\uc131\uc5d0 \uc124\uc815\ud55c \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694."
},
"discovery_confirm": {
- "description": "Home Assistant \uc5d0 ESPHome node `{name}` \uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "description": "Home Assistant\uc5d0 ESPHome node `{name}`\uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "\ubc1c\uacac\ub41c ESPHome node"
},
"user": {
diff --git a/homeassistant/components/esphome/translations/nl.json b/homeassistant/components/esphome/translations/nl.json
index f32dbd1723ef97..1aae006feedf3c 100644
--- a/homeassistant/components/esphome/translations/nl.json
+++ b/homeassistant/components/esphome/translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "ESP is al geconfigureerd",
+ "already_configured": "Apparaat is al geconfigureerd",
"already_in_progress": "De configuratiestroom is al begonnen"
},
"error": {
diff --git a/homeassistant/components/esphome/translations/pt.json b/homeassistant/components/esphome/translations/pt.json
index 6ff4d786447909..60eeaa3f4b2103 100644
--- a/homeassistant/components/esphome/translations/pt.json
+++ b/homeassistant/components/esphome/translations/pt.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "O ESP j\u00e1 est\u00e1 configurado",
+ "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado",
"already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer"
},
"error": {
diff --git a/homeassistant/components/esphome/translations/ru.json b/homeassistant/components/esphome/translations/ru.json
index bcbe914885495d..4277a057a86d32 100644
--- a/homeassistant/components/esphome/translations/ru.json
+++ b/homeassistant/components/esphome/translations/ru.json
@@ -6,7 +6,7 @@
},
"error": {
"connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips."
},
"flow_title": "ESPHome: {name}",
diff --git a/homeassistant/components/esphome/translations/tr.json b/homeassistant/components/esphome/translations/tr.json
index 15028c4fe65592..81f85d4980bb0b 100644
--- a/homeassistant/components/esphome/translations/tr.json
+++ b/homeassistant/components/esphome/translations/tr.json
@@ -1,8 +1,27 @@
{
"config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor"
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
"step": {
+ "authenticate": {
+ "data": {
+ "password": "Parola"
+ },
+ "description": "L\u00fctfen yap\u0131land\u0131rman\u0131zda {name} i\u00e7in belirledi\u011finiz parolay\u0131 girin."
+ },
"discovery_confirm": {
"title": "Ke\u015ffedilen ESPHome d\u00fc\u011f\u00fcm\u00fc"
+ },
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ }
}
}
}
diff --git a/homeassistant/components/esphome/translations/uk.json b/homeassistant/components/esphome/translations/uk.json
index d17ec64e5480e5..4643c19cf5ddeb 100644
--- a/homeassistant/components/esphome/translations/uk.json
+++ b/homeassistant/components/esphome/translations/uk.json
@@ -1,22 +1,25 @@
{
"config": {
"abort": {
- "already_configured": "ESP \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e"
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454."
},
"error": {
"connection_error": "\u041d\u0435 \u0432\u0434\u0430\u0454\u0442\u044c\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e ESP. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0444\u0430\u0439\u043b YAML \u043c\u0456\u0441\u0442\u0438\u0442\u044c \u0440\u044f\u0434\u043e\u043a \"api:\".",
- "resolve_error": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0430\u0434\u0440\u0435\u0441\u0443 ESP. \u042f\u043a\u0449\u043e \u0446\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043d\u0435 \u0437\u043d\u0438\u043a\u0430\u0454, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0456\u0442\u044c \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u0443 IP-\u0430\u0434\u0440\u0435\u0441\u0443: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "resolve_error": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 \u0430\u0434\u0440\u0435\u0441\u0443 ESP. \u042f\u043a\u0449\u043e \u0446\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044e\u0454\u0442\u044c\u0441\u044f, \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u0443 IP-\u0430\u0434\u0440\u0435\u0441\u0443: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips."
},
+ "flow_title": "ESPHome: {name}",
"step": {
"authenticate": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c"
},
- "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c, \u044f\u043a\u0438\u0439 \u0432\u0438 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043b\u0438 \u0443 \u0441\u0432\u043e\u0457\u0439 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457."
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c, \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0439 \u0432 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457 {name}."
},
"discovery_confirm": {
"description": "\u0414\u043e\u0434\u0430\u0442\u0438 ESPHome \u0432\u0443\u0437\u043e\u043b {name} \u0443 Home Assistant?",
- "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0432\u0443\u0437\u043e\u043b ESPHome"
+ "title": "ESPHome"
},
"user": {
"data": {
diff --git a/homeassistant/components/essent/sensor.py b/homeassistant/components/essent/sensor.py
index 3ac7af315b72ab..f0dc70d7be4fde 100644
--- a/homeassistant/components/essent/sensor.py
+++ b/homeassistant/components/essent/sensor.py
@@ -1,14 +1,14 @@
"""Support for Essent API."""
+from __future__ import annotations
+
from datetime import timedelta
-from typing import Optional
from pyessent import PyEssent
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, ENERGY_KILO_WATT_HOUR
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
SCAN_INTERVAL = timedelta(hours=1)
@@ -81,7 +81,7 @@ def update(self):
self._meter_data[possible_meter] = meter_data
-class EssentMeter(Entity):
+class EssentMeter(SensorEntity):
"""Representation of Essent measurements."""
def __init__(self, essent_base, meter, meter_type, tariff, unit):
@@ -94,7 +94,7 @@ def __init__(self, essent_base, meter, meter_type, tariff, unit):
self._unit = unit
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID."""
return f"{self._meter}-{self._type}-{self._tariff}"
diff --git a/homeassistant/components/etherscan/sensor.py b/homeassistant/components/etherscan/sensor.py
index 1c14ce578c16fc..1fa2edbf2e8f1b 100644
--- a/homeassistant/components/etherscan/sensor.py
+++ b/homeassistant/components/etherscan/sensor.py
@@ -4,10 +4,9 @@
from pyetherscan import get_balance
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME, CONF_TOKEN
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
ATTRIBUTION = "Data provided by etherscan.io"
@@ -42,7 +41,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([EtherscanSensor(name, address, token, token_address)], True)
-class EtherscanSensor(Entity):
+class EtherscanSensor(SensorEntity):
"""Representation of an Etherscan.io sensor."""
def __init__(self, name, address, token, token_address):
@@ -70,7 +69,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py
index 243bd2913b1c06..e451b83c1fa534 100644
--- a/homeassistant/components/everlights/light.py
+++ b/homeassistant/components/everlights/light.py
@@ -1,7 +1,8 @@
"""Support for EverLights lights."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Tuple
import pyeverlights
import voluptuous as vol
@@ -38,7 +39,7 @@ def color_rgb_to_int(red: int, green: int, blue: int) -> int:
return red * 256 * 256 + green * 256 + blue
-def color_int_to_rgb(value: int) -> Tuple[int, int, int]:
+def color_int_to_rgb(value: int) -> tuple[int, int, int]:
"""Return an RGB tuple from an integer."""
return (value >> 16, (value >> 8) & 0xFF, value & 0xFF)
diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py
index 268e7709af38c4..8c83308a8b7673 100644
--- a/homeassistant/components/evohome/__init__.py
+++ b/homeassistant/components/evohome/__init__.py
@@ -2,10 +2,12 @@
Such systems include evohome, Round Thermostat, and others.
"""
+from __future__ import annotations
+
from datetime import datetime as dt, timedelta
import logging
import re
-from typing import Any, Dict, Optional, Tuple
+from typing import Any
import aiohttp.client_exceptions
import evohomeasync
@@ -114,7 +116,7 @@ def convert_until(status_dict: dict, until_key: str) -> None:
status_dict[until_key] = dt_util.as_local(dt_utc_naive).isoformat()
-def convert_dict(dictionary: Dict[str, Any]) -> Dict[str, Any]:
+def convert_dict(dictionary: dict[str, Any]) -> dict[str, Any]:
"""Recursively convert a dict's keys to snake_case."""
def convert_key(key: str) -> str:
@@ -176,7 +178,7 @@ def _handle_exception(err) -> bool:
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
"""Create a (EMEA/EU-based) Honeywell TCC system."""
- async def load_auth_tokens(store) -> Tuple[Dict, Optional[Dict]]:
+ async def load_auth_tokens(store) -> tuple[dict, dict | None]:
app_storage = await store.async_load()
tokens = dict(app_storage if app_storage else {})
@@ -435,7 +437,7 @@ async def call_client_api(self, api_function, update_state=True) -> Any:
async def _update_v1_api_temps(self, *args, **kwargs) -> None:
"""Get the latest high-precision temperatures of the default Location."""
- def get_session_id(client_v1) -> Optional[str]:
+ def get_session_id(client_v1) -> str | None:
user_data = client_v1.user_data if client_v1 else None
return user_data.get("sessionId") if user_data else None
@@ -520,7 +522,7 @@ def __init__(self, evo_broker, evo_device) -> None:
self._supported_features = None
self._device_state_attrs = {}
- async def async_refresh(self, payload: Optional[dict] = None) -> None:
+ async def async_refresh(self, payload: dict | None = None) -> None:
"""Process any signals."""
if payload is None:
self.async_schedule_update_ha_state(force_refresh=True)
@@ -546,7 +548,7 @@ def should_poll(self) -> bool:
return False
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID."""
return self._unique_id
@@ -556,7 +558,7 @@ def name(self) -> str:
return self._name
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the evohome-specific state attributes."""
status = self._device_state_attrs
if "systemModeStatus" in status:
@@ -606,7 +608,7 @@ def __init__(self, evo_broker, evo_device) -> None:
self._setpoints = {}
@property
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the current temperature of a Zone."""
if (
self._evo_broker.temps
@@ -618,7 +620,7 @@ def current_temperature(self) -> Optional[float]:
return self._evo_device.temperatureStatus["temperature"]
@property
- def setpoints(self) -> Dict[str, Any]:
+ def setpoints(self) -> dict[str, Any]:
"""Return the current/next setpoints from the schedule.
Only Zones & DHW controllers (but not the TCS) can have schedules.
diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py
index e99cae5e22e5d3..f291fcd9cb3084 100644
--- a/homeassistant/components/evohome/climate.py
+++ b/homeassistant/components/evohome/climate.py
@@ -1,7 +1,8 @@
"""Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems."""
+from __future__ import annotations
+
from datetime import datetime as dt
import logging
-from typing import List, Optional
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
@@ -129,12 +130,12 @@ def __init__(self, evo_broker, evo_device) -> None:
self._preset_modes = None
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return a list of available hvac operation modes."""
return list(HA_HVAC_TO_TCS)
@property
- def preset_modes(self) -> Optional[List[str]]:
+ def preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes."""
return self._preset_modes
@@ -203,7 +204,7 @@ def target_temperature(self) -> float:
return self._evo_device.setpointStatus["targetHeatTemperature"]
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp."""
if self._evo_tcs.systemModeStatus["mode"] in [EVO_AWAY, EVO_HEATOFF]:
return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"])
@@ -268,7 +269,7 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None:
self._evo_device.cancel_temp_override()
)
- async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None:
+ async def async_set_preset_mode(self, preset_mode: str | None) -> None:
"""Set the preset mode; if None, then revert to following the schedule."""
evo_preset_mode = HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)
@@ -347,7 +348,7 @@ async def async_tcs_svc_request(self, service: dict, data: dict) -> None:
await self._set_tcs_mode(mode, until=until)
- async def _set_tcs_mode(self, mode: str, until: Optional[dt] = None) -> None:
+ async def _set_tcs_mode(self, mode: str, until: dt | None = None) -> None:
"""Set a Controller to any of its native EVO_* operating modes."""
until = dt_util.as_utc(until) if until else None
await self._evo_broker.call_client_api(
@@ -361,7 +362,7 @@ def hvac_mode(self) -> str:
return HVAC_MODE_OFF if tcs_mode == EVO_HEATOFF else HVAC_MODE_HEAT
@property
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the average current temperature of the heating Zones.
Controllers do not have a current temp, but one is expected by HA.
@@ -374,7 +375,7 @@ def current_temperature(self) -> Optional[float]:
return round(sum(temps) / len(temps), 1) if temps else None
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp."""
return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"])
@@ -396,7 +397,7 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set an operating mode for a Controller."""
await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode))
- async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None:
+ async def async_set_preset_mode(self, preset_mode: str | None) -> None:
"""Set the preset mode; if None, then revert to 'Auto' mode."""
await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO))
diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json
index 8bcecca551b14d..e707387ce4f000 100644
--- a/homeassistant/components/evohome/manifest.json
+++ b/homeassistant/components/evohome/manifest.json
@@ -2,6 +2,6 @@
"domain": "evohome",
"name": "Honeywell Total Connect Comfort (Europe)",
"documentation": "https://www.home-assistant.io/integrations/evohome",
- "requirements": ["evohome-async==0.3.5.post1"],
+ "requirements": ["evohome-async==0.3.8"],
"codeowners": ["@zxdavb"]
}
diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py
index 846c8c09155320..4e05c5534616e2 100644
--- a/homeassistant/components/evohome/water_heater.py
+++ b/homeassistant/components/evohome/water_heater.py
@@ -1,6 +1,7 @@
"""Support for WaterHeater devices of (EMEA/EU) Honeywell TCC systems."""
+from __future__ import annotations
+
import logging
-from typing import List
from homeassistant.components.water_heater import (
SUPPORT_AWAY_MODE,
@@ -70,7 +71,7 @@ def current_operation(self) -> str:
return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]]
@property
- def operation_list(self) -> List[str]:
+ def operation_list(self) -> list[str]:
"""Return the list of available operations."""
return list(HA_STATE_TO_EVO)
diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py
index 96891e8b291e47..7619d83e27bc8b 100644
--- a/homeassistant/components/ezviz/__init__.py
+++ b/homeassistant/components/ezviz/__init__.py
@@ -1 +1,129 @@
-"""Support for Ezviz devices via Ezviz Cloud API."""
+"""Support for Ezviz camera."""
+import asyncio
+from datetime import timedelta
+import logging
+
+from pyezviz.client import EzvizClient, HTTPError, InvalidURL, PyEzvizError
+
+from homeassistant.const import (
+ CONF_PASSWORD,
+ CONF_TIMEOUT,
+ CONF_TYPE,
+ CONF_URL,
+ CONF_USERNAME,
+)
+from homeassistant.exceptions import ConfigEntryNotReady
+
+from .const import (
+ ATTR_TYPE_CAMERA,
+ ATTR_TYPE_CLOUD,
+ CONF_FFMPEG_ARGUMENTS,
+ DATA_COORDINATOR,
+ DATA_UNDO_UPDATE_LISTENER,
+ DEFAULT_FFMPEG_ARGUMENTS,
+ DEFAULT_TIMEOUT,
+ DOMAIN,
+)
+from .coordinator import EzvizDataUpdateCoordinator
+
+_LOGGER = logging.getLogger(__name__)
+
+MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
+
+PLATFORMS = [
+ "binary_sensor",
+ "camera",
+ "sensor",
+ "switch",
+]
+
+
+async def async_setup_entry(hass, entry):
+ """Set up Ezviz from a config entry."""
+ hass.data.setdefault(DOMAIN, {})
+
+ if not entry.options:
+ options = {
+ CONF_FFMPEG_ARGUMENTS: entry.data.get(
+ CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS
+ ),
+ CONF_TIMEOUT: entry.data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
+ }
+ hass.config_entries.async_update_entry(entry, options=options)
+
+ if entry.data.get(CONF_TYPE) == ATTR_TYPE_CAMERA:
+ if hass.data.get(DOMAIN):
+ # Should only execute on addition of new camera entry.
+ # 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")
+ await hass.config_entries.async_reload(item.entry_id)
+
+ return True
+
+ try:
+ ezviz_client = await hass.async_add_executor_job(
+ _get_ezviz_client_instance, entry
+ )
+ except (InvalidURL, HTTPError, PyEzvizError) as error:
+ _LOGGER.error("Unable to connect to Ezviz service: %s", str(error))
+ raise ConfigEntryNotReady from error
+
+ coordinator = EzvizDataUpdateCoordinator(hass, api=ezviz_client)
+ await coordinator.async_refresh()
+
+ if not coordinator.last_update_success:
+ raise ConfigEntryNotReady
+
+ undo_listener = entry.add_update_listener(_async_update_listener)
+
+ hass.data[DOMAIN][entry.entry_id] = {
+ DATA_COORDINATOR: coordinator,
+ DATA_UNDO_UPDATE_LISTENER: undo_listener,
+ }
+ for component in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, component)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass, entry):
+ """Unload a config entry."""
+
+ if entry.data.get(CONF_TYPE) == ATTR_TYPE_CAMERA:
+ return True
+
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, component)
+ for component in PLATFORMS
+ ]
+ )
+ )
+
+ if unload_ok:
+ hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]()
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
+
+
+async def _async_update_listener(hass, entry):
+ """Handle options update."""
+ await hass.config_entries.async_reload(entry.entry_id)
+
+
+def _get_ezviz_client_instance(entry):
+ """Initialize a new instance of EzvizClientApi."""
+ ezviz_client = EzvizClient(
+ entry.data[CONF_USERNAME],
+ entry.data[CONF_PASSWORD],
+ entry.data[CONF_URL],
+ entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
+ )
+ ezviz_client.login()
+ return ezviz_client
diff --git a/homeassistant/components/ezviz/binary_sensor.py b/homeassistant/components/ezviz/binary_sensor.py
new file mode 100644
index 00000000000000..9d8db7fbb30ee8
--- /dev/null
+++ b/homeassistant/components/ezviz/binary_sensor.py
@@ -0,0 +1,77 @@
+"""Support for Ezviz binary sensors."""
+import logging
+
+from pyezviz.constants import BinarySensorType
+
+from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up Ezviz sensors based on a config entry."""
+ coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
+ sensors = []
+ sensor_type_name = "None"
+
+ for idx, camera in enumerate(coordinator.data):
+ for name in camera:
+ # Only add sensor with value.
+ if camera.get(name) is None:
+ continue
+
+ if name in BinarySensorType.__members__:
+ sensor_type_name = getattr(BinarySensorType, name).value
+ sensors.append(
+ EzvizBinarySensor(coordinator, idx, name, sensor_type_name)
+ )
+
+ async_add_entities(sensors)
+
+
+class EzvizBinarySensor(CoordinatorEntity, BinarySensorEntity):
+ """Representation of a Ezviz sensor."""
+
+ def __init__(self, coordinator, idx, name, sensor_type_name):
+ """Initialize the sensor."""
+ super().__init__(coordinator)
+ self._idx = idx
+ self._camera_name = self.coordinator.data[self._idx]["name"]
+ self._name = name
+ self._sensor_name = f"{self._camera_name}.{self._name}"
+ self.sensor_type_name = sensor_type_name
+ self._serial = self.coordinator.data[self._idx]["serial"]
+
+ @property
+ def name(self):
+ """Return the name of the Ezviz sensor."""
+ return self._sensor_name
+
+ @property
+ def is_on(self):
+ """Return the state of the sensor."""
+ return self.coordinator.data[self._idx][self._name]
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of this sensor."""
+ return f"{self._serial}_{self._sensor_name}"
+
+ @property
+ def device_info(self):
+ """Return the device_info of the device."""
+ return {
+ "identifiers": {(DOMAIN, self._serial)},
+ "name": self.coordinator.data[self._idx]["name"],
+ "model": self.coordinator.data[self._idx]["device_sub_category"],
+ "manufacturer": MANUFACTURER,
+ "sw_version": self.coordinator.data[self._idx]["version"],
+ }
+
+ @property
+ def device_class(self):
+ """Device class for the sensor."""
+ return self.sensor_type_name
diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py
index b9b2463314b166..919ff5039b2166 100644
--- a/homeassistant/components/ezviz/camera.py
+++ b/homeassistant/components/ezviz/camera.py
@@ -1,28 +1,30 @@
-"""This component provides basic support for Ezviz IP cameras."""
+"""Support ezviz camera devices."""
import asyncio
+from datetime import timedelta
import logging
-# pylint: disable=import-error
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
-from pyezviz.camera import EzvizCamera
-from pyezviz.client import EzvizClient, PyEzvizError
import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant.components.ffmpeg import DATA_FFMPEG
+from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IGNORE, SOURCE_IMPORT
+from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import config_validation as cv
-
-_LOGGER = logging.getLogger(__name__)
-
-CONF_CAMERAS = "cameras"
-
-DEFAULT_CAMERA_USERNAME = "admin"
-DEFAULT_RTSP_PORT = "554"
-
-DATA_FFMPEG = "ffmpeg"
-
-EZVIZ_DATA = "ezviz"
-ENTITIES = "entities"
+from homeassistant.helpers.restore_state import RestoreEntity
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import (
+ ATTR_SERIAL,
+ CONF_CAMERAS,
+ CONF_FFMPEG_ARGUMENTS,
+ DATA_COORDINATOR,
+ DEFAULT_CAMERA_USERNAME,
+ DEFAULT_FFMPEG_ARGUMENTS,
+ DEFAULT_RTSP_PORT,
+ DOMAIN,
+ MANUFACTURER,
+)
CAMERA_SCHEMA = vol.Schema(
{vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string}
@@ -36,162 +38,162 @@
}
)
+_LOGGER = logging.getLogger(__name__)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Ezviz IP Cameras."""
-
- conf_cameras = config[CONF_CAMERAS]
+MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90)
- account = config[CONF_USERNAME]
- password = config[CONF_PASSWORD]
- try:
- ezviz_client = EzvizClient(account, password)
- ezviz_client.login()
- cameras = ezviz_client.load_cameras()
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Set up a Ezviz IP Camera from platform config."""
+ _LOGGER.warning(
+ "Loading ezviz via platform config is deprecated, it will be automatically imported. Please remove it afterwards"
+ )
- except PyEzvizError as exp:
- _LOGGER.error(exp)
+ # Check if entry config exists and skips import if it does.
+ if hass.config_entries.async_entries(DOMAIN):
return
- # now, let's build the HASS devices
- camera_entities = []
+ # Check if importing camera account.
+ if CONF_CAMERAS in config:
+ cameras_conf = config[CONF_CAMERAS]
+ for serial, camera in cameras_conf.items():
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data={
+ ATTR_SERIAL: serial,
+ CONF_USERNAME: camera[CONF_USERNAME],
+ CONF_PASSWORD: camera[CONF_PASSWORD],
+ },
+ )
+ )
+
+ # Check if importing main ezviz cloud account.
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data=config,
+ )
+ )
+
- # Add the cameras as devices in HASS
- for camera in cameras:
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up Ezviz cameras based on a config entry."""
- camera_username = DEFAULT_CAMERA_USERNAME
- camera_password = ""
- camera_rtsp_stream = ""
- camera_serial = camera["serial"]
+ coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
+ camera_config_entries = hass.config_entries.async_entries(DOMAIN)
+
+ camera_entities = []
+
+ for idx, camera in enumerate(coordinator.data):
# There seem to be a bug related to localRtspPort in Ezviz API...
local_rtsp_port = DEFAULT_RTSP_PORT
- if camera["local_rtsp_port"] and camera["local_rtsp_port"] != 0:
+
+ camera_rtsp_entry = [
+ item
+ for item in camera_config_entries
+ if item.unique_id == camera[ATTR_SERIAL]
+ ]
+
+ if camera["local_rtsp_port"] != 0:
local_rtsp_port = camera["local_rtsp_port"]
- if camera_serial in conf_cameras:
- camera_username = conf_cameras[camera_serial][CONF_USERNAME]
- camera_password = conf_cameras[camera_serial][CONF_PASSWORD]
- camera_rtsp_stream = f"rtsp://{camera_username}:{camera_password}@{camera['local_ip']}:{local_rtsp_port}"
+ if camera_rtsp_entry:
+ conf_cameras = camera_rtsp_entry[0]
+
+ # Skip ignored entities.
+ if conf_cameras.source == SOURCE_IGNORE:
+ continue
+
+ ffmpeg_arguments = conf_cameras.options.get(
+ CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS
+ )
+
+ camera_username = conf_cameras.data[CONF_USERNAME]
+ camera_password = conf_cameras.data[CONF_PASSWORD]
+
+ camera_rtsp_stream = f"rtsp://{camera_username}:{camera_password}@{camera['local_ip']}:{local_rtsp_port}{ffmpeg_arguments}"
_LOGGER.debug(
- "Camera %s source stream: %s", camera["serial"], camera_rtsp_stream
+ "Camera %s source stream: %s", camera[ATTR_SERIAL], camera_rtsp_stream
)
else:
- _LOGGER.info(
- "Found camera with serial %s without configuration. Add it to configuration.yaml to see the camera stream",
- camera_serial,
- )
- camera["username"] = camera_username
- camera["password"] = camera_password
- camera["rtsp_stream"] = camera_rtsp_stream
-
- camera["ezviz_camera"] = EzvizCamera(ezviz_client, camera_serial)
-
- camera_entities.append(HassEzvizCamera(**camera))
-
- add_entities(camera_entities)
-
-
-class HassEzvizCamera(Camera):
- """An implementation of a Foscam IP camera."""
-
- def __init__(self, **data):
- """Initialize an Ezviz camera."""
- super().__init__()
-
- self._username = data["username"]
- self._password = data["password"]
- self._rtsp_stream = data["rtsp_stream"]
-
- self._ezviz_camera = data["ezviz_camera"]
- self._serial = data["serial"]
- self._name = data["name"]
- self._status = data["status"]
- self._privacy = data["privacy"]
- self._audio = data["audio"]
- self._ir_led = data["ir_led"]
- self._state_led = data["state_led"]
- self._follow_move = data["follow_move"]
- self._alarm_notify = data["alarm_notify"]
- self._alarm_sound_mod = data["alarm_sound_mod"]
- self._encrypted = data["encrypted"]
- self._local_ip = data["local_ip"]
- self._detection_sensibility = data["detection_sensibility"]
- self._device_sub_category = data["device_sub_category"]
- self._local_rtsp_port = data["local_rtsp_port"]
-
- self._ffmpeg = None
-
- def update(self):
- """Update the camera states."""
-
- data = self._ezviz_camera.status()
-
- self._name = data["name"]
- self._status = data["status"]
- self._privacy = data["privacy"]
- self._audio = data["audio"]
- self._ir_led = data["ir_led"]
- self._state_led = data["state_led"]
- self._follow_move = data["follow_move"]
- self._alarm_notify = data["alarm_notify"]
- self._alarm_sound_mod = data["alarm_sound_mod"]
- self._encrypted = data["encrypted"]
- self._local_ip = data["local_ip"]
- self._detection_sensibility = data["detection_sensibility"]
- self._device_sub_category = data["device_sub_category"]
- self._local_rtsp_port = data["local_rtsp_port"]
-
- async def async_added_to_hass(self):
- """Subscribe to ffmpeg and add camera to list."""
- self._ffmpeg = self.hass.data[DATA_FFMPEG]
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_DISCOVERY},
+ data={
+ ATTR_SERIAL: camera[ATTR_SERIAL],
+ CONF_IP_ADDRESS: camera["local_ip"],
+ },
+ )
+ )
- @property
- def should_poll(self) -> bool:
- """Return True if entity has to be polled for state.
+ camera_username = DEFAULT_CAMERA_USERNAME
+ camera_password = ""
+ camera_rtsp_stream = ""
+ ffmpeg_arguments = DEFAULT_FFMPEG_ARGUMENTS
+ _LOGGER.warning(
+ "Found camera with serial %s without configuration. Please go to integration to complete setup",
+ camera[ATTR_SERIAL],
+ )
- False if entity pushes its state to HA.
- """
- return True
+ camera_entities.append(
+ EzvizCamera(
+ hass,
+ coordinator,
+ idx,
+ camera_username,
+ camera_password,
+ camera_rtsp_stream,
+ local_rtsp_port,
+ ffmpeg_arguments,
+ )
+ )
- @property
- def device_state_attributes(self):
- """Return the Ezviz-specific camera state attributes."""
- return {
- # if privacy == true, the device closed the lid or did a 180° tilt
- "privacy": self._privacy,
- # is the camera listening ?
- "audio": self._audio,
- # infrared led on ?
- "ir_led": self._ir_led,
- # state led on ?
- "state_led": self._state_led,
- # if true, the camera will move automatically to follow movements
- "follow_move": self._follow_move,
- # if true, if some movement is detected, the app is notified
- "alarm_notify": self._alarm_notify,
- # if true, if some movement is detected, the camera makes some sound
- "alarm_sound_mod": self._alarm_sound_mod,
- # are the camera's stored videos/images encrypted?
- "encrypted": self._encrypted,
- # camera's local ip on local network
- "local_ip": self._local_ip,
- # from 1 to 9, the higher is the sensibility, the more it will detect small movements
- "detection_sensibility": self._detection_sensibility,
- }
+ async_add_entities(camera_entities)
+
+
+class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity):
+ """An implementation of a Ezviz security camera."""
+
+ def __init__(
+ self,
+ hass,
+ coordinator,
+ idx,
+ camera_username,
+ camera_password,
+ camera_rtsp_stream,
+ local_rtsp_port,
+ ffmpeg_arguments,
+ ):
+ """Initialize a Ezviz security camera."""
+ super().__init__(coordinator)
+ Camera.__init__(self)
+ self._username = camera_username
+ self._password = camera_password
+ self._rtsp_stream = camera_rtsp_stream
+ self._idx = idx
+ self._ffmpeg = hass.data[DATA_FFMPEG]
+ self._local_rtsp_port = local_rtsp_port
+ self._ffmpeg_arguments = ffmpeg_arguments
+
+ self._serial = self.coordinator.data[self._idx]["serial"]
+ self._name = self.coordinator.data[self._idx]["name"]
+ self._local_ip = self.coordinator.data[self._idx]["local_ip"]
@property
def available(self):
"""Return True if entity is available."""
- return self._status
+ if self.coordinator.data[self._idx]["status"] == 2:
+ return False
- @property
- def brand(self):
- """Return the camera brand."""
- return "Ezviz"
+ return True
@property
def supported_features(self):
@@ -200,20 +202,40 @@ def supported_features(self):
return SUPPORT_STREAM
return 0
+ @property
+ def name(self):
+ """Return the name of this device."""
+ return self._name
+
@property
def model(self):
- """Return the camera model."""
- return self._device_sub_category
+ """Return the model of this device."""
+ return self.coordinator.data[self._idx]["device_sub_category"]
+
+ @property
+ def brand(self):
+ """Return the manufacturer of this device."""
+ return MANUFACTURER
@property
def is_on(self):
"""Return true if on."""
- return self._status
+ return bool(self.coordinator.data[self._idx]["status"])
@property
- def name(self):
+ def is_recording(self):
+ """Return true if the device is recording."""
+ return self.coordinator.data[self._idx]["alarm_notify"]
+
+ @property
+ def motion_detection_enabled(self):
+ """Camera Motion Detection Status."""
+ return self.coordinator.data[self._idx]["alarm_notify"]
+
+ @property
+ def unique_id(self):
"""Return the name of this camera."""
- return self._name
+ return self._serial
async def async_camera_image(self):
"""Return a frame from the camera stream."""
@@ -224,12 +246,24 @@ async def async_camera_image(self):
)
return image
+ @property
+ def device_info(self):
+ """Return the device_info of the device."""
+ return {
+ "identifiers": {(DOMAIN, self._serial)},
+ "name": self.coordinator.data[self._idx]["name"],
+ "model": self.coordinator.data[self._idx]["device_sub_category"],
+ "manufacturer": MANUFACTURER,
+ "sw_version": self.coordinator.data[self._idx]["version"],
+ }
+
async def stream_source(self):
"""Return the stream source."""
+ local_ip = self.coordinator.data[self._idx]["local_ip"]
if self._local_rtsp_port:
rtsp_stream_source = (
f"rtsp://{self._username}:{self._password}@"
- f"{self._local_ip}:{self._local_rtsp_port}"
+ f"{local_ip}:{self._local_rtsp_port}{self._ffmpeg_arguments}"
)
_LOGGER.debug(
"Camera %s source stream: %s", self._serial, rtsp_stream_source
diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py
new file mode 100644
index 00000000000000..ba514879703f07
--- /dev/null
+++ b/homeassistant/components/ezviz/config_flow.py
@@ -0,0 +1,374 @@
+"""Config flow for ezviz."""
+import logging
+
+from pyezviz.client import EzvizClient, HTTPError, InvalidURL, PyEzvizError
+from pyezviz.test_cam_rtsp import AuthTestResultFailed, InvalidHost, TestRTSPAuth
+import voluptuous as vol
+
+from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL, ConfigFlow, OptionsFlow
+from homeassistant.const import (
+ CONF_CUSTOMIZE,
+ CONF_IP_ADDRESS,
+ CONF_PASSWORD,
+ CONF_TIMEOUT,
+ CONF_TYPE,
+ CONF_URL,
+ CONF_USERNAME,
+)
+from homeassistant.core import callback
+
+from .const import ( # pylint: disable=unused-import
+ ATTR_SERIAL,
+ ATTR_TYPE_CAMERA,
+ ATTR_TYPE_CLOUD,
+ CONF_FFMPEG_ARGUMENTS,
+ DEFAULT_CAMERA_USERNAME,
+ DEFAULT_FFMPEG_ARGUMENTS,
+ DEFAULT_TIMEOUT,
+ DOMAIN,
+ EU_URL,
+ RUSSIA_URL,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def _get_ezviz_client_instance(data):
+ """Initialize a new instance of EzvizClientApi."""
+
+ ezviz_client = EzvizClient(
+ data[CONF_USERNAME],
+ data[CONF_PASSWORD],
+ data.get(CONF_URL, EU_URL),
+ data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
+ )
+
+ ezviz_client.login()
+ return ezviz_client
+
+
+def _test_camera_rtsp_creds(data):
+ """Try DESCRIBE on RTSP camera with credentials."""
+
+ test_rtsp = TestRTSPAuth(
+ data[CONF_IP_ADDRESS], data[CONF_USERNAME], data[CONF_PASSWORD]
+ )
+
+ test_rtsp.main()
+
+
+class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Ezviz."""
+
+ VERSION = 1
+ CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL
+
+ async def _validate_and_create_auth(self, data):
+ """Try to login to ezviz cloud account and create entry if successful."""
+ await self.async_set_unique_id(data[CONF_USERNAME])
+ self._abort_if_unique_id_configured()
+
+ # Verify cloud credentials by attempting a login request.
+ try:
+ await self.hass.async_add_executor_job(_get_ezviz_client_instance, data)
+
+ except InvalidURL as err:
+ raise InvalidURL from err
+
+ except HTTPError as err:
+ raise InvalidHost from err
+
+ except PyEzvizError as err:
+ raise PyEzvizError from err
+
+ auth_data = {
+ CONF_USERNAME: data[CONF_USERNAME],
+ CONF_PASSWORD: data[CONF_PASSWORD],
+ CONF_URL: data.get(CONF_URL, EU_URL),
+ CONF_TYPE: ATTR_TYPE_CLOUD,
+ }
+
+ return self.async_create_entry(title=data[CONF_USERNAME], data=auth_data)
+
+ async def _validate_and_create_camera_rtsp(self, data):
+ """Try DESCRIBE on RTSP camera with credentials."""
+
+ # Get Ezviz cloud credentials from config entry
+ ezviz_client_creds = {
+ CONF_USERNAME: None,
+ CONF_PASSWORD: None,
+ CONF_URL: None,
+ }
+
+ for item in self._async_current_entries():
+ if item.data.get(CONF_TYPE) == ATTR_TYPE_CLOUD:
+ ezviz_client_creds = {
+ CONF_USERNAME: item.data.get(CONF_USERNAME),
+ CONF_PASSWORD: item.data.get(CONF_PASSWORD),
+ CONF_URL: item.data.get(CONF_URL),
+ }
+
+ # Abort flow if user removed cloud account before adding camera.
+ if ezviz_client_creds[CONF_USERNAME] is None:
+ return self.async_abort(reason="ezviz_cloud_account_missing")
+
+ # We need to wake hibernating cameras.
+ # First create EZVIZ API instance.
+ try:
+ ezviz_client = await self.hass.async_add_executor_job(
+ _get_ezviz_client_instance, ezviz_client_creds
+ )
+
+ except InvalidURL as err:
+ raise InvalidURL from err
+
+ except HTTPError as err:
+ raise InvalidHost from err
+
+ except PyEzvizError as err:
+ raise PyEzvizError from err
+
+ # Secondly try to wake hybernating camera.
+ try:
+ await self.hass.async_add_executor_job(
+ ezviz_client.get_detection_sensibility, data[ATTR_SERIAL]
+ )
+
+ except HTTPError as err:
+ raise InvalidHost from err
+
+ # Thirdly attempts an authenticated RTSP DESCRIBE request.
+ try:
+ await self.hass.async_add_executor_job(_test_camera_rtsp_creds, data)
+
+ except InvalidHost as err:
+ raise InvalidHost from err
+
+ except AuthTestResultFailed as err:
+ raise AuthTestResultFailed from err
+
+ return self.async_create_entry(
+ title=data[ATTR_SERIAL],
+ data={
+ CONF_USERNAME: data[CONF_USERNAME],
+ CONF_PASSWORD: data[CONF_PASSWORD],
+ CONF_TYPE: ATTR_TYPE_CAMERA,
+ },
+ )
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for this handler."""
+ return EzvizOptionsFlowHandler(config_entry)
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initiated by the user."""
+
+ # Check if ezviz cloud account is present in entry config,
+ # abort if already configured.
+ for item in self._async_current_entries():
+ if item.data.get(CONF_TYPE) == ATTR_TYPE_CLOUD:
+ return self.async_abort(reason="already_configured_account")
+
+ errors = {}
+
+ if user_input is not None:
+
+ if user_input[CONF_URL] == CONF_CUSTOMIZE:
+ self.context["data"] = {
+ CONF_USERNAME: user_input[CONF_USERNAME],
+ CONF_PASSWORD: user_input[CONF_PASSWORD],
+ }
+ return await self.async_step_user_custom_url()
+
+ if CONF_TIMEOUT not in user_input:
+ user_input[CONF_TIMEOUT] = DEFAULT_TIMEOUT
+
+ try:
+ return await self._validate_and_create_auth(user_input)
+
+ except InvalidURL:
+ errors["base"] = "invalid_host"
+
+ except InvalidHost:
+ errors["base"] = "cannot_connect"
+
+ except PyEzvizError:
+ errors["base"] = "invalid_auth"
+
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ return self.async_abort(reason="unknown")
+
+ data_schema = vol.Schema(
+ {
+ vol.Required(CONF_USERNAME): str,
+ vol.Required(CONF_PASSWORD): str,
+ vol.Required(CONF_URL, default=EU_URL): vol.In(
+ [EU_URL, RUSSIA_URL, CONF_CUSTOMIZE]
+ ),
+ }
+ )
+
+ return self.async_show_form(
+ step_id="user", data_schema=data_schema, errors=errors
+ )
+
+ async def async_step_user_custom_url(self, user_input=None):
+ """Handle a flow initiated by the user for custom region url."""
+
+ errors = {}
+
+ if user_input is not None:
+ user_input[CONF_USERNAME] = self.context["data"][CONF_USERNAME]
+ user_input[CONF_PASSWORD] = self.context["data"][CONF_PASSWORD]
+
+ if CONF_TIMEOUT not in user_input:
+ user_input[CONF_TIMEOUT] = DEFAULT_TIMEOUT
+
+ try:
+ return await self._validate_and_create_auth(user_input)
+
+ except InvalidURL:
+ errors["base"] = "invalid_host"
+
+ except InvalidHost:
+ errors["base"] = "cannot_connect"
+
+ except PyEzvizError:
+ errors["base"] = "invalid_auth"
+
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ return self.async_abort(reason="unknown")
+
+ data_schema_custom_url = vol.Schema(
+ {
+ vol.Required(CONF_URL, default=EU_URL): str,
+ }
+ )
+
+ return self.async_show_form(
+ step_id="user_custom_url", data_schema=data_schema_custom_url, errors=errors
+ )
+
+ async def async_step_discovery(self, discovery_info):
+ """Handle a flow for discovered camera without rtsp config entry."""
+
+ await self.async_set_unique_id(discovery_info[ATTR_SERIAL])
+ self._abort_if_unique_id_configured()
+
+ self.context["title_placeholders"] = {"serial": self.unique_id}
+ self.context["data"] = {CONF_IP_ADDRESS: discovery_info[CONF_IP_ADDRESS]}
+
+ return await self.async_step_confirm()
+
+ async def async_step_confirm(self, user_input=None):
+ """Confirm and create entry from discovery step."""
+ errors = {}
+
+ if user_input is not None:
+ user_input[ATTR_SERIAL] = self.unique_id
+ user_input[CONF_IP_ADDRESS] = self.context["data"][CONF_IP_ADDRESS]
+ try:
+ return await self._validate_and_create_camera_rtsp(user_input)
+
+ except (InvalidHost, InvalidURL):
+ errors["base"] = "invalid_host"
+
+ except (PyEzvizError, AuthTestResultFailed):
+ errors["base"] = "invalid_auth"
+
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ return self.async_abort(reason="unknown")
+
+ discovered_camera_schema = vol.Schema(
+ {
+ vol.Required(CONF_USERNAME, default=DEFAULT_CAMERA_USERNAME): str,
+ vol.Required(CONF_PASSWORD): str,
+ }
+ )
+
+ return self.async_show_form(
+ step_id="confirm",
+ data_schema=discovered_camera_schema,
+ errors=errors,
+ description_placeholders={
+ "serial": self.unique_id,
+ CONF_IP_ADDRESS: self.context["data"][CONF_IP_ADDRESS],
+ },
+ )
+
+ async def async_step_import(self, import_config):
+ """Handle config import from yaml."""
+ _LOGGER.debug("import config: %s", import_config)
+
+ # Check importing camera.
+ if ATTR_SERIAL in import_config:
+ return await self.async_step_import_camera(import_config)
+
+ # Validate and setup of main ezviz cloud account.
+ try:
+ return await self._validate_and_create_auth(import_config)
+
+ except InvalidURL:
+ _LOGGER.error("Error importing Ezviz platform config: invalid host")
+ return self.async_abort(reason="invalid_host")
+
+ except InvalidHost:
+ _LOGGER.error("Error importing Ezviz platform config: cannot connect")
+ return self.async_abort(reason="cannot_connect")
+
+ except (AuthTestResultFailed, PyEzvizError):
+ _LOGGER.error("Error importing Ezviz platform config: invalid auth")
+ return self.async_abort(reason="invalid_auth")
+
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception(
+ "Error importing ezviz platform config: unexpected exception"
+ )
+
+ return self.async_abort(reason="unknown")
+
+ async def async_step_import_camera(self, data):
+ """Create RTSP auth entry per camera in config."""
+
+ await self.async_set_unique_id(data[ATTR_SERIAL])
+ self._abort_if_unique_id_configured()
+
+ _LOGGER.debug("Create camera with: %s", data)
+
+ cam_serial = data.pop(ATTR_SERIAL)
+ data[CONF_TYPE] = ATTR_TYPE_CAMERA
+
+ return self.async_create_entry(title=cam_serial, data=data)
+
+
+class EzvizOptionsFlowHandler(OptionsFlow):
+ """Handle Ezviz client options."""
+
+ def __init__(self, config_entry):
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(self, user_input=None):
+ """Manage Ezviz options."""
+ if user_input is not None:
+ return self.async_create_entry(title="", data=user_input)
+
+ options = {
+ vol.Optional(
+ CONF_TIMEOUT,
+ default=self.config_entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
+ ): int,
+ vol.Optional(
+ CONF_FFMPEG_ARGUMENTS,
+ default=self.config_entry.options.get(
+ CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS
+ ),
+ ): str,
+ }
+
+ return self.async_show_form(step_id="init", data_schema=vol.Schema(options))
diff --git a/homeassistant/components/ezviz/const.py b/homeassistant/components/ezviz/const.py
new file mode 100644
index 00000000000000..c307f0693f6b52
--- /dev/null
+++ b/homeassistant/components/ezviz/const.py
@@ -0,0 +1,42 @@
+"""Constants for the ezviz integration."""
+
+DOMAIN = "ezviz"
+MANUFACTURER = "Ezviz"
+
+# Configuration
+ATTR_SERIAL = "serial"
+CONF_CAMERAS = "cameras"
+ATTR_SWITCH = "switch"
+ATTR_ENABLE = "enable"
+ATTR_DIRECTION = "direction"
+ATTR_SPEED = "speed"
+ATTR_LEVEL = "level"
+ATTR_TYPE = "type_value"
+DIR_UP = "up"
+DIR_DOWN = "down"
+DIR_LEFT = "left"
+DIR_RIGHT = "right"
+ATTR_LIGHT = "LIGHT"
+ATTR_SOUND = "SOUND"
+ATTR_INFRARED_LIGHT = "INFRARED_LIGHT"
+ATTR_PRIVACY = "PRIVACY"
+ATTR_SLEEP = "SLEEP"
+ATTR_MOBILE_TRACKING = "MOBILE_TRACKING"
+ATTR_TRACKING = "TRACKING"
+CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments"
+ATTR_HOME = "HOME_MODE"
+ATTR_AWAY = "AWAY_MODE"
+ATTR_TYPE_CLOUD = "EZVIZ_CLOUD_ACCOUNT"
+ATTR_TYPE_CAMERA = "CAMERA_ACCOUNT"
+
+# Defaults
+EU_URL = "apiieu.ezvizlife.com"
+RUSSIA_URL = "apirus.ezvizru.com"
+DEFAULT_CAMERA_USERNAME = "admin"
+DEFAULT_RTSP_PORT = "554"
+DEFAULT_TIMEOUT = 25
+DEFAULT_FFMPEG_ARGUMENTS = ""
+
+# Data
+DATA_COORDINATOR = "coordinator"
+DATA_UNDO_UPDATE_LISTENER = "undo_update_listener"
diff --git a/homeassistant/components/ezviz/coordinator.py b/homeassistant/components/ezviz/coordinator.py
new file mode 100644
index 00000000000000..2fc9f6c9f825b3
--- /dev/null
+++ b/homeassistant/components/ezviz/coordinator.py
@@ -0,0 +1,38 @@
+"""Provides the ezviz DataUpdateCoordinator."""
+from datetime import timedelta
+import logging
+
+from async_timeout import timeout
+from pyezviz.client import HTTPError, InvalidURL, PyEzvizError
+
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class EzvizDataUpdateCoordinator(DataUpdateCoordinator):
+ """Class to manage fetching Ezviz data."""
+
+ def __init__(self, hass, *, api):
+ """Initialize global Ezviz data updater."""
+ self.ezviz_client = api
+ update_interval = timedelta(seconds=30)
+
+ super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
+
+ def _update_data(self):
+ """Fetch data from Ezviz via camera load function."""
+ cameras = self.ezviz_client.load_cameras()
+
+ return cameras
+
+ async def _async_update_data(self):
+ """Fetch data from Ezviz."""
+ try:
+ async with timeout(35):
+ return await self.hass.async_add_executor_job(self._update_data)
+
+ except (InvalidURL, HTTPError, PyEzvizError) as error:
+ raise UpdateFailed(f"Invalid response from API: {error}") from error
diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json
index 03bdfc5217c8bf..32742de203578b 100644
--- a/homeassistant/components/ezviz/manifest.json
+++ b/homeassistant/components/ezviz/manifest.json
@@ -1,8 +1,9 @@
{
- "disabled": "Dependency contains code that breaks Home Assistant.",
"domain": "ezviz",
"name": "Ezviz",
"documentation": "https://www.home-assistant.io/integrations/ezviz",
- "codeowners": ["@baqs"],
- "requirements": ["pyezviz==0.1.5"]
+ "dependencies": ["ffmpeg"],
+ "codeowners": ["@RenierM26", "@baqs"],
+ "requirements": ["pyezviz==0.1.8.7"],
+ "config_flow": true
}
diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py
new file mode 100644
index 00000000000000..f4f9f6588f01ef
--- /dev/null
+++ b/homeassistant/components/ezviz/sensor.py
@@ -0,0 +1,75 @@
+"""Support for Ezviz sensors."""
+import logging
+
+from pyezviz.constants import SensorType
+
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up Ezviz sensors based on a config entry."""
+ coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
+ sensors = []
+ sensor_type_name = "None"
+
+ for idx, camera in enumerate(coordinator.data):
+ for name in camera:
+ # Only add sensor with value.
+ if camera.get(name) is None:
+ continue
+
+ if name in SensorType.__members__:
+ sensor_type_name = getattr(SensorType, name).value
+ sensors.append(EzvizSensor(coordinator, idx, name, sensor_type_name))
+
+ async_add_entities(sensors)
+
+
+class EzvizSensor(CoordinatorEntity, Entity):
+ """Representation of a Ezviz sensor."""
+
+ def __init__(self, coordinator, idx, name, sensor_type_name):
+ """Initialize the sensor."""
+ super().__init__(coordinator)
+ self._idx = idx
+ self._camera_name = self.coordinator.data[self._idx]["name"]
+ self._name = name
+ self._sensor_name = f"{self._camera_name}.{self._name}"
+ self.sensor_type_name = sensor_type_name
+ self._serial = self.coordinator.data[self._idx]["serial"]
+
+ @property
+ def name(self):
+ """Return the name of the Ezviz sensor."""
+ return self._sensor_name
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self.coordinator.data[self._idx][self._name]
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of this sensor."""
+ return f"{self._serial}_{self._sensor_name}"
+
+ @property
+ def device_info(self):
+ """Return the device_info of the device."""
+ return {
+ "identifiers": {(DOMAIN, self._serial)},
+ "name": self.coordinator.data[self._idx]["name"],
+ "model": self.coordinator.data[self._idx]["device_sub_category"],
+ "manufacturer": MANUFACTURER,
+ "sw_version": self.coordinator.data[self._idx]["version"],
+ }
+
+ @property
+ def device_class(self):
+ """Device class for the sensor."""
+ return self.sensor_type_name
diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json
new file mode 100644
index 00000000000000..a8831d2ae34351
--- /dev/null
+++ b/homeassistant/components/ezviz/strings.json
@@ -0,0 +1,52 @@
+{
+ "config": {
+ "flow_title": "{serial}",
+ "step": {
+ "user": {
+ "title": "Connect to Ezviz Cloud",
+ "data": {
+ "username": "[%key:common::config_flow::data::username%]",
+ "password": "[%key:common::config_flow::data::password%]",
+ "url": "[%key:common::config_flow::data::url%]"
+ }
+ },
+ "user_custom_url": {
+ "title": "Connect to custom Ezviz URL",
+ "description": "Manually specify your region URL",
+ "data": {
+ "username": "[%key:common::config_flow::data::username%]",
+ "password": "[%key:common::config_flow::data::password%]",
+ "url": "[%key:common::config_flow::data::url%]"
+ }
+ },
+ "confirm": {
+ "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%]"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "invalid_host": "[%key:common::config_flow::error::invalid_host%]"
+ },
+ "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"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "Request Timeout (seconds)",
+ "ffmpeg_arguments": "Arguments passed to ffmpeg for cameras"
+ }
+ }
+ }
+ }
+}
diff --git a/homeassistant/components/ezviz/switch.py b/homeassistant/components/ezviz/switch.py
new file mode 100644
index 00000000000000..00230a3ac2d502
--- /dev/null
+++ b/homeassistant/components/ezviz/switch.py
@@ -0,0 +1,90 @@
+"""Support for Ezviz Switch sensors."""
+import logging
+
+from pyezviz.constants import DeviceSwitchType
+
+from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up Ezviz switch based on a config entry."""
+ coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
+ switch_entities = []
+ supported_switches = []
+
+ for switches in DeviceSwitchType:
+ supported_switches.append(switches.value)
+
+ supported_switches = set(supported_switches)
+
+ for idx, camera in enumerate(coordinator.data):
+ if not camera.get("switches"):
+ continue
+ for switch in camera["switches"]:
+ if switch not in supported_switches:
+ continue
+ switch_entities.append(EzvizSwitch(coordinator, idx, switch))
+
+ async_add_entities(switch_entities)
+
+
+class EzvizSwitch(CoordinatorEntity, SwitchEntity):
+ """Representation of a Ezviz sensor."""
+
+ def __init__(self, coordinator, idx, switch):
+ """Initialize the switch."""
+ super().__init__(coordinator)
+ self._idx = idx
+ self._camera_name = self.coordinator.data[self._idx]["name"]
+ self._name = switch
+ self._sensor_name = f"{self._camera_name}.{DeviceSwitchType(self._name).name}"
+ self._serial = self.coordinator.data[self._idx]["serial"]
+ self._device_class = DEVICE_CLASS_SWITCH
+
+ @property
+ def name(self):
+ """Return the name of the Ezviz switch."""
+ return f"{self._camera_name}.{DeviceSwitchType(self._name).name}"
+
+ @property
+ def is_on(self):
+ """Return the state of the switch."""
+ return self.coordinator.data[self._idx]["switches"][self._name]
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of this switch."""
+ return f"{self._serial}_{self._sensor_name}"
+
+ def turn_on(self, **kwargs):
+ """Change a device switch on the camera."""
+ _LOGGER.debug("Set EZVIZ Switch '%s' to on", self._name)
+
+ self.coordinator.ezviz_client.switch_status(self._serial, self._name, 1)
+
+ def turn_off(self, **kwargs):
+ """Change a device switch on the camera."""
+ _LOGGER.debug("Set EZVIZ Switch '%s' to off", self._name)
+
+ self.coordinator.ezviz_client.switch_status(self._serial, self._name, 0)
+
+ @property
+ def device_info(self):
+ """Return the device_info of the device."""
+ return {
+ "identifiers": {(DOMAIN, self._serial)},
+ "name": self.coordinator.data[self._idx]["name"],
+ "model": self.coordinator.data[self._idx]["device_sub_category"],
+ "manufacturer": MANUFACTURER,
+ "sw_version": self.coordinator.data[self._idx]["version"],
+ }
+
+ @property
+ def device_class(self):
+ """Device class for the sensor."""
+ return self._device_class
diff --git a/homeassistant/components/ezviz/translations/en.json b/homeassistant/components/ezviz/translations/en.json
new file mode 100644
index 00000000000000..e5103f07973eb3
--- /dev/null
+++ b/homeassistant/components/ezviz/translations/en.json
@@ -0,0 +1,52 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_account": "Account is already configured.",
+ "unknown": "Unexpected error",
+ "ezviz_cloud_account_missing": "Ezviz cloud account missing. Please reconfigure Ezviz cloud account"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
+ "invalid_host": "Invalid IP or URL"
+ },
+ "flow_title": "{serial}",
+ "step": {
+ "user": {
+ "data": {
+ "username": "Username",
+ "password": "Password",
+ "url": "URL"
+ },
+ "title": "Connect to Ezviz Cloud"
+ },
+ "user_custom_url": {
+ "data": {
+ "username": "Username",
+ "password": "Password",
+ "url": "URL"
+ },
+ "title": "Connect to custom Ezviz URL",
+ "description": "Manually specify your region URL"
+ },
+ "confirm": {
+ "data": {
+ "username": "Username",
+ "password": "Password"
+ },
+ "title": "Discovered Ezviz Camera",
+ "description": "Enter RTSP credentials for Ezviz camera {serial} with IP as {ip_address}"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "Request Timeout (seconds)",
+ "ffmpeg_arguments": "Arguments passed to ffmpeg for cameras"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/faa_delays/__init__.py b/homeassistant/components/faa_delays/__init__.py
new file mode 100644
index 00000000000000..2669105469e065
--- /dev/null
+++ b/homeassistant/components/faa_delays/__init__.py
@@ -0,0 +1,80 @@
+"""The FAA Delays integration."""
+import asyncio
+from datetime import timedelta
+import logging
+
+from aiohttp import ClientConnectionError
+from async_timeout import timeout
+from faadelays import Airport
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_ID
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import aiohttp_client
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORMS = ["binary_sensor"]
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+ """Set up the FAA Delays component."""
+ hass.data[DOMAIN] = {}
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up FAA Delays from a config entry."""
+ code = entry.data[CONF_ID]
+
+ coordinator = FAADataUpdateCoordinator(hass, code)
+ await coordinator.async_config_entry_first_refresh()
+
+ hass.data[DOMAIN][entry.entry_id] = coordinator
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
+
+
+class FAADataUpdateCoordinator(DataUpdateCoordinator):
+ """Class to manage fetching FAA API data from a single endpoint."""
+
+ def __init__(self, hass, code):
+ """Initialize the coordinator."""
+ super().__init__(
+ hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=1)
+ )
+ self.session = aiohttp_client.async_get_clientsession(hass)
+ self.data = Airport(code, self.session)
+ self.code = code
+
+ async def _async_update_data(self):
+ try:
+ with timeout(10):
+ await self.data.update()
+ except ClientConnectionError as err:
+ raise UpdateFailed(err) from err
+ return self.data
diff --git a/homeassistant/components/faa_delays/binary_sensor.py b/homeassistant/components/faa_delays/binary_sensor.py
new file mode 100644
index 00000000000000..b96ee24a5bc217
--- /dev/null
+++ b/homeassistant/components/faa_delays/binary_sensor.py
@@ -0,0 +1,93 @@
+"""Platform for FAA Delays sensor component."""
+from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.const import ATTR_ICON, ATTR_NAME
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import DOMAIN, FAA_BINARY_SENSORS
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up a FAA sensor based on a config entry."""
+ coordinator = hass.data[DOMAIN][entry.entry_id]
+
+ binary_sensors = []
+ for kind, attrs in FAA_BINARY_SENSORS.items():
+ name = attrs[ATTR_NAME]
+ icon = attrs[ATTR_ICON]
+
+ binary_sensors.append(
+ FAABinarySensor(coordinator, kind, name, icon, entry.entry_id)
+ )
+
+ async_add_entities(binary_sensors)
+
+
+class FAABinarySensor(CoordinatorEntity, BinarySensorEntity):
+ """Define a binary sensor for FAA Delays."""
+
+ def __init__(self, coordinator, sensor_type, name, icon, entry_id):
+ """Initialize the sensor."""
+ super().__init__(coordinator)
+
+ self.coordinator = coordinator
+ self._entry_id = entry_id
+ self._icon = icon
+ self._name = name
+ self._sensor_type = sensor_type
+ self._id = self.coordinator.data.iata
+ self._attrs = {}
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return f"{self._id} {self._name}"
+
+ @property
+ def icon(self):
+ """Return the icon."""
+ return self._icon
+
+ @property
+ def is_on(self):
+ """Return the status of the sensor."""
+ if self._sensor_type == "GROUND_DELAY":
+ return self.coordinator.data.ground_delay.status
+ if self._sensor_type == "GROUND_STOP":
+ return self.coordinator.data.ground_stop.status
+ if self._sensor_type == "DEPART_DELAY":
+ return self.coordinator.data.depart_delay.status
+ if self._sensor_type == "ARRIVE_DELAY":
+ return self.coordinator.data.arrive_delay.status
+ if self._sensor_type == "CLOSURE":
+ return self.coordinator.data.closure.status
+ return None
+
+ @property
+ def unique_id(self):
+ """Return a unique, Home Assistant friendly identifier for this entity."""
+ return f"{self._id}_{self._sensor_type}"
+
+ @property
+ def extra_state_attributes(self):
+ """Return attributes for sensor."""
+ if self._sensor_type == "GROUND_DELAY":
+ self._attrs["average"] = self.coordinator.data.ground_delay.average
+ self._attrs["reason"] = self.coordinator.data.ground_delay.reason
+ elif self._sensor_type == "GROUND_STOP":
+ self._attrs["endtime"] = self.coordinator.data.ground_stop.endtime
+ self._attrs["reason"] = self.coordinator.data.ground_stop.reason
+ elif self._sensor_type == "DEPART_DELAY":
+ self._attrs["minimum"] = self.coordinator.data.depart_delay.minimum
+ self._attrs["maximum"] = self.coordinator.data.depart_delay.maximum
+ self._attrs["trend"] = self.coordinator.data.depart_delay.trend
+ self._attrs["reason"] = self.coordinator.data.depart_delay.reason
+ elif self._sensor_type == "ARRIVE_DELAY":
+ self._attrs["minimum"] = self.coordinator.data.arrive_delay.minimum
+ self._attrs["maximum"] = self.coordinator.data.arrive_delay.maximum
+ self._attrs["trend"] = self.coordinator.data.arrive_delay.trend
+ self._attrs["reason"] = self.coordinator.data.arrive_delay.reason
+ elif self._sensor_type == "CLOSURE":
+ self._attrs["begin"] = self.coordinator.data.closure.begin
+ self._attrs["end"] = self.coordinator.data.closure.end
+ self._attrs["reason"] = self.coordinator.data.closure.reason
+ return self._attrs
diff --git a/homeassistant/components/faa_delays/config_flow.py b/homeassistant/components/faa_delays/config_flow.py
new file mode 100644
index 00000000000000..b77ab3554b56ed
--- /dev/null
+++ b/homeassistant/components/faa_delays/config_flow.py
@@ -0,0 +1,62 @@
+"""Config flow for FAA Delays integration."""
+import logging
+
+from aiohttp import ClientConnectionError
+import faadelays
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_ID
+from homeassistant.helpers import aiohttp_client
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+DATA_SCHEMA = vol.Schema({vol.Required(CONF_ID): str})
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for FAA Delays."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ errors = {}
+ if user_input is not None:
+
+ await self.async_set_unique_id(user_input[CONF_ID])
+ self._abort_if_unique_id_configured()
+
+ websession = aiohttp_client.async_get_clientsession(self.hass)
+
+ data = faadelays.Airport(user_input[CONF_ID], websession)
+
+ try:
+ await data.update()
+
+ except faadelays.InvalidAirport:
+ _LOGGER.error("Airport code %s is invalid", user_input[CONF_ID])
+ errors[CONF_ID] = "invalid_airport"
+
+ except ClientConnectionError:
+ _LOGGER.error("Error connecting to FAA API")
+ errors["base"] = "cannot_connect"
+
+ except Exception as error: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception: %s", error)
+ errors["base"] = "unknown"
+
+ if not errors:
+ _LOGGER.debug(
+ "Creating entry with id: %s, name: %s",
+ user_input[CONF_ID],
+ data.name,
+ )
+ return self.async_create_entry(title=data.name, data=user_input)
+
+ return self.async_show_form(
+ step_id="user", data_schema=DATA_SCHEMA, errors=errors
+ )
diff --git a/homeassistant/components/faa_delays/const.py b/homeassistant/components/faa_delays/const.py
new file mode 100644
index 00000000000000..c725be88106442
--- /dev/null
+++ b/homeassistant/components/faa_delays/const.py
@@ -0,0 +1,28 @@
+"""Constants for the FAA Delays integration."""
+
+from homeassistant.const import ATTR_ICON, ATTR_NAME
+
+DOMAIN = "faa_delays"
+
+FAA_BINARY_SENSORS = {
+ "GROUND_DELAY": {
+ ATTR_NAME: "Ground Delay",
+ ATTR_ICON: "mdi:airport",
+ },
+ "GROUND_STOP": {
+ ATTR_NAME: "Ground Stop",
+ ATTR_ICON: "mdi:airport",
+ },
+ "DEPART_DELAY": {
+ ATTR_NAME: "Departure Delay",
+ ATTR_ICON: "mdi:airplane-takeoff",
+ },
+ "ARRIVE_DELAY": {
+ ATTR_NAME: "Arrival Delay",
+ ATTR_ICON: "mdi:airplane-landing",
+ },
+ "CLOSURE": {
+ ATTR_NAME: "Closure",
+ ATTR_ICON: "mdi:airplane:off",
+ },
+}
diff --git a/homeassistant/components/faa_delays/manifest.json b/homeassistant/components/faa_delays/manifest.json
new file mode 100644
index 00000000000000..7ffe7898b603ef
--- /dev/null
+++ b/homeassistant/components/faa_delays/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "faa_delays",
+ "name": "FAA Delays",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/faa_delays",
+ "requirements": ["faadelays==0.0.6"],
+ "codeowners": ["@ntilley905"]
+}
diff --git a/homeassistant/components/faa_delays/strings.json b/homeassistant/components/faa_delays/strings.json
new file mode 100644
index 00000000000000..92a9dafb4da987
--- /dev/null
+++ b/homeassistant/components/faa_delays/strings.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "FAA Delays",
+ "description": "Enter a US Airport Code in IATA Format",
+ "data": {
+ "id": "Airport"
+ }
+ }
+ },
+ "error": {
+ "invalid_airport": "Airport code is not valid",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "abort": {
+ "already_configured": "This airport is already configured."
+ }
+ }
+}
diff --git a/homeassistant/components/faa_delays/translations/bg.json b/homeassistant/components/faa_delays/translations/bg.json
new file mode 100644
index 00000000000000..0995436221b2d2
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/bg.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "error": {
+ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "\u041b\u0435\u0442\u0438\u0449\u0435"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/faa_delays/translations/ca.json b/homeassistant/components/faa_delays/translations/ca.json
new file mode 100644
index 00000000000000..e7e600f7f075fa
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/ca.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Aeroport ja est\u00e0 configurat."
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_airport": "Codi d'aeroport inv\u00e0lid",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Aeroport"
+ },
+ "description": "Introdueix codi d'un aeroport dels EUA en format IATA",
+ "title": "FAA Delays"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/cs.json b/homeassistant/components/faa_delays/translations/cs.json
similarity index 61%
rename from homeassistant/components/griddy/translations/cs.json
rename to homeassistant/components/faa_delays/translations/cs.json
index fa5918fa5da8a7..60e4aed57a2784 100644
--- a/homeassistant/components/griddy/translations/cs.json
+++ b/homeassistant/components/faa_delays/translations/cs.json
@@ -1,8 +1,5 @@
{
"config": {
- "abort": {
- "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno"
- },
"error": {
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
"unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
diff --git a/homeassistant/components/faa_delays/translations/de.json b/homeassistant/components/faa_delays/translations/de.json
new file mode 100644
index 00000000000000..9519c7d44705fc
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/de.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Dieser Flughafen ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_airport": "Flughafencode ist ung\u00fcltig",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Flughafen"
+ },
+ "description": "Geben Sie einen US-Flughafencode im IATA-Format ein",
+ "title": "FAA Delays"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/faa_delays/translations/en.json b/homeassistant/components/faa_delays/translations/en.json
new file mode 100644
index 00000000000000..e78b15c68cb186
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/en.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "This airport is already configured."
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_airport": "Airport code is not valid",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Airport"
+ },
+ "description": "Enter a US Airport Code in IATA Format",
+ "title": "FAA Delays"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/faa_delays/translations/es.json b/homeassistant/components/faa_delays/translations/es.json
new file mode 100644
index 00000000000000..71f7fecef41b6d
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/es.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Este aeropuerto ya est\u00e1 configurado."
+ },
+ "error": {
+ "cannot_connect": "Fallo al conectar",
+ "invalid_airport": "El c\u00f3digo del aeropuerto no es v\u00e1lido",
+ "unknown": "Error inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Aeropuerto"
+ },
+ "description": "Introduzca un c\u00f3digo de aeropuerto estadounidense en formato IATA",
+ "title": "Retrasos de la FAA"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/faa_delays/translations/et.json b/homeassistant/components/faa_delays/translations/et.json
new file mode 100644
index 00000000000000..75b52558374920
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/et.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "See lennujaam on juba seadistatud."
+ },
+ "error": {
+ "cannot_connect": "\u00dchendumine nurjus",
+ "invalid_airport": "Lennujaama kood ei sobi",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Lennujaam"
+ },
+ "description": "Sisesta USA lennujaama kood IATA vormingus",
+ "title": ""
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/faa_delays/translations/fr.json b/homeassistant/components/faa_delays/translations/fr.json
new file mode 100644
index 00000000000000..996a22c842209c
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/fr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cet a\u00e9roport est d\u00e9j\u00e0 configur\u00e9."
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "invalid_airport": "Le code de l'a\u00e9roport n'est pas valide",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "A\u00e9roport"
+ },
+ "description": "Entrez un code d'a\u00e9roport am\u00e9ricain au format IATA",
+ "title": "D\u00e9lais FAA"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/faa_delays/translations/he.json b/homeassistant/components/faa_delays/translations/he.json
new file mode 100644
index 00000000000000..af8d410eb18c34
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/he.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05e0\u05de\u05dc \u05ea\u05e2\u05d5\u05e4\u05d4 \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "\u05e0\u05de\u05dc \u05ea\u05e2\u05d5\u05e4\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/faa_delays/translations/hu.json b/homeassistant/components/faa_delays/translations/hu.json
new file mode 100644
index 00000000000000..c511f42a726bc9
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/hu.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ez a rep\u00fcl\u0151t\u00e9r m\u00e1r konfigur\u00e1lva van."
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_airport": "A rep\u00fcl\u0151t\u00e9r k\u00f3dja \u00e9rv\u00e9nytelen",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Rep\u00fcl\u0151t\u00e9r"
+ },
+ "description": "Amerikai rep\u00fcl\u0151t\u00e9ri k\u00f3d be\u00edr\u00e1sa IATA form\u00e1tumban",
+ "title": "FAA Delays"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/faa_delays/translations/id.json b/homeassistant/components/faa_delays/translations/id.json
new file mode 100644
index 00000000000000..4f4c3a93924472
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Bandara ini sudah dikonfigurasi."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_airport": "Kode bandara tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Bandara"
+ },
+ "description": "Masukkan Kode Bandara AS dalam Format IATA",
+ "title": "Penundaan FAA"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/faa_delays/translations/it.json b/homeassistant/components/faa_delays/translations/it.json
new file mode 100644
index 00000000000000..e1bf6ad0646a09
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/it.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Questo aeroporto \u00e8 gi\u00e0 configurato."
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_airport": "Il codice dell'aeroporto non \u00e8 valido",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Aeroporto"
+ },
+ "description": "Immettere un codice aeroporto statunitense in formato IATA",
+ "title": "Ritardi FAA"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/faa_delays/translations/ko.json b/homeassistant/components/faa_delays/translations/ko.json
new file mode 100644
index 00000000000000..2d755e5de28afd
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/ko.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uacf5\ud56d\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_airport": "\uacf5\ud56d \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "\uacf5\ud56d"
+ },
+ "description": "IATA \ud615\uc2dd\uc758 \ubbf8\uad6d \uacf5\ud56d \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "FAA \ud56d\uacf5 \uc5f0\ucc29 \uc815\ubcf4"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/faa_delays/translations/nl.json b/homeassistant/components/faa_delays/translations/nl.json
new file mode 100644
index 00000000000000..3dbc55f5b1b863
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/nl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Deze luchthaven is al geconfigureerd."
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_airport": "Luchthavencode is ongeldig",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Luchthaven"
+ },
+ "description": "Voer een Amerikaanse luchthavencode in IATA-indeling in",
+ "title": "FAA-vertragingen"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/faa_delays/translations/no.json b/homeassistant/components/faa_delays/translations/no.json
new file mode 100644
index 00000000000000..5a5aac723ad71d
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/no.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Denne flyplassen er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_airport": "Flyplasskoden er ikke gyldig",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Flyplass"
+ },
+ "description": "Skriv inn en amerikansk flyplasskode i IATA-format",
+ "title": "FAA forsinkelser"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/faa_delays/translations/pl.json b/homeassistant/components/faa_delays/translations/pl.json
new file mode 100644
index 00000000000000..7073597f5291a6
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/pl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "To lotnisko jest ju\u017c skonfigurowane."
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_airport": "Kod lotniska jest nieprawid\u0142owy",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Lotnisko"
+ },
+ "description": "Wprowad\u017a kod lotniska w Stanach w formacie IATA",
+ "title": "Op\u00f3\u017anienia FAA"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/pt-BR.json b/homeassistant/components/faa_delays/translations/pt.json
similarity index 58%
rename from homeassistant/components/griddy/translations/pt-BR.json
rename to homeassistant/components/faa_delays/translations/pt.json
index dc9c1362dc4418..49cb628dd85871 100644
--- a/homeassistant/components/griddy/translations/pt-BR.json
+++ b/homeassistant/components/faa_delays/translations/pt.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "cannot_connect": "Falha ao conectar, tente novamente",
+ "cannot_connect": "Falha na liga\u00e7\u00e3o",
"unknown": "Erro inesperado"
}
}
diff --git a/homeassistant/components/faa_delays/translations/ru.json b/homeassistant/components/faa_delays/translations/ru.json
new file mode 100644
index 00000000000000..d68810fc9575bc
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/ru.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0430\u044d\u0440\u043e\u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_airport": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434 \u0430\u044d\u0440\u043e\u043f\u043e\u0440\u0442\u0430.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "\u0410\u044d\u0440\u043e\u043f\u043e\u0440\u0442"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0430\u044d\u0440\u043e\u043f\u043e\u0440\u0442\u0430 \u0421\u0428\u0410 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 IATA.",
+ "title": "FAA Delays"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/faa_delays/translations/zh-Hans.json b/homeassistant/components/faa_delays/translations/zh-Hans.json
new file mode 100644
index 00000000000000..4052f12f524693
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/zh-Hans.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u8fde\u63a5\u5931\u8d25",
+ "invalid_airport": "\u822a\u73ed\u53f7\u65e0\u6548",
+ "unknown": "\u9884\u671f\u5916\u7684\u9519\u8bef"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/faa_delays/translations/zh-Hant.json b/homeassistant/components/faa_delays/translations/zh-Hant.json
new file mode 100644
index 00000000000000..f2585bb790f07c
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/zh-Hant.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u6b64\u6a5f\u5834\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_airport": "\u6a5f\u5834\u4ee3\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "\u6a5f\u5834"
+ },
+ "description": "\u8f38\u5165\u7f8e\u570b\u6a5f\u5834 IATA \u4ee3\u78bc",
+ "title": "FAA \u822a\u73ed\u5ef6\u8aa4"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/facebox/image_processing.py b/homeassistant/components/facebox/image_processing.py
index ee6e4d8a6fa50e..5c90ce73560ffe 100644
--- a/homeassistant/components/facebox/image_processing.py
+++ b/homeassistant/components/facebox/image_processing.py
@@ -15,6 +15,7 @@
)
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_ID,
ATTR_NAME,
CONF_IP_ADDRESS,
CONF_PASSWORD,
@@ -34,7 +35,6 @@
ATTR_BOUNDING_BOX = "bounding_box"
ATTR_CLASSIFIER = "classifier"
ATTR_IMAGE_ID = "image_id"
-ATTR_ID = "id"
ATTR_MATCHED = "matched"
FACEBOX_NAME = "name"
CLASSIFIER = "facebox"
@@ -263,7 +263,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the classifier attributes."""
return {
"matched_faces": self._matched,
diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py
index 2f206dca737ad3..29ac5c3d0b5f99 100644
--- a/homeassistant/components/fail2ban/sensor.py
+++ b/homeassistant/components/fail2ban/sensor.py
@@ -6,10 +6,9 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_FILE_PATH, CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -45,7 +44,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(device_list, True)
-class BanSensor(Entity):
+class BanSensor(SensorEntity):
"""Implementation of a fail2ban sensor."""
def __init__(self, name, jail, log_parser):
@@ -66,7 +65,7 @@ def name(self):
return self._name
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the fail2ban sensor."""
return self.ban_dict
@@ -91,9 +90,11 @@ def update(self):
if len(self.ban_dict[STATE_ALL_BANS]) > 10:
self.ban_dict[STATE_ALL_BANS].pop(0)
- elif entry[0] == "Unban":
- if current_ip in self.ban_dict[STATE_CURRENT_BANS]:
- self.ban_dict[STATE_CURRENT_BANS].remove(current_ip)
+ elif (
+ entry[0] == "Unban"
+ and current_ip in self.ban_dict[STATE_CURRENT_BANS]
+ ):
+ self.ban_dict[STATE_CURRENT_BANS].remove(current_ip)
if self.ban_dict[STATE_CURRENT_BANS]:
self.last_ban = self.ban_dict[STATE_CURRENT_BANS][-1]
diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py
index 7b6b083c964059..f484ca36b25aa6 100644
--- a/homeassistant/components/fan/__init__.py
+++ b/homeassistant/components/fan/__init__.py
@@ -1,8 +1,11 @@
"""Provides functionality to interact with fans."""
+from __future__ import annotations
+
from datetime import timedelta
import functools as ft
import logging
-from typing import List, Optional
+import math
+from typing import final
import voluptuous as vol
@@ -23,6 +26,8 @@
from homeassistant.util.percentage import (
ordered_list_item_to_percentage,
percentage_to_ordered_list_item,
+ percentage_to_ranged_value,
+ ranged_value_to_percentage,
)
_LOGGER = logging.getLogger(__name__)
@@ -39,6 +44,8 @@
SUPPORT_PRESET_MODE = 8
SERVICE_SET_SPEED = "set_speed"
+SERVICE_INCREASE_SPEED = "increase_speed"
+SERVICE_DECREASE_SPEED = "decrease_speed"
SERVICE_OSCILLATE = "oscillate"
SERVICE_SET_DIRECTION = "set_direction"
SERVICE_SET_PERCENTAGE = "set_percentage"
@@ -54,6 +61,7 @@
ATTR_SPEED = "speed"
ATTR_PERCENTAGE = "percentage"
+ATTR_PERCENTAGE_STEP = "percentage_step"
ATTR_SPEED_LIST = "speed_list"
ATTR_OSCILLATING = "oscillating"
ATTR_DIRECTION = "direction"
@@ -64,18 +72,24 @@
# into core integrations at some point so we are temporarily
# accommodating them in the transition to percentages.
_NOT_SPEED_OFF = "off"
+_NOT_SPEED_ON = "on"
_NOT_SPEED_AUTO = "auto"
_NOT_SPEED_SMART = "smart"
_NOT_SPEED_INTERVAL = "interval"
_NOT_SPEED_IDLE = "idle"
_NOT_SPEED_FAVORITE = "favorite"
+_NOT_SPEED_SLEEP = "sleep"
+_NOT_SPEED_SILENT = "silent"
_NOT_SPEEDS_FILTER = {
_NOT_SPEED_OFF,
+ _NOT_SPEED_ON,
_NOT_SPEED_AUTO,
_NOT_SPEED_SMART,
_NOT_SPEED_INTERVAL,
_NOT_SPEED_IDLE,
+ _NOT_SPEED_SILENT,
+ _NOT_SPEED_SLEEP,
_NOT_SPEED_FAVORITE,
}
@@ -83,6 +97,8 @@
OFF_SPEED_VALUES = [SPEED_OFF, None]
+LEGACY_SPEED_LIST = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
+
class NoValidSpeedsError(ValueError):
"""Exception class when there are no valid speeds."""
@@ -136,6 +152,26 @@ async def async_setup(hass, config: dict):
"async_set_speed_deprecated",
[SUPPORT_SET_SPEED],
)
+ component.async_register_entity_service(
+ SERVICE_INCREASE_SPEED,
+ {
+ vol.Optional(ATTR_PERCENTAGE_STEP): vol.All(
+ vol.Coerce(int), vol.Range(min=0, max=100)
+ )
+ },
+ "async_increase_speed",
+ [SUPPORT_SET_SPEED],
+ )
+ component.async_register_entity_service(
+ SERVICE_DECREASE_SPEED,
+ {
+ vol.Optional(ATTR_PERCENTAGE_STEP): vol.All(
+ vol.Coerce(int), vol.Range(min=0, max=100)
+ )
+ },
+ "async_decrease_speed",
+ [SUPPORT_SET_SPEED],
+ )
component.async_register_entity_service(
SERVICE_OSCILLATE,
{vol.Required(ATTR_OSCILLATING): cv.boolean},
@@ -185,7 +221,7 @@ def _fan_native(method):
class FanEntity(ToggleEntity):
- """Representation of a fan."""
+ """Base class for fan entities."""
@_fan_native
def set_speed(self, speed: str) -> None:
@@ -195,7 +231,7 @@ def set_speed(self, speed: str) -> None:
async def async_set_speed_deprecated(self, speed: str):
"""Set the speed of the fan."""
_LOGGER.warning(
- "fan.set_speed is deprecated, use fan.set_percentage or fan.set_preset_mode instead."
+ "The fan.set_speed service is deprecated, use fan.set_percentage or fan.set_preset_mode instead"
)
await self.async_set_speed(speed)
@@ -240,6 +276,35 @@ async def async_set_percentage(self, percentage: int) -> None:
else:
await self.async_set_speed(self.percentage_to_speed(percentage))
+ async def async_increase_speed(self, percentage_step: int | None = None) -> None:
+ """Increase the speed of the fan."""
+ await self._async_adjust_speed(1, percentage_step)
+
+ async def async_decrease_speed(self, percentage_step: int | None = None) -> None:
+ """Decrease the speed of the fan."""
+ await self._async_adjust_speed(-1, percentage_step)
+
+ async def _async_adjust_speed(
+ self, modifier: int, percentage_step: int | None
+ ) -> None:
+ """Increase or decrease the speed of the fan."""
+ current_percentage = self.percentage or 0
+
+ if percentage_step is not None:
+ new_percentage = current_percentage + (percentage_step * modifier)
+ else:
+ speed_range = (1, self.speed_count)
+ speed_index = math.ceil(
+ percentage_to_ranged_value(speed_range, current_percentage)
+ )
+ new_percentage = ranged_value_to_percentage(
+ speed_range, speed_index + modifier
+ )
+
+ new_percentage = max(0, min(100, new_percentage))
+
+ await self.async_set_percentage(new_percentage)
+
@_fan_native
def set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
@@ -275,20 +340,19 @@ async def async_set_direction(self, direction: str):
# pylint: disable=arguments-differ
def turn_on(
self,
- speed: Optional[str] = None,
- percentage: Optional[int] = None,
- preset_mode: Optional[str] = None,
+ speed: str | None = None,
+ percentage: int | None = None,
+ preset_mode: str | None = None,
**kwargs,
) -> None:
"""Turn on the fan."""
raise NotImplementedError()
- # pylint: disable=arguments-differ
async def async_turn_on_compat(
self,
- speed: Optional[str] = None,
- percentage: Optional[int] = None,
- preset_mode: Optional[str] = None,
+ speed: str | None = None,
+ percentage: int | None = None,
+ preset_mode: str | None = None,
**kwargs,
) -> None:
"""Turn on the fan.
@@ -305,7 +369,7 @@ async def async_turn_on_compat(
percentage = None
elif speed is not None:
_LOGGER.warning(
- "Calling fan.turn_on with the speed argument is deprecated, use percentage or preset_mode instead."
+ "Calling fan.turn_on with the speed argument is deprecated, use percentage or preset_mode instead"
)
if speed in self.preset_modes:
preset_mode = speed
@@ -325,9 +389,9 @@ async def async_turn_on_compat(
# pylint: disable=arguments-differ
async def async_turn_on(
self,
- speed: Optional[str] = None,
- percentage: Optional[int] = None,
- preset_mode: Optional[str] = None,
+ speed: str | None = None,
+ percentage: int | None = None,
+ preset_mode: str | None = None,
**kwargs,
) -> None:
"""Turn on the fan."""
@@ -358,59 +422,74 @@ def is_on(self):
return self.speed not in [SPEED_OFF, None]
@property
- def _implemented_percentage(self):
+ def _implemented_percentage(self) -> bool:
"""Return true if percentage has been implemented."""
return not hasattr(self.set_percentage, _FAN_NATIVE) or not hasattr(
self.async_set_percentage, _FAN_NATIVE
)
@property
- def _implemented_preset_mode(self):
+ def _implemented_preset_mode(self) -> bool:
"""Return true if preset_mode has been implemented."""
return not hasattr(self.set_preset_mode, _FAN_NATIVE) or not hasattr(
self.async_set_preset_mode, _FAN_NATIVE
)
@property
- def _implemented_speed(self):
+ def _implemented_speed(self) -> bool:
"""Return true if speed has been implemented."""
return not hasattr(self.set_speed, _FAN_NATIVE) or not hasattr(
self.async_set_speed, _FAN_NATIVE
)
@property
- def speed(self) -> Optional[str]:
+ def speed(self) -> str | None:
"""Return the current speed."""
if self._implemented_preset_mode:
preset_mode = self.preset_mode
if preset_mode:
return preset_mode
if self._implemented_percentage:
- return self.percentage_to_speed(self.percentage)
+ percentage = self.percentage
+ if percentage is None:
+ return None
+ return self.percentage_to_speed(percentage)
return None
@property
- def percentage(self) -> Optional[int]:
+ def percentage(self) -> int | None:
"""Return the current speed as a percentage."""
- if not self._implemented_preset_mode:
- if self.speed in self.preset_modes:
- return None
+ if not self._implemented_preset_mode and self.speed in self.preset_modes:
+ return None
if not self._implemented_percentage:
return self.speed_to_percentage(self.speed)
return 0
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ speed_list = speed_list_without_preset_modes(self.speed_list)
+ if speed_list:
+ return len(speed_list)
+ return 100
+
+ @property
+ def percentage_step(self) -> float:
+ """Return the step size for percentage."""
+ return 100 / self.speed_count
+
@property
def speed_list(self) -> list:
"""Get the list of available speeds."""
speeds = []
if self._implemented_percentage:
- speeds += [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
+ speeds += [SPEED_OFF, *LEGACY_SPEED_LIST]
if self._implemented_preset_mode:
speeds += self.preset_modes
return speeds
@property
- def current_direction(self) -> Optional[str]:
+ def current_direction(self) -> str | None:
"""Return the current direction of the fan."""
return None
@@ -434,6 +513,17 @@ def capability_attributes(self):
return attrs
+ @property
+ def _speed_list_without_preset_modes(self) -> list:
+ """Return the speed list without preset modes.
+
+ This property provides forward and backwards
+ compatibility for conversion to percentage speeds.
+ """
+ if not self._implemented_speed:
+ return LEGACY_SPEED_LIST
+ return speed_list_without_preset_modes(self.speed_list)
+
def speed_to_percentage(self, speed: str) -> int:
"""
Map a speed to a percentage.
@@ -453,7 +543,7 @@ def speed_to_percentage(self, speed: str) -> int:
if speed in OFF_SPEED_VALUES:
return 0
- speed_list = speed_list_without_preset_modes(self.speed_list)
+ speed_list = self._speed_list_without_preset_modes
if speed_list and speed not in speed_list:
raise NotValidSpeedError(f"The speed {speed} is not a valid speed.")
@@ -487,7 +577,7 @@ def percentage_to_speed(self, percentage: int) -> str:
if percentage == 0:
return SPEED_OFF
- speed_list = speed_list_without_preset_modes(self.speed_list)
+ speed_list = self._speed_list_without_preset_modes
try:
return percentage_to_ordered_list_item(speed_list, percentage)
@@ -496,6 +586,7 @@ def percentage_to_speed(self, percentage: int) -> str:
f"The speed_list {speed_list} does not contain any valid speeds."
) from ex
+ @final
@property
def state_attributes(self) -> dict:
"""Return optional state attributes."""
@@ -511,6 +602,7 @@ def state_attributes(self) -> dict:
if supported_features & SUPPORT_SET_SPEED:
data[ATTR_SPEED] = self.speed
data[ATTR_PERCENTAGE] = self.percentage
+ data[ATTR_PERCENTAGE_STEP] = self.percentage_step
if (
supported_features & SUPPORT_PRESET_MODE
@@ -526,7 +618,7 @@ def supported_features(self) -> int:
return 0
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., auto, smart, interval, favorite.
Requires SUPPORT_SET_SPEED.
@@ -537,7 +629,7 @@ def preset_mode(self) -> Optional[str]:
return None
@property
- def preset_modes(self) -> Optional[List[str]]:
+ def preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes.
Requires SUPPORT_SET_SPEED.
@@ -545,7 +637,7 @@ def preset_modes(self) -> Optional[List[str]]:
return preset_modes_from_speed_list(self.speed_list)
-def speed_list_without_preset_modes(speed_list: List):
+def speed_list_without_preset_modes(speed_list: list):
"""Filter out non-speeds from the speed list.
The goal is to get the speeds in a list from lowest to
@@ -563,13 +655,13 @@ def speed_list_without_preset_modes(speed_list: List):
output: ["1", "2", "3", "4", "5", "6", "7"]
input: ["Auto", "Silent", "Favorite", "Idle", "Medium", "High", "Strong"]
- output: ["Silent", "Medium", "High", "Strong"]
+ output: ["Medium", "High", "Strong"]
"""
return [speed for speed in speed_list if speed.lower() not in _NOT_SPEEDS_FILTER]
-def preset_modes_from_speed_list(speed_list: List):
+def preset_modes_from_speed_list(speed_list: list):
"""Filter out non-preset modes from the speed list.
The goal is to return only preset modes.
@@ -585,7 +677,7 @@ def preset_modes_from_speed_list(speed_list: List):
output: ["smart"]
input: ["Auto", "Silent", "Favorite", "Idle", "Medium", "High", "Strong"]
- output: ["Auto", "Favorite", "Idle"]
+ output: ["Auto", "Silent", "Favorite", "Idle"]
"""
return [
diff --git a/homeassistant/components/fan/device_action.py b/homeassistant/components/fan/device_action.py
index a5d35d741b640e..f4611d353d512a 100644
--- a/homeassistant/components/fan/device_action.py
+++ b/homeassistant/components/fan/device_action.py
@@ -1,5 +1,5 @@
"""Provides device automations for Fan."""
-from typing import List, Optional
+from __future__ import annotations
import voluptuous as vol
@@ -28,7 +28,7 @@
)
-async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device actions for Fan devices."""
registry = await entity_registry.async_get_registry(hass)
actions = []
@@ -59,11 +59,9 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
async def async_call_action_from_config(
- hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context]
+ hass: HomeAssistant, config: dict, variables: dict, context: Context | None
) -> None:
"""Execute a device action."""
- config = ACTION_SCHEMA(config)
-
service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]}
if config[CONF_TYPE] == "turn_on":
diff --git a/homeassistant/components/fan/device_condition.py b/homeassistant/components/fan/device_condition.py
index d3a8aa5c395485..9aa9620ef720e2 100644
--- a/homeassistant/components/fan/device_condition.py
+++ b/homeassistant/components/fan/device_condition.py
@@ -1,5 +1,5 @@
"""Provide the device automations for Fan."""
-from typing import Dict, List
+from __future__ import annotations
import voluptuous as vol
@@ -32,7 +32,7 @@
async def async_get_conditions(
hass: HomeAssistant, device_id: str
-) -> List[Dict[str, str]]:
+) -> list[dict[str, str]]:
"""List device conditions for Fan devices."""
registry = await entity_registry.async_get_registry(hass)
conditions = []
diff --git a/homeassistant/components/fan/device_trigger.py b/homeassistant/components/fan/device_trigger.py
index c78ebcfffe4cb8..15f8f4be45e5b2 100644
--- a/homeassistant/components/fan/device_trigger.py
+++ b/homeassistant/components/fan/device_trigger.py
@@ -1,67 +1,29 @@
"""Provides device automations for Fan."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
-from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
-from homeassistant.components.homeassistant.triggers import state as state_trigger
-from homeassistant.const import (
- CONF_DEVICE_ID,
- CONF_DOMAIN,
- CONF_ENTITY_ID,
- CONF_PLATFORM,
- CONF_TYPE,
- STATE_OFF,
- STATE_ON,
-)
+from homeassistant.components.device_automation import toggle_entity
+from homeassistant.const import CONF_DOMAIN
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
-from homeassistant.helpers import config_validation as cv, entity_registry
from homeassistant.helpers.typing import ConfigType
from . import DOMAIN
-TRIGGER_TYPES = {"turned_on", "turned_off"}
-
-TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
- {
- vol.Required(CONF_ENTITY_ID): cv.entity_id,
- vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
- }
+TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend(
+ {vol.Required(CONF_DOMAIN): DOMAIN}
)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers for Fan devices."""
- registry = await entity_registry.async_get_registry(hass)
- triggers = []
-
- # Get all the integrations entities for this device
- for entry in entity_registry.async_entries_for_device(registry, device_id):
- if entry.domain != DOMAIN:
- continue
+ return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN)
- # Add triggers for each entity that belongs to this integration
- triggers.append(
- {
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "turned_on",
- }
- )
- triggers.append(
- {
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "turned_off",
- }
- )
- return triggers
+async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict:
+ """List trigger capabilities."""
+ return await toggle_entity.async_get_trigger_capabilities(hass, config)
async def async_attach_trigger(
@@ -70,23 +32,7 @@ async def async_attach_trigger(
action: AutomationActionType,
automation_info: dict,
) -> CALLBACK_TYPE:
- """Attach a trigger."""
- config = TRIGGER_SCHEMA(config)
-
- if config[CONF_TYPE] == "turned_on":
- from_state = STATE_OFF
- to_state = STATE_ON
- else:
- from_state = STATE_ON
- to_state = STATE_OFF
-
- state_config = {
- state_trigger.CONF_PLATFORM: "state",
- CONF_ENTITY_ID: config[CONF_ENTITY_ID],
- state_trigger.CONF_FROM: from_state,
- state_trigger.CONF_TO: to_state,
- }
- state_config = state_trigger.TRIGGER_SCHEMA(state_config)
- return await state_trigger.async_attach_trigger(
- hass, state_config, action, automation_info, platform_type="device"
+ """Listen for state changes based on configuration."""
+ return await toggle_entity.async_attach_trigger(
+ hass, config, action, automation_info
)
diff --git a/homeassistant/components/fan/group.py b/homeassistant/components/fan/group.py
index 1636054663dc69..234883ffd5a041 100644
--- a/homeassistant/components/fan/group.py
+++ b/homeassistant/components/fan/group.py
@@ -3,13 +3,12 @@
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OFF, STATE_ON
-from homeassistant.core import callback
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import HomeAssistant, callback
@callback
def async_describe_on_off_states(
- hass: HomeAssistantType, registry: GroupIntegrationRegistry
+ hass: HomeAssistant, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_ON}, STATE_OFF)
diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py
index b5f0fca47b7a6b..c9da43ebe3aac8 100644
--- a/homeassistant/components/fan/reproduce_state.py
+++ b/homeassistant/components/fan/reproduce_state.py
@@ -1,8 +1,10 @@
"""Reproduce an Fan state."""
+from __future__ import annotations
+
import asyncio
import logging
from types import MappingProxyType
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -11,8 +13,7 @@
STATE_OFF,
STATE_ON,
)
-from homeassistant.core import Context, State
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import Context, HomeAssistant, State
from . import (
ATTR_DIRECTION,
@@ -41,11 +42,11 @@
async def _async_reproduce_state(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -95,11 +96,11 @@ async def _async_reproduce_state(
async def async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Fan states."""
await asyncio.gather(
diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml
index 760aaabcf4ad21..f86a32823dcf82 100644
--- a/homeassistant/components/fan/services.yaml
+++ b/homeassistant/components/fan/services.yaml
@@ -1,80 +1,146 @@
# Describes the format for available fan services
set_speed:
- description: Sets fan speed.
+ name: Set speed
+ description: Set fan speed.
+ target:
fields:
- entity_id:
- description: Name(s) of the entities to set
- example: "fan.living_room"
speed:
- description: Speed setting
+ name: Speed
+ description: Speed setting.
+ required: true
example: "low"
+ selector:
+ text:
set_preset_mode:
+ name: Set preset mode
description: Set preset mode for a fan device.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to change.
- example: "fan.kitchen"
preset_mode:
- description: New value of preset mode
+ name: Preset mode
+ description: New value of preset mode.
+ required: true
example: "auto"
+ selector:
+ text:
set_percentage:
- description: Sets fan speed percentage.
+ name: Set speed percentage
+ description: Set fan speed percentage.
+ target:
fields:
- entity_id:
- description: Name(s) of the entities to set
- example: "fan.living_room"
percentage:
- description: Percentage speed setting
+ name: Percentage
+ description: Percentage speed setting.
+ required: true
example: 25
+ selector:
+ number:
+ min: 0
+ max: 100
+ step: 1
+ unit_of_measurement: "%"
+ mode: slider
turn_on:
- description: Turns fan on.
+ name: Turn on
+ description: Turn fan on.
+ target:
fields:
- entity_id:
- description: Names(s) of the entities to turn on
- example: "fan.living_room"
speed:
- description: Speed setting
+ name: Speed
+ description: Speed setting.
example: "high"
percentage:
- description: Percentage speed setting
+ name: Percentage
+ description: Percentage speed setting.
example: 75
+ selector:
+ number:
+ min: 0
+ max: 100
+ step: 1
+ unit_of_measurement: "%"
+ mode: slider
preset_mode:
- description: Preset mode setting
+ name: Preset mode
+ description: Preset mode setting.
example: "auto"
+ selector:
+ text:
turn_off:
- description: Turns fan off.
- fields:
- entity_id:
- description: Names(s) of the entities to turn off
- example: "fan.living_room"
+ name: Turn off
+ description: Turn fan off.
+ target:
oscillate:
- description: Oscillates the fan.
+ name: Oscillate
+ description: Oscillate the fan.
+ target:
fields:
- entity_id:
- description: Name(s) of the entities to oscillate
- example: "fan.desk_fan"
oscillating:
- description: Flag to turn on/off oscillation
+ name: Oscillating
+ description: Flag to turn on/off oscillation.
+ required: true
example: true
+ selector:
+ boolean:
toggle:
+ name: Toggle
description: Toggle the fan on/off.
- fields:
- entity_id:
- description: Name(s) of the entities to toggle
- example: "fan.living_room"
+ target:
set_direction:
+ name: Set direction
description: Set the fan rotation.
+ target:
fields:
- entity_id:
- description: Name(s) of the entities to set
- example: "fan.living_room"
direction:
- description: The direction to rotate. Either 'forward' or 'reverse'
+ name: Direction
+ description: The direction to rotate.
+ required: true
example: "forward"
+ selector:
+ select:
+ options:
+ - "forward"
+ - "reverse"
+
+increase_speed:
+ name: Increase speed
+ description: Increase the speed of the fan by one speed or a percentage_step.
+ target:
+ fields:
+ percentage_step:
+ advanced: true
+ required: false
+ description: Increase speed by a percentage.
+ example: 50
+ selector:
+ number:
+ min: 0
+ max: 100
+ step: 1
+ unit_of_measurement: "%"
+ mode: slider
+
+decrease_speed:
+ name: Decrease speed
+ description: Decrease the speed of the fan by one speed or a percentage_step.
+ target:
+ fields:
+ percentage_step:
+ advanced: true
+ required: false
+ description: Decrease speed by a percentage.
+ example: 50
+ selector:
+ number:
+ min: 0
+ max: 100
+ step: 1
+ unit_of_measurement: "%"
+ mode: slider
diff --git a/homeassistant/components/fan/translations/id.json b/homeassistant/components/fan/translations/id.json
index b5324f36f6a8de..054ec10754c1c7 100644
--- a/homeassistant/components/fan/translations/id.json
+++ b/homeassistant/components/fan/translations/id.json
@@ -1,9 +1,23 @@
{
+ "device_automation": {
+ "action_type": {
+ "turn_off": "Matikan {entity_name}",
+ "turn_on": "Nyalakan {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} mati",
+ "is_on": "{entity_name} nyala"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} dimatikan",
+ "turned_on": "{entity_name} dinyalakan"
+ }
+ },
"state": {
"_": {
- "off": "Off",
- "on": "On"
+ "off": "Mati",
+ "on": "Nyala"
}
},
- "title": "Kipas angin"
+ "title": "Kipas Angin"
}
\ No newline at end of file
diff --git a/homeassistant/components/fan/translations/ko.json b/homeassistant/components/fan/translations/ko.json
index 5f6116d48d23d6..c2157f29e724bc 100644
--- a/homeassistant/components/fan/translations/ko.json
+++ b/homeassistant/components/fan/translations/ko.json
@@ -1,16 +1,16 @@
{
"device_automation": {
"action_type": {
- "turn_off": "{entity_name} \ub044\uae30",
- "turn_on": "{entity_name} \ucf1c\uae30"
+ "turn_off": "{entity_name}\uc744(\ub97c) \ub044\uae30",
+ "turn_on": "{entity_name}\uc744(\ub97c) \ucf1c\uae30"
},
"condition_type": {
- "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74",
- "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74"
+ "is_off": "{entity_name}\uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74",
+ "is_on": "{entity_name}\uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74"
},
"trigger_type": {
- "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c",
- "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c"
+ "turned_off": "{entity_name}\uc774(\uac00) \uaebc\uc84c\uc744 \ub54c",
+ "turned_on": "{entity_name}\uc774(\uac00) \ucf1c\uc84c\uc744 \ub54c"
}
},
"state": {
diff --git a/homeassistant/components/fan/translations/tr.json b/homeassistant/components/fan/translations/tr.json
index 4ffc57601bdd53..52a07c35d8326b 100644
--- a/homeassistant/components/fan/translations/tr.json
+++ b/homeassistant/components/fan/translations/tr.json
@@ -1,4 +1,14 @@
{
+ "device_automation": {
+ "action_type": {
+ "turn_off": "{entity_name} kapat",
+ "turn_on": "{entity_name} a\u00e7\u0131n"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} kapat\u0131ld\u0131",
+ "turned_on": "{entity_name} a\u00e7\u0131ld\u0131"
+ }
+ },
"state": {
"_": {
"off": "Kapal\u0131",
diff --git a/homeassistant/components/fan/translations/uk.json b/homeassistant/components/fan/translations/uk.json
index 3fd103cd244c56..0e0bafcbfc4f2b 100644
--- a/homeassistant/components/fan/translations/uk.json
+++ b/homeassistant/components/fan/translations/uk.json
@@ -1,8 +1,16 @@
{
"device_automation": {
+ "action_type": {
+ "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438",
+ "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456"
+ },
"trigger_type": {
- "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
- "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e"
+ "turned_off": "{entity_name} \u0432\u0438\u043c\u0438\u043a\u0430\u0454\u0442\u044c\u0441\u044f",
+ "turned_on": "{entity_name} \u0432\u043c\u0438\u043a\u0430\u0454\u0442\u044c\u0441\u044f"
}
},
"state": {
diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py
index ad3711b131923a..b4406a4de95f8b 100644
--- a/homeassistant/components/fastdotcom/sensor.py
+++ b/homeassistant/components/fastdotcom/sensor.py
@@ -1,4 +1,5 @@
"""Support for Fast.com internet speed testing sensor."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DATA_RATE_MEGABITS_PER_SECOND
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -14,7 +15,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([SpeedtestSensor(hass.data[FASTDOTCOM_DOMAIN])])
-class SpeedtestSensor(RestoreEntity):
+class SpeedtestSensor(RestoreEntity, SensorEntity):
"""Implementation of a FAst.com sensor."""
def __init__(self, speedtest_data):
diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py
index 4b67f06ba23b9a..4bf8de91edc184 100644
--- a/homeassistant/components/ffmpeg/__init__.py
+++ b/homeassistant/components/ffmpeg/__init__.py
@@ -1,7 +1,8 @@
"""Support for FFmpeg."""
+from __future__ import annotations
+
import asyncio
import re
-from typing import Optional
from haffmpeg.tools import IMAGE_JPEG, FFVersion, ImageFrame
import voluptuous as vol
@@ -93,7 +94,7 @@ async def async_get_image(
hass: HomeAssistantType,
input_source: str,
output_format: str = IMAGE_JPEG,
- extra_cmd: Optional[str] = None,
+ extra_cmd: str | None = None,
):
"""Get an image from a frame of an RTSP stream."""
manager = hass.data[DATA_FFMPEG]
diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py
index 314fbbd22107ee..ecbf6f3b1aecb2 100644
--- a/homeassistant/components/ffmpeg_motion/binary_sensor.py
+++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py
@@ -14,13 +14,12 @@
DATA_FFMPEG,
FFmpegBase,
)
-from homeassistant.const import CONF_NAME
+from homeassistant.const import CONF_NAME, CONF_REPEAT
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
CONF_RESET = "reset"
CONF_CHANGES = "changes"
-CONF_REPEAT = "repeat"
CONF_REPEAT_TIME = "repeat_time"
DEFAULT_NAME = "FFmpeg Motion"
@@ -88,7 +87,6 @@ class FFmpegMotion(FFmpegBinarySensor):
def __init__(self, hass, manager, config):
"""Initialize FFmpeg motion binary sensor."""
-
super().__init__(config)
self.ffmpeg = ffmpeg_sensor.SensorMotion(manager.binary, self._async_callback)
diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py
index 51e31cf27adf17..964c111284053b 100644
--- a/homeassistant/components/fibaro/__init__.py
+++ b/homeassistant/components/fibaro/__init__.py
@@ -1,7 +1,8 @@
"""Support for the Fibaro devices."""
+from __future__ import annotations
+
from collections import defaultdict
import logging
-from typing import Optional
from fiblary3.client.v4.client import Client as FibaroClient, StateHandler
import voluptuous as vol
@@ -37,7 +38,7 @@
DOMAIN = "fibaro"
FIBARO_CONTROLLERS = "fibaro_controllers"
FIBARO_DEVICES = "fibaro_devices"
-FIBARO_COMPONENTS = [
+PLATFORMS = [
"binary_sensor",
"climate",
"cover",
@@ -365,21 +366,21 @@ def stop_fibaro(event):
controller.disable_state_handler()
hass.data[FIBARO_DEVICES] = {}
- for component in FIBARO_COMPONENTS:
- hass.data[FIBARO_DEVICES][component] = []
+ for platform in PLATFORMS:
+ hass.data[FIBARO_DEVICES][platform] = []
for gateway in gateways:
controller = FibaroController(gateway)
if controller.connect():
hass.data[FIBARO_CONTROLLERS][controller.hub_serial] = controller
- for component in FIBARO_COMPONENTS:
- hass.data[FIBARO_DEVICES][component].extend(
- controller.fibaro_devices[component]
+ for platform in PLATFORMS:
+ hass.data[FIBARO_DEVICES][platform].extend(
+ controller.fibaro_devices[platform]
)
if hass.data[FIBARO_CONTROLLERS]:
- for component in FIBARO_COMPONENTS:
- discovery.load_platform(hass, component, DOMAIN, {}, base_config)
+ for platform in PLATFORMS:
+ discovery.load_platform(hass, platform, DOMAIN, {}, base_config)
for controller in hass.data[FIBARO_CONTROLLERS].values():
controller.enable_state_handler()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_fibaro)
@@ -496,7 +497,7 @@ def unique_id(self) -> str:
return self.fibaro_device.unique_id_str
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the device."""
return self._name
@@ -506,7 +507,7 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
attr = {"fibaro_id": self.fibaro_device.id}
diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py
index 4e9af8803f23b4..3161e173b2a5ae 100644
--- a/homeassistant/components/fibaro/sensor.py
+++ b/homeassistant/components/fibaro/sensor.py
@@ -1,7 +1,10 @@
"""Support for Fibaro sensors."""
-from homeassistant.components.sensor import DOMAIN
+from contextlib import suppress
+
+from homeassistant.components.sensor import DOMAIN, SensorEntity
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
+ DEVICE_CLASS_CO2,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
@@ -10,7 +13,6 @@
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
-from homeassistant.helpers.entity import Entity
from . import FIBARO_DEVICES, FibaroDevice
@@ -27,7 +29,13 @@
"mdi:fire",
None,
],
- "CO2": ["CO2", CONCENTRATION_PARTS_PER_MILLION, "mdi:cloud", None],
+ "CO2": [
+ "CO2",
+ CONCENTRATION_PARTS_PER_MILLION,
+ None,
+ None,
+ DEVICE_CLASS_CO2,
+ ],
"com.fibaro.humiditySensor": [
"Humidity",
PERCENTAGE,
@@ -48,7 +56,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
)
-class FibaroSensor(FibaroDevice, Entity):
+class FibaroSensor(FibaroDevice, SensorEntity):
"""Representation of a Fibaro Sensor."""
def __init__(self, fibaro_device):
@@ -65,7 +73,7 @@ def __init__(self, fibaro_device):
self._unit = None
self._icon = None
self._device_class = None
- try:
+ with suppress(KeyError, ValueError):
if not self._unit:
if self.fibaro_device.properties.unit == "lux":
self._unit = LIGHT_LUX
@@ -75,8 +83,6 @@ def __init__(self, fibaro_device):
self._unit = TEMP_FAHRENHEIT
else:
self._unit = self.fibaro_device.properties.unit
- except (KeyError, ValueError):
- pass
@property
def state(self):
@@ -100,7 +106,5 @@ def device_class(self):
def update(self):
"""Update the state."""
- try:
+ with suppress(KeyError, ValueError):
self.current_value = float(self.fibaro_device.properties.value)
- except (KeyError, ValueError):
- pass
diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py
index 22522d1ab74f44..55ec455d8f15bd 100644
--- a/homeassistant/components/fido/sensor.py
+++ b/homeassistant/components/fido/sensor.py
@@ -11,7 +11,7 @@
from pyfido.client import PyFidoError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_MONITORED_VARIABLES,
CONF_NAME,
@@ -21,7 +21,6 @@
TIME_MINUTES,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -90,7 +89,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(sensors, True)
-class FidoSensor(Entity):
+class FidoSensor(SensorEntity):
"""Implementation of a Fido sensor."""
def __init__(self, fido_data, sensor_type, name, number):
@@ -125,7 +124,7 @@ def icon(self):
return self._icon
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {"number": self._number}
diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py
index e928541a724ad1..5d8a9475235ca8 100644
--- a/homeassistant/components/file/sensor.py
+++ b/homeassistant/components/file/sensor.py
@@ -4,15 +4,17 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.const import (
+ CONF_FILE_PATH,
+ CONF_NAME,
+ CONF_UNIT_OF_MEASUREMENT,
+ CONF_VALUE_TEMPLATE,
+)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
-CONF_FILE_PATH = "file_path"
-
DEFAULT_NAME = "File"
ICON = "mdi:file"
@@ -43,7 +45,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
_LOGGER.error("'%s' is not an allowed directory", file_path)
-class FileSensor(Entity):
+class FileSensor(SensorEntity):
"""Implementation of a file sensor."""
def __init__(self, name, file_path, unit_of_measurement, value_template):
diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py
index 27122a3cb9c568..856b29364aeafe 100644
--- a/homeassistant/components/filesize/sensor.py
+++ b/homeassistant/components/filesize/sensor.py
@@ -5,10 +5,9 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import DATA_MEGABYTES
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.reload import setup_reload_service
from . import DOMAIN, PLATFORMS
@@ -40,7 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class Filesize(Entity):
+class Filesize(SensorEntity):
"""Encapsulates file size information."""
def __init__(self, path):
@@ -76,7 +75,7 @@ def icon(self):
return ICON
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return other details about the sensor state."""
return {
"path": self._path,
diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py
index d46709924ea0a2..2f1705f5f4dd38 100644
--- a/homeassistant/components/filter/sensor.py
+++ b/homeassistant/components/filter/sensor.py
@@ -1,4 +1,6 @@
"""Allows the creation of a sensor that filters state property."""
+from __future__ import annotations
+
from collections import Counter, deque
from copy import copy
from datetime import timedelta
@@ -6,7 +8,6 @@
import logging
from numbers import Number
import statistics
-from typing import Optional
import voluptuous as vol
@@ -17,6 +18,7 @@
DEVICE_CLASSES as SENSOR_DEVICE_CLASSES,
DOMAIN as SENSOR_DOMAIN,
PLATFORM_SCHEMA,
+ SensorEntity,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
@@ -30,7 +32,6 @@
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.util.decorator import Registry
@@ -178,7 +179,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([SensorFilter(name, entity_id, filters)])
-class SensorFilter(Entity):
+class SensorFilter(SensorEntity):
"""Representation of a Filter Sensor."""
def __init__(self, name, entity_id, filters):
@@ -351,7 +352,7 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {ATTR_ENTITY_ID: self._entity}
@@ -394,8 +395,8 @@ def __init__(
self,
name,
window_size: int = 1,
- precision: Optional[int] = None,
- entity: Optional[str] = None,
+ precision: int | None = None,
+ entity: str | None = None,
):
"""Initialize common attributes.
@@ -453,7 +454,7 @@ def filter_state(self, new_state):
@FILTERS.register(FILTER_NAME_RANGE)
-class RangeFilter(Filter):
+class RangeFilter(Filter, SensorEntity):
"""Range filter.
Determines if new state is in the range of upper_bound and lower_bound.
@@ -463,9 +464,9 @@ class RangeFilter(Filter):
def __init__(
self,
entity,
- precision: Optional[int] = DEFAULT_PRECISION,
- lower_bound: Optional[float] = None,
- upper_bound: Optional[float] = None,
+ precision: int | None = DEFAULT_PRECISION,
+ lower_bound: float | None = None,
+ upper_bound: float | None = None,
):
"""Initialize Filter.
@@ -508,7 +509,7 @@ def _filter_state(self, new_state):
@FILTERS.register(FILTER_NAME_OUTLIER)
-class OutlierFilter(Filter):
+class OutlierFilter(Filter, SensorEntity):
"""BASIC outlier filter.
Determines if new state is in a band around the median.
@@ -546,7 +547,7 @@ def _filter_state(self, new_state):
@FILTERS.register(FILTER_NAME_LOWPASS)
-class LowPassFilter(Filter):
+class LowPassFilter(Filter, SensorEntity):
"""BASIC Low Pass Filter."""
def __init__(self, window_size, precision, entity, time_constant: int):
@@ -570,7 +571,7 @@ def _filter_state(self, new_state):
@FILTERS.register(FILTER_NAME_TIME_SMA)
-class TimeSMAFilter(Filter):
+class TimeSMAFilter(Filter, SensorEntity):
"""Simple Moving Average (SMA) Filter.
The window_size is determined by time, and SMA is time weighted.
@@ -616,7 +617,7 @@ def _filter_state(self, new_state):
@FILTERS.register(FILTER_NAME_THROTTLE)
-class ThrottleFilter(Filter):
+class ThrottleFilter(Filter, SensorEntity):
"""Throttle Filter.
One sample per window.
@@ -639,7 +640,7 @@ def _filter_state(self, new_state):
@FILTERS.register(FILTER_NAME_TIME_THROTTLE)
-class TimeThrottleFilter(Filter):
+class TimeThrottleFilter(Filter, SensorEntity):
"""Time Throttle Filter.
One sample per time period.
diff --git a/homeassistant/components/filter/services.yaml b/homeassistant/components/filter/services.yaml
index 6f6ea1b04d6aa1..7d64b34a4f7605 100644
--- a/homeassistant/components/filter/services.yaml
+++ b/homeassistant/components/filter/services.yaml
@@ -1,2 +1,2 @@
reload:
- description: Reload all filter entities.
+ description: Reload all filter entities
diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py
index 6cd62333a8714b..e7faff46155c2e 100644
--- a/homeassistant/components/fints/sensor.py
+++ b/homeassistant/components/fints/sensor.py
@@ -8,10 +8,9 @@
from fints.dialog import FinTSDialogError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, CONF_PIN, CONF_URL, CONF_USERNAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -75,7 +74,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for account in balance_accounts:
if config[CONF_ACCOUNTS] and account.iban not in account_config:
- _LOGGER.info("skipping account %s for bank %s", account.iban, fints_name)
+ _LOGGER.info("Skipping account %s for bank %s", account.iban, fints_name)
continue
account_name = account_config.get(account.iban)
@@ -87,7 +86,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for account in holdings_accounts:
if config[CONF_HOLDINGS] and account.accountnumber not in holdings_config:
_LOGGER.info(
- "skipping holdings %s for bank %s", account.accountnumber, fints_name
+ "Skipping holdings %s for bank %s", account.accountnumber, fints_name
)
continue
@@ -154,7 +153,7 @@ def detect_accounts(self):
return balance_accounts, holdings_accounts
-class FinTsAccount(Entity):
+class FinTsAccount(SensorEntity):
"""Sensor for a FinTS balance account.
A balance account contains an amount of money (=balance). The amount may
@@ -193,7 +192,7 @@ def unit_of_measurement(self) -> str:
return self._currency
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Additional attributes of the sensor."""
attributes = {ATTR_ACCOUNT: self._account.iban, ATTR_ACCOUNT_TYPE: "balance"}
if self._client.name:
@@ -206,7 +205,7 @@ def icon(self) -> str:
return ICON
-class FinTsHoldingsAccount(Entity):
+class FinTsHoldingsAccount(SensorEntity):
"""Sensor for a FinTS holdings account.
A holdings account does not contain money but rather some financial
@@ -238,7 +237,7 @@ def icon(self) -> str:
return ICON
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Additional attributes of the sensor.
Lists each holding of the account with the current value.
diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py
index bf5f3f6beea54a..593109b4f52ee3 100644
--- a/homeassistant/components/fireservicerota/__init__.py
+++ b/homeassistant/components/fireservicerota/__init__.py
@@ -26,13 +26,7 @@
_LOGGER = logging.getLogger(__name__)
-SUPPORTED_PLATFORMS = {SENSOR_DOMAIN, BINARYSENSOR_DOMAIN, SWITCH_DOMAIN}
-
-
-async def async_setup(hass: HomeAssistant, config: dict) -> bool:
- """Set up the FireServiceRota component."""
-
- return True
+PLATFORMS = [SENSOR_DOMAIN, BINARYSENSOR_DOMAIN, SWITCH_DOMAIN]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -57,14 +51,14 @@ async def async_update_data():
update_interval=MIN_TIME_BETWEEN_UPDATES,
)
- await coordinator.async_refresh()
+ await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = {
DATA_CLIENT: client,
DATA_COORDINATOR: coordinator,
}
- for platform in SUPPORTED_PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)
@@ -83,7 +77,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, platform)
- for platform in SUPPORTED_PLATFORMS
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/fireservicerota/binary_sensor.py b/homeassistant/components/fireservicerota/binary_sensor.py
index fc06e605cbd755..29fc97ae50300c 100644
--- a/homeassistant/components/fireservicerota/binary_sensor.py
+++ b/homeassistant/components/fireservicerota/binary_sensor.py
@@ -1,6 +1,4 @@
"""Binary Sensor platform for FireServiceRota integration."""
-import logging
-
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
@@ -11,8 +9,6 @@
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN as FIRESERVICEROTA_DOMAIN
-_LOGGER = logging.getLogger(__name__)
-
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
@@ -66,7 +62,7 @@ def is_on(self) -> bool:
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return available attributes for binary sensor."""
attr = {}
if not self.coordinator.data:
diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py
index 83272eff926c36..04d8c97a4a5e71 100644
--- a/homeassistant/components/fireservicerota/sensor.py
+++ b/homeassistant/components/fireservicerota/sensor.py
@@ -1,6 +1,7 @@
"""Sensor platform for FireServiceRota integration."""
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -21,7 +22,7 @@ async def async_setup_entry(
async_add_entities([IncidentsSensor(client)])
-class IncidentsSensor(RestoreEntity):
+class IncidentsSensor(RestoreEntity, SensorEntity):
"""Representation of FireServiceRota incidents sensor."""
def __init__(self, client):
@@ -64,7 +65,7 @@ def should_poll(self) -> bool:
return False
@property
- def device_state_attributes(self) -> object:
+ def extra_state_attributes(self) -> object:
"""Return available attributes for sensor."""
attr = {}
data = self._state_attributes
diff --git a/homeassistant/components/fireservicerota/strings.json b/homeassistant/components/fireservicerota/strings.json
index c44673d6c2c361..aef6f1b684918b 100644
--- a/homeassistant/components/fireservicerota/strings.json
+++ b/homeassistant/components/fireservicerota/strings.json
@@ -9,7 +9,7 @@
}
},
"reauth": {
- "description": "Authentication tokens baceame invalid, login to recreate them.",
+ "description": "Authentication tokens became invalid, login to recreate them.",
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py
index 7519270ca5c5b8..e2385f02e5c9b2 100644
--- a/homeassistant/components/fireservicerota/switch.py
+++ b/homeassistant/components/fireservicerota/switch.py
@@ -73,7 +73,7 @@ def available(self):
return self._client.on_duty
@property
- def device_state_attributes(self) -> object:
+ def extra_state_attributes(self) -> object:
"""Return available attributes for switch."""
attr = {}
if not self._state_attributes:
diff --git a/homeassistant/components/fireservicerota/translations/en.json b/homeassistant/components/fireservicerota/translations/en.json
index 288b89c31b8276..a059081760dec3 100644
--- a/homeassistant/components/fireservicerota/translations/en.json
+++ b/homeassistant/components/fireservicerota/translations/en.json
@@ -15,7 +15,7 @@
"data": {
"password": "Password"
},
- "description": "Authentication tokens baceame invalid, login to recreate them."
+ "description": "Authentication tokens became invalid, login to recreate them."
},
"user": {
"data": {
diff --git a/homeassistant/components/fireservicerota/translations/fr.json b/homeassistant/components/fireservicerota/translations/fr.json
new file mode 100644
index 00000000000000..fdbf28e32e1f1a
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/fr.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Le compte \u00e0 d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9",
+ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi"
+ },
+ "create_entry": {
+ "default": "Autentification r\u00e9ussie"
+ },
+ "error": {
+ "invalid_auth": "Autentification invalide"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Mot de passe"
+ },
+ "description": "Les jetons d'authentification sont invalides, connectez-vous pour les recr\u00e9er."
+ },
+ "user": {
+ "data": {
+ "password": "Mot de passe",
+ "url": "Site web",
+ "username": "Utilisateur"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fireservicerota/translations/hu.json b/homeassistant/components/fireservicerota/translations/hu.json
index 63c887ff28140c..8e8432d5df4a25 100644
--- a/homeassistant/components/fireservicerota/translations/hu.json
+++ b/homeassistant/components/fireservicerota/translations/hu.json
@@ -1,9 +1,26 @@
{
"config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
+ },
+ "create_entry": {
+ "default": "Sikeres hiteles\u00edt\u00e9s"
+ },
+ "error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
+ },
"step": {
+ "reauth": {
+ "data": {
+ "password": "Jelsz\u00f3"
+ }
+ },
"user": {
"data": {
- "url": "Weboldal"
+ "password": "Jelsz\u00f3",
+ "url": "Weboldal",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
}
}
}
diff --git a/homeassistant/components/fireservicerota/translations/id.json b/homeassistant/components/fireservicerota/translations/id.json
new file mode 100644
index 00000000000000..0c4462a1ea7636
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/id.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi",
+ "reauth_successful": "Autentikasi ulang berhasil"
+ },
+ "create_entry": {
+ "default": "Berhasil diautentikasi"
+ },
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Kata Sandi"
+ },
+ "description": "Token autentikasi menjadi tidak valid, masuk untuk membuat token lagi."
+ },
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "url": "Situs Web",
+ "username": "Nama Pengguna"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fireservicerota/translations/it.json b/homeassistant/components/fireservicerota/translations/it.json
index 8fc43f294ec5aa..6960b68b2a20ed 100644
--- a/homeassistant/components/fireservicerota/translations/it.json
+++ b/homeassistant/components/fireservicerota/translations/it.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "L'account \u00e8 gi\u00e0 configurato",
- "reauth_successful": "La riautenticazione ha avuto successo"
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
},
"create_entry": {
"default": "Autenticazione riuscita"
diff --git a/homeassistant/components/fireservicerota/translations/ko.json b/homeassistant/components/fireservicerota/translations/ko.json
new file mode 100644
index 00000000000000..843371ed03561b
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/ko.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "create_entry": {
+ "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638"
+ },
+ "description": "\uc778\uc99d \ud1a0\ud070\uc774 \ub354 \uc774\uc0c1 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc0dd\uc131\ud558\ub824\uba74 \ub85c\uadf8\uc778\ud574\uc8fc\uc138\uc694."
+ },
+ "user": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "url": "\uc6f9\uc0ac\uc774\ud2b8",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fireservicerota/translations/nl.json b/homeassistant/components/fireservicerota/translations/nl.json
new file mode 100644
index 00000000000000..3a6ba936dee1d1
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/nl.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Account is al geconfigureerd",
+ "reauth_successful": "Herauthenticatie was succesvol"
+ },
+ "create_entry": {
+ "default": "Succesvol geauthenticeerd"
+ },
+ "error": {
+ "invalid_auth": "Ongeldige authenticatie"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Wachtwoord"
+ },
+ "description": "Authenticatietokens zijn ongeldig geworden, log in om ze opnieuw te maken."
+ },
+ "user": {
+ "data": {
+ "password": "Wachtwoord",
+ "url": "Website",
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fireservicerota/translations/no.json b/homeassistant/components/fireservicerota/translations/no.json
index af1ceba2c97e90..be485577e65aea 100644
--- a/homeassistant/components/fireservicerota/translations/no.json
+++ b/homeassistant/components/fireservicerota/translations/no.json
@@ -15,7 +15,7 @@
"data": {
"password": "Passord"
},
- "description": "Godkjenningstokener ble ugyldige, logg inn for \u00e5 gjenopprette dem"
+ "description": "Autentiseringstokener ble ugyldige, logg inn for \u00e5 gjenskape dem."
},
"user": {
"data": {
diff --git a/homeassistant/components/fireservicerota/translations/ru.json b/homeassistant/components/fireservicerota/translations/ru.json
index 2c90bd53ca95e5..046a65081ec61b 100644
--- a/homeassistant/components/fireservicerota/translations/ru.json
+++ b/homeassistant/components/fireservicerota/translations/ru.json
@@ -8,7 +8,7 @@
"default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
},
"error": {
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"step": {
"reauth": {
@@ -21,7 +21,7 @@
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"url": "\u0412\u0435\u0431-\u0441\u0430\u0439\u0442",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
}
}
}
diff --git a/homeassistant/components/fireservicerota/translations/tr.json b/homeassistant/components/fireservicerota/translations/tr.json
index a2d2cab3b7469f..f54d10f6cbf71a 100644
--- a/homeassistant/components/fireservicerota/translations/tr.json
+++ b/homeassistant/components/fireservicerota/translations/tr.json
@@ -1,5 +1,15 @@
{
"config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu"
+ },
+ "create_entry": {
+ "default": "Ba\u015far\u0131yla do\u011fruland\u0131"
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
"step": {
"reauth": {
"data": {
diff --git a/homeassistant/components/fireservicerota/translations/uk.json b/homeassistant/components/fireservicerota/translations/uk.json
new file mode 100644
index 00000000000000..2d3bf8c596e037
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/uk.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e"
+ },
+ "create_entry": {
+ "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e."
+ },
+ "error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "description": "\u0422\u043e\u043a\u0435\u043d\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0456, \u0443\u0432\u0456\u0439\u0434\u0456\u0442\u044c, \u0449\u043e\u0431 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u0457\u0445 \u0437\u0430\u043d\u043e\u0432\u043e."
+ },
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "url": "\u0412\u0435\u0431-\u0441\u0430\u0439\u0442",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fireservicerota/translations/zh-Hant.json b/homeassistant/components/fireservicerota/translations/zh-Hant.json
index af3cba40dc6785..8e5f4d9f20db82 100644
--- a/homeassistant/components/fireservicerota/translations/zh-Hant.json
+++ b/homeassistant/components/fireservicerota/translations/zh-Hant.json
@@ -15,7 +15,7 @@
"data": {
"password": "\u5bc6\u78bc"
},
- "description": "\u8a8d\u8b49\u5bc6\u9470\u5df2\u7d93\u5931\u6548\uff0c\u8acb\u767b\u5165\u91cd\u65b0\u65b0\u589e\u3002"
+ "description": "\u8a8d\u8b49\u6b0a\u6756\u5df2\u7d93\u5931\u6548\uff0c\u8acb\u767b\u5165\u91cd\u65b0\u65b0\u589e\u3002"
},
"user": {
"data": {
diff --git a/homeassistant/components/firmata/config_flow.py b/homeassistant/components/firmata/config_flow.py
index a86d97e9e2e9a2..e9483998060619 100644
--- a/homeassistant/components/firmata/config_flow.py
+++ b/homeassistant/components/firmata/config_flow.py
@@ -8,7 +8,7 @@
from homeassistant.const import CONF_NAME
from .board import get_board
-from .const import CONF_SERIAL_PORT, DOMAIN # pylint: disable=unused-import
+from .const import CONF_SERIAL_PORT, DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/firmata/const.py b/homeassistant/components/firmata/const.py
index 6259582b5f790b..0d859363e2b489 100644
--- a/homeassistant/components/firmata/const.py
+++ b/homeassistant/components/firmata/const.py
@@ -10,7 +10,6 @@
CONF_ARDUINO_WAIT = "arduino_wait"
CONF_DIFFERENTIAL = "differential"
CONF_INITIAL_STATE = "initial"
-CONF_NAME = "name"
CONF_NEGATE_STATE = "negate"
CONF_PINS = "pins"
CONF_PIN_MODE = "pin_mode"
diff --git a/homeassistant/components/firmata/entity.py b/homeassistant/components/firmata/entity.py
index 50ab58b904608a..8f843d29272d5e 100644
--- a/homeassistant/components/firmata/entity.py
+++ b/homeassistant/components/firmata/entity.py
@@ -1,5 +1,5 @@
"""Entity for Firmata devices."""
-from typing import Type
+from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
@@ -32,7 +32,7 @@ class FirmataPinEntity(FirmataEntity):
def __init__(
self,
- api: Type[FirmataBoardPin],
+ api: type[FirmataBoardPin],
config_entry: ConfigEntry,
name: str,
pin: FirmataPinType,
diff --git a/homeassistant/components/firmata/light.py b/homeassistant/components/firmata/light.py
index e95b51014136b0..3c50a559e51b8c 100644
--- a/homeassistant/components/firmata/light.py
+++ b/homeassistant/components/firmata/light.py
@@ -1,7 +1,7 @@
"""Support for Firmata light output."""
+from __future__ import annotations
import logging
-from typing import Type
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -55,7 +55,7 @@ class FirmataLight(FirmataPinEntity, LightEntity):
def __init__(
self,
- api: Type[FirmataBoardPin],
+ api: type[FirmataBoardPin],
config_entry: ConfigEntry,
name: str,
pin: FirmataPinType,
diff --git a/homeassistant/components/firmata/sensor.py b/homeassistant/components/firmata/sensor.py
index cb9db1f11e5f55..fedac6f76d9070 100644
--- a/homeassistant/components/firmata/sensor.py
+++ b/homeassistant/components/firmata/sensor.py
@@ -2,10 +2,10 @@
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CONF_PIN
from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity import Entity
from .const import CONF_DIFFERENTIAL, CONF_PIN_MODE, DOMAIN
from .entity import FirmataPinEntity
@@ -42,7 +42,7 @@ async def async_setup_entry(
async_add_entities(new_entities)
-class FirmataSensor(FirmataPinEntity, Entity):
+class FirmataSensor(FirmataPinEntity, SensorEntity):
"""Representation of a sensor on a Firmata board."""
async def async_added_to_hass(self) -> None:
diff --git a/homeassistant/components/firmata/translations/fr.json b/homeassistant/components/firmata/translations/fr.json
index a66d58dce876d5..b3509c9126c1d0 100644
--- a/homeassistant/components/firmata/translations/fr.json
+++ b/homeassistant/components/firmata/translations/fr.json
@@ -2,6 +2,10 @@
"config": {
"abort": {
"cannot_connect": "Impossible de se connecter \u00e0 la carte Firmata pendant la configuration"
+ },
+ "step": {
+ "one": "Vide ",
+ "other": "Vide"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/firmata/translations/hu.json b/homeassistant/components/firmata/translations/hu.json
new file mode 100644
index 00000000000000..563ede561557c5
--- /dev/null
+++ b/homeassistant/components/firmata/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/firmata/translations/id.json b/homeassistant/components/firmata/translations/id.json
new file mode 100644
index 00000000000000..3f10b4aa77c440
--- /dev/null
+++ b/homeassistant/components/firmata/translations/id.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Gagal terhubung"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/firmata/translations/ko.json b/homeassistant/components/firmata/translations/ko.json
index 753a5851811001..b5b8d46f329cfe 100644
--- a/homeassistant/components/firmata/translations/ko.json
+++ b/homeassistant/components/firmata/translations/ko.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "cannot_connect": "\uc124\uce58\ud558\ub294 \ub3d9\uc548 Firmata \ubcf4\ub4dc\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/firmata/translations/nl.json b/homeassistant/components/firmata/translations/nl.json
new file mode 100644
index 00000000000000..7cb0141826a8fb
--- /dev/null
+++ b/homeassistant/components/firmata/translations/nl.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Kan geen verbinding maken"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/firmata/translations/tr.json b/homeassistant/components/firmata/translations/tr.json
new file mode 100644
index 00000000000000..b7d038a229b0df
--- /dev/null
+++ b/homeassistant/components/firmata/translations/tr.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/firmata/translations/uk.json b/homeassistant/components/firmata/translations/uk.json
new file mode 100644
index 00000000000000..41b670fbb184cf
--- /dev/null
+++ b/homeassistant/components/firmata/translations/uk.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py
index 387eb78448c7a8..8571d31bc8abc0 100644
--- a/homeassistant/components/fitbit/sensor.py
+++ b/homeassistant/components/fitbit/sensor.py
@@ -10,7 +10,7 @@
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_CLIENT_ID,
@@ -25,7 +25,6 @@
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.helpers.network import get_url
from homeassistant.util.json import load_json, save_json
@@ -403,7 +402,7 @@ def get(self, request):
return html_response
-class FitbitSensor(Entity):
+class FitbitSensor(SensorEntity):
"""Implementation of a Fitbit sensor."""
def __init__(
@@ -457,7 +456,7 @@ def icon(self):
return f"mdi:{FITBIT_RESOURCES_LIST[self.resource_type][2]}"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attrs = {}
diff --git a/homeassistant/components/fixer/sensor.py b/homeassistant/components/fixer/sensor.py
index e3dfd432a416a9..9214dd6907e027 100644
--- a/homeassistant/components/fixer/sensor.py
+++ b/homeassistant/components/fixer/sensor.py
@@ -6,10 +6,9 @@
from fixerio.exceptions import FixerioException
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME, CONF_TARGET
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -17,8 +16,6 @@
ATTR_TARGET = "Target currency"
ATTRIBUTION = "Data provided by the European Central Bank (ECB)"
-CONF_TARGET = "target"
-
DEFAULT_BASE = "USD"
DEFAULT_NAME = "Exchange rate"
@@ -37,7 +34,6 @@
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Fixer.io sensor."""
-
api_key = config.get(CONF_API_KEY)
name = config.get(CONF_NAME)
target = config.get(CONF_TARGET)
@@ -52,7 +48,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([ExchangeRateSensor(data, name, target)], True)
-class ExchangeRateSensor(Entity):
+class ExchangeRateSensor(SensorEntity):
"""Representation of a Exchange sensor."""
def __init__(self, data, name, target):
@@ -78,7 +74,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self.data.rate is not None:
return {
@@ -103,7 +99,6 @@ class ExchangeData:
def __init__(self, target_currency, api_key):
"""Initialize the data object."""
-
self.api_key = api_key
self.rate = None
self.target_currency = target_currency
diff --git a/homeassistant/components/fleetgo/device_tracker.py b/homeassistant/components/fleetgo/device_tracker.py
index d46bbc9c85bc91..1f4c0d0ddfcb58 100644
--- a/homeassistant/components/fleetgo/device_tracker.py
+++ b/homeassistant/components/fleetgo/device_tracker.py
@@ -9,6 +9,7 @@
from homeassistant.const import (
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
+ CONF_INCLUDE,
CONF_PASSWORD,
CONF_USERNAME,
)
@@ -17,8 +18,6 @@
_LOGGER = logging.getLogger(__name__)
-CONF_INCLUDE = "include"
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_USERNAME): cv.string,
@@ -44,7 +43,6 @@ class FleetGoDeviceScanner:
def __init__(self, config, see):
"""Initialize FleetGoDeviceScanner."""
-
self._include = config.get(CONF_INCLUDE)
self._see = see
diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py
index 450d09edeb8af9..cf4662b98666e0 100644
--- a/homeassistant/components/flexit/climate.py
+++ b/homeassistant/components/flexit/climate.py
@@ -1,6 +1,7 @@
"""Platform for Flexit AC units with CI66 Modbus adapter."""
+from __future__ import annotations
+
import logging
-from typing import List
from pyflexit.pyflexit import pyflexit
import voluptuous as vol
@@ -93,7 +94,7 @@ def update(self):
self._current_operation = self.unit.get_operation
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
return {
"filter_hours": self._filter_hours,
@@ -135,7 +136,7 @@ def hvac_mode(self):
return self._current_operation
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes.
Need to be a subset of HVAC_MODES.
diff --git a/homeassistant/components/flic/binary_sensor.py b/homeassistant/components/flic/binary_sensor.py
index e81f8f2f5b048d..6ddaddced0de80 100644
--- a/homeassistant/components/flic/binary_sensor.py
+++ b/homeassistant/components/flic/binary_sensor.py
@@ -186,7 +186,7 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
return {"address": self.address}
diff --git a/homeassistant/components/flick_electric/config_flow.py b/homeassistant/components/flick_electric/config_flow.py
index 2dba50cccca8c2..6956c034d4a5fa 100644
--- a/homeassistant/components/flick_electric/config_flow.py
+++ b/homeassistant/components/flick_electric/config_flow.py
@@ -16,7 +16,7 @@
)
from homeassistant.helpers import aiohttp_client
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/flick_electric/sensor.py b/homeassistant/components/flick_electric/sensor.py
index 9d441ce7574d99..e68806002126e2 100644
--- a/homeassistant/components/flick_electric/sensor.py
+++ b/homeassistant/components/flick_electric/sensor.py
@@ -5,10 +5,10 @@
import async_timeout
from pyflick import FlickAPI, FlickPrice
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME
from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity import Entity
from homeassistant.util.dt import utcnow
from .const import ATTR_COMPONENTS, ATTR_END_AT, ATTR_START_AT, DOMAIN
@@ -33,7 +33,7 @@ async def async_setup_entry(
async_add_entities([FlickPricingSensor(api)], True)
-class FlickPricingSensor(Entity):
+class FlickPricingSensor(SensorEntity):
"""Entity object for Flick Electric sensor."""
def __init__(self, api: FlickAPI):
@@ -61,7 +61,7 @@ def unit_of_measurement(self):
return UNIT_NAME
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attributes
diff --git a/homeassistant/components/flick_electric/translations/de.json b/homeassistant/components/flick_electric/translations/de.json
index ed0ef205ff031a..13ae8555608406 100644
--- a/homeassistant/components/flick_electric/translations/de.json
+++ b/homeassistant/components/flick_electric/translations/de.json
@@ -4,7 +4,7 @@
"already_configured": "Dieses Konto ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
@@ -12,9 +12,11 @@
"user": {
"data": {
"client_id": "Client-ID (optional)",
+ "client_secret": "Client Secret (optional)",
"password": "Passwort",
"username": "Benutzername"
- }
+ },
+ "title": "Flick Anmeldedaten"
}
}
}
diff --git a/homeassistant/components/flick_electric/translations/hu.json b/homeassistant/components/flick_electric/translations/hu.json
index dee4ed9ee0fa4d..f7ed726e43305f 100644
--- a/homeassistant/components/flick_electric/translations/hu.json
+++ b/homeassistant/components/flick_electric/translations/hu.json
@@ -1,11 +1,20 @@
{
"config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
"step": {
"user": {
"data": {
"password": "Jelsz\u00f3",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
- }
+ },
+ "title": "Flick Bejelentkez\u00e9si Adatok"
}
}
}
diff --git a/homeassistant/components/flick_electric/translations/id.json b/homeassistant/components/flick_electric/translations/id.json
new file mode 100644
index 00000000000000..8c283cfd56eddb
--- /dev/null
+++ b/homeassistant/components/flick_electric/translations/id.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "client_id": "ID Klien (Opsional)",
+ "client_secret": "Kode Rahasia Klien (Opsional)",
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "title": "Kredensial Masuk Flick"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flick_electric/translations/ko.json b/homeassistant/components/flick_electric/translations/ko.json
index 82d095f275512d..e5b69253fa7186 100644
--- a/homeassistant/components/flick_electric/translations/ko.json
+++ b/homeassistant/components/flick_electric/translations/ko.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\ud574\ub2f9 \uacc4\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
diff --git a/homeassistant/components/flick_electric/translations/ru.json b/homeassistant/components/flick_electric/translations/ru.json
index c97bb9133cc562..08bfc3ffb02ab9 100644
--- a/homeassistant/components/flick_electric/translations/ru.json
+++ b/homeassistant/components/flick_electric/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
@@ -14,7 +14,7 @@
"client_id": "ID \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)",
"client_secret": "\u0421\u0435\u043a\u0440\u0435\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "Flick Electric"
}
diff --git a/homeassistant/components/flick_electric/translations/tr.json b/homeassistant/components/flick_electric/translations/tr.json
new file mode 100644
index 00000000000000..a83e1936fb4a15
--- /dev/null
+++ b/homeassistant/components/flick_electric/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flick_electric/translations/uk.json b/homeassistant/components/flick_electric/translations/uk.json
new file mode 100644
index 00000000000000..4d72844bc74fd0
--- /dev/null
+++ b/homeassistant/components/flick_electric/translations/uk.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "client_id": "ID \u043a\u043b\u0456\u0454\u043d\u0442\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)",
+ "client_secret": "\u0421\u0435\u043a\u0440\u0435\u0442 \u043a\u043b\u0456\u0454\u043d\u0442\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "Flick Electric"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py
index b57cdd5f8715ff..71f8a8bfe5c42c 100644
--- a/homeassistant/components/flo/__init__.py
+++ b/homeassistant/components/flo/__init__.py
@@ -49,9 +49,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
tasks = [device.async_refresh() for device in devices]
await asyncio.gather(*tasks)
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -62,8 +62,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/flo/binary_sensor.py b/homeassistant/components/flo/binary_sensor.py
index a8bac49867471b..bd623aa38bb1bd 100644
--- a/homeassistant/components/flo/binary_sensor.py
+++ b/homeassistant/components/flo/binary_sensor.py
@@ -1,6 +1,5 @@
"""Support for Flo Water Monitor binary sensors."""
-
-from typing import List
+from __future__ import annotations
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_PROBLEM,
@@ -14,10 +13,24 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Flo sensors from config entry."""
- devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][
+ devices: list[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][
config_entry.entry_id
]["devices"]
- entities = [FloPendingAlertsBinarySensor(device) for device in devices]
+ entities = []
+ for device in devices:
+ if device.device_type == "puck_oem":
+ # Flo "pucks" (leak detectors) *do* support pending alerts.
+ # However these pending alerts mix unrelated issues like
+ # low-battery alerts, humidity alerts, & temperature alerts
+ # in addition to the critical "water detected" alert.
+ #
+ # Since there are non-binary sensors for battery, humidity,
+ # and temperature, the binary sensor should only cover
+ # water detection. If this sensor trips, you really have
+ # a problem - vs. battery/temp/humidity which are warnings.
+ entities.append(FloWaterDetectedBinarySensor(device))
+ else:
+ entities.append(FloPendingAlertsBinarySensor(device))
async_add_entities(entities)
@@ -34,7 +47,7 @@ def is_on(self):
return self._device.has_alerts
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if not self._device.has_alerts:
return {}
@@ -48,3 +61,21 @@ def device_state_attributes(self):
def device_class(self):
"""Return the device class for the binary sensor."""
return DEVICE_CLASS_PROBLEM
+
+
+class FloWaterDetectedBinarySensor(FloEntity, BinarySensorEntity):
+ """Binary sensor that reports if water is detected (for leak detectors)."""
+
+ def __init__(self, device):
+ """Initialize the pending alerts binary sensor."""
+ super().__init__("water_detected", "Water Detected", device)
+
+ @property
+ def is_on(self):
+ """Return true if the Flo device is detecting water."""
+ return self._device.water_detected
+
+ @property
+ def device_class(self):
+ """Return the device class for the binary sensor."""
+ return DEVICE_CLASS_PROBLEM
diff --git a/homeassistant/components/flo/config_flow.py b/homeassistant/components/flo/config_flow.py
index 54c6ae94ee26e9..78899035bfa733 100644
--- a/homeassistant/components/flo/config_flow.py
+++ b/homeassistant/components/flo/config_flow.py
@@ -7,7 +7,7 @@
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from .const import DOMAIN, LOGGER # pylint:disable=unused-import
+from .const import DOMAIN, LOGGER
DATA_SCHEMA = vol.Schema({"username": str, "password": str})
diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py
index af36034026d710..50e0ccda87f47a 100644
--- a/homeassistant/components/flo/device.py
+++ b/homeassistant/components/flo/device.py
@@ -1,7 +1,9 @@
"""Flo device object."""
+from __future__ import annotations
+
import asyncio
from datetime import datetime, timedelta
-from typing import Any, Dict, Optional
+from typing import Any
from aioflo.api import API
from aioflo.errors import RequestError
@@ -26,8 +28,8 @@ def __init__(
self._flo_location_id: str = location_id
self._flo_device_id: str = device_id
self._manufacturer: str = "Flo by Moen"
- self._device_information: Optional[Dict[str, Any]] = None
- self._water_usage: Optional[Dict[str, Any]] = None
+ self._device_information: dict[str, Any] | None = None
+ self._water_usage: dict[str, Any] | None = None
super().__init__(
hass,
LOGGER,
@@ -58,7 +60,9 @@ def id(self) -> str:
@property
def device_name(self) -> str:
"""Return device name."""
- return f"{self.manufacturer} {self.model}"
+ return self._device_information.get(
+ "nickname", f"{self.manufacturer} {self.model}"
+ )
@property
def manufacturer(self) -> str:
@@ -120,6 +124,11 @@ def temperature(self) -> float:
"""Return the current temperature in degrees F."""
return self._device_information["telemetry"]["current"]["tempF"]
+ @property
+ def humidity(self) -> float:
+ """Return the current humidity in percent (0-100)."""
+ return self._device_information["telemetry"]["current"]["humidity"]
+
@property
def consumption_today(self) -> float:
"""Return the current consumption for today in gallons."""
@@ -159,6 +168,11 @@ def has_alerts(self) -> bool:
or self.pending_warning_alerts_count
)
+ @property
+ def water_detected(self) -> bool:
+ """Return whether water is detected, for leak detectors."""
+ return self._device_information["fwProperties"]["telemetry_water"]
+
@property
def last_known_valve_state(self) -> str:
"""Return the last known valve state for the device."""
@@ -169,6 +183,11 @@ def target_valve_state(self) -> str:
"""Return the target valve state for the device."""
return self._device_information["valve"]["target"]
+ @property
+ def battery_level(self) -> float:
+ """Return the battery level for battery-powered device, e.g. leak detectors."""
+ return self._device_information["battery"]["level"]
+
async def async_set_mode_home(self):
"""Set the Flo location to home mode."""
await self.api_client.location.set_mode_home(self._flo_location_id)
diff --git a/homeassistant/components/flo/entity.py b/homeassistant/components/flo/entity.py
index 35c6e022dcf0da..878c4188815d52 100644
--- a/homeassistant/components/flo/entity.py
+++ b/homeassistant/components/flo/entity.py
@@ -1,6 +1,7 @@
"""Base entity class for Flo entities."""
+from __future__ import annotations
-from typing import Any, Dict
+from typing import Any
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.entity import Entity
@@ -36,7 +37,7 @@ def unique_id(self) -> str:
return self._unique_id
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return a device description for device registry."""
return {
"identifiers": {(FLO_DOMAIN, self._device.id)},
diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py
index 2feeb3702a6e96..1e362e75f8cbf1 100644
--- a/homeassistant/components/flo/sensor.py
+++ b/homeassistant/components/flo/sensor.py
@@ -1,15 +1,17 @@
"""Support for Flo Water Monitor sensors."""
+from __future__ import annotations
-from typing import List, Optional
-
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
+ DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
+ PERCENTAGE,
PRESSURE_PSI,
- TEMP_CELSIUS,
+ TEMP_FAHRENHEIT,
VOLUME_GALLONS,
)
-from homeassistant.util.temperature import fahrenheit_to_celsius
from .const import DOMAIN as FLO_DOMAIN
from .device import FloDeviceDataUpdateCoordinator
@@ -20,25 +22,42 @@
NAME_DAILY_USAGE = "Today's Water Usage"
NAME_CURRENT_SYSTEM_MODE = "Current System Mode"
NAME_FLOW_RATE = "Water Flow Rate"
-NAME_TEMPERATURE = "Water Temperature"
+NAME_WATER_TEMPERATURE = "Water Temperature"
+NAME_AIR_TEMPERATURE = "Temperature"
NAME_WATER_PRESSURE = "Water Pressure"
+NAME_HUMIDITY = "Humidity"
+NAME_BATTERY = "Battery"
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Flo sensors from config entry."""
- devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][
+ devices: list[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][
config_entry.entry_id
]["devices"]
entities = []
- entities.extend([FloDailyUsageSensor(device) for device in devices])
- entities.extend([FloSystemModeSensor(device) for device in devices])
- entities.extend([FloCurrentFlowRateSensor(device) for device in devices])
- entities.extend([FloTemperatureSensor(device) for device in devices])
- entities.extend([FloPressureSensor(device) for device in devices])
+ for device in devices:
+ if device.device_type == "puck_oem":
+ entities.extend(
+ [
+ FloTemperatureSensor(NAME_AIR_TEMPERATURE, device),
+ FloHumiditySensor(device),
+ FloBatterySensor(device),
+ ]
+ )
+ else:
+ entities.extend(
+ [
+ FloDailyUsageSensor(device),
+ FloSystemModeSensor(device),
+ FloCurrentFlowRateSensor(device),
+ FloTemperatureSensor(NAME_WATER_TEMPERATURE, device),
+ FloPressureSensor(device),
+ ]
+ )
async_add_entities(entities)
-class FloDailyUsageSensor(FloEntity):
+class FloDailyUsageSensor(FloEntity, SensorEntity):
"""Monitors the daily water usage."""
def __init__(self, device):
@@ -52,7 +71,7 @@ def icon(self) -> str:
return WATER_ICON
@property
- def state(self) -> Optional[float]:
+ def state(self) -> float | None:
"""Return the current daily usage."""
if self._device.consumption_today is None:
return None
@@ -64,7 +83,7 @@ def unit_of_measurement(self) -> str:
return VOLUME_GALLONS
-class FloSystemModeSensor(FloEntity):
+class FloSystemModeSensor(FloEntity, SensorEntity):
"""Monitors the current Flo system mode."""
def __init__(self, device):
@@ -73,14 +92,14 @@ def __init__(self, device):
self._state: str = None
@property
- def state(self) -> Optional[str]:
+ def state(self) -> str | None:
"""Return the current system mode."""
if not self._device.current_system_mode:
return None
return self._device.current_system_mode
-class FloCurrentFlowRateSensor(FloEntity):
+class FloCurrentFlowRateSensor(FloEntity, SensorEntity):
"""Monitors the current water flow rate."""
def __init__(self, device):
@@ -94,7 +113,7 @@ def icon(self) -> str:
return GAUGE_ICON
@property
- def state(self) -> Optional[float]:
+ def state(self) -> float | None:
"""Return the current flow rate."""
if self._device.current_flow_rate is None:
return None
@@ -106,33 +125,59 @@ def unit_of_measurement(self) -> str:
return "gpm"
-class FloTemperatureSensor(FloEntity):
+class FloTemperatureSensor(FloEntity, SensorEntity):
"""Monitors the temperature."""
- def __init__(self, device):
+ def __init__(self, name, device):
"""Initialize the temperature sensor."""
- super().__init__("temperature", NAME_TEMPERATURE, device)
+ super().__init__("temperature", name, device)
self._state: float = None
@property
- def state(self) -> Optional[float]:
+ def state(self) -> float | None:
"""Return the current temperature."""
if self._device.temperature is None:
return None
- return round(fahrenheit_to_celsius(self._device.temperature), 1)
+ return round(self._device.temperature, 1)
@property
def unit_of_measurement(self) -> str:
- """Return gallons as the unit measurement for water."""
- return TEMP_CELSIUS
+ """Return fahrenheit as the unit measurement for temperature."""
+ return TEMP_FAHRENHEIT
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the device class for this sensor."""
return DEVICE_CLASS_TEMPERATURE
-class FloPressureSensor(FloEntity):
+class FloHumiditySensor(FloEntity, SensorEntity):
+ """Monitors the humidity."""
+
+ def __init__(self, device):
+ """Initialize the humidity sensor."""
+ super().__init__("humidity", NAME_HUMIDITY, device)
+ self._state: float = None
+
+ @property
+ def state(self) -> float | None:
+ """Return the current humidity."""
+ if self._device.humidity is None:
+ return None
+ return round(self._device.humidity, 1)
+
+ @property
+ def unit_of_measurement(self) -> str:
+ """Return percent as the unit measurement for humidity."""
+ return PERCENTAGE
+
+ @property
+ def device_class(self) -> str | None:
+ """Return the device class for this sensor."""
+ return DEVICE_CLASS_HUMIDITY
+
+
+class FloPressureSensor(FloEntity, SensorEntity):
"""Monitors the water pressure."""
def __init__(self, device):
@@ -141,7 +186,7 @@ def __init__(self, device):
self._state: float = None
@property
- def state(self) -> Optional[float]:
+ def state(self) -> float | None:
"""Return the current water pressure."""
if self._device.current_psi is None:
return None
@@ -153,6 +198,30 @@ def unit_of_measurement(self) -> str:
return PRESSURE_PSI
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the device class for this sensor."""
return DEVICE_CLASS_PRESSURE
+
+
+class FloBatterySensor(FloEntity, SensorEntity):
+ """Monitors the battery level for battery-powered leak detectors."""
+
+ def __init__(self, device):
+ """Initialize the battery sensor."""
+ super().__init__("battery", NAME_BATTERY, device)
+ self._state: float = None
+
+ @property
+ def state(self) -> float | None:
+ """Return the current battery level."""
+ return self._device.battery_level
+
+ @property
+ def unit_of_measurement(self) -> str:
+ """Return percentage as the unit measurement for battery."""
+ return PERCENTAGE
+
+ @property
+ def device_class(self) -> str | None:
+ """Return the device class for this sensor."""
+ return DEVICE_CLASS_BATTERY
diff --git a/homeassistant/components/flo/switch.py b/homeassistant/components/flo/switch.py
index 91f3fdf54e4f3c..e5f00a6125fb52 100644
--- a/homeassistant/components/flo/switch.py
+++ b/homeassistant/components/flo/switch.py
@@ -1,6 +1,5 @@
"""Switch representing the shutoff valve for the Flo by Moen integration."""
-
-from typing import List
+from __future__ import annotations
from aioflo.location import SLEEP_MINUTE_OPTIONS, SYSTEM_MODE_HOME, SYSTEM_REVERT_MODES
import voluptuous as vol
@@ -23,10 +22,14 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Flo switches from config entry."""
- devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][
+ devices: list[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][
config_entry.entry_id
]["devices"]
- async_add_entities([FloSwitch(device) for device in devices])
+ entities = []
+ for device in devices:
+ if device.device_type != "puck_oem":
+ entities.append(FloSwitch(device))
+ async_add_entities(entities)
platform = entity_platform.current_platform.get()
diff --git a/homeassistant/components/flo/translations/de.json b/homeassistant/components/flo/translations/de.json
index 382156757010d5..625c7372347a61 100644
--- a/homeassistant/components/flo/translations/de.json
+++ b/homeassistant/components/flo/translations/de.json
@@ -1,12 +1,17 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"step": {
"user": {
"data": {
+ "host": "Host",
"password": "Passwort",
"username": "Benutzername"
}
diff --git a/homeassistant/components/flo/translations/hu.json b/homeassistant/components/flo/translations/hu.json
index 3b2d79a34a77e2..0abcc301f0c854 100644
--- a/homeassistant/components/flo/translations/hu.json
+++ b/homeassistant/components/flo/translations/hu.json
@@ -2,6 +2,20 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/flo/translations/id.json b/homeassistant/components/flo/translations/id.json
new file mode 100644
index 00000000000000..ed8fde321061cf
--- /dev/null
+++ b/homeassistant/components/flo/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flo/translations/ko.json b/homeassistant/components/flo/translations/ko.json
index ab85b70afa76b7..9ba063c37ddf46 100644
--- a/homeassistant/components/flo/translations/ko.json
+++ b/homeassistant/components/flo/translations/ko.json
@@ -1,17 +1,19 @@
{
"config": {
"abort": {
- "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
- "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d",
- "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
"data": {
- "host": "\ud638\uc2a4\ud2b8"
+ "host": "\ud638\uc2a4\ud2b8",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
}
}
}
diff --git a/homeassistant/components/flo/translations/ru.json b/homeassistant/components/flo/translations/ru.json
index 6f71ee41376162..9b02cafd466691 100644
--- a/homeassistant/components/flo/translations/ru.json
+++ b/homeassistant/components/flo/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
@@ -13,7 +13,7 @@
"data": {
"host": "\u0425\u043e\u0441\u0442",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
}
}
}
diff --git a/homeassistant/components/flo/translations/tr.json b/homeassistant/components/flo/translations/tr.json
new file mode 100644
index 00000000000000..40c9c39b967721
--- /dev/null
+++ b/homeassistant/components/flo/translations/tr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flo/translations/uk.json b/homeassistant/components/flo/translations/uk.json
new file mode 100644
index 00000000000000..2df11f744559db
--- /dev/null
+++ b/homeassistant/components/flo/translations/uk.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py
index b9a5fc17682d2a..fb87588ac8247a 100644
--- a/homeassistant/components/flume/__init__.py
+++ b/homeassistant/components/flume/__init__.py
@@ -79,9 +79,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
FLUME_HTTP_SESSION: http_session,
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -92,8 +92,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/flume/config_flow.py b/homeassistant/components/flume/config_flow.py
index e0e5bf8efe9a67..7a028b94eb031b 100644
--- a/homeassistant/components/flume/config_flow.py
+++ b/homeassistant/components/flume/config_flow.py
@@ -14,8 +14,7 @@
CONF_USERNAME,
)
-from .const import BASE_TOKEN_FILENAME
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import BASE_TOKEN_FILENAME, DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py
index 6b19c9c5476bca..d890443d238edc 100644
--- a/homeassistant/components/flume/sensor.py
+++ b/homeassistant/components/flume/sensor.py
@@ -6,7 +6,7 @@
from pyflume import FlumeData
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
CONF_CLIENT_ID,
@@ -108,7 +108,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(flume_entity_list)
-class FlumeSensor(CoordinatorEntity):
+class FlumeSensor(CoordinatorEntity, SensorEntity):
"""Representation of the Flume sensor."""
def __init__(self, coordinator, flume_device, flume_query_sensor, name, device_id):
@@ -175,7 +175,7 @@ async def _async_update_data():
_LOGGER.debug("Updating Flume data")
try:
await hass.async_add_executor_job(flume_device.update_force)
- except Exception as ex: # pylint: disable=broad-except
+ except Exception as ex:
raise UpdateFailed(f"Error communicating with flume API: {ex}") from ex
_LOGGER.debug(
"Flume update details: %s",
diff --git a/homeassistant/components/flume/translations/de.json b/homeassistant/components/flume/translations/de.json
index 692c38350a8303..c38a5593ac7c12 100644
--- a/homeassistant/components/flume/translations/de.json
+++ b/homeassistant/components/flume/translations/de.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Dieses Konto ist bereits konfiguriert"
+ "already_configured": "Konto wurde bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
diff --git a/homeassistant/components/flume/translations/he.json b/homeassistant/components/flume/translations/he.json
new file mode 100644
index 00000000000000..ac90b3264eab33
--- /dev/null
+++ b/homeassistant/components/flume/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flume/translations/hu.json b/homeassistant/components/flume/translations/hu.json
index dee4ed9ee0fa4d..cc0c820facf293 100644
--- a/homeassistant/components/flume/translations/hu.json
+++ b/homeassistant/components/flume/translations/hu.json
@@ -1,5 +1,13 @@
{
"config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/flume/translations/id.json b/homeassistant/components/flume/translations/id.json
new file mode 100644
index 00000000000000..333afb167e6e82
--- /dev/null
+++ b/homeassistant/components/flume/translations/id.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "client_id": "ID Klien",
+ "client_secret": "Kode Rahasia Klien",
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "description": "Untuk mengakses API Flume Personal, Anda harus meminta 'ID Klien' dan 'Kode Rahasia Klien' di https://portal.flumetech.com/settings#token",
+ "title": "Hubungkan ke Akun Flume Anda"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flume/translations/ko.json b/homeassistant/components/flume/translations/ko.json
index faac5e9c579d32..c82f0a990d8509 100644
--- a/homeassistant/components/flume/translations/ko.json
+++ b/homeassistant/components/flume/translations/ko.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 \uacc4\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
@@ -16,7 +16,7 @@
"password": "\ube44\ubc00\ubc88\ud638",
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
},
- "description": "Flume Personal API \uc5d0 \uc561\uc138\uc2a4 \ud558\ub824\uba74 https://portal.flumetech.com/settings#token \uc5d0\uc11c '\ud074\ub77c\uc774\uc5b8\ud2b8 ID'\ubc0f '\ud074\ub77c\uc774\uc5b8\ud2b8 \uc2dc\ud06c\ub9bf'\uc744 \uc694\uccad\ud574\uc57c \ud569\ub2c8\ub2e4.",
+ "description": "Flume Personal API \uc5d0 \uc811\uadfc\ud558\ub824\uba74 https://portal.flumetech.com/settings#token \uc5d0\uc11c '\ud074\ub77c\uc774\uc5b8\ud2b8 ID'\ubc0f '\ud074\ub77c\uc774\uc5b8\ud2b8 \uc2dc\ud06c\ub9bf'\uc744 \uc694\uccad\ud574\uc57c \ud569\ub2c8\ub2e4.",
"title": "Flume \uacc4\uc815\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
diff --git a/homeassistant/components/flume/translations/nl.json b/homeassistant/components/flume/translations/nl.json
index d176eb133656c9..97daf42d11eb26 100644
--- a/homeassistant/components/flume/translations/nl.json
+++ b/homeassistant/components/flume/translations/nl.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Dit account is al geconfigureerd."
+ "already_configured": "Account is al geconfigureerd"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
diff --git a/homeassistant/components/flume/translations/ru.json b/homeassistant/components/flume/translations/ru.json
index f35579c2dee5b7..757ec6e5226e33 100644
--- a/homeassistant/components/flume/translations/ru.json
+++ b/homeassistant/components/flume/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
@@ -14,7 +14,7 @@
"client_id": "ID \u043a\u043b\u0438\u0435\u043d\u0442\u0430",
"client_secret": "\u0421\u0435\u043a\u0440\u0435\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u0427\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u043c\u0443 API Flume, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c 'ID \u043a\u043b\u0438\u0435\u043d\u0442\u0430' \u0438 '\u0421\u0435\u043a\u0440\u0435\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430' \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 https://portal.flumetech.com/settings#token.",
"title": "Flume"
diff --git a/homeassistant/components/flume/translations/tr.json b/homeassistant/components/flume/translations/tr.json
new file mode 100644
index 00000000000000..a83e1936fb4a15
--- /dev/null
+++ b/homeassistant/components/flume/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flume/translations/uk.json b/homeassistant/components/flume/translations/uk.json
new file mode 100644
index 00000000000000..53fb4f3d6d7e1c
--- /dev/null
+++ b/homeassistant/components/flume/translations/uk.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "client_id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0456\u0454\u043d\u0442\u0430",
+ "client_secret": "\u0421\u0435\u043a\u0440\u0435\u0442 \u043a\u043b\u0456\u0454\u043d\u0442\u0430",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u0429\u043e\u0431 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e API Flume, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 'ID \u043a\u043b\u0456\u0454\u043d\u0442\u0430' \u0456 '\u0421\u0435\u043a\u0440\u0435\u0442 \u043a\u043b\u0456\u0454\u043d\u0442\u0430' \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e https://portal.flumetech.com/settings#token.",
+ "title": "Flume"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py
index 46442f112b6487..8e5e3762f32c89 100644
--- a/homeassistant/components/flunearyou/__init__.py
+++ b/homeassistant/components/flunearyou/__init__.py
@@ -67,9 +67,9 @@ async def async_update(api_category):
await asyncio.gather(*data_init_tasks)
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return True
@@ -80,8 +80,8 @@ async def async_unload_entry(hass, config_entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/flunearyou/config_flow.py b/homeassistant/components/flunearyou/config_flow.py
index 8bdef58f7e1201..5229394b58bd9c 100644
--- a/homeassistant/components/flunearyou/config_flow.py
+++ b/homeassistant/components/flunearyou/config_flow.py
@@ -7,7 +7,7 @@
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.helpers import aiohttp_client, config_validation as cv
-from .const import DOMAIN, LOGGER # pylint: disable=unused-import
+from .const import DOMAIN, LOGGER
class FluNearYouFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py
index 8bb5f1317d12cd..066126c390e71e 100644
--- a/homeassistant/components/flunearyou/sensor.py
+++ b/homeassistant/components/flunearyou/sensor.py
@@ -1,4 +1,5 @@
"""Support for user- and CDC-based flu info sensors from Flu Near You."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_STATE,
@@ -85,7 +86,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors)
-class FluNearYouSensor(CoordinatorEntity):
+class FluNearYouSensor(CoordinatorEntity, SensorEntity):
"""Define a base Flu Near You sensor."""
def __init__(self, coordinator, config_entry, sensor_type, name, icon, unit):
@@ -100,7 +101,7 @@ def __init__(self, coordinator, config_entry, sensor_type, name, icon, unit):
self._unit = unit
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
return self._attrs
diff --git a/homeassistant/components/flunearyou/translations/de.json b/homeassistant/components/flunearyou/translations/de.json
index cd2934170c9c06..1c94931f405e17 100644
--- a/homeassistant/components/flunearyou/translations/de.json
+++ b/homeassistant/components/flunearyou/translations/de.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Diese Koordinaten sind bereits registriert."
+ "already_configured": "Standort ist bereits konfiguriert"
},
"error": {
"unknown": "Unerwarteter Fehler"
diff --git a/homeassistant/components/flunearyou/translations/he.json b/homeassistant/components/flunearyou/translations/he.json
new file mode 100644
index 00000000000000..4c49313d97741a
--- /dev/null
+++ b/homeassistant/components/flunearyou/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flunearyou/translations/hu.json b/homeassistant/components/flunearyou/translations/hu.json
new file mode 100644
index 00000000000000..4f8cca2a93946a
--- /dev/null
+++ b/homeassistant/components/flunearyou/translations/hu.json
@@ -0,0 +1,18 @@
+{
+ "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"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flunearyou/translations/id.json b/homeassistant/components/flunearyou/translations/id.json
new file mode 100644
index 00000000000000..86afc7bb5fd929
--- /dev/null
+++ b/homeassistant/components/flunearyou/translations/id.json
@@ -0,0 +1,20 @@
+{
+ "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"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flunearyou/translations/ko.json b/homeassistant/components/flunearyou/translations/ko.json
index 68e65d3c349ae5..bfe1945fa675bb 100644
--- a/homeassistant/components/flunearyou/translations/ko.json
+++ b/homeassistant/components/flunearyou/translations/ko.json
@@ -1,7 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\uc88c\ud45c\uac12\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "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": {
diff --git a/homeassistant/components/flunearyou/translations/nl.json b/homeassistant/components/flunearyou/translations/nl.json
index c63a59e18e72bf..d78abfcc187be8 100644
--- a/homeassistant/components/flunearyou/translations/nl.json
+++ b/homeassistant/components/flunearyou/translations/nl.json
@@ -1,7 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Deze co\u00f6rdinaten zijn al geregistreerd."
+ "already_configured": "Locatie is al geconfigureerd."
+ },
+ "error": {
+ "unknown": "Onverwachte fout"
},
"step": {
"user": {
@@ -9,7 +12,7 @@
"latitude": "Breedtegraad",
"longitude": "Lengtegraad"
},
- "description": "Bewaak op gebruikers gebaseerde en CDC-repots voor een paar co\u00f6rdinaten.",
+ "description": "Bewaak gebruikersgebaseerde en CDC-rapporten voor een paar co\u00f6rdinaten.",
"title": "Configureer \nFlu Near You"
}
}
diff --git a/homeassistant/components/flunearyou/translations/tr.json b/homeassistant/components/flunearyou/translations/tr.json
new file mode 100644
index 00000000000000..6e749e3c8270fa
--- /dev/null
+++ b/homeassistant/components/flunearyou/translations/tr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Enlem",
+ "longitude": "Boylam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flunearyou/translations/uk.json b/homeassistant/components/flunearyou/translations/uk.json
new file mode 100644
index 00000000000000..354a04d8e7afff
--- /dev/null
+++ b/homeassistant/components/flunearyou/translations/uk.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435."
+ },
+ "error": {
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
+ "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430"
+ },
+ "description": "\u041c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0446\u044c\u043a\u0438\u0445 \u0456 CDC \u0437\u0432\u0456\u0442\u0456\u0432 \u0437\u0430 \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u043c\u0438 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c\u0438.",
+ "title": "Flu Near You"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flunearyou/translations/zh-Hans.json b/homeassistant/components/flunearyou/translations/zh-Hans.json
new file mode 100644
index 00000000000000..f55159dc2356af
--- /dev/null
+++ b/homeassistant/components/flunearyou/translations/zh-Hans.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u4f4d\u7f6e\u5b8c\u6210\u914d\u7f6e"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py
index 4d45f217a59832..ab0d296928f963 100644
--- a/homeassistant/components/flux/switch.py
+++ b/homeassistant/components/flux/switch.py
@@ -22,6 +22,7 @@
from homeassistant.components.switch import DOMAIN, SwitchEntity
from homeassistant.const import (
ATTR_ENTITY_ID,
+ CONF_BRIGHTNESS,
CONF_LIGHTS,
CONF_MODE,
CONF_NAME,
@@ -49,7 +50,6 @@
CONF_START_CT = "start_colortemp"
CONF_SUNSET_CT = "sunset_colortemp"
CONF_STOP_CT = "stop_colortemp"
-CONF_BRIGHTNESS = "brightness"
CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust"
CONF_INTERVAL = "interval"
diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py
index 4bfd0c0a26c77a..2f8d2cc553694e 100644
--- a/homeassistant/components/flux_led/light.py
+++ b/homeassistant/components/flux_led/light.py
@@ -98,7 +98,7 @@
TRANSITION_JUMP = "jump"
TRANSITION_STROBE = "strobe"
-FLUX_EFFECT_LIST = sorted(list(EFFECT_MAP)) + [EFFECT_RANDOM]
+FLUX_EFFECT_LIST = sorted(EFFECT_MAP) + [EFFECT_RANDOM]
CUSTOM_EFFECT_SCHEMA = vol.Schema(
{
diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py
index 9a062133718579..707f22f98ba76c 100644
--- a/homeassistant/components/folder/sensor.py
+++ b/homeassistant/components/folder/sensor.py
@@ -6,10 +6,9 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import DATA_MEGABYTES
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -45,13 +44,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
path = config.get(CONF_FOLDER_PATHS)
if not hass.config.is_allowed_path(path):
- _LOGGER.error("folder %s is not valid or allowed", path)
+ _LOGGER.error("Folder %s is not valid or allowed", path)
else:
folder = Folder(path, config.get(CONF_FILTER))
add_entities([folder], True)
-class Folder(Entity):
+class Folder(SensorEntity):
"""Representation of a folder."""
ICON = "mdi:folder"
@@ -92,7 +91,7 @@ def icon(self):
return self.ICON
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return other details about the sensor state."""
return {
"path": self._folder_path,
diff --git a/homeassistant/components/folder_watcher/__init__.py b/homeassistant/components/folder_watcher/__init__.py
index d99e4928cc5d5d..7d3fd5e77a7a87 100644
--- a/homeassistant/components/folder_watcher/__init__.py
+++ b/homeassistant/components/folder_watcher/__init__.py
@@ -43,7 +43,7 @@ def setup(hass, config):
path = watcher[CONF_FOLDER]
patterns = watcher[CONF_PATTERNS]
if not hass.config.is_allowed_path(path):
- _LOGGER.error("folder %s is not valid or allowed", path)
+ _LOGGER.error("Folder %s is not valid or allowed", path)
return False
Watcher(path, patterns, hass)
diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json
index 722b60a952dcc2..60239aeb0d19f1 100644
--- a/homeassistant/components/folder_watcher/manifest.json
+++ b/homeassistant/components/folder_watcher/manifest.json
@@ -2,7 +2,7 @@
"domain": "folder_watcher",
"name": "Folder Watcher",
"documentation": "https://www.home-assistant.io/integrations/folder_watcher",
- "requirements": ["watchdog==0.8.3"],
+ "requirements": ["watchdog==1.0.2"],
"codeowners": [],
"quality_scale": "internal"
}
diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py
index 9ca265ba9ef4d0..996ac1b1049172 100644
--- a/homeassistant/components/foobot/sensor.py
+++ b/homeassistant/components/foobot/sensor.py
@@ -7,6 +7,7 @@
from foobot_async import FoobotClient
import voluptuous as vol
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_TEMPERATURE,
ATTR_TIME,
@@ -91,7 +92,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(dev, True)
-class FoobotSensor(Entity):
+class FoobotSensor(SensorEntity):
"""Implementation of a Foobot sensor."""
def __init__(self, data, device, sensor_type):
diff --git a/homeassistant/components/forked_daapd/__init__.py b/homeassistant/components/forked_daapd/__init__.py
index 45cefc739b9055..0186b18ee74f98 100644
--- a/homeassistant/components/forked_daapd/__init__.py
+++ b/homeassistant/components/forked_daapd/__init__.py
@@ -4,11 +4,6 @@
from .const import DOMAIN, HASS_DATA_REMOVE_LISTENERS_KEY, HASS_DATA_UPDATER_KEY
-async def async_setup(hass, config):
- """Set up the forked-daapd component."""
- return True
-
-
async def async_setup_entry(hass, entry):
"""Set up forked-daapd from a config entry by forwarding to platform."""
hass.async_create_task(
diff --git a/homeassistant/components/forked_daapd/config_flow.py b/homeassistant/components/forked_daapd/config_flow.py
index 285f138264407a..6bcc35f0a525a3 100644
--- a/homeassistant/components/forked_daapd/config_flow.py
+++ b/homeassistant/components/forked_daapd/config_flow.py
@@ -1,4 +1,5 @@
"""Config flow to configure forked-daapd devices."""
+from contextlib import suppress
import logging
from pyforked_daapd import ForkedDaapdAPI
@@ -9,7 +10,7 @@
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from .const import ( # pylint:disable=unused-import
+from .const import (
CONF_LIBRESPOT_JAVA_PORT,
CONF_MAX_PLAYLISTS,
CONF_TTS_PAUSE_TIME,
@@ -161,12 +162,10 @@ async def async_step_zeroconf(self, discovery_info):
if discovery_info.get("properties") and discovery_info["properties"].get(
"Machine Name"
):
- try:
+ with suppress(ValueError):
version_num = int(
discovery_info["properties"].get("mtd-version", "0").split(".")[0]
)
- except ValueError:
- pass
if version_num < 27:
return self.async_abort(reason="not_forked_daapd")
await self.async_set_unique_id(discovery_info["properties"]["Machine Name"])
@@ -188,6 +187,5 @@ async def async_step_zeroconf(self, discovery_info):
CONF_NAME: discovery_info["properties"]["Machine Name"],
}
self.discovery_schema = vol.Schema(fill_in_schema_dict(zeroconf_data))
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update({"title_placeholders": zeroconf_data})
return await self.async_step_user()
diff --git a/homeassistant/components/forked_daapd/manifest.json b/homeassistant/components/forked_daapd/manifest.json
index 15f043dbbfed0f..b9f78875a2d65a 100644
--- a/homeassistant/components/forked_daapd/manifest.json
+++ b/homeassistant/components/forked_daapd/manifest.json
@@ -1,7 +1,7 @@
{
"domain": "forked_daapd",
"name": "forked-daapd",
- "documentation": "https://www.home-assistant.io/integrations/forked-daapd",
+ "documentation": "https://www.home-assistant.io/integrations/forked_daapd",
"codeowners": ["@uvjustin"],
"requirements": ["pyforked-daapd==0.1.11", "pylibrespot-java==0.1.0"],
"config_flow": true,
diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py
index 195ebf7e2cf4c1..724db80fabdac7 100644
--- a/homeassistant/components/forked_daapd/media_player.py
+++ b/homeassistant/components/forked_daapd/media_player.py
@@ -855,7 +855,7 @@ async def _update(self, update_types):
)
if update_events:
await asyncio.wait(
- [event.wait() for event in update_events.values()]
+ [asyncio.create_task(event.wait()) for event in update_events.values()]
) # make sure callbacks done before update
async_dispatcher_send(
self.hass, SIGNAL_UPDATE_MASTER.format(self._entry_id), True
diff --git a/homeassistant/components/forked_daapd/translations/de.json b/homeassistant/components/forked_daapd/translations/de.json
index a3cdc53c52a102..559db72d42cd01 100644
--- a/homeassistant/components/forked_daapd/translations/de.json
+++ b/homeassistant/components/forked_daapd/translations/de.json
@@ -1,21 +1,26 @@
{
"config": {
"abort": {
- "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "not_forked_daapd": "Das Ger\u00e4t ist kein Forked-Daapd-Server."
},
"error": {
+ "forbidden": "Verbindung kann nicht hergestellt werden. Bitte \u00fcberpr\u00fcfen Sie Ihre forked-daapd-Netzwerkberechtigungen.",
"unknown_error": "Unbekannter Fehler",
- "wrong_host_or_port": "Verbindung konnte nicht hergestellt werden. Bitte \u00fcberpr\u00fcfen Sie Host und Port.",
+ "websocket_not_enabled": "Forked-Daapd-Server-Websocket nicht aktiviert.",
+ "wrong_host_or_port": "Verbindung konnte nicht hergestellt werden. Bitte Host und Port pr\u00fcfen.",
"wrong_password": "Ung\u00fcltiges Passwort",
"wrong_server_type": "F\u00fcr die forked-daapd Integration ist ein forked-daapd Server mit der Version > = 27.0 erforderlich."
},
+ "flow_title": "Forked-Daapd-Server: {name} ({host})",
"step": {
"user": {
"data": {
"host": "Host",
"password": "API-Passwort (leer lassen, wenn kein Passwort vorhanden ist)",
"port": "API Port"
- }
+ },
+ "title": "Forked-Daapd-Ger\u00e4t einrichten"
}
}
},
@@ -24,8 +29,11 @@
"init": {
"data": {
"max_playlists": "Maximale Anzahl der als Quellen verwendeten Wiedergabelisten",
- "tts_pause_time": "Sekunden bis zur Pause vor und nach der TTS"
- }
+ "tts_pause_time": "Sekunden bis zur Pause vor und nach der TTS",
+ "tts_volume": "TTS-Lautst\u00e4rke (Float im Bereich [0,1])"
+ },
+ "description": "Lege verschiedene Optionen f\u00fcr die Forked-Daapd-Integration fest.",
+ "title": "Konfigurieren der Forked-Daapd-Optionen"
}
}
}
diff --git a/homeassistant/components/forked_daapd/translations/hu.json b/homeassistant/components/forked_daapd/translations/hu.json
index a9c13f1ee68c7d..ca90fad3048fd2 100644
--- a/homeassistant/components/forked_daapd/translations/hu.json
+++ b/homeassistant/components/forked_daapd/translations/hu.json
@@ -1,7 +1,18 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
- "forbidden": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a forked-daapd h\u00e1l\u00f3zati enged\u00e9lyeket."
+ "forbidden": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a forked-daapd h\u00e1l\u00f3zati enged\u00e9lyeket.",
+ "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/forked_daapd/translations/id.json b/homeassistant/components/forked_daapd/translations/id.json
new file mode 100644
index 00000000000000..76787e2a19bf3d
--- /dev/null
+++ b/homeassistant/components/forked_daapd/translations/id.json
@@ -0,0 +1,42 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "not_forked_daapd": "Perangkat bukan server forked-daapd."
+ },
+ "error": {
+ "forbidden": "Tidak dapat terhubung. Periksa izin jaringan forked-daapd Anda.",
+ "unknown_error": "Kesalahan yang tidak diharapkan",
+ "websocket_not_enabled": "Websocket server forked-daapd tidak diaktifkan.",
+ "wrong_host_or_port": "Tidak dapat terhubung. Periksa nilai host dan port.",
+ "wrong_password": "Kata sandi salah.",
+ "wrong_server_type": "Integrasi forked-daapd membutuhkan server forked-daapd dengan versi >= 27.0."
+ },
+ "flow_title": "forked-daapd server: {name} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nama alias",
+ "password": "Kata sandi API (kosongkan jika tidak ada kata sandi)",
+ "port": "Port API"
+ },
+ "title": "Siapkan perangkat forked-daapd"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "librespot_java_port": "Port untuk kontrol pipa librespot-java (jika digunakan)",
+ "max_playlists": "Jumlah maksimum daftar putar yang digunakan sebagai sumber",
+ "tts_pause_time": "Tenggang waktu dalam detik sebelum dan setelah TTS",
+ "tts_volume": "Volume TTS (bilangan float dalam rentang [0,1])"
+ },
+ "description": "Tentukan berbagai opsi untuk integrasi forked-daapd.",
+ "title": "Konfigurasikan opsi forked-daapd"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/forked_daapd/translations/ko.json b/homeassistant/components/forked_daapd/translations/ko.json
index 5522eda3a766b0..60b585af958b44 100644
--- a/homeassistant/components/forked_daapd/translations/ko.json
+++ b/homeassistant/components/forked_daapd/translations/ko.json
@@ -5,7 +5,8 @@
"not_forked_daapd": "\uae30\uae30\uac00 forked-daapd \uc11c\ubc84\uac00 \uc544\ub2d9\ub2c8\ub2e4."
},
"error": {
- "unknown_error": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958",
+ "forbidden": "\uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. fork-daapd \ub124\ud2b8\uc6cc\ud06c \uc0ac\uc6a9 \uad8c\ud55c\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694",
+ "unknown_error": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4",
"websocket_not_enabled": "forked-daapd \uc11c\ubc84 \uc6f9\uc18c\ucf13\uc774 \ube44\ud65c\uc131\ud654 \ub418\uc5b4\uc788\uc2b5\ub2c8\ub2e4.",
"wrong_host_or_port": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694.",
"wrong_password": "\ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.",
@@ -30,11 +31,11 @@
"data": {
"librespot_java_port": "librespot-java \ud30c\uc774\ud504 \ucee8\ud2b8\ub864\uc6a9 \ud3ec\ud2b8 (\uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0)",
"max_playlists": "\uc18c\uc2a4\ub85c \uc0ac\uc6a9\ub41c \ucd5c\ub300 \uc7ac\uc0dd \ubaa9\ub85d \uc218",
- "tts_pause_time": "TTS \uc804\ud6c4\uc5d0 \uc77c\uc2dc\uc911\uc9c0\ud560 \uc2dc\uac04(\ucd08)",
+ "tts_pause_time": "TTS \uc804\ud6c4\uc5d0 \uc77c\uc2dc\uc911\uc9c0\ud560 \uc2dc\uac04 (\ucd08)",
"tts_volume": "TTS \ubcfc\ub968 (0~1 \uc758 \uc2e4\uc218\uac12)"
},
"description": "forked-daapd \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \ub2e4\uc591\ud55c \uc635\uc158\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694.",
- "title": "forked-daapd \uc635\uc158 \uc124\uc815\ud558\uae30"
+ "title": "forked-daapd \uc635\uc158 \uad6c\uc131\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/forked_daapd/translations/nl.json b/homeassistant/components/forked_daapd/translations/nl.json
index 5391615fcb1e30..7eec6a34571ed8 100644
--- a/homeassistant/components/forked_daapd/translations/nl.json
+++ b/homeassistant/components/forked_daapd/translations/nl.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Apparaat is al geconfigureerd.",
+ "already_configured": "Apparaat is al geconfigureerd",
"not_forked_daapd": "Apparaat is geen forked-daapd-server."
},
"error": {
- "unknown_error": "Onbekende fout.",
+ "forbidden": "Niet in staat te verbinden. Controleer alstublieft uw forked-daapd netwerkrechten.",
+ "unknown_error": "Onverwachte fout",
"websocket_not_enabled": "forked-daapd server websocket niet ingeschakeld.",
"wrong_host_or_port": "Verbinding mislukt, controleer het host-adres en poort.",
"wrong_password": "Onjuist wachtwoord.",
diff --git a/homeassistant/components/forked_daapd/translations/tr.json b/homeassistant/components/forked_daapd/translations/tr.json
new file mode 100644
index 00000000000000..cf354c5c87f54c
--- /dev/null
+++ b/homeassistant/components/forked_daapd/translations/tr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "unknown_error": "Beklenmeyen hata",
+ "wrong_password": "Yanl\u0131\u015f parola."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "name": "Kolay ad",
+ "password": "API parolas\u0131 (parola yoksa bo\u015f b\u0131rak\u0131n)",
+ "port": "API ba\u011flant\u0131 noktas\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/forked_daapd/translations/uk.json b/homeassistant/components/forked_daapd/translations/uk.json
new file mode 100644
index 00000000000000..19caf9b5bd0db4
--- /dev/null
+++ b/homeassistant/components/forked_daapd/translations/uk.json
@@ -0,0 +1,42 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "not_forked_daapd": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 forked-daapd."
+ },
+ "error": {
+ "forbidden": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0456 \u0434\u043e\u0437\u0432\u043e\u043b\u0438 forked-daapd.",
+ "unknown_error": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430",
+ "websocket_not_enabled": "\u0412\u0435\u0431-\u0441\u043e\u043a\u0435\u0442 forked-daapd \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439.",
+ "wrong_host_or_port": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u0445\u043e\u0441\u0442\u0430.",
+ "wrong_password": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.",
+ "wrong_server_type": "\u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0438\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 forked-daapd \u0432\u0435\u0440\u0441\u0456\u0457 27.0 \u0430\u0431\u043e \u0432\u0438\u0449\u0435."
+ },
+ "flow_title": "\u0421\u0435\u0440\u0432\u0435\u0440 forked-daapd: {name} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c API (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c, \u044f\u043a\u0449\u043e \u0443 \u0432\u0430\u0441 \u043d\u0435\u043c\u0430\u0454 \u043f\u0430\u0440\u043e\u043b\u044f)",
+ "port": "\u041f\u043e\u0440\u0442 API"
+ },
+ "title": "forked-daapd"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "librespot_java_port": "\u041f\u043e\u0440\u0442 \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f \u043a\u0430\u043d\u0430\u043b\u043e\u043c librespot-java (\u044f\u043a\u0449\u043e \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f)",
+ "max_playlists": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u043a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0456\u0432, \u0449\u043e \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0442\u044c\u0441\u044f \u044f\u043a \u0434\u0436\u0435\u0440\u0435\u043b\u0430",
+ "tts_pause_time": "\u0427\u0430\u0441 \u043f\u0430\u0443\u0437\u0438 \u0434\u043e \u0456 \u043f\u0456\u0441\u043b\u044f TTS (\u0441\u0435\u043a.)",
+ "tts_volume": "\u0413\u0443\u0447\u043d\u0456\u0441\u0442\u044c TTS (\u0447\u0438\u0441\u043b\u043e \u0432 \u0434\u0456\u0430\u043f\u0430\u0437\u043e\u043d\u0456 \u0432\u0456\u0434 0 \u0434\u043e 1)"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 forked-daapd.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f forked-daapd"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py
index e5b82817d4bea4..1b3ae5e72163b1 100644
--- a/homeassistant/components/foscam/__init__.py
+++ b/homeassistant/components/foscam/__init__.py
@@ -1,10 +1,15 @@
"""The foscam component."""
import asyncio
+from libpyfoscam import FoscamCamera
+
from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant
+from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.entity_registry import async_migrate_entries
-from .const import DOMAIN, SERVICE_PTZ, SERVICE_PTZ_PRESET
+from .config_flow import DEFAULT_RTSP_PORT
+from .const import CONF_RTSP_PORT, DOMAIN, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRESET
PLATFORMS = ["camera"]
@@ -17,12 +22,12 @@ async def async_setup(hass: HomeAssistant, config: dict):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up foscam from a config entry."""
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
- hass.data[DOMAIN][entry.unique_id] = entry.data
+ hass.data[DOMAIN][entry.entry_id] = entry.data
return True
@@ -32,17 +37,57 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
if unload_ok:
- hass.data[DOMAIN].pop(entry.unique_id)
+ hass.data[DOMAIN].pop(entry.entry_id)
if not hass.data[DOMAIN]:
hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ)
hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ_PRESET)
return unload_ok
+
+
+async def async_migrate_entry(hass, config_entry: ConfigEntry):
+ """Migrate old entry."""
+ LOGGER.debug("Migrating from version %s", config_entry.version)
+
+ if config_entry.version == 1:
+ # Change unique id
+ @callback
+ def update_unique_id(entry):
+ return {"new_unique_id": config_entry.entry_id}
+
+ await async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
+
+ config_entry.unique_id = None
+
+ # Get RTSP port from the camera or use the fallback one and store it in data
+ camera = FoscamCamera(
+ config_entry.data[CONF_HOST],
+ config_entry.data[CONF_PORT],
+ config_entry.data[CONF_USERNAME],
+ config_entry.data[CONF_PASSWORD],
+ verbose=False,
+ )
+
+ ret, response = await hass.async_add_executor_job(camera.get_port_info)
+
+ rtsp_port = DEFAULT_RTSP_PORT
+
+ if ret != 0:
+ rtsp_port = response.get("rtspPort") or response.get("mediaPort")
+
+ config_entry.data = {**config_entry.data, CONF_RTSP_PORT: rtsp_port}
+
+ # Change entry version
+ config_entry.version = 2
+
+ LOGGER.info("Migration to version %s successful", config_entry.version)
+
+ return True
diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py
index f66ad31c2a827d..ea20f0a07fb0ab 100644
--- a/homeassistant/components/foscam/camera.py
+++ b/homeassistant/components/foscam/camera.py
@@ -15,7 +15,14 @@
)
from homeassistant.helpers import config_validation as cv, entity_platform
-from .const import CONF_STREAM, DOMAIN, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRESET
+from .const import (
+ CONF_RTSP_PORT,
+ CONF_STREAM,
+ DOMAIN,
+ LOGGER,
+ SERVICE_PTZ,
+ SERVICE_PTZ_PRESET,
+)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -24,7 +31,7 @@
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_NAME, default="Foscam Camera"): cv.string,
vol.Optional(CONF_PORT, default=88): cv.port,
- vol.Optional("rtsp_port"): cv.port,
+ vol.Optional(CONF_RTSP_PORT): cv.port,
}
)
@@ -61,7 +68,7 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up a Foscam IP Camera."""
LOGGER.warning(
- "Loading foscam via platform config is deprecated, it will be automatically imported. Please remove it afterwards."
+ "Loading foscam via platform config is deprecated, it will be automatically imported; Please remove it afterwards"
)
config_new = {
@@ -71,6 +78,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
CONF_USERNAME: config[CONF_USERNAME],
CONF_PASSWORD: config[CONF_PASSWORD],
CONF_STREAM: "Main",
+ CONF_RTSP_PORT: config.get(CONF_RTSP_PORT, 554),
}
hass.async_create_task(
@@ -134,8 +142,8 @@ def __init__(self, camera, config_entry):
self._username = config_entry.data[CONF_USERNAME]
self._password = config_entry.data[CONF_PASSWORD]
self._stream = config_entry.data[CONF_STREAM]
- self._unique_id = config_entry.unique_id
- self._rtsp_port = None
+ self._unique_id = config_entry.entry_id
+ self._rtsp_port = config_entry.data[CONF_RTSP_PORT]
self._motion_status = False
async def async_added_to_hass(self):
@@ -145,7 +153,13 @@ async def async_added_to_hass(self):
self._foscam_session.get_motion_detect_config
)
- if ret != 0:
+ if ret == -3:
+ LOGGER.info(
+ "Can't get motion detection status, camera %s configured with non-admin user",
+ self._name,
+ )
+
+ elif ret != 0:
LOGGER.error(
"Error getting motion detection status of %s: %s", self._name, ret
)
@@ -153,17 +167,6 @@ async def async_added_to_hass(self):
else:
self._motion_status = response == 1
- # Get RTSP port
- ret, response = await self.hass.async_add_executor_job(
- self._foscam_session.get_port_info
- )
-
- if ret != 0:
- LOGGER.error("Error getting RTSP port of %s: %s", self._name, ret)
-
- else:
- self._rtsp_port = response.get("rtspPort") or response.get("mediaPort")
-
@property
def unique_id(self):
"""Return the entity unique ID."""
@@ -205,6 +208,11 @@ def enable_motion_detection(self):
ret = self._foscam_session.enable_motion_detection()
if ret != 0:
+ if ret == -3:
+ LOGGER.info(
+ "Can't set motion detection status, camera %s configured with non-admin user",
+ self._name,
+ )
return
self._motion_status = True
@@ -220,6 +228,11 @@ def disable_motion_detection(self):
ret = self._foscam_session.disable_motion_detection()
if ret != 0:
+ if ret == -3:
+ LOGGER.info(
+ "Can't set motion detection status, camera %s configured with non-admin user",
+ self._name,
+ )
return
self._motion_status = False
diff --git a/homeassistant/components/foscam/config_flow.py b/homeassistant/components/foscam/config_flow.py
index 7bb8cb50a5109a..5ec36c97fa0768 100644
--- a/homeassistant/components/foscam/config_flow.py
+++ b/homeassistant/components/foscam/config_flow.py
@@ -1,6 +1,10 @@
"""Config flow for foscam integration."""
from libpyfoscam import FoscamCamera
-from libpyfoscam.foscam import ERROR_FOSCAM_AUTH, ERROR_FOSCAM_UNAVAILABLE
+from libpyfoscam.foscam import (
+ ERROR_FOSCAM_AUTH,
+ ERROR_FOSCAM_UNAVAILABLE,
+ FOSCAM_SUCCESS,
+)
import voluptuous as vol
from homeassistant import config_entries, exceptions
@@ -13,12 +17,12 @@
)
from homeassistant.data_entry_flow import AbortFlow
-from .const import CONF_STREAM, LOGGER
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import CONF_RTSP_PORT, CONF_STREAM, DOMAIN, LOGGER
STREAMS = ["Main", "Sub"]
DEFAULT_PORT = 88
+DEFAULT_RTSP_PORT = 554
DATA_SCHEMA = vol.Schema(
@@ -28,6 +32,7 @@
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_STREAM, default=STREAMS[0]): vol.In(STREAMS),
+ vol.Required(CONF_RTSP_PORT, default=DEFAULT_RTSP_PORT): int,
}
)
@@ -35,7 +40,7 @@
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for foscam."""
- VERSION = 1
+ VERSION = 2
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
async def _validate_and_create(self, data):
@@ -43,6 +48,14 @@ async def _validate_and_create(self, data):
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
+
+ for entry in self.hass.config_entries.async_entries(DOMAIN):
+ if (
+ entry.data[CONF_HOST] == data[CONF_HOST]
+ and entry.data[CONF_PORT] == data[CONF_PORT]
+ ):
+ raise AbortFlow("already_configured")
+
camera = FoscamCamera(
data[CONF_HOST],
data[CONF_PORT],
@@ -52,7 +65,7 @@ async def _validate_and_create(self, data):
)
# Validate data by sending a request to the camera
- ret, response = await self.hass.async_add_executor_job(camera.get_dev_info)
+ ret, _ = await self.hass.async_add_executor_job(camera.get_product_all_info)
if ret == ERROR_FOSCAM_UNAVAILABLE:
raise CannotConnect
@@ -60,10 +73,23 @@ async def _validate_and_create(self, data):
if ret == ERROR_FOSCAM_AUTH:
raise InvalidAuth
- await self.async_set_unique_id(response["mac"])
- self._abort_if_unique_id_configured()
+ if ret != FOSCAM_SUCCESS:
+ LOGGER.error(
+ "Unexpected error code from camera %s:%s: %s",
+ data[CONF_HOST],
+ data[CONF_PORT],
+ ret,
+ )
+ raise InvalidResponse
+
+ # Try to get camera name (only possible with admin account)
+ ret, response = await self.hass.async_add_executor_job(camera.get_dev_info)
- name = data.pop(CONF_NAME, response["devName"])
+ dev_name = response.get(
+ "devName", f"Foscam {data[CONF_HOST]}:{data[CONF_PORT]}"
+ )
+
+ name = data.pop(CONF_NAME, dev_name)
return self.async_create_entry(title=name, data=data)
@@ -81,6 +107,9 @@ async def async_step_user(self, user_input=None):
except InvalidAuth:
errors["base"] = "invalid_auth"
+ except InvalidResponse:
+ errors["base"] = "invalid_response"
+
except AbortFlow:
raise
@@ -98,19 +127,25 @@ async def async_step_import(self, import_config):
return await self._validate_and_create(import_config)
except CannotConnect:
- LOGGER.error("Error importing foscam platform config: cannot connect.")
+ LOGGER.error("Error importing foscam platform config: cannot connect")
return self.async_abort(reason="cannot_connect")
except InvalidAuth:
- LOGGER.error("Error importing foscam platform config: invalid auth.")
+ LOGGER.error("Error importing foscam platform config: invalid auth")
return self.async_abort(reason="invalid_auth")
+ except InvalidResponse:
+ LOGGER.exception(
+ "Error importing foscam platform config: invalid response from camera"
+ )
+ return self.async_abort(reason="invalid_response")
+
except AbortFlow:
raise
except Exception: # pylint: disable=broad-except
LOGGER.exception(
- "Error importing foscam platform config: unexpected exception."
+ "Error importing foscam platform config: unexpected exception"
)
return self.async_abort(reason="unknown")
@@ -121,3 +156,7 @@ class CannotConnect(exceptions.HomeAssistantError):
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""
+
+
+class InvalidResponse(exceptions.HomeAssistantError):
+ """Error to indicate there is invalid response."""
diff --git a/homeassistant/components/foscam/const.py b/homeassistant/components/foscam/const.py
index a42b430993e027..d5ac0f5c567d25 100644
--- a/homeassistant/components/foscam/const.py
+++ b/homeassistant/components/foscam/const.py
@@ -5,6 +5,7 @@
DOMAIN = "foscam"
+CONF_RTSP_PORT = "rtsp_port"
CONF_STREAM = "stream"
SERVICE_PTZ = "ptz"
diff --git a/homeassistant/components/foscam/strings.json b/homeassistant/components/foscam/strings.json
index 6033fa099cd99a..5c0622af9d17b2 100644
--- a/homeassistant/components/foscam/strings.json
+++ b/homeassistant/components/foscam/strings.json
@@ -8,6 +8,7 @@
"port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
+ "rtsp_port": "RTSP port",
"stream": "Stream"
}
}
@@ -15,6 +16,7 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "invalid_response": "Invalid response from the device",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
diff --git a/homeassistant/components/foscam/translations/af.json b/homeassistant/components/foscam/translations/af.json
new file mode 100644
index 00000000000000..4a9930dd95d355
--- /dev/null
+++ b/homeassistant/components/foscam/translations/af.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Senha"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/bg.json b/homeassistant/components/foscam/translations/bg.json
new file mode 100644
index 00000000000000..5c41e03c838396
--- /dev/null
+++ b/homeassistant/components/foscam/translations/bg.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u0430",
+ "port": "\u041f\u043e\u0440\u0442",
+ "rtsp_port": "RTSP \u043f\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/foscam/translations/ca.json b/homeassistant/components/foscam/translations/ca.json
new file mode 100644
index 00000000000000..b7f71c8c9229cc
--- /dev/null
+++ b/homeassistant/components/foscam/translations/ca.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "invalid_response": "Resposta del dispositiu inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "password": "Contrasenya",
+ "port": "Port",
+ "rtsp_port": "Port RTSP",
+ "stream": "Flux de v\u00eddeo",
+ "username": "Nom d'usuari"
+ }
+ }
+ }
+ },
+ "title": "Foscam"
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/cs.json b/homeassistant/components/foscam/translations/cs.json
new file mode 100644
index 00000000000000..b6f3c40abf6897
--- /dev/null
+++ b/homeassistant/components/foscam/translations/cs.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno"
+ },
+ "error": {
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
+ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
+ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hostitel",
+ "password": "Heslo",
+ "port": "Port",
+ "username": "U\u017eivatelsk\u00e9 jm\u00e9no"
+ }
+ }
+ }
+ },
+ "title": "Foscam"
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/de.json b/homeassistant/components/foscam/translations/de.json
new file mode 100644
index 00000000000000..d87044b579a80a
--- /dev/null
+++ b/homeassistant/components/foscam/translations/de.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "invalid_response": "Ung\u00fcltige Antwort vom Ger\u00e4t",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Passwort",
+ "port": "Port",
+ "rtsp_port": "RTSP-Port",
+ "username": "Benutzername"
+ }
+ }
+ }
+ },
+ "title": "Foscam"
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/en.json b/homeassistant/components/foscam/translations/en.json
index 521a22076dd3e5..16a7d0b78001a5 100644
--- a/homeassistant/components/foscam/translations/en.json
+++ b/homeassistant/components/foscam/translations/en.json
@@ -1,24 +1,26 @@
{
- "config": {
- "abort": {
- "already_configured": "Device is already configured"
- },
- "error": {
- "cannot_connect": "Failed to connect",
- "invalid_auth": "Invalid authentication",
- "unknown": "Unexpected error"
- },
- "step": {
- "user": {
- "data": {
- "host": "Host",
- "password": "Password",
- "port": "Port",
- "stream": "Stream",
- "username": "Username"
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
+ "invalid_response": "Invalid response from the device",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Password",
+ "port": "Port",
+ "rtsp_port": "RTSP port",
+ "stream": "Stream",
+ "username": "Username"
+ }
+ }
}
- }
- }
- },
- "title": "Foscam"
-}
+ },
+ "title": "Foscam"
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/es.json b/homeassistant/components/foscam/translations/es.json
new file mode 100644
index 00000000000000..7e8b7c1427dc71
--- /dev/null
+++ b/homeassistant/components/foscam/translations/es.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositivo ya est\u00e1 configurado"
+ },
+ "error": {
+ "cannot_connect": "No se pudo conectar",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
+ "invalid_response": "Respuesta no v\u00e1lida del dispositivo",
+ "unknown": "Error inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Contrase\u00f1a",
+ "port": "Puerto",
+ "rtsp_port": "Puerto RTSP",
+ "stream": "Stream",
+ "username": "Usuario"
+ }
+ }
+ }
+ },
+ "title": "Foscam"
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/et.json b/homeassistant/components/foscam/translations/et.json
new file mode 100644
index 00000000000000..c21ffa0cdd1f53
--- /dev/null
+++ b/homeassistant/components/foscam/translations/et.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_auth": "Vigane autentimine",
+ "invalid_response": "Seadme vastus on vigane",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Salas\u00f5na",
+ "port": "Port",
+ "rtsp_port": "RTSP port",
+ "stream": "Voog",
+ "username": "Kasutajanimi"
+ }
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/fr.json b/homeassistant/components/foscam/translations/fr.json
new file mode 100644
index 00000000000000..1424c22ad6121f
--- /dev/null
+++ b/homeassistant/components/foscam/translations/fr.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Le p\u00e9riph\u00e9rique est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "Echec de connection",
+ "invalid_auth": "Authentification invalide",
+ "invalid_response": "R\u00e9ponse invalide de l\u2019appareil",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "H\u00f4te",
+ "password": "Mot de passe",
+ "port": "Port",
+ "rtsp_port": "Port RTSP",
+ "stream": "Flux",
+ "username": "Nom d'utilisateur"
+ }
+ }
+ }
+ },
+ "title": "Foscam"
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/hu.json b/homeassistant/components/foscam/translations/hu.json
new file mode 100644
index 00000000000000..63ea95210ffa7d
--- /dev/null
+++ b/homeassistant/components/foscam/translations/hu.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "invalid_response": "\u00c9rv\u00e9nytelen v\u00e1lasz az eszk\u00f6zt\u0151l",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "password": "Jelsz\u00f3",
+ "port": "Port",
+ "rtsp_port": "RTSP port",
+ "stream": "Stream",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ },
+ "title": "Foscam"
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/id.json b/homeassistant/components/foscam/translations/id.json
new file mode 100644
index 00000000000000..21a7682b92c311
--- /dev/null
+++ b/homeassistant/components/foscam/translations/id.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "invalid_response": "Response tidak valid dari perangkat",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "rtsp_port": "Port RTSP",
+ "stream": "Stream",
+ "username": "Nama Pengguna"
+ }
+ }
+ }
+ },
+ "title": "Foscam"
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/it.json b/homeassistant/components/foscam/translations/it.json
new file mode 100644
index 00000000000000..63868a0f07fd63
--- /dev/null
+++ b/homeassistant/components/foscam/translations/it.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
+ "invalid_response": "Risposta non valida dal dispositivo",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Password",
+ "port": "Porta",
+ "rtsp_port": "Porta RTSP",
+ "stream": "Flusso",
+ "username": "Nome utente"
+ }
+ }
+ }
+ },
+ "title": "Foscam"
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/ko.json b/homeassistant/components/foscam/translations/ko.json
new file mode 100644
index 00000000000000..ba743f6b27a565
--- /dev/null
+++ b/homeassistant/components/foscam/translations/ko.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_response": "\uae30\uae30\uc5d0\uc11c \uc798\ubabb\ub41c \uc751\ub2f5\uc744 \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "port": "\ud3ec\ud2b8",
+ "rtsp_port": "RTSP \ud3ec\ud2b8",
+ "stream": "\uc2a4\ud2b8\ub9bc",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ }
+ }
+ }
+ },
+ "title": "Foscam"
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/lb.json b/homeassistant/components/foscam/translations/lb.json
new file mode 100644
index 00000000000000..123b3f4be76d2f
--- /dev/null
+++ b/homeassistant/components/foscam/translations/lb.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Passwuert",
+ "port": "Port",
+ "stream": "Stream",
+ "username": "Benotzernumm"
+ }
+ }
+ }
+ },
+ "title": "Foscam"
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/nl.json b/homeassistant/components/foscam/translations/nl.json
new file mode 100644
index 00000000000000..9bea23ad702c60
--- /dev/null
+++ b/homeassistant/components/foscam/translations/nl.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
+ "invalid_response": "Ongeldig antwoord van het apparaat",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Wachtwoord",
+ "port": "Poort",
+ "rtsp_port": "RTSP-poort",
+ "stream": "Stream",
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ },
+ "title": "Foscam"
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/no.json b/homeassistant/components/foscam/translations/no.json
new file mode 100644
index 00000000000000..0184213de27bf1
--- /dev/null
+++ b/homeassistant/components/foscam/translations/no.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_auth": "Ugyldig godkjenning",
+ "invalid_response": "Ugyldig respons fra enheten",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Vert",
+ "password": "Passord",
+ "port": "Port",
+ "rtsp_port": "RTSP-port",
+ "stream": "Str\u00f8m",
+ "username": "Brukernavn"
+ }
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/pl.json b/homeassistant/components/foscam/translations/pl.json
new file mode 100644
index 00000000000000..d7494e22063b3a
--- /dev/null
+++ b/homeassistant/components/foscam/translations/pl.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "invalid_response": "Nieprawid\u0142owa odpowied\u017a z urz\u0105dzenia",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP",
+ "password": "Has\u0142o",
+ "port": "Port",
+ "rtsp_port": "Port RTSP",
+ "stream": "Strumie\u0144",
+ "username": "Nazwa u\u017cytkownika"
+ }
+ }
+ }
+ },
+ "title": "Foscam"
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/pt.json b/homeassistant/components/foscam/translations/pt.json
new file mode 100644
index 00000000000000..b8a454fbaba9a4
--- /dev/null
+++ b/homeassistant/components/foscam/translations/pt.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Palavra-passe"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/ru.json b/homeassistant/components/foscam/translations/ru.json
new file mode 100644
index 00000000000000..8e8404c501e19e
--- /dev/null
+++ b/homeassistant/components/foscam/translations/ru.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "invalid_response": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043e\u0442\u0432\u0435\u0442 \u043e\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "rtsp_port": "\u041f\u043e\u0440\u0442 RTSP",
+ "stream": "\u041f\u043e\u0442\u043e\u043a",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ }
+ }
+ }
+ },
+ "title": "Foscam"
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/tr.json b/homeassistant/components/foscam/translations/tr.json
new file mode 100644
index 00000000000000..b3e964ae08eda2
--- /dev/null
+++ b/homeassistant/components/foscam/translations/tr.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen Hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "password": "\u015eifre",
+ "port": "Port",
+ "stream": "Ak\u0131\u015f",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ },
+ "title": "Foscam"
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/zh-Hant.json b/homeassistant/components/foscam/translations/zh-Hant.json
new file mode 100644
index 00000000000000..a0920c935487e7
--- /dev/null
+++ b/homeassistant/components/foscam/translations/zh-Hant.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "invalid_response": "\u4f86\u81ea\u88dd\u7f6e\u56de\u61c9\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "password": "\u5bc6\u78bc",
+ "port": "\u901a\u8a0a\u57e0",
+ "rtsp_port": "RTSP \u57e0",
+ "stream": "\u4e32\u6d41",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ }
+ }
+ }
+ },
+ "title": "Foscam"
+}
\ No newline at end of file
diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py
index 9120c7d0866824..c6c98e6c2dfb10 100644
--- a/homeassistant/components/freebox/__init__.py
+++ b/homeassistant/components/freebox/__init__.py
@@ -4,13 +4,12 @@
import voluptuous as vol
-from homeassistant.components.discovery import SERVICE_FREEBOX
-from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, ConfigEntry
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP
-from homeassistant.helpers import config_validation as cv, discovery
+from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType
-from .const import DOMAIN, PLATFORMS
+from .const import DOMAIN, PLATFORMS, SERVICE_REBOOT
from .router import FreeboxRouter
_LOGGER = logging.getLogger(__name__)
@@ -26,41 +25,20 @@
async def async_setup(hass, config):
- """Set up the Freebox component."""
- conf = config.get(DOMAIN)
-
- async def discovery_dispatch(service, discovery_info):
- if conf is None:
- host = discovery_info.get("properties", {}).get("api_domain")
- port = discovery_info.get("properties", {}).get("https_port")
- _LOGGER.info("Discovered Freebox server: %s:%s", host, port)
+ """Set up the Freebox integration."""
+ if DOMAIN in config:
+ for entry_config in config[DOMAIN]:
hass.async_create_task(
hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": SOURCE_DISCOVERY},
- data={CONF_HOST: host, CONF_PORT: port},
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config
)
)
- discovery.async_listen(hass, SERVICE_FREEBOX, discovery_dispatch)
-
- if conf is None:
- return True
-
- for freebox_conf in conf:
- hass.async_create_task(
- hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": SOURCE_IMPORT},
- data=freebox_conf,
- )
- )
-
return True
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
- """Set up Freebox component."""
+ """Set up Freebox entry."""
router = FreeboxRouter(hass, entry)
await router.setup()
@@ -77,7 +55,7 @@ async def async_reboot(call):
"""Handle reboot service call."""
await router.reboot()
- hass.services.async_register(DOMAIN, "reboot", async_reboot)
+ hass.services.async_register(DOMAIN, SERVICE_REBOOT, async_reboot)
async def async_close_connection(event):
"""Close Freebox connection on HA Stop."""
@@ -101,5 +79,6 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
if unload_ok:
router = hass.data[DOMAIN].pop(entry.unique_id)
await router.close()
+ hass.services.async_remove(DOMAIN, SERVICE_REBOOT)
return unload_ok
diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py
index d776c34c4f9bdf..f2e115ddf9b94c 100644
--- a/homeassistant/components/freebox/config_flow.py
+++ b/homeassistant/components/freebox/config_flow.py
@@ -1,13 +1,13 @@
"""Config flow to configure the Freebox integration."""
import logging
-from aiofreepybox.exceptions import AuthorizationError, HttpRequestError
+from freebox_api.exceptions import AuthorizationError, HttpRequestError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PORT
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import DOMAIN
from .router import get_api
_LOGGER = logging.getLogger(__name__)
@@ -106,6 +106,8 @@ async def async_step_import(self, user_input=None):
"""Import a config entry."""
return await self.async_step_user(user_input)
- async def async_step_discovery(self, discovery_info):
- """Initialize step from discovery."""
- return await self.async_step_user(discovery_info)
+ async def async_step_zeroconf(self, discovery_info: dict):
+ """Initialize flow from zeroconf."""
+ host = discovery_info["properties"]["api_domain"]
+ port = discovery_info["properties"]["https_port"]
+ return await self.async_step_user({CONF_HOST: host, CONF_PORT: port})
diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py
index d0ac63fa9bb957..df251dcf954790 100644
--- a/homeassistant/components/freebox/const.py
+++ b/homeassistant/components/freebox/const.py
@@ -4,10 +4,12 @@
from homeassistant.const import (
DATA_RATE_KILOBYTES_PER_SECOND,
DEVICE_CLASS_TEMPERATURE,
+ PERCENTAGE,
TEMP_CELSIUS,
)
DOMAIN = "freebox"
+SERVICE_REBOOT = "reboot"
APP_DESC = {
"app_id": "hass",
@@ -55,6 +57,15 @@
},
}
+DISK_PARTITION_SENSORS = {
+ "partition_free_space": {
+ SENSOR_NAME: "free space",
+ SENSOR_UNIT: PERCENTAGE,
+ SENSOR_ICON: "mdi:harddisk",
+ SENSOR_DEVICE_CLASS: None,
+ },
+}
+
TEMPERATURE_SENSOR_TEMPLATE = {
SENSOR_NAME: None,
SENSOR_UNIT: TEMP_CELSIUS,
diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py
index 10c5b8eb2c51b7..6510a29bbfc77e 100644
--- a/homeassistant/components/freebox/device_tracker.py
+++ b/homeassistant/components/freebox/device_tracker.py
@@ -1,6 +1,7 @@
"""Support for Freebox devices (Freebox v6 and Freebox mini 4K)."""
+from __future__ import annotations
+
from datetime import datetime
-from typing import Dict
from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER
from homeassistant.components.device_tracker.config_entry import ScannerEntity
@@ -52,7 +53,7 @@ def add_entities(router, async_add_entities, tracked):
class FreeboxDevice(ScannerEntity):
"""Representation of a Freebox device."""
- def __init__(self, router: FreeboxRouter, device: Dict[str, any]) -> None:
+ def __init__(self, router: FreeboxRouter, device: dict[str, any]) -> None:
"""Initialize a Freebox device."""
self._router = router
self._name = device["primary_name"].strip() or DEFAULT_DEVICE_NAME
@@ -105,12 +106,12 @@ def icon(self) -> str:
return self._icon
@property
- def device_state_attributes(self) -> Dict[str, any]:
+ def extra_state_attributes(self) -> dict[str, any]:
"""Return the attributes."""
return self._attrs
@property
- def device_info(self) -> Dict[str, any]:
+ def device_info(self) -> dict[str, any]:
"""Return the device information."""
return {
"connections": {(CONNECTION_NETWORK_MAC, self._mac)},
diff --git a/homeassistant/components/freebox/manifest.json b/homeassistant/components/freebox/manifest.json
index ae96f7f6510c5c..2d55553511bd00 100644
--- a/homeassistant/components/freebox/manifest.json
+++ b/homeassistant/components/freebox/manifest.json
@@ -3,7 +3,7 @@
"name": "Freebox",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/freebox",
- "requirements": ["aiofreepybox==0.0.8"],
- "after_dependencies": ["discovery"],
- "codeowners": ["@snoof85", "@Quentame"]
+ "requirements": ["freebox-api==0.0.10"],
+ "zeroconf": ["_fbx-api._tcp.local."],
+ "codeowners": ["@hacf-fr", "@Quentame"]
}
diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py
index daa57a89c47cdc..fbeca869d1dd0c 100644
--- a/homeassistant/components/freebox/router.py
+++ b/homeassistant/components/freebox/router.py
@@ -1,12 +1,15 @@
"""Represent the Freebox router and its devices and sensors."""
+from __future__ import annotations
+
from datetime import datetime, timedelta
import logging
+import os
from pathlib import Path
-from typing import Any, Dict, List, Optional
+from typing import Any
-from aiofreepybox import Freepybox
-from aiofreepybox.api.wifi import Wifi
-from aiofreepybox.exceptions import HttpRequestError
+from freebox_api import Freepybox
+from freebox_api.api.wifi import Wifi
+from freebox_api.exceptions import HttpRequestError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT
@@ -31,6 +34,18 @@
SCAN_INTERVAL = timedelta(seconds=30)
+async def get_api(hass: HomeAssistantType, host: str) -> Freepybox:
+ """Get the Freebox API."""
+ freebox_path = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY).path
+
+ if not os.path.exists(freebox_path):
+ await hass.async_add_executor_job(os.makedirs, freebox_path)
+
+ token_file = Path(f"{freebox_path}/{slugify(host)}.conf")
+
+ return Freepybox(APP_DESC, token_file, API_VERSION)
+
+
class FreeboxRouter:
"""Representation of a Freebox router."""
@@ -42,15 +57,16 @@ def __init__(self, hass: HomeAssistantType, entry: ConfigEntry) -> None:
self._port = entry.data[CONF_PORT]
self._api: Freepybox = None
- self._name = None
+ self.name = None
self.mac = None
self._sw_v = None
self._attrs = {}
- self.devices: Dict[str, Any] = {}
- self.sensors_temperature: Dict[str, int] = {}
- self.sensors_connection: Dict[str, float] = {}
- self.call_list: List[Dict[str, Any]] = []
+ self.devices: dict[str, dict[str, Any]] = {}
+ self.disks: dict[int, dict[str, Any]] = {}
+ self.sensors_temperature: dict[str, int] = {}
+ self.sensors_connection: dict[str, float] = {}
+ self.call_list: list[dict[str, Any]] = []
self._unsub_dispatcher = None
self.listeners = []
@@ -68,7 +84,7 @@ async def setup(self) -> None:
# System
fbx_config = await self._api.system.get_config()
self.mac = fbx_config["mac"]
- self._name = fbx_config["model_info"]["pretty_name"]
+ self.name = fbx_config["model_info"]["pretty_name"]
self._sw_v = fbx_config["firmware_version"]
# Devices & sensors
@@ -77,20 +93,20 @@ async def setup(self) -> None:
self.hass, self.update_all, SCAN_INTERVAL
)
- async def update_all(self, now: Optional[datetime] = None) -> None:
+ async def update_all(self, now: datetime | None = None) -> None:
"""Update all Freebox platforms."""
+ await self.update_device_trackers()
await self.update_sensors()
- await self.update_devices()
- async def update_devices(self) -> None:
+ async def update_device_trackers(self) -> None:
"""Update Freebox devices."""
new_device = False
- fbx_devices: Dict[str, Any] = await self._api.lan.get_hosts_list()
+ fbx_devices: [dict[str, Any]] = await self._api.lan.get_hosts_list()
# Adds the Freebox itself
fbx_devices.append(
{
- "primary_name": self._name,
+ "primary_name": self.name,
"l2ident": {"id": self.mac},
"vendor_name": "Freebox SAS",
"host_type": "router",
@@ -115,7 +131,7 @@ async def update_devices(self) -> None:
async def update_sensors(self) -> None:
"""Update Freebox sensors."""
# System sensors
- syst_datas: Dict[str, Any] = await self._api.system.get_config()
+ syst_datas: dict[str, Any] = await self._api.system.get_config()
# According to the doc `syst_datas["sensors"]` is temperature sensors in celsius degree.
# Name and id of sensors may vary under Freebox devices.
@@ -123,7 +139,7 @@ async def update_sensors(self) -> None:
self.sensors_temperature[sensor["name"]] = sensor["value"]
# Connection sensors
- connection_datas: Dict[str, Any] = await self._api.connection.get_status()
+ connection_datas: dict[str, Any] = await self._api.connection.get_status()
for sensor_key in CONNECTION_SENSORS:
self.sensors_connection[sensor_key] = connection_datas[sensor_key]
@@ -138,10 +154,20 @@ async def update_sensors(self) -> None:
"serial": syst_datas["serial"],
}
- self.call_list = await self._api.call.get_call_list()
+ self.call_list = await self._api.call.get_calls_log()
+
+ await self._update_disks_sensors()
async_dispatcher_send(self.hass, self.signal_sensor_update)
+ async def _update_disks_sensors(self) -> None:
+ """Update Freebox disks."""
+ # None at first request
+ fbx_disks: [dict[str, Any]] = await self._api.storage.get_disks() or []
+
+ for fbx_disk in fbx_disks:
+ self.disks[fbx_disk["id"]] = fbx_disk
+
async def reboot(self) -> None:
"""Reboot the Freebox."""
await self._api.system.reboot()
@@ -154,12 +180,12 @@ async def close(self) -> None:
self._api = None
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return the device information."""
return {
"connections": {(CONNECTION_NETWORK_MAC, self.mac)},
"identifiers": {(DOMAIN, self.mac)},
- "name": self._name,
+ "name": self.name,
"manufacturer": "Freebox SAS",
"sw_version": self._sw_v,
}
@@ -180,7 +206,7 @@ def signal_sensor_update(self) -> str:
return f"{DOMAIN}-{self._host}-sensor-update"
@property
- def sensors(self) -> Dict[str, Any]:
+ def sensors(self) -> dict[str, Any]:
"""Return sensors."""
return {**self.sensors_temperature, **self.sensors_connection}
@@ -188,13 +214,3 @@ def sensors(self) -> Dict[str, Any]:
def wifi(self) -> Wifi:
"""Return the wifi."""
return self._api.wifi
-
-
-async def get_api(hass: HomeAssistantType, host: str) -> Freepybox:
- """Get the Freebox API."""
- freebox_path = Path(hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY).path)
- freebox_path.mkdir(exist_ok=True)
-
- token_file = Path(f"{freebox_path}/{slugify(host)}.conf")
-
- return Freepybox(APP_DESC, token_file, API_VERSION)
diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py
index aeeaba438ffc14..fd0685f76676f7 100644
--- a/homeassistant/components/freebox/sensor.py
+++ b/homeassistant/components/freebox/sensor.py
@@ -1,17 +1,20 @@
"""Support for Freebox devices (Freebox v6 and Freebox mini 4K)."""
-from typing import Dict
+from __future__ import annotations
+import logging
+
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import DATA_RATE_KILOBYTES_PER_SECOND
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
import homeassistant.util.dt as dt_util
from .const import (
CALL_SENSORS,
CONNECTION_SENSORS,
+ DISK_PARTITION_SENSORS,
DOMAIN,
SENSOR_DEVICE_CLASS,
SENSOR_ICON,
@@ -21,6 +24,8 @@
)
from .router import FreeboxRouter
+_LOGGER = logging.getLogger(__name__)
+
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
@@ -29,6 +34,12 @@ async def async_setup_entry(
router = hass.data[DOMAIN][entry.unique_id]
entities = []
+ _LOGGER.debug(
+ "%s - %s - %s temperature sensors",
+ router.name,
+ router.mac,
+ len(router.sensors_temperature),
+ )
for sensor_name in router.sensors_temperature:
entities.append(
FreeboxSensor(
@@ -46,14 +57,28 @@ async def async_setup_entry(
for sensor_key in CALL_SENSORS:
entities.append(FreeboxCallSensor(router, sensor_key, CALL_SENSORS[sensor_key]))
+ _LOGGER.debug("%s - %s - %s disk(s)", router.name, router.mac, len(router.disks))
+ for disk in router.disks.values():
+ for partition in disk["partitions"]:
+ for sensor_key in DISK_PARTITION_SENSORS:
+ entities.append(
+ FreeboxDiskSensor(
+ router,
+ disk,
+ partition,
+ sensor_key,
+ DISK_PARTITION_SENSORS[sensor_key],
+ )
+ )
+
async_add_entities(entities, True)
-class FreeboxSensor(Entity):
+class FreeboxSensor(SensorEntity):
"""Representation of a Freebox sensor."""
def __init__(
- self, router: FreeboxRouter, sensor_type: str, sensor: Dict[str, any]
+ self, router: FreeboxRouter, sensor_type: str, sensor: dict[str, any]
) -> None:
"""Initialize a Freebox sensor."""
self._state = None
@@ -105,7 +130,7 @@ def device_class(self) -> str:
return self._device_class
@property
- def device_info(self) -> Dict[str, any]:
+ def device_info(self) -> dict[str, any]:
"""Return the device information."""
return self._router.device_info
@@ -136,11 +161,11 @@ class FreeboxCallSensor(FreeboxSensor):
"""Representation of a Freebox call sensor."""
def __init__(
- self, router: FreeboxRouter, sensor_type: str, sensor: Dict[str, any]
+ self, router: FreeboxRouter, sensor_type: str, sensor: dict[str, any]
) -> None:
"""Initialize a Freebox call sensor."""
- self._call_list_for_type = []
super().__init__(router, sensor_type, sensor)
+ self._call_list_for_type = []
@callback
def async_update_state(self) -> None:
@@ -156,9 +181,49 @@ def async_update_state(self) -> None:
self._state = len(self._call_list_for_type)
@property
- def device_state_attributes(self) -> Dict[str, any]:
+ def extra_state_attributes(self) -> dict[str, any]:
"""Return device specific state attributes."""
return {
dt_util.utc_from_timestamp(call["datetime"]).isoformat(): call["name"]
for call in self._call_list_for_type
}
+
+
+class FreeboxDiskSensor(FreeboxSensor):
+ """Representation of a Freebox disk sensor."""
+
+ def __init__(
+ self,
+ router: FreeboxRouter,
+ disk: dict[str, any],
+ partition: dict[str, any],
+ sensor_type: str,
+ sensor: dict[str, any],
+ ) -> None:
+ """Initialize a Freebox disk sensor."""
+ super().__init__(router, sensor_type, sensor)
+ self._disk = disk
+ self._partition = partition
+ self._name = f"{partition['label']} {sensor[SENSOR_NAME]}"
+ self._unique_id = f"{self._router.mac} {sensor_type} {self._disk['id']} {self._partition['id']}"
+
+ @property
+ def device_info(self) -> dict[str, any]:
+ """Return the device information."""
+ return {
+ "identifiers": {(DOMAIN, self._disk["id"])},
+ "name": f"Disk {self._disk['id']}",
+ "model": self._disk["model"],
+ "sw_version": self._disk["firmware"],
+ "via_device": (
+ DOMAIN,
+ self._router.mac,
+ ),
+ }
+
+ @callback
+ def async_update_state(self) -> None:
+ """Update the Freebox disk sensor."""
+ self._state = round(
+ self._partition["free_bytes"] * 100 / self._partition["total_bytes"], 2
+ )
diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py
index 00f87e21f47a37..a15a86f46d879b 100644
--- a/homeassistant/components/freebox/switch.py
+++ b/homeassistant/components/freebox/switch.py
@@ -1,8 +1,9 @@
"""Support for Freebox Delta, Revolution and Mini 4K."""
+from __future__ import annotations
+
import logging
-from typing import Dict
-from aiofreepybox.exceptions import InsufficientPermissionsError
+from freebox_api.exceptions import InsufficientPermissionsError
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
@@ -48,7 +49,7 @@ def is_on(self) -> bool:
return self._state
@property
- def device_info(self) -> Dict[str, any]:
+ def device_info(self) -> dict[str, any]:
"""Return the device information."""
return self._router.device_info
diff --git a/homeassistant/components/freebox/translations/de.json b/homeassistant/components/freebox/translations/de.json
index c21e3c6b67fe70..738b9d48f3cdc2 100644
--- a/homeassistant/components/freebox/translations/de.json
+++ b/homeassistant/components/freebox/translations/de.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Host bereits konfiguriert"
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"register_failed": "Registrieren fehlgeschlagen, bitte versuche es erneut",
- "unknown": "Unbekannter Fehler: Bitte versuchen Sie es sp\u00e4ter erneut"
+ "unknown": "Unerwarteter Fehler"
},
"step": {
"link": {
diff --git a/homeassistant/components/freebox/translations/hu.json b/homeassistant/components/freebox/translations/hu.json
index d13f5fa17c8c1d..1f0b848d3b6ecb 100644
--- a/homeassistant/components/freebox/translations/hu.json
+++ b/homeassistant/components/freebox/translations/hu.json
@@ -1,7 +1,12 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
- "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, k\u00e9rem pr\u00f3b\u00e1lja \u00fajra"
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, k\u00e9rem pr\u00f3b\u00e1lja \u00fajra",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"step": {
"user": {
diff --git a/homeassistant/components/freebox/translations/id.json b/homeassistant/components/freebox/translations/id.json
new file mode 100644
index 00000000000000..b03ec248edbdc6
--- /dev/null
+++ b/homeassistant/components/freebox/translations/id.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "register_failed": "Gagal mendaftar, coba lagi.",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "link": {
+ "description": "Klik \"Kirim\", lalu sentuh panah kanan di router untuk mendaftarkan Freebox dengan Home Assistant. \n\n",
+ "title": "Tautkan router Freebox"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ },
+ "title": "Freebox"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/freebox/translations/ko.json b/homeassistant/components/freebox/translations/ko.json
index 986f345b3ecc29..c3bb5a9bd401f1 100644
--- a/homeassistant/components/freebox/translations/ko.json
+++ b/homeassistant/components/freebox/translations/ko.json
@@ -1,16 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"register_failed": "\ub4f1\ub85d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
- "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694"
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"link": {
- "description": "\ud655\uc778\uc744 \ud074\ub9ad\ud55c \ub2e4\uc74c \ub77c\uc6b0\ud130\uc758 \uc624\ub978\ucabd \ud654\uc0b4\ud45c\ub97c \ud130\uce58\ud558\uc5ec Home Assistant \uc5d0 Freebox \ub97c \ub4f1\ub85d\ud574\uc8fc\uc138\uc694.\n\n",
+ "description": "\ud655\uc778\uc744 \ud074\ub9ad\ud55c \ub2e4\uc74c \ub77c\uc6b0\ud130\uc758 \uc624\ub978\ucabd \ud654\uc0b4\ud45c\ub97c \ud130\uce58\ud558\uc5ec Home Assistant\uc5d0 Freebox\ub97c \ub4f1\ub85d\ud574\uc8fc\uc138\uc694.\n\n",
"title": "Freebox \ub77c\uc6b0\ud130 \uc5f0\uacb0\ud558\uae30"
},
"user": {
diff --git a/homeassistant/components/freebox/translations/nl.json b/homeassistant/components/freebox/translations/nl.json
index ea41fcfcd6a8e9..7fbd57dd6ff69c 100644
--- a/homeassistant/components/freebox/translations/nl.json
+++ b/homeassistant/components/freebox/translations/nl.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Host is al geconfigureerd."
+ "already_configured": "Apparaat is al geconfigureerd"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Kan geen verbinding maken",
"register_failed": "Registratie is mislukt, probeer het opnieuw",
- "unknown": "Onbekende fout: probeer het later nog eens"
+ "unknown": "Onverwachte fout"
},
"step": {
"link": {
diff --git a/homeassistant/components/freebox/translations/tr.json b/homeassistant/components/freebox/translations/tr.json
new file mode 100644
index 00000000000000..b675d38057dc64
--- /dev/null
+++ b/homeassistant/components/freebox/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ },
+ "title": "Freebox"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/freebox/translations/uk.json b/homeassistant/components/freebox/translations/uk.json
new file mode 100644
index 00000000000000..8676c9164a1902
--- /dev/null
+++ b/homeassistant/components/freebox/translations/uk.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "register_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "link": {
+ "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c '\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438', \u043f\u043e\u0442\u0456\u043c \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u0437\u0456 \u0441\u0442\u0440\u0456\u043b\u043a\u043e\u044e \u043d\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0456, \u0449\u043e\u0431 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438 Freebox \u0432 Home Assistant. \n\n![\u0420\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0440\u043e\u0443\u0442\u0435\u0440\u0456] (/ static / images / config_freebox.png)",
+ "title": "Freebox"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "title": "Freebox"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py
index 3c01657da4e25b..34f56ddc6f9f78 100644
--- a/homeassistant/components/fritzbox/__init__.py
+++ b/homeassistant/components/fritzbox/__init__.py
@@ -93,9 +93,9 @@ async def async_setup_entry(hass, entry):
hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}, CONF_DEVICES: set()})
hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = fritz
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
def logout_fritzbox(event):
@@ -115,8 +115,8 @@ async def async_unload_entry(hass, entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py
index 4abe82776a9cca..50f56f3d510f9f 100644
--- a/homeassistant/components/fritzbox/climate.py
+++ b/homeassistant/components/fritzbox/climate.py
@@ -191,7 +191,7 @@ def max_temp(self):
return MAX_TEMPERATURE
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
attrs = {
ATTR_STATE_BATTERY_LOW: self._device.battery_low,
diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py
index f54211aa8a2a09..a462f885484ea7 100644
--- a/homeassistant/components/fritzbox/config_flow.py
+++ b/homeassistant/components/fritzbox/config_flow.py
@@ -13,7 +13,6 @@
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
-# pylint:disable=unused-import
from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN
DATA_SCHEMA_USER = vol.Schema(
@@ -43,8 +42,6 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
-
def __init__(self):
"""Initialize flow."""
self._entry = None
diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py
index 85238d80f27695..4d9c1693c1f60d 100644
--- a/homeassistant/components/fritzbox/sensor.py
+++ b/homeassistant/components/fritzbox/sensor.py
@@ -1,8 +1,8 @@
"""Support for AVM Fritz!Box smarthome temperature sensor only devices."""
import requests
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_DEVICES, TEMP_CELSIUS
-from homeassistant.helpers.entity import Entity
from .const import (
ATTR_STATE_DEVICE_LOCKED,
@@ -32,7 +32,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities)
-class FritzBoxTempSensor(Entity):
+class FritzBoxTempSensor(SensorEntity):
"""The entity class for Fritzbox temperature sensors."""
def __init__(self, device, fritz):
@@ -80,7 +80,7 @@ def update(self):
self._fritz.login()
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
attrs = {
ATTR_STATE_DEVICE_LOCKED: self._device.device_lock,
diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py
index b179464182ff22..50c60f7bb39266 100644
--- a/homeassistant/components/fritzbox/switch.py
+++ b/homeassistant/components/fritzbox/switch.py
@@ -93,7 +93,7 @@ def update(self):
self._fritz.login()
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
attrs = {}
attrs[ATTR_STATE_DEVICE_LOCKED] = self._device.device_lock
diff --git a/homeassistant/components/fritzbox/translations/bg.json b/homeassistant/components/fritzbox/translations/bg.json
new file mode 100644
index 00000000000000..ec678d2d76c0e5
--- /dev/null
+++ b/homeassistant/components/fritzbox/translations/bg.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "reauth_confirm": {
+ "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/fritzbox/translations/ca.json b/homeassistant/components/fritzbox/translations/ca.json
index 8b0122dbe183b6..f8550b5bc3216c 100644
--- a/homeassistant/components/fritzbox/translations/ca.json
+++ b/homeassistant/components/fritzbox/translations/ca.json
@@ -4,7 +4,8 @@
"already_configured": "El dispositiu ja est\u00e0 configurat",
"already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
"no_devices_found": "No s'han trobat dispositius a la xarxa",
- "not_supported": "Connectat a AVM FRITZ!Box per\u00f2 no es poden controlar dispositius Smart Home."
+ "not_supported": "Connectat a AVM FRITZ!Box per\u00f2 no es poden controlar dispositius Smart Home.",
+ "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament"
},
"error": {
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida"
@@ -18,6 +19,13 @@
},
"description": "Vols configurar {name}?"
},
+ "reauth_confirm": {
+ "data": {
+ "password": "Contrasenya",
+ "username": "Nom d'usuari"
+ },
+ "description": "Actualitza la informaci\u00f3 d'inici de sessi\u00f3 de {name}."
+ },
"user": {
"data": {
"host": "Amfitri\u00f3",
diff --git a/homeassistant/components/fritzbox/translations/cs.json b/homeassistant/components/fritzbox/translations/cs.json
index 67ff5db7f9952c..b3b41afe3833c1 100644
--- a/homeassistant/components/fritzbox/translations/cs.json
+++ b/homeassistant/components/fritzbox/translations/cs.json
@@ -3,7 +3,8 @@
"abort": {
"already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno",
"already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1",
- "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed"
+ "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed",
+ "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9"
},
"error": {
"invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed"
@@ -17,6 +18,12 @@
},
"description": "Chcete nastavit {name}?"
},
+ "reauth_confirm": {
+ "data": {
+ "password": "Heslo",
+ "username": "U\u017eivatelsk\u00e9 jm\u00e9no"
+ }
+ },
"user": {
"data": {
"host": "Hostitel",
diff --git a/homeassistant/components/fritzbox/translations/de.json b/homeassistant/components/fritzbox/translations/de.json
index 19ca2e809036cb..16263722482430 100644
--- a/homeassistant/components/fritzbox/translations/de.json
+++ b/homeassistant/components/fritzbox/translations/de.json
@@ -1,9 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "Diese AVM FRITZ! Box ist bereits konfiguriert.",
- "already_in_progress": "Die Konfiguration der AVM FRITZ! Box ist bereits in Bearbeitung.",
- "not_supported": "Verbunden mit AVM FRITZ! Box, kann jedoch keine Smart Home-Ger\u00e4te steuern."
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
+ "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden",
+ "not_supported": "Verbunden mit AVM FRITZ! Box, kann jedoch keine Smart Home-Ger\u00e4te steuern.",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
+ },
+ "error": {
+ "invalid_auth": "Ung\u00fcltige Zugangsdaten"
},
"flow_title": "AVM FRITZ! Box: {name}",
"step": {
@@ -12,7 +17,14 @@
"password": "Passwort",
"username": "Benutzername"
},
- "description": "M\u00f6chten Sie {name} einrichten?"
+ "description": "M\u00f6chtest du {name} einrichten?"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ },
+ "description": "Aktualisiere deine Anmeldeinformationen f\u00fcr {name}."
},
"user": {
"data": {
@@ -20,7 +32,7 @@
"password": "Passwort",
"username": "Benutzername"
},
- "description": "Geben Sie Ihre AVM FRITZ! Box-Informationen ein."
+ "description": "Gib deine AVM FRITZ! Box-Informationen ein."
}
}
}
diff --git a/homeassistant/components/fritzbox/translations/en.json b/homeassistant/components/fritzbox/translations/en.json
index 1f22bc30252cbe..61ca1e957bb2b9 100644
--- a/homeassistant/components/fritzbox/translations/en.json
+++ b/homeassistant/components/fritzbox/translations/en.json
@@ -4,7 +4,8 @@
"already_configured": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress",
"no_devices_found": "No devices found on the network",
- "not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices."
+ "not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices.",
+ "reauth_successful": "Re-authentication was successful"
},
"error": {
"invalid_auth": "Invalid authentication"
@@ -18,6 +19,13 @@
},
"description": "Do you want to set up {name}?"
},
+ "reauth_confirm": {
+ "data": {
+ "password": "Password",
+ "username": "Username"
+ },
+ "description": "Update your login information for {name}."
+ },
"user": {
"data": {
"host": "Host",
diff --git a/homeassistant/components/fritzbox/translations/es.json b/homeassistant/components/fritzbox/translations/es.json
index 5a9544df4e50e3..fcb240deb77792 100644
--- a/homeassistant/components/fritzbox/translations/es.json
+++ b/homeassistant/components/fritzbox/translations/es.json
@@ -4,7 +4,8 @@
"already_configured": "Este AVM FRITZ!Box ya est\u00e1 configurado.",
"already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso",
"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."
+ "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"
},
"error": {
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida"
@@ -18,6 +19,13 @@
},
"description": "\u00bfQuieres configurar {name}?"
},
+ "reauth_confirm": {
+ "data": {
+ "password": "Contrase\u00f1a",
+ "username": "Usuario"
+ },
+ "description": "Actualice la informaci\u00f3n de inicio de sesi\u00f3n para {name}."
+ },
"user": {
"data": {
"host": "Host",
diff --git a/homeassistant/components/fritzbox/translations/et.json b/homeassistant/components/fritzbox/translations/et.json
index 702488bce0c8a1..5ee2dc801f4610 100644
--- a/homeassistant/components/fritzbox/translations/et.json
+++ b/homeassistant/components/fritzbox/translations/et.json
@@ -4,7 +4,8 @@
"already_configured": "Seade on juba h\u00e4\u00e4lestatud",
"already_in_progress": "Seadistamine on juba k\u00e4imas",
"no_devices_found": "V\u00f5rgust ei leitud seadmeid",
- "not_supported": "\u00dchendatud AVM FRITZ!Boxiga! kuid see ei saa juhtida Smart Home seadmeid."
+ "not_supported": "\u00dchendatud AVM FRITZ!Boxiga! kuid see ei saa juhtida Smart Home seadmeid.",
+ "reauth_successful": "Taastuvastamine \u00f5nnestus"
},
"error": {
"invalid_auth": "Tuvastamise viga"
@@ -18,6 +19,13 @@
},
"description": "Kas soovid seadistada {name}?"
},
+ "reauth_confirm": {
+ "data": {
+ "password": "Salas\u00f5na",
+ "username": "Kasutajanimi"
+ },
+ "description": "V\u00e4rskenda konto {name} sisselogimisteavet."
+ },
"user": {
"data": {
"host": "",
diff --git a/homeassistant/components/fritzbox/translations/fr.json b/homeassistant/components/fritzbox/translations/fr.json
index e7a8acaa76221b..e6302964988cfd 100644
--- a/homeassistant/components/fritzbox/translations/fr.json
+++ b/homeassistant/components/fritzbox/translations/fr.json
@@ -4,7 +4,8 @@
"already_configured": "Cette AVM FRITZ!Box est d\u00e9j\u00e0 configur\u00e9e.",
"already_in_progress": "Une configuration d'AVM FRITZ!Box est d\u00e9j\u00e0 en cours.",
"no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau",
- "not_supported": "Connect\u00e9 \u00e0 AVM FRITZ! Box mais impossible de contr\u00f4ler les appareils Smart Home."
+ "not_supported": "Connect\u00e9 \u00e0 AVM FRITZ! Box mais impossible de contr\u00f4ler les appareils Smart Home.",
+ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi"
},
"error": {
"invalid_auth": "Authentification invalide"
@@ -18,6 +19,13 @@
},
"description": "Voulez-vous configurer {name} ?"
},
+ "reauth_confirm": {
+ "data": {
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur"
+ },
+ "description": "Mettez \u00e0 jour vos informations de connexion pour {name} ."
+ },
"user": {
"data": {
"host": "Nom d'h\u00f4te ou adresse IP",
diff --git a/homeassistant/components/fritzbox/translations/he.json b/homeassistant/components/fritzbox/translations/he.json
new file mode 100644
index 00000000000000..035cb07a170a87
--- /dev/null
+++ b/homeassistant/components/fritzbox/translations/he.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "step": {
+ "confirm": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox/translations/hu.json b/homeassistant/components/fritzbox/translations/hu.json
index 6a08b68d863e1b..44b68d5f540ca2 100644
--- a/homeassistant/components/fritzbox/translations/hu.json
+++ b/homeassistant/components/fritzbox/translations/hu.json
@@ -1,7 +1,24 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
+ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
+ },
+ "error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
+ },
+ "flow_title": "AVM FRITZ!Box: {name}",
"step": {
"confirm": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?"
+ },
+ "reauth_confirm": {
"data": {
"password": "Jelsz\u00f3",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
diff --git a/homeassistant/components/fritzbox/translations/id.json b/homeassistant/components/fritzbox/translations/id.json
new file mode 100644
index 00000000000000..8dbd1f71534042
--- /dev/null
+++ b/homeassistant/components/fritzbox/translations/id.json
@@ -0,0 +1,39 @@
+{
+ "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": "Tersambung ke AVM FRITZ!Box tetapi tidak dapat mengontrol perangkat Smart Home.",
+ "reauth_successful": "Autentikasi ulang berhasil"
+ },
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "flow_title": "AVM FRITZ!Box: {name}",
+ "step": {
+ "confirm": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "description": "Ingin menyiapkan {name}?"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "description": "Perbarui informasi masuk Anda untuk {name}."
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "description": "Masukkan informasi AVM FRITZ!Box Anda."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox/translations/it.json b/homeassistant/components/fritzbox/translations/it.json
index ab44ae138631ad..6aba6a007d7205 100644
--- a/homeassistant/components/fritzbox/translations/it.json
+++ b/homeassistant/components/fritzbox/translations/it.json
@@ -4,7 +4,8 @@
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
"already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"no_devices_found": "Nessun dispositivo trovato sulla rete",
- "not_supported": "Collegato a AVM FRITZ!Box ma non \u00e8 in grado di controllare i dispositivi Smart Home."
+ "not_supported": "Collegato a AVM FRITZ!Box ma non \u00e8 in grado di controllare i dispositivi Smart Home.",
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
},
"error": {
"invalid_auth": "Autenticazione non valida"
@@ -18,6 +19,13 @@
},
"description": "Vuoi impostare {name}?"
},
+ "reauth_confirm": {
+ "data": {
+ "password": "Password",
+ "username": "Nome utente"
+ },
+ "description": "Aggiorna le tue informazioni di accesso per {name}."
+ },
"user": {
"data": {
"host": "Host",
diff --git a/homeassistant/components/fritzbox/translations/ko.json b/homeassistant/components/fritzbox/translations/ko.json
index b04b6905284a16..21fd6b6afdf63c 100644
--- a/homeassistant/components/fritzbox/translations/ko.json
+++ b/homeassistant/components/fritzbox/translations/ko.json
@@ -1,9 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 AVM FRITZ!Box \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "already_in_progress": "AVM FRITZ!Box \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.",
- "not_supported": "AVM FRITZ!Box \uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc9c0\ub9cc \uc2a4\ub9c8\ud2b8 \ud648 \uae30\uae30\ub97c \uc81c\uc5b4\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "not_supported": "AVM FRITZ!Box\uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc9c0\ub9cc \uc2a4\ub9c8\ud2b8 \ud648 \uae30\uae30\ub97c \uc81c\uc5b4\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"flow_title": "AVM FRITZ!Box: {name}",
"step": {
@@ -12,7 +17,14 @@
"password": "\ube44\ubc00\ubc88\ud638",
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
},
- "description": "{name} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ "description": "{name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ },
+ "description": "{name}\uc5d0 \ub300\ud55c \ub85c\uadf8\uc778 \uc815\ubcf4\ub97c \uc5c5\ub370\uc774\ud2b8\ud574\uc8fc\uc138\uc694."
},
"user": {
"data": {
diff --git a/homeassistant/components/fritzbox/translations/nl.json b/homeassistant/components/fritzbox/translations/nl.json
index b72374547bc157..aa4f796f44c43f 100644
--- a/homeassistant/components/fritzbox/translations/nl.json
+++ b/homeassistant/components/fritzbox/translations/nl.json
@@ -1,9 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "Deze AVM FRITZ!Box is al geconfigureerd.",
- "already_in_progress": "AVM FRITZ!Box configuratie is al bezig.",
- "not_supported": "Verbonden met AVM FRITZ! Box, maar het kan geen Smart Home-apparaten bedienen."
+ "already_configured": "Apparaat is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
+ "no_devices_found": "Geen apparaten gevonden op het netwerk",
+ "not_supported": "Verbonden met AVM FRITZ! Box, maar het kan geen Smart Home-apparaten bedienen.",
+ "reauth_successful": "Herauthenticatie was succesvol"
+ },
+ "error": {
+ "invalid_auth": "Ongeldige authenticatie"
},
"flow_title": "AVM FRITZ!Box: {name}",
"step": {
@@ -14,9 +19,16 @@
},
"description": "Wilt u {name} instellen?"
},
+ "reauth_confirm": {
+ "data": {
+ "password": "Wachtwoord",
+ "username": "Gebruikersnaam"
+ },
+ "description": "Gelieve uw logingegevens voor {name} te actualiseren."
+ },
"user": {
"data": {
- "host": "Host of IP-adres",
+ "host": "Host",
"password": "Wachtwoord",
"username": "Gebruikersnaam"
},
diff --git a/homeassistant/components/fritzbox/translations/no.json b/homeassistant/components/fritzbox/translations/no.json
index 024e25741a7a1a..bd64b428bdf99f 100644
--- a/homeassistant/components/fritzbox/translations/no.json
+++ b/homeassistant/components/fritzbox/translations/no.json
@@ -4,7 +4,8 @@
"already_configured": "Enheten er allerede konfigurert",
"already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
"no_devices_found": "Ingen enheter funnet p\u00e5 nettverket",
- "not_supported": "Tilkoblet AVM FRITZ! Box, men den klarer ikke \u00e5 kontrollere Smart Home-enheter."
+ "not_supported": "Tilkoblet AVM FRITZ! Box, men den klarer ikke \u00e5 kontrollere Smart Home-enheter.",
+ "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket"
},
"error": {
"invalid_auth": "Ugyldig godkjenning"
@@ -18,6 +19,13 @@
},
"description": "Vil du sette opp {name} ?"
},
+ "reauth_confirm": {
+ "data": {
+ "password": "Passord",
+ "username": "Brukernavn"
+ },
+ "description": "Oppdater p\u00e5loggingsinformasjonen for {name} ."
+ },
"user": {
"data": {
"host": "Vert",
diff --git a/homeassistant/components/fritzbox/translations/pl.json b/homeassistant/components/fritzbox/translations/pl.json
index fc1623101895e6..dc05e431832259 100644
--- a/homeassistant/components/fritzbox/translations/pl.json
+++ b/homeassistant/components/fritzbox/translations/pl.json
@@ -4,7 +4,8 @@
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"already_in_progress": "Konfiguracja jest ju\u017c w toku",
"no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci",
- "not_supported": "Po\u0142\u0105czony z AVM FRITZ!Box, ale nie jest w stanie kontrolowa\u0107 urz\u0105dze\u0144 Smart Home"
+ "not_supported": "Po\u0142\u0105czony z AVM FRITZ!Box, ale nie jest w stanie kontrolowa\u0107 urz\u0105dze\u0144 Smart Home",
+ "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119"
},
"error": {
"invalid_auth": "Niepoprawne uwierzytelnienie"
@@ -18,6 +19,13 @@
},
"description": "Czy chcesz skonfigurowa\u0107 {name}?"
},
+ "reauth_confirm": {
+ "data": {
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ },
+ "description": "Zaktualizuj dane logowania dla {name}"
+ },
"user": {
"data": {
"host": "Nazwa hosta lub adres IP",
diff --git a/homeassistant/components/fritzbox/translations/ru.json b/homeassistant/components/fritzbox/translations/ru.json
index 322f677c2afacf..adbdfa13d6bc9c 100644
--- a/homeassistant/components/fritzbox/translations/ru.json
+++ b/homeassistant/components/fritzbox/translations/ru.json
@@ -4,25 +4,33 @@
"already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
"already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
"no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.",
- "not_supported": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AVM FRITZ! Box \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e, \u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c\u0438 Smart Home \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e."
+ "not_supported": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AVM FRITZ! Box \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e, \u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c\u0438 Smart Home \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
},
"error": {
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"flow_title": "AVM FRITZ!Box: {name}",
"step": {
"confirm": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?"
},
+ "reauth_confirm": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ },
+ "description": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0434\u043b\u044f {name}."
+ },
"user": {
"data": {
"host": "\u0425\u043e\u0441\u0442",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 AVM FRITZ!Box."
}
diff --git a/homeassistant/components/fritzbox/translations/tr.json b/homeassistant/components/fritzbox/translations/tr.json
new file mode 100644
index 00000000000000..746fe594e19902
--- /dev/null
+++ b/homeassistant/components/fritzbox/translations/tr.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor",
+ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu"
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "confirm": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ },
+ "description": "{name} kurmak istiyor musunuz?"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "\u015eifre",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ },
+ "description": "Giri\u015f bilgilerinizi {name} i\u00e7in g\u00fcncelleyin."
+ },
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox/translations/uk.json b/homeassistant/components/fritzbox/translations/uk.json
new file mode 100644
index 00000000000000..5a2d8a1c35e06d
--- /dev/null
+++ b/homeassistant/components/fritzbox/translations/uk.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.",
+ "not_supported": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e AVM FRITZ! Box \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u043e, \u0430\u043b\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044f\u043c\u0438 Smart Home \u043d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e."
+ },
+ "error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "flow_title": "AVM FRITZ!Box: {name}",
+ "step": {
+ "confirm": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name}?"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 AVM FRITZ! Box."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox/translations/zh-Hant.json b/homeassistant/components/fritzbox/translations/zh-Hant.json
index 7b85df577ef148..71a74785267681 100644
--- a/homeassistant/components/fritzbox/translations/zh-Hant.json
+++ b/homeassistant/components/fritzbox/translations/zh-Hant.json
@@ -4,7 +4,8 @@
"already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e",
- "not_supported": "\u5df2\u9023\u7dda\u81f3 AVM FRITZ!Box \u4f46\u7121\u6cd5\u63a7\u5236\u667a\u80fd\u5bb6\u5ead\u88dd\u7f6e\u3002"
+ "not_supported": "\u5df2\u9023\u7dda\u81f3 AVM FRITZ!Box \u4f46\u7121\u6cd5\u63a7\u5236\u667a\u80fd\u5bb6\u5ead\u88dd\u7f6e\u3002",
+ "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f"
},
"error": {
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548"
@@ -18,6 +19,13 @@
},
"description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f"
},
+ "reauth_confirm": {
+ "data": {
+ "password": "\u5bc6\u78bc",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ },
+ "description": "\u66f4\u65b0 {name} \u767b\u5165\u8cc7\u8a0a\u3002"
+ },
"user": {
"data": {
"host": "\u4e3b\u6a5f\u7aef",
diff --git a/homeassistant/components/fritzbox_callmonitor/__init__.py b/homeassistant/components/fritzbox_callmonitor/__init__.py
index 933dd797dfcef4..0ba0de598497f7 100644
--- a/homeassistant/components/fritzbox_callmonitor/__init__.py
+++ b/homeassistant/components/fritzbox_callmonitor/__init__.py
@@ -21,11 +21,6 @@
_LOGGER = logging.getLogger(__name__)
-async def async_setup(hass, config):
- """Set up the fritzbox_callmonitor integration."""
- return True
-
-
async def async_setup_entry(hass, config_entry):
"""Set up the fritzbox_callmonitor platforms."""
fritzbox_phonebook = FritzBoxPhonebook(
@@ -59,9 +54,9 @@ async def async_setup_entry(hass, config_entry):
UNDO_UPDATE_LISTENER: undo_listener,
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return True
@@ -73,8 +68,8 @@ async def async_unload_entry(hass, config_entry):
unload_ok = all(
await gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/fritzbox_callmonitor/base.py b/homeassistant/components/fritzbox_callmonitor/base.py
index 79f82de95b7959..af0612d7632576 100644
--- a/homeassistant/components/fritzbox_callmonitor/base.py
+++ b/homeassistant/components/fritzbox_callmonitor/base.py
@@ -1,4 +1,5 @@
"""Base class for fritzbox_callmonitor entities."""
+from contextlib import suppress
from datetime import timedelta
import logging
import re
@@ -41,7 +42,7 @@ def init_phonebook(self):
@Throttle(MIN_TIME_PHONEBOOK_UPDATE)
def update_phonebook(self):
"""Update the phone book dictionary."""
- if not self.phonebook_id:
+ if self.phonebook_id is None:
return
self.phonebook_dict = self.fph.get_all_names(self.phonebook_id)
@@ -69,11 +70,7 @@ def get_name(self, number):
return UNKOWN_NAME
for prefix in self.prefixes:
- try:
+ with suppress(KeyError):
return self.number_dict[prefix + number]
- except KeyError:
- pass
- try:
+ with suppress(KeyError):
return self.number_dict[prefix + number.lstrip("0")]
- except KeyError:
- pass
diff --git a/homeassistant/components/fritzbox_callmonitor/config_flow.py b/homeassistant/components/fritzbox_callmonitor/config_flow.py
index ab296c84121ebf..c4d0076a792177 100644
--- a/homeassistant/components/fritzbox_callmonitor/config_flow.py
+++ b/homeassistant/components/fritzbox_callmonitor/config_flow.py
@@ -16,8 +16,6 @@
from homeassistant.core import callback
from .base import FritzBoxPhonebook
-
-# pylint:disable=unused-import
from .const import (
CONF_PHONEBOOK,
CONF_PREFIXES,
@@ -165,9 +163,7 @@ async def async_step_user(self, user_input=None):
if result != RESULT_SUCCESS:
return self.async_abort(reason=result)
- if ( # pylint: disable=no-member
- self.context["source"] == config_entries.SOURCE_IMPORT
- ):
+ if self.context["source"] == config_entries.SOURCE_IMPORT:
self._phonebook_id = user_input[CONF_PHONEBOOK]
self._phonebook_name = user_input[CONF_NAME]
diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py
index 891bf8131d6faa..a325c0ca71dccc 100644
--- a/homeassistant/components/fritzbox_callmonitor/sensor.py
+++ b/homeassistant/components/fritzbox_callmonitor/sensor.py
@@ -8,7 +8,7 @@
from fritzconnection.core.fritzmonitor import FritzMonitor
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
CONF_HOST,
@@ -19,7 +19,6 @@
EVENT_HOMEASSISTANT_STOP,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from .const import (
ATTR_PREFIXES,
@@ -102,7 +101,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities([sensor])
-class FritzBoxCallSensor(Entity):
+class FritzBoxCallSensor(SensorEntity):
"""Implementation of a Fritz!Box call monitor."""
def __init__(self, name, unique_id, fritzbox_phonebook, prefixes, host, port):
@@ -169,7 +168,7 @@ def icon(self):
return ICON_PHONE
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._prefixes:
self._attributes[ATTR_PREFIXES] = self._prefixes
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/bg.json b/homeassistant/components/fritzbox_callmonitor/translations/bg.json
new file mode 100644
index 00000000000000..fc2115d9ca038b
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/bg.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "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/fritzbox_callmonitor/translations/ca.json b/homeassistant/components/fritzbox_callmonitor/translations/ca.json
new file mode 100644
index 00000000000000..808b642f4ff680
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/ca.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "insufficient_permissions": "L'usuari no t\u00e9 permisos suficients per accedir a la configuraci\u00f3 d'AVM FRITZ!Box i les seves agendes.",
+ "no_devices_found": "No s'han trobat dispositius a la xarxa"
+ },
+ "error": {
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida"
+ },
+ "flow_title": "Sensor de trucades d'AVM FRITZ!Box: {name}",
+ "step": {
+ "phonebook": {
+ "data": {
+ "phonebook": "Agenda"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "password": "Contrasenya",
+ "port": "Port",
+ "username": "Nom d'usuari"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "malformed_prefixes": "El format dels prefixos no \u00e9s correcte, comprova'l."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "prefixes": "Prefixos (llista separada per comes)"
+ },
+ "title": "Configuraci\u00f3 dels prefixos"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/cs.json b/homeassistant/components/fritzbox_callmonitor/translations/cs.json
new file mode 100644
index 00000000000000..c40da2900bcb80
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/cs.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno",
+ "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed"
+ },
+ "error": {
+ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hostitel",
+ "password": "Heslo",
+ "port": "Port",
+ "username": "U\u017eivatelsk\u00e9 jm\u00e9no"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/de.json b/homeassistant/components/fritzbox_callmonitor/translations/de.json
new file mode 100644
index 00000000000000..a26f301a9bdb5c
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/de.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "insufficient_permissions": "Der Benutzer verf\u00fcgt nicht \u00fcber ausreichende Berechtigungen, um auf die Einstellungen der AVM FRITZ!Box und ihre Telefonb\u00fccher zuzugreifen.",
+ "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden"
+ },
+ "error": {
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
+ },
+ "flow_title": "AVM FRITZ! Box-Anrufmonitor: {name}",
+ "step": {
+ "phonebook": {
+ "data": {
+ "phonebook": "Telefonbuch"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Passwort",
+ "port": "Port",
+ "username": "Benutzername"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "malformed_prefixes": "Die Pr\u00e4fixe sind fehlerhaft, bitte das Format \u00fcberpr\u00fcfen."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "prefixes": "Pr\u00e4fixe (kommagetrennte Liste)"
+ },
+ "title": "Pr\u00e4fixe konfigurieren"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/es.json b/homeassistant/components/fritzbox_callmonitor/translations/es.json
new file mode 100644
index 00000000000000..4d4aa4cd86b84b
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/es.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "abort": {
+ "insufficient_permissions": "El usuario no tiene permisos suficientes para acceder a la configuraci\u00f3n de AVM FRITZ! Box y sus agendas telef\u00f3nicas."
+ },
+ "error": {
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida"
+ },
+ "flow_title": "Monitor de llamadas de AVM FRITZ! Box: {name}",
+ "step": {
+ "phonebook": {
+ "data": {
+ "phonebook": "Directorio telef\u00f3nico"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Contrase\u00f1a",
+ "port": "Puerto",
+ "username": "Usuario"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "malformed_prefixes": "Los prefijos tienen un formato incorrecto, comprueba el formato."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "prefixes": "Prefijos (lista separada por comas)"
+ },
+ "title": "Configurar prefijos"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/et.json b/homeassistant/components/fritzbox_callmonitor/translations/et.json
new file mode 100644
index 00000000000000..7770f31ae0e073
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/et.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "insufficient_permissions": "Kasutajal ei ole piisavalt \u00f5igusi juurdep\u00e4\u00e4suks AVM FRITZ! Box'i seadetele jatelefoniraamatutele.",
+ "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet"
+ },
+ "error": {
+ "invalid_auth": "Vigane autentimine"
+ },
+ "flow_title": "AVM FRITZ! K\u00f5nekontroll: {name}",
+ "step": {
+ "phonebook": {
+ "data": {
+ "phonebook": "Telefoniraamat"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Salas\u00f5na",
+ "port": "Port",
+ "username": "Kasutajanimi"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "malformed_prefixes": "Eesliited on valesti vormindatud, kontrolli nende vormingut."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "prefixes": "Eesliited (komadega eraldatud loend)"
+ },
+ "title": "Eesliidete seadistamine"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/fr.json b/homeassistant/components/fritzbox_callmonitor/translations/fr.json
new file mode 100644
index 00000000000000..cde9023273c68b
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/fr.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ",
+ "insufficient_permissions": "L'utilisateur ne dispose pas des autorisations n\u00e9cessaires pour acc\u00e9der aux param\u00e8tres d'AVM FRITZ! Box et \u00e0 ses r\u00e9pertoires.",
+ "no_devices_found": "Aucun appreil trouv\u00e9 sur le r\u00e9seau "
+ },
+ "error": {
+ "invalid_auth": "Authentification invalide"
+ },
+ "flow_title": "Moniteur d'appels AVM FRITZ! Box: {name}",
+ "step": {
+ "phonebook": {
+ "data": {
+ "phonebook": "Annuaire"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Hote",
+ "password": "Mot de passe",
+ "port": "Port",
+ "username": "Nom d'utilisateur "
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "malformed_prefixes": "Les pr\u00e9fixes sont mal form\u00e9s, veuillez v\u00e9rifier leur format."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "prefixes": "Pr\u00e9fixes (liste s\u00e9par\u00e9e par des virgules)"
+ },
+ "title": "Configurer les pr\u00e9fixes"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/hu.json b/homeassistant/components/fritzbox_callmonitor/translations/hu.json
new file mode 100644
index 00000000000000..8c2c34775e5e8e
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/hu.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton"
+ },
+ "error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
+ },
+ "step": {
+ "phonebook": {
+ "data": {
+ "phonebook": "Telefonk\u00f6nyv"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "password": "Jelsz\u00f3",
+ "port": "Port",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/id.json b/homeassistant/components/fritzbox_callmonitor/translations/id.json
new file mode 100644
index 00000000000000..43bb4a16b47263
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/id.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "insufficient_permissions": "Izin pengguna tidak memadai untuk mengakses pengaturan dan buku telepon AVM FRITZ!Box.",
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan"
+ },
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "flow_title": "Pantau panggilan AVM FRITZ!Box: {name}",
+ "step": {
+ "phonebook": {
+ "data": {
+ "phonebook": "Buku telepon"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "username": "Nama Pengguna"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "malformed_prefixes": "Format prefiks salah, periksa formatnya."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "prefixes": "Prefiks (daftar yang dipisahkan koma)"
+ },
+ "title": "Konfigurasikan Prefiks"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/it.json b/homeassistant/components/fritzbox_callmonitor/translations/it.json
new file mode 100644
index 00000000000000..5696bf86fd1f7d
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/it.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "insufficient_permissions": "L'utente non dispone di autorizzazioni sufficienti per accedere alle impostazioni di AVM FRITZ! Box e alle sue rubriche.",
+ "no_devices_found": "Nessun dispositivo trovato sulla rete"
+ },
+ "error": {
+ "invalid_auth": "Autenticazione non valida"
+ },
+ "flow_title": "Monitoraggio chiamate FRITZ! Box AVM: {name}",
+ "step": {
+ "phonebook": {
+ "data": {
+ "phonebook": "Rubrica telefonica"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Password",
+ "port": "Porta",
+ "username": "Nome utente"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "malformed_prefixes": "I prefissi non sono corretti, controlla il loro formato."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "prefixes": "Prefissi (elenco separato da virgole)"
+ },
+ "title": "Configura prefissi"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ko.json b/homeassistant/components/fritzbox_callmonitor/translations/ko.json
new file mode 100644
index 00000000000000..51f4ccec35aa73
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/ko.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "insufficient_permissions": "AVM FRIZ!Box \uc124\uc815 \ubc0f \uc804\ud654\ubc88\ud638\ubd80\uc5d0 \ud544\uc694\ud55c \uc0ac\uc6a9\uc790 \uc811\uadfc \uad8c\ud55c\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4.",
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "flow_title": "AVM FRITZ!Box \uc804\ud654 \ubaa8\ub2c8\ud130\ub9c1: {name}",
+ "step": {
+ "phonebook": {
+ "data": {
+ "phonebook": "\uc804\ud654\ubc88\ud638\ubd80"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "port": "\ud3ec\ud2b8",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "malformed_prefixes": "\uc811\ub450\uc0ac\uc758 \ud615\uc2dd\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud615\uc2dd\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "prefixes": "\uc811\ub450\uc0ac (\uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \ubaa9\ub85d)"
+ },
+ "title": "\uc811\ub450\uc0ac \uad6c\uc131\ud558\uae30"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/lb.json b/homeassistant/components/fritzbox_callmonitor/translations/lb.json
new file mode 100644
index 00000000000000..67b5879a557a2c
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/lb.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "flow_title": "AVM FRITZ!Box Call Monitor: {name}",
+ "step": {
+ "phonebook": {
+ "data": {
+ "phonebook": "Adressbuch"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Passwuert",
+ "port": "Port",
+ "username": "Benotzernumm"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "malformed_prefixes": "Pr\u00e9fixe sinn am falsche Format, iwwerpr\u00e9if dat w.e.g"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "prefixes": "Pr\u00e9fixe (komma getrennte L\u00ebscht)"
+ },
+ "title": "Pr\u00e9fixe konfigur\u00e9ieren"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/nl.json b/homeassistant/components/fritzbox_callmonitor/translations/nl.json
new file mode 100644
index 00000000000000..bc706861313f9e
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/nl.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd",
+ "insufficient_permissions": "Gebruiker heeft onvoldoende rechten voor toegang tot AVM FRITZ!Box instellingen en de telefoonboeken.",
+ "no_devices_found": "Geen apparaten gevonden op het netwerk"
+ },
+ "error": {
+ "invalid_auth": "Ongeldige authenticatie"
+ },
+ "flow_title": "AVM FRITZ!Box oproepmonitor: {name}",
+ "step": {
+ "phonebook": {
+ "data": {
+ "phonebook": "Telefoonboek"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Wachtwoord",
+ "port": "Poort",
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "malformed_prefixes": "Voorvoegsels hebben een onjuiste indeling, controleer hun indeling."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "prefixes": "Voorvoegsels (door komma's gescheiden lijst)"
+ },
+ "title": "Configureer voorvoegsels"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/no.json b/homeassistant/components/fritzbox_callmonitor/translations/no.json
new file mode 100644
index 00000000000000..12883b0140d518
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/no.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert",
+ "insufficient_permissions": "Brukeren har utilstrekkelig tillatelse til \u00e5 f\u00e5 tilgang til AVM FRITZ! Box-innstillingene og telefonb\u00f8kene.",
+ "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket"
+ },
+ "error": {
+ "invalid_auth": "Ugyldig godkjenning"
+ },
+ "flow_title": "AVM FRITZ! Box monitor: {name}",
+ "step": {
+ "phonebook": {
+ "data": {
+ "phonebook": "Telefonbok"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Vert",
+ "password": "Passord",
+ "port": "Port",
+ "username": "Brukernavn"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "malformed_prefixes": "Prefikser er misformet, vennligst sjekk deres format."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "prefixes": "Prefikser (kommaseparert liste)"
+ },
+ "title": "Konfigurer prefiks"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/pl.json b/homeassistant/components/fritzbox_callmonitor/translations/pl.json
new file mode 100644
index 00000000000000..fa0317f5c9d913
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/pl.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "insufficient_permissions": "U\u017cytkownik ma niewystarczaj\u0105ce uprawnienia, aby uzyska\u0107 dost\u0119p do ustawie\u0144 AVM FRITZ! Box i jego ksi\u0105\u017cek telefonicznych.",
+ "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci"
+ },
+ "error": {
+ "invalid_auth": "Niepoprawne uwierzytelnienie"
+ },
+ "flow_title": "Monitor po\u0142\u0105cze\u0144 AVM FRITZ!Box: {name}",
+ "step": {
+ "phonebook": {
+ "data": {
+ "phonebook": "Ksi\u0105\u017cka telefoniczna"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP",
+ "password": "Has\u0142o",
+ "port": "Port",
+ "username": "Nazwa u\u017cytkownika"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "malformed_prefixes": "Prefiksy s\u0105 nieprawid\u0142owe, prosz\u0119 sprawdzi\u0107 ich format."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "prefixes": "Prefiksy (lista oddzielona przecinkami)"
+ },
+ "title": "Skonfiguruj prefiksy"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ru.json b/homeassistant/components/fritzbox_callmonitor/translations/ru.json
new file mode 100644
index 00000000000000..38448ac8c59828
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/ru.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "insufficient_permissions": "\u0423 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0435\u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043f\u0440\u0430\u0432 \u0434\u043b\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c AVM FRITZ!Box \u0438 \u0435\u0433\u043e \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u043d\u044b\u043c \u043a\u043d\u0438\u0433\u0430\u043c.",
+ "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438."
+ },
+ "error": {
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
+ },
+ "flow_title": "AVM FRITZ!Box call monitor: {name}",
+ "step": {
+ "phonebook": {
+ "data": {
+ "phonebook": "\u0422\u0435\u043b\u0435\u0444\u043e\u043d\u043d\u0430\u044f \u043a\u043d\u0438\u0433\u0430"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "malformed_prefixes": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441\u044b \u0438\u043c\u0435\u044e\u0442 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0438\u0445."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "prefixes": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441\u044b (\u0441\u043f\u0438\u0441\u043e\u043a, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0439 \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438)"
+ },
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u0432"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/tr.json b/homeassistant/components/fritzbox_callmonitor/translations/tr.json
new file mode 100644
index 00000000000000..76799f24af824f
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/tr.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "insufficient_permissions": "Kullan\u0131c\u0131, AVM FRITZ! Box ayarlar\u0131na ve telefon defterlerine eri\u015fmek i\u00e7in yeterli izne sahip de\u011fil.",
+ "no_devices_found": "A\u011fda cihaz bulunamad\u0131"
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "flow_title": "AVM FRITZ! Box \u00e7a\u011fr\u0131 monit\u00f6r\u00fc: {name}",
+ "step": {
+ "phonebook": {
+ "data": {
+ "phonebook": "Telefon rehberi"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "password": "\u015eifre",
+ "port": "Port",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "malformed_prefixes": "\u00d6nekler yanl\u0131\u015f bi\u00e7imlendirilmi\u015ftir, l\u00fctfen bi\u00e7imlerini kontrol edin."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "prefixes": "\u00d6nekler (virg\u00fclle ayr\u0131lm\u0131\u015f liste)"
+ },
+ "title": "\u00d6nekleri Yap\u0131land\u0131r"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json b/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json
new file mode 100644
index 00000000000000..d159f5df0f9706
--- /dev/null
+++ b/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "insufficient_permissions": "\u4f7f\u7528\u8005\u6c92\u6709\u8db3\u5920\u6b0a\u9650\u4ee5\u5b58\u53d6 AVM FRITZ!Box \u8a2d\u5b9a\u53ca\u96fb\u8a71\u7c3f\u3002",
+ "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e"
+ },
+ "error": {
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548"
+ },
+ "flow_title": "AVM FRITZ!Box \u901a\u8a71\u76e3\u63a7\u5668\uff1a{name}",
+ "step": {
+ "phonebook": {
+ "data": {
+ "phonebook": "\u96fb\u8a71\u7c3f"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "password": "\u5bc6\u78bc",
+ "port": "\u901a\u8a0a\u57e0",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "malformed_prefixes": "\u524d\u7db4\u5b57\u9996\u683c\u5f0f\u932f\u8aa4\uff0c\u8acb\u518d\u78ba\u8a8d\u5176\u683c\u5f0f\u3002"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "prefixes": "\u524d\u7db4\u5b57\u9996\uff08\u4ee5\u9017\u865f\u5206\u9694\uff09"
+ },
+ "title": "\u8a2d\u5b9a\u524d\u7db4\u5b57\u9996"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fritzbox_netmonitor/sensor.py b/homeassistant/components/fritzbox_netmonitor/sensor.py
index 13b822ae8a42b8..3c37de7664c665 100644
--- a/homeassistant/components/fritzbox_netmonitor/sensor.py
+++ b/homeassistant/components/fritzbox_netmonitor/sensor.py
@@ -7,10 +7,9 @@
from requests.exceptions import RequestException
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_HOST, CONF_NAME, STATE_UNAVAILABLE
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -62,7 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([FritzboxMonitorSensor(name, fstatus)], True)
-class FritzboxMonitorSensor(Entity):
+class FritzboxMonitorSensor(SensorEntity):
"""Implementation of a fritzbox monitor sensor."""
def __init__(self, name, fstatus):
@@ -93,7 +92,7 @@ def state(self):
return self._state
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
# Don't return attributes if FritzBox is unreachable
if self._state == STATE_UNAVAILABLE:
diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py
index 130a8d55072c6c..a908f2605f8ca1 100644
--- a/homeassistant/components/fronius/sensor.py
+++ b/homeassistant/components/fronius/sensor.py
@@ -1,4 +1,6 @@
"""Support for Fronius devices."""
+from __future__ import annotations
+
import copy
from datetime import timedelta
import logging
@@ -6,7 +8,7 @@
from pyfronius import Fronius
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_DEVICE,
CONF_MONITORED_CONDITIONS,
@@ -16,7 +18,6 @@
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
_LOGGER = logging.getLogger(__name__)
@@ -130,6 +131,7 @@ def __init__(self, bridge, name, device, add_entities):
self._name = name
self._device = device
self._fetched = {}
+ self._available = True
self.sensors = set()
self._registered_sensors = set()
@@ -145,21 +147,32 @@ def data(self):
"""Return the state attributes."""
return self._fetched
+ @property
+ def available(self):
+ """Whether the fronius device is active."""
+ return self._available
+
async def async_update(self):
"""Retrieve and update latest state."""
- values = {}
try:
values = await self._update()
except ConnectionError:
- _LOGGER.error("Failed to update: connection error")
+ # fronius devices are often powered by self-produced solar energy
+ # and henced turned off at night.
+ # Therefore we will not print multiple errors when connection fails
+ if self._available:
+ self._available = False
+ _LOGGER.error("Failed to update: connection error")
+ return
except ValueError:
_LOGGER.error(
"Failed to update: invalid response returned."
"Maybe the configured device is not supported"
)
-
- if not values:
return
+
+ self._available = True # reset connection failure
+
attributes = self._fetched
# Copy data of current fronius device
for key, entry in values.items():
@@ -182,7 +195,7 @@ async def async_update(self):
for sensor in self._registered_sensors:
sensor.async_schedule_update_ha_state(True)
- async def _update(self):
+ async def _update(self) -> dict:
"""Return values of interest."""
async def register(self, sensor):
@@ -238,7 +251,7 @@ async def _update(self):
return await self.bridge.current_power_flow()
-class FroniusTemplateSensor(Entity):
+class FroniusTemplateSensor(SensorEntity):
"""Sensor for the single values (e.g. pv power, ac power)."""
def __init__(self, parent: FroniusAdapter, name):
@@ -268,6 +281,11 @@ def should_poll(self):
"""Device should not be polled, returns False."""
return False
+ @property
+ def available(self):
+ """Whether the fronius device is active."""
+ return self.parent.available
+
async def async_update(self):
"""Update the internal state."""
state = self.parent.data.get(self._name)
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index 080d786d4e487c..0529fd6dbb2f08 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -1,10 +1,12 @@
"""Handle the frontend for Home Assistant."""
+from __future__ import annotations
+
import json
import logging
import mimetypes
import os
import pathlib
-from typing import Any, Dict, Optional, Set, Tuple
+from typing import Any
from aiohttp import hdrs, web, web_urldispatcher
import jinja2
@@ -14,7 +16,7 @@
from homeassistant.components import websocket_api
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.config import async_hass_config_yaml
-from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
+from homeassistant.const import CONF_MODE, CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback
from homeassistant.helpers import service
import homeassistant.helpers.config_validation as cv
@@ -57,6 +59,13 @@
}
for size in (192, 384, 512, 1024)
],
+ "screenshots": [
+ {
+ "src": "/static/images/screenshots/screenshot-1.png",
+ "sizes": "413x792",
+ "type": "image/png",
+ }
+ ],
"lang": "en-US",
"name": "Home Assistant",
"short_name": "Assistant",
@@ -113,26 +122,25 @@
SERVICE_SET_THEME = "set_theme"
SERVICE_RELOAD_THEMES = "reload_themes"
-CONF_MODE = "mode"
class Panel:
"""Abstract class for panels."""
# Name of the webcomponent
- component_name: Optional[str] = None
+ component_name: str | None = None
# Icon to show in the sidebar
- sidebar_icon: Optional[str] = None
+ sidebar_icon: str | None = None
# Title to show in the sidebar
- sidebar_title: Optional[str] = None
+ sidebar_title: str | None = None
# Url to show the panel in the frontend
- frontend_url_path: Optional[str] = None
+ frontend_url_path: str | None = None
# Config to pass to the webcomponent
- config: Optional[Dict[str, Any]] = None
+ config: dict[str, Any] | None = None
# If the panel should only be visible to admins
require_admin = False
@@ -262,10 +270,10 @@ async def async_setup(hass, config):
for path, should_cache in (
("service_worker.js", False),
("robots.txt", False),
- ("onboarding.html", True),
- ("static", True),
- ("frontend_latest", True),
- ("frontend_es5", True),
+ ("onboarding.html", not is_dev),
+ ("static", not is_dev),
+ ("frontend_latest", not is_dev),
+ ("frontend_es5", not is_dev),
):
hass.http.register_static_path(f"/{path}", str(root_path / path), should_cache)
@@ -444,7 +452,7 @@ def url_for(self, **kwargs: str) -> URL:
async def resolve(
self, request: web.Request
- ) -> Tuple[Optional[web_urldispatcher.UrlMappingMatchInfo], Set[str]]:
+ ) -> tuple[web_urldispatcher.UrlMappingMatchInfo | None, set[str]]:
"""Resolve resource.
Return (UrlMappingMatchInfo, allowed_methods) pair.
diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json
index eb455a5a6c1f79..98ae51341af4ff 100644
--- a/homeassistant/components/frontend/manifest.json
+++ b/homeassistant/components/frontend/manifest.json
@@ -2,7 +2,9 @@
"domain": "frontend",
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
- "requirements": ["home-assistant-frontend==20210127.5"],
+ "requirements": [
+ "home-assistant-frontend==20210407.2"
+ ],
"dependencies": [
"api",
"auth",
@@ -15,6 +17,8 @@
"system_log",
"websocket_api"
],
- "codeowners": ["@home-assistant/frontend"],
+ "codeowners": [
+ "@home-assistant/frontend"
+ ],
"quality_scale": "internal"
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/frontend/services.yaml b/homeassistant/components/frontend/services.yaml
index cc0d6bde21645b..075b73986ffc1d 100644
--- a/homeassistant/components/frontend/services.yaml
+++ b/homeassistant/components/frontend/services.yaml
@@ -11,4 +11,4 @@ set_theme:
example: "dark"
reload_themes:
- description: Reload themes from yaml configuration.
+ description: Reload themes from YAML configuration.
diff --git a/homeassistant/components/futurenow/light.py b/homeassistant/components/futurenow/light.py
index 6b05c96416f180..04a731a7272385 100644
--- a/homeassistant/components/futurenow/light.py
+++ b/homeassistant/components/futurenow/light.py
@@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
def to_futurenow_level(level):
"""Convert the given Home Assistant light level (0-255) to FutureNow (0-100)."""
- return int((level * 100) / 255)
+ return round((level * 100) / 255)
def to_hass_level(level):
diff --git a/homeassistant/components/garadget/cover.py b/homeassistant/components/garadget/cover.py
index 9c089e85811768..ad89c3a035b976 100644
--- a/homeassistant/components/garadget/cover.py
+++ b/homeassistant/components/garadget/cover.py
@@ -120,9 +120,8 @@ def __init__(self, hass, args):
def __del__(self):
"""Try to remove token."""
- if self._obtained_token is True:
- if self.access_token is not None:
- self.remove_token()
+ if self._obtained_token is True and self.access_token is not None:
+ self.remove_token()
@property
def name(self):
@@ -135,7 +134,7 @@ def available(self):
return self._available
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
data = {}
@@ -239,10 +238,12 @@ def update(self):
)
self._state = STATE_OFFLINE
- if self._state not in [STATE_CLOSING, STATE_OPENING]:
- if self._unsub_listener_cover is not None:
- self._unsub_listener_cover()
- self._unsub_listener_cover = None
+ if (
+ self._state not in [STATE_CLOSING, STATE_OPENING]
+ and self._unsub_listener_cover is not None
+ ):
+ self._unsub_listener_cover()
+ self._unsub_listener_cover = None
def _get_variable(self, var):
"""Get latest status."""
diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py
index 896c243f386d04..c009124b024499 100644
--- a/homeassistant/components/garmin_connect/__init__.py
+++ b/homeassistant/components/garmin_connect/__init__.py
@@ -57,9 +57,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
garmin_data = GarminConnectData(hass, garmin_client)
hass.data[DOMAIN][entry.entry_id] = garmin_data
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -70,8 +70,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/garmin_connect/config_flow.py b/homeassistant/components/garmin_connect/config_flow.py
index e0b50fa371bc35..7cd2309214aead 100644
--- a/homeassistant/components/garmin_connect/config_flow.py
+++ b/homeassistant/components/garmin_connect/config_flow.py
@@ -12,7 +12,7 @@
from homeassistant import config_entries
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/garmin_connect/const.py b/homeassistant/components/garmin_connect/const.py
index 7a143e2e63ac14..991ac90526a9b0 100644
--- a/homeassistant/components/garmin_connect/const.py
+++ b/homeassistant/components/garmin_connect/const.py
@@ -42,14 +42,14 @@
],
"wellnessStartTimeLocal": [
"Wellness Start Time",
- "",
+ None,
"mdi:clock",
DEVICE_CLASS_TIMESTAMP,
False,
],
"wellnessEndTimeLocal": [
"Wellness End Time",
- "",
+ None,
"mdi:clock",
DEVICE_CLASS_TIMESTAMP,
False,
@@ -299,7 +299,7 @@
"latestSpo2": ["Latest SPO2", PERCENTAGE, "mdi:diabetes", None, True],
"latestSpo2ReadingTimeLocal": [
"Latest SPO2 Time",
- "",
+ None,
"mdi:diabetes",
DEVICE_CLASS_TIMESTAMP,
False,
@@ -334,7 +334,7 @@
],
"latestRespirationTimeGMT": [
"Latest Respiration Update",
- "",
+ None,
"mdi:progress-clock",
DEVICE_CLASS_TIMESTAMP,
False,
@@ -348,5 +348,5 @@
"physiqueRating": ["Physique Rating", "", "mdi:numeric", None, False],
"visceralFat": ["Visceral Fat", "", "mdi:food", None, False],
"metabolicAge": ["Metabolic Age", "", "mdi:calendar-heart", None, False],
- "nextAlarm": ["Next Alarm Time", "", "mdi:alarm", DEVICE_CLASS_TIMESTAMP, True],
+ "nextAlarm": ["Next Alarm Time", None, "mdi:alarm", DEVICE_CLASS_TIMESTAMP, True],
}
diff --git a/homeassistant/components/garmin_connect/manifest.json b/homeassistant/components/garmin_connect/manifest.json
index c7880f9b416a65..59597750ce8123 100644
--- a/homeassistant/components/garmin_connect/manifest.json
+++ b/homeassistant/components/garmin_connect/manifest.json
@@ -2,7 +2,7 @@
"domain": "garmin_connect",
"name": "Garmin Connect",
"documentation": "https://www.home-assistant.io/integrations/garmin_connect",
- "requirements": ["garminconnect==0.1.16"],
+ "requirements": ["garminconnect==0.1.19"],
"codeowners": ["@cyberjunky"],
"config_flow": true
}
diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py
index 5d18f0a0dd0930..46db6e615f1c40 100644
--- a/homeassistant/components/garmin_connect/sensor.py
+++ b/homeassistant/components/garmin_connect/sensor.py
@@ -1,6 +1,8 @@
"""Platform for Garmin Connect integration."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict
+from typing import Any
from garminconnect import (
GarminConnectAuthenticationError,
@@ -8,9 +10,9 @@
GarminConnectTooManyRequestsError,
)
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ATTRIBUTION, CONF_ID
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
from .alarm_util import calculate_next_active_alarms
@@ -68,7 +70,7 @@ async def async_setup_entry(
async_add_entities(entities, True)
-class GarminConnectSensor(Entity):
+class GarminConnectSensor(SensorEntity):
"""Representation of a Garmin Connect Sensor."""
def __init__(
@@ -120,7 +122,7 @@ def unit_of_measurement(self):
return self._unit
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return attributes for sensor."""
if not self._data.data:
return {}
@@ -136,7 +138,7 @@ def device_state_attributes(self):
return attributes
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information."""
return {
"identifiers": {(DOMAIN, self._unique_id)},
diff --git a/homeassistant/components/garmin_connect/translations/de.json b/homeassistant/components/garmin_connect/translations/de.json
index 54d27e9956e0a7..9186f753a77246 100644
--- a/homeassistant/components/garmin_connect/translations/de.json
+++ b/homeassistant/components/garmin_connect/translations/de.json
@@ -4,10 +4,10 @@
"already_configured": "Dieses Konto ist bereits konfiguriert."
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuchen Sie es erneut.",
- "invalid_auth": "Ung\u00fcltige Authentifizierung.",
- "too_many_requests": "Zu viele Anfragen, wiederholen Sie es sp\u00e4ter.",
- "unknown": "Unerwarteter Fehler."
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "too_many_requests": "Zu viele Anfragen, versuche es sp\u00e4ter erneut.",
+ "unknown": "Unerwarteter Fehler"
},
"step": {
"user": {
diff --git a/homeassistant/components/garmin_connect/translations/he.json b/homeassistant/components/garmin_connect/translations/he.json
new file mode 100644
index 00000000000000..ac90b3264eab33
--- /dev/null
+++ b/homeassistant/components/garmin_connect/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/garmin_connect/translations/hu.json b/homeassistant/components/garmin_connect/translations/hu.json
index 2ada884847fc96..ae518acf001c47 100644
--- a/homeassistant/components/garmin_connect/translations/hu.json
+++ b/homeassistant/components/garmin_connect/translations/hu.json
@@ -4,10 +4,10 @@
"already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
},
"error": {
- "cannot_connect": "Nem siker\u00fclt csatlakozni, pr\u00f3b\u00e1lkozzon \u00fajra.",
- "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s.",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"too_many_requests": "T\u00fal sok k\u00e9r\u00e9s, pr\u00f3b\u00e1lkozzon k\u00e9s\u0151bb \u00fajra.",
- "unknown": "V\u00e1ratlan hiba."
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"step": {
"user": {
diff --git a/homeassistant/components/garmin_connect/translations/id.json b/homeassistant/components/garmin_connect/translations/id.json
new file mode 100644
index 00000000000000..27460757234863
--- /dev/null
+++ b/homeassistant/components/garmin_connect/translations/id.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "too_many_requests": "Terlalu banyak permintaan, coba lagi nanti.",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "description": "Masukkan kredensial Anda.",
+ "title": "Garmin Connect"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/garmin_connect/translations/ko.json b/homeassistant/components/garmin_connect/translations/ko.json
index fee07e579fe974..4d5330a824f135 100644
--- a/homeassistant/components/garmin_connect/translations/ko.json
+++ b/homeassistant/components/garmin_connect/translations/ko.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"too_many_requests": "\uc694\uccad\uc774 \ub108\ubb34 \ub9ce\uc2b5\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
diff --git a/homeassistant/components/garmin_connect/translations/nl.json b/homeassistant/components/garmin_connect/translations/nl.json
index e9b71c49c71768..e751aaf1b5c692 100644
--- a/homeassistant/components/garmin_connect/translations/nl.json
+++ b/homeassistant/components/garmin_connect/translations/nl.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Dit account is al geconfigureerd."
+ "already_configured": "Account is al geconfigureerd"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw.",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"too_many_requests": "Te veel aanvragen, probeer het later opnieuw.",
"unknown": "Onverwachte fout"
diff --git a/homeassistant/components/garmin_connect/translations/ru.json b/homeassistant/components/garmin_connect/translations/ru.json
index 69fa96c2a5e9af..066c337309f7c6 100644
--- a/homeassistant/components/garmin_connect/translations/ru.json
+++ b/homeassistant/components/garmin_connect/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"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."
},
@@ -13,7 +13,7 @@
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
"title": "Garmin Connect"
diff --git a/homeassistant/components/garmin_connect/translations/tr.json b/homeassistant/components/garmin_connect/translations/tr.json
new file mode 100644
index 00000000000000..a83e1936fb4a15
--- /dev/null
+++ b/homeassistant/components/garmin_connect/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/garmin_connect/translations/uk.json b/homeassistant/components/garmin_connect/translations/uk.json
new file mode 100644
index 00000000000000..aef0632b0f17d7
--- /dev/null
+++ b/homeassistant/components/garmin_connect/translations/uk.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "too_many_requests": "\u0417\u0430\u043d\u0430\u0434\u0442\u043e \u0431\u0430\u0433\u0430\u0442\u043e \u0437\u0430\u043f\u0438\u0442\u0456\u0432, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437 \u043f\u0456\u0437\u043d\u0456\u0448\u0435.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.",
+ "title": "Garmin Connect"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gc100/__init__.py b/homeassistant/components/gc100/__init__.py
index 77380afc2b5f1c..51abd9d5693322 100644
--- a/homeassistant/components/gc100/__init__.py
+++ b/homeassistant/components/gc100/__init__.py
@@ -25,7 +25,6 @@
)
-# pylint: disable=no-member
def setup(hass, base_config):
"""Set up the gc100 component."""
config = base_config[DOMAIN]
diff --git a/homeassistant/components/gdacs/config_flow.py b/homeassistant/components/gdacs/config_flow.py
index 1e12a116ed5a68..b672b56ad9b9b6 100644
--- a/homeassistant/components/gdacs/config_flow.py
+++ b/homeassistant/components/gdacs/config_flow.py
@@ -12,12 +12,7 @@
)
from homeassistant.helpers import config_validation as cv
-from .const import ( # pylint: disable=unused-import
- CONF_CATEGORIES,
- DEFAULT_RADIUS,
- DEFAULT_SCAN_INTERVAL,
- DOMAIN,
-)
+from .const import CONF_CATEGORIES, DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN
DATA_SCHEMA = vol.Schema(
{vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): cv.positive_int}
diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py
index c45d6e56425adf..7e3dc7484bb94f 100644
--- a/homeassistant/components/gdacs/geo_location.py
+++ b/homeassistant/components/gdacs/geo_location.py
@@ -1,6 +1,7 @@
"""Geolocation support for GDACS Feed."""
+from __future__ import annotations
+
import logging
-from typing import Optional
from homeassistant.components.geo_location import GeolocationEvent
from homeassistant.const import (
@@ -116,7 +117,7 @@ async def async_will_remove_from_hass(self) -> None:
@callback
def _delete_callback(self):
"""Remove this entity."""
- self.hass.async_create_task(self.async_remove())
+ self.hass.async_create_task(self.async_remove(force_remove=True))
@callback
def _update_callback(self):
@@ -169,7 +170,7 @@ def _update_from_feed(self, feed_entry):
self._version = feed_entry.version
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID containing latitude/longitude and external id."""
return f"{self._integration_id}_{self._external_id}"
@@ -186,22 +187,22 @@ def source(self) -> str:
return SOURCE
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the entity."""
return self._title
@property
- def distance(self) -> Optional[float]:
+ def distance(self) -> float | None:
"""Return distance value of this external event."""
return self._distance
@property
- def latitude(self) -> Optional[float]:
+ def latitude(self) -> float | None:
"""Return latitude value of this external event."""
return self._latitude
@property
- def longitude(self) -> Optional[float]:
+ def longitude(self) -> float | None:
"""Return longitude value of this external event."""
return self._longitude
@@ -213,7 +214,7 @@ def unit_of_measurement(self):
return LENGTH_KILOMETERS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
for key, value in (
diff --git a/homeassistant/components/gdacs/sensor.py b/homeassistant/components/gdacs/sensor.py
index fbbb199499b32e..2e4759088fc5b2 100644
--- a/homeassistant/components/gdacs/sensor.py
+++ b/homeassistant/components/gdacs/sensor.py
@@ -1,10 +1,11 @@
"""Feed Entity Manager Sensor support for GDACS Feed."""
+from __future__ import annotations
+
import logging
-from typing import Optional
+from homeassistant.components.sensor import SensorEntity
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from homeassistant.util import dt
from .const import DEFAULT_ICON, DOMAIN, FEED
@@ -33,7 +34,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
_LOGGER.debug("Sensor setup done")
-class GdacsSensor(Entity):
+class GdacsSensor(SensorEntity):
"""This is a status sensor for the GDACS integration."""
def __init__(self, config_entry_id, config_unique_id, config_title, manager):
@@ -109,12 +110,12 @@ def state(self):
return self._total
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID containing latitude/longitude."""
return self._config_unique_id
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the entity."""
return f"GDACS ({self._config_title})"
@@ -129,7 +130,7 @@ def unit_of_measurement(self):
return DEFAULT_UNIT_OF_MEASUREMENT
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
for key, value in (
diff --git a/homeassistant/components/gdacs/translations/de.json b/homeassistant/components/gdacs/translations/de.json
index 07d1a4bdb79bf7..a69295f06406a4 100644
--- a/homeassistant/components/gdacs/translations/de.json
+++ b/homeassistant/components/gdacs/translations/de.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Der Standort ist bereits konfiguriert."
+ "already_configured": "Der Dienst ist bereits konfiguriert"
},
"step": {
"user": {
diff --git a/homeassistant/components/gdacs/translations/hu.json b/homeassistant/components/gdacs/translations/hu.json
index 47eca9a7fac33b..fefcabae802009 100644
--- a/homeassistant/components/gdacs/translations/hu.json
+++ b/homeassistant/components/gdacs/translations/hu.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "A hely m\u00e1r konfigur\u00e1lva van."
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
},
"step": {
"user": {
diff --git a/homeassistant/components/gdacs/translations/id.json b/homeassistant/components/gdacs/translations/id.json
new file mode 100644
index 00000000000000..55e1db686a221a
--- /dev/null
+++ b/homeassistant/components/gdacs/translations/id.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Radius"
+ },
+ "title": "Isi detail filter Anda."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/translations/ko.json b/homeassistant/components/gdacs/translations/ko.json
index 1aeaf2192889e9..b91d512039ad66 100644
--- a/homeassistant/components/gdacs/translations/ko.json
+++ b/homeassistant/components/gdacs/translations/ko.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
diff --git a/homeassistant/components/gdacs/translations/nl.json b/homeassistant/components/gdacs/translations/nl.json
index f2a09892a66335..6dd3e5aa196977 100644
--- a/homeassistant/components/gdacs/translations/nl.json
+++ b/homeassistant/components/gdacs/translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Locatie is al geconfigureerd."
+ "already_configured": "Service is al geconfigureerd"
},
"step": {
"user": {
diff --git a/homeassistant/components/gdacs/translations/tr.json b/homeassistant/components/gdacs/translations/tr.json
new file mode 100644
index 00000000000000..aeb6a5a345e28a
--- /dev/null
+++ b/homeassistant/components/gdacs/translations/tr.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Yar\u0131\u00e7ap"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/translations/uk.json b/homeassistant/components/gdacs/translations/uk.json
new file mode 100644
index 00000000000000..0ab20bc55a37e8
--- /dev/null
+++ b/homeassistant/components/gdacs/translations/uk.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "\u0420\u0430\u0434\u0456\u0443\u0441"
+ },
+ "title": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gdacs/translations/zh-Hant.json b/homeassistant/components/gdacs/translations/zh-Hant.json
index 164d90fdcfdbeb..e5c6dfe6e50e8a 100644
--- a/homeassistant/components/gdacs/translations/zh-Hant.json
+++ b/homeassistant/components/gdacs/translations/zh-Hant.json
@@ -8,7 +8,7 @@
"data": {
"radius": "\u534a\u5f91"
},
- "title": "\u586b\u5beb\u904e\u6ffe\u5668\u8cc7\u8a0a\u3002"
+ "title": "\u586b\u5beb\u7be9\u9078\u5668\u8cc7\u8a0a\u3002"
}
}
}
diff --git a/homeassistant/components/geizhals/__init__.py b/homeassistant/components/geizhals/__init__.py
deleted file mode 100644
index 28b1d62307358a..00000000000000
--- a/homeassistant/components/geizhals/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The geizhals component."""
diff --git a/homeassistant/components/geizhals/manifest.json b/homeassistant/components/geizhals/manifest.json
deleted file mode 100644
index 17b4b5e9df0455..00000000000000
--- a/homeassistant/components/geizhals/manifest.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "domain": "geizhals",
- "name": "Geizhals",
- "documentation": "https://www.home-assistant.io/integrations/geizhals",
- "requirements": ["geizhals==0.0.9"],
- "codeowners": []
-}
diff --git a/homeassistant/components/geizhals/sensor.py b/homeassistant/components/geizhals/sensor.py
deleted file mode 100644
index 43e41e25e5eb62..00000000000000
--- a/homeassistant/components/geizhals/sensor.py
+++ /dev/null
@@ -1,94 +0,0 @@
-"""Parse prices of a device from geizhals."""
-from datetime import timedelta
-
-from geizhals import Device, Geizhals
-import voluptuous as vol
-
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
-from homeassistant.util import Throttle
-
-CONF_DESCRIPTION = "description"
-CONF_PRODUCT_ID = "product_id"
-CONF_LOCALE = "locale"
-
-ICON = "mdi:currency-usd-circle"
-
-MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_NAME): cv.string,
- vol.Required(CONF_PRODUCT_ID): cv.positive_int,
- vol.Optional(CONF_DESCRIPTION, default="Price"): cv.string,
- vol.Optional(CONF_LOCALE, default="DE"): vol.In(["AT", "EU", "DE", "UK", "PL"]),
- }
-)
-
-
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Geizwatch sensor."""
- name = config.get(CONF_NAME)
- description = config.get(CONF_DESCRIPTION)
- product_id = config.get(CONF_PRODUCT_ID)
- domain = config.get(CONF_LOCALE)
-
- add_entities([Geizwatch(name, description, product_id, domain)], True)
-
-
-class Geizwatch(Entity):
- """Implementation of Geizwatch."""
-
- def __init__(self, name, description, product_id, domain):
- """Initialize the sensor."""
-
- # internal
- self._name = name
- self._geizhals = Geizhals(product_id, domain)
- self._device = Device()
-
- # external
- self.description = description
- self.product_id = product_id
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
- @property
- def icon(self):
- """Return the icon for the frontend."""
- return ICON
-
- @property
- def state(self):
- """Return the best price of the selected product."""
- if not self._device.prices:
- return None
-
- return self._device.prices[0]
-
- @property
- def device_state_attributes(self):
- """Return the state attributes."""
- while len(self._device.prices) < 4:
- self._device.prices.append("None")
- attrs = {
- "device_name": self._device.name,
- "description": self.description,
- "unit_of_measurement": self._device.price_currency,
- "product_id": self.product_id,
- "price1": self._device.prices[0],
- "price2": self._device.prices[1],
- "price3": self._device.prices[2],
- "price4": self._device.prices[3],
- }
- return attrs
-
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
- def update(self):
- """Get the latest price from geizhals and updates the state."""
- self._device = self._geizhals.parse()
diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py
index 2e798b8cc4b5e0..1ec7f0874e0932 100644
--- a/homeassistant/components/generic/camera.py
+++ b/homeassistant/components/generic/camera.py
@@ -2,10 +2,7 @@
import asyncio
import logging
-import aiohttp
-import async_timeout
-import requests
-from requests.auth import HTTPDigestAuth
+import httpx
import voluptuous as vol
from homeassistant.components.camera import (
@@ -25,7 +22,7 @@
)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.reload import async_setup_reload_service
from . import DOMAIN, PLATFORMS
@@ -37,8 +34,12 @@
CONF_STILL_IMAGE_URL = "still_image_url"
CONF_STREAM_SOURCE = "stream_source"
CONF_FRAMERATE = "framerate"
+CONF_RTSP_TRANSPORT = "rtsp_transport"
+FFMPEG_OPTION_MAP = {CONF_RTSP_TRANSPORT: "rtsp_transport"}
+ALLOWED_RTSP_TRANSPORT_PROTOCOLS = {"tcp", "udp", "udp_multicast", "http"}
DEFAULT_NAME = "Generic Camera"
+GET_IMAGE_TIMEOUT = 10
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -56,6 +57,7 @@
cv.small_float, cv.positive_int
),
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
+ vol.Optional(CONF_RTSP_TRANSPORT): vol.In(ALLOWED_RTSP_TRANSPORT_PROTOCOLS),
}
)
@@ -87,15 +89,19 @@ def __init__(self, hass, device_info):
self._supported_features = SUPPORT_STREAM if self._stream_source else 0
self.content_type = device_info[CONF_CONTENT_TYPE]
self.verify_ssl = device_info[CONF_VERIFY_SSL]
+ if device_info.get(CONF_RTSP_TRANSPORT):
+ self.stream_options[FFMPEG_OPTION_MAP[CONF_RTSP_TRANSPORT]] = device_info[
+ CONF_RTSP_TRANSPORT
+ ]
username = device_info.get(CONF_USERNAME)
password = device_info.get(CONF_PASSWORD)
if username and password:
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
- self._auth = HTTPDigestAuth(username, password)
+ self._auth = httpx.DigestAuth(username=username, password=password)
else:
- self._auth = aiohttp.BasicAuth(username, password=password)
+ self._auth = httpx.BasicAuth(username=username, password=password)
else:
self._auth = None
@@ -119,53 +125,45 @@ def camera_image(self):
).result()
async def async_camera_image(self):
+ """Wrap _async_camera_image with an asyncio.shield."""
+ # Shield the request because of https://github.com/encode/httpx/issues/1461
+ try:
+ self._last_url, self._last_image = await asyncio.shield(
+ self._async_camera_image()
+ )
+ except asyncio.CancelledError as err:
+ _LOGGER.warning("Timeout getting camera image from %s", self._name)
+ raise err
+ return self._last_image
+
+ async def _async_camera_image(self):
"""Return a still image response from the camera."""
try:
url = self._still_image_url.async_render(parse_result=False)
except TemplateError as err:
_LOGGER.error("Error parsing template %s: %s", self._still_image_url, err)
- return self._last_image
+ return self._last_url, self._last_image
if url == self._last_url and self._limit_refetch:
- return self._last_image
-
- # aiohttp don't support DigestAuth yet
- if self._authentication == HTTP_DIGEST_AUTHENTICATION:
-
- def fetch():
- """Read image from a URL."""
- try:
- response = requests.get(
- url, timeout=10, auth=self._auth, verify=self.verify_ssl
- )
- return response.content
- except requests.exceptions.RequestException as error:
- _LOGGER.error(
- "Error getting new camera image from %s: %s", self._name, error
- )
- return self._last_image
-
- self._last_image = await self.hass.async_add_executor_job(fetch)
- # async
- else:
- try:
- websession = async_get_clientsession(
- self.hass, verify_ssl=self.verify_ssl
- )
- with async_timeout.timeout(10):
- response = await websession.get(url, auth=self._auth)
- self._last_image = await response.read()
- except asyncio.TimeoutError:
- _LOGGER.error("Timeout getting camera image from %s", self._name)
- return self._last_image
- except aiohttp.ClientError as err:
- _LOGGER.error(
- "Error getting new camera image from %s: %s", self._name, err
- )
- return self._last_image
-
- self._last_url = url
- return self._last_image
+ return self._last_url, self._last_image
+ response = None
+ try:
+ async_client = get_async_client(self.hass, verify_ssl=self.verify_ssl)
+ response = await async_client.get(
+ url, auth=self._auth, timeout=GET_IMAGE_TIMEOUT
+ )
+ response.raise_for_status()
+ image = response.content
+ except httpx.TimeoutException:
+ _LOGGER.error("Timeout getting camera image from %s", self._name)
+ return self._last_url, self._last_image
+ except (httpx.RequestError, httpx.HTTPStatusError) as err:
+ _LOGGER.error("Error getting new camera image from %s: %s", self._name, err)
+ return self._last_url, self._last_image
+ finally:
+ if response:
+ await response.aclose()
+ return url, image
@property
def name(self):
diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py
index 433e91104ad2de..e83852d122fa05 100644
--- a/homeassistant/components/generic_thermostat/climate.py
+++ b/homeassistant/components/generic_thermostat/climate.py
@@ -1,6 +1,7 @@
"""Adds support for generic thermostat units."""
import asyncio
import logging
+import math
import voluptuous as vol
@@ -23,6 +24,7 @@
ATTR_ENTITY_ID,
ATTR_TEMPERATURE,
CONF_NAME,
+ CONF_UNIQUE_ID,
EVENT_HOMEASSISTANT_START,
PRECISION_HALVES,
PRECISION_TENTHS,
@@ -34,6 +36,7 @@
STATE_UNKNOWN,
)
from homeassistant.core import DOMAIN as HA_DOMAIN, CoreState, callback
+from homeassistant.exceptions import ConditionError
from homeassistant.helpers import condition
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import (
@@ -85,6 +88,7 @@
vol.Optional(CONF_PRECISION): vol.In(
[PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]
),
+ vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)
@@ -109,6 +113,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
away_temp = config.get(CONF_AWAY_TEMP)
precision = config.get(CONF_PRECISION)
unit = hass.config.units.temperature_unit
+ unique_id = config.get(CONF_UNIQUE_ID)
async_add_entities(
[
@@ -128,6 +133,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
away_temp,
precision,
unit,
+ unique_id,
)
]
)
@@ -153,6 +159,7 @@ def __init__(
away_temp,
precision,
unit,
+ unique_id,
):
"""Initialize the thermostat."""
self._name = name
@@ -177,6 +184,7 @@ def __init__(
self._max_temp = max_temp
self._target_temp = target_temp
self._unit = unit
+ self._unique_id = unique_id
self._support_flags = SUPPORT_FLAGS
if away_temp:
self._support_flags = SUPPORT_FLAGS | SUPPORT_PRESET_MODE
@@ -259,6 +267,14 @@ def _async_startup(*_):
if not self._hvac_mode:
self._hvac_mode = HVAC_MODE_OFF
+ # Prevent the device from keep running if HVAC_MODE_OFF
+ if self._hvac_mode == HVAC_MODE_OFF and self._is_device_active:
+ await self._async_heater_turn_off()
+ _LOGGER.warning(
+ "The climate mode is OFF, but the switch device is ON. Turning off device %s",
+ self.heater_entity_id,
+ )
+
@property
def should_poll(self):
"""Return the polling state."""
@@ -269,6 +285,11 @@ def name(self):
"""Return the name of the thermostat."""
return self._name
+ @property
+ def unique_id(self):
+ """Return the unique id of this thermostat."""
+ return self._unique_id
+
@property
def precision(self):
"""Return the precision of the system."""
@@ -399,14 +420,21 @@ def _async_switch_changed(self, event):
def _async_update_temp(self, state):
"""Update thermostat with latest state from sensor."""
try:
- self._cur_temp = float(state.state)
+ cur_temp = float(state.state)
+ if math.isnan(cur_temp) or math.isinf(cur_temp):
+ raise ValueError(f"Sensor has illegal state {state.state}")
+ self._cur_temp = cur_temp
except ValueError as ex:
_LOGGER.error("Unable to update from sensor: %s", ex)
async def _async_control_heating(self, time=None, force=False):
"""Check if we need to turn heating on or off."""
async with self._temp_lock:
- if not self._active and None not in (self._cur_temp, self._target_temp):
+ if not self._active and None not in (
+ self._cur_temp,
+ self._target_temp,
+ self._is_device_active,
+ ):
self._active = True
_LOGGER.info(
"Obtained current and target temperature. "
@@ -418,24 +446,27 @@ async def _async_control_heating(self, time=None, force=False):
if not self._active or self._hvac_mode == HVAC_MODE_OFF:
return
- if not force and time is None:
- # If the `force` argument is True, we
- # ignore `min_cycle_duration`.
- # If the `time` argument is not none, we were invoked for
- # keep-alive purposes, and `min_cycle_duration` is irrelevant.
- if self.min_cycle_duration:
- if self._is_device_active:
- current_state = STATE_ON
- else:
- current_state = HVAC_MODE_OFF
+ # If the `force` argument is True, we
+ # ignore `min_cycle_duration`.
+ # If the `time` argument is not none, we were invoked for
+ # keep-alive purposes, and `min_cycle_duration` is irrelevant.
+ if not force and time is None and self.min_cycle_duration:
+ if self._is_device_active:
+ current_state = STATE_ON
+ else:
+ current_state = HVAC_MODE_OFF
+ try:
long_enough = condition.state(
self.hass,
self.heater_entity_id,
current_state,
self.min_cycle_duration,
)
- if not long_enough:
- return
+ except ConditionError:
+ long_enough = False
+
+ if not long_enough:
+ return
too_cold = self._target_temp >= self._cur_temp + self._cold_tolerance
too_hot = self._cur_temp >= self._target_temp + self._hot_tolerance
@@ -464,6 +495,9 @@ async def _async_control_heating(self, time=None, force=False):
@property
def _is_device_active(self):
"""If the toggleable device is currently active."""
+ if not self.hass.states.get(self.heater_entity_id):
+ return None
+
return self.hass.states.is_state(self.heater_entity_id, STATE_ON)
@property
diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py
index 909de81521c109..f1d2a1d47c1bd8 100644
--- a/homeassistant/components/geniushub/__init__.py
+++ b/homeassistant/components/geniushub/__init__.py
@@ -1,7 +1,9 @@
"""Support for a Genius Hub system."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Any, Dict, Optional
+from typing import Any
import aiohttp
from geniushubclient import GeniusHub
@@ -173,7 +175,6 @@ def __init__(self, hass, client, hub_uid) -> None:
@property
def hub_uid(self) -> int:
"""Return the Hub UID (MAC address)."""
- # pylint: disable=no-member
return self._hub_uid if self._hub_uid is not None else self.client.uid
async def async_update(self, now, **kwargs) -> None:
@@ -219,12 +220,12 @@ async def async_added_to_hass(self) -> None:
"""Set up a listener when this entity is added to HA."""
self.async_on_remove(async_dispatcher_connect(self.hass, DOMAIN, self._refresh))
- async def _refresh(self, payload: Optional[dict] = None) -> None:
+ async def _refresh(self, payload: dict | None = None) -> None:
"""Process any signals."""
self.async_schedule_update_ha_state(force_refresh=True)
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID."""
return self._unique_id
@@ -251,7 +252,7 @@ def __init__(self, broker, device) -> None:
self._last_comms = self._state_attr = None
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the device state attributes."""
attrs = {}
attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"]
@@ -286,7 +287,7 @@ def __init__(self, broker, zone) -> None:
self._zone = zone
self._unique_id = f"{broker.hub_uid}_zone_{zone.id}"
- async def _refresh(self, payload: Optional[dict] = None) -> None:
+ async def _refresh(self, payload: dict | None = None) -> None:
"""Process any signals."""
if payload is None:
self.async_schedule_update_ha_state(force_refresh=True)
@@ -318,7 +319,7 @@ def name(self) -> str:
return self._zone.name
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the device state attributes."""
status = {k: v for k, v in self._zone.data.items() if k in GH_ZONE_ATTRS}
return {"status": status}
@@ -334,7 +335,7 @@ def __init__(self, broker, zone) -> None:
self._max_temp = self._min_temp = self._supported_features = None
@property
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self._zone.data.get("temperature")
diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py
index 70d08dc2d1fa69..089fd96483555e 100644
--- a/homeassistant/components/geniushub/climate.py
+++ b/homeassistant/components/geniushub/climate.py
@@ -1,5 +1,5 @@
"""Support for Genius Hub climate devices."""
-from typing import List, Optional
+from __future__ import annotations
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
@@ -67,12 +67,12 @@ def hvac_mode(self) -> str:
return GH_HVAC_TO_HA.get(self._zone.data["mode"], HVAC_MODE_HEAT)
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes."""
return list(HA_HVAC_TO_GH)
@property
- def hvac_action(self) -> Optional[str]:
+ def hvac_action(self) -> str | None:
"""Return the current running hvac operation if supported."""
if "_state" in self._zone.data: # only for v3 API
if not self._zone.data["_state"].get("bIsActive"):
@@ -83,12 +83,12 @@ def hvac_action(self) -> Optional[str]:
return None
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp."""
return GH_PRESET_TO_HA.get(self._zone.data["mode"])
@property
- def preset_modes(self) -> Optional[List[str]]:
+ def preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes."""
if "occupied" in self._zone.data: # if has a movement sensor
return [PRESET_ACTIVITY, PRESET_BOOST]
diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py
index 7e4fe81fc7781a..3234ccd577ff8c 100644
--- a/homeassistant/components/geniushub/sensor.py
+++ b/homeassistant/components/geniushub/sensor.py
@@ -1,7 +1,10 @@
"""Support for Genius Hub sensor devices."""
+from __future__ import annotations
+
from datetime import timedelta
-from typing import Any, Dict
+from typing import Any
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
import homeassistant.util.dt as dt_util
@@ -36,7 +39,7 @@ async def async_setup_platform(
async_add_entities(sensors + issues, update_before_add=True)
-class GeniusBattery(GeniusDevice):
+class GeniusBattery(GeniusDevice, SensorEntity):
"""Representation of a Genius Hub sensor."""
def __init__(self, broker, device, state_attr) -> None:
@@ -86,7 +89,7 @@ def state(self) -> str:
return level if level != 255 else 0
-class GeniusIssue(GeniusEntity):
+class GeniusIssue(GeniusEntity, SensorEntity):
"""Representation of a Genius Hub sensor."""
def __init__(self, broker, level) -> None:
@@ -106,7 +109,7 @@ def state(self) -> str:
return len(self._issues)
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the device state attributes."""
return {f"{self._level}_list": self._issues}
diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py
index 51fdce4a6d7c59..bb775432d8e15d 100644
--- a/homeassistant/components/geniushub/water_heater.py
+++ b/homeassistant/components/geniushub/water_heater.py
@@ -1,5 +1,5 @@
"""Support for Genius Hub water_heater devices."""
-from typing import List
+from __future__ import annotations
from homeassistant.components.water_heater import (
SUPPORT_OPERATION_MODE,
@@ -61,7 +61,7 @@ def __init__(self, broker, zone) -> None:
self._supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
@property
- def operation_list(self) -> List[str]:
+ def operation_list(self) -> list[str]:
"""Return the list of available operation modes."""
return list(HA_OPMODE_TO_GH)
diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py
index bb2d86539e98a2..cfd58124c16e8b 100644
--- a/homeassistant/components/geo_json_events/geo_location.py
+++ b/homeassistant/components/geo_json_events/geo_location.py
@@ -1,7 +1,8 @@
"""Support for generic GeoJSON events."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Optional
from geojson_client.generic_feed import GenericFeedManager
import voluptuous as vol
@@ -144,7 +145,7 @@ def _delete_callback(self):
"""Remove this entity."""
self._remove_signal_delete()
self._remove_signal_update()
- self.hass.async_create_task(self.async_remove())
+ self.hass.async_create_task(self.async_remove(force_remove=True))
@callback
def _update_callback(self):
@@ -176,22 +177,22 @@ def source(self) -> str:
return SOURCE
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the entity."""
return self._name
@property
- def distance(self) -> Optional[float]:
+ def distance(self) -> float | None:
"""Return distance value of this external event."""
return self._distance
@property
- def latitude(self) -> Optional[float]:
+ def latitude(self) -> float | None:
"""Return latitude value of this external event."""
return self._latitude
@property
- def longitude(self) -> Optional[float]:
+ def longitude(self) -> float | None:
"""Return longitude value of this external event."""
return self._longitude
@@ -201,7 +202,7 @@ def unit_of_measurement(self):
return LENGTH_KILOMETERS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
if not self._external_id:
return {}
diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py
index 6142fa222095d7..11294e73f63519 100644
--- a/homeassistant/components/geo_location/__init__.py
+++ b/homeassistant/components/geo_location/__init__.py
@@ -1,7 +1,9 @@
"""Support for Geolocation."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Optional
+from typing import final
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
from homeassistant.helpers.config_validation import ( # noqa: F401
@@ -45,7 +47,7 @@ async def async_unload_entry(hass, entry):
class GeolocationEvent(Entity):
- """This represents an external event with an associated geolocation."""
+ """Base class for an external event with an associated geolocation."""
@property
def state(self):
@@ -60,20 +62,21 @@ def source(self) -> str:
raise NotImplementedError
@property
- def distance(self) -> Optional[float]:
+ def distance(self) -> float | None:
"""Return distance value of this external event."""
return None
@property
- def latitude(self) -> Optional[float]:
+ def latitude(self) -> float | None:
"""Return latitude value of this external event."""
return None
@property
- def longitude(self) -> Optional[float]:
+ def longitude(self) -> float | None:
"""Return longitude value of this external event."""
return None
+ @final
@property
def state_attributes(self):
"""Return the state attributes of this external event."""
diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py
index aad281da117355..3cdefccfeab0d5 100644
--- a/homeassistant/components/geo_location/trigger.py
+++ b/homeassistant/components/geo_location/trigger.py
@@ -33,6 +33,7 @@ def source_match(state, source):
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
source = config.get(CONF_SOURCE).lower()
zone_entity_id = config.get(CONF_ZONE)
trigger_event = config.get(CONF_EVENT)
@@ -48,8 +49,11 @@ def state_change_listener(event):
return
zone_state = hass.states.get(zone_entity_id)
- from_match = condition.zone(hass, zone_state, from_state)
- to_match = condition.zone(hass, zone_state, to_state)
+
+ from_match = (
+ condition.zone(hass, zone_state, from_state) if from_state else False
+ )
+ to_match = condition.zone(hass, zone_state, to_state) if to_state else False
if (
trigger_event == EVENT_ENTER
@@ -71,6 +75,7 @@ def state_change_listener(event):
"zone": zone_state,
"event": trigger_event,
"description": f"geo_location - {source}",
+ "id": trigger_id,
}
},
event.context,
diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py
index c75234f5f2b312..df5f11850fdef3 100644
--- a/homeassistant/components/geo_rss_events/sensor.py
+++ b/homeassistant/components/geo_rss_events/sensor.py
@@ -12,7 +12,7 @@
from georss_generic_client import GenericFeed
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_LATITUDE,
CONF_LONGITUDE,
@@ -23,7 +23,6 @@
LENGTH_KILOMETERS,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -96,7 +95,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(devices, True)
-class GeoRssServiceSensor(Entity):
+class GeoRssServiceSensor(SensorEntity):
"""Representation of a Sensor."""
def __init__(
@@ -137,7 +136,7 @@ def icon(self):
return DEFAULT_ICON
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._state_attributes
diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py
index f090f516fb1e1f..5a58e73d44a423 100644
--- a/homeassistant/components/geofency/device_tracker.py
+++ b/homeassistant/components/geofency/device_tracker.py
@@ -56,7 +56,7 @@ def __init__(self, device, gps=None, location_name=None, attributes=None):
self._unique_id = device
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific attributes."""
return self._attributes
diff --git a/homeassistant/components/geofency/translations/de.json b/homeassistant/components/geofency/translations/de.json
index 31b8a5eb321501..9c3fd3ea1b0de7 100644
--- a/homeassistant/components/geofency/translations/de.json
+++ b/homeassistant/components/geofency/translations/de.json
@@ -1,5 +1,9 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.",
+ "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen."
+ },
"create_entry": {
"default": "Um Ereignisse an den Home Assistant zu senden, musst das Webhook Feature in Geofency konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})."
},
diff --git a/homeassistant/components/geofency/translations/hu.json b/homeassistant/components/geofency/translations/hu.json
index 99dc0feceadb7d..826b943e2f80ad 100644
--- a/homeassistant/components/geofency/translations/hu.json
+++ b/homeassistant/components/geofency/translations/hu.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.",
+ "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz."
+ },
"create_entry": {
- "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a Geofencyben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3] ( {docs_url} ) linken tal\u00e1lhat\u00f3k."
+ "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a Geofencyben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k."
},
"step": {
"user": {
diff --git a/homeassistant/components/geofency/translations/id.json b/homeassistant/components/geofency/translations/id.json
new file mode 100644
index 00000000000000..0e5163b96cd5c2
--- /dev/null
+++ b/homeassistant/components/geofency/translations/id.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.",
+ "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook."
+ },
+ "create_entry": {
+ "default": "Untuk mengirim event ke Home Assistant, Anda harus menyiapkan fitur webhook di Geofency.\n\nIsi info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nBaca [dokumentasi]({docs_url}) tentang detail lebih lanjut."
+ },
+ "step": {
+ "user": {
+ "description": "Yakin ingin menyiapkan Geofency Webhook?",
+ "title": "Siapkan Geofency Webhook"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geofency/translations/ko.json b/homeassistant/components/geofency/translations/ko.json
index 8bab38a8a34591..b9303110e358c7 100644
--- a/homeassistant/components/geofency/translations/ko.json
+++ b/homeassistant/components/geofency/translations/ko.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4."
+ },
"create_entry": {
- "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Geofency \uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
+ "default": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Geofency\uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
},
"step": {
"user": {
diff --git a/homeassistant/components/geofency/translations/nl.json b/homeassistant/components/geofency/translations/nl.json
index 763d903a8ba5f1..59ed1cf6b5bf1b 100644
--- a/homeassistant/components/geofency/translations/nl.json
+++ b/homeassistant/components/geofency/translations/nl.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk."
+ "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.",
+ "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen."
},
"create_entry": {
"default": "Om locaties naar Home Assistant te sturen, moet u de Webhook-functie instellen in Geofency.\n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Methode: POST \n\n Zie [de documentatie]({docs_url}) voor meer informatie."
diff --git a/homeassistant/components/geofency/translations/tr.json b/homeassistant/components/geofency/translations/tr.json
new file mode 100644
index 00000000000000..84adcdf8225c43
--- /dev/null
+++ b/homeassistant/components/geofency/translations/tr.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.",
+ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geofency/translations/uk.json b/homeassistant/components/geofency/translations/uk.json
new file mode 100644
index 00000000000000..54a14afb764d9d
--- /dev/null
+++ b/homeassistant/components/geofency/translations/uk.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.",
+ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c."
+ },
+ "create_entry": {
+ "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f Geofency. \n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457."
+ },
+ "step": {
+ "user": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Geofency?",
+ "title": "Geofency"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_quakes/config_flow.py b/homeassistant/components/geonetnz_quakes/config_flow.py
index f3f5829f465a40..735c6cd6d9fe36 100644
--- a/homeassistant/components/geonetnz_quakes/config_flow.py
+++ b/homeassistant/components/geonetnz_quakes/config_flow.py
@@ -12,7 +12,7 @@
)
from homeassistant.helpers import config_validation as cv
-from .const import ( # pylint: disable=unused-import
+from .const import (
CONF_MINIMUM_MAGNITUDE,
CONF_MMI,
DEFAULT_MINIMUM_MAGNITUDE,
diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py
index ed0b9f9f714453..1643264fd751e7 100644
--- a/homeassistant/components/geonetnz_quakes/geo_location.py
+++ b/homeassistant/components/geonetnz_quakes/geo_location.py
@@ -1,6 +1,7 @@
"""Geolocation support for GeoNet NZ Quakes Feeds."""
+from __future__ import annotations
+
import logging
-from typing import Optional
from homeassistant.components.geo_location import GeolocationEvent
from homeassistant.const import (
@@ -102,7 +103,7 @@ async def async_will_remove_from_hass(self) -> None:
@callback
def _delete_callback(self):
"""Remove this entity."""
- self.hass.async_create_task(self.async_remove())
+ self.hass.async_create_task(self.async_remove(force_remove=True))
@callback
def _update_callback(self):
@@ -142,7 +143,7 @@ def _update_from_feed(self, feed_entry):
self._time = feed_entry.time
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID containing latitude/longitude and external id."""
return f"{self._integration_id}_{self._external_id}"
@@ -157,22 +158,22 @@ def source(self) -> str:
return SOURCE
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the entity."""
return self._title
@property
- def distance(self) -> Optional[float]:
+ def distance(self) -> float | None:
"""Return distance value of this external event."""
return self._distance
@property
- def latitude(self) -> Optional[float]:
+ def latitude(self) -> float | None:
"""Return latitude value of this external event."""
return self._latitude
@property
- def longitude(self) -> Optional[float]:
+ def longitude(self) -> float | None:
"""Return longitude value of this external event."""
return self._longitude
@@ -184,7 +185,7 @@ def unit_of_measurement(self):
return LENGTH_KILOMETERS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
for key, value in (
diff --git a/homeassistant/components/geonetnz_quakes/sensor.py b/homeassistant/components/geonetnz_quakes/sensor.py
index 1cb2d0dc0915fd..94c7965663af3f 100644
--- a/homeassistant/components/geonetnz_quakes/sensor.py
+++ b/homeassistant/components/geonetnz_quakes/sensor.py
@@ -1,10 +1,11 @@
"""Feed Entity Manager Sensor support for GeoNet NZ Quakes Feeds."""
+from __future__ import annotations
+
import logging
-from typing import Optional
+from homeassistant.components.sensor import SensorEntity
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from homeassistant.util import dt
from .const import DOMAIN, FEED
@@ -34,7 +35,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
_LOGGER.debug("Sensor setup done")
-class GeonetnzQuakesSensor(Entity):
+class GeonetnzQuakesSensor(SensorEntity):
"""This is a status sensor for the GeoNet NZ Quakes integration."""
def __init__(self, config_entry_id, config_unique_id, config_title, manager):
@@ -115,7 +116,7 @@ def unique_id(self) -> str:
return self._config_unique_id
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the entity."""
return f"GeoNet NZ Quakes ({self._config_title})"
@@ -130,7 +131,7 @@ def unit_of_measurement(self):
return DEFAULT_UNIT_OF_MEASUREMENT
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
for key, value in (
diff --git a/homeassistant/components/geonetnz_quakes/translations/hu.json b/homeassistant/components/geonetnz_quakes/translations/hu.json
index 4a163d24b75925..21a38c18e28344 100644
--- a/homeassistant/components/geonetnz_quakes/translations/hu.json
+++ b/homeassistant/components/geonetnz_quakes/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/geonetnz_quakes/translations/id.json b/homeassistant/components/geonetnz_quakes/translations/id.json
new file mode 100644
index 00000000000000..7a4e340e230e68
--- /dev/null
+++ b/homeassistant/components/geonetnz_quakes/translations/id.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "mmi": "MMI",
+ "radius": "Radius"
+ },
+ "title": "Isi detail filter Anda."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_quakes/translations/ko.json b/homeassistant/components/geonetnz_quakes/translations/ko.json
index b231629e8567a7..277aa945792bcb 100644
--- a/homeassistant/components/geonetnz_quakes/translations/ko.json
+++ b/homeassistant/components/geonetnz_quakes/translations/ko.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
diff --git a/homeassistant/components/geonetnz_quakes/translations/nl.json b/homeassistant/components/geonetnz_quakes/translations/nl.json
index 865860a5adfc4b..74766300e11176 100644
--- a/homeassistant/components/geonetnz_quakes/translations/nl.json
+++ b/homeassistant/components/geonetnz_quakes/translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Locatie is al geconfigureerd."
+ "already_configured": "Service is al geconfigureerd"
},
"step": {
"user": {
diff --git a/homeassistant/components/geonetnz_quakes/translations/tr.json b/homeassistant/components/geonetnz_quakes/translations/tr.json
new file mode 100644
index 00000000000000..717f6d72b94e5d
--- /dev/null
+++ b/homeassistant/components/geonetnz_quakes/translations/tr.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_quakes/translations/uk.json b/homeassistant/components/geonetnz_quakes/translations/uk.json
new file mode 100644
index 00000000000000..35653baa945886
--- /dev/null
+++ b/homeassistant/components/geonetnz_quakes/translations/uk.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "mmi": "MMI",
+ "radius": "\u0420\u0430\u0434\u0456\u0443\u0441"
+ },
+ "title": "GeoNet NZ Quakes"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_quakes/translations/zh-Hant.json b/homeassistant/components/geonetnz_quakes/translations/zh-Hant.json
index f022792121c942..dd6e15b82d1ab9 100644
--- a/homeassistant/components/geonetnz_quakes/translations/zh-Hant.json
+++ b/homeassistant/components/geonetnz_quakes/translations/zh-Hant.json
@@ -9,7 +9,7 @@
"mmi": "MMI",
"radius": "\u534a\u5f91"
},
- "title": "\u586b\u5beb\u904e\u6ffe\u5668\u8cc7\u8a0a\u3002"
+ "title": "\u586b\u5beb\u7be9\u9078\u5668\u8cc7\u8a0a\u3002"
}
}
}
diff --git a/homeassistant/components/geonetnz_volcano/__init__.py b/homeassistant/components/geonetnz_volcano/__init__.py
index e2c6cb77083f49..c3db7770499f95 100644
--- a/homeassistant/components/geonetnz_volcano/__init__.py
+++ b/homeassistant/components/geonetnz_volcano/__init__.py
@@ -1,8 +1,9 @@
"""The GeoNet NZ Volcano integration."""
+from __future__ import annotations
+
import asyncio
from datetime import datetime, timedelta
import logging
-from typing import Optional
from aio_geojson_geonetnz_volcano import GeonetnzVolcanoFeedManager
import voluptuous as vol
@@ -172,11 +173,11 @@ def get_entry(self, external_id):
"""Get feed entry by external id."""
return self._feed_manager.feed_entries.get(external_id)
- def last_update(self) -> Optional[datetime]:
+ def last_update(self) -> datetime | None:
"""Return the last update of this feed."""
return self._feed_manager.last_update
- def last_update_successful(self) -> Optional[datetime]:
+ def last_update_successful(self) -> datetime | None:
"""Return the last successful update of this feed."""
return self._feed_manager.last_update_successful
diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py
index 3d5d0681f02222..c0cc68014378ae 100644
--- a/homeassistant/components/geonetnz_volcano/sensor.py
+++ b/homeassistant/components/geonetnz_volcano/sensor.py
@@ -1,7 +1,9 @@
"""Feed Entity Manager Sensor support for GeoNet NZ Volcano Feeds."""
+from __future__ import annotations
+
import logging
-from typing import Optional
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_LATITUDE,
@@ -11,7 +13,6 @@
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from homeassistant.util import dt
from homeassistant.util.unit_system import IMPERIAL_SYSTEM
@@ -53,7 +54,7 @@ def async_add_sensor(feed_manager, external_id, unit_system):
_LOGGER.debug("Sensor setup done")
-class GeonetnzVolcanoSensor(Entity):
+class GeonetnzVolcanoSensor(SensorEntity):
"""This represents an external event with GeoNet NZ Volcano feed data."""
def __init__(self, config_entry_id, feed_manager, external_id, unit_system):
@@ -139,7 +140,7 @@ def icon(self):
return DEFAULT_ICON
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the entity."""
return f"Volcano {self._title}"
@@ -149,7 +150,7 @@ def unit_of_measurement(self):
return "alert level"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
for key, value in (
diff --git a/homeassistant/components/geonetnz_volcano/translations/de.json b/homeassistant/components/geonetnz_volcano/translations/de.json
index b573d93cd5a570..a29555e53ab71d 100644
--- a/homeassistant/components/geonetnz_volcano/translations/de.json
+++ b/homeassistant/components/geonetnz_volcano/translations/de.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Standort ist bereits konfiguriert"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/geonetnz_volcano/translations/hu.json b/homeassistant/components/geonetnz_volcano/translations/hu.json
index 42de5a1314239b..dadc8142d7efc7 100644
--- a/homeassistant/components/geonetnz_volcano/translations/hu.json
+++ b/homeassistant/components/geonetnz_volcano/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "A hely m\u00e1r konfigur\u00e1lva van"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/geonetnz_volcano/translations/id.json b/homeassistant/components/geonetnz_volcano/translations/id.json
new file mode 100644
index 00000000000000..5dd4414ca62176
--- /dev/null
+++ b/homeassistant/components/geonetnz_volcano/translations/id.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Lokasi sudah dikonfigurasi"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Radius"
+ },
+ "title": "Isi detail filter Anda."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_volcano/translations/ko.json b/homeassistant/components/geonetnz_volcano/translations/ko.json
index 26e83789e8f9a8..6d743c3a18de4d 100644
--- a/homeassistant/components/geonetnz_volcano/translations/ko.json
+++ b/homeassistant/components/geonetnz_volcano/translations/ko.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/geonetnz_volcano/translations/tr.json b/homeassistant/components/geonetnz_volcano/translations/tr.json
new file mode 100644
index 00000000000000..980be333568596
--- /dev/null
+++ b/homeassistant/components/geonetnz_volcano/translations/tr.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "Yar\u0131\u00e7ap"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_volcano/translations/uk.json b/homeassistant/components/geonetnz_volcano/translations/uk.json
new file mode 100644
index 00000000000000..77a4f1eee68568
--- /dev/null
+++ b/homeassistant/components/geonetnz_volcano/translations/uk.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "radius": "\u0420\u0430\u0434\u0456\u0443\u0441"
+ },
+ "title": "GeoNet NZ Volcano"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_volcano/translations/zh-Hant.json b/homeassistant/components/geonetnz_volcano/translations/zh-Hant.json
index 9dd69261e59704..9c0e9a3df1d82c 100644
--- a/homeassistant/components/geonetnz_volcano/translations/zh-Hant.json
+++ b/homeassistant/components/geonetnz_volcano/translations/zh-Hant.json
@@ -8,7 +8,7 @@
"data": {
"radius": "\u534a\u5f91"
},
- "title": "\u586b\u5beb\u904e\u6ffe\u5668\u8cc7\u8a0a\u3002"
+ "title": "\u586b\u5beb\u7be9\u9078\u5668\u8cc7\u8a0a\u3002"
}
}
}
diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py
index 005cc4c9c260a6..f25f7e76f59efa 100644
--- a/homeassistant/components/gios/__init__.py
+++ b/homeassistant/components/gios/__init__.py
@@ -5,8 +5,6 @@
from async_timeout import timeout
from gios import ApiError, Gios, InvalidSensorsData, NoStationError
-from homeassistant.core import Config, HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -15,11 +13,6 @@
_LOGGER = logging.getLogger(__name__)
-async def async_setup(hass: HomeAssistant, config: Config) -> bool:
- """Set up configured GIOS."""
- return True
-
-
async def async_setup_entry(hass, config_entry):
"""Set up GIOS as config entry."""
station_id = config_entry.data[CONF_STATION_ID]
@@ -28,10 +21,7 @@ async def async_setup_entry(hass, config_entry):
websession = async_get_clientsession(hass)
coordinator = GiosDataUpdateCoordinator(hass, websession, station_id)
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = coordinator
diff --git a/homeassistant/components/gios/air_quality.py b/homeassistant/components/gios/air_quality.py
index 2853570ce58eb0..ab83191a1ac0d8 100644
--- a/homeassistant/components/gios/air_quality.py
+++ b/homeassistant/components/gios/air_quality.py
@@ -131,7 +131,7 @@ def device_info(self):
}
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
# Different measuring stations have different sets of sensors. We don't know
# what data we will get.
diff --git a/homeassistant/components/gios/config_flow.py b/homeassistant/components/gios/config_flow.py
index dfef829c479f20..d2a9f8b73fcda4 100644
--- a/homeassistant/components/gios/config_flow.py
+++ b/homeassistant/components/gios/config_flow.py
@@ -10,7 +10,7 @@
from homeassistant.const import CONF_NAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from .const import CONF_STATION_ID, DEFAULT_NAME, DOMAIN # pylint:disable=unused-import
+from .const import CONF_STATION_ID, DEFAULT_NAME, DOMAIN
DATA_SCHEMA = vol.Schema(
{
diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py
index 117eada036b2a6..ab354e319a8232 100644
--- a/homeassistant/components/gios/const.py
+++ b/homeassistant/components/gios/const.py
@@ -1,7 +1,6 @@
"""Constants for GIOS integration."""
from datetime import timedelta
-ATTR_NAME = "name"
ATTR_STATION = "station"
CONF_STATION_ID = "station_id"
DEFAULT_NAME = "GIOŚ"
diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json
index 468e22260b514a..3f520525a5a0f7 100644
--- a/homeassistant/components/gios/manifest.json
+++ b/homeassistant/components/gios/manifest.json
@@ -3,7 +3,7 @@
"name": "GIOŚ",
"documentation": "https://www.home-assistant.io/integrations/gios",
"codeowners": ["@bieniu"],
- "requirements": ["gios==0.1.5"],
+ "requirements": ["gios==0.2.1"],
"config_flow": true,
"quality_scale": "platinum"
}
diff --git a/homeassistant/components/gios/translations/de.json b/homeassistant/components/gios/translations/de.json
index 0a5cea1819dbc7..7bbb01cf18db27 100644
--- a/homeassistant/components/gios/translations/de.json
+++ b/homeassistant/components/gios/translations/de.json
@@ -4,14 +4,14 @@
"already_configured": "GIO\u015a integration f\u00fcr diese Messstation ist bereits konfiguriert. "
},
"error": {
- "cannot_connect": "Es kann keine Verbindung zum GIO\u015a-Server hergestellt werden.",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_sensors_data": "Ung\u00fcltige Sensordaten f\u00fcr diese Messstation.",
"wrong_station_id": "ID der Messstation ist nicht korrekt."
},
"step": {
"user": {
"data": {
- "name": "Name der Integration",
+ "name": "Name",
"station_id": "ID der Messstation"
},
"description": "Einrichtung von GIO\u015a (Polnische Hauptinspektion f\u00fcr Umweltschutz) Integration der Luftqualit\u00e4t. Wenn du Hilfe bei der Konfiguration ben\u00f6tigst, schaue hier: https://www.home-assistant.io/integrations/gios",
diff --git a/homeassistant/components/gios/translations/fr.json b/homeassistant/components/gios/translations/fr.json
index b06c41208bc374..2b02b5cfea086d 100644
--- a/homeassistant/components/gios/translations/fr.json
+++ b/homeassistant/components/gios/translations/fr.json
@@ -18,5 +18,10 @@
"title": "GIO\u015a (Inspection g\u00e9n\u00e9rale polonaise de la protection de l'environnement)"
}
}
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "Acc\u00e9der au serveur GIO\u015a"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/gios/translations/hu.json b/homeassistant/components/gios/translations/hu.json
index 5702d3b33d265f..b35904e9d7623c 100644
--- a/homeassistant/components/gios/translations/hu.json
+++ b/homeassistant/components/gios/translations/hu.json
@@ -1,17 +1,17 @@
{
"config": {
"abort": {
- "already_configured": "A GIO\u015a integr\u00e1ci\u00f3 ehhez a m\u00e9r\u0151\u00e1llom\u00e1shoz m\u00e1r konfigur\u00e1lva van."
+ "already_configured": "A hely m\u00e1r konfigur\u00e1lva van"
},
"error": {
- "cannot_connect": "Nem lehet csatlakozni a GIO\u015a szerverhez.",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
"invalid_sensors_data": "\u00c9rv\u00e9nytelen \u00e9rz\u00e9kel\u0151k adatai ehhez a m\u00e9r\u0151\u00e1llom\u00e1shoz.",
"wrong_station_id": "A m\u00e9r\u0151\u00e1llom\u00e1s azonos\u00edt\u00f3ja nem megfelel\u0151."
},
"step": {
"user": {
"data": {
- "name": "Az integr\u00e1ci\u00f3 neve",
+ "name": "N\u00e9v",
"station_id": "A m\u00e9r\u0151\u00e1llom\u00e1s azonos\u00edt\u00f3ja"
},
"description": "A GIO\u015a (lengyel k\u00f6rnyezetv\u00e9delmi f\u0151fel\u00fcgyel\u0151) leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Ha seg\u00edts\u00e9gre van sz\u00fcks\u00e9ged a konfigur\u00e1ci\u00f3val kapcsolatban, l\u00e1togass ide: https://www.home-assistant.io/integrations/gios",
diff --git a/homeassistant/components/gios/translations/id.json b/homeassistant/components/gios/translations/id.json
new file mode 100644
index 00000000000000..b32210c30d5026
--- /dev/null
+++ b/homeassistant/components/gios/translations/id.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Lokasi sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_sensors_data": "Data sensor tidak valid untuk stasiun pengukuran ini.",
+ "wrong_station_id": "ID stasiun pengukuran salah."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Nama",
+ "station_id": "ID stasiun pengukuran"
+ },
+ "description": "Siapkan integrasi kualitas udara GIO\u015a (Inspektorat Jenderal Perlindungan Lingkungan Polandia). Jika Anda memerlukan bantuan tentang konfigurasi, baca di sini: https://www.home-assistant.io/integrations/gios",
+ "title": "GIO\u015a (Inspektorat Jenderal Perlindungan Lingkungan Polandia)"
+ }
+ }
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "Keterjangkauan server GIO\u015a"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gios/translations/it.json b/homeassistant/components/gios/translations/it.json
index 26bf8386d6669f..5d1e99d17f400f 100644
--- a/homeassistant/components/gios/translations/it.json
+++ b/homeassistant/components/gios/translations/it.json
@@ -21,7 +21,7 @@
},
"system_health": {
"info": {
- "can_reach_server": "Raggiungi il server GIO\u015a"
+ "can_reach_server": "Server GIO\u015a raggiungibile"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/gios/translations/ko.json b/homeassistant/components/gios/translations/ko.json
index 2ad64efadc1de6..e462ef4e3b6565 100644
--- a/homeassistant/components/gios/translations/ko.json
+++ b/homeassistant/components/gios/translations/ko.json
@@ -1,22 +1,27 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc5d0 \ub300\ud55c GIO\u015a \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "GIO\u015a \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_sensors_data": "\uc774 \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc5d0 \ub300\ud55c \uc13c\uc11c \ub370\uc774\ud130\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"wrong_station_id": "\uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc758 ID \uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4."
},
"step": {
"user": {
"data": {
- "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984",
+ "name": "\uc774\ub984",
"station_id": "\uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc758 ID"
},
"description": "\ud3f4\ub780\ub4dc \ud658\uacbd\uccad (GIO\u015a) \ub300\uae30\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. \uad6c\uc131\uc5d0 \ub3c4\uc6c0\uc774 \ud544\uc694\ud55c \uacbd\uc6b0 https://www.home-assistant.io/integrations/gios \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694",
"title": "\ud3f4\ub780\ub4dc \ud658\uacbd\uccad (GIO\u015a)"
}
}
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "GIO\u015a \uc11c\ubc84 \uc5f0\uacb0"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/gios/translations/lb.json b/homeassistant/components/gios/translations/lb.json
index 8e8ab861b437e7..cafea72fb78296 100644
--- a/homeassistant/components/gios/translations/lb.json
+++ b/homeassistant/components/gios/translations/lb.json
@@ -18,5 +18,10 @@
"title": "GIO\u015a (Polnesch Chefinspektorat vum \u00cbmweltschutz)"
}
}
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "GIO\u015a Server ereechbar"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/gios/translations/nl.json b/homeassistant/components/gios/translations/nl.json
index 09fddb56225f7d..87104523a31cf1 100644
--- a/homeassistant/components/gios/translations/nl.json
+++ b/homeassistant/components/gios/translations/nl.json
@@ -1,22 +1,27 @@
{
"config": {
"abort": {
- "already_configured": "GIO\u015a-integratie voor dit meetstation is al geconfigureerd."
+ "already_configured": "Locatie is al geconfigureerd."
},
"error": {
- "cannot_connect": "Kan geen verbinding maken met de GIO\u015a-server.",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_sensors_data": "Ongeldige sensorgegevens voor dit meetstation.",
"wrong_station_id": "ID van het meetstation is niet correct."
},
"step": {
"user": {
"data": {
- "name": "Naam van de integratie",
+ "name": "Naam",
"station_id": "ID van het meetstation"
},
"description": "GIO\u015a (Poolse hoofdinspectie van milieubescherming) luchtkwaliteitintegratie instellen. Als u hulp nodig hebt bij de configuratie, kijk dan hier: https://www.home-assistant.io/integrations/gios",
"title": "GIO\u015a (Poolse hoofdinspectie van milieubescherming)"
}
}
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "GIO\u015a server bereikbaar"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/gios/translations/tr.json b/homeassistant/components/gios/translations/tr.json
new file mode 100644
index 00000000000000..590aec1894cc3b
--- /dev/null
+++ b/homeassistant/components/gios/translations/tr.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gios/translations/uk.json b/homeassistant/components/gios/translations/uk.json
new file mode 100644
index 00000000000000..f62408c5e8ecdb
--- /dev/null
+++ b/homeassistant/components/gios/translations/uk.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_sensors_data": "\u041d\u0435\u0432\u0456\u0440\u043d\u0456 \u0434\u0430\u043d\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432 \u0434\u043b\u044f \u0446\u0456\u0454\u0457 \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043b\u044c\u043d\u043e\u0457 \u0441\u0442\u0430\u043d\u0446\u0456\u0457.",
+ "wrong_station_id": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 ID \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043b\u044c\u043d\u043e\u0457 \u0441\u0442\u0430\u043d\u0446\u0456\u0457."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "station_id": "ID \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043b\u044c\u043d\u043e\u0457 \u0441\u0442\u0430\u043d\u0446\u0456\u0457"
+ },
+ "description": "\u0406\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044f \u043f\u0440\u043e \u044f\u043a\u0456\u0441\u0442\u044c \u043f\u043e\u0432\u0456\u0442\u0440\u044f \u0432\u0456\u0434 \u041f\u043e\u043b\u044c\u0441\u044c\u043a\u043e\u0457 \u0456\u043d\u0441\u043f\u0435\u043a\u0446\u0456\u0457 \u0437 \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \u043d\u0430\u0432\u043a\u043e\u043b\u0438\u0448\u043d\u044c\u043e\u0433\u043e \u0441\u0435\u0440\u0435\u0434\u043e\u0432\u0438\u0449\u0430 (GIO\u015a). \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0454\u044e \u043f\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457: https://www.home-assistant.io/integrations/gios.",
+ "title": "GIO\u015a (\u041f\u043e\u043b\u044c\u0441\u044c\u043a\u0430 \u0456\u043d\u0441\u043f\u0435\u043a\u0446\u0456\u044f \u0437 \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \u043d\u0430\u0432\u043a\u043e\u043b\u0438\u0448\u043d\u044c\u043e\u0433\u043e \u0441\u0435\u0440\u0435\u0434\u043e\u0432\u0438\u0449\u0430)"
+ }
+ }
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 GIO\u015a"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py
index 312e726b91d39e..c7812fa621d6bc 100644
--- a/homeassistant/components/github/sensor.py
+++ b/homeassistant/components/github/sensor.py
@@ -5,7 +5,7 @@
import github
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_NAME,
CONF_ACCESS_TOKEN,
@@ -14,7 +14,6 @@
CONF_URL,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -72,7 +71,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class GitHubSensor(Entity):
+class GitHubSensor(SensorEntity):
"""Representation of a GitHub sensor."""
def __init__(self, github_data):
@@ -119,7 +118,7 @@ def available(self):
return self._available
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attrs = {
ATTR_PATH: self._repository_path,
@@ -228,18 +227,25 @@ def update(self):
self.stargazers = repo.stargazers_count
self.forks = repo.forks_count
- open_issues = repo.get_issues(state="open", sort="created")
- if open_issues is not None:
- self.open_issue_count = open_issues.totalCount
- if open_issues.totalCount > 0:
- self.latest_open_issue_url = open_issues[0].html_url
-
open_pull_requests = repo.get_pulls(state="open", sort="created")
if open_pull_requests is not None:
self.pull_request_count = open_pull_requests.totalCount
if open_pull_requests.totalCount > 0:
self.latest_open_pr_url = open_pull_requests[0].html_url
+ open_issues = repo.get_issues(state="open", sort="created")
+ if open_issues is not None:
+ if self.pull_request_count is None:
+ self.open_issue_count = open_issues.totalCount
+ else:
+ # pull requests are treated as issues too so we need to reduce the received count
+ self.open_issue_count = (
+ open_issues.totalCount - self.pull_request_count
+ )
+
+ if open_issues.totalCount > 0:
+ self.latest_open_issue_url = open_issues[0].html_url
+
latest_commit = repo.get_commits()[0]
self.latest_commit_sha = latest_commit.sha
self.latest_commit_message = latest_commit.commit.message
diff --git a/homeassistant/components/gitlab_ci/sensor.py b/homeassistant/components/gitlab_ci/sensor.py
index 9edbe9733a8991..0b619853348b25 100644
--- a/homeassistant/components/gitlab_ci/sensor.py
+++ b/homeassistant/components/gitlab_ci/sensor.py
@@ -5,7 +5,7 @@
from gitlab import Gitlab, GitlabAuthenticationError, GitlabGetError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_NAME,
@@ -14,7 +14,6 @@
CONF_URL,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -66,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([GitLabSensor(_gitlab_data, _name)], True)
-class GitLabSensor(Entity):
+class GitLabSensor(SensorEntity):
"""Representation of a GitLab sensor."""
def __init__(self, gitlab_data, name):
@@ -99,7 +98,7 @@ def available(self):
return self._available
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/gitter/sensor.py b/homeassistant/components/gitter/sensor.py
index aff6dc17923b83..20b68b2e5a9b05 100644
--- a/homeassistant/components/gitter/sensor.py
+++ b/homeassistant/components/gitter/sensor.py
@@ -5,10 +5,9 @@
from gitterpy.errors import GitterRoomError, GitterTokenError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_ROOM
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -47,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([GitterSensor(gitter, room, name, username)], True)
-class GitterSensor(Entity):
+class GitterSensor(SensorEntity):
"""Representation of a Gitter sensor."""
def __init__(self, data, room, name, username):
@@ -76,7 +75,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_USERNAME: self._username,
diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py
index 69e4ce0c016e7c..18865a232d7787 100644
--- a/homeassistant/components/glances/const.py
+++ b/homeassistant/components/glances/const.py
@@ -36,9 +36,10 @@
"process_thread": ["processcount", "Thread", "Count", CPU_ICON],
"process_sleeping": ["processcount", "Sleeping", "Count", CPU_ICON],
"cpu_use_percent": ["cpu", "CPU used", PERCENTAGE, CPU_ICON],
- "temperature_core": ["sensors", "temperature", TEMP_CELSIUS, "mdi:thermometer"],
- "fan_speed": ["sensors", "fan speed", "RPM", "mdi:fan"],
- "battery": ["sensors", "charge", PERCENTAGE, "mdi:battery"],
+ "temperature_core": ["sensors", "Temperature", TEMP_CELSIUS, "mdi:thermometer"],
+ "temperature_hdd": ["sensors", "Temperature", TEMP_CELSIUS, "mdi:thermometer"],
+ "fan_speed": ["sensors", "Fan speed", "RPM", "mdi:fan"],
+ "battery": ["sensors", "Charge", PERCENTAGE, "mdi:battery"],
"docker_active": ["docker", "Containers active", "", "mdi:docker"],
"docker_cpu_use": ["docker", "Containers CPU used", PERCENTAGE, "mdi:docker"],
"docker_memory_use": [
diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py
index 4c534a90ae1596..bbe045eb23220a 100644
--- a/homeassistant/components/glances/sensor.py
+++ b/homeassistant/components/glances/sensor.py
@@ -1,8 +1,8 @@
"""Support gathering system information of hosts which are running glances."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES
@@ -15,52 +15,51 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
dev = []
for sensor_type, sensor_details in SENSOR_TYPES.items():
- if not sensor_details[0] in client.api.data:
+ if sensor_details[0] not in client.api.data:
continue
- if sensor_details[0] in client.api.data:
- if sensor_details[0] == "fs":
- # fs will provide a list of disks attached
- for disk in client.api.data[sensor_details[0]]:
+ if sensor_details[0] == "fs":
+ # fs will provide a list of disks attached
+ for disk in client.api.data[sensor_details[0]]:
+ dev.append(
+ GlancesSensor(
+ client,
+ name,
+ disk["mnt_point"],
+ SENSOR_TYPES[sensor_type][1],
+ sensor_type,
+ SENSOR_TYPES[sensor_type],
+ )
+ )
+ elif sensor_details[0] == "sensors":
+ # sensors will provide temp for different devices
+ for sensor in client.api.data[sensor_details[0]]:
+ if sensor["type"] == sensor_type:
dev.append(
GlancesSensor(
client,
name,
- disk["mnt_point"],
+ sensor["label"],
SENSOR_TYPES[sensor_type][1],
sensor_type,
SENSOR_TYPES[sensor_type],
)
)
- elif sensor_details[0] == "sensors":
- # sensors will provide temp for different devices
- for sensor in client.api.data[sensor_details[0]]:
- if sensor["type"] == sensor_type:
- dev.append(
- GlancesSensor(
- client,
- name,
- sensor["label"],
- SENSOR_TYPES[sensor_type][1],
- sensor_type,
- SENSOR_TYPES[sensor_type],
- )
- )
- elif client.api.data[sensor_details[0]]:
- dev.append(
- GlancesSensor(
- client,
- name,
- "",
- SENSOR_TYPES[sensor_type][1],
- sensor_type,
- SENSOR_TYPES[sensor_type],
- )
+ elif client.api.data[sensor_details[0]]:
+ dev.append(
+ GlancesSensor(
+ client,
+ name,
+ "",
+ SENSOR_TYPES[sensor_type][1],
+ sensor_type,
+ SENSOR_TYPES[sensor_type],
)
+ )
async_add_entities(dev, True)
-class GlancesSensor(Entity):
+class GlancesSensor(SensorEntity):
"""Implementation of a Glances sensor."""
def __init__(
@@ -139,100 +138,103 @@ async def async_update(self):
if value is None:
return
- if value is not None:
- if self.sensor_details[0] == "fs":
- for var in value["fs"]:
- if var["mnt_point"] == self._sensor_name_prefix:
- disk = var
- break
- if self.type == "disk_use_percent":
- self._state = disk["percent"]
- elif self.type == "disk_use":
- self._state = round(disk["used"] / 1024 ** 3, 1)
- elif self.type == "disk_free":
- try:
- self._state = round(disk["free"] / 1024 ** 3, 1)
- except KeyError:
- self._state = round(
- (disk["size"] - disk["used"]) / 1024 ** 3,
- 1,
- )
- elif self.type == "battery":
- for sensor in value["sensors"]:
- if sensor["type"] == "battery":
- if sensor["label"] == self._sensor_name_prefix:
- self._state = sensor["value"]
- elif self.type == "fan_speed":
- for sensor in value["sensors"]:
- if sensor["type"] == "fan_speed":
- if sensor["label"] == self._sensor_name_prefix:
- self._state = sensor["value"]
- elif self.type == "temperature_core":
- for sensor in value["sensors"]:
- if sensor["type"] == "temperature_core":
- if sensor["label"] == self._sensor_name_prefix:
- self._state = sensor["value"]
- elif self.type == "memory_use_percent":
- self._state = value["mem"]["percent"]
- elif self.type == "memory_use":
- self._state = round(value["mem"]["used"] / 1024 ** 2, 1)
- elif self.type == "memory_free":
- self._state = round(value["mem"]["free"] / 1024 ** 2, 1)
- elif self.type == "swap_use_percent":
- self._state = value["memswap"]["percent"]
- elif self.type == "swap_use":
- self._state = round(value["memswap"]["used"] / 1024 ** 3, 1)
- elif self.type == "swap_free":
- self._state = round(value["memswap"]["free"] / 1024 ** 3, 1)
- elif self.type == "processor_load":
- # Windows systems don't provide load details
+ if self.sensor_details[0] == "fs":
+ for var in value["fs"]:
+ if var["mnt_point"] == self._sensor_name_prefix:
+ disk = var
+ break
+ if self.type == "disk_free":
try:
- self._state = value["load"]["min15"]
+ self._state = round(disk["free"] / 1024 ** 3, 1)
except KeyError:
- self._state = value["cpu"]["total"]
- elif self.type == "process_running":
- self._state = value["processcount"]["running"]
- elif self.type == "process_total":
- self._state = value["processcount"]["total"]
- elif self.type == "process_thread":
- self._state = value["processcount"]["thread"]
- elif self.type == "process_sleeping":
- self._state = value["processcount"]["sleeping"]
- elif self.type == "cpu_use_percent":
- self._state = value["quicklook"]["cpu"]
- elif self.type == "docker_active":
- count = 0
- try:
- for container in value["docker"]["containers"]:
- if (
- container["Status"] == "running"
- or "Up" in container["Status"]
- ):
- count += 1
- self._state = count
- except KeyError:
- self._state = count
- elif self.type == "docker_cpu_use":
- cpu_use = 0.0
- try:
- for container in value["docker"]["containers"]:
- if (
- container["Status"] == "running"
- or "Up" in container["Status"]
- ):
- cpu_use += container["cpu"]["total"]
- self._state = round(cpu_use, 1)
- except KeyError:
- self._state = STATE_UNAVAILABLE
- elif self.type == "docker_memory_use":
- mem_use = 0.0
- try:
- for container in value["docker"]["containers"]:
- if (
- container["Status"] == "running"
- or "Up" in container["Status"]
- ):
- mem_use += container["memory"]["usage"]
- self._state = round(mem_use / 1024 ** 2, 1)
- except KeyError:
- self._state = STATE_UNAVAILABLE
+ self._state = round(
+ (disk["size"] - disk["used"]) / 1024 ** 3,
+ 1,
+ )
+ elif self.type == "disk_use":
+ self._state = round(disk["used"] / 1024 ** 3, 1)
+ elif self.type == "disk_use_percent":
+ self._state = disk["percent"]
+ elif self.type == "battery":
+ for sensor in value["sensors"]:
+ if (
+ sensor["type"] == "battery"
+ and sensor["label"] == self._sensor_name_prefix
+ ):
+ self._state = sensor["value"]
+ elif self.type == "fan_speed":
+ for sensor in value["sensors"]:
+ if (
+ sensor["type"] == "fan_speed"
+ and sensor["label"] == self._sensor_name_prefix
+ ):
+ self._state = sensor["value"]
+ elif self.type == "temperature_core":
+ for sensor in value["sensors"]:
+ if (
+ sensor["type"] == "temperature_core"
+ and sensor["label"] == self._sensor_name_prefix
+ ):
+ self._state = sensor["value"]
+ elif self.type == "temperature_hdd":
+ for sensor in value["sensors"]:
+ if (
+ sensor["type"] == "temperature_hdd"
+ and sensor["label"] == self._sensor_name_prefix
+ ):
+ self._state = sensor["value"]
+ elif self.type == "memory_use_percent":
+ self._state = value["mem"]["percent"]
+ elif self.type == "memory_use":
+ self._state = round(value["mem"]["used"] / 1024 ** 2, 1)
+ elif self.type == "memory_free":
+ self._state = round(value["mem"]["free"] / 1024 ** 2, 1)
+ elif self.type == "swap_use_percent":
+ self._state = value["memswap"]["percent"]
+ elif self.type == "swap_use":
+ self._state = round(value["memswap"]["used"] / 1024 ** 3, 1)
+ elif self.type == "swap_free":
+ self._state = round(value["memswap"]["free"] / 1024 ** 3, 1)
+ elif self.type == "processor_load":
+ # Windows systems don't provide load details
+ try:
+ self._state = value["load"]["min15"]
+ except KeyError:
+ self._state = value["cpu"]["total"]
+ elif self.type == "process_running":
+ self._state = value["processcount"]["running"]
+ elif self.type == "process_total":
+ self._state = value["processcount"]["total"]
+ elif self.type == "process_thread":
+ self._state = value["processcount"]["thread"]
+ elif self.type == "process_sleeping":
+ self._state = value["processcount"]["sleeping"]
+ elif self.type == "cpu_use_percent":
+ self._state = value["quicklook"]["cpu"]
+ elif self.type == "docker_active":
+ count = 0
+ try:
+ for container in value["docker"]["containers"]:
+ if container["Status"] == "running" or "Up" in container["Status"]:
+ count += 1
+ self._state = count
+ except KeyError:
+ self._state = count
+ elif self.type == "docker_cpu_use":
+ cpu_use = 0.0
+ try:
+ for container in value["docker"]["containers"]:
+ if container["Status"] == "running" or "Up" in container["Status"]:
+ cpu_use += container["cpu"]["total"]
+ self._state = round(cpu_use, 1)
+ except KeyError:
+ self._state = STATE_UNAVAILABLE
+ elif self.type == "docker_memory_use":
+ mem_use = 0.0
+ try:
+ for container in value["docker"]["containers"]:
+ if container["Status"] == "running" or "Up" in container["Status"]:
+ mem_use += container["memory"]["usage"]
+ self._state = round(mem_use / 1024 ** 2, 1)
+ except KeyError:
+ self._state = STATE_UNAVAILABLE
diff --git a/homeassistant/components/glances/translations/de.json b/homeassistant/components/glances/translations/de.json
index 69c34907f19b64..e464bfdee34106 100644
--- a/homeassistant/components/glances/translations/de.json
+++ b/homeassistant/components/glances/translations/de.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Host ist bereits konfiguriert."
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung zum Host nicht m\u00f6glich",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"wrong_version": "Version nicht unterst\u00fctzt (nur 2 oder 3)"
},
"step": {
diff --git a/homeassistant/components/glances/translations/he.json b/homeassistant/components/glances/translations/he.json
new file mode 100644
index 00000000000000..6f4191da70d538
--- /dev/null
+++ b/homeassistant/components/glances/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/translations/hu.json b/homeassistant/components/glances/translations/hu.json
index 0958efee4ae1bb..d85baecb5ca3ac 100644
--- a/homeassistant/components/glances/translations/hu.json
+++ b/homeassistant/components/glances/translations/hu.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Kiszolg\u00e1l\u00f3 m\u00e1r konfigur\u00e1lva van."
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
},
"error": {
- "cannot_connect": "Nem lehet csatlakozni a kiszolg\u00e1l\u00f3hoz",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
"wrong_version": "Nem t\u00e1mogatott verzi\u00f3 (2 vagy 3 csak)"
},
"step": {
@@ -14,9 +14,9 @@
"name": "N\u00e9v",
"password": "Jelsz\u00f3",
"port": "Port",
- "ssl": "Az SSL / TLS haszn\u00e1lat\u00e1val csatlakozzon a Glances rendszerhez",
+ "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v",
- "verify_ssl": "A rendszer tan\u00fas\u00edt\u00e1s\u00e1nak ellen\u0151rz\u00e9se",
+ "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se",
"version": "Glances API-verzi\u00f3 (2 vagy 3)"
},
"title": "Glances Be\u00e1ll\u00edt\u00e1sa"
diff --git a/homeassistant/components/glances/translations/id.json b/homeassistant/components/glances/translations/id.json
new file mode 100644
index 00000000000000..13127e74322e40
--- /dev/null
+++ b/homeassistant/components/glances/translations/id.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "wrong_version": "Versi tidak didukung (hanya versi 2 atau versi 3)"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nama",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "ssl": "Menggunakan sertifikat SSL",
+ "username": "Nama Pengguna",
+ "verify_ssl": "Verifikasi sertifikat SSL",
+ "version": "Versi API Glances (2 atau 3)"
+ },
+ "title": "Siapkan Glances"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Frekuensi pembaruan"
+ },
+ "description": "Konfigurasikan opsi untuk Glances"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/translations/ko.json b/homeassistant/components/glances/translations/ko.json
index 336c9f3b3e5a1b..e50206fade52d2 100644
--- a/homeassistant/components/glances/translations/ko.json
+++ b/homeassistant/components/glances/translations/ko.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\ud638\uc2a4\ud2b8\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"wrong_version": "\ud574\ub2f9 \ubc84\uc804\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4 (2 \ub610\ub294 3\ub9cc \uc9c0\uc6d0)"
},
"step": {
@@ -14,9 +14,9 @@
"name": "\uc774\ub984",
"password": "\ube44\ubc00\ubc88\ud638",
"port": "\ud3ec\ud2b8",
- "ssl": "SSL/TLS \ub97c \uc0ac\uc6a9\ud558\uc5ec Glances \uc2dc\uc2a4\ud15c\uc5d0 \uc5f0\uacb0",
+ "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9",
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984",
- "verify_ssl": "\uc2dc\uc2a4\ud15c \uc778\uc99d \ud655\uc778",
+ "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778",
"version": "Glances API \ubc84\uc804 (2 \ub610\ub294 3)"
},
"title": "Glances \uc124\uce58\ud558\uae30"
@@ -29,7 +29,7 @@
"data": {
"scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4"
},
- "description": "Glances \uc635\uc158 \uc124\uc815\ud558\uae30"
+ "description": "Glances\uc5d0 \ub300\ud55c \uc635\uc158 \uad6c\uc131\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/glances/translations/nl.json b/homeassistant/components/glances/translations/nl.json
index c2f2b9d473a5bc..6cb7fb445bbc8c 100644
--- a/homeassistant/components/glances/translations/nl.json
+++ b/homeassistant/components/glances/translations/nl.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Host is al geconfigureerd."
+ "already_configured": "Apparaat is al geconfigureerd"
},
"error": {
- "cannot_connect": "Kan geen verbinding maken met host",
+ "cannot_connect": "Kan geen verbinding maken",
"wrong_version": "Versie niet ondersteund (alleen 2 of 3)"
},
"step": {
@@ -14,9 +14,9 @@
"name": "Naam",
"password": "Wachtwoord",
"port": "Poort",
- "ssl": "Gebruik SSL / TLS om verbinding te maken met het Glances-systeem",
+ "ssl": "Gebruik een SSL-certificaat",
"username": "Gebruikersnaam",
- "verify_ssl": "Controleer de certificering van het systeem",
+ "verify_ssl": "SSL-certificaat verifi\u00ebren",
"version": "Glances API-versie (2 of 3)"
},
"title": "Glances instellen"
diff --git a/homeassistant/components/glances/translations/ru.json b/homeassistant/components/glances/translations/ru.json
index 0dc8c72dc9f55e..aecffe204c8087 100644
--- a/homeassistant/components/glances/translations/ru.json
+++ b/homeassistant/components/glances/translations/ru.json
@@ -15,7 +15,7 @@
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
"ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
- "username": "\u041b\u043e\u0433\u0438\u043d",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f",
"verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
"version": "\u0412\u0435\u0440\u0441\u0438\u044f API Glances (2 \u0438\u043b\u0438 3)"
},
diff --git a/homeassistant/components/glances/translations/tr.json b/homeassistant/components/glances/translations/tr.json
new file mode 100644
index 00000000000000..69f0cd7ceb123f
--- /dev/null
+++ b/homeassistant/components/glances/translations/tr.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "password": "Parola",
+ "port": "Port",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "G\u00fcncelleme s\u0131kl\u0131\u011f\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/translations/uk.json b/homeassistant/components/glances/translations/uk.json
new file mode 100644
index 00000000000000..1fab197fe4291b
--- /dev/null
+++ b/homeassistant/components/glances/translations/uk.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "wrong_version": "\u041f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u044e\u0442\u044c\u0441\u044f \u0442\u0456\u043b\u044c\u043a\u0438 \u0432\u0435\u0440\u0441\u0456\u0457 2 \u0442\u0430 3."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430",
+ "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL",
+ "version": "\u0412\u0435\u0440\u0441\u0456\u044f API Glances (2 \u0430\u0431\u043e 3)"
+ },
+ "title": "Glances"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f"
+ },
+ "description": "\u0420\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 Glances"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py
index ff60a9ac0435bf..e00b17ebae4b1e 100644
--- a/homeassistant/components/goalzero/__init__.py
+++ b/homeassistant/components/goalzero/__init__.py
@@ -76,8 +76,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py
index 35b6953865c35b..8a1333d6cf137f 100644
--- a/homeassistant/components/goalzero/config_flow.py
+++ b/homeassistant/components/goalzero/config_flow.py
@@ -8,7 +8,7 @@
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from .const import DEFAULT_NAME, DOMAIN # pylint:disable=unused-import
+from .const import DEFAULT_NAME, DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/goalzero/strings.json b/homeassistant/components/goalzero/strings.json
index 0a1b8909f543b0..bd59cd5e7f5f43 100644
--- a/homeassistant/components/goalzero/strings.json
+++ b/homeassistant/components/goalzero/strings.json
@@ -3,7 +3,7 @@
"step": {
"user": {
"title": "Goal Zero Yeti",
- "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. Then get the host ip from your router. DHCP must be set up in your router settings for the device to ensure the host ip does not change. Refer to your router's user manual.",
+ "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. Then get the host IP from your router. DHCP must be set up in your router settings for the device to ensure the host IP does not change. Refer to your router's user manual.",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"name": "[%key:common::config_flow::data::name%]"
diff --git a/homeassistant/components/goalzero/translations/ca.json b/homeassistant/components/goalzero/translations/ca.json
index 2d301d5ef07800..ac4c2a696e23e6 100644
--- a/homeassistant/components/goalzero/translations/ca.json
+++ b/homeassistant/components/goalzero/translations/ca.json
@@ -14,7 +14,7 @@
"host": "Amfitri\u00f3",
"name": "Nom"
},
- "description": "En primer lloc, has de baixar-te l'aplicaci\u00f3 Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nSegueix les instruccions per connectar el teu Yeti a la teva xarxa Wifi. A continuaci\u00f3, has d'obtenir la IP d'amfitri\u00f3 del teu encaminador (router). Cal que aquest tingui la configuraci\u00f3 DHCP activada per al teu dispositiu per aix\u00ed garantir que la IP no canvi\u00ef. Si cal, consulta el manual del teu encaminador.",
+ "description": "En primer lloc, has de baixar-te l'aplicaci\u00f3 Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nSegueix les instruccions per connectar el Yeti a la xarxa Wifi. A continuaci\u00f3, has d'obtenir la IP d'amfitri\u00f3 del teu router. Cal que aquest tingui la configuraci\u00f3 DHCP activada per al teu dispositiu, per aix\u00ed garantir que la IP no canvi\u00ef. Si cal, consulta el manual del router.",
"title": "Goal Zero Yeti"
}
}
diff --git a/homeassistant/components/goalzero/translations/de.json b/homeassistant/components/goalzero/translations/de.json
index d79c03f0179fbe..3916b987981764 100644
--- a/homeassistant/components/goalzero/translations/de.json
+++ b/homeassistant/components/goalzero/translations/de.json
@@ -1,9 +1,21 @@
{
"config": {
+ "abort": {
+ "already_configured": "Konto wurde bereits konfiguriert"
+ },
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse",
"unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Name"
+ },
+ "description": "Zun\u00e4chst musst du die Goal Zero App herunterladen: https://www.goalzero.com/product-features/yeti-app/\n\nFolge den Anweisungen, um deinen Yeti mit deinem Wifi-Netzwerk zu verbinden. Bekomme dann die Host-IP von deinem Router. DHCP muss in den Router-Einstellungen f\u00fcr das Ger\u00e4t richtig eingerichtet werden, um sicherzustellen, dass sich die Host-IP nicht \u00e4ndert. Schaue hierzu im Benutzerhandbuch deines Routers nach."
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/en.json b/homeassistant/components/goalzero/translations/en.json
index 08c823e2ad2e4f..25aa32e4b754cf 100644
--- a/homeassistant/components/goalzero/translations/en.json
+++ b/homeassistant/components/goalzero/translations/en.json
@@ -14,7 +14,7 @@
"host": "Host",
"name": "Name"
},
- "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. Then get the host ip from your router. DHCP must be set up in your router settings for the device to ensure the host ip does not change. Refer to your router's user manual.",
+ "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. Then get the host IP from your router. DHCP must be set up in your router settings for the device to ensure the host IP does not change. Refer to your router's user manual.",
"title": "Goal Zero Yeti"
}
}
diff --git a/homeassistant/components/goalzero/translations/et.json b/homeassistant/components/goalzero/translations/et.json
index 4479ba8669e0eb..74f84f1d72b092 100644
--- a/homeassistant/components/goalzero/translations/et.json
+++ b/homeassistant/components/goalzero/translations/et.json
@@ -14,7 +14,7 @@
"host": "",
"name": "Nimi"
},
- "description": "Alustuseks peate alla laadima rakenduse Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\n Yeti Wifi-v\u00f5rguga \u00fchendamiseks j\u00e4rgige juhiseid. Seej\u00e4rel hankige oma ruuterilt host IP. DHCP peab olema ruuteri seadetes seadistatud, et tagada, et host-IP ei muutuks. Vaadake ruuteri kasutusjuhendit.",
+ "description": "Alustuseks pead alla laadima rakenduse Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\n Yeti Wifi-v\u00f5rguga \u00fchendamiseks j\u00e4rgi juhiseid. Seej\u00e4rel hangi oma ruuterilt host IP. DHCP peab olema ruuteri seadetes seadistatud, et tagada, et host-IP ei muutuks. Vaata ruuteri kasutusjuhendit.",
"title": ""
}
}
diff --git a/homeassistant/components/goalzero/translations/fr.json b/homeassistant/components/goalzero/translations/fr.json
index 5c4b7a01580ba1..7bd4929ad929cd 100644
--- a/homeassistant/components/goalzero/translations/fr.json
+++ b/homeassistant/components/goalzero/translations/fr.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9"
+ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9"
},
"error": {
"cannot_connect": "\u00c9chec de connexion",
diff --git a/homeassistant/components/goalzero/translations/hu.json b/homeassistant/components/goalzero/translations/hu.json
new file mode 100644
index 00000000000000..c876a55301f160
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/hu.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "name": "N\u00e9v"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/id.json b/homeassistant/components/goalzero/translations/id.json
new file mode 100644
index 00000000000000..63fddf13a8e132
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/id.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_host": "Nama host atau alamat IP tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nama"
+ },
+ "description": "Pertama, Anda perlu mengunduh aplikasi Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nIkuti petunjuk untuk menghubungkan Yeti Anda ke jaringan Wi-Fi Anda. Kemudian dapatkan IP host dari router Anda. DHCP harus disetel di pengaturan router Anda untuk perangkat host agar IP host tidak berubah. Lihat manual pengguna router Anda.",
+ "title": "Goal Zero Yeti"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/it.json b/homeassistant/components/goalzero/translations/it.json
index 10df269d59aef8..24f04a0bafebd5 100644
--- a/homeassistant/components/goalzero/translations/it.json
+++ b/homeassistant/components/goalzero/translations/it.json
@@ -14,7 +14,7 @@
"host": "Host",
"name": "Nome"
},
- "description": "Innanzitutto, devi scaricare l'app Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nSegui le istruzioni per connettere il tuo Yeti alla tua rete Wifi. Quindi ottieni l'ip host dal tuo router. Il DHCP deve essere configurato nelle impostazioni del router affinch\u00e9 il dispositivo assicuri che l'ip host non cambi. Fare riferimento al manuale utente del router.",
+ "description": "Innanzitutto, devi scaricare l'app Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nSegui le istruzioni per connettere il tuo Yeti alla tua rete Wifi. Quindi ottieni l'ip host dal tuo router. Il DHCP deve essere configurato nelle impostazioni del router affinch\u00e9 assicuri che l'ip host non cambi. Fare riferimento al manuale utente del router.",
"title": "Goal Zero Yeti"
}
}
diff --git a/homeassistant/components/goalzero/translations/ko.json b/homeassistant/components/goalzero/translations/ko.json
new file mode 100644
index 00000000000000..d51193630026fc
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/ko.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "name": "\uc774\ub984"
+ },
+ "description": "\uba3c\uc800 Goal Zero \uc571\uc744 \ub2e4\uc6b4\ub85c\ub4dc\ud574\uc57c \ud569\ub2c8\ub2e4. https://www.goalzero.com/product-features/yeti-app/\n\n\uc9c0\uce68\uc5d0 \ub530\ub77c Yeti\ub97c Wifi \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ud55c \ub2e4\uc74c \ub77c\uc6b0\ud130\uc5d0\uc11c \ud638\uc2a4\ud2b8 IP\ub97c \uac00\uc838\uc640\uc8fc\uc138\uc694. \ud638\uc2a4\ud2b8 IP\uac00 \ubcc0\uacbd\ub418\uc9c0 \uc54a\ub3c4\ub85d \ud558\ub824\uba74 \uae30\uae30\uc5d0 \ub300\ud574 \ub77c\uc6b0\ud130\uc5d0\uc11c DHCP\ub97c \uc54c\ub9de\uac8c \uc124\uc815\ud574\uc8fc\uc5b4\uc57c \ud569\ub2c8\ub2e4. \ud574\ub2f9 \ub0b4\uc6a9\uc5d0 \ub300\ud574\uc11c\ub294 \ub77c\uc6b0\ud130\uc758 \uc0ac\uc6a9\uc790 \uc124\uba85\uc11c\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
+ "title": "Goal Zero Yeti"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/nl.json b/homeassistant/components/goalzero/translations/nl.json
index 86958670d70af5..c84ef7adb1f4ed 100644
--- a/homeassistant/components/goalzero/translations/nl.json
+++ b/homeassistant/components/goalzero/translations/nl.json
@@ -11,9 +11,11 @@
"step": {
"user": {
"data": {
+ "host": "Host",
"name": "Naam"
},
- "description": "Eerst moet u de Goal Zero-app downloaden: https://www.goalzero.com/product-features/yeti-app/ \n\n Volg de instructies om je Yeti te verbinden met je wifi-netwerk. Haal dan de host-ip van uw router. DHCP moet zijn ingesteld in uw routerinstellingen voor het apparaat om ervoor te zorgen dat het host-ip niet verandert. Raadpleeg de gebruikershandleiding van uw router."
+ "description": "Eerst moet u de Goal Zero-app downloaden: https://www.goalzero.com/product-features/yeti-app/ \n\n Volg de instructies om je Yeti te verbinden met je wifi-netwerk. Haal dan de host-ip van uw router. DHCP moet zijn ingesteld in uw routerinstellingen voor het apparaat om ervoor te zorgen dat het host-ip niet verandert. Raadpleeg de gebruikershandleiding van uw router.",
+ "title": "Goal Zero Yeti"
}
}
}
diff --git a/homeassistant/components/goalzero/translations/no.json b/homeassistant/components/goalzero/translations/no.json
index 1aaf27d1b09985..4ae6f564a99751 100644
--- a/homeassistant/components/goalzero/translations/no.json
+++ b/homeassistant/components/goalzero/translations/no.json
@@ -14,7 +14,7 @@
"host": "Vert",
"name": "Navn"
},
- "description": "F\u00f8rst m\u00e5 du laste ned appen Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\n F\u00f8lg instruksjonene for \u00e5 koble Yeti til Wifi-nettverket. S\u00e5 f\u00e5 verts-ip fra ruteren din. DHCP m\u00e5 v\u00e6re satt opp i ruteren innstillinger for enheten for \u00e5 sikre at verts-IP ikke endres. Se brukerh\u00e5ndboken til ruteren.",
+ "description": "F\u00f8rst m\u00e5 du laste ned appen Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\n F\u00f8lg instruksjonene for \u00e5 koble Yeti til Wifi-nettverket. S\u00e5 f\u00e5 verts-IP-en fra ruteren din. DHCP m\u00e5 v\u00e6re satt opp i ruteren innstillinger for enheten for \u00e5 sikre at verts-IP ikke endres. Se ruteren din.",
"title": ""
}
}
diff --git a/homeassistant/components/goalzero/translations/tr.json b/homeassistant/components/goalzero/translations/tr.json
new file mode 100644
index 00000000000000..ae77262b2b3f9a
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/tr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/uk.json b/homeassistant/components/goalzero/translations/uk.json
new file mode 100644
index 00000000000000..6d67d949c28692
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/uk.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430"
+ },
+ "description": "\u0421\u043f\u043e\u0447\u0430\u0442\u043a\u0443 \u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0437\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0438\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Goal Zero: https://www.goalzero.com/product-features/yeti-app/. \n\n \u0414\u043e\u0442\u0440\u0438\u043c\u0443\u0439\u0442\u0435\u0441\u044c \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0439 \u043f\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044e Yeti \u0434\u043e \u043c\u0435\u0440\u0435\u0436\u0456 WiFi. \u041f\u043e\u0442\u0456\u043c \u0434\u0456\u0437\u043d\u0430\u0439\u0442\u0435\u0441\u044f IP \u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e, \u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434, \u0437 \u0412\u0430\u0448\u043e\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430. \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u0442\u0430\u043a\u0438\u043c\u0438, \u0449\u043e\u0431 IP \u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u0437\u043c\u0456\u043d\u044e\u0432\u0430\u043b\u0430\u0441\u044c \u0437 \u0447\u0430\u0441\u043e\u043c. \u041f\u0440\u043e \u0442\u0435, \u044f\u043a \u0446\u0435 \u0437\u0440\u043e\u0431\u0438\u0442\u0438, \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u0432 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0457 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0412\u0430\u0448\u043e\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430.",
+ "title": "Goal Zero Yeti"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gogogate2/__init__.py b/homeassistant/components/gogogate2/__init__.py
index 93f000e6a3ab0a..4c9e646c54d254 100644
--- a/homeassistant/components/gogogate2/__init__.py
+++ b/homeassistant/components/gogogate2/__init__.py
@@ -1,17 +1,16 @@
"""The gogogate2 component."""
-from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
+import asyncio
+
+from homeassistant.components.cover import DOMAIN as COVER
+from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
from .common import get_data_update_coordinator
from .const import DEVICE_TYPE_GOGOGATE2
-
-async def async_setup(hass: HomeAssistant, base_config: dict) -> bool:
- """Set up for Gogogate2 controllers."""
- return True
+PLATFORMS = [COVER, SENSOR]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
@@ -29,22 +28,25 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
hass.config_entries.async_update_entry(config_entry, **config_updates)
data_update_coordinator = get_data_update_coordinator(hass, config_entry)
- await data_update_coordinator.async_refresh()
+ await data_update_coordinator.async_config_entry_first_refresh()
- if not data_update_coordinator.last_update_success:
- raise ConfigEntryNotReady()
-
- hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, COVER_DOMAIN)
- )
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ )
return True
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload Gogogate2 config entry."""
- hass.async_create_task(
- hass.config_entries.async_forward_entry_unload(config_entry, COVER_DOMAIN)
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
)
- return True
+ return unload_ok
diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py
index 2817c351013819..e8b17184bbe559 100644
--- a/homeassistant/components/gogogate2/common.py
+++ b/homeassistant/components/gogogate2/common.py
@@ -1,10 +1,12 @@
"""Common code for GogoGate2 component."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Awaitable, Callable, NamedTuple, Optional
+from typing import Awaitable, Callable, NamedTuple
from gogogate2_api import AbstractGateApi, GogoGate2Api, ISmartGateApi
-from gogogate2_api.common import AbstractDoor
+from gogogate2_api.common import AbstractDoor, get_door_by_id
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -15,9 +17,13 @@
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.debounce import Debouncer
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+ UpdateFailed,
+)
-from .const import DATA_UPDATE_COORDINATOR, DEVICE_TYPE_ISMARTGATE, DOMAIN
+from .const import DATA_UPDATE_COORDINATOR, DEVICE_TYPE_ISMARTGATE, DOMAIN, MANUFACTURER
_LOGGER = logging.getLogger(__name__)
@@ -26,8 +32,8 @@ class StateData(NamedTuple):
"""State data for a cover entity."""
config_unique_id: str
- unique_id: Optional[str]
- door: Optional[AbstractDoor]
+ unique_id: str | None
+ door: AbstractDoor | None
class DeviceDataUpdateCoordinator(DataUpdateCoordinator):
@@ -41,8 +47,8 @@ def __init__(
*,
name: str,
update_interval: timedelta,
- update_method: Optional[Callable[[], Awaitable]] = None,
- request_refresh_debouncer: Optional[Debouncer] = None,
+ update_method: Callable[[], Awaitable] | None = None,
+ request_refresh_debouncer: Debouncer | None = None,
):
"""Initialize the data update coordinator."""
DataUpdateCoordinator.__init__(
@@ -57,6 +63,45 @@ def __init__(
self.api = api
+class GoGoGate2Entity(CoordinatorEntity):
+ """Base class for gogogate2 entities."""
+
+ def __init__(
+ self,
+ config_entry: ConfigEntry,
+ data_update_coordinator: DeviceDataUpdateCoordinator,
+ door: AbstractDoor,
+ unique_id: str,
+ ) -> None:
+ """Initialize gogogate2 base entity."""
+ super().__init__(data_update_coordinator)
+ self._config_entry = config_entry
+ self._door = door
+ self._unique_id = unique_id
+
+ @property
+ def unique_id(self) -> str | None:
+ """Return a unique ID."""
+ return self._unique_id
+
+ def _get_door(self) -> AbstractDoor:
+ door = get_door_by_id(self._door.door_id, self.coordinator.data)
+ self._door = door or self._door
+ return self._door
+
+ @property
+ def device_info(self):
+ """Device info for the controller."""
+ data = self.coordinator.data
+ return {
+ "identifiers": {(DOMAIN, self._config_entry.unique_id)},
+ "name": self._config_entry.title,
+ "manufacturer": MANUFACTURER,
+ "model": data.model,
+ "sw_version": data.firmwareversion,
+ }
+
+
def get_data_update_coordinator(
hass: HomeAssistant, config_entry: ConfigEntry
) -> DeviceDataUpdateCoordinator:
@@ -95,6 +140,13 @@ def cover_unique_id(config_entry: ConfigEntry, door: AbstractDoor) -> str:
return f"{config_entry.unique_id}_{door.door_id}"
+def sensor_unique_id(
+ config_entry: ConfigEntry, door: AbstractDoor, sensor_type: str
+) -> str:
+ """Generate a cover entity unique id."""
+ return f"{config_entry.unique_id}_{door.door_id}_{sensor_type}"
+
+
def get_api(config_data: dict) -> AbstractGateApi:
"""Get an api object for config data."""
gate_class = GogoGate2Api
diff --git a/homeassistant/components/gogogate2/config_flow.py b/homeassistant/components/gogogate2/config_flow.py
index 0c3f1b3653cac9..3ecd3e85c3f4c5 100644
--- a/homeassistant/components/gogogate2/config_flow.py
+++ b/homeassistant/components/gogogate2/config_flow.py
@@ -16,8 +16,7 @@
)
from .common import get_api
-from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, DOMAIN
class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py
index 8b83073d0c8b49..05fcb639e4725f 100644
--- a/homeassistant/components/gogogate2/cover.py
+++ b/homeassistant/components/gogogate2/cover.py
@@ -1,13 +1,10 @@
"""Support for Gogogate2 garage Doors."""
-from typing import Callable, List, Optional
+from __future__ import annotations
-from gogogate2_api.common import (
- AbstractDoor,
- DoorStatus,
- get_configured_doors,
- get_door_by_id,
-)
-import voluptuous as vol
+import logging
+from typing import Callable
+
+from gogogate2_api.common import AbstractDoor, DoorStatus, get_configured_doors
from homeassistant.components.cover import (
DEVICE_CLASS_GARAGE,
@@ -17,40 +14,28 @@
CoverEntity,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
-from homeassistant.const import (
- CONF_DEVICE,
- CONF_IP_ADDRESS,
- CONF_PASSWORD,
- CONF_USERNAME,
-)
from homeassistant.core import HomeAssistant
-import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .common import (
DeviceDataUpdateCoordinator,
+ GoGoGate2Entity,
cover_unique_id,
get_data_update_coordinator,
)
-from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, DOMAIN, MANUFACTURER
-
-COVER_SCHEMA = vol.Schema(
- {
- vol.Required(CONF_IP_ADDRESS): cv.string,
- vol.Required(CONF_DEVICE, default=DEVICE_TYPE_GOGOGATE2): vol.In(
- (DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE)
- ),
- vol.Required(CONF_PASSWORD): cv.string,
- vol.Required(CONF_USERNAME): cv.string,
- }
-)
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(
hass: HomeAssistant, config: dict, add_entities: Callable, discovery_info=None
) -> None:
"""Convert old style file configs to new style configs."""
+ _LOGGER.warning(
+ "Loading gogogate2 via platform config is deprecated; The configuration"
+ " has been migrated to a config entry and can be safely removed"
+ )
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
@@ -61,7 +46,7 @@ async def async_setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], Optional[bool]], None],
+ async_add_entities: Callable[[list[Entity], bool | None], None],
) -> None:
"""Set up the config entry."""
data_update_coordinator = get_data_update_coordinator(hass, config_entry)
@@ -74,7 +59,7 @@ async def async_setup_entry(
)
-class DeviceCover(CoordinatorEntity, CoverEntity):
+class DeviceCover(GoGoGate2Entity, CoverEntity):
"""Cover entity for goggate2."""
def __init__(
@@ -84,18 +69,11 @@ def __init__(
door: AbstractDoor,
) -> None:
"""Initialize the object."""
- super().__init__(data_update_coordinator)
- self._config_entry = config_entry
- self._door = door
+ unique_id = cover_unique_id(config_entry, door)
+ super().__init__(config_entry, data_update_coordinator, door, unique_id)
self._api = data_update_coordinator.api
- self._unique_id = cover_unique_id(config_entry, door)
self._is_available = True
- @property
- def unique_id(self) -> Optional[str]:
- """Return a unique ID."""
- return self._unique_id
-
@property
def name(self):
"""Return the name of the door."""
@@ -136,25 +114,6 @@ async def async_close_cover(self, **kwargs):
await self._api.async_close_door(self._get_door().door_id)
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
- attrs = super().state_attributes
- attrs["door_id"] = self._get_door().door_id
- return attrs
-
- def _get_door(self) -> AbstractDoor:
- door = get_door_by_id(self._door.door_id, self.coordinator.data)
- self._door = door or self._door
- return self._door
-
- @property
- def device_info(self):
- """Device info for the controller."""
- data = self.coordinator.data
- return {
- "identifiers": {(DOMAIN, self._config_entry.unique_id)},
- "name": self._config_entry.title,
- "manufacturer": MANUFACTURER,
- "model": data.model,
- "sw_version": data.firmwareversion,
- }
+ return {"door_id": self._get_door().door_id}
diff --git a/homeassistant/components/gogogate2/sensor.py b/homeassistant/components/gogogate2/sensor.py
new file mode 100644
index 00000000000000..9062bc0b352af6
--- /dev/null
+++ b/homeassistant/components/gogogate2/sensor.py
@@ -0,0 +1,130 @@
+"""Support for Gogogate2 garage Doors."""
+from __future__ import annotations
+
+from itertools import chain
+from typing import Callable
+
+from gogogate2_api.common import AbstractDoor, get_configured_doors
+
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_TEMPERATURE,
+ TEMP_CELSIUS,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+
+from .common import (
+ DeviceDataUpdateCoordinator,
+ GoGoGate2Entity,
+ get_data_update_coordinator,
+ sensor_unique_id,
+)
+
+SENSOR_ID_WIRED = "WIRE"
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: Callable[[list[Entity], bool | None], None],
+) -> None:
+ """Set up the config entry."""
+ data_update_coordinator = get_data_update_coordinator(hass, config_entry)
+
+ sensors = chain(
+ [
+ DoorSensorBattery(config_entry, data_update_coordinator, door)
+ for door in get_configured_doors(data_update_coordinator.data)
+ if door.sensorid and door.sensorid != SENSOR_ID_WIRED
+ ],
+ [
+ DoorSensorTemperature(config_entry, data_update_coordinator, door)
+ for door in get_configured_doors(data_update_coordinator.data)
+ if door.sensorid and door.sensorid != SENSOR_ID_WIRED
+ ],
+ )
+ async_add_entities(sensors)
+
+
+class DoorSensorBattery(GoGoGate2Entity, SensorEntity):
+ """Battery sensor entity for gogogate2 door sensor."""
+
+ def __init__(
+ self,
+ config_entry: ConfigEntry,
+ data_update_coordinator: DeviceDataUpdateCoordinator,
+ door: AbstractDoor,
+ ) -> None:
+ """Initialize the object."""
+ unique_id = sensor_unique_id(config_entry, door, "battery")
+ super().__init__(config_entry, data_update_coordinator, door, unique_id)
+
+ @property
+ def name(self):
+ """Return the name of the door."""
+ return f"{self._get_door().name} battery"
+
+ @property
+ def device_class(self):
+ """Return the class of this device, from component DEVICE_CLASSES."""
+ return DEVICE_CLASS_BATTERY
+
+ @property
+ def state(self):
+ """Return the state of the entity."""
+ door = self._get_door()
+ return door.voltage # This is a percentage, not an absolute voltage
+
+ @property
+ def extra_state_attributes(self):
+ """Return the state attributes."""
+ door = self._get_door()
+ if door.sensorid is not None:
+ return {"door_id": door.door_id, "sensor_id": door.sensorid}
+ return None
+
+
+class DoorSensorTemperature(GoGoGate2Entity, SensorEntity):
+ """Temperature sensor entity for gogogate2 door sensor."""
+
+ def __init__(
+ self,
+ config_entry: ConfigEntry,
+ data_update_coordinator: DeviceDataUpdateCoordinator,
+ door: AbstractDoor,
+ ) -> None:
+ """Initialize the object."""
+ unique_id = sensor_unique_id(config_entry, door, "temperature")
+ super().__init__(config_entry, data_update_coordinator, door, unique_id)
+
+ @property
+ def name(self):
+ """Return the name of the door."""
+ return f"{self._get_door().name} temperature"
+
+ @property
+ def device_class(self):
+ """Return the class of this device, from component DEVICE_CLASSES."""
+ return DEVICE_CLASS_TEMPERATURE
+
+ @property
+ def state(self):
+ """Return the state of the entity."""
+ door = self._get_door()
+ return door.temperature
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit_of_measurement."""
+ return TEMP_CELSIUS
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ door = self._get_door()
+ if door.sensorid is not None:
+ return {"door_id": door.door_id, "sensor_id": door.sensorid}
+ return None
diff --git a/homeassistant/components/gogogate2/translations/de.json b/homeassistant/components/gogogate2/translations/de.json
index 119d198615c2df..30a1ff67b65348 100644
--- a/homeassistant/components/gogogate2/translations/de.json
+++ b/homeassistant/components/gogogate2/translations/de.json
@@ -13,7 +13,8 @@
"ip_address": "IP-Adresse",
"password": "Passwort",
"username": "Benutzername"
- }
+ },
+ "title": "GogoGate2 oder iSmartGate einrichten"
}
}
}
diff --git a/homeassistant/components/gogogate2/translations/hu.json b/homeassistant/components/gogogate2/translations/hu.json
index 952a502a72d189..cdc76a4145aa20 100644
--- a/homeassistant/components/gogogate2/translations/hu.json
+++ b/homeassistant/components/gogogate2/translations/hu.json
@@ -10,9 +10,11 @@
"step": {
"user": {
"data": {
+ "ip_address": "IP c\u00edm",
"password": "Jelsz\u00f3",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
- }
+ },
+ "title": "A GogoGate2 vagy az iSmartGate be\u00e1ll\u00edt\u00e1sa"
}
}
}
diff --git a/homeassistant/components/gogogate2/translations/id.json b/homeassistant/components/gogogate2/translations/id.json
new file mode 100644
index 00000000000000..9de61641d41e24
--- /dev/null
+++ b/homeassistant/components/gogogate2/translations/id.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "Alamat IP",
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "description": "Berikan informasi yang diperlukan di bawah ini.",
+ "title": "Siapkan GogoGate2 atau iSmartGate"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gogogate2/translations/ko.json b/homeassistant/components/gogogate2/translations/ko.json
index 55b32812bfacb5..dc37928db76fe2 100644
--- a/homeassistant/components/gogogate2/translations/ko.json
+++ b/homeassistant/components/gogogate2/translations/ko.json
@@ -15,7 +15,7 @@
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
},
"description": "\uc544\ub798\uc5d0 \ud544\uc218 \uc815\ubcf4\ub97c \uc81c\uacf5\ud574\uc8fc\uc138\uc694.",
- "title": "GogoGate2 \uc124\uce58\ud558\uae30"
+ "title": "GogoGate2 \ub610\ub294 iSmartGate \uc124\uce58\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/gogogate2/translations/nl.json b/homeassistant/components/gogogate2/translations/nl.json
index ad8e894d093ffe..5418735ec07a91 100644
--- a/homeassistant/components/gogogate2/translations/nl.json
+++ b/homeassistant/components/gogogate2/translations/nl.json
@@ -15,7 +15,7 @@
"username": "Gebruikersnaam"
},
"description": "Geef hieronder de vereiste informatie op.",
- "title": "Stel GogoGate2 in"
+ "title": "Stel GogoGate2 of iSmartGate in"
}
}
}
diff --git a/homeassistant/components/gogogate2/translations/ru.json b/homeassistant/components/gogogate2/translations/ru.json
index 0c8f14f65f4e75..4efa554fc913db 100644
--- a/homeassistant/components/gogogate2/translations/ru.json
+++ b/homeassistant/components/gogogate2/translations/ru.json
@@ -5,14 +5,14 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"step": {
"user": {
"data": {
"ip_address": "IP-\u0430\u0434\u0440\u0435\u0441",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 GogoGate2.",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 GogoGate2 \u0438\u043b\u0438 iSmartGate"
diff --git a/homeassistant/components/gogogate2/translations/tr.json b/homeassistant/components/gogogate2/translations/tr.json
new file mode 100644
index 00000000000000..e912e7f8012f86
--- /dev/null
+++ b/homeassistant/components/gogogate2/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "\u0130p Adresi",
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gogogate2/translations/uk.json b/homeassistant/components/gogogate2/translations/uk.json
new file mode 100644
index 00000000000000..c88b9b603840d3
--- /dev/null
+++ b/homeassistant/components/gogogate2/translations/uk.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 GogoGate2.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f GogoGate2 \u0430\u0431\u043e iSmartGate"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py
index 78ea1616f99f03..b46d48848daaee 100644
--- a/homeassistant/components/google/__init__.py
+++ b/homeassistant/components/google/__init__.py
@@ -15,7 +15,14 @@
from voluptuous.error import Error as VoluptuousError
import yaml
-from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
+from homeassistant.const import (
+ CONF_CLIENT_ID,
+ CONF_CLIENT_SECRET,
+ CONF_DEVICE_ID,
+ CONF_ENTITIES,
+ CONF_NAME,
+ CONF_OFFSET,
+)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import generate_entity_id
@@ -30,12 +37,8 @@
CONF_TRACK_NEW = "track_new_calendar"
CONF_CAL_ID = "cal_id"
-CONF_DEVICE_ID = "device_id"
-CONF_NAME = "name"
-CONF_ENTITIES = "entities"
CONF_TRACK = "track"
CONF_SEARCH = "search"
-CONF_OFFSET = "offset"
CONF_IGNORE_AVAILABILITY = "ignore_availability"
CONF_MAX_RESULTS = "max_results"
diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py
index 6448e035171d80..2cc6612194800f 100644
--- a/homeassistant/components/google/calendar.py
+++ b/homeassistant/components/google/calendar.py
@@ -3,7 +3,7 @@
from datetime import timedelta
import logging
-from httplib2 import ServerNotFoundError # pylint: disable=import-error
+from httplib2 import ServerNotFoundError
from homeassistant.components.calendar import (
ENTITY_ID_FORMAT,
@@ -11,17 +11,14 @@
calculate_offset,
is_offset_reached,
)
+from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.util import Throttle, dt
from . import (
CONF_CAL_ID,
- CONF_DEVICE_ID,
- CONF_ENTITIES,
CONF_IGNORE_AVAILABILITY,
CONF_MAX_RESULTS,
- CONF_NAME,
- CONF_OFFSET,
CONF_SEARCH,
CONF_TRACK,
DEFAULT_CONF_OFFSET,
@@ -83,7 +80,7 @@ def __init__(self, calendar_service, calendar, data, entity_id):
self.entity_id = entity_id
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
return {"offset_reached": self._offset_reached}
diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json
index 6df116effa5fdf..859f1b332960b4 100644
--- a/homeassistant/components/google/manifest.json
+++ b/homeassistant/components/google/manifest.json
@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/google",
"requirements": [
"google-api-python-client==1.6.4",
- "httplib2==0.18.1",
+ "httplib2==0.19.0",
"oauth2client==4.0.0"
],
"codeowners": []
diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py
index 8f4ee3b51c4b34..7793ed4d65974a 100644
--- a/homeassistant/components/google_assistant/__init__.py
+++ b/homeassistant/components/google_assistant/__init__.py
@@ -1,11 +1,13 @@
"""Support for Actions on Google Assistant Smart Home Control."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict
+from typing import Any
import voluptuous as vol
# Typing imports
-from homeassistant.const import CONF_NAME
+from homeassistant.const import CONF_API_KEY, CONF_NAME
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
@@ -35,7 +37,6 @@
_LOGGER = logging.getLogger(__name__)
CONF_ALLOW_UNLOCK = "allow_unlock"
-CONF_API_KEY = "api_key"
ENTITY_SCHEMA = vol.Schema(
{
@@ -88,7 +89,7 @@ def _check_report_state(data):
CONFIG_SCHEMA = vol.Schema({DOMAIN: GOOGLE_ASSISTANT_SCHEMA}, extra=vol.ALLOW_EXTRA)
-async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
+async def async_setup(hass: HomeAssistant, yaml_config: dict[str, Any]):
"""Activate Google Actions component."""
config = yaml_config.get(DOMAIN, {})
diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py
index b4900d83b64d12..7eb69d0872432f 100644
--- a/homeassistant/components/google_assistant/helpers.py
+++ b/homeassistant/components/google_assistant/helpers.py
@@ -1,10 +1,11 @@
"""Helper classes for Google Assistant integration."""
+from __future__ import annotations
+
from abc import ABC, abstractmethod
from asyncio import gather
from collections.abc import Mapping
import logging
import pprint
-from typing import Dict, List, Optional, Tuple
from aiohttp.web import json_response
@@ -14,9 +15,10 @@
ATTR_SUPPORTED_FEATURES,
CLOUD_NEVER_EXPOSED_ENTITIES,
CONF_NAME,
+ EVENT_HOMEASSISTANT_STARTED,
STATE_UNAVAILABLE,
)
-from homeassistant.core import Context, HomeAssistant, State, callback
+from homeassistant.core import Context, CoreState, HomeAssistant, State, callback
from homeassistant.helpers.area_registry import AreaEntry
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.entity_registry import RegistryEntry
@@ -44,7 +46,7 @@
async def _get_entity_and_device(
hass, entity_id
-) -> Optional[Tuple[RegistryEntry, DeviceEntry]]:
+) -> tuple[RegistryEntry, DeviceEntry] | None:
"""Fetch the entity and device entries for a entity_id."""
dev_reg, ent_reg = await gather(
hass.helpers.device_registry.async_get_registry(),
@@ -58,7 +60,7 @@ async def _get_entity_and_device(
return entity_entry, device_entry
-async def _get_area(hass, entity_entry, device_entry) -> Optional[AreaEntry]:
+async def _get_area(hass, entity_entry, device_entry) -> AreaEntry | None:
"""Calculate the area for an entity."""
if entity_entry and entity_entry.area_id:
area_id = entity_entry.area_id
@@ -71,7 +73,7 @@ async def _get_area(hass, entity_entry, device_entry) -> Optional[AreaEntry]:
return area_reg.areas.get(area_id)
-async def _get_device_info(device_entry) -> Optional[Dict[str, str]]:
+async def _get_device_info(device_entry) -> dict[str, str] | None:
"""Retrieve the device info for a device."""
if not device_entry:
return None
@@ -103,6 +105,16 @@ async def async_initialize(self):
self._store = GoogleConfigStore(self.hass)
await self._store.async_load()
+ if self.hass.state == CoreState.running:
+ await self.async_sync_entities_all()
+ return
+
+ async def sync_google(_):
+ """Sync entities to Google."""
+ await self.async_sync_entities_all()
+
+ self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, sync_google)
+
@property
def enabled(self):
"""Return if Google is enabled."""
@@ -192,7 +204,10 @@ async def async_sync_entities(self, agent_user_id: str):
"""Sync all entities to Google."""
# Remove any pending sync
self._google_sync_unsub.pop(agent_user_id, lambda: None)()
- return await self._async_request_sync_devices(agent_user_id)
+ status = await self._async_request_sync_devices(agent_user_id)
+ if status == 404:
+ await self.async_disconnect_agent_user(agent_user_id)
+ return status
async def async_sync_entities_all(self):
"""Sync all entities to Google for all registered agents."""
@@ -254,13 +269,17 @@ def async_enable_local_sdk(self):
if webhook_id is None:
return
- webhook.async_register(
- self.hass,
- DOMAIN,
- "Local Support",
- webhook_id,
- self._handle_local_webhook,
- )
+ try:
+ webhook.async_register(
+ self.hass,
+ DOMAIN,
+ "Local Support",
+ webhook_id,
+ self._handle_local_webhook,
+ )
+ except ValueError:
+ _LOGGER.info("Webhook handler is already defined!")
+ return
self._local_sdk_active = True
@@ -344,7 +363,7 @@ def __init__(
user_id: str,
source: str,
request_id: str,
- devices: Optional[List[dict]],
+ devices: list[dict] | None,
):
"""Initialize the request data."""
self.config = config
@@ -578,7 +597,7 @@ def deep_update(target, source):
@callback
-def async_get_entities(hass, config) -> List[GoogleEntity]:
+def async_get_entities(hass, config) -> list[GoogleEntity]:
"""Return all entities that are supported by Google."""
entities = []
for state in hass.states.async_all():
diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py
index 5cf1cb143792cd..3787a63a514f5b 100644
--- a/homeassistant/components/google_assistant/http.py
+++ b/homeassistant/components/google_assistant/http.py
@@ -135,11 +135,12 @@ def should_2fa(self, state):
async def _async_request_sync_devices(self, agent_user_id: str):
if CONF_SERVICE_ACCOUNT in self._config:
- await self.async_call_homegraph_api(
+ return await self.async_call_homegraph_api(
REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id}
)
- else:
- _LOGGER.error("No configuration for request_sync available")
+
+ _LOGGER.error("No configuration for request_sync available")
+ return HTTP_INTERNAL_SERVER_ERROR
async def _async_update_token(self, force=False):
if CONF_SERVICE_ACCOUNT not in self._config:
diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py
index 8943d4d211e70e..cdfb06c5c39631 100644
--- a/homeassistant/components/google_assistant/report_state.py
+++ b/homeassistant/components/google_assistant/report_state.py
@@ -38,41 +38,58 @@ async def async_entity_state_listener(changed_entity, old_state, new_state):
if not entity.is_supported():
return
- if not checker.async_is_significant_change(new_state):
- return
-
try:
entity_data = entity.query_serialize()
except SmartHomeError as err:
_LOGGER.debug("Not reporting state for %s: %s", changed_entity, err.code)
return
+ if not checker.async_is_significant_change(new_state, extra_arg=entity_data):
+ return
+
_LOGGER.debug("Reporting state for %s: %s", changed_entity, entity_data)
await google_config.async_report_state_all(
{"devices": {"states": {changed_entity: entity_data}}}
)
+ @callback
+ def extra_significant_check(
+ hass: HomeAssistant,
+ old_state: str,
+ old_attrs: dict,
+ old_extra_arg: dict,
+ new_state: str,
+ new_attrs: dict,
+ new_extra_arg: dict,
+ ):
+ """Check if the serialized data has changed."""
+ return old_extra_arg != new_extra_arg
+
async def inital_report(_now):
"""Report initially all states."""
nonlocal unsub, checker
entities = {}
- checker = await create_checker(hass, DOMAIN)
+ checker = await create_checker(hass, DOMAIN, extra_significant_check)
for entity in async_get_entities(hass, google_config):
if not entity.should_expose():
continue
+ try:
+ entity_data = entity.query_serialize()
+ except SmartHomeError:
+ continue
+
# Tell our significant change checker that we're reporting
# So it knows with subsequent changes what was already reported.
- if not checker.async_is_significant_change(entity.state):
+ if not checker.async_is_significant_change(
+ entity.state, extra_arg=entity_data
+ ):
continue
- try:
- entities[entity.entity_id] = entity.query_serialize()
- except SmartHomeError:
- continue
+ entities[entity.entity_id] = entity_data
if not entities:
return
diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py
index b5dc2afd3e284c..384c5bfd0ae29d 100644
--- a/homeassistant/components/google_assistant/trait.py
+++ b/homeassistant/components/google_assistant/trait.py
@@ -1,6 +1,7 @@
"""Implement the Google Smart Home traits."""
+from __future__ import annotations
+
import logging
-from typing import List, Optional
from homeassistant.components import (
alarm_control_panel,
@@ -27,6 +28,7 @@
ATTR_CODE,
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
+ ATTR_MODE,
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
CAST_APP_ID_HOMEASSISTANT,
@@ -152,7 +154,7 @@ def _google_temp_unit(units):
return "C"
-def _next_selected(items: List[str], selected: Optional[str]) -> Optional[str]:
+def _next_selected(items: list[str], selected: str | None) -> str | None:
"""Return the next item in a item list starting at given value.
If selected is missing in items, None is returned
@@ -762,7 +764,7 @@ def sync_attributes(self):
mode in modes for mode in ("heatcool", "heat", "cool")
):
modes.append("on")
- response["availableThermostatModes"] = ",".join(modes)
+ response["availableThermostatModes"] = modes
return response
@@ -1276,6 +1278,7 @@ def sync_attributes(self):
return {
"availableFanSpeeds": {"speeds": speeds, "ordered": True},
"reversible": reversible,
+ "supportsFanSpeedPercent": True,
}
def query_attributes(self):
@@ -1289,9 +1292,11 @@ def query_attributes(self):
response["currentFanSpeedSetting"] = speed
if domain == fan.DOMAIN:
speed = attrs.get(fan.ATTR_SPEED)
+ percent = attrs.get(fan.ATTR_PERCENTAGE) or 0
if speed is not None:
response["on"] = speed != fan.SPEED_OFF
response["currentFanSpeedSetting"] = speed
+ response["currentFanSpeedPercent"] = percent
return response
async def execute(self, command, data, params, challenge):
@@ -1309,13 +1314,20 @@ async def execute(self, command, data, params, challenge):
context=data.context,
)
if domain == fan.DOMAIN:
+ service_params = {
+ ATTR_ENTITY_ID: self.state.entity_id,
+ }
+ if "fanSpeedPercent" in params:
+ service = fan.SERVICE_SET_PERCENTAGE
+ service_params[fan.ATTR_PERCENTAGE] = params["fanSpeedPercent"]
+ else:
+ service = fan.SERVICE_SET_SPEED
+ service_params[fan.ATTR_SPEED] = params["fanSpeed"]
+
await self.hass.services.async_call(
fan.DOMAIN,
- fan.SERVICE_SET_SPEED,
- {
- ATTR_ENTITY_ID: self.state.entity_id,
- fan.ATTR_SPEED: params["fanSpeed"],
- },
+ service,
+ service_params,
blocking=True,
context=data.context,
)
@@ -1414,11 +1426,10 @@ def query_attributes(self):
elif self.state.domain == input_select.DOMAIN:
mode_settings["option"] = self.state.state
elif self.state.domain == humidifier.DOMAIN:
- if humidifier.ATTR_MODE in attrs:
- mode_settings["mode"] = attrs.get(humidifier.ATTR_MODE)
- elif self.state.domain == light.DOMAIN:
- if light.ATTR_EFFECT in attrs:
- mode_settings["effect"] = attrs.get(light.ATTR_EFFECT)
+ if ATTR_MODE in attrs:
+ mode_settings["mode"] = attrs.get(ATTR_MODE)
+ elif self.state.domain == light.DOMAIN and light.ATTR_EFFECT in attrs:
+ mode_settings["effect"] = attrs.get(light.ATTR_EFFECT)
if mode_settings:
response["on"] = self.state.state not in (STATE_OFF, STATE_UNKNOWN)
@@ -1450,7 +1461,7 @@ async def execute(self, command, data, params, challenge):
humidifier.DOMAIN,
humidifier.SERVICE_SET_MODE,
{
- humidifier.ATTR_MODE: requested_mode,
+ ATTR_MODE: requested_mode,
ATTR_ENTITY_ID: self.state.entity_id,
},
blocking=True,
@@ -1606,15 +1617,17 @@ def sync_attributes(self):
if self.state.domain == binary_sensor.DOMAIN:
response["queryOnlyOpenClose"] = True
response["discreteOnlyOpenClose"] = True
- elif self.state.domain == cover.DOMAIN:
- if features & cover.SUPPORT_SET_POSITION == 0:
- response["discreteOnlyOpenClose"] = True
+ elif (
+ self.state.domain == cover.DOMAIN
+ and features & cover.SUPPORT_SET_POSITION == 0
+ ):
+ response["discreteOnlyOpenClose"] = True
- if (
- features & cover.SUPPORT_OPEN == 0
- and features & cover.SUPPORT_CLOSE == 0
- ):
- response["queryOnlyOpenClose"] = True
+ if (
+ features & cover.SUPPORT_OPEN == 0
+ and features & cover.SUPPORT_CLOSE == 0
+ ):
+ response["queryOnlyOpenClose"] = True
if self.state.attributes.get(ATTR_ASSUMED_STATE):
response["commandOnlyOpenClose"] = True
@@ -1675,17 +1688,17 @@ async def execute(self, command, data, params, challenge):
else:
position = params["openPercent"]
- if features & cover.SUPPORT_SET_POSITION:
- service = cover.SERVICE_SET_COVER_POSITION
- if position > 0:
- should_verify = True
- svc_params[cover.ATTR_POSITION] = position
- elif position == 0:
+ if position == 0:
service = cover.SERVICE_CLOSE_COVER
should_verify = False
elif position == 100:
service = cover.SERVICE_OPEN_COVER
should_verify = True
+ elif features & cover.SUPPORT_SET_POSITION:
+ service = cover.SERVICE_SET_COVER_POSITION
+ if position > 0:
+ should_verify = True
+ svc_params[cover.ATTR_POSITION] = position
else:
raise SmartHomeError(
ERR_NOT_SUPPORTED, "No support for partial open close"
@@ -1928,12 +1941,10 @@ def sync_attributes(self):
def query_attributes(self):
"""Return the attributes of this trait for this entity."""
-
return {}
async def execute(self, command, data, params, challenge):
"""Execute a media command."""
-
service_attrs = {ATTR_ENTITY_ID: self.state.entity_id}
if command == COMMAND_MEDIA_SEEK_RELATIVE:
diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py
index 8d7a675860c07c..365c118e99e7f1 100644
--- a/homeassistant/components/google_pubsub/__init__.py
+++ b/homeassistant/components/google_pubsub/__init__.py
@@ -1,9 +1,11 @@
"""Support for Google Cloud Pub/Sub."""
+from __future__ import annotations
+
import datetime
import json
import logging
import os
-from typing import Any, Dict
+from typing import Any
from google.cloud import pubsub_v1
import voluptuous as vol
@@ -37,7 +39,7 @@
)
-def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
+def setup(hass: HomeAssistant, yaml_config: dict[str, Any]):
"""Activate Google Pub/Sub component."""
config = yaml_config[DOMAIN]
@@ -57,9 +59,7 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
service_principal_path
)
- topic_path = publisher.topic_path( # pylint: disable=no-member
- project_id, topic_name
- )
+ topic_path = publisher.topic_path(project_id, topic_name)
encoder = DateTimeJSONEncoder()
diff --git a/homeassistant/components/google_translate/manifest.json b/homeassistant/components/google_translate/manifest.json
index c5b3edc879869f..64d19bed277228 100644
--- a/homeassistant/components/google_translate/manifest.json
+++ b/homeassistant/components/google_translate/manifest.json
@@ -2,6 +2,6 @@
"domain": "google_translate",
"name": "Google Translate Text-to-Speech",
"documentation": "https://www.home-assistant.io/integrations/google_translate",
- "requirements": ["gTTS==2.2.1"],
+ "requirements": ["gTTS==2.2.2"],
"codeowners": []
}
diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py
index 9d9a7cffe1ddd4..ef53db9c815c85 100644
--- a/homeassistant/components/google_travel_time/__init__.py
+++ b/homeassistant/components/google_travel_time/__init__.py
@@ -1 +1,31 @@
"""The google_travel_time component."""
+import asyncio
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+
+PLATFORMS = ["sensor"]
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up Google Maps Travel Time from a config entry."""
+ for component in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, component)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, component)
+ for component in PLATFORMS
+ ]
+ )
+ )
+
+ return unload_ok
diff --git a/homeassistant/components/google_travel_time/config_flow.py b/homeassistant/components/google_travel_time/config_flow.py
new file mode 100644
index 00000000000000..5c66220af02198
--- /dev/null
+++ b/homeassistant/components/google_travel_time/config_flow.py
@@ -0,0 +1,166 @@
+"""Config flow for Google Maps Travel Time integration."""
+import logging
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME
+from homeassistant.core import callback
+import homeassistant.helpers.config_validation as cv
+from homeassistant.util import slugify
+
+from .const import (
+ ALL_LANGUAGES,
+ ARRIVAL_TIME,
+ AVOID,
+ CONF_ARRIVAL_TIME,
+ CONF_AVOID,
+ CONF_DEPARTURE_TIME,
+ CONF_DESTINATION,
+ CONF_LANGUAGE,
+ CONF_ORIGIN,
+ CONF_TIME,
+ CONF_TIME_TYPE,
+ CONF_TRAFFIC_MODEL,
+ CONF_TRANSIT_MODE,
+ CONF_TRANSIT_ROUTING_PREFERENCE,
+ CONF_UNITS,
+ DEFAULT_NAME,
+ DEPARTURE_TIME,
+ DOMAIN,
+ TIME_TYPES,
+ TRANSIT_PREFS,
+ TRANSPORT_TYPE,
+ TRAVEL_MODE,
+ TRAVEL_MODEL,
+ UNITS,
+)
+from .helpers import is_valid_config_entry
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class GoogleOptionsFlow(config_entries.OptionsFlow):
+ """Handle an options flow for Google Travel Time."""
+
+ def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
+ """Initialize google options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(self, user_input=None):
+ """Handle the initial step."""
+ if user_input is not None:
+ time_type = user_input.pop(CONF_TIME_TYPE)
+ if time := user_input.pop(CONF_TIME, None):
+ if time_type == ARRIVAL_TIME:
+ user_input[CONF_ARRIVAL_TIME] = time
+ else:
+ user_input[CONF_DEPARTURE_TIME] = time
+ return self.async_create_entry(title="", data=user_input)
+
+ if CONF_ARRIVAL_TIME in self.config_entry.options:
+ default_time_type = ARRIVAL_TIME
+ default_time = self.config_entry.options[CONF_ARRIVAL_TIME]
+ else:
+ default_time_type = DEPARTURE_TIME
+ default_time = self.config_entry.options.get(CONF_ARRIVAL_TIME)
+
+ return self.async_show_form(
+ step_id="init",
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_MODE, default=self.config_entry.options[CONF_MODE]
+ ): vol.In(TRAVEL_MODE),
+ vol.Optional(
+ CONF_LANGUAGE,
+ default=self.config_entry.options.get(CONF_LANGUAGE),
+ ): vol.In(ALL_LANGUAGES),
+ vol.Optional(
+ CONF_AVOID, default=self.config_entry.options.get(CONF_AVOID)
+ ): vol.In(AVOID),
+ vol.Optional(
+ CONF_UNITS, default=self.config_entry.options[CONF_UNITS]
+ ): vol.In(UNITS),
+ vol.Optional(CONF_TIME_TYPE, default=default_time_type): vol.In(
+ TIME_TYPES
+ ),
+ vol.Optional(CONF_TIME, default=default_time): cv.string,
+ vol.Optional(
+ CONF_TRAFFIC_MODEL,
+ default=self.config_entry.options.get(CONF_TRAFFIC_MODEL),
+ ): vol.In(TRAVEL_MODEL),
+ vol.Optional(
+ CONF_TRANSIT_MODE,
+ default=self.config_entry.options.get(CONF_TRANSIT_MODE),
+ ): vol.In(TRANSPORT_TYPE),
+ vol.Optional(
+ CONF_TRANSIT_ROUTING_PREFERENCE,
+ default=self.config_entry.options.get(
+ CONF_TRANSIT_ROUTING_PREFERENCE
+ ),
+ ): vol.In(TRANSIT_PREFS),
+ }
+ ),
+ )
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Google Maps Travel Time."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(
+ config_entry: config_entries.ConfigEntry,
+ ) -> GoogleOptionsFlow:
+ """Get the options flow for this handler."""
+ return GoogleOptionsFlow(config_entry)
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ errors = {}
+ if user_input is not None:
+ if await self.hass.async_add_executor_job(
+ is_valid_config_entry,
+ self.hass,
+ _LOGGER,
+ user_input[CONF_API_KEY],
+ user_input[CONF_ORIGIN],
+ user_input[CONF_DESTINATION],
+ ):
+ await self.async_set_unique_id(
+ slugify(
+ f"{DOMAIN}_{user_input[CONF_ORIGIN]}_{user_input[CONF_DESTINATION]}"
+ )
+ )
+ self._abort_if_unique_id_configured()
+ return self.async_create_entry(
+ title=user_input.get(
+ CONF_NAME,
+ (
+ f"{DEFAULT_NAME}: {user_input[CONF_ORIGIN]} -> "
+ f"{user_input[CONF_DESTINATION]}"
+ ),
+ ),
+ data=user_input,
+ )
+
+ # If we get here, it's because we couldn't connect
+ errors["base"] = "cannot_connect"
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_API_KEY): cv.string,
+ vol.Required(CONF_DESTINATION): cv.string,
+ vol.Required(CONF_ORIGIN): cv.string,
+ }
+ ),
+ errors=errors,
+ )
+
+ async_step_import = async_step_user
diff --git a/homeassistant/components/google_travel_time/const.py b/homeassistant/components/google_travel_time/const.py
new file mode 100644
index 00000000000000..6b9b77242ba29d
--- /dev/null
+++ b/homeassistant/components/google_travel_time/const.py
@@ -0,0 +1,89 @@
+"""Constants for Google Travel Time."""
+from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC
+
+DOMAIN = "google_travel_time"
+
+ATTRIBUTION = "Powered by Google"
+
+CONF_DESTINATION = "destination"
+CONF_OPTIONS = "options"
+CONF_ORIGIN = "origin"
+CONF_TRAVEL_MODE = "travel_mode"
+CONF_LANGUAGE = "language"
+CONF_AVOID = "avoid"
+CONF_UNITS = "units"
+CONF_ARRIVAL_TIME = "arrival_time"
+CONF_DEPARTURE_TIME = "departure_time"
+CONF_TRAFFIC_MODEL = "traffic_model"
+CONF_TRANSIT_MODE = "transit_mode"
+CONF_TRANSIT_ROUTING_PREFERENCE = "transit_routing_preference"
+CONF_TIME_TYPE = "time_type"
+CONF_TIME = "time"
+
+ARRIVAL_TIME = "Arrival Time"
+DEPARTURE_TIME = "Departure Time"
+TIME_TYPES = [ARRIVAL_TIME, DEPARTURE_TIME]
+
+DEFAULT_NAME = "Google Travel Time"
+
+TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"]
+
+ALL_LANGUAGES = [
+ "ar",
+ "bg",
+ "bn",
+ "ca",
+ "cs",
+ "da",
+ "de",
+ "el",
+ "en",
+ "es",
+ "eu",
+ "fa",
+ "fi",
+ "fr",
+ "gl",
+ "gu",
+ "hi",
+ "hr",
+ "hu",
+ "id",
+ "it",
+ "iw",
+ "ja",
+ "kn",
+ "ko",
+ "lt",
+ "lv",
+ "ml",
+ "mr",
+ "nl",
+ "no",
+ "pl",
+ "pt",
+ "pt-BR",
+ "pt-PT",
+ "ro",
+ "ru",
+ "sk",
+ "sl",
+ "sr",
+ "sv",
+ "ta",
+ "te",
+ "th",
+ "tl",
+ "tr",
+ "uk",
+ "vi",
+ "zh-CN",
+ "zh-TW",
+]
+
+AVOID = ["tolls", "highways", "ferries", "indoor"]
+TRANSIT_PREFS = ["less_walking", "fewer_transfers"]
+TRANSPORT_TYPE = ["bus", "subway", "train", "tram", "rail"]
+TRAVEL_MODE = ["driving", "walking", "bicycling", "transit"]
+TRAVEL_MODEL = ["best_guess", "pessimistic", "optimistic"]
+UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL]
diff --git a/homeassistant/components/google_travel_time/helpers.py b/homeassistant/components/google_travel_time/helpers.py
new file mode 100644
index 00000000000000..425d21ee181548
--- /dev/null
+++ b/homeassistant/components/google_travel_time/helpers.py
@@ -0,0 +1,72 @@
+"""Helpers for Google Time Travel integration."""
+from googlemaps import Client
+from googlemaps.distance_matrix import distance_matrix
+from googlemaps.exceptions import ApiError
+
+from homeassistant.components.google_travel_time.const import TRACKABLE_DOMAINS
+from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
+from homeassistant.helpers import location
+
+
+def is_valid_config_entry(hass, logger, api_key, origin, destination):
+ """Return whether the config entry data is valid."""
+ origin = resolve_location(hass, logger, origin)
+ destination = resolve_location(hass, logger, destination)
+ client = Client(api_key, timeout=10)
+ try:
+ distance_matrix(client, origin, destination, mode="driving")
+ except ApiError:
+ return False
+ return True
+
+
+def resolve_location(hass, logger, loc):
+ """Resolve a location."""
+ if loc.split(".", 1)[0] in TRACKABLE_DOMAINS:
+ return get_location_from_entity(hass, logger, loc)
+
+ return resolve_zone(hass, loc)
+
+
+def get_location_from_entity(hass, logger, entity_id):
+ """Get the location from the entity state or attributes."""
+ entity = hass.states.get(entity_id)
+
+ if entity is None:
+ logger.error("Unable to find entity %s", entity_id)
+ return None
+
+ # Check if the entity has location attributes
+ if location.has_location(entity):
+ return get_location_from_attributes(entity)
+
+ # Check if device is in a zone
+ zone_entity = hass.states.get("zone.%s" % entity.state)
+ if location.has_location(zone_entity):
+ logger.debug(
+ "%s is in %s, getting zone location", entity_id, zone_entity.entity_id
+ )
+ return get_location_from_attributes(zone_entity)
+
+ # If zone was not found in state then use the state as the location
+ if entity_id.startswith("sensor."):
+ return entity.state
+
+ # When everything fails just return nothing
+ return None
+
+
+def get_location_from_attributes(entity):
+ """Get the lat/long string from an entities attributes."""
+ attr = entity.attributes
+ return f"{attr.get(ATTR_LATITUDE)},{attr.get(ATTR_LONGITUDE)}"
+
+
+def resolve_zone(hass, friendly_name):
+ """Resolve a location from a zone's friendly name."""
+ entities = hass.states.all()
+ for entity in entities:
+ if entity.domain == "zone" and entity.name == friendly_name:
+ return get_location_from_attributes(entity)
+
+ return friendly_name
diff --git a/homeassistant/components/google_travel_time/manifest.json b/homeassistant/components/google_travel_time/manifest.json
index 2d97b92ccb6b69..d8981fe4283d42 100644
--- a/homeassistant/components/google_travel_time/manifest.json
+++ b/homeassistant/components/google_travel_time/manifest.json
@@ -2,6 +2,9 @@
"domain": "google_travel_time",
"name": "Google Maps Travel Time",
"documentation": "https://www.home-assistant.io/integrations/google_travel_time",
- "requirements": ["googlemaps==2.5.1"],
- "codeowners": []
-}
+ "requirements": [
+ "googlemaps==2.5.1"
+ ],
+ "codeowners": [],
+ "config_flow": true
+}
\ No newline at end of file
diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py
index 098c6d2d59c9ad..3980d0323b2dc4 100644
--- a/homeassistant/components/google_travel_time/sensor.py
+++ b/homeassistant/components/google_travel_time/sensor.py
@@ -1,99 +1,60 @@
"""Support for Google travel time sensors."""
+from __future__ import annotations
+
from datetime import datetime, timedelta
import logging
+from typing import Callable
-import googlemaps
+from googlemaps import Client
+from googlemaps.distance_matrix import distance_matrix
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
ATTR_ATTRIBUTION,
- ATTR_LATITUDE,
- ATTR_LONGITUDE,
CONF_API_KEY,
CONF_MODE,
CONF_NAME,
EVENT_HOMEASSISTANT_START,
TIME_MINUTES,
)
-from homeassistant.helpers import location
+from homeassistant.core import CoreState, HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
-_LOGGER = logging.getLogger(__name__)
-
-ATTRIBUTION = "Powered by Google"
-
-CONF_DESTINATION = "destination"
-CONF_OPTIONS = "options"
-CONF_ORIGIN = "origin"
-CONF_TRAVEL_MODE = "travel_mode"
+from .const import (
+ ALL_LANGUAGES,
+ ATTRIBUTION,
+ AVOID,
+ CONF_ARRIVAL_TIME,
+ CONF_AVOID,
+ CONF_DEPARTURE_TIME,
+ CONF_DESTINATION,
+ CONF_LANGUAGE,
+ CONF_OPTIONS,
+ CONF_ORIGIN,
+ CONF_TRAFFIC_MODEL,
+ CONF_TRANSIT_MODE,
+ CONF_TRANSIT_ROUTING_PREFERENCE,
+ CONF_TRAVEL_MODE,
+ CONF_UNITS,
+ DEFAULT_NAME,
+ DOMAIN,
+ TRACKABLE_DOMAINS,
+ TRANSIT_PREFS,
+ TRANSPORT_TYPE,
+ TRAVEL_MODE,
+ TRAVEL_MODEL,
+ UNITS,
+)
+from .helpers import get_location_from_entity, is_valid_config_entry, resolve_zone
-DEFAULT_NAME = "Google Travel Time"
+_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=5)
-ALL_LANGUAGES = [
- "ar",
- "bg",
- "bn",
- "ca",
- "cs",
- "da",
- "de",
- "el",
- "en",
- "es",
- "eu",
- "fa",
- "fi",
- "fr",
- "gl",
- "gu",
- "hi",
- "hr",
- "hu",
- "id",
- "it",
- "iw",
- "ja",
- "kn",
- "ko",
- "lt",
- "lv",
- "ml",
- "mr",
- "nl",
- "no",
- "pl",
- "pt",
- "pt-BR",
- "pt-PT",
- "ro",
- "ru",
- "sk",
- "sl",
- "sr",
- "sv",
- "ta",
- "te",
- "th",
- "tl",
- "tr",
- "uk",
- "vi",
- "zh-CN",
- "zh-TW",
-]
-
-AVOID = ["tolls", "highways", "ferries", "indoor"]
-TRANSIT_PREFS = ["less_walking", "fewer_transfers"]
-TRANSPORT_TYPE = ["bus", "subway", "train", "tram", "rail"]
-TRAVEL_MODE = ["driving", "walking", "bicycling", "transit"]
-TRAVEL_MODEL = ["best_guess", "pessimistic", "optimistic"]
-UNITS = ["metric", "imperial"]
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_API_KEY): cv.string,
@@ -106,23 +67,22 @@
vol.Schema(
{
vol.Optional(CONF_MODE, default="driving"): vol.In(TRAVEL_MODE),
- vol.Optional("language"): vol.In(ALL_LANGUAGES),
- vol.Optional("avoid"): vol.In(AVOID),
- vol.Optional("units"): vol.In(UNITS),
- vol.Exclusive("arrival_time", "time"): cv.string,
- vol.Exclusive("departure_time", "time"): cv.string,
- vol.Optional("traffic_model"): vol.In(TRAVEL_MODEL),
- vol.Optional("transit_mode"): vol.In(TRANSPORT_TYPE),
- vol.Optional("transit_routing_preference"): vol.In(TRANSIT_PREFS),
+ vol.Optional(CONF_LANGUAGE): vol.In(ALL_LANGUAGES),
+ vol.Optional(CONF_AVOID): vol.In(AVOID),
+ vol.Optional(CONF_UNITS): vol.In(UNITS),
+ vol.Exclusive(CONF_ARRIVAL_TIME, "time"): cv.string,
+ vol.Exclusive(CONF_DEPARTURE_TIME, "time"): cv.string,
+ vol.Optional(CONF_TRAFFIC_MODEL): vol.In(TRAVEL_MODEL),
+ vol.Optional(CONF_TRANSIT_MODE): vol.In(TRANSPORT_TYPE),
+ vol.Optional(CONF_TRANSIT_ROUTING_PREFERENCE): vol.In(
+ TRANSIT_PREFS
+ ),
}
),
),
}
)
-TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"]
-DATA_KEY = "google_travel_time"
-
def convert_time_to_utc(timestr):
"""Take a string like 08:00:00 and convert it to a unix timestamp."""
@@ -134,63 +94,88 @@ def convert_time_to_utc(timestr):
return dt_util.as_timestamp(combined)
-def setup_platform(hass, config, add_entities_callback, discovery_info=None):
- """Set up the Google travel time platform."""
-
- def run_setup(event):
- """
- Delay the setup until Home Assistant is fully initialized.
-
- This allows any entities to be created already
- """
- hass.data.setdefault(DATA_KEY, [])
- options = config.get(CONF_OPTIONS)
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: Callable[[list[SensorEntity], bool], None],
+) -> None:
+ """Set up a Google travel time sensor entry."""
+ name = None
+ if not config_entry.options:
+ new_data = config_entry.data.copy()
+ options = new_data.pop(CONF_OPTIONS, {})
+ name = new_data.pop(CONF_NAME, None)
- if options.get("units") is None:
- options["units"] = hass.config.units.name
+ if CONF_UNITS not in options:
+ options[CONF_UNITS] = hass.config.units.name
- travel_mode = config.get(CONF_TRAVEL_MODE)
- mode = options.get(CONF_MODE)
-
- if travel_mode is not None:
+ if CONF_TRAVEL_MODE in new_data:
wstr = (
"Google Travel Time: travel_mode is deprecated, please "
"add mode to the options dictionary instead!"
)
_LOGGER.warning(wstr)
- if mode is None:
+ travel_mode = new_data.pop(CONF_TRAVEL_MODE)
+ if CONF_MODE not in options:
options[CONF_MODE] = travel_mode
- titled_mode = options.get(CONF_MODE).title()
- formatted_name = f"{DEFAULT_NAME} - {titled_mode}"
- name = config.get(CONF_NAME, formatted_name)
- api_key = config.get(CONF_API_KEY)
- origin = config.get(CONF_ORIGIN)
- destination = config.get(CONF_DESTINATION)
+ if CONF_MODE not in options:
+ options[CONF_MODE] = "driving"
- sensor = GoogleTravelTimeSensor(
- hass, name, api_key, origin, destination, options
+ hass.config_entries.async_update_entry(
+ config_entry, data=new_data, options=options
)
- hass.data[DATA_KEY].append(sensor)
- if sensor.valid_api_connection:
- add_entities_callback([sensor])
+ api_key = config_entry.data[CONF_API_KEY]
+ origin = config_entry.data[CONF_ORIGIN]
+ destination = config_entry.data[CONF_DESTINATION]
+ name = name or f"{DEFAULT_NAME}: {origin} -> {destination}"
+
+ if not await hass.async_add_executor_job(
+ is_valid_config_entry, hass, _LOGGER, api_key, origin, destination
+ ):
+ raise ConfigEntryNotReady
+
+ client = Client(api_key, timeout=10)
+
+ sensor = GoogleTravelTimeSensor(
+ config_entry, name, api_key, origin, destination, client
+ )
+
+ async_add_entities([sensor], False)
- # Wait until start event is sent to load this component.
- hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup)
+async def async_setup_platform(
+ hass: HomeAssistant, config, add_entities_callback, discovery_info=None
+):
+ """Set up the Google travel time platform."""
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data=config,
+ )
+ )
+
+ _LOGGER.warning(
+ "Your Google 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"
+ )
-class GoogleTravelTimeSensor(Entity):
+
+class GoogleTravelTimeSensor(SensorEntity):
"""Representation of a Google travel time sensor."""
- def __init__(self, hass, name, api_key, origin, destination, options):
+ def __init__(self, config_entry, name, api_key, origin, destination, client):
"""Initialize the sensor."""
- self._hass = hass
self._name = name
- self._options = options
+ self._config_entry = config_entry
self._unit_of_measurement = TIME_MINUTES
self._matrix = None
- self.valid_api_connection = True
+ self._api_key = api_key
+ self._unique_id = config_entry.unique_id
+ self._client = client
# Check if location is a trackable entity
if origin.split(".", 1)[0] in TRACKABLE_DOMAINS:
@@ -203,13 +188,14 @@ def __init__(self, hass, name, api_key, origin, destination, options):
else:
self._destination = destination
- self._client = googlemaps.Client(api_key, timeout=10)
- try:
- self.update()
- except googlemaps.exceptions.ApiError as exp:
- _LOGGER.error(exp)
- self.valid_api_connection = False
- return
+ async def async_added_to_hass(self) -> None:
+ """Handle when entity is added."""
+ if self.hass.state != CoreState.running:
+ self.hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_START, self.first_update
+ )
+ else:
+ await self.first_update()
@property
def state(self):
@@ -224,19 +210,34 @@ def state(self):
return round(_data["duration"]["value"] / 60)
return None
+ @property
+ def device_info(self):
+ """Return device specific attributes."""
+ return {
+ "name": DOMAIN,
+ "identifiers": {(DOMAIN, self._api_key)},
+ "entry_type": "service",
+ }
+
+ @property
+ def unique_id(self) -> str:
+ """Return unique ID of entity."""
+ return self._unique_id
+
@property
def name(self):
"""Get the name of the sensor."""
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._matrix is None:
return None
res = self._matrix.copy()
- res.update(self._options)
+ options = self._config_entry.options.copy()
+ res.update(options)
del res["rows"]
_data = self._matrix["rows"][0]["elements"][0]
if "duration_in_traffic" in _data:
@@ -255,78 +256,43 @@ def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
return self._unit_of_measurement
+ async def first_update(self, _=None):
+ """Run the first update and write the state."""
+ await self.hass.async_add_executor_job(self.update)
+ self.async_write_ha_state()
+
def update(self):
"""Get the latest data from Google."""
- options_copy = self._options.copy()
- dtime = options_copy.get("departure_time")
- atime = options_copy.get("arrival_time")
+ options_copy = self._config_entry.options.copy()
+ dtime = options_copy.get(CONF_DEPARTURE_TIME)
+ atime = options_copy.get(CONF_ARRIVAL_TIME)
if dtime is not None and ":" in dtime:
- options_copy["departure_time"] = convert_time_to_utc(dtime)
+ options_copy[CONF_DEPARTURE_TIME] = convert_time_to_utc(dtime)
elif dtime is not None:
- options_copy["departure_time"] = dtime
+ options_copy[CONF_DEPARTURE_TIME] = dtime
elif atime is None:
- options_copy["departure_time"] = "now"
+ options_copy[CONF_DEPARTURE_TIME] = "now"
if atime is not None and ":" in atime:
- options_copy["arrival_time"] = convert_time_to_utc(atime)
+ options_copy[CONF_ARRIVAL_TIME] = convert_time_to_utc(atime)
elif atime is not None:
- options_copy["arrival_time"] = atime
+ options_copy[CONF_ARRIVAL_TIME] = atime
# Convert device_trackers to google friendly location
if hasattr(self, "_origin_entity_id"):
- self._origin = self._get_location_from_entity(self._origin_entity_id)
+ self._origin = get_location_from_entity(
+ self.hass, _LOGGER, self._origin_entity_id
+ )
if hasattr(self, "_destination_entity_id"):
- self._destination = self._get_location_from_entity(
- self._destination_entity_id
+ self._destination = get_location_from_entity(
+ self.hass, _LOGGER, self._destination_entity_id
)
- self._destination = self._resolve_zone(self._destination)
- self._origin = self._resolve_zone(self._origin)
+ self._destination = resolve_zone(self.hass, self._destination)
+ self._origin = resolve_zone(self.hass, self._origin)
if self._destination is not None and self._origin is not None:
- self._matrix = self._client.distance_matrix(
- self._origin, self._destination, **options_copy
+ self._matrix = distance_matrix(
+ self._client, self._origin, self._destination, **options_copy
)
-
- def _get_location_from_entity(self, entity_id):
- """Get the location from the entity state or attributes."""
- entity = self._hass.states.get(entity_id)
-
- if entity is None:
- _LOGGER.error("Unable to find entity %s", entity_id)
- self.valid_api_connection = False
- return None
-
- # Check if the entity has location attributes
- if location.has_location(entity):
- return self._get_location_from_attributes(entity)
-
- # Check if device is in a zone
- zone_entity = self._hass.states.get("zone.%s" % entity.state)
- if location.has_location(zone_entity):
- _LOGGER.debug(
- "%s is in %s, getting zone location", entity_id, zone_entity.entity_id
- )
- return self._get_location_from_attributes(zone_entity)
-
- # If zone was not found in state then use the state as the location
- if entity_id.startswith("sensor."):
- return entity.state
-
- # When everything fails just return nothing
- return None
-
- @staticmethod
- def _get_location_from_attributes(entity):
- """Get the lat/long string from an entities attributes."""
- attr = entity.attributes
- return f"{attr.get(ATTR_LATITUDE)},{attr.get(ATTR_LONGITUDE)}"
-
- def _resolve_zone(self, friendly_name):
- entities = self._hass.states.all()
- for entity in entities:
- if entity.domain == "zone" and entity.name == friendly_name:
- return self._get_location_from_attributes(entity)
-
- return friendly_name
diff --git a/homeassistant/components/google_travel_time/strings.json b/homeassistant/components/google_travel_time/strings.json
new file mode 100644
index 00000000000000..8dcc8f2fa1bc60
--- /dev/null
+++ b/homeassistant/components/google_travel_time/strings.json
@@ -0,0 +1,38 @@
+{
+ "title": "Google Maps Travel Time",
+ "config": {
+ "step": {
+ "user": {
+ "description": "When specifying the origin and destination, you can supply one or more locations separated by the pipe character, in the form of an address, latitude/longitude coordinates, or a Google place ID. When specifying the location using a Google place ID, the ID must be prefixed with `place_id:`.",
+ "data": {
+ "api_key": "[%key:common::config_flow::data::api_key%]",
+ "origin": "Origin",
+ "destination": "Destination"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_location%]"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "description": "You can optionally specify either a Departure Time or Arrival Time. If specifying a departure time, you can enter `now`, a Unix timestamp, or a 24 hour time string like `08:00:00`. If specifying an arrival time, you can use a Unix timestamp or a 24 hour time string like `08:00:00`",
+ "data": {
+ "mode": "Travel Mode",
+ "language": "Language",
+ "time_type": "Time Type",
+ "time": "Time",
+ "avoid": "Avoid",
+ "transit_mode": "Transit Mode",
+ "transit_routing_preference": "Transit Routing Preference",
+ "units": "Units"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/google_travel_time/translations/ca.json b/homeassistant/components/google_travel_time/translations/ca.json
new file mode 100644
index 00000000000000..0edced29690408
--- /dev/null
+++ b/homeassistant/components/google_travel_time/translations/ca.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Clau API",
+ "destination": "Destinaci\u00f3",
+ "origin": "Origen"
+ },
+ "description": "Quan especifiquis l'origen i la destinaci\u00f3, pots proporcionar m\u00e9s d'una ubicaci\u00f3 (les has de separar pel car\u00e0cter 'pipe'); poden ser en forma d'adre\u00e7a, coordenades de latitud/longitud o un identificador de lloc de Google. En especificar la ubicaci\u00f3 mitjan\u00e7ant un ID de lloc de Google, l'identificador ha de tenir el prefix `place_id:`."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid": "Evita",
+ "language": "Idioma",
+ "mode": "Mode de transport",
+ "time": "Temps",
+ "time_type": "Tipus de temps",
+ "transit_mode": "Tipus de transport",
+ "transit_routing_preference": "Prefer\u00e8ncia de rutes de tr\u00e0nsit",
+ "units": "Unitats"
+ },
+ "description": "Opcionalment, pots especificar una hora de sortida o una hora d'arribada. Si especifiques una hora de sortida, pots introduir `ara`, una marca de temps Unix o una cadena de temps de 24 hores com per exemple `08:00:00`. Si especifiques una hora d'arribada, pots utilitzar els mateixos formats excepte `ara`."
+ }
+ }
+ },
+ "title": "Temps de viatge de Google Maps"
+}
\ No newline at end of file
diff --git a/homeassistant/components/google_travel_time/translations/de.json b/homeassistant/components/google_travel_time/translations/de.json
new file mode 100644
index 00000000000000..c2a95e49afbd85
--- /dev/null
+++ b/homeassistant/components/google_travel_time/translations/de.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Standort ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-Schl\u00fcssel",
+ "destination": "Zielort",
+ "origin": "Startort"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "Sprache"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/google_travel_time/translations/en.json b/homeassistant/components/google_travel_time/translations/en.json
new file mode 100644
index 00000000000000..464d518a70be84
--- /dev/null
+++ b/homeassistant/components/google_travel_time/translations/en.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Location is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Key",
+ "destination": "Destination",
+ "origin": "Origin"
+ },
+ "description": "When specifying the origin and destination, you can supply one or more locations separated by the pipe character, in the form of an address, latitude/longitude coordinates, or a Google place ID. When specifying the location using a Google place ID, the ID must be prefixed with `place_id:`."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid": "Avoid",
+ "language": "Language",
+ "mode": "Travel Mode",
+ "time": "Time",
+ "time_type": "Time Type",
+ "transit_mode": "Transit Mode",
+ "transit_routing_preference": "Transit Routing Preference",
+ "units": "Units"
+ },
+ "description": "You can optionally specify either a Departure Time or Arrival Time. If specifying a departure time, you can enter `now`, a Unix timestamp, or a 24 hour time string like `08:00:00`. If specifying an arrival time, you can use a Unix timestamp or a 24 hour time string like `08:00:00`"
+ }
+ }
+ },
+ "title": "Google Maps Travel Time"
+}
\ No newline at end of file
diff --git a/homeassistant/components/google_travel_time/translations/et.json b/homeassistant/components/google_travel_time/translations/et.json
new file mode 100644
index 00000000000000..e99472f46a3bc4
--- /dev/null
+++ b/homeassistant/components/google_travel_time/translations/et.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Asukoht on juba m\u00e4\u00e4ratud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API v\u00f5ti",
+ "destination": "Sihtkoht",
+ "origin": "L\u00e4htekoht"
+ },
+ "description": "L\u00e4hte- ja sihtkoha m\u00e4\u00e4ramisel v\u00f5ib sisestada \u00fche v\u00f5i mitu eraldusm\u00e4rgiga eraldatud asukohta aadressi, laius- / pikkuskraadi koordinaatide v\u00f5i Google'i koha ID kujul. Asukoha m\u00e4\u00e4ramisel Google'i koha ID abil tuleb ID-le lisada eesliide \"place_id:\"."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid": "V\u00e4ldi",
+ "language": "Keel",
+ "mode": "Reisimise viis",
+ "time": "Aeg",
+ "time_type": "Aja t\u00fc\u00fcp",
+ "transit_mode": "Liikumisviis",
+ "transit_routing_preference": "Teekonna eelistused",
+ "units": "\u00dchikud"
+ },
+ "description": "Soovi korral saad m\u00e4\u00e4rata kas v\u00e4ljumisaja v\u00f5i saabumisaja. V\u00e4ljumisaja m\u00e4\u00e4ramisel saad sisestada \"kohe\", Unix-ajatempli v\u00f5i 24-tunnise ajastringi (nt 08:00:00). Saabumisaja m\u00e4\u00e4ramisel saad kasutada Unix-ajatemplit v\u00f5i 24-tunnist ajastringi nagu '08:00:00'"
+ }
+ }
+ },
+ "title": "Google Mapsi reisiaeg"
+}
\ No newline at end of file
diff --git a/homeassistant/components/google_travel_time/translations/fr.json b/homeassistant/components/google_travel_time/translations/fr.json
new file mode 100644
index 00000000000000..b5b59b5329c617
--- /dev/null
+++ b/homeassistant/components/google_travel_time/translations/fr.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "common::config_flow::data::api_key",
+ "origin": "Origine"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "Langue",
+ "units": "Unit\u00e9s"
+ }
+ }
+ }
+ },
+ "title": "Temps de trajet Google Maps"
+}
\ No newline at end of file
diff --git a/homeassistant/components/google_travel_time/translations/hu.json b/homeassistant/components/google_travel_time/translations/hu.json
new file mode 100644
index 00000000000000..5bee8045c4fe14
--- /dev/null
+++ b/homeassistant/components/google_travel_time/translations/hu.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A hely m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Csatlakoz\u00e1si hiba"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Api kucs",
+ "destination": "C\u00e9l",
+ "origin": "Eredet"
+ },
+ "description": "Az eredet \u00e9s a c\u00e9l megad\u00e1sakor megadhat egy vagy t\u00f6bb helyet a pipa karakterrel elv\u00e1lasztva, c\u00edm, sz\u00e9less\u00e9gi / hossz\u00fas\u00e1gi koordin\u00e1t\u00e1k vagy Google helyazonos\u00edt\u00f3 form\u00e1j\u00e1ban. Amikor a helyet megadja egy Google helyazonos\u00edt\u00f3val, akkor az azonos\u00edt\u00f3t el\u0151taggal kell ell\u00e1tni a `hely_azonos\u00edt\u00f3:` sz\u00f3val."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid": "Elker\u00fcl",
+ "language": "Nyelv",
+ "mode": "Utaz\u00e1si m\u00f3d",
+ "time": "Id\u0151",
+ "time_type": "Id\u0151 t\u00edpusa",
+ "transit_mode": "Tranzit m\u00f3d",
+ "transit_routing_preference": "Tranzit \u00fatv\u00e1laszt\u00e1si be\u00e1ll\u00edt\u00e1s",
+ "units": "Egys\u00e9gek"
+ },
+ "description": "Opcion\u00e1lisan megadhatja az indul\u00e1si id\u0151t vagy az \u00e9rkez\u00e9si id\u0151t. Indul\u00e1si id\u0151 megad\u00e1sakor megadhatja a \"most\", a Unix id\u0151b\u00e9lyegz\u0151t vagy a 24 \u00f3r\u00e1s id\u0151l\u00e1ncot, p\u00e9ld\u00e1ul a \"08:00:00\" karakterl\u00e1ncot. \u00c9rkez\u00e9si id\u0151 megad\u00e1sakor unix id\u0151b\u00e9lyeget vagy 24 \u00f3r\u00e1s id\u0151l\u00e1ncot haszn\u00e1lhat, p\u00e9ld\u00e1ul \"08:00:00\""
+ }
+ }
+ },
+ "title": "Google T\u00e9rk\u00e9p utaz\u00e1si id\u0151"
+}
\ No newline at end of file
diff --git a/homeassistant/components/google_travel_time/translations/it.json b/homeassistant/components/google_travel_time/translations/it.json
new file mode 100644
index 00000000000000..426e7f96c3c5c7
--- /dev/null
+++ b/homeassistant/components/google_travel_time/translations/it.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La posizione \u00e8 gi\u00e0 configurata"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Chiave API",
+ "destination": "Destinazione",
+ "origin": "Origine"
+ },
+ "description": "Quando specifichi l'origine e la destinazione, puoi fornire una o pi\u00f9 posizioni separate dal carattere barra verticale, sotto forma di un indirizzo, coordinate di latitudine/longitudine o un ID luogo di Google. Quando si specifica la posizione utilizzando un ID luogo di Google, l'ID deve essere preceduto da \"place_id:\"."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid": "Evitare",
+ "language": "Lingua",
+ "mode": "Modalit\u00e0 di viaggio",
+ "time": "Ora",
+ "time_type": "Tipo di ora",
+ "transit_mode": "Modalit\u00e0 di transito",
+ "transit_routing_preference": "Preferenza percorso di transito",
+ "units": "Unit\u00e0"
+ },
+ "description": "Facoltativamente, \u00e8 possibile specificare un orario di partenza o un orario di arrivo. Se si specifica un orario di partenza, \u00e8 possibile inserire \"now\", un timestamp Unix o una stringa di 24 ore come \"08: 00: 00\". Se si specifica un'ora di arrivo, \u00e8 possibile utilizzare un timestamp Unix o una stringa di 24 ore come \"08: 00: 00\""
+ }
+ }
+ },
+ "title": "Tempo di viaggio di Google Maps"
+}
\ No newline at end of file
diff --git a/homeassistant/components/google_travel_time/translations/ko.json b/homeassistant/components/google_travel_time/translations/ko.json
new file mode 100644
index 00000000000000..41873626ea561b
--- /dev/null
+++ b/homeassistant/components/google_travel_time/translations/ko.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API \ud0a4",
+ "destination": "\ubaa9\uc801\uc9c0",
+ "origin": "\ucd9c\ubc1c\uc9c0"
+ },
+ "description": "\ucd9c\ubc1c\uc9c0\uc640 \ubaa9\uc801\uc9c0\ub97c \uc9c0\uc815\ud560 \ub54c \uc8fc\uc18c, \uc704\ub3c4/\uacbd\ub3c4 \uc88c\ud45c \ub610\ub294 Google Place ID \ud615\uc2dd\uc73c\ub85c \ud30c\uc774\ud504 \ubb38\uc790(|)\ub85c \uad6c\ubd84\ub41c \ud558\ub098 \uc774\uc0c1\uc758 \uc704\uce58\ub97c \uc785\ub825\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. Google Place ID\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc704\uce58\ub97c \uc9c0\uc815\ud560 \ub54c\ub294 ID \uc55e\uc5d0 `place_id:`\ub97c \ubd99\uc5ec\uc57c \ud569\ub2c8\ub2e4."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid": "\ud68c\ud53c",
+ "language": "\uc5b8\uc5b4",
+ "mode": "\uae38\ucc3e\uae30 \ubaa8\ub4dc",
+ "time": "\uc2dc\uac04",
+ "time_type": "\uc2dc\uac04 \uc720\ud615",
+ "transit_mode": "\ub300\uc911\uad50\ud1b5 \ubaa8\ub4dc",
+ "transit_routing_preference": "\ub300\uc911\uad50\ud1b5 \uacbd\ub85c \uae30\ubcf8 \uc124\uc815",
+ "units": "\ub2e8\uc704"
+ },
+ "description": "\uc120\ud0dd\uc801\uc73c\ub85c \ucd9c\ubc1c \uc2dc\uac04 \ub610\ub294 \ub3c4\ucc29 \uc2dc\uac04\uc744 \uc9c0\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ucd9c\ubc1c \uc2dc\uac04\uc744 \uc9c0\uc815\ud558\ub294 \uacbd\uc6b0 'now' \ub610\ub294 Unix \ud0c0\uc784\uc2a4\ud0ec\ud504 \ub610\ub294 '08:00:00'\uacfc \uac19\uc740 24\uc2dc\uac04 \ubb38\uc790\uc5f4\uc744 \uc785\ub825\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub3c4\ucc29 \uc2dc\uac04\uc744 \uc9c0\uc815\ud558\ub294 \uacbd\uc6b0 Unix \ud0c0\uc784\uc2a4\ud0ec\ud504 \ub610\ub294 '08:00:00'\uacfc \uac19\uc740 24\uc2dc\uac04 \ubb38\uc790\uc5f4\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ }
+ }
+ },
+ "title": "Google Maps \uc774\ub3d9 \uc2dc\uac04"
+}
\ No newline at end of file
diff --git a/homeassistant/components/google_travel_time/translations/nl.json b/homeassistant/components/google_travel_time/translations/nl.json
new file mode 100644
index 00000000000000..7341fd0a6a2ce4
--- /dev/null
+++ b/homeassistant/components/google_travel_time/translations/nl.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Locatie is al geconfigureerd."
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-sleutel",
+ "destination": "Bestemming",
+ "origin": "Vertrekpunt"
+ },
+ "description": "Wanneer u de oorsprong en bestemming opgeeft, kunt u een of meer locaties opgeven, gescheiden door het pijp-symbool, in de vorm van een adres, lengte- / breedtegraadco\u00f6rdinaten of een Google-plaats-ID. Wanneer u de locatie opgeeft met behulp van een Google-plaats-ID, moet de ID worden voorafgegaan door `place_id:`."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid": "Vermijd",
+ "language": "Taal",
+ "mode": "Reiswijze",
+ "time": "Tijd",
+ "time_type": "Tijd Type",
+ "transit_mode": "Transitmodus",
+ "transit_routing_preference": "Transit Route Voorkeur",
+ "units": "Eenheden"
+ },
+ "description": "U kunt optioneel een vertrektijd of aankomsttijd opgeven. Als u een vertrektijd opgeeft, kunt u 'nu', een Unix-tijdstempel of een 24-uurs tijdreeks zoals '08: 00: 00' invoeren. Als u een aankomsttijd specificeert, kunt u een Unix-tijdstempel of een 24-uurs tijdreeks gebruiken, zoals '08: 00: 00'"
+ }
+ }
+ },
+ "title": "Google Maps Reistijd"
+}
\ No newline at end of file
diff --git a/homeassistant/components/google_travel_time/translations/no.json b/homeassistant/components/google_travel_time/translations/no.json
new file mode 100644
index 00000000000000..5dfe345af0183a
--- /dev/null
+++ b/homeassistant/components/google_travel_time/translations/no.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Plasseringen er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-n\u00f8kkel",
+ "destination": "Destinasjon",
+ "origin": "Opprinnelse"
+ },
+ "description": "N\u00e5r du spesifiserer opprinnelse og destinasjon, kan du oppgi en eller flere steder atskilt med r\u00f8rtegnet, i form av en adresse, breddegrad / lengdegradskoordinat eller en Google-sted-ID. N\u00e5r du spesifiserer stedet ved hjelp av en Google-sted-ID, m\u00e5 ID-en v\u00e6re foran \"place_id:`."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid": "Unng\u00e5",
+ "language": "Spr\u00e5k",
+ "mode": "Reisemodus",
+ "time": "Tid",
+ "time_type": "Tidstype",
+ "transit_mode": "Transittmodus",
+ "transit_routing_preference": "Ruteinnstillinger for kollektivtransport",
+ "units": "Enheter"
+ },
+ "description": "Du kan eventuelt angi enten avgangstid eller ankomsttid. Hvis du spesifiserer en avgangstid, kan du angi \"n\u00e5\", et Unix-tidsstempel eller en 24-timers tidsstreng som \"08: 00: 00\". Hvis du spesifiserer en ankomsttid, kan du bruke et Unix-tidsstempel eller en 24-timers tidsstreng som '08: 00: 00'"
+ }
+ }
+ },
+ "title": "Google Maps reisetid"
+}
\ No newline at end of file
diff --git a/homeassistant/components/google_travel_time/translations/pl.json b/homeassistant/components/google_travel_time/translations/pl.json
new file mode 100644
index 00000000000000..c420e65912f0ea
--- /dev/null
+++ b/homeassistant/components/google_travel_time/translations/pl.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Lokalizacja jest ju\u017c skonfigurowana"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Klucz API",
+ "destination": "Punkt docelowy",
+ "origin": "Punkt pocz\u0105tkowy"
+ },
+ "description": "Okre\u015blaj\u0105c punkt pocz\u0105tkowy i docelowy, mo\u017cesz poda\u0107 jedn\u0105 lub wi\u0119cej lokalizacji oddzielonych pionow\u0105 kresk\u0105, w postaci adresu, wsp\u00f3\u0142rz\u0119dnych szeroko\u015bci / d\u0142ugo\u015bci geograficznej lub identyfikatora miejsca Google. Okre\u015blaj\u0105c lokalizacj\u0119 za pomoc\u0105 identyfikatora miejsca Google, identyfikator musi by\u0107 poprzedzony przedrostkiem \u201eplace_id:\u201d."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid": "Unikaj",
+ "language": "J\u0119zyk",
+ "mode": "Tryb podr\u00f3\u017cy",
+ "time": "Czas",
+ "time_type": "Typ czasu",
+ "transit_mode": "Tryb tranzytu",
+ "transit_routing_preference": "Preferencje trasy tranzytowej",
+ "units": "Jednostki"
+ },
+ "description": "Opcjonalnie mo\u017cesz okre\u015bli\u0107 godzin\u0119 wyjazdu lub przyjazdu. Je\u015bli okre\u015blasz czas wyjazdu, mo\u017cesz wprowadzi\u0107 \u201eteraz\u201d, uniksowy znacznik czasu lub ci\u0105g 24-godzinny, taki jak \u201e08:00:00\u201d. Je\u015bli okre\u015blasz czas przyjazdu, mo\u017cesz u\u017cy\u0107 uniksowego znacznika czasu lub ci\u0105gu 24-godzinnego, takiego jak \u201e08:00:00\u201d."
+ }
+ }
+ },
+ "title": "Czas podr\u00f3\u017cy w Mapach Google"
+}
\ No newline at end of file
diff --git a/homeassistant/components/google_travel_time/translations/ru.json b/homeassistant/components/google_travel_time/translations/ru.json
new file mode 100644
index 00000000000000..1198c3d62f9b0c
--- /dev/null
+++ b/homeassistant/components/google_travel_time/translations/ru.json
@@ -0,0 +1,38 @@
+{
+ "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": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "destination": "\u041f\u0443\u043d\u043a\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f",
+ "origin": "\u041f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f"
+ },
+ "description": "\u041f\u0440\u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0438 \u043f\u0443\u043d\u043a\u0442\u043e\u0432 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0438 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043e\u0434\u043d\u043e \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445 \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0447\u0435\u0440\u0442\u043e\u0439, \u0432 \u0432\u0438\u0434\u0435 \u0430\u0434\u0440\u0435\u0441\u0430, \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442 \u0448\u0438\u0440\u043e\u0442\u044b/\u0434\u043e\u043b\u0433\u043e\u0442\u044b \u0438\u043b\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u043c\u0435\u0441\u0442\u0430 Google. \u041f\u0440\u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0438 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u043c\u0435\u0441\u0442\u0430 Google, \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0434\u043e\u043b\u0436\u0435\u043d \u043d\u0430\u0447\u0438\u043d\u0430\u0442\u044c\u0441\u044f \u0441 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u0430 `place_id:`."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid": "\u0418\u0437\u0431\u0435\u0433\u0430\u0442\u044c",
+ "language": "\u042f\u0437\u044b\u043a",
+ "mode": "\u0421\u043f\u043e\u0441\u043e\u0431 \u043f\u0435\u0440\u0435\u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f",
+ "time": "\u0412\u0440\u0435\u043c\u044f",
+ "time_type": "\u0422\u0438\u043f \u0432\u0440\u0435\u043c\u0435\u043d\u0438",
+ "transit_mode": "\u0420\u0435\u0436\u0438\u043c \u0442\u0440\u0430\u043d\u0437\u0438\u0442\u0430",
+ "transit_routing_preference": "\u041f\u0440\u0435\u0434\u043f\u043e\u0447\u0442\u0435\u043d\u0438\u0435 \u043f\u043e \u0442\u0440\u0430\u043d\u0437\u0438\u0442\u043d\u043e\u043c\u0443 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0443",
+ "units": "\u0415\u0434\u0438\u043d\u0438\u0446\u044b \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f"
+ },
+ "description": "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0432\u0440\u0435\u043c\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u0438\u0431\u044b\u0442\u0438\u044f. \u041f\u0440\u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f ('now'), Unix-\u0432\u0440\u0435\u043c\u044f \u0438\u043b\u0438 \u0441\u0442\u0440\u043e\u043a\u0443 \u0432 24-\u0447\u0430\u0441\u043e\u0432\u043e\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0435 \u0438\u0441\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 '08:00:00'. \u041f\u0440\u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043f\u0440\u0438\u0431\u044b\u0442\u0438\u044f \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c Unix-\u0432\u0440\u0435\u043c\u044f \u0438\u043b\u0438 \u0441\u0442\u0440\u043e\u043a\u0443 \u0432 24-\u0447\u0430\u0441\u043e\u0432\u043e\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0435 \u0438\u0441\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 '08:00:00'."
+ }
+ }
+ },
+ "title": "Google Maps Travel Time"
+}
\ No newline at end of file
diff --git a/homeassistant/components/google_travel_time/translations/zh-Hant.json b/homeassistant/components/google_travel_time/translations/zh-Hant.json
new file mode 100644
index 00000000000000..e834d3b2f36a7d
--- /dev/null
+++ b/homeassistant/components/google_travel_time/translations/zh-Hant.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API \u5bc6\u9470",
+ "destination": "\u76ee\u7684\u5730",
+ "origin": "\u51fa\u767c\u5730"
+ },
+ "description": "\u7576\u6307\u5b9a\u51fa\u767c\u5730\u8207\u76ee\u7684\u5730\u6642\uff0c\u53ef\u4ee5\u5305\u542b\u4e00\u500b\u6216\u4ee5\u4e0a\u7684\u4f4d\u7f6e\u3001\u4f9d\u5730\u5740\u683c\u5f0f\u3001\u7d93\u7def\u5ea6\u6216\u8005 Goolge Place ID\uff0c\u4ee5\u8c4e\u7dda\u5206\u9694\u9032\u884c\u3002\u7576\u4ee5 Google Place ID \u6307\u5b9a\u4f4d\u7f6e\u6642\uff0c\u5fc5\u9808\u5305\u542b\u683c\u5f0f\u70ba `place_id:`\u3002"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid": "\u8ff4\u907f",
+ "language": "\u8a9e\u8a00",
+ "mode": "\u65c5\u884c\u6a21\u5f0f",
+ "time": "\u6642\u9593",
+ "time_type": "\u6642\u9593\u985e\u578b",
+ "transit_mode": "\u79fb\u52d5\u6a21\u5f0f",
+ "transit_routing_preference": "\u504f\u597d\u79fb\u52d5\u8def\u7dda",
+ "units": "\u55ae\u4f4d"
+ },
+ "description": "\u53ef\u9078\u9805\u6307\u5b9a\u51fa\u767c\u6642\u9593\u6216\u62b5\u9054\u6642\u9593\u3002\u5047\u5982\u6b32\u6307\u5b9a\u51fa\u767c\u6642\u9593\u3001\u53ef\u4ee5\u8f38\u5165\u70ba `\u7acb\u5373\u51fa\u767c`\u3001Unix \u6642\u9593\u6a19\u8a18\u6216 24 \u5c0f\u6642\u6642\u9593\u5236\uff0c\u5982 `08:00:00`\u3002\u5047\u5982\u6b32\u6307\u5b9a\u62b5\u9054\u6642\u9593\uff0c\u53ef\u4f7f\u7528 Unix \u6642\u9593\u6a19\u8a18\u6216 24 \u5c0f\u6642\u6642\u9593\u5236\u5982 `08:00:00`"
+ }
+ }
+ },
+ "title": "Google Maps \u65c5\u7a0b\u6642\u9593"
+}
\ No newline at end of file
diff --git a/homeassistant/components/google_wifi/sensor.py b/homeassistant/components/google_wifi/sensor.py
index 9dfa26fab75c61..28ec5df7486fe1 100644
--- a/homeassistant/components/google_wifi/sensor.py
+++ b/homeassistant/components/google_wifi/sensor.py
@@ -5,7 +5,7 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_HOST,
CONF_MONITORED_CONDITIONS,
@@ -14,7 +14,6 @@
TIME_DAYS,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle, dt
_LOGGER = logging.getLogger(__name__)
@@ -71,7 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(dev, True)
-class GoogleWifiSensor(Entity):
+class GoogleWifiSensor(SensorEntity):
"""Representation of a Google Wifi sensor."""
def __init__(self, api, name, variable):
@@ -176,9 +175,10 @@ def data_format(self):
sensor_value = "Online"
else:
sensor_value = "Offline"
- elif attr_key == ATTR_LOCAL_IP:
- if not self.raw_data["wan"]["online"]:
- sensor_value = STATE_UNKNOWN
+ elif (
+ attr_key == ATTR_LOCAL_IP and not self.raw_data["wan"]["online"]
+ ):
+ sensor_value = STATE_UNKNOWN
self.data[attr_key] = sensor_value
except KeyError:
diff --git a/homeassistant/components/gpmdp/media_player.py b/homeassistant/components/gpmdp/media_player.py
index 2fa227f0953d9a..5680eb755009ce 100644
--- a/homeassistant/components/gpmdp/media_player.py
+++ b/homeassistant/components/gpmdp/media_player.py
@@ -227,9 +227,8 @@ def send_gpmdp_msg(self, namespace, method, with_id=True):
return
while True:
msg = json.loads(websocket.recv())
- if "requestID" in msg:
- if msg["requestID"] == self._request_id:
- return msg
+ if "requestID" in msg and msg["requestID"] == self._request_id:
+ return msg
except (
ConnectionRefusedError,
ConnectionResetError,
diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py
index ea238269e598e5..2f97f62337c47a 100644
--- a/homeassistant/components/gpsd/sensor.py
+++ b/homeassistant/components/gpsd/sensor.py
@@ -5,7 +5,7 @@
from gps3.agps3threaded import AGPS3mechanism
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_LATITUDE,
ATTR_LONGITUDE,
@@ -15,7 +15,6 @@
CONF_PORT,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -65,7 +64,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([GpsdSensor(hass, name, host, port)])
-class GpsdSensor(Entity):
+class GpsdSensor(SensorEntity):
"""Representation of a GPS receiver available via GPSD."""
def __init__(self, hass, name, host, port):
@@ -94,7 +93,7 @@ def state(self):
return None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the GPS."""
return {
ATTR_LATITUDE: self.agps_thread.data_stream.lat,
diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py
index 6999f26d7520ff..25701e8c2e79a5 100644
--- a/homeassistant/components/gpslogger/device_tracker.py
+++ b/homeassistant/components/gpslogger/device_tracker.py
@@ -79,7 +79,7 @@ def battery_level(self):
return self._battery
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific attributes."""
return self._attributes
diff --git a/homeassistant/components/gpslogger/translations/de.json b/homeassistant/components/gpslogger/translations/de.json
index d976a5fd6638fa..7215f0c458f623 100644
--- a/homeassistant/components/gpslogger/translations/de.json
+++ b/homeassistant/components/gpslogger/translations/de.json
@@ -1,5 +1,9 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.",
+ "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen."
+ },
"create_entry": {
"default": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in der GPSLogger konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})."
},
diff --git a/homeassistant/components/gpslogger/translations/hu.json b/homeassistant/components/gpslogger/translations/hu.json
index 4fa3678043ebeb..fe459ca3164956 100644
--- a/homeassistant/components/gpslogger/translations/hu.json
+++ b/homeassistant/components/gpslogger/translations/hu.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.",
+ "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz."
+ },
"create_entry": {
- "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a GPSLoggerben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3] ( {docs_url} ) linken tal\u00e1lhat\u00f3k."
+ "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a GPSLoggerben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k."
},
"step": {
"user": {
diff --git a/homeassistant/components/gpslogger/translations/id.json b/homeassistant/components/gpslogger/translations/id.json
new file mode 100644
index 00000000000000..3be2d91f1f3641
--- /dev/null
+++ b/homeassistant/components/gpslogger/translations/id.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.",
+ "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook."
+ },
+ "create_entry": {
+ "default": "Untuk mengirim event ke Home Assistant, Anda harus menyiapkan fitur webhook di GPSLogger.\n\nIsi info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nBaca [dokumentasi]({docs_url}) tentang detail lebih lanjut."
+ },
+ "step": {
+ "user": {
+ "description": "Yakin ingin menyiapkan GPSLogger Webhook?",
+ "title": "Siapkan GPSLogger Webhook"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gpslogger/translations/ko.json b/homeassistant/components/gpslogger/translations/ko.json
index a6d95a0e51b200..3d32cb44736b3f 100644
--- a/homeassistant/components/gpslogger/translations/ko.json
+++ b/homeassistant/components/gpslogger/translations/ko.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4."
+ },
"create_entry": {
- "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 GPSLogger \uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
+ "default": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 GPSLogger\uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
},
"step": {
"user": {
diff --git a/homeassistant/components/gpslogger/translations/nl.json b/homeassistant/components/gpslogger/translations/nl.json
index dbf7f47a2e9cbc..d90b648760db9d 100644
--- a/homeassistant/components/gpslogger/translations/nl.json
+++ b/homeassistant/components/gpslogger/translations/nl.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk."
+ "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.",
+ "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen."
},
"create_entry": {
"default": "Om evenementen naar Home Assistant te verzenden, moet u de webhook-functie instellen in GPSLogger. \n\n Vul de volgende info in: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n\n Zie [de documentatie] ( {docs_url} ) voor meer informatie."
diff --git a/homeassistant/components/gpslogger/translations/tr.json b/homeassistant/components/gpslogger/translations/tr.json
new file mode 100644
index 00000000000000..84adcdf8225c43
--- /dev/null
+++ b/homeassistant/components/gpslogger/translations/tr.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.",
+ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gpslogger/translations/uk.json b/homeassistant/components/gpslogger/translations/uk.json
new file mode 100644
index 00000000000000..5b0b6305cdb638
--- /dev/null
+++ b/homeassistant/components/gpslogger/translations/uk.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.",
+ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c."
+ },
+ "create_entry": {
+ "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f GPSLogger. \n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457."
+ },
+ "step": {
+ "user": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 GPSLogger?",
+ "title": "GPSLogger"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/graphite/__init__.py b/homeassistant/components/graphite/__init__.py
index 327e8293be7ef1..9405b576b4d6a5 100644
--- a/homeassistant/components/graphite/__init__.py
+++ b/homeassistant/components/graphite/__init__.py
@@ -1,4 +1,5 @@
"""Support for sending data to a Graphite installation."""
+from contextlib import suppress
import logging
import queue
import socket
@@ -11,6 +12,7 @@
CONF_HOST,
CONF_PORT,
CONF_PREFIX,
+ CONF_PROTOCOL,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
EVENT_STATE_CHANGED,
@@ -20,8 +22,11 @@
_LOGGER = logging.getLogger(__name__)
+PROTOCOL_TCP = "tcp"
+PROTOCOL_UDP = "udp"
DEFAULT_HOST = "localhost"
DEFAULT_PORT = 2003
+DEFAULT_PROTOCOL = PROTOCOL_TCP
DEFAULT_PREFIX = "ha"
DOMAIN = "graphite"
@@ -31,6 +36,9 @@
{
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
+ vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.Any(
+ PROTOCOL_TCP, PROTOCOL_UDP
+ ),
vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): cv.string,
}
)
@@ -45,29 +53,34 @@ def setup(hass, config):
host = conf.get(CONF_HOST)
prefix = conf.get(CONF_PREFIX)
port = conf.get(CONF_PORT)
+ protocol = conf.get(CONF_PROTOCOL)
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- try:
- sock.connect((host, port))
- sock.shutdown(2)
- _LOGGER.debug("Connection to Graphite possible")
- except OSError:
- _LOGGER.error("Not able to connect to Graphite")
- return False
+ if protocol == PROTOCOL_TCP:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ try:
+ sock.connect((host, port))
+ sock.shutdown(2)
+ _LOGGER.debug("Connection to Graphite possible")
+ except OSError:
+ _LOGGER.error("Not able to connect to Graphite")
+ return False
+ else:
+ _LOGGER.debug("No connection check for UDP possible")
- GraphiteFeeder(hass, host, port, prefix)
+ GraphiteFeeder(hass, host, port, protocol, prefix)
return True
class GraphiteFeeder(threading.Thread):
"""Feed data to Graphite."""
- def __init__(self, hass, host, port, prefix):
+ def __init__(self, hass, host, port, protocol, prefix):
"""Initialize the feeder."""
super().__init__(daemon=True)
self._hass = hass
self._host = host
self._port = port
+ self._protocol = protocol
# rstrip any trailing dots in case they think they need it
self._prefix = prefix.rstrip(".")
self._queue = queue.Queue()
@@ -100,21 +113,23 @@ def event_listener(self, event):
def _send_to_graphite(self, data):
"""Send data to Graphite."""
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- sock.settimeout(10)
- sock.connect((self._host, self._port))
- sock.sendall(data.encode("ascii"))
- sock.send(b"\n")
- sock.close()
+ if self._protocol == PROTOCOL_TCP:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(10)
+ sock.connect((self._host, self._port))
+ sock.sendall(data.encode("ascii"))
+ sock.send(b"\n")
+ sock.close()
+ else:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ sock.sendto(data.encode("ascii") + b"\n", (self._host, self._port))
def _report_attributes(self, entity_id, new_state):
"""Report the attributes."""
now = time.time()
things = dict(new_state.attributes)
- try:
+ with suppress(ValueError):
things["state"] = state.state_as_number(new_state)
- except ValueError:
- pass
lines = [
"%s.%s.%s %f %i"
% (self._prefix, entity_id, key.replace(" ", "_"), value, now)
diff --git a/homeassistant/components/gree/bridge.py b/homeassistant/components/gree/bridge.py
index 3fbf4a21fb39d7..af523f385aa3e1 100644
--- a/homeassistant/components/gree/bridge.py
+++ b/homeassistant/components/gree/bridge.py
@@ -1,7 +1,8 @@
"""Helper and wrapper classes for Gree module."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import List
from greeclimate.device import Device, DeviceInfo
from greeclimate.discovery import Discovery
@@ -86,7 +87,7 @@ async def try_bind_device(device_info: DeviceInfo) -> Device:
return device
@staticmethod
- async def find_devices() -> List[DeviceInfo]:
+ async def find_devices() -> list[DeviceInfo]:
"""Gather a list of device infos from the local network."""
return await Discovery.search_devices()
diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py
index 8d0170fbe50328..a5ef39be071d4c 100644
--- a/homeassistant/components/gree/climate.py
+++ b/homeassistant/components/gree/climate.py
@@ -1,6 +1,7 @@
"""Support for interface with a Gree climate systems."""
+from __future__ import annotations
+
import logging
-from typing import List
from greeclimate.device import (
FanSpeed,
@@ -234,7 +235,7 @@ async def async_turn_off(self) -> None:
self.async_write_ha_state()
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the HVAC modes support by the device."""
modes = [*HVAC_MODES_REVERSE]
modes.append(HVAC_MODE_OFF)
@@ -282,7 +283,7 @@ async def async_set_preset_mode(self, preset_mode):
self.async_write_ha_state()
@property
- def preset_modes(self) -> List[str]:
+ def preset_modes(self) -> list[str]:
"""Return the preset modes support by the device."""
return PRESET_MODES
@@ -302,7 +303,7 @@ async def async_set_fan_mode(self, fan_mode):
self.async_write_ha_state()
@property
- def fan_modes(self) -> List[str]:
+ def fan_modes(self) -> list[str]:
"""Return the fan modes support by the device."""
return [*FAN_MODES_REVERSE]
@@ -342,7 +343,7 @@ async def async_set_swing_mode(self, swing_mode):
self.async_write_ha_state()
@property
- def swing_modes(self) -> List[str]:
+ def swing_modes(self) -> list[str]:
"""Return the swing modes currently supported for this device."""
return SWING_MODES
diff --git a/homeassistant/components/gree/switch.py b/homeassistant/components/gree/switch.py
index f4e9792a5897df..12c94ddec61893 100644
--- a/homeassistant/components/gree/switch.py
+++ b/homeassistant/components/gree/switch.py
@@ -1,6 +1,5 @@
"""Support for interface with a Gree climate systems."""
-import logging
-from typing import Optional
+from __future__ import annotations
from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
@@ -8,8 +7,6 @@
from .const import COORDINATOR, DOMAIN
-_LOGGER = logging.getLogger(__name__)
-
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Gree HVAC device from a config entry."""
@@ -41,7 +38,7 @@ def unique_id(self) -> str:
return f"{self._mac}-panel-light"
@property
- def icon(self) -> Optional[str]:
+ def icon(self) -> str | None:
"""Return the icon for the device."""
return "mdi:lightbulb"
diff --git a/homeassistant/components/gree/translations/de.json b/homeassistant/components/gree/translations/de.json
new file mode 100644
index 00000000000000..86bc8e3673075f
--- /dev/null
+++ b/homeassistant/components/gree/translations/de.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden",
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
+ "step": {
+ "confirm": {
+ "description": "M\u00f6chten Sie mit der Einrichtung beginnen?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gree/translations/hu.json b/homeassistant/components/gree/translations/hu.json
new file mode 100644
index 00000000000000..6c61530acbebb9
--- /dev/null
+++ b/homeassistant/components/gree/translations/hu.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
+ "step": {
+ "confirm": {
+ "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gree/translations/id.json b/homeassistant/components/gree/translations/id.json
new file mode 100644
index 00000000000000..223836a8b40992
--- /dev/null
+++ b/homeassistant/components/gree/translations/id.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "step": {
+ "confirm": {
+ "description": "Ingin memulai penyiapan?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gree/translations/ko.json b/homeassistant/components/gree/translations/ko.json
new file mode 100644
index 00000000000000..e5ae04d6e5c810
--- /dev/null
+++ b/homeassistant/components/gree/translations/ko.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "step": {
+ "confirm": {
+ "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gree/translations/nl.json b/homeassistant/components/gree/translations/nl.json
new file mode 100644
index 00000000000000..d11896014fd2c6
--- /dev/null
+++ b/homeassistant/components/gree/translations/nl.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Geen apparaten gevonden op het netwerk",
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
+ },
+ "step": {
+ "confirm": {
+ "description": "Wil je beginnen met instellen?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gree/translations/tr.json b/homeassistant/components/gree/translations/tr.json
new file mode 100644
index 00000000000000..8de4663957ea85
--- /dev/null
+++ b/homeassistant/components/gree/translations/tr.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "step": {
+ "confirm": {
+ "description": "Kuruluma ba\u015flamak ister misiniz?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gree/translations/uk.json b/homeassistant/components/gree/translations/uk.json
new file mode 100644
index 00000000000000..292861e9129dbd
--- /dev/null
+++ b/homeassistant/components/gree/translations/uk.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/greeneye_monitor/__init__.py b/homeassistant/components/greeneye_monitor/__init__.py
index 697a96649ab4a1..51471739e98b65 100644
--- a/homeassistant/components/greeneye_monitor/__init__.py
+++ b/homeassistant/components/greeneye_monitor/__init__.py
@@ -7,6 +7,8 @@
from homeassistant.const import (
CONF_NAME,
CONF_PORT,
+ CONF_SENSOR_TYPE,
+ CONF_SENSORS,
CONF_TEMPERATURE_UNIT,
EVENT_HOMEASSISTANT_STOP,
TIME_HOURS,
@@ -27,8 +29,6 @@
CONF_NUMBER = "number"
CONF_PULSE_COUNTERS = "pulse_counters"
CONF_SERIAL_NUMBER = "serial_number"
-CONF_SENSORS = "sensors"
-CONF_SENSOR_TYPE = "sensor_type"
CONF_TEMPERATURE_SENSORS = "temperature_sensors"
CONF_TIME_UNIT = "time_unit"
CONF_VOLTAGE_SENSORS = "voltage"
@@ -119,7 +119,6 @@
async def async_setup(hass, config):
"""Set up the GreenEye Monitor component."""
-
monitors = Monitors()
hass.data[DATA_GREENEYE_MONITOR] = monitors
diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py
index c8cd1669f0594b..4e792bf56e4959 100644
--- a/homeassistant/components/greeneye_monitor/sensor.py
+++ b/homeassistant/components/greeneye_monitor/sensor.py
@@ -1,6 +1,8 @@
"""Support for the sensors in a GreenEye Monitor."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
CONF_NAME,
+ CONF_SENSOR_TYPE,
CONF_TEMPERATURE_UNIT,
POWER_WATT,
TIME_HOURS,
@@ -8,7 +10,6 @@
TIME_SECONDS,
VOLT,
)
-from homeassistant.helpers.entity import Entity
from . import (
CONF_COUNTED_QUANTITY,
@@ -16,7 +17,6 @@
CONF_MONITOR_SERIAL_NUMBER,
CONF_NET_METERING,
CONF_NUMBER,
- CONF_SENSOR_TYPE,
CONF_TIME_UNIT,
DATA_GREENEYE_MONITOR,
SENSOR_TYPE_CURRENT,
@@ -85,7 +85,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(entities)
-class GEMSensor(Entity):
+class GEMSensor(SensorEntity):
"""Base class for GreenEye Monitor sensors."""
def __init__(self, monitor_serial_number, name, sensor_type, number):
@@ -175,7 +175,7 @@ def state(self):
return self._sensor.watts
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return total wattseconds in the state dictionary."""
if not self._sensor:
return None
@@ -242,7 +242,7 @@ def unit_of_measurement(self):
return f"{self._counted_quantity}/{self._time_unit}"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return total pulses in the data dictionary."""
if not self._sensor:
return None
diff --git a/homeassistant/components/griddy/__init__.py b/homeassistant/components/griddy/__init__.py
deleted file mode 100644
index fb5079b00f8c66..00000000000000
--- a/homeassistant/components/griddy/__init__.py
+++ /dev/null
@@ -1,96 +0,0 @@
-"""The Griddy Power integration."""
-import asyncio
-from datetime import timedelta
-import logging
-
-from griddypower.async_api import LOAD_ZONES, AsyncGriddy
-import voluptuous as vol
-
-from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers import aiohttp_client
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
-
-from .const import CONF_LOADZONE, DOMAIN, UPDATE_INTERVAL
-
-_LOGGER = logging.getLogger(__name__)
-
-CONFIG_SCHEMA = vol.Schema(
- {DOMAIN: vol.Schema({vol.Required(CONF_LOADZONE): vol.In(LOAD_ZONES)})},
- extra=vol.ALLOW_EXTRA,
-)
-
-PLATFORMS = ["sensor"]
-
-
-async def async_setup(hass: HomeAssistant, config: dict):
- """Set up the Griddy Power component."""
-
- hass.data.setdefault(DOMAIN, {})
- conf = config.get(DOMAIN)
-
- if not conf:
- return True
-
- hass.async_create_task(
- hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": SOURCE_IMPORT},
- data={CONF_LOADZONE: conf.get(CONF_LOADZONE)},
- )
- )
- return True
-
-
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
- """Set up Griddy Power from a config entry."""
-
- entry_data = entry.data
-
- async_griddy = AsyncGriddy(
- aiohttp_client.async_get_clientsession(hass),
- settlement_point=entry_data[CONF_LOADZONE],
- )
-
- async def async_update_data():
- """Fetch data from API endpoint."""
- return await async_griddy.async_getnow()
-
- coordinator = DataUpdateCoordinator(
- hass,
- _LOGGER,
- name="Griddy getnow",
- update_method=async_update_data,
- update_interval=timedelta(seconds=UPDATE_INTERVAL),
- )
-
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
-
- hass.data[DOMAIN][entry.entry_id] = coordinator
-
- for component in PLATFORMS:
- hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
- )
-
- return True
-
-
-async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
- """Unload a config entry."""
- unload_ok = all(
- await asyncio.gather(
- *[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
- ]
- )
- )
- if unload_ok:
- hass.data[DOMAIN].pop(entry.entry_id)
-
- return unload_ok
diff --git a/homeassistant/components/griddy/config_flow.py b/homeassistant/components/griddy/config_flow.py
deleted file mode 100644
index 675e48cc9993dc..00000000000000
--- a/homeassistant/components/griddy/config_flow.py
+++ /dev/null
@@ -1,75 +0,0 @@
-"""Config flow for Griddy Power integration."""
-import asyncio
-import logging
-
-from aiohttp import ClientError
-from griddypower.async_api import LOAD_ZONES, AsyncGriddy
-import voluptuous as vol
-
-from homeassistant import config_entries, core, exceptions
-from homeassistant.helpers import aiohttp_client
-
-from .const import CONF_LOADZONE
-from .const import DOMAIN # pylint:disable=unused-import
-
-_LOGGER = logging.getLogger(__name__)
-
-DATA_SCHEMA = vol.Schema({vol.Required(CONF_LOADZONE): vol.In(LOAD_ZONES)})
-
-
-async def validate_input(hass: core.HomeAssistant, data):
- """Validate the user input allows us to connect.
-
- Data has the keys from DATA_SCHEMA with values provided by the user.
- """
- client_session = aiohttp_client.async_get_clientsession(hass)
-
- try:
- await AsyncGriddy(
- client_session, settlement_point=data[CONF_LOADZONE]
- ).async_getnow()
- except (asyncio.TimeoutError, ClientError) as err:
- raise CannotConnect from err
-
- # Return info that you want to store in the config entry.
- return {"title": f"Load Zone {data[CONF_LOADZONE]}"}
-
-
-class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
- """Handle a config flow for Griddy Power."""
-
- VERSION = 1
- CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
-
- async def async_step_user(self, user_input=None):
- """Handle the initial step."""
- errors = {}
- info = None
- if user_input is not None:
- try:
- info = await validate_input(self.hass, user_input)
- except CannotConnect:
- errors["base"] = "cannot_connect"
- except Exception: # pylint: disable=broad-except
- _LOGGER.exception("Unexpected exception")
- errors["base"] = "unknown"
-
- if "base" not in errors:
- await self.async_set_unique_id(user_input[CONF_LOADZONE])
- self._abort_if_unique_id_configured()
- return self.async_create_entry(title=info["title"], data=user_input)
-
- return self.async_show_form(
- step_id="user", data_schema=DATA_SCHEMA, errors=errors
- )
-
- async def async_step_import(self, user_input):
- """Handle import."""
- await self.async_set_unique_id(user_input[CONF_LOADZONE])
- self._abort_if_unique_id_configured()
-
- return await self.async_step_user(user_input)
-
-
-class CannotConnect(exceptions.HomeAssistantError):
- """Error to indicate we cannot connect."""
diff --git a/homeassistant/components/griddy/const.py b/homeassistant/components/griddy/const.py
deleted file mode 100644
index 034567a806e150..00000000000000
--- a/homeassistant/components/griddy/const.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""Constants for the Griddy Power integration."""
-
-DOMAIN = "griddy"
-
-UPDATE_INTERVAL = 90
-
-CONF_LOADZONE = "loadzone"
diff --git a/homeassistant/components/griddy/manifest.json b/homeassistant/components/griddy/manifest.json
deleted file mode 100644
index 1e31b1b7aa8320..00000000000000
--- a/homeassistant/components/griddy/manifest.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "domain": "griddy",
- "name": "Griddy Power",
- "config_flow": true,
- "documentation": "https://www.home-assistant.io/integrations/griddy",
- "requirements": ["griddypower==0.1.0"],
- "codeowners": ["@bdraco"]
-}
diff --git a/homeassistant/components/griddy/sensor.py b/homeassistant/components/griddy/sensor.py
deleted file mode 100644
index f8a900d92be8cf..00000000000000
--- a/homeassistant/components/griddy/sensor.py
+++ /dev/null
@@ -1,48 +0,0 @@
-"""Support for August sensors."""
-from homeassistant.const import CURRENCY_CENT, ENERGY_KILO_WATT_HOUR
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-
-from .const import CONF_LOADZONE, DOMAIN
-
-
-async def async_setup_entry(hass, config_entry, async_add_entities):
- """Set up the August sensors."""
- coordinator = hass.data[DOMAIN][config_entry.entry_id]
-
- settlement_point = config_entry.data[CONF_LOADZONE]
-
- async_add_entities([GriddyPriceSensor(settlement_point, coordinator)], True)
-
-
-class GriddyPriceSensor(CoordinatorEntity):
- """Representation of an August sensor."""
-
- def __init__(self, settlement_point, coordinator):
- """Initialize the sensor."""
- super().__init__(coordinator)
- self._settlement_point = settlement_point
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return f"{CURRENCY_CENT}/{ENERGY_KILO_WATT_HOUR}"
-
- @property
- def name(self):
- """Device Name."""
- return f"{self._settlement_point} Price Now"
-
- @property
- def icon(self):
- """Device Ice."""
- return "mdi:currency-usd"
-
- @property
- def unique_id(self):
- """Device Uniqueid."""
- return f"{self._settlement_point}_price_now"
-
- @property
- def state(self):
- """Get the current price."""
- return round(float(self.coordinator.data.now.price_cents_kwh), 4)
diff --git a/homeassistant/components/griddy/strings.json b/homeassistant/components/griddy/strings.json
deleted file mode 100644
index 99bd8946c3437c..00000000000000
--- a/homeassistant/components/griddy/strings.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "config": {
- "error": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "unknown": "[%key:common::config_flow::error::unknown%]"
- },
- "step": {
- "user": {
- "description": "Your Load Zone is in your Griddy account under \u201cAccount > Meter > Load Zone.\u201d",
- "data": { "loadzone": "Load Zone (Settlement Point)" },
- "title": "Setup your Griddy Load Zone"
- }
- },
- "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" }
- }
-}
diff --git a/homeassistant/components/griddy/translations/ca.json b/homeassistant/components/griddy/translations/ca.json
deleted file mode 100644
index 33aca3cd302a4c..00000000000000
--- a/homeassistant/components/griddy/translations/ca.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "config": {
- "abort": {
- "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada"
- },
- "error": {
- "cannot_connect": "Ha fallat la connexi\u00f3",
- "unknown": "Error inesperat"
- },
- "step": {
- "user": {
- "data": {
- "loadzone": "Zona de c\u00e0rrega (Load Zone)"
- },
- "description": "La teva zona de c\u00e0rrega (Load Zone) est\u00e0 al teu compte de Griddy v\u00e9s a \"Account > Meter > Load Zone\".",
- "title": "Configuraci\u00f3 de la zona de c\u00e0rrega (Load Zone) de Griddy"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/de.json b/homeassistant/components/griddy/translations/de.json
deleted file mode 100644
index ad6a6e10ab0514..00000000000000
--- a/homeassistant/components/griddy/translations/de.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "config": {
- "abort": {
- "already_configured": "Diese Ladezone ist bereits konfiguriert"
- },
- "error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
- "unknown": "Unerwarteter Fehler"
- },
- "step": {
- "user": {
- "data": {
- "loadzone": "Ladezone (Abwicklungspunkt)"
- },
- "description": "Ihre Ladezone befindet sich in Ihrem Griddy-Konto unter \"Konto > Messger\u00e4t > Ladezone\".",
- "title": "Richten Sie Ihre Griddy Ladezone ein"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/en.json b/homeassistant/components/griddy/translations/en.json
deleted file mode 100644
index 2a82421dd7c12b..00000000000000
--- a/homeassistant/components/griddy/translations/en.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "config": {
- "abort": {
- "already_configured": "Location is already configured"
- },
- "error": {
- "cannot_connect": "Failed to connect",
- "unknown": "Unexpected error"
- },
- "step": {
- "user": {
- "data": {
- "loadzone": "Load Zone (Settlement Point)"
- },
- "description": "Your Load Zone is in your Griddy account under \u201cAccount > Meter > Load Zone.\u201d",
- "title": "Setup your Griddy Load Zone"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/es-419.json b/homeassistant/components/griddy/translations/es-419.json
deleted file mode 100644
index 652c8484b4e727..00000000000000
--- a/homeassistant/components/griddy/translations/es-419.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "config": {
- "abort": {
- "already_configured": "Esta zona de carga ya est\u00e1 configurada"
- },
- "error": {
- "cannot_connect": "No se pudo conectar, intente nuevamente",
- "unknown": "Error inesperado"
- },
- "step": {
- "user": {
- "data": {
- "loadzone": "Zona de carga (punto de asentamiento)"
- },
- "description": "Su zona de carga est\u00e1 en su cuenta de Griddy en \"Cuenta > Medidor > Zona de carga\".",
- "title": "Configura tu zona de carga Griddy"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/es.json b/homeassistant/components/griddy/translations/es.json
deleted file mode 100644
index a3727721b2d552..00000000000000
--- a/homeassistant/components/griddy/translations/es.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "config": {
- "abort": {
- "already_configured": "Esta Zona de Carga ya est\u00e1 configurada"
- },
- "error": {
- "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.",
- "unknown": "Error inesperado"
- },
- "step": {
- "user": {
- "data": {
- "loadzone": "Zona de Carga (Punto del Asentamiento)"
- },
- "description": "Tu Zona de Carga est\u00e1 en tu cuenta de Griddy en \"Account > Meter > Load Zone\"",
- "title": "Configurar tu Zona de Carga de Griddy"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/et.json b/homeassistant/components/griddy/translations/et.json
deleted file mode 100644
index 82d2232b04e23e..00000000000000
--- a/homeassistant/components/griddy/translations/et.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "config": {
- "abort": {
- "already_configured": "See Load Zone on juba m\u00e4\u00e4ratud"
- },
- "error": {
- "cannot_connect": "\u00dchendamine nurjus",
- "unknown": "Tundmatu viga"
- },
- "step": {
- "user": {
- "data": {
- "loadzone": "Load Zone (arvelduspunkt)"
- },
- "description": "Load Zone asub Griddy konto valikutes \u201cAccount > Meter > Load Zone.\u201d",
- "title": "Seadista oma Griddy Load Zone"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/fr.json b/homeassistant/components/griddy/translations/fr.json
deleted file mode 100644
index c2fd4d8d62772c..00000000000000
--- a/homeassistant/components/griddy/translations/fr.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "config": {
- "abort": {
- "already_configured": "Cette zone de chargement est d\u00e9j\u00e0 configur\u00e9e"
- },
- "error": {
- "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer",
- "unknown": "Erreur inattendue"
- },
- "step": {
- "user": {
- "data": {
- "loadzone": "Zone de charge (point d'\u00e9tablissement)"
- },
- "description": "Votre zone de charge se trouve dans votre compte Griddy sous \"Compte > Compteur > Zone de charge\".",
- "title": "Configurez votre zone de charge Griddy"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/it.json b/homeassistant/components/griddy/translations/it.json
deleted file mode 100644
index 40fa69b1229bc8..00000000000000
--- a/homeassistant/components/griddy/translations/it.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "config": {
- "abort": {
- "already_configured": "La posizione \u00e8 gi\u00e0 configurata"
- },
- "error": {
- "cannot_connect": "Impossibile connettersi",
- "unknown": "Errore imprevisto"
- },
- "step": {
- "user": {
- "data": {
- "loadzone": "Zona di Carico (Punto di insediamento)"
- },
- "description": "La tua Zona di Carico si trova nel tuo account Griddy in \"Account > Meter > Load zone\".",
- "title": "Configurazione della Zona di Carico Griddy"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/ko.json b/homeassistant/components/griddy/translations/ko.json
deleted file mode 100644
index a17db380aa01b1..00000000000000
--- a/homeassistant/components/griddy/translations/ko.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "config": {
- "abort": {
- "already_configured": "\uc774 \uc804\ub825 \uacf5\uae09 \uc9c0\uc5ed\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
- },
- "error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
- "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
- },
- "step": {
- "user": {
- "data": {
- "loadzone": "\uc804\ub825 \uacf5\uae09 \uc9c0\uc5ed (\uc815\uc0b0\uc810)"
- },
- "description": "\uc804\ub825 \uacf5\uae09 \uc9c0\uc5ed\uc740 Griddy \uacc4\uc815\uc758 \"Account > Meter > Load Zone\"\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
- "title": "Griddy \uc804\ub825 \uacf5\uae09 \uc9c0\uc5ed \uc124\uc815\ud558\uae30"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/lb.json b/homeassistant/components/griddy/translations/lb.json
deleted file mode 100644
index 84511186f88b60..00000000000000
--- a/homeassistant/components/griddy/translations/lb.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "config": {
- "abort": {
- "already_configured": "Standuert ass scho konfigur\u00e9iert"
- },
- "error": {
- "cannot_connect": "Feeler beim verbannen",
- "unknown": "Onerwaarte Feeler"
- },
- "step": {
- "user": {
- "data": {
- "loadzone": "Lued Zone (Punkt vum R\u00e9glement)"
- },
- "description": "Deng Lued Zon ass an dengem Griddy Kont enner \"Account > Meter > Load Zone.\"",
- "title": "Griddy Lued Zon ariichten"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/nl.json b/homeassistant/components/griddy/translations/nl.json
deleted file mode 100644
index bd97b9ccf7c141..00000000000000
--- a/homeassistant/components/griddy/translations/nl.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "config": {
- "abort": {
- "already_configured": "Deze laadzone is al geconfigureerd"
- },
- "error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
- "unknown": "Onverwachte fout"
- },
- "step": {
- "user": {
- "data": {
- "loadzone": "Laadzone (vestigingspunt)"
- },
- "description": "Uw Load Zone staat op uw Griddy account onder \"Account > Meter > Load Zone\".",
- "title": "Stel uw Griddy Load Zone in"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/no.json b/homeassistant/components/griddy/translations/no.json
deleted file mode 100644
index 7f01fa198a34f1..00000000000000
--- a/homeassistant/components/griddy/translations/no.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "config": {
- "abort": {
- "already_configured": "Plasseringen er allerede konfigurert"
- },
- "error": {
- "cannot_connect": "Tilkobling mislyktes",
- "unknown": "Uventet feil"
- },
- "step": {
- "user": {
- "data": {
- "loadzone": "Load Zone (settlingspunkt)"
- },
- "description": "Din Load Zone er p\u00e5 din Griddy-konto under \"Konto > M\u00e5ler > Lastesone.\"",
- "title": "Sett opp din Griddy Load Zone"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/pl.json b/homeassistant/components/griddy/translations/pl.json
deleted file mode 100644
index 035521336f6850..00000000000000
--- a/homeassistant/components/griddy/translations/pl.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "config": {
- "abort": {
- "already_configured": "Lokalizacja jest ju\u017c skonfigurowana"
- },
- "error": {
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
- "unknown": "Nieoczekiwany b\u0142\u0105d"
- },
- "step": {
- "user": {
- "data": {
- "loadzone": "Strefa obci\u0105\u017cenia (punkt rozliczenia)"
- },
- "description": "Twoja strefa obci\u0105\u017cenia znajduje si\u0119 na twoim koncie Griddy w sekcji \"Konto > Licznik > Strefa obci\u0105\u017cenia\".",
- "title": "Konfiguracja strefy obci\u0105\u017cenia Griddy"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/pt.json b/homeassistant/components/griddy/translations/pt.json
deleted file mode 100644
index 9b067d35f897c5..00000000000000
--- a/homeassistant/components/griddy/translations/pt.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "config": {
- "abort": {
- "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada"
- },
- "error": {
- "cannot_connect": "Falha na liga\u00e7\u00e3o",
- "unknown": "Erro inesperado"
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/ru.json b/homeassistant/components/griddy/translations/ru.json
deleted file mode 100644
index 3483953fce16a2..00000000000000
--- a/homeassistant/components/griddy/translations/ru.json
+++ /dev/null
@@ -1,20 +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": {
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
- },
- "step": {
- "user": {
- "data": {
- "loadzone": "\u0417\u043e\u043d\u0430 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0438 (\u0440\u0430\u0441\u0447\u0435\u0442\u043d\u0430\u044f \u0442\u043e\u0447\u043a\u0430)"
- },
- "description": "\u0417\u043e\u043d\u0430 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Griddy \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 Account > Meter > Load Zone.",
- "title": "Griddy"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/sl.json b/homeassistant/components/griddy/translations/sl.json
deleted file mode 100644
index 8df85c6dc67af9..00000000000000
--- a/homeassistant/components/griddy/translations/sl.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "config": {
- "abort": {
- "already_configured": "Ta obremenitvena cona je \u017ee konfigurirana"
- },
- "error": {
- "cannot_connect": "Povezava ni uspela, poskusite znova",
- "unknown": "Nepri\u010dakovana napaka"
- },
- "step": {
- "user": {
- "data": {
- "loadzone": "Obremenitvena cona (poselitvena to\u010dka)"
- },
- "description": "Va\u0161a obremenitvena cona je v va\u0161em ra\u010dunu Griddy pod \"Ra\u010dun > Merilnik > Nalo\u017ei cono.\"",
- "title": "Nastavite svojo Griddy Load Cono"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/sv.json b/homeassistant/components/griddy/translations/sv.json
deleted file mode 100644
index e9ddacf2714959..00000000000000
--- a/homeassistant/components/griddy/translations/sv.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "config": {
- "error": {
- "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen",
- "unknown": "Ov\u00e4ntat fel"
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/tr.json b/homeassistant/components/griddy/translations/tr.json
deleted file mode 100644
index d887b1486584fa..00000000000000
--- a/homeassistant/components/griddy/translations/tr.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "config": {
- "error": {
- "cannot_connect": "Ba\u011flant\u0131 kurulamad\u0131, l\u00fctfen tekrar deneyin",
- "unknown": "Beklenmeyen hata"
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/griddy/translations/zh-Hant.json b/homeassistant/components/griddy/translations/zh-Hant.json
deleted file mode 100644
index 4918c1e818e5a3..00000000000000
--- a/homeassistant/components/griddy/translations/zh-Hant.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "config": {
- "abort": {
- "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
- },
- "error": {
- "cannot_connect": "\u9023\u7dda\u5931\u6557",
- "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
- },
- "step": {
- "user": {
- "data": {
- "loadzone": "\u8ca0\u8f09\u5340\u57df\uff08\u5c45\u4f4f\u9ede\uff09"
- },
- "description": "\u8ca0\u8f09\u5340\u57df\u986f\u793a\u65bc Griddy \u5e33\u865f\uff0c\u4f4d\u65bc \u201cAccount > Meter > Load Zone\u201d\u3002",
- "title": "\u8a2d\u5b9a Griddy \u8ca0\u8f09\u5340\u57df"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py
index 32a9bd41014cf0..5af53768bc0eb8 100644
--- a/homeassistant/components/group/__init__.py
+++ b/homeassistant/components/group/__init__.py
@@ -1,9 +1,11 @@
"""Provide the functionality to group entities."""
+from __future__ import annotations
+
from abc import abstractmethod
import asyncio
from contextvars import ContextVar
import logging
-from typing import Any, Dict, Iterable, List, Optional, Set, cast
+from typing import Any, Iterable, List, cast
import voluptuous as vol
@@ -13,6 +15,7 @@
ATTR_ENTITY_ID,
ATTR_ICON,
ATTR_NAME,
+ CONF_ENTITIES,
CONF_ICON,
CONF_NAME,
ENTITY_MATCH_ALL,
@@ -22,7 +25,7 @@
STATE_OFF,
STATE_ON,
)
-from homeassistant.core import CoreState, callback, split_entity_id
+from homeassistant.core import CoreState, HomeAssistant, callback, split_entity_id
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity, async_generate_entity_id
from homeassistant.helpers.entity_component import EntityComponent
@@ -31,7 +34,6 @@
async_process_integration_platforms,
)
from homeassistant.helpers.reload import async_reload_integration_platforms
-from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.loader import bind_hass
# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
@@ -41,7 +43,6 @@
ENTITY_ID_FORMAT = DOMAIN + ".{}"
-CONF_ENTITIES = "entities"
CONF_ALL = "all"
ATTR_ADD_ENTITIES = "add_entities"
@@ -91,16 +92,16 @@ def _conf_preprocess(value):
class GroupIntegrationRegistry:
"""Class to hold a registry of integrations."""
- on_off_mapping: Dict[str, str] = {STATE_ON: STATE_OFF}
- off_on_mapping: Dict[str, str] = {STATE_OFF: STATE_ON}
- on_states_by_domain: Dict[str, Set] = {}
- exclude_domains: Set = set()
+ on_off_mapping: dict[str, str] = {STATE_ON: STATE_OFF}
+ off_on_mapping: dict[str, str] = {STATE_OFF: STATE_ON}
+ on_states_by_domain: dict[str, set] = {}
+ exclude_domains: set = set()
def exclude_domain(self) -> None:
"""Exclude the current domain."""
self.exclude_domains.add(current_domain.get())
- def on_off_states(self, on_states: Set, off_state: str) -> None:
+ def on_off_states(self, on_states: set, off_state: str) -> None:
"""Register on and off states for the current domain."""
for on_state in on_states:
if on_state not in self.on_off_mapping:
@@ -128,12 +129,12 @@ def is_on(hass, entity_id):
@bind_hass
-def expand_entity_ids(hass: HomeAssistantType, entity_ids: Iterable[Any]) -> List[str]:
+def expand_entity_ids(hass: HomeAssistant, entity_ids: Iterable[Any]) -> list[str]:
"""Return entity_ids with group entity ids replaced by their members.
Async friendly.
"""
- found_ids: List[str] = []
+ found_ids: list[str] = []
for entity_id in entity_ids:
if not isinstance(entity_id, str) or entity_id in (
ENTITY_MATCH_NONE,
@@ -171,8 +172,8 @@ def expand_entity_ids(hass: HomeAssistantType, entity_ids: Iterable[Any]) -> Lis
@bind_hass
def get_entity_ids(
- hass: HomeAssistantType, entity_id: str, domain_filter: Optional[str] = None
-) -> List[str]:
+ hass: HomeAssistant, entity_id: str, domain_filter: str | None = None
+) -> list[str]:
"""Get members of this group.
Async friendly.
@@ -192,7 +193,7 @@ def get_entity_ids(
@bind_hass
-def groups_with_entity(hass: HomeAssistantType, entity_id: str) -> List[str]:
+def groups_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]:
"""Get all groups that contain this entity.
Async friendly.
@@ -345,7 +346,6 @@ async def groups_service_handler(service):
async def _process_group_platform(hass, domain, platform):
"""Process a group platform."""
-
current_domain.set(domain)
platform.async_describe_on_off_states(hass, hass.data[REG_KEY])
@@ -396,7 +396,6 @@ def should_poll(self) -> bool:
async def async_added_to_hass(self) -> None:
"""Register listeners."""
- assert self.hass is not None
async def _update_at_start(_):
await self.async_update()
@@ -406,8 +405,6 @@ async def _update_at_start(_):
async def async_defer_or_update_ha_state(self) -> None:
"""Only update once at start."""
- assert self.hass is not None
-
if self.hass.state != CoreState.running:
return
@@ -549,7 +546,7 @@ def icon(self, value):
self._icon = value
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes for the group."""
data = {ATTR_ENTITY_ID: self.tracking, ATTR_ORDER: self._order}
if not self.user_defined:
diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py
index b52546c48d7cae..5e8d18b28e2371 100644
--- a/homeassistant/components/group/cover.py
+++ b/homeassistant/components/group/cover.py
@@ -1,5 +1,5 @@
"""This platform allows several cover to be grouped into one cover."""
-from typing import Dict, Optional, Set
+from __future__ import annotations
import voluptuous as vol
@@ -76,18 +76,18 @@ def __init__(self, name, entities):
self._is_closed = False
self._is_closing = False
self._is_opening = False
- self._cover_position: Optional[int] = 100
+ self._cover_position: int | None = 100
self._tilt_position = None
self._supported_features = 0
self._assumed_state = True
self._entities = entities
- self._covers: Dict[str, Set[str]] = {
+ self._covers: dict[str, set[str]] = {
KEY_OPEN_CLOSE: set(),
KEY_STOP: set(),
KEY_POSITION: set(),
}
- self._tilts: Dict[str, Set[str]] = {
+ self._tilts: dict[str, set[str]] = {
KEY_OPEN_CLOSE: set(),
KEY_STOP: set(),
KEY_POSITION: set(),
@@ -102,7 +102,7 @@ async def _update_supported_features_event(self, event):
async def async_update_supported_features(
self,
entity_id: str,
- new_state: Optional[State],
+ new_state: State | None,
update_state: bool = True,
) -> None:
"""Update dictionaries with supported features."""
@@ -155,7 +155,6 @@ async def async_added_to_hass(self):
await self.async_update_supported_features(
entity_id, new_state, update_state=False
)
- assert self.hass is not None
self.async_on_remove(
async_track_state_change_event(
self.hass, self._entities, self._update_supported_features_event
@@ -198,7 +197,7 @@ def is_closing(self):
return self._is_closing
@property
- def current_cover_position(self) -> Optional[int]:
+ def current_cover_position(self) -> int | None:
"""Return current position for all covers."""
return self._cover_position
@@ -208,7 +207,7 @@ def current_cover_tilt_position(self):
return self._tilt_position
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes for the cover group."""
return {ATTR_ENTITY_ID: self._entities}
diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py
index 00b7321076f950..b45dd1ec5e3f41 100644
--- a/homeassistant/components/group/light.py
+++ b/homeassistant/components/group/light.py
@@ -1,8 +1,10 @@
"""This platform allows several lights to be grouped into one light."""
+from __future__ import annotations
+
import asyncio
from collections import Counter
import itertools
-from typing import Any, Callable, Iterator, List, Optional, Tuple, cast
+from typing import Any, Callable, Iterator, cast
import voluptuous as vol
@@ -35,10 +37,10 @@
STATE_ON,
STATE_UNAVAILABLE,
)
-from homeassistant.core import CoreState, State
+from homeassistant.core import CoreState, HomeAssistant, State
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_state_change_event
-from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+from homeassistant.helpers.typing import ConfigType
from homeassistant.util import color as color_util
from . import GroupEntity
@@ -67,7 +69,7 @@
async def async_setup_platform(
- hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None
+ hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None
) -> None:
"""Initialize light.group platform."""
async_add_entities(
@@ -78,21 +80,21 @@ async def async_setup_platform(
class LightGroup(GroupEntity, light.LightEntity):
"""Representation of a light group."""
- def __init__(self, name: str, entity_ids: List[str]) -> None:
+ def __init__(self, name: str, entity_ids: list[str]) -> None:
"""Initialize a light group."""
self._name = name
self._entity_ids = entity_ids
self._is_on = False
self._available = False
self._icon = "mdi:lightbulb-group"
- self._brightness: Optional[int] = None
- self._hs_color: Optional[Tuple[float, float]] = None
- self._color_temp: Optional[int] = None
- self._min_mireds: Optional[int] = 154
- self._max_mireds: Optional[int] = 500
- self._white_value: Optional[int] = None
- self._effect_list: Optional[List[str]] = None
- self._effect: Optional[str] = None
+ self._brightness: int | None = None
+ self._hs_color: tuple[float, float] | None = None
+ self._color_temp: int | None = None
+ self._min_mireds: int = 154
+ self._max_mireds: int = 500
+ self._white_value: int | None = None
+ self._effect_list: list[str] | None = None
+ self._effect: str | None = None
self._supported_features: int = 0
async def async_added_to_hass(self) -> None:
@@ -103,7 +105,6 @@ async def async_state_changed_listener(event):
self.async_set_context(event.context)
await self.async_defer_or_update_ha_state()
- assert self.hass
self.async_on_remove(
async_track_state_change_event(
self.hass, self._entity_ids, async_state_changed_listener
@@ -137,42 +138,42 @@ def icon(self):
return self._icon
@property
- def brightness(self) -> Optional[int]:
+ def brightness(self) -> int | None:
"""Return the brightness of this light group between 0..255."""
return self._brightness
@property
- def hs_color(self) -> Optional[Tuple[float, float]]:
+ def hs_color(self) -> tuple[float, float] | None:
"""Return the HS color value [float, float]."""
return self._hs_color
@property
- def color_temp(self) -> Optional[int]:
+ def color_temp(self) -> int | None:
"""Return the CT color value in mireds."""
return self._color_temp
@property
- def min_mireds(self) -> Optional[int]:
+ def min_mireds(self) -> int:
"""Return the coldest color_temp that this light group supports."""
return self._min_mireds
@property
- def max_mireds(self) -> Optional[int]:
+ def max_mireds(self) -> int:
"""Return the warmest color_temp that this light group supports."""
return self._max_mireds
@property
- def white_value(self) -> Optional[int]:
+ def white_value(self) -> int | None:
"""Return the white value of this light group between 0..255."""
return self._white_value
@property
- def effect_list(self) -> Optional[List[str]]:
+ def effect_list(self) -> list[str] | None:
"""Return the list of supported effects."""
return self._effect_list
@property
- def effect(self) -> Optional[str]:
+ def effect(self) -> str | None:
"""Return the current effect."""
return self._effect
@@ -187,7 +188,7 @@ def should_poll(self) -> bool:
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes for the light group."""
return {ATTR_ENTITY_ID: self._entity_ids}
@@ -289,7 +290,7 @@ async def async_turn_off(self, **kwargs):
async def async_update(self):
"""Query all members and determine the light group state."""
all_states = [self.hass.states.get(x) for x in self._entity_ids]
- states: List[State] = list(filter(None, all_states))
+ states: list[State] = list(filter(None, all_states))
on_states = [state for state in states if state.state == STATE_ON]
self._is_on = len(on_states) > 0
@@ -332,7 +333,7 @@ async def async_update(self):
self._supported_features &= SUPPORT_GROUP_LIGHT
-def _find_state_attributes(states: List[State], key: str) -> Iterator[Any]:
+def _find_state_attributes(states: list[State], key: str) -> Iterator[Any]:
"""Find attributes with matching key from states."""
for state in states:
value = state.attributes.get(key)
@@ -351,9 +352,9 @@ def _mean_tuple(*args):
def _reduce_attribute(
- states: List[State],
+ states: list[State],
key: str,
- default: Optional[Any] = None,
+ default: Any | None = None,
reduce: Callable[..., Any] = _mean_int,
) -> Any:
"""Find the first attribute matching key from states.
diff --git a/homeassistant/components/group/reproduce_state.py b/homeassistant/components/group/reproduce_state.py
index 95915412e4f997..ea21c147b9b7ff 100644
--- a/homeassistant/components/group/reproduce_state.py
+++ b/homeassistant/components/group/reproduce_state.py
@@ -1,22 +1,22 @@
"""Module that groups code required to handle state restore for component."""
-from typing import Any, Dict, Iterable, Optional
+from __future__ import annotations
-from homeassistant.core import Context, State
+from typing import Any, Iterable
+
+from homeassistant.core import Context, HomeAssistant, State
from homeassistant.helpers.state import async_reproduce_state
-from homeassistant.helpers.typing import HomeAssistantType
from . import get_entity_ids
async def async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce component states."""
-
states_copy = []
for state in states:
members = get_entity_ids(hass, state.entity_id)
diff --git a/homeassistant/components/group/translations/id.json b/homeassistant/components/group/translations/id.json
index 9a38f0f2de3c15..553cbce0550f0c 100644
--- a/homeassistant/components/group/translations/id.json
+++ b/homeassistant/components/group/translations/id.json
@@ -2,14 +2,14 @@
"state": {
"_": {
"closed": "Tertutup",
- "home": "Rumah",
+ "home": "Di Rumah",
"locked": "Terkunci",
"not_home": "Keluar",
- "off": "Off",
- "ok": "OK",
- "on": "On",
+ "off": "Mati",
+ "ok": "Oke",
+ "on": "Nyala",
"open": "Terbuka",
- "problem": "Masalah",
+ "problem": "Bermasalah",
"unlocked": "Terbuka"
}
},
diff --git a/homeassistant/components/group/translations/tr.json b/homeassistant/components/group/translations/tr.json
index f92785a737f6ff..5a596efdf01089 100644
--- a/homeassistant/components/group/translations/tr.json
+++ b/homeassistant/components/group/translations/tr.json
@@ -2,9 +2,9 @@
"state": {
"_": {
"closed": "Kapand\u0131",
- "home": "[%key:common::state::evde%]",
+ "home": "Evde",
"locked": "Kilitli",
- "not_home": "[%key:common::state::evde_degil%]",
+ "not_home": "D\u0131\u015far\u0131da",
"off": "Kapal\u0131",
"ok": "Tamam",
"on": "A\u00e7\u0131k",
diff --git a/homeassistant/components/group/translations/uk.json b/homeassistant/components/group/translations/uk.json
index 2d57686134a7d8..08cee558f273ce 100644
--- a/homeassistant/components/group/translations/uk.json
+++ b/homeassistant/components/group/translations/uk.json
@@ -9,7 +9,7 @@
"ok": "\u041e\u041a",
"on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e",
"open": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u043e",
- "problem": "\u0425\u0430\u043b\u0435\u043f\u0430",
+ "problem": "\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430",
"unlocked": "\u0420\u043e\u0437\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e"
}
},
diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py
index e6ed422db0f33e..86b88872a8ab09 100644
--- a/homeassistant/components/growatt_server/sensor.py
+++ b/homeassistant/components/growatt_server/sensor.py
@@ -7,7 +7,7 @@
import growattServer
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_NAME,
CONF_PASSWORD,
@@ -28,7 +28,6 @@
VOLT,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -399,7 +398,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
sensors = STORAGE_SENSOR_TYPES
else:
_LOGGER.debug(
- "Device type %s was found but is not supported right now.",
+ "Device type %s was found but is not supported right now",
device["deviceType"],
)
@@ -416,7 +415,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(entities, True)
-class GrowattInverter(Entity):
+class GrowattInverter(SensorEntity):
"""Representation of a Growatt Sensor."""
def __init__(self, probe, name, sensor, unique_id):
diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py
index ea211ccd748add..c927b04de25269 100644
--- a/homeassistant/components/gstreamer/media_player.py
+++ b/homeassistant/components/gstreamer/media_player.py
@@ -82,7 +82,7 @@ def set_volume_level(self, volume):
def play_media(self, media_type, media_id, **kwargs):
"""Play media."""
if media_type != MEDIA_TYPE_MUSIC:
- _LOGGER.error("invalid media type")
+ _LOGGER.error("Invalid media type")
return
self._player.queue(media_id)
diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py
index d21ab67f053468..46a31f464a1aac 100644
--- a/homeassistant/components/gtfs/sensor.py
+++ b/homeassistant/components/gtfs/sensor.py
@@ -1,15 +1,17 @@
"""Support for GTFS (Google/General Transport Format Schema)."""
+from __future__ import annotations
+
import datetime
import logging
import os
import threading
-from typing import Any, Callable, Optional
+from typing import Any, Callable
import pygtfs
from sqlalchemy.sql import text
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_NAME,
@@ -18,7 +20,6 @@
STATE_UNKNOWN,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import (
ConfigType,
DiscoveryInfoType,
@@ -484,7 +485,7 @@ def setup_platform(
hass: HomeAssistantType,
config: ConfigType,
add_entities: Callable[[list], None],
- discovery_info: Optional[DiscoveryInfoType] = None,
+ discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the GTFS sensor."""
gtfs_dir = hass.config.path(DEFAULT_PATH)
@@ -517,13 +518,13 @@ def setup_platform(
)
-class GTFSDepartureSensor(Entity):
+class GTFSDepartureSensor(SensorEntity):
"""Implementation of a GTFS departure sensor."""
def __init__(
self,
gtfs: Any,
- name: Optional[Any],
+ name: Any | None,
origin: Any,
destination: Any,
offset: cv.time_period,
@@ -540,7 +541,7 @@ def __init__(
self._available = False
self._icon = ICON
self._name = ""
- self._state: Optional[str] = None
+ self._state: str | None = None
self._attributes = {}
self._agency = None
@@ -559,7 +560,7 @@ def name(self) -> str:
return self._name
@property
- def state(self) -> Optional[str]: # type: ignore
+ def state(self) -> str | None: # type: ignore
"""Return the state of the sensor."""
return self._state
@@ -569,7 +570,7 @@ def available(self) -> bool:
return self._available
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return the state attributes."""
return self._attributes
@@ -811,7 +812,7 @@ def dict_for_table(resource: Any) -> dict:
col: getattr(resource, col) for col in resource.__table__.columns.keys()
}
- def append_keys(self, resource: dict, prefix: Optional[str] = None) -> None:
+ def append_keys(self, resource: dict, prefix: str | None = None) -> None:
"""Properly format key val pairs to append to attributes."""
for attr, val in resource.items():
if val == "" or val is None or attr == "feed_id":
diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py
index fdc82f14778f7d..ebb5e71e1cbb6b 100644
--- a/homeassistant/components/guardian/__init__.py
+++ b/homeassistant/components/guardian/__init__.py
@@ -1,6 +1,7 @@
"""The Elexa Guardian integration."""
+from __future__ import annotations
+
import asyncio
-from typing import Dict
from aioguardian import Client
@@ -104,9 +105,9 @@ def async_process_paired_sensor_uids():
].async_add_listener(async_process_paired_sensor_uids)
# Set up all of the Guardian entity platforms:
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -117,8 +118,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -246,7 +247,7 @@ def device_info(self) -> dict:
return self._device_info
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return the state attributes."""
return self._attrs
@@ -314,7 +315,7 @@ class ValveControllerEntity(GuardianEntity):
def __init__(
self,
entry: ConfigEntry,
- coordinators: Dict[str, DataUpdateCoordinator],
+ coordinators: dict[str, DataUpdateCoordinator],
kind: str,
name: str,
device_class: str,
diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py
index d8d0498304d082..e8c736eabe5535 100644
--- a/homeassistant/components/guardian/binary_sensor.py
+++ b/homeassistant/components/guardian/binary_sensor.py
@@ -1,5 +1,7 @@
"""Binary sensors for the Elexa Guardian integration."""
-from typing import Callable, Dict, Optional
+from __future__ import annotations
+
+from typing import Callable
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_CONNECTIVITY,
@@ -122,8 +124,8 @@ def __init__(
coordinator: DataUpdateCoordinator,
kind: str,
name: str,
- device_class: Optional[str],
- icon: Optional[str],
+ device_class: str | None,
+ icon: str | None,
) -> None:
"""Initialize."""
super().__init__(entry, coordinator, kind, name, device_class, icon)
@@ -155,11 +157,11 @@ class ValveControllerBinarySensor(ValveControllerEntity, BinarySensorEntity):
def __init__(
self,
entry: ConfigEntry,
- coordinators: Dict[str, DataUpdateCoordinator],
+ coordinators: dict[str, DataUpdateCoordinator],
kind: str,
name: str,
- device_class: Optional[str],
- icon: Optional[str],
+ device_class: str | None,
+ icon: str | None,
) -> None:
"""Initialize."""
super().__init__(entry, coordinators, kind, name, device_class, icon)
diff --git a/homeassistant/components/guardian/config_flow.py b/homeassistant/components/guardian/config_flow.py
index 760cf960e4389a..d6dcc0614f2f0b 100644
--- a/homeassistant/components/guardian/config_flow.py
+++ b/homeassistant/components/guardian/config_flow.py
@@ -7,7 +7,7 @@
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT
from homeassistant.core import callback
-from .const import CONF_UID, DOMAIN, LOGGER # pylint:disable=unused-import
+from .const import CONF_UID, DOMAIN, LOGGER
DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_IP_ADDRESS): str, vol.Required(CONF_PORT, default=7777): int}
@@ -88,7 +88,6 @@ async def async_step_zeroconf(self, discovery_info):
pin = async_get_pin_from_discovery_hostname(discovery_info["hostname"])
await self._async_set_unique_id(pin)
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context[CONF_IP_ADDRESS] = discovery_info["host"]
if any(
diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py
index 160246b2014915..48807c9cfeb789 100644
--- a/homeassistant/components/guardian/sensor.py
+++ b/homeassistant/components/guardian/sensor.py
@@ -1,6 +1,9 @@
"""Sensors for the Elexa Guardian integration."""
-from typing import Callable, Dict, Optional
+from __future__ import annotations
+from typing import Callable
+
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
DEVICE_CLASS_BATTERY,
@@ -108,7 +111,7 @@ def add_new_paired_sensor(uid: str) -> None:
async_add_entities(sensors)
-class PairedSensorSensor(PairedSensorEntity):
+class PairedSensorSensor(PairedSensorEntity, SensorEntity):
"""Define a binary sensor related to a Guardian valve controller."""
def __init__(
@@ -117,9 +120,9 @@ def __init__(
coordinator: DataUpdateCoordinator,
kind: str,
name: str,
- device_class: Optional[str],
- icon: Optional[str],
- unit: Optional[str],
+ device_class: str | None,
+ icon: str | None,
+ unit: str | None,
) -> None:
"""Initialize."""
super().__init__(entry, coordinator, kind, name, device_class, icon)
@@ -151,18 +154,18 @@ def _async_update_from_latest_data(self) -> None:
self._state = self.coordinator.data["temperature"]
-class ValveControllerSensor(ValveControllerEntity):
+class ValveControllerSensor(ValveControllerEntity, SensorEntity):
"""Define a generic Guardian sensor."""
def __init__(
self,
entry: ConfigEntry,
- coordinators: Dict[str, DataUpdateCoordinator],
+ coordinators: dict[str, DataUpdateCoordinator],
kind: str,
name: str,
- device_class: Optional[str],
- icon: Optional[str],
- unit: Optional[str],
+ device_class: str | None,
+ icon: str | None,
+ unit: str | None,
) -> None:
"""Initialize."""
super().__init__(entry, coordinators, kind, name, device_class, icon)
diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py
index 20a38ea5ce72ca..c574f283bdd946 100644
--- a/homeassistant/components/guardian/switch.py
+++ b/homeassistant/components/guardian/switch.py
@@ -1,5 +1,7 @@
"""Switches for the Elexa Guardian integration."""
-from typing import Callable, Dict
+from __future__ import annotations
+
+from typing import Callable
from aioguardian import Client
from aioguardian.errors import GuardianError
@@ -84,7 +86,7 @@ def __init__(
self,
entry: ConfigEntry,
client: Client,
- coordinators: Dict[str, DataUpdateCoordinator],
+ coordinators: dict[str, DataUpdateCoordinator],
):
"""Initialize."""
super().__init__(
diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json
index 27770d690f0ac7..432afe8df27eed 100644
--- a/homeassistant/components/guardian/translations/de.json
+++ b/homeassistant/components/guardian/translations/de.json
@@ -2,14 +2,19 @@
"config": {
"abort": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
- "cannot_connect": "Verbindungsfehler"
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"step": {
"user": {
"data": {
"ip_address": "IP-Adresse",
"port": "Port"
- }
+ },
+ "description": "Konfiguriere ein lokales Elexa Guardian Ger\u00e4t."
+ },
+ "zeroconf_confirm": {
+ "description": "M\u00f6chtest du dieses Guardian-Ger\u00e4t einrichten?"
}
}
}
diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json
index 563ede561557c5..bd43ce7672c385 100644
--- a/homeassistant/components/guardian/translations/hu.json
+++ b/homeassistant/components/guardian/translations/hu.json
@@ -1,7 +1,17 @@
{
"config": {
"abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "IP c\u00edm",
+ "port": "Port"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/guardian/translations/id.json b/homeassistant/components/guardian/translations/id.json
new file mode 100644
index 00000000000000..b5b753210378d9
--- /dev/null
+++ b/homeassistant/components/guardian/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "Alamat IP",
+ "port": "Port"
+ },
+ "description": "Konfigurasikan perangkat Elexa Guardian lokal."
+ },
+ "zeroconf_confirm": {
+ "description": "Ingin menyiapkan perangkat Guardian ini?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/guardian/translations/ko.json b/homeassistant/components/guardian/translations/ko.json
index da40b674009bab..d9f70ad2d33ecb 100644
--- a/homeassistant/components/guardian/translations/ko.json
+++ b/homeassistant/components/guardian/translations/ko.json
@@ -1,8 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 Guardian \uae30\uae30\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "already_in_progress": "Guardian \uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
diff --git a/homeassistant/components/guardian/translations/nl.json b/homeassistant/components/guardian/translations/nl.json
index 959a847a7cf337..a33cb9357a9e6b 100644
--- a/homeassistant/components/guardian/translations/nl.json
+++ b/homeassistant/components/guardian/translations/nl.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Dit Guardian-apparaat is al geconfigureerd.",
- "already_in_progress": "De configuratie van het Guardian-apparaat is al bezig.",
+ "already_configured": "Apparaat is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
"cannot_connect": "Kan geen verbinding maken"
},
"step": {
diff --git a/homeassistant/components/guardian/translations/tr.json b/homeassistant/components/guardian/translations/tr.json
new file mode 100644
index 00000000000000..1e520a16995d97
--- /dev/null
+++ b/homeassistant/components/guardian/translations/tr.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor",
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "\u0130p Adresi",
+ "port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/guardian/translations/uk.json b/homeassistant/components/guardian/translations/uk.json
new file mode 100644
index 00000000000000..439a225895e8fd
--- /dev/null
+++ b/homeassistant/components/guardian/translations/uk.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Elexa Guardian."
+ },
+ "zeroconf_confirm": {
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0446\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Elexa Guardian?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py
index b2c3fb168316ab..159e760e2235d7 100644
--- a/homeassistant/components/habitica/__init__.py
+++ b/homeassistant/components/habitica/__init__.py
@@ -1,52 +1,49 @@
-"""Support for Habitica devices."""
-from collections import namedtuple
+"""The habitica integration."""
+import asyncio
import logging
from habitipy.aio import HabitipyAsync
import voluptuous as vol
+from homeassistant import config_entries
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
+ ATTR_NAME,
CONF_API_KEY,
CONF_NAME,
- CONF_PATH,
CONF_SENSORS,
CONF_URL,
)
-from homeassistant.helpers import config_validation as cv, discovery
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-_LOGGER = logging.getLogger(__name__)
-
-CONF_API_USER = "api_user"
-
-DEFAULT_URL = "https://habitica.com"
-DOMAIN = "habitica"
-
-ST = SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"])
+from .const import (
+ ATTR_ARGS,
+ ATTR_PATH,
+ CONF_API_USER,
+ DEFAULT_URL,
+ DOMAIN,
+ EVENT_API_CALL_SUCCESS,
+ SERVICE_API_CALL,
+)
+from .sensor import SENSORS_TYPES
-SENSORS_TYPES = {
- "name": ST("Name", None, "", ["profile", "name"]),
- "hp": ST("HP", "mdi:heart", "HP", ["stats", "hp"]),
- "maxHealth": ST("max HP", "mdi:heart", "HP", ["stats", "maxHealth"]),
- "mp": ST("Mana", "mdi:auto-fix", "MP", ["stats", "mp"]),
- "maxMP": ST("max Mana", "mdi:auto-fix", "MP", ["stats", "maxMP"]),
- "exp": ST("EXP", "mdi:star", "EXP", ["stats", "exp"]),
- "toNextLevel": ST("Next Lvl", "mdi:star", "EXP", ["stats", "toNextLevel"]),
- "lvl": ST("Lvl", "mdi:arrow-up-bold-circle-outline", "Lvl", ["stats", "lvl"]),
- "gp": ST("Gold", "mdi:currency-usd-circle", "Gold", ["stats", "gp"]),
- "class": ST("Class", "mdi:sword", "", ["stats", "class"]),
-}
+_LOGGER = logging.getLogger(__name__)
-INSTANCE_SCHEMA = vol.Schema(
- {
- vol.Optional(CONF_URL, default=DEFAULT_URL): cv.url,
- vol.Optional(CONF_NAME): cv.string,
- vol.Required(CONF_API_USER): cv.string,
- vol.Required(CONF_API_KEY): cv.string,
- vol.Optional(CONF_SENSORS, default=list(SENSORS_TYPES)): vol.All(
- cv.ensure_list, vol.Unique(), [vol.In(list(SENSORS_TYPES))]
- ),
- }
+INSTANCE_SCHEMA = vol.All(
+ cv.deprecated(CONF_SENSORS),
+ vol.Schema(
+ {
+ vol.Optional(CONF_URL, default=DEFAULT_URL): cv.url,
+ vol.Optional(CONF_NAME): cv.string,
+ vol.Required(CONF_API_USER): cv.string,
+ vol.Required(CONF_API_KEY): cv.string,
+ vol.Optional(CONF_SENSORS, default=list(SENSORS_TYPES)): vol.All(
+ cv.ensure_list, vol.Unique(), [vol.In(list(SENSORS_TYPES))]
+ ),
+ }
+ ),
)
has_unique_values = vol.Schema(vol.Unique())
@@ -73,14 +70,9 @@ def has_all_unique_users_names(value):
INSTANCE_LIST_SCHEMA = vol.All(
cv.ensure_list, has_all_unique_users, has_all_unique_users_names, [INSTANCE_SCHEMA]
)
-
CONFIG_SCHEMA = vol.Schema({DOMAIN: INSTANCE_LIST_SCHEMA}, extra=vol.ALLOW_EXTRA)
-SERVICE_API_CALL = "api_call"
-ATTR_NAME = CONF_NAME
-ATTR_PATH = CONF_PATH
-ATTR_ARGS = "args"
-EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success"
+PLATFORMS = ["sensor"]
SERVICE_API_CALL_SCHEMA = vol.Schema(
{
@@ -91,12 +83,25 @@ def has_all_unique_users_names(value):
)
-async def async_setup(hass, config):
+async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up the Habitica service."""
+ configs = config.get(DOMAIN, [])
+
+ for conf in configs:
+ if conf.get(CONF_URL) is None:
+ conf[CONF_URL] = DEFAULT_URL
+
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf
+ )
+ )
+
+ return True
- conf = config[DOMAIN]
- data = hass.data[DOMAIN] = {}
- websession = async_get_clientsession(hass)
+
+async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+ """Set up habitica from a config entry."""
class HAHabitipyAsync(HabitipyAsync):
"""Closure API class to hold session."""
@@ -104,28 +109,6 @@ class HAHabitipyAsync(HabitipyAsync):
def __call__(self, **kwargs):
return super().__call__(websession, **kwargs)
- for instance in conf:
- url = instance[CONF_URL]
- username = instance[CONF_API_USER]
- password = instance[CONF_API_KEY]
- name = instance.get(CONF_NAME)
- config_dict = {"url": url, "login": username, "password": password}
- api = HAHabitipyAsync(config_dict)
- user = await api.user.get()
- if name is None:
- name = user["profile"]["name"]
- data[name] = api
- if CONF_SENSORS in instance:
- hass.async_create_task(
- discovery.async_load_platform(
- hass,
- "sensor",
- DOMAIN,
- {"name": name, "sensors": instance[CONF_SENSORS]},
- config,
- )
- )
-
async def handle_api_call(call):
name = call.data[ATTR_NAME]
path = call.data[ATTR_PATH]
@@ -147,7 +130,50 @@ async def handle_api_call(call):
EVENT_API_CALL_SUCCESS, {"name": name, "path": path, "data": data}
)
- hass.services.async_register(
- DOMAIN, SERVICE_API_CALL, handle_api_call, schema=SERVICE_API_CALL_SCHEMA
- )
+ data = hass.data.setdefault(DOMAIN, {})
+ config = config_entry.data
+ websession = async_get_clientsession(hass)
+ url = config[CONF_URL]
+ username = config[CONF_API_USER]
+ password = config[CONF_API_KEY]
+ name = config.get(CONF_NAME)
+ config_dict = {"url": url, "login": username, "password": password}
+ api = HAHabitipyAsync(config_dict)
+ user = await api.user.get()
+ if name is None:
+ name = user["profile"]["name"]
+ hass.config_entries.async_update_entry(
+ config_entry,
+ data={**config_entry.data, CONF_NAME: name},
+ )
+ data[config_entry.entry_id] = api
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ )
+
+ if not hass.services.has_service(DOMAIN, SERVICE_API_CALL):
+ hass.services.async_register(
+ DOMAIN, SERVICE_API_CALL, handle_api_call, schema=SERVICE_API_CALL_SCHEMA
+ )
+
return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ if len(hass.config_entries.async_entries(DOMAIN)) == 1:
+ hass.services.async_remove(DOMAIN, SERVICE_API_CALL)
+ return unload_ok
diff --git a/homeassistant/components/habitica/config_flow.py b/homeassistant/components/habitica/config_flow.py
new file mode 100644
index 00000000000000..051c15d17153b7
--- /dev/null
+++ b/homeassistant/components/habitica/config_flow.py
@@ -0,0 +1,86 @@
+"""Config flow for habitica integration."""
+from __future__ import annotations
+
+import logging
+
+from aiohttp import ClientResponseError
+from habitipy.aio import HabitipyAsync
+import voluptuous as vol
+
+from homeassistant import config_entries, core, exceptions
+from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+
+from .const import CONF_API_USER, DEFAULT_URL, DOMAIN
+
+DATA_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_API_USER): str,
+ vol.Required(CONF_API_KEY): str,
+ vol.Optional(CONF_NAME): str,
+ vol.Optional(CONF_URL, default=DEFAULT_URL): str,
+ }
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def validate_input(
+ hass: core.HomeAssistant, data: dict[str, str]
+) -> dict[str, str]:
+ """Validate the user input allows us to connect."""
+
+ websession = async_get_clientsession(hass)
+ api = HabitipyAsync(
+ conf={
+ "login": data[CONF_API_USER],
+ "password": data[CONF_API_KEY],
+ "url": data[CONF_URL] or DEFAULT_URL,
+ }
+ )
+ try:
+ await api.user.get(session=websession)
+ return {
+ "title": f"{data.get('name', 'Default username')}",
+ CONF_API_USER: data[CONF_API_USER],
+ }
+ except ClientResponseError as ex:
+ raise InvalidAuth() from ex
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for habitica."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+
+ errors = {}
+ if user_input is not None:
+ try:
+ info = await validate_input(self.hass, user_input)
+ except InvalidAuth:
+ errors = {"base": "invalid_credentials"}
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors = {"base": "unknown"}
+ else:
+ await self.async_set_unique_id(info[CONF_API_USER])
+ self._abort_if_unique_id_configured()
+ return self.async_create_entry(title=info["title"], data=user_input)
+ return self.async_show_form(
+ step_id="user",
+ data_schema=DATA_SCHEMA,
+ errors=errors,
+ description_placeholders={},
+ )
+
+ async def async_step_import(self, import_data):
+ """Import habitica config from configuration.yaml."""
+ return await self.async_step_user(import_data)
+
+
+class InvalidAuth(exceptions.HomeAssistantError):
+ """Error to indicate there is invalid auth."""
diff --git a/homeassistant/components/habitica/const.py b/homeassistant/components/habitica/const.py
new file mode 100644
index 00000000000000..02a46334c7a056
--- /dev/null
+++ b/homeassistant/components/habitica/const.py
@@ -0,0 +1,13 @@
+"""Constants for the habitica integration."""
+
+from homeassistant.const import CONF_PATH
+
+CONF_API_USER = "api_user"
+
+DEFAULT_URL = "https://habitica.com"
+DOMAIN = "habitica"
+
+SERVICE_API_CALL = "api_call"
+ATTR_PATH = CONF_PATH
+ATTR_ARGS = "args"
+EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success"
diff --git a/homeassistant/components/habitica/manifest.json b/homeassistant/components/habitica/manifest.json
index 50664d862ada6f..0779a2d3248640 100644
--- a/homeassistant/components/habitica/manifest.json
+++ b/homeassistant/components/habitica/manifest.json
@@ -1,7 +1,8 @@
{
- "domain": "habitica",
- "name": "Habitica",
- "documentation": "https://www.home-assistant.io/integrations/habitica",
- "requirements": ["habitipy==0.2.0"],
- "codeowners": []
+ "domain": "habitica",
+ "name": "Habitica",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/habitica",
+ "requirements": ["habitipy==0.2.0"],
+ "codeowners": ["@ASMfreaK", "@leikoilja"]
}
diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py
index f885aa832c78f0..52748ddadad8a3 100644
--- a/homeassistant/components/habitica/sensor.py
+++ b/homeassistant/components/habitica/sensor.py
@@ -1,25 +1,82 @@
"""Support for Habitica sensors."""
+from collections import namedtuple
from datetime import timedelta
+import logging
-from homeassistant.components import habitica
-from homeassistant.helpers.entity import Entity
+from aiohttp import ClientResponseError
+
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.const import CONF_NAME, HTTP_TOO_MANY_REQUESTS
from homeassistant.util import Throttle
-MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
+from .const import DOMAIN
+_LOGGER = logging.getLogger(__name__)
-async def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
- """Set up the habitica platform."""
- if discovery_info is None:
- return
+MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
- name = discovery_info[habitica.CONF_NAME]
- sensors = discovery_info[habitica.CONF_SENSORS]
- sensor_data = HabitipyData(hass.data[habitica.DOMAIN][name])
+ST = SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"])
+
+SENSORS_TYPES = {
+ "name": ST("Name", None, "", ["profile", "name"]),
+ "hp": ST("HP", "mdi:heart", "HP", ["stats", "hp"]),
+ "maxHealth": ST("max HP", "mdi:heart", "HP", ["stats", "maxHealth"]),
+ "mp": ST("Mana", "mdi:auto-fix", "MP", ["stats", "mp"]),
+ "maxMP": ST("max Mana", "mdi:auto-fix", "MP", ["stats", "maxMP"]),
+ "exp": ST("EXP", "mdi:star", "EXP", ["stats", "exp"]),
+ "toNextLevel": ST("Next Lvl", "mdi:star", "EXP", ["stats", "toNextLevel"]),
+ "lvl": ST("Lvl", "mdi:arrow-up-bold-circle-outline", "Lvl", ["stats", "lvl"]),
+ "gp": ST("Gold", "mdi:currency-usd-circle", "Gold", ["stats", "gp"]),
+ "class": ST("Class", "mdi:sword", "", ["stats", "class"]),
+}
+
+TASKS_TYPES = {
+ "habits": ST("Habits", "mdi:clipboard-list-outline", "n_of_tasks", ["habits"]),
+ "dailys": ST("Dailys", "mdi:clipboard-list-outline", "n_of_tasks", ["dailys"]),
+ "todos": ST("TODOs", "mdi:clipboard-list-outline", "n_of_tasks", ["todos"]),
+ "rewards": ST("Rewards", "mdi:clipboard-list-outline", "n_of_tasks", ["rewards"]),
+}
+
+TASKS_MAP_ID = "id"
+TASKS_MAP = {
+ "repeat": "repeat",
+ "challenge": "challenge",
+ "group": "group",
+ "frequency": "frequency",
+ "every_x": "everyX",
+ "streak": "streak",
+ "counter_up": "counterUp",
+ "counter_down": "counterDown",
+ "next_due": "nextDue",
+ "yester_daily": "yesterDaily",
+ "completed": "completed",
+ "collapse_checklist": "collapseChecklist",
+ "type": "type",
+ "notes": "notes",
+ "tags": "tags",
+ "value": "value",
+ "priority": "priority",
+ "start_date": "startDate",
+ "days_of_month": "daysOfMonth",
+ "weeks_of_month": "weeksOfMonth",
+ "created_at": "createdAt",
+ "text": "text",
+ "is_due": "isDue",
+}
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the habitica sensors."""
+
+ entities = []
+ name = config_entry.data[CONF_NAME]
+ sensor_data = HabitipyData(hass.data[DOMAIN][config_entry.entry_id])
await sensor_data.update()
- async_add_devices(
- [HabitipySensor(name, sensor, sensor_data) for sensor in sensors], True
- )
+ for sensor_type in SENSORS_TYPES:
+ entities.append(HabitipySensor(name, sensor_type, sensor_data))
+ for task_type in TASKS_TYPES:
+ entities.append(HabitipyTaskSensor(name, task_type, sensor_data))
+ async_add_entities(entities, True)
class HabitipyData:
@@ -29,21 +86,53 @@ def __init__(self, api):
"""Habitica API user data cache."""
self.api = api
self.data = None
+ self.tasks = {}
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def update(self):
"""Get a new fix from Habitica servers."""
- self.data = await self.api.user.get()
-
-
-class HabitipySensor(Entity):
+ try:
+ self.data = await self.api.user.get()
+ except ClientResponseError as error:
+ if error.status == HTTP_TOO_MANY_REQUESTS:
+ _LOGGER.warning(
+ "Sensor data update for %s has too many API requests;"
+ " Skipping the update",
+ DOMAIN,
+ )
+ else:
+ _LOGGER.error(
+ "Count not update sensor data for %s (%s)",
+ DOMAIN,
+ error,
+ )
+
+ for task_type in TASKS_TYPES:
+ try:
+ self.tasks[task_type] = await self.api.tasks.user.get(type=task_type)
+ except ClientResponseError as error:
+ if error.status == HTTP_TOO_MANY_REQUESTS:
+ _LOGGER.warning(
+ "Sensor data update for %s has too many API requests;"
+ " Skipping the update",
+ DOMAIN,
+ )
+ else:
+ _LOGGER.error(
+ "Count not update sensor data for %s (%s)",
+ DOMAIN,
+ error,
+ )
+
+
+class HabitipySensor(SensorEntity):
"""A generic Habitica sensor."""
def __init__(self, name, sensor_name, updater):
"""Initialize a generic Habitica sensor."""
self._name = name
self._sensor_name = sensor_name
- self._sensor_type = habitica.SENSORS_TYPES[sensor_name]
+ self._sensor_type = SENSORS_TYPES[sensor_name]
self._state = None
self._updater = updater
@@ -63,7 +152,7 @@ def icon(self):
@property
def name(self):
"""Return the name of the sensor."""
- return f"{habitica.DOMAIN}_{self._name}_{self._sensor_name}"
+ return f"{DOMAIN}_{self._name}_{self._sensor_name}"
@property
def state(self):
@@ -74,3 +163,63 @@ def state(self):
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._sensor_type.unit
+
+
+class HabitipyTaskSensor(SensorEntity):
+ """A Habitica task sensor."""
+
+ def __init__(self, name, task_name, updater):
+ """Initialize a generic Habitica task."""
+ self._name = name
+ self._task_name = task_name
+ self._task_type = TASKS_TYPES[task_name]
+ self._state = None
+ self._updater = updater
+
+ async def async_update(self):
+ """Update Condition and Forecast."""
+ await self._updater.update()
+ all_tasks = self._updater.tasks
+ for element in self._task_type.path:
+ tasks_length = len(all_tasks[element])
+ self._state = tasks_length
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend, if any."""
+ return self._task_type.icon
+
+ @property
+ def name(self):
+ """Return the name of the task."""
+ return f"{DOMAIN}_{self._name}_{self._task_name}"
+
+ @property
+ def state(self):
+ """Return the state of the device."""
+ return self._state
+
+ @property
+ def extra_state_attributes(self):
+ """Return the state attributes of all user tasks."""
+ if self._updater.tasks is not None:
+ all_received_tasks = self._updater.tasks
+ for element in self._task_type.path:
+ received_tasks = all_received_tasks[element]
+ attrs = {}
+
+ # Map tasks to TASKS_MAP
+ for received_task in received_tasks:
+ task_id = received_task[TASKS_MAP_ID]
+ task = {}
+ for map_key, map_value in TASKS_MAP.items():
+ value = received_task.get(map_value)
+ if value:
+ task[map_key] = value
+ attrs[task_id] = task
+ return attrs
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit the value is expressed in."""
+ return self._task_type.unit
diff --git a/homeassistant/components/habitica/services.yaml b/homeassistant/components/habitica/services.yaml
index 20794b4c47bfce..6fa8589ba4c119 100644
--- a/homeassistant/components/habitica/services.yaml
+++ b/homeassistant/components/habitica/services.yaml
@@ -1,6 +1,6 @@
# Describes the format for Habitica service
api_call:
- description: Call Habitica api
+ description: Call Habitica API
fields:
name:
description: Habitica's username to call for
@@ -9,5 +9,5 @@ api_call:
description: "Items from API URL in form of an array with method attached at the end. Consult https://habitica.com/apidoc/. Example uses https://habitica.com/apidoc/#api-Task-CreateUserTasks"
example: '["tasks", "user", "post"]'
args:
- description: Any additional json or url parameter arguments. See apidoc mentioned for path. Example uses same api endpoint
+ description: Any additional JSON or URL parameter arguments. See apidoc mentioned for path. Example uses same API endpoint
example: '{"text": "Use API from Home Assistant", "type": "todo"}'
diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json
new file mode 100644
index 00000000000000..868d024b02efa9
--- /dev/null
+++ b/homeassistant/components/habitica/strings.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "url": "[%key:common::config_flow::data::url%]",
+ "name": "Override for Habitica’s username. Will be used for service calls",
+ "api_user": "Habitica’s API user ID",
+ "api_key": "[%key:common::config_flow::data::api_key%]"
+ },
+ "description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks. Note that api_id and api_key must be gotten from https://habitica.com/user/settings/api"
+ }
+ }
+ },
+ "title": "Habitica"
+}
diff --git a/homeassistant/components/habitica/translations/bg.json b/homeassistant/components/habitica/translations/bg.json
new file mode 100644
index 00000000000000..02c83a6e9167b4
--- /dev/null
+++ b/homeassistant/components/habitica/translations/bg.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "url": "URL"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/ca.json b/homeassistant/components/habitica/translations/ca.json
new file mode 100644
index 00000000000000..675fc33db8cafc
--- /dev/null
+++ b/homeassistant/components/habitica/translations/ca.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Clau API",
+ "api_user": "ID d'usuari de l'API d'Habitica",
+ "name": "Substitueix el nom d'usuari d'Habitica. S'utilitzar\u00e0 per a crides de servei",
+ "url": "URL"
+ },
+ "description": "Connecta el perfil d'Habitica per permetre el seguiment del teu perfil i tasques d'usuari. Tingues en compte que l'api_id i l'api_key els has d'obtenir des de https://habitica.com/user/settings/api"
+ }
+ }
+ },
+ "title": "Habitica"
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/cs.json b/homeassistant/components/habitica/translations/cs.json
new file mode 100644
index 00000000000000..5ebfec2cf122ba
--- /dev/null
+++ b/homeassistant/components/habitica/translations/cs.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
+ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kl\u00ed\u010d API",
+ "url": "URL"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/de.json b/homeassistant/components/habitica/translations/de.json
new file mode 100644
index 00000000000000..04f985946fb509
--- /dev/null
+++ b/homeassistant/components/habitica/translations/de.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-Schl\u00fcssel",
+ "url": "URL"
+ }
+ }
+ }
+ },
+ "title": "Habitica"
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/en.json b/homeassistant/components/habitica/translations/en.json
new file mode 100644
index 00000000000000..ffbbd2de8408b8
--- /dev/null
+++ b/homeassistant/components/habitica/translations/en.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Key",
+ "api_user": "Habitica\u2019s API user ID",
+ "name": "Override for Habitica\u2019s username. Will be used for service calls",
+ "url": "URL"
+ },
+ "description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks. Note that api_id and api_key must be gotten from https://habitica.com/user/settings/api"
+ }
+ }
+ },
+ "title": "Habitica"
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/es.json b/homeassistant/components/habitica/translations/es.json
new file mode 100644
index 00000000000000..afdbb6666ad141
--- /dev/null
+++ b/homeassistant/components/habitica/translations/es.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "api_user": "ID de usuario de la API de Habitica",
+ "name": "Anular el nombre de usuario de Habitica. Se utilizar\u00e1 para llamadas de servicio.",
+ "url": "URL"
+ },
+ "description": "Conecta tu perfil de Habitica para permitir la supervisi\u00f3n del perfil y las tareas de tu usuario. Ten en cuenta que api_id y api_key deben obtenerse de https://habitica.com/user/settings/api"
+ }
+ }
+ },
+ "title": "Habitica"
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/et.json b/homeassistant/components/habitica/translations/et.json
new file mode 100644
index 00000000000000..cfc2bcf898c383
--- /dev/null
+++ b/homeassistant/components/habitica/translations/et.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "Vigane autentimine",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API v\u00f5ti",
+ "api_user": "Habitica API kasutaja ID",
+ "name": "Habitica kasutajanime alistamine. Kasutatakse teenuste kutsumiseks",
+ "url": "URL"
+ },
+ "description": "\u00dchenda oma Habitica profiil, et saaksid j\u00e4lgida oma kasutaja profiili ja \u00fclesandeid. Pane t\u00e4hele, et api_id ja api_key tuleb hankida aadressilt https://habitica.com/user/settings/api"
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/fr.json b/homeassistant/components/habitica/translations/fr.json
new file mode 100644
index 00000000000000..00fcd36a50821f
--- /dev/null
+++ b/homeassistant/components/habitica/translations/fr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "Authentification invalide",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Cl\u00e9 API",
+ "api_user": "ID utilisateur de l'API d'Habitica",
+ "name": "Remplacez le nom d\u2019utilisateur d\u2019Habitica. Sera utilis\u00e9 pour les appels de service",
+ "url": "URL"
+ },
+ "description": "Connectez votre profil Habitica pour permettre la surveillance du profil et des t\u00e2ches de votre utilisateur. Notez que api_id et api_key doivent \u00eatre obtenus de https://habitica.com/user/settings/api"
+ }
+ }
+ },
+ "title": "Habitica"
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/hu.json b/homeassistant/components/habitica/translations/hu.json
new file mode 100644
index 00000000000000..4914a1bd27a717
--- /dev/null
+++ b/homeassistant/components/habitica/translations/hu.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API kulcs",
+ "api_user": "Habitica API felhaszn\u00e1l\u00f3i azonos\u00edt\u00f3ja",
+ "url": "URL"
+ }
+ }
+ }
+ },
+ "title": "Habitica"
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/id.json b/homeassistant/components/habitica/translations/id.json
new file mode 100644
index 00000000000000..c7e4c549206ab0
--- /dev/null
+++ b/homeassistant/components/habitica/translations/id.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kunci API",
+ "api_user": "ID pengguna API Habitica",
+ "name": "Ganti nama pengguna Habitica. Nama ini akan digunakan untuk panggilan layanan",
+ "url": "URL"
+ },
+ "description": "Hubungkan profil Habitica Anda untuk memungkinkan pemantauan profil dan tugas pengguna Anda. Perhatikan bahwa api_id dan api_key harus diperoleh dari https://habitica.com/user/settings/api"
+ }
+ }
+ },
+ "title": "Habitica"
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/it.json b/homeassistant/components/habitica/translations/it.json
new file mode 100644
index 00000000000000..2bef21519b6581
--- /dev/null
+++ b/homeassistant/components/habitica/translations/it.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Chiave API",
+ "api_user": "ID utente API di Habitica",
+ "name": "Sostituisci il nome utente di Habitica. Verr\u00e0 utilizzato per le chiamate di servizio",
+ "url": "URL"
+ },
+ "description": "Collega il tuo profilo Habitica per consentire il monitoraggio del profilo e delle attivit\u00e0 dell'utente. Nota che api_id e api_key devono essere ottenuti da https://habitica.com/user/settings/api"
+ }
+ }
+ },
+ "title": "Habitica"
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/ko.json b/homeassistant/components/habitica/translations/ko.json
new file mode 100644
index 00000000000000..6b890a320df075
--- /dev/null
+++ b/homeassistant/components/habitica/translations/ko.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API \ud0a4",
+ "api_user": "Habitica\uc758 API \uc0ac\uc6a9\uc790 ID",
+ "name": "Habitica\uc758 \uc0ac\uc6a9\uc790 \uc774\ub984\uc744 \uc7ac\uc815\uc758\ud574\uc8fc\uc138\uc694. \uc11c\ube44\uc2a4 \ud638\ucd9c\uc5d0 \uc0ac\uc6a9\ub429\ub2c8\ub2e4",
+ "url": "URL \uc8fc\uc18c"
+ },
+ "description": "\uc0ac\uc6a9\uc790\uc758 \ud504\ub85c\ud544 \ubc0f \uc791\uc5c5\uc744 \ubaa8\ub2c8\ud130\ub9c1\ud560 \uc218 \uc788\ub3c4\ub85d \ud558\ub824\uba74 Habitica \ud504\ub85c\ud544\uc744 \uc5f0\uacb0\ud574\uc8fc\uc138\uc694.\n\ucc38\uace0\ub85c api_id \ubc0f api_key\ub294 https://habitica.com/user/settings/api \uc5d0\uc11c \uac00\uc838\uc640\uc57c \ud569\ub2c8\ub2e4."
+ }
+ }
+ },
+ "title": "Habitica"
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/nl.json b/homeassistant/components/habitica/translations/nl.json
new file mode 100644
index 00000000000000..817ffd8c616d7e
--- /dev/null
+++ b/homeassistant/components/habitica/translations/nl.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-sleutel",
+ "api_user": "Habitica's API-gebruikers-ID",
+ "name": "Vervanging voor de gebruikersnaam van Habitica. Wordt gebruikt voor serviceoproepen",
+ "url": "URL"
+ },
+ "description": "Verbind uw Habitica-profiel om het profiel en de taken van uw gebruiker te bewaken. Houd er rekening mee dat api_id en api_key van https://habitica.com/user/settings/api moeten worden gehaald"
+ }
+ }
+ },
+ "title": "Habitica"
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/no.json b/homeassistant/components/habitica/translations/no.json
new file mode 100644
index 00000000000000..cdb72d3c3d674e
--- /dev/null
+++ b/homeassistant/components/habitica/translations/no.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-n\u00f8kkel",
+ "api_user": "Habiticas API-bruker-ID",
+ "name": "Overstyr for Habiticas brukernavn. Blir brukt til serviceanrop",
+ "url": "URL"
+ },
+ "description": "Koble til Habitica-profilen din for \u00e5 tillate overv\u00e5king av brukerens profil og oppgaver. Merk at api_id og api_key m\u00e5 hentes fra https://habitica.com/user/settings/api"
+ }
+ }
+ },
+ "title": "Habitica"
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/pl.json b/homeassistant/components/habitica/translations/pl.json
new file mode 100644
index 00000000000000..f06f1a0e1aaa8b
--- /dev/null
+++ b/homeassistant/components/habitica/translations/pl.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Klucz API",
+ "api_user": "Identyfikator API u\u017cytkownika Habitica",
+ "name": "Nadpisanie nazwy u\u017cytkownika Habitica. B\u0119dzie u\u017cywany do wywo\u0142a\u0144 serwisowych.",
+ "url": "URL"
+ },
+ "description": "Po\u0142\u0105cz sw\u00f3j profil Habitica, aby umo\u017cliwi\u0107 monitorowanie profilu i zada\u0144 u\u017cytkownika. Pami\u0119taj, \u017ce api_id i api_key musz\u0105 zosta\u0107 pobrane z https://habitica.com/user/settings/api"
+ }
+ }
+ },
+ "title": "Habitica"
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/pt.json b/homeassistant/components/habitica/translations/pt.json
new file mode 100644
index 00000000000000..034099e4828a8f
--- /dev/null
+++ b/homeassistant/components/habitica/translations/pt.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "Autentica\u00e7\u00e3o inv\u00e1lida",
+ "unknown": "Erro inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Key",
+ "url": ""
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/ru.json b/homeassistant/components/habitica/translations/ru.json
new file mode 100644
index 00000000000000..4899cd1e43b212
--- /dev/null
+++ b/homeassistant/components/habitica/translations/ru.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "api_user": "ID \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f API Habitica",
+ "name": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0438\u043c\u0435\u043d\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f Habitica. \u0411\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0432\u044b\u0437\u043e\u0432\u043e\u0432 \u0441\u043b\u0443\u0436\u0431",
+ "url": "URL-\u0430\u0434\u0440\u0435\u0441"
+ },
+ "description": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 \u043f\u0440\u043e\u0444\u0438\u043b\u044c Habitica, \u0447\u0442\u043e\u0431\u044b \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0444\u0438\u043b\u044c \u0438 \u0437\u0430\u0434\u0430\u0447\u0438 \u0412\u0430\u0448\u0435\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f. \u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e api_id \u0438 api_key \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u044b \u0441 https://habitica.com/user/settings/api"
+ }
+ }
+ },
+ "title": "Habitica"
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/tr.json b/homeassistant/components/habitica/translations/tr.json
new file mode 100644
index 00000000000000..f77cc77798c5e9
--- /dev/null
+++ b/homeassistant/components/habitica/translations/tr.json
@@ -0,0 +1,3 @@
+{
+ "title": "Habitica"
+}
\ No newline at end of file
diff --git a/homeassistant/components/habitica/translations/zh-Hant.json b/homeassistant/components/habitica/translations/zh-Hant.json
new file mode 100644
index 00000000000000..001682b5c88577
--- /dev/null
+++ b/homeassistant/components/habitica/translations/zh-Hant.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "invalid_credentials": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API \u5bc6\u9470",
+ "api_user": "Habitica \u4e4b API \u4f7f\u7528\u8005 ID",
+ "name": "\u8986\u5beb Habitica \u4f7f\u7528\u8005\u540d\u7a31\u3001\u7528\u4ee5\u670d\u52d9\u547c\u53eb",
+ "url": "\u7db2\u5740"
+ },
+ "description": "\u9023\u7dda\u81f3 Habitica \u8a2d\u5b9a\u6a94\u4ee5\u4f9b\u76e3\u63a7\u500b\u4eba\u8a2d\u5b9a\u8207\u4efb\u52d9\u3002\u6ce8\u610f\uff1a\u5fc5\u9808\u7531 https://habitica.com/user/settings/api \u53d6\u5f97 api_id \u8207 api_key"
+ }
+ }
+ },
+ "title": "Habitica"
+}
\ No newline at end of file
diff --git a/homeassistant/components/hangouts/const.py b/homeassistant/components/hangouts/const.py
index 0508bf4870347b..3a78e9bbe80509 100644
--- a/homeassistant/components/hangouts/const.py
+++ b/homeassistant/components/hangouts/const.py
@@ -1,14 +1,9 @@
"""Constants for Google Hangouts Component."""
-import logging
-
import voluptuous as vol
from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET
import homeassistant.helpers.config_validation as cv
-_LOGGER = logging.getLogger(".")
-
-
DOMAIN = "hangouts"
CONF_2FA = "2fa"
diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py
index 56045f0eb1cf0e..65e3c3923adf25 100644
--- a/homeassistant/components/hangouts/hangouts_bot.py
+++ b/homeassistant/components/hangouts/hangouts_bot.py
@@ -1,5 +1,6 @@
"""The Hangouts Bot."""
import asyncio
+from contextlib import suppress
import io
import logging
@@ -103,12 +104,10 @@ def async_update_conversation_commands(self):
self._conversation_intents[conv_id][intent_type] = data
- try:
+ with suppress(ValueError):
self._conversation_list.on_event.remove_observer(
self._async_handle_conversation_event
)
- except ValueError:
- pass
self._conversation_list.on_event.add_observer(
self._async_handle_conversation_event
)
@@ -221,7 +220,7 @@ def _on_connect(self):
async def _on_disconnect(self):
"""Handle disconnecting."""
if self._connected:
- _LOGGER.debug("Connection lost! Reconnect...")
+ _LOGGER.debug("Connection lost! Reconnect")
await self.async_connect()
else:
dispatcher.async_dispatcher_send(self.hass, EVENT_HANGOUTS_DISCONNECTED)
diff --git a/homeassistant/components/hangouts/strings.json b/homeassistant/components/hangouts/strings.json
index bed46e823d93f7..0128363a1ab46f 100644
--- a/homeassistant/components/hangouts/strings.json
+++ b/homeassistant/components/hangouts/strings.json
@@ -7,7 +7,7 @@
"error": {
"invalid_login": "Invalid Login, please try again.",
"invalid_2fa": "Invalid 2 Factor Authentication, please try again.",
- "invalid_2fa_method": "Invalid 2FA Method (Verify on Phone)."
+ "invalid_2fa_method": "Invalid 2FA Method (verify on Phone)."
},
"step": {
"user": {
@@ -20,7 +20,7 @@
},
"2fa": {
"data": {
- "2fa": "2FA Pin"
+ "2fa": "2FA PIN"
},
"title": "2-Factor-Authentication"
}
diff --git a/homeassistant/components/hangouts/translations/ca.json b/homeassistant/components/hangouts/translations/ca.json
index 2d1d81bb08db66..b4114312f3e9e9 100644
--- a/homeassistant/components/hangouts/translations/ca.json
+++ b/homeassistant/components/hangouts/translations/ca.json
@@ -6,13 +6,13 @@
},
"error": {
"invalid_2fa": "La verificaci\u00f3 en dos passos no \u00e9s v\u00e0lida, torna-ho a provar.",
- "invalid_2fa_method": "El m\u00e8tode de verificaci\u00f3 en dos passos no \u00e9s v\u00e0lid (verifica-ho al m\u00f2bil).",
+ "invalid_2fa_method": "M\u00e8tode 2FA inv\u00e0lid (verifica-ho al m\u00f2bil).",
"invalid_login": "L'inici de sessi\u00f3 no \u00e9s v\u00e0lid, torna-ho a provar."
},
"step": {
"2fa": {
"data": {
- "2fa": "Pin 2FA"
+ "2fa": "PIN 2FA"
},
"description": "Buit",
"title": "Verificaci\u00f3 en dos passos"
diff --git a/homeassistant/components/hangouts/translations/de.json b/homeassistant/components/hangouts/translations/de.json
index 5c8ab51cf4e8c8..7b888cf531e079 100644
--- a/homeassistant/components/hangouts/translations/de.json
+++ b/homeassistant/components/hangouts/translations/de.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Google Hangouts ist bereits konfiguriert",
- "unknown": "Ein unbekannter Fehler ist aufgetreten."
+ "unknown": "Unerwarteter Fehler"
},
"error": {
"invalid_2fa": "Ung\u00fcltige 2-Faktor Authentifizierung, bitte versuche es erneut.",
diff --git a/homeassistant/components/hangouts/translations/en.json b/homeassistant/components/hangouts/translations/en.json
index 5de8ac249706f6..b2d7076bd75323 100644
--- a/homeassistant/components/hangouts/translations/en.json
+++ b/homeassistant/components/hangouts/translations/en.json
@@ -6,13 +6,13 @@
},
"error": {
"invalid_2fa": "Invalid 2 Factor Authentication, please try again.",
- "invalid_2fa_method": "Invalid 2FA Method (Verify on Phone).",
+ "invalid_2fa_method": "Invalid 2FA Method (verify on Phone).",
"invalid_login": "Invalid Login, please try again."
},
"step": {
"2fa": {
"data": {
- "2fa": "2FA Pin"
+ "2fa": "2FA PIN"
},
"title": "2-Factor-Authentication"
},
diff --git a/homeassistant/components/hangouts/translations/et.json b/homeassistant/components/hangouts/translations/et.json
index a587edcd632b17..7d6deb2ef53bd6 100644
--- a/homeassistant/components/hangouts/translations/et.json
+++ b/homeassistant/components/hangouts/translations/et.json
@@ -5,14 +5,14 @@
"unknown": "Tundmatu viga"
},
"error": {
- "invalid_2fa": "Vale 2-teguriline autentimine, proovige uuesti.",
- "invalid_2fa_method": "Kehtetu 2FA meetod (kontrollige telefoni teel).",
- "invalid_login": "Vale Kasutajanimi, palun proovige uuesti."
+ "invalid_2fa": "Vale 2-teguriline autentimine, proovi uuesti.",
+ "invalid_2fa_method": "Kehtetu kaheastmelise tuvastuse meetod (kontrolli telefonistl).",
+ "invalid_login": "Vale kasutajanimi, palun proovi uuesti."
},
"step": {
"2fa": {
"data": {
- "2fa": "2FA PIN"
+ "2fa": "Kaheastmelise tuvastuse PIN"
},
"description": "",
"title": "Kaheastmeline autentimine"
diff --git a/homeassistant/components/hangouts/translations/fr.json b/homeassistant/components/hangouts/translations/fr.json
index 2e8bec54c34a06..68e652db30956f 100644
--- a/homeassistant/components/hangouts/translations/fr.json
+++ b/homeassistant/components/hangouts/translations/fr.json
@@ -12,7 +12,7 @@
"step": {
"2fa": {
"data": {
- "2fa": "Code PIN d'authentification \u00e0 2 facteurs"
+ "2fa": "Code NIP d'authentification \u00e0 2 facteurs"
},
"description": "Vide",
"title": "Authentification \u00e0 2 facteurs"
diff --git a/homeassistant/components/hangouts/translations/hu.json b/homeassistant/components/hangouts/translations/hu.json
index 9a9f5b41598069..b81e3fcf0dd201 100644
--- a/homeassistant/components/hangouts/translations/hu.json
+++ b/homeassistant/components/hangouts/translations/hu.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "A Google Hangouts m\u00e1r konfigur\u00e1lva van",
- "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt."
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"error": {
"invalid_2fa": "\u00c9rv\u00e9nytelen K\u00e9tfaktoros hiteles\u00edt\u00e9s, pr\u00f3b\u00e1ld \u00fajra.",
@@ -14,7 +14,6 @@
"data": {
"2fa": "2FA Pin"
},
- "description": "\u00dcres",
"title": "K\u00e9tfaktoros Hiteles\u00edt\u00e9s"
},
"user": {
@@ -22,7 +21,6 @@
"email": "E-mail",
"password": "Jelsz\u00f3"
},
- "description": "\u00dcres",
"title": "Google Hangouts Bejelentkez\u00e9s"
}
}
diff --git a/homeassistant/components/hangouts/translations/id.json b/homeassistant/components/hangouts/translations/id.json
index 1bcfeaeba50ddf..39c68dda2110a5 100644
--- a/homeassistant/components/hangouts/translations/id.json
+++ b/homeassistant/components/hangouts/translations/id.json
@@ -1,29 +1,30 @@
{
"config": {
"abort": {
- "already_configured": "Google Hangouts sudah dikonfigurasikan",
- "unknown": "Kesalahan tidak dikenal terjadi."
+ "already_configured": "Layanan sudah dikonfigurasi",
+ "unknown": "Kesalahan yang tidak diharapkan"
},
"error": {
- "invalid_2fa": "Autentikasi 2 Faktor Tidak Valid, silakan coba lagi.",
- "invalid_2fa_method": "Metode 2FA Tidak Sah (Verifikasi di Ponsel).",
- "invalid_login": "Login tidak valid, silahkan coba lagi."
+ "invalid_2fa": "Autentikasi 2 Faktor Tidak Valid, coba lagi.",
+ "invalid_2fa_method": "Metode 2FA Tidak Valid (Verifikasikan di Ponsel).",
+ "invalid_login": "Info Masuk Tidak Valid, coba lagi."
},
"step": {
"2fa": {
"data": {
- "2fa": "Pin 2FA"
+ "2fa": "PIN 2FA"
},
"description": "Kosong",
- "title": "2-Faktor-Otentikasi"
+ "title": "Autentikasi Dua Faktor"
},
"user": {
"data": {
- "email": "Alamat email",
- "password": "Kata sandi"
+ "authorization_code": "Kode Otorisasi (diperlukan untuk autentikasi manual)",
+ "email": "Email",
+ "password": "Kata Sandi"
},
"description": "Kosong",
- "title": "Google Hangouts Login"
+ "title": "Info Masuk Google Hangouts"
}
}
}
diff --git a/homeassistant/components/hangouts/translations/it.json b/homeassistant/components/hangouts/translations/it.json
index 4831d51ef12c2b..3e89327ca30f0b 100644
--- a/homeassistant/components/hangouts/translations/it.json
+++ b/homeassistant/components/hangouts/translations/it.json
@@ -12,7 +12,7 @@
"step": {
"2fa": {
"data": {
- "2fa": "2FA Pin"
+ "2fa": "2FA PIN"
},
"description": "Vuoto",
"title": "Autenticazione a due fattori"
diff --git a/homeassistant/components/hangouts/translations/ko.json b/homeassistant/components/hangouts/translations/ko.json
index 51bd857e3581cb..56c3c577a89542 100644
--- a/homeassistant/components/hangouts/translations/ko.json
+++ b/homeassistant/components/hangouts/translations/ko.json
@@ -1,13 +1,13 @@
{
"config": {
"abort": {
- "already_configured": "Google \ud589\uc544\uc6c3\uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4",
- "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
"invalid_2fa": "2\ub2e8\uacc4 \uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
"invalid_2fa_method": "2\ub2e8\uacc4 \uc778\uc99d \ubc29\ubc95\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. (\uc804\ud654\uae30\uc5d0\uc11c \ud655\uc778)",
- "invalid_login": "\uc798\ubabb\ub41c \ub85c\uadf8\uc778\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694."
+ "invalid_login": "\ub85c\uadf8\uc778\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694."
},
"step": {
"2fa": {
diff --git a/homeassistant/components/hangouts/translations/nl.json b/homeassistant/components/hangouts/translations/nl.json
index fac77660251a88..456d2193922615 100644
--- a/homeassistant/components/hangouts/translations/nl.json
+++ b/homeassistant/components/hangouts/translations/nl.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Google Hangouts is al geconfigureerd",
- "unknown": "Onbekende fout opgetreden."
+ "already_configured": "Service is al geconfigureerd",
+ "unknown": "Onverwachte fout"
},
"error": {
"invalid_2fa": "Ongeldige twee-factor-authenticatie, probeer het opnieuw.",
@@ -20,7 +20,7 @@
"user": {
"data": {
"authorization_code": "Autorisatiecode (vereist voor handmatige authenticatie)",
- "email": "E-mailadres",
+ "email": "E-mail",
"password": "Wachtwoord"
},
"description": "Leeg",
diff --git a/homeassistant/components/hangouts/translations/no.json b/homeassistant/components/hangouts/translations/no.json
index fa341509634aa3..d4fe4dbb5a6656 100644
--- a/homeassistant/components/hangouts/translations/no.json
+++ b/homeassistant/components/hangouts/translations/no.json
@@ -6,13 +6,13 @@
},
"error": {
"invalid_2fa": "Ugyldig totrinnsbekreftelse, vennligst pr\u00f8v igjen.",
- "invalid_2fa_method": "Ugyldig totrinnsbekreftelse-metode (Bekreft p\u00e5 telefon)",
+ "invalid_2fa_method": "Ugyldig 2FA-metode (bekreft p\u00e5 telefon).",
"invalid_login": "Ugyldig innlogging, vennligst pr\u00f8v igjen."
},
"step": {
"2fa": {
"data": {
- "2fa": "Totrinnsbekreftelse Pin"
+ "2fa": "2FA PIN"
},
"description": "",
"title": "Totrinnsbekreftelse"
diff --git a/homeassistant/components/hangouts/translations/pl.json b/homeassistant/components/hangouts/translations/pl.json
index ff60deeece2c47..8fb7e9e64d92fd 100644
--- a/homeassistant/components/hangouts/translations/pl.json
+++ b/homeassistant/components/hangouts/translations/pl.json
@@ -12,7 +12,7 @@
"step": {
"2fa": {
"data": {
- "2fa": "PIN"
+ "2fa": "Kod uwierzytelniania dwusk\u0142adnikowego"
},
"description": "Pusty",
"title": "Uwierzytelnianie dwusk\u0142adnikowe"
diff --git a/homeassistant/components/hangouts/translations/tr.json b/homeassistant/components/hangouts/translations/tr.json
new file mode 100644
index 00000000000000..a204200a2d8438
--- /dev/null
+++ b/homeassistant/components/hangouts/translations/tr.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-posta",
+ "password": "Parola"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hangouts/translations/uk.json b/homeassistant/components/hangouts/translations/uk.json
new file mode 100644
index 00000000000000..93eb699d37c80b
--- /dev/null
+++ b/homeassistant/components/hangouts/translations/uk.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "error": {
+ "invalid_2fa": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443.",
+ "invalid_2fa_method": "\u041d\u0435\u043f\u0440\u0438\u043f\u0443\u0441\u0442\u0438\u043c\u0438\u0439 \u0441\u043f\u043e\u0441\u0456\u0431 \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 (\u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0438\u0442\u0438 \u043d\u0430 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0456).",
+ "invalid_login": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043b\u043e\u0433\u0456\u043d, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443."
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "\u041f\u0456\u043d-\u043a\u043e\u0434 \u0434\u043b\u044f \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ },
+ "description": "\u043f\u043e\u0440\u043e\u0436\u043d\u044c\u043e",
+ "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f"
+ },
+ "user": {
+ "data": {
+ "authorization_code": "\u041a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457 (\u0432\u0438\u043c\u0430\u0433\u0430\u0454\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0440\u0443\u0447\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457)",
+ "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "description": "\u043f\u043e\u0440\u043e\u0436\u043d\u044c\u043e",
+ "title": "Google Hangouts"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hangouts/translations/zh-Hant.json b/homeassistant/components/hangouts/translations/zh-Hant.json
index 62a220eaa9462d..678aacc5b62ca7 100644
--- a/homeassistant/components/hangouts/translations/zh-Hant.json
+++ b/homeassistant/components/hangouts/translations/zh-Hant.json
@@ -5,17 +5,17 @@
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"error": {
- "invalid_2fa": "\u96d9\u91cd\u9a57\u8b49\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002",
- "invalid_2fa_method": "\u8a8d\u8b49\u65b9\u5f0f\u7121\u6548\uff08\u65bc\u96fb\u8a71\u4e0a\u9a57\u8b49\uff09\u3002",
+ "invalid_2fa": "\u96d9\u91cd\u8a8d\u8b49\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002",
+ "invalid_2fa_method": "\u5169\u968e\u6bb5\u8a8d\u8b49\u65b9\u5f0f\u7121\u6548\uff08\u65bc\u96fb\u8a71\u4e0a\u9a57\u8b49\uff09\u3002",
"invalid_login": "\u767b\u5165\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002"
},
"step": {
"2fa": {
"data": {
- "2fa": "\u8a8d\u8b49\u78bc"
+ "2fa": "\u5169\u968e\u6bb5\u8a8d\u8b49\u78bc"
},
"description": "\u7a7a\u767d",
- "title": "\u96d9\u91cd\u9a57\u8b49"
+ "title": "\u96d9\u91cd\u8a8d\u8b49"
},
"user": {
"data": {
diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py
index 6ba63ee0f81eaa..c4056044ca0747 100644
--- a/homeassistant/components/harmony/__init__.py
+++ b/homeassistant/components/harmony/__init__.py
@@ -1,16 +1,20 @@
"""The Logitech Harmony Hub integration."""
import asyncio
+import logging
from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import entity_registry
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import DOMAIN, HARMONY_OPTIONS_UPDATE, PLATFORMS
from .data import HarmonyData
+_LOGGER = logging.getLogger(__name__)
+
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Logitech Harmony Hub component."""
@@ -40,16 +44,45 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.data[DOMAIN][entry.entry_id] = data
+ await _migrate_old_unique_ids(hass, entry.entry_id, data)
+
entry.add_update_listener(_update_listener)
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
+async def _migrate_old_unique_ids(
+ hass: HomeAssistant, entry_id: str, data: HarmonyData
+):
+ names_to_ids = {activity["label"]: activity["id"] for activity in data.activities}
+
+ @callback
+ def _async_migrator(entity_entry: entity_registry.RegistryEntry):
+ # Old format for switches was {remote_unique_id}-{activity_name}
+ # New format is activity_{activity_id}
+ parts = entity_entry.unique_id.split("-", 1)
+ if len(parts) > 1: # old format
+ activity_name = parts[1]
+ activity_id = names_to_ids.get(activity_name)
+
+ if activity_id is not None:
+ _LOGGER.info(
+ "Migrating unique_id from [%s] to [%s]",
+ entity_entry.unique_id,
+ activity_id,
+ )
+ return {"new_unique_id": f"activity_{activity_id}"}
+
+ return None
+
+ await entity_registry.async_migrate_entries(hass, entry_id, _async_migrator)
+
+
@callback
def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry):
options = dict(entry.options)
@@ -75,8 +108,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py
index e01febbef43b61..a91c1f3b5ca236 100644
--- a/homeassistant/components/harmony/config_flow.py
+++ b/homeassistant/components/harmony/config_flow.py
@@ -89,7 +89,6 @@ async def async_step_ssdp(self, discovery_info):
if self._host_already_configured(parsed_url.hostname):
return self.async_abort(reason="already_configured")
- # pylint: disable=no-member
self.context["title_placeholders"] = {"name": friendly_name}
self.harmony_config = {
@@ -120,6 +119,7 @@ async def async_step_link(self, user_input=None):
self.harmony_config, {}
)
+ self._set_confirm_only()
return self.async_show_form(
step_id="link",
errors=errors,
diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py
index ee4a454847e629..d7b4d8248ed139 100644
--- a/homeassistant/components/harmony/const.py
+++ b/homeassistant/components/harmony/const.py
@@ -6,9 +6,7 @@
UNIQUE_ID = "unique_id"
ACTIVITY_POWER_OFF = "PowerOff"
HARMONY_OPTIONS_UPDATE = "harmony_options_update"
-ATTR_ACTIVITY_LIST = "activity_list"
ATTR_DEVICES_LIST = "devices_list"
ATTR_LAST_ACTIVITY = "last_activity"
-ATTR_CURRENT_ACTIVITY = "current_activity"
ATTR_ACTIVITY_STARTING = "activity_starting"
PREVIOUS_ACTIVE_ACTIVITY = "Previous Active Activity"
diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py
index 6c3ad874fa9d2c..340596ff1efb4d 100644
--- a/homeassistant/components/harmony/data.py
+++ b/homeassistant/components/harmony/data.py
@@ -22,30 +22,25 @@ def __init__(self, hass, address: str, name: str, unique_id: str):
self._name = name
self._unique_id = unique_id
self._available = False
+ self._client = None
+ self._address = address
- callbacks = {
- "config_updated": self._config_updated,
- "connect": self._connected,
- "disconnect": self._disconnected,
- "new_activity_starting": self._activity_starting,
- "new_activity": self._activity_started,
- }
- self._client = HarmonyClient(
- ip_address=address, callbacks=ClientCallbackType(**callbacks)
- )
+ @property
+ def activities(self):
+ """List of all non-poweroff activity objects."""
+ activity_infos = self._client.config.get("activity", [])
+ return [
+ info
+ for info in activity_infos
+ if info["label"] is not None and info["label"] != ACTIVITY_POWER_OFF
+ ]
@property
def activity_names(self):
"""Names of all the remotes activities."""
- activity_infos = self._client.config.get("activity", [])
+ activity_infos = self.activities
activities = [activity["label"] for activity in activity_infos]
- # Remove both ways of representing PowerOff
- if None in activities:
- activities.remove(None)
- if ACTIVITY_POWER_OFF in activities:
- activities.remove(ACTIVITY_POWER_OFF)
-
return activities
@property
@@ -101,6 +96,18 @@ def device_info(self, domain: str):
async def connect(self) -> bool:
"""Connect to the Harmony Hub."""
_LOGGER.debug("%s: Connecting", self._name)
+
+ callbacks = {
+ "config_updated": self._config_updated,
+ "connect": self._connected,
+ "disconnect": self._disconnected,
+ "new_activity_starting": self._activity_starting,
+ "new_activity": self._activity_started,
+ }
+ self._client = HarmonyClient(
+ ip_address=self._address, callbacks=ClientCallbackType(**callbacks)
+ )
+
try:
if not await self._client.connect():
_LOGGER.warning("%s: Unable to connect to HUB", self._name)
@@ -109,6 +116,7 @@ async def connect(self) -> bool:
except aioexc.TimeOut:
_LOGGER.warning("%s: Connection timed-out", self._name)
return False
+
return True
async def shutdown(self):
@@ -155,10 +163,12 @@ async def async_start_activity(self, activity: str):
)
return
+ await self.async_lock_start_activity()
try:
await self._client.start_activity(activity_id)
except aioexc.TimeOut:
_LOGGER.error("%s: Starting activity %s timed-out", self.name, activity)
+ self.async_unlock_start_activity()
async def async_power_off(self):
"""Start the PowerOff activity."""
diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json
index 7509f3d4f4df6c..eb7a99fffa8eae 100644
--- a/homeassistant/components/harmony/manifest.json
+++ b/homeassistant/components/harmony/manifest.json
@@ -2,7 +2,7 @@
"domain": "harmony",
"name": "Logitech Harmony Hub",
"documentation": "https://www.home-assistant.io/integrations/harmony",
- "requirements": ["aioharmony==0.2.6"],
+ "requirements": ["aioharmony==0.2.7"],
"codeowners": ["@ehendrix23", "@bramkragten", "@bdraco", "@mkeesey"],
"ssdp": [
{
diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py
index 8409983789b225..a09f32ee95e4dd 100644
--- a/homeassistant/components/harmony/remote.py
+++ b/homeassistant/components/harmony/remote.py
@@ -12,6 +12,7 @@
ATTR_HOLD_SECS,
ATTR_NUM_REPEATS,
DEFAULT_DELAY_SECS,
+ SUPPORT_ACTIVITY,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID
@@ -24,9 +25,7 @@
from .connection_state import ConnectionStateMixin
from .const import (
ACTIVITY_POWER_OFF,
- ATTR_ACTIVITY_LIST,
ATTR_ACTIVITY_STARTING,
- ATTR_CURRENT_ACTIVITY,
ATTR_DEVICES_LIST,
ATTR_LAST_ACTIVITY,
DOMAIN,
@@ -100,6 +99,11 @@ def __init__(self, data, activity, delay_secs, out_path):
self._last_activity = None
self._config_path = out_path
+ @property
+ def supported_features(self):
+ """Supported features for the remote."""
+ return SUPPORT_ACTIVITY
+
async def _async_update_options(self, data):
"""Change options when the options flow does."""
if ATTR_DELAY_SECS in data:
@@ -161,7 +165,7 @@ async def async_added_to_hass(self):
@property
def device_info(self):
"""Return device info."""
- self._data.device_info(DOMAIN)
+ return self._data.device_info(DOMAIN)
@property
def unique_id(self):
@@ -179,12 +183,20 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def current_activity(self):
+ """Return the current activity."""
+ return self._current_activity
+
+ @property
+ def activity_list(self):
+ """Return the available activities."""
+ return self._data.activity_names
+
+ @property
+ def extra_state_attributes(self):
"""Add platform specific attributes."""
return {
ATTR_ACTIVITY_STARTING: self._activity_starting,
- ATTR_CURRENT_ACTIVITY: self._current_activity,
- ATTR_ACTIVITY_LIST: self._data.activity_names,
ATTR_DEVICES_LIST: self._data.device_names,
ATTR_LAST_ACTIVITY: self._last_activity,
}
diff --git a/homeassistant/components/harmony/subscriber.py b/homeassistant/components/harmony/subscriber.py
index d3bed33d560669..b2652cc43d118d 100644
--- a/homeassistant/components/harmony/subscriber.py
+++ b/homeassistant/components/harmony/subscriber.py
@@ -1,5 +1,6 @@
"""Mixin class for handling harmony callback subscriptions."""
+import asyncio
import logging
from typing import Any, Callable, NamedTuple, Optional
@@ -29,6 +30,17 @@ def __init__(self, hass):
super().__init__()
self._hass = hass
self._subscriptions = []
+ self._activity_lock = asyncio.Lock()
+
+ async def async_lock_start_activity(self):
+ """Acquire the lock."""
+ await self._activity_lock.acquire()
+
+ @callback
+ def async_unlock_start_activity(self):
+ """Release the lock."""
+ if self._activity_lock.locked():
+ self._activity_lock.release()
@callback
def async_subscribe(self, update_callbacks: HarmonyCallback) -> Callable:
@@ -51,11 +63,13 @@ def _config_updated(self, _=None) -> None:
def _connected(self, _=None) -> None:
_LOGGER.debug("connected")
+ self.async_unlock_start_activity()
self._available = True
self._call_callbacks("connected")
def _disconnected(self, _=None) -> None:
_LOGGER.debug("disconnected")
+ self.async_unlock_start_activity()
self._available = False
self._call_callbacks("disconnected")
@@ -65,6 +79,7 @@ def _activity_starting(self, activity_info: tuple) -> None:
def _activity_started(self, activity_info: tuple) -> None:
_LOGGER.debug("activity %s started", activity_info)
+ self.async_unlock_start_activity()
self._call_callbacks("activity_started", activity_info)
def _call_callbacks(self, callback_func_name: str, argument: tuple = None):
diff --git a/homeassistant/components/harmony/switch.py b/homeassistant/components/harmony/switch.py
index 5fae07c431ba35..5aac145e749934 100644
--- a/homeassistant/components/harmony/switch.py
+++ b/homeassistant/components/harmony/switch.py
@@ -15,12 +15,12 @@
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up harmony activity switches."""
data = hass.data[DOMAIN][entry.entry_id]
- activities = data.activity_names
+ activities = data.activities
switches = []
for activity in activities:
_LOGGER.debug("creating switch for activity: %s", activity)
- name = f"{entry.data[CONF_NAME]} {activity}"
+ name = f"{entry.data[CONF_NAME]} {activity['label']}"
switches.append(HarmonyActivitySwitch(name, activity, data))
async_add_entities(switches, True)
@@ -29,11 +29,12 @@ async def async_setup_entry(hass, entry, async_add_entities):
class HarmonyActivitySwitch(ConnectionStateMixin, SwitchEntity):
"""Switch representation of a Harmony activity."""
- def __init__(self, name: str, activity: str, data: HarmonyData):
+ def __init__(self, name: str, activity: dict, data: HarmonyData):
"""Initialize HarmonyActivitySwitch class."""
super().__init__()
self._name = name
- self._activity = activity
+ self._activity_name = activity["label"]
+ self._activity_id = activity["id"]
self._data = data
@property
@@ -44,13 +45,18 @@ def name(self):
@property
def unique_id(self):
"""Return the unique id."""
- return f"{self._data.unique_id}-{self._activity}"
+ return f"activity_{self._activity_id}"
+
+ @property
+ def device_info(self):
+ """Return device info."""
+ return self._data.device_info(DOMAIN)
@property
def is_on(self):
"""Return if the current activity is the one for this switch."""
_, activity_name = self._data.current_activity
- return activity_name == self._activity
+ return activity_name == self._activity_name
@property
def should_poll(self):
@@ -64,7 +70,7 @@ def available(self):
async def async_turn_on(self, **kwargs):
"""Start this activity."""
- await self._data.async_start_activity(self._activity)
+ await self._data.async_start_activity(self._activity_name)
async def async_turn_off(self, **kwargs):
"""Stop this activity."""
diff --git a/homeassistant/components/harmony/translations/de.json b/homeassistant/components/harmony/translations/de.json
index f10dfe1432cb56..9cd07f09529f44 100644
--- a/homeassistant/components/harmony/translations/de.json
+++ b/homeassistant/components/harmony/translations/de.json
@@ -4,7 +4,7 @@
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"unknown": "Unerwarteter Fehler"
},
"flow_title": "Logitech Harmony Hub {name}",
diff --git a/homeassistant/components/harmony/translations/hu.json b/homeassistant/components/harmony/translations/hu.json
index cbf055e2fba477..a9cb6ccecee7d7 100644
--- a/homeassistant/components/harmony/translations/hu.json
+++ b/homeassistant/components/harmony/translations/hu.json
@@ -1,5 +1,12 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/harmony/translations/id.json b/homeassistant/components/harmony/translations/id.json
new file mode 100644
index 00000000000000..0d2991b1feb106
--- /dev/null
+++ b/homeassistant/components/harmony/translations/id.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "flow_title": "Logitech Harmony Hub {name}",
+ "step": {
+ "link": {
+ "description": "Ingin menyiapkan {name} ({host})?",
+ "title": "Siapkan Logitech Harmony Hub"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nama Hub"
+ },
+ "title": "Siapkan Logitech Harmony Hub"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "activity": "Aktivitas default yang akan dijalankan jika tidak ada yang ditentukan.",
+ "delay_secs": "Penundaan antara mengirim perintah."
+ },
+ "description": "Sesuaikan Opsi Hub Harmony"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/harmony/translations/ko.json b/homeassistant/components/harmony/translations/ko.json
index 528f5e9cc7e10b..0e9d2a2cf573f1 100644
--- a/homeassistant/components/harmony/translations/ko.json
+++ b/homeassistant/components/harmony/translations/ko.json
@@ -4,13 +4,13 @@
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"flow_title": "Logitech Harmony Hub: {name}",
"step": {
"link": {
- "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "description": "{name} ({host})\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "Logitech Harmony Hub \uc124\uc815\ud558\uae30"
},
"user": {
diff --git a/homeassistant/components/harmony/translations/nl.json b/homeassistant/components/harmony/translations/nl.json
index 63d8026d9c2d02..33cbeca8893f71 100644
--- a/homeassistant/components/harmony/translations/nl.json
+++ b/homeassistant/components/harmony/translations/nl.json
@@ -4,7 +4,7 @@
"already_configured": "Apparaat is al geconfigureerd"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Kan geen verbinding maken",
"unknown": "Onverwachte fout"
},
"flow_title": "Logitech Harmony Hub {name}",
@@ -15,7 +15,7 @@
},
"user": {
"data": {
- "host": "Hostnaam of IP-adres",
+ "host": "Host",
"name": "Naam van hub"
},
"title": "Logitech Harmony Hub instellen"
diff --git a/homeassistant/components/harmony/translations/tr.json b/homeassistant/components/harmony/translations/tr.json
new file mode 100644
index 00000000000000..c77f0f8e07e7ba
--- /dev/null
+++ b/homeassistant/components/harmony/translations/tr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/harmony/translations/uk.json b/homeassistant/components/harmony/translations/uk.json
new file mode 100644
index 00000000000000..5bb2da811f3d5f
--- /dev/null
+++ b/homeassistant/components/harmony/translations/uk.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "flow_title": "Logitech Harmony Hub {name}",
+ "step": {
+ "link": {
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} ({host})?",
+ "title": "Logitech Harmony Hub"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430"
+ },
+ "title": "Logitech Harmony Hub"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "activity": "\u0410\u043a\u0442\u0438\u0432\u043d\u0456\u0441\u0442\u044c \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c, \u043a\u043e\u043b\u0438 \u0436\u043e\u0434\u043d\u0430 \u0437 \u043d\u0438\u0445 \u043d\u0435 \u0432\u043a\u0430\u0437\u0430\u043d\u0430.",
+ "delay_secs": "\u0417\u0430\u0442\u0440\u0438\u043c\u043a\u0430 \u043c\u0456\u0436 \u043d\u0430\u0434\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c \u043a\u043e\u043c\u0430\u043d\u0434."
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 Harmony Hub"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py
index e8b874b23345e8..a5a2a1886d744e 100644
--- a/homeassistant/components/hassio/__init__.py
+++ b/homeassistant/components/hassio/__init__.py
@@ -1,40 +1,63 @@
"""Support for Hass.io."""
+from __future__ import annotations
+
+import asyncio
from datetime import timedelta
import logging
import os
-from typing import Optional
+from typing import Any
import voluptuous as vol
from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.components.homeassistant import SERVICE_CHECK_CONFIG
import homeassistant.config as conf_util
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_NAME,
+ ATTR_SERVICE,
EVENT_CORE_CONFIG_UPDATE,
SERVICE_HOMEASSISTANT_RESTART,
SERVICE_HOMEASSISTANT_STOP,
)
-from homeassistant.core import DOMAIN as HASS_DOMAIN, callback
+from homeassistant.core import DOMAIN as HASS_DOMAIN, Config, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.device_registry import DeviceRegistry, async_get_registry
from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.loader import bind_hass
from homeassistant.util.dt import utcnow
from .addon_panel import async_setup_addon_panel
from .auth import async_setup_auth_view
-from .const import ATTR_DISCOVERY
+from .const import (
+ ATTR_ADDON,
+ ATTR_ADDONS,
+ ATTR_DISCOVERY,
+ ATTR_FOLDERS,
+ ATTR_HOMEASSISTANT,
+ ATTR_INPUT,
+ ATTR_PASSWORD,
+ ATTR_REPOSITORY,
+ ATTR_SLUG,
+ ATTR_SNAPSHOT,
+ ATTR_URL,
+ ATTR_VERSION,
+ DOMAIN,
+)
from .discovery import async_setup_discovery_view
from .handler import HassIO, HassioAPIError, api_data
from .http import HassIOView
from .ingress import async_setup_ingress_view
+from .websocket_api import async_load_websocket_api
_LOGGER = logging.getLogger(__name__)
-DOMAIN = "hassio"
+
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
+PLATFORMS = ["binary_sensor", "sensor"]
CONF_FRONTEND_REPO = "development_repo"
@@ -51,9 +74,12 @@
DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
HASSIO_UPDATE_INTERVAL = timedelta(minutes=55)
+ADDONS_COORDINATOR = "hassio_addons_coordinator"
+
SERVICE_ADDON_START = "addon_start"
SERVICE_ADDON_STOP = "addon_stop"
SERVICE_ADDON_RESTART = "addon_restart"
+SERVICE_ADDON_UPDATE = "addon_update"
SERVICE_ADDON_STDIN = "addon_stdin"
SERVICE_HOST_SHUTDOWN = "host_shutdown"
SERVICE_HOST_REBOOT = "host_reboot"
@@ -62,17 +88,10 @@
SERVICE_RESTORE_FULL = "restore_full"
SERVICE_RESTORE_PARTIAL = "restore_partial"
-ATTR_ADDON = "addon"
-ATTR_INPUT = "input"
-ATTR_SNAPSHOT = "snapshot"
-ATTR_ADDONS = "addons"
-ATTR_FOLDERS = "folders"
-ATTR_HOMEASSISTANT = "homeassistant"
-ATTR_PASSWORD = "password"
SCHEMA_NO_DATA = vol.Schema({})
-SCHEMA_ADDON = vol.Schema({vol.Required(ATTR_ADDON): cv.slug})
+SCHEMA_ADDON = vol.Schema({vol.Required(ATTR_ADDON): cv.string})
SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend(
{vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)}
@@ -101,10 +120,12 @@
}
)
+
MAP_SERVICE_API = {
SERVICE_ADDON_START: ("/addons/{addon}/start", SCHEMA_ADDON, 60, False),
SERVICE_ADDON_STOP: ("/addons/{addon}/stop", SCHEMA_ADDON, 60, False),
SERVICE_ADDON_RESTART: ("/addons/{addon}/restart", SCHEMA_ADDON, 60, False),
+ SERVICE_ADDON_UPDATE: ("/addons/{addon}/update", SCHEMA_ADDON, 60, False),
SERVICE_ADDON_STDIN: ("/addons/{addon}/stdin", SCHEMA_ADDON_STDIN, 60, False),
SERVICE_HOST_SHUTDOWN: ("/host/shutdown", SCHEMA_NO_DATA, 60, False),
SERVICE_HOST_REBOOT: ("/host/reboot", SCHEMA_NO_DATA, 60, False),
@@ -140,6 +161,16 @@ async def async_get_addon_info(hass: HomeAssistantType, slug: str) -> dict:
return await hassio.get_addon_info(slug)
+@bind_hass
+async def async_update_diagnostics(hass: HomeAssistantType, diagnostics: bool) -> dict:
+ """Update Supervisor diagnostics toggle.
+
+ The caller of the function should handle HassioAPIError.
+ """
+ hassio = hass.data[DOMAIN]
+ return await hassio.update_diagnostics(diagnostics)
+
+
@bind_hass
@api_data
async def async_install_addon(hass: HomeAssistantType, slug: str) -> dict:
@@ -164,6 +195,18 @@ async def async_uninstall_addon(hass: HomeAssistantType, slug: str) -> dict:
return await hassio.send_command(command, timeout=60)
+@bind_hass
+@api_data
+async def async_update_addon(hass: HomeAssistantType, slug: str) -> dict:
+ """Update add-on.
+
+ The caller of the function should handle HassioAPIError.
+ """
+ hassio = hass.data[DOMAIN]
+ command = f"/addons/{slug}/update"
+ return await hassio.send_command(command, timeout=None)
+
+
@bind_hass
@api_data
async def async_start_addon(hass: HomeAssistantType, slug: str) -> dict:
@@ -205,7 +248,7 @@ async def async_set_addon_options(
@bind_hass
async def async_get_addon_discovery_info(
hass: HomeAssistantType, slug: str
-) -> Optional[dict]:
+) -> dict | None:
"""Return discovery data for an add-on."""
hassio = hass.data[DOMAIN]
data = await hassio.retrieve_discovery_messages()
@@ -213,6 +256,21 @@ async def async_get_addon_discovery_info(
return next((addon for addon in discovered_addons if addon["addon"] == slug), None)
+@bind_hass
+@api_data
+async def async_create_snapshot(
+ hass: HomeAssistantType, payload: dict, partial: bool = False
+) -> dict:
+ """Create a full or partial snapshot.
+
+ The caller of the function should handle HassioAPIError.
+ """
+ hassio = hass.data[DOMAIN]
+ snapshot_type = "partial" if partial else "full"
+ command = f"/snapshots/new/{snapshot_type}"
+ return await hassio.send_command(command, payload=payload, timeout=None)
+
+
@callback
@bind_hass
def get_info(hass):
@@ -281,15 +339,21 @@ def get_supervisor_ip():
return os.environ["SUPERVISOR"].partition(":")[0]
-async def async_setup(hass, config):
+async def async_setup(hass: HomeAssistant, config: Config) -> bool:
"""Set up the Hass.io component."""
# Check local setup
for env in ("HASSIO", "HASSIO_TOKEN"):
if os.environ.get(env):
continue
_LOGGER.error("Missing %s environment variable", env)
+ if config_entries := hass.config_entries.async_entries(DOMAIN):
+ hass.async_create_task(
+ hass.config_entries.async_remove(config_entries[0].entry_id)
+ )
return False
+ async_load_websocket_api(hass)
+
host = os.environ["HASSIO"]
websession = hass.helpers.aiohttp_client.async_get_clientsession()
hass.data[DOMAIN] = hassio = HassIO(hass.loop, websession, host)
@@ -395,6 +459,8 @@ async def update_info_data(now):
hass.data[DATA_CORE_INFO] = await hassio.get_core_info()
hass.data[DATA_SUPERVISOR_INFO] = await hassio.get_supervisor_info()
hass.data[DATA_OS_INFO] = await hassio.get_os_info()
+ if ADDONS_COORDINATOR in hass.data:
+ await hass.data[ADDONS_COORDINATOR].async_refresh()
except HassioAPIError as err:
_LOGGER.warning("Can't read last version: %s", err)
@@ -448,4 +514,144 @@ async def async_handle_core_service(call):
# Init add-on ingress panels
await async_setup_addon_panel(hass, hassio)
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"})
+ )
+
return True
+
+
+async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+ """Set up a config entry."""
+ dev_reg = await async_get_registry(hass)
+ coordinator = HassioDataUpdateCoordinator(hass, config_entry, dev_reg)
+ hass.data[ADDONS_COORDINATOR] = coordinator
+ await coordinator.async_refresh()
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry
+) -> bool:
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+
+ # Pop add-on data
+ hass.data.pop(ADDONS_COORDINATOR, None)
+
+ return unload_ok
+
+
+@callback
+def async_register_addons_in_dev_reg(
+ entry_id: str, dev_reg: DeviceRegistry, addons: list[dict[str, Any]]
+) -> None:
+ """Register addons in the device registry."""
+ for addon in addons:
+ params = {
+ "config_entry_id": entry_id,
+ "identifiers": {(DOMAIN, addon[ATTR_SLUG])},
+ "model": "Home Assistant Add-on",
+ "sw_version": addon[ATTR_VERSION],
+ "name": addon[ATTR_NAME],
+ "entry_type": ATTR_SERVICE,
+ }
+ if manufacturer := addon.get(ATTR_REPOSITORY) or addon.get(ATTR_URL):
+ params["manufacturer"] = manufacturer
+ dev_reg.async_get_or_create(**params)
+
+
+@callback
+def async_register_os_in_dev_reg(
+ entry_id: str, dev_reg: DeviceRegistry, os_dict: dict[str, Any]
+) -> None:
+ """Register OS in the device registry."""
+ params = {
+ "config_entry_id": entry_id,
+ "identifiers": {(DOMAIN, "OS")},
+ "manufacturer": "Home Assistant",
+ "model": "Home Assistant Operating System",
+ "sw_version": os_dict[ATTR_VERSION],
+ "name": "Home Assistant Operating System",
+ "entry_type": ATTR_SERVICE,
+ }
+ dev_reg.async_get_or_create(**params)
+
+
+@callback
+def async_remove_addons_from_dev_reg(
+ dev_reg: DeviceRegistry, addons: list[dict[str, Any]]
+) -> None:
+ """Remove addons from the device registry."""
+ for addon_slug in addons:
+ if dev := dev_reg.async_get_device({(DOMAIN, addon_slug)}):
+ dev_reg.async_remove_device(dev.id)
+
+
+class HassioDataUpdateCoordinator(DataUpdateCoordinator):
+ """Class to retrieve Hass.io status."""
+
+ def __init__(
+ self, hass: HomeAssistant, config_entry: ConfigEntry, dev_reg: DeviceRegistry
+ ) -> None:
+ """Initialize coordinator."""
+ super().__init__(
+ hass,
+ _LOGGER,
+ name=DOMAIN,
+ update_method=self._async_update_data,
+ )
+ self.data = {}
+ self.entry_id = config_entry.entry_id
+ self.dev_reg = dev_reg
+ self.is_hass_os = "hassos" in get_info(self.hass)
+
+ async def _async_update_data(self) -> dict[str, Any]:
+ """Update data via library."""
+ new_data = {}
+ addon_data = get_supervisor_info(self.hass)
+
+ new_data["addons"] = {
+ addon[ATTR_SLUG]: addon for addon in addon_data.get("addons", [])
+ }
+ if self.is_hass_os:
+ new_data["os"] = get_os_info(self.hass)
+
+ # If this is the initial refresh, register all addons and return the dict
+ if not self.data:
+ async_register_addons_in_dev_reg(
+ self.entry_id, self.dev_reg, new_data["addons"].values()
+ )
+ if self.is_hass_os:
+ async_register_os_in_dev_reg(
+ self.entry_id, self.dev_reg, new_data["os"]
+ )
+ return new_data
+
+ # Remove add-ons that are no longer installed from device registry
+ if removed_addons := list(set(self.data["addons"]) - set(new_data["addons"])):
+ async_remove_addons_from_dev_reg(self.dev_reg, removed_addons)
+
+ # If there are new add-ons, we should reload the config entry so we can
+ # create new devices and entities. We can return an empty dict because
+ # coordinator will be recreated.
+ if list(set(new_data["addons"]) - set(self.data["addons"])):
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self.entry_id)
+ )
+ return {}
+
+ return new_data
diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py
index 9e44b961a1c534..a48c8b4d05b14e 100644
--- a/homeassistant/components/hassio/addon_panel.py
+++ b/homeassistant/components/hassio/addon_panel.py
@@ -5,10 +5,10 @@
from aiohttp import web
from homeassistant.components.http import HomeAssistantView
-from homeassistant.const import HTTP_BAD_REQUEST
+from homeassistant.const import ATTR_ICON, HTTP_BAD_REQUEST
from homeassistant.helpers.typing import HomeAssistantType
-from .const import ATTR_ADMIN, ATTR_ENABLE, ATTR_ICON, ATTR_PANELS, ATTR_TITLE
+from .const import ATTR_ADMIN, ATTR_ENABLE, ATTR_PANELS, ATTR_TITLE
from .handler import HassioAPIError
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py
new file mode 100644
index 00000000000000..b6faf566807349
--- /dev/null
+++ b/homeassistant/components/hassio/binary_sensor.py
@@ -0,0 +1,52 @@
+"""Binary sensor platform for Hass.io addons."""
+from __future__ import annotations
+
+from typing import Callable
+
+from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+
+from . import ADDONS_COORDINATOR
+from .const import ATTR_UPDATE_AVAILABLE
+from .entity import HassioAddonEntity, HassioOSEntity
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: Callable[[list[Entity], bool], None],
+) -> None:
+ """Binary sensor set up for Hass.io config entry."""
+ coordinator = hass.data[ADDONS_COORDINATOR]
+
+ entities = [
+ HassioAddonBinarySensor(
+ coordinator, addon, ATTR_UPDATE_AVAILABLE, "Update Available"
+ )
+ for addon in coordinator.data["addons"].values()
+ ]
+ if coordinator.is_hass_os:
+ entities.append(
+ HassioOSBinarySensor(coordinator, ATTR_UPDATE_AVAILABLE, "Update Available")
+ )
+ async_add_entities(entities)
+
+
+class HassioAddonBinarySensor(HassioAddonEntity, BinarySensorEntity):
+ """Binary sensor to track whether an update is available for a Hass.io add-on."""
+
+ @property
+ def is_on(self) -> bool:
+ """Return true if the binary sensor is on."""
+ return self.addon_info[self.attribute_name]
+
+
+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.os_info[self.attribute_name]
diff --git a/homeassistant/components/hassio/config_flow.py b/homeassistant/components/hassio/config_flow.py
new file mode 100644
index 00000000000000..acc39f4cf91d23
--- /dev/null
+++ b/homeassistant/components/hassio/config_flow.py
@@ -0,0 +1,22 @@
+"""Config flow for Home Assistant Supervisor integration."""
+import logging
+
+from homeassistant import config_entries
+
+from . import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Home Assistant Supervisor."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
+
+ async def async_step_system(self, user_input=None):
+ """Handle the initial step."""
+ # We only need one Hass.io config entry
+ await self.async_set_unique_id(DOMAIN)
+ self._abort_if_unique_id_configured()
+ return self.async_create_entry(title="Supervisor", data={})
diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py
index ffccb32539563f..417a62a1a8c08d 100644
--- a/homeassistant/components/hassio/const.py
+++ b/homeassistant/components/hassio/const.py
@@ -1,21 +1,47 @@
"""Hass.io const variables."""
-ATTR_ADDONS = "addons"
-ATTR_DISCOVERY = "discovery"
+DOMAIN = "hassio"
+
ATTR_ADDON = "addon"
-ATTR_NAME = "name"
-ATTR_SERVICE = "service"
+ATTR_ADDONS = "addons"
+ATTR_ADMIN = "admin"
ATTR_CONFIG = "config"
-ATTR_UUID = "uuid"
-ATTR_USERNAME = "username"
-ATTR_PASSWORD = "password"
-ATTR_PANELS = "panels"
+ATTR_DATA = "data"
+ATTR_DISCOVERY = "discovery"
ATTR_ENABLE = "enable"
+ATTR_FOLDERS = "folders"
+ATTR_HOMEASSISTANT = "homeassistant"
+ATTR_INPUT = "input"
+ATTR_PANELS = "panels"
+ATTR_PASSWORD = "password"
+ATTR_SNAPSHOT = "snapshot"
ATTR_TITLE = "title"
-ATTR_ICON = "icon"
-ATTR_ADMIN = "admin"
+ATTR_USERNAME = "username"
+ATTR_UUID = "uuid"
+ATTR_WS_EVENT = "event"
+ATTR_ENDPOINT = "endpoint"
+ATTR_METHOD = "method"
+ATTR_TIMEOUT = "timeout"
+
X_HASSIO = "X-Hassio-Key"
X_INGRESS_PATH = "X-Ingress-Path"
X_HASS_USER_ID = "X-Hass-User-ID"
X_HASS_IS_ADMIN = "X-Hass-Is-Admin"
+
+WS_TYPE = "type"
+WS_ID = "id"
+
+WS_TYPE_API = "supervisor/api"
+WS_TYPE_EVENT = "supervisor/event"
+WS_TYPE_SUBSCRIBE = "supervisor/subscribe"
+
+EVENT_SUPERVISOR_EVENT = "supervisor_event"
+
+# Add-on keys
+ATTR_VERSION = "version"
+ATTR_VERSION_LATEST = "version_latest"
+ATTR_UPDATE_AVAILABLE = "update_available"
+ATTR_SLUG = "slug"
+ATTR_URL = "url"
+ATTR_REPOSITORY = "repository"
diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py
index f3337254f1a9b4..c682e34c301063 100644
--- a/homeassistant/components/hassio/discovery.py
+++ b/homeassistant/components/hassio/discovery.py
@@ -6,17 +6,10 @@
from aiohttp.web_exceptions import HTTPServiceUnavailable
from homeassistant.components.http import HomeAssistantView
-from homeassistant.const import EVENT_HOMEASSISTANT_START
+from homeassistant.const import ATTR_NAME, ATTR_SERVICE, EVENT_HOMEASSISTANT_START
from homeassistant.core import callback
-from .const import (
- ATTR_ADDON,
- ATTR_CONFIG,
- ATTR_DISCOVERY,
- ATTR_NAME,
- ATTR_SERVICE,
- ATTR_UUID,
-)
+from .const import ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_UUID
from .handler import HassioAPIError
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py
new file mode 100644
index 00000000000000..5f35235bb5ded3
--- /dev/null
+++ b/homeassistant/components/hassio/entity.py
@@ -0,0 +1,95 @@
+"""Base for Hass.io entities."""
+from __future__ import annotations
+
+from typing import Any
+
+from homeassistant.const import ATTR_NAME
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from . import DOMAIN, HassioDataUpdateCoordinator
+from .const import ATTR_SLUG
+
+
+class HassioAddonEntity(CoordinatorEntity):
+ """Base entity for a Hass.io add-on."""
+
+ def __init__(
+ self,
+ coordinator: HassioDataUpdateCoordinator,
+ addon: dict[str, Any],
+ attribute_name: str,
+ sensor_name: str,
+ ) -> None:
+ """Initialize base entity."""
+ self.addon_slug = addon[ATTR_SLUG]
+ self.addon_name = addon[ATTR_NAME]
+ self._data_key = "addons"
+ self.attribute_name = attribute_name
+ self.sensor_name = sensor_name
+ super().__init__(coordinator)
+
+ @property
+ def addon_info(self) -> dict[str, Any]:
+ """Return add-on info."""
+ return self.coordinator.data[self._data_key][self.addon_slug]
+
+ @property
+ def name(self) -> str:
+ """Return entity name."""
+ return f"{self.addon_name}: {self.sensor_name}"
+
+ @property
+ def entity_registry_enabled_default(self) -> bool:
+ """Return if the entity should be enabled when first added to the entity registry."""
+ return False
+
+ @property
+ def unique_id(self) -> str:
+ """Return unique ID for entity."""
+ return f"{self.addon_slug}_{self.attribute_name}"
+
+ @property
+ def device_info(self) -> dict[str, Any]:
+ """Return device specific attributes."""
+ return {"identifiers": {(DOMAIN, self.addon_slug)}}
+
+
+class HassioOSEntity(CoordinatorEntity):
+ """Base Entity for Hass.io OS."""
+
+ def __init__(
+ self,
+ coordinator: HassioDataUpdateCoordinator,
+ attribute_name: str,
+ sensor_name: str,
+ ) -> None:
+ """Initialize base entity."""
+ self._data_key = "os"
+ self.attribute_name = attribute_name
+ self.sensor_name = sensor_name
+ super().__init__(coordinator)
+
+ @property
+ def os_info(self) -> dict[str, Any]:
+ """Return OS info."""
+ return self.coordinator.data[self._data_key]
+
+ @property
+ def name(self) -> str:
+ """Return entity name."""
+ return f"Home Assistant Operating System: {self.sensor_name}"
+
+ @property
+ def entity_registry_enabled_default(self) -> bool:
+ """Return if the entity should be enabled when first added to the entity registry."""
+ return False
+
+ @property
+ def unique_id(self) -> str:
+ """Return unique ID for entity."""
+ return f"home_assistant_os_{self.attribute_name}"
+
+ @property
+ def device_info(self) -> dict[str, Any]:
+ """Return device specific attributes."""
+ return {"identifiers": {(DOMAIN, "OS")}}
diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py
index 6bc3cb345a5167..90077261185126 100644
--- a/homeassistant/components/hassio/handler.py
+++ b/homeassistant/components/hassio/handler.py
@@ -4,7 +4,6 @@
import os
import aiohttp
-import async_timeout
from homeassistant.components.http import (
CONF_SERVER_HOST,
@@ -52,7 +51,12 @@ async def _wrapper(*argv, **kwargs):
class HassIO:
"""Small API wrapper for Hass.io."""
- def __init__(self, loop, websession, ip):
+ def __init__(
+ self,
+ loop: asyncio.AbstractEventLoop,
+ websession: aiohttp.ClientSession,
+ ip: str,
+ ) -> None:
"""Initialize Hass.io API."""
self.loop = loop
self.websession = websession
@@ -181,26 +185,36 @@ def update_hass_timezone(self, timezone):
"""
return self.send_command("/supervisor/options", payload={"timezone": timezone})
+ @_api_bool
+ def update_diagnostics(self, diagnostics: bool):
+ """Update Supervisor diagnostics setting.
+
+ This method return a coroutine.
+ """
+ return self.send_command(
+ "/supervisor/options", payload={"diagnostics": diagnostics}
+ )
+
async def send_command(self, command, method="post", payload=None, timeout=10):
"""Send API command to Hass.io.
This method is a coroutine.
"""
try:
- with async_timeout.timeout(timeout):
- request = await self.websession.request(
- method,
- f"http://{self._ip}{command}",
- json=payload,
- headers={X_HASSIO: os.environ.get("HASSIO_TOKEN", "")},
- )
-
- if request.status not in (HTTP_OK, HTTP_BAD_REQUEST):
- _LOGGER.error("%s return code %d", command, request.status)
- raise HassioAPIError()
-
- answer = await request.json()
- return answer
+ request = await self.websession.request(
+ method,
+ f"http://{self._ip}{command}",
+ json=payload,
+ headers={X_HASSIO: os.environ.get("HASSIO_TOKEN", "")},
+ timeout=aiohttp.ClientTimeout(total=timeout),
+ )
+
+ if request.status not in (HTTP_OK, HTTP_BAD_REQUEST):
+ _LOGGER.error("%s return code %d", command, request.status)
+ raise HassioAPIError()
+
+ answer = await request.json()
+ return answer
except asyncio.TimeoutError:
_LOGGER.error("Timeout on %s request", command)
diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py
index 2aa05ae6ab429e..e1bd1cb095c53e 100644
--- a/homeassistant/components/hassio/http.py
+++ b/homeassistant/components/hassio/http.py
@@ -1,9 +1,10 @@
"""HTTP Support for Hass.io."""
+from __future__ import annotations
+
import asyncio
import logging
import os
import re
-from typing import Dict, Union
import aiohttp
from aiohttp import web
@@ -57,7 +58,7 @@ def __init__(self, host: str, websession: aiohttp.ClientSession):
async def _handle(
self, request: web.Request, path: str
- ) -> Union[web.Response, web.StreamResponse]:
+ ) -> web.Response | web.StreamResponse:
"""Route data to Hass.io."""
hass = request.app["hass"]
if _need_auth(hass, path) and not request[KEY_AUTHENTICATED]:
@@ -71,7 +72,7 @@ async def _handle(
async def _command_proxy(
self, path: str, request: web.Request
- ) -> Union[web.Response, web.StreamResponse]:
+ ) -> web.Response | web.StreamResponse:
"""Return a client request with proxy origin for Hass.io supervisor.
This method is a coroutine.
@@ -131,7 +132,7 @@ async def _command_proxy(
raise HTTPBadGateway()
-def _init_header(request: web.Request) -> Dict[str, str]:
+def _init_header(request: web.Request) -> dict[str, str]:
"""Create initial header."""
headers = {
X_HASSIO: os.environ.get("HASSIO_TOKEN", ""),
diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py
index c69d2078468fd7..1f0a49ae497ee0 100644
--- a/homeassistant/components/hassio/ingress.py
+++ b/homeassistant/components/hassio/ingress.py
@@ -1,9 +1,10 @@
"""Hass.io Add-on ingress service."""
+from __future__ import annotations
+
import asyncio
from ipaddress import ip_address
import logging
import os
-from typing import Dict, Union
import aiohttp
from aiohttp import hdrs, web
@@ -46,7 +47,7 @@ def _create_url(self, token: str, path: str) -> str:
async def _handle(
self, request: web.Request, token: str, path: str
- ) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]:
+ ) -> web.Response | web.StreamResponse | web.WebSocketResponse:
"""Route data to Hass.io ingress service."""
try:
# Websocket
@@ -114,7 +115,7 @@ async def _handle_websocket(
async def _handle_request(
self, request: web.Request, token: str, path: str
- ) -> Union[web.Response, web.StreamResponse]:
+ ) -> web.Response | web.StreamResponse:
"""Ingress route for request."""
url = self._create_url(token, path)
data = await request.read()
@@ -159,9 +160,7 @@ async def _handle_request(
return response
-def _init_header(
- request: web.Request, token: str
-) -> Union[CIMultiDict, Dict[str, str]]:
+def _init_header(request: web.Request, token: str) -> CIMultiDict | dict[str, str]:
"""Create initial header."""
headers = {}
@@ -208,7 +207,7 @@ def _init_header(
return headers
-def _response_header(response: aiohttp.ClientResponse) -> Dict[str, str]:
+def _response_header(response: aiohttp.ClientResponse) -> dict[str, str]:
"""Create response header."""
headers = {}
diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py
new file mode 100644
index 00000000000000..c41c0dc5090384
--- /dev/null
+++ b/homeassistant/components/hassio/sensor.py
@@ -0,0 +1,55 @@
+"""Sensor platform for Hass.io addons."""
+from __future__ import annotations
+
+from typing import Callable
+
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+
+from . import ADDONS_COORDINATOR
+from .const import ATTR_VERSION, ATTR_VERSION_LATEST
+from .entity import HassioAddonEntity, HassioOSEntity
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: Callable[[list[Entity], bool], None],
+) -> None:
+ """Sensor set up for Hass.io config entry."""
+ coordinator = hass.data[ADDONS_COORDINATOR]
+
+ entities = []
+
+ for attribute_name, sensor_name in (
+ (ATTR_VERSION, "Version"),
+ (ATTR_VERSION_LATEST, "Newest Version"),
+ ):
+ for addon in coordinator.data["addons"].values():
+ entities.append(
+ HassioAddonSensor(coordinator, addon, attribute_name, sensor_name)
+ )
+ if coordinator.is_hass_os:
+ entities.append(HassioOSSensor(coordinator, attribute_name, sensor_name))
+
+ async_add_entities(entities)
+
+
+class HassioAddonSensor(HassioAddonEntity, SensorEntity):
+ """Sensor to track a Hass.io add-on attribute."""
+
+ @property
+ def state(self) -> str:
+ """Return state of entity."""
+ return self.addon_info[self.attribute_name]
+
+
+class HassioOSSensor(HassioOSEntity, SensorEntity):
+ """Sensor to track a Hass.io add-on attribute."""
+
+ @property
+ def state(self) -> str:
+ """Return state of entity."""
+ return self.os_info[self.attribute_name]
diff --git a/homeassistant/components/hassio/services.yaml b/homeassistant/components/hassio/services.yaml
index 8afdcc633bff1f..0652b65d6e281f 100644
--- a/homeassistant/components/hassio/services.yaml
+++ b/homeassistant/components/hassio/services.yaml
@@ -1,110 +1,113 @@
-addon_install:
- description: Install a Hass.io docker add-on.
- fields:
- addon:
- description: The add-on slug.
- example: core_ssh
- version:
- description: Optional or it will be use the latest version.
- example: "0.2"
-
addon_start:
- description: Start a Hass.io docker add-on.
+ name: Start add-on
+ description: Start add-on.
fields:
addon:
+ name: Add-on
+ required: true
description: The add-on slug.
example: core_ssh
+ selector:
+ addon:
addon_restart:
- description: Restart a Hass.io docker add-on.
+ name: Restart add-on.
+ description: Restart add-on.
fields:
addon:
+ name: Add-on
+ required: true
description: The add-on slug.
example: core_ssh
+ selector:
+ addon:
addon_stdin:
- description: Write data to a Hass.io docker add-on stdin .
+ name: Write data to add-on stdin.
+ description: Write data to add-on stdin.
fields:
addon:
+ name: Add-on
+ required: true
description: The add-on slug.
example: core_ssh
+ selector:
+ addon:
addon_stop:
- description: Stop a Hass.io docker add-on.
- fields:
- addon:
- description: The add-on slug.
- example: core_ssh
-
-addon_uninstall:
- description: Uninstall a Hass.io docker add-on.
+ name: Stop add-on.
+ description: Stop add-on.
fields:
addon:
+ name: Add-on
+ required: true
description: The add-on slug.
example: core_ssh
+ selector:
+ addon:
addon_update:
- description: Update a Hass.io docker add-on.
+ name: Update add-on.
+ description: Update add-on. This service should be used with caution since add-on updates can contain breaking changes. It is highly recommended that you review release notes/change logs before updating an add-on.
fields:
addon:
+ name: Add-on
+ required: true
description: The add-on slug.
example: core_ssh
- version:
- description: Optional or it will be use the latest version.
- example: "0.2"
-
-homeassistant_update:
- description: Update the Home Assistant docker image.
- fields:
- version:
- description: Optional or it will be use the latest version.
- example: 0.40.1
+ selector:
+ addon:
host_reboot:
+ name: Reboot the host system.
description: Reboot the host system.
host_shutdown:
+ name: Poweroff the host system.
description: Poweroff the host system.
-host_update:
- description: Update the host system.
- fields:
- version:
- description: Optional or it will be use the latest version.
- example: "0.3"
-
snapshot_full:
+ name: Create a full snapshot.
description: Create a full snapshot.
fields:
name:
+ name: Name
description: Optional or it will be the current date and time.
example: "Snapshot 1"
+ selector:
+ text:
password:
+ name: Password
description: Optional password.
example: "password"
+ selector:
+ text:
snapshot_partial:
+ name: Create a partial snapshot.
description: Create a partial snapshot.
fields:
addons:
+ name: Add-ons
description: Optional list of addon slugs.
example: ["core_ssh", "core_samba", "core_mosquitto"]
+ selector:
+ object:
folders:
+ name: Folders
description: Optional list of directories.
example: ["homeassistant", "share"]
+ selector:
+ object:
name:
+ name: Name
description: Optional or it will be the current date and time.
example: "Partial Snapshot 1"
+ selector:
+ text:
password:
+ name: Password
description: Optional password.
example: "password"
-
-supervisor_reload:
- description: Reload the Hass.io supervisor.
-
-supervisor_update:
- description: Update the Hass.io supervisor.
- fields:
- version:
- description: Optional or it will be use the latest version.
- example: "0.3"
+ selector:
+ text:
diff --git a/homeassistant/components/hassio/translations/af.json b/homeassistant/components/hassio/translations/af.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/af.json
+++ b/homeassistant/components/hassio/translations/af.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/bg.json b/homeassistant/components/hassio/translations/bg.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/bg.json
+++ b/homeassistant/components/hassio/translations/bg.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json
index 0cdb9318428fc1..d2e712c230d6fe 100644
--- a/homeassistant/components/hassio/translations/ca.json
+++ b/homeassistant/components/hassio/translations/ca.json
@@ -12,8 +12,8 @@
"supervisor_version": "Versi\u00f3 del Supervisor",
"supported": "Compatible",
"update_channel": "Canal d'actualitzaci\u00f3",
- "version_api": "API de versions"
+ "version_api": "Versi\u00f3 d'APIs"
}
},
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/cs.json b/homeassistant/components/hassio/translations/cs.json
index cf1a28c3cc2f7a..eb64ed58baac0d 100644
--- a/homeassistant/components/hassio/translations/cs.json
+++ b/homeassistant/components/hassio/translations/cs.json
@@ -15,5 +15,5 @@
"version_api": "Verze API"
}
},
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/cy.json b/homeassistant/components/hassio/translations/cy.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/cy.json
+++ b/homeassistant/components/hassio/translations/cy.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/da.json b/homeassistant/components/hassio/translations/da.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/da.json
+++ b/homeassistant/components/hassio/translations/da.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/de.json b/homeassistant/components/hassio/translations/de.json
index 981cb51c83ab8b..a0d02f4a7b90c1 100644
--- a/homeassistant/components/hassio/translations/de.json
+++ b/homeassistant/components/hassio/translations/de.json
@@ -1,3 +1,18 @@
{
- "title": "Hass.io"
+ "system_health": {
+ "info": {
+ "board": "Board",
+ "disk_total": "Speicherplatz gesamt",
+ "disk_used": "Speicherplatz genutzt",
+ "docker_version": "Docker-Version",
+ "host_os": "Host-Betriebssystem",
+ "installed_addons": "Installierte Add-ons",
+ "supervisor_api": "Supervisor-API",
+ "supervisor_version": "Supervisor-Version",
+ "supported": "Unterst\u00fctzt",
+ "update_channel": "Update-Channel",
+ "version_api": "Versions-API"
+ }
+ },
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/el.json b/homeassistant/components/hassio/translations/el.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/el.json
+++ b/homeassistant/components/hassio/translations/el.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/en.json b/homeassistant/components/hassio/translations/en.json
index aadcdabfb941de..16911be41109b2 100644
--- a/homeassistant/components/hassio/translations/en.json
+++ b/homeassistant/components/hassio/translations/en.json
@@ -9,7 +9,7 @@
"host_os": "Host Operating System",
"installed_addons": "Installed Add-ons",
"supervisor_api": "Supervisor API",
- "supervisor_version": "Version",
+ "supervisor_version": "Supervisor Version",
"supported": "Supported",
"update_channel": "Update Channel",
"version_api": "Version API"
diff --git a/homeassistant/components/hassio/translations/es-419.json b/homeassistant/components/hassio/translations/es-419.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/es-419.json
+++ b/homeassistant/components/hassio/translations/es-419.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/es.json b/homeassistant/components/hassio/translations/es.json
index 5faf32e515f7cc..f3bdf14c4468d2 100644
--- a/homeassistant/components/hassio/translations/es.json
+++ b/homeassistant/components/hassio/translations/es.json
@@ -15,5 +15,5 @@
"version_api": "Versi\u00f3n del API"
}
},
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/et.json b/homeassistant/components/hassio/translations/et.json
index 9e5e776013f0c9..9d3ef08afbed7a 100644
--- a/homeassistant/components/hassio/translations/et.json
+++ b/homeassistant/components/hassio/translations/et.json
@@ -15,5 +15,5 @@
"version_api": "API versioon"
}
},
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/eu.json b/homeassistant/components/hassio/translations/eu.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/eu.json
+++ b/homeassistant/components/hassio/translations/eu.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/fa.json b/homeassistant/components/hassio/translations/fa.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/fa.json
+++ b/homeassistant/components/hassio/translations/fa.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/fi.json b/homeassistant/components/hassio/translations/fi.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/fi.json
+++ b/homeassistant/components/hassio/translations/fi.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/fr.json b/homeassistant/components/hassio/translations/fr.json
index 981cb51c83ab8b..e4fe8a63bba3d6 100644
--- a/homeassistant/components/hassio/translations/fr.json
+++ b/homeassistant/components/hassio/translations/fr.json
@@ -1,3 +1,19 @@
{
- "title": "Hass.io"
+ "system_health": {
+ "info": {
+ "board": "Tableau de bord",
+ "disk_total": "Taille total du disque",
+ "disk_used": "Taille du disque utilis\u00e9",
+ "docker_version": "Version de Docker",
+ "healthy": "Sain",
+ "host_os": "Syst\u00e8me d'exploitation h\u00f4te",
+ "installed_addons": "Modules compl\u00e9mentaires install\u00e9s",
+ "supervisor_api": "API du Supervisor",
+ "supervisor_version": "Version du Supervisor",
+ "supported": "Prise en charge",
+ "update_channel": "Mise \u00e0 jour",
+ "version_api": "Version API"
+ }
+ },
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/he.json b/homeassistant/components/hassio/translations/he.json
index 981cb51c83ab8b..80c1a0c48eea7d 100644
--- a/homeassistant/components/hassio/translations/he.json
+++ b/homeassistant/components/hassio/translations/he.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/hr.json b/homeassistant/components/hassio/translations/hr.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/hr.json
+++ b/homeassistant/components/hassio/translations/hr.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/hu.json b/homeassistant/components/hassio/translations/hu.json
index 216e8d391b620e..e0fc98408d49ac 100644
--- a/homeassistant/components/hassio/translations/hu.json
+++ b/homeassistant/components/hassio/translations/hu.json
@@ -7,12 +7,12 @@
"healthy": "Eg\u00e9szs\u00e9ges",
"host_os": "Gazdag\u00e9p oper\u00e1ci\u00f3s rendszer",
"installed_addons": "Telep\u00edtett kieg\u00e9sz\u00edt\u0151k",
- "supervisor_api": "Adminisztr\u00e1tor API",
- "supervisor_version": "Adminisztr\u00e1tor verzi\u00f3",
+ "supervisor_api": "Supervisor API",
+ "supervisor_version": "Supervisor verzi\u00f3",
"supported": "T\u00e1mogatott",
"update_channel": "Friss\u00edt\u00e9si csatorna",
"version_api": "API verzi\u00f3"
}
},
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/hy.json b/homeassistant/components/hassio/translations/hy.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/hy.json
+++ b/homeassistant/components/hassio/translations/hy.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/id.json b/homeassistant/components/hassio/translations/id.json
new file mode 100644
index 00000000000000..b95ffb35d81c97
--- /dev/null
+++ b/homeassistant/components/hassio/translations/id.json
@@ -0,0 +1,19 @@
+{
+ "system_health": {
+ "info": {
+ "board": "Board",
+ "disk_total": "Total Disk",
+ "disk_used": "Disk Digunakan",
+ "docker_version": "Versi Docker",
+ "healthy": "Kesehatan",
+ "host_os": "Sistem Operasi Host",
+ "installed_addons": "Add-on yang Diinstal",
+ "supervisor_api": "API Supervisor",
+ "supervisor_version": "Versi Supervisor",
+ "supported": "Didukung",
+ "update_channel": "Kanal Pembaruan",
+ "version_api": "API Versi"
+ }
+ },
+ "title": "Home Assistant Supervisor"
+}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/is.json b/homeassistant/components/hassio/translations/is.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/is.json
+++ b/homeassistant/components/hassio/translations/is.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/it.json b/homeassistant/components/hassio/translations/it.json
index 385a0eedff24a0..86d573cba40abf 100644
--- a/homeassistant/components/hassio/translations/it.json
+++ b/homeassistant/components/hassio/translations/it.json
@@ -8,12 +8,12 @@
"healthy": "Integrit\u00e0",
"host_os": "Sistema Operativo Host",
"installed_addons": "Componenti aggiuntivi installati",
- "supervisor_api": "API Supervisore",
- "supervisor_version": "Versione Supervisore",
+ "supervisor_api": "API Supervisor",
+ "supervisor_version": "Versione Supervisor",
"supported": "Supportato",
"update_channel": "Canale di aggiornamento",
"version_api": "Versione API"
}
},
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/ja.json b/homeassistant/components/hassio/translations/ja.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/ja.json
+++ b/homeassistant/components/hassio/translations/ja.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/ko.json b/homeassistant/components/hassio/translations/ko.json
index 981cb51c83ab8b..aba9a665f703e3 100644
--- a/homeassistant/components/hassio/translations/ko.json
+++ b/homeassistant/components/hassio/translations/ko.json
@@ -1,3 +1,19 @@
{
- "title": "Hass.io"
+ "system_health": {
+ "info": {
+ "board": "\ubcf4\ub4dc \uc720\ud615",
+ "disk_total": "\ub514\uc2a4\ud06c \ucd1d \uc6a9\ub7c9",
+ "disk_used": "\ub514\uc2a4\ud06c \uc0ac\uc6a9\ub7c9",
+ "docker_version": "Docker \ubc84\uc804",
+ "healthy": "\uc2dc\uc2a4\ud15c \uc0c1\ud0dc",
+ "host_os": "\ud638\uc2a4\ud2b8 \uc6b4\uc601 \uccb4\uc81c",
+ "installed_addons": "\uc124\uce58\ub41c \uc560\ub4dc\uc628",
+ "supervisor_api": "Supervisor API",
+ "supervisor_version": "Supervisor \ubc84\uc804",
+ "supported": "\uc9c0\uc6d0 \uc5ec\ubd80",
+ "update_channel": "\uc5c5\ub370\uc774\ud2b8 \ucc44\ub110",
+ "version_api": "\ubc84\uc804 API"
+ }
+ },
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/lb.json b/homeassistant/components/hassio/translations/lb.json
index 54aae5a2c59550..c0d0f42ed94c8d 100644
--- a/homeassistant/components/hassio/translations/lb.json
+++ b/homeassistant/components/hassio/translations/lb.json
@@ -15,5 +15,5 @@
"version_api": "API Versioun"
}
},
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/lt.json b/homeassistant/components/hassio/translations/lt.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/lt.json
+++ b/homeassistant/components/hassio/translations/lt.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/lv.json b/homeassistant/components/hassio/translations/lv.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/lv.json
+++ b/homeassistant/components/hassio/translations/lv.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/nb.json b/homeassistant/components/hassio/translations/nb.json
deleted file mode 100644
index d8a4c453015156..00000000000000
--- a/homeassistant/components/hassio/translations/nb.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "title": ""
-}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/nl.json b/homeassistant/components/hassio/translations/nl.json
index fca08d49d7c3dc..7224857a10c6a0 100644
--- a/homeassistant/components/hassio/translations/nl.json
+++ b/homeassistant/components/hassio/translations/nl.json
@@ -1,6 +1,7 @@
{
"system_health": {
"info": {
+ "board": "Bord",
"disk_total": "Totale schijfruimte",
"disk_used": "Gebruikte schijfruimte",
"docker_version": "Docker versie",
@@ -14,5 +15,5 @@
"version_api": "API Versie"
}
},
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/nn.json b/homeassistant/components/hassio/translations/nn.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/nn.json
+++ b/homeassistant/components/hassio/translations/nn.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/no.json b/homeassistant/components/hassio/translations/no.json
index e652f76a12c74f..9f0c5ba89b24ae 100644
--- a/homeassistant/components/hassio/translations/no.json
+++ b/homeassistant/components/hassio/translations/no.json
@@ -15,5 +15,5 @@
"version_api": "Versjon API"
}
},
- "title": ""
+ "title": "Home Assistant veileder"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/pl.json b/homeassistant/components/hassio/translations/pl.json
index 10ee7c9d16ca3f..5266d640d7c447 100644
--- a/homeassistant/components/hassio/translations/pl.json
+++ b/homeassistant/components/hassio/translations/pl.json
@@ -15,5 +15,5 @@
"version_api": "Wersja API"
}
},
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/pt-BR.json b/homeassistant/components/hassio/translations/pt-BR.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/pt-BR.json
+++ b/homeassistant/components/hassio/translations/pt-BR.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/pt.json b/homeassistant/components/hassio/translations/pt.json
index 06083bae759951..326560409e4eac 100644
--- a/homeassistant/components/hassio/translations/pt.json
+++ b/homeassistant/components/hassio/translations/pt.json
@@ -13,5 +13,5 @@
"supported": "Suportado"
}
},
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/ro.json b/homeassistant/components/hassio/translations/ro.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/ro.json
+++ b/homeassistant/components/hassio/translations/ro.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/ru.json b/homeassistant/components/hassio/translations/ru.json
index 4f9b16621fc56f..56c3522ba3c293 100644
--- a/homeassistant/components/hassio/translations/ru.json
+++ b/homeassistant/components/hassio/translations/ru.json
@@ -15,5 +15,5 @@
"version_api": "\u0412\u0435\u0440\u0441\u0438\u044f API"
}
},
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/sk.json b/homeassistant/components/hassio/translations/sk.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/sk.json
+++ b/homeassistant/components/hassio/translations/sk.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/sl.json b/homeassistant/components/hassio/translations/sl.json
index eb2f5f7ca8b85e..cfc71ce0832e1d 100644
--- a/homeassistant/components/hassio/translations/sl.json
+++ b/homeassistant/components/hassio/translations/sl.json
@@ -14,5 +14,5 @@
"version_api": "API razli\u010dica"
}
},
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/sv.json b/homeassistant/components/hassio/translations/sv.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/sv.json
+++ b/homeassistant/components/hassio/translations/sv.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/th.json b/homeassistant/components/hassio/translations/th.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/th.json
+++ b/homeassistant/components/hassio/translations/th.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/tr.json b/homeassistant/components/hassio/translations/tr.json
index d368ac0fb3c903..06a8d3fd661797 100644
--- a/homeassistant/components/hassio/translations/tr.json
+++ b/homeassistant/components/hassio/translations/tr.json
@@ -6,8 +6,14 @@
"disk_used": "Kullan\u0131lan Disk",
"docker_version": "Docker S\u00fcr\u00fcm\u00fc",
"healthy": "Sa\u011fl\u0131kl\u0131",
- "host_os": "Ana Bilgisayar \u0130\u015fletim Sistemi"
+ "host_os": "Ana Bilgisayar \u0130\u015fletim Sistemi",
+ "installed_addons": "Y\u00fckl\u00fc Eklentiler",
+ "supervisor_api": "Supervisor API",
+ "supervisor_version": "S\u00fcperviz\u00f6r S\u00fcr\u00fcm\u00fc",
+ "supported": "Destekleniyor",
+ "update_channel": "Kanal\u0131 G\u00fcncelle",
+ "version_api": "S\u00fcr\u00fcm API"
}
},
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/uk.json b/homeassistant/components/hassio/translations/uk.json
index 981cb51c83ab8b..d25ad6e79795a3 100644
--- a/homeassistant/components/hassio/translations/uk.json
+++ b/homeassistant/components/hassio/translations/uk.json
@@ -1,3 +1,19 @@
{
- "title": "Hass.io"
+ "system_health": {
+ "info": {
+ "board": "\u041f\u043b\u0430\u0442\u0430",
+ "disk_total": "\u0414\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u043f\u0430\u043c'\u044f\u0442\u044c",
+ "disk_used": "\u041f\u0430\u043c'\u044f\u0442\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043e",
+ "docker_version": "\u0412\u0435\u0440\u0441\u0456\u044f Docker",
+ "healthy": "\u0412 \u043d\u043e\u0440\u043c\u0456",
+ "host_os": "\u041e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u0445\u043e\u0441\u0442\u0430",
+ "installed_addons": "\u0412\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0456 \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f",
+ "supervisor_api": "Supervisor API",
+ "supervisor_version": "\u0412\u0435\u0440\u0441\u0456\u044f Supervisor",
+ "supported": "\u041f\u0456\u0434\u0442\u0440\u0438\u043c\u043a\u0430",
+ "update_channel": "\u041a\u0430\u043d\u0430\u043b \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u044c",
+ "version_api": "\u0412\u0435\u0440\u0441\u0456\u044f API"
+ }
+ },
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/vi.json b/homeassistant/components/hassio/translations/vi.json
index 981cb51c83ab8b..91588c5529aa44 100644
--- a/homeassistant/components/hassio/translations/vi.json
+++ b/homeassistant/components/hassio/translations/vi.json
@@ -1,3 +1,3 @@
{
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/zh-Hans.json b/homeassistant/components/hassio/translations/zh-Hans.json
index 0d74360b8f3385..a48cbeb95a8b0f 100644
--- a/homeassistant/components/hassio/translations/zh-Hans.json
+++ b/homeassistant/components/hassio/translations/zh-Hans.json
@@ -15,5 +15,5 @@
"version_api": "API \u7248\u672c"
}
},
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/zh-Hant.json b/homeassistant/components/hassio/translations/zh-Hant.json
index 574b82358d6be3..b8b3a1e7b930ff 100644
--- a/homeassistant/components/hassio/translations/zh-Hant.json
+++ b/homeassistant/components/hassio/translations/zh-Hant.json
@@ -7,7 +7,7 @@
"docker_version": "Docker \u7248\u672c",
"healthy": "\u5065\u5eb7\u5ea6",
"host_os": "\u4e3b\u6a5f\u4f5c\u696d\u7cfb\u7d71",
- "installed_addons": "\u5df2\u5b89\u88dd Add-on",
+ "installed_addons": "\u5df2\u5b89\u88dd\u9644\u52a0\u5143\u4ef6",
"supervisor_api": "Supervisor API",
"supervisor_version": "Supervisor \u7248\u672c",
"supported": "\u652f\u63f4",
@@ -15,5 +15,5 @@
"version_api": "\u7248\u672c API"
}
},
- "title": "Hass.io"
+ "title": "Home Assistant Supervisor"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py
new file mode 100644
index 00000000000000..387aa9264891a4
--- /dev/null
+++ b/homeassistant/components/hassio/websocket_api.py
@@ -0,0 +1,111 @@
+"""Websocekt API handlers for the hassio integration."""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components import websocket_api
+from homeassistant.components.websocket_api.connection import ActiveConnection
+from homeassistant.core import HomeAssistant, callback
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.dispatcher import (
+ async_dispatcher_connect,
+ async_dispatcher_send,
+)
+
+from .const import (
+ ATTR_DATA,
+ ATTR_ENDPOINT,
+ ATTR_METHOD,
+ ATTR_TIMEOUT,
+ ATTR_WS_EVENT,
+ DOMAIN,
+ EVENT_SUPERVISOR_EVENT,
+ WS_ID,
+ WS_TYPE,
+ WS_TYPE_API,
+ WS_TYPE_EVENT,
+ WS_TYPE_SUBSCRIBE,
+)
+from .handler import HassIO
+
+SCHEMA_WEBSOCKET_EVENT = vol.Schema(
+ {vol.Required(ATTR_WS_EVENT): cv.string},
+ extra=vol.ALLOW_EXTRA,
+)
+
+_LOGGER: logging.Logger = logging.getLogger(__package__)
+
+
+@callback
+def async_load_websocket_api(hass: HomeAssistant):
+ """Set up the websocket API."""
+ websocket_api.async_register_command(hass, websocket_supervisor_event)
+ websocket_api.async_register_command(hass, websocket_supervisor_api)
+ websocket_api.async_register_command(hass, websocket_subscribe)
+
+
+@websocket_api.require_admin
+@websocket_api.async_response
+@websocket_api.websocket_command({vol.Required(WS_TYPE): WS_TYPE_SUBSCRIBE})
+async def websocket_subscribe(
+ hass: HomeAssistant, connection: ActiveConnection, msg: dict
+):
+ """Subscribe to supervisor events."""
+
+ @callback
+ def forward_messages(data):
+ """Forward events to websocket."""
+ connection.send_message(websocket_api.event_message(msg[WS_ID], data))
+
+ connection.subscriptions[msg[WS_ID]] = async_dispatcher_connect(
+ hass, EVENT_SUPERVISOR_EVENT, forward_messages
+ )
+ connection.send_message(websocket_api.result_message(msg[WS_ID]))
+
+
+@websocket_api.async_response
+@websocket_api.websocket_command(
+ {
+ vol.Required(WS_TYPE): WS_TYPE_EVENT,
+ vol.Required(ATTR_DATA): SCHEMA_WEBSOCKET_EVENT,
+ }
+)
+async def websocket_supervisor_event(
+ hass: HomeAssistant, connection: ActiveConnection, msg: dict
+):
+ """Publish events from the Supervisor."""
+ async_dispatcher_send(hass, EVENT_SUPERVISOR_EVENT, msg[ATTR_DATA])
+ connection.send_result(msg[WS_ID])
+
+
+@websocket_api.require_admin
+@websocket_api.async_response
+@websocket_api.websocket_command(
+ {
+ vol.Required(WS_TYPE): WS_TYPE_API,
+ vol.Required(ATTR_ENDPOINT): cv.string,
+ vol.Required(ATTR_METHOD): cv.string,
+ vol.Optional(ATTR_DATA): dict,
+ vol.Optional(ATTR_TIMEOUT): vol.Any(cv.Number, None),
+ }
+)
+async def websocket_supervisor_api(
+ hass: HomeAssistant, connection: ActiveConnection, msg: dict
+):
+ """Websocket handler to call Supervisor API."""
+ supervisor: HassIO = hass.data[DOMAIN]
+ result = False
+ try:
+ result = await supervisor.send_command(
+ msg[ATTR_ENDPOINT],
+ method=msg[ATTR_METHOD],
+ timeout=msg.get(ATTR_TIMEOUT, 10),
+ payload=msg.get(ATTR_DATA, {}),
+ )
+ except hass.components.hassio.HassioAPIError as err:
+ _LOGGER.error("Failed to to call %s - %s", msg[ATTR_ENDPOINT], err)
+ connection.send_error(
+ msg[WS_ID], code=websocket_api.ERR_UNKNOWN_ERROR, message=str(err)
+ )
+ else:
+ connection.send_result(msg[WS_ID], result.get(ATTR_DATA, {}))
diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py
index 0f5a9b5ebfd865..55b369c2fde8f0 100644
--- a/homeassistant/components/haveibeenpwned/sensor.py
+++ b/homeassistant/components/haveibeenpwned/sensor.py
@@ -6,7 +6,7 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_API_KEY,
@@ -15,7 +15,6 @@
HTTP_OK,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_point_in_time
from homeassistant.util import Throttle
import homeassistant.util.dt as dt_util
@@ -54,7 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(devices)
-class HaveIBeenPwnedSensor(Entity):
+class HaveIBeenPwnedSensor(SensorEntity):
"""Implementation of a HaveIBeenPwned sensor."""
def __init__(self, data, email):
@@ -80,7 +79,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the attributes of the sensor."""
val = {ATTR_ATTRIBUTION: ATTRIBUTION}
if self._email not in self._data.data:
diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py
index a1052b0440a11a..4376c7f128985f 100644
--- a/homeassistant/components/hddtemp/sensor.py
+++ b/homeassistant/components/hddtemp/sensor.py
@@ -6,7 +6,7 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_DISKS,
CONF_HOST,
@@ -16,7 +16,6 @@
TEMP_FAHRENHEIT,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -60,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(dev, True)
-class HddTempSensor(Entity):
+class HddTempSensor(SensorEntity):
"""Representation of a HDDTemp sensor."""
def __init__(self, name, disk, hddtemp):
@@ -88,7 +87,7 @@ def unit_of_measurement(self):
return self._unit
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
if self._details is not None:
return {ATTR_DEVICE: self._details[0], ATTR_MODEL: self._details[1]}
diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py
index c272ad19c8d2fb..c7dfd335c3212f 100644
--- a/homeassistant/components/hdmi_cec/__init__.py
+++ b/homeassistant/components/hdmi_cec/__init__.py
@@ -1,16 +1,12 @@
"""Support for HDMI CEC."""
from collections import defaultdict
-from functools import reduce
+from functools import partial, reduce
import logging
import multiprocessing
-from pycec.cec import CecAdapter # pylint: disable=import-error
-from pycec.commands import ( # pylint: disable=import-error
- CecCommand,
- KeyPressCommand,
- KeyReleaseCommand,
-)
-from pycec.const import ( # pylint: disable=import-error
+from pycec.cec import CecAdapter
+from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand
+from pycec.const import (
ADDR_AUDIOSYSTEM,
ADDR_BROADCAST,
ADDR_UNREGISTERED,
@@ -25,8 +21,8 @@
STATUS_STILL,
STATUS_STOP,
)
-from pycec.network import HDMINetwork, PhysicalAddress # pylint: disable=import-error
-from pycec.tcp import TcpAdapter # pylint: disable=import-error
+from pycec.network import HDMINetwork, PhysicalAddress
+from pycec.tcp import TcpAdapter
import voluptuous as vol
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER
@@ -42,9 +38,10 @@
STATE_ON,
STATE_PAUSED,
STATE_PLAYING,
+ STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
-from homeassistant.helpers import discovery
+from homeassistant.helpers import discovery, event
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -166,6 +163,9 @@
extra=vol.ALLOW_EXTRA,
)
+WATCHDOG_INTERVAL = 120
+EVENT_HDMI_CEC_UNAVAILABLE = "hdmi_cec_unavailable"
+
def pad_physical_address(addr):
"""Right-pad a physical address."""
@@ -214,6 +214,18 @@ def setup(hass: HomeAssistant, base_config):
adapter = CecAdapter(name=display_name[:12], activate_source=False)
hdmi_network = HDMINetwork(adapter, loop=loop)
+ def _adapter_watchdog(now=None):
+ _LOGGER.debug("Reached _adapter_watchdog")
+ event.async_call_later(hass, WATCHDOG_INTERVAL, _adapter_watchdog)
+ if not adapter.initialized:
+ _LOGGER.info("Adapter not initialized; Trying to restart")
+ hass.bus.fire(EVENT_HDMI_CEC_UNAVAILABLE)
+ adapter.init()
+
+ hdmi_network.set_initialized_callback(
+ partial(event.async_call_later, hass, WATCHDOG_INTERVAL, _adapter_watchdog)
+ )
+
def _volume(call):
"""Increase/decrease volume and mute/unmute system."""
mute_key_mapping = {
@@ -331,7 +343,7 @@ def _new_device(device):
def _shutdown(call):
hdmi_network.stop()
- def _start_cec(event):
+ def _start_cec(callback_event):
"""Register services and start HDMI network to watch for devices."""
hass.services.register(
DOMAIN, SERVICE_SEND_COMMAND, _tx, SERVICE_SEND_COMMAND_SCHEMA
@@ -368,6 +380,12 @@ def __init__(self, device, logical) -> None:
self._logical_address = logical
self.entity_id = "%s.%d" % (DOMAIN, self._logical_address)
+ 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.schedule_update_ha_state(False)
+
def update(self):
"""Update device status."""
device = self._device
@@ -387,6 +405,9 @@ def update(self):
async def async_added_to_hass(self):
"""Register HDMI callbacks after initialization."""
self._device.set_update_callback(self._update)
+ self.hass.bus.async_listen(
+ EVENT_HDMI_CEC_UNAVAILABLE, self._hdmi_cec_unavailable
+ )
def _update(self, device=None):
"""Device status changed, schedule an update."""
@@ -454,7 +475,7 @@ def icon(self):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
state_attr = {}
if self.vendor_id is not None:
diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json
index 4c307582281157..4f6975f52df83f 100644
--- a/homeassistant/components/hdmi_cec/manifest.json
+++ b/homeassistant/components/hdmi_cec/manifest.json
@@ -2,6 +2,6 @@
"domain": "hdmi_cec",
"name": "HDMI-CEC",
"documentation": "https://www.home-assistant.io/integrations/hdmi_cec",
- "requirements": ["pyCEC==0.4.14"],
- "codeowners": ["@newAM"]
+ "requirements": ["pyCEC==0.5.1"],
+ "codeowners": []
}
diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py
index f81ee20afe33ad..c3cab6a8f981e1 100644
--- a/homeassistant/components/hdmi_cec/media_player.py
+++ b/homeassistant/components/hdmi_cec/media_player.py
@@ -1,12 +1,8 @@
"""Support for HDMI CEC devices as media players."""
import logging
-from pycec.commands import ( # pylint: disable=import-error
- CecCommand,
- KeyPressCommand,
- KeyReleaseCommand,
-)
-from pycec.const import ( # pylint: disable=import-error
+from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand
+from pycec.const import (
KEY_BACKWARD,
KEY_FORWARD,
KEY_MUTE_TOGGLE,
diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py
index b3f3363818c3be..c8e1db0a10f7c7 100644
--- a/homeassistant/components/heatmiser/climate.py
+++ b/homeassistant/components/heatmiser/climate.py
@@ -1,6 +1,7 @@
"""Support for the PRT Heatmiser themostats using the V3 protocol."""
+from __future__ import annotations
+
import logging
-from typing import List
from heatmiserV3 import connection, heatmiser
import voluptuous as vol
@@ -103,7 +104,7 @@ def hvac_mode(self) -> str:
return self._hvac_mode
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes.
Need to be a subset of HVAC_MODES.
diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py
index 11020d1166e1d0..a71d0d2de50a03 100644
--- a/homeassistant/components/heos/__init__.py
+++ b/homeassistant/components/heos/__init__.py
@@ -1,8 +1,9 @@
"""Denon HEOS Media Player."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
import logging
-from typing import Dict
from pyheos import Heos, HeosError, const as heos_const
import voluptuous as vol
@@ -191,7 +192,7 @@ async def _heos_event(self, event):
# Update players
self._hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_HEOS_UPDATED)
- def update_ids(self, mapped_ids: Dict[int, int]):
+ def update_ids(self, mapped_ids: dict[int, int]):
"""Update the IDs in the device and entity registry."""
# mapped_ids contains the mapped IDs (new:old)
for new_id, old_id in mapped_ids.items():
diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py
index 15d3c4573dbab7..6e271bf60cd995 100644
--- a/homeassistant/components/heos/media_player.py
+++ b/homeassistant/components/heos/media_player.py
@@ -262,7 +262,7 @@ def device_info(self) -> dict:
}
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Get additional attribute about the state."""
return {
"media_album_id": self._player.now_playing_media.album_id,
diff --git a/homeassistant/components/heos/translations/de.json b/homeassistant/components/heos/translations/de.json
index 92ab6c1c8ff145..ba8a5318951226 100644
--- a/homeassistant/components/heos/translations/de.json
+++ b/homeassistant/components/heos/translations/de.json
@@ -1,7 +1,10 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"step": {
"user": {
diff --git a/homeassistant/components/heos/translations/hu.json b/homeassistant/components/heos/translations/hu.json
index cf688d6fdeb63f..2fbce1993cd031 100644
--- a/homeassistant/components/heos/translations/hu.json
+++ b/homeassistant/components/heos/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
diff --git a/homeassistant/components/heos/translations/id.json b/homeassistant/components/heos/translations/id.json
new file mode 100644
index 00000000000000..b7c57b46f66d4e
--- /dev/null
+++ b/homeassistant/components/heos/translations/id.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Masukkan nama host atau alamat IP perangkat Heos (sebaiknya yang terhubung melalui kabel ke jaringan).",
+ "title": "Hubungkan ke Heos"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/heos/translations/ko.json b/homeassistant/components/heos/translations/ko.json
index fc20a77d7b88f8..5e4057ae482843 100644
--- a/homeassistant/components/heos/translations/ko.json
+++ b/homeassistant/components/heos/translations/ko.json
@@ -1,12 +1,18 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
"step": {
"user": {
"data": {
"host": "\ud638\uc2a4\ud2b8"
},
"description": "Heos \uae30\uae30\uc758 \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. (\uc720\uc120 \ub124\ud2b8\uc6cc\ud06c\ub85c \uc5f0\uacb0\ud558\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4)",
- "title": "Heos \uc5d0 \uc5f0\uacb0\ud558\uae30"
+ "title": "Heos\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/heos/translations/tr.json b/homeassistant/components/heos/translations/tr.json
new file mode 100644
index 00000000000000..4f1ad7759054c4
--- /dev/null
+++ b/homeassistant/components/heos/translations/tr.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/heos/translations/uk.json b/homeassistant/components/heos/translations/uk.json
new file mode 100644
index 00000000000000..c0a5fdf04bf94f
--- /dev/null
+++ b/homeassistant/components/heos/translations/uk.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043c'\u044f \u0445\u043e\u0441\u0442\u0430 \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e HEOS (\u0431\u0430\u0436\u0430\u043d\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0434\u043e \u043c\u0435\u0440\u0435\u0436\u0456 \u0447\u0435\u0440\u0435\u0437 \u043a\u0430\u0431\u0435\u043b\u044c).",
+ "title": "HEOS"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py
index afc6534d0c6b7e..4b8f765d08a6aa 100644
--- a/homeassistant/components/here_travel_time/sensor.py
+++ b/homeassistant/components/here_travel_time/sensor.py
@@ -1,17 +1,20 @@
"""Support for HERE travel time sensors."""
+from __future__ import annotations
+
from datetime import datetime, timedelta
import logging
-from typing import Callable, Dict, Optional, Union
+from typing import Callable
import herepy
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_LATITUDE,
ATTR_LONGITUDE,
ATTR_MODE,
+ CONF_API_KEY,
CONF_MODE,
CONF_NAME,
CONF_UNIT_SYSTEM,
@@ -23,7 +26,6 @@
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers import location
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import DiscoveryInfoType
import homeassistant.util.dt as dt
@@ -35,7 +37,6 @@
CONF_ORIGIN_LATITUDE = "origin_latitude"
CONF_ORIGIN_LONGITUDE = "origin_longitude"
CONF_ORIGIN_ENTITY_ID = "origin_entity_id"
-CONF_API_KEY = "api_key"
CONF_TRAFFIC_MODE = "traffic_mode"
CONF_ROUTE_MODE = "route_mode"
CONF_ARRIVAL = "arrival"
@@ -143,12 +144,11 @@
async def async_setup_platform(
hass: HomeAssistant,
- config: Dict[str, Union[str, bool]],
+ config: dict[str, str | bool],
async_add_entities: Callable,
- discovery_info: Optional[DiscoveryInfoType] = None,
+ discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the HERE travel time platform."""
-
api_key = config[CONF_API_KEY]
here_client = herepy.RoutingApi(api_key)
@@ -214,7 +214,7 @@ def _are_valid_client_credentials(here_client: herepy.RoutingApi) -> bool:
return True
-class HERETravelTimeSensor(Entity):
+class HERETravelTimeSensor(SensorEntity):
"""Representation of a HERE travel time sensor."""
def __init__(
@@ -224,7 +224,7 @@ def __init__(
destination: str,
origin_entity_id: str,
destination_entity_id: str,
- here_data: "HERETravelTimeData",
+ here_data: HERETravelTimeData,
) -> None:
"""Initialize the sensor."""
self._name = name
@@ -256,11 +256,10 @@ def delayed_sensor_update(event):
)
@property
- def state(self) -> Optional[str]:
+ def state(self) -> str | None:
"""Return the state of the sensor."""
- if self._here_data.traffic_mode:
- if self._here_data.traffic_time is not None:
- return str(round(self._here_data.traffic_time / 60))
+ if self._here_data.traffic_mode and self._here_data.traffic_time is not None:
+ return str(round(self._here_data.traffic_time / 60))
if self._here_data.base_time is not None:
return str(round(self._here_data.base_time / 60))
@@ -272,9 +271,9 @@ def name(self) -> str:
return self._name
@property
- def device_state_attributes(
+ def extra_state_attributes(
self,
- ) -> Optional[Dict[str, Union[None, float, str, bool]]]:
+ ) -> dict[str, None | float | str | bool] | None:
"""Return the state attributes."""
if self._here_data.base_time is None:
return None
@@ -325,7 +324,7 @@ async def async_update(self) -> None:
await self.hass.async_add_executor_job(self._here_data.update)
- async def _get_location_from_entity(self, entity_id: str) -> Optional[str]:
+ async def _get_location_from_entity(self, entity_id: str) -> str | None:
"""Get the location from the entity state or attributes."""
entity = self.hass.states.get(entity_id)
@@ -458,11 +457,9 @@ def update(self) -> None:
_LOGGER.debug("Raw response is: %s", response.response)
- # pylint: disable=no-member
source_attribution = response.response.get("sourceAttribution")
if source_attribution is not None:
self.attribution = self._build_hass_attribution(source_attribution)
- # pylint: disable=no-member
route = response.response["route"]
summary = route[0]["summary"]
waypoint = route[0]["waypoint"]
@@ -478,13 +475,12 @@ def update(self) -> None:
else:
# Convert to kilometers
self.distance = distance / 1000
- # pylint: disable=no-member
self.route = response.route_short
self.origin_name = waypoint[0]["mappedRoadName"]
self.destination_name = waypoint[1]["mappedRoadName"]
@staticmethod
- def _build_hass_attribution(source_attribution: Dict) -> Optional[str]:
+ def _build_hass_attribution(source_attribution: dict) -> str | None:
"""Build a hass frontend ready string out of the sourceAttribution."""
suppliers = source_attribution.get("supplier")
if suppliers is not None:
diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py
index 359f966d119174..0d57278c826f5d 100644
--- a/homeassistant/components/hikvision/binary_sensor.py
+++ b/homeassistant/components/hikvision/binary_sensor.py
@@ -14,6 +14,7 @@
from homeassistant.const import (
ATTR_LAST_TRIP_TIME,
CONF_CUSTOMIZE,
+ CONF_DELAY,
CONF_HOST,
CONF_NAME,
CONF_PASSWORD,
@@ -30,7 +31,6 @@
_LOGGER = logging.getLogger(__name__)
CONF_IGNORED = "ignored"
-CONF_DELAY = "delay"
DEFAULT_PORT = 80
DEFAULT_IGNORED = False
@@ -139,7 +139,6 @@ class HikvisionData:
def __init__(self, hass, url, port, name, username, password):
"""Initialize the data object."""
-
self._url = url
self._port = port
self._name = name
@@ -253,7 +252,7 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attr = {ATTR_LAST_TRIP_TIME: self._sensor_last_update()}
diff --git a/homeassistant/components/hisense_aehw4a1/translations/de.json b/homeassistant/components/hisense_aehw4a1/translations/de.json
index d5f4f4297401c7..7c0bd96a9c9cee 100644
--- a/homeassistant/components/hisense_aehw4a1/translations/de.json
+++ b/homeassistant/components/hisense_aehw4a1/translations/de.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"no_devices_found": "Es wurden keine Hisense AEH-W4A1-Ger\u00e4te im Netzwerk gefunden.",
- "single_instance_allowed": "Es ist nur eine einzige Konfiguration von Hisense AEH-W4A1 m\u00f6glich."
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/hisense_aehw4a1/translations/hu.json b/homeassistant/components/hisense_aehw4a1/translations/hu.json
index 0b21d7c4c3207e..c71972772ebc89 100644
--- a/homeassistant/components/hisense_aehw4a1/translations/hu.json
+++ b/homeassistant/components/hisense_aehw4a1/translations/hu.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "A h\u00e1l\u00f3zaton nem tal\u00e1lhat\u00f3 Hisense AEH-W4A1 eszk\u00f6z.",
- "single_instance_allowed": "Csak egy konfigur\u00e1ci\u00f3 lehet Hisense AEH-W4A1 eset\u00e9n."
+ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/hisense_aehw4a1/translations/id.json b/homeassistant/components/hisense_aehw4a1/translations/id.json
new file mode 100644
index 00000000000000..eb31eb979c4ce9
--- /dev/null
+++ b/homeassistant/components/hisense_aehw4a1/translations/id.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "step": {
+ "confirm": {
+ "description": "Ingin menyiapkan Hisense AEH-W4A1?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hisense_aehw4a1/translations/ko.json b/homeassistant/components/hisense_aehw4a1/translations/ko.json
index 27d0ff88f6a029..dadb6cc79482c7 100644
--- a/homeassistant/components/hisense_aehw4a1/translations/ko.json
+++ b/homeassistant/components/hisense_aehw4a1/translations/ko.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Hisense AEH-W4A1 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
- "single_instance_allowed": "\ud558\ub098\uc758 Hisense AEH-W4A1 \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/hisense_aehw4a1/translations/nl.json b/homeassistant/components/hisense_aehw4a1/translations/nl.json
index 14f2445f63e3b0..c1f353558b6aba 100644
--- a/homeassistant/components/hisense_aehw4a1/translations/nl.json
+++ b/homeassistant/components/hisense_aehw4a1/translations/nl.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Geen Hisense AEH-W4A1-apparaten gevonden op het netwerk.",
- "single_instance_allowed": "Slechts een enkele configuratie van Hisense AEH-W4A1 is mogelijk."
+ "no_devices_found": "Geen apparaten gevonden op het netwerk",
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/hisense_aehw4a1/translations/tr.json b/homeassistant/components/hisense_aehw4a1/translations/tr.json
new file mode 100644
index 00000000000000..a893a653a78c9a
--- /dev/null
+++ b/homeassistant/components/hisense_aehw4a1/translations/tr.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "step": {
+ "confirm": {
+ "description": "Hisense AEH-W4A1'i kurmak istiyor musunuz?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hisense_aehw4a1/translations/uk.json b/homeassistant/components/hisense_aehw4a1/translations/uk.json
new file mode 100644
index 00000000000000..900882513d5128
--- /dev/null
+++ b/homeassistant/components/hisense_aehw4a1/translations/uk.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Hisense AEH-W4A1?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py
index 1e22e45a89249d..09f459b32d6b22 100644
--- a/homeassistant/components/history/__init__.py
+++ b/homeassistant/components/history/__init__.py
@@ -1,11 +1,13 @@
"""Provide pre-made queries on top of the recorder component."""
+from __future__ import annotations
+
from collections import defaultdict
from datetime import datetime as dt, timedelta
from itertools import groupby
import json
import logging
import time
-from typing import Iterable, Optional, cast
+from typing import Iterable, cast
from aiohttp import web
from sqlalchemy import and_, bindparam, func, not_, or_
@@ -27,13 +29,12 @@
CONF_INCLUDE,
HTTP_BAD_REQUEST,
)
-from homeassistant.core import Context, State, split_entity_id
+from homeassistant.core import Context, HomeAssistant, State, split_entity_id
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import (
CONF_ENTITY_GLOBS,
INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA,
)
-from homeassistant.helpers.typing import HomeAssistantType
import homeassistant.util.dt as dt_util
# mypy: allow-untyped-defs, no-check-untyped-defs
@@ -71,6 +72,7 @@
NEED_ATTRIBUTE_DOMAINS = {
"climate",
"humidifier",
+ "input_datetime",
"thermostat",
"water_heater",
}
@@ -461,7 +463,7 @@ def __init__(self, filters, use_include_order):
self.use_include_order = use_include_order
async def get(
- self, request: web.Request, datetime: Optional[str] = None
+ self, request: web.Request, datetime: str | None = None
) -> web.Response:
"""Return history over a period of time."""
datetime_ = None
@@ -670,7 +672,7 @@ def _glob_to_like(glob_str):
def _entities_may_have_state_changes_after(
- hass: HomeAssistantType, entity_ids: Iterable, start_time: dt
+ hass: HomeAssistant, entity_ids: Iterable, start_time: dt
) -> bool:
"""Check the state machine to see if entities have changed since start time."""
for entity_id in entity_ids:
@@ -713,7 +715,7 @@ def attributes(self):
self._attributes = json.loads(self._row.attributes)
except ValueError:
# When json.loads fails
- _LOGGER.exception("Error converting row to state: %s", self)
+ _LOGGER.exception("Error converting row to state: %s", self._row)
self._attributes = {}
return self._attributes
diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py
index 6778e893f6f330..b8d3dc39187abe 100644
--- a/homeassistant/components/history_stats/sensor.py
+++ b/homeassistant/components/history_stats/sensor.py
@@ -6,7 +6,7 @@
import voluptuous as vol
from homeassistant.components import history
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_ENTITY_ID,
CONF_NAME,
@@ -19,7 +19,6 @@
from homeassistant.core import CoreState, callback
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.reload import setup_reload_service
import homeassistant.util.dt as dt_util
@@ -102,7 +101,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return True
-class HistoryStatsSensor(Entity):
+class HistoryStatsSensor(SensorEntity):
"""Representation of a HistoryStats sensor."""
def __init__(
@@ -174,7 +173,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
if self.value is None:
return {}
diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py
index ace6540fe71b0c..cbd6b7eeff87e8 100644
--- a/homeassistant/components/hitron_coda/device_tracker.py
+++ b/homeassistant/components/hitron_coda/device_tracker.py
@@ -74,14 +74,13 @@ def scan_devices(self):
def get_device_name(self, device):
"""Return the name of the device with the given MAC address."""
- name = next(
+ return next(
(result.name for result in self.last_results if result.mac == device), None
)
- return name
def _login(self):
"""Log in to the router. This is required for subsequent api calls."""
- _LOGGER.info("Logging in to CODA...")
+ _LOGGER.info("Logging in to CODA")
try:
data = [("user", self._username), (self._type, self._password)]
@@ -101,12 +100,11 @@ def _login(self):
def _update_info(self):
"""Get ARP from router."""
- _LOGGER.info("Fetching...")
+ _LOGGER.info("Fetching")
- if self._userid is None:
- if not self._login():
- _LOGGER.error("Could not obtain a user ID from the router")
- return False
+ if self._userid is None and not self._login():
+ _LOGGER.error("Could not obtain a user ID from the router")
+ return False
last_results = []
# doing a request
diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py
index 98d625cbb1d264..040ef7b4674394 100644
--- a/homeassistant/components/hive/__init__.py
+++ b/homeassistant/components/hive/__init__.py
@@ -1,39 +1,26 @@
"""Support for the Hive devices and services."""
+import asyncio
from functools import wraps
import logging
-from pyhiveapi import Pyhiveapi
+from aiohttp.web_exceptions import HTTPException
+from apyhiveapi import Hive
+from apyhiveapi.helper.hive_exceptions import HiveReauthRequired
import voluptuous as vol
-from homeassistant.const import (
- ATTR_ENTITY_ID,
- ATTR_TEMPERATURE,
- CONF_PASSWORD,
- CONF_SCAN_INTERVAL,
- CONF_USERNAME,
+from homeassistant import config_entries
+from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import aiohttp_client, config_validation as cv
+from homeassistant.helpers.dispatcher import (
+ async_dispatcher_connect,
+ async_dispatcher_send,
)
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.discovery import load_platform
-from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send
from homeassistant.helpers.entity import Entity
-_LOGGER = logging.getLogger(__name__)
+from .const import DOMAIN, PLATFORM_LOOKUP, PLATFORMS
-DOMAIN = "hive"
-DATA_HIVE = "data_hive"
-SERVICES = ["Heating", "HotWater", "TRV"]
-SERVICE_BOOST_HOT_WATER = "boost_hot_water"
-SERVICE_BOOST_HEATING = "boost_heating"
-ATTR_TIME_PERIOD = "time_period"
-ATTR_MODE = "on_off"
-DEVICETYPES = {
- "binary_sensor": "device_list_binary_sensor",
- "climate": "device_list_climate",
- "water_heater": "device_list_water_heater",
- "light": "device_list_light",
- "switch": "device_list_plug",
- "sensor": "device_list_sensor",
-}
+_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema(
{
@@ -48,124 +35,95 @@
extra=vol.ALLOW_EXTRA,
)
-BOOST_HEATING_SCHEMA = vol.Schema(
- {
- vol.Required(ATTR_ENTITY_ID): cv.entity_id,
- vol.Required(ATTR_TIME_PERIOD): vol.All(
- cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds() // 60
- ),
- vol.Optional(ATTR_TEMPERATURE, default="25.0"): vol.Coerce(float),
- }
-)
-
-BOOST_HOT_WATER_SCHEMA = vol.Schema(
- {
- vol.Required(ATTR_ENTITY_ID): cv.entity_id,
- vol.Optional(ATTR_TIME_PERIOD, default="00:30:00"): vol.All(
- cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds() // 60
- ),
- vol.Required(ATTR_MODE): cv.string,
- }
-)
-
-
-class HiveSession:
- """Initiate Hive Session Class."""
-
- entity_lookup = {}
- core = None
- heating = None
- hotwater = None
- light = None
- sensor = None
- switch = None
- weather = None
- attributes = None
- trv = None
+async def async_setup(hass, config):
+ """Hive configuration setup."""
+ hass.data[DOMAIN] = {}
-def setup(hass, config):
- """Set up the Hive Component."""
+ if DOMAIN not in config:
+ return True
- def heating_boost(service):
- """Handle the service call."""
- node_id = HiveSession.entity_lookup.get(service.data[ATTR_ENTITY_ID])
- if not node_id:
- # log or raise error
- _LOGGER.error("Cannot boost entity id entered")
- return
+ conf = config[DOMAIN]
- minutes = service.data[ATTR_TIME_PERIOD]
- temperature = service.data[ATTR_TEMPERATURE]
-
- session.heating.turn_boost_on(node_id, minutes, temperature)
+ if not hass.config_entries.async_entries(DOMAIN):
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={
+ CONF_USERNAME: conf[CONF_USERNAME],
+ CONF_PASSWORD: conf[CONF_PASSWORD],
+ },
+ )
+ )
+ return True
- def hot_water_boost(service):
- """Handle the service call."""
- node_id = HiveSession.entity_lookup.get(service.data[ATTR_ENTITY_ID])
- if not node_id:
- # log or raise error
- _LOGGER.error("Cannot boost entity id entered")
- return
- minutes = service.data[ATTR_TIME_PERIOD]
- mode = service.data[ATTR_MODE]
- if mode == "on":
- session.hotwater.turn_boost_on(node_id, minutes)
- elif mode == "off":
- session.hotwater.turn_boost_off(node_id)
+async def async_setup_entry(hass, entry):
+ """Set up Hive from a config entry."""
+
+ websession = aiohttp_client.async_get_clientsession(hass)
+ hive = Hive(websession)
+ hive_config = dict(entry.data)
+
+ hive_config["options"] = {}
+ hive_config["options"].update(
+ {CONF_SCAN_INTERVAL: dict(entry.options).get(CONF_SCAN_INTERVAL, 120)}
+ )
+ hass.data[DOMAIN][entry.entry_id] = hive
+
+ try:
+ devices = await hive.session.startSession(hive_config)
+ except HTTPException as error:
+ _LOGGER.error("Could not connect to the internet: %s", error)
+ raise ConfigEntryNotReady() from error
+ except HiveReauthRequired:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={
+ "source": config_entries.SOURCE_REAUTH,
+ "unique_id": entry.unique_id,
+ },
+ data=entry.data,
+ )
+ )
+ return False
- session = HiveSession()
- session.core = Pyhiveapi()
+ for ha_type, hive_type in PLATFORM_LOOKUP.items():
+ device_list = devices.get(hive_type)
+ if device_list:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, ha_type)
+ )
- username = config[DOMAIN][CONF_USERNAME]
- password = config[DOMAIN][CONF_PASSWORD]
- update_interval = config[DOMAIN][CONF_SCAN_INTERVAL]
+ return True
- devices = session.core.initialise_api(username, password, update_interval)
- if devices is None:
- _LOGGER.error("Hive API initialization failed")
- return False
+async def async_unload_entry(hass, entry):
+ """Unload a config entry."""
- session.sensor = Pyhiveapi.Sensor()
- session.heating = Pyhiveapi.Heating()
- session.hotwater = Pyhiveapi.Hotwater()
- session.light = Pyhiveapi.Light()
- session.switch = Pyhiveapi.Switch()
- session.weather = Pyhiveapi.Weather()
- session.attributes = Pyhiveapi.Attributes()
- hass.data[DATA_HIVE] = session
-
- for ha_type in DEVICETYPES:
- devicelist = devices.get(DEVICETYPES[ha_type])
- if devicelist:
- load_platform(hass, ha_type, DOMAIN, devicelist, config)
- if ha_type == "climate":
- hass.services.register(
- DOMAIN,
- SERVICE_BOOST_HEATING,
- heating_boost,
- schema=BOOST_HEATING_SCHEMA,
- )
- if ha_type == "water_heater":
- hass.services.register(
- DOMAIN,
- SERVICE_BOOST_HOT_WATER,
- hot_water_boost,
- schema=BOOST_HOT_WATER_SCHEMA,
- )
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, component)
+ for component in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
- return True
+ return unload_ok
def refresh_system(func):
"""Force update all entities after state change."""
@wraps(func)
- def wrapper(self, *args, **kwargs):
- func(self, *args, **kwargs)
- dispatcher_send(self.hass, DOMAIN)
+ async def wrapper(self, *args, **kwargs):
+ await func(self, *args, **kwargs)
+ async_dispatcher_send(self.hass, DOMAIN)
return wrapper
@@ -173,20 +131,15 @@ def wrapper(self, *args, **kwargs):
class HiveEntity(Entity):
"""Initiate Hive Base Class."""
- def __init__(self, session, hive_device):
+ def __init__(self, hive, hive_device):
"""Initialize the instance."""
- self.node_id = hive_device["Hive_NodeID"]
- self.node_name = hive_device["Hive_NodeName"]
- self.device_type = hive_device["HA_DeviceType"]
- self.node_device_type = hive_device["Hive_DeviceType"]
- self.session = session
+ self.hive = hive
+ self.device = hive_device
self.attributes = {}
- self._unique_id = f"{self.node_id}-{self.device_type}"
+ self._unique_id = f'{self.device["hiveID"]}-{self.device["hiveType"]}'
async def async_added_to_hass(self):
"""When entity is added to Home Assistant."""
self.async_on_remove(
async_dispatcher_connect(self.hass, DOMAIN, self.async_write_ha_state)
)
- if self.device_type in SERVICES:
- self.session.entity_lookup[self.entity_id] = self.node_id
diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py
index 120148a8f813f4..d5f1ca53afde60 100644
--- a/homeassistant/components/hive/binary_sensor.py
+++ b/homeassistant/components/hive/binary_sensor.py
@@ -1,28 +1,40 @@
"""Support for the Hive binary sensors."""
+from datetime import timedelta
+
from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
DEVICE_CLASS_MOTION,
DEVICE_CLASS_OPENING,
+ DEVICE_CLASS_SMOKE,
+ DEVICE_CLASS_SOUND,
BinarySensorEntity,
)
-from . import DATA_HIVE, DOMAIN, HiveEntity
+from . import HiveEntity
+from .const import ATTR_MODE, DOMAIN
-DEVICETYPE_DEVICE_CLASS = {
- "motionsensor": DEVICE_CLASS_MOTION,
+DEVICETYPE = {
"contactsensor": DEVICE_CLASS_OPENING,
+ "motionsensor": DEVICE_CLASS_MOTION,
+ "Connectivity": DEVICE_CLASS_CONNECTIVITY,
+ "SMOKE_CO": DEVICE_CLASS_SMOKE,
+ "DOG_BARK": DEVICE_CLASS_SOUND,
+ "GLASS_BREAK": DEVICE_CLASS_SOUND,
}
+PARALLEL_UPDATES = 0
+SCAN_INTERVAL = timedelta(seconds=15)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up Hive sensor devices."""
- if discovery_info is None:
- return
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up Hive thermostat based on a config entry."""
- session = hass.data.get(DATA_HIVE)
- devs = []
- for dev in discovery_info:
- devs.append(HiveBinarySensorEntity(session, dev))
- add_entities(devs)
+ hive = hass.data[DOMAIN][entry.entry_id]
+ devices = hive.session.deviceList.get("binary_sensor")
+ entities = []
+ if devices:
+ for dev in devices:
+ entities.append(HiveBinarySensorEntity(hive, dev))
+ async_add_entities(entities, True)
class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity):
@@ -36,29 +48,46 @@ def unique_id(self):
@property
def device_info(self):
"""Return device information."""
- return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name}
+ return {
+ "identifiers": {(DOMAIN, self.device["device_id"])},
+ "name": self.device["device_name"],
+ "model": self.device["deviceData"]["model"],
+ "manufacturer": self.device["deviceData"]["manufacturer"],
+ "sw_version": self.device["deviceData"]["version"],
+ "via_device": (DOMAIN, self.device["parentDevice"]),
+ }
@property
def device_class(self):
"""Return the class of this sensor."""
- return DEVICETYPE_DEVICE_CLASS.get(self.node_device_type)
+ return DEVICETYPE.get(self.device["hiveType"])
@property
def name(self):
"""Return the name of the binary sensor."""
- return self.node_name
+ return self.device["haName"]
+
+ @property
+ def available(self):
+ """Return if the device is available."""
+ if self.device["hiveType"] != "Connectivity":
+ return self.device["deviceData"]["online"]
+ return True
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Show Device Attributes."""
- return self.attributes
+ return {
+ ATTR_MODE: self.attributes.get(ATTR_MODE),
+ }
@property
def is_on(self):
"""Return true if the binary sensor is on."""
- return self.session.sensor.get_state(self.node_id, self.node_device_type)
+ return self.device["status"]["state"]
- def update(self):
+ async def async_update(self):
"""Update all Node data from Hive."""
- self.session.core.update_data(self.node_id)
- self.attributes = self.session.attributes.state_attributes(self.node_id)
+ await self.hive.session.updateData(self.device)
+ self.device = await self.hive.sensor.getSensor(self.device)
+ self.attributes = self.device.get("attributes", {})
diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py
index 33c8fed4ecad41..d5b60fa4b952f1 100644
--- a/homeassistant/components/hive/climate.py
+++ b/homeassistant/components/hive/climate.py
@@ -1,4 +1,9 @@
"""Support for the Hive climate devices."""
+from datetime import timedelta
+import logging
+
+import voluptuous as vol
+
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
CURRENT_HVAC_HEAT,
@@ -12,9 +17,16 @@
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
)
-from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
-
-from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system
+from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.helpers import config_validation as cv, entity_platform
+
+from . import HiveEntity, refresh_system
+from .const import (
+ ATTR_TIME_PERIOD,
+ DOMAIN,
+ SERVICE_BOOST_HEATING_OFF,
+ SERVICE_BOOST_HEATING_ON,
+)
HIVE_TO_HASS_STATE = {
"SCHEDULE": HVAC_MODE_AUTO,
@@ -34,21 +46,60 @@
True: CURRENT_HVAC_HEAT,
}
+TEMP_UNIT = {"C": TEMP_CELSIUS, "F": TEMP_FAHRENHEIT}
+
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
SUPPORT_HVAC = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF]
SUPPORT_PRESET = [PRESET_NONE, PRESET_BOOST]
-
-
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up Hive climate devices."""
- if discovery_info is None:
- return
-
- session = hass.data.get(DATA_HIVE)
- devs = []
- for dev in discovery_info:
- devs.append(HiveClimateEntity(session, dev))
- add_entities(devs)
+PARALLEL_UPDATES = 0
+SCAN_INTERVAL = timedelta(seconds=15)
+_LOGGER = logging.getLogger()
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up Hive thermostat based on a config entry."""
+
+ hive = hass.data[DOMAIN][entry.entry_id]
+ devices = hive.session.deviceList.get("climate")
+ entities = []
+ if devices:
+ for dev in devices:
+ entities.append(HiveClimateEntity(hive, dev))
+ async_add_entities(entities, True)
+
+ platform = entity_platform.current_platform.get()
+
+ platform.async_register_entity_service(
+ "boost_heating",
+ {
+ vol.Required(ATTR_TIME_PERIOD): vol.All(
+ cv.time_period,
+ cv.positive_timedelta,
+ lambda td: td.total_seconds() // 60,
+ ),
+ vol.Optional(ATTR_TEMPERATURE, default="25.0"): vol.Coerce(float),
+ },
+ "async_heating_boost",
+ )
+
+ platform.async_register_entity_service(
+ SERVICE_BOOST_HEATING_ON,
+ {
+ vol.Required(ATTR_TIME_PERIOD): vol.All(
+ cv.time_period,
+ cv.positive_timedelta,
+ lambda td: td.total_seconds() // 60,
+ ),
+ vol.Optional(ATTR_TEMPERATURE, default="25.0"): vol.Coerce(float),
+ },
+ "async_heating_boost_on",
+ )
+
+ platform.async_register_entity_service(
+ SERVICE_BOOST_HEATING_OFF,
+ {},
+ "async_heating_boost_off",
+ )
class HiveClimateEntity(HiveEntity, ClimateEntity):
@@ -57,7 +108,8 @@ class HiveClimateEntity(HiveEntity, ClimateEntity):
def __init__(self, hive_session, hive_device):
"""Initialize the Climate device."""
super().__init__(hive_session, hive_device)
- self.thermostat_node_id = hive_device["Thermostat_NodeID"]
+ self.thermostat_node_id = hive_device["device_id"]
+ self.temperature_type = TEMP_UNIT.get(hive_device["temperatureunit"])
@property
def unique_id(self):
@@ -67,7 +119,14 @@ def unique_id(self):
@property
def device_info(self):
"""Return device information."""
- return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name}
+ return {
+ "identifiers": {(DOMAIN, self.device["device_id"])},
+ "name": self.device["device_name"],
+ "model": self.device["deviceData"]["model"],
+ "manufacturer": self.device["deviceData"]["manufacturer"],
+ "sw_version": self.device["deviceData"]["version"],
+ "via_device": (DOMAIN, self.device["parentDevice"]),
+ }
@property
def supported_features(self):
@@ -77,19 +136,12 @@ def supported_features(self):
@property
def name(self):
"""Return the name of the Climate device."""
- friendly_name = "Heating"
- if self.node_name is not None:
- if self.device_type == "TRV":
- friendly_name = self.node_name
- else:
- friendly_name = f"{self.node_name} {friendly_name}"
-
- return friendly_name
+ return self.device["haName"]
@property
- def device_state_attributes(self):
- """Show Device Attributes."""
- return self.attributes
+ def available(self):
+ """Return if the device is available."""
+ return self.device["deviceData"]["online"]
@property
def hvac_modes(self):
@@ -105,47 +157,42 @@ def hvac_mode(self):
Need to be one of HVAC_MODE_*.
"""
- return HIVE_TO_HASS_STATE[self.session.heating.get_mode(self.node_id)]
+ return HIVE_TO_HASS_STATE[self.device["status"]["mode"]]
@property
def hvac_action(self):
"""Return current HVAC action."""
- return HIVE_TO_HASS_HVAC_ACTION[
- self.session.heating.operational_status(self.node_id, self.device_type)
- ]
+ return HIVE_TO_HASS_HVAC_ACTION[self.device["status"]["action"]]
@property
def temperature_unit(self):
"""Return the unit of measurement."""
- return TEMP_CELSIUS
+ return self.temperature_type
@property
def current_temperature(self):
"""Return the current temperature."""
- return self.session.heating.current_temperature(self.node_id)
+ return self.device["status"]["current_temperature"]
@property
def target_temperature(self):
"""Return the target temperature."""
- return self.session.heating.get_target_temperature(self.node_id)
+ return self.device["status"]["target_temperature"]
@property
def min_temp(self):
"""Return minimum temperature."""
- return self.session.heating.min_temperature(self.node_id)
+ return self.device["min_temp"]
@property
def max_temp(self):
"""Return the maximum temperature."""
- return self.session.heating.max_temperature(self.node_id)
+ return self.device["max_temp"]
@property
def preset_mode(self):
"""Return the current preset mode, e.g., home, away, temp."""
- if (
- self.device_type == "Heating"
- and self.session.heating.get_boost(self.node_id) == "ON"
- ):
+ if self.device["status"]["boost"] == "ON":
return PRESET_BOOST
return None
@@ -155,31 +202,46 @@ def preset_modes(self):
return SUPPORT_PRESET
@refresh_system
- def set_hvac_mode(self, hvac_mode):
+ async def async_set_hvac_mode(self, hvac_mode):
"""Set new target hvac mode."""
new_mode = HASS_TO_HIVE_STATE[hvac_mode]
- self.session.heating.set_mode(self.node_id, new_mode)
+ await self.hive.heating.setMode(self.device, new_mode)
@refresh_system
- def set_temperature(self, **kwargs):
+ async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
new_temperature = kwargs.get(ATTR_TEMPERATURE)
if new_temperature is not None:
- self.session.heating.set_target_temperature(self.node_id, new_temperature)
+ await self.hive.heating.setTargetTemperature(self.device, new_temperature)
@refresh_system
- def set_preset_mode(self, preset_mode):
+ async def async_set_preset_mode(self, preset_mode):
"""Set new preset mode."""
if preset_mode == PRESET_NONE and self.preset_mode == PRESET_BOOST:
- self.session.heating.turn_boost_off(self.node_id)
+ await self.hive.heating.setBoostOff(self.device)
elif preset_mode == PRESET_BOOST:
curtemp = round(self.current_temperature * 2) / 2
temperature = curtemp + 0.5
- self.session.heating.turn_boost_on(self.node_id, 30, temperature)
+ await self.hive.heating.setBoostOn(self.device, 30, temperature)
- def update(self):
- """Update all Node data from Hive."""
- self.session.core.update_data(self.node_id)
- self.attributes = self.session.attributes.state_attributes(
- self.thermostat_node_id
+ async def async_heating_boost(self, time_period, temperature):
+ """Handle boost heating service call."""
+ _LOGGER.warning(
+ "Hive Service heating_boost will be removed in 2021.7.0, please update to heating_boost_on"
)
+ await self.async_heating_boost_on(time_period, temperature)
+
+ @refresh_system
+ async def async_heating_boost_on(self, time_period, temperature):
+ """Handle boost heating service call."""
+ await self.hive.heating.setBoostOn(self.device, time_period, temperature)
+
+ @refresh_system
+ async def async_heating_boost_off(self):
+ """Handle boost heating service call."""
+ await self.hive.heating.setBoostOff(self.device)
+
+ async def async_update(self):
+ """Update all Node data from Hive."""
+ await self.hive.session.updateData(self.device)
+ self.device = await self.hive.heating.getClimate(self.device)
diff --git a/homeassistant/components/hive/config_flow.py b/homeassistant/components/hive/config_flow.py
new file mode 100644
index 00000000000000..b00ba57a96ec88
--- /dev/null
+++ b/homeassistant/components/hive/config_flow.py
@@ -0,0 +1,167 @@
+"""Config Flow for Hive."""
+
+from apyhiveapi import Auth
+from apyhiveapi.helper.hive_exceptions import (
+ HiveApiError,
+ HiveInvalid2FACode,
+ HiveInvalidPassword,
+ HiveInvalidUsername,
+)
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
+from homeassistant.core import callback
+
+from .const import CONF_CODE, CONFIG_ENTRY_VERSION, DOMAIN
+
+
+class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a Hive config flow."""
+
+ VERSION = CONFIG_ENTRY_VERSION
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ def __init__(self):
+ """Initialize the config flow."""
+ self.hive_auth = None
+ self.data = {}
+ self.tokens = {}
+ self.entry = None
+
+ async def async_step_user(self, user_input=None):
+ """Prompt user input. Create or edit entry."""
+ errors = {}
+ # Login to Hive with user data.
+ if user_input is not None:
+ self.data.update(user_input)
+ self.hive_auth = Auth(
+ username=self.data[CONF_USERNAME], password=self.data[CONF_PASSWORD]
+ )
+
+ # Get user from existing entry and abort if already setup
+ self.entry = await self.async_set_unique_id(self.data[CONF_USERNAME])
+ if self.context["source"] != config_entries.SOURCE_REAUTH:
+ self._abort_if_unique_id_configured()
+
+ # Login to the Hive.
+ try:
+ self.tokens = await self.hive_auth.login()
+ except HiveInvalidUsername:
+ errors["base"] = "invalid_username"
+ except HiveInvalidPassword:
+ errors["base"] = "invalid_password"
+ except HiveApiError:
+ errors["base"] = "no_internet_available"
+
+ if self.tokens.get("ChallengeName") == "SMS_MFA":
+ # Complete SMS 2FA.
+ return await self.async_step_2fa()
+
+ if not errors:
+ # Complete the entry setup.
+ try:
+ return await self.async_setup_hive_entry()
+ except UnknownHiveError:
+ errors["base"] = "unknown"
+
+ # Show User Input form.
+ schema = vol.Schema(
+ {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
+ )
+ return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
+
+ async def async_step_2fa(self, user_input=None):
+ """Handle 2fa step."""
+ errors = {}
+
+ if user_input and user_input["2fa"] == "0000":
+ self.tokens = await self.hive_auth.login()
+ elif user_input:
+ try:
+ self.tokens = await self.hive_auth.sms_2fa(
+ user_input["2fa"], self.tokens
+ )
+ except HiveInvalid2FACode:
+ errors["base"] = "invalid_code"
+ except HiveApiError:
+ errors["base"] = "no_internet_available"
+
+ if not errors:
+ try:
+ return await self.async_setup_hive_entry()
+ except UnknownHiveError:
+ errors["base"] = "unknown"
+
+ schema = vol.Schema({vol.Required(CONF_CODE): str})
+ return self.async_show_form(step_id="2fa", data_schema=schema, errors=errors)
+
+ async def async_setup_hive_entry(self):
+ """Finish setup and create the config entry."""
+
+ if "AuthenticationResult" not in self.tokens:
+ raise UnknownHiveError
+
+ # Setup the config entry
+ self.data["tokens"] = self.tokens
+ if self.context["source"] == config_entries.SOURCE_REAUTH:
+ self.hass.config_entries.async_update_entry(
+ self.entry, title=self.data["username"], data=self.data
+ )
+ await self.hass.config_entries.async_reload(self.entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
+ return self.async_create_entry(title=self.data["username"], data=self.data)
+
+ async def async_step_reauth(self, user_input=None):
+ """Re Authenticate a user."""
+ data = {
+ CONF_USERNAME: user_input[CONF_USERNAME],
+ CONF_PASSWORD: user_input[CONF_PASSWORD],
+ }
+ return await self.async_step_user(data)
+
+ async def async_step_import(self, user_input=None):
+ """Import user."""
+ return await self.async_step_user(user_input)
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Hive options callback."""
+ return HiveOptionsFlowHandler(config_entry)
+
+
+class HiveOptionsFlowHandler(config_entries.OptionsFlow):
+ """Config flow options for Hive."""
+
+ def __init__(self, config_entry):
+ """Initialize Hive options flow."""
+ self.hive = None
+ self.config_entry = config_entry
+ self.interval = config_entry.options.get(CONF_SCAN_INTERVAL, 120)
+
+ async def async_step_init(self, user_input=None):
+ """Manage the options."""
+ return await self.async_step_user()
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ self.hive = self.hass.data["hive"][self.config_entry.entry_id]
+ errors = {}
+ if user_input is not None:
+ new_interval = user_input.get(CONF_SCAN_INTERVAL)
+ await self.hive.updateInterval(new_interval)
+ return self.async_create_entry(title="", data=user_input)
+
+ schema = vol.Schema(
+ {
+ vol.Optional(CONF_SCAN_INTERVAL, default=self.interval): vol.All(
+ vol.Coerce(int), vol.Range(min=30)
+ )
+ }
+ )
+ return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
+
+
+class UnknownHiveError(Exception):
+ """Catch unknown hive error."""
diff --git a/homeassistant/components/hive/const.py b/homeassistant/components/hive/const.py
new file mode 100644
index 00000000000000..9e1d7fc1f80344
--- /dev/null
+++ b/homeassistant/components/hive/const.py
@@ -0,0 +1,21 @@
+"""Constants for Hive."""
+ATTR_MODE = "mode"
+ATTR_TIME_PERIOD = "time_period"
+ATTR_ONOFF = "on_off"
+CONF_CODE = "2fa"
+CONFIG_ENTRY_VERSION = 1
+DEFAULT_NAME = "Hive"
+DOMAIN = "hive"
+PLATFORMS = ["binary_sensor", "climate", "light", "sensor", "switch", "water_heater"]
+PLATFORM_LOOKUP = {
+ "binary_sensor": "binary_sensor",
+ "climate": "climate",
+ "light": "light",
+ "sensor": "sensor",
+ "switch": "switch",
+ "water_heater": "water_heater",
+}
+SERVICE_BOOST_HOT_WATER = "boost_hot_water"
+SERVICE_BOOST_HEATING_ON = "boost_heating_on"
+SERVICE_BOOST_HEATING_OFF = "boost_heating_off"
+WATER_HEATER_MODES = ["on", "off"]
diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py
index 7659d43aeba294..46e8c5b579069e 100644
--- a/homeassistant/components/hive/light.py
+++ b/homeassistant/components/hive/light.py
@@ -1,4 +1,6 @@
-"""Support for the Hive lights."""
+"""Support for Hive light devices."""
+from datetime import timedelta
+
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
@@ -10,29 +12,28 @@
)
import homeassistant.util.color as color_util
-from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system
+from . import HiveEntity, refresh_system
+from .const import ATTR_MODE, DOMAIN
+
+PARALLEL_UPDATES = 0
+SCAN_INTERVAL = timedelta(seconds=15)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up Hive light devices."""
- if discovery_info is None:
- return
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up Hive thermostat based on a config entry."""
- session = hass.data.get(DATA_HIVE)
- devs = []
- for dev in discovery_info:
- devs.append(HiveDeviceLight(session, dev))
- add_entities(devs)
+ hive = hass.data[DOMAIN][entry.entry_id]
+ devices = hive.session.deviceList.get("light")
+ entities = []
+ if devices:
+ for dev in devices:
+ entities.append(HiveDeviceLight(hive, dev))
+ async_add_entities(entities, True)
class HiveDeviceLight(HiveEntity, LightEntity):
"""Hive Active Light Device."""
- def __init__(self, hive_session, hive_device):
- """Initialize the Light device."""
- super().__init__(hive_session, hive_device)
- self.light_device_type = hive_device["Hive_Light_DeviceType"]
-
@property
def unique_id(self):
"""Return unique ID of entity."""
@@ -41,64 +42,67 @@ def unique_id(self):
@property
def device_info(self):
"""Return device information."""
- return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name}
+ return {
+ "identifiers": {(DOMAIN, self.device["device_id"])},
+ "name": self.device["device_name"],
+ "model": self.device["deviceData"]["model"],
+ "manufacturer": self.device["deviceData"]["manufacturer"],
+ "sw_version": self.device["deviceData"]["version"],
+ "via_device": (DOMAIN, self.device["parentDevice"]),
+ }
@property
def name(self):
"""Return the display name of this light."""
- return self.node_name
+ return self.device["haName"]
@property
- def device_state_attributes(self):
+ def available(self):
+ """Return if the device is available."""
+ return self.device["deviceData"]["online"]
+
+ @property
+ def extra_state_attributes(self):
"""Show Device Attributes."""
- return self.attributes
+ return {
+ ATTR_MODE: self.attributes.get(ATTR_MODE),
+ }
@property
def brightness(self):
"""Brightness of the light (an integer in the range 1-255)."""
- return self.session.light.get_brightness(self.node_id)
+ return self.device["status"]["brightness"]
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
- if (
- self.light_device_type == "tuneablelight"
- or self.light_device_type == "colourtuneablelight"
- ):
- return self.session.light.get_min_color_temp(self.node_id)
+ return self.device.get("min_mireds")
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
- if (
- self.light_device_type == "tuneablelight"
- or self.light_device_type == "colourtuneablelight"
- ):
- return self.session.light.get_max_color_temp(self.node_id)
+ return self.device.get("max_mireds")
@property
def color_temp(self):
"""Return the CT color value in mireds."""
- if (
- self.light_device_type == "tuneablelight"
- or self.light_device_type == "colourtuneablelight"
- ):
- return self.session.light.get_color_temp(self.node_id)
+ return self.device["status"].get("color_temp")
@property
- def hs_color(self) -> tuple:
+ def hs_color(self):
"""Return the hs color value."""
- if self.light_device_type == "colourtuneablelight":
- rgb = self.session.light.get_color(self.node_id)
+ if self.device["status"]["mode"] == "COLOUR":
+ rgb = self.device["status"].get("hs_color")
return color_util.color_RGB_to_hs(*rgb)
+ return None
@property
def is_on(self):
"""Return true if light is on."""
- return self.session.light.get_state(self.node_id)
+ return self.device["status"]["state"]
@refresh_system
- def turn_on(self, **kwargs):
+ async def async_turn_on(self, **kwargs):
"""Instruct the light to turn on."""
new_brightness = None
new_color_temp = None
@@ -116,35 +120,32 @@ def turn_on(self, **kwargs):
get_new_color = kwargs.get(ATTR_HS_COLOR)
hue = int(get_new_color[0])
saturation = int(get_new_color[1])
- new_color = (hue, saturation, self.brightness)
-
- self.session.light.turn_on(
- self.node_id,
- self.light_device_type,
- new_brightness,
- new_color_temp,
- new_color,
+ new_color = (hue, saturation, 100)
+
+ await self.hive.light.turnOn(
+ self.device, new_brightness, new_color_temp, new_color
)
@refresh_system
- def turn_off(self, **kwargs):
+ async def async_turn_off(self, **kwargs):
"""Instruct the light to turn off."""
- self.session.light.turn_off(self.node_id)
+ await self.hive.light.turnOff(self.device)
@property
def supported_features(self):
"""Flag supported features."""
supported_features = None
- if self.light_device_type == "warmwhitelight":
+ if self.device["hiveType"] == "warmwhitelight":
supported_features = SUPPORT_BRIGHTNESS
- elif self.light_device_type == "tuneablelight":
+ elif self.device["hiveType"] == "tuneablelight":
supported_features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP
- elif self.light_device_type == "colourtuneablelight":
+ elif self.device["hiveType"] == "colourtuneablelight":
supported_features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR
return supported_features
- def update(self):
+ async def async_update(self):
"""Update all Node data from Hive."""
- self.session.core.update_data(self.node_id)
- self.attributes = self.session.attributes.state_attributes(self.node_id)
+ await self.hive.session.updateData(self.device)
+ self.device = await self.hive.light.getLight(self.device)
+ self.attributes.update(self.device.get("attributes", {}))
diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json
index f8fb9bc8c2a181..a1d74c023f1bbe 100644
--- a/homeassistant/components/hive/manifest.json
+++ b/homeassistant/components/hive/manifest.json
@@ -1,7 +1,13 @@
{
"domain": "hive",
"name": "Hive",
+ "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hive",
- "requirements": ["pyhiveapi==0.2.20.2"],
- "codeowners": ["@Rendili", "@KJonline"]
-}
+ "requirements": [
+ "pyhiveapi==0.4.1"
+ ],
+ "codeowners": [
+ "@Rendili",
+ "@KJonline"
+ ]
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py
index 360fb61bfbee82..518f3286231601 100644
--- a/homeassistant/components/hive/sensor.py
+++ b/homeassistant/components/hive/sensor.py
@@ -1,34 +1,32 @@
-"""Support for the Hive sensors."""
-from homeassistant.const import TEMP_CELSIUS
-from homeassistant.helpers.entity import Entity
+"""Support for the Hive sesnors."""
-from . import DATA_HIVE, DOMAIN, HiveEntity
+from datetime import timedelta
-FRIENDLY_NAMES = {
- "Hub_OnlineStatus": "Hive Hub Status",
- "Hive_OutsideTemperature": "Outside Temperature",
-}
+from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, SensorEntity
+
+from . import HiveEntity
+from .const import DOMAIN
-DEVICETYPE_ICONS = {
- "Hub_OnlineStatus": "mdi:switch",
- "Hive_OutsideTemperature": "mdi:thermometer",
+PARALLEL_UPDATES = 0
+SCAN_INTERVAL = timedelta(seconds=15)
+DEVICETYPE = {
+ "Battery": {"unit": " % ", "type": DEVICE_CLASS_BATTERY},
}
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up Hive sensor devices."""
- if discovery_info is None:
- return
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up Hive thermostat based on a config entry."""
- session = hass.data.get(DATA_HIVE)
- devs = []
- for dev in discovery_info:
- if dev["HA_DeviceType"] in FRIENDLY_NAMES:
- devs.append(HiveSensorEntity(session, dev))
- add_entities(devs)
+ hive = hass.data[DOMAIN][entry.entry_id]
+ devices = hive.session.deviceList.get("sensor")
+ entities = []
+ if devices:
+ for dev in devices:
+ entities.append(HiveSensorEntity(hive, dev))
+ async_add_entities(entities, True)
-class HiveSensorEntity(HiveEntity, Entity):
+class HiveSensorEntity(HiveEntity, SensorEntity):
"""Hive Sensor Entity."""
@property
@@ -39,32 +37,41 @@ def unique_id(self):
@property
def device_info(self):
"""Return device information."""
- return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name}
+ return {
+ "identifiers": {(DOMAIN, self.device["device_id"])},
+ "name": self.device["device_name"],
+ "model": self.device["deviceData"]["model"],
+ "manufacturer": self.device["deviceData"]["manufacturer"],
+ "sw_version": self.device["deviceData"]["version"],
+ "via_device": (DOMAIN, self.device["parentDevice"]),
+ }
@property
- def name(self):
- """Return the name of the sensor."""
- return FRIENDLY_NAMES.get(self.device_type)
+ def available(self):
+ """Return if sensor is available."""
+ return self.device.get("deviceData", {}).get("online")
@property
- def state(self):
- """Return the state of the sensor."""
- if self.device_type == "Hub_OnlineStatus":
- return self.session.sensor.hub_online_status(self.node_id)
- if self.device_type == "Hive_OutsideTemperature":
- return self.session.weather.temperature()
+ def device_class(self):
+ """Device class of the entity."""
+ return DEVICETYPE[self.device["hiveType"]].get("type")
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
- if self.device_type == "Hive_OutsideTemperature":
- return TEMP_CELSIUS
+ return DEVICETYPE[self.device["hiveType"]].get("unit")
@property
- def icon(self):
- """Return the icon to use."""
- return DEVICETYPE_ICONS.get(self.device_type)
+ def name(self):
+ """Return the name of the sensor."""
+ return self.device["haName"]
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self.device["status"]["state"]
- def update(self):
+ async def async_update(self):
"""Update all Node data from Hive."""
- self.session.core.update_data(self.node_id)
+ await self.hive.session.updateData(self.device)
+ self.device = await self.hive.sensor.getSensor(self.device)
diff --git a/homeassistant/components/hive/services.yaml b/homeassistant/components/hive/services.yaml
index f09baea7655f25..de1439eead4563 100644
--- a/homeassistant/components/hive/services.yaml
+++ b/homeassistant/components/hive/services.yaml
@@ -1,24 +1,94 @@
boost_heating:
+ name: Boost Heating (To be deprecated)
+ description: To be deprecated please use boost_heating_on.
+ fields:
+ entity_id:
+ name: Entity ID
+ description: Select entity_id to boost.
+ required: true
+ example: climate.heating
+ time_period:
+ name: Time Period
+ description: Set the time period for the boost.
+ required: true
+ example: 01:30:00
+ temperature:
+ name: Temperature
+ description: Set the target temperature for the boost period.
+ required: true
+ example: 20.5
+boost_heating_on:
+ name: Boost Heating On
description: Set the boost mode ON defining the period of time and the desired target temperature for the boost.
fields:
entity_id:
- description: Enter the entity_id for the device required to set the boost mode.
- example: "climate.heating"
+ name: Entity ID
+ description: Select entity_id to boost.
+ required: true
+ example: climate.heating
+ selector:
+ entity:
+ integration: hive
+ domain: climate
time_period:
+ name: Time Period
description: Set the time period for the boost.
- example: "01:30:00"
+ required: true
+ example: 01:30:00
+ selector:
+ time:
temperature:
+ name: Temperature
description: Set the target temperature for the boost period.
- example: "20.5"
+ required: true
+ example: 20.5
+ selector:
+ number:
+ min: 7
+ max: 35
+ step: 0.5
+ unit_of_measurement: degrees
+ mode: slider
+boost_heating_off:
+ name: Boost Heating Off
+ description: Set the boost mode OFF.
+ fields:
+ entity_id:
+ name: Entity ID
+ description: Select entity_id to turn boost off.
+ required: true
+ example: climate.heating
+ selector:
+ entity:
+ integration: hive
+ domain: climate
boost_hot_water:
- description: "Set the boost mode ON or OFF defining the period of time for the boost."
+ name: Boost Hotwater
+ description: Set the boost mode ON or OFF defining the period of time for the boost.
fields:
entity_id:
- description: Enter the entity_id for the device reuired to set the boost mode.
- example: "water_heater.hot_water"
+ name: Entity ID
+ description: Select entity_id to boost.
+ required: true
+ example: water_heater.hot_water
+ selector:
+ entity:
+ integration: hive
+ domain: water_heater
time_period:
+ name: Time Period
description: Set the time period for the boost.
- example: "01:30:00"
+ required: true
+ example: 01:30:00
+ selector:
+ time:
on_off:
+ name: Mode
description: Set the boost function on or off.
+ required: true
example: "on"
+ selector:
+ select:
+ options:
+ - "on"
+ - "off"
diff --git a/homeassistant/components/hive/strings.json b/homeassistant/components/hive/strings.json
new file mode 100644
index 00000000000000..0a7a587b2dbe62
--- /dev/null
+++ b/homeassistant/components/hive/strings.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "Hive Login",
+ "description": "Enter your Hive login information and configuration.",
+ "data": {
+ "username": "[%key:common::config_flow::data::username%]",
+ "password": "[%key:common::config_flow::data::password%]",
+ "scan_interval": "Scan Interval (seconds)"
+ }
+ },
+ "2fa": {
+ "title": "Hive Two-factor Authentication.",
+ "description": "Enter your Hive authentication code. \n \n Please enter code 0000 to request another code.",
+ "data": {
+ "2fa": "Two-factor code"
+ }
+ },
+ "reauth": {
+ "title": "Hive Login",
+ "description": "Re-enter your Hive login information.",
+ "data": {
+ "username": "[%key:common::config_flow::data::username%]",
+ "password": "[%key:common::config_flow::data::password%]"
+ }
+ }
+ },
+ "error": {
+ "invalid_username": "Failed to sign into Hive. Your email address is not recognised.",
+ "invalid_password": "Failed to sign into Hive. Incorrect password please try again.",
+ "invalid_code": "Failed to sign into Hive. Your two-factor authentication code was incorrect.",
+ "no_internet_available": "An internet connection is required to connect to Hive.",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
+ "unknown_entry": "Unable to find existing entry.",
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "title": "Options for Hive",
+ "description": "Update the scan interval to poll for data more often.",
+ "data": {
+ "scan_interval": "Scan Interval (seconds)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py
index 734581b0db3785..1151fcf346b472 100644
--- a/homeassistant/components/hive/switch.py
+++ b/homeassistant/components/hive/switch.py
@@ -1,19 +1,25 @@
"""Support for the Hive switches."""
+from datetime import timedelta
+
from homeassistant.components.switch import SwitchEntity
-from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system
+from . import HiveEntity, refresh_system
+from .const import ATTR_MODE, DOMAIN
+
+PARALLEL_UPDATES = 0
+SCAN_INTERVAL = timedelta(seconds=15)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up Hive switches."""
- if discovery_info is None:
- return
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up Hive thermostat based on a config entry."""
- session = hass.data.get(DATA_HIVE)
- devs = []
- for dev in discovery_info:
- devs.append(HiveDevicePlug(session, dev))
- add_entities(devs)
+ hive = hass.data[DOMAIN][entry.entry_id]
+ devices = hive.session.deviceList.get("switch")
+ entities = []
+ if devices:
+ for dev in devices:
+ entities.append(HiveDevicePlug(hive, dev))
+ async_add_entities(entities, True)
class HiveDevicePlug(HiveEntity, SwitchEntity):
@@ -27,39 +33,54 @@ def unique_id(self):
@property
def device_info(self):
"""Return device information."""
- return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name}
+ if self.device["hiveType"] == "activeplug":
+ return {
+ "identifiers": {(DOMAIN, self.device["device_id"])},
+ "name": self.device["device_name"],
+ "model": self.device["deviceData"]["model"],
+ "manufacturer": self.device["deviceData"]["manufacturer"],
+ "sw_version": self.device["deviceData"]["version"],
+ "via_device": (DOMAIN, self.device["parentDevice"]),
+ }
@property
def name(self):
"""Return the name of this Switch device if any."""
- return self.node_name
+ return self.device["haName"]
+
+ @property
+ def available(self):
+ """Return if the device is available."""
+ return self.device["deviceData"].get("online")
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Show Device Attributes."""
- return self.attributes
+ return {
+ ATTR_MODE: self.attributes.get(ATTR_MODE),
+ }
@property
def current_power_w(self):
"""Return the current power usage in W."""
- return self.session.switch.get_power_usage(self.node_id)
+ return self.device["status"].get("power_usage")
@property
def is_on(self):
"""Return true if switch is on."""
- return self.session.switch.get_state(self.node_id)
+ return self.device["status"]["state"]
@refresh_system
- def turn_on(self, **kwargs):
+ async def async_turn_on(self, **kwargs):
"""Turn the switch on."""
- self.session.switch.turn_on(self.node_id)
+ await self.hive.switch.turnOn(self.device)
@refresh_system
- def turn_off(self, **kwargs):
+ async def async_turn_off(self, **kwargs):
"""Turn the device off."""
- self.session.switch.turn_off(self.node_id)
+ await self.hive.switch.turnOff(self.device)
- def update(self):
+ async def async_update(self):
"""Update all Node data from Hive."""
- self.session.core.update_data(self.node_id)
- self.attributes = self.session.attributes.state_attributes(self.node_id)
+ await self.hive.session.updateData(self.device)
+ self.device = await self.hive.switch.getSwitch(self.device)
diff --git a/homeassistant/components/hive/translations/ca.json b/homeassistant/components/hive/translations/ca.json
new file mode 100644
index 00000000000000..eacccda82e7ba1
--- /dev/null
+++ b/homeassistant/components/hive/translations/ca.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El compte ja ha estat configurat",
+ "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament",
+ "unknown_entry": "No s'ha pogut trobar l'entrada existent."
+ },
+ "error": {
+ "invalid_code": "No s'ha pogut iniciar sessi\u00f3 a Hive. El codi de verificaci\u00f3 en dos passos no \u00e9s correcte.",
+ "invalid_password": "No s'ha pogut iniciar sessi\u00f3 a Hive. Contrasenya incorrecta, torna-ho a provar.",
+ "invalid_username": "No s'ha pogut iniciar sessi\u00f3 a Hive. L'adre\u00e7a de correu electr\u00f2nic no s'ha reconegut.",
+ "no_internet_available": "Cal una connexi\u00f3 a Internet per connectar-se al Hive.",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "Codi de verificaci\u00f3 en dos passos"
+ },
+ "description": "Introdueix codi d'autenticaci\u00f3 Hive. \n\n Introdueix el codi 0000 per demanar un altre codi.",
+ "title": "Verificaci\u00f3 en dos passos de Hive."
+ },
+ "reauth": {
+ "data": {
+ "password": "Contrasenya",
+ "username": "Nom d'usuari"
+ },
+ "description": "Torna a introduir la informaci\u00f3 d'inici de sessi\u00f3 del Hive.",
+ "title": "Inici de sessi\u00f3 Hive"
+ },
+ "user": {
+ "data": {
+ "password": "Contrasenya",
+ "scan_interval": "Interval d'escaneig (segons)",
+ "username": "Nom d'usuari"
+ },
+ "description": "Actualitza la informaci\u00f3 i configuraci\u00f3 d'inici de sessi\u00f3.",
+ "title": "Inici de sessi\u00f3 Hive"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "scan_interval": "Interval d'escaneig (segons)"
+ },
+ "description": "Actualitza l'interval d'escaneig per sondejar les dades m\u00e9s sovint.",
+ "title": "Opcions de Hive"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/translations/cs.json b/homeassistant/components/hive/translations/cs.json
new file mode 100644
index 00000000000000..8544a3de7b893e
--- /dev/null
+++ b/homeassistant/components/hive/translations/cs.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "step": {
+ "reauth": {
+ "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/hive/translations/de.json b/homeassistant/components/hive/translations/de.json
new file mode 100644
index 00000000000000..bd5876bb023d26
--- /dev/null
+++ b/homeassistant/components/hive/translations/de.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich",
+ "unknown_entry": "Vorhandener Eintrag kann nicht gefunden werden."
+ },
+ "error": {
+ "invalid_code": "Anmeldung bei Hive fehlgeschlagen. Dein Zwei-Faktor-Authentifizierungscode war falsch.",
+ "invalid_password": "Anmeldung bei Hive fehlgeschlagen. Falsches Passwort, bitte versuche es erneut.",
+ "invalid_username": "Die Anmeldung bei Hive ist fehlgeschlagen. Deine E-Mail-Adresse wird nicht erkannt.",
+ "no_internet_available": "F\u00fcr die Verbindung mit Hive ist eine Internetverbindung erforderlich.",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "Zwei-Faktor Authentifizierungscode"
+ },
+ "description": "Gib deinen Hive-Authentifizierungscode ein. \n \nBitte gib den Code 0000 ein, um einen anderen Code anzufordern.",
+ "title": "Hive Zwei-Faktor-Authentifizierung."
+ },
+ "reauth": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ },
+ "description": "Gebe deine Hive Anmeldeinformationen erneut ein.",
+ "title": "Hive Anmeldung"
+ },
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "scan_interval": "Scanintervall (Sekunden)",
+ "username": "Benutzername"
+ },
+ "description": "Gebe deine Anmeldeinformationen und -konfiguration f\u00fcr Hive ein",
+ "title": "Hive Anmeldung"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "scan_interval": "Scanintervall (Sekunden)"
+ },
+ "description": "Aktualisiere den Scanintervall, um Daten \u00f6fters abzufragen.",
+ "title": "Optionen f\u00fcr Hive"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/translations/el.json b/homeassistant/components/hive/translations/el.json
new file mode 100644
index 00000000000000..986dd52ef199ec
--- /dev/null
+++ b/homeassistant/components/hive/translations/el.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u039f \u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2",
+ "reauth_successful": "H \u03b5\u03c0\u03b1\u03bd\u03b1\u03b5\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03af\u03c9\u03c3\u03b7 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2.",
+ "unknown_entry": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7\u03c2."
+ },
+ "error": {
+ "invalid_code": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf Hive. \u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd \u03ae\u03c4\u03b1\u03bd \u03bb\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2.",
+ "invalid_password": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf Hive. \u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bb\u03ac\u03b8\u03bf\u03c2, \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.",
+ "invalid_username": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf Hive. \u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03b4\u03b5\u03bd \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9.",
+ "no_internet_available": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf \u0394\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf Hive.",
+ "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd"
+ },
+ "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 Hive. \n\n \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc 0000 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b6\u03b7\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ac\u03bb\u03bb\u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc.",
+ "title": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd \u03c4\u03bf\u03c5 Hive."
+ },
+ "reauth": {
+ "data": {
+ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2",
+ "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7"
+ },
+ "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Hive.",
+ "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 Hive"
+ },
+ "user": {
+ "data": {
+ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2",
+ "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03a3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)",
+ "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7"
+ },
+ "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03ba\u03b1\u03b9 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf Hive.",
+ "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 Hive"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03a3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)"
+ },
+ "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03c4\u03b5 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c0\u03b9\u03bf \u03c3\u03c5\u03c7\u03bd\u03ac.",
+ "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 Hive"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/translations/en.json b/homeassistant/components/hive/translations/en.json
new file mode 100644
index 00000000000000..32453da0a0c49c
--- /dev/null
+++ b/homeassistant/components/hive/translations/en.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Account is already configured",
+ "reauth_successful": "Re-authentication was successful",
+ "unknown_entry": "Unable to find existing entry."
+ },
+ "error": {
+ "invalid_code": "Failed to sign into Hive. Your two-factor authentication code was incorrect.",
+ "invalid_password": "Failed to sign into Hive. Incorrect password please try again.",
+ "invalid_username": "Failed to sign into Hive. Your email address is not recognised.",
+ "no_internet_available": "An internet connection is required to connect to Hive.",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "Two-factor code"
+ },
+ "description": "Enter your Hive authentication code. \n \n Please enter code 0000 to request another code.",
+ "title": "Hive Two-factor Authentication."
+ },
+ "reauth": {
+ "data": {
+ "password": "Password",
+ "username": "Username"
+ },
+ "description": "Re-enter your Hive login information.",
+ "title": "Hive Login"
+ },
+ "user": {
+ "data": {
+ "password": "Password",
+ "scan_interval": "Scan Interval (seconds)",
+ "username": "Username"
+ },
+ "description": "Enter your Hive login information and configuration.",
+ "title": "Hive Login"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "scan_interval": "Scan Interval (seconds)"
+ },
+ "description": "Update the scan interval to poll for data more often.",
+ "title": "Options for Hive"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/translations/es.json b/homeassistant/components/hive/translations/es.json
new file mode 100644
index 00000000000000..eb5ef0fd6eb019
--- /dev/null
+++ b/homeassistant/components/hive/translations/es.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "abort": {
+ "unknown_entry": "No se puede encontrar una entrada existente."
+ },
+ "error": {
+ "invalid_code": "No se ha podido iniciar la sesi\u00f3n en Hive. Tu c\u00f3digo de autenticaci\u00f3n de dos factores era incorrecto.",
+ "invalid_password": "No se ha podido iniciar la sesi\u00f3n en Hive. Contrase\u00f1a incorrecta, por favor, int\u00e9ntelo de nuevo.",
+ "invalid_username": "No se ha podido iniciar la sesi\u00f3n en Hive. No se reconoce su direcci\u00f3n de correo electr\u00f3nico.",
+ "no_internet_available": "Se requiere una conexi\u00f3n a Internet para conectarse a Hive."
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "C\u00f3digo de dos factores"
+ },
+ "description": "Introduzca su c\u00f3digo de autentificaci\u00f3n Hive. \n \n Introduzca el c\u00f3digo 0000 para solicitar otro c\u00f3digo.",
+ "title": "Autenticaci\u00f3n de dos factores de Hive."
+ },
+ "reauth": {
+ "description": "Vuelva a introducir sus datos de acceso a Hive.",
+ "title": "Inicio de sesi\u00f3n en Hive"
+ },
+ "user": {
+ "data": {
+ "scan_interval": "Intervalo de exploraci\u00f3n (segundos)"
+ },
+ "description": "Ingrese su configuraci\u00f3n e informaci\u00f3n de inicio de sesi\u00f3n de Hive.",
+ "title": "Inicio de sesi\u00f3n en Hive"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "scan_interval": "Intervalo de exploraci\u00f3n (segundos)"
+ },
+ "description": "Actualice el intervalo de escaneo para buscar datos m\u00e1s a menudo.",
+ "title": "Opciones para Hive"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/translations/et.json b/homeassistant/components/hive/translations/et.json
new file mode 100644
index 00000000000000..5cffcb036d327c
--- /dev/null
+++ b/homeassistant/components/hive/translations/et.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kasutaja on juba seadistatud",
+ "reauth_successful": "Taastuvastamine \u00f5nnestus",
+ "unknown_entry": "Olemasolevat kirjet ei leitud."
+ },
+ "error": {
+ "invalid_code": "Hive sisselogimine nurjus. Kaheastmeline autentimiskood oli vale.",
+ "invalid_password": "Hive sisselogimine nurjus. Vale parool, proovi uuesti.",
+ "invalid_username": "Hive sisselogimine nurjus. E-posti aadressi ei tuvastatud.",
+ "no_internet_available": "Hive-ga \u00fchenduse loomiseks on vajalik Interneti\u00fchendus.",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "Kaheastmelise tuvastuse kood"
+ },
+ "description": "Sisesta oma Hive autentimiskood. \n\n Uue koodi taotlemiseks sisesta kood 0000.",
+ "title": "Hive kaheastmeline autentimine."
+ },
+ "reauth": {
+ "data": {
+ "password": "Salas\u00f5na",
+ "username": "Kasutajanimi"
+ },
+ "description": "Sisesta oma Hive sisselogimisandmed uuesti.",
+ "title": "Hive sisselogimine"
+ },
+ "user": {
+ "data": {
+ "password": "Salas\u00f5na",
+ "scan_interval": "P\u00e4ringute intervall (sekundites)",
+ "username": "Kasutajanimi"
+ },
+ "description": "Sisesta oma Hive sisselogimisteave ja s\u00e4tted.",
+ "title": "Hive sisselogimine"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "scan_interval": "P\u00e4ringute intervall (sekundites)"
+ },
+ "description": "Muuda k\u00fcsitlemise intervalli p\u00e4ringute tihendamiseks.",
+ "title": "Hive s\u00e4tted"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/translations/fr.json b/homeassistant/components/hive/translations/fr.json
new file mode 100644
index 00000000000000..5868a2bf175166
--- /dev/null
+++ b/homeassistant/components/hive/translations/fr.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9",
+ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi",
+ "unknown_entry": "Impossible de trouver l'entr\u00e9e existante."
+ },
+ "error": {
+ "invalid_code": "\u00c9chec de la connexion \u00e0 Hive. Votre code d'authentification \u00e0 deux facteurs \u00e9tait incorrect.",
+ "invalid_password": "\u00c9chec de la connexion \u00e0 Hive. Mot de passe incorrect, veuillez r\u00e9essayer.",
+ "invalid_username": "\u00c9chec de la connexion \u00e0 Hive. Votre adresse e-mail n'est pas reconnue.",
+ "no_internet_available": "Une connexion Internet est requise pour se connecter \u00e0 Hive.",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "Code \u00e0 deux facteurs"
+ },
+ "description": "Entrez votre code d\u2019authentification Hive. \n \nVeuillez entrer le code 0000 pour demander un autre code.",
+ "title": "Authentification \u00e0 deux facteurs Hive."
+ },
+ "reauth": {
+ "data": {
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur"
+ },
+ "description": "Entrez \u00e0 nouveau vos informations de connexion Hive.",
+ "title": "Connexion \u00e0 Hive"
+ },
+ "user": {
+ "data": {
+ "password": "Mot de passe",
+ "scan_interval": "Intervalle de scan (secondes)",
+ "username": "Nom d'utilisateur"
+ },
+ "description": "Entrez vos informations de connexion et votre configuration Hive.",
+ "title": "Connexion \u00e0 Hive"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "scan_interval": "Intervalle de scan (secondes)"
+ },
+ "description": "Mettez \u00e0 jour l\u2019intervalle d\u2019analyse pour obtenir des donn\u00e9es plus souvent.",
+ "title": "Options pour Hive"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/translations/hu.json b/homeassistant/components/hive/translations/hu.json
new file mode 100644
index 00000000000000..80c6a7e40f11c9
--- /dev/null
+++ b/homeassistant/components/hive/translations/hu.json
@@ -0,0 +1,48 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt",
+ "unknown_entry": "Nem tal\u00e1lhat\u00f3 megl\u00e9v\u0151 bejegyz\u00e9s."
+ },
+ "error": {
+ "invalid_code": "Nem siker\u00fclt bejelentkezni a Hive-ba. A k\u00e9tfaktoros hiteles\u00edt\u00e9si k\u00f3d helytelen volt.",
+ "invalid_password": "Nem siker\u00fclt bejelentkezni a Hive-ba. Helytelen jelsz\u00f3, pr\u00f3b\u00e1lkozz \u00fajra.",
+ "invalid_username": "Nem siker\u00fclt bejelentkezni a Hive-ba. Az email c\u00edmedet nem siker\u00fclt felismerni.",
+ "no_internet_available": "A Hive-hoz val\u00f3 csatlakoz\u00e1shoz internetkapcsolat sz\u00fcks\u00e9ges.",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "K\u00e9tfaktoros k\u00f3d"
+ },
+ "description": "Add meg a Hive hiteles\u00edt\u00e9si k\u00f3dj\u00e1t. \n \n\u00cdrd be a 0000 k\u00f3dot m\u00e1sik k\u00f3d k\u00e9r\u00e9s\u00e9hez.",
+ "title": "Hive k\u00e9tfaktoros hiteles\u00edt\u00e9s."
+ },
+ "reauth": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "description": "Add meg \u00fajra a Hive bejelentkez\u00e9si adatait.",
+ "title": "Hive Bejelentkez\u00e9s"
+ },
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "description": "Add meg a Hive bejelentkez\u00e9si adatait \u00e9s konfigur\u00e1ci\u00f3j\u00e1t.",
+ "title": "Hive Bejelentkez\u00e9s"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "title": "Hive be\u00e1ll\u00edt\u00e1sok"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/translations/it.json b/homeassistant/components/hive/translations/it.json
new file mode 100644
index 00000000000000..fd79ca35b79787
--- /dev/null
+++ b/homeassistant/components/hive/translations/it.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato",
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente",
+ "unknown_entry": "Impossibile trovare la voce esistente."
+ },
+ "error": {
+ "invalid_code": "Impossibile accedere a Hive. Il codice di autenticazione a due fattori non era corretto.",
+ "invalid_password": "Impossibile accedere a Hive. Password errata, riprova.",
+ "invalid_username": "Impossibile accedere a Hive. Il tuo indirizzo email non \u00e8 riconosciuto.",
+ "no_internet_available": "\u00c8 necessaria una connessione Internet per connettersi a Hive.",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "Codice a due fattori"
+ },
+ "description": "Inserisci il tuo codice di autenticazione Hive. \n\n Inserisci il codice 0000 per richiedere un altro codice.",
+ "title": "Autenticazione a due fattori di Hive."
+ },
+ "reauth": {
+ "data": {
+ "password": "Password",
+ "username": "Nome utente"
+ },
+ "description": "Inserisci nuovamente le tue informazioni di accesso a Hive.",
+ "title": "Accesso Hive"
+ },
+ "user": {
+ "data": {
+ "password": "Password",
+ "scan_interval": "Intervallo di scansione (secondi)",
+ "username": "Nome utente"
+ },
+ "description": "Immettere le informazioni di accesso e la configurazione di Hive.",
+ "title": "Accesso Hive"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "scan_interval": "Intervallo di scansione (secondi)"
+ },
+ "description": "Aggiorna l'intervallo di scansione per eseguire la verifica ciclica dei dati pi\u00f9 spesso.",
+ "title": "Opzioni per Hive"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/translations/ko.json b/homeassistant/components/hive/translations/ko.json
new file mode 100644
index 00000000000000..1f06a2f1ac76dc
--- /dev/null
+++ b/homeassistant/components/hive/translations/ko.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4",
+ "unknown_entry": "\uae30\uc874 \ud56d\ubaa9\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "invalid_code": "Hive\uc5d0 \ub85c\uadf8\uc778\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. 2\ub2e8\uacc4 \uc778\uc99d \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "invalid_password": "Hive\uc5d0 \ub85c\uadf8\uc778\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ube44\ubc00\ubc88\ud638\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "invalid_username": "Hive\uc5d0 \ub85c\uadf8\uc778\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uc774\uba54\uc77c \uc8fc\uc18c\ub97c \uc778\uc2dd\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
+ "no_internet_available": "Hive\uc5d0 \uc5f0\uacb0\ud558\ub824\uba74 \uc778\ud130\ub137 \uc5f0\uacb0\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "2\ub2e8\uacc4 \uc778\uc99d \ucf54\ub4dc"
+ },
+ "description": "Hive \uc778\uc99d \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\n\ub2e4\ub978 \ucf54\ub4dc\ub97c \uc694\uccad\ud558\ub824\uba74 \ucf54\ub4dc 0000\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "Hive 2\ub2e8\uacc4 \uc778\uc99d."
+ },
+ "reauth": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ },
+ "description": "Hive \ub85c\uadf8\uc778 \uc815\ubcf4\ub97c \ub2e4\uc2dc \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "Hive \ub85c\uadf8\uc778"
+ },
+ "user": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ucd08)",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ },
+ "description": "Hive \ub85c\uadf8\uc778 \uc815\ubcf4 \ubc0f \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "Hive \ub85c\uadf8\uc778"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ucd08)"
+ },
+ "description": "\ub370\uc774\ud130\ub97c \ub354 \uc790\uc8fc \ud3f4\ub9c1\ud558\ub824\uba74 \uac80\uc0c9 \uac04\uaca9\uc744 \uc5c5\ub370\uc774\ud2b8\ud574\uc8fc\uc138\uc694.",
+ "title": "Hive \uc635\uc158"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/translations/nl.json b/homeassistant/components/hive/translations/nl.json
new file mode 100644
index 00000000000000..3ac45ae14d7f4c
--- /dev/null
+++ b/homeassistant/components/hive/translations/nl.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Account is al geconfigureerd",
+ "reauth_successful": "Herauthenticatie was succesvol",
+ "unknown_entry": "Kan bestaand item niet vinden."
+ },
+ "error": {
+ "invalid_code": "Aanmelden bij Hive is mislukt. Uw tweefactorauthenticatiecode was onjuist.",
+ "invalid_password": "Aanmelden bij Hive is mislukt. Onjuist wachtwoord, probeer het opnieuw.",
+ "invalid_username": "Aanmelden bij Hive is mislukt. Uw e-mailadres wordt niet herkend.",
+ "no_internet_available": "Een internetverbinding is vereist om verbinding te maken met Hive.",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "Tweefactorauthenticatiecode"
+ },
+ "description": "Voer uw Hive-verificatiecode in. \n \n Voer code 0000 in om een andere code aan te vragen.",
+ "title": "Hive tweefactorauthenticatie"
+ },
+ "reauth": {
+ "data": {
+ "password": "Wachtwoord",
+ "username": "Gebruikersnaam"
+ },
+ "description": "Voer uw Hive-aanmeldingsgegevens opnieuw in.",
+ "title": "Hive-aanmelding"
+ },
+ "user": {
+ "data": {
+ "password": "Wachtwoord",
+ "scan_interval": "Scaninterval (seconden)",
+ "username": "Gebruikersnaam"
+ },
+ "description": "Voer uw Hive login informatie en configuratie in.",
+ "title": "Hive-aanmelding"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "scan_interval": "Scaninterval (seconden)"
+ },
+ "description": "Werk het scaninterval bij om vaker naar gegevens te vragen.",
+ "title": "Opties voor Hive"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/translations/no.json b/homeassistant/components/hive/translations/no.json
new file mode 100644
index 00000000000000..c5213aafeee9a7
--- /dev/null
+++ b/homeassistant/components/hive/translations/no.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kontoen er allerede konfigurert",
+ "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket",
+ "unknown_entry": "Kunne ikke finne eksisterende oppf\u00f8ring."
+ },
+ "error": {
+ "invalid_code": "Kunne ikke logge p\u00e5 Hive. Tofaktorautentiseringskoden din var feil.",
+ "invalid_password": "Kunne ikke logge p\u00e5 Hive. Feil passord. Vennligst pr\u00f8v igjen.",
+ "invalid_username": "Kunne ikke logge p\u00e5 Hive. E-postadressen din blir ikke gjenkjent.",
+ "no_internet_available": "Det kreves en internettforbindelse for \u00e5 koble til Hive.",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "Totrinnsbekreftelse kode"
+ },
+ "description": "Skriv inn din Hive-godkjenningskode. \n\n Vennligst skriv inn kode 0000 for \u00e5 be om en annen kode.",
+ "title": "Hive Totrinnsbekreftelse autentisering."
+ },
+ "reauth": {
+ "data": {
+ "password": "Passord",
+ "username": "Brukernavn"
+ },
+ "description": "Skriv innloggingsinformasjonen for Hive p\u00e5 nytt.",
+ "title": "Hive-p\u00e5logging"
+ },
+ "user": {
+ "data": {
+ "password": "Passord",
+ "scan_interval": "Skanneintervall (sekunder)",
+ "username": "Brukernavn"
+ },
+ "description": "Skriv inn inn innloggingsinformasjonen og konfigurasjonen for Hive.",
+ "title": "Hive-p\u00e5logging"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "scan_interval": "Skanneintervall (sekunder)"
+ },
+ "description": "Oppdater skanneintervallet for \u00e5 avstemme etter data oftere.",
+ "title": "Alternativer for Hive"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/translations/pl.json b/homeassistant/components/hive/translations/pl.json
new file mode 100644
index 00000000000000..0c61fa74febead
--- /dev/null
+++ b/homeassistant/components/hive/translations/pl.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto jest ju\u017c skonfigurowane",
+ "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119",
+ "unknown_entry": "Nie mo\u017cna znale\u017a\u0107 istniej\u0105cego wpisu."
+ },
+ "error": {
+ "invalid_code": "Nie uda\u0142o si\u0119 zalogowa\u0107 do Hive. Tw\u00f3j kod uwierzytelniania dwusk\u0142adnikowego by\u0142 nieprawid\u0142owy.",
+ "invalid_password": "Nie uda\u0142o si\u0119 zalogowa\u0107 do Hive. Nieprawid\u0142owe has\u0142o, spr\u00f3buj ponownie.",
+ "invalid_username": "Nie uda\u0142o si\u0119 zalogowa\u0107 do Hive. Tw\u00f3j adres e-mail nie zosta\u0142 rozpoznany.",
+ "no_internet_available": "Do po\u0142\u0105czenia z Hive wymagane jest po\u0142\u0105czenie z internetem.",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "Kod uwierzytelniania dwusk\u0142adnikowego"
+ },
+ "description": "Wprowad\u017a sw\u00f3j kod uwierzytelniaj\u0105cy Hive. \n\nWprowad\u017a kod 0000, aby poprosi\u0107 o kolejny kod.",
+ "title": "Uwierzytelnianie dwusk\u0142adnikowe Hive"
+ },
+ "reauth": {
+ "data": {
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ },
+ "description": "Wprowad\u017a ponownie swoje dane logowania do Hive.",
+ "title": "Login Hive"
+ },
+ "user": {
+ "data": {
+ "password": "Has\u0142o",
+ "scan_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania (w sekundach)",
+ "username": "Nazwa u\u017cytkownika"
+ },
+ "description": "Wprowad\u017a dane logowania i konfiguracj\u0119 Hive.",
+ "title": "Login Hive"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "scan_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania (w sekundach)"
+ },
+ "description": "Zaktualizuj cz\u0119stotliwo\u015b\u0107 skanowania, aby cz\u0119\u015bciej sondowa\u0107 dane.",
+ "title": "Opcje Hive"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/translations/pt.json b/homeassistant/components/hive/translations/pt.json
new file mode 100644
index 00000000000000..8397b5cb0a457e
--- /dev/null
+++ b/homeassistant/components/hive/translations/pt.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Conta j\u00e1 configurada",
+ "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida"
+ },
+ "error": {
+ "unknown": "Erro inesperado"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Palavra-passe",
+ "username": "Nome de Utilizador"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "Palavra-passe",
+ "username": "Nome de Utilizador"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/translations/ru.json b/homeassistant/components/hive/translations/ru.json
new file mode 100644
index 00000000000000..02736871d240be
--- /dev/null
+++ b/homeassistant/components/hive/translations/ru.json
@@ -0,0 +1,53 @@
+{
+ "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.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.",
+ "unknown_entry": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c."
+ },
+ "error": {
+ "invalid_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u043e\u0439\u0442\u0438 \u0432 Hive. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "invalid_password": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u043e\u0439\u0442\u0438 \u0432 Hive. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.",
+ "invalid_username": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u043e\u0439\u0442\u0438 \u0432 Hive. \u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.",
+ "no_internet_available": "\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "\u041a\u043e\u0434 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 Hive \u0438\u043b\u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 0000, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043f\u0440\u043e\u0441\u0438\u0442\u044c \u0434\u0440\u0443\u0433\u043e\u0439 \u043a\u043e\u0434.",
+ "title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f"
+ },
+ "reauth": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0432 Hive.",
+ "title": "Hive"
+ },
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u0432 Hive.",
+ "title": "Hive"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
+ },
+ "description": "\u0427\u0442\u043e\u0431\u044b \u0447\u0430\u0449\u0435 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435, \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430.",
+ "title": "Hive"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/translations/zh-Hant.json b/homeassistant/components/hive/translations/zh-Hant.json
new file mode 100644
index 00000000000000..0af7e218f6e0bd
--- /dev/null
+++ b/homeassistant/components/hive/translations/zh-Hant.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f",
+ "unknown_entry": "\u7121\u6cd5\u627e\u5230\u73fe\u6709\u5be6\u9ad4\u3002"
+ },
+ "error": {
+ "invalid_code": "Hive \u767b\u5165\u5931\u6557\u3002\u96d9\u91cd\u8a8d\u8b49\u78bc\u4e0d\u6b63\u78ba\u3002",
+ "invalid_password": "Hive \u767b\u5165\u5931\u6557\u3002\u5bc6\u78bc\u932f\u8aa4\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002",
+ "invalid_username": "Hive \u767b\u5165\u5931\u6557\u3002\u627e\u4e0d\u5230\u96fb\u5b50\u90f5\u4ef6\u3002",
+ "no_internet_available": "\u9700\u8981\u7db2\u969b\u7db2\u8def\u9023\u7dda\u4ee5\u9023\u7dda\u81f3 Hive\u3002",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "\u96d9\u91cd\u8a8d\u8b49\u78bc"
+ },
+ "description": "\u8f38\u5165 Hive \u8a8d\u8b49\u78bc\u3002\n \n \u8acb\u8f38\u5165 0000 \u4ee5\u7372\u53d6\u5176\u4ed6\u8a8d\u8b49\u78bc\u3002",
+ "title": "\u96d9\u91cd\u8a8d\u8b49"
+ },
+ "reauth": {
+ "data": {
+ "password": "\u5bc6\u78bc",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ },
+ "description": "\u91cd\u65b0\u8f38\u5165 Hive \u767b\u5165\u8cc7\u8a0a\u3002",
+ "title": "Hive \u767b\u5165\u8cc7\u8a0a"
+ },
+ "user": {
+ "data": {
+ "password": "\u5bc6\u78bc",
+ "scan_interval": "\u6383\u63cf\u9593\u8ddd\uff08\u79d2\uff09",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ },
+ "description": "\u8f38\u5165 Hive \u767b\u5165\u8cc7\u8a0a\u8207\u8a2d\u5b9a\u3002",
+ "title": "Hive \u767b\u5165\u8cc7\u8a0a"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "scan_interval": "\u6383\u63cf\u9593\u8ddd\uff08\u79d2\uff09"
+ },
+ "description": "\u66f4\u65b0\u6383\u63cf\u9593\u8ddd\u4ee5\u66f4\u983b\u7e41\u7372\u53d6\u66f4\u65b0\u3002",
+ "title": "Hive \u9078\u9805"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py
index 693fd6f322b8e8..0df10a9ed22cdb 100644
--- a/homeassistant/components/hive/water_heater.py
+++ b/homeassistant/components/hive/water_heater.py
@@ -1,4 +1,9 @@
"""Support for hive water heaters."""
+
+from datetime import timedelta
+
+import voluptuous as vol
+
from homeassistant.components.water_heater import (
STATE_ECO,
STATE_OFF,
@@ -7,26 +12,61 @@
WaterHeaterEntity,
)
from homeassistant.const import TEMP_CELSIUS
-
-from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system
+from homeassistant.helpers import config_validation as cv, entity_platform
+
+from . import HiveEntity, refresh_system
+from .const import (
+ ATTR_ONOFF,
+ ATTR_TIME_PERIOD,
+ DOMAIN,
+ SERVICE_BOOST_HOT_WATER,
+ WATER_HEATER_MODES,
+)
SUPPORT_FLAGS_HEATER = SUPPORT_OPERATION_MODE
+HOTWATER_NAME = "Hot Water"
+PARALLEL_UPDATES = 0
+SCAN_INTERVAL = timedelta(seconds=15)
+HIVE_TO_HASS_STATE = {
+ "SCHEDULE": STATE_ECO,
+ "ON": STATE_ON,
+ "OFF": STATE_OFF,
+}
+
+HASS_TO_HIVE_STATE = {
+ STATE_ECO: "SCHEDULE",
+ STATE_ON: "MANUAL",
+ STATE_OFF: "OFF",
+}
-HIVE_TO_HASS_STATE = {"SCHEDULE": STATE_ECO, "ON": STATE_ON, "OFF": STATE_OFF}
-HASS_TO_HIVE_STATE = {STATE_ECO: "SCHEDULE", STATE_ON: "ON", STATE_OFF: "OFF"}
SUPPORT_WATER_HEATER = [STATE_ECO, STATE_ON, STATE_OFF]
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Hive water heater devices."""
- if discovery_info is None:
- return
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up Hive thermostat based on a config entry."""
+
+ hive = hass.data[DOMAIN][entry.entry_id]
+ devices = hive.session.deviceList.get("water_heater")
+ entities = []
+ if devices:
+ for dev in devices:
+ entities.append(HiveWaterHeater(hive, dev))
+ async_add_entities(entities, True)
+
+ platform = entity_platform.current_platform.get()
- session = hass.data.get(DATA_HIVE)
- devs = []
- for dev in discovery_info:
- devs.append(HiveWaterHeater(session, dev))
- add_entities(devs)
+ platform.async_register_entity_service(
+ SERVICE_BOOST_HOT_WATER,
+ {
+ vol.Optional(ATTR_TIME_PERIOD, default="00:30:00"): vol.All(
+ cv.time_period,
+ cv.positive_timedelta,
+ lambda td: td.total_seconds() // 60,
+ ),
+ vol.Required(ATTR_ONOFF): vol.In(WATER_HEATER_MODES),
+ },
+ "async_hot_water_boost",
+ )
class HiveWaterHeater(HiveEntity, WaterHeaterEntity):
@@ -40,7 +80,14 @@ def unique_id(self):
@property
def device_info(self):
"""Return device information."""
- return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name}
+ return {
+ "identifiers": {(DOMAIN, self.device["device_id"])},
+ "name": self.device["device_name"],
+ "model": self.device["deviceData"]["model"],
+ "manufacturer": self.device["deviceData"]["manufacturer"],
+ "sw_version": self.device["deviceData"]["version"],
+ "via_device": (DOMAIN, self.device["parentDevice"]),
+ }
@property
def supported_features(self):
@@ -50,9 +97,12 @@ def supported_features(self):
@property
def name(self):
"""Return the name of the water heater."""
- if self.node_name is None:
- self.node_name = "Hot Water"
- return self.node_name
+ return HOTWATER_NAME
+
+ @property
+ def available(self):
+ """Return if the device is available."""
+ return self.device["deviceData"]["online"]
@property
def temperature_unit(self):
@@ -62,7 +112,7 @@ def temperature_unit(self):
@property
def current_operation(self):
"""Return current operation."""
- return HIVE_TO_HASS_STATE[self.session.hotwater.get_mode(self.node_id)]
+ return HIVE_TO_HASS_STATE[self.device["status"]["current_operation"]]
@property
def operation_list(self):
@@ -70,11 +120,30 @@ def operation_list(self):
return SUPPORT_WATER_HEATER
@refresh_system
- def set_operation_mode(self, operation_mode):
+ async def async_turn_on(self, **kwargs):
+ """Turn on hotwater."""
+ await self.hive.hotwater.setMode(self.device, "MANUAL")
+
+ @refresh_system
+ async def async_turn_off(self, **kwargs):
+ """Turn on hotwater."""
+ await self.hive.hotwater.setMode(self.device, "OFF")
+
+ @refresh_system
+ async def async_set_operation_mode(self, operation_mode):
"""Set operation mode."""
new_mode = HASS_TO_HIVE_STATE[operation_mode]
- self.session.hotwater.set_mode(self.node_id, new_mode)
+ await self.hive.hotwater.setMode(self.device, new_mode)
- def update(self):
+ @refresh_system
+ async def async_hot_water_boost(self, time_period, on_off):
+ """Handle the service call."""
+ if on_off == "on":
+ await self.hive.hotwater.turnBoostOn(self.device, time_period)
+ elif on_off == "off":
+ await self.hive.hotwater.turnBoostOff(self.device)
+
+ async def async_update(self):
"""Update all Node data from Hive."""
- self.session.core.update_data(self.node_id)
+ await self.hive.session.updateData(self.device)
+ self.device = await self.hive.hotwater.getWaterHeater(self.device)
diff --git a/homeassistant/components/hlk_sw16/translations/de.json b/homeassistant/components/hlk_sw16/translations/de.json
index 94b8d6526d13ba..625c7372347a61 100644
--- a/homeassistant/components/hlk_sw16/translations/de.json
+++ b/homeassistant/components/hlk_sw16/translations/de.json
@@ -1,11 +1,17 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
"error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"step": {
"user": {
"data": {
+ "host": "Host",
"password": "Passwort",
"username": "Benutzername"
}
diff --git a/homeassistant/components/hlk_sw16/translations/hu.json b/homeassistant/components/hlk_sw16/translations/hu.json
index 3b2d79a34a77e2..0abcc301f0c854 100644
--- a/homeassistant/components/hlk_sw16/translations/hu.json
+++ b/homeassistant/components/hlk_sw16/translations/hu.json
@@ -2,6 +2,20 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/hlk_sw16/translations/id.json b/homeassistant/components/hlk_sw16/translations/id.json
new file mode 100644
index 00000000000000..ed8fde321061cf
--- /dev/null
+++ b/homeassistant/components/hlk_sw16/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hlk_sw16/translations/ko.json b/homeassistant/components/hlk_sw16/translations/ko.json
new file mode 100644
index 00000000000000..9ba063c37ddf46
--- /dev/null
+++ b/homeassistant/components/hlk_sw16/translations/ko.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hlk_sw16/translations/nl.json b/homeassistant/components/hlk_sw16/translations/nl.json
index 0569c39321a24b..8ad15260b0de5c 100644
--- a/homeassistant/components/hlk_sw16/translations/nl.json
+++ b/homeassistant/components/hlk_sw16/translations/nl.json
@@ -4,6 +4,7 @@
"already_configured": "Apparaat is al geconfigureerd"
},
"error": {
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
diff --git a/homeassistant/components/hlk_sw16/translations/ru.json b/homeassistant/components/hlk_sw16/translations/ru.json
index 6f71ee41376162..9b02cafd466691 100644
--- a/homeassistant/components/hlk_sw16/translations/ru.json
+++ b/homeassistant/components/hlk_sw16/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
@@ -13,7 +13,7 @@
"data": {
"host": "\u0425\u043e\u0441\u0442",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
}
}
}
diff --git a/homeassistant/components/hlk_sw16/translations/tr.json b/homeassistant/components/hlk_sw16/translations/tr.json
new file mode 100644
index 00000000000000..40c9c39b967721
--- /dev/null
+++ b/homeassistant/components/hlk_sw16/translations/tr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hlk_sw16/translations/uk.json b/homeassistant/components/hlk_sw16/translations/uk.json
new file mode 100644
index 00000000000000..2df11f744559db
--- /dev/null
+++ b/homeassistant/components/hlk_sw16/translations/uk.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py
index 301bd1976e66a3..baf4fd17f85b72 100644
--- a/homeassistant/components/home_connect/__init__.py
+++ b/homeassistant/components/home_connect/__init__.py
@@ -71,9 +71,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await update_all_devices(hass, entry)
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -84,8 +84,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py
index 8db8afa3a6bb5c..da5f1df20c6664 100644
--- a/homeassistant/components/home_connect/api.py
+++ b/homeassistant/components/home_connect/api.py
@@ -7,12 +7,29 @@
from homeconnect.api import HomeConnectError
from homeassistant import config_entries, core
-from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE, TIME_SECONDS
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ ATTR_ICON,
+ CONF_DEVICE,
+ CONF_ENTITIES,
+ DEVICE_CLASS_TIMESTAMP,
+ PERCENTAGE,
+ TIME_SECONDS,
+)
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.dispatcher import dispatcher_send
from .const import (
+ ATTR_AMBIENT,
+ ATTR_DESC,
+ ATTR_DEVICE,
+ ATTR_KEY,
+ ATTR_SENSOR_TYPE,
+ ATTR_SIGN,
+ ATTR_UNIT,
+ ATTR_VALUE,
BSH_ACTIVE_PROGRAM,
+ BSH_OPERATION_STATE,
BSH_POWER_OFF,
BSH_POWER_STANDBY,
SIGNAL_UPDATE_ENTITIES,
@@ -71,7 +88,9 @@ def get_devices(self):
else:
_LOGGER.warning("Appliance type %s not implemented", app.type)
continue
- devices.append({"device": device, "entities": device.get_entity_info()})
+ devices.append(
+ {CONF_DEVICE: device, CONF_ENTITIES: device.get_entity_info()}
+ )
self.devices = devices
return devices
@@ -103,8 +122,10 @@ def initialize(self):
except (HomeConnectError, ValueError):
_LOGGER.debug("Unable to fetch active programs. Probably offline")
program_active = None
- if program_active and "key" in program_active:
- self.appliance.status[BSH_ACTIVE_PROGRAM] = {"value": program_active["key"]}
+ if program_active and ATTR_KEY in program_active:
+ self.appliance.status[BSH_ACTIVE_PROGRAM] = {
+ ATTR_VALUE: program_active[ATTR_KEY]
+ }
self.appliance.listen_events(callback=self.event_callback)
def event_callback(self, appliance):
@@ -129,7 +150,7 @@ def get_program_switches(self):
There will be one switch for each program.
"""
programs = self.get_programs_available()
- return [{"device": self, "program_name": p["name"]} for p in programs]
+ return [{ATTR_DEVICE: self, "program_name": p["name"]} for p in programs]
def get_program_sensors(self):
"""Get a dictionary with info about program sensors.
@@ -144,27 +165,47 @@ def get_program_sensors(self):
}
return [
{
- "device": self,
- "desc": k,
- "unit": unit,
- "key": "BSH.Common.Option.{}".format(k.replace(" ", "")),
- "icon": icon,
- "device_class": device_class,
- "sign": sign,
+ ATTR_DEVICE: self,
+ ATTR_DESC: k,
+ ATTR_UNIT: unit,
+ ATTR_KEY: "BSH.Common.Option.{}".format(k.replace(" ", "")),
+ ATTR_ICON: icon,
+ ATTR_DEVICE_CLASS: device_class,
+ ATTR_SIGN: sign,
}
for k, (unit, icon, device_class, sign) in sensors.items()
]
+class DeviceWithOpState(HomeConnectDevice):
+ """Device that has an operation state sensor."""
+
+ def get_opstate_sensor(self):
+ """Get a list with info about operation state sensors."""
+
+ return [
+ {
+ ATTR_DEVICE: self,
+ ATTR_DESC: "Operation State",
+ ATTR_UNIT: None,
+ ATTR_KEY: BSH_OPERATION_STATE,
+ ATTR_ICON: "mdi:state-machine",
+ ATTR_DEVICE_CLASS: None,
+ ATTR_SIGN: 1,
+ }
+ ]
+
+
class DeviceWithDoor(HomeConnectDevice):
"""Device that has a door sensor."""
def get_door_entity(self):
"""Get a dictionary with info about the door binary sensor."""
return {
- "device": self,
- "desc": "Door",
- "device_class": "door",
+ ATTR_DEVICE: self,
+ ATTR_DESC: "Door",
+ ATTR_SENSOR_TYPE: "door",
+ ATTR_DEVICE_CLASS: "door",
}
@@ -173,11 +214,7 @@ class DeviceWithLight(HomeConnectDevice):
def get_light_entity(self):
"""Get a dictionary with info about the lighting."""
- return {
- "device": self,
- "desc": "Light",
- "ambient": None,
- }
+ return {ATTR_DEVICE: self, ATTR_DESC: "Light", ATTR_AMBIENT: None}
class DeviceWithAmbientLight(HomeConnectDevice):
@@ -185,14 +222,40 @@ class DeviceWithAmbientLight(HomeConnectDevice):
def get_ambientlight_entity(self):
"""Get a dictionary with info about the ambient lighting."""
+ return {ATTR_DEVICE: self, ATTR_DESC: "AmbientLight", ATTR_AMBIENT: True}
+
+
+class DeviceWithRemoteControl(HomeConnectDevice):
+ """Device that has Remote Control binary sensor."""
+
+ def get_remote_control(self):
+ """Get a dictionary with info about the remote control sensor."""
+ return {
+ ATTR_DEVICE: self,
+ ATTR_DESC: "Remote Control",
+ ATTR_SENSOR_TYPE: "remote_control",
+ }
+
+
+class DeviceWithRemoteStart(HomeConnectDevice):
+ """Device that has a Remote Start binary sensor."""
+
+ def get_remote_start(self):
+ """Get a dictionary with info about the remote start sensor."""
return {
- "device": self,
- "desc": "AmbientLight",
- "ambient": True,
+ ATTR_DEVICE: self,
+ ATTR_DESC: "Remote Start",
+ ATTR_SENSOR_TYPE: "remote_start",
}
-class Dryer(DeviceWithDoor, DeviceWithPrograms):
+class Dryer(
+ DeviceWithDoor,
+ DeviceWithOpState,
+ DeviceWithPrograms,
+ DeviceWithRemoteControl,
+ DeviceWithRemoteStart,
+):
"""Dryer class."""
PROGRAMS = [
@@ -217,16 +280,26 @@ class Dryer(DeviceWithDoor, DeviceWithPrograms):
def get_entity_info(self):
"""Get a dictionary with infos about the associated entities."""
door_entity = self.get_door_entity()
+ remote_control = self.get_remote_control()
+ remote_start = self.get_remote_start()
+ op_state_sensor = self.get_opstate_sensor()
program_sensors = self.get_program_sensors()
program_switches = self.get_program_switches()
return {
- "binary_sensor": [door_entity],
+ "binary_sensor": [door_entity, remote_control, remote_start],
"switch": program_switches,
- "sensor": program_sensors,
+ "sensor": program_sensors + op_state_sensor,
}
-class Dishwasher(DeviceWithDoor, DeviceWithAmbientLight, DeviceWithPrograms):
+class Dishwasher(
+ DeviceWithDoor,
+ DeviceWithAmbientLight,
+ DeviceWithOpState,
+ DeviceWithPrograms,
+ DeviceWithRemoteControl,
+ DeviceWithRemoteStart,
+):
"""Dishwasher class."""
PROGRAMS = [
@@ -257,16 +330,25 @@ class Dishwasher(DeviceWithDoor, DeviceWithAmbientLight, DeviceWithPrograms):
def get_entity_info(self):
"""Get a dictionary with infos about the associated entities."""
door_entity = self.get_door_entity()
+ remote_control = self.get_remote_control()
+ remote_start = self.get_remote_start()
+ op_state_sensor = self.get_opstate_sensor()
program_sensors = self.get_program_sensors()
program_switches = self.get_program_switches()
return {
- "binary_sensor": [door_entity],
+ "binary_sensor": [door_entity, remote_control, remote_start],
"switch": program_switches,
- "sensor": program_sensors,
+ "sensor": program_sensors + op_state_sensor,
}
-class Oven(DeviceWithDoor, DeviceWithPrograms):
+class Oven(
+ DeviceWithDoor,
+ DeviceWithOpState,
+ DeviceWithPrograms,
+ DeviceWithRemoteControl,
+ DeviceWithRemoteStart,
+):
"""Oven class."""
PROGRAMS = [
@@ -282,16 +364,25 @@ class Oven(DeviceWithDoor, DeviceWithPrograms):
def get_entity_info(self):
"""Get a dictionary with infos about the associated entities."""
door_entity = self.get_door_entity()
+ remote_control = self.get_remote_control()
+ remote_start = self.get_remote_start()
+ op_state_sensor = self.get_opstate_sensor()
program_sensors = self.get_program_sensors()
program_switches = self.get_program_switches()
return {
- "binary_sensor": [door_entity],
+ "binary_sensor": [door_entity, remote_control, remote_start],
"switch": program_switches,
- "sensor": program_sensors,
+ "sensor": program_sensors + op_state_sensor,
}
-class Washer(DeviceWithDoor, DeviceWithPrograms):
+class Washer(
+ DeviceWithDoor,
+ DeviceWithOpState,
+ DeviceWithPrograms,
+ DeviceWithRemoteControl,
+ DeviceWithRemoteStart,
+):
"""Washer class."""
PROGRAMS = [
@@ -321,16 +412,19 @@ class Washer(DeviceWithDoor, DeviceWithPrograms):
def get_entity_info(self):
"""Get a dictionary with infos about the associated entities."""
door_entity = self.get_door_entity()
+ remote_control = self.get_remote_control()
+ remote_start = self.get_remote_start()
+ op_state_sensor = self.get_opstate_sensor()
program_sensors = self.get_program_sensors()
program_switches = self.get_program_switches()
return {
- "binary_sensor": [door_entity],
+ "binary_sensor": [door_entity, remote_control, remote_start],
"switch": program_switches,
- "sensor": program_sensors,
+ "sensor": program_sensors + op_state_sensor,
}
-class CoffeeMaker(DeviceWithPrograms):
+class CoffeeMaker(DeviceWithOpState, DeviceWithPrograms, DeviceWithRemoteStart):
"""Coffee maker class."""
PROGRAMS = [
@@ -354,12 +448,25 @@ class CoffeeMaker(DeviceWithPrograms):
def get_entity_info(self):
"""Get a dictionary with infos about the associated entities."""
+ remote_start = self.get_remote_start()
+ op_state_sensor = self.get_opstate_sensor()
program_sensors = self.get_program_sensors()
program_switches = self.get_program_switches()
- return {"switch": program_switches, "sensor": program_sensors}
+ return {
+ "binary_sensor": [remote_start],
+ "switch": program_switches,
+ "sensor": program_sensors + op_state_sensor,
+ }
-class Hood(DeviceWithLight, DeviceWithAmbientLight, DeviceWithPrograms):
+class Hood(
+ DeviceWithLight,
+ DeviceWithAmbientLight,
+ DeviceWithOpState,
+ DeviceWithPrograms,
+ DeviceWithRemoteControl,
+ DeviceWithRemoteStart,
+):
"""Hood class."""
PROGRAMS = [
@@ -370,13 +477,17 @@ class Hood(DeviceWithLight, DeviceWithAmbientLight, DeviceWithPrograms):
def get_entity_info(self):
"""Get a dictionary with infos about the associated entities."""
+ remote_control = self.get_remote_control()
+ remote_start = self.get_remote_start()
light_entity = self.get_light_entity()
ambientlight_entity = self.get_ambientlight_entity()
+ op_state_sensor = self.get_opstate_sensor()
program_sensors = self.get_program_sensors()
program_switches = self.get_program_switches()
return {
+ "binary_sensor": [remote_control, remote_start],
"switch": program_switches,
- "sensor": program_sensors,
+ "sensor": program_sensors + op_state_sensor,
"light": [light_entity, ambientlight_entity],
}
@@ -390,13 +501,19 @@ def get_entity_info(self):
return {"binary_sensor": [door_entity]}
-class Hob(DeviceWithPrograms):
+class Hob(DeviceWithOpState, DeviceWithPrograms, DeviceWithRemoteControl):
"""Hob class."""
PROGRAMS = [{"name": "Cooking.Hob.Program.PowerLevelMode"}]
def get_entity_info(self):
"""Get a dictionary with infos about the associated entities."""
+ remote_control = self.get_remote_control()
+ op_state_sensor = self.get_opstate_sensor()
program_sensors = self.get_program_sensors()
program_switches = self.get_program_switches()
- return {"switch": program_switches, "sensor": program_sensors}
+ return {
+ "binary_sensor": [remote_control],
+ "switch": program_switches,
+ "sensor": program_sensors + op_state_sensor,
+ }
diff --git a/homeassistant/components/home_connect/binary_sensor.py b/homeassistant/components/home_connect/binary_sensor.py
index 4810231b432075..4dc21f2fd582c8 100644
--- a/homeassistant/components/home_connect/binary_sensor.py
+++ b/homeassistant/components/home_connect/binary_sensor.py
@@ -2,8 +2,18 @@
import logging
from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.const import CONF_ENTITIES
-from .const import BSH_DOOR_STATE, DOMAIN
+from .const import (
+ ATTR_VALUE,
+ BSH_DOOR_STATE,
+ BSH_DOOR_STATE_CLOSED,
+ BSH_DOOR_STATE_LOCKED,
+ BSH_DOOR_STATE_OPEN,
+ BSH_REMOTE_CONTROL_ACTIVATION_STATE,
+ BSH_REMOTE_START_ALLOWANCE_STATE,
+ DOMAIN,
+)
from .entity import HomeConnectEntity
_LOGGER = logging.getLogger(__name__)
@@ -16,7 +26,7 @@ def get_entities():
entities = []
hc_api = hass.data[DOMAIN][config_entry.entry_id]
for device_dict in hc_api.devices:
- entity_dicts = device_dict.get("entities", {}).get("binary_sensor", [])
+ entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("binary_sensor", [])
entities += [HomeConnectBinarySensor(**d) for d in entity_dicts]
return entities
@@ -26,11 +36,24 @@ def get_entities():
class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity):
"""Binary sensor for Home Connect."""
- def __init__(self, device, desc, device_class):
+ def __init__(self, device, desc, sensor_type, device_class=None):
"""Initialize the entity."""
super().__init__(device, desc)
- self._device_class = device_class
self._state = None
+ self._device_class = device_class
+ self._type = sensor_type
+ if self._type == "door":
+ self._update_key = BSH_DOOR_STATE
+ self._false_value_list = (BSH_DOOR_STATE_CLOSED, BSH_DOOR_STATE_LOCKED)
+ self._true_value_list = [BSH_DOOR_STATE_OPEN]
+ elif self._type == "remote_control":
+ self._update_key = BSH_REMOTE_CONTROL_ACTIVATION_STATE
+ self._false_value_list = [False]
+ self._true_value_list = [True]
+ elif self._type == "remote_start":
+ self._update_key = BSH_REMOTE_START_ALLOWANCE_STATE
+ self._false_value_list = [False]
+ self._true_value_list = [True]
@property
def is_on(self):
@@ -44,18 +67,17 @@ def available(self):
async def async_update(self):
"""Update the binary sensor's status."""
- state = self.device.appliance.status.get(BSH_DOOR_STATE, {})
+ state = self.device.appliance.status.get(self._update_key, {})
if not state:
self._state = None
- elif state.get("value") in [
- "BSH.Common.EnumType.DoorState.Closed",
- "BSH.Common.EnumType.DoorState.Locked",
- ]:
+ elif state.get(ATTR_VALUE) in self._false_value_list:
self._state = False
- elif state.get("value") == "BSH.Common.EnumType.DoorState.Open":
+ elif state.get(ATTR_VALUE) in self._true_value_list:
self._state = True
else:
- _LOGGER.warning("Unexpected value for HomeConnect door state: %s", state)
+ _LOGGER.warning(
+ "Unexpected value for HomeConnect %s state: %s", self._type, state
+ )
self._state = None
_LOGGER.debug("Updated, new state: %s", self._state)
diff --git a/homeassistant/components/home_connect/const.py b/homeassistant/components/home_connect/const.py
index 22ce4dba6768fe..438ee5ace164ab 100644
--- a/homeassistant/components/home_connect/const.py
+++ b/homeassistant/components/home_connect/const.py
@@ -11,6 +11,8 @@
BSH_POWER_STANDBY = "BSH.Common.EnumType.PowerState.Standby"
BSH_ACTIVE_PROGRAM = "BSH.Common.Root.ActiveProgram"
BSH_OPERATION_STATE = "BSH.Common.Status.OperationState"
+BSH_REMOTE_CONTROL_ACTIVATION_STATE = "BSH.Common.Status.RemoteControlActive"
+BSH_REMOTE_START_ALLOWANCE_STATE = "BSH.Common.Status.RemoteControlStartAllowed"
COOKING_LIGHTING = "Cooking.Common.Setting.Lighting"
COOKING_LIGHTING_BRIGHTNESS = "Cooking.Common.Setting.LightingBrightness"
@@ -24,5 +26,17 @@
BSH_AMBIENT_LIGHT_CUSTOM_COLOR = "BSH.Common.Setting.AmbientLightCustomColor"
BSH_DOOR_STATE = "BSH.Common.Status.DoorState"
+BSH_DOOR_STATE_CLOSED = "BSH.Common.EnumType.DoorState.Closed"
+BSH_DOOR_STATE_LOCKED = "BSH.Common.EnumType.DoorState.Locked"
+BSH_DOOR_STATE_OPEN = "BSH.Common.EnumType.DoorState.Open"
SIGNAL_UPDATE_ENTITIES = "home_connect.update_entities"
+
+ATTR_AMBIENT = "ambient"
+ATTR_DESC = "desc"
+ATTR_DEVICE = "device"
+ATTR_KEY = "key"
+ATTR_SENSOR_TYPE = "sensor_type"
+ATTR_SIGN = "sign"
+ATTR_UNIT = "unit"
+ATTR_VALUE = "value"
diff --git a/homeassistant/components/home_connect/light.py b/homeassistant/components/home_connect/light.py
index 814e3b0ed03a17..dc176ba90f2234 100644
--- a/homeassistant/components/home_connect/light.py
+++ b/homeassistant/components/home_connect/light.py
@@ -11,9 +11,11 @@
SUPPORT_COLOR,
LightEntity,
)
+from homeassistant.const import CONF_ENTITIES
import homeassistant.util.color as color_util
from .const import (
+ ATTR_VALUE,
BSH_AMBIENT_LIGHT_BRIGHTNESS,
BSH_AMBIENT_LIGHT_COLOR,
BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR,
@@ -36,7 +38,7 @@ def get_entities():
entities = []
hc_api = hass.data[DOMAIN][config_entry.entry_id]
for device_dict in hc_api.devices:
- entity_dicts = device_dict.get("entities", {}).get("light", [])
+ entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("light", [])
entity_list = [HomeConnectLight(**d) for d in entity_dicts]
entities += entity_list
return entities
@@ -93,9 +95,7 @@ async def async_turn_on(self, **kwargs):
_LOGGER.debug("Switching ambient light on for: %s", self.name)
try:
await self.hass.async_add_executor_job(
- self.device.appliance.set_setting,
- self._key,
- True,
+ self.device.appliance.set_setting, self._key, True
)
except HomeConnectError as err:
_LOGGER.error("Error while trying to turn on ambient light: %s", err)
@@ -135,9 +135,7 @@ async def async_turn_on(self, **kwargs):
brightness = 10 + ceil(kwargs[ATTR_BRIGHTNESS] / 255 * 90)
try:
await self.hass.async_add_executor_job(
- self.device.appliance.set_setting,
- self._brightness_key,
- brightness,
+ self.device.appliance.set_setting, self._brightness_key, brightness
)
except HomeConnectError as err:
_LOGGER.error("Error while trying set the brightness: %s", err)
@@ -145,9 +143,7 @@ async def async_turn_on(self, **kwargs):
_LOGGER.debug("Switching light on for: %s", self.name)
try:
await self.hass.async_add_executor_job(
- self.device.appliance.set_setting,
- self._key,
- True,
+ self.device.appliance.set_setting, self._key, True
)
except HomeConnectError as err:
_LOGGER.error("Error while trying to turn on light: %s", err)
@@ -159,9 +155,7 @@ async def async_turn_off(self, **kwargs):
_LOGGER.debug("Switching light off for: %s", self.name)
try:
await self.hass.async_add_executor_job(
- self.device.appliance.set_setting,
- self._key,
- False,
+ self.device.appliance.set_setting, self._key, False
)
except HomeConnectError as err:
_LOGGER.error("Error while trying to turn off light: %s", err)
@@ -169,9 +163,9 @@ async def async_turn_off(self, **kwargs):
async def async_update(self):
"""Update the light's status."""
- if self.device.appliance.status.get(self._key, {}).get("value") is True:
+ if self.device.appliance.status.get(self._key, {}).get(ATTR_VALUE) is True:
self._state = True
- elif self.device.appliance.status.get(self._key, {}).get("value") is False:
+ elif self.device.appliance.status.get(self._key, {}).get(ATTR_VALUE) is False:
self._state = False
else:
self._state = None
@@ -185,7 +179,7 @@ async def async_update(self):
self._hs_color = None
self._brightness = None
else:
- colorvalue = color.get("value")[1:]
+ colorvalue = color.get(ATTR_VALUE)[1:]
rgb = color_util.rgb_hex_to_rgb_list(colorvalue)
hsv = color_util.color_RGB_to_hsv(rgb[0], rgb[1], rgb[2])
self._hs_color = [hsv[0], hsv[1]]
@@ -197,5 +191,5 @@ async def async_update(self):
if brightness is None:
self._brightness = None
else:
- self._brightness = ceil((brightness.get("value") - 10) * 255 / 90)
+ self._brightness = ceil((brightness.get(ATTR_VALUE) - 10) * 255 / 90)
_LOGGER.debug("Updated, new brightness: %s", self._brightness)
diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py
index 0ae5a9fcd3693a..463de6cda51458 100644
--- a/homeassistant/components/home_connect/sensor.py
+++ b/homeassistant/components/home_connect/sensor.py
@@ -3,10 +3,11 @@
from datetime import timedelta
import logging
-from homeassistant.const import DEVICE_CLASS_TIMESTAMP
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.const import CONF_ENTITIES, DEVICE_CLASS_TIMESTAMP
import homeassistant.util.dt as dt_util
-from .const import DOMAIN
+from .const import ATTR_VALUE, BSH_OPERATION_STATE, DOMAIN
from .entity import HomeConnectEntity
_LOGGER = logging.getLogger(__name__)
@@ -20,14 +21,14 @@ def get_entities():
entities = []
hc_api = hass.data[DOMAIN][config_entry.entry_id]
for device_dict in hc_api.devices:
- entity_dicts = device_dict.get("entities", {}).get("sensor", [])
+ entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("sensor", [])
entities += [HomeConnectSensor(**d) for d in entity_dicts]
return entities
async_add_entities(await hass.async_add_executor_job(get_entities), True)
-class HomeConnectSensor(HomeConnectEntity):
+class HomeConnectSensor(HomeConnectEntity, SensorEntity):
"""Sensor class for Home Connect."""
def __init__(self, device, desc, key, unit, icon, device_class, sign=1):
@@ -51,13 +52,13 @@ def available(self):
return self._state is not None
async def async_update(self):
- """Update the sensos status."""
+ """Update the sensor's status."""
status = self.device.appliance.status
if self._key not in status:
self._state = None
else:
if self.device_class == DEVICE_CLASS_TIMESTAMP:
- if "value" not in status[self._key]:
+ if ATTR_VALUE not in status[self._key]:
self._state = None
elif (
self._state is not None
@@ -68,12 +69,17 @@ async def async_update(self):
# already past it, set state to None.
self._state = None
else:
- seconds = self._sign * float(status[self._key]["value"])
+ seconds = self._sign * float(status[self._key][ATTR_VALUE])
self._state = (
dt_util.utcnow() + timedelta(seconds=seconds)
).isoformat()
else:
- self._state = status[self._key].get("value")
+ self._state = status[self._key].get(ATTR_VALUE)
+ if self._key == BSH_OPERATION_STATE:
+ # Value comes back as an enum, we only really care about the
+ # last part, so split it off
+ # https://developer.home-connect.com/docs/status/operation_state
+ self._state = self._state.split(".")[-1]
_LOGGER.debug("Updated, new state: %s", self._state)
@property
diff --git a/homeassistant/components/home_connect/switch.py b/homeassistant/components/home_connect/switch.py
index 346e739e5ff562..5e12d724a5e3ce 100644
--- a/homeassistant/components/home_connect/switch.py
+++ b/homeassistant/components/home_connect/switch.py
@@ -4,8 +4,10 @@
from homeconnect.api import HomeConnectError
from homeassistant.components.switch import SwitchEntity
+from homeassistant.const import CONF_DEVICE, CONF_ENTITIES
from .const import (
+ ATTR_VALUE,
BSH_ACTIVE_PROGRAM,
BSH_OPERATION_STATE,
BSH_POWER_ON,
@@ -25,9 +27,9 @@ def get_entities():
entities = []
hc_api = hass.data[DOMAIN][config_entry.entry_id]
for device_dict in hc_api.devices:
- entity_dicts = device_dict.get("entities", {}).get("switch", [])
+ entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("switch", [])
entity_list = [HomeConnectProgramSwitch(**d) for d in entity_dicts]
- entity_list += [HomeConnectPowerSwitch(device_dict["device"])]
+ entity_list += [HomeConnectPowerSwitch(device_dict[CONF_DEVICE])]
entities += entity_list
return entities
@@ -78,7 +80,7 @@ async def async_turn_off(self, **kwargs):
async def async_update(self):
"""Update the switch's status."""
state = self.device.appliance.status.get(BSH_ACTIVE_PROGRAM, {})
- if state.get("value") == self.program_name:
+ if state.get(ATTR_VALUE) == self.program_name:
self._state = True
else:
self._state = False
@@ -103,9 +105,7 @@ async def async_turn_on(self, **kwargs):
_LOGGER.debug("Tried to switch on %s", self.name)
try:
await self.hass.async_add_executor_job(
- self.device.appliance.set_setting,
- BSH_POWER_STATE,
- BSH_POWER_ON,
+ self.device.appliance.set_setting, BSH_POWER_STATE, BSH_POWER_ON
)
except HomeConnectError as err:
_LOGGER.error("Error while trying to turn on device: %s", err)
@@ -129,17 +129,17 @@ async def async_turn_off(self, **kwargs):
async def async_update(self):
"""Update the switch's status."""
if (
- self.device.appliance.status.get(BSH_POWER_STATE, {}).get("value")
+ self.device.appliance.status.get(BSH_POWER_STATE, {}).get(ATTR_VALUE)
== BSH_POWER_ON
):
self._state = True
elif (
- self.device.appliance.status.get(BSH_POWER_STATE, {}).get("value")
+ self.device.appliance.status.get(BSH_POWER_STATE, {}).get(ATTR_VALUE)
== self.device.power_off_state
):
self._state = False
elif self.device.appliance.status.get(BSH_OPERATION_STATE, {}).get(
- "value", None
+ ATTR_VALUE, None
) in [
"BSH.Common.EnumType.OperationState.Ready",
"BSH.Common.EnumType.OperationState.DelayedStart",
@@ -151,7 +151,7 @@ async def async_update(self):
]:
self._state = True
elif (
- self.device.appliance.status.get(BSH_OPERATION_STATE, {}).get("value")
+ self.device.appliance.status.get(BSH_OPERATION_STATE, {}).get(ATTR_VALUE)
== "BSH.Common.EnumType.OperationState.Inactive"
):
self._state = False
diff --git a/homeassistant/components/home_connect/translations/de.json b/homeassistant/components/home_connect/translations/de.json
index 05204c35c4120f..2454c039361105 100644
--- a/homeassistant/components/home_connect/translations/de.json
+++ b/homeassistant/components/home_connect/translations/de.json
@@ -1,14 +1,15 @@
{
"config": {
"abort": {
- "missing_configuration": "Die Komponente Home Connect ist nicht konfiguriert. Bitte folgen Sie der Dokumentation."
+ "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.",
+ "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})."
},
"create_entry": {
- "default": "Erfolgreich mit Home Connect authentifiziert."
+ "default": "Erfolgreich authentifiziert"
},
"step": {
"pick_implementation": {
- "title": "Authentifizierungsmethode ausw\u00e4hlen"
+ "title": "W\u00e4hle die Authentifizierungsmethode"
}
}
}
diff --git a/homeassistant/components/home_connect/translations/hu.json b/homeassistant/components/home_connect/translations/hu.json
index f02fb97b9df645..aa43f65b520e9e 100644
--- a/homeassistant/components/home_connect/translations/hu.json
+++ b/homeassistant/components/home_connect/translations/hu.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.",
+ "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz."
+ },
"create_entry": {
- "default": "Sikeres autentik\u00e1ci\u00f3"
+ "default": "Sikeres hiteles\u00edt\u00e9s"
},
"step": {
"pick_implementation": {
diff --git a/homeassistant/components/home_connect/translations/id.json b/homeassistant/components/home_connect/translations/id.json
new file mode 100644
index 00000000000000..bc6089beb2b819
--- /dev/null
+++ b/homeassistant/components/home_connect/translations/id.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.",
+ "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})"
+ },
+ "create_entry": {
+ "default": "Berhasil diautentikasi"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Pilih Metode Autentikasi"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_connect/translations/ko.json b/homeassistant/components/home_connect/translations/ko.json
index 8d1f5554e7f880..425968d1460cd3 100644
--- a/homeassistant/components/home_connect/translations/ko.json
+++ b/homeassistant/components/home_connect/translations/ko.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "missing_configuration": "Home Connect \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
- "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})"
+ "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
+ "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694."
},
"create_entry": {
- "default": "Home Connect \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
"pick_implementation": {
diff --git a/homeassistant/components/home_connect/translations/nl.json b/homeassistant/components/home_connect/translations/nl.json
index 41b27cc387fdca..25a812096074dd 100644
--- a/homeassistant/components/home_connect/translations/nl.json
+++ b/homeassistant/components/home_connect/translations/nl.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "missing_configuration": "Het component is niet geconfigureerd. Volg de documentatie."
+ "missing_configuration": "Het component is niet geconfigureerd. Volg de documentatie.",
+ "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})"
},
"create_entry": {
"default": "Succesvol geverifieerd"
diff --git a/homeassistant/components/home_connect/translations/uk.json b/homeassistant/components/home_connect/translations/uk.json
new file mode 100644
index 00000000000000..247ffd16713cbf
--- /dev/null
+++ b/homeassistant/components/home_connect/translations/uk.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.",
+ "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443."
+ },
+ "create_entry": {
+ "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e."
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py
new file mode 100644
index 00000000000000..e559cd030b355c
--- /dev/null
+++ b/homeassistant/components/home_plus_control/__init__.py
@@ -0,0 +1,179 @@
+"""The Legrand Home+ Control integration."""
+import asyncio
+from datetime import timedelta
+import logging
+
+import async_timeout
+from homepluscontrol.homeplusapi import HomePlusControlApiError
+import voluptuous as vol
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import (
+ config_entry_oauth2_flow,
+ config_validation as cv,
+ dispatcher,
+)
+from homeassistant.helpers.device_registry import async_get as async_get_device_registry
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from . import config_flow, helpers
+from .api import HomePlusControlAsyncApi
+from .const import (
+ API,
+ CONF_SUBSCRIPTION_KEY,
+ DATA_COORDINATOR,
+ DISPATCHER_REMOVERS,
+ DOMAIN,
+ ENTITY_UIDS,
+ SIGNAL_ADD_ENTITIES,
+)
+
+# Configuration schema for component in configuration.yaml
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Required(CONF_CLIENT_ID): cv.string,
+ vol.Required(CONF_CLIENT_SECRET): cv.string,
+ vol.Required(CONF_SUBSCRIPTION_KEY): cv.string,
+ }
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+# The Legrand Home+ Control platform is currently limited to "switch" entities
+PLATFORMS = ["switch"]
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup(hass: HomeAssistant, config: dict) -> bool:
+ """Set up the Legrand Home+ Control component from configuration.yaml."""
+ hass.data[DOMAIN] = {}
+
+ if DOMAIN not in config:
+ return True
+
+ # Register the implementation from the config information
+ config_flow.HomePlusControlFlowHandler.async_register_implementation(
+ hass,
+ helpers.HomePlusControlOAuth2Implementation(hass, config[DOMAIN]),
+ )
+
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+ """Set up Legrand Home+ Control from a config entry."""
+ hass_entry_data = hass.data[DOMAIN].setdefault(config_entry.entry_id, {})
+
+ # Retrieve the registered implementation
+ implementation = (
+ await config_entry_oauth2_flow.async_get_config_entry_implementation(
+ hass, config_entry
+ )
+ )
+
+ # Using an aiohttp-based API lib, so rely on async framework
+ # Add the API object to the domain's data in HA
+ api = hass_entry_data[API] = HomePlusControlAsyncApi(
+ hass, config_entry, implementation
+ )
+
+ # Set of entity unique identifiers of this integration
+ uids = hass_entry_data[ENTITY_UIDS] = set()
+
+ # Integration dispatchers
+ hass_entry_data[DISPATCHER_REMOVERS] = []
+
+ device_registry = async_get_device_registry(hass)
+
+ # Register the Data Coordinator with the integration
+ async def async_update_data():
+ """Fetch data from API endpoint.
+
+ This is the place to pre-process the data to lookup tables
+ so entities can quickly look up their data.
+ """
+ try:
+ # Note: asyncio.TimeoutError and aiohttp.ClientError are already
+ # handled by the data update coordinator.
+ async with async_timeout.timeout(10):
+ module_data = await api.async_get_modules()
+ except HomePlusControlApiError as err:
+ raise UpdateFailed(
+ f"Error communicating with API: {err} [{type(err)}]"
+ ) from err
+
+ # Remove obsolete entities from Home Assistant
+ entity_uids_to_remove = uids - set(module_data)
+ for uid in entity_uids_to_remove:
+ uids.remove(uid)
+ device = device_registry.async_get_device({(DOMAIN, uid)})
+ device_registry.async_remove_device(device.id)
+
+ # Send out signal for new entity addition to Home Assistant
+ new_entity_uids = set(module_data) - uids
+ if new_entity_uids:
+ uids.update(new_entity_uids)
+ dispatcher.async_dispatcher_send(
+ hass,
+ SIGNAL_ADD_ENTITIES,
+ new_entity_uids,
+ coordinator,
+ )
+
+ return module_data
+
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ # Name of the data. For logging purposes.
+ name="home_plus_control_module",
+ update_method=async_update_data,
+ # Polling interval. Will only be polled if there are subscribers.
+ update_interval=timedelta(seconds=60),
+ )
+ hass_entry_data[DATA_COORDINATOR] = coordinator
+
+ async def start_platforms():
+ """Continue setting up the platforms."""
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ # Only refresh the coordinator after all platforms are loaded.
+ await coordinator.async_refresh()
+
+ hass.async_create_task(start_platforms())
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+ """Unload the Legrand Home+ Control config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(config_entry, component)
+ for component in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ # Unsubscribe the config_entry signal dispatcher connections
+ dispatcher_removers = hass.data[DOMAIN][config_entry.entry_id].pop(
+ "dispatcher_removers"
+ )
+ for remover in dispatcher_removers:
+ remover()
+
+ # And finally unload the domain config entry data
+ hass.data[DOMAIN].pop(config_entry.entry_id)
+
+ return unload_ok
diff --git a/homeassistant/components/home_plus_control/api.py b/homeassistant/components/home_plus_control/api.py
new file mode 100644
index 00000000000000..d9db95323de7b0
--- /dev/null
+++ b/homeassistant/components/home_plus_control/api.py
@@ -0,0 +1,55 @@
+"""API for Legrand Home+ Control bound to Home Assistant OAuth."""
+from homepluscontrol.homeplusapi import HomePlusControlAPI
+
+from homeassistant import config_entries, core
+from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
+
+from .const import DEFAULT_UPDATE_INTERVALS
+
+
+class HomePlusControlAsyncApi(HomePlusControlAPI):
+ """Legrand Home+ Control object that interacts with the OAuth2-based API of the provider.
+
+ This API is bound the HomeAssistant Config Entry that corresponds to this component.
+
+ Attributes:.
+ hass (HomeAssistant): HomeAssistant core object.
+ config_entry (ConfigEntry): ConfigEntry object that configures this API.
+ implementation (AbstractOAuth2Implementation): OAuth2 implementation that handles AA and
+ token refresh.
+ _oauth_session (OAuth2Session): OAuth2Session object within implementation.
+ """
+
+ def __init__(
+ self,
+ hass: core.HomeAssistant,
+ config_entry: config_entries.ConfigEntry,
+ implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation,
+ ) -> None:
+ """Initialize the HomePlusControlAsyncApi object.
+
+ Initialize the authenticated API for the Legrand Home+ Control component.
+
+ Args:.
+ hass (HomeAssistant): HomeAssistant core object.
+ config_entry (ConfigEntry): ConfigEntry object that configures this API.
+ implementation (AbstractOAuth2Implementation): OAuth2 implementation that handles AA
+ and token refresh.
+ """
+ self._oauth_session = config_entry_oauth2_flow.OAuth2Session(
+ hass, config_entry, implementation
+ )
+
+ # Create the API authenticated client - external library
+ super().__init__(
+ subscription_key=implementation.subscription_key,
+ oauth_client=aiohttp_client.async_get_clientsession(hass),
+ update_intervals=DEFAULT_UPDATE_INTERVALS,
+ )
+
+ async def async_get_access_token(self) -> str:
+ """Return a valid access token."""
+ if not self._oauth_session.valid_token:
+ await self._oauth_session.async_ensure_token_valid()
+
+ return self._oauth_session.token["access_token"]
diff --git a/homeassistant/components/home_plus_control/config_flow.py b/homeassistant/components/home_plus_control/config_flow.py
new file mode 100644
index 00000000000000..ed1686f7af18e4
--- /dev/null
+++ b/homeassistant/components/home_plus_control/config_flow.py
@@ -0,0 +1,32 @@
+"""Config flow for Legrand Home+ Control."""
+import logging
+
+from homeassistant import config_entries
+from homeassistant.helpers import config_entry_oauth2_flow
+
+from .const import DOMAIN
+
+
+class HomePlusControlFlowHandler(
+ config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
+):
+ """Config flow to handle Home+ Control OAuth2 authentication."""
+
+ DOMAIN = DOMAIN
+
+ # Pick the Cloud Poll class
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ @property
+ def logger(self) -> logging.Logger:
+ """Return logger."""
+ return logging.getLogger(__name__)
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow start initiated by the user."""
+ await self.async_set_unique_id(DOMAIN)
+
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
+ return await super().async_step_user(user_input)
diff --git a/homeassistant/components/home_plus_control/const.py b/homeassistant/components/home_plus_control/const.py
new file mode 100644
index 00000000000000..0ebae0bef20df9
--- /dev/null
+++ b/homeassistant/components/home_plus_control/const.py
@@ -0,0 +1,45 @@
+"""Constants for the Legrand Home+ Control integration."""
+API = "api"
+CONF_SUBSCRIPTION_KEY = "subscription_key"
+CONF_PLANT_UPDATE_INTERVAL = "plant_update_interval"
+CONF_PLANT_TOPOLOGY_UPDATE_INTERVAL = "plant_topology_update_interval"
+CONF_MODULE_STATUS_UPDATE_INTERVAL = "module_status_update_interval"
+
+DATA_COORDINATOR = "coordinator"
+DOMAIN = "home_plus_control"
+ENTITY_UIDS = "entity_unique_ids"
+DISPATCHER_REMOVERS = "dispatcher_removers"
+
+# Legrand Model Identifiers - https://developer.legrand.com/documentation/product-cluster-list/#
+HW_TYPE = {
+ "NLC": "NLC - Cable Outlet",
+ "NLF": "NLF - On-Off Dimmer Switch w/o Neutral",
+ "NLP": "NLP - Socket (Connected) Outlet",
+ "NLPM": "NLPM - Mobile Socket Outlet",
+ "NLM": "NLM - Micromodule Switch",
+ "NLV": "NLV - Shutter Switch with Neutral",
+ "NLLV": "NLLV - Shutter Switch with Level Control",
+ "NLL": "NLL - On-Off Toggle Switch with Neutral",
+ "NLT": "NLT - Remote Switch",
+ "NLD": "NLD - Double Gangs On-Off Remote Switch",
+}
+
+# Legrand OAuth2 URIs
+OAUTH2_AUTHORIZE = "https://partners-login.eliotbylegrand.com/authorize"
+OAUTH2_TOKEN = "https://partners-login.eliotbylegrand.com/token"
+
+# The Legrand Home+ Control API has very limited request quotas - at the time of writing, it is
+# limited to 500 calls per day (resets at 00:00) - so we want to keep updates to a minimum.
+DEFAULT_UPDATE_INTERVALS = {
+ # Seconds between API checks for plant information updates. This is expected to change very
+ # little over time because a user's plants (homes) should rarely change.
+ CONF_PLANT_UPDATE_INTERVAL: 7200, # 120 minutes
+ # Seconds between API checks for plant topology updates. This is expected to change little
+ # over time because the modules in the user's plant should be relatively stable.
+ CONF_PLANT_TOPOLOGY_UPDATE_INTERVAL: 3600, # 60 minutes
+ # Seconds between API checks for module status updates. This can change frequently so we
+ # check often
+ CONF_MODULE_STATUS_UPDATE_INTERVAL: 300, # 5 minutes
+}
+
+SIGNAL_ADD_ENTITIES = "home_plus_control_add_entities_signal"
diff --git a/homeassistant/components/home_plus_control/helpers.py b/homeassistant/components/home_plus_control/helpers.py
new file mode 100644
index 00000000000000..95d538def01b80
--- /dev/null
+++ b/homeassistant/components/home_plus_control/helpers.py
@@ -0,0 +1,53 @@
+"""Helper classes and functions for the Legrand Home+ Control integration."""
+from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import config_entry_oauth2_flow
+
+from .const import CONF_SUBSCRIPTION_KEY, DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN
+
+
+class HomePlusControlOAuth2Implementation(
+ config_entry_oauth2_flow.LocalOAuth2Implementation
+):
+ """OAuth2 implementation that extends the HomeAssistant local implementation.
+
+ It provides the name of the integration and adds support for the subscription key.
+
+ Attributes:
+ hass (HomeAssistant): HomeAssistant core object.
+ client_id (str): Client identifier assigned by the API provider when registering an app.
+ client_secret (str): Client secret assigned by the API provider when registering an app.
+ subscription_key (str): Subscription key obtained from the API provider.
+ authorize_url (str): Authorization URL initiate authentication flow.
+ token_url (str): URL to retrieve access/refresh tokens.
+ name (str): Name of the implementation (appears in the HomeAssitant GUI).
+ """
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ config_data: dict,
+ ):
+ """HomePlusControlOAuth2Implementation Constructor.
+
+ Initialize the authentication implementation for the Legrand Home+ Control API.
+
+ Args:
+ hass (HomeAssistant): HomeAssistant core object.
+ config_data (dict): Configuration data that complies with the config Schema
+ of this component.
+ """
+ super().__init__(
+ hass=hass,
+ domain=DOMAIN,
+ client_id=config_data[CONF_CLIENT_ID],
+ client_secret=config_data[CONF_CLIENT_SECRET],
+ authorize_url=OAUTH2_AUTHORIZE,
+ token_url=OAUTH2_TOKEN,
+ )
+ self.subscription_key = config_data[CONF_SUBSCRIPTION_KEY]
+
+ @property
+ def name(self) -> str:
+ """Name of the implementation."""
+ return "Home+ Control"
diff --git a/homeassistant/components/home_plus_control/manifest.json b/homeassistant/components/home_plus_control/manifest.json
new file mode 100644
index 00000000000000..1eb143ca3c26c2
--- /dev/null
+++ b/homeassistant/components/home_plus_control/manifest.json
@@ -0,0 +1,15 @@
+{
+ "domain": "home_plus_control",
+ "name": "Legrand Home+ Control",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/home_plus_control",
+ "requirements": [
+ "homepluscontrol==0.0.5"
+ ],
+ "dependencies": [
+ "http"
+ ],
+ "codeowners": [
+ "@chemaaa"
+ ]
+}
diff --git a/homeassistant/components/home_plus_control/strings.json b/homeassistant/components/home_plus_control/strings.json
new file mode 100644
index 00000000000000..c991c9e0279afe
--- /dev/null
+++ b/homeassistant/components/home_plus_control/strings.json
@@ -0,0 +1,21 @@
+{
+ "title": "Legrand Home+ Control",
+ "config": {
+ "step": {
+ "pick_implementation": {
+ "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
+ }
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
+ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
+ "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
+ "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
+ },
+ "create_entry": {
+ "default": "[%key:common::config_flow::create_entry::authenticated%]"
+ }
+ }
+}
diff --git a/homeassistant/components/home_plus_control/switch.py b/homeassistant/components/home_plus_control/switch.py
new file mode 100644
index 00000000000000..d4167ae1f9ead8
--- /dev/null
+++ b/homeassistant/components/home_plus_control/switch.py
@@ -0,0 +1,129 @@
+"""Legrand Home+ Control Switch Entity Module that uses the HomeAssistant DataUpdateCoordinator."""
+from functools import partial
+
+from homeassistant.components.switch import (
+ DEVICE_CLASS_OUTLET,
+ DEVICE_CLASS_SWITCH,
+ SwitchEntity,
+)
+from homeassistant.core import callback
+from homeassistant.helpers import dispatcher
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import DISPATCHER_REMOVERS, DOMAIN, HW_TYPE, SIGNAL_ADD_ENTITIES
+
+
+@callback
+def add_switch_entities(new_unique_ids, coordinator, add_entities):
+ """Add switch entities to the platform.
+
+ Args:
+ new_unique_ids (set): Unique identifiers of entities to be added to Home Assistant.
+ coordinator (DataUpdateCoordinator): Data coordinator of this platform.
+ add_entities (function): Method called to add entities to Home Assistant.
+ """
+ new_entities = []
+ for uid in new_unique_ids:
+ new_ent = HomeControlSwitchEntity(coordinator, uid)
+ new_entities.append(new_ent)
+ add_entities(new_entities)
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the Legrand Home+ Control Switch platform in HomeAssistant.
+
+ Args:
+ hass (HomeAssistant): HomeAssistant core object.
+ config_entry (ConfigEntry): ConfigEntry object that configures this platform.
+ async_add_entities (function): Function called to add entities of this platform.
+ """
+ partial_add_switch_entities = partial(
+ add_switch_entities, add_entities=async_add_entities
+ )
+ # Connect the dispatcher for the switch platform
+ hass.data[DOMAIN][config_entry.entry_id][DISPATCHER_REMOVERS].append(
+ dispatcher.async_dispatcher_connect(
+ hass, SIGNAL_ADD_ENTITIES, partial_add_switch_entities
+ )
+ )
+
+
+class HomeControlSwitchEntity(CoordinatorEntity, SwitchEntity):
+ """Entity that represents a Legrand Home+ Control switch.
+
+ It extends the HomeAssistant-provided classes of the CoordinatorEntity and the SwitchEntity.
+
+ The CoordinatorEntity class provides:
+ should_poll
+ async_update
+ async_added_to_hass
+
+ The SwitchEntity class provides the functionality of a ToggleEntity and additional power
+ consumption methods and state attributes.
+ """
+
+ def __init__(self, coordinator, idx):
+ """Pass coordinator to CoordinatorEntity."""
+ super().__init__(coordinator)
+ self.idx = idx
+ self.module = self.coordinator.data[self.idx]
+
+ @property
+ def name(self):
+ """Name of the device."""
+ return self.module.name
+
+ @property
+ def unique_id(self):
+ """ID (unique) of the device."""
+ return self.idx
+
+ @property
+ def device_info(self):
+ """Device information."""
+ return {
+ "identifiers": {
+ # Unique identifiers within the domain
+ (DOMAIN, self.unique_id)
+ },
+ "name": self.name,
+ "manufacturer": "Legrand",
+ "model": HW_TYPE.get(self.module.hw_type),
+ "sw_version": self.module.fw,
+ }
+
+ @property
+ def device_class(self):
+ """Return the class of this device, from component DEVICE_CLASSES."""
+ if self.module.device == "plug":
+ return DEVICE_CLASS_OUTLET
+ return DEVICE_CLASS_SWITCH
+
+ @property
+ def available(self) -> bool:
+ """Return if entity is available.
+
+ This is the case when the coordinator is able to update the data successfully
+ AND the switch entity is reachable.
+
+ This method overrides the one of the CoordinatorEntity
+ """
+ return self.coordinator.last_update_success and self.module.reachable
+
+ @property
+ def is_on(self):
+ """Return entity state."""
+ return self.module.status == "on"
+
+ async def async_turn_on(self, **kwargs):
+ """Turn the light on."""
+ # Do the turning on.
+ await self.module.turn_on()
+ # Update the data
+ await self.coordinator.async_request_refresh()
+
+ async def async_turn_off(self, **kwargs):
+ """Turn the entity off."""
+ await self.module.turn_off()
+ # Update the data
+ await self.coordinator.async_request_refresh()
diff --git a/homeassistant/components/home_plus_control/translations/ca.json b/homeassistant/components/home_plus_control/translations/ca.json
new file mode 100644
index 00000000000000..90e23fcd7ab089
--- /dev/null
+++ b/homeassistant/components/home_plus_control/translations/ca.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El compte ja ha estat configurat",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
+ "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.",
+ "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.",
+ "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})",
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
+ },
+ "create_entry": {
+ "default": "Autenticaci\u00f3 exitosa"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3"
+ }
+ }
+ },
+ "title": "Legrand Home+ Control"
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_plus_control/translations/cs.json b/homeassistant/components/home_plus_control/translations/cs.json
new file mode 100644
index 00000000000000..9d7f5156bc3aca
--- /dev/null
+++ b/homeassistant/components/home_plus_control/translations/cs.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u00da\u010det je ji\u017e nastaven",
+ "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1",
+ "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})",
+ "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace."
+ },
+ "create_entry": {
+ "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Vyberte metodu ov\u011b\u0159en\u00ed"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_plus_control/translations/de.json b/homeassistant/components/home_plus_control/translations/de.json
new file mode 100644
index 00000000000000..8e7d9e9bc240df
--- /dev/null
+++ b/homeassistant/components/home_plus_control/translations/de.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
+ "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.",
+ "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.",
+ "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).",
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
+ "create_entry": {
+ "default": "Erfolgreich authentifiziert"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "W\u00e4hle die Authentifizierungsmethode"
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_plus_control/translations/en.json b/homeassistant/components/home_plus_control/translations/en.json
new file mode 100644
index 00000000000000..f5f8afe73d153f
--- /dev/null
+++ b/homeassistant/components/home_plus_control/translations/en.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Account is already configured",
+ "already_in_progress": "Configuration flow is already in progress",
+ "authorize_url_timeout": "Timeout generating authorize URL.",
+ "missing_configuration": "The component is not configured. Please follow the documentation.",
+ "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
+ },
+ "create_entry": {
+ "default": "Successfully authenticated"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Pick Authentication Method"
+ }
+ }
+ },
+ "title": "Legrand Home+ Control"
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_plus_control/translations/es-419.json b/homeassistant/components/home_plus_control/translations/es-419.json
new file mode 100644
index 00000000000000..e35bb529a85659
--- /dev/null
+++ b/homeassistant/components/home_plus_control/translations/es-419.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La cuenta ya est\u00e1 configurada",
+ "already_in_progress": "El flujo de configuraci\u00f3n ya ha iniciado",
+ "authorize_url_timeout": "Se agot\u00f3 el tiempo al generar el URL de autorizaci\u00f3n",
+ "missing_configuration": "Este componente no est\u00e1 configurado. Por favor sigue la documentaci\u00f3n",
+ "no_url_available": "Ning\u00fan URL disponible. Para m\u00e1s informaci\u00f3n sobre este error [revisa la secci\u00f3n de ayuda] ({docs_url})",
+ "single_instance_allowed": "Previamente configurado. S\u00f3lo es posible una configuraci\u00f3n"
+ },
+ "create_entry": {
+ "default": "Autenticado exitosamente"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Escoja el m\u00e9todo de autenticaci\u00f3n"
+ }
+ }
+ },
+ "title": "Legrand Home+ Control"
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_plus_control/translations/et.json b/homeassistant/components/home_plus_control/translations/et.json
new file mode 100644
index 00000000000000..cfe40d86bcd784
--- /dev/null
+++ b/homeassistant/components/home_plus_control/translations/et.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kasutaja on juba seadistatud",
+ "already_in_progress": "Seadistamine on juba k\u00e4imas",
+ "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp",
+ "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.",
+ "no_url_available": "URL-i pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [check the help section]({docs_url})",
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ },
+ "create_entry": {
+ "default": "Tuvastamine \u00f5nnestus"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Vali tuvastusmeetod"
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_plus_control/translations/fr.json b/homeassistant/components/home_plus_control/translations/fr.json
new file mode 100644
index 00000000000000..c39d4a2867eaff
--- /dev/null
+++ b/homeassistant/components/home_plus_control/translations/fr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Compte d\u00e9j\u00e0 configur\u00e9",
+ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours",
+ "authorize_url_timeout": "[%key::common::config_flow::abort::oauth2_authorize_url_timeout%]",
+ "missing_configuration": "Le composant n'est pas configur\u00e9. Merci de suivre la documentation.",
+ "no_url_available": "Aucune URL disponible. Pour plus d'information sur cette erreur, [v\u00e9rifier la section d'aide]({docs_url})",
+ "single_instance_allowed": "[%key::common::config_flow::abort::single_instance_allowed%]"
+ },
+ "create_entry": {
+ "default": "Authentification r\u00e9ussie"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Choisir une m\u00e9thode d'authentification"
+ }
+ }
+ },
+ "title": "Legrand Home + Control"
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_plus_control/translations/hu.json b/homeassistant/components/home_plus_control/translations/hu.json
new file mode 100644
index 00000000000000..7bc04beb0578ac
--- /dev/null
+++ b/homeassistant/components/home_plus_control/translations/hu.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
+ "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.",
+ "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.",
+ "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
+ "create_entry": {
+ "default": "Sikeres hiteles\u00edt\u00e9s"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert"
+ }
+ }
+ },
+ "title": "Legrand Home+ vez\u00e9rl\u00e9s"
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_plus_control/translations/it.json b/homeassistant/components/home_plus_control/translations/it.json
new file mode 100644
index 00000000000000..789a7db85ebf19
--- /dev/null
+++ b/homeassistant/components/home_plus_control/translations/it.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
+ "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.",
+ "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.",
+ "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})",
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
+ },
+ "create_entry": {
+ "default": "Autenticazione riuscita"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Scegli il metodo di autenticazione"
+ }
+ }
+ },
+ "title": "Legrand Home + Control"
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_plus_control/translations/ko.json b/homeassistant/components/home_plus_control/translations/ko.json
new file mode 100644
index 00000000000000..94c8bb916485c0
--- /dev/null
+++ b/homeassistant/components/home_plus_control/translations/ko.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
+ "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
+ "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "create_entry": {
+ "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30"
+ }
+ }
+ },
+ "title": "Legrand Home+ Control"
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_plus_control/translations/nl.json b/homeassistant/components/home_plus_control/translations/nl.json
new file mode 100644
index 00000000000000..9d448e480a104b
--- /dev/null
+++ b/homeassistant/components/home_plus_control/translations/nl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Account is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
+ "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
+ "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.",
+ "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})",
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
+ },
+ "create_entry": {
+ "default": "Succesvol geauthenticeerd"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Kies een authenticatie methode"
+ }
+ }
+ },
+ "title": "Legrand Home+ Control"
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_plus_control/translations/no.json b/homeassistant/components/home_plus_control/translations/no.json
new file mode 100644
index 00000000000000..363f5cf54f7708
--- /dev/null
+++ b/homeassistant/components/home_plus_control/translations/no.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kontoen er allerede konfigurert",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
+ "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse",
+ "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen",
+ "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})",
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
+ },
+ "create_entry": {
+ "default": "Vellykket godkjenning"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Velg godkjenningsmetode"
+ }
+ }
+ },
+ "title": "Legrand Home + Control"
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_plus_control/translations/pl.json b/homeassistant/components/home_plus_control/translations/pl.json
new file mode 100644
index 00000000000000..b684c874a7dc66
--- /dev/null
+++ b/homeassistant/components/home_plus_control/translations/pl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto jest ju\u017c skonfigurowane",
+ "already_in_progress": "Konfiguracja jest ju\u017c w toku",
+ "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji",
+ "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.",
+ "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})",
+ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
+ },
+ "create_entry": {
+ "default": "Pomy\u015blnie uwierzytelniono"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Wybierz metod\u0119 uwierzytelniania"
+ }
+ }
+ },
+ "title": "Legrand Home+ Control"
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_plus_control/translations/pt.json b/homeassistant/components/home_plus_control/translations/pt.json
new file mode 100644
index 00000000000000..2a1a4a7174da73
--- /dev/null
+++ b/homeassistant/components/home_plus_control/translations/pt.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Conta j\u00e1 configurada",
+ "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer",
+ "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o",
+ "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.",
+ "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})",
+ "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel."
+ },
+ "create_entry": {
+ "default": "Autenticado com sucesso"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_plus_control/translations/ru.json b/homeassistant/components/home_plus_control/translations/ru.json
new file mode 100644
index 00000000000000..fd3da6929d8321
--- /dev/null
+++ b/homeassistant/components/home_plus_control/translations/ru.json
@@ -0,0 +1,21 @@
+{
+ "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.",
+ "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
+ "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.",
+ "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.",
+ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e."
+ },
+ "create_entry": {
+ "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438"
+ }
+ }
+ },
+ "title": "Legrand Home+ Control"
+}
\ No newline at end of file
diff --git a/homeassistant/components/home_plus_control/translations/zh-Hant.json b/homeassistant/components/home_plus_control/translations/zh-Hant.json
new file mode 100644
index 00000000000000..0faa311028720a
--- /dev/null
+++ b/homeassistant/components/home_plus_control/translations/zh-Hant.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
+ "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002",
+ "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002",
+ "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})",
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002"
+ },
+ "create_entry": {
+ "default": "\u5df2\u6210\u529f\u8a8d\u8b49"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f"
+ }
+ }
+ },
+ "title": "Legrand Home+ Control"
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py
index c2ee40b7d43f25..67eb94a97e73be 100644
--- a/homeassistant/components/homeassistant/__init__.py
+++ b/homeassistant/components/homeassistant/__init__.py
@@ -21,15 +21,30 @@
import homeassistant.core as ha
from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.service import async_extract_referenced_entity_ids
+from homeassistant.helpers.service import (
+ async_extract_config_entry_ids,
+ async_extract_referenced_entity_ids,
+)
+
+ATTR_ENTRY_ID = "entry_id"
_LOGGER = logging.getLogger(__name__)
DOMAIN = ha.DOMAIN
SERVICE_RELOAD_CORE_CONFIG = "reload_core_config"
+SERVICE_RELOAD_CONFIG_ENTRY = "reload_config_entry"
SERVICE_CHECK_CONFIG = "check_config"
SERVICE_UPDATE_ENTITY = "update_entity"
SERVICE_SET_LOCATION = "set_location"
SCHEMA_UPDATE_ENTITY = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
+SCHEMA_RELOAD_CONFIG_ENTRY = vol.All(
+ vol.Schema(
+ {
+ vol.Optional(ATTR_ENTRY_ID): str,
+ **cv.ENTITY_SERVICE_FIELDS,
+ },
+ ),
+ cv.has_at_least_one_key(ATTR_ENTRY_ID, *cv.ENTITY_SERVICE_FIELDS),
+)
async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool:
@@ -43,7 +58,8 @@ async def async_handle_turn_service(service):
# Generic turn on/off method requires entity id
if not all_referenced:
_LOGGER.error(
- "homeassistant.%s cannot be called without a target", service.service
+ "The service homeassistant.%s cannot be called without a target",
+ service.service,
)
return
@@ -203,4 +219,26 @@ async def async_set_location(call):
vol.Schema({ATTR_LATITUDE: cv.latitude, ATTR_LONGITUDE: cv.longitude}),
)
+ async def async_handle_reload_config_entry(call):
+ """Service handler for reloading a config entry."""
+ reload_entries = set()
+ if ATTR_ENTRY_ID in call.data:
+ reload_entries.add(call.data[ATTR_ENTRY_ID])
+ reload_entries.update(await async_extract_config_entry_ids(hass, call))
+ if not reload_entries:
+ raise ValueError("There were no matching config entries to reload")
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_reload(config_entry_id)
+ for config_entry_id in reload_entries
+ ]
+ )
+
+ hass.helpers.service.async_register_admin_service(
+ ha.DOMAIN,
+ SERVICE_RELOAD_CONFIG_ENTRY,
+ async_handle_reload_config_entry,
+ schema=SCHEMA_RELOAD_CONFIG_ENTRY,
+ )
+
return True
diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py
index 1ff3915f1215c9..3173d2d8c32abf 100644
--- a/homeassistant/components/homeassistant/scene.py
+++ b/homeassistant/components/homeassistant/scene.py
@@ -1,7 +1,9 @@
"""Allow users to set and activate scenes."""
+from __future__ import annotations
+
from collections import namedtuple
import logging
-from typing import Any, List
+from typing import Any
import voluptuous as vol
@@ -118,7 +120,7 @@ def _ensure_no_intersection(value):
@callback
-def scenes_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]:
+def scenes_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]:
"""Return all scenes that reference the entity."""
if DATA_PLATFORM not in hass.data:
return []
@@ -133,7 +135,7 @@ def scenes_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]:
@callback
-def entities_in_scene(hass: HomeAssistant, entity_id: str) -> List[str]:
+def entities_in_scene(hass: HomeAssistant, entity_id: str) -> list[str]:
"""Return all entities in a scene."""
if DATA_PLATFORM not in hass.data:
return []
@@ -298,7 +300,7 @@ def unique_id(self):
return self.scene_config.id
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the scene state attributes."""
attributes = {ATTR_ENTITY_ID: list(self.scene_config.states)}
unique_id = self.unique_id
diff --git a/homeassistant/components/homeassistant/services.yaml b/homeassistant/components/homeassistant/services.yaml
index cb3efb0d524a67..251ee171b6af0b 100644
--- a/homeassistant/components/homeassistant/services.yaml
+++ b/homeassistant/components/homeassistant/services.yaml
@@ -1,49 +1,76 @@
check_config:
- description: Check the Home Assistant configuration files for errors. Errors will be displayed in the Home Assistant log.
+ name: Check configuration
+ description:
+ Check the Home Assistant configuration files for errors. Errors will be
+ displayed in the Home Assistant log.
reload_core_config:
+ name: Reload core configuration
description: Reload the core configuration.
restart:
+ name: Restart
description: Restart the Home Assistant service.
set_location:
+ name: Set location
description: Update the Home Assistant location.
fields:
latitude:
- description: Latitude of your location
+ name: Latitude
+ description: Latitude of your location.
+ required: true
example: 32.87336
+ selector:
+ text:
longitude:
- description: Longitude of your location
+ name: Longitude
+ description: Longitude of your location.
+ required: true
example: 117.22743
+ selector:
+ text:
stop:
+ name: Stop
description: Stop the Home Assistant service.
toggle:
- description: Generic service to toggle devices on/off under any domain. Same usage as the light.turn_on, switch.turn_on, etc. services.
- fields:
- entity_id:
- description: The entity_id of the device to toggle on/off.
- example: light.living_room
+ name: Generic toggle
+ description: Generic service to toggle devices on/off under any domain
+ target:
+ entity: {}
turn_on:
- description: Generic service to turn devices on under any domain. Same usage as the light.turn_on, switch.turn_on, etc. services.
- fields:
- entity_id:
- description: The entity_id of the device to turn on.
- example: light.living_room
+ name: Generic turn on
+ description: Generic service to turn devices on under any domain.
+ target:
+ entity: {}
turn_off:
- description: Generic service to turn devices off under any domain. Same usage as the light.turn_on, switch.turn_on, etc. services.
- fields:
- entity_id:
- description: The entity_id of the device to turn off.
- example: light.living_room
+ name: Generic turn off
+ description: Generic service to turn devices off under any domain.
+ target:
+ entity: {}
update_entity:
+ name: Update entity
description: Force one or more entities to update its data
+ target:
+ entity: {}
+
+reload_config_entry:
+ name: Reload config entry
+ description: Reload a config entry that matches a target.
+ target:
+ entity: {}
+ device: {}
fields:
- entity_id:
- description: One or multiple entity_ids to update. Can be a list.
- example: light.living_room
+ entry_id:
+ advanced: true
+ name: Config entry id
+ description: A configuration entry id
+ required: false
+ example: 8955375327824e14ba89e4b29cc3ec9a
+ selector:
+ text:
diff --git a/homeassistant/components/homeassistant/translations/de.json b/homeassistant/components/homeassistant/translations/de.json
index 45768a9f127009..e24568ff2125ca 100644
--- a/homeassistant/components/homeassistant/translations/de.json
+++ b/homeassistant/components/homeassistant/translations/de.json
@@ -1,10 +1,21 @@
{
"system_health": {
"info": {
+ "arch": "CPU-Architektur",
+ "chassis": "Chassis",
+ "dev": "Entwicklung",
"docker": "Docker",
"docker_version": "Docker",
"hassio": "Supervisor",
- "host_os": "Home Assistant OS"
+ "host_os": "Home Assistant OS",
+ "installation_type": "Installationstyp",
+ "os_name": "Betriebssystemfamilie",
+ "os_version": "Betriebssystem-Version",
+ "python_version": "Python-Version",
+ "supervisor": "Supervisor",
+ "timezone": "Zeitzone",
+ "version": "Version",
+ "virtualenv": "Virtuelle Umgebung"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/translations/fr.json b/homeassistant/components/homeassistant/translations/fr.json
new file mode 100644
index 00000000000000..8d76ff76b79c58
--- /dev/null
+++ b/homeassistant/components/homeassistant/translations/fr.json
@@ -0,0 +1,21 @@
+{
+ "system_health": {
+ "info": {
+ "arch": "Architecture du processeur",
+ "chassis": "Ch\u00e2ssis",
+ "dev": "D\u00e9veloppement",
+ "docker": "Docker",
+ "docker_version": "Docker",
+ "hassio": "Supervisor",
+ "host_os": "Home Assistant OS",
+ "installation_type": "Type d'installation",
+ "os_name": "Famille du syst\u00e8me d'exploitation",
+ "os_version": "Version du syst\u00e8me d'exploitation",
+ "python_version": "Version de Python",
+ "supervisor": "Supervisor",
+ "timezone": "Fuseau horaire",
+ "version": "Version",
+ "virtualenv": "Environnement virtuel"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/translations/he.json b/homeassistant/components/homeassistant/translations/he.json
new file mode 100644
index 00000000000000..f45b17b1a13263
--- /dev/null
+++ b/homeassistant/components/homeassistant/translations/he.json
@@ -0,0 +1,7 @@
+{
+ "system_health": {
+ "info": {
+ "os_name": "\u05de\u05e9\u05e4\u05d7\u05ea \u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/translations/hu.json b/homeassistant/components/homeassistant/translations/hu.json
index e202a747ac7532..f6bfe03321e5d7 100644
--- a/homeassistant/components/homeassistant/translations/hu.json
+++ b/homeassistant/components/homeassistant/translations/hu.json
@@ -6,13 +6,13 @@
"dev": "Fejleszt\u00e9s",
"docker": "Docker",
"docker_version": "Docker",
- "hassio": "Adminisztr\u00e1tor",
+ "hassio": "Supervisor",
"host_os": "Home Assistant OS",
"installation_type": "Telep\u00edt\u00e9s t\u00edpusa",
"os_name": "Oper\u00e1ci\u00f3s rendszer csal\u00e1d",
"os_version": "Oper\u00e1ci\u00f3s rendszer verzi\u00f3ja",
"python_version": "Python verzi\u00f3",
- "supervisor": "Adminisztr\u00e1tor",
+ "supervisor": "Supervisor",
"timezone": "Id\u0151z\u00f3na",
"version": "Verzi\u00f3",
"virtualenv": "Virtu\u00e1lis k\u00f6rnyezet"
diff --git a/homeassistant/components/homeassistant/translations/id.json b/homeassistant/components/homeassistant/translations/id.json
new file mode 100644
index 00000000000000..2ee86bba8157c8
--- /dev/null
+++ b/homeassistant/components/homeassistant/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "system_health": {
+ "info": {
+ "arch": "Arsitektur CPU",
+ "chassis": "Kerangka",
+ "dev": "Pengembangan",
+ "docker": "Docker",
+ "docker_version": "Docker",
+ "hassio": "Supervisor",
+ "host_os": "Home Assistant OS",
+ "installation_type": "Jenis Instalasi",
+ "os_name": "Keluarga Sistem Operasi",
+ "os_version": "Versi Sistem Operasi",
+ "python_version": "Versi Python",
+ "supervisor": "Supervisor",
+ "timezone": "Zona Waktu",
+ "version": "Versi",
+ "virtualenv": "Lingkungan Virtual"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/translations/it.json b/homeassistant/components/homeassistant/translations/it.json
index f3168807715245..66b8f8a1d14c85 100644
--- a/homeassistant/components/homeassistant/translations/it.json
+++ b/homeassistant/components/homeassistant/translations/it.json
@@ -6,13 +6,13 @@
"dev": "Sviluppo",
"docker": "Docker",
"docker_version": "Docker",
- "hassio": "Supervisore",
+ "hassio": "Supervisor",
"host_os": "Sistema Operativo di Home Assistant",
"installation_type": "Tipo di installazione",
"os_name": "Famiglia del Sistema Operativo",
"os_version": "Versione del Sistema Operativo",
"python_version": "Versione Python",
- "supervisor": "Supervisore",
+ "supervisor": "Supervisor",
"timezone": "Fuso orario",
"version": "Versione",
"virtualenv": "Ambiente virtuale"
diff --git a/homeassistant/components/homeassistant/translations/ko.json b/homeassistant/components/homeassistant/translations/ko.json
new file mode 100644
index 00000000000000..801d63fd44992d
--- /dev/null
+++ b/homeassistant/components/homeassistant/translations/ko.json
@@ -0,0 +1,21 @@
+{
+ "system_health": {
+ "info": {
+ "arch": "CPU \uc544\ud0a4\ud14d\ucc98",
+ "chassis": "\uc100\uc2dc",
+ "dev": "\uac1c\ubc1c\uc790 \ubaa8\ub4dc",
+ "docker": "Docker",
+ "docker_version": "Docker",
+ "hassio": "Supervisor",
+ "host_os": "Home Assistant OS",
+ "installation_type": "\uc124\uce58 \uc720\ud615",
+ "os_name": "\uc6b4\uc601 \uccb4\uc81c \uc81c\ud488\uad70",
+ "os_version": "\uc6b4\uc601 \uccb4\uc81c \ubc84\uc804",
+ "python_version": "Python \ubc84\uc804",
+ "supervisor": "Supervisor",
+ "timezone": "\uc2dc\uac04\ub300",
+ "version": "\ubc84\uc804",
+ "virtualenv": "\uac00\uc0c1 \ud658\uacbd"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/translations/nl.json b/homeassistant/components/homeassistant/translations/nl.json
index 338a019019feac..6dba5eec8b94ba 100644
--- a/homeassistant/components/homeassistant/translations/nl.json
+++ b/homeassistant/components/homeassistant/translations/nl.json
@@ -1,14 +1,17 @@
{
"system_health": {
"info": {
+ "arch": "CPU-architectuur",
+ "chassis": "Chassis",
"dev": "Ontwikkeling",
"docker": "Docker",
"docker_version": "Docker",
"hassio": "Supervisor",
"host_os": "Home Assistant OS",
"installation_type": "Type installatie",
+ "os_name": "Besturingssysteem",
"os_version": "Versie van het besturingssysteem",
- "python_version": "Python versie",
+ "python_version": "Python-versie",
"supervisor": "Supervisor",
"timezone": "Tijdzone",
"version": "Versie",
diff --git a/homeassistant/components/homeassistant/translations/tr.json b/homeassistant/components/homeassistant/translations/tr.json
index 1ff8ea1b3a91ee..c2b7ca1b10cfb5 100644
--- a/homeassistant/components/homeassistant/translations/tr.json
+++ b/homeassistant/components/homeassistant/translations/tr.json
@@ -2,9 +2,11 @@
"system_health": {
"info": {
"arch": "CPU Mimarisi",
+ "chassis": "Ana G\u00f6vde",
"dev": "Geli\u015ftirme",
"docker": "Konteyner",
"docker_version": "Konteyner",
+ "hassio": "S\u00fcperviz\u00f6r",
"host_os": "Home Assistant OS",
"installation_type": "Kurulum T\u00fcr\u00fc",
"os_name": "\u0130\u015fletim Sistemi Ailesi",
diff --git a/homeassistant/components/homeassistant/translations/uk.json b/homeassistant/components/homeassistant/translations/uk.json
new file mode 100644
index 00000000000000..19e07c8f822527
--- /dev/null
+++ b/homeassistant/components/homeassistant/translations/uk.json
@@ -0,0 +1,21 @@
+{
+ "system_health": {
+ "info": {
+ "arch": "\u0410\u0440\u0445\u0456\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0426\u041f",
+ "chassis": "\u0428\u0430\u0441\u0456",
+ "dev": "\u0421\u0435\u0440\u0435\u0434\u043e\u0432\u0438\u0449\u0435 \u0440\u043e\u0437\u0440\u043e\u0431\u043a\u0438",
+ "docker": "Docker",
+ "docker_version": "Docker",
+ "hassio": "Supervisor",
+ "host_os": "Home Assistant OS",
+ "installation_type": "\u0422\u0438\u043f \u0456\u043d\u0441\u0442\u0430\u043b\u044f\u0446\u0456\u0457",
+ "os_name": "\u0421\u0456\u043c\u0435\u0439\u0441\u0442\u0432\u043e \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u0438\u0445 \u0441\u0438\u0441\u0442\u0435\u043c",
+ "os_version": "\u0412\u0435\u0440\u0441\u0456\u044f \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u043e\u0457 \u0441\u0438\u0441\u0442\u0435\u043c\u0438",
+ "python_version": "\u0412\u0435\u0440\u0441\u0456\u044f Python",
+ "supervisor": "Supervisor",
+ "timezone": "\u0427\u0430\u0441\u043e\u0432\u0438\u0439 \u043f\u043e\u044f\u0441",
+ "version": "\u0412\u0435\u0440\u0441\u0456\u044f",
+ "virtualenv": "\u0412\u0456\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u0435 \u043e\u0442\u043e\u0447\u0435\u043d\u043d\u044f"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py
index b7ab081d266c15..2e78a93315daee 100644
--- a/homeassistant/components/homeassistant/triggers/event.py
+++ b/homeassistant/components/homeassistant/triggers/event.py
@@ -1,22 +1,21 @@
"""Offer event listening automation rules."""
import voluptuous as vol
-from homeassistant.const import CONF_PLATFORM
+from homeassistant.const import CONF_EVENT_DATA, CONF_PLATFORM
from homeassistant.core import HassJob, callback
-from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers import config_validation as cv, template
# mypy: allow-untyped-defs
CONF_EVENT_TYPE = "event_type"
-CONF_EVENT_DATA = "event_data"
CONF_EVENT_CONTEXT = "context"
TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_PLATFORM): "event",
- vol.Required(CONF_EVENT_TYPE): vol.All(cv.ensure_list, [cv.string]),
- vol.Optional(CONF_EVENT_DATA): dict,
- vol.Optional(CONF_EVENT_CONTEXT): dict,
+ vol.Required(CONF_EVENT_TYPE): vol.All(cv.ensure_list, [cv.template]),
+ vol.Optional(CONF_EVENT_DATA): vol.All(dict, cv.template_complex),
+ vol.Optional(CONF_EVENT_CONTEXT): vol.All(dict, cv.template_complex),
}
)
@@ -32,25 +31,44 @@ async def async_attach_trigger(
hass, config, action, automation_info, *, platform_type="event"
):
"""Listen for events based on configuration."""
- event_types = config.get(CONF_EVENT_TYPE)
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
+ variables = None
+ if automation_info:
+ variables = automation_info.get("variables")
+
+ template.attach(hass, config[CONF_EVENT_TYPE])
+ event_types = template.render_complex(
+ config[CONF_EVENT_TYPE], variables, limited=True
+ )
removes = []
event_data_schema = None
- if config.get(CONF_EVENT_DATA):
+ if CONF_EVENT_DATA in config:
+ # Render the schema input
+ template.attach(hass, config[CONF_EVENT_DATA])
+ event_data = {}
+ event_data.update(
+ template.render_complex(config[CONF_EVENT_DATA], variables, limited=True)
+ )
+ # Build the schema
event_data_schema = vol.Schema(
- {
- vol.Required(key): value
- for key, value in config.get(CONF_EVENT_DATA).items()
- },
+ {vol.Required(key): value for key, value in event_data.items()},
extra=vol.ALLOW_EXTRA,
)
event_context_schema = None
- if config.get(CONF_EVENT_CONTEXT):
+ if CONF_EVENT_CONTEXT in config:
+ # Render the schema input
+ template.attach(hass, config[CONF_EVENT_CONTEXT])
+ event_context = {}
+ event_context.update(
+ template.render_complex(config[CONF_EVENT_CONTEXT], variables, limited=True)
+ )
+ # Build the schema
event_context_schema = vol.Schema(
{
vol.Required(key): _schema_value(value)
- for key, value in config.get(CONF_EVENT_CONTEXT).items()
+ for key, value in event_context.items()
},
extra=vol.ALLOW_EXTRA,
)
@@ -78,6 +96,7 @@ def handle_event(event):
"platform": platform_type,
"event": event,
"description": f"event '{event.event_type}'",
+ "id": trigger_id,
}
},
event.context,
diff --git a/homeassistant/components/homeassistant/triggers/homeassistant.py b/homeassistant/components/homeassistant/triggers/homeassistant.py
index 2f3ebfb82d906e..2f3ae8e6ad2384 100644
--- a/homeassistant/components/homeassistant/triggers/homeassistant.py
+++ b/homeassistant/components/homeassistant/triggers/homeassistant.py
@@ -19,6 +19,7 @@
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for events based on configuration."""
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
event = config.get(CONF_EVENT)
job = HassJob(action)
@@ -34,6 +35,7 @@ def hass_shutdown(event):
"platform": "homeassistant",
"event": event,
"description": "Home Assistant stopping",
+ "id": trigger_id,
}
},
event.context,
@@ -51,6 +53,7 @@ def hass_shutdown(event):
"platform": "homeassistant",
"event": event,
"description": "Home Assistant starting",
+ "id": trigger_id,
}
},
)
diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py
index 7cfee8fad933d1..05eed9ee27bc36 100644
--- a/homeassistant/components/homeassistant/triggers/numeric_state.py
+++ b/homeassistant/components/homeassistant/triggers/numeric_state.py
@@ -73,17 +73,22 @@ async def async_attach_trigger(
template.attach(hass, time_delta)
value_template = config.get(CONF_VALUE_TEMPLATE)
unsub_track_same = {}
- entities_triggered = set()
+ armed_entities = set()
period: dict = {}
attribute = config.get(CONF_ATTRIBUTE)
job = HassJob(action)
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
+ _variables = {}
+ if automation_info:
+ _variables = automation_info.get("variables") or {}
+
if value_template is not None:
value_template.hass = hass
def variables(entity_id):
"""Return a dict with trigger variables."""
- return {
+ trigger_info = {
"trigger": {
"platform": "numeric_state",
"entity_id": entity_id,
@@ -92,17 +97,27 @@ def variables(entity_id):
"attribute": attribute,
}
}
+ return {**_variables, **trigger_info}
@callback
def check_numeric_state(entity_id, from_s, to_s):
- """Return True if criteria are now met."""
- if to_s is None:
- return False
-
+ """Return whether the criteria are met, raise ConditionError if unknown."""
return condition.async_numeric_state(
hass, to_s, below, above, value_template, variables(entity_id), attribute
)
+ # Each entity that starts outside the range is already armed (ready to fire).
+ for entity_id in entity_ids:
+ try:
+ if not check_numeric_state(entity_id, None, entity_id):
+ armed_entities.add(entity_id)
+ except exceptions.ConditionError as ex:
+ _LOGGER.warning(
+ "Error initializing '%s' trigger: %s",
+ automation_info["name"],
+ ex,
+ )
+
@callback
def state_automation_listener(event):
"""Listen for state changes and calls action."""
@@ -125,17 +140,33 @@ def call_action():
"to_state": to_s,
"for": time_delta if not time_delta else period[entity_id],
"description": f"numeric state of {entity_id}",
+ "id": trigger_id,
}
},
to_s.context,
)
- matching = check_numeric_state(entity_id, from_s, to_s)
+ @callback
+ def check_numeric_state_no_raise(entity_id, from_s, to_s):
+ """Return True if the criteria are now met, False otherwise."""
+ try:
+ return check_numeric_state(entity_id, from_s, to_s)
+ except exceptions.ConditionError:
+ # This is an internal same-state listener so we just drop the
+ # error. The same error will be reached and logged by the
+ # primary async_track_state_change_event() listener.
+ return False
+
+ try:
+ matching = check_numeric_state(entity_id, from_s, to_s)
+ except exceptions.ConditionError as ex:
+ _LOGGER.warning("Error in '%s' trigger: %s", automation_info["name"], ex)
+ return
if not matching:
- entities_triggered.discard(entity_id)
- elif entity_id not in entities_triggered:
- entities_triggered.add(entity_id)
+ armed_entities.add(entity_id)
+ elif entity_id in armed_entities:
+ armed_entities.discard(entity_id)
if time_delta:
try:
@@ -148,7 +179,6 @@ def call_action():
automation_info["name"],
ex,
)
- entities_triggered.discard(entity_id)
return
unsub_track_same[entity_id] = async_track_same_state(
@@ -156,7 +186,7 @@ def call_action():
period[entity_id],
call_action,
entity_ids=entity_id,
- async_check_same_func=check_numeric_state,
+ async_check_same_func=check_numeric_state_no_raise,
)
else:
call_action()
diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py
index 5dd7335f56e6fb..69cddbfe126400 100644
--- a/homeassistant/components/homeassistant/triggers/state.py
+++ b/homeassistant/components/homeassistant/triggers/state.py
@@ -1,7 +1,9 @@
"""Offer state listening automation rules."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Any, Dict, Optional
+from typing import Any
import voluptuous as vol
@@ -79,18 +81,23 @@ async def async_attach_trigger(
template.attach(hass, time_delta)
match_all = from_state == MATCH_ALL and to_state == MATCH_ALL
unsub_track_same = {}
- period: Dict[str, timedelta] = {}
+ period: dict[str, timedelta] = {}
match_from_state = process_state_match(from_state)
match_to_state = process_state_match(to_state)
attribute = config.get(CONF_ATTRIBUTE)
job = HassJob(action)
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
+ _variables = {}
+ if automation_info:
+ _variables = automation_info.get("variables") or {}
+
@callback
def state_automation_listener(event: Event):
"""Listen for state changes and calls action."""
entity: str = event.data["entity_id"]
- from_s: Optional[State] = event.data.get("old_state")
- to_s: Optional[State] = event.data.get("new_state")
+ from_s: State | None = event.data.get("old_state")
+ to_s: State | None = event.data.get("new_state")
if from_s is None:
old_value = None
@@ -134,6 +141,7 @@ def call_action():
"for": time_delta if not time_delta else period[entity],
"attribute": attribute,
"description": f"state of {entity}",
+ "id": trigger_id,
}
},
event.context,
@@ -143,7 +151,7 @@ def call_action():
call_action()
return
- variables = {
+ trigger_info = {
"trigger": {
"platform": "state",
"entity_id": entity,
@@ -151,6 +159,7 @@ def call_action():
"to_state": to_s,
}
}
+ variables = {**_variables, **trigger_info}
try:
period[entity] = cv.positive_time_period(
diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py
index b0fced4d55d0fe..69f01672078cc2 100644
--- a/homeassistant/components/homeassistant/triggers/time.py
+++ b/homeassistant/components/homeassistant/triggers/time.py
@@ -39,6 +39,7 @@
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
entities = {}
removes = []
job = HassJob(action)
@@ -54,6 +55,7 @@ def time_automation_listener(description, now, *, entity_id=None):
"now": now,
"description": description,
"entity_id": entity_id,
+ "id": trigger_id,
}
},
)
diff --git a/homeassistant/components/homeassistant/triggers/time_pattern.py b/homeassistant/components/homeassistant/triggers/time_pattern.py
index 3107d590d912f8..859f76b773bbb3 100644
--- a/homeassistant/components/homeassistant/triggers/time_pattern.py
+++ b/homeassistant/components/homeassistant/triggers/time_pattern.py
@@ -57,6 +57,7 @@ def __call__(self, value):
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
hours = config.get(CONF_HOURS)
minutes = config.get(CONF_MINUTES)
seconds = config.get(CONF_SECONDS)
@@ -78,6 +79,7 @@ def time_automation_listener(now):
"platform": "time_pattern",
"now": now,
"description": "time pattern",
+ "id": trigger_id,
}
},
)
diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py
index 5cbc9bb6f18aa6..0e4bcc28aabfd1 100644
--- a/homeassistant/components/homekit/__init__.py
+++ b/homeassistant/components/homekit/__init__.py
@@ -5,7 +5,7 @@
import os
from aiohttp import web
-from pyhap.const import CATEGORY_CAMERA, CATEGORY_TELEVISION, STANDALONE_AID
+from pyhap.const import STANDALONE_AID
import voluptuous as vol
from homeassistant.components import zeroconf
@@ -34,7 +34,7 @@
SERVICE_RELOAD,
)
from homeassistant.core import CoreState, HomeAssistant, callback
-from homeassistant.exceptions import ConfigEntryNotReady, Unauthorized
+from homeassistant.exceptions import Unauthorized
from homeassistant.helpers import device_registry, entity_registry
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA, FILTER_SCHEMA
@@ -42,10 +42,23 @@
from homeassistant.loader import IntegrationNotFound, async_get_integration
from homeassistant.util import get_local_ip
-from .accessories import get_accessory
+from . import ( # noqa: F401
+ type_cameras,
+ type_covers,
+ type_fans,
+ type_humidifiers,
+ type_lights,
+ type_locks,
+ type_media_players,
+ type_remotes,
+ type_security_systems,
+ type_sensors,
+ type_switches,
+ type_thermostats,
+)
+from .accessories import HomeBridge, HomeDriver, get_accessory
from .aidmanager import AccessoryAidStorage
from .const import (
- AID_STORAGE,
ATTR_INTERGRATION,
ATTR_MANUFACTURER,
ATTR_MODEL,
@@ -56,6 +69,7 @@
CONF_AUTO_START,
CONF_ENTITY_CONFIG,
CONF_ENTRY_INDEX,
+ CONF_EXCLUDE_ACCESSORY_MODE,
CONF_FILTER,
CONF_HOMEKIT_MODE,
CONF_LINKED_BATTERY_CHARGING_SENSOR,
@@ -67,6 +81,7 @@
CONF_ZEROCONF_DEFAULT_INTERFACE,
CONFIG_OPTIONS,
DEFAULT_AUTO_START,
+ DEFAULT_EXCLUDE_ACCESSORY_MODE,
DEFAULT_HOMEKIT_MODE,
DEFAULT_PORT,
DEFAULT_SAFE_MODE,
@@ -83,12 +98,13 @@
UNDO_UPDATE_LISTENER,
)
from .util import (
+ accessory_friendly_name,
dismiss_setup_message,
get_persist_fullpath_for_entry_id,
- migrate_filesystem_state_data_for_primary_imported_entry_id,
port_is_available,
remove_state_files_for_entry_id,
show_setup_message,
+ state_needs_accessory_mode,
validate_entity_config,
)
@@ -102,6 +118,8 @@
STATUS_STOPPED = 2
STATUS_WAIT = 3
+PORT_CLEANUP_CHECK_INTERVAL_SECS = 1
+
def _has_all_unique_names_and_ports(bridges):
"""Validate that each homekit bridge configured has a unique name."""
@@ -115,6 +133,7 @@ def _has_all_unique_names_and_ports(bridges):
BRIDGE_SCHEMA = vol.All(
cv.deprecated(CONF_ZEROCONF_DEFAULT_INTERFACE),
cv.deprecated(CONF_SAFE_MODE),
+ cv.deprecated(CONF_AUTO_START),
vol.Schema(
{
vol.Optional(CONF_HOMEKIT_MODE, default=DEFAULT_HOMEKIT_MODE): vol.In(
@@ -224,26 +243,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
port = conf[CONF_PORT]
_LOGGER.debug("Begin setup HomeKit for %s", name)
- # If the previous instance hasn't cleaned up yet
- # we need to wait a bit
- if not await hass.async_add_executor_job(port_is_available, port):
- _LOGGER.warning("The local port %s is in use", port)
- raise ConfigEntryNotReady
-
- if CONF_ENTRY_INDEX in conf and conf[CONF_ENTRY_INDEX] == 0:
- _LOGGER.debug("Migrating legacy HomeKit data for %s", name)
- hass.async_add_executor_job(
- migrate_filesystem_state_data_for_primary_imported_entry_id,
- hass,
- entry.entry_id,
- )
-
- aid_storage = AccessoryAidStorage(hass, entry.entry_id)
-
- await aid_storage.async_initialize()
# ip_address and advertise_ip are yaml only
ip_address = conf.get(CONF_IP_ADDRESS)
advertise_ip = conf.get(CONF_ADVERTISE_IP)
+ # exclude_accessory_mode is only used for config flow
+ # to indicate that the config entry was setup after
+ # we started creating config entries for entities that
+ # to run in accessory mode and that we should never include
+ # these entities on the bridge. For backwards compatibility
+ # with users who have not migrated yet we do not do exclude
+ # these entities by default as we cannot migrate automatically
+ # since it requires a re-pairing.
+ exclude_accessory_mode = conf.get(
+ CONF_EXCLUDE_ACCESSORY_MODE, DEFAULT_EXCLUDE_ACCESSORY_MODE
+ )
homekit_mode = options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE)
entity_config = options.get(CONF_ENTITY_CONFIG, {}).copy()
auto_start = options.get(CONF_AUTO_START, DEFAULT_AUTO_START)
@@ -255,22 +268,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
port,
ip_address,
entity_filter,
+ exclude_accessory_mode,
entity_config,
homekit_mode,
advertise_ip,
entry.entry_id,
+ entry.title,
)
- zeroconf_instance = await zeroconf.async_get_instance(hass)
- await hass.async_add_executor_job(homekit.setup, zeroconf_instance)
-
- undo_listener = entry.add_update_listener(_async_update_listener)
hass.data[DOMAIN][entry.entry_id] = {
- AID_STORAGE: aid_storage,
HOMEKIT: homekit,
- UNDO_UPDATE_LISTENER: undo_listener,
+ UNDO_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener),
}
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, homekit.async_stop)
+
if hass.state == CoreState.running:
await homekit.async_start()
elif auto_start:
@@ -297,12 +309,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
if homekit.status == STATUS_RUNNING:
await homekit.async_stop()
+ logged_shutdown_wait = False
for _ in range(0, SHUTDOWN_TIMEOUT):
- if not await hass.async_add_executor_job(
- port_is_available, entry.data[CONF_PORT]
- ):
+ if await hass.async_add_executor_job(port_is_available, entry.data[CONF_PORT]):
+ break
+
+ if not logged_shutdown_wait:
_LOGGER.info("Waiting for the HomeKit server to shutdown")
- await asyncio.sleep(1)
+ logged_shutdown_wait = True
+
+ await asyncio.sleep(PORT_CLEANUP_CHECK_INTERVAL_SECS)
hass.data[DOMAIN].pop(entry.entry_id)
@@ -419,10 +435,12 @@ def __init__(
port,
ip_address,
entity_filter,
+ exclude_accessory_mode,
entity_config,
homekit_mode,
advertise_ip=None,
entry_id=None,
+ entry_title=None,
):
"""Initialize a HomeKit object."""
self.hass = hass
@@ -431,9 +449,12 @@ def __init__(
self._ip_address = ip_address
self._filter = entity_filter
self._config = entity_config
+ self._exclude_accessory_mode = exclude_accessory_mode
self._advertise_ip = advertise_ip
self._entry_id = entry_id
+ self._entry_title = entry_title
self._homekit_mode = homekit_mode
+ self.aid_storage = None
self.status = STATUS_READY
self.bridge = None
@@ -441,10 +462,6 @@ def __init__(
def setup(self, zeroconf_instance):
"""Set up bridge and accessory driver."""
- # pylint: disable=import-outside-toplevel
- from .accessories import HomeDriver
-
- self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop)
ip_addr = self._ip_address or get_local_ip()
persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id)
@@ -452,6 +469,7 @@ def setup(self, zeroconf_instance):
self.hass,
self._entry_id,
self._name,
+ self._entry_title,
loop=self.hass.loop,
address=ip_addr,
port=self._port,
@@ -464,8 +482,11 @@ def setup(self, zeroconf_instance):
# as pyhap uses a random one until state is restored
if os.path.exists(persist_file):
self.driver.load()
- else:
- self.driver.persist()
+ self.driver.state.config_version += 1
+ if self.driver.state.config_version > 65535:
+ self.driver.state.config_version = 1
+
+ self.driver.persist()
def reset_accessories(self, entity_ids):
"""Reset the accessory to load the latest configuration."""
@@ -473,10 +494,9 @@ def reset_accessories(self, entity_ids):
self.driver.config_changed()
return
- aid_storage = self.hass.data[DOMAIN][self._entry_id][AID_STORAGE]
removed = []
for entity_id in entity_ids:
- aid = aid_storage.get_or_allocate_aid_for_entity_id(entity_id)
+ aid = self.aid_storage.get_or_allocate_aid_for_entity_id(entity_id)
if aid not in self.bridge.accessories:
continue
@@ -501,9 +521,6 @@ def reset_accessories(self, entity_ids):
def add_bridge_accessory(self, state):
"""Try adding accessory to bridge if configured beforehand."""
- if not self._filter(state.entity_id):
- return
-
# The bridge itself counts as an accessory
if len(self.bridge.accessories) + 1 >= MAX_DEVICES:
_LOGGER.warning(
@@ -513,9 +530,19 @@ def add_bridge_accessory(self, state):
)
return
- aid = self.hass.data[DOMAIN][self._entry_id][
- AID_STORAGE
- ].get_or_allocate_aid_for_entity_id(state.entity_id)
+ if state_needs_accessory_mode(state):
+ if self._exclude_accessory_mode:
+ return
+ _LOGGER.warning(
+ "The bridge %s has entity %s. For best performance, "
+ "and to prevent unexpected unavailability, create and "
+ "pair a separate HomeKit instance in accessory mode for "
+ "this entity",
+ self._name,
+ state.entity_id,
+ )
+
+ aid = self.aid_storage.get_or_allocate_aid_for_entity_id(state.entity_id)
conf = self._config.pop(state.entity_id, {})
# If an accessory cannot be created or added due to an exception
# of any kind (usually in pyhap) it should not prevent
@@ -523,24 +550,6 @@ def add_bridge_accessory(self, state):
try:
acc = get_accessory(self.hass, self.driver, state, aid, conf)
if acc is not None:
- if acc.category == CATEGORY_CAMERA:
- _LOGGER.warning(
- "The bridge %s has camera %s. For best performance, "
- "and to prevent unexpected unavailability, create and "
- "pair a separate HomeKit instance in accessory mode for "
- "each camera.",
- self._name,
- acc.entity_id,
- )
- elif acc.category == CATEGORY_TELEVISION:
- _LOGGER.warning(
- "The bridge %s has tv %s. For best performance, "
- "and to prevent unexpected unavailability, create and "
- "pair a separate HomeKit instance in accessory mode for "
- "each tv media player.",
- self._name,
- acc.entity_id,
- )
self.bridge.add_accessory(acc)
except Exception: # pylint: disable=broad-except
_LOGGER.exception(
@@ -554,15 +563,10 @@ def remove_bridge_accessory(self, aid):
acc = self.bridge.accessories.pop(aid)
return acc
- async def async_start(self, *args):
- """Start the accessory driver."""
- if self.status != STATUS_READY:
- return
- self.status = STATUS_WAIT
-
- ent_reg = await entity_registry.async_get_registry(self.hass)
- dev_reg = await device_registry.async_get_registry(self.hass)
-
+ async def async_configure_accessories(self):
+ """Configure accessories for the included states."""
+ dev_reg = device_registry.async_get(self.hass)
+ ent_reg = entity_registry.async_get(self.hass)
device_lookup = ent_reg.async_get_device_class_lookup(
{
(BINARY_SENSOR_DOMAIN, DEVICE_CLASS_BATTERY_CHARGING),
@@ -573,10 +577,9 @@ async def async_start(self, *args):
}
)
- bridged_states = []
+ entity_states = []
for state in self.hass.states.async_all():
entity_id = state.entity_id
-
if not self._filter(entity_id):
continue
@@ -587,17 +590,40 @@ async def async_start(self, *args):
)
self._async_configure_linked_sensors(ent_reg_ent, device_lookup, state)
- bridged_states.append(state)
+ entity_states.append(state)
- self._async_register_bridge(dev_reg)
- await self.hass.async_add_executor_job(self._start, bridged_states)
+ return entity_states
+
+ async def async_start(self, *args):
+ """Load storage and start."""
+ if self.status != STATUS_READY:
+ return
+ self.status = STATUS_WAIT
+ zc_instance = await zeroconf.async_get_instance(self.hass)
+ await self.hass.async_add_executor_job(self.setup, zc_instance)
+ self.aid_storage = AccessoryAidStorage(self.hass, self._entry_id)
+ await self.aid_storage.async_initialize()
+ await self._async_create_accessories()
+ self._async_register_bridge()
_LOGGER.debug("Driver start for %s", self._name)
- self.hass.add_job(self.driver.start_service)
+ await self.driver.async_start()
self.status = STATUS_RUNNING
+ if self.driver.state.paired:
+ return
+
+ show_setup_message(
+ self.hass,
+ self._entry_id,
+ accessory_friendly_name(self._entry_title, self.driver.accessory),
+ self.driver.state.pincode,
+ self.driver.accessory.xhm_uri(),
+ )
+
@callback
- def _async_register_bridge(self, dev_reg):
+ def _async_register_bridge(self):
"""Register the bridge as a device so homekit_controller and exclude it from discovery."""
+ dev_reg = device_registry.async_get(self.hass)
formatted_mac = device_registry.format_mac(self.driver.state.mac)
# Connections and identifiers are both used here.
#
@@ -621,8 +647,9 @@ def _async_register_bridge(self, dev_reg):
identifiers={identifier},
connections={connection},
manufacturer=MANUFACTURER,
- name=self._name,
- model=f"Home Assistant HomeKit {hk_mode_name}",
+ name=accessory_friendly_name(self._entry_title, self.driver.accessory),
+ model=f"HomeKit {hk_mode_name}",
+ entry_type="service",
)
@callback
@@ -639,43 +666,20 @@ def _async_purge_old_bridges(self, dev_reg, identifier, connection):
for device_id in devices_to_purge:
dev_reg.async_remove_device(device_id)
- def _start(self, bridged_states):
- # pylint: disable=unused-import, import-outside-toplevel
- from . import ( # noqa: F401
- type_cameras,
- type_covers,
- type_fans,
- type_humidifiers,
- type_lights,
- type_locks,
- type_media_players,
- type_security_systems,
- type_sensors,
- type_switches,
- type_thermostats,
- )
-
+ async def _async_create_accessories(self):
+ """Create the accessories."""
+ entity_states = await self.async_configure_accessories()
if self._homekit_mode == HOMEKIT_MODE_ACCESSORY:
- state = bridged_states[0]
+ state = entity_states[0]
conf = self._config.pop(state.entity_id, {})
acc = get_accessory(self.hass, self.driver, state, STANDALONE_AID, conf)
- self.driver.add_accessory(acc)
else:
- from .accessories import HomeBridge
-
self.bridge = HomeBridge(self.hass, self.driver, self._name)
- for state in bridged_states:
+ for state in entity_states:
self.add_bridge_accessory(state)
- self.driver.add_accessory(self.bridge)
+ acc = self.bridge
- if not self.driver.state.paired:
- show_setup_message(
- self.hass,
- self._entry_id,
- self._name,
- self.driver.state.pincode,
- self.driver.accessory.xhm_uri(),
- )
+ await self.hass.async_add_executor_job(self.driver.add_accessory, acc)
async def async_stop(self, *args):
"""Stop the accessory driver."""
diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py
index 51b6508149b776..307dbf0e80636c 100644
--- a/homeassistant/components/homekit/accessories.py
+++ b/homeassistant/components/homekit/accessories.py
@@ -1,7 +1,4 @@
"""Extend the basic Accessory and Bridge functions."""
-from datetime import timedelta
-from functools import partial, wraps
-from inspect import getmodule
import logging
from pyhap.accessory import Accessory, Bridge
@@ -15,6 +12,7 @@
DEVICE_CLASS_WINDOW,
)
from homeassistant.components.media_player import DEVICE_CLASS_TV
+from homeassistant.components.remote import SUPPORT_ACTIVITY
from homeassistant.const import (
ATTR_BATTERY_CHARGING,
ATTR_BATTERY_LEVEL,
@@ -25,6 +23,8 @@
ATTR_UNIT_OF_MEASUREMENT,
CONF_NAME,
CONF_TYPE,
+ DEVICE_CLASS_CO,
+ DEVICE_CLASS_CO2,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
@@ -37,11 +37,7 @@
__version__,
)
from homeassistant.core import Context, callback as ha_callback, split_entity_id
-from homeassistant.helpers.event import (
- async_track_state_change_event,
- track_point_in_utc_time,
-)
-from homeassistant.util import dt as dt_util
+from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.util.decorator import Registry
from .const import (
@@ -60,10 +56,7 @@
CONF_LINKED_BATTERY_CHARGING_SENSOR,
CONF_LINKED_BATTERY_SENSOR,
CONF_LOW_BATTERY_THRESHOLD,
- DEBOUNCE_TIMEOUT,
DEFAULT_LOW_BATTERY_THRESHOLD,
- DEVICE_CLASS_CO,
- DEVICE_CLASS_CO2,
DEVICE_CLASS_PM25,
EVENT_HOMEKIT_CHANGED,
HK_CHARGING,
@@ -79,6 +72,7 @@
TYPE_VALVE,
)
from .util import (
+ accessory_friendly_name,
convert_to_float,
dismiss_setup_message,
format_sw_version,
@@ -98,37 +92,6 @@
TYPES = Registry()
-def debounce(func):
- """Decorate function to debounce callbacks from HomeKit."""
-
- @ha_callback
- def call_later_listener(self, *args):
- """Handle call_later callback."""
- debounce_params = self.debounce.pop(func.__name__, None)
- if debounce_params:
- self.hass.async_add_executor_job(func, self, *debounce_params[1:])
-
- @wraps(func)
- def wrapper(self, *args):
- """Start async timer."""
- debounce_params = self.debounce.pop(func.__name__, None)
- if debounce_params:
- debounce_params[0]() # remove listener
- remove_listener = track_point_in_utc_time(
- self.hass,
- partial(call_later_listener, self),
- dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT),
- )
- self.debounce[func.__name__] = (remove_listener, *args)
- logger.debug(
- "%s: Start %s timeout", self.entity_id, func.__name__.replace("set_", "")
- )
-
- name = getmodule(func).__name__
- logger = logging.getLogger(name)
- return wrapper
-
-
def get_accessory(hass, driver, state, aid, config):
"""Take state and return an accessory object if supported."""
if not aid:
@@ -141,6 +104,7 @@ def get_accessory(hass, driver, state, aid, config):
a_type = None
name = config.get(CONF_NAME, state.name)
+ features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if state.domain == "alarm_control_panel":
a_type = "SecuritySystem"
@@ -153,7 +117,6 @@ def get_accessory(hass, driver, state, aid, config):
elif state.domain == "cover":
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
- features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if device_class in (DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE) and features & (
cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE
@@ -205,7 +168,7 @@ def get_accessory(hass, driver, state, aid, config):
a_type = "AirQualitySensor"
elif device_class == DEVICE_CLASS_CO:
a_type = "CarbonMonoxideSensor"
- elif device_class == DEVICE_CLASS_CO2 or DEVICE_CLASS_CO2 in state.entity_id:
+ elif device_class == DEVICE_CLASS_CO2 or "co2" in state.entity_id:
a_type = "CarbonDioxideSensor"
elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ("lm", LIGHT_LUX):
a_type = "LightSensor"
@@ -217,6 +180,9 @@ def get_accessory(hass, driver, state, aid, config):
elif state.domain == "vacuum":
a_type = "Vacuum"
+ elif state.domain == "remote" and features & SUPPORT_ACTIVITY:
+ a_type = "ActivityRemote"
+
elif state.domain in ("automation", "input_boolean", "remote", "scene", "script"):
a_type = "Switch"
@@ -278,7 +244,6 @@ def __init__(
self.category = category
self.entity_id = entity_id
self.hass = hass
- self.debounce = {}
self._subscriptions = []
self._char_battery = None
self._char_charging = None
@@ -340,17 +305,7 @@ def available(self):
return state is not None and state.state != STATE_UNAVAILABLE
async def run(self):
- """Handle accessory driver started event.
-
- Run inside the HAP-python event loop.
- """
- self.hass.add_job(self.run_handler)
-
- async def run_handler(self):
- """Handle accessory driver started event.
-
- Run inside the Home Assistant event loop.
- """
+ """Handle accessory driver started event."""
state = self.hass.states.get(self.entity_id)
self.async_update_state_callback(state)
self._subscriptions.append(
@@ -481,15 +436,9 @@ def async_update_state(self, new_state):
"""
raise NotImplementedError()
- def call_service(self, domain, service, service_data, value=None):
+ @ha_callback
+ def async_call_service(self, domain, service, service_data, value=None):
"""Fire event and call service for changes from HomeKit."""
- self.hass.add_job(self.async_call_service, domain, service, service_data, value)
-
- async def async_call_service(self, domain, service, service_data, value=None):
- """Fire event and call service for changes from HomeKit.
-
- This method must be run in the event loop.
- """
event_data = {
ATTR_ENTITY_ID: self.entity_id,
ATTR_DISPLAY_NAME: self.display_name,
@@ -499,8 +448,10 @@ async def async_call_service(self, domain, service, service_data, value=None):
context = Context()
self.hass.bus.async_fire(EVENT_HOMEKIT_CHANGED, event_data, context=context)
- await self.hass.services.async_call(
- domain, service, service_data, context=context
+ self.hass.async_create_task(
+ self.hass.services.async_call(
+ domain, service, service_data, context=context
+ )
)
@ha_callback
@@ -527,28 +478,29 @@ def __init__(self, hass, driver, name):
def setup_message(self):
"""Prevent print of pyhap setup message to terminal."""
- def get_snapshot(self, info):
+ async def async_get_snapshot(self, info):
"""Get snapshot from accessory if supported."""
acc = self.accessories.get(info["aid"])
if acc is None:
raise ValueError("Requested snapshot for missing accessory")
- if not hasattr(acc, "get_snapshot"):
+ if not hasattr(acc, "async_get_snapshot"):
raise ValueError(
"Got a request for snapshot, but the Accessory "
- 'does not define a "get_snapshot" method'
+ 'does not define a "async_get_snapshot" method'
)
- return acc.get_snapshot(info)
+ return await acc.async_get_snapshot(info)
class HomeDriver(AccessoryDriver):
"""Adapter class for AccessoryDriver."""
- def __init__(self, hass, entry_id, bridge_name, **kwargs):
+ def __init__(self, hass, entry_id, bridge_name, entry_title, **kwargs):
"""Initialize a AccessoryDriver object."""
super().__init__(**kwargs)
self.hass = hass
self._entry_id = entry_id
self._bridge_name = bridge_name
+ self._entry_title = entry_title
def pair(self, client_uuid, client_public):
"""Override super function to dismiss setup message if paired."""
@@ -560,10 +512,14 @@ def pair(self, client_uuid, client_public):
def unpair(self, client_uuid):
"""Override super function to show setup message if unpaired."""
super().unpair(client_uuid)
+
+ if self.state.paired:
+ return
+
show_setup_message(
self.hass,
self._entry_id,
- self._bridge_name,
+ accessory_friendly_name(self._entry_title, self.accessory),
self.state.pincode,
self.accessory.xhm_uri(),
)
diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py
index d8708168e12ad7..683e533d2dfbf4 100644
--- a/homeassistant/components/homekit/config_flow.py
+++ b/homeassistant/components/homekit/config_flow.py
@@ -1,11 +1,14 @@
"""Config flow for HomeKit integration."""
-import logging
import random
+import re
import string
import voluptuous as vol
from homeassistant import config_entries
+from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
+from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
+from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_FRIENDLY_NAME,
@@ -27,20 +30,21 @@
from .const import (
CONF_AUTO_START,
CONF_ENTITY_CONFIG,
+ CONF_EXCLUDE_ACCESSORY_MODE,
CONF_FILTER,
CONF_HOMEKIT_MODE,
CONF_VIDEO_CODEC,
DEFAULT_AUTO_START,
DEFAULT_CONFIG_FLOW_PORT,
DEFAULT_HOMEKIT_MODE,
+ DOMAIN,
HOMEKIT_MODE_ACCESSORY,
+ HOMEKIT_MODE_BRIDGE,
HOMEKIT_MODES,
- SHORT_ACCESSORY_NAME,
SHORT_BRIDGE_NAME,
VIDEO_CODEC_COPY,
)
-from .const import DOMAIN # pylint:disable=unused-import
-from .util import find_next_available_port
+from .util import async_find_next_available_port, state_needs_accessory_mode
CONF_CAMERA_COPY = "camera_copy"
CONF_INCLUDE_EXCLUDE_MODE = "include_exclude_mode"
@@ -50,11 +54,16 @@
INCLUDE_EXCLUDE_MODES = [MODE_EXCLUDE, MODE_INCLUDE]
+DOMAINS_NEED_ACCESSORY_MODE = [CAMERA_DOMAIN, MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN]
+NEVER_BRIDGED_DOMAINS = [CAMERA_DOMAIN]
+
+CAMERA_ENTITY_PREFIX = f"{CAMERA_DOMAIN}."
+
SUPPORTED_DOMAINS = [
"alarm_control_panel",
"automation",
"binary_sensor",
- "camera",
+ CAMERA_DOMAIN,
"climate",
"cover",
"demo",
@@ -64,9 +73,9 @@
"input_boolean",
"light",
"lock",
- "media_player",
+ MEDIA_PLAYER_DOMAIN,
"person",
- "remote",
+ REMOTE_DOMAIN,
"scene",
"script",
"sensor",
@@ -78,21 +87,19 @@
DEFAULT_DOMAINS = [
"alarm_control_panel",
"climate",
+ CAMERA_DOMAIN,
"cover",
"humidifier",
"fan",
"light",
"lock",
- "media_player",
+ MEDIA_PLAYER_DOMAIN,
+ REMOTE_DOMAIN,
"switch",
"vacuum",
"water_heater",
]
-DOMAINS_PREFER_ACCESSORY_MODE = ["camera", "media_player"]
-
-CAMERA_ENTITY_PREFIX = "camera."
-
_EMPTY_ENTITY_FILTER = {
CONF_INCLUDE_DOMAINS: [],
CONF_EXCLUDE_DOMAINS: [],
@@ -100,8 +107,6 @@
CONF_EXCLUDE_ENTITIES: [],
}
-_LOGGER = logging.getLogger(__name__)
-
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for HomeKit."""
@@ -112,32 +117,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self):
"""Initialize config flow."""
self.hk_data = {}
- self.entry_title = None
- async def async_step_accessory_mode(self, user_input=None):
- """Choose specific entity in accessory mode."""
- if user_input is not None:
- entity_id = user_input[CONF_ENTITY_ID]
- entity_filter = _EMPTY_ENTITY_FILTER.copy()
- entity_filter[CONF_INCLUDE_ENTITIES] = [entity_id]
- self.hk_data[CONF_FILTER] = entity_filter
- if entity_id.startswith(CAMERA_ENTITY_PREFIX):
- self.hk_data[CONF_ENTITY_CONFIG] = {
- entity_id: {CONF_VIDEO_CODEC: VIDEO_CODEC_COPY}
- }
- return await self.async_step_pairing()
-
- all_supported_entities = _async_get_matching_entities(
- self.hass, domains=DOMAINS_PREFER_ACCESSORY_MODE
- )
- return self.async_show_form(
- step_id="accessory_mode",
- data_schema=vol.Schema(
- {vol.Required(CONF_ENTITY_ID): vol.In(all_supported_entities)}
- ),
- )
-
- async def async_step_bridge_mode(self, user_input=None):
+ async def async_step_user(self, user_input=None):
"""Choose specific domains in bridge mode."""
if user_input is not None:
entity_filter = _EMPTY_ENTITY_FILTER.copy()
@@ -145,9 +126,10 @@ async def async_step_bridge_mode(self, user_input=None):
self.hk_data[CONF_FILTER] = entity_filter
return await self.async_step_pairing()
+ self.hk_data[CONF_HOMEKIT_MODE] = HOMEKIT_MODE_BRIDGE
default_domains = [] if self._async_current_names() else DEFAULT_DOMAINS
return self.async_show_form(
- step_id="bridge_mode",
+ step_id="user",
data_schema=vol.Schema(
{
vol.Required(
@@ -160,41 +142,72 @@ async def async_step_bridge_mode(self, user_input=None):
async def async_step_pairing(self, user_input=None):
"""Pairing instructions."""
if user_input is not None:
- return self.async_create_entry(title=self.entry_title, data=self.hk_data)
+ port = await async_find_next_available_port(
+ self.hass, DEFAULT_CONFIG_FLOW_PORT
+ )
+ await self._async_add_entries_for_accessory_mode_entities(port)
+ self.hk_data[CONF_PORT] = port
+ include_domains_filter = self.hk_data[CONF_FILTER][CONF_INCLUDE_DOMAINS]
+ for domain in NEVER_BRIDGED_DOMAINS:
+ if domain in include_domains_filter:
+ include_domains_filter.remove(domain)
+ return self.async_create_entry(
+ title=f"{self.hk_data[CONF_NAME]}:{self.hk_data[CONF_PORT]}",
+ data=self.hk_data,
+ )
- self.hk_data[CONF_PORT] = await self._async_available_port()
- self.hk_data[CONF_NAME] = self._async_available_name(
- self.hk_data[CONF_HOMEKIT_MODE]
- )
- self.entry_title = f"{self.hk_data[CONF_NAME]}:{self.hk_data[CONF_PORT]}"
+ self.hk_data[CONF_NAME] = self._async_available_name(SHORT_BRIDGE_NAME)
+ self.hk_data[CONF_EXCLUDE_ACCESSORY_MODE] = True
return self.async_show_form(
step_id="pairing",
description_placeholders={CONF_NAME: self.hk_data[CONF_NAME]},
)
- async def async_step_user(self, user_input=None):
- """Handle the initial step."""
- errors = {}
+ async def _async_add_entries_for_accessory_mode_entities(self, last_assigned_port):
+ """Generate new flows for entities that need their own instances."""
+ accessory_mode_entity_ids = _async_get_entity_ids_for_accessory_mode(
+ self.hass, self.hk_data[CONF_FILTER][CONF_INCLUDE_DOMAINS]
+ )
+ exiting_entity_ids_accessory_mode = _async_entity_ids_with_accessory_mode(
+ self.hass
+ )
+ next_port_to_check = last_assigned_port + 1
+ for entity_id in accessory_mode_entity_ids:
+ if entity_id in exiting_entity_ids_accessory_mode:
+ continue
+ port = await async_find_next_available_port(self.hass, next_port_to_check)
+ next_port_to_check = port + 1
+ self.hass.async_create_task(
+ self.hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "accessory"},
+ data={CONF_ENTITY_ID: entity_id, CONF_PORT: port},
+ )
+ )
- if user_input is not None:
- self.hk_data = {
- CONF_HOMEKIT_MODE: user_input[CONF_HOMEKIT_MODE],
+ async def async_step_accessory(self, accessory_input):
+ """Handle creation a single accessory in accessory mode."""
+ entity_id = accessory_input[CONF_ENTITY_ID]
+ port = accessory_input[CONF_PORT]
+
+ state = self.hass.states.get(entity_id)
+ name = state.attributes.get(ATTR_FRIENDLY_NAME) or state.entity_id
+ entity_filter = _EMPTY_ENTITY_FILTER.copy()
+ entity_filter[CONF_INCLUDE_ENTITIES] = [entity_id]
+
+ entry_data = {
+ CONF_PORT: port,
+ CONF_NAME: self._async_available_name(name),
+ CONF_HOMEKIT_MODE: HOMEKIT_MODE_ACCESSORY,
+ CONF_FILTER: entity_filter,
+ }
+ if entity_id.startswith(CAMERA_ENTITY_PREFIX):
+ entry_data[CONF_ENTITY_CONFIG] = {
+ entity_id: {CONF_VIDEO_CODEC: VIDEO_CODEC_COPY}
}
- if user_input[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY:
- return await self.async_step_accessory_mode()
- return await self.async_step_bridge_mode()
- homekit_mode = self.hk_data.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE)
- return self.async_show_form(
- step_id="user",
- data_schema=vol.Schema(
- {
- vol.Required(CONF_HOMEKIT_MODE, default=homekit_mode): vol.In(
- HOMEKIT_MODES
- )
- }
- ),
- errors=errors,
+ return self.async_create_entry(
+ title=f"{name}:{entry_data[CONF_PORT]}", data=entry_data
)
async def async_step_import(self, user_input=None):
@@ -205,37 +218,29 @@ async def async_step_import(self, user_input=None):
title=f"{user_input[CONF_NAME]}:{user_input[CONF_PORT]}", data=user_input
)
- async def _async_available_port(self):
- """Return an available port the bridge."""
- return await self.hass.async_add_executor_job(
- find_next_available_port, DEFAULT_CONFIG_FLOW_PORT
- )
-
@callback
def _async_current_names(self):
"""Return a set of bridge names."""
return {
entry.data[CONF_NAME]
- for entry in self._async_current_entries()
+ for entry in self._async_current_entries(include_ignore=False)
if CONF_NAME in entry.data
}
@callback
- def _async_available_name(self, homekit_mode):
+ def _async_available_name(self, requested_name):
"""Return an available for the bridge."""
+ current_names = self._async_current_names()
+ valid_mdns_name = re.sub("[^A-Za-z0-9 ]+", " ", requested_name)
- base_name = SHORT_BRIDGE_NAME
- if homekit_mode == HOMEKIT_MODE_ACCESSORY:
- base_name = SHORT_ACCESSORY_NAME
+ if valid_mdns_name not in current_names:
+ return valid_mdns_name
- # We always pick a RANDOM name to avoid Zeroconf
- # name collisions. If the name has been seen before
- # pairing will probably fail.
- acceptable_chars = string.ascii_uppercase + string.digits
+ acceptable_mdns_chars = string.ascii_uppercase + string.digits
suggested_name = None
- while not suggested_name or suggested_name in self._async_current_names():
- trailer = "".join(random.choices(acceptable_chars, k=4))
- suggested_name = f"{base_name} {trailer}"
+ while not suggested_name or suggested_name in current_names:
+ trailer = "".join(random.choices(acceptable_mdns_chars, k=2))
+ suggested_name = f"{valid_mdns_name} {trailer}"
return suggested_name
@@ -244,10 +249,10 @@ def _async_is_unique_name_port(self, user_input):
"""Determine is a name or port is already used."""
name = user_input[CONF_NAME]
port = user_input[CONF_PORT]
- for entry in self._async_current_entries():
- if entry.data[CONF_NAME] == name or entry.data[CONF_PORT] == port:
- return False
- return True
+ return not any(
+ entry.data[CONF_NAME] == name or entry.data[CONF_PORT] == port
+ for entry in self._async_current_entries(include_ignore=False)
+ )
@staticmethod
@callback
@@ -360,15 +365,26 @@ async def async_step_include_exclude(self, user_input=None):
if domain not in domains_with_entities_selected
]
- for entity_id in list(self.included_cameras):
- if entity_id not in entities:
- self.included_cameras.remove(entity_id)
+ self.included_cameras = {
+ entity_id
+ for entity_id in entities
+ if entity_id.startswith(CAMERA_ENTITY_PREFIX)
+ }
else:
entity_filter[CONF_INCLUDE_DOMAINS] = self.hk_options[CONF_DOMAINS]
entity_filter[CONF_EXCLUDE_ENTITIES] = entities
- for entity_id in entities:
- if entity_id in self.included_cameras:
- self.included_cameras.remove(entity_id)
+ if CAMERA_DOMAIN in entity_filter[CONF_INCLUDE_DOMAINS]:
+ camera_entities = _async_get_matching_entities(
+ self.hass,
+ domains=[CAMERA_DOMAIN],
+ )
+ self.included_cameras = {
+ entity_id
+ for entity_id in camera_entities
+ if entity_id not in entities
+ }
+ else:
+ self.included_cameras = set()
self.hk_options[CONF_FILTER] = entity_filter
@@ -382,11 +398,6 @@ async def async_step_include_exclude(self, user_input=None):
self.hass,
domains=self.hk_options[CONF_DOMAINS],
)
- self.included_cameras = {
- entity_id
- for entity_id in all_supported_entities
- if entity_id.startswith(CAMERA_ENTITY_PREFIX)
- }
data_schema = {}
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
@@ -420,10 +431,9 @@ async def async_step_init(self, user_input=None):
self.hk_options.update(user_input)
return await self.async_step_include_exclude()
- hk_options = dict(self.config_entry.options)
- entity_filter = hk_options.get(CONF_FILTER, {})
-
- homekit_mode = hk_options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE)
+ self.hk_options = dict(self.config_entry.options)
+ entity_filter = self.hk_options.get(CONF_FILTER, {})
+ homekit_mode = self.hk_options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE)
domains = entity_filter.get(CONF_INCLUDE_DOMAINS, [])
include_entities = entity_filter.get(CONF_INCLUDE_ENTITIES)
if include_entities:
@@ -448,7 +458,7 @@ async def async_step_init(self, user_input=None):
def _async_get_matching_entities(hass, domains=None):
"""Fetch all entities or entities in the given domains."""
return {
- state.entity_id: f"{state.entity_id} ({state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)})"
+ state.entity_id: f"{state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)} ({state.entity_id})"
for state in sorted(
hass.states.async_all(domains and set(domains)),
key=lambda item: item.entity_id,
@@ -458,7 +468,41 @@ def _async_get_matching_entities(hass, domains=None):
def _domains_set_from_entities(entity_ids):
"""Build a set of domains for the given entity ids."""
- domains = set()
- for entity_id in entity_ids:
- domains.add(split_entity_id(entity_id)[0])
- return domains
+ return {split_entity_id(entity_id)[0] for entity_id in entity_ids}
+
+
+@callback
+def _async_get_entity_ids_for_accessory_mode(hass, include_domains):
+ """Build a list of entities that should be paired in accessory mode."""
+ accessory_mode_domains = {
+ domain for domain in include_domains if domain in DOMAINS_NEED_ACCESSORY_MODE
+ }
+
+ if not accessory_mode_domains:
+ return []
+
+ return [
+ state.entity_id
+ for state in hass.states.async_all(accessory_mode_domains)
+ if state_needs_accessory_mode(state)
+ ]
+
+
+@callback
+def _async_entity_ids_with_accessory_mode(hass):
+ """Return a set of entity ids that have config entries in accessory mode."""
+
+ entity_ids = set()
+
+ current_entries = hass.config_entries.async_entries(DOMAIN)
+ for entry in current_entries:
+ # We have to handle the case where the data has not yet
+ # been migrated to options because the data was just
+ # imported and the entry was never started
+ target = entry.options if CONF_HOMEKIT_MODE in entry.options else entry.data
+ if target.get(CONF_HOMEKIT_MODE) != HOMEKIT_MODE_ACCESSORY:
+ continue
+
+ entity_ids.add(target[CONF_FILTER][CONF_INCLUDE_ENTITIES][0])
+
+ return entity_ids
diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py
index fac4168a79b309..abfc6a2aa381fa 100644
--- a/homeassistant/components/homekit/const.py
+++ b/homeassistant/components/homekit/const.py
@@ -5,7 +5,6 @@
DEVICE_PRECISION_LEEWAY = 6
DOMAIN = "homekit"
HOMEKIT_FILE = ".homekit.state"
-AID_STORAGE = "homekit-aid-allocations"
HOMEKIT_PAIRING_QR = "homekit-pairing-qr"
HOMEKIT_PAIRING_QR_SECRET = "homekit-pairing-qr-secret"
HOMEKIT = "homekit"
@@ -42,6 +41,7 @@
CONF_FEATURE = "feature"
CONF_FEATURE_LIST = "feature_list"
CONF_FILTER = "filter"
+CONF_EXCLUDE_ACCESSORY_MODE = "exclude_accessory_mode"
CONF_LINKED_BATTERY_SENSOR = "linked_battery_sensor"
CONF_LINKED_BATTERY_CHARGING_SENSOR = "linked_battery_charging_sensor"
CONF_LINKED_DOORBELL_SENSOR = "linked_doorbell_sensor"
@@ -68,12 +68,13 @@
DEFAULT_AUDIO_MAP = "0:a:0"
DEFAULT_AUDIO_PACKET_SIZE = 188
DEFAULT_AUTO_START = True
+DEFAULT_EXCLUDE_ACCESSORY_MODE = False
DEFAULT_LOW_BATTERY_THRESHOLD = 20
DEFAULT_MAX_FPS = 30
DEFAULT_MAX_HEIGHT = 1080
DEFAULT_MAX_WIDTH = 1920
-DEFAULT_PORT = 51827
-DEFAULT_CONFIG_FLOW_PORT = 51828
+DEFAULT_PORT = 21063
+DEFAULT_CONFIG_FLOW_PORT = 21064
DEFAULT_SAFE_MODE = False
DEFAULT_VIDEO_CODEC = VIDEO_CODEC_LIBX264
DEFAULT_VIDEO_MAP = "0:v:0"
@@ -236,8 +237,6 @@
PROP_VALID_VALUES = "ValidValues"
# #### Device Classes ####
-DEVICE_CLASS_CO = "co"
-DEVICE_CLASS_CO2 = "co2"
DEVICE_CLASS_DOOR = "door"
DEVICE_CLASS_GARAGE_DOOR = "garage_door"
DEVICE_CLASS_GAS = "gas"
diff --git a/homeassistant/components/homekit/img_util.py b/homeassistant/components/homekit/img_util.py
index 2baede8d957bb8..860d798f1137b6 100644
--- a/homeassistant/components/homekit/img_util.py
+++ b/homeassistant/components/homekit/img_util.py
@@ -63,6 +63,6 @@ def __init__(self):
TurboJPEGSingleton.__instance = TurboJPEG()
except Exception: # pylint: disable=broad-except
_LOGGER.exception(
- "libturbojpeg is not installed, cameras may impact HomeKit performance"
+ "Error loading libturbojpeg; Cameras may impact HomeKit performance"
)
TurboJPEGSingleton.__instance = False
diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json
index d188dd270ab0b7..53438138e43c0c 100644
--- a/homeassistant/components/homekit/manifest.json
+++ b/homeassistant/components/homekit/manifest.json
@@ -3,7 +3,7 @@
"name": "HomeKit",
"documentation": "https://www.home-assistant.io/integrations/homekit",
"requirements": [
- "HAP-python==3.1.0",
+ "HAP-python==3.4.1",
"fnvhash==0.1.0",
"PyQRCode==1.2.1",
"base36==0.1.1",
diff --git a/homeassistant/components/homekit/services.yaml b/homeassistant/components/homekit/services.yaml
index c2dde2cac6c975..a6b09a80e7ffe9 100644
--- a/homeassistant/components/homekit/services.yaml
+++ b/homeassistant/components/homekit/services.yaml
@@ -1,14 +1,13 @@
# Describes the format for available HomeKit services
start:
- description: Starts the HomeKit driver.
+ description: Starts the HomeKit driver
reload:
- description: Reload homekit and re-process yaml configuration.
+ description: Reload homekit and re-process YAML configuration
reset_accessory:
- description: Reset a HomeKit accessory. This can be useful when changing a media_player’s device class to tv, linking a battery, or whenever Home Assistant adds support for new HomeKit features to existing entities.
- fields:
- entity_id:
- description: Name of the entity to reset.
- example: "binary_sensor.grid_status"
+ description: Reset a HomeKit accessory
+ target:
+ entity: {}
+
diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json
index ed825ada23ce7b..56bc5438eac511 100644
--- a/homeassistant/components/homekit/strings.json
+++ b/homeassistant/components/homekit/strings.json
@@ -8,7 +8,7 @@
"init": {
"data": {
"mode": "[%key:common::config_flow::data::mode%]",
- "include_domains": "[%key:component::homekit::config::step::bridge_mode::data::include_domains%]"
+ "include_domains": "[%key:component::homekit::config::step::user::data::include_domains%]"
},
"description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be included to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.",
"title": "Select domains to be included."
@@ -18,7 +18,7 @@
"mode": "[%key:common::config_flow::data::mode%]",
"entities": "Entities"
},
- "description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.",
+ "description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, a separate HomeKit accessory will be created for each tv media player, activity based remote, lock, and camera.",
"title": "Select entities to be included"
},
"cameras": {
@@ -40,29 +40,15 @@
"config": {
"step": {
"user": {
- "data": {
- "mode": "[%key:common::config_flow::data::mode%]"
- },
- "description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.",
- "title": "Activate HomeKit"
- },
- "accessory_mode": {
- "data": {
- "entity_id": "Entity"
- },
- "description": "Choose the entity to be included. In accessory mode, only a single entity is included.",
- "title": "Select entity to be included"
- },
- "bridge_mode": {
"data": {
"include_domains": "Domains to include"
},
- "description": "Choose the domains to be included. All supported entities in the domain will be included.",
+ "description": "Choose the domains to be included. All supported entities in the domain will be included. A separate HomeKit instance in accessory mode will be created for each tv media player and camera.",
"title": "Select domains to be included"
},
"pairing": {
"title": "Pair HomeKit",
- "description": "As soon as the {name} is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d."
+ "description": "To complete pairing following the instructions in \u201cNotifications\u201d under \u201cHomeKit Pairing\u201d."
}
},
"abort": {
diff --git a/homeassistant/components/homekit/translations/ca.json b/homeassistant/components/homekit/translations/ca.json
index 63f461ea3447a8..8093cb1792f275 100644
--- a/homeassistant/components/homekit/translations/ca.json
+++ b/homeassistant/components/homekit/translations/ca.json
@@ -4,17 +4,32 @@
"port_name_in_use": "Ja hi ha un enlla\u00e7 o accessori configurat amb aquest nom o port."
},
"step": {
+ "accessory_mode": {
+ "data": {
+ "entity_id": "Entitat"
+ },
+ "description": "Escull l'entitat que vulguis incloure. En mode accessori, nom\u00e9s s'inclou una sola entitat.",
+ "title": "Selecciona l'entitat a incloure"
+ },
+ "bridge_mode": {
+ "data": {
+ "include_domains": "Dominis a incloure"
+ },
+ "description": "Escull els dominis que vulguis incloure. S'inclouran totes les entitats del domini que siguin compatibles.",
+ "title": "Selecciona els dominis a incloure"
+ },
"pairing": {
- "description": "Tan aviat com {name} estigui llest, la vinculaci\u00f3 estar\u00e0 disponible a \"Notificacions\" com a \"Configuraci\u00f3 de l'enlla\u00e7 HomeKit\".",
+ "description": "Per completar la vinculaci\u00f3, segueix les instruccions a \"Configuraci\u00f3 de l'enlla\u00e7 HomeKit\" sota \"Notificacions\".",
"title": "Vinculaci\u00f3 HomeKit"
},
"user": {
"data": {
"auto_start": "Autoarrencada (desactiva-ho si fas servir Z-Wave o algun altre sistema d'inici lent)",
- "include_domains": "Dominis a incloure"
+ "include_domains": "Dominis a incloure",
+ "mode": "Mode"
},
- "description": "La integraci\u00f3 HomeKit et permet l'acc\u00e9s a les teves entitats de Home Assistant a HomeKit. En mode enlla\u00e7, els enlla\u00e7os HomeKit estan limitats a un m\u00e0xim de 150 accessoris per inst\u00e0ncia (incl\u00f2s el propi enlla\u00e7). Si volguessis enlla\u00e7ar m\u00e9s accessoris que el m\u00e0xim perm\u00e8s, \u00e9s recomanable que utilitzis diferents enlla\u00e7os HomeKit per a dominis diferents. La configuraci\u00f3 avan\u00e7ada d'entitat nom\u00e9s est\u00e0 disponible en YAML per l'enlla\u00e7 prinipal.",
- "title": "Activaci\u00f3 de HomeKit"
+ "description": "Selecciona els dominis a incloure. S'inclouran totes les entitats del domini compatibles. Es crear\u00e0 una inst\u00e0ncia HomeKit en mode accessori per a cada repoductor multim\u00e8dia/TV i c\u00e0mera.",
+ "title": "Selecciona els dominis a incloure"
}
}
},
@@ -22,7 +37,7 @@
"step": {
"advanced": {
"data": {
- "auto_start": "[%key::component::homekit::config::step::user::data::auto_start%]",
+ "auto_start": "Inici autom\u00e0tic (desactiva-ho si crides el servei homekit.start manualment)",
"safe_mode": "Mode segur (habilita-ho nom\u00e9s si falla la vinculaci\u00f3)"
},
"description": "Aquests par\u00e0metres nom\u00e9s s'han d'ajustar si HomeKit no \u00e9s funcional.",
@@ -40,16 +55,16 @@
"entities": "Entitats",
"mode": "Mode"
},
- "description": "Tria les entitats que vulguis exposar. En mode accessori, nom\u00e9s s'exposa una sola entitat. En mode enlla\u00e7 inclusiu, s'exposaran totes les entitats del domini tret que se seleccionin algunes en concret. En mode enlla\u00e7 excusiu, s'exposaran totes les entitats del domini excepte les entitats excloses.",
- "title": "Selecci\u00f3 de les entitats a exposar"
+ "description": "Tria les entitats a incloure. En mode accessori, nom\u00e9s s'inclou una sola entitat. En mode enlla\u00e7 inclusiu, s'inclouran totes les entitats del domini tret de que se'n seleccionin algunes en concret. En mode enlla\u00e7 excusiu, s'inclouran totes les entitats del domini excepte les entitats excloses. Per obtenir el millor rendiment, es crea una inst\u00e0ncia HomeKit per a cada repoductor multim\u00e8dia/TV i c\u00e0mera.",
+ "title": "Selecciona les entitats a incloure"
},
"init": {
"data": {
- "include_domains": "[%key::component::homekit::config::step::user::data::include_domains%]",
+ "include_domains": "Dominis a incloure",
"mode": "Mode"
},
- "description": "HomeKit es pot configurar per exposar un enlla\u00e7 o un sol accessori. En mode accessori, nom\u00e9s es pot utilitzar una entitat. El mode accessori \u00e9s necessari en reproductors multim\u00e8dia amb classe de dispositiu TV perqu\u00e8 funcionin correctament. Les entitats a \"Dominis a incloure\" s'exposaran a HomeKit. A la seg\u00fcent pantalla podr\u00e0s seleccionar quines entitats vols incloure o excloure d'aquesta llista.",
- "title": "Selecci\u00f3 dels dominis a exposar."
+ "description": "HomeKit es pot configurar per exposar un enlla\u00e7 o un sol accessori. En mode accessori, nom\u00e9s es pot utilitzar una entitat. El mode accessori \u00e9s necessari perqu\u00e8 els reproductors multim\u00e8dia amb classe de dispositiu TV funcionin correctament. Les entitats a \"Dominis a incloure\" s'inclouran a HomeKit. A la seg\u00fcent pantalla podr\u00e0s seleccionar quines entitats vols incloure o excloure d'aquesta llista.",
+ "title": "Selecciona els dominis a incloure."
},
"yaml": {
"description": "Aquesta entrada es controla en YAML",
diff --git a/homeassistant/components/homekit/translations/cs.json b/homeassistant/components/homekit/translations/cs.json
index c070c852f0978f..cdfaed9183c1f4 100644
--- a/homeassistant/components/homekit/translations/cs.json
+++ b/homeassistant/components/homekit/translations/cs.json
@@ -4,14 +4,20 @@
"port_name_in_use": "P\u0159\u00edslu\u0161enstv\u00ed nebo p\u0159emost\u011bn\u00ed se stejn\u00fdm n\u00e1zvem nebo portem je ji\u017e nastaveno."
},
"step": {
+ "accessory_mode": {
+ "data": {
+ "entity_id": "Entita"
+ }
+ },
"pairing": {
"title": "P\u00e1rov\u00e1n\u00ed s HomeKit"
},
"user": {
"data": {
- "include_domains": "Dom\u00e9ny, kter\u00e9 maj\u00ed b\u00fdt zahrnuty"
+ "include_domains": "Dom\u00e9ny, kter\u00e9 maj\u00ed b\u00fdt zahrnuty",
+ "mode": "Re\u017eim"
},
- "title": "Aktivace HomeKit"
+ "title": "Vyberte dom\u00e9ny, kter\u00e9 chcete zahrnout"
}
}
},
diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json
index 9b1ec14a1dd20b..55df691b58b94c 100644
--- a/homeassistant/components/homekit/translations/de.json
+++ b/homeassistant/components/homekit/translations/de.json
@@ -1,16 +1,33 @@
{
"config": {
"abort": {
- "port_name_in_use": "A bridge with the same name or port is already configured.\nEine HomeKit Bridge mit dem selben Namen oder Port ist bereits vorhanden"
+ "port_name_in_use": "Eine HomeKit Bridge mit demselben Namen oder Port ist bereits vorhanden."
},
"step": {
+ "accessory_mode": {
+ "data": {
+ "entity_id": "Entit\u00e4t"
+ },
+ "description": "W\u00e4hle die Entit\u00e4t aus, die aufgenommen werden soll. Im Zubeh\u00f6rmodus ist nur eine einzelne Entit\u00e4t enthalten.",
+ "title": "W\u00e4hle die Entit\u00e4t aus, die aufgenommen werden soll"
+ },
+ "bridge_mode": {
+ "data": {
+ "include_domains": "Einzubeziehende Domains"
+ },
+ "description": "W\u00e4hle die Domains aus, die aufgenommen werden sollen. Alle unterst\u00fctzten Ger\u00e4te innerhalb der Domain werden aufgenommen.",
+ "title": "W\u00e4hle die Domains aus, die aufgenommen werden sollen"
+ },
"pairing": {
"title": "HomeKit verbinden"
},
"user": {
"data": {
- "include_domains": "Einzubeziehende Domains"
+ "auto_start": "Autostart (deaktivieren, wenn Z-Wave oder ein anderes verz\u00f6gertes Startsystem verwendet wird)",
+ "include_domains": "Einzubeziehende Domains",
+ "mode": "Modus"
},
+ "description": "W\u00e4hlen Sie die Domains aus, die aufgenommen werden sollen. Alle unterst\u00fctzten Entit\u00e4ten in der Domain werden aufgenommen. F\u00fcr jeden TV-Mediaplayer und jede Kamera wird eine separate HomeKit-Instanz im Zubeh\u00f6rmodus erstellt.",
"title": "HomeKit aktivieren"
}
}
@@ -19,32 +36,37 @@
"step": {
"advanced": {
"data": {
+ "auto_start": "Autostart (deaktivieren, wenn du den homekit.start-Dienst manuell aufrufst)",
"safe_mode": "Abgesicherter Modus (nur aktivieren, wenn das Pairing fehlschl\u00e4gt)"
},
+ "description": "Diese Einstellungen m\u00fcssen nur angepasst werden, wenn HomeKit nicht funktioniert.",
"title": "Erweiterte Konfiguration"
},
"cameras": {
"data": {
"camera_copy": "Kameras, die native H.264-Streams unterst\u00fctzen"
},
+ "description": "Pr\u00fcfe alle Kameras, die native H.264-Streams unterst\u00fctzen. Wenn die Kamera keinen H.264-Stream ausgibt, transkodiert das System das Video in H.264 f\u00fcr HomeKit. Die Transkodierung erfordert eine leistungsstarke CPU und wird wahrscheinlich nicht auf Einplatinencomputern funktionieren.",
"title": "W\u00e4hlen Sie den Kamera-Video-Codec."
},
"include_exclude": {
"data": {
"entities": "Entit\u00e4ten",
"mode": "Modus"
- }
+ },
+ "title": "W\u00e4hle die Entit\u00e4ten aus, die aufgenommen werden sollen"
},
"init": {
"data": {
+ "include_domains": "Einzubeziehende Domains",
"mode": "Modus"
},
"description": "HomeKit kann so konfiguriert werden, dass eine Br\u00fccke oder ein einzelnes Zubeh\u00f6r verf\u00fcgbar gemacht wird. Im Zubeh\u00f6rmodus kann nur eine einzelne Entit\u00e4t verwendet werden. F\u00fcr Media Player mit der TV-Ger\u00e4teklasse ist ein Zubeh\u00f6rmodus erforderlich, damit sie ordnungsgem\u00e4\u00df funktionieren. Entit\u00e4ten in den \"einzuschlie\u00dfenden Dom\u00e4nen\" werden f\u00fcr HomeKit verf\u00fcgbar gemacht. Auf dem n\u00e4chsten Bildschirm k\u00f6nnen Sie ausw\u00e4hlen, welche Entit\u00e4ten in diese Liste aufgenommen oder aus dieser ausgeschlossen werden sollen.",
- "title": "W\u00e4hlen Sie die zu \u00fcberbr\u00fcckenden Dom\u00e4nen aus."
+ "title": "W\u00e4hle die zu \u00fcberbr\u00fcckenden Dom\u00e4nen aus."
},
"yaml": {
"description": "Dieser Eintrag wird \u00fcber YAML gesteuert",
- "title": "Passen Sie die HomeKit Bridge-Optionen an"
+ "title": "Passe die HomeKit Bridge-Optionen an"
}
}
}
diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json
index cc6c8f8dc31225..a48b6fdee240eb 100644
--- a/homeassistant/components/homekit/translations/en.json
+++ b/homeassistant/components/homekit/translations/en.json
@@ -19,15 +19,17 @@
"title": "Select domains to be included"
},
"pairing": {
- "description": "As soon as the {name} is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d.",
+ "description": "To complete pairing following the instructions in \u201cNotifications\u201d under \u201cHomeKit Pairing\u201d.",
"title": "Pair HomeKit"
},
"user": {
"data": {
+ "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)",
+ "include_domains": "Domains to include",
"mode": "Mode"
},
- "description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each TV, media player and camera.",
- "title": "Activate HomeKit"
+ "description": "Choose the domains to be included. All supported entities in the domain will be included. A separate HomeKit instance in accessory mode will be created for each tv media player and camera.",
+ "title": "Select domains to be included"
}
}
},
@@ -35,7 +37,8 @@
"step": {
"advanced": {
"data": {
- "auto_start": "Autostart (disable if you are calling the homekit.start service manually)"
+ "auto_start": "Autostart (disable if you are calling the homekit.start service manually)",
+ "safe_mode": "Safe Mode (enable only if pairing fails)"
},
"description": "These settings only need to be adjusted if HomeKit is not functional.",
"title": "Advanced Configuration"
@@ -52,7 +55,7 @@
"entities": "Entities",
"mode": "Mode"
},
- "description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.",
+ "description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, a separate HomeKit accessory will be created for each tv media player, activity based remote, lock, and camera.",
"title": "Select entities to be included"
},
"init": {
@@ -69,4 +72,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json
index aeb75f838c1507..694b7dcdb6cf90 100644
--- a/homeassistant/components/homekit/translations/es.json
+++ b/homeassistant/components/homekit/translations/es.json
@@ -4,6 +4,20 @@
"port_name_in_use": "Ya est\u00e1 configurada una pasarela con el mismo nombre o puerto."
},
"step": {
+ "accessory_mode": {
+ "data": {
+ "entity_id": "Entidad"
+ },
+ "description": "Elija la entidad que desea incluir. En el modo accesorio, s\u00f3lo se incluye una \u00fanica entidad.",
+ "title": "Seleccione la entidad a incluir"
+ },
+ "bridge_mode": {
+ "data": {
+ "include_domains": "Dominios a incluir"
+ },
+ "description": "Elija los dominios que se van a incluir. Se incluir\u00e1n todas las entidades admitidas en el dominio.",
+ "title": "Selecciona los dominios a incluir"
+ },
"pairing": {
"description": "Tan pronto como la pasarela {name} est\u00e9 lista, la vinculaci\u00f3n estar\u00e1 disponible en \"Notificaciones\" como \"configuraci\u00f3n de pasarela Homekit\"",
"title": "Vincular pasarela Homekit"
@@ -11,7 +25,8 @@
"user": {
"data": {
"auto_start": "Arranque autom\u00e1tico (desactivado si se utiliza Z-Wave u otro sistema de arranque retardado)",
- "include_domains": "Dominios para incluir"
+ "include_domains": "Dominios para incluir",
+ "mode": "Modo"
},
"description": "Una pasarela Homekit permitir\u00e1 a Homekit acceder a sus entidades de Home Assistant. La pasarela Homekit est\u00e1 limitada a 150 accesorios por instancia incluyendo la propia pasarela. Si desea enlazar m\u00e1s del m\u00e1ximo n\u00famero de accesorios, se recomienda que use multiples pasarelas Homekit para diferentes dominios. Configuraci\u00f3n detallada de la entidad solo est\u00e1 disponible via YAML para la pasarela primaria.",
"title": "Activar pasarela Homekit"
diff --git a/homeassistant/components/homekit/translations/et.json b/homeassistant/components/homekit/translations/et.json
index 76a78602bd3e87..38d063d9bf3904 100644
--- a/homeassistant/components/homekit/translations/et.json
+++ b/homeassistant/components/homekit/translations/et.json
@@ -4,17 +4,32 @@
"port_name_in_use": "Sama nime v\u00f5i pordiga tarvik v\u00f5i sild on juba konfigureeritud."
},
"step": {
+ "accessory_mode": {
+ "data": {
+ "entity_id": "Olem"
+ },
+ "description": "Vali kaasatav olem. Lisare\u017eiimis on kaasatav ainult \u00fcks olem.",
+ "title": "Vali kaasatav olem"
+ },
+ "bridge_mode": {
+ "data": {
+ "include_domains": "Kaasatavad domeenid"
+ },
+ "description": "Vali kaasatavad domeenid. Kaasatakse k\u00f5ik domeenis toetatud olemid.",
+ "title": "Vali kaasatavad domeenid"
+ },
"pairing": {
- "description": "Niipea kui {name} on valmis, on sidumine saadaval jaotises \"Notifications\" kui \"HomeKit Bridge Setup\".",
+ "description": "Sidumise l\u00f5puleviimiseks j\u00e4rgi jaotises \"HomeKiti sidumine\" toodud juhiseid alajaotises \"Teatised\".",
"title": "HomeKiti sidumine"
},
"user": {
"data": {
"auto_start": "Autostart (keela, kui kasutad Z-Wave'i v\u00f5i muud viivitatud k\u00e4ivituss\u00fcsteemi)",
- "include_domains": "Kaasatavad domeenid"
+ "include_domains": "Kaasatavad domeenid",
+ "mode": "Re\u017eiim"
},
- "description": "HomeKiti integreerimine v\u00f5imaldab teil p\u00e4\u00e4seda juurde HomeKiti \u00fcksustele Home Assistant. Sildire\u017eiimis on HomeKit Bridges piiratud 150 lisaseadmega, sealhulgas sild ise. Kui soovid \u00fchendada rohkem lisatarvikuid, on soovitatav kasutada erinevate domeenide jaoks mitut HomeKiti silda. \u00dcksuse \u00fcksikasjalik konfiguratsioon on esmase silla jaoks saadaval ainult YAML-i kaudu.",
- "title": "Aktiveeri HomeKit"
+ "description": "Vali kaasatavad domeenid. Kaasatakse k\u00f5ik domeenis toetatud olemid. Iga telemeedia pleieri ja kaamera jaoks luuakse eraldi HomeKiti eksemplar tarvikure\u017eiimis.",
+ "title": "Vali kaasatavad domeenid"
}
}
},
@@ -22,7 +37,7 @@
"step": {
"advanced": {
"data": {
- "auto_start": "Autostart (keela, kui kasutad Z-Wave'i v\u00f5i muud viivitatud k\u00e4ivituss\u00fcsteemi)",
+ "auto_start": "Autostart (keela kui kasutad homekit.start teenust k\u00e4sitsi)",
"safe_mode": "Turvare\u017eiim (luba ainult siis, kui sidumine nurjub)"
},
"description": "Neid s\u00e4tteid tuleb muuta ainult siis kui HomeKit ei t\u00f6\u00f6ta.",
@@ -40,16 +55,16 @@
"entities": "Olemid",
"mode": "Re\u017eiim"
},
- "description": "Vali avaldatavad olemid. Tarvikute re\u017eiimis on avaldatav ainult \u00fcks olem. Silla re\u017eiimis, kuvatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud juhul, kui valitud on kindlad olemid. Silla v\u00e4listamisre\u017eiimis avaldatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud v\u00e4listatud olemid.",
- "title": "Vali avaldatavad olemid"
+ "description": "Vali kaasatavad olemid. Tarvikute re\u017eiimis on kaasatav ainult \u00fcks olem. Silla re\u017eiimis kaasatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud juhul, kui valitud on kindlad olemid. Silla v\u00e4listamisre\u017eiimis kaasatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud v\u00e4listatud olemid. Parima kasutuskogemuse jaoks on eraldi HomeKit seadmed iga TV meediumim\u00e4ngija, luku, juhtpuldi ja kaamera jaoks.",
+ "title": "Vali kaasatavd olemid"
},
"init": {
"data": {
- "include_domains": "Kaasatavad domeenid",
+ "include_domains": "Kaasatud domeenid",
"mode": "Re\u017eiim"
},
"description": "HomeKiti saab seadistada silla v\u00f5i \u00fche lisaseadme avaldamiseks. Lisare\u017eiimis saab kasutada ainult \u00fchte \u00fcksust. Teleriseadmete klassiga meediumipleierite n\u00f5uetekohaseks toimimiseks on vaja lisare\u017eiimi. \u201eKaasatavate domeenide\u201d \u00fcksused puutuvad kokku HomeKitiga. J\u00e4rgmisel ekraanil saad valida, millised \u00fcksused sellesse loendisse lisada v\u00f5i sellest v\u00e4lja j\u00e4tta.",
- "title": "Valige avaldatavad domeenid."
+ "title": "Vali kaasatavad domeenid"
},
"yaml": {
"description": "Seda sisestust juhitakse YAML-i kaudu",
diff --git a/homeassistant/components/homekit/translations/fr.json b/homeassistant/components/homekit/translations/fr.json
index be7d30c30ee59a..dae09002c54ae4 100644
--- a/homeassistant/components/homekit/translations/fr.json
+++ b/homeassistant/components/homekit/translations/fr.json
@@ -4,17 +4,32 @@
"port_name_in_use": "Une passerelle avec le m\u00eame nom ou port est d\u00e9j\u00e0 configur\u00e9e."
},
"step": {
+ "accessory_mode": {
+ "data": {
+ "entity_id": "Entit\u00e9"
+ },
+ "description": "Choisissez l'entit\u00e9 \u00e0 inclure. En mode accessoire, une seule entit\u00e9 est incluse.",
+ "title": "S\u00e9lectionnez l'entit\u00e9 \u00e0 inclure"
+ },
+ "bridge_mode": {
+ "data": {
+ "include_domains": "Domaines \u00e0 inclure"
+ },
+ "description": "Choisissez les domaines \u00e0 inclure. Toutes les entit\u00e9s prises en charge dans le domaine seront incluses.",
+ "title": "S\u00e9lectionnez les domaines \u00e0 inclure"
+ },
"pairing": {
- "description": "D\u00e8s que le pont {name} est pr\u00eat, l'appairage sera disponible dans \"Notifications\" sous \"Configuration de la Passerelle HomeKit\".",
+ "description": "Pour compl\u00e9ter l'appariement, suivez les instructions dans les \"Notifications\" sous \"Appariement HomeKit\".",
"title": "Appairage de la Passerelle Homekit"
},
"user": {
"data": {
"auto_start": "D\u00e9marrage automatique (d\u00e9sactiver si vous utilisez Z-Wave ou un autre syst\u00e8me de d\u00e9marrage diff\u00e9r\u00e9)",
- "include_domains": "Domaines \u00e0 inclure"
+ "include_domains": "Domaines \u00e0 inclure",
+ "mode": "Mode"
},
- "description": "La passerelle HomeKit vous permettra d'acc\u00e9der \u00e0 vos entit\u00e9s Home Assistant dans HomeKit. Les passerelles HomeKit sont limit\u00e9es \u00e0 150 accessoires par instance, y compris la passerelle elle-m\u00eame. Si vous souhaitez connecter plus que le nombre maximum d'accessoires, il est recommand\u00e9 d'utiliser plusieurs passerelles HomeKit pour diff\u00e9rents domaines. La configuration d\u00e9taill\u00e9e des entit\u00e9s est uniquement disponible via YAML pour la passerelle principale.",
- "title": "Activer la Passerelle HomeKit"
+ "description": "Choisissez les domaines \u00e0 inclure. Toutes les entit\u00e9s prises en charge dans le domaine seront incluses. Une instance HomeKit distincte en mode accessoire sera cr\u00e9\u00e9e pour chaque lecteur multim\u00e9dia TV et cam\u00e9ra.",
+ "title": "S\u00e9lectionnez les domaines \u00e0 inclure"
}
}
},
@@ -40,12 +55,12 @@
"entities": "Entit\u00e9s",
"mode": "Mode"
},
- "description": "Choisissez les entit\u00e9s \u00e0 exposer. En mode accessoire, une seule entit\u00e9 est expos\u00e9e. En mode d'inclusion de pont, toutes les entit\u00e9s du domaine seront expos\u00e9es \u00e0 moins que des entit\u00e9s sp\u00e9cifiques ne soient s\u00e9lectionn\u00e9es. En mode d'exclusion de pont, toutes les entit\u00e9s du domaine seront expos\u00e9es \u00e0 l'exception des entit\u00e9s exclues.",
+ "description": "Choisissez les entit\u00e9s \u00e0 inclure. En mode accessoire, une seule entit\u00e9 est incluse. En mode d'inclusion de pont, toutes les entit\u00e9s du domaine seront incluses \u00e0 moins que des entit\u00e9s sp\u00e9cifiques ne soient s\u00e9lectionn\u00e9es. En mode d'exclusion de pont, toutes les entit\u00e9s du domaine seront incluses \u00e0 l'exception des entit\u00e9s exclues. Pour de meilleures performances, un accessoire HomeKit distinct sera cr\u00e9\u00e9 pour chaque lecteur multim\u00e9dia TV et cam\u00e9ra.",
"title": "S\u00e9lectionnez les entit\u00e9s \u00e0 exposer"
},
"init": {
"data": {
- "include_domains": "Domaine \u00e0 inclure",
+ "include_domains": "Domaines \u00e0 inclure",
"mode": "Mode"
},
"description": "Les entit\u00e9s des \u00abdomaines \u00e0 inclure\u00bb seront pont\u00e9es vers HomeKit. Vous pourrez s\u00e9lectionner les entit\u00e9s \u00e0 exclure de cette liste sur l'\u00e9cran suivant.",
diff --git a/homeassistant/components/homekit/translations/he.json b/homeassistant/components/homekit/translations/he.json
index 87ad743dca5be9..cb5a530b739d2b 100644
--- a/homeassistant/components/homekit/translations/he.json
+++ b/homeassistant/components/homekit/translations/he.json
@@ -1,6 +1,17 @@
{
"options": {
"step": {
+ "include_exclude": {
+ "data": {
+ "mode": "\u05de\u05e6\u05d1"
+ },
+ "title": "\u05d1\u05d7\u05e8 \u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05e9\u05d9\u05d9\u05db\u05dc\u05dc\u05d5"
+ },
+ "init": {
+ "data": {
+ "mode": "\u05de\u05e6\u05d1"
+ }
+ },
"yaml": {
"description": "\u05d9\u05e9\u05d5\u05ea \u05d6\u05d5 \u05e0\u05e9\u05dc\u05d8\u05ea \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea YAML"
}
diff --git a/homeassistant/components/homekit/translations/hu.json b/homeassistant/components/homekit/translations/hu.json
index 4cf1f44c439064..7cc2577cb313ca 100644
--- a/homeassistant/components/homekit/translations/hu.json
+++ b/homeassistant/components/homekit/translations/hu.json
@@ -1,16 +1,52 @@
{
+ "config": {
+ "step": {
+ "accessory_mode": {
+ "data": {
+ "entity_id": "Entit\u00e1s"
+ },
+ "title": "V\u00e1laszd ki a felvenni k\u00edv\u00e1nt entit\u00e1st"
+ },
+ "pairing": {
+ "title": "HomeKit p\u00e1ros\u00edt\u00e1s"
+ },
+ "user": {
+ "data": {
+ "include_domains": "Felvenni k\u00edv\u00e1nt domainek",
+ "mode": "M\u00f3d"
+ },
+ "title": "Felvenni k\u00edv\u00e1nt domainek kiv\u00e1laszt\u00e1sa"
+ }
+ }
+ },
"options": {
"step": {
+ "advanced": {
+ "title": "Halad\u00f3 be\u00e1ll\u00edt\u00e1sok"
+ },
+ "cameras": {
+ "data": {
+ "camera_copy": "A nat\u00edv H.264 streameket t\u00e1mogat\u00f3 kamer\u00e1k"
+ },
+ "title": "V\u00e1laszd ki a kamera vide\u00f3 kodekj\u00e9t."
+ },
"include_exclude": {
"data": {
"entities": "Entit\u00e1sok",
"mode": "M\u00f3d"
- }
+ },
+ "title": "V\u00e1laszd ki a felvenni k\u00edv\u00e1nt entit\u00e1sokat"
},
"init": {
"data": {
+ "include_domains": "Felvenni k\u00edv\u00e1nt domainek",
"mode": "M\u00f3d"
- }
+ },
+ "title": "V\u00e1laszd ki a felvenni k\u00edv\u00e1nt domaineket."
+ },
+ "yaml": {
+ "description": "Ez a bejegyz\u00e9s YAML-en kereszt\u00fcl vez\u00e9relhet\u0151",
+ "title": "HomeKit be\u00e1ll\u00edt\u00e1sok m\u00f3dos\u00edt\u00e1sa"
}
}
}
diff --git a/homeassistant/components/homekit/translations/id.json b/homeassistant/components/homekit/translations/id.json
new file mode 100644
index 00000000000000..588631a5215ce5
--- /dev/null
+++ b/homeassistant/components/homekit/translations/id.json
@@ -0,0 +1,75 @@
+{
+ "config": {
+ "abort": {
+ "port_name_in_use": "Aksesori atau bridge dengan nama atau port yang sama telah dikonfigurasi."
+ },
+ "step": {
+ "accessory_mode": {
+ "data": {
+ "entity_id": "Entitas"
+ },
+ "description": "Pilih entitas yang akan disertakan. Dalam mode aksesori, hanya satu entitas yang disertakan.",
+ "title": "Pilih entitas yang akan disertakan"
+ },
+ "bridge_mode": {
+ "data": {
+ "include_domains": "Domain yang disertakan"
+ },
+ "description": "Pilih domain yang akan disertakan. Semua entitas yang didukung di domain akan disertakan.",
+ "title": "Pilih domain yang akan disertakan"
+ },
+ "pairing": {
+ "description": "Untuk menyelesaikan pemasangan ikuti petunjuk di \"Notifikasi\" di bawah \"Pemasangan HomeKit\".",
+ "title": "Pasangkan HomeKit"
+ },
+ "user": {
+ "data": {
+ "auto_start": "Mulai otomatis (nonaktifkan jika menggunakan Z-Wave atau sistem mulai tertunda lainnya)",
+ "include_domains": "Domain yang disertakan",
+ "mode": "Mode"
+ },
+ "description": "Pilih domain yang akan disertakan. Semua entitas yang didukung di domain akan disertakan. Instans HomeKit terpisah dalam mode aksesori akan dibuat untuk setiap pemutar media TV dan kamera.",
+ "title": "Pilih domain yang akan disertakan"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "advanced": {
+ "data": {
+ "auto_start": "Mulai otomatis (nonaktifkan jika Anda memanggil layanan homekit.start secara manual)",
+ "safe_mode": "Mode Aman (aktifkan hanya jika pemasangan gagal)"
+ },
+ "description": "Pengaturan ini hanya perlu disesuaikan jika HomeKit tidak berfungsi.",
+ "title": "Konfigurasi Tingkat Lanjut"
+ },
+ "cameras": {
+ "data": {
+ "camera_copy": "Kamera yang mendukung aliran H.264 asli"
+ },
+ "description": "Periksa semua kamera yang mendukung streaming H.264 asli. Jika kamera tidak mengeluarkan aliran H.264, sistem akan mentranskode video ke H.264 untuk HomeKit. Proses transcoding membutuhkan CPU kinerja tinggi dan tidak mungkin bekerja pada komputer papan tunggal.",
+ "title": "Pilih codec video kamera."
+ },
+ "include_exclude": {
+ "data": {
+ "entities": "Entitas",
+ "mode": "Mode"
+ },
+ "description": "Pilih entitas yang akan disertakan. Dalam mode aksesori, hanya satu entitas yang disertakan. Dalam mode \"bridge include\", semua entitas di domain akan disertakan, kecuali entitas tertentu dipilih. Dalam mode \"bridge exclude\", semua entitas di domain akan disertakan, kecuali untuk entitas tertentu yang dipilih. Untuk kinerja terbaik, aksesori HomeKit terpisah diperlukan untuk masing-masing pemutar media, TV, dan kamera.",
+ "title": "Pilih entitas untuk disertakan"
+ },
+ "init": {
+ "data": {
+ "include_domains": "Domain yang disertakan",
+ "mode": "Mode"
+ },
+ "description": "HomeKit dapat dikonfigurasi untuk memaparkakan sebuah bridge atau sebuah aksesori. Dalam mode aksesori, hanya satu entitas yang dapat digunakan. Mode aksesori diperlukan agar pemutar media dengan kelas perangkat TV berfungsi dengan baik. Entitas di \"Domain yang akan disertakan\" akan disertakan ke HomeKit. Anda akan dapat memilih entitas mana yang akan disertakan atau dikecualikan dari daftar ini pada layar berikutnya.",
+ "title": "Pilih domain yang akan disertakan."
+ },
+ "yaml": {
+ "description": "Entri ini dikontrol melalui YAML",
+ "title": "Sesuaikan Opsi HomeKit"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json
index 7e9c8a05b9da55..c61aececec7e98 100644
--- a/homeassistant/components/homekit/translations/it.json
+++ b/homeassistant/components/homekit/translations/it.json
@@ -4,17 +4,32 @@
"port_name_in_use": "Un accessorio o un bridge con lo stesso nome o porta \u00e8 gi\u00e0 configurato."
},
"step": {
+ "accessory_mode": {
+ "data": {
+ "entity_id": "Entit\u00e0"
+ },
+ "description": "Scegli l'entit\u00e0 da includere. In modalit\u00e0 accessorio, \u00e8 inclusa solo una singola entit\u00e0.",
+ "title": "Seleziona l'entit\u00e0 da includere"
+ },
+ "bridge_mode": {
+ "data": {
+ "include_domains": "Domini da includere"
+ },
+ "description": "Scegli i domini da includere. Verranno incluse tutte le entit\u00e0 supportate nel dominio.",
+ "title": "Seleziona i domini da includere"
+ },
"pairing": {
- "description": "Non appena il {name} \u00e8 pronto, l'associazione sar\u00e0 disponibile in \"Notifiche\" come \"Configurazione HomeKit Bridge\".",
+ "description": "Per completare l'associazione, seguire le istruzioni in \"Notifiche\" sotto \"Associazione HomeKit\".",
"title": "Associa HomeKit"
},
"user": {
"data": {
"auto_start": "Avvio automatico (disabilitare se si utilizza Z-Wave o un altro sistema di avvio ritardato)",
- "include_domains": "Domini da includere"
+ "include_domains": "Domini da includere",
+ "mode": "Modalit\u00e0"
},
- "description": "L'integrazione di HomeKit ti consentir\u00e0 di accedere alle entit\u00e0 di Home Assistant in HomeKit. In modalit\u00e0 bridge, i bridge HomeKit sono limitati a 150 accessori per istanza, incluso il bridge stesso. Se desideri eseguire il bridge di un numero di accessori superiore a quello massimo, si consiglia di utilizzare pi\u00f9 bridge HomeKit per domini diversi. La configurazione dettagliata dell'entit\u00e0 \u00e8 disponibile solo tramite YAML per il bridge principale.",
- "title": "Attiva HomeKit"
+ "description": "Scegli i domini da includere. Verranno incluse tutte le entit\u00e0 supportate nel dominio. Verr\u00e0 creata un'istanza HomeKit separata in modalit\u00e0 accessorio per ogni lettore multimediale TV e telecamera.",
+ "title": "Seleziona i domini da includere"
}
}
},
@@ -22,7 +37,7 @@
"step": {
"advanced": {
"data": {
- "auto_start": "Avvio automatico (disabilitare se si utilizza Z-Wave o un altro sistema di avvio ritardato)",
+ "auto_start": "Avvio automatico (disabilitare se stai chiamando manualmente il servizio homekit.start)",
"safe_mode": "Modalit\u00e0 provvisoria (attivare solo in caso di errore di associazione)"
},
"description": "Queste impostazioni devono essere regolate solo se HomeKit non funziona.",
@@ -40,8 +55,8 @@
"entities": "Entit\u00e0",
"mode": "Modalit\u00e0"
},
- "description": "Scegliere le entit\u00e0 da esporre. In modalit\u00e0 accessorio, \u00e8 esposta una sola entit\u00e0. In modalit\u00e0 di inclusione bridge, tutte le entit\u00e0 nel dominio saranno esposte, a meno che non siano selezionate entit\u00e0 specifiche. In modalit\u00e0 di esclusione bridge, tutte le entit\u00e0 nel dominio saranno esposte, ad eccezione delle entit\u00e0 escluse.",
- "title": "Selezionare le entit\u00e0 da esporre"
+ "description": "Scegliere le entit\u00e0 da includere. In modalit\u00e0 accessorio, \u00e8 inclusa una sola entit\u00e0. In modalit\u00e0 di inclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, a meno che non siano selezionate entit\u00e0 specifiche. In modalit\u00e0 di esclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, ad eccezione delle entit\u00e0 escluse. Per prestazioni ottimali, sar\u00e0 creata una HomeKit separata accessoria per ogni lettore multimediale TV, telecomando basato sulle attivit\u00e0, serratura e videocamera.",
+ "title": "Seleziona le entit\u00e0 da includere"
},
"init": {
"data": {
@@ -49,7 +64,7 @@
"mode": "Modalit\u00e0"
},
"description": "HomeKit pu\u00f2 essere configurato esponendo un bridge o un singolo accessorio. In modalit\u00e0 accessorio, pu\u00f2 essere utilizzata solo una singola entit\u00e0. La modalit\u00e0 accessorio \u00e8 necessaria per il corretto funzionamento dei lettori multimediali con la classe di apparecchi TV. Le entit\u00e0 nei \"Domini da includere\" saranno esposte ad HomeKit. Sar\u00e0 possibile selezionare quali entit\u00e0 includere o escludere da questo elenco nella schermata successiva.",
- "title": "Selezionare i domini da esporre."
+ "title": "Seleziona i domini da includere."
},
"yaml": {
"description": "Questa voce \u00e8 controllata tramite YAML",
diff --git a/homeassistant/components/homekit/translations/ko.json b/homeassistant/components/homekit/translations/ko.json
index 1b7276d41712f5..b9b04aec0a7fd0 100644
--- a/homeassistant/components/homekit/translations/ko.json
+++ b/homeassistant/components/homekit/translations/ko.json
@@ -1,20 +1,35 @@
{
"config": {
"abort": {
- "port_name_in_use": "\uc774\ub984\uc774\ub098 \ud3ec\ud2b8\uac00 \uac19\uc740 \ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "port_name_in_use": "\uc774\ub984\uc774\ub098 \ud3ec\ud2b8\uac00 \uac19\uc740 \ube0c\ub9ac\uc9c0 \ub610\ub294 \uc561\uc138\uc11c\ub9ac\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
},
"step": {
+ "accessory_mode": {
+ "data": {
+ "entity_id": "\uad6c\uc131\uc694\uc18c"
+ },
+ "description": "\ud3ec\ud568\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ub2e8\uc77c \uad6c\uc131\uc694\uc18c\ub9cc \ud3ec\ud568\ub429\ub2c8\ub2e4.",
+ "title": "\ud3ec\ud568\ud560 \uad6c\uc131\uc694\uc18c \uc120\ud0dd\ud558\uae30"
+ },
+ "bridge_mode": {
+ "data": {
+ "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778"
+ },
+ "description": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc9c0\uc6d0\ub418\ub294 \ub3c4\uba54\uc778\uc758 \ubaa8\ub4e0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4.",
+ "title": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778 \uc120\ud0dd\ud558\uae30"
+ },
"pairing": {
- "description": "{name} \ube0c\ub9ac\uc9c0\uac00 \uc900\ube44\ub418\uba74 \"\uc54c\ub9bc\"\uc5d0\uc11c \"HomeKit \ube0c\ub9ac\uc9c0 \uc124\uc815\"\uc73c\ub85c \ud398\uc5b4\ub9c1\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
- "title": "HomeKit \ube0c\ub9ac\uc9c0 \ud398\uc5b4\ub9c1\ud558\uae30"
+ "description": "\"\uc54c\ub9bc\"\uc5d0\uc11c \"HomeKit Pairing\"\uc5d0 \uc788\ub294 \uc548\ub0b4\uc5d0 \ub530\ub77c \ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud574\uc8fc\uc138\uc694.",
+ "title": "HomeKit \ud398\uc5b4\ub9c1\ud558\uae30"
},
"user": {
"data": {
"auto_start": "\uc790\ub3d9 \uc2dc\uc791 (Z-Wave \ub610\ub294 \uae30\ud0c0 \uc9c0\uc5f0\ub41c \uc2dc\uc791 \uc2dc\uc2a4\ud15c\uc744 \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0 \ube44\ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)",
- "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778"
+ "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778",
+ "mode": "\ubaa8\ub4dc"
},
- "description": "HomeKit \ube0c\ub9ac\uc9c0\ub97c \uc0ac\uc6a9\ud558\uba74 HomeKit \uc5d0\uc11c Home Assistant \uad6c\uc131\uc694\uc18c\uc5d0 \uc561\uc138\uc2a4\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. HomeKit \ube0c\ub9ac\uc9c0\ub294 \ube0c\ub9ac\uc9c0 \uc790\uccb4\ub97c \ud3ec\ud568\ud558\uc5ec \uc778\uc2a4\ud134\uc2a4\ub2f9 150\uac1c\uc758 \uc561\uc138\uc11c\ub9ac\ub85c \uc81c\ud55c\ub429\ub2c8\ub2e4. \ucd5c\ub300 \uc561\uc138\uc11c\ub9ac \uc218\ub97c \ucd08\uacfc\ud558\uc5ec \ube0c\ub9ac\uc9d5\ud558\ub824\uba74 \uc5ec\ub7ec \ub3c4\uba54\uc778\uc5d0 \ub300\ud574 \uc5ec\ub7ec \uac1c\uc758 \ud648\ud0b7 \ube0c\ub9ac\uc9c0\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4. \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 \uae30\ubcf8 \ube0c\ub9ac\uc9c0\uc758 YAML \uc744 \ud1b5\ud574\uc11c\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
- "title": "HomeKit \ube0c\ub9ac\uc9c0 \ud65c\uc131\ud654\ud558\uae30"
+ "description": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ub3c4\uba54\uc778\uc5d0\uc11c \uc9c0\uc6d0\ub418\ub294 \ubaa8\ub4e0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4. \uac01 TV \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\uc640 \uce74\uba54\ub77c\uc5d0 \ub300\ud574 \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc758 \uac1c\ubcc4 HomeKit \uc778\uc2a4\ud134\uc2a4\uac00 \uc0dd\uc131\ub429\ub2c8\ub2e4.",
+ "title": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778 \uc120\ud0dd\ud558\uae30"
}
}
},
@@ -22,29 +37,38 @@
"step": {
"advanced": {
"data": {
- "auto_start": "\uc790\ub3d9 \uc2dc\uc791 (Z-Wave \ub610\ub294 \uae30\ud0c0 \uc9c0\uc5f0\ub41c \uc2dc\uc791 \uc2dc\uc2a4\ud15c\uc744 \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0 \ube44\ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)",
+ "auto_start": "\uc790\ub3d9 \uc2dc\uc791 (homekit.start \uc11c\ube44\uc2a4\ub97c \uc218\ub3d9\uc73c\ub85c \ud638\ucd9c\ud558\ub824\uba74 \ube44\ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)",
"safe_mode": "\uc548\uc804 \ubaa8\ub4dc (\ud398\uc5b4\ub9c1\uc774 \uc2e4\ud328\ud55c \uacbd\uc6b0\uc5d0\ub9cc \ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)"
},
- "description": "\uc774 \uc124\uc815\uc740 HomeKit \ube0c\ub9ac\uc9c0\uac00 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0\uc5d0\ub9cc \uc124\uc815\ud574\uc8fc\uc138\uc694.",
- "title": "\uace0\uae09 \uad6c\uc131\ud558\uae30"
+ "description": "\uc774 \uc124\uc815\uc740 HomeKit\uac00 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0\uc5d0\ub9cc \uc124\uc815\ud574\uc8fc\uc138\uc694.",
+ "title": "\uace0\uae09 \uad6c\uc131"
},
"cameras": {
"data": {
"camera_copy": "\ub124\uc774\ud2f0\ube0c H.264 \uc2a4\ud2b8\ub9bc\uc744 \uc9c0\uc6d0\ud558\ub294 \uce74\uba54\ub77c"
},
- "description": "\ub124\uc774\ud2f0\ube0c H.264 \uc2a4\ud2b8\ub9bc\uc744 \uc9c0\uc6d0\ud558\ub294 \ubaa8\ub4e0 \uce74\uba54\ub77c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694. \uce74\uba54\ub77c\uac00 H.264 \uc2a4\ud2b8\ub9bc\uc744 \ucd9c\ub825\ud558\uc9c0 \uc54a\uc73c\uba74 \uc2dc\uc2a4\ud15c\uc740 \ube44\ub514\uc624\ub97c HomeKit \uc6a9 H.264 \ud3ec\ub9f7\uc73c\ub85c \ubcc0\ud658\uc2dc\ud0b5\ub2c8\ub2e4. \ud2b8\ub79c\uc2a4\ucf54\ub529 \ubcc0\ud658\uc5d0\ub294 \ub192\uc740 CPU \uc131\ub2a5\uc774 \ud544\uc694\ud558\uba70 Raspberry Pi \uc640 \uac19\uc740 \ub2e8\uc77c \ubcf4\ub4dc \ucef4\ud4e8\ud130\uc5d0\uc11c\ub294 \uc791\ub3d9\ud558\uc9c0 \uc54a\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "description": "\ub124\uc774\ud2f0\ube0c H.264 \uc2a4\ud2b8\ub9bc\uc744 \uc9c0\uc6d0\ud558\ub294 \ubaa8\ub4e0 \uce74\uba54\ub77c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694. \uce74\uba54\ub77c\uac00 H.264 \uc2a4\ud2b8\ub9bc\uc744 \ucd9c\ub825\ud558\uc9c0 \uc54a\uc73c\uba74 \uc2dc\uc2a4\ud15c\uc740 \ube44\ub514\uc624\ub97c HomeKit\uc6a9 H.264 \ud3ec\ub9f7\uc73c\ub85c \ubcc0\ud658\uc2dc\ud0b5\ub2c8\ub2e4. \ud2b8\ub79c\uc2a4\ucf54\ub529 \ubcc0\ud658\uc5d0\ub294 \ub192\uc740 CPU \uc131\ub2a5\uc774 \ud544\uc694\ud558\uba70 Raspberry Pi\uc640 \uac19\uc740 \ub2e8\uc77c \ubcf4\ub4dc \ucef4\ud4e8\ud130\uc5d0\uc11c\ub294 \uc791\ub3d9\ud558\uc9c0 \uc54a\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
"title": "\uce74\uba54\ub77c \ube44\ub514\uc624 \ucf54\ub371 \uc120\ud0dd\ud558\uae30"
},
+ "include_exclude": {
+ "data": {
+ "entities": "\uad6c\uc131\uc694\uc18c",
+ "mode": "\ubaa8\ub4dc"
+ },
+ "description": "\ud3ec\ud568\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ub2e8\uc77c \uad6c\uc131\uc694\uc18c\ub9cc \ud3ec\ud568\ub429\ub2c8\ub2e4. \ube0c\ub9ac\uc9c0 \ud3ec\ud568 \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ud2b9\uc815 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud558\uc9c0 \uc54a\ub294 \ud55c \ub3c4\uba54\uc778\uc758 \ubaa8\ub4e0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4. \ube0c\ub9ac\uc9c0 \uc81c\uc678 \ubaa8\ub4dc\uc5d0\uc11c\ub294 \uc81c\uc678\ub41c \uad6c\uc131\uc694\uc18c\ub97c \uc81c\uc678\ud55c \ub3c4\uba54\uc778\uc758 \ub098\uba38\uc9c0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4. \ucd5c\uc0c1\uc758 \uc131\ub2a5\uc744 \uc704\ud574 \uac01 TV \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\uc640 \ud65c\ub3d9 \uae30\ubc18 \ub9ac\ubaa8\ucf58, \uc7a0\uae08\uae30\uae30, \uce74\uba54\ub77c\ub294 \ubcc4\ub3c4\uc758 HomeKit \uc561\uc138\uc11c\ub9ac\ub85c \uc0dd\uc131\ub429\ub2c8\ub2e4.",
+ "title": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778 \uc120\ud0dd\ud558\uae30"
+ },
"init": {
"data": {
- "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778"
+ "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778",
+ "mode": "\ubaa8\ub4dc"
},
- "description": "\"\ud3ec\ud568\ud560 \ub3c4\uba54\uc778\"\uc758 \uad6c\uc131\uc694\uc18c\ub294 HomeKit \uc5d0 \uc5f0\uacb0\ub429\ub2c8\ub2e4. \ub2e4\uc74c \ud654\uba74\uc5d0\uc11c \uc774 \ubaa9\ub85d\uc758 \uc81c\uc678\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
- "title": "\ube0c\ub9ac\uc9c0 \ud560 \ub3c4\uba54\uc778 \uc120\ud0dd\ud558\uae30"
+ "description": "\ube0c\ub9ac\uc9c0 \ub610\ub294 \ub2e8\uc77c \uc561\uc138\uc11c\ub9ac\ub97c \ub178\ucd9c\ud558\uc5ec HomeKit\ub97c \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ub2e8\uc77c \uad6c\uc131\uc694\uc18c\ub9cc \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. TV \uae30\uae30 \ud074\ub798\uc2a4\uac00 \uc788\ub294 \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\uac00 \uc81c\ub300\ub85c \uc791\ub3d9\ud558\ub824\uba74 \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. \"\ud3ec\ud568\ud560 \ub3c4\uba54\uc778\"\uc758 \uad6c\uc131\uc694\uc18c\uac00 HomeKit\uc5d0 \ud3ec\ud568\ub418\uba70, \ub2e4\uc74c \ud654\uba74\uc5d0\uc11c \uc774 \ubaa9\ub85d\uc5d0 \ud3ec\ud568\ud558\uac70\ub098 \uc81c\uc678\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "title": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694."
},
"yaml": {
- "description": "\uc774 \ud56d\ubaa9\uc740 YAML \uc744 \ud1b5\ud574 \uc81c\uc5b4\ub429\ub2c8\ub2e4",
- "title": "HomeKit \ube0c\ub9ac\uc9c0 \uc635\uc158 \uc870\uc815\ud558\uae30"
+ "description": "\uc774 \ud56d\ubaa9\uc740 YAML\uc744 \ud1b5\ud574 \uc81c\uc5b4\ub429\ub2c8\ub2e4",
+ "title": "HomeKit \uc635\uc158 \uc870\uc815\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json
index 2733d6bd12d367..154f271e1a33b1 100644
--- a/homeassistant/components/homekit/translations/nl.json
+++ b/homeassistant/components/homekit/translations/nl.json
@@ -4,17 +4,32 @@
"port_name_in_use": "Er is al een bridge of apparaat met dezelfde naam of poort geconfigureerd."
},
"step": {
+ "accessory_mode": {
+ "data": {
+ "entity_id": "Entiteit"
+ },
+ "description": "Kies de entiteit die moet worden opgenomen. In de accessoiremodus wordt slechts \u00e9\u00e9n entiteit opgenomen.",
+ "title": "Selecteer de entiteit die u wilt opnemen"
+ },
+ "bridge_mode": {
+ "data": {
+ "include_domains": "Domeinen om op te nemen"
+ },
+ "description": "Kies de domeinen die moeten worden opgenomen. Alle ondersteunde entiteiten in het domein zullen worden opgenomen.",
+ "title": "Selecteer domeinen die u wilt opnemen"
+ },
"pairing": {
- "description": "Zodra de {name} klaar is, is het koppelen beschikbaar in \"Meldingen\" als \"HomeKit Bridge Setup\".",
- "title": "Koppel HomeKit Bridge"
+ "description": "Volg de instructies in \"Meldingen\" onder \"HomeKit-koppeling\" om het koppelen te voltooien.",
+ "title": "Koppel HomeKit"
},
"user": {
"data": {
"auto_start": "Automatisch starten (uitschakelen als u Z-Wave of een ander vertraagd startsysteem gebruikt)",
- "include_domains": "Domeinen om op te nemen"
+ "include_domains": "Domeinen om op te nemen",
+ "mode": "Mode"
},
- "description": "De HomeKit-integratie geeft u toegang tot uw Home Assistant-entiteiten in HomeKit. In bridge-modus zijn HomeKit-bruggen beperkt tot 150 accessoires per exemplaar, inclusief de brug zelf. Als u meer dan het maximale aantal accessoires wilt overbruggen, is het aan te raden om meerdere HomeKit-bridges voor verschillende domeinen te gebruiken. Gedetailleerde entiteitsconfiguratie is alleen beschikbaar via YAML voor de primaire bridge.",
- "title": "Activeer HomeKit Bridge"
+ "description": "Kies de domeinen die moeten worden opgenomen. Alle ondersteunde entiteiten in het domein zullen worden opgenomen. Voor elke tv-mediaspeler en camera wordt een afzonderlijke HomeKit-instantie in accessoiremodus aangemaakt.",
+ "title": "Selecteer domeinen die u wilt opnemen"
}
}
},
@@ -22,7 +37,7 @@
"step": {
"advanced": {
"data": {
- "auto_start": "Autostart (uitschakelen bij gebruik van Z-Wave of een ander vertraagd startsysteem)",
+ "auto_start": "Autostart (deactiveer als je de homekit.start service handmatig aanroept)",
"safe_mode": "Veilige modus (alleen inschakelen als het koppelen mislukt)"
},
"description": "Deze instellingen hoeven alleen te worden aangepast als HomeKit niet functioneert.",
@@ -37,12 +52,15 @@
},
"include_exclude": {
"data": {
- "entities": "Entiteiten"
- }
+ "entities": "Entiteiten",
+ "mode": "Mode"
+ },
+ "description": "Kies de entiteiten die u wilt opnemen. In de accessoiremodus is slechts een enkele entiteit inbegrepen. In de bridge-include-modus worden alle entiteiten in het domein opgenomen, tenzij specifieke entiteiten zijn geselecteerd. In de bridge-uitsluitingsmodus worden alle entiteiten in het domein opgenomen, behalve de uitgesloten entiteiten. Voor de beste prestaties is elke tv-mediaspeler en camera een apart HomeKit-accessoire.",
+ "title": "Selecteer de entiteiten die u wilt opnemen"
},
"init": {
"data": {
- "include_domains": "Op te nemen domeinen",
+ "include_domains": "Domeinen om op te nemen",
"mode": "modus"
},
"description": "HomeKit kan worden geconfigureerd om een brug of een enkel accessoire te tonen. In de accessoiremodus kan slechts \u00e9\u00e9n entiteit worden gebruikt. De accessoiremodus is vereist om mediaspelers met de tv-apparaatklasse correct te laten werken. Entiteiten in de \"Op te nemen domeinen\" zullen worden blootgesteld aan HomeKit. U kunt op het volgende scherm selecteren welke entiteiten u wilt opnemen of uitsluiten van deze lijst.",
@@ -50,7 +68,7 @@
},
"yaml": {
"description": "Deze invoer wordt beheerd via YAML",
- "title": "Pas de HomeKit Bridge-opties aan"
+ "title": "Pas de HomeKit-opties aan"
}
}
}
diff --git a/homeassistant/components/homekit/translations/no.json b/homeassistant/components/homekit/translations/no.json
index 7eff4d37668e13..e18f9224c68166 100644
--- a/homeassistant/components/homekit/translations/no.json
+++ b/homeassistant/components/homekit/translations/no.json
@@ -4,17 +4,32 @@
"port_name_in_use": "Et tilbeh\u00f8r eller bro med samme navn eller port er allerede konfigurert."
},
"step": {
+ "accessory_mode": {
+ "data": {
+ "entity_id": "Enhet"
+ },
+ "description": "Velg enheten som skal inkluderes. I tilbeh\u00f8rsmodus er bare en enkelt enhet inkludert.",
+ "title": "Velg enhet som skal inkluderes"
+ },
+ "bridge_mode": {
+ "data": {
+ "include_domains": "Domener \u00e5 inkludere"
+ },
+ "description": "Velg domenene som skal inkluderes. Alle st\u00f8ttede enheter i domenet vil bli inkludert.",
+ "title": "Velg domener som skal inkluderes"
+ },
"pairing": {
- "description": "S\u00e5 snart {name} er klart, vil sammenkobling v\u00e6re tilgjengelig i \"Notifications\" som \"HomeKit Bridge Setup\".",
+ "description": "For \u00e5 fullf\u00f8re sammenkoblingen ved \u00e5 f\u00f8lge instruksjonene i \"Varsler\" under \"Sammenkobling av HomeKit\".",
"title": "Koble sammen HomeKit"
},
"user": {
"data": {
"auto_start": "Autostart (deaktiver hvis du bruker Z-Wave eller annet forsinket startsystem)",
- "include_domains": "Domener \u00e5 inkludere"
+ "include_domains": "Domener \u00e5 inkludere",
+ "mode": "Modus"
},
- "description": "HomeKit-integrasjonen gir deg tilgang til Home Assistant-entitetene dine i HomeKit. I bromodus er HomeKit Broer begrenset til 150 tilbeh\u00f8rsenhet per forekomst inkludert selve broen. Hvis du \u00f8nsker \u00e5 \u00f8ke maksimalt antall tilbeh\u00f8rsenheter, anbefales det at du bruker flere HomeKit-broer for forskjellige domener. Detaljert entitetskonfigurasjon er bare tilgjengelig via YAML for den prim\u00e6re broen.",
- "title": "Aktiver HomeKit"
+ "description": "Velg domenene som skal inkluderes. Alle st\u00f8ttede enheter i domenet vil bli inkludert. Det opprettes en egen HomeKit-forekomst i tilbeh\u00f8rsmodus for hver tv-mediaspiller og kamera.",
+ "title": "Velg domener som skal inkluderes"
}
}
},
@@ -22,7 +37,7 @@
"step": {
"advanced": {
"data": {
- "auto_start": "Autostart (deaktiver hvis du bruker Z-Wave eller annet forsinket startsystem)",
+ "auto_start": "Autostart (deaktiver hvis du ringer til homekit.start-tjenesten manuelt)",
"safe_mode": "Sikker modus (aktiver bare hvis sammenkoblingen mislykkes)"
},
"description": "Disse innstillingene m\u00e5 bare justeres hvis HomeKit ikke fungerer.",
@@ -40,16 +55,16 @@
"entities": "Entiteter",
"mode": "Modus"
},
- "description": "Velg entitene som skal eksponeres. I tilbeh\u00f8rsmodus er bare en enkelt entitet eksponert. I bro-inkluderingsmodus vil alle entiteter i domenet bli eksponert med mindre spesifikke entiteter er valgt. I bro-ekskluderingsmodus vil alle entiteter i domenet bli eksponert bortsett fra de ekskluderte entitetene.",
- "title": "Velg entiteter som skal eksponeres"
+ "description": "Velg enhetene som skal inkluderes. I tilbeh\u00f8rsmodus er bare en enkelt enhet inkludert. I bridge-inkluderingsmodus vil alle enheter i domenet bli inkludert, med mindre spesifikke enheter er valgt. I bridge-ekskluderingsmodus vil alle enheter i domenet bli inkludert, bortsett fra de ekskluderte enhetene. For best ytelse opprettes et eget HomeKit-tilbeh\u00f8r for hver tv-mediaspiller, aktivitetsbasert fjernkontroll, l\u00e5s og kamera.",
+ "title": "Velg enheter som skal inkluderes"
},
"init": {
"data": {
"include_domains": "Domener \u00e5 inkludere",
"mode": "Modus"
},
- "description": "HomeKit kan konfigureres for \u00e5 eksponere en bro eller en enkelt tilbeh\u00f8rsenhet. I tilbeh\u00f8rsmodus kan bare en enkelt entitet brukes. Tilbeh\u00f8rsmodus er n\u00f8dvendig for at mediaspillere med TV-enhetsklasse skal fungere skikkelig. Entiteter i \u201cDomains to include\u201d vil bli eksponert for HomeKit. Du vil kunne velge hvilke entiteter du vil inkludere eller ekskludere fra denne listen p\u00e5 neste skjermbilde.",
- "title": "Velg domener du vil eksponere."
+ "description": "HomeKit kan konfigureres vise en bro eller ett enkelt tilbeh\u00f8r. I tilbeh\u00f8rsmodus kan bare \u00e9n enkelt enhet brukes. Tilbeh\u00f8rsmodus kreves for at mediespillere med TV-enhetsklassen skal fungere som de skal. Enheter i \"Domener som skal inkluderes\" inkluderes i HomeKit. Du kan velge hvilke enheter som skal inkluderes eller ekskluderes fra denne listen p\u00e5 neste skjermbilde.",
+ "title": "Velg domener som skal inkluderes."
},
"yaml": {
"description": "Denne oppf\u00f8ringen kontrolleres via YAML",
diff --git a/homeassistant/components/homekit/translations/pl.json b/homeassistant/components/homekit/translations/pl.json
index 3210f0f4430bb7..bcd088762ca966 100644
--- a/homeassistant/components/homekit/translations/pl.json
+++ b/homeassistant/components/homekit/translations/pl.json
@@ -4,17 +4,32 @@
"port_name_in_use": "Akcesorium lub mostek o tej samej nazwie lub adresie IP jest ju\u017c skonfigurowany"
},
"step": {
+ "accessory_mode": {
+ "data": {
+ "entity_id": "Encja"
+ },
+ "description": "Wybierz uwzgl\u0119dniane encje. W trybie akcesori\u00f3w uwzgl\u0119dniana jest tylko jedna encja.",
+ "title": "Wybierz uwzgl\u0119dniane encje"
+ },
+ "bridge_mode": {
+ "data": {
+ "include_domains": "Domeny do uwzgl\u0119dnienia"
+ },
+ "description": "Wybierz uwzgl\u0119dniane domeny. Wszystkie obs\u0142ugiwane encje w domenie zostan\u0105 uwzgl\u0119dnione.",
+ "title": "Wybierz uwzgl\u0119dniane domeny"
+ },
"pairing": {
- "description": "Gdy tylko {name} b\u0119dzie gotowy, opcja parowania b\u0119dzie dost\u0119pna w \u201ePowiadomieniach\u201d jako \u201eKonfiguracja mostka HomeKit\u201d.",
+ "description": "Aby doko\u0144czy\u0107 parowanie, post\u0119puj wg instrukcji \u201eParowanie HomeKit\u201d w \u201ePowiadomieniach\u201d.",
"title": "Parowanie z HomeKit"
},
"user": {
"data": {
"auto_start": "Automatyczne uruchomienie (wy\u0142\u0105cz, je\u015bli u\u017cywasz Z-Wave lub innej integracji op\u00f3\u017aniaj\u0105cej start systemu)",
- "include_domains": "Domeny do uwzgl\u0119dnienia"
+ "include_domains": "Domeny do uwzgl\u0119dnienia",
+ "mode": "Tryb"
},
- "description": "Integracja HomeKit pozwala na dost\u0119p do Twoich encji Home Assistant w HomeKit. W trybie \"Mostka\", mostki HomeKit s\u0105 ograniczone do 150 urz\u0105dze\u0144, w\u0142\u0105czaj\u0105c w to sam mostek. Je\u015bli chcesz wi\u0119cej ni\u017c dozwolona maksymalna liczba urz\u0105dze\u0144, zaleca si\u0119 u\u017cywanie wielu most\u00f3w HomeKit dla r\u00f3\u017cnych domen. Szczeg\u00f3\u0142owa konfiguracja encji jest dost\u0119pna tylko w trybie YAML dla g\u0142\u00f3wnego mostka.",
- "title": "Aktywacja HomeKit"
+ "description": "Wybierz domeny do uwzgl\u0119dnienia. Wszystkie wspierane encje w danej domenie b\u0119d\u0105 uwzgl\u0119dnione. W trybie akcesorium, oddzielna instancja HomeKit zostanie utworzona dla ka\u017cdego tv media playera oraz kamery.",
+ "title": "Wybierz uwzgl\u0119dniane domeny"
}
}
},
@@ -22,7 +37,7 @@
"step": {
"advanced": {
"data": {
- "auto_start": "Automatyczne uruchomienie (wy\u0142\u0105cz, je\u015bli u\u017cywasz Z-Wave lub innej integracji op\u00f3\u017aniaj\u0105cej start systemu)",
+ "auto_start": "Automatyczne uruchomienie (wy\u0142\u0105cz, je\u015bli r\u0119cznie uruchamiasz us\u0142ug\u0119 homekit.start)",
"safe_mode": "Tryb awaryjny (w\u0142\u0105cz tylko wtedy, gdy parowanie nie powiedzie si\u0119)"
},
"description": "Te ustawienia nale\u017cy dostosowa\u0107 tylko wtedy, gdy HomeKit nie dzia\u0142a.",
@@ -40,8 +55,8 @@
"entities": "Encje",
"mode": "Tryb"
},
- "description": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 widoczne. W trybie \"Akcesorium\", tylko jedna encja jest widoczna. W trybie \"Uwzgl\u0119dnij mostek\", wszystkie encje w danej domenie b\u0119d\u0105 widoczne, chyba \u017ce wybrane s\u0105 tylko konkretne encje. W trybie \"Wyklucz mostek\", wszystkie encje b\u0119d\u0105 widoczne, z wyj\u0105tkiem tych wybranych.",
- "title": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 widoczne"
+ "description": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 uwzgl\u0119dnione. W trybie \"Akcesorium\" tylko jedna encja jest uwzgl\u0119dniona. W trybie \"Uwzgl\u0119dnij mostek\", wszystkie encje w danej domenie b\u0119d\u0105 uwzgl\u0119dnione, chyba \u017ce wybrane s\u0105 tylko konkretne encje. W trybie \"Wyklucz mostek\", wszystkie encje b\u0119d\u0105 uwzgl\u0119dnione, z wyj\u0105tkiem tych wybranych. Dla najlepszej wydajno\u015bci, zostanie utworzone oddzielne akcesorium HomeKit dla ka\u017cdego tv media playera, aktywno\u015bci pilota, zamka oraz kamery.",
+ "title": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 uwzgl\u0119dnione"
},
"init": {
"data": {
diff --git a/homeassistant/components/homekit/translations/ro.json b/homeassistant/components/homekit/translations/ro.json
new file mode 100644
index 00000000000000..82e8344417ba17
--- /dev/null
+++ b/homeassistant/components/homekit/translations/ro.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "description": "Integrarea HomeKit v\u0103 va permite s\u0103 accesa\u021bi entit\u0103\u021bile Home Assistant din HomeKit. \u00cen modul bridge, HomeKit Bridges sunt limitate la 150 de accesorii pe instan\u021b\u0103, inclusiv bridge-ul \u00een sine. Dac\u0103 dori\u021bi s\u0103 face\u021bi mai mult dec\u00e2t num\u0103rul maxim de accesorii, este recomandat s\u0103 utiliza\u021bi mai multe poduri HomeKit pentru diferite domenii. Configurarea detaliat\u0103 a entit\u0103\u021bii este disponibil\u0103 numai prin YAML. Pentru cele mai bune performan\u021be \u0219i pentru a preveni indisponibilitatea nea\u0219teptat\u0103, crea\u021bi \u0219i \u00eemperechea\u021bi o instan\u021b\u0103 HomeKit separat\u0103 \u00een modul accesoriu pentru fiecare player media TV \u0219i camer\u0103."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homekit/translations/ru.json b/homeassistant/components/homekit/translations/ru.json
index 3cb5e84936a8a5..81199b2971ce12 100644
--- a/homeassistant/components/homekit/translations/ru.json
+++ b/homeassistant/components/homekit/translations/ru.json
@@ -4,17 +4,32 @@
"port_name_in_use": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0438\u043b\u0438 \u043f\u043e\u0440\u0442\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
},
"step": {
+ "accessory_mode": {
+ "data": {
+ "entity_id": "\u041e\u0431\u044a\u0435\u043a\u0442"
+ },
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043e\u0431\u044a\u0435\u043a\u0442.",
+ "title": "\u0412\u044b\u0431\u043e\u0440 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit"
+ },
+ "bridge_mode": {
+ "data": {
+ "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b"
+ },
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u044b. \u0411\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u0437 \u0434\u043e\u043c\u0435\u043d\u0430.",
+ "title": "\u0412\u044b\u0431\u043e\u0440 \u0434\u043e\u043c\u0435\u043d\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit"
+ },
"pairing": {
- "description": "\u041a\u0430\u043a \u0442\u043e\u043b\u044c\u043a\u043e {name} \u0431\u0443\u0434\u0435\u0442 \u0433\u043e\u0442\u043e\u0432\u043e, \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u0432 \"\u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f\u0445\" \u043a\u0430\u043a \"HomeKit Bridge Setup\".",
+ "description": "\u0414\u043b\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f \u0441\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u043c \u0432 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0438 \"HomeKit Pairing\".",
"title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 HomeKit"
},
"user": {
"data": {
"auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Z-Wave \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430)",
- "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b"
+ "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b",
+ "mode": "\u0420\u0435\u0436\u0438\u043c"
},
- "description": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0430\u043c Home Assistant \u0447\u0435\u0440\u0435\u0437 HomeKit. HomeKit Bridge \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d 150 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430\u043c\u0438 \u043d\u0430 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440, \u0432\u043a\u043b\u044e\u0447\u0430\u044f \u0441\u0430\u043c \u0431\u0440\u0438\u0434\u0436. \u0415\u0441\u043b\u0438 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0431\u043e\u043b\u044c\u0448\u0435, \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e HomeKit Bridge \u0434\u043b\u044f \u0440\u0430\u0437\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432. \u0414\u0435\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 YAML \u0434\u043b\u044f \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e \u0431\u0440\u0438\u0434\u0436\u0430.",
- "title": "HomeKit"
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u044b. \u0411\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u0437 \u0434\u043e\u043c\u0435\u043d\u0430. \u0414\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u0430 \u0438\u043b\u0438 \u043a\u0430\u043c\u0435\u0440\u044b \u0431\u0443\u0434\u0435\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430.",
+ "title": "\u0412\u044b\u0431\u043e\u0440 \u0434\u043e\u043c\u0435\u043d\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit"
}
}
},
@@ -22,7 +37,7 @@
"step": {
"advanced": {
"data": {
- "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Z-Wave \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430)",
+ "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u0412\u044b \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0435 \u0441\u043b\u0443\u0436\u0431\u0443 homekit.start)",
"safe_mode": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c (\u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0441\u0431\u043e\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f)"
},
"description": "\u042d\u0442\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b, \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 HomeKit \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442.",
@@ -40,7 +55,7 @@
"entities": "\u041e\u0431\u044a\u0435\u043a\u0442\u044b",
"mode": "\u0420\u0435\u0436\u0438\u043c"
},
- "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043e\u0431\u044a\u0435\u043a\u0442. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u043c\u043e\u0441\u0442\u0430 \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u0435\u0441\u043b\u0438 \u043d\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u044b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u043a\u0440\u043e\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u0445.",
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043e\u0431\u044a\u0435\u043a\u0442. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u043c\u043e\u0441\u0442\u0430 \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u0435\u0441\u043b\u0438 \u043d\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u044b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u043a\u0440\u043e\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u0445. \u0414\u043b\u044f \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u044b, \u043f\u0443\u043b\u044c\u0442\u044b \u0414\u0423 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439, \u0437\u0430\u043c\u043a\u0438 \u0438 \u043a\u0430\u043c\u0435\u0440\u044b \u0431\u0443\u0434\u0443\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b \u043a\u0430\u043a \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430.",
"title": "\u0412\u044b\u0431\u043e\u0440 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit"
},
"init": {
diff --git a/homeassistant/components/homekit/translations/sv.json b/homeassistant/components/homekit/translations/sv.json
index 5aa9507a85d6fc..1e2fcae04b5bc5 100644
--- a/homeassistant/components/homekit/translations/sv.json
+++ b/homeassistant/components/homekit/translations/sv.json
@@ -1,4 +1,19 @@
{
+ "config": {
+ "step": {
+ "bridge_mode": {
+ "data": {
+ "include_domains": "Dom\u00e4ner att inkludera"
+ }
+ },
+ "pairing": {
+ "title": "Para HomeKit"
+ },
+ "user": {
+ "title": "Aktivera HomeKit"
+ }
+ }
+ },
"options": {
"step": {
"cameras": {
@@ -7,6 +22,12 @@
},
"description": "Kontrollera alla kameror som st\u00f6der inbyggda H.264-str\u00f6mmar. Om kameran inte skickar ut en H.264-str\u00f6m kodar systemet videon till H.264 f\u00f6r HomeKit. Transkodning kr\u00e4ver h\u00f6g prestanda och kommer troligtvis inte att fungera p\u00e5 enkortsdatorer.",
"title": "V\u00e4lj kamerans videoavkodare."
+ },
+ "init": {
+ "data": {
+ "include_domains": "Dom\u00e4ner att inkludera"
+ },
+ "title": "V\u00e4lj dom\u00e4ner som ska inkluderas."
}
}
}
diff --git a/homeassistant/components/homekit/translations/tr.json b/homeassistant/components/homekit/translations/tr.json
new file mode 100644
index 00000000000000..f9391fd0686c61
--- /dev/null
+++ b/homeassistant/components/homekit/translations/tr.json
@@ -0,0 +1,60 @@
+{
+ "config": {
+ "abort": {
+ "port_name_in_use": "Ayn\u0131 ada veya ba\u011flant\u0131 noktas\u0131na sahip bir aksesuar veya k\u00f6pr\u00fc zaten yap\u0131land\u0131r\u0131lm\u0131\u015f."
+ },
+ "step": {
+ "accessory_mode": {
+ "data": {
+ "entity_id": "Varl\u0131k"
+ },
+ "description": "Dahil edilecek varl\u0131\u011f\u0131 se\u00e7in. Aksesuar modunda, yaln\u0131zca tek bir varl\u0131k dahildir.",
+ "title": "Dahil edilecek varl\u0131\u011f\u0131 se\u00e7in"
+ },
+ "bridge_mode": {
+ "data": {
+ "include_domains": "\u0130\u00e7erecek etki alanlar\u0131"
+ },
+ "description": "Dahil edilecek alanlar\u0131 se\u00e7in. Etki alan\u0131ndaki t\u00fcm desteklenen varl\u0131klar dahil edilecektir.",
+ "title": "Dahil edilecek etki alanlar\u0131n\u0131 se\u00e7in"
+ },
+ "pairing": {
+ "description": "{name} haz\u0131r olur olmaz e\u015fle\u015ftirme, \"Bildirimler\" i\u00e7inde \"HomeKit K\u00f6pr\u00fc Kurulumu\" olarak mevcut olacakt\u0131r.",
+ "title": "HomeKit'i E\u015fle\u015ftir"
+ },
+ "user": {
+ "data": {
+ "mode": "Mod"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "advanced": {
+ "data": {
+ "safe_mode": "G\u00fcvenli Mod (yaln\u0131zca e\u015fle\u015ftirme ba\u015far\u0131s\u0131z olursa etkinle\u015ftirin)"
+ }
+ },
+ "cameras": {
+ "data": {
+ "camera_copy": "Yerel H.264 ak\u0131\u015flar\u0131n\u0131 destekleyen kameralar"
+ },
+ "description": "Yerel H.264 ak\u0131\u015flar\u0131n\u0131 destekleyen t\u00fcm kameralar\u0131 kontrol edin. Kamera bir H.264 ak\u0131\u015f\u0131 vermezse, sistem videoyu HomeKit i\u00e7in H.264'e d\u00f6n\u00fc\u015ft\u00fcr\u00fcr. Kod d\u00f6n\u00fc\u015ft\u00fcrme, y\u00fcksek performansl\u0131 bir CPU gerektirir ve tek kartl\u0131 bilgisayarlarda \u00e7al\u0131\u015fma olas\u0131l\u0131\u011f\u0131 d\u00fc\u015f\u00fckt\u00fcr.",
+ "title": "Kamera video codec bile\u015fenini se\u00e7in."
+ },
+ "include_exclude": {
+ "data": {
+ "entities": "Varl\u0131klar",
+ "mode": "Mod"
+ },
+ "title": "Dahil edilecek varl\u0131klar\u0131 se\u00e7in"
+ },
+ "init": {
+ "data": {
+ "mode": "Mod"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homekit/translations/uk.json b/homeassistant/components/homekit/translations/uk.json
index 10cd42ccecb6e4..876b200bdf8219 100644
--- a/homeassistant/components/homekit/translations/uk.json
+++ b/homeassistant/components/homekit/translations/uk.json
@@ -1,10 +1,59 @@
{
+ "config": {
+ "abort": {
+ "port_name_in_use": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437 \u0442\u0430\u043a\u043e\u044e \u0436 \u043d\u0430\u0437\u0432\u043e\u044e \u0430\u0431\u043e \u043f\u043e\u0440\u0442\u043e\u043c \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435."
+ },
+ "step": {
+ "pairing": {
+ "description": "\u042f\u043a \u0442\u0456\u043b\u044c\u043a\u0438 {name} \u0431\u0443\u0434\u0435 \u0433\u043e\u0442\u043e\u0432\u0438\u0439, \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438 \u0431\u0443\u0434\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u0432 \"\u0421\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u043d\u044f\u0445\" \u044f\u043a \"HomeKit Bridge Setup\".",
+ "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0437 HomeKit"
+ },
+ "user": {
+ "data": {
+ "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043f\u0440\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u0456 Z-Wave \u0430\u0431\u043e \u0456\u043d\u0448\u043e\u0457 \u0441\u0438\u0441\u0442\u0435\u043c\u0438 \u0432\u0456\u0434\u043a\u043b\u0430\u0434\u0435\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0443)",
+ "include_domains": "\u0412\u0438\u0431\u0440\u0430\u0442\u0438 \u0434\u043e\u043c\u0435\u043d\u0438"
+ },
+ "description": "\u0426\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0434\u043e\u0437\u0432\u043e\u043b\u044f\u0454 \u043e\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u043e\u0431'\u0454\u043a\u0442\u0456\u0432 Home Assistant \u0447\u0435\u0440\u0435\u0437 HomeKit. HomeKit Bridge \u043e\u0431\u043c\u0435\u0436\u0435\u043d\u0438\u0439 150 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430\u043c\u0438 \u043d\u0430 \u0435\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440, \u0432\u043a\u043b\u044e\u0447\u0430\u044e\u0447\u0438 \u0441\u0430\u043c \u0431\u0440\u0438\u0434\u0436. \u042f\u043a\u0449\u043e \u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0431\u0456\u043b\u044c\u0448\u0435, \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0454\u0442\u044c\u0441\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u043a\u0456\u043b\u044c\u043a\u0430 HomeKit Bridge \u0434\u043b\u044f \u0440\u0456\u0437\u043d\u0438\u0445 \u0434\u043e\u043c\u0435\u043d\u0456\u0432. \u0414\u0435\u0442\u0430\u043b\u044c\u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u043a\u043e\u0436\u043d\u043e\u0433\u043e \u043e\u0431'\u0454\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0435 \u0442\u0456\u043b\u044c\u043a\u0438 \u0447\u0435\u0440\u0435\u0437 YAML \u0434\u043b\u044f \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e \u043c\u043e\u0441\u0442\u0430.",
+ "title": "HomeKit"
+ }
+ }
+ },
"options": {
"step": {
+ "advanced": {
+ "data": {
+ "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043f\u0440\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u0456 Z-Wave \u0430\u0431\u043e \u0456\u043d\u0448\u043e\u0457 \u0441\u0438\u0441\u0442\u0435\u043c\u0438 \u0432\u0456\u0434\u043a\u043b\u0430\u0434\u0435\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0443)",
+ "safe_mode": "\u0411\u0435\u0437\u043f\u0435\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c (\u0443\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c \u0442\u0456\u043b\u044c\u043a\u0438 \u0432 \u0440\u0430\u0437\u0456 \u0437\u0431\u043e\u044e \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f)"
+ },
+ "description": "\u0426\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0456, \u043b\u0438\u0448\u0435 \u044f\u043a\u0449\u043e HomeKit \u043d\u0435 \u043f\u0440\u0430\u0446\u044e\u0454.",
+ "title": "\u0420\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f"
+ },
+ "cameras": {
+ "data": {
+ "camera_copy": "\u041a\u0430\u043c\u0435\u0440\u0438, \u044f\u043a\u0456 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u044e\u0442\u044c \u043f\u043e\u0442\u043e\u043a\u0438 H.264"
+ },
+ "description": "\u042f\u043a\u0449\u043e \u043a\u0430\u043c\u0435\u0440\u0430 \u043d\u0435 \u0432\u0438\u0432\u043e\u0434\u0438\u0442\u044c \u043f\u043e\u0442\u0456\u043a H.264, \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u043f\u0435\u0440\u0435\u043a\u043e\u0434\u043e\u0432\u0443\u0454 \u0432\u0456\u0434\u0435\u043e \u0432 H.264 \u0434\u043b\u044f HomeKit. \u0422\u0440\u0430\u043d\u0441\u043a\u043e\u0434\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u043c\u0430\u0433\u0430\u0454 \u0432\u0438\u0441\u043e\u043a\u043e\u043f\u0440\u043e\u0434\u0443\u043a\u0442\u0438\u0432\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u043e\u0440\u0430 \u0456 \u043d\u0430\u0432\u0440\u044f\u0434 \u0447\u0438 \u0431\u0443\u0434\u0435 \u043f\u0440\u0430\u0446\u044e\u0432\u0430\u0442\u0438 \u043d\u0430 \u043e\u0434\u043d\u043e\u043f\u043b\u0430\u0442\u043d\u0438\u0445 \u043a\u043e\u043c\u043f'\u044e\u0442\u0435\u0440\u0430\u0445.",
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0432\u0456\u0434\u0435\u043e\u043a\u043e\u0434\u0435\u043a \u043a\u0430\u043c\u0435\u0440\u0438."
+ },
+ "include_exclude": {
+ "data": {
+ "entities": "\u0421\u0443\u0442\u043d\u043e\u0441\u0442\u0456",
+ "mode": "\u0420\u0435\u0436\u0438\u043c"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0456 \u0432 HomeKit. \u0423 \u0440\u0435\u0436\u0438\u043c\u0456 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u0430 \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c. \u0423 \u0440\u0435\u0436\u0438\u043c\u0456 \u043c\u043e\u0441\u0442\u0430 \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u0456 \u0432\u0441\u0456 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456, \u0449\u043e \u043d\u0430\u043b\u0435\u0436\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u0443, \u044f\u043a\u0449\u043e \u043d\u0435 \u0432\u0438\u0431\u0440\u0430\u043d\u0456 \u043f\u0435\u0432\u043d\u0456 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456. \u0423 \u0440\u0435\u0436\u0438\u043c\u0456 \u0432\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u0456 \u0432\u0441\u0456 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456, \u0449\u043e \u043d\u0430\u043b\u0435\u0436\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u0443, \u043a\u0440\u0456\u043c \u0432\u0438\u0431\u0440\u0430\u043d\u0438\u0445.",
+ "title": "\u0412\u0438\u0431\u0456\u0440 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0435\u0439 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0456 \u0432 HomeKit"
+ },
"init": {
"data": {
+ "include_domains": "\u0412\u0438\u0431\u0440\u0430\u0442\u0438 \u0434\u043e\u043c\u0435\u043d\u0438",
"mode": "\u0420\u0435\u0436\u0438\u043c"
- }
+ },
+ "description": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0437 HomeKit \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0430 \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u043c\u043e\u0441\u0442\u0430 \u0430\u0431\u043e \u044f\u043a \u043e\u043a\u0440\u0435\u043c\u0438\u0439 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440. \u0423 \u0440\u0435\u0436\u0438\u043c\u0456 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0442\u0456\u043b\u044c\u043a\u0438 \u043e\u0434\u0438\u043d \u043e\u0431'\u0454\u043a\u0442. \u041c\u0435\u0434\u0456\u0430\u043f\u043b\u0435\u0454\u0440\u0438, \u044f\u043a\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0442\u044c\u0441\u044f \u0432 Home Assistant \u0437 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u043c 'device_class: tv', \u0434\u043b\u044f \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0457 \u0440\u043e\u0431\u043e\u0442\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u043e\u0432\u0430\u043d\u0456 \u0432 Homekit \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430. \u041e\u0431'\u0454\u043a\u0442\u0438, \u0449\u043e \u043d\u0430\u043b\u0435\u0436\u0430\u0442\u044c \u043e\u0431\u0440\u0430\u043d\u0438\u043c \u0434\u043e\u043c\u0435\u043d\u0430\u043c, \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u0456 \u0432 HomeKit. \u041d\u0430 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u043e\u043c\u0443 \u0435\u0442\u0430\u043f\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0412\u0438 \u0437\u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u0438\u0431\u0440\u0430\u0442\u0438, \u044f\u043a\u0456 \u043e\u0431'\u0454\u043a\u0442\u0438 \u0432\u0438\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0437 \u0446\u0438\u0445 \u0434\u043e\u043c\u0435\u043d\u0456\u0432.",
+ "title": "\u0412\u0438\u0431\u0456\u0440 \u0434\u043e\u043c\u0435\u043d\u0456\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0456 \u0432 HomeKit"
+ },
+ "yaml": {
+ "description": "\u0426\u0435\u0439 \u0437\u0430\u043f\u0438\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e \u0447\u0435\u0440\u0435\u0437 YAML",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f HomeKit"
}
}
}
diff --git a/homeassistant/components/homekit/translations/zh-Hant.json b/homeassistant/components/homekit/translations/zh-Hant.json
index 0f1093f5b5bbdd..09f9220c20f887 100644
--- a/homeassistant/components/homekit/translations/zh-Hant.json
+++ b/homeassistant/components/homekit/translations/zh-Hant.json
@@ -4,17 +4,32 @@
"port_name_in_use": "\u4f7f\u7528\u76f8\u540c\u540d\u7a31\u6216\u901a\u8a0a\u57e0\u7684\u914d\u4ef6\u6216 Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002"
},
"step": {
+ "accessory_mode": {
+ "data": {
+ "entity_id": "\u5be6\u9ad4"
+ },
+ "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4\u3002\u65bc\u914d\u4ef6\u6a21\u5f0f\u4e0b\uff0c\u50c5\u80fd\u5305\u542b\u55ae\u4e00\u5be6\u9ad4\u3002",
+ "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4"
+ },
+ "bridge_mode": {
+ "data": {
+ "include_domains": "\u5305\u542b\u7db2\u57df"
+ },
+ "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u7db2\u57df\u3002\u6240\u6709\u7db2\u57df\u5167\u652f\u63f4\u7684\u5be6\u9ad4\u90fd\u6703\u5305\u542b\u3002",
+ "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u7db2\u57df"
+ },
"pairing": {
- "description": "\u65bc {name} \u5c31\u7dd2\u5f8c\u3001\u5c07\u6703\u65bc\u300c\u901a\u77e5\u300d\u4e2d\u986f\u793a\u300cHomeKit Bridge \u8a2d\u5b9a\u300d\u7684\u914d\u5c0d\u8cc7\u8a0a\u3002",
+ "description": "\u6b32\u5b8c\u6210\u914d\u5c0d\u3001\u8acb\u8ddf\u96a8\u300c\u901a\u77e5\u300d\u5167\u7684\u300cHomekit \u914d\u5c0d\u300d\u6307\u5f15\u3002",
"title": "\u914d\u5c0d HomeKit"
},
"user": {
"data": {
"auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u9072\u555f\u52d5\u7cfb\u7d71\u6642\u3001\u8acb\u95dc\u9589\uff09",
- "include_domains": "\u5305\u542b Domain"
+ "include_domains": "\u5305\u542b\u7db2\u57df",
+ "mode": "\u6a21\u5f0f"
},
- "description": "HomeKit \u6574\u5408\u5c07\u53ef\u5141\u8a31\u65bc Homekit \u4e2d\u4f7f\u7528 Home Assistant \u5be6\u9ad4\u3002\u65bc\u6a4b\u63a5\u6a21\u5f0f\u4e0b\u3001HomeKit Bridges \u6700\u9ad8\u9650\u5236\u70ba 150 \u500b\u914d\u4ef6\u3001\u5305\u542b Bridge \u672c\u8eab\u3002\u5047\u5982\u60f3\u8981\u4f7f\u7528\u8d85\u904e\u9650\u5236\u4ee5\u4e0a\u7684\u914d\u4ef6\uff0c\u5efa\u8b70\u53ef\u4ee5\u4e0d\u540c Domain \u4f7f\u7528\u591a\u500b HomeKit bridges \u9054\u5230\u6b64\u9700\u6c42\u3002\u50c5\u80fd\u65bc\u4e3b Bridge \u4ee5 YAML \u8a2d\u5b9a\u8a73\u7d30\u5be6\u9ad4\u3002",
- "title": "\u555f\u7528 HomeKit"
+ "description": "\u9078\u64c7\u6240\u8981\u5305\u542b\u7684\u7db2\u57df\uff0c\u6240\u6709\u8a72\u7db2\u57df\u5167\u652f\u63f4\u7684\u5be6\u9ad4\u90fd\u5c07\u6703\u88ab\u5305\u542b\u3002 \u5176\u4ed6 Homekit \u5a92\u9ad4\u64ad\u653e\u5668\u8207\u651d\u5f71\u6a5f\u5be6\u4f8b\uff0c\u5c07\u6703\u4ee5\u914d\u4ef6\u6a21\u5f0f\u65b0\u589e\u3002",
+ "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u7db2\u57df"
}
}
},
@@ -22,7 +37,7 @@
"step": {
"advanced": {
"data": {
- "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u9072\u555f\u52d5\u7cfb\u7d71\u6642\u3001\u8acb\u95dc\u9589\uff09",
+ "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u624b\u52d5\u4f7f\u7528 homekit.start \u670d\u52d9\u6642\u3001\u8acb\u95dc\u9589\uff09",
"safe_mode": "\u5b89\u5168\u6a21\u5f0f\uff08\u50c5\u65bc\u914d\u5c0d\u5931\u6557\u6642\u4f7f\u7528\uff09"
},
"description": "\u50c5\u65bc Homekit \u7121\u6cd5\u6b63\u5e38\u4f7f\u7528\u6642\uff0c\u8abf\u6574\u6b64\u4e9b\u8a2d\u5b9a\u3002",
@@ -40,16 +55,16 @@
"entities": "\u5be6\u9ad4",
"mode": "\u6a21\u5f0f"
},
- "description": "\u9078\u64c7\u9032\u884c\u63a5\u901a\u7684\u5be6\u9ad4\u3002\u65bc\u5305\u542b\u6a21\u5f0f\u4e2d\u3001\u6240\u6709\u7db2\u57df\u7684\u5be6\u9ad4\u90fd\u5c07\u9032\u884c\u63a5\u901a\uff0c\u9664\u975e\u9078\u64c7\u7279\u5b9a\u7684\u5be6\u9ad4\u3002\u65bc\u6392\u9664\u6a21\u5f0f\u4e2d\u3001\u6240\u6709\u7db2\u57df\u4e2d\u7684\u9032\u884c\u63a5\u901a\uff0c\u9664\u4e86\u6392\u9664\u7684\u5be6\u9ad4\u3002",
- "title": "\u9078\u64c7\u8981\u63a5\u901a\u7684\u5be6\u9ad4"
+ "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4\u3002\u65bc\u914d\u4ef6\u6a21\u5f0f\u4e0b\u3001\u50c5\u6709\u55ae\u4e00\u5be6\u9ad4\u5c07\u6703\u5305\u542b\u3002\u65bc\u6a4b\u63a5\u5305\u542b\u6a21\u5f0f\u4e0b\u3001\u6240\u6709\u7db2\u57df\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u975e\u9078\u64c7\u7279\u5b9a\u7684\u5be6\u9ad4\u3002\u65bc\u6a4b\u63a5\u6392\u9664\u6a21\u5f0f\u4e2d\u3001\u6240\u6709\u7db2\u57df\u4e2d\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u4e86\u6392\u9664\u7684\u5be6\u9ad4\u3002\u70ba\u53d6\u5f97\u6700\u4f73\u6548\u80fd\u3001\u6bcf\u4e00\u500b\u96fb\u8996\u5a92\u9ad4\u64ad\u653e\u5668\u3001\u9060\u7aef\u9059\u63a7\u5668\u3001\u9580\u9396\u8207\u651d\u5f71\u6a5f\uff0c\u5c07\u65bc Homekit \u914d\u4ef6\u6a21\u5f0f\u9032\u884c\u3002",
+ "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4"
},
"init": {
"data": {
- "include_domains": "\u5305\u542b Domain",
+ "include_domains": "\u5305\u542b\u7db2\u57df",
"mode": "\u6a21\u5f0f"
},
- "description": "HomeKit \u80fd\u5920\u8a2d\u5b9a\u63a5\u901a\u6a4b\u63a5\u6216\u55ae\u4e00\u914d\u4ef6\u6a21\u5f0f\u3002\u5a92\u9ad4\u64ad\u653e\u5668\u9700\u8981\u4ee5\u96fb\u8996\u88dd\u7f6e\u914d\u4ef6\u6a21\u5f0f\u624d\u80fd\u6b63\u5e38\u4f7f\u7528\u3002\"\u5305\u542b Domains\"\u4e2d\u7684\u5be6\u9ad4\u5c07\u6703\u6a4b\u63a5\u81f3 Homekit\u3001\u53ef\u4ee5\u65bc\u4e0b\u4e00\u500b\u756b\u9762\u4e2d\u9078\u64c7\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684\u5be6\u9ad4\u5217\u8868\u3002",
- "title": "\u9078\u64c7\u6240\u8981\u63a5\u901a\u7684 Domain\u3002"
+ "description": "HomeKit \u80fd\u5920\u8a2d\u5b9a\u63a5\u901a\u6a4b\u63a5\u6216\u55ae\u4e00\u914d\u4ef6\u6a21\u5f0f\u3002 \u65bc\u914d\u4ef6\u6a21\u5f0f\u4e0b\u3001\u50c5\u6709\u55ae\u4e00\u5be6\u9ad4\u5c07\u6703\u5305\u542b\u3002\u5a92\u9ad4\u64ad\u653e\u5668\u9700\u8981\u4ee5\u96fb\u8996\u88dd\u7f6e\u914d\u4ef6\u6a21\u5f0f\u624d\u80fd\u6b63\u5e38\u4f7f\u7528\u3002\"\u5305\u542b\u7db2\u57df\" \u4e2d\u7684\u5be6\u9ad4\u5c07\u6703\u6a4b\u63a5\u81f3 Homekit\u3001\u53ef\u4ee5\u65bc\u4e0b\u4e00\u500b\u756b\u9762\u4e2d\u9078\u64c7\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684\u5be6\u9ad4\u5217\u8868\u3002",
+ "title": "\u9078\u64c7\u6240\u8981\u5305\u542b\u7684\u7db2\u57df\u3002"
},
"yaml": {
"description": "\u6b64\u5be6\u9ad4\u70ba\u900f\u904e YAML \u63a7\u5236",
diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py
index b61a2c57612836..48f7ad9b064792 100644
--- a/homeassistant/components/homekit/type_cameras.py
+++ b/homeassistant/components/homekit/type_cameras.py
@@ -3,7 +3,7 @@
from datetime import timedelta
import logging
-from haffmpeg.core import HAFFmpeg
+from haffmpeg.core import FFMPEG_STDERR, HAFFmpeg
from pyhap.camera import (
VIDEO_CODEC_PARAM_LEVEL_TYPES,
VIDEO_CODEC_PARAM_PROFILE_ID_TYPES,
@@ -115,6 +115,7 @@
VIDEO_PROFILE_NAMES = ["baseline", "main", "high"]
FFMPEG_WATCH_INTERVAL = timedelta(seconds=5)
+FFMPEG_LOGGER = "ffmpeg_logger"
FFMPEG_WATCHER = "ffmpeg_watcher"
FFMPEG_PID = "ffmpeg_pid"
SESSION_ID = "session_id"
@@ -239,7 +240,7 @@ def __init__(self, hass, driver, name, entity_id, aid, config):
self._async_update_doorbell_state(state)
- async def run_handler(self):
+ async def run(self):
"""Handle accessory driver started event.
Run inside the Home Assistant event loop.
@@ -258,7 +259,7 @@ async def run_handler(self):
self._async_update_doorbell_state_event,
)
- await super().run_handler()
+ await super().run()
@callback
def _async_update_motion_state_event(self, event):
@@ -372,7 +373,12 @@ async def start_stream(self, session_info, stream_config):
_LOGGER.debug("FFmpeg output settings: %s", output)
stream = HAFFmpeg(self._ffmpeg.binary)
opened = await stream.open(
- cmd=[], input_source=input_source, output=output, stdout_pipe=False
+ cmd=[],
+ input_source=input_source,
+ output=output,
+ extra_cmd="-hide_banner -nostats",
+ stderr_pipe=True,
+ stdout_pipe=False,
)
if not opened:
_LOGGER.error("Failed to open ffmpeg stream")
@@ -387,9 +393,14 @@ async def start_stream(self, session_info, stream_config):
session_info["stream"] = stream
session_info[FFMPEG_PID] = stream.process.pid
+ stderr_reader = await stream.get_reader(source=FFMPEG_STDERR)
+
async def watch_session(_):
await self._async_ffmpeg_watch(session_info["id"])
+ session_info[FFMPEG_LOGGER] = asyncio.create_task(
+ self._async_log_stderr_stream(stderr_reader)
+ )
session_info[FFMPEG_WATCHER] = async_track_time_interval(
self.hass,
watch_session,
@@ -398,6 +409,16 @@ async def watch_session(_):
return await self._async_ffmpeg_watch(session_info["id"])
+ async def _async_log_stderr_stream(self, stderr_reader):
+ """Log output from ffmpeg."""
+ _LOGGER.debug("%s: ffmpeg: started", self.display_name)
+ while True:
+ line = await stderr_reader.readline()
+ if line == b"":
+ return
+
+ _LOGGER.debug("%s: ffmpeg: %s", self.display_name, line.rstrip())
+
async def _async_ffmpeg_watch(self, session_id):
"""Check to make sure ffmpeg is still running and cleanup if not."""
ffmpeg_pid = self.sessions[session_id][FFMPEG_PID]
@@ -415,6 +436,7 @@ def _async_stop_ffmpeg_watch(self, session_id):
if FFMPEG_WATCHER not in self.sessions[session_id]:
return
self.sessions[session_id].pop(FFMPEG_WATCHER)()
+ self.sessions[session_id].pop(FFMPEG_LOGGER).cancel()
async def stop_stream(self, session_info):
"""Stop the stream for the given ``session_id``."""
@@ -444,13 +466,10 @@ async def reconfigure_stream(self, session_info, stream_config):
"""Reconfigure the stream so that it uses the given ``stream_config``."""
return True
- def get_snapshot(self, image_size):
+ async def async_get_snapshot(self, image_size):
"""Return a jpeg of a snapshot from the camera."""
return scale_jpeg_camera_image(
- asyncio.run_coroutine_threadsafe(
- self.hass.components.camera.async_get_image(self.entity_id),
- self.hass.loop,
- ).result(),
+ await self.hass.components.camera.async_get_image(self.entity_id),
image_size["image-width"],
image_size["image-height"],
)
diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py
index 75cabf144839c6..f21287b3bf847e 100644
--- a/homeassistant/components/homekit/type_covers.py
+++ b/homeassistant/components/homekit/type_covers.py
@@ -33,7 +33,7 @@
from homeassistant.core import callback
from homeassistant.helpers.event import async_track_state_change_event
-from .accessories import TYPES, HomeAccessory, debounce
+from .accessories import TYPES, HomeAccessory
from .const import (
ATTR_OBSTRUCTION_DETECTED,
CHAR_CURRENT_DOOR_STATE,
@@ -113,7 +113,7 @@ def __init__(self, *args):
self.async_update_state(state)
- async def run_handler(self):
+ async def run(self):
"""Handle accessory driver started event.
Run inside the Home Assistant event loop.
@@ -125,7 +125,7 @@ async def run_handler(self):
self._async_update_obstruction_event,
)
- await super().run_handler()
+ await super().run()
@callback
def _async_update_obstruction_event(self, event):
@@ -158,11 +158,11 @@ def set_state(self, value):
if value == HK_DOOR_OPEN:
if self.char_current_state.value != value:
self.char_current_state.set_value(HK_DOOR_OPENING)
- self.call_service(DOMAIN, SERVICE_OPEN_COVER, params)
+ self.async_call_service(DOMAIN, SERVICE_OPEN_COVER, params)
elif value == HK_DOOR_CLOSED:
if self.char_current_state.value != value:
self.char_current_state.set_value(HK_DOOR_CLOSING)
- self.call_service(DOMAIN, SERVICE_CLOSE_COVER, params)
+ self.async_call_service(DOMAIN, SERVICE_CLOSE_COVER, params)
@callback
def async_update_state(self, new_state):
@@ -231,9 +231,10 @@ def set_stop(self, value):
"""Stop the cover motion from HomeKit."""
if value != 1:
return
- self.call_service(DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: self.entity_id})
+ self.async_call_service(
+ DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: self.entity_id}
+ )
- @debounce
def set_tilt(self, value):
"""Set tilt to value if call came from HomeKit."""
_LOGGER.info("%s: Set tilt to %d", self.entity_id, value)
@@ -244,7 +245,7 @@ def set_tilt(self, value):
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_TILT_POSITION: value}
- self.call_service(DOMAIN, SERVICE_SET_COVER_TILT_POSITION, params, value)
+ self.async_call_service(DOMAIN, SERVICE_SET_COVER_TILT_POSITION, params, value)
@callback
def async_update_state(self, new_state):
@@ -284,12 +285,11 @@ def __init__(self, *args, category, service):
)
self.async_update_state(state)
- @debounce
def move_cover(self, value):
"""Move cover to value if call came from HomeKit."""
_LOGGER.debug("%s: Set position to %d", self.entity_id, value)
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_POSITION: value}
- self.call_service(DOMAIN, SERVICE_SET_COVER_POSITION, params, value)
+ self.async_call_service(DOMAIN, SERVICE_SET_COVER_POSITION, params, value)
@callback
def async_update_state(self, new_state):
@@ -360,26 +360,24 @@ def __init__(self, *args):
)
self.async_update_state(state)
- @debounce
def move_cover(self, value):
"""Move cover to value if call came from HomeKit."""
_LOGGER.debug("%s: Set position to %d", self.entity_id, value)
- if self._supports_stop:
- if value > 70:
- service, position = (SERVICE_OPEN_COVER, 100)
- elif value < 30:
- service, position = (SERVICE_CLOSE_COVER, 0)
- else:
- service, position = (SERVICE_STOP_COVER, 50)
+ if (
+ self._supports_stop
+ and value > 70
+ or not self._supports_stop
+ and value >= 50
+ ):
+ service, position = (SERVICE_OPEN_COVER, 100)
+ elif value < 30 or not self._supports_stop:
+ service, position = (SERVICE_CLOSE_COVER, 0)
else:
- if value >= 50:
- service, position = (SERVICE_OPEN_COVER, 100)
- else:
- service, position = (SERVICE_CLOSE_COVER, 0)
+ service, position = (SERVICE_STOP_COVER, 50)
params = {ATTR_ENTITY_ID: self.entity_id}
- self.call_service(DOMAIN, service, params)
+ self.async_call_service(DOMAIN, service, params)
# Snap the current/target position to the expected final position.
self.char_current_position.set_value(position)
diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py
index 7ed7256d48c59f..1efb3b6c8bed85 100644
--- a/homeassistant/components/homekit/type_fans.py
+++ b/homeassistant/components/homekit/type_fans.py
@@ -7,12 +7,16 @@
ATTR_DIRECTION,
ATTR_OSCILLATING,
ATTR_PERCENTAGE,
+ ATTR_PERCENTAGE_STEP,
+ ATTR_PRESET_MODE,
+ ATTR_PRESET_MODES,
DIRECTION_FORWARD,
DIRECTION_REVERSE,
DOMAIN,
SERVICE_OSCILLATE,
SERVICE_SET_DIRECTION,
SERVICE_SET_PERCENTAGE,
+ SERVICE_SET_PRESET_MODE,
SUPPORT_DIRECTION,
SUPPORT_OSCILLATE,
SUPPORT_SET_SPEED,
@@ -30,10 +34,14 @@
from .accessories import TYPES, HomeAccessory
from .const import (
CHAR_ACTIVE,
+ CHAR_NAME,
+ CHAR_ON,
CHAR_ROTATION_DIRECTION,
CHAR_ROTATION_SPEED,
CHAR_SWING_MODE,
+ PROP_MIN_STEP,
SERV_FANV2,
+ SERV_SWITCH,
)
_LOGGER = logging.getLogger(__name__)
@@ -53,6 +61,8 @@ def __init__(self, *args):
state = self.hass.states.get(self.entity_id)
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
+ percentage_step = state.attributes.get(ATTR_PERCENTAGE_STEP, 1)
+ preset_modes = state.attributes.get(ATTR_PRESET_MODES)
if features & SUPPORT_DIRECTION:
chars.append(CHAR_ROTATION_DIRECTION)
@@ -62,11 +72,13 @@ def __init__(self, *args):
chars.append(CHAR_ROTATION_SPEED)
serv_fan = self.add_preload_service(SERV_FANV2, chars)
+ self.set_primary_service(serv_fan)
self.char_active = serv_fan.configure_char(CHAR_ACTIVE, value=0)
self.char_direction = None
self.char_speed = None
self.char_swing = None
+ self.preset_mode_chars = {}
if CHAR_ROTATION_DIRECTION in chars:
self.char_direction = serv_fan.configure_char(
@@ -77,7 +89,27 @@ def __init__(self, *args):
# Initial value is set to 100 because 0 is a special value (off). 100 is
# an arbitrary non-zero value. It is updated immediately by async_update_state
# to set to the correct initial value.
- self.char_speed = serv_fan.configure_char(CHAR_ROTATION_SPEED, value=100)
+ self.char_speed = serv_fan.configure_char(
+ CHAR_ROTATION_SPEED,
+ value=100,
+ properties={PROP_MIN_STEP: percentage_step},
+ )
+
+ if preset_modes:
+ for preset_mode in preset_modes:
+ preset_serv = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
+ serv_fan.add_linked_service(preset_serv)
+ preset_serv.configure_char(
+ CHAR_NAME, value=f"{self.display_name} {preset_mode}"
+ )
+
+ self.preset_mode_chars[preset_mode] = preset_serv.configure_char(
+ CHAR_ON,
+ value=False,
+ setter_callback=lambda value, preset_mode=preset_mode: self.set_preset_mode(
+ value, preset_mode
+ ),
+ )
if CHAR_SWING_MODE in chars:
self.char_swing = serv_fan.configure_char(CHAR_SWING_MODE, value=0)
@@ -113,32 +145,44 @@ def _set_chars(self, char_values):
if CHAR_ROTATION_SPEED in char_values:
self.set_percentage(char_values[CHAR_ROTATION_SPEED])
+ def set_preset_mode(self, value, preset_mode):
+ """Set preset_mode if call came from HomeKit."""
+ _LOGGER.debug(
+ "%s: Set preset_mode %s to %d", self.entity_id, preset_mode, value
+ )
+ params = {ATTR_ENTITY_ID: self.entity_id}
+ if value:
+ params[ATTR_PRESET_MODE] = preset_mode
+ self.async_call_service(DOMAIN, SERVICE_SET_PRESET_MODE, params)
+ else:
+ self.async_call_service(DOMAIN, SERVICE_TURN_ON, params)
+
def set_state(self, value):
"""Set state if call came from HomeKit."""
_LOGGER.debug("%s: Set state to %d", self.entity_id, value)
service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF
params = {ATTR_ENTITY_ID: self.entity_id}
- self.call_service(DOMAIN, service, params)
+ self.async_call_service(DOMAIN, service, params)
def set_direction(self, value):
"""Set state if call came from HomeKit."""
_LOGGER.debug("%s: Set direction to %d", self.entity_id, value)
direction = DIRECTION_REVERSE if value == 1 else DIRECTION_FORWARD
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_DIRECTION: direction}
- self.call_service(DOMAIN, SERVICE_SET_DIRECTION, params, direction)
+ self.async_call_service(DOMAIN, SERVICE_SET_DIRECTION, params, direction)
def set_oscillating(self, value):
"""Set state if call came from HomeKit."""
_LOGGER.debug("%s: Set oscillating to %d", self.entity_id, value)
oscillating = value == 1
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_OSCILLATING: oscillating}
- self.call_service(DOMAIN, SERVICE_OSCILLATE, params, oscillating)
+ self.async_call_service(DOMAIN, SERVICE_OSCILLATE, params, oscillating)
def set_percentage(self, value):
"""Set state if call came from HomeKit."""
_LOGGER.debug("%s: Set speed to %d", self.entity_id, value)
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_PERCENTAGE: value}
- self.call_service(DOMAIN, SERVICE_SET_PERCENTAGE, params, value)
+ self.async_call_service(DOMAIN, SERVICE_SET_PERCENTAGE, params, value)
@callback
def async_update_state(self, new_state):
@@ -186,3 +230,9 @@ def async_update_state(self, new_state):
hk_oscillating = 1 if oscillating else 0
if self.char_swing.value != hk_oscillating:
self.char_swing.set_value(hk_oscillating)
+
+ current_preset_mode = new_state.attributes.get(ATTR_PRESET_MODE)
+ for preset_mode, char in self.preset_mode_chars.items():
+ hk_value = 1 if preset_mode == current_preset_mode else 0
+ if char.value != hk_value:
+ char.set_value(hk_value)
diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py
index dd829206b0cf7b..a4a73abf998fae 100644
--- a/homeassistant/components/homekit/type_humidifiers.py
+++ b/homeassistant/components/homekit/type_humidifiers.py
@@ -143,7 +143,7 @@ def __init__(self, *args):
if humidity_state:
self._async_update_current_humidity(humidity_state)
- async def run_handler(self):
+ async def run(self):
"""Handle accessory driver started event.
Run inside the Home Assistant event loop.
@@ -155,7 +155,7 @@ async def run_handler(self):
self.async_update_current_humidity_event,
)
- await super().run_handler()
+ await super().run()
@callback
def async_update_current_humidity_event(self, event):
@@ -201,7 +201,7 @@ def _set_chars(self, char_values):
)
if CHAR_ACTIVE in char_values:
- self.call_service(
+ self.async_call_service(
DOMAIN,
SERVICE_TURN_ON if char_values[CHAR_ACTIVE] else SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: self.entity_id},
@@ -210,7 +210,7 @@ def _set_chars(self, char_values):
if self._target_humidity_char_name in char_values:
humidity = round(char_values[self._target_humidity_char_name])
- self.call_service(
+ self.async_call_service(
DOMAIN,
SERVICE_SET_HUMIDITY,
{ATTR_ENTITY_ID: self.entity_id, ATTR_HUMIDITY: humidity},
@@ -240,6 +240,8 @@ def async_update_state(self, new_state):
# Update target humidity
target_humidity = new_state.attributes.get(ATTR_HUMIDITY)
- if isinstance(target_humidity, (int, float)):
- if self.char_target_humidity.value != target_humidity:
- self.char_target_humidity.set_value(target_humidity)
+ if (
+ isinstance(target_humidity, (int, float))
+ and self.char_target_humidity.value != target_humidity
+ ):
+ self.char_target_humidity.set_value(target_humidity)
diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py
index 086934ea6f756f..8be1580537dd45 100644
--- a/homeassistant/components/homekit/type_lights.py
+++ b/homeassistant/components/homekit/type_lights.py
@@ -139,7 +139,7 @@ def _set_chars(self, char_values):
params[ATTR_HS_COLOR] = color
events.append(f"set color at {color}")
- self.call_service(DOMAIN, service, params, ", ".join(events))
+ self.async_call_service(DOMAIN, service, params, ", ".join(events))
@callback
def async_update_state(self, new_state):
diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py
index af5b24c50e17d2..17e2eee46e87f9 100644
--- a/homeassistant/components/homekit/type_locks.py
+++ b/homeassistant/components/homekit/type_locks.py
@@ -61,7 +61,7 @@ def set_state(self, value):
params = {ATTR_ENTITY_ID: self.entity_id}
if self._code:
params[ATTR_CODE] = self._code
- self.call_service(DOMAIN, service, params)
+ self.async_call_service(DOMAIN, service, params)
@callback
def async_update_state(self, new_state):
@@ -78,9 +78,11 @@ def async_update_state(self, new_state):
# LockTargetState only supports locked and unlocked
# Must set lock target state before current state
# or there will be no notification
- if hass_state in (STATE_LOCKED, STATE_UNLOCKED):
- if self.char_target_state.value != current_lock_state:
- self.char_target_state.set_value(current_lock_state)
+ if (
+ hass_state in (STATE_LOCKED, STATE_UNLOCKED)
+ and self.char_target_state.value != current_lock_state
+ ):
+ self.char_target_state.set_value(current_lock_state)
# Set lock current state ONLY after ensuring that
# target state is correct or there will be no
diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py
index 901ac2173f4a0a..5cd27109bd8fc7 100644
--- a/homeassistant/components/homekit/type_media_players.py
+++ b/homeassistant/components/homekit/type_media_players.py
@@ -1,7 +1,7 @@
"""Class to hold all media player accessories."""
import logging
-from pyhap.const import CATEGORY_SWITCH, CATEGORY_TELEVISION
+from pyhap.const import CATEGORY_SWITCH
from homeassistant.components.media_player import (
ATTR_INPUT_SOURCE,
@@ -42,17 +42,9 @@
from .const import (
ATTR_KEY_NAME,
CHAR_ACTIVE,
- CHAR_ACTIVE_IDENTIFIER,
- CHAR_CONFIGURED_NAME,
- CHAR_CURRENT_VISIBILITY_STATE,
- CHAR_IDENTIFIER,
- CHAR_INPUT_SOURCE_TYPE,
- CHAR_IS_CONFIGURED,
CHAR_MUTE,
CHAR_NAME,
CHAR_ON,
- CHAR_REMOTE_KEY,
- CHAR_SLEEP_DISCOVER_MODE,
CHAR_VOLUME,
CHAR_VOLUME_CONTROL_TYPE,
CHAR_VOLUME_SELECTOR,
@@ -62,43 +54,15 @@
FEATURE_PLAY_PAUSE,
FEATURE_PLAY_STOP,
FEATURE_TOGGLE_MUTE,
- KEY_ARROW_DOWN,
- KEY_ARROW_LEFT,
- KEY_ARROW_RIGHT,
- KEY_ARROW_UP,
- KEY_BACK,
- KEY_EXIT,
- KEY_FAST_FORWARD,
- KEY_INFORMATION,
- KEY_NEXT_TRACK,
KEY_PLAY_PAUSE,
- KEY_PREVIOUS_TRACK,
- KEY_REWIND,
- KEY_SELECT,
- SERV_INPUT_SOURCE,
SERV_SWITCH,
- SERV_TELEVISION,
SERV_TELEVISION_SPEAKER,
)
+from .type_remotes import REMOTE_KEYS, RemoteInputSelectAccessory
from .util import get_media_player_features
_LOGGER = logging.getLogger(__name__)
-MEDIA_PLAYER_KEYS = {
- 0: KEY_REWIND,
- 1: KEY_FAST_FORWARD,
- 2: KEY_NEXT_TRACK,
- 3: KEY_PREVIOUS_TRACK,
- 4: KEY_ARROW_UP,
- 5: KEY_ARROW_DOWN,
- 6: KEY_ARROW_LEFT,
- 7: KEY_ARROW_RIGHT,
- 8: KEY_SELECT,
- 9: KEY_BACK,
- 10: KEY_EXIT,
- 11: KEY_PLAY_PAUSE,
- 15: KEY_INFORMATION,
-}
# Names may not contain special characters
# or emjoi (/ is a special character for Apple)
@@ -177,7 +141,7 @@ def set_on_off(self, value):
_LOGGER.debug('%s: Set switch state for "on_off" to %s', self.entity_id, value)
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
params = {ATTR_ENTITY_ID: self.entity_id}
- self.call_service(DOMAIN, service, params)
+ self.async_call_service(DOMAIN, service, params)
def set_play_pause(self, value):
"""Move switch state to value if call came from HomeKit."""
@@ -186,7 +150,7 @@ def set_play_pause(self, value):
)
service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_PAUSE
params = {ATTR_ENTITY_ID: self.entity_id}
- self.call_service(DOMAIN, service, params)
+ self.async_call_service(DOMAIN, service, params)
def set_play_stop(self, value):
"""Move switch state to value if call came from HomeKit."""
@@ -195,7 +159,7 @@ def set_play_stop(self, value):
)
service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_STOP
params = {ATTR_ENTITY_ID: self.entity_id}
- self.call_service(DOMAIN, service, params)
+ self.async_call_service(DOMAIN, service, params)
def set_toggle_mute(self, value):
"""Move switch state to value if call came from HomeKit."""
@@ -203,7 +167,7 @@ def set_toggle_mute(self, value):
'%s: Set switch state for "toggle_mute" to %s', self.entity_id, value
)
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_MEDIA_VOLUME_MUTED: value}
- self.call_service(DOMAIN, SERVICE_VOLUME_MUTE, params)
+ self.async_call_service(DOMAIN, SERVICE_VOLUME_MUTE, params)
@callback
def async_update_state(self, new_state):
@@ -250,21 +214,21 @@ def async_update_state(self, new_state):
@TYPES.register("TelevisionMediaPlayer")
-class TelevisionMediaPlayer(HomeAccessory):
+class TelevisionMediaPlayer(RemoteInputSelectAccessory):
"""Generate a Television Media Player accessory."""
def __init__(self, *args):
- """Initialize a Switch accessory object."""
- super().__init__(*args, category=CATEGORY_TELEVISION)
+ """Initialize a Television Media Player accessory object."""
+ super().__init__(
+ SUPPORT_SELECT_SOURCE,
+ ATTR_INPUT_SOURCE,
+ ATTR_INPUT_SOURCE_LIST,
+ *args,
+ )
state = self.hass.states.get(self.entity_id)
+ features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
- self.support_select_source = False
-
- self.sources = []
-
- self.chars_tv = [CHAR_REMOTE_KEY]
self.chars_speaker = []
- features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
self._supports_play_pause = features & (SUPPORT_PLAY | SUPPORT_PAUSE)
if features & SUPPORT_VOLUME_MUTE or features & SUPPORT_VOLUME_STEP:
@@ -274,27 +238,11 @@ def __init__(self, *args):
if features & SUPPORT_VOLUME_SET:
self.chars_speaker.append(CHAR_VOLUME)
- source_list = state.attributes.get(ATTR_INPUT_SOURCE_LIST, [])
- if source_list and features & SUPPORT_SELECT_SOURCE:
- self.support_select_source = True
-
- serv_tv = self.add_preload_service(SERV_TELEVISION, self.chars_tv)
- self.set_primary_service(serv_tv)
- serv_tv.configure_char(CHAR_CONFIGURED_NAME, value=self.display_name)
- serv_tv.configure_char(CHAR_SLEEP_DISCOVER_MODE, value=True)
- self.char_active = serv_tv.configure_char(
- CHAR_ACTIVE, setter_callback=self.set_on_off
- )
-
- self.char_remote_key = serv_tv.configure_char(
- CHAR_REMOTE_KEY, setter_callback=self.set_remote_key
- )
-
if CHAR_VOLUME_SELECTOR in self.chars_speaker:
serv_speaker = self.add_preload_service(
SERV_TELEVISION_SPEAKER, self.chars_speaker
)
- serv_tv.add_linked_service(serv_speaker)
+ self.serv_tv.add_linked_service(serv_speaker)
name = f"{self.display_name} Volume"
serv_speaker.configure_char(CHAR_NAME, value=name)
@@ -318,25 +266,6 @@ def __init__(self, *args):
CHAR_VOLUME, setter_callback=self.set_volume
)
- if self.support_select_source:
- self.sources = source_list
- self.char_input_source = serv_tv.configure_char(
- CHAR_ACTIVE_IDENTIFIER, setter_callback=self.set_input_source
- )
- for index, source in enumerate(self.sources):
- serv_input = self.add_preload_service(
- SERV_INPUT_SOURCE, [CHAR_IDENTIFIER, CHAR_NAME]
- )
- serv_tv.add_linked_service(serv_input)
- serv_input.configure_char(CHAR_CONFIGURED_NAME, value=source)
- serv_input.configure_char(CHAR_NAME, value=source)
- serv_input.configure_char(CHAR_IDENTIFIER, value=index)
- serv_input.configure_char(CHAR_IS_CONFIGURED, value=True)
- input_type = 3 if "hdmi" in source.lower() else 0
- serv_input.configure_char(CHAR_INPUT_SOURCE_TYPE, value=input_type)
- serv_input.configure_char(CHAR_CURRENT_VISIBILITY_STATE, value=False)
- _LOGGER.debug("%s: Added source %s", self.entity_id, source)
-
self.async_update_state(state)
def set_on_off(self, value):
@@ -344,7 +273,7 @@ def set_on_off(self, value):
_LOGGER.debug('%s: Set switch state for "on_off" to %s', self.entity_id, value)
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
params = {ATTR_ENTITY_ID: self.entity_id}
- self.call_service(DOMAIN, service, params)
+ self.async_call_service(DOMAIN, service, params)
def set_mute(self, value):
"""Move switch state to value if call came from HomeKit."""
@@ -352,32 +281,32 @@ def set_mute(self, value):
'%s: Set switch state for "toggle_mute" to %s', self.entity_id, value
)
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_MEDIA_VOLUME_MUTED: value}
- self.call_service(DOMAIN, SERVICE_VOLUME_MUTE, params)
+ self.async_call_service(DOMAIN, SERVICE_VOLUME_MUTE, params)
def set_volume(self, value):
"""Send volume step value if call came from HomeKit."""
_LOGGER.debug("%s: Set volume to %s", self.entity_id, value)
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_MEDIA_VOLUME_LEVEL: value}
- self.call_service(DOMAIN, SERVICE_VOLUME_SET, params)
+ self.async_call_service(DOMAIN, SERVICE_VOLUME_SET, params)
def set_volume_step(self, value):
"""Send volume step value if call came from HomeKit."""
_LOGGER.debug("%s: Step volume by %s", self.entity_id, value)
service = SERVICE_VOLUME_DOWN if value else SERVICE_VOLUME_UP
params = {ATTR_ENTITY_ID: self.entity_id}
- self.call_service(DOMAIN, service, params)
+ self.async_call_service(DOMAIN, service, params)
def set_input_source(self, value):
"""Send input set value if call came from HomeKit."""
_LOGGER.debug("%s: Set current input to %s", self.entity_id, value)
source = self.sources[value]
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_INPUT_SOURCE: source}
- self.call_service(DOMAIN, SERVICE_SELECT_SOURCE, params)
+ self.async_call_service(DOMAIN, SERVICE_SELECT_SOURCE, params)
def set_remote_key(self, value):
"""Send remote key value if call came from HomeKit."""
_LOGGER.debug("%s: Set remote key to %s", self.entity_id, value)
- key_name = MEDIA_PLAYER_KEYS.get(value)
+ key_name = REMOTE_KEYS.get(value)
if key_name is None:
_LOGGER.warning("%s: Unhandled key press for %s", self.entity_id, value)
return
@@ -392,13 +321,14 @@ def set_remote_key(self, value):
else:
service = SERVICE_MEDIA_PLAY_PAUSE
params = {ATTR_ENTITY_ID: self.entity_id}
- self.call_service(DOMAIN, service, params)
- else:
- # Unhandled keys can be handled by listening to the event bus
- self.hass.bus.fire(
- EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED,
- {ATTR_KEY_NAME: key_name, ATTR_ENTITY_ID: self.entity_id},
- )
+ self.async_call_service(DOMAIN, service, params)
+ return
+
+ # Unhandled keys can be handled by listening to the event bus
+ self.hass.bus.async_fire(
+ EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED,
+ {ATTR_KEY_NAME: key_name, ATTR_ENTITY_ID: self.entity_id},
+ )
@callback
def async_update_state(self, new_state):
@@ -424,18 +354,4 @@ def async_update_state(self, new_state):
if self.char_mute.value != current_mute_state:
self.char_mute.set_value(current_mute_state)
- # Set active input
- if self.support_select_source and self.sources:
- source_name = new_state.attributes.get(ATTR_INPUT_SOURCE)
- _LOGGER.debug("%s: Set current input to %s", self.entity_id, source_name)
- if source_name in self.sources:
- index = self.sources.index(source_name)
- if self.char_input_source.value != index:
- self.char_input_source.set_value(index)
- elif hk_state:
- _LOGGER.warning(
- "%s: Sources out of sync. Restart Home Assistant",
- self.entity_id,
- )
- if self.char_input_source.value != 0:
- self.char_input_source.set_value(0)
+ self._async_update_input_state(hk_state, new_state)
diff --git a/homeassistant/components/homekit/type_remotes.py b/homeassistant/components/homekit/type_remotes.py
new file mode 100644
index 00000000000000..e4f18a7c16f42b
--- /dev/null
+++ b/homeassistant/components/homekit/type_remotes.py
@@ -0,0 +1,214 @@
+"""Class to hold remote accessories."""
+from abc import abstractmethod
+import logging
+
+from pyhap.const import CATEGORY_TELEVISION
+
+from homeassistant.components.remote import (
+ ATTR_ACTIVITY,
+ ATTR_ACTIVITY_LIST,
+ ATTR_CURRENT_ACTIVITY,
+ DOMAIN as REMOTE_DOMAIN,
+ SUPPORT_ACTIVITY,
+)
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ STATE_ON,
+)
+from homeassistant.core import callback
+
+from .accessories import TYPES, HomeAccessory
+from .const import (
+ ATTR_KEY_NAME,
+ CHAR_ACTIVE,
+ CHAR_ACTIVE_IDENTIFIER,
+ CHAR_CONFIGURED_NAME,
+ CHAR_CURRENT_VISIBILITY_STATE,
+ CHAR_IDENTIFIER,
+ CHAR_INPUT_SOURCE_TYPE,
+ CHAR_IS_CONFIGURED,
+ CHAR_NAME,
+ CHAR_REMOTE_KEY,
+ CHAR_SLEEP_DISCOVER_MODE,
+ EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED,
+ KEY_ARROW_DOWN,
+ KEY_ARROW_LEFT,
+ KEY_ARROW_RIGHT,
+ KEY_ARROW_UP,
+ KEY_BACK,
+ KEY_EXIT,
+ KEY_FAST_FORWARD,
+ KEY_INFORMATION,
+ KEY_NEXT_TRACK,
+ KEY_PLAY_PAUSE,
+ KEY_PREVIOUS_TRACK,
+ KEY_REWIND,
+ KEY_SELECT,
+ SERV_INPUT_SOURCE,
+ SERV_TELEVISION,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+REMOTE_KEYS = {
+ 0: KEY_REWIND,
+ 1: KEY_FAST_FORWARD,
+ 2: KEY_NEXT_TRACK,
+ 3: KEY_PREVIOUS_TRACK,
+ 4: KEY_ARROW_UP,
+ 5: KEY_ARROW_DOWN,
+ 6: KEY_ARROW_LEFT,
+ 7: KEY_ARROW_RIGHT,
+ 8: KEY_SELECT,
+ 9: KEY_BACK,
+ 10: KEY_EXIT,
+ 11: KEY_PLAY_PAUSE,
+ 15: KEY_INFORMATION,
+}
+
+
+class RemoteInputSelectAccessory(HomeAccessory):
+ """Generate a InputSelect accessory."""
+
+ def __init__(
+ self,
+ required_feature,
+ source_key,
+ source_list_key,
+ *args,
+ **kwargs,
+ ):
+ """Initialize a InputSelect accessory object."""
+ super().__init__(*args, category=CATEGORY_TELEVISION, **kwargs)
+ state = self.hass.states.get(self.entity_id)
+ features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
+
+ self.source_key = source_key
+ self.sources = []
+ self.support_select_source = False
+ if features & required_feature:
+ self.sources = state.attributes.get(source_list_key, [])
+ if self.sources:
+ self.support_select_source = True
+
+ self.chars_tv = [CHAR_REMOTE_KEY]
+ serv_tv = self.serv_tv = self.add_preload_service(
+ SERV_TELEVISION, self.chars_tv
+ )
+ self.char_remote_key = self.serv_tv.configure_char(
+ CHAR_REMOTE_KEY, setter_callback=self.set_remote_key
+ )
+ self.set_primary_service(serv_tv)
+ serv_tv.configure_char(CHAR_CONFIGURED_NAME, value=self.display_name)
+ serv_tv.configure_char(CHAR_SLEEP_DISCOVER_MODE, value=True)
+ self.char_active = serv_tv.configure_char(
+ CHAR_ACTIVE, setter_callback=self.set_on_off
+ )
+
+ if not self.support_select_source:
+ return
+
+ self.char_input_source = serv_tv.configure_char(
+ CHAR_ACTIVE_IDENTIFIER, setter_callback=self.set_input_source
+ )
+ for index, source in enumerate(self.sources):
+ serv_input = self.add_preload_service(
+ SERV_INPUT_SOURCE, [CHAR_IDENTIFIER, CHAR_NAME]
+ )
+ serv_tv.add_linked_service(serv_input)
+ serv_input.configure_char(CHAR_CONFIGURED_NAME, value=source)
+ serv_input.configure_char(CHAR_NAME, value=source)
+ serv_input.configure_char(CHAR_IDENTIFIER, value=index)
+ serv_input.configure_char(CHAR_IS_CONFIGURED, value=True)
+ input_type = 3 if "hdmi" in source.lower() else 0
+ serv_input.configure_char(CHAR_INPUT_SOURCE_TYPE, value=input_type)
+ serv_input.configure_char(CHAR_CURRENT_VISIBILITY_STATE, value=False)
+ _LOGGER.debug("%s: Added source %s", self.entity_id, source)
+
+ @abstractmethod
+ def set_on_off(self, value):
+ """Move switch state to value if call came from HomeKit."""
+
+ @abstractmethod
+ def set_input_source(self, value):
+ """Send input set value if call came from HomeKit."""
+
+ @abstractmethod
+ def set_remote_key(self, value):
+ """Send remote key value if call came from HomeKit."""
+
+ @callback
+ def _async_update_input_state(self, hk_state, new_state):
+ """Update input state after state changed."""
+ # Set active input
+ if not self.support_select_source or not self.sources:
+ return
+ source_name = new_state.attributes.get(self.source_key)
+ _LOGGER.debug("%s: Set current input to %s", self.entity_id, source_name)
+ if source_name in self.sources:
+ index = self.sources.index(source_name)
+ if self.char_input_source.value != index:
+ self.char_input_source.set_value(index)
+ elif hk_state:
+ _LOGGER.warning(
+ "%s: Sources out of sync. Restart Home Assistant",
+ self.entity_id,
+ )
+ if self.char_input_source.value != 0:
+ self.char_input_source.set_value(0)
+
+
+@TYPES.register("ActivityRemote")
+class ActivityRemote(RemoteInputSelectAccessory):
+ """Generate a Activity Remote accessory."""
+
+ def __init__(self, *args):
+ """Initialize a Activity Remote accessory object."""
+ super().__init__(
+ SUPPORT_ACTIVITY,
+ ATTR_CURRENT_ACTIVITY,
+ ATTR_ACTIVITY_LIST,
+ *args,
+ )
+ self.async_update_state(self.hass.states.get(self.entity_id))
+
+ def set_on_off(self, value):
+ """Move switch state to value if call came from HomeKit."""
+ _LOGGER.debug('%s: Set switch state for "on_off" to %s', self.entity_id, value)
+ service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
+ params = {ATTR_ENTITY_ID: self.entity_id}
+ self.async_call_service(REMOTE_DOMAIN, service, params)
+
+ def set_input_source(self, value):
+ """Send input set value if call came from HomeKit."""
+ _LOGGER.debug("%s: Set current input to %s", self.entity_id, value)
+ source = self.sources[value]
+ params = {ATTR_ENTITY_ID: self.entity_id, ATTR_ACTIVITY: source}
+ self.async_call_service(REMOTE_DOMAIN, SERVICE_TURN_ON, params)
+
+ def set_remote_key(self, value):
+ """Send remote key value if call came from HomeKit."""
+ _LOGGER.debug("%s: Set remote key to %s", self.entity_id, value)
+ key_name = REMOTE_KEYS.get(value)
+ if key_name is None:
+ _LOGGER.warning("%s: Unhandled key press for %s", self.entity_id, value)
+ return
+ self.hass.bus.async_fire(
+ EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED,
+ {ATTR_KEY_NAME: key_name, ATTR_ENTITY_ID: self.entity_id},
+ )
+
+ @callback
+ def async_update_state(self, new_state):
+ """Update Television remote state after state changed."""
+ current_state = new_state.state
+ # Power state remote
+ hk_state = 1 if current_state == STATE_ON else 0
+ _LOGGER.debug("%s: Set current active state to %s", self.entity_id, hk_state)
+ if self.char_active.value != hk_state:
+ self.char_active.set_value(hk_state)
+
+ self._async_update_input_state(hk_state, new_state)
diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py
index feae1b5cd0676b..acbf636c1c3b30 100644
--- a/homeassistant/components/homekit/type_security_systems.py
+++ b/homeassistant/components/homekit/type_security_systems.py
@@ -150,7 +150,7 @@ def set_security_state(self, value):
params = {ATTR_ENTITY_ID: self.entity_id}
if self._alarm_code:
params[ATTR_CODE] = self._alarm_code
- self.call_service(DOMAIN, service, params)
+ self.async_call_service(DOMAIN, service, params)
@callback
def async_update_state(self, new_state):
diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py
index 28c7ea26009769..b6cc4b05125624 100644
--- a/homeassistant/components/homekit/type_sensors.py
+++ b/homeassistant/components/homekit/type_sensors.py
@@ -6,6 +6,8 @@
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
+ DEVICE_CLASS_CO,
+ DEVICE_CLASS_CO2,
STATE_HOME,
STATE_ON,
TEMP_CELSIUS,
@@ -30,7 +32,6 @@
CHAR_MOTION_DETECTED,
CHAR_OCCUPANCY_DETECTED,
CHAR_SMOKE_DETECTED,
- DEVICE_CLASS_CO2,
DEVICE_CLASS_DOOR,
DEVICE_CLASS_GARAGE_DOOR,
DEVICE_CLASS_GAS,
@@ -60,6 +61,7 @@
_LOGGER = logging.getLogger(__name__)
BINARY_SENSOR_SERVICE_MAP = {
+ DEVICE_CLASS_CO: (SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED, int),
DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED, int),
DEVICE_CLASS_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int),
DEVICE_CLASS_GARAGE_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int),
diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py
index b3ee8a06497497..8ea19897420b82 100644
--- a/homeassistant/components/homekit/type_switches.py
+++ b/homeassistant/components/homekit/type_switches.py
@@ -27,7 +27,7 @@
STATE_ON,
)
from homeassistant.core import callback, split_entity_id
-from homeassistant.helpers.event import call_later
+from homeassistant.helpers.event import async_call_later
from .accessories import TYPES, HomeAccessory
from .const import (
@@ -80,7 +80,7 @@ def set_state(self, value):
_LOGGER.debug("%s: Set switch state to %s", self.entity_id, value)
params = {ATTR_ENTITY_ID: self.entity_id}
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
- self.call_service(DOMAIN, service, params)
+ self.async_call_service(DOMAIN, service, params)
@callback
def async_update_state(self, new_state):
@@ -131,10 +131,10 @@ def set_state(self, value):
return
params = {ATTR_ENTITY_ID: self.entity_id}
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
- self.call_service(self._domain, service, params)
+ self.async_call_service(self._domain, service, params)
if self.activate_only:
- call_later(self.hass, 1, self.reset_switch)
+ async_call_later(self.hass, 1, self.reset_switch)
@callback
def async_update_state(self, new_state):
@@ -169,7 +169,9 @@ def set_state(self, value):
sup_return_home = features & SUPPORT_RETURN_HOME
service = SERVICE_RETURN_TO_BASE if sup_return_home else SERVICE_TURN_OFF
- self.call_service(VACUUM_DOMAIN, service, {ATTR_ENTITY_ID: self.entity_id})
+ self.async_call_service(
+ VACUUM_DOMAIN, service, {ATTR_ENTITY_ID: self.entity_id}
+ )
@callback
def async_update_state(self, new_state):
@@ -209,7 +211,7 @@ def set_state(self, value):
self.char_in_use.set_value(value)
params = {ATTR_ENTITY_ID: self.entity_id}
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
- self.call_service(DOMAIN, service, params)
+ self.async_call_service(DOMAIN, service, params)
@callback
def async_update_state(self, new_state):
diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py
index 54e2e9f92a8616..fb3063704c2b15 100644
--- a/homeassistant/components/homekit/type_thermostats.py
+++ b/homeassistant/components/homekit/type_thermostats.py
@@ -253,42 +253,44 @@ def _set_chars(self, char_values):
hvac_mode = state.state
homekit_hvac_mode = HC_HASS_TO_HOMEKIT[hvac_mode]
- if CHAR_TARGET_HEATING_COOLING in char_values:
- # Homekit will reset the mode when VIEWING the temp
- # Ignore it if its the same mode
- if char_values[CHAR_TARGET_HEATING_COOLING] != homekit_hvac_mode:
- target_hc = char_values[CHAR_TARGET_HEATING_COOLING]
- if target_hc not in self.hc_homekit_to_hass:
- # If the target heating cooling state we want does not
- # exist on the device, we have to sort it out
- # based on the the current and target temperature since
- # siri will always send HC_HEAT_COOL_AUTO in this case
- # and hope for the best.
- hc_target_temp = char_values.get(CHAR_TARGET_TEMPERATURE)
- hc_current_temp = _get_current_temperature(state, self._unit)
- hc_fallback_order = HC_HEAT_COOL_PREFER_HEAT
- if (
- hc_target_temp is not None
- and hc_current_temp is not None
- and hc_target_temp < hc_current_temp
- ):
- hc_fallback_order = HC_HEAT_COOL_PREFER_COOL
- for hc_fallback in hc_fallback_order:
- if hc_fallback in self.hc_homekit_to_hass:
- _LOGGER.debug(
- "Siri requested target mode: %s and the device does not support, falling back to %s",
- target_hc,
- hc_fallback,
- )
- target_hc = hc_fallback
- break
-
- service = SERVICE_SET_HVAC_MODE_THERMOSTAT
- hass_value = self.hc_homekit_to_hass[target_hc]
- params = {ATTR_HVAC_MODE: hass_value}
- events.append(
- f"{CHAR_TARGET_HEATING_COOLING} to {char_values[CHAR_TARGET_HEATING_COOLING]}"
- )
+ # Homekit will reset the mode when VIEWING the temp
+ # Ignore it if its the same mode
+ if (
+ CHAR_TARGET_HEATING_COOLING in char_values
+ and char_values[CHAR_TARGET_HEATING_COOLING] != homekit_hvac_mode
+ ):
+ target_hc = char_values[CHAR_TARGET_HEATING_COOLING]
+ if target_hc not in self.hc_homekit_to_hass:
+ # If the target heating cooling state we want does not
+ # exist on the device, we have to sort it out
+ # based on the the current and target temperature since
+ # siri will always send HC_HEAT_COOL_AUTO in this case
+ # and hope for the best.
+ hc_target_temp = char_values.get(CHAR_TARGET_TEMPERATURE)
+ hc_current_temp = _get_current_temperature(state, self._unit)
+ hc_fallback_order = HC_HEAT_COOL_PREFER_HEAT
+ if (
+ hc_target_temp is not None
+ and hc_current_temp is not None
+ and hc_target_temp < hc_current_temp
+ ):
+ hc_fallback_order = HC_HEAT_COOL_PREFER_COOL
+ for hc_fallback in hc_fallback_order:
+ if hc_fallback in self.hc_homekit_to_hass:
+ _LOGGER.debug(
+ "Siri requested target mode: %s and the device does not support, falling back to %s",
+ target_hc,
+ hc_fallback,
+ )
+ target_hc = hc_fallback
+ break
+
+ service = SERVICE_SET_HVAC_MODE_THERMOSTAT
+ hass_value = self.hc_homekit_to_hass[target_hc]
+ params = {ATTR_HVAC_MODE: hass_value}
+ events.append(
+ f"{CHAR_TARGET_HEATING_COOLING} to {char_values[CHAR_TARGET_HEATING_COOLING]}"
+ )
if CHAR_TARGET_TEMPERATURE in char_values:
hc_target_temp = char_values[CHAR_TARGET_TEMPERATURE]
@@ -356,7 +358,7 @@ def _set_chars(self, char_values):
if service:
params[ATTR_ENTITY_ID] = self.entity_id
- self.call_service(
+ self.async_call_service(
DOMAIN_CLIMATE,
service,
params,
@@ -407,7 +409,7 @@ def set_target_humidity(self, value):
"""Set target humidity to value if call came from HomeKit."""
_LOGGER.debug("%s: Set target humidity to %d", self.entity_id, value)
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HUMIDITY: value}
- self.call_service(
+ self.async_call_service(
DOMAIN_CLIMATE, SERVICE_SET_HUMIDITY, params, f"{value}{PERCENTAGE}"
)
@@ -462,23 +464,26 @@ def _async_update_state(self, new_state):
# Update current temperature
current_temp = _get_current_temperature(new_state, self._unit)
- if current_temp is not None:
- if self.char_current_temp.value != current_temp:
- self.char_current_temp.set_value(current_temp)
+ if current_temp is not None and self.char_current_temp.value != current_temp:
+ self.char_current_temp.set_value(current_temp)
# Update current humidity
if CHAR_CURRENT_HUMIDITY in self.chars:
current_humdity = new_state.attributes.get(ATTR_CURRENT_HUMIDITY)
- if isinstance(current_humdity, (int, float)):
- if self.char_current_humidity.value != current_humdity:
- self.char_current_humidity.set_value(current_humdity)
+ if (
+ isinstance(current_humdity, (int, float))
+ and self.char_current_humidity.value != current_humdity
+ ):
+ self.char_current_humidity.set_value(current_humdity)
# Update target humidity
if CHAR_TARGET_HUMIDITY in self.chars:
target_humdity = new_state.attributes.get(ATTR_HUMIDITY)
- if isinstance(target_humdity, (int, float)):
- if self.char_target_humidity.value != target_humdity:
- self.char_target_humidity.set_value(target_humdity)
+ if (
+ isinstance(target_humdity, (int, float))
+ and self.char_target_humidity.value != target_humdity
+ ):
+ self.char_target_humidity.set_value(target_humdity)
# Update cooling threshold temperature if characteristic exists
if self.char_cooling_thresh_temp:
@@ -575,16 +580,15 @@ def set_heat_cool(self, value):
"""Change operation mode to value if call came from HomeKit."""
_LOGGER.debug("%s: Set heat-cool to %d", self.entity_id, value)
hass_value = HC_HOMEKIT_TO_HASS[value]
- if hass_value != HVAC_MODE_HEAT:
- if self.char_target_heat_cool.value != 1:
- self.char_target_heat_cool.set_value(1) # Heat
+ if hass_value != HVAC_MODE_HEAT and self.char_target_heat_cool.value != 1:
+ self.char_target_heat_cool.set_value(1) # Heat
def set_target_temperature(self, value):
"""Set target temperature to value if call came from HomeKit."""
_LOGGER.debug("%s: Set target temperature to %.1f°C", self.entity_id, value)
temperature = temperature_to_states(value, self._unit)
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_TEMPERATURE: temperature}
- self.call_service(
+ self.async_call_service(
DOMAIN_WATER_HEATER,
SERVICE_SET_TEMPERATURE_WATER_HEATER,
params,
@@ -595,10 +599,19 @@ def set_target_temperature(self, value):
def async_update_state(self, new_state):
"""Update water_heater state after state change."""
# Update current and target temperature
- temperature = _get_target_temperature(new_state, self._unit)
- if temperature is not None:
- if temperature != self.char_current_temp.value:
- self.char_target_temp.set_value(temperature)
+ target_temperature = _get_target_temperature(new_state, self._unit)
+ if (
+ target_temperature is not None
+ and target_temperature != self.char_target_temp.value
+ ):
+ self.char_target_temp.set_value(target_temperature)
+
+ current_temperature = _get_current_temperature(new_state, self._unit)
+ if (
+ current_temperature is not None
+ and current_temperature != self.char_current_temp.value
+ ):
+ self.char_current_temp.set_value(current_temperature)
# Update display units
if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT:
diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py
index 98374b73f40a29..a746355e124058 100644
--- a/homeassistant/components/homekit/util.py
+++ b/homeassistant/components/homekit/util.py
@@ -11,10 +11,19 @@
import voluptuous as vol
from homeassistant.components import binary_sensor, media_player, sensor
+from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
+from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
+from homeassistant.components.media_player import (
+ DEVICE_CLASS_TV,
+ DOMAIN as MEDIA_PLAYER_DOMAIN,
+)
+from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN, SUPPORT_ACTIVITY
from homeassistant.const import (
ATTR_CODE,
+ ATTR_DEVICE_CLASS,
ATTR_SUPPORTED_FEATURES,
CONF_NAME,
+ CONF_PORT,
CONF_TYPE,
TEMP_CELSIUS,
)
@@ -65,7 +74,6 @@
FEATURE_PLAY_PAUSE,
FEATURE_PLAY_STOP,
FEATURE_TOGGLE_MUTE,
- HOMEKIT_FILE,
HOMEKIT_PAIRING_QR,
HOMEKIT_PAIRING_QR_SECRET,
TYPE_FAUCET,
@@ -328,9 +336,7 @@ def show_setup_message(hass, entry_id, bridge_name, pincode, uri):
f"### {pin}\n"
f""
)
- hass.components.persistent_notification.create(
- message, "HomeKit Bridge Setup", entry_id
- )
+ hass.components.persistent_notification.create(message, "HomeKit Pairing", entry_id)
def dismiss_setup_message(hass, entry_id):
@@ -409,24 +415,6 @@ def format_sw_version(version):
return None
-def migrate_filesystem_state_data_for_primary_imported_entry_id(
- hass: HomeAssistant, entry_id: str
-):
- """Migrate the old paths to the storage directory."""
- legacy_persist_file_path = hass.config.path(HOMEKIT_FILE)
- if os.path.exists(legacy_persist_file_path):
- os.rename(
- legacy_persist_file_path, get_persist_fullpath_for_entry_id(hass, entry_id)
- )
-
- legacy_aid_storage_path = hass.config.path(STORAGE_DIR, "homekit.aids")
- if os.path.exists(legacy_aid_storage_path):
- os.rename(
- legacy_aid_storage_path,
- get_aid_storage_fullpath_for_entry_id(hass, entry_id),
- )
-
-
def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str):
"""Remove the state files from disk."""
persist_file_path = get_persist_fullpath_for_entry_id(hass, entry_id)
@@ -445,7 +433,7 @@ def _get_test_socket():
return test_socket
-def port_is_available(port: int):
+def port_is_available(port: int) -> bool:
"""Check to see if a port is available."""
test_socket = _get_test_socket()
try:
@@ -456,10 +444,25 @@ def port_is_available(port: int):
return True
-def find_next_available_port(start_port: int):
+async def async_find_next_available_port(hass: HomeAssistant, start_port: int) -> int:
+ """Find the next available port not assigned to a config entry."""
+ exclude_ports = {
+ entry.data[CONF_PORT]
+ for entry in hass.config_entries.async_entries(DOMAIN)
+ if CONF_PORT in entry.data
+ }
+
+ return await hass.async_add_executor_job(
+ _find_next_available_port, start_port, exclude_ports
+ )
+
+
+def _find_next_available_port(start_port: int, exclude_ports: set) -> int:
"""Find the next available port starting with the given port."""
test_socket = _get_test_socket()
for port in range(start_port, MAX_PORT):
+ if port in exclude_ports:
+ continue
try:
test_socket.bind(("", port))
return port
@@ -469,7 +472,7 @@ def find_next_available_port(start_port: int):
continue
-def pid_is_alive(pid):
+def pid_is_alive(pid) -> bool:
"""Check to see if a process is alive."""
try:
os.kill(pid, 0)
@@ -477,3 +480,32 @@ def pid_is_alive(pid):
except OSError:
pass
return False
+
+
+def accessory_friendly_name(hass_name, accessory):
+ """Return the combined name for the accessory.
+
+ The mDNS name and the Home Assistant config entry
+ name are usually different which means they need to
+ see both to identify the accessory.
+ """
+ accessory_mdns_name = accessory.display_name
+ if hass_name.casefold().startswith(accessory_mdns_name.casefold()):
+ return hass_name
+ if accessory_mdns_name.casefold().startswith(hass_name.casefold()):
+ return accessory_mdns_name
+ return f"{hass_name} ({accessory_mdns_name})"
+
+
+def state_needs_accessory_mode(state):
+ """Return if the entity represented by the state must be paired in accessory mode."""
+ if state.domain == CAMERA_DOMAIN:
+ return True
+
+ return (
+ state.domain == LOCK_DOMAIN
+ or state.domain == MEDIA_PLAYER_DOMAIN
+ and state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TV
+ or state.domain == REMOTE_DOMAIN
+ and state.attributes.get(ATTR_SUPPORTED_FEATURES) & SUPPORT_ACTIVITY
+ )
diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py
index 0a8f376fb3387b..d7b28036426d8e 100644
--- a/homeassistant/components/homekit_controller/__init__.py
+++ b/homeassistant/components/homekit_controller/__init__.py
@@ -1,5 +1,7 @@
"""Support for Homekit device discovery."""
-from typing import Any, Dict
+from __future__ import annotations
+
+from typing import Any
import aiohomekit
from aiohomekit.model import Accessory
@@ -77,7 +79,7 @@ async def async_will_remove_from_hass(self):
signal_remove()
self._signals.clear()
- async def async_put_characteristics(self, characteristics: Dict[str, Any]):
+ async def async_put_characteristics(self, characteristics: dict[str, Any]):
"""
Write characteristics to the device.
diff --git a/homeassistant/components/homekit_controller/air_quality.py b/homeassistant/components/homekit_controller/air_quality.py
index 896034a2ca02b8..2a162eb2b2a370 100644
--- a/homeassistant/components/homekit_controller/air_quality.py
+++ b/homeassistant/components/homekit_controller/air_quality.py
@@ -69,7 +69,7 @@ def volatile_organic_compounds(self):
return self.service.value(CharacteristicsTypes.DENSITY_VOC)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
data = {"air_quality_text": self.air_quality_text}
diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py
index 621fb01ff74a3a..c40252c9fdc4bb 100644
--- a/homeassistant/components/homekit_controller/alarm_control_panel.py
+++ b/homeassistant/components/homekit_controller/alarm_control_panel.py
@@ -105,7 +105,7 @@ async def set_alarm_state(self, state, code=None):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the optional state attributes."""
battery_level = self.service.value(CharacteristicsTypes.BATTERY_LEVEL)
diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py
index cb0feb6ba771d1..2c251d41fb3989 100644
--- a/homeassistant/components/homekit_controller/climate.py
+++ b/homeassistant/components/homekit_controller/climate.py
@@ -132,8 +132,8 @@ async def async_set_temperature(self, **kwargs):
else:
hvac_mode = TARGET_HEATER_COOLER_STATE_HOMEKIT_TO_HASS.get(state)
_LOGGER.warning(
- "HomeKit device %s: Setting temperature in %s mode is not supported yet."
- " Consider raising a ticket if you have this device and want to help us implement this feature.",
+ "HomeKit device %s: Setting temperature in %s mode is not supported yet;"
+ " Consider raising a ticket if you have this device and want to help us implement this feature",
self.entity_id,
hvac_mode,
)
@@ -147,8 +147,8 @@ async def async_set_hvac_mode(self, hvac_mode):
return
if hvac_mode not in {HVAC_MODE_HEAT, HVAC_MODE_COOL}:
_LOGGER.warning(
- "HomeKit device %s: Setting temperature in %s mode is not supported yet."
- " Consider raising a ticket if you have this device and want to help us implement this feature.",
+ "HomeKit device %s: Setting temperature in %s mode is not supported yet;"
+ " Consider raising a ticket if you have this device and want to help us implement this feature",
self.entity_id,
hvac_mode,
)
diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py
index e046a131a6bfb5..fcf83918fda071 100644
--- a/homeassistant/components/homekit_controller/config_flow.py
+++ b/homeassistant/components/homekit_controller/config_flow.py
@@ -18,8 +18,6 @@
HOMEKIT_DIR = ".homekit"
HOMEKIT_BRIDGE_DOMAIN = "homekit"
-HOMEKIT_BRIDGE_SERIAL_NUMBER = "homekit.bridge"
-HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge"
HOMEKIT_IGNORE = [
# eufy Indoor Cam 2K and 2K Pan & Tilt
@@ -181,8 +179,8 @@ async def async_step_unignore(self, user_input):
return self.async_abort(reason="no_devices")
- async def _hkid_is_homekit_bridge(self, hkid):
- """Determine if the device is a homekit bridge."""
+ async def _hkid_is_homekit(self, hkid):
+ """Determine if the device is a homekit bridge or accessory."""
dev_reg = await async_get_device_registry(self.hass)
device = dev_reg.async_get_device(
identifiers=set(), connections={(CONNECTION_NETWORK_MAC, hkid)}
@@ -190,7 +188,13 @@ async def _hkid_is_homekit_bridge(self, hkid):
if device is None:
return False
- return device.model == HOMEKIT_BRIDGE_MODEL
+
+ for entry_id in device.config_entries:
+ entry = self.hass.config_entries.async_get_entry(entry_id)
+ if entry and entry.domain == HOMEKIT_BRIDGE_DOMAIN:
+ return True
+
+ return False
async def async_step_zeroconf(self, discovery_info):
"""Handle a discovered HomeKit accessory.
@@ -253,7 +257,6 @@ async def async_step_zeroconf(self, discovery_info):
await self.async_set_unique_id(normalize_hkid(hkid))
self._abort_if_unique_id_configured()
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["hkid"] = hkid
if paired:
@@ -267,8 +270,8 @@ async def async_step_zeroconf(self, discovery_info):
if model in HOMEKIT_IGNORE:
return self.async_abort(reason="ignored_model")
- # If this is a HomeKit bridge exported by *this* HA instance ignore it.
- if await self._hkid_is_homekit_bridge(hkid):
+ # If this is a HomeKit bridge/accessory exported by *this* HA instance ignore it.
+ if await self._hkid_is_homekit(hkid):
return self.async_abort(reason="ignored_model")
self.name = name
@@ -392,7 +395,6 @@ async def async_step_protocol_error(self, user_input=None):
@callback
def _async_step_pair_show_form(self, errors=None):
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
placeholders = {"name": self.name}
self.context["title_placeholders"] = {"name": self.name}
diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py
index 6c945c8111524a..dd25e32b3c4605 100644
--- a/homeassistant/components/homekit_controller/cover.py
+++ b/homeassistant/components/homekit_controller/cover.py
@@ -108,7 +108,7 @@ async def set_door_state(self, state):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the optional state attributes."""
obstruction_detected = self.service.value(
CharacteristicsTypes.OBSTRUCTION_DETECTED
@@ -235,7 +235,7 @@ async def async_set_cover_tilt_position(self, **kwargs):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the optional state attributes."""
obstruction_detected = self.service.value(
CharacteristicsTypes.OBSTRUCTION_DETECTED
diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py
index b2e668915d7188..c9cd771edf6029 100644
--- a/homeassistant/components/homekit_controller/device_trigger.py
+++ b/homeassistant/components/homekit_controller/device_trigger.py
@@ -1,5 +1,5 @@
"""Provides device automations for homekit devices."""
-from typing import List
+from __future__ import annotations
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.characteristics.const import InputEventValues
@@ -75,11 +75,14 @@ async def async_attach_trigger(
automation_info: dict,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
def event_handler(char):
if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]:
return
- self._hass.async_create_task(action({"trigger": config}))
+ self._hass.async_create_task(
+ action({"trigger": {**config, "id": trigger_id}})
+ )
trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]]
iid = trigger["characteristic"]
@@ -99,9 +102,11 @@ def enumerate_stateless_switch(service):
# A stateless switch that has a SERVICE_LABEL_INDEX is part of a group
# And is handled separately
- if service.has(CharacteristicsTypes.SERVICE_LABEL_INDEX):
- if len(service.linked) > 0:
- return []
+ if (
+ service.has(CharacteristicsTypes.SERVICE_LABEL_INDEX)
+ and len(service.linked) > 0
+ ):
+ return []
char = service[CharacteristicsTypes.INPUT_EVENT]
@@ -109,17 +114,15 @@ def enumerate_stateless_switch(service):
# manufacturer might not - clamp options to what they say.
all_values = clamp_enum_to_char(InputEventValues, char)
- results = []
- for event_type in all_values:
- results.append(
- {
- "characteristic": char.iid,
- "value": event_type,
- "type": "button1",
- "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type],
- }
- )
- return results
+ return [
+ {
+ "characteristic": char.iid,
+ "value": event_type,
+ "type": "button1",
+ "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type],
+ }
+ for event_type in all_values
+ ]
def enumerate_stateless_switch_group(service):
@@ -226,7 +229,7 @@ def async_fire_triggers(conn, events):
source.fire(iid, ev)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers for homekit devices."""
if device_id not in hass.data.get(TRIGGERS, {}):
@@ -234,20 +237,16 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
device = hass.data[TRIGGERS][device_id]
- triggers = []
-
- for trigger, subtype in device.async_get_triggers():
- triggers.append(
- {
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_TYPE: trigger,
- CONF_SUBTYPE: subtype,
- }
- )
-
- return triggers
+ return [
+ {
+ CONF_PLATFORM: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_TYPE: trigger,
+ CONF_SUBTYPE: subtype,
+ }
+ for trigger, subtype in device.async_get_triggers()
+ ]
async def async_attach_trigger(
@@ -257,8 +256,6 @@ async def async_attach_trigger(
automation_info: dict,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
- config = TRIGGER_SCHEMA(config)
-
device_id = config[CONF_DEVICE_ID]
device = hass.data[TRIGGERS][device_id]
return await device.async_attach_trigger(config, action, automation_info)
diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py
index e2cdf7b3cfdea8..591050f5fd9c17 100644
--- a/homeassistant/components/homekit_controller/fan.py
+++ b/homeassistant/components/homekit_controller/fan.py
@@ -80,6 +80,13 @@ def supported_features(self):
return features
+ @property
+ def speed_count(self):
+ """Speed count for the fan."""
+ return round(
+ 100 / max(1, self.service[CharacteristicsTypes.ROTATION_SPEED].minStep or 0)
+ )
+
async def async_set_direction(self, direction):
"""Set the direction of the fan."""
await self.async_put_characteristics(
@@ -110,7 +117,7 @@ async def async_turn_on(
if not self.is_on:
characteristics[self.on_characteristic] = True
- if self.supported_features & SUPPORT_SET_SPEED:
+ if percentage is not None and self.supported_features & SUPPORT_SET_SPEED:
characteristics[CharacteristicsTypes.ROTATION_SPEED] = percentage
if characteristics:
diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py
index e4bed25d618fb6..227174d00e98d7 100644
--- a/homeassistant/components/homekit_controller/humidifier.py
+++ b/homeassistant/components/homekit_controller/humidifier.py
@@ -1,5 +1,5 @@
"""Support for HomeKit Controller humidifier."""
-from typing import List, Optional
+from __future__ import annotations
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import ServicesTypes
@@ -69,14 +69,14 @@ async def async_turn_off(self, **kwargs):
await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False})
@property
- def target_humidity(self) -> Optional[int]:
+ def target_humidity(self) -> int | None:
"""Return the humidity we try to reach."""
return self.service.value(
CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD
)
@property
- def mode(self) -> Optional[str]:
+ def mode(self) -> str | None:
"""Return the current mode, e.g., home, auto, baby.
Requires SUPPORT_MODES.
@@ -87,7 +87,7 @@ def mode(self) -> Optional[str]:
return MODE_AUTO if mode == 1 else MODE_NORMAL
@property
- def available_modes(self) -> Optional[List[str]]:
+ def available_modes(self) -> list[str] | None:
"""Return a list of available modes.
Requires SUPPORT_MODES.
@@ -175,14 +175,14 @@ async def async_turn_off(self, **kwargs):
await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False})
@property
- def target_humidity(self) -> Optional[int]:
+ def target_humidity(self) -> int | None:
"""Return the humidity we try to reach."""
return self.service.value(
CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD
)
@property
- def mode(self) -> Optional[str]:
+ def mode(self) -> str | None:
"""Return the current mode, e.g., home, auto, baby.
Requires SUPPORT_MODES.
@@ -193,7 +193,7 @@ def mode(self) -> Optional[str]:
return MODE_AUTO if mode == 1 else MODE_NORMAL
@property
- def available_modes(self) -> Optional[List[str]]:
+ def available_modes(self) -> list[str] | None:
"""Return a list of available modes.
Requires SUPPORT_MODES.
diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py
index 8ac7fd608fd414..09c02ce0ff97b0 100644
--- a/homeassistant/components/homekit_controller/lock.py
+++ b/homeassistant/components/homekit_controller/lock.py
@@ -63,7 +63,7 @@ async def _set_lock_state(self, state):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the optional state attributes."""
attributes = {}
diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py
index 6dfa8720ee5c69..71bde5f0af9b16 100644
--- a/homeassistant/components/homekit_controller/media_player.py
+++ b/homeassistant/components/homekit_controller/media_player.py
@@ -93,9 +93,11 @@ def supported_features(self):
if TargetMediaStateValues.STOP in self.supported_media_states:
features |= SUPPORT_STOP
- if self.service.has(CharacteristicsTypes.REMOTE_KEY):
- if RemoteKeyValues.PLAY_PAUSE in self.supported_remote_keys:
- features |= SUPPORT_PAUSE | SUPPORT_PLAY
+ if (
+ self.service.has(CharacteristicsTypes.REMOTE_KEY)
+ and RemoteKeyValues.PLAY_PAUSE in self.supported_remote_keys
+ ):
+ features |= SUPPORT_PAUSE | SUPPORT_PLAY
return features
diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py
index 094d0a500d1a98..2ae264fabb92cd 100644
--- a/homeassistant/components/homekit_controller/sensor.py
+++ b/homeassistant/components/homekit_controller/sensor.py
@@ -2,6 +2,7 @@
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import ServicesTypes
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
DEVICE_CLASS_BATTERY,
@@ -39,7 +40,7 @@
}
-class HomeKitHumiditySensor(HomeKitEntity):
+class HomeKitHumiditySensor(HomeKitEntity, SensorEntity):
"""Representation of a Homekit humidity sensor."""
def get_characteristic_types(self):
@@ -72,7 +73,7 @@ def state(self):
return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT)
-class HomeKitTemperatureSensor(HomeKitEntity):
+class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity):
"""Representation of a Homekit temperature sensor."""
def get_characteristic_types(self):
@@ -105,7 +106,7 @@ def state(self):
return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT)
-class HomeKitLightSensor(HomeKitEntity):
+class HomeKitLightSensor(HomeKitEntity, SensorEntity):
"""Representation of a Homekit light level sensor."""
def get_characteristic_types(self):
@@ -138,7 +139,7 @@ def state(self):
return self.service.value(CharacteristicsTypes.LIGHT_LEVEL_CURRENT)
-class HomeKitCarbonDioxideSensor(HomeKitEntity):
+class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity):
"""Representation of a Homekit Carbon Dioxide sensor."""
def get_characteristic_types(self):
@@ -166,7 +167,7 @@ def state(self):
return self.service.value(CharacteristicsTypes.CARBON_DIOXIDE_LEVEL)
-class HomeKitBatterySensor(HomeKitEntity):
+class HomeKitBatterySensor(HomeKitEntity, SensorEntity):
"""Representation of a Homekit battery sensor."""
def get_characteristic_types(self):
@@ -233,7 +234,7 @@ def state(self):
return self.service.value(CharacteristicsTypes.BATTERY_LEVEL)
-class SimpleSensor(CharacteristicEntity):
+class SimpleSensor(CharacteristicEntity, SensorEntity):
"""
A simple sensor for a single characteristic.
diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py
index b9d0b273cb1ba8..36ed379bc805b4 100644
--- a/homeassistant/components/homekit_controller/switch.py
+++ b/homeassistant/components/homekit_controller/switch.py
@@ -39,7 +39,7 @@ async def async_turn_off(self, **kwargs):
await self.async_put_characteristics({CharacteristicsTypes.ON: False})
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the optional state attributes."""
outlet_in_use = self.service.value(CharacteristicsTypes.OUTLET_IN_USE)
if outlet_in_use is not None:
@@ -77,7 +77,7 @@ def is_on(self):
return self.service.value(CharacteristicsTypes.ACTIVE)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the optional state attributes."""
attrs = {}
diff --git a/homeassistant/components/homekit_controller/translations/bg.json b/homeassistant/components/homekit_controller/translations/bg.json
index 8e2762f9f32885..014398897342a7 100644
--- a/homeassistant/components/homekit_controller/translations/bg.json
+++ b/homeassistant/components/homekit_controller/translations/bg.json
@@ -34,5 +34,19 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "\u0411\u0443\u0442\u043e\u043d 1",
+ "button10": "\u0411\u0443\u0442\u043e\u043d 10",
+ "button2": "\u0411\u0443\u0442\u043e\u043d 2",
+ "button3": "\u0411\u0443\u0442\u043e\u043d 3",
+ "button4": "\u0411\u0443\u0442\u043e\u043d 4",
+ "button5": "\u0411\u0443\u0442\u043e\u043d 5",
+ "button6": "\u0411\u0443\u0442\u043e\u043d 6",
+ "button7": "\u0411\u0443\u0442\u043e\u043d 7",
+ "button8": "\u0411\u0443\u0442\u043e\u043d 8",
+ "button9": "\u0411\u0443\u0442\u043e\u043d 9"
+ }
+ },
"title": "HomeKit \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e"
}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/cs.json b/homeassistant/components/homekit_controller/translations/cs.json
index 9a2159eda0599d..d5f7a50292118b 100644
--- a/homeassistant/components/homekit_controller/translations/cs.json
+++ b/homeassistant/components/homekit_controller/translations/cs.json
@@ -62,9 +62,9 @@
"doorbell": "Zvonek"
},
"trigger_type": {
- "double_press": "Dvakr\u00e1t stisknuto \"{subtype}\"",
- "long_press": "Stisknuto a podr\u017eeno \"{subtype}\"",
- "single_press": "Stisknuto \"{subtype}\""
+ "double_press": "\"{subtype}\" stisknuto dvakr\u00e1t",
+ "long_press": "\"{subtype}\" stisknuto a podr\u017eeno",
+ "single_press": "\"{subtype}\" stisknuto"
}
},
"title": "HomeKit ovlada\u010d"
diff --git a/homeassistant/components/homekit_controller/translations/de.json b/homeassistant/components/homekit_controller/translations/de.json
index 3d5c538b62b135..49586a23634b05 100644
--- a/homeassistant/components/homekit_controller/translations/de.json
+++ b/homeassistant/components/homekit_controller/translations/de.json
@@ -3,7 +3,7 @@
"abort": {
"accessory_not_found_error": "Die Kopplung kann nicht durchgef\u00fchrt werden, da das Ger\u00e4t nicht mehr gefunden werden kann.",
"already_configured": "Das Zubeh\u00f6r ist mit diesem Controller bereits konfiguriert.",
- "already_in_progress": "Der Konfigurationsablauf f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.",
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
"already_paired": "Dieses Zubeh\u00f6r ist bereits mit einem anderen Ger\u00e4t gekoppelt. Setze das Zubeh\u00f6r zur\u00fcck und versuche es erneut.",
"ignored_model": "Die Unterst\u00fctzung von HomeKit f\u00fcr dieses Modell ist blockiert, da eine vollst\u00e4ndige native Integration verf\u00fcgbar ist.",
"invalid_config_entry": "Dieses Ger\u00e4t wird als bereit zum Koppeln angezeigt, es gibt jedoch bereits einen widerspr\u00fcchlichen Konfigurationseintrag in Home Assistant, der zuerst entfernt werden muss.",
@@ -18,6 +18,12 @@
},
"flow_title": "HomeKit-Zubeh\u00f6r: {name}",
"step": {
+ "busy_error": {
+ "title": "Das Ger\u00e4t wird bereits mit einem anderen Controller gekoppelt"
+ },
+ "max_tries_error": {
+ "title": "Maximale Authentifizierungsversuche \u00fcberschritten"
+ },
"pair": {
"data": {
"pairing_code": "Kopplungscode"
@@ -25,12 +31,15 @@
"description": "Gib deinen HomeKit-Kopplungscode ein, um dieses Zubeh\u00f6r zu verwenden",
"title": "Mit HomeKit Zubeh\u00f6r koppeln"
},
+ "protocol_error": {
+ "title": "Fehler bei der Kommunikation mit dem Zubeh\u00f6r"
+ },
"user": {
"data": {
"device": "Ger\u00e4t"
},
"description": "W\u00e4hle das Ger\u00e4t aus, mit dem du die Kopplung herstellen m\u00f6chtest",
- "title": "Mit HomeKit Zubeh\u00f6r koppeln"
+ "title": "Ger\u00e4teauswahl"
}
}
},
diff --git a/homeassistant/components/homekit_controller/translations/et.json b/homeassistant/components/homekit_controller/translations/et.json
index 40537554ee2a69..6df49751478c2c 100644
--- a/homeassistant/components/homekit_controller/translations/et.json
+++ b/homeassistant/components/homekit_controller/translations/et.json
@@ -13,7 +13,7 @@
"error": {
"authentication_error": "Vale HomeKiti kood. Kontrolli seda ja proovi uuesti.",
"max_peers_error": "Seade keeldus sidumist lisamast kuna puudub piisav salvestusruum.",
- "pairing_failed": "Selle seadmega sidumise katsel ilmnes tundmatu t\u00f5rge. See v\u00f5ib olla ajutine t\u00f5rge v\u00f5i tseadet ei toetata praegu.",
+ "pairing_failed": "Selle seadmega sidumise katsel ilmnes tundmatu t\u00f5rge. See v\u00f5ib olla ajutine t\u00f5rge v\u00f5i seadet ei toetata praegu.",
"unable_to_pair": "Ei saa siduda, proovi uuesti.",
"unknown_error": "Seade teatas tundmatust t\u00f5rkest. Sidumine nurjus."
},
diff --git a/homeassistant/components/homekit_controller/translations/hu.json b/homeassistant/components/homekit_controller/translations/hu.json
index 49e6fc53231e04..90e2405ed64f8a 100644
--- a/homeassistant/components/homekit_controller/translations/hu.json
+++ b/homeassistant/components/homekit_controller/translations/hu.json
@@ -3,7 +3,7 @@
"abort": {
"accessory_not_found_error": "Nem adhat\u00f3 hozz\u00e1 p\u00e1ros\u00edt\u00e1s, mert az eszk\u00f6z m\u00e1r nem tal\u00e1lhat\u00f3.",
"already_configured": "A tartoz\u00e9k m\u00e1r konfigur\u00e1lva van ezzel a vez\u00e9rl\u0151vel.",
- "already_in_progress": "Az eszk\u00f6z konfigur\u00e1ci\u00f3ja m\u00e1r folyamatban van.",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
"already_paired": "Ez a tartoz\u00e9k m\u00e1r p\u00e1ros\u00edtva van egy m\u00e1sik eszk\u00f6zzel. \u00c1ll\u00edtsa alaphelyzetbe a tartoz\u00e9kot, majd pr\u00f3b\u00e1lkozzon \u00fajra.",
"ignored_model": "A HomeKit t\u00e1mogat\u00e1sa e modelln\u00e9l blokkolva van, mivel a szolg\u00e1ltat\u00e1shoz teljes nat\u00edv integr\u00e1ci\u00f3 \u00e9rhet\u0151 el.",
"invalid_config_entry": "Ez az eszk\u00f6z k\u00e9szen \u00e1ll a p\u00e1ros\u00edt\u00e1sra, de m\u00e1r van egy \u00fctk\u00f6z\u0151 konfigur\u00e1ci\u00f3s bejegyz\u00e9s a Home Assistant-ben, amelyet el\u0151sz\u00f6r el kell t\u00e1vol\u00edtani.",
@@ -30,9 +30,29 @@
"device": "Eszk\u00f6z"
},
"description": "V\u00e1lassza ki azt az eszk\u00f6zt, amelyet p\u00e1ros\u00edtani szeretne",
- "title": "HomeKit tartoz\u00e9k p\u00e1ros\u00edt\u00e1sa"
+ "title": "Eszk\u00f6z kiv\u00e1laszt\u00e1sa"
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "Gomb 1",
+ "button10": "Gomb 10",
+ "button2": "Gomb 2",
+ "button3": "Gomb 3",
+ "button4": "Gomb 4",
+ "button5": "Gomb 5",
+ "button6": "Gomb 6",
+ "button7": "Gomb 7",
+ "button8": "Gomb 8",
+ "button9": "Gomb 9",
+ "doorbell": "Cseng\u0151"
+ },
+ "trigger_type": {
+ "double_press": "\"{subtype}\" k\u00e9tszer lenyomva",
+ "long_press": "\"{subtype}\" lenyomva \u00e9s nyomva tartva",
+ "single_press": "\"{subtype}\" lenyomva"
+ }
+ },
"title": "HomeKit Vez\u00e9rl\u0151"
}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/id.json b/homeassistant/components/homekit_controller/translations/id.json
new file mode 100644
index 00000000000000..49a37d3b3fb683
--- /dev/null
+++ b/homeassistant/components/homekit_controller/translations/id.json
@@ -0,0 +1,71 @@
+{
+ "config": {
+ "abort": {
+ "accessory_not_found_error": "Tidak dapat menambahkan pemasangan karena perangkat tidak dapat ditemukan lagi.",
+ "already_configured": "Aksesori sudah dikonfigurasi dengan pengontrol ini.",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "already_paired": "Aksesori ini sudah dipasangkan ke perangkat lain. Setel ulang aksesori dan coba lagi.",
+ "ignored_model": "Dukungan HomeKit untuk model ini diblokir karena integrasi asli dengan fitur lebih lengkap telah tersedia.",
+ "invalid_config_entry": "Perangkat ini ditampilkan sebagai siap untuk dipasangkan tetapi sudah ada entri konfigurasi yang bertentangan untuk perangkat tersebut dalam Home Assistant, yang harus dihapus terlebih dulu.",
+ "invalid_properties": "Properti tidak valid diumumkan oleh perangkat.",
+ "no_devices": "Tidak ada perangkat yang belum dipasangkan yang dapat ditemukan"
+ },
+ "error": {
+ "authentication_error": "Kode HomeKit salah. Periksa dan coba lagi.",
+ "max_peers_error": "Perangkat menolak untuk menambahkan pemasangan karena tidak memiliki penyimpanan pemasangan yang tersedia.",
+ "pairing_failed": "Terjadi kesalahan yang tidak tertangani saat mencoba memasangkan dengan perangkat ini. Ini mungkin kegagalan sementara atau perangkat Anda mungkin tidak didukung saat ini.",
+ "unable_to_pair": "Gagal memasangkan, coba lagi.",
+ "unknown_error": "Perangkat melaporkan kesalahan yang tidak diketahui. Pemasangan gagal."
+ },
+ "flow_title": "{name} lewat HomeKit Accessory Protocol",
+ "step": {
+ "busy_error": {
+ "description": "Batalkan pemasangan di semua pengontrol, atau coba mulai ulang perangkat, lalu lanjutkan untuk melanjutkan pemasangan.",
+ "title": "Perangkat sudah dipasangkan dengan pengontrol lain"
+ },
+ "max_tries_error": {
+ "description": "Perangkat telah menerima lebih dari 100 upaya autentikasi yang gagal. Coba mulai ulang perangkat, lalu lanjutkan pemasangan.",
+ "title": "Upaya autentikasi maksimum terlampaui"
+ },
+ "pair": {
+ "data": {
+ "pairing_code": "Kode Pemasangan"
+ },
+ "description": "Pengontrol HomeKit berkomunikasi dengan {name} melalui jaringan area lokal menggunakan koneksi terenkripsi yang aman tanpa pengontrol HomeKit atau iCloud terpisah. Masukkan kode pemasangan HomeKit Anda (dalam format XXX-XX-XXX) untuk menggunakan aksesori ini. Kode ini biasanya ditemukan pada perangkat itu sendiri atau dalam kemasan.",
+ "title": "Pasangkan dengan perangkat melalui HomeKit Accessory Protocol"
+ },
+ "protocol_error": {
+ "description": "Perangkat mungkin tidak dalam mode pemasangan dan mungkin memerlukan tombol fisik atau virtual. Pastikan perangkat dalam mode pemasangan atau coba mulai ulang perangkat, lalu lanjutkan pemasangan.",
+ "title": "Terjadi kesalahan saat berkomunikasi dengan aksesori"
+ },
+ "user": {
+ "data": {
+ "device": "Perangkat"
+ },
+ "description": "Pengontrol HomeKit berkomunikasi melalui jaringan area lokal menggunakan koneksi terenkripsi yang aman tanpa pengontrol HomeKit atau iCloud terpisah. Pilih perangkat yang ingin Anda pasangkan:",
+ "title": "Pemilihan perangkat"
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "Tombol 1",
+ "button10": "Tombol 10",
+ "button2": "Tombol 2",
+ "button3": "Tombol 3",
+ "button4": "Tombol 4",
+ "button5": "Tombol 5",
+ "button6": "Tombol 6",
+ "button7": "Tombol 7",
+ "button8": "Tombol 8",
+ "button9": "Tombol 9",
+ "doorbell": "Bel pintu"
+ },
+ "trigger_type": {
+ "double_press": "\"{subtype}\" ditekan dua kali",
+ "long_press": "\"{subtype}\" ditekan dan ditahan",
+ "single_press": "\"{subtype}\" ditekan"
+ }
+ },
+ "title": "Pengontrol HomeKit"
+}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/ko.json b/homeassistant/components/homekit_controller/translations/ko.json
index 2c41447b5a0436..c28573ee6ac136 100644
--- a/homeassistant/components/homekit_controller/translations/ko.json
+++ b/homeassistant/components/homekit_controller/translations/ko.json
@@ -1,27 +1,27 @@
{
"config": {
"abort": {
- "accessory_not_found_error": "\uae30\uae30\ub97c \ub354 \uc774\uc0c1 \ucc3e\uc744 \uc218 \uc5c6\uc73c\ubbc0\ub85c \ud398\uc5b4\ub9c1\uc744 \ucd94\uac00 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
+ "accessory_not_found_error": "\uae30\uae30\ub97c \ub354 \uc774\uc0c1 \ucc3e\uc744 \uc218 \uc5c6\uc5b4 \ud398\uc5b4\ub9c1\uc744 \ucd94\uac00\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
"already_configured": "\uc561\uc138\uc11c\ub9ac\uac00 \ucee8\ud2b8\ub864\ub7ec\uc5d0 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
"already_paired": "\uc774 \uc561\uc138\uc11c\ub9ac\ub294 \uc774\ubbf8 \ub2e4\ub978 \uae30\uae30\uc640 \ud398\uc5b4\ub9c1\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc11c\ub9ac\ub97c \uc7ac\uc124\uc815\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
"ignored_model": "\uc774 \ubaa8\ub378\uc5d0 \ub300\ud55c HomeKit \uc9c0\uc6d0\uc740 \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc81c\uacf5\ud558\ub294 \uae30\ubcf8 \uad6c\uc131\uc694\uc18c\ub85c \uc778\ud574 \ucc28\ub2e8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "invalid_config_entry": "\uc774 \uae30\uae30\ub294 \ud398\uc5b4\ub9c1 \ud560 \uc900\ube44\uac00 \ub418\uc5c8\uc9c0\ub9cc Home Assistant \uc5d0 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \ucda9\ub3cc\ud558\ub294 \uad6c\uc131\uc694\uc18c\uac00 \uc788\uc2b5\ub2c8\ub2e4. \uba3c\uc800 \ud574\ub2f9 \uad6c\uc131\uc694\uc18c\ub97c \uc81c\uac70\ud574\uc8fc\uc138\uc694.",
- "invalid_properties": "\uc7a5\uce58\uc5d0\uc11c\uc120\uc5b8\ud55c \uc798\ubabb\ub41c \uc18d\uc131\uc785\ub2c8\ub2e4.",
+ "invalid_config_entry": "\uc774 \uae30\uae30\ub294 \ud398\uc5b4\ub9c1 \ud560 \uc900\ube44\uac00 \ub418\uc5c8\uc9c0\ub9cc Home Assistant\uc5d0 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \ucda9\ub3cc\ud558\ub294 \uad6c\uc131\uc694\uc18c\uac00 \uc788\uc2b5\ub2c8\ub2e4. \uba3c\uc800 \ud574\ub2f9 \uad6c\uc131\uc694\uc18c\ub97c \uc81c\uac70\ud574\uc8fc\uc138\uc694.",
+ "invalid_properties": "\uae30\uae30\uc5d0\uc11c \uc798\ubabb\ub41c \uc18d\uc131\uc744 \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4.",
"no_devices": "\ud398\uc5b4\ub9c1\uc774 \ud544\uc694\ud55c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
},
"error": {
"authentication_error": "HomeKit \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud655\uc778 \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
- "max_peers_error": "\uae30\uae30\uc5d0 \ube44\uc5b4\uc788\ub294 \ud398\uc5b4\ub9c1 \uc7a5\uc18c\uac00 \uc5c6\uc5b4 \ud398\uc5b4\ub9c1 \ucd94\uac00\ub97c \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
+ "max_peers_error": "\uae30\uae30\uc5d0 \ube44\uc5b4\uc788\ub294 \ud398\uc5b4\ub9c1 \uc7a5\uc18c\uac00 \uc5c6\uc5b4 \ud398\uc5b4\ub9c1\uc744 \ucd94\uac00\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
"pairing_failed": "\uc774 \uae30\uae30\uc640 \ud398\uc5b4\ub9c1\uc744 \uc2dc\ub3c4\ud558\ub294 \uc911 \ucc98\ub9ac\ub418\uc9c0 \uc54a\uc740 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc77c\uc2dc\uc801\uc778 \uc624\ub958\uc774\uac70\ub098 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \uae30\uae30 \uc77c \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
"unable_to_pair": "\ud398\uc5b4\ub9c1 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
"unknown_error": "\uae30\uae30\uc5d0\uc11c \uc54c \uc218\uc5c6\ub294 \uc624\ub958\ub97c \ubcf4\uace0\ud588\uc2b5\ub2c8\ub2e4. \ud398\uc5b4\ub9c1\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4."
},
- "flow_title": "HomeKit \uc561\uc138\uc11c\ub9ac: {name}",
+ "flow_title": "HomeKit \uc561\uc138\uc11c\ub9ac \ud504\ub85c\ud1a0\ucf5c\uc744 \ud1b5\ud55c {name}",
"step": {
"busy_error": {
"description": "\ubaa8\ub4e0 \ucee8\ud2b8\ub864\ub7ec\uc5d0\uc11c \ud398\uc5b4\ub9c1\uc744 \uc911\ub2e8\ud558\uac70\ub098 \uae30\uae30\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ub2e4\uc74c \ud398\uc5b4\ub9c1\uc744 \uacc4\uc18d\ud574\uc8fc\uc138\uc694.",
- "title": "\uae30\uae30\uac00 \uc774\ubbf8 \ub2e4\ub978 \ucee8\ud2b8\ub864\ub7ec\uc640 \ud398\uc774\ub9c1\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4"
+ "title": "\uae30\uae30\uac00 \uc774\ubbf8 \ub2e4\ub978 \ucee8\ud2b8\ub864\ub7ec\uc640 \ud398\uc774\ub9c1 \ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4"
},
"max_tries_error": {
"description": "\uae30\uae30\uac00 100\ud68c \uc774\uc0c1\uc758 \uc2e4\ud328\ud55c \uc778\uc99d \uc2dc\ub3c4\ub97c \ubc1b\uc558\uc2b5\ub2c8\ub2e4. \uae30\uae30\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ub2e4\uc74c \ud398\uc5b4\ub9c1\uc744 \uacc4\uc18d\ud574\uc8fc\uc138\uc694.",
@@ -31,19 +31,19 @@
"data": {
"pairing_code": "\ud398\uc5b4\ub9c1 \ucf54\ub4dc"
},
- "description": "\uc774 \uc561\uc138\uc11c\ub9ac\ub97c \uc0ac\uc6a9\ud558\ub824\uba74 HomeKit \ud398\uc5b4\ub9c1 \ucf54\ub4dc (XXX-XX-XXX \ud615\uc2dd) \ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694",
- "title": "HomeKit \uc561\uc138\uc11c\ub9ac \ud398\uc5b4\ub9c1\ud558\uae30"
+ "description": "HomeKit \ucee8\ud2b8\ub864\ub7ec\ub294 \ubcc4\ub3c4\uc758 HomeKit \ucee8\ud2b8\ub864\ub7ec \ub610\ub294 iCloud \uc5c6\uc774 \uc554\ud638\ud654\ub41c \ubcf4\uc548 \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uc5ec \ub85c\uceec \uc601\uc5ed \ub124\ud2b8\uc6cc\ud06c \uc0c1\uc5d0\uc11c {name}\uacfc(\uc640) \ud1b5\uc2e0\ud569\ub2c8\ub2e4. \uc774 \uc561\uc138\uc11c\ub9ac\ub97c \uc0ac\uc6a9\ud558\ub824\uba74 HomeKit \ud398\uc5b4\ub9c1 \ucf54\ub4dc(XX-XX-XXX \ud615\uc2dd)\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc774 \ucf54\ub4dc\ub294 \uc77c\ubc18\uc801\uc73c\ub85c \uae30\uae30\ub098 \ud3ec\uc7a5 \ubc15\uc2a4\uc5d0 \ud45c\uc2dc\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.",
+ "title": "HomeKit \uc561\uc138\uc11c\ub9ac \ud504\ub85c\ud1a0\ucf5c\ub85c \uae30\uae30\uc640 \ud398\uc5b4\ub9c1 \ud558\uae30"
},
"protocol_error": {
- "description": "\uae30\uae30\uac00 \ud398\uc5b4\ub9c1 \ubaa8\ub4dc\uc5d0 \uc788\uc9c0 \uc54a\uc744 \uc218 \uc788\uc73c\uba70 \ubb3c\ub9ac\uc801 \ub610\ub294 \uac00\uc0c1 \uc758 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc57c \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uae30\uae30\uac00 \ud398\uc5b4\ub9c1 \ubaa8\ub4dc\uc5d0 \uc788\ub294\uc9c0 \ud655\uc778\ud558\uac70\ub098 \uae30\uae30\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ub2e4\uc74c \ud398\uc5b4\ub9c1\uc744 \uacc4\uc18d\ud574\uc8fc\uc138\uc694.",
+ "description": "\uae30\uae30\uac00 \ud398\uc5b4\ub9c1 \ubaa8\ub4dc\uc5d0 \uc788\uc9c0 \uc54a\uc744 \uc218 \uc788\uc73c\uba70 \ubb3c\ub9ac\uc801 \ub610\ub294 \uac00\uc0c1\uc758 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc57c \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uae30\uae30\uac00 \ud398\uc5b4\ub9c1 \ubaa8\ub4dc\uc5d0 \uc788\ub294\uc9c0 \ud655\uc778\ud558\uac70\ub098 \uae30\uae30\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ub2e4\uc74c \ud398\uc5b4\ub9c1\uc744 \uacc4\uc18d\ud574\uc8fc\uc138\uc694.",
"title": "\uc561\uc138\uc11c\ub9ac\uc640 \ud1b5\uc2e0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"user": {
"data": {
"device": "\uae30\uae30"
},
- "description": "\ud398\uc5b4\ub9c1 \ud560 \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694",
- "title": "HomeKit \uc561\uc138\uc11c\ub9ac \ud398\uc5b4\ub9c1\ud558\uae30"
+ "description": "HomeKit \ucee8\ud2b8\ub864\ub7ec\ub294 \ubcc4\ub3c4\uc758 HomeKit \ucee8\ud2b8\ub864\ub7ec \ub610\ub294 iCloud \uc5c6\uc774 \uc554\ud638\ud654\ub41c \ubcf4\uc548 \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uc5ec \ub85c\uceec \uc601\uc5ed \ub124\ud2b8\uc6cc\ud06c \uc0c1\uc5d0\uc11c \uae30\uae30\uc640 \ud1b5\uc2e0\ud569\ub2c8\ub2e4. \ud398\uc5b4\ub9c1 \ud560 \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:",
+ "title": "\uae30\uae30 \uc120\ud0dd\ud558\uae30"
}
}
},
@@ -62,9 +62,9 @@
"doorbell": "\ucd08\uc778\uc885"
},
"trigger_type": {
- "double_press": "\" {subtype} \"\uc744 \ub450\ubc88 \ub204\ub984",
- "long_press": "\" {subtype} \"\uc744 \uae38\uac8c \ub204\ub984",
- "single_press": "\"{subtype}\" \uc744 \ud55c\ubc88 \ub204\ub984"
+ "double_press": "\"{subtype}\"\uc774(\uac00) \ub450 \ubc88 \ub20c\ub838\uc744 \ub54c",
+ "long_press": "\"{subtype}\"\uc774(\uac00) \ub20c\ub824\uc9c4 \ucc44\ub85c \uc788\uc744 \ub54c",
+ "single_press": "\"{subtype}\"\uc774(\uac00) \ub20c\ub838\uc744 \ub54c"
}
},
"title": "HomeKit \ucee8\ud2b8\ub864\ub7ec"
diff --git a/homeassistant/components/homekit_controller/translations/nl.json b/homeassistant/components/homekit_controller/translations/nl.json
index ce4279229ab709..57692426ce0ff4 100644
--- a/homeassistant/components/homekit_controller/translations/nl.json
+++ b/homeassistant/components/homekit_controller/translations/nl.json
@@ -3,7 +3,7 @@
"abort": {
"accessory_not_found_error": "Kan geen koppeling toevoegen omdat het apparaat niet langer kan worden gevonden.",
"already_configured": "Accessoire is al geconfigureerd met deze controller.",
- "already_in_progress": "De configuratiestroom voor het apparaat is al in volle gang.",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
"already_paired": "Dit accessoire is al gekoppeld aan een ander apparaat. Reset het accessoire en probeer het opnieuw.",
"ignored_model": "HomeKit-ondersteuning voor dit model is geblokkeerd omdat er een meer functie volledige native integratie beschikbaar is.",
"invalid_config_entry": "Dit apparaat geeft aan dat het gereed is om te koppelen, maar er is al een conflicterend configuratie-item voor in de Home Assistant dat eerst moet worden verwijderd.",
@@ -17,21 +17,33 @@
"unable_to_pair": "Kan niet koppelen, probeer het opnieuw.",
"unknown_error": "Apparaat meldde een onbekende fout. Koppelen mislukt."
},
- "flow_title": "HomeKit-accessoire: {name}",
+ "flow_title": "{name} via HomeKit-accessoireprotocol",
"step": {
+ "busy_error": {
+ "description": "Onderbreek het koppelen op alle controllers, of probeer het apparaat opnieuw op te starten, en ga dan verder om het koppelen te hervatten.",
+ "title": "Het apparaat is al aan het koppelen met een andere controller"
+ },
+ "max_tries_error": {
+ "description": "Het apparaat heeft meer dan 100 mislukte verificatiepogingen ontvangen. Probeer het apparaat opnieuw op te starten en ga dan verder om het koppelen te hervatten.",
+ "title": "Maximum aantal authenticatiepogingen overschreden"
+ },
"pair": {
"data": {
"pairing_code": "Koppelingscode"
},
- "description": "Voer uw HomeKit pairing code (in het formaat XXX-XX-XXX) om dit accessoire te gebruiken",
+ "description": "HomeKit Controller communiceert met {name} via het lokale netwerk met behulp van een beveiligde versleutelde verbinding zonder een aparte HomeKit-controller of iCloud. Voer uw HomeKit-koppelcode in (in de indeling XXX-XX-XXX) om dit accessoire te gebruiken. Deze code is meestal te vinden op het apparaat zelf of in de verpakking.",
"title": "Koppel met HomeKit accessoire"
},
+ "protocol_error": {
+ "description": "Het apparaat staat mogelijk niet in de koppelingsmodus en vereist mogelijk een fysieke of virtuele druk op de knop. Zorg ervoor dat het apparaat in de koppelingsmodus staat of probeer het apparaat opnieuw op te starten en ga dan verder om het koppelen te hervatten.",
+ "title": "Fout bij het communiceren met de accessoire"
+ },
"user": {
"data": {
"device": "Apparaat"
},
- "description": "Selecteer het apparaat waarmee u wilt koppelen",
- "title": "Koppel met HomeKit accessoire"
+ "description": "HomeKit Controller communiceert via het lokale netwerk met behulp van een veilige versleutelde verbinding zonder een aparte HomeKit-controller of iCloud. Selecteer het apparaat dat u wilt koppelen:",
+ "title": "Apparaat selectie"
}
}
},
@@ -55,5 +67,5 @@
"single_press": "\" {subtype} \" ingedrukt"
}
},
- "title": "HomeKit Accessoires"
+ "title": "HomeKit Controller"
}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/pl.json b/homeassistant/components/homekit_controller/translations/pl.json
index 50fdf6a17e425f..3ccdfe452e559b 100644
--- a/homeassistant/components/homekit_controller/translations/pl.json
+++ b/homeassistant/components/homekit_controller/translations/pl.json
@@ -62,9 +62,9 @@
"doorbell": "dzwonek do drzwi"
},
"trigger_type": {
- "double_press": "\"{subtype}\" naci\u015bni\u0119ty dwukrotnie",
- "long_press": "\"{subtype}\" naci\u015bni\u0119ty i przytrzymany",
- "single_press": "\"{subtype}\" naci\u015bni\u0119ty"
+ "double_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty dwukrotnie",
+ "long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty i przytrzymany",
+ "single_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty"
}
},
"title": "Kontroler HomeKit"
diff --git a/homeassistant/components/homekit_controller/translations/tr.json b/homeassistant/components/homekit_controller/translations/tr.json
new file mode 100644
index 00000000000000..9d72049ba21920
--- /dev/null
+++ b/homeassistant/components/homekit_controller/translations/tr.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Aksesuar zaten bu denetleyici ile yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r.",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor"
+ },
+ "error": {
+ "authentication_error": "Yanl\u0131\u015f HomeKit kodu. L\u00fctfen kontrol edip tekrar deneyin.",
+ "unknown_error": "Cihaz bilinmeyen bir hata bildirdi. E\u015fle\u015ftirme ba\u015far\u0131s\u0131z oldu."
+ },
+ "step": {
+ "busy_error": {
+ "title": "Cihaz zaten ba\u015fka bir oyun kumandas\u0131yla e\u015fle\u015fiyor"
+ },
+ "max_tries_error": {
+ "title": "Maksimum kimlik do\u011frulama giri\u015fimi a\u015f\u0131ld\u0131"
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "D\u00fc\u011fme 1",
+ "button10": "D\u00fc\u011fme 10",
+ "button2": "D\u00fc\u011fme 2",
+ "button3": "D\u00fc\u011fme 3",
+ "button4": "D\u00fc\u011fme 4",
+ "button5": "D\u00fc\u011fme 5",
+ "button6": "D\u00fc\u011fme 6",
+ "button7": "D\u00fc\u011fme 7",
+ "button8": "D\u00fc\u011fme 8",
+ "button9": "D\u00fc\u011fme 9",
+ "doorbell": "Kap\u0131 zili"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/translations/uk.json b/homeassistant/components/homekit_controller/translations/uk.json
new file mode 100644
index 00000000000000..66eb1741208dea
--- /dev/null
+++ b/homeassistant/components/homekit_controller/translations/uk.json
@@ -0,0 +1,71 @@
+{
+ "config": {
+ "abort": {
+ "accessory_not_found_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0438\u043a\u043e\u043d\u0430\u0442\u0438 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f, \u043e\u0441\u043a\u0456\u043b\u044c\u043a\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e.",
+ "already_configured": "\u0410\u043a\u0441\u0435\u0441\u0443\u0430\u0440 \u0432\u0436\u0435 \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0438\u0439 \u0437 \u0446\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u043e\u043c.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "already_paired": "\u0426\u0435\u0439 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440 \u0432\u0436\u0435 \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0438\u0439 \u0437 \u0456\u043d\u0448\u0438\u043c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043a\u0438\u043d\u044c\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430 \u0456 \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443.",
+ "ignored_model": "\u041f\u0456\u0434\u0442\u0440\u0438\u043c\u043a\u0430 HomeKit \u0434\u043b\u044f \u0446\u0456\u0454\u0457 \u043c\u043e\u0434\u0435\u043b\u0456 \u0437\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u0430, \u043e\u0441\u043a\u0456\u043b\u044c\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0431\u0456\u043b\u044c\u0448 \u043f\u043e\u0432\u043d\u0430 \u043d\u0430\u0442\u0438\u0432\u043d\u0430 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f.",
+ "invalid_config_entry": "\u0426\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u044f\u043a \u0433\u043e\u0442\u043e\u0432\u0438\u0439 \u0434\u043e \u043e\u0431'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0432 \u043f\u0430\u0440\u0443, \u0430\u043b\u0435 \u0432 Home Assistant \u0432\u0436\u0435 \u0454 \u043a\u043e\u043d\u0444\u043b\u0456\u043a\u0442\u043d\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457 \u0434\u043b\u044f \u043d\u044c\u043e\u0433\u043e, \u044f\u043a\u0438\u0439 \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438.",
+ "invalid_properties": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0456 \u0432\u043b\u0430\u0441\u0442\u0438\u0432\u043e\u0441\u0442\u0456, \u043e\u0433\u043e\u043b\u043e\u0448\u0435\u043d\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c.",
+ "no_devices": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457, \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0456 \u0434\u043b\u044f \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f, \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456."
+ },
+ "error": {
+ "authentication_error": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434 HomeKit. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043a\u043e\u0434 \u0456 \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.",
+ "max_peers_error": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0456\u0434\u0445\u0438\u043b\u0438\u0432 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0447\u0435\u0440\u0435\u0437 \u0432\u0456\u0434\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c \u0432\u0456\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u044f.",
+ "pairing_failed": "\u041f\u0456\u0434 \u0447\u0430\u0441 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438 \u0441\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430. \u0426\u0435 \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0442\u0438\u043c\u0447\u0430\u0441\u043e\u0432\u0438\u0439 \u0437\u0431\u0456\u0439 \u0430\u0431\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0430 \u0434\u0430\u043d\u0438\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0449\u0435 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.",
+ "unable_to_pair": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043f\u0430\u0440\u0443. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.",
+ "unknown_error": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u0438\u0432 \u043f\u0440\u043e \u043d\u0435\u0432\u0456\u0434\u043e\u043c\u0443 \u043f\u043e\u043c\u0438\u043b\u043a\u0443. \u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043f\u0430\u0440\u0443."
+ },
+ "flow_title": "{name} \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0456\u0432 HomeKit",
+ "step": {
+ "busy_error": {
+ "description": "\u0421\u043a\u0430\u0441\u0443\u0439\u0442\u0435 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u043d\u0430 \u0432\u0441\u0456\u0445 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430\u0445 \u0430\u0431\u043e \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0442\u044c \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f.",
+ "title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0443\u0436\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e \u0456\u043d\u0448\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430."
+ },
+ "max_tries_error": {
+ "description": "\u041f\u043e\u043d\u0430\u0434 100 \u0441\u043f\u0440\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 \u043f\u0440\u043e\u0439\u0448\u043b\u0438 \u043d\u0435\u0432\u0434\u0430\u043b\u043e. \u0421\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u0430 \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0442\u044c \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f.",
+ "title": "\u041f\u0435\u0440\u0435\u0432\u0438\u0449\u0435\u043d\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0443 \u043a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0441\u043f\u0440\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457."
+ },
+ "pair": {
+ "data": {
+ "pairing_code": "\u041a\u043e\u0434 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f"
+ },
+ "description": "HomeKit Controller \u043e\u0431\u043c\u0456\u043d\u044e\u0454\u0442\u044c\u0441\u044f \u0434\u0430\u043d\u0438\u043c\u0438 \u0437 {name} \u043f\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0456\u0439 \u043c\u0435\u0440\u0435\u0436\u0456, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0447\u0438 \u0431\u0435\u0437\u043f\u0435\u0447\u043d\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0435 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e\u0441\u0442\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f \u043e\u043a\u0440\u0435\u043c\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430 HomeKit \u0430\u0431\u043e iCloud. \u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 \u0441\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 XXX-XX-XXX), \u0449\u043e\u0431 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0446\u0435\u0439 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440. \u0426\u0435\u0439 \u043a\u043e\u0434 \u0437\u0430\u0437\u0432\u0438\u0447\u0430\u0439 \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u043d\u0430 \u0441\u0430\u043c\u043e\u043c\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0430\u0431\u043e \u043d\u0430 \u0443\u043f\u0430\u043a\u043e\u0432\u0446\u0456.",
+ "title": "\u0421\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438 \u0437 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0456\u0432 HomeKit"
+ },
+ "protocol_error": {
+ "description": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u043c\u043e\u0436\u043b\u0438\u0432\u043e, \u043d\u0435 \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0456 \u043c\u043e\u0436\u0435 \u0437\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0438\u0441\u044f \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f \u0444\u0456\u0437\u0438\u0447\u043d\u043e\u0457 \u0430\u0431\u043e \u0432\u0456\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0457 \u043a\u043d\u043e\u043f\u043a\u0438. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438, \u0430\u0431\u043e \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u0456 \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0442\u044c \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f.",
+ "title": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437\u0432'\u044f\u0437\u043a\u0443 \u0437 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u043e\u043c."
+ },
+ "user": {
+ "data": {
+ "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439"
+ },
+ "description": "HomeKit Controller \u043e\u0431\u043c\u0456\u043d\u044e\u0454\u0442\u044c\u0441\u044f \u0434\u0430\u043d\u0438\u043c\u0438 \u0432 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0456\u0439 \u043c\u0435\u0440\u0435\u0436\u0456 \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c \u0431\u0435\u0437\u043f\u0435\u0447\u043d\u043e\u0433\u043e \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043e\u0433\u043e \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e\u0441\u0442\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f \u043e\u043a\u0440\u0435\u043c\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430 HomeKit \u0430\u0431\u043e iCloud. \u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u0437 \u044f\u043a\u0438\u043c \u0445\u043e\u0447\u0435\u0442\u0435 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043f\u0430\u0440\u0443:",
+ "title": "\u0412\u0438\u0431\u0456\u0440 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button1": "\u041a\u043d\u043e\u043f\u043a\u0430 1",
+ "button10": "\u041a\u043d\u043e\u043f\u043a\u0430 10",
+ "button2": "\u041a\u043d\u043e\u043f\u043a\u0430 2",
+ "button3": "\u041a\u043d\u043e\u043f\u043a\u0430 3",
+ "button4": "\u041a\u043d\u043e\u043f\u043a\u0430 4",
+ "button5": "\u041a\u043d\u043e\u043f\u043a\u0430 5",
+ "button6": "\u041a\u043d\u043e\u043f\u043a\u0430 6",
+ "button7": "\u041a\u043d\u043e\u043f\u043a\u0430 7",
+ "button8": "\u041a\u043d\u043e\u043f\u043a\u0430 8",
+ "button9": "\u041a\u043d\u043e\u043f\u043a\u0430 9",
+ "doorbell": "\u0414\u0432\u0435\u0440\u043d\u0438\u0439 \u0434\u0437\u0432\u0456\u043d\u043e\u043a"
+ },
+ "trigger_type": {
+ "double_press": "\"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0434\u0432\u0456\u0447\u0456",
+ "long_press": "\"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0456 \u0443\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f",
+ "single_press": "\"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430"
+ }
+ },
+ "title": "HomeKit Controller"
+}
\ No newline at end of file
diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py
index 1f727bab4e1a1a..46f3ac6caf2043 100644
--- a/homeassistant/components/homematic/__init__.py
+++ b/homeassistant/components/homematic/__init__.py
@@ -10,10 +10,13 @@
ATTR_ENTITY_ID,
ATTR_MODE,
ATTR_NAME,
+ ATTR_TIME,
CONF_HOST,
CONF_HOSTS,
CONF_PASSWORD,
+ CONF_PATH,
CONF_PLATFORM,
+ CONF_PORT,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
@@ -37,7 +40,6 @@
ATTR_PARAMSET,
ATTR_PARAMSET_KEY,
ATTR_RX_MODE,
- ATTR_TIME,
ATTR_UNIQUE_ID,
ATTR_VALUE,
ATTR_VALUE_TYPE,
@@ -47,8 +49,6 @@
CONF_JSONPORT,
CONF_LOCAL_IP,
CONF_LOCAL_PORT,
- CONF_PATH,
- CONF_PORT,
CONF_RESOLVENAMES,
CONF_RESOLVENAMES_OPTIONS,
DATA_CONF,
@@ -209,7 +209,6 @@
def setup(hass, config):
"""Set up the Homematic component."""
-
conf = config[DOMAIN]
hass.data[DATA_CONF] = remotes = {}
hass.data[DATA_STORE] = set()
@@ -612,6 +611,8 @@ def _device_from_servicecall(hass, service):
interface = service.data.get(ATTR_INTERFACE)
if address == "BIDCOS-RF":
address = "BidCoS-RF"
+ if address == "HMIP-RCV-1":
+ address = "HmIP-RCV-1"
if interface:
return hass.data[DATA_HOMEMATIC].devices[interface].get(address)
diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py
index 3b77b90ff2528f..aa5fb4a8e44591 100644
--- a/homeassistant/components/homematic/climate.py
+++ b/homeassistant/components/homematic/climate.py
@@ -7,6 +7,7 @@
PRESET_BOOST,
PRESET_COMFORT,
PRESET_ECO,
+ PRESET_NONE,
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
)
@@ -92,14 +93,14 @@ def preset_mode(self):
return "boost"
if not self._hm_control_mode:
- return None
+ return PRESET_NONE
mode = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][self._hm_control_mode]
mode = mode.lower()
# Filter HVAC states
if mode not in (HVAC_MODE_AUTO, HVAC_MODE_HEAT):
- return None
+ return PRESET_NONE
return mode
@property
diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py
index a6ff19a6eeab08..864441c2aa68f2 100644
--- a/homeassistant/components/homematic/const.py
+++ b/homeassistant/components/homematic/const.py
@@ -21,7 +21,6 @@
ATTR_INTERFACE = "interface"
ATTR_ERRORCODE = "error"
ATTR_MESSAGE = "message"
-ATTR_TIME = "time"
ATTR_UNIQUE_ID = "unique_id"
ATTR_PARAMSET_KEY = "paramset_key"
ATTR_PARAMSET = "paramset"
@@ -61,6 +60,7 @@
"IPWSwitch",
"IOSwitchWireless",
"IPWIODevice",
+ "IPSwitchBattery",
],
DISCOVER_LIGHTS: [
"Dimmer",
@@ -120,6 +120,8 @@
"ValveBox",
"IPKeyBlind",
"IPKeyBlindTilt",
+ "IPLanRouter",
+ "TempModuleSTE2",
],
DISCOVER_CLIMATE: [
"Thermostat",
@@ -164,6 +166,7 @@
"IPWMotionDection",
"IPAlarmSensor",
"IPRainSensor",
+ "IPLanRouter",
],
DISCOVER_COVER: [
"Blind",
@@ -232,8 +235,6 @@
CONF_INTERFACES = "interfaces"
CONF_LOCAL_IP = "local_ip"
CONF_LOCAL_PORT = "local_port"
-CONF_PORT = "port"
-CONF_PATH = "path"
CONF_CALLBACK_IP = "callback_ip"
CONF_CALLBACK_PORT = "callback_port"
CONF_RESOLVENAMES = "resolvenames"
diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py
index a391fa80461798..50b9bcb2bfcd78 100644
--- a/homeassistant/components/homematic/entity.py
+++ b/homeassistant/components/homematic/entity.py
@@ -71,9 +71,8 @@ def available(self):
return self._available
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
-
# Static attributes
attr = {
"id": self._hmdevice.ADDRESS,
@@ -232,7 +231,7 @@ def state(self):
return self._state
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._variables.copy()
diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py
index 11731e2ae5feec..036034bf801846 100644
--- a/homeassistant/components/homematic/light.py
+++ b/homeassistant/components/homematic/light.py
@@ -9,6 +9,7 @@
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT,
+ SUPPORT_TRANSITION,
LightEntity,
)
@@ -53,7 +54,8 @@ def is_on(self):
@property
def supported_features(self):
"""Flag supported features."""
- features = SUPPORT_BRIGHTNESS
+ features = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
+
if "COLOR" in self._hmdevice.WRITENODE:
features |= SUPPORT_COLOR
if "PROGRAM" in self._hmdevice.WRITENODE:
@@ -95,7 +97,7 @@ def effect(self):
def turn_on(self, **kwargs):
"""Turn the light on and/or change color or color effect settings."""
if ATTR_TRANSITION in kwargs:
- self._hmdevice.setValue("RAMP_TIME", kwargs[ATTR_TRANSITION])
+ self._hmdevice.setValue("RAMP_TIME", kwargs[ATTR_TRANSITION], self._channel)
if ATTR_BRIGHTNESS in kwargs and self._state == "LEVEL":
percent_bright = float(kwargs[ATTR_BRIGHTNESS]) / 255
@@ -123,6 +125,9 @@ def turn_on(self, **kwargs):
def turn_off(self, **kwargs):
"""Turn the light off."""
+ if ATTR_TRANSITION in kwargs:
+ self._hmdevice.setValue("RAMP_TIME", kwargs[ATTR_TRANSITION], self._channel)
+
self._hmdevice.off(self._channel)
def _init_data_struct(self):
diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json
index 36414b606f9990..d81dc97cdb7669 100644
--- a/homeassistant/components/homematic/manifest.json
+++ b/homeassistant/components/homematic/manifest.json
@@ -2,6 +2,6 @@
"domain": "homematic",
"name": "Homematic",
"documentation": "https://www.home-assistant.io/integrations/homematic",
- "requirements": ["pyhomematic==0.1.71"],
+ "requirements": ["pyhomematic==0.1.72"],
"codeowners": ["@pvizeli", "@danielperna84"]
}
diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py
index e6439c451c1670..4525d5a48fcbf0 100644
--- a/homeassistant/components/homematic/sensor.py
+++ b/homeassistant/components/homematic/sensor.py
@@ -1,6 +1,7 @@
"""Support for HomeMatic sensors."""
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEGREE,
DEVICE_CLASS_HUMIDITY,
@@ -97,7 +98,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(devices, True)
-class HMSensor(HMDevice):
+class HMSensor(HMDevice, SensorEntity):
"""Representation of a HomeMatic sensor."""
@property
diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py
index 7ea6a4fe0b41b0..ca1af8266c6295 100644
--- a/homeassistant/components/homematicip_cloud/__init__.py
+++ b/homeassistant/components/homematicip_cloud/__init__.py
@@ -1,6 +1,4 @@
"""Support for HomematicIP Cloud devices."""
-import logging
-
import voluptuous as vol
from homeassistant import config_entries
@@ -23,8 +21,6 @@
from .hap import HomematicipAuth, HomematicipHAP # noqa: F401
from .services import async_setup_services, async_unload_services
-_LOGGER = logging.getLogger(__name__)
-
CONFIG_SCHEMA = vol.Schema(
{
vol.Optional(DOMAIN, default=[]): vol.All(
diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py
index 51cec6ac0cd87a..7fa5e197aa8247 100644
--- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py
+++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py
@@ -1,6 +1,8 @@
"""Support for HomematicIP Cloud alarm control panel."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict
+from typing import Any
from homematicip.functionalHomes import SecurityAndAlarmHome
@@ -44,7 +46,7 @@ def __init__(self, hap: HomematicipHAP) -> None:
_LOGGER.info("Setting up %s", self.name)
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device specific attributes."""
return {
"identifiers": {(HMIPC_DOMAIN, f"ACP {self._home.id}")},
diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py
index 4f1ae523ecc76a..4fcf1f67dd4f16 100644
--- a/homeassistant/components/homematicip_cloud/binary_sensor.py
+++ b/homeassistant/components/homematicip_cloud/binary_sensor.py
@@ -1,5 +1,7 @@
"""Support for HomematicIP Cloud binary sensor."""
-from typing import Any, Dict
+from __future__ import annotations
+
+from typing import Any
from homematicip.aio.device import (
AsyncAccelerationSensor,
@@ -166,7 +168,7 @@ def name(self) -> str:
return name if not self._home.name else f"{self._home.name} {name}"
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device specific attributes."""
# Adds a sensor to the existing HAP device
return {
@@ -210,9 +212,9 @@ def is_on(self) -> bool:
return self._device.accelerationSensorTriggered
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the acceleration sensor."""
- state_attr = super().device_state_attributes
+ state_attr = super().extra_state_attributes
for attr, attr_key in SAM_DEVICE_ATTRIBUTES.items():
attr_value = getattr(self._device, attr, None)
@@ -285,9 +287,9 @@ def device_class(self) -> str:
return DEVICE_CLASS_DOOR
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the Shutter Contact."""
- state_attr = super().device_state_attributes
+ state_attr = super().extra_state_attributes
if self.has_additional_state:
window_state = getattr(self._device, "windowState", None)
@@ -412,9 +414,9 @@ def is_on(self) -> bool:
return self._device.sunshine
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the illuminance sensor."""
- state_attr = super().device_state_attributes
+ state_attr = super().extra_state_attributes
today_sunshine_duration = getattr(self._device, "todaySunshineDuration", None)
if today_sunshine_duration:
@@ -482,9 +484,9 @@ def available(self) -> bool:
return True
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the security zone group."""
- state_attr = super().device_state_attributes
+ state_attr = super().extra_state_attributes
for attr, attr_key in GROUP_ATTRIBUTES.items():
attr_value = getattr(self._device, attr, None)
@@ -526,9 +528,9 @@ def __init__(self, hap: HomematicipHAP, device) -> None:
super().__init__(hap, device, post="Sensors")
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the security group."""
- state_attr = super().device_state_attributes
+ state_attr = super().extra_state_attributes
smoke_detector_at = getattr(self._device, "smokeDetectorAlarmType", None)
if smoke_detector_at:
diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py
index dcd5aeb284e3db..5cdadf4d5f1e0c 100644
--- a/homeassistant/components/homematicip_cloud/climate.py
+++ b/homeassistant/components/homematicip_cloud/climate.py
@@ -1,5 +1,7 @@
"""Support for HomematicIP Cloud climate devices."""
-from typing import Any, Dict, List, Optional, Union
+from __future__ import annotations
+
+from typing import Any
from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact
from homematicip.aio.group import AsyncHeatingGroup
@@ -71,7 +73,7 @@ def __init__(self, hap: HomematicipHAP, device: AsyncHeatingGroup) -> None:
self._simple_heating = self._first_radiator_thermostat
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device specific attributes."""
return {
"identifiers": {(HMIPC_DOMAIN, self._device.id)},
@@ -121,7 +123,7 @@ def hvac_mode(self) -> str:
return HVAC_MODE_AUTO
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes."""
if self._disabled_by_cooling_mode and not self._has_switch:
return [HVAC_MODE_OFF]
@@ -133,7 +135,7 @@ def hvac_modes(self) -> List[str]:
)
@property
- def hvac_action(self) -> Optional[str]:
+ def hvac_action(self) -> str | None:
"""
Return the current hvac_action.
@@ -151,7 +153,7 @@ def hvac_action(self) -> Optional[str]:
return None
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return the current preset mode."""
if self._device.boostMode:
return PRESET_BOOST
@@ -174,7 +176,7 @@ def preset_mode(self) -> Optional[str]:
)
@property
- def preset_modes(self) -> List[str]:
+ def preset_modes(self) -> list[str]:
"""Return a list of available preset modes incl. hmip profiles."""
# Boost is only available if a radiator thermostat is in the room,
# and heat mode is enabled.
@@ -237,9 +239,9 @@ async def async_set_preset_mode(self, preset_mode: str) -> None:
await self._device.set_active_profile(profile_idx)
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the access point."""
- state_attr = super().device_state_attributes
+ state_attr = super().extra_state_attributes
if self._device.controlMode == HMIP_ECO_CM:
if self._indoor_climate.absenceType in [
@@ -259,7 +261,7 @@ def _indoor_climate(self) -> IndoorClimateHome:
return self._home.get_functionalHome(IndoorClimateHome)
@property
- def _device_profiles(self) -> List[str]:
+ def _device_profiles(self) -> list[str]:
"""Return the relevant profiles."""
return [
profile
@@ -270,7 +272,7 @@ def _device_profiles(self) -> List[str]:
]
@property
- def _device_profile_names(self) -> List[str]:
+ def _device_profile_names(self) -> list[str]:
"""Return a collection of profile names."""
return [profile.name for profile in self._device_profiles]
@@ -298,7 +300,7 @@ def _disabled_by_cooling_mode(self) -> bool:
)
@property
- def _relevant_profile_group(self) -> List[str]:
+ def _relevant_profile_group(self) -> list[str]:
"""Return the relevant profile groups."""
if self._disabled_by_cooling_mode:
return []
@@ -322,7 +324,7 @@ def _has_radiator_thermostat(self) -> bool:
@property
def _first_radiator_thermostat(
self,
- ) -> Optional[Union[AsyncHeatingThermostat, AsyncHeatingThermostatCompact]]:
+ ) -> AsyncHeatingThermostat | AsyncHeatingThermostatCompact | None:
"""Return the first radiator thermostat from the hmip heating group."""
for device in self._device.devices:
if isinstance(
diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py
index b6b78948894c8c..d90d8d7023b8a3 100644
--- a/homeassistant/components/homematicip_cloud/config_flow.py
+++ b/homeassistant/components/homematicip_cloud/config_flow.py
@@ -1,5 +1,7 @@
"""Config flow to configure the HomematicIP Cloud component."""
-from typing import Any, Dict
+from __future__ import annotations
+
+from typing import Any
import voluptuous as vol
@@ -27,11 +29,11 @@ def __init__(self) -> None:
"""Initialize HomematicIP Cloud config flow."""
self.auth = None
- async def async_step_user(self, user_input=None) -> Dict[str, Any]:
+ async def async_step_user(self, user_input=None) -> dict[str, Any]:
"""Handle a flow initialized by the user."""
return await self.async_step_init(user_input)
- async def async_step_init(self, user_input=None) -> Dict[str, Any]:
+ async def async_step_init(self, user_input=None) -> dict[str, Any]:
"""Handle a flow start."""
errors = {}
@@ -62,7 +64,7 @@ async def async_step_init(self, user_input=None) -> Dict[str, Any]:
errors=errors,
)
- async def async_step_link(self, user_input=None) -> Dict[str, Any]:
+ async def async_step_link(self, user_input=None) -> dict[str, Any]:
"""Attempt to link with the HomematicIP Cloud access point."""
errors = {}
@@ -84,7 +86,7 @@ async def async_step_link(self, user_input=None) -> Dict[str, Any]:
return self.async_show_form(step_id="link", errors=errors)
- async def async_step_import(self, import_info) -> Dict[str, Any]:
+ async def async_step_import(self, import_info) -> dict[str, Any]:
"""Import a new access point as a config entry."""
hapid = import_info[HMIPC_HAPID].replace("-", "").upper()
authtoken = import_info[HMIPC_AUTHTOKEN]
diff --git a/homeassistant/components/homematicip_cloud/const.py b/homeassistant/components/homematicip_cloud/const.py
index 4fb21febb40d11..31ccb8b9bc7248 100644
--- a/homeassistant/components/homematicip_cloud/const.py
+++ b/homeassistant/components/homematicip_cloud/const.py
@@ -16,7 +16,7 @@
DOMAIN = "homematicip_cloud"
-COMPONENTS = [
+PLATFORMS = [
ALARM_CONTROL_PANEL_DOMAIN,
BINARY_SENSOR_DOMAIN,
CLIMATE_DOMAIN,
diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py
index 29a06c558fe893..aa1be11758eeb4 100644
--- a/homeassistant/components/homematicip_cloud/cover.py
+++ b/homeassistant/components/homematicip_cloud/cover.py
@@ -1,5 +1,5 @@
"""Support for HomematicIP Cloud cover devices."""
-from typing import Optional
+from __future__ import annotations
from homematicip.aio.device import (
AsyncBlindModule,
@@ -95,7 +95,7 @@ async def async_set_cover_tilt_position(self, **kwargs) -> None:
)
@property
- def is_closed(self) -> Optional[bool]:
+ def is_closed(self) -> bool | None:
"""Return if the cover is closed."""
if self._device.primaryShadingLevel is not None:
return self._device.primaryShadingLevel == HMIP_COVER_CLOSED
@@ -168,7 +168,7 @@ async def async_set_cover_position(self, **kwargs) -> None:
await self._device.set_shutter_level(level, self._channel)
@property
- def is_closed(self) -> Optional[bool]:
+ def is_closed(self) -> bool | None:
"""Return if the cover is closed."""
if self._device.functionalChannels[self._channel].shutterLevel is not None:
return (
@@ -265,7 +265,7 @@ def current_cover_position(self) -> int:
return door_state_to_position.get(self._device.doorState)
@property
- def is_closed(self) -> Optional[bool]:
+ def is_closed(self) -> bool | None:
"""Return if the cover is closed."""
return self._device.doorState == DoorState.CLOSED
@@ -305,7 +305,7 @@ def current_cover_tilt_position(self) -> int:
return None
@property
- def is_closed(self) -> Optional[bool]:
+ def is_closed(self) -> bool | None:
"""Return if the cover is closed."""
if self._device.shutterLevel is not None:
return self._device.shutterLevel == HMIP_COVER_CLOSED
diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py
index a8df0107eebe40..856e47a1dee168 100644
--- a/homeassistant/components/homematicip_cloud/generic_entity.py
+++ b/homeassistant/components/homematicip_cloud/generic_entity.py
@@ -1,10 +1,13 @@
"""Generic entity for the HomematicIP Cloud component."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from homematicip.aio.device import AsyncDevice
from homematicip.aio.group import AsyncGroup
+from homeassistant.const import ATTR_ID
from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.entity import Entity
@@ -19,7 +22,6 @@
ATTR_CONFIG_PENDING = "config_pending"
ATTR_CONNECTION_TYPE = "connection_type"
ATTR_DUTY_CYCLE_REACHED = "duty_cycle_reached"
-ATTR_ID = "id"
ATTR_IS_GROUP = "is_group"
# RSSI HAP -> Device
ATTR_RSSI_DEVICE = "rssi_device"
@@ -74,9 +76,9 @@ def __init__(
self,
hap: HomematicipHAP,
device,
- post: Optional[str] = None,
- channel: Optional[int] = None,
- is_multi_channel: Optional[bool] = False,
+ post: str | None = None,
+ channel: int | None = None,
+ is_multi_channel: bool | None = False,
) -> None:
"""Initialize the generic entity."""
self._hap = hap
@@ -90,7 +92,7 @@ def __init__(
_LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType)
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device specific attributes."""
# Only physical devices should be HA devices.
if isinstance(self._device, AsyncDevice):
@@ -172,7 +174,7 @@ def _async_device_removed(self, *args, **kwargs) -> None:
"""Handle hmip device removal."""
# Set marker showing that the HmIP device hase been removed.
self.hmip_device_removed = True
- self.hass.async_create_task(self.async_remove())
+ self.hass.async_create_task(self.async_remove(force_remove=True))
@property
def name(self) -> str:
@@ -223,7 +225,7 @@ def unique_id(self) -> str:
return unique_id
@property
- def icon(self) -> Optional[str]:
+ def icon(self) -> str | None:
"""Return the icon."""
for attr, icon in DEVICE_ATTRIBUTE_ICONS.items():
if getattr(self._device, attr, None):
@@ -232,7 +234,7 @@ def icon(self) -> Optional[str]:
return None
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the generic entity."""
state_attr = {}
diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py
index 151807391b9589..5ad4efed1f674b 100644
--- a/homeassistant/components/homematicip_cloud/hap.py
+++ b/homeassistant/components/homematicip_cloud/hap.py
@@ -13,7 +13,7 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import HomeAssistantType
-from .const import COMPONENTS, HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN
+from .const import HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN, PLATFORMS
from .errors import HmipcConnectionError
_LOGGER = logging.getLogger(__name__)
@@ -102,10 +102,10 @@ async def async_setup(self, tries: int = 0) -> bool:
"Connected to HomematicIP with HAP %s", self.config_entry.unique_id
)
- for component in COMPONENTS:
+ for platform in PLATFORMS:
self.hass.async_create_task(
self.hass.config_entries.async_forward_entry_setup(
- self.config_entry, component
+ self.config_entry, platform
)
)
return True
@@ -215,9 +215,9 @@ async def async_reset(self) -> bool:
self._retry_task.cancel()
await self.home.disable_events()
_LOGGER.info("Closed connection to HomematicIP cloud server")
- for component in COMPONENTS:
+ for platform in PLATFORMS:
await self.hass.config_entries.async_forward_entry_unload(
- self.config_entry, component
+ self.config_entry, platform
)
self.hmip_device_by_entity_id = {}
return True
diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py
index 1909ff818b9323..5732ea1bf9670a 100644
--- a/homeassistant/components/homematicip_cloud/light.py
+++ b/homeassistant/components/homematicip_cloud/light.py
@@ -1,5 +1,7 @@
"""Support for HomematicIP Cloud lights."""
-from typing import Any, Dict
+from __future__ import annotations
+
+from typing import Any
from homematicip.aio.device import (
AsyncBrandDimmer,
@@ -90,9 +92,9 @@ class HomematicipLightMeasuring(HomematicipLight):
"""Representation of the HomematicIP measuring light."""
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the light."""
- state_attr = super().device_state_attributes
+ state_attr = super().extra_state_attributes
current_power_w = self._device.currentPowerConsumption
if current_power_w > 0.05:
@@ -206,9 +208,9 @@ def hs_color(self) -> tuple:
return self._color_switcher.get(simple_rgb_color, [0.0, 0.0])
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the notification light sensor."""
- state_attr = super().device_state_attributes
+ state_attr = super().extra_state_attributes
if self.is_on:
state_attr[ATTR_COLOR_NAME] = self._func_channel.simpleRGBColorState
diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json
index 93e96267be378a..f247a58f364ecd 100644
--- a/homeassistant/components/homematicip_cloud/manifest.json
+++ b/homeassistant/components/homematicip_cloud/manifest.json
@@ -4,6 +4,6 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
"requirements": ["homematicip==0.13.1"],
- "codeowners": ["@SukramJ"],
+ "codeowners": [],
"quality_scale": "platinum"
}
diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py
index 9e202302c1083b..9e6e96232b46cb 100644
--- a/homeassistant/components/homematicip_cloud/sensor.py
+++ b/homeassistant/components/homematicip_cloud/sensor.py
@@ -1,5 +1,7 @@
"""Support for HomematicIP Cloud sensors."""
-from typing import Any, Dict
+from __future__ import annotations
+
+from typing import Any
from homematicip.aio.device import (
AsyncBrandSwitchMeasuring,
@@ -24,6 +26,7 @@
)
from homematicip.base.enums import ValveState
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
@@ -121,7 +124,7 @@ async def async_setup_entry(
async_add_entities(entities)
-class HomematicipAccesspointDutyCycle(HomematicipGenericEntity):
+class HomematicipAccesspointDutyCycle(HomematicipGenericEntity, SensorEntity):
"""Representation of then HomeMaticIP access point."""
def __init__(self, hap: HomematicipHAP, device) -> None:
@@ -144,7 +147,7 @@ def unit_of_measurement(self) -> str:
return PERCENTAGE
-class HomematicipHeatingThermostat(HomematicipGenericEntity):
+class HomematicipHeatingThermostat(HomematicipGenericEntity, SensorEntity):
"""Representation of the HomematicIP heating thermostat."""
def __init__(self, hap: HomematicipHAP, device) -> None:
@@ -173,7 +176,7 @@ def unit_of_measurement(self) -> str:
return PERCENTAGE
-class HomematicipHumiditySensor(HomematicipGenericEntity):
+class HomematicipHumiditySensor(HomematicipGenericEntity, SensorEntity):
"""Representation of the HomematicIP humidity sensor."""
def __init__(self, hap: HomematicipHAP, device) -> None:
@@ -196,7 +199,7 @@ def unit_of_measurement(self) -> str:
return PERCENTAGE
-class HomematicipTemperatureSensor(HomematicipGenericEntity):
+class HomematicipTemperatureSensor(HomematicipGenericEntity, SensorEntity):
"""Representation of the HomematicIP thermometer."""
def __init__(self, hap: HomematicipHAP, device) -> None:
@@ -222,9 +225,9 @@ def unit_of_measurement(self) -> str:
return TEMP_CELSIUS
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the windspeed sensor."""
- state_attr = super().device_state_attributes
+ state_attr = super().extra_state_attributes
temperature_offset = getattr(self._device, "temperatureOffset", None)
if temperature_offset:
@@ -233,7 +236,7 @@ def device_state_attributes(self) -> Dict[str, Any]:
return state_attr
-class HomematicipIlluminanceSensor(HomematicipGenericEntity):
+class HomematicipIlluminanceSensor(HomematicipGenericEntity, SensorEntity):
"""Representation of the HomematicIP Illuminance sensor."""
def __init__(self, hap: HomematicipHAP, device) -> None:
@@ -259,9 +262,9 @@ def unit_of_measurement(self) -> str:
return LIGHT_LUX
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the wind speed sensor."""
- state_attr = super().device_state_attributes
+ state_attr = super().extra_state_attributes
for attr, attr_key in ILLUMINATION_DEVICE_ATTRIBUTES.items():
attr_value = getattr(self._device, attr, None)
@@ -271,7 +274,7 @@ def device_state_attributes(self) -> Dict[str, Any]:
return state_attr
-class HomematicipPowerSensor(HomematicipGenericEntity):
+class HomematicipPowerSensor(HomematicipGenericEntity, SensorEntity):
"""Representation of the HomematicIP power measuring sensor."""
def __init__(self, hap: HomematicipHAP, device) -> None:
@@ -294,7 +297,7 @@ def unit_of_measurement(self) -> str:
return POWER_WATT
-class HomematicipWindspeedSensor(HomematicipGenericEntity):
+class HomematicipWindspeedSensor(HomematicipGenericEntity, SensorEntity):
"""Representation of the HomematicIP wind speed sensor."""
def __init__(self, hap: HomematicipHAP, device) -> None:
@@ -312,9 +315,9 @@ def unit_of_measurement(self) -> str:
return SPEED_KILOMETERS_PER_HOUR
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the wind speed sensor."""
- state_attr = super().device_state_attributes
+ state_attr = super().extra_state_attributes
wind_direction = getattr(self._device, "windDirection", None)
if wind_direction is not None:
@@ -327,7 +330,7 @@ def device_state_attributes(self) -> Dict[str, Any]:
return state_attr
-class HomematicipTodayRainSensor(HomematicipGenericEntity):
+class HomematicipTodayRainSensor(HomematicipGenericEntity, SensorEntity):
"""Representation of the HomematicIP rain counter of a day sensor."""
def __init__(self, hap: HomematicipHAP, device) -> None:
@@ -345,7 +348,7 @@ def unit_of_measurement(self) -> str:
return LENGTH_MILLIMETERS
-class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity):
+class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity, SensorEntity):
"""Representation of the HomematicIP passage detector delta counter."""
@property
@@ -354,9 +357,9 @@ def state(self) -> int:
return self._device.leftRightCounterDelta
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the delta counter."""
- state_attr = super().device_state_attributes
+ state_attr = super().extra_state_attributes
state_attr[ATTR_LEFT_COUNTER] = self._device.leftCounter
state_attr[ATTR_RIGHT_COUNTER] = self._device.rightCounter
diff --git a/homeassistant/components/homematicip_cloud/services.py b/homeassistant/components/homematicip_cloud/services.py
index d8535edda50e97..aa82e72e284c97 100644
--- a/homeassistant/components/homematicip_cloud/services.py
+++ b/homeassistant/components/homematicip_cloud/services.py
@@ -1,7 +1,8 @@
"""Support for HomematicIP Cloud devices."""
+from __future__ import annotations
+
import logging
from pathlib import Path
-from typing import Optional
from homematicip.aio.device import AsyncSwitchMeasuring
from homematicip.aio.group import AsyncHeatingGroup
@@ -9,7 +10,7 @@
from homematicip.base.helpers import handle_config
import voluptuous as vol
-from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import comp_entity_ids
from homeassistant.helpers.service import (
@@ -29,7 +30,6 @@
ATTR_CONFIG_OUTPUT_PATH = "config_output_path"
ATTR_DURATION = "duration"
ATTR_ENDTIME = "endtime"
-ATTR_TEMPERATURE = "temperature"
DEFAULT_CONFIG_FILE_PREFIX = "hmip-config"
@@ -343,7 +343,7 @@ async def _async_reset_energy_counter(
await device.reset_energy_counter()
-def _get_home(hass: HomeAssistantType, hapid: str) -> Optional[AsyncHome]:
+def _get_home(hass: HomeAssistantType, hapid: str) -> AsyncHome | None:
"""Return a HmIP home."""
hap = hass.data[HMIPC_DOMAIN].get(hapid)
if hap:
diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py
index f8c37d336d507f..8172d64d35734d 100644
--- a/homeassistant/components/homematicip_cloud/switch.py
+++ b/homeassistant/components/homematicip_cloud/switch.py
@@ -1,5 +1,7 @@
"""Support for HomematicIP Cloud switches."""
-from typing import Any, Dict
+from __future__ import annotations
+
+from typing import Any
from homematicip.aio.device import (
AsyncBrandSwitchMeasuring,
@@ -141,9 +143,9 @@ def available(self) -> bool:
return True
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the switch-group."""
- state_attr = super().device_state_attributes
+ state_attr = super().extra_state_attributes
if self._device.unreach:
state_attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True
diff --git a/homeassistant/components/homematicip_cloud/translations/de.json b/homeassistant/components/homematicip_cloud/translations/de.json
index c421620fd98830..1da1e06c0fb8d8 100644
--- a/homeassistant/components/homematicip_cloud/translations/de.json
+++ b/homeassistant/components/homematicip_cloud/translations/de.json
@@ -2,11 +2,11 @@
"config": {
"abort": {
"already_configured": "Der Accesspoint ist bereits konfiguriert",
- "connection_aborted": "Konnte nicht mit HMIP Server verbinden",
- "unknown": "Ein unbekannter Fehler ist aufgetreten."
+ "connection_aborted": "Verbindung fehlgeschlagen",
+ "unknown": "Unerwarteter Fehler"
},
"error": {
- "invalid_sgtin_or_pin": "Ung\u00fcltige PIN, bitte versuche es erneut.",
+ "invalid_sgtin_or_pin": "Ung\u00fcltige SGTIN oder PIN-Code, bitte versuche es erneut.",
"press_the_button": "Bitte dr\u00fccke die blaue Taste.",
"register_failed": "Registrierung fehlgeschlagen, bitte versuche es erneut.",
"timeout_button": "Zeit\u00fcberschreitung beim Dr\u00fccken der blauen Taste. Bitte versuche es erneut."
@@ -16,7 +16,7 @@
"data": {
"hapid": "Accesspoint ID (SGTIN)",
"name": "Name (optional, wird als Pr\u00e4fix f\u00fcr alle Ger\u00e4te verwendet)",
- "pin": "PIN Code (optional)"
+ "pin": "PIN-Code"
},
"title": "HomematicIP Accesspoint ausw\u00e4hlen"
},
diff --git a/homeassistant/components/homematicip_cloud/translations/hu.json b/homeassistant/components/homematicip_cloud/translations/hu.json
index 1ae318c45ad028..eaa8d8834c36d0 100644
--- a/homeassistant/components/homematicip_cloud/translations/hu.json
+++ b/homeassistant/components/homematicip_cloud/translations/hu.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "A hozz\u00e1f\u00e9r\u00e9si pontot m\u00e1r konfigur\u00e1ltuk",
- "connection_aborted": "Nem siker\u00fclt csatlakozni a HMIP szerverhez",
- "unknown": "Unknown error occurred."
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "connection_aborted": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"error": {
- "invalid_sgtin_or_pin": "\u00c9rv\u00e9nytelen PIN, pr\u00f3b\u00e1lkozz \u00fajra.",
+ "invalid_sgtin_or_pin": "\u00c9rv\u00e9nytelen PIN-k\u00f3d, pr\u00f3b\u00e1lkozz \u00fajra.",
"press_the_button": "Nyomd meg a k\u00e9k gombot.",
"register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, pr\u00f3b\u00e1ld \u00fajra.",
"timeout_button": "K\u00e9k gomb megnyom\u00e1s\u00e1nak id\u0151t\u00fall\u00e9p\u00e9se, pr\u00f3b\u00e1lkozz \u00fajra."
@@ -16,7 +16,7 @@
"data": {
"hapid": "Hozz\u00e1f\u00e9r\u00e9si pont azonos\u00edt\u00f3ja (SGTIN)",
"name": "N\u00e9v (opcion\u00e1lis, minden eszk\u00f6z n\u00e9vel\u0151tagjak\u00e9nt haszn\u00e1latos)",
- "pin": "Pin k\u00f3d (opcion\u00e1lis)"
+ "pin": "PIN-k\u00f3d"
},
"title": "V\u00e1lassz HomematicIP hozz\u00e1f\u00e9r\u00e9si pontot"
},
diff --git a/homeassistant/components/homematicip_cloud/translations/id.json b/homeassistant/components/homematicip_cloud/translations/id.json
index 43525955c3674a..26679d3b37f21c 100644
--- a/homeassistant/components/homematicip_cloud/translations/id.json
+++ b/homeassistant/components/homematicip_cloud/translations/id.json
@@ -1,28 +1,28 @@
{
"config": {
"abort": {
- "already_configured": "Jalur akses sudah dikonfigurasi",
- "connection_aborted": "Tidak dapat terhubung ke server HMIP",
- "unknown": "Kesalahan tidak dikenal terjadi."
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "connection_aborted": "Gagal terhubung",
+ "unknown": "Kesalahan yang tidak diharapkan"
},
"error": {
- "invalid_sgtin_or_pin": "PIN tidak valid, silakan coba lagi.",
- "press_the_button": "Silakan tekan tombol biru.",
- "register_failed": "Gagal mendaftar, silakan coba lagi.",
- "timeout_button": "Batas waktu tekan tombol biru berakhir, silakan coba lagi."
+ "invalid_sgtin_or_pin": "SGTIN atau Kode PIN tidak valid, coba lagi.",
+ "press_the_button": "Tekan tombol biru.",
+ "register_failed": "Gagal mendaftar, coba lagi.",
+ "timeout_button": "Tenggang waktu penekanan tombol biru berakhir, coba lagi."
},
"step": {
"init": {
"data": {
"hapid": "Titik akses ID (SGTIN)",
- "name": "Nama (opsional, digunakan sebagai awalan nama untuk semua perangkat)",
- "pin": "Kode Pin (opsional)"
+ "name": "Nama (opsional, digunakan sebagai prefiks nama untuk semua perangkat)",
+ "pin": "Kode PIN"
},
- "title": "Pilih HomematicIP Access point"
+ "title": "Pilih Access Point HomematicIP"
},
"link": {
- "description": "Tekan tombol biru pada access point dan tombol submit untuk mendaftarkan HomematicIP dengan rumah asisten.\n\n! [Lokasi tombol di bridge] (/ static/images/config_flows/config_homematicip_cloud.png)",
- "title": "Tautkan jalur akses"
+ "description": "Tekan tombol biru pada access point dan tombol sukirimbmit untuk mendaftarkan HomematicIP dengan Home Assistant.\n\n",
+ "title": "Tautkan Titik akses"
}
}
}
diff --git a/homeassistant/components/homematicip_cloud/translations/ko.json b/homeassistant/components/homematicip_cloud/translations/ko.json
index b85b8ac00b1153..07f6c5c70fc741 100644
--- a/homeassistant/components/homematicip_cloud/translations/ko.json
+++ b/homeassistant/components/homematicip_cloud/translations/ko.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "connection_aborted": "HMIP \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
- "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "connection_aborted": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
- "invalid_sgtin_or_pin": "PIN\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "invalid_sgtin_or_pin": "SGTIN \ub610\ub294 PIN \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
"press_the_button": "\ud30c\ub780\uc0c9 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.",
"register_failed": "\ub4f1\ub85d\uc5d0 \uc2e4\ud328\ud558\uc600\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
"timeout_button": "\uc815\ud574\uc9c4 \uc2dc\uac04\ub0b4\uc5d0 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uc744 \ub20c\ub974\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694."
@@ -15,13 +15,13 @@
"init": {
"data": {
"hapid": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 ID (SGTIN)",
- "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d, \ubaa8\ub4e0 \uae30\uae30 \uc774\ub984\uc758 \uc811\ub450\uc5b4\ub85c \uc0ac\uc6a9)",
- "pin": "PIN \ucf54\ub4dc (\uc120\ud0dd\uc0ac\ud56d)"
+ "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d, \ubaa8\ub4e0 \uae30\uae30 \uc774\ub984\uc758 \uc811\ub450\uc0ac\ub85c \uc0ac\uc6a9)",
+ "pin": "PIN \ucf54\ub4dc"
},
"title": "HomematicIP \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 \uc120\ud0dd\ud558\uae30"
},
"link": {
- "description": "Home Assistant \uc5d0 HomematicIP \ub97c \ub4f1\ub85d\ud558\ub824\uba74 \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uc758 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uacfc \ud655\uc778\uc744 \ud074\ub9ad\ud574\uc8fc\uc138\uc694.\n\n",
+ "description": "Home Assistant\uc5d0 HomematicIP\ub97c \ub4f1\ub85d\ud558\ub824\uba74 \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uc758 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uacfc \ud655\uc778\uc744 \ud074\ub9ad\ud574\uc8fc\uc138\uc694.\n\n",
"title": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 \uc5f0\uacb0\ud558\uae30"
}
}
diff --git a/homeassistant/components/homematicip_cloud/translations/nl.json b/homeassistant/components/homematicip_cloud/translations/nl.json
index 7127b5c5aae98e..cb65dee7bd1135 100644
--- a/homeassistant/components/homematicip_cloud/translations/nl.json
+++ b/homeassistant/components/homematicip_cloud/translations/nl.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Accesspoint is al geconfigureerd",
- "connection_aborted": "Kon geen verbinding maken met de HMIP-server",
- "unknown": "Er is een onbekende fout opgetreden."
+ "already_configured": "Apparaat is al geconfigureerd",
+ "connection_aborted": "Kan geen verbinding maken",
+ "unknown": "Onverwachte fout"
},
"error": {
- "invalid_sgtin_or_pin": "Ongeldige PIN-code, probeer het nogmaals.",
+ "invalid_sgtin_or_pin": "Ongeldige SGTIN of PIN-code, probeer het opnieuw.",
"press_the_button": "Druk op de blauwe knop.",
"register_failed": "Kan niet registreren, gelieve opnieuw te proberen.",
"timeout_button": "Blauwe knop druk op timeout, probeer het opnieuw."
@@ -15,8 +15,8 @@
"init": {
"data": {
"hapid": "Accesspoint ID (SGTIN)",
- "name": "Naam (optioneel, gebruikt als naamprefix voor alle apparaten)",
- "pin": "Pin-Code (optioneel)"
+ "name": "Naam (optioneel, gebruikt als naamvoorvoegsel voor alle apparaten)",
+ "pin": "PIN-code"
},
"title": "Kies HomematicIP accesspoint"
},
diff --git a/homeassistant/components/homematicip_cloud/translations/tr.json b/homeassistant/components/homematicip_cloud/translations/tr.json
new file mode 100644
index 00000000000000..72f139217cace4
--- /dev/null
+++ b/homeassistant/components/homematicip_cloud/translations/tr.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "connection_aborted": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homematicip_cloud/translations/uk.json b/homeassistant/components/homematicip_cloud/translations/uk.json
new file mode 100644
index 00000000000000..1ed2e317f8b29e
--- /dev/null
+++ b/homeassistant/components/homematicip_cloud/translations/uk.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "connection_aborted": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "error": {
+ "invalid_sgtin_or_pin": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 SGTIN \u0430\u0431\u043e PIN-\u043a\u043e\u0434, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443.",
+ "press_the_button": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u0441\u0438\u043d\u044e \u043a\u043d\u043e\u043f\u043a\u0443.",
+ "register_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443.",
+ "timeout_button": "\u0412\u0438 \u043d\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u043b\u0438 \u043d\u0430 \u0441\u0438\u043d\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u0432 \u043c\u0435\u0436\u0430\u0445 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e\u0433\u043e \u0447\u0430\u0441\u0443, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "hapid": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0443 (SGTIN)",
+ "name": "\u041d\u0430\u0437\u0432\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u044f\u043a \u043f\u0440\u0435\u0444\u0456\u043a\u0441 \u0434\u043b\u044f \u043d\u0430\u0437\u0432\u0438 \u0432\u0441\u0456\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432)",
+ "pin": "PIN-\u043a\u043e\u0434"
+ },
+ "title": "HomematicIP Cloud"
+ },
+ "link": {
+ "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u0441\u0438\u043d\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0442\u043e\u0447\u0446\u0456 \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0456 \u043a\u043d\u043e\u043f\u043a\u0443 ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **, \u0449\u043e\u0431 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438 HomematicIP \u0432 Home Assistant. \n\n![\u0420\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043d\u043e\u043f\u043a\u0438] (/static/images/config_flows/config_homematicip_cloud.png)",
+ "title": "\u041f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u0442\u043e\u0447\u043a\u0443 \u0434\u043e\u0441\u0442\u0443\u043f\u0443"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py
index a5a3b9ed07713f..f62477148f72f2 100644
--- a/homeassistant/components/homeworks/light.py
+++ b/homeassistant/components/homeworks/light.py
@@ -82,7 +82,7 @@ def _set_brightness(self, level):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Supported attributes."""
return {"homeworks_address": self._addr}
diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py
index 4b87350aec8169..8053ad85502274 100644
--- a/homeassistant/components/honeywell/climate.py
+++ b/homeassistant/components/honeywell/climate.py
@@ -1,7 +1,9 @@
"""Support for Honeywell (US) Total Connect Comfort climate systems."""
+from __future__ import annotations
+
import datetime
import logging
-from typing import Any, Dict, List, Optional
+from typing import Any
import requests
import somecomfort
@@ -192,12 +194,12 @@ def __init__(
self._supported_features |= SUPPORT_FAN_MODE
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the honeywell, if any."""
return self._device.name
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the device specific state attributes."""
data = {}
data[ATTR_FAN_ACTION] = "running" if self._device.fan_running else "idle"
@@ -235,7 +237,7 @@ def temperature_unit(self) -> str:
return TEMP_CELSIUS if self._device.temperature_unit == "C" else TEMP_FAHRENHEIT
@property
- def current_humidity(self) -> Optional[int]:
+ def current_humidity(self) -> int | None:
"""Return the current humidity."""
return self._device.current_humidity
@@ -245,24 +247,24 @@ def hvac_mode(self) -> str:
return HW_MODE_TO_HVAC_MODE[self._device.system_mode]
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes."""
return list(self._hvac_mode_map)
@property
- def hvac_action(self) -> Optional[str]:
+ def hvac_action(self) -> str | None:
"""Return the current running hvac operation if supported."""
if self.hvac_mode == HVAC_MODE_OFF:
return None
return HW_MODE_TO_HA_HVAC_ACTION[self._device.equipment_output_status]
@property
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self._device.current_temperature
@property
- def target_temperature(self) -> Optional[float]:
+ def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
if self.hvac_mode == HVAC_MODE_COOL:
return self._device.setpoint_cool
@@ -271,41 +273,41 @@ def target_temperature(self) -> Optional[float]:
return None
@property
- def target_temperature_high(self) -> Optional[float]:
+ def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach."""
if self.hvac_mode == HVAC_MODE_HEAT_COOL:
return self._device.setpoint_cool
return None
@property
- def target_temperature_low(self) -> Optional[float]:
+ def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach."""
if self.hvac_mode == HVAC_MODE_HEAT_COOL:
return self._device.setpoint_heat
return None
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp."""
return PRESET_AWAY if self._away else None
@property
- def preset_modes(self) -> Optional[List[str]]:
+ def preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes."""
return [PRESET_NONE, PRESET_AWAY]
@property
- def is_aux_heat(self) -> Optional[str]:
+ def is_aux_heat(self) -> str | None:
"""Return true if aux heater."""
return self._device.system_mode == "emheat"
@property
- def fan_mode(self) -> Optional[str]:
+ def fan_mode(self) -> str | None:
"""Return the fan setting."""
return HW_FAN_MODE_TO_HA[self._device.fan_mode]
@property
- def fan_modes(self) -> Optional[List[str]]:
+ def fan_modes(self) -> list[str] | None:
"""Return the list of available fan modes."""
return list(self._fan_mode_map)
diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py
index 5b9fb6569381a3..e6eb211206d49d 100644
--- a/homeassistant/components/horizon/media_player.py
+++ b/homeassistant/components/horizon/media_player.py
@@ -184,9 +184,7 @@ def _send(self, key=None, channel=None):
elif channel:
self._client.select_channel(channel)
except OSError as msg:
- _LOGGER.error(
- "%s disconnected: %s. Trying to reconnect...", self._name, msg
- )
+ _LOGGER.error("%s disconnected: %s. Trying to reconnect", self._name, msg)
# for reconnect, first gracefully disconnect
self._client.disconnect()
diff --git a/homeassistant/components/hp_ilo/sensor.py b/homeassistant/components/hp_ilo/sensor.py
index da597acb8b7336..297bfa5264f21b 100644
--- a/homeassistant/components/hp_ilo/sensor.py
+++ b/homeassistant/components/hp_ilo/sensor.py
@@ -5,7 +5,7 @@
import hpilo
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_HOST,
CONF_MONITORED_VARIABLES,
@@ -18,7 +18,6 @@
CONF_VALUE_TEMPLATE,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -100,7 +99,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(devices, True)
-class HpIloSensor(Entity):
+class HpIloSensor(SensorEntity):
"""Representation of a HP iLO sensor."""
def __init__(
@@ -144,7 +143,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
return self._state_attributes
diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py
index c07cddb7a9cc56..b3d5a081d1b46b 100644
--- a/homeassistant/components/html5/notify.py
+++ b/homeassistant/components/html5/notify.py
@@ -1,4 +1,5 @@
"""HTML5 Push Messaging notification service."""
+from contextlib import suppress
from datetime import datetime, timedelta
from functools import partial
import json
@@ -26,6 +27,7 @@
BaseNotificationService,
)
from homeassistant.const import (
+ ATTR_NAME,
HTTP_BAD_REQUEST,
HTTP_INTERNAL_SERVER_ERROR,
HTTP_UNAUTHORIZED,
@@ -73,7 +75,6 @@ def gcm_api_deprecated(value):
ATTR_SUBSCRIPTION = "subscription"
ATTR_BROWSER = "browser"
-ATTR_NAME = "name"
ATTR_ENDPOINT = "endpoint"
ATTR_KEYS = "keys"
@@ -202,10 +203,8 @@ def websocket_appkey(hass, connection, msg):
def _load_config(filename):
"""Load configuration."""
- try:
+ with suppress(HomeAssistantError):
return load_json(filename)
- except HomeAssistantError:
- pass
return {}
@@ -325,10 +324,8 @@ def decode_jwt(self, token):
if target_check.get(ATTR_TARGET) in self.registrations:
possible_target = self.registrations[target_check[ATTR_TARGET]]
key = possible_target[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH]
- try:
+ with suppress(jwt.exceptions.DecodeError):
return jwt.decode(token, key, algorithms=["ES256", "HS256"])
- except jwt.exceptions.DecodeError:
- pass
return self.json_message(
"No target found in JWT", status_code=HTTP_UNAUTHORIZED
diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py
index 7f70d49f6861e5..5f57b4b77b8afc 100644
--- a/homeassistant/components/http/__init__.py
+++ b/homeassistant/components/http/__init__.py
@@ -1,11 +1,12 @@
"""Support to serve the Home Assistant API as WSGI application."""
+from __future__ import annotations
+
from contextvars import ContextVar
from ipaddress import ip_network
import logging
import os
import ssl
-from traceback import extract_stack
-from typing import Dict, Optional, cast
+from typing import Optional, cast
from aiohttp import web
from aiohttp.web_exceptions import HTTPMovedPermanently
@@ -58,8 +59,9 @@
_LOGGER = logging.getLogger(__name__)
DEFAULT_DEVELOPMENT = "0"
-# To be able to load custom cards.
-DEFAULT_CORS = "https://cast.home-assistant.io"
+# Cast to be able to load custom cards.
+# My to be able to check url and version info.
+DEFAULT_CORS = ["https://cast.home-assistant.io"]
NO_LOGIN_ATTEMPT_THRESHOLD = -1
MAX_CLIENT_SIZE: int = 1024 ** 2 * 16
@@ -80,7 +82,7 @@
vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile,
vol.Optional(CONF_SSL_KEY): cv.isfile,
- vol.Optional(CONF_CORS_ORIGINS, default=[DEFAULT_CORS]): vol.All(
+ vol.Optional(CONF_CORS_ORIGINS, default=DEFAULT_CORS): vol.All(
cv.ensure_list, [cv.string]
),
vol.Inclusive(CONF_USE_X_FORWARDED_FOR, "proxy"): cv.boolean,
@@ -102,7 +104,7 @@
@bind_hass
-async def async_get_last_config(hass: HomeAssistant) -> Optional[dict]:
+async def async_get_last_config(hass: HomeAssistant) -> dict | None:
"""Return the last known working config."""
store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY)
return cast(Optional[dict], await store.async_load())
@@ -115,7 +117,7 @@ def __init__(
self,
local_ip: str,
host: str,
- port: Optional[int] = SERVER_PORT,
+ port: int | None = SERVER_PORT,
use_ssl: bool = False,
) -> None:
"""Initialize a new API config object."""
@@ -124,69 +126,6 @@ def __init__(
self.port = port
self.use_ssl = use_ssl
- host = host.rstrip("/")
- if host.startswith(("http://", "https://")):
- self.deprecated_base_url = host
- elif use_ssl:
- self.deprecated_base_url = f"https://{host}"
- else:
- self.deprecated_base_url = f"http://{host}"
-
- if port is not None:
- self.deprecated_base_url += f":{port}"
-
- @property
- def base_url(self) -> str:
- """Proxy property to find caller of this deprecated property."""
- found_frame = None
- for frame in reversed(extract_stack()[:-1]):
- for path in ("custom_components/", "homeassistant/components/"):
- try:
- index = frame.filename.index(path)
-
- # Skip webhook from the stack
- if frame.filename[index:].startswith(
- "homeassistant/components/webhook/"
- ):
- continue
-
- found_frame = frame
- break
- except ValueError:
- continue
-
- if found_frame is not None:
- break
-
- # Did not source from an integration? Hard error.
- if found_frame is None:
- raise RuntimeError(
- "Detected use of deprecated `base_url` property in the Home Assistant core. Please report this issue."
- )
-
- # If a frame was found, it originated from an integration
- if found_frame:
- start = index + len(path)
- end = found_frame.filename.index("/", start)
-
- integration = found_frame.filename[start:end]
-
- if path == "custom_components/":
- extra = " to the custom component author"
- else:
- extra = ""
-
- _LOGGER.warning(
- "Detected use of deprecated `base_url` property, use `homeassistant.helpers.network.get_url` method instead. Please report issue%s for %s using this method at %s, line %s: %s",
- extra,
- integration,
- found_frame.filename[index:],
- found_frame.lineno,
- found_frame.line.strip(),
- )
-
- return self.deprecated_base_url
-
async def async_setup(hass, config):
"""Set up the HTTP API and debug interface."""
@@ -255,20 +194,16 @@ async def async_wait_frontend_load(event: Event) -> None:
hass.http = server
- host = conf.get(CONF_BASE_URL)
local_ip = await hass.async_add_executor_job(hass_util.get_local_ip)
- if host:
- port = None
- elif server_host is not None:
+ host = local_ip
+ if server_host is not None:
# Assume the first server host name provided as API host
host = server_host[0]
- port = server_port
- else:
- host = local_ip
- port = server_port
- hass.config.api = ApiConfig(local_ip, host, port, ssl_certificate is not None)
+ hass.config.api = ApiConfig(
+ local_ip, host, server_port, ssl_certificate is not None
+ )
return True
@@ -446,7 +381,7 @@ async def stop(self):
async def start_http_server_and_save_config(
- hass: HomeAssistant, conf: Dict, server: HomeAssistantHTTP
+ hass: HomeAssistant, conf: dict, server: HomeAssistantHTTP
) -> None:
"""Startup the http server and save the config."""
await server.start() # type: ignore
@@ -462,6 +397,6 @@ async def start_http_server_and_save_config(
await store.async_save(conf)
-current_request: ContextVar[Optional[web.Request]] = ContextVar(
+current_request: ContextVar[web.Request | None] = ContextVar(
"current_request", default=None
)
diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py
index 2e51dc35d8803a..5350ae5d4c81fc 100644
--- a/homeassistant/components/http/ban.py
+++ b/homeassistant/components/http/ban.py
@@ -1,10 +1,12 @@
"""Ban logic for HTTP component."""
+from __future__ import annotations
+
from collections import defaultdict
+from contextlib import suppress
from datetime import datetime
from ipaddress import ip_address
import logging
from socket import gethostbyaddr, herror
-from typing import List, Optional
from aiohttp.web import middleware
from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized
@@ -98,12 +100,10 @@ async def process_wrong_login(request):
remote_addr = ip_address(request.remote)
remote_host = request.remote
- try:
+ with suppress(herror):
remote_host, _, _ = await hass.async_add_executor_job(
gethostbyaddr, request.remote
)
- except herror:
- pass
base_msg = f"Login attempt or request with invalid authentication from {remote_host} ({remote_addr})."
@@ -178,15 +178,15 @@ async def process_success_login(request):
class IpBan:
"""Represents banned IP address."""
- def __init__(self, ip_ban: str, banned_at: Optional[datetime] = None) -> None:
+ def __init__(self, ip_ban: str, banned_at: datetime | None = None) -> None:
"""Initialize IP Ban object."""
self.ip_address = ip_address(ip_ban)
self.banned_at = banned_at or dt_util.utcnow()
-async def async_load_ip_bans_config(hass: HomeAssistant, path: str) -> List[IpBan]:
+async def async_load_ip_bans_config(hass: HomeAssistant, path: str) -> list[IpBan]:
"""Load list of banned IPs from config file."""
- ip_list: List[IpBan] = []
+ ip_list: list[IpBan] = []
try:
list_ = await hass.async_add_executor_job(load_yaml_config_file, path)
diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py
index 354159f13bef7b..b4dbb845638dce 100644
--- a/homeassistant/components/http/view.py
+++ b/homeassistant/components/http/view.py
@@ -1,8 +1,10 @@
"""Support for views."""
+from __future__ import annotations
+
import asyncio
import json
import logging
-from typing import Any, Callable, List, Optional
+from typing import Any, Callable
from aiohttp import web
from aiohttp.typedefs import LooseHeaders
@@ -26,8 +28,8 @@
class HomeAssistantView:
"""Base view for all views."""
- url: Optional[str] = None
- extra_urls: List[str] = []
+ url: str | None = None
+ extra_urls: list[str] = []
# Views inheriting from this class can override this
requires_auth = True
cors_allowed = False
@@ -45,7 +47,7 @@ def context(request: web.Request) -> Context:
def json(
result: Any,
status_code: int = HTTP_OK,
- headers: Optional[LooseHeaders] = None,
+ headers: LooseHeaders | None = None,
) -> web.Response:
"""Return a JSON response."""
try:
@@ -66,8 +68,8 @@ def json_message(
self,
message: str,
status_code: int = HTTP_OK,
- message_code: Optional[str] = None,
- headers: Optional[LooseHeaders] = None,
+ message_code: str | None = None,
+ headers: LooseHeaders | None = None,
) -> web.Response:
"""Return a JSON message response."""
data = {"message": message}
diff --git a/homeassistant/components/http/web_runner.py b/homeassistant/components/http/web_runner.py
index c30ba32b780544..f3dd59bf9d78b5 100644
--- a/homeassistant/components/http/web_runner.py
+++ b/homeassistant/components/http/web_runner.py
@@ -1,7 +1,8 @@
"""HomeAssistant specific aiohttp Site."""
+from __future__ import annotations
+
import asyncio
from ssl import SSLContext
-from typing import List, Optional, Union
from aiohttp import web
from yarl import URL
@@ -22,18 +23,18 @@ class HomeAssistantTCPSite(web.BaseSite):
__slots__ = ("_host", "_port", "_reuse_address", "_reuse_port", "_hosturl")
- def __init__(
+ def __init__( # noqa: D107
self,
- runner: "web.BaseRunner",
- host: Union[None, str, List[str]],
+ runner: web.BaseRunner,
+ host: None | str | list[str],
port: int,
*,
shutdown_timeout: float = 10.0,
- ssl_context: Optional[SSLContext] = None,
+ ssl_context: SSLContext | None = None,
backlog: int = 128,
- reuse_address: Optional[bool] = None,
- reuse_port: Optional[bool] = None,
- ) -> None: # noqa: D107
+ reuse_address: bool | None = None,
+ reuse_port: bool | None = None,
+ ) -> None:
super().__init__(
runner,
shutdown_timeout=shutdown_timeout,
diff --git a/homeassistant/components/htu21d/sensor.py b/homeassistant/components/htu21d/sensor.py
index a9b235faa0b270..d32eebf6d5fe54 100644
--- a/homeassistant/components/htu21d/sensor.py
+++ b/homeassistant/components/htu21d/sensor.py
@@ -4,13 +4,12 @@
import logging
from i2csense.htu21d import HTU21D # pylint: disable=import-error
-import smbus # pylint: disable=import-error
+import smbus
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, PERCENTAGE, TEMP_FAHRENHEIT
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from homeassistant.util.temperature import celsius_to_fahrenheit
@@ -70,7 +69,7 @@ def update(self):
self.sensor.update()
-class HTU21DSensor(Entity):
+class HTU21DSensor(SensorEntity):
"""Implementation of the HTU21D sensor."""
def __init__(self, htu21d_client, name, variable, unit):
diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py
index 341f9c0a118b26..67170aaf866477 100644
--- a/homeassistant/components/huawei_lte/__init__.py
+++ b/homeassistant/components/huawei_lte/__init__.py
@@ -1,12 +1,14 @@
"""Support for Huawei LTE routers."""
+from __future__ import annotations
from collections import defaultdict
+from contextlib import suppress
from datetime import timedelta
from functools import partial
import ipaddress
import logging
import time
-from typing import Any, Callable, Dict, List, Set, Tuple, cast
+from typing import Any, Callable, cast
from urllib.parse import urlparse
import attr
@@ -138,13 +140,13 @@ class Router:
mac: str = attr.ib()
signal_update: CALLBACK_TYPE = attr.ib()
- data: Dict[str, Any] = attr.ib(init=False, factory=dict)
- subscriptions: Dict[str, Set[str]] = attr.ib(
+ data: dict[str, Any] = attr.ib(init=False, factory=dict)
+ subscriptions: dict[str, set[str]] = attr.ib(
init=False,
factory=lambda: defaultdict(set, ((x, {"initial_scan"}) for x in ALL_KEYS)),
)
- inflight_gets: Set[str] = attr.ib(init=False, factory=set)
- unload_handlers: List[CALLBACK_TYPE] = attr.ib(init=False, factory=list)
+ inflight_gets: set[str] = attr.ib(init=False, factory=set)
+ unload_handlers: list[CALLBACK_TYPE] = attr.ib(init=False, factory=list)
client: Client
suspended = attr.ib(init=False, default=False)
notify_last_attempt: float = attr.ib(init=False, default=-1)
@@ -160,14 +162,12 @@ def device_name(self) -> str:
(KEY_DEVICE_BASIC_INFORMATION, "devicename"),
(KEY_DEVICE_INFORMATION, "DeviceName"),
):
- try:
+ with suppress(KeyError, TypeError):
return cast(str, self.data[key][item])
- except (KeyError, TypeError):
- pass
return DEFAULT_DEVICE_NAME
@property
- def device_identifiers(self) -> Set[Tuple[str, str]]:
+ def device_identifiers(self) -> set[tuple[str, str]]:
"""Get router identifiers for device registry."""
try:
return {(DOMAIN, self.data[KEY_DEVICE_INFORMATION]["SerialNumber"])}
@@ -175,7 +175,7 @@ def device_identifiers(self) -> Set[Tuple[str, str]]:
return set()
@property
- def device_connections(self) -> Set[Tuple[str, str]]:
+ def device_connections(self) -> set[tuple[str, str]]:
"""Get router connections for device registry."""
return {(dr.CONNECTION_NETWORK_MAC, self.mac)} if self.mac else set()
@@ -196,14 +196,14 @@ def _get_data(self, key: str, func: Callable[[], Any]) -> None:
self.subscriptions.pop(key)
except ResponseErrorLoginRequiredException:
if isinstance(self.connection, AuthorizedConnection):
- _LOGGER.debug("Trying to authorize again...")
+ _LOGGER.debug("Trying to authorize again")
if self.connection.enforce_authorized_connection():
_LOGGER.debug(
- "...success, %s will be updated by a future periodic run",
+ "success, %s will be updated by a future periodic run",
key,
)
else:
- _LOGGER.debug("...failed")
+ _LOGGER.debug("failed")
return
_LOGGER.info(
"%s requires authorization, excluding from future updates", key
@@ -304,8 +304,8 @@ class HuaweiLteData:
hass_config: dict = attr.ib()
# Our YAML config, keyed by router URL
- config: Dict[str, Dict[str, Any]] = attr.ib()
- routers: Dict[str, Router] = attr.ib(init=False, factory=dict)
+ config: dict[str, dict[str, Any]] = attr.ib()
+ routers: dict[str, Router] = attr.ib(init=False, factory=dict)
async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool:
@@ -484,7 +484,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
logging.getLogger("dicttoxml").setLevel(logging.WARNING)
# Arrange our YAML config to dict with normalized URLs as keys
- domain_config: Dict[str, Dict[str, Any]] = {}
+ domain_config: dict[str, dict[str, Any]] = {}
if DOMAIN not in hass.data:
hass.data[DOMAIN] = HuaweiLteData(hass_config=config, config=domain_config)
for router_config in config.get(DOMAIN, []):
@@ -588,7 +588,7 @@ class HuaweiLteBaseEntity(Entity):
router: Router = attr.ib()
_available: bool = attr.ib(init=False, default=True)
- _unsub_handlers: List[Callable] = attr.ib(init=False, factory=list)
+ _unsub_handlers: list[Callable] = attr.ib(init=False, factory=list)
@property
def _entity_name(self) -> str:
@@ -620,7 +620,7 @@ def should_poll(self) -> bool:
return False
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Get info for matching with parent router."""
return {
"identifiers": self.router.device_identifiers,
@@ -636,7 +636,6 @@ async def async_update_options(self, config_entry: ConfigEntry) -> None:
async def async_added_to_hass(self) -> None:
"""Connect to update signals."""
- assert self.hass is not None
self._unsub_handlers.append(
async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self._async_maybe_update)
)
diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py
index 9a5f148d1388d7..833a632b0dba26 100644
--- a/homeassistant/components/huawei_lte/binary_sensor.py
+++ b/homeassistant/components/huawei_lte/binary_sensor.py
@@ -1,7 +1,8 @@
"""Support for Huawei LTE binary sensors."""
+from __future__ import annotations
import logging
-from typing import Any, Callable, Dict, List, Optional
+from typing import Any, Callable
import attr
from huawei_lte_api.enums.cradle import ConnectionStatusEnum
@@ -29,11 +30,11 @@
async def async_setup_entry(
hass: HomeAssistantType,
config_entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up from config entry."""
router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]]
- entities: List[Entity] = []
+ entities: list[Entity] = []
if router.data.get(KEY_MONITORING_STATUS):
entities.append(HuaweiLteMobileConnectionBinarySensor(router))
@@ -53,7 +54,7 @@ class HuaweiLteBaseBinarySensor(HuaweiLteBaseEntity, BinarySensorEntity):
key: str
item: str
- _raw_state: Optional[str] = attr.ib(init=False, default=None)
+ _raw_state: str | None = attr.ib(init=False, default=None)
@property
def entity_registry_enabled_default(self) -> bool:
@@ -142,12 +143,10 @@ def entity_registry_enabled_default(self) -> bool:
return True
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Get additional attributes related to connection status."""
- attributes = super().device_state_attributes
+ attributes = {}
if self._raw_state in CONNECTION_STATE_ATTRIBUTES:
- if attributes is None:
- attributes = {}
attributes["additional_state"] = CONNECTION_STATE_ATTRIBUTES[
self._raw_state
]
diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py
index ba8baedcaf7a58..415e2ea2bc3bde 100644
--- a/homeassistant/components/huawei_lte/config_flow.py
+++ b/homeassistant/components/huawei_lte/config_flow.py
@@ -1,8 +1,9 @@
"""Config flow for the Huawei LTE platform."""
+from __future__ import annotations
from collections import OrderedDict
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from urllib.parse import urlparse
from huawei_lte_api.AuthorizedConnection import AuthorizedConnection
@@ -30,10 +31,12 @@
)
from homeassistant.core import callback
-from .const import CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME, DEFAULT_NOTIFY_SERVICE_NAME
-
-# see https://github.com/PyCQA/pylint/issues/3202 about the DOMAIN's pylint issue
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import (
+ CONNECTION_TIMEOUT,
+ DEFAULT_DEVICE_NAME,
+ DEFAULT_NOTIFY_SERVICE_NAME,
+ DOMAIN,
+)
_LOGGER = logging.getLogger(__name__)
@@ -48,15 +51,15 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
- ) -> "OptionsFlowHandler":
+ ) -> OptionsFlowHandler:
"""Get options flow."""
return OptionsFlowHandler(config_entry)
async def _async_show_user_form(
self,
- user_input: Optional[Dict[str, Any]] = None,
- errors: Optional[Dict[str, str]] = None,
- ) -> Dict[str, Any]:
+ user_input: dict[str, Any] | None = None,
+ errors: dict[str, str] | None = None,
+ ) -> dict[str, Any]:
if user_input is None:
user_input = {}
return self.async_show_form(
@@ -69,10 +72,7 @@ async def _async_show_user_form(
CONF_URL,
default=user_input.get(
CONF_URL,
- # https://github.com/PyCQA/pylint/issues/3167
- self.context.get( # pylint: disable=no-member
- CONF_URL, ""
- ),
+ self.context.get(CONF_URL, ""),
),
),
str,
@@ -96,12 +96,12 @@ async def _async_show_user_form(
)
async def async_step_import(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle import initiated config flow."""
return await self.async_step_user(user_input)
- def _already_configured(self, user_input: Dict[str, Any]) -> bool:
+ def _already_configured(self, user_input: dict[str, Any]) -> bool:
"""See if we already have a router matching user input configured."""
existing_urls = {
url_normalize(entry.data[CONF_URL], default_scheme="http")
@@ -110,8 +110,8 @@ def _already_configured(self, user_input: Dict[str, Any]) -> bool:
return user_input[CONF_URL] in existing_urls
async def async_step_user(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle user initiated config flow."""
if user_input is None:
return await self._async_show_user_form()
@@ -131,7 +131,7 @@ async def async_step_user(
if self._already_configured(user_input):
return self.async_abort(reason="already_configured")
- conn: Optional[Connection] = None
+ conn: Connection | None = None
def logout() -> None:
if isinstance(conn, AuthorizedConnection):
@@ -140,7 +140,7 @@ def logout() -> None:
except Exception: # pylint: disable=broad-except
_LOGGER.debug("Could not logout", exc_info=True)
- def try_connect(user_input: Dict[str, Any]) -> Connection:
+ def try_connect(user_input: dict[str, Any]) -> Connection:
"""Try connecting with given credentials."""
username = user_input.get(CONF_USERNAME)
password = user_input.get(CONF_PASSWORD)
@@ -191,7 +191,6 @@ def get_router_title(conn: Connection) -> str:
title = info.get("DeviceName")
return title or DEFAULT_DEVICE_NAME
- assert self.hass is not None
try:
conn = await self.hass.async_add_executor_job(try_connect, user_input)
except LoginErrorUsernameWrongException:
@@ -217,7 +216,6 @@ def get_router_title(conn: Connection) -> str:
user_input=user_input, errors=errors
)
- # pylint: disable=no-member
title = self.context.get("title_placeholders", {}).get(
CONF_NAME
) or await self.hass.async_add_executor_job(get_router_title, conn)
@@ -226,8 +224,8 @@ def get_router_title(conn: Connection) -> str:
return self.async_create_entry(title=title, data=user_input)
async def async_step_ssdp( # type: ignore # mypy says signature incompatible with supertype, but it's the same?
- self, discovery_info: Dict[str, Any]
- ) -> Dict[str, Any]:
+ self, discovery_info: dict[str, Any]
+ ) -> dict[str, Any]:
"""Handle SSDP initiated config flow."""
await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN])
self._abort_if_unique_id_configured()
@@ -237,8 +235,7 @@ async def async_step_ssdp( # type: ignore # mypy says signature incompatible w
if "mobile" not in discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "").lower():
return self.async_abort(reason="not_huawei_lte")
- # https://github.com/PyCQA/pylint/issues/3167
- url = self.context[CONF_URL] = url_normalize( # pylint: disable=no-member
+ url = self.context[CONF_URL] = url_normalize(
discovery_info.get(
ssdp.ATTR_UPNP_PRESENTATION_URL,
f"http://{urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname}/",
@@ -254,7 +251,6 @@ async def async_step_ssdp( # type: ignore # mypy says signature incompatible w
if self._already_configured(user_input):
return self.async_abort(reason="already_configured")
- # pylint: disable=no-member
self.context["title_placeholders"] = {
CONF_NAME: discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME)
}
@@ -269,8 +265,8 @@ def __init__(self, config_entry: config_entries.ConfigEntry):
self.config_entry = config_entry
async def async_step_init(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle options flow."""
# Recipients are persisted as a list, but handled as comma separated string in UI
diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py
index 52e59b713dd4aa..b042c0c2912eff 100644
--- a/homeassistant/components/huawei_lte/device_tracker.py
+++ b/homeassistant/components/huawei_lte/device_tracker.py
@@ -1,8 +1,9 @@
"""Support for device tracking of Huawei LTE routers."""
+from __future__ import annotations
import logging
import re
-from typing import Any, Callable, Dict, List, Optional, Set, cast
+from typing import Any, Callable, cast
import attr
from stringcase import snakecase
@@ -31,7 +32,7 @@
async def async_setup_entry(
hass: HomeAssistantType,
config_entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up from config entry."""
@@ -46,9 +47,9 @@ async def async_setup_entry(
return
# Initialize already tracked entities
- tracked: Set[str] = set()
+ tracked: set[str] = set()
registry = await entity_registry.async_get_registry(hass)
- known_entities: List[Entity] = []
+ known_entities: list[Entity] = []
for entity in registry.entities.values():
if (
entity.domain == DEVICE_TRACKER_DOMAIN
@@ -82,8 +83,8 @@ async def _async_maybe_add_new_entities(url: str) -> None:
def async_add_new_entities(
hass: HomeAssistantType,
router_url: str,
- async_add_entities: Callable[[List[Entity], bool], None],
- tracked: Set[str],
+ async_add_entities: Callable[[list[Entity], bool], None],
+ tracked: set[str],
) -> None:
"""Add new entities that are not already being tracked."""
router = hass.data[DOMAIN].routers[router_url]
@@ -93,7 +94,7 @@ def async_add_new_entities(
_LOGGER.debug("%s[%s][%s] not in data", KEY_WLAN_HOST_LIST, "Hosts", "Host")
return
- new_entities: List[Entity] = []
+ new_entities: list[Entity] = []
for host in (x for x in hosts if x.get("MacAddress")):
entity = HuaweiLteScannerEntity(router, host["MacAddress"])
if entity.unique_id in tracked:
@@ -125,12 +126,12 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity):
mac: str = attr.ib()
_is_connected: bool = attr.ib(init=False, default=False)
- _hostname: Optional[str] = attr.ib(init=False, default=None)
- _device_state_attributes: Dict[str, Any] = attr.ib(init=False, factory=dict)
+ _hostname: str | None = attr.ib(init=False, default=None)
+ _extra_state_attributes: dict[str, Any] = attr.ib(init=False, factory=dict)
def __attrs_post_init__(self) -> None:
"""Initialize internal state."""
- self._device_state_attributes["mac_address"] = self.mac
+ self._extra_state_attributes["mac_address"] = self.mac
@property
def _entity_name(self) -> str:
@@ -151,9 +152,9 @@ def is_connected(self) -> bool:
return self._is_connected
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Get additional attributes related to entity state."""
- return self._device_state_attributes
+ return self._extra_state_attributes
async def async_update(self) -> None:
"""Update state."""
@@ -162,6 +163,6 @@ async def async_update(self) -> None:
self._is_connected = host is not None
if host is not None:
self._hostname = host.get("HostName")
- self._device_state_attributes = {
+ self._extra_state_attributes = {
_better_snakecase(k): v for k, v in host.items() if k != "HostName"
}
diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py
index 5659e66ea98dfb..ea7b5d9f6ab8b2 100644
--- a/homeassistant/components/huawei_lte/notify.py
+++ b/homeassistant/components/huawei_lte/notify.py
@@ -1,8 +1,9 @@
"""Support for Huawei LTE router notifications."""
+from __future__ import annotations
import logging
import time
-from typing import Any, Dict, List, Optional
+from typing import Any
import attr
from huawei_lte_api.exceptions import ResponseErrorException
@@ -19,9 +20,9 @@
async def async_get_service(
hass: HomeAssistantType,
- config: Dict[str, Any],
- discovery_info: Optional[Dict[str, Any]] = None,
-) -> Optional["HuaweiLteSmsNotificationService"]:
+ config: dict[str, Any],
+ discovery_info: dict[str, Any] | None = None,
+) -> HuaweiLteSmsNotificationService | None:
"""Get the notification service."""
if discovery_info is None:
return None
@@ -37,7 +38,7 @@ class HuaweiLteSmsNotificationService(BaseNotificationService):
"""Huawei LTE router SMS notification service."""
router: Router = attr.ib()
- default_targets: List[str] = attr.ib()
+ default_targets: list[str] = attr.ib()
def send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send message to target numbers."""
diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py
index 3815ac831b5546..c6cb93f0e678ab 100644
--- a/homeassistant/components/huawei_lte/sensor.py
+++ b/homeassistant/components/huawei_lte/sensor.py
@@ -1,9 +1,10 @@
"""Support for Huawei LTE sensors."""
+from __future__ import annotations
from bisect import bisect
import logging
import re
-from typing import Callable, Dict, List, NamedTuple, Optional, Pattern, Tuple, Union
+from typing import Callable, NamedTuple, Pattern
import attr
@@ -11,6 +12,7 @@
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_SIGNAL_STRENGTH,
DOMAIN as SENSOR_DOMAIN,
+ SensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -45,17 +47,17 @@
class SensorMeta(NamedTuple):
"""Metadata for defining sensors."""
- name: Optional[str] = None
- device_class: Optional[str] = None
- icon: Union[str, Callable[[StateType], str], None] = None
- unit: Optional[str] = None
+ name: str | None = None
+ device_class: str | None = None
+ icon: str | Callable[[StateType], str] | None = None
+ unit: str | None = None
enabled_default: bool = False
- include: Optional[Pattern[str]] = None
- exclude: Optional[Pattern[str]] = None
- formatter: Optional[Callable[[str], Tuple[StateType, Optional[str]]]] = None
+ include: Pattern[str] | None = None
+ exclude: Pattern[str] | None = None
+ formatter: Callable[[str], tuple[StateType, str | None]] | None = None
-SENSOR_META: Dict[Union[str, Tuple[str, str]], SensorMeta] = {
+SENSOR_META: dict[str | tuple[str, str], SensorMeta] = {
KEY_DEVICE_INFORMATION: SensorMeta(
include=re.compile(r"^WanIP.*Address$", re.IGNORECASE)
),
@@ -329,11 +331,11 @@ class SensorMeta(NamedTuple):
async def async_setup_entry(
hass: HomeAssistantType,
config_entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up from config entry."""
router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]]
- sensors: List[Entity] = []
+ sensors: list[Entity] = []
for key in SENSOR_KEYS:
items = router.data.get(key)
if not items:
@@ -354,7 +356,7 @@ async def async_setup_entry(
async_add_entities(sensors, True)
-def format_default(value: StateType) -> Tuple[StateType, Optional[str]]:
+def format_default(value: StateType) -> tuple[StateType, str | None]:
"""Format value."""
unit = None
if value is not None:
@@ -372,7 +374,7 @@ def format_default(value: StateType) -> Tuple[StateType, Optional[str]]:
@attr.s
-class HuaweiLteSensor(HuaweiLteBaseEntity):
+class HuaweiLteSensor(HuaweiLteBaseEntity, SensorEntity):
"""Huawei LTE sensor entity."""
key: str = attr.ib()
@@ -380,7 +382,7 @@ class HuaweiLteSensor(HuaweiLteBaseEntity):
meta: SensorMeta = attr.ib()
_state: StateType = attr.ib(init=False, default=STATE_UNKNOWN)
- _unit: Optional[str] = attr.ib(init=False)
+ _unit: str | None = attr.ib(init=False)
async def async_added_to_hass(self) -> None:
"""Subscribe to needed data on add."""
@@ -406,17 +408,17 @@ def state(self) -> StateType:
return self._state
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return sensor device class."""
return self.meta.device_class
@property
- def unit_of_measurement(self) -> Optional[str]:
+ def unit_of_measurement(self) -> str | None:
"""Return sensor's unit of measurement."""
return self.meta.unit or self._unit
@property
- def icon(self) -> Optional[str]:
+ def icon(self) -> str | None:
"""Return icon for sensor."""
icon = self.meta.icon
if callable(icon):
diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py
index 4dfa1e32df25d3..9279226e8eccb5 100644
--- a/homeassistant/components/huawei_lte/switch.py
+++ b/homeassistant/components/huawei_lte/switch.py
@@ -1,7 +1,8 @@
"""Support for Huawei LTE switches."""
+from __future__ import annotations
import logging
-from typing import Any, Callable, List, Optional
+from typing import Any, Callable
import attr
@@ -24,11 +25,11 @@
async def async_setup_entry(
hass: HomeAssistantType,
config_entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up from config entry."""
router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]]
- switches: List[Entity] = []
+ switches: list[Entity] = []
if router.data.get(KEY_DIALUP_MOBILE_DATASWITCH):
switches.append(HuaweiLteMobileDataSwitch(router))
@@ -42,7 +43,7 @@ class HuaweiLteBaseSwitch(HuaweiLteBaseEntity, SwitchEntity):
key: str
item: str
- _raw_state: Optional[str] = attr.ib(init=False, default=None)
+ _raw_state: str | None = attr.ib(init=False, default=None)
def _turn(self, state: bool) -> None:
raise NotImplementedError
diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json
index 7da997f12d6645..43361e46929aa1 100644
--- a/homeassistant/components/huawei_lte/translations/de.json
+++ b/homeassistant/components/huawei_lte/translations/de.json
@@ -1,14 +1,15 @@
{
"config": {
"abort": {
- "already_configured": "Dieses Ger\u00e4t wurde bereits konfiguriert",
- "already_in_progress": "Dieses Ger\u00e4t wurde bereits konfiguriert",
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
"not_huawei_lte": "Kein Huawei LTE-Ger\u00e4t"
},
"error": {
"connection_timeout": "Verbindungszeit\u00fcberschreitung",
"incorrect_password": "Ung\u00fcltiges Passwort",
"incorrect_username": "Ung\u00fcltiger Benutzername",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"invalid_url": "Ung\u00fcltige URL",
"login_attempts_exceeded": "Maximale Anzahl von Anmeldeversuchen \u00fcberschritten. Bitte versuche es sp\u00e4ter erneut",
"response_error": "Unbekannter Fehler vom Ger\u00e4t",
diff --git a/homeassistant/components/huawei_lte/translations/he.json b/homeassistant/components/huawei_lte/translations/he.json
new file mode 100644
index 00000000000000..6f4191da70d538
--- /dev/null
+++ b/homeassistant/components/huawei_lte/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json
index 90814fea5a529b..815794133d2b01 100644
--- a/homeassistant/components/huawei_lte/translations/hu.json
+++ b/homeassistant/components/huawei_lte/translations/hu.json
@@ -1,20 +1,24 @@
{
"config": {
"abort": {
- "already_configured": "Ez az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
- "already_in_progress": "Ez az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
"not_huawei_lte": "Nem Huawei LTE eszk\u00f6z"
},
"error": {
"connection_timeout": "Kapcsolat id\u0151t\u00fall\u00e9p\u00e9se",
"incorrect_password": "Hib\u00e1s jelsz\u00f3",
"incorrect_username": "Helytelen felhaszn\u00e1l\u00f3n\u00e9v",
- "invalid_url": "\u00c9rv\u00e9nytelen URL"
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "invalid_url": "\u00c9rv\u00e9nytelen URL",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
+ "flow_title": "Huawei LTE: {name}",
"step": {
"user": {
"data": {
"password": "Jelsz\u00f3",
+ "url": "URL",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
},
"title": "Huawei LTE konfigur\u00e1l\u00e1sa"
diff --git a/homeassistant/components/huawei_lte/translations/id.json b/homeassistant/components/huawei_lte/translations/id.json
new file mode 100644
index 00000000000000..2077b31ccd7d60
--- /dev/null
+++ b/homeassistant/components/huawei_lte/translations/id.json
@@ -0,0 +1,42 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "not_huawei_lte": "Bukan perangkat Huawei LTE"
+ },
+ "error": {
+ "connection_timeout": "Tenggang waktu terhubung habis",
+ "incorrect_password": "Kata sandi salah",
+ "incorrect_username": "Nama pengguna salah",
+ "invalid_auth": "Autentikasi tidak valid",
+ "invalid_url": "URL tidak valid",
+ "login_attempts_exceeded": "Upaya login maksimum telah terlampaui, coba lagi nanti",
+ "response_error": "Kesalahan tidak dikenal dari perangkat",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "flow_title": "Huawei LTE: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "url": "URL",
+ "username": "Nama Pengguna"
+ },
+ "description": "Masukkan detail akses perangkat. Menentukan nama pengguna dan kata sandi bersifat opsional, tetapi memungkinkan dukungan untuk fitur integrasi lainnya. Selain itu, penggunaan koneksi resmi dapat menyebabkan masalah mengakses antarmuka web perangkat dari luar Home Assistant saat integrasi aktif, dan sebaliknya.",
+ "title": "Konfigurasikan Huawei LTE"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "name": "Nama layanan notifikasi (perubahan harus dimulai ulang)",
+ "recipient": "Penerima notifikasi SMS",
+ "track_new_devices": "Lacak perangkat baru"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huawei_lte/translations/ko.json b/homeassistant/components/huawei_lte/translations/ko.json
index 6469bf6a696a21..ab108964ed88f6 100644
--- a/homeassistant/components/huawei_lte/translations/ko.json
+++ b/homeassistant/components/huawei_lte/translations/ko.json
@@ -2,25 +2,28 @@
"config": {
"abort": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "already_in_progress": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
"not_huawei_lte": "\ud654\uc6e8\uc774 LTE \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4"
},
"error": {
"connection_timeout": "\uc811\uc18d \uc2dc\uac04 \ucd08\uacfc",
"incorrect_password": "\ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4",
"incorrect_username": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"invalid_url": "URL \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"login_attempts_exceeded": "\ucd5c\ub300 \ub85c\uadf8\uc778 \uc2dc\ub3c4 \ud69f\uc218\ub97c \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694",
- "response_error": "\uae30\uae30\uc5d0\uc11c \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ "response_error": "\uae30\uae30\uc5d0\uc11c \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"flow_title": "Huawei LTE: {name}",
"step": {
"user": {
"data": {
"password": "\ube44\ubc00\ubc88\ud638",
+ "url": "URL \uc8fc\uc18c",
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
},
- "description": "\uae30\uae30 \uc561\uc138\uc2a4 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc124\uc815\ud558\ub294 \uac83\uc740 \uc120\ud0dd \uc0ac\ud56d\uc774\uc9c0\ub9cc \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc9c0\uc6d0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ubc18\uba74, \uc778\uc99d\ub41c \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uba74, \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \ud65c\uc131\ud654\ub41c \uc0c1\ud0dc\uc5d0\uc11c \ub2e4\ub978 \ubc29\ubc95\uc73c\ub85c Home Assistant \uc758 \uc678\ubd80\uc5d0\uc11c \uae30\uae30\uc758 \uc6f9 \uc778\ud130\ud398\uc774\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud558\ub294 \ub370 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "description": "\uae30\uae30 \uc811\uadfc\uc5d0 \ub300\ud55c \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc124\uc815\ud558\ub294 \uac83\uc740 \uc120\ud0dd \uc0ac\ud56d\uc774\uc9c0\ub9cc \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc9c0\uc6d0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ubc18\uba74, \uc778\uc99d\ub41c \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uba74, \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \ud65c\uc131\ud654\ub41c \uc0c1\ud0dc\uc5d0\uc11c \ub2e4\ub978 \ubc29\ubc95\uc73c\ub85c Home Assistant\uc758 \uc678\ubd80\uc5d0\uc11c \uae30\uae30\uc758 \uc6f9 \uc778\ud130\ud398\uc774\uc2a4\uc5d0 \uc811\uadfc\ud558\ub294 \ub370 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
"title": "Huawei LTE \uc124\uc815\ud558\uae30"
}
}
diff --git a/homeassistant/components/huawei_lte/translations/nl.json b/homeassistant/components/huawei_lte/translations/nl.json
index dd51fdc1bc5edb..799a9ce50af1e1 100644
--- a/homeassistant/components/huawei_lte/translations/nl.json
+++ b/homeassistant/components/huawei_lte/translations/nl.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Dit apparaat is reeds geconfigureerd",
- "already_in_progress": "Dit apparaat wordt al geconfigureerd",
+ "already_configured": "Apparaat is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
"not_huawei_lte": "Geen Huawei LTE-apparaat"
},
"error": {
@@ -15,10 +15,12 @@
"response_error": "Onbekende fout van het apparaat",
"unknown": "Onverwachte fout"
},
+ "flow_title": "Huawei LTE: {name}",
"step": {
"user": {
"data": {
"password": "Wachtwoord",
+ "url": "URL",
"username": "Gebruikersnaam"
},
"description": "Voer de toegangsgegevens van het apparaat in. Opgeven van gebruikersnaam en wachtwoord is optioneel, maar biedt ondersteuning voor meer integratiefuncties. Aan de andere kant kan het gebruik van een geautoriseerde verbinding problemen veroorzaken bij het openen van het webinterface van het apparaat buiten de Home Assitant, terwijl de integratie actief is en andersom.",
diff --git a/homeassistant/components/huawei_lte/translations/ru.json b/homeassistant/components/huawei_lte/translations/ru.json
index 884574577965dd..d3f95e3fbf1bbe 100644
--- a/homeassistant/components/huawei_lte/translations/ru.json
+++ b/homeassistant/components/huawei_lte/translations/ru.json
@@ -8,8 +8,8 @@
"error": {
"connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.",
"incorrect_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.",
- "incorrect_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "incorrect_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"invalid_url": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.",
"login_attempts_exceeded": "\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u0432\u0445\u043e\u0434\u0430, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.",
"response_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.",
@@ -21,9 +21,9 @@
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"url": "URL-\u0430\u0434\u0440\u0435\u0441",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
- "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u0423\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043d\u043e \u044d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438. \u0421 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u044b, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u0435\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c \u043a \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437 Home Assistant, \u043a\u043e\u0433\u0434\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0430\u043a\u0442\u0438\u0432\u043d\u0430, \u0438 \u043d\u0430\u043e\u0431\u043e\u0440\u043e\u0442.",
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u0423\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043d\u043e \u044d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438. \u0421 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u044b, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u0435\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c \u043a \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437 Home Assistant, \u043a\u043e\u0433\u0434\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0430\u043a\u0442\u0438\u0432\u043d\u0430, \u0438 \u043d\u0430\u043e\u0431\u043e\u0440\u043e\u0442.",
"title": "Huawei LTE"
}
}
diff --git a/homeassistant/components/huawei_lte/translations/tr.json b/homeassistant/components/huawei_lte/translations/tr.json
index a76e31fa483542..ba934acc39b8c8 100644
--- a/homeassistant/components/huawei_lte/translations/tr.json
+++ b/homeassistant/components/huawei_lte/translations/tr.json
@@ -1,8 +1,35 @@
{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor"
+ },
+ "error": {
+ "connection_timeout": "Ba\u011flant\u0131 zamana\u015f\u0131m\u0131",
+ "incorrect_password": "Yanl\u0131\u015f parola",
+ "incorrect_username": "Yanl\u0131\u015f kullan\u0131c\u0131 ad\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "invalid_url": "Ge\u00e7ersiz URL",
+ "login_attempts_exceeded": "Maksimum oturum a\u00e7ma denemesi a\u015f\u0131ld\u0131, l\u00fctfen daha sonra tekrar deneyin",
+ "response_error": "Cihazdan bilinmeyen hata",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "url": "URL",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ },
+ "description": "Cihaz eri\u015fim ayr\u0131nt\u0131lar\u0131n\u0131 girin. Kullan\u0131c\u0131 ad\u0131 ve parolan\u0131n belirtilmesi iste\u011fe ba\u011fl\u0131d\u0131r, ancak daha fazla entegrasyon \u00f6zelli\u011fi i\u00e7in destek sa\u011flar. \u00d6te yandan, yetkili bir ba\u011flant\u0131n\u0131n kullan\u0131lmas\u0131, entegrasyon aktifken Ev Asistan\u0131 d\u0131\u015f\u0131ndan cihaz web aray\u00fcz\u00fcne eri\u015fimde sorunlara neden olabilir ve tam tersi."
+ }
+ }
+ },
"options": {
"step": {
"init": {
"data": {
+ "recipient": "SMS bildirimi al\u0131c\u0131lar\u0131",
"track_new_devices": "Yeni cihazlar\u0131 izle"
}
}
diff --git a/homeassistant/components/huawei_lte/translations/uk.json b/homeassistant/components/huawei_lte/translations/uk.json
new file mode 100644
index 00000000000000..17f3d3b71c3386
--- /dev/null
+++ b/homeassistant/components/huawei_lte/translations/uk.json
@@ -0,0 +1,42 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "not_huawei_lte": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0454 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c Huawei LTE"
+ },
+ "error": {
+ "connection_timeout": "\u0427\u0430\u0441 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043c\u0438\u043d\u0443\u0432.",
+ "incorrect_password": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.",
+ "incorrect_username": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0456\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "invalid_url": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430 URL-\u0430\u0434\u0440\u0435\u0441\u0430.",
+ "login_attempts_exceeded": "\u041f\u0435\u0440\u0435\u0432\u0438\u0449\u0435\u043d\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0443 \u043a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0441\u043f\u0440\u043e\u0431 \u0432\u0445\u043e\u0434\u0443, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437 \u043f\u0456\u0437\u043d\u0456\u0448\u0435.",
+ "response_error": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "flow_title": "Huawei LTE: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u0430\u043d\u0456 \u0434\u043b\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \u0412\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043b\u043e\u0433\u0456\u043d \u0456 \u043f\u0430\u0440\u043e\u043b\u044c \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0430\u043b\u0435 \u0446\u0435 \u0434\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u044c \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u0444\u0443\u043d\u043a\u0446\u0456\u0457. \u0417 \u0456\u043d\u0448\u043e\u0433\u043e \u0431\u043e\u043a\u0443, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043e\u0433\u043e \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u043c\u043e\u0436\u0435 \u0432\u0438\u043a\u043b\u0438\u043a\u0430\u0442\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0437 \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c \u0434\u043e \u0432\u0435\u0431-\u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u0437 Home Assistant, \u043a\u043e\u043b\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0430\u043a\u0442\u0438\u0432\u043d\u0430, \u0456 \u043d\u0430\u0432\u043f\u0430\u043a\u0438.",
+ "title": "Huawei LTE"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "name": "\u041d\u0430\u0437\u0432\u0430 \u0441\u043b\u0443\u0436\u0431\u0438 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u044c (\u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a)",
+ "recipient": "\u041e\u0434\u0435\u0440\u0436\u0443\u0432\u0430\u0447\u0456 SMS-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c",
+ "track_new_devices": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u0442\u0438 \u043d\u043e\u0432\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py
index cfbe041aafe4e6..e408e995ad42f4 100644
--- a/homeassistant/components/hue/binary_sensor.py
+++ b/homeassistant/components/hue/binary_sensor.py
@@ -31,9 +31,9 @@ def is_on(self):
return self.sensor.presence
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
- attributes = super().device_state_attributes
+ attributes = super().extra_state_attributes
if "sensitivity" in self.sensor.config:
attributes["sensitivity"] = self.sensor.config["sensitivity"]
if "sensitivitymax" in self.sensor.config:
diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py
index 880d9abcc350c5..c14caa89620e42 100644
--- a/homeassistant/components/hue/bridge.py
+++ b/homeassistant/components/hue/bridge.py
@@ -28,8 +28,13 @@
SERVICE_HUE_SCENE = "hue_activate_scene"
ATTR_GROUP_NAME = "group_name"
ATTR_SCENE_NAME = "scene_name"
+ATTR_TRANSITION = "transition"
SCENE_SCHEMA = vol.Schema(
- {vol.Required(ATTR_GROUP_NAME): cv.string, vol.Required(ATTR_SCENE_NAME): cv.string}
+ {
+ vol.Required(ATTR_GROUP_NAME): cv.string,
+ vol.Required(ATTR_SCENE_NAME): cv.string,
+ vol.Optional(ATTR_TRANSITION): cv.positive_int,
+ }
)
# How long should we sleep if the hub is busy
HUB_BUSY_SLEEP = 0.5
@@ -201,6 +206,7 @@ async def hue_activate_scene(self, call, updated=False, hide_warnings=False):
"""Service to call directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME]
scene_name = call.data[ATTR_SCENE_NAME]
+ transition = call.data.get(ATTR_TRANSITION)
group = next(
(group for group in self.api.groups.values() if group.name == group_name),
@@ -236,7 +242,9 @@ async def hue_activate_scene(self, call, updated=False, hide_warnings=False):
LOGGER.warning("Unable to find scene %s", scene_name)
return False
- return await self.async_request_call(partial(group.set_action, scene=scene.id))
+ return await self.async_request_call(
+ partial(group.set_action, scene=scene.id, transitiontime=transition)
+ )
async def handle_unauthorized_error(self):
"""Create a new config flow when the authorization is no longer valid."""
@@ -244,7 +252,7 @@ async def handle_unauthorized_error(self):
# we already created a new config flow, no need to do it again
return
LOGGER.error(
- "Unable to authorize to bridge %s, setup the linking again.", self.host
+ "Unable to authorize to bridge %s, setup the linking again", self.host
)
self.authorized = False
create_config_flow(self.hass, self.host)
diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py
index ecb3fd8c48901a..9fd025d7b6ad77 100644
--- a/homeassistant/components/hue/config_flow.py
+++ b/homeassistant/components/hue/config_flow.py
@@ -1,6 +1,8 @@
"""Config flow to configure Philips Hue."""
+from __future__ import annotations
+
import asyncio
-from typing import Any, Dict, Optional
+from typing import Any
from urllib.parse import urlparse
import aiohue
@@ -15,15 +17,17 @@
from homeassistant.helpers import aiohttp_client
from .bridge import authenticate_bridge
-from .const import ( # pylint: disable=unused-import
+from .const import (
CONF_ALLOW_HUE_GROUPS,
CONF_ALLOW_UNREACHABLE,
+ DEFAULT_ALLOW_HUE_GROUPS,
+ DEFAULT_ALLOW_UNREACHABLE,
DOMAIN,
LOGGER,
)
from .errors import AuthenticationRequired, CannotConnect
-HUE_MANUFACTURERURL = "http://www.philips.com"
+HUE_MANUFACTURERURL = ("http://www.philips.com", "http://www.philips-hue.com")
HUE_IGNORED_BRIDGE_NAMES = ["Home Assistant Bridge", "Espalexa"]
HUE_MANUAL_BRIDGE_ID = "manual"
@@ -42,8 +46,8 @@ def async_get_options_flow(config_entry):
def __init__(self):
"""Initialize the Hue flow."""
- self.bridge: Optional[aiohue.Bridge] = None
- self.discovered_bridges: Optional[Dict[str, aiohue.Bridge]] = None
+ self.bridge: aiohue.Bridge | None = None
+ self.discovered_bridges: dict[str, aiohue.Bridge] | None = None
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
@@ -51,7 +55,7 @@ async def async_step_user(self, user_input=None):
return await self.async_step_init(user_input)
@core.callback
- def _async_get_bridge(self, host: str, bridge_id: Optional[str] = None):
+ def _async_get_bridge(self, host: str, bridge_id: str | None = None):
"""Return a bridge object."""
if bridge_id is not None:
bridge_id = normalize_bridge_id(bridge_id)
@@ -112,8 +116,8 @@ async def async_step_init(self, user_input=None):
)
async def async_step_manual(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle manual bridge setup."""
if user_input is None:
return self.async_show_form(
@@ -177,7 +181,10 @@ async def async_step_ssdp(self, discovery_info):
host is already configured and delegate to the import step if not.
"""
# Filter out non-Hue bridges #1
- if discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER_URL) != HUE_MANUFACTURERURL:
+ if (
+ discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER_URL)
+ not in HUE_MANUFACTURERURL
+ ):
return self.async_abort(reason="not_hue_bridge")
# Filter out non-Hue bridges #2
@@ -205,6 +212,17 @@ async def async_step_ssdp(self, discovery_info):
self.bridge = bridge
return await self.async_step_link()
+ async def async_step_homekit(self, discovery_info):
+ """Handle a discovered Hue bridge on HomeKit.
+
+ The bridge ID communicated over HomeKit differs, so we cannot use that
+ as the unique identifier. Therefore, this method uses discovery without
+ a unique ID.
+ """
+ self.bridge = self._async_get_bridge(discovery_info[CONF_HOST])
+ await self._async_handle_discovery_without_unique_id()
+ return await self.async_step_link()
+
async def async_step_import(self, import_info):
"""Import a new bridge as a config entry.
@@ -233,8 +251,8 @@ def __init__(self, config_entry):
self.config_entry = config_entry
async def async_step_init(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Manage Hue options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
@@ -246,13 +264,13 @@ async def async_step_init(
vol.Optional(
CONF_ALLOW_HUE_GROUPS,
default=self.config_entry.options.get(
- CONF_ALLOW_HUE_GROUPS, False
+ CONF_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_HUE_GROUPS
),
): bool,
vol.Optional(
CONF_ALLOW_UNREACHABLE,
default=self.config_entry.options.get(
- CONF_ALLOW_UNREACHABLE, False
+ CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE
),
): bool,
}
diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py
index e2189515482819..8d01617073b8d3 100644
--- a/homeassistant/components/hue/const.py
+++ b/homeassistant/components/hue/const.py
@@ -12,4 +12,9 @@
DEFAULT_ALLOW_UNREACHABLE = False
CONF_ALLOW_HUE_GROUPS = "allow_hue_groups"
-DEFAULT_ALLOW_HUE_GROUPS = True
+DEFAULT_ALLOW_HUE_GROUPS = False
+
+GROUP_TYPE_LIGHT_GROUP = "LightGroup"
+GROUP_TYPE_ROOM = "Room"
+GROUP_TYPE_LUMINAIRE = "Luminaire"
+GROUP_TYPE_LIGHT_SOURCE = "LightSource"
diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py
index 1760c59a69dc8f..739e27d3360d09 100644
--- a/homeassistant/components/hue/helpers.py
+++ b/homeassistant/components/hue/helpers.py
@@ -17,7 +17,7 @@ async def remove_devices(bridge, api_ids, current):
# Device is removed from Hue, so we remove it from Home Assistant
entity = current[item_id]
removed_items.append(item_id)
- await entity.async_remove()
+ await entity.async_remove(force_remove=True)
ent_registry = await get_ent_reg(bridge.hass)
if entity.entity_id in ent_registry.entities:
ent_registry.async_remove(entity.entity_id)
diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py
index 821d482ec25dd9..3d1937340059f4 100644
--- a/homeassistant/components/hue/light.py
+++ b/homeassistant/components/hue/light.py
@@ -36,7 +36,14 @@
)
from homeassistant.util import color
-from .const import DOMAIN as HUE_DOMAIN, REQUEST_REFRESH_DELAY
+from .const import (
+ DOMAIN as HUE_DOMAIN,
+ GROUP_TYPE_LIGHT_GROUP,
+ GROUP_TYPE_LIGHT_SOURCE,
+ GROUP_TYPE_LUMINAIRE,
+ GROUP_TYPE_ROOM,
+ REQUEST_REFRESH_DELAY,
+)
from .helpers import remove_devices
SCAN_INTERVAL = timedelta(seconds=5)
@@ -74,24 +81,35 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"""
-def create_light(item_class, coordinator, bridge, is_group, api, item_id):
+def create_light(item_class, coordinator, bridge, is_group, rooms, api, item_id):
"""Create the light."""
+ api_item = api[item_id]
+
if is_group:
supported_features = 0
- for light_id in api[item_id].lights:
+ for light_id in api_item.lights:
if light_id not in bridge.api.lights:
continue
light = bridge.api.lights[light_id]
supported_features |= SUPPORT_HUE.get(light.type, SUPPORT_HUE_EXTENDED)
supported_features = supported_features or SUPPORT_HUE_EXTENDED
else:
- supported_features = SUPPORT_HUE.get(api[item_id].type, SUPPORT_HUE_EXTENDED)
- return item_class(coordinator, bridge, is_group, api[item_id], supported_features)
+ supported_features = SUPPORT_HUE.get(api_item.type, SUPPORT_HUE_EXTENDED)
+ return item_class(
+ coordinator, bridge, is_group, api_item, supported_features, rooms
+ )
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Hue lights from a config entry."""
bridge = hass.data[HUE_DOMAIN][config_entry.entry_id]
+ api_version = tuple(int(v) for v in bridge.api.config.apiversion.split("."))
+ rooms = {}
+
+ allow_groups = bridge.allow_groups
+ supports_groups = api_version >= GROUP_MIN_API_VERSION
+ if allow_groups and not supports_groups:
+ _LOGGER.warning("Please update your Hue bridge to support groups")
light_coordinator = DataUpdateCoordinator(
hass,
@@ -111,27 +129,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
if not light_coordinator.last_update_success:
raise PlatformNotReady
- update_lights = partial(
- async_update_items,
- bridge,
- bridge.api.lights,
- {},
- async_add_entities,
- partial(create_light, HueLight, light_coordinator, bridge, False),
- )
-
- # We add a listener after fetching the data, so manually trigger listener
- bridge.reset_jobs.append(light_coordinator.async_add_listener(update_lights))
- update_lights()
-
- api_version = tuple(int(v) for v in bridge.api.config.apiversion.split("."))
-
- allow_groups = bridge.allow_groups
- if allow_groups and api_version < GROUP_MIN_API_VERSION:
- _LOGGER.warning("Please update your Hue bridge to support groups")
- allow_groups = False
-
- if not allow_groups:
+ if not supports_groups:
+ update_lights_without_group_support = partial(
+ async_update_items,
+ bridge,
+ bridge.api.lights,
+ {},
+ async_add_entities,
+ partial(create_light, HueLight, light_coordinator, bridge, False, rooms),
+ None,
+ )
+ # We add a listener after fetching the data, so manually trigger listener
+ bridge.reset_jobs.append(
+ light_coordinator.async_add_listener(update_lights_without_group_support)
+ )
return
group_coordinator = DataUpdateCoordinator(
@@ -145,17 +156,69 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
),
)
- update_groups = partial(
+ if allow_groups:
+ update_groups = partial(
+ async_update_items,
+ bridge,
+ bridge.api.groups,
+ {},
+ async_add_entities,
+ partial(create_light, HueLight, group_coordinator, bridge, True, None),
+ None,
+ )
+
+ bridge.reset_jobs.append(group_coordinator.async_add_listener(update_groups))
+
+ cancel_update_rooms_listener = None
+
+ @callback
+ def _async_update_rooms():
+ """Update rooms."""
+ nonlocal cancel_update_rooms_listener
+ rooms.clear()
+ for item_id in bridge.api.groups:
+ group = bridge.api.groups[item_id]
+ if group.type != GROUP_TYPE_ROOM:
+ continue
+ for light_id in group.lights:
+ rooms[light_id] = group.name
+
+ # Once we do a rooms update, we cancel the listener
+ # until the next time lights are added
+ bridge.reset_jobs.remove(cancel_update_rooms_listener)
+ cancel_update_rooms_listener() # pylint: disable=not-callable
+ cancel_update_rooms_listener = None
+
+ @callback
+ def _setup_rooms_listener():
+ nonlocal cancel_update_rooms_listener
+ if cancel_update_rooms_listener is not None:
+ # If there are new lights added before _async_update_rooms
+ # is called we should not add another listener
+ return
+
+ cancel_update_rooms_listener = group_coordinator.async_add_listener(
+ _async_update_rooms
+ )
+ bridge.reset_jobs.append(cancel_update_rooms_listener)
+
+ _setup_rooms_listener()
+ await group_coordinator.async_refresh()
+
+ update_lights_with_group_support = partial(
async_update_items,
bridge,
- bridge.api.groups,
+ bridge.api.lights,
{},
async_add_entities,
- partial(create_light, HueLight, group_coordinator, bridge, True),
+ partial(create_light, HueLight, light_coordinator, bridge, False, rooms),
+ _setup_rooms_listener,
)
-
- bridge.reset_jobs.append(group_coordinator.async_add_listener(update_groups))
- await group_coordinator.async_refresh()
+ # We add a listener after fetching the data, so manually trigger listener
+ bridge.reset_jobs.append(
+ light_coordinator.async_add_listener(update_lights_with_group_support)
+ )
+ update_lights_with_group_support()
async def async_safe_fetch(bridge, fetch_method):
@@ -166,12 +229,14 @@ async def async_safe_fetch(bridge, fetch_method):
except aiohue.Unauthorized as err:
await bridge.handle_unauthorized_error()
raise UpdateFailed("Unauthorized") from err
- except (aiohue.AiohueException,) as err:
+ except aiohue.AiohueException as err:
raise UpdateFailed(f"Hue error: {err}") from err
@callback
-def async_update_items(bridge, api, current, async_add_entities, create_item):
+def async_update_items(
+ bridge, api, current, async_add_entities, create_item, new_items_callback
+):
"""Update items."""
new_items = []
@@ -185,6 +250,9 @@ def async_update_items(bridge, api, current, async_add_entities, create_item):
bridge.hass.async_create_task(remove_devices(bridge, api, current))
if new_items:
+ # This is currently used to setup the listener to update rooms
+ if new_items_callback:
+ new_items_callback()
async_add_entities(new_items)
@@ -201,13 +269,14 @@ def hass_to_hue_brightness(value):
class HueLight(CoordinatorEntity, LightEntity):
"""Representation of a Hue light."""
- def __init__(self, coordinator, bridge, is_group, light, supported_features):
+ def __init__(self, coordinator, bridge, is_group, light, supported_features, rooms):
"""Initialize the light."""
super().__init__(coordinator)
self.light = light
self.bridge = bridge
self.is_group = is_group
self._supported_features = supported_features
+ self._rooms = rooms
if is_group:
self.is_osram = False
@@ -228,12 +297,11 @@ def __init__(self, coordinator, bridge, is_group, light, supported_features):
"bulb in the Philips Hue App."
)
_LOGGER.warning(err, self.name)
- if self.gamut:
- if not color.check_valid_gamut(self.gamut):
- err = "Color gamut of %s: %s, not valid, setting gamut to None."
- _LOGGER.warning(err, self.name, str(self.gamut))
- self.gamut_typ = GAMUT_TYPE_UNAVAILABLE
- self.gamut = None
+ if self.gamut and not color.check_valid_gamut(self.gamut):
+ err = "Color gamut of %s: %s, not valid, setting gamut to None."
+ _LOGGER.warning(err, self.name, str(self.gamut))
+ self.gamut_typ = GAMUT_TYPE_UNAVAILABLE
+ self.gamut = None
@property
def unique_id(self):
@@ -355,10 +423,15 @@ def effect_list(self):
@property
def device_info(self):
"""Return the device info."""
- if self.light.type in ("LightGroup", "Room", "Luminaire", "LightSource"):
+ if self.light.type in (
+ GROUP_TYPE_LIGHT_GROUP,
+ GROUP_TYPE_ROOM,
+ GROUP_TYPE_LUMINAIRE,
+ GROUP_TYPE_LIGHT_SOURCE,
+ ):
return None
- return {
+ info = {
"identifiers": {(HUE_DOMAIN, self.device_id)},
"name": self.name,
"manufacturer": self.light.manufacturername,
@@ -370,6 +443,11 @@ def device_info(self):
"via_device": (HUE_DOMAIN, self.bridge.api.config.bridgeid),
}
+ if self.light.id in self._rooms:
+ info["suggested_area"] = self._rooms[self.light.id]
+
+ return info
+
async def async_turn_on(self, **kwargs):
"""Turn the specified or all lights on."""
command = {"on": True}
@@ -456,7 +534,7 @@ async def async_turn_off(self, **kwargs):
await self.coordinator.async_request_refresh()
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
if not self.is_group:
return {}
diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py
index f5911bbb50c7d3..6ac8d134327fe0 100644
--- a/homeassistant/components/hue/sensor.py
+++ b/homeassistant/components/hue/sensor.py
@@ -6,6 +6,7 @@
TYPE_ZLL_TEMPERATURE,
)
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_ILLUMINANCE,
@@ -14,7 +15,6 @@
PERCENTAGE,
TEMP_CELSIUS,
)
-from homeassistant.helpers.entity import Entity
from .const import DOMAIN as HUE_DOMAIN
from .sensor_base import SENSOR_CONFIG_MAP, GenericHueSensor, GenericZLLSensor
@@ -31,7 +31,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
].sensor_manager.async_register_component("sensor", async_add_entities)
-class GenericHueGaugeSensorEntity(GenericZLLSensor, Entity):
+class GenericHueGaugeSensorEntity(GenericZLLSensor, SensorEntity):
"""Parent class for all 'gauge' Hue device sensors."""
async def _async_update_ha_state(self, *args, **kwargs):
@@ -58,9 +58,9 @@ def state(self):
return round(float(10 ** ((self.sensor.lightlevel - 1) / 10000)), 2)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
- attributes = super().device_state_attributes
+ attributes = super().extra_state_attributes
attributes.update(
{
"lightlevel": self.sensor.lightlevel,
@@ -88,7 +88,7 @@ def state(self):
return self.sensor.temperature / 100
-class HueBattery(GenericHueSensor):
+class HueBattery(GenericHueSensor, SensorEntity):
"""Battery class for when a batt-powered device is only represented as an event."""
@property
diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py
index 263140464aa1cd..9f764e04d28f92 100644
--- a/homeassistant/components/hue/sensor_base.py
+++ b/homeassistant/components/hue/sensor_base.py
@@ -197,6 +197,6 @@ class GenericZLLSensor(GenericHueSensor):
"""Representation of a Hue-brand, physical sensor."""
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
return {"battery_level": self.sensor.battery}
diff --git a/homeassistant/components/hue/services.yaml b/homeassistant/components/hue/services.yaml
index 68eaf6ac377b2f..07eeca6fa0fb5b 100644
--- a/homeassistant/components/hue/services.yaml
+++ b/homeassistant/components/hue/services.yaml
@@ -1,11 +1,18 @@
# Describes the format for available hue services
hue_activate_scene:
+ name: Activate scene
description: Activate a hue scene stored in the hue hub.
fields:
group_name:
+ name: Group
description: Name of hue group/room from the hue app.
example: "Living Room"
+ selector:
+ text:
scene_name:
+ name: Scene
description: Name of hue scene from the hue app.
example: "Energize"
+ selector:
+ text:
diff --git a/homeassistant/components/hue/translations/cs.json b/homeassistant/components/hue/translations/cs.json
index 76606338320a14..1708abfe750173 100644
--- a/homeassistant/components/hue/translations/cs.json
+++ b/homeassistant/components/hue/translations/cs.json
@@ -48,7 +48,7 @@
},
"trigger_type": {
"remote_button_long_release": "Tla\u010d\u00edtko \"{subtype}\" uvoln\u011bno po dlouh\u00e9m stisku",
- "remote_button_short_press": "Stisknuto tla\u010d\u00edtko \"{subtype}\"",
+ "remote_button_short_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto",
"remote_button_short_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\"",
"remote_double_button_long_press": "Oba \"{subtype}\" uvoln\u011bny po dlouh\u00e9m stisku",
"remote_double_button_short_press": "Oba \"{subtype}\" uvoln\u011bny"
diff --git a/homeassistant/components/hue/translations/de.json b/homeassistant/components/hue/translations/de.json
index 0defb33ae5e509..122e1ba6f5c35a 100644
--- a/homeassistant/components/hue/translations/de.json
+++ b/homeassistant/components/hue/translations/de.json
@@ -1,17 +1,17 @@
{
"config": {
"abort": {
- "all_configured": "Alle Philips Hue Bridges sind bereits konfiguriert",
- "already_configured": "Bridge ist bereits konfiguriert",
- "already_in_progress": "Der Konfigurationsablauf f\u00fcr die Bridge wird bereits ausgef\u00fchrt.",
- "cannot_connect": "Verbindung zur Bridge nicht m\u00f6glich",
- "discover_timeout": "Nicht in der Lage Hue Bridges zu entdecken",
- "no_bridges": "Keine Philips Hue Bridges entdeckt",
+ "all_configured": "Es sind bereits alle Philips Hue Bridges konfiguriert",
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "discover_timeout": "Es k\u00f6nnen keine Hue Bridges erkannt werden",
+ "no_bridges": "Keine Philips Hue Bridges erkannt",
"not_hue_bridge": "Keine Philips Hue Bridge entdeckt",
"unknown": "Unbekannter Fehler ist aufgetreten"
},
"error": {
- "linking": "Unbekannter Link-Fehler aufgetreten.",
+ "linking": "Unerwarteter Fehler",
"register_failed": "Registrieren fehlgeschlagen, bitte versuche es erneut"
},
"step": {
@@ -22,7 +22,7 @@
"title": "W\u00e4hle eine Hue Bridge"
},
"link": {
- "description": "Dr\u00fccke den Knopf auf der Bridge, um Philips Hue mit Home Assistant zu registrieren.\n\n",
+ "description": "Dr\u00fccke den Knopf auf der Bridge, um Philips Hue mit Home Assistant zu verkn\u00fcpfen.\n\n",
"title": "Hub verbinden"
},
"manual": {
@@ -58,8 +58,8 @@
"step": {
"init": {
"data": {
- "allow_hue_groups": "Erlaube Hue Gruppen",
- "allow_unreachable": "Erlauben Sie unerreichbaren Gl\u00fchbirnen, ihren Zustand korrekt zu melden"
+ "allow_hue_groups": "Hue-Gruppen erlauben",
+ "allow_unreachable": "Erlaube nicht erreichbaren Gl\u00fchlampen, ihren Zustand korrekt zu melden"
}
}
}
diff --git a/homeassistant/components/hue/translations/et.json b/homeassistant/components/hue/translations/et.json
index fcec56b9d0b4e2..afde880690e6d4 100644
--- a/homeassistant/components/hue/translations/et.json
+++ b/homeassistant/components/hue/translations/et.json
@@ -12,7 +12,7 @@
},
"error": {
"linking": "Ilmnes tundmatu linkimist\u00f5rge.",
- "register_failed": "Registreerimine nurjus. Proovige uuesti"
+ "register_failed": "Registreerimine nurjus. Proovi uuesti"
},
"step": {
"init": {
@@ -22,7 +22,7 @@
"title": "Vali Hue sild"
},
"link": {
- "description": "Vajutage silla nuppu, et registreerida Philips Hue Home Assistant abil. \n\n ! [Nupu asukoht sillal] (/ static / images / config_philips_hue.jpg)",
+ "description": "Vajuta silla nuppu, et registreerida Philips Hue Home Assistant abil. \n\n ! [Nupu asukoht sillal] (/ static / images / config_philips_hue.jpg)",
"title": "\u00dchenda jaotusseade"
},
"manual": {
diff --git a/homeassistant/components/hue/translations/hu.json b/homeassistant/components/hue/translations/hu.json
index c3e3203a66c1a0..d0aa043b10b44d 100644
--- a/homeassistant/components/hue/translations/hu.json
+++ b/homeassistant/components/hue/translations/hu.json
@@ -2,14 +2,16 @@
"config": {
"abort": {
"all_configured": "M\u00e1r minden Philips Hue bridge konfigur\u00e1lt",
- "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van",
- "cannot_connect": "Nem siker\u00fclt csatlakozni a bridge-hez.",
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
"discover_timeout": "Nem tal\u00e1ltam a Hue bridget",
"no_bridges": "Nem tal\u00e1ltam Philips Hue bridget",
+ "not_hue_bridge": "Nem egy Hue Bridge",
"unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt"
},
"error": {
- "linking": "Ismeretlen \u00f6sszekapcsol\u00e1si hiba t\u00f6rt\u00e9nt.",
+ "linking": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt",
"register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, k\u00e9rem pr\u00f3b\u00e1lja \u00fajra"
},
"step": {
@@ -22,6 +24,12 @@
"link": {
"description": "Nyomja meg a gombot a bridge-en a Philips Hue Home Assistant-ben val\u00f3 regisztr\u00e1l\u00e1s\u00e1hoz.\n\n",
"title": "Kapcsol\u00f3d\u00e1s a hubhoz"
+ },
+ "manual": {
+ "data": {
+ "host": "Hoszt"
+ },
+ "title": "A Hue bridge manu\u00e1lis konfigur\u00e1l\u00e1sa"
}
}
},
@@ -29,6 +37,10 @@
"trigger_subtype": {
"turn_off": "Kikapcsol\u00e1s",
"turn_on": "Bekapcsol\u00e1s"
+ },
+ "trigger_type": {
+ "remote_button_short_press": "\"{subtype}\" gomb lenyomva",
+ "remote_button_short_release": "\"{subtype}\" gomb elengedve"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/hue/translations/id.json b/homeassistant/components/hue/translations/id.json
index 2af5753f6548b1..c9e0bcd75d4cc2 100644
--- a/homeassistant/components/hue/translations/id.json
+++ b/homeassistant/components/hue/translations/id.json
@@ -1,27 +1,66 @@
{
"config": {
"abort": {
- "all_configured": "Semua Philips Hue bridges sudah dikonfigurasi",
- "already_configured": "Bridge sudah dikonfigurasi",
- "cannot_connect": "Tidak dapat terhubung ke bridge",
- "discover_timeout": "Tidak dapat menemukan Hue Bridges.",
+ "all_configured": "Semua bridge Philips Hue sudah dikonfigurasi",
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "cannot_connect": "Gagal terhubung",
+ "discover_timeout": "Tidak dapat menemukan bridge Hue",
"no_bridges": "Bridge Philips Hue tidak ditemukan",
- "unknown": "Kesalahan tidak dikenal terjadi."
+ "not_hue_bridge": "Bukan bridge Hue",
+ "unknown": "Kesalahan yang tidak diharapkan"
},
"error": {
- "linking": "Terjadi kesalahan tautan tidak dikenal.",
- "register_failed": "Gagal mendaftar, silakan coba lagi."
+ "linking": "Kesalahan yang tidak diharapkan",
+ "register_failed": "Gagal mendaftar, coba lagi."
},
"step": {
"init": {
"data": {
"host": "Host"
},
- "title": "Pilih Hue bridge"
+ "title": "Pilih bridge Hue"
},
"link": {
- "description": "Tekan tombol di bridge untuk mendaftar Philips Hue dengan Home Assistant.\n\n",
- "title": "Tautan Hub"
+ "description": "Tekan tombol di bridge untuk mendaftarkan Philips Hue dengan Home Assistant.\n\n",
+ "title": "Tautkan Hub"
+ },
+ "manual": {
+ "data": {
+ "host": "Host"
+ },
+ "title": "Konfigurasi bridge Hue secara manual"
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "Tombol pertama",
+ "button_2": "Tombol kedua",
+ "button_3": "Tombol ketiga",
+ "button_4": "Tombol keempat",
+ "dim_down": "Redupkan",
+ "dim_up": "Terangkan",
+ "double_buttons_1_3": "Tombol Pertama dan Ketiga",
+ "double_buttons_2_4": "Tombol Kedua dan Keempat",
+ "turn_off": "Matikan",
+ "turn_on": "Nyalakan"
+ },
+ "trigger_type": {
+ "remote_button_long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama",
+ "remote_button_short_press": "Tombol \"{subtype}\" ditekan",
+ "remote_button_short_release": "Tombol \"{subtype}\" dilepaskan",
+ "remote_double_button_long_press": "Kedua \"{subtype}\" dilepaskan setelah ditekan lama",
+ "remote_double_button_short_press": "Kedua \"{subtype}\" dilepas"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "allow_hue_groups": "Izinkan grup Hue",
+ "allow_unreachable": "Izinkan bohlam yang tidak dapat dijangkau untuk melaporkan statusnya dengan benar"
+ }
}
}
}
diff --git a/homeassistant/components/hue/translations/ko.json b/homeassistant/components/hue/translations/ko.json
index 050b5c51c978e1..30da5cee4849f3 100644
--- a/homeassistant/components/hue/translations/ko.json
+++ b/homeassistant/components/hue/translations/ko.json
@@ -2,16 +2,16 @@
"config": {
"abort": {
"all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "already_configured": "\ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "already_in_progress": "\ube0c\ub9ac\uc9c0 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.",
- "cannot_connect": "\ube0c\ub9ac\uc9c0\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"discover_timeout": "Hue \ube0c\ub9ac\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
"no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9ac\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4",
"not_hue_bridge": "Hue \ube0c\ub9ac\uc9c0\uac00 \uc544\ub2d9\ub2c8\ub2e4",
- "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
- "linking": "\uc54c \uc218 \uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
+ "linking": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4",
"register_failed": "\ub4f1\ub85d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694"
},
"step": {
@@ -47,11 +47,11 @@
"turn_on": "\ucf1c\uae30"
},
"trigger_type": {
- "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c",
- "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub9b4 \ub54c",
- "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c",
- "remote_double_button_long_press": "\"{subtype}\"\uc5d0\uc11c \ubaa8\ub450 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c",
- "remote_double_button_short_press": "\"{subtype}\"\uc5d0\uc11c \ubaa8\ub450 \uc190\uc744 \ub5c4 \ub54c"
+ "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \ub5bc\uc600\uc744 \ub54c",
+ "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub838\uc744 \ub54c",
+ "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5bc\uc5c8\uc744 \ub54c",
+ "remote_double_button_long_press": "\ub450 \"{subtype}\"\uc774(\uac00) \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \ub5bc\uc600\uc744 \ub54c",
+ "remote_double_button_short_press": "\ub450 \"{subtype}\"\uc5d0\uc11c \ubaa8\ub450 \uc190\uc744 \ub5bc\uc5c8\uc744 \ub54c"
}
},
"options": {
diff --git a/homeassistant/components/hue/translations/nl.json b/homeassistant/components/hue/translations/nl.json
index f04d372bf6ad59..0938c18e1ea421 100644
--- a/homeassistant/components/hue/translations/nl.json
+++ b/homeassistant/components/hue/translations/nl.json
@@ -2,16 +2,16 @@
"config": {
"abort": {
"all_configured": "Alle Philips Hue bridges zijn al geconfigureerd",
- "already_configured": "Bridge is al geconfigureerd",
- "already_in_progress": "De configuratiestroom voor het apparaat is al in volle gang.",
- "cannot_connect": "Kan geen verbinding maken met bridge",
+ "already_configured": "Apparaat is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
+ "cannot_connect": "Kan geen verbinding maken",
"discover_timeout": "Hue bridges kunnen niet worden gevonden",
"no_bridges": "Geen Philips Hue bridges ontdekt",
"not_hue_bridge": "Dit is geen Hue bridge",
- "unknown": "Onbekende fout opgetreden"
+ "unknown": "Onverwachte fout"
},
"error": {
- "linking": "Er is een onbekende verbindingsfout opgetreden.",
+ "linking": "Onverwachte fout",
"register_failed": "Registratie is mislukt, probeer het opnieuw"
},
"step": {
@@ -28,7 +28,8 @@
"manual": {
"data": {
"host": "Host"
- }
+ },
+ "title": "Handmatig een Hue bridge configureren"
}
}
},
@@ -52,5 +53,15 @@
"remote_double_button_long_press": "Beide \"{subtype}\" losgelaten na lang indrukken",
"remote_double_button_short_press": "Beide \"{subtype}\" losgelaten"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "allow_hue_groups": "Sta Hue-groepen toe",
+ "allow_unreachable": "Onbereikbare lampen toestaan hun status correct te melden"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json
index 873f60946d5a80..b144393c3d1bae 100644
--- a/homeassistant/components/hue/translations/pl.json
+++ b/homeassistant/components/hue/translations/pl.json
@@ -47,11 +47,11 @@
"turn_on": "w\u0142\u0105cznik"
},
"trigger_type": {
- "remote_button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu",
- "remote_button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty",
- "remote_button_short_release": "\"{subtype}\" zostanie zwolniony",
- "remote_double_button_long_press": "oba \"{subtype}\" zostan\u0105 zwolnione po d\u0142ugim naci\u015bni\u0119ciu",
- "remote_double_button_short_press": "oba \"{subtype}\" zostan\u0105 zwolnione"
+ "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu",
+ "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty",
+ "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony",
+ "remote_double_button_long_press": "oba przyciski \"{subtype}\" zostan\u0105 zwolnione po d\u0142ugim naci\u015bni\u0119ciu",
+ "remote_double_button_short_press": "oba przyciski \"{subtype}\" zostan\u0105 zwolnione"
}
},
"options": {
diff --git a/homeassistant/components/hue/translations/pt.json b/homeassistant/components/hue/translations/pt.json
index 8eabbbb08cc417..09d839cbd5c08a 100644
--- a/homeassistant/components/hue/translations/pt.json
+++ b/homeassistant/components/hue/translations/pt.json
@@ -2,16 +2,16 @@
"config": {
"abort": {
"all_configured": "Todos os hubs Philips Hue j\u00e1 est\u00e3o configurados",
- "already_configured": "Hue j\u00e1 est\u00e1 configurado",
+ "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado",
"already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer",
- "cannot_connect": "N\u00e3o foi poss\u00edvel conectar-se ao hub",
+ "cannot_connect": "Falha na liga\u00e7\u00e3o",
"discover_timeout": "Nenhum hub Hue descoberto",
"no_bridges": "Nenhum hub Philips Hue descoberto",
"not_hue_bridge": "N\u00e3o \u00e9 uma bridge Hue",
- "unknown": "Ocorreu um erro desconhecido"
+ "unknown": "Erro inesperado"
},
"error": {
- "linking": "Ocorreu um erro de liga\u00e7\u00e3o desconhecido.",
+ "linking": "Erro inesperado",
"register_failed": "Falha ao registar, por favor, tente novamente"
},
"step": {
diff --git a/homeassistant/components/hue/translations/tr.json b/homeassistant/components/hue/translations/tr.json
new file mode 100644
index 00000000000000..984c91e8f366b0
--- /dev/null
+++ b/homeassistant/components/hue/translations/tr.json
@@ -0,0 +1,48 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor",
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ },
+ "error": {
+ "linking": "Beklenmeyen hata"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ }
+ },
+ "manual": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ },
+ "title": "Bir Hue k\u00f6pr\u00fcs\u00fcn\u00fc manuel olarak yap\u0131land\u0131rma"
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "\u0130lk d\u00fc\u011fme",
+ "button_2": "\u0130kinci d\u00fc\u011fme",
+ "button_3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme",
+ "button_4": "D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fme",
+ "double_buttons_1_3": "Birinci ve \u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fmeler",
+ "double_buttons_2_4": "\u0130kinci ve D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fmeler",
+ "turn_off": "Kapat",
+ "turn_on": "A\u00e7"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "allow_hue_groups": "Hue gruplar\u0131na izin ver",
+ "allow_unreachable": "Ula\u015f\u0131lamayan ampullerin durumlar\u0131n\u0131 do\u011fru \u015fekilde bildirmesine izin verin"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hue/translations/uk.json b/homeassistant/components/hue/translations/uk.json
new file mode 100644
index 00000000000000..8e9c5ca82cbbf3
--- /dev/null
+++ b/homeassistant/components/hue/translations/uk.json
@@ -0,0 +1,67 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "\u0412\u0441\u0456 \u0448\u043b\u044e\u0437\u0438 Philips Hue \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0456.",
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e.",
+ "no_bridges": "\u0428\u043b\u044e\u0437\u0438 Philips Hue \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456.",
+ "not_hue_bridge": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0454 \u0448\u043b\u044e\u0437\u043e\u043c Hue.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "error": {
+ "linking": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430",
+ "register_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0448\u043b\u044e\u0437 Hue"
+ },
+ "link": {
+ "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0448\u043b\u044e\u0437\u0456 \u0434\u043b\u044f \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u0457 Philips Hue \u0432 Home Assistant. \n\n![\u0420\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0448\u043b\u044e\u0437\u0456] (/static/images/config_philips_hue.jpg)",
+ "title": "Philips Hue"
+ },
+ "manual": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "title": "\u0420\u0443\u0447\u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0448\u043b\u044e\u0437\u0443"
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "\u041f\u0435\u0440\u0448\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_2": "\u0414\u0440\u0443\u0433\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_3": "\u0422\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "dim_down": "\u0417\u043c\u0435\u043d\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c",
+ "dim_up": "\u0417\u0431\u0456\u043b\u044c\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c",
+ "double_buttons_1_3": "\u041f\u0435\u0440\u0448\u0430 \u0456 \u0442\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0438",
+ "double_buttons_2_4": "\u0414\u0440\u0443\u0433\u0430 \u0456 \u0447\u0435\u0442\u0432\u0435\u0440\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0438",
+ "turn_off": "\u0412\u0438\u043c\u043a\u043d\u0443\u0442\u0438",
+ "turn_on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438"
+ },
+ "trigger_type": {
+ "remote_button_long_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f",
+ "remote_button_short_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430",
+ "remote_button_short_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f",
+ "remote_double_button_long_press": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u043e \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f",
+ "remote_double_button_short_press": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u043e \u043f\u0456\u0441\u043b\u044f \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "allow_hue_groups": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0433\u0440\u0443\u043f\u0438 Hue",
+ "allow_unreachable": "\u041f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u044f\u0442\u0438 \u0441\u0442\u0430\u043d \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hue/translations/zh-Hans.json b/homeassistant/components/hue/translations/zh-Hans.json
index c34bd68aad86e7..1dcc6b8f59b92a 100644
--- a/homeassistant/components/hue/translations/zh-Hans.json
+++ b/homeassistant/components/hue/translations/zh-Hans.json
@@ -29,6 +29,13 @@
"device_automation": {
"trigger_subtype": {
"turn_off": "\u5173\u95ed"
+ },
+ "trigger_type": {
+ "remote_button_long_release": "\"{subtype}\" \u957f\u6309\u540e\u677e\u5f00",
+ "remote_button_short_press": "\"{subtype}\" \u5355\u51fb",
+ "remote_button_short_release": "\"{subtype}\" \u677e\u5f00",
+ "remote_double_button_long_press": "\"{subtype}\" \u4e24\u952e\u540c\u65f6\u957f\u6309\u540e\u677e\u5f00",
+ "remote_double_button_short_press": "\"{subtype}\" \u4e24\u952e\u540c\u65f6\u677e\u5f00"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py
index 23dc3cb7edaf98..3af6db3efb5f26 100644
--- a/homeassistant/components/huisbaasje/__init__.py
+++ b/homeassistant/components/huisbaasje/__init__.py
@@ -8,7 +8,6 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
@@ -61,10 +60,7 @@ async def async_update_data():
update_interval=timedelta(seconds=POLLING_INTERVAL),
)
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
# Load the client in the data of home assistant
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = {
@@ -100,7 +96,7 @@ async def async_update_huisbaasje(huisbaasje):
# handled by the data update coordinator.
async with async_timeout.timeout(FETCH_TIMEOUT):
if not huisbaasje.is_authenticated():
- _LOGGER.warning("Huisbaasje is unauthenticated. Reauthenticating...")
+ _LOGGER.warning("Huisbaasje is unauthenticated. Reauthenticating")
await huisbaasje.authenticate()
current_measurements = await huisbaasje.current_measurements()
@@ -141,7 +137,7 @@ def _get_cumulative_value(
:param source_type: The source of energy (electricity or gas)
:param period_type: The period for which cumulative value should be given.
"""
- if source_type in current_measurements.keys():
+ if source_type in current_measurements:
if (
period_type in current_measurements[source_type]
and current_measurements[source_type][period_type] is not None
diff --git a/homeassistant/components/huisbaasje/config_flow.py b/homeassistant/components/huisbaasje/config_flow.py
index 59e4840529ddc7..c8681c31188fe5 100644
--- a/homeassistant/components/huisbaasje/config_flow.py
+++ b/homeassistant/components/huisbaasje/config_flow.py
@@ -8,7 +8,7 @@
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import AbortFlow
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -32,9 +32,18 @@ async def async_step_user(self, user_input=None):
try:
user_id = await self._validate_input(user_input)
-
- _LOGGER.info("Input for Huisbaasje is valid!")
-
+ except HuisbaasjeConnectionException as exception:
+ _LOGGER.warning(exception)
+ errors["base"] = "cannot_connect"
+ except HuisbaasjeException as exception:
+ _LOGGER.warning(exception)
+ errors["base"] = "invalid_auth"
+ except AbortFlow:
+ raise
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+ else:
# Set user id as unique id
await self.async_set_unique_id(user_id)
self._abort_if_unique_id_configured()
@@ -48,17 +57,6 @@ async def async_step_user(self, user_input=None):
CONF_PASSWORD: user_input[CONF_PASSWORD],
},
)
- except HuisbaasjeConnectionException as exception:
- _LOGGER.warning(exception)
- errors["base"] = "connection_exception"
- except HuisbaasjeException as exception:
- _LOGGER.warning(exception)
- errors["base"] = "invalid_auth"
- except AbortFlow as exception:
- raise exception
- except Exception: # pylint: disable=broad-except
- _LOGGER.exception("Unexpected exception")
- errors["base"] = "unknown"
return await self._show_setup_form(user_input, errors)
@@ -72,7 +70,6 @@ async def _validate_input(self, user_input):
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
-
username = user_input[CONF_USERNAME]
password = user_input[CONF_PASSWORD]
diff --git a/homeassistant/components/huisbaasje/const.py b/homeassistant/components/huisbaasje/const.py
index 07ad84567e5a01..abac03e61828ae 100644
--- a/homeassistant/components/huisbaasje/const.py
+++ b/homeassistant/components/huisbaasje/const.py
@@ -9,6 +9,7 @@
)
from homeassistant.const import (
+ DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
ENERGY_KILO_WATT_HOUR,
TIME_HOURS,
@@ -70,34 +71,34 @@
},
{
"name": "Huisbaasje Energy Today",
+ "device_class": DEVICE_CLASS_ENERGY,
"unit_of_measurement": ENERGY_KILO_WATT_HOUR,
"source_type": SOURCE_TYPE_ELECTRICITY,
"sensor_type": SENSOR_TYPE_THIS_DAY,
- "icon": "mdi:counter",
"precision": 1,
},
{
"name": "Huisbaasje Energy This Week",
+ "device_class": DEVICE_CLASS_ENERGY,
"unit_of_measurement": ENERGY_KILO_WATT_HOUR,
"source_type": SOURCE_TYPE_ELECTRICITY,
"sensor_type": SENSOR_TYPE_THIS_WEEK,
- "icon": "mdi:counter",
"precision": 1,
},
{
"name": "Huisbaasje Energy This Month",
+ "device_class": DEVICE_CLASS_ENERGY,
"unit_of_measurement": ENERGY_KILO_WATT_HOUR,
"source_type": SOURCE_TYPE_ELECTRICITY,
"sensor_type": SENSOR_TYPE_THIS_MONTH,
- "icon": "mdi:counter",
"precision": 1,
},
{
"name": "Huisbaasje Energy This Year",
+ "device_class": DEVICE_CLASS_ENERGY,
"unit_of_measurement": ENERGY_KILO_WATT_HOUR,
"source_type": SOURCE_TYPE_ELECTRICITY,
"sensor_type": SENSOR_TYPE_THIS_YEAR,
- "icon": "mdi:counter",
"precision": 1,
},
{
diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py
index e84052fe029626..3acf39a140da77 100644
--- a/homeassistant/components/huisbaasje/sensor.py
+++ b/homeassistant/components/huisbaasje/sensor.py
@@ -1,4 +1,5 @@
"""Platform for sensor integration."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID, POWER_WATT
from homeassistant.core import HomeAssistant
@@ -23,7 +24,7 @@ async def async_setup_entry(
)
-class HuisbaasjeSensor(CoordinatorEntity):
+class HuisbaasjeSensor(CoordinatorEntity, SensorEntity):
"""Defines a Huisbaasje sensor."""
def __init__(
diff --git a/homeassistant/components/huisbaasje/strings.json b/homeassistant/components/huisbaasje/strings.json
index f126ac0afffd27..169b9a0e901c18 100644
--- a/homeassistant/components/huisbaasje/strings.json
+++ b/homeassistant/components/huisbaasje/strings.json
@@ -10,8 +10,7 @@
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
- "unauthenticated_exception": "[%key:common::config_flow::error::invalid_auth%]",
- "connection_exception": "[%key:common::config_flow::error::cannot_connect%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
diff --git a/homeassistant/components/huisbaasje/translations/bg.json b/homeassistant/components/huisbaasje/translations/bg.json
new file mode 100644
index 00000000000000..67a484573aa0c1
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/bg.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "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/huisbaasje/translations/ca.json b/homeassistant/components/huisbaasje/translations/ca.json
new file mode 100644
index 00000000000000..3ee45b4c38b840
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/ca.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "connection_exception": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unauthenticated_exception": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrasenya",
+ "username": "Nom d'usuari"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/cs.json b/homeassistant/components/huisbaasje/translations/cs.json
new file mode 100644
index 00000000000000..8c89c265fe56b7
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/cs.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno"
+ },
+ "error": {
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
+ "connection_exception": "Nepoda\u0159ilo se p\u0159ipojit",
+ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
+ "unauthenticated_exception": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
+ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Heslo",
+ "username": "U\u017eivatelsk\u00e9 jm\u00e9no"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/de.json b/homeassistant/components/huisbaasje/translations/de.json
new file mode 100644
index 00000000000000..5f8b8ef4c1a431
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/de.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "connection_exception": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unauthenticated_exception": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/en.json b/homeassistant/components/huisbaasje/translations/en.json
new file mode 100644
index 00000000000000..42bb4b59196179
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/en.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "connection_exception": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
+ "unauthenticated_exception": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password",
+ "username": "Username"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/es-419.json b/homeassistant/components/huisbaasje/translations/es-419.json
new file mode 100644
index 00000000000000..c456531ee49a28
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/es-419.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "No se logr\u00f3 una conecci\u00f3n"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/es.json b/homeassistant/components/huisbaasje/translations/es.json
new file mode 100644
index 00000000000000..def06b0941dec3
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/es.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositivo ya est\u00e1 configurado"
+ },
+ "error": {
+ "connection_exception": "No se pudo conectar",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
+ "unauthenticated_exception": "Autenticaci\u00f3n no v\u00e1lida",
+ "unknown": "Error inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrase\u00f1a",
+ "username": "Usuario"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/et.json b/homeassistant/components/huisbaasje/translations/et.json
new file mode 100644
index 00000000000000..b41701427782d8
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/et.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "connection_exception": "\u00dchendamine nurjus",
+ "invalid_auth": "Vigane autentimine",
+ "unauthenticated_exception": "Vigane autentimine",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Salas\u00f5na",
+ "username": "Kasutajanimi"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/fr.json b/homeassistant/components/huisbaasje/translations/fr.json
new file mode 100644
index 00000000000000..567f4a08f4ad16
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/fr.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9ja configur\u00e9 "
+ },
+ "error": {
+ "cannot_connect": "[%key::common::config_flow::error::cannot_connect%]",
+ "connection_exception": "\u00c9chec de la connexion ",
+ "invalid_auth": "Authentification invalide ",
+ "unauthenticated_exception": "Authentification invalide ",
+ "unknown": "Erreur inatendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/hu.json b/homeassistant/components/huisbaasje/translations/hu.json
new file mode 100644
index 00000000000000..9d94d9d76abc6f
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/hu.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "connection_exception": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unauthenticated_exception": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/id.json b/homeassistant/components/huisbaasje/translations/id.json
new file mode 100644
index 00000000000000..76e8805524e8a2
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "connection_exception": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unauthenticated_exception": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/it.json b/homeassistant/components/huisbaasje/translations/it.json
new file mode 100644
index 00000000000000..a8f899f9f82047
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/it.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "connection_exception": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
+ "unauthenticated_exception": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password",
+ "username": "Nome utente"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/ko.json b/homeassistant/components/huisbaasje/translations/ko.json
new file mode 100644
index 00000000000000..19387dfe542d74
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/ko.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "connection_exception": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unauthenticated_exception": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/nl.json b/homeassistant/components/huisbaasje/translations/nl.json
new file mode 100644
index 00000000000000..a13c1837b9f9a4
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/nl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "connection_exception": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
+ "unauthenticated_exception": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Wachtwoord",
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/no.json b/homeassistant/components/huisbaasje/translations/no.json
new file mode 100644
index 00000000000000..3eebfb72df2374
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/no.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "connection_exception": "Tilkobling mislyktes",
+ "invalid_auth": "Ugyldig godkjenning",
+ "unauthenticated_exception": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passord",
+ "username": "Brukernavn"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/pl.json b/homeassistant/components/huisbaasje/translations/pl.json
new file mode 100644
index 00000000000000..c87d3d0be7d540
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/pl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "connection_exception": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unauthenticated_exception": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/pt.json b/homeassistant/components/huisbaasje/translations/pt.json
new file mode 100644
index 00000000000000..3b5850222d9485
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/pt.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Falha na liga\u00e7\u00e3o"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/ru.json b/homeassistant/components/huisbaasje/translations/ru.json
new file mode 100644
index 00000000000000..c9fbe5cdcb2eb5
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/ru.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "connection_exception": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "unauthenticated_exception": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/tr.json b/homeassistant/components/huisbaasje/translations/tr.json
new file mode 100644
index 00000000000000..fa5bd3112861b5
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/tr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "connection_exception": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unauthenticated_exception": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen Hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u015eifre",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/zh-Hant.json b/homeassistant/components/huisbaasje/translations/zh-Hant.json
new file mode 100644
index 00000000000000..b1e95586376433
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/zh-Hant.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "connection_exception": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unauthenticated_exception": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u5bc6\u78bc",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py
index fc455feb477a6c..9500b74aba63c6 100644
--- a/homeassistant/components/humidifier/__init__.py
+++ b/homeassistant/components/humidifier/__init__.py
@@ -1,17 +1,21 @@
"""Provides functionality to interact with humidifier devices."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Any, Dict, List, Optional
+from typing import Any, final
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
+ ATTR_MODE,
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_ON,
)
+from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
@@ -19,7 +23,7 @@
)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
-from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from .const import (
@@ -27,7 +31,6 @@
ATTR_HUMIDITY,
ATTR_MAX_HUMIDITY,
ATTR_MIN_HUMIDITY,
- ATTR_MODE,
DEFAULT_MAX_HUMIDITY,
DEFAULT_MIN_HUMIDITY,
DEVICE_CLASS_DEHUMIDIFIER,
@@ -57,7 +60,7 @@ def is_on(hass, entity_id):
return hass.states.is_state(entity_id, STATE_ON)
-async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up humidifier devices."""
component = hass.data[DOMAIN] = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
@@ -86,21 +89,21 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
return True
-async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)
-async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)
class HumidifierEntity(ToggleEntity):
- """Representation of a humidifier device."""
+ """Base class for humidifier entities."""
@property
- def capability_attributes(self) -> Dict[str, Any]:
+ def capability_attributes(self) -> dict[str, Any]:
"""Return capability attributes."""
supported_features = self.supported_features or 0
data = {
@@ -113,8 +116,9 @@ def capability_attributes(self) -> Dict[str, Any]:
return data
+ @final
@property
- def state_attributes(self) -> Dict[str, Any]:
+ def state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
supported_features = self.supported_features or 0
data = {}
@@ -128,12 +132,12 @@ def state_attributes(self) -> Dict[str, Any]:
return data
@property
- def target_humidity(self) -> Optional[int]:
+ def target_humidity(self) -> int | None:
"""Return the humidity we try to reach."""
return None
@property
- def mode(self) -> Optional[str]:
+ def mode(self) -> str | None:
"""Return the current mode, e.g., home, auto, baby.
Requires SUPPORT_MODES.
@@ -141,7 +145,7 @@ def mode(self) -> Optional[str]:
raise NotImplementedError
@property
- def available_modes(self) -> Optional[List[str]]:
+ def available_modes(self) -> list[str] | None:
"""Return a list of available modes.
Requires SUPPORT_MODES.
diff --git a/homeassistant/components/humidifier/const.py b/homeassistant/components/humidifier/const.py
index 82e87ae5c31d59..c25087701877dc 100644
--- a/homeassistant/components/humidifier/const.py
+++ b/homeassistant/components/humidifier/const.py
@@ -1,5 +1,4 @@
"""Provides the constants needed for component."""
-
MODE_NORMAL = "normal"
MODE_ECO = "eco"
MODE_AWAY = "away"
@@ -10,7 +9,6 @@
MODE_AUTO = "auto"
MODE_BABY = "baby"
-ATTR_MODE = "mode"
ATTR_AVAILABLE_MODES = "available_modes"
ATTR_HUMIDITY = "humidity"
ATTR_MAX_HUMIDITY = "max_humidity"
diff --git a/homeassistant/components/humidifier/device_action.py b/homeassistant/components/humidifier/device_action.py
index 6bccd375207cc2..fa9c1eb71e7703 100644
--- a/homeassistant/components/humidifier/device_action.py
+++ b/homeassistant/components/humidifier/device_action.py
@@ -1,11 +1,12 @@
"""Provides device actions for Humidifier."""
-from typing import List, Optional
+from __future__ import annotations
import voluptuous as vol
from homeassistant.components.device_automation import toggle_entity
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_MODE,
ATTR_SUPPORTED_FEATURES,
CONF_DEVICE_ID,
CONF_DOMAIN,
@@ -30,7 +31,7 @@
{
vol.Required(CONF_TYPE): "set_mode",
vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN),
- vol.Required(const.ATTR_MODE): cv.string,
+ vol.Required(ATTR_MODE): cv.string,
}
)
@@ -39,7 +40,7 @@
ACTION_SCHEMA = vol.Any(SET_HUMIDITY_SCHEMA, SET_MODE_SCHEMA, ONOFF_SCHEMA)
-async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device actions for Humidifier devices."""
registry = await entity_registry.async_get_registry(hass)
actions = await toggle_entity.async_get_actions(hass, device_id, DOMAIN)
@@ -78,11 +79,9 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
async def async_call_action_from_config(
- hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context]
+ hass: HomeAssistant, config: dict, variables: dict, context: Context | None
) -> None:
"""Execute a device action."""
- config = ACTION_SCHEMA(config)
-
service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]}
if config[CONF_TYPE] == "set_humidity":
@@ -90,7 +89,7 @@ async def async_call_action_from_config(
service_data[const.ATTR_HUMIDITY] = config[const.ATTR_HUMIDITY]
elif config[CONF_TYPE] == "set_mode":
service = const.SERVICE_SET_MODE
- service_data[const.ATTR_MODE] = config[const.ATTR_MODE]
+ service_data[ATTR_MODE] = config[ATTR_MODE]
else:
return await toggle_entity.async_call_action_from_config(
hass, config, variables, context, DOMAIN
@@ -115,7 +114,7 @@ async def async_get_action_capabilities(hass, config):
available_modes = state.attributes.get(const.ATTR_AVAILABLE_MODES, [])
else:
available_modes = []
- fields[vol.Required(const.ATTR_MODE)] = vol.In(available_modes)
+ fields[vol.Required(ATTR_MODE)] = vol.In(available_modes)
else:
return {}
diff --git a/homeassistant/components/humidifier/device_condition.py b/homeassistant/components/humidifier/device_condition.py
index 714a51ab0161dc..02a667f2f68f05 100644
--- a/homeassistant/components/humidifier/device_condition.py
+++ b/homeassistant/components/humidifier/device_condition.py
@@ -1,11 +1,12 @@
"""Provide the device automations for Humidifier."""
-from typing import Dict, List
+from __future__ import annotations
import voluptuous as vol
from homeassistant.components.device_automation import toggle_entity
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_MODE,
ATTR_SUPPORTED_FEATURES,
CONF_CONDITION,
CONF_DEVICE_ID,
@@ -28,7 +29,7 @@
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): "is_mode",
- vol.Required(const.ATTR_MODE): str,
+ vol.Required(ATTR_MODE): str,
}
)
@@ -37,7 +38,7 @@
async def async_get_conditions(
hass: HomeAssistant, device_id: str
-) -> List[Dict[str, str]]:
+) -> list[dict[str, str]]:
"""List device conditions for Humidifier devices."""
registry = await entity_registry.async_get_registry(hass)
conditions = await toggle_entity.async_get_conditions(hass, device_id, DOMAIN)
@@ -72,7 +73,7 @@ def async_condition_from_config(
config = CONDITION_SCHEMA(config)
if config[CONF_TYPE] == "is_mode":
- attribute = const.ATTR_MODE
+ attribute = ATTR_MODE
else:
return toggle_entity.async_condition_from_config(config)
@@ -97,7 +98,7 @@ async def async_get_condition_capabilities(hass, config):
else:
modes = []
- fields[vol.Required(const.ATTR_AVAILABLE_MODES)] = vol.In(modes)
+ fields[vol.Required(ATTR_MODE)] = vol.In(modes)
return {"extra_fields": vol.Schema(fields)}
diff --git a/homeassistant/components/humidifier/device_trigger.py b/homeassistant/components/humidifier/device_trigger.py
index 6bc9682f79a41b..d0f462f6b0ff52 100644
--- a/homeassistant/components/humidifier/device_trigger.py
+++ b/homeassistant/components/humidifier/device_trigger.py
@@ -1,5 +1,5 @@
"""Provides device automations for Climate."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -48,7 +48,7 @@
TRIGGER_SCHEMA = vol.Any(TARGET_TRIGGER_SCHEMA, TOGGLE_TRIGGER_SCHEMA)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers for Humidifier devices."""
registry = await entity_registry.async_get_registry(hass)
triggers = await toggle_entity.async_get_triggers(hass, device_id, DOMAIN)
diff --git a/homeassistant/components/humidifier/group.py b/homeassistant/components/humidifier/group.py
index 1636054663dc69..234883ffd5a041 100644
--- a/homeassistant/components/humidifier/group.py
+++ b/homeassistant/components/humidifier/group.py
@@ -3,13 +3,12 @@
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OFF, STATE_ON
-from homeassistant.core import callback
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import HomeAssistant, callback
@callback
def async_describe_on_off_states(
- hass: HomeAssistantType, registry: GroupIntegrationRegistry
+ hass: HomeAssistant, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_ON}, STATE_OFF)
diff --git a/homeassistant/components/humidifier/intent.py b/homeassistant/components/humidifier/intent.py
index fafbb0a494a624..d9ecafbc537c46 100644
--- a/homeassistant/components/humidifier/intent.py
+++ b/homeassistant/components/humidifier/intent.py
@@ -1,7 +1,7 @@
"""Intents for the humidifier integration."""
import voluptuous as vol
-from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF
+from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, STATE_OFF
from homeassistant.core import HomeAssistant
from homeassistant.helpers import intent
import homeassistant.helpers.config_validation as cv
@@ -9,7 +9,6 @@
from . import (
ATTR_AVAILABLE_MODES,
ATTR_HUMIDITY,
- ATTR_MODE,
DOMAIN,
SERVICE_SET_HUMIDITY,
SERVICE_SET_MODE,
diff --git a/homeassistant/components/humidifier/reproduce_state.py b/homeassistant/components/humidifier/reproduce_state.py
index e9b1777d63f99f..3f73ebf4e0abcf 100644
--- a/homeassistant/components/humidifier/reproduce_state.py
+++ b/homeassistant/components/humidifier/reproduce_state.py
@@ -1,29 +1,30 @@
"""Module that groups code required to handle state restore for component."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
-
-from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON
-from homeassistant.core import Context, State
-from homeassistant.helpers.typing import HomeAssistantType
+from typing import Any, Iterable
-from .const import (
- ATTR_HUMIDITY,
+from homeassistant.const import (
ATTR_MODE,
- DOMAIN,
- SERVICE_SET_HUMIDITY,
- SERVICE_SET_MODE,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ STATE_OFF,
+ STATE_ON,
)
+from homeassistant.core import Context, HomeAssistant, State
+
+from .const import ATTR_HUMIDITY, DOMAIN, SERVICE_SET_HUMIDITY, SERVICE_SET_MODE
_LOGGER = logging.getLogger(__name__)
async def _async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce component states."""
cur_state = hass.states.get(state.entity_id)
@@ -79,11 +80,11 @@ async def call_service(service: str, keys: Iterable, data=None):
async def async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce component states."""
await asyncio.gather(
diff --git a/homeassistant/components/humidifier/translations/de.json b/homeassistant/components/humidifier/translations/de.json
index 24d8b01353e2e1..a9bd89055ef042 100644
--- a/homeassistant/components/humidifier/translations/de.json
+++ b/homeassistant/components/humidifier/translations/de.json
@@ -1,12 +1,19 @@
{
"device_automation": {
"action_type": {
+ "set_humidity": "Luftfeuchtigkeit f\u00fcr {entity_name} einstellen",
"set_mode": "Wechsele Modus auf {entity_name}",
"toggle": "{entity_name} umschalten",
"turn_off": "Schalte {entity_name} aus",
"turn_on": "Schalte {entity_name} an"
},
+ "condition_type": {
+ "is_mode": "{entity_name} ist auf einen bestimmten Modus festgelegt",
+ "is_off": "{entity_name} ist ausgeschaltet",
+ "is_on": "{entity_name} ist eingeschaltet"
+ },
"trigger_type": {
+ "target_humidity_changed": "{entity_name} Soll-Luftfeuchtigkeit ge\u00e4ndert",
"turned_off": "{entity_name} ausgeschaltet",
"turned_on": "{entity_name} eingeschaltet"
}
diff --git a/homeassistant/components/humidifier/translations/hu.json b/homeassistant/components/humidifier/translations/hu.json
new file mode 100644
index 00000000000000..7dd723df738511
--- /dev/null
+++ b/homeassistant/components/humidifier/translations/hu.json
@@ -0,0 +1,28 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_humidity": "{entity_name} p\u00e1ratartalom be\u00e1ll\u00edt\u00e1sa",
+ "set_mode": "{entity_name} m\u00f3d m\u00f3dos\u00edt\u00e1sa",
+ "toggle": "{entity_name} be/kikapcsol\u00e1sa",
+ "turn_off": "{entity_name} kikapcsol\u00e1sa",
+ "turn_on": "{entity_name} bekapcsol\u00e1sa"
+ },
+ "condition_type": {
+ "is_mode": "A(z) {entity_name} egy adott m\u00f3dra van \u00e1ll\u00edtva",
+ "is_off": "{entity_name} ki van kapcsolva",
+ "is_on": "{entity_name} be van kapcsolva"
+ },
+ "trigger_type": {
+ "target_humidity_changed": "{name} k\u00edv\u00e1nt p\u00e1ratartalom megv\u00e1ltozott",
+ "turned_off": "{entity_name} ki lett kapcsolva",
+ "turned_on": "{entity_name} be lett kapcsolva"
+ }
+ },
+ "state": {
+ "_": {
+ "off": "Ki",
+ "on": "Be"
+ }
+ },
+ "title": "P\u00e1r\u00e1s\u00edt\u00f3"
+}
\ No newline at end of file
diff --git a/homeassistant/components/humidifier/translations/id.json b/homeassistant/components/humidifier/translations/id.json
new file mode 100644
index 00000000000000..b06b2bfee45aaa
--- /dev/null
+++ b/homeassistant/components/humidifier/translations/id.json
@@ -0,0 +1,28 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_humidity": "Setel kelembaban untuk {entity_name}",
+ "set_mode": "Ubah mode di {entity_name}",
+ "toggle": "Nyala/matikan {entity_name}",
+ "turn_off": "Matikan {entity_name}",
+ "turn_on": "Nyalakan {entity_name}"
+ },
+ "condition_type": {
+ "is_mode": "{entity_name} disetel ke mode tertentu",
+ "is_off": "{entity_name} mati",
+ "is_on": "{entity_name} nyala"
+ },
+ "trigger_type": {
+ "target_humidity_changed": "Kelembapan target {entity_name} berubah",
+ "turned_off": "{entity_name} dimatikan",
+ "turned_on": "{entity_name} dinyalakan"
+ }
+ },
+ "state": {
+ "_": {
+ "off": "Mati",
+ "on": "Nyala"
+ }
+ },
+ "title": "Pelembab"
+}
\ No newline at end of file
diff --git a/homeassistant/components/humidifier/translations/ko.json b/homeassistant/components/humidifier/translations/ko.json
index c484a532156559..9c556f246e38a0 100644
--- a/homeassistant/components/humidifier/translations/ko.json
+++ b/homeassistant/components/humidifier/translations/ko.json
@@ -1,21 +1,21 @@
{
"device_automation": {
"action_type": {
- "set_humidity": "{entity_name} \uc2b5\ub3c4 \uc124\uc815\ud558\uae30",
- "set_mode": "{entity_name} \uc758 \uc6b4\uc804 \ubaa8\ub4dc \ubcc0\uacbd",
- "toggle": "{entity_name} \ud1a0\uae00",
- "turn_off": "{entity_name} \ub044\uae30",
- "turn_on": "{entity_name} \ucf1c\uae30"
+ "set_humidity": "{entity_name}\uc758 \uc2b5\ub3c4 \uc124\uc815\ud558\uae30",
+ "set_mode": "{entity_name}\uc758 \uc6b4\uc804 \ubaa8\ub4dc \ubcc0\uacbd",
+ "toggle": "{entity_name}\uc744(\ub97c) \ud1a0\uae00\ud558\uae30",
+ "turn_off": "{entity_name}\uc744(\ub97c) \ub044\uae30",
+ "turn_on": "{entity_name}\uc744(\ub97c) \ucf1c\uae30"
},
"condition_type": {
- "is_mode": "{entity_name} \uc774(\uac00) \ud2b9\uc815 \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74",
- "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74",
- "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74"
+ "is_mode": "{entity_name}\uc774(\uac00) \ud2b9\uc815 \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74",
+ "is_off": "{entity_name}\uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74",
+ "is_on": "{entity_name}\uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74"
},
"trigger_type": {
- "target_humidity_changed": "{entity_name} \ubaa9\ud45c \uc2b5\ub3c4\uac00 \ubcc0\uacbd\ub420 \ub54c",
- "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c",
- "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c"
+ "target_humidity_changed": "{entity_name}\uc758 \ubaa9\ud45c \uc2b5\ub3c4\uac00 \ubcc0\uacbd\ub418\uc5c8\uc744 \ub54c",
+ "turned_off": "{entity_name}\uc774(\uac00) \uaebc\uc84c\uc744 \ub54c",
+ "turned_on": "{entity_name}\uc774(\uac00) \ucf1c\uc84c\uc744 \ub54c"
}
},
"state": {
diff --git a/homeassistant/components/humidifier/translations/nl.json b/homeassistant/components/humidifier/translations/nl.json
index 311943bbd23709..9505a6a0838619 100644
--- a/homeassistant/components/humidifier/translations/nl.json
+++ b/homeassistant/components/humidifier/translations/nl.json
@@ -1,9 +1,17 @@
{
"device_automation": {
"action_type": {
+ "set_humidity": "Luchtvochtigheid instellen voor {entity_name}",
+ "set_mode": "Wijzig modus van {entity_name}",
+ "toggle": "Schakel {entity_name}",
"turn_off": "{entity_name} uitschakelen",
"turn_on": "{entity_name} inschakelen"
},
+ "condition_type": {
+ "is_mode": "{entity_name} is ingesteld op een specifieke modus",
+ "is_off": "{entity_name} is uitgeschakeld",
+ "is_on": "{entity_name} staat aan"
+ },
"trigger_type": {
"target_humidity_changed": "{entity_name} doel luchtvochtigheid gewijzigd",
"turned_off": "{entity_name} is uitgeschakeld",
@@ -15,5 +23,6 @@
"off": "Uit",
"on": "Aan"
}
- }
+ },
+ "title": "Luchtbevochtiger"
}
\ No newline at end of file
diff --git a/homeassistant/components/humidifier/translations/tr.json b/homeassistant/components/humidifier/translations/tr.json
new file mode 100644
index 00000000000000..7bcdbc46a0b2d7
--- /dev/null
+++ b/homeassistant/components/humidifier/translations/tr.json
@@ -0,0 +1,25 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_mode": "{entity_name} \u00fczerindeki mod de\u011fi\u015ftirme",
+ "turn_on": "{entity_name} a\u00e7\u0131n"
+ },
+ "condition_type": {
+ "is_mode": "{entity_name} belirli bir moda ayarland\u0131",
+ "is_off": "{entity_name} kapal\u0131",
+ "is_on": "{entity_name} a\u00e7\u0131k"
+ },
+ "trigger_type": {
+ "target_humidity_changed": "{entity_name} hedef nem de\u011fi\u015fti",
+ "turned_off": "{entity_name} kapat\u0131ld\u0131",
+ "turned_on": "{entity_name} a\u00e7\u0131ld\u0131"
+ }
+ },
+ "state": {
+ "_": {
+ "off": "Kapal\u0131",
+ "on": "A\u00e7\u0131k"
+ }
+ },
+ "title": "Nemlendirici"
+}
\ No newline at end of file
diff --git a/homeassistant/components/humidifier/translations/uk.json b/homeassistant/components/humidifier/translations/uk.json
index 4081c4e13fc281..484f014bd92fbe 100644
--- a/homeassistant/components/humidifier/translations/uk.json
+++ b/homeassistant/components/humidifier/translations/uk.json
@@ -1,8 +1,28 @@
{
"device_automation": {
+ "action_type": {
+ "set_humidity": "{entity_name}: \u0437\u0430\u0434\u0430\u0442\u0438 \u0432\u043e\u043b\u043e\u0433\u0456\u0441\u0442\u044c",
+ "set_mode": "{entity_name}: \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u0440\u0435\u0436\u0438\u043c",
+ "toggle": "{entity_name}: \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u0438",
+ "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438",
+ "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438"
+ },
+ "condition_type": {
+ "is_mode": "{entity_name} \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432 \u0437\u0430\u0434\u0430\u043d\u043e\u043c\u0443 \u0440\u0435\u0436\u0438\u043c\u0456 \u0440\u043e\u0431\u043e\u0442\u0438",
+ "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456"
+ },
"trigger_type": {
+ "target_humidity_changed": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0437\u0430\u0434\u0430\u043d\u043e\u0457 \u0432\u043e\u043b\u043e\u0433\u043e\u0441\u0442\u0456",
"turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
- "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u043e"
+ "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e"
}
- }
+ },
+ "state": {
+ "_": {
+ "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
+ "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e"
+ }
+ },
+ "title": "\u0417\u0432\u043e\u043b\u043e\u0436\u0443\u0432\u0430\u0447"
}
\ No newline at end of file
diff --git a/homeassistant/components/humidifier/translations/zh-Hans.json b/homeassistant/components/humidifier/translations/zh-Hans.json
index 8fa6b0be0dae63..d21c7bf61f79ec 100644
--- a/homeassistant/components/humidifier/translations/zh-Hans.json
+++ b/homeassistant/components/humidifier/translations/zh-Hans.json
@@ -8,9 +8,12 @@
"turn_on": "\u6253\u5f00 {entity_name}"
},
"condition_type": {
- "is_off": "{entity_name} \u5df2\u5173\u95ed"
+ "is_mode": "{entity_name} \u5904\u4e8e\u6307\u5b9a\u6a21\u5f0f",
+ "is_off": "{entity_name} \u5df2\u5173\u95ed",
+ "is_on": "{entity_name} \u5df2\u6253\u5f00"
},
"trigger_type": {
+ "target_humidity_changed": "{entity_name} \u7684\u8bbe\u5b9a\u6e7f\u5ea6\u53d8\u5316",
"turned_off": "{entity_name} \u88ab\u5173\u95ed",
"turned_on": "{entity_name} \u88ab\u6253\u5f00"
}
diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py
index 7555146ba8e4f4..2a5c5061cae614 100644
--- a/homeassistant/components/hunterdouglas_powerview/__init__.py
+++ b/homeassistant/components/hunterdouglas_powerview/__init__.py
@@ -132,9 +132,9 @@ async def async_update_data():
DEVICE_INFO: device_info,
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -180,8 +180,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py
index 34ae94e4b8882a..928c4b4819f467 100644
--- a/homeassistant/components/hunterdouglas_powerview/config_flow.py
+++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py
@@ -10,8 +10,7 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import async_get_device_info
-from .const import DEVICE_NAME, DEVICE_SERIAL_NUMBER, HUB_EXCEPTIONS
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import DEVICE_NAME, DEVICE_SERIAL_NUMBER, DOMAIN, HUB_EXCEPTIONS
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py
index d96beec53aee1f..58c7e90994c542 100644
--- a/homeassistant/components/hunterdouglas_powerview/cover.py
+++ b/homeassistant/components/hunterdouglas_powerview/cover.py
@@ -1,5 +1,6 @@
"""Support for hunter douglas shades."""
import asyncio
+from contextlib import suppress
import logging
from aiopvapi.helpers.constants import ATTR_POSITION1, ATTR_POSITION_DATA
@@ -65,21 +66,21 @@ async def async_setup_entry(hass, entry, async_add_entities):
# possible
shade = PvShade(raw_shade, pv_request)
name_before_refresh = shade.name
- try:
+ with suppress(asyncio.TimeoutError):
async with async_timeout.timeout(1):
await shade.refresh()
- except asyncio.TimeoutError:
- # Forced refresh is not required for setup
- pass
+
if ATTR_POSITION_DATA not in shade.raw_data:
_LOGGER.info(
"The %s shade was skipped because it is missing position data",
name_before_refresh,
)
continue
+ room_id = shade.raw_data.get(ROOM_ID_IN_SHADE)
+ room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "")
entities.append(
PowerViewShade(
- shade, name_before_refresh, room_data, coordinator, device_info
+ coordinator, device_info, room_name, shade, name_before_refresh
)
)
async_add_entities(entities)
@@ -98,21 +99,18 @@ def hass_position_to_hd(hass_positon):
class PowerViewShade(ShadeEntity, CoverEntity):
"""Representation of a powerview shade."""
- def __init__(self, shade, name, room_data, coordinator, device_info):
+ def __init__(self, coordinator, device_info, room_name, shade, name):
"""Initialize the shade."""
- room_id = shade.raw_data.get(ROOM_ID_IN_SHADE)
- super().__init__(coordinator, device_info, shade, name)
+ super().__init__(coordinator, device_info, room_name, shade, name)
self._shade = shade
- self._device_info = device_info
self._is_opening = False
self._is_closing = False
self._last_action_timestamp = 0
self._scheduled_transition_update = None
- self._room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "")
self._current_cover_position = MIN_POSITION
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {STATE_ATTRIBUTE_ROOM_NAME: self._room_name}
diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py
index 4ed68fc3557fd2..679e55e806c797 100644
--- a/homeassistant/components/hunterdouglas_powerview/entity.py
+++ b/homeassistant/components/hunterdouglas_powerview/entity.py
@@ -23,9 +23,10 @@
class HDEntity(CoordinatorEntity):
"""Base class for hunter douglas entities."""
- def __init__(self, coordinator, device_info, unique_id):
+ def __init__(self, coordinator, device_info, room_name, unique_id):
"""Initialize the entity."""
super().__init__(coordinator)
+ self._room_name = room_name
self._unique_id = unique_id
self._device_info = device_info
@@ -45,6 +46,7 @@ def device_info(self):
(dr.CONNECTION_NETWORK_MAC, self._device_info[DEVICE_MAC_ADDRESS])
},
"name": self._device_info[DEVICE_NAME],
+ "suggested_area": self._room_name,
"model": self._device_info[DEVICE_MODEL],
"sw_version": sw_version,
"manufacturer": MANUFACTURER,
@@ -54,9 +56,9 @@ def device_info(self):
class ShadeEntity(HDEntity):
"""Base class for hunter douglas shade entities."""
- def __init__(self, coordinator, device_info, shade, shade_name):
+ def __init__(self, coordinator, device_info, room_name, shade, shade_name):
"""Initialize the shade."""
- super().__init__(coordinator, device_info, shade.id)
+ super().__init__(coordinator, device_info, room_name, shade.id)
self._shade_name = shade_name
self._shade = shade
@@ -67,6 +69,7 @@ def device_info(self):
device_info = {
"identifiers": {(DOMAIN, self._shade.id)},
"name": self._shade_name,
+ "suggested_area": self._room_name,
"manufacturer": MANUFACTURER,
"via_device": (DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]),
}
diff --git a/homeassistant/components/hunterdouglas_powerview/scene.py b/homeassistant/components/hunterdouglas_powerview/scene.py
index 61c93078aa1238..c30cde8d043ca1 100644
--- a/homeassistant/components/hunterdouglas_powerview/scene.py
+++ b/homeassistant/components/hunterdouglas_powerview/scene.py
@@ -49,23 +49,21 @@ async def async_setup_entry(hass, entry, async_add_entities):
coordinator = pv_data[COORDINATOR]
device_info = pv_data[DEVICE_INFO]
- pvscenes = (
- PowerViewScene(
- PvScene(raw_scene, pv_request), room_data, coordinator, device_info
- )
- for scene_id, raw_scene in scene_data.items()
- )
+ pvscenes = []
+ for raw_scene in scene_data.values():
+ scene = PvScene(raw_scene, pv_request)
+ room_name = room_data.get(scene.room_id, {}).get(ROOM_NAME_UNICODE, "")
+ pvscenes.append(PowerViewScene(coordinator, device_info, room_name, scene))
async_add_entities(pvscenes)
class PowerViewScene(HDEntity, Scene):
"""Representation of a Powerview scene."""
- def __init__(self, scene, room_data, coordinator, device_info):
+ def __init__(self, coordinator, device_info, room_name, scene):
"""Initialize the scene."""
- super().__init__(coordinator, device_info, scene.id)
+ super().__init__(coordinator, device_info, room_name, scene.id)
self._scene = scene
- self._room_name = room_data.get(scene.room_id, {}).get(ROOM_NAME_UNICODE, "")
@property
def name(self):
@@ -73,7 +71,7 @@ def name(self):
return self._scene.name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {STATE_ATTRIBUTE_ROOM_NAME: self._room_name}
diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py
index 6241ddd4d62dd0..d66671fe1ea313 100644
--- a/homeassistant/components/hunterdouglas_powerview/sensor.py
+++ b/homeassistant/components/hunterdouglas_powerview/sensor.py
@@ -1,6 +1,7 @@
"""Support for hunterdouglass_powerview sensors."""
from aiopvapi.resources.shade import factory as PvShade
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE
from homeassistant.core import callback
@@ -9,7 +10,10 @@
DEVICE_INFO,
DOMAIN,
PV_API,
+ PV_ROOM_DATA,
PV_SHADE_DATA,
+ ROOM_ID_IN_SHADE,
+ ROOM_NAME_UNICODE,
SHADE_BATTERY_LEVEL,
SHADE_BATTERY_LEVEL_MAX,
)
@@ -20,6 +24,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the hunter douglas shades sensors."""
pv_data = hass.data[DOMAIN][entry.entry_id]
+ room_data = pv_data[PV_ROOM_DATA]
shade_data = pv_data[PV_SHADE_DATA]
pv_request = pv_data[PV_API]
coordinator = pv_data[COORDINATOR]
@@ -31,15 +36,17 @@ async def async_setup_entry(hass, entry, async_add_entities):
if SHADE_BATTERY_LEVEL not in shade.raw_data:
continue
name_before_refresh = shade.name
+ room_id = shade.raw_data.get(ROOM_ID_IN_SHADE)
+ room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "")
entities.append(
PowerViewShadeBatterySensor(
- coordinator, device_info, shade, name_before_refresh
+ coordinator, device_info, room_name, shade, name_before_refresh
)
)
async_add_entities(entities)
-class PowerViewShadeBatterySensor(ShadeEntity):
+class PowerViewShadeBatterySensor(ShadeEntity, SensorEntity):
"""Representation of an shade battery charge sensor."""
@property
diff --git a/homeassistant/components/hunterdouglas_powerview/translations/hu.json b/homeassistant/components/hunterdouglas_powerview/translations/hu.json
index 61461d1796ccf2..063e0dad3c4428 100644
--- a/homeassistant/components/hunterdouglas_powerview/translations/hu.json
+++ b/homeassistant/components/hunterdouglas_powerview/translations/hu.json
@@ -4,8 +4,8 @@
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
},
"error": {
- "cannot_connect": "Nem siker\u00fclt csatlakozni, pr\u00f3b\u00e1lkozzon \u00fajra.",
- "unknown": "V\u00e1ratlan hiba"
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"step": {
"user": {
diff --git a/homeassistant/components/hunterdouglas_powerview/translations/id.json b/homeassistant/components/hunterdouglas_powerview/translations/id.json
new file mode 100644
index 00000000000000..2d21f87bf67020
--- /dev/null
+++ b/homeassistant/components/hunterdouglas_powerview/translations/id.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "link": {
+ "description": "Ingin menyiapkan {name} ({host})?",
+ "title": "Hubungkan ke PowerView Hub"
+ },
+ "user": {
+ "data": {
+ "host": "Alamat IP"
+ },
+ "title": "Hubungkan ke PowerView Hub"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hunterdouglas_powerview/translations/ko.json b/homeassistant/components/hunterdouglas_powerview/translations/ko.json
index cba1b76168286d..5520800c38d644 100644
--- a/homeassistant/components/hunterdouglas_powerview/translations/ko.json
+++ b/homeassistant/components/hunterdouglas_powerview/translations/ko.json
@@ -4,12 +4,12 @@
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"link": {
- "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "description": "{name} ({host})\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "PowerView \ud5c8\ube0c\uc5d0 \uc5f0\uacb0\ud558\uae30"
},
"user": {
diff --git a/homeassistant/components/hunterdouglas_powerview/translations/tr.json b/homeassistant/components/hunterdouglas_powerview/translations/tr.json
new file mode 100644
index 00000000000000..01b0359789e8e8
--- /dev/null
+++ b/homeassistant/components/hunterdouglas_powerview/translations/tr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0130p Adresi"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hunterdouglas_powerview/translations/uk.json b/homeassistant/components/hunterdouglas_powerview/translations/uk.json
new file mode 100644
index 00000000000000..959fcff12b0cdd
--- /dev/null
+++ b/homeassistant/components/hunterdouglas_powerview/translations/uk.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "link": {
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} ({host})?",
+ "title": "Hunter Douglas PowerView"
+ },
+ "user": {
+ "data": {
+ "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430"
+ },
+ "title": "Hunter Douglas PowerView"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hvv_departures/__init__.py b/homeassistant/components/hvv_departures/__init__.py
index e003b25ea85a50..c90e5cb6d9c123 100644
--- a/homeassistant/components/hvv_departures/__init__.py
+++ b/homeassistant/components/hvv_departures/__init__.py
@@ -32,9 +32,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = hub
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -45,8 +45,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/hvv_departures/binary_sensor.py b/homeassistant/components/hvv_departures/binary_sensor.py
index 7d19fcc8fdfcc1..45ac0e45ad970d 100644
--- a/homeassistant/components/hvv_departures/binary_sensor.py
+++ b/homeassistant/components/hvv_departures/binary_sensor.py
@@ -92,7 +92,7 @@ async def async_update_data():
raise UpdateFailed(f"Authentication failed: {err}") from err
except ClientConnectorError as err:
raise UpdateFailed(f"Network not available: {err}") from err
- except Exception as err: # pylint: disable=broad-except
+ except Exception as err:
raise UpdateFailed(f"Error occurred while fetching data: {err}") from err
coordinator = DataUpdateCoordinator(
@@ -174,7 +174,7 @@ def device_class(self):
return DEVICE_CLASS_PROBLEM
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if not (
self.coordinator.last_update_success
diff --git a/homeassistant/components/hvv_departures/config_flow.py b/homeassistant/components/hvv_departures/config_flow.py
index 1a49bffd2f55e5..556505101eb74c 100644
--- a/homeassistant/components/hvv_departures/config_flow.py
+++ b/homeassistant/components/hvv_departures/config_flow.py
@@ -11,12 +11,7 @@
from homeassistant.helpers import aiohttp_client
import homeassistant.helpers.config_validation as cv
-from .const import ( # pylint:disable=unused-import
- CONF_FILTER,
- CONF_REAL_TIME,
- CONF_STATION,
- DOMAIN,
-)
+from .const import CONF_FILTER, CONF_REAL_TIME, CONF_STATION, DOMAIN
from .hub import GTIHub
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py
index d6957e6beec29b..5bc70c7a3b4cd6 100644
--- a/homeassistant/components/hvv_departures/sensor.py
+++ b/homeassistant/components/hvv_departures/sensor.py
@@ -6,9 +6,9 @@
from pygti.exceptions import InvalidAuth
from pytz import timezone
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ID, DEVICE_CLASS_TIMESTAMP
from homeassistant.helpers import aiohttp_client
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from homeassistant.util.dt import utcnow
@@ -18,7 +18,6 @@
MAX_LIST = 20
MAX_TIME_OFFSET = 360
ICON = "mdi:bus"
-UNIT_OF_MEASUREMENT = "min"
ATTR_DEPARTURE = "departure"
ATTR_LINE = "line"
@@ -43,7 +42,7 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
async_add_devices([sensor], True)
-class HVVDepartureSensor(Entity):
+class HVVDepartureSensor(SensorEntity):
"""HVVDepartureSensor class."""
def __init__(self, hass, config_entry, session, hub):
@@ -199,6 +198,6 @@ def device_class(self):
return DEVICE_CLASS_TIMESTAMP
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self.attr
diff --git a/homeassistant/components/hvv_departures/translations/he.json b/homeassistant/components/hvv_departures/translations/he.json
new file mode 100644
index 00000000000000..ac90b3264eab33
--- /dev/null
+++ b/homeassistant/components/hvv_departures/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hvv_departures/translations/hu.json b/homeassistant/components/hvv_departures/translations/hu.json
new file mode 100644
index 00000000000000..91da2d13a7c9d9
--- /dev/null
+++ b/homeassistant/components/hvv_departures/translations/hu.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "Be\u00e1ll\u00edt\u00e1sok"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hvv_departures/translations/id.json b/homeassistant/components/hvv_departures/translations/id.json
new file mode 100644
index 00000000000000..d43b306d2925db
--- /dev/null
+++ b/homeassistant/components/hvv_departures/translations/id.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "no_results": "Tidak ada hasil. Coba stasiun/alamat lainnya"
+ },
+ "step": {
+ "station": {
+ "data": {
+ "station": "Stasiun/Alamat"
+ },
+ "title": "Masukkan Stasiun/Alamat"
+ },
+ "station_select": {
+ "data": {
+ "station": "Stasiun/Alamat"
+ },
+ "title": "Pilih Stasiun/Alamat"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "title": "Hubungkan ke API HVV"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "filter": "Pilih jalur",
+ "offset": "Tenggang (menit)",
+ "real_time": "Gunakan data waktu nyata"
+ },
+ "description": "Ubah opsi untuk sensor keberangkatan ini",
+ "title": "Opsi"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hvv_departures/translations/ko.json b/homeassistant/components/hvv_departures/translations/ko.json
index 41c7f44be7ff0d..ca8de2ff5cd5e2 100644
--- a/homeassistant/components/hvv_departures/translations/ko.json
+++ b/homeassistant/components/hvv_departures/translations/ko.json
@@ -4,7 +4,7 @@
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"no_results": "\uacb0\uacfc\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\ub978 \uc2a4\ud14c\uc774\uc158\uc774\ub098 \uc8fc\uc18c\ub97c \uc0ac\uc6a9\ud574\uc8fc\uc138\uc694"
},
@@ -13,7 +13,7 @@
"data": {
"station": "\uc2a4\ud14c\uc774\uc158 / \uc8fc\uc18c"
},
- "title": "\uc2a4\ud14c\uc774\uc158 / \uc8fc\uc18c \uc785\ub825\ud558\uae30"
+ "title": "\uc2a4\ud14c\uc774\uc158 / \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694"
},
"station_select": {
"data": {
@@ -27,7 +27,7 @@
"password": "\ube44\ubc00\ubc88\ud638",
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
},
- "title": "HVV API \uc5d0 \uc5f0\uacb0\ud558\uae30"
+ "title": "HVV API\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
},
diff --git a/homeassistant/components/hvv_departures/translations/nl.json b/homeassistant/components/hvv_departures/translations/nl.json
index 8c80ae5b942271..8782499ee05965 100644
--- a/homeassistant/components/hvv_departures/translations/nl.json
+++ b/homeassistant/components/hvv_departures/translations/nl.json
@@ -5,9 +5,22 @@
},
"error": {
"cannot_connect": "Kon niet verbinden",
- "invalid_auth": "Ongeldige authenticatie"
+ "invalid_auth": "Ongeldige authenticatie",
+ "no_results": "Geen resultaten. Probeer het met een ander station/adres"
},
"step": {
+ "station": {
+ "data": {
+ "station": "Station/Adres"
+ },
+ "title": "Voer station/adres in"
+ },
+ "station_select": {
+ "data": {
+ "station": "Station/Adres"
+ },
+ "title": "Selecteer Station/Adres"
+ },
"user": {
"data": {
"host": "Host",
@@ -17,5 +30,18 @@
"title": "Maak verbinding met de HVV API"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "filter": "Selecteer lijnen",
+ "offset": "Afwijking (minuten)",
+ "real_time": "Gebruik realtime gegevens"
+ },
+ "description": "Wijzig opties voor deze vertreksensor",
+ "title": "Opties"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/hvv_departures/translations/ru.json b/homeassistant/components/hvv_departures/translations/ru.json
index ff5819a562d7c2..55e60b23c32c15 100644
--- a/homeassistant/components/hvv_departures/translations/ru.json
+++ b/homeassistant/components/hvv_departures/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"no_results": "\u041d\u0435\u0442 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u0432. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0435\u0439 / \u0430\u0434\u0440\u0435\u0441\u043e\u043c."
},
"step": {
@@ -25,7 +25,7 @@
"data": {
"host": "\u0425\u043e\u0441\u0442",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a API HVV"
}
diff --git a/homeassistant/components/hvv_departures/translations/tr.json b/homeassistant/components/hvv_departures/translations/tr.json
new file mode 100644
index 00000000000000..74fc593062b960
--- /dev/null
+++ b/homeassistant/components/hvv_departures/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hvv_departures/translations/uk.json b/homeassistant/components/hvv_departures/translations/uk.json
new file mode 100644
index 00000000000000..364d351a99c3dc
--- /dev/null
+++ b/homeassistant/components/hvv_departures/translations/uk.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "no_results": "\u041d\u0435\u043c\u0430\u0454 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0456\u0432. \u0421\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437 \u0456\u043d\u0448\u043e\u044e \u0441\u0442\u0430\u043d\u0446\u0456\u0454\u044e / \u0430\u0434\u0440\u0435\u0441\u043e\u044e."
+ },
+ "step": {
+ "station": {
+ "data": {
+ "station": "\u0421\u0442\u0430\u043d\u0446\u0456\u044f / \u0410\u0434\u0440\u0435\u0441\u0430"
+ },
+ "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0456\u044e / \u0430\u0434\u0440\u0435\u0441\u0443"
+ },
+ "station_select": {
+ "data": {
+ "station": "\u0421\u0442\u0430\u043d\u0446\u0456\u044f / \u0410\u0434\u0440\u0435\u0441\u0430"
+ },
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0456\u044e / \u0430\u0434\u0440\u0435\u0441\u0443"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e API HVV"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "filter": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043b\u0456\u043d\u0456\u0457",
+ "offset": "\u0417\u043c\u0456\u0449\u0435\u043d\u043d\u044f (\u0432 \u0445\u0432\u0438\u043b\u0438\u043d\u0430\u0445)",
+ "real_time": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u0430\u043d\u0456 \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0447\u0430\u0441\u0443"
+ },
+ "description": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u044f",
+ "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py
index 08827baae6846a..1f1b2c03157c22 100644
--- a/homeassistant/components/hydrawise/__init__.py
+++ b/homeassistant/components/hydrawise/__init__.py
@@ -144,14 +144,7 @@ def _update_callback(self):
self.async_schedule_update_ha_state(True)
@property
- def unit_of_measurement(self):
- """Return the units of measurement."""
- return DEVICE_MAP[self._sensor_type][
- DEVICE_MAP_INDEX.index("UNIT_OF_MEASURE_INDEX")
- ]
-
- @property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION, "identifier": self.data.get("relay")}
diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py
index 6a0c6ab0d80bd1..62108afbded0b2 100644
--- a/homeassistant/components/hydrawise/sensor.py
+++ b/homeassistant/components/hydrawise/sensor.py
@@ -3,12 +3,12 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_MONITORED_CONDITIONS
import homeassistant.helpers.config_validation as cv
from homeassistant.util import dt
-from . import DATA_HYDRAWISE, SENSORS, HydrawiseEntity
+from . import DATA_HYDRAWISE, DEVICE_MAP, DEVICE_MAP_INDEX, SENSORS, HydrawiseEntity
_LOGGER = logging.getLogger(__name__)
@@ -36,7 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class HydrawiseSensor(HydrawiseEntity):
+class HydrawiseSensor(HydrawiseEntity, SensorEntity):
"""A sensor implementation for Hydrawise device."""
@property
@@ -44,6 +44,13 @@ def state(self):
"""Return the state of the sensor."""
return self._state
+ @property
+ def unit_of_measurement(self):
+ """Return the units of measurement."""
+ return DEVICE_MAP[self._sensor_type][
+ DEVICE_MAP_INDEX.index("UNIT_OF_MEASURE_INDEX")
+ ]
+
def update(self):
"""Get the latest data and updates the states."""
mydata = self.hass.data[DATA_HYDRAWISE].data
diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py
index b3606880f8c12c..03b892ce83b706 100644
--- a/homeassistant/components/hyperion/__init__.py
+++ b/homeassistant/components/hyperion/__init__.py
@@ -1,11 +1,13 @@
"""The Hyperion component."""
+from __future__ import annotations
import asyncio
+from contextlib import suppress
import logging
-from typing import Any, Callable, Dict, List, Optional, Set, Tuple, cast
+from typing import Any, Callable, cast
+from awesomeversion import AwesomeVersion
from hyperion import client, const as hyperion_const
-from pkg_resources import parse_version
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
@@ -70,7 +72,7 @@ def get_hyperion_unique_id(server_id: str, instance: int, name: str) -> str:
return f"{server_id}_{instance}_{name}"
-def split_hyperion_unique_id(unique_id: str) -> Optional[Tuple[str, int, str]]:
+def split_hyperion_unique_id(unique_id: str) -> tuple[str, int, str] | None:
"""Split a unique_id into a (server_id, instance, type) tuple."""
data = tuple(unique_id.split("_", 2))
if len(data) != 3:
@@ -92,7 +94,7 @@ def create_hyperion_client(
async def async_create_connect_hyperion_client(
*args: Any,
**kwargs: Any,
-) -> Optional[client.HyperionClient]:
+) -> client.HyperionClient | None:
"""Create and connect a Hyperion Client."""
hyperion_client = create_hyperion_client(*args, **kwargs)
@@ -158,8 +160,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
raise ConfigEntryNotReady
version = await hyperion_client.async_sysinfo_version()
if version is not None:
- try:
- if parse_version(version) < parse_version(HYPERION_VERSION_WARN_CUTOFF):
+ with suppress(ValueError):
+ if AwesomeVersion(version) < AwesomeVersion(HYPERION_VERSION_WARN_CUTOFF):
_LOGGER.warning(
"Using a Hyperion server version < %s is not recommended -- "
"some features may be unavailable or may not function correctly. "
@@ -167,8 +169,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
HYPERION_VERSION_WARN_CUTOFF,
HYPERION_RELEASES_URL,
)
- except ValueError:
- pass
# Client needs authentication, but no token provided? => Reauth.
auth_resp = await hyperion_client.async_is_auth_required()
@@ -207,17 +207,17 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
CONF_ON_UNLOAD: [],
}
- async def async_instances_to_clients(response: Dict[str, Any]) -> None:
+ async def async_instances_to_clients(response: dict[str, Any]) -> None:
"""Convert instances to Hyperion clients."""
if not response or hyperion_const.KEY_DATA not in response:
return
await async_instances_to_clients_raw(response[hyperion_const.KEY_DATA])
- async def async_instances_to_clients_raw(instances: List[Dict[str, Any]]) -> None:
+ async def async_instances_to_clients_raw(instances: list[dict[str, Any]]) -> None:
"""Convert instances to Hyperion clients."""
registry = await async_get_registry(hass)
- running_instances: Set[int] = set()
- stopped_instances: Set[int] = set()
+ running_instances: set[int] = set()
+ stopped_instances: set[int] = set()
existing_instances = hass.data[DOMAIN][config_entry.entry_id][
CONF_INSTANCE_CLIENTS
]
@@ -281,12 +281,13 @@ async def async_instances_to_clients_raw(instances: List[Dict[str, Any]]) -> Non
async def setup_then_listen() -> None:
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_setup(config_entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ for platform in PLATFORMS
]
)
assert hyperion_client
- await async_instances_to_clients_raw(hyperion_client.instances)
+ if hyperion_client.instances is not None:
+ await async_instances_to_clients_raw(hyperion_client.instances)
hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append(
config_entry.add_update_listener(_async_entry_updated)
)
@@ -309,8 +310,8 @@ async def async_unload_entry(
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py
index f4528b0efbe68d..7ceedcbf00544a 100644
--- a/homeassistant/components/hyperion/config_flow.py
+++ b/homeassistant/components/hyperion/config_flow.py
@@ -2,8 +2,9 @@
from __future__ import annotations
import asyncio
+from contextlib import suppress
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from urllib.parse import urlparse
from hyperion import client, const
@@ -26,14 +27,15 @@
CONF_TOKEN,
)
from homeassistant.core import callback
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
from . import create_hyperion_client
-
-# pylint: disable=unused-import
from .const import (
CONF_AUTH_ID,
CONF_CREATE_TOKEN,
+ CONF_EFFECT_HIDE_LIST,
+ CONF_EFFECT_SHOW_LIST,
CONF_PRIORITY,
DEFAULT_ORIGIN,
DEFAULT_PRIORITY,
@@ -111,9 +113,9 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Instantiate config flow."""
- self._data: Dict[str, Any] = {}
- self._request_token_task: Optional[asyncio.Task] = None
- self._auth_id: Optional[str] = None
+ self._data: dict[str, Any] = {}
+ self._request_token_task: asyncio.Task | None = None
+ self._auth_id: str | None = None
self._require_confirm: bool = False
self._port_ui: int = const.DEFAULT_PORT_UI
@@ -128,7 +130,7 @@ def _create_client(self, raw_connection: bool = False) -> client.HyperionClient:
async def _advance_to_auth_step_if_necessary(
self, hyperion_client: client.HyperionClient
- ) -> Dict[str, Any]:
+ ) -> dict[str, Any]:
"""Determine if auth is required."""
auth_resp = await hyperion_client.async_is_auth_required()
@@ -143,7 +145,7 @@ async def _advance_to_auth_step_if_necessary(
async def async_step_reauth(
self,
config_data: ConfigType,
- ) -> Dict[str, Any]:
+ ) -> dict[str, Any]:
"""Handle a reauthentication flow."""
self._data = dict(config_data)
async with self._create_client(raw_connection=True) as hyperion_client:
@@ -152,8 +154,8 @@ async def async_step_reauth(
return await self._advance_to_auth_step_if_necessary(hyperion_client)
async def async_step_ssdp( # type: ignore[override]
- self, discovery_info: Dict[str, Any]
- ) -> Dict[str, Any]:
+ self, discovery_info: dict[str, Any]
+ ) -> dict[str, Any]:
"""Handle a flow initiated by SSDP."""
# Sample data provided by SSDP: {
# 'ssdp_location': 'http://192.168.0.1:8090/description.xml',
@@ -221,11 +223,10 @@ async def async_step_ssdp( # type: ignore[override]
return self.async_abort(reason="cannot_connect")
return await self._advance_to_auth_step_if_necessary(hyperion_client)
- # pylint: disable=arguments-differ
async def async_step_user(
self,
- user_input: Optional[ConfigType] = None,
- ) -> Dict[str, Any]:
+ user_input: ConfigType | None = None,
+ ) -> dict[str, Any]:
"""Handle a flow initiated by the user."""
errors = {}
if user_input:
@@ -255,22 +256,19 @@ async def _cancel_request_token_task(self) -> None:
if not self._request_token_task.done():
self._request_token_task.cancel()
- try:
+ with suppress(asyncio.CancelledError):
await self._request_token_task
- except asyncio.CancelledError:
- pass
self._request_token_task = None
async def _request_token_task_func(self, auth_id: str) -> None:
"""Send an async_request_token request."""
- auth_resp: Optional[Dict[str, Any]] = None
+ auth_resp: dict[str, Any] | None = None
async with self._create_client(raw_connection=True) as hyperion_client:
if hyperion_client:
# The Hyperion-py client has a default timeout of 3 minutes on this request.
auth_resp = await hyperion_client.async_request_token(
comment=DEFAULT_ORIGIN, id=auth_id
)
- assert self.hass
await self.hass.config_entries.flow.async_configure(
flow_id=self.flow_id, user_input=auth_resp
)
@@ -285,7 +283,7 @@ def _get_hyperion_url(self) -> str:
# used to open a URL, that the user already knows the address of).
return f"http://{self._data[CONF_HOST]}:{self._port_ui}"
- async def _can_login(self) -> Optional[bool]:
+ async def _can_login(self) -> bool | None:
"""Verify login details."""
async with self._create_client(raw_connection=True) as hyperion_client:
if not hyperion_client:
@@ -298,8 +296,8 @@ async def _can_login(self) -> Optional[bool]:
async def async_step_auth(
self,
- user_input: Optional[ConfigType] = None,
- ) -> Dict[str, Any]:
+ user_input: ConfigType | None = None,
+ ) -> dict[str, Any]:
"""Handle the auth step of a flow."""
errors = {}
if user_input:
@@ -327,8 +325,8 @@ async def async_step_auth(
)
async def async_step_create_token(
- self, user_input: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, user_input: ConfigType | None = None
+ ) -> dict[str, Any]:
"""Send a request for a new token."""
if user_input is None:
self._auth_id = client.generate_random_auth_id()
@@ -344,7 +342,6 @@ async def async_step_create_token(
# Start a task in the background requesting a new token. The next step will
# wait on the response (which includes the user needing to visit the Hyperion
# UI to approve the request for a new token).
- assert self.hass
assert self._auth_id is not None
self._request_token_task = self.hass.async_create_task(
self._request_token_task_func(self._auth_id)
@@ -354,8 +351,8 @@ async def async_step_create_token(
)
async def async_step_create_token_external(
- self, auth_resp: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, auth_resp: ConfigType | None = None
+ ) -> dict[str, Any]:
"""Handle completion of the request for a new token."""
if auth_resp is not None and client.ResponseOK(auth_resp):
token = auth_resp.get(const.KEY_INFO, {}).get(const.KEY_TOKEN)
@@ -367,8 +364,8 @@ async def async_step_create_token_external(
return self.async_external_step_done(next_step_id="create_token_fail")
async def async_step_create_token_success(
- self, _: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, _: ConfigType | None = None
+ ) -> dict[str, Any]:
"""Create an entry after successful token creation."""
# Clean-up the request task.
await self._cancel_request_token_task()
@@ -383,16 +380,16 @@ async def async_step_create_token_success(
return await self.async_step_confirm()
async def async_step_create_token_fail(
- self, _: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, _: ConfigType | None = None
+ ) -> dict[str, Any]:
"""Show an error on the auth form."""
# Clean-up the request task.
await self._cancel_request_token_task()
return self.async_abort(reason="auth_new_token_not_granted_error")
async def async_step_confirm(
- self, user_input: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, user_input: ConfigType | None = None
+ ) -> dict[str, Any]:
"""Get final confirmation before entry creation."""
if user_input is None and self._require_confirm:
return self.async_show_form(
@@ -414,9 +411,7 @@ async def async_step_confirm(
entry = await self.async_set_unique_id(hyperion_id, raise_on_progress=False)
- # pylint: disable=no-member
if self.context.get(CONF_SOURCE) == SOURCE_REAUTH and entry is not None:
- assert self.hass
self.hass.config_entries.async_update_entry(entry, data=self._data)
# Need to manually reload, as the listener won't have been installed because
# the initial load did not succeed (the reauth flow will not be initiated if
@@ -426,7 +421,6 @@ async def async_step_confirm(
self._abort_if_unique_id_configured()
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
return self.async_create_entry(
title=f"{self._data[CONF_HOST]}:{self._data[CONF_PORT]}", data=self._data
)
@@ -445,13 +439,44 @@ def __init__(self, config_entry: ConfigEntry):
"""Initialize a Hyperion options flow."""
self._config_entry = config_entry
+ def _create_client(self) -> client.HyperionClient:
+ """Create and connect a client instance."""
+ return create_hyperion_client(
+ self._config_entry.data[CONF_HOST],
+ self._config_entry.data[CONF_PORT],
+ token=self._config_entry.data.get(CONF_TOKEN),
+ )
+
async def async_step_init(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Manage the options."""
+
+ effects = {source: source for source in const.KEY_COMPONENTID_EXTERNAL_SOURCES}
+ async with self._create_client() as hyperion_client:
+ if not hyperion_client:
+ return self.async_abort(reason="cannot_connect")
+ for effect in hyperion_client.effects or []:
+ if const.KEY_NAME in effect:
+ effects[effect[const.KEY_NAME]] = effect[const.KEY_NAME]
+
+ # If a new effect is added to Hyperion, we always want it to show by default. So
+ # rather than store a 'show list' in the config entry, we store a 'hide list'.
+ # However, it's more intuitive to ask the user to select which effects to show,
+ # so we inverse the meaning prior to storage.
+
if user_input is not None:
+ effect_show_list = user_input.pop(CONF_EFFECT_SHOW_LIST)
+ user_input[CONF_EFFECT_HIDE_LIST] = sorted(
+ set(effects) - set(effect_show_list)
+ )
return self.async_create_entry(title="", data=user_input)
+ default_effect_show_list = list(
+ set(effects)
+ - set(self._config_entry.options.get(CONF_EFFECT_HIDE_LIST, []))
+ )
+
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
@@ -462,6 +487,10 @@ async def async_step_init(
CONF_PRIORITY, DEFAULT_PRIORITY
),
): vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
+ vol.Optional(
+ CONF_EFFECT_SHOW_LIST,
+ default=default_effect_show_list,
+ ): cv.multi_select(effects),
}
),
)
diff --git a/homeassistant/components/hyperion/const.py b/homeassistant/components/hyperion/const.py
index 64c2f20052b911..994ef580c9153c 100644
--- a/homeassistant/components/hyperion/const.py
+++ b/homeassistant/components/hyperion/const.py
@@ -31,6 +31,8 @@
CONF_ON_UNLOAD = "ON_UNLOAD"
CONF_PRIORITY = "priority"
CONF_ROOT_CLIENT = "ROOT_CLIENT"
+CONF_EFFECT_HIDE_LIST = "effect_hide_list"
+CONF_EFFECT_SHOW_LIST = "effect_show_list"
DEFAULT_NAME = "Hyperion"
DEFAULT_ORIGIN = "Home Assistant"
diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py
index ce672194b9adf6..248a45ec753bd2 100644
--- a/homeassistant/components/hyperion/light.py
+++ b/homeassistant/components/hyperion/light.py
@@ -1,9 +1,10 @@
"""Support for Hyperion-NG remotes."""
from __future__ import annotations
+import functools
import logging
from types import MappingProxyType
-from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
+from typing import Any, Callable, Mapping, Sequence
from hyperion import client, const
@@ -27,6 +28,7 @@
from . import get_hyperion_unique_id, listen_for_instance_updates
from .const import (
+ CONF_EFFECT_HIDE_LIST,
CONF_INSTANCE_CLIENTS,
CONF_PRIORITY,
DEFAULT_ORIGIN,
@@ -62,7 +64,7 @@
DEFAULT_NAME = "Hyperion"
DEFAULT_PORT = const.DEFAULT_PORT_JSON
DEFAULT_HDMI_PRIORITY = 880
-DEFAULT_EFFECT_LIST: List[str] = []
+DEFAULT_EFFECT_LIST: list[str] = []
SUPPORT_HYPERION = SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT
@@ -141,12 +143,12 @@ def __init__(
self._rgb_color: Sequence[int] = DEFAULT_COLOR
self._effect: str = KEY_EFFECT_SOLID
- self._static_effect_list: List[str] = [KEY_EFFECT_SOLID]
+ self._static_effect_list: list[str] = [KEY_EFFECT_SOLID]
if self._support_external_effects:
self._static_effect_list += list(const.KEY_COMPONENTID_EXTERNAL_SOURCES)
- self._effect_list: List[str] = self._static_effect_list[:]
+ self._effect_list: list[str] = self._static_effect_list[:]
- self._client_callbacks = {
+ self._client_callbacks: Mapping[str, Callable[[dict[str, Any]], None]] = {
f"{const.KEY_ADJUSTMENT}-{const.KEY_UPDATE}": self._update_adjustment,
f"{const.KEY_COMPONENTS}-{const.KEY_UPDATE}": self._update_components,
f"{const.KEY_EFFECTS}-{const.KEY_UPDATE}": self._update_effect_list,
@@ -175,7 +177,7 @@ def brightness(self) -> int:
return self._brightness
@property
- def hs_color(self) -> Tuple[float, float]:
+ def hs_color(self) -> tuple[float, float]:
"""Return last color value set."""
return color_util.color_RGB_to_hs(*self._rgb_color)
@@ -195,7 +197,7 @@ def effect(self) -> str:
return self._effect
@property
- def effect_list(self) -> List[str]:
+ def effect_list(self) -> list[str]:
"""Return the list of supported effects."""
return self._effect_list
@@ -216,7 +218,10 @@ def unique_id(self) -> str:
def _get_option(self, key: str) -> Any:
"""Get a value from the provided options."""
- defaults = {CONF_PRIORITY: DEFAULT_PRIORITY}
+ defaults = {
+ CONF_PRIORITY: DEFAULT_PRIORITY,
+ CONF_EFFECT_HIDE_LIST: [],
+ }
return self._options.get(key, defaults[key])
async def async_turn_on(self, **kwargs: Any) -> None:
@@ -235,9 +240,10 @@ async def async_turn_on(self, **kwargs: Any) -> None:
# == Set brightness ==
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS]
- for item in self._client.adjustment:
- if const.KEY_ID in item:
- if not await self._client.async_send_set_adjustment(
+ for item in self._client.adjustment or []:
+ if (
+ const.KEY_ID in item
+ and not await self._client.async_send_set_adjustment(
**{
const.KEY_ADJUSTMENT: {
const.KEY_BRIGHTNESS: int(
@@ -246,8 +252,9 @@ async def async_turn_on(self, **kwargs: Any) -> None:
const.KEY_ID: item[const.KEY_ID],
}
}
- ):
- return
+ )
+ ):
+ return
# == Set an external source
if (
@@ -304,9 +311,9 @@ async def async_turn_on(self, **kwargs: Any) -> None:
def _set_internal_state(
self,
- brightness: Optional[int] = None,
- rgb_color: Optional[Sequence[int]] = None,
- effect: Optional[str] = None,
+ brightness: int | None = None,
+ rgb_color: Sequence[int] | None = None,
+ effect: str | None = None,
) -> None:
"""Set the internal state."""
if brightness is not None:
@@ -317,12 +324,12 @@ def _set_internal_state(
self._effect = effect
@callback
- def _update_components(self, _: Optional[Dict[str, Any]] = None) -> None:
+ def _update_components(self, _: dict[str, Any] | None = None) -> None:
"""Update Hyperion components."""
self.async_write_ha_state()
@callback
- def _update_adjustment(self, _: Optional[Dict[str, Any]] = None) -> None:
+ def _update_adjustment(self, _: dict[str, Any] | None = None) -> None:
"""Update Hyperion adjustments."""
if self._client.adjustment:
brightness_pct = self._client.adjustment[0].get(
@@ -336,7 +343,7 @@ def _update_adjustment(self, _: Optional[Dict[str, Any]] = None) -> None:
self.async_write_ha_state()
@callback
- def _update_priorities(self, _: Optional[Dict[str, Any]] = None) -> None:
+ def _update_priorities(self, _: dict[str, Any] | None = None) -> None:
"""Update Hyperion priorities."""
priority = self._get_priority_entry_that_dictates_state()
if priority and self._allow_priority_update(priority):
@@ -360,17 +367,23 @@ def _update_priorities(self, _: Optional[Dict[str, Any]] = None) -> None:
self.async_write_ha_state()
@callback
- def _update_effect_list(self, _: Optional[Dict[str, Any]] = None) -> None:
+ def _update_effect_list(self, _: dict[str, Any] | None = None) -> None:
"""Update Hyperion effects."""
if not self._client.effects:
return
- effect_list: List[str] = []
+ effect_list: list[str] = []
+ hide_effects = self._get_option(CONF_EFFECT_HIDE_LIST)
+
for effect in self._client.effects or []:
if const.KEY_NAME in effect:
- effect_list.append(effect[const.KEY_NAME])
- if effect_list:
- self._effect_list = self._static_effect_list + effect_list
- self.async_write_ha_state()
+ effect_name = effect[const.KEY_NAME]
+ if effect_name not in hide_effects:
+ effect_list.append(effect_name)
+
+ self._effect_list = [
+ effect for effect in self._static_effect_list if effect not in hide_effects
+ ] + effect_list
+ self.async_write_ha_state()
@callback
def _update_full_state(self) -> None:
@@ -390,18 +403,17 @@ def _update_full_state(self) -> None:
)
@callback
- def _update_client(self, _: Optional[Dict[str, Any]] = None) -> None:
+ def _update_client(self, _: dict[str, Any] | None = None) -> None:
"""Update client connection state."""
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Register callbacks when entity added to hass."""
- assert self.hass
self.async_on_remove(
async_dispatcher_connect(
self.hass,
SIGNAL_ENTITY_REMOVE.format(self._unique_id),
- self.async_remove,
+ functools.partial(self.async_remove, force_remove=True),
)
)
@@ -419,13 +431,18 @@ def _support_external_effects(self) -> bool:
"""Whether or not to support setting external effects from the light entity."""
return True
- def _get_priority_entry_that_dictates_state(self) -> Optional[Dict[str, Any]]:
+ def _get_priority_entry_that_dictates_state(self) -> dict[str, Any] | None:
"""Get the relevant Hyperion priority entry to consider."""
# Return the visible priority (whether or not it is the HA priority).
- return self._client.visible_priority # type: ignore[no-any-return]
+
+ # Explicit type specifier to ensure this works when the underlying (typed)
+ # library is installed along with the tests. Casts would trigger a
+ # redundant-cast warning in this case.
+ priority: dict[str, Any] | None = self._client.visible_priority
+ return priority
# pylint: disable=no-self-use
- def _allow_priority_update(self, priority: Optional[Dict[str, Any]] = None) -> bool:
+ def _allow_priority_update(self, priority: dict[str, Any] | None = None) -> bool:
"""Determine whether to allow a priority to update internal state."""
return True
@@ -520,7 +537,7 @@ def _support_external_effects(self) -> bool:
"""Whether or not to support setting external effects from the light entity."""
return False
- def _get_priority_entry_that_dictates_state(self) -> Optional[Dict[str, Any]]:
+ def _get_priority_entry_that_dictates_state(self) -> dict[str, Any] | None:
"""Get the relevant Hyperion priority entry to consider."""
# Return the active priority (if any) at the configured HA priority.
for candidate in self._client.priorities or []:
@@ -529,11 +546,15 @@ def _get_priority_entry_that_dictates_state(self) -> Optional[Dict[str, Any]]:
if candidate[const.KEY_PRIORITY] == self._get_option(
CONF_PRIORITY
) and candidate.get(const.KEY_ACTIVE, False):
- return candidate # type: ignore[no-any-return]
+ # Explicit type specifier to ensure this works when the underlying
+ # (typed) library is installed along with the tests. Casts would trigger
+ # a redundant-cast warning in this case.
+ output: dict[str, Any] = candidate
+ return output
return None
@classmethod
- def _is_priority_entry_black(cls, priority: Optional[Dict[str, Any]]) -> bool:
+ def _is_priority_entry_black(cls, priority: dict[str, Any] | None) -> bool:
"""Determine if a given priority entry is the color black."""
if not priority:
return False
@@ -543,8 +564,7 @@ def _is_priority_entry_black(cls, priority: Optional[Dict[str, Any]]) -> bool:
return True
return False
- # pylint: disable=no-self-use
- def _allow_priority_update(self, priority: Optional[Dict[str, Any]] = None) -> bool:
+ def _allow_priority_update(self, priority: dict[str, Any] | None = None) -> bool:
"""Determine whether to allow a Hyperion priority to update entity attributes."""
# Black is treated as 'off' (and Home Assistant does not support selecting black
# from the color selector). Do not set our internal attributes if the priority is
diff --git a/homeassistant/components/hyperion/strings.json b/homeassistant/components/hyperion/strings.json
index ca7ed238f0bf42..54beb7704c9c4d 100644
--- a/homeassistant/components/hyperion/strings.json
+++ b/homeassistant/components/hyperion/strings.json
@@ -45,9 +45,10 @@
"step": {
"init": {
"data": {
- "priority": "Hyperion priority to use for colors and effects"
+ "priority": "Hyperion priority to use for colors and effects",
+ "effect_show_list": "Hyperion effects to show"
}
}
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py
index 372e9876c35418..4a4f8d4da135dd 100644
--- a/homeassistant/components/hyperion/switch.py
+++ b/homeassistant/components/hyperion/switch.py
@@ -1,6 +1,8 @@
"""Switch platform for Hyperion."""
+from __future__ import annotations
-from typing import Any, Callable, Dict, Optional
+import functools
+from typing import Any, Callable
from hyperion import client
from hyperion.const import (
@@ -156,7 +158,7 @@ def name(self) -> str:
@property
def is_on(self) -> bool:
"""Return true if the switch is on."""
- for component in self._client.components:
+ for component in self._client.components or []:
if component[KEY_NAME] == self._component_name:
return bool(component.setdefault(KEY_ENABLED, False))
return False
@@ -177,29 +179,26 @@ async def _async_send_set_component(self, value: bool) -> None:
}
)
- # pylint: disable=unused-argument
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the switch."""
await self._async_send_set_component(True)
- # pylint: disable=unused-argument
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the switch."""
await self._async_send_set_component(False)
@callback
- def _update_components(self, _: Optional[Dict[str, Any]] = None) -> None:
+ def _update_components(self, _: dict[str, Any] | None = None) -> None:
"""Update Hyperion components."""
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Register callbacks when entity added to hass."""
- assert self.hass
self.async_on_remove(
async_dispatcher_connect(
self.hass,
SIGNAL_ENTITY_REMOVE.format(self._unique_id),
- self.async_remove,
+ functools.partial(self.async_remove, force_remove=True),
)
)
diff --git a/homeassistant/components/hyperion/translations/ca.json b/homeassistant/components/hyperion/translations/ca.json
index 50ac384d8ca362..3a1de53102bb02 100644
--- a/homeassistant/components/hyperion/translations/ca.json
+++ b/homeassistant/components/hyperion/translations/ca.json
@@ -45,6 +45,7 @@
"step": {
"init": {
"data": {
+ "effect_show_list": "Efectes d'Hyperion a mostrar",
"priority": "Prioritat Hyperion a utilitzar per als colors i efectes"
}
}
diff --git a/homeassistant/components/hyperion/translations/el.json b/homeassistant/components/hyperion/translations/el.json
new file mode 100644
index 00000000000000..5ba51046f21294
--- /dev/null
+++ b/homeassistant/components/hyperion/translations/el.json
@@ -0,0 +1,11 @@
+{
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "effect_show_list": "\u0395\u03c6\u03ad Hyperion \u03b3\u03b9\u03b1 \u03b5\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hyperion/translations/en.json b/homeassistant/components/hyperion/translations/en.json
index d1277b411e0254..38ab5cf70550e9 100644
--- a/homeassistant/components/hyperion/translations/en.json
+++ b/homeassistant/components/hyperion/translations/en.json
@@ -45,6 +45,7 @@
"step": {
"init": {
"data": {
+ "effect_show_list": "Hyperion effects to show",
"priority": "Hyperion priority to use for colors and effects"
}
}
diff --git a/homeassistant/components/hyperion/translations/es-419.json b/homeassistant/components/hyperion/translations/es-419.json
new file mode 100644
index 00000000000000..ee8c8c108d9f91
--- /dev/null
+++ b/homeassistant/components/hyperion/translations/es-419.json
@@ -0,0 +1,11 @@
+{
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "effect_show_list": "Efectos de Hyperion a mostrar"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hyperion/translations/et.json b/homeassistant/components/hyperion/translations/et.json
index a225b7f2c4711c..dba661d24c2dc3 100644
--- a/homeassistant/components/hyperion/translations/et.json
+++ b/homeassistant/components/hyperion/translations/et.json
@@ -45,6 +45,7 @@
"step": {
"init": {
"data": {
+ "effect_show_list": "Kuvatavad Hyperioni efektid",
"priority": "V\u00e4rvide ja efektide puhul on kasutatavad Hyperioni eelistused"
}
}
diff --git a/homeassistant/components/hyperion/translations/fr.json b/homeassistant/components/hyperion/translations/fr.json
index 8c1cb919d11f12..57870c3b3efcf5 100644
--- a/homeassistant/components/hyperion/translations/fr.json
+++ b/homeassistant/components/hyperion/translations/fr.json
@@ -1,6 +1,38 @@
{
"config": {
+ "abort": {
+ "already_configured": "Le service est d\u00e9ja configur\u00e9 ",
+ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours",
+ "auth_new_token_not_granted_error": "Le jeton nouvellement cr\u00e9\u00e9 n'a pas \u00e9t\u00e9 approuv\u00e9 sur l'interface utilisateur Hyperion",
+ "auth_new_token_not_work_error": "\u00c9chec de l'authentification \u00e0 l'aide du jeton nouvellement cr\u00e9\u00e9",
+ "auth_required_error": "Impossible de d\u00e9terminer si une autorisation est requise",
+ "cannot_connect": "Echec de connection",
+ "no_id": "L'instance Hyperion Ambilight n'a pas signal\u00e9 son identifiant",
+ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi"
+ },
+ "error": {
+ "cannot_connect": "Echec de la connexion ",
+ "invalid_access_token": "jeton d'acc\u00e8s Invalide"
+ },
"step": {
+ "auth": {
+ "data": {
+ "create_token": "Cr\u00e9er automatiquement un nouveau jeton",
+ "token": "Ou fournir un jeton pr\u00e9existant"
+ },
+ "description": "Configurer l'autorisation sur votre serveur Hyperion Ambilight"
+ },
+ "confirm": {
+ "description": "Voulez-vous ajouter cet Hyperion Ambilight \u00e0 Home Assistant? \n\n ** H\u00f4te: ** {host}\n ** Port: ** {port}\n ** ID **: {id}",
+ "title": "Confirmer l'ajout du service Hyperion Ambilight"
+ },
+ "create_token": {
+ "description": "Choisissez ** Soumettre ** ci-dessous pour demander un nouveau jeton d'authentification. Vous serez redirig\u00e9 vers l'interface utilisateur Hyperion pour approuver la demande. Veuillez v\u00e9rifier que l'identifiant affich\u00e9 est \" {auth_id} \"",
+ "title": "Cr\u00e9er automatiquement un nouveau jeton d'authentification"
+ },
+ "create_token_external": {
+ "title": "Accepter un nouveau jeton dans l'interface utilisateur Hyperion"
+ },
"user": {
"data": {
"host": "H\u00f4te",
@@ -8,5 +40,15 @@
}
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "effect_show_list": "Effets Hyperion \u00e0 montrer",
+ "priority": "Priorit\u00e9 Hyperion \u00e0 utiliser pour les couleurs et les effets"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/hyperion/translations/hu.json b/homeassistant/components/hyperion/translations/hu.json
index 50ccd9f3b63099..5096423c143672 100644
--- a/homeassistant/components/hyperion/translations/hu.json
+++ b/homeassistant/components/hyperion/translations/hu.json
@@ -1,13 +1,35 @@
{
"config": {
"abort": {
- "reauth_successful": "Az \u00fajb\u00f3li azonos\u00edt\u00e1s sikeres"
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token"
},
"step": {
"auth": {
"data": {
"create_token": "\u00daj token automatikus l\u00e9trehoz\u00e1sa"
}
+ },
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "port": "Port"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "effect_show_list": "Megjelen\u00edtend\u0151 Hyperion effektusok"
+ }
}
}
}
diff --git a/homeassistant/components/hyperion/translations/id.json b/homeassistant/components/hyperion/translations/id.json
new file mode 100644
index 00000000000000..c1c2a62e0d9339
--- /dev/null
+++ b/homeassistant/components/hyperion/translations/id.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "auth_new_token_not_granted_error": "Token yang baru dibuat tidak disetujui di antarmuka Hyperion",
+ "auth_new_token_not_work_error": "Gagal mengautentikasi menggunakan token yang baru dibuat",
+ "auth_required_error": "Gagal menentukan apakah otorisasi diperlukan",
+ "cannot_connect": "Gagal terhubung",
+ "no_id": "Instans Hyperion Ambilight tidak melaporkan ID-nya",
+ "reauth_successful": "Autentikasi ulang berhasil"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_access_token": "Token akses tidak valid"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "create_token": "Buat token baru secara otomatis",
+ "token": "Atau berikan token yang sudah ada sebelumnya"
+ },
+ "description": "Konfigurasikan otorisasi ke server Hyperion Ambilight Anda"
+ },
+ "confirm": {
+ "description": "Apakah Anda ingin menambahkan Hyperion Ambilight ini ke Home Assistant?\n\n**Host:** {host}\n**Port:** {port}\n**ID**: {id}",
+ "title": "Konfirmasikan penambahan layanan Hyperion Ambilight"
+ },
+ "create_token": {
+ "description": "Pilih **Kirim** di bawah ini untuk meminta token autentikasi baru. Anda akan diarahkan ke antarmuka Hyperion untuk menyetujui permintaan. Pastikan ID yang ditampilkan adalah \"{auth_id}\"",
+ "title": "Buat token autentikasi baru secara otomatis"
+ },
+ "create_token_external": {
+ "title": "Terima token baru di antarmuka Hyperion"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "priority": "Prioritas hyperion digunakan untuk warna dan efek"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hyperion/translations/it.json b/homeassistant/components/hyperion/translations/it.json
index ff3170ffb98450..81f049125ed235 100644
--- a/homeassistant/components/hyperion/translations/it.json
+++ b/homeassistant/components/hyperion/translations/it.json
@@ -8,7 +8,7 @@
"auth_required_error": "Impossibile determinare se \u00e8 necessaria l'autorizzazione",
"cannot_connect": "Impossibile connettersi",
"no_id": "L'istanza Hyperion Ambilight non ha segnalato il suo ID",
- "reauth_successful": "Ri-autenticazione completata con successo"
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
},
"error": {
"cannot_connect": "Impossibile connettersi",
@@ -45,6 +45,7 @@
"step": {
"init": {
"data": {
+ "effect_show_list": "Effetti di Hyperion da mostrare",
"priority": "Priorit\u00e0 Hyperion da usare per colori ed effetti"
}
}
diff --git a/homeassistant/components/hyperion/translations/ko.json b/homeassistant/components/hyperion/translations/ko.json
new file mode 100644
index 00000000000000..f42450675fbb68
--- /dev/null
+++ b/homeassistant/components/hyperion/translations/ko.json
@@ -0,0 +1,54 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
+ "auth_new_token_not_granted_error": "\uc0c8\ub85c \uc0dd\uc131\ub41c \ud1a0\ud070\uc774 Hyperion UI\uc5d0\uc11c \uc2b9\uc778\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4",
+ "auth_new_token_not_work_error": "\uc0c8\ub85c \uc0dd\uc131\ub41c \ud1a0\ud070\uc744 \uc0ac\uc6a9\ud558\uc5ec \uc778\uc99d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "auth_required_error": "\uc778\uc99d\uc774 \ud544\uc694\ud55c\uc9c0 \ud655\uc778\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "no_id": "Hyperion Amblight \uc778\uc2a4\ud134\uc2a4\uac00 \ud574\ub2f9 ID\ub97c \ubcf4\uace0\ud558\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "create_token": "\uc0c8\ub85c\uc6b4 \ud1a0\ud070\uc744 \uc790\ub3d9\uc73c\ub85c \uc0dd\uc131\ud558\uae30",
+ "token": "\ub610\ub294 \uae30\uc874 \ud1a0\ud070 \uc81c\uacf5\ud558\uae30"
+ },
+ "description": "Hyperion Amblight \uc11c\ubc84\uc5d0 \ub300\ud55c \uad8c\ud55c \uad6c\uc131\ud558\uae30"
+ },
+ "confirm": {
+ "description": "\uc774 Hyperion Amblight\ub97c Home Assistant\uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?\n\n**\ud638\uc2a4\ud2b8**: {host}\n**\ud3ec\ud2b8**: {port}\n**ID**: {id}",
+ "title": "Hyperion Amblight \uc11c\ube44\uc2a4 \ucd94\uac00 \ud655\uc778\ud558\uae30"
+ },
+ "create_token": {
+ "description": "\uc544\ub798 **\ud655\uc778**\uc744 \uc120\ud0dd\ud558\uc5ec \uc0c8\ub85c\uc6b4 \uc778\uc99d \ud1a0\ud070\uc744 \uc694\uccad\ud574\uc8fc\uc138\uc694. \uc694\uccad\uc744 \uc2b9\uc778\ud558\ub3c4\ub85d Hyperion UI\ub85c \ub9ac\ub514\ub809\uc158\ub429\ub2c8\ub2e4. \ud45c\uc2dc\ub41c ID\uac00 \"{auth_id}\"\uc778\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694",
+ "title": "\uc0c8\ub85c\uc6b4 \uc778\uc99d \ud1a0\ud070\uc744 \uc790\ub3d9\uc73c\ub85c \uc0dd\uc131\ud558\uae30"
+ },
+ "create_token_external": {
+ "title": "Hyperion UI\uc5d0\uc11c \uc0c8\ub85c\uc6b4 \ud1a0\ud070 \uc218\ub77d\ud558\uae30"
+ },
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "port": "\ud3ec\ud2b8"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "effect_show_list": "\ucd9c\ub825\ud560 Hyperion \ud6a8\uacfc",
+ "priority": "\uc0c9\uc0c1 \ubc0f \ud6a8\uacfc\uc5d0 \uc0ac\uc6a9\ud560 Hyperion \uc6b0\uc120 \uc21c\uc704"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hyperion/translations/nl.json b/homeassistant/components/hyperion/translations/nl.json
index d93018f8a3cb5c..056971b435faeb 100644
--- a/homeassistant/components/hyperion/translations/nl.json
+++ b/homeassistant/components/hyperion/translations/nl.json
@@ -1,9 +1,52 @@
{
"config": {
+ "abort": {
+ "already_configured": "Service is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
+ "auth_new_token_not_granted_error": "Nieuw aangemaakte token is niet goedgekeurd in Hyperion UI",
+ "auth_new_token_not_work_error": "Verificatie met nieuw aangemaakt token mislukt",
+ "auth_required_error": "Kan niet bepalen of autorisatie vereist is",
+ "cannot_connect": "Kan geen verbinding maken",
+ "no_id": "De Hyperion Ambilight instantie heeft zijn id niet gerapporteerd",
+ "reauth_successful": "Herauthenticatie was succesvol"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_access_token": "Ongeldig toegangstoken"
+ },
"step": {
"auth": {
"data": {
- "create_token": "Maak automatisch een nieuw token aan"
+ "create_token": "Maak automatisch een nieuw token aan",
+ "token": "Of geef een reeds bestaand token op"
+ },
+ "description": "Configureer autorisatie voor uw Hyperion Ambilight-server"
+ },
+ "confirm": {
+ "description": "Wilt u deze Hyperion Ambilight toevoegen aan Home Assistant? \n\n ** Host: ** {host}\n ** Poort: ** {port}\n ** ID **: {id}",
+ "title": "Bevestig de toevoeging van Hyperion Ambilight-service"
+ },
+ "create_token": {
+ "description": "Kies **Submit** hieronder om een nieuw authenticatie token aan te vragen. U wordt doorgestuurd naar de Hyperion UI om de aanvraag goed te keuren. Controleer of de getoonde id \"{auth_id}\" is.",
+ "title": "Automatisch nieuw authenticatie token aanmaken"
+ },
+ "create_token_external": {
+ "title": "Accepteer nieuwe token in Hyperion UI"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Poort"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "effect_show_list": "Hyperion-effecten om te laten zien",
+ "priority": "Hyperion prioriteit te gebruiken voor kleuren en effecten"
}
}
}
diff --git a/homeassistant/components/hyperion/translations/no.json b/homeassistant/components/hyperion/translations/no.json
index e411982b58af6d..8fed4ee2437804 100644
--- a/homeassistant/components/hyperion/translations/no.json
+++ b/homeassistant/components/hyperion/translations/no.json
@@ -45,6 +45,7 @@
"step": {
"init": {
"data": {
+ "effect_show_list": "Hyperion-effekter \u00e5 vise",
"priority": "Hyperion-prioritet for bruke til farger og effekter"
}
}
diff --git a/homeassistant/components/hyperion/translations/pl.json b/homeassistant/components/hyperion/translations/pl.json
index 33b7c927520e9a..67e89e817f0213 100644
--- a/homeassistant/components/hyperion/translations/pl.json
+++ b/homeassistant/components/hyperion/translations/pl.json
@@ -45,6 +45,7 @@
"step": {
"init": {
"data": {
+ "effect_show_list": "Efekty Hyperiona do pokazania",
"priority": "Hyperion ma pierwsze\u0144stwo w u\u017cyciu dla kolor\u00f3w i efekt\u00f3w"
}
}
diff --git a/homeassistant/components/hyperion/translations/ru.json b/homeassistant/components/hyperion/translations/ru.json
index 9e74680a951505..a6716bb74ead4c 100644
--- a/homeassistant/components/hyperion/translations/ru.json
+++ b/homeassistant/components/hyperion/translations/ru.json
@@ -45,6 +45,7 @@
"step": {
"init": {
"data": {
+ "effect_show_list": "\u042d\u0444\u0444\u0435\u043a\u0442\u044b Hyperion",
"priority": "\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 Hyperion \u0434\u043b\u044f \u0446\u0432\u0435\u0442\u043e\u0432 \u0438 \u044d\u0444\u0444\u0435\u043a\u0442\u043e\u0432"
}
}
diff --git a/homeassistant/components/hyperion/translations/tr.json b/homeassistant/components/hyperion/translations/tr.json
index 6f46000e3e20b6..7b3f9f845a1329 100644
--- a/homeassistant/components/hyperion/translations/tr.json
+++ b/homeassistant/components/hyperion/translations/tr.json
@@ -2,11 +2,17 @@
"config": {
"abort": {
"already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor",
"auth_new_token_not_granted_error": "Hyperion UI'de yeni olu\u015fturulan belirte\u00e7 onaylanmad\u0131",
"auth_new_token_not_work_error": "Yeni olu\u015fturulan belirte\u00e7 kullan\u0131larak kimlik do\u011frulamas\u0131 ba\u015far\u0131s\u0131z oldu",
"auth_required_error": "Yetkilendirmenin gerekli olup olmad\u0131\u011f\u0131 belirlenemedi",
"cannot_connect": "Ba\u011flanma hatas\u0131",
- "no_id": "Hyperion Ambilight \u00f6rne\u011fi kimli\u011fini bildirmedi"
+ "no_id": "Hyperion Ambilight \u00f6rne\u011fi kimli\u011fini bildirmedi",
+ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci"
},
"step": {
"auth": {
@@ -15,6 +21,9 @@
"token": "Veya \u00f6nceden varolan belirte\u00e7 leri sa\u011flay\u0131n"
}
},
+ "confirm": {
+ "title": "Hyperion Ambilight hizmetinin eklenmesini onaylay\u0131n"
+ },
"create_token": {
"title": "Otomatik olarak yeni kimlik do\u011frulama belirteci olu\u015fturun"
},
@@ -23,6 +32,7 @@
},
"user": {
"data": {
+ "host": "Ana Bilgisayar",
"port": "Port"
}
}
diff --git a/homeassistant/components/hyperion/translations/uk.json b/homeassistant/components/hyperion/translations/uk.json
new file mode 100644
index 00000000000000..ae44b0610da56c
--- /dev/null
+++ b/homeassistant/components/hyperion/translations/uk.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "auth_new_token_not_granted_error": "\u0421\u0442\u0432\u043e\u0440\u0435\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u043d\u0435 \u0431\u0443\u0432 \u0441\u0445\u0432\u0430\u043b\u0435\u043d\u0438\u0439 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 Hyperion.",
+ "auth_new_token_not_work_error": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043f\u0440\u043e\u0439\u0442\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043e\u0433\u043e \u0442\u043e\u043a\u0435\u043d\u0430.",
+ "auth_required_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438, \u0447\u0438 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "no_id": "Hyperion Ambilight \u043d\u0435 \u043d\u0430\u0434\u0430\u0432 \u0441\u0432\u0456\u0439 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_access_token": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443."
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "create_token": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043d\u043e\u0432\u0438\u0439 \u0442\u043e\u043a\u0435\u043d",
+ "token": "\u0410\u0431\u043e \u043d\u0430\u0434\u0430\u0442\u0438 \u043d\u0430\u044f\u0432\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d"
+ },
+ "description": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0456 Hyperion Ambilight."
+ },
+ "confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 Hyperion Ambilight? \n\n ** \u0425\u043e\u0441\u0442: ** {host}\n ** \u041f\u043e\u0440\u0442: ** {port}\n ** ID **: {id}",
+ "title": "\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0456\u0442\u044c \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f \u0441\u043b\u0443\u0436\u0431\u0438 Hyperion Ambilight"
+ },
+ "create_token": {
+ "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438 ** \u043d\u0438\u0436\u0447\u0435, \u0449\u043e\u0431 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u043d\u043e\u0432\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457. \u0412\u0438 \u0431\u0443\u0434\u0435\u0442\u0435 \u043f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0456 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 Hyperion \u0434\u043b\u044f \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f \u0437\u0430\u043f\u0438\u0442\u0443. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u0439 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 - \" {auth_id} \"",
+ "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043d\u043e\u0432\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ },
+ "create_token_external": {
+ "title": "\u041f\u0440\u0438\u0439\u043d\u044f\u0442\u0438 \u043d\u043e\u0432\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 Hyperion"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "priority": "\u041f\u0440\u0456\u043e\u0440\u0438\u0442\u0435\u0442 Hyperion \u0434\u043b\u044f \u043a\u043e\u043b\u044c\u043e\u0440\u0456\u0432 \u0456 \u0435\u0444\u0435\u043a\u0442\u0456\u0432"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hyperion/translations/zh-Hant.json b/homeassistant/components/hyperion/translations/zh-Hant.json
index ed003131bf296c..d9757ccc22a09a 100644
--- a/homeassistant/components/hyperion/translations/zh-Hant.json
+++ b/homeassistant/components/hyperion/translations/zh-Hant.json
@@ -3,8 +3,8 @@
"abort": {
"already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
- "auth_new_token_not_granted_error": "\u65b0\u5275\u5bc6\u9470\u672a\u7372\u5f97 Hyperion UI \u6838\u51c6",
- "auth_new_token_not_work_error": "\u4f7f\u7528\u65b0\u5275\u5bc6\u9470\u8a8d\u8b49\u5931\u6557",
+ "auth_new_token_not_granted_error": "\u65b0\u5275\u6b0a\u6756\u672a\u7372\u5f97 Hyperion UI \u6838\u51c6",
+ "auth_new_token_not_work_error": "\u4f7f\u7528\u65b0\u5275\u6b0a\u6756\u8a8d\u8b49\u5931\u6557",
"auth_required_error": "\u7121\u6cd5\u5224\u5b9a\u662f\u5426\u9700\u8981\u9a57\u8b49",
"cannot_connect": "\u9023\u7dda\u5931\u6557",
"no_id": "Hyperion Ambilight \u5be6\u9ad4\u672a\u56de\u5831\u5176 ID",
@@ -12,13 +12,13 @@
},
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557",
- "invalid_access_token": "\u5b58\u53d6\u5bc6\u9470\u7121\u6548"
+ "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548"
},
"step": {
"auth": {
"data": {
- "create_token": "\u81ea\u52d5\u65b0\u5275\u5bc6\u9470",
- "token": "\u6216\u63d0\u4f9b\u73fe\u6709\u5bc6\u9470"
+ "create_token": "\u81ea\u52d5\u65b0\u5275\u6b0a\u6756",
+ "token": "\u6216\u63d0\u4f9b\u73fe\u6709\u6b0a\u6756"
},
"description": "\u8a2d\u5b9a Hyperion Ambilight \u4f3a\u670d\u5668\u8a8d\u8b49"
},
@@ -27,11 +27,11 @@
"title": "\u78ba\u8a8d\u9644\u52a0 Hyperion Ambilight \u670d\u52d9"
},
"create_token": {
- "description": "\u9ede\u9078\u4e0b\u65b9 **\u50b3\u9001** \u4ee5\u8acb\u6c42\u65b0\u8a8d\u8b49\u5bc6\u9470\u3002\u5c07\u6703\u91cd\u65b0\u5c0e\u5411\u81f3 Hyperion UI \u4ee5\u6838\u51c6\u8981\u6c42\u3002\u8acb\u78ba\u8a8d\u986f\u793a ID \u70ba \"{auth_id}\"",
- "title": "\u81ea\u52d5\u65b0\u5275\u8a8d\u8b49\u5bc6\u9470"
+ "description": "\u9ede\u9078\u4e0b\u65b9 **\u50b3\u9001** \u4ee5\u8acb\u6c42\u65b0\u8a8d\u8b49\u6b0a\u6756\u3002\u5c07\u6703\u91cd\u65b0\u5c0e\u5411\u81f3 Hyperion UI \u4ee5\u6838\u51c6\u8981\u6c42\u3002\u8acb\u78ba\u8a8d\u986f\u793a ID \u70ba \"{auth_id}\"",
+ "title": "\u81ea\u52d5\u65b0\u5275\u8a8d\u8b49\u6b0a\u6756"
},
"create_token_external": {
- "title": "\u63a5\u53d7 Hyperion UI \u4e2d\u7684\u65b0\u5bc6\u9470"
+ "title": "\u63a5\u53d7 Hyperion UI \u4e2d\u7684\u65b0\u6b0a\u6756"
},
"user": {
"data": {
@@ -45,6 +45,7 @@
"step": {
"init": {
"data": {
+ "effect_show_list": "\u986f\u793a Hyperion \u6548\u61c9",
"priority": "Hyperion \u512a\u5148\u4f7f\u7528\u4e4b\u8272\u6eab\u8207\u7279\u6548"
}
}
diff --git a/homeassistant/components/iammeter/sensor.py b/homeassistant/components/iammeter/sensor.py
index f885c2c49d3c87..b1882619fdadc0 100644
--- a/homeassistant/components/iammeter/sensor.py
+++ b/homeassistant/components/iammeter/sensor.py
@@ -8,7 +8,7 @@
from iammeter.power_meter import IamMeterError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import debounce
@@ -74,7 +74,7 @@ async def async_update_data():
async_add_entities(entities)
-class IamMeter(CoordinatorEntity):
+class IamMeter(CoordinatorEntity, SensorEntity):
"""Class for a sensor."""
def __init__(self, coordinator, uid, sensor_name, unit, dev_name):
diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py
index d0667aab72afbd..0435645d87cd15 100644
--- a/homeassistant/components/iaqualink/__init__.py
+++ b/homeassistant/components/iaqualink/__init__.py
@@ -1,8 +1,10 @@
"""Component to embed Aqualink devices."""
+from __future__ import annotations
+
import asyncio
from functools import wraps
import logging
-from typing import Any, Dict
+from typing import Any
import aiohttp.client_exceptions
from iaqualink import (
@@ -234,7 +236,7 @@ def available(self) -> bool:
return self.dev.system.online
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return the device info."""
return {
"identifiers": {(DOMAIN, self.unique_id)},
diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py
index 2c26b2bc363072..73988c4e523895 100644
--- a/homeassistant/components/iaqualink/climate.py
+++ b/homeassistant/components/iaqualink/climate.py
@@ -1,6 +1,7 @@
"""Support for Aqualink Thermostats."""
+from __future__ import annotations
+
import logging
-from typing import List, Optional
from iaqualink import AqualinkHeater, AqualinkPump, AqualinkSensor, AqualinkState
from iaqualink.const import (
@@ -53,7 +54,7 @@ def supported_features(self) -> int:
return SUPPORT_TARGET_TEMPERATURE
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of supported HVAC modes."""
return CLIMATE_SUPPORTED_MODES
@@ -119,7 +120,7 @@ def sensor(self) -> AqualinkSensor:
return self.dev.system.devices[sensor]
@property
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
if self.sensor.state != "":
return float(self.sensor.state)
diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py
index c083aee7c1c076..df8880a0e8f336 100644
--- a/homeassistant/components/iaqualink/config_flow.py
+++ b/homeassistant/components/iaqualink/config_flow.py
@@ -1,5 +1,5 @@
"""Config flow to configure zone component."""
-from typing import Optional
+from __future__ import annotations
from iaqualink import AqualinkClient, AqualinkLoginException
import voluptuous as vol
@@ -18,7 +18,7 @@ class AqualinkFlowHandler(config_entries.ConfigFlow):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
- async def async_step_user(self, user_input: Optional[ConfigType] = None):
+ async def async_step_user(self, user_input: ConfigType | None = None):
"""Handle a flow start."""
# Supporting a single account.
entries = self.hass.config_entries.async_entries(DOMAIN)
@@ -46,6 +46,6 @@ async def async_step_user(self, user_input: Optional[ConfigType] = None):
errors=errors,
)
- async def async_step_import(self, user_input: Optional[ConfigType] = None):
+ async def async_step_import(self, user_input: ConfigType | None = None):
"""Occurs when an entry is setup through config."""
return await self.async_step_user(user_input)
diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py
index 50f2a83221199f..eac6e2b785190e 100644
--- a/homeassistant/components/iaqualink/sensor.py
+++ b/homeassistant/components/iaqualink/sensor.py
@@ -1,7 +1,7 @@
"""Support for Aqualink temperature sensors."""
-from typing import Optional
+from __future__ import annotations
-from homeassistant.components.sensor import DOMAIN
+from homeassistant.components.sensor import DOMAIN, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.helpers.typing import HomeAssistantType
@@ -22,7 +22,7 @@ async def async_setup_entry(
async_add_entities(devs, True)
-class HassAqualinkSensor(AqualinkEntity):
+class HassAqualinkSensor(AqualinkEntity, SensorEntity):
"""Representation of a sensor."""
@property
@@ -31,7 +31,7 @@ def name(self) -> str:
return self.dev.label
@property
- def unit_of_measurement(self) -> Optional[str]:
+ def unit_of_measurement(self) -> str | None:
"""Return the measurement unit for the sensor."""
if self.dev.name.endswith("_temp"):
if self.dev.system.temp_unit == "F":
@@ -40,7 +40,7 @@ def unit_of_measurement(self) -> Optional[str]:
return None
@property
- def state(self) -> Optional[str]:
+ def state(self) -> str | None:
"""Return the state of the sensor."""
if self.dev.state == "":
return None
@@ -52,7 +52,7 @@ def state(self) -> Optional[str]:
return state
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the class of the sensor."""
if self.dev.name.endswith("_temp"):
return DEVICE_CLASS_TEMPERATURE
diff --git a/homeassistant/components/iaqualink/translations/de.json b/homeassistant/components/iaqualink/translations/de.json
index e7e1002015c36a..0a678baf7ca765 100644
--- a/homeassistant/components/iaqualink/translations/de.json
+++ b/homeassistant/components/iaqualink/translations/de.json
@@ -1,5 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/iaqualink/translations/he.json b/homeassistant/components/iaqualink/translations/he.json
new file mode 100644
index 00000000000000..6f4191da70d538
--- /dev/null
+++ b/homeassistant/components/iaqualink/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/translations/hu.json b/homeassistant/components/iaqualink/translations/hu.json
index 149fee90583d53..dcb7b906ee3beb 100644
--- a/homeassistant/components/iaqualink/translations/hu.json
+++ b/homeassistant/components/iaqualink/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
diff --git a/homeassistant/components/iaqualink/translations/id.json b/homeassistant/components/iaqualink/translations/id.json
new file mode 100644
index 00000000000000..4591cf11e055e2
--- /dev/null
+++ b/homeassistant/components/iaqualink/translations/id.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "description": "Masukkan nama pengguna dan kata sandi untuk akun iAqualink Anda.",
+ "title": "Hubungkan ke iAqualink"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/translations/ko.json b/homeassistant/components/iaqualink/translations/ko.json
index 6396d5d250e954..ef77daf978b272 100644
--- a/homeassistant/components/iaqualink/translations/ko.json
+++ b/homeassistant/components/iaqualink/translations/ko.json
@@ -1,5 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
"step": {
"user": {
"data": {
@@ -7,7 +13,7 @@
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
},
"description": "iAqualink \uacc4\uc815\uc758 \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.",
- "title": "iAqualink \uc5d0 \uc5f0\uacb0\ud558\uae30"
+ "title": "iAqualink\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/iaqualink/translations/nl.json b/homeassistant/components/iaqualink/translations/nl.json
index fae8693ce4cc06..fc5b00694a1df0 100644
--- a/homeassistant/components/iaqualink/translations/nl.json
+++ b/homeassistant/components/iaqualink/translations/nl.json
@@ -10,7 +10,7 @@
"user": {
"data": {
"password": "Wachtwoord",
- "username": "Gebruikersnaam/E-mailadres"
+ "username": "Gebruikersnaam"
},
"description": "Voer de gebruikersnaam en het wachtwoord voor uw iAqualink-account in.",
"title": "Verbinding maken met iAqualink"
diff --git a/homeassistant/components/iaqualink/translations/ru.json b/homeassistant/components/iaqualink/translations/ru.json
index 27531c65d9e19f..b7c9779e11ae50 100644
--- a/homeassistant/components/iaqualink/translations/ru.json
+++ b/homeassistant/components/iaqualink/translations/ru.json
@@ -10,9 +10,9 @@
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
- "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 iAqualink.",
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 iAqualink.",
"title": "Jandy iAqualink"
}
}
diff --git a/homeassistant/components/iaqualink/translations/tr.json b/homeassistant/components/iaqualink/translations/tr.json
new file mode 100644
index 00000000000000..c2c70f3e45b398
--- /dev/null
+++ b/homeassistant/components/iaqualink/translations/tr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ },
+ "description": "L\u00fctfen iAqualink hesab\u0131n\u0131z i\u00e7in kullan\u0131c\u0131 ad\u0131 ve parolay\u0131 girin."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/translations/uk.json b/homeassistant/components/iaqualink/translations/uk.json
new file mode 100644
index 00000000000000..b855d75572603c
--- /dev/null
+++ b/homeassistant/components/iaqualink/translations/uk.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043b\u043e\u0433\u0456\u043d \u0456 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 iAqualink.",
+ "title": "Jandy iAqualink"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py
index e6337085e04e2b..5c3bd2bf51968a 100644
--- a/homeassistant/components/icloud/account.py
+++ b/homeassistant/components/icloud/account.py
@@ -1,8 +1,9 @@
"""iCloud account."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
import operator
-from typing import Dict, Optional
from pyicloud import PyiCloudService
from pyicloud.exceptions import (
@@ -95,7 +96,7 @@ def __init__(
self._icloud_dir = icloud_dir
- self.api: Optional[PyiCloudService] = None
+ self.api: PyiCloudService | None = None
self._owner_fullname = None
self._family_members_fullname = {}
self._devices = {}
@@ -113,28 +114,24 @@ def setup(self) -> None:
self._icloud_dir.path,
with_family=self._with_family,
)
+
+ if self.api.requires_2fa:
+ # Trigger a new log in to ensure the user enters the 2FA code again.
+ raise PyiCloudFailedLoginException
+
except PyiCloudFailedLoginException:
self.api = None
# Login failed which means credentials need to be updated.
_LOGGER.error(
(
- "Your password for '%s' is no longer working. Go to the "
+ "Your password for '%s' is no longer working; Go to the "
"Integrations menu and click on Configure on the discovered Apple "
- "iCloud card to login again."
+ "iCloud card to login again"
),
self._config_entry.data[CONF_USERNAME],
)
- self.hass.add_job(
- self.hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": SOURCE_REAUTH},
- data={
- **self._config_entry.data,
- "unique_id": self._config_entry.unique_id,
- },
- )
- )
+ self._require_reauth()
return
try:
@@ -165,6 +162,10 @@ def update_devices(self) -> None:
if self.api is None:
return
+ if self.api.requires_2fa:
+ self._require_reauth()
+ return
+
api_devices = {}
try:
api_devices = self.api.devices
@@ -228,6 +229,19 @@ def update_devices(self) -> None:
utcnow() + timedelta(minutes=self._fetch_interval),
)
+ def _require_reauth(self):
+ """Require the user to log in again."""
+ self.hass.add_job(
+ self.hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_REAUTH},
+ data={
+ **self._config_entry.data,
+ "unique_id": self._config_entry.unique_id,
+ },
+ )
+ )
+
def _determine_interval(self) -> int:
"""Calculate new interval between two API fetch (in minutes)."""
intervals = {"default": self._max_interval}
@@ -331,7 +345,7 @@ def owner_fullname(self) -> str:
return self._owner_fullname
@property
- def family_members_fullname(self) -> Dict[str, str]:
+ def family_members_fullname(self) -> dict[str, str]:
"""Return the account family members fullname."""
return self._family_members_fullname
@@ -341,7 +355,7 @@ def fetch_interval(self) -> int:
return self._fetch_interval
@property
- def devices(self) -> Dict[str, any]:
+ def devices(self) -> dict[str, any]:
"""Return the account devices."""
return self._devices
@@ -482,11 +496,11 @@ def battery_status(self) -> str:
return self._battery_status
@property
- def location(self) -> Dict[str, any]:
+ def location(self) -> dict[str, any]:
"""Return the Apple device location."""
return self._location
@property
- def state_attributes(self) -> Dict[str, any]:
+ def extra_state_attributes(self) -> dict[str, any]:
"""Return the attributes."""
return self._attrs
diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py
index d447790e43229f..28570f3d93c6bb 100644
--- a/homeassistant/components/icloud/config_flow.py
+++ b/homeassistant/components/icloud/config_flow.py
@@ -21,10 +21,10 @@
DEFAULT_GPS_ACCURACY_THRESHOLD,
DEFAULT_MAX_INTERVAL,
DEFAULT_WITH_FAMILY,
+ DOMAIN,
STORAGE_KEY,
STORAGE_VERSION,
)
-from .const import DOMAIN # pylint: disable=unused-import
CONF_TRUSTED_DEVICE = "trusted_device"
CONF_VERIFICATION_CODE = "verification_code"
@@ -125,6 +125,9 @@ async def _validate_and_create_entry(self, user_input, step_id):
errors = {CONF_PASSWORD: "invalid_auth"}
return self._show_setup_form(user_input, errors, step_id)
+ if self.api.requires_2fa:
+ return await self.async_step_verification_code()
+
if self.api.requires_2sa:
return await self.async_step_trusted_device()
@@ -243,22 +246,29 @@ async def _show_trusted_device_form(
errors=errors or {},
)
- async def async_step_verification_code(self, user_input=None):
+ async def async_step_verification_code(self, user_input=None, errors=None):
"""Ask the verification code to the user."""
- errors = {}
+ if errors is None:
+ errors = {}
if user_input is None:
- return await self._show_verification_code_form(user_input)
+ return await self._show_verification_code_form(user_input, errors)
self._verification_code = user_input[CONF_VERIFICATION_CODE]
try:
- if not await self.hass.async_add_executor_job(
- self.api.validate_verification_code,
- self._trusted_device,
- self._verification_code,
- ):
- raise PyiCloudException("The code you entered is not valid.")
+ if self.api.requires_2fa:
+ if not await self.hass.async_add_executor_job(
+ self.api.validate_2fa_code, self._verification_code
+ ):
+ raise PyiCloudException("The code you entered is not valid.")
+ else:
+ if not await self.hass.async_add_executor_job(
+ self.api.validate_verification_code,
+ self._trusted_device,
+ self._verification_code,
+ ):
+ raise PyiCloudException("The code you entered is not valid.")
except PyiCloudException as error:
# Reset to the initial 2FA state to allow the user to retry
_LOGGER.error("Failed to verify verification code: %s", error)
@@ -266,7 +276,27 @@ async def async_step_verification_code(self, user_input=None):
self._verification_code = None
errors["base"] = "validate_verification_code"
- return await self.async_step_trusted_device(None, errors)
+ if self.api.requires_2fa:
+ try:
+ self.api = await self.hass.async_add_executor_job(
+ PyiCloudService,
+ self._username,
+ self._password,
+ self.hass.helpers.storage.Store(
+ STORAGE_VERSION, STORAGE_KEY
+ ).path,
+ True,
+ None,
+ self._with_family,
+ )
+ return await self.async_step_verification_code(None, errors)
+ except PyiCloudFailedLoginException as error:
+ _LOGGER.error("Error logging into iCloud service: %s", error)
+ self.api = None
+ errors = {CONF_PASSWORD: "invalid_auth"}
+ return self._show_setup_form(user_input, errors, "user")
+ else:
+ return await self.async_step_trusted_device(None, errors)
return await self.async_step_user(
{
@@ -278,11 +308,11 @@ async def async_step_verification_code(self, user_input=None):
}
)
- async def _show_verification_code_form(self, user_input=None):
+ async def _show_verification_code_form(self, user_input=None, errors=None):
"""Show the verification_code form to the user."""
return self.async_show_form(
step_id=CONF_VERIFICATION_CODE,
data_schema=vol.Schema({vol.Required(CONF_VERIFICATION_CODE): str}),
- errors=None,
+ errors=errors or {},
)
diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py
index d62bacf12120a5..58c62f8a868702 100644
--- a/homeassistant/components/icloud/const.py
+++ b/homeassistant/components/icloud/const.py
@@ -12,7 +12,7 @@
# to store the cookie
STORAGE_KEY = DOMAIN
-STORAGE_VERSION = 1
+STORAGE_VERSION = 2
PLATFORMS = ["device_tracker", "sensor"]
diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py
index 59554c001ef237..502c2b00f8bb04 100644
--- a/homeassistant/components/icloud/device_tracker.py
+++ b/homeassistant/components/icloud/device_tracker.py
@@ -1,5 +1,5 @@
"""Support for tracking for iCloud devices."""
-from typing import Dict
+from __future__ import annotations
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
from homeassistant.components.device_tracker.config_entry import TrackerEntity
@@ -108,12 +108,12 @@ def icon(self) -> str:
return icon_for_icloud_device(self._device)
@property
- def device_state_attributes(self) -> Dict[str, any]:
+ def extra_state_attributes(self) -> dict[str, any]:
"""Return the device state attributes."""
- return self._device.state_attributes
+ return self._device.extra_state_attributes
@property
- def device_info(self) -> Dict[str, any]:
+ def device_info(self) -> dict[str, any]:
"""Return the device information."""
return {
"identifiers": {(DOMAIN, self._device.unique_id)},
diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json
index 40b58cbf2d0ae0..4d96f42b8cbfa2 100644
--- a/homeassistant/components/icloud/manifest.json
+++ b/homeassistant/components/icloud/manifest.json
@@ -3,6 +3,6 @@
"name": "Apple iCloud",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/icloud",
- "requirements": ["pyicloud==0.9.7"],
- "codeowners": ["@Quentame"]
+ "requirements": ["pyicloud==0.10.2"],
+ "codeowners": ["@Quentame", "@nzapponi"]
}
diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py
index 859148d8190f2c..f889495af25e55 100644
--- a/homeassistant/components/icloud/sensor.py
+++ b/homeassistant/components/icloud/sensor.py
@@ -1,11 +1,11 @@
"""Support for iCloud sensors."""
-from typing import Dict
+from __future__ import annotations
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.helpers.typing import HomeAssistantType
@@ -48,7 +48,7 @@ def add_entities(account, async_add_entities, tracked):
async_add_entities(new_tracked, True)
-class IcloudDeviceBatterySensor(Entity):
+class IcloudDeviceBatterySensor(SensorEntity):
"""Representation of a iCloud device battery sensor."""
def __init__(self, account: IcloudAccount, device: IcloudDevice):
@@ -91,12 +91,12 @@ def icon(self) -> str:
)
@property
- def device_state_attributes(self) -> Dict[str, any]:
+ def extra_state_attributes(self) -> dict[str, any]:
"""Return default attributes for the iCloud device entity."""
- return self._device.state_attributes
+ return self._device.extra_state_attributes
@property
- def device_info(self) -> Dict[str, any]:
+ def device_info(self) -> dict[str, any]:
"""Return the device information."""
return {
"identifiers": {(DOMAIN, self._device.unique_id)},
diff --git a/homeassistant/components/icloud/strings.json b/homeassistant/components/icloud/strings.json
index d07b3c3b870005..70ab11157d37ce 100644
--- a/homeassistant/components/icloud/strings.json
+++ b/homeassistant/components/icloud/strings.json
@@ -35,7 +35,7 @@
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"send_verification_code": "Failed to send verification code",
- "validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again"
+ "validate_verification_code": "Failed to verify your verification code, try again"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
diff --git a/homeassistant/components/icloud/translations/ca.json b/homeassistant/components/icloud/translations/ca.json
index a0d74aba98caa5..6e92897161aff5 100644
--- a/homeassistant/components/icloud/translations/ca.json
+++ b/homeassistant/components/icloud/translations/ca.json
@@ -8,7 +8,7 @@
"error": {
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"send_verification_code": "No s'ha pogut enviar el codi de verificaci\u00f3",
- "validate_verification_code": "No s'ha pogut verificar el codi de verificaci\u00f3, tria un dispositiu de confian\u00e7a i torna a iniciar el proc\u00e9s"
+ "validate_verification_code": "No s'ha pogut verificar el codi de verificaci\u00f3, torna-ho a provar"
},
"step": {
"reauth": {
diff --git a/homeassistant/components/icloud/translations/de.json b/homeassistant/components/icloud/translations/de.json
index e7441792d91231..7baf9dc3917bf5 100644
--- a/homeassistant/components/icloud/translations/de.json
+++ b/homeassistant/components/icloud/translations/de.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Konto bereits konfiguriert",
- "no_device": "Auf keinem Ihrer Ger\u00e4te ist \"Find my iPhone\" aktiviert"
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "no_device": "Auf keinem Ihrer Ger\u00e4te ist \"Find my iPhone\" aktiviert",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
},
"error": {
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"send_verification_code": "Fehler beim Senden des Best\u00e4tigungscodes",
"validate_verification_code": "Verifizierung des Verifizierungscodes fehlgeschlagen. W\u00e4hle ein vertrauensw\u00fcrdiges Ger\u00e4t aus und starte die Verifizierung erneut"
},
@@ -12,7 +14,9 @@
"reauth": {
"data": {
"password": "Passwort"
- }
+ },
+ "description": "Ihr zuvor eingegebenes Passwort f\u00fcr {username} funktioniert nicht mehr. Aktualisieren Sie Ihr Passwort, um diese Integration weiterhin zu verwenden.",
+ "title": "Integration erneut authentifizieren"
},
"trusted_device": {
"data": {
diff --git a/homeassistant/components/icloud/translations/en.json b/homeassistant/components/icloud/translations/en.json
index 3097302ded2954..36e657011e3242 100644
--- a/homeassistant/components/icloud/translations/en.json
+++ b/homeassistant/components/icloud/translations/en.json
@@ -8,7 +8,7 @@
"error": {
"invalid_auth": "Invalid authentication",
"send_verification_code": "Failed to send verification code",
- "validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again"
+ "validate_verification_code": "Failed to verify your verification code, try again"
},
"step": {
"reauth": {
diff --git a/homeassistant/components/icloud/translations/et.json b/homeassistant/components/icloud/translations/et.json
index 29b24aabf5a4b2..af3457bb0dbd81 100644
--- a/homeassistant/components/icloud/translations/et.json
+++ b/homeassistant/components/icloud/translations/et.json
@@ -15,7 +15,7 @@
"data": {
"password": "Salas\u00f5na"
},
- "description": "Varem sisestatud salas\u00f5na kasutajale {username} ei t\u00f6\u00f6ta enam. Selle sidumise kasutamise j\u00e4tkamiseks v\u00e4rskendage oma salas\u00f5na.",
+ "description": "Varem sisestatud salas\u00f5na kasutajale {username} ei t\u00f6\u00f6ta enam. Selle sidumise kasutamise j\u00e4tkamiseks v\u00e4rskenda oma salas\u00f5na.",
"title": "iCloudi tuvastusandmed"
},
"trusted_device": {
diff --git a/homeassistant/components/icloud/translations/he.json b/homeassistant/components/icloud/translations/he.json
index 71466dddc39b42..139d7a1e39907f 100644
--- a/homeassistant/components/icloud/translations/he.json
+++ b/homeassistant/components/icloud/translations/he.json
@@ -3,6 +3,7 @@
"step": {
"user": {
"data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
"username": "\u05d3\u05d5\u05d0\u05e8 \u05d0\u05dc\u05e7\u05d8\u05e8\u05d5\u05e0\u05d9"
}
}
diff --git a/homeassistant/components/icloud/translations/hu.json b/homeassistant/components/icloud/translations/hu.json
index 2e820418e94d51..bb47cdd879b73c 100644
--- a/homeassistant/components/icloud/translations/hu.json
+++ b/homeassistant/components/icloud/translations/hu.json
@@ -1,6 +1,11 @@
{
"config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
+ },
"error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"send_verification_code": "Nem siker\u00fclt elk\u00fcldeni az ellen\u0151rz\u0151 k\u00f3dot",
"validate_verification_code": "Nem siker\u00fclt ellen\u0151rizni az ellen\u0151rz\u0151 k\u00f3dot, ki kell v\u00e1lasztania egy megb\u00edzhat\u00f3s\u00e1gi eszk\u00f6zt, \u00e9s \u00fajra kell ind\u00edtania az ellen\u0151rz\u00e9st"
},
@@ -8,7 +13,8 @@
"reauth": {
"data": {
"password": "Jelsz\u00f3"
- }
+ },
+ "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se"
},
"trusted_device": {
"data": {
diff --git a/homeassistant/components/icloud/translations/id.json b/homeassistant/components/icloud/translations/id.json
new file mode 100644
index 00000000000000..cd7abc1945dbb6
--- /dev/null
+++ b/homeassistant/components/icloud/translations/id.json
@@ -0,0 +1,46 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi",
+ "no_device": "Tidak ada perangkat Anda yang mengaktifkan \"Temukan iPhone saya\"",
+ "reauth_successful": "Autentikasi ulang berhasil"
+ },
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid",
+ "send_verification_code": "Gagal mengirim kode verifikasi",
+ "validate_verification_code": "Gagal memverifikasi kode verifikasi Anda, coba lagi"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Kata Sandi"
+ },
+ "description": "Kata sandi yang Anda masukkan sebelumnya untuk {username} tidak lagi berfungsi. Perbarui kata sandi Anda untuk tetap menggunakan integrasi ini.",
+ "title": "Autentikasi Ulang Integrasi"
+ },
+ "trusted_device": {
+ "data": {
+ "trusted_device": "Perangkat tepercaya"
+ },
+ "description": "Pilih perangkat tepercaya Anda",
+ "title": "Perangkat tepercaya iCloud"
+ },
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Email",
+ "with_family": "Dengan keluarga"
+ },
+ "description": "Masukkan kredensial Anda",
+ "title": "Kredensial iCloud"
+ },
+ "verification_code": {
+ "data": {
+ "verification_code": "Kode verifikasi"
+ },
+ "description": "Masukkan kode verifikasi yang baru saja diterima dari iCloud",
+ "title": "Kode verifikasi iCloud"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/icloud/translations/it.json b/homeassistant/components/icloud/translations/it.json
index 4fde8b33526447..cfb18caee1e477 100644
--- a/homeassistant/components/icloud/translations/it.json
+++ b/homeassistant/components/icloud/translations/it.json
@@ -3,12 +3,12 @@
"abort": {
"already_configured": "L'account \u00e8 gi\u00e0 configurato",
"no_device": "Nessuno dei tuoi dispositivi ha attivato \"Trova il mio iPhone\"",
- "reauth_successful": "La riautenticazione ha avuto successo"
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
},
"error": {
"invalid_auth": "Autenticazione non valida",
"send_verification_code": "Impossibile inviare il codice di verifica",
- "validate_verification_code": "Impossibile verificare il codice di verifica, scegliere un dispositivo attendibile e riavviare la verifica"
+ "validate_verification_code": "Impossibile verificare il codice di verifica, riprovare"
},
"step": {
"reauth": {
@@ -16,7 +16,7 @@
"password": "Password"
},
"description": "La password inserita in precedenza per {username} non funziona pi\u00f9. Aggiorna la tua password per continuare a utilizzare questa integrazione.",
- "title": "Reautenticare l'integrazione"
+ "title": "Autenticare nuovamente l'integrazione"
},
"trusted_device": {
"data": {
diff --git a/homeassistant/components/icloud/translations/ko.json b/homeassistant/components/icloud/translations/ko.json
index 045042362b7727..52319b888ca9ca 100644
--- a/homeassistant/components/icloud/translations/ko.json
+++ b/homeassistant/components/icloud/translations/ko.json
@@ -1,14 +1,23 @@
{
"config": {
"abort": {
- "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "no_device": "\"\ub098\uc758 iPhone \ucc3e\uae30\"\uac00 \ud65c\uc131\ud654\ub41c \uae30\uae30\uac00 \uc5c6\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "no_device": "\"\ub098\uc758 iPhone \ucc3e\uae30\"\uac00 \ud65c\uc131\ud654\ub41c \uae30\uae30\uac00 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"send_verification_code": "\uc778\uc99d \ucf54\ub4dc\ub97c \ubcf4\ub0b4\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
- "validate_verification_code": "\uc778\uc99d \ucf54\ub4dc\ub97c \ud655\uc778\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uc2e0\ub8b0\ud560 \uc218 \uc788\ub294 \uae30\uae30\ub97c \uc120\ud0dd\ud558\uace0 \uc778\uc99d\uc744 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694"
+ "validate_verification_code": "\uc778\uc99d \ucf54\ub4dc \ud655\uc778\uc5d0 \uc2e4\ud328\ud558\uc600\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694"
},
"step": {
+ "reauth": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638"
+ },
+ "description": "\uc774\uc804\uc5d0 \uc785\ub825\ud55c {username}\uc5d0 \ub300\ud55c \ube44\ubc00\ubc88\ud638\uac00 \ub354 \uc774\uc0c1 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uacc4\uc18d \uc0ac\uc6a9\ud558\ub824\uba74 \ube44\ubc00\ubc88\ud638\ub97c \uc5c5\ub370\uc774\ud2b8\ud574\uc8fc\uc138\uc694.",
+ "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30"
+ },
"trusted_device": {
"data": {
"trusted_device": "\uc2e0\ub8b0\ud560 \uc218 \uc788\ub294 \uae30\uae30"
diff --git a/homeassistant/components/icloud/translations/nl.json b/homeassistant/components/icloud/translations/nl.json
index 537d310b0a7861..7260f954c3834f 100644
--- a/homeassistant/components/icloud/translations/nl.json
+++ b/homeassistant/components/icloud/translations/nl.json
@@ -1,10 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "Account reeds geconfigureerd",
- "no_device": "Op geen van uw apparaten is \"Find my iPhone\" geactiveerd"
+ "already_configured": "Account is al geconfigureerd",
+ "no_device": "Op geen van uw apparaten is \"Find my iPhone\" geactiveerd",
+ "reauth_successful": "Herauthenticatie was succesvol"
},
"error": {
+ "invalid_auth": "Ongeldige authenticatie",
"send_verification_code": "Kan verificatiecode niet verzenden",
"validate_verification_code": "Kan uw verificatiecode niet verifi\u00ebren, kies een vertrouwensapparaat en start de verificatie opnieuw"
},
@@ -13,7 +15,8 @@
"data": {
"password": "Wachtwoord"
},
- "description": "Uw eerder ingevoerde wachtwoord voor {username} werkt niet meer. Update uw wachtwoord om deze integratie te blijven gebruiken."
+ "description": "Uw eerder ingevoerde wachtwoord voor {username} werkt niet meer. Update uw wachtwoord om deze integratie te blijven gebruiken.",
+ "title": "Verifieer de integratie opnieuw"
},
"trusted_device": {
"data": {
@@ -25,6 +28,7 @@
"user": {
"data": {
"password": "Wachtwoord",
+ "username": "E-mail",
"with_family": "Met gezin"
},
"description": "Voer uw gegevens in",
diff --git a/homeassistant/components/icloud/translations/no.json b/homeassistant/components/icloud/translations/no.json
index 62e123eb84c8e3..3e20aef032eb92 100644
--- a/homeassistant/components/icloud/translations/no.json
+++ b/homeassistant/components/icloud/translations/no.json
@@ -8,7 +8,7 @@
"error": {
"invalid_auth": "Ugyldig godkjenning",
"send_verification_code": "Kunne ikke sende bekreftelseskode",
- "validate_verification_code": "Kunne ikke bekrefte bekreftelseskoden din, velg en tillitsenhet og start bekreftelsen p\u00e5 nytt"
+ "validate_verification_code": "Kunne ikke bekrefte bekreftelseskoden, pr\u00f8v p\u00e5 nytt"
},
"step": {
"reauth": {
diff --git a/homeassistant/components/icloud/translations/pl.json b/homeassistant/components/icloud/translations/pl.json
index 4ac02d1f3f0dcc..e111518710b35e 100644
--- a/homeassistant/components/icloud/translations/pl.json
+++ b/homeassistant/components/icloud/translations/pl.json
@@ -8,7 +8,7 @@
"error": {
"invalid_auth": "Niepoprawne uwierzytelnienie",
"send_verification_code": "Nie uda\u0142o si\u0119 wys\u0142a\u0107 kodu weryfikacyjnego",
- "validate_verification_code": "Nie uda\u0142o si\u0119 zweryfikowa\u0107 kodu weryfikacyjnego, wybierz urz\u0105dzenie zaufane i ponownie rozpocznij weryfikacj\u0119"
+ "validate_verification_code": "Nie uda\u0142o si\u0119 zweryfikowa\u0107 kodu weryfikacyjnego, spr\u00f3buj ponownie"
},
"step": {
"reauth": {
diff --git a/homeassistant/components/icloud/translations/ru.json b/homeassistant/components/icloud/translations/ru.json
index d977899d9021ca..f3f856302157ea 100644
--- a/homeassistant/components/icloud/translations/ru.json
+++ b/homeassistant/components/icloud/translations/ru.json
@@ -6,9 +6,9 @@
"reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
},
"error": {
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"send_verification_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f.",
- "validate_verification_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438 \u043d\u0430\u0447\u043d\u0438\u0442\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u043d\u043e\u0432\u0430."
+ "validate_verification_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437."
},
"step": {
"reauth": {
@@ -16,7 +16,7 @@
"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 \u043f\u0440\u043e\u0444\u0438\u043b\u044f"
+ "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": {
diff --git a/homeassistant/components/icloud/translations/sv.json b/homeassistant/components/icloud/translations/sv.json
index 6caf02f56c56aa..2bba72d49dfd3a 100644
--- a/homeassistant/components/icloud/translations/sv.json
+++ b/homeassistant/components/icloud/translations/sv.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Kontot har redan konfigurerats"
+ "already_configured": "Kontot har redan konfigurerats",
+ "no_device": "Ingen av dina enheter har \"Hitta min iPhone\" aktiverat"
},
"error": {
"send_verification_code": "Det gick inte att skicka verifieringskod",
diff --git a/homeassistant/components/icloud/translations/tr.json b/homeassistant/components/icloud/translations/tr.json
index 3d74852ce50ecb..86581625d96e27 100644
--- a/homeassistant/components/icloud/translations/tr.json
+++ b/homeassistant/components/icloud/translations/tr.json
@@ -1,7 +1,28 @@
{
"config": {
"abort": {
- "no_device": "Hi\u00e7bir cihaz\u0131n\u0131zda \"iPhone'umu bul\" etkin de\u011fil"
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "no_device": "Hi\u00e7bir cihaz\u0131n\u0131zda \"iPhone'umu bul\" etkin de\u011fil",
+ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu"
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "validate_verification_code": "Do\u011frulama kodunuzu do\u011frulamay\u0131 ba\u015faramad\u0131n\u0131z, bir g\u00fcven ayg\u0131t\u0131 se\u00e7in ve do\u011frulamay\u0131 yeniden ba\u015flat\u0131n"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Parola"
+ },
+ "description": "{username} i\u00e7in \u00f6nceden girdi\u011finiz \u015fifreniz art\u0131k \u00e7al\u0131\u015fm\u0131yor. Bu entegrasyonu kullanmaya devam etmek i\u00e7in \u015fifrenizi g\u00fcncelleyin."
+ },
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "E-posta",
+ "with_family": "Aileyle"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/icloud/translations/uk.json b/homeassistant/components/icloud/translations/uk.json
new file mode 100644
index 00000000000000..ac65157f050bd1
--- /dev/null
+++ b/homeassistant/components/icloud/translations/uk.json
@@ -0,0 +1,46 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "no_device": "\u041d\u0430 \u0436\u043e\u0434\u043d\u043e\u043c\u0443 \u0437 \u0412\u0430\u0448\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u043d\u0435 \u0430\u043a\u0442\u0438\u0432\u043e\u0432\u0430\u043d\u0430 \u0444\u0443\u043d\u043a\u0446\u0456\u044f \"\u0417\u043d\u0430\u0439\u0442\u0438 iPhone\".",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e"
+ },
+ "error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "send_verification_code": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u0438\u0442\u0438 \u043a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f.",
+ "validate_verification_code": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0438\u0442\u0438 \u043a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f, \u0432\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0434\u043e\u0432\u0456\u0440\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0442\u0430 \u043f\u043e\u0447\u043d\u0456\u0442\u044c \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0443 \u0437\u043d\u043e\u0432\u0443."
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "description": "\u0420\u0430\u043d\u0456\u0448\u0435 \u0432\u0432\u0435\u0434\u0435\u043d\u0438\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u0431\u0456\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u0440\u0430\u0446\u044e\u0454. \u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c, \u0449\u043e\u0431 \u043f\u0440\u043e\u0434\u043e\u0432\u0436\u0438\u0442\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0446\u044e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e.",
+ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e"
+ },
+ "trusted_device": {
+ "data": {
+ "trusted_device": "\u0414\u043e\u0432\u0456\u0440\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0434\u043e\u0432\u0456\u0440\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439",
+ "title": "\u0414\u043e\u0432\u0456\u0440\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 iCloud"
+ },
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438",
+ "with_family": "\u0417 \u0441\u0456\u043c'\u0454\u044e"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.",
+ "title": "\u041e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456 iCloud"
+ },
+ "verification_code": {
+ "data": {
+ "verification_code": "\u041a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f, \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 \u0432\u0456\u0434 iCloud",
+ "title": "\u041a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f iCloud"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/icloud/translations/zh-Hant.json b/homeassistant/components/icloud/translations/zh-Hant.json
index 1c16db77faf56e..fe421275e2a61d 100644
--- a/homeassistant/components/icloud/translations/zh-Hant.json
+++ b/homeassistant/components/icloud/translations/zh-Hant.json
@@ -8,7 +8,7 @@
"error": {
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"send_verification_code": "\u50b3\u9001\u9a57\u8b49\u78bc\u5931\u6557",
- "validate_verification_code": "\u7121\u6cd5\u9a57\u8b49\u8f38\u5165\u9a57\u8b49\u78bc\uff0c\u9078\u64c7\u4e00\u90e8\u4fe1\u4efb\u88dd\u7f6e\u3001\u7136\u5f8c\u91cd\u65b0\u57f7\u884c\u9a57\u8b49\u3002"
+ "validate_verification_code": "\u9a57\u8b49\u8f38\u5165\u9a57\u8b49\u78bc\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002"
},
"step": {
"reauth": {
diff --git a/homeassistant/components/ifttt/translations/de.json b/homeassistant/components/ifttt/translations/de.json
index c96928afa18d23..5184e89f29a11a 100644
--- a/homeassistant/components/ifttt/translations/de.json
+++ b/homeassistant/components/ifttt/translations/de.json
@@ -1,5 +1,9 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.",
+ "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen."
+ },
"create_entry": {
"default": "Um Ereignisse an Home Assistant zu senden, musst du die Aktion \"Eine Webanforderung erstellen\" aus dem [IFTTT Webhook Applet]({applet_url}) ausw\u00e4hlen.\n\nF\u00fclle folgende Informationen aus: \n- URL: `{webhook_url}`\n- Methode: POST\n- Inhaltstyp: application/json\n\nIn der Dokumentation ({docs_url}) findest du Informationen zur Konfiguration der Automation eingehender Daten."
},
diff --git a/homeassistant/components/ifttt/translations/hu.json b/homeassistant/components/ifttt/translations/hu.json
index c3e7007b1a02a5..9898beb3e92cfa 100644
--- a/homeassistant/components/ifttt/translations/hu.json
+++ b/homeassistant/components/ifttt/translations/hu.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.",
+ "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz."
+ },
"create_entry": {
- "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, akkor az \u201eIFTTT Webhook kisalkalmaz\u00e1s\u201d ( {applet_url} ) \"Webk\u00e9r\u00e9s k\u00e9sz\u00edt\u00e9se\" m\u0171velet\u00e9t kell haszn\u00e1lnia. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n L\u00e1sd [a dokument\u00e1ci\u00f3] ( {docs_url} ), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re."
+ "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, akkor az [IFTTT Webhook applet]({applet_url}) \"Make a web request\" m\u0171velet\u00e9t kell haszn\u00e1lnia. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n L\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re."
},
"step": {
"user": {
diff --git a/homeassistant/components/ifttt/translations/id.json b/homeassistant/components/ifttt/translations/id.json
new file mode 100644
index 00000000000000..f997f39a54eadc
--- /dev/null
+++ b/homeassistant/components/ifttt/translations/id.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.",
+ "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook."
+ },
+ "create_entry": {
+ "default": "Untuk mengirim event ke Home Assistant, Anda harus menggunakan tindakan \"Make a web request\" dari [applet IFTTT Webhook]({applet_url}).\n\nIsi info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nBaca [dokumentasi]({docs_url}) tentang cara mengonfigurasi otomasi untuk menangani data masuk."
+ },
+ "step": {
+ "user": {
+ "description": "Yakin ingin menyiapkan IFTTT?",
+ "title": "Siapkan Applet IFTTT Webhook"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ifttt/translations/ko.json b/homeassistant/components/ifttt/translations/ko.json
index 93daad9e18267d..8f01109da76801 100644
--- a/homeassistant/components/ifttt/translations/ko.json
+++ b/homeassistant/components/ifttt/translations/ko.json
@@ -1,11 +1,15 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4."
+ },
"create_entry": {
- "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\uae30 \uc704\ud574\uc11c\ub294 [IFTTT \uc6f9 \ud6c5 \uc560\ud50c\ub9bf]({applet_url}) \uc5d0\uc11c \"Make a web request\" \ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nHome Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
+ "default": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\uae30 \uc704\ud574\uc11c\ub294 [IFTTT \uc6f9 \ud6c5 \uc560\ud50c\ub9bf]({applet_url})\uc5d0\uc11c \"Make a web request\"\ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nHome Assistant\ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
},
"step": {
"user": {
- "description": "IFTTT \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "description": "IFTTT\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "IFTTT \uc6f9 \ud6c5 \uc560\ud50c\ub9bf \uc124\uc815\ud558\uae30"
}
}
diff --git a/homeassistant/components/ifttt/translations/nl.json b/homeassistant/components/ifttt/translations/nl.json
index e7da47dd658b76..82006860db3adc 100644
--- a/homeassistant/components/ifttt/translations/nl.json
+++ b/homeassistant/components/ifttt/translations/nl.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk."
+ "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.",
+ "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen."
},
"create_entry": {
"default": "Om evenementen naar de Home Assistant te verzenden, moet u de actie \"Een webverzoek doen\" gebruiken vanuit de [IFTTT Webhook-applet]({applet_url}). \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nZie [the documentation]({docs_url}) voor informatie over het configureren van automatiseringen om inkomende gegevens te verwerken."
diff --git a/homeassistant/components/ifttt/translations/tr.json b/homeassistant/components/ifttt/translations/tr.json
new file mode 100644
index 00000000000000..84adcdf8225c43
--- /dev/null
+++ b/homeassistant/components/ifttt/translations/tr.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.",
+ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ifttt/translations/uk.json b/homeassistant/components/ifttt/translations/uk.json
new file mode 100644
index 00000000000000..8ea8f2b1970da3
--- /dev/null
+++ b/homeassistant/components/ifttt/translations/uk.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.",
+ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c."
+ },
+ "create_entry": {
+ "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u0456\u044e \"Make a web request\" \u0437 [IFTTT Webhook applet]({applet_url}). \n\n\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0456\u0439 \u043f\u043e \u043e\u0431\u0440\u043e\u0431\u0446\u0456 \u0434\u0430\u043d\u0438\u0445, \u0449\u043e \u043d\u0430\u0434\u0445\u043e\u0434\u044f\u0442\u044c."
+ },
+ "step": {
+ "user": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 IFTTT?",
+ "title": "IFTTT"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py
index cc06110c111130..314a7bdea31a60 100644
--- a/homeassistant/components/ign_sismologia/geo_location.py
+++ b/homeassistant/components/ign_sismologia/geo_location.py
@@ -1,7 +1,8 @@
"""Support for IGN Sismologia (Earthquakes) Feeds."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Optional
from georss_ign_sismologia_client import IgnSismologiaFeedManager
import voluptuous as vol
@@ -165,7 +166,7 @@ def _delete_callback(self):
"""Remove this entity."""
self._remove_signal_delete()
self._remove_signal_update()
- self.hass.async_create_task(self.async_remove())
+ self.hass.async_create_task(self.async_remove(force_remove=True))
@callback
def _update_callback(self):
@@ -207,7 +208,7 @@ def source(self) -> str:
return SOURCE
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the entity."""
if self._magnitude and self._region:
return f"M {self._magnitude:.1f} - {self._region}"
@@ -218,17 +219,17 @@ def name(self) -> Optional[str]:
return self._title
@property
- def distance(self) -> Optional[float]:
+ def distance(self) -> float | None:
"""Return distance value of this external event."""
return self._distance
@property
- def latitude(self) -> Optional[float]:
+ def latitude(self) -> float | None:
"""Return latitude value of this external event."""
return self._latitude
@property
- def longitude(self) -> Optional[float]:
+ def longitude(self) -> float | None:
"""Return longitude value of this external event."""
return self._longitude
@@ -238,7 +239,7 @@ def unit_of_measurement(self):
return LENGTH_KILOMETERS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
for key, value in (
diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py
index f200c9651f0ea9..959d86a7cc1787 100644
--- a/homeassistant/components/ihc/__init__.py
+++ b/homeassistant/components/ihc/__init__.py
@@ -23,6 +23,7 @@
from homeassistant.helpers.typing import HomeAssistantType
from .const import (
+ ATTR_CONTROLLER_ID,
ATTR_IHC_ID,
ATTR_VALUE,
CONF_AUTOSETUP,
@@ -54,7 +55,7 @@
IHC_CONTROLLER = "controller"
IHC_INFO = "info"
-IHC_PLATFORMS = ("binary_sensor", "light", "sensor", "switch")
+PLATFORMS = ("binary_sensor", "light", "sensor", "switch")
def validate_name(config):
@@ -186,13 +187,18 @@ def validate_name(config):
)
SET_RUNTIME_VALUE_BOOL_SCHEMA = vol.Schema(
- {vol.Required(ATTR_IHC_ID): cv.positive_int, vol.Required(ATTR_VALUE): cv.boolean}
+ {
+ vol.Required(ATTR_IHC_ID): cv.positive_int,
+ vol.Required(ATTR_VALUE): cv.boolean,
+ vol.Optional(ATTR_CONTROLLER_ID, default=0): cv.positive_int,
+ }
)
SET_RUNTIME_VALUE_INT_SCHEMA = vol.Schema(
{
vol.Required(ATTR_IHC_ID): cv.positive_int,
vol.Required(ATTR_VALUE): vol.Coerce(int),
+ vol.Optional(ATTR_CONTROLLER_ID, default=0): cv.positive_int,
}
)
@@ -200,14 +206,20 @@ def validate_name(config):
{
vol.Required(ATTR_IHC_ID): cv.positive_int,
vol.Required(ATTR_VALUE): vol.Coerce(float),
+ vol.Optional(ATTR_CONTROLLER_ID, default=0): cv.positive_int,
}
)
-PULSE_SCHEMA = vol.Schema({vol.Required(ATTR_IHC_ID): cv.positive_int})
+PULSE_SCHEMA = vol.Schema(
+ {
+ vol.Required(ATTR_IHC_ID): cv.positive_int,
+ vol.Optional(ATTR_CONTROLLER_ID, default=0): cv.positive_int,
+ }
+)
def setup(hass, config):
- """Set up the IHC platform."""
+ """Set up the IHC integration."""
conf = config.get(DOMAIN)
for index, controller_conf in enumerate(conf):
if not ihc_setup(hass, config, controller_conf, index):
@@ -217,8 +229,7 @@ def setup(hass, config):
def ihc_setup(hass, config, conf, controller_id):
- """Set up the IHC component."""
-
+ """Set up the IHC integration."""
url = conf[CONF_URL]
username = conf[CONF_USERNAME]
password = conf[CONF_PASSWORD]
@@ -237,17 +248,19 @@ def ihc_setup(hass, config, conf, controller_id):
# Store controller configuration
ihc_key = f"ihc{controller_id}"
hass.data[ihc_key] = {IHC_CONTROLLER: ihc_controller, IHC_INFO: conf[CONF_INFO]}
- setup_service_functions(hass, ihc_controller)
+ # We only want to register the service functions once for the first controller
+ if controller_id == 0:
+ setup_service_functions(hass)
return True
def get_manual_configuration(hass, config, conf, ihc_controller, controller_id):
"""Get manual configuration for IHC devices."""
- for component in IHC_PLATFORMS:
+ for platform in PLATFORMS:
discovery_info = {}
- if component in conf:
- component_setup = conf.get(component)
- for sensor_cfg in component_setup:
+ if platform in conf:
+ platform_setup = conf.get(platform)
+ for sensor_cfg in platform_setup:
name = sensor_cfg[CONF_NAME]
device = {
"ihc_id": sensor_cfg[CONF_ID],
@@ -268,14 +281,13 @@ def get_manual_configuration(hass, config, conf, ihc_controller, controller_id):
}
discovery_info[name] = device
if discovery_info:
- discovery.load_platform(hass, component, DOMAIN, discovery_info, config)
+ discovery.load_platform(hass, platform, DOMAIN, discovery_info, config)
def autosetup_ihc_products(
hass: HomeAssistantType, config, ihc_controller, controller_id
):
"""Auto setup of IHC products from the IHC project file."""
-
project_xml = ihc_controller.get_project()
if not project_xml:
_LOGGER.error("Unable to read project from IHC controller")
@@ -292,21 +304,23 @@ def autosetup_ihc_products(
except vol.Invalid as exception:
_LOGGER.error("Invalid IHC auto setup data: %s", exception)
return False
+
groups = project.findall(".//group")
- for component in IHC_PLATFORMS:
- component_setup = auto_setup_conf[component]
- discovery_info = get_discovery_info(component_setup, groups, controller_id)
+ for platform in PLATFORMS:
+ platform_setup = auto_setup_conf[platform]
+ discovery_info = get_discovery_info(platform_setup, groups, controller_id)
if discovery_info:
- discovery.load_platform(hass, component, DOMAIN, discovery_info, config)
+ discovery.load_platform(hass, platform, DOMAIN, discovery_info, config)
+
return True
-def get_discovery_info(component_setup, groups, controller_id):
- """Get discovery info for specified IHC component."""
+def get_discovery_info(platform_setup, groups, controller_id):
+ """Get discovery info for specified IHC platform."""
discovery_data = {}
for group in groups:
groupname = group.attrib["name"]
- for product_cfg in component_setup:
+ for product_cfg in platform_setup:
products = group.findall(product_cfg[CONF_XPATH])
for product in products:
nodes = product.findall(product_cfg[CONF_NODE])
@@ -329,30 +343,39 @@ def get_discovery_info(component_setup, groups, controller_id):
return discovery_data
-def setup_service_functions(hass: HomeAssistantType, ihc_controller):
+def setup_service_functions(hass: HomeAssistantType):
"""Set up the IHC service functions."""
+ def _get_controller(call):
+ controller_id = call.data[ATTR_CONTROLLER_ID]
+ ihc_key = f"ihc{controller_id}"
+ return hass.data[ihc_key][IHC_CONTROLLER]
+
def set_runtime_value_bool(call):
"""Set a IHC runtime bool value service function."""
ihc_id = call.data[ATTR_IHC_ID]
value = call.data[ATTR_VALUE]
+ ihc_controller = _get_controller(call)
ihc_controller.set_runtime_value_bool(ihc_id, value)
def set_runtime_value_int(call):
"""Set a IHC runtime integer value service function."""
ihc_id = call.data[ATTR_IHC_ID]
value = call.data[ATTR_VALUE]
+ ihc_controller = _get_controller(call)
ihc_controller.set_runtime_value_int(ihc_id, value)
def set_runtime_value_float(call):
"""Set a IHC runtime float value service function."""
ihc_id = call.data[ATTR_IHC_ID]
value = call.data[ATTR_VALUE]
+ ihc_controller = _get_controller(call)
ihc_controller.set_runtime_value_float(ihc_id, value)
async def async_pulse_runtime_input(call):
"""Pulse a IHC controller input function."""
ihc_id = call.data[ATTR_IHC_ID]
+ ihc_controller = _get_controller(call)
await async_pulse(hass, ihc_controller, ihc_id)
hass.services.register(
diff --git a/homeassistant/components/ihc/const.py b/homeassistant/components/ihc/const.py
index 15db19ba58b981..c751d7990e4740 100644
--- a/homeassistant/components/ihc/const.py
+++ b/homeassistant/components/ihc/const.py
@@ -6,7 +6,6 @@
CONF_INFO = "info"
CONF_INVERTING = "inverting"
CONF_LIGHT = "light"
-CONF_NAME = "name"
CONF_NODE = "node"
CONF_NOTE = "note"
CONF_OFF_ID = "off_id"
@@ -18,6 +17,7 @@
ATTR_IHC_ID = "ihc_id"
ATTR_VALUE = "value"
+ATTR_CONTROLLER_ID = "controller_id"
SERVICE_SET_RUNTIME_VALUE_BOOL = "set_runtime_value_bool"
SERVICE_SET_RUNTIME_VALUE_FLOAT = "set_runtime_value_float"
diff --git a/homeassistant/components/ihc/ihc_auto_setup.yaml b/homeassistant/components/ihc/ihc_auto_setup.yaml
index d5f8d26e2b79bb..7a94afdae44d3c 100644
--- a/homeassistant/components/ihc/ihc_auto_setup.yaml
+++ b/homeassistant/components/ihc/ihc_auto_setup.yaml
@@ -34,6 +34,30 @@ binary_sensor:
type: "light"
light:
+ # Swedish Wireless dimmer (Mobil VU/Dimmer 1-knapp/touch)
+ - xpath: './/product_airlink[@product_identifier="_0x4301"]'
+ node: "airlink_dimming"
+ dimmable: true
+ # Swedish Wireless dimmer (Lamputtag/Dimmer 1-knapp/touch)
+ - xpath: './/product_airlink[@product_identifier="_0x4302"]'
+ node: "airlink_dimming"
+ dimmable: true
+ # Swedish Wireless dimmer (Blind/Dimmer 1-knapp/touch)
+ - xpath: './/product_airlink[@product_identifier="_0x4305"]'
+ node: "airlink_dimming"
+ dimmable: true
+ # Swedish Wireless dimmer (3-tråds Puck/Dimmer 1-knapp/touch)
+ - xpath: './/product_airlink[@product_identifier="_0x4307"]'
+ node: "airlink_dimming"
+ dimmable: true
+ # Swedish Wireless dimmer (3-tråds Puck/Dimmer 2-knapp)
+ - xpath: './/product_airlink[@product_identifier="_0x4308"]'
+ node: "airlink_dimming"
+ dimmable: true
+ # 2 channel RS485 dimmer
+ - xpath: './/rs485_led_dimmer_channel[@product_identifier="_0x4410"]'
+ node: "airlink_dimming"
+ dimmable: true
# Wireless Combi dimmer 4 buttons
- xpath: './/product_airlink[@product_identifier="_0x4406"]'
node: "airlink_dimming"
diff --git a/homeassistant/components/ihc/ihcdevice.py b/homeassistant/components/ihc/ihcdevice.py
index 0b3dc763ca07fe..e351d2f38eafff 100644
--- a/homeassistant/components/ihc/ihcdevice.py
+++ b/homeassistant/components/ihc/ihcdevice.py
@@ -42,7 +42,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if not self.info:
return {}
diff --git a/homeassistant/components/ihc/sensor.py b/homeassistant/components/ihc/sensor.py
index cb1688bc7bee8d..3348e857f517be 100644
--- a/homeassistant/components/ihc/sensor.py
+++ b/homeassistant/components/ihc/sensor.py
@@ -1,6 +1,6 @@
"""Support for IHC sensors."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_UNIT_OF_MEASUREMENT
-from homeassistant.helpers.entity import Entity
from . import IHC_CONTROLLER, IHC_INFO
from .ihcdevice import IHCDevice
@@ -26,7 +26,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(devices)
-class IHCSensor(IHCDevice, Entity):
+class IHCSensor(IHCDevice, SensorEntity):
"""Implementation of the IHC sensor."""
def __init__(
diff --git a/homeassistant/components/ihc/services.yaml b/homeassistant/components/ihc/services.yaml
index ad41539162cc10..a65d5f5b78c354 100644
--- a/homeassistant/components/ihc/services.yaml
+++ b/homeassistant/components/ihc/services.yaml
@@ -3,29 +3,56 @@
set_runtime_value_bool:
description: Set a boolean runtime value on the IHC controller.
fields:
+ controller_id:
+ description: |
+ If you have multiple controller, this is the index of you controller
+ starting with 0 (0 is default)
+ example: 0
ihc_id:
description: The integer IHC resource ID.
+ example: 123456
value:
description: The boolean value to set.
+ example: true
set_runtime_value_int:
description: Set an integer runtime value on the IHC controller.
fields:
+ controller_id:
+ description: |
+ If you have multiple controller, this is the index of you controller
+ starting with 0 (0 is default)
+ example: 0
ihc_id:
description: The integer IHC resource ID.
+ example: 123456
value:
description: The integer value to set.
+ example: 50
set_runtime_value_float:
description: Set a float runtime value on the IHC controller.
fields:
+ controller_id:
+ description: |
+ If you have multiple controller, this is the index of you controller
+ starting with 0 (0 is default)
+ example: 0
ihc_id:
description: The integer IHC resource ID.
+ example: 123456
value:
description: The float value to set.
+ example: 1.47
pulse:
description: Pulses an input on the IHC controller.
fields:
+ controller_id:
+ description: |
+ If you have multiple controller, this is the index of you controller
+ starting with 0 (0 is default)
+ example: 0
ihc_id:
description: The integer IHC resource ID.
+ example: 123456
diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py
index c68df580643f38..37b3bd7ff6ad51 100644
--- a/homeassistant/components/image/__init__.py
+++ b/homeassistant/components/image/__init__.py
@@ -1,10 +1,11 @@
"""The Picture integration."""
+from __future__ import annotations
+
import asyncio
import logging
import pathlib
import secrets
import shutil
-import typing
from PIL import Image, ImageOps, UnidentifiedImageError
from aiohttp import hdrs, web
@@ -69,7 +70,7 @@ def __init__(self, hass: HomeAssistant, image_dir: pathlib.Path) -> None:
self.async_add_listener(self._change_listener)
self.image_dir = image_dir
- async def _process_create_data(self, data: typing.Dict) -> typing.Dict:
+ async def _process_create_data(self, data: dict) -> dict:
"""Validate the config is valid."""
data = self.CREATE_SCHEMA(dict(data))
uploaded_file: FileField = data["file"]
@@ -117,11 +118,11 @@ def _move_data(self, data):
return media_file.stat().st_size
@callback
- def _get_suggested_id(self, info: typing.Dict) -> str:
+ def _get_suggested_id(self, info: dict) -> str:
"""Suggest an ID based on the config."""
return info[CONF_ID]
- async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict:
+ async def _update_data(self, data: dict, update_data: dict) -> dict:
"""Return a new updated data object."""
return {**data, **self.UPDATE_SCHEMA(update_data)}
diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json
index 6978f09ab680f0..741fb8511a6e55 100644
--- a/homeassistant/components/image/manifest.json
+++ b/homeassistant/components/image/manifest.json
@@ -3,7 +3,7 @@
"name": "Image",
"config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/image",
- "requirements": ["pillow==8.1.0"],
+ "requirements": ["pillow==8.1.2"],
"dependencies": ["http"],
"codeowners": ["@home-assistant/core"],
"quality_scale": "internal"
diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py
index 82672c22015490..58a6582e33c010 100644
--- a/homeassistant/components/image_processing/__init__.py
+++ b/homeassistant/components/image_processing/__init__.py
@@ -2,10 +2,17 @@
import asyncio
from datetime import timedelta
import logging
+from typing import final
import voluptuous as vol
-from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_ENTITY_ID, CONF_NAME
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_NAME,
+ CONF_ENTITY_ID,
+ CONF_NAME,
+ CONF_SOURCE,
+)
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
@@ -39,7 +46,6 @@
ATTR_MOTION = "motion"
ATTR_TOTAL_FACES = "total_faces"
-CONF_SOURCE = "source"
CONF_CONFIDENCE = "confidence"
DEFAULT_TIMEOUT = 10
@@ -76,7 +82,7 @@ async def async_scan_service(service):
update_tasks = []
for entity in image_entities:
entity.async_set_context(service.context)
- update_tasks.append(entity.async_update_ha_state(True))
+ update_tasks.append(asyncio.create_task(entity.async_update_ha_state(True)))
if update_tasks:
await asyncio.wait(update_tasks)
@@ -170,6 +176,7 @@ def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return "face"
+ @final
@property
def state_attributes(self):
"""Return device specific state attributes."""
@@ -201,9 +208,12 @@ def async_process_faces(self, faces, total):
"""
# Send events
for face in faces:
- if ATTR_CONFIDENCE in face and self.confidence:
- if face[ATTR_CONFIDENCE] < self.confidence:
- continue
+ if (
+ ATTR_CONFIDENCE in face
+ and self.confidence
+ and face[ATTR_CONFIDENCE] < self.confidence
+ ):
+ continue
face.update({ATTR_ENTITY_ID: self.entity_id})
self.hass.async_add_job(self.hass.bus.async_fire, EVENT_DETECT_FACE, face)
diff --git a/homeassistant/components/image_processing/services.yaml b/homeassistant/components/image_processing/services.yaml
index 69e455344b0ab2..cd074acd9f433b 100644
--- a/homeassistant/components/image_processing/services.yaml
+++ b/homeassistant/components/image_processing/services.yaml
@@ -1,8 +1,5 @@
# Describes the format for available image processing services
scan:
- description: Process an image immediately.
- fields:
- entity_id:
- description: Name(s) of entities to scan immediately.
- example: "image_processing.alpr_garage"
+ description: Process an image immediately
+ target:
diff --git a/homeassistant/components/image_processing/translations/id.json b/homeassistant/components/image_processing/translations/id.json
index 19e3a64dcbadd8..c186898fd07a75 100644
--- a/homeassistant/components/image_processing/translations/id.json
+++ b/homeassistant/components/image_processing/translations/id.json
@@ -1,3 +1,3 @@
{
- "title": "Pengolahan gambar"
+ "title": "Pengolahan citra"
}
\ No newline at end of file
diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py
index 4917abc6028ac5..4158d1be801943 100644
--- a/homeassistant/components/imap/sensor.py
+++ b/homeassistant/components/imap/sensor.py
@@ -6,7 +6,7 @@
import async_timeout
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_NAME,
CONF_PASSWORD,
@@ -16,7 +16,6 @@
)
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -62,7 +61,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([sensor], True)
-class ImapSensor(Entity):
+class ImapSensor(SensorEntity):
"""Representation of an IMAP sensor."""
def __init__(self, name, user, password, server, port, charset, folder, search):
diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py
index 04d4ca97c5ab2e..cdd47d68d76130 100644
--- a/homeassistant/components/imap_email_content/sensor.py
+++ b/homeassistant/components/imap_email_content/sensor.py
@@ -7,7 +7,7 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_DATE,
CONF_NAME,
@@ -18,7 +18,6 @@
CONTENT_TYPE_TEXT_PLAIN,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -145,7 +144,7 @@ def read_next(self):
return None
-class EmailContentSensor(Entity):
+class EmailContentSensor(SensorEntity):
"""Representation of an EMail sensor."""
def __init__(self, hass, email_reader, name, allowed_senders, value_template):
@@ -171,7 +170,7 @@ def state(self):
return self._message
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return other state attributes for the message."""
return self._state_attributes
@@ -221,9 +220,11 @@ def get_msg_text(email_message):
elif part.get_content_type() == "text/html":
if message_html is None:
message_html = part.get_payload()
- elif part.get_content_type().startswith("text"):
- if message_untyped_text is None:
- message_untyped_text = part.get_payload()
+ elif (
+ part.get_content_type().startswith("text")
+ and message_untyped_text is None
+ ):
+ message_untyped_text = part.get_payload()
if message_text is not None:
return message_text
diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py
index cec550d24d95e2..cea7244919b180 100644
--- a/homeassistant/components/incomfort/__init__.py
+++ b/homeassistant/components/incomfort/__init__.py
@@ -1,6 +1,7 @@
"""Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway."""
+from __future__ import annotations
+
import logging
-from typing import Optional
from aiohttp import ClientResponseError
from incomfortclient import Gateway as InComfortGateway
@@ -68,12 +69,12 @@ def __init__(self) -> None:
self._unique_id = self._name = None
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID."""
return self._unique_id
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the sensor."""
return self._name
diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py
index bf1340fb235496..cc8d2e24a0aee5 100644
--- a/homeassistant/components/incomfort/binary_sensor.py
+++ b/homeassistant/components/incomfort/binary_sensor.py
@@ -1,5 +1,7 @@
"""Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway."""
-from typing import Any, Dict, Optional
+from __future__ import annotations
+
+from typing import Any
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
@@ -40,6 +42,6 @@ def is_on(self) -> bool:
return self._heater.status["is_failed"]
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the device state attributes."""
return {"fault_code": self._heater.status["fault_code"]}
diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py
index 274308efe064c1..e44090a0b48ca0 100644
--- a/homeassistant/components/incomfort/climate.py
+++ b/homeassistant/components/incomfort/climate.py
@@ -1,5 +1,7 @@
"""Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway."""
-from typing import Any, Dict, List, Optional
+from __future__ import annotations
+
+from typing import Any
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity
from homeassistant.components.climate.const import (
@@ -39,7 +41,7 @@ def __init__(self, client, heater, room) -> None:
self._room = room
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the device state attributes."""
return {"status": self._room.status}
@@ -54,17 +56,17 @@ def hvac_mode(self) -> str:
return HVAC_MODE_HEAT
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes."""
return [HVAC_MODE_HEAT]
@property
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self._room.room_temp
@property
- def target_temperature(self) -> Optional[float]:
+ def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
return self._room.setpoint
diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json
index 80b6952c3836a6..891cbb20be453c 100644
--- a/homeassistant/components/incomfort/manifest.json
+++ b/homeassistant/components/incomfort/manifest.json
@@ -2,6 +2,6 @@
"domain": "incomfort",
"name": "Intergas InComfort/Intouch Lan2RF gateway",
"documentation": "https://www.home-assistant.io/integrations/incomfort",
- "requirements": ["incomfort-client==0.4.0"],
+ "requirements": ["incomfort-client==0.4.4"],
"codeowners": ["@zxdavb"]
}
diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py
index 692eecf2317b55..a9e1faaba10223 100644
--- a/homeassistant/components/incomfort/sensor.py
+++ b/homeassistant/components/incomfort/sensor.py
@@ -1,7 +1,9 @@
"""Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway."""
-from typing import Any, Dict, Optional
+from __future__ import annotations
-from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from typing import Any
+
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity
from homeassistant.const import (
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
@@ -38,7 +40,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
-class IncomfortSensor(IncomfortChild):
+class IncomfortSensor(IncomfortChild, SensorEntity):
"""Representation of an InComfort/InTouch sensor device."""
def __init__(self, client, heater, name) -> None:
@@ -57,17 +59,17 @@ def __init__(self, client, heater, name) -> None:
self._unit_of_measurement = None
@property
- def state(self) -> Optional[str]:
+ def state(self) -> str | None:
"""Return the state of the sensor."""
return self._heater.status[self._state_attr]
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the device class of the sensor."""
return self._device_class
@property
- def unit_of_measurement(self) -> Optional[str]:
+ def unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of the sensor."""
return self._unit_of_measurement
@@ -95,6 +97,6 @@ def __init__(self, client, heater, name) -> None:
self._unit_of_measurement = TEMP_CELSIUS
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the device state attributes."""
return {self._attr: self._heater.status[self._attr]}
diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py
index da6e6d893150a7..84ed0212d3bfd0 100644
--- a/homeassistant/components/incomfort/water_heater.py
+++ b/homeassistant/components/incomfort/water_heater.py
@@ -1,7 +1,9 @@
"""Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict
+from typing import Any
from aiohttp import ClientResponseError
@@ -50,7 +52,7 @@ def icon(self) -> str:
return "mdi:thermometer-lines"
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the device state attributes."""
return {k: v for k, v in self._heater.status.items() if k in HEATER_ATTRS}
diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py
index 16b6971b11fab1..dde10ffca76e49 100644
--- a/homeassistant/components/influxdb/__init__.py
+++ b/homeassistant/components/influxdb/__init__.py
@@ -1,11 +1,14 @@
"""Support for sending data to an Influx database."""
+from __future__ import annotations
+
+from contextlib import suppress
from dataclasses import dataclass
import logging
import math
import queue
import threading
import time
-from typing import Any, Callable, Dict, List
+from typing import Any, Callable
from influxdb import InfluxDBClient, exceptions
from influxdb_client import InfluxDBClient as InfluxDBClientV2
@@ -62,6 +65,7 @@
CONF_PRECISION,
CONF_RETRY_COUNT,
CONF_SSL,
+ CONF_SSL_CA_CERT,
CONF_TAGS,
CONF_TAGS_ATTRIBUTES,
CONF_TOKEN,
@@ -99,7 +103,7 @@
_LOGGER = logging.getLogger(__name__)
-def create_influx_url(conf: Dict) -> Dict:
+def create_influx_url(conf: dict) -> dict:
"""Build URL used from config inputs and default when necessary."""
if conf[CONF_API_VERSION] == API_VERSION_2:
if CONF_SSL not in conf:
@@ -124,7 +128,7 @@ def create_influx_url(conf: Dict) -> Dict:
return conf
-def validate_version_specific_config(conf: Dict) -> Dict:
+def validate_version_specific_config(conf: dict) -> dict:
"""Ensure correct config fields are provided based on API version used."""
if conf[CONF_API_VERSION] == API_VERSION_2:
if CONF_TOKEN not in conf:
@@ -192,7 +196,7 @@ def validate_version_specific_config(conf: Dict) -> Dict:
)
-def _generate_event_to_json(conf: Dict) -> Callable[[Dict], str]:
+def _generate_event_to_json(conf: dict) -> Callable[[dict], str]:
"""Build event to json converter and add to config."""
entity_filter = convert_include_exclude_filter(conf)
tags = conf.get(CONF_TAGS)
@@ -207,7 +211,7 @@ def _generate_event_to_json(conf: Dict) -> Callable[[Dict], str]:
conf[CONF_COMPONENT_CONFIG_GLOB],
)
- def event_to_json(event: Dict) -> str:
+ def event_to_json(event: dict) -> str:
"""Convert event into json in format Influx expects."""
state = event.data.get(EVENT_NEW_STATE)
if (
@@ -301,11 +305,9 @@ def event_to_json(event: Dict) -> str:
)
# Infinity and NaN are not valid floats in InfluxDB
- try:
+ with suppress(KeyError, TypeError):
if not math.isfinite(json[INFLUX_CONF_FIELDS][key]):
del json[INFLUX_CONF_FIELDS][key]
- except (KeyError, TypeError):
- pass
json[INFLUX_CONF_TAGS].update(tags)
@@ -318,9 +320,9 @@ def event_to_json(event: Dict) -> str:
class InfluxClient:
"""An InfluxDB client wrapper for V1 or V2."""
- data_repositories: List[str]
+ data_repositories: list[str]
write: Callable[[str], None]
- query: Callable[[str, str], List[Any]]
+ query: Callable[[str, str], list[Any]]
close: Callable[[], None]
@@ -335,6 +337,9 @@ def get_influx_connection(conf, test_write=False, test_read=False):
kwargs[CONF_URL] = conf[CONF_URL]
kwargs[CONF_TOKEN] = conf[CONF_TOKEN]
kwargs[INFLUX_CONF_ORG] = conf[CONF_ORG]
+ kwargs[CONF_VERIFY_SSL] = conf[CONF_VERIFY_SSL]
+ if CONF_SSL_CA_CERT in conf:
+ kwargs[CONF_SSL_CA_CERT] = conf[CONF_SSL_CA_CERT]
bucket = conf.get(CONF_BUCKET)
influx = InfluxDBClientV2(**kwargs)
query_api = influx.query_api()
@@ -376,10 +381,8 @@ def close_v2():
if test_write:
# Try to write b"" to influx. If we can connect and creds are valid
# Then invalid inputs is returned. Anything else is a broken config
- try:
+ with suppress(ValueError):
write_v2(b"")
- except ValueError:
- pass
write_api = influx.write_api(write_options=ASYNCHRONOUS)
if test_read:
@@ -392,7 +395,10 @@ def close_v2():
return InfluxClient(buckets, write_v2, query_v2, close_v2)
# Else it's a V1 client
- kwargs[CONF_VERIFY_SSL] = conf[CONF_VERIFY_SSL]
+ if CONF_SSL_CA_CERT in conf and conf[CONF_VERIFY_SSL]:
+ kwargs[CONF_VERIFY_SSL] = conf[CONF_SSL_CA_CERT]
+ else:
+ kwargs[CONF_VERIFY_SSL] = conf[CONF_VERIFY_SSL]
if CONF_DB_NAME in conf:
kwargs[CONF_DB_NAME] = conf[CONF_DB_NAME]
@@ -521,7 +527,7 @@ def get_events_json(self):
dropped = 0
- try:
+ with suppress(queue.Empty):
while len(json) < BATCH_BUFFER_SIZE and not self.shutdown:
timeout = None if count == 0 else self.batch_timeout()
item = self.queue.get(timeout=timeout)
@@ -540,9 +546,6 @@ def get_events_json(self):
else:
dropped += 1
- except queue.Empty:
- pass
-
if dropped:
_LOGGER.warning(CATCHING_UP_MESSAGE, dropped)
diff --git a/homeassistant/components/influxdb/const.py b/homeassistant/components/influxdb/const.py
index 1a827c1b63ca39..e66a0fe10c4ba9 100644
--- a/homeassistant/components/influxdb/const.py
+++ b/homeassistant/components/influxdb/const.py
@@ -31,6 +31,7 @@
CONF_RETRY_COUNT = "max_retries"
CONF_IGNORE_ATTRIBUTES = "ignore_attributes"
CONF_PRECISION = "precision"
+CONF_SSL_CA_CERT = "ssl_ca_cert"
CONF_LANGUAGE = "language"
CONF_QUERIES = "queries"
@@ -139,12 +140,13 @@
vol.Optional(CONF_PATH): cv.string,
vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_SSL): cv.boolean,
+ vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
+ vol.Optional(CONF_SSL_CA_CERT): cv.isfile,
vol.Optional(CONF_PRECISION): vol.In(["ms", "s", "us", "ns"]),
# Connection config for V1 API only.
vol.Inclusive(CONF_USERNAME, "authentication"): cv.string,
vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string,
vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string,
- vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
# Connection config for V2 API only.
vol.Inclusive(CONF_TOKEN, "v2_authentication"): cv.string,
vol.Inclusive(CONF_ORG, "v2_authentication"): cv.string,
diff --git a/homeassistant/components/influxdb/manifest.json b/homeassistant/components/influxdb/manifest.json
index ec1bd8f9594e4e..c2d6f77e7c1757 100644
--- a/homeassistant/components/influxdb/manifest.json
+++ b/homeassistant/components/influxdb/manifest.json
@@ -2,6 +2,6 @@
"domain": "influxdb",
"name": "InfluxDB",
"documentation": "https://www.home-assistant.io/integrations/influxdb",
- "requirements": ["influxdb==5.2.3", "influxdb-client==1.8.0"],
+ "requirements": ["influxdb==5.2.3", "influxdb-client==1.14.0"],
"codeowners": ["@fabaff", "@mdegat01"]
}
diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py
index ff9f6f931532f2..299fc595f4b458 100644
--- a/homeassistant/components/influxdb/sensor.py
+++ b/homeassistant/components/influxdb/sensor.py
@@ -1,10 +1,14 @@
"""InfluxDB component which allows you to get data from an Influx database."""
+from __future__ import annotations
+
import logging
-from typing import Dict
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA
+from homeassistant.components.sensor import (
+ PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
+ SensorEntity,
+)
from homeassistant.const import (
CONF_API_VERSION,
CONF_NAME,
@@ -15,7 +19,6 @@
)
from homeassistant.exceptions import PlatformNotReady, TemplateError
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from . import create_influx_url, get_influx_connection, validate_version_specific_config
@@ -67,7 +70,7 @@ def _merge_connection_config_into_query(conf, query):
query[key] = conf[key]
-def validate_query_format_for_version(conf: Dict) -> Dict:
+def validate_query_format_for_version(conf: dict) -> dict:
"""Ensure queries are provided in correct format based on API version."""
if conf[CONF_API_VERSION] == API_VERSION_2:
if CONF_QUERIES_FLUX not in conf:
@@ -168,7 +171,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda _: influx.close())
-class InfluxSensor(Entity):
+class InfluxSensor(SensorEntity):
"""Implementation of a Influxdb sensor."""
def __init__(self, hass, influx, query):
diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py
index f123d6d32979fc..e030a53025349f 100644
--- a/homeassistant/components/input_boolean/__init__.py
+++ b/homeassistant/components/input_boolean/__init__.py
@@ -1,6 +1,7 @@
"""Support to keep track of user controlled booleans for within automation."""
+from __future__ import annotations
+
import logging
-import typing
import voluptuous as vol
@@ -15,7 +16,7 @@
SERVICE_TURN_ON,
STATE_ON,
)
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import collection
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import ToggleEntity
@@ -23,7 +24,7 @@
from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.helpers.service
from homeassistant.helpers.storage import Store
-from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType
+from homeassistant.helpers.typing import ConfigType, ServiceCallType
from homeassistant.loader import bind_hass
DOMAIN = "input_boolean"
@@ -60,16 +61,16 @@ class InputBooleanStorageCollection(collection.StorageCollection):
CREATE_SCHEMA = vol.Schema(CREATE_FIELDS)
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
- async def _process_create_data(self, data: typing.Dict) -> typing.Dict:
+ async def _process_create_data(self, data: dict) -> dict:
"""Validate the config is valid."""
return self.CREATE_SCHEMA(data)
@callback
- def _get_suggested_id(self, info: typing.Dict) -> str:
+ def _get_suggested_id(self, info: dict) -> str:
"""Suggest an ID based on the config."""
return info[CONF_NAME]
- async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict:
+ async def _update_data(self, data: dict, update_data: dict) -> dict:
"""Return a new updated data object."""
update_data = self.UPDATE_SCHEMA(update_data)
return {**data, **update_data}
@@ -81,7 +82,7 @@ def is_on(hass, entity_id):
return hass.states.is_state(entity_id, STATE_ON)
-async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up an input boolean."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
id_manager = collection.IDManager()
@@ -89,8 +90,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
yaml_collection = collection.YamlCollection(
logging.getLogger(f"{__name__}.yaml_collection"), id_manager
)
- collection.attach_entity_component_collection(
- component, yaml_collection, lambda conf: InputBoolean(conf, from_yaml=True)
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, component, yaml_collection, InputBoolean.from_yaml
)
storage_collection = InputBooleanStorageCollection(
@@ -98,8 +99,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
logging.getLogger(f"{__name__}.storage_collection"),
id_manager,
)
- collection.attach_entity_component_collection(
- component, storage_collection, InputBoolean
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, component, storage_collection, InputBoolean
)
await yaml_collection.async_load(
@@ -111,9 +112,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
).async_setup(hass)
- collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection)
- collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection)
-
async def reload_service_handler(service_call: ServiceCallType) -> None:
"""Remove all input booleans and load new ones from config."""
conf = await component.async_prepare_reload(skip_reset=True)
@@ -146,14 +144,19 @@ async def reload_service_handler(service_call: ServiceCallType) -> None:
class InputBoolean(ToggleEntity, RestoreEntity):
"""Representation of a boolean input."""
- def __init__(self, config: typing.Optional[dict], from_yaml: bool = False):
+ def __init__(self, config: dict | None):
"""Initialize a boolean input."""
self._config = config
- self._editable = True
+ self.editable = True
self._state = config.get(CONF_INITIAL)
- if from_yaml:
- self._editable = False
- self.entity_id = f"{DOMAIN}.{self.unique_id}"
+
+ @classmethod
+ def from_yaml(cls, config: dict) -> InputBoolean:
+ """Return entity instance initialized from yaml storage."""
+ input_bool = cls(config)
+ input_bool.entity_id = f"{DOMAIN}.{config[CONF_ID]}"
+ input_bool.editable = False
+ return input_bool
@property
def should_poll(self):
@@ -166,9 +169,9 @@ def name(self):
return self._config.get(CONF_NAME)
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the entity."""
- return {ATTR_EDITABLE: self._editable}
+ return {ATTR_EDITABLE: self.editable}
@property
def icon(self):
@@ -205,7 +208,7 @@ async def async_turn_off(self, **kwargs):
self._state = False
self.async_write_ha_state()
- async def async_update_config(self, config: typing.Dict) -> None:
+ async def async_update_config(self, config: dict) -> None:
"""Handle when the config is updated."""
self._config = config
self.async_write_ha_state()
diff --git a/homeassistant/components/input_boolean/reproduce_state.py b/homeassistant/components/input_boolean/reproduce_state.py
index d01e931c5cceb6..5fe7e779a988ab 100644
--- a/homeassistant/components/input_boolean/reproduce_state.py
+++ b/homeassistant/components/input_boolean/reproduce_state.py
@@ -1,7 +1,9 @@
"""Reproduce an input boolean state."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -10,8 +12,7 @@
STATE_OFF,
STATE_ON,
)
-from homeassistant.core import Context, State
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import Context, HomeAssistant, State
from . import DOMAIN
@@ -19,11 +20,11 @@
async def _async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce input boolean states."""
cur_state = hass.states.get(state.entity_id)
@@ -53,11 +54,11 @@ async def _async_reproduce_states(
async def async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce component states."""
await asyncio.gather(
diff --git a/homeassistant/components/input_boolean/services.yaml b/homeassistant/components/input_boolean/services.yaml
index 5ab5e7a9b821d1..8cefe2b4974614 100644
--- a/homeassistant/components/input_boolean/services.yaml
+++ b/homeassistant/components/input_boolean/services.yaml
@@ -1,20 +1,14 @@
toggle:
- description: Toggles an input boolean.
- fields:
- entity_id:
- description: Entity id of the input boolean to toggle.
- example: input_boolean.notify_alerts
+ description: Toggle an input boolean
+ target:
+
turn_off:
- description: Turns off an input boolean
- fields:
- entity_id:
- description: Entity id of the input boolean to turn off.
- example: input_boolean.notify_alerts
+ description: Turn off an input boolean
+ target:
+
turn_on:
- description: Turns on an input boolean.
- fields:
- entity_id:
- description: Entity id of the input boolean to turn on.
- example: input_boolean.notify_alerts
+ description: Turn on an input boolean
+ target:
+
reload:
- description: Reload the input_boolean configuration.
+ description: Reload the input_boolean configuration
diff --git a/homeassistant/components/input_boolean/translations/id.json b/homeassistant/components/input_boolean/translations/id.json
index 4401df1f4530ae..df890baae4a2d9 100644
--- a/homeassistant/components/input_boolean/translations/id.json
+++ b/homeassistant/components/input_boolean/translations/id.json
@@ -1,8 +1,8 @@
{
"state": {
"_": {
- "off": "Off",
- "on": "On"
+ "off": "Mati",
+ "on": "Nyala"
}
},
"title": "Input boolean"
diff --git a/homeassistant/components/input_boolean/translations/ko.json b/homeassistant/components/input_boolean/translations/ko.json
index 712051d04a4729..5d7d597f79063e 100644
--- a/homeassistant/components/input_boolean/translations/ko.json
+++ b/homeassistant/components/input_boolean/translations/ko.json
@@ -5,5 +5,5 @@
"on": "\ucf1c\uc9d0"
}
},
- "title": "\ub17c\ub9ac\uc785\ub825"
+ "title": "\ub17c\ub9ac \uc785\ub825"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_boolean/translations/uk.json b/homeassistant/components/input_boolean/translations/uk.json
index c677957de475da..be22ae53807113 100644
--- a/homeassistant/components/input_boolean/translations/uk.json
+++ b/homeassistant/components/input_boolean/translations/uk.json
@@ -5,5 +5,5 @@
"on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e"
}
},
- "title": "\u0412\u0432\u0435\u0434\u0435\u043d\u043d\u044f \u043b\u043e\u0433\u0456\u0447\u043d\u043e\u0433\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f"
+ "title": "Input Boolean"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py
index 0eab810245ddea..68b7f9f32d5407 100644
--- a/homeassistant/components/input_datetime/__init__.py
+++ b/homeassistant/components/input_datetime/__init__.py
@@ -1,7 +1,8 @@
"""Support to select a date and/or a time."""
+from __future__ import annotations
+
import datetime as py_datetime
import logging
-import typing
import voluptuous as vol
@@ -14,14 +15,14 @@
CONF_NAME,
SERVICE_RELOAD,
)
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import collection
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.helpers.service
from homeassistant.helpers.storage import Store
-from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType
+from homeassistant.helpers.typing import ConfigType, ServiceCallType
from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -100,7 +101,7 @@ def has_date_or_time(conf):
RELOAD_SERVICE_SCHEMA = vol.Schema({})
-async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up an input datetime."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
id_manager = collection.IDManager()
@@ -108,8 +109,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
yaml_collection = collection.YamlCollection(
logging.getLogger(f"{__name__}.yaml_collection"), id_manager
)
- collection.attach_entity_component_collection(
- component, yaml_collection, InputDatetime.from_yaml
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, component, yaml_collection, InputDatetime.from_yaml
)
storage_collection = DateTimeStorageCollection(
@@ -117,8 +118,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
logging.getLogger(f"{__name__}.storage_collection"),
id_manager,
)
- collection.attach_entity_component_collection(
- component, storage_collection, InputDatetime
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, component, storage_collection, InputDatetime
)
await yaml_collection.async_load(
@@ -130,9 +131,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
).async_setup(hass)
- collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection)
- collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection)
-
async def reload_service_handler(service_call: ServiceCallType) -> None:
"""Reload yaml entities."""
conf = await component.async_prepare_reload(skip_reset=True)
@@ -179,16 +177,16 @@ class DateTimeStorageCollection(collection.StorageCollection):
CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, has_date_or_time))
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
- async def _process_create_data(self, data: typing.Dict) -> typing.Dict:
+ async def _process_create_data(self, data: dict) -> dict:
"""Validate the config is valid."""
return self.CREATE_SCHEMA(data)
@callback
- def _get_suggested_id(self, info: typing.Dict) -> str:
+ def _get_suggested_id(self, info: dict) -> str:
"""Suggest an ID based on the config."""
return info[CONF_NAME]
- async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict:
+ async def _update_data(self, data: dict, update_data: dict) -> dict:
"""Return a new updated data object."""
update_data = self.UPDATE_SCHEMA(update_data)
return has_date_or_time({**data, **update_data})
@@ -197,7 +195,7 @@ async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dic
class InputDatetime(RestoreEntity):
"""Representation of a datetime input."""
- def __init__(self, config: typing.Dict) -> None:
+ def __init__(self, config: dict) -> None:
"""Initialize a select input."""
self._config = config
self.editable = True
@@ -231,7 +229,7 @@ def __init__(self, config: typing.Dict) -> None:
)
@classmethod
- def from_yaml(cls, config: typing.Dict) -> "InputDatetime":
+ def from_yaml(cls, config: dict) -> InputDatetime:
"""Return entity instance initialized from yaml storage."""
input_dt = cls(config)
input_dt.entity_id = f"{DOMAIN}.{config[CONF_ID]}"
@@ -321,7 +319,7 @@ def state(self):
return self._current_datetime.strftime(FMT_TIME)
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attrs = {
ATTR_EDITABLE: self.editable,
@@ -361,7 +359,7 @@ def state_attributes(self):
return attrs
@property
- def unique_id(self) -> typing.Optional[str]:
+ def unique_id(self) -> str | None:
"""Return unique id of the entity."""
return self._config[CONF_ID]
@@ -395,7 +393,7 @@ def async_set_datetime(self, date=None, time=None, datetime=None, timestamp=None
)
self.async_write_ha_state()
- async def async_update_config(self, config: typing.Dict) -> None:
+ async def async_update_config(self, config: dict) -> None:
"""Handle when the config is updated."""
self._config = config
self.async_write_ha_state()
diff --git a/homeassistant/components/input_datetime/reproduce_state.py b/homeassistant/components/input_datetime/reproduce_state.py
index cc906ac50b3886..f996721eabd572 100644
--- a/homeassistant/components/input_datetime/reproduce_state.py
+++ b/homeassistant/components/input_datetime/reproduce_state.py
@@ -1,11 +1,12 @@
"""Reproduce an Input datetime state."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import ATTR_ENTITY_ID
-from homeassistant.core import Context, State
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import Context, HomeAssistant, State
from homeassistant.util import dt as dt_util
from . import ATTR_DATE, ATTR_DATETIME, ATTR_TIME, CONF_HAS_DATE, CONF_HAS_TIME, DOMAIN
@@ -32,11 +33,11 @@ def is_valid_time(string: str) -> bool:
async def _async_reproduce_state(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -77,11 +78,11 @@ async def _async_reproduce_state(
async def async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Input datetime states."""
await asyncio.gather(
diff --git a/homeassistant/components/input_datetime/services.yaml b/homeassistant/components/input_datetime/services.yaml
index bcbadc45aad433..0243ca9f67dfe8 100644
--- a/homeassistant/components/input_datetime/services.yaml
+++ b/homeassistant/components/input_datetime/services.yaml
@@ -1,21 +1,38 @@
set_datetime:
- description: This can be used to dynamically set the date and/or time. Use date/time, datetime or timestamp.
+ name: Set
+ description: This can be used to dynamically set the date and/or time.
+ target:
fields:
- entity_id:
- description: Entity id of the input datetime to set the new value.
- example: input_datetime.test_date_time
date:
+ name: Date
description: The target date the entity should be set to.
example: '"2019-04-20"'
+ selector:
+ text:
time:
+ name: Time
description: The target time the entity should be set to.
example: '"05:04:20"'
+ selector:
+ time:
datetime:
+ name: Date & Time
description: The target date & time the entity should be set to.
example: '"2019-04-20 05:04:20"'
+ selector:
+ text:
timestamp:
- description: The target date & time the entity should be set to as expressed by a UNIX timestamp.
+ name: Timestamp
+ description:
+ The target date & time the entity should be set to as expressed by a
+ UNIX timestamp.
example: 1598027400
+ selector:
+ number:
+ min: 0
+ max: 9223372036854775807
+ mode: box
reload:
+ name: Reload
description: Reload the input_datetime configuration.
diff --git a/homeassistant/components/input_datetime/translations/ko.json b/homeassistant/components/input_datetime/translations/ko.json
index a5984527e151bc..3f6a71e23d787f 100644
--- a/homeassistant/components/input_datetime/translations/ko.json
+++ b/homeassistant/components/input_datetime/translations/ko.json
@@ -1,3 +1,3 @@
{
- "title": "\ub0a0\uc9dc / \uc2dc\uac04\uc785\ub825"
+ "title": "\ub0a0\uc9dc/\uc2dc\uac04 \uc785\ub825"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_datetime/translations/uk.json b/homeassistant/components/input_datetime/translations/uk.json
index bd087e535a5c61..c0aeb11882fe88 100644
--- a/homeassistant/components/input_datetime/translations/uk.json
+++ b/homeassistant/components/input_datetime/translations/uk.json
@@ -1,3 +1,3 @@
{
- "title": "\u0412\u0432\u0435\u0434\u0435\u043d\u043d\u044f \u0434\u0430\u0442\u0438"
+ "title": "Input Datetime"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py
index 1f979cad7a9074..a895326c677b84 100644
--- a/homeassistant/components/input_number/__init__.py
+++ b/homeassistant/components/input_number/__init__.py
@@ -1,6 +1,7 @@
"""Support to set a numeric value from a slider or text box."""
+from __future__ import annotations
+
import logging
-import typing
import voluptuous as vol
@@ -14,14 +15,14 @@
CONF_UNIT_OF_MEASUREMENT,
SERVICE_RELOAD,
)
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import collection
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.helpers.service
from homeassistant.helpers.storage import Store
-from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType
+from homeassistant.helpers.typing import ConfigType, ServiceCallType
_LOGGER = logging.getLogger(__name__)
@@ -111,7 +112,7 @@ def _cv_input_number(cfg):
STORAGE_VERSION = 1
-async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up an input slider."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
id_manager = collection.IDManager()
@@ -119,8 +120,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
yaml_collection = collection.YamlCollection(
logging.getLogger(f"{__name__}.yaml_collection"), id_manager
)
- collection.attach_entity_component_collection(
- component, yaml_collection, InputNumber.from_yaml
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, component, yaml_collection, InputNumber.from_yaml
)
storage_collection = NumberStorageCollection(
@@ -128,8 +129,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
logging.getLogger(f"{__name__}.storage_collection"),
id_manager,
)
- collection.attach_entity_component_collection(
- component, storage_collection, InputNumber
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, component, storage_collection, InputNumber
)
await yaml_collection.async_load(
@@ -141,9 +142,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
).async_setup(hass)
- collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection)
- collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection)
-
async def reload_service_handler(service_call: ServiceCallType) -> None:
"""Reload yaml entities."""
conf = await component.async_prepare_reload(skip_reset=True)
@@ -180,16 +178,16 @@ class NumberStorageCollection(collection.StorageCollection):
CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_number))
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
- async def _process_create_data(self, data: typing.Dict) -> typing.Dict:
+ async def _process_create_data(self, data: dict) -> dict:
"""Validate the config is valid."""
return self.CREATE_SCHEMA(data)
@callback
- def _get_suggested_id(self, info: typing.Dict) -> str:
+ def _get_suggested_id(self, info: dict) -> str:
"""Suggest an ID based on the config."""
return info[CONF_NAME]
- async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict:
+ async def _update_data(self, data: dict, update_data: dict) -> dict:
"""Return a new updated data object."""
update_data = self.UPDATE_SCHEMA(update_data)
return _cv_input_number({**data, **update_data})
@@ -198,14 +196,14 @@ async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dic
class InputNumber(RestoreEntity):
"""Representation of a slider."""
- def __init__(self, config: typing.Dict):
+ def __init__(self, config: dict):
"""Initialize an input number."""
self._config = config
self.editable = True
self._current_value = config.get(CONF_INITIAL)
@classmethod
- def from_yaml(cls, config: typing.Dict) -> "InputNumber":
+ def from_yaml(cls, config: dict) -> InputNumber:
"""Return entity instance initialized from yaml storage."""
input_num = cls(config)
input_num.entity_id = f"{DOMAIN}.{config[CONF_ID]}"
@@ -253,12 +251,12 @@ def unit_of_measurement(self):
return self._config.get(CONF_UNIT_OF_MEASUREMENT)
@property
- def unique_id(self) -> typing.Optional[str]:
+ def unique_id(self) -> str | None:
"""Return unique id of the entity."""
return self._config[CONF_ID]
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_INITIAL: self._config.get(CONF_INITIAL),
@@ -304,7 +302,7 @@ async def async_decrement(self):
"""Decrement value."""
await self.async_set_value(max(self._current_value - self._step, self._minimum))
- async def async_update_config(self, config: typing.Dict) -> None:
+ async def async_update_config(self, config: dict) -> None:
"""Handle when the config is updated."""
self._config = config
# just in case min/max values changed
diff --git a/homeassistant/components/input_number/reproduce_state.py b/homeassistant/components/input_number/reproduce_state.py
index 5a6324c43334eb..a897aec2ba8d33 100644
--- a/homeassistant/components/input_number/reproduce_state.py
+++ b/homeassistant/components/input_number/reproduce_state.py
@@ -1,13 +1,14 @@
"""Reproduce an Input number state."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID
-from homeassistant.core import Context, State
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import Context, HomeAssistant, State
from . import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE
@@ -15,11 +16,11 @@
async def _async_reproduce_state(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -53,11 +54,11 @@ async def _async_reproduce_state(
async def async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Input number states."""
# Reproduce states in parallel.
diff --git a/homeassistant/components/input_number/services.yaml b/homeassistant/components/input_number/services.yaml
index 4d69bf72eda693..7d388238022582 100644
--- a/homeassistant/components/input_number/services.yaml
+++ b/homeassistant/components/input_number/services.yaml
@@ -1,23 +1,30 @@
decrement:
+ name: Decrement
description: Decrement the value of an input number entity by its stepping.
- fields:
- entity_id:
- description: Entity id of the input number that should be decremented.
- example: input_number.threshold
+ target:
+
increment:
+ name: Increment
description: Increment the value of an input number entity by its stepping.
- fields:
- entity_id:
- description: Entity id of the input number that should be incremented.
- example: input_number.threshold
+ target:
+
set_value:
+ name: Set
description: Set the value of an input number entity.
+ target:
fields:
- entity_id:
- description: Entity id of the input number to set the new value.
- example: input_number.threshold
value:
+ name: Value
description: The target value the entity should be set to.
+ required: true
example: 42
+ selector:
+ number:
+ min: 0
+ max: 9223372036854775807
+ step: 0.001
+ mode: box
+
reload:
+ name: Reload
description: Reload the input_number configuration.
diff --git a/homeassistant/components/input_number/translations/ko.json b/homeassistant/components/input_number/translations/ko.json
index 68960733dd8e8b..635e1e26b5ef96 100644
--- a/homeassistant/components/input_number/translations/ko.json
+++ b/homeassistant/components/input_number/translations/ko.json
@@ -1,3 +1,3 @@
{
- "title": "\uc22b\uc790\uc785\ub825"
+ "title": "\uc22b\uc790 \uc785\ub825"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_number/translations/uk.json b/homeassistant/components/input_number/translations/uk.json
index 0e4265d7ca0afb..e09531134cdffa 100644
--- a/homeassistant/components/input_number/translations/uk.json
+++ b/homeassistant/components/input_number/translations/uk.json
@@ -1,3 +1,3 @@
{
- "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043d\u043e\u043c\u0435\u0440"
+ "title": "Input Number"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py
index 6272992f24363f..374254a50523f8 100644
--- a/homeassistant/components/input_select/__init__.py
+++ b/homeassistant/components/input_select/__init__.py
@@ -1,24 +1,26 @@
"""Support to select an option from a list."""
+from __future__ import annotations
+
import logging
-import typing
import voluptuous as vol
from homeassistant.const import (
ATTR_EDITABLE,
+ ATTR_OPTION,
CONF_ICON,
CONF_ID,
CONF_NAME,
SERVICE_RELOAD,
)
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import collection
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.helpers.service
from homeassistant.helpers.storage import Store
-from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType
+from homeassistant.helpers.typing import ConfigType, ServiceCallType
_LOGGER = logging.getLogger(__name__)
@@ -27,7 +29,6 @@
CONF_INITIAL = "initial"
CONF_OPTIONS = "options"
-ATTR_OPTION = "option"
ATTR_OPTIONS = "options"
ATTR_CYCLE = "cycle"
@@ -86,7 +87,7 @@ def _cv_input_select(cfg):
RELOAD_SERVICE_SCHEMA = vol.Schema({})
-async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up an input select."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
id_manager = collection.IDManager()
@@ -94,8 +95,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
yaml_collection = collection.YamlCollection(
logging.getLogger(f"{__name__}.yaml_collection"), id_manager
)
- collection.attach_entity_component_collection(
- component, yaml_collection, InputSelect.from_yaml
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, component, yaml_collection, InputSelect.from_yaml
)
storage_collection = InputSelectStorageCollection(
@@ -103,8 +104,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
logging.getLogger(f"{__name__}.storage_collection"),
id_manager,
)
- collection.attach_entity_component_collection(
- component, storage_collection, InputSelect
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, component, storage_collection, InputSelect
)
await yaml_collection.async_load(
@@ -116,9 +117,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
).async_setup(hass)
- collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection)
- collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection)
-
async def reload_service_handler(service_call: ServiceCallType) -> None:
"""Reload yaml entities."""
conf = await component.async_prepare_reload(skip_reset=True)
@@ -185,16 +183,16 @@ class InputSelectStorageCollection(collection.StorageCollection):
CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_select))
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
- async def _process_create_data(self, data: typing.Dict) -> typing.Dict:
+ async def _process_create_data(self, data: dict) -> dict:
"""Validate the config is valid."""
return self.CREATE_SCHEMA(data)
@callback
- def _get_suggested_id(self, info: typing.Dict) -> str:
+ def _get_suggested_id(self, info: dict) -> str:
"""Suggest an ID based on the config."""
return info[CONF_NAME]
- async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict:
+ async def _update_data(self, data: dict, update_data: dict) -> dict:
"""Return a new updated data object."""
update_data = self.UPDATE_SCHEMA(update_data)
return _cv_input_select({**data, **update_data})
@@ -203,14 +201,14 @@ async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dic
class InputSelect(RestoreEntity):
"""Representation of a select input."""
- def __init__(self, config: typing.Dict):
+ def __init__(self, config: dict):
"""Initialize a select input."""
self._config = config
self.editable = True
self._current_option = config.get(CONF_INITIAL)
@classmethod
- def from_yaml(cls, config: typing.Dict) -> "InputSelect":
+ def from_yaml(cls, config: dict) -> InputSelect:
"""Return entity instance initialized from yaml storage."""
input_select = cls(config)
input_select.entity_id = f"{DOMAIN}.{config[CONF_ID]}"
@@ -245,7 +243,7 @@ def icon(self):
return self._config.get(CONF_ICON)
@property
- def _options(self) -> typing.List[str]:
+ def _options(self) -> list[str]:
"""Return a list of selection options."""
return self._config[CONF_OPTIONS]
@@ -255,12 +253,12 @@ def state(self):
return self._current_option
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_OPTIONS: self._config[ATTR_OPTIONS], ATTR_EDITABLE: self.editable}
@property
- def unique_id(self) -> typing.Optional[str]:
+ def unique_id(self) -> str | None:
"""Return unique id for the entity."""
return self._config[CONF_ID]
@@ -316,7 +314,7 @@ def async_set_options(self, options):
self._config[CONF_OPTIONS] = options
self.async_write_ha_state()
- async def async_update_config(self, config: typing.Dict) -> None:
+ async def async_update_config(self, config: dict) -> None:
"""Handle when the config is updated."""
self._config = config
self.async_write_ha_state()
diff --git a/homeassistant/components/input_select/reproduce_state.py b/homeassistant/components/input_select/reproduce_state.py
index bf6877387409e4..5ea7072e932d8e 100644
--- a/homeassistant/components/input_select/reproduce_state.py
+++ b/homeassistant/components/input_select/reproduce_state.py
@@ -1,12 +1,13 @@
"""Reproduce an Input select state."""
+from __future__ import annotations
+
import asyncio
import logging
from types import MappingProxyType
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import ATTR_ENTITY_ID
-from homeassistant.core import Context, State
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import Context, HomeAssistant, State
from . import (
ATTR_OPTION,
@@ -22,11 +23,11 @@
async def _async_reproduce_state(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -68,11 +69,11 @@ async def _async_reproduce_state(
async def async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Input select states."""
# Reproduce states in parallel.
diff --git a/homeassistant/components/input_select/services.yaml b/homeassistant/components/input_select/services.yaml
index 0eddb158d34aa0..f8fbe158aab5d4 100644
--- a/homeassistant/components/input_select/services.yaml
+++ b/homeassistant/components/input_select/services.yaml
@@ -1,50 +1,65 @@
select_next:
+ name: Next
description: Select the next options of an input select entity.
+ target:
fields:
- entity_id:
- description: Entity id of the input select to select the next value for.
- example: input_select.my_select
cycle:
- description: If the option should cycle from the last to the first (defaults to true).
+ name: Cycle
+ description: If the option should cycle from the last to the first.
+ default: true
example: true
+ selector:
+ boolean:
+
select_option:
+ name: Select
description: Select an option of an input select entity.
+ target:
fields:
- entity_id:
- description: Entity id of the input select to select the value.
- example: input_select.my_select
option:
+ name: Option
description: Option to be selected.
+ required: true
example: '"Item A"'
+ selector:
+ text:
+
select_previous:
+ name: Previous
description: Select the previous options of an input select entity.
+ target:
fields:
- entity_id:
- description: Entity id of the input select to select the previous value for.
- example: input_select.my_select
cycle:
- description: If the option should cycle from the first to the last (defaults to true).
+ name: Cycle
+ description: If the option should cycle from the first to the last.
+ default: true
example: true
+ selector:
+ boolean:
+
select_first:
+ name: First
description: Select the first option of an input select entity.
- fields:
- entity_id:
- description: Entity id of the input select to select the first value for.
- example: input_select.my_select
+ target:
+
select_last:
+ name: Last
description: Select the last option of an input select entity.
- fields:
- entity_id:
- description: Entity id of the input select to select the last value for.
- example: input_select.my_select
+ target:
+
set_options:
+ name: Set options
description: Set the options of an input select entity.
+ target:
fields:
- entity_id:
- description: Entity id of the input select to set the new options for.
- example: input_select.my_select
options:
+ name: Options
description: Options for the input select entity.
+ required: true
example: '["Item A", "Item B", "Item C"]'
+ selector:
+ object:
+
reload:
+ name: Reload
description: Reload the input_select configuration.
diff --git a/homeassistant/components/input_select/translations/ko.json b/homeassistant/components/input_select/translations/ko.json
index 5620635d90325c..59b3f59f143b57 100644
--- a/homeassistant/components/input_select/translations/ko.json
+++ b/homeassistant/components/input_select/translations/ko.json
@@ -1,3 +1,3 @@
{
- "title": "\uc120\ud0dd\uc785\ub825"
+ "title": "\uc120\ud0dd \uc785\ub825"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_select/translations/uk.json b/homeassistant/components/input_select/translations/uk.json
index ace44f8d7a79ac..b33e64fbf48159 100644
--- a/homeassistant/components/input_select/translations/uk.json
+++ b/homeassistant/components/input_select/translations/uk.json
@@ -1,3 +1,3 @@
{
- "title": "\u0412\u0438\u0431\u0440\u0430\u0442\u0438"
+ "title": "Input Select"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py
index c512bc221db1b9..2f9f6cb47ba847 100644
--- a/homeassistant/components/input_text/__init__.py
+++ b/homeassistant/components/input_text/__init__.py
@@ -1,6 +1,7 @@
"""Support to enter a value into a text box."""
+from __future__ import annotations
+
import logging
-import typing
import voluptuous as vol
@@ -14,14 +15,14 @@
CONF_UNIT_OF_MEASUREMENT,
SERVICE_RELOAD,
)
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import collection
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.helpers.service
from homeassistant.helpers.storage import Store
-from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType
+from homeassistant.helpers.typing import ConfigType, ServiceCallType
_LOGGER = logging.getLogger(__name__)
@@ -111,7 +112,7 @@ def _cv_input_text(cfg):
RELOAD_SERVICE_SCHEMA = vol.Schema({})
-async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up an input text."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
id_manager = collection.IDManager()
@@ -119,8 +120,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
yaml_collection = collection.YamlCollection(
logging.getLogger(f"{__name__}.yaml_collection"), id_manager
)
- collection.attach_entity_component_collection(
- component, yaml_collection, InputText.from_yaml
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, component, yaml_collection, InputText.from_yaml
)
storage_collection = InputTextStorageCollection(
@@ -128,8 +129,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
logging.getLogger(f"{__name__}.storage_collection"),
id_manager,
)
- collection.attach_entity_component_collection(
- component, storage_collection, InputText
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, component, storage_collection, InputText
)
await yaml_collection.async_load(
@@ -141,9 +142,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
).async_setup(hass)
- collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection)
- collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection)
-
async def reload_service_handler(service_call: ServiceCallType) -> None:
"""Reload yaml entities."""
conf = await component.async_prepare_reload(skip_reset=True)
@@ -174,16 +172,16 @@ class InputTextStorageCollection(collection.StorageCollection):
CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_text))
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
- async def _process_create_data(self, data: typing.Dict) -> typing.Dict:
+ async def _process_create_data(self, data: dict) -> dict:
"""Validate the config is valid."""
return self.CREATE_SCHEMA(data)
@callback
- def _get_suggested_id(self, info: typing.Dict) -> str:
+ def _get_suggested_id(self, info: dict) -> str:
"""Suggest an ID based on the config."""
return info[CONF_NAME]
- async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict:
+ async def _update_data(self, data: dict, update_data: dict) -> dict:
"""Return a new updated data object."""
update_data = self.UPDATE_SCHEMA(update_data)
return _cv_input_text({**data, **update_data})
@@ -192,14 +190,14 @@ async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dic
class InputText(RestoreEntity):
"""Represent a text box."""
- def __init__(self, config: typing.Dict):
+ def __init__(self, config: dict):
"""Initialize a text input."""
self._config = config
self.editable = True
self._current_value = config.get(CONF_INITIAL)
@classmethod
- def from_yaml(cls, config: typing.Dict) -> "InputText":
+ def from_yaml(cls, config: dict) -> InputText:
"""Return entity instance initialized from yaml storage."""
input_text = cls(config)
input_text.entity_id = f"{DOMAIN}.{config[CONF_ID]}"
@@ -242,12 +240,12 @@ def unit_of_measurement(self):
return self._config.get(CONF_UNIT_OF_MEASUREMENT)
@property
- def unique_id(self) -> typing.Optional[str]:
+ def unique_id(self) -> str | None:
"""Return unique id for the entity."""
return self._config[CONF_ID]
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_EDITABLE: self.editable,
@@ -283,7 +281,7 @@ async def async_set_value(self, value):
self._current_value = value
self.async_write_ha_state()
- async def async_update_config(self, config: typing.Dict) -> None:
+ async def async_update_config(self, config: dict) -> None:
"""Handle when the config is updated."""
self._config = config
self.async_write_ha_state()
diff --git a/homeassistant/components/input_text/reproduce_state.py b/homeassistant/components/input_text/reproduce_state.py
index abd28195d8d629..ce1b7c12c46111 100644
--- a/homeassistant/components/input_text/reproduce_state.py
+++ b/homeassistant/components/input_text/reproduce_state.py
@@ -1,11 +1,12 @@
"""Reproduce an Input text state."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import ATTR_ENTITY_ID
-from homeassistant.core import Context, State
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import Context, HomeAssistant, State
from . import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE
@@ -13,11 +14,11 @@
async def _async_reproduce_state(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -41,11 +42,11 @@ async def _async_reproduce_state(
async def async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Input text states."""
# Reproduce states in parallel.
diff --git a/homeassistant/components/input_text/services.yaml b/homeassistant/components/input_text/services.yaml
index 0f74cd8940ea9c..5983683ec6dc0c 100644
--- a/homeassistant/components/input_text/services.yaml
+++ b/homeassistant/components/input_text/services.yaml
@@ -1,11 +1,16 @@
set_value:
+ name: Set
description: Set the value of an input text entity.
+ target:
fields:
- entity_id:
- description: Entity id of the input text to set the new value.
- example: input_text.text1
value:
+ name: Value
description: The target value the entity should be set to.
+ required: true
example: This is an example text
+ selector:
+ text:
+
reload:
+ name: Reload
description: Reload the input_text configuration.
diff --git a/homeassistant/components/input_text/translations/ko.json b/homeassistant/components/input_text/translations/ko.json
index 6f8e3a04f2a854..43e792dd2ad17a 100644
--- a/homeassistant/components/input_text/translations/ko.json
+++ b/homeassistant/components/input_text/translations/ko.json
@@ -1,3 +1,3 @@
{
- "title": "\ubb38\uc790\uc785\ub825"
+ "title": "\ubb38\uc790 \uc785\ub825"
}
\ No newline at end of file
diff --git a/homeassistant/components/input_text/translations/uk.json b/homeassistant/components/input_text/translations/uk.json
index a80f4325203ed6..84bddfe3e07e1d 100644
--- a/homeassistant/components/input_text/translations/uk.json
+++ b/homeassistant/components/input_text/translations/uk.json
@@ -1,3 +1,3 @@
{
- "title": "\u0412\u0432\u0435\u0434\u0435\u043d\u043d\u044f \u0442\u0435\u043a\u0441\u0442\u0443"
+ "title": "Input Text"
}
\ No newline at end of file
diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py
index 0188671e07f888..509878f9613259 100644
--- a/homeassistant/components/insteon/__init__.py
+++ b/homeassistant/components/insteon/__init__.py
@@ -1,5 +1,6 @@
"""Support for INSTEON Modems (PLM and Hub)."""
import asyncio
+from contextlib import suppress
import logging
from pyinsteon import async_close, async_connect, devices
@@ -17,7 +18,7 @@
CONF_UNITCODE,
CONF_X10,
DOMAIN,
- INSTEON_COMPONENTS,
+ INSTEON_PLATFORMS,
ON_OFF_EVENTS,
)
from .schemas import convert_yaml_to_config_flow
@@ -37,10 +38,8 @@ async def async_get_device_config(hass, config_entry):
# Make a copy of addresses due to edge case where the list of devices could change during status update
# Cannot be done concurrently due to issues with the underlying protocol.
for address in list(devices):
- try:
+ with suppress(AttributeError):
await devices[address].async_status()
- except AttributeError:
- pass
await devices.async_load(id_devices=1)
for addr in devices:
@@ -138,9 +137,9 @@ async def async_setup_entry(hass, entry):
)
device = devices.add_x10_device(housecode, unitcode, x10_type, steps)
- for component in INSTEON_COMPONENTS:
+ for platform in INSTEON_PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
for address in devices:
diff --git a/homeassistant/components/insteon/binary_sensor.py b/homeassistant/components/insteon/binary_sensor.py
index ad87c69bd0feef..69a4a5f5280ffd 100644
--- a/homeassistant/components/insteon/binary_sensor.py
+++ b/homeassistant/components/insteon/binary_sensor.py
@@ -27,6 +27,7 @@
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorEntity,
)
+from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import SIGNAL_ADD_ENTITIES
@@ -51,7 +52,8 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Insteon binary sensors from a config entry."""
- def add_entities(discovery_info=None):
+ @callback
+ def async_add_insteon_binary_sensor_entities(discovery_info=None):
"""Add the Insteon entities for the platform."""
async_add_insteon_entities(
hass,
@@ -62,8 +64,8 @@ def add_entities(discovery_info=None):
)
signal = f"{SIGNAL_ADD_ENTITIES}_{BINARY_SENSOR_DOMAIN}"
- async_dispatcher_connect(hass, signal, add_entities)
- add_entities()
+ async_dispatcher_connect(hass, signal, async_add_insteon_binary_sensor_entities)
+ async_add_insteon_binary_sensor_entities()
class InsteonBinarySensorEntity(InsteonEntity, BinarySensorEntity):
diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py
index 7d4d9543c3fe6c..7e034311a82936 100644
--- a/homeassistant/components/insteon/climate.py
+++ b/homeassistant/components/insteon/climate.py
@@ -1,5 +1,5 @@
"""Support for Insteon thermostat."""
-from typing import List, Optional
+from __future__ import annotations
from pyinsteon.constants import ThermostatMode
from pyinsteon.operating_flag import CELSIUS
@@ -25,6 +25,7 @@
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import SIGNAL_ADD_ENTITIES
@@ -64,7 +65,8 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Insteon climate entities from a config entry."""
- def add_entities(discovery_info=None):
+ @callback
+ def async_add_insteon_climate_entities(discovery_info=None):
"""Add the Insteon entities for the platform."""
async_add_insteon_entities(
hass,
@@ -75,8 +77,8 @@ def add_entities(discovery_info=None):
)
signal = f"{SIGNAL_ADD_ENTITIES}_{CLIMATE_DOMAIN}"
- async_dispatcher_connect(hass, signal, add_entities)
- add_entities()
+ async_dispatcher_connect(hass, signal, async_add_insteon_climate_entities)
+ async_add_insteon_climate_entities()
class InsteonClimateEntity(InsteonEntity, ClimateEntity):
@@ -95,7 +97,7 @@ def temperature_unit(self) -> str:
return TEMP_FAHRENHEIT
@property
- def current_humidity(self) -> Optional[int]:
+ def current_humidity(self) -> int | None:
"""Return the current humidity."""
return self._insteon_device.groups[HUMIDITY].value
@@ -105,17 +107,17 @@ def hvac_mode(self) -> str:
return HVAC_MODES[self._insteon_device.groups[SYSTEM_MODE].value]
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes."""
return list(HVAC_MODES.values())
@property
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self._insteon_device.groups[TEMPERATURE].value
@property
- def target_temperature(self) -> Optional[float]:
+ def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
if self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.HEAT:
return self._insteon_device.groups[HEAT_SET_POINT].value
@@ -124,31 +126,31 @@ def target_temperature(self) -> Optional[float]:
return None
@property
- def target_temperature_high(self) -> Optional[float]:
+ def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach."""
if self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.AUTO:
return self._insteon_device.groups[COOL_SET_POINT].value
return None
@property
- def target_temperature_low(self) -> Optional[float]:
+ def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach."""
if self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.AUTO:
return self._insteon_device.groups[HEAT_SET_POINT].value
return None
@property
- def fan_mode(self) -> Optional[str]:
+ def fan_mode(self) -> str | None:
"""Return the fan setting."""
return FAN_MODES[self._insteon_device.groups[FAN_MODE].value]
@property
- def fan_modes(self) -> Optional[List[str]]:
+ def fan_modes(self) -> list[str] | None:
"""Return the list of available fan modes."""
return list(FAN_MODES.values())
@property
- def target_humidity(self) -> Optional[int]:
+ def target_humidity(self) -> int | None:
"""Return the humidity we try to reach."""
high = self._insteon_device.groups[HUMIDITY_HIGH].value
low = self._insteon_device.groups[HUMIDITY_LOW].value
@@ -161,7 +163,7 @@ def min_humidity(self) -> int:
return 1
@property
- def hvac_action(self) -> Optional[str]:
+ def hvac_action(self) -> str | None:
"""Return the current running hvac operation if supported.
Need to be one of CURRENT_HVAC_*.
@@ -175,9 +177,9 @@ def hvac_action(self) -> Optional[str]:
return CURRENT_HVAC_IDLE
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Provide attributes for display on device card."""
- attr = super().device_state_attributes
+ attr = super().extra_state_attributes
humidifier = "off"
if self._insteon_device.groups[DEHUMIDIFYING].value:
humidifier = "dehumidifying"
diff --git a/homeassistant/components/insteon/config_flow.py b/homeassistant/components/insteon/config_flow.py
index d8e17cfa03fe2d..d9081c5b45e5ee 100644
--- a/homeassistant/components/insteon/config_flow.py
+++ b/homeassistant/components/insteon/config_flow.py
@@ -16,7 +16,6 @@
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
-# pylint: disable=unused-import
from .const import (
CONF_HOUSECODE,
CONF_HUB_VERSION,
@@ -65,10 +64,10 @@ async def _async_connect(**kwargs):
"""Connect to the Insteon modem."""
try:
await async_connect(**kwargs)
- _LOGGER.info("Connected to Insteon modem.")
+ _LOGGER.info("Connected to Insteon modem")
return True
except ConnectionError:
- _LOGGER.error("Could not connect to Insteon modem.")
+ _LOGGER.error("Could not connect to Insteon modem")
return False
diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py
index 07717dade9bca8..a40a0b0d4b022e 100644
--- a/homeassistant/components/insteon/const.py
+++ b/homeassistant/components/insteon/const.py
@@ -34,7 +34,7 @@
DOMAIN = "insteon"
-INSTEON_COMPONENTS = [
+INSTEON_PLATFORMS = [
"binary_sensor",
"climate",
"cover",
diff --git a/homeassistant/components/insteon/cover.py b/homeassistant/components/insteon/cover.py
index 498d194667c3f7..fd20637b174a8b 100644
--- a/homeassistant/components/insteon/cover.py
+++ b/homeassistant/components/insteon/cover.py
@@ -9,6 +9,7 @@
SUPPORT_SET_POSITION,
CoverEntity,
)
+from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import SIGNAL_ADD_ENTITIES
@@ -21,15 +22,16 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Insteon covers from a config entry."""
- def add_entities(discovery_info=None):
+ @callback
+ def async_add_insteon_cover_entities(discovery_info=None):
"""Add the Insteon entities for the platform."""
async_add_insteon_entities(
hass, COVER_DOMAIN, InsteonCoverEntity, async_add_entities, discovery_info
)
signal = f"{SIGNAL_ADD_ENTITIES}_{COVER_DOMAIN}"
- async_dispatcher_connect(hass, signal, add_entities)
- add_entities()
+ async_dispatcher_connect(hass, signal, async_add_insteon_cover_entities)
+ async_add_insteon_cover_entities()
class InsteonCoverEntity(InsteonEntity, CoverEntity):
diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py
index f9d1c381f496ef..00ada3e9a588ba 100644
--- a/homeassistant/components/insteon/fan.py
+++ b/homeassistant/components/insteon/fan.py
@@ -1,75 +1,60 @@
"""Support for INSTEON fans via PowerLinc Modem."""
-from pyinsteon.constants import FanSpeed
+import math
from homeassistant.components.fan import (
DOMAIN as FAN_DOMAIN,
- SPEED_HIGH,
- SPEED_LOW,
- SPEED_MEDIUM,
- SPEED_OFF,
SUPPORT_SET_SPEED,
FanEntity,
)
+from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.util.percentage import (
+ percentage_to_ranged_value,
+ ranged_value_to_percentage,
+)
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
-FAN_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
-SPEED_TO_VALUE = {
- SPEED_OFF: FanSpeed.OFF,
- SPEED_LOW: FanSpeed.LOW,
- SPEED_MEDIUM: FanSpeed.MEDIUM,
- SPEED_HIGH: FanSpeed.HIGH,
-}
+SPEED_RANGE = (1, 255) # off is not included
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Insteon fans from a config entry."""
- def add_entities(discovery_info=None):
+ @callback
+ def async_add_insteon_fan_entities(discovery_info=None):
"""Add the Insteon entities for the platform."""
async_add_insteon_entities(
hass, FAN_DOMAIN, InsteonFanEntity, async_add_entities, discovery_info
)
signal = f"{SIGNAL_ADD_ENTITIES}_{FAN_DOMAIN}"
- async_dispatcher_connect(hass, signal, add_entities)
- add_entities()
+ async_dispatcher_connect(hass, signal, async_add_insteon_fan_entities)
+ async_add_insteon_fan_entities()
class InsteonFanEntity(InsteonEntity, FanEntity):
"""An INSTEON fan entity."""
@property
- def speed(self) -> str:
- """Return the current speed."""
- if self._insteon_device_group.value == FanSpeed.HIGH:
- return SPEED_HIGH
- if self._insteon_device_group.value == FanSpeed.MEDIUM:
- return SPEED_MEDIUM
- if self._insteon_device_group.value == FanSpeed.LOW:
- return SPEED_LOW
- return SPEED_OFF
-
- @property
- def speed_list(self) -> list:
- """Get the list of available speeds."""
- return FAN_SPEEDS
+ def percentage(self) -> int:
+ """Return the current speed percentage."""
+ if self._insteon_device_group.value is None:
+ return None
+ return ranged_value_to_percentage(SPEED_RANGE, self._insteon_device_group.value)
@property
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_SET_SPEED
- #
- # The fan entity model has changed to use percentages and preset_modes
- # instead of speeds.
- #
- # Please review
- # https://developers.home-assistant.io/docs/core/entity/fan/
- #
+ @property
+ def speed_count(self) -> int:
+ """Flag supported features."""
+ return 3
+
async def async_turn_on(
self,
speed: str = None,
@@ -78,18 +63,16 @@ async def async_turn_on(
**kwargs,
) -> None:
"""Turn on the fan."""
- if speed is None:
- speed = SPEED_MEDIUM
- await self.async_set_speed(speed)
+ await self.async_set_percentage(percentage or 67)
async def async_turn_off(self, **kwargs) -> None:
"""Turn off the fan."""
await self._insteon_device.async_fan_off()
- async def async_set_speed(self, speed: str) -> None:
- """Set the speed of the fan."""
- fan_speed = SPEED_TO_VALUE[speed]
- if fan_speed == FanSpeed.OFF:
- await self._insteon_device.async_fan_off()
- else:
- await self._insteon_device.async_fan_on(on_level=fan_speed)
+ async def async_set_percentage(self, percentage: int) -> None:
+ """Set the speed percentage of the fan."""
+ if percentage == 0:
+ await self.async_turn_off()
+ return
+ on_level = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
+ await self._insteon_device.async_on(group=2, on_level=on_level)
diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py
index e2b9dd39f34fa8..3f83440d69006d 100644
--- a/homeassistant/components/insteon/insteon_entity.py
+++ b/homeassistant/components/insteon/insteon_entity.py
@@ -1,4 +1,5 @@
"""Insteon base entity."""
+import functools
import logging
from pyinsteon import devices
@@ -74,7 +75,7 @@ def name(self):
return f"{description} {self._insteon_device.address}{extension}"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Provide attributes for display on device card."""
return {"insteon_address": self.address, "insteon_group": self.group}
@@ -122,7 +123,11 @@ async def async_added_to_hass(self):
)
remove_signal = f"{self._insteon_device.address.id}_{SIGNAL_REMOVE_ENTITY}"
self.async_on_remove(
- async_dispatcher_connect(self.hass, remove_signal, self.async_remove)
+ async_dispatcher_connect(
+ self.hass,
+ remove_signal,
+ functools.partial(self.async_remove, force_remove=True),
+ )
)
async def async_will_remove_from_hass(self):
diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py
index f49dafed2fe7b1..206aa078dc3f0f 100644
--- a/homeassistant/components/insteon/light.py
+++ b/homeassistant/components/insteon/light.py
@@ -6,6 +6,7 @@
SUPPORT_BRIGHTNESS,
LightEntity,
)
+from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import SIGNAL_ADD_ENTITIES
@@ -18,15 +19,16 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Insteon lights from a config entry."""
- def add_entities(discovery_info=None):
+ @callback
+ def async_add_insteon_light_entities(discovery_info=None):
"""Add the Insteon entities for the platform."""
async_add_insteon_entities(
hass, LIGHT_DOMAIN, InsteonDimmerEntity, async_add_entities, discovery_info
)
signal = f"{SIGNAL_ADD_ENTITIES}_{LIGHT_DOMAIN}"
- async_dispatcher_connect(hass, signal, add_entities)
- add_entities()
+ async_dispatcher_connect(hass, signal, async_add_insteon_light_entities)
+ async_add_insteon_light_entities()
class InsteonDimmerEntity(InsteonEntity, LightEntity):
diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py
index adc7e945eba2f5..c43df24b4cb03e 100644
--- a/homeassistant/components/insteon/schemas.py
+++ b/homeassistant/components/insteon/schemas.py
@@ -1,6 +1,7 @@
"""Schemas used by insteon component."""
+from __future__ import annotations
+
from binascii import Error as HexError, unhexlify
-from typing import Dict
from pyinsteon.address import Address
from pyinsteon.constants import HC_LOOKUP
@@ -51,7 +52,7 @@
)
-def set_default_port(schema: Dict) -> Dict:
+def set_default_port(schema: dict) -> dict:
"""Set the default port based on the Hub version."""
# If the ip_port is found do nothing
# If it is not found the set the default
@@ -187,47 +188,47 @@ def add_device_override(config_data, new_override):
except ValueError as err:
raise ValueError("Incorrect values") from err
- overrides = config_data.get(CONF_OVERRIDE, [])
- curr_override = {}
+ overrides = []
- # If this address has an override defined, remove it
- for override in overrides:
- if override[CONF_ADDRESS] == address:
- curr_override = override
- break
- if curr_override:
- overrides.remove(curr_override)
+ for override in config_data.get(CONF_OVERRIDE, []):
+ if override[CONF_ADDRESS] != address:
+ overrides.append(override)
+ curr_override = {}
curr_override[CONF_ADDRESS] = address
curr_override[CONF_CAT] = cat
curr_override[CONF_SUBCAT] = subcat
overrides.append(curr_override)
- config_data[CONF_OVERRIDE] = overrides
- return config_data
+
+ new_config = {}
+ if config_data.get(CONF_X10):
+ new_config[CONF_X10] = config_data[CONF_X10]
+ new_config[CONF_OVERRIDE] = overrides
+ return new_config
def add_x10_device(config_data, new_x10):
"""Add a new X10 device to X10 device list."""
- curr_device = {}
- x10_devices = config_data.get(CONF_X10, [])
- for x10_device in x10_devices:
+ x10_devices = []
+ for x10_device in config_data.get(CONF_X10, []):
if (
- x10_device[CONF_HOUSECODE] == new_x10[CONF_HOUSECODE]
- and x10_device[CONF_UNITCODE] == new_x10[CONF_UNITCODE]
+ x10_device[CONF_HOUSECODE] != new_x10[CONF_HOUSECODE]
+ or x10_device[CONF_UNITCODE] != new_x10[CONF_UNITCODE]
):
- curr_device = x10_device
- break
-
- if curr_device:
- x10_devices.remove(curr_device)
+ x10_devices.append(x10_device)
+ curr_device = {}
curr_device[CONF_HOUSECODE] = new_x10[CONF_HOUSECODE]
curr_device[CONF_UNITCODE] = new_x10[CONF_UNITCODE]
curr_device[CONF_PLATFORM] = new_x10[CONF_PLATFORM]
curr_device[CONF_DIM_STEPS] = new_x10[CONF_DIM_STEPS]
x10_devices.append(curr_device)
- config_data[CONF_X10] = x10_devices
- return config_data
+
+ new_config = {}
+ if config_data.get(CONF_OVERRIDE):
+ new_config[CONF_OVERRIDE] = config_data[CONF_OVERRIDE]
+ new_config[CONF_X10] = x10_devices
+ return new_config
def build_device_override_schema(
diff --git a/homeassistant/components/insteon/switch.py b/homeassistant/components/insteon/switch.py
index 43430ceb7a062c..0a1a0253b1df82 100644
--- a/homeassistant/components/insteon/switch.py
+++ b/homeassistant/components/insteon/switch.py
@@ -1,5 +1,6 @@
"""Support for INSTEON dimmers via PowerLinc Modem."""
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity
+from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import SIGNAL_ADD_ENTITIES
@@ -10,15 +11,16 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Insteon switches from a config entry."""
- def add_entities(discovery_info=None):
+ @callback
+ def async_add_insteon_switch_entities(discovery_info=None):
"""Add the Insteon entities for the platform."""
async_add_insteon_entities(
hass, SWITCH_DOMAIN, InsteonSwitchEntity, async_add_entities, discovery_info
)
signal = f"{SIGNAL_ADD_ENTITIES}_{SWITCH_DOMAIN}"
- async_dispatcher_connect(hass, signal, add_entities)
- add_entities()
+ async_dispatcher_connect(hass, signal, async_add_insteon_switch_entities)
+ async_add_insteon_switch_entities()
class InsteonSwitchEntity(InsteonEntity, SwitchEntity):
diff --git a/homeassistant/components/insteon/translations/de.json b/homeassistant/components/insteon/translations/de.json
index dfa4f3f7567c47..6bbc4d5474f063 100644
--- a/homeassistant/components/insteon/translations/de.json
+++ b/homeassistant/components/insteon/translations/de.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "cannot_connect": "Verbindung fehlgeschlagen"
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"error": {
"cannot_connect": "Verbindung fehlgeschlagen"
@@ -22,6 +23,9 @@
}
},
"plm": {
+ "data": {
+ "device": "USB-Ger\u00e4te-Pfad"
+ },
"title": "Insteon PLM"
},
"user": {
diff --git a/homeassistant/components/insteon/translations/et.json b/homeassistant/components/insteon/translations/et.json
index 5fee63e1219b93..69368300c7e969 100644
--- a/homeassistant/components/insteon/translations/et.json
+++ b/homeassistant/components/insteon/translations/et.json
@@ -76,7 +76,7 @@
"port": "",
"username": "Kasutajanimi"
},
- "description": "Muutda Insteon Hubi \u00fchenduse teavet. P\u00e4rast selle muudatuse tegemist pead Home Assistanti taask\u00e4ivitama. See ei muuda jaoturi enda konfiguratsiooni. Hubis muudatuste tegemiseks kasutage rakendust Hub.",
+ "description": "Muutda Insteon Hubi \u00fchenduse teavet. P\u00e4rast selle muudatuse tegemist pead Home Assistanti taask\u00e4ivitama. See ei muuda jaoturi enda konfiguratsiooni. Hubis muudatuste tegemiseks kasuta rakendust Hub.",
"title": ""
},
"init": {
diff --git a/homeassistant/components/insteon/translations/he.json b/homeassistant/components/insteon/translations/he.json
new file mode 100644
index 00000000000000..8aabed0bfce040
--- /dev/null
+++ b/homeassistant/components/insteon/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "options": {
+ "step": {
+ "change_hub_config": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/insteon/translations/hu.json b/homeassistant/components/insteon/translations/hu.json
new file mode 100644
index 00000000000000..06033fb1321c24
--- /dev/null
+++ b/homeassistant/components/insteon/translations/hu.json
@@ -0,0 +1,72 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "select_single": "V\u00e1lassz egy lehet\u0151s\u00e9get"
+ },
+ "step": {
+ "hubv1": {
+ "data": {
+ "host": "IP c\u00edm",
+ "port": "Port"
+ }
+ },
+ "hubv2": {
+ "data": {
+ "host": "IP c\u00edm",
+ "password": "Jelsz\u00f3",
+ "port": "Port",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "title": "Insteon Hub 2. verzi\u00f3"
+ },
+ "plm": {
+ "data": {
+ "device": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat"
+ }
+ },
+ "user": {
+ "data": {
+ "modem_type": "Modem t\u00edpusa."
+ },
+ "description": "V\u00e1laszd ki az Insteon modem t\u00edpus\u00e1t.",
+ "title": "Insteon"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "step": {
+ "add_override": {
+ "title": "Insteon"
+ },
+ "add_x10": {
+ "title": "Insteon"
+ },
+ "change_hub_config": {
+ "data": {
+ "host": "IP c\u00edm",
+ "password": "Jelsz\u00f3",
+ "port": "Port",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "title": "Insteon"
+ },
+ "init": {
+ "title": "Insteon"
+ },
+ "remove_override": {
+ "title": "Insteon"
+ },
+ "remove_x10": {
+ "title": "Insteon"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/insteon/translations/id.json b/homeassistant/components/insteon/translations/id.json
new file mode 100644
index 00000000000000..efae528415015d
--- /dev/null
+++ b/homeassistant/components/insteon/translations/id.json
@@ -0,0 +1,109 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Gagal terhubung",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "select_single": "Pilih satu opsi."
+ },
+ "step": {
+ "hubv1": {
+ "data": {
+ "host": "Alamat IP",
+ "port": "Port"
+ },
+ "description": "Konfigurasikan Insteon Hub Versio 1 (pra-2014).",
+ "title": "Insteon Hub Versi 1"
+ },
+ "hubv2": {
+ "data": {
+ "host": "Alamat IP",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "username": "Nama Pengguna"
+ },
+ "description": "Konfigurasikan Insteon Hub Versi 2.",
+ "title": "Insteon Hub Versi 2"
+ },
+ "plm": {
+ "data": {
+ "device": "Jalur Perangkat USB"
+ },
+ "description": "Konfigurasikan Insteon PowerLink Modem (PLM).",
+ "title": "Insteon PLM"
+ },
+ "user": {
+ "data": {
+ "modem_type": "Jenis modem."
+ },
+ "description": "Pilih jenis modem Insteon.",
+ "title": "Insteon"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "input_error": "Entri tidak valid, periksa nilai Anda.",
+ "select_single": "Pilih satu opsi."
+ },
+ "step": {
+ "add_override": {
+ "data": {
+ "address": "Alamat perangkat (yaitu 1a2b3c)",
+ "cat": "Kategori perangkat (mis. 0x10)",
+ "subcat": "Subkategori perangkat (mis. 0x0a)"
+ },
+ "description": "Tambahkan penimpaan nilai perangkat.",
+ "title": "Insteon"
+ },
+ "add_x10": {
+ "data": {
+ "housecode": "Kode rumah (a - p)",
+ "platform": "Platform",
+ "steps": "Langkah peredup (hanya untuk perangkat ringan, nilai bawaan adalah 22)",
+ "unitcode": "Unitcode (1 - 16)"
+ },
+ "description": "Ubah kata sandi Insteon Hub.",
+ "title": "Insteon"
+ },
+ "change_hub_config": {
+ "data": {
+ "host": "Alamat IP",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "username": "Nama Pengguna"
+ },
+ "description": "Ubah informasi koneksi Insteon Hub. Anda harus memulai ulang Home Assistant setelah melakukan perubahan ini. Ini tidak mengubah konfigurasi Hub itu sendiri. Untuk mengubah konfigurasi di Hub, gunakan aplikasi Hub.",
+ "title": "Insteon"
+ },
+ "init": {
+ "data": {
+ "add_override": "Tambahkan penimpaan nilai perangkat.",
+ "add_x10": "Tambahkan perangkat X10.",
+ "change_hub_config": "Ubah konfigurasi Hub.",
+ "remove_override": "Hapus penimpaan nilai perangkat.",
+ "remove_x10": "Hapus perangkat X10."
+ },
+ "description": "Pilih opsi untuk dikonfigurasi.",
+ "title": "Insteon"
+ },
+ "remove_override": {
+ "data": {
+ "address": "Pilih alamat perangkat untuk dihapus"
+ },
+ "description": "Hapus penimpaan nilai perangkat",
+ "title": "Insteon"
+ },
+ "remove_x10": {
+ "data": {
+ "address": "Pilih alamat perangkat untuk dihapus"
+ },
+ "description": "Hapus perangkat X10",
+ "title": "Insteon"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/insteon/translations/ko.json b/homeassistant/components/insteon/translations/ko.json
index 76ef956672537f..559e3351932b08 100644
--- a/homeassistant/components/insteon/translations/ko.json
+++ b/homeassistant/components/insteon/translations/ko.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
- "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub428. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4."
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"error": {
- "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
- "select_single": "\ud558\ub098\uc758 \uc635\uc158\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624."
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "select_single": "\ud558\ub098\uc758 \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694."
},
"step": {
"hubv1": {
@@ -14,15 +14,15 @@
"host": "IP \uc8fc\uc18c",
"port": "\ud3ec\ud2b8"
},
- "description": "Insteon Hub \ubc84\uc804 1 (2014 \ub144 \uc774\uc804)\uc744 \uad6c\uc131\ud569\ub2c8\ub2e4.",
+ "description": "Insteon Hub \ubc84\uc804 1\uc744 \uad6c\uc131\ud569\ub2c8\ub2e4. (2014\ub144 \uc774\uc804)",
"title": "Insteon Hub \ubc84\uc804 1"
},
"hubv2": {
"data": {
"host": "IP \uc8fc\uc18c",
- "password": "\uc554\ud638",
+ "password": "\ube44\ubc00\ubc88\ud638",
"port": "\ud3ec\ud2b8",
- "username": "\uc0ac\uc6a9\uc790\uba85"
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
},
"description": "Insteon Hub \ubc84\uc804 2\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.",
"title": "Insteon Hub \ubc84\uc804 2"
@@ -30,56 +30,79 @@
"plm": {
"data": {
"device": "USB \uc7a5\uce58 \uacbd\ub85c"
- }
+ },
+ "description": "Insteon PowerLinc Modem (PLM)\uc744 \uad6c\uc131\ud569\ub2c8\ub2e4.",
+ "title": "Insteon PLM"
},
"user": {
"data": {
"modem_type": "\ubaa8\ub380 \uc720\ud615."
},
- "description": "Insteon \ubaa8\ub380 \uc720\ud615\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624.",
+ "description": "Insteon \ubaa8\ub380 \uc720\ud615\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694.",
"title": "Insteon"
}
}
},
"options": {
"error": {
- "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
- "select_single": "\uc635\uc158 \uc120\ud0dd"
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "input_error": "\ud56d\ubaa9\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uac12\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
+ "select_single": "\uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694."
},
"step": {
"add_override": {
"data": {
- "cat": "\uc7a5\uce58 \ubc94\uc8fc(\uc608: 0x10)"
- }
+ "address": "\uae30\uae30 \uc8fc\uc18c (\uc608: 1a2b3c)",
+ "cat": "\uae30\uae30 \ubc94\uc8fc (\uc608: 0x10)",
+ "subcat": "\uae30\uae30 \ud558\uc704 \ubc94\uc8fc (\uc608: 0x0a)"
+ },
+ "description": "\uae30\uae30 \uc7ac\uc815\uc758\ub97c \ucd94\uac00\ud569\ub2c8\ub2e4.",
+ "title": "Insteon"
},
"add_x10": {
"data": {
- "steps": "\ub514\uba38 \ub2e8\uacc4(\ub77c\uc774\ud2b8 \uc7a5\uce58\uc5d0\ub9cc, \uae30\ubcf8\uac12 22)",
- "unitcode": "\ub2e8\uc704 \ucf54\ub4dc (1-16)"
+ "housecode": "\ud558\uc6b0\uc2a4 \ucf54\ub4dc (a - p)",
+ "platform": "\ud50c\ub7ab\ud3fc",
+ "steps": "\ubc1d\uae30 \uc870\uc808 \ub2e8\uacc4 (\uc870\uba85 \uae30\uae30 \uc804\uc6a9, \uae30\ubcf8\uac12 22)",
+ "unitcode": "\uc720\ub2db \ucf54\ub4dc (1-16)"
},
- "description": "Insteon Hub \ube44\ubc00\ubc88\ud638\ub97c \ubcc0\uacbd\ud569\ub2c8\ub2e4."
+ "description": "Insteon Hub \ube44\ubc00\ubc88\ud638\ub97c \ubcc0\uacbd\ud569\ub2c8\ub2e4.",
+ "title": "Insteon"
+ },
+ "change_hub_config": {
+ "data": {
+ "host": "IP \uc8fc\uc18c",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "port": "\ud3ec\ud2b8",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ },
+ "description": "Insteon \ud5c8\ube0c\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \ubcc0\uacbd\ud569\ub2c8\ub2e4. \ubcc0\uacbd\ud55c \ud6c4\uc5d0\ub294 Home Assistant\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud574\uc57c \ud569\ub2c8\ub2e4. \ud5c8\ube0c \uc790\uccb4\uc758 \uad6c\uc131\uc740 \ubcc0\uacbd\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ud5c8\ube0c\uc758 \uad6c\uc131\uc744 \ubcc0\uacbd\ud558\ub824\uba74 \ud5c8\ube0c \uc571\uc744 \uc0ac\uc6a9\ud574\uc8fc\uc138\uc694.",
+ "title": "Insteon"
},
"init": {
"data": {
- "add_override": "\uc7a5\uce58 Override \ucd94\uac00",
- "add_x10": "X10 \uc7a5\uce58\ub97c \ucd94\uac00\ud569\ub2c8\ub2e4.",
+ "add_override": "\uae30\uae30 \uc7ac\uc815\uc758\ub97c \ucd94\uac00\ud569\ub2c8\ub2e4.",
+ "add_x10": "X10 \uae30\uae30\ub97c \ucd94\uac00\ud569\ub2c8\ub2e4.",
"change_hub_config": "\ud5c8\ube0c \uad6c\uc131\uc744 \ubcc0\uacbd\ud569\ub2c8\ub2e4.",
- "remove_override": "\uc7a5\uce58 Override \uc81c\uac70",
- "remove_x10": "X10 \uc7a5\uce58\ub97c \uc81c\uac70\ud569\ub2c8\ub2e4."
+ "remove_override": "\uae30\uae30 \uc7ac\uc815\uc758\ub97c \uc81c\uac70\ud569\ub2c8\ub2e4.",
+ "remove_x10": "X10 \uae30\uae30\ub97c \uc81c\uac70\ud569\ub2c8\ub2e4."
},
- "description": "\uad6c\uc131 \ud560 \uc635\uc158\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624."
+ "description": "\uad6c\uc131\ud560 \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694.",
+ "title": "Insteon"
},
"remove_override": {
"data": {
- "address": "\uc81c\uac70 \ud560 \uc7a5\uce58 \uc8fc\uc18c\ub97c \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624."
+ "address": "\uc81c\uac70\ud560 \uae30\uae30\uc758 \uc8fc\uc18c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694"
},
- "description": "\uc7a5\uce58 Override \uc81c\uac70"
+ "description": "\uae30\uae30 \uc7ac\uc815\uc758 \uc81c\uac70\ud558\uae30",
+ "title": "Insteon"
},
"remove_x10": {
"data": {
- "address": "\uc81c\uac70 \ud560 \uc7a5\uce58 \uc8fc\uc18c\ub97c \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624."
+ "address": "\uc81c\uac70\ud560 \uae30\uae30\uc758 \uc8fc\uc18c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694"
},
- "description": "X10 \uc7a5\uce58 \uc81c\uac70"
+ "description": "X10 \uae30\uae30 \uc81c\uac70\ud558\uae30",
+ "title": "Insteon"
}
}
}
diff --git a/homeassistant/components/insteon/translations/nl.json b/homeassistant/components/insteon/translations/nl.json
index d2f73fca37bb74..0c9191e807791b 100644
--- a/homeassistant/components/insteon/translations/nl.json
+++ b/homeassistant/components/insteon/translations/nl.json
@@ -28,6 +28,9 @@
"title": "Insteon Hub versie 2"
},
"plm": {
+ "data": {
+ "device": "USB-apparaatpad"
+ },
"description": "Configureer de Insteon PowerLink Modem (PLM).",
"title": "Insteon PLM"
},
@@ -83,7 +86,23 @@
"change_hub_config": "Wijzig de Hub-configuratie.",
"remove_override": "Verwijder een apparaatoverschrijving.",
"remove_x10": "Verwijder een X10-apparaat."
- }
+ },
+ "description": "Selecteer een optie om te configureren.",
+ "title": "Insteon"
+ },
+ "remove_override": {
+ "data": {
+ "address": "Selecteer een apparaatadres om te verwijderen"
+ },
+ "description": "Verwijder een apparaatoverschrijving",
+ "title": "Insteon"
+ },
+ "remove_x10": {
+ "data": {
+ "address": "Selecteer een apparaatadres om te verwijderen"
+ },
+ "description": "Een X10 apparaat verwijderen",
+ "title": "Insteon"
}
}
}
diff --git a/homeassistant/components/insteon/translations/ru.json b/homeassistant/components/insteon/translations/ru.json
index dec25f1fe4bc7c..69b5354fe8739a 100644
--- a/homeassistant/components/insteon/translations/ru.json
+++ b/homeassistant/components/insteon/translations/ru.json
@@ -22,7 +22,7 @@
"host": "IP-\u0430\u0434\u0440\u0435\u0441",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Insteon Hub \u0432\u0435\u0440\u0441\u0438\u0438 2",
"title": "Insteon Hub. \u0412\u0435\u0440\u0441\u0438\u044f 2"
@@ -74,7 +74,7 @@
"host": "IP-\u0430\u0434\u0440\u0435\u0441",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 Insteon Hub. \u041f\u043e\u0441\u043b\u0435 \u0432\u043d\u0435\u0441\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c Home Assistant. \u042d\u0442\u043e \u043d\u0435 \u043c\u0435\u043d\u044f\u0435\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0441\u0430\u043c\u043e\u0433\u043e \u0445\u0430\u0431\u0430. \u0427\u0442\u043e\u0431\u044b \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0445\u0430\u0431\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Hub.",
"title": "Insteon"
diff --git a/homeassistant/components/insteon/translations/tr.json b/homeassistant/components/insteon/translations/tr.json
new file mode 100644
index 00000000000000..6c41f53b31eb41
--- /dev/null
+++ b/homeassistant/components/insteon/translations/tr.json
@@ -0,0 +1,89 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "hubv1": {
+ "data": {
+ "host": "\u0130p Adresi",
+ "port": "Port"
+ },
+ "title": "Insteon Hub S\u00fcr\u00fcm 1"
+ },
+ "hubv2": {
+ "data": {
+ "host": "\u0130p Adresi",
+ "password": "Parola",
+ "port": "Port",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ },
+ "description": "Insteon Hub S\u00fcr\u00fcm 2'yi yap\u0131land\u0131r\u0131n.",
+ "title": "Insteon Hub S\u00fcr\u00fcm 2"
+ },
+ "plm": {
+ "description": "Insteon PowerLink Modemini (PLM) yap\u0131land\u0131r\u0131n."
+ },
+ "user": {
+ "data": {
+ "modem_type": "Modem t\u00fcr\u00fc."
+ },
+ "description": "Insteon modem tipini se\u00e7in.",
+ "title": "Insteon"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "input_error": "Ge\u00e7ersiz giri\u015fler, l\u00fctfen de\u011ferlerinizi kontrol edin."
+ },
+ "step": {
+ "add_x10": {
+ "data": {
+ "unitcode": "Birim kodu (1-16)"
+ },
+ "description": "Insteon Hub parolas\u0131n\u0131 de\u011fi\u015ftirin.",
+ "title": "Insteon"
+ },
+ "change_hub_config": {
+ "data": {
+ "host": "\u0130p Adresi",
+ "password": "Parola",
+ "port": "Port",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ },
+ "title": "Insteon"
+ },
+ "init": {
+ "data": {
+ "add_override": "Bir cihaz\u0131 ge\u00e7ersiz k\u0131lma ekleyin.",
+ "add_x10": "Bir X10 cihaz\u0131 ekleyin.",
+ "change_hub_config": "Hub yap\u0131land\u0131rmas\u0131n\u0131 de\u011fi\u015ftirin.",
+ "remove_override": "Bir cihaz\u0131 ge\u00e7ersiz k\u0131lma i\u015flemini kald\u0131r\u0131n.",
+ "remove_x10": "Bir X10 cihaz\u0131n\u0131 \u00e7\u0131kar\u0131n."
+ },
+ "description": "Yap\u0131land\u0131rmak i\u00e7in bir se\u00e7enek se\u00e7in.",
+ "title": "Insteon"
+ },
+ "remove_override": {
+ "data": {
+ "address": "Kald\u0131r\u0131lacak bir cihaz adresi se\u00e7in"
+ },
+ "description": "Bir cihaz\u0131 ge\u00e7ersiz k\u0131lmay\u0131 kald\u0131rma",
+ "title": "Insteon"
+ },
+ "remove_x10": {
+ "data": {
+ "address": "Kald\u0131r\u0131lacak bir cihaz adresi se\u00e7in"
+ },
+ "description": "Bir X10 cihaz\u0131n\u0131 kald\u0131r\u0131n",
+ "title": "Insteon"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/insteon/translations/uk.json b/homeassistant/components/insteon/translations/uk.json
new file mode 100644
index 00000000000000..302d8c3676a00d
--- /dev/null
+++ b/homeassistant/components/insteon/translations/uk.json
@@ -0,0 +1,109 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "select_single": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u043f\u0446\u0456\u044e."
+ },
+ "step": {
+ "hubv1": {
+ "data": {
+ "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Insteon Hub \u0432\u0435\u0440\u0441\u0456\u0457 1 (\u0434\u043e 2014 \u0440\u043e\u043a\u0443)",
+ "title": "Insteon Hub. \u0412\u0435\u0440\u0441\u0456\u044f 1"
+ },
+ "hubv2": {
+ "data": {
+ "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Insteon Hub \u0432\u0435\u0440\u0441\u0456\u0457 2",
+ "title": "Insteon Hub. \u0412\u0435\u0440\u0441\u0456\u044f 2"
+ },
+ "plm": {
+ "data": {
+ "device": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u043e\u0434\u0435\u043c\u0443 Insteon PowerLink (PLM)",
+ "title": "Insteon PLM"
+ },
+ "user": {
+ "data": {
+ "modem_type": "\u0422\u0438\u043f \u043c\u043e\u0434\u0435\u043c\u0443"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u043c\u043e\u0434\u0435\u043c\u0443 Insteon.",
+ "title": "Insteon"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "input_error": "\u041d\u0435\u0432\u0456\u0440\u043d\u0456 \u0434\u0430\u043d\u0456, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0432\u043a\u0430\u0437\u0430\u043d\u0456 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f.",
+ "select_single": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u043f\u0446\u0456\u044e."
+ },
+ "step": {
+ "add_override": {
+ "data": {
+ "address": "\u0410\u0434\u0440\u0435\u0441\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 1a2b3c)",
+ "cat": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0456\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 0x10)",
+ "subcat": "\u041f\u0456\u0434\u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0456\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434, 0x0a)"
+ },
+ "description": "\u041f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e",
+ "title": "Insteon"
+ },
+ "add_x10": {
+ "data": {
+ "housecode": "\u041a\u043e\u0434 \u0431\u0443\u0434\u0438\u043d\u043a\u0443 (a - p)",
+ "platform": "\u041f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430",
+ "steps": "\u041a\u0440\u043e\u043a \u0434\u0456\u043c\u043c\u0435\u0440\u0430 (\u0442\u0456\u043b\u044c\u043a\u0438 \u0434\u043b\u044f \u043e\u0441\u0432\u0456\u0442\u043b\u044e\u0432\u0430\u043b\u044c\u043d\u0438\u0445 \u043f\u0440\u0438\u043b\u0430\u0434\u0456\u0432, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c 22)",
+ "unitcode": "\u042e\u043d\u0456\u0442\u043a\u043e\u0434 (1 - 16)"
+ },
+ "description": "\u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043e Insteon Hub",
+ "title": "Insteon"
+ },
+ "change_hub_config": {
+ "data": {
+ "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u0417\u043c\u0456\u043d\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f Insteon Hub. \u041f\u0456\u0441\u043b\u044f \u0432\u043d\u0435\u0441\u0435\u043d\u043d\u044f \u0446\u0438\u0445 \u0437\u043c\u0456\u043d \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 Home Assistant. \u0426\u0435 \u043d\u0435 \u0437\u043c\u0456\u043d\u044e\u0454 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0441\u0430\u043c\u043e\u0433\u043e \u0445\u0430\u0431\u0430. \u0429\u043e\u0431 \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0445\u0430\u0431\u0430, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Hub.",
+ "title": "Insteon"
+ },
+ "init": {
+ "data": {
+ "add_override": "\u041f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e",
+ "add_x10": "\u0414\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 X10",
+ "change_hub_config": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0445\u0430\u0431\u0430",
+ "remove_override": "\u0412\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u043f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e",
+ "remove_x10": "\u0412\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 X10"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u043f\u0446\u0456\u044e \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f",
+ "title": "Insteon"
+ },
+ "remove_override": {
+ "data": {
+ "address": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e, \u044f\u043a\u0438\u0439 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438"
+ },
+ "description": "\u0412\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u043f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e",
+ "title": "Insteon"
+ },
+ "remove_x10": {
+ "data": {
+ "address": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e, \u044f\u043a\u0438\u0439 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438"
+ },
+ "description": "\u0412\u0438\u0434\u0430\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e X10",
+ "title": "Insteon"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py
index a776920b8e644c..0ab4ac0d2c49fd 100644
--- a/homeassistant/components/integration/sensor.py
+++ b/homeassistant/components/integration/sensor.py
@@ -4,9 +4,10 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
+ CONF_METHOD,
CONF_NAME,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
@@ -31,7 +32,6 @@
CONF_UNIT_PREFIX = "unit_prefix"
CONF_UNIT_TIME = "unit_time"
CONF_UNIT_OF_MEASUREMENT = "unit"
-CONF_METHOD = "method"
TRAPEZOIDAL_METHOD = "trapezoidal"
LEFT_METHOD = "left"
@@ -83,7 +83,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([integral])
-class IntegrationSensor(RestoreEntity):
+class IntegrationSensor(RestoreEntity, SensorEntity):
"""Representation of an integration sensor."""
def __init__(
@@ -201,7 +201,7 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {ATTR_SOURCE_ID: self._sensor_source_id}
diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py
index a41161c7a6ef73..d58efddeb3c35e 100644
--- a/homeassistant/components/intesishome/climate.py
+++ b/homeassistant/components/intesishome/climate.py
@@ -212,7 +212,7 @@ def temperature_unit(self):
return TEMP_CELSIUS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
attrs = {}
if self._outdoor_temp:
diff --git a/homeassistant/components/intesishome/manifest.json b/homeassistant/components/intesishome/manifest.json
index 4131811807af45..d17014cdf0d6aa 100644
--- a/homeassistant/components/intesishome/manifest.json
+++ b/homeassistant/components/intesishome/manifest.json
@@ -3,5 +3,5 @@
"name": "IntesisHome",
"documentation": "https://www.home-assistant.io/integrations/intesishome",
"codeowners": ["@jnimmo"],
- "requirements": ["pyintesishome==1.7.5"]
+ "requirements": ["pyintesishome==1.7.6"]
}
diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py
index f9c682bf52743c..853fb0d479a4c9 100644
--- a/homeassistant/components/ios/notify.py
+++ b/homeassistant/components/ios/notify.py
@@ -69,10 +69,12 @@ def send_message(self, message="", **kwargs):
"""Send a message to the Lambda APNS gateway."""
data = {ATTR_MESSAGE: message}
- if kwargs.get(ATTR_TITLE) is not None:
- # Remove default title from notifications.
- if kwargs.get(ATTR_TITLE) != ATTR_TITLE_DEFAULT:
- data[ATTR_TITLE] = kwargs.get(ATTR_TITLE)
+ # Remove default title from notifications.
+ if (
+ kwargs.get(ATTR_TITLE) is not None
+ and kwargs.get(ATTR_TITLE) != ATTR_TITLE_DEFAULT
+ ):
+ data[ATTR_TITLE] = kwargs.get(ATTR_TITLE)
targets = kwargs.get(ATTR_TARGET)
diff --git a/homeassistant/components/ios/sensor.py b/homeassistant/components/ios/sensor.py
index ccbc118a68171e..c1442f0de9fa3e 100644
--- a/homeassistant/components/ios/sensor.py
+++ b/homeassistant/components/ios/sensor.py
@@ -1,9 +1,9 @@
"""Support for Home Assistant iOS app sensors."""
from homeassistant.components import ios
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import PERCENTAGE
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level
from .const import DOMAIN
@@ -32,7 +32,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(dev, True)
-class IOSSensor(Entity):
+class IOSSensor(SensorEntity):
"""Representation of an iOS sensor."""
def __init__(self, sensor_type, device_name, device):
@@ -88,7 +88,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
device = self._device[ios.ATTR_DEVICE]
device_battery = self._device[ios.ATTR_BATTERY]
diff --git a/homeassistant/components/ios/translations/de.json b/homeassistant/components/ios/translations/de.json
index e9e592d18c298f..bc427bd2992cbb 100644
--- a/homeassistant/components/ios/translations/de.json
+++ b/homeassistant/components/ios/translations/de.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Es wird nur eine Konfiguration von Home Assistant iOS ben\u00f6tigt"
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/ios/translations/hu.json b/homeassistant/components/ios/translations/hu.json
index f716fd36e9ac69..dda7af8c541fa6 100644
--- a/homeassistant/components/ios/translations/hu.json
+++ b/homeassistant/components/ios/translations/hu.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Csak egyetlen Home Assistant iOS konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges."
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
},
"step": {
"confirm": {
- "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Home Assistant iOS komponenst?"
+ "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?"
}
}
}
diff --git a/homeassistant/components/ios/translations/id.json b/homeassistant/components/ios/translations/id.json
index 46449f1c0443a0..c8003cc0d3d8b3 100644
--- a/homeassistant/components/ios/translations/id.json
+++ b/homeassistant/components/ios/translations/id.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Hanya satu konfigurasi Home Assistant iOS yang diperlukan."
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
},
"step": {
"confirm": {
- "description": "Apakah Anda ingin mengatur komponen iOS Home Assistant?"
+ "description": "Ingin memulai penyiapan?"
}
}
}
diff --git a/homeassistant/components/ios/translations/ko.json b/homeassistant/components/ios/translations/ko.json
index 6abe9380473470..d7eaa77481f131 100644
--- a/homeassistant/components/ios/translations/ko.json
+++ b/homeassistant/components/ios/translations/ko.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\ud558\ub098\uc758 Home Assistant iOS \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"step": {
"confirm": {
- "description": "Home Assistant iOS \ucef4\ud3ec\ub10c\ud2b8\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
}
}
}
diff --git a/homeassistant/components/ios/translations/nl.json b/homeassistant/components/ios/translations/nl.json
index 0575b4558afe29..78757f9f715dbc 100644
--- a/homeassistant/components/ios/translations/nl.json
+++ b/homeassistant/components/ios/translations/nl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van Home Assistant iOS nodig."
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
},
"step": {
"confirm": {
- "description": "Wilt u het Home Assistant iOS component instellen?"
+ "description": "Wil je beginnen met instellen?"
}
}
}
diff --git a/homeassistant/components/ios/translations/tr.json b/homeassistant/components/ios/translations/tr.json
new file mode 100644
index 00000000000000..8de4663957ea85
--- /dev/null
+++ b/homeassistant/components/ios/translations/tr.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "step": {
+ "confirm": {
+ "description": "Kuruluma ba\u015flamak ister misiniz?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ios/translations/uk.json b/homeassistant/components/ios/translations/uk.json
new file mode 100644
index 00000000000000..5f8d69f5f29b89
--- /dev/null
+++ b/homeassistant/components/ios/translations/uk.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iota/__init__.py b/homeassistant/components/iota/__init__.py
index 41c5ad35d83e09..04db9140122e73 100644
--- a/homeassistant/components/iota/__init__.py
+++ b/homeassistant/components/iota/__init__.py
@@ -67,7 +67,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
return {CONF_WALLET_NAME: self._name}
diff --git a/homeassistant/components/iota/sensor.py b/homeassistant/components/iota/sensor.py
index a6f689e8c2d93c..62260be241013e 100644
--- a/homeassistant/components/iota/sensor.py
+++ b/homeassistant/components/iota/sensor.py
@@ -1,6 +1,7 @@
"""Support for IOTA wallet sensors."""
from datetime import timedelta
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_NAME
from . import CONF_WALLETS, IotaDevice
@@ -27,7 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors)
-class IotaBalanceSensor(IotaDevice):
+class IotaBalanceSensor(IotaDevice, SensorEntity):
"""Implement an IOTA sensor for displaying wallets balance."""
def __init__(self, wallet_config, iota_config):
@@ -60,7 +61,7 @@ def update(self):
self._state = self.api.get_inputs()["totalBalance"]
-class IotaNodeSensor(IotaDevice):
+class IotaNodeSensor(IotaDevice, SensorEntity):
"""Implement an IOTA sensor for displaying attributes of node."""
def __init__(self, iota_config):
@@ -85,7 +86,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
return self._attr
diff --git a/homeassistant/components/iperf3/sensor.py b/homeassistant/components/iperf3/sensor.py
index 749a3e83217a65..610ff91250f591 100644
--- a/homeassistant/components/iperf3/sensor.py
+++ b/homeassistant/components/iperf3/sensor.py
@@ -1,4 +1,5 @@
"""Support for Iperf3 sensors."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -23,7 +24,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info)
async_add_entities(sensors, True)
-class Iperf3Sensor(RestoreEntity):
+class Iperf3Sensor(RestoreEntity, SensorEntity):
"""A Iperf3 sensor implementation."""
def __init__(self, iperf3_data, sensor_type):
@@ -55,7 +56,7 @@ def icon(self):
return ICON
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py
index a00941624f550c..9a4d7f932e166a 100644
--- a/homeassistant/components/ipma/__init__.py
+++ b/homeassistant/components/ipma/__init__.py
@@ -1,17 +1,10 @@
"""Component for the Portuguese weather service - IPMA."""
-from homeassistant.core import Config, HomeAssistant
-
from .config_flow import IpmaFlowHandler # noqa: F401
from .const import DOMAIN # noqa: F401
DEFAULT_NAME = "ipma"
-async def async_setup(hass: HomeAssistant, config: Config) -> bool:
- """Set up configured IPMA."""
- return True
-
-
async def async_setup_entry(hass, config_entry):
"""Set up IPMA station as config entry."""
hass.async_create_task(
diff --git a/homeassistant/components/ipma/const.py b/homeassistant/components/ipma/const.py
index 04064db2b88059..47434d7f76bf7f 100644
--- a/homeassistant/components/ipma/const.py
+++ b/homeassistant/components/ipma/const.py
@@ -1,6 +1,4 @@
"""Constants for IPMA component."""
-import logging
-
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
DOMAIN = "ipma"
@@ -8,5 +6,3 @@
HOME_LOCATION_NAME = "Home"
ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.ipma_{HOME_LOCATION_NAME}"
-
-_LOGGER = logging.getLogger(".")
diff --git a/homeassistant/components/ipma/translations/fr.json b/homeassistant/components/ipma/translations/fr.json
index 9a3a11a7a739e8..eaff9d211db431 100644
--- a/homeassistant/components/ipma/translations/fr.json
+++ b/homeassistant/components/ipma/translations/fr.json
@@ -15,5 +15,10 @@
"title": "Emplacement"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "Point de terminaison de l'API IPMA accessible"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ipma/translations/hu.json b/homeassistant/components/ipma/translations/hu.json
index 0aeb36b0442767..00ba66d2dd7194 100644
--- a/homeassistant/components/ipma/translations/hu.json
+++ b/homeassistant/components/ipma/translations/hu.json
@@ -12,8 +12,13 @@
"name": "N\u00e9v"
},
"description": "Portug\u00e1l Atmoszf\u00e9ra Int\u00e9zet",
- "title": "Hely"
+ "title": "Elhelyezked\u00e9s"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "IPMA API v\u00e9gpont el\u00e9rhet\u0151"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ipma/translations/id.json b/homeassistant/components/ipma/translations/id.json
new file mode 100644
index 00000000000000..2f7e8324fdfc61
--- /dev/null
+++ b/homeassistant/components/ipma/translations/id.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "Nama sudah ada"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Lintang",
+ "longitude": "Bujur",
+ "mode": "Mode",
+ "name": "Nama"
+ },
+ "description": "Instituto Portugu\u00eas do Mar e Atmosfera",
+ "title": "Lokasi"
+ }
+ }
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "Titik akhir API IPMA dapat dijangkau"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ipma/translations/ko.json b/homeassistant/components/ipma/translations/ko.json
index 3fd1a8d8ea9e94..826309a4ee4371 100644
--- a/homeassistant/components/ipma/translations/ko.json
+++ b/homeassistant/components/ipma/translations/ko.json
@@ -15,5 +15,10 @@
"title": "\uc704\uce58"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "IPMA API \uc5d4\ub4dc\ud3ec\uc778\ud2b8 \uc5f0\uacb0"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ipma/translations/lb.json b/homeassistant/components/ipma/translations/lb.json
index 7b2d374b6f551a..006d80d37869d8 100644
--- a/homeassistant/components/ipma/translations/lb.json
+++ b/homeassistant/components/ipma/translations/lb.json
@@ -15,5 +15,10 @@
"title": "Standuert"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "IPMA API Endpunkt ereechbar"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ipma/translations/nl.json b/homeassistant/components/ipma/translations/nl.json
index 79248e0d064e40..154aafe10333c3 100644
--- a/homeassistant/components/ipma/translations/nl.json
+++ b/homeassistant/components/ipma/translations/nl.json
@@ -6,8 +6,8 @@
"step": {
"user": {
"data": {
- "latitude": "Latitude",
- "longitude": "Longitude",
+ "latitude": "Breedtegraad",
+ "longitude": "Lengtegraad",
"mode": "Mode",
"name": "Naam"
},
@@ -15,5 +15,10 @@
"title": "Locatie"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "IPMA API-eindpunt bereikbaar"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ipma/translations/tr.json b/homeassistant/components/ipma/translations/tr.json
index 488ad379942076..a8df63645abe95 100644
--- a/homeassistant/components/ipma/translations/tr.json
+++ b/homeassistant/components/ipma/translations/tr.json
@@ -1,4 +1,15 @@
{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Enlem",
+ "longitude": "Boylam",
+ "mode": "Mod"
+ }
+ }
+ }
+ },
"system_health": {
"info": {
"api_endpoint_reachable": "Ula\u015f\u0131labilir IPMA API u\u00e7 noktas\u0131"
diff --git a/homeassistant/components/ipma/translations/uk.json b/homeassistant/components/ipma/translations/uk.json
index bb294cc5d21e0c..ee84e7d16f2249 100644
--- a/homeassistant/components/ipma/translations/uk.json
+++ b/homeassistant/components/ipma/translations/uk.json
@@ -1,9 +1,24 @@
{
"config": {
+ "error": {
+ "name_exists": "\u0426\u044f \u043d\u0430\u0437\u0432\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f."
+ },
"step": {
"user": {
- "title": "\u0420\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f"
+ "data": {
+ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
+ "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430",
+ "mode": "\u0420\u0435\u0436\u0438\u043c",
+ "name": "\u041d\u0430\u0437\u0432\u0430"
+ },
+ "description": "\u041f\u043e\u0440\u0442\u0443\u0433\u0430\u043b\u044c\u0441\u044c\u043a\u0438\u0439 \u0456\u043d\u0441\u0442\u0438\u0442\u0443\u0442 \u043c\u043e\u0440\u044f \u0442\u0430 \u0430\u0442\u043c\u043e\u0441\u0444\u0435\u0440\u0438.",
+ "title": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e API IPMA"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py
index 7a18da03ddcff6..86bde4bba6cd31 100644
--- a/homeassistant/components/ipp/__init__.py
+++ b/homeassistant/components/ipp/__init__.py
@@ -1,8 +1,10 @@
"""The Internet Printing Protocol (IPP) integration."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
import logging
-from typing import Any, Dict
+from typing import Any
from pyipp import IPP, IPPError, Printer as IPPPrinter
@@ -16,7 +18,6 @@
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
@@ -39,7 +40,7 @@
_LOGGER = logging.getLogger(__name__)
-async def async_setup(hass: HomeAssistant, config: Dict) -> bool:
+async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up the IPP component."""
hass.data.setdefault(DOMAIN, {})
return True
@@ -61,14 +62,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
hass.data[DOMAIN][entry.entry_id] = coordinator
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -79,8 +77,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -166,7 +164,7 @@ def entity_registry_enabled_default(self) -> bool:
return self._enabled_default
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about this IPP device."""
if self._device_id is None:
return None
diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py
index feed7e7b5289f1..d2624931ea0d6f 100644
--- a/homeassistant/components/ipp/config_flow.py
+++ b/homeassistant/components/ipp/config_flow.py
@@ -1,6 +1,8 @@
"""Config flow to configure the IPP integration."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from pyipp import (
IPP,
@@ -24,13 +26,12 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
-from .const import CONF_BASE_PATH, CONF_SERIAL, CONF_UUID
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import CONF_BASE_PATH, CONF_SERIAL, CONF_UUID, DOMAIN
_LOGGER = logging.getLogger(__name__)
-async def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]:
+async def validate_input(hass: HomeAssistantType, data: dict) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
@@ -61,8 +62,8 @@ def __init__(self):
self.discovery_info = {}
async def async_step_user(
- self, user_input: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, user_input: ConfigType | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initiated by the user."""
if user_input is None:
return self._show_setup_form()
@@ -98,7 +99,7 @@ async def async_step_user(
return self.async_create_entry(title=user_input[CONF_HOST], data=user_input)
- async def async_step_zeroconf(self, discovery_info: ConfigType) -> Dict[str, Any]:
+ async def async_step_zeroconf(self, discovery_info: ConfigType) -> dict[str, Any]:
"""Handle zeroconf discovery."""
port = discovery_info[CONF_PORT]
zctype = discovery_info["type"]
@@ -106,7 +107,6 @@ async def async_step_zeroconf(self, discovery_info: ConfigType) -> Dict[str, Any
tls = zctype == "_ipps._tcp.local."
base_path = discovery_info["properties"].get("rp", "ipp/print")
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update({"title_placeholders": {"name": name}})
self.discovery_info.update(
@@ -167,7 +167,7 @@ async def async_step_zeroconf(self, discovery_info: ConfigType) -> Dict[str, Any
async def async_step_zeroconf_confirm(
self, user_input: ConfigType = None
- ) -> Dict[str, Any]:
+ ) -> dict[str, Any]:
"""Handle a confirmation flow initiated by zeroconf."""
if user_input is None:
return self.async_show_form(
@@ -181,7 +181,7 @@ async def async_step_zeroconf_confirm(
data=self.discovery_info,
)
- def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
+ def _show_setup_form(self, errors: dict | None = None) -> dict[str, Any]:
"""Show the setup form to the user."""
return self.async_show_form(
step_id="user",
diff --git a/homeassistant/components/ipp/const.py b/homeassistant/components/ipp/const.py
index a5345f4145e09b..d482f2d73e4505 100644
--- a/homeassistant/components/ipp/const.py
+++ b/homeassistant/components/ipp/const.py
@@ -7,7 +7,6 @@
ATTR_COMMAND_SET = "command_set"
ATTR_IDENTIFIERS = "identifiers"
ATTR_INFO = "info"
-ATTR_LOCATION = "location"
ATTR_MANUFACTURER = "manufacturer"
ATTR_MARKER_TYPE = "marker_type"
ATTR_MARKER_LOW_LEVEL = "marker_low_level"
diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py
index 6991e6d19ea999..83826409ed8d4f 100644
--- a/homeassistant/components/ipp/sensor.py
+++ b/homeassistant/components/ipp/sensor.py
@@ -1,9 +1,12 @@
"""Support for IPP sensors."""
+from __future__ import annotations
+
from datetime import timedelta
-from typing import Any, Callable, Dict, List, Optional
+from typing import Any, Callable
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE
+from homeassistant.const import ATTR_LOCATION, DEVICE_CLASS_TIMESTAMP, PERCENTAGE
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.dt import utcnow
@@ -12,7 +15,6 @@
from .const import (
ATTR_COMMAND_SET,
ATTR_INFO,
- ATTR_LOCATION,
ATTR_MARKER_HIGH_LEVEL,
ATTR_MARKER_LOW_LEVEL,
ATTR_MARKER_TYPE,
@@ -27,7 +29,7 @@
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up IPP sensor based on a config entry."""
coordinator: IPPDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
@@ -51,7 +53,7 @@ async def async_setup_entry(
async_add_entities(sensors, True)
-class IPPSensor(IPPEntity):
+class IPPSensor(IPPEntity, SensorEntity):
"""Defines an IPP sensor."""
def __init__(
@@ -64,7 +66,7 @@ def __init__(
icon: str,
key: str,
name: str,
- unit_of_measurement: Optional[str] = None,
+ unit_of_measurement: str | None = None,
) -> None:
"""Initialize IPP sensor."""
self._unit_of_measurement = unit_of_measurement
@@ -118,7 +120,7 @@ def __init__(
)
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the entity."""
return {
ATTR_MARKER_HIGH_LEVEL: self.coordinator.data.markers[
@@ -133,7 +135,7 @@ def device_state_attributes(self) -> Optional[Dict[str, Any]]:
}
@property
- def state(self) -> Optional[int]:
+ def state(self) -> int | None:
"""Return the state of the sensor."""
level = self.coordinator.data.markers[self.marker_index].level
@@ -161,7 +163,7 @@ def __init__(
)
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the entity."""
return {
ATTR_INFO: self.coordinator.data.info.printer_info,
@@ -203,6 +205,6 @@ def state(self) -> str:
return uptime.replace(microsecond=0).isoformat()
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the class of this sensor."""
return DEVICE_CLASS_TIMESTAMP
diff --git a/homeassistant/components/ipp/translations/de.json b/homeassistant/components/ipp/translations/de.json
index 73dd3f69bcc10f..69402c8fdba3d9 100644
--- a/homeassistant/components/ipp/translations/de.json
+++ b/homeassistant/components/ipp/translations/de.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"connection_upgrade": "Verbindung zum Drucker fehlgeschlagen, da ein Verbindungsupgrade erforderlich ist.",
"ipp_error": "IPP-Fehler festgestellt.",
"ipp_version_error": "IPP-Version wird vom Drucker nicht unterst\u00fctzt.",
@@ -9,6 +10,7 @@
"unique_id_required": "Ger\u00e4t fehlt die f\u00fcr die Entdeckung erforderliche eindeutige Identifizierung."
},
"error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
"connection_upgrade": "Verbindung zum Drucker fehlgeschlagen. Bitte versuchen Sie es erneut mit aktivierter SSL / TLS-Option."
},
"flow_title": "Drucker: {name}",
@@ -18,14 +20,14 @@
"base_path": "Relativer Pfad zum Drucker",
"host": "Host",
"port": "Port",
- "ssl": "Der Drucker unterst\u00fctzt die Kommunikation \u00fcber SSL / TLS",
- "verify_ssl": "Der Drucker verwendet ein ordnungsgem\u00e4\u00dfes SSL-Zertifikat"
+ "ssl": "Verwendet ein SSL-Zertifikat",
+ "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen"
},
"description": "Richten Sie Ihren Drucker \u00fcber das Internet Printing Protocol (IPP) f\u00fcr die Integration in Home Assistant ein.",
"title": "Verbinden Sie Ihren Drucker"
},
"zeroconf_confirm": {
- "description": "M\u00f6chten Sie den Drucker mit dem Namen \"{name}\" zu Home Assistant hinzuf\u00fcgen?",
+ "description": "M\u00f6chtest du {name} einrichten?",
"title": "Entdeckter Drucker"
}
}
diff --git a/homeassistant/components/ipp/translations/et.json b/homeassistant/components/ipp/translations/et.json
index 622b71d758ad9e..5a0a2e69cfb4d9 100644
--- a/homeassistant/components/ipp/translations/et.json
+++ b/homeassistant/components/ipp/translations/et.json
@@ -11,7 +11,7 @@
},
"error": {
"cannot_connect": "\u00dchendamine nurjus",
- "connection_upgrade": "Printeriga \u00fchenduse loomine nurjus. Proovige uuesti kui SSL/TLS-i suvand on m\u00e4rgitud."
+ "connection_upgrade": "Printeriga \u00fchenduse loomine nurjus. Proovi uuesti kui SSL/TLS-i suvand on m\u00e4rgitud."
},
"flow_title": "Printer: {name}",
"step": {
@@ -23,8 +23,8 @@
"ssl": "Printer toetab SSL/TLS \u00fchendust",
"verify_ssl": "Printer kasutab \u00f5iget SSL-serti"
},
- "description": "Seadistage oma printer Interneti-printimisprotokolli (IPP) kaudu, et see integreeruks Home Assistantiga.",
- "title": "Linkige oma printer"
+ "description": "Seadista oma printer Interneti-printimisprotokolli (IPP) kaudu, et see integreeruks Home Assistantiga.",
+ "title": "Lingi oma printer"
},
"zeroconf_confirm": {
"description": "Kas soovite seadistada {name}?",
diff --git a/homeassistant/components/ipp/translations/hu.json b/homeassistant/components/ipp/translations/hu.json
index 396992156c0f23..8c988eff55117a 100644
--- a/homeassistant/components/ipp/translations/hu.json
+++ b/homeassistant/components/ipp/translations/hu.json
@@ -14,8 +14,13 @@
"user": {
"data": {
"host": "Hoszt",
- "port": "Port"
+ "port": "Port",
+ "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata",
+ "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se"
}
+ },
+ "zeroconf_confirm": {
+ "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?"
}
}
}
diff --git a/homeassistant/components/ipp/translations/id.json b/homeassistant/components/ipp/translations/id.json
new file mode 100644
index 00000000000000..c2b95751d4bd20
--- /dev/null
+++ b/homeassistant/components/ipp/translations/id.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung",
+ "connection_upgrade": "Gagal terhubung ke printer karena peningkatan koneksi diperlukan.",
+ "ipp_error": "Terjadi kesalahan IPP.",
+ "ipp_version_error": "Versi IPP tidak didukung oleh printer.",
+ "parse_error": "Gagal mengurai respons dari printer.",
+ "unique_id_required": "Perangkat tidak memiliki identifikasi unik yang diperlukan untuk ditemukan."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "connection_upgrade": "Gagal terhubung ke printer. Coba lagi dengan mencentang opsi SSL/TLS."
+ },
+ "flow_title": "Printer: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "base_path": "Jalur relatif ke printer",
+ "host": "Host",
+ "port": "Port",
+ "ssl": "Menggunakan sertifikat SSL",
+ "verify_ssl": "Verifikasi sertifikat SSL"
+ },
+ "description": "Siapkan printer Anda melalui Internet Printing Protocol (IPP) untuk diintegrasikan dengan Home Assistant.",
+ "title": "Tautkan printer Anda"
+ },
+ "zeroconf_confirm": {
+ "description": "Ingin menyiapkan {name}?",
+ "title": "Printer yang ditemukan"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ipp/translations/ko.json b/homeassistant/components/ipp/translations/ko.json
index bc2e18cb5c2f3d..2ae4e08beaf5cd 100644
--- a/homeassistant/components/ipp/translations/ko.json
+++ b/homeassistant/components/ipp/translations/ko.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"connection_upgrade": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud558\ub824\uba74 \uc5f0\uacb0\uc744 \uc5c5\uadf8\ub808\uc774\ub4dc\ud574\uc57c \ud569\ub2c8\ub2e4.",
"ipp_error": "IPP \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
"ipp_version_error": "\ud504\ub9b0\ud130\uc5d0\uc11c IPP \ubc84\uc804\uc744 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.",
@@ -9,7 +10,8 @@
"unique_id_required": "\uae30\uae30 \uac80\uc0c9\uc5d0 \ud544\uc694\ud55c \uace0\uc720\ud55c ID \uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4."
},
"error": {
- "connection_upgrade": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. SSL/TLS \uc635\uc158\uc744 \ud655\uc778\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694."
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "connection_upgrade": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. SSL/TLS \uc635\uc158\uc744 \ud655\uc778\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694."
},
"flow_title": "\ud504\ub9b0\ud130: {name}",
"step": {
@@ -18,14 +20,14 @@
"base_path": "\ud504\ub9b0\ud130\uc758 \uc0c1\ub300 \uacbd\ub85c",
"host": "\ud638\uc2a4\ud2b8",
"port": "\ud3ec\ud2b8",
- "ssl": "\ud504\ub9b0\ud130\ub294 SSL/TLS \ub97c \ud1b5\ud55c \ud1b5\uc2e0\uc744 \uc9c0\uc6d0\ud569\ub2c8\ub2e4",
- "verify_ssl": "\ud504\ub9b0\ud130\ub294 \uc62c\ubc14\ub978 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4"
+ "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9",
+ "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778"
},
"description": "\uc778\ud130\ub137 \uc778\uc1c4 \ud504\ub85c\ud1a0\ucf5c (IPP) \ub97c \ud1b5\ud574 \ud504\ub9b0\ud130\ub97c \uc124\uc815\ud558\uc5ec Home Assistant \uc640 \uc5f0\ub3d9\ud569\ub2c8\ub2e4.",
"title": "\ud504\ub9b0\ud130 \uc5f0\uacb0\ud558\uae30"
},
"zeroconf_confirm": {
- "description": "{name} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "description": "{name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "\ubc1c\uacac\ub41c \ud504\ub9b0\ud130"
}
}
diff --git a/homeassistant/components/ipp/translations/nl.json b/homeassistant/components/ipp/translations/nl.json
index bcbd495be91324..f3d2ee60797909 100644
--- a/homeassistant/components/ipp/translations/nl.json
+++ b/homeassistant/components/ipp/translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Deze printer is al geconfigureerd.",
+ "already_configured": "Apparaat is al geconfigureerd",
"cannot_connect": "Kan geen verbinding maken",
"connection_upgrade": "Kan geen verbinding maken met de printer omdat een upgrade van de verbinding vereist is.",
"ipp_error": "Er is een IPP-fout opgetreden.",
@@ -18,10 +18,10 @@
"user": {
"data": {
"base_path": "Relatief pad naar de printer",
- "host": "Host- of IP-adres",
+ "host": "Host",
"port": "Poort",
"ssl": "Printer ondersteunt communicatie via SSL / TLS",
- "verify_ssl": "Printer gebruikt een correct SSL-certificaat"
+ "verify_ssl": "SSL-certificaat verifi\u00ebren"
},
"description": "Stel uw printer in via Internet Printing Protocol (IPP) om te integreren met Home Assistant.",
"title": "Koppel uw printer"
diff --git a/homeassistant/components/ipp/translations/tr.json b/homeassistant/components/ipp/translations/tr.json
index dbb14fe825ed01..78b9a868bd2f9f 100644
--- a/homeassistant/components/ipp/translations/tr.json
+++ b/homeassistant/components/ipp/translations/tr.json
@@ -1,7 +1,24 @@
{
"config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "connection_upgrade": "Yaz\u0131c\u0131ya ba\u011flan\u0131lamad\u0131. L\u00fctfen SSL / TLS se\u00e7ene\u011fi i\u015faretli olarak tekrar deneyin."
+ },
+ "flow_title": "Yaz\u0131c\u0131: {name}",
"step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ },
+ "title": "Yaz\u0131c\u0131n\u0131z\u0131 ba\u011flay\u0131n"
+ },
"zeroconf_confirm": {
+ "description": "{name} kurmak istiyor musunuz?",
"title": "Ke\u015ffedilen yaz\u0131c\u0131"
}
}
diff --git a/homeassistant/components/ipp/translations/uk.json b/homeassistant/components/ipp/translations/uk.json
new file mode 100644
index 00000000000000..bb6df07f1e491a
--- /dev/null
+++ b/homeassistant/components/ipp/translations/uk.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "connection_upgrade": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u043e\u043c \u0447\u0435\u0440\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0456\u0441\u0442\u044c \u043f\u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f.",
+ "ipp_error": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 IPP.",
+ "ipp_version_error": "\u0412\u0435\u0440\u0441\u0456\u044f IPP \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u043e\u043c.",
+ "parse_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0440\u043e\u0437\u0456\u0431\u0440\u0430\u0442\u0438 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u044c \u0432\u0456\u0434 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430.",
+ "unique_id_required": "\u041d\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0432\u0456\u0434\u0441\u0443\u0442\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044f, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0430 \u0434\u043b\u044f \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "connection_upgrade": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u043e\u043c. \u0421\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0447\u0435\u0440\u0435\u0437 SSL / TLS."
+ },
+ "flow_title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "base_path": "\u0412\u0456\u0434\u043d\u043e\u0441\u043d\u0438\u0439 \u0448\u043b\u044f\u0445 \u0434\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430",
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442",
+ "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL",
+ "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043f\u043e \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 IPP.",
+ "title": "Internet Printing Protocol (IPP)"
+ },
+ "zeroconf_confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 `{name}`?",
+ "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py
index bf1725a036a9ed..c548a115e04eb4 100644
--- a/homeassistant/components/iqvia/__init__.py
+++ b/homeassistant/components/iqvia/__init__.py
@@ -6,6 +6,7 @@
from pyiqvia import Client
from pyiqvia.errors import IQVIAError
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
@@ -84,9 +85,9 @@ async def async_get_data_from_api(api_coro):
await asyncio.gather(*init_data_update_tasks)
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -97,8 +98,8 @@ async def async_unload_entry(hass, entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -109,7 +110,7 @@ async def async_unload_entry(hass, entry):
return unload_ok
-class IQVIAEntity(CoordinatorEntity):
+class IQVIAEntity(CoordinatorEntity, SensorEntity):
"""Define a base IQVIA entity."""
def __init__(self, coordinator, entry, sensor_type, name, icon):
@@ -123,7 +124,7 @@ def __init__(self, coordinator, entry, sensor_type, name, icon):
self._type = sensor_type
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
return self._attrs
diff --git a/homeassistant/components/iqvia/config_flow.py b/homeassistant/components/iqvia/config_flow.py
index ecd1e3c3c4b133..1e90a983e4585c 100644
--- a/homeassistant/components/iqvia/config_flow.py
+++ b/homeassistant/components/iqvia/config_flow.py
@@ -6,7 +6,7 @@
from homeassistant import config_entries
from homeassistant.helpers import aiohttp_client
-from .const import CONF_ZIP_CODE, DOMAIN # pylint:disable=unused-import
+from .const import CONF_ZIP_CODE, DOMAIN
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json
index 6445b4ad91f845..145972e287555e 100644
--- a/homeassistant/components/iqvia/manifest.json
+++ b/homeassistant/components/iqvia/manifest.json
@@ -3,6 +3,6 @@
"name": "IQVIA",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/iqvia",
- "requirements": ["numpy==1.19.2", "pyiqvia==0.3.1"],
+ "requirements": ["numpy==1.20.2", "pyiqvia==0.3.1"],
"codeowners": ["@bachya"]
}
diff --git a/homeassistant/components/iqvia/translations/hu.json b/homeassistant/components/iqvia/translations/hu.json
new file mode 100644
index 00000000000000..f5301e874eae05
--- /dev/null
+++ b/homeassistant/components/iqvia/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iqvia/translations/id.json b/homeassistant/components/iqvia/translations/id.json
index a93f9aac26fc74..32f9b77135b9f0 100644
--- a/homeassistant/components/iqvia/translations/id.json
+++ b/homeassistant/components/iqvia/translations/id.json
@@ -1,10 +1,18 @@
{
"config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi"
+ },
+ "error": {
+ "invalid_zip_code": "Kode pos tidak valid"
+ },
"step": {
"user": {
"data": {
"zip_code": "Kode Pos"
- }
+ },
+ "description": "Isi kode pos AS atau Kanada.",
+ "title": "IQVIA"
}
}
}
diff --git a/homeassistant/components/iqvia/translations/ko.json b/homeassistant/components/iqvia/translations/ko.json
index f6a914bd07d9f0..1b0dfd980ec176 100644
--- a/homeassistant/components/iqvia/translations/ko.json
+++ b/homeassistant/components/iqvia/translations/ko.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 \uc6b0\ud3b8 \ubc88\ud638\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
"invalid_zip_code": "\uc6b0\ud3b8\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
diff --git a/homeassistant/components/iqvia/translations/tr.json b/homeassistant/components/iqvia/translations/tr.json
new file mode 100644
index 00000000000000..717f6d72b94e5d
--- /dev/null
+++ b/homeassistant/components/iqvia/translations/tr.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iqvia/translations/uk.json b/homeassistant/components/iqvia/translations/uk.json
new file mode 100644
index 00000000000000..ab9813d6289d97
--- /dev/null
+++ b/homeassistant/components/iqvia/translations/uk.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant."
+ },
+ "error": {
+ "invalid_zip_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "zip_code": "\u041f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0441\u0432\u0456\u0439 \u043f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441 (\u0434\u043b\u044f \u0421\u0428\u0410 \u0430\u0431\u043e \u041a\u0430\u043d\u0430\u0434\u0438).",
+ "title": "IQVIA"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/irish_rail_transport/sensor.py b/homeassistant/components/irish_rail_transport/sensor.py
index fba69a6f578120..b5ba16f8541f0c 100644
--- a/homeassistant/components/irish_rail_transport/sensor.py
+++ b/homeassistant/components/irish_rail_transport/sensor.py
@@ -4,10 +4,9 @@
from pyirishrail.pyirishrail import IrishRailRTPI
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, TIME_MINUTES
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
ATTR_STATION = "Station"
ATTR_ORIGIN = "Origin"
@@ -64,7 +63,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
)
-class IrishRailTransportSensor(Entity):
+class IrishRailTransportSensor(SensorEntity):
"""Implementation of an irish rail public transport sensor."""
def __init__(self, data, station, direction, destination, stops_at, name):
@@ -89,7 +88,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._times:
next_up = "None"
diff --git a/homeassistant/components/islamic_prayer_times/config_flow.py b/homeassistant/components/islamic_prayer_times/config_flow.py
index 065af0bd6110f3..c496bd6673209d 100644
--- a/homeassistant/components/islamic_prayer_times/config_flow.py
+++ b/homeassistant/components/islamic_prayer_times/config_flow.py
@@ -4,7 +4,6 @@
from homeassistant import config_entries
from homeassistant.core import callback
-# pylint: disable=unused-import
from .const import CALC_METHODS, CONF_CALC_METHOD, DEFAULT_CALC_METHOD, DOMAIN, NAME
diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py
index 41b3cf4c667871..3133320d978056 100644
--- a/homeassistant/components/islamic_prayer_times/sensor.py
+++ b/homeassistant/components/islamic_prayer_times/sensor.py
@@ -1,8 +1,8 @@
"""Platform to retrieve Islamic prayer times information for Home Assistant."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_TIMESTAMP
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
from .const import DATA_UPDATED, DOMAIN, PRAYER_TIMES_ICON, SENSOR_TYPES
@@ -20,7 +20,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities, True)
-class IslamicPrayerTimeSensor(Entity):
+class IslamicPrayerTimeSensor(SensorEntity):
"""Representation of an Islamic prayer time sensor."""
def __init__(self, sensor_type, client):
diff --git a/homeassistant/components/islamic_prayer_times/translations/de.json b/homeassistant/components/islamic_prayer_times/translations/de.json
index af38303c9a2f94..b8097e9bd39a3c 100644
--- a/homeassistant/components/islamic_prayer_times/translations/de.json
+++ b/homeassistant/components/islamic_prayer_times/translations/de.json
@@ -1,3 +1,23 @@
{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
+ "step": {
+ "user": {
+ "description": "M\u00f6chtest du islamische Gebetszeiten einrichten?",
+ "title": "Islamische Gebetszeiten einrichten"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "calculation_method": "Gebetsberechnungsmethode"
+ }
+ }
+ }
+ },
"title": "Islamische Gebetszeiten"
}
\ No newline at end of file
diff --git a/homeassistant/components/islamic_prayer_times/translations/hu.json b/homeassistant/components/islamic_prayer_times/translations/hu.json
new file mode 100644
index 00000000000000..065747fb39df50
--- /dev/null
+++ b/homeassistant/components/islamic_prayer_times/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/islamic_prayer_times/translations/id.json b/homeassistant/components/islamic_prayer_times/translations/id.json
new file mode 100644
index 00000000000000..30eb3497847b81
--- /dev/null
+++ b/homeassistant/components/islamic_prayer_times/translations/id.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "step": {
+ "user": {
+ "description": "Ingin menyiapkan Jadwal Sholat Islam?",
+ "title": "Siapkan Jadwal Sholat Islam"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "calculation_method": "Metode perhitungan waktu sholat"
+ }
+ }
+ }
+ },
+ "title": "Jadwal Sholat Islami"
+}
\ No newline at end of file
diff --git a/homeassistant/components/islamic_prayer_times/translations/ko.json b/homeassistant/components/islamic_prayer_times/translations/ko.json
index 52ac68698559f9..aa0905b361237a 100644
--- a/homeassistant/components/islamic_prayer_times/translations/ko.json
+++ b/homeassistant/components/islamic_prayer_times/translations/ko.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
"step": {
"user": {
"description": "\uc774\uc2ac\ub78c \uae30\ub3c4 \uc2dc\uac04\uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
diff --git a/homeassistant/components/islamic_prayer_times/translations/tr.json b/homeassistant/components/islamic_prayer_times/translations/tr.json
new file mode 100644
index 00000000000000..a152eb194683cb
--- /dev/null
+++ b/homeassistant/components/islamic_prayer_times/translations/tr.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/islamic_prayer_times/translations/uk.json b/homeassistant/components/islamic_prayer_times/translations/uk.json
new file mode 100644
index 00000000000000..9290114899a4ee
--- /dev/null
+++ b/homeassistant/components/islamic_prayer_times/translations/uk.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "step": {
+ "user": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u0440\u043e\u0437\u043a\u043b\u0430\u0434 \u0447\u0430\u0441\u0443 \u043d\u0430\u043c\u0430\u0437\u0443?",
+ "title": "\u0427\u0430\u0441 \u043d\u0430\u043c\u0430\u0437\u0443"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "calculation_method": "\u0421\u043f\u043e\u0441\u0456\u0431 \u0440\u043e\u0437\u0440\u0430\u0445\u0443\u043d\u043a\u0443"
+ }
+ }
+ }
+ },
+ "title": "\u0427\u0430\u0441 \u043d\u0430\u043c\u0430\u0437\u0443"
+}
\ No newline at end of file
diff --git a/homeassistant/components/iss/binary_sensor.py b/homeassistant/components/iss/binary_sensor.py
index e1f0d7a19ceca5..787d7471d4374f 100644
--- a/homeassistant/components/iss/binary_sensor.py
+++ b/homeassistant/components/iss/binary_sensor.py
@@ -79,7 +79,7 @@ def device_class(self):
return DEFAULT_DEVICE_CLASS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self.iss_data:
attrs = {
diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py
index 9fd844521d705d..de43407c371ce4 100644
--- a/homeassistant/components/isy994/__init__.py
+++ b/homeassistant/components/isy994/__init__.py
@@ -1,7 +1,8 @@
"""Support the ISY-994 controllers."""
+from __future__ import annotations
+
import asyncio
from functools import partial
-from typing import Optional
from urllib.parse import urlparse
from pyisy import ISY
@@ -31,7 +32,7 @@
ISY994_PROGRAMS,
ISY994_VARIABLES,
MANUFACTURER,
- SUPPORTED_PLATFORMS,
+ PLATFORMS,
SUPPORTED_PROGRAM_PLATFORMS,
UNDO_UPDATE_LISTENER,
)
@@ -67,7 +68,7 @@
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the isy994 integration from YAML."""
- isy_config: Optional[ConfigType] = config.get(DOMAIN)
+ isy_config: ConfigType | None = config.get(DOMAIN)
hass.data.setdefault(DOMAIN, {})
if not isy_config:
@@ -111,7 +112,7 @@ async def async_setup_entry(
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
hass_isy_data[ISY994_NODES] = {}
- for platform in SUPPORTED_PLATFORMS:
+ for platform in PLATFORMS:
hass_isy_data[ISY994_NODES][platform] = []
hass_isy_data[ISY994_PROGRAMS] = {}
@@ -143,7 +144,7 @@ async def async_setup_entry(
https = True
port = host.port or 443
else:
- _LOGGER.error("isy994 host value in configuration is invalid")
+ _LOGGER.error("The isy994 host value in configuration is invalid")
return False
# Connect to ISY controller.
@@ -176,7 +177,7 @@ async def async_setup_entry(
await _async_get_or_create_isy_device_in_registry(hass, entry, isy)
# Load platforms for the devices in the ISY controller that we support.
- for platform in SUPPORTED_PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)
@@ -248,7 +249,7 @@ async def async_unload_entry(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, platform)
- for platform in SUPPORTED_PLATFORMS
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py
index 6355a9bcece144..57b134e0900606 100644
--- a/homeassistant/components/isy994/binary_sensor.py
+++ b/homeassistant/components/isy994/binary_sensor.py
@@ -1,6 +1,8 @@
"""Support for ISY994 binary sensors."""
+from __future__ import annotations
+
from datetime import timedelta
-from typing import Callable, Union
+from typing import Callable
from pyisy.constants import (
CMD_OFF,
@@ -127,7 +129,7 @@ async def async_setup_entry(
if (
device_class == DEVICE_CLASS_MOTION
and device_type is not None
- and any([device_type.startswith(t) for t in TYPE_INSTEON_MOTION])
+ and any(device_type.startswith(t) for t in TYPE_INSTEON_MOTION)
):
# Special cases for Insteon Motion Sensors I & II:
# Some subnodes never report status until activated, so
@@ -173,7 +175,7 @@ async def async_setup_entry(
async_add_entities(devices)
-def _detect_device_type_and_class(node: Union[Group, Node]) -> (str, str):
+def _detect_device_type_and_class(node: Group | Node) -> (str, str):
try:
device_type = node.type
except AttributeError:
@@ -194,10 +196,8 @@ def _detect_device_type_and_class(node: Union[Group, Node]) -> (str, str):
# Other devices (incl Insteon.)
for device_class in [*BINARY_SENSOR_DEVICE_TYPES_ISY]:
if any(
- [
- device_type.startswith(t)
- for t in set(BINARY_SENSOR_DEVICE_TYPES_ISY[device_class])
- ]
+ device_type.startswith(t)
+ for t in set(BINARY_SENSOR_DEVICE_TYPES_ISY[device_class])
):
return device_class, device_type
return (None, device_type)
@@ -281,15 +281,17 @@ def add_negative_node(self, child) -> None:
"""
self._negative_node = child
- if self._negative_node.status != ISY_VALUE_UNKNOWN:
- # If the negative node has a value, it means the negative node is
- # in use for this device. Next we need to check to see if the
- # negative and positive nodes disagree on the state (both ON or
- # both OFF).
- if self._negative_node.status == self._node.status:
- # The states disagree, therefore we cannot determine the state
- # of the sensor until we receive our first ON event.
- self._computed_state = None
+ # If the negative node has a value, it means the negative node is
+ # in use for this device. Next we need to check to see if the
+ # negative and positive nodes disagree on the state (both ON or
+ # both OFF).
+ if (
+ self._negative_node.status != ISY_VALUE_UNKNOWN
+ and self._negative_node.status == self._node.status
+ ):
+ # The states disagree, therefore we cannot determine the state
+ # of the sensor until we receive our first ON event.
+ self._computed_state = None
def _negative_node_control_handler(self, event: object) -> None:
"""Handle an "On" control event from the "negative" node."""
@@ -457,9 +459,9 @@ def device_class(self) -> str:
return DEVICE_CLASS_BATTERY
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Get the state attributes for the device."""
- attr = super().device_state_attributes
+ attr = super().extra_state_attributes
attr["parent_entity_id"] = self._parent_device.entity_id
return attr
diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py
index bb98c3d31bfb9d..2c9aa52b3a7406 100644
--- a/homeassistant/components/isy994/climate.py
+++ b/homeassistant/components/isy994/climate.py
@@ -1,5 +1,7 @@
"""Support for Insteon Thermostats via ISY994 Platform."""
-from typing import Callable, List, Optional
+from __future__ import annotations
+
+from typing import Callable
from pyisy.constants import (
CMD_CLIMATE_FAN_SETTING,
@@ -114,7 +116,7 @@ def temperature_unit(self) -> str:
return TEMP_FAHRENHEIT
@property
- def current_humidity(self) -> Optional[int]:
+ def current_humidity(self) -> int | None:
"""Return the current humidity."""
humidity = self._node.aux_properties.get(PROP_HUMIDITY)
if not humidity:
@@ -122,7 +124,7 @@ def current_humidity(self) -> Optional[int]:
return int(humidity.value)
@property
- def hvac_mode(self) -> Optional[str]:
+ def hvac_mode(self) -> str | None:
"""Return hvac operation ie. heat, cool mode."""
hvac_mode = self._node.aux_properties.get(CMD_CLIMATE_MODE)
if not hvac_mode:
@@ -140,12 +142,12 @@ def hvac_mode(self) -> Optional[str]:
return UOM_TO_STATES[uom].get(hvac_mode.value)
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes."""
return ISY_HVAC_MODES
@property
- def hvac_action(self) -> Optional[str]:
+ def hvac_action(self) -> str | None:
"""Return the current running hvac operation if supported."""
hvac_action = self._node.aux_properties.get(PROP_HEAT_COOL_STATE)
if not hvac_action:
@@ -153,19 +155,19 @@ def hvac_action(self) -> Optional[str]:
return UOM_TO_STATES[UOM_HVAC_ACTIONS].get(hvac_action.value)
@property
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
return convert_isy_value_to_hass(
self._node.status, self._uom, self._node.prec, 1
)
@property
- def target_temperature_step(self) -> Optional[float]:
+ def target_temperature_step(self) -> float | None:
"""Return the supported step of target temperature."""
return 1.0
@property
- def target_temperature(self) -> Optional[float]:
+ def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
if self.hvac_mode == HVAC_MODE_COOL:
return self.target_temperature_high
@@ -174,7 +176,7 @@ def target_temperature(self) -> Optional[float]:
return None
@property
- def target_temperature_high(self) -> Optional[float]:
+ def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach."""
target = self._node.aux_properties.get(PROP_SETPOINT_COOL)
if not target:
@@ -182,7 +184,7 @@ def target_temperature_high(self) -> Optional[float]:
return convert_isy_value_to_hass(target.value, target.uom, target.prec, 1)
@property
- def target_temperature_low(self) -> Optional[float]:
+ def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach."""
target = self._node.aux_properties.get(PROP_SETPOINT_HEAT)
if not target:
diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py
index 6049b8c6ec323c..41e91e2ca6b7e6 100644
--- a/homeassistant/components/isy994/config_flow.py
+++ b/homeassistant/components/isy994/config_flow.py
@@ -22,10 +22,10 @@
DEFAULT_SENSOR_STRING,
DEFAULT_TLS_VERSION,
DEFAULT_VAR_SENSOR_STRING,
+ DOMAIN,
ISY_URL_POSTFIX,
UDN_UUID_PREFIX,
)
-from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
@@ -60,7 +60,7 @@ async def validate_input(hass: core.HomeAssistant, data):
https = True
port = host.port or 443
else:
- _LOGGER.error("isy994 host value in configuration is invalid")
+ _LOGGER.error("The isy994 host value in configuration is invalid")
raise InvalidHost
# Connect to ISY controller.
@@ -168,7 +168,6 @@ async def async_step_ssdp(self, discovery_info):
CONF_HOST: url,
}
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = self.discovered_conf
return await self.async_step_user()
diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py
index ef75a974377e5f..9fdef92c84f990 100644
--- a/homeassistant/components/isy994/const.py
+++ b/homeassistant/components/isy994/const.py
@@ -129,7 +129,7 @@
KEY_ACTIONS = "actions"
KEY_STATUS = "status"
-SUPPORTED_PLATFORMS = [BINARY_SENSOR, SENSOR, LOCK, FAN, COVER, LIGHT, SWITCH, CLIMATE]
+PLATFORMS = [BINARY_SENSOR, SENSOR, LOCK, FAN, COVER, LIGHT, SWITCH, CLIMATE]
SUPPORTED_PROGRAM_PLATFORMS = [BINARY_SENSOR, LOCK, FAN, COVER, SWITCH]
SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"]
diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py
index 95bd43facde0b6..f3dbe579dd85d9 100644
--- a/homeassistant/components/isy994/entity.py
+++ b/homeassistant/components/isy994/entity.py
@@ -100,6 +100,8 @@ def device_info(self):
f"ProductID:{node.zwave_props.product_id}"
)
# Note: sw_version is not exposed by the ISY for the individual devices.
+ if hasattr(node, "folder") and node.folder is not None:
+ device_info["suggested_area"] = node.folder
return device_info
@@ -132,7 +134,7 @@ class ISYNodeEntity(ISYEntity):
"""Representation of a ISY Nodebase (Node/Group) entity."""
@property
- def device_state_attributes(self) -> Dict:
+ def extra_state_attributes(self) -> Dict:
"""Get the state attributes for the device.
The 'aux_properties' in the pyisy Node class are combined with the
@@ -184,7 +186,7 @@ def __init__(self, name: str, status, actions=None) -> None:
self._actions = actions
@property
- def device_state_attributes(self) -> Dict:
+ def extra_state_attributes(self) -> Dict:
"""Get the state attributes for the device."""
attr = {}
if self._actions:
diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py
index 74ed477d3a7cdb..183d4b31d3b55e 100644
--- a/homeassistant/components/isy994/fan.py
+++ b/homeassistant/components/isy994/fan.py
@@ -1,13 +1,16 @@
"""Support for ISY994 fans."""
+from __future__ import annotations
+
import math
from typing import Callable
-from pyisy.constants import ISY_VALUE_UNKNOWN
+from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_INSTEON
from homeassistant.components.fan import DOMAIN as FAN, SUPPORT_SET_SPEED, FanEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.percentage import (
+ int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@@ -42,12 +45,19 @@ class ISYFanEntity(ISYNodeEntity, FanEntity):
"""Representation of an ISY994 fan device."""
@property
- def percentage(self) -> str:
+ def percentage(self) -> int | None:
"""Return the current speed percentage."""
if self._node.status == ISY_VALUE_UNKNOWN:
return None
return ranged_value_to_percentage(SPEED_RANGE, self._node.status)
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ if self._node.protocol == PROTO_INSTEON:
+ return 3
+ return int_states_in_range(SPEED_RANGE)
+
@property
def is_on(self) -> bool:
"""Get if the fan is on."""
@@ -89,12 +99,19 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity):
"""Representation of an ISY994 fan program."""
@property
- def percentage(self) -> str:
+ def percentage(self) -> int | None:
"""Return the current speed percentage."""
if self._node.status == ISY_VALUE_UNKNOWN:
return None
return ranged_value_to_percentage(SPEED_RANGE, self._node.status)
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ if self._node.protocol == PROTO_INSTEON:
+ return 3
+ return int_states_in_range(SPEED_RANGE)
+
@property
def is_on(self) -> bool:
"""Get if the fan is on."""
diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py
index e1ab689eb7a0ff..81a74430d3ad87 100644
--- a/homeassistant/components/isy994/helpers.py
+++ b/homeassistant/components/isy994/helpers.py
@@ -1,5 +1,7 @@
"""Sorting helpers for ISY994 device classifications."""
-from typing import Any, List, Optional, Union
+from __future__ import annotations
+
+from typing import Any
from pyisy.constants import (
ISY_VALUE_UNKNOWN,
@@ -38,12 +40,12 @@
KEY_ACTIONS,
KEY_STATUS,
NODE_FILTERS,
+ PLATFORMS,
SUBNODE_CLIMATE_COOL,
SUBNODE_CLIMATE_HEAT,
SUBNODE_EZIO2X4_SENSORS,
SUBNODE_FANLINC_LIGHT,
SUBNODE_IOLINC_RELAY,
- SUPPORTED_PLATFORMS,
SUPPORTED_PROGRAM_PLATFORMS,
TYPE_CATEGORY_SENSOR_ACTUATORS,
TYPE_EZIO2X4,
@@ -56,7 +58,7 @@
def _check_for_node_def(
- hass_isy_data: dict, node: Union[Group, Node], single_platform: str = None
+ hass_isy_data: dict, node: Group | Node, single_platform: str = None
) -> bool:
"""Check if the node matches the node_def_id for any platforms.
@@ -69,7 +71,7 @@ def _check_for_node_def(
node_def_id = node.node_def_id
- platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform]
+ platforms = PLATFORMS if not single_platform else [single_platform]
for platform in platforms:
if node_def_id in NODE_FILTERS[platform][FILTER_NODE_DEF_ID]:
hass_isy_data[ISY994_NODES][platform].append(node)
@@ -79,7 +81,7 @@ def _check_for_node_def(
def _check_for_insteon_type(
- hass_isy_data: dict, node: Union[Group, Node], single_platform: str = None
+ hass_isy_data: dict, node: Group | Node, single_platform: str = None
) -> bool:
"""Check if the node matches the Insteon type for any platforms.
@@ -94,13 +96,11 @@ def _check_for_insteon_type(
return False
device_type = node.type
- platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform]
+ platforms = PLATFORMS if not single_platform else [single_platform]
for platform in platforms:
if any(
- [
- device_type.startswith(t)
- for t in set(NODE_FILTERS[platform][FILTER_INSTEON_TYPE])
- ]
+ device_type.startswith(t)
+ for t in set(NODE_FILTERS[platform][FILTER_INSTEON_TYPE])
):
# Hacky special-cases for certain devices with different platforms
@@ -146,7 +146,7 @@ def _check_for_insteon_type(
def _check_for_zwave_cat(
- hass_isy_data: dict, node: Union[Group, Node], single_platform: str = None
+ hass_isy_data: dict, node: Group | Node, single_platform: str = None
) -> bool:
"""Check if the node matches the ISY Z-Wave Category for any platforms.
@@ -161,13 +161,11 @@ def _check_for_zwave_cat(
return False
device_type = node.zwave_props.category
- platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform]
+ platforms = PLATFORMS if not single_platform else [single_platform]
for platform in platforms:
if any(
- [
- device_type.startswith(t)
- for t in set(NODE_FILTERS[platform][FILTER_ZWAVE_CAT])
- ]
+ device_type.startswith(t)
+ for t in set(NODE_FILTERS[platform][FILTER_ZWAVE_CAT])
):
hass_isy_data[ISY994_NODES][platform].append(node)
@@ -178,7 +176,7 @@ def _check_for_zwave_cat(
def _check_for_uom_id(
hass_isy_data: dict,
- node: Union[Group, Node],
+ node: Group | Node,
single_platform: str = None,
uom_list: list = None,
) -> bool:
@@ -202,7 +200,7 @@ def _check_for_uom_id(
return True
return False
- platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform]
+ platforms = PLATFORMS if not single_platform else [single_platform]
for platform in platforms:
if node_uom in NODE_FILTERS[platform][FILTER_UOM]:
hass_isy_data[ISY994_NODES][platform].append(node)
@@ -213,7 +211,7 @@ def _check_for_uom_id(
def _check_for_states_in_uom(
hass_isy_data: dict,
- node: Union[Group, Node],
+ node: Group | Node,
single_platform: str = None,
states_list: list = None,
) -> bool:
@@ -239,7 +237,7 @@ def _check_for_states_in_uom(
return True
return False
- platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform]
+ platforms = PLATFORMS if not single_platform else [single_platform]
for platform in platforms:
if node_uom == set(NODE_FILTERS[platform][FILTER_STATES]):
hass_isy_data[ISY994_NODES][platform].append(node)
@@ -248,7 +246,7 @@ def _check_for_states_in_uom(
return False
-def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Union[Group, Node]) -> bool:
+def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Group | Node) -> bool:
"""Determine if the given sensor node should be a binary_sensor."""
if _check_for_node_def(hass_isy_data, node, single_platform=BINARY_SENSOR):
return True
@@ -328,7 +326,7 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None:
actions = None
status = entity_folder.get_by_name(KEY_STATUS)
- if not status or not status.protocol == PROTO_PROGRAM:
+ if not status or status.protocol != PROTO_PROGRAM:
_LOGGER.warning(
"Program %s entity '%s' not loaded, invalid/missing status program",
platform,
@@ -338,7 +336,7 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None:
if platform != BINARY_SENSOR:
actions = entity_folder.get_by_name(KEY_ACTIONS)
- if not actions or not actions.protocol == PROTO_PROGRAM:
+ if not actions or actions.protocol != PROTO_PROGRAM:
_LOGGER.warning(
"Program %s entity '%s' not loaded, invalid/missing actions program",
platform,
@@ -368,7 +366,7 @@ def _categorize_variables(
async def migrate_old_unique_ids(
- hass: HomeAssistantType, platform: str, devices: Optional[List[Any]]
+ hass: HomeAssistantType, platform: str, devices: list[Any] | None
) -> None:
"""Migrate to new controller-specific unique ids."""
registry = await async_get_registry(hass)
@@ -400,11 +398,11 @@ async def migrate_old_unique_ids(
def convert_isy_value_to_hass(
- value: Union[int, float, None],
+ value: int | float | None,
uom: str,
- precision: Union[int, str],
- fallback_precision: Optional[int] = None,
-) -> Union[float, int]:
+ precision: int | str,
+ fallback_precision: int | None = None,
+) -> float | int:
"""Fix ISY Reported Values.
ISY provides float values as an integer and precision component.
diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py
index 7ff44863f6b6ab..7f35e96acaf57b 100644
--- a/homeassistant/components/isy994/light.py
+++ b/homeassistant/components/isy994/light.py
@@ -1,5 +1,7 @@
"""Support for ISY994 lights."""
-from typing import Callable, Dict
+from __future__ import annotations
+
+from typing import Callable
from pyisy.constants import ISY_VALUE_UNKNOWN
@@ -98,9 +100,9 @@ def turn_on(self, brightness=None, **kwargs) -> None:
_LOGGER.debug("Unable to turn on light")
@property
- def device_state_attributes(self) -> Dict:
+ def extra_state_attributes(self) -> dict:
"""Return the light attributes."""
- attribs = super().device_state_attributes
+ attribs = super().extra_state_attributes
attribs[ATTR_LAST_BRIGHTNESS] = self._last_brightness
return attribs
diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json
index 9e22b3533d7a5b..3769cc328db36f 100644
--- a/homeassistant/components/isy994/manifest.json
+++ b/homeassistant/components/isy994/manifest.json
@@ -2,7 +2,7 @@
"domain": "isy994",
"name": "Universal Devices ISY994",
"documentation": "https://www.home-assistant.io/integrations/isy994",
- "requirements": ["pyisy==2.1.0"],
+ "requirements": ["pyisy==2.1.1"],
"codeowners": ["@bdraco", "@shbatm"],
"config_flow": true,
"ssdp": [
diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py
index d3d192b1b3b93c..2927fbb62b1596 100644
--- a/homeassistant/components/isy994/sensor.py
+++ b/homeassistant/components/isy994/sensor.py
@@ -1,9 +1,11 @@
"""Support for ISY994 sensors."""
-from typing import Callable, Dict, Union
+from __future__ import annotations
+
+from typing import Callable
from pyisy.constants import ISY_VALUE_UNKNOWN
-from homeassistant.components.sensor import DOMAIN as SENSOR
+from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.helpers.typing import HomeAssistantType
@@ -43,11 +45,11 @@ async def async_setup_entry(
async_add_entities(devices)
-class ISYSensorEntity(ISYNodeEntity):
+class ISYSensorEntity(ISYNodeEntity, SensorEntity):
"""Representation of an ISY994 sensor device."""
@property
- def raw_unit_of_measurement(self) -> Union[dict, str]:
+ def raw_unit_of_measurement(self) -> dict | str:
"""Get the raw unit of measurement for the ISY994 sensor device."""
uom = self._node.uom
@@ -103,7 +105,7 @@ def unit_of_measurement(self) -> str:
return raw_units
-class ISYSensorVariableEntity(ISYEntity):
+class ISYSensorVariableEntity(ISYEntity, SensorEntity):
"""Representation of an ISY994 variable as a sensor device."""
def __init__(self, vname: str, vobj: object) -> None:
@@ -117,7 +119,7 @@ def state(self):
return convert_isy_value_to_hass(self._node.status, "", self._node.prec)
@property
- def device_state_attributes(self) -> Dict:
+ def extra_state_attributes(self) -> dict:
"""Get the state attributes for the device."""
return {
"init_value": convert_isy_value_to_hass(
diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py
index 3e9e0ae1d290cf..39966a9d9947de 100644
--- a/homeassistant/components/isy994/services.py
+++ b/homeassistant/components/isy994/services.py
@@ -27,7 +27,7 @@
ISY994_NODES,
ISY994_PROGRAMS,
ISY994_VARIABLES,
- SUPPORTED_PLATFORMS,
+ PLATFORMS,
SUPPORTED_PROGRAM_PLATFORMS,
)
@@ -174,7 +174,7 @@ async def async_system_query_service_handler(service):
for config_entry_id in hass.data[DOMAIN]:
isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY]
- if isy_name and not isy_name == isy.configuration["name"]:
+ if isy_name and isy_name != isy.configuration["name"]:
continue
# If an address is provided, make sure we query the correct ISY.
# Otherwise, query the whole system on all ISY's connected.
@@ -199,7 +199,7 @@ async def async_run_network_resource_service_handler(service):
for config_entry_id in hass.data[DOMAIN]:
isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY]
- if isy_name and not isy_name == isy.configuration["name"]:
+ if isy_name and isy_name != isy.configuration["name"]:
continue
if not hasattr(isy, "networking") or isy.networking is None:
continue
@@ -224,7 +224,7 @@ async def async_send_program_command_service_handler(service):
for config_entry_id in hass.data[DOMAIN]:
isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY]
- if isy_name and not isy_name == isy.configuration["name"]:
+ if isy_name and isy_name != isy.configuration["name"]:
continue
program = None
if address:
@@ -247,7 +247,7 @@ async def async_set_variable_service_handler(service):
for config_entry_id in hass.data[DOMAIN]:
isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY]
- if isy_name and not isy_name == isy.configuration["name"]:
+ if isy_name and isy_name != isy.configuration["name"]:
continue
variable = None
if name:
@@ -279,7 +279,7 @@ async def async_cleanup_registry_entries(service) -> None:
hass_isy_data = hass.data[DOMAIN][config_entry_id]
uuid = hass_isy_data[ISY994_ISY].configuration["uuid"]
- for platform in SUPPORTED_PLATFORMS:
+ for platform in PLATFORMS:
for node in hass_isy_data[ISY994_NODES][platform]:
if hasattr(node, "address"):
current_unique_ids.append(f"{uuid}_{node.address}")
diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json
index 99d11e5d6c97b2..0a4758e1156dee 100644
--- a/homeassistant/components/isy994/translations/de.json
+++ b/homeassistant/components/isy994/translations/de.json
@@ -9,6 +9,7 @@
"invalid_host": "Der Hosteintrag hatte nicht das vollst\u00e4ndige URL-Format, z. B. http://192.168.10.100:80",
"unknown": "Unerwarteter Fehler"
},
+ "flow_title": "Universalger\u00e4te ISY994 {name} ({host})",
"step": {
"user": {
"data": {
@@ -18,7 +19,7 @@
"username": "Benutzername"
},
"description": "Der Hosteintrag muss im vollst\u00e4ndigen URL-Format vorliegen, z. B. http://192.168.10.100:80",
- "title": "Stellen Sie eine Verbindung zu Ihrem ISY994 her"
+ "title": "Stelle eine Verbindung zu deinem ISY994 her"
}
}
},
@@ -27,8 +28,11 @@
"init": {
"data": {
"ignore_string": "Zeichenfolge ignorieren",
- "restore_light_state": "Lichthelligkeit wiederherstellen"
+ "restore_light_state": "Lichthelligkeit wiederherstellen",
+ "sensor_string": "Knoten Sensor String",
+ "variable_sensor_string": "Variabler Sensor String"
},
+ "description": "Stelle die Optionen f\u00fcr die ISY-Integration ein: \n - Node Sensor String: Jedes Ger\u00e4t oder jeder Ordner, der 'Node Sensor String' im Namen enth\u00e4lt, wird als Sensor oder bin\u00e4rer Sensor behandelt. \n - String ignorieren: Jedes Ger\u00e4t mit 'Ignore String' im Namen wird ignoriert. \n - Variable Sensor Zeichenfolge: Jede Variable, die 'Variable Sensor String' im Namen enth\u00e4lt, wird als Sensor hinzugef\u00fcgt. \n - Lichthelligkeit wiederherstellen: Wenn diese Option aktiviert ist, wird beim Einschalten eines Lichts die vorherige Helligkeit wiederhergestellt und nicht der integrierte Ein-Pegel des Ger\u00e4ts.",
"title": "ISY994 Optionen"
}
}
diff --git a/homeassistant/components/isy994/translations/hu.json b/homeassistant/components/isy994/translations/hu.json
index 6177c39b231a9a..427a51157bf78a 100644
--- a/homeassistant/components/isy994/translations/hu.json
+++ b/homeassistant/components/isy994/translations/hu.json
@@ -6,15 +6,24 @@
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
- "unknown": "V\u00e1ratlan hiba"
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
+ "flow_title": "Universal Devices ISY994 {name} ({host})",
"step": {
"user": {
"data": {
+ "host": "URL",
"password": "Jelsz\u00f3",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
}
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "ISY994 Be\u00e1ll\u00edt\u00e1sok"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/isy994/translations/id.json b/homeassistant/components/isy994/translations/id.json
new file mode 100644
index 00000000000000..fec6d1090b044b
--- /dev/null
+++ b/homeassistant/components/isy994/translations/id.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "invalid_host": "Entri host tidak dalam format URL lengkap, misalnya, http://192.168.10.100:80",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "flow_title": "Universal Devices ISY994 {name} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "URL",
+ "password": "Kata Sandi",
+ "tls": "Versi TLS dari pengontrol ISY.",
+ "username": "Nama Pengguna"
+ },
+ "description": "Entri host harus dalam format URL lengkap, misalnya, http://192.168.10.100:80",
+ "title": "Hubungkan ke ISY994 Anda"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "ignore_string": "Abaikan String",
+ "restore_light_state": "Pulihkan Kecerahan Cahaya",
+ "sensor_string": "String Sensor Node",
+ "variable_sensor_string": "String Sensor Variabel"
+ },
+ "description": "Mengatur opsi untuk Integrasi ISY: \n \u2022 String Sensor Node: Setiap perangkat atau folder yang berisi 'String Sensor Node' dalam nama akan diperlakukan sebagai sensor atau sensor biner. \n \u2022 Abaikan String: Setiap perangkat dengan 'Abaikan String' dalam nama akan diabaikan. \n \u2022 String Sensor Variabel: Variabel apa pun yang berisi 'String Sensor Variabel' akan ditambahkan sebagai sensor. \n \u2022 Pulihkan Kecerahan Cahaya: Jika diaktifkan, kecerahan sebelumnya akan dipulihkan saat menyalakan lampu alih-alih bawaan perangkat On-Level.",
+ "title": "Opsi ISY994"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/isy994/translations/ko.json b/homeassistant/components/isy994/translations/ko.json
index b8c80f1ee74a90..29829e066b9c01 100644
--- a/homeassistant/components/isy994/translations/ko.json
+++ b/homeassistant/components/isy994/translations/ko.json
@@ -19,7 +19,7 @@
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
},
"description": "\ud638\uc2a4\ud2b8 \ud56d\ubaa9\uc740 \uc644\uc804\ud55c URL \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: http://192.168.10.100:80",
- "title": "ISY994 \uc5d0 \uc5f0\uacb0\ud558\uae30"
+ "title": "ISY994\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
},
diff --git a/homeassistant/components/isy994/translations/nl.json b/homeassistant/components/isy994/translations/nl.json
index d263815158ae98..9fed7b8c99c950 100644
--- a/homeassistant/components/isy994/translations/nl.json
+++ b/homeassistant/components/isy994/translations/nl.json
@@ -29,7 +29,8 @@
"data": {
"ignore_string": "Tekenreeks negeren",
"restore_light_state": "Herstel lichthelderheid",
- "sensor_string": "Node Sensor String"
+ "sensor_string": "Node Sensor String",
+ "variable_sensor_string": "Variabele sensor string"
},
"description": "Stel de opties in voor de ISY-integratie:\n \u2022 Node Sensor String: elk apparaat of elke map die 'Node Sensor String' in de naam bevat, wordt behandeld als een sensor of binaire sensor.\n \u2022 Ignore String: elk apparaat met 'Ignore String' in de naam wordt genegeerd.\n \u2022 Variabele sensorreeks: elke variabele die 'Variabele sensorreeks' bevat, wordt als sensor toegevoegd.\n \u2022 Lichthelderheid herstellen: indien ingeschakeld, wordt de vorige helderheid hersteld wanneer u een lamp inschakelt in plaats van het ingebouwde Aan-niveau van het apparaat.",
"title": "ISY994-opties"
diff --git a/homeassistant/components/isy994/translations/ru.json b/homeassistant/components/isy994/translations/ru.json
index c0a658423c6729..50aa75cab37e36 100644
--- a/homeassistant/components/isy994/translations/ru.json
+++ b/homeassistant/components/isy994/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\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": "URL-\u0430\u0434\u0440\u0435\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 'http://192.168.10.100:80').",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
@@ -16,7 +16,7 @@
"host": "URL-\u0430\u0434\u0440\u0435\u0441",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"tls": "\u0412\u0435\u0440\u0441\u0438\u044f TLS \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 URL-\u0430\u0434\u0440\u0435\u0441 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 'http://192.168.10.100:80').",
"title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443"
diff --git a/homeassistant/components/isy994/translations/tr.json b/homeassistant/components/isy994/translations/tr.json
new file mode 100644
index 00000000000000..d1423202fe052b
--- /dev/null
+++ b/homeassistant/components/isy994/translations/tr.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "URL",
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "variable_sensor_string": "De\u011fi\u015fken Sens\u00f6r Dizesi"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/isy994/translations/uk.json b/homeassistant/components/isy994/translations/uk.json
new file mode 100644
index 00000000000000..c874b8654f58dc
--- /dev/null
+++ b/homeassistant/components/isy994/translations/uk.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "invalid_host": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043c\u0430\u0454 \u0431\u0443\u0442\u0438 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 'http://192.168.10.100:80').",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "flow_title": "Universal Devices ISY994 {name} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "URL-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "tls": "\u0412\u0435\u0440\u0441\u0456\u044f TLS \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u0412\u043a\u0430\u0436\u0456\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441\u0443 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 'http://192.168.10.100:80').",
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "ignore_string": "\u0406\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438",
+ "restore_light_state": "\u0412\u0456\u0434\u043d\u043e\u0432\u043b\u044e\u0432\u0430\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c \u0441\u0432\u0456\u0442\u043b\u0430",
+ "sensor_string": "\u0406\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0432\u0443\u0437\u043e\u043b \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440",
+ "variable_sensor_string": "\u0406\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0437\u043c\u0456\u043d\u043d\u0443 \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440"
+ },
+ "description": "\u041e\u043f\u0438\u0441 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432:\n \u2022 \u0406\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0432\u0443\u0437\u043e\u043b \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440: \u0431\u0443\u0434\u044c-\u044f\u043a\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0430\u0431\u043e \u043f\u0430\u043f\u043a\u0430, \u0432 \u0456\u043c\u0435\u043d\u0456 \u044f\u043a\u043e\u0457 \u043c\u0456\u0441\u0442\u0438\u0442\u044c\u0441\u044f \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0440\u044f\u0434\u043e\u043a, \u0431\u0443\u0434\u0435 \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u043e \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440 \u0430\u0431\u043e \u0431\u0456\u043d\u0430\u0440\u043d\u0438\u0439 \u0441\u0435\u043d\u0441\u043e\u0440.\n \u2022 \u0406\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0437\u043c\u0456\u043d\u043d\u0443 \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440: \u0431\u0443\u0434\u044c-\u044f\u043a\u0430 \u0437\u043c\u0456\u043d\u043d\u0430, \u044f\u043a\u0430 \u043c\u0456\u0441\u0442\u0438\u0442\u044c \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0440\u044f\u0434\u043e\u043a, \u0431\u0443\u0434\u0435 \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u0430 \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440.\n \u2022 \u0406\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438: \u0431\u0443\u0434\u044c-\u044f\u043a\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u0432 \u0456\u043c\u0435\u043d\u0456 \u044f\u043a\u043e\u0433\u043e \u043c\u0456\u0441\u0442\u0438\u0442\u044c\u0441\u044f \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0440\u044f\u0434\u043e\u043a, \u0431\u0443\u0434\u0435 \u0456\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f.\n \u2022 \u0412\u0456\u0434\u043d\u043e\u0432\u043b\u044e\u0432\u0430\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c \u0441\u0432\u0456\u0442\u043b\u0430: \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456 \u043e\u0441\u0432\u0456\u0442\u043b\u0435\u043d\u043d\u044f \u0431\u0443\u0434\u0435 \u0432\u0456\u0434\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u044f\u0441\u043a\u0440\u0430\u0432\u043e\u0441\u0442\u0456, \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0435 \u0434\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f ISY994"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py
index 443a80298f1310..d509896e841b4e 100644
--- a/homeassistant/components/izone/climate.py
+++ b/homeassistant/components/izone/climate.py
@@ -1,8 +1,10 @@
"""Support for the iZone HVAC."""
+from __future__ import annotations
+
import logging
-from typing import List, Optional
from pizone import Controller, Zone
+import voluptuous as vol
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
@@ -30,6 +32,7 @@
TEMP_CELSIUS,
)
from homeassistant.core import callback
+from homeassistant.helpers import entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.temperature import display_temp as show_temp
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
@@ -54,6 +57,17 @@
Controller.Fan.AUTO: FAN_AUTO,
}
+ATTR_AIRFLOW = "airflow"
+
+IZONE_SERVICE_AIRFLOW_MIN = "airflow_min"
+IZONE_SERVICE_AIRFLOW_MAX = "airflow_max"
+
+IZONE_SERVICE_AIRFLOW_SCHEMA = {
+ vol.Required(ATTR_AIRFLOW): vol.All(
+ vol.Coerce(int), vol.Range(min=0, max=100), msg="invalid airflow"
+ ),
+}
+
async def async_setup_entry(
hass: HomeAssistantType, config: ConfigType, async_add_entities
@@ -64,7 +78,7 @@ async def async_setup_entry(
@callback
def init_controller(ctrl: Controller):
"""Register the controller device and the containing zones."""
- conf = hass.data.get(DATA_CONFIG) # type: ConfigType
+ conf: ConfigType = hass.data.get(DATA_CONFIG)
# Filter out any entities excluded in the config file
if conf and ctrl.device_uid in conf[CONF_EXCLUDE]:
@@ -83,6 +97,18 @@ def init_controller(ctrl: Controller):
# connect to register any further components
async_dispatcher_connect(hass, DISPATCH_CONTROLLER_DISCOVERED, init_controller)
+ platform = entity_platform.current_platform.get()
+ platform.async_register_entity_service(
+ IZONE_SERVICE_AIRFLOW_MIN,
+ IZONE_SERVICE_AIRFLOW_SCHEMA,
+ "async_set_airflow_min",
+ )
+ platform.async_register_entity_service(
+ IZONE_SERVICE_AIRFLOW_MAX,
+ IZONE_SERVICE_AIRFLOW_SCHEMA,
+ "async_set_airflow_max",
+ )
+
return True
@@ -110,6 +136,8 @@ def __init__(self, controller: Controller) -> None:
self._supported_features = SUPPORT_FAN_MODE
+ # If mode RAS, or mode master with CtrlZone 13 then can set master temperature,
+ # otherwise the unit determines which zone to use as target. See interface manual p. 8
if (
controller.ras_mode == "master" and controller.zone_ctrl == 13
) or controller.ras_mode == "RAS":
@@ -254,7 +282,7 @@ def precision(self) -> float:
return PRECISION_TENTHS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the optional state attributes."""
return {
"supply_temperature": show_temp(
@@ -269,6 +297,16 @@ def device_state_attributes(self):
self.temperature_unit,
PRECISION_HALVES,
),
+ "control_zone": self._controller.zone_ctrl,
+ "control_zone_name": self.control_zone_name,
+ # Feature SUPPORT_TARGET_TEMPERATURE controls both displaying target temp & setting it
+ # As the feature is turned off for zone control, report target temp as extra state attribute
+ "control_zone_setpoint": show_temp(
+ self.hass,
+ self.control_zone_setpoint,
+ self.temperature_unit,
+ PRECISION_HALVES,
+ ),
}
@property
@@ -286,7 +324,7 @@ def hvac_mode(self) -> str:
@property
@_return_on_connection_error([])
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available operation modes."""
if self._controller.free_air:
return [HVAC_MODE_OFF, HVAC_MODE_FAN_ONLY]
@@ -308,19 +346,41 @@ def preset_modes(self):
@property
@_return_on_connection_error()
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
if self._controller.mode == Controller.Mode.FREE_AIR:
return self._controller.temp_supply
return self._controller.temp_return
@property
- @_return_on_connection_error()
- def target_temperature(self) -> Optional[float]:
- """Return the temperature we try to reach."""
- if not self._supported_features & SUPPORT_TARGET_TEMPERATURE:
+ def control_zone_name(self):
+ """Return the zone that currently controls the AC unit (if target temp not set by controller)."""
+ if self._supported_features & SUPPORT_TARGET_TEMPERATURE:
+ return None
+ zone_ctrl = self._controller.zone_ctrl
+ zone = next((z for z in self.zones.values() if z.zone_index == zone_ctrl), None)
+ if zone is None:
return None
- return self._controller.temp_setpoint
+ return zone.name
+
+ @property
+ def control_zone_setpoint(self) -> float | None:
+ """Return the temperature setpoint of the zone that currently controls the AC unit (if target temp not set by controller)."""
+ if self._supported_features & SUPPORT_TARGET_TEMPERATURE:
+ return None
+ zone_ctrl = self._controller.zone_ctrl
+ zone = next((z for z in self.zones.values() if z.zone_index == zone_ctrl), None)
+ if zone is None:
+ return None
+ return zone.target_temperature
+
+ @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)."""
+ if self._supported_features & SUPPORT_TARGET_TEMPERATURE:
+ return self._controller.temp_setpoint
+ return self.control_zone_setpoint
@property
def supply_temperature(self) -> float:
@@ -328,17 +388,17 @@ def supply_temperature(self) -> float:
return self._controller.temp_supply
@property
- def target_temperature_step(self) -> Optional[float]:
+ def target_temperature_step(self) -> float | None:
"""Return the supported step of target temperature."""
return 0.5
@property
- def fan_mode(self) -> Optional[str]:
+ def fan_mode(self) -> str | None:
"""Return the fan setting."""
return _IZONE_FAN_TO_HA[self._controller.fan]
@property
- def fan_modes(self) -> Optional[List[str]]:
+ def fan_modes(self) -> list[str] | None:
"""Return the list of available fan modes."""
return list(self._fan_to_pizone)
@@ -538,6 +598,30 @@ def max_temp(self):
"""Return the maximum temperature."""
return self._controller.max_temp
+ @property
+ def airflow_min(self):
+ """Return the minimum air flow."""
+ return self._zone.airflow_min
+
+ @property
+ def airflow_max(self):
+ """Return the maximum air flow."""
+ return self._zone.airflow_max
+
+ async def async_set_airflow_min(self, **kwargs):
+ """Set new airflow minimum."""
+ await self._controller.wrap_and_catch(
+ self._zone.set_airflow_min(int(kwargs[ATTR_AIRFLOW]))
+ )
+ self.async_write_ha_state()
+
+ async def async_set_airflow_max(self, **kwargs):
+ """Set new airflow maximum."""
+ await self._controller.wrap_and_catch(
+ self._zone.set_airflow_max(int(kwargs[ATTR_AIRFLOW]))
+ )
+ self.async_write_ha_state()
+
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
if self._zone.mode != Zone.Mode.AUTO:
@@ -569,3 +653,17 @@ async def async_turn_off(self):
"""Turn device off (close zone)."""
await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.CLOSE))
self.async_write_ha_state()
+
+ @property
+ def zone_index(self):
+ """Return the zone index for matching to CtrlZone."""
+ return self._zone.index
+
+ @property
+ def extra_state_attributes(self):
+ """Return the optional state attributes."""
+ return {
+ "airflow_max": self._zone.airflow_max,
+ "airflow_min": self._zone.airflow_min,
+ "zone_index": self.zone_index,
+ }
diff --git a/homeassistant/components/izone/config_flow.py b/homeassistant/components/izone/config_flow.py
index a64356051d0d65..bb647f6273c6bc 100644
--- a/homeassistant/components/izone/config_flow.py
+++ b/homeassistant/components/izone/config_flow.py
@@ -1,6 +1,7 @@
"""Config flow for izone."""
import asyncio
+from contextlib import suppress
import logging
from async_timeout import timeout
@@ -28,11 +29,9 @@ def dispatch_discovered(_):
disco = await async_start_discovery_service(hass)
- try:
+ with suppress(asyncio.TimeoutError):
async with timeout(TIMEOUT_DISCOVERY):
await controller_ready.wait()
- except asyncio.TimeoutError:
- pass
if not disco.pi_disco.controllers:
await async_stop_discovery_service(hass)
diff --git a/homeassistant/components/izone/discovery.py b/homeassistant/components/izone/discovery.py
index 7690600786e59f..2a4ad516af1463 100644
--- a/homeassistant/components/izone/discovery.py
+++ b/homeassistant/components/izone/discovery.py
@@ -1,7 +1,4 @@
"""Internal discovery service for iZone AC."""
-
-import logging
-
import pizone
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
@@ -18,8 +15,6 @@
DISPATCH_ZONE_UPDATE,
)
-_LOGGER = logging.getLogger(__name__)
-
class DiscoveryService(pizone.Listener):
"""Discovery data and interfacing with pizone library."""
diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json
index 479ac4969060da..bed7654b7e836d 100644
--- a/homeassistant/components/izone/manifest.json
+++ b/homeassistant/components/izone/manifest.json
@@ -2,7 +2,7 @@
"domain": "izone",
"name": "iZone",
"documentation": "https://www.home-assistant.io/integrations/izone",
- "requirements": ["python-izone==1.1.3"],
+ "requirements": ["python-izone==1.1.4"],
"codeowners": ["@Swamp-Ig"],
"config_flow": true,
"homekit": {
diff --git a/homeassistant/components/izone/services.yaml b/homeassistant/components/izone/services.yaml
new file mode 100644
index 00000000000000..d03ad66421a279
--- /dev/null
+++ b/homeassistant/components/izone/services.yaml
@@ -0,0 +1,40 @@
+airflow_min:
+ name: Set minimum airflow
+ description: Set the airflow minimum percent for a zone
+ target:
+ entity:
+ integration: izone
+ domain: climate
+ fields:
+ airflow:
+ name: Percent
+ description: Airflow percent in 5% increments
+ required: true
+ example: 95
+ selector:
+ number:
+ min: 0
+ max: 100
+ step: 5
+ unit_of_measurement: "%"
+ mode: slider
+airflow_max:
+ name: Set maximum airflow
+ description: Set the airflow maximum percent for a zone
+ target:
+ entity:
+ integration: izone
+ domain: climate
+ fields:
+ airflow:
+ name: Percent
+ description: Airflow percent in 5% increments
+ required: true
+ example: 95
+ selector:
+ number:
+ min: 0
+ max: 100
+ step: 5
+ unit_of_measurement: "%"
+ mode: slider
diff --git a/homeassistant/components/izone/translations/de.json b/homeassistant/components/izone/translations/de.json
index ea59cc39b27e05..f6e03c3af27c8c 100644
--- a/homeassistant/components/izone/translations/de.json
+++ b/homeassistant/components/izone/translations/de.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"no_devices_found": "Es wurden keine iZone-Ger\u00e4te im Netzwerk gefunden.",
- "single_instance_allowed": "Es ist nur eine einzige Konfiguration von iZone erforderlich."
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/izone/translations/hu.json b/homeassistant/components/izone/translations/hu.json
index 026093232a3b29..2d474986415595 100644
--- a/homeassistant/components/izone/translations/hu.json
+++ b/homeassistant/components/izone/translations/hu.json
@@ -1,5 +1,9 @@
{
"config": {
+ "abort": {
+ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
"step": {
"confirm": {
"description": "Szeretn\u00e9 be\u00e1ll\u00edtani az iZone-t?"
diff --git a/homeassistant/components/izone/translations/id.json b/homeassistant/components/izone/translations/id.json
new file mode 100644
index 00000000000000..208b59dc9ac1b6
--- /dev/null
+++ b/homeassistant/components/izone/translations/id.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "step": {
+ "confirm": {
+ "description": "Ingin menyiapkan iZone?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/izone/translations/ko.json b/homeassistant/components/izone/translations/ko.json
index 85aec276562787..d7173fc2db4fb5 100644
--- a/homeassistant/components/izone/translations/ko.json
+++ b/homeassistant/components/izone/translations/ko.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "iZone \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
- "single_instance_allowed": "\ud558\ub098\uc758 iZone \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/izone/translations/nl.json b/homeassistant/components/izone/translations/nl.json
index 22d1a3c4963671..b70bb738df02d2 100644
--- a/homeassistant/components/izone/translations/nl.json
+++ b/homeassistant/components/izone/translations/nl.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Geen iZone-apparaten gevonden op het netwerk.",
- "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van iZone nodig."
+ "no_devices_found": "Geen apparaten gevonden op het netwerk",
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/izone/translations/tr.json b/homeassistant/components/izone/translations/tr.json
new file mode 100644
index 00000000000000..faa20ed0ece1c9
--- /dev/null
+++ b/homeassistant/components/izone/translations/tr.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0130Zone'u kurmak istiyor musunuz?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/izone/translations/uk.json b/homeassistant/components/izone/translations/uk.json
new file mode 100644
index 00000000000000..8ab6c1e1664374
--- /dev/null
+++ b/homeassistant/components/izone/translations/uk.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 iZone?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py
index d1474c3cf5fd37..35c1505561dba0 100644
--- a/homeassistant/components/jewish_calendar/__init__.py
+++ b/homeassistant/components/jewish_calendar/__init__.py
@@ -1,5 +1,5 @@
"""The jewish_calendar component."""
-from typing import Optional
+from __future__ import annotations
import hdate
import voluptuous as vol
@@ -78,8 +78,8 @@
def get_unique_prefix(
location: hdate.Location,
language: str,
- candle_lighting_offset: Optional[int],
- havdalah_offset: Optional[int],
+ candle_lighting_offset: int | None,
+ havdalah_offset: int | None,
) -> str:
"""Create a prefix for unique ids."""
config_properties = [
diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py
index 6edcc7b27c3ce7..bda2bd5a1173d8 100644
--- a/homeassistant/components/jewish_calendar/binary_sensor.py
+++ b/homeassistant/components/jewish_calendar/binary_sensor.py
@@ -1,7 +1,11 @@
"""Support for Jewish Calendar binary sensors."""
+import datetime as dt
+
import hdate
from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.core import callback
+from homeassistant.helpers import event
import homeassistant.util.dt as dt_util
from . import DOMAIN, SENSOR_TYPES
@@ -32,8 +36,8 @@ def __init__(self, data, sensor, sensor_info):
self._hebrew = data["language"] == "hebrew"
self._candle_lighting_offset = data["candle_lighting_offset"]
self._havdalah_offset = data["havdalah_offset"]
- self._state = False
self._prefix = data["prefix"]
+ self._update_unsub = None
@property
def icon(self):
@@ -53,11 +57,16 @@ def name(self):
@property
def is_on(self):
"""Return true if sensor is on."""
- return self._state
+ return self._get_zmanim().issur_melacha_in_effect
- async def async_update(self):
- """Update the state of the sensor."""
- zmanim = hdate.Zmanim(
+ @property
+ def should_poll(self):
+ """No polling needed."""
+ return False
+
+ def _get_zmanim(self):
+ """Return the Zmanim object for now()."""
+ return hdate.Zmanim(
date=dt_util.now(),
location=self._location,
candle_lighting_offset=self._candle_lighting_offset,
@@ -65,4 +74,31 @@ async def async_update(self):
hebrew=self._hebrew,
)
- self._state = zmanim.issur_melacha_in_effect
+ async def async_added_to_hass(self):
+ """Run when entity about to be added to hass."""
+ await super().async_added_to_hass()
+ self._schedule_update()
+
+ @callback
+ def _update(self, now=None):
+ """Update the state of the sensor."""
+ self._update_unsub = None
+ self._schedule_update()
+ self.async_write_ha_state()
+
+ def _schedule_update(self):
+ """Schedule the next update of the sensor."""
+ now = dt_util.now()
+ zmanim = self._get_zmanim()
+ update = zmanim.zmanim["sunrise"] + dt.timedelta(days=1)
+ candle_lighting = zmanim.candle_lighting
+ if candle_lighting is not None and now < candle_lighting < update:
+ update = candle_lighting
+ havdalah = zmanim.havdalah
+ if havdalah is not None and now < havdalah < update:
+ update = havdalah
+ if self._update_unsub:
+ self._update_unsub()
+ self._update_unsub = event.async_track_point_in_time(
+ self.hass, self._update, update
+ )
diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py
index 6881f29b963df9..5690cd35a03140 100644
--- a/homeassistant/components/jewish_calendar/sensor.py
+++ b/homeassistant/components/jewish_calendar/sensor.py
@@ -3,8 +3,8 @@
import hdate
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_TIMESTAMP, SUN_EVENT_SUNSET
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.sun import get_astral_event_date
import homeassistant.util.dt as dt_util
@@ -30,7 +30,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(sensors)
-class JewishCalendarSensor(Entity):
+class JewishCalendarSensor(SensorEntity):
"""Representation of an Jewish calendar sensor."""
def __init__(self, data, sensor, sensor_info):
@@ -111,7 +111,7 @@ def make_zmanim(self, date):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._type != "holiday":
return {}
@@ -153,7 +153,7 @@ def device_class(self):
return DEVICE_CLASS_TIMESTAMP
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attrs = {}
diff --git a/homeassistant/components/joaoapps_join/__init__.py b/homeassistant/components/joaoapps_join/__init__.py
index 1bc4ae298c4785..1331afbfe970b2 100644
--- a/homeassistant/components/joaoapps_join/__init__.py
+++ b/homeassistant/components/joaoapps_join/__init__.py
@@ -12,14 +12,13 @@
)
import voluptuous as vol
-from homeassistant.const import CONF_API_KEY, CONF_NAME
+from homeassistant.const import CONF_API_KEY, CONF_DEVICE_ID, CONF_NAME
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DOMAIN = "joaoapps_join"
-CONF_DEVICE_ID = "device_id"
CONF_DEVICE_IDS = "device_ids"
CONF_DEVICE_NAMES = "device_names"
@@ -115,7 +114,6 @@ def send_sms_service(service):
def setup(hass, config):
"""Set up the Join services."""
-
for device in config[DOMAIN]:
api_key = device.get(CONF_API_KEY)
device_id = device.get(CONF_DEVICE_ID)
@@ -123,10 +121,9 @@ def setup(hass, config):
device_names = device.get(CONF_DEVICE_NAMES)
name = device.get(CONF_NAME)
name = f"{name.lower().replace(' ', '_')}_" if name else ""
- if api_key:
- if not get_devices(api_key):
- _LOGGER.error("Error connecting to Join, check API key")
- return False
+ if api_key and not get_devices(api_key):
+ _LOGGER.error("Error connecting to Join, check API key")
+ return False
if device_id is None and device_ids is None and device_names is None:
_LOGGER.error(
"No device was provided. Please specify device_id"
diff --git a/homeassistant/components/joaoapps_join/notify.py b/homeassistant/components/joaoapps_join/notify.py
index d01e49c77d8f84..06cad45bdde170 100644
--- a/homeassistant/components/joaoapps_join/notify.py
+++ b/homeassistant/components/joaoapps_join/notify.py
@@ -11,12 +11,11 @@
PLATFORM_SCHEMA,
BaseNotificationService,
)
-from homeassistant.const import CONF_API_KEY
+from homeassistant.const import CONF_API_KEY, CONF_DEVICE_ID
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
-CONF_DEVICE_ID = "device_id"
CONF_DEVICE_IDS = "device_ids"
CONF_DEVICE_NAMES = "device_names"
@@ -36,10 +35,9 @@ def get_service(hass, config, discovery_info=None):
device_id = config.get(CONF_DEVICE_ID)
device_ids = config.get(CONF_DEVICE_IDS)
device_names = config.get(CONF_DEVICE_NAMES)
- if api_key:
- if not get_devices(api_key):
- _LOGGER.error("Error connecting to Join. Check the API key")
- return False
+ if api_key and not get_devices(api_key):
+ _LOGGER.error("Error connecting to Join. Check the API key")
+ return False
if device_id is None and device_ids is None and device_names is None:
_LOGGER.error(
"No device was provided. Please specify device_id"
@@ -61,7 +59,6 @@ def __init__(self, api_key, device_id, device_ids, device_names):
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
-
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
data = kwargs.get(ATTR_DATA) or {}
send_notification(
diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py
index 080df7be6bf730..a7fb5e6b9b5e0b 100644
--- a/homeassistant/components/juicenet/__init__.py
+++ b/homeassistant/components/juicenet/__init__.py
@@ -89,11 +89,11 @@ async def async_update_data():
JUICENET_COORDINATOR: coordinator,
}
- await coordinator.async_refresh()
+ await coordinator.async_config_entry_first_refresh()
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -104,8 +104,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/juicenet/config_flow.py b/homeassistant/components/juicenet/config_flow.py
index 5ea7e4f267ac40..de814be2072eb0 100644
--- a/homeassistant/components/juicenet/config_flow.py
+++ b/homeassistant/components/juicenet/config_flow.py
@@ -9,7 +9,7 @@
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py
index 10008f30e7c49b..d908dc069efb44 100644
--- a/homeassistant/components/juicenet/sensor.py
+++ b/homeassistant/components/juicenet/sensor.py
@@ -1,4 +1,5 @@
"""Support for monitoring juicenet/juicepoint/juicebox based EVSE sensors."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ELECTRICAL_CURRENT_AMPERE,
ENERGY_WATT_HOUR,
@@ -7,7 +8,6 @@
TIME_SECONDS,
VOLT,
)
-from homeassistant.helpers.entity import Entity
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
from .entity import JuiceNetDevice
@@ -36,7 +36,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities)
-class JuiceNetSensorDevice(JuiceNetDevice, Entity):
+class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity):
"""Implementation of a JuiceNet sensor."""
def __init__(self, device, sensor_type, coordinator):
diff --git a/homeassistant/components/juicenet/translations/de.json b/homeassistant/components/juicenet/translations/de.json
index 16f48ef3837022..fbdea4c321f0af 100644
--- a/homeassistant/components/juicenet/translations/de.json
+++ b/homeassistant/components/juicenet/translations/de.json
@@ -1,20 +1,20 @@
{
"config": {
"abort": {
- "already_configured": "Dieses JuiceNet-Konto ist bereits konfiguriert"
+ "already_configured": "Konto wurde bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"step": {
"user": {
"data": {
- "api_token": "JuiceNet API Token"
+ "api_token": "API-Token"
},
"description": "Sie ben\u00f6tigen das API-Token von https://home.juice.net/Manage.",
- "title": "Stellen Sie eine Verbindung zu JuiceNet her"
+ "title": "Stelle eine Verbindung zu JuiceNet her"
}
}
}
diff --git a/homeassistant/components/juicenet/translations/hu.json b/homeassistant/components/juicenet/translations/hu.json
new file mode 100644
index 00000000000000..f04a8c1e6ca4da
--- /dev/null
+++ b/homeassistant/components/juicenet/translations/hu.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_token": "API Token"
+ },
+ "title": "Csatlakoz\u00e1s a JuiceNethez"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/juicenet/translations/id.json b/homeassistant/components/juicenet/translations/id.json
new file mode 100644
index 00000000000000..a150b7b7bbfa0d
--- /dev/null
+++ b/homeassistant/components/juicenet/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_token": "Token API"
+ },
+ "description": "Anda akan memerlukan Token API dari https://home.juice.net/Manage.",
+ "title": "Hubungkan ke JuiceNet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/juicenet/translations/ko.json b/homeassistant/components/juicenet/translations/ko.json
index 50b824ec82f085..b38ddbd1f6931a 100644
--- a/homeassistant/components/juicenet/translations/ko.json
+++ b/homeassistant/components/juicenet/translations/ko.json
@@ -1,20 +1,20 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 JuiceNet \uacc4\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
"data": {
- "api_token": "JuiceNet API \ud1a0\ud070"
+ "api_token": "API \ud1a0\ud070"
},
"description": "https://home.juice.net/Manage \uc758 API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.",
- "title": "JuiceNet \uc5d0 \uc5f0\uacb0\ud558\uae30"
+ "title": "JuiceNet\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/juicenet/translations/ru.json b/homeassistant/components/juicenet/translations/ru.json
index 2fec7d485c477c..d582e6f17038d3 100644
--- a/homeassistant/components/juicenet/translations/ru.json
+++ b/homeassistant/components/juicenet/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
diff --git a/homeassistant/components/juicenet/translations/tr.json b/homeassistant/components/juicenet/translations/tr.json
new file mode 100644
index 00000000000000..53890eb41e228e
--- /dev/null
+++ b/homeassistant/components/juicenet/translations/tr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_token": "API Belirteci"
+ },
+ "description": "API Belirtecine https://home.juice.net/Manage adresinden ihtiyac\u0131n\u0131z olacak.",
+ "title": "JuiceNet'e ba\u011flan\u0131n"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/juicenet/translations/uk.json b/homeassistant/components/juicenet/translations/uk.json
new file mode 100644
index 00000000000000..903ea5f6e743dd
--- /dev/null
+++ b/homeassistant/components/juicenet/translations/uk.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_token": "\u0422\u043e\u043a\u0435\u043d API"
+ },
+ "description": "\u0414\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d \u0442\u043e\u043a\u0435\u043d API \u0437 \u0441\u0430\u0439\u0442\u0443 https://home.juice.net/Manage.",
+ "title": "JuiceNet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/juicenet/translations/zh-Hant.json b/homeassistant/components/juicenet/translations/zh-Hant.json
index 815edb1fb2710b..f310babfd80aa1 100644
--- a/homeassistant/components/juicenet/translations/zh-Hant.json
+++ b/homeassistant/components/juicenet/translations/zh-Hant.json
@@ -11,9 +11,9 @@
"step": {
"user": {
"data": {
- "api_token": "API \u5bc6\u9470"
+ "api_token": "API \u6b0a\u6756"
},
- "description": "\u5c07\u9700\u8981\u7531 https://home.juice.net/Manage \u53d6\u5f97 API \u5bc6\u9470\u3002",
+ "description": "\u5c07\u9700\u8981\u7531 https://home.juice.net/Manage \u53d6\u5f97 API \u6b0a\u6756\u3002",
"title": "\u9023\u7dda\u81f3 JuiceNet"
}
}
diff --git a/homeassistant/components/kaiterra/__init__.py b/homeassistant/components/kaiterra/__init__.py
index d043dc15eafc26..eae14bd330e2e8 100644
--- a/homeassistant/components/kaiterra/__init__.py
+++ b/homeassistant/components/kaiterra/__init__.py
@@ -25,7 +25,7 @@
DEFAULT_PREFERRED_UNIT,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
- KAITERRA_COMPONENTS,
+ PLATFORMS,
)
KAITERRA_DEVICE_SCHEMA = vol.Schema(
@@ -54,7 +54,7 @@
async def async_setup(hass, config):
- """Set up the Kaiterra components."""
+ """Set up the Kaiterra integration."""
conf = config[DOMAIN]
scan_interval = conf[CONF_SCAN_INTERVAL]
@@ -76,11 +76,11 @@ async def _update(now=None):
device.get(CONF_NAME) or device[CONF_TYPE],
device[CONF_DEVICE_ID],
)
- for component in KAITERRA_COMPONENTS:
+ for platform in PLATFORMS:
hass.async_create_task(
async_load_platform(
hass,
- component,
+ platform,
DOMAIN,
{CONF_NAME: device_name, CONF_DEVICE_ID: device_id},
config,
diff --git a/homeassistant/components/kaiterra/air_quality.py b/homeassistant/components/kaiterra/air_quality.py
index ae5df387884fb5..68377d6b254f7a 100644
--- a/homeassistant/components/kaiterra/air_quality.py
+++ b/homeassistant/components/kaiterra/air_quality.py
@@ -96,7 +96,7 @@ def unique_id(self):
return f"{self._device_id}_air_quality"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
data = {}
attributes = [
diff --git a/homeassistant/components/kaiterra/const.py b/homeassistant/components/kaiterra/const.py
index 583cd60085d920..f4cc5638c18a06 100644
--- a/homeassistant/components/kaiterra/const.py
+++ b/homeassistant/components/kaiterra/const.py
@@ -71,4 +71,4 @@
DEFAULT_PREFERRED_UNIT = []
DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
-KAITERRA_COMPONENTS = ["sensor", "air_quality"]
+PLATFORMS = ["sensor", "air_quality"]
diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py
index d9500c7a00064d..1e4dd0cbbca2b6 100644
--- a/homeassistant/components/kaiterra/sensor.py
+++ b/homeassistant/components/kaiterra/sensor.py
@@ -1,7 +1,7 @@
"""Support for Kaiterra Temperature ahn Humidity Sensors."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from .const import DISPATCHER_KAITERRA, DOMAIN
@@ -25,7 +25,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
-class KaiterraSensor(Entity):
+class KaiterraSensor(SensorEntity):
"""Implementation of a Kaittera sensor."""
def __init__(self, api, name, device_id, sensor):
diff --git a/homeassistant/components/keba/__init__.py b/homeassistant/components/keba/__init__.py
index 764110f94b9603..e1cf9bfd3ea966 100644
--- a/homeassistant/components/keba/__init__.py
+++ b/homeassistant/components/keba/__init__.py
@@ -233,7 +233,7 @@ async def async_set_failsafe(self, param=None):
self._set_fast_polling()
except (KeyError, ValueError) as ex:
_LOGGER.warning(
- "failsafe_timeout, failsafe_fallback and/or "
- "failsafe_persist value are not correct. %s",
+ "Values are not correct for: failsafe_timeout, failsafe_fallback and/or "
+ "failsafe_persist: %s",
ex,
)
diff --git a/homeassistant/components/keba/binary_sensor.py b/homeassistant/components/keba/binary_sensor.py
index 3fed7bbf5ab50e..292924701559cf 100644
--- a/homeassistant/components/keba/binary_sensor.py
+++ b/homeassistant/components/keba/binary_sensor.py
@@ -71,7 +71,7 @@ def is_on(self):
return self._is_on
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the binary sensor."""
return self._attributes
diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py
index f7993c283938a4..836785490e8df0 100644
--- a/homeassistant/components/keba/sensor.py
+++ b/homeassistant/components/keba/sensor.py
@@ -1,10 +1,10 @@
"""Support for KEBA charging station sensors."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEVICE_CLASS_POWER,
ELECTRICAL_CURRENT_AMPERE,
ENERGY_KILO_WATT_HOUR,
)
-from homeassistant.helpers.entity import Entity
from . import DOMAIN
@@ -62,7 +62,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(sensors)
-class KebaSensor(Entity):
+class KebaSensor(SensorEntity):
"""The entity class for KEBA charging stations sensors."""
def __init__(self, keba, key, name, entity_type, icon, unit, device_class=None):
@@ -114,7 +114,7 @@ def unit_of_measurement(self):
return self._unit
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the binary sensor."""
return self._attributes
diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py
index cb0a718d71688c..d0217b2a4f556e 100644
--- a/homeassistant/components/keenetic_ndms2/__init__.py
+++ b/homeassistant/components/keenetic_ndms2/__init__.py
@@ -1 +1,92 @@
"""The keenetic_ndms2 component."""
+
+from homeassistant.components import binary_sensor, device_tracker
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL
+from homeassistant.core import Config, HomeAssistant
+
+from .const import (
+ CONF_CONSIDER_HOME,
+ CONF_INCLUDE_ARP,
+ CONF_INCLUDE_ASSOCIATED,
+ CONF_INTERFACES,
+ CONF_TRY_HOTSPOT,
+ DEFAULT_CONSIDER_HOME,
+ DEFAULT_INTERFACE,
+ DEFAULT_SCAN_INTERVAL,
+ DOMAIN,
+ ROUTER,
+ UNDO_UPDATE_LISTENER,
+)
+from .router import KeeneticRouter
+
+PLATFORMS = [device_tracker.DOMAIN, binary_sensor.DOMAIN]
+
+
+async def async_setup(hass: HomeAssistant, _config: Config) -> bool:
+ """Set up configured entries."""
+ hass.data.setdefault(DOMAIN, {})
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+ """Set up the component."""
+
+ async_add_defaults(hass, config_entry)
+
+ router = KeeneticRouter(hass, config_entry)
+ await router.async_setup()
+
+ undo_listener = config_entry.add_update_listener(update_listener)
+
+ hass.data[DOMAIN][config_entry.entry_id] = {
+ ROUTER: router,
+ UNDO_UPDATE_LISTENER: undo_listener,
+ }
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+ """Unload a config entry."""
+ hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]()
+
+ for platform in PLATFORMS:
+ await hass.config_entries.async_forward_entry_unload(config_entry, platform)
+
+ router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER]
+
+ await router.async_teardown()
+
+ hass.data[DOMAIN].pop(config_entry.entry_id)
+
+ return True
+
+
+async def update_listener(hass, config_entry):
+ """Handle options update."""
+ await hass.config_entries.async_reload(config_entry.entry_id)
+
+
+def async_add_defaults(hass: HomeAssistant, config_entry: ConfigEntry):
+ """Populate default options."""
+ host: str = config_entry.data[CONF_HOST]
+ imported_options: dict = hass.data[DOMAIN].get(f"imported_options_{host}", {})
+ options = {
+ CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
+ CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME,
+ CONF_INTERFACES: [DEFAULT_INTERFACE],
+ CONF_TRY_HOTSPOT: True,
+ CONF_INCLUDE_ARP: True,
+ CONF_INCLUDE_ASSOCIATED: True,
+ **imported_options,
+ **config_entry.options,
+ }
+
+ if options.keys() - config_entry.options.keys():
+ hass.config_entries.async_update_entry(config_entry, options=options)
diff --git a/homeassistant/components/keenetic_ndms2/binary_sensor.py b/homeassistant/components/keenetic_ndms2/binary_sensor.py
new file mode 100644
index 00000000000000..5da52eff00d119
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/binary_sensor.py
@@ -0,0 +1,72 @@
+"""The Keenetic Client class."""
+import logging
+
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
+ BinarySensorEntity,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+from . import KeeneticRouter
+from .const import DOMAIN, ROUTER
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
+):
+ """Set up device tracker for Keenetic NDMS2 component."""
+ router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER]
+
+ async_add_entities([RouterOnlineBinarySensor(router)])
+
+
+class RouterOnlineBinarySensor(BinarySensorEntity):
+ """Representation router connection status."""
+
+ def __init__(self, router: KeeneticRouter):
+ """Initialize the APCUPSd binary device."""
+ self._router = router
+
+ @property
+ def name(self):
+ """Return the name of the online status sensor."""
+ return f"{self._router.name} Online"
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique identifier for this device."""
+ return f"online_{self._router.config_entry.entry_id}"
+
+ @property
+ def is_on(self):
+ """Return true if the UPS is online, else false."""
+ return self._router.available
+
+ @property
+ def device_class(self):
+ """Return the class of this device, from component DEVICE_CLASSES."""
+ return DEVICE_CLASS_CONNECTIVITY
+
+ @property
+ def should_poll(self) -> bool:
+ """Return False since entity pushes its state to HA."""
+ return False
+
+ @property
+ def device_info(self):
+ """Return a client description for device registry."""
+ return self._router.device_info
+
+ async def async_added_to_hass(self):
+ """Client entity created."""
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ self._router.signal_update,
+ self.async_write_ha_state,
+ )
+ )
diff --git a/homeassistant/components/keenetic_ndms2/config_flow.py b/homeassistant/components/keenetic_ndms2/config_flow.py
new file mode 100644
index 00000000000000..a832f68e01762a
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/config_flow.py
@@ -0,0 +1,159 @@
+"""Config flow for Keenetic NDMS2."""
+from __future__ import annotations
+
+from ndms2_client import Client, ConnectionException, InterfaceInfo, TelnetConnection
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_SCAN_INTERVAL,
+ CONF_USERNAME,
+)
+from homeassistant.core import callback
+import homeassistant.helpers.config_validation as cv
+
+from .const import (
+ CONF_CONSIDER_HOME,
+ CONF_INCLUDE_ARP,
+ CONF_INCLUDE_ASSOCIATED,
+ CONF_INTERFACES,
+ CONF_TRY_HOTSPOT,
+ DEFAULT_CONSIDER_HOME,
+ DEFAULT_INTERFACE,
+ DEFAULT_SCAN_INTERVAL,
+ DEFAULT_TELNET_PORT,
+ DOMAIN,
+ ROUTER,
+)
+from .router import KeeneticRouter
+
+
+class KeeneticFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for this handler."""
+ return KeeneticOptionsFlowHandler(config_entry)
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ errors = {}
+ if user_input is not None:
+ for entry in self.hass.config_entries.async_entries(DOMAIN):
+ if entry.data[CONF_HOST] == user_input[CONF_HOST]:
+ return self.async_abort(reason="already_configured")
+
+ _client = Client(
+ TelnetConnection(
+ user_input[CONF_HOST],
+ user_input[CONF_PORT],
+ user_input[CONF_USERNAME],
+ user_input[CONF_PASSWORD],
+ timeout=10,
+ )
+ )
+
+ try:
+ router_info = await self.hass.async_add_executor_job(
+ _client.get_router_info
+ )
+ except ConnectionException:
+ errors["base"] = "cannot_connect"
+ else:
+ return self.async_create_entry(title=router_info.name, data=user_input)
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_HOST): str,
+ vol.Required(CONF_USERNAME): str,
+ vol.Required(CONF_PASSWORD): str,
+ vol.Optional(CONF_PORT, default=DEFAULT_TELNET_PORT): int,
+ }
+ ),
+ errors=errors,
+ )
+
+ async def async_step_import(self, user_input=None):
+ """Import a config entry."""
+ return await self.async_step_user(user_input)
+
+
+class KeeneticOptionsFlowHandler(config_entries.OptionsFlow):
+ """Handle options."""
+
+ def __init__(self, config_entry: ConfigEntry):
+ """Initialize options flow."""
+ self.config_entry = config_entry
+ self._interface_options = {}
+
+ async def async_step_init(self, _user_input=None):
+ """Manage the options."""
+ router: KeeneticRouter = self.hass.data[DOMAIN][self.config_entry.entry_id][
+ ROUTER
+ ]
+
+ interfaces: list[InterfaceInfo] = await self.hass.async_add_executor_job(
+ router.client.get_interfaces
+ )
+
+ self._interface_options = {
+ interface.name: (interface.description or interface.name)
+ for interface in interfaces
+ if interface.type.lower() == "bridge"
+ }
+ return await self.async_step_user()
+
+ async def async_step_user(self, user_input=None):
+ """Manage the device tracker options."""
+ if user_input is not None:
+ return self.async_create_entry(title="", data=user_input)
+
+ options = vol.Schema(
+ {
+ vol.Required(
+ CONF_SCAN_INTERVAL,
+ default=self.config_entry.options.get(
+ CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
+ ),
+ ): int,
+ vol.Required(
+ CONF_CONSIDER_HOME,
+ default=self.config_entry.options.get(
+ CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME
+ ),
+ ): int,
+ vol.Required(
+ CONF_INTERFACES,
+ default=self.config_entry.options.get(
+ CONF_INTERFACES, [DEFAULT_INTERFACE]
+ ),
+ ): cv.multi_select(self._interface_options),
+ vol.Optional(
+ CONF_TRY_HOTSPOT,
+ default=self.config_entry.options.get(CONF_TRY_HOTSPOT, True),
+ ): bool,
+ vol.Optional(
+ CONF_INCLUDE_ARP,
+ default=self.config_entry.options.get(CONF_INCLUDE_ARP, True),
+ ): bool,
+ vol.Optional(
+ CONF_INCLUDE_ASSOCIATED,
+ default=self.config_entry.options.get(
+ CONF_INCLUDE_ASSOCIATED, True
+ ),
+ ): bool,
+ }
+ )
+
+ return self.async_show_form(step_id="user", data_schema=options)
diff --git a/homeassistant/components/keenetic_ndms2/const.py b/homeassistant/components/keenetic_ndms2/const.py
new file mode 100644
index 00000000000000..1818cfab6a661a
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/const.py
@@ -0,0 +1,21 @@
+"""Constants used in the Keenetic NDMS2 components."""
+
+from homeassistant.components.device_tracker.const import (
+ DEFAULT_CONSIDER_HOME as _DEFAULT_CONSIDER_HOME,
+)
+
+DOMAIN = "keenetic_ndms2"
+ROUTER = "router"
+UNDO_UPDATE_LISTENER = "undo_update_listener"
+DEFAULT_TELNET_PORT = 23
+DEFAULT_SCAN_INTERVAL = 120
+DEFAULT_CONSIDER_HOME = _DEFAULT_CONSIDER_HOME.seconds
+DEFAULT_INTERFACE = "Home"
+
+CONF_CONSIDER_HOME = "consider_home"
+CONF_INTERFACES = "interfaces"
+CONF_TRY_HOTSPOT = "try_hotspot"
+CONF_INCLUDE_ARP = "include_arp"
+CONF_INCLUDE_ASSOCIATED = "include_associated"
+
+CONF_LEGACY_INTERFACE = "interface"
diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py
index d98806dfc05a17..461814b19170d8 100644
--- a/homeassistant/components/keenetic_ndms2/device_tracker.py
+++ b/homeassistant/components/keenetic_ndms2/device_tracker.py
@@ -1,102 +1,254 @@
-"""Support for Zyxel Keenetic NDMS2 based routers."""
+"""Support for Keenetic routers as device tracker."""
+from __future__ import annotations
+
+from datetime import timedelta
import logging
-from ndms2_client import Client, ConnectionException, TelnetConnection
+from ndms2_client import Device
import voluptuous as vol
from homeassistant.components.device_tracker import (
- DOMAIN,
- PLATFORM_SCHEMA,
- DeviceScanner,
+ DOMAIN as DEVICE_TRACKER_DOMAIN,
+ PLATFORM_SCHEMA as DEVICE_TRACKER_SCHEMA,
+ SOURCE_TYPE_ROUTER,
+)
+from homeassistant.components.device_tracker.config_entry import ScannerEntity
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_SCAN_INTERVAL,
+ CONF_USERNAME,
)
-from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers import entity_registry
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+import homeassistant.util.dt as dt_util
+
+from .const import (
+ CONF_CONSIDER_HOME,
+ CONF_INTERFACES,
+ CONF_LEGACY_INTERFACE,
+ DEFAULT_CONSIDER_HOME,
+ DEFAULT_INTERFACE,
+ DEFAULT_SCAN_INTERVAL,
+ DEFAULT_TELNET_PORT,
+ DOMAIN,
+ ROUTER,
+)
+from .router import KeeneticRouter
_LOGGER = logging.getLogger(__name__)
-# Interface name to track devices for. Most likely one will not need to
-# change it from default 'Home'. This is needed not to track Guest WI-FI-
-# clients and router itself
-CONF_INTERFACE = "interface"
-
-DEFAULT_INTERFACE = "Home"
-DEFAULT_PORT = 23
-
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
+PLATFORM_SCHEMA = DEVICE_TRACKER_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
+ vol.Required(CONF_PORT, default=DEFAULT_TELNET_PORT): cv.port,
vol.Required(CONF_PASSWORD): cv.string,
- vol.Required(CONF_INTERFACE, default=DEFAULT_INTERFACE): cv.string,
+ vol.Required(CONF_LEGACY_INTERFACE, default=DEFAULT_INTERFACE): cv.string,
}
)
-def get_scanner(_hass, config):
- """Validate the configuration and return a Keenetic NDMS2 scanner."""
- scanner = KeeneticNDMS2DeviceScanner(config[DOMAIN])
-
- return scanner if scanner.success_init else None
-
+async def async_get_scanner(hass: HomeAssistant, config):
+ """Import legacy configuration from YAML."""
-class KeeneticNDMS2DeviceScanner(DeviceScanner):
- """This class scans for devices using keenetic NDMS2 web interface."""
+ scanner_config = config[DEVICE_TRACKER_DOMAIN]
+ scan_interval: timedelta | None = scanner_config.get(CONF_SCAN_INTERVAL)
+ consider_home: timedelta | None = scanner_config.get(CONF_CONSIDER_HOME)
- def __init__(self, config):
- """Initialize the scanner."""
-
- self.last_results = []
-
- self._interface = config[CONF_INTERFACE]
+ host: str = scanner_config[CONF_HOST]
+ hass.data[DOMAIN][f"imported_options_{host}"] = {
+ CONF_INTERFACES: [scanner_config[CONF_LEGACY_INTERFACE]],
+ CONF_SCAN_INTERVAL: int(scan_interval.total_seconds())
+ if scan_interval
+ else DEFAULT_SCAN_INTERVAL,
+ CONF_CONSIDER_HOME: int(consider_home.total_seconds())
+ if consider_home
+ else DEFAULT_CONSIDER_HOME,
+ }
- self._client = Client(
- TelnetConnection(
- config.get(CONF_HOST),
- config.get(CONF_PORT),
- config.get(CONF_USERNAME),
- config.get(CONF_PASSWORD),
- )
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data={
+ CONF_HOST: scanner_config[CONF_HOST],
+ CONF_PORT: scanner_config[CONF_PORT],
+ CONF_USERNAME: scanner_config[CONF_USERNAME],
+ CONF_PASSWORD: scanner_config[CONF_PASSWORD],
+ },
+ )
+ )
+
+ _LOGGER.warning(
+ "Your Keenetic NDMS2 configuration has been imported into the UI, "
+ "please remove it from configuration.yaml. "
+ "Loading Keenetic NDMS2 via scanner setup is now deprecated"
+ )
+
+ return None
+
+
+async def async_setup_entry(
+ hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
+):
+ """Set up device tracker for Keenetic NDMS2 component."""
+ router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER]
+
+ tracked = set()
+
+ @callback
+ def update_from_router():
+ """Update the status of devices."""
+ update_items(router, async_add_entities, tracked)
+
+ update_from_router()
+
+ registry = await entity_registry.async_get_registry(hass)
+ # Restore devices that are not a part of active clients list.
+ restored = []
+ for entity_entry in registry.entities.values():
+ if (
+ entity_entry.config_entry_id == config_entry.entry_id
+ and entity_entry.domain == DEVICE_TRACKER_DOMAIN
+ ):
+ mac = entity_entry.unique_id.partition("_")[0]
+ if mac not in tracked:
+ tracked.add(mac)
+ restored.append(
+ KeeneticTracker(
+ Device(
+ mac=mac,
+ # restore the original name as set by the router before
+ name=entity_entry.original_name,
+ ip=None,
+ interface=None,
+ ),
+ router,
+ )
+ )
+
+ if restored:
+ async_add_entities(restored)
+
+ async_dispatcher_connect(hass, router.signal_update, update_from_router)
+
+
+@callback
+def update_items(router: KeeneticRouter, async_add_entities, tracked: set[str]):
+ """Update tracked device state from the hub."""
+ new_tracked: list[KeeneticTracker] = []
+ for mac, device in router.last_devices.items():
+ if mac not in tracked:
+ tracked.add(mac)
+ new_tracked.append(KeeneticTracker(device, router))
+
+ if new_tracked:
+ async_add_entities(new_tracked)
+
+
+class KeeneticTracker(ScannerEntity):
+ """Representation of network device."""
+
+ def __init__(self, device: Device, router: KeeneticRouter):
+ """Initialize the tracked device."""
+ self._device = device
+ self._router = router
+ self._last_seen = (
+ dt_util.utcnow() if device.mac in router.last_devices else None
)
- self.success_init = self._update_info()
- _LOGGER.info("Scanner initialized")
-
- def scan_devices(self):
- """Scan for new devices and return a list with found device IDs."""
- self._update_info()
+ @property
+ def should_poll(self) -> bool:
+ """Return False since entity pushes its state to HA."""
+ return False
+
+ @property
+ def is_connected(self):
+ """Return true if the device is connected to the network."""
+ return (
+ self._last_seen
+ and (dt_util.utcnow() - self._last_seen)
+ < self._router.consider_home_interval
+ )
- return [device.mac for device in self.last_results]
+ @property
+ def source_type(self):
+ """Return the source type of the client."""
+ return SOURCE_TYPE_ROUTER
+
+ @property
+ def name(self) -> str:
+ """Return the name of the device."""
+ return self._device.name or self._device.mac
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique identifier for this device."""
+ return f"{self._device.mac}_{self._router.config_entry.entry_id}"
+
+ @property
+ def ip_address(self) -> str:
+ """Return the primary ip address of the device."""
+ return self._device.ip if self.is_connected else None
+
+ @property
+ def mac_address(self) -> str:
+ """Return the mac address of the device."""
+ return self._device.mac
+
+ @property
+ def available(self) -> bool:
+ """Return if controller is available."""
+ return self._router.available
+
+ @property
+ def extra_state_attributes(self):
+ """Return the device state attributes."""
+ if self.is_connected:
+ return {
+ "interface": self._device.interface,
+ }
+ return None
+
+ @property
+ def device_info(self):
+ """Return a client description for device registry."""
+ info = {
+ "connections": {(CONNECTION_NETWORK_MAC, self._device.mac)},
+ "identifiers": {(DOMAIN, self._device.mac)},
+ }
+
+ if self._device.name:
+ info["name"] = self._device.name
+
+ return info
+
+ async def async_added_to_hass(self):
+ """Client entity created."""
+ _LOGGER.debug("New network device tracker %s (%s)", self.name, self.unique_id)
+
+ @callback
+ def update_device():
+ _LOGGER.debug(
+ "Updating Keenetic tracked device %s (%s)",
+ self.entity_id,
+ self.unique_id,
+ )
+ new_device = self._router.last_devices.get(self._device.mac)
+ if new_device:
+ self._device = new_device
+ self._last_seen = dt_util.utcnow()
- def get_device_name(self, device):
- """Return the name of the given device or None if we don't know."""
- name = next(
- (result.name for result in self.last_results if result.mac == device), None
- )
- return name
+ self.async_write_ha_state()
- def get_extra_attributes(self, device):
- """Return the IP of the given device."""
- attributes = next(
- ({"ip": result.ip} for result in self.last_results if result.mac == device),
- {},
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass, self._router.signal_update, update_device
+ )
)
- return attributes
-
- def _update_info(self):
- """Get ARP from keenetic router."""
- _LOGGER.debug("Fetching devices from router...")
-
- try:
- self.last_results = [
- dev
- for dev in self._client.get_devices()
- if dev.interface == self._interface
- ]
- _LOGGER.debug("Successfully fetched data from router")
- return True
-
- except ConnectionException:
- _LOGGER.error("Error fetching data from router")
- return False
diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json
index 9d4c9f35716272..da8321a8bdc568 100644
--- a/homeassistant/components/keenetic_ndms2/manifest.json
+++ b/homeassistant/components/keenetic_ndms2/manifest.json
@@ -1,7 +1,8 @@
{
"domain": "keenetic_ndms2",
- "name": "Keenetic NDMS2 Routers",
+ "name": "Keenetic NDMS2 Router",
+ "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2",
- "requirements": ["ndms2_client==0.0.11"],
+ "requirements": ["ndms2_client==0.1.1"],
"codeowners": ["@foxel"]
}
diff --git a/homeassistant/components/keenetic_ndms2/router.py b/homeassistant/components/keenetic_ndms2/router.py
new file mode 100644
index 00000000000000..049d9aab0de736
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/router.py
@@ -0,0 +1,189 @@
+"""The Keenetic Client class."""
+from __future__ import annotations
+
+from datetime import timedelta
+import logging
+from typing import Callable
+
+from ndms2_client import Client, ConnectionException, Device, TelnetConnection
+from ndms2_client.client import RouterInfo
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_SCAN_INTERVAL,
+ CONF_USERNAME,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.event import async_call_later
+import homeassistant.util.dt as dt_util
+
+from .const import (
+ CONF_CONSIDER_HOME,
+ CONF_INCLUDE_ARP,
+ CONF_INCLUDE_ASSOCIATED,
+ CONF_INTERFACES,
+ CONF_TRY_HOTSPOT,
+ DOMAIN,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class KeeneticRouter:
+ """Keenetic client Object."""
+
+ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry):
+ """Initialize the Client."""
+ self.hass = hass
+ self.config_entry = config_entry
+ self._last_devices: dict[str, Device] = {}
+ self._router_info: RouterInfo | None = None
+ self._connection: TelnetConnection | None = None
+ self._client: Client | None = None
+ self._cancel_periodic_update: Callable | None = None
+ self._available = False
+ self._progress = None
+
+ @property
+ def client(self):
+ """Read-only accessor for the client connection."""
+ return self._client
+
+ @property
+ def last_devices(self):
+ """Read-only accessor for last_devices."""
+ return self._last_devices
+
+ @property
+ def host(self):
+ """Return the host of this hub."""
+ return self.config_entry.data[CONF_HOST]
+
+ @property
+ def device_info(self):
+ """Return the host of this hub."""
+ return {
+ "identifiers": {(DOMAIN, f"router-{self.config_entry.entry_id}")},
+ "manufacturer": self.manufacturer,
+ "model": self.model,
+ "name": self.name,
+ "sw_version": self.firmware,
+ }
+
+ @property
+ def name(self):
+ """Return the name of the hub."""
+ return self._router_info.name if self._router_info else self.host
+
+ @property
+ def model(self):
+ """Return the model of the hub."""
+ return self._router_info.model if self._router_info else None
+
+ @property
+ def firmware(self):
+ """Return the firmware of the hub."""
+ return self._router_info.fw_version if self._router_info else None
+
+ @property
+ def manufacturer(self):
+ """Return the firmware of the hub."""
+ return self._router_info.manufacturer if self._router_info else None
+
+ @property
+ def available(self):
+ """Return if the hub is connected."""
+ return self._available
+
+ @property
+ def consider_home_interval(self):
+ """Config entry option defining number of seconds from last seen to away."""
+ return timedelta(seconds=self.config_entry.options[CONF_CONSIDER_HOME])
+
+ @property
+ def signal_update(self):
+ """Event specific per router entry to signal updates."""
+ return f"keenetic-update-{self.config_entry.entry_id}"
+
+ async def request_update(self):
+ """Request an update."""
+ if self._progress is not None:
+ await self._progress
+ return
+
+ self._progress = self.hass.async_create_task(self.async_update())
+ await self._progress
+
+ self._progress = None
+
+ async def async_update(self):
+ """Update devices information."""
+ await self.hass.async_add_executor_job(self._update_devices)
+ async_dispatcher_send(self.hass, self.signal_update)
+
+ async def async_setup(self):
+ """Set up the connection."""
+ self._connection = TelnetConnection(
+ self.config_entry.data[CONF_HOST],
+ self.config_entry.data[CONF_PORT],
+ self.config_entry.data[CONF_USERNAME],
+ self.config_entry.data[CONF_PASSWORD],
+ )
+ self._client = Client(self._connection)
+
+ try:
+ await self.hass.async_add_executor_job(self._update_router_info)
+ except ConnectionException as error:
+ raise ConfigEntryNotReady from error
+
+ async def async_update_data(_now):
+ await self.request_update()
+ self._cancel_periodic_update = async_call_later(
+ self.hass,
+ self.config_entry.options[CONF_SCAN_INTERVAL],
+ async_update_data,
+ )
+
+ await async_update_data(dt_util.utcnow())
+
+ async def async_teardown(self):
+ """Teardown up the connection."""
+ if self._cancel_periodic_update:
+ self._cancel_periodic_update()
+ self._connection.disconnect()
+
+ def _update_router_info(self):
+ try:
+ self._router_info = self._client.get_router_info()
+ self._available = True
+ except Exception:
+ self._available = False
+ raise
+
+ def _update_devices(self):
+ """Get ARP from keenetic router."""
+ _LOGGER.debug("Fetching devices from router")
+
+ try:
+ _response = self._client.get_devices(
+ try_hotspot=self.config_entry.options[CONF_TRY_HOTSPOT],
+ include_arp=self.config_entry.options[CONF_INCLUDE_ARP],
+ include_associated=self.config_entry.options[CONF_INCLUDE_ASSOCIATED],
+ )
+ self._last_devices = {
+ dev.mac: dev
+ for dev in _response
+ if dev.interface in self.config_entry.options[CONF_INTERFACES]
+ }
+ _LOGGER.debug("Successfully fetched data from router: %s", str(_response))
+ self._router_info = self._client.get_router_info()
+ self._available = True
+
+ except ConnectionException:
+ _LOGGER.error("Error fetching data from router")
+ self._available = False
diff --git a/homeassistant/components/keenetic_ndms2/strings.json b/homeassistant/components/keenetic_ndms2/strings.json
new file mode 100644
index 00000000000000..0dc1c9c302fc40
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/strings.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "Set up Keenetic NDMS2 Router",
+ "data": {
+ "host": "[%key:common::config_flow::data::host%]",
+ "username": "[%key:common::config_flow::data::username%]",
+ "password": "[%key:common::config_flow::data::password%]",
+ "port": "[%key:common::config_flow::data::port%]"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "scan_interval": "Scan interval",
+ "consider_home": "Consider home interval",
+ "interfaces": "Choose interfaces to scan",
+ "try_hotspot": "Use 'ip hotspot' data (most accurate)",
+ "include_arp": "Use ARP data (ignored if hotspot data used)",
+ "include_associated": "Use WiFi AP associations data (ignored if hotspot data used)"
+ }
+ }
+ }
+ }
+}
diff --git a/homeassistant/components/keenetic_ndms2/translations/bg.json b/homeassistant/components/keenetic_ndms2/translations/bg.json
new file mode 100644
index 00000000000000..db122ec078bfdb
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/bg.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "name": "\u0418\u043c\u0435",
+ "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/keenetic_ndms2/translations/ca.json b/homeassistant/components/keenetic_ndms2/translations/ca.json
new file mode 100644
index 00000000000000..f15b11b3eb42fc
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/ca.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El compte ja ha estat configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "name": "Nom",
+ "password": "Contrasenya",
+ "port": "Port",
+ "username": "Nom d'usuari"
+ },
+ "title": "Configuraci\u00f3 del router Keenetic NDMS2"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "consider_home": "Interval per considerar a casa",
+ "include_arp": "Utilitza dades d'ARP (s'ignorar\u00e0 si s'utilitzen dades de 'punt d'acc\u00e9s')",
+ "include_associated": "Utilitza dades d'associacions d'AP WiFi (s'ignorar\u00e0 si s'utilitzen dades de 'punt d'acc\u00e9s')",
+ "interfaces": "Escull les interf\u00edcies a escanejar",
+ "scan_interval": "Interval d'escaneig",
+ "try_hotspot": "Utilitza dades de 'punt d'acc\u00e9s IP' (m\u00e9s precisi\u00f3)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keenetic_ndms2/translations/cs.json b/homeassistant/components/keenetic_ndms2/translations/cs.json
new file mode 100644
index 00000000000000..f34807f3fee64c
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/cs.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u00da\u010det je ji\u017e nastaven"
+ },
+ "error": {
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hostitel",
+ "name": "N\u00e1zev",
+ "password": "Heslo",
+ "port": "Port",
+ "username": "U\u017eivatelsk\u00e9 jm\u00e9no"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keenetic_ndms2/translations/de.json b/homeassistant/components/keenetic_ndms2/translations/de.json
new file mode 100644
index 00000000000000..cc9a3630ab1d02
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/de.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto wurde bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Name",
+ "password": "Passwort",
+ "port": "Port",
+ "username": "Benutzername"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "interfaces": "Schnittstellen zum Scannen ausw\u00e4hlen",
+ "scan_interval": "Scanintervall"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keenetic_ndms2/translations/en.json b/homeassistant/components/keenetic_ndms2/translations/en.json
new file mode 100644
index 00000000000000..e95f2f740efd6c
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/en.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Account is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Name",
+ "password": "Password",
+ "port": "Port",
+ "username": "Username"
+ },
+ "title": "Set up Keenetic NDMS2 Router"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "consider_home": "Consider home interval",
+ "include_arp": "Use ARP data (ignored if hotspot data used)",
+ "include_associated": "Use WiFi AP associations data (ignored if hotspot data used)",
+ "interfaces": "Choose interfaces to scan",
+ "scan_interval": "Scan interval",
+ "try_hotspot": "Use 'ip hotspot' data (most accurate)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keenetic_ndms2/translations/es.json b/homeassistant/components/keenetic_ndms2/translations/es.json
new file mode 100644
index 00000000000000..6846cfbef4203a
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/es.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La cuenta ya est\u00e1 configurada"
+ },
+ "error": {
+ "cannot_connect": "Fallo de conexi\u00f3n"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nombre",
+ "password": "Contrase\u00f1a",
+ "port": "Puerto",
+ "username": "Usuario"
+ },
+ "title": "Configurar el router Keenetic NDMS2"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "consider_home": "Considerar el intervalo en casa",
+ "include_arp": "Usar datos ARP (ignorado si se usan datos de hotspot)",
+ "include_associated": "Utilizar los datos de las asociaciones WiFi AP (se ignora si se utilizan los datos del hotspot)",
+ "interfaces": "Elija las interfaces para escanear",
+ "scan_interval": "Intervalo de escaneo",
+ "try_hotspot": "Utilizar datos de 'punto de acceso ip' (m\u00e1s precisos)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keenetic_ndms2/translations/et.json b/homeassistant/components/keenetic_ndms2/translations/et.json
new file mode 100644
index 00000000000000..dc500be7e1a115
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/et.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kasutaja on juba seadistatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nimi",
+ "password": "Salas\u00f5na",
+ "port": "Port",
+ "username": "Kasutajanimi"
+ },
+ "title": "Seadista Keenetic NDMS2 ruuter"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "consider_home": "M\u00e4\u00e4ra n\u00e4htavuse aeg",
+ "include_arp": "Kasuta ARP andmeid (ignoreeritakse, kui kasutatakse kuumkoha andmeid)",
+ "include_associated": "Kasuta WiFi AP seoste andmeid (ignoreeritakse kui kasutatakse kuumkoha andmeid)",
+ "interfaces": "Sk\u00e4nnitavate liideste valimine",
+ "scan_interval": "P\u00e4ringute intervall",
+ "try_hotspot": "Kasuta 'ip hotspot' andmeid (k\u00f5ige t\u00e4psem)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keenetic_ndms2/translations/fr.json b/homeassistant/components/keenetic_ndms2/translations/fr.json
new file mode 100644
index 00000000000000..2ac19dcdc64aca
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/fr.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "H\u00f4te",
+ "name": "Nom",
+ "password": "Mot de passe",
+ "port": "Port",
+ "username": "Nom d'utilisateur"
+ },
+ "title": "Configurer le routeur Keenetic NDMS2"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "consider_home": "Consid\u00e9rez l'intervalle de home assistant",
+ "include_arp": "Utiliser les donn\u00e9es ARP (ignor\u00e9es si les donn\u00e9es du hotspot sont utilis\u00e9es)",
+ "include_associated": "Utiliser les donn\u00e9es d'associations WiFi AP (ignor\u00e9es si les donn\u00e9es du hotspot sont utilis\u00e9es)",
+ "interfaces": "Choisissez les interfaces \u00e0 analyser",
+ "scan_interval": "Intervalle d\u2019analyse",
+ "try_hotspot": "Utiliser les donn\u00e9es 'ip hotspot' (plus pr\u00e9cis)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keenetic_ndms2/translations/hu.json b/homeassistant/components/keenetic_ndms2/translations/hu.json
new file mode 100644
index 00000000000000..72482de8604b6d
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/hu.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "name": "N\u00e9v",
+ "password": "Jelsz\u00f3",
+ "port": "Port",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keenetic_ndms2/translations/id.json b/homeassistant/components/keenetic_ndms2/translations/id.json
new file mode 100644
index 00000000000000..6a427a875a0c15
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/id.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nama",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "username": "Nama Pengguna"
+ },
+ "title": "Siapkan Router NDMS2 Keenetik"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "consider_home": "Pertimbangkan interval rumah",
+ "include_arp": "Gunakan data ARP (diabaikan jika data hotspot digunakan)",
+ "include_associated": "Gunakan data asosiasi Wi-Fi AP (diabaikan jika data hotspot digunakan)",
+ "interfaces": "Pilih antarmuka untuk dipindai",
+ "scan_interval": "Interval pindai",
+ "try_hotspot": "Gunakan data 'ip hotspot' (paling akurat)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keenetic_ndms2/translations/it.json b/homeassistant/components/keenetic_ndms2/translations/it.json
new file mode 100644
index 00000000000000..e5a705d14b8c6f
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/it.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nome",
+ "password": "Password",
+ "port": "Porta",
+ "username": "Nome utente"
+ },
+ "title": "Configurare il router Keenetic NDMS2"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "consider_home": "Considerare in casa nell'intervallo di",
+ "include_arp": "Usa i dati ARP (ignorati se vengono utilizzati i dati dell'hotspot)",
+ "include_associated": "Usa i dati delle associazioni WiFi AP (ignorati se si usano i dati dell'hotspot)",
+ "interfaces": "Scegli le interfacce da scansionare",
+ "scan_interval": "Intervallo di scansione",
+ "try_hotspot": "Utilizza i dati \"ip hotspot\" (pi\u00f9 accurato)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keenetic_ndms2/translations/ko.json b/homeassistant/components/keenetic_ndms2/translations/ko.json
new file mode 100644
index 00000000000000..8968d9e7709b2c
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/ko.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "name": "\uc774\ub984",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "port": "\ud3ec\ud2b8",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ },
+ "title": "Keenetic NDMS2 \ub77c\uc6b0\ud130 \uc124\uc815\ud558\uae30"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "consider_home": "\uc7ac\uc2e4 \ucd94\uce21 \uac04\uaca9",
+ "include_arp": "ARP \ub370\uc774\ud130 \uc0ac\uc6a9\ud558\uae30 (\ud56b\uc2a4\ud31f \ub370\uc774\ud130\uac00 \uc0ac\uc6a9\ub418\ub294 \uacbd\uc6b0 \ubb34\uc2dc\ub428)",
+ "include_associated": "WiFi AP \uc5f0\uacb0 \ub370\uc774\ud130 \uc0ac\uc6a9\ud558\uae30 (\ud56b\uc2a4\ud31f \ub370\uc774\ud130\uac00 \uc0ac\uc6a9\ub418\ub294 \uacbd\uc6b0 \ubb34\uc2dc\ub428)",
+ "interfaces": "\uc2a4\uce94\ud560 \uc778\ud130\ud398\uc774\uc2a4\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694",
+ "scan_interval": "\uc2a4\uce94 \uac04\uaca9",
+ "try_hotspot": "'ip \ud56b\uc2a4\ud31f' \ub370\uc774\ud130 \uc0ac\uc6a9\ud558\uae30 (\uac00\uc7a5 \uc815\ud655\ud568)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keenetic_ndms2/translations/nl.json b/homeassistant/components/keenetic_ndms2/translations/nl.json
new file mode 100644
index 00000000000000..b7c89bb65e9f6f
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/nl.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Account is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Naam",
+ "password": "Wachtwoord",
+ "port": "Poort",
+ "username": "Gebruikersnaam"
+ },
+ "title": "Keenetic NDMS2 Router instellen"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "consider_home": "Overweeg thuisinterval",
+ "include_arp": "Gebruik ARP-gegevens (genegeerd als hotspot-gegevens worden gebruikt)",
+ "include_associated": "Gebruik WiFi AP-koppelingsgegevens (genegeerd als hotspot-gegevens worden gebruikt)",
+ "interfaces": "Kies interfaces om te scannen",
+ "scan_interval": "Scaninterval",
+ "try_hotspot": "Gebruik 'ip hotspot'-gegevens (meest nauwkeurig)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keenetic_ndms2/translations/no.json b/homeassistant/components/keenetic_ndms2/translations/no.json
new file mode 100644
index 00000000000000..6ad2805eb3ddb4
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/no.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kontoen er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Vert",
+ "name": "Navn",
+ "password": "Passord",
+ "port": "Port",
+ "username": "Brukernavn"
+ },
+ "title": "Sett opp Keenetic NDMS2 Router"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "consider_home": "Vurder hjemmeintervall",
+ "include_arp": "Bruk ARP-data (ignorert hvis hotspot-data brukes)",
+ "include_associated": "Bruk WiFi AP-tilknytningsdata (ignoreres hvis hotspot-data brukes)",
+ "interfaces": "Velg grensesnitt for \u00e5 skanne",
+ "scan_interval": "Skanneintervall",
+ "try_hotspot": "Bruk 'ip hotspot'-data (mest n\u00f8yaktig)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keenetic_ndms2/translations/pl.json b/homeassistant/components/keenetic_ndms2/translations/pl.json
new file mode 100644
index 00000000000000..13bcbfb91baf23
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/pl.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP",
+ "name": "Nazwa",
+ "password": "Has\u0142o",
+ "port": "Port",
+ "username": "Nazwa u\u017cytkownika"
+ },
+ "title": "Konfiguracja routera Keenetic NDMS2"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "consider_home": "Czas przed oznaczeniem \"poza domem\"",
+ "include_arp": "U\u017cyj danych ARP (ignorowane, je\u015bli u\u017cywane s\u0105 dane hotspotu)",
+ "include_associated": "U\u017cyj danych skojarze\u0144 WiFi AP (ignorowane, je\u015bli u\u017cywane s\u0105 dane hotspotu)",
+ "interfaces": "Wybierz interfejsy do skanowania",
+ "scan_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania",
+ "try_hotspot": "U\u017cyj danych \u201eIP hotspot\u201d (najdok\u0142adniejsze)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keenetic_ndms2/translations/ru.json b/homeassistant/components/keenetic_ndms2/translations/ru.json
new file mode 100644
index 00000000000000..6f99453888e4a1
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/ru.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ },
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 Keenetic NDMS2"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "consider_home": "\u0412\u0440\u0435\u043c\u044f \u043e\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0434\u043e\u043c\u0430\"",
+ "include_arp": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 ARP (\u0438\u0433\u043d\u043e\u0440\u0438\u0440\u0443\u044e\u0442\u0441\u044f, \u0435\u0441\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u0435 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430)",
+ "include_associated": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u0442\u043e\u0447\u0435\u043a \u0434\u043e\u0441\u0442\u0443\u043f\u0430 WiFi (\u0438\u0433\u043d\u043e\u0440\u0438\u0440\u0443\u044e\u0442\u0441\u044f, \u0435\u0441\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u0435 hotspot)",
+ "interfaces": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u044b \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f",
+ "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f",
+ "try_hotspot": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 'ip hotspot' (\u043d\u0430\u0438\u0431\u043e\u043b\u0435\u0435 \u0442\u043e\u0447\u043d\u044b\u0435)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json b/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json
new file mode 100644
index 00000000000000..7900f3a885430b
--- /dev/null
+++ b/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "name": "\u540d\u7a31",
+ "password": "\u5bc6\u78bc",
+ "port": "\u901a\u8a0a\u57e0",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ },
+ "title": "\u8a2d\u5b9a Keenetic NDMS2 \u8def\u7531\u5668"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "consider_home": "\u5224\u5b9a\u5728\u5bb6\u9593\u9694",
+ "include_arp": "\u4f7f\u7528 ARP \u8cc7\u6599\uff08\u5047\u5982\u5df2\u4f7f\u7528 hotspot \u8cc7\u6599\u5247\u5ffd\u7565\uff09",
+ "include_associated": "\u4f7f\u7528 WiFi AP \u95dc\u806f\u8cc7\u6599\uff08\u5047\u5982\u5df2\u4f7f\u7528 hotspot \u8cc7\u6599\u5247\u5ffd\u7565\uff09",
+ "interfaces": "\u9078\u64c7\u6383\u63cf\u4ecb\u9762",
+ "scan_interval": "\u6383\u63cf\u9593\u8ddd",
+ "try_hotspot": "\u4f7f\u7528 'ip hotspot' \u8cc7\u6599\uff08\u6700\u7cbe\u6e96\uff09"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py
index a36fe11ef652de..5316568ab52e91 100644
--- a/homeassistant/components/kef/media_player.py
+++ b/homeassistant/components/kef/media_player.py
@@ -395,7 +395,7 @@ async def async_will_remove_from_hass(self):
self._update_dsp_task_remover = None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the DSP settings of the KEF device."""
return self._dsp or {}
diff --git a/homeassistant/components/keyboard/services.yaml b/homeassistant/components/keyboard/services.yaml
index 8e49cdd6a124c5..d0919d595143bf 100644
--- a/homeassistant/components/keyboard/services.yaml
+++ b/homeassistant/components/keyboard/services.yaml
@@ -1,17 +1,29 @@
volume_up:
- description: Simulates a key press of the "Volume Up" button on Home Assistant's host machine.
+ description:
+ Simulates a key press of the "Volume Up" button on Home Assistant's host
+ machine
volume_down:
- description: Simulates a key press of the "Volume Down" button on Home Assistant's host machine.
+ description:
+ Simulates a key press of the "Volume Down" button on Home Assistant's host
+ machine
volume_mute:
- description: Simulates a key press of the "Volume Mute" button on Home Assistant's host machine.
+ description:
+ Simulates a key press of the "Volume Mute" button on Home Assistant's host
+ machine
media_play_pause:
- description: Simulates a key press of the "Media Play/Pause" button on Home Assistant's host machine.
+ description:
+ Simulates a key press of the "Media Play/Pause" button on Home Assistant's
+ host machine
media_next_track:
- description: Simulates a key press of the "Media Next Track" button on Home Assistant's host machine.
+ description:
+ Simulates a key press of the "Media Next Track" button on Home Assistant's
+ host machine
media_prev_track:
- description: Simulates a key press of the "Media Previous Track" button on Home Assistant's host machine.
+ description:
+ Simulates a key press of the "Media Previous Track" button on Home
+ Assistant's host machine
diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py
index 310bd0189bda39..2ada56e1c44445 100644
--- a/homeassistant/components/keyboard_remote/__init__.py
+++ b/homeassistant/components/keyboard_remote/__init__.py
@@ -1,6 +1,7 @@
"""Receive signals from a keyboard and use it as a remote control."""
# pylint: disable=import-error
import asyncio
+from contextlib import suppress
import logging
import os
@@ -255,10 +256,8 @@ async def async_start_monitoring(self, dev):
async def async_stop_monitoring(self):
"""Stop event monitoring task and issue event."""
if self.monitor_task is not None:
- try:
+ with suppress(OSError):
await self.hass.async_add_executor_job(self.dev.ungrab)
- except OSError:
- pass
# monitoring of the device form the event loop and closing of the
# device has to occur before cancelling the task to avoid
# triggering unhandled exceptions inside evdev coroutines
@@ -313,10 +312,12 @@ async def async_monitor_input(self, dev):
self.emulate_key_hold_repeat,
)
)
- elif event.value == KEY_VALUE["key_up"]:
- if event.code in repeat_tasks:
- repeat_tasks[event.code].cancel()
- del repeat_tasks[event.code]
+ elif (
+ event.value == KEY_VALUE["key_up"]
+ and event.code in repeat_tasks
+ ):
+ repeat_tasks[event.code].cancel()
+ del repeat_tasks[event.code]
except (OSError, PermissionError, asyncio.CancelledError):
# cancel key repeat tasks
for task in repeat_tasks.values():
diff --git a/homeassistant/components/kira/__init__.py b/homeassistant/components/kira/__init__.py
index 8948fbd0b8f51c..732008e5780d97 100644
--- a/homeassistant/components/kira/__init__.py
+++ b/homeassistant/components/kira/__init__.py
@@ -13,6 +13,7 @@
CONF_HOST,
CONF_NAME,
CONF_PORT,
+ CONF_REPEAT,
CONF_SENSORS,
CONF_TYPE,
EVENT_HOMEASSISTANT_STOP,
@@ -28,7 +29,6 @@
DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 65432
-CONF_REPEAT = "repeat"
CONF_REMOTES = "remotes"
CONF_SENSOR = "sensor"
CONF_REMOTE = "remote"
diff --git a/homeassistant/components/kira/remote.py b/homeassistant/components/kira/remote.py
index c9b51fd7ab7a5d..9c02a3199e40bc 100644
--- a/homeassistant/components/kira/remote.py
+++ b/homeassistant/components/kira/remote.py
@@ -6,12 +6,10 @@
from homeassistant.const import CONF_DEVICE, CONF_NAME
from homeassistant.helpers.entity import Entity
-DOMAIN = "kira"
+from . import CONF_REMOTE, DOMAIN
_LOGGER = logging.getLogger(__name__)
-CONF_REMOTE = "remote"
-
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Kira platform."""
diff --git a/homeassistant/components/kira/sensor.py b/homeassistant/components/kira/sensor.py
index 71aeec63232e7b..a6b1b9ada220fe 100644
--- a/homeassistant/components/kira/sensor.py
+++ b/homeassistant/components/kira/sensor.py
@@ -1,17 +1,15 @@
"""KIRA interface to receive UDP packets from an IR-IP bridge."""
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_DEVICE, CONF_NAME, STATE_UNKNOWN
-from homeassistant.helpers.entity import Entity
-DOMAIN = "kira"
+from . import CONF_SENSOR, DOMAIN
_LOGGER = logging.getLogger(__name__)
ICON = "mdi:remote"
-CONF_SENSOR = "sensor"
-
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a Kira sensor."""
@@ -23,7 +21,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([KiraReceiver(device, kira)])
-class KiraReceiver(Entity):
+class KiraReceiver(SensorEntity):
"""Implementation of a Kira Receiver."""
def __init__(self, name, kira):
@@ -57,7 +55,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
return {CONF_DEVICE: self._device}
diff --git a/homeassistant/components/kiwi/lock.py b/homeassistant/components/kiwi/lock.py
index 047eaa1ed3c2c9..8a0eeed83f0ead 100644
--- a/homeassistant/components/kiwi/lock.py
+++ b/homeassistant/components/kiwi/lock.py
@@ -86,7 +86,7 @@ def is_locked(self):
return self._state == STATE_LOCKED
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
return self._device_attrs
@@ -102,7 +102,7 @@ def unlock(self, **kwargs):
try:
self._client.open_door(self.lock_id)
except KiwiException:
- _LOGGER.error("failed to open door")
+ _LOGGER.error("Failed to open door")
else:
self._state = STATE_UNLOCKED
self.hass.add_job(
diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py
new file mode 100644
index 00000000000000..241e65fbe7fddd
--- /dev/null
+++ b/homeassistant/components/kmtronic/__init__.py
@@ -0,0 +1,102 @@
+"""The kmtronic integration."""
+import asyncio
+from datetime import timedelta
+import logging
+
+import aiohttp
+import async_timeout
+from pykmtronic.auth import Auth
+from pykmtronic.hub import KMTronicHubAPI
+import voluptuous as vol
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import aiohttp_client
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import DATA_COORDINATOR, DATA_HUB, DOMAIN, MANUFACTURER, UPDATE_LISTENER
+
+CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
+
+PLATFORMS = ["switch"]
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+ """Set up the kmtronic component."""
+ hass.data[DOMAIN] = {}
+
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up kmtronic from a config entry."""
+ session = aiohttp_client.async_get_clientsession(hass)
+ auth = Auth(
+ session,
+ f"http://{entry.data[CONF_HOST]}",
+ entry.data[CONF_USERNAME],
+ entry.data[CONF_PASSWORD],
+ )
+ hub = KMTronicHubAPI(auth)
+
+ async def async_update_data():
+ try:
+ async with async_timeout.timeout(10):
+ await hub.async_update_relays()
+ except aiohttp.client_exceptions.ClientResponseError as err:
+ raise UpdateFailed(f"Wrong credentials: {err}") from err
+ except (
+ asyncio.TimeoutError,
+ aiohttp.client_exceptions.ClientConnectorError,
+ ) as err:
+ raise UpdateFailed(f"Error communicating with API: {err}") from err
+
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name=f"{MANUFACTURER} {hub.name}",
+ update_method=async_update_data,
+ update_interval=timedelta(seconds=30),
+ )
+ await coordinator.async_config_entry_first_refresh()
+
+ hass.data[DOMAIN][entry.entry_id] = {
+ DATA_HUB: hub,
+ DATA_COORDINATOR: coordinator,
+ }
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
+ update_listener = entry.add_update_listener(async_update_options)
+ hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener
+
+ return True
+
+
+async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
+ """Update options."""
+ await hass.config_entries.async_reload(config_entry.entry_id)
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ update_listener = hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER]
+ update_listener()
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
diff --git a/homeassistant/components/kmtronic/config_flow.py b/homeassistant/components/kmtronic/config_flow.py
new file mode 100644
index 00000000000000..914c00ae6f2046
--- /dev/null
+++ b/homeassistant/components/kmtronic/config_flow.py
@@ -0,0 +1,111 @@
+"""Config flow for kmtronic integration."""
+import logging
+
+import aiohttp
+from pykmtronic.auth import Auth
+from pykmtronic.hub import KMTronicHubAPI
+import voluptuous as vol
+
+from homeassistant import config_entries, core, exceptions
+from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
+from homeassistant.core import callback
+from homeassistant.helpers import aiohttp_client
+
+from .const import CONF_REVERSE, DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+DATA_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_HOST): str,
+ vol.Required(CONF_USERNAME): str,
+ vol.Required(CONF_PASSWORD): str,
+ }
+)
+
+
+async def validate_input(hass: core.HomeAssistant, data):
+ """Validate the user input allows us to connect."""
+ session = aiohttp_client.async_get_clientsession(hass)
+ auth = Auth(
+ session,
+ f"http://{data[CONF_HOST]}",
+ data[CONF_USERNAME],
+ data[CONF_PASSWORD],
+ )
+ hub = KMTronicHubAPI(auth)
+
+ try:
+ await hub.async_get_status()
+ except aiohttp.client_exceptions.ClientResponseError as err:
+ raise InvalidAuth from err
+ except aiohttp.client_exceptions.ClientConnectorError as err:
+ raise CannotConnect from err
+
+ return data
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for kmtronic."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for this handler."""
+ return KMTronicOptionsFlow(config_entry)
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ errors = {}
+ if user_input is not None:
+ try:
+ info = await validate_input(self.hass, user_input)
+
+ return self.async_create_entry(title=info["host"], data=user_input)
+ except CannotConnect:
+ errors["base"] = "cannot_connect"
+ except InvalidAuth:
+ errors["base"] = "invalid_auth"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+
+ return self.async_show_form(
+ step_id="user", data_schema=DATA_SCHEMA, errors=errors
+ )
+
+
+class CannotConnect(exceptions.HomeAssistantError):
+ """Error to indicate we cannot connect."""
+
+
+class InvalidAuth(exceptions.HomeAssistantError):
+ """Error to indicate there is invalid auth."""
+
+
+class KMTronicOptionsFlow(config_entries.OptionsFlow):
+ """Handle options."""
+
+ def __init__(self, config_entry):
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(self, user_input=None):
+ """Manage the options."""
+ if user_input is not None:
+ return self.async_create_entry(title="", data=user_input)
+
+ return self.async_show_form(
+ step_id="init",
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_REVERSE,
+ default=self.config_entry.options.get(CONF_REVERSE),
+ ): bool,
+ }
+ ),
+ )
diff --git a/homeassistant/components/kmtronic/const.py b/homeassistant/components/kmtronic/const.py
new file mode 100644
index 00000000000000..8b34d423724392
--- /dev/null
+++ b/homeassistant/components/kmtronic/const.py
@@ -0,0 +1,14 @@
+"""Constants for the kmtronic integration."""
+
+DOMAIN = "kmtronic"
+
+CONF_REVERSE = "reverse"
+
+DATA_HUB = "hub"
+DATA_COORDINATOR = "coordinator"
+
+MANUFACTURER = "KMtronic"
+ATTR_MANUFACTURER = "manufacturer"
+ATTR_IDENTIFIERS = "identifiers"
+
+UPDATE_LISTENER = "update_listener"
diff --git a/homeassistant/components/kmtronic/manifest.json b/homeassistant/components/kmtronic/manifest.json
new file mode 100644
index 00000000000000..27e9f953eb780e
--- /dev/null
+++ b/homeassistant/components/kmtronic/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "kmtronic",
+ "name": "KMtronic",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/kmtronic",
+ "requirements": ["pykmtronic==0.0.3"],
+ "codeowners": ["@dgomes"]
+}
diff --git a/homeassistant/components/kmtronic/strings.json b/homeassistant/components/kmtronic/strings.json
new file mode 100644
index 00000000000000..2aaa0d2f8dddec
--- /dev/null
+++ b/homeassistant/components/kmtronic/strings.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "[%key:common::config_flow::data::host%]",
+ "username": "[%key:common::config_flow::data::username%]",
+ "password": "[%key:common::config_flow::data::password%]"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "reverse": "Reverse switch logic (use NC)"
+ }
+ }
+ }
+ }
+}
diff --git a/homeassistant/components/kmtronic/switch.py b/homeassistant/components/kmtronic/switch.py
new file mode 100644
index 00000000000000..d37cd54ce1a00c
--- /dev/null
+++ b/homeassistant/components/kmtronic/switch.py
@@ -0,0 +1,65 @@
+"""KMtronic Switch integration."""
+
+from homeassistant.components.switch import SwitchEntity
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import CONF_REVERSE, DATA_COORDINATOR, DATA_HUB, DOMAIN
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Config entry example."""
+ coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
+ hub = hass.data[DOMAIN][entry.entry_id][DATA_HUB]
+ reverse = entry.options.get(CONF_REVERSE, False)
+ await hub.async_get_relays()
+
+ async_add_entities(
+ [
+ KMtronicSwitch(coordinator, relay, reverse, entry.entry_id)
+ for relay in hub.relays
+ ]
+ )
+
+
+class KMtronicSwitch(CoordinatorEntity, SwitchEntity):
+ """KMtronic Switch Entity."""
+
+ def __init__(self, coordinator, relay, reverse, config_entry_id):
+ """Pass coordinator to CoordinatorEntity."""
+ super().__init__(coordinator)
+ self._relay = relay
+ self._config_entry_id = config_entry_id
+ self._reverse = reverse
+
+ @property
+ def name(self) -> str:
+ """Return the name of the entity."""
+ return f"Relay{self._relay.id}"
+
+ @property
+ def unique_id(self) -> str:
+ """Return the unique ID of the entity."""
+ return f"{self._config_entry_id}_relay{self._relay.id}"
+
+ @property
+ def is_on(self):
+ """Return entity state."""
+ if self._reverse:
+ return not self._relay.is_on
+ return self._relay.is_on
+
+ async def async_turn_on(self, **kwargs) -> None:
+ """Turn the switch on."""
+ if self._reverse:
+ await self._relay.turn_off()
+ else:
+ await self._relay.turn_on()
+ self.async_write_ha_state()
+
+ async def async_turn_off(self, **kwargs) -> None:
+ """Turn the switch off."""
+ if self._reverse:
+ await self._relay.turn_on()
+ else:
+ await self._relay.turn_off()
+ self.async_write_ha_state()
diff --git a/homeassistant/components/kmtronic/translations/bg.json b/homeassistant/components/kmtronic/translations/bg.json
new file mode 100644
index 00000000000000..a84e1c3bfdf358
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/bg.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "error": {
+ "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/kmtronic/translations/ca.json b/homeassistant/components/kmtronic/translations/ca.json
new file mode 100644
index 00000000000000..0847a86a41bcf3
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/ca.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "password": "Contrasenya",
+ "username": "Nom d'usuari"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "reverse": "L\u00f2gica de commutaci\u00f3 inversa (utilitza NC)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kmtronic/translations/cs.json b/homeassistant/components/kmtronic/translations/cs.json
new file mode 100644
index 00000000000000..0f02cd974c207c
--- /dev/null
+++ b/homeassistant/components/kmtronic/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",
+ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hostitel",
+ "password": "Heslo",
+ "username": "U\u017eivatelsk\u00e9 jm\u00e9no"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kmtronic/translations/de.json b/homeassistant/components/kmtronic/translations/de.json
new file mode 100644
index 00000000000000..625c7372347a61
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/de.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kmtronic/translations/en.json b/homeassistant/components/kmtronic/translations/en.json
new file mode 100644
index 00000000000000..61da788028e29e
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/en.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Password",
+ "username": "Username"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "reverse": "Reverse switch logic (use NC)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kmtronic/translations/es.json b/homeassistant/components/kmtronic/translations/es.json
new file mode 100644
index 00000000000000..f7c20f7805bade
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/es.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositivo ya est\u00e1 configurado"
+ },
+ "error": {
+ "cannot_connect": "Fallo al conectar",
+ "unknown": "Error inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrase\u00f1a",
+ "username": "Usuario"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "reverse": "L\u00f3gica de conmutaci\u00f3n inversa (utilizar NC)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kmtronic/translations/et.json b/homeassistant/components/kmtronic/translations/et.json
new file mode 100644
index 00000000000000..d501e1d1962a9f
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/et.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_auth": "Vigane autentimine",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Salas\u00f5na",
+ "username": "Kasutajanimi"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "reverse": "P\u00f6\u00f6rdl\u00fcliti loogika (kasuta NC-d)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kmtronic/translations/fr.json b/homeassistant/components/kmtronic/translations/fr.json
new file mode 100644
index 00000000000000..7c0050bfad82c8
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/fr.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "invalid_auth": "Authentification invalide",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "H\u00f4te",
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "reverse": "Logique de commutation inverse (utiliser NC)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kmtronic/translations/hu.json b/homeassistant/components/kmtronic/translations/hu.json
new file mode 100644
index 00000000000000..0abcc301f0c854
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/hu.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kmtronic/translations/id.json b/homeassistant/components/kmtronic/translations/id.json
new file mode 100644
index 00000000000000..ed8fde321061cf
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kmtronic/translations/it.json b/homeassistant/components/kmtronic/translations/it.json
new file mode 100644
index 00000000000000..59aeaa25f6f66a
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/it.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Password",
+ "username": "Nome utente"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "reverse": "Invertire la logica dell'interruttore (usare NC)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kmtronic/translations/ko.json b/homeassistant/components/kmtronic/translations/ko.json
new file mode 100644
index 00000000000000..cf62f34c75568f
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/ko.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "reverse": "\uc2a4\uc704\uce58 \ubc29\uc2dd \ubcc0\uacbd (\uc0c1\uc2dc\ud3d0\ub85c(NC)\ub85c \uc0ac\uc6a9)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kmtronic/translations/nl.json b/homeassistant/components/kmtronic/translations/nl.json
new file mode 100644
index 00000000000000..9c32e48b20ad10
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/nl.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Wachtwoord",
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "reverse": "Omgekeerde schakelaarlogica (gebruik NC)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kmtronic/translations/no.json b/homeassistant/components/kmtronic/translations/no.json
new file mode 100644
index 00000000000000..1b192f7895ea09
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/no.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_auth": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Vert",
+ "password": "Passord",
+ "username": "Brukernavn"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "reverse": "Omvendt bryterlogikk (bruk NC)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kmtronic/translations/pl.json b/homeassistant/components/kmtronic/translations/pl.json
new file mode 100644
index 00000000000000..6b0d06df11be12
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/pl.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP",
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "reverse": "Odwr\u00f3\u0107 logik\u0119 prze\u0142\u0105cznika (u\u017cyj NC)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kmtronic/translations/pt.json b/homeassistant/components/kmtronic/translations/pt.json
new file mode 100644
index 00000000000000..6ff15c6c8d7bf0
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/pt.json
@@ -0,0 +1,30 @@
+{
+ "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": {
+ "host": "Servidor",
+ "password": "Palavra-passe",
+ "username": "Nome de Utilizador"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "reverse": "Inverter l\u00f3gica do interruptor (usar NC)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kmtronic/translations/ru.json b/homeassistant/components/kmtronic/translations/ru.json
new file mode 100644
index 00000000000000..219af38fae9d8c
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/ru.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "reverse": "\u041b\u043e\u0433\u0438\u043a\u0430 \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f (\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c NC)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kmtronic/translations/zh-Hant.json b/homeassistant/components/kmtronic/translations/zh-Hant.json
new file mode 100644
index 00000000000000..5027bc2f5b2612
--- /dev/null
+++ b/homeassistant/components/kmtronic/translations/zh-Hant.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "password": "\u5bc6\u78bc",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "reverse": "\u53cd\u5411\u958b\u95dc\u908f\u8f2f\uff08\u4f7f\u7528 NC\uff09"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py
index 7dbeb513e091f1..0fe3e133b6eb12 100644
--- a/homeassistant/components/knx/__init__.py
+++ b/homeassistant/components/knx/__init__.py
@@ -1,11 +1,12 @@
"""Support KNX devices."""
+from __future__ import annotations
+
import asyncio
import logging
import voluptuous as vol
from xknx import XKNX
from xknx.core.telegram_queue import TelegramQueue
-from xknx.devices import DateTime, ExposeSensor
from xknx.dpt import DPTArray, DPTBase, DPTBinary
from xknx.exceptions import XKNXException
from xknx.io import (
@@ -15,29 +16,25 @@
ConnectionType,
)
from xknx.telegram import AddressFilter, GroupAddress, Telegram
-from xknx.telegram.apci import GroupValueResponse, GroupValueWrite
+from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite
from homeassistant.const import (
- CONF_ENTITY_ID,
CONF_HOST,
CONF_PORT,
EVENT_HOMEASSISTANT_STOP,
SERVICE_RELOAD,
- STATE_OFF,
- STATE_ON,
- STATE_UNAVAILABLE,
- STATE_UNKNOWN,
)
-from homeassistant.core import callback
+from homeassistant.core import Event, HomeAssistant, ServiceCall
+from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import async_get_platforms
-from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.service import async_register_admin_service
-from homeassistant.helpers.typing import ServiceCallType
+from homeassistant.helpers.typing import ConfigType
-from .const import DOMAIN, SupportedPlatforms
+from .const import DOMAIN, KNX_ADDRESS, SupportedPlatforms
+from .expose import KNXExposeSensor, KNXExposeTime, create_knx_exposure
from .factory import create_knx_device
from .schema import (
BinarySensorSchema,
@@ -45,18 +42,20 @@
ConnectionSchema,
CoverSchema,
ExposeSchema,
+ FanSchema,
LightSchema,
NotifySchema,
SceneSchema,
SensorSchema,
SwitchSchema,
WeatherSchema,
+ ga_validator,
+ ia_validator,
+ sensor_type_validator,
)
_LOGGER = logging.getLogger(__name__)
-CONF_KNX_CONFIG = "config_file"
-
CONF_KNX_ROUTING = "routing"
CONF_KNX_TUNNELING = "tunneling"
CONF_KNX_FIRE_EVENT = "fire_event"
@@ -69,20 +68,23 @@
CONF_KNX_EXPOSE = "expose"
SERVICE_KNX_SEND = "send"
-SERVICE_KNX_ATTR_ADDRESS = "address"
SERVICE_KNX_ATTR_PAYLOAD = "payload"
SERVICE_KNX_ATTR_TYPE = "type"
SERVICE_KNX_ATTR_REMOVE = "remove"
SERVICE_KNX_EVENT_REGISTER = "event_register"
+SERVICE_KNX_EXPOSURE_REGISTER = "exposure_register"
+SERVICE_KNX_READ = "read"
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
+ # deprecated since 2021.4
+ cv.deprecated("config_file"),
+ # deprecated since 2021.2
cv.deprecated(CONF_KNX_FIRE_EVENT),
cv.deprecated("fire_event_filter", replacement_key=CONF_KNX_EVENT_FILTER),
vol.Schema(
{
- vol.Optional(CONF_KNX_CONFIG): cv.string,
vol.Exclusive(
CONF_KNX_ROUTING, "connection_type"
): ConnectionSchema.ROUTING_SCHEMA,
@@ -95,7 +97,7 @@
),
vol.Optional(
CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS
- ): cv.string,
+ ): ia_validator,
vol.Optional(
CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP
): cv.string,
@@ -109,33 +111,36 @@
vol.Optional(CONF_KNX_EXPOSE): vol.All(
cv.ensure_list, [ExposeSchema.SCHEMA]
),
- vol.Optional(SupportedPlatforms.cover.value): vol.All(
+ vol.Optional(SupportedPlatforms.COVER.value): vol.All(
cv.ensure_list, [CoverSchema.SCHEMA]
),
- vol.Optional(SupportedPlatforms.binary_sensor.value): vol.All(
+ vol.Optional(SupportedPlatforms.BINARY_SENSOR.value): vol.All(
cv.ensure_list, [BinarySensorSchema.SCHEMA]
),
- vol.Optional(SupportedPlatforms.light.value): vol.All(
+ vol.Optional(SupportedPlatforms.LIGHT.value): vol.All(
cv.ensure_list, [LightSchema.SCHEMA]
),
- vol.Optional(SupportedPlatforms.climate.value): vol.All(
+ vol.Optional(SupportedPlatforms.CLIMATE.value): vol.All(
cv.ensure_list, [ClimateSchema.SCHEMA]
),
- vol.Optional(SupportedPlatforms.notify.value): vol.All(
+ vol.Optional(SupportedPlatforms.NOTIFY.value): vol.All(
cv.ensure_list, [NotifySchema.SCHEMA]
),
- vol.Optional(SupportedPlatforms.switch.value): vol.All(
+ vol.Optional(SupportedPlatforms.SWITCH.value): vol.All(
cv.ensure_list, [SwitchSchema.SCHEMA]
),
- vol.Optional(SupportedPlatforms.sensor.value): vol.All(
+ vol.Optional(SupportedPlatforms.SENSOR.value): vol.All(
cv.ensure_list, [SensorSchema.SCHEMA]
),
- vol.Optional(SupportedPlatforms.scene.value): vol.All(
+ vol.Optional(SupportedPlatforms.SCENE.value): vol.All(
cv.ensure_list, [SceneSchema.SCHEMA]
),
- vol.Optional(SupportedPlatforms.weather.value): vol.All(
+ vol.Optional(SupportedPlatforms.WEATHER.value): vol.All(
cv.ensure_list, [WeatherSchema.SCHEMA]
),
+ vol.Optional(SupportedPlatforms.FAN.value): vol.All(
+ cv.ensure_list, [FanSchema.SCHEMA]
+ ),
}
),
)
@@ -146,15 +151,21 @@
SERVICE_KNX_SEND_SCHEMA = vol.Any(
vol.Schema(
{
- vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string,
+ vol.Required(KNX_ADDRESS): vol.All(
+ cv.ensure_list,
+ [ga_validator],
+ ),
vol.Required(SERVICE_KNX_ATTR_PAYLOAD): cv.match_all,
- vol.Required(SERVICE_KNX_ATTR_TYPE): vol.Any(int, float, str),
+ vol.Required(SERVICE_KNX_ATTR_TYPE): sensor_type_validator,
}
),
vol.Schema(
# without type given payload is treated as raw bytes
{
- vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string,
+ vol.Required(KNX_ADDRESS): vol.All(
+ cv.ensure_list,
+ [ga_validator],
+ ),
vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any(
cv.positive_int, [cv.positive_int]
),
@@ -162,30 +173,64 @@
),
)
+SERVICE_KNX_READ_SCHEMA = vol.Schema(
+ {
+ vol.Required(KNX_ADDRESS): vol.All(
+ cv.ensure_list,
+ [ga_validator],
+ )
+ }
+)
+
SERVICE_KNX_EVENT_REGISTER_SCHEMA = vol.Schema(
{
- vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string,
+ vol.Required(KNX_ADDRESS): vol.All(
+ cv.ensure_list,
+ [ga_validator],
+ ),
vol.Optional(SERVICE_KNX_ATTR_REMOVE, default=False): cv.boolean,
}
)
+SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any(
+ ExposeSchema.EXPOSE_SENSOR_SCHEMA.extend(
+ {
+ vol.Optional(SERVICE_KNX_ATTR_REMOVE, default=False): cv.boolean,
+ }
+ ),
+ vol.Schema(
+ # for removing only `address` is required
+ {
+ vol.Required(KNX_ADDRESS): ga_validator,
+ vol.Required(SERVICE_KNX_ATTR_REMOVE): vol.All(cv.boolean, True),
+ },
+ extra=vol.ALLOW_EXTRA,
+ ),
+)
-async def async_setup(hass, config):
- """Set up the KNX component."""
+
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
+ """Set up the KNX integration."""
try:
- hass.data[DOMAIN] = KNXModule(hass, config)
- hass.data[DOMAIN].async_create_exposures()
- await hass.data[DOMAIN].start()
+ knx_module = KNXModule(hass, config)
+ hass.data[DOMAIN] = knx_module
+ await knx_module.start()
except XKNXException as ex:
_LOGGER.warning("Could not connect to KNX interface: %s", ex)
hass.components.persistent_notification.async_create(
f"Could not connect to KNX interface:
{ex}", title="KNX"
)
+ if CONF_KNX_EXPOSE in config[DOMAIN]:
+ for expose_config in config[DOMAIN][CONF_KNX_EXPOSE]:
+ knx_module.exposures.append(
+ create_knx_exposure(hass, knx_module.xknx, expose_config)
+ )
+
for platform in SupportedPlatforms:
if platform.value in config[DOMAIN]:
for device_config in config[DOMAIN][platform.value]:
- create_knx_device(platform, hass.data[DOMAIN].xknx, device_config)
+ create_knx_device(platform, knx_module.xknx, device_config)
# We need to wait until all entities are loaded into the device list since they could also be created from other platforms
for platform in SupportedPlatforms:
@@ -193,28 +238,37 @@ async def async_setup(hass, config):
discovery.async_load_platform(hass, platform.value, DOMAIN, {}, config)
)
- if not hass.data[DOMAIN].xknx.devices:
- _LOGGER.warning(
- "No KNX devices are configured. Please read "
- "https://www.home-assistant.io/blog/2020/09/17/release-115/#breaking-changes"
- )
-
hass.services.async_register(
DOMAIN,
SERVICE_KNX_SEND,
- hass.data[DOMAIN].service_send_to_knx_bus,
+ knx_module.service_send_to_knx_bus,
schema=SERVICE_KNX_SEND_SCHEMA,
)
+ hass.services.async_register(
+ DOMAIN,
+ SERVICE_KNX_READ,
+ knx_module.service_read_to_knx_bus,
+ schema=SERVICE_KNX_READ_SCHEMA,
+ )
+
async_register_admin_service(
hass,
DOMAIN,
SERVICE_KNX_EVENT_REGISTER,
- hass.data[DOMAIN].service_event_register_modify,
+ knx_module.service_event_register_modify,
schema=SERVICE_KNX_EVENT_REGISTER_SCHEMA,
)
- async def reload_service_handler(service_call: ServiceCallType) -> None:
+ async_register_admin_service(
+ hass,
+ DOMAIN,
+ SERVICE_KNX_EXPOSURE_REGISTER,
+ knx_module.service_exposure_register_modify,
+ schema=SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA,
+ )
+
+ async def reload_service_handler(service_call: ServiceCall) -> None:
"""Remove all KNX components and load new ones from config."""
# First check for config file. If for some reason it is no longer there
@@ -224,7 +278,7 @@ async def reload_service_handler(service_call: ServiceCallType) -> None:
if not config or DOMAIN not in config:
return
- await hass.data[DOMAIN].xknx.stop()
+ await knx_module.xknx.stop()
await asyncio.gather(
*[platform.async_reset() for platform in async_get_platforms(hass, DOMAIN)]
@@ -242,20 +296,20 @@ async def reload_service_handler(service_call: ServiceCallType) -> None:
class KNXModule:
"""Representation of KNX Object."""
- def __init__(self, hass, config):
- """Initialize of KNX module."""
+ def __init__(self, hass: HomeAssistant, config: ConfigType) -> None:
+ """Initialize KNX module."""
self.hass = hass
self.config = config
self.connected = False
- self.exposures = []
+ self.exposures: list[KNXExposeSensor | KNXExposeTime] = []
+ self.service_exposures: dict[str, KNXExposeSensor | KNXExposeTime] = {}
self.init_xknx()
self._knx_event_callback: TelegramQueue.Callback = self.register_callback()
- def init_xknx(self):
- """Initialize of KNX object."""
+ def init_xknx(self) -> None:
+ """Initialize XKNX object."""
self.xknx = XKNX(
- config=self.config_file(),
own_address=self.config[DOMAIN][CONF_KNX_INDIVIDUAL_ADDRESS],
rate_limit=self.config[DOMAIN][CONF_KNX_RATE_LIMIT],
multicast_group=self.config[DOMAIN][CONF_KNX_MCAST_GRP],
@@ -264,92 +318,64 @@ def init_xknx(self):
state_updater=self.config[DOMAIN][CONF_KNX_STATE_UPDATER],
)
- async def start(self):
- """Start KNX object. Connect to tunneling or Routing device."""
+ async def start(self) -> None:
+ """Start XKNX object. Connect to tunneling or Routing device."""
await self.xknx.start()
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
self.connected = True
- async def stop(self, event):
- """Stop KNX object. Disconnect from tunneling or Routing device."""
+ async def stop(self, event: Event) -> None:
+ """Stop XKNX object. Disconnect from tunneling or Routing device."""
await self.xknx.stop()
- def config_file(self):
- """Resolve and return the full path of xknx.yaml if configured."""
- config_file = self.config[DOMAIN].get(CONF_KNX_CONFIG)
- if not config_file:
- return None
- if not config_file.startswith("/"):
- return self.hass.config.path(config_file)
- return config_file
-
- def connection_config(self):
+ def connection_config(self) -> ConnectionConfig:
"""Return the connection_config."""
if CONF_KNX_TUNNELING in self.config[DOMAIN]:
return self.connection_config_tunneling()
if CONF_KNX_ROUTING in self.config[DOMAIN]:
return self.connection_config_routing()
# config from xknx.yaml always has priority later on
- return ConnectionConfig()
+ return ConnectionConfig(auto_reconnect=True)
- def connection_config_routing(self):
+ def connection_config_routing(self) -> ConnectionConfig:
"""Return the connection_config if routing is configured."""
- local_ip = self.config[DOMAIN][CONF_KNX_ROUTING].get(
- ConnectionSchema.CONF_KNX_LOCAL_IP
- )
+ local_ip = None
+ # all configuration values are optional
+ if self.config[DOMAIN][CONF_KNX_ROUTING] is not None:
+ local_ip = self.config[DOMAIN][CONF_KNX_ROUTING].get(
+ ConnectionSchema.CONF_KNX_LOCAL_IP
+ )
return ConnectionConfig(
connection_type=ConnectionType.ROUTING, local_ip=local_ip
)
- def connection_config_tunneling(self):
+ def connection_config_tunneling(self) -> ConnectionConfig:
"""Return the connection_config if tunneling is configured."""
gateway_ip = self.config[DOMAIN][CONF_KNX_TUNNELING][CONF_HOST]
gateway_port = self.config[DOMAIN][CONF_KNX_TUNNELING][CONF_PORT]
local_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get(
ConnectionSchema.CONF_KNX_LOCAL_IP
)
+ route_back = self.config[DOMAIN][CONF_KNX_TUNNELING][
+ ConnectionSchema.CONF_KNX_ROUTE_BACK
+ ]
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING,
gateway_ip=gateway_ip,
gateway_port=gateway_port,
local_ip=local_ip,
+ route_back=route_back,
auto_reconnect=True,
)
- @callback
- def async_create_exposures(self):
- """Create exposures."""
- if CONF_KNX_EXPOSE not in self.config[DOMAIN]:
- return
- for to_expose in self.config[DOMAIN][CONF_KNX_EXPOSE]:
- expose_type = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_TYPE)
- entity_id = to_expose.get(CONF_ENTITY_ID)
- attribute = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_ATTRIBUTE)
- default = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT)
- address = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_ADDRESS)
- if expose_type.lower() in ["time", "date", "datetime"]:
- exposure = KNXExposeTime(self.xknx, expose_type, address)
- exposure.async_register()
- self.exposures.append(exposure)
- else:
- exposure = KNXExposeSensor(
- self.hass,
- self.xknx,
- expose_type,
- entity_id,
- attribute,
- default,
- address,
- )
- exposure.async_register()
- self.exposures.append(exposure)
-
- async def telegram_received_cb(self, telegram):
+ async def telegram_received_cb(self, telegram: Telegram) -> None:
"""Call invoked after a KNX telegram was received."""
data = None
-
# Not all telegrams have serializable data.
- if isinstance(telegram.payload, (GroupValueWrite, GroupValueResponse)):
+ if (
+ isinstance(telegram.payload, (GroupValueWrite, GroupValueResponse))
+ and telegram.payload.value is not None
+ ):
data = telegram.payload.value.value
self.hass.bus.async_fire(
@@ -368,139 +394,97 @@ def register_callback(self) -> TelegramQueue.Callback:
address_filters = list(
map(AddressFilter, self.config[DOMAIN][CONF_KNX_EVENT_FILTER])
)
- return self.xknx.telegram_queue.register_telegram_received_cb(
+ return self.xknx.telegram_queue.register_telegram_received_cb( # type: ignore[no-any-return]
self.telegram_received_cb,
address_filters=address_filters,
group_addresses=[],
+ match_for_outgoing=True,
)
- async def service_event_register_modify(self, call):
+ async def service_event_register_modify(self, call: ServiceCall) -> None:
"""Service for adding or removing a GroupAddress to the knx_event filter."""
- group_address = GroupAddress(call.data.get(SERVICE_KNX_ATTR_ADDRESS))
+ attr_address = call.data[KNX_ADDRESS]
+ group_addresses = map(GroupAddress, attr_address)
+
+ if call.data.get(SERVICE_KNX_ATTR_REMOVE):
+ for group_address in group_addresses:
+ try:
+ self._knx_event_callback.group_addresses.remove(group_address)
+ except ValueError:
+ _LOGGER.warning(
+ "Service event_register could not remove event for '%s'",
+ str(group_address),
+ )
+ else:
+ for group_address in group_addresses:
+ if group_address not in self._knx_event_callback.group_addresses:
+ self._knx_event_callback.group_addresses.append(group_address)
+ _LOGGER.debug(
+ "Service event_register registered event for '%s'",
+ str(group_address),
+ )
+
+ async def service_exposure_register_modify(self, call: ServiceCall) -> None:
+ """Service for adding or removing an exposure to KNX bus."""
+ group_address = call.data[KNX_ADDRESS]
+
if call.data.get(SERVICE_KNX_ATTR_REMOVE):
try:
- self._knx_event_callback.group_addresses.remove(group_address)
- except ValueError:
- _LOGGER.warning(
- "Service event_register could not remove event for '%s'",
- group_address,
- )
- elif group_address not in self._knx_event_callback.group_addresses:
- self._knx_event_callback.group_addresses.append(group_address)
- _LOGGER.debug(
- "Service event_register registered event for '%s'",
+ removed_exposure = self.service_exposures.pop(group_address)
+ except KeyError as err:
+ raise HomeAssistantError(
+ f"Could not find exposure for '{group_address}' to remove."
+ ) from err
+ else:
+ removed_exposure.shutdown()
+ return
+
+ if group_address in self.service_exposures:
+ replaced_exposure = self.service_exposures.pop(group_address)
+ assert replaced_exposure.device is not None
+ _LOGGER.warning(
+ "Service exposure_register replacing already registered exposure for '%s' - %s",
group_address,
+ replaced_exposure.device.name,
)
+ replaced_exposure.shutdown()
+ exposure = create_knx_exposure(self.hass, self.xknx, call.data) # type: ignore[arg-type]
+ self.service_exposures[group_address] = exposure
+ _LOGGER.debug(
+ "Service exposure_register registered exposure for '%s' - %s",
+ group_address,
+ exposure.device.name,
+ )
- async def service_send_to_knx_bus(self, call):
+ async def service_send_to_knx_bus(self, call: ServiceCall) -> None:
"""Service for sending an arbitrary KNX message to the KNX bus."""
- attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD)
- attr_address = call.data.get(SERVICE_KNX_ATTR_ADDRESS)
+ attr_address = call.data[KNX_ADDRESS]
+ attr_payload = call.data[SERVICE_KNX_ATTR_PAYLOAD]
attr_type = call.data.get(SERVICE_KNX_ATTR_TYPE)
- def calculate_payload(attr_payload):
- """Calculate payload depending on type of attribute."""
- if attr_type is not None:
- transcoder = DPTBase.parse_transcoder(attr_type)
- if transcoder is None:
- raise ValueError(f"Invalid type for knx.send service: {attr_type}")
- return DPTArray(transcoder.to_knx(attr_payload))
- if isinstance(attr_payload, int):
- return DPTBinary(attr_payload)
- return DPTArray(attr_payload)
-
- telegram = Telegram(
- destination_address=GroupAddress(attr_address),
- payload=GroupValueWrite(calculate_payload(attr_payload)),
- )
- await self.xknx.telegrams.put(telegram)
-
-
-class KNXExposeTime:
- """Object to Expose Time/Date object to KNX bus."""
-
- def __init__(self, xknx: XKNX, expose_type: str, address: str):
- """Initialize of Expose class."""
- self.xknx = xknx
- self.expose_type = expose_type
- self.address = address
- self.device = None
-
- @callback
- def async_register(self):
- """Register listener."""
- self.device = DateTime(
- self.xknx,
- name=self.expose_type.capitalize(),
- broadcast_type=self.expose_type.upper(),
- localtime=True,
- group_address=self.address,
- )
-
-
-class KNXExposeSensor:
- """Object to Expose Home Assistant entity to KNX bus."""
-
- def __init__(self, hass, xknx, expose_type, entity_id, attribute, default, address):
- """Initialize of Expose class."""
- self.hass = hass
- self.xknx = xknx
- self.type = expose_type
- self.entity_id = entity_id
- self.expose_attribute = attribute
- self.expose_default = default
- self.address = address
- self.device = None
-
- @callback
- def async_register(self):
- """Register listener."""
- if self.expose_attribute is not None:
- _name = self.entity_id + "__" + self.expose_attribute
+ payload: DPTBinary | DPTArray
+ if attr_type is not None:
+ transcoder = DPTBase.parse_transcoder(attr_type)
+ if transcoder is None:
+ raise ValueError(f"Invalid type for knx.send service: {attr_type}")
+ payload = DPTArray(transcoder.to_knx(attr_payload))
+ elif isinstance(attr_payload, int):
+ payload = DPTBinary(attr_payload)
else:
- _name = self.entity_id
- self.device = ExposeSensor(
- self.xknx,
- name=_name,
- group_address=self.address,
- value_type=self.type,
- )
- async_track_state_change_event(
- self.hass, [self.entity_id], self._async_entity_changed
- )
+ payload = DPTArray(attr_payload)
- async def _async_entity_changed(self, event):
- """Handle entity change."""
- new_state = event.data.get("new_state")
- if new_state is None:
- return
- if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
- return
-
- if self.expose_attribute is not None:
- new_attribute = new_state.attributes.get(self.expose_attribute)
- old_state = event.data.get("old_state")
-
- if old_state is not None:
- old_attribute = old_state.attributes.get(self.expose_attribute)
- if old_attribute == new_attribute:
- # don't send same value sequentially
- return
- await self._async_set_knx_value(new_attribute)
- else:
- await self._async_set_knx_value(new_state.state)
-
- async def _async_set_knx_value(self, value):
- """Set new value on xknx ExposeSensor."""
- if value is None:
- if self.expose_default is None:
- return
- value = self.expose_default
-
- if self.type == "binary":
- if value == STATE_ON:
- value = True
- elif value == STATE_OFF:
- value = False
-
- await self.device.set(value)
+ for address in attr_address:
+ telegram = Telegram(
+ destination_address=GroupAddress(address),
+ payload=GroupValueWrite(payload),
+ )
+ await self.xknx.telegrams.put(telegram)
+
+ async def service_read_to_knx_bus(self, call: ServiceCall) -> None:
+ """Service for sending a GroupValueRead telegram to the KNX bus."""
+ for address in call.data[KNX_ADDRESS]:
+ telegram = Telegram(
+ destination_address=GroupAddress(address),
+ payload=GroupValueRead(),
+ )
+ await self.xknx.telegrams.put(telegram)
diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py
index 35feb09dc1d9b2..0faeb9f37b4678 100644
--- a/homeassistant/components/knx/binary_sensor.py
+++ b/homeassistant/components/knx/binary_sensor.py
@@ -1,15 +1,25 @@
"""Support for KNX/IP binary sensors."""
-from typing import Any, Dict, Optional
+from __future__ import annotations
+
+from typing import Any, Callable, Iterable
from xknx.devices import BinarySensor as XknxBinarySensor
from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import ATTR_COUNTER, DOMAIN
from .knx_entity import KnxEntity
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: Callable[[Iterable[Entity]], None],
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
"""Set up binary sensor(s) for KNX platform."""
entities = []
for device in hass.data[DOMAIN].xknx.devices:
@@ -21,26 +31,29 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class KNXBinarySensor(KnxEntity, BinarySensorEntity):
"""Representation of a KNX binary sensor."""
- def __init__(self, device: XknxBinarySensor):
+ def __init__(self, device: XknxBinarySensor) -> None:
"""Initialize of KNX binary sensor."""
+ self._device: XknxBinarySensor
super().__init__(device)
@property
- def device_class(self):
+ def device_class(self) -> str | None:
"""Return the class of this sensor."""
if self._device.device_class in DEVICE_CLASSES:
return self._device.device_class
return None
@property
- def is_on(self):
+ def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return self._device.is_on()
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return device specific state attributes."""
- return {ATTR_COUNTER: self._device.counter}
+ if self._device.counter is not None:
+ return {ATTR_COUNTER: self._device.counter}
+ return None
@property
def force_update(self) -> bool:
diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py
index 565c41298a39b1..ca3f7b0f22aa78 100644
--- a/homeassistant/components/knx/climate.py
+++ b/homeassistant/components/knx/climate.py
@@ -1,5 +1,7 @@
"""Support for KNX/IP climate devices."""
-from typing import List, Optional
+from __future__ import annotations
+
+from typing import Any, Callable, Iterable
from xknx.devices import Climate as XknxClimate
from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode
@@ -13,6 +15,9 @@
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONTROLLER_MODES, DOMAIN, PRESET_MODES
from .knx_entity import KnxEntity
@@ -21,7 +26,12 @@
PRESET_MODES_INV = {value: key for key, value in PRESET_MODES.items()}
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: Callable[[Iterable[Entity]], None],
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
"""Set up climate(s) for KNX platform."""
entities = []
for device in hass.data[DOMAIN].xknx.devices:
@@ -33,8 +43,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class KNXClimate(KnxEntity, ClimateEntity):
"""Representation of a KNX climate device."""
- def __init__(self, device: XknxClimate):
+ def __init__(self, device: XknxClimate) -> None:
"""Initialize of a KNX climate device."""
+ self._device: XknxClimate
super().__init__(device)
self._unit_of_measurement = TEMP_CELSIUS
@@ -44,42 +55,45 @@ def supported_features(self) -> int:
"""Return the list of supported features."""
return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
- async def async_update(self):
+ async def async_update(self) -> None:
"""Request a state update from KNX bus."""
await self._device.sync()
- await self._device.mode.sync()
+ if self._device.mode is not None:
+ await self._device.mode.sync()
@property
- def temperature_unit(self):
+ def temperature_unit(self) -> str:
"""Return the unit of measurement."""
return self._unit_of_measurement
@property
- def current_temperature(self):
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self._device.temperature.value
@property
- def target_temperature_step(self):
+ def target_temperature_step(self) -> float:
"""Return the supported step of target temperature."""
return self._device.temperature_step
@property
- def target_temperature(self):
+ def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
return self._device.target_temperature.value
@property
- def min_temp(self):
+ def min_temp(self) -> float:
"""Return the minimum temperature."""
- return self._device.target_temperature_min
+ temp = self._device.target_temperature_min
+ return temp if temp is not None else super().min_temp
@property
- def max_temp(self):
+ def max_temp(self) -> float:
"""Return the maximum temperature."""
- return self._device.target_temperature_max
+ temp = self._device.target_temperature_max
+ return temp if temp is not None else super().max_temp
- async def async_set_temperature(self, **kwargs) -> None:
+ async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None:
@@ -88,11 +102,11 @@ async def async_set_temperature(self, **kwargs) -> None:
self.async_write_ha_state()
@property
- def hvac_mode(self) -> Optional[str]:
+ def hvac_mode(self) -> str:
"""Return current operation ie. heat, cool, idle."""
if self._device.supports_on_off and not self._device.is_on:
return HVAC_MODE_OFF
- if self._device.mode.supports_controller_mode:
+ if self._device.mode is not None and self._device.mode.supports_controller_mode:
return CONTROLLER_MODES.get(
self._device.mode.controller_mode.value, HVAC_MODE_HEAT
)
@@ -100,21 +114,23 @@ def hvac_mode(self) -> Optional[str]:
return HVAC_MODE_HEAT
@property
- def hvac_modes(self) -> Optional[List[str]]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available operation/controller modes."""
- _controller_modes = [
- CONTROLLER_MODES.get(controller_mode.value)
- for controller_mode in self._device.mode.controller_modes
- ]
+ ha_controller_modes: list[str | None] = []
+ if self._device.mode is not None:
+ for knx_controller_mode in self._device.mode.controller_modes:
+ ha_controller_modes.append(
+ CONTROLLER_MODES.get(knx_controller_mode.value)
+ )
if self._device.supports_on_off:
- if not _controller_modes:
- _controller_modes.append(HVAC_MODE_HEAT)
- _controller_modes.append(HVAC_MODE_OFF)
+ if not ha_controller_modes:
+ ha_controller_modes.append(HVAC_MODE_HEAT)
+ ha_controller_modes.append(HVAC_MODE_OFF)
- _modes = list(set(filter(None, _controller_modes)))
+ hvac_modes = list(set(filter(None, ha_controller_modes)))
# default to ["heat"]
- return _modes if _modes else [HVAC_MODE_HEAT]
+ return hvac_modes if hvac_modes else [HVAC_MODE_HEAT]
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set operation mode."""
@@ -123,7 +139,10 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None:
else:
if self._device.supports_on_off and not self._device.is_on:
await self._device.turn_on()
- if self._device.mode.supports_controller_mode:
+ if (
+ self._device.mode is not None
+ and self._device.mode.supports_controller_mode
+ ):
knx_controller_mode = HVACControllerMode(
CONTROLLER_MODES_INV.get(hvac_mode)
)
@@ -131,31 +150,33 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None:
self.async_write_ha_state()
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp.
Requires SUPPORT_PRESET_MODE.
"""
- if self._device.mode.supports_operation_mode:
+ if self._device.mode is not None and self._device.mode.supports_operation_mode:
return PRESET_MODES.get(self._device.mode.operation_mode.value, PRESET_AWAY)
return None
@property
- def preset_modes(self) -> Optional[List[str]]:
+ def preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes.
Requires SUPPORT_PRESET_MODE.
"""
- _presets = [
+ if self._device.mode is None:
+ return None
+
+ presets = [
PRESET_MODES.get(operation_mode.value)
for operation_mode in self._device.mode.operation_modes
]
-
- return list(filter(None, _presets))
+ return list(filter(None, presets))
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
- if self._device.mode.supports_operation_mode:
+ if self._device.mode is not None and self._device.mode.supports_operation_mode:
knx_operation_mode = HVACOperationMode(PRESET_MODES_INV.get(preset_mode))
await self._device.mode.set_operation_mode(knx_operation_mode)
self.async_write_ha_state()
diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py
index e434aed395d3e9..dfe357ef33c21d 100644
--- a/homeassistant/components/knx/const.py
+++ b/homeassistant/components/knx/const.py
@@ -17,31 +17,37 @@
DOMAIN = "knx"
+# Address is used for configuration and services by the same functions so the key has to match
+KNX_ADDRESS = "address"
+
CONF_INVERT = "invert"
CONF_STATE_ADDRESS = "state_address"
CONF_SYNC_STATE = "sync_state"
CONF_RESET_AFTER = "reset_after"
+ATTR_COUNTER = "counter"
+
class ColorTempModes(Enum):
"""Color temperature modes for config validation."""
- absolute = "DPT-7.600"
- relative = "DPT-5.001"
+ ABSOLUTE = "DPT-7.600"
+ RELATIVE = "DPT-5.001"
class SupportedPlatforms(Enum):
"""Supported platforms."""
- cover = "cover"
- light = "light"
- binary_sensor = "binary_sensor"
- climate = "climate"
- switch = "switch"
- notify = "notify"
- scene = "scene"
- sensor = "sensor"
- weather = "weather"
+ BINARY_SENSOR = "binary_sensor"
+ CLIMATE = "climate"
+ COVER = "cover"
+ FAN = "fan"
+ LIGHT = "light"
+ NOTIFY = "notify"
+ SCENE = "scene"
+ SENSOR = "sensor"
+ SWITCH = "switch"
+ WEATHER = "weather"
# Map KNX controller modes to HA modes. This list might not be complete.
@@ -63,5 +69,3 @@ class SupportedPlatforms(Enum):
"Standby": PRESET_AWAY,
"Comfort": PRESET_COMFORT,
}
-
-ATTR_COUNTER = "counter"
diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py
index 33da600976e6de..c45d057c3afde8 100644
--- a/homeassistant/components/knx/cover.py
+++ b/homeassistant/components/knx/cover.py
@@ -1,5 +1,10 @@
"""Support for KNX/IP covers."""
-from xknx.devices import Cover as XknxCover
+from __future__ import annotations
+
+from datetime import datetime
+from typing import Any, Callable, Iterable
+
+from xknx.devices import Cover as XknxCover, Device as XknxDevice
from homeassistant.components.cover import (
ATTR_POSITION,
@@ -7,20 +12,30 @@
DEVICE_CLASS_BLIND,
DEVICE_CLASSES,
SUPPORT_CLOSE,
+ SUPPORT_CLOSE_TILT,
SUPPORT_OPEN,
+ SUPPORT_OPEN_TILT,
SUPPORT_SET_POSITION,
SUPPORT_SET_TILT_POSITION,
SUPPORT_STOP,
+ SUPPORT_STOP_TILT,
CoverEntity,
)
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_utc_time_change
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN
from .knx_entity import KnxEntity
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: Callable[[Iterable[Entity]], None],
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
"""Set up cover(s) for KNX platform."""
entities = []
for device in hass.data[DOMAIN].xknx.devices:
@@ -34,19 +49,20 @@ class KNXCover(KnxEntity, CoverEntity):
def __init__(self, device: XknxCover):
"""Initialize the cover."""
+ self._device: XknxCover
super().__init__(device)
- self._unsubscribe_auto_updater = None
+ self._unsubscribe_auto_updater: Callable[[], None] | None = None
@callback
- async def after_update_callback(self, device):
+ async def after_update_callback(self, device: XknxDevice) -> None:
"""Call after device was updated."""
self.async_write_ha_state()
if self._device.is_traveling():
self.start_auto_updater()
@property
- def device_class(self):
+ def device_class(self) -> str | None:
"""Return the class of this device, from component DEVICE_CLASSES."""
if self._device.device_class in DEVICE_CLASSES:
return self._device.device_class
@@ -55,29 +71,32 @@ def device_class(self):
return None
@property
- def supported_features(self):
+ def supported_features(self) -> int:
"""Flag supported features."""
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
if self._device.supports_stop:
supported_features |= SUPPORT_STOP
if self._device.supports_angle:
- supported_features |= SUPPORT_SET_TILT_POSITION
+ supported_features |= (
+ SUPPORT_SET_TILT_POSITION
+ | SUPPORT_OPEN_TILT
+ | SUPPORT_CLOSE_TILT
+ | SUPPORT_STOP_TILT
+ )
return supported_features
@property
- def current_cover_position(self):
+ def current_cover_position(self) -> int | None:
"""Return the current position of the cover.
None is unknown, 0 is closed, 100 is fully open.
"""
# In KNX 0 is open, 100 is closed.
- try:
- return 100 - self._device.current_position()
- except TypeError:
- return None
+ pos = self._device.current_position()
+ return 100 - pos if pos is not None else None
@property
- def is_closed(self):
+ def is_closed(self) -> bool | None:
"""Return if the cover is closed."""
# state shall be "unknown" when xknx travelcalculator is not initialized
if self._device.current_position() is None:
@@ -85,66 +104,76 @@ def is_closed(self):
return self._device.is_closed()
@property
- def is_opening(self):
+ def is_opening(self) -> bool:
"""Return if the cover is opening or not."""
return self._device.is_opening()
@property
- def is_closing(self):
+ def is_closing(self) -> bool:
"""Return if the cover is closing or not."""
return self._device.is_closing()
- async def async_close_cover(self, **kwargs):
+ async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
await self._device.set_down()
- async def async_open_cover(self, **kwargs):
+ async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
await self._device.set_up()
- async def async_set_cover_position(self, **kwargs):
+ async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
knx_position = 100 - kwargs[ATTR_POSITION]
await self._device.set_position(knx_position)
- async def async_stop_cover(self, **kwargs):
+ async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
await self._device.stop()
self.stop_auto_updater()
@property
- def current_cover_tilt_position(self):
+ def current_cover_tilt_position(self) -> int | None:
"""Return current tilt position of cover."""
if not self._device.supports_angle:
return None
- try:
- return 100 - self._device.current_angle()
- except TypeError:
- return None
+ ang = self._device.current_angle()
+ return 100 - ang if ang is not None else None
- async def async_set_cover_tilt_position(self, **kwargs):
+ async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover tilt to a specific position."""
knx_tilt_position = 100 - kwargs[ATTR_TILT_POSITION]
await self._device.set_angle(knx_tilt_position)
- def start_auto_updater(self):
+ async def async_open_cover_tilt(self, **kwargs: Any) -> None:
+ """Open the cover tilt."""
+ await self._device.set_short_up()
+
+ async def async_close_cover_tilt(self, **kwargs: Any) -> None:
+ """Close the cover tilt."""
+ await self._device.set_short_down()
+
+ async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
+ """Stop the cover tilt."""
+ await self._device.stop()
+ self.stop_auto_updater()
+
+ def start_auto_updater(self) -> None:
"""Start the autoupdater to update Home Assistant while cover is moving."""
if self._unsubscribe_auto_updater is None:
self._unsubscribe_auto_updater = async_track_utc_time_change(
self.hass, self.auto_updater_hook
)
- def stop_auto_updater(self):
+ def stop_auto_updater(self) -> None:
"""Stop the autoupdater."""
if self._unsubscribe_auto_updater is not None:
self._unsubscribe_auto_updater()
self._unsubscribe_auto_updater = None
@callback
- def auto_updater_hook(self, now):
+ def auto_updater_hook(self, now: datetime) -> None:
"""Call for the autoupdater."""
self.async_write_ha_state()
if self._device.position_reached():
+ self.hass.async_create_task(self._device.auto_stop_if_necessary())
self.stop_auto_updater()
-
- self.hass.add_job(self._device.auto_stop_if_necessary())
diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py
new file mode 100644
index 00000000000000..5616a6deb23752
--- /dev/null
+++ b/homeassistant/components/knx/expose.py
@@ -0,0 +1,170 @@
+"""Exposures to KNX bus."""
+from __future__ import annotations
+
+from typing import Callable
+
+from xknx import XKNX
+from xknx.devices import DateTime, ExposeSensor
+
+from homeassistant.const import (
+ CONF_ENTITY_ID,
+ STATE_OFF,
+ STATE_ON,
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+)
+from homeassistant.core import Event, HomeAssistant, callback
+from homeassistant.helpers.event import async_track_state_change_event
+from homeassistant.helpers.typing import ConfigType, StateType
+
+from .const import KNX_ADDRESS
+from .schema import ExposeSchema
+
+
+@callback
+def create_knx_exposure(
+ hass: HomeAssistant, xknx: XKNX, config: ConfigType
+) -> KNXExposeSensor | KNXExposeTime:
+ """Create exposures from config."""
+ address = config[KNX_ADDRESS]
+ expose_type = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]
+ attribute = config.get(ExposeSchema.CONF_KNX_EXPOSE_ATTRIBUTE)
+ default = config.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT)
+
+ exposure: KNXExposeSensor | KNXExposeTime
+ if (
+ isinstance(expose_type, str)
+ and expose_type.lower() in ExposeSchema.EXPOSE_TIME_TYPES
+ ):
+ exposure = KNXExposeTime(xknx, expose_type, address)
+ else:
+ entity_id = config[CONF_ENTITY_ID]
+ exposure = KNXExposeSensor(
+ hass,
+ xknx,
+ expose_type,
+ entity_id,
+ attribute,
+ default,
+ address,
+ )
+ return exposure
+
+
+class KNXExposeSensor:
+ """Object to Expose Home Assistant entity to KNX bus."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ xknx: XKNX,
+ expose_type: int | str,
+ entity_id: str,
+ attribute: str | None,
+ default: StateType,
+ address: str,
+ ) -> None:
+ """Initialize of Expose class."""
+ self.hass = hass
+ self.xknx = xknx
+ self.type = expose_type
+ self.entity_id = entity_id
+ self.expose_attribute = attribute
+ self.expose_default = default
+ self.address = address
+ self._remove_listener: Callable[[], None] | None = None
+ self.device: ExposeSensor = self.async_register()
+
+ @callback
+ def async_register(self) -> ExposeSensor:
+ """Register listener."""
+ if self.expose_attribute is not None:
+ _name = self.entity_id + "__" + self.expose_attribute
+ else:
+ _name = self.entity_id
+ device = ExposeSensor(
+ self.xknx,
+ name=_name,
+ group_address=self.address,
+ value_type=self.type,
+ )
+ self._remove_listener = async_track_state_change_event(
+ self.hass, [self.entity_id], self._async_entity_changed
+ )
+ return device
+
+ @callback
+ def shutdown(self) -> None:
+ """Prepare for deletion."""
+ if self._remove_listener is not None:
+ self._remove_listener()
+ self._remove_listener = None
+ self.device.shutdown()
+
+ async def _async_entity_changed(self, event: Event) -> None:
+ """Handle entity change."""
+ new_state = event.data.get("new_state")
+ if new_state is None:
+ return
+ if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
+ return
+
+ old_state = event.data.get("old_state")
+
+ if self.expose_attribute is None:
+ if old_state is None or old_state.state != new_state.state:
+ # don't send same value sequentially
+ await self._async_set_knx_value(new_state.state)
+ return
+
+ new_attribute = new_state.attributes.get(self.expose_attribute)
+
+ if old_state is not None:
+ old_attribute = old_state.attributes.get(self.expose_attribute)
+ if old_attribute == new_attribute:
+ # don't send same value sequentially
+ return
+ await self._async_set_knx_value(new_attribute)
+
+ async def _async_set_knx_value(self, value: StateType) -> None:
+ """Set new value on xknx ExposeSensor."""
+ assert self.device is not None
+ if value is None:
+ if self.expose_default is None:
+ return
+ value = self.expose_default
+
+ if self.type == "binary":
+ if value == STATE_ON:
+ value = True
+ elif value == STATE_OFF:
+ value = False
+
+ await self.device.set(value)
+
+
+class KNXExposeTime:
+ """Object to Expose Time/Date object to KNX bus."""
+
+ def __init__(self, xknx: XKNX, expose_type: str, address: str) -> None:
+ """Initialize of Expose class."""
+ self.xknx = xknx
+ self.expose_type = expose_type
+ self.address = address
+ self.device: DateTime = self.async_register()
+
+ @callback
+ def async_register(self) -> DateTime:
+ """Register listener."""
+ return DateTime(
+ self.xknx,
+ name=self.expose_type.capitalize(),
+ broadcast_type=self.expose_type.upper(),
+ localtime=True,
+ group_address=self.address,
+ )
+
+ @callback
+ def shutdown(self) -> None:
+ """Prepare for deletion."""
+ self.device.shutdown()
diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py
index c1e73733b22f9e..827ec83a8e18f4 100644
--- a/homeassistant/components/knx/factory.py
+++ b/homeassistant/components/knx/factory.py
@@ -1,5 +1,5 @@
"""Factory function to initialize KNX devices from config."""
-from typing import Optional, Tuple
+from __future__ import annotations
from xknx import XKNX
from xknx.devices import (
@@ -8,6 +8,7 @@
ClimateMode as XknxClimateMode,
Cover as XknxCover,
Device as XknxDevice,
+ Fan as XknxFan,
Light as XknxLight,
Notification as XknxNotification,
Scene as XknxScene,
@@ -16,14 +17,15 @@
Weather as XknxWeather,
)
-from homeassistant.const import CONF_ADDRESS, CONF_DEVICE_CLASS, CONF_NAME, CONF_TYPE
+from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_TYPE
from homeassistant.helpers.typing import ConfigType
-from .const import ColorTempModes, SupportedPlatforms
+from .const import KNX_ADDRESS, ColorTempModes, SupportedPlatforms
from .schema import (
BinarySensorSchema,
ClimateSchema,
CoverSchema,
+ FanSchema,
LightSchema,
SceneSchema,
SensorSchema,
@@ -38,33 +40,36 @@ def create_knx_device(
config: ConfigType,
) -> XknxDevice:
"""Return the requested XKNX device."""
- if platform is SupportedPlatforms.light:
+ if platform is SupportedPlatforms.LIGHT:
return _create_light(knx_module, config)
- if platform is SupportedPlatforms.cover:
+ if platform is SupportedPlatforms.COVER:
return _create_cover(knx_module, config)
- if platform is SupportedPlatforms.climate:
+ if platform is SupportedPlatforms.CLIMATE:
return _create_climate(knx_module, config)
- if platform is SupportedPlatforms.switch:
+ if platform is SupportedPlatforms.SWITCH:
return _create_switch(knx_module, config)
- if platform is SupportedPlatforms.sensor:
+ if platform is SupportedPlatforms.SENSOR:
return _create_sensor(knx_module, config)
- if platform is SupportedPlatforms.notify:
+ if platform is SupportedPlatforms.NOTIFY:
return _create_notify(knx_module, config)
- if platform is SupportedPlatforms.scene:
+ if platform is SupportedPlatforms.SCENE:
return _create_scene(knx_module, config)
- if platform is SupportedPlatforms.binary_sensor:
+ if platform is SupportedPlatforms.BINARY_SENSOR:
return _create_binary_sensor(knx_module, config)
- if platform is SupportedPlatforms.weather:
+ if platform is SupportedPlatforms.WEATHER:
return _create_weather(knx_module, config)
+ if platform is SupportedPlatforms.FAN:
+ return _create_fan(knx_module, config)
+
def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover:
"""Return a KNX Cover device to be used within XKNX."""
@@ -90,11 +95,11 @@ def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover:
def _create_light_color(
color: str, config: ConfigType
-) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]:
+) -> tuple[str | None, str | None, str | None, str | None]:
"""Load color configuration from configuration structure."""
if "individual_colors" in config and color in config["individual_colors"]:
sub_config = config["individual_colors"][color]
- group_address_switch = sub_config.get(CONF_ADDRESS)
+ group_address_switch = sub_config.get(KNX_ADDRESS)
group_address_switch_state = sub_config.get(LightSchema.CONF_STATE_ADDRESS)
group_address_brightness = sub_config.get(LightSchema.CONF_BRIGHTNESS_ADDRESS)
group_address_brightness_state = sub_config.get(
@@ -116,12 +121,12 @@ def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight:
group_address_tunable_white_state = None
group_address_color_temp = None
group_address_color_temp_state = None
- if config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.absolute:
+ if config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.ABSOLUTE:
group_address_color_temp = config.get(LightSchema.CONF_COLOR_TEMP_ADDRESS)
group_address_color_temp_state = config.get(
LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS
)
- elif config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.relative:
+ elif config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.RELATIVE:
group_address_tunable_white = config.get(LightSchema.CONF_COLOR_TEMP_ADDRESS)
group_address_tunable_white_state = config.get(
LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS
@@ -155,7 +160,7 @@ def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight:
return XknxLight(
knx_module,
name=config[CONF_NAME],
- group_address_switch=config.get(CONF_ADDRESS),
+ group_address_switch=config.get(KNX_ADDRESS),
group_address_switch_state=config.get(LightSchema.CONF_STATE_ADDRESS),
group_address_brightness=config.get(LightSchema.CONF_BRIGHTNESS_ADDRESS),
group_address_brightness_state=config.get(
@@ -259,6 +264,9 @@ def _create_climate(knx_module: XKNX, config: ConfigType) -> XknxClimate:
max_temp=config.get(ClimateSchema.CONF_MAX_TEMP),
mode=climate_mode,
on_off_invert=config[ClimateSchema.CONF_ON_OFF_INVERT],
+ create_temperature_sensors=config[
+ ClimateSchema.CONF_CREATE_TEMPERATURE_SENSORS
+ ],
)
@@ -267,9 +275,9 @@ def _create_switch(knx_module: XKNX, config: ConfigType) -> XknxSwitch:
return XknxSwitch(
knx_module,
name=config[CONF_NAME],
- group_address=config[CONF_ADDRESS],
+ group_address=config[KNX_ADDRESS],
group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS),
- invert=config.get(SwitchSchema.CONF_INVERT),
+ invert=config[SwitchSchema.CONF_INVERT],
)
@@ -290,7 +298,7 @@ def _create_notify(knx_module: XKNX, config: ConfigType) -> XknxNotification:
return XknxNotification(
knx_module,
name=config[CONF_NAME],
- group_address=config[CONF_ADDRESS],
+ group_address=config[KNX_ADDRESS],
)
@@ -299,7 +307,7 @@ def _create_scene(knx_module: XKNX, config: ConfigType) -> XknxScene:
return XknxScene(
knx_module,
name=config[CONF_NAME],
- group_address=config[CONF_ADDRESS],
+ group_address=config[KNX_ADDRESS],
scene_number=config[SceneSchema.CONF_SCENE_NUMBER],
)
@@ -312,7 +320,7 @@ def _create_binary_sensor(knx_module: XKNX, config: ConfigType) -> XknxBinarySen
knx_module,
name=device_name,
group_address_state=config[BinarySensorSchema.CONF_STATE_ADDRESS],
- invert=config.get(BinarySensorSchema.CONF_INVERT),
+ invert=config[BinarySensorSchema.CONF_INVERT],
sync_state=config[BinarySensorSchema.CONF_SYNC_STATE],
device_class=config.get(CONF_DEVICE_CLASS),
ignore_internal_state=config[BinarySensorSchema.CONF_IGNORE_INTERNAL_STATE],
@@ -327,7 +335,7 @@ def _create_weather(knx_module: XKNX, config: ConfigType) -> XknxWeather:
knx_module,
name=config[CONF_NAME],
sync_state=config[WeatherSchema.CONF_SYNC_STATE],
- expose_sensors=config[WeatherSchema.CONF_KNX_EXPOSE_SENSORS],
+ create_sensors=config[WeatherSchema.CONF_KNX_CREATE_SENSORS],
group_address_temperature=config[WeatherSchema.CONF_KNX_TEMPERATURE_ADDRESS],
group_address_brightness_south=config.get(
WeatherSchema.CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS
@@ -342,6 +350,9 @@ def _create_weather(knx_module: XKNX, config: ConfigType) -> XknxWeather:
WeatherSchema.CONF_KNX_BRIGHTNESS_NORTH_ADDRESS
),
group_address_wind_speed=config.get(WeatherSchema.CONF_KNX_WIND_SPEED_ADDRESS),
+ group_address_wind_bearing=config.get(
+ WeatherSchema.CONF_KNX_WIND_BEARING_ADDRESS
+ ),
group_address_rain_alarm=config.get(WeatherSchema.CONF_KNX_RAIN_ALARM_ADDRESS),
group_address_frost_alarm=config.get(
WeatherSchema.CONF_KNX_FROST_ALARM_ADDRESS
@@ -353,3 +364,20 @@ def _create_weather(knx_module: XKNX, config: ConfigType) -> XknxWeather:
),
group_address_humidity=config.get(WeatherSchema.CONF_KNX_HUMIDITY_ADDRESS),
)
+
+
+def _create_fan(knx_module: XKNX, config: ConfigType) -> XknxFan:
+ """Return a KNX Fan device to be used within XKNX."""
+
+ fan = XknxFan(
+ knx_module,
+ name=config[CONF_NAME],
+ group_address_speed=config.get(KNX_ADDRESS),
+ group_address_speed_state=config.get(FanSchema.CONF_STATE_ADDRESS),
+ group_address_oscillation=config.get(FanSchema.CONF_OSCILLATION_ADDRESS),
+ group_address_oscillation_state=config.get(
+ FanSchema.CONF_OSCILLATION_STATE_ADDRESS
+ ),
+ max_step=config.get(FanSchema.CONF_MAX_STEP),
+ )
+ return fan
diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py
new file mode 100644
index 00000000000000..38680e15bf85e5
--- /dev/null
+++ b/homeassistant/components/knx/fan.py
@@ -0,0 +1,113 @@
+"""Support for KNX/IP fans."""
+from __future__ import annotations
+
+import math
+from typing import Any, Callable, Iterable
+
+from xknx.devices import Fan as XknxFan
+
+from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
+from homeassistant.util.percentage import (
+ int_states_in_range,
+ percentage_to_ranged_value,
+ ranged_value_to_percentage,
+)
+
+from .const import DOMAIN
+from .knx_entity import KnxEntity
+
+DEFAULT_PERCENTAGE = 50
+
+
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: Callable[[Iterable[Entity]], None],
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
+ """Set up fans for KNX platform."""
+ entities = []
+ for device in hass.data[DOMAIN].xknx.devices:
+ if isinstance(device, XknxFan):
+ entities.append(KNXFan(device))
+ async_add_entities(entities)
+
+
+class KNXFan(KnxEntity, FanEntity):
+ """Representation of a KNX fan."""
+
+ def __init__(self, device: XknxFan) -> None:
+ """Initialize of KNX fan."""
+ self._device: XknxFan
+ super().__init__(device)
+
+ self._step_range: tuple[int, int] | None = None
+ if device.max_step:
+ # FanSpeedMode.STEP:
+ self._step_range = (1, device.max_step)
+
+ async def async_set_percentage(self, percentage: int) -> None:
+ """Set the speed of the fan, as a percentage."""
+ if self._step_range:
+ step = math.ceil(percentage_to_ranged_value(self._step_range, percentage))
+ await self._device.set_speed(step)
+ else:
+ await self._device.set_speed(percentage)
+
+ @property
+ def supported_features(self) -> int:
+ """Flag supported features."""
+ flags = SUPPORT_SET_SPEED
+
+ if self._device.supports_oscillation:
+ flags |= SUPPORT_OSCILLATE
+
+ return flags
+
+ @property
+ def percentage(self) -> int | None:
+ """Return the current speed as a percentage."""
+ if self._device.current_speed is None:
+ return None
+
+ if self._step_range:
+ return ranged_value_to_percentage(
+ self._step_range, self._device.current_speed
+ )
+ return self._device.current_speed
+
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ if self._step_range is None:
+ return super().speed_count
+ return int_states_in_range(self._step_range)
+
+ async def async_turn_on(
+ self,
+ speed: str | None = None,
+ percentage: int | None = None,
+ preset_mode: str | None = None,
+ **kwargs: Any,
+ ) -> None:
+ """Turn on the fan."""
+ if percentage is None:
+ await self.async_set_percentage(DEFAULT_PERCENTAGE)
+ else:
+ await self.async_set_percentage(percentage)
+
+ async def async_turn_off(self, **kwargs: Any) -> None:
+ """Turn the fan off."""
+ await self.async_set_percentage(0)
+
+ async def async_oscillate(self, oscillating: bool) -> None:
+ """Oscillate the fan."""
+ await self._device.set_oscillation(oscillating)
+
+ @property
+ def oscillating(self) -> bool | None:
+ """Return whether or not the fan is currently oscillating."""
+ return self._device.current_oscillation
diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py
index 296bcb2f5405fb..670f1ddf44d5e0 100644
--- a/homeassistant/components/knx/knx_entity.py
+++ b/homeassistant/components/knx/knx_entity.py
@@ -1,38 +1,49 @@
"""Base class for KNX devices."""
+from __future__ import annotations
+
+from typing import cast
+
from xknx.devices import Climate as XknxClimate, Device as XknxDevice
from homeassistant.helpers.entity import Entity
+from . import KNXModule
from .const import DOMAIN
class KnxEntity(Entity):
"""Representation of a KNX entity."""
- def __init__(self, device: XknxDevice):
+ def __init__(self, device: XknxDevice) -> None:
"""Set up device."""
self._device = device
@property
- def name(self):
+ def name(self) -> str:
"""Return the name of the KNX device."""
return self._device.name
@property
- def available(self):
+ def available(self) -> bool:
"""Return True if entity is available."""
- return self.hass.data[DOMAIN].connected
+ knx_module = cast(KNXModule, self.hass.data[DOMAIN])
+ return knx_module.connected
@property
- def should_poll(self):
+ def should_poll(self) -> bool:
"""No polling needed within KNX."""
return False
- async def async_update(self):
+ @property
+ def unique_id(self) -> str | None:
+ """Return the unique id of the device."""
+ return self._device.unique_id
+
+ async def async_update(self) -> None:
"""Request a state update from KNX bus."""
await self._device.sync()
- async def after_update_callback(self, device: XknxDevice):
+ async def after_update_callback(self, device: XknxDevice) -> None:
"""Call after device was updated."""
self.async_write_ha_state()
@@ -40,12 +51,12 @@ async def async_added_to_hass(self) -> None:
"""Store register state change callback."""
self._device.register_device_updated_cb(self.after_update_callback)
- if isinstance(self._device, XknxClimate):
+ if isinstance(self._device, XknxClimate) and self._device.mode is not None:
self._device.mode.register_device_updated_cb(self.after_update_callback)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
self._device.unregister_device_updated_cb(self.after_update_callback)
- if isinstance(self._device, XknxClimate):
+ if isinstance(self._device, XknxClimate) and self._device.mode is not None:
self._device.mode.unregister_device_updated_cb(self.after_update_callback)
diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py
index 50d067bf29ac57..0eb6243373432d 100644
--- a/homeassistant/components/knx/light.py
+++ b/homeassistant/components/knx/light.py
@@ -1,4 +1,8 @@
"""Support for KNX/IP lights."""
+from __future__ import annotations
+
+from typing import Any, Callable, Iterable
+
from xknx.devices import Light as XknxLight
from homeassistant.components.light import (
@@ -12,17 +16,26 @@
SUPPORT_WHITE_VALUE,
LightEntity,
)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
import homeassistant.util.color as color_util
from .const import DOMAIN
from .knx_entity import KnxEntity
+from .schema import LightSchema
DEFAULT_COLOR = (0.0, 0.0)
DEFAULT_BRIGHTNESS = 255
DEFAULT_WHITE_VALUE = 255
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: Callable[[Iterable[Entity]], None],
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
"""Set up lights for KNX platform."""
entities = []
for device in hass.data[DOMAIN].xknx.devices:
@@ -34,12 +47,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class KNXLight(KnxEntity, LightEntity):
"""Representation of a KNX light."""
- def __init__(self, device: XknxLight):
+ def __init__(self, device: XknxLight) -> None:
"""Initialize of KNX light."""
+ self._device: XknxLight
super().__init__(device)
- self._min_kelvin = device.min_kelvin
- self._max_kelvin = device.max_kelvin
+ self._min_kelvin = device.min_kelvin or LightSchema.DEFAULT_MIN_KELVIN
+ self._max_kelvin = device.max_kelvin or LightSchema.DEFAULT_MAX_KELVIN
self._min_mireds = color_util.color_temperature_kelvin_to_mired(
self._max_kelvin
)
@@ -48,7 +62,7 @@ def __init__(self, device: XknxLight):
)
@property
- def brightness(self):
+ def brightness(self) -> int | None:
"""Return the brightness of this light between 0..255."""
if self._device.supports_brightness:
return self._device.current_brightness
@@ -58,31 +72,31 @@ def brightness(self):
return None
@property
- def hs_color(self):
+ def hs_color(self) -> tuple[float, float] | None:
"""Return the HS color value."""
- rgb = None
+ rgb: tuple[int, int, int] | None = None
if self._device.supports_rgbw or self._device.supports_color:
rgb, _ = self._device.current_color
return color_util.color_RGB_to_hs(*rgb) if rgb else None
@property
- def _hsv_color(self):
+ def _hsv_color(self) -> tuple[float, float, float] | None:
"""Return the HSV color value."""
- rgb = None
+ rgb: tuple[int, int, int] | None = None
if self._device.supports_rgbw or self._device.supports_color:
rgb, _ = self._device.current_color
return color_util.color_RGB_to_hsv(*rgb) if rgb else None
@property
- def white_value(self):
+ def white_value(self) -> int | None:
"""Return the white value."""
- white = None
+ white: int | None = None
if self._device.supports_rgbw:
_, white = self._device.current_color
return white
@property
- def color_temp(self):
+ def color_temp(self) -> int | None:
"""Return the color temperature in mireds."""
if self._device.supports_color_temperature:
kelvin = self._device.current_color_temperature
@@ -101,32 +115,32 @@ def color_temp(self):
return None
@property
- def min_mireds(self):
+ def min_mireds(self) -> int:
"""Return the coldest color temp this light supports in mireds."""
return self._min_mireds
@property
- def max_mireds(self):
+ def max_mireds(self) -> int:
"""Return the warmest color temp this light supports in mireds."""
return self._max_mireds
@property
- def effect_list(self):
+ def effect_list(self) -> list[str] | None:
"""Return the list of supported effects."""
return None
@property
- def effect(self):
+ def effect(self) -> str | None:
"""Return the current effect."""
return None
@property
- def is_on(self):
+ def is_on(self) -> bool:
"""Return true if light is on."""
- return self._device.state
+ return bool(self._device.state)
@property
- def supported_features(self):
+ def supported_features(self) -> int:
"""Flag supported features."""
flags = 0
if self._device.supports_brightness:
@@ -142,7 +156,7 @@ def supported_features(self):
flags |= SUPPORT_COLOR_TEMP
return flags
- async def async_turn_on(self, **kwargs):
+ async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness)
hs_color = kwargs.get(ATTR_HS_COLOR, self.hs_color)
@@ -183,7 +197,8 @@ async def async_turn_on(self, **kwargs):
hs_color = DEFAULT_COLOR
if white_value is None and self._device.supports_rgbw:
white_value = DEFAULT_WHITE_VALUE
- rgb = color_util.color_hsv_to_RGB(*hs_color, brightness * 100 / 255)
+ hsv_color = hs_color + (brightness * 100 / 255,)
+ rgb = color_util.color_hsv_to_RGB(*hsv_color)
await self._device.set_color(rgb, white_value)
if update_color_temp:
@@ -200,6 +215,6 @@ async def async_turn_on(self, **kwargs):
)
await self._device.set_tunable_white(relative_ct)
- async def async_turn_off(self, **kwargs):
+ async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
await self._device.set_off()
diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json
index 93daee0e348c15..abb7fff37e0274 100644
--- a/homeassistant/components/knx/manifest.json
+++ b/homeassistant/components/knx/manifest.json
@@ -2,7 +2,7 @@
"domain": "knx",
"name": "KNX",
"documentation": "https://www.home-assistant.io/integrations/knx",
- "requirements": ["xknx==0.16.2"],
+ "requirements": ["xknx==0.18.0"],
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
"quality_scale": "silver"
}
diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py
index 7210795bd71ab8..62ba1109526839 100644
--- a/homeassistant/components/knx/notify.py
+++ b/homeassistant/components/knx/notify.py
@@ -1,14 +1,22 @@
"""Support for KNX/IP notification services."""
-from typing import List
+from __future__ import annotations
+
+from typing import Any
from xknx.devices import Notification as XknxNotification
from homeassistant.components.notify import BaseNotificationService
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN
-async def async_get_service(hass, config, discovery_info=None):
+async def async_get_service(
+ hass: HomeAssistant,
+ config: ConfigType,
+ discovery_info: DiscoveryInfoType | None = None,
+) -> KNXNotificationService | None:
"""Get the KNX notification service."""
notification_devices = []
for device in hass.data[DOMAIN].xknx.devices:
@@ -22,31 +30,31 @@ async def async_get_service(hass, config, discovery_info=None):
class KNXNotificationService(BaseNotificationService):
"""Implement demo notification service."""
- def __init__(self, devices: List[XknxNotification]):
+ def __init__(self, devices: list[XknxNotification]) -> None:
"""Initialize the service."""
self.devices = devices
@property
- def targets(self):
+ def targets(self) -> dict[str, str]:
"""Return a dictionary of registered targets."""
ret = {}
for device in self.devices:
ret[device.name] = device.name
return ret
- async def async_send_message(self, message="", **kwargs):
+ async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a notification to knx bus."""
if "target" in kwargs:
await self._async_send_to_device(message, kwargs["target"])
else:
await self._async_send_to_all_devices(message)
- async def _async_send_to_all_devices(self, message):
+ async def _async_send_to_all_devices(self, message: str) -> None:
"""Send a notification to knx bus to all connected devices."""
for device in self.devices:
await device.set(message)
- async def _async_send_to_device(self, message, names):
+ async def _async_send_to_device(self, message: str, names: str) -> None:
"""Send a notification to knx bus to device with given names."""
for device in self.devices:
if device.name in names:
diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py
index 6c76fdbd199e2b..ff08cdf411c7ba 100644
--- a/homeassistant/components/knx/scene.py
+++ b/homeassistant/components/knx/scene.py
@@ -1,15 +1,25 @@
"""Support for KNX scenes."""
-from typing import Any
+from __future__ import annotations
+
+from typing import Any, Callable, Iterable
from xknx.devices import Scene as XknxScene
from homeassistant.components.scene import Scene
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN
from .knx_entity import KnxEntity
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: Callable[[Iterable[Entity]], None],
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
"""Set up the scenes for KNX platform."""
entities = []
for device in hass.data[DOMAIN].xknx.devices:
@@ -21,8 +31,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class KNXScene(KnxEntity, Scene):
"""Representation of a KNX scene."""
- def __init__(self, device: XknxScene):
+ def __init__(self, device: XknxScene) -> None:
"""Init KNX scene."""
+ self._device: XknxScene
super().__init__(device)
async def async_activate(self, **kwargs: Any) -> None:
diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py
index 22599014d0fd08..fb4b29fbd70531 100644
--- a/homeassistant/components/knx/schema.py
+++ b/homeassistant/components/knx/schema.py
@@ -2,9 +2,9 @@
import voluptuous as vol
from xknx.devices.climate import SetpointShiftMode
from xknx.io import DEFAULT_MCAST_PORT
+from xknx.telegram.address import GroupAddress, IndividualAddress
from homeassistant.const import (
- CONF_ADDRESS,
CONF_DEVICE_CLASS,
CONF_ENTITY_ID,
CONF_HOST,
@@ -20,66 +20,63 @@
CONF_STATE_ADDRESS,
CONF_SYNC_STATE,
CONTROLLER_MODES,
+ KNX_ADDRESS,
PRESET_MODES,
ColorTempModes,
)
+##################
+# KNX VALIDATORS
+##################
+
+ga_validator = vol.Any(
+ cv.matches_regex(GroupAddress.ADDRESS_RE.pattern),
+ vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
+ msg="value does not match pattern for KNX group address '//', '/' or '' (eg.'1/2/3', '9/234', '123')",
+)
+ga_list_validator = vol.All(cv.ensure_list, [ga_validator])
+
+ia_validator = vol.Any(
+ cv.matches_regex(IndividualAddress.ADDRESS_RE.pattern),
+ vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
+ msg="value does not match pattern for KNX individual address '..' (eg.'1.1.100')",
+)
+
+sync_state_validator = vol.Any(
+ vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)),
+ cv.boolean,
+ cv.matches_regex(r"^(init|expire|every)( \d*)?$"),
+)
+
+sensor_type_validator = vol.Any(int, str)
+
+
+##############
+# CONNECTION
+##############
+
class ConnectionSchema:
"""Voluptuous schema for KNX connection."""
CONF_KNX_LOCAL_IP = "local_ip"
+ CONF_KNX_ROUTE_BACK = "route_back"
TUNNELING_SCHEMA = vol.Schema(
{
vol.Optional(CONF_PORT, default=DEFAULT_MCAST_PORT): cv.port,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_KNX_LOCAL_IP): cv.string,
+ vol.Optional(CONF_KNX_ROUTE_BACK, default=False): cv.boolean,
}
)
- ROUTING_SCHEMA = vol.Schema({vol.Optional(CONF_KNX_LOCAL_IP): cv.string})
-
-
-class CoverSchema:
- """Voluptuous schema for KNX covers."""
-
- CONF_MOVE_LONG_ADDRESS = "move_long_address"
- CONF_MOVE_SHORT_ADDRESS = "move_short_address"
- CONF_STOP_ADDRESS = "stop_address"
- CONF_POSITION_ADDRESS = "position_address"
- CONF_POSITION_STATE_ADDRESS = "position_state_address"
- CONF_ANGLE_ADDRESS = "angle_address"
- CONF_ANGLE_STATE_ADDRESS = "angle_state_address"
- CONF_TRAVELLING_TIME_DOWN = "travelling_time_down"
- CONF_TRAVELLING_TIME_UP = "travelling_time_up"
- CONF_INVERT_POSITION = "invert_position"
- CONF_INVERT_ANGLE = "invert_angle"
+ ROUTING_SCHEMA = vol.Maybe(vol.Schema({vol.Optional(CONF_KNX_LOCAL_IP): cv.string}))
- DEFAULT_TRAVEL_TIME = 25
- DEFAULT_NAME = "KNX Cover"
- SCHEMA = vol.Schema(
- {
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_MOVE_LONG_ADDRESS): cv.string,
- vol.Optional(CONF_MOVE_SHORT_ADDRESS): cv.string,
- vol.Optional(CONF_STOP_ADDRESS): cv.string,
- vol.Optional(CONF_POSITION_ADDRESS): cv.string,
- vol.Optional(CONF_POSITION_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_ANGLE_ADDRESS): cv.string,
- vol.Optional(CONF_ANGLE_STATE_ADDRESS): cv.string,
- vol.Optional(
- CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME
- ): cv.positive_int,
- vol.Optional(
- CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME
- ): cv.positive_int,
- vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
- vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
- vol.Optional(CONF_DEVICE_CLASS): cv.string,
- }
- )
+#############
+# PLATFORMS
+#############
class BinarySensorSchema:
@@ -100,113 +97,20 @@ class BinarySensorSchema:
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_SYNC_STATE, default=True): vol.Any(
- vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)),
- cv.boolean,
- cv.string,
- ),
+ vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
vol.Optional(CONF_IGNORE_INTERNAL_STATE, default=False): cv.boolean,
- vol.Required(CONF_STATE_ADDRESS): cv.string,
+ vol.Optional(CONF_INVERT, default=False): cv.boolean,
+ vol.Required(CONF_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_CONTEXT_TIMEOUT): vol.All(
vol.Coerce(float), vol.Range(min=0, max=10)
),
vol.Optional(CONF_DEVICE_CLASS): cv.string,
- vol.Optional(CONF_INVERT): cv.boolean,
vol.Optional(CONF_RESET_AFTER): cv.positive_float,
}
),
)
-class LightSchema:
- """Voluptuous schema for KNX lights."""
-
- CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
- CONF_BRIGHTNESS_ADDRESS = "brightness_address"
- CONF_BRIGHTNESS_STATE_ADDRESS = "brightness_state_address"
- CONF_COLOR_ADDRESS = "color_address"
- CONF_COLOR_STATE_ADDRESS = "color_state_address"
- CONF_COLOR_TEMP_ADDRESS = "color_temperature_address"
- CONF_COLOR_TEMP_STATE_ADDRESS = "color_temperature_state_address"
- CONF_COLOR_TEMP_MODE = "color_temperature_mode"
- CONF_RGBW_ADDRESS = "rgbw_address"
- CONF_RGBW_STATE_ADDRESS = "rgbw_state_address"
- CONF_MIN_KELVIN = "min_kelvin"
- CONF_MAX_KELVIN = "max_kelvin"
-
- DEFAULT_NAME = "KNX Light"
- DEFAULT_COLOR_TEMP_MODE = "absolute"
- DEFAULT_MIN_KELVIN = 2700 # 370 mireds
- DEFAULT_MAX_KELVIN = 6000 # 166 mireds
-
- CONF_INDIVIDUAL_COLORS = "individual_colors"
- CONF_RED = "red"
- CONF_GREEN = "green"
- CONF_BLUE = "blue"
- CONF_WHITE = "white"
-
- COLOR_SCHEMA = vol.Schema(
- {
- vol.Optional(CONF_ADDRESS): cv.string,
- vol.Optional(CONF_STATE_ADDRESS): cv.string,
- vol.Required(CONF_BRIGHTNESS_ADDRESS): cv.string,
- vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string,
- }
- )
-
- SCHEMA = vol.All(
- vol.Schema(
- {
- vol.Optional(CONF_ADDRESS): cv.string,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_BRIGHTNESS_ADDRESS): cv.string,
- vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string,
- vol.Exclusive(CONF_INDIVIDUAL_COLORS, "color"): {
- vol.Inclusive(CONF_RED, "colors"): COLOR_SCHEMA,
- vol.Inclusive(CONF_GREEN, "colors"): COLOR_SCHEMA,
- vol.Inclusive(CONF_BLUE, "colors"): COLOR_SCHEMA,
- vol.Optional(CONF_WHITE): COLOR_SCHEMA,
- },
- vol.Exclusive(CONF_COLOR_ADDRESS, "color"): cv.string,
- vol.Optional(CONF_COLOR_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_COLOR_TEMP_ADDRESS): cv.string,
- vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): cv.string,
- vol.Optional(
- CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE
- ): cv.enum(ColorTempModes),
- vol.Exclusive(CONF_RGBW_ADDRESS, "color"): cv.string,
- vol.Optional(CONF_RGBW_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All(
- vol.Coerce(int), vol.Range(min=1)
- ),
- vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All(
- vol.Coerce(int), vol.Range(min=1)
- ),
- }
- ),
- vol.Any(
- # either global "address" or all addresses for individual colors are required
- vol.Schema(
- {
- vol.Required(CONF_INDIVIDUAL_COLORS): {
- vol.Required(CONF_RED): {vol.Required(CONF_ADDRESS): object},
- vol.Required(CONF_GREEN): {vol.Required(CONF_ADDRESS): object},
- vol.Required(CONF_BLUE): {vol.Required(CONF_ADDRESS): object},
- },
- },
- extra=vol.ALLOW_EXTRA,
- ),
- vol.Schema(
- {
- vol.Required(CONF_ADDRESS): object,
- },
- extra=vol.ALLOW_EXTRA,
- ),
- ),
- )
-
-
class ClimateSchema:
"""Voluptuous schema for KNX climate devices."""
@@ -240,6 +144,7 @@ class ClimateSchema:
CONF_ON_OFF_INVERT = "on_off_invert"
CONF_MIN_TEMP = "min_temp"
CONF_MAX_TEMP = "max_temp"
+ CONF_CREATE_TEMPERATURE_SENSORS = "create_temperature_sensors"
DEFAULT_NAME = "KNX Climate"
DEFAULT_SETPOINT_SHIFT_MODE = "DPT6010"
@@ -255,7 +160,7 @@ class ClimateSchema:
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(
CONF_SETPOINT_SHIFT_MODE, default=DEFAULT_SETPOINT_SHIFT_MODE
- ): cv.enum(SetpointShiftMode),
+ ): vol.All(vol.Upper, cv.enum(SetpointShiftMode)),
vol.Optional(
CONF_SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX
): vol.All(int, vol.Range(min=0, max=32)),
@@ -265,25 +170,27 @@ class ClimateSchema:
vol.Optional(
CONF_TEMPERATURE_STEP, default=DEFAULT_TEMPERATURE_STEP
): vol.All(float, vol.Range(min=0, max=2)),
- vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string,
- vol.Required(CONF_TARGET_TEMPERATURE_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_TARGET_TEMPERATURE_ADDRESS): cv.string,
- vol.Optional(CONF_SETPOINT_SHIFT_ADDRESS): cv.string,
- vol.Optional(CONF_SETPOINT_SHIFT_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_OPERATION_MODE_ADDRESS): cv.string,
- vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string,
- vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_CONTROLLER_MODE_ADDRESS): cv.string,
- vol.Optional(CONF_CONTROLLER_MODE_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_HEAT_COOL_ADDRESS): cv.string,
- vol.Optional(CONF_HEAT_COOL_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS): cv.string,
- vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): cv.string,
- vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string,
- vol.Optional(CONF_OPERATION_MODE_STANDBY_ADDRESS): cv.string,
- vol.Optional(CONF_ON_OFF_ADDRESS): cv.string,
- vol.Optional(CONF_ON_OFF_STATE_ADDRESS): cv.string,
+ vol.Required(CONF_TEMPERATURE_ADDRESS): ga_list_validator,
+ vol.Required(CONF_TARGET_TEMPERATURE_STATE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_TARGET_TEMPERATURE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_SETPOINT_SHIFT_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_SETPOINT_SHIFT_STATE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_OPERATION_MODE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_CONTROLLER_MODE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_CONTROLLER_MODE_STATE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_HEAT_COOL_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_HEAT_COOL_STATE_ADDRESS): ga_list_validator,
+ vol.Optional(
+ CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS
+ ): ga_list_validator,
+ vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_OPERATION_MODE_STANDBY_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_ON_OFF_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_ON_OFF_STATE_ADDRESS): ga_list_validator,
vol.Optional(
CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT
): cv.boolean,
@@ -295,24 +202,51 @@ class ClimateSchema:
),
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
+ vol.Optional(
+ CONF_CREATE_TEMPERATURE_SENSORS, default=False
+ ): cv.boolean,
}
),
)
-class SwitchSchema:
- """Voluptuous schema for KNX switches."""
+class CoverSchema:
+ """Voluptuous schema for KNX covers."""
- CONF_INVERT = CONF_INVERT
- CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
+ CONF_MOVE_LONG_ADDRESS = "move_long_address"
+ CONF_MOVE_SHORT_ADDRESS = "move_short_address"
+ CONF_STOP_ADDRESS = "stop_address"
+ CONF_POSITION_ADDRESS = "position_address"
+ CONF_POSITION_STATE_ADDRESS = "position_state_address"
+ CONF_ANGLE_ADDRESS = "angle_address"
+ CONF_ANGLE_STATE_ADDRESS = "angle_state_address"
+ CONF_TRAVELLING_TIME_DOWN = "travelling_time_down"
+ CONF_TRAVELLING_TIME_UP = "travelling_time_up"
+ CONF_INVERT_POSITION = "invert_position"
+ CONF_INVERT_ANGLE = "invert_angle"
+
+ DEFAULT_TRAVEL_TIME = 25
+ DEFAULT_NAME = "KNX Cover"
- DEFAULT_NAME = "KNX Switch"
SCHEMA = vol.Schema(
{
- vol.Required(CONF_ADDRESS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_STATE_ADDRESS): cv.string,
- vol.Optional(CONF_INVERT): cv.boolean,
+ vol.Optional(CONF_MOVE_LONG_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_MOVE_SHORT_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_STOP_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_POSITION_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_POSITION_STATE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_ANGLE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_ANGLE_STATE_ADDRESS): ga_list_validator,
+ vol.Optional(
+ CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME
+ ): cv.positive_float,
+ vol.Optional(
+ CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME
+ ): cv.positive_float,
+ vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
+ vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
+ vol.Optional(CONF_DEVICE_CLASS): cv.string,
}
)
@@ -323,17 +257,141 @@ class ExposeSchema:
CONF_KNX_EXPOSE_TYPE = CONF_TYPE
CONF_KNX_EXPOSE_ATTRIBUTE = "attribute"
CONF_KNX_EXPOSE_DEFAULT = "default"
- CONF_KNX_EXPOSE_ADDRESS = CONF_ADDRESS
+ EXPOSE_TIME_TYPES = [
+ "time",
+ "date",
+ "datetime",
+ ]
- SCHEMA = vol.Schema(
+ EXPOSE_TIME_SCHEMA = vol.Schema(
{
- vol.Required(CONF_KNX_EXPOSE_TYPE): vol.Any(int, float, str),
- vol.Optional(CONF_ENTITY_ID): cv.entity_id,
+ vol.Required(CONF_KNX_EXPOSE_TYPE): vol.All(
+ cv.string, str.lower, vol.In(EXPOSE_TIME_TYPES)
+ ),
+ vol.Required(KNX_ADDRESS): ga_validator,
+ }
+ )
+ EXPOSE_SENSOR_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_KNX_EXPOSE_TYPE): sensor_type_validator,
+ vol.Required(KNX_ADDRESS): ga_validator,
+ vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_KNX_EXPOSE_ATTRIBUTE): cv.string,
vol.Optional(CONF_KNX_EXPOSE_DEFAULT): cv.match_all,
- vol.Required(CONF_KNX_EXPOSE_ADDRESS): cv.string,
}
)
+ SCHEMA = vol.Any(EXPOSE_TIME_SCHEMA, EXPOSE_SENSOR_SCHEMA)
+
+
+class FanSchema:
+ """Voluptuous schema for KNX fans."""
+
+ CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
+ CONF_OSCILLATION_ADDRESS = "oscillation_address"
+ CONF_OSCILLATION_STATE_ADDRESS = "oscillation_state_address"
+ CONF_MAX_STEP = "max_step"
+
+ DEFAULT_NAME = "KNX Fan"
+
+ SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Required(KNX_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_OSCILLATION_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_MAX_STEP): cv.byte,
+ }
+ )
+
+
+class LightSchema:
+ """Voluptuous schema for KNX lights."""
+
+ CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
+ CONF_BRIGHTNESS_ADDRESS = "brightness_address"
+ CONF_BRIGHTNESS_STATE_ADDRESS = "brightness_state_address"
+ CONF_COLOR_ADDRESS = "color_address"
+ CONF_COLOR_STATE_ADDRESS = "color_state_address"
+ CONF_COLOR_TEMP_ADDRESS = "color_temperature_address"
+ CONF_COLOR_TEMP_STATE_ADDRESS = "color_temperature_state_address"
+ CONF_COLOR_TEMP_MODE = "color_temperature_mode"
+ CONF_RGBW_ADDRESS = "rgbw_address"
+ CONF_RGBW_STATE_ADDRESS = "rgbw_state_address"
+ CONF_MIN_KELVIN = "min_kelvin"
+ CONF_MAX_KELVIN = "max_kelvin"
+
+ DEFAULT_NAME = "KNX Light"
+ DEFAULT_COLOR_TEMP_MODE = "absolute"
+ DEFAULT_MIN_KELVIN = 2700 # 370 mireds
+ DEFAULT_MAX_KELVIN = 6000 # 166 mireds
+
+ CONF_INDIVIDUAL_COLORS = "individual_colors"
+ CONF_RED = "red"
+ CONF_GREEN = "green"
+ CONF_BLUE = "blue"
+ CONF_WHITE = "white"
+
+ COLOR_SCHEMA = vol.Schema(
+ {
+ vol.Optional(KNX_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
+ vol.Required(CONF_BRIGHTNESS_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): ga_list_validator,
+ }
+ )
+
+ SCHEMA = vol.All(
+ vol.Schema(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(KNX_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_BRIGHTNESS_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): ga_list_validator,
+ vol.Exclusive(CONF_INDIVIDUAL_COLORS, "color"): {
+ vol.Inclusive(CONF_RED, "colors"): COLOR_SCHEMA,
+ vol.Inclusive(CONF_GREEN, "colors"): COLOR_SCHEMA,
+ vol.Inclusive(CONF_BLUE, "colors"): COLOR_SCHEMA,
+ vol.Optional(CONF_WHITE): COLOR_SCHEMA,
+ },
+ vol.Exclusive(CONF_COLOR_ADDRESS, "color"): ga_list_validator,
+ vol.Optional(CONF_COLOR_STATE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_COLOR_TEMP_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): ga_list_validator,
+ vol.Optional(
+ CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE
+ ): vol.All(vol.Upper, cv.enum(ColorTempModes)),
+ vol.Exclusive(CONF_RGBW_ADDRESS, "color"): ga_list_validator,
+ vol.Optional(CONF_RGBW_STATE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All(
+ vol.Coerce(int), vol.Range(min=1)
+ ),
+ vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All(
+ vol.Coerce(int), vol.Range(min=1)
+ ),
+ }
+ ),
+ vol.Any(
+ # either global "address" or all addresses for individual colors are required
+ vol.Schema(
+ {
+ vol.Required(CONF_INDIVIDUAL_COLORS): {
+ vol.Required(CONF_RED): {vol.Required(KNX_ADDRESS): object},
+ vol.Required(CONF_GREEN): {vol.Required(KNX_ADDRESS): object},
+ vol.Required(CONF_BLUE): {vol.Required(KNX_ADDRESS): object},
+ },
+ },
+ extra=vol.ALLOW_EXTRA,
+ ),
+ vol.Schema(
+ {
+ vol.Required(KNX_ADDRESS): object,
+ },
+ extra=vol.ALLOW_EXTRA,
+ ),
+ ),
+ )
class NotifySchema:
@@ -343,8 +401,23 @@ class NotifySchema:
SCHEMA = vol.Schema(
{
- vol.Required(CONF_ADDRESS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Required(KNX_ADDRESS): ga_validator,
+ }
+ )
+
+
+class SceneSchema:
+ """Voluptuous schema for KNX scenes."""
+
+ CONF_SCENE_NUMBER = "scene_number"
+
+ DEFAULT_NAME = "KNX SCENE"
+ SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Required(KNX_ADDRESS): ga_list_validator,
+ vol.Required(CONF_SCENE_NUMBER): cv.positive_int,
}
)
@@ -360,29 +433,27 @@ class SensorSchema:
SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_SYNC_STATE, default=True): vol.Any(
- vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)),
- cv.boolean,
- cv.string,
- ),
+ vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
vol.Optional(CONF_ALWAYS_CALLBACK, default=False): cv.boolean,
- vol.Required(CONF_STATE_ADDRESS): cv.string,
- vol.Required(CONF_TYPE): vol.Any(int, float, str),
+ vol.Required(CONF_TYPE): sensor_type_validator,
+ vol.Required(CONF_STATE_ADDRESS): ga_list_validator,
}
)
-class SceneSchema:
- """Voluptuous schema for KNX scenes."""
+class SwitchSchema:
+ """Voluptuous schema for KNX switches."""
- CONF_SCENE_NUMBER = "scene_number"
+ CONF_INVERT = CONF_INVERT
+ CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
- DEFAULT_NAME = "KNX SCENE"
+ DEFAULT_NAME = "KNX Switch"
SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Required(CONF_ADDRESS): cv.string,
- vol.Required(CONF_SCENE_NUMBER): cv.positive_int,
+ vol.Optional(CONF_INVERT, default=False): cv.boolean,
+ vol.Required(KNX_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
}
)
@@ -397,36 +468,34 @@ class WeatherSchema:
CONF_KNX_BRIGHTNESS_WEST_ADDRESS = "address_brightness_west"
CONF_KNX_BRIGHTNESS_NORTH_ADDRESS = "address_brightness_north"
CONF_KNX_WIND_SPEED_ADDRESS = "address_wind_speed"
+ CONF_KNX_WIND_BEARING_ADDRESS = "address_wind_bearing"
CONF_KNX_RAIN_ALARM_ADDRESS = "address_rain_alarm"
CONF_KNX_FROST_ALARM_ADDRESS = "address_frost_alarm"
CONF_KNX_WIND_ALARM_ADDRESS = "address_wind_alarm"
CONF_KNX_DAY_NIGHT_ADDRESS = "address_day_night"
CONF_KNX_AIR_PRESSURE_ADDRESS = "address_air_pressure"
CONF_KNX_HUMIDITY_ADDRESS = "address_humidity"
- CONF_KNX_EXPOSE_SENSORS = "expose_sensors"
+ CONF_KNX_CREATE_SENSORS = "create_sensors"
DEFAULT_NAME = "KNX Weather Station"
SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_SYNC_STATE, default=True): vol.Any(
- vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)),
- cv.boolean,
- cv.string,
- ),
- vol.Optional(CONF_KNX_EXPOSE_SENSORS, default=False): cv.boolean,
- vol.Required(CONF_KNX_TEMPERATURE_ADDRESS): cv.string,
- vol.Optional(CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS): cv.string,
- vol.Optional(CONF_KNX_BRIGHTNESS_EAST_ADDRESS): cv.string,
- vol.Optional(CONF_KNX_BRIGHTNESS_WEST_ADDRESS): cv.string,
- vol.Optional(CONF_KNX_BRIGHTNESS_NORTH_ADDRESS): cv.string,
- vol.Optional(CONF_KNX_WIND_SPEED_ADDRESS): cv.string,
- vol.Optional(CONF_KNX_RAIN_ALARM_ADDRESS): cv.string,
- vol.Optional(CONF_KNX_FROST_ALARM_ADDRESS): cv.string,
- vol.Optional(CONF_KNX_WIND_ALARM_ADDRESS): cv.string,
- vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): cv.string,
- vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): cv.string,
- vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): cv.string,
+ vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
+ vol.Optional(CONF_KNX_CREATE_SENSORS, default=False): cv.boolean,
+ vol.Required(CONF_KNX_TEMPERATURE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_KNX_BRIGHTNESS_EAST_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_KNX_BRIGHTNESS_WEST_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_KNX_BRIGHTNESS_NORTH_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_KNX_WIND_SPEED_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_KNX_WIND_BEARING_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_KNX_RAIN_ALARM_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_KNX_FROST_ALARM_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_KNX_WIND_ALARM_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): ga_list_validator,
+ vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): ga_list_validator,
}
)
diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py
index dc9ffcb61b74f7..f14cf7e5b29fa0 100644
--- a/homeassistant/components/knx/sensor.py
+++ b/homeassistant/components/knx/sensor.py
@@ -1,14 +1,25 @@
"""Support for KNX/IP sensors."""
+from __future__ import annotations
+
+from typing import Callable, Iterable
+
from xknx.devices import Sensor as XknxSensor
-from homeassistant.components.sensor import DEVICE_CLASSES
+from homeassistant.components.sensor import DEVICE_CLASSES, SensorEntity
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from .const import DOMAIN
from .knx_entity import KnxEntity
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: Callable[[Iterable[Entity]], None],
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
"""Set up sensor(s) for KNX platform."""
entities = []
for device in hass.data[DOMAIN].xknx.devices:
@@ -17,25 +28,26 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(entities)
-class KNXSensor(KnxEntity, Entity):
+class KNXSensor(KnxEntity, SensorEntity):
"""Representation of a KNX sensor."""
- def __init__(self, device: XknxSensor):
+ def __init__(self, device: XknxSensor) -> None:
"""Initialize of a KNX sensor."""
+ self._device: XknxSensor
super().__init__(device)
@property
- def state(self):
+ def state(self) -> StateType:
"""Return the state of the sensor."""
return self._device.resolve_state()
@property
- def unit_of_measurement(self):
+ def unit_of_measurement(self) -> str | None:
"""Return the unit this state is expressed in."""
return self._device.unit_of_measurement()
@property
- def device_class(self):
+ def device_class(self) -> str | None:
"""Return the device class of the sensor."""
device_class = self._device.ha_device_class()
if device_class in DEVICE_CLASSES:
diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml
index cab8c100b01fd1..c13abb23d94fb9 100644
--- a/homeassistant/components/knx/services.yaml
+++ b/homeassistant/components/knx/services.yaml
@@ -1,20 +1,99 @@
send:
+ name: "Send to KNX bus"
description: "Send arbitrary data directly to the KNX bus."
fields:
address:
- description: "Group address(es) to write to."
+ name: "Group address"
+ description: "Group address(es) to write to. Lists will send to multiple group addresses successively."
+ required: true
example: "1/1/0"
+ selector:
+ object:
payload:
+ name: "Payload"
description: "Payload to send to the bus. Integers are treated as DPT 1/2/3 payloads. For DPTs > 6 bits send a list. Each value represents 1 octet (0-255). Pad with 0 to DPT byte length."
+ required: true
example: "[0, 4]"
+ selector:
+ object:
type:
+ name: "Value type"
description: "Optional. If set, the payload will not be sent as raw bytes, but encoded as given DPT. Knx sensor types are valid values (see https://www.home-assistant.io/integrations/sensor.knx)."
+ required: false
example: "temperature"
+ selector:
+ text:
+read:
+ name: "Read from KNX bus"
+ description: "Send GroupValueRead requests to the KNX bus. Response can be used from `knx_event` and will be processed in KNX entities."
+ fields:
+ address:
+ name: "Group address"
+ description: "Group address(es) to send read request to. Lists will read multiple group addresses."
+ required: true
+ example: "1/1/0"
+ selector:
+ object:
event_register:
- description: "Add or remove single group address to knx_event filter for triggering `knx_event`s. Only addresses added with this service can be removed."
+ name: "Register knx_event"
+ description: "Add or remove group addresses to knx_event filter for triggering `knx_event`s. Only addresses added with this service can be removed."
+ fields:
+ address:
+ name: "Group address"
+ description: "Group address(es) that shall be added or removed. Lists are allowed."
+ required: true
+ example: "1/1/0"
+ selector:
+ object:
+ remove:
+ name: "Remove event registration"
+ description: "Optional. If `True` the group address(es) will be removed."
+ default: false
+ selector:
+ boolean:
+exposure_register:
+ name: "Expose to KNX bus"
+ description: "Add or remove exposures to KNX bus. Only exposures added with this service can be removed."
fields:
address:
- description: "Group address that shall be added or removed."
+ name: "Group address"
+ description: "Group address state or attribute updates will be sent to. GroupValueRead requests will be answered. Per address only one exposure can be registered."
+ required: true
example: "1/1/0"
+ selector:
+ text:
+ type:
+ name: "Value type"
+ description: "Telegrams will be encoded as given DPT. 'binary' and all Knx sensor types are valid values (see https://www.home-assistant.io/integrations/sensor.knx)"
+ required: true
+ example: "percentU8"
+ selector:
+ text:
+ entity_id:
+ name: "Entity"
+ description: "Entity id whose state or attribute shall be exposed."
+ required: true
+ example: "light.kitchen"
+ selector:
+ entity:
+ attribute:
+ name: "Entity attribute"
+ description: "Optional. Attribute of the entity that shall be sent to the KNX bus. If not set the state will be sent. Eg. for a light the state is eigther “on” or “off” - with attribute you can expose its “brightness”."
+ example: "brightness"
+ selector:
+ text:
+ default:
+ name: "Default value"
+ description: "Optional. Default value to send to the bus if the state or attribute value is None. Eg. a light with state “off” has no brightness attribute so a default value of 0 could be used. If not set (or None) no value would be sent to the bus and a GroupReadRequest to the address would return the last known value."
+ example: "0"
+ selector:
+ object:
remove:
- description: "Optional. If `True` the group address will be removed. Defaults to `False`."
+ name: "Remove exposure"
+ description: "Optional. If `True` the exposure will be removed. Only `address` is required for removal."
+ default: false
+ selector:
+ boolean:
+reload:
+ name: "Reload KNX configuration"
+ description: "Reload the KNX configuration from YAML."
diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py
index ae3048e2d23adc..82fe2f40be3791 100644
--- a/homeassistant/components/knx/switch.py
+++ b/homeassistant/components/knx/switch.py
@@ -1,13 +1,25 @@
"""Support for KNX/IP switches."""
+from __future__ import annotations
+
+from typing import Any, Callable, Iterable
+
from xknx.devices import Switch as XknxSwitch
from homeassistant.components.switch import SwitchEntity
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
-from . import DOMAIN
+from .const import DOMAIN
from .knx_entity import KnxEntity
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: Callable[[Iterable[Entity]], None],
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
"""Set up switch(es) for KNX platform."""
entities = []
for device in hass.data[DOMAIN].xknx.devices:
@@ -19,19 +31,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class KNXSwitch(KnxEntity, SwitchEntity):
"""Representation of a KNX switch."""
- def __init__(self, device: XknxSwitch):
+ def __init__(self, device: XknxSwitch) -> None:
"""Initialize of KNX switch."""
+ self._device: XknxSwitch
super().__init__(device)
@property
- def is_on(self):
+ def is_on(self) -> bool:
"""Return true if device is on."""
- return self._device.state
+ return bool(self._device.state)
- async def async_turn_on(self, **kwargs):
+ async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
await self._device.set_on()
- async def async_turn_off(self, **kwargs):
+ async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
await self._device.set_off()
diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py
index 097fa661f4ad0a..cc2f3c0a09c60a 100644
--- a/homeassistant/components/knx/weather.py
+++ b/homeassistant/components/knx/weather.py
@@ -1,16 +1,27 @@
"""Support for KNX/IP weather station."""
+from __future__ import annotations
+
+from typing import Callable, Iterable
from xknx.devices import Weather as XknxWeather
from homeassistant.components.weather import WeatherEntity
from homeassistant.const import TEMP_CELSIUS
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN
from .knx_entity import KnxEntity
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the scenes for KNX platform."""
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: Callable[[Iterable[Entity]], None],
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
+ """Set up weather entities for KNX platform."""
entities = []
for device in hass.data[DOMAIN].xknx.devices:
if isinstance(device, XknxWeather):
@@ -21,22 +32,23 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class KNXWeather(KnxEntity, WeatherEntity):
"""Representation of a KNX weather device."""
- def __init__(self, device: XknxWeather):
+ def __init__(self, device: XknxWeather) -> None:
"""Initialize of a KNX sensor."""
+ self._device: XknxWeather
super().__init__(device)
@property
- def temperature(self):
+ def temperature(self) -> float | None:
"""Return current temperature."""
return self._device.temperature
@property
- def temperature_unit(self):
+ def temperature_unit(self) -> str:
"""Return temperature unit."""
return TEMP_CELSIUS
@property
- def pressure(self):
+ def pressure(self) -> float | None:
"""Return current air pressure."""
# KNX returns pA - HA requires hPa
return (
@@ -46,17 +58,22 @@ def pressure(self):
)
@property
- def condition(self):
+ def condition(self) -> str:
"""Return current weather condition."""
return self._device.ha_current_state().value
@property
- def humidity(self):
+ def humidity(self) -> float | None:
"""Return current humidity."""
- return self._device.humidity if self._device.humidity is not None else None
+ return self._device.humidity
+
+ @property
+ def wind_bearing(self) -> int | None:
+ """Return current wind bearing in degrees."""
+ return self._device.wind_bearing
@property
- def wind_speed(self):
+ def wind_speed(self) -> float | None:
"""Return current wind speed in km/h."""
# KNX only supports wind speed in m/s
return (
diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py
index 4dcb25b3ea9a00..ea867e8c407f14 100644
--- a/homeassistant/components/kodi/__init__.py
+++ b/homeassistant/components/kodi/__init__.py
@@ -72,9 +72,9 @@ async def _close(event):
DATA_REMOVE_LISTENER: remove_stop_listener,
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -85,8 +85,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py
index bdbac4ab7624c0..a7e87a6ae272b2 100644
--- a/homeassistant/components/kodi/browse_media.py
+++ b/homeassistant/components/kodi/browse_media.py
@@ -1,4 +1,5 @@
"""Support for media browsing."""
+import asyncio
import logging
from homeassistant.components.media_player import BrowseError, BrowseMedia
@@ -58,123 +59,20 @@ class UnknownMediaType(BrowseError):
"""Unknown media type."""
-async def build_item_response(media_library, payload):
+async def build_item_response(media_library, payload, get_thumbnail_url=None):
"""Create response payload for the provided media query."""
search_id = payload["search_id"]
search_type = payload["search_type"]
- thumbnail = None
- title = None
- media = None
-
- properties = ["thumbnail"]
- if search_type == MEDIA_TYPE_ALBUM:
- if search_id:
- album = await media_library.get_album_details(
- album_id=int(search_id), properties=properties
- )
- thumbnail = media_library.thumbnail_url(
- album["albumdetails"].get("thumbnail")
- )
- title = album["albumdetails"]["label"]
- media = await media_library.get_songs(
- album_id=int(search_id),
- properties=[
- "albumid",
- "artist",
- "duration",
- "album",
- "thumbnail",
- "track",
- ],
- )
- media = media.get("songs")
- else:
- media = await media_library.get_albums(properties=properties)
- media = media.get("albums")
- title = "Albums"
-
- elif search_type == MEDIA_TYPE_ARTIST:
- if search_id:
- media = await media_library.get_albums(
- artist_id=int(search_id), properties=properties
- )
- media = media.get("albums")
- artist = await media_library.get_artist_details(
- artist_id=int(search_id), properties=properties
- )
- thumbnail = media_library.thumbnail_url(
- artist["artistdetails"].get("thumbnail")
- )
- title = artist["artistdetails"]["label"]
- else:
- media = await media_library.get_artists(properties)
- media = media.get("artists")
- title = "Artists"
-
- elif search_type == "library_music":
- library = {MEDIA_TYPE_ALBUM: "Albums", MEDIA_TYPE_ARTIST: "Artists"}
- media = [{"label": name, "type": type_} for type_, name in library.items()]
- title = "Music Library"
-
- elif search_type == MEDIA_TYPE_MOVIE:
- media = await media_library.get_movies(properties)
- media = media.get("movies")
- title = "Movies"
-
- elif search_type == MEDIA_TYPE_TVSHOW:
- if search_id:
- media = await media_library.get_seasons(
- tv_show_id=int(search_id),
- properties=["thumbnail", "season", "tvshowid"],
- )
- media = media.get("seasons")
- tvshow = await media_library.get_tv_show_details(
- tv_show_id=int(search_id), properties=properties
- )
- thumbnail = media_library.thumbnail_url(
- tvshow["tvshowdetails"].get("thumbnail")
- )
- title = tvshow["tvshowdetails"]["label"]
- else:
- media = await media_library.get_tv_shows(properties)
- media = media.get("tvshows")
- title = "TV Shows"
-
- elif search_type == MEDIA_TYPE_SEASON:
- tv_show_id, season_id = search_id.split("/", 1)
- media = await media_library.get_episodes(
- tv_show_id=int(tv_show_id),
- season_id=int(season_id),
- properties=["thumbnail", "tvshowid", "seasonid"],
- )
- media = media.get("episodes")
- if media:
- season = await media_library.get_season_details(
- season_id=int(media[0]["seasonid"]), properties=properties
- )
- thumbnail = media_library.thumbnail_url(
- season["seasondetails"].get("thumbnail")
- )
- title = season["seasondetails"]["label"]
-
- elif search_type == MEDIA_TYPE_CHANNEL:
- media = await media_library.get_channels(
- channel_group_id="alltv",
- properties=["thumbnail", "channeltype", "channel", "broadcastnow"],
- )
- media = media.get("channels")
- title = "Channels"
+ _, title, media = await get_media_info(media_library, search_id, search_type)
+ thumbnail = await get_thumbnail_url(search_type, search_id)
if media is None:
return None
- children = []
- for item in media:
- try:
- children.append(item_payload(item, media_library))
- except UnknownMediaType:
- pass
+ children = await asyncio.gather(
+ *[item_payload(item, get_thumbnail_url) for item in media]
+ )
if search_type in (MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE) and search_id == "":
children.sort(key=lambda x: x.title.replace("The ", "", 1), reverse=False)
@@ -200,16 +98,13 @@ async def build_item_response(media_library, payload):
return response
-def item_payload(item, media_library):
+async def item_payload(item, get_thumbnail_url=None):
"""
Create response payload for a single media item.
Used by async_browse_media.
"""
title = item["label"]
- thumbnail = item.get("thumbnail")
- if thumbnail:
- thumbnail = media_library.thumbnail_url(thumbnail)
media_class = None
@@ -273,6 +168,12 @@ def item_payload(item, media_library):
_LOGGER.debug("Unknown media type received: %s", media_content_type)
raise UnknownMediaType from err
+ thumbnail = item.get("thumbnail")
+ if thumbnail is not None and get_thumbnail_url is not None:
+ thumbnail = await get_thumbnail_url(
+ media_content_type, media_content_id, thumbnail_url=thumbnail
+ )
+
return BrowseMedia(
title=title,
media_class=media_class,
@@ -284,7 +185,7 @@ def item_payload(item, media_library):
)
-def library_payload(media_library):
+async def library_payload():
"""
Create response payload to describe contents of a specific library.
@@ -306,12 +207,137 @@ def library_payload(media_library):
MEDIA_TYPE_TVSHOW: "TV shows",
MEDIA_TYPE_CHANNEL: "Channels",
}
- for item in [{"label": name, "type": type_} for type_, name in library.items()]:
- library_info.children.append(
+
+ library_info.children = await asyncio.gather(
+ *[
item_payload(
- {"label": item["label"], "type": item["type"], "uri": item["type"]},
- media_library,
+ {
+ "label": item["label"],
+ "type": item["type"],
+ "uri": item["type"],
+ },
)
- )
+ for item in [
+ {"label": name, "type": type_} for type_, name in library.items()
+ ]
+ ]
+ )
return library_info
+
+
+async def get_media_info(media_library, search_id, search_type):
+ """Fetch media/album."""
+ thumbnail = None
+ title = None
+ media = None
+
+ properties = ["thumbnail"]
+ if search_type == MEDIA_TYPE_ALBUM:
+ if search_id:
+ album = await media_library.get_album_details(
+ album_id=int(search_id), properties=properties
+ )
+ thumbnail = media_library.thumbnail_url(
+ album["albumdetails"].get("thumbnail")
+ )
+ title = album["albumdetails"]["label"]
+ media = await media_library.get_songs(
+ album_id=int(search_id),
+ properties=[
+ "albumid",
+ "artist",
+ "duration",
+ "album",
+ "thumbnail",
+ "track",
+ ],
+ )
+ media = media.get("songs")
+ else:
+ media = await media_library.get_albums(properties=properties)
+ media = media.get("albums")
+ title = "Albums"
+
+ elif search_type == MEDIA_TYPE_ARTIST:
+ if search_id:
+ media = await media_library.get_albums(
+ artist_id=int(search_id), properties=properties
+ )
+ media = media.get("albums")
+ artist = await media_library.get_artist_details(
+ artist_id=int(search_id), properties=properties
+ )
+ thumbnail = media_library.thumbnail_url(
+ artist["artistdetails"].get("thumbnail")
+ )
+ title = artist["artistdetails"]["label"]
+ else:
+ media = await media_library.get_artists(properties)
+ media = media.get("artists")
+ title = "Artists"
+
+ elif search_type == "library_music":
+ library = {MEDIA_TYPE_ALBUM: "Albums", MEDIA_TYPE_ARTIST: "Artists"}
+ media = [{"label": name, "type": type_} for type_, name in library.items()]
+ title = "Music Library"
+
+ elif search_type == MEDIA_TYPE_MOVIE:
+ if search_id:
+ movie = await media_library.get_movie_details(
+ movie_id=int(search_id), properties=properties
+ )
+ thumbnail = media_library.thumbnail_url(
+ movie["moviedetails"].get("thumbnail")
+ )
+ title = movie["moviedetails"]["label"]
+ else:
+ media = await media_library.get_movies(properties)
+ media = media.get("movies")
+ title = "Movies"
+
+ elif search_type == MEDIA_TYPE_TVSHOW:
+ if search_id:
+ media = await media_library.get_seasons(
+ tv_show_id=int(search_id),
+ properties=["thumbnail", "season", "tvshowid"],
+ )
+ media = media.get("seasons")
+ tvshow = await media_library.get_tv_show_details(
+ tv_show_id=int(search_id), properties=properties
+ )
+ thumbnail = media_library.thumbnail_url(
+ tvshow["tvshowdetails"].get("thumbnail")
+ )
+ title = tvshow["tvshowdetails"]["label"]
+ else:
+ media = await media_library.get_tv_shows(properties)
+ media = media.get("tvshows")
+ title = "TV Shows"
+
+ elif search_type == MEDIA_TYPE_SEASON:
+ tv_show_id, season_id = search_id.split("/", 1)
+ media = await media_library.get_episodes(
+ tv_show_id=int(tv_show_id),
+ season_id=int(season_id),
+ properties=["thumbnail", "tvshowid", "seasonid"],
+ )
+ media = media.get("episodes")
+ if media:
+ season = await media_library.get_season_details(
+ season_id=int(media[0]["seasonid"]), properties=properties
+ )
+ thumbnail = media_library.thumbnail_url(
+ season["seasondetails"].get("thumbnail")
+ )
+ title = season["seasondetails"]["label"]
+
+ elif search_type == MEDIA_TYPE_CHANNEL:
+ media = await media_library.get_channels(
+ channel_group_id="alltv",
+ properties=["thumbnail", "channeltype", "channel", "broadcastnow"],
+ )
+ media = media.get("channels")
+ title = "Channels"
+
+ return thumbnail, title, media
diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py
index c48e4564f926bd..0f5509a4e6639b 100644
--- a/homeassistant/components/kodi/config_flow.py
+++ b/homeassistant/components/kodi/config_flow.py
@@ -24,8 +24,8 @@
DEFAULT_SSL,
DEFAULT_TIMEOUT,
DEFAULT_WS_PORT,
+ DOMAIN,
)
-from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
@@ -119,7 +119,6 @@ async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType):
}
)
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update({"title_placeholders": {CONF_NAME: self._name}})
try:
diff --git a/homeassistant/components/kodi/device_trigger.py b/homeassistant/components/kodi/device_trigger.py
index 314f73a927e452..b8653290c0daa0 100644
--- a/homeassistant/components/kodi/device_trigger.py
+++ b/homeassistant/components/kodi/device_trigger.py
@@ -1,5 +1,5 @@
"""Provides device automations for Kodi."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -29,7 +29,7 @@
)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers for Kodi devices."""
registry = await entity_registry.async_get_registry(hass)
triggers = []
@@ -61,8 +61,13 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
@callback
def _attach_trigger(
- hass: HomeAssistant, config: ConfigType, action: AutomationActionType, event_type
+ hass: HomeAssistant,
+ config: ConfigType,
+ action: AutomationActionType,
+ event_type,
+ automation_info: dict,
):
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
job = HassJob(action)
@callback
@@ -70,7 +75,7 @@ def _handle_event(event: Event):
if event.data[ATTR_ENTITY_ID] == config[CONF_ENTITY_ID]:
hass.async_run_hass_job(
job,
- {"trigger": {**config, "description": event_type}},
+ {"trigger": {**config, "description": event_type, "id": trigger_id}},
event.context,
)
@@ -84,12 +89,10 @@ async def async_attach_trigger(
automation_info: dict,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
- config = TRIGGER_SCHEMA(config)
-
if config[CONF_TYPE] == "turn_on":
- return _attach_trigger(hass, config, action, EVENT_TURN_ON)
+ return _attach_trigger(hass, config, action, EVENT_TURN_ON, automation_info)
if config[CONF_TYPE] == "turn_off":
- return _attach_trigger(hass, config, action, EVENT_TURN_OFF)
+ return _attach_trigger(hass, config, action, EVENT_TURN_OFF, automation_info)
return lambda: None
diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json
index 24d3393d7c37ba..58d46aea8ba717 100644
--- a/homeassistant/components/kodi/manifest.json
+++ b/homeassistant/components/kodi/manifest.json
@@ -3,7 +3,7 @@
"name": "Kodi",
"documentation": "https://www.home-assistant.io/integrations/kodi",
"requirements": [
- "pykodi==0.2.1"
+ "pykodi==0.2.4"
],
"codeowners": [
"@OnFreund",
diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py
index 6b849cb711b1bf..72197f6b8e2ba5 100644
--- a/homeassistant/components/kodi/media_player.py
+++ b/homeassistant/components/kodi/media_player.py
@@ -3,8 +3,10 @@
from functools import wraps
import logging
import re
+import urllib.parse
import jsonrpc_base
+from jsonrpc_base.jsonrpc import ProtocolError, TransportError
from pykodi import CannotConnectError
import voluptuous as vol
@@ -61,9 +63,10 @@
entity_platform,
)
from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.helpers.network import is_internal_request
import homeassistant.util.dt as dt_util
-from .browse_media import build_item_response, library_payload
+from .browse_media import build_item_response, get_media_info, library_payload
from .const import (
CONF_WS_PORT,
DATA_CONNECTION,
@@ -871,16 +874,50 @@ def _find(key_word, words):
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper."""
+ is_internal = is_internal_request(self.hass)
+
+ async def _get_thumbnail_url(
+ media_content_type,
+ media_content_id,
+ media_image_id=None,
+ thumbnail_url=None,
+ ):
+ if is_internal:
+ return self._kodi.thumbnail_url(thumbnail_url)
+
+ return self.get_browse_image_url(
+ media_content_type,
+ urllib.parse.quote_plus(media_content_id),
+ media_image_id,
+ )
+
if media_content_type in [None, "library"]:
- return await self.hass.async_add_executor_job(library_payload, self._kodi)
+ return await library_payload()
payload = {
"search_type": media_content_type,
"search_id": media_content_id,
}
- response = await build_item_response(self._kodi, payload)
+
+ response = await build_item_response(self._kodi, payload, _get_thumbnail_url)
if response is None:
raise BrowseError(
f"Media not found: {media_content_type} / {media_content_id}"
)
return response
+
+ async def async_get_browse_image(
+ self, media_content_type, media_content_id, media_image_id=None
+ ):
+ """Get media image from kodi server."""
+ try:
+ image_url, _, _ = await get_media_info(
+ self._kodi, media_content_id, media_content_type
+ )
+ except (ProtocolError, TransportError):
+ return (None, None)
+
+ if image_url:
+ return await self._async_fetch_image(image_url)
+
+ return (None, None)
diff --git a/homeassistant/components/kodi/translations/de.json b/homeassistant/components/kodi/translations/de.json
index a0bf05cb5ecd76..15fd212fdbd922 100644
--- a/homeassistant/components/kodi/translations/de.json
+++ b/homeassistant/components/kodi/translations/de.json
@@ -1,10 +1,15 @@
{
"config": {
"abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
"cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "no_uuid": "Die Kodi-Instanz hat keine eindeutige ID. Dies ist h\u00f6chstwahrscheinlich auf eine alte Kodi-Version (17.x oder niedriger) zur\u00fcckzuf\u00fchren. Du kannst die Integration manuell konfigurieren oder auf eine neuere Kodi-Version aktualisieren.",
"unknown": "Unerwarteter Fehler"
},
"error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"flow_title": "Kodi: {name}",
diff --git a/homeassistant/components/kodi/translations/fr.json b/homeassistant/components/kodi/translations/fr.json
index fd0d3e38e81602..a7c4b3f34a1649 100644
--- a/homeassistant/components/kodi/translations/fr.json
+++ b/homeassistant/components/kodi/translations/fr.json
@@ -4,6 +4,7 @@
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
"cannot_connect": "\u00c9chec de connexion",
"invalid_auth": "Authentification erron\u00e9e",
+ "no_uuid": "L'instance Kodi n'a pas d'identifiant unique. Cela est probablement d\u00fb \u00e0 une ancienne version de Kodi (17.x ou inf\u00e9rieure). Vous pouvez configurer l'int\u00e9gration manuellement ou passer \u00e0 une version plus r\u00e9cente de Kodi.",
"unknown": "Erreur inattendue"
},
"error": {
diff --git a/homeassistant/components/kodi/translations/hu.json b/homeassistant/components/kodi/translations/hu.json
index 3b2d79a34a77e2..64dbfac0c8b2cd 100644
--- a/homeassistant/components/kodi/translations/hu.json
+++ b/homeassistant/components/kodi/translations/hu.json
@@ -1,7 +1,41 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "flow_title": "Kodi: {name}",
+ "step": {
+ "credentials": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "description": "Add meg a Kodi felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t. Ezek megtal\u00e1lhat\u00f3k a Rendszer/Be\u00e1ll\u00edt\u00e1sok/H\u00e1l\u00f3zat/Szolg\u00e1ltat\u00e1sok r\u00e9szben."
+ },
+ "discovery_confirm": {
+ "description": "Szeretn\u00e9d hozz\u00e1adni a Kodi (`{name}`)-t a Home Assistant-hoz?",
+ "title": "Felfedezett Kodi"
+ },
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "port": "Port",
+ "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata"
+ }
+ },
+ "ws_port": {
+ "data": {
+ "ws_port": "Port"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/translations/id.json b/homeassistant/components/kodi/translations/id.json
new file mode 100644
index 00000000000000..1a81ab72fabfac
--- /dev/null
+++ b/homeassistant/components/kodi/translations/id.json
@@ -0,0 +1,50 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "no_uuid": "Instans Kodi tidak memiliki ID yang unik. Ini kemungkinan besar karena versi Kodi lama (17.x atau sebelumnya). Anda dapat mengonfigurasi integrasi secara manual atau meningkatkan ke versi Kodi yang lebih baru.",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "flow_title": "Kodi: {name}",
+ "step": {
+ "credentials": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "description": "Masukkan nama pengguna dan kata sandi Kodi Anda. Ini dapat ditemukan di dalam Sistem/Setelan/Jaringan/Layanan."
+ },
+ "discovery_confirm": {
+ "description": "Ingin menambahkan Kodi (`{name}`) to Home Assistant?",
+ "title": "Kodi yang ditemukan"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port",
+ "ssl": "Menggunakan sertifikat SSL"
+ },
+ "description": "Informasi koneksi Kodi. Pastikan untuk mengaktifkan \"Izinkan kontrol Kodi melalui HTTP\" di Sistem/Pengaturan/Jaringan/Layanan."
+ },
+ "ws_port": {
+ "data": {
+ "ws_port": "Port"
+ },
+ "description": "Port WebSocket (kadang-kadang disebut port TCP di Kodi). Untuk terhubung melalui WebSocket, Anda perlu mengaktifkan \"Izinkan program... untuk mengontrol Kodi\" dalam Sistem/Pengaturan/Jaringan/Layanan. Jika WebSocket tidak diaktifkan, hapus port dan biarkan kosong."
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_off": "{entity_name} diminta untuk dimatikan",
+ "turn_on": "{entity_name} diminta untuk dinyalakan"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/translations/ko.json b/homeassistant/components/kodi/translations/ko.json
index 64b08475b688b3..3c5a1a38e963a7 100644
--- a/homeassistant/components/kodi/translations/ko.json
+++ b/homeassistant/components/kodi/translations/ko.json
@@ -1,29 +1,50 @@
{
"config": {
"abort": {
- "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
- "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d"
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "no_uuid": "Kodi \uc778\uc2a4\ud134\uc2a4\uc5d0 \uace0\uc720\ud55c ID\uac00 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774\ub294 \uc624\ub798\ub41c Kodi \ubc84\uc804(17.x \uc774\ud558) \ub54c\ubb38\uc77c \uac00\ub2a5\uc131\uc774 \ub192\uc2b5\ub2c8\ub2e4. \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ud558\uac70\ub098 \ucd5c\uc2e0 Kodi \ubc84\uc804\uc73c\ub85c \uc5c5\uadf8\ub808\uc774\ub4dc\ud574\ubcf4\uc138\uc694.",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
- "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d",
- "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
+ "flow_title": "Kodi: {name}",
"step": {
"credentials": {
- "description": "Kodi \uc0ac\uc6a9\uc790\uba85\uacfc \uc554\ud638\ub97c \uc785\ub825\ud558\uc2ed\uc2dc\uc624. \uc774\ub7ec\ud55c \ub0b4\uc6a9\uc740 \uc2dc\uc2a4\ud15c/\uc124\uc815/\ub124\ud2b8\uc6cc\ud06c/\uc11c\ube44\uc2a4\uc5d0\uc11c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ },
+ "description": "Kodi \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc124\uc815/\uc11c\ube44\uc2a4/\ucee8\ud2b8\ub864/\uc6f9 \uc11c\ubc84\uc5d0\uc11c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"discovery_confirm": {
- "description": "Kodi (` {name} `)\ub97c Home Assistant\uc5d0 \ucd94\uac00 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
- "title": "Kodi \ubc1c\uacac"
+ "description": "Home Assistant\uc5d0 Kodi (`{name}`)\uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "\ubc1c\uacac\ub41c Kodi"
},
"user": {
- "description": "Kodi \uc5f0\uacb0 \uc815\ubcf4. \uc2dc\uc2a4\ud15c / \uc124\uc815 / \ub124\ud2b8\uc6cc\ud06c / \uc11c\ube44\uc2a4\uc5d0\uc11c \"HTTP\ub97c \ud1b5\ud55c Kodi \uc81c\uc5b4 \ud5c8\uc6a9\"\uc744 \ud65c\uc131\ud654\ud588\ub294\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624."
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "port": "\ud3ec\ud2b8",
+ "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9"
+ },
+ "description": "Kodi \uc5f0\uacb0 \uc815\ubcf4\uc785\ub2c8\ub2e4. \uc124\uc815/\uc11c\ube44\uc2a4/\ucee8\ud2b8\ub864/\uc6f9 \uc11c\ubc84\uc5d0\uc11c \"HTTP\ub97c \ud1b5\ud55c \uc6d0\uaca9 \uc81c\uc5b4 \ud5c8\uc6a9\"\uc744 \ud65c\uc131\ud654\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694."
},
"ws_port": {
- "description": "WebSocket \ud3ec\ud2b8 (Kodi\uc5d0\uc11c TCP \ud3ec\ud2b8\ub77c\uace0\ub3c4 \ud568). WebSocket\uc744 \ud1b5\ud574 \uc5f0\uacb0\ud558\ub824\uba74 \uc2dc\uc2a4\ud15c / \uc124\uc815 / \ub124\ud2b8\uc6cc\ud06c / \uc11c\ube44\uc2a4\uc5d0\uc11c \"\ud504\ub85c\uadf8\ub7a8\uc774 Kodi\ub97c \uc81c\uc5b4\ud558\ub3c4\ub85d \ud5c8\uc6a9\"\uc744 \ud65c\uc131\ud654\ud574\uc57c\ud569\ub2c8\ub2e4. WebSocket\uc774 \ud65c\uc131\ud654\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \ud3ec\ud2b8\ub97c \uc81c\uac70\ud558\uace0 \ube44\uc6cc \ub461\ub2c8\ub2e4."
+ "data": {
+ "ws_port": "\ud3ec\ud2b8"
+ },
+ "description": "\uc6f9 \uc18c\ucf13 \ud3ec\ud2b8(Kodi\uc5d0\uc11c\ub294 \ud3ec\ud2b8\ub77c\uace0\ub3c4 \ud568)\uc785\ub2c8\ub2e4. \uc6f9 \uc18c\ucf13\uc744 \ud1b5\ud574 \uc5f0\uacb0\ud558\ub824\uba74 \uc124\uc815/\uc11c\ube44\uc2a4/\ucee8\ud2b8\ub864/\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ucee8\ud2b8\ub864\uc5d0\uc11c \"\uc774 \uc2dc\uc2a4\ud15c\uc758/\ub2e4\ub978 \uc2dc\uc2a4\ud15c\uc758 \ud504\ub85c\uadf8\ub7a8\uc5d0 \uc758\ud55c \uc6d0\uaca9 \uc81c\uc5b4 \ud5c8\uc6a9\"\uc744 \ud65c\uc131\ud654\ud574\uc57c \ud569\ub2c8\ub2e4. \uc6f9 \uc18c\ucf13\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\ub294 \uacbd\uc6b0 \ud3ec\ud2b8\ub97c \uc81c\uac70\ud558\uace0 \ube44\uc6cc\ub450\uc138\uc694."
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_off": "{entity_name}\uc774(\uac00) \uaebc\uc9c0\ub3c4\ub85d \uc694\uccad\ub418\uc5c8\uc744 \ub54c",
+ "turn_on": "{entity_name}\uc774(\uac00) \ucf1c\uc9c0\ub3c4\ub85d \uc694\uccad\ub418\uc5c8\uc744 \ub54c"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/translations/nl.json b/homeassistant/components/kodi/translations/nl.json
index 8eb4a39cfb698a..4143d933d19114 100644
--- a/homeassistant/components/kodi/translations/nl.json
+++ b/homeassistant/components/kodi/translations/nl.json
@@ -4,6 +4,7 @@
"already_configured": "Apparaat is al geconfigureerd",
"cannot_connect": "Kon niet verbinden",
"invalid_auth": "Ongeldige authenticatie",
+ "no_uuid": "Kodi-instantie heeft geen unieke ID. Dit komt waarschijnlijk door een oude Kodi-versie (17.x of lager). U kunt de integratie handmatig configureren of upgraden naar een recentere Kodi-versie.",
"unknown": "Onverwachte fout"
},
"error": {
@@ -11,6 +12,7 @@
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
+ "flow_title": "Kodi: {name}",
"step": {
"credentials": {
"data": {
@@ -19,11 +21,15 @@
},
"description": "Voer uw Kodi gebruikersnaam en wachtwoord in. Deze zijn te vinden in Systeem / Instellingen / Netwerk / Services."
},
+ "discovery_confirm": {
+ "description": "Wil je Kodi (`{name}`) toevoegen aan Home Assistant?",
+ "title": "Kodi ontdekt"
+ },
"user": {
"data": {
"host": "Host",
"port": "Poort",
- "ssl": "Maak verbinding via SSL"
+ "ssl": "Gebruik een SSL-certificaat"
},
"description": "Kodi-verbindingsinformatie. Zorg ervoor dat u \"Controle van Kodi via HTTP toestaan\" in Systeem / Instellingen / Netwerk / Services inschakelt."
},
@@ -34,5 +40,11 @@
"description": "De WebSocket-poort (ook wel TCP-poort genoemd in Kodi). Om verbinding te maken via WebSocket, moet u \"Programma's toestaan ... om Kodi te besturen\" inschakelen in Systeem / Instellingen / Netwerk / Services. Als WebSocket niet is ingeschakeld, verwijdert u de poort en laat u deze leeg."
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_off": "{entity_name} werd gevraagd om uit te schakelen",
+ "turn_on": "{entity_name} is gevraagd om in te schakelen"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/translations/ru.json b/homeassistant/components/kodi/translations/ru.json
index 312008c9b629cd..f0ec31654ddb1b 100644
--- a/homeassistant/components/kodi/translations/ru.json
+++ b/homeassistant/components/kodi/translations/ru.json
@@ -3,13 +3,13 @@
"abort": {
"already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"no_uuid": "\u0423 \u044d\u0442\u043e\u0433\u043e \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 Kodi \u043d\u0435\u0442 \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430. \u0421\u043a\u043e\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043e, \u044d\u0442\u043e \u0441\u0432\u044f\u0437\u0430\u043d\u043e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0441\u0442\u0430\u0440\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0435\u0439 Kodi (17.x \u0438\u043b\u0438 \u043d\u0438\u0436\u0435). \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043d\u0430 \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e Kodi.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"flow_title": "Kodi: {name}",
@@ -17,7 +17,7 @@
"credentials": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c Kodi. \u0418\u0445 \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438, \u043f\u0435\u0440\u0435\u0439\u0434\u044f \u0432 \"\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\" - \"\u0421\u043b\u0443\u0436\u0431\u044b\" - \"\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\"."
},
@@ -43,8 +43,8 @@
},
"device_automation": {
"trigger_type": {
- "turn_off": "\u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}",
- "turn_on": "\u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}"
+ "turn_off": "\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}",
+ "turn_on": "\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/translations/tr.json b/homeassistant/components/kodi/translations/tr.json
new file mode 100644
index 00000000000000..54ad8e0b6fdbff
--- /dev/null
+++ b/homeassistant/components/kodi/translations/tr.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "credentials": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ },
+ "description": "L\u00fctfen Kodi kullan\u0131c\u0131 ad\u0131n\u0131z\u0131 ve \u015fifrenizi girin. Bunlar Sistem / Ayarlar / A\u011f / Hizmetler'de bulunabilir."
+ },
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ }
+ },
+ "ws_port": {
+ "data": {
+ "ws_port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/translations/uk.json b/homeassistant/components/kodi/translations/uk.json
new file mode 100644
index 00000000000000..d2acde5dffb767
--- /dev/null
+++ b/homeassistant/components/kodi/translations/uk.json
@@ -0,0 +1,50 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "no_uuid": "\u0423 \u0434\u0430\u043d\u043e\u0433\u043e \u0435\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 Kodi \u043d\u0435\u043c\u0430\u0454 \u0443\u043d\u0456\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440\u0430. \u0406\u043c\u043e\u0432\u0456\u0440\u043d\u043e, \u0446\u0435 \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u043e \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c \u0441\u0442\u0430\u0440\u043e\u0457 \u0432\u0435\u0440\u0441\u0456\u0457 Kodi (17.x \u0430\u0431\u043e \u043d\u0438\u0436\u0447\u0435). \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u0432\u0440\u0443\u0447\u043d\u0443 \u0430\u0431\u043e \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043d\u0430 \u043d\u043e\u0432\u0456\u0448\u0443 \u0432\u0435\u0440\u0441\u0456\u044e Kodi.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "flow_title": "Kodi: {name}",
+ "step": {
+ "credentials": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0456 \u043f\u0430\u0440\u043e\u043b\u044c Kodi. \u0407\u0445 \u043c\u043e\u0436\u043d\u0430 \u0437\u043d\u0430\u0439\u0442\u0438, \u043f\u0435\u0440\u0435\u0439\u0448\u043e\u0432\u0448\u0438 \u0432 \"\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\" - \"\u0421\u043b\u0443\u0436\u0431\u0438\" - \"\u0423\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f\"."
+ },
+ "discovery_confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 Kodi (`{name}`)?",
+ "title": "Kodi"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442",
+ "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL"
+ },
+ "description": "\u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e \"\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0432\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f \u043f\u043e HTTP\" \u0432 \u0440\u043e\u0437\u0434\u0456\u043b\u0456 \"\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\" - \"\u0421\u043b\u0443\u0436\u0431\u0438\" - \"\u0423\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f\"."
+ },
+ "ws_port": {
+ "data": {
+ "ws_port": "\u041f\u043e\u0440\u0442"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043f\u043e WebSocket. \u0429\u043e\u0431 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0447\u0435\u0440\u0435\u0437 WebSocket, \u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u0430\u043c\u0438 \u0432 \u0440\u043e\u0437\u0434\u0456\u043b\u0456 \"\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\" - \"\u0421\u043b\u0443\u0436\u0431\u0438\" - \"\u0423\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f\". \u042f\u043a\u0449\u043e WebSocket \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439, \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c."
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_off": "\u0437\u0430\u043f\u0438\u0442\u0430\u043d\u043e \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f {entity_name}",
+ "turn_on": "\u0437\u0430\u043f\u0438\u0442\u0430\u043d\u043e \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py
index a6bc7eff5ca8ba..db1e20204cd7c2 100644
--- a/homeassistant/components/konnected/__init__.py
+++ b/homeassistant/components/konnected/__init__.py
@@ -18,11 +18,13 @@
CONF_ACCESS_TOKEN,
CONF_BINARY_SENSORS,
CONF_DEVICES,
+ CONF_DISCOVERY,
CONF_HOST,
CONF_ID,
CONF_NAME,
CONF_PIN,
CONF_PORT,
+ CONF_REPEAT,
CONF_SENSORS,
CONF_SWITCHES,
CONF_TYPE,
@@ -48,12 +50,10 @@
CONF_ACTIVATION,
CONF_API_HOST,
CONF_BLINK,
- CONF_DISCOVERY,
CONF_INVERSE,
CONF_MOMENTARY,
CONF_PAUSE,
CONF_POLL_INTERVAL,
- CONF_REPEAT,
DOMAIN,
PIN_TO_ZONE,
STATE_HIGH,
@@ -261,9 +261,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
# async_connect will handle retries until it establishes a connection
await client.async_connect()
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
# config entry specific data to enable unload
@@ -278,8 +278,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -360,8 +360,14 @@ async def update_sensor(self, request: Request, device_id) -> Response:
try:
zone_num = str(payload.get(CONF_ZONE) or PIN_TO_ZONE[payload[CONF_PIN]])
payload[CONF_ZONE] = zone_num
- zone_data = device[CONF_BINARY_SENSORS].get(zone_num) or next(
- (s for s in device[CONF_SENSORS] if s[CONF_ZONE] == zone_num), None
+ zone_data = (
+ device[CONF_BINARY_SENSORS].get(zone_num)
+ or next(
+ (s for s in device[CONF_SWITCHES] if s[CONF_ZONE] == zone_num), None
+ )
+ or next(
+ (s for s in device[CONF_SENSORS] if s[CONF_ZONE] == zone_num), None
+ )
)
except KeyError:
zone_data = None
diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py
index 4e8d13c999e71d..219148e37cfce8 100644
--- a/homeassistant/components/konnected/config_flow.py
+++ b/homeassistant/components/konnected/config_flow.py
@@ -17,10 +17,12 @@
from homeassistant.const import (
CONF_ACCESS_TOKEN,
CONF_BINARY_SENSORS,
+ CONF_DISCOVERY,
CONF_HOST,
CONF_ID,
CONF_NAME,
CONF_PORT,
+ CONF_REPEAT,
CONF_SENSORS,
CONF_SWITCHES,
CONF_TYPE,
@@ -34,13 +36,11 @@
CONF_API_HOST,
CONF_BLINK,
CONF_DEFAULT_OPTIONS,
- CONF_DISCOVERY,
CONF_INVERSE,
CONF_MODEL,
CONF_MOMENTARY,
CONF_PAUSE,
CONF_POLL_INTERVAL,
- CONF_REPEAT,
DOMAIN,
STATE_HIGH,
STATE_LOW,
@@ -169,8 +169,6 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
# class variable to store/share discovered host information
discovered_hosts = {}
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
-
def __init__(self):
"""Initialize the Konnected flow."""
self.data = {}
diff --git a/homeassistant/components/konnected/const.py b/homeassistant/components/konnected/const.py
index c1e7d6b6f269b5..270b26045383e4 100644
--- a/homeassistant/components/konnected/const.py
+++ b/homeassistant/components/konnected/const.py
@@ -9,10 +9,8 @@
CONF_PAUSE = "pause"
CONF_POLL_INTERVAL = "poll_interval"
CONF_PRECISION = "precision"
-CONF_REPEAT = "repeat"
CONF_INVERSE = "inverse"
CONF_BLINK = "blink"
-CONF_DISCOVERY = "discovery"
CONF_DHT_SENSORS = "dht_sensors"
CONF_DS18B20_SENSORS = "ds18b20_sensors"
CONF_MODEL = "model"
diff --git a/homeassistant/components/konnected/handlers.py b/homeassistant/components/konnected/handlers.py
index 923e5d63899b68..879d0d4cf8fa6e 100644
--- a/homeassistant/components/konnected/handlers.py
+++ b/homeassistant/components/konnected/handlers.py
@@ -18,7 +18,7 @@
@HANDLERS.register("state")
async def async_handle_state_update(hass, context, msg):
- """Handle a binary sensor state update."""
+ """Handle a binary sensor or switch state update."""
_LOGGER.debug("[state handler] context: %s msg: %s", context, msg)
entity_id = context.get(ATTR_ENTITY_ID)
state = bool(int(msg.get(ATTR_STATE)))
diff --git a/homeassistant/components/konnected/panel.py b/homeassistant/components/konnected/panel.py
index 76e751592904e8..cf2f33de33292b 100644
--- a/homeassistant/components/konnected/panel.py
+++ b/homeassistant/components/konnected/panel.py
@@ -10,11 +10,13 @@
CONF_ACCESS_TOKEN,
CONF_BINARY_SENSORS,
CONF_DEVICES,
+ CONF_DISCOVERY,
CONF_HOST,
CONF_ID,
CONF_NAME,
CONF_PIN,
CONF_PORT,
+ CONF_REPEAT,
CONF_SENSORS,
CONF_SWITCHES,
CONF_TYPE,
@@ -31,13 +33,11 @@
CONF_BLINK,
CONF_DEFAULT_OPTIONS,
CONF_DHT_SENSORS,
- CONF_DISCOVERY,
CONF_DS18B20_SENSORS,
CONF_INVERSE,
CONF_MOMENTARY,
CONF_PAUSE,
CONF_POLL_INTERVAL,
- CONF_REPEAT,
DOMAIN,
ENDPOINT_ROOT,
STATE_LOW,
@@ -376,7 +376,7 @@ async def async_sync_device_config(self):
self.async_desired_settings_payload()
!= self.async_current_settings_payload()
):
- _LOGGER.info("pushing settings to device %s", self.device_id)
+ _LOGGER.info("Pushing settings to device %s", self.device_id)
await self.client.put_settings(**self.async_desired_settings_payload())
diff --git a/homeassistant/components/konnected/sensor.py b/homeassistant/components/konnected/sensor.py
index dece8d06c87097..18975bdb4679cb 100644
--- a/homeassistant/components/konnected/sensor.py
+++ b/homeassistant/components/konnected/sensor.py
@@ -1,4 +1,5 @@
"""Support for DHT and DS18B20 sensors attached to a Konnected device."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
CONF_DEVICES,
CONF_NAME,
@@ -12,7 +13,6 @@
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from .const import DOMAIN as KONNECTED_DOMAIN, SIGNAL_DS18B20_NEW
@@ -70,7 +70,7 @@ def async_add_ds18b20(attrs):
async_dispatcher_connect(hass, SIGNAL_DS18B20_NEW, async_add_ds18b20)
-class KonnectedSensor(Entity):
+class KonnectedSensor(SensorEntity):
"""Represents a Konnected DHT Sensor."""
def __init__(self, device_id, data, sensor_type, addr=None, initial_state=None):
diff --git a/homeassistant/components/konnected/strings.json b/homeassistant/components/konnected/strings.json
index 4fd62dee8e7c9d..e53838ad0d7db6 100644
--- a/homeassistant/components/konnected/strings.json
+++ b/homeassistant/components/konnected/strings.json
@@ -99,7 +99,7 @@
}
},
"error": {
- "bad_host": "Invalid Override API host url"
+ "bad_host": "Invalid Override API host URL"
},
"abort": {
"not_konn_panel": "Not a recognized Konnected.io device"
diff --git a/homeassistant/components/konnected/switch.py b/homeassistant/components/konnected/switch.py
index 1d26f7875c76cd..9c9f8193dcd6c6 100644
--- a/homeassistant/components/konnected/switch.py
+++ b/homeassistant/components/konnected/switch.py
@@ -5,16 +5,18 @@
ATTR_STATE,
CONF_DEVICES,
CONF_NAME,
+ CONF_REPEAT,
CONF_SWITCHES,
CONF_ZONE,
)
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import ToggleEntity
from .const import (
CONF_ACTIVATION,
CONF_MOMENTARY,
CONF_PAUSE,
- CONF_REPEAT,
DOMAIN as KONNECTED_DOMAIN,
STATE_HIGH,
STATE_LOW,
@@ -130,6 +132,16 @@ def _set_state(self, state):
state,
)
+ @callback
+ def async_set_state(self, state):
+ """Update the switch state."""
+ self._set_state(state)
+
async def async_added_to_hass(self):
- """Store entity_id."""
+ """Store entity_id and register state change callback."""
self._data["entity_id"] = self.entity_id
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass, f"konnected.{self.entity_id}.update", self.async_set_state
+ )
+ )
diff --git a/homeassistant/components/konnected/translations/de.json b/homeassistant/components/konnected/translations/de.json
index ad2ed659522fcb..2ec1657990b227 100644
--- a/homeassistant/components/konnected/translations/de.json
+++ b/homeassistant/components/konnected/translations/de.json
@@ -2,9 +2,9 @@
"config": {
"abort": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
- "already_in_progress": "Der Konfigurationsfluss f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.",
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
"not_konn_panel": "Kein anerkanntes Konnected.io-Ger\u00e4t",
- "unknown": "Unbekannter Fehler ist aufgetreten"
+ "unknown": "Unerwarteter Fehler"
},
"error": {
"cannot_connect": "Es konnte keine Verbindung zu einem Konnected-Panel unter {host}:{port} hergestellt werden."
@@ -43,7 +43,7 @@
"name": "Name (optional)",
"type": "Bin\u00e4rer Sensortyp"
},
- "description": "Bitte w\u00e4hlen Sie die Optionen f\u00fcr den an {zone} angeschlossenen Bin\u00e4rsensor",
+ "description": "Bitte w\u00e4hle die Optionen f\u00fcr den an {zone} angeschlossenen Bin\u00e4rsensor",
"title": "Konfigurieren Sie den Bin\u00e4rsensor"
},
"options_digital": {
@@ -52,7 +52,7 @@
"poll_interval": "Abfrageintervall (Minuten) (optional)",
"type": "Sensortyp"
},
- "description": "Bitte w\u00e4hlen Sie die Optionen f\u00fcr den an {zone} angeschlossenen digitalen Sensor aus",
+ "description": "Bitte w\u00e4hle die Optionen f\u00fcr den an {zone} angeschlossenen digitalen Sensor aus",
"title": "Konfigurieren Sie den digitalen Sensor"
},
"options_io": {
@@ -98,9 +98,9 @@
"more_states": "Konfigurieren Sie zus\u00e4tzliche Zust\u00e4nde f\u00fcr diese Zone",
"name": "Name (optional)",
"pause": "Pause zwischen Impulsen (ms) (optional)",
- "repeat": "Zeit zum Wiederholen (-1 = unendlich) (optional)"
+ "repeat": "Mal wiederholen (-1 = unendlich) (optional)"
},
- "description": "Bitte w\u00e4hlen Sie die Ausgabeoptionen f\u00fcr {zone} : Status {state}",
+ "description": "Bitte w\u00e4hlen die Ausgabeoptionen f\u00fcr {zone} : Status {state}",
"title": "Konfigurieren Sie den schaltbaren Ausgang"
}
}
diff --git a/homeassistant/components/konnected/translations/en.json b/homeassistant/components/konnected/translations/en.json
index 920e1453e499e5..32cf120e8af1f9 100644
--- a/homeassistant/components/konnected/translations/en.json
+++ b/homeassistant/components/konnected/translations/en.json
@@ -32,7 +32,7 @@
"not_konn_panel": "Not a recognized Konnected.io device"
},
"error": {
- "bad_host": "Invalid Override API host url"
+ "bad_host": "Invalid Override API host URL"
},
"step": {
"options_binary": {
diff --git a/homeassistant/components/konnected/translations/hu.json b/homeassistant/components/konnected/translations/hu.json
index 1cc44a02646be7..507e5d258f295e 100644
--- a/homeassistant/components/konnected/translations/hu.json
+++ b/homeassistant/components/konnected/translations/hu.json
@@ -1,5 +1,13 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
"step": {
"user": {
"data": {
@@ -11,6 +19,11 @@
},
"options": {
"step": {
+ "options_binary": {
+ "data": {
+ "name": "N\u00e9v (nem k\u00f6telez\u0151)"
+ }
+ },
"options_digital": {
"data": {
"name": "N\u00e9v (nem k\u00f6telez\u0151)",
diff --git a/homeassistant/components/konnected/translations/id.json b/homeassistant/components/konnected/translations/id.json
new file mode 100644
index 00000000000000..633e6bba2df496
--- /dev/null
+++ b/homeassistant/components/konnected/translations/id.json
@@ -0,0 +1,108 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "not_konn_panel": "Bukan perangkat Konnected.io yang dikenali",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "confirm": {
+ "description": "Model: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nAnda dapat mengonfigurasi IO dan perilaku panel di pengaturan Panel Alarm Konnected.",
+ "title": "Perangkat Konnected Siap"
+ },
+ "import_confirm": {
+ "description": "Panel Alarm Konnected dengan ID {id} telah ditemukan di configuration.yaml. Aliran ini akan memungkinkan Anda mengimpornya menjadi entri konfigurasi.",
+ "title": "Impor Perangkat Konnected"
+ },
+ "user": {
+ "data": {
+ "host": "Alamat IP",
+ "port": "Port"
+ },
+ "description": "Masukkan informasi host untuk Panel Konnected Anda."
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "not_konn_panel": "Bukan perangkat Konnected.io yang dikenali"
+ },
+ "error": {
+ "bad_host": "URL host API yang Menimpa Tidak Valid"
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "Balikkan status buka/tutup",
+ "name": "Nama (opsional)",
+ "type": "Jenis Sensor Biner"
+ },
+ "description": "Opsi {zone}",
+ "title": "Konfigurasikan Sensor Biner"
+ },
+ "options_digital": {
+ "data": {
+ "name": "Nama (opsional)",
+ "poll_interval": "Interval Polling (dalam menit) (opsional)",
+ "type": "Jenis Sensor"
+ },
+ "description": "Opsi {zone}",
+ "title": "Konfigurasi Sensor Digital"
+ },
+ "options_io": {
+ "data": {
+ "1": "Zona 1",
+ "2": "Zona 2",
+ "3": "Zona 3",
+ "4": "Zona 4",
+ "5": "Zona 5",
+ "6": "Zona 6",
+ "7": "Zona 7",
+ "out": "OUT"
+ },
+ "description": "Ditemukan {model} di {host}. Pilih konfigurasi dasar setiap I/O di bawah ini - tergantung pada I/O yang mungkin memungkinkan sensor biner (kontak terbuka/tutup), sensor digital (dht dan ds18b20), atau sakelar output. Anda dapat mengonfigurasi opsi terperinci dalam langkah berikutnya.",
+ "title": "Konfigurasikan I/O"
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "Zona 10",
+ "11": "Zona 11",
+ "12": "Zona 12",
+ "8": "Zona 8",
+ "9": "Zona 9",
+ "alarm1": "ALARM1",
+ "alarm2_out2": "OUT2/ALARM2",
+ "out1": "OUT1"
+ },
+ "description": "Pilih konfigurasi I/O lainnya di bawah ini. Anda dapat mengonfigurasi detail opsi pada langkah berikutnya.",
+ "title": "Konfigurasikan I/O yang Diperluas"
+ },
+ "options_misc": {
+ "data": {
+ "api_host": "Ganti URL host API (opsional)",
+ "blink": "Kedipkan panel LED saat mengirim perubahan status",
+ "discovery": "Tanggapi permintaan penemuan di jaringan Anda",
+ "override_api_host": "Timpa URL panel host API Home Assistant bawaan"
+ },
+ "description": "Pilih perilaku yang diinginkan untuk panel Anda",
+ "title": "Konfigurasikan Lainnya"
+ },
+ "options_switch": {
+ "data": {
+ "activation": "Keluaran saat nyala",
+ "momentary": "Durasi pulsa (milidetik) (opsional)",
+ "more_states": "Konfigurasikan status tambahan untuk zona ini",
+ "name": "Nama (opsional)",
+ "pause": "Jeda di antara pulsa (milidetik) (opsional)",
+ "repeat": "Waktu pengulangan (-1 = tak terbatas) (opsional)"
+ },
+ "description": "Opsi {zone}: status {state}",
+ "title": "Konfigurasikan Output yang Dapat Dialihkan"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/translations/it.json b/homeassistant/components/konnected/translations/it.json
index b618ee04b48e51..da88fb0ac4de7e 100644
--- a/homeassistant/components/konnected/translations/it.json
+++ b/homeassistant/components/konnected/translations/it.json
@@ -32,7 +32,7 @@
"not_konn_panel": "Non \u00e8 un dispositivo Konnected.io riconosciuto"
},
"error": {
- "bad_host": "URL dell'host API di sostituzione non valido"
+ "bad_host": "URL host API di sostituzione non valido"
},
"step": {
"options_binary": {
diff --git a/homeassistant/components/konnected/translations/ko.json b/homeassistant/components/konnected/translations/ko.json
index d8d2b70d909c09..eb7fa3de4e0171 100644
--- a/homeassistant/components/konnected/translations/ko.json
+++ b/homeassistant/components/konnected/translations/ko.json
@@ -2,12 +2,12 @@
"config": {
"abort": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
"not_konn_panel": "\uc778\uc2dd\ub41c Konnected.io \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4",
- "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "{host}:{port} \uc758 Konnected \ud328\ub110\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"confirm": {
@@ -15,7 +15,7 @@
"title": "Konnected \uae30\uae30 \uc900\ube44"
},
"import_confirm": {
- "description": "Konnected \uc54c\ub78c \ud328\ub110 ID {id} \uac00 configuration.yaml \uc5d0\uc11c \ubc1c\uacac\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc774 \uacfc\uc815\uc744 \ud1b5\ud574 \uad6c\uc131 \ud56d\ubaa9\uc73c\ub85c \uac00\uc838\uc62c \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "description": "Konnected \uc54c\ub78c \ud328\ub110 ID {id}\uac00 configuration.yaml\uc5d0\uc11c \ubc1c\uacac\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc774 \uacfc\uc815\uc744 \ud1b5\ud574 \uad6c\uc131 \ud56d\ubaa9\uc73c\ub85c \uac00\uc838\uc62c \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
"title": "Konnected \uae30\uae30 \uac00\uc838\uc624\uae30"
},
"user": {
@@ -38,7 +38,7 @@
"options_binary": {
"data": {
"inverse": "\uc5f4\ub9bc / \ub2eb\ud798 \uc0c1\ud0dc \ubc18\uc804",
- "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)",
+ "name": "\uc774\ub984 (\uc120\ud0dd\uc0ac\ud56d)",
"type": "\uc774\uc9c4 \uc13c\uc11c \uc720\ud615"
},
"description": "{zone} \uc635\uc158",
@@ -46,7 +46,7 @@
},
"options_digital": {
"data": {
- "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)",
+ "name": "\uc774\ub984 (\uc120\ud0dd\uc0ac\ud56d)",
"poll_interval": "\ud3f4\ub9c1 \uac04\uaca9 (\ubd84) (\uc120\ud0dd \uc0ac\ud56d)",
"type": "\uc13c\uc11c \uc720\ud615"
},
@@ -85,7 +85,7 @@
"data": {
"api_host": "API \ud638\uc2a4\ud2b8 URL \uc7ac\uc815\uc758 (\uc120\ud0dd \uc0ac\ud56d)",
"blink": "\uc0c1\ud0dc \ubcc0\uacbd\uc744 \ubcf4\ub0bc \ub54c \uae5c\ubc15\uc784 \ud328\ub110 LED \ub97c \ucf2d\ub2c8\ub2e4",
- "discovery": "\ub124\ud2b8\uc6cc\ud06c\uc758 \uac80\uc0c9 \uc694\uccad\uc5d0 \uc751\ub2f5",
+ "discovery": "\ub124\ud2b8\uc6cc\ud06c\uc758 \uac80\uc0c9 \uc694\uccad\uc5d0 \uc751\ub2f5\ud558\uae30",
"override_api_host": "\uae30\ubcf8 Home Assistant API \ud638\uc2a4\ud2b8 \ud328\ub110 URL \uc7ac\uc815\uc758"
},
"description": "\ud328\ub110\uc5d0 \uc6d0\ud558\ub294 \ub3d9\uc791\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694",
@@ -96,7 +96,7 @@
"activation": "\uc2a4\uc704\uce58\uac00 \ucf1c\uc9c8 \ub54c \ucd9c\ub825",
"momentary": "\ud384\uc2a4 \uc9c0\uc18d\uc2dc\uac04 (ms) (\uc120\ud0dd \uc0ac\ud56d)",
"more_states": "\uc774 \uad6c\uc5ed\uc5d0 \ub300\ud55c \ucd94\uac00 \uc0c1\ud0dc \uad6c\uc131",
- "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)",
+ "name": "\uc774\ub984 (\uc120\ud0dd\uc0ac\ud56d)",
"pause": "\ud384\uc2a4 \uac04 \uc77c\uc2dc\uc815\uc9c0 \uc2dc\uac04 (ms) (\uc120\ud0dd \uc0ac\ud56d)",
"repeat": "\ubc18\ubcf5 \uc2dc\uac04 (-1 = \ubb34\ud55c) (\uc120\ud0dd \uc0ac\ud56d)"
},
diff --git a/homeassistant/components/konnected/translations/nl.json b/homeassistant/components/konnected/translations/nl.json
index 9a7f20ac1e1379..0387dc8c7b047f 100644
--- a/homeassistant/components/konnected/translations/nl.json
+++ b/homeassistant/components/konnected/translations/nl.json
@@ -2,12 +2,12 @@
"config": {
"abort": {
"already_configured": "Apparaat is al geconfigureerd",
- "already_in_progress": "De configuratiestroom voor het apparaat wordt al uitgevoerd.",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
"not_konn_panel": "Geen herkend Konnected.io apparaat",
- "unknown": "Onbekende fout opgetreden"
+ "unknown": "Onverwachte fout"
},
"error": {
- "cannot_connect": "Kan geen verbinding maken met een Konnected Panel op {host} : {port}"
+ "cannot_connect": "Kan geen verbinding maken"
},
"step": {
"confirm": {
@@ -20,8 +20,8 @@
},
"user": {
"data": {
- "host": "IP-adres van Konnected apparaat",
- "port": "Konnected apparaat poort"
+ "host": "IP-adres",
+ "port": "Poort"
},
"description": "Voer de host-informatie in voor uw Konnected-paneel."
}
@@ -48,7 +48,7 @@
},
"options_digital": {
"data": {
- "name": "Naam (optioneel)",
+ "name": "Naam (optional)",
"poll_interval": "Poll interval (minuten) (optioneel)",
"type": "Type sensor"
},
@@ -87,6 +87,7 @@
"data": {
"api_host": "API host-URL overschrijven (optioneel)",
"blink": "Led knipperen bij het verzenden van statuswijziging",
+ "discovery": "Reageer op detectieverzoeken op uw netwerk",
"override_api_host": "Overschrijf standaard Home Assistant API hostpaneel-URL"
},
"description": "Selecteer het gewenste gedrag voor uw paneel",
diff --git a/homeassistant/components/konnected/translations/no.json b/homeassistant/components/konnected/translations/no.json
index 1a11c7c76d3869..47be4c20bf00ff 100644
--- a/homeassistant/components/konnected/translations/no.json
+++ b/homeassistant/components/konnected/translations/no.json
@@ -32,7 +32,7 @@
"not_konn_panel": "Ikke en anerkjent Konnected.io-enhet"
},
"error": {
- "bad_host": "Ugyldig overstyr API-vertsadresse"
+ "bad_host": "Url-adresse for ugyldig overstyring av API-vert"
},
"step": {
"options_binary": {
diff --git a/homeassistant/components/konnected/translations/pl.json b/homeassistant/components/konnected/translations/pl.json
index ee6c10cbdd8f22..f6e9a2dbfbc566 100644
--- a/homeassistant/components/konnected/translations/pl.json
+++ b/homeassistant/components/konnected/translations/pl.json
@@ -45,7 +45,7 @@
"name": "Nazwa (opcjonalnie)",
"type": "Typ sensora binarnego"
},
- "description": "Wybierz opcje dla sensora binarnego powi\u0105zanego ze {zone}",
+ "description": "Opcje {zone}",
"title": "Konfiguracja sensora binarnego"
},
"options_digital": {
@@ -54,7 +54,7 @@
"poll_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (minuty) (opcjonalnie)",
"type": "Typ sensora"
},
- "description": "Wybierz opcje dla cyfrowego sensora powi\u0105zanego ze {zone}",
+ "description": "Opcje {zone}",
"title": "Konfiguracja sensora cyfrowego"
},
"options_io": {
diff --git a/homeassistant/components/konnected/translations/ru.json b/homeassistant/components/konnected/translations/ru.json
index 931f0802dc0586..4357c924572e8e 100644
--- a/homeassistant/components/konnected/translations/ru.json
+++ b/homeassistant/components/konnected/translations/ru.json
@@ -32,7 +32,7 @@
"not_konn_panel": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Konnected.io \u043d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u043e."
},
"error": {
- "bad_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0445\u043e\u0441\u0442\u0430 API."
+ "bad_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 Override API."
},
"step": {
"options_binary": {
diff --git a/homeassistant/components/konnected/translations/tr.json b/homeassistant/components/konnected/translations/tr.json
new file mode 100644
index 00000000000000..a0e759903bdea8
--- /dev/null
+++ b/homeassistant/components/konnected/translations/tr.json
@@ -0,0 +1,60 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor",
+ "unknown": "Beklenmeyen hata"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0130p Adresi",
+ "port": "Port"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "bad_host": "Ge\u00e7ersiz, Ge\u00e7ersiz K\u0131lma API ana makine url'si"
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "A\u00e7\u0131k / kapal\u0131 durumunu tersine \u00e7evirin"
+ }
+ },
+ "options_io": {
+ "data": {
+ "3": "B\u00f6lge 3",
+ "4": "B\u00f6lge 4",
+ "5": "B\u00f6lge 5",
+ "6": "B\u00f6lge 6",
+ "7": "B\u00f6lge 7",
+ "out": "OUT"
+ }
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "B\u00f6lge 10",
+ "11": "B\u00f6lge 11",
+ "12": "B\u00f6lge 12",
+ "8": "B\u00f6lge 8",
+ "9": "B\u00f6lge 9",
+ "alarm1": "ALARM1",
+ "alarm2_out2": "OUT2/ALARM2",
+ "out1": "OUT1"
+ }
+ },
+ "options_misc": {
+ "data": {
+ "api_host": "API ana makine URL'sini ge\u00e7ersiz k\u0131l (iste\u011fe ba\u011fl\u0131)",
+ "override_api_host": "Varsay\u0131lan Home Assistant API ana bilgisayar paneli URL'sini ge\u00e7ersiz k\u0131l"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/translations/uk.json b/homeassistant/components/konnected/translations/uk.json
new file mode 100644
index 00000000000000..92cd3744d945c2
--- /dev/null
+++ b/homeassistant/components/konnected/translations/uk.json
@@ -0,0 +1,108 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "not_konn_panel": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Konnected.io \u043d\u0435 \u0440\u043e\u0437\u043f\u0456\u0437\u043d\u0430\u043d\u043e.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "confirm": {
+ "description": "\u041c\u043e\u0434\u0435\u043b\u044c: {model}\nID: {id}\n\u0425\u043e\u0441\u0442: {host}\n\u041f\u043e\u0440\u0442: {port} \n\n\u0417\u043c\u0456\u043d\u0430 \u043b\u043e\u0433\u0456\u043a\u0438 \u0440\u043e\u0431\u043e\u0442\u0438 \u043f\u0430\u043d\u0435\u043b\u0456, \u0430 \u0442\u0430\u043a\u043e\u0436 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u0432\u0445\u043e\u0434\u0456\u0432 \u0456 \u0432\u0438\u0445\u043e\u0434\u0456\u0432 \u0432\u0438\u043a\u043e\u043d\u0443\u0454\u0442\u044c\u0441\u044f \u0432 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u0445 \u043f\u0430\u043d\u0435\u043b\u0456 \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Konnected.",
+ "title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Konnected \u0433\u043e\u0442\u043e\u0432\u0456\u0439 \u0434\u043e \u0440\u043e\u0431\u043e\u0442\u0438."
+ },
+ "import_confirm": {
+ "description": "\u041f\u0430\u043d\u0435\u043b\u044c \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Konnected ID {id} \u0440\u0430\u043d\u0456\u0448\u0435 \u0432\u0436\u0435 \u0431\u0443\u043b\u0430 \u0434\u043e\u0434\u0430\u043d\u0430 \u0447\u0435\u0440\u0435\u0437 configuration.yaml. \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0456\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0446\u0435\u0439 \u0437\u0430\u043f\u0438\u0441 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0434\u0430\u043d\u043e\u0433\u043e \u043f\u043e\u0441\u0456\u0431\u043d\u0438\u043a\u0430 \u0437 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f.",
+ "title": "\u0406\u043c\u043f\u043e\u0440\u0442 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Konnected"
+ },
+ "user": {
+ "data": {
+ "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0430\u043d\u0435\u043b\u0456 Konnected."
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "not_konn_panel": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Konnected.io \u043d\u0435 \u0440\u043e\u0437\u043f\u0456\u0437\u043d\u0430\u043d\u043e."
+ },
+ "error": {
+ "bad_host": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 URL \u043f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0445\u043e\u0441\u0442\u0430 API."
+ },
+ "step": {
+ "options_binary": {
+ "data": {
+ "inverse": "\u0406\u043d\u0432\u0435\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u0438\u0439/\u0437\u0430\u043a\u0440\u0438\u0442\u0438\u0439 \u0441\u0442\u0430\u043d",
+ "name": "\u041d\u0430\u0437\u0432\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)",
+ "type": "\u0422\u0438\u043f \u0431\u0456\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430"
+ },
+ "description": "\u041e\u043f\u0446\u0456\u0457 {zone}",
+ "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0431\u0456\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430"
+ },
+ "options_digital": {
+ "data": {
+ "name": "\u041d\u0430\u0437\u0432\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)",
+ "poll_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432 \u0445\u0432\u0438\u043b\u0438\u043d\u0430\u0445 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)",
+ "type": "\u0422\u0438\u043f \u0441\u0435\u043d\u0441\u043e\u0440\u0430"
+ },
+ "description": "\u041e\u043f\u0446\u0456\u0457 {zone}",
+ "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430"
+ },
+ "options_io": {
+ "data": {
+ "1": "\u0417\u043e\u043d\u0430 1",
+ "2": "\u0417\u043e\u043d\u0430 2",
+ "3": "\u0417\u043e\u043d\u0430 3",
+ "4": "\u0417\u043e\u043d\u0430 4",
+ "5": "\u0417\u043e\u043d\u0430 5",
+ "6": "\u0417\u043e\u043d\u0430 6",
+ "7": "\u0417\u043e\u043d\u0430 7",
+ "out": "\u0412\u0418\u0425\u0406\u0414"
+ },
+ "description": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 {model} \u0437 \u0430\u0434\u0440\u0435\u0441\u043e\u044e {host}. \u0417\u0430\u043b\u0435\u0436\u043d\u043e \u0432\u0456\u0434 \u043e\u0431\u0440\u0430\u043d\u043e\u0457 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457 \u0432\u0445\u043e\u0434\u0456\u0432 / \u0432\u0438\u0445\u043e\u0434\u0456\u0432, \u0434\u043e \u043f\u0430\u043d\u0435\u043b\u0456 \u043c\u043e\u0436\u0443\u0442\u044c \u0431\u0443\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0456 \u0431\u0456\u043d\u0430\u0440\u043d\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 (\u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u0442\u044f / \u0437\u0430\u043a\u0440\u0438\u0442\u0442\u044f), \u0446\u0438\u0444\u0440\u043e\u0432\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 (dht \u0456 ds18b20) \u0430\u0431\u043e \u043f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u044e\u0447\u0456 \u0432\u0438\u0445\u043e\u0434\u0438. \u0411\u0456\u043b\u044c\u0448 \u0434\u0435\u0442\u0430\u043b\u044c\u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0431\u0443\u0434\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0435 \u043d\u0430 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u043a\u0440\u043e\u043a\u0430\u0445.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0445\u043e\u0434\u0456\u0432 / \u0432\u0438\u0445\u043e\u0434\u0456\u0432"
+ },
+ "options_io_ext": {
+ "data": {
+ "10": "\u0417\u043e\u043d\u0430 10",
+ "11": "\u0417\u043e\u043d\u0430 11",
+ "12": "\u0417\u043e\u043d\u0430 12",
+ "8": "\u0417\u043e\u043d\u0430 8",
+ "9": "\u0417\u043e\u043d\u0430 9",
+ "alarm1": "\u0422\u0420\u0418\u0412\u041e\u0413\u04101",
+ "alarm2_out2": "\u0412\u0418\u0425\u0406\u04142 / \u0422\u0420\u0418\u0412\u041e\u0413\u04102",
+ "out1": "\u0412\u0418\u0425\u0406\u04141"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0440\u0435\u0448\u0442\u0438 \u0432\u0445\u043e\u0434\u0456\u0432 / \u0432\u0438\u0445\u043e\u0434\u0456\u0432. \u0411\u0456\u043b\u044c\u0448 \u0434\u0435\u0442\u0430\u043b\u044c\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0431\u0443\u0434\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u043d\u0430 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u043a\u0440\u043e\u043a\u0430\u0445.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0438\u0445 \u0432\u0445\u043e\u0434\u0456\u0432 / \u0432\u0438\u0445\u043e\u0434\u0456\u0432"
+ },
+ "options_misc": {
+ "data": {
+ "api_host": "\u041f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 URL \u0445\u043e\u0441\u0442\u0430 API (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)",
+ "blink": "LED-\u0456\u043d\u0434\u0438\u043a\u0430\u0446\u0456\u044f \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 \u043f\u0440\u0438 \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u0446\u0456 \u0441\u0442\u0430\u043d\u0443",
+ "discovery": "\u0412\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0442\u0438 \u043d\u0430 \u0437\u0430\u043f\u0438\u0442\u0438 \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u0443 \u0412\u0430\u0448\u0456\u0439 \u043c\u0435\u0440\u0435\u0436\u0456",
+ "override_api_host": "\u041f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 URL-\u0430\u0434\u0440\u0435\u0441\u0443 \u0445\u043e\u0441\u0442-\u043f\u0430\u043d\u0435\u043b\u0456 Home Assistant API"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0431\u0430\u0436\u0430\u043d\u0443 \u043f\u043e\u0432\u0435\u0434\u0456\u043d\u043a\u0443 \u0434\u043b\u044f \u0412\u0430\u0448\u043e\u0457 \u043f\u0430\u043d\u0435\u043b\u0456.",
+ "title": "\u0406\u043d\u0448\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f"
+ },
+ "options_switch": {
+ "data": {
+ "activation": "\u0412\u0438\u0445\u0456\u0434 \u043f\u0440\u0438 \u0432\u043c\u0438\u043a\u0430\u043d\u043d\u0456",
+ "momentary": "\u0422\u0440\u0438\u0432\u0430\u043b\u0456\u0441\u0442\u044c \u0456\u043c\u043f\u0443\u043b\u044c\u0441\u0443 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)",
+ "more_states": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u0441\u0442\u0430\u043d\u0438 \u0434\u043b\u044f \u0446\u0456\u0454\u0457 \u0437\u043e\u043d\u0438",
+ "name": "\u041d\u0430\u0437\u0432\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)",
+ "pause": "\u041f\u0430\u0443\u0437\u0430 \u043c\u0456\u0436 \u0456\u043c\u043f\u0443\u043b\u044c\u0441\u0430\u043c\u0438 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)",
+ "repeat": "\u041a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u044c (-1 = \u043d\u0435\u0441\u043a\u0456\u043d\u0447\u0435\u043d\u043d\u043e) (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)"
+ },
+ "description": "{zone}: \u0441\u0442\u0430\u043d {state}",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u044e\u0447\u043e\u0433\u043e \u0432\u0438\u0445\u043e\u0434\u0443"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kostal_plenticore/__init__.py b/homeassistant/components/kostal_plenticore/__init__.py
new file mode 100644
index 00000000000000..f06657fdaa1625
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/__init__.py
@@ -0,0 +1,60 @@
+"""The Kostal Plenticore Solar Inverter integration."""
+import asyncio
+import logging
+
+from kostal.plenticore import PlenticoreApiException
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+
+from .const import DOMAIN
+from .helper import Plenticore
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORMS = ["sensor"]
+
+
+async def async_setup(hass: HomeAssistant, config: dict) -> bool:
+ """Set up the Kostal Plenticore Solar Inverter component."""
+ hass.data.setdefault(DOMAIN, {})
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Set up Kostal Plenticore Solar Inverter from a config entry."""
+
+ plenticore = Plenticore(hass, entry)
+
+ if not await plenticore.async_setup():
+ return False
+
+ hass.data[DOMAIN][entry.entry_id] = plenticore
+
+ for component in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, component)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, component)
+ for component in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ # remove API object
+ plenticore = hass.data[DOMAIN].pop(entry.entry_id)
+ try:
+ await plenticore.async_unload()
+ except PlenticoreApiException as err:
+ _LOGGER.error("Error logging out from inverter: %s", err)
+
+ return unload_ok
diff --git a/homeassistant/components/kostal_plenticore/config_flow.py b/homeassistant/components/kostal_plenticore/config_flow.py
new file mode 100644
index 00000000000000..d70115a499f8a0
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/config_flow.py
@@ -0,0 +1,78 @@
+"""Config flow for Kostal Plenticore Solar Inverter integration."""
+import asyncio
+import logging
+
+from aiohttp.client_exceptions import ClientError
+from kostal.plenticore import PlenticoreApiClient, PlenticoreAuthenticationException
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_BASE, CONF_HOST, CONF_PASSWORD
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+DATA_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_HOST): str,
+ vol.Required(CONF_PASSWORD): str,
+ }
+)
+
+
+@callback
+def configured_instances(hass):
+ """Return a set of configured Kostal Plenticore HOSTS."""
+ return {
+ entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN)
+ }
+
+
+async def test_connection(hass: HomeAssistant, data) -> str:
+ """Test the connection to the inverter.
+
+ Data has the keys from DATA_SCHEMA with values provided by the user.
+ """
+
+ session = async_get_clientsession(hass)
+ async with PlenticoreApiClient(session, data["host"]) as client:
+ await client.login(data["password"])
+ values = await client.get_setting_values("scb:network", "Hostname")
+
+ return values["scb:network"]["Hostname"]
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Kostal Plenticore Solar Inverter."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ errors = {}
+ hostname = None
+
+ if user_input is not None:
+ if user_input[CONF_HOST] in configured_instances(self.hass):
+ return self.async_abort(reason="already_configured")
+ try:
+ hostname = await test_connection(self.hass, user_input)
+ except PlenticoreAuthenticationException as ex:
+ errors[CONF_PASSWORD] = "invalid_auth"
+ _LOGGER.error("Error response: %s", ex)
+ except (ClientError, asyncio.TimeoutError):
+ errors[CONF_HOST] = "cannot_connect"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors[CONF_BASE] = "unknown"
+
+ if not errors:
+ return self.async_create_entry(title=hostname, data=user_input)
+
+ return self.async_show_form(
+ step_id="user", data_schema=DATA_SCHEMA, errors=errors
+ )
diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py
new file mode 100644
index 00000000000000..8342ff74adaf13
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/const.py
@@ -0,0 +1,521 @@
+"""Constants for the Kostal Plenticore Solar Inverter integration."""
+
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ ATTR_ICON,
+ ATTR_UNIT_OF_MEASUREMENT,
+ DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_ENERGY,
+ DEVICE_CLASS_POWER,
+ 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: DEVICE_CLASS_POWER,
+ ATTR_ENABLED_DEFAULT: True,
+ },
+ "format_round",
+ ),
+ (
+ "devices:local",
+ "Grid_P",
+ "Grid Power",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER,
+ ATTR_ENABLED_DEFAULT: True,
+ },
+ "format_round",
+ ),
+ (
+ "devices:local",
+ "HomeBat_P",
+ "Home Power from Battery",
+ {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER},
+ "format_round",
+ ),
+ (
+ "devices:local",
+ "HomeGrid_P",
+ "Home Power from Grid",
+ {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER},
+ "format_round",
+ ),
+ (
+ "devices:local",
+ "HomeOwn_P",
+ "Home Power from Own",
+ {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER},
+ "format_round",
+ ),
+ (
+ "devices:local",
+ "HomePv_P",
+ "Home Power from PV",
+ {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER},
+ "format_round",
+ ),
+ (
+ "devices:local",
+ "Home_P",
+ "Home Power",
+ {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER},
+ "format_round",
+ ),
+ (
+ "devices:local:ac",
+ "P",
+ "AC Power",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER,
+ ATTR_ENABLED_DEFAULT: True,
+ },
+ "format_round",
+ ),
+ (
+ "devices:local:pv1",
+ "P",
+ "DC1 Power",
+ {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER},
+ "format_round",
+ ),
+ (
+ "devices:local:pv2",
+ "P",
+ "DC2 Power",
+ {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER},
+ "format_round",
+ ),
+ (
+ "devices:local",
+ "PV2Bat_P",
+ "PV to Battery Power",
+ {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER},
+ "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"},
+ "format_round",
+ ),
+ (
+ "devices:local:battery",
+ "P",
+ "Battery Power",
+ {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER},
+ "format_round",
+ ),
+ (
+ "devices:local:battery",
+ "SoC",
+ "Battery SoC",
+ {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: DEVICE_CLASS_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"},
+ "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"},
+ "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: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+ (
+ "scb:statistic:EnergyFlow",
+ "Statistic:EnergyHome:Month",
+ "Home Consumption Month",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+ (
+ "scb:statistic:EnergyFlow",
+ "Statistic:EnergyHome:Year",
+ "Home Consumption Year",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+ (
+ "scb:statistic:EnergyFlow",
+ "Statistic:EnergyHome:Total",
+ "Home Consumption Total",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+ (
+ "scb:statistic:EnergyFlow",
+ "Statistic:EnergyHomeBat:Day",
+ "Home Consumption from Battery Day",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_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: DEVICE_CLASS_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: DEVICE_CLASS_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: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+ (
+ "scb:statistic:EnergyFlow",
+ "Statistic:EnergyHomeGrid:Day",
+ "Home Consumption from Grid Day",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_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: DEVICE_CLASS_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: DEVICE_CLASS_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: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+ (
+ "scb:statistic:EnergyFlow",
+ "Statistic:EnergyHomePv:Day",
+ "Home Consumption from PV Day",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_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: DEVICE_CLASS_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: DEVICE_CLASS_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: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+ (
+ "scb:statistic:EnergyFlow",
+ "Statistic:EnergyPv1:Day",
+ "Energy PV1 Day",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+ (
+ "scb:statistic:EnergyFlow",
+ "Statistic:EnergyPv1:Month",
+ "Energy PV1 Month",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+ (
+ "scb:statistic:EnergyFlow",
+ "Statistic:EnergyPv1:Year",
+ "Energy PV1 Year",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+ (
+ "scb:statistic:EnergyFlow",
+ "Statistic:EnergyPv1:Total",
+ "Energy PV1 Total",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+ (
+ "scb:statistic:EnergyFlow",
+ "Statistic:EnergyPv2:Day",
+ "Energy PV2 Day",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+ (
+ "scb:statistic:EnergyFlow",
+ "Statistic:EnergyPv2:Month",
+ "Energy PV2 Month",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+ (
+ "scb:statistic:EnergyFlow",
+ "Statistic:EnergyPv2:Year",
+ "Energy PV2 Year",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+ (
+ "scb:statistic:EnergyFlow",
+ "Statistic:EnergyPv2:Total",
+ "Energy PV2 Total",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+ (
+ "scb:statistic:EnergyFlow",
+ "Statistic:Yield:Day",
+ "Energy Yield Day",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_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: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+ (
+ "scb:statistic:EnergyFlow",
+ "Statistic:Yield:Year",
+ "Energy Yield Year",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+ (
+ "scb:statistic:EnergyFlow",
+ "Statistic:Yield:Total",
+ "Energy Yield Total",
+ {
+ ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
+ "format_energy",
+ ),
+]
+
+# Defines all entities for settings.
+#
+# 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_SETTINGS_DATA = [
+ (
+ "devices:local",
+ "Battery:MinHomeComsumption",
+ "Battery min Home Consumption",
+ {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER},
+ "format_round",
+ ),
+ (
+ "devices:local",
+ "Battery:MinSoc",
+ "Battery min Soc",
+ {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:battery-negative"},
+ "format_round",
+ ),
+ (
+ "devices:local",
+ "Battery:Strategy",
+ "Battery Strategy",
+ {},
+ "format_round",
+ ),
+]
diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py
new file mode 100644
index 00000000000000..6f9cc4f5ee0a7d
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/helper.py
@@ -0,0 +1,259 @@
+"""Code to handle the Plenticore API."""
+import asyncio
+from collections import defaultdict
+from datetime import datetime, timedelta
+import logging
+from typing import Dict, Union
+
+from aiohttp.client_exceptions import ClientError
+from kostal.plenticore import PlenticoreApiClient, PlenticoreAuthenticationException
+
+from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.event import async_call_later
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class Plenticore:
+ """Manages the Plenticore API."""
+
+ def __init__(self, hass, config_entry):
+ """Create a new plenticore manager instance."""
+ self.hass = hass
+ self.config_entry = config_entry
+
+ self._client = None
+ self._shutdown_remove_listener = None
+
+ self.device_info = {}
+
+ @property
+ def host(self) -> str:
+ """Return the host of the Plenticore inverter."""
+ return self.config_entry.data[CONF_HOST]
+
+ @property
+ def client(self) -> PlenticoreApiClient:
+ """Return the Plenticore API client."""
+ return self._client
+
+ async def async_setup(self) -> bool:
+ """Set up Plenticore API client."""
+ self._client = PlenticoreApiClient(
+ async_get_clientsession(self.hass), host=self.host
+ )
+ try:
+ await self._client.login(self.config_entry.data[CONF_PASSWORD])
+ except PlenticoreAuthenticationException as err:
+ _LOGGER.error(
+ "Authentication exception connecting to %s: %s", self.host, err
+ )
+ return False
+ except (ClientError, asyncio.TimeoutError) as err:
+ _LOGGER.error("Error connecting to %s", self.host)
+ raise ConfigEntryNotReady from err
+ else:
+ _LOGGER.debug("Log-in successfully to %s", self.host)
+
+ self._shutdown_remove_listener = self.hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_STOP, self._async_shutdown
+ )
+
+ # get some device meta data
+ settings = await self._client.get_setting_values(
+ {
+ "devices:local": [
+ "Properties:SerialNo",
+ "Branding:ProductName1",
+ "Branding:ProductName2",
+ "Properties:VersionIOC",
+ "Properties:VersionMC",
+ ],
+ "scb:network": ["Hostname"],
+ }
+ )
+
+ device_local = settings["devices:local"]
+ prod1 = device_local["Branding:ProductName1"]
+ prod2 = device_local["Branding:ProductName2"]
+
+ self.device_info = {
+ "identifiers": {(DOMAIN, device_local["Properties:SerialNo"])},
+ "manufacturer": "Kostal",
+ "model": f"{prod1} {prod2}",
+ "name": settings["scb:network"]["Hostname"],
+ "sw_version": f'IOC: {device_local["Properties:VersionIOC"]}'
+ + f' MC: {device_local["Properties:VersionMC"]}',
+ }
+
+ return True
+
+ async def _async_shutdown(self, event):
+ """Call from Homeassistant shutdown event."""
+ # unset remove listener otherwise calling it would raise an exception
+ self._shutdown_remove_listener = None
+ await self.async_unload()
+
+ async def async_unload(self) -> None:
+ """Unload the Plenticore API client."""
+ if self._shutdown_remove_listener:
+ self._shutdown_remove_listener()
+
+ await self._client.logout()
+ self._client = None
+ _LOGGER.debug("Logged out from %s", self.host)
+
+
+class PlenticoreUpdateCoordinator(DataUpdateCoordinator):
+ """Base implementation of DataUpdateCoordinator for Plenticore data."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ logger: logging.Logger,
+ name: str,
+ update_inverval: timedelta,
+ plenticore: Plenticore,
+ ):
+ """Create a new update coordinator for plenticore data."""
+ super().__init__(
+ hass=hass,
+ logger=logger,
+ name=name,
+ update_interval=update_inverval,
+ )
+ # data ids to poll
+ self._fetch = defaultdict(list)
+ self._plenticore = plenticore
+
+ def start_fetch_data(self, module_id: str, data_id: str) -> None:
+ """Start fetching the given data (module-id and data-id)."""
+ self._fetch[module_id].append(data_id)
+
+ # Force an update of all data. Multiple refresh calls
+ # are ignored by the debouncer.
+ async def force_refresh(event_time: datetime) -> None:
+ await self.async_request_refresh()
+
+ async_call_later(self.hass, 2, force_refresh)
+
+ def stop_fetch_data(self, module_id: str, data_id: str) -> None:
+ """Stop fetching the given data (module-id and data-id)."""
+ self._fetch[module_id].remove(data_id)
+
+
+class ProcessDataUpdateCoordinator(PlenticoreUpdateCoordinator):
+ """Implementation of PlenticoreUpdateCoordinator for process data."""
+
+ async def _async_update_data(self) -> Dict[str, Dict[str, str]]:
+ client = self._plenticore.client
+
+ if not self._fetch or client is None:
+ return {}
+
+ _LOGGER.debug("Fetching %s for %s", self.name, self._fetch)
+
+ fetched_data = await client.get_process_data_values(self._fetch)
+ return {
+ module_id: {
+ process_data.id: process_data.value
+ for process_data in fetched_data[module_id]
+ }
+ for module_id in fetched_data
+ }
+
+
+class SettingDataUpdateCoordinator(PlenticoreUpdateCoordinator):
+ """Implementation of PlenticoreUpdateCoordinator for settings data."""
+
+ async def _async_update_data(self) -> Dict[str, Dict[str, str]]:
+ client = self._plenticore.client
+
+ if not self._fetch or client is None:
+ return {}
+
+ _LOGGER.debug("Fetching %s for %s", self.name, self._fetch)
+
+ fetched_data = await client.get_setting_values(self._fetch)
+
+ return fetched_data
+
+
+class PlenticoreDataFormatter:
+ """Provides method to format values of process or settings data."""
+
+ INVERTER_STATES = {
+ 0: "Off",
+ 1: "Init",
+ 2: "IsoMEas",
+ 3: "GridCheck",
+ 4: "StartUp",
+ 6: "FeedIn",
+ 7: "Throttled",
+ 8: "ExtSwitchOff",
+ 9: "Update",
+ 10: "Standby",
+ 11: "GridSync",
+ 12: "GridPreCheck",
+ 13: "GridSwitchOff",
+ 14: "Overheating",
+ 15: "Shutdown",
+ 16: "ImproperDcVoltage",
+ 17: "ESB",
+ }
+
+ EM_STATES = {
+ 0: "Idle",
+ 1: "n/a",
+ 2: "Emergency Battery Charge",
+ 4: "n/a",
+ 8: "Winter Mode Step 1",
+ 16: "Winter Mode Step 2",
+ }
+
+ @classmethod
+ def get_method(cls, name: str) -> callable:
+ """Return a callable formatter of the given name."""
+ return getattr(cls, name)
+
+ @staticmethod
+ def format_round(state: str) -> Union[int, str]:
+ """Return the given state value as rounded integer."""
+ try:
+ return round(float(state))
+ except (TypeError, ValueError):
+ return state
+
+ @staticmethod
+ def format_energy(state: str) -> Union[float, str]:
+ """Return the given state value as energy value, scaled to kWh."""
+ try:
+ return round(float(state) / 1000, 1)
+ except (TypeError, ValueError):
+ return state
+
+ @staticmethod
+ def format_inverter_state(state: str) -> str:
+ """Return a readable string of the inverter state."""
+ try:
+ value = int(state)
+ except (TypeError, ValueError):
+ return state
+
+ return PlenticoreDataFormatter.INVERTER_STATES.get(value)
+
+ @staticmethod
+ def format_em_manager_state(state: str) -> str:
+ """Return a readable state of the energy manager."""
+ try:
+ value = int(state)
+ except (TypeError, ValueError):
+ return state
+
+ return PlenticoreDataFormatter.EM_STATES.get(value)
diff --git a/homeassistant/components/kostal_plenticore/manifest.json b/homeassistant/components/kostal_plenticore/manifest.json
new file mode 100644
index 00000000000000..427c730833cf49
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/manifest.json
@@ -0,0 +1,10 @@
+{
+ "domain": "kostal_plenticore",
+ "name": "Kostal Plenticore Solar Inverter",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/kostal_plenticore",
+ "requirements": ["kostal_plenticore==0.2.0"],
+ "codeowners": [
+ "@stegm"
+ ]
+}
\ No newline at end of file
diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py
new file mode 100644
index 00000000000000..82b06c96a77312
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/sensor.py
@@ -0,0 +1,193 @@
+"""Platform for Kostal Plenticore sensors."""
+from datetime import timedelta
+import logging
+from typing import Any, Callable, Dict, Optional
+
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import (
+ ATTR_ENABLED_DEFAULT,
+ DOMAIN,
+ SENSOR_PROCESS_DATA,
+ SENSOR_SETTINGS_DATA,
+)
+from .helper import (
+ PlenticoreDataFormatter,
+ ProcessDataUpdateCoordinator,
+ SettingDataUpdateCoordinator,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities
+):
+ """Add kostal plenticore Sensors."""
+ plenticore = hass.data[DOMAIN][entry.entry_id]
+
+ entities = []
+
+ available_process_data = await plenticore.client.get_process_data()
+ process_data_update_coordinator = ProcessDataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ "Process Data",
+ timedelta(seconds=10),
+ plenticore,
+ )
+ for module_id, data_id, name, sensor_data, fmt in SENSOR_PROCESS_DATA:
+ if (
+ module_id not in available_process_data
+ or data_id not in available_process_data[module_id]
+ ):
+ _LOGGER.debug(
+ "Skipping non existing process data %s/%s", module_id, data_id
+ )
+ continue
+
+ entities.append(
+ PlenticoreDataSensor(
+ process_data_update_coordinator,
+ entry.entry_id,
+ entry.title,
+ module_id,
+ data_id,
+ name,
+ sensor_data,
+ PlenticoreDataFormatter.get_method(fmt),
+ plenticore.device_info,
+ )
+ )
+
+ available_settings_data = await plenticore.client.get_settings()
+ settings_data_update_coordinator = SettingDataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ "Settings Data",
+ timedelta(seconds=300),
+ plenticore,
+ )
+ for module_id, data_id, name, sensor_data, fmt in SENSOR_SETTINGS_DATA:
+ if module_id not in available_settings_data or data_id not in (
+ setting.id for setting in available_settings_data[module_id]
+ ):
+ _LOGGER.debug(
+ "Skipping non existing setting data %s/%s", module_id, data_id
+ )
+ continue
+
+ entities.append(
+ PlenticoreDataSensor(
+ settings_data_update_coordinator,
+ entry.entry_id,
+ entry.title,
+ module_id,
+ data_id,
+ name,
+ sensor_data,
+ PlenticoreDataFormatter.get_method(fmt),
+ plenticore.device_info,
+ )
+ )
+
+ async_add_entities(entities)
+
+
+class PlenticoreDataSensor(CoordinatorEntity, SensorEntity):
+ """Representation of a Plenticore data Sensor."""
+
+ def __init__(
+ self,
+ coordinator,
+ 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: Dict[str, Any],
+ ):
+ """Create a new Sensor Entity for Plenticore process data."""
+ super().__init__(coordinator)
+ self.entry_id = entry_id
+ self.platform_name = platform_name
+ self.module_id = module_id
+ self.data_id = data_id
+
+ self._sensor_name = sensor_name
+ self._sensor_data = sensor_data
+ self._formatter = formatter
+
+ self._device_info = device_info
+
+ @property
+ def available(self) -> bool:
+ """Return if entity is available."""
+ return (
+ super().available
+ and self.coordinator.data is not None
+ and self.module_id in self.coordinator.data
+ and self.data_id in self.coordinator.data[self.module_id]
+ )
+
+ 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)
+
+ 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)
+ await super().async_will_remove_from_hass()
+
+ @property
+ def device_info(self) -> Dict[str, Any]:
+ """Return the device info."""
+ return self._device_info
+
+ @property
+ def unique_id(self) -> str:
+ """Return the unique id of this Sensor Entity."""
+ return f"{self.entry_id}_{self.module_id}_{self.data_id}"
+
+ @property
+ def name(self) -> str:
+ """Return the name of this Sensor Entity."""
+ return f"{self.platform_name} {self._sensor_name}"
+
+ @property
+ def unit_of_measurement(self) -> Optional[str]:
+ """Return the unit of this Sensor Entity or None."""
+ return self._sensor_data.get(ATTR_UNIT_OF_MEASUREMENT)
+
+ @property
+ def icon(self) -> Optional[str]:
+ """Return the icon name of this Sensor Entity or None."""
+ return self._sensor_data.get(ATTR_ICON)
+
+ @property
+ def device_class(self) -> Optional[str]:
+ """Return the class of this device, from component DEVICE_CLASSES."""
+ return self._sensor_data.get(ATTR_DEVICE_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 state(self) -> Optional[Any]:
+ """Return the state of the sensor."""
+ if self.coordinator.data is None:
+ # None is translated to STATE_UNKNOWN
+ return None
+
+ raw_value = self.coordinator.data[self.module_id][self.data_id]
+
+ return self._formatter(raw_value) if self._formatter else raw_value
diff --git a/homeassistant/components/kostal_plenticore/strings.json b/homeassistant/components/kostal_plenticore/strings.json
new file mode 100644
index 00000000000000..771c3ada744bf2
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/strings.json
@@ -0,0 +1,21 @@
+{
+ "title": "Kostal Plenticore Solar Inverter",
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "[%key:common::config_flow::data::host%]",
+ "password": "[%key:common::config_flow::data::password%]"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kostal_plenticore/translations/ca.json b/homeassistant/components/kostal_plenticore/translations/ca.json
new file mode 100644
index 00000000000000..2ce39d904a6ab7
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/translations/ca.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "password": "Contrasenya"
+ }
+ }
+ }
+ },
+ "title": "Inversor solar Kostal Plenticore"
+}
\ No newline at end of file
diff --git a/homeassistant/components/kostal_plenticore/translations/de.json b/homeassistant/components/kostal_plenticore/translations/de.json
new file mode 100644
index 00000000000000..095487fff3f326
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/translations/de.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Passwort"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kostal_plenticore/translations/en.json b/homeassistant/components/kostal_plenticore/translations/en.json
new file mode 100644
index 00000000000000..a058336b077f1b
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/translations/en.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Password"
+ }
+ }
+ }
+ },
+ "title": "Kostal Plenticore Solar Inverter"
+}
\ No newline at end of file
diff --git a/homeassistant/components/kostal_plenticore/translations/et.json b/homeassistant/components/kostal_plenticore/translations/et.json
new file mode 100644
index 00000000000000..c96935d5db86e6
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/translations/et.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_auth": "Tuvastamise viga",
+ "unknown": "Tundmatu viga"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Salas\u00f5na"
+ }
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/kostal_plenticore/translations/it.json b/homeassistant/components/kostal_plenticore/translations/it.json
new file mode 100644
index 00000000000000..8e46b765fe041f
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/translations/it.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Password"
+ }
+ }
+ }
+ },
+ "title": "Inverter solare Kostal Plenticore"
+}
\ No newline at end of file
diff --git a/homeassistant/components/kostal_plenticore/translations/ko.json b/homeassistant/components/kostal_plenticore/translations/ko.json
new file mode 100644
index 00000000000000..98a520d9444cb7
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/translations/ko.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "password": "\ube44\ubc00\ubc88\ud638"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kostal_plenticore/translations/nl.json b/homeassistant/components/kostal_plenticore/translations/nl.json
new file mode 100644
index 00000000000000..83a77fb6e0d80f
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/translations/nl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Wachtwoord"
+ }
+ }
+ }
+ },
+ "title": "Kostal Plenticore omvormer voor zonne-energie"
+}
\ No newline at end of file
diff --git a/homeassistant/components/kostal_plenticore/translations/no.json b/homeassistant/components/kostal_plenticore/translations/no.json
new file mode 100644
index 00000000000000..0f0d77a83e6656
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/translations/no.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_auth": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Vert",
+ "password": "Passord"
+ }
+ }
+ }
+ },
+ "title": "Kostal Plenticore Solar Inverter"
+}
\ No newline at end of file
diff --git a/homeassistant/components/kostal_plenticore/translations/pl.json b/homeassistant/components/kostal_plenticore/translations/pl.json
new file mode 100644
index 00000000000000..781bddfc979ae3
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/translations/pl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP",
+ "password": "Has\u0142o"
+ }
+ }
+ }
+ },
+ "title": "Inwerter solarny Kostal Plenticore"
+}
\ No newline at end of file
diff --git a/homeassistant/components/kostal_plenticore/translations/ru.json b/homeassistant/components/kostal_plenticore/translations/ru.json
new file mode 100644
index 00000000000000..d272fd0f304d22
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/translations/ru.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ }
+ }
+ }
+ },
+ "title": "Kostal Plenticore Solar Inverter"
+}
\ No newline at end of file
diff --git a/homeassistant/components/kostal_plenticore/translations/zh-Hant.json b/homeassistant/components/kostal_plenticore/translations/zh-Hant.json
new file mode 100644
index 00000000000000..b1fef7a7143162
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/translations/zh-Hant.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "password": "\u5bc6\u78bc"
+ }
+ }
+ }
+ },
+ "title": "Kostal Plenticore \u592a\u967d\u80fd\u63db\u6d41\u5668"
+}
\ No newline at end of file
diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py
index 9459d44805c0a2..951e2a5353f3f3 100644
--- a/homeassistant/components/kulersky/__init__.py
+++ b/homeassistant/components/kulersky/__init__.py
@@ -4,7 +4,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
-from .const import DOMAIN
+from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN
PLATFORMS = ["light"]
@@ -16,9 +16,14 @@ async def async_setup(hass: HomeAssistant, config: dict):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Kuler Sky from a config entry."""
- for component in PLATFORMS:
+ if DOMAIN not in hass.data:
+ hass.data[DOMAIN] = {}
+ if DATA_ADDRESSES not in hass.data[DOMAIN]:
+ hass.data[DOMAIN][DATA_ADDRESSES] = set()
+
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -26,15 +31,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
- unload_ok = all(
+ # Stop discovery
+ unregister_discovery = hass.data[DOMAIN].pop(DATA_DISCOVERY_SUBSCRIPTION, None)
+ if unregister_discovery:
+ unregister_discovery()
+
+ hass.data.pop(DOMAIN, None)
+
+ return all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
- if unload_ok:
- hass.data[DOMAIN].pop(entry.entry_id)
-
- return unload_ok
diff --git a/homeassistant/components/kulersky/config_flow.py b/homeassistant/components/kulersky/config_flow.py
index 04f7719b8e6d92..2a11a3c2e178e9 100644
--- a/homeassistant/components/kulersky/config_flow.py
+++ b/homeassistant/components/kulersky/config_flow.py
@@ -15,9 +15,7 @@ async def _async_has_devices(hass) -> bool:
"""Return if there are devices that can be discovered."""
# Check if there are any devices that can be discovered in the network.
try:
- devices = await hass.async_add_executor_job(
- pykulersky.discover_bluetooth_devices
- )
+ devices = await pykulersky.discover()
except pykulersky.PykulerskyException as exc:
_LOGGER.error("Unable to discover nearby Kuler Sky devices: %s", exc)
return False
diff --git a/homeassistant/components/kulersky/const.py b/homeassistant/components/kulersky/const.py
index ae1e7a435dca99..8b314d7bde94d3 100644
--- a/homeassistant/components/kulersky/const.py
+++ b/homeassistant/components/kulersky/const.py
@@ -1,2 +1,5 @@
"""Constants for the Kuler Sky integration."""
DOMAIN = "kulersky"
+
+DATA_ADDRESSES = "addresses"
+DATA_DISCOVERY_SUBSCRIPTION = "discovery_subscription"
diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py
index 71dd4a158ca134..980d4612ce9aa2 100644
--- a/homeassistant/components/kulersky/light.py
+++ b/homeassistant/components/kulersky/light.py
@@ -1,8 +1,9 @@
"""Kuler Sky light platform."""
-import asyncio
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Callable, List
+from typing import Callable
import pykulersky
@@ -22,7 +23,7 @@
from homeassistant.helpers.typing import HomeAssistantType
import homeassistant.util.color as color_util
-from .const import DOMAIN
+from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -30,68 +31,39 @@
DISCOVERY_INTERVAL = timedelta(seconds=60)
-PARALLEL_UPDATES = 0
-
-
-def check_light(light: pykulersky.Light):
- """Attempt to connect to this light and read the color."""
- light.connect()
- light.get_color()
-
async def async_setup_entry(
hass: HomeAssistantType,
config_entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up Kuler sky light devices."""
- if DOMAIN not in hass.data:
- hass.data[DOMAIN] = {}
- if "devices" not in hass.data[DOMAIN]:
- hass.data[DOMAIN]["devices"] = set()
- if "discovery" not in hass.data[DOMAIN]:
- hass.data[DOMAIN]["discovery"] = asyncio.Lock()
async def discover(*args):
"""Attempt to discover new lights."""
- # Since discovery needs to connect to all discovered bluetooth devices, and
- # only rules out devices after a timeout, it can potentially take a long
- # time. If there's already a discovery running, just skip this poll.
- if hass.data[DOMAIN]["discovery"].locked():
- return
+ lights = await pykulersky.discover()
- async with hass.data[DOMAIN]["discovery"]:
- bluetooth_devices = await hass.async_add_executor_job(
- pykulersky.discover_bluetooth_devices
- )
+ # Filter out already discovered lights
+ new_lights = [
+ light
+ for light in lights
+ if light.address not in hass.data[DOMAIN][DATA_ADDRESSES]
+ ]
+
+ new_entities = []
+ for light in new_lights:
+ hass.data[DOMAIN][DATA_ADDRESSES].add(light.address)
+ new_entities.append(KulerskyLight(light))
- # Filter out already connected lights
- new_devices = [
- device
- for device in bluetooth_devices
- if device["address"] not in hass.data[DOMAIN]["devices"]
- ]
-
- for device in new_devices:
- light = pykulersky.Light(device["address"], device["name"])
- try:
- # If the connection fails, either this is not a Kuler Sky
- # light, or it's bluetooth connection is currently locked
- # by another device. If the vendor's app is connected to
- # the light when home assistant tries to connect, this
- # connection will fail.
- await hass.async_add_executor_job(check_light, light)
- except pykulersky.PykulerskyException:
- continue
- # The light has successfully connected
- hass.data[DOMAIN]["devices"].add(device["address"])
- async_add_entities([KulerskyLight(light)], update_before_add=True)
+ async_add_entities(new_entities, update_before_add=True)
# Start initial discovery
hass.async_create_task(discover())
# Perform recurring discovery of new devices
- async_track_time_interval(hass, discover, DISCOVERY_INTERVAL)
+ hass.data[DOMAIN][DATA_DISCOVERY_SUBSCRIPTION] = async_track_time_interval(
+ hass, discover, DISCOVERY_INTERVAL
+ )
class KulerskyLight(LightEntity):
@@ -103,21 +75,24 @@ def __init__(self, light: pykulersky.Light):
self._hs_color = None
self._brightness = None
self._white_value = None
- self._available = True
+ self._available = None
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
self.async_on_remove(
- self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.disconnect)
+ self.hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_STOP, self.async_will_remove_from_hass
+ )
)
- async def async_will_remove_from_hass(self) -> None:
+ async def async_will_remove_from_hass(self, *args) -> None:
"""Run when entity will be removed from hass."""
- await self.hass.async_add_executor_job(self.disconnect)
-
- def disconnect(self, *args) -> None:
- """Disconnect the underlying device."""
- self._light.disconnect()
+ try:
+ await self._light.disconnect()
+ except pykulersky.PykulerskyException:
+ _LOGGER.debug(
+ "Exception disconnected from %s", self._light.address, exc_info=True
+ )
@property
def name(self):
@@ -168,7 +143,7 @@ def available(self) -> bool:
"""Return True if entity is available."""
return self._available
- def turn_on(self, **kwargs):
+ async def async_turn_on(self, **kwargs):
"""Instruct the light to turn on."""
default_hs = (0, 0) if self._hs_color is None else self._hs_color
hue_sat = kwargs.get(ATTR_HS_COLOR, default_hs)
@@ -187,28 +162,28 @@ def turn_on(self, **kwargs):
rgb = color_util.color_hsv_to_RGB(*hue_sat, brightness / 255 * 100)
- self._light.set_color(*rgb, white_value)
+ await self._light.set_color(*rgb, white_value)
- def turn_off(self, **kwargs):
+ async def async_turn_off(self, **kwargs):
"""Instruct the light to turn off."""
- self._light.set_color(0, 0, 0, 0)
+ await self._light.set_color(0, 0, 0, 0)
- def update(self):
+ async def async_update(self):
"""Fetch new state data for this light."""
try:
- if not self._light.connected:
- self._light.connect()
+ if not self._available:
+ await self._light.connect()
# pylint: disable=invalid-name
- r, g, b, w = self._light.get_color()
+ r, g, b, w = await self._light.get_color()
except pykulersky.PykulerskyException as exc:
if self._available:
_LOGGER.warning("Unable to connect to %s: %s", self._light.address, exc)
self._available = False
return
- if not self._available:
- _LOGGER.info("Reconnected to %s", self.entity_id)
- self._available = True
+ if self._available is False:
+ _LOGGER.info("Reconnected to %s", self._light.address)
+ self._available = True
hsv = color_util.color_RGB_to_hsv(r, g, b)
self._hs_color = hsv[:2]
self._brightness = int(round((hsv[2] / 100) * 255))
diff --git a/homeassistant/components/kulersky/manifest.json b/homeassistant/components/kulersky/manifest.json
index 4f445e4fc18caa..b690d94e8d4a5b 100644
--- a/homeassistant/components/kulersky/manifest.json
+++ b/homeassistant/components/kulersky/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/kulersky",
"requirements": [
- "pykulersky==0.4.0"
+ "pykulersky==0.5.2"
],
"codeowners": [
"@emlove"
diff --git a/homeassistant/components/kulersky/translations/de.json b/homeassistant/components/kulersky/translations/de.json
index 3fc69f85947a70..86bc8e3673075f 100644
--- a/homeassistant/components/kulersky/translations/de.json
+++ b/homeassistant/components/kulersky/translations/de.json
@@ -6,7 +6,7 @@
},
"step": {
"confirm": {
- "description": "Wollen Sie mit der Einrichtung beginnen?"
+ "description": "M\u00f6chten Sie mit der Einrichtung beginnen?"
}
}
}
diff --git a/homeassistant/components/kulersky/translations/fr.json b/homeassistant/components/kulersky/translations/fr.json
index 4c984a556904b5..42f356ac365fdb 100644
--- a/homeassistant/components/kulersky/translations/fr.json
+++ b/homeassistant/components/kulersky/translations/fr.json
@@ -1,7 +1,13 @@
{
"config": {
"abort": {
- "no_devices_found": "Aucun appareil n'a \u00e9t\u00e9 d\u00e9tect\u00e9 sur le r\u00e9seau"
+ "no_devices_found": "Aucun appareil n'a \u00e9t\u00e9 d\u00e9tect\u00e9 sur le r\u00e9seau",
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Seulement une seule configuration est possible "
+ },
+ "step": {
+ "confirm": {
+ "description": "Voulez-vous commencer la configuration ?"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/kulersky/translations/hu.json b/homeassistant/components/kulersky/translations/hu.json
index 3d5be90042efea..6c61530acbebb9 100644
--- a/homeassistant/components/kulersky/translations/hu.json
+++ b/homeassistant/components/kulersky/translations/hu.json
@@ -1,8 +1,12 @@
{
"config": {
+ "abort": {
+ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
"step": {
"confirm": {
- "description": "El akarod kezdeni a be\u00e1ll\u00edt\u00e1st?"
+ "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?"
}
}
}
diff --git a/homeassistant/components/kulersky/translations/id.json b/homeassistant/components/kulersky/translations/id.json
new file mode 100644
index 00000000000000..223836a8b40992
--- /dev/null
+++ b/homeassistant/components/kulersky/translations/id.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "step": {
+ "confirm": {
+ "description": "Ingin memulai penyiapan?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kulersky/translations/ko.json b/homeassistant/components/kulersky/translations/ko.json
new file mode 100644
index 00000000000000..e5ae04d6e5c810
--- /dev/null
+++ b/homeassistant/components/kulersky/translations/ko.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "step": {
+ "confirm": {
+ "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kulersky/translations/lb.json b/homeassistant/components/kulersky/translations/lb.json
new file mode 100644
index 00000000000000..4ea09574c0ba8f
--- /dev/null
+++ b/homeassistant/components/kulersky/translations/lb.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Keng Apparater am Netzwierk fonnt",
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech."
+ },
+ "step": {
+ "confirm": {
+ "description": "Soll den Ariichtungs Prozess gestart ginn?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kulersky/translations/nl.json b/homeassistant/components/kulersky/translations/nl.json
new file mode 100644
index 00000000000000..d11896014fd2c6
--- /dev/null
+++ b/homeassistant/components/kulersky/translations/nl.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Geen apparaten gevonden op het netwerk",
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
+ },
+ "step": {
+ "confirm": {
+ "description": "Wil je beginnen met instellen?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kulersky/translations/tr.json b/homeassistant/components/kulersky/translations/tr.json
index 49fa9545e94d2c..3df15466f030f1 100644
--- a/homeassistant/components/kulersky/translations/tr.json
+++ b/homeassistant/components/kulersky/translations/tr.json
@@ -1,7 +1,13 @@
{
"config": {
"abort": {
- "no_devices_found": "A\u011fda cihaz bulunamad\u0131"
+ "no_devices_found": "A\u011fda cihaz bulunamad\u0131",
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "step": {
+ "confirm": {
+ "description": "Kuruluma ba\u015flamak ister misiniz?"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/kulersky/translations/uk.json b/homeassistant/components/kulersky/translations/uk.json
new file mode 100644
index 00000000000000..292861e9129dbd
--- /dev/null
+++ b/homeassistant/components/kulersky/translations/uk.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kwb/sensor.py b/homeassistant/components/kwb/sensor.py
index bd0430f9786d40..eb96b20665342b 100644
--- a/homeassistant/components/kwb/sensor.py
+++ b/homeassistant/components/kwb/sensor.py
@@ -2,7 +2,7 @@
from pykwb import kwb
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_DEVICE,
CONF_HOST,
@@ -11,7 +11,6 @@
EVENT_HOMEASSISTANT_STOP,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
DEFAULT_RAW = False
DEFAULT_NAME = "KWB"
@@ -74,7 +73,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors)
-class KWBSensor(Entity):
+class KWBSensor(SensorEntity):
"""Representation of a KWB Easyfire sensor."""
def __init__(self, easyfire, sensor, client_name):
diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py
index 2c7f5d294a989f..32090797f1103f 100644
--- a/homeassistant/components/lacrosse/sensor.py
+++ b/homeassistant/components/lacrosse/sensor.py
@@ -6,7 +6,11 @@
from serial import SerialException
import voluptuous as vol
-from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA
+from homeassistant.components.sensor import (
+ ENTITY_ID_FORMAT,
+ PLATFORM_SCHEMA,
+ SensorEntity,
+)
from homeassistant.const import (
CONF_DEVICE,
CONF_ID,
@@ -19,7 +23,7 @@
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity, async_generate_entity_id
+from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util import dt as dt_util
@@ -78,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_LOGGER.warning("Unable to open serial port: %s", exc)
return False
- hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lacrosse.close)
+ hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: lacrosse.close())
if CONF_JEELINK_LED in config:
lacrosse.led_mode_state(config.get(CONF_JEELINK_LED))
@@ -108,7 +112,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors)
-class LaCrosseSensor(Entity):
+class LaCrosseSensor(SensorEntity):
"""Implementation of a Lacrosse sensor."""
_temperature = None
@@ -138,7 +142,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attributes = {
"low_battery": self._low_battery,
diff --git a/homeassistant/components/lastfm/manifest.json b/homeassistant/components/lastfm/manifest.json
index 9fe3a182844be1..e732b5d7000937 100644
--- a/homeassistant/components/lastfm/manifest.json
+++ b/homeassistant/components/lastfm/manifest.json
@@ -2,6 +2,6 @@
"domain": "lastfm",
"name": "Last.fm",
"documentation": "https://www.home-assistant.io/integrations/lastfm",
- "requirements": ["pylast==4.1.0"],
+ "requirements": ["pylast==4.2.0"],
"codeowners": []
}
diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py
index 56124e2c0fee0a..128450826d6fef 100644
--- a/homeassistant/components/lastfm/sensor.py
+++ b/homeassistant/components/lastfm/sensor.py
@@ -7,10 +7,9 @@
from pylast import WSError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -52,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(entities, True)
-class LastfmSensor(Entity):
+class LastfmSensor(SensorEntity):
"""A class for the Last.fm account."""
def __init__(self, user, lastfm_api):
@@ -107,7 +106,7 @@ def update(self):
self._state = f"{now_playing.artist} - {now_playing.title}"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/launch_library/sensor.py b/homeassistant/components/launch_library/sensor.py
index ef816eef0ba431..831e44dca8f978 100644
--- a/homeassistant/components/launch_library/sensor.py
+++ b/homeassistant/components/launch_library/sensor.py
@@ -1,16 +1,16 @@
"""A sensor platform that give you information about the next space launch."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Optional
from pylaunches import PyLaunches, PyLaunchesException
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from .const import (
ATTR_AGENCY,
@@ -39,7 +39,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([LaunchLibrarySensor(launches, name)], True)
-class LaunchLibrarySensor(Entity):
+class LaunchLibrarySensor(SensorEntity):
"""Representation of a launch_library Sensor."""
def __init__(self, launches: PyLaunches, name: str) -> None:
@@ -64,7 +64,7 @@ def name(self) -> str:
return self._name
@property
- def state(self) -> Optional[str]:
+ def state(self) -> str | None:
"""Return the state of the sensor."""
if self.next_launch:
return self.next_launch.name
@@ -76,7 +76,7 @@ def icon(self) -> str:
return "mdi:rocket"
@property
- def device_state_attributes(self) -> Optional[dict]:
+ def extra_state_attributes(self) -> dict | None:
"""Return attributes for the sensor."""
if self.next_launch:
return {
diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py
index cc1e47d71fc0ed..9384fbed29d3ee 100644
--- a/homeassistant/components/lcn/__init__.py
+++ b/homeassistant/components/lcn/__init__.py
@@ -1,137 +1,162 @@
"""Support for LCN devices."""
+import asyncio
import logging
import pypck
+from homeassistant import config_entries
from homeassistant.const import (
- CONF_BINARY_SENSORS,
- CONF_COVERS,
- CONF_HOST,
- CONF_LIGHTS,
+ CONF_IP_ADDRESS,
CONF_NAME,
CONF_PASSWORD,
CONF_PORT,
- CONF_SENSORS,
- CONF_SWITCHES,
+ CONF_RESOURCE,
CONF_USERNAME,
)
-from homeassistant.helpers.discovery import async_load_platform
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity import Entity
-from .const import (
- CONF_CLIMATES,
- CONF_CONNECTIONS,
- CONF_DIM_MODE,
- CONF_SCENES,
- CONF_SK_NUM_TRIES,
- DATA_LCN,
- DOMAIN,
-)
-from .schemas import CONFIG_SCHEMA # noqa: 401
-from .services import (
- DynText,
- Led,
- LockKeys,
- LockRegulator,
- OutputAbs,
- OutputRel,
- OutputToggle,
- Pck,
- Relays,
- SendKeys,
- VarAbs,
- VarRel,
- VarReset,
-)
+from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, CONNECTION, DOMAIN
+from .helpers import generate_unique_id, import_lcn_config
+from .schemas import CONFIG_SCHEMA # noqa: F401
+from .services import SERVICES
+
+PLATFORMS = ["binary_sensor", "climate", "cover", "light", "scene", "sensor", "switch"]
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config):
"""Set up the LCN component."""
- hass.data[DATA_LCN] = {}
-
- conf_connections = config[DOMAIN][CONF_CONNECTIONS]
- connections = []
- for conf_connection in conf_connections:
- connection_name = conf_connection.get(CONF_NAME)
-
- settings = {
- "SK_NUM_TRIES": conf_connection[CONF_SK_NUM_TRIES],
- "DIM_MODE": pypck.lcn_defs.OutputPortDimMode[
- conf_connection[CONF_DIM_MODE]
- ],
- }
-
- connection = pypck.connection.PchkConnectionManager(
- conf_connection[CONF_HOST],
- conf_connection[CONF_PORT],
- conf_connection[CONF_USERNAME],
- conf_connection[CONF_PASSWORD],
- settings=settings,
- connection_id=connection_name,
+ if DOMAIN not in config:
+ return True
+
+ # initialize a config_flow for all LCN configurations read from
+ # configuration.yaml
+ config_entries_data = import_lcn_config(config[DOMAIN])
+
+ for config_entry_data in config_entries_data:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data=config_entry_data,
+ )
)
+ return True
- try:
- # establish connection to PCHK server
- await hass.async_create_task(connection.async_connect(timeout=15))
- connections.append(connection)
- _LOGGER.info('LCN connected to "%s"', connection_name)
- except TimeoutError:
- _LOGGER.error('Connection to PCHK server "%s" failed', connection_name)
- return False
-
- hass.data[DATA_LCN][CONF_CONNECTIONS] = connections
-
- # load platforms
- for component, conf_key in (
- ("binary_sensor", CONF_BINARY_SENSORS),
- ("climate", CONF_CLIMATES),
- ("cover", CONF_COVERS),
- ("light", CONF_LIGHTS),
- ("scene", CONF_SCENES),
- ("sensor", CONF_SENSORS),
- ("switch", CONF_SWITCHES),
- ):
- if conf_key in config[DOMAIN]:
- hass.async_create_task(
- async_load_platform(
- hass, component, DOMAIN, config[DOMAIN][conf_key], config
- )
- )
- # register service calls
- for service_name, service in (
- ("output_abs", OutputAbs),
- ("output_rel", OutputRel),
- ("output_toggle", OutputToggle),
- ("relays", Relays),
- ("var_abs", VarAbs),
- ("var_reset", VarReset),
- ("var_rel", VarRel),
- ("lock_regulator", LockRegulator),
- ("led", Led),
- ("send_keys", SendKeys),
- ("lock_keys", LockKeys),
- ("dyn_text", DynText),
- ("pck", Pck),
- ):
- hass.services.async_register(
- DOMAIN, service_name, service(hass).async_call_service, service.schema
+async def async_setup_entry(hass, config_entry):
+ """Set up a connection to PCHK host from a config entry."""
+ hass.data.setdefault(DOMAIN, {})
+ if config_entry.entry_id in hass.data[DOMAIN]:
+ return False
+
+ settings = {
+ "SK_NUM_TRIES": config_entry.data[CONF_SK_NUM_TRIES],
+ "DIM_MODE": pypck.lcn_defs.OutputPortDimMode[config_entry.data[CONF_DIM_MODE]],
+ }
+
+ # connect to PCHK
+ lcn_connection = pypck.connection.PchkConnectionManager(
+ config_entry.data[CONF_IP_ADDRESS],
+ config_entry.data[CONF_PORT],
+ config_entry.data[CONF_USERNAME],
+ config_entry.data[CONF_PASSWORD],
+ settings=settings,
+ connection_id=config_entry.entry_id,
+ )
+ try:
+ # establish connection to PCHK server
+ await lcn_connection.async_connect(timeout=15)
+ except pypck.connection.PchkAuthenticationError:
+ _LOGGER.warning('Authentication on PCHK "%s" failed', config_entry.title)
+ return False
+ except pypck.connection.PchkLicenseError:
+ _LOGGER.warning(
+ 'Maximum number of connections on PCHK "%s" was '
+ "reached. An additional license key is required",
+ config_entry.title,
)
+ return False
+ except TimeoutError:
+ _LOGGER.warning('Connection to PCHK "%s" failed', config_entry.title)
+ return False
+
+ _LOGGER.debug('LCN connected to "%s"', config_entry.title)
+ hass.data[DOMAIN][config_entry.entry_id] = {
+ CONNECTION: lcn_connection,
+ }
+
+ # remove orphans from entity registry which are in ConfigEntry but were removed
+ # from configuration.yaml
+ if config_entry.source == config_entries.SOURCE_IMPORT:
+ entity_registry = await er.async_get_registry(hass)
+ entity_registry.async_clear_config_entry(config_entry.entry_id)
+
+ # forward config_entry to components
+ for component in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, component)
+ )
+
+ # register service calls
+ for service_name, service in SERVICES:
+ if not hass.services.has_service(DOMAIN, service_name):
+ hass.services.async_register(
+ DOMAIN, service_name, service(hass).async_call_service, service.schema
+ )
return True
+async def async_unload_entry(hass, config_entry):
+ """Close connection to PCHK host represented by config_entry."""
+ # forward unloading to platforms
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(config_entry, component)
+ for component in PLATFORMS
+ ]
+ )
+ )
+
+ if unload_ok and config_entry.entry_id in hass.data[DOMAIN]:
+ host = hass.data[DOMAIN].pop(config_entry.entry_id)
+ await host[CONNECTION].async_close()
+
+ # unregister service calls
+ if unload_ok and not hass.data[DOMAIN]: # check if this is the last entry to unload
+ for service_name, _ in SERVICES:
+ hass.services.async_remove(DOMAIN, service_name)
+
+ return unload_ok
+
+
class LcnEntity(Entity):
- """Parent class for all devices associated with the LCN component."""
+ """Parent class for all entities associated with the LCN component."""
- def __init__(self, config, device_connection):
+ def __init__(self, config, entry_id, device_connection):
"""Initialize the LCN device."""
self.config = config
+ self.entry_id = entry_id
self.device_connection = device_connection
+ self._unregister_for_inputs = None
self._name = config[CONF_NAME]
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ unique_device_id = generate_unique_id(
+ (
+ self.device_connection.seg_id,
+ self.device_connection.addr_id,
+ self.device_connection.is_group,
+ )
+ )
+ return f"{self.entry_id}-{unique_device_id}-{self.config[CONF_RESOURCE]}"
+
@property
def should_poll(self):
"""Lcn device entity pushes its state to HA."""
@@ -140,7 +165,14 @@ def should_poll(self):
async def async_added_to_hass(self):
"""Run when entity about to be added to hass."""
if not self.device_connection.is_group:
- self.device_connection.register_for_inputs(self.input_received)
+ self._unregister_for_inputs = self.device_connection.register_for_inputs(
+ self.input_received
+ )
+
+ async def async_will_remove_from_hass(self):
+ """Run when entity will be removed from hass."""
+ if self._unregister_for_inputs is not None:
+ self._unregister_for_inputs()
@property
def name(self):
diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py
index 415668f5924b6b..3bea502cc76063 100644
--- a/homeassistant/components/lcn/binary_sensor.py
+++ b/homeassistant/components/lcn/binary_sensor.py
@@ -1,49 +1,56 @@
"""Support for LCN binary sensors."""
import pypck
-from homeassistant.components.binary_sensor import BinarySensorEntity
-from homeassistant.const import CONF_ADDRESS
+from homeassistant.components.binary_sensor import (
+ DOMAIN as DOMAIN_BINARY_SENSOR,
+ BinarySensorEntity,
+)
+from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES, CONF_SOURCE
from . import LcnEntity
-from .const import BINSENSOR_PORTS, CONF_CONNECTIONS, CONF_SOURCE, DATA_LCN, SETPOINTS
-from .helpers import get_connection
-
-
-async def async_setup_platform(
- hass, hass_config, async_add_entities, discovery_info=None
-):
- """Set up the LCN binary sensor platform."""
- if discovery_info is None:
- return
-
- devices = []
- for config in discovery_info:
- address, connection_id = config[CONF_ADDRESS]
- addr = pypck.lcn_addr.LcnAddr(*address)
- connections = hass.data[DATA_LCN][CONF_CONNECTIONS]
- connection = get_connection(connections, connection_id)
- address_connection = connection.get_address_conn(addr)
-
- if config[CONF_SOURCE] in SETPOINTS:
- device = LcnRegulatorLockSensor(config, address_connection)
- elif config[CONF_SOURCE] in BINSENSOR_PORTS:
- device = LcnBinarySensor(config, address_connection)
- else: # in KEYS
- device = LcnLockKeysSensor(config, address_connection)
-
- devices.append(device)
+from .const import BINSENSOR_PORTS, CONF_DOMAIN_DATA, SETPOINTS
+from .helpers import get_device_connection
+
+
+def create_lcn_binary_sensor_entity(hass, entity_config, config_entry):
+ """Set up an entity for this domain."""
+ device_connection = get_device_connection(
+ hass, tuple(entity_config[CONF_ADDRESS]), config_entry
+ )
+
+ if entity_config[CONF_DOMAIN_DATA][CONF_SOURCE] in SETPOINTS:
+ return LcnRegulatorLockSensor(
+ entity_config, config_entry.entry_id, device_connection
+ )
+ if entity_config[CONF_DOMAIN_DATA][CONF_SOURCE] in BINSENSOR_PORTS:
+ return LcnBinarySensor(entity_config, config_entry.entry_id, device_connection)
+ # in KEY
+ return LcnLockKeysSensor(entity_config, config_entry.entry_id, device_connection)
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up LCN switch entities from a config entry."""
+ entities = []
+
+ for entity_config in config_entry.data[CONF_ENTITIES]:
+ if entity_config[CONF_DOMAIN] == DOMAIN_BINARY_SENSOR:
+ entities.append(
+ create_lcn_binary_sensor_entity(hass, entity_config, config_entry)
+ )
- async_add_entities(devices)
+ async_add_entities(entities)
class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity):
"""Representation of a LCN binary sensor for regulator locks."""
- def __init__(self, config, device_connection):
+ def __init__(self, config, entry_id, device_connection):
"""Initialize the LCN binary sensor."""
- super().__init__(config, device_connection)
+ super().__init__(config, entry_id, device_connection)
- self.setpoint_variable = pypck.lcn_defs.Var[config[CONF_SOURCE]]
+ self.setpoint_variable = pypck.lcn_defs.Var[
+ config[CONF_DOMAIN_DATA][CONF_SOURCE]
+ ]
self._value = None
@@ -55,6 +62,14 @@ async def async_added_to_hass(self):
self.setpoint_variable
)
+ async def async_will_remove_from_hass(self):
+ """Run when entity will be removed from hass."""
+ await super().async_will_remove_from_hass()
+ if not self.device_connection.is_group:
+ await self.device_connection.cancel_status_request_handler(
+ self.setpoint_variable
+ )
+
@property
def is_on(self):
"""Return true if the binary sensor is on."""
@@ -75,11 +90,13 @@ def input_received(self, input_obj):
class LcnBinarySensor(LcnEntity, BinarySensorEntity):
"""Representation of a LCN binary sensor for binary sensor ports."""
- def __init__(self, config, device_connection):
+ def __init__(self, config, entry_id, device_connection):
"""Initialize the LCN binary sensor."""
- super().__init__(config, device_connection)
+ super().__init__(config, entry_id, device_connection)
- self.bin_sensor_port = pypck.lcn_defs.BinSensorPort[config[CONF_SOURCE]]
+ self.bin_sensor_port = pypck.lcn_defs.BinSensorPort[
+ config[CONF_DOMAIN_DATA][CONF_SOURCE]
+ ]
self._value = None
@@ -91,6 +108,14 @@ async def async_added_to_hass(self):
self.bin_sensor_port
)
+ async def async_will_remove_from_hass(self):
+ """Run when entity will be removed from hass."""
+ await super().async_will_remove_from_hass()
+ if not self.device_connection.is_group:
+ await self.device_connection.cancel_status_request_handler(
+ self.bin_sensor_port
+ )
+
@property
def is_on(self):
"""Return true if the binary sensor is on."""
@@ -108,11 +133,11 @@ def input_received(self, input_obj):
class LcnLockKeysSensor(LcnEntity, BinarySensorEntity):
"""Representation of a LCN sensor for key locks."""
- def __init__(self, config, device_connection):
+ def __init__(self, config, entry_id, device_connection):
"""Initialize the LCN sensor."""
- super().__init__(config, device_connection)
+ super().__init__(config, entry_id, device_connection)
- self.source = pypck.lcn_defs.Key[config[CONF_SOURCE]]
+ self.source = pypck.lcn_defs.Key[config[CONF_DOMAIN_DATA][CONF_SOURCE]]
self._value = None
async def async_added_to_hass(self):
@@ -121,6 +146,12 @@ async def async_added_to_hass(self):
if not self.device_connection.is_group:
await self.device_connection.activate_status_request_handler(self.source)
+ async def async_will_remove_from_hass(self):
+ """Run when entity will be removed from hass."""
+ await super().async_will_remove_from_hass()
+ if not self.device_connection.is_group:
+ await self.device_connection.cancel_status_request_handler(self.source)
+
@property
def is_on(self):
"""Return true if the binary sensor is on."""
diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py
index ece3994f651300..056abcda2b0f3a 100644
--- a/homeassistant/components/lcn/climate.py
+++ b/homeassistant/components/lcn/climate.py
@@ -1,64 +1,76 @@
"""Support for LCN climate control."""
-
import pypck
-from homeassistant.components.climate import ClimateEntity, const
-from homeassistant.const import ATTR_TEMPERATURE, CONF_ADDRESS, CONF_UNIT_OF_MEASUREMENT
+from homeassistant.components.climate import (
+ DOMAIN as DOMAIN_CLIMATE,
+ ClimateEntity,
+ const,
+)
+from homeassistant.const import (
+ ATTR_TEMPERATURE,
+ CONF_ADDRESS,
+ CONF_DOMAIN,
+ CONF_ENTITIES,
+ CONF_SOURCE,
+ CONF_UNIT_OF_MEASUREMENT,
+)
from . import LcnEntity
from .const import (
- CONF_CONNECTIONS,
+ CONF_DOMAIN_DATA,
CONF_LOCKABLE,
CONF_MAX_TEMP,
CONF_MIN_TEMP,
CONF_SETPOINT,
- CONF_SOURCE,
- DATA_LCN,
)
-from .helpers import get_connection
+from .helpers import get_device_connection
PARALLEL_UPDATES = 0
-async def async_setup_platform(
- hass, hass_config, async_add_entities, discovery_info=None
-):
- """Set up the LCN climate platform."""
- if discovery_info is None:
- return
+def create_lcn_climate_entity(hass, entity_config, config_entry):
+ """Set up an entity for this domain."""
+ device_connection = get_device_connection(
+ hass, tuple(entity_config[CONF_ADDRESS]), config_entry
+ )
- devices = []
- for config in discovery_info:
- address, connection_id = config[CONF_ADDRESS]
- addr = pypck.lcn_addr.LcnAddr(*address)
- connections = hass.data[DATA_LCN][CONF_CONNECTIONS]
- connection = get_connection(connections, connection_id)
- address_connection = connection.get_address_conn(addr)
+ return LcnClimate(entity_config, config_entry.entry_id, device_connection)
- devices.append(LcnClimate(config, address_connection))
- async_add_entities(devices)
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up LCN switch entities from a config entry."""
+ entities = []
+
+ for entity_config in config_entry.data[CONF_ENTITIES]:
+ if entity_config[CONF_DOMAIN] == DOMAIN_CLIMATE:
+ entities.append(
+ create_lcn_climate_entity(hass, entity_config, config_entry)
+ )
+
+ async_add_entities(entities)
class LcnClimate(LcnEntity, ClimateEntity):
"""Representation of a LCN climate device."""
- def __init__(self, config, device_connection):
+ def __init__(self, config, entry_id, device_connection):
"""Initialize of a LCN climate device."""
- super().__init__(config, device_connection)
+ super().__init__(config, entry_id, device_connection)
- self.variable = pypck.lcn_defs.Var[config[CONF_SOURCE]]
- self.setpoint = pypck.lcn_defs.Var[config[CONF_SETPOINT]]
- self.unit = pypck.lcn_defs.VarUnit.parse(config[CONF_UNIT_OF_MEASUREMENT])
+ self.variable = pypck.lcn_defs.Var[config[CONF_DOMAIN_DATA][CONF_SOURCE]]
+ self.setpoint = pypck.lcn_defs.Var[config[CONF_DOMAIN_DATA][CONF_SETPOINT]]
+ self.unit = pypck.lcn_defs.VarUnit.parse(
+ config[CONF_DOMAIN_DATA][CONF_UNIT_OF_MEASUREMENT]
+ )
self.regulator_id = pypck.lcn_defs.Var.to_set_point_id(self.setpoint)
- self.is_lockable = config[CONF_LOCKABLE]
- self._max_temp = config[CONF_MAX_TEMP]
- self._min_temp = config[CONF_MIN_TEMP]
+ self.is_lockable = config[CONF_DOMAIN_DATA][CONF_LOCKABLE]
+ self._max_temp = config[CONF_DOMAIN_DATA][CONF_MAX_TEMP]
+ self._min_temp = config[CONF_DOMAIN_DATA][CONF_MIN_TEMP]
self._current_temperature = None
self._target_temperature = None
- self._is_on = None
+ self._is_on = True
async def async_added_to_hass(self):
"""Run when entity about to be added to hass."""
@@ -67,6 +79,13 @@ async def async_added_to_hass(self):
await self.device_connection.activate_status_request_handler(self.variable)
await self.device_connection.activate_status_request_handler(self.setpoint)
+ async def async_will_remove_from_hass(self):
+ """Run when entity will be removed from hass."""
+ await super().async_will_remove_from_hass()
+ if not self.device_connection.is_group:
+ await self.device_connection.cancel_status_request_handler(self.variable)
+ await self.device_connection.cancel_status_request_handler(self.setpoint)
+
@property
def supported_features(self):
"""Return the list of supported features."""
diff --git a/homeassistant/components/lcn/config_flow.py b/homeassistant/components/lcn/config_flow.py
new file mode 100644
index 00000000000000..fe353cdb4c54ff
--- /dev/null
+++ b/homeassistant/components/lcn/config_flow.py
@@ -0,0 +1,97 @@
+"""Config flow to configure the LCN integration."""
+import logging
+
+import pypck
+
+from homeassistant import config_entries
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_IP_ADDRESS,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_USERNAME,
+)
+
+from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def get_config_entry(hass, data):
+ """Check config entries for already configured entries based on the ip address/port."""
+ return next(
+ (
+ entry
+ for entry in hass.config_entries.async_entries(DOMAIN)
+ if entry.data[CONF_IP_ADDRESS] == data[CONF_IP_ADDRESS]
+ and entry.data[CONF_PORT] == data[CONF_PORT]
+ ),
+ None,
+ )
+
+
+async def validate_connection(host_name, data):
+ """Validate if a connection to LCN can be established."""
+ host = data[CONF_IP_ADDRESS]
+ port = data[CONF_PORT]
+ username = data[CONF_USERNAME]
+ password = data[CONF_PASSWORD]
+ sk_num_tries = data[CONF_SK_NUM_TRIES]
+ dim_mode = data[CONF_DIM_MODE]
+
+ settings = {
+ "SK_NUM_TRIES": sk_num_tries,
+ "DIM_MODE": pypck.lcn_defs.OutputPortDimMode[dim_mode],
+ }
+
+ _LOGGER.debug("Validating connection parameters to PCHK host '%s'", host_name)
+
+ connection = pypck.connection.PchkConnectionManager(
+ host, port, username, password, settings=settings
+ )
+
+ await connection.async_connect(timeout=5)
+
+ _LOGGER.debug("LCN connection validated")
+ await connection.async_close()
+ return data
+
+
+@config_entries.HANDLERS.register(DOMAIN)
+class LcnFlowHandler(config_entries.ConfigFlow):
+ """Handle a LCN config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
+
+ async def async_step_import(self, data):
+ """Import existing configuration from LCN."""
+ host_name = data[CONF_HOST]
+ # validate the imported connection parameters
+ try:
+ await validate_connection(host_name, data)
+ except pypck.connection.PchkAuthenticationError:
+ _LOGGER.warning('Authentication on PCHK "%s" failed', host_name)
+ return self.async_abort(reason="authentication_error")
+ except pypck.connection.PchkLicenseError:
+ _LOGGER.warning(
+ 'Maximum number of connections on PCHK "%s" was '
+ "reached. An additional license key is required",
+ host_name,
+ )
+ return self.async_abort(reason="license_error")
+ except TimeoutError:
+ _LOGGER.warning('Connection to PCHK "%s" failed', host_name)
+ return self.async_abort(reason="connection_timeout")
+
+ # check if we already have a host with the same address configured
+ entry = get_config_entry(self.hass, data)
+ if entry:
+ entry.source = config_entries.SOURCE_IMPORT
+ self.hass.config_entries.async_update_entry(entry, data=data)
+ return self.async_abort(reason="existing_configuration_updated")
+
+ return self.async_create_entry(
+ title=f"{host_name}",
+ data=data,
+ )
diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py
index 821a7102154712..4e3e765ace0516 100644
--- a/homeassistant/components/lcn/const.py
+++ b/homeassistant/components/lcn/const.py
@@ -14,6 +14,13 @@
DATA_LCN = "lcn"
DEFAULT_NAME = "pchk"
+CONNECTION = "connection"
+CONF_HARDWARE_SERIAL = "hardware_serial"
+CONF_SOFTWARE_SERIAL = "software_serial"
+CONF_HARDWARE_TYPE = "hardware_type"
+CONF_RESOURCE = "resource"
+CONF_DOMAIN_DATA = "domain_data"
+
CONF_CONNECTIONS = "connections"
CONF_SK_NUM_TRIES = "sk_num_tries"
CONF_OUTPUT = "output"
@@ -25,7 +32,6 @@
CONF_VARIABLE = "variable"
CONF_VALUE = "value"
CONF_RELVARREF = "value_reference"
-CONF_SOURCE = "source"
CONF_SETPOINT = "setpoint"
CONF_LED = "led"
CONF_KEYS = "keys"
@@ -40,7 +46,6 @@
CONF_MIN_TEMP = "min_temp"
CONF_SCENES = "scenes"
CONF_REGISTER = "register"
-CONF_SCENE = "scene"
CONF_OUTPUTS = "outputs"
CONF_REVERSE_TIME = "reverse_time"
diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py
index 3d7c2a06a3b1ee..bf777ad93f2b0a 100644
--- a/homeassistant/components/lcn/cover.py
+++ b/homeassistant/components/lcn/cover.py
@@ -1,53 +1,54 @@
"""Support for LCN covers."""
+
import pypck
-from homeassistant.components.cover import CoverEntity
-from homeassistant.const import CONF_ADDRESS
+from homeassistant.components.cover import DOMAIN as DOMAIN_COVER, CoverEntity
+from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES
from . import LcnEntity
-from .const import CONF_CONNECTIONS, CONF_MOTOR, CONF_REVERSE_TIME, DATA_LCN
-from .helpers import get_connection
+from .const import CONF_DOMAIN_DATA, CONF_MOTOR, CONF_REVERSE_TIME
+from .helpers import get_device_connection
PARALLEL_UPDATES = 0
-async def async_setup_platform(
- hass, hass_config, async_add_entities, discovery_info=None
-):
- """Setups the LCN cover platform."""
- if discovery_info is None:
- return
+def create_lcn_cover_entity(hass, entity_config, config_entry):
+ """Set up an entity for this domain."""
+ device_connection = get_device_connection(
+ hass, tuple(entity_config[CONF_ADDRESS]), config_entry
+ )
+
+ if entity_config[CONF_DOMAIN_DATA][CONF_MOTOR] in "OUTPUTS":
+ return LcnOutputsCover(entity_config, config_entry.entry_id, device_connection)
+ # in RELAYS
+ return LcnRelayCover(entity_config, config_entry.entry_id, device_connection)
- devices = []
- for config in discovery_info:
- address, connection_id = config[CONF_ADDRESS]
- addr = pypck.lcn_addr.LcnAddr(*address)
- connections = hass.data[DATA_LCN][CONF_CONNECTIONS]
- connection = get_connection(connections, connection_id)
- address_connection = connection.get_address_conn(addr)
- if config[CONF_MOTOR] == "OUTPUTS":
- devices.append(LcnOutputsCover(config, address_connection))
- else: # RELAYS
- devices.append(LcnRelayCover(config, address_connection))
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up LCN cover entities from a config entry."""
+ entities = []
- async_add_entities(devices)
+ for entity_config in config_entry.data[CONF_ENTITIES]:
+ if entity_config[CONF_DOMAIN] == DOMAIN_COVER:
+ entities.append(create_lcn_cover_entity(hass, entity_config, config_entry))
+
+ async_add_entities(entities)
class LcnOutputsCover(LcnEntity, CoverEntity):
"""Representation of a LCN cover connected to output ports."""
- def __init__(self, config, device_connection):
+ def __init__(self, config, entry_id, device_connection):
"""Initialize the LCN cover."""
- super().__init__(config, device_connection)
+ super().__init__(config, entry_id, device_connection)
self.output_ids = [
pypck.lcn_defs.OutputPort["OUTPUTUP"].value,
pypck.lcn_defs.OutputPort["OUTPUTDOWN"].value,
]
- if CONF_REVERSE_TIME in config:
+ if CONF_REVERSE_TIME in config[CONF_DOMAIN_DATA]:
self.reverse_time = pypck.lcn_defs.MotorReverseTime[
- config[CONF_REVERSE_TIME]
+ config[CONF_DOMAIN_DATA][CONF_REVERSE_TIME]
]
else:
self.reverse_time = None
@@ -59,12 +60,24 @@ def __init__(self, config, device_connection):
async def async_added_to_hass(self):
"""Run when entity about to be added to hass."""
await super().async_added_to_hass()
- await self.device_connection.activate_status_request_handler(
- pypck.lcn_defs.OutputPort["OUTPUTUP"]
- )
- await self.device_connection.activate_status_request_handler(
- pypck.lcn_defs.OutputPort["OUTPUTDOWN"]
- )
+ if not self.device_connection.is_group:
+ await self.device_connection.activate_status_request_handler(
+ pypck.lcn_defs.OutputPort["OUTPUTUP"]
+ )
+ await self.device_connection.activate_status_request_handler(
+ pypck.lcn_defs.OutputPort["OUTPUTDOWN"]
+ )
+
+ async def async_will_remove_from_hass(self):
+ """Run when entity will be removed from hass."""
+ await super().async_will_remove_from_hass()
+ if not self.device_connection.is_group:
+ await self.device_connection.cancel_status_request_handler(
+ pypck.lcn_defs.OutputPort["OUTPUTUP"]
+ )
+ await self.device_connection.cancel_status_request_handler(
+ pypck.lcn_defs.OutputPort["OUTPUTDOWN"]
+ )
@property
def is_closed(self):
@@ -146,11 +159,11 @@ def input_received(self, input_obj):
class LcnRelayCover(LcnEntity, CoverEntity):
"""Representation of a LCN cover connected to relays."""
- def __init__(self, config, device_connection):
+ def __init__(self, config, entry_id, device_connection):
"""Initialize the LCN cover."""
- super().__init__(config, device_connection)
+ super().__init__(config, entry_id, device_connection)
- self.motor = pypck.lcn_defs.MotorPort[config[CONF_MOTOR]]
+ self.motor = pypck.lcn_defs.MotorPort[config[CONF_DOMAIN_DATA][CONF_MOTOR]]
self.motor_port_onoff = self.motor.value * 2
self.motor_port_updown = self.motor_port_onoff + 1
@@ -164,6 +177,12 @@ async def async_added_to_hass(self):
if not self.device_connection.is_group:
await self.device_connection.activate_status_request_handler(self.motor)
+ async def async_will_remove_from_hass(self):
+ """Run when entity will be removed from hass."""
+ await super().async_will_remove_from_hass()
+ if not self.device_connection.is_group:
+ await self.device_connection.cancel_status_request_handler(self.motor)
+
@property
def is_closed(self):
"""Return if the cover is closed."""
diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py
index 18342aa1d988d3..3f93ec95a69c3f 100644
--- a/homeassistant/components/lcn/helpers.py
+++ b/homeassistant/components/lcn/helpers.py
@@ -1,11 +1,42 @@
"""Helpers for LCN component."""
import re
+import pypck
import voluptuous as vol
-from homeassistant.const import CONF_NAME
+from homeassistant.const import (
+ CONF_ADDRESS,
+ CONF_BINARY_SENSORS,
+ CONF_COVERS,
+ CONF_DEVICES,
+ CONF_DOMAIN,
+ CONF_ENTITIES,
+ CONF_HOST,
+ CONF_IP_ADDRESS,
+ CONF_LIGHTS,
+ CONF_NAME,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_SENSORS,
+ CONF_SWITCHES,
+ CONF_USERNAME,
+)
-from .const import DEFAULT_NAME
+from .const import (
+ CONF_CLIMATES,
+ CONF_CONNECTIONS,
+ CONF_DIM_MODE,
+ CONF_DOMAIN_DATA,
+ CONF_HARDWARE_SERIAL,
+ CONF_HARDWARE_TYPE,
+ CONF_RESOURCE,
+ CONF_SCENES,
+ CONF_SK_NUM_TRIES,
+ CONF_SOFTWARE_SERIAL,
+ CONNECTION,
+ DEFAULT_NAME,
+ DOMAIN,
+)
# Regex for address validation
PATTERN_ADDRESS = re.compile(
@@ -13,17 +44,145 @@
)
-def get_connection(connections, connection_id=None):
- """Return the connection object from list."""
- if connection_id is None:
- connection = connections[0]
- else:
- for connection in connections:
- if connection.connection_id == connection_id:
- break
- else:
- raise ValueError("Unknown connection_id.")
- return connection
+DOMAIN_LOOKUP = {
+ CONF_BINARY_SENSORS: "binary_sensor",
+ CONF_CLIMATES: "climate",
+ CONF_COVERS: "cover",
+ CONF_LIGHTS: "light",
+ CONF_SCENES: "scene",
+ CONF_SENSORS: "sensor",
+ CONF_SWITCHES: "switch",
+}
+
+
+def get_device_connection(hass, address, config_entry):
+ """Return a lcn device_connection."""
+ host_connection = hass.data[DOMAIN][config_entry.entry_id][CONNECTION]
+ addr = pypck.lcn_addr.LcnAddr(*address)
+ return host_connection.get_address_conn(addr)
+
+
+def get_resource(domain_name, domain_data):
+ """Return the resource for the specified domain_data."""
+ if domain_name in ["switch", "light"]:
+ return domain_data["output"]
+ if domain_name in ["binary_sensor", "sensor"]:
+ return domain_data["source"]
+ if domain_name == "cover":
+ return domain_data["motor"]
+ if domain_name == "climate":
+ return f'{domain_data["source"]}.{domain_data["setpoint"]}'
+ if domain_name == "scene":
+ return f'{domain_data["register"]}.{domain_data["scene"]}'
+ raise ValueError("Unknown domain")
+
+
+def generate_unique_id(address):
+ """Generate a unique_id from the given parameters."""
+ is_group = "g" if address[2] else "m"
+ return f"{is_group}{address[0]:03d}{address[1]:03d}"
+
+
+def import_lcn_config(lcn_config):
+ """Convert lcn settings from configuration.yaml to config_entries data.
+
+ Create a list of config_entry data structures like:
+
+ "data": {
+ "host": "pchk",
+ "ip_address": "192.168.2.41",
+ "port": 4114,
+ "username": "lcn",
+ "password": "lcn,
+ "sk_num_tries: 0,
+ "dim_mode: "STEPS200",
+ "devices": [
+ {
+ "address": (0, 7, False)
+ "name": "",
+ "hardware_serial": -1,
+ "software_serial": -1,
+ "hardware_type": -1
+ }, ...
+ ],
+ "entities": [
+ {
+ "address": (0, 7, False)
+ "name": "Light_Output1",
+ "resource": "output1",
+ "domain": "light",
+ "domain_data": {
+ "output": "OUTPUT1",
+ "dimmable": True,
+ "transition": 5000.0
+ }
+ }, ...
+ ]
+ }
+ """
+ data = {}
+ for connection in lcn_config[CONF_CONNECTIONS]:
+ host = {
+ CONF_HOST: connection[CONF_NAME],
+ CONF_IP_ADDRESS: connection[CONF_HOST],
+ CONF_PORT: connection[CONF_PORT],
+ CONF_USERNAME: connection[CONF_USERNAME],
+ CONF_PASSWORD: connection[CONF_PASSWORD],
+ CONF_SK_NUM_TRIES: connection[CONF_SK_NUM_TRIES],
+ CONF_DIM_MODE: connection[CONF_DIM_MODE],
+ CONF_DEVICES: [],
+ CONF_ENTITIES: [],
+ }
+ data[connection[CONF_NAME]] = host
+
+ for confkey, domain_config in lcn_config.items():
+ if confkey == CONF_CONNECTIONS:
+ continue
+ domain = DOMAIN_LOOKUP[confkey]
+ # loop over entities in configuration.yaml
+ for domain_data in domain_config:
+ # remove name and address from domain_data
+ entity_name = domain_data.pop(CONF_NAME)
+ address, host_name = domain_data.pop(CONF_ADDRESS)
+
+ if host_name is None:
+ host_name = DEFAULT_NAME
+
+ # check if we have a new device config
+ for device_config in data[host_name][CONF_DEVICES]:
+ if address == device_config[CONF_ADDRESS]:
+ break
+ else: # create new device_config
+ device_config = {
+ CONF_ADDRESS: address,
+ CONF_NAME: "",
+ CONF_HARDWARE_SERIAL: -1,
+ CONF_SOFTWARE_SERIAL: -1,
+ CONF_HARDWARE_TYPE: -1,
+ }
+
+ data[host_name][CONF_DEVICES].append(device_config)
+
+ # insert entity config
+ resource = get_resource(domain, domain_data).lower()
+ for entity_config in data[host_name][CONF_ENTITIES]:
+ if (
+ address == entity_config[CONF_ADDRESS]
+ and resource == entity_config[CONF_RESOURCE]
+ and domain == entity_config[CONF_DOMAIN]
+ ):
+ break
+ else: # create new entity_config
+ entity_config = {
+ CONF_ADDRESS: address,
+ CONF_NAME: entity_name,
+ CONF_RESOURCE: resource,
+ CONF_DOMAIN: domain,
+ CONF_DOMAIN_DATA: domain_data.copy(),
+ }
+ data[host_name][CONF_ENTITIES].append(entity_config)
+
+ return list(data.values())
def has_unique_host_names(hosts):
diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py
index 5242ed1cc59a00..8697d8e0319ce7 100644
--- a/homeassistant/components/lcn/light.py
+++ b/homeassistant/components/lcn/light.py
@@ -1,68 +1,69 @@
"""Support for LCN lights."""
+
import pypck
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_TRANSITION,
+ DOMAIN as DOMAIN_LIGHT,
SUPPORT_BRIGHTNESS,
SUPPORT_TRANSITION,
LightEntity,
)
-from homeassistant.const import CONF_ADDRESS
+from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES
from . import LcnEntity
from .const import (
- CONF_CONNECTIONS,
CONF_DIMMABLE,
+ CONF_DOMAIN_DATA,
CONF_OUTPUT,
CONF_TRANSITION,
- DATA_LCN,
OUTPUT_PORTS,
)
-from .helpers import get_connection
+from .helpers import get_device_connection
PARALLEL_UPDATES = 0
-async def async_setup_platform(
- hass, hass_config, async_add_entities, discovery_info=None
-):
- """Set up the LCN light platform."""
- if discovery_info is None:
- return
+def create_lcn_light_entity(hass, entity_config, config_entry):
+ """Set up an entity for this domain."""
+ device_connection = get_device_connection(
+ hass, tuple(entity_config[CONF_ADDRESS]), config_entry
+ )
+
+ if entity_config[CONF_DOMAIN_DATA][CONF_OUTPUT] in OUTPUT_PORTS:
+ return LcnOutputLight(entity_config, config_entry.entry_id, device_connection)
+ # in RELAY_PORTS
+ return LcnRelayLight(entity_config, config_entry.entry_id, device_connection)
- devices = []
- for config in discovery_info:
- address, connection_id = config[CONF_ADDRESS]
- addr = pypck.lcn_addr.LcnAddr(*address)
- connections = hass.data[DATA_LCN][CONF_CONNECTIONS]
- connection = get_connection(connections, connection_id)
- address_connection = connection.get_address_conn(addr)
- if config[CONF_OUTPUT] in OUTPUT_PORTS:
- device = LcnOutputLight(config, address_connection)
- else: # in RELAY_PORTS
- device = LcnRelayLight(config, address_connection)
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up LCN light entities from a config entry."""
+ entities = []
- devices.append(device)
+ for entity_config in config_entry.data[CONF_ENTITIES]:
+ if entity_config[CONF_DOMAIN] == DOMAIN_LIGHT:
+ entities.append(create_lcn_light_entity(hass, entity_config, config_entry))
- async_add_entities(devices)
+ async_add_entities(entities)
class LcnOutputLight(LcnEntity, LightEntity):
"""Representation of a LCN light for output ports."""
- def __init__(self, config, device_connection):
+ def __init__(self, config, entry_id, device_connection):
"""Initialize the LCN light."""
- super().__init__(config, device_connection)
+ super().__init__(config, entry_id, device_connection)
- self.output = pypck.lcn_defs.OutputPort[config[CONF_OUTPUT]]
+ self.output = pypck.lcn_defs.OutputPort[config[CONF_DOMAIN_DATA][CONF_OUTPUT]]
- self._transition = pypck.lcn_defs.time_to_ramp_value(config[CONF_TRANSITION])
- self.dimmable = config[CONF_DIMMABLE]
+ self._transition = pypck.lcn_defs.time_to_ramp_value(
+ config[CONF_DOMAIN_DATA][CONF_TRANSITION]
+ )
+ self.dimmable = config[CONF_DOMAIN_DATA][CONF_DIMMABLE]
self._brightness = 255
- self._is_on = None
+ self._is_on = False
self._is_dimming_to_zero = False
async def async_added_to_hass(self):
@@ -71,6 +72,12 @@ async def async_added_to_hass(self):
if not self.device_connection.is_group:
await self.device_connection.activate_status_request_handler(self.output)
+ async def async_will_remove_from_hass(self):
+ """Run when entity will be removed from hass."""
+ await super().async_will_remove_from_hass()
+ if not self.device_connection.is_group:
+ await self.device_connection.cancel_status_request_handler(self.output)
+
@property
def supported_features(self):
"""Flag supported features."""
@@ -145,13 +152,13 @@ def input_received(self, input_obj):
class LcnRelayLight(LcnEntity, LightEntity):
"""Representation of a LCN light for relay ports."""
- def __init__(self, config, device_connection):
+ def __init__(self, config, entry_id, device_connection):
"""Initialize the LCN light."""
- super().__init__(config, device_connection)
+ super().__init__(config, entry_id, device_connection)
- self.output = pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]]
+ self.output = pypck.lcn_defs.RelayPort[config[CONF_DOMAIN_DATA][CONF_OUTPUT]]
- self._is_on = None
+ self._is_on = False
async def async_added_to_hass(self):
"""Run when entity about to be added to hass."""
@@ -159,6 +166,12 @@ async def async_added_to_hass(self):
if not self.device_connection.is_group:
await self.device_connection.activate_status_request_handler(self.output)
+ async def async_will_remove_from_hass(self):
+ """Run when entity will be removed from hass."""
+ await super().async_will_remove_from_hass()
+ if not self.device_connection.is_group:
+ await self.device_connection.cancel_status_request_handler(self.output)
+
@property
def is_on(self):
"""Return True if entity is on."""
@@ -166,7 +179,6 @@ def is_on(self):
async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
-
states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8
states[self.output.value] = pypck.lcn_defs.RelayStateModifier.ON
if not await self.device_connection.control_relays(states):
@@ -176,7 +188,6 @@ async def async_turn_on(self, **kwargs):
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
-
states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8
states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF
if not await self.device_connection.control_relays(states):
diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json
index c5077bdf4092ab..5c8be5829e01b2 100644
--- a/homeassistant/components/lcn/manifest.json
+++ b/homeassistant/components/lcn/manifest.json
@@ -1,6 +1,7 @@
{
"domain": "lcn",
"name": "LCN",
+ "config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/lcn",
"requirements": [
"pypck==0.7.9"
diff --git a/homeassistant/components/lcn/scene.py b/homeassistant/components/lcn/scene.py
index ed211473e29747..8f770df7668b19 100644
--- a/homeassistant/components/lcn/scene.py
+++ b/homeassistant/components/lcn/scene.py
@@ -1,73 +1,69 @@
"""Support for LCN scenes."""
-from typing import Any
import pypck
-from homeassistant.components.scene import Scene
-from homeassistant.const import CONF_ADDRESS
+from homeassistant.components.scene import DOMAIN as DOMAIN_SCENE, Scene
+from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES, CONF_SCENE
from . import LcnEntity
from .const import (
- CONF_CONNECTIONS,
+ CONF_DOMAIN_DATA,
CONF_OUTPUTS,
CONF_REGISTER,
- CONF_SCENE,
CONF_TRANSITION,
- DATA_LCN,
OUTPUT_PORTS,
)
-from .helpers import get_connection
+from .helpers import get_device_connection
PARALLEL_UPDATES = 0
-async def async_setup_platform(
- hass, hass_config, async_add_entities, discovery_info=None
-):
- """Set up the LCN scene platform."""
- if discovery_info is None:
- return
+def create_lcn_scene_entity(hass, entity_config, config_entry):
+ """Set up an entity for this domain."""
+ device_connection = get_device_connection(
+ hass, tuple(entity_config[CONF_ADDRESS]), config_entry
+ )
- devices = []
- for config in discovery_info:
- address, connection_id = config[CONF_ADDRESS]
- addr = pypck.lcn_addr.LcnAddr(*address)
- connections = hass.data[DATA_LCN][CONF_CONNECTIONS]
- connection = get_connection(connections, connection_id)
- address_connection = connection.get_address_conn(addr)
+ return LcnScene(entity_config, config_entry.entry_id, device_connection)
- devices.append(LcnScene(config, address_connection))
- async_add_entities(devices)
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up LCN switch entities from a config entry."""
+ entities = []
+
+ for entity_config in config_entry.data[CONF_ENTITIES]:
+ if entity_config[CONF_DOMAIN] == DOMAIN_SCENE:
+ entities.append(create_lcn_scene_entity(hass, entity_config, config_entry))
+
+ async_add_entities(entities)
class LcnScene(LcnEntity, Scene):
"""Representation of a LCN scene."""
- def __init__(self, config, device_connection):
+ def __init__(self, config, entry_id, device_connection):
"""Initialize the LCN scene."""
- super().__init__(config, device_connection)
+ super().__init__(config, entry_id, device_connection)
- self.register_id = config[CONF_REGISTER]
- self.scene_id = config[CONF_SCENE]
+ self.register_id = config[CONF_DOMAIN_DATA][CONF_REGISTER]
+ self.scene_id = config[CONF_DOMAIN_DATA][CONF_SCENE]
self.output_ports = []
self.relay_ports = []
- for port in config[CONF_OUTPUTS]:
+ for port in config[CONF_DOMAIN_DATA][CONF_OUTPUTS]:
if port in OUTPUT_PORTS:
self.output_ports.append(pypck.lcn_defs.OutputPort[port])
else: # in RELEAY_PORTS
self.relay_ports.append(pypck.lcn_defs.RelayPort[port])
- if config[CONF_TRANSITION] is None:
+ if config[CONF_DOMAIN_DATA][CONF_TRANSITION] is None:
self.transition = None
else:
- self.transition = pypck.lcn_defs.time_to_ramp_value(config[CONF_TRANSITION])
-
- async def async_added_to_hass(self):
- """Run when entity about to be added to hass."""
+ self.transition = pypck.lcn_defs.time_to_ramp_value(
+ config[CONF_DOMAIN_DATA][CONF_TRANSITION]
+ )
- async def async_activate(self, **kwargs: Any) -> None:
+ async def async_activate(self, **kwargs):
"""Activate scene."""
await self.device_connection.activate_scene(
self.register_id,
diff --git a/homeassistant/components/lcn/schemas.py b/homeassistant/components/lcn/schemas.py
index 1cc51f400da565..5244bac3b6b341 100644
--- a/homeassistant/components/lcn/schemas.py
+++ b/homeassistant/components/lcn/schemas.py
@@ -11,7 +11,9 @@
CONF_NAME,
CONF_PASSWORD,
CONF_PORT,
+ CONF_SCENE,
CONF_SENSORS,
+ CONF_SOURCE,
CONF_SWITCHES,
CONF_UNIT_OF_MEASUREMENT,
CONF_USERNAME,
@@ -32,11 +34,9 @@
CONF_OUTPUTS,
CONF_REGISTER,
CONF_REVERSE_TIME,
- CONF_SCENE,
CONF_SCENES,
CONF_SETPOINT,
CONF_SK_NUM_TRIES,
- CONF_SOURCE,
CONF_TRANSITION,
DIM_MODES,
DOMAIN,
diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py
index 4d4be5e125995f..64870e22e4c4f8 100644
--- a/homeassistant/components/lcn/sensor.py
+++ b/homeassistant/components/lcn/sensor.py
@@ -1,56 +1,67 @@
"""Support for LCN sensors."""
+
import pypck
-from homeassistant.const import CONF_ADDRESS, CONF_UNIT_OF_MEASUREMENT
+from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR, SensorEntity
+from homeassistant.const import (
+ CONF_ADDRESS,
+ CONF_DOMAIN,
+ CONF_ENTITIES,
+ CONF_SOURCE,
+ CONF_UNIT_OF_MEASUREMENT,
+)
from . import LcnEntity
from .const import (
- CONF_CONNECTIONS,
- CONF_SOURCE,
- DATA_LCN,
+ CONF_DOMAIN_DATA,
LED_PORTS,
S0_INPUTS,
SETPOINTS,
THRESHOLDS,
VARIABLES,
)
-from .helpers import get_connection
+from .helpers import get_device_connection
+
+def create_lcn_sensor_entity(hass, entity_config, config_entry):
+ """Set up an entity for this domain."""
+ device_connection = get_device_connection(
+ hass, tuple(entity_config[CONF_ADDRESS]), config_entry
+ )
-async def async_setup_platform(
- hass, hass_config, async_add_entities, discovery_info=None
-):
- """Set up the LCN sensor platform."""
- if discovery_info is None:
- return
+ if (
+ entity_config[CONF_DOMAIN_DATA][CONF_SOURCE]
+ in VARIABLES + SETPOINTS + THRESHOLDS + S0_INPUTS
+ ):
+ return LcnVariableSensor(
+ entity_config, config_entry.entry_id, device_connection
+ )
+ # in LED_PORTS + LOGICOP_PORTS
+ return LcnLedLogicSensor(entity_config, config_entry.entry_id, device_connection)
- devices = []
- for config in discovery_info:
- address, connection_id = config[CONF_ADDRESS]
- addr = pypck.lcn_addr.LcnAddr(*address)
- connections = hass.data[DATA_LCN][CONF_CONNECTIONS]
- connection = get_connection(connections, connection_id)
- device_connection = connection.get_address_conn(addr)
- if config[CONF_SOURCE] in VARIABLES + SETPOINTS + THRESHOLDS + S0_INPUTS:
- device = LcnVariableSensor(config, device_connection)
- else: # in LED_PORTS + LOGICOP_PORTS
- device = LcnLedLogicSensor(config, device_connection)
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up LCN switch entities from a config entry."""
+ entities = []
- devices.append(device)
+ for entity_config in config_entry.data[CONF_ENTITIES]:
+ if entity_config[CONF_DOMAIN] == DOMAIN_SENSOR:
+ entities.append(create_lcn_sensor_entity(hass, entity_config, config_entry))
- async_add_entities(devices)
+ async_add_entities(entities)
-class LcnVariableSensor(LcnEntity):
+class LcnVariableSensor(LcnEntity, SensorEntity):
"""Representation of a LCN sensor for variables."""
- def __init__(self, config, device_connection):
+ def __init__(self, config, entry_id, device_connection):
"""Initialize the LCN sensor."""
- super().__init__(config, device_connection)
+ super().__init__(config, entry_id, device_connection)
- self.variable = pypck.lcn_defs.Var[config[CONF_SOURCE]]
- self.unit = pypck.lcn_defs.VarUnit.parse(config[CONF_UNIT_OF_MEASUREMENT])
+ self.variable = pypck.lcn_defs.Var[config[CONF_DOMAIN_DATA][CONF_SOURCE]]
+ self.unit = pypck.lcn_defs.VarUnit.parse(
+ config[CONF_DOMAIN_DATA][CONF_UNIT_OF_MEASUREMENT]
+ )
self._value = None
@@ -60,6 +71,12 @@ async def async_added_to_hass(self):
if not self.device_connection.is_group:
await self.device_connection.activate_status_request_handler(self.variable)
+ async def async_will_remove_from_hass(self):
+ """Run when entity will be removed from hass."""
+ await super().async_will_remove_from_hass()
+ if not self.device_connection.is_group:
+ await self.device_connection.cancel_status_request_handler(self.variable)
+
@property
def state(self):
"""Return the state of the entity."""
@@ -82,17 +99,19 @@ def input_received(self, input_obj):
self.async_write_ha_state()
-class LcnLedLogicSensor(LcnEntity):
+class LcnLedLogicSensor(LcnEntity, SensorEntity):
"""Representation of a LCN sensor for leds and logicops."""
- def __init__(self, config, device_connection):
+ def __init__(self, config, entry_id, device_connection):
"""Initialize the LCN sensor."""
- super().__init__(config, device_connection)
+ super().__init__(config, entry_id, device_connection)
- if config[CONF_SOURCE] in LED_PORTS:
- self.source = pypck.lcn_defs.LedPort[config[CONF_SOURCE]]
+ if config[CONF_DOMAIN_DATA][CONF_SOURCE] in LED_PORTS:
+ self.source = pypck.lcn_defs.LedPort[config[CONF_DOMAIN_DATA][CONF_SOURCE]]
else:
- self.source = pypck.lcn_defs.LogicOpPort[config[CONF_SOURCE]]
+ self.source = pypck.lcn_defs.LogicOpPort[
+ config[CONF_DOMAIN_DATA][CONF_SOURCE]
+ ]
self._value = None
@@ -102,6 +121,12 @@ async def async_added_to_hass(self):
if not self.device_connection.is_group:
await self.device_connection.activate_status_request_handler(self.source)
+ async def async_will_remove_from_hass(self):
+ """Run when entity will be removed from hass."""
+ await super().async_will_remove_from_hass()
+ if not self.device_connection.is_group:
+ await self.device_connection.cancel_status_request_handler(self.source)
+
@property
def state(self):
"""Return the state of the entity."""
diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py
index d7d8acf4f298c2..c6c33270264d66 100644
--- a/homeassistant/components/lcn/services.py
+++ b/homeassistant/components/lcn/services.py
@@ -6,6 +6,7 @@
from homeassistant.const import (
CONF_ADDRESS,
CONF_BRIGHTNESS,
+ CONF_HOST,
CONF_STATE,
CONF_UNIT_OF_MEASUREMENT,
TIME_SECONDS,
@@ -13,7 +14,6 @@
import homeassistant.helpers.config_validation as cv
from .const import (
- CONF_CONNECTIONS,
CONF_KEYS,
CONF_LED,
CONF_OUTPUT,
@@ -28,7 +28,7 @@
CONF_TRANSITION,
CONF_VALUE,
CONF_VARIABLE,
- DATA_LCN,
+ DOMAIN,
LED_PORTS,
LED_STATUS,
OUTPUT_PORTS,
@@ -41,7 +41,7 @@
VARIABLES,
)
from .helpers import (
- get_connection,
+ get_device_connection,
is_address,
is_key_lock_states_string,
is_relays_states_string,
@@ -56,18 +56,20 @@ class LcnServiceCall:
def __init__(self, hass):
"""Initialize service call."""
self.hass = hass
- self.connections = hass.data[DATA_LCN][CONF_CONNECTIONS]
def get_device_connection(self, service):
- """Get device connection object."""
- addr, connection_id = service.data[CONF_ADDRESS]
- addr = pypck.lcn_addr.LcnAddr(*addr)
- if connection_id is None:
- connection = self.connections[0]
- else:
- connection = get_connection(self.connections, connection_id)
+ """Get address connection object."""
+ address, host_name = service.data[CONF_ADDRESS]
- return connection.get_address_conn(addr)
+ for config_entry in self.hass.config_entries.async_entries(DOMAIN):
+ if config_entry.data[CONF_HOST] == host_name:
+ device_connection = get_device_connection(
+ self.hass, address, config_entry
+ )
+ if device_connection is None:
+ raise ValueError("Wrong address.")
+ return device_connection
+ raise ValueError("Invalid host name.")
async def async_call_service(self, service):
"""Execute service call."""
@@ -392,3 +394,20 @@ async def async_call_service(self, service):
pck = service.data[CONF_PCK]
device_connection = self.get_device_connection(service)
await device_connection.pck(pck)
+
+
+SERVICES = (
+ ("output_abs", OutputAbs),
+ ("output_rel", OutputRel),
+ ("output_toggle", OutputToggle),
+ ("relays", Relays),
+ ("var_abs", VarAbs),
+ ("var_reset", VarReset),
+ ("var_rel", VarRel),
+ ("lock_regulator", LockRegulator),
+ ("led", Led),
+ ("send_keys", SendKeys),
+ ("lock_keys", LockKeys),
+ ("dyn_text", DynText),
+ ("pck", Pck),
+)
diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py
index 6f9cc25db99033..1429bf67f7e2d1 100644
--- a/homeassistant/components/lcn/switch.py
+++ b/homeassistant/components/lcn/switch.py
@@ -1,49 +1,49 @@
"""Support for LCN switches."""
+
import pypck
-from homeassistant.components.switch import SwitchEntity
-from homeassistant.const import CONF_ADDRESS
+from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH, SwitchEntity
+from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES
from . import LcnEntity
-from .const import CONF_CONNECTIONS, CONF_OUTPUT, DATA_LCN, OUTPUT_PORTS
-from .helpers import get_connection
+from .const import CONF_DOMAIN_DATA, CONF_OUTPUT, OUTPUT_PORTS
+from .helpers import get_device_connection
PARALLEL_UPDATES = 0
-async def async_setup_platform(
- hass, hass_config, async_add_entities, discovery_info=None
-):
- """Set up the LCN switch platform."""
- if discovery_info is None:
- return
+def create_lcn_switch_entity(hass, entity_config, config_entry):
+ """Set up an entity for this domain."""
+ device_connection = get_device_connection(
+ hass, tuple(entity_config[CONF_ADDRESS]), config_entry
+ )
+
+ if entity_config[CONF_DOMAIN_DATA][CONF_OUTPUT] in OUTPUT_PORTS:
+ return LcnOutputSwitch(entity_config, config_entry.entry_id, device_connection)
+ # in RELAY_PORTS
+ return LcnRelaySwitch(entity_config, config_entry.entry_id, device_connection)
- devices = []
- for config in discovery_info:
- address, connection_id = config[CONF_ADDRESS]
- addr = pypck.lcn_addr.LcnAddr(*address)
- connections = hass.data[DATA_LCN][CONF_CONNECTIONS]
- connection = get_connection(connections, connection_id)
- address_connection = connection.get_address_conn(addr)
- if config[CONF_OUTPUT] in OUTPUT_PORTS:
- device = LcnOutputSwitch(config, address_connection)
- else: # in RELAY_PORTS
- device = LcnRelaySwitch(config, address_connection)
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up LCN switch entities from a config entry."""
- devices.append(device)
+ entities = []
- async_add_entities(devices)
+ for entity_config in config_entry.data[CONF_ENTITIES]:
+ if entity_config[CONF_DOMAIN] == DOMAIN_SWITCH:
+ entities.append(create_lcn_switch_entity(hass, entity_config, config_entry))
+
+ async_add_entities(entities)
class LcnOutputSwitch(LcnEntity, SwitchEntity):
"""Representation of a LCN switch for output ports."""
- def __init__(self, config, device_connection):
+ def __init__(self, config, entry_id, device_connection):
"""Initialize the LCN switch."""
- super().__init__(config, device_connection)
+ super().__init__(config, entry_id, device_connection)
- self.output = pypck.lcn_defs.OutputPort[config[CONF_OUTPUT]]
+ self.output = pypck.lcn_defs.OutputPort[config[CONF_DOMAIN_DATA][CONF_OUTPUT]]
self._is_on = None
@@ -53,6 +53,12 @@ async def async_added_to_hass(self):
if not self.device_connection.is_group:
await self.device_connection.activate_status_request_handler(self.output)
+ async def async_will_remove_from_hass(self):
+ """Run when entity will be removed from hass."""
+ await super().async_will_remove_from_hass()
+ if not self.device_connection.is_group:
+ await self.device_connection.cancel_status_request_handler(self.output)
+
@property
def is_on(self):
"""Return True if entity is on."""
@@ -87,11 +93,11 @@ def input_received(self, input_obj):
class LcnRelaySwitch(LcnEntity, SwitchEntity):
"""Representation of a LCN switch for relay ports."""
- def __init__(self, config, device_connection):
+ def __init__(self, config, entry_id, device_connection):
"""Initialize the LCN switch."""
- super().__init__(config, device_connection)
+ super().__init__(config, entry_id, device_connection)
- self.output = pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]]
+ self.output = pypck.lcn_defs.RelayPort[config[CONF_DOMAIN_DATA][CONF_OUTPUT]]
self._is_on = None
@@ -101,6 +107,12 @@ async def async_added_to_hass(self):
if not self.device_connection.is_group:
await self.device_connection.activate_status_request_handler(self.output)
+ async def async_will_remove_from_hass(self):
+ """Run when entity will be removed from hass."""
+ await super().async_will_remove_from_hass()
+ if not self.device_connection.is_group:
+ await self.device_connection.cancel_status_request_handler(self.output)
+
@property
def is_on(self):
"""Return True if entity is on."""
@@ -117,7 +129,6 @@ async def async_turn_on(self, **kwargs):
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
-
states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8
states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF
if not await self.device_connection.control_relays(states):
diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py
index ee396a7a9ee3bb..78bf7e050ef45a 100644
--- a/homeassistant/components/lg_soundbar/media_player.py
+++ b/homeassistant/components/lg_soundbar/media_player.py
@@ -29,12 +29,11 @@ class LGDevice(MediaPlayerEntity):
def __init__(self, discovery_info):
"""Initialize the LG speakers."""
- self._host = discovery_info.get("host")
- self._port = discovery_info.get("port")
- properties = discovery_info.get("properties")
- self._uuid = properties.get("UUID")
+ self._host = discovery_info["host"]
+ self._port = discovery_info["port"]
+ self._hostname = discovery_info["hostname"]
- self._name = ""
+ self._name = self._hostname.split(".")[0]
self._volume = 0
self._volume_min = 0
self._volume_max = 0
@@ -122,9 +121,9 @@ def update(self):
self._device.get_product_info()
@property
- def unique_id(self):
- """Return the device's unique ID."""
- return self._uuid
+ def should_poll(self):
+ """No polling needed."""
+ return False
@property
def name(self):
@@ -160,7 +159,8 @@ def sound_mode_list(self):
"""Return the available sound modes."""
modes = []
for equaliser in self._equalisers:
- modes.append(temescal.equalisers[equaliser])
+ if equaliser < len(temescal.equalisers):
+ modes.append(temescal.equalisers[equaliser])
return sorted(modes)
@property
@@ -175,7 +175,8 @@ def source_list(self):
"""List of available input sources."""
sources = []
for function in self._functions:
- sources.append(temescal.functions[function])
+ if function < len(temescal.functions):
+ sources.append(temescal.functions[function])
return sorted(sources)
@property
diff --git a/homeassistant/components/life360/translations/de.json b/homeassistant/components/life360/translations/de.json
index 731ebdceef7598..7e495987b45540 100644
--- a/homeassistant/components/life360/translations/de.json
+++ b/homeassistant/components/life360/translations/de.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"create_entry": {
@@ -8,6 +9,7 @@
},
"error": {
"already_configured": "Konto ist bereits konfiguriert",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"invalid_username": "Ung\u00fcltiger Benutzername",
"unknown": "Unerwarteter Fehler"
},
diff --git a/homeassistant/components/life360/translations/fr.json b/homeassistant/components/life360/translations/fr.json
index 72f56ed87844b6..cb86d8c65907cd 100644
--- a/homeassistant/components/life360/translations/fr.json
+++ b/homeassistant/components/life360/translations/fr.json
@@ -8,7 +8,7 @@
"default": "Pour d\u00e9finir les options avanc\u00e9es, voir [Documentation de Life360]( {docs_url} )."
},
"error": {
- "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9",
+ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9",
"invalid_auth": "Authentification invalide",
"invalid_username": "Nom d'utilisateur invalide",
"unknown": "Erreur inattendue"
diff --git a/homeassistant/components/life360/translations/he.json b/homeassistant/components/life360/translations/he.json
new file mode 100644
index 00000000000000..3007c0e968c1dc
--- /dev/null
+++ b/homeassistant/components/life360/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/life360/translations/hu.json b/homeassistant/components/life360/translations/hu.json
index 086e3ebf7d2c76..603efee6d9d505 100644
--- a/homeassistant/components/life360/translations/hu.json
+++ b/homeassistant/components/life360/translations/hu.json
@@ -1,15 +1,17 @@
{
"config": {
"abort": {
- "unknown": "V\u00e1ratlan hiba"
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"create_entry": {
- "default": "A speci\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1s\u00e1hoz l\u00e1sd: [Life360 dokument\u00e1ci\u00f3] ( {docs_url} )."
+ "default": "A speci\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1s\u00e1hoz l\u00e1sd: [Life360 dokument\u00e1ci\u00f3]({docs_url})."
},
"error": {
"already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"invalid_username": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v",
- "unknown": "V\u00e1ratlan hiba"
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"step": {
"user": {
diff --git a/homeassistant/components/life360/translations/id.json b/homeassistant/components/life360/translations/id.json
index 2bb7a1cca688ee..21a93366c446b8 100644
--- a/homeassistant/components/life360/translations/id.json
+++ b/homeassistant/components/life360/translations/id.json
@@ -1,7 +1,27 @@
{
"config": {
+ "abort": {
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "create_entry": {
+ "default": "Untuk mengatur opsi tingkat lanjut, baca [dokumentasi Life360]({docs_url})."
+ },
"error": {
- "invalid_username": "Nama pengguna tidak valid"
+ "already_configured": "Akun sudah dikonfigurasi",
+ "invalid_auth": "Autentikasi tidak valid",
+ "invalid_username": "Nama pengguna tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "description": "Untuk mengatur opsi tingkat lanjut, baca [dokumentasi Life360]({docs_url}).\nAnda mungkin ingin melakukannya sebelum menambahkan akun.",
+ "title": "Info Akun Life360"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/life360/translations/ko.json b/homeassistant/components/life360/translations/ko.json
index d419c5fdc020eb..d2ebd7c674ff3c 100644
--- a/homeassistant/components/life360/translations/ko.json
+++ b/homeassistant/components/life360/translations/ko.json
@@ -1,10 +1,17 @@
{
"config": {
+ "abort": {
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
"create_entry": {
"default": "\uace0\uae09 \uc635\uc158\uc744 \uc124\uc815\ud558\ub824\uba74 [Life360 \uc124\uba85\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
},
"error": {
- "invalid_username": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_username": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
diff --git a/homeassistant/components/life360/translations/nl.json b/homeassistant/components/life360/translations/nl.json
index c3b667722d0dc3..612b0d5c4f707b 100644
--- a/homeassistant/components/life360/translations/nl.json
+++ b/homeassistant/components/life360/translations/nl.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "invalid_auth": "Ongeldige authenticatie"
+ "invalid_auth": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
},
"create_entry": {
"default": "Om geavanceerde opties in te stellen, zie [Life360 documentatie]({docs_url})."
@@ -9,7 +10,8 @@
"error": {
"already_configured": "Account is al geconfigureerd",
"invalid_auth": "Ongeldige authenticatie",
- "invalid_username": "Ongeldige gebruikersnaam"
+ "invalid_username": "Ongeldige gebruikersnaam",
+ "unknown": "Onverwachte fout"
},
"step": {
"user": {
diff --git a/homeassistant/components/life360/translations/ru.json b/homeassistant/components/life360/translations/ru.json
index 2de2f63dbd64d3..b7bc71989879cb 100644
--- a/homeassistant/components/life360/translations/ru.json
+++ b/homeassistant/components/life360/translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_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."
},
"create_entry": {
@@ -9,15 +9,15 @@
},
"error": {
"already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
- "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.",
"title": "Life360"
diff --git a/homeassistant/components/life360/translations/tr.json b/homeassistant/components/life360/translations/tr.json
index 3f923c096cd056..e1e57b39737210 100644
--- a/homeassistant/components/life360/translations/tr.json
+++ b/homeassistant/components/life360/translations/tr.json
@@ -1,11 +1,22 @@
{
"config": {
"abort": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
"unknown": "Beklenmedik hata"
},
"error": {
"already_configured": "Hesap zaten konfig\u00fcre edilmi\u015fi durumda",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "invalid_username": "Ge\u00e7ersiz kullan\u0131c\u0131 ad\u0131",
"unknown": "Beklenmedik hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/life360/translations/uk.json b/homeassistant/components/life360/translations/uk.json
new file mode 100644
index 00000000000000..caecf494388971
--- /dev/null
+++ b/homeassistant/components/life360/translations/uk.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "create_entry": {
+ "default": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0438\u0445 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u044c."
+ },
+ "error": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "invalid_username": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0456\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0438\u0445 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u044c. \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0437\u0440\u043e\u0431\u0438\u0442\u0438 \u0446\u0435 \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.",
+ "title": "Life360"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py
index e775b5623d33c8..9f1c5747aa8614 100644
--- a/homeassistant/components/lifx/light.py
+++ b/homeassistant/components/lifx/light.py
@@ -4,7 +4,6 @@
from functools import partial
import logging
import math
-import sys
import aiolifx as aiolifx_module
import aiolifx_effects as aiolifx_effects_module
@@ -166,12 +165,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up LIFX from a config entry."""
- if sys.platform == "win32":
- _LOGGER.warning(
- "The lifx platform is known to not work on Windows. "
- "Consider using the lifx_legacy platform instead"
- )
-
# Priority 1: manual config
interfaces = hass.data[LIFX_DOMAIN].get(DOMAIN)
if not interfaces:
@@ -608,9 +601,13 @@ async def set_state(self, **kwargs):
if not self.is_on:
if power_off:
await self.set_power(ack, False)
- if hsbk:
+ # If fading on with color, set color immediately
+ if hsbk and power_on:
await self.set_color(ack, hsbk, kwargs)
- if power_on:
+ await self.set_power(ack, True, duration=fade)
+ elif hsbk:
+ await self.set_color(ack, hsbk, kwargs, duration=fade)
+ elif power_on:
await self.set_power(ack, True, duration=fade)
else:
if power_on:
diff --git a/homeassistant/components/lifx/translations/de.json b/homeassistant/components/lifx/translations/de.json
index f88e27ff168c0a..83eded1ddc69e7 100644
--- a/homeassistant/components/lifx/translations/de.json
+++ b/homeassistant/components/lifx/translations/de.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Keine LIFX Ger\u00e4te im Netzwerk gefunden.",
- "single_instance_allowed": "Nur eine einzige Konfiguration von LIFX ist zul\u00e4ssig."
+ "no_devices_found": "Keine LIFX Ger\u00e4te im Netzwerk gefunden",
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/lifx/translations/hu.json b/homeassistant/components/lifx/translations/hu.json
index 0b6cdb39fd4994..f706dcefa962b3 100644
--- a/homeassistant/components/lifx/translations/hu.json
+++ b/homeassistant/components/lifx/translations/hu.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Nem tal\u00e1lhat\u00f3k LIFX eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton.",
- "single_instance_allowed": "Csak egyetlen LIFX konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/lifx/translations/id.json b/homeassistant/components/lifx/translations/id.json
new file mode 100644
index 00000000000000..03b2b387c6f12b
--- /dev/null
+++ b/homeassistant/components/lifx/translations/id.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "step": {
+ "confirm": {
+ "description": "Ingin menyiapkan LIFX?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lifx/translations/ko.json b/homeassistant/components/lifx/translations/ko.json
index 040ac405e2d1b9..4d388cbeda289d 100644
--- a/homeassistant/components/lifx/translations/ko.json
+++ b/homeassistant/components/lifx/translations/ko.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "no_devices_found": "LIFX \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
- "single_instance_allowed": "\ud558\ub098\uc758 LIFX \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"step": {
"confirm": {
- "description": "LIFX \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ "description": "LIFX\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
}
}
}
diff --git a/homeassistant/components/lifx/translations/nl.json b/homeassistant/components/lifx/translations/nl.json
index 60efcdffa463ca..0e0a6190f0d783 100644
--- a/homeassistant/components/lifx/translations/nl.json
+++ b/homeassistant/components/lifx/translations/nl.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Geen LIFX-apparaten gevonden op het netwerk.",
- "single_instance_allowed": "Slechts een enkele configuratie van LIFX is mogelijk."
+ "no_devices_found": "Geen apparaten gevonden op het netwerk",
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/lifx/translations/tr.json b/homeassistant/components/lifx/translations/tr.json
new file mode 100644
index 00000000000000..fc7532a1e3411d
--- /dev/null
+++ b/homeassistant/components/lifx/translations/tr.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "step": {
+ "confirm": {
+ "description": "LIFX'i kurmak istiyor musunuz?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lifx/translations/uk.json b/homeassistant/components/lifx/translations/uk.json
new file mode 100644
index 00000000000000..8c32e79533dc00
--- /dev/null
+++ b/homeassistant/components/lifx/translations/uk.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 LIFX?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lifx_legacy/light.py b/homeassistant/components/lifx_legacy/light.py
index f0ed9105b99cab..795f3e177932bb 100644
--- a/homeassistant/components/lifx_legacy/light.py
+++ b/homeassistant/components/lifx_legacy/light.py
@@ -46,13 +46,22 @@
SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR | SUPPORT_TRANSITION
)
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {vol.Optional(CONF_SERVER): cv.string, vol.Optional(CONF_BROADCAST): cv.string}
+PLATFORM_SCHEMA = vol.All(
+ cv.deprecated(CONF_SERVER),
+ cv.deprecated(CONF_BROADCAST),
+ PLATFORM_SCHEMA.extend(
+ {vol.Optional(CONF_SERVER): cv.string, vol.Optional(CONF_BROADCAST): cv.string}
+ ),
)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the LIFX platform."""
+ _LOGGER.warning(
+ "The LIFX Legacy platform is deprecated and will be removed in "
+ "Home Assistant Core 2021.6.0; Use the LIFX integration instead"
+ )
+
server_addr = config.get(CONF_SERVER)
broadcast_addr = config.get(CONF_BROADCAST)
diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py
index c46b7568b59e13..fe9a38d12b4f71 100644
--- a/homeassistant/components/light/__init__.py
+++ b/homeassistant/components/light/__init__.py
@@ -1,10 +1,12 @@
"""Provides functionality to interact with lights."""
+from __future__ import annotations
+
import csv
import dataclasses
from datetime import timedelta
import logging
import os
-from typing import Dict, List, Optional, Tuple, cast
+from typing import cast, final
import voluptuous as vol
@@ -14,7 +16,7 @@
SERVICE_TURN_ON,
STATE_ON,
)
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
@@ -23,7 +25,6 @@
)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
-from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.loader import bind_hass
import homeassistant.util.color as color_util
@@ -36,19 +37,63 @@
ENTITY_ID_FORMAT = DOMAIN + ".{}"
# Bitfield of features supported by the light entity
-SUPPORT_BRIGHTNESS = 1
-SUPPORT_COLOR_TEMP = 2
+SUPPORT_BRIGHTNESS = 1 # Deprecated, replaced by color modes
+SUPPORT_COLOR_TEMP = 2 # Deprecated, replaced by color modes
SUPPORT_EFFECT = 4
SUPPORT_FLASH = 8
-SUPPORT_COLOR = 16
+SUPPORT_COLOR = 16 # Deprecated, replaced by color modes
SUPPORT_TRANSITION = 32
-SUPPORT_WHITE_VALUE = 128
+SUPPORT_WHITE_VALUE = 128 # Deprecated, replaced by color modes
+
+# Color mode of the light
+ATTR_COLOR_MODE = "color_mode"
+# List of color modes supported by the light
+ATTR_SUPPORTED_COLOR_MODES = "supported_color_modes"
+# Possible color modes
+COLOR_MODE_UNKNOWN = "unknown" # Ambiguous color mode
+COLOR_MODE_ONOFF = "onoff" # Must be the only supported mode
+COLOR_MODE_BRIGHTNESS = "brightness" # Must be the only supported mode
+COLOR_MODE_COLOR_TEMP = "color_temp"
+COLOR_MODE_HS = "hs"
+COLOR_MODE_XY = "xy"
+COLOR_MODE_RGB = "rgb"
+COLOR_MODE_RGBW = "rgbw"
+COLOR_MODE_RGBWW = "rgbww"
+
+VALID_COLOR_MODES = {
+ COLOR_MODE_ONOFF,
+ COLOR_MODE_BRIGHTNESS,
+ COLOR_MODE_COLOR_TEMP,
+ COLOR_MODE_HS,
+ COLOR_MODE_XY,
+ COLOR_MODE_RGB,
+ COLOR_MODE_RGBW,
+ COLOR_MODE_RGBWW,
+}
+COLOR_MODES_BRIGHTNESS = VALID_COLOR_MODES - {COLOR_MODE_ONOFF}
+COLOR_MODES_COLOR = {COLOR_MODE_HS, COLOR_MODE_RGB, COLOR_MODE_XY}
+
+
+def valid_supported_color_modes(color_modes):
+ """Validate the given color modes."""
+ color_modes = set(color_modes)
+ if (
+ not color_modes
+ or COLOR_MODE_UNKNOWN in color_modes
+ or (COLOR_MODE_BRIGHTNESS in color_modes and len(color_modes) > 1)
+ or (COLOR_MODE_ONOFF in color_modes and len(color_modes) > 1)
+ ):
+ raise vol.Error(f"Invalid supported_color_modes {sorted(color_modes)}")
+ return color_modes
+
# Float that represents transition time in seconds to make change.
ATTR_TRANSITION = "transition"
# Lists holding color values
ATTR_RGB_COLOR = "rgb_color"
+ATTR_RGBW_COLOR = "rgbw_color"
+ATTR_RGBWW_COLOR = "rgbww_color"
ATTR_XY_COLOR = "xy_color"
ATTR_HS_COLOR = "hs_color"
ATTR_COLOR_TEMP = "color_temp"
@@ -102,7 +147,13 @@
vol.Exclusive(ATTR_BRIGHTNESS_STEP_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP_PCT,
vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string,
vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All(
- vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple)
+ vol.ExactSequence((cv.byte,) * 3), vol.Coerce(tuple)
+ ),
+ vol.Exclusive(ATTR_RGBW_COLOR, COLOR_GROUP): vol.All(
+ vol.ExactSequence((cv.byte,) * 4), vol.Coerce(tuple)
+ ),
+ vol.Exclusive(ATTR_RGBWW_COLOR, COLOR_GROUP): vol.All(
+ vol.ExactSequence((cv.byte,) * 5), vol.Coerce(tuple)
),
vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All(
vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple)
@@ -164,14 +215,6 @@ def preprocess_turn_on_alternatives(hass, params):
if brightness_pct is not None:
params[ATTR_BRIGHTNESS] = round(255 * brightness_pct / 100)
- xy_color = params.pop(ATTR_XY_COLOR, None)
- if xy_color is not None:
- params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color)
-
- rgb_color = params.pop(ATTR_RGB_COLOR, None)
- if rgb_color is not None:
- params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
-
def filter_turn_off_params(params):
"""Filter out params not used in turn off."""
@@ -205,7 +248,7 @@ async def async_handle_light_on_service(light, call):
If brightness is set to 0, this service will turn the light off.
"""
- params = call.data["params"]
+ params = dict(call.data["params"])
# Only process params once we processed brightness step
if params and (
@@ -226,6 +269,52 @@ async def async_handle_light_on_service(light, call):
if ATTR_PROFILE not in params:
profiles.apply_default(light.entity_id, params)
+ supported_color_modes = light.supported_color_modes
+ # Backwards compatibility: if an RGBWW color is specified, convert to RGB + W
+ # for legacy lights
+ if ATTR_RGBW_COLOR in params:
+ legacy_supported_color_modes = (
+ light._light_internal_supported_color_modes # pylint: disable=protected-access
+ )
+ if (
+ COLOR_MODE_RGBW in legacy_supported_color_modes
+ and not supported_color_modes
+ ):
+ rgbw_color = params.pop(ATTR_RGBW_COLOR)
+ params[ATTR_RGB_COLOR] = rgbw_color[0:3]
+ params[ATTR_WHITE_VALUE] = rgbw_color[3]
+
+ # 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
+ 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)
+ elif (xy_color := params.pop(ATTR_XY_COLOR, None)) is not None:
+ params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color)
+ elif ATTR_HS_COLOR in params and COLOR_MODE_HS not in supported_color_modes:
+ hs_color = params.pop(ATTR_HS_COLOR)
+ if COLOR_MODE_RGB in supported_color_modes:
+ params[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color)
+ elif COLOR_MODE_XY in supported_color_modes:
+ params[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color)
+ elif ATTR_RGB_COLOR in params and COLOR_MODE_RGB not in supported_color_modes:
+ rgb_color = params.pop(ATTR_RGB_COLOR)
+ if COLOR_MODE_HS in supported_color_modes:
+ params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
+ elif COLOR_MODE_XY in supported_color_modes:
+ params[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color)
+ elif ATTR_XY_COLOR in params and COLOR_MODE_XY not in supported_color_modes:
+ xy_color = params.pop(ATTR_XY_COLOR)
+ if COLOR_MODE_HS in supported_color_modes:
+ params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color)
+ elif COLOR_MODE_RGB in supported_color_modes:
+ params[ATTR_RGB_COLOR] = color_util.color_xy_to_RGB(*xy_color)
+
+ # Remove deprecated white value if the light supports color mode
+ if supported_color_modes:
+ params.pop(ATTR_WHITE_VALUE, None)
+
# Zero brightness: Light will be turned off
if params.get(ATTR_BRIGHTNESS) == 0:
await light.async_turn_off(**filter_turn_off_params(params))
@@ -288,11 +377,11 @@ class Profile:
"""Representation of a profile."""
name: str
- color_x: Optional[float] = dataclasses.field(repr=False)
- color_y: Optional[float] = dataclasses.field(repr=False)
- brightness: Optional[int]
- transition: Optional[int] = None
- hs_color: Optional[Tuple[float, float]] = dataclasses.field(init=False)
+ color_x: float | None = dataclasses.field(repr=False)
+ color_y: float | None = dataclasses.field(repr=False)
+ brightness: int | None
+ transition: int | None = None
+ hs_color: tuple[float, float] | None = dataclasses.field(init=False)
SCHEMA = vol.Schema( # pylint: disable=invalid-name
vol.Any(
@@ -327,7 +416,7 @@ def __post_init__(self) -> None:
)
@classmethod
- def from_csv_row(cls, csv_row: List[str]) -> "Profile":
+ def from_csv_row(cls, csv_row: list[str]) -> Profile:
"""Create profile from a CSV row tuple."""
return cls(*cls.SCHEMA(csv_row))
@@ -335,12 +424,12 @@ def from_csv_row(cls, csv_row: List[str]) -> "Profile":
class Profiles:
"""Representation of available color profiles."""
- def __init__(self, hass: HomeAssistantType):
+ def __init__(self, hass: HomeAssistant):
"""Initialize profiles."""
self.hass = hass
- self.data: Dict[str, Profile] = {}
+ self.data: dict[str, Profile] = {}
- def _load_profile_data(self) -> Dict[str, Profile]:
+ def _load_profile_data(self) -> dict[str, Profile]:
"""Load built-in profiles and custom profiles."""
profile_paths = [
os.path.join(os.path.dirname(__file__), LIGHT_PROFILES_FILE),
@@ -377,7 +466,7 @@ async def async_initialize(self) -> None:
self.data = await self.hass.async_add_executor_job(self._load_profile_data)
@callback
- def apply_default(self, entity_id: str, params: Dict) -> None:
+ def apply_default(self, entity_id: str, params: dict) -> None:
"""Return the default turn-on profile for the given light."""
for _entity_id in (entity_id, "group.all_lights"):
name = f"{_entity_id}.default"
@@ -386,7 +475,7 @@ def apply_default(self, entity_id: str, params: Dict) -> None:
return
@callback
- def apply_profile(self, name: str, params: Dict) -> None:
+ def apply_profile(self, name: str, params: dict) -> None:
"""Apply a profile."""
profile = self.data.get(name)
@@ -402,49 +491,121 @@ def apply_profile(self, name: str, params: Dict) -> None:
class LightEntity(ToggleEntity):
- """Representation of a light."""
+ """Base class for light entities."""
@property
- def brightness(self):
+ def brightness(self) -> int | None:
"""Return the brightness of this light between 0..255."""
return None
@property
- def hs_color(self):
+ def color_mode(self) -> str | None:
+ """Return the color mode of the light."""
+ return None
+
+ @property
+ def _light_internal_color_mode(self) -> str:
+ """Return the color mode of the light with backwards compatibility."""
+ color_mode = self.color_mode
+
+ if color_mode is None:
+ # Backwards compatibility for color_mode added in 2021.4
+ # Add warning in 2021.6, remove in 2021.10
+ supported = self._light_internal_supported_color_modes
+
+ if (
+ COLOR_MODE_RGBW in supported
+ and self.white_value is not None
+ and self.hs_color is not None
+ ):
+ return COLOR_MODE_RGBW
+ if COLOR_MODE_HS in supported and self.hs_color is not None:
+ return COLOR_MODE_HS
+ if COLOR_MODE_COLOR_TEMP in supported and self.color_temp is not None:
+ return COLOR_MODE_COLOR_TEMP
+ if COLOR_MODE_BRIGHTNESS in supported and self.brightness is not None:
+ return COLOR_MODE_BRIGHTNESS
+ if COLOR_MODE_ONOFF in supported:
+ return COLOR_MODE_ONOFF
+ return COLOR_MODE_UNKNOWN
+
+ return color_mode
+
+ @property
+ def hs_color(self) -> tuple[float, float] | None:
"""Return the hue and saturation color value [float, float]."""
return None
@property
- def color_temp(self):
+ def xy_color(self) -> tuple[float, float] | None:
+ """Return the xy color value [float, float]."""
+ return None
+
+ @property
+ def rgb_color(self) -> tuple[int, int, int] | None:
+ """Return the rgb color value [int, int, int]."""
+ return None
+
+ @property
+ def rgbw_color(self) -> tuple[int, int, int, int] | None:
+ """Return the rgbw color value [int, int, int, int]."""
+ return None
+
+ @property
+ def _light_internal_rgbw_color(self) -> tuple[int, int, int, int] | None:
+ """Return the rgbw color value [int, int, int, int]."""
+ rgbw_color = self.rgbw_color
+ if (
+ rgbw_color is None
+ and self.hs_color is not None
+ and self.white_value is not None
+ ):
+ # Backwards compatibility for rgbw_color added in 2021.4
+ # Add warning in 2021.6, remove in 2021.10
+ r, g, b = color_util.color_hs_to_RGB( # pylint: disable=invalid-name
+ *self.hs_color
+ )
+ w = self.white_value # pylint: disable=invalid-name
+ rgbw_color = (r, g, b, w)
+
+ return rgbw_color
+
+ @property
+ def rgbww_color(self) -> tuple[int, int, int, int, int] | None:
+ """Return the rgbww color value [int, int, int, int, int]."""
+ return None
+
+ @property
+ def color_temp(self) -> int | None:
"""Return the CT color value in mireds."""
return None
@property
- def min_mireds(self):
+ def min_mireds(self) -> int:
"""Return the coldest color_temp that this light supports."""
# Default to the Philips Hue value that HA has always assumed
# https://developers.meethue.com/documentation/core-concepts
return 153
@property
- def max_mireds(self):
+ def max_mireds(self) -> int:
"""Return the warmest color_temp that this light supports."""
# Default to the Philips Hue value that HA has always assumed
# https://developers.meethue.com/documentation/core-concepts
return 500
@property
- def white_value(self):
+ def white_value(self) -> int | None:
"""Return the white value of this light between 0..255."""
return None
@property
- def effect_list(self):
+ def effect_list(self) -> list[str] | None:
"""Return the list of supported effects."""
return None
@property
- def effect(self):
+ def effect(self) -> str | None:
"""Return the current effect."""
return None
@@ -461,8 +622,32 @@ def capability_attributes(self):
if supported_features & SUPPORT_EFFECT:
data[ATTR_EFFECT_LIST] = self.effect_list
+ data[ATTR_SUPPORTED_COLOR_MODES] = sorted(
+ self._light_internal_supported_color_modes
+ )
+
return data
+ def _light_internal_convert_color(self, color_mode: str) -> dict:
+ data: dict[str, tuple] = {}
+ if color_mode == COLOR_MODE_HS and self.hs_color:
+ hs_color = self.hs_color
+ data[ATTR_HS_COLOR] = (round(hs_color[0], 3), round(hs_color[1], 3))
+ data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color)
+ data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color)
+ elif color_mode == COLOR_MODE_XY and self.xy_color:
+ xy_color = self.xy_color
+ data[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color)
+ data[ATTR_RGB_COLOR] = color_util.color_xy_to_RGB(*xy_color)
+ data[ATTR_XY_COLOR] = (round(xy_color[0], 6), round(xy_color[1], 6))
+ elif color_mode == COLOR_MODE_RGB and self.rgb_color:
+ rgb_color = self.rgb_color
+ data[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
+ data[ATTR_RGB_COLOR] = tuple(int(x) for x in rgb_color[0:3])
+ data[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color)
+ return data
+
+ @final
@property
def state_attributes(self):
"""Return state attributes."""
@@ -471,21 +656,49 @@ def state_attributes(self):
data = {}
supported_features = self.supported_features
+ color_mode = self._light_internal_color_mode
+
+ if color_mode not in self._light_internal_supported_color_modes:
+ # Increase severity to warning in 2021.6, reject in 2021.10
+ _LOGGER.debug(
+ "%s: set to unsupported color_mode: %s, supported_color_modes: %s",
+ self.entity_id,
+ color_mode,
+ self._light_internal_supported_color_modes,
+ )
+
+ data[ATTR_COLOR_MODE] = color_mode
- if supported_features & SUPPORT_BRIGHTNESS:
+ if color_mode in COLOR_MODES_BRIGHTNESS:
+ data[ATTR_BRIGHTNESS] = self.brightness
+ elif supported_features & SUPPORT_BRIGHTNESS:
+ # Backwards compatibility for ambiguous / incomplete states
+ # Add warning in 2021.6, remove in 2021.10
data[ATTR_BRIGHTNESS] = self.brightness
- if supported_features & SUPPORT_COLOR_TEMP:
+ if color_mode == COLOR_MODE_COLOR_TEMP:
data[ATTR_COLOR_TEMP] = self.color_temp
- if supported_features & SUPPORT_COLOR and self.hs_color:
- hs_color = self.hs_color
- data[ATTR_HS_COLOR] = (round(hs_color[0], 3), round(hs_color[1], 3))
- data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color)
- data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color)
+ if color_mode in COLOR_MODES_COLOR:
+ data.update(self._light_internal_convert_color(color_mode))
+
+ if color_mode == COLOR_MODE_RGBW:
+ data[ATTR_RGBW_COLOR] = self._light_internal_rgbw_color
+
+ if color_mode == COLOR_MODE_RGBWW:
+ data[ATTR_RGBWW_COLOR] = self.rgbww_color
- if supported_features & SUPPORT_WHITE_VALUE:
+ if supported_features & SUPPORT_COLOR_TEMP and not self.supported_color_modes:
+ # Backwards compatibility
+ # Add warning in 2021.6, remove in 2021.10
+ data[ATTR_COLOR_TEMP] = self.color_temp
+
+ if supported_features & SUPPORT_WHITE_VALUE and not self.supported_color_modes:
+ # Backwards compatibility
+ # Add warning in 2021.6, remove in 2021.10
data[ATTR_WHITE_VALUE] = self.white_value
+ if self.hs_color is not None:
+ data.update(self._light_internal_convert_color(COLOR_MODE_HS))
if supported_features & SUPPORT_EFFECT:
data[ATTR_EFFECT] = self.effect
@@ -493,7 +706,37 @@ def state_attributes(self):
return {key: val for key, val in data.items() if val is not None}
@property
- def supported_features(self):
+ def _light_internal_supported_color_modes(self) -> set:
+ """Calculate supported color modes with backwards compatibility."""
+ supported_color_modes = self.supported_color_modes
+
+ if supported_color_modes is None:
+ # Backwards compatibility for supported_color_modes added in 2021.4
+ # Add warning in 2021.6, remove in 2021.10
+ supported_features = self.supported_features
+ supported_color_modes = set()
+
+ if supported_features & SUPPORT_COLOR_TEMP:
+ supported_color_modes.add(COLOR_MODE_COLOR_TEMP)
+ if supported_features & SUPPORT_COLOR:
+ supported_color_modes.add(COLOR_MODE_HS)
+ if supported_features & SUPPORT_WHITE_VALUE:
+ supported_color_modes.add(COLOR_MODE_RGBW)
+ if supported_features & SUPPORT_BRIGHTNESS and not supported_color_modes:
+ supported_color_modes = {COLOR_MODE_BRIGHTNESS}
+
+ if not supported_color_modes:
+ supported_color_modes = {COLOR_MODE_ONOFF}
+
+ return supported_color_modes
+
+ @property
+ def supported_color_modes(self) -> set | None:
+ """Flag supported color modes."""
+ return None
+
+ @property
+ def supported_features(self) -> int:
"""Flag supported features."""
return 0
diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py
index d499bc0c2a2738..4c37647f1683b3 100644
--- a/homeassistant/components/light/device_action.py
+++ b/homeassistant/components/light/device_action.py
@@ -1,5 +1,5 @@
"""Provides device actions for lights."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -78,7 +78,7 @@ async def async_call_action_from_config(
)
-async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device actions."""
actions = await toggle_entity.async_get_actions(hass, device_id, DOMAIN)
diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py
index 1d9323907f2048..7396ddeea3165c 100644
--- a/homeassistant/components/light/device_condition.py
+++ b/homeassistant/components/light/device_condition.py
@@ -1,5 +1,5 @@
"""Provides device conditions for lights."""
-from typing import Dict, List
+from __future__ import annotations
import voluptuous as vol
@@ -28,7 +28,7 @@ def async_condition_from_config(
async def async_get_conditions(
hass: HomeAssistant, device_id: str
-) -> List[Dict[str, str]]:
+) -> list[dict[str, str]]:
"""List device conditions."""
return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN)
diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py
index 066d1f4c0205b7..e1b1412483167e 100644
--- a/homeassistant/components/light/device_trigger.py
+++ b/homeassistant/components/light/device_trigger.py
@@ -1,5 +1,5 @@
"""Provides device trigger for lights."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -28,7 +28,7 @@ async def async_attach_trigger(
)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers."""
return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN)
diff --git a/homeassistant/components/light/group.py b/homeassistant/components/light/group.py
index 1636054663dc69..234883ffd5a041 100644
--- a/homeassistant/components/light/group.py
+++ b/homeassistant/components/light/group.py
@@ -3,13 +3,12 @@
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OFF, STATE_ON
-from homeassistant.core import callback
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import HomeAssistant, callback
@callback
def async_describe_on_off_states(
- hass: HomeAssistantType, registry: GroupIntegrationRegistry
+ hass: HomeAssistant, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_ON}, STATE_OFF)
diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py
index a7939beb91e16b..68fac8aa5c96cf 100644
--- a/homeassistant/components/light/reproduce_state.py
+++ b/homeassistant/components/light/reproduce_state.py
@@ -1,8 +1,10 @@
"""Reproduce an Light state."""
+from __future__ import annotations
+
import asyncio
import logging
from types import MappingProxyType
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -11,12 +13,12 @@
STATE_OFF,
STATE_ON,
)
-from homeassistant.core import Context, State
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import Context, HomeAssistant, State
from . import (
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
+ ATTR_COLOR_MODE,
ATTR_COLOR_NAME,
ATTR_COLOR_TEMP,
ATTR_EFFECT,
@@ -25,9 +27,18 @@
ATTR_KELVIN,
ATTR_PROFILE,
ATTR_RGB_COLOR,
+ ATTR_RGBW_COLOR,
+ ATTR_RGBWW_COLOR,
ATTR_TRANSITION,
ATTR_WHITE_VALUE,
ATTR_XY_COLOR,
+ COLOR_MODE_COLOR_TEMP,
+ COLOR_MODE_HS,
+ COLOR_MODE_RGB,
+ COLOR_MODE_RGBW,
+ COLOR_MODE_RGBWW,
+ COLOR_MODE_UNKNOWN,
+ COLOR_MODE_XY,
DOMAIN,
)
@@ -48,6 +59,8 @@
ATTR_HS_COLOR,
ATTR_COLOR_TEMP,
ATTR_RGB_COLOR,
+ ATTR_RGBW_COLOR,
+ ATTR_RGBWW_COLOR,
ATTR_XY_COLOR,
# The following color attributes are deprecated
ATTR_PROFILE,
@@ -55,6 +68,15 @@
ATTR_KELVIN,
]
+COLOR_MODE_TO_ATTRIBUTE = {
+ COLOR_MODE_COLOR_TEMP: ATTR_COLOR_TEMP,
+ COLOR_MODE_HS: ATTR_HS_COLOR,
+ COLOR_MODE_RGB: ATTR_RGB_COLOR,
+ COLOR_MODE_RGBW: ATTR_RGBW_COLOR,
+ COLOR_MODE_RGBWW: ATTR_RGBWW_COLOR,
+ COLOR_MODE_XY: ATTR_XY_COLOR,
+}
+
DEPRECATED_GROUP = [
ATTR_BRIGHTNESS_PCT,
ATTR_COLOR_NAME,
@@ -71,11 +93,11 @@
async def _async_reproduce_state(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -102,7 +124,7 @@ async def _async_reproduce_state(
):
return
- service_data: Dict[str, Any] = {ATTR_ENTITY_ID: state.entity_id}
+ service_data: dict[str, Any] = {ATTR_ENTITY_ID: state.entity_id}
if reproduce_options is not None and ATTR_TRANSITION in reproduce_options:
service_data[ATTR_TRANSITION] = reproduce_options[ATTR_TRANSITION]
@@ -114,11 +136,29 @@ async def _async_reproduce_state(
if attr in state.attributes:
service_data[attr] = state.attributes[attr]
- for color_attr in COLOR_GROUP:
- # Choose the first color that is specified
- if color_attr in state.attributes:
+ if (
+ state.attributes.get(ATTR_COLOR_MODE, COLOR_MODE_UNKNOWN)
+ != COLOR_MODE_UNKNOWN
+ ):
+ # Remove deprecated white value if we got a valid color mode
+ service_data.pop(ATTR_WHITE_VALUE, None)
+ color_mode = state.attributes[ATTR_COLOR_MODE]
+ if color_attr := COLOR_MODE_TO_ATTRIBUTE.get(color_mode):
+ if color_attr not in state.attributes:
+ _LOGGER.warning(
+ "Color mode %s specified but attribute %s missing for: %s",
+ color_mode,
+ color_attr,
+ state.entity_id,
+ )
+ return
service_data[color_attr] = state.attributes[color_attr]
- break
+ else:
+ # Fall back to Choosing the first color that is specified
+ for color_attr in COLOR_GROUP:
+ if color_attr in state.attributes:
+ service_data[color_attr] = state.attributes[color_attr]
+ break
elif state.state == STATE_OFF:
service = SERVICE_TURN_OFF
@@ -129,11 +169,11 @@ async def _async_reproduce_state(
async def async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Light states."""
await asyncio.gather(
diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml
index a2b71f5632bb25..fe96f3a6777765 100644
--- a/homeassistant/components/light/services.yaml
+++ b/homeassistant/components/light/services.yaml
@@ -1,127 +1,634 @@
# Describes the format for available light services
turn_on:
- description: Turn a light on.
+ name: Turn on
+ description: >
+ Turn on one or more lights and adjust properties of the light, even when
+ they are turned on already.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to turn on
- example: "light.kitchen"
transition:
- description: Duration in seconds it takes to get to next state
+ name: Transition
+ description: Duration in seconds it takes to get to next state.
example: 60
+ selector:
+ number:
+ min: 0
+ max: 300
+ step: 1
+ unit_of_measurement: seconds
+ mode: slider
rgb_color:
+ name: RGB-color
description: Color for the light in RGB-format.
+ advanced: true
example: "[255, 100, 100]"
+ selector:
+ object:
color_name:
+ name: Color name
description: A human readable color name.
+ advanced: true
example: "red"
+ selector:
+ select:
+ options:
+ - "homeassistant"
+ - "aliceblue"
+ - "antiquewhite"
+ - "aqua"
+ - "aquamarine"
+ - "azure"
+ - "beige"
+ - "bisque"
+ - "black"
+ - "blanchedalmond"
+ - "blue"
+ - "blueviolet"
+ - "brown"
+ - "burlywood"
+ - "cadetblue"
+ - "chartreuse"
+ - "chocolate"
+ - "coral"
+ - "cornflowerblue"
+ - "cornsilk"
+ - "crimson"
+ - "cyan"
+ - "darkblue"
+ - "darkcyan"
+ - "darkgoldenrod"
+ - "darkgray"
+ - "darkgreen"
+ - "darkgrey"
+ - "darkkhaki"
+ - "darkmagenta"
+ - "darkolivegreen"
+ - "darkorange"
+ - "darkorchid"
+ - "darkred"
+ - "darksalmon"
+ - "darkseagreen"
+ - "darkslateblue"
+ - "darkslategray"
+ - "darkslategrey"
+ - "darkturquoise"
+ - "darkviolet"
+ - "deeppink"
+ - "deepskyblue"
+ - "dimgray"
+ - "dimgrey"
+ - "dodgerblue"
+ - "firebrick"
+ - "floralwhite"
+ - "forestgreen"
+ - "fuchsia"
+ - "gainsboro"
+ - "ghostwhite"
+ - "gold"
+ - "goldenrod"
+ - "gray"
+ - "green"
+ - "greenyellow"
+ - "grey"
+ - "honeydew"
+ - "hotpink"
+ - "indianred"
+ - "indigo"
+ - "ivory"
+ - "khaki"
+ - "lavender"
+ - "lavenderblush"
+ - "lawngreen"
+ - "lemonchiffon"
+ - "lightblue"
+ - "lightcoral"
+ - "lightcyan"
+ - "lightgoldenrodyellow"
+ - "lightgray"
+ - "lightgreen"
+ - "lightgrey"
+ - "lightpink"
+ - "lightsalmon"
+ - "lightseagreen"
+ - "lightskyblue"
+ - "lightslategray"
+ - "lightslategrey"
+ - "lightsteelblue"
+ - "lightyellow"
+ - "lime"
+ - "limegreen"
+ - "linen"
+ - "magenta"
+ - "maroon"
+ - "mediumaquamarine"
+ - "mediumblue"
+ - "mediumorchid"
+ - "mediumpurple"
+ - "mediumseagreen"
+ - "mediumslateblue"
+ - "mediumspringgreen"
+ - "mediumturquoise"
+ - "mediumvioletred"
+ - "midnightblue"
+ - "mintcream"
+ - "mistyrose"
+ - "moccasin"
+ - "navajowhite"
+ - "navy"
+ - "navyblue"
+ - "oldlace"
+ - "olive"
+ - "olivedrab"
+ - "orange"
+ - "orangered"
+ - "orchid"
+ - "palegoldenrod"
+ - "palegreen"
+ - "paleturquoise"
+ - "palevioletred"
+ - "papayawhip"
+ - "peachpuff"
+ - "peru"
+ - "pink"
+ - "plum"
+ - "powderblue"
+ - "purple"
+ - "red"
+ - "rosybrown"
+ - "royalblue"
+ - "saddlebrown"
+ - "salmon"
+ - "sandybrown"
+ - "seagreen"
+ - "seashell"
+ - "sienna"
+ - "silver"
+ - "skyblue"
+ - "slateblue"
+ - "slategray"
+ - "slategrey"
+ - "snow"
+ - "springgreen"
+ - "steelblue"
+ - "tan"
+ - "teal"
+ - "thistle"
+ - "tomato"
+ - "turquoise"
+ - "violet"
+ - "wheat"
+ - "white"
+ - "whitesmoke"
+ - "yellow"
+ - "yellowgreen"
hs_color:
- description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100.
+ name: Hue/Sat color
+ description:
+ Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100.
+ advanced: true
example: "[300, 70]"
+ selector:
+ object:
xy_color:
+ name: XY-color
description: Color for the light in XY-format.
+ advanced: true
example: "[0.52, 0.43]"
+ selector:
+ object:
color_temp:
+ name: Color temperature (mireds)
description: Color temperature for the light in mireds.
+ advanced: true
example: 250
+ selector:
+ number:
+ min: 153
+ max: 500
+ step: 1
+ unit_of_measurement: mireds
+ mode: slider
kelvin:
+ name: Color temperature (Kelvin)
description: Color temperature for the light in Kelvin.
+ advanced: true
example: 4000
+ selector:
+ number:
+ min: 2000
+ max: 6500
+ step: 100
+ unit_of_measurement: K
+ mode: slider
white_value:
+ name: White level
description: Number between 0..255 indicating level of white.
+ advanced: true
example: "250"
+ selector:
+ number:
+ min: 0
+ max: 255
+ step: 1
+ mode: slider
brightness:
- description: Number between 0..255 indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light.
+ name: Brightness value
+ description:
+ Number between 0..255 indicating brightness, where 0 turns the light
+ off, 1 is the minimum brightness and 255 is the maximum brightness
+ supported by the light.
+ advanced: true
example: 120
+ selector:
+ number:
+ min: 0
+ max: 255
+ step: 1
+ mode: slider
brightness_pct:
- description: Number between 0..100 indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness supported by the light.
+ name: Brightness
+ description:
+ Number between 0..100 indicating percentage of full brightness, where 0
+ turns the light off, 1 is the minimum brightness and 100 is the maximum
+ brightness supported by the light.
example: 47
+ selector:
+ number:
+ min: 0
+ max: 100
+ step: 1
+ unit_of_measurement: "%"
+ mode: slider
brightness_step:
+ name: Brightness step value
description: Change brightness by an amount. Should be between -255..255.
+ advanced: true
example: -25.5
+ selector:
+ number:
+ min: -225
+ max: 255
+ step: 1
+ mode: slider
brightness_step_pct:
- description: Change brightness by a percentage. Should be between -100..100.
+ name: Brightness step
+ description:
+ Change brightness by a percentage. Should be between -100..100.
example: -10
+ selector:
+ number:
+ min: -100
+ max: 100
+ step: 1
+ unit_of_measurement: "%"
+ mode: slider
profile:
+ name: Profile
description: Name of a light profile to use.
+ advanced: true
example: relax
+ selector:
+ text:
flash:
+ name: Flash
description: If the light should flash. Valid values are short and long.
+ advanced: true
example: short
values:
- short
- long
+ selector:
+ select:
+ options:
+ - long
+ - short
effect:
+ name: Effect
description: Light effect.
example: random
values:
- colorloop
- random
+ selector:
+ text:
turn_off:
- description: Turn a light off.
+ name: Turn off
+ description: Turns off one or more lights.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to turn off.
- example: "light.kitchen"
transition:
+ name: Transition
description: Duration in seconds it takes to get to next state.
example: 60
+ selector:
+ number:
+ min: 0
+ max: 300
+ step: 1
+ unit_of_measurement: seconds
+ mode: slider
flash:
+ name: Flash
description: If the light should flash. Valid values are short and long.
+ advanced: true
example: short
values:
- short
- long
+ selector:
+ select:
+ options:
+ - long
+ - short
toggle:
- description: Toggles a light.
+ name: Toggle
+ description: >
+ Toggles one or more lights, from on to off, or, off to on, based on their
+ current state.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to turn on
- example: "light.kitchen"
transition:
- description: Duration in seconds it takes to get to next state
+ name: Transition
+ description: Duration in seconds it takes to get to next state.
example: 60
+ selector:
+ number:
+ min: 0
+ max: 300
+ step: 1
+ unit_of_measurement: seconds
+ mode: slider
rgb_color:
+ name: RGB-color
description: Color for the light in RGB-format.
+ advanced: true
example: "[255, 100, 100]"
+ selector:
+ object:
color_name:
+ name: Color name
description: A human readable color name.
+ advanced: true
example: "red"
+ selector:
+ select:
+ options:
+ - "homeassistant"
+ - "aliceblue"
+ - "antiquewhite"
+ - "aqua"
+ - "aquamarine"
+ - "azure"
+ - "beige"
+ - "bisque"
+ - "black"
+ - "blanchedalmond"
+ - "blue"
+ - "blueviolet"
+ - "brown"
+ - "burlywood"
+ - "cadetblue"
+ - "chartreuse"
+ - "chocolate"
+ - "coral"
+ - "cornflowerblue"
+ - "cornsilk"
+ - "crimson"
+ - "cyan"
+ - "darkblue"
+ - "darkcyan"
+ - "darkgoldenrod"
+ - "darkgray"
+ - "darkgreen"
+ - "darkgrey"
+ - "darkkhaki"
+ - "darkmagenta"
+ - "darkolivegreen"
+ - "darkorange"
+ - "darkorchid"
+ - "darkred"
+ - "darksalmon"
+ - "darkseagreen"
+ - "darkslateblue"
+ - "darkslategray"
+ - "darkslategrey"
+ - "darkturquoise"
+ - "darkviolet"
+ - "deeppink"
+ - "deepskyblue"
+ - "dimgray"
+ - "dimgrey"
+ - "dodgerblue"
+ - "firebrick"
+ - "floralwhite"
+ - "forestgreen"
+ - "fuchsia"
+ - "gainsboro"
+ - "ghostwhite"
+ - "gold"
+ - "goldenrod"
+ - "gray"
+ - "green"
+ - "greenyellow"
+ - "grey"
+ - "honeydew"
+ - "hotpink"
+ - "indianred"
+ - "indigo"
+ - "ivory"
+ - "khaki"
+ - "lavender"
+ - "lavenderblush"
+ - "lawngreen"
+ - "lemonchiffon"
+ - "lightblue"
+ - "lightcoral"
+ - "lightcyan"
+ - "lightgoldenrodyellow"
+ - "lightgray"
+ - "lightgreen"
+ - "lightgrey"
+ - "lightpink"
+ - "lightsalmon"
+ - "lightseagreen"
+ - "lightskyblue"
+ - "lightslategray"
+ - "lightslategrey"
+ - "lightsteelblue"
+ - "lightyellow"
+ - "lime"
+ - "limegreen"
+ - "linen"
+ - "magenta"
+ - "maroon"
+ - "mediumaquamarine"
+ - "mediumblue"
+ - "mediumorchid"
+ - "mediumpurple"
+ - "mediumseagreen"
+ - "mediumslateblue"
+ - "mediumspringgreen"
+ - "mediumturquoise"
+ - "mediumvioletred"
+ - "midnightblue"
+ - "mintcream"
+ - "mistyrose"
+ - "moccasin"
+ - "navajowhite"
+ - "navy"
+ - "navyblue"
+ - "oldlace"
+ - "olive"
+ - "olivedrab"
+ - "orange"
+ - "orangered"
+ - "orchid"
+ - "palegoldenrod"
+ - "palegreen"
+ - "paleturquoise"
+ - "palevioletred"
+ - "papayawhip"
+ - "peachpuff"
+ - "peru"
+ - "pink"
+ - "plum"
+ - "powderblue"
+ - "purple"
+ - "red"
+ - "rosybrown"
+ - "royalblue"
+ - "saddlebrown"
+ - "salmon"
+ - "sandybrown"
+ - "seagreen"
+ - "seashell"
+ - "sienna"
+ - "silver"
+ - "skyblue"
+ - "slateblue"
+ - "slategray"
+ - "slategrey"
+ - "snow"
+ - "springgreen"
+ - "steelblue"
+ - "tan"
+ - "teal"
+ - "thistle"
+ - "tomato"
+ - "turquoise"
+ - "violet"
+ - "wheat"
+ - "white"
+ - "whitesmoke"
+ - "yellow"
+ - "yellowgreen"
hs_color:
- description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100.
+ name: Hue/Sat color
+ description:
+ Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100.
+ advanced: true
example: "[300, 70]"
+ selector:
+ object:
xy_color:
+ name: XY-color
description: Color for the light in XY-format.
+ advanced: true
example: "[0.52, 0.43]"
+ selector:
+ object:
color_temp:
+ name: Color temperature (mireds)
description: Color temperature for the light in mireds.
+ advanced: true
example: 250
+ selector:
+ number:
+ min: 153
+ max: 500
+ step: 1
+ unit_of_measurement: mireds
+ mode: slider
kelvin:
+ name: Color temperature (Kelvin)
description: Color temperature for the light in Kelvin.
+ advanced: true
example: 4000
+ selector:
+ number:
+ min: 2000
+ max: 6500
+ step: 100
+ unit_of_measurement: K
+ mode: slider
white_value:
+ name: White level
description: Number between 0..255 indicating level of white.
+ advanced: true
example: "250"
+ selector:
+ number:
+ min: 0
+ max: 255
+ step: 1
+ mode: slider
brightness:
- description: Number between 0..255 indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light.
+ name: Brightness value
+ description:
+ Number between 0..255 indicating brightness, where 0 turns the light
+ off, 1 is the minimum brightness and 255 is the maximum brightness
+ supported by the light.
+ advanced: true
example: 120
+ selector:
+ number:
+ min: 0
+ max: 255
+ step: 1
+ mode: slider
brightness_pct:
- description: Number between 0..100 indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness supported by the light.
+ name: Brightness
+ description:
+ Number between 0..100 indicating percentage of full brightness, where 0
+ turns the light off, 1 is the minimum brightness and 100 is the maximum
+ brightness supported by the light.
example: 47
+ selector:
+ number:
+ min: 0
+ max: 100
+ step: 1
+ unit_of_measurement: "%"
+ mode: slider
profile:
+ name: Profile
description: Name of a light profile to use.
+ advanced: true
example: relax
+ selector:
+ text:
flash:
+ name: Flash
description: If the light should flash. Valid values are short and long.
+ advanced: true
example: short
values:
- short
- long
+ selector:
+ select:
+ options:
+ - long
+ - short
effect:
+ name: Effect
description: Light effect.
example: random
values:
- colorloop
- random
+ selector:
+ text:
diff --git a/homeassistant/components/light/significant_change.py b/homeassistant/components/light/significant_change.py
index a0bd5203101d42..9e0f10fae474bf 100644
--- a/homeassistant/components/light/significant_change.py
+++ b/homeassistant/components/light/significant_change.py
@@ -1,5 +1,7 @@
"""Helper to test significant Light state changes."""
-from typing import Any, Optional
+from __future__ import annotations
+
+from typing import Any
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.significant_change import (
@@ -24,7 +26,7 @@ def async_check_significant_change(
new_state: str,
new_attrs: dict,
**kwargs: Any,
-) -> Optional[bool]:
+) -> bool | None:
"""Test if state significantly changed."""
if old_state != new_state:
return True
diff --git a/homeassistant/components/light/translations/id.json b/homeassistant/components/light/translations/id.json
index bc2ba732df2bd2..25c636ac1c7e56 100644
--- a/homeassistant/components/light/translations/id.json
+++ b/homeassistant/components/light/translations/id.json
@@ -1,8 +1,26 @@
{
+ "device_automation": {
+ "action_type": {
+ "brightness_decrease": "Kurangi kecerahan {entity_name}",
+ "brightness_increase": "Tingkatkan kecerahan {entity_name}",
+ "flash": "Lampu kilat {entity_name}",
+ "toggle": "Nyala/matikan {entity_name}",
+ "turn_off": "Matikan {entity_name}",
+ "turn_on": "Nyalakan {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} mati",
+ "is_on": "{entity_name} nyala"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} dimatikan",
+ "turned_on": "{entity_name} dinyalakan"
+ }
+ },
"state": {
"_": {
- "off": "Off",
- "on": "On"
+ "off": "Mati",
+ "on": "Nyala"
}
},
"title": "Lampu"
diff --git a/homeassistant/components/light/translations/ko.json b/homeassistant/components/light/translations/ko.json
index 33afda2d06d5b3..5413ecd567e682 100644
--- a/homeassistant/components/light/translations/ko.json
+++ b/homeassistant/components/light/translations/ko.json
@@ -1,20 +1,20 @@
{
"device_automation": {
"action_type": {
- "brightness_decrease": "{entity_name} \uc744(\ub97c) \uc5b4\ub461\uac8c \ud558\uae30",
- "brightness_increase": "{entity_name} \uc744(\ub97c) \ubc1d\uac8c \ud558\uae30",
- "flash": "{entity_name} \ud50c\ub798\uc2dc",
- "toggle": "{entity_name} \ud1a0\uae00",
- "turn_off": "{entity_name} \ub044\uae30",
- "turn_on": "{entity_name} \ucf1c\uae30"
+ "brightness_decrease": "{entity_name}\uc744(\ub97c) \uc5b4\ub461\uac8c \ud558\uae30",
+ "brightness_increase": "{entity_name}\uc744(\ub97c) \ubc1d\uac8c \ud558\uae30",
+ "flash": "{entity_name}\uc744(\ub97c) \uae5c\ube61\uc774\uae30",
+ "toggle": "{entity_name}\uc744(\ub97c) \ud1a0\uae00\ud558\uae30",
+ "turn_off": "{entity_name}\uc744(\ub97c) \ub044\uae30",
+ "turn_on": "{entity_name}\uc744(\ub97c) \ucf1c\uae30"
},
"condition_type": {
- "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74",
- "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74"
+ "is_off": "{entity_name}\uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74",
+ "is_on": "{entity_name}\uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74"
},
"trigger_type": {
- "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c",
- "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c"
+ "turned_off": "{entity_name}\uc774(\uac00) \uaebc\uc84c\uc744 \ub54c",
+ "turned_on": "{entity_name}\uc774(\uac00) \ucf1c\uc84c\uc744 \ub54c"
}
},
"state": {
diff --git a/homeassistant/components/light/translations/sv.json b/homeassistant/components/light/translations/sv.json
index 0d0e29a87ed93b..d5f0bdaf767143 100644
--- a/homeassistant/components/light/translations/sv.json
+++ b/homeassistant/components/light/translations/sv.json
@@ -1,6 +1,8 @@
{
"device_automation": {
"action_type": {
+ "brightness_decrease": "Minska ljusstyrkan f\u00f6r {entity_name}",
+ "brightness_increase": "\u00d6ka ljusstyrkan f\u00f6r {entity_name}",
"toggle": "V\u00e4xla {entity_name}",
"turn_off": "St\u00e4ng av {entity_name}",
"turn_on": "Sl\u00e5 p\u00e5 {entity_name}"
diff --git a/homeassistant/components/light/translations/uk.json b/homeassistant/components/light/translations/uk.json
index 67685889c5489d..86eee7d6b23e93 100644
--- a/homeassistant/components/light/translations/uk.json
+++ b/homeassistant/components/light/translations/uk.json
@@ -1,5 +1,17 @@
{
"device_automation": {
+ "action_type": {
+ "brightness_decrease": "{entity_name}: \u0437\u043c\u0435\u043d\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c",
+ "brightness_increase": "{entity_name}: \u0437\u0431\u0456\u043b\u044c\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c",
+ "flash": "{entity_name}: \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043c\u0438\u0433\u0430\u043d\u043d\u044f",
+ "toggle": "{entity_name}: \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u0438",
+ "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438",
+ "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456"
+ },
"trigger_type": {
"turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
"turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e"
diff --git a/homeassistant/components/lightwave/sensor.py b/homeassistant/components/lightwave/sensor.py
index 2144979106a3bf..1128078f8bc400 100644
--- a/homeassistant/components/lightwave/sensor.py
+++ b/homeassistant/components/lightwave/sensor.py
@@ -1,6 +1,6 @@
"""Support for LightwaveRF TRV - Associated Battery."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_NAME, DEVICE_CLASS_BATTERY, PERCENTAGE
-from homeassistant.helpers.entity import Entity
from . import CONF_SERIAL, LIGHTWAVE_LINK
@@ -22,7 +22,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(batteries)
-class LightwaveBattery(Entity):
+class LightwaveBattery(SensorEntity):
"""Lightwave TRV Battery."""
def __init__(self, name, lwlink, serial):
diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py
index bb81a022891139..70a15eaf4e0e3a 100644
--- a/homeassistant/components/linode/binary_sensor.py
+++ b/homeassistant/components/linode/binary_sensor.py
@@ -75,7 +75,7 @@ def device_class(self):
return DEVICE_CLASS_MOVING
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the Linode Node."""
return self._attrs
diff --git a/homeassistant/components/linode/switch.py b/homeassistant/components/linode/switch.py
index c9207ec1be709f..9002cb7bd1176e 100644
--- a/homeassistant/components/linode/switch.py
+++ b/homeassistant/components/linode/switch.py
@@ -67,7 +67,7 @@ def is_on(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the Linode Node."""
return self._attrs
diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py
index f4d4e92cb785e2..b7746392ceecf2 100644
--- a/homeassistant/components/linux_battery/sensor.py
+++ b/homeassistant/components/linux_battery/sensor.py
@@ -5,10 +5,9 @@
from batinfo import Batteries
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_NAME, CONF_NAME, DEVICE_CLASS_BATTERY, PERCENTAGE
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -68,7 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([LinuxBatterySensor(name, battery_id, system)], True)
-class LinuxBatterySensor(Entity):
+class LinuxBatterySensor(SensorEntity):
"""Representation of a Linux Battery sensor."""
def __init__(self, name, battery_id, system):
@@ -101,7 +100,7 @@ def unit_of_measurement(self):
return PERCENTAGE
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
if self._system == "android":
return {
diff --git a/homeassistant/components/lirc/__init__.py b/homeassistant/components/lirc/__init__.py
index bf939fb7535137..b52cf3cf1074c9 100644
--- a/homeassistant/components/lirc/__init__.py
+++ b/homeassistant/components/lirc/__init__.py
@@ -1,5 +1,5 @@
"""Support for LIRC devices."""
-# pylint: disable=no-member, import-error
+# pylint: disable=import-error
import logging
import threading
import time
diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py
index 9977bb9bdb4b50..f00853af524837 100644
--- a/homeassistant/components/litejet/__init__.py
+++ b/homeassistant/components/litejet/__init__.py
@@ -1,49 +1,86 @@
"""Support for the LiteJet lighting system."""
-from pylitejet import LiteJet
+import asyncio
+import logging
+
+import pylitejet
+from serial import SerialException
import voluptuous as vol
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_PORT
-from homeassistant.helpers import discovery
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv
-CONF_EXCLUDE_NAMES = "exclude_names"
-CONF_INCLUDE_SWITCHES = "include_switches"
+from .const import CONF_EXCLUDE_NAMES, CONF_INCLUDE_SWITCHES, DOMAIN, PLATFORMS
-DOMAIN = "litejet"
+_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema(
- {
- DOMAIN: vol.Schema(
- {
- vol.Required(CONF_PORT): cv.string,
- vol.Optional(CONF_EXCLUDE_NAMES): vol.All(cv.ensure_list, [cv.string]),
- vol.Optional(CONF_INCLUDE_SWITCHES, default=False): cv.boolean,
- }
- )
- },
+ vol.All(
+ cv.deprecated(DOMAIN),
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Required(CONF_PORT): cv.string,
+ vol.Optional(CONF_EXCLUDE_NAMES): vol.All(
+ cv.ensure_list, [cv.string]
+ ),
+ vol.Optional(CONF_INCLUDE_SWITCHES, default=False): cv.boolean,
+ }
+ )
+ },
+ ),
extra=vol.ALLOW_EXTRA,
)
def setup(hass, config):
"""Set up the LiteJet component."""
+ if DOMAIN in config and not hass.config_entries.async_entries(DOMAIN):
+ # No config entry exists and configuration.yaml config exists, trigger the import flow.
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN]
+ )
+ )
+ return True
+
- url = config[DOMAIN].get(CONF_PORT)
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Set up LiteJet via a config entry."""
+ port = entry.data[CONF_PORT]
- hass.data["litejet_system"] = LiteJet(url)
- hass.data["litejet_config"] = config[DOMAIN]
+ try:
+ system = pylitejet.LiteJet(port)
+ except SerialException as ex:
+ _LOGGER.error("Error connecting to the LiteJet MCP at %s", port, exc_info=ex)
+ raise ConfigEntryNotReady from ex
- discovery.load_platform(hass, "light", DOMAIN, {}, config)
- if config[DOMAIN].get(CONF_INCLUDE_SWITCHES):
- discovery.load_platform(hass, "switch", DOMAIN, {}, config)
- discovery.load_platform(hass, "scene", DOMAIN, {}, config)
+ hass.data[DOMAIN] = system
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
return True
-def is_ignored(hass, name):
- """Determine if a load, switch, or scene should be ignored."""
- for prefix in hass.data["litejet_config"].get(CONF_EXCLUDE_NAMES, []):
- if name.startswith(prefix):
- return True
- return False
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a LiteJet config entry."""
+
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+
+ if unload_ok:
+ hass.data[DOMAIN].close()
+ hass.data.pop(DOMAIN)
+
+ return unload_ok
diff --git a/homeassistant/components/litejet/config_flow.py b/homeassistant/components/litejet/config_flow.py
new file mode 100644
index 00000000000000..124b229c78600f
--- /dev/null
+++ b/homeassistant/components/litejet/config_flow.py
@@ -0,0 +1,55 @@
+"""Config flow for the LiteJet lighting system."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+import pylitejet
+from serial import SerialException
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_PORT
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """LiteJet config flow."""
+
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
+ """Create a LiteJet config entry based upon user input."""
+ if self.hass.config_entries.async_entries(DOMAIN):
+ return self.async_abort(reason="single_instance_allowed")
+
+ errors = {}
+ if user_input is not None:
+ port = user_input[CONF_PORT]
+
+ await self.async_set_unique_id(port)
+ self._abort_if_unique_id_configured()
+
+ try:
+ system = pylitejet.LiteJet(port)
+ system.close()
+ except SerialException:
+ errors[CONF_PORT] = "open_failed"
+ else:
+ return self.async_create_entry(
+ title=port,
+ data={CONF_PORT: port},
+ )
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema({vol.Required(CONF_PORT): str}),
+ errors=errors,
+ )
+
+ async def async_step_import(self, import_data):
+ """Import litejet config from configuration.yaml."""
+ return self.async_create_entry(title=import_data[CONF_PORT], data=import_data)
diff --git a/homeassistant/components/litejet/const.py b/homeassistant/components/litejet/const.py
new file mode 100644
index 00000000000000..8e27aa3a0a7341
--- /dev/null
+++ b/homeassistant/components/litejet/const.py
@@ -0,0 +1,8 @@
+"""LiteJet constants."""
+
+DOMAIN = "litejet"
+
+CONF_EXCLUDE_NAMES = "exclude_names"
+CONF_INCLUDE_SWITCHES = "include_switches"
+
+PLATFORMS = ["light", "switch", "scene"]
diff --git a/homeassistant/components/litejet/light.py b/homeassistant/components/litejet/light.py
index efc6830d775149..5248afb4dbd413 100644
--- a/homeassistant/components/litejet/light.py
+++ b/homeassistant/components/litejet/light.py
@@ -1,43 +1,53 @@
"""Support for LiteJet lights."""
import logging
-from homeassistant.components import litejet
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
SUPPORT_BRIGHTNESS,
LightEntity,
)
+from .const import DOMAIN
+
_LOGGER = logging.getLogger(__name__)
ATTR_NUMBER = "number"
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up lights for the LiteJet platform."""
- litejet_ = hass.data["litejet_system"]
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up entry."""
+
+ system = hass.data[DOMAIN]
+
+ def get_entities(system):
+ entities = []
+ for i in system.loads():
+ name = system.get_load_name(i)
+ entities.append(LiteJetLight(config_entry.entry_id, system, i, name))
+ return entities
- devices = []
- for i in litejet_.loads():
- name = litejet_.get_load_name(i)
- if not litejet.is_ignored(hass, name):
- devices.append(LiteJetLight(hass, litejet_, i, name))
- add_entities(devices, True)
+ async_add_entities(await hass.async_add_executor_job(get_entities, system), True)
class LiteJetLight(LightEntity):
"""Representation of a single LiteJet light."""
- def __init__(self, hass, lj, i, name):
+ def __init__(self, entry_id, lj, i, name):
"""Initialize a LiteJet light."""
- self._hass = hass
+ self._entry_id = entry_id
self._lj = lj
self._index = i
self._brightness = 0
self._name = name
- lj.on_load_activated(i, self._on_load_changed)
- lj.on_load_deactivated(i, self._on_load_changed)
+ async def async_added_to_hass(self):
+ """Run when this Entity has been added to HA."""
+ self._lj.on_load_activated(self._index, self._on_load_changed)
+ self._lj.on_load_deactivated(self._index, self._on_load_changed)
+
+ async def async_will_remove_from_hass(self):
+ """Entity being removed from hass."""
+ self._lj.unsubscribe(self._on_load_changed)
def _on_load_changed(self):
"""Handle state changes."""
@@ -54,6 +64,11 @@ def name(self):
"""Return the light's name."""
return self._name
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this light."""
+ return f"{self._entry_id}_{self._index}"
+
@property
def brightness(self):
"""Return the light's brightness."""
@@ -70,7 +85,7 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
return {ATTR_NUMBER: self._index}
diff --git a/homeassistant/components/litejet/manifest.json b/homeassistant/components/litejet/manifest.json
index 1e469370b4338d..e23e5ac2964cca 100644
--- a/homeassistant/components/litejet/manifest.json
+++ b/homeassistant/components/litejet/manifest.json
@@ -2,6 +2,7 @@
"domain": "litejet",
"name": "LiteJet",
"documentation": "https://www.home-assistant.io/integrations/litejet",
- "requirements": ["pylitejet==0.1"],
- "codeowners": []
+ "requirements": ["pylitejet==0.3.0"],
+ "codeowners": ["@joncar"],
+ "config_flow": true
}
diff --git a/homeassistant/components/litejet/scene.py b/homeassistant/components/litejet/scene.py
index 3311b8d86a0616..5ae0aec95591cb 100644
--- a/homeassistant/components/litejet/scene.py
+++ b/homeassistant/components/litejet/scene.py
@@ -1,29 +1,37 @@
"""Support for LiteJet scenes."""
+import logging
from typing import Any
-from homeassistant.components import litejet
from homeassistant.components.scene import Scene
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
ATTR_NUMBER = "number"
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up scenes for the LiteJet platform."""
- litejet_ = hass.data["litejet_system"]
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up entry."""
+
+ system = hass.data[DOMAIN]
+
+ def get_entities(system):
+ entities = []
+ for i in system.scenes():
+ name = system.get_scene_name(i)
+ entities.append(LiteJetScene(config_entry.entry_id, system, i, name))
+ return entities
- devices = []
- for i in litejet_.scenes():
- name = litejet_.get_scene_name(i)
- if not litejet.is_ignored(hass, name):
- devices.append(LiteJetScene(litejet_, i, name))
- add_entities(devices)
+ async_add_entities(await hass.async_add_executor_job(get_entities, system), True)
class LiteJetScene(Scene):
"""Representation of a single LiteJet scene."""
- def __init__(self, lj, i, name):
+ def __init__(self, entry_id, lj, i, name):
"""Initialize the scene."""
+ self._entry_id = entry_id
self._lj = lj
self._index = i
self._name = name
@@ -34,10 +42,20 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def unique_id(self):
+ """Return a unique identifier for this scene."""
+ return f"{self._entry_id}_{self._index}"
+
+ @property
+ def extra_state_attributes(self):
"""Return the device-specific state attributes."""
return {ATTR_NUMBER: self._index}
def activate(self, **kwargs: Any) -> None:
"""Activate the scene."""
self._lj.activate_scene(self._index)
+
+ @property
+ def entity_registry_enabled_default(self) -> bool:
+ """Scenes are only enabled by explicit user choice."""
+ return False
diff --git a/homeassistant/components/litejet/strings.json b/homeassistant/components/litejet/strings.json
new file mode 100644
index 00000000000000..79c4ed5f329b3a
--- /dev/null
+++ b/homeassistant/components/litejet/strings.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "Connect To LiteJet",
+ "description": "Connect the LiteJet's RS232-2 port to your computer and enter the path to the serial port device.\n\nThe LiteJet MCP must be configured for 19.2 K baud, 8 data bits, 1 stop bit, no parity, and to transmit a 'CR' after each response.",
+ "data": {
+ "port": "[%key:common::config_flow::data::port%]"
+ }
+ }
+ },
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
+ },
+ "error": {
+ "open_failed": "Cannot open the specified serial port."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/switch.py b/homeassistant/components/litejet/switch.py
index a734dc46d3e6b9..343d8393f1c724 100644
--- a/homeassistant/components/litejet/switch.py
+++ b/homeassistant/components/litejet/switch.py
@@ -1,39 +1,50 @@
"""Support for LiteJet switch."""
import logging
-from homeassistant.components import litejet
from homeassistant.components.switch import SwitchEntity
+from .const import DOMAIN
+
ATTR_NUMBER = "number"
_LOGGER = logging.getLogger(__name__)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the LiteJet switch platform."""
- litejet_ = hass.data["litejet_system"]
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up entry."""
+
+ system = hass.data[DOMAIN]
+
+ def get_entities(system):
+ entities = []
+ for i in system.button_switches():
+ name = system.get_switch_name(i)
+ entities.append(LiteJetSwitch(config_entry.entry_id, system, i, name))
+ return entities
- devices = []
- for i in litejet_.button_switches():
- name = litejet_.get_switch_name(i)
- if not litejet.is_ignored(hass, name):
- devices.append(LiteJetSwitch(hass, litejet_, i, name))
- add_entities(devices, True)
+ async_add_entities(await hass.async_add_executor_job(get_entities, system), True)
class LiteJetSwitch(SwitchEntity):
"""Representation of a single LiteJet switch."""
- def __init__(self, hass, lj, i, name):
+ def __init__(self, entry_id, lj, i, name):
"""Initialize a LiteJet switch."""
- self._hass = hass
+ self._entry_id = entry_id
self._lj = lj
self._index = i
self._state = False
self._name = name
- lj.on_switch_pressed(i, self._on_switch_pressed)
- lj.on_switch_released(i, self._on_switch_released)
+ async def async_added_to_hass(self):
+ """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):
+ """Entity being removed from hass."""
+ self._lj.unsubscribe(self._on_switch_pressed)
+ self._lj.unsubscribe(self._on_switch_released)
def _on_switch_pressed(self):
_LOGGER.debug("Updating pressed for %s", self._name)
@@ -50,6 +61,11 @@ def name(self):
"""Return the name of the switch."""
return self._name
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this switch."""
+ return f"{self._entry_id}_{self._index}"
+
@property
def is_on(self):
"""Return if the switch is pressed."""
@@ -61,7 +77,7 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device-specific state attributes."""
return {ATTR_NUMBER: self._index}
@@ -72,3 +88,8 @@ def turn_on(self, **kwargs):
def turn_off(self, **kwargs):
"""Release the switch."""
self._lj.release_switch(self._index)
+
+ @property
+ def entity_registry_enabled_default(self) -> bool:
+ """Switches are only enabled by explicit user choice."""
+ return False
diff --git a/homeassistant/components/litejet/translations/bg.json b/homeassistant/components/litejet/translations/bg.json
new file mode 100644
index 00000000000000..4983c9a14b265d
--- /dev/null
+++ b/homeassistant/components/litejet/translations/bg.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "port": "\u041f\u043e\u0440\u0442"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/ca.json b/homeassistant/components/litejet/translations/ca.json
new file mode 100644
index 00000000000000..39e2a56dc4dd18
--- /dev/null
+++ b/homeassistant/components/litejet/translations/ca.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
+ },
+ "error": {
+ "open_failed": "No s'ha pogut obrir el port s\u00e8rie especificat."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Port"
+ },
+ "description": "Connecta el port RS232-2 LiteJet a l'ordinador i introdueix la ruta al port s\u00e8rie del dispositiu.\n\nEl LiteJet MCP ha d'estar configurat amb una velocitat de 19.2 k baudis, 8 bits de dades, 1 bit de parada, sense paritat i ha de transmetre un 'CR' despr\u00e9s de cada resposta.",
+ "title": "Connexi\u00f3 amb LiteJet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/de.json b/homeassistant/components/litejet/translations/de.json
new file mode 100644
index 00000000000000..ff528dd79b2187
--- /dev/null
+++ b/homeassistant/components/litejet/translations/de.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
+ "error": {
+ "open_failed": "Die angegebene serielle Schnittstelle kann nicht ge\u00f6ffnet werden."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Port"
+ },
+ "description": "Verbinde den RS232-2-Anschluss des LiteJet mit deinen Computer und gib den Pfad zum Ger\u00e4t der seriellen Schnittstelle ein.\n\nDer LiteJet MCP muss f\u00fcr 19,2 K Baud, 8 Datenbits, 1 Stoppbit, keine Parit\u00e4t und zur \u00dcbertragung eines 'CR' nach jeder Antwort konfiguriert werden.",
+ "title": "Verbinde zu LiteJet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/en.json b/homeassistant/components/litejet/translations/en.json
new file mode 100644
index 00000000000000..e09b20dc9f2527
--- /dev/null
+++ b/homeassistant/components/litejet/translations/en.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
+ },
+ "error": {
+ "open_failed": "Cannot open the specified serial port."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Port"
+ },
+ "description": "Connect the LiteJet's RS232-2 port to your computer and enter the path to the serial port device.\n\nThe LiteJet MCP must be configured for 19.2 K baud, 8 data bits, 1 stop bit, no parity, and to transmit a 'CR' after each response.",
+ "title": "Connect To LiteJet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/es.json b/homeassistant/components/litejet/translations/es.json
new file mode 100644
index 00000000000000..b0641022bf01d1
--- /dev/null
+++ b/homeassistant/components/litejet/translations/es.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "open_failed": "No se puede abrir el puerto serie especificado."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Puerto"
+ },
+ "description": "Conecte el puerto RS232-2 del LiteJet a su computadora e ingrese la ruta al dispositivo del puerto serial. \n\nEl LiteJet MCP debe configurarse para 19,2 K baudios, 8 bits de datos, 1 bit de parada, sin paridad y para transmitir un 'CR' despu\u00e9s de cada respuesta.",
+ "title": "Conectarse a LiteJet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/et.json b/homeassistant/components/litejet/translations/et.json
new file mode 100644
index 00000000000000..6e50b5dcdf36be
--- /dev/null
+++ b/homeassistant/components/litejet/translations/et.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ },
+ "error": {
+ "open_failed": "valitud jadaporti ei saa avada."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Port"
+ },
+ "description": "\u00dchenda LiteJeti RS232-2 port arvutiga ja sisesta jadapordi seadme tee.\n\nLiteJet MCP peab olema konfigureeritud: 19200 boodi, 8 andmebitti, 1 stopp bitt, paarsus puudub ja edastada \"CR\" p\u00e4rast iga vastust.",
+ "title": "Loo \u00fchendus LiteJetiga"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/fr.json b/homeassistant/components/litejet/translations/fr.json
new file mode 100644
index 00000000000000..89459d1829f3e0
--- /dev/null
+++ b/homeassistant/components/litejet/translations/fr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
+ },
+ "error": {
+ "open_failed": "Impossible d'ouvrir le port s\u00e9rie sp\u00e9cifi\u00e9."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Port"
+ },
+ "description": "Connectez le port RS232-2 du LiteJet \u00e0 votre ordinateur et entrez le chemin d'acc\u00e8s au p\u00e9riph\u00e9rique de port s\u00e9rie. \n\n Le LiteJet MCP doit \u00eatre configur\u00e9 pour 19,2 K bauds, 8 bits de donn\u00e9es, 1 bit d'arr\u00eat, sans parit\u00e9 et pour transmettre un \u00abCR\u00bb apr\u00e8s chaque r\u00e9ponse.",
+ "title": "Connectez-vous \u00e0 LiteJet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/he.json b/homeassistant/components/litejet/translations/he.json
new file mode 100644
index 00000000000000..a06c89f1d2ac72
--- /dev/null
+++ b/homeassistant/components/litejet/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "port": "\u05e4\u05d5\u05e8\u05d8"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/hu.json b/homeassistant/components/litejet/translations/hu.json
new file mode 100644
index 00000000000000..6d895624c30dd9
--- /dev/null
+++ b/homeassistant/components/litejet/translations/hu.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Port"
+ },
+ "title": "Csatlakoz\u00e1s a LiteJet-hez"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/id.json b/homeassistant/components/litejet/translations/id.json
new file mode 100644
index 00000000000000..690692ca4ccba7
--- /dev/null
+++ b/homeassistant/components/litejet/translations/id.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "open_failed": "Tidak dapat membuka port serial yang ditentukan."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Port"
+ },
+ "description": "Hubungkan port RS232-2 LiteJet ke komputer Anda dan masukkan jalur ke perangkat port serial.\n\nLiteJet MCP harus dikonfigurasi untuk baud 19,2 K, bit data 8,bit stop 1, tanpa paritas, dan untuk mengirimkan 'CR' setelah setiap respons.",
+ "title": "Hubungkan ke LiteJet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/it.json b/homeassistant/components/litejet/translations/it.json
new file mode 100644
index 00000000000000..5b3dc46753d888
--- /dev/null
+++ b/homeassistant/components/litejet/translations/it.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
+ },
+ "error": {
+ "open_failed": "Impossibile aprire la porta seriale specificata."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Porta"
+ },
+ "description": "Collega la porta RS232-2 del LiteJet al tuo computer e inserisci il percorso del dispositivo della porta seriale. \n\nL'MCP LiteJet deve essere configurato per 19,2 K baud, 8 bit di dati, 1 bit di stop, nessuna parit\u00e0 e per trasmettere un \"CR\" dopo ogni risposta.",
+ "title": "Connetti a LiteJet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/ko.json b/homeassistant/components/litejet/translations/ko.json
new file mode 100644
index 00000000000000..829918dc55712e
--- /dev/null
+++ b/homeassistant/components/litejet/translations/ko.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "open_failed": "\uc9c0\uc815\ud55c \uc2dc\ub9ac\uc5bc \ud3ec\ud2b8\ub97c \uc5f4 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "\ud3ec\ud2b8"
+ },
+ "description": "LiteJet\uc758 RS232-2 \ud3ec\ud2b8\ub97c \ucef4\ud4e8\ud130\uc5d0 \uc5f0\uacb0\ud558\uace0 \uc2dc\ub9ac\uc5bc \ud3ec\ud2b8 \uc7a5\uce58\uc758 \uacbd\ub85c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\nLiteJet MCP\ub294 19.2 K \uc804\uc1a1\uc18d\ub3c4, 8 \ub370\uc774\ud130 \ube44\ud2b8, 1 \uc815\uc9c0 \ube44\ud2b8, \ud328\ub9ac\ud2f0 \uc5c6\uc74c\uc73c\ub85c \uad6c\uc131\ub418\uc5b4\uc57c \ud558\uba70 \uac01 \uc751\ub2f5 \ud6c4\uc5d0 'CR'\uc744 \uc804\uc1a1\ud574\uc57c \ud569\ub2c8\ub2e4.",
+ "title": "Litejet\uc5d0 \uc5f0\uacb0\ud558\uae30"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/nl.json b/homeassistant/components/litejet/translations/nl.json
new file mode 100644
index 00000000000000..a96f8de6171158
--- /dev/null
+++ b/homeassistant/components/litejet/translations/nl.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
+ },
+ "error": {
+ "open_failed": "Kan de opgegeven seri\u00eble poort niet openen."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Poort"
+ },
+ "description": "Verbind de RS232-2 poort van de LiteJet met uw computer en voer het pad naar het seri\u00eble poortapparaat in.\n\nDe LiteJet MCP moet worden geconfigureerd voor 19,2 K baud, 8 databits, 1 stopbit, geen pariteit, en om een 'CR' uit te zenden na elk antwoord.",
+ "title": "Maak verbinding met LiteJet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/no.json b/homeassistant/components/litejet/translations/no.json
new file mode 100644
index 00000000000000..d3206ca2897c74
--- /dev/null
+++ b/homeassistant/components/litejet/translations/no.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
+ },
+ "error": {
+ "open_failed": "Kan ikke \u00e5pne den angitte serielle porten"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Port"
+ },
+ "description": "Koble LiteJets RS232-2-port til datamaskinen og skriv stien til den serielle portenheten. \n\n LiteJet MCP m\u00e5 konfigureres for 19,2 K baud, 8 databiter, 1 stoppbit, ingen paritet, og for \u00e5 overf\u00f8re en 'CR' etter hvert svar.",
+ "title": "Koble til LiteJet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/pl.json b/homeassistant/components/litejet/translations/pl.json
new file mode 100644
index 00000000000000..20e5d68288d89a
--- /dev/null
+++ b/homeassistant/components/litejet/translations/pl.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
+ },
+ "error": {
+ "open_failed": "Nie mo\u017cna otworzy\u0107 okre\u015blonego portu szeregowego."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Port"
+ },
+ "description": "Pod\u0142\u0105cz port RS232-2 LiteJet do komputera i wprowad\u017a \u015bcie\u017ck\u0119 do urz\u0105dzenia portu szeregowego. \n\nLiteJet MCP musi by\u0107 skonfigurowany dla szybko\u015bci 19,2K, 8 bit\u00f3w danych, 1 bit stopu, brak parzysto\u015bci i przesy\u0142anie \u201eCR\u201d po ka\u017cdej odpowiedzi.",
+ "title": "Po\u0142\u0105czenie z LiteJet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/pt.json b/homeassistant/components/litejet/translations/pt.json
new file mode 100644
index 00000000000000..8b09f9a245f3d6
--- /dev/null
+++ b/homeassistant/components/litejet/translations/pt.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Porta"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/ru.json b/homeassistant/components/litejet/translations/ru.json
new file mode 100644
index 00000000000000..c90e6956301436
--- /dev/null
+++ b/homeassistant/components/litejet/translations/ru.json
@@ -0,0 +1,19 @@
+{
+ "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."
+ },
+ "error": {
+ "open_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "description": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u043f\u043e\u0440\u0442 RS232-2 LiteJet \u043a \u043a\u043e\u043c\u043f\u044c\u044e\u0442\u0435\u0440\u0443, \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0443\u0442\u044c \u043a \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u043c\u0443 \u043f\u043e\u0440\u0442\u0443. \n\nLiteJet MCP \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0430 \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c 19,2 \u041a\u0431\u043e\u0434, 8 \u0431\u0438\u0442 \u0434\u0430\u043d\u043d\u044b\u0445, 1 \u0441\u0442\u043e\u043f\u043e\u0432\u044b\u0439 \u0431\u0438\u0442, \u0431\u0435\u0437 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f \u0447\u0435\u0442\u043d\u043e\u0441\u0442\u0438 \u0438 \u043d\u0430 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0443 'CR' \u043f\u043e\u0441\u043b\u0435 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043e\u0442\u0432\u0435\u0442\u0430.",
+ "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a LiteJet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/tr.json b/homeassistant/components/litejet/translations/tr.json
new file mode 100644
index 00000000000000..de4ea12cb6f360
--- /dev/null
+++ b/homeassistant/components/litejet/translations/tr.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "LiteJet'e Ba\u011flan\u0131n"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/translations/zh-Hant.json b/homeassistant/components/litejet/translations/zh-Hant.json
new file mode 100644
index 00000000000000..8a268f3db494b5
--- /dev/null
+++ b/homeassistant/components/litejet/translations/zh-Hant.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002"
+ },
+ "error": {
+ "open_failed": "\u7121\u6cd5\u958b\u555f\u6307\u5b9a\u7684\u5e8f\u5217\u57e0"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "\u901a\u8a0a\u57e0"
+ },
+ "description": "\u9023\u7dda\u81f3\u96fb\u8166\u4e0a\u7684 LiteJet RS232-2 \u57e0\uff0c\u4e26\u8f38\u5165\u5e8f\u5217\u57e0\u88dd\u7f6e\u7684\u8def\u5f91\u3002\n\nLiteJet MCP \u5fc5\u9808\u8a2d\u5b9a\u70ba\u901a\u8a0a\u901f\u7387 19.2 K baud\u30018 \u6578\u64da\u4f4d\u5143\u30011 \u505c\u6b62\u4f4d\u5143\u3001\u7121\u540c\u4f4d\u4f4d\u5143\u4e26\u65bc\u6bcf\u500b\u56de\u5fa9\u5f8c\u50b3\u9001 'CR'\u3002",
+ "title": "\u9023\u7dda\u81f3 LiteJet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litejet/trigger.py b/homeassistant/components/litejet/trigger.py
index 0b0117465df472..6800282766bae2 100644
--- a/homeassistant/components/litejet/trigger.py
+++ b/homeassistant/components/litejet/trigger.py
@@ -1,4 +1,6 @@
"""Trigger an automation when a LiteJet switch is released."""
+from typing import Callable
+
import voluptuous as vol
from homeassistant.const import CONF_PLATFORM
@@ -7,7 +9,7 @@
from homeassistant.helpers.event import track_point_in_utc_time
import homeassistant.util.dt as dt_util
-# mypy: allow-untyped-defs, no-check-untyped-defs
+from .const import DOMAIN
CONF_NUMBER = "number"
CONF_HELD_MORE_THAN = "held_more_than"
@@ -29,11 +31,12 @@
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for events based on configuration."""
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
number = config.get(CONF_NUMBER)
held_more_than = config.get(CONF_HELD_MORE_THAN)
held_less_than = config.get(CONF_HELD_LESS_THAN)
pressed_time = None
- cancel_pressed_more_than = None
+ cancel_pressed_more_than: Callable = None
job = HassJob(action)
@callback
@@ -48,6 +51,7 @@ def call_action():
CONF_HELD_MORE_THAN: held_more_than,
CONF_HELD_LESS_THAN: held_less_than,
"description": f"litejet switch #{number}",
+ "id": trigger_id,
}
},
)
@@ -91,12 +95,15 @@ def released():
):
hass.add_job(call_action)
- hass.data["litejet_system"].on_switch_pressed(number, pressed)
- hass.data["litejet_system"].on_switch_released(number, released)
+ system = hass.data[DOMAIN]
+
+ system.on_switch_pressed(number, pressed)
+ system.on_switch_released(number, released)
@callback
def async_remove():
"""Remove all subscriptions used for this trigger."""
- return
+ system.unsubscribe(pressed)
+ system.unsubscribe(released)
return async_remove
diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py
new file mode 100644
index 00000000000000..84e6822dc13f48
--- /dev/null
+++ b/homeassistant/components/litterrobot/__init__.py
@@ -0,0 +1,54 @@
+"""The Litter-Robot integration."""
+import asyncio
+
+from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+
+from .const import DOMAIN
+from .hub import LitterRobotHub
+
+PLATFORMS = ["sensor", "switch", "vacuum"]
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+ """Set up the Litter-Robot component."""
+ hass.data.setdefault(DOMAIN, {})
+
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up Litter-Robot from a config entry."""
+ hub = hass.data[DOMAIN][entry.entry_id] = LitterRobotHub(hass, entry.data)
+ try:
+ await hub.login(load_robots=True)
+ except LitterRobotLoginException:
+ return False
+ except LitterRobotException as ex:
+ raise ConfigEntryNotReady from ex
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
diff --git a/homeassistant/components/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py
new file mode 100644
index 00000000000000..67b73ee12d544b
--- /dev/null
+++ b/homeassistant/components/litterrobot/config_flow.py
@@ -0,0 +1,53 @@
+"""Config flow for Litter-Robot integration."""
+import logging
+
+from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+
+from .const import DOMAIN
+from .hub import LitterRobotHub
+
+_LOGGER = logging.getLogger(__name__)
+
+STEP_USER_DATA_SCHEMA = vol.Schema(
+ {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
+)
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Litter-Robot."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ errors = {}
+
+ if user_input is not None:
+ for entry in self._async_current_entries():
+ if entry.data[CONF_USERNAME] == user_input[CONF_USERNAME]:
+ return self.async_abort(reason="already_configured")
+
+ hub = LitterRobotHub(self.hass, user_input)
+ try:
+ await hub.login()
+ except LitterRobotLoginException:
+ errors["base"] = "invalid_auth"
+ except LitterRobotException:
+ errors["base"] = "cannot_connect"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+
+ if not errors:
+ return self.async_create_entry(
+ title=user_input[CONF_USERNAME], data=user_input
+ )
+
+ return self.async_show_form(
+ step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
+ )
diff --git a/homeassistant/components/litterrobot/const.py b/homeassistant/components/litterrobot/const.py
new file mode 100644
index 00000000000000..5ac889d9b738a9
--- /dev/null
+++ b/homeassistant/components/litterrobot/const.py
@@ -0,0 +1,2 @@
+"""Constants for the Litter-Robot integration."""
+DOMAIN = "litterrobot"
diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py
new file mode 100644
index 00000000000000..86c3aff5462cab
--- /dev/null
+++ b/homeassistant/components/litterrobot/hub.py
@@ -0,0 +1,126 @@
+"""A wrapper 'hub' for the Litter-Robot API and base entity for common attributes."""
+from __future__ import annotations
+
+from datetime import time, timedelta
+import logging
+from types import MethodType
+from typing import Any
+
+import pylitterbot
+from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException
+
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant.core import HomeAssistant
+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
+
+_LOGGER = logging.getLogger(__name__)
+
+REFRESH_WAIT_TIME = 12
+UPDATE_INTERVAL = 10
+
+
+class LitterRobotHub:
+ """A Litter-Robot hub wrapper class."""
+
+ def __init__(self, hass: HomeAssistant, data: dict):
+ """Initialize the Litter-Robot hub."""
+ self._data = data
+ self.account = None
+ self.logged_in = False
+
+ async def _async_update_data():
+ """Update all device states from the Litter-Robot API."""
+ await self.account.refresh_robots()
+ return True
+
+ self.coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name=DOMAIN,
+ update_method=_async_update_data,
+ update_interval=timedelta(seconds=UPDATE_INTERVAL),
+ )
+
+ async def login(self, load_robots: bool = False):
+ """Login to Litter-Robot."""
+ self.logged_in = False
+ self.account = pylitterbot.Account()
+ try:
+ await self.account.connect(
+ username=self._data[CONF_USERNAME],
+ password=self._data[CONF_PASSWORD],
+ load_robots=load_robots,
+ )
+ self.logged_in = True
+ return self.logged_in
+ except LitterRobotLoginException as ex:
+ _LOGGER.error("Invalid credentials")
+ raise ex
+ except LitterRobotException as ex:
+ _LOGGER.error("Unable to connect to Litter-Robot API")
+ raise ex
+
+
+class LitterRobotEntity(CoordinatorEntity):
+ """Generic Litter-Robot entity representing common data and methods."""
+
+ def __init__(self, robot: pylitterbot.Robot, entity_type: str, hub: LitterRobotHub):
+ """Pass coordinator to CoordinatorEntity."""
+ super().__init__(hub.coordinator)
+ self.robot = robot
+ self.entity_type = entity_type
+ self.hub = hub
+
+ @property
+ def name(self):
+ """Return the name of this entity."""
+ return f"{self.robot.name} {self.entity_type}"
+
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return f"{self.robot.serial}-{self.entity_type}"
+
+ @property
+ def device_info(self):
+ """Return the device information for a Litter-Robot."""
+ return {
+ "identifiers": {(DOMAIN, self.robot.serial)},
+ "name": self.robot.name,
+ "manufacturer": "Litter-Robot",
+ "model": self.robot.model,
+ }
+
+ async def perform_action_and_refresh(self, action: MethodType, *args: Any):
+ """Perform an action and initiates a refresh of the robot data after a few seconds."""
+
+ async def async_call_later_callback(*_) -> None:
+ await self.hub.coordinator.async_request_refresh()
+
+ await action(*args)
+ async_call_later(self.hass, REFRESH_WAIT_TIME, async_call_later_callback)
+
+ @staticmethod
+ def parse_time_at_default_timezone(time_str: str) -> time | None:
+ """Parse a time string and add default timezone."""
+ parsed_time = dt_util.parse_time(time_str)
+
+ if parsed_time is None:
+ return None
+
+ return (
+ dt_util.start_of_local_day()
+ .replace(
+ hour=parsed_time.hour,
+ minute=parsed_time.minute,
+ second=parsed_time.second,
+ )
+ .timetz()
+ )
diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json
new file mode 100644
index 00000000000000..8fa7ab8dcb5404
--- /dev/null
+++ b/homeassistant/components/litterrobot/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "litterrobot",
+ "name": "Litter-Robot",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/litterrobot",
+ "requirements": ["pylitterbot==2021.2.8"],
+ "codeowners": ["@natekspencer"]
+}
diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py
new file mode 100644
index 00000000000000..8038fdbb2cbc64
--- /dev/null
+++ b/homeassistant/components/litterrobot/sensor.py
@@ -0,0 +1,87 @@
+"""Support for Litter-Robot sensors."""
+from __future__ import annotations
+
+from pylitterbot.robot import Robot
+
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE
+
+from .const import DOMAIN
+from .hub import LitterRobotEntity, LitterRobotHub
+
+
+def icon_for_gauge_level(gauge_level: int | None = None, offset: int = 0) -> str:
+ """Return a gauge icon valid identifier."""
+ if gauge_level is None or gauge_level <= 0 + offset:
+ return "mdi:gauge-empty"
+ if gauge_level > 70 + offset:
+ return "mdi:gauge-full"
+ if gauge_level > 30 + offset:
+ return "mdi:gauge"
+ return "mdi:gauge-low"
+
+
+class LitterRobotPropertySensor(LitterRobotEntity, SensorEntity):
+ """Litter-Robot property sensors."""
+
+ def __init__(
+ self, robot: Robot, entity_type: str, hub: LitterRobotHub, sensor_attribute: str
+ ):
+ """Pass coordinator to CoordinatorEntity."""
+ super().__init__(robot, entity_type, hub)
+ self.sensor_attribute = sensor_attribute
+
+ @property
+ def state(self):
+ """Return the state."""
+ return getattr(self.robot, self.sensor_attribute)
+
+
+class LitterRobotWasteSensor(LitterRobotPropertySensor):
+ """Litter-Robot sensors."""
+
+ @property
+ def unit_of_measurement(self):
+ """Return unit of measurement."""
+ return PERCENTAGE
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend, if any."""
+ return icon_for_gauge_level(self.state, 10)
+
+
+class LitterRobotSleepTimeSensor(LitterRobotPropertySensor):
+ """Litter-Robot sleep time sensors."""
+
+ @property
+ def state(self):
+ """Return the state."""
+ if self.robot.sleep_mode_active:
+ return super().state.isoformat()
+ return None
+
+ @property
+ def device_class(self):
+ """Return the device class, if any."""
+ return DEVICE_CLASS_TIMESTAMP
+
+
+ROBOT_SENSORS = [
+ (LitterRobotWasteSensor, "Waste Drawer", "waste_drawer_gauge"),
+ (LitterRobotSleepTimeSensor, "Sleep Mode Start Time", "sleep_mode_start_time"),
+ (LitterRobotSleepTimeSensor, "Sleep Mode End Time", "sleep_mode_end_time"),
+]
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Litter-Robot sensors using config entry."""
+ hub = hass.data[DOMAIN][config_entry.entry_id]
+
+ entities = []
+ for robot in hub.account.robots:
+ for (sensor_class, entity_type, sensor_attribute) in ROBOT_SENSORS:
+ entities.append(sensor_class(robot, entity_type, hub, sensor_attribute))
+
+ if entities:
+ async_add_entities(entities, True)
diff --git a/homeassistant/components/litterrobot/strings.json b/homeassistant/components/litterrobot/strings.json
new file mode 100644
index 00000000000000..96dc8b371d143c
--- /dev/null
+++ b/homeassistant/components/litterrobot/strings.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "[%key:common::config_flow::data::username%]",
+ "password": "[%key:common::config_flow::data::password%]"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ }
+ }
+}
diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py
new file mode 100644
index 00000000000000..9164cc35e900af
--- /dev/null
+++ b/homeassistant/components/litterrobot/switch.py
@@ -0,0 +1,68 @@
+"""Support for Litter-Robot switches."""
+from homeassistant.components.switch import SwitchEntity
+
+from .const import DOMAIN
+from .hub import LitterRobotEntity
+
+
+class LitterRobotNightLightModeSwitch(LitterRobotEntity, SwitchEntity):
+ """Litter-Robot Night Light Mode Switch."""
+
+ @property
+ def is_on(self):
+ """Return true if switch is on."""
+ return self.robot.night_light_active
+
+ @property
+ def icon(self):
+ """Return the icon."""
+ return "mdi:lightbulb-on" if self.is_on else "mdi:lightbulb-off"
+
+ async def async_turn_on(self, **kwargs):
+ """Turn the switch on."""
+ await self.perform_action_and_refresh(self.robot.set_night_light, True)
+
+ async def async_turn_off(self, **kwargs):
+ """Turn the switch off."""
+ await self.perform_action_and_refresh(self.robot.set_night_light, False)
+
+
+class LitterRobotPanelLockoutSwitch(LitterRobotEntity, SwitchEntity):
+ """Litter-Robot Panel Lockout Switch."""
+
+ @property
+ def is_on(self):
+ """Return true if switch is on."""
+ return self.robot.panel_lock_active
+
+ @property
+ def icon(self):
+ """Return the icon."""
+ return "mdi:lock" if self.is_on else "mdi:lock-open"
+
+ async def async_turn_on(self, **kwargs):
+ """Turn the switch on."""
+ await self.perform_action_and_refresh(self.robot.set_panel_lockout, True)
+
+ async def async_turn_off(self, **kwargs):
+ """Turn the switch off."""
+ await self.perform_action_and_refresh(self.robot.set_panel_lockout, False)
+
+
+ROBOT_SWITCHES = {
+ "Night Light Mode": LitterRobotNightLightModeSwitch,
+ "Panel Lockout": LitterRobotPanelLockoutSwitch,
+}
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Litter-Robot switches using config entry."""
+ hub = hass.data[DOMAIN][config_entry.entry_id]
+
+ entities = []
+ for robot in hub.account.robots:
+ for switch_type, switch_class in ROBOT_SWITCHES.items():
+ entities.append(switch_class(robot, switch_type, hub))
+
+ if entities:
+ async_add_entities(entities, True)
diff --git a/homeassistant/components/litterrobot/translations/bg.json b/homeassistant/components/litterrobot/translations/bg.json
new file mode 100644
index 00000000000000..67a484573aa0c1
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/bg.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "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/litterrobot/translations/ca.json b/homeassistant/components/litterrobot/translations/ca.json
new file mode 100644
index 00000000000000..9677f944330e80
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/ca.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrasenya",
+ "username": "Nom d'usuari"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litterrobot/translations/de.json b/homeassistant/components/litterrobot/translations/de.json
new file mode 100644
index 00000000000000..0eee2778d05365
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/de.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "Benutzername"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litterrobot/translations/en.json b/homeassistant/components/litterrobot/translations/en.json
new file mode 100644
index 00000000000000..cb0e7bed7ea2bf
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/en.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password",
+ "username": "Username"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litterrobot/translations/es.json b/homeassistant/components/litterrobot/translations/es.json
new file mode 100644
index 00000000000000..12a48f17c3229b
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/es.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositivo ya est\u00e1 configurado"
+ },
+ "error": {
+ "cannot_connect": "Fallo al conectar",
+ "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida",
+ "unknown": "Error inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrase\u00f1a",
+ "username": "Usuario"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litterrobot/translations/et.json b/homeassistant/components/litterrobot/translations/et.json
new file mode 100644
index 00000000000000..ce02ca14929ad2
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/et.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_auth": "Vigane autentimine",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Salas\u00f5na",
+ "username": "Kasutajanimi"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litterrobot/translations/fr.json b/homeassistant/components/litterrobot/translations/fr.json
new file mode 100644
index 00000000000000..aa84ec33d8cdad
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/fr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "invalid_auth": "Authentification invalide",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litterrobot/translations/hu.json b/homeassistant/components/litterrobot/translations/hu.json
new file mode 100644
index 00000000000000..fd8db27da5efd0
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/hu.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litterrobot/translations/id.json b/homeassistant/components/litterrobot/translations/id.json
new file mode 100644
index 00000000000000..4a84db42a14f2a
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/id.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litterrobot/translations/it.json b/homeassistant/components/litterrobot/translations/it.json
new file mode 100644
index 00000000000000..843262aa31858b
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/it.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password",
+ "username": "Nome utente"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litterrobot/translations/ko.json b/homeassistant/components/litterrobot/translations/ko.json
new file mode 100644
index 00000000000000..94261de9637607
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/ko.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litterrobot/translations/nl.json b/homeassistant/components/litterrobot/translations/nl.json
new file mode 100644
index 00000000000000..50b4c3f2fe6e24
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/nl.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Wachtwoord",
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litterrobot/translations/no.json b/homeassistant/components/litterrobot/translations/no.json
new file mode 100644
index 00000000000000..4ea7b2401c30f5
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/no.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_auth": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passord",
+ "username": "Brukernavn"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litterrobot/translations/pl.json b/homeassistant/components/litterrobot/translations/pl.json
new file mode 100644
index 00000000000000..8a08a06c69904c
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/pl.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litterrobot/translations/pt.json b/homeassistant/components/litterrobot/translations/pt.json
new file mode 100644
index 00000000000000..7953cf5625ced6
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/pt.json
@@ -0,0 +1,20 @@
+{
+ "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": {
+ "password": "Palavra-passe",
+ "username": "Nome de Utilizador"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litterrobot/translations/ru.json b/homeassistant/components/litterrobot/translations/ru.json
new file mode 100644
index 00000000000000..aef0fdff54e603
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/ru.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litterrobot/translations/zh-Hant.json b/homeassistant/components/litterrobot/translations/zh-Hant.json
new file mode 100644
index 00000000000000..d232b491b68e52
--- /dev/null
+++ b/homeassistant/components/litterrobot/translations/zh-Hant.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u5bc6\u78bc",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py
new file mode 100644
index 00000000000000..a36ef6563610b9
--- /dev/null
+++ b/homeassistant/components/litterrobot/vacuum.py
@@ -0,0 +1,123 @@
+"""Support for Litter-Robot "Vacuum"."""
+from pylitterbot import Robot
+
+from homeassistant.components.vacuum import (
+ STATE_CLEANING,
+ STATE_DOCKED,
+ STATE_ERROR,
+ SUPPORT_SEND_COMMAND,
+ SUPPORT_START,
+ SUPPORT_STATE,
+ SUPPORT_STATUS,
+ SUPPORT_TURN_OFF,
+ SUPPORT_TURN_ON,
+ VacuumEntity,
+)
+from homeassistant.const import STATE_OFF
+
+from .const import DOMAIN
+from .hub import LitterRobotEntity
+
+SUPPORT_LITTERROBOT = (
+ SUPPORT_SEND_COMMAND
+ | SUPPORT_START
+ | SUPPORT_STATE
+ | SUPPORT_STATUS
+ | SUPPORT_TURN_OFF
+ | SUPPORT_TURN_ON
+)
+TYPE_LITTER_BOX = "Litter Box"
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Litter-Robot cleaner using config entry."""
+ hub = hass.data[DOMAIN][config_entry.entry_id]
+
+ entities = []
+ for robot in hub.account.robots:
+ entities.append(LitterRobotCleaner(robot, TYPE_LITTER_BOX, hub))
+
+ if entities:
+ async_add_entities(entities, True)
+
+
+class LitterRobotCleaner(LitterRobotEntity, VacuumEntity):
+ """Litter-Robot "Vacuum" Cleaner."""
+
+ @property
+ def supported_features(self):
+ """Flag cleaner robot features that are supported."""
+ return SUPPORT_LITTERROBOT
+
+ @property
+ def state(self):
+ """Return the state of the cleaner."""
+ switcher = {
+ Robot.UnitStatus.CLEAN_CYCLE: STATE_CLEANING,
+ Robot.UnitStatus.EMPTY_CYCLE: STATE_CLEANING,
+ Robot.UnitStatus.CLEAN_CYCLE_COMPLETE: STATE_DOCKED,
+ Robot.UnitStatus.CAT_SENSOR_TIMING: STATE_DOCKED,
+ Robot.UnitStatus.DRAWER_FULL_1: STATE_DOCKED,
+ Robot.UnitStatus.DRAWER_FULL_2: STATE_DOCKED,
+ Robot.UnitStatus.READY: STATE_DOCKED,
+ Robot.UnitStatus.OFF: STATE_OFF,
+ }
+
+ return switcher.get(self.robot.unit_status, STATE_ERROR)
+
+ @property
+ def status(self):
+ """Return the status of the cleaner."""
+ return f"{self.robot.unit_status.label}{' (Sleeping)' if self.robot.is_sleeping else ''}"
+
+ async def async_turn_on(self, **kwargs):
+ """Turn the cleaner on, starting a clean cycle."""
+ await self.perform_action_and_refresh(self.robot.set_power_status, True)
+
+ async def async_turn_off(self, **kwargs):
+ """Turn the unit off, stopping any cleaning in progress as is."""
+ await self.perform_action_and_refresh(self.robot.set_power_status, False)
+
+ async def async_start(self):
+ """Start a clean cycle."""
+ await self.perform_action_and_refresh(self.robot.start_cleaning)
+
+ async def async_send_command(self, command, params=None, **kwargs):
+ """Send command.
+
+ Available commands:
+ - reset_waste_drawer
+ * params: none
+ - set_sleep_mode
+ * params:
+ - enabled: bool
+ - sleep_time: str (optional)
+
+ """
+ if command == "reset_waste_drawer":
+ # Normally we need to request a refresh of data after a command is sent.
+ # However, the API for resetting the waste drawer returns a refreshed
+ # data set for the robot. Thus, we only need to tell hass to update the
+ # state of devices associated with this robot.
+ await self.robot.reset_waste_drawer()
+ self.hub.coordinator.async_set_updated_data(True)
+ elif command == "set_sleep_mode":
+ await self.perform_action_and_refresh(
+ self.robot.set_sleep_mode,
+ params.get("enabled"),
+ self.parse_time_at_default_timezone(params.get("sleep_time")),
+ )
+ else:
+ raise NotImplementedError()
+
+ @property
+ def extra_state_attributes(self):
+ """Return device specific state attributes."""
+ return {
+ "clean_cycle_wait_time_minutes": self.robot.clean_cycle_wait_time_minutes,
+ "is_sleeping": self.robot.is_sleeping,
+ "sleep_mode_active": self.robot.sleep_mode_active,
+ "power_status": self.robot.power_status,
+ "unit_status_code": self.robot.unit_status.value,
+ "last_seen": self.robot.last_seen,
+ }
diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py
index 1d06efeb708554..c94aeff24b0fda 100644
--- a/homeassistant/components/local_file/camera.py
+++ b/homeassistant/components/local_file/camera.py
@@ -10,16 +10,10 @@
PLATFORM_SCHEMA,
Camera,
)
-from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME
+from homeassistant.const import ATTR_ENTITY_ID, CONF_FILE_PATH, CONF_NAME
from homeassistant.helpers import config_validation as cv
-from .const import (
- CONF_FILE_PATH,
- DATA_LOCAL_FILE,
- DEFAULT_NAME,
- DOMAIN,
- SERVICE_UPDATE_FILE_PATH,
-)
+from .const import DATA_LOCAL_FILE, DEFAULT_NAME, DOMAIN, SERVICE_UPDATE_FILE_PATH
_LOGGER = logging.getLogger(__name__)
@@ -111,6 +105,6 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the camera state attributes."""
return {"file_path": self._file_path}
diff --git a/homeassistant/components/local_file/const.py b/homeassistant/components/local_file/const.py
index 5225a70daedc7f..3ea98f89c0e285 100644
--- a/homeassistant/components/local_file/const.py
+++ b/homeassistant/components/local_file/const.py
@@ -1,6 +1,5 @@
"""Constants for the Local File Camera component."""
DOMAIN = "local_file"
SERVICE_UPDATE_FILE_PATH = "update_file_path"
-CONF_FILE_PATH = "file_path"
DATA_LOCAL_FILE = "local_file_cameras"
DEFAULT_NAME = "Local File"
diff --git a/homeassistant/components/local_ip/sensor.py b/homeassistant/components/local_ip/sensor.py
index d159b641fa2939..1d2cce721052a4 100644
--- a/homeassistant/components/local_ip/sensor.py
+++ b/homeassistant/components/local_ip/sensor.py
@@ -1,7 +1,7 @@
"""Sensor platform for local_ip."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_NAME
-from homeassistant.helpers.entity import Entity
from homeassistant.util import get_local_ip
from .const import DOMAIN, SENSOR
@@ -13,7 +13,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities([IPSensor(name)], True)
-class IPSensor(Entity):
+class IPSensor(SensorEntity):
"""A simple sensor."""
def __init__(self, name):
diff --git a/homeassistant/components/local_ip/translations/de.json b/homeassistant/components/local_ip/translations/de.json
index 072f6ec964d12d..2d31f90139d1e3 100644
--- a/homeassistant/components/local_ip/translations/de.json
+++ b/homeassistant/components/local_ip/translations/de.json
@@ -1,13 +1,14 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Es ist nur eine einzige Konfiguration der lokalen IP zul\u00e4ssig."
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"step": {
"user": {
"data": {
"name": "Sensorname"
},
+ "description": "M\u00f6chten Sie mit der Einrichtung beginnen?",
"title": "Lokale IP-Adresse"
}
}
diff --git a/homeassistant/components/local_ip/translations/es.json b/homeassistant/components/local_ip/translations/es.json
index fe9a0ad14147ec..a3048d396d567a 100644
--- a/homeassistant/components/local_ip/translations/es.json
+++ b/homeassistant/components/local_ip/translations/es.json
@@ -8,7 +8,7 @@
"data": {
"name": "Nombre del sensor"
},
- "description": "\u00bfQuieres empezar a configurar?",
+ "description": "\u00bfQuieres iniciar la configuraci\u00f3n?",
"title": "Direcci\u00f3n IP local"
}
}
diff --git a/homeassistant/components/local_ip/translations/fr.json b/homeassistant/components/local_ip/translations/fr.json
index c1933032ed0a27..1c5a8fc963454f 100644
--- a/homeassistant/components/local_ip/translations/fr.json
+++ b/homeassistant/components/local_ip/translations/fr.json
@@ -8,6 +8,7 @@
"data": {
"name": "Nom du capteur"
},
+ "description": "Voulez-vous commencer la configuration ?",
"title": "Adresse IP locale"
}
}
diff --git a/homeassistant/components/local_ip/translations/hu.json b/homeassistant/components/local_ip/translations/hu.json
index 09b894742a6a9e..b692e87f92222f 100644
--- a/homeassistant/components/local_ip/translations/hu.json
+++ b/homeassistant/components/local_ip/translations/hu.json
@@ -1,10 +1,14 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
"step": {
"user": {
"data": {
"name": "\u00c9rz\u00e9kel\u0151 neve"
},
+ "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?",
"title": "Helyi IP c\u00edm"
}
}
diff --git a/homeassistant/components/local_ip/translations/id.json b/homeassistant/components/local_ip/translations/id.json
new file mode 100644
index 00000000000000..a7d8993baa6fe1
--- /dev/null
+++ b/homeassistant/components/local_ip/translations/id.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Nama Sensor"
+ },
+ "description": "Ingin memulai penyiapan?",
+ "title": "Alamat IP Lokal"
+ }
+ }
+ },
+ "title": "Alamat IP Lokal"
+}
\ No newline at end of file
diff --git a/homeassistant/components/local_ip/translations/ko.json b/homeassistant/components/local_ip/translations/ko.json
index 050229dbf087f6..f7248ab0f6f83f 100644
--- a/homeassistant/components/local_ip/translations/ko.json
+++ b/homeassistant/components/local_ip/translations/ko.json
@@ -1,13 +1,14 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\ud558\ub098\uc758 \ub85c\uceec IP \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"step": {
"user": {
"data": {
"name": "\uc13c\uc11c \uc774\ub984"
},
+ "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "\ub85c\uceec IP \uc8fc\uc18c"
}
}
diff --git a/homeassistant/components/local_ip/translations/nl.json b/homeassistant/components/local_ip/translations/nl.json
index ba75a9b2a4db8c..b8b033d2e73442 100644
--- a/homeassistant/components/local_ip/translations/nl.json
+++ b/homeassistant/components/local_ip/translations/nl.json
@@ -1,13 +1,14 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van lokaal IP toegestaan."
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
},
"step": {
"user": {
"data": {
"name": "Sensor Naam"
},
+ "description": "Wil je beginnen met instellen?",
"title": "Lokaal IP-adres"
}
}
diff --git a/homeassistant/components/local_ip/translations/tr.json b/homeassistant/components/local_ip/translations/tr.json
new file mode 100644
index 00000000000000..e8e82814f8af2d
--- /dev/null
+++ b/homeassistant/components/local_ip/translations/tr.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Sens\u00f6r Ad\u0131"
+ },
+ "description": "Kuruluma ba\u015flamak ister misiniz?",
+ "title": "Yerel IP Adresi"
+ }
+ }
+ },
+ "title": "Yerel IP Adresi"
+}
\ No newline at end of file
diff --git a/homeassistant/components/local_ip/translations/uk.json b/homeassistant/components/local_ip/translations/uk.json
new file mode 100644
index 00000000000000..b88c1c002bf593
--- /dev/null
+++ b/homeassistant/components/local_ip/translations/uk.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "\u041d\u0430\u0437\u0432\u0430"
+ },
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?",
+ "title": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u0430 IP-\u0430\u0434\u0440\u0435\u0441\u0430"
+ }
+ }
+ },
+ "title": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u0430 IP-\u0430\u0434\u0440\u0435\u0441\u0430"
+}
\ No newline at end of file
diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py
index 28af822ae63412..bb2a19c6380191 100644
--- a/homeassistant/components/locative/__init__.py
+++ b/homeassistant/components/locative/__init__.py
@@ -1,6 +1,7 @@
"""Support for Locative."""
+from __future__ import annotations
+
import logging
-from typing import Dict
from aiohttp import web
import voluptuous as vol
@@ -34,7 +35,7 @@ def _id(value: str) -> str:
return value.replace("-", "")
-def _validate_test_mode(obj: Dict) -> Dict:
+def _validate_test_mode(obj: dict) -> dict:
"""Validate that id is provided outside of test mode."""
if ATTR_ID not in obj and obj[ATTR_TRIGGER] != "test":
raise vol.Invalid("Location id not specified")
diff --git a/homeassistant/components/locative/translations/de.json b/homeassistant/components/locative/translations/de.json
index 326170941466e5..2df9f889e85ab9 100644
--- a/homeassistant/components/locative/translations/de.json
+++ b/homeassistant/components/locative/translations/de.json
@@ -1,11 +1,15 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.",
+ "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen."
+ },
"create_entry": {
"default": "Um Standorte Home Assistant zu senden, muss das Webhook Feature in der Locative App konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})."
},
"step": {
"user": {
- "description": "M\u00f6chtest du den Locative Webhook wirklich einrichten?",
+ "description": "M\u00f6chten Sie mit der Einrichtung beginnen?",
"title": "Locative Webhook einrichten"
}
}
diff --git a/homeassistant/components/locative/translations/hu.json b/homeassistant/components/locative/translations/hu.json
index 983ffacfa7df6a..8dc03e9c37a0c6 100644
--- a/homeassistant/components/locative/translations/hu.json
+++ b/homeassistant/components/locative/translations/hu.json
@@ -1,11 +1,15 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.",
+ "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz."
+ },
"create_entry": {
- "default": "Ha helyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Locative alkalmaz\u00e1sban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} )."
+ "default": "Ha helyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Locative alkalmaz\u00e1sban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t]({docs_url})."
},
"step": {
"user": {
- "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Locative Webhook-ot?",
+ "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?",
"title": "Locative Webhook be\u00e1ll\u00edt\u00e1sa"
}
}
diff --git a/homeassistant/components/locative/translations/id.json b/homeassistant/components/locative/translations/id.json
new file mode 100644
index 00000000000000..71aea5cc63cff7
--- /dev/null
+++ b/homeassistant/components/locative/translations/id.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.",
+ "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook."
+ },
+ "create_entry": {
+ "default": "Untuk mengirim lokasi ke Home Assistant, Anda harus mengatur fitur webhook di aplikasi Locative.\n\nIsi info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nBaca [dokumentasi]({docs_url}) untuk detail lebih lanjut."
+ },
+ "step": {
+ "user": {
+ "description": "Ingin memulai penyiapan?",
+ "title": "Siapkan Locative Webhook"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/locative/translations/ko.json b/homeassistant/components/locative/translations/ko.json
index eb10a8ca1671dc..50652f76fc543c 100644
--- a/homeassistant/components/locative/translations/ko.json
+++ b/homeassistant/components/locative/translations/ko.json
@@ -1,11 +1,15 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4."
+ },
"create_entry": {
- "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Locative \uc571\uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
+ "default": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Locative\uc571\uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
},
"step": {
"user": {
- "description": "Locative \uc6f9 \ud6c5\uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "Locative \uc6f9 \ud6c5 \uc124\uc815\ud558\uae30"
}
}
diff --git a/homeassistant/components/locative/translations/nl.json b/homeassistant/components/locative/translations/nl.json
index e02378432abaa5..d66a1262b5dbc0 100644
--- a/homeassistant/components/locative/translations/nl.json
+++ b/homeassistant/components/locative/translations/nl.json
@@ -1,14 +1,15 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk."
+ "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.",
+ "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen."
},
"create_entry": {
"default": "Om locaties naar Home Assistant te sturen, moet u de Webhook-functie instellen in de Locative app. \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Methode: POST \n\n Zie [de documentatie]({docs_url}) voor meer informatie."
},
"step": {
"user": {
- "description": "Weet u zeker dat u de Locative Webhook wilt instellen?",
+ "description": "Wil je beginnen met instellen?",
"title": "Stel de Locative Webhook in"
}
}
diff --git a/homeassistant/components/locative/translations/tr.json b/homeassistant/components/locative/translations/tr.json
new file mode 100644
index 00000000000000..84adcdf8225c43
--- /dev/null
+++ b/homeassistant/components/locative/translations/tr.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.",
+ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/locative/translations/uk.json b/homeassistant/components/locative/translations/uk.json
new file mode 100644
index 00000000000000..d9a4713087117f
--- /dev/null
+++ b/homeassistant/components/locative/translations/uk.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.",
+ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c."
+ },
+ "create_entry": {
+ "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f Locative. \n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457."
+ },
+ "step": {
+ "user": {
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?",
+ "title": "Locative"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py
index bc7dc10ba8db97..237daedae80a9b 100644
--- a/homeassistant/components/lock/__init__.py
+++ b/homeassistant/components/lock/__init__.py
@@ -2,6 +2,7 @@
from datetime import timedelta
import functools as ft
import logging
+from typing import final
import voluptuous as vol
@@ -76,7 +77,7 @@ async def async_unload_entry(hass, entry):
class LockEntity(Entity):
- """Representation of a lock."""
+ """Base class for lock entities."""
@property
def changed_by(self):
@@ -117,6 +118,7 @@ async def async_open(self, **kwargs):
"""Open the door latch."""
await self.hass.async_add_executor_job(ft.partial(self.open, **kwargs))
+ @final
@property
def state_attributes(self):
"""Return the state attributes."""
diff --git a/homeassistant/components/lock/device_action.py b/homeassistant/components/lock/device_action.py
index efdb5e352cfe31..cb0e2b0daadea1 100644
--- a/homeassistant/components/lock/device_action.py
+++ b/homeassistant/components/lock/device_action.py
@@ -1,5 +1,5 @@
"""Provides device automations for Lock."""
-from typing import List, Optional
+from __future__ import annotations
import voluptuous as vol
@@ -30,7 +30,7 @@
)
-async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device actions for Lock devices."""
registry = await entity_registry.async_get_registry(hass)
actions = []
@@ -75,11 +75,9 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
async def async_call_action_from_config(
- hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context]
+ hass: HomeAssistant, config: dict, variables: dict, context: Context | None
) -> None:
"""Execute a device action."""
- config = ACTION_SCHEMA(config)
-
service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]}
if config[CONF_TYPE] == "lock":
diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py
index a25018dc70981d..0fae680f8293f1 100644
--- a/homeassistant/components/lock/device_condition.py
+++ b/homeassistant/components/lock/device_condition.py
@@ -1,5 +1,5 @@
"""Provides device automations for Lock."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -30,7 +30,7 @@
)
-async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_conditions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device conditions for Lock devices."""
registry = await entity_registry.async_get_registry(hass)
conditions = []
diff --git a/homeassistant/components/lock/device_trigger.py b/homeassistant/components/lock/device_trigger.py
index 091811446b575c..77eb04e3735401 100644
--- a/homeassistant/components/lock/device_trigger.py
+++ b/homeassistant/components/lock/device_trigger.py
@@ -1,5 +1,5 @@
"""Provides device automations for Lock."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -10,6 +10,7 @@
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
+ CONF_FOR,
CONF_PLATFORM,
CONF_TYPE,
STATE_LOCKED,
@@ -27,11 +28,12 @@
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
+ vol.Optional(CONF_FOR): cv.positive_time_period_dict,
}
)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers for Lock devices."""
registry = await entity_registry.async_get_registry(hass)
triggers = []
@@ -42,28 +44,29 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
continue
# Add triggers for each entity that belongs to this integration
- triggers.append(
+ triggers += [
{
CONF_PLATFORM: "device",
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "locked",
+ CONF_TYPE: trigger,
}
- )
- triggers.append(
- {
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "unlocked",
- }
- )
+ for trigger in TRIGGER_TYPES
+ ]
return triggers
+async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict:
+ """List trigger capabilities."""
+ return {
+ "extra_fields": vol.Schema(
+ {vol.Optional(CONF_FOR): cv.positive_time_period_dict}
+ )
+ }
+
+
async def async_attach_trigger(
hass: HomeAssistant,
config: ConfigType,
@@ -71,21 +74,18 @@ async def async_attach_trigger(
automation_info: dict,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
- config = TRIGGER_SCHEMA(config)
-
if config[CONF_TYPE] == "locked":
- from_state = STATE_UNLOCKED
to_state = STATE_LOCKED
else:
- from_state = STATE_LOCKED
to_state = STATE_UNLOCKED
state_config = {
CONF_PLATFORM: "state",
CONF_ENTITY_ID: config[CONF_ENTITY_ID],
- state_trigger.CONF_FROM: from_state,
state_trigger.CONF_TO: to_state,
}
+ if CONF_FOR in config:
+ state_config[CONF_FOR] = config[CONF_FOR]
state_config = state_trigger.TRIGGER_SCHEMA(state_config)
return await state_trigger.async_attach_trigger(
hass, state_config, action, automation_info, platform_type="device"
diff --git a/homeassistant/components/lock/reproduce_state.py b/homeassistant/components/lock/reproduce_state.py
index 812b9bf04df9b1..0d575964b2bd3e 100644
--- a/homeassistant/components/lock/reproduce_state.py
+++ b/homeassistant/components/lock/reproduce_state.py
@@ -1,7 +1,9 @@
"""Reproduce an Lock state."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -24,8 +26,8 @@ async def _async_reproduce_state(
hass: HomeAssistantType,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -60,8 +62,8 @@ async def async_reproduce_states(
hass: HomeAssistantType,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Lock states."""
await asyncio.gather(
diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml
index d1456f1e68e464..f5f6077ddc10c5 100644
--- a/homeassistant/components/lock/services.yaml
+++ b/homeassistant/components/lock/services.yaml
@@ -21,26 +21,31 @@ get_usercode:
example: 1
lock:
+ name: Lock
description: Lock all or specified locks.
+ target:
fields:
- entity_id:
- description: Name of lock to lock.
- example: "lock.front_door"
code:
+ name: Code
description: An optional code to lock the lock with.
example: 1234
+ selector:
+ text:
open:
+ name: Open
description: Open all or specified locks.
+ target:
fields:
- entity_id:
- description: Name of lock to open.
- example: "lock.front_door"
code:
+ name: Code
description: An optional code to open the lock with.
example: 1234
+ selector:
+ text:
set_usercode:
+ name: Set usercode
description: Set a usercode to lock.
fields:
node_id:
@@ -54,11 +59,13 @@ set_usercode:
example: 1234
unlock:
+ name: Unlock
description: Unlock all or specified locks.
+ target:
fields:
- entity_id:
- description: Name of lock to unlock.
- example: "lock.front_door"
code:
+ name: Code
description: An optional code to unlock the lock with.
example: 1234
+ selector:
+ text:
diff --git a/homeassistant/components/lock/significant_change.py b/homeassistant/components/lock/significant_change.py
new file mode 100644
index 00000000000000..172bf2559c5a6d
--- /dev/null
+++ b/homeassistant/components/lock/significant_change.py
@@ -0,0 +1,22 @@
+"""Helper to test significant Lock state changes."""
+from __future__ import annotations
+
+from typing import Any
+
+from homeassistant.core import HomeAssistant, callback
+
+
+@callback
+def async_check_significant_change(
+ hass: HomeAssistant,
+ old_state: str,
+ old_attrs: dict,
+ new_state: str,
+ new_attrs: dict,
+ **kwargs: Any,
+) -> bool | None:
+ """Test if state significantly changed."""
+ if old_state != new_state:
+ return True
+
+ return False
diff --git a/homeassistant/components/lock/translations/id.json b/homeassistant/components/lock/translations/id.json
index da11e3422f15ee..d8778868651d52 100644
--- a/homeassistant/components/lock/translations/id.json
+++ b/homeassistant/components/lock/translations/id.json
@@ -1,8 +1,23 @@
{
+ "device_automation": {
+ "action_type": {
+ "lock": "Kunci {entity_name}",
+ "open": "Buka {entity_name}",
+ "unlock": "Buka kunci {entity_name}"
+ },
+ "condition_type": {
+ "is_locked": "{entity_name} terkunci",
+ "is_unlocked": "{entity_name} tidak terkunci"
+ },
+ "trigger_type": {
+ "locked": "{entity_name} terkunci",
+ "unlocked": "{entity_name} tidak terkunci"
+ }
+ },
"state": {
"_": {
"locked": "Terkunci",
- "unlocked": "Terbuka"
+ "unlocked": "Tidak Terkunci"
}
},
"title": "Kunci"
diff --git a/homeassistant/components/lock/translations/ko.json b/homeassistant/components/lock/translations/ko.json
index 6f04e2b41101be..b8db32ad51372f 100644
--- a/homeassistant/components/lock/translations/ko.json
+++ b/homeassistant/components/lock/translations/ko.json
@@ -1,17 +1,17 @@
{
"device_automation": {
"action_type": {
- "lock": "{entity_name} \uc7a0\uae08",
- "open": "{entity_name} \uc5f4\uae30",
- "unlock": "{entity_name} \uc7a0\uae08 \ud574\uc81c"
+ "lock": "{entity_name}\uc744(\ub97c) \uc7a0\uadf8\uae30",
+ "open": "{entity_name}\uc744(\ub97c) \uc5f4\uae30",
+ "unlock": "{entity_name}\uc744(\ub97c) \uc7a0\uae08 \ud574\uc81c\ud558\uae30"
},
"condition_type": {
- "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc73c\uba74",
- "is_unlocked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc9c0 \uc54a\uc73c\uba74"
+ "is_locked": "{entity_name}\uc774(\uac00) \uc7a0\uaca8\uc788\uc73c\uba74",
+ "is_unlocked": "{entity_name}\uc774(\uac00) \uc7a0\uaca8\uc788\uc9c0 \uc54a\uc73c\uba74"
},
"trigger_type": {
- "locked": "{entity_name} \uc774(\uac00) \uc7a0\uae38 \ub54c",
- "unlocked": "{entity_name} \uc774(\uac00) \uc7a0\uae08\uc774 \ud574\uc81c\ub420 \ub54c"
+ "locked": "{entity_name}\uc774(\uac00) \uc7a0\uacbc\uc744 \ub54c",
+ "unlocked": "{entity_name}\uc774(\uac00) \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc744 \ub54c"
}
},
"state": {
@@ -20,5 +20,5 @@
"unlocked": "\ud574\uc81c"
}
},
- "title": "\uc7a0\uae40"
+ "title": "\uc7a0\uae08\uc7a5\uce58"
}
\ No newline at end of file
diff --git a/homeassistant/components/lock/translations/pt.json b/homeassistant/components/lock/translations/pt.json
index 5ba9f10db141d8..44f30900572344 100644
--- a/homeassistant/components/lock/translations/pt.json
+++ b/homeassistant/components/lock/translations/pt.json
@@ -5,6 +5,9 @@
"open": "Abrir {entity_name}",
"unlock": "Desbloquear {entity_name}"
},
+ "condition_type": {
+ "is_unlocked": "{entity_name} est\u00e1 destrancado"
+ },
"trigger_type": {
"locked": "{entity_name} fechada",
"unlocked": "{entity_name} aberta"
diff --git a/homeassistant/components/lock/translations/tr.json b/homeassistant/components/lock/translations/tr.json
index 95b50398fdaad2..ea6ff1a157da23 100644
--- a/homeassistant/components/lock/translations/tr.json
+++ b/homeassistant/components/lock/translations/tr.json
@@ -1,4 +1,10 @@
{
+ "device_automation": {
+ "trigger_type": {
+ "locked": "{entity_name} kilitlendi",
+ "unlocked": "{entity_name} kilidi a\u00e7\u0131ld\u0131"
+ }
+ },
"state": {
"_": {
"locked": "Kilitli",
diff --git a/homeassistant/components/lock/translations/uk.json b/homeassistant/components/lock/translations/uk.json
index d919252eb56204..96b92012e9d30f 100644
--- a/homeassistant/components/lock/translations/uk.json
+++ b/homeassistant/components/lock/translations/uk.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "lock": "{entity_name}: \u0437\u0430\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u0442\u0438",
+ "open": "{entity_name}: \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u0438",
+ "unlock": "{entity_name}: \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u0442\u0438"
+ },
+ "condition_type": {
+ "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_unlocked": "{entity_name} \u0432 \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456"
+ },
+ "trigger_type": {
+ "locked": "{entity_name} \u0431\u043b\u043e\u043a\u0443\u0454\u0442\u044c\u0441\u044f",
+ "unlocked": "{entity_name} \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0454\u0442\u044c\u0441\u044f"
+ }
+ },
"state": {
"_": {
"locked": "\u0417\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e",
diff --git a/homeassistant/components/lock/translations/zh-Hans.json b/homeassistant/components/lock/translations/zh-Hans.json
index 4999c52f8e00c8..7e2a771971813b 100644
--- a/homeassistant/components/lock/translations/zh-Hans.json
+++ b/homeassistant/components/lock/translations/zh-Hans.json
@@ -1,13 +1,13 @@
{
"device_automation": {
"action_type": {
- "lock": "\u4e0a\u9501{entity_name}",
- "open": "\u5f00\u542f{entity_name}",
- "unlock": "\u89e3\u9501{entity_name}"
+ "lock": "\u9501\u5b9a {entity_name}",
+ "open": "\u5f00\u542f {entity_name}",
+ "unlock": "\u89e3\u9501 {entity_name}"
},
"condition_type": {
- "is_locked": "{entity_name}\u5df2\u4e0a\u9501",
- "is_unlocked": "{entity_name}\u5df2\u89e3\u9501"
+ "is_locked": "{entity_name} \u5df2\u9501\u5b9a",
+ "is_unlocked": "{entity_name} \u5df2\u89e3\u9501"
},
"trigger_type": {
"locked": "{entity_name} \u88ab\u9501\u5b9a",
diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py
index e2d8a22c251f6a..8d216b5c6f0ace 100644
--- a/homeassistant/components/logbook/__init__.py
+++ b/homeassistant/components/logbook/__init__.py
@@ -1,4 +1,5 @@
"""Event parser and human readable log generator."""
+from contextlib import suppress
from datetime import timedelta
from itertools import groupby
import json
@@ -54,8 +55,6 @@
ATTR_MESSAGE = "message"
-CONF_DOMAINS = "domains"
-CONF_ENTITIES = "entities"
CONTINUOUS_DOMAINS = ["proximity", "sensor"]
DOMAIN = "logbook"
@@ -233,6 +232,12 @@ async def get(self, request, datetime=None):
hass = request.app["hass"]
entity_matches_only = "entity_matches_only" in request.query
+ context_id = request.query.get("context_id")
+
+ if entity_ids and context_id:
+ return self.json_message(
+ "Can't combine entity with context_id", HTTP_BAD_REQUEST
+ )
def json_events():
"""Fetch events and generate JSON."""
@@ -245,6 +250,7 @@ def json_events():
self.filters,
self.entities_filter,
entity_matches_only,
+ context_id,
)
)
@@ -379,10 +385,8 @@ def humanify(hass, events, entity_attr_cache, context_lookup):
domain = event_data.get(ATTR_DOMAIN)
entity_id = event_data.get(ATTR_ENTITY_ID)
if domain is None and entity_id is not None:
- try:
+ with suppress(IndexError):
domain = split_entity_id(str(entity_id))[0]
- except IndexError:
- pass
data = {
"when": event.time_fired_isoformat,
@@ -415,8 +419,12 @@ def _get_events(
filters=None,
entities_filter=None,
entity_matches_only=False,
+ context_id=None,
):
"""Get events for a period of time."""
+ assert not (
+ entity_ids and context_id
+ ), "can't pass in both entity_ids and context_id"
entity_attr_cache = EntityAttributeCache(hass)
context_lookup = {None: None}
@@ -469,6 +477,9 @@ def yield_events(query):
filters.entity_filter() | (Events.event_type != EVENT_STATE_CHANGED)
)
+ if context_id is not None:
+ query = query.filter(Events.context_id == context_id)
+
query = query.order_by(Events.time_fired)
return list(
diff --git a/homeassistant/components/logbook/services.yaml b/homeassistant/components/logbook/services.yaml
index fb1736d7784b5c..252b0b6a39b3fa 100644
--- a/homeassistant/components/logbook/services.yaml
+++ b/homeassistant/components/logbook/services.yaml
@@ -1,15 +1,29 @@
log:
- description: Create a custom entry in your logbook.
+ description: Create a custom entry in your logbook
fields:
name:
+ name: Name
description: Custom name for an entity, can be referenced with entity_id
+ required: true
example: "Kitchen"
+ selector:
+ text:
message:
+ name: Message
description: Message of the custom logbook entry
+ required: true
example: "is being used"
+ selector:
+ text:
entity_id:
+ name: Entity ID
description: Entity to reference in custom logbook entry [Optional]
example: "light.kitchen"
+ selector:
+ entity:
domain:
+ name: Domain
description: Icon of domain to display in custom logbook entry [Optional]
example: "light"
+ selector:
+ text:
diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py
index ba0026fedc5133..fb2920fb6e25df 100644
--- a/homeassistant/components/logger/__init__.py
+++ b/homeassistant/components/logger/__init__.py
@@ -26,6 +26,7 @@
LOGGER_DEFAULT = "default"
LOGGER_LOGS = "logs"
+LOGGER_FILTERS = "filters"
ATTR_LEVEL = "level"
@@ -40,6 +41,7 @@
{
vol.Optional(LOGGER_DEFAULT): _VALID_LOG_LEVEL,
vol.Optional(LOGGER_LOGS): vol.Schema({cv.string: _VALID_LOG_LEVEL}),
+ vol.Optional(LOGGER_FILTERS): vol.Schema({cv.string: [cv.is_regex]}),
}
)
},
@@ -70,6 +72,11 @@ def set_log_levels(logpoints):
if LOGGER_LOGS in config[DOMAIN]:
set_log_levels(config[DOMAIN][LOGGER_LOGS])
+ if LOGGER_FILTERS in config[DOMAIN]:
+ for key, value in config[DOMAIN][LOGGER_FILTERS].items():
+ logger = logging.getLogger(key)
+ _add_log_filter(logger, value)
+
@callback
def async_service_handler(service):
"""Handle logger services."""
@@ -103,6 +110,15 @@ def _set_log_level(logger, level):
getattr(logger, "orig_setLevel", logger.setLevel)(LOGSEVERITY[level])
+def _add_log_filter(logger, patterns):
+ """Add a Filter to the logger based on a regexp of the filter_str."""
+
+ def filter_func(logrecord):
+ return not any(p.match(logrecord.getMessage()) for p in patterns)
+
+ logger.addFilter(filter_func)
+
+
def _get_logger_class(hass_overrides):
"""Create a logger subclass.
diff --git a/homeassistant/components/logger/services.yaml b/homeassistant/components/logger/services.yaml
index 514aac4c71cb0b..39c0bcfdfe19ff 100644
--- a/homeassistant/components/logger/services.yaml
+++ b/homeassistant/components/logger/services.yaml
@@ -1,22 +1,43 @@
set_default_level:
- description: Set the default log level for components.
+ name: Set default level
+ description: Set the default log level for integrations.
fields:
level:
- description: "Default severity level. Possible values are debug, info, warn, warning, error, fatal, critical"
+ name: Level
+ description: Default severity level for all integrations.
example: debug
+ selector:
+ select:
+ options:
+ - debug
+ - info
+ - warning
+ - error
+ - fatal
+ - critical
set_level:
- description: Set log level for components.
+ name: Set level
+ description: Set log level for integrations.
fields:
homeassistant.core:
- description: "Example on how to change the logging level for a Home Assistant core components. Possible values are debug, info, warn, warning, error, fatal, critical"
+ description:
+ "Example on how to change the logging level for a Home Assistant Core
+ integrations. Possible values are debug, info, warn, warning, error,
+ fatal, critical."
example: debug
homeassistant.components.mqtt:
- description: "Example on how to change the logging level for an Integration. Possible values are debug, info, warn, warning, error, fatal, critical"
+ description:
+ "Example on how to change the logging level for an Integration. Possible
+ values are debug, info, warn, warning, error, fatal, critical."
example: warning
custom_components.my_integration:
- description: "Example on how to change the logging level for a Custom Integration. Possible values are debug, info, warn, warning, error, fatal, critical"
+ description:
+ "Example on how to change the logging level for a Custom Integration.
+ Possible values are debug, info, warn, warning, error, fatal, critical."
example: debug
aiohttp:
- description: "Example on how to change the logging level for a Python module. Possible values are debug, info, warn, warning, error, fatal, critical"
+ description:
+ "Example on how to change the logging level for a Python module.
+ Possible values are debug, info, warn, warning, error, fatal, critical."
example: error
diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py
index d3551765079340..3364cd725c71be 100644
--- a/homeassistant/components/logi_circle/__init__.py
+++ b/homeassistant/components/logi_circle/__init__.py
@@ -11,6 +11,7 @@
from homeassistant.components.camera import ATTR_FILENAME, CAMERA_SERVICE_SCHEMA
from homeassistant.const import (
ATTR_MODE,
+ CONF_API_KEY,
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_MONITORED_CONDITIONS,
@@ -22,7 +23,6 @@
from . import config_flow
from .const import (
- CONF_API_KEY,
CONF_REDIRECT_URI,
DATA_LOGI,
DEFAULT_CACHEDB,
@@ -47,6 +47,8 @@
ATTR_VALUE = "value"
ATTR_DURATION = "duration"
+PLATFORMS = ["camera", "sensor"]
+
SENSOR_SCHEMA = vol.Schema(
{
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(LOGI_SENSORS)): vol.All(
@@ -117,7 +119,6 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, entry):
"""Set up Logi Circle from a config entry."""
-
logi_circle = LogiCircle(
client_id=entry.data[CONF_CLIENT_ID],
client_secret=entry.data[CONF_CLIENT_SECRET],
@@ -172,9 +173,9 @@ async def async_setup_entry(hass, entry):
hass.data[DATA_LOGI] = logi_circle
- for component in "camera", "sensor":
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
async def service_handler(service):
@@ -220,8 +221,8 @@ async def shut_down(event=None):
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
- for component in "camera", "sensor":
- await hass.config_entries.async_forward_entry_unload(entry, component)
+ for platform in PLATFORMS:
+ await hass.config_entries.async_forward_entry_unload(entry, platform)
logi_circle = hass.data.pop(DATA_LOGI)
diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py
index 20bc829d75d517..1afeb190c8bd12 100644
--- a/homeassistant/components/logi_circle/camera.py
+++ b/homeassistant/components/logi_circle/camera.py
@@ -125,7 +125,7 @@ def device_info(self):
}
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
state = {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py
index 279c305201085a..00fd0edc4372f9 100644
--- a/homeassistant/components/logi_circle/config_flow.py
+++ b/homeassistant/components/logi_circle/config_flow.py
@@ -10,6 +10,7 @@
from homeassistant import config_entries
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
+ CONF_API_KEY,
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_SENSORS,
@@ -17,7 +18,7 @@
)
from homeassistant.core import callback
-from .const import CONF_API_KEY, CONF_REDIRECT_URI, DEFAULT_CACHEDB, DOMAIN
+from .const import CONF_REDIRECT_URI, DEFAULT_CACHEDB, DOMAIN
_TIMEOUT = 15 # seconds
@@ -120,7 +121,6 @@ async def async_step_auth(self, user_input=None):
def _get_authorization_url(self):
"""Create temporary Circle session and generate authorization url."""
-
flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl]
client_id = flow[CONF_CLIENT_ID]
client_secret = flow[CONF_CLIENT_SECRET]
@@ -147,7 +147,6 @@ async def async_step_code(self, code=None):
async def _async_create_session(self, code):
"""Create Logi Circle session and entries."""
-
flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN]
client_id = flow[CONF_CLIENT_ID]
client_secret = flow[CONF_CLIENT_SECRET]
diff --git a/homeassistant/components/logi_circle/const.py b/homeassistant/components/logi_circle/const.py
index fb22338b2c7589..92967d2eb84845 100644
--- a/homeassistant/components/logi_circle/const.py
+++ b/homeassistant/components/logi_circle/const.py
@@ -4,7 +4,6 @@
DOMAIN = "logi_circle"
DATA_LOGI = DOMAIN
-CONF_API_KEY = "api_key"
CONF_REDIRECT_URI = "redirect_uri"
DEFAULT_CACHEDB = ".logi_cache.pickle"
diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py
index 4a5fedaf57a860..29cd6e28e1c7d4 100644
--- a/homeassistant/components/logi_circle/sensor.py
+++ b/homeassistant/components/logi_circle/sensor.py
@@ -1,6 +1,7 @@
"""Support for Logi Circle sensors."""
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_BATTERY_CHARGING,
@@ -9,7 +10,6 @@
STATE_OFF,
STATE_ON,
)
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.util.dt import as_local
@@ -42,7 +42,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
async_add_entities(sensors, True)
-class LogiSensor(Entity):
+class LogiSensor(SensorEntity):
"""A sensor implementation for a Logi Circle camera."""
def __init__(self, camera, time_zone, sensor_type):
@@ -83,7 +83,7 @@ def device_info(self):
}
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
state = {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/logi_circle/translations/de.json b/homeassistant/components/logi_circle/translations/de.json
index ab4a194fda00af..1eec1d3c4a5218 100644
--- a/homeassistant/components/logi_circle/translations/de.json
+++ b/homeassistant/components/logi_circle/translations/de.json
@@ -1,11 +1,15 @@
{
"config": {
"abort": {
+ "already_configured": "Konto wurde bereits konfiguriert",
"external_error": "Es ist eine Ausnahme in einem anderen Flow aufgetreten.",
- "external_setup": "Logi Circle wurde erfolgreich aus einem anderen Flow konfiguriert."
+ "external_setup": "Logi Circle wurde erfolgreich aus einem anderen Flow konfiguriert.",
+ "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen."
},
"error": {
- "follow_link": "Bitte folge dem Link und authentifiziere dich, bevor du auf Senden klickst."
+ "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.",
+ "follow_link": "Bitte folge dem Link und authentifiziere dich, bevor du auf Senden klickst.",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
},
"step": {
"auth": {
diff --git a/homeassistant/components/logi_circle/translations/fr.json b/homeassistant/components/logi_circle/translations/fr.json
index 7ac388ccb3f371..6bd22f473e7cca 100644
--- a/homeassistant/components/logi_circle/translations/fr.json
+++ b/homeassistant/components/logi_circle/translations/fr.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9",
+ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9",
"external_error": "Une exception est survenue \u00e0 partir d'un autre flux.",
"external_setup": "Logi Circle a \u00e9t\u00e9 configur\u00e9 avec succ\u00e8s \u00e0 partir d'un autre flux.",
"missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation."
diff --git a/homeassistant/components/logi_circle/translations/hu.json b/homeassistant/components/logi_circle/translations/hu.json
index 04c8229f5ff941..9c788350de4962 100644
--- a/homeassistant/components/logi_circle/translations/hu.json
+++ b/homeassistant/components/logi_circle/translations/hu.json
@@ -1,7 +1,13 @@
{
"config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t."
+ },
"error": {
- "follow_link": "K\u00e9rlek, k\u00f6vesd a hivatkoz\u00e1st \u00e9s hiteles\u00edtsd magad miel\u0151tt megnyomod a K\u00fcld\u00e9s gombot"
+ "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.",
+ "follow_link": "K\u00e9rlek, k\u00f6vesd a hivatkoz\u00e1st \u00e9s hiteles\u00edtsd magad miel\u0151tt megnyomod a K\u00fcld\u00e9s gombot",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
},
"step": {
"user": {
diff --git a/homeassistant/components/logi_circle/translations/id.json b/homeassistant/components/logi_circle/translations/id.json
new file mode 100644
index 00000000000000..3cbfdd03978f27
--- /dev/null
+++ b/homeassistant/components/logi_circle/translations/id.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi",
+ "external_error": "Eksepsi terjadi dari alur lain.",
+ "external_setup": "Logi Circle berhasil dikonfigurasi dari alur lain.",
+ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi."
+ },
+ "error": {
+ "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.",
+ "follow_link": "Ikuti tautan dan autentikasi sebelum menekan Kirim.",
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "step": {
+ "auth": {
+ "description": "Buka tautan di bawah ini dan **Terima** akses ke akun Logi Circle Anda, lalu kembali dan tekan **Kirim** di bawah ini.\n\n[Tautan] ({authorization_url})",
+ "title": "Autentikasi dengan Logi Circle"
+ },
+ "user": {
+ "data": {
+ "flow_impl": "Penyedia"
+ },
+ "description": "Pilih melalui penyedia autentikasi mana yang ingin Anda autentikasi dengan Logi Circle.",
+ "title": "Penyedia Autentikasi"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/logi_circle/translations/ko.json b/homeassistant/components/logi_circle/translations/ko.json
index 3fe8ce4824e097..733c95f95e0338 100644
--- a/homeassistant/components/logi_circle/translations/ko.json
+++ b/homeassistant/components/logi_circle/translations/ko.json
@@ -1,11 +1,15 @@
{
"config": {
"abort": {
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"external_error": "\ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc608\uc678\uc0ac\ud56d\uc774 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
- "external_setup": "Logi Circle \uc774 \ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "external_setup": "Logi Circle\uc774 \ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694."
},
"error": {
- "follow_link": "\ud655\uc778\uc744 \ud074\ub9ad\ud558\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694"
+ "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "follow_link": "\ud655\uc778\uc744 \ud074\ub9ad\ud558\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
"auth": {
diff --git a/homeassistant/components/logi_circle/translations/lb.json b/homeassistant/components/logi_circle/translations/lb.json
index fab157b2655920..82be2f6a82d7d9 100644
--- a/homeassistant/components/logi_circle/translations/lb.json
+++ b/homeassistant/components/logi_circle/translations/lb.json
@@ -7,6 +7,7 @@
"missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun."
},
"error": {
+ "authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL.",
"follow_link": "Follegt w.e.g dem Link an authentifiz\u00e9iert iech ier de op Ofsch\u00e9cken dr\u00e9ckt.",
"invalid_auth": "Ong\u00eblteg Authentifikatioun"
},
diff --git a/homeassistant/components/logi_circle/translations/nl.json b/homeassistant/components/logi_circle/translations/nl.json
index b521af1f969a2a..8c4d81d120eb64 100644
--- a/homeassistant/components/logi_circle/translations/nl.json
+++ b/homeassistant/components/logi_circle/translations/nl.json
@@ -3,15 +3,17 @@
"abort": {
"already_configured": "Account is al geconfigureerd",
"external_error": "Uitzondering opgetreden uit een andere stroom.",
- "external_setup": "Logi Circle is met succes geconfigureerd vanuit een andere stroom."
+ "external_setup": "Logi Circle is met succes geconfigureerd vanuit een andere stroom.",
+ "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen."
},
"error": {
+ "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
"follow_link": "Volg de link en authenticeer voordat u op Verzenden drukt.",
"invalid_auth": "Ongeldige authenticatie"
},
"step": {
"auth": {
- "description": "Volg de onderstaande link en Accepteer toegang tot uw Logi Circle-account, kom dan terug en druk hieronder op Verzenden . \n\n [Link] ({authorization_url})",
+ "description": "Volg de onderstaande link en **Accepteer** toegang tot uw Logi Circle-account, kom dan terug en druk hieronder op **Verzenden** . \n\n [Link] ({authorization_url})",
"title": "Authenticeren met Logi Circle"
},
"user": {
diff --git a/homeassistant/components/logi_circle/translations/ru.json b/homeassistant/components/logi_circle/translations/ru.json
index 2a7ccc4f3741bc..8da20b60c394c8 100644
--- a/homeassistant/components/logi_circle/translations/ru.json
+++ b/homeassistant/components/logi_circle/translations/ru.json
@@ -9,7 +9,7 @@
"error": {
"authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
"follow_link": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e, \u043f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043d\u0430\u0436\u0430\u0442\u044c \"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\".",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"step": {
"auth": {
diff --git a/homeassistant/components/logi_circle/translations/tr.json b/homeassistant/components/logi_circle/translations/tr.json
new file mode 100644
index 00000000000000..0b0f58116c234f
--- /dev/null
+++ b/homeassistant/components/logi_circle/translations/tr.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/logi_circle/translations/uk.json b/homeassistant/components/logi_circle/translations/uk.json
new file mode 100644
index 00000000000000..2c021992413d79
--- /dev/null
+++ b/homeassistant/components/logi_circle/translations/uk.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "external_error": "\u0412\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0432\u0456\u0434\u0431\u0443\u043b\u043e\u0441\u044f \u0437 \u0456\u043d\u0448\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0443.",
+ "external_setup": "Logi Circle \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439 \u0437 \u0456\u043d\u0448\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0443.",
+ "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438."
+ },
+ "error": {
+ "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "follow_link": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c \u0456 \u043f\u0440\u043e\u0439\u0434\u0456\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e, \u043f\u0435\u0440\u0448 \u043d\u0456\u0436 \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0438 \"\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438\".",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "step": {
+ "auth": {
+ "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043f\u043e [\u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c]({authorization_url}) \u0456 ** \u0414\u043e\u0437\u0432\u043e\u043b\u044c\u0442\u0435 ** \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0432\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Logi Circle, \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0435\u0440\u043d\u0456\u0442\u044c\u0441\u044f \u0441\u044e\u0434\u0438 \u0456 \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **.",
+ "title": "Logi Circle"
+ },
+ "user": {
+ "data": {
+ "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457, \u0447\u0435\u0440\u0435\u0437 \u044f\u043a\u0438\u0439 \u0431\u0443\u0434\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0438\u0439 \u0432\u0445\u0456\u0434.",
+ "title": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/london_air/sensor.py b/homeassistant/components/london_air/sensor.py
index a8bebc20cf566d..23eea5c00e0b98 100644
--- a/homeassistant/components/london_air/sensor.py
+++ b/homeassistant/components/london_air/sensor.py
@@ -5,10 +5,9 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import HTTP_OK
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -90,7 +89,7 @@ def update(self):
self.data = parse_api_response(response.json())
-class AirSensor(Entity):
+class AirSensor(SensorEntity):
"""Single authority air sensor."""
ICON = "mdi:cloud-outline"
@@ -124,7 +123,7 @@ def icon(self):
return self.ICON
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return other details about the sensor state."""
attrs = {}
attrs["updated"] = self._updated
diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py
index c39ef2885b0d03..b7f2cb50cbfbe6 100644
--- a/homeassistant/components/london_underground/sensor.py
+++ b/homeassistant/components/london_underground/sensor.py
@@ -4,10 +4,9 @@
from london_tube_status import TubeData
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
ATTRIBUTION = "Powered by TfL Open Data"
@@ -51,7 +50,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class LondonTubeSensor(Entity):
+class LondonTubeSensor(SensorEntity):
"""Sensor that reads the status of a line from Tube Data."""
def __init__(self, name, data):
@@ -78,7 +77,7 @@ def icon(self):
return ICON
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return other details about the sensor state."""
self.attrs["Description"] = self._description
return self.attrs
diff --git a/homeassistant/components/loopenergy/sensor.py b/homeassistant/components/loopenergy/sensor.py
index 85494d354b50ad..78e55f22eb8e92 100644
--- a/homeassistant/components/loopenergy/sensor.py
+++ b/homeassistant/components/loopenergy/sensor.py
@@ -4,14 +4,13 @@
import pyloopenergy
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_UNIT_SYSTEM_IMPERIAL,
CONF_UNIT_SYSTEM_METRIC,
EVENT_HOMEASSISTANT_STOP,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -82,7 +81,7 @@ def stop_loopenergy(event):
add_entities(sensors)
-class LoopEnergyDevice(Entity):
+class LoopEnergySensor(SensorEntity):
"""Implementation of an Loop Energy base sensor."""
def __init__(self, controller):
@@ -116,7 +115,7 @@ def _callback(self):
self.schedule_update_ha_state(True)
-class LoopEnergyElec(LoopEnergyDevice):
+class LoopEnergyElec(LoopEnergySensor):
"""Implementation of an Loop Energy Electricity sensor."""
def __init__(self, controller):
@@ -133,7 +132,7 @@ def update(self):
self._state = round(self._controller.electricity_useage, 2)
-class LoopEnergyGas(LoopEnergyDevice):
+class LoopEnergyGas(LoopEnergySensor):
"""Implementation of an Loop Energy Gas sensor."""
def __init__(self, controller):
diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py
index 99b00a92289c04..45011239f167ee 100644
--- a/homeassistant/components/lovelace/__init__.py
+++ b/homeassistant/components/lovelace/__init__.py
@@ -5,7 +5,7 @@
from homeassistant.components import frontend
from homeassistant.config import async_hass_config_yaml, async_process_component_config
-from homeassistant.const import CONF_FILENAME
+from homeassistant.const import CONF_FILENAME, CONF_MODE, CONF_RESOURCES
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import collection, config_validation as cv
@@ -16,9 +16,7 @@
from . import dashboard, resources, websocket
from .const import (
CONF_ICON,
- CONF_MODE,
CONF_REQUIRE_ADMIN,
- CONF_RESOURCES,
CONF_SHOW_IN_SIDEBAR,
CONF_TITLE,
CONF_URL_PATH,
@@ -36,7 +34,7 @@
STORAGE_DASHBOARD_UPDATE_FIELDS,
url_slug,
)
-from .system_health import system_health_info # NOQA
+from .system_health import system_health_info # noqa: F401
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/lovelace/const.py b/homeassistant/components/lovelace/const.py
index e93649de451f43..6952a80a214a62 100644
--- a/homeassistant/components/lovelace/const.py
+++ b/homeassistant/components/lovelace/const.py
@@ -3,7 +3,7 @@
import voluptuous as vol
-from homeassistant.const import CONF_ICON, CONF_TYPE, CONF_URL
+from homeassistant.const import CONF_ICON, CONF_MODE, CONF_TYPE, CONF_URL
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.util import slugify
@@ -13,13 +13,11 @@
DEFAULT_ICON = "hass:view-dashboard"
-CONF_MODE = "mode"
MODE_YAML = "yaml"
MODE_STORAGE = "storage"
MODE_AUTO = "auto-gen"
LOVELACE_CONFIG_FILE = "ui-lovelace.yaml"
-CONF_RESOURCES = "resources"
CONF_URL_PATH = "url_path"
CONF_RESOURCE_TYPE_WS = "res_type"
diff --git a/homeassistant/components/lovelace/dashboard.py b/homeassistant/components/lovelace/dashboard.py
index 2d3196054e30a9..93b127259d20aa 100644
--- a/homeassistant/components/lovelace/dashboard.py
+++ b/homeassistant/components/lovelace/dashboard.py
@@ -1,7 +1,10 @@
"""Lovelace dashboard support."""
+from __future__ import annotations
+
from abc import ABC, abstractmethod
import logging
import os
+from pathlib import Path
import time
from typing import Optional, cast
@@ -12,7 +15,7 @@
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import collection, storage
-from homeassistant.util.yaml import load_yaml
+from homeassistant.util.yaml import Secrets, load_yaml
from .const import (
CONF_ICON,
@@ -201,7 +204,7 @@ def _load_config(self, force):
is_updated = self._cache is not None
try:
- config = load_yaml(self.path)
+ config = load_yaml(self.path, Secrets(Path(self.hass.config.config_dir)))
except FileNotFoundError:
raise ConfigNotFound from None
@@ -230,7 +233,7 @@ def __init__(self, hass):
_LOGGER,
)
- async def _async_load_data(self) -> Optional[dict]:
+ async def _async_load_data(self) -> dict | None:
"""Load the data."""
data = await self.store.async_load()
diff --git a/homeassistant/components/lovelace/resources.py b/homeassistant/components/lovelace/resources.py
index 78a23540ed497f..6a97d5c4192136 100644
--- a/homeassistant/components/lovelace/resources.py
+++ b/homeassistant/components/lovelace/resources.py
@@ -1,18 +1,19 @@
"""Lovelace resources support."""
+from __future__ import annotations
+
import logging
-from typing import List, Optional, cast
+from typing import Optional, cast
import uuid
import voluptuous as vol
-from homeassistant.const import CONF_TYPE
+from homeassistant.const import CONF_RESOURCES, CONF_TYPE
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import collection, storage
from .const import (
CONF_RESOURCE_TYPE_WS,
- CONF_RESOURCES,
DOMAIN,
RESOURCE_CREATE_FIELDS,
RESOURCE_SCHEMA,
@@ -39,7 +40,7 @@ async def async_get_info(self):
return {"resources": len(self.async_items() or [])}
@callback
- def async_items(self) -> List[dict]:
+ def async_items(self) -> list[dict]:
"""Return list of items in collection."""
return self.data
@@ -67,7 +68,7 @@ async def async_get_info(self):
return {"resources": len(self.async_items() or [])}
- async def _async_load_data(self) -> Optional[dict]:
+ async def _async_load_data(self) -> dict | None:
"""Load the data."""
data = await self.store.async_load()
diff --git a/homeassistant/components/lovelace/services.yaml b/homeassistant/components/lovelace/services.yaml
index 1147f287e596af..b324b551e942ce 100644
--- a/homeassistant/components/lovelace/services.yaml
+++ b/homeassistant/components/lovelace/services.yaml
@@ -1,4 +1,4 @@
# Describes the format for available lovelace services
reload_resources:
- description: Reload Lovelace resources from yaml configuration.
+ description: Reload Lovelace resources from YAML configuration
diff --git a/homeassistant/components/lovelace/system_health.py b/homeassistant/components/lovelace/system_health.py
index a148427c9bd8cd..2f4cfc6af76dee 100644
--- a/homeassistant/components/lovelace/system_health.py
+++ b/homeassistant/components/lovelace/system_health.py
@@ -2,9 +2,10 @@
import asyncio
from homeassistant.components import system_health
+from homeassistant.const import CONF_MODE
from homeassistant.core import HomeAssistant, callback
-from .const import CONF_MODE, DOMAIN, MODE_AUTO, MODE_STORAGE, MODE_YAML
+from .const import DOMAIN, MODE_AUTO, MODE_STORAGE, MODE_YAML
@callback
diff --git a/homeassistant/components/lovelace/translations/de.json b/homeassistant/components/lovelace/translations/de.json
index c8680fcb7e5fbc..b6c7562f0ec2e3 100644
--- a/homeassistant/components/lovelace/translations/de.json
+++ b/homeassistant/components/lovelace/translations/de.json
@@ -1,6 +1,9 @@
{
"system_health": {
"info": {
+ "dashboards": "Dashboards",
+ "mode": "Modus",
+ "resources": "Ressourcen",
"views": "Ansichten"
}
}
diff --git a/homeassistant/components/lovelace/translations/fr.json b/homeassistant/components/lovelace/translations/fr.json
new file mode 100644
index 00000000000000..f2847bcc177204
--- /dev/null
+++ b/homeassistant/components/lovelace/translations/fr.json
@@ -0,0 +1,10 @@
+{
+ "system_health": {
+ "info": {
+ "dashboards": "Tableaux de bord",
+ "mode": "Mode",
+ "resources": "Ressources",
+ "views": "Vues"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lovelace/translations/id.json b/homeassistant/components/lovelace/translations/id.json
new file mode 100644
index 00000000000000..d945bc04f2235f
--- /dev/null
+++ b/homeassistant/components/lovelace/translations/id.json
@@ -0,0 +1,10 @@
+{
+ "system_health": {
+ "info": {
+ "dashboards": "Dasbor",
+ "mode": "Mode",
+ "resources": "Sumber Daya",
+ "views": "Tampilan"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lovelace/translations/ko.json b/homeassistant/components/lovelace/translations/ko.json
new file mode 100644
index 00000000000000..48a26a1371c9ef
--- /dev/null
+++ b/homeassistant/components/lovelace/translations/ko.json
@@ -0,0 +1,10 @@
+{
+ "system_health": {
+ "info": {
+ "dashboards": "\ub300\uc2dc\ubcf4\ub4dc",
+ "mode": "\ubaa8\ub4dc",
+ "resources": "\ub9ac\uc18c\uc2a4",
+ "views": "\ubdf0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lovelace/translations/tr.json b/homeassistant/components/lovelace/translations/tr.json
index 9f763d0d6cc351..d159e058ffa4e5 100644
--- a/homeassistant/components/lovelace/translations/tr.json
+++ b/homeassistant/components/lovelace/translations/tr.json
@@ -3,7 +3,8 @@
"info": {
"dashboards": "Kontrol panelleri",
"mode": "Mod",
- "resources": "Kaynaklar"
+ "resources": "Kaynaklar",
+ "views": "G\u00f6r\u00fcn\u00fcmler"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lovelace/translations/uk.json b/homeassistant/components/lovelace/translations/uk.json
new file mode 100644
index 00000000000000..21d97fd14c38bb
--- /dev/null
+++ b/homeassistant/components/lovelace/translations/uk.json
@@ -0,0 +1,10 @@
+{
+ "system_health": {
+ "info": {
+ "dashboards": "\u041f\u0430\u043d\u0435\u043b\u0456",
+ "mode": "\u0420\u0435\u0436\u0438\u043c",
+ "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438",
+ "views": "\u0412\u043a\u043b\u0430\u0434\u043a\u0438"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json
index 3b51aab6e4acbb..95fd6fc35ad84c 100644
--- a/homeassistant/components/luci/manifest.json
+++ b/homeassistant/components/luci/manifest.json
@@ -2,6 +2,6 @@
"domain": "luci",
"name": "OpenWRT (luci)",
"documentation": "https://www.home-assistant.io/integrations/luci",
- "requirements": ["openwrt-luci-rpc==1.1.6"],
+ "requirements": ["openwrt-luci-rpc==1.1.8"],
"codeowners": ["@mzdrale"]
}
diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py
index 515d8ad577f0d8..aec77961b94dbc 100644
--- a/homeassistant/components/luftdaten/sensor.py
+++ b/homeassistant/components/luftdaten/sensor.py
@@ -1,6 +1,7 @@
"""Support for Luftdaten sensors."""
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_LATITUDE,
@@ -9,7 +10,6 @@
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from . import (
DATA_LUFTDATEN,
@@ -45,7 +45,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
async_add_entities(sensors, True)
-class LuftdatenSensor(Entity):
+class LuftdatenSensor(SensorEntity):
"""Implementation of a Luftdaten sensor."""
def __init__(self, luftdaten, sensor_type, name, icon, unit, show):
@@ -94,7 +94,7 @@ def unique_id(self) -> str:
return None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
self._attrs[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION
diff --git a/homeassistant/components/luftdaten/translations/de.json b/homeassistant/components/luftdaten/translations/de.json
index 122dc611870de5..499a65623b004b 100644
--- a/homeassistant/components/luftdaten/translations/de.json
+++ b/homeassistant/components/luftdaten/translations/de.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "already_configured": "Der Dienst ist bereits konfiguriert",
"cannot_connect": "Verbindung fehlgeschlagen",
"invalid_sensor": "Sensor nicht verf\u00fcgbar oder ung\u00fcltig"
},
diff --git a/homeassistant/components/luftdaten/translations/hu.json b/homeassistant/components/luftdaten/translations/hu.json
index 4385c09d6efad0..2fa90c23ca8d25 100644
--- a/homeassistant/components/luftdaten/translations/hu.json
+++ b/homeassistant/components/luftdaten/translations/hu.json
@@ -1,6 +1,8 @@
{
"config": {
"error": {
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
"invalid_sensor": "Az \u00e9rz\u00e9kel\u0151 nem el\u00e9rhet\u0151 vagy \u00e9rv\u00e9nytelen"
},
"step": {
diff --git a/homeassistant/components/luftdaten/translations/id.json b/homeassistant/components/luftdaten/translations/id.json
new file mode 100644
index 00000000000000..96ec6d5f20ff10
--- /dev/null
+++ b/homeassistant/components/luftdaten/translations/id.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Layanan sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung",
+ "invalid_sensor": "Sensor tidak tersedia atau tidak valid"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "show_on_map": "Tampilkan di peta",
+ "station_id": "ID Sensor Luftdaten"
+ },
+ "title": "Konfigurasikan Luftdaten"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/luftdaten/translations/ko.json b/homeassistant/components/luftdaten/translations/ko.json
index eb69dfb64a2687..fbb5a26e7ee4ec 100644
--- a/homeassistant/components/luftdaten/translations/ko.json
+++ b/homeassistant/components/luftdaten/translations/ko.json
@@ -1,6 +1,8 @@
{
"config": {
"error": {
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_sensor": "\uc13c\uc11c\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uac70\ub098 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4"
},
"step": {
diff --git a/homeassistant/components/luftdaten/translations/nl.json b/homeassistant/components/luftdaten/translations/nl.json
index b3bdf2442b857d..dc913232e8cc4f 100644
--- a/homeassistant/components/luftdaten/translations/nl.json
+++ b/homeassistant/components/luftdaten/translations/nl.json
@@ -2,6 +2,7 @@
"config": {
"error": {
"already_configured": "Service is al geconfigureerd",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_sensor": "Sensor niet beschikbaar of ongeldig"
},
"step": {
diff --git a/homeassistant/components/luftdaten/translations/tr.json b/homeassistant/components/luftdaten/translations/tr.json
new file mode 100644
index 00000000000000..04565de3d28b2d
--- /dev/null
+++ b/homeassistant/components/luftdaten/translations/tr.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/luftdaten/translations/uk.json b/homeassistant/components/luftdaten/translations/uk.json
new file mode 100644
index 00000000000000..9fd33dc3da2697
--- /dev/null
+++ b/homeassistant/components/luftdaten/translations/uk.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_sensor": "\u0421\u0435\u043d\u0441\u043e\u0440 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0430\u0431\u043e \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043d\u0430 \u043c\u0430\u043f\u0456",
+ "station_id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 Luftdaten"
+ },
+ "title": "Luftdaten"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py
index 2eeebac4c96ceb..b38968c36b8e2c 100644
--- a/homeassistant/components/lutron/__init__.py
+++ b/homeassistant/components/lutron/__init__.py
@@ -12,6 +12,8 @@
DOMAIN = "lutron"
+PLATFORMS = ["light", "cover", "switch", "scene", "binary_sensor"]
+
_LOGGER = logging.getLogger(__name__)
LUTRON_BUTTONS = "lutron_buttons"
@@ -37,7 +39,7 @@
def setup(hass, base_config):
- """Set up the Lutron component."""
+ """Set up the Lutron integration."""
hass.data[LUTRON_BUTTONS] = []
hass.data[LUTRON_CONTROLLER] = None
hass.data[LUTRON_DEVICES] = {
@@ -92,8 +94,8 @@ def setup(hass, base_config):
(area.name, area.occupancy_group)
)
- for component in ("light", "cover", "switch", "scene", "binary_sensor"):
- discovery.load_platform(hass, component, DOMAIN, {}, base_config)
+ for platform in PLATFORMS:
+ discovery.load_platform(hass, platform, DOMAIN, {}, base_config)
return True
diff --git a/homeassistant/components/lutron/binary_sensor.py b/homeassistant/components/lutron/binary_sensor.py
index f77a2b120daced..6fb394d333cf6b 100644
--- a/homeassistant/components/lutron/binary_sensor.py
+++ b/homeassistant/components/lutron/binary_sensor.py
@@ -49,6 +49,6 @@ def name(self):
return f"{self._area_name} Occupancy"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {"lutron_integration_id": self._lutron_device.id}
diff --git a/homeassistant/components/lutron/cover.py b/homeassistant/components/lutron/cover.py
index f1faed32161f53..6ee53950ef2445 100644
--- a/homeassistant/components/lutron/cover.py
+++ b/homeassistant/components/lutron/cover.py
@@ -64,6 +64,6 @@ def update(self):
_LOGGER.debug("Lutron ID: %d updated to %f", self._lutron_device.id, level)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {"lutron_integration_id": self._lutron_device.id}
diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py
index d74d24f71a197c..de94b6d6ead2f8 100644
--- a/homeassistant/components/lutron/light.py
+++ b/homeassistant/components/lutron/light.py
@@ -65,7 +65,7 @@ def turn_off(self, **kwargs):
self._lutron_device.level = 0
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {"lutron_integration_id": self._lutron_device.id}
diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json
index 2dbeb51da58ede..fdd47d9005d622 100644
--- a/homeassistant/components/lutron/manifest.json
+++ b/homeassistant/components/lutron/manifest.json
@@ -2,6 +2,6 @@
"domain": "lutron",
"name": "Lutron",
"documentation": "https://www.home-assistant.io/integrations/lutron",
- "requirements": ["pylutron==0.2.5"],
+ "requirements": ["pylutron==0.2.7"],
"codeowners": ["@JonGilmore"]
}
diff --git a/homeassistant/components/lutron/switch.py b/homeassistant/components/lutron/switch.py
index 21586aaa26640b..f78f46b6733592 100644
--- a/homeassistant/components/lutron/switch.py
+++ b/homeassistant/components/lutron/switch.py
@@ -42,7 +42,7 @@ def turn_off(self, **kwargs):
self._lutron_device.level = 0
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {"lutron_integration_id": self._lutron_device.id}
@@ -75,7 +75,7 @@ def turn_off(self, **kwargs):
self._lutron_device.state = 0
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
"keypad": self._keypad_name,
diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py
index 7526d4874f6713..89eef781c256cd 100644
--- a/homeassistant/components/lutron_caseta/__init__.py
+++ b/homeassistant/components/lutron_caseta/__init__.py
@@ -1,10 +1,12 @@
"""Component for interacting with a Lutron Caseta system."""
import asyncio
import logging
+import ssl
from aiolip import LIP
from aiolip.data import LIPMode
from aiolip.protocol import LIP_BUTTON_PRESS
+import async_timeout
from pylutron_caseta.smartbridge import Smartbridge
import voluptuous as vol
@@ -29,6 +31,7 @@
BRIDGE_DEVICE_ID,
BRIDGE_LEAP,
BRIDGE_LIP,
+ BRIDGE_TIMEOUT,
BUTTON_DEVICES,
CONF_CA_CERTS,
CONF_CERTFILE,
@@ -59,12 +62,11 @@
extra=vol.ALLOW_EXTRA,
)
-LUTRON_CASETA_COMPONENTS = ["light", "switch", "cover", "scene", "fan", "binary_sensor"]
+PLATFORMS = ["light", "switch", "cover", "scene", "fan", "binary_sensor"]
async def async_setup(hass, base_config):
"""Set up the Lutron component."""
-
hass.data.setdefault(DOMAIN, {})
if DOMAIN in base_config:
@@ -89,20 +91,30 @@ async def async_setup(hass, base_config):
async def async_setup_entry(hass, config_entry):
"""Set up a bridge from a config entry."""
-
host = config_entry.data[CONF_HOST]
keyfile = hass.config.path(config_entry.data[CONF_KEYFILE])
certfile = hass.config.path(config_entry.data[CONF_CERTFILE])
ca_certs = hass.config.path(config_entry.data[CONF_CA_CERTS])
+ bridge = None
- bridge = Smartbridge.create_tls(
- hostname=host, keyfile=keyfile, certfile=certfile, ca_certs=ca_certs
- )
+ try:
+ bridge = Smartbridge.create_tls(
+ hostname=host, keyfile=keyfile, certfile=certfile, ca_certs=ca_certs
+ )
+ except ssl.SSLError:
+ _LOGGER.error("Invalid certificate used to connect to bridge at %s", host)
+ return False
- await bridge.connect()
- if not bridge.is_connected():
+ timed_out = True
+ try:
+ async with async_timeout.timeout(BRIDGE_TIMEOUT):
+ await bridge.connect()
+ timed_out = False
+ except asyncio.TimeoutError:
+ _LOGGER.error("Timeout while trying to connect to bridge at %s", host)
+
+ if timed_out or not bridge.is_connected():
await bridge.close()
- _LOGGER.error("Unable to connect to Lutron Caseta bridge at %s", host)
raise ConfigEntryNotReady
_LOGGER.debug("Connected to Lutron Caseta bridge via LEAP at %s", host)
@@ -111,7 +123,7 @@ async def async_setup_entry(hass, config_entry):
bridge_device = devices[BRIDGE_DEVICE_ID]
await _async_register_bridge_device(hass, config_entry.entry_id, bridge_device)
# Store this bridge (keyed by entry_id) so it can be retrieved by the
- # components we're setting up.
+ # platforms we're setting up.
hass.data[DOMAIN][config_entry.entry_id] = {
BRIDGE_LEAP: bridge,
BRIDGE_DEVICE: bridge_device,
@@ -125,9 +137,9 @@ async def async_setup_entry(hass, config_entry):
# pico remotes to control other devices.
await async_setup_lip(hass, config_entry, bridge.lip_devices)
- for component in LUTRON_CASETA_COMPONENTS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return True
@@ -144,7 +156,11 @@ async def async_setup_lip(hass, config_entry, lip_devices):
try:
await lip.async_connect(host)
except asyncio.TimeoutError:
- _LOGGER.error("Failed to connect to via LIP at %s:23", host)
+ _LOGGER.warning(
+ "Failed to connect to via LIP at %s:23, Pico and Shade remotes will not be available; "
+ "Enable Telnet Support in the Lutron app under Settings >> Advanced >> Integration",
+ host,
+ )
return
_LOGGER.debug("Connected to Lutron Caseta bridge via LIP at %s:23", host)
@@ -211,6 +227,7 @@ async def _async_register_button_devices(
dr_device = device_registry.async_get_or_create(
name=device["leap_name"],
+ suggested_area=device["leap_name"].split("_")[0],
manufacturer=MANUFACTURER,
config_entry_id=config_entry_id,
identifiers={(DOMAIN, device["serial"])},
@@ -261,7 +278,6 @@ def _async_lip_event(lip_message):
async def async_unload_entry(hass, config_entry):
"""Unload the bridge bridge from a config entry."""
-
data = hass.data[DOMAIN][config_entry.entry_id]
data[BRIDGE_LEAP].close()
if data[BRIDGE_LIP]:
@@ -270,8 +286,8 @@ async def async_unload_entry(hass, config_entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in LUTRON_CASETA_COMPONENTS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -326,13 +342,14 @@ def device_info(self):
return {
"identifiers": {(DOMAIN, self.serial)},
"name": self.name,
+ "suggested_area": self._device["name"].split("_")[0],
"manufacturer": MANUFACTURER,
"model": f"{self._device['model']} ({self._device['type']})",
"via_device": (DOMAIN, self._bridge_device["serial"]),
}
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {"device_id": self.device_id, "zone_id": self._device["zone"]}
diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py
index 97053eba08c98a..c2fc311de439d3 100644
--- a/homeassistant/components/lutron_caseta/binary_sensor.py
+++ b/homeassistant/components/lutron_caseta/binary_sensor.py
@@ -16,7 +16,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
Adds occupancy groups from the Caseta bridge associated with the
config_entry as binary_sensor entities.
"""
-
entities = []
data = hass.data[CASETA_DOMAIN][config_entry.entry_id]
bridge = data[BRIDGE_LEAP]
@@ -70,6 +69,6 @@ def device_info(self):
return None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {"device_id": self.device_id}
diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py
index bb76c4b4ff77c8..f591369b57052d 100644
--- a/homeassistant/components/lutron_caseta/config_flow.py
+++ b/homeassistant/components/lutron_caseta/config_flow.py
@@ -2,26 +2,28 @@
import asyncio
import logging
import os
+import ssl
+import async_timeout
from pylutron_caseta.pairing import PAIR_CA, PAIR_CERT, PAIR_KEY, async_pair
from pylutron_caseta.smartbridge import Smartbridge
import voluptuous as vol
from homeassistant import config_entries
-from homeassistant.components.zeroconf import ATTR_HOSTNAME
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.core import callback
from .const import (
ABORT_REASON_ALREADY_CONFIGURED,
ABORT_REASON_CANNOT_CONNECT,
+ BRIDGE_TIMEOUT,
CONF_CA_CERTS,
CONF_CERTFILE,
CONF_KEYFILE,
+ DOMAIN,
ERROR_CANNOT_CONNECT,
STEP_IMPORT_FAILED,
)
-from .const import DOMAIN # pylint: disable=unused-import
HOSTNAME = "hostname"
@@ -50,6 +52,8 @@ def __init__(self):
"""Initialize a Lutron Caseta flow."""
self.data = {}
self.lutron_id = None
+ self.tls_assets_validated = False
+ self.attempted_tls_validation = False
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
@@ -61,7 +65,7 @@ async def async_step_user(self, user_input=None):
async def async_step_zeroconf(self, discovery_info):
"""Handle a flow initialized by zeroconf discovery."""
- hostname = discovery_info[ATTR_HOSTNAME]
+ hostname = discovery_info["hostname"]
if hostname is None or not hostname.startswith("lutron-"):
return self.async_abort(reason="not_lutron_device")
@@ -72,14 +76,15 @@ async def async_step_zeroconf(self, discovery_info):
self._abort_if_unique_id_configured({CONF_HOST: host})
self.data[CONF_HOST] = host
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {
CONF_NAME: self.bridge_id,
CONF_HOST: host,
}
return await self.async_step_link()
- async_step_homekit = async_step_zeroconf
+ async def async_step_homekit(self, discovery_info):
+ """Handle a flow initialized by homekit discovery."""
+ return await self.async_step_zeroconf(discovery_info)
async def async_step_link(self, user_input=None):
"""Handle pairing with the hub."""
@@ -90,11 +95,16 @@ async def async_step_link(self, user_input=None):
self._configure_tls_assets()
+ if (
+ not self.attempted_tls_validation
+ and await self.hass.async_add_executor_job(self._tls_assets_exist)
+ and await self.async_validate_connectable_bridge_config()
+ ):
+ self.tls_assets_validated = True
+ self.attempted_tls_validation = True
+
if user_input is not None:
- if (
- await self.hass.async_add_executor_job(self._tls_assets_exist)
- and await self.async_validate_connectable_bridge_config()
- ):
+ if self.tls_assets_validated:
# If we previous paired and the tls assets already exist,
# we do not need to go though pairing again.
return self.async_create_entry(title=self.bridge_id, data=self.data)
@@ -160,7 +170,6 @@ async def async_step_import(self, import_info):
This flow is triggered by `async_setup`.
"""
-
host = import_info[CONF_HOST]
# Store the imported config for other steps in this flow to access.
self.data[CONF_HOST] = host
@@ -189,8 +198,6 @@ async def async_step_import(self, import_info):
async def async_step_import_failed(self, user_input=None):
"""Make failed import surfaced to user."""
-
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {CONF_NAME: self.data[CONF_HOST]}
if user_input is None:
@@ -204,6 +211,7 @@ async def async_step_import_failed(self, user_input=None):
async def async_validate_connectable_bridge_config(self):
"""Check if we can connect to the bridge with the current config."""
+ bridge = None
try:
bridge = Smartbridge.create_tls(
@@ -212,16 +220,23 @@ async def async_validate_connectable_bridge_config(self):
certfile=self.hass.config.path(self.data[CONF_CERTFILE]),
ca_certs=self.hass.config.path(self.data[CONF_CA_CERTS]),
)
-
- await bridge.connect()
- if not bridge.is_connected():
- return False
-
- await bridge.close()
- return True
- except Exception: # pylint: disable=broad-except
- _LOGGER.exception(
- "Unknown exception while checking connectivity to bridge %s",
+ except ssl.SSLError:
+ _LOGGER.error(
+ "Invalid certificate used to connect to bridge at %s",
self.data[CONF_HOST],
)
return False
+
+ connected_ok = False
+ try:
+ async with async_timeout.timeout(BRIDGE_TIMEOUT):
+ await bridge.connect()
+ connected_ok = bridge.is_connected()
+ except asyncio.TimeoutError:
+ _LOGGER.error(
+ "Timeout while trying to connect to bridge at %s",
+ self.data[CONF_HOST],
+ )
+
+ await bridge.close()
+ return connected_ok
diff --git a/homeassistant/components/lutron_caseta/const.py b/homeassistant/components/lutron_caseta/const.py
index 4226be36f05262..40a5d2b01fd73c 100644
--- a/homeassistant/components/lutron_caseta/const.py
+++ b/homeassistant/components/lutron_caseta/const.py
@@ -31,5 +31,6 @@
ACTION_PRESS = "press"
ACTION_RELEASE = "release"
-CONF_TYPE = "type"
CONF_SUBTYPE = "subtype"
+
+BRIDGE_TIMEOUT = 35
diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py
index b3924ba31c8839..31f7e9b55bd1f9 100644
--- a/homeassistant/components/lutron_caseta/cover.py
+++ b/homeassistant/components/lutron_caseta/cover.py
@@ -24,7 +24,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
Adds shades from the Caseta bridge associated with the config_entry as
cover entities.
"""
-
entities = []
data = hass.data[CASETA_DOMAIN][config_entry.entry_id]
bridge = data[BRIDGE_LEAP]
diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py
index 402db7286afb29..230301c12f2ef8 100644
--- a/homeassistant/components/lutron_caseta/device_trigger.py
+++ b/homeassistant/components/lutron_caseta/device_trigger.py
@@ -1,6 +1,5 @@
"""Provides device triggers for lutron caseta."""
-import logging
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -32,9 +31,6 @@
LUTRON_CASETA_BUTTON_EVENT,
)
-_LOGGER = logging.getLogger(__name__)
-
-
SUPPORTED_INPUTS_EVENTS_TYPES = [ACTION_PRESS, ACTION_RELEASE]
LUTRON_BUTTON_TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
@@ -229,7 +225,7 @@ async def async_validate_trigger_config(hass: HomeAssistant, config: ConfigType)
return schema(config)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers for lutron caseta devices."""
triggers = []
@@ -265,17 +261,15 @@ async def async_attach_trigger(
schema = DEVICE_TYPE_SCHEMA_MAP.get(device["type"])
valid_buttons = DEVICE_TYPE_SUBTYPE_MAP.get(device["type"])
config = schema(config)
- event_config = event_trigger.TRIGGER_SCHEMA(
- {
- event_trigger.CONF_PLATFORM: CONF_EVENT,
- event_trigger.CONF_EVENT_TYPE: LUTRON_CASETA_BUTTON_EVENT,
- event_trigger.CONF_EVENT_DATA: {
- ATTR_SERIAL: device["serial"],
- ATTR_BUTTON_NUMBER: valid_buttons[config[CONF_SUBTYPE]],
- ATTR_ACTION: config[CONF_TYPE],
- },
- }
- )
+ event_config = {
+ event_trigger.CONF_PLATFORM: CONF_EVENT,
+ event_trigger.CONF_EVENT_TYPE: LUTRON_CASETA_BUTTON_EVENT,
+ event_trigger.CONF_EVENT_DATA: {
+ ATTR_SERIAL: device["serial"],
+ ATTR_BUTTON_NUMBER: valid_buttons[config[CONF_SUBTYPE]],
+ ATTR_ACTION: config[CONF_TYPE],
+ },
+ }
event_config = event_trigger.TRIGGER_SCHEMA(event_config)
return await event_trigger.async_attach_trigger(
hass, event_config, action, automation_info, platform_type="device"
diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py
index 935c8827c84c11..edca88c10fcc49 100644
--- a/homeassistant/components/lutron_caseta/fan.py
+++ b/homeassistant/components/lutron_caseta/fan.py
@@ -1,4 +1,6 @@
"""Support for Lutron Caseta fans."""
+from __future__ import annotations
+
import logging
from pylutron_caseta import FAN_HIGH, FAN_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_OFF
@@ -24,7 +26,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
Adds fan controllers from the Caseta bridge associated with the config_entry
as fan entities.
"""
-
entities = []
data = hass.data[CASETA_DOMAIN][config_entry.entry_id]
bridge = data[BRIDGE_LEAP]
@@ -42,12 +43,21 @@ class LutronCasetaFan(LutronCasetaDevice, FanEntity):
"""Representation of a Lutron Caseta fan. Including Fan Speed."""
@property
- def percentage(self) -> str:
+ def percentage(self) -> int | None:
"""Return the current speed percentage."""
+ if self._device["fan_speed"] is None:
+ return None
+ if self._device["fan_speed"] == FAN_OFF:
+ return 0
return ordered_list_item_to_percentage(
ORDERED_NAMED_FAN_SPEEDS, self._device["fan_speed"]
)
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return len(ORDERED_NAMED_FAN_SPEEDS)
+
@property
def supported_features(self) -> int:
"""Flag supported features. Speed Only."""
diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py
index ec20011808281c..016dd925b23720 100644
--- a/homeassistant/components/lutron_caseta/light.py
+++ b/homeassistant/components/lutron_caseta/light.py
@@ -33,7 +33,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
Adds dimmers from the Caseta bridge associated with the config_entry as
light entities.
"""
-
entities = []
data = hass.data[CASETA_DOMAIN][config_entry.entry_id]
bridge = data[BRIDGE_LEAP]
diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json
index 34ab75dc0cd764..88c6eddd0bf294 100644
--- a/homeassistant/components/lutron_caseta/manifest.json
+++ b/homeassistant/components/lutron_caseta/manifest.json
@@ -3,7 +3,7 @@
"name": "Lutron Caséta",
"documentation": "https://www.home-assistant.io/integrations/lutron_caseta",
"requirements": [
- "pylutron-caseta==0.9.0", "aiolip==1.0.1"
+ "pylutron-caseta==0.9.0", "aiolip==1.1.4"
],
"config_flow": true,
"zeroconf": ["_leap._tcp.local."],
diff --git a/homeassistant/components/lutron_caseta/scene.py b/homeassistant/components/lutron_caseta/scene.py
index d70048db8cda11..43e0429d151ac7 100644
--- a/homeassistant/components/lutron_caseta/scene.py
+++ b/homeassistant/components/lutron_caseta/scene.py
@@ -12,7 +12,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
Adds scenes from the Caseta bridge associated with the config_entry as
scene entities.
"""
-
entities = []
data = hass.data[CASETA_DOMAIN][config_entry.entry_id]
bridge = data[BRIDGE_LEAP]
diff --git a/homeassistant/components/lutron_caseta/strings.json b/homeassistant/components/lutron_caseta/strings.json
index bdaec22e776475..9464523fcce3c6 100644
--- a/homeassistant/components/lutron_caseta/strings.json
+++ b/homeassistant/components/lutron_caseta/strings.json
@@ -7,8 +7,8 @@
"description": "Couldn’t setup bridge (host: {host}) imported from configuration.yaml."
},
"user": {
- "title": "Automaticlly connect to the bridge",
- "description": "Enter the ip address of the device.",
+ "title": "Automatically connect to the bridge",
+ "description": "Enter the IP address of the device.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py
index 1e5b4ab6fe5bc8..c6aea447055d5d 100644
--- a/homeassistant/components/lutron_caseta/switch.py
+++ b/homeassistant/components/lutron_caseta/switch.py
@@ -15,7 +15,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
Adds switches from the Caseta bridge associated with the config_entry as
switch entities.
"""
-
entities = []
data = hass.data[CASETA_DOMAIN][config_entry.entry_id]
bridge = data[BRIDGE_LEAP]
diff --git a/homeassistant/components/lutron_caseta/translations/bg.json b/homeassistant/components/lutron_caseta/translations/bg.json
new file mode 100644
index 00000000000000..ba9f144cb0aa50
--- /dev/null
+++ b/homeassistant/components/lutron_caseta/translations/bg.json
@@ -0,0 +1,12 @@
+{
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d",
+ "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d",
+ "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d",
+ "button_4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d",
+ "off": "\u0418\u0437\u043a\u043b.",
+ "on": "\u0412\u043a\u043b."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/ca.json b/homeassistant/components/lutron_caseta/translations/ca.json
index c3b0e686cc4616..5f2cc5d40872ea 100644
--- a/homeassistant/components/lutron_caseta/translations/ca.json
+++ b/homeassistant/components/lutron_caseta/translations/ca.json
@@ -2,16 +2,75 @@
"config": {
"abort": {
"already_configured": "El dispositiu ja est\u00e0 configurat",
- "cannot_connect": "Ha fallat la connexi\u00f3"
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "not_lutron_device": "El dispositiu descobert no \u00e9s un dispositiu Lutron"
},
"error": {
"cannot_connect": "Ha fallat la connexi\u00f3"
},
+ "flow_title": "Lutron Cas\u00e9ta {name} ({host})",
"step": {
"import_failed": {
"description": "No s'ha pogut configurar l'enlla\u00e7 (amfitri\u00f3: {host}) importat de configuration.yaml.",
"title": "No s'ha pogut importar la configuraci\u00f3 de l'enlla\u00e7 de Cas\u00e9ta."
+ },
+ "link": {
+ "description": "Per a vincular amb {name} ({host}), despr\u00e9s d'enviar aquest formulari, prem el bot\u00f3 negre de la part posterior de l'enlla\u00e7.",
+ "title": "Vinculaci\u00f3 amb enlla\u00e7"
+ },
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3"
+ },
+ "description": "Introdueix l'adre\u00e7a IP del dispositiu.",
+ "title": "Connexi\u00f3 autom\u00e0tica amb l'enlla\u00e7"
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "Primer bot\u00f3",
+ "button_2": "Segon bot\u00f3",
+ "button_3": "Tercer bot\u00f3",
+ "button_4": "Quart bot\u00f3",
+ "close_1": "Tanca 1",
+ "close_2": "Tanca 2",
+ "close_3": "Tanca 3",
+ "close_4": "Tanca 4",
+ "close_all": "Tanca-ho tot",
+ "group_1_button_1": "Primer bot\u00f3 del primer grup",
+ "group_1_button_2": "Segon bot\u00f3 del primer grup",
+ "group_2_button_1": "Primer bot\u00f3 del segon grup",
+ "group_2_button_2": "Segon bot\u00f3 del segon grup",
+ "lower": "Baixa",
+ "lower_1": "Baixa 1",
+ "lower_2": "Baixa 2",
+ "lower_3": "Baixa 3",
+ "lower_4": "Baixa 4",
+ "lower_all": "Baixa-ho tot",
+ "off": "OFF",
+ "on": "ON",
+ "open_1": "Obre 1",
+ "open_2": "Obre 2",
+ "open_3": "Obre 3",
+ "open_4": "Obre 4",
+ "open_all": "Obre-ho tot",
+ "raise": "Puja",
+ "raise_1": "Puja 1",
+ "raise_2": "Puja 2",
+ "raise_3": "Puja 3",
+ "raise_4": "Puja 4",
+ "raise_all": "Puja-ho tot",
+ "stop": "Atura (preferit)",
+ "stop_1": "Atura 1",
+ "stop_2": "Atura 2",
+ "stop_3": "Atura 3",
+ "stop_4": "Atura 4",
+ "stop_all": "Atura-ho tot"
+ },
+ "trigger_type": {
+ "press": "\"{subtype}\" premut",
+ "release": "\"{subtype}\" alliberat"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/cs.json b/homeassistant/components/lutron_caseta/translations/cs.json
index 60fa7fddced2c5..4ccfa17e6d3a03 100644
--- a/homeassistant/components/lutron_caseta/translations/cs.json
+++ b/homeassistant/components/lutron_caseta/translations/cs.json
@@ -6,6 +6,13 @@
},
"error": {
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hostitel"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/de.json b/homeassistant/components/lutron_caseta/translations/de.json
index 13f8c6bd800db1..75cdac794828ae 100644
--- a/homeassistant/components/lutron_caseta/translations/de.json
+++ b/homeassistant/components/lutron_caseta/translations/de.json
@@ -2,10 +2,39 @@
"config": {
"abort": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
- "cannot_connect": "Verbindung fehlgeschlagen"
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "not_lutron_device": "Erkanntes Ger\u00e4t ist kein Lutron-Ger\u00e4t"
},
"error": {
"cannot_connect": "Verbindung fehlgeschlagen"
+ },
+ "flow_title": "Lutron Cas\u00e9ta {name} ({host})",
+ "step": {
+ "import_failed": {
+ "description": "Konnte die aus configuration.yaml importierte Bridge (Host: {host}) nicht einrichten.",
+ "title": "Import der Cas\u00e9ta-Bridge-Konfiguration fehlgeschlagen."
+ },
+ "link": {
+ "title": "Mit der Bridge verbinden"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Gib die IP-Adresse des Ger\u00e4ts ein.",
+ "title": "Automatisch mit der Bridge verbinden"
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "Erste Taste",
+ "button_2": "Zweite Taste",
+ "button_3": "Dritte Taste",
+ "button_4": "Vierte Taste",
+ "off": "Aus",
+ "on": "An",
+ "stop_all": "Alle anhalten"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/en.json b/homeassistant/components/lutron_caseta/translations/en.json
index 8ea0672a3f3c39..2088cd5232766f 100644
--- a/homeassistant/components/lutron_caseta/translations/en.json
+++ b/homeassistant/components/lutron_caseta/translations/en.json
@@ -22,8 +22,8 @@
"data": {
"host": "Host"
},
- "description": "Enter the ip address of the device.",
- "title": "Automaticlly connect to the bridge"
+ "description": "Enter the IP address of the device.",
+ "title": "Automatically connect to the bridge"
}
}
},
diff --git a/homeassistant/components/lutron_caseta/translations/es.json b/homeassistant/components/lutron_caseta/translations/es.json
index cfd8551bab92a9..9dbedba145722c 100644
--- a/homeassistant/components/lutron_caseta/translations/es.json
+++ b/homeassistant/components/lutron_caseta/translations/es.json
@@ -2,16 +2,75 @@
"config": {
"abort": {
"already_configured": "El dispositivo ya est\u00e1 configurado",
- "cannot_connect": "No se pudo conectar"
+ "cannot_connect": "No se pudo conectar",
+ "not_lutron_device": "El dispositivo descubierto no es un dispositivo de Lutron"
},
"error": {
"cannot_connect": "No se pudo conectar"
},
+ "flow_title": "Lutron Cas\u00e9ta {name} ({host})",
"step": {
"import_failed": {
"description": "No se puede configurar bridge (host: {host}) importado desde configuration.yaml.",
"title": "Error al importar la configuraci\u00f3n del bridge Cas\u00e9ta."
+ },
+ "link": {
+ "description": "Para emparejar con {name} ({host}), despu\u00e9s de enviar este formulario, presione el bot\u00f3n negro en la parte posterior del puente.",
+ "title": "Emparejar con el puente"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Introduzca la direcci\u00f3n ip del dispositivo.",
+ "title": "Conectar autom\u00e1ticamente con el dispositivo"
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "Primer bot\u00f3n",
+ "button_2": "Segundo bot\u00f3n",
+ "button_3": "Tercer bot\u00f3n",
+ "button_4": "Cuarto bot\u00f3n",
+ "close_1": "Cerrar 1",
+ "close_2": "Cerrar 2",
+ "close_3": "Cerrar 3",
+ "close_4": "Cerrar 4",
+ "close_all": "Cerrar todo",
+ "group_1_button_1": "Primer bot\u00f3n de primer grupo",
+ "group_1_button_2": "Segundo bot\u00f3n del primer grupo",
+ "group_2_button_1": "Primer bot\u00f3n del segundo grupo",
+ "group_2_button_2": "Segundo bot\u00f3n del segundo grupo",
+ "lower": "Inferior",
+ "lower_1": "Inferior 1",
+ "lower_2": "Inferior 2",
+ "lower_3": "Inferior 3",
+ "lower_4": "Inferior 4",
+ "lower_all": "Bajar todo",
+ "off": "Apagado",
+ "on": "Encendido",
+ "open_1": "Abrir 1",
+ "open_2": "Abrir 2",
+ "open_3": "Abrir 3",
+ "open_4": "Abrir 4",
+ "open_all": "Abrir todo",
+ "raise": "Levantar",
+ "raise_1": "Levantar 1",
+ "raise_2": "Levantar 2",
+ "raise_3": "Levantar 3",
+ "raise_4": "Levantar 4",
+ "raise_all": "Levantar todo",
+ "stop": "Detener (favorito)",
+ "stop_1": "Detener 1",
+ "stop_2": "Detener 2",
+ "stop_3": "Detener 3",
+ "stop_4": "Detener 4",
+ "stop_all": "Detener todo"
+ },
+ "trigger_type": {
+ "press": "\"{subtipo}\" presionado",
+ "release": "\"{subtipo}\" liberado"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/et.json b/homeassistant/components/lutron_caseta/translations/et.json
index ed352c7bcc4b58..5e57dd63b8b471 100644
--- a/homeassistant/components/lutron_caseta/translations/et.json
+++ b/homeassistant/components/lutron_caseta/translations/et.json
@@ -2,16 +2,75 @@
"config": {
"abort": {
"already_configured": "Seade on juba h\u00e4\u00e4lestatud",
- "cannot_connect": "\u00dchendamine nurjus"
+ "cannot_connect": "\u00dchendamine nurjus",
+ "not_lutron_device": "Avastatud seade ei ole Lutroni seade"
},
"error": {
"cannot_connect": "\u00dchendamine nurjus"
},
+ "flow_title": "Lutron Cas\u00e9ta {name} ( {host} )",
"step": {
"import_failed": {
"description": "Silla (host: {host} ) seadistamine configuration.yaml kirje teabest nurjus.",
"title": "Cas\u00e9ta Bridge seadete importimine nurjus."
+ },
+ "link": {
+ "description": "{name} ({host}) sidumiseks vajuta p\u00e4rast selle vormi esitamist silla tagak\u00fcljel olevat musta nuppu.",
+ "title": "Sillaga sidumine"
+ },
+ "user": {
+ "data": {
+ "host": ""
+ },
+ "description": "Sisesta seadme IP-aadress.",
+ "title": "\u00dchenda sillaga automaatselt"
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "Esimene nupp",
+ "button_2": "Teine nupp",
+ "button_3": "Kolmas nupp",
+ "button_4": "Neljas nupp",
+ "close_1": "Sule #1",
+ "close_2": "Sule #2",
+ "close_3": "Sule #3",
+ "close_4": "Sule #4",
+ "close_all": "Sulge k\u00f5ik",
+ "group_1_button_1": "Esimese r\u00fchma esimene nupp",
+ "group_1_button_2": "Esimene r\u00fchma teine nupp",
+ "group_2_button_1": "Teise r\u00fchma esimene nupp",
+ "group_2_button_2": "Teise r\u00fchma teine nupp",
+ "lower": "Langeta",
+ "lower_1": "Langeta #1",
+ "lower_2": "Langeta #2",
+ "lower_3": "Langeta #3",
+ "lower_4": "Langeta #4",
+ "lower_all": "Langeta k\u00f5ik",
+ "off": "V\u00e4ljas",
+ "on": "Sees",
+ "open_1": "Ava #1",
+ "open_2": "Ava #2",
+ "open_3": "Ava #3",
+ "open_4": "Ava #4",
+ "open_all": "Ava k\u00f5ik",
+ "raise": "T\u00f5sta",
+ "raise_1": "T\u00f5sta #1",
+ "raise_2": "T\u00f5sta #2",
+ "raise_3": "T\u00f5sta #3",
+ "raise_4": "T\u00f5sta #4",
+ "raise_all": "T\u00f5sta k\u00f5ik",
+ "stop": "Peata lemmikasendis",
+ "stop_1": "Peata #1",
+ "stop_2": "Peata #2",
+ "stop_3": "Peata #3",
+ "stop_4": "Peata #4",
+ "stop_all": "Peata k\u00f5ik"
+ },
+ "trigger_type": {
+ "press": "vajutati \" {subtype} \"",
+ "release": "\" {subtype} \" vabastati"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/fr.json b/homeassistant/components/lutron_caseta/translations/fr.json
index 0674172e97581f..ff561548b44deb 100644
--- a/homeassistant/components/lutron_caseta/translations/fr.json
+++ b/homeassistant/components/lutron_caseta/translations/fr.json
@@ -2,16 +2,75 @@
"config": {
"abort": {
"already_configured": "Pont Cas\u00e9ta d\u00e9j\u00e0 configur\u00e9.",
- "cannot_connect": "Installation annul\u00e9e du pont Cas\u00e9ta en raison d'un \u00e9chec de connexion."
+ "cannot_connect": "Installation annul\u00e9e du pont Cas\u00e9ta en raison d'un \u00e9chec de connexion.",
+ "not_lutron_device": "L'appareil d\u00e9couvert n'est pas un appareil Lutron"
},
"error": {
"cannot_connect": "\u00c9chec de la connexion \u00e0 la passerelle Cas\u00e9ta; v\u00e9rifiez la configuration de votre h\u00f4te et de votre certificat."
},
+ "flow_title": "Lutron Cas\u00e9ta {name} ( {host} )",
"step": {
"import_failed": {
"description": "Impossible de configurer la passerelle (h\u00f4te: {host} ) import\u00e9 \u00e0 partir de configuration.yaml.",
"title": "\u00c9chec de l'importation de la configuration de la passerelle Cas\u00e9ta."
+ },
+ "link": {
+ "description": "Pour jumeler avec {name} ( {host} ), apr\u00e8s avoir soumis ce formulaire, appuyez sur le bouton noir \u00e0 l'arri\u00e8re du pont.",
+ "title": "Paire avec le pont"
+ },
+ "user": {
+ "data": {
+ "host": "Hote"
+ },
+ "description": "Saisissez l'adresse IP de l'appareil.",
+ "title": "Se connecter automatiquement au pont"
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "Premier bouton",
+ "button_2": "Deuxi\u00e8me bouton",
+ "button_3": "Troisi\u00e8me bouton",
+ "button_4": "Quatri\u00e8me bouton",
+ "close_1": "Fermer 1",
+ "close_2": "Fermer 2",
+ "close_3": "Fermer 3",
+ "close_4": "Fermer 4",
+ "close_all": "Ferme tout",
+ "group_1_button_1": "Premier bouton du premier groupe",
+ "group_1_button_2": "Premier groupe deuxi\u00e8me bouton",
+ "group_2_button_1": "Premier bouton du deuxi\u00e8me groupe",
+ "group_2_button_2": "Deuxi\u00e8me bouton du deuxi\u00e8me groupe",
+ "lower": "Bas",
+ "lower_1": "Bas 1",
+ "lower_2": "Bas 2",
+ "lower_3": "Bas 3",
+ "lower_4": "Bas 4",
+ "lower_all": "Tout baisser",
+ "off": "Eteint",
+ "on": "Allumer",
+ "open_1": "Ouvrir 1",
+ "open_2": "Ouvrir 2",
+ "open_3": "Ouvrir 3",
+ "open_4": "Ouvrir 4",
+ "open_all": "Ouvre tout",
+ "raise": "Haut",
+ "raise_1": "Haut 1",
+ "raise_2": "Haut 2",
+ "raise_3": "Haut 3",
+ "raise_4": "Haut 4",
+ "raise_all": "Lever tout",
+ "stop": "Stop (favori)",
+ "stop_1": "Arr\u00eat 1",
+ "stop_2": "Arr\u00eat 2",
+ "stop_3": "Arr\u00eat 3",
+ "stop_4": "Arr\u00eat 4",
+ "stop_all": "Arr\u00eate tout"
+ },
+ "trigger_type": {
+ "press": "\" {subtype} \" appuy\u00e9",
+ "release": "\" {subtype} \" publi\u00e9"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/he.json b/homeassistant/components/lutron_caseta/translations/he.json
new file mode 100644
index 00000000000000..7b55b0743fba7b
--- /dev/null
+++ b/homeassistant/components/lutron_caseta/translations/he.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4- IP \u05e9\u05dc \u05d4\u05de\u05db\u05e9\u05d9\u05e8."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/hu.json b/homeassistant/components/lutron_caseta/translations/hu.json
new file mode 100644
index 00000000000000..921f5e83409094
--- /dev/null
+++ b/homeassistant/components/lutron_caseta/translations/hu.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt"
+ },
+ "description": "Add meg az eszk\u00f6z IP-c\u00edm\u00e9t."
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "Els\u0151 gomb",
+ "button_2": "M\u00e1sodik gomb",
+ "button_3": "Harmadik gomb",
+ "button_4": "Negyedik gomb",
+ "off": "Ki",
+ "on": "Be",
+ "stop_all": "Az \u00f6sszes le\u00e1ll\u00edt\u00e1sa"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/id.json b/homeassistant/components/lutron_caseta/translations/id.json
new file mode 100644
index 00000000000000..b14e9ad1c23a10
--- /dev/null
+++ b/homeassistant/components/lutron_caseta/translations/id.json
@@ -0,0 +1,70 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung",
+ "not_lutron_device": "Perangkat yang ditemukan bukan perangkat Lutron"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "flow_title": "Lutron Cas\u00e9ta {name} ({host})",
+ "step": {
+ "import_failed": {
+ "description": "Tidak dapat menyiapkan bridge (host: {host} ) yang diimpor dari configuration.yaml.",
+ "title": "Gagal mengimpor konfigurasi bridge Cas\u00e9ta."
+ },
+ "link": {
+ "description": "Untuk memasangkan dengan {name} ({host}), setelah mengirimkan formulir ini, tekan tombol hitam di bagian belakang bridge.",
+ "title": "Pasangkan dengan bridge"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Masukkan alamat IP perangkat.",
+ "title": "Sambungkan secara otomatis ke bridge"
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "Tombol pertama",
+ "button_2": "Tombol kedua",
+ "button_3": "Tombol ketiga",
+ "button_4": "Tombol keempat",
+ "close_1": "Tutup 1",
+ "close_2": "Tutup 2",
+ "close_3": "Tutup 3",
+ "close_4": "Tutup 4",
+ "close_all": "Tutup semua",
+ "group_1_button_1": "Tombol pertama Grup Pertama",
+ "group_1_button_2": "Tombol kedua Grup Pertama",
+ "group_2_button_1": "Tombol pertama Grup Kedua",
+ "group_2_button_2": "Tombol kedua Grup Kedua",
+ "lower": "Rendah",
+ "lower_1": "Rendah 1",
+ "lower_2": "Rendah 2",
+ "lower_3": "Rendah 3",
+ "lower_4": "Rendah 4",
+ "lower_all": "Rendahkan semua",
+ "off": "Mati",
+ "on": "Nyala",
+ "open_1": "Buka 1",
+ "open_2": "Buka 2",
+ "open_3": "Buka 3",
+ "open_4": "Buka 4",
+ "open_all": "Buka semua",
+ "stop": "Hentikan (favorit)",
+ "stop_1": "Hentikan 1",
+ "stop_2": "Hentikan 2",
+ "stop_3": "Hentikan 3",
+ "stop_4": "Hentikan 4",
+ "stop_all": "Hentikan semuanya"
+ },
+ "trigger_type": {
+ "press": "\"{subtype}\" ditekan",
+ "release": "\"{subtype}\" dilepas"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/it.json b/homeassistant/components/lutron_caseta/translations/it.json
index 5bdcf87607dccc..d1b3b754812aa3 100644
--- a/homeassistant/components/lutron_caseta/translations/it.json
+++ b/homeassistant/components/lutron_caseta/translations/it.json
@@ -2,16 +2,75 @@
"config": {
"abort": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
- "cannot_connect": "Impossibile connettersi"
+ "cannot_connect": "Impossibile connettersi",
+ "not_lutron_device": "Il dispositivo rilevato non \u00e8 un dispositivo Lutron"
},
"error": {
"cannot_connect": "Impossibile connettersi"
},
+ "flow_title": "Lutron Cas\u00e9ta {name} ({host})",
"step": {
"import_failed": {
"description": "Impossibile impostare il bridge (host: {host}) importato da configuration.yaml.",
"title": "Impossibile importare la configurazione del bridge Cas\u00e9ta."
+ },
+ "link": {
+ "description": "Per eseguire l'associazione con {name} ({host}), dopo aver inviato questo modulo, premere il pulsante nero sul retro del bridge.",
+ "title": "Associa con il bridge"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Immettere l'indirizzo IP del dispositivo.",
+ "title": "Connetti automaticamente al bridge"
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "Primo pulsante",
+ "button_2": "Secondo pulsante",
+ "button_3": "Terzo pulsante",
+ "button_4": "Quarto pulsante",
+ "close_1": "Chiudi 1",
+ "close_2": "Chiudi 2",
+ "close_3": "Chiudi 3",
+ "close_4": "Chiudi 4",
+ "close_all": "Chiudi tutti",
+ "group_1_button_1": "Primo Gruppo primo pulsante",
+ "group_1_button_2": "Primo Gruppo secondo pulsante",
+ "group_2_button_1": "Secondo Gruppo primo pulsante",
+ "group_2_button_2": "Secondo Gruppo secondo pulsante",
+ "lower": "Abbassa",
+ "lower_1": "Abbassa 1",
+ "lower_2": "Abbassa 2",
+ "lower_3": "Abbassa 3",
+ "lower_4": "Abbassa 4",
+ "lower_all": "Abbassa tutti",
+ "off": "Spento",
+ "on": "Acceso",
+ "open_1": "Apri 1",
+ "open_2": "Apri 2",
+ "open_3": "Apri 3",
+ "open_4": "Apri 4",
+ "open_all": "Apri tutti",
+ "raise": "Alza",
+ "raise_1": "Alza 1",
+ "raise_2": "Alza 2",
+ "raise_3": "Alza 3",
+ "raise_4": "Alza 4",
+ "raise_all": "Alza tutti",
+ "stop": "Ferma (preferito)",
+ "stop_1": "Ferma 1",
+ "stop_2": "Ferma 2",
+ "stop_3": "Ferma 3",
+ "stop_4": "Ferma 4",
+ "stop_all": "Fermare tutti"
+ },
+ "trigger_type": {
+ "press": "\"{subtype}\" premuto",
+ "release": "\"{subtype}\" rilasciato"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/ko.json b/homeassistant/components/lutron_caseta/translations/ko.json
index 8c5caec998e6e9..862bfab8cfd734 100644
--- a/homeassistant/components/lutron_caseta/translations/ko.json
+++ b/homeassistant/components/lutron_caseta/translations/ko.json
@@ -1,17 +1,76 @@
{
"config": {
"abort": {
- "already_configured": "Cas\u00e9ta \ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "cannot_connect": "Cas\u00e9ta \ube0c\ub9ac\uc9c0 \uc5f0\uacb0 \uc2e4\ud328\ub85c \uc124\uc815\uc774 \ucde8\uc18c\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "not_lutron_device": "\ubc1c\uacac\ub41c \uae30\uae30\ub294 Lutron \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "Cas\u00e9ta \ube0c\ub9ac\uc9c0\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8 \ubc0f \uc778\uc99d\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694."
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
+ "flow_title": "Lutron Cas\u00e9ta: {name} ({host})",
"step": {
"import_failed": {
"description": "configuration.yaml \uc5d0\uc11c \uac00\uc838\uc628 \ube0c\ub9ac\uc9c0 (\ud638\uc2a4\ud2b8:{host}) \ub97c \uc124\uc815\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
"title": "Cas\u00e9ta \ube0c\ub9ac\uc9c0 \uad6c\uc131\uc744 \uac00\uc838\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4."
+ },
+ "link": {
+ "description": "{name} ({host})\uacfc(\uc640) \ud398\uc5b4\ub9c1\ud558\ub824\uba74 \uc774 \uc591\uc2dd\uc744 \uc81c\ucd9c\ud55c \ud6c4 \ube0c\ub9ac\uc9c0 \ub4a4\ucabd\uc5d0 \uc788\ub294 \uac80\uc740\uc0c9 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.",
+ "title": "\ube0c\ub9ac\uc9c0\uc640 \ud398\uc5b4\ub9c1\ud558\uae30"
+ },
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8"
+ },
+ "description": "\uae30\uae30\uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "\ube0c\ub9ac\uc9c0\uc5d0 \uc790\ub3d9\uc73c\ub85c \uc5f0\uacb0\ud558\uae30"
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "\uccab \ubc88\uc9f8 \ubc84\ud2bc",
+ "button_2": "\ub450 \ubc88\uc9f8 \ubc84\ud2bc",
+ "button_3": "\uc138 \ubc88\uc9f8 \ubc84\ud2bc",
+ "button_4": "\ub124 \ubc88\uc9f8 \ubc84\ud2bc",
+ "close_1": "1 \ub2eb\uae30",
+ "close_2": "2 \ub2eb\uae30",
+ "close_3": "3 \ub2eb\uae30",
+ "close_4": "4 \ub2eb\uae30",
+ "close_all": "\ubaa8\ub450 \ub2eb\uae30",
+ "group_1_button_1": "\uccab \ubc88\uc9f8 \uadf8\ub8f9 \uccab \ubc88\uc9f8 \ubc84\ud2bc",
+ "group_1_button_2": "\uccab \ubc88\uc9f8 \uadf8\ub8f9 \ub450 \ubc88\uc9f8 \ubc84\ud2bc",
+ "group_2_button_1": "\ub450 \ubc88\uc9f8 \uadf8\ub8f9 \uccab \ubc88\uc9f8 \ubc84\ud2bc",
+ "group_2_button_2": "\ub450 \ubc88\uc9f8 \uadf8\ub8f9 \ub450 \ubc88\uc9f8 \ubc84\ud2bc",
+ "lower": "\ub0ae\ucd94\uae30",
+ "lower_1": "1 \ub0ae\ucd94\uae30",
+ "lower_2": "2 \ub0ae\ucd94\uae30",
+ "lower_3": "3 \ub0ae\ucd94\uae30",
+ "lower_4": "4 \ub0ae\ucd94\uae30",
+ "lower_all": "\ubaa8\ub450 \ub0ae\ucd94\uae30",
+ "off": "\ub044\uae30",
+ "on": "\ucf1c\uae30",
+ "open_1": "1 \uc5f4\uae30",
+ "open_2": "2 \uc5f4\uae30",
+ "open_3": "3 \uc5f4\uae30",
+ "open_4": "4 \uc5f4\uae30",
+ "open_all": "\ubaa8\ub450 \uc5f4\uae30",
+ "raise": "\ub192\uc774\uae30",
+ "raise_1": "1 \uc62c\ub9ac\uae30",
+ "raise_2": "2 \uc62c\ub9ac\uae30",
+ "raise_3": "3 \uc62c\ub9ac\uae30",
+ "raise_4": "4 \uc62c\ub9ac\uae30",
+ "raise_all": "\ubaa8\ub450 \uc62c\ub9ac\uae30",
+ "stop": "\uc911\uc9c0 (\uc990\uaca8 \ucc3e\uae30)",
+ "stop_1": "1 \uc911\uc9c0",
+ "stop_2": "2 \uc911\uc9c0",
+ "stop_3": "3 \uc911\uc9c0",
+ "stop_4": "4 \uc911\uc9c0",
+ "stop_all": "\ubaa8\ub450 \uc911\uc9c0"
+ },
+ "trigger_type": {
+ "press": "\"{subtype}\"\uc774(\uac00) \ub20c\ub838\uc744 \ub54c",
+ "release": "\"{subtype}\"\uc5d0\uc11c \uc190\uc744 \ub5bc\uc5c8\uc744 \ub54c"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/lb.json b/homeassistant/components/lutron_caseta/translations/lb.json
index e78f88397976f4..6a390bc5a1f6d1 100644
--- a/homeassistant/components/lutron_caseta/translations/lb.json
+++ b/homeassistant/components/lutron_caseta/translations/lb.json
@@ -13,5 +13,37 @@
"title": "Feeler beim import vun der Cas\u00e9ta Bridge Konfiguratioun"
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "lower": "Erofsetzen",
+ "lower_1": "1 erofsetzen",
+ "lower_2": "2 erofsetzen",
+ "lower_3": "3 erofsetzen",
+ "lower_4": "4 erofsetzen",
+ "lower_all": "All erofsetzen",
+ "off": "Aus",
+ "on": "Un",
+ "open_1": "1 opmaachen",
+ "open_2": "2 opmaachen",
+ "open_3": "3 opmaachen",
+ "open_4": "4 opmaachen",
+ "open_all": "All opmaachen",
+ "raise": "Unhiewen",
+ "raise_1": "1 unhiewen",
+ "raise_2": "2 unhiewen",
+ "raise_3": "3 unhiewen",
+ "raise_4": "4 unhiewen",
+ "raise_all": "All unhiewen",
+ "stop_1": "Stop 1",
+ "stop_2": "Stop 2",
+ "stop_3": "Stop 3",
+ "stop_4": "Stop 4",
+ "stop_all": "All stoppen"
+ },
+ "trigger_type": {
+ "press": "\"{subtype}\" gedr\u00e9ckt",
+ "release": "\"{subtype}\" lassgelooss"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/nl.json b/homeassistant/components/lutron_caseta/translations/nl.json
index 8e48dea075d5d4..b281d3cd22cfff 100644
--- a/homeassistant/components/lutron_caseta/translations/nl.json
+++ b/homeassistant/components/lutron_caseta/translations/nl.json
@@ -2,15 +2,75 @@
"config": {
"abort": {
"already_configured": "Apparaat is al geconfigureerd",
- "cannot_connect": "Kon niet verbinden"
+ "cannot_connect": "Kon niet verbinden",
+ "not_lutron_device": "Ontdekt apparaat is geen Lutron-apparaat"
},
"error": {
"cannot_connect": "Kon niet verbinden"
},
+ "flow_title": "Lutron Cas\u00e9ta {name} ({host})",
"step": {
"import_failed": {
- "description": "Kan bridge (host: {host} ) niet instellen, ge\u00efmporteerd uit configuration.yaml."
+ "description": "Kan bridge (host: {host} ) niet instellen, ge\u00efmporteerd uit configuration.yaml.",
+ "title": "Het importeren van de Cas\u00e9ta bridge configuratie is mislukt."
+ },
+ "link": {
+ "description": "Om te koppelen met {name} ({host}), na het verzenden van dit formulier, druk op de zwarte knop op de achterkant van de brug.",
+ "title": "Koppel met de bridge"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Voer het IP-adres van het apparaat in.",
+ "title": "Automatisch verbinding maken met de bridge"
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "Eerste knop",
+ "button_2": "Tweede knop",
+ "button_3": "Derde knop",
+ "button_4": "Vierde knop",
+ "close_1": "Sluit 1",
+ "close_2": "Sluit 2",
+ "close_3": "Sluit 3",
+ "close_4": "Sluit 4",
+ "close_all": "Sluit alles",
+ "group_1_button_1": "Eerste Groep eerste knop",
+ "group_1_button_2": "Eerste Groep tweede knop",
+ "group_2_button_1": "Tweede Groep eerste knop",
+ "group_2_button_2": "Tweede Groep tweede knop",
+ "lower": "Verlagen",
+ "lower_1": "Verlagen 1",
+ "lower_2": "Verlagen 2",
+ "lower_3": "Verlagen 3",
+ "lower_4": "Verlaag 4",
+ "lower_all": "Verlaag alles",
+ "off": "Uit",
+ "on": "Aan",
+ "open_1": "Open 1",
+ "open_2": "Open 2",
+ "open_3": "Open 3",
+ "open_4": "Open 4",
+ "open_all": "Open alle",
+ "raise": "Verhogen",
+ "raise_1": "Verhogen 1",
+ "raise_2": "Verhogen 2",
+ "raise_3": "Verhogen 3",
+ "raise_4": "Verhogen 4",
+ "raise_all": "Verhoog alles",
+ "stop": "Stop (favoriet)",
+ "stop_1": "Stop 1",
+ "stop_2": "Stop 2",
+ "stop_3": "Stop 3",
+ "stop_4": "Stop 4",
+ "stop_all": "Stop alles"
+ },
+ "trigger_type": {
+ "press": "\" {subtype} \" ingedrukt",
+ "release": "\"{subtype}\" losgelaten"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/no.json b/homeassistant/components/lutron_caseta/translations/no.json
index 7afac9c51a5303..b985c87caf03ca 100644
--- a/homeassistant/components/lutron_caseta/translations/no.json
+++ b/homeassistant/components/lutron_caseta/translations/no.json
@@ -2,16 +2,75 @@
"config": {
"abort": {
"already_configured": "Enheten er allerede konfigurert",
- "cannot_connect": "Tilkobling mislyktes"
+ "cannot_connect": "Tilkobling mislyktes",
+ "not_lutron_device": "Oppdaget enhet er ikke en Lutron-enhet"
},
"error": {
"cannot_connect": "Tilkobling mislyktes"
},
+ "flow_title": "Lutron Cas\u00e9ta {name} ({host})",
"step": {
"import_failed": {
"description": "Kunne ikke konfigurere bridge (host: {host} ) importert fra configuration.yaml.",
"title": "Kan ikke importere Cas\u00e9ta bridge-konfigurasjon."
+ },
+ "link": {
+ "description": "Hvis du vil pare med {name} ({host}), trykker du den svarte knappen p\u00e5 baksiden av broen etter at du har sendt dette skjemaet.",
+ "title": "Par med broen"
+ },
+ "user": {
+ "data": {
+ "host": "Vert"
+ },
+ "description": "Skriv inn IP-adressen til enheten.",
+ "title": "Koble automatisk til broen"
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "F\u00f8rste knapp",
+ "button_2": "Andre knapp",
+ "button_3": "Tredje knapp",
+ "button_4": "Fjerde knapp",
+ "close_1": "Lukk 1",
+ "close_2": "Lukk 2",
+ "close_3": "Lukk 3",
+ "close_4": "Lukk 4",
+ "close_all": "Lukk alle",
+ "group_1_button_1": "F\u00f8rste gruppe f\u00f8rste knapp",
+ "group_1_button_2": "F\u00f8rste gruppe andre knapp",
+ "group_2_button_1": "Andre gruppe f\u00f8rste knapp",
+ "group_2_button_2": "Andre gruppeknapp",
+ "lower": "Senk",
+ "lower_1": "Senk 1",
+ "lower_2": "Senk 2",
+ "lower_3": "Senk 3",
+ "lower_4": "Senk 4",
+ "lower_all": "Senk alle",
+ "off": "Av",
+ "on": "P\u00e5",
+ "open_1": "\u00c5pne 1",
+ "open_2": "\u00c5pne 2",
+ "open_3": "\u00c5pne 3",
+ "open_4": "\u00c5pne 4",
+ "open_all": "\u00c5pne alle",
+ "raise": "Hev",
+ "raise_1": "Hev 1",
+ "raise_2": "Hev 2",
+ "raise_3": "Hev 3",
+ "raise_4": "Hev 4",
+ "raise_all": "Hev alle",
+ "stop": "Stopp (favoritt)",
+ "stop_1": "Stopp 1",
+ "stop_2": "Stopp 2",
+ "stop_3": "Stopp 3",
+ "stop_4": "Stopp 4",
+ "stop_all": "Stopp alle"
+ },
+ "trigger_type": {
+ "press": "\"{subtype}\" trykket",
+ "release": "\"{subtype}\" utgitt"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/pl.json b/homeassistant/components/lutron_caseta/translations/pl.json
index 07417b0149e46c..8a8c0a759b01e9 100644
--- a/homeassistant/components/lutron_caseta/translations/pl.json
+++ b/homeassistant/components/lutron_caseta/translations/pl.json
@@ -2,16 +2,75 @@
"config": {
"abort": {
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
- "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "not_lutron_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Lutron"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
+ "flow_title": "Lutron Cas\u00e9ta {name} ({host})",
"step": {
"import_failed": {
"description": "Nie mo\u017cna skonfigurowa\u0107 mostka (host: {host}) zaimportowanego z pliku configuration.yaml.",
"title": "Nie uda\u0142o si\u0119 zaimportowa\u0107 konfiguracji mostka Cas\u00e9ta."
+ },
+ "link": {
+ "description": "Aby sparowa\u0107 z {name} ({host}), po przes\u0142aniu tego formularza naci\u015bnij czarny przycisk z ty\u0142u mostka.",
+ "title": "Sparuj z mostkiem"
+ },
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP"
+ },
+ "description": "Wprowad\u017a adres IP urz\u0105dzenia",
+ "title": "Po\u0142\u0105cz si\u0119 automatycznie z mostkiem"
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "pierwszy",
+ "button_2": "drugi",
+ "button_3": "trzeci",
+ "button_4": "czwarty",
+ "close_1": "zamknij 1",
+ "close_2": "zamknij 2",
+ "close_3": "zamknij 3",
+ "close_4": "zamknij 4",
+ "close_all": "zamknij wszystkie",
+ "group_1_button_1": "pierwsza grupa pierwszy przycisk",
+ "group_1_button_2": "pierwsza grupa drugi przycisk",
+ "group_2_button_1": "druga grupa pierwszy przycisk",
+ "group_2_button_2": "druga grupa drugi przycisk",
+ "lower": "opu\u015b\u0107",
+ "lower_1": "opu\u015b\u0107 1",
+ "lower_2": "opu\u015b\u0107 2",
+ "lower_3": "opu\u015b\u0107 3",
+ "lower_4": "opu\u015b\u0107 4",
+ "lower_all": "opu\u015b\u0107 wszystkie",
+ "off": "wy\u0142\u0105cz",
+ "on": "w\u0142\u0105cz",
+ "open_1": "otw\u00f3rz 1",
+ "open_2": "otw\u00f3rz 2",
+ "open_3": "otw\u00f3rz 3",
+ "open_4": "otw\u00f3rz 4",
+ "open_all": "otw\u00f3rz wszystkie",
+ "raise": "podnie\u015b",
+ "raise_1": "podnie\u015b 1",
+ "raise_2": "podnie\u015b 2",
+ "raise_3": "podnie\u015b 3",
+ "raise_4": "podnie\u015b 4",
+ "raise_all": "podnie\u015b wszystkie",
+ "stop": "zatrzymaj (ulubione)",
+ "stop_1": "zatrzymaj 1",
+ "stop_2": "zatrzymaj 2",
+ "stop_3": "zatrzymaj 3",
+ "stop_4": "zatrzymaj 4",
+ "stop_all": "zatrzymaj wszystkie"
+ },
+ "trigger_type": {
+ "press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty",
+ "release": "przycisk \"{subtype}\" zostanie zwolniony"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/ru.json b/homeassistant/components/lutron_caseta/translations/ru.json
index 05bd4f51c70bf1..f54057f464e7f6 100644
--- a/homeassistant/components/lutron_caseta/translations/ru.json
+++ b/homeassistant/components/lutron_caseta/translations/ru.json
@@ -2,16 +2,75 @@
"config": {
"abort": {
"already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
- "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "not_lutron_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 Lutron."
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
},
+ "flow_title": "Lutron Cas\u00e9ta {name} ({host})",
"step": {
"import_failed": {
"description": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0448\u043b\u044e\u0437 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml (\u0445\u043e\u0441\u0442: {host}).",
"title": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0448\u043b\u044e\u0437\u0430."
+ },
+ "link": {
+ "description": "\u0427\u0442\u043e\u0431\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 {name} ({host}), \u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u044d\u0442\u043e\u0439 \u0444\u043e\u0440\u043c\u044b \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0447\u0435\u0440\u043d\u0443\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0437\u0430\u0434\u043d\u0435\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u0448\u043b\u044e\u0437\u0430.",
+ "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441\u043e \u0448\u043b\u044e\u0437\u043e\u043c"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.",
+ "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443"
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "close_1": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c 1",
+ "close_2": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c 2",
+ "close_3": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c 3",
+ "close_4": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c 4",
+ "close_all": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c \u0432\u0441\u0435",
+ "group_1_button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "group_1_button_2": "\u041f\u0435\u0440\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u0432\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "group_2_button_1": "\u0412\u0442\u043e\u0440\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "group_2_button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u0432\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "lower": "\u041e\u043f\u0443\u0441\u0442\u0438\u0442\u044c",
+ "lower_1": "\u041e\u043f\u0443\u0441\u0442\u0438\u0442\u044c 1",
+ "lower_2": "\u041e\u043f\u0443\u0441\u0442\u0438\u0442\u044c 2",
+ "lower_3": "\u041e\u043f\u0443\u0441\u0442\u0438\u0442\u044c 3",
+ "lower_4": "\u041e\u043f\u0443\u0441\u0442\u0438\u0442\u044c 4",
+ "lower_all": "\u041e\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0432\u0441\u0435",
+ "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e",
+ "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e",
+ "open_1": "\u041e\u0442\u043a\u0440\u044b\u0442\u044c 1",
+ "open_2": "\u041e\u0442\u043a\u0440\u044b\u0442\u044c 2",
+ "open_3": "\u041e\u0442\u043a\u0440\u044b\u0442\u044c 3",
+ "open_4": "\u041e\u0442\u043a\u0440\u044b\u0442\u044c 4",
+ "open_all": "\u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0432\u0441\u0435",
+ "raise": "\u041f\u043e\u0434\u043d\u044f\u0442\u044c",
+ "raise_1": "\u041f\u043e\u0434\u043d\u044f\u0442\u044c 1",
+ "raise_2": "\u041f\u043e\u0434\u043d\u044f\u0442\u044c 2",
+ "raise_3": "\u041f\u043e\u0434\u043d\u044f\u0442\u044c 3",
+ "raise_4": "\u041f\u043e\u0434\u043d\u044f\u0442\u044c 4",
+ "raise_all": "\u041f\u043e\u0434\u043d\u044f\u0442\u044c \u0432\u0441\u0435",
+ "stop": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c (\u043b\u044e\u0431\u0438\u043c\u0430\u044f)",
+ "stop_1": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c 1",
+ "stop_2": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c 2",
+ "stop_3": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c 3",
+ "stop_4": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c 4",
+ "stop_all": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0432\u0441\u0435"
+ },
+ "trigger_type": {
+ "press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430",
+ "release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/tr.json b/homeassistant/components/lutron_caseta/translations/tr.json
new file mode 100644
index 00000000000000..fdc5e71a7ac518
--- /dev/null
+++ b/homeassistant/components/lutron_caseta/translations/tr.json
@@ -0,0 +1,72 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "not_lutron_device": "Bulunan cihaz bir Lutron cihaz\u0131 de\u011fil"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "flow_title": "Lutron Cas\u00e9ta {name} ( {host} )",
+ "step": {
+ "link": {
+ "description": "{name} ( {host} ) ile e\u015fle\u015ftirmek i\u00e7in, bu formu g\u00f6nderdikten sonra k\u00f6pr\u00fcn\u00fcn arkas\u0131ndaki siyah d\u00fc\u011fmeye bas\u0131n.",
+ "title": "K\u00f6pr\u00fc ile e\u015fle\u015ftirin"
+ },
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ },
+ "description": "Cihaz\u0131n ip adresini girin.",
+ "title": "K\u00f6pr\u00fcye otomatik olarak ba\u011flan\u0131n"
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "\u0130lk d\u00fc\u011fme",
+ "button_2": "\u0130kinci d\u00fc\u011fme",
+ "button_3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme",
+ "button_4": "D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fme",
+ "close_1": "Kapat 1",
+ "close_2": "Kapat 2",
+ "close_3": "Kapat 3",
+ "close_4": "Kapat 4",
+ "close_all": "Hepsini kapat",
+ "group_1_button_1": "Birinci Grup ilk d\u00fc\u011fme",
+ "group_1_button_2": "Birinci Grup ikinci d\u00fc\u011fme",
+ "group_2_button_1": "\u0130kinci Grup birinci d\u00fc\u011fme",
+ "group_2_button_2": "\u0130kinci Grup ikinci d\u00fc\u011fme",
+ "lower": "Alt",
+ "lower_1": "Alt 1",
+ "lower_2": "Alt 2",
+ "lower_3": "Alt 3",
+ "lower_4": "Alt 4",
+ "lower_all": "Hepsini indir",
+ "off": "Kapal\u0131",
+ "on": "A\u00e7\u0131k",
+ "open_1": "A\u00e7 1",
+ "open_2": "A\u00e7 2",
+ "open_3": "A\u00e7 3",
+ "open_4": "A\u00e7\u0131k 4",
+ "open_all": "Hepsini a\u00e7",
+ "raise": "Y\u00fckseltmek",
+ "raise_1": "Y\u00fckselt 1",
+ "raise_2": "Y\u00fckselt 2",
+ "raise_3": "Y\u00fckselt 3",
+ "raise_4": "Y\u00fckselt 4",
+ "raise_all": "Hepsini Y\u00fckseltin",
+ "stop": "Durak (favori)",
+ "stop_1": "Durak 1",
+ "stop_2": "Durdur 2",
+ "stop_3": "Durdur 3",
+ "stop_4": "Durdur 4",
+ "stop_all": "Hepsini durdur"
+ },
+ "trigger_type": {
+ "press": "\" {subtype} \" bas\u0131ld\u0131",
+ "release": "\" {subtype} \" yay\u0131nland\u0131"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/uk.json b/homeassistant/components/lutron_caseta/translations/uk.json
new file mode 100644
index 00000000000000..238e17405ceea3
--- /dev/null
+++ b/homeassistant/components/lutron_caseta/translations/uk.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "import_failed": {
+ "description": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0456\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0448\u043b\u044e\u0437 \u0437 \u0444\u0430\u0439\u043b\u0443 'configuration.yaml' (\u0445\u043e\u0441\u0442: {host}).",
+ "title": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0456\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0448\u043b\u044e\u0437\u0443."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/zh-Hant.json b/homeassistant/components/lutron_caseta/translations/zh-Hant.json
index 4e8df0d5e9f254..50762fafac1836 100644
--- a/homeassistant/components/lutron_caseta/translations/zh-Hant.json
+++ b/homeassistant/components/lutron_caseta/translations/zh-Hant.json
@@ -2,16 +2,75 @@
"config": {
"abort": {
"already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210",
- "cannot_connect": "\u9023\u7dda\u5931\u6557"
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "not_lutron_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Lutron \u88dd\u7f6e"
},
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557"
},
+ "flow_title": "Lutron Cas\u00e9ta {name} ({host})",
"step": {
"import_failed": {
"description": "\u7121\u6cd5\u8a2d\u5b9a\u7531 configuration.yaml \u532f\u5165\u7684 bridge\uff08\u4e3b\u6a5f\uff1a{host}\uff09\u3002",
"title": "\u532f\u5165 Cas\u00e9ta bridge \u8a2d\u5b9a\u5931\u6557\u3002"
+ },
+ "link": {
+ "description": "\u6b32\u8207 {name} ({host}) \u9032\u884c\u914d\u5c0d\uff0c\u65bc\u50b3\u9001\u8868\u683c\u5f8c\u3001\u4e8c\u4e0b Bridge \u5f8c\u65b9\u7684\u9ed1\u8272\u6309\u9215\u3002",
+ "title": "\u8207 Bridge \u914d\u5c0d"
+ },
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef"
+ },
+ "description": "\u8f38\u5165\u88dd\u7f6e IP \u4f4d\u5740\u3002",
+ "title": "\u81ea\u52d5\u9023\u7dda\u81f3 Bridge"
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "\u7b2c\u4e00\u500b\u6309\u9215",
+ "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215",
+ "button_3": "\u7b2c\u4e09\u500b\u6309\u9215",
+ "button_4": "\u7b2c\u56db\u500b\u6309\u9215",
+ "close_1": "\u95dc\u9589 1",
+ "close_2": "\u95dc\u9589 2",
+ "close_3": "\u95dc\u9589 3",
+ "close_4": "\u95dc\u9589 4",
+ "close_all": "\u5168\u90e8\u95dc\u9589",
+ "group_1_button_1": "\u7b2c\u4e00\u7d44\u7b2c\u4e00\u500b\u6309\u9215",
+ "group_1_button_2": "\u7b2c\u4e00\u7d44\u7b2c\u4e8c\u500b\u6309\u9215",
+ "group_2_button_1": "\u7b2c\u4e8c\u7d44\u7b2c\u4e00\u500b\u6309\u9215",
+ "group_2_button_2": "\u7b2c\u4e8c\u7d44\u7b2c\u4e8c\u500b\u6309\u9215",
+ "lower": "\u964d\u4f4e ",
+ "lower_1": "\u964d\u4f4e 1",
+ "lower_2": "\u964d\u4f4e 2",
+ "lower_3": "\u964d\u4f4e 3",
+ "lower_4": "\u964d\u4f4e 4",
+ "lower_all": "\u5168\u90e8\u964d\u4f4e",
+ "off": "\u95dc\u9589",
+ "on": "\u958b\u555f",
+ "open_1": "\u958b\u555f 1",
+ "open_2": "\u958b\u555f 2",
+ "open_3": "\u958b\u555f 3",
+ "open_4": "\u958b\u555f 4",
+ "open_all": "\u5168\u90e8\u958b\u555f",
+ "raise": "\u62ac\u8d77",
+ "raise_1": "\u62ac\u8d77 1",
+ "raise_2": "\u62ac\u8d77 2",
+ "raise_3": "\u62ac\u8d77 3",
+ "raise_4": "\u62ac\u8d77 4",
+ "raise_all": "\u5168\u90e8\u62ac\u8d77",
+ "stop": "\u505c\u6b62\uff08\u6700\u611b\uff09",
+ "stop_1": "\u505c\u6b62 1",
+ "stop_2": "\u505c\u6b62 2",
+ "stop_3": "\u505c\u6b62 3",
+ "stop_4": "\u505c\u6b62 4",
+ "stop_all": "\u5168\u90e8\u505c\u6b62"
+ },
+ "trigger_type": {
+ "press": "\"{subtype}\" \u6309\u4e0b",
+ "release": "\"{subtype}\" \u91cb\u653e"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lyft/sensor.py b/homeassistant/components/lyft/sensor.py
index 98084b28f0c154..c979231a216ce6 100644
--- a/homeassistant/components/lyft/sensor.py
+++ b/homeassistant/components/lyft/sensor.py
@@ -7,10 +7,9 @@
from lyft_rides.errors import APIError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, TIME_MINUTES
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -74,7 +73,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(dev, True)
-class LyftSensor(Entity):
+class LyftSensor(SensorEntity):
"""Implementation of an Lyft sensor."""
def __init__(self, sensorType, products, product_id, product):
@@ -110,7 +109,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
params = {
"Product ID": self._product["ride_type"],
diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py
new file mode 100644
index 00000000000000..c3ef18e7c7fa18
--- /dev/null
+++ b/homeassistant/components/lyric/__init__.py
@@ -0,0 +1,199 @@
+"""The Honeywell Lyric integration."""
+from __future__ import annotations
+
+import asyncio
+from datetime import timedelta
+import logging
+from typing import Any
+
+from aiolyric import Lyric
+from aiolyric.objects.device import LyricDevice
+from aiolyric.objects.location import LyricLocation
+import async_timeout
+import voluptuous as vol
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import (
+ aiohttp_client,
+ config_entry_oauth2_flow,
+ config_validation as cv,
+ device_registry as dr,
+)
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+ UpdateFailed,
+)
+
+from .api import ConfigEntryLyricClient, LyricLocalOAuth2Implementation
+from .config_flow import OAuth2FlowHandler
+from .const import DOMAIN, LYRIC_EXCEPTIONS, OAUTH2_AUTHORIZE, OAUTH2_TOKEN
+
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Required(CONF_CLIENT_ID): cv.string,
+ vol.Required(CONF_CLIENT_SECRET): cv.string,
+ }
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORMS = ["climate", "sensor"]
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+ """Set up the Honeywell Lyric component."""
+ hass.data[DOMAIN] = {}
+
+ if DOMAIN not in config:
+ return True
+
+ hass.data[DOMAIN][CONF_CLIENT_ID] = config[DOMAIN][CONF_CLIENT_ID]
+
+ OAuth2FlowHandler.async_register_implementation(
+ hass,
+ LyricLocalOAuth2Implementation(
+ hass,
+ DOMAIN,
+ config[DOMAIN][CONF_CLIENT_ID],
+ config[DOMAIN][CONF_CLIENT_SECRET],
+ OAUTH2_AUTHORIZE,
+ OAUTH2_TOKEN,
+ ),
+ )
+
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Set up Honeywell Lyric from a config entry."""
+ implementation = (
+ await config_entry_oauth2_flow.async_get_config_entry_implementation(
+ hass, entry
+ )
+ )
+
+ session = aiohttp_client.async_get_clientsession(hass)
+ oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
+
+ client = ConfigEntryLyricClient(session, oauth_session)
+
+ client_id = hass.data[DOMAIN][CONF_CLIENT_ID]
+ lyric = Lyric(client, client_id)
+
+ async def async_update_data() -> Lyric:
+ """Fetch data from Lyric."""
+ try:
+ async with async_timeout.timeout(60):
+ await lyric.get_locations()
+ return lyric
+ except LYRIC_EXCEPTIONS as exception:
+ raise UpdateFailed(exception) from exception
+
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ # Name of the data. For logging purposes.
+ name="lyric_coordinator",
+ update_method=async_update_data,
+ # Polling interval. Will only be polled if there are subscribers.
+ update_interval=timedelta(seconds=120),
+ )
+
+ hass.data[DOMAIN][entry.entry_id] = coordinator
+
+ # Fetch initial data so we have data when entities subscribe
+ await coordinator.async_config_entry_first_refresh()
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
+
+
+class LyricEntity(CoordinatorEntity):
+ """Defines a base Honeywell Lyric entity."""
+
+ def __init__(
+ self,
+ coordinator: DataUpdateCoordinator,
+ location: LyricLocation,
+ device: LyricDevice,
+ key: str,
+ name: str,
+ icon: str | None,
+ ) -> None:
+ """Initialize the Honeywell Lyric entity."""
+ super().__init__(coordinator)
+ self._key = key
+ self._name = name
+ self._icon = icon
+ self._location = location
+ self._mac_id = device.macID
+ self._device_name = device.name
+ self._device_model = device.deviceModel
+ self._update_thermostat = coordinator.data.update_thermostat
+
+ @property
+ def unique_id(self) -> str:
+ """Return the unique ID for this entity."""
+ return self._key
+
+ @property
+ def name(self) -> str:
+ """Return the name of the entity."""
+ return self._name
+
+ @property
+ def icon(self) -> str:
+ """Return the mdi icon of the entity."""
+ return self._icon
+
+ @property
+ def location(self) -> LyricLocation:
+ """Get the Lyric Location."""
+ return self.coordinator.data.locations_dict[self._location.locationID]
+
+ @property
+ def device(self) -> LyricDevice:
+ """Get the Lyric Device."""
+ return self.location.devices_dict[self._mac_id]
+
+
+class LyricDeviceEntity(LyricEntity):
+ """Defines a Honeywell Lyric device entity."""
+
+ @property
+ def device_info(self) -> dict[str, Any]:
+ """Return device information about this Honeywell Lyric instance."""
+ return {
+ "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac_id)},
+ "manufacturer": "Honeywell",
+ "model": self._device_model,
+ "name": self._device_name,
+ }
diff --git a/homeassistant/components/lyric/api.py b/homeassistant/components/lyric/api.py
new file mode 100644
index 00000000000000..a77c6365bafb66
--- /dev/null
+++ b/homeassistant/components/lyric/api.py
@@ -0,0 +1,55 @@
+"""API for Honeywell Lyric bound to Home Assistant OAuth."""
+import logging
+from typing import cast
+
+from aiohttp import BasicAuth, ClientSession
+from aiolyric.client import LyricClient
+
+from homeassistant.helpers import config_entry_oauth2_flow
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class ConfigEntryLyricClient(LyricClient):
+ """Provide Honeywell Lyric authentication tied to an OAuth2 based config entry."""
+
+ def __init__(
+ self,
+ websession: ClientSession,
+ oauth_session: config_entry_oauth2_flow.OAuth2Session,
+ ):
+ """Initialize Honeywell Lyric auth."""
+ super().__init__(websession)
+ self._oauth_session = oauth_session
+
+ async def async_get_access_token(self):
+ """Return a valid access token."""
+ if not self._oauth_session.valid_token:
+ await self._oauth_session.async_ensure_token_valid()
+
+ return self._oauth_session.token["access_token"]
+
+
+class LyricLocalOAuth2Implementation(
+ config_entry_oauth2_flow.LocalOAuth2Implementation
+):
+ """Lyric Local OAuth2 implementation."""
+
+ async def _token_request(self, data: dict) -> dict:
+ """Make a token request."""
+ session = async_get_clientsession(self.hass)
+
+ data["client_id"] = self.client_id
+
+ if self.client_secret is not None:
+ data["client_secret"] = self.client_secret
+
+ headers = {
+ "Authorization": BasicAuth(self.client_id, self.client_secret).encode(),
+ "Content-Type": "application/x-www-form-urlencoded",
+ }
+
+ resp = await session.post(self.token_url, headers=headers, data=data)
+ resp.raise_for_status()
+ return cast(dict, await resp.json())
diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py
new file mode 100644
index 00000000000000..0e3672f952ed8a
--- /dev/null
+++ b/homeassistant/components/lyric/climate.py
@@ -0,0 +1,300 @@
+"""Support for Honeywell Lyric climate platform."""
+from __future__ import annotations
+
+import logging
+from time import gmtime, strftime, time
+
+from aiolyric.objects.device import LyricDevice
+from aiolyric.objects.location import LyricLocation
+import voluptuous as vol
+
+from homeassistant.components.climate import ClimateEntity
+from homeassistant.components.climate.const import (
+ ATTR_TARGET_TEMP_HIGH,
+ ATTR_TARGET_TEMP_LOW,
+ CURRENT_HVAC_COOL,
+ CURRENT_HVAC_HEAT,
+ CURRENT_HVAC_IDLE,
+ CURRENT_HVAC_OFF,
+ HVAC_MODE_COOL,
+ HVAC_MODE_HEAT,
+ HVAC_MODE_HEAT_COOL,
+ HVAC_MODE_OFF,
+ SUPPORT_PRESET_MODE,
+ SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import ATTR_TEMPERATURE
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers import entity_platform
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+
+from . import LyricDeviceEntity
+from .const import (
+ DOMAIN,
+ LYRIC_EXCEPTIONS,
+ PRESET_HOLD_UNTIL,
+ PRESET_NO_HOLD,
+ PRESET_PERMANENT_HOLD,
+ PRESET_TEMPORARY_HOLD,
+ PRESET_VACATION_HOLD,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
+
+LYRIC_HVAC_ACTION_OFF = "EquipmentOff"
+LYRIC_HVAC_ACTION_HEAT = "Heat"
+LYRIC_HVAC_ACTION_COOL = "Cool"
+
+LYRIC_HVAC_MODE_OFF = "Off"
+LYRIC_HVAC_MODE_HEAT = "Heat"
+LYRIC_HVAC_MODE_COOL = "Cool"
+LYRIC_HVAC_MODE_HEAT_COOL = "Auto"
+
+LYRIC_HVAC_MODES = {
+ HVAC_MODE_OFF: LYRIC_HVAC_MODE_OFF,
+ HVAC_MODE_HEAT: LYRIC_HVAC_MODE_HEAT,
+ HVAC_MODE_COOL: LYRIC_HVAC_MODE_COOL,
+ HVAC_MODE_HEAT_COOL: LYRIC_HVAC_MODE_HEAT_COOL,
+}
+
+HVAC_MODES = {
+ LYRIC_HVAC_MODE_OFF: HVAC_MODE_OFF,
+ LYRIC_HVAC_MODE_HEAT: HVAC_MODE_HEAT,
+ LYRIC_HVAC_MODE_COOL: HVAC_MODE_COOL,
+ LYRIC_HVAC_MODE_HEAT_COOL: HVAC_MODE_HEAT_COOL,
+}
+
+HVAC_ACTIONS = {
+ LYRIC_HVAC_ACTION_OFF: CURRENT_HVAC_OFF,
+ LYRIC_HVAC_ACTION_HEAT: CURRENT_HVAC_HEAT,
+ LYRIC_HVAC_ACTION_COOL: CURRENT_HVAC_COOL,
+}
+
+SERVICE_HOLD_TIME = "set_hold_time"
+ATTR_TIME_PERIOD = "time_period"
+
+SCHEMA_HOLD_TIME = {
+ vol.Required(ATTR_TIME_PERIOD, default="01:00:00"): vol.All(
+ cv.time_period,
+ cv.positive_timedelta,
+ lambda td: strftime("%H:%M:%S", gmtime(time() + td.total_seconds())),
+ )
+}
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up the Honeywell Lyric climate platform based on a config entry."""
+ coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
+
+ entities = []
+
+ for location in coordinator.data.locations:
+ for device in location.devices:
+ entities.append(
+ LyricClimate(
+ coordinator, location, device, hass.config.units.temperature_unit
+ )
+ )
+
+ async_add_entities(entities, True)
+
+ platform = entity_platform.current_platform.get()
+
+ platform.async_register_entity_service(
+ SERVICE_HOLD_TIME,
+ SCHEMA_HOLD_TIME,
+ "async_set_hold_time",
+ )
+
+
+class LyricClimate(LyricDeviceEntity, ClimateEntity):
+ """Defines a Honeywell Lyric climate entity."""
+
+ def __init__(
+ self,
+ coordinator: DataUpdateCoordinator,
+ location: LyricLocation,
+ device: LyricDevice,
+ temperature_unit: str,
+ ) -> None:
+ """Initialize Honeywell Lyric climate entity."""
+ self._temperature_unit = temperature_unit
+
+ # Setup supported hvac modes
+ self._hvac_modes = [HVAC_MODE_OFF]
+
+ # Add supported lyric thermostat features
+ if LYRIC_HVAC_MODE_HEAT in device.allowedModes:
+ self._hvac_modes.append(HVAC_MODE_HEAT)
+
+ if LYRIC_HVAC_MODE_COOL in device.allowedModes:
+ self._hvac_modes.append(HVAC_MODE_COOL)
+
+ if (
+ LYRIC_HVAC_MODE_HEAT in device.allowedModes
+ and LYRIC_HVAC_MODE_COOL in device.allowedModes
+ ):
+ self._hvac_modes.append(HVAC_MODE_HEAT_COOL)
+
+ super().__init__(
+ coordinator,
+ location,
+ device,
+ f"{device.macID}_thermostat",
+ device.name,
+ None,
+ )
+
+ @property
+ def supported_features(self) -> int:
+ """Return the list of supported features."""
+ return SUPPORT_FLAGS
+
+ @property
+ def temperature_unit(self) -> str:
+ """Return the unit of measurement."""
+ return self._temperature_unit
+
+ @property
+ def current_temperature(self) -> float | None:
+ """Return the current temperature."""
+ return self.device.indoorTemperature
+
+ @property
+ def hvac_action(self) -> str:
+ """Return the current hvac action."""
+ action = HVAC_ACTIONS.get(self.device.operationStatus.mode, None)
+ if action == CURRENT_HVAC_OFF and self.hvac_mode != HVAC_MODE_OFF:
+ action = CURRENT_HVAC_IDLE
+ return action
+
+ @property
+ def hvac_mode(self) -> str:
+ """Return the hvac mode."""
+ return HVAC_MODES[self.device.changeableValues.mode]
+
+ @property
+ def hvac_modes(self) -> list[str]:
+ """List of available hvac modes."""
+ return self._hvac_modes
+
+ @property
+ def target_temperature(self) -> float | None:
+ """Return the temperature we try to reach."""
+ device = self.device
+ if not device.hasDualSetpointStatus:
+ return device.changeableValues.heatSetpoint
+ return None
+
+ @property
+ def target_temperature_low(self) -> float | None:
+ """Return the upper bound temperature we try to reach."""
+ device = self.device
+ if device.hasDualSetpointStatus:
+ return device.changeableValues.coolSetpoint
+ return None
+
+ @property
+ def target_temperature_high(self) -> float | None:
+ """Return the upper bound temperature we try to reach."""
+ device = self.device
+ if device.hasDualSetpointStatus:
+ return device.changeableValues.heatSetpoint
+ return None
+
+ @property
+ def preset_mode(self) -> str | None:
+ """Return current preset mode."""
+ return self.device.changeableValues.thermostatSetpointStatus
+
+ @property
+ def preset_modes(self) -> list[str] | None:
+ """Return preset modes."""
+ return [
+ PRESET_NO_HOLD,
+ PRESET_HOLD_UNTIL,
+ PRESET_PERMANENT_HOLD,
+ PRESET_TEMPORARY_HOLD,
+ PRESET_VACATION_HOLD,
+ ]
+
+ @property
+ def min_temp(self) -> float:
+ """Identify min_temp in Lyric API or defaults if not available."""
+ device = self.device
+ if LYRIC_HVAC_MODE_COOL in device.allowedModes:
+ return device.minCoolSetpoint
+ return device.minHeatSetpoint
+
+ @property
+ def max_temp(self) -> float:
+ """Identify max_temp in Lyric API or defaults if not available."""
+ device = self.device
+ if LYRIC_HVAC_MODE_HEAT in device.allowedModes:
+ return device.maxHeatSetpoint
+ return device.maxCoolSetpoint
+
+ async def async_set_temperature(self, **kwargs) -> None:
+ """Set new target temperature."""
+ target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
+ target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
+
+ device = self.device
+ if device.hasDualSetpointStatus:
+ if target_temp_low is not None and target_temp_high is not None:
+ temp = (target_temp_low, target_temp_high)
+ else:
+ raise HomeAssistantError(
+ "Could not find target_temp_low and/or target_temp_high in arguments"
+ )
+ else:
+ temp = kwargs.get(ATTR_TEMPERATURE)
+ _LOGGER.debug("Set temperature: %s", temp)
+ try:
+ await self._update_thermostat(self.location, device, heatSetpoint=temp)
+ except LYRIC_EXCEPTIONS as exception:
+ _LOGGER.error(exception)
+ await self.coordinator.async_refresh()
+
+ async def async_set_hvac_mode(self, hvac_mode: str) -> None:
+ """Set hvac mode."""
+ _LOGGER.debug("Set hvac mode: %s", hvac_mode)
+ try:
+ await self._update_thermostat(
+ self.location, self.device, mode=LYRIC_HVAC_MODES[hvac_mode]
+ )
+ except LYRIC_EXCEPTIONS as exception:
+ _LOGGER.error(exception)
+ await self.coordinator.async_refresh()
+
+ async def async_set_preset_mode(self, preset_mode: str) -> None:
+ """Set preset (PermanentHold, HoldUntil, NoHold, VacationHold) mode."""
+ _LOGGER.debug("Set preset mode: %s", preset_mode)
+ try:
+ await self._update_thermostat(
+ self.location, self.device, thermostatSetpointStatus=preset_mode
+ )
+ except LYRIC_EXCEPTIONS as exception:
+ _LOGGER.error(exception)
+ await self.coordinator.async_refresh()
+
+ async def async_set_hold_time(self, time_period: str) -> None:
+ """Set the time to hold until."""
+ _LOGGER.debug("set_hold_time: %s", time_period)
+ try:
+ await self._update_thermostat(
+ self.location,
+ self.device,
+ thermostatSetpointStatus=PRESET_HOLD_UNTIL,
+ nextPeriodTime=time_period,
+ )
+ except LYRIC_EXCEPTIONS as exception:
+ _LOGGER.error(exception)
+ await self.coordinator.async_refresh()
diff --git a/homeassistant/components/lyric/config_flow.py b/homeassistant/components/lyric/config_flow.py
new file mode 100644
index 00000000000000..1370d5e67ea9c8
--- /dev/null
+++ b/homeassistant/components/lyric/config_flow.py
@@ -0,0 +1,23 @@
+"""Config flow for Honeywell Lyric."""
+import logging
+
+from homeassistant import config_entries
+from homeassistant.helpers import config_entry_oauth2_flow
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class OAuth2FlowHandler(
+ config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
+):
+ """Config flow to handle Honeywell Lyric OAuth2 authentication."""
+
+ DOMAIN = DOMAIN
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
+
+ @property
+ def logger(self) -> logging.Logger:
+ """Return logger."""
+ return logging.getLogger(__name__)
diff --git a/homeassistant/components/lyric/const.py b/homeassistant/components/lyric/const.py
new file mode 100644
index 00000000000000..4f2f72b937b3fd
--- /dev/null
+++ b/homeassistant/components/lyric/const.py
@@ -0,0 +1,20 @@
+"""Constants for the Honeywell Lyric integration."""
+from aiohttp.client_exceptions import ClientResponseError
+from aiolyric.exceptions import LyricAuthenticationException, LyricException
+
+DOMAIN = "lyric"
+
+OAUTH2_AUTHORIZE = "https://api.honeywell.com/oauth2/authorize"
+OAUTH2_TOKEN = "https://api.honeywell.com/oauth2/token"
+
+PRESET_NO_HOLD = "NoHold"
+PRESET_TEMPORARY_HOLD = "TemporaryHold"
+PRESET_HOLD_UNTIL = "HoldUntil"
+PRESET_PERMANENT_HOLD = "PermanentHold"
+PRESET_VACATION_HOLD = "VacationHold"
+
+LYRIC_EXCEPTIONS = (
+ LyricAuthenticationException,
+ LyricException,
+ ClientResponseError,
+)
diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json
new file mode 100644
index 00000000000000..6aa028e26366a5
--- /dev/null
+++ b/homeassistant/components/lyric/manifest.json
@@ -0,0 +1,24 @@
+{
+ "domain": "lyric",
+ "name": "Honeywell Lyric",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/lyric",
+ "dependencies": ["http"],
+ "requirements": ["aiolyric==1.0.6"],
+ "codeowners": ["@timmo001"],
+ "quality_scale": "silver",
+ "dhcp": [
+ {
+ "hostname": "lyric-*",
+ "macaddress": "48A2E6"
+ },
+ {
+ "hostname": "lyric-*",
+ "macaddress": "B82CA0"
+ },
+ {
+ "hostname": "lyric-*",
+ "macaddress": "00D02D"
+ }
+ ]
+}
diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py
new file mode 100644
index 00000000000000..db90f474124dc7
--- /dev/null
+++ b/homeassistant/components/lyric/sensor.py
@@ -0,0 +1,252 @@
+"""Support for Honeywell Lyric sensor platform."""
+from datetime import datetime, timedelta
+
+from aiolyric.objects.device import LyricDevice
+from aiolyric.objects.location import LyricLocation
+
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ DEVICE_CLASS_HUMIDITY,
+ DEVICE_CLASS_TEMPERATURE,
+ DEVICE_CLASS_TIMESTAMP,
+)
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+from homeassistant.util import dt as dt_util
+
+from . import LyricDeviceEntity
+from .const import (
+ DOMAIN,
+ PRESET_HOLD_UNTIL,
+ PRESET_NO_HOLD,
+ PRESET_PERMANENT_HOLD,
+ PRESET_TEMPORARY_HOLD,
+ PRESET_VACATION_HOLD,
+)
+
+LYRIC_SETPOINT_STATUS_NAMES = {
+ PRESET_NO_HOLD: "Following Schedule",
+ PRESET_PERMANENT_HOLD: "Held Permanently",
+ PRESET_TEMPORARY_HOLD: "Held Temporarily",
+ PRESET_VACATION_HOLD: "Holiday",
+}
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up the Honeywell Lyric sensor platform based on a config entry."""
+ coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
+
+ entities = []
+
+ for location in coordinator.data.locations:
+ for device in location.devices:
+ cls_list = []
+ if device.indoorTemperature:
+ cls_list.append(LyricIndoorTemperatureSensor)
+ if device.outdoorTemperature:
+ cls_list.append(LyricOutdoorTemperatureSensor)
+ if device.displayedOutdoorHumidity:
+ cls_list.append(LyricOutdoorHumiditySensor)
+ if device.changeableValues:
+ if device.changeableValues.nextPeriodTime:
+ cls_list.append(LyricNextPeriodSensor)
+ if device.changeableValues.thermostatSetpointStatus:
+ cls_list.append(LyricSetpointStatusSensor)
+ for cls in cls_list:
+ entities.append(
+ cls(
+ coordinator,
+ location,
+ device,
+ hass.config.units.temperature_unit,
+ )
+ )
+
+ async_add_entities(entities, True)
+
+
+class LyricSensor(LyricDeviceEntity, SensorEntity):
+ """Defines a Honeywell Lyric sensor."""
+
+ def __init__(
+ self,
+ coordinator: DataUpdateCoordinator,
+ location: LyricLocation,
+ device: LyricDevice,
+ key: str,
+ name: str,
+ icon: str,
+ device_class: str = None,
+ unit_of_measurement: str = None,
+ ) -> None:
+ """Initialize Honeywell Lyric sensor."""
+ self._device_class = device_class
+ self._unit_of_measurement = unit_of_measurement
+
+ super().__init__(coordinator, location, device, key, name, icon)
+
+ @property
+ def device_class(self) -> str:
+ """Return the device class of the sensor."""
+ return self._device_class
+
+ @property
+ def unit_of_measurement(self) -> str:
+ """Return the unit this state is expressed in."""
+ return self._unit_of_measurement
+
+
+class LyricIndoorTemperatureSensor(LyricSensor):
+ """Defines a Honeywell Lyric sensor."""
+
+ def __init__(
+ self,
+ coordinator: DataUpdateCoordinator,
+ location: LyricLocation,
+ device: LyricDevice,
+ unit_of_measurement: str = None,
+ ) -> None:
+ """Initialize Honeywell Lyric sensor."""
+
+ super().__init__(
+ coordinator,
+ location,
+ device,
+ f"{device.macID}_indoor_temperature",
+ "Indoor Temperature",
+ None,
+ DEVICE_CLASS_TEMPERATURE,
+ unit_of_measurement,
+ )
+
+ @property
+ def state(self) -> str:
+ """Return the state of the sensor."""
+ return self.device.indoorTemperature
+
+
+class LyricOutdoorTemperatureSensor(LyricSensor):
+ """Defines a Honeywell Lyric sensor."""
+
+ def __init__(
+ self,
+ coordinator: DataUpdateCoordinator,
+ location: LyricLocation,
+ device: LyricDevice,
+ unit_of_measurement: str = None,
+ ) -> None:
+ """Initialize Honeywell Lyric sensor."""
+
+ super().__init__(
+ coordinator,
+ location,
+ device,
+ f"{device.macID}_outdoor_temperature",
+ "Outdoor Temperature",
+ None,
+ DEVICE_CLASS_TEMPERATURE,
+ unit_of_measurement,
+ )
+
+ @property
+ def state(self) -> str:
+ """Return the state of the sensor."""
+ return self.device.outdoorTemperature
+
+
+class LyricOutdoorHumiditySensor(LyricSensor):
+ """Defines a Honeywell Lyric sensor."""
+
+ def __init__(
+ self,
+ coordinator: DataUpdateCoordinator,
+ location: LyricLocation,
+ device: LyricDevice,
+ unit_of_measurement: str = None,
+ ) -> None:
+ """Initialize Honeywell Lyric sensor."""
+
+ super().__init__(
+ coordinator,
+ location,
+ device,
+ f"{device.macID}_outdoor_humidity",
+ "Outdoor Humidity",
+ None,
+ DEVICE_CLASS_HUMIDITY,
+ "%",
+ )
+
+ @property
+ def state(self) -> str:
+ """Return the state of the sensor."""
+ return self.device.displayedOutdoorHumidity
+
+
+class LyricNextPeriodSensor(LyricSensor):
+ """Defines a Honeywell Lyric sensor."""
+
+ def __init__(
+ self,
+ coordinator: DataUpdateCoordinator,
+ location: LyricLocation,
+ device: LyricDevice,
+ unit_of_measurement: str = None,
+ ) -> None:
+ """Initialize Honeywell Lyric sensor."""
+
+ super().__init__(
+ coordinator,
+ location,
+ device,
+ f"{device.macID}_next_period_time",
+ "Next Period Time",
+ None,
+ DEVICE_CLASS_TIMESTAMP,
+ )
+
+ @property
+ def state(self) -> datetime:
+ """Return the state of the sensor."""
+ device = self.device
+ time = dt_util.parse_time(device.changeableValues.nextPeriodTime)
+ now = dt_util.utcnow()
+ if time <= now.time():
+ now = now + timedelta(days=1)
+ return dt_util.as_utc(datetime.combine(now.date(), time))
+
+
+class LyricSetpointStatusSensor(LyricSensor):
+ """Defines a Honeywell Lyric sensor."""
+
+ def __init__(
+ self,
+ coordinator: DataUpdateCoordinator,
+ location: LyricLocation,
+ device: LyricDevice,
+ unit_of_measurement: str = None,
+ ) -> None:
+ """Initialize Honeywell Lyric sensor."""
+
+ super().__init__(
+ coordinator,
+ location,
+ device,
+ f"{device.macID}_setpoint_status",
+ "Setpoint Status",
+ "mdi:thermostat",
+ None,
+ )
+
+ @property
+ def state(self) -> str:
+ """Return the state of the sensor."""
+ device = self.device
+ if device.changeableValues.thermostatSetpointStatus == PRESET_HOLD_UNTIL:
+ return f"Held until {device.changeableValues.nextPeriodTime}"
+ return LYRIC_SETPOINT_STATUS_NAMES.get(
+ device.changeableValues.thermostatSetpointStatus, "Unknown"
+ )
diff --git a/homeassistant/components/lyric/services.yaml b/homeassistant/components/lyric/services.yaml
new file mode 100644
index 00000000000000..69c802d90aaa2c
--- /dev/null
+++ b/homeassistant/components/lyric/services.yaml
@@ -0,0 +1,18 @@
+set_hold_time:
+ name: Set Hold Time
+ description: "Sets the time to hold until"
+ target:
+ device:
+ integration: lyric
+ entity:
+ integration: lyric
+ domain: climate
+ fields:
+ time_period:
+ name: Time Period
+ description: Time to hold until
+ default: "01:00:00"
+ example: "01:00:00"
+ required: true
+ selector:
+ text:
diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json
new file mode 100644
index 00000000000000..4e5f2330840ee7
--- /dev/null
+++ b/homeassistant/components/lyric/strings.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "step": {
+ "pick_implementation": {
+ "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
+ }
+ },
+ "abort": {
+ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
+ "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]"
+ },
+ "create_entry": {
+ "default": "[%key:common::config_flow::create_entry::authenticated%]"
+ }
+ }
+}
diff --git a/homeassistant/components/lyric/translations/ca.json b/homeassistant/components/lyric/translations/ca.json
new file mode 100644
index 00000000000000..195d3d59262a9f
--- /dev/null
+++ b/homeassistant/components/lyric/translations/ca.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.",
+ "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3."
+ },
+ "create_entry": {
+ "default": "Autenticaci\u00f3 exitosa"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lyric/translations/cs.json b/homeassistant/components/lyric/translations/cs.json
new file mode 100644
index 00000000000000..2a54a82f41b427
--- /dev/null
+++ b/homeassistant/components/lyric/translations/cs.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el",
+ "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace."
+ },
+ "create_entry": {
+ "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Vyberte metodu ov\u011b\u0159en\u00ed"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lyric/translations/de.json b/homeassistant/components/lyric/translations/de.json
new file mode 100644
index 00000000000000..5bab6ed132bf52
--- /dev/null
+++ b/homeassistant/components/lyric/translations/de.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.",
+ "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen."
+ },
+ "create_entry": {
+ "default": "Erfolgreich authentifiziert"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "W\u00e4hle die Authentifizierungsmethode"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lyric/translations/en.json b/homeassistant/components/lyric/translations/en.json
new file mode 100644
index 00000000000000..e3849fc17a3aab
--- /dev/null
+++ b/homeassistant/components/lyric/translations/en.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Timeout generating authorize URL.",
+ "missing_configuration": "The component is not configured. Please follow the documentation."
+ },
+ "create_entry": {
+ "default": "Successfully authenticated"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Pick Authentication Method"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lyric/translations/es.json b/homeassistant/components/lyric/translations/es.json
new file mode 100644
index 00000000000000..db8d744d17620f
--- /dev/null
+++ b/homeassistant/components/lyric/translations/es.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.",
+ "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n."
+ },
+ "create_entry": {
+ "default": "Autenticado correctamente"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lyric/translations/et.json b/homeassistant/components/lyric/translations/et.json
new file mode 100644
index 00000000000000..c7d46e7e9426d2
--- /dev/null
+++ b/homeassistant/components/lyric/translations/et.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp",
+ "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni."
+ },
+ "create_entry": {
+ "default": "Tuvastamine \u00f5nnestus"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Vali tuvastusmeetod"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lyric/translations/fr.json b/homeassistant/components/lyric/translations/fr.json
new file mode 100644
index 00000000000000..540d3e1e6c2956
--- /dev/null
+++ b/homeassistant/components/lyric/translations/fr.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.",
+ "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation."
+ },
+ "create_entry": {
+ "default": "Authentification r\u00e9ussie"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "S\u00e9lectionner une m\u00e9thode d'authentification"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lyric/translations/hu.json b/homeassistant/components/lyric/translations/hu.json
new file mode 100644
index 00000000000000..cae1f6d20c0393
--- /dev/null
+++ b/homeassistant/components/lyric/translations/hu.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.",
+ "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t."
+ },
+ "create_entry": {
+ "default": "Sikeres hiteles\u00edt\u00e9s"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lyric/translations/id.json b/homeassistant/components/lyric/translations/id.json
new file mode 100644
index 00000000000000..876fe2f8c39c0c
--- /dev/null
+++ b/homeassistant/components/lyric/translations/id.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.",
+ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi."
+ },
+ "create_entry": {
+ "default": "Berhasil diautentikasi"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Pilih Metode Autentikasi"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lyric/translations/it.json b/homeassistant/components/lyric/translations/it.json
new file mode 100644
index 00000000000000..42536508716c7c
--- /dev/null
+++ b/homeassistant/components/lyric/translations/it.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.",
+ "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione."
+ },
+ "create_entry": {
+ "default": "Autenticazione riuscita"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Scegli il metodo di autenticazione"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lyric/translations/ko.json b/homeassistant/components/lyric/translations/ko.json
new file mode 100644
index 00000000000000..fa000ea1c06d1b
--- /dev/null
+++ b/homeassistant/components/lyric/translations/ko.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694."
+ },
+ "create_entry": {
+ "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lyric/translations/nl.json b/homeassistant/components/lyric/translations/nl.json
new file mode 100644
index 00000000000000..d490acb1b599fd
--- /dev/null
+++ b/homeassistant/components/lyric/translations/nl.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
+ "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen."
+ },
+ "create_entry": {
+ "default": "Succesvol geauthenticeerd"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Kies een authenticatie methode"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lyric/translations/no.json b/homeassistant/components/lyric/translations/no.json
new file mode 100644
index 00000000000000..a8f6ce4f9a3f3c
--- /dev/null
+++ b/homeassistant/components/lyric/translations/no.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse",
+ "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen"
+ },
+ "create_entry": {
+ "default": "Vellykket godkjenning"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Velg godkjenningsmetode"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lyric/translations/pl.json b/homeassistant/components/lyric/translations/pl.json
new file mode 100644
index 00000000000000..8c75c11dd7c4c3
--- /dev/null
+++ b/homeassistant/components/lyric/translations/pl.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji",
+ "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105."
+ },
+ "create_entry": {
+ "default": "Pomy\u015blnie uwierzytelniono"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Wybierz metod\u0119 uwierzytelniania"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lyric/translations/ru.json b/homeassistant/components/lyric/translations/ru.json
new file mode 100644
index 00000000000000..8d41a95fd29936
--- /dev/null
+++ b/homeassistant/components/lyric/translations/ru.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
+ "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438."
+ },
+ "create_entry": {
+ "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lyric/translations/tr.json b/homeassistant/components/lyric/translations/tr.json
new file mode 100644
index 00000000000000..773577271d2ad3
--- /dev/null
+++ b/homeassistant/components/lyric/translations/tr.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Yetki URL'si olu\u015fturulurken zaman a\u015f\u0131m\u0131 olu\u015ftu.",
+ "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin."
+ },
+ "create_entry": {
+ "default": "Ba\u015far\u0131yla do\u011fruland\u0131"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lyric/translations/zh-Hant.json b/homeassistant/components/lyric/translations/zh-Hant.json
new file mode 100644
index 00000000000000..b740fd3e063c93
--- /dev/null
+++ b/homeassistant/components/lyric/translations/zh-Hant.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002",
+ "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002"
+ },
+ "create_entry": {
+ "default": "\u5df2\u6210\u529f\u8a8d\u8b49"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/magicseaweed/sensor.py b/homeassistant/components/magicseaweed/sensor.py
index 9364bee27b2d0a..0dd27a60ae065c 100644
--- a/homeassistant/components/magicseaweed/sensor.py
+++ b/homeassistant/components/magicseaweed/sensor.py
@@ -5,7 +5,7 @@
import magicseaweed
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_API_KEY,
@@ -13,7 +13,6 @@
CONF_NAME,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
import homeassistant.util.dt as dt_util
@@ -90,7 +89,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class MagicSeaweedSensor(Entity):
+class MagicSeaweedSensor(SensorEntity):
"""Implementation of a MagicSeaweed sensor."""
def __init__(self, forecast_data, sensor_type, name, unit_system, hour=None):
@@ -136,7 +135,7 @@ def icon(self):
return ICON
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attrs
diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py
index e5a0f16863d219..5d05596fb23af3 100644
--- a/homeassistant/components/mailbox/__init__.py
+++ b/homeassistant/components/mailbox/__init__.py
@@ -84,7 +84,7 @@ async def async_setup_platform(p_type, p_config=None, discovery_info=None):
await component.async_add_entities([mailbox_entity])
setup_tasks = [
- async_setup_platform(p_type, p_config)
+ asyncio.create_task(async_setup_platform(p_type, p_config))
for p_type, p_config in config_per_platform(config, DOMAIN)
]
diff --git a/homeassistant/components/mailgun/__init__.py b/homeassistant/components/mailgun/__init__.py
index 220b6a1abc1f1c..39ee6e4635088a 100644
--- a/homeassistant/components/mailgun/__init__.py
+++ b/homeassistant/components/mailgun/__init__.py
@@ -51,11 +51,14 @@ async def handle_webhook(hass, webhook_id, request):
except ValueError:
return None
- if isinstance(data, dict) and "signature" in data:
- if await verify_webhook(hass, **data["signature"]):
- data["webhook_id"] = webhook_id
- hass.bus.async_fire(MESSAGE_RECEIVED, data)
- return
+ if (
+ isinstance(data, dict)
+ and "signature" in data
+ and await verify_webhook(hass, **data["signature"])
+ ):
+ data["webhook_id"] = webhook_id
+ hass.bus.async_fire(MESSAGE_RECEIVED, data)
+ return
_LOGGER.warning(
"Mailgun webhook received an unauthenticated message - webhook_id: %s",
diff --git a/homeassistant/components/mailgun/translations/de.json b/homeassistant/components/mailgun/translations/de.json
index f684f822fd51c6..118192b65160fe 100644
--- a/homeassistant/components/mailgun/translations/de.json
+++ b/homeassistant/components/mailgun/translations/de.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.",
+ "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen."
+ },
"create_entry": {
- "default": "Um Ereignisse an den Home Assistant zu senden, musst [Webhooks mit Mailgun]({mailgun_url}) einrichten. \n\n F\u00fclle die folgenden Informationen aus: \n\n - URL: `{webhook_url}` \n - Methode: POST \n - Inhaltstyp: application/json \n\nLies in der [Dokumentation]({docs_url}) wie du Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurierst."
+ "default": "Um Ereignisse an Home Assistant zu senden, musst du [Webhooks mit Mailgun]({mailgun_url}) einrichten. \n\n F\u00fclle die folgenden Informationen aus: \n\n - URL: `{webhook_url}` \n - Methode: POST \n - Inhaltstyp: application/json \n\nLies in der [Dokumentation]({docs_url}), wie du Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurierst."
},
"step": {
"user": {
diff --git a/homeassistant/components/mailgun/translations/hu.json b/homeassistant/components/mailgun/translations/hu.json
index 51bbe6ef04cc6c..14c2293734cd73 100644
--- a/homeassistant/components/mailgun/translations/hu.json
+++ b/homeassistant/components/mailgun/translations/hu.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.",
+ "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz."
+ },
"create_entry": {
- "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant programnak, be kell \u00e1ll\u00edtania a [Webhooks Mailgun-al] ( {mailgun_url} ) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n L\u00e1sd [a dokument\u00e1ci\u00f3] ( {docs_url} ), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re."
+ "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant programnak, be kell \u00e1ll\u00edtania a [Webhooks with Mailgun]({mailgun_url}) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n L\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re."
},
"step": {
"user": {
diff --git a/homeassistant/components/mailgun/translations/id.json b/homeassistant/components/mailgun/translations/id.json
new file mode 100644
index 00000000000000..b58deb171bef01
--- /dev/null
+++ b/homeassistant/components/mailgun/translations/id.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.",
+ "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook."
+ },
+ "create_entry": {
+ "default": "Untuk mengirim event ke Home Assistant, Anda harus menyiapkan [Webhooks dengan Mailgun]({mailgun_url}).\n\nIsi info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nBaca [dokumentasi]({docs_url}) tentang cara mengonfigurasi otomasi untuk menangani data masuk."
+ },
+ "step": {
+ "user": {
+ "description": "Yakin ingin menyiapkan Mailgun?",
+ "title": "Siapkan Mailgun Webhook"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mailgun/translations/ko.json b/homeassistant/components/mailgun/translations/ko.json
index 43b6586b14ff43..2a296303d58586 100644
--- a/homeassistant/components/mailgun/translations/ko.json
+++ b/homeassistant/components/mailgun/translations/ko.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4."
+ },
"create_entry": {
- "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Mailgun \uc6f9 \ud6c5]({mailgun_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \nHome Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
+ "default": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Mailgun \uc6f9 \ud6c5]({mailgun_url})\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \nHome Assistant\ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
},
"step": {
"user": {
diff --git a/homeassistant/components/mailgun/translations/nl.json b/homeassistant/components/mailgun/translations/nl.json
index 772a67c118e6df..dea33946af51f6 100644
--- a/homeassistant/components/mailgun/translations/nl.json
+++ b/homeassistant/components/mailgun/translations/nl.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk."
+ "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.",
+ "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen."
},
"create_entry": {
"default": "Om evenementen naar Home Assistant te verzenden, moet u [Webhooks with Mailgun]({mailgun_url}) instellen. \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Methode: POST \n - Inhoudstype: application/json \n\n Zie [de documentatie]({docs_url}) voor informatie over het configureren van automatiseringen om binnenkomende gegevens te verwerken."
diff --git a/homeassistant/components/mailgun/translations/tr.json b/homeassistant/components/mailgun/translations/tr.json
new file mode 100644
index 00000000000000..84adcdf8225c43
--- /dev/null
+++ b/homeassistant/components/mailgun/translations/tr.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.",
+ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mailgun/translations/uk.json b/homeassistant/components/mailgun/translations/uk.json
new file mode 100644
index 00000000000000..d999b52085a213
--- /dev/null
+++ b/homeassistant/components/mailgun/translations/uk.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.",
+ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c."
+ },
+ "create_entry": {
+ "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f [Mailgun]({mailgun_url}). \n\n\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0456\u0439 \u043f\u043e \u043e\u0431\u0440\u043e\u0431\u0446\u0456 \u0434\u0430\u043d\u0438\u0445, \u0449\u043e \u043d\u0430\u0434\u0445\u043e\u0434\u044f\u0442\u044c."
+ },
+ "step": {
+ "user": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Mailgun?",
+ "title": "Mailgun"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py
index 2313bcace19a76..00c155615ee6f7 100644
--- a/homeassistant/components/manual/alarm_control_panel.py
+++ b/homeassistant/components/manual/alarm_control_panel.py
@@ -394,7 +394,7 @@ def _validate_code(self, code, state):
return check
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self.state == STATE_ALARM_PENDING or self.state == STATE_ALARM_ARMING:
return {
diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py
index f11938396a7ec9..2fa0e631c1d342 100644
--- a/homeassistant/components/manual_mqtt/alarm_control_panel.py
+++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py
@@ -415,7 +415,7 @@ def _validate_code(self, code, state):
return check
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self.state != STATE_ALARM_PENDING:
return {}
diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py
index c89de5552d5bae..62af53079e831b 100644
--- a/homeassistant/components/matrix/__init__.py
+++ b/homeassistant/components/matrix/__init__.py
@@ -1,12 +1,13 @@
"""The Matrix bot component."""
from functools import partial
import logging
+import mimetypes
import os
from matrix_client.client import MatrixClient, MatrixRequestError
import voluptuous as vol
-from homeassistant.components.notify import ATTR_MESSAGE, ATTR_TARGET
+from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET
from homeassistant.const import (
CONF_NAME,
CONF_PASSWORD,
@@ -31,8 +32,12 @@
CONF_WORD = "word"
CONF_EXPRESSION = "expression"
+DEFAULT_CONTENT_TYPE = "application/octet-stream"
+
EVENT_MATRIX_COMMAND = "matrix_command"
+ATTR_IMAGES = "images" # optional images
+
COMMAND_SCHEMA = vol.All(
vol.Schema(
{
@@ -67,6 +72,9 @@
SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema(
{
vol.Required(ATTR_MESSAGE): cv.string,
+ vol.Optional(ATTR_DATA): {
+ vol.Optional(ATTR_IMAGES): vol.All(cv.ensure_list, [cv.string]),
+ },
vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]),
}
)
@@ -336,13 +344,20 @@ def _login_by_password(self):
return _client
- def _send_message(self, message, target_rooms):
- """Send the message to the Matrix server."""
+ def _send_image(self, img, target_rooms):
+ _LOGGER.debug("Uploading file from path, %s", img)
+ if not self.hass.config.is_allowed_path(img):
+ _LOGGER.error("Path not allowed: %s", img)
+ return
+ with open(img, "rb") as upfile:
+ imgfile = upfile.read()
+ content_type = mimetypes.guess_type(img)[0]
+ mxc = self._client.upload(imgfile, content_type)
for target_room in target_rooms:
try:
room = self._join_or_get_room(target_room)
- _LOGGER.debug(room.send_text(message))
+ room.send_image(mxc, img)
except MatrixRequestError as ex:
_LOGGER.error(
"Unable to deliver message to room '%s': %d, %s",
@@ -351,6 +366,28 @@ def _send_message(self, message, target_rooms):
ex.content,
)
+ def _send_message(self, message, data, target_rooms):
+ """Send the message to the Matrix server."""
+ for target_room in target_rooms:
+ try:
+ room = self._join_or_get_room(target_room)
+ if message is not None:
+ _LOGGER.debug(room.send_text(message))
+ except MatrixRequestError as ex:
+ _LOGGER.error(
+ "Unable to deliver message to room '%s': %d, %s",
+ target_room,
+ ex.code,
+ ex.content,
+ )
+ if data is not None:
+ for img in data.get(ATTR_IMAGES, []):
+ self._send_image(img, target_rooms)
+
def handle_send_message(self, service):
"""Handle the send_message service."""
- self._send_message(service.data[ATTR_MESSAGE], service.data[ATTR_TARGET])
+ self._send_message(
+ service.data.get(ATTR_MESSAGE),
+ service.data.get(ATTR_DATA),
+ service.data[ATTR_TARGET],
+ )
diff --git a/homeassistant/components/matrix/notify.py b/homeassistant/components/matrix/notify.py
index 0965783bf4d858..8643d7511bc4d2 100644
--- a/homeassistant/components/matrix/notify.py
+++ b/homeassistant/components/matrix/notify.py
@@ -2,6 +2,7 @@
import voluptuous as vol
from homeassistant.components.notify import (
+ ATTR_DATA,
ATTR_MESSAGE,
ATTR_TARGET,
PLATFORM_SCHEMA,
@@ -31,9 +32,10 @@ def __init__(self, default_room):
def send_message(self, message="", **kwargs):
"""Send the message to the Matrix server."""
target_rooms = kwargs.get(ATTR_TARGET) or [self._default_room]
-
service_data = {ATTR_TARGET: target_rooms, ATTR_MESSAGE: message}
-
+ data = kwargs.get(ATTR_DATA)
+ if data is not None:
+ service_data[ATTR_DATA] = data
return self.hass.services.call(
DOMAIN, SERVICE_SEND_MESSAGE, service_data=service_data
)
diff --git a/homeassistant/components/matrix/services.yaml b/homeassistant/components/matrix/services.yaml
index f8b0c53bda633c..fe99bf6365a657 100644
--- a/homeassistant/components/matrix/services.yaml
+++ b/homeassistant/components/matrix/services.yaml
@@ -7,3 +7,6 @@ send_message:
target:
description: A list of room(s) to send the message to.
example: "#hasstest:matrix.org"
+ data:
+ description: Extended information of notification. Supports list of images. Optional.
+ example: "{'images': ['/tmp/test.jpg']}"
diff --git a/homeassistant/components/maxcube/__init__.py b/homeassistant/components/maxcube/__init__.py
index ffd156b5e0033f..e38f08809a749a 100644
--- a/homeassistant/components/maxcube/__init__.py
+++ b/homeassistant/components/maxcube/__init__.py
@@ -4,7 +4,6 @@
from threading import Lock
import time
-from maxcube.connection import MaxCubeConnection
from maxcube.cube import MaxCube
import voluptuous as vol
@@ -60,7 +59,7 @@ def setup(hass, config):
scan_interval = gateway[CONF_SCAN_INTERVAL].total_seconds()
try:
- cube = MaxCube(MaxCubeConnection(host, port))
+ cube = MaxCube(host, port)
hass.data[DATA_KEY][host] = MaxCubeHandle(cube, scan_interval)
except timeout as ex:
_LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex))
@@ -86,6 +85,7 @@ class MaxCubeHandle:
def __init__(self, cube, scan_interval):
"""Initialize the Cube Handle."""
self.cube = cube
+ self.cube.use_persistent_connection = scan_interval <= 300 # seconds
self.scan_interval = scan_interval
self.mutex = Lock()
self._updatets = time.monotonic()
diff --git a/homeassistant/components/maxcube/binary_sensor.py b/homeassistant/components/maxcube/binary_sensor.py
index 376076352a639c..223c0e3fc9916a 100644
--- a/homeassistant/components/maxcube/binary_sensor.py
+++ b/homeassistant/components/maxcube/binary_sensor.py
@@ -11,13 +11,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"""Iterate through all MAX! Devices and add window shutters."""
devices = []
for handler in hass.data[DATA_KEY].values():
- cube = handler.cube
- for device in cube.devices:
- name = f"{cube.room_by_id(device.room_id).name} {device.name}"
-
+ for device in handler.cube.devices:
# Only add Window Shutters
if device.is_windowshutter():
- devices.append(MaxCubeShutter(handler, name, device.rf_address))
+ devices.append(MaxCubeShutter(handler, device))
if devices:
add_entities(devices)
@@ -26,13 +23,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class MaxCubeShutter(BinarySensorEntity):
"""Representation of a MAX! Cube Binary Sensor device."""
- def __init__(self, handler, name, rf_address):
+ def __init__(self, handler, device):
"""Initialize MAX! Cube BinarySensorEntity."""
- self._name = name
- self._sensor_type = DEVICE_CLASS_WINDOW
- self._rf_address = rf_address
+ room = handler.cube.room_by_id(device.room_id)
+ self._name = f"{room.name} {device.name}"
self._cubehandle = handler
- self._state = None
+ self._device = device
@property
def name(self):
@@ -42,15 +38,13 @@ def name(self):
@property
def device_class(self):
"""Return the class of this sensor."""
- return self._sensor_type
+ return DEVICE_CLASS_WINDOW
@property
def is_on(self):
"""Return true if the binary sensor is on/open."""
- return self._state
+ return self._device.is_open
def update(self):
"""Get latest data from MAX! Cube."""
self._cubehandle.update()
- device = self._cubehandle.cube.device_by_rf(self._rf_address)
- self._state = device.is_open
diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py
index c17cc988c1d9e3..75ee7ef21f096d 100644
--- a/homeassistant/components/maxcube/climate.py
+++ b/homeassistant/components/maxcube/climate.py
@@ -1,4 +1,6 @@
"""Support for MAX! Thermostats via MAX! Cube."""
+from __future__ import annotations
+
import logging
import socket
@@ -47,31 +49,14 @@
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
-HASS_PRESET_TO_MAX_MODE = {
- PRESET_AWAY: MAX_DEVICE_MODE_VACATION,
- PRESET_BOOST: MAX_DEVICE_MODE_BOOST,
- PRESET_NONE: MAX_DEVICE_MODE_AUTOMATIC,
- PRESET_ON: MAX_DEVICE_MODE_MANUAL,
-}
-
-MAX_MODE_TO_HASS_PRESET = {
- MAX_DEVICE_MODE_AUTOMATIC: PRESET_NONE,
- MAX_DEVICE_MODE_BOOST: PRESET_BOOST,
- MAX_DEVICE_MODE_MANUAL: PRESET_NONE,
- MAX_DEVICE_MODE_VACATION: PRESET_AWAY,
-}
-
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Iterate through all MAX! Devices and add thermostats."""
devices = []
for handler in hass.data[DATA_KEY].values():
- cube = handler.cube
- for device in cube.devices:
- name = f"{cube.room_by_id(device.room_id).name} {device.name}"
-
+ for device in handler.cube.devices:
if device.is_thermostat() or device.is_wallthermostat():
- devices.append(MaxCubeClimate(handler, name, device.rf_address))
+ devices.append(MaxCubeClimate(handler, device))
if devices:
add_entities(devices)
@@ -80,11 +65,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class MaxCubeClimate(ClimateEntity):
"""MAX! Cube ClimateEntity."""
- def __init__(self, handler, name, rf_address):
+ def __init__(self, handler, device):
"""Initialize MAX! Cube ClimateEntity."""
- self._name = name
- self._rf_address = rf_address
+ room = handler.cube.room_by_id(device.room_id)
+ self._name = f"{room.name} {device.name}"
self._cubehandle = handler
+ self._device = device
@property
def supported_features(self):
@@ -104,18 +90,15 @@ def name(self):
@property
def min_temp(self):
"""Return the minimum temperature."""
- device = self._cubehandle.cube.device_by_rf(self._rf_address)
- if device.min_temperature is None:
- return MIN_TEMPERATURE
- return device.min_temperature
+ temp = self._device.min_temperature or MIN_TEMPERATURE
+ # OFF_TEMPERATURE (always off) a is valid temperature to maxcube but not to Home Assistant.
+ # We use HVAC_MODE_OFF instead to represent a turned off thermostat.
+ return max(temp, MIN_TEMPERATURE)
@property
def max_temp(self):
"""Return the maximum temperature."""
- device = self._cubehandle.cube.device_by_rf(self._rf_address)
- if device.max_temperature is None:
- return MAX_TEMPERATURE
- return device.max_temperature
+ return self._device.max_temperature or MAX_TEMPERATURE
@property
def temperature_unit(self):
@@ -125,18 +108,17 @@ def temperature_unit(self):
@property
def current_temperature(self):
"""Return the current temperature."""
- device = self._cubehandle.cube.device_by_rf(self._rf_address)
- return device.actual_temperature
+ return self._device.actual_temperature
@property
def hvac_mode(self):
"""Return current operation mode."""
- device = self._cubehandle.cube.device_by_rf(self._rf_address)
- if device.mode in [MAX_DEVICE_MODE_AUTOMATIC, MAX_DEVICE_MODE_BOOST]:
+ mode = self._device.mode
+ if mode in [MAX_DEVICE_MODE_AUTOMATIC, MAX_DEVICE_MODE_BOOST]:
return HVAC_MODE_AUTO
if (
- device.mode == MAX_DEVICE_MODE_MANUAL
- and device.target_temperature == OFF_TEMPERATURE
+ mode == MAX_DEVICE_MODE_MANUAL
+ and self._device.target_temperature == OFF_TEMPERATURE
):
return HVAC_MODE_OFF
@@ -149,41 +131,46 @@ def hvac_modes(self):
def set_hvac_mode(self, hvac_mode: str):
"""Set new target hvac mode."""
- device = self._cubehandle.cube.device_by_rf(self._rf_address)
- temp = device.target_temperature
- mode = MAX_DEVICE_MODE_MANUAL
-
if hvac_mode == HVAC_MODE_OFF:
- temp = OFF_TEMPERATURE
- elif hvac_mode != HVAC_MODE_HEAT:
- # Reset the temperature to a sane value.
- # Ideally, we should send 0 and the device will set its
- # temperature according to the schedule. However, current
- # version of the library has a bug which causes an
- # exception when setting values below 8.
- if temp in [OFF_TEMPERATURE, ON_TEMPERATURE]:
- temp = device.eco_temperature
- mode = MAX_DEVICE_MODE_AUTOMATIC
-
- cube = self._cubehandle.cube
+ self._set_target(MAX_DEVICE_MODE_MANUAL, OFF_TEMPERATURE)
+ elif hvac_mode == HVAC_MODE_HEAT:
+ temp = max(self._device.target_temperature, self.min_temp)
+ self._set_target(MAX_DEVICE_MODE_MANUAL, temp)
+ elif hvac_mode == HVAC_MODE_AUTO:
+ self._set_target(MAX_DEVICE_MODE_AUTOMATIC, None)
+ else:
+ raise ValueError(f"unsupported HVAC mode {hvac_mode}")
+
+ def _set_target(self, mode: int | None, temp: float | None) -> None:
+ """
+ Set the mode and/or temperature of the thermostat.
+
+ @param mode: this is the mode to change to.
+ @param temp: the temperature to target.
+
+ Both parameters are optional. When mode is undefined, it keeps
+ the previous mode. When temp is undefined, it fetches the
+ temperature from the weekly schedule when mode is
+ MAX_DEVICE_MODE_AUTOMATIC and keeps the previous
+ temperature otherwise.
+ """
with self._cubehandle.mutex:
try:
- cube.set_temperature_mode(device, temp, mode)
+ self._cubehandle.cube.set_temperature_mode(self._device, temp, mode)
except (socket.timeout, OSError):
_LOGGER.error("Setting HVAC mode failed")
- return
@property
def hvac_action(self):
"""Return the current running hvac operation if supported."""
- cube = self._cubehandle.cube
- device = cube.device_by_rf(self._rf_address)
valve = 0
- if device.is_thermostat():
- valve = device.valve_position
- elif device.is_wallthermostat():
- for device in cube.devices_by_room(cube.room_by_id(device.room_id)):
+ if self._device.is_thermostat():
+ valve = self._device.valve_position
+ elif self._device.is_wallthermostat():
+ cube = self._cubehandle.cube
+ room = cube.room_by_id(self._device.room_id)
+ for device in cube.devices_by_room(room):
if device.is_thermostat() and device.valve_position > 0:
valve = device.valve_position
break
@@ -201,49 +188,35 @@ def hvac_action(self):
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
- device = self._cubehandle.cube.device_by_rf(self._rf_address)
- if (
- device.target_temperature is None
- or device.target_temperature < self.min_temp
- or device.target_temperature > self.max_temp
- ):
+ temp = self._device.target_temperature
+ if temp is None or temp < self.min_temp or temp > self.max_temp:
return None
- return device.target_temperature
+ return temp
def set_temperature(self, **kwargs):
"""Set new target temperatures."""
- if kwargs.get(ATTR_TEMPERATURE) is None:
- return False
-
- target_temperature = kwargs.get(ATTR_TEMPERATURE)
- device = self._cubehandle.cube.device_by_rf(self._rf_address)
-
- cube = self._cubehandle.cube
-
- with self._cubehandle.mutex:
- try:
- cube.set_target_temperature(device, target_temperature)
- except (socket.timeout, OSError):
- _LOGGER.error("Setting target temperature failed")
- return False
+ temp = kwargs.get(ATTR_TEMPERATURE)
+ if temp is None:
+ raise ValueError(
+ f"No {ATTR_TEMPERATURE} parameter passed to set_temperature method."
+ )
+ self._set_target(None, temp)
@property
def preset_mode(self):
"""Return the current preset mode."""
- device = self._cubehandle.cube.device_by_rf(self._rf_address)
- if self.hvac_mode == HVAC_MODE_OFF:
- return PRESET_NONE
-
- if device.mode == MAX_DEVICE_MODE_MANUAL:
- if device.target_temperature == device.comfort_temperature:
+ if self._device.mode == MAX_DEVICE_MODE_MANUAL:
+ if self._device.target_temperature == self._device.comfort_temperature:
return PRESET_COMFORT
- if device.target_temperature == device.eco_temperature:
+ if self._device.target_temperature == self._device.eco_temperature:
return PRESET_ECO
- if device.target_temperature == ON_TEMPERATURE:
+ if self._device.target_temperature == ON_TEMPERATURE:
return PRESET_ON
- return PRESET_NONE
-
- return MAX_MODE_TO_HASS_PRESET[device.mode]
+ elif self._device.mode == MAX_DEVICE_MODE_BOOST:
+ return PRESET_BOOST
+ elif self._device.mode == MAX_DEVICE_MODE_VACATION:
+ return PRESET_AWAY
+ return PRESET_NONE
@property
def preset_modes(self):
@@ -259,37 +232,27 @@ def preset_modes(self):
def set_preset_mode(self, preset_mode):
"""Set new operation mode."""
- device = self._cubehandle.cube.device_by_rf(self._rf_address)
- temp = device.target_temperature
- mode = MAX_DEVICE_MODE_AUTOMATIC
-
- if preset_mode in [PRESET_COMFORT, PRESET_ECO, PRESET_ON]:
- mode = MAX_DEVICE_MODE_MANUAL
- if preset_mode == PRESET_COMFORT:
- temp = device.comfort_temperature
- elif preset_mode == PRESET_ECO:
- temp = device.eco_temperature
- else:
- temp = ON_TEMPERATURE
+ if preset_mode == PRESET_COMFORT:
+ self._set_target(MAX_DEVICE_MODE_MANUAL, self._device.comfort_temperature)
+ elif preset_mode == PRESET_ECO:
+ self._set_target(MAX_DEVICE_MODE_MANUAL, self._device.eco_temperature)
+ elif preset_mode == PRESET_ON:
+ self._set_target(MAX_DEVICE_MODE_MANUAL, ON_TEMPERATURE)
+ elif preset_mode == PRESET_AWAY:
+ self._set_target(MAX_DEVICE_MODE_VACATION, None)
+ elif preset_mode == PRESET_BOOST:
+ self._set_target(MAX_DEVICE_MODE_BOOST, None)
+ elif preset_mode == PRESET_NONE:
+ self._set_target(MAX_DEVICE_MODE_AUTOMATIC, None)
else:
- mode = HASS_PRESET_TO_MAX_MODE[preset_mode] or MAX_DEVICE_MODE_AUTOMATIC
-
- with self._cubehandle.mutex:
- try:
- self._cubehandle.cube.set_temperature_mode(device, temp, mode)
- except (socket.timeout, OSError):
- _LOGGER.error("Setting operation mode failed")
- return
+ raise ValueError(f"unsupported preset mode {preset_mode}")
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the optional state attributes."""
- cube = self._cubehandle.cube
- device = cube.device_by_rf(self._rf_address)
-
- if not device.is_thermostat():
+ if not self._device.is_thermostat():
return {}
- return {ATTR_VALVE_POSITION: device.valve_position}
+ return {ATTR_VALVE_POSITION: self._device.valve_position}
def update(self):
"""Get latest data from MAX! Cube."""
diff --git a/homeassistant/components/maxcube/manifest.json b/homeassistant/components/maxcube/manifest.json
index e6badb254f7af5..ddc21bd2358f0c 100644
--- a/homeassistant/components/maxcube/manifest.json
+++ b/homeassistant/components/maxcube/manifest.json
@@ -2,6 +2,6 @@
"domain": "maxcube",
"name": "eQ-3 MAX!",
"documentation": "https://www.home-assistant.io/integrations/maxcube",
- "requirements": ["maxcube-api==0.3.0"],
+ "requirements": ["maxcube-api==0.4.1"],
"codeowners": []
}
diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py
new file mode 100644
index 00000000000000..2f4e0e84f13bda
--- /dev/null
+++ b/homeassistant/components/mazda/__init__.py
@@ -0,0 +1,172 @@
+"""The Mazda Connected Services integration."""
+import asyncio
+from datetime import timedelta
+import logging
+
+import async_timeout
+from pymazda import (
+ Client as MazdaAPI,
+ MazdaAccountLockedException,
+ MazdaAPIEncryptionException,
+ MazdaAuthenticationException,
+ MazdaException,
+ MazdaTokenExpiredException,
+)
+
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
+from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import aiohttp_client
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+ UpdateFailed,
+)
+from homeassistant.util.async_ import gather_with_concurrency
+
+from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORMS = ["sensor"]
+
+
+async def with_timeout(task, timeout_seconds=10):
+ """Run an async task with a timeout."""
+ async with async_timeout.timeout(timeout_seconds):
+ return await task
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+ """Set up the Mazda Connected Services component."""
+ hass.data[DOMAIN] = {}
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up Mazda Connected Services from a config entry."""
+ email = entry.data[CONF_EMAIL]
+ password = entry.data[CONF_PASSWORD]
+ region = entry.data[CONF_REGION]
+
+ websession = aiohttp_client.async_get_clientsession(hass)
+ mazda_client = MazdaAPI(email, password, region, websession)
+
+ try:
+ await mazda_client.validate_credentials()
+ except MazdaAuthenticationException:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_REAUTH},
+ data=entry.data,
+ )
+ )
+ return False
+ except (
+ MazdaException,
+ MazdaAccountLockedException,
+ MazdaTokenExpiredException,
+ MazdaAPIEncryptionException,
+ ) as ex:
+ _LOGGER.error("Error occurred during Mazda login request: %s", ex)
+ raise ConfigEntryNotReady from ex
+
+ async def async_update_data():
+ """Fetch data from Mazda API."""
+ try:
+ vehicles = await with_timeout(mazda_client.get_vehicles())
+
+ vehicle_status_tasks = [
+ with_timeout(mazda_client.get_vehicle_status(vehicle["id"]))
+ for vehicle in vehicles
+ ]
+ statuses = await gather_with_concurrency(5, *vehicle_status_tasks)
+
+ for vehicle, status in zip(vehicles, statuses):
+ vehicle["status"] = status
+
+ return vehicles
+ except MazdaAuthenticationException as ex:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_REAUTH},
+ data=entry.data,
+ )
+ )
+ raise UpdateFailed("Not authenticated with Mazda API") from ex
+ except Exception as ex:
+ _LOGGER.exception(
+ "Unknown error occurred during Mazda update request: %s", ex
+ )
+ raise UpdateFailed(ex) from ex
+
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name=DOMAIN,
+ update_method=async_update_data,
+ update_interval=timedelta(seconds=60),
+ )
+
+ hass.data[DOMAIN][entry.entry_id] = {
+ DATA_CLIENT: mazda_client,
+ DATA_COORDINATOR: coordinator,
+ }
+
+ # Fetch initial data so we have data when entities subscribe
+ await coordinator.async_config_entry_first_refresh()
+
+ # Setup components
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
+
+
+class MazdaEntity(CoordinatorEntity):
+ """Defines a base Mazda entity."""
+
+ def __init__(self, coordinator, index):
+ """Initialize the Mazda entity."""
+ super().__init__(coordinator)
+ self.index = index
+ self.vin = self.coordinator.data[self.index]["vin"]
+
+ @property
+ def device_info(self):
+ """Return device info for the Mazda entity."""
+ data = self.coordinator.data[self.index]
+ return {
+ "identifiers": {(DOMAIN, self.vin)},
+ "name": self.get_vehicle_name(),
+ "manufacturer": "Mazda",
+ "model": f"{data['modelYear']} {data['carlineName']}",
+ }
+
+ def get_vehicle_name(self):
+ """Return the vehicle name, to be used as a prefix for names of other entities."""
+ data = self.coordinator.data[self.index]
+ if "nickname" in data and len(data["nickname"]) > 0:
+ return data["nickname"]
+ return f"{data['modelYear']} {data['carlineName']}"
diff --git a/homeassistant/components/mazda/config_flow.py b/homeassistant/components/mazda/config_flow.py
new file mode 100644
index 00000000000000..3c1137b8e8053c
--- /dev/null
+++ b/homeassistant/components/mazda/config_flow.py
@@ -0,0 +1,115 @@
+"""Config flow for Mazda Connected Services integration."""
+import logging
+
+import aiohttp
+from pymazda import (
+ Client as MazdaAPI,
+ MazdaAccountLockedException,
+ MazdaAuthenticationException,
+)
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
+from homeassistant.helpers import aiohttp_client
+
+from .const import DOMAIN, MAZDA_REGIONS
+
+_LOGGER = logging.getLogger(__name__)
+
+DATA_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_EMAIL): str,
+ vol.Required(CONF_PASSWORD): str,
+ vol.Required(CONF_REGION): vol.In(MAZDA_REGIONS),
+ }
+)
+
+
+class MazdaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Mazda Connected Services."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ errors = {}
+
+ if user_input is not None:
+ await self.async_set_unique_id(user_input[CONF_EMAIL].lower())
+ websession = aiohttp_client.async_get_clientsession(self.hass)
+ mazda_client = MazdaAPI(
+ user_input[CONF_EMAIL],
+ user_input[CONF_PASSWORD],
+ user_input[CONF_REGION],
+ websession,
+ )
+
+ try:
+ await mazda_client.validate_credentials()
+ except MazdaAuthenticationException:
+ errors["base"] = "invalid_auth"
+ except MazdaAccountLockedException:
+ errors["base"] = "account_locked"
+ except aiohttp.ClientError:
+ errors["base"] = "cannot_connect"
+ except Exception as ex: # pylint: disable=broad-except
+ errors["base"] = "unknown"
+ _LOGGER.exception(
+ "Unknown error occurred during Mazda login request: %s", ex
+ )
+ else:
+ return self.async_create_entry(
+ title=user_input[CONF_EMAIL], data=user_input
+ )
+
+ return self.async_show_form(
+ step_id="user", data_schema=DATA_SCHEMA, errors=errors
+ )
+
+ async def async_step_reauth(self, user_input=None):
+ """Perform reauth if the user credentials have changed."""
+ errors = {}
+
+ if user_input is not None:
+ try:
+ websession = aiohttp_client.async_get_clientsession(self.hass)
+ mazda_client = MazdaAPI(
+ user_input[CONF_EMAIL],
+ user_input[CONF_PASSWORD],
+ user_input[CONF_REGION],
+ websession,
+ )
+ await mazda_client.validate_credentials()
+ except MazdaAuthenticationException:
+ errors["base"] = "invalid_auth"
+ except MazdaAccountLockedException:
+ errors["base"] = "account_locked"
+ except aiohttp.ClientError:
+ errors["base"] = "cannot_connect"
+ except Exception as ex: # pylint: disable=broad-except
+ errors["base"] = "unknown"
+ _LOGGER.exception(
+ "Unknown error occurred during Mazda login request: %s", ex
+ )
+ else:
+ await self.async_set_unique_id(user_input[CONF_EMAIL].lower())
+
+ for entry in self._async_current_entries():
+ if entry.unique_id == self.unique_id:
+ self.hass.config_entries.async_update_entry(
+ entry, data=user_input
+ )
+
+ # Reload the config entry otherwise devices will remain unavailable
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(entry.entry_id)
+ )
+
+ return self.async_abort(reason="reauth_successful")
+ errors["base"] = "unknown"
+
+ return self.async_show_form(
+ step_id="reauth", data_schema=DATA_SCHEMA, errors=errors
+ )
diff --git a/homeassistant/components/mazda/const.py b/homeassistant/components/mazda/const.py
new file mode 100644
index 00000000000000..c75f6bf3b77b03
--- /dev/null
+++ b/homeassistant/components/mazda/const.py
@@ -0,0 +1,8 @@
+"""Constants for the Mazda Connected Services integration."""
+
+DOMAIN = "mazda"
+
+DATA_CLIENT = "mazda_client"
+DATA_COORDINATOR = "coordinator"
+
+MAZDA_REGIONS = {"MNAO": "North America", "MME": "Europe", "MJO": "Japan"}
diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json
new file mode 100644
index 00000000000000..c3a05a351c304d
--- /dev/null
+++ b/homeassistant/components/mazda/manifest.json
@@ -0,0 +1,9 @@
+{
+ "domain": "mazda",
+ "name": "Mazda Connected Services",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/mazda",
+ "requirements": ["pymazda==0.0.9"],
+ "codeowners": ["@bdr99"],
+ "quality_scale": "platinum"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py
new file mode 100644
index 00000000000000..7382347e6de5a9
--- /dev/null
+++ b/homeassistant/components/mazda/sensor.py
@@ -0,0 +1,270 @@
+"""Platform for Mazda sensor integration."""
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.const import (
+ CONF_UNIT_SYSTEM_IMPERIAL,
+ LENGTH_KILOMETERS,
+ LENGTH_MILES,
+ PERCENTAGE,
+ PRESSURE_PSI,
+)
+
+from . import MazdaEntity
+from .const import DATA_COORDINATOR, DOMAIN
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the sensor platform."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
+
+ entities = []
+
+ for index, _ in enumerate(coordinator.data):
+ entities.append(MazdaFuelRemainingSensor(coordinator, index))
+ entities.append(MazdaFuelDistanceSensor(coordinator, index))
+ entities.append(MazdaOdometerSensor(coordinator, index))
+ entities.append(MazdaFrontLeftTirePressureSensor(coordinator, index))
+ entities.append(MazdaFrontRightTirePressureSensor(coordinator, index))
+ entities.append(MazdaRearLeftTirePressureSensor(coordinator, index))
+ entities.append(MazdaRearRightTirePressureSensor(coordinator, index))
+
+ async_add_entities(entities)
+
+
+class MazdaFuelRemainingSensor(MazdaEntity, SensorEntity):
+ """Class for the fuel remaining sensor."""
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ vehicle_name = self.get_vehicle_name()
+ return f"{vehicle_name} Fuel Remaining Percentage"
+
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this entity."""
+ return f"{self.vin}_fuel_remaining_percentage"
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return PERCENTAGE
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend."""
+ return "mdi:gas-station"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self.coordinator.data[self.index]["status"]["fuelRemainingPercent"]
+
+
+class MazdaFuelDistanceSensor(MazdaEntity, SensorEntity):
+ """Class for the fuel distance sensor."""
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ vehicle_name = self.get_vehicle_name()
+ return f"{vehicle_name} Fuel Distance Remaining"
+
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this entity."""
+ return f"{self.vin}_fuel_distance_remaining"
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL:
+ return LENGTH_MILES
+ return LENGTH_KILOMETERS
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend."""
+ return "mdi:gas-station"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ fuel_distance_km = self.coordinator.data[self.index]["status"][
+ "fuelDistanceRemainingKm"
+ ]
+ return (
+ None
+ if fuel_distance_km is None
+ else round(
+ self.hass.config.units.length(fuel_distance_km, LENGTH_KILOMETERS)
+ )
+ )
+
+
+class MazdaOdometerSensor(MazdaEntity, SensorEntity):
+ """Class for the odometer sensor."""
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ vehicle_name = self.get_vehicle_name()
+ return f"{vehicle_name} Odometer"
+
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this entity."""
+ return f"{self.vin}_odometer"
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL:
+ return LENGTH_MILES
+ return LENGTH_KILOMETERS
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend."""
+ return "mdi:speedometer"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ odometer_km = self.coordinator.data[self.index]["status"]["odometerKm"]
+ return (
+ None
+ if odometer_km is None
+ else round(self.hass.config.units.length(odometer_km, LENGTH_KILOMETERS))
+ )
+
+
+class MazdaFrontLeftTirePressureSensor(MazdaEntity, SensorEntity):
+ """Class for the front left tire pressure sensor."""
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ vehicle_name = self.get_vehicle_name()
+ return f"{vehicle_name} Front Left Tire Pressure"
+
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this entity."""
+ return f"{self.vin}_front_left_tire_pressure"
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return PRESSURE_PSI
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend."""
+ return "mdi:car-tire-alert"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ tire_pressure = self.coordinator.data[self.index]["status"]["tirePressure"][
+ "frontLeftTirePressurePsi"
+ ]
+ return None if tire_pressure is None else round(tire_pressure)
+
+
+class MazdaFrontRightTirePressureSensor(MazdaEntity, SensorEntity):
+ """Class for the front right tire pressure sensor."""
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ vehicle_name = self.get_vehicle_name()
+ return f"{vehicle_name} Front Right Tire Pressure"
+
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this entity."""
+ return f"{self.vin}_front_right_tire_pressure"
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return PRESSURE_PSI
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend."""
+ return "mdi:car-tire-alert"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ tire_pressure = self.coordinator.data[self.index]["status"]["tirePressure"][
+ "frontRightTirePressurePsi"
+ ]
+ return None if tire_pressure is None else round(tire_pressure)
+
+
+class MazdaRearLeftTirePressureSensor(MazdaEntity, SensorEntity):
+ """Class for the rear left tire pressure sensor."""
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ vehicle_name = self.get_vehicle_name()
+ return f"{vehicle_name} Rear Left Tire Pressure"
+
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this entity."""
+ return f"{self.vin}_rear_left_tire_pressure"
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return PRESSURE_PSI
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend."""
+ return "mdi:car-tire-alert"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ tire_pressure = self.coordinator.data[self.index]["status"]["tirePressure"][
+ "rearLeftTirePressurePsi"
+ ]
+ return None if tire_pressure is None else round(tire_pressure)
+
+
+class MazdaRearRightTirePressureSensor(MazdaEntity, SensorEntity):
+ """Class for the rear right tire pressure sensor."""
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ vehicle_name = self.get_vehicle_name()
+ return f"{vehicle_name} Rear Right Tire Pressure"
+
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this entity."""
+ return f"{self.vin}_rear_right_tire_pressure"
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return PRESSURE_PSI
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend."""
+ return "mdi:car-tire-alert"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ tire_pressure = self.coordinator.data[self.index]["status"]["tirePressure"][
+ "rearRightTirePressurePsi"
+ ]
+ return None if tire_pressure is None else round(tire_pressure)
diff --git a/homeassistant/components/mazda/strings.json b/homeassistant/components/mazda/strings.json
new file mode 100644
index 00000000000000..1950260bfcbea5
--- /dev/null
+++ b/homeassistant/components/mazda/strings.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ },
+ "error": {
+ "account_locked": "Account locked. Please try again later.",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "email": "[%key:common::config_flow::data::email%]",
+ "password": "[%key:common::config_flow::data::password%]",
+ "region": "Region"
+ },
+ "description": "Authentication failed for Mazda Connected Services. Please enter your current credentials.",
+ "title": "Mazda Connected Services - Authentication Failed"
+ },
+ "user": {
+ "data": {
+ "email": "[%key:common::config_flow::data::email%]",
+ "password": "[%key:common::config_flow::data::password%]",
+ "region": "Region"
+ },
+ "description": "Please enter the email address and password you use to log into the MyMazda mobile app.",
+ "title": "Mazda Connected Services - Add Account"
+ }
+ }
+ },
+ "title": "Mazda Connected Services"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/bg.json b/homeassistant/components/mazda/translations/bg.json
new file mode 100644
index 00000000000000..6f3c5a54f3f3b8
--- /dev/null
+++ b/homeassistant/components/mazda/translations/bg.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "error": {
+ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u0430",
+ "region": "\u0420\u0435\u0433\u0438\u043e\u043d"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u0430",
+ "region": "\u0420\u0435\u0433\u0438\u043e\u043d"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/ca.json b/homeassistant/components/mazda/translations/ca.json
new file mode 100644
index 00000000000000..d45b9177c3f1d8
--- /dev/null
+++ b/homeassistant/components/mazda/translations/ca.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El compte ja ha estat configurat",
+ "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament"
+ },
+ "error": {
+ "account_locked": "Compte bloquejat. Intenta-ho m\u00e9s tard.",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "email": "Correu electr\u00f2nic",
+ "password": "Contrasenya",
+ "region": "Regi\u00f3"
+ },
+ "description": "Ha fallat l'autenticaci\u00f3 dels Serveis connectats de Mazda. Introdueix les teves credencials actuals.",
+ "title": "Serveis connectats de Mazda - Ha fallat l'autenticaci\u00f3"
+ },
+ "user": {
+ "data": {
+ "email": "Correu electr\u00f2nic",
+ "password": "Contrasenya",
+ "region": "Regi\u00f3"
+ },
+ "description": "Introdueix el correu electr\u00f2nic i la contrasenya que utilitzes per iniciar sessi\u00f3 a l'aplicaci\u00f3 de m\u00f2bil MyMazda.",
+ "title": "Serveis connectats de Mazda - Afegeix un compte"
+ }
+ }
+ },
+ "title": "Serveis connectats de Mazda"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/cs.json b/homeassistant/components/mazda/translations/cs.json
new file mode 100644
index 00000000000000..89fde600735578
--- /dev/null
+++ b/homeassistant/components/mazda/translations/cs.json
@@ -0,0 +1,29 @@
+{
+ "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": {
+ "data": {
+ "email": "E-mail",
+ "password": "Heslo",
+ "region": "Region"
+ }
+ },
+ "user": {
+ "data": {
+ "email": "E-mail",
+ "password": "Heslo",
+ "region": "Region"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/de.json b/homeassistant/components/mazda/translations/de.json
new file mode 100644
index 00000000000000..9050ee9f00cf04
--- /dev/null
+++ b/homeassistant/components/mazda/translations/de.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
+ },
+ "error": {
+ "account_locked": "Konto gesperrt. Bitte versuchen Sie es sp\u00e4ter erneut.",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "email": "E-Mail",
+ "password": "Passwort",
+ "region": "Region"
+ },
+ "description": "Die Authentifizierung f\u00fcr Mazda Connected Services ist fehlgeschlagen. Bitte geben Sie Ihre aktuellen Anmeldedaten ein.",
+ "title": "Mazda Connected Services - Authentifizierung fehlgeschlagen"
+ },
+ "user": {
+ "data": {
+ "email": "E-Mail",
+ "password": "Passwort",
+ "region": "Region"
+ },
+ "description": "Bitte geben Sie die E-Mail-Adresse und das Passwort ein, die Sie f\u00fcr die Anmeldung bei der MyMazda Mobile App verwenden.",
+ "title": "Mazda Connected Services - Konto hinzuf\u00fcgen"
+ }
+ }
+ },
+ "title": "Mazda Connected Services"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/en.json b/homeassistant/components/mazda/translations/en.json
new file mode 100644
index 00000000000000..b9e02fb3a412f8
--- /dev/null
+++ b/homeassistant/components/mazda/translations/en.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Account is already configured",
+ "reauth_successful": "Re-authentication was successful"
+ },
+ "error": {
+ "account_locked": "Account locked. Please try again later.",
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "email": "Email",
+ "password": "Password",
+ "region": "Region"
+ },
+ "description": "Authentication failed for Mazda Connected Services. Please enter your current credentials.",
+ "title": "Mazda Connected Services - Authentication Failed"
+ },
+ "user": {
+ "data": {
+ "email": "Email",
+ "password": "Password",
+ "region": "Region"
+ },
+ "description": "Please enter the email address and password you use to log into the MyMazda mobile app.",
+ "title": "Mazda Connected Services - Add Account"
+ }
+ }
+ },
+ "title": "Mazda Connected Services"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/es.json b/homeassistant/components/mazda/translations/es.json
new file mode 100644
index 00000000000000..868ae0d770ea6c
--- /dev/null
+++ b/homeassistant/components/mazda/translations/es.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "error": {
+ "account_locked": "Cuenta bloqueada. Por favor, int\u00e9ntelo de nuevo m\u00e1s tarde."
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Contrase\u00f1a",
+ "region": "Regi\u00f3n"
+ },
+ "description": "Ha fallado la autenticaci\u00f3n para los Servicios Conectados de Mazda. Por favor, introduce tus credenciales actuales.",
+ "title": "Servicios Conectados de Mazda - Fallo de autenticaci\u00f3n"
+ },
+ "user": {
+ "data": {
+ "email": "Correo electronico",
+ "password": "Contrase\u00f1a",
+ "region": "Regi\u00f3n"
+ },
+ "description": "Introduce la direcci\u00f3n de correo electr\u00f3nico y la contrase\u00f1a que utilizas para iniciar sesi\u00f3n en la aplicaci\u00f3n m\u00f3vil MyMazda.",
+ "title": "Servicios Conectados de Mazda - A\u00f1adir cuenta"
+ }
+ }
+ },
+ "title": "Servicios Conectados de Mazda"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/et.json b/homeassistant/components/mazda/translations/et.json
new file mode 100644
index 00000000000000..4ce2e2fa5f373f
--- /dev/null
+++ b/homeassistant/components/mazda/translations/et.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kasutaja on juba seadistatud",
+ "reauth_successful": "Taastuvastamine \u00f5nnestus"
+ },
+ "error": {
+ "account_locked": "Konto on lukus. Proovi hiljem uuesti.",
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_auth": "Vigane autentimine",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "email": "E-posti aadress",
+ "password": "Salas\u00f5na",
+ "region": "Piirkond"
+ },
+ "description": "Mazda Connected Services tuvastamine nurjus. Sisesta oma kehtivad andmed.",
+ "title": "Mazda Connected Services - tuvastamine nurjus"
+ },
+ "user": {
+ "data": {
+ "email": "E-posti aadress",
+ "password": "Salas\u00f5na",
+ "region": "Piirkond"
+ },
+ "description": "Sisesta e-posti aadress ja salas\u00f5na mida kasutad MyMazda mobiilirakendusse sisselogimiseks.",
+ "title": "Mazda Connected Services - lisa konto"
+ }
+ }
+ },
+ "title": "Mazda Connected Services"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/fr.json b/homeassistant/components/mazda/translations/fr.json
new file mode 100644
index 00000000000000..aa1ea252c0cd4a
--- /dev/null
+++ b/homeassistant/components/mazda/translations/fr.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Le compte est d\u00e9ja configur\u00e9",
+ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi"
+ },
+ "error": {
+ "account_locked": "Compte bloqu\u00e9. Veuillez r\u00e9essayer plus tard.",
+ "cannot_connect": "Echec de la connexion",
+ "invalid_auth": "Authentification invalide",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "email": "Email",
+ "password": "Mot de passe",
+ "region": "R\u00e9gion"
+ },
+ "description": "L'authentification a \u00e9chou\u00e9 pour les services connect\u00e9s Mazda. Veuillez saisir vos informations d'identification actuelles.",
+ "title": "Services connect\u00e9s Mazda - \u00c9chec de l'authentification"
+ },
+ "user": {
+ "data": {
+ "email": "Email",
+ "password": "Mot de passe",
+ "region": "R\u00e9gion"
+ },
+ "description": "Veuillez saisir l'adresse e-mail et le mot de passe que vous utilisez pour vous connecter \u00e0 l'application mobile MyMazda.",
+ "title": "Services connect\u00e9s Mazda - Ajouter un compte"
+ }
+ }
+ },
+ "title": "Services connect\u00e9s Mazda"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/hu.json b/homeassistant/components/mazda/translations/hu.json
new file mode 100644
index 00000000000000..1b9c6893ed547a
--- /dev/null
+++ b/homeassistant/components/mazda/translations/hu.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
+ },
+ "error": {
+ "account_locked": "Fi\u00f3k z\u00e1rolva. K\u00e9rlek, pr\u00f3b\u00e1ld \u00fajra k\u00e9s\u0151bb.",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "email": "E-mail",
+ "password": "Jelsz\u00f3",
+ "region": "R\u00e9gi\u00f3"
+ },
+ "title": "Mazda Connected Services - A hiteles\u00edt\u00e9s sikertelen"
+ },
+ "user": {
+ "data": {
+ "email": "E-mail",
+ "password": "Jelsz\u00f3",
+ "region": "R\u00e9gi\u00f3"
+ },
+ "title": "Mazda Connected Services - Fi\u00f3k hozz\u00e1ad\u00e1sa"
+ }
+ }
+ },
+ "title": "Mazda Connected Services"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/id.json b/homeassistant/components/mazda/translations/id.json
new file mode 100644
index 00000000000000..0a6e81e8454718
--- /dev/null
+++ b/homeassistant/components/mazda/translations/id.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi",
+ "reauth_successful": "Autentikasi ulang berhasil"
+ },
+ "error": {
+ "account_locked": "Akun terkunci. Coba lagi nanti.",
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "email": "Email",
+ "password": "Kata Sandi",
+ "region": "Wilayah"
+ },
+ "description": "Autentikasi gagal untuk Mazda Connected Services. Masukkan kredensial Anda saat ini.",
+ "title": "Mazda Connected Services - Autentikasi Gagal"
+ },
+ "user": {
+ "data": {
+ "email": "Email",
+ "password": "Kata Sandi",
+ "region": "Wilayah"
+ },
+ "description": "Masukkan alamat email dan kata sandi yang digunakan untuk masuk ke aplikasi seluler MyMazda.",
+ "title": "Mazda Connected Services - Tambahkan Akun"
+ }
+ }
+ },
+ "title": "Mazda Connected Services"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/it.json b/homeassistant/components/mazda/translations/it.json
new file mode 100644
index 00000000000000..d5a2796ed1845c
--- /dev/null
+++ b/homeassistant/components/mazda/translations/it.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato",
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
+ },
+ "error": {
+ "account_locked": "Account bloccato. Per favore riprova pi\u00f9 tardi.",
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "email": "E-mail",
+ "password": "Password",
+ "region": "Area geografica"
+ },
+ "description": "Autenticazione non riuscita per Mazda Connected Services. Inserisci le tue credenziali attuali.",
+ "title": "Mazda Connected Services - Autenticazione non riuscita"
+ },
+ "user": {
+ "data": {
+ "email": "E-mail",
+ "password": "Password",
+ "region": "Area geografica"
+ },
+ "description": "Inserisci l'indirizzo e-mail e la password che utilizzi per accedere all'app mobile MyMazda.",
+ "title": "Mazda Connected Services - Aggiungi account"
+ }
+ }
+ },
+ "title": "Mazda Connected Services"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/ko.json b/homeassistant/components/mazda/translations/ko.json
new file mode 100644
index 00000000000000..aa9dcf99d14499
--- /dev/null
+++ b/homeassistant/components/mazda/translations/ko.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "account_locked": "\uacc4\uc815\uc774 \uc7a0\uacbc\uc2b5\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574 \uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "email": "\uc774\uba54\uc77c",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "region": "\uc9c0\uc5ed"
+ },
+ "description": "Mazda Connected Services\uc5d0 \ub300\ud55c \uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \ud604\uc7ac \uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "Mazda Connected Services - \uc778\uc99d \uc2e4\ud328"
+ },
+ "user": {
+ "data": {
+ "email": "\uc774\uba54\uc77c",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "region": "\uc9c0\uc5ed"
+ },
+ "description": "MyMazda \ubaa8\ubc14\uc77c \uc571\uc5d0 \ub85c\uadf8\uc778\ud558\uae30 \uc704\ud574 \uc0ac\uc6a9\ud558\ub294 \uc774\uba54\uc77c \uc8fc\uc18c\uc640 \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "Mazda Connected Services - \uacc4\uc815 \ucd94\uac00\ud558\uae30"
+ }
+ }
+ },
+ "title": "Mazda Connected Services"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/nl.json b/homeassistant/components/mazda/translations/nl.json
new file mode 100644
index 00000000000000..64975c9b14b4f5
--- /dev/null
+++ b/homeassistant/components/mazda/translations/nl.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Account is al geconfigureerd",
+ "reauth_successful": "Herauthenticatie was succesvol"
+ },
+ "error": {
+ "account_locked": "Account vergrendeld. Probeer het later nog eens.",
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "email": "E-mail",
+ "password": "Wachtwoord",
+ "region": "Regio"
+ },
+ "description": "Verificatie mislukt voor Mazda Connected Services. Voer uw huidige inloggegevens in.",
+ "title": "Mazda Connected Services - Authenticatie mislukt"
+ },
+ "user": {
+ "data": {
+ "email": "E-mail",
+ "password": "Wachtwoord",
+ "region": "Regio"
+ },
+ "description": "Voer het e-mailadres en wachtwoord in dat u gebruikt om in te loggen op de MyMazda mobiele app.",
+ "title": "Mazda Connected Services - Account toevoegen"
+ }
+ }
+ },
+ "title": "Mazda Connected Services"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/no.json b/homeassistant/components/mazda/translations/no.json
new file mode 100644
index 00000000000000..e3a05de51f9e63
--- /dev/null
+++ b/homeassistant/components/mazda/translations/no.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kontoen er allerede konfigurert",
+ "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket"
+ },
+ "error": {
+ "account_locked": "Kontoen er l\u00e5st. Pr\u00f8v igjen senere.",
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_auth": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "email": "E-post",
+ "password": "Passord",
+ "region": "Region"
+ },
+ "description": "Autentisering mislyktes for Mazda Connected Services. Vennligst skriv inn din n\u00e5v\u00e6rende legitimasjon.",
+ "title": "Mazda Connected Services - Autentisering mislyktes"
+ },
+ "user": {
+ "data": {
+ "email": "E-post",
+ "password": "Passord",
+ "region": "Region"
+ },
+ "description": "Vennligst skriv inn e-postadressen og passordet du bruker for \u00e5 logge p\u00e5 MyMazda-mobilappen.",
+ "title": "Mazda Connected Services - Legg til konto"
+ }
+ }
+ },
+ "title": "Mazda Connected Services"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/pl.json b/homeassistant/components/mazda/translations/pl.json
new file mode 100644
index 00000000000000..12254f20662653
--- /dev/null
+++ b/homeassistant/components/mazda/translations/pl.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto jest ju\u017c skonfigurowane",
+ "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119"
+ },
+ "error": {
+ "account_locked": "Konto zablokowane. Spr\u00f3buj ponownie p\u00f3\u017aniej.",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "email": "Adres e-mail",
+ "password": "Has\u0142o",
+ "region": "Region"
+ },
+ "description": "Uwierzytelnianie dla Mazda Connected Services nie powiod\u0142o si\u0119. Wprowad\u017a aktualne dane uwierzytelniaj\u0105ce.",
+ "title": "Mazda Connected Services - Uwierzytelnianie nie powiod\u0142o si\u0119"
+ },
+ "user": {
+ "data": {
+ "email": "Adres e-mail",
+ "password": "Has\u0142o",
+ "region": "Region"
+ },
+ "description": "Wprowad\u017a adres e-mail i has\u0142o, kt\u00f3rych u\u017cywasz do logowania si\u0119 do aplikacji mobilnej MyMazda.",
+ "title": "Mazda Connected Services - Dodawanie konta"
+ }
+ }
+ },
+ "title": "Mazda Connected Services"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/ru.json b/homeassistant/components/mazda/translations/ru.json
new file mode 100644
index 00000000000000..be3f861d4066a5
--- /dev/null
+++ b/homeassistant/components/mazda/translations/ru.json
@@ -0,0 +1,35 @@
+{
+ "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.",
+ "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": {
+ "account_locked": "\u0410\u043a\u043a\u0430\u0443\u043d\u0442 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d. \u041f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "region": "\u0420\u0435\u0433\u0438\u043e\u043d"
+ },
+ "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. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0442\u0435\u043a\u0443\u0449\u0438\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
+ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f"
+ },
+ "user": {
+ "data": {
+ "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "region": "\u0420\u0435\u0433\u0438\u043e\u043d"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u0432 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 MyMazda.",
+ "title": "Mazda Connected Services"
+ }
+ }
+ },
+ "title": "Mazda Connected Services"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/zh-Hant.json b/homeassistant/components/mazda/translations/zh-Hant.json
new file mode 100644
index 00000000000000..48232664683c30
--- /dev/null
+++ b/homeassistant/components/mazda/translations/zh-Hant.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f"
+ },
+ "error": {
+ "account_locked": "\u5e33\u865f\u5df2\u9396\u5b9a\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66\u3002",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "email": "\u96fb\u5b50\u90f5\u4ef6",
+ "password": "\u5bc6\u78bc",
+ "region": "\u5340\u57df"
+ },
+ "description": "Mazda Connected \u670d\u52d9\u8a8d\u8b49\u5931\u6557\u3002\u8acb\u8f38\u5165\u76ee\u524d\u6191\u8b49\u3002",
+ "title": "Mazda Connected \u670d\u52d9 - \u8a8d\u8b49\u5931\u6557"
+ },
+ "user": {
+ "data": {
+ "email": "\u96fb\u5b50\u90f5\u4ef6",
+ "password": "\u5bc6\u78bc",
+ "region": "\u5340\u57df"
+ },
+ "description": "\u8acb\u8f38\u5165\u767b\u5165MyMazda \u884c\u52d5 App \u4e4b Email \u5730\u5740\u8207\u5bc6\u78bc\u3002",
+ "title": "Mazda Connected \u670d\u52d9 - \u65b0\u589e\u5e33\u865f"
+ }
+ }
+ },
+ "title": "Mazda Connected \u670d\u52d9"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mcp23017/binary_sensor.py b/homeassistant/components/mcp23017/binary_sensor.py
index f6dafad43acbf7..c650393a26f3de 100644
--- a/homeassistant/components/mcp23017/binary_sensor.py
+++ b/homeassistant/components/mcp23017/binary_sensor.py
@@ -1,8 +1,8 @@
"""Support for binary sensor using I2C MCP23017 chip."""
-from adafruit_mcp230xx.mcp23017 import MCP23017 # pylint: disable=import-error
-import board # pylint: disable=import-error
-import busio # pylint: disable=import-error
-import digitalio # pylint: disable=import-error
+from adafruit_mcp230xx.mcp23017 import MCP23017
+import board
+import busio
+import digitalio
import voluptuous as vol
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json
index aeda638710e364..7460529f8fee06 100644
--- a/homeassistant/components/mcp23017/manifest.json
+++ b/homeassistant/components/mcp23017/manifest.json
@@ -3,7 +3,7 @@
"name": "MCP23017 I/O Expander",
"documentation": "https://www.home-assistant.io/integrations/mcp23017",
"requirements": [
- "RPi.GPIO==0.7.0",
+ "RPi.GPIO==0.7.1a4",
"adafruit-circuitpython-mcp230xx==2.2.2"
],
"codeowners": ["@jardiamj"]
diff --git a/homeassistant/components/mcp23017/switch.py b/homeassistant/components/mcp23017/switch.py
index d22593a4c3ef84..6b1ced540aeaa2 100644
--- a/homeassistant/components/mcp23017/switch.py
+++ b/homeassistant/components/mcp23017/switch.py
@@ -1,8 +1,8 @@
"""Support for switch sensor using I2C MCP23017 chip."""
-from adafruit_mcp230xx.mcp23017 import MCP23017 # pylint: disable=import-error
-import board # pylint: disable=import-error
-import busio # pylint: disable=import-error
-import digitalio # pylint: disable=import-error
+from adafruit_mcp230xx.mcp23017 import MCP23017
+import board
+import busio
+import digitalio
import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA
diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json
index 5a09171df80c2a..35a5b0981843b0 100644
--- a/homeassistant/components/media_extractor/manifest.json
+++ b/homeassistant/components/media_extractor/manifest.json
@@ -2,7 +2,7 @@
"domain": "media_extractor",
"name": "Media Extractor",
"documentation": "https://www.home-assistant.io/integrations/media_extractor",
- "requirements": ["youtube_dl==2021.01.16"],
+ "requirements": ["youtube_dl==2021.03.14"],
"dependencies": ["media_player"],
"codeowners": [],
"quality_scale": "internal"
diff --git a/homeassistant/components/media_extractor/services.yaml b/homeassistant/components/media_extractor/services.yaml
index 17abffee89d31a..1e58c19baf1beb 100644
--- a/homeassistant/components/media_extractor/services.yaml
+++ b/homeassistant/components/media_extractor/services.yaml
@@ -1,5 +1,5 @@
play_media:
- description: Downloads file from given url.
+ description: Downloads file from given URL.
fields:
entity_id:
description: Name(s) of entities to play media on.
diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py
index d670acb7af98b5..f98c6eeceafdfc 100644
--- a/homeassistant/components/media_player/__init__.py
+++ b/homeassistant/components/media_player/__init__.py
@@ -1,13 +1,16 @@
"""Component to interface with various media players."""
+from __future__ import annotations
+
import asyncio
import base64
import collections
+from contextlib import suppress
from datetime import timedelta
import functools as ft
import hashlib
import logging
import secrets
-from typing import List, Optional, Tuple
+from typing import final
from urllib.parse import urlparse
from aiohttp import web
@@ -63,6 +66,7 @@
from .const import (
ATTR_APP_ID,
ATTR_APP_NAME,
+ ATTR_GROUP_MEMBERS,
ATTR_INPUT_SOURCE,
ATTR_INPUT_SOURCE_LIST,
ATTR_MEDIA_ALBUM_ARTIST,
@@ -93,11 +97,14 @@
MEDIA_CLASS_DIRECTORY,
REPEAT_MODES,
SERVICE_CLEAR_PLAYLIST,
+ SERVICE_JOIN,
SERVICE_PLAY_MEDIA,
SERVICE_SELECT_SOUND_MODE,
SERVICE_SELECT_SOURCE,
+ SERVICE_UNJOIN,
SUPPORT_BROWSE_MEDIA,
SUPPORT_CLEAR_PLAYLIST,
+ SUPPORT_GROUPING,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY,
@@ -298,6 +305,12 @@ async def async_setup(hass, config):
"async_media_seek",
[SUPPORT_SEEK],
)
+ component.async_register_entity_service(
+ SERVICE_JOIN,
+ {vol.Required(ATTR_GROUP_MEMBERS): list},
+ "async_join_players",
+ [SUPPORT_GROUPING],
+ )
component.async_register_entity_service(
SERVICE_SELECT_SOURCE,
{vol.Required(ATTR_INPUT_SOURCE): cv.string},
@@ -329,6 +342,9 @@ async def async_setup(hass, config):
"async_set_shuffle",
[SUPPORT_SHUFFLE_SET],
)
+ component.async_register_entity_service(
+ SERVICE_UNJOIN, {}, "async_unjoin_player", [SUPPORT_GROUPING]
+ )
component.async_register_entity_service(
SERVICE_REPEAT_SET,
@@ -353,7 +369,7 @@ async def async_unload_entry(hass, entry):
class MediaPlayerEntity(Entity):
"""ABC for media player entities."""
- _access_token: Optional[str] = None
+ _access_token: str | None = None
# Implement these for your media player
@property
@@ -437,8 +453,8 @@ async def async_get_browse_image(
self,
media_content_type: str,
media_content_id: str,
- media_image_id: Optional[str] = None,
- ) -> Tuple[Optional[str], Optional[str]]:
+ media_image_id: str | None = None,
+ ) -> tuple[str | None, str | None]:
"""
Optionally fetch internally accessible image for media browser.
@@ -536,6 +552,11 @@ def repeat(self):
"""Return current repeat mode."""
return None
+ @property
+ def group_members(self):
+ """List of members which are currently grouped together."""
+ return None
+
@property
def supported_features(self):
"""Flag media player features that are supported."""
@@ -737,6 +758,11 @@ def support_shuffle_set(self):
"""Boolean if shuffle is supported."""
return bool(self.supported_features & SUPPORT_SHUFFLE_SET)
+ @property
+ def support_grouping(self):
+ """Boolean if player grouping is supported."""
+ return bool(self.supported_features & SUPPORT_GROUPING)
+
async def async_toggle(self):
"""Toggle the power on the media player."""
if hasattr(self, "toggle"):
@@ -829,6 +855,7 @@ def capability_attributes(self):
return data
+ @final
@property
def state_attributes(self):
"""Return the state attributes."""
@@ -845,13 +872,16 @@ def state_attributes(self):
if self.media_image_remotely_accessible:
state_attr["entity_picture_local"] = self.media_image_local
+ if self.support_grouping:
+ state_attr[ATTR_GROUP_MEMBERS] = self.group_members
+
return state_attr
async def async_browse_media(
self,
- media_content_type: Optional[str] = None,
- media_content_id: Optional[str] = None,
- ) -> "BrowseMedia":
+ media_content_type: str | None = None,
+ media_content_id: str | None = None,
+ ) -> BrowseMedia:
"""Return a BrowseMedia instance.
The BrowseMedia instance will be used by the
@@ -859,6 +889,22 @@ async def async_browse_media(
"""
raise NotImplementedError()
+ def join_players(self, group_members):
+ """Join `group_members` as a player group with the current player."""
+ raise NotImplementedError()
+
+ async def async_join_players(self, group_members):
+ """Join `group_members` as a player group with the current player."""
+ await self.hass.async_add_executor_job(self.join_players, group_members)
+
+ def unjoin_player(self):
+ """Remove this player from any group."""
+ raise NotImplementedError()
+
+ async def async_unjoin_player(self):
+ """Remove this player from any group."""
+ await self.hass.async_add_executor_job(self.unjoin_player)
+
async def _async_fetch_image_from_cache(self, url):
"""Fetch image.
@@ -890,18 +936,13 @@ async def _async_fetch_image(self, url):
"""Retrieve an image."""
content, content_type = (None, None)
websession = async_get_clientsession(self.hass)
- try:
- with async_timeout.timeout(10):
- response = await websession.get(url)
-
- if response.status == HTTP_OK:
- content = await response.read()
- content_type = response.headers.get(CONTENT_TYPE)
- if content_type:
- content_type = content_type.split(";")[0]
-
- except asyncio.TimeoutError:
- pass
+ with suppress(asyncio.TimeoutError), async_timeout.timeout(10):
+ response = await websession.get(url)
+ if response.status == HTTP_OK:
+ content = await response.read()
+ content_type = response.headers.get(CONTENT_TYPE)
+ if content_type:
+ content_type = content_type.split(";")[0]
if content is None:
_LOGGER.warning("Error retrieving proxied image from %s", url)
@@ -912,7 +953,7 @@ def get_browse_image_url(
self,
media_content_type: str,
media_content_id: str,
- media_image_id: Optional[str] = None,
+ media_image_id: str | None = None,
) -> str:
"""Generate an url for a media browser image."""
url_path = (
@@ -945,8 +986,8 @@ async def get(
self,
request: web.Request,
entity_id: str,
- media_content_type: Optional[str] = None,
- media_content_id: Optional[str] = None,
+ media_content_type: str | None = None,
+ media_content_id: str | None = None,
) -> web.Response:
"""Start a get request."""
player = self.component.get_entity(entity_id)
@@ -1045,7 +1086,7 @@ async def websocket_browse_media(hass, connection, msg):
To use, media_player integrations can implement MediaPlayerEntity.async_browse_media()
"""
component = hass.data[DOMAIN]
- player: Optional[MediaPlayerDevice] = component.get_entity(msg["entity_id"])
+ player: MediaPlayerDevice | None = component.get_entity(msg["entity_id"])
if player is None:
connection.send_error(msg["id"], "entity_not_found", "Entity not found")
@@ -1117,9 +1158,9 @@ def __init__(
title: str,
can_play: bool,
can_expand: bool,
- children: Optional[List["BrowseMedia"]] = None,
- children_media_class: Optional[str] = None,
- thumbnail: Optional[str] = None,
+ children: list[BrowseMedia] | None = None,
+ children_media_class: str | None = None,
+ thumbnail: str | None = None,
):
"""Initialize browse media item."""
self.media_class = media_class
diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py
index 87ccca75d36e18..67f4331aa60c43 100644
--- a/homeassistant/components/media_player/const.py
+++ b/homeassistant/components/media_player/const.py
@@ -2,6 +2,7 @@
ATTR_APP_ID = "app_id"
ATTR_APP_NAME = "app_name"
+ATTR_GROUP_MEMBERS = "group_members"
ATTR_INPUT_SOURCE = "source"
ATTR_INPUT_SOURCE_LIST = "source_list"
ATTR_MEDIA_ALBUM_ARTIST = "media_album_artist"
@@ -75,9 +76,11 @@
MEDIA_TYPE_VIDEO = "video"
SERVICE_CLEAR_PLAYLIST = "clear_playlist"
+SERVICE_JOIN = "join"
SERVICE_PLAY_MEDIA = "play_media"
SERVICE_SELECT_SOUND_MODE = "select_sound_mode"
SERVICE_SELECT_SOURCE = "select_source"
+SERVICE_UNJOIN = "unjoin"
REPEAT_MODE_ALL = "all"
REPEAT_MODE_OFF = "off"
@@ -103,3 +106,4 @@
SUPPORT_SELECT_SOUND_MODE = 65536
SUPPORT_BROWSE_MEDIA = 131072
SUPPORT_REPEAT_SET = 262144
+SUPPORT_GROUPING = 524288
diff --git a/homeassistant/components/media_player/device_condition.py b/homeassistant/components/media_player/device_condition.py
index 6faa6521b707b3..0e6e0f96c403b3 100644
--- a/homeassistant/components/media_player/device_condition.py
+++ b/homeassistant/components/media_player/device_condition.py
@@ -1,5 +1,5 @@
"""Provides device automations for Media player."""
-from typing import Dict, List
+from __future__ import annotations
import voluptuous as vol
@@ -35,7 +35,7 @@
async def async_get_conditions(
hass: HomeAssistant, device_id: str
-) -> List[Dict[str, str]]:
+) -> list[dict[str, str]]:
"""List device conditions for Media player devices."""
registry = await entity_registry.async_get_registry(hass)
conditions = []
diff --git a/homeassistant/components/media_player/device_trigger.py b/homeassistant/components/media_player/device_trigger.py
new file mode 100644
index 00000000000000..889bc776962b7e
--- /dev/null
+++ b/homeassistant/components/media_player/device_trigger.py
@@ -0,0 +1,101 @@
+"""Provides device automations for Media player."""
+from __future__ import annotations
+
+import voluptuous as vol
+
+from homeassistant.components.automation import AutomationActionType
+from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.homeassistant.triggers import state as state_trigger
+from homeassistant.const import (
+ CONF_DEVICE_ID,
+ CONF_DOMAIN,
+ CONF_ENTITY_ID,
+ CONF_FOR,
+ CONF_PLATFORM,
+ CONF_TYPE,
+ STATE_IDLE,
+ STATE_OFF,
+ STATE_ON,
+ STATE_PAUSED,
+ STATE_PLAYING,
+)
+from homeassistant.core import CALLBACK_TYPE, HomeAssistant
+from homeassistant.helpers import config_validation as cv, entity_registry
+from homeassistant.helpers.typing import ConfigType
+
+from . import DOMAIN
+
+TRIGGER_TYPES = {"turned_on", "turned_off", "idle", "paused", "playing"}
+
+TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_ENTITY_ID): cv.entity_id,
+ vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
+ vol.Optional(CONF_FOR): cv.positive_time_period_dict,
+ }
+)
+
+
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
+ """List device triggers for Media player entities."""
+ registry = await entity_registry.async_get_registry(hass)
+ triggers = []
+
+ # Get all the integration entities for this device
+ for entry in entity_registry.async_entries_for_device(registry, device_id):
+ if entry.domain != DOMAIN:
+ continue
+
+ # Add triggers for each entity that belongs to this integration
+ triggers += [
+ {
+ CONF_PLATFORM: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: trigger,
+ }
+ for trigger in TRIGGER_TYPES
+ ]
+
+ return triggers
+
+
+async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict:
+ """List trigger capabilities."""
+ return {
+ "extra_fields": vol.Schema(
+ {vol.Optional(CONF_FOR): cv.positive_time_period_dict}
+ )
+ }
+
+
+async def async_attach_trigger(
+ hass: HomeAssistant,
+ config: ConfigType,
+ action: AutomationActionType,
+ automation_info: dict,
+) -> CALLBACK_TYPE:
+ """Attach a trigger."""
+ if config[CONF_TYPE] == "turned_on":
+ to_state = STATE_ON
+ elif config[CONF_TYPE] == "turned_off":
+ to_state = STATE_OFF
+ elif config[CONF_TYPE] == "idle":
+ to_state = STATE_IDLE
+ elif config[CONF_TYPE] == "paused":
+ to_state = STATE_PAUSED
+ else:
+ to_state = STATE_PLAYING
+
+ state_config = {
+ CONF_PLATFORM: "state",
+ CONF_ENTITY_ID: config[CONF_ENTITY_ID],
+ state_trigger.CONF_TO: to_state,
+ }
+ if CONF_FOR in config:
+ state_config[CONF_FOR] = config[CONF_FOR]
+ state_config = state_trigger.TRIGGER_SCHEMA(state_config)
+ return await state_trigger.async_attach_trigger(
+ hass, state_config, action, automation_info, platform_type="device"
+ )
diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py
index 64955d1913ba30..1707109197f83f 100644
--- a/homeassistant/components/media_player/reproduce_state.py
+++ b/homeassistant/components/media_player/reproduce_state.py
@@ -1,6 +1,8 @@
"""Module that groups code required to handle state restore for component."""
+from __future__ import annotations
+
import asyncio
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import (
SERVICE_MEDIA_PAUSE,
@@ -40,8 +42,8 @@ async def _async_reproduce_states(
hass: HomeAssistantType,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce component states."""
@@ -104,8 +106,8 @@ async def async_reproduce_states(
hass: HomeAssistantType,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce component states."""
await asyncio.gather(
diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml
index 08637df0745f06..e2a260dc80f556 100644
--- a/homeassistant/components/media_player/services.yaml
+++ b/homeassistant/components/media_player/services.yaml
@@ -1,168 +1,207 @@
# Describes the format for available media player services
turn_on:
+ name: Turn on
description: Turn a media player power on.
- fields:
- entity_id:
- description: Name(s) of entities to turn on.
- example: "media_player.living_room_chromecast"
+ target:
turn_off:
+ name: Turn off
description: Turn a media player power off.
- fields:
- entity_id:
- description: Name(s) of entities to turn off.
- example: "media_player.living_room_chromecast"
+ target:
toggle:
+ name: Toggle
description: Toggles a media player power state.
- fields:
- entity_id:
- description: Name(s) of entities to toggle.
- example: "media_player.living_room_chromecast"
+ target:
volume_up:
+ name: Turn up volume
description: Turn a media player volume up.
- fields:
- entity_id:
- description: Name(s) of entities to turn volume up on.
- example: "media_player.living_room_sonos"
+ target:
volume_down:
+ name: Turn down volume
description: Turn a media player volume down.
- fields:
- entity_id:
- description: Name(s) of entities to turn volume down on.
- example: "media_player.living_room_sonos"
+ target:
volume_mute:
+ name: Mute volume
description: Mute a media player's volume.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to mute.
- example: "media_player.living_room_sonos"
is_volume_muted:
+ name: Muted
description: True/false for mute/unmute.
+ required: true
example: true
+ selector:
+ boolean:
volume_set:
+ name: Set volume
description: Set a media player's volume level.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to set volume level on.
- example: "media_player.living_room_sonos"
volume_level:
+ name: Level
description: Volume level to set as float.
+ required: true
example: 0.6
+ selector:
+ number:
+ min: 0
+ max: 1
+ step: 0.01
+ mode: slider
media_play_pause:
+ name: Play/Pause
description: Toggle media player play/pause state.
- fields:
- entity_id:
- description: Name(s) of entities to toggle play/pause state on.
- example: "media_player.living_room_sonos"
+ target:
media_play:
+ name: Play
description: Send the media player the command for play.
- fields:
- entity_id:
- description: Name(s) of entities to play on.
- example: "media_player.living_room_sonos"
+ target:
media_pause:
+ name: Pause
description: Send the media player the command for pause.
- fields:
- entity_id:
- description: Name(s) of entities to pause on.
- example: "media_player.living_room_sonos"
+ target:
media_stop:
+ name: Stop
description: Send the media player the stop command.
- fields:
- entity_id:
- description: Name(s) of entities to stop on.
- example: "media_player.living_room_sonos"
+ target:
media_next_track:
+ name: Next
description: Send the media player the command for next track.
- fields:
- entity_id:
- description: Name(s) of entities to send next track command to.
- example: "media_player.living_room_sonos"
+ target:
media_previous_track:
+ name: Previous
description: Send the media player the command for previous track.
- fields:
- entity_id:
- description: Name(s) of entities to send previous track command to.
- example: "media_player.living_room_sonos"
+ target:
media_seek:
- description: Send the media player the command to seek in current playing media.
+ name: Seek
+ description:
+ Send the media player the command to seek in current playing media.
fields:
- entity_id:
- description: Name(s) of entities to seek media on.
- example: "media_player.living_room_chromecast"
seek_position:
+ name: Position
description: Position to seek to. The format is platform dependent.
+ required: true
example: 100
+ selector:
+ number:
+ min: 0
+ max: 9223372036854775807
+ step: 0.01
+ mode: box
play_media:
+ name: Play media
description: Send the media player the command for playing media.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to seek media on
- example: "media_player.living_room_chromecast"
media_content_id:
+ name: Content ID
description: The ID of the content to play. Platform dependent.
+ required: true
example: "https://home-assistant.io/images/cast/splash.png"
+ selector:
+ text:
+
media_content_type:
- description: The type of the content to play. Must be one of image, music, tvshow, video, episode, channel or playlist
+ name: Content type
+ description:
+ The type of the content to play. Like image, music, tvshow, video,
+ episode, channel or playlist.
+ required: true
example: "music"
+ selector:
+ text:
select_source:
+ name: Select source
description: Send the media player the command to change input source.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to change source on.
- example: "media_player.txnr535_0009b0d81f82"
source:
+ name: Source
description: Name of the source to switch to. Platform dependent.
+ required: true
example: "video1"
+ selector:
+ text:
select_sound_mode:
+ name: Select sound mode
description: Send the media player the command to change sound mode.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to change sound mode on.
- example: "media_player.marantz"
sound_mode:
+ name: Sound mode
description: Name of the sound mode to switch to.
example: "Music"
+ selector:
+ text:
clear_playlist:
+ name: Clear playlist
description: Send the media player the command to clear players playlist.
- fields:
- entity_id:
- description: Name(s) of entities to change source on.
- example: "media_player.living_room_chromecast"
+ target:
shuffle_set:
+ name: Shuffle
description: Set shuffling state.
+ target:
fields:
- entity_id:
- description: Name(s) of entities to set.
- example: "media_player.spotify"
shuffle:
+ name: Shuffle
description: True/false for enabling/disabling shuffle.
+ required: true
example: true
+ selector:
+ boolean:
repeat_set:
- description: Set repeat mode.
+ name: Repeat
+ description: Set repeat mode
+ target:
fields:
- entity_id:
- description: Name(s) of entities to set.
- example: "media_player.sonos"
repeat:
+ name: Repeat mode
description: Repeat mode to set (off, all, one).
+ required: true
example: "off"
+ selector:
+ select:
+ options:
+ - "off"
+ - "all"
+ - "one"
+
+join:
+ description:
+ Group players together. Only works on platforms with support for player
+ groups.
+ name: Join
+ target:
+ fields:
+ group_members:
+ description:
+ The players which will be synced with the target player.
+ example:
+ - "media_player.multiroom_player2"
+ - "media_player.multiroom_player3"
+
+unjoin:
+ description:
+ Unjoin the player from a group. Only works on platforms with support for
+ player groups.
+ name: Unjoin
+ target:
diff --git a/homeassistant/components/media_player/strings.json b/homeassistant/components/media_player/strings.json
index 14f1eea131c7f7..64841413f12c45 100644
--- a/homeassistant/components/media_player/strings.json
+++ b/homeassistant/components/media_player/strings.json
@@ -7,6 +7,13 @@
"is_idle": "{entity_name} is idle",
"is_paused": "{entity_name} is paused",
"is_playing": "{entity_name} is playing"
+ },
+ "trigger_type": {
+ "turned_on": "{entity_name} turned on",
+ "turned_off": "{entity_name} turned off",
+ "idle": "{entity_name} becomes idle",
+ "paused": "{entity_name} is paused",
+ "playing": "{entity_name} starts playing"
}
},
"state": {
diff --git a/homeassistant/components/media_player/translations/ca.json b/homeassistant/components/media_player/translations/ca.json
index 67f7aad655b268..e1fce3340531a1 100644
--- a/homeassistant/components/media_player/translations/ca.json
+++ b/homeassistant/components/media_player/translations/ca.json
@@ -6,6 +6,13 @@
"is_on": "{entity_name} est\u00e0 enc\u00e8s",
"is_paused": "{entity_name} est\u00e0 en pausa",
"is_playing": "{entity_name} est\u00e0 reproduint"
+ },
+ "trigger_type": {
+ "idle": "{entity_name} es torna inactiu",
+ "paused": "{entity_name} est\u00e0 en pausa",
+ "playing": "{entity_name} comen\u00e7a a reproduir",
+ "turned_off": "{entity_name} s'ha apagat",
+ "turned_on": "{entity_name} s'ha engegat"
}
},
"state": {
diff --git a/homeassistant/components/media_player/translations/de.json b/homeassistant/components/media_player/translations/de.json
index a7f25fa9d7cddd..4909c85d053b80 100644
--- a/homeassistant/components/media_player/translations/de.json
+++ b/homeassistant/components/media_player/translations/de.json
@@ -6,6 +6,13 @@
"is_on": "{entity_name} ist eingeschaltet",
"is_paused": "{entity_name} ist pausiert",
"is_playing": "{entity_name} spielt"
+ },
+ "trigger_type": {
+ "idle": "{entity_name} wird inaktiv",
+ "paused": "{entity_name} ist angehalten",
+ "playing": "{entity_name} beginnt zu spielen",
+ "turned_off": "{entity_name} ausgeschaltet",
+ "turned_on": "{entity_name} eingeschaltet"
}
},
"state": {
diff --git a/homeassistant/components/media_player/translations/en.json b/homeassistant/components/media_player/translations/en.json
index 3a96a2b3a906b4..aa995be9904aff 100644
--- a/homeassistant/components/media_player/translations/en.json
+++ b/homeassistant/components/media_player/translations/en.json
@@ -6,6 +6,13 @@
"is_on": "{entity_name} is on",
"is_paused": "{entity_name} is paused",
"is_playing": "{entity_name} is playing"
+ },
+ "trigger_type": {
+ "idle": "{entity_name} becomes idle",
+ "paused": "{entity_name} is paused",
+ "playing": "{entity_name} starts playing",
+ "turned_off": "{entity_name} turned off",
+ "turned_on": "{entity_name} turned on"
}
},
"state": {
diff --git a/homeassistant/components/media_player/translations/es.json b/homeassistant/components/media_player/translations/es.json
index fffaedc1d972bc..f1ffc44957eefb 100644
--- a/homeassistant/components/media_player/translations/es.json
+++ b/homeassistant/components/media_player/translations/es.json
@@ -6,6 +6,13 @@
"is_on": "{entity_name} est\u00e1 activado",
"is_paused": "{entity_name} est\u00e1 en pausa",
"is_playing": "{entity_name} est\u00e1 reproduciendo"
+ },
+ "trigger_type": {
+ "idle": "{entity_name} est\u00e1 inactivo",
+ "paused": "{entity_name} est\u00e1 en pausa",
+ "playing": "{entity_name} comienza a reproducirse",
+ "turned_off": "{entity_name} desactivado",
+ "turned_on": "{entity_name} activado"
}
},
"state": {
diff --git a/homeassistant/components/media_player/translations/et.json b/homeassistant/components/media_player/translations/et.json
index 4d71a30a8ac2f8..687a0e5953df54 100644
--- a/homeassistant/components/media_player/translations/et.json
+++ b/homeassistant/components/media_player/translations/et.json
@@ -6,6 +6,13 @@
"is_on": "{entity_name} on sisse l\u00fclitatud",
"is_paused": "{entity_name} on peatatud",
"is_playing": "{entity_name} m\u00e4ngib"
+ },
+ "trigger_type": {
+ "idle": "{entity_name} muutub j\u00f5udeolekusse",
+ "paused": "{entity_name} on pausil",
+ "playing": "{entity_name} alustab taasesitamist",
+ "turned_off": "{entity_name} l\u00fclitus v\u00e4lja",
+ "turned_on": "{entity_name} l\u00fclitus sisse"
}
},
"state": {
diff --git a/homeassistant/components/media_player/translations/fr.json b/homeassistant/components/media_player/translations/fr.json
index f3992f7461614d..9ecdd19037f8ca 100644
--- a/homeassistant/components/media_player/translations/fr.json
+++ b/homeassistant/components/media_player/translations/fr.json
@@ -6,6 +6,13 @@
"is_on": "{entity_name} est activ\u00e9",
"is_paused": "{entity_name} est en pause",
"is_playing": "{entity_name} joue"
+ },
+ "trigger_type": {
+ "idle": "{entity_name} devient inactif",
+ "paused": "{entity_name} est mis en pause",
+ "playing": "{entity_name} commence \u00e0 jouer",
+ "turned_off": "{entity_name} d\u00e9sactiv\u00e9",
+ "turned_on": "{entity_name} activ\u00e9"
}
},
"state": {
diff --git a/homeassistant/components/media_player/translations/hu.json b/homeassistant/components/media_player/translations/hu.json
index 0eae14fdd986b8..83b5dc4e12281b 100644
--- a/homeassistant/components/media_player/translations/hu.json
+++ b/homeassistant/components/media_player/translations/hu.json
@@ -6,6 +6,13 @@
"is_on": "{entity_name} be van kapcsolva",
"is_paused": "{entity_name} sz\u00fcneteltetve van",
"is_playing": "{entity_name} lej\u00e1tszik"
+ },
+ "trigger_type": {
+ "idle": "{entity_name} t\u00e9tlenn\u00e9 v\u00e1lik",
+ "paused": "{entity_name} sz\u00fcneteltetve van",
+ "playing": "{entity_name} megkezdi a lej\u00e1tsz\u00e1st",
+ "turned_off": "{entity_name} ki lett kapcsolva",
+ "turned_on": "{entity_name} be lett kapcsolva"
}
},
"state": {
diff --git a/homeassistant/components/media_player/translations/id.json b/homeassistant/components/media_player/translations/id.json
index bcf12d72542e8d..e759f88a15a36c 100644
--- a/homeassistant/components/media_player/translations/id.json
+++ b/homeassistant/components/media_player/translations/id.json
@@ -1,11 +1,27 @@
{
+ "device_automation": {
+ "condition_type": {
+ "is_idle": "{entity_name} siaga",
+ "is_off": "{entity_name} mati",
+ "is_on": "{entity_name} nyala",
+ "is_paused": "{entity_name} dijeda",
+ "is_playing": "{entity_name} sedang memutar"
+ },
+ "trigger_type": {
+ "idle": "{entity_name} menjadi siaga",
+ "paused": "{entity_name} dijeda",
+ "playing": "{entity_name} mulai memutar",
+ "turned_off": "{entity_name} dimatikan",
+ "turned_on": "{entity_name} dinyalakan"
+ }
+ },
"state": {
"_": {
- "idle": "Diam",
- "off": "Off",
- "on": "On",
+ "idle": "Siaga",
+ "off": "Mati",
+ "on": "Nyala",
"paused": "Jeda",
- "playing": "Memainkan",
+ "playing": "Memutar",
"standby": "Siaga"
}
},
diff --git a/homeassistant/components/media_player/translations/it.json b/homeassistant/components/media_player/translations/it.json
index 23d1afa0625985..a3ebfdfe4112ae 100644
--- a/homeassistant/components/media_player/translations/it.json
+++ b/homeassistant/components/media_player/translations/it.json
@@ -6,6 +6,13 @@
"is_on": "{entity_name} \u00e8 acceso",
"is_paused": "{entity_name} \u00e8 in pausa",
"is_playing": "{entity_name} \u00e8 in esecuzione"
+ },
+ "trigger_type": {
+ "idle": "{entity_name} diventa inattivo",
+ "paused": "{entity_name} \u00e8 in pausa",
+ "playing": "{entity_name} inizia l'esecuzione",
+ "turned_off": "{entity_name} disattivato",
+ "turned_on": "{entity_name} attivato"
}
},
"state": {
diff --git a/homeassistant/components/media_player/translations/ko.json b/homeassistant/components/media_player/translations/ko.json
index e727e744d7368e..213b61ef6b9f18 100644
--- a/homeassistant/components/media_player/translations/ko.json
+++ b/homeassistant/components/media_player/translations/ko.json
@@ -1,11 +1,18 @@
{
"device_automation": {
"condition_type": {
- "is_idle": "{entity_name} \uc774(\uac00) \uc720\ud734 \uc0c1\ud0dc\uc774\uba74",
- "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74",
- "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74",
- "is_paused": "{entity_name} \uc774(\uac00) \uc77c\uc2dc\uc911\uc9c0\ub418\uc5b4 \uc788\uc73c\uba74",
- "is_playing": "{entity_name} \uc774(\uac00) \uc7ac\uc0dd \uc911\uc774\uba74"
+ "is_idle": "{entity_name}\uc774(\uac00) \ub300\uae30 \uc0c1\ud0dc\uc774\uba74",
+ "is_off": "{entity_name}\uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74",
+ "is_on": "{entity_name}\uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74",
+ "is_paused": "{entity_name}\uc774(\uac00) \uc77c\uc2dc\uc911\uc9c0\ub418\uc5b4 \uc788\uc73c\uba74",
+ "is_playing": "{entity_name}\uc774(\uac00) \uc7ac\uc0dd \uc911\uc774\uba74"
+ },
+ "trigger_type": {
+ "idle": "{entity_name}\uc774(\uac00) \ub300\uae30 \uc0c1\ud0dc\uac00 \ub420 \ub54c",
+ "paused": "{entity_name}\uc774(\uac00) \uc77c\uc2dc\uc911\uc9c0\ub420 \ub54c",
+ "playing": "{entity_name}\uc774(\uac00) \uc7ac\uc0dd\uc744 \uc2dc\uc791\ud560 \ub54c",
+ "turned_off": "{entity_name}\uc774(\uac00) \uaebc\uc84c\uc744 \ub54c",
+ "turned_on": "{entity_name}\uc774(\uac00) \ucf1c\uc84c\uc744 \ub54c"
}
},
"state": {
diff --git a/homeassistant/components/media_player/translations/nl.json b/homeassistant/components/media_player/translations/nl.json
index 5e690f35f8a119..6ad22742533bf7 100644
--- a/homeassistant/components/media_player/translations/nl.json
+++ b/homeassistant/components/media_player/translations/nl.json
@@ -6,6 +6,13 @@
"is_on": "{entity_name} is ingeschakeld",
"is_paused": "{entity_name} is gepauzeerd",
"is_playing": "{entity_name} wordt afgespeeld"
+ },
+ "trigger_type": {
+ "idle": "{entity_name} wordt inactief",
+ "paused": "{entity_name} is gepauzeerd",
+ "playing": "{entity_name} begint te spelen",
+ "turned_off": "{entity_name} uitgeschakeld",
+ "turned_on": "{entity_name} ingeschakeld"
}
},
"state": {
@@ -15,7 +22,7 @@
"on": "Aan",
"paused": "Gepauzeerd",
"playing": "Afspelen",
- "standby": "Standby"
+ "standby": "Stand-by"
}
},
"title": "Mediaspeler"
diff --git a/homeassistant/components/media_player/translations/no.json b/homeassistant/components/media_player/translations/no.json
index 691ec894a7b9f6..fa5618efc35962 100644
--- a/homeassistant/components/media_player/translations/no.json
+++ b/homeassistant/components/media_player/translations/no.json
@@ -6,6 +6,13 @@
"is_on": "{entity_name} er sl\u00e5tt p\u00e5",
"is_paused": "{entity_name} er satt p\u00e5 pause",
"is_playing": "{entity_name} spiller n\u00e5"
+ },
+ "trigger_type": {
+ "idle": "{entity_name} blir inaktiv",
+ "paused": "{entity_name} er satt p\u00e5 pause",
+ "playing": "{entity_name} begynner \u00e5 spille",
+ "turned_off": "{entity_name} sl\u00e5tt av",
+ "turned_on": "{entity_name} sl\u00e5tt p\u00e5"
}
},
"state": {
diff --git a/homeassistant/components/media_player/translations/pl.json b/homeassistant/components/media_player/translations/pl.json
index 23ba46f93391f6..2a70661d788831 100644
--- a/homeassistant/components/media_player/translations/pl.json
+++ b/homeassistant/components/media_player/translations/pl.json
@@ -6,6 +6,13 @@
"is_on": "odtwarzacz {entity_name} jest w\u0142\u0105czony",
"is_paused": "odtwarzanie medi\u00f3w na {entity_name} jest wstrzymane",
"is_playing": "{entity_name} odtwarza media"
+ },
+ "trigger_type": {
+ "idle": "odtwarzacz {entity_name} stanie si\u0119 bezczynny",
+ "paused": "odtwarzacz {entity_name} zostanie wstrzymany",
+ "playing": "odtwarzacz {entity_name} rozpocznie odtwarzanie",
+ "turned_off": "odtwarzacz {entity_name} zostanie wy\u0142\u0105czony",
+ "turned_on": "odtwarzacz {entity_name} zostanie w\u0142\u0105czony"
}
},
"state": {
diff --git a/homeassistant/components/media_player/translations/ru.json b/homeassistant/components/media_player/translations/ru.json
index 8ed46953675f48..df0b00d2482a1a 100644
--- a/homeassistant/components/media_player/translations/ru.json
+++ b/homeassistant/components/media_player/translations/ru.json
@@ -6,6 +6,13 @@
"is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
"is_paused": "{entity_name} \u043d\u0430 \u043f\u0430\u0443\u0437\u0435",
"is_playing": "{entity_name} \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442 \u043c\u0435\u0434\u0438\u0430"
+ },
+ "trigger_type": {
+ "idle": "{entity_name} \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0438\u0442 \u0432 \u0440\u0435\u0436\u0438\u043c \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f",
+ "paused": "{entity_name} \u043d\u0430 \u043f\u0430\u0443\u0437\u0435",
+ "playing": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u0435",
+ "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f",
+ "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f"
}
},
"state": {
diff --git a/homeassistant/components/media_player/translations/tr.json b/homeassistant/components/media_player/translations/tr.json
index 0130b5fb94cbd7..f7b9be9da5361b 100644
--- a/homeassistant/components/media_player/translations/tr.json
+++ b/homeassistant/components/media_player/translations/tr.json
@@ -1,4 +1,14 @@
{
+ "device_automation": {
+ "condition_type": {
+ "is_idle": "{entity_name} bo\u015fta",
+ "is_off": "{entity_name} kapal\u0131"
+ },
+ "trigger_type": {
+ "playing": "{entity_name} oynamaya ba\u015flar",
+ "turned_off": "{entity_name} kapat\u0131ld\u0131"
+ }
+ },
"state": {
"_": {
"idle": "Bo\u015fta",
diff --git a/homeassistant/components/media_player/translations/uk.json b/homeassistant/components/media_player/translations/uk.json
index f475829a524119..21c7f2897a3d85 100644
--- a/homeassistant/components/media_player/translations/uk.json
+++ b/homeassistant/components/media_player/translations/uk.json
@@ -1,7 +1,16 @@
{
+ "device_automation": {
+ "condition_type": {
+ "is_idle": "{entity_name} \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f",
+ "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_paused": "{entity_name} \u043d\u0430 \u043f\u0430\u0443\u0437\u0456",
+ "is_playing": "{entity_name} \u0432\u0456\u0434\u0442\u0432\u043e\u0440\u044e\u0454 \u043c\u0435\u0434\u0456\u0430"
+ }
+ },
"state": {
"_": {
- "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c",
+ "idle": "\u041e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f",
"off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
"on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e",
"paused": "\u041f\u0440\u0438\u0437\u0443\u043f\u0438\u043d\u0435\u043d\u043e",
@@ -9,5 +18,5 @@
"standby": "\u041e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f"
}
},
- "title": "\u041c\u0435\u0434\u0456\u0430 \u043f\u043b\u0435\u0454\u0440"
+ "title": "\u041c\u0435\u0434\u0456\u0430\u043f\u0440\u043e\u0433\u0440\u0430\u0432\u0430\u0447"
}
\ No newline at end of file
diff --git a/homeassistant/components/media_player/translations/zh-Hans.json b/homeassistant/components/media_player/translations/zh-Hans.json
index af8579075be2c3..0fa034898c39c0 100644
--- a/homeassistant/components/media_player/translations/zh-Hans.json
+++ b/homeassistant/components/media_player/translations/zh-Hans.json
@@ -6,6 +6,13 @@
"is_on": "{entity_name} \u5df2\u5f00\u542f",
"is_paused": "{entity_name} \u5df2\u6682\u505c",
"is_playing": "{entity_name} \u6b63\u5728\u64ad\u653e"
+ },
+ "trigger_type": {
+ "idle": "{entity_name} \u7a7a\u95f2",
+ "paused": "{entity_name} \u6682\u505c",
+ "playing": "{entity_name} \u5f00\u59cb\u64ad\u653e",
+ "turned_off": "{entity_name} \u88ab\u5173\u95ed",
+ "turned_on": "{entity_name} \u88ab\u6253\u5f00"
}
},
"state": {
diff --git a/homeassistant/components/media_player/translations/zh-Hant.json b/homeassistant/components/media_player/translations/zh-Hant.json
index 3ae786cbed945c..a3a4b82380e8e2 100644
--- a/homeassistant/components/media_player/translations/zh-Hant.json
+++ b/homeassistant/components/media_player/translations/zh-Hant.json
@@ -6,6 +6,13 @@
"is_on": "{entity_name}\u958b\u555f",
"is_paused": "{entity_name}\u5df2\u66ab\u505c",
"is_playing": "{entity_name}\u6b63\u5728\u64ad\u653e"
+ },
+ "trigger_type": {
+ "idle": "{entity_name}\u8b8a\u6210\u9592\u7f6e",
+ "paused": "{entity_name}\u5df2\u66ab\u505c",
+ "playing": "{entity_name}\u958b\u59cb\u64ad\u653e",
+ "turned_off": "{entity_name}\u5df2\u95dc\u9589",
+ "turned_on": "{entity_name}\u5df2\u958b\u555f"
}
},
"state": {
diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py
index 3dff949d5dd6ad..6aa01403a5f1f6 100644
--- a/homeassistant/components/media_source/__init__.py
+++ b/homeassistant/components/media_source/__init__.py
@@ -1,6 +1,7 @@
"""The media_source integration."""
+from __future__ import annotations
+
from datetime import timedelta
-from typing import Optional
import voluptuous as vol
@@ -54,7 +55,7 @@ async def _process_media_source_platform(hass, domain, platform):
@callback
def _get_media_item(
- hass: HomeAssistant, media_content_id: Optional[str]
+ hass: HomeAssistant, media_content_id: str | None
) -> models.MediaSourceItem:
"""Return media item."""
if media_content_id:
diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py
index d7a2bdfd9383dd..fb5e9094dfb21c 100644
--- a/homeassistant/components/media_source/local_source.py
+++ b/homeassistant/components/media_source/local_source.py
@@ -1,7 +1,8 @@
"""Local Media Source Implementation."""
+from __future__ import annotations
+
import mimetypes
from pathlib import Path
-from typing import Tuple
from aiohttp import web
@@ -10,7 +11,7 @@
from homeassistant.components.media_player.errors import BrowseError
from homeassistant.components.media_source.error import Unresolvable
from homeassistant.core import HomeAssistant, callback
-from homeassistant.util import raise_if_invalid_filename
+from homeassistant.util import raise_if_invalid_path
from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES
from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia
@@ -40,7 +41,7 @@ def async_full_path(self, source_dir_id, location) -> Path:
return Path(self.hass.config.media_dirs[source_dir_id], location)
@callback
- def async_parse_identifier(self, item: MediaSourceItem) -> Tuple[str, str]:
+ def async_parse_identifier(self, item: MediaSourceItem) -> tuple[str, str]:
"""Parse identifier."""
if not item.identifier:
# Empty source_dir_id and location
@@ -51,7 +52,7 @@ def async_parse_identifier(self, item: MediaSourceItem) -> Tuple[str, str]:
raise Unresolvable("Unknown source directory.")
try:
- raise_if_invalid_filename(location)
+ raise_if_invalid_path(location)
except ValueError as err:
raise Unresolvable("Invalid path.") from err
@@ -69,7 +70,7 @@ async def async_resolve_media(self, item: MediaSourceItem) -> str:
return PlayMedia(f"/media/{item.identifier}", mime_type)
async def async_browse_media(
- self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES
+ self, item: MediaSourceItem, media_types: tuple[str] = MEDIA_MIME_TYPES
) -> BrowseMediaSource:
"""Return media."""
try:
@@ -192,7 +193,7 @@ async def get(
) -> web.FileResponse:
"""Start a GET request."""
try:
- raise_if_invalid_filename(location)
+ raise_if_invalid_path(location)
except ValueError as err:
raise web.HTTPBadRequest() from err
diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py
index e16ecbe578ecf1..aa17fff320efe1 100644
--- a/homeassistant/components/media_source/models.py
+++ b/homeassistant/components/media_source/models.py
@@ -1,7 +1,8 @@
"""Media Source models."""
+from __future__ import annotations
+
from abc import ABC
from dataclasses import dataclass
-from typing import List, Optional, Tuple
from homeassistant.components.media_player import BrowseMedia
from homeassistant.components.media_player.const import (
@@ -26,9 +27,9 @@ class PlayMedia:
class BrowseMediaSource(BrowseMedia):
"""Represent a browsable media file."""
- children: Optional[List["BrowseMediaSource"]]
+ children: list[BrowseMediaSource] | None
- def __init__(self, *, domain: Optional[str], identifier: Optional[str], **kwargs):
+ def __init__(self, *, domain: str | None, identifier: str | None, **kwargs):
"""Initialize media source browse media."""
media_content_id = f"{URI_SCHEME}{domain or ''}"
if identifier:
@@ -45,7 +46,7 @@ class MediaSourceItem:
"""A parsed media item."""
hass: HomeAssistant
- domain: Optional[str]
+ domain: str | None
identifier: str
async def async_browse(self) -> BrowseMediaSource:
@@ -82,12 +83,12 @@ async def async_resolve(self) -> PlayMedia:
return await self.async_media_source().async_resolve_media(self)
@callback
- def async_media_source(self) -> "MediaSource":
+ def async_media_source(self) -> MediaSource:
"""Return media source that owns this item."""
return self.hass.data[DOMAIN][self.domain]
@classmethod
- def from_uri(cls, hass: HomeAssistant, uri: str) -> "MediaSourceItem":
+ def from_uri(cls, hass: HomeAssistant, uri: str) -> MediaSourceItem:
"""Create an item from a uri."""
match = URI_SCHEME_REGEX.match(uri)
@@ -116,7 +117,7 @@ async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
raise NotImplementedError
async def async_browse_media(
- self, item: MediaSourceItem, media_types: Tuple[str]
+ self, item: MediaSourceItem, media_types: tuple[str]
) -> BrowseMediaSource:
"""Browse media."""
raise NotImplementedError
diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py
index 0e81d6101b3059..0f48db96bf846f 100644
--- a/homeassistant/components/melcloud/__init__.py
+++ b/homeassistant/components/melcloud/__init__.py
@@ -1,8 +1,10 @@
"""The MELCloud Climate integration."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
import logging
-from typing import Any, Dict, List
+from typing import Any
from aiohttp import ClientConnectionError
from async_timeout import timeout
@@ -101,7 +103,7 @@ async def async_update(self, **kwargs):
_LOGGER.warning("Connection failed for %s", self.name)
self._available = False
- async def async_set(self, properties: Dict[str, Any]):
+ async def async_set(self, properties: dict[str, Any]):
"""Write state changes to the MELCloud API."""
try:
await self.device.set(properties)
@@ -142,7 +144,7 @@ def device_info(self):
return _device_info
-async def mel_devices_setup(hass, token) -> List[MelCloudDevice]:
+async def mel_devices_setup(hass, token) -> list[MelCloudDevice]:
"""Query connected devices from MELCloud."""
session = hass.helpers.aiohttp_client.async_get_clientsession()
try:
diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py
index 4c409ec5a4d0fc..8e45cc3d9a4c18 100644
--- a/homeassistant/components/melcloud/climate.py
+++ b/homeassistant/components/melcloud/climate.py
@@ -1,6 +1,8 @@
"""Platform for climate integration."""
+from __future__ import annotations
+
from datetime import timedelta
-from typing import Any, Dict, List, Optional
+from typing import Any
from pymelcloud import DEVICE_TYPE_ATA, DEVICE_TYPE_ATW, AtaDevice, AtwDevice
import pymelcloud.ata_device as ata
@@ -114,7 +116,7 @@ def device_info(self):
return self.api.device_info
@property
- def target_temperature_step(self) -> Optional[float]:
+ def target_temperature_step(self) -> float | None:
"""Return the supported step of target temperature."""
return self._base_device.temperature_increment
@@ -128,7 +130,7 @@ def __init__(self, device: MelCloudDevice, ata_device: AtaDevice) -> None:
self._device = ata_device
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID."""
return f"{self.api.device.serial}-{self.api.device.mac}"
@@ -138,7 +140,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the optional state attributes with device specific additions."""
attr = {}
@@ -190,19 +192,19 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None:
await self._device.set(props)
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes."""
return [HVAC_MODE_OFF] + [
ATA_HVAC_MODE_LOOKUP.get(mode) for mode in self._device.operation_modes
]
@property
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self._device.room_temperature
@property
- def target_temperature(self) -> Optional[float]:
+ def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
return self._device.target_temperature
@@ -213,7 +215,7 @@ async def async_set_temperature(self, **kwargs) -> None:
)
@property
- def fan_mode(self) -> Optional[str]:
+ def fan_mode(self) -> str | None:
"""Return the fan setting."""
return self._device.fan_speed
@@ -222,7 +224,7 @@ async def async_set_fan_mode(self, fan_mode: str) -> None:
await self._device.set({"fan_speed": fan_mode})
@property
- def fan_modes(self) -> Optional[List[str]]:
+ def fan_modes(self) -> list[str] | None:
"""Return the list of available fan modes."""
return self._device.fan_speeds
@@ -243,7 +245,7 @@ async def async_set_vane_vertical(self, position: str) -> None:
await self._device.set({ata.PROPERTY_VANE_VERTICAL: position})
@property
- def swing_mode(self) -> Optional[str]:
+ def swing_mode(self) -> str | None:
"""Return vertical vane position or mode."""
return self._device.vane_vertical
@@ -252,7 +254,7 @@ async def async_set_swing_mode(self, swing_mode) -> None:
await self.async_set_vane_vertical(swing_mode)
@property
- def swing_modes(self) -> Optional[str]:
+ def swing_modes(self) -> str | None:
"""Return a list of available vertical vane positions and modes."""
return self._device.vane_vertical_positions
@@ -300,7 +302,7 @@ def __init__(
self._zone = atw_zone
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID."""
return f"{self.api.device.serial}-{self._zone.zone_index}"
@@ -310,7 +312,7 @@ def name(self) -> str:
return f"{self._name} {self._zone.name}"
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes with device specific additions."""
data = {
ATTR_STATUS: ATW_ZONE_HVAC_MODE_LOOKUP.get(
@@ -351,17 +353,17 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None:
await self._device.set(props)
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes."""
return [self.hvac_mode]
@property
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self._zone.room_temperature
@property
- def target_temperature(self) -> Optional[float]:
+ def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
return self._zone.target_temperature
diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py
index 41ce24989a5d00..98ba343bfcb151 100644
--- a/homeassistant/components/melcloud/config_flow.py
+++ b/homeassistant/components/melcloud/config_flow.py
@@ -1,6 +1,7 @@
"""Config flow for the MELCloud platform."""
+from __future__ import annotations
+
import asyncio
-from typing import Optional
from aiohttp import ClientError, ClientResponseError
from async_timeout import timeout
@@ -16,7 +17,7 @@
HTTP_UNAUTHORIZED,
)
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import DOMAIN
class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@@ -37,8 +38,8 @@ async def _create_client(
self,
username: str,
*,
- password: Optional[str] = None,
- token: Optional[str] = None,
+ password: str | None = None,
+ token: str | None = None,
):
"""Create client."""
if password is None and token is None:
diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py
index c96433f17dfd20..356992ece11ae3 100644
--- a/homeassistant/components/melcloud/sensor.py
+++ b/homeassistant/components/melcloud/sensor.py
@@ -2,20 +2,20 @@
from pymelcloud import DEVICE_TYPE_ATA, DEVICE_TYPE_ATW
from pymelcloud.atw_device import Zone
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ ATTR_ICON,
DEVICE_CLASS_TEMPERATURE,
ENERGY_KILO_WATT_HOUR,
TEMP_CELSIUS,
)
-from homeassistant.helpers.entity import Entity
from . import MelCloudDevice
from .const import DOMAIN
ATTR_MEASUREMENT_NAME = "measurement_name"
-ATTR_ICON = "icon"
ATTR_UNIT = "unit"
-ATTR_DEVICE_CLASS = "device_class"
ATTR_VALUE_FN = "value_fn"
ATTR_ENABLED_FN = "enabled"
@@ -110,7 +110,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
)
-class MelDeviceSensor(Entity):
+class MelDeviceSensor(SensorEntity):
"""Representation of a Sensor."""
def __init__(self, api: MelCloudDevice, measurement, definition):
diff --git a/homeassistant/components/melcloud/translations/de.json b/homeassistant/components/melcloud/translations/de.json
index 640c96e47c462d..54ae78f8680878 100644
--- a/homeassistant/components/melcloud/translations/de.json
+++ b/homeassistant/components/melcloud/translations/de.json
@@ -4,7 +4,7 @@
"already_configured": "Die MELCloud-Integration ist bereits f\u00fcr diese E-Mail konfiguriert. Das Zugriffstoken wurde aktualisiert."
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuchen Sie es erneut.",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
diff --git a/homeassistant/components/melcloud/translations/he.json b/homeassistant/components/melcloud/translations/he.json
new file mode 100644
index 00000000000000..ac90b3264eab33
--- /dev/null
+++ b/homeassistant/components/melcloud/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/translations/hu.json b/homeassistant/components/melcloud/translations/hu.json
index 62699ecb4683b9..7f81269c70011c 100644
--- a/homeassistant/components/melcloud/translations/hu.json
+++ b/homeassistant/components/melcloud/translations/hu.json
@@ -1,5 +1,10 @@
{
"config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/melcloud/translations/id.json b/homeassistant/components/melcloud/translations/id.json
new file mode 100644
index 00000000000000..d2847541537ff4
--- /dev/null
+++ b/homeassistant/components/melcloud/translations/id.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Integrasi MELCloud sudah dikonfigurasi untuk email ini. Token akses telah disegarkan."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Email"
+ },
+ "description": "Hubungkan menggunakan akun MELCloud Anda.",
+ "title": "Hubungkan ke MELCloud"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/translations/ko.json b/homeassistant/components/melcloud/translations/ko.json
index a43d4cfbcb3062..ce984e26906c6e 100644
--- a/homeassistant/components/melcloud/translations/ko.json
+++ b/homeassistant/components/melcloud/translations/ko.json
@@ -4,7 +4,7 @@
"already_configured": "\uc774 \uc774\uba54\uc77c\uc5d0 \ub300\ud55c MELCloud \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uac31\uc2e0\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
@@ -15,7 +15,7 @@
"username": "\uc774\uba54\uc77c"
},
"description": "MELCloud \uacc4\uc815\uc73c\ub85c \uc5f0\uacb0\ud558\uc138\uc694.",
- "title": "MELCloud \uc5d0 \uc5f0\uacb0\ud558\uae30"
+ "title": "MELCloud\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/melcloud/translations/nl.json b/homeassistant/components/melcloud/translations/nl.json
index 8ef8cc716b1b82..481027f9092a38 100644
--- a/homeassistant/components/melcloud/translations/nl.json
+++ b/homeassistant/components/melcloud/translations/nl.json
@@ -4,15 +4,15 @@
"already_configured": "MELCloud integratie is al geconfigureerd voor deze e-mail. Toegangstoken is vernieuwd."
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
"step": {
"user": {
"data": {
- "password": "MELCloud wachtwoord.",
- "username": "E-mail gebruikt om in te loggen op MELCloud."
+ "password": "Wachtwoord",
+ "username": "E-mail"
},
"description": "Maak verbinding via uw MELCloud account.",
"title": "Maak verbinding met MELCloud"
diff --git a/homeassistant/components/melcloud/translations/ru.json b/homeassistant/components/melcloud/translations/ru.json
index e904ea4e8b7206..5c5081cb0c6fd2 100644
--- a/homeassistant/components/melcloud/translations/ru.json
+++ b/homeassistant/components/melcloud/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
diff --git a/homeassistant/components/melcloud/translations/tr.json b/homeassistant/components/melcloud/translations/tr.json
new file mode 100644
index 00000000000000..6bce50f3de659e
--- /dev/null
+++ b/homeassistant/components/melcloud/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "MELCloud entegrasyonu bu e-posta i\u00e7in zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Eri\u015fim belirteci yenilendi."
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "E-posta"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/translations/uk.json b/homeassistant/components/melcloud/translations/uk.json
new file mode 100644
index 00000000000000..001239a8b47a48
--- /dev/null
+++ b/homeassistant/components/melcloud/translations/uk.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f MELCloud \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430 \u0434\u043b\u044f \u0446\u0456\u0454\u0457 \u0430\u0434\u0440\u0435\u0441\u0438 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438. \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0439."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438"
+ },
+ "description": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0456\u0442\u044c\u0441\u044f, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0447\u0438 \u0441\u0432\u0456\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 MELCloud.",
+ "title": "MELCloud"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/translations/zh-Hant.json b/homeassistant/components/melcloud/translations/zh-Hant.json
index 9947b5ac990869..27f4d0e5d7f0d2 100644
--- a/homeassistant/components/melcloud/translations/zh-Hant.json
+++ b/homeassistant/components/melcloud/translations/zh-Hant.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u5df2\u4f7f\u7528\u6b64\u90f5\u4ef6\u8a2d\u5b9a MELCloud \u6574\u5408\u3002\u5b58\u53d6\u5bc6\u9470\u5df2\u66f4\u65b0\u3002"
+ "already_configured": "\u5df2\u4f7f\u7528\u6b64\u90f5\u4ef6\u8a2d\u5b9a MELCloud \u6574\u5408\u3002\u5b58\u53d6\u6b0a\u6756\u5df2\u66f4\u65b0\u3002"
},
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557",
diff --git a/homeassistant/components/melcloud/water_heater.py b/homeassistant/components/melcloud/water_heater.py
index ae10b5140f76a1..e01d78a527068e 100644
--- a/homeassistant/components/melcloud/water_heater.py
+++ b/homeassistant/components/melcloud/water_heater.py
@@ -1,5 +1,5 @@
"""Platform for water_heater integration."""
-from typing import List, Optional
+from __future__ import annotations
from pymelcloud import DEVICE_TYPE_ATW, AtwDevice
from pymelcloud.atw_device import (
@@ -49,7 +49,7 @@ async def async_update(self):
await self._api.async_update()
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID."""
return f"{self._api.device.serial}"
@@ -72,7 +72,7 @@ async def async_turn_off(self) -> None:
await self._device.set({PROPERTY_POWER: False})
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the optional state attributes with device specific additions."""
data = {ATTR_STATUS: self._device.status}
return data
@@ -83,17 +83,17 @@ def temperature_unit(self) -> str:
return TEMP_CELSIUS
@property
- def current_operation(self) -> Optional[str]:
+ def current_operation(self) -> str | None:
"""Return current operation as reported by pymelcloud."""
return self._device.operation_mode
@property
- def operation_list(self) -> List[str]:
+ def operation_list(self) -> list[str]:
"""Return the list of available operation modes as reported by pymelcloud."""
return self._device.operation_modes
@property
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self._device.tank_temperature
@@ -122,11 +122,11 @@ def supported_features(self):
return SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
@property
- def min_temp(self) -> Optional[float]:
+ def min_temp(self) -> float | None:
"""Return the minimum temperature."""
return self._device.target_tank_temperature_min
@property
- def max_temp(self) -> Optional[float]:
+ def max_temp(self) -> float | None:
"""Return the maximum temperature."""
return self._device.target_tank_temperature_max
diff --git a/homeassistant/components/meraki/device_tracker.py b/homeassistant/components/meraki/device_tracker.py
index 55186d6314673d..13644c1d341d12 100644
--- a/homeassistant/components/meraki/device_tracker.py
+++ b/homeassistant/components/meraki/device_tracker.py
@@ -56,7 +56,7 @@ async def post(self, request):
return self.json_message("Invalid JSON", HTTP_BAD_REQUEST)
_LOGGER.debug("Meraki Data from Post: %s", json.dumps(data))
if not data.get("secret", False):
- _LOGGER.error("secret invalid")
+ _LOGGER.error("The secret is invalid")
return self.json_message("No secret", HTTP_UNPROCESSABLE_ENTITY)
if data["secret"] != self.secret:
_LOGGER.error("Invalid Secret received from Meraki")
diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py
index 5a357467920cc1..47d946b92e7a6b 100644
--- a/homeassistant/components/met/__init__.py
+++ b/homeassistant/components/met/__init__.py
@@ -14,13 +14,17 @@
LENGTH_METERS,
)
from homeassistant.core import Config, HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
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 .const import CONF_TRACK_HOME, DOMAIN
+from .const import (
+ CONF_TRACK_HOME,
+ DEFAULT_HOME_LATITUDE,
+ DEFAULT_HOME_LONGITUDE,
+ DOMAIN,
+)
URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/complete"
@@ -36,11 +40,22 @@ async def async_setup(hass: HomeAssistant, config: Config) -> bool:
async def async_setup_entry(hass, config_entry):
"""Set up Met as config entry."""
- coordinator = MetDataUpdateCoordinator(hass, config_entry)
- await coordinator.async_refresh()
+ # Don't setup if tracking home location and latitude or longitude isn't set.
+ # Also, filters out our onboarding default location.
+ if config_entry.data.get(CONF_TRACK_HOME, False) and (
+ (not hass.config.latitude and not hass.config.longitude)
+ or (
+ hass.config.latitude == DEFAULT_HOME_LATITUDE
+ and hass.config.longitude == DEFAULT_HOME_LONGITUDE
+ )
+ ):
+ _LOGGER.warning(
+ "Skip setting up met.no integration; No Home location has been set"
+ )
+ return False
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ coordinator = MetDataUpdateCoordinator(hass, config_entry)
+ await coordinator.async_config_entry_first_refresh()
if config_entry.data.get(CONF_TRACK_HOME, False):
coordinator.track_home()
@@ -72,7 +87,7 @@ def __init__(self, hass, config_entry):
self.weather = MetWeatherData(
hass, config_entry.data, hass.config.units.is_metric
)
- self.weather.init_data()
+ self.weather.set_coordinates()
update_interval = timedelta(minutes=randrange(55, 65))
@@ -92,8 +107,8 @@ def track_home(self):
async def _async_update_weather_data(_event=None):
"""Update weather data."""
- self.weather.init_data()
- await self.async_refresh()
+ if self.weather.set_coordinates():
+ await self.async_refresh()
self._unsub_track_home = self.hass.bus.async_listen(
EVENT_CORE_CONFIG_UPDATE, _async_update_weather_data
@@ -118,9 +133,10 @@ def __init__(self, hass, config, is_metric):
self.current_weather_data = {}
self.daily_forecast = None
self.hourly_forecast = None
+ self._coordinates = None
- def init_data(self):
- """Weather data inialization - get the coordinates."""
+ def set_coordinates(self):
+ """Weather data inialization - set the coordinates."""
if self._config.get(CONF_TRACK_HOME, False):
latitude = self.hass.config.latitude
longitude = self.hass.config.longitude
@@ -140,10 +156,14 @@ def init_data(self):
"lon": str(longitude),
"msl": str(elevation),
}
+ if coordinates == self._coordinates:
+ return False
+ self._coordinates = coordinates
self._weather_data = metno.MetWeatherData(
coordinates, async_get_clientsession(self.hass), api_url=URL
)
+ return True
async def fetch_data(self):
"""Fetch data from API - (current weather and forecast)."""
diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py
index 6b3d6735f46604..5cfd71ea80121b 100644
--- a/homeassistant/components/met/config_flow.py
+++ b/homeassistant/components/met/config_flow.py
@@ -1,5 +1,7 @@
"""Config flow to configure Met component."""
-from typing import Any, Dict, Optional
+from __future__ import annotations
+
+from typing import Any
import voluptuous as vol
@@ -8,7 +10,13 @@
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from .const import CONF_TRACK_HOME, DOMAIN, HOME_LOCATION_NAME
+from .const import (
+ CONF_TRACK_HOME,
+ DEFAULT_HOME_LATITUDE,
+ DEFAULT_HOME_LONGITUDE,
+ DOMAIN,
+ HOME_LOCATION_NAME,
+)
@callback
@@ -73,14 +81,20 @@ async def _show_config_form(
errors=self._errors,
)
- async def async_step_import(
- self, user_input: Optional[Dict] = None
- ) -> Dict[str, Any]:
+ async def async_step_import(self, user_input: dict | None = None) -> dict[str, Any]:
"""Handle configuration by yaml file."""
return await self.async_step_user(user_input)
async def async_step_onboarding(self, data=None):
"""Handle a flow initialized by onboarding."""
+ # Don't create entry if latitude or longitude isn't set.
+ # Also, filters out our onboarding default location.
+ if (not self.hass.config.latitude and not self.hass.config.longitude) or (
+ self.hass.config.latitude == DEFAULT_HOME_LATITUDE
+ and self.hass.config.longitude == DEFAULT_HOME_LONGITUDE
+ ):
+ return self.async_abort(reason="no_home")
+
return self.async_create_entry(
title=HOME_LOCATION_NAME, data={CONF_TRACK_HOME: True}
)
diff --git a/homeassistant/components/met/const.py b/homeassistant/components/met/const.py
index 8c507eb0b8d285..0f4c22dbba3349 100644
--- a/homeassistant/components/met/const.py
+++ b/homeassistant/components/met/const.py
@@ -1,6 +1,4 @@
"""Constants for Met component."""
-import logging
-
from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_CLOUDY,
@@ -36,6 +34,9 @@
CONF_TRACK_HOME = "track_home"
+DEFAULT_HOME_LATITUDE = 52.3731339
+DEFAULT_HOME_LONGITUDE = 4.8903147
+
ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_{HOME_LOCATION_NAME}"
CONDITIONS_MAP = {
@@ -191,5 +192,3 @@
ATTR_WEATHER_WIND_BEARING: "wind_bearing",
ATTR_WEATHER_WIND_SPEED: "wind_speed",
}
-
-_LOGGER = logging.getLogger(".")
diff --git a/homeassistant/components/met/strings.json b/homeassistant/components/met/strings.json
index b9e94aba8658e4..b9d251e21d890f 100644
--- a/homeassistant/components/met/strings.json
+++ b/homeassistant/components/met/strings.json
@@ -12,6 +12,11 @@
}
}
},
- "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" }
+ "error": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
+ },
+ "abort": {
+ "no_home": "No home coordinates are set in the Home Assistant configuration"
+ }
}
}
diff --git a/homeassistant/components/met/translations/ca.json b/homeassistant/components/met/translations/ca.json
index 11815222df5cec..7b227fd8df04a1 100644
--- a/homeassistant/components/met/translations/ca.json
+++ b/homeassistant/components/met/translations/ca.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "no_home": "No s'han configurat coordenades d'ubicaci\u00f3 principal en la configuraci\u00f3 de Home Assistant"
+ },
"error": {
"already_configured": "El servei ja est\u00e0 configurat"
},
diff --git a/homeassistant/components/met/translations/de.json b/homeassistant/components/met/translations/de.json
index 901b4fb97b5ee7..e2bb171c749bb1 100644
--- a/homeassistant/components/met/translations/de.json
+++ b/homeassistant/components/met/translations/de.json
@@ -1,5 +1,8 @@
{
"config": {
+ "error": {
+ "already_configured": "Der Dienst ist bereits konfiguriert"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/met/translations/en.json b/homeassistant/components/met/translations/en.json
index 590bf48e635fb9..498c23aa3282d0 100644
--- a/homeassistant/components/met/translations/en.json
+++ b/homeassistant/components/met/translations/en.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "no_home": "No home coordinates are set in the Home Assistant configuration"
+ },
"error": {
"already_configured": "Service is already configured"
},
diff --git a/homeassistant/components/met/translations/et.json b/homeassistant/components/met/translations/et.json
index d25ca8df0a51be..81155c80d549ef 100644
--- a/homeassistant/components/met/translations/et.json
+++ b/homeassistant/components/met/translations/et.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "no_home": "Home Assistanti s\u00e4tetes pole kodu koordinaate m\u00e4\u00e4ratud"
+ },
"error": {
"already_configured": "Teenus on juba h\u00e4\u00e4lestatud"
},
diff --git a/homeassistant/components/met/translations/he.json b/homeassistant/components/met/translations/he.json
new file mode 100644
index 00000000000000..4c49313d97741a
--- /dev/null
+++ b/homeassistant/components/met/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met/translations/hu.json b/homeassistant/components/met/translations/hu.json
index 4e3ccf87ab76a1..b9141541a93f51 100644
--- a/homeassistant/components/met/translations/hu.json
+++ b/homeassistant/components/met/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "error": {
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/met/translations/id.json b/homeassistant/components/met/translations/id.json
index 12854e4ed619ff..639ed5086ce23e 100644
--- a/homeassistant/components/met/translations/id.json
+++ b/homeassistant/components/met/translations/id.json
@@ -1,11 +1,17 @@
{
"config": {
+ "error": {
+ "already_configured": "Layanan sudah dikonfigurasi"
+ },
"step": {
"user": {
"data": {
"elevation": "Ketinggian",
+ "latitude": "Lintang",
+ "longitude": "Bujur",
"name": "Nama"
},
+ "description": "Meteorologisk institutt",
"title": "Lokasi"
}
}
diff --git a/homeassistant/components/met/translations/it.json b/homeassistant/components/met/translations/it.json
index 2a00b31eedb330..9ff994a2aeabc3 100644
--- a/homeassistant/components/met/translations/it.json
+++ b/homeassistant/components/met/translations/it.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "no_home": "Nessuna coordinata di casa \u00e8 impostata nella configurazione di Home Assistant"
+ },
"error": {
"already_configured": "Il servizio \u00e8 gi\u00e0 configurato"
},
diff --git a/homeassistant/components/met/translations/ko.json b/homeassistant/components/met/translations/ko.json
index e7263aba3d252a..17175c196c0af0 100644
--- a/homeassistant/components/met/translations/ko.json
+++ b/homeassistant/components/met/translations/ko.json
@@ -1,5 +1,8 @@
{
"config": {
+ "error": {
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/met/translations/nl.json b/homeassistant/components/met/translations/nl.json
index 108c2a44f66313..7c3d03fdb1ff3b 100644
--- a/homeassistant/components/met/translations/nl.json
+++ b/homeassistant/components/met/translations/nl.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "no_home": "Er zijn geen thuisco\u00f6rdinaten ingesteld in de Home Assistant-configuratie"
+ },
"error": {
"already_configured": "Service is al geconfigureerd"
},
diff --git a/homeassistant/components/met/translations/no.json b/homeassistant/components/met/translations/no.json
index 05ba5b0c9d914e..b2fabd10a1c994 100644
--- a/homeassistant/components/met/translations/no.json
+++ b/homeassistant/components/met/translations/no.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "no_home": "Ingen hjemmekoordinater er angitt i Home Assistant-konfigurasjonen"
+ },
"error": {
"already_configured": "Tjenesten er allerede konfigurert"
},
diff --git a/homeassistant/components/met/translations/pl.json b/homeassistant/components/met/translations/pl.json
index 7b357f6b7eb373..1e7cf1ac67e7e7 100644
--- a/homeassistant/components/met/translations/pl.json
+++ b/homeassistant/components/met/translations/pl.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "no_home": "Nie ustawiono wsp\u00f3\u0142rz\u0119dnych domu w konfiguracji Home Assistant"
+ },
"error": {
"already_configured": "Us\u0142uga jest ju\u017c skonfigurowana"
},
diff --git a/homeassistant/components/met/translations/ru.json b/homeassistant/components/met/translations/ru.json
index f28ce7f28131fb..6dc9a667a8b428 100644
--- a/homeassistant/components/met/translations/ru.json
+++ b/homeassistant/components/met/translations/ru.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "no_home": "\u0412 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 Home Assistant \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u044b \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043e\u043c\u0430."
+ },
"error": {
"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."
},
diff --git a/homeassistant/components/met/translations/tr.json b/homeassistant/components/met/translations/tr.json
new file mode 100644
index 00000000000000..d256711728c58b
--- /dev/null
+++ b/homeassistant/components/met/translations/tr.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Enlem",
+ "longitude": "Boylam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met/translations/uk.json b/homeassistant/components/met/translations/uk.json
new file mode 100644
index 00000000000000..d980db91147a56
--- /dev/null
+++ b/homeassistant/components/met/translations/uk.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "elevation": "\u0412\u0438\u0441\u043e\u0442\u0430",
+ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
+ "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430",
+ "name": "\u041d\u0430\u0437\u0432\u0430"
+ },
+ "description": "\u041d\u043e\u0440\u0432\u0435\u0437\u044c\u043a\u0438\u0439 \u043c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0456\u0447\u043d\u0438\u0439 \u0456\u043d\u0441\u0442\u0438\u0442\u0443\u0442.",
+ "title": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met/translations/zh-Hant.json b/homeassistant/components/met/translations/zh-Hant.json
index d5cba312536a08..e4b2c65e7015f7 100644
--- a/homeassistant/components/met/translations/zh-Hant.json
+++ b/homeassistant/components/met/translations/zh-Hant.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "no_home": "Home Assistant \u672a\u8a2d\u5b9a\u4f4f\u5bb6\u5ea7\u6a19"
+ },
"error": {
"already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py
index c0c8c11c6447d8..4657da9e5d48a2 100644
--- a/homeassistant/components/met/weather.py
+++ b/homeassistant/components/met/weather.py
@@ -230,14 +230,13 @@ def forecast(self):
for k, v in FORECAST_MAP.items()
if met_item.get(v) is not None
}
- if not self._is_metric:
- if ATTR_FORECAST_PRECIPITATION in ha_item:
- precip_inches = convert_distance(
- ha_item[ATTR_FORECAST_PRECIPITATION],
- LENGTH_MILLIMETERS,
- LENGTH_INCHES,
- )
- ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2)
+ if not self._is_metric and ATTR_FORECAST_PRECIPITATION in ha_item:
+ precip_inches = convert_distance(
+ ha_item[ATTR_FORECAST_PRECIPITATION],
+ LENGTH_MILLIMETERS,
+ LENGTH_INCHES,
+ )
+ ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2)
if ha_item.get(ATTR_FORECAST_CONDITION):
ha_item[ATTR_FORECAST_CONDITION] = format_condition(
ha_item[ATTR_FORECAST_CONDITION]
diff --git a/homeassistant/components/met_eireann/__init__.py b/homeassistant/components/met_eireann/__init__.py
new file mode 100644
index 00000000000000..365e4dbafb3642
--- /dev/null
+++ b/homeassistant/components/met_eireann/__init__.py
@@ -0,0 +1,84 @@
+"""The met_eireann component."""
+from datetime import timedelta
+import logging
+
+import meteireann
+
+from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+import homeassistant.util.dt as dt_util
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+UPDATE_INTERVAL = timedelta(minutes=60)
+
+
+async def async_setup_entry(hass, config_entry):
+ """Set up Met Éireann as config entry."""
+ hass.data.setdefault(DOMAIN, {})
+
+ raw_weather_data = meteireann.WeatherData(
+ async_get_clientsession(hass),
+ latitude=config_entry.data[CONF_LATITUDE],
+ longitude=config_entry.data[CONF_LONGITUDE],
+ altitude=config_entry.data[CONF_ELEVATION],
+ )
+
+ weather_data = MetEireannWeatherData(hass, config_entry.data, raw_weather_data)
+
+ async def _async_update_data():
+ """Fetch data from Met Éireann."""
+ try:
+ return await weather_data.fetch_data()
+ except Exception as err:
+ raise UpdateFailed(f"Update failed: {err}") from err
+
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name=DOMAIN,
+ update_method=_async_update_data,
+ update_interval=UPDATE_INTERVAL,
+ )
+ await coordinator.async_refresh()
+
+ hass.data[DOMAIN][config_entry.entry_id] = coordinator
+
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, "weather")
+ )
+
+ return True
+
+
+async def async_unload_entry(hass, config_entry):
+ """Unload a config entry."""
+ await hass.config_entries.async_forward_entry_unload(config_entry, "weather")
+ hass.data[DOMAIN].pop(config_entry.entry_id)
+
+ return True
+
+
+class MetEireannWeatherData:
+ """Keep data for Met Éireann weather entities."""
+
+ def __init__(self, hass, config, weather_data):
+ """Initialise the weather entity data."""
+ self.hass = hass
+ self._config = config
+ self._weather_data = weather_data
+ self.current_weather_data = {}
+ self.daily_forecast = None
+ self.hourly_forecast = None
+
+ async def fetch_data(self):
+ """Fetch data from API - (current weather and forecast)."""
+ await self._weather_data.fetching_data()
+ self.current_weather_data = self._weather_data.get_current_weather()
+ time_zone = dt_util.DEFAULT_TIME_ZONE
+ self.daily_forecast = self._weather_data.get_forecast(time_zone, False)
+ self.hourly_forecast = self._weather_data.get_forecast(time_zone, True)
+ return self
diff --git a/homeassistant/components/met_eireann/config_flow.py b/homeassistant/components/met_eireann/config_flow.py
new file mode 100644
index 00000000000000..6d736b9061a84c
--- /dev/null
+++ b/homeassistant/components/met_eireann/config_flow.py
@@ -0,0 +1,48 @@
+"""Config flow to configure Met Éireann component."""
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+import homeassistant.helpers.config_validation as cv
+
+# pylint:disable=unused-import
+from .const import DOMAIN, HOME_LOCATION_NAME
+
+
+class MetEireannFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Config flow for Met Eireann component."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ errors = {}
+
+ if user_input is not None:
+ # Check if an identical entity is already configured
+ await self.async_set_unique_id(
+ f"{user_input.get(CONF_LATITUDE)},{user_input.get(CONF_LONGITUDE)}"
+ )
+ self._abort_if_unique_id_configured()
+ else:
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_NAME, default=HOME_LOCATION_NAME): str,
+ vol.Required(
+ CONF_LATITUDE, default=self.hass.config.latitude
+ ): cv.latitude,
+ vol.Required(
+ CONF_LONGITUDE, default=self.hass.config.longitude
+ ): cv.longitude,
+ vol.Required(
+ CONF_ELEVATION, default=self.hass.config.elevation
+ ): int,
+ }
+ ),
+ errors=errors,
+ )
+ return self.async_create_entry(title=user_input[CONF_NAME], data=user_input)
diff --git a/homeassistant/components/met_eireann/const.py b/homeassistant/components/met_eireann/const.py
new file mode 100644
index 00000000000000..98d862183c450f
--- /dev/null
+++ b/homeassistant/components/met_eireann/const.py
@@ -0,0 +1,121 @@
+"""Constants for Met Éireann component."""
+import logging
+
+from homeassistant.components.weather import (
+ ATTR_CONDITION_CLEAR_NIGHT,
+ ATTR_CONDITION_CLOUDY,
+ ATTR_CONDITION_FOG,
+ ATTR_CONDITION_LIGHTNING_RAINY,
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_RAINY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_CONDITION_SNOWY_RAINY,
+ ATTR_CONDITION_SUNNY,
+ ATTR_FORECAST_CONDITION,
+ ATTR_FORECAST_PRECIPITATION,
+ ATTR_FORECAST_PRESSURE,
+ ATTR_FORECAST_TEMP,
+ ATTR_FORECAST_TEMP_LOW,
+ ATTR_FORECAST_TIME,
+ ATTR_FORECAST_WIND_BEARING,
+ ATTR_FORECAST_WIND_SPEED,
+ DOMAIN as WEATHER_DOMAIN,
+)
+
+ATTRIBUTION = "Data provided by Met Éireann"
+
+DEFAULT_NAME = "Met Éireann"
+
+DOMAIN = "met_eireann"
+
+HOME_LOCATION_NAME = "Home"
+
+ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_eireann_{HOME_LOCATION_NAME}"
+
+_LOGGER = logging.getLogger(".")
+
+FORECAST_MAP = {
+ ATTR_FORECAST_CONDITION: "condition",
+ ATTR_FORECAST_PRESSURE: "pressure",
+ ATTR_FORECAST_PRECIPITATION: "precipitation",
+ ATTR_FORECAST_TEMP: "temperature",
+ ATTR_FORECAST_TEMP_LOW: "templow",
+ ATTR_FORECAST_TIME: "datetime",
+ ATTR_FORECAST_WIND_BEARING: "wind_bearing",
+ ATTR_FORECAST_WIND_SPEED: "wind_speed",
+}
+
+CONDITION_MAP = {
+ ATTR_CONDITION_CLEAR_NIGHT: ["Dark_Sun"],
+ ATTR_CONDITION_CLOUDY: ["Cloud"],
+ ATTR_CONDITION_FOG: ["Fog"],
+ ATTR_CONDITION_LIGHTNING_RAINY: [
+ "LightRainThunderSun",
+ "LightRainThunderSun",
+ "RainThunder",
+ "SnowThunder",
+ "SleetSunThunder",
+ "Dark_SleetSunThunder",
+ "SnowSunThunder",
+ "Dark_SnowSunThunder",
+ "LightRainThunder",
+ "SleetThunder",
+ "DrizzleThunderSun",
+ "Dark_DrizzleThunderSun",
+ "RainThunderSun",
+ "Dark_RainThunderSun",
+ "LightSleetThunderSun",
+ "Dark_LightSleetThunderSun",
+ "HeavySleetThunderSun",
+ "Dark_HeavySleetThunderSun",
+ "LightSnowThunderSun",
+ "Dark_LightSnowThunderSun",
+ "HeavySnowThunderSun",
+ "Dark_HeavySnowThunderSun",
+ "DrizzleThunder",
+ "LightSleetThunder",
+ "HeavySleetThunder",
+ "LightSnowThunder",
+ "HeavySnowThunder",
+ ],
+ ATTR_CONDITION_PARTLYCLOUDY: [
+ "LightCloud",
+ "Dark_LightCloud",
+ "PartlyCloud",
+ "Dark_PartlyCloud",
+ ],
+ ATTR_CONDITION_RAINY: [
+ "LightRainSun",
+ "Dark_LightRainSun",
+ "LightRain",
+ "Rain",
+ "DrizzleSun",
+ "Dark_DrizzleSun",
+ "RainSun",
+ "Dark_RainSun",
+ "Drizzle",
+ ],
+ ATTR_CONDITION_SNOWY: [
+ "SnowSun",
+ "Dark_SnowSun",
+ "Snow",
+ "LightSnowSun",
+ "Dark_LightSnowSun",
+ "HeavySnowSun",
+ "Dark_HeavySnowSun",
+ "LightSnow",
+ "HeavySnow",
+ ],
+ ATTR_CONDITION_SNOWY_RAINY: [
+ "SleetSun",
+ "Dark_SleetSun",
+ "Sleet",
+ "LightSleetSun",
+ "Dark_LightSleetSun",
+ "HeavySleetSun",
+ "Dark_HeavySleetSun",
+ "LightSleet",
+ "HeavySleet",
+ ],
+ ATTR_CONDITION_SUNNY: "Sun",
+}
diff --git a/homeassistant/components/met_eireann/manifest.json b/homeassistant/components/met_eireann/manifest.json
new file mode 100644
index 00000000000000..5fe6ec51045920
--- /dev/null
+++ b/homeassistant/components/met_eireann/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "met_eireann",
+ "name": "Met Éireann",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/met_eireann",
+ "requirements": ["pyMetEireann==0.2"],
+ "codeowners": ["@DylanGore"]
+}
diff --git a/homeassistant/components/met_eireann/strings.json b/homeassistant/components/met_eireann/strings.json
new file mode 100644
index 00000000000000..687631f2cae6da
--- /dev/null
+++ b/homeassistant/components/met_eireann/strings.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "[%key:common::config_flow::data::location%]",
+ "description": "Enter your location to use weather data from the Met Éireann Public Weather Forecast API",
+ "data": {
+ "name": "[%key:common::config_flow::data::name%]",
+ "latitude": "[%key:common::config_flow::data::latitude%]",
+ "longitude": "[%key:common::config_flow::data::longitude%]",
+ "elevation": "[%key:common::config_flow::data::elevation%]"
+ }
+ }
+ },
+ "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" }
+ }
+}
diff --git a/homeassistant/components/met_eireann/translations/ca.json b/homeassistant/components/met_eireann/translations/ca.json
new file mode 100644
index 00000000000000..6a694a73c67109
--- /dev/null
+++ b/homeassistant/components/met_eireann/translations/ca.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "El servei ja est\u00e0 configurat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "elevation": "Altitud",
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "name": "Nom"
+ },
+ "description": "Introdueix la treva ubicaci\u00f3 per utilitzar les dades meteorol\u00f2giques de l'API p\u00fablica de previsi\u00f3 meteorol\u00f2gica de Met \u00c9ireann",
+ "title": "Ubicaci\u00f3"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met_eireann/translations/de.json b/homeassistant/components/met_eireann/translations/de.json
new file mode 100644
index 00000000000000..0d979ed800b95c
--- /dev/null
+++ b/homeassistant/components/met_eireann/translations/de.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Der Dienst ist bereits konfiguriert"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "elevation": "H\u00f6he",
+ "latitude": "Breitengrad",
+ "longitude": "L\u00e4ngengrad",
+ "name": "Name"
+ },
+ "title": "Standort"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met_eireann/translations/en.json b/homeassistant/components/met_eireann/translations/en.json
new file mode 100644
index 00000000000000..f01586a15a7e6d
--- /dev/null
+++ b/homeassistant/components/met_eireann/translations/en.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Service is already configured"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "elevation": "Elevation",
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "name": "Name"
+ },
+ "description": "Enter your location to use weather data from the Met \u00c9ireann Public Weather Forecast API",
+ "title": "Location"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met_eireann/translations/et.json b/homeassistant/components/met_eireann/translations/et.json
new file mode 100644
index 00000000000000..48646b03049d53
--- /dev/null
+++ b/homeassistant/components/met_eireann/translations/et.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Teenus on juba seadistatud"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "elevation": "K\u00f5rgus merepinnast",
+ "latitude": "Laiuskraad",
+ "longitude": "Pikkuskraad",
+ "name": "Nimi"
+ },
+ "description": "Met \u00c9ireanni avaliku ilmaprognoosi API ilmaandmete kasutamiseks sisesta oma asukoht",
+ "title": "Asukoht"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met_eireann/translations/hu.json b/homeassistant/components/met_eireann/translations/hu.json
new file mode 100644
index 00000000000000..65108e183a94af
--- /dev/null
+++ b/homeassistant/components/met_eireann/translations/hu.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "elevation": "Magass\u00e1g",
+ "latitude": "Sz\u00e9less\u00e9g",
+ "longitude": "Hossz\u00fas\u00e1g",
+ "name": "N\u00e9v"
+ },
+ "title": "Elhelyezked\u00e9s"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met_eireann/translations/it.json b/homeassistant/components/met_eireann/translations/it.json
new file mode 100644
index 00000000000000..2d89c6983af5da
--- /dev/null
+++ b/homeassistant/components/met_eireann/translations/it.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "elevation": "Altitudine",
+ "latitude": "Latitudine",
+ "longitude": "Logitudine",
+ "name": "Nome"
+ },
+ "description": "Inserisci la tua posizione per utilizzare i dati meteorologici dall'API Met \u00c9ireann Public Weather Forecast",
+ "title": "Posizione"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met_eireann/translations/ko.json b/homeassistant/components/met_eireann/translations/ko.json
new file mode 100644
index 00000000000000..d0adc5f4addb06
--- /dev/null
+++ b/homeassistant/components/met_eireann/translations/ko.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "elevation": "\uace0\ub3c4",
+ "latitude": "\uc704\ub3c4",
+ "longitude": "\uacbd\ub3c4",
+ "name": "\uc774\ub984"
+ },
+ "description": "Met \u00c9ireann \uacf5\uacf5 \uae30\uc0c1\uc608\ubcf4 API\uc5d0\uc11c \ub0a0\uc528 \ub370\uc774\ud130\ub97c \uc0ac\uc6a9\ud560 \uc704\uce58\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "\uc704\uce58"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met_eireann/translations/nl.json b/homeassistant/components/met_eireann/translations/nl.json
new file mode 100644
index 00000000000000..b67c167ca8d39a
--- /dev/null
+++ b/homeassistant/components/met_eireann/translations/nl.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Service is al geconfigureerd"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "elevation": "Hoogte",
+ "latitude": "Breedtegraad",
+ "longitude": "Lengtegraad",
+ "name": "Naam"
+ },
+ "description": "Voer uw locatie in om weergegevens van de Met \u00c9ireann Public Weather Forecast API te gebruiken",
+ "title": "Locatie"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met_eireann/translations/no.json b/homeassistant/components/met_eireann/translations/no.json
new file mode 100644
index 00000000000000..307efb3a1b0b25
--- /dev/null
+++ b/homeassistant/components/met_eireann/translations/no.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Tjenesten er allerede konfigurert"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "elevation": "Elevasjon",
+ "latitude": "Breddegrad",
+ "longitude": "Lengdegrad",
+ "name": "Navn"
+ },
+ "description": "Skriv inn posisjonen din for \u00e5 bruke v\u00e6rdata fra Met \u00c9ireann Public Weather Forecast API",
+ "title": "Plassering"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met_eireann/translations/pl.json b/homeassistant/components/met_eireann/translations/pl.json
new file mode 100644
index 00000000000000..888017b790bb60
--- /dev/null
+++ b/homeassistant/components/met_eireann/translations/pl.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "elevation": "Wysoko\u015b\u0107",
+ "latitude": "Szeroko\u015b\u0107 geograficzna",
+ "longitude": "D\u0142ugo\u015b\u0107 geograficzna",
+ "name": "Nazwa"
+ },
+ "description": "Wprowad\u017a swoj\u0105 lokalizacj\u0119, aby korzysta\u0107 z danych pogodowych z API Met \u00c9ireann Public Weather Forecast",
+ "title": "Lokalizacja"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met_eireann/translations/ru.json b/homeassistant/components/met_eireann/translations/ru.json
new file mode 100644
index 00000000000000..de121b259668f6
--- /dev/null
+++ b/homeassistant/components/met_eireann/translations/ru.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "elevation": "\u0412\u044b\u0441\u043e\u0442\u0430",
+ "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": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u043e \u043f\u043e\u0433\u043e\u0434\u0435 \u0438\u0437 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e API Met \u00c9ireann.",
+ "title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met_eireann/translations/zh-Hant.json b/homeassistant/components/met_eireann/translations/zh-Hant.json
new file mode 100644
index 00000000000000..5e7bc04e24c9ee
--- /dev/null
+++ b/homeassistant/components/met_eireann/translations/zh-Hant.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "elevation": "\u6d77\u62d4",
+ "latitude": "\u7def\u5ea6",
+ "longitude": "\u7d93\u5ea6",
+ "name": "\u540d\u7a31"
+ },
+ "description": "\u8f38\u5165\u5ea7\u6a19\u4ee5\u4f7f\u7528 Met \u00c9ireann Public Weather Forecast API \u5929\u6c23\u8cc7\u6599",
+ "title": "\u5ea7\u6a19"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py
new file mode 100644
index 00000000000000..190da06f3d9576
--- /dev/null
+++ b/homeassistant/components/met_eireann/weather.py
@@ -0,0 +1,191 @@
+"""Support for Met Éireann weather service."""
+import logging
+
+from homeassistant.components.weather import (
+ ATTR_FORECAST_CONDITION,
+ ATTR_FORECAST_PRECIPITATION,
+ ATTR_FORECAST_TEMP,
+ ATTR_FORECAST_TIME,
+ WeatherEntity,
+)
+from homeassistant.const import (
+ CONF_LATITUDE,
+ CONF_LONGITUDE,
+ CONF_NAME,
+ LENGTH_INCHES,
+ LENGTH_METERS,
+ LENGTH_MILES,
+ LENGTH_MILLIMETERS,
+ PRESSURE_HPA,
+ PRESSURE_INHG,
+ TEMP_CELSIUS,
+)
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+from homeassistant.util import dt as dt_util
+from homeassistant.util.distance import convert as convert_distance
+from homeassistant.util.pressure import convert as convert_pressure
+
+from .const import ATTRIBUTION, CONDITION_MAP, DEFAULT_NAME, DOMAIN, FORECAST_MAP
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def format_condition(condition: str):
+ """Map the conditions provided by the weather API to those supported by the frontend."""
+ if condition is not None:
+ for key, value in CONDITION_MAP.items():
+ if condition in value:
+ return key
+ return condition
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Add a weather entity from a config_entry."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]
+ async_add_entities(
+ [
+ MetEireannWeather(
+ coordinator, config_entry.data, hass.config.units.is_metric, False
+ ),
+ MetEireannWeather(
+ coordinator, config_entry.data, hass.config.units.is_metric, True
+ ),
+ ]
+ )
+
+
+class MetEireannWeather(CoordinatorEntity, WeatherEntity):
+ """Implementation of a Met Éireann weather condition."""
+
+ def __init__(self, coordinator, config, is_metric, hourly):
+ """Initialise the platform with a data instance and site."""
+ super().__init__(coordinator)
+ self._config = config
+ self._is_metric = is_metric
+ self._hourly = hourly
+
+ @property
+ def unique_id(self):
+ """Return unique ID."""
+ name_appendix = ""
+ if self._hourly:
+ name_appendix = "-hourly"
+
+ return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}{name_appendix}"
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ name = self._config.get(CONF_NAME)
+ name_appendix = ""
+ if self._hourly:
+ name_appendix = " Hourly"
+
+ if name is not None:
+ return f"{name}{name_appendix}"
+
+ return f"{DEFAULT_NAME}{name_appendix}"
+
+ @property
+ def entity_registry_enabled_default(self) -> bool:
+ """Return if the entity should be enabled when first added to the entity registry."""
+ return not self._hourly
+
+ @property
+ def condition(self):
+ """Return the current condition."""
+ return format_condition(
+ self.coordinator.data.current_weather_data.get("condition")
+ )
+
+ @property
+ def temperature(self):
+ """Return the temperature."""
+ return self.coordinator.data.current_weather_data.get("temperature")
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement."""
+ return TEMP_CELSIUS
+
+ @property
+ def pressure(self):
+ """Return the pressure."""
+ pressure_hpa = self.coordinator.data.current_weather_data.get("pressure")
+ if self._is_metric or pressure_hpa is None:
+ return pressure_hpa
+
+ return round(convert_pressure(pressure_hpa, PRESSURE_HPA, PRESSURE_INHG), 2)
+
+ @property
+ def humidity(self):
+ """Return the humidity."""
+ return self.coordinator.data.current_weather_data.get("humidity")
+
+ @property
+ def wind_speed(self):
+ """Return the wind speed."""
+ speed_m_s = self.coordinator.data.current_weather_data.get("wind_speed")
+ if self._is_metric or speed_m_s is None:
+ return speed_m_s
+
+ speed_mi_s = convert_distance(speed_m_s, LENGTH_METERS, LENGTH_MILES)
+ speed_mi_h = speed_mi_s / 3600.0
+ return int(round(speed_mi_h))
+
+ @property
+ def wind_bearing(self):
+ """Return the wind direction."""
+ return self.coordinator.data.current_weather_data.get("wind_bearing")
+
+ @property
+ def attribution(self):
+ """Return the attribution."""
+ return ATTRIBUTION
+
+ @property
+ def forecast(self):
+ """Return the forecast array."""
+ if self._hourly:
+ me_forecast = self.coordinator.data.hourly_forecast
+ else:
+ me_forecast = self.coordinator.data.daily_forecast
+ required_keys = {ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME}
+
+ ha_forecast = []
+
+ for item in me_forecast:
+ if not set(item).issuperset(required_keys):
+ continue
+ ha_item = {
+ k: item[v] for k, v in FORECAST_MAP.items() if item.get(v) is not None
+ }
+ if not self._is_metric and ATTR_FORECAST_PRECIPITATION in ha_item:
+ precip_inches = convert_distance(
+ ha_item[ATTR_FORECAST_PRECIPITATION],
+ LENGTH_MILLIMETERS,
+ LENGTH_INCHES,
+ )
+ ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2)
+ if ha_item.get(ATTR_FORECAST_CONDITION):
+ ha_item[ATTR_FORECAST_CONDITION] = format_condition(
+ ha_item[ATTR_FORECAST_CONDITION]
+ )
+ # Convert timestamp to UTC
+ if ha_item.get(ATTR_FORECAST_TIME):
+ ha_item[ATTR_FORECAST_TIME] = dt_util.as_utc(
+ ha_item.get(ATTR_FORECAST_TIME)
+ ).isoformat()
+ ha_forecast.append(ha_item)
+ return ha_forecast
+
+ @property
+ def device_info(self):
+ """Device info."""
+ return {
+ "identifiers": {(DOMAIN,)},
+ "manufacturer": "Met Éireann",
+ "model": "Forecast",
+ "default_name": "Forecast",
+ "entry_type": "service",
+ }
diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py
index 3034135f847453..1229a4e43affaa 100644
--- a/homeassistant/components/meteo_france/__init__.py
+++ b/homeassistant/components/meteo_france/__init__.py
@@ -159,7 +159,7 @@ async def _async_update_data_alert():
)
else:
_LOGGER.warning(
- "Weather alert not available: The city %s is not in metropolitan France or Andorre.",
+ "Weather alert not available: The city %s is not in metropolitan France or Andorre",
entry.title,
)
@@ -189,7 +189,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
].data.position.get("dept")
hass.data[DOMAIN][department] = False
_LOGGER.debug(
- "Weather alert for depatment %s unloaded and released. It can be added now by another city.",
+ "Weather alert for depatment %s unloaded and released. It can be added now by another city",
department,
)
diff --git a/homeassistant/components/meteo_france/config_flow.py b/homeassistant/components/meteo_france/config_flow.py
index f4d7c5dccfae2c..2ea9ed0568cba4 100644
--- a/homeassistant/components/meteo_france/config_flow.py
+++ b/homeassistant/components/meteo_france/config_flow.py
@@ -9,8 +9,7 @@
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE
from homeassistant.core import callback
-from .const import CONF_CITY, FORECAST_MODE, FORECAST_MODE_DAILY
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import CONF_CITY, DOMAIN, FORECAST_MODE, FORECAST_MODE_DAILY
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json
index 8de4e76c6f6ac5..6ffcda29229972 100644
--- a/homeassistant/components/meteo_france/manifest.json
+++ b/homeassistant/components/meteo_france/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/meteo_france",
"requirements": [
- "meteofrance-api==1.0.1"
+ "meteofrance-api==1.0.2"
],
"codeowners": [
"@hacf-fr",
diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py
index 201cca7ae9daf6..b6ec221a97e92a 100644
--- a/homeassistant/components/meteo_france/sensor.py
+++ b/homeassistant/components/meteo_france/sensor.py
@@ -6,6 +6,7 @@
readeable_phenomenoms_dict,
)
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.helpers.typing import HomeAssistantType
@@ -74,7 +75,7 @@ async def async_setup_entry(
)
-class MeteoFranceSensor(CoordinatorEntity):
+class MeteoFranceSensor(CoordinatorEntity, SensorEntity):
"""Representation of a Meteo-France sensor."""
def __init__(self, sensor_type: str, coordinator: DataUpdateCoordinator):
@@ -154,7 +155,7 @@ def entity_registry_enabled_default(self) -> bool:
return SENSOR_TYPES[self._type][ENTITY_ENABLE]
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
@@ -177,7 +178,7 @@ def state(self):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
reference_dt = self.coordinator.data.forecast[0]["dt"]
return {
@@ -193,7 +194,6 @@ def device_state_attributes(self):
class MeteoFranceAlertSensor(MeteoFranceSensor):
"""Representation of a Meteo-France alert sensor."""
- # pylint: disable=super-init-not-called
def __init__(self, sensor_type: str, coordinator: DataUpdateCoordinator):
"""Initialize the Meteo-France sensor."""
super().__init__(sensor_type, coordinator)
@@ -209,7 +209,7 @@ def state(self):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
**readeable_phenomenoms_dict(self.coordinator.data.phenomenons_max_colors),
diff --git a/homeassistant/components/meteo_france/translations/de.json b/homeassistant/components/meteo_france/translations/de.json
index 65313f16c41117..74637594d5ff1c 100644
--- a/homeassistant/components/meteo_france/translations/de.json
+++ b/homeassistant/components/meteo_france/translations/de.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Stadt bereits konfiguriert",
- "unknown": "Unbekannter Fehler: Bitte versuchen Sie es sp\u00e4ter erneut"
+ "already_configured": "Standort ist bereits konfiguriert",
+ "unknown": "Unerwarteter Fehler"
},
"error": {
"empty": "Kein Ergebnis bei der Stadtsuche: Bitte \u00fcberpr\u00fcfe das Stadtfeld"
diff --git a/homeassistant/components/meteo_france/translations/hu.json b/homeassistant/components/meteo_france/translations/hu.json
index dc74eafa409fde..112f70b6ea6dc4 100644
--- a/homeassistant/components/meteo_france/translations/hu.json
+++ b/homeassistant/components/meteo_france/translations/hu.json
@@ -1,13 +1,20 @@
{
"config": {
"abort": {
- "already_configured": "A v\u00e1ros m\u00e1r konfigur\u00e1lva van",
- "unknown": "Ismeretlen hiba: k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb"
+ "already_configured": "A hely m\u00e1r konfigur\u00e1lva van",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"error": {
"empty": "Nincs eredm\u00e9ny a v\u00e1roskeres\u00e9sben: ellen\u0151rizze a v\u00e1ros mez\u0151t"
},
"step": {
+ "cities": {
+ "data": {
+ "city": "V\u00e1ros"
+ },
+ "description": "V\u00e1laszd ki a v\u00e1rost a list\u00e1b\u00f3l",
+ "title": "M\u00e9t\u00e9o-France"
+ },
"user": {
"data": {
"city": "V\u00e1ros"
@@ -16,5 +23,14 @@
"title": "M\u00e9t\u00e9o-France"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "mode": "El\u0151rejelz\u00e9si m\u00f3d"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/meteo_france/translations/id.json b/homeassistant/components/meteo_france/translations/id.json
new file mode 100644
index 00000000000000..07d8450e873b33
--- /dev/null
+++ b/homeassistant/components/meteo_france/translations/id.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Lokasi sudah dikonfigurasi",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "error": {
+ "empty": "Tidak ada hasil dalam penelusuran kota: periksa bidang isian kota"
+ },
+ "step": {
+ "cities": {
+ "data": {
+ "city": "Kota"
+ },
+ "description": "Pilih kota Anda dari daftar",
+ "title": "M\u00e9t\u00e9o-France"
+ },
+ "user": {
+ "data": {
+ "city": "Kota"
+ },
+ "description": "Masukkan kode pos (hanya untuk Prancis, disarankan) atau nama kota",
+ "title": "M\u00e9t\u00e9o-France"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "mode": "Mode prakiraan"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteo_france/translations/ko.json b/homeassistant/components/meteo_france/translations/ko.json
index 4b8dc3204dd57a..83cda0e4dcf394 100644
--- a/homeassistant/components/meteo_france/translations/ko.json
+++ b/homeassistant/components/meteo_france/translations/ko.json
@@ -1,14 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "\ub3c4\uc2dc\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694"
+ "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
- "empty": "\ub3c4\uc2dc \uac80\uc0c9 \uacb0\uacfc \uc5c6\uc74c: \ub3c4\uc2dc \ud544\ub4dc\ub97c \ud655\uc778\ud558\uc2ed\uc2dc\uc624."
+ "empty": "\ub3c4\uc2dc \uac80\uc0c9 \uacb0\uacfc\uac00 \uc5c6\uc2b5\ub2c8\ub2e4. \ub3c4\uc2dc \ud544\ub4dc\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694."
},
"step": {
"cities": {
+ "data": {
+ "city": "\ub3c4\uc2dc"
+ },
+ "description": "\ubaa9\ub85d\uc5d0\uc11c \ub3c4\uc2dc\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694",
"title": "\ud504\ub791\uc2a4 \uae30\uc0c1\uccad (M\u00e9t\u00e9o-France)"
},
"user": {
diff --git a/homeassistant/components/meteo_france/translations/nl.json b/homeassistant/components/meteo_france/translations/nl.json
index 27dfb56f8d78d3..f69db3ed47e6b7 100644
--- a/homeassistant/components/meteo_france/translations/nl.json
+++ b/homeassistant/components/meteo_france/translations/nl.json
@@ -1,10 +1,20 @@
{
"config": {
"abort": {
- "already_configured": "Stad al geconfigureerd",
- "unknown": "Onbekende fout: probeer het later nog eens"
+ "already_configured": "Locatie is al geconfigureerd.",
+ "unknown": "Onverwachte fout"
+ },
+ "error": {
+ "empty": "Geen resultaat bij het zoeken naar een stad: controleer de invoer: stad"
},
"step": {
+ "cities": {
+ "data": {
+ "city": "Stad"
+ },
+ "description": "Kies uw stad uit de lijst",
+ "title": "M\u00e9t\u00e9o-France"
+ },
"user": {
"data": {
"city": "Stad"
@@ -13,5 +23,14 @@
"title": "M\u00e9t\u00e9o-France"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "mode": "Voorspellingsmodus"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/meteo_france/translations/tr.json b/homeassistant/components/meteo_france/translations/tr.json
index 57fc9f768815fb..59c3886a90055a 100644
--- a/homeassistant/components/meteo_france/translations/tr.json
+++ b/homeassistant/components/meteo_france/translations/tr.json
@@ -1,7 +1,28 @@
{
"config": {
+ "abort": {
+ "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "unknown": "Beklenmeyen hata"
+ },
"error": {
"empty": "\u015eehir aramas\u0131nda sonu\u00e7 yok: l\u00fctfen \u015fehir alan\u0131n\u0131 kontrol edin"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "city": "\u015eehir"
+ },
+ "title": "M\u00e9t\u00e9o-Fransa"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "mode": "Tahmin modu"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/meteo_france/translations/uk.json b/homeassistant/components/meteo_france/translations/uk.json
new file mode 100644
index 00000000000000..a84c230e21886b
--- /dev/null
+++ b/homeassistant/components/meteo_france/translations/uk.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "error": {
+ "empty": "\u041d\u0435\u043c\u0430\u0454 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0456\u0432 \u043f\u043e\u0448\u0443\u043a\u0443. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043f\u043e\u043b\u0435 \"\u041c\u0456\u0441\u0442\u043e\"."
+ },
+ "step": {
+ "cities": {
+ "data": {
+ "city": "\u041c\u0456\u0441\u0442\u043e"
+ },
+ "description": "\u041e\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u0456\u0441\u0442\u043e \u0437\u0456 \u0441\u043f\u0438\u0441\u043a\u0443",
+ "title": "M\u00e9t\u00e9o-France"
+ },
+ "user": {
+ "data": {
+ "city": "\u041c\u0456\u0441\u0442\u043e"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441 (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0454\u0442\u044c\u0441\u044f \u0442\u0456\u043b\u044c\u043a\u0438 \u0434\u043b\u044f \u0424\u0440\u0430\u043d\u0446\u0456\u0457) \u0430\u0431\u043e \u043d\u0430\u0437\u0432\u0443 \u043c\u0456\u0441\u0442\u0430",
+ "title": "M\u00e9t\u00e9o-France"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "mode": "\u0420\u0435\u0436\u0438\u043c \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0443"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py
index 09e062cc715632..08d5c1c4f6ad1a 100644
--- a/homeassistant/components/meteo_france/weather.py
+++ b/homeassistant/components/meteo_france/weather.py
@@ -59,7 +59,7 @@ async def async_setup_entry(
True,
)
_LOGGER.debug(
- "Weather entity (%s) added for %s.",
+ "Weather entity (%s) added for %s",
entry.options.get(CONF_MODE, FORECAST_MODE_DAILY),
coordinator.data.position["name"],
)
diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py
index 6b13d03ebbad44..6d237c696f675b 100644
--- a/homeassistant/components/meteoalarm/binary_sensor.py
+++ b/homeassistant/components/meteoalarm/binary_sensor.py
@@ -73,7 +73,7 @@ def is_on(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
self._attributes[ATTR_ATTRIBUTION] = ATTRIBUTION
return self._attributes
diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py
index 8a68646240a412..87a5488fe0196b 100644
--- a/homeassistant/components/metoffice/__init__.py
+++ b/homeassistant/components/metoffice/__init__.py
@@ -61,9 +61,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
if metoffice_data.now is None:
raise ConfigEntryNotReady()
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -74,8 +74,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/metoffice/config_flow.py b/homeassistant/components/metoffice/config_flow.py
index b71c3de67e3d00..7dd3788f8b7ef9 100644
--- a/homeassistant/components/metoffice/config_flow.py
+++ b/homeassistant/components/metoffice/config_flow.py
@@ -7,7 +7,7 @@
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.helpers import config_validation as cv
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import DOMAIN
from .data import MetOfficeData
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py
index aed763ca4a415c..6a7cf5254a63e9 100644
--- a/homeassistant/components/metoffice/sensor.py
+++ b/homeassistant/components/metoffice/sensor.py
@@ -1,4 +1,5 @@
"""Support for UK Met Office weather service."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
DEVICE_CLASS_HUMIDITY,
@@ -10,7 +11,6 @@
UV_INDEX,
)
from homeassistant.core import callback
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from .const import (
@@ -92,7 +92,7 @@ async def async_setup_entry(
)
-class MetOfficeCurrentSensor(Entity):
+class MetOfficeCurrentSensor(SensorEntity):
"""Implementation of a Met Office current weather condition sensor."""
def __init__(self, entry_data, hass_data, sensor_type):
@@ -171,7 +171,7 @@ def device_class(self):
return SENSOR_TYPES[self._type][1]
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/metoffice/translations/de.json b/homeassistant/components/metoffice/translations/de.json
index 74c204b96838e0..8f35c2aaeaaa9c 100644
--- a/homeassistant/components/metoffice/translations/de.json
+++ b/homeassistant/components/metoffice/translations/de.json
@@ -4,6 +4,7 @@
"already_configured": "Service ist bereits konfiguriert"
},
"error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
"unknown": "Unerwarteter Fehler"
},
"step": {
@@ -12,7 +13,9 @@
"api_key": "API Key",
"latitude": "Breitengrad",
"longitude": "L\u00e4ngengrad"
- }
+ },
+ "description": "Der Breiten- und L\u00e4ngengrad wird verwendet, um die n\u00e4chstgelegene Wetterstation zu finden.",
+ "title": "Mit UK Met Office verbinden"
}
}
}
diff --git a/homeassistant/components/metoffice/translations/he.json b/homeassistant/components/metoffice/translations/he.json
new file mode 100644
index 00000000000000..4c49313d97741a
--- /dev/null
+++ b/homeassistant/components/metoffice/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/metoffice/translations/hu.json b/homeassistant/components/metoffice/translations/hu.json
index 3b2d79a34a77e2..350e6f92f32910 100644
--- a/homeassistant/components/metoffice/translations/hu.json
+++ b/homeassistant/components/metoffice/translations/hu.json
@@ -1,7 +1,21 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API kulcs",
+ "latitude": "Sz\u00e9less\u00e9g",
+ "longitude": "Hossz\u00fas\u00e1g"
+ },
+ "title": "Csatlakoz\u00e1s a UK Met Office-hoz"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/metoffice/translations/id.json b/homeassistant/components/metoffice/translations/id.json
new file mode 100644
index 00000000000000..d9bb784a99fd4c
--- /dev/null
+++ b/homeassistant/components/metoffice/translations/id.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kunci API",
+ "latitude": "Lintang",
+ "longitude": "Bujur"
+ },
+ "description": "Lintang dan bujur akan digunakan untuk menemukan stasiun cuaca terdekat.",
+ "title": "Hubungkan ke the UK Met Office"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/metoffice/translations/ko.json b/homeassistant/components/metoffice/translations/ko.json
index b1af2afaf30ccf..b2f09a4a9e5a0f 100644
--- a/homeassistant/components/metoffice/translations/ko.json
+++ b/homeassistant/components/metoffice/translations/ko.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
@@ -10,7 +10,7 @@
"step": {
"user": {
"data": {
- "api_key": "\uc601\uad6d \uae30\uc0c1\uccad DataPoint API \ud0a4",
+ "api_key": "API \ud0a4",
"latitude": "\uc704\ub3c4",
"longitude": "\uacbd\ub3c4"
},
diff --git a/homeassistant/components/metoffice/translations/nl.json b/homeassistant/components/metoffice/translations/nl.json
index 1b3063459c461b..a6ba36f07aff18 100644
--- a/homeassistant/components/metoffice/translations/nl.json
+++ b/homeassistant/components/metoffice/translations/nl.json
@@ -14,6 +14,7 @@
"latitude": "Breedtegraad",
"longitude": "Lengtegraad"
},
+ "description": "De lengte- en breedtegraad worden gebruikt om het dichtstbijzijnde weerstation te vinden.",
"title": "Maak verbinding met het UK Met Office"
}
}
diff --git a/homeassistant/components/metoffice/translations/tr.json b/homeassistant/components/metoffice/translations/tr.json
new file mode 100644
index 00000000000000..55064a139ef169
--- /dev/null
+++ b/homeassistant/components/metoffice/translations/tr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Anahtar\u0131",
+ "latitude": "Enlem",
+ "longitude": "Boylam"
+ },
+ "description": "Enlem ve boylam, en yak\u0131n hava istasyonunu bulmak i\u00e7in kullan\u0131lacakt\u0131r."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/metoffice/translations/uk.json b/homeassistant/components/metoffice/translations/uk.json
new file mode 100644
index 00000000000000..53ab2115e82e32
--- /dev/null
+++ b/homeassistant/components/metoffice/translations/uk.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
+ "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430"
+ },
+ "description": "\u0428\u0438\u0440\u043e\u0442\u0430 \u0456 \u0434\u043e\u0432\u0433\u043e\u0442\u0430 \u0431\u0443\u0434\u0443\u0442\u044c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u0456 \u0434\u043b\u044f \u043f\u043e\u0448\u0443\u043a\u0443 \u043d\u0430\u0439\u0431\u043b\u0438\u0436\u0447\u043e\u0457 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0456\u0457.",
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Met Office UK"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mfi/sensor.py b/homeassistant/components/mfi/sensor.py
index 671a52bbf01635..c7a64f17bd68a4 100644
--- a/homeassistant/components/mfi/sensor.py
+++ b/homeassistant/components/mfi/sensor.py
@@ -5,7 +5,7 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
@@ -18,7 +18,6 @@
TEMP_CELSIUS,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -74,7 +73,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
)
-class MfiSensor(Entity):
+class MfiSensor(SensorEntity):
"""Representation of a mFi sensor."""
def __init__(self, port, hass):
diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py
index 21963140547f14..150f81298cda90 100644
--- a/homeassistant/components/mfi/switch.py
+++ b/homeassistant/components/mfi/switch.py
@@ -107,7 +107,7 @@ def current_power_w(self):
return int(self._port.data.get("active_pwr", 0))
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes for the device."""
return {
"volts": round(self._port.data.get("v_rms", 0), 1),
diff --git a/homeassistant/components/mhz19/sensor.py b/homeassistant/components/mhz19/sensor.py
index e77f17c9140c65..0f0735dd5dadb6 100644
--- a/homeassistant/components/mhz19/sensor.py
+++ b/homeassistant/components/mhz19/sensor.py
@@ -5,7 +5,7 @@
from pmsensor import co2sensor
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_TEMPERATURE,
CONCENTRATION_PARTS_PER_MILLION,
@@ -14,7 +14,6 @@
TEMP_FAHRENHEIT,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from homeassistant.util.temperature import celsius_to_fahrenheit
@@ -69,7 +68,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return True
-class MHZ19Sensor(Entity):
+class MHZ19Sensor(SensorEntity):
"""Representation of an CO2 sensor."""
def __init__(self, mhz_client, sensor_type, temp_unit, name):
@@ -107,7 +106,7 @@ def update(self):
self._ppm = data.get(SENSOR_CO2)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
result = {}
if self._sensor_type == SENSOR_TEMPERATURE and self._ppm is not None:
diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py
index c349589ebdd84d..1e1c088b351d5b 100644
--- a/homeassistant/components/microsoft/tts.py
+++ b/homeassistant/components/microsoft/tts.py
@@ -6,7 +6,7 @@
import voluptuous as vol
from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider
-from homeassistant.const import CONF_API_KEY, CONF_TYPE, PERCENTAGE
+from homeassistant.const import CONF_API_KEY, CONF_REGION, CONF_TYPE, PERCENTAGE
import homeassistant.helpers.config_validation as cv
CONF_GENDER = "gender"
@@ -15,8 +15,6 @@
CONF_VOLUME = "volume"
CONF_PITCH = "pitch"
CONF_CONTOUR = "contour"
-CONF_REGION = "region"
-
_LOGGER = logging.getLogger(__name__)
SUPPORTED_LANGUAGES = [
@@ -56,6 +54,7 @@
"ro-ro",
"ru-ru",
"sk-sk",
+ "sl-si",
"sv-se",
"th-th",
"tr-tr",
diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py
index 69a738724c3225..9f7131d1935b70 100644
--- a/homeassistant/components/microsoft_face/__init__.py
+++ b/homeassistant/components/microsoft_face/__init__.py
@@ -231,7 +231,7 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
attr = {}
for name, p_id in self._api.store[self._id].items():
@@ -275,7 +275,9 @@ async def update_store(self):
for person in persons:
self._store[g_id][person["name"]] = person["personId"]
- tasks.append(self._entities[g_id].async_update_ha_state())
+ tasks.append(
+ asyncio.create_task(self._entities[g_id].async_update_ha_state())
+ )
if tasks:
await asyncio.wait(tasks)
diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py
index 6583f5f7e0ca2e..0c22a943bb35b4 100644
--- a/homeassistant/components/miflora/sensor.py
+++ b/homeassistant/components/miflora/sensor.py
@@ -5,10 +5,10 @@
import btlewrap
from btlewrap import BluetoothBackendException
-from miflora import miflora_poller # pylint: disable=import-error
+from miflora import miflora_poller
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONDUCTIVITY,
CONF_FORCE_UPDATE,
@@ -27,7 +27,6 @@
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
from homeassistant.util.temperature import celsius_to_fahrenheit
@@ -130,7 +129,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(devs)
-class MiFloraSensor(Entity):
+class MiFloraSensor(SensorEntity):
"""Implementing the MiFlora sensor."""
def __init__(
@@ -189,7 +188,7 @@ def available(self):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
return {ATTR_LAST_SUCCESSFUL_UPDATE: self.last_successful_update}
diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py
index b9e0b051aba584..025eff8d07a3b7 100644
--- a/homeassistant/components/mikrotik/device_tracker.py
+++ b/homeassistant/components/mikrotik/device_tracker.py
@@ -123,7 +123,7 @@ def available(self) -> bool:
return self.hub.available
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
if self.is_connected:
return {k: v for k, v in self.device.attrs.items() if k not in FILTER_ATTRS}
diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py
index 28a78d0ee1a448..2f1f89ba60da34 100644
--- a/homeassistant/components/mikrotik/hub.py
+++ b/homeassistant/components/mikrotik/hub.py
@@ -276,9 +276,8 @@ def command(self, cmd, params=None):
def update(self):
"""Update device_tracker from Mikrotik API."""
- if not self.available or not self.api:
- if not self.connect_to_hub():
- return
+ if (not self.available or not self.api) and not self.connect_to_hub():
+ return
_LOGGER.debug("updating network devices for host: %s", self._host)
self.update_devices()
diff --git a/homeassistant/components/mikrotik/translations/de.json b/homeassistant/components/mikrotik/translations/de.json
index 4211077c82ce26..82ea47dc4bf48c 100644
--- a/homeassistant/components/mikrotik/translations/de.json
+++ b/homeassistant/components/mikrotik/translations/de.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Mikrotik ist bereits konfiguriert"
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
"cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"name_exists": "Name vorhanden"
},
"step": {
@@ -25,7 +26,7 @@
"step": {
"device_tracker": {
"data": {
- "arp_ping": "ARP Ping aktivieren",
+ "arp_ping": "ARP-Ping aktivieren",
"force_dhcp": "Erzwingen Sie das Scannen \u00fcber DHCP"
}
}
diff --git a/homeassistant/components/mikrotik/translations/he.json b/homeassistant/components/mikrotik/translations/he.json
new file mode 100644
index 00000000000000..ac90b3264eab33
--- /dev/null
+++ b/homeassistant/components/mikrotik/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mikrotik/translations/hu.json b/homeassistant/components/mikrotik/translations/hu.json
index 67a2e8d8fc3455..248884f9687ab8 100644
--- a/homeassistant/components/mikrotik/translations/hu.json
+++ b/homeassistant/components/mikrotik/translations/hu.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "A Mikrotik m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
},
"error": {
- "cannot_connect": "A kapcsolat sikertelen",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik"
},
"step": {
diff --git a/homeassistant/components/mikrotik/translations/id.json b/homeassistant/components/mikrotik/translations/id.json
new file mode 100644
index 00000000000000..3ef0dacb763217
--- /dev/null
+++ b/homeassistant/components/mikrotik/translations/id.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "name_exists": "Nama sudah ada"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nama",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "username": "Nama Pengguna",
+ "verify_ssl": "Gunakan SSL"
+ },
+ "title": "Siapkan Router Mikrotik"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "device_tracker": {
+ "data": {
+ "arp_ping": "Aktifkan ping ARP",
+ "detection_time": "Pertimbangkan interval rumah",
+ "force_dhcp": "Paksa pemindaian menggunakan DHCP"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mikrotik/translations/ko.json b/homeassistant/components/mikrotik/translations/ko.json
index f32e2260501c2a..05a8f50066c5d6 100644
--- a/homeassistant/components/mikrotik/translations/ko.json
+++ b/homeassistant/components/mikrotik/translations/ko.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Mikrotik \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4"
},
"step": {
diff --git a/homeassistant/components/mikrotik/translations/nl.json b/homeassistant/components/mikrotik/translations/nl.json
index 53e05b5cf5f8ea..78e143ddadbb31 100644
--- a/homeassistant/components/mikrotik/translations/nl.json
+++ b/homeassistant/components/mikrotik/translations/nl.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Mikrotik is al geconfigureerd"
+ "already_configured": "Apparaat is al geconfigureerd"
},
"error": {
- "cannot_connect": "Verbinding niet geslaagd",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"name_exists": "Naam bestaat al"
},
diff --git a/homeassistant/components/mikrotik/translations/ru.json b/homeassistant/components/mikrotik/translations/ru.json
index 868ed49b5c4ba1..06e9d647545905 100644
--- a/homeassistant/components/mikrotik/translations/ru.json
+++ b/homeassistant/components/mikrotik/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f."
},
"step": {
@@ -15,7 +15,7 @@
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
- "username": "\u041b\u043e\u0433\u0438\u043d",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f",
"verify_ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL"
},
"title": "MikroTik"
diff --git a/homeassistant/components/mikrotik/translations/tr.json b/homeassistant/components/mikrotik/translations/tr.json
new file mode 100644
index 00000000000000..cffcc65151c6d6
--- /dev/null
+++ b/homeassistant/components/mikrotik/translations/tr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "password": "Parola",
+ "port": "Port",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mikrotik/translations/uk.json b/homeassistant/components/mikrotik/translations/uk.json
new file mode 100644
index 00000000000000..b44d5979d13058
--- /dev/null
+++ b/homeassistant/components/mikrotik/translations/uk.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "name_exists": "\u0426\u044f \u043d\u0430\u0437\u0432\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430",
+ "verify_ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 SSL"
+ },
+ "title": "MikroTik"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "device_tracker": {
+ "data": {
+ "arp_ping": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 ARP-\u043f\u0456\u043d\u0433",
+ "detection_time": "\u0427\u0430\u0441 \u0432\u0456\u0434 \u043e\u0441\u0442\u0430\u043d\u043d\u044c\u043e\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0443 \u0437\u0432'\u044f\u0437\u043a\u0443 \u0437 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0437\u0430\u043a\u0456\u043d\u0447\u0435\u043d\u043d\u044e \u044f\u043a\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043e\u0442\u0440\u0438\u043c\u0430\u0454 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\".",
+ "force_dhcp": "\u041f\u0440\u0438\u043c\u0443\u0441\u043e\u0432\u0435 \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c DHCP"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py
index 0bb94242d646bb..7a1adc6a0bca8f 100644
--- a/homeassistant/components/mill/climate.py
+++ b/homeassistant/components/mill/climate.py
@@ -107,7 +107,7 @@ def name(self):
return self._heater.name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
res = {
"open_window": self._heater.open_window,
diff --git a/homeassistant/components/mill/config_flow.py b/homeassistant/components/mill/config_flow.py
index f1b94ab2ac8aa9..dead7a1ff9d480 100644
--- a/homeassistant/components/mill/config_flow.py
+++ b/homeassistant/components/mill/config_flow.py
@@ -6,7 +6,7 @@
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import DOMAIN
DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
diff --git a/homeassistant/components/mill/translations/de.json b/homeassistant/components/mill/translations/de.json
index 886e7e3c4589e2..63b6b7ea6e93ac 100644
--- a/homeassistant/components/mill/translations/de.json
+++ b/homeassistant/components/mill/translations/de.json
@@ -3,6 +3,9 @@
"abort": {
"already_configured": "Account ist bereits konfiguriert"
},
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/mill/translations/fr.json b/homeassistant/components/mill/translations/fr.json
index e171086a084e66..ffcff15ade8388 100644
--- a/homeassistant/components/mill/translations/fr.json
+++ b/homeassistant/components/mill/translations/fr.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9"
+ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9"
},
"error": {
"cannot_connect": "\u00c9chec de connexion"
diff --git a/homeassistant/components/mill/translations/hu.json b/homeassistant/components/mill/translations/hu.json
index 387a73041d92f8..74a6f9abbac214 100644
--- a/homeassistant/components/mill/translations/hu.json
+++ b/homeassistant/components/mill/translations/hu.json
@@ -3,6 +3,9 @@
"abort": {
"already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
},
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/mill/translations/id.json b/homeassistant/components/mill/translations/id.json
new file mode 100644
index 00000000000000..ab929d3d7c80c2
--- /dev/null
+++ b/homeassistant/components/mill/translations/id.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mill/translations/ko.json b/homeassistant/components/mill/translations/ko.json
index d2c6fd74284d5e..48c8cdc6eaa515 100644
--- a/homeassistant/components/mill/translations/ko.json
+++ b/homeassistant/components/mill/translations/ko.json
@@ -1,7 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
diff --git a/homeassistant/components/mill/translations/nl.json b/homeassistant/components/mill/translations/nl.json
index 4699b6fb733552..fff0a8232e4017 100644
--- a/homeassistant/components/mill/translations/nl.json
+++ b/homeassistant/components/mill/translations/nl.json
@@ -3,10 +3,13 @@
"abort": {
"already_configured": "Account is al geconfigureerd"
},
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken"
+ },
"step": {
"user": {
"data": {
- "password": "Password",
+ "password": "Wachtwoord",
"username": "Gebruikersnaam"
}
}
diff --git a/homeassistant/components/mill/translations/ru.json b/homeassistant/components/mill/translations/ru.json
index db2d4651c2d58d..eac6c63c559db7 100644
--- a/homeassistant/components/mill/translations/ru.json
+++ b/homeassistant/components/mill/translations/ru.json
@@ -10,7 +10,7 @@
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
}
}
}
diff --git a/homeassistant/components/mill/translations/tr.json b/homeassistant/components/mill/translations/tr.json
new file mode 100644
index 00000000000000..0f14728873a538
--- /dev/null
+++ b/homeassistant/components/mill/translations/tr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mill/translations/uk.json b/homeassistant/components/mill/translations/uk.json
new file mode 100644
index 00000000000000..b8a5aea578e7fd
--- /dev/null
+++ b/homeassistant/components/mill/translations/uk.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py
index d2ffb9f5ec0733..d103ff8eaa68ee 100644
--- a/homeassistant/components/min_max/sensor.py
+++ b/homeassistant/components/min_max/sensor.py
@@ -3,7 +3,7 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
CONF_NAME,
@@ -13,7 +13,6 @@
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.reload import async_setup_reload_service
@@ -85,9 +84,10 @@ def calc_min(sensor_values):
val = None
entity_id = None
for sensor_id, sensor_value in sensor_values:
- if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE]:
- if val is None or val > sensor_value:
- entity_id, val = sensor_id, sensor_value
+ if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE] and (
+ val is None or val > sensor_value
+ ):
+ entity_id, val = sensor_id, sensor_value
return entity_id, val
@@ -96,30 +96,35 @@ def calc_max(sensor_values):
val = None
entity_id = None
for sensor_id, sensor_value in sensor_values:
- if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE]:
- if val is None or val < sensor_value:
- entity_id, val = sensor_id, sensor_value
+ if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE] and (
+ val is None or val < sensor_value
+ ):
+ entity_id, val = sensor_id, sensor_value
return entity_id, val
def calc_mean(sensor_values, round_digits):
"""Calculate mean value, honoring unknown states."""
- result = []
- for _, sensor_value in sensor_values:
- if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE]:
- result.append(sensor_value)
- if len(result) == 0:
+ 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(sum(result) / len(result), round_digits)
def calc_median(sensor_values, round_digits):
"""Calculate median value, honoring unknown states."""
- result = []
- for _, sensor_value in sensor_values:
- if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE]:
- result.append(sensor_value)
- if len(result) == 0:
+ result = [
+ sensor_value
+ for _, sensor_value in sensor_values
+ if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE]
+ ]
+
+ if not result:
return None
result.sort()
if len(result) % 2 == 0:
@@ -131,7 +136,7 @@ def calc_median(sensor_values, round_digits):
return round(median, round_digits)
-class MinMaxSensor(Entity):
+class MinMaxSensor(SensorEntity):
"""Representation of a min/max sensor."""
def __init__(self, entity_ids, name, sensor_type, round_digits):
@@ -188,7 +193,7 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {
attr: getattr(self, attr)
diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py
index 164bb264f90ae4..f76e8e8467edb4 100644
--- a/homeassistant/components/minecraft_server/__init__.py
+++ b/homeassistant/components/minecraft_server/__init__.py
@@ -1,9 +1,10 @@
"""The Minecraft Server integration."""
+from __future__ import annotations
import asyncio
from datetime import datetime, timedelta
import logging
-from typing import Any, Dict
+from typing import Any
from mcstatus.server import MinecraftServer as MCStatus
@@ -246,7 +247,7 @@ def __init__(
"sw_version": self._server.protocol_version,
}
self._device_class = device_class
- self._device_state_attributes = None
+ self._extra_state_attributes = None
self._disconnect_dispatcher = None
@property
@@ -260,7 +261,7 @@ def unique_id(self) -> str:
return self._unique_id
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information."""
return self._device_info
diff --git a/homeassistant/components/minecraft_server/config_flow.py b/homeassistant/components/minecraft_server/config_flow.py
index a7cb0371f6701d..b252a7121531fa 100644
--- a/homeassistant/components/minecraft_server/config_flow.py
+++ b/homeassistant/components/minecraft_server/config_flow.py
@@ -1,4 +1,5 @@
"""Config flow for Minecraft Server integration."""
+from contextlib import suppress
from functools import partial
import ipaddress
@@ -10,12 +11,7 @@
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
from . import MinecraftServer, helpers
-from .const import ( # pylint: disable=unused-import
- DEFAULT_HOST,
- DEFAULT_NAME,
- DEFAULT_PORT,
- DOMAIN,
-)
+from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, DOMAIN
class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN):
@@ -40,10 +36,8 @@ async def async_step_user(self, user_input=None):
host = address_right
else:
host = address_left
- try:
+ with suppress(ValueError):
port = int(address_right)
- except ValueError:
- pass # 'port' is already set to default value.
# Remove '[' and ']' in case of an IPv6 address.
host = host.strip("[]")
diff --git a/homeassistant/components/minecraft_server/helpers.py b/homeassistant/components/minecraft_server/helpers.py
index 7f9380cdec24c0..f6409ce525de80 100644
--- a/homeassistant/components/minecraft_server/helpers.py
+++ b/homeassistant/components/minecraft_server/helpers.py
@@ -1,6 +1,7 @@
"""Helper functions for the Minecraft Server integration."""
+from __future__ import annotations
-from typing import Any, Dict
+from typing import Any
import aiodns
@@ -10,7 +11,7 @@
from .const import SRV_RECORD_PREFIX
-async def async_check_srv_record(hass: HomeAssistantType, host: str) -> Dict[str, Any]:
+async def async_check_srv_record(hass: HomeAssistantType, host: str) -> dict[str, Any]:
"""Check if the given host is a valid Minecraft SRV record."""
# Check if 'host' is a valid SRV record.
return_value = None
diff --git a/homeassistant/components/minecraft_server/manifest.json b/homeassistant/components/minecraft_server/manifest.json
index 03710520b90cdb..2c4a2ae4b8eacb 100644
--- a/homeassistant/components/minecraft_server/manifest.json
+++ b/homeassistant/components/minecraft_server/manifest.json
@@ -3,7 +3,7 @@
"name": "Minecraft Server",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/minecraft_server",
- "requirements": ["aiodns==2.0.0", "getmac==0.8.2", "mcstatus==2.3.0"],
+ "requirements": ["aiodns==2.0.0", "getmac==0.8.2", "mcstatus==5.1.1"],
"codeowners": ["@elmurato"],
"quality_scale": "silver"
}
diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py
index 171ff9d17013ad..3d77d9e27727ff 100644
--- a/homeassistant/components/minecraft_server/sensor.py
+++ b/homeassistant/components/minecraft_server/sensor.py
@@ -1,6 +1,9 @@
"""The Minecraft Server sensor platform."""
-from typing import Any, Dict
+from __future__ import annotations
+from typing import Any
+
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import TIME_MILLISECONDS
from homeassistant.helpers.typing import HomeAssistantType
@@ -45,7 +48,7 @@ async def async_setup_entry(
async_add_entities(entities, True)
-class MinecraftServerSensorEntity(MinecraftServerEntity):
+class MinecraftServerSensorEntity(MinecraftServerEntity, SensorEntity):
"""Representation of a Minecraft Server sensor base entity."""
def __init__(
@@ -141,19 +144,18 @@ async def async_update(self) -> None:
"""Update online players state and device state attributes."""
self._state = self._server.players_online
- device_state_attributes = None
+ extra_state_attributes = None
players_list = self._server.players_list
- if players_list is not None:
- if len(players_list) != 0:
- device_state_attributes = {ATTR_PLAYERS_LIST: self._server.players_list}
+ if players_list is not None and len(players_list) != 0:
+ extra_state_attributes = {ATTR_PLAYERS_LIST: self._server.players_list}
- self._device_state_attributes = device_state_attributes
+ self._extra_state_attributes = extra_state_attributes
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return players list in device state attributes."""
- return self._device_state_attributes
+ return self._extra_state_attributes
class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity):
diff --git a/homeassistant/components/minecraft_server/translations/de.json b/homeassistant/components/minecraft_server/translations/de.json
index 484be7bd41867a..a0bbe60a842dcc 100644
--- a/homeassistant/components/minecraft_server/translations/de.json
+++ b/homeassistant/components/minecraft_server/translations/de.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Der Host ist bereits konfiguriert."
+ "already_configured": "Der Dienst ist bereits konfiguriert"
},
"error": {
"cannot_connect": "Verbindung zum Server fehlgeschlagen. Bitte \u00fcberpr\u00fcfe den Host und den Port und versuche es erneut. Stelle au\u00dferdem sicher, dass Du mindestens Minecraft Version 1.7 auf Deinem Server ausf\u00fchrst.",
diff --git a/homeassistant/components/minecraft_server/translations/et.json b/homeassistant/components/minecraft_server/translations/et.json
index a92449b0512b55..f8de21662aa5c2 100644
--- a/homeassistant/components/minecraft_server/translations/et.json
+++ b/homeassistant/components/minecraft_server/translations/et.json
@@ -4,7 +4,7 @@
"already_configured": "Teenus on juba seadistatud"
},
"error": {
- "cannot_connect": "Serveriga \u00fchenduse loomine nurjus. Kontrollige hosti ja porti ning proovige uuesti. Samuti veenduge, et kasutate oma serveris v\u00e4hemalt Minecrafti versiooni 1.7.",
+ "cannot_connect": "Serveriga \u00fchenduse loomine nurjus. Kontrolli hosti ja porti ning proovi uuesti. Samuti veendu, et kasutad oma serveris v\u00e4hemalt Minecrafti versiooni 1.7.",
"invalid_ip": "IP-aadress on vale (MAC-aadressi ei \u00f5nnestunud tuvastada). Paranda ja proovi uuesti.",
"invalid_port": "Lubatud pordivahemik on 1024\u201365535. Paranda ja proovi uuesti."
},
diff --git a/homeassistant/components/minecraft_server/translations/hu.json b/homeassistant/components/minecraft_server/translations/hu.json
index 7a8958bd7c6764..247c1ffc1c378d 100644
--- a/homeassistant/components/minecraft_server/translations/hu.json
+++ b/homeassistant/components/minecraft_server/translations/hu.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Kiszolg\u00e1l\u00f3 m\u00e1r konfigur\u00e1lva van."
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
},
"error": {
"cannot_connect": "Nem siker\u00fclt csatlakozni a szerverhez. K\u00e9rj\u00fck, ellen\u0151rizze a gazdag\u00e9pet \u00e9s a portot, majd pr\u00f3b\u00e1lkozzon \u00fajra. Gondoskodjon arr\u00f3l, hogy a szerveren legal\u00e1bb a Minecraft 1.7-es verzi\u00f3j\u00e1t futtassa."
diff --git a/homeassistant/components/minecraft_server/translations/id.json b/homeassistant/components/minecraft_server/translations/id.json
new file mode 100644
index 00000000000000..fffb0865b241bc
--- /dev/null
+++ b/homeassistant/components/minecraft_server/translations/id.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung ke server. Periksa host dan port lalu coba lagi. Pastikan juga Anda menjalankan Minecraft dengan versi minimal 1.7 di server Anda.",
+ "invalid_ip": "Alamat IP tidak valid (alamat MAC tidak dapat ditentukan). Perbaiki, lalu coba lagi.",
+ "invalid_port": "Port harus berada dalam rentang dari 1024 hingga 65535. Perbaiki, lalu coba lagi."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nama"
+ },
+ "description": "Siapkan instans Minecraft Server Anda untuk pemantauan.",
+ "title": "Tautkan Server Minecraft Anda"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/translations/ko.json b/homeassistant/components/minecraft_server/translations/ko.json
index 30605d729369bb..98ab72e94fccdb 100644
--- a/homeassistant/components/minecraft_server/translations/ko.json
+++ b/homeassistant/components/minecraft_server/translations/ko.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
"cannot_connect": "\uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\ub97c \ud655\uc778\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \ub610\ud55c \uc11c\ubc84\uc5d0\uc11c Minecraft \ubc84\uc804 1.7 \uc774\uc0c1\uc744 \uc2e4\ud589 \uc911\uc778\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
diff --git a/homeassistant/components/minecraft_server/translations/nl.json b/homeassistant/components/minecraft_server/translations/nl.json
index 1d589f942e7c0d..0170964d5b0722 100644
--- a/homeassistant/components/minecraft_server/translations/nl.json
+++ b/homeassistant/components/minecraft_server/translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Host is al geconfigureerd."
+ "already_configured": "Service is al geconfigureerd"
},
"error": {
"cannot_connect": "Kan geen verbinding maken met de server. Controleer de host en de poort en probeer het opnieuw. Zorg er ook voor dat u minimaal Minecraft versie 1.7 op uw server uitvoert.",
diff --git a/homeassistant/components/minecraft_server/translations/tr.json b/homeassistant/components/minecraft_server/translations/tr.json
index 7527294a3c7aa0..422dab32a01311 100644
--- a/homeassistant/components/minecraft_server/translations/tr.json
+++ b/homeassistant/components/minecraft_server/translations/tr.json
@@ -11,7 +11,7 @@
"step": {
"user": {
"data": {
- "host": "Host",
+ "host": "Ana Bilgisayar",
"name": "Ad"
},
"description": "G\u00f6zetmeye izin vermek i\u00e7in Minecraft server nesnesini ayarla.",
diff --git a/homeassistant/components/minecraft_server/translations/uk.json b/homeassistant/components/minecraft_server/translations/uk.json
new file mode 100644
index 00000000000000..0c8528b2cab1fb
--- /dev/null
+++ b/homeassistant/components/minecraft_server/translations/uk.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u0427\u0438 \u043d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0456\u0441\u0442\u044c \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0445 \u0434\u0430\u043d\u0438\u0445 \u0456 \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443. \u0422\u0430\u043a\u043e\u0436 \u043f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u043d\u0430 \u0412\u0430\u0448\u043e\u043c\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0456 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0439 Minecraft \u0432\u0435\u0440\u0441\u0456\u0457 1.7, \u0430\u0431\u043e \u0432\u0438\u0449\u0435.",
+ "invalid_ip": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430 IP-\u0430\u0434\u0440\u0435\u0441\u0430 (\u043d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 MAC-\u0430\u0434\u0440\u0435\u0441\u0443).",
+ "invalid_port": "\u041f\u043e\u0440\u0442 \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0432 \u0434\u0456\u0430\u043f\u0430\u0437\u043e\u043d\u0456 \u0432\u0456\u0434 1024 \u0434\u043e 65535."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0446\u0435\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443 \u0412\u0430\u0448\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Minecraft.",
+ "title": "Minecraft Server"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minio/__init__.py b/homeassistant/components/minio/__init__.py
index 178058986cc099..6e7174b60ee34f 100644
--- a/homeassistant/components/minio/__init__.py
+++ b/homeassistant/components/minio/__init__.py
@@ -1,9 +1,10 @@
"""Minio component."""
+from __future__ import annotations
+
import logging
import os
from queue import Queue
import threading
-from typing import List
import voluptuous as vol
@@ -230,7 +231,7 @@ def __init__(
bucket_name: str,
prefix: str,
suffix: str,
- events: List[str],
+ events: list[str],
):
"""Create Listener."""
self._queue = queue
diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py
index 2aaba9d408528c..f2d860675525c2 100644
--- a/homeassistant/components/minio/minio_helper.py
+++ b/homeassistant/components/minio/minio_helper.py
@@ -1,4 +1,6 @@
"""Minio helper methods."""
+from __future__ import annotations
+
from collections.abc import Iterable
import json
import logging
@@ -6,7 +8,7 @@
import re
import threading
import time
-from typing import Iterator, List
+from typing import Iterator
from urllib.parse import unquote
from minio import Minio
@@ -38,7 +40,7 @@ def create_minio_client(
def get_minio_notification_response(
- minio_client, bucket_name: str, prefix: str, suffix: str, events: List[str]
+ minio_client, bucket_name: str, prefix: str, suffix: str, events: list[str]
):
"""Start listening to minio events. Copied from minio-py."""
query = {"prefix": prefix, "suffix": suffix, "events": events}
@@ -87,7 +89,7 @@ def __init__(
bucket_name: str,
prefix: str,
suffix: str,
- events: List[str],
+ events: list[str],
):
"""Copy over all Minio client options."""
super().__init__()
diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py
index 6b64c88c1cea14..670a6daf3d35ce 100644
--- a/homeassistant/components/mitemp_bt/sensor.py
+++ b/homeassistant/components/mitemp_bt/sensor.py
@@ -6,12 +6,13 @@
from mitemp_bt import mitemp_bt_poller
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_FORCE_UPDATE,
CONF_MAC,
CONF_MONITORED_CONDITIONS,
CONF_NAME,
+ CONF_TIMEOUT,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
@@ -19,7 +20,6 @@
TEMP_CELSIUS,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
try:
import bluepy.btle # noqa: F401 pylint: disable=unused-import
@@ -34,7 +34,6 @@
CONF_CACHE = "cache_value"
CONF_MEDIAN = "median"
CONF_RETRIES = "retries"
-CONF_TIMEOUT = "timeout"
DEFAULT_ADAPTER = "hci0"
DEFAULT_UPDATE_INTERVAL = 300
@@ -104,7 +103,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(devs)
-class MiTempBtSensor(Entity):
+class MiTempBtSensor(SensorEntity):
"""Implementing the MiTempBt sensor."""
def __init__(self, poller, parameter, device, name, unit, force_update, median):
diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py
index 1a8b132709360c..d5008d1778c4a7 100644
--- a/homeassistant/components/mjpeg/camera.py
+++ b/homeassistant/components/mjpeg/camera.py
@@ -98,9 +98,12 @@ def __init__(self, device_info):
self._still_image_url = device_info.get(CONF_STILL_IMAGE_URL)
self._auth = None
- if self._username and self._password:
- if self._authentication == HTTP_BASIC_AUTHENTICATION:
- self._auth = aiohttp.BasicAuth(self._username, password=self._password)
+ if (
+ self._username
+ and self._password
+ and self._authentication == HTTP_BASIC_AUTHENTICATION
+ ):
+ self._auth = aiohttp.BasicAuth(self._username, password=self._password)
self._verify_ssl = device_info.get(CONF_VERIFY_SSL)
async def async_camera_image(self):
@@ -144,8 +147,6 @@ def camera_image(self):
else:
req = requests.get(self._mjpeg_url, stream=True, timeout=10)
- # https://github.com/PyCQA/pylint/issues/1437
- # pylint: disable=no-member
with closing(req) as response:
return extract_image_from_mjpeg(response.iter_content(102400))
diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py
index 3bc95bf3e0520d..e63698d3eb5878 100644
--- a/homeassistant/components/mobile_app/__init__.py
+++ b/homeassistant/components/mobile_app/__init__.py
@@ -1,27 +1,25 @@
"""Integrates Native Apps to Home Assistant."""
import asyncio
+from contextlib import suppress
from homeassistant.components import cloud, notify as hass_notify
from homeassistant.components.webhook import (
async_register as webhook_register,
async_unregister as webhook_unregister,
)
-from homeassistant.const import CONF_WEBHOOK_ID
+from homeassistant.const import ATTR_DEVICE_ID, CONF_WEBHOOK_ID
from homeassistant.helpers import device_registry as dr, discovery
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from .const import (
- ATTR_DEVICE_ID,
ATTR_DEVICE_NAME,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_OS_VERSION,
CONF_CLOUDHOOK_URL,
- DATA_BINARY_SENSOR,
DATA_CONFIG_ENTRIES,
DATA_DELETED_IDS,
DATA_DEVICES,
- DATA_SENSOR,
DATA_STORE,
DOMAIN,
STORAGE_KEY,
@@ -40,30 +38,24 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
app_config = await store.async_load()
if app_config is None:
app_config = {
- DATA_BINARY_SENSOR: {},
DATA_CONFIG_ENTRIES: {},
DATA_DELETED_IDS: [],
- DATA_SENSOR: {},
}
hass.data[DOMAIN] = {
- DATA_BINARY_SENSOR: app_config.get(DATA_BINARY_SENSOR, {}),
DATA_CONFIG_ENTRIES: {},
DATA_DELETED_IDS: app_config.get(DATA_DELETED_IDS, []),
DATA_DEVICES: {},
- DATA_SENSOR: app_config.get(DATA_SENSOR, {}),
DATA_STORE: store,
}
hass.http.register_view(RegistrationsView())
for deleted_id in hass.data[DOMAIN][DATA_DELETED_IDS]:
- try:
+ with suppress(ValueError):
webhook_register(
hass, DOMAIN, "Deleted Webhook", deleted_id, handle_webhook
)
- except ValueError:
- pass
hass.async_create_task(
discovery.async_load_platform(hass, "notify", DOMAIN, {}, config)
@@ -111,8 +103,8 @@ async def async_unload_entry(hass, entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -136,7 +128,5 @@ async def async_remove_entry(hass, entry):
await store.async_save(savable_state(hass))
if CONF_CLOUDHOOK_URL in entry.data:
- try:
+ with suppress(cloud.CloudNotAvailable):
await cloud.async_delete_cloudhook(hass, entry.data[CONF_WEBHOOK_ID])
- except cloud.CloudNotAvailable:
- pass
diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py
index ae8efc0c11325e..616cd97a775f0c 100644
--- a/homeassistant/components/mobile_app/binary_sensor.py
+++ b/homeassistant/components/mobile_app/binary_sensor.py
@@ -1,19 +1,24 @@
"""Binary sensor platform for mobile_app."""
-from functools import partial
-
from homeassistant.components.binary_sensor import BinarySensorEntity
-from homeassistant.const import CONF_WEBHOOK_ID
+from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID, STATE_ON
from homeassistant.core import callback
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (
+ ATTR_DEVICE_NAME,
+ ATTR_SENSOR_ATTRIBUTES,
+ ATTR_SENSOR_DEVICE_CLASS,
+ ATTR_SENSOR_ICON,
+ ATTR_SENSOR_NAME,
ATTR_SENSOR_STATE,
+ ATTR_SENSOR_TYPE,
ATTR_SENSOR_TYPE_BINARY_SENSOR as ENTITY_TYPE,
ATTR_SENSOR_UNIQUE_ID,
DATA_DEVICES,
DOMAIN,
)
-from .entity import MobileAppEntity, sensor_id
+from .entity import MobileAppEntity, unique_id
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -22,29 +27,35 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
- for config in hass.data[DOMAIN][ENTITY_TYPE].values():
- if config[CONF_WEBHOOK_ID] != webhook_id:
+ entity_registry = await er.async_get_registry(hass)
+ entries = er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)
+ for entry in entries:
+ if entry.domain != ENTITY_TYPE or entry.disabled_by:
continue
-
- device = hass.data[DOMAIN][DATA_DEVICES][webhook_id]
-
- entities.append(MobileAppBinarySensor(config, device, config_entry))
+ config = {
+ ATTR_SENSOR_ATTRIBUTES: {},
+ ATTR_SENSOR_DEVICE_CLASS: entry.device_class,
+ ATTR_SENSOR_ICON: entry.original_icon,
+ ATTR_SENSOR_NAME: entry.original_name,
+ ATTR_SENSOR_STATE: None,
+ ATTR_SENSOR_TYPE: entry.domain,
+ ATTR_SENSOR_UNIQUE_ID: entry.unique_id,
+ }
+ entities.append(MobileAppBinarySensor(config, entry.device_id, config_entry))
async_add_entities(entities)
@callback
- def handle_sensor_registration(webhook_id, data):
+ def handle_sensor_registration(data):
if data[CONF_WEBHOOK_ID] != webhook_id:
return
- unique_id = sensor_id(data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID])
-
- entity = hass.data[DOMAIN][ENTITY_TYPE][unique_id]
-
- if "added" in entity:
- return
-
- entity["added"] = True
+ data[CONF_UNIQUE_ID] = unique_id(
+ data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID]
+ )
+ data[
+ CONF_NAME
+ ] = f"{config_entry.data[ATTR_DEVICE_NAME]} {data[ATTR_SENSOR_NAME]}"
device = hass.data[DOMAIN][DATA_DEVICES][data[CONF_WEBHOOK_ID]]
@@ -53,7 +64,7 @@ def handle_sensor_registration(webhook_id, data):
async_dispatcher_connect(
hass,
f"{DOMAIN}_{ENTITY_TYPE}_register",
- partial(handle_sensor_registration, webhook_id),
+ handle_sensor_registration,
)
@@ -64,3 +75,10 @@ class MobileAppBinarySensor(MobileAppEntity, BinarySensorEntity):
def is_on(self):
"""Return the state of the binary sensor."""
return self._config[ATTR_SENSOR_STATE]
+
+ @callback
+ def async_restore_last_state(self, last_state):
+ """Restore previous state."""
+
+ super().async_restore_last_state(last_state)
+ self._config[ATTR_SENSOR_STATE] = last_state.state == STATE_ON
diff --git a/homeassistant/components/mobile_app/config_flow.py b/homeassistant/components/mobile_app/config_flow.py
index 08fdecf364d16a..752ef86d68dd0e 100644
--- a/homeassistant/components/mobile_app/config_flow.py
+++ b/homeassistant/components/mobile_app/config_flow.py
@@ -3,9 +3,10 @@
from homeassistant import config_entries
from homeassistant.components import person
-from homeassistant.helpers import entity_registry
+from homeassistant.const import ATTR_DEVICE_ID
+from homeassistant.helpers import entity_registry as er
-from .const import ATTR_APP_ID, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, CONF_USER_ID, DOMAIN
+from .const import ATTR_APP_ID, ATTR_DEVICE_NAME, CONF_USER_ID, DOMAIN
@config_entries.HANDLERS.register(DOMAIN)
@@ -36,8 +37,8 @@ async def async_step_registration(self, user_input=None):
user_input[ATTR_DEVICE_ID] = str(uuid.uuid4()).replace("-", "")
# Register device tracker entity and add to person registering app
- ent_reg = await entity_registry.async_get_registry(self.hass)
- devt_entry = ent_reg.async_get_or_create(
+ entity_registry = await er.async_get_registry(self.hass)
+ devt_entry = entity_registry.async_get_or_create(
"device_tracker",
DOMAIN,
user_input[ATTR_DEVICE_ID],
diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py
index b35468a6fb3e8c..af828ce423e2a7 100644
--- a/homeassistant/components/mobile_app/const.py
+++ b/homeassistant/components/mobile_app/const.py
@@ -9,11 +9,9 @@
CONF_SECRET = "secret"
CONF_USER_ID = "user_id"
-DATA_BINARY_SENSOR = "binary_sensor"
DATA_CONFIG_ENTRIES = "config_entries"
DATA_DELETED_IDS = "deleted_ids"
DATA_DEVICES = "devices"
-DATA_SENSOR = "sensor"
DATA_STORE = "store"
DATA_NOTIFY = "notify"
@@ -22,7 +20,6 @@
ATTR_APP_NAME = "app_name"
ATTR_APP_VERSION = "app_version"
ATTR_CONFIG_ENTRY_ID = "entry_id"
-ATTR_DEVICE_ID = "device_id"
ATTR_DEVICE_NAME = "device_name"
ATTR_MANUFACTURER = "manufacturer"
ATTR_MODEL = "model"
diff --git a/homeassistant/components/mobile_app/device_action.py b/homeassistant/components/mobile_app/device_action.py
index 2592d4b486b176..33a7510da2149a 100644
--- a/homeassistant/components/mobile_app/device_action.py
+++ b/homeassistant/components/mobile_app/device_action.py
@@ -1,5 +1,5 @@
"""Provides device actions for Mobile App."""
-from typing import List, Optional
+from __future__ import annotations
import voluptuous as vol
@@ -22,7 +22,7 @@
)
-async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device actions for Mobile App devices."""
webhook_id = webhook_id_from_device_id(hass, device_id)
@@ -33,7 +33,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
async def async_call_action_from_config(
- hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context]
+ hass: HomeAssistant, config: dict, variables: dict, context: Context | None
) -> None:
"""Execute a device action."""
webhook_id = webhook_id_from_device_id(hass, config[CONF_DEVICE_ID])
diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py
index d2e987066ef79e..1b006f698279c6 100644
--- a/homeassistant/components/mobile_app/device_tracker.py
+++ b/homeassistant/components/mobile_app/device_tracker.py
@@ -7,14 +7,18 @@
)
from homeassistant.components.device_tracker.config_entry import TrackerEntity
from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS
-from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_LATITUDE, ATTR_LONGITUDE
+from homeassistant.const import (
+ ATTR_BATTERY_LEVEL,
+ ATTR_DEVICE_ID,
+ ATTR_LATITUDE,
+ ATTR_LONGITUDE,
+)
from homeassistant.core import callback
from homeassistant.helpers.restore_state import RestoreEntity
from .const import (
ATTR_ALTITUDE,
ATTR_COURSE,
- ATTR_DEVICE_ID,
ATTR_DEVICE_NAME,
ATTR_SPEED,
ATTR_VERTICAL_ACCURACY,
@@ -52,7 +56,7 @@ def battery_level(self):
return self._data.get(ATTR_BATTERY)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific attributes."""
attrs = {}
for key in ATTR_KEYS:
diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py
index 7a12f617740ec7..46f4589fa2cb2e 100644
--- a/homeassistant/components/mobile_app/entity.py
+++ b/homeassistant/components/mobile_app/entity.py
@@ -1,56 +1,65 @@
"""A entity class for mobile_app."""
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_WEBHOOK_ID
+from homeassistant.const import ATTR_ICON, CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.restore_state import RestoreEntity
from .const import (
- ATTR_DEVICE_NAME,
ATTR_SENSOR_ATTRIBUTES,
ATTR_SENSOR_DEVICE_CLASS,
ATTR_SENSOR_ICON,
- ATTR_SENSOR_NAME,
+ ATTR_SENSOR_STATE,
ATTR_SENSOR_TYPE,
ATTR_SENSOR_UNIQUE_ID,
- DOMAIN,
SIGNAL_SENSOR_UPDATE,
)
from .helpers import device_info
-def sensor_id(webhook_id, unique_id):
+def unique_id(webhook_id, sensor_unique_id):
"""Return a unique sensor ID."""
- return f"{webhook_id}_{unique_id}"
+ return f"{webhook_id}_{sensor_unique_id}"
-class MobileAppEntity(Entity):
+class MobileAppEntity(RestoreEntity):
"""Representation of an mobile app entity."""
def __init__(self, config: dict, device: DeviceEntry, entry: ConfigEntry):
- """Initialize the sensor."""
+ """Initialize the entity."""
self._config = config
self._device = device
self._entry = entry
self._registration = entry.data
- self._sensor_id = sensor_id(
- self._registration[CONF_WEBHOOK_ID], config[ATTR_SENSOR_UNIQUE_ID]
- )
+ self._unique_id = config[CONF_UNIQUE_ID]
self._entity_type = config[ATTR_SENSOR_TYPE]
- self.unsub_dispatcher = None
- self._name = f"{entry.data[ATTR_DEVICE_NAME]} {config[ATTR_SENSOR_NAME]}"
+ self._name = config[CONF_NAME]
async def async_added_to_hass(self):
"""Register callbacks."""
- self.unsub_dispatcher = async_dispatcher_connect(
- self.hass, SIGNAL_SENSOR_UPDATE, self._handle_update
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass, SIGNAL_SENSOR_UPDATE, self._handle_update
+ )
)
+ state = await self.async_get_last_state()
+
+ if state is None:
+ return
- async def async_will_remove_from_hass(self):
- """Disconnect dispatcher listener when removed."""
- if self.unsub_dispatcher is not None:
- self.unsub_dispatcher()
+ self.async_restore_last_state(state)
+
+ @callback
+ def async_restore_last_state(self, last_state):
+ """Restore previous state."""
+ self._config[ATTR_SENSOR_STATE] = last_state.state
+ self._config[ATTR_SENSOR_ATTRIBUTES] = {
+ **last_state.attributes,
+ **self._config[ATTR_SENSOR_ATTRIBUTES],
+ }
+ if ATTR_ICON in last_state.attributes:
+ self._config[ATTR_SENSOR_ICON] = last_state.attributes[ATTR_ICON]
@property
def should_poll(self) -> bool:
@@ -68,7 +77,7 @@ def device_class(self):
return self._config.get(ATTR_SENSOR_DEVICE_CLASS)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
return self._config[ATTR_SENSOR_ATTRIBUTES]
@@ -80,27 +89,19 @@ def icon(self):
@property
def unique_id(self):
"""Return the unique ID of this sensor."""
- return self._sensor_id
+ return self._unique_id
@property
def device_info(self):
"""Return device registry information for this entity."""
return device_info(self._registration)
- async def async_update(self):
- """Get the latest state of the sensor."""
- data = self.hass.data[DOMAIN]
- try:
- self._config = data[self._entity_type][self._sensor_id]
- except KeyError:
- return
-
@callback
def _handle_update(self, data):
"""Handle async event updates."""
- incoming_id = sensor_id(data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID])
- if incoming_id != self._sensor_id:
+ incoming_id = unique_id(data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID])
+ if incoming_id != self._unique_id:
return
- self._config = data
+ self._config = {**self._config, **data}
self.async_write_ha_state()
diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py
index 7c5cbd135ed454..63d638cd9e557f 100644
--- a/homeassistant/components/mobile_app/helpers.py
+++ b/homeassistant/components/mobile_app/helpers.py
@@ -1,13 +1,20 @@
"""Helpers for mobile_app."""
+from __future__ import annotations
+
import json
import logging
-from typing import Callable, Dict, Tuple
+from typing import Callable
from aiohttp.web import Response, json_response
from nacl.encoding import Base64Encoder
from nacl.secret import SecretBox
-from homeassistant.const import CONTENT_TYPE_JSON, HTTP_BAD_REQUEST, HTTP_OK
+from homeassistant.const import (
+ ATTR_DEVICE_ID,
+ CONTENT_TYPE_JSON,
+ HTTP_BAD_REQUEST,
+ HTTP_OK,
+)
from homeassistant.core import Context
from homeassistant.helpers.json import JSONEncoder
from homeassistant.helpers.typing import HomeAssistantType
@@ -17,7 +24,6 @@
ATTR_APP_ID,
ATTR_APP_NAME,
ATTR_APP_VERSION,
- ATTR_DEVICE_ID,
ATTR_DEVICE_NAME,
ATTR_MANUFACTURER,
ATTR_MODEL,
@@ -25,16 +31,14 @@
ATTR_SUPPORTS_ENCRYPTION,
CONF_SECRET,
CONF_USER_ID,
- DATA_BINARY_SENSOR,
DATA_DELETED_IDS,
- DATA_SENSOR,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
-def setup_decrypt() -> Tuple[int, Callable]:
+def setup_decrypt() -> tuple[int, Callable]:
"""Return decryption function and length of key.
Async friendly.
@@ -47,7 +51,7 @@ def decrypt(ciphertext, key):
return (SecretBox.KEY_SIZE, decrypt)
-def setup_encrypt() -> Tuple[int, Callable]:
+def setup_encrypt() -> tuple[int, Callable]:
"""Return encryption function and length of key.
Async friendly.
@@ -60,7 +64,7 @@ def encrypt(ciphertext, key):
return (SecretBox.KEY_SIZE, encrypt)
-def _decrypt_payload(key: str, ciphertext: str) -> Dict[str, str]:
+def _decrypt_payload(key: str, ciphertext: str) -> dict[str, str]:
"""Decrypt encrypted payload."""
try:
keylen, decrypt = setup_decrypt()
@@ -86,12 +90,12 @@ def _decrypt_payload(key: str, ciphertext: str) -> Dict[str, str]:
return None
-def registration_context(registration: Dict) -> Context:
+def registration_context(registration: dict) -> Context:
"""Generate a context from a request."""
return Context(user_id=registration[CONF_USER_ID])
-def empty_okay_response(headers: Dict = None, status: int = HTTP_OK) -> Response:
+def empty_okay_response(headers: dict = None, status: int = HTTP_OK) -> Response:
"""Return a Response with empty JSON object and a 200."""
return Response(
text="{}", status=status, content_type=CONTENT_TYPE_JSON, headers=headers
@@ -119,7 +123,7 @@ def supports_encryption() -> bool:
return False
-def safe_registration(registration: Dict) -> Dict:
+def safe_registration(registration: dict) -> dict:
"""Return a registration without sensitive values."""
# Sensitive values: webhook_id, secret, cloudhook_url
return {
@@ -135,17 +139,15 @@ def safe_registration(registration: Dict) -> Dict:
}
-def savable_state(hass: HomeAssistantType) -> Dict:
+def savable_state(hass: HomeAssistantType) -> dict:
"""Return a clean object containing things that should be saved."""
return {
- DATA_BINARY_SENSOR: hass.data[DOMAIN][DATA_BINARY_SENSOR],
DATA_DELETED_IDS: hass.data[DOMAIN][DATA_DELETED_IDS],
- DATA_SENSOR: hass.data[DOMAIN][DATA_SENSOR],
}
def webhook_response(
- data, *, registration: Dict, status: int = HTTP_OK, headers: Dict = None
+ data, *, registration: dict, status: int = HTTP_OK, headers: dict = None
) -> Response:
"""Return a encrypted response if registration supports it."""
data = json.dumps(data, cls=JSONEncoder)
@@ -165,7 +167,7 @@ def webhook_response(
)
-def device_info(registration: Dict) -> Dict:
+def device_info(registration: dict) -> dict:
"""Return the device info for this registration."""
return {
"identifiers": {(DOMAIN, registration[ATTR_DEVICE_ID])},
diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py
index a5a96b83bc6e01..63bf13bad5e0f6 100644
--- a/homeassistant/components/mobile_app/http_api.py
+++ b/homeassistant/components/mobile_app/http_api.py
@@ -1,6 +1,8 @@
"""Provides an HTTP API for mobile_app."""
+from __future__ import annotations
+
+from contextlib import suppress
import secrets
-from typing import Dict
from aiohttp.web import Request, Response
import emoji
@@ -9,7 +11,7 @@
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
-from homeassistant.const import CONF_WEBHOOK_ID, HTTP_CREATED
+from homeassistant.const import ATTR_DEVICE_ID, CONF_WEBHOOK_ID, HTTP_CREATED
from homeassistant.helpers import config_validation as cv
from homeassistant.util import slugify
@@ -18,7 +20,6 @@
ATTR_APP_ID,
ATTR_APP_NAME,
ATTR_APP_VERSION,
- ATTR_DEVICE_ID,
ATTR_DEVICE_NAME,
ATTR_MANUFACTURER,
ATTR_MODEL,
@@ -59,7 +60,7 @@ class RegistrationsView(HomeAssistantView):
extra=vol.REMOVE_EXTRA,
)
)
- async def post(self, request: Request, data: Dict) -> Response:
+ async def post(self, request: Request, data: dict) -> Response:
"""Handle the POST request for registration."""
hass = request.app["hass"]
@@ -98,10 +99,8 @@ async def post(self, request: Request, data: Dict) -> Response:
)
remote_ui_url = None
- try:
+ with suppress(hass.components.cloud.CloudNotAvailable):
remote_ui_url = hass.components.cloud.async_remote_ui_url()
- except hass.components.cloud.CloudNotAvailable:
- pass
return self.json(
{
diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json
index 758df70c3d0621..bd8ed7713484fd 100644
--- a/homeassistant/components/mobile_app/manifest.json
+++ b/homeassistant/components/mobile_app/manifest.json
@@ -3,7 +3,7 @@
"name": "Mobile App",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/mobile_app",
- "requirements": ["PyNaCl==1.3.0", "emoji==0.5.4"],
+ "requirements": ["PyNaCl==1.3.0", "emoji==1.2.0"],
"dependencies": ["http", "webhook", "person", "tag"],
"after_dependencies": ["cloud", "camera", "notify"],
"codeowners": ["@robbiet480"],
diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py
index 46a34fa7a850ba..803f00764e7cd9 100644
--- a/homeassistant/components/mobile_app/notify.py
+++ b/homeassistant/components/mobile_app/notify.py
@@ -84,17 +84,16 @@ def log_rate_limits(hass, device_name, resp, level=logging.INFO):
async def async_get_service(hass, config, discovery_info=None):
"""Get the mobile_app notification service."""
- session = async_get_clientsession(hass)
- service = hass.data[DOMAIN][DATA_NOTIFY] = MobileAppNotificationService(session)
+ service = hass.data[DOMAIN][DATA_NOTIFY] = MobileAppNotificationService(hass)
return service
class MobileAppNotificationService(BaseNotificationService):
"""Implement the notification service for mobile_app."""
- def __init__(self, session):
+ def __init__(self, hass):
"""Initialize the service."""
- self._session = session
+ self._hass = hass
@property
def targets(self):
@@ -105,10 +104,12 @@ async def async_send_message(self, message="", **kwargs):
"""Send a message to the Lambda APNS gateway."""
data = {ATTR_MESSAGE: message}
- if kwargs.get(ATTR_TITLE) is not None:
- # Remove default title from notifications.
- if kwargs.get(ATTR_TITLE) != ATTR_TITLE_DEFAULT:
- data[ATTR_TITLE] = kwargs.get(ATTR_TITLE)
+ # Remove default title from notifications.
+ if (
+ kwargs.get(ATTR_TITLE) is not None
+ and kwargs.get(ATTR_TITLE) != ATTR_TITLE_DEFAULT
+ ):
+ data[ATTR_TITLE] = kwargs.get(ATTR_TITLE)
targets = kwargs.get(ATTR_TARGET)
@@ -139,7 +140,9 @@ async def async_send_message(self, message="", **kwargs):
try:
with async_timeout.timeout(10):
- response = await self._session.post(push_url, json=data)
+ response = await async_get_clientsession(self._hass).post(
+ push_url, json=data
+ )
result = await response.json()
if response.status in [HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED]:
diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py
index 11e07ed5e7975b..7e3c1c13148a8a 100644
--- a/homeassistant/components/mobile_app/sensor.py
+++ b/homeassistant/components/mobile_app/sensor.py
@@ -1,19 +1,25 @@
"""Sensor platform for mobile_app."""
-from functools import partial
-
-from homeassistant.const import CONF_WEBHOOK_ID
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID
from homeassistant.core import callback
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (
+ ATTR_DEVICE_NAME,
+ ATTR_SENSOR_ATTRIBUTES,
+ ATTR_SENSOR_DEVICE_CLASS,
+ ATTR_SENSOR_ICON,
+ ATTR_SENSOR_NAME,
ATTR_SENSOR_STATE,
+ ATTR_SENSOR_TYPE,
ATTR_SENSOR_TYPE_SENSOR as ENTITY_TYPE,
ATTR_SENSOR_UNIQUE_ID,
ATTR_SENSOR_UOM,
DATA_DEVICES,
DOMAIN,
)
-from .entity import MobileAppEntity, sensor_id
+from .entity import MobileAppEntity, unique_id
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -22,29 +28,36 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
- for config in hass.data[DOMAIN][ENTITY_TYPE].values():
- if config[CONF_WEBHOOK_ID] != webhook_id:
+ entity_registry = await er.async_get_registry(hass)
+ entries = er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)
+ for entry in entries:
+ if entry.domain != ENTITY_TYPE or entry.disabled_by:
continue
-
- device = hass.data[DOMAIN][DATA_DEVICES][webhook_id]
-
- entities.append(MobileAppSensor(config, device, config_entry))
+ config = {
+ ATTR_SENSOR_ATTRIBUTES: {},
+ ATTR_SENSOR_DEVICE_CLASS: entry.device_class,
+ ATTR_SENSOR_ICON: entry.original_icon,
+ ATTR_SENSOR_NAME: entry.original_name,
+ ATTR_SENSOR_STATE: None,
+ ATTR_SENSOR_TYPE: entry.domain,
+ ATTR_SENSOR_UNIQUE_ID: entry.unique_id,
+ ATTR_SENSOR_UOM: entry.unit_of_measurement,
+ }
+ entities.append(MobileAppSensor(config, entry.device_id, config_entry))
async_add_entities(entities)
@callback
- def handle_sensor_registration(webhook_id, data):
+ def handle_sensor_registration(data):
if data[CONF_WEBHOOK_ID] != webhook_id:
return
- unique_id = sensor_id(data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID])
-
- entity = hass.data[DOMAIN][ENTITY_TYPE][unique_id]
-
- if "added" in entity:
- return
-
- entity["added"] = True
+ data[CONF_UNIQUE_ID] = unique_id(
+ data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID]
+ )
+ data[
+ CONF_NAME
+ ] = f"{config_entry.data[ATTR_DEVICE_NAME]} {data[ATTR_SENSOR_NAME]}"
device = hass.data[DOMAIN][DATA_DEVICES][data[CONF_WEBHOOK_ID]]
@@ -53,11 +66,11 @@ def handle_sensor_registration(webhook_id, data):
async_dispatcher_connect(
hass,
f"{DOMAIN}_{ENTITY_TYPE}_register",
- partial(handle_sensor_registration, webhook_id),
+ handle_sensor_registration,
)
-class MobileAppSensor(MobileAppEntity):
+class MobileAppSensor(MobileAppEntity, SensorEntity):
"""Representation of an mobile app sensor."""
@property
diff --git a/homeassistant/components/mobile_app/strings.json b/homeassistant/components/mobile_app/strings.json
index b18f3e7265c552..9e388ebc76cd71 100644
--- a/homeassistant/components/mobile_app/strings.json
+++ b/homeassistant/components/mobile_app/strings.json
@@ -1,4 +1,5 @@
{
+ "title": "Mobile App",
"config": {
"step": {
"confirm": {
diff --git a/homeassistant/components/mobile_app/translations/ca.json b/homeassistant/components/mobile_app/translations/ca.json
index a36fd1ca13ab8b..70709d1be64611 100644
--- a/homeassistant/components/mobile_app/translations/ca.json
+++ b/homeassistant/components/mobile_app/translations/ca.json
@@ -13,5 +13,6 @@
"action_type": {
"notify": "Envia una notificaci\u00f3"
}
- }
+ },
+ "title": "Aplicaci\u00f3 m\u00f2bil"
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/de.json b/homeassistant/components/mobile_app/translations/de.json
index 493ceb4dfd1bad..721cbc09f8dd48 100644
--- a/homeassistant/components/mobile_app/translations/de.json
+++ b/homeassistant/components/mobile_app/translations/de.json
@@ -13,5 +13,6 @@
"action_type": {
"notify": "Sende eine Benachrichtigung"
}
- }
+ },
+ "title": "Mobile App"
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/en.json b/homeassistant/components/mobile_app/translations/en.json
index 34631f86afa558..0b564a38174e31 100644
--- a/homeassistant/components/mobile_app/translations/en.json
+++ b/homeassistant/components/mobile_app/translations/en.json
@@ -13,5 +13,6 @@
"action_type": {
"notify": "Send a notification"
}
- }
+ },
+ "title": "Mobile App"
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/es.json b/homeassistant/components/mobile_app/translations/es.json
index 43ba004ac722f1..8ac5c909e17dfb 100644
--- a/homeassistant/components/mobile_app/translations/es.json
+++ b/homeassistant/components/mobile_app/translations/es.json
@@ -13,5 +13,6 @@
"action_type": {
"notify": "Enviar una notificaci\u00f3n"
}
- }
+ },
+ "title": "Aplicaci\u00f3n m\u00f3vil"
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/et.json b/homeassistant/components/mobile_app/translations/et.json
index 41d2be9d455bd9..e5b4ea8009c826 100644
--- a/homeassistant/components/mobile_app/translations/et.json
+++ b/homeassistant/components/mobile_app/translations/et.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "install_app": "Home Assistantiga sidumiseks avage mobiilirakendus. \u00dchilduvate rakenduste loendi leiate jaotisest [dokumendid] ( {apps_url} )."
+ "install_app": "Home Assistantiga sidumiseks ava mobiilirakendus. \u00dchilduvate rakenduste loendi leiate jaotisest [dokumendid] ( {apps_url} )."
},
"step": {
"confirm": {
@@ -13,5 +13,6 @@
"action_type": {
"notify": "Saada teavitus"
}
- }
+ },
+ "title": "Mobiilirakendus"
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/fr.json b/homeassistant/components/mobile_app/translations/fr.json
index 09317e4a00df4b..f888b8062f8298 100644
--- a/homeassistant/components/mobile_app/translations/fr.json
+++ b/homeassistant/components/mobile_app/translations/fr.json
@@ -8,5 +8,11 @@
"description": "Voulez-vous configurer le composant Application mobile?"
}
}
- }
+ },
+ "device_automation": {
+ "action_type": {
+ "notify": "Envoyer une notification"
+ }
+ },
+ "title": "Application mobile"
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/hu.json b/homeassistant/components/mobile_app/translations/hu.json
index 301075e0ad4c4f..90690e2545b9f1 100644
--- a/homeassistant/components/mobile_app/translations/hu.json
+++ b/homeassistant/components/mobile_app/translations/hu.json
@@ -13,5 +13,6 @@
"action_type": {
"notify": "\u00c9rtes\u00edt\u00e9s k\u00fcld\u00e9se"
}
- }
+ },
+ "title": "Mobil alkalmaz\u00e1s"
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/id.json b/homeassistant/components/mobile_app/translations/id.json
new file mode 100644
index 00000000000000..d346ca76eabc9d
--- /dev/null
+++ b/homeassistant/components/mobile_app/translations/id.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "install_app": "Buka aplikasi seluler untuk menyiapkan integrasi dengan Home Assistant. Baca [dokumentasi]({apps_url}) tentang daftar aplikasi yang kompatibel."
+ },
+ "step": {
+ "confirm": {
+ "description": "Ingin menyiapkan komponen Aplikasi Seluler?"
+ }
+ }
+ },
+ "device_automation": {
+ "action_type": {
+ "notify": "Kirim notifikasi"
+ }
+ },
+ "title": "Aplikasi Seluler"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/it.json b/homeassistant/components/mobile_app/translations/it.json
index f5ba52b1a53dde..3784a158930fa5 100644
--- a/homeassistant/components/mobile_app/translations/it.json
+++ b/homeassistant/components/mobile_app/translations/it.json
@@ -13,5 +13,6 @@
"action_type": {
"notify": "Invia una notifica"
}
- }
+ },
+ "title": "App per dispositivi mobili"
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/ko.json b/homeassistant/components/mobile_app/translations/ko.json
index 03478e1cf2a264..7f99e1f2100c7e 100644
--- a/homeassistant/components/mobile_app/translations/ko.json
+++ b/homeassistant/components/mobile_app/translations/ko.json
@@ -1,12 +1,18 @@
{
"config": {
"abort": {
- "install_app": "\ubaa8\ubc14\uc77c \uc571\uc744 \uc5f4\uc5b4 Home Assistant \uc640 \uc5f0\ub3d9\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694. \ud638\ud658\ub418\ub294 \uc571 \ubaa9\ub85d\uc740 [\uc548\ub0b4]({apps_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
+ "install_app": "Mobile App\uc744 \uc5f4\uc5b4 Home Assistant\uc640 \uc5f0\ub3d9\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694. \ud638\ud658\ub418\ub294 \uc571 \ubaa9\ub85d\uc740 [\uad00\ub828 \ubb38\uc11c]({apps_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
},
"step": {
"confirm": {
- "description": "\ubaa8\ubc14\uc77c \uc571 \ucef4\ud3ec\ub10c\ud2b8\uc758 \uc124\uc815\uc744 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ "description": "Mobile App \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
}
}
- }
+ },
+ "device_automation": {
+ "action_type": {
+ "notify": "\uc54c\ub9bc \ubcf4\ub0b4\uae30"
+ }
+ },
+ "title": "Mobile App"
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/nl.json b/homeassistant/components/mobile_app/translations/nl.json
index 17a20705cd4cf2..e7bfb6150a5019 100644
--- a/homeassistant/components/mobile_app/translations/nl.json
+++ b/homeassistant/components/mobile_app/translations/nl.json
@@ -13,5 +13,6 @@
"action_type": {
"notify": "Stuur een notificatie"
}
- }
+ },
+ "title": "Mobiele app"
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/no.json b/homeassistant/components/mobile_app/translations/no.json
index 65d465723b1edf..0f6f6dbb0fd8b8 100644
--- a/homeassistant/components/mobile_app/translations/no.json
+++ b/homeassistant/components/mobile_app/translations/no.json
@@ -13,5 +13,6 @@
"action_type": {
"notify": "Send et varsel"
}
- }
+ },
+ "title": "Mobilapp"
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/pl.json b/homeassistant/components/mobile_app/translations/pl.json
index cd083447634632..00a4631684c084 100644
--- a/homeassistant/components/mobile_app/translations/pl.json
+++ b/homeassistant/components/mobile_app/translations/pl.json
@@ -13,5 +13,6 @@
"action_type": {
"notify": "wy\u015blij powiadomienie"
}
- }
+ },
+ "title": "Aplikacja mobilna"
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/ru.json b/homeassistant/components/mobile_app/translations/ru.json
index fc4496ba1d86c3..65b6cc15c6588d 100644
--- a/homeassistant/components/mobile_app/translations/ru.json
+++ b/homeassistant/components/mobile_app/translations/ru.json
@@ -13,5 +13,6 @@
"action_type": {
"notify": "\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435"
}
- }
+ },
+ "title": "\u041c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435"
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/tr.json b/homeassistant/components/mobile_app/translations/tr.json
new file mode 100644
index 00000000000000..10d79751ec12e7
--- /dev/null
+++ b/homeassistant/components/mobile_app/translations/tr.json
@@ -0,0 +1,7 @@
+{
+ "device_automation": {
+ "action_type": {
+ "notify": "Bildirim g\u00f6nder"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/uk.json b/homeassistant/components/mobile_app/translations/uk.json
index 4a48dd3775d5f7..db471bbdc7fa5a 100644
--- a/homeassistant/components/mobile_app/translations/uk.json
+++ b/homeassistant/components/mobile_app/translations/uk.json
@@ -1,9 +1,17 @@
{
"config": {
+ "abort": {
+ "install_app": "\u0412\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u043c\u043e\u0431\u0456\u043b\u044c\u043d\u0438\u0439 \u0434\u043e\u0434\u0430\u0442\u043e\u043a, \u0449\u043e\u0431 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u0437 Home Assistant. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({apps_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0441\u043f\u0438\u0441\u043a\u0443 \u0441\u0443\u043c\u0456\u0441\u043d\u0438\u0445 \u0434\u043e\u0434\u0430\u0442\u043a\u0456\u0432."
+ },
"step": {
"confirm": {
- "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0431\u0456\u043b\u044c\u043d\u043e\u0433\u043e \u0434\u043e\u0434\u0430\u0442\u043a\u0430?"
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043c\u043e\u0431\u0456\u043b\u044c\u043d\u0438\u0439 \u0434\u043e\u0434\u0430\u0442\u043e\u043a?"
}
}
+ },
+ "device_automation": {
+ "action_type": {
+ "notify": "\u041d\u0430\u0434\u0456\u0441\u043b\u0430\u0442\u0438 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u043d\u044f"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/zh-Hans.json b/homeassistant/components/mobile_app/translations/zh-Hans.json
index b48ca1e4263bb4..6a884b156bc90e 100644
--- a/homeassistant/components/mobile_app/translations/zh-Hans.json
+++ b/homeassistant/components/mobile_app/translations/zh-Hans.json
@@ -8,5 +8,11 @@
"description": "\u60a8\u60f3\u8981\u914d\u7f6e\u79fb\u52a8\u5e94\u7528\u7a0b\u5e8f\u7ec4\u4ef6\u5417\uff1f"
}
}
- }
+ },
+ "device_automation": {
+ "action_type": {
+ "notify": "\u63a8\u9001\u901a\u77e5"
+ }
+ },
+ "title": "\u79fb\u52a8\u5e94\u7528"
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/zh-Hant.json b/homeassistant/components/mobile_app/translations/zh-Hant.json
index d54afd94f54bab..2793794b8b6e77 100644
--- a/homeassistant/components/mobile_app/translations/zh-Hant.json
+++ b/homeassistant/components/mobile_app/translations/zh-Hant.json
@@ -13,5 +13,6 @@
"action_type": {
"notify": "\u50b3\u9001\u901a\u77e5"
}
- }
+ },
+ "title": "\u624b\u6a5f App"
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/util.py b/homeassistant/components/mobile_app/util.py
index 60dfe242e0430e..cd4b7c22939310 100644
--- a/homeassistant/components/mobile_app/util.py
+++ b/homeassistant/components/mobile_app/util.py
@@ -1,5 +1,7 @@
"""Mobile app utility functions."""
-from typing import TYPE_CHECKING, Optional
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
from homeassistant.core import callback
@@ -18,7 +20,7 @@
@callback
-def webhook_id_from_device_id(hass, device_id: str) -> Optional[str]:
+def webhook_id_from_device_id(hass, device_id: str) -> str | None:
"""Get webhook ID from device ID."""
if DOMAIN not in hass.data:
return None
@@ -39,9 +41,9 @@ def supports_push(hass, webhook_id: str) -> bool:
@callback
-def get_notify_service(hass, webhook_id: str) -> Optional[str]:
+def get_notify_service(hass, webhook_id: str) -> str | None:
"""Return the notify service for this webhook ID."""
- notify_service: "MobileAppNotificationService" = hass.data[DOMAIN][DATA_NOTIFY]
+ notify_service: MobileAppNotificationService = hass.data[DOMAIN][DATA_NOTIFY]
for target_service, target_webhook_id in notify_service.registered_targets.items():
if target_webhook_id == webhook_id:
diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py
index 043a555b6b757c..6be39f34f00fa1 100644
--- a/homeassistant/components/mobile_app/webhook.py
+++ b/homeassistant/components/mobile_app/webhook.py
@@ -1,5 +1,6 @@
"""Webhook handlers for mobile_app."""
import asyncio
+from contextlib import suppress
from functools import wraps
import logging
import secrets
@@ -23,6 +24,7 @@
from homeassistant.components.sensor import DEVICE_CLASSES as SENSOR_CLASSES
from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN
from homeassistant.const import (
+ ATTR_DEVICE_ID,
ATTR_DOMAIN,
ATTR_SERVICE,
ATTR_SERVICE_DATA,
@@ -36,6 +38,7 @@
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
+ entity_registry as er,
template,
)
from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -48,7 +51,6 @@
ATTR_APP_VERSION,
ATTR_CAMERA_ENTITY_ID,
ATTR_COURSE,
- ATTR_DEVICE_ID,
ATTR_DEVICE_NAME,
ATTR_EVENT_DATA,
ATTR_EVENT_TYPE,
@@ -79,7 +81,6 @@
CONF_SECRET,
DATA_CONFIG_ENTRIES,
DATA_DELETED_IDS,
- DATA_STORE,
DOMAIN,
ERR_ENCRYPTION_ALREADY_ENABLED,
ERR_ENCRYPTION_NOT_AVAILABLE,
@@ -95,7 +96,6 @@
error_response,
registration_context,
safe_registration,
- savable_state,
supports_encryption,
webhook_response,
)
@@ -415,7 +415,10 @@ async def webhook_register_sensor(hass, config_entry, data):
device_name = config_entry.data[ATTR_DEVICE_NAME]
unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}"
- existing_sensor = unique_store_key in hass.data[DOMAIN][entity_type]
+ entity_registry = await er.async_get_registry(hass)
+ existing_sensor = entity_registry.async_get_entity_id(
+ entity_type, DOMAIN, unique_store_key
+ )
data[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID]
@@ -424,16 +427,7 @@ async def webhook_register_sensor(hass, config_entry, data):
_LOGGER.debug(
"Re-register for %s of existing sensor %s", device_name, unique_id
)
- entry = hass.data[DOMAIN][entity_type][unique_store_key]
- data = {**entry, **data}
-
- hass.data[DOMAIN][entity_type][unique_store_key] = data
- hass.data[DOMAIN][DATA_STORE].async_delay_save(
- lambda: savable_state(hass), DELAY_SAVE
- )
-
- if existing_sensor:
async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, data)
else:
register_signal = f"{DOMAIN}_{data[ATTR_SENSOR_TYPE]}_register"
@@ -478,6 +472,7 @@ async def webhook_update_sensor_states(hass, config_entry, data):
device_name = config_entry.data[ATTR_DEVICE_NAME]
resp = {}
+
for sensor in data:
entity_type = sensor[ATTR_SENSOR_TYPE]
@@ -485,7 +480,10 @@ async def webhook_update_sensor_states(hass, config_entry, data):
unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}"
- if unique_store_key not in hass.data[DOMAIN][entity_type]:
+ entity_registry = await er.async_get_registry(hass)
+ if not entity_registry.async_get_entity_id(
+ entity_type, DOMAIN, unique_store_key
+ ):
_LOGGER.error(
"Refusing to update %s non-registered sensor: %s",
device_name,
@@ -498,8 +496,6 @@ async def webhook_update_sensor_states(hass, config_entry, data):
}
continue
- entry = hass.data[DOMAIN][entity_type][unique_store_key]
-
try:
sensor = sensor_schema_full(sensor)
except vol.Invalid as err:
@@ -516,18 +512,11 @@ async def webhook_update_sensor_states(hass, config_entry, data):
}
continue
- new_state = {**entry, **sensor}
-
- hass.data[DOMAIN][entity_type][unique_store_key] = new_state
-
- async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state)
+ sensor[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID]
+ async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, sensor)
resp[unique_id] = {"success": True}
- hass.data[DOMAIN][DATA_STORE].async_delay_save(
- lambda: savable_state(hass), DELAY_SAVE
- )
-
return webhook_response(resp, registration=config_entry.data)
@@ -561,10 +550,8 @@ async def webhook_get_config(hass, config_entry, data):
if CONF_CLOUDHOOK_URL in config_entry.data:
resp[CONF_CLOUDHOOK_URL] = config_entry.data[CONF_CLOUDHOOK_URL]
- try:
+ with suppress(hass.components.cloud.CloudNotAvailable):
resp[CONF_REMOTE_UI_URL] = hass.components.cloud.async_remote_ui_url()
- except hass.components.cloud.CloudNotAvailable:
- pass
return webhook_response(resp, registration=config_entry.data)
diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py
index 77e9b6f7ca9a31..a4e0c21ec5f62b 100644
--- a/homeassistant/components/modbus/__init__.py
+++ b/homeassistant/components/modbus/__init__.py
@@ -1,39 +1,54 @@
"""Support for Modbus."""
-import logging
-import threading
+from typing import Any, Union
-from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient
-from pymodbus.transaction import ModbusRtuFramer
import voluptuous as vol
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
+)
from homeassistant.components.cover import (
DEVICE_CLASSES_SCHEMA as COVER_DEVICE_CLASSES_SCHEMA,
)
+from homeassistant.components.sensor import (
+ DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
+)
+from homeassistant.components.switch import (
+ DEVICE_CLASSES_SCHEMA as SWITCH_DEVICE_CLASSES_SCHEMA,
+)
from homeassistant.const import (
- ATTR_STATE,
+ CONF_ADDRESS,
+ CONF_BINARY_SENSORS,
+ CONF_COMMAND_OFF,
+ CONF_COMMAND_ON,
+ CONF_COUNT,
CONF_COVERS,
CONF_DELAY,
CONF_DEVICE_CLASS,
CONF_HOST,
CONF_METHOD,
CONF_NAME,
+ CONF_OFFSET,
CONF_PORT,
CONF_SCAN_INTERVAL,
+ CONF_SENSORS,
CONF_SLAVE,
CONF_STRUCTURE,
+ CONF_SWITCHES,
+ CONF_TEMPERATURE_UNIT,
CONF_TIMEOUT,
CONF_TYPE,
- EVENT_HOMEASSISTANT_STOP,
+ CONF_UNIT_OF_MEASUREMENT,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.discovery import load_platform
from .const import (
ATTR_ADDRESS,
ATTR_HUB,
+ ATTR_STATE,
ATTR_UNIT,
ATTR_VALUE,
CALL_TYPE_COIL,
+ CALL_TYPE_DISCRETE,
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
CONF_BAUDRATE,
@@ -46,13 +61,15 @@
CONF_INPUT_TYPE,
CONF_MAX_TEMP,
CONF_MIN_TEMP,
- CONF_OFFSET,
CONF_PARITY,
CONF_PRECISION,
CONF_REGISTER,
+ CONF_REVERSE_ORDER,
CONF_SCALE,
CONF_STATE_CLOSED,
CONF_STATE_CLOSING,
+ CONF_STATE_OFF,
+ CONF_STATE_ON,
CONF_STATE_OPEN,
CONF_STATE_OPENING,
CONF_STATUS_REGISTER,
@@ -60,30 +77,57 @@
CONF_STEP,
CONF_STOPBITS,
CONF_TARGET_TEMP,
- CONF_UNIT,
+ CONF_VERIFY_REGISTER,
+ CONF_VERIFY_STATE,
DATA_TYPE_CUSTOM,
DATA_TYPE_FLOAT,
DATA_TYPE_INT,
+ DATA_TYPE_STRING,
DATA_TYPE_UINT,
DEFAULT_HUB,
DEFAULT_SCAN_INTERVAL,
- DEFAULT_SLAVE,
DEFAULT_STRUCTURE_PREFIX,
DEFAULT_TEMP_UNIT,
MODBUS_DOMAIN as DOMAIN,
- SERVICE_WRITE_COIL,
- SERVICE_WRITE_REGISTER,
)
-
-_LOGGER = logging.getLogger(__name__)
+from .modbus import modbus_setup
BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string})
-CLIMATE_SCHEMA = vol.Schema(
+
+def number(value: Any) -> Union[int, float]:
+ """Coerce a value to number without losing precision."""
+ if isinstance(value, int):
+ return value
+ if isinstance(value, float):
+ return value
+
+ try:
+ value = int(value)
+ return value
+ except (TypeError, ValueError):
+ pass
+ try:
+ value = float(value)
+ return value
+ except (TypeError, ValueError) as err:
+ raise vol.Invalid(f"invalid number {value}") from err
+
+
+BASE_COMPONENT_SCHEMA = vol.Schema(
{
- vol.Required(CONF_CURRENT_TEMP): cv.positive_int,
vol.Required(CONF_NAME): cv.string,
- vol.Required(CONF_SLAVE): cv.positive_int,
+ vol.Optional(CONF_SLAVE): cv.positive_int,
+ vol.Optional(
+ CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
+ ): cv.positive_int,
+ }
+)
+
+
+CLIMATE_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
+ {
+ vol.Required(CONF_CURRENT_TEMP): cv.positive_int,
vol.Required(CONF_TARGET_TEMP): cv.positive_int,
vol.Optional(CONF_DATA_COUNT, default=2): cv.positive_int,
vol.Optional(
@@ -94,28 +138,20 @@
),
vol.Optional(CONF_PRECISION, default=1): cv.positive_int,
vol.Optional(CONF_SCALE, default=1): vol.Coerce(float),
- vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): vol.All(
- cv.time_period, lambda value: value.total_seconds()
- ),
vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP, default=35): cv.positive_int,
vol.Optional(CONF_MIN_TEMP, default=5): cv.positive_int,
vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float),
vol.Optional(CONF_STRUCTURE, default=DEFAULT_STRUCTURE_PREFIX): cv.string,
- vol.Optional(CONF_UNIT, default=DEFAULT_TEMP_UNIT): cv.string,
+ vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string,
}
)
COVERS_SCHEMA = vol.All(
cv.has_at_least_one_key(CALL_TYPE_COIL, CONF_REGISTER),
- vol.Schema(
+ BASE_COMPONENT_SCHEMA.extend(
{
- vol.Required(CONF_NAME): cv.string,
- vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): vol.All(
- cv.time_period, lambda value: value.total_seconds()
- ),
vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA,
- vol.Optional(CONF_SLAVE, default=DEFAULT_SLAVE): cv.positive_int,
vol.Optional(CONF_STATE_CLOSED, default=0): cv.positive_int,
vol.Optional(CONF_STATE_CLOSING, default=3): cv.positive_int,
vol.Optional(CONF_STATE_OPEN, default=1): cv.positive_int,
@@ -131,33 +167,105 @@
),
)
-SERIAL_SCHEMA = BASE_SCHEMA.extend(
+SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
+ {
+ vol.Required(CONF_ADDRESS): cv.positive_int,
+ vol.Optional(CONF_DEVICE_CLASS): SWITCH_DEVICE_CLASSES_SCHEMA,
+ vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In(
+ [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, CALL_TYPE_COIL]
+ ),
+ vol.Optional(CONF_COMMAND_OFF, default=0x00): cv.positive_int,
+ vol.Optional(CONF_COMMAND_ON, default=0x01): cv.positive_int,
+ vol.Optional(CONF_STATE_OFF): cv.positive_int,
+ vol.Optional(CONF_STATE_ON): cv.positive_int,
+ vol.Optional(CONF_VERIFY_REGISTER): cv.positive_int,
+ vol.Optional(CONF_VERIFY_STATE, default=True): cv.boolean,
+ }
+)
+
+SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
+ {
+ vol.Required(CONF_ADDRESS): cv.positive_int,
+ vol.Optional(CONF_COUNT, default=1): cv.positive_int,
+ vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): vol.In(
+ [
+ DATA_TYPE_INT,
+ DATA_TYPE_UINT,
+ DATA_TYPE_FLOAT,
+ DATA_TYPE_STRING,
+ DATA_TYPE_CUSTOM,
+ ]
+ ),
+ vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
+ vol.Optional(CONF_OFFSET, default=0): number,
+ vol.Optional(CONF_PRECISION, default=0): cv.positive_int,
+ vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In(
+ [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]
+ ),
+ vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean,
+ vol.Optional(CONF_SCALE, default=1): number,
+ vol.Optional(CONF_STRUCTURE): cv.string,
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
+ }
+)
+
+BINARY_SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
+ {
+ vol.Required(CONF_ADDRESS): cv.positive_int,
+ vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
+ vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_COIL): vol.In(
+ [CALL_TYPE_COIL, CALL_TYPE_DISCRETE]
+ ),
+ }
+)
+
+MODBUS_SCHEMA = vol.Schema(
{
+ vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string,
+ vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
+ vol.Optional(CONF_DELAY, default=0): cv.positive_int,
+ vol.Optional(CONF_BINARY_SENSORS): vol.All(
+ cv.ensure_list, [BINARY_SENSOR_SCHEMA]
+ ),
+ vol.Optional(CONF_CLIMATES): vol.All(cv.ensure_list, [CLIMATE_SCHEMA]),
+ vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]),
+ vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA]),
+ vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA]),
+ }
+)
+
+SERIAL_SCHEMA = MODBUS_SCHEMA.extend(
+ {
+ vol.Required(CONF_TYPE): "serial",
vol.Required(CONF_BAUDRATE): cv.positive_int,
vol.Required(CONF_BYTESIZE): vol.Any(5, 6, 7, 8),
vol.Required(CONF_METHOD): vol.Any("rtu", "ascii"),
vol.Required(CONF_PORT): cv.string,
vol.Required(CONF_PARITY): vol.Any("E", "O", "N"),
vol.Required(CONF_STOPBITS): vol.Any(1, 2),
- vol.Required(CONF_TYPE): "serial",
- vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
- vol.Optional(CONF_CLIMATES): vol.All(cv.ensure_list, [CLIMATE_SCHEMA]),
- vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]),
}
)
-ETHERNET_SCHEMA = BASE_SCHEMA.extend(
+ETHERNET_SCHEMA = MODBUS_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Required(CONF_TYPE): vol.Any("tcp", "udp", "rtuovertcp"),
- vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
- vol.Optional(CONF_DELAY, default=0): cv.positive_int,
- vol.Optional(CONF_CLIMATES): vol.All(cv.ensure_list, [CLIMATE_SCHEMA]),
- vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]),
}
)
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.All(
+ cv.ensure_list,
+ [
+ vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA),
+ ],
+ ),
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
SERVICE_WRITE_REGISTER_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string,
@@ -174,206 +282,15 @@
vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string,
vol.Required(ATTR_UNIT): cv.positive_int,
vol.Required(ATTR_ADDRESS): cv.positive_int,
- vol.Required(ATTR_STATE): cv.boolean,
- }
-)
-
-CONFIG_SCHEMA = vol.Schema(
- {
- DOMAIN: vol.All(
- cv.ensure_list,
- [
- vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA),
- ],
+ vol.Required(ATTR_STATE): vol.Any(
+ cv.boolean, vol.All(cv.ensure_list, [cv.boolean])
),
- },
- extra=vol.ALLOW_EXTRA,
+ }
)
def setup(hass, config):
"""Set up Modbus component."""
- hass.data[DOMAIN] = hub_collect = {}
-
- for conf_hub in config[DOMAIN]:
- hub_collect[conf_hub[CONF_NAME]] = ModbusHub(conf_hub)
-
- # load platforms
- for component, conf_key in (
- ("climate", CONF_CLIMATES),
- ("cover", CONF_COVERS),
- ):
- if conf_key in conf_hub:
- load_platform(hass, component, DOMAIN, conf_hub, config)
-
- def stop_modbus(event):
- """Stop Modbus service."""
- for client in hub_collect.values():
- client.close()
-
- def write_register(service):
- """Write Modbus registers."""
- unit = int(float(service.data[ATTR_UNIT]))
- address = int(float(service.data[ATTR_ADDRESS]))
- value = service.data[ATTR_VALUE]
- client_name = service.data[ATTR_HUB]
- if isinstance(value, list):
- hub_collect[client_name].write_registers(
- unit, address, [int(float(i)) for i in value]
- )
- else:
- hub_collect[client_name].write_register(unit, address, int(float(value)))
-
- def write_coil(service):
- """Write Modbus coil."""
- unit = service.data[ATTR_UNIT]
- address = service.data[ATTR_ADDRESS]
- state = service.data[ATTR_STATE]
- client_name = service.data[ATTR_HUB]
- hub_collect[client_name].write_coil(unit, address, state)
-
- # do not wait for EVENT_HOMEASSISTANT_START, activate pymodbus now
- for client in hub_collect.values():
- client.setup()
-
- # register function to gracefully stop modbus
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus)
-
- # Register services for modbus
- hass.services.register(
- DOMAIN,
- SERVICE_WRITE_REGISTER,
- write_register,
- schema=SERVICE_WRITE_REGISTER_SCHEMA,
- )
- hass.services.register(
- DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA
+ return modbus_setup(
+ hass, config, SERVICE_WRITE_REGISTER_SCHEMA, SERVICE_WRITE_COIL_SCHEMA
)
- return True
-
-
-class ModbusHub:
- """Thread safe wrapper class for pymodbus."""
-
- def __init__(self, client_config):
- """Initialize the Modbus hub."""
-
- # generic configuration
- self._client = None
- self._lock = threading.Lock()
- self._config_name = client_config[CONF_NAME]
- self._config_type = client_config[CONF_TYPE]
- self._config_port = client_config[CONF_PORT]
- self._config_timeout = client_config[CONF_TIMEOUT]
- self._config_delay = 0
-
- if self._config_type == "serial":
- # serial configuration
- self._config_method = client_config[CONF_METHOD]
- self._config_baudrate = client_config[CONF_BAUDRATE]
- self._config_stopbits = client_config[CONF_STOPBITS]
- self._config_bytesize = client_config[CONF_BYTESIZE]
- self._config_parity = client_config[CONF_PARITY]
- else:
- # network configuration
- self._config_host = client_config[CONF_HOST]
- self._config_delay = client_config[CONF_DELAY]
- if self._config_delay > 0:
- _LOGGER.warning(
- "Parameter delay is accepted but not used in this version"
- )
-
- @property
- def name(self):
- """Return the name of this hub."""
- return self._config_name
-
- def setup(self):
- """Set up pymodbus client."""
- if self._config_type == "serial":
- self._client = ModbusSerialClient(
- method=self._config_method,
- port=self._config_port,
- baudrate=self._config_baudrate,
- stopbits=self._config_stopbits,
- bytesize=self._config_bytesize,
- parity=self._config_parity,
- timeout=self._config_timeout,
- retry_on_empty=True,
- )
- elif self._config_type == "rtuovertcp":
- self._client = ModbusTcpClient(
- host=self._config_host,
- port=self._config_port,
- framer=ModbusRtuFramer,
- timeout=self._config_timeout,
- )
- elif self._config_type == "tcp":
- self._client = ModbusTcpClient(
- host=self._config_host,
- port=self._config_port,
- timeout=self._config_timeout,
- )
- elif self._config_type == "udp":
- self._client = ModbusUdpClient(
- host=self._config_host,
- port=self._config_port,
- timeout=self._config_timeout,
- )
- else:
- assert False
-
- # Connect device
- self.connect()
-
- def close(self):
- """Disconnect client."""
- with self._lock:
- self._client.close()
-
- def connect(self):
- """Connect client."""
- with self._lock:
- self._client.connect()
-
- def read_coils(self, unit, address, count):
- """Read coils."""
- with self._lock:
- kwargs = {"unit": unit} if unit else {}
- return self._client.read_coils(address, count, **kwargs)
-
- def read_discrete_inputs(self, unit, address, count):
- """Read discrete inputs."""
- with self._lock:
- kwargs = {"unit": unit} if unit else {}
- return self._client.read_discrete_inputs(address, count, **kwargs)
-
- def read_input_registers(self, unit, address, count):
- """Read input registers."""
- with self._lock:
- kwargs = {"unit": unit} if unit else {}
- return self._client.read_input_registers(address, count, **kwargs)
-
- def read_holding_registers(self, unit, address, count):
- """Read holding registers."""
- with self._lock:
- kwargs = {"unit": unit} if unit else {}
- return self._client.read_holding_registers(address, count, **kwargs)
-
- def write_coil(self, unit, address, value):
- """Write coil."""
- with self._lock:
- kwargs = {"unit": unit} if unit else {}
- self._client.write_coil(address, value, **kwargs)
-
- def write_register(self, unit, address, value):
- """Write register."""
- with self._lock:
- kwargs = {"unit": unit} if unit else {}
- self._client.write_register(address, value, **kwargs)
-
- def write_registers(self, unit, address, values):
- """Write registers."""
- with self._lock:
- kwargs = {"unit": unit} if unit else {}
- self._client.write_registers(address, values, **kwargs)
diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py
index c9e9cc4196a5bf..e422eb7528ea80 100644
--- a/homeassistant/components/modbus/binary_sensor.py
+++ b/homeassistant/components/modbus/binary_sensor.py
@@ -1,5 +1,8 @@
"""Support for Modbus Coil and Discrete Input sensors."""
-from typing import Optional
+from __future__ import annotations
+
+from datetime import timedelta
+import logging
from pymodbus.exceptions import ConnectionException, ModbusException
from pymodbus.pdu import ExceptionResponse
@@ -10,20 +13,37 @@
PLATFORM_SCHEMA,
BinarySensorEntity,
)
-from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_SLAVE
+from homeassistant.const import (
+ CONF_ADDRESS,
+ CONF_BINARY_SENSORS,
+ CONF_DEVICE_CLASS,
+ CONF_NAME,
+ CONF_SCAN_INTERVAL,
+ CONF_SLAVE,
+)
from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.helpers.typing import (
+ ConfigType,
+ DiscoveryInfoType,
+ HomeAssistantType,
+)
from .const import (
CALL_TYPE_COIL,
CALL_TYPE_DISCRETE,
- CONF_ADDRESS,
CONF_COILS,
CONF_HUB,
CONF_INPUT_TYPE,
CONF_INPUTS,
DEFAULT_HUB,
+ DEFAULT_SCAN_INTERVAL,
MODBUS_DOMAIN,
)
+from .modbus import ModbusHub
+
+_LOGGER = logging.getLogger(__name__)
+
PLATFORM_SCHEMA = vol.All(
cv.deprecated(CONF_COILS, CONF_INPUTS),
@@ -51,11 +71,33 @@
)
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(
+ hass: HomeAssistantType,
+ config: ConfigType,
+ async_add_entities,
+ discovery_info: DiscoveryInfoType | None = None,
+):
"""Set up the Modbus binary sensors."""
sensors = []
- for entry in config[CONF_INPUTS]:
- hub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]]
+
+ # check for old config:
+ if discovery_info is None:
+ _LOGGER.warning(
+ "Binary_sensor configuration is deprecated, will be removed in a future release"
+ )
+ discovery_info = {
+ CONF_NAME: "no name",
+ CONF_BINARY_SENSORS: config[CONF_INPUTS],
+ }
+ config = None
+
+ for entry in discovery_info[CONF_BINARY_SENSORS]:
+ if CONF_HUB in entry:
+ # from old config!
+ discovery_info[CONF_NAME] = entry[CONF_HUB]
+ if CONF_SCAN_INTERVAL not in entry:
+ entry[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL
+ hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]]
sensors.append(
ModbusBinarySensor(
hub,
@@ -64,16 +106,19 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
entry[CONF_ADDRESS],
entry.get(CONF_DEVICE_CLASS),
entry[CONF_INPUT_TYPE],
+ entry[CONF_SCAN_INTERVAL],
)
)
- add_entities(sensors)
+ async_add_entities(sensors)
class ModbusBinarySensor(BinarySensorEntity):
"""Modbus binary sensor."""
- def __init__(self, hub, name, slave, address, device_class, input_type):
+ def __init__(
+ self, hub, name, slave, address, device_class, input_type, scan_interval
+ ):
"""Initialize the Modbus binary sensor."""
self._hub = hub
self._name = name
@@ -83,6 +128,13 @@ def __init__(self, hub, name, slave, address, device_class, input_type):
self._input_type = input_type
self._value = None
self._available = True
+ self._scan_interval = timedelta(seconds=scan_interval)
+
+ async def async_added_to_hass(self):
+ """Handle entity which will be added."""
+ async_track_time_interval(
+ self.hass, lambda arg: self._update(), self._scan_interval
+ )
@property
def name(self):
@@ -95,16 +147,26 @@ def is_on(self):
return self._value
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the device class of the sensor."""
return self._device_class
+ @property
+ def should_poll(self):
+ """Return True if entity has to be polled for state.
+
+ False if entity pushes its state to HA.
+ """
+
+ # Handle polling directly in this entity
+ return False
+
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._available
- def update(self):
+ def _update(self):
"""Update the state of the sensor."""
try:
if self._input_type == CALL_TYPE_COIL:
@@ -121,3 +183,4 @@ def update(self):
self._value = result.bits[0] & 1
self._available = True
+ self.schedule_update_ha_state()
diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py
index b09a38f082eb05..6140ac038f7471 100644
--- a/homeassistant/components/modbus/climate.py
+++ b/homeassistant/components/modbus/climate.py
@@ -1,8 +1,10 @@
"""Support for Generic Modbus Thermostats."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
import struct
-from typing import Any, Dict, Optional
+from typing import Any
from pymodbus.exceptions import ConnectionException, ModbusException
from pymodbus.pdu import ExceptionResponse
@@ -13,11 +15,12 @@
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.const import (
- ATTR_TEMPERATURE,
CONF_NAME,
+ CONF_OFFSET,
CONF_SCAN_INTERVAL,
CONF_SLAVE,
CONF_STRUCTURE,
+ CONF_TEMPERATURE_UNIT,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
@@ -28,8 +31,8 @@
HomeAssistantType,
)
-from . import ModbusHub
from .const import (
+ ATTR_TEMPERATURE,
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
CONF_CLIMATES,
@@ -39,16 +42,15 @@
CONF_DATA_TYPE,
CONF_MAX_TEMP,
CONF_MIN_TEMP,
- CONF_OFFSET,
CONF_PRECISION,
CONF_SCALE,
CONF_STEP,
CONF_TARGET_TEMP,
- CONF_UNIT,
DATA_TYPE_CUSTOM,
DEFAULT_STRUCT_FORMAT,
MODBUS_DOMAIN,
)
+from .modbus import ModbusHub
_LOGGER = logging.getLogger(__name__)
@@ -57,7 +59,7 @@ async def async_setup_platform(
hass: HomeAssistantType,
config: ConfigType,
async_add_entities,
- discovery_info: Optional[DiscoveryInfoType] = None,
+ discovery_info: DiscoveryInfoType | None = None,
):
"""Read configuration and create Modbus climate."""
if discovery_info is None:
@@ -108,12 +110,12 @@ class ModbusThermostat(ClimateEntity):
def __init__(
self,
hub: ModbusHub,
- config: Dict[str, Any],
+ config: dict[str, Any],
):
"""Initialize the modbus thermostat."""
self._hub: ModbusHub = hub
self._name = config[CONF_NAME]
- self._slave = config[CONF_SLAVE]
+ self._slave = config.get(CONF_SLAVE)
self._target_temperature_register = config[CONF_TARGET_TEMP]
self._current_temperature_register = config[CONF_CURRENT_TEMP]
self._current_temperature_register_type = config[
@@ -128,7 +130,7 @@ def __init__(
self._scale = config[CONF_SCALE]
self._scan_interval = timedelta(seconds=config[CONF_SCAN_INTERVAL])
self._offset = config[CONF_OFFSET]
- self._unit = config[CONF_UNIT]
+ self._unit = config[CONF_TEMPERATURE_UNIT]
self._max_temp = config[CONF_MAX_TEMP]
self._min_temp = config[CONF_MIN_TEMP]
self._temp_step = config[CONF_STEP]
@@ -146,7 +148,6 @@ def should_poll(self):
False if entity pushes its state to HA.
"""
-
# Handle polling directly in this entity
return False
@@ -207,11 +208,11 @@ def target_temperature_step(self):
def set_temperature(self, **kwargs):
"""Set new target temperature."""
+ if ATTR_TEMPERATURE not in kwargs:
+ return
target_temperature = int(
(kwargs.get(ATTR_TEMPERATURE) - self._offset) / self._scale
)
- if target_temperature is None:
- return
byte_string = struct.pack(self._structure, target_temperature)
register_value = struct.unpack(">h", byte_string[0:2])[0]
self._write_register(self._target_temperature_register, register_value)
@@ -233,7 +234,7 @@ def _update(self):
self.schedule_update_ha_state()
- def _read_register(self, register_type, register) -> Optional[float]:
+ def _read_register(self, register_type, register) -> float | None:
"""Read register using the Modbus hub slave."""
try:
if register_type == CALL_TYPE_REGISTER_INPUT:
@@ -275,7 +276,7 @@ def _read_register(self, register_type, register) -> Optional[float]:
def _write_register(self, register, value):
"""Write holding register using the Modbus hub slave."""
try:
- self._hub.write_registers(self._slave, register, [value, 0])
+ self._hub.write_registers(self._slave, register, value)
except ConnectionException:
self._available = False
return
diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py
index e79b69bbb87842..ffe89757ef127d 100644
--- a/homeassistant/components/modbus/const.py
+++ b/homeassistant/components/modbus/const.py
@@ -2,23 +2,51 @@
# configuration names
CONF_BAUDRATE = "baudrate"
+CONF_BINARY_SENSOR = "binary_sensor"
CONF_BYTESIZE = "bytesize"
+CONF_CLIMATE = "climate"
+CONF_CLIMATES = "climates"
+CONF_COILS = "coils"
+CONF_COVER = "cover"
+CONF_CURRENT_TEMP = "current_temp_register"
+CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type"
+CONF_DATA_COUNT = "data_count"
+CONF_DATA_TYPE = "data_type"
CONF_HUB = "hub"
+CONF_INPUTS = "inputs"
+CONF_INPUT_TYPE = "input_type"
+CONF_MAX_TEMP = "max_temp"
+CONF_MIN_TEMP = "min_temp"
CONF_PARITY = "parity"
-CONF_STOPBITS = "stopbits"
CONF_REGISTER = "register"
CONF_REGISTER_TYPE = "register_type"
CONF_REGISTERS = "registers"
CONF_REVERSE_ORDER = "reverse_order"
-CONF_SCALE = "scale"
-CONF_COUNT = "count"
CONF_PRECISION = "precision"
-CONF_OFFSET = "offset"
-CONF_COILS = "coils"
+CONF_SCALE = "scale"
+CONF_SENSOR = "sensor"
+CONF_STATE_CLOSED = "state_closed"
+CONF_STATE_CLOSING = "state_closing"
+CONF_STATE_OFF = "state_off"
+CONF_STATE_ON = "state_on"
+CONF_STATE_OPEN = "state_open"
+CONF_STATE_OPENING = "state_opening"
+CONF_STATUS_REGISTER = "status_register"
+CONF_STATUS_REGISTER_TYPE = "status_register_type"
+CONF_STEP = "temp_step"
+CONF_STOPBITS = "stopbits"
+CONF_SWITCH = "switch"
+CONF_TARGET_TEMP = "target_temp_register"
+CONF_VERIFY_REGISTER = "verify_register"
+CONF_VERIFY_STATE = "verify_state"
-# integration names
-DEFAULT_HUB = "default"
-MODBUS_DOMAIN = "modbus"
+# service call attributes
+ATTR_ADDRESS = "address"
+ATTR_HUB = "hub"
+ATTR_UNIT = "unit"
+ATTR_VALUE = "value"
+ATTR_STATE = "state"
+ATTR_TEMPERATURE = "temperature"
# data types
DATA_TYPE_CUSTOM = "custom"
@@ -33,59 +61,19 @@
CALL_TYPE_REGISTER_HOLDING = "holding"
CALL_TYPE_REGISTER_INPUT = "input"
-# the following constants are TBD.
-# changing those in general causes a breaking change, because
-# the contents of configuration.yaml needs to be updated,
-# therefore they are left to a later date.
-# but kept here, with a reference to the file using them.
-
-# __init.py
-ATTR_ADDRESS = "address"
-ATTR_HUB = "hub"
-ATTR_UNIT = "unit"
-ATTR_VALUE = "value"
+# service calls
SERVICE_WRITE_COIL = "write_coil"
SERVICE_WRITE_REGISTER = "write_register"
-DEFAULT_SCAN_INTERVAL = 15 # seconds
-# binary_sensor.py
-CONF_INPUTS = "inputs"
-CONF_INPUT_TYPE = "input_type"
-CONF_ADDRESS = "address"
-
-# sensor.py
-# CONF_DATA_TYPE = "data_type"
+# integration names
+DEFAULT_HUB = "modbus_hub"
+DEFAULT_SCAN_INTERVAL = 15 # seconds
+DEFAULT_SLAVE = 1
+DEFAULT_STRUCTURE_PREFIX = ">f"
DEFAULT_STRUCT_FORMAT = {
DATA_TYPE_INT: {1: "h", 2: "i", 4: "q"},
DATA_TYPE_UINT: {1: "H", 2: "I", 4: "Q"},
DATA_TYPE_FLOAT: {1: "e", 2: "f", 4: "d"},
}
-
-# switch.py
-CONF_STATE_OFF = "state_off"
-CONF_STATE_ON = "state_on"
-CONF_VERIFY_REGISTER = "verify_register"
-CONF_VERIFY_STATE = "verify_state"
-
-# climate.py
-CONF_CLIMATES = "climates"
-CONF_TARGET_TEMP = "target_temp_register"
-CONF_CURRENT_TEMP = "current_temp_register"
-CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type"
-CONF_DATA_TYPE = "data_type"
-CONF_DATA_COUNT = "data_count"
-CONF_UNIT = "temperature_unit"
-CONF_MAX_TEMP = "max_temp"
-CONF_MIN_TEMP = "min_temp"
-CONF_STEP = "temp_step"
-DEFAULT_STRUCTURE_PREFIX = ">f"
DEFAULT_TEMP_UNIT = "C"
-
-# cover.py
-CONF_STATE_OPEN = "state_open"
-CONF_STATE_CLOSED = "state_closed"
-CONF_STATE_OPENING = "state_opening"
-CONF_STATE_CLOSING = "state_closing"
-CONF_STATUS_REGISTER = "status_register"
-CONF_STATUS_REGISTER_TYPE = "status_register_type"
-DEFAULT_SLAVE = 1
+MODBUS_DOMAIN = "modbus"
diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py
index ab16e5306f112b..bc7c946402b5c4 100644
--- a/homeassistant/components/modbus/cover.py
+++ b/homeassistant/components/modbus/cover.py
@@ -1,6 +1,8 @@
"""Support for Modbus covers."""
+from __future__ import annotations
+
from datetime import timedelta
-from typing import Any, Dict, Optional
+from typing import Any
from pymodbus.exceptions import ConnectionException, ModbusException
from pymodbus.pdu import ExceptionResponse
@@ -21,7 +23,6 @@
HomeAssistantType,
)
-from . import ModbusHub
from .const import (
CALL_TYPE_COIL,
CALL_TYPE_REGISTER_HOLDING,
@@ -35,13 +36,14 @@
CONF_STATUS_REGISTER_TYPE,
MODBUS_DOMAIN,
)
+from .modbus import ModbusHub
async def async_setup_platform(
hass: HomeAssistantType,
config: ConfigType,
async_add_entities,
- discovery_info: Optional[DiscoveryInfoType] = None,
+ discovery_info: DiscoveryInfoType | None = None,
):
"""Read configuration and create Modbus cover."""
if discovery_info is None:
@@ -61,7 +63,7 @@ class ModbusCover(CoverEntity, RestoreEntity):
def __init__(
self,
hub: ModbusHub,
- config: Dict[str, Any],
+ config: dict[str, Any],
):
"""Initialize the modbus cover."""
self._hub: ModbusHub = hub
@@ -69,7 +71,7 @@ def __init__(
self._device_class = config.get(CONF_DEVICE_CLASS)
self._name = config[CONF_NAME]
self._register = config.get(CONF_REGISTER)
- self._slave = config[CONF_SLAVE]
+ self._slave = config.get(CONF_SLAVE)
self._state_closed = config[CONF_STATE_CLOSED]
self._state_closing = config[CONF_STATE_CLOSING]
self._state_open = config[CONF_STATE_OPEN]
@@ -108,7 +110,7 @@ async def async_added_to_hass(self):
)
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the device class of the sensor."""
return self._device_class
@@ -148,7 +150,6 @@ def should_poll(self):
False if entity pushes its state to HA.
"""
-
# Handle polling directly in this entity
return False
@@ -179,7 +180,7 @@ def _update(self):
self.schedule_update_ha_state()
- def _read_status_register(self) -> Optional[int]:
+ def _read_status_register(self) -> int | None:
"""Read status register using the Modbus hub slave."""
try:
if self._status_register_type == CALL_TYPE_REGISTER_INPUT:
@@ -213,7 +214,7 @@ def _write_register(self, value):
self._available = True
- def _read_coil(self) -> Optional[bool]:
+ def _read_coil(self) -> bool | None:
"""Read coil using the Modbus hub slave."""
try:
result = self._hub.read_coils(self._slave, self._coil, 1)
diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py
new file mode 100644
index 00000000000000..0a5422ff6be0eb
--- /dev/null
+++ b/homeassistant/components/modbus/modbus.py
@@ -0,0 +1,245 @@
+"""Support for Modbus."""
+import logging
+import threading
+
+from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient
+from pymodbus.transaction import ModbusRtuFramer
+
+from homeassistant.const import (
+ CONF_BINARY_SENSORS,
+ CONF_COVERS,
+ CONF_DELAY,
+ CONF_HOST,
+ CONF_METHOD,
+ CONF_NAME,
+ CONF_PORT,
+ CONF_SENSORS,
+ CONF_SWITCHES,
+ CONF_TIMEOUT,
+ CONF_TYPE,
+ EVENT_HOMEASSISTANT_STOP,
+)
+from homeassistant.helpers.discovery import load_platform
+
+from .const import (
+ ATTR_ADDRESS,
+ ATTR_HUB,
+ ATTR_STATE,
+ ATTR_UNIT,
+ ATTR_VALUE,
+ CONF_BAUDRATE,
+ CONF_BINARY_SENSOR,
+ CONF_BYTESIZE,
+ CONF_CLIMATE,
+ CONF_CLIMATES,
+ CONF_COVER,
+ CONF_PARITY,
+ CONF_SENSOR,
+ CONF_STOPBITS,
+ CONF_SWITCH,
+ MODBUS_DOMAIN as DOMAIN,
+ SERVICE_WRITE_COIL,
+ SERVICE_WRITE_REGISTER,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def modbus_setup(
+ hass, config, service_write_register_schema, service_write_coil_schema
+):
+ """Set up Modbus component."""
+ hass.data[DOMAIN] = hub_collect = {}
+
+ for conf_hub in config[DOMAIN]:
+ hub_collect[conf_hub[CONF_NAME]] = ModbusHub(conf_hub)
+
+ # modbus needs to be activated before components are loaded
+ # to avoid a racing problem
+ hub_collect[conf_hub[CONF_NAME]].setup()
+
+ # load platforms
+ for component, conf_key in (
+ (CONF_CLIMATE, CONF_CLIMATES),
+ (CONF_COVER, CONF_COVERS),
+ (CONF_BINARY_SENSOR, CONF_BINARY_SENSORS),
+ (CONF_SENSOR, CONF_SENSORS),
+ (CONF_SWITCH, CONF_SWITCHES),
+ ):
+ if conf_key in conf_hub:
+ load_platform(hass, component, DOMAIN, conf_hub, config)
+
+ def stop_modbus(event):
+ """Stop Modbus service."""
+ for client in hub_collect.values():
+ client.close()
+
+ def write_register(service):
+ """Write Modbus registers."""
+ unit = int(float(service.data[ATTR_UNIT]))
+ address = int(float(service.data[ATTR_ADDRESS]))
+ value = service.data[ATTR_VALUE]
+ client_name = service.data[ATTR_HUB]
+ if isinstance(value, list):
+ hub_collect[client_name].write_registers(
+ unit, address, [int(float(i)) for i in value]
+ )
+ else:
+ hub_collect[client_name].write_register(unit, address, int(float(value)))
+
+ def write_coil(service):
+ """Write Modbus coil."""
+ unit = service.data[ATTR_UNIT]
+ address = service.data[ATTR_ADDRESS]
+ state = service.data[ATTR_STATE]
+ client_name = service.data[ATTR_HUB]
+ if isinstance(state, list):
+ hub_collect[client_name].write_coils(unit, address, state)
+ else:
+ hub_collect[client_name].write_coil(unit, address, state)
+
+ # register function to gracefully stop modbus
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus)
+
+ # Register services for modbus
+ hass.services.register(
+ DOMAIN,
+ SERVICE_WRITE_REGISTER,
+ write_register,
+ schema=service_write_register_schema,
+ )
+ hass.services.register(
+ DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=service_write_coil_schema
+ )
+ return True
+
+
+class ModbusHub:
+ """Thread safe wrapper class for pymodbus."""
+
+ def __init__(self, client_config):
+ """Initialize the Modbus hub."""
+
+ # generic configuration
+ self._client = None
+ self._lock = threading.Lock()
+ self._config_name = client_config[CONF_NAME]
+ self._config_type = client_config[CONF_TYPE]
+ self._config_port = client_config[CONF_PORT]
+ self._config_timeout = client_config[CONF_TIMEOUT]
+ self._config_delay = 0
+
+ if self._config_type == "serial":
+ # serial configuration
+ self._config_method = client_config[CONF_METHOD]
+ self._config_baudrate = client_config[CONF_BAUDRATE]
+ self._config_stopbits = client_config[CONF_STOPBITS]
+ self._config_bytesize = client_config[CONF_BYTESIZE]
+ self._config_parity = client_config[CONF_PARITY]
+ else:
+ # network configuration
+ self._config_host = client_config[CONF_HOST]
+ self._config_delay = client_config[CONF_DELAY]
+ if self._config_delay > 0:
+ _LOGGER.warning(
+ "Parameter delay is accepted but not used in this version"
+ )
+
+ @property
+ def name(self):
+ """Return the name of this hub."""
+ return self._config_name
+
+ def setup(self):
+ """Set up pymodbus client."""
+ if self._config_type == "serial":
+ self._client = ModbusSerialClient(
+ method=self._config_method,
+ port=self._config_port,
+ baudrate=self._config_baudrate,
+ stopbits=self._config_stopbits,
+ bytesize=self._config_bytesize,
+ parity=self._config_parity,
+ timeout=self._config_timeout,
+ retry_on_empty=True,
+ )
+ elif self._config_type == "rtuovertcp":
+ self._client = ModbusTcpClient(
+ host=self._config_host,
+ port=self._config_port,
+ framer=ModbusRtuFramer,
+ timeout=self._config_timeout,
+ )
+ elif self._config_type == "tcp":
+ self._client = ModbusTcpClient(
+ host=self._config_host,
+ port=self._config_port,
+ timeout=self._config_timeout,
+ )
+ elif self._config_type == "udp":
+ self._client = ModbusUdpClient(
+ host=self._config_host,
+ port=self._config_port,
+ timeout=self._config_timeout,
+ )
+
+ # Connect device
+ self.connect()
+
+ def close(self):
+ """Disconnect client."""
+ with self._lock:
+ self._client.close()
+
+ def connect(self):
+ """Connect client."""
+ with self._lock:
+ self._client.connect()
+
+ def read_coils(self, unit, address, count):
+ """Read coils."""
+ with self._lock:
+ kwargs = {"unit": unit} if unit else {}
+ return self._client.read_coils(address, count, **kwargs)
+
+ def read_discrete_inputs(self, unit, address, count):
+ """Read discrete inputs."""
+ with self._lock:
+ kwargs = {"unit": unit} if unit else {}
+ return self._client.read_discrete_inputs(address, count, **kwargs)
+
+ def read_input_registers(self, unit, address, count):
+ """Read input registers."""
+ with self._lock:
+ kwargs = {"unit": unit} if unit else {}
+ return self._client.read_input_registers(address, count, **kwargs)
+
+ def read_holding_registers(self, unit, address, count):
+ """Read holding registers."""
+ with self._lock:
+ kwargs = {"unit": unit} if unit else {}
+ return self._client.read_holding_registers(address, count, **kwargs)
+
+ def write_coil(self, unit, address, value):
+ """Write coil."""
+ with self._lock:
+ kwargs = {"unit": unit} if unit else {}
+ self._client.write_coil(address, value, **kwargs)
+
+ def write_coils(self, unit, address, value):
+ """Write coil."""
+ with self._lock:
+ kwargs = {"unit": unit} if unit else {}
+ self._client.write_coils(address, value, **kwargs)
+
+ def write_register(self, unit, address, value):
+ """Write register."""
+ with self._lock:
+ kwargs = {"unit": unit} if unit else {}
+ self._client.write_register(address, value, **kwargs)
+
+ def write_registers(self, unit, address, values):
+ """Write registers."""
+ with self._lock:
+ kwargs = {"unit": unit} if unit else {}
+ self._client.write_registers(address, values, **kwargs)
diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py
index 656e5e2986d0af..21069d8642773c 100644
--- a/homeassistant/components/modbus/sensor.py
+++ b/homeassistant/components/modbus/sensor.py
@@ -1,30 +1,47 @@
"""Support for Modbus Register sensors."""
+from __future__ import annotations
+
+from datetime import timedelta
import logging
import struct
-from typing import Any, Optional, Union
+from typing import Any
from pymodbus.exceptions import ConnectionException, ModbusException
from pymodbus.pdu import ExceptionResponse
import voluptuous as vol
-from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA
+from homeassistant.components.sensor import (
+ DEVICE_CLASSES_SCHEMA,
+ PLATFORM_SCHEMA,
+ SensorEntity,
+)
from homeassistant.const import (
+ CONF_ADDRESS,
+ CONF_COUNT,
CONF_DEVICE_CLASS,
CONF_NAME,
CONF_OFFSET,
+ CONF_SCAN_INTERVAL,
+ CONF_SENSORS,
CONF_SLAVE,
CONF_STRUCTURE,
CONF_UNIT_OF_MEASUREMENT,
)
from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.restore_state import RestoreEntity
+from homeassistant.helpers.typing import (
+ ConfigType,
+ DiscoveryInfoType,
+ HomeAssistantType,
+)
from .const import (
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
- CONF_COUNT,
CONF_DATA_TYPE,
CONF_HUB,
+ CONF_INPUT_TYPE,
CONF_PRECISION,
CONF_REGISTER,
CONF_REGISTER_TYPE,
@@ -37,14 +54,16 @@
DATA_TYPE_STRING,
DATA_TYPE_UINT,
DEFAULT_HUB,
+ DEFAULT_SCAN_INTERVAL,
DEFAULT_STRUCT_FORMAT,
MODBUS_DOMAIN,
)
+from .modbus import ModbusHub
_LOGGER = logging.getLogger(__name__)
-def number(value: Any) -> Union[int, float]:
+def number(value: Any) -> int | float:
"""Coerce a value to number without losing precision."""
if isinstance(value, int):
return value
@@ -97,66 +116,92 @@ def number(value: Any) -> Union[int, float]:
)
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(
+ hass: HomeAssistantType,
+ config: ConfigType,
+ async_add_entities,
+ discovery_info: DiscoveryInfoType | None = None,
+):
"""Set up the Modbus sensors."""
sensors = []
- for register in config[CONF_REGISTERS]:
- if register[CONF_DATA_TYPE] == DATA_TYPE_STRING:
- structure = str(register[CONF_COUNT] * 2) + "s"
- elif register[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM:
+ # check for old config:
+ if discovery_info is None:
+ _LOGGER.warning(
+ "Sensor configuration is deprecated, will be removed in a future release"
+ )
+ discovery_info = {
+ CONF_NAME: "no name",
+ CONF_SENSORS: config[CONF_REGISTERS],
+ }
+ for entry in discovery_info[CONF_SENSORS]:
+ entry[CONF_ADDRESS] = entry[CONF_REGISTER]
+ entry[CONF_INPUT_TYPE] = entry[CONF_REGISTER_TYPE]
+ del entry[CONF_REGISTER]
+ del entry[CONF_REGISTER_TYPE]
+ config = None
+
+ for entry in discovery_info[CONF_SENSORS]:
+ if entry[CONF_DATA_TYPE] == DATA_TYPE_STRING:
+ structure = str(entry[CONF_COUNT] * 2) + "s"
+ elif entry[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM:
try:
- structure = f">{DEFAULT_STRUCT_FORMAT[register[CONF_DATA_TYPE]][register[CONF_COUNT]]}"
+ structure = f">{DEFAULT_STRUCT_FORMAT[entry[CONF_DATA_TYPE]][entry[CONF_COUNT]]}"
except KeyError:
_LOGGER.error(
"Unable to detect data type for %s sensor, try a custom type",
- register[CONF_NAME],
+ entry[CONF_NAME],
)
continue
else:
- structure = register.get(CONF_STRUCTURE)
+ structure = entry.get(CONF_STRUCTURE)
try:
size = struct.calcsize(structure)
except struct.error as err:
- _LOGGER.error("Error in sensor %s structure: %s", register[CONF_NAME], err)
+ _LOGGER.error("Error in sensor %s structure: %s", entry[CONF_NAME], err)
continue
- if register[CONF_COUNT] * 2 != size:
+ if entry[CONF_COUNT] * 2 != size:
_LOGGER.error(
"Structure size (%d bytes) mismatch registers count (%d words)",
size,
- register[CONF_COUNT],
+ entry[CONF_COUNT],
)
continue
- hub_name = register[CONF_HUB]
- hub = hass.data[MODBUS_DOMAIN][hub_name]
+ if CONF_HUB in entry:
+ # from old config!
+ discovery_info[CONF_NAME] = entry[CONF_HUB]
+ if CONF_SCAN_INTERVAL not in entry:
+ entry[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL
+ hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]]
sensors.append(
ModbusRegisterSensor(
hub,
- register[CONF_NAME],
- register.get(CONF_SLAVE),
- register[CONF_REGISTER],
- register[CONF_REGISTER_TYPE],
- register.get(CONF_UNIT_OF_MEASUREMENT),
- register[CONF_COUNT],
- register[CONF_REVERSE_ORDER],
- register[CONF_SCALE],
- register[CONF_OFFSET],
+ entry[CONF_NAME],
+ entry.get(CONF_SLAVE),
+ entry[CONF_ADDRESS],
+ entry[CONF_INPUT_TYPE],
+ entry.get(CONF_UNIT_OF_MEASUREMENT),
+ entry[CONF_COUNT],
+ entry[CONF_REVERSE_ORDER],
+ entry[CONF_SCALE],
+ entry[CONF_OFFSET],
structure,
- register[CONF_PRECISION],
- register[CONF_DATA_TYPE],
- register.get(CONF_DEVICE_CLASS),
+ entry[CONF_PRECISION],
+ entry[CONF_DATA_TYPE],
+ entry.get(CONF_DEVICE_CLASS),
+ entry[CONF_SCAN_INTERVAL],
)
)
if not sensors:
- return False
- add_entities(sensors)
+ return
+ async_add_entities(sensors)
-class ModbusRegisterSensor(RestoreEntity):
+class ModbusRegisterSensor(RestoreEntity, SensorEntity):
"""Modbus register sensor."""
def __init__(
@@ -175,6 +220,7 @@ def __init__(
precision,
data_type,
device_class,
+ scan_interval,
):
"""Initialize the modbus register sensor."""
self._hub = hub
@@ -193,13 +239,17 @@ def __init__(
self._device_class = device_class
self._value = None
self._available = True
+ self._scan_interval = timedelta(seconds=scan_interval)
async def async_added_to_hass(self):
"""Handle entity which will be added."""
state = await self.async_get_last_state()
- if not state:
- return
- self._value = state.state
+ if state:
+ self._value = state.state
+
+ async_track_time_interval(
+ self.hass, lambda arg: self._update(), self._scan_interval
+ )
@property
def state(self):
@@ -211,13 +261,23 @@ def name(self):
"""Return the name of the sensor."""
return self._name
+ @property
+ def should_poll(self):
+ """Return True if entity has to be polled for state.
+
+ False if entity pushes its state to HA.
+ """
+
+ # Handle polling directly in this entity
+ return False
+
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the device class of the sensor."""
return self._device_class
@@ -226,7 +286,7 @@ def available(self) -> bool:
"""Return True if entity is available."""
return self._available
- def update(self):
+ def _update(self):
"""Update the state of the sensor."""
try:
if self._register_type == CALL_TYPE_REGISTER_INPUT:
@@ -279,3 +339,4 @@ def update(self):
self._value = str(val)
self._available = True
+ self.schedule_update_ha_state()
diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py
index 8fe1f886c3e568..2985d8b2c05915 100644
--- a/homeassistant/components/modbus/switch.py
+++ b/homeassistant/components/modbus/switch.py
@@ -1,7 +1,10 @@
"""Support for Modbus switches."""
-from abc import ABC
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from datetime import timedelta
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from pymodbus.exceptions import ConnectionException, ModbusException
from pymodbus.pdu import ExceptionResponse
@@ -9,24 +12,27 @@
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
from homeassistant.const import (
+ CONF_ADDRESS,
CONF_COMMAND_OFF,
CONF_COMMAND_ON,
CONF_NAME,
+ CONF_SCAN_INTERVAL,
CONF_SLAVE,
+ CONF_SWITCHES,
STATE_ON,
)
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.entity import ToggleEntity
+from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
-from . import ModbusHub
from .const import (
CALL_TYPE_COIL,
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
CONF_COILS,
CONF_HUB,
+ CONF_INPUT_TYPE,
CONF_REGISTER,
CONF_REGISTER_TYPE,
CONF_REGISTERS,
@@ -35,8 +41,10 @@
CONF_VERIFY_REGISTER,
CONF_VERIFY_STATE,
DEFAULT_HUB,
+ DEFAULT_SCAN_INTERVAL,
MODBUS_DOMAIN,
)
+from .modbus import ModbusHub
_LOGGER = logging.getLogger(__name__)
@@ -84,35 +92,72 @@ async def async_setup_platform(
):
"""Read configuration and create Modbus switches."""
switches = []
- if CONF_COILS in config:
- for coil in config[CONF_COILS]:
- hub: ModbusHub = hass.data[MODBUS_DOMAIN][coil[CONF_HUB]]
- switches.append(ModbusCoilSwitch(hub, coil))
- if CONF_REGISTERS in config:
- for register in config[CONF_REGISTERS]:
- hub: ModbusHub = hass.data[MODBUS_DOMAIN][register[CONF_HUB]]
- switches.append(ModbusRegisterSwitch(hub, register))
+ # check for old config:
+ if discovery_info is None:
+ _LOGGER.warning(
+ "Switch configuration is deprecated, will be removed in a future release"
+ )
+ discovery_info = {
+ CONF_NAME: "no name",
+ CONF_SWITCHES: [],
+ }
+ if CONF_COILS in config:
+ discovery_info[CONF_SWITCHES].extend(config[CONF_COILS])
+ if CONF_REGISTERS in config:
+ discovery_info[CONF_SWITCHES].extend(config[CONF_REGISTERS])
+ for entry in discovery_info[CONF_SWITCHES]:
+ if CALL_TYPE_COIL in entry:
+ entry[CONF_ADDRESS] = entry[CALL_TYPE_COIL]
+ entry[CONF_INPUT_TYPE] = CALL_TYPE_COIL
+ del entry[CALL_TYPE_COIL]
+ if CONF_REGISTER in entry:
+ entry[CONF_ADDRESS] = entry[CONF_REGISTER]
+ del entry[CONF_REGISTER]
+ if CONF_REGISTER_TYPE in entry:
+ entry[CONF_INPUT_TYPE] = entry[CONF_REGISTER_TYPE]
+ del entry[CONF_REGISTER_TYPE]
+ if CONF_SCAN_INTERVAL not in entry:
+ entry[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL
+ config = None
+
+ for entry in discovery_info[CONF_SWITCHES]:
+ if CONF_HUB in entry:
+ # from old config!
+ discovery_info[CONF_NAME] = entry[CONF_HUB]
+ hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]]
+ if entry[CONF_INPUT_TYPE] == CALL_TYPE_COIL:
+ switches.append(ModbusCoilSwitch(hub, entry))
+ else:
+ switches.append(ModbusRegisterSwitch(hub, entry))
async_add_entities(switches)
-class ModbusBaseSwitch(ToggleEntity, RestoreEntity, ABC):
+class ModbusBaseSwitch(SwitchEntity, RestoreEntity, ABC):
"""Base class representing a Modbus switch."""
- def __init__(self, hub: ModbusHub, config: Dict[str, Any]):
+ def __init__(self, hub: ModbusHub, config: dict[str, Any]):
"""Initialize the switch."""
self._hub: ModbusHub = hub
self._name = config[CONF_NAME]
self._slave = config.get(CONF_SLAVE)
self._is_on = None
self._available = True
+ self._scan_interval = timedelta(seconds=config[CONF_SCAN_INTERVAL])
async def async_added_to_hass(self):
"""Handle entity which will be added."""
state = await self.async_get_last_state()
- if not state:
- return
- self._is_on = state.state == STATE_ON
+ if state:
+ self._is_on = state.state == STATE_ON
+
+ async_track_time_interval(
+ self.hass, lambda arg: self._update(), self._scan_interval
+ )
+
+ @abstractmethod
+ def _update(self):
+ """Update the entity state."""
@property
def is_on(self):
@@ -124,6 +169,16 @@ def name(self):
"""Return the name of the switch."""
return self._name
+ @property
+ def should_poll(self):
+ """Return True if entity has to be polled for state.
+
+ False if entity pushes its state to HA.
+ """
+
+ # Handle polling directly in this entity
+ return False
+
@property
def available(self) -> bool:
"""Return True if entity is available."""
@@ -133,24 +188,27 @@ def available(self) -> bool:
class ModbusCoilSwitch(ModbusBaseSwitch, SwitchEntity):
"""Representation of a Modbus coil switch."""
- def __init__(self, hub: ModbusHub, config: Dict[str, Any]):
+ def __init__(self, hub: ModbusHub, config: dict[str, Any]):
"""Initialize the coil switch."""
super().__init__(hub, config)
- self._coil = config[CALL_TYPE_COIL]
+ self._coil = config[CONF_ADDRESS]
def turn_on(self, **kwargs):
"""Set switch on."""
self._write_coil(self._coil, True)
self._is_on = True
+ self.schedule_update_ha_state()
def turn_off(self, **kwargs):
"""Set switch off."""
self._write_coil(self._coil, False)
self._is_on = False
+ self.schedule_update_ha_state()
- def update(self):
+ def _update(self):
"""Update the state of the switch."""
self._is_on = self._read_coil(self._coil)
+ self.schedule_update_ha_state()
def _read_coil(self, coil) -> bool:
"""Read coil using the Modbus hub slave."""
@@ -184,44 +242,44 @@ def _write_coil(self, coil, value):
class ModbusRegisterSwitch(ModbusBaseSwitch, SwitchEntity):
"""Representation of a Modbus register switch."""
- def __init__(self, hub: ModbusHub, config: Dict[str, Any]):
+ def __init__(self, hub: ModbusHub, config: dict[str, Any]):
"""Initialize the register switch."""
super().__init__(hub, config)
- self._register = config[CONF_REGISTER]
+ self._register = config[CONF_ADDRESS]
self._command_on = config[CONF_COMMAND_ON]
self._command_off = config[CONF_COMMAND_OFF]
self._state_on = config.get(CONF_STATE_ON, self._command_on)
self._state_off = config.get(CONF_STATE_OFF, self._command_off)
self._verify_state = config[CONF_VERIFY_STATE]
self._verify_register = config.get(CONF_VERIFY_REGISTER, self._register)
- self._register_type = config[CONF_REGISTER_TYPE]
+ self._register_type = config[CONF_INPUT_TYPE]
self._available = True
self._is_on = None
def turn_on(self, **kwargs):
"""Set switch on."""
-
# Only holding register is writable
if self._register_type == CALL_TYPE_REGISTER_HOLDING:
self._write_register(self._command_on)
if not self._verify_state:
self._is_on = True
+ self.schedule_update_ha_state()
def turn_off(self, **kwargs):
"""Set switch off."""
-
# Only holding register is writable
if self._register_type == CALL_TYPE_REGISTER_HOLDING:
self._write_register(self._command_off)
if not self._verify_state:
self._is_on = False
+ self.schedule_update_ha_state()
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._available
- def update(self):
+ def _update(self):
"""Update the state of the switch."""
if not self._verify_state:
return
@@ -239,8 +297,9 @@ def update(self):
self._register,
value,
)
+ self.schedule_update_ha_state()
- def _read_register(self) -> Optional[int]:
+ def _read_register(self) -> int | None:
try:
if self._register_type == CALL_TYPE_REGISTER_INPUT:
result = self._hub.read_input_registers(
diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py
index c58a4b67eedb97..080e077a45759f 100644
--- a/homeassistant/components/modem_callerid/sensor.py
+++ b/homeassistant/components/modem_callerid/sensor.py
@@ -4,7 +4,7 @@
from basicmodem.basicmodem import BasicModem as bm
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_DEVICE,
CONF_NAME,
@@ -12,7 +12,6 @@
STATE_IDLE,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Modem CallerID"
@@ -44,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([ModemCalleridSensor(hass, name, port, modem)])
-class ModemCalleridSensor(Entity):
+class ModemCalleridSensor(SensorEntity):
"""Implementation of USB modem caller ID sensor."""
def __init__(self, hass, name, port, modem):
@@ -86,7 +85,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attributes
diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py
index e2d9909c7ca1d4..7bfa161f9eccd8 100644
--- a/homeassistant/components/mold_indicator/sensor.py
+++ b/homeassistant/components/mold_indicator/sensor.py
@@ -5,7 +5,7 @@
import voluptuous as vol
from homeassistant import util
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
CONF_NAME,
@@ -17,7 +17,6 @@
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_state_change_event
_LOGGER = logging.getLogger(__name__)
@@ -69,7 +68,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
-class MoldIndicator(Entity):
+class MoldIndicator(SensorEntity):
"""Represents a MoldIndication sensor."""
def __init__(
@@ -375,7 +374,7 @@ def available(self):
return self._available
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._is_metric:
return {
diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py
index 06883ddc8a8fd4..adc0b05bab7699 100644
--- a/homeassistant/components/monoprice/__init__.py
+++ b/homeassistant/components/monoprice/__init__.py
@@ -54,9 +54,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
FIRST_RUN: first_run,
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -67,8 +67,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py
index 0515814491607d..a65fa8d23f36d1 100644
--- a/homeassistant/components/monoprice/config_flow.py
+++ b/homeassistant/components/monoprice/config_flow.py
@@ -16,8 +16,8 @@
CONF_SOURCE_5,
CONF_SOURCE_6,
CONF_SOURCES,
+ DOMAIN,
)
-from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/monoprice/translations/de.json b/homeassistant/components/monoprice/translations/de.json
index 820d3a972d3151..8f6d1d88196c05 100644
--- a/homeassistant/components/monoprice/translations/de.json
+++ b/homeassistant/components/monoprice/translations/de.json
@@ -4,7 +4,7 @@
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"unknown": "Unerwarteter Fehler"
},
"step": {
diff --git a/homeassistant/components/monoprice/translations/hu.json b/homeassistant/components/monoprice/translations/hu.json
index 892b8b2cd91240..a845f8621608ca 100644
--- a/homeassistant/components/monoprice/translations/hu.json
+++ b/homeassistant/components/monoprice/translations/hu.json
@@ -1,5 +1,12 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/monoprice/translations/id.json b/homeassistant/components/monoprice/translations/id.json
new file mode 100644
index 00000000000000..bf4269c492ea64
--- /dev/null
+++ b/homeassistant/components/monoprice/translations/id.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Port",
+ "source_1": "Nama sumber #1",
+ "source_2": "Nama sumber #2",
+ "source_3": "Nama sumber #3",
+ "source_4": "Nama sumber #4",
+ "source_5": "Nama sumber #5",
+ "source_6": "Nama sumber #6"
+ },
+ "title": "Hubungkan ke perangkat"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "source_1": "Nama sumber #1",
+ "source_2": "Nama sumber #2",
+ "source_3": "Nama sumber #3",
+ "source_4": "Nama sumber #4",
+ "source_5": "Nama sumber #5",
+ "source_6": "Nama sumber #6"
+ },
+ "title": "Konfigurasikan sumber"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/monoprice/translations/ko.json b/homeassistant/components/monoprice/translations/ko.json
index 23e191735359b4..6afed3aa6d6178 100644
--- a/homeassistant/components/monoprice/translations/ko.json
+++ b/homeassistant/components/monoprice/translations/ko.json
@@ -4,7 +4,7 @@
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
diff --git a/homeassistant/components/monoprice/translations/nl.json b/homeassistant/components/monoprice/translations/nl.json
index 74bc677dbe8728..28ecedab3d39d1 100644
--- a/homeassistant/components/monoprice/translations/nl.json
+++ b/homeassistant/components/monoprice/translations/nl.json
@@ -4,13 +4,13 @@
"already_configured": "Apparaat is al geconfigureerd"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Kan geen verbinding maken",
"unknown": "Onverwachte fout"
},
"step": {
"user": {
"data": {
- "port": "Seri\u00eble poort",
+ "port": "Poort",
"source_1": "Naam van bron #1",
"source_2": "Naam van bron #2",
"source_3": "Naam van bron #3",
diff --git a/homeassistant/components/monoprice/translations/tr.json b/homeassistant/components/monoprice/translations/tr.json
new file mode 100644
index 00000000000000..7c622a3cb4a140
--- /dev/null
+++ b/homeassistant/components/monoprice/translations/tr.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Port",
+ "source_1": "Kaynak #1 ad\u0131",
+ "source_2": "Kaynak #2 ad\u0131",
+ "source_3": "Kaynak #3 ad\u0131",
+ "source_4": "Kaynak #4 ad\u0131",
+ "source_5": "Kaynak #5 ad\u0131",
+ "source_6": "Kaynak #6 ad\u0131"
+ },
+ "title": "Cihaza ba\u011flan\u0131n"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/monoprice/translations/uk.json b/homeassistant/components/monoprice/translations/uk.json
new file mode 100644
index 00000000000000..08857cc26f9b1a
--- /dev/null
+++ b/homeassistant/components/monoprice/translations/uk.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "\u041f\u043e\u0440\u0442",
+ "source_1": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #1",
+ "source_2": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #2",
+ "source_3": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #3",
+ "source_4": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #4",
+ "source_5": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #5",
+ "source_6": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #6"
+ },
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "source_1": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #1",
+ "source_2": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #2",
+ "source_3": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #3",
+ "source_4": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #4",
+ "source_5": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #5",
+ "source_6": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #6"
+ },
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u0436\u0435\u0440\u0435\u043b"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py
index 9e0f8ef51d6326..6213e218d24d0d 100644
--- a/homeassistant/components/moon/sensor.py
+++ b/homeassistant/components/moon/sensor.py
@@ -1,11 +1,10 @@
"""Support for tracking the moon phases."""
-from astral import Astral
+from astral import moon
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
DEFAULT_NAME = "Moon"
@@ -42,14 +41,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([MoonSensor(name)], True)
-class MoonSensor(Entity):
+class MoonSensor(SensorEntity):
"""Representation of a Moon sensor."""
def __init__(self, name):
"""Initialize the moon sensor."""
self._name = name
self._state = None
- self._astral = Astral()
@property
def name(self):
@@ -88,4 +86,4 @@ def icon(self):
async def async_update(self):
"""Get the time and updates the states."""
today = dt_util.as_local(dt_util.utcnow()).date()
- self._state = self._astral.moon_phase(today)
+ self._state = moon.phase(today)
diff --git a/homeassistant/components/moon/translations/sensor.id.json b/homeassistant/components/moon/translations/sensor.id.json
new file mode 100644
index 00000000000000..197bc609813242
--- /dev/null
+++ b/homeassistant/components/moon/translations/sensor.id.json
@@ -0,0 +1,14 @@
+{
+ "state": {
+ "moon__phase": {
+ "first_quarter": "Seperempat pertama",
+ "full_moon": "Bulan purnama",
+ "last_quarter": "Seperempat ketiga",
+ "new_moon": "Bulan baru",
+ "waning_crescent": "Bulan sabit tua",
+ "waning_gibbous": "Bulan cembung tua",
+ "waxing_crescent": "Bulan sabit muda",
+ "waxing_gibbous": "Bulan cembung muda"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/moon/translations/sensor.uk.json b/homeassistant/components/moon/translations/sensor.uk.json
index 71c2d80eb9804f..f916c03c3a1e87 100644
--- a/homeassistant/components/moon/translations/sensor.uk.json
+++ b/homeassistant/components/moon/translations/sensor.uk.json
@@ -4,7 +4,11 @@
"first_quarter": "\u041f\u0435\u0440\u0448\u0430 \u0447\u0432\u0435\u0440\u0442\u044c",
"full_moon": "\u041f\u043e\u0432\u043d\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c",
"last_quarter": "\u041e\u0441\u0442\u0430\u043d\u043d\u044f \u0447\u0432\u0435\u0440\u0442\u044c",
- "new_moon": "\u041d\u043e\u0432\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c"
+ "new_moon": "\u041d\u043e\u0432\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c",
+ "waning_crescent": "\u0421\u0442\u0430\u0440\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c",
+ "waning_gibbous": "\u0421\u043f\u0430\u0434\u0430\u044e\u0447\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c",
+ "waxing_crescent": "\u041c\u043e\u043b\u043e\u0434\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c",
+ "waxing_gibbous": "\u041f\u0440\u0438\u0431\u0443\u0432\u0430\u044e\u0447\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py
index e10f1655d2f186..73a27c90140b54 100644
--- a/homeassistant/components/motion_blinds/__init__.py
+++ b/homeassistant/components/motion_blinds/__init__.py
@@ -5,6 +5,7 @@
from socket import timeout
from motionblinds import MotionMulticast
+from motionblinds.motion_blinds import ParseException
from homeassistant import config_entries, core
from homeassistant.const import CONF_API_KEY, CONF_HOST, EVENT_HOMEASSISTANT_STOP
@@ -13,18 +14,87 @@
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import (
+ ATTR_AVAILABLE,
DOMAIN,
KEY_COORDINATOR,
KEY_GATEWAY,
KEY_MULTICAST_LISTENER,
MANUFACTURER,
- MOTION_PLATFORMS,
+ PLATFORMS,
+ UPDATE_INTERVAL,
+ UPDATE_INTERVAL_FAST,
)
from .gateway import ConnectMotionGateway
_LOGGER = logging.getLogger(__name__)
+class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator):
+ """Class to manage fetching data from single endpoint."""
+
+ def __init__(
+ self,
+ hass,
+ logger,
+ gateway,
+ *,
+ name,
+ update_interval=None,
+ update_method=None,
+ ):
+ """Initialize global data updater."""
+ super().__init__(
+ hass,
+ logger,
+ name=name,
+ update_method=update_method,
+ update_interval=update_interval,
+ )
+
+ self._gateway = gateway
+
+ def update_gateway(self):
+ """Call all updates using one async_add_executor_job."""
+ data = {}
+
+ try:
+ self._gateway.Update()
+ except (timeout, ParseException):
+ # let the error be logged and handled by the motionblinds library
+ data[KEY_GATEWAY] = {ATTR_AVAILABLE: False}
+ return data
+ else:
+ data[KEY_GATEWAY] = {ATTR_AVAILABLE: True}
+
+ for blind in self._gateway.device_list.values():
+ try:
+ blind.Update()
+ except (timeout, ParseException):
+ # let the error be logged and handled by the motionblinds library
+ data[blind.mac] = {ATTR_AVAILABLE: False}
+ else:
+ data[blind.mac] = {ATTR_AVAILABLE: True}
+
+ return data
+
+ async def _async_update_data(self):
+ """Fetch the latest data from the gateway and blinds."""
+ data = await self.hass.async_add_executor_job(self.update_gateway)
+
+ all_available = True
+ for device in data.values():
+ if not device[ATTR_AVAILABLE]:
+ all_available = False
+ break
+
+ if all_available:
+ self.update_interval = timedelta(seconds=UPDATE_INTERVAL)
+ else:
+ self.update_interval = timedelta(seconds=UPDATE_INTERVAL_FAST)
+
+ return data
+
+
def setup(hass: core.HomeAssistant, config: dict):
"""Set up the Motion Blinds component."""
return True
@@ -54,41 +124,24 @@ def stop_motion_multicast(event):
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_motion_multicast)
# Connect to motion gateway
+ multicast = hass.data[DOMAIN][KEY_MULTICAST_LISTENER]
connect_gateway_class = ConnectMotionGateway(hass, multicast)
if not await connect_gateway_class.async_connect_gateway(host, key):
raise ConfigEntryNotReady
motion_gateway = connect_gateway_class.gateway_device
- def update_gateway():
- """Call all updates using one async_add_executor_job."""
- motion_gateway.Update()
- for blind in motion_gateway.device_list.values():
- try:
- blind.Update()
- except timeout:
- # let the error be logged and handled by the motionblinds library
- pass
-
- async def async_update_data():
- """Fetch data from the gateway and blinds."""
- try:
- await hass.async_add_executor_job(update_gateway)
- except timeout:
- # let the error be logged and handled by the motionblinds library
- pass
-
- coordinator = DataUpdateCoordinator(
+ coordinator = DataUpdateCoordinatorMotionBlinds(
hass,
_LOGGER,
+ motion_gateway,
# Name of the data. For logging purposes.
name=entry.title,
- update_method=async_update_data,
# Polling interval. Will only be polled if there are subscribers.
- update_interval=timedelta(seconds=600),
+ update_interval=timedelta(seconds=UPDATE_INTERVAL),
)
# Fetch initial data so we have data when entities subscribe
- await coordinator.async_refresh()
+ await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = {
KEY_GATEWAY: motion_gateway,
@@ -106,9 +159,9 @@ async def async_update_data():
sw_version=motion_gateway.protocol,
)
- for component in MOTION_PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -121,8 +174,8 @@ async def async_unload_entry(
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in MOTION_PLATFORMS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py
index cb85b45e0e0a64..9aa62ca2d05c82 100644
--- a/homeassistant/components/motion_blinds/config_flow.py
+++ b/homeassistant/components/motion_blinds/config_flow.py
@@ -1,19 +1,13 @@
"""Config flow to configure Motion Blinds using their WLAN API."""
-import logging
-
from motionblinds import MotionDiscovery
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_HOST
-# pylint: disable=unused-import
from .const import DEFAULT_GATEWAY_NAME, DOMAIN
from .gateway import ConnectMotionGateway
-_LOGGER = logging.getLogger(__name__)
-
-
CONFIG_SCHEMA = vol.Schema(
{
vol.Optional(CONF_HOST): str,
diff --git a/homeassistant/components/motion_blinds/const.py b/homeassistant/components/motion_blinds/const.py
index 27f2310c7ce277..52c6e39b096c72 100644
--- a/homeassistant/components/motion_blinds/const.py
+++ b/homeassistant/components/motion_blinds/const.py
@@ -3,7 +3,7 @@
MANUFACTURER = "Motion Blinds, Coulisse B.V."
DEFAULT_GATEWAY_NAME = "Motion Blinds Gateway"
-MOTION_PLATFORMS = ["cover", "sensor"]
+PLATFORMS = ["cover", "sensor"]
KEY_GATEWAY = "gateway"
KEY_COORDINATOR = "coordinator"
@@ -11,5 +11,9 @@
ATTR_WIDTH = "width"
ATTR_ABSOLUTE_POSITION = "absolute_position"
+ATTR_AVAILABLE = "available"
SERVICE_SET_ABSOLUTE_POSITION = "set_absolute_position"
+
+UPDATE_INTERVAL = 600
+UPDATE_INTERVAL_FAST = 60
diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py
index 3087401c3aea6c..2c4fee5f8aa559 100644
--- a/homeassistant/components/motion_blinds/cover.py
+++ b/homeassistant/components/motion_blinds/cover.py
@@ -21,6 +21,7 @@
from .const import (
ATTR_ABSOLUTE_POSITION,
+ ATTR_AVAILABLE,
ATTR_WIDTH,
DOMAIN,
KEY_COORDINATOR,
@@ -160,7 +161,13 @@ def name(self):
@property
def available(self):
"""Return True if entity is available."""
- return self._blind.available
+ if self.coordinator.data is None:
+ return False
+
+ if not self.coordinator.data[KEY_GATEWAY][ATTR_AVAILABLE]:
+ return False
+
+ return self.coordinator.data[self._blind.mac][ATTR_AVAILABLE]
@property
def current_cover_position(self):
@@ -294,7 +301,7 @@ def is_closed(self):
return self._blind.position[self._motor_key] == 100
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
attributes = {}
if self._blind.position is not None:
diff --git a/homeassistant/components/motion_blinds/gateway.py b/homeassistant/components/motion_blinds/gateway.py
index 14dd36ce5b0e8d..6f8032e5a652eb 100644
--- a/homeassistant/components/motion_blinds/gateway.py
+++ b/homeassistant/components/motion_blinds/gateway.py
@@ -30,7 +30,7 @@ def update_gateway(self):
async def async_connect_gateway(self, host, key):
"""Connect to the Motion Gateway."""
- _LOGGER.debug("Initializing with host %s (key %s...)", host, key[:3])
+ _LOGGER.debug("Initializing with host %s (key %s)", host, key[:3])
self._gateway_device = MotionGateway(
ip=host, key=key, multicast=self._multicast
)
diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json
index ec2823dbd2e27b..c144dc99bc5b3e 100644
--- a/homeassistant/components/motion_blinds/manifest.json
+++ b/homeassistant/components/motion_blinds/manifest.json
@@ -3,6 +3,6 @@
"name": "Motion Blinds",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
- "requirements": ["motionblinds==0.4.8"],
+ "requirements": ["motionblinds==0.4.10"],
"codeowners": ["@starkillerOG"]
}
diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py
index dd637696e775d7..0da38795f7b1a8 100644
--- a/homeassistant/components/motion_blinds/sensor.py
+++ b/homeassistant/components/motion_blinds/sensor.py
@@ -1,20 +1,16 @@
"""Support for Motion Blinds sensors."""
-import logging
-
from motionblinds import BlindType
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_SIGNAL_STRENGTH,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
)
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY
-
-_LOGGER = logging.getLogger(__name__)
+from .const import ATTR_AVAILABLE, DOMAIN, KEY_COORDINATOR, KEY_GATEWAY
ATTR_BATTERY_VOLTAGE = "battery_voltage"
TYPE_BLIND = "blind"
@@ -43,7 +39,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities)
-class MotionBatterySensor(CoordinatorEntity, Entity):
+class MotionBatterySensor(CoordinatorEntity, SensorEntity):
"""
Representation of a Motion Battery Sensor.
@@ -74,7 +70,13 @@ def name(self):
@property
def available(self):
"""Return True if entity is available."""
- return self._blind.available
+ if self.coordinator.data is None:
+ return False
+
+ if not self.coordinator.data[KEY_GATEWAY][ATTR_AVAILABLE]:
+ return False
+
+ return self.coordinator.data[self._blind.mac][ATTR_AVAILABLE]
@property
def unit_of_measurement(self):
@@ -92,7 +94,7 @@ def state(self):
return self._blind.battery_level
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
return {ATTR_BATTERY_VOLTAGE: self._blind.battery_voltage}
@@ -138,7 +140,7 @@ def state(self):
return self._blind.battery_level[self._motor[0]]
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
attributes = {}
if self._blind.battery_voltage is not None:
@@ -148,7 +150,7 @@ def device_state_attributes(self):
return attributes
-class MotionSignalStrengthSensor(CoordinatorEntity, Entity):
+class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity):
"""Representation of a Motion Signal Strength Sensor."""
def __init__(self, coordinator, device, device_type):
@@ -178,7 +180,17 @@ def name(self):
@property
def available(self):
"""Return True if entity is available."""
- return self._device.available
+ if self.coordinator.data is None:
+ return False
+
+ gateway_available = self.coordinator.data[KEY_GATEWAY][ATTR_AVAILABLE]
+ if self._device_type == TYPE_GATEWAY:
+ return gateway_available
+
+ return (
+ gateway_available
+ and self.coordinator.data[self._device.mac][ATTR_AVAILABLE]
+ )
@property
def unit_of_measurement(self):
diff --git a/homeassistant/components/motion_blinds/translations/bg.json b/homeassistant/components/motion_blinds/translations/bg.json
new file mode 100644
index 00000000000000..39f706036fd2a5
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/bg.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "select": {
+ "data": {
+ "select_ip": "IP \u0430\u0434\u0440\u0435\u0441"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/ca.json b/homeassistant/components/motion_blinds/translations/ca.json
index a4bf96457e6f15..b83746b9ccfaca 100644
--- a/homeassistant/components/motion_blinds/translations/ca.json
+++ b/homeassistant/components/motion_blinds/translations/ca.json
@@ -5,14 +5,31 @@
"already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
"connection_error": "Ha fallat la connexi\u00f3"
},
+ "error": {
+ "discovery_error": "No s'ha pogut descobrir cap Motion Gateway"
+ },
"flow_title": "Motion Blinds",
"step": {
+ "connect": {
+ "data": {
+ "api_key": "Clau API"
+ },
+ "description": "Necessitar\u00e0s la clau API de 16 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.",
+ "title": "Motion Blinds"
+ },
+ "select": {
+ "data": {
+ "select_ip": "Adre\u00e7a IP"
+ },
+ "description": "Torna a executar la configuraci\u00f3 si vols connectar m\u00e9s Motion Gateways",
+ "title": "Selecciona el Motion Gateway que vulguis connectar"
+ },
"user": {
"data": {
"api_key": "Clau API",
"host": "Adre\u00e7a IP"
},
- "description": "Necessitar\u00e0s el token d'API de 16 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.",
+ "description": "Connecta el teu Motion Gateway, si no es configura l'adre\u00e7a IP, s'utilitza el descobriment autom\u00e0tic",
"title": "Motion Blinds"
}
}
diff --git a/homeassistant/components/motion_blinds/translations/cs.json b/homeassistant/components/motion_blinds/translations/cs.json
index 41b5db3c83ec40..899f04d7cd4393 100644
--- a/homeassistant/components/motion_blinds/translations/cs.json
+++ b/homeassistant/components/motion_blinds/translations/cs.json
@@ -7,12 +7,21 @@
},
"flow_title": "Motion Blinds",
"step": {
+ "connect": {
+ "data": {
+ "api_key": "Kl\u00ed\u010d API"
+ }
+ },
+ "select": {
+ "data": {
+ "select_ip": "IP adresa"
+ }
+ },
"user": {
"data": {
"api_key": "Kl\u00ed\u010d API",
"host": "IP adresa"
},
- "description": "Budete pot\u0159ebovat 16m\u00edstn\u00fd API kl\u00ed\u010d, pokyny najdete na https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key",
"title": "Motion Blinds"
}
}
diff --git a/homeassistant/components/motion_blinds/translations/de.json b/homeassistant/components/motion_blinds/translations/de.json
index dd1acc230f1e82..01eba9c7ecd61c 100644
--- a/homeassistant/components/motion_blinds/translations/de.json
+++ b/homeassistant/components/motion_blinds/translations/de.json
@@ -1,16 +1,32 @@
{
"config": {
"abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
"connection_error": "Verbindung fehlgeschlagen"
},
"flow_title": "Jalousien",
"step": {
+ "connect": {
+ "data": {
+ "api_key": "API-Schl\u00fcssel"
+ },
+ "description": "Ein 16-Zeichen-API-Schl\u00fcssel wird ben\u00f6tigt, siehe https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key",
+ "title": "Motion Jalousien"
+ },
+ "select": {
+ "data": {
+ "select_ip": "IP-Adresse"
+ },
+ "description": "F\u00fchre das Setup erneut aus, wenn du weitere Motion Gateways verbinden m\u00f6chtest",
+ "title": "W\u00e4hle das Motion Gateway aus, zu dem du eine Verbindung herstellen m\u00f6chten"
+ },
"user": {
"data": {
"api_key": "API-Schl\u00fcssel",
"host": "IP-Adresse"
},
- "description": "Ein 16-Zeichen-API-Schl\u00fcssel wird ben\u00f6tigt, siehe https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key",
+ "description": "Stelle eine Verbindung zu deinem Motion Gateway her. Wenn die IP-Adresse leer bleibt, wird die automatische Erkennung verwendet",
"title": "Jalousien"
}
}
diff --git a/homeassistant/components/motion_blinds/translations/en.json b/homeassistant/components/motion_blinds/translations/en.json
index b7830a255fc073..3a968bc6491b91 100644
--- a/homeassistant/components/motion_blinds/translations/en.json
+++ b/homeassistant/components/motion_blinds/translations/en.json
@@ -5,14 +5,31 @@
"already_in_progress": "Configuration flow is already in progress",
"connection_error": "Failed to connect"
},
+ "error": {
+ "discovery_error": "Failed to discover a Motion Gateway"
+ },
"flow_title": "Motion Blinds",
"step": {
+ "connect": {
+ "data": {
+ "api_key": "API Key"
+ },
+ "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions",
+ "title": "Motion Blinds"
+ },
+ "select": {
+ "data": {
+ "select_ip": "IP Address"
+ },
+ "description": "Run the setup again if you want to connect additional Motion Gateways",
+ "title": "Select the Motion Gateway that you wish to connect"
+ },
"user": {
"data": {
"api_key": "API Key",
"host": "IP Address"
},
- "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions",
+ "description": "Connect to your Motion Gateway, if the IP address is not set, auto-discovery is used",
"title": "Motion Blinds"
}
}
diff --git a/homeassistant/components/motion_blinds/translations/es.json b/homeassistant/components/motion_blinds/translations/es.json
index bac5ffddbd31d0..7d7c6c1510fc92 100644
--- a/homeassistant/components/motion_blinds/translations/es.json
+++ b/homeassistant/components/motion_blinds/translations/es.json
@@ -5,14 +5,31 @@
"already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso",
"connection_error": "No se pudo conectar"
},
+ "error": {
+ "discovery_error": "No se pudo descubrir un detector de movimiento"
+ },
"flow_title": "Motion Blinds",
"step": {
+ "connect": {
+ "data": {
+ "api_key": "Clave API"
+ },
+ "description": "Necesitar\u00e1 la clave de API de 16 caracteres, consulte https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key para obtener instrucciones",
+ "title": "Estores motorizados"
+ },
+ "select": {
+ "data": {
+ "select_ip": "Direcci\u00f3n IP"
+ },
+ "description": "Ejecute la configuraci\u00f3n de nuevo si desea conectar detectores de movimiento adicionales",
+ "title": "Selecciona el detector de Movimiento que deseas conectar"
+ },
"user": {
"data": {
"api_key": "Clave API",
"host": "Direcci\u00f3n IP"
},
- "description": "Necesitar\u00e1s la Clave API de 16 caracteres, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key para instrucciones",
+ "description": "Con\u00e9ctate a tu Motion Gateway, si la direcci\u00f3n IP no est\u00e1 establecida, se utilitzar\u00e1 la detecci\u00f3n autom\u00e1tica",
"title": "Motion Blinds"
}
}
diff --git a/homeassistant/components/motion_blinds/translations/et.json b/homeassistant/components/motion_blinds/translations/et.json
index b55640d8905646..5e585dec1a3d64 100644
--- a/homeassistant/components/motion_blinds/translations/et.json
+++ b/homeassistant/components/motion_blinds/translations/et.json
@@ -5,14 +5,31 @@
"already_in_progress": "Seadistamine on juba k\u00e4imas",
"connection_error": "\u00dchendamine nurjus"
},
+ "error": {
+ "discovery_error": "Motion Gateway avastamine nurjus"
+ },
"flow_title": "",
"step": {
+ "connect": {
+ "data": {
+ "api_key": "API v\u00f5ti"
+ },
+ "description": "On vaja 16-kohalist API-v\u00f5tit, juhiste saamiseks vaata https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key",
+ "title": ""
+ },
+ "select": {
+ "data": {
+ "select_ip": "IP aadress"
+ },
+ "description": "K\u00e4ivita seadistamine uuesti kui soovid \u00fchendada t\u00e4iendavaid Motion Gateway sidumisi",
+ "title": "Vali Motion Gateway, mille soovid \u00fchendada"
+ },
"user": {
"data": {
"api_key": "API v\u00f5ti",
"host": "IP-aadress"
},
- "description": "Vaja on 16-kohalist API-v\u00f5tit. Juhiste saamiseks vt https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key",
+ "description": "\u00dchenda oma Motion Gatewayga. Kui IP-aadress on m\u00e4\u00e4ramata kasutatakse automaatset avastamist",
"title": ""
}
}
diff --git a/homeassistant/components/motion_blinds/translations/fr.json b/homeassistant/components/motion_blinds/translations/fr.json
new file mode 100644
index 00000000000000..b6715970e40a90
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/fr.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ",
+ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours",
+ "connection_error": "\u00c9chec de la connexion "
+ },
+ "error": {
+ "discovery_error": "Impossible de d\u00e9couvrir une Motion Gateway"
+ },
+ "flow_title": "Stores de mouvement",
+ "step": {
+ "connect": {
+ "data": {
+ "api_key": "Cl\u00e9 API"
+ },
+ "description": "Vous aurez besoin de la cl\u00e9 API de 16 caract\u00e8res, voir https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key pour les instructions",
+ "title": "Stores de mouvement"
+ },
+ "select": {
+ "data": {
+ "select_ip": "Adresse IP"
+ },
+ "description": "Ex\u00e9cutez \u00e0 nouveau la configuration si vous souhaitez connecter des passerelles Motion suppl\u00e9mentaires",
+ "title": "S\u00e9lectionnez la Motion Gateway que vous souhaitez connecter"
+ },
+ "user": {
+ "data": {
+ "api_key": "Clef d'API",
+ "host": "Adresse IP"
+ },
+ "description": "Connectez-vous \u00e0 votre Motion Gateway, si l'adresse IP n'est pas d\u00e9finie, la d\u00e9tection automatique est utilis\u00e9e",
+ "title": "Stores de mouvement"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/hu.json b/homeassistant/components/motion_blinds/translations/hu.json
new file mode 100644
index 00000000000000..541cefd2110734
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/hu.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
+ "connection_error": "Sikertelen csatlakoz\u00e1s"
+ },
+ "step": {
+ "connect": {
+ "data": {
+ "api_key": "API kulcs"
+ }
+ },
+ "select": {
+ "data": {
+ "select_ip": "IP c\u00edm"
+ }
+ },
+ "user": {
+ "data": {
+ "api_key": "API kulcs",
+ "host": "IP c\u00edm"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/id.json b/homeassistant/components/motion_blinds/translations/id.json
new file mode 100644
index 00000000000000..9248531a751c36
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/id.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "connection_error": "Gagal terhubung"
+ },
+ "error": {
+ "discovery_error": "Gagal menemukan Motion Gateway"
+ },
+ "flow_title": "Motion Blinds",
+ "step": {
+ "connect": {
+ "data": {
+ "api_key": "Kunci API"
+ },
+ "description": "Anda akan memerlukan Kunci API 16 karakter, baca petunjuknya di https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key",
+ "title": "Motion Blinds"
+ },
+ "select": {
+ "data": {
+ "select_ip": "Alamat IP"
+ },
+ "description": "Jalankan penyiapan lagi jika ingin menghubungkan Motion Gateway lainnya",
+ "title": "Pilih Motion Gateway yang ingin dihubungkan"
+ },
+ "user": {
+ "data": {
+ "api_key": "Kunci API",
+ "host": "Alamat IP"
+ },
+ "description": "Hubungkan ke Motion Gateway Anda, jika alamat IP tidak disetel, penemuan otomatis akan digunakan",
+ "title": "Motion Blinds"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/it.json b/homeassistant/components/motion_blinds/translations/it.json
index ff56f184ac2e1e..1d79ae28ee5173 100644
--- a/homeassistant/components/motion_blinds/translations/it.json
+++ b/homeassistant/components/motion_blinds/translations/it.json
@@ -5,14 +5,31 @@
"already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"connection_error": "Impossibile connettersi"
},
+ "error": {
+ "discovery_error": "Impossibile rilevare un Motion Gateway"
+ },
"flow_title": "Tende Motion",
"step": {
+ "connect": {
+ "data": {
+ "api_key": "Chiave API"
+ },
+ "description": "Avrai bisogno della chiave API di 16 caratteri, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key per le istruzioni",
+ "title": "Motion Blinds"
+ },
+ "select": {
+ "data": {
+ "select_ip": "Indirizzo IP"
+ },
+ "description": "Esegui nuovamente l'installazione se desideri collegare altri Motion Gateway",
+ "title": "Seleziona il Motion Gateway che vorresti collegare"
+ },
"user": {
"data": {
"api_key": "Chiave API",
"host": "Indirizzo IP"
},
- "description": "Avrai bisogno della chiave API di 16 caratteri, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key per le istruzioni",
+ "description": "Connetti il tuo Motion Gateway, se l'indirizzo IP non \u00e8 impostato, sar\u00e0 utilizzato il rilevamento automatico",
"title": "Tende Motion"
}
}
diff --git a/homeassistant/components/motion_blinds/translations/ko.json b/homeassistant/components/motion_blinds/translations/ko.json
new file mode 100644
index 00000000000000..69ed2cd7b35ee2
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/ko.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
+ "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "discovery_error": "Motion \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \ucc3e\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "flow_title": "Motion Blinds",
+ "step": {
+ "connect": {
+ "data": {
+ "api_key": "API \ud0a4"
+ },
+ "description": "16\uac1c\uc758 \ubb38\uc790\uc5f4\ub85c \uad6c\uc131\ub41c API Key\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
+ "title": "Motion Blinds"
+ },
+ "select": {
+ "data": {
+ "select_ip": "IP \uc8fc\uc18c"
+ },
+ "description": "Motion \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \ucd94\uac00 \uc5f0\uacb0\ud558\ub824\uba74 \uc124\uc815\uc744 \ub2e4\uc2dc \uc2e4\ud589\ud574\uc8fc\uc138\uc694",
+ "title": "\uc5f0\uacb0\ud560 Motion \uac8c\uc774\ud2b8\uc6e8\uc774 \uc120\ud0dd\ud558\uae30"
+ },
+ "user": {
+ "data": {
+ "api_key": "API \ud0a4",
+ "host": "IP \uc8fc\uc18c"
+ },
+ "description": "Motion \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud569\ub2c8\ub2e4. IP \uc8fc\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc790\ub3d9 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4",
+ "title": "Motion Blinds"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/lb.json b/homeassistant/components/motion_blinds/translations/lb.json
index 7a3dcfdbf07eec..85caeea79e52f3 100644
--- a/homeassistant/components/motion_blinds/translations/lb.json
+++ b/homeassistant/components/motion_blinds/translations/lb.json
@@ -5,7 +5,21 @@
"already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang",
"connection_error": "Feeler beim verbannen"
},
+ "error": {
+ "discovery_error": "Feeler beim Entdecken vun enger Motion Gateway"
+ },
"step": {
+ "connect": {
+ "data": {
+ "api_key": "API Schl\u00ebssel"
+ },
+ "description": "Du brauchs de 16 stellegen API Schl\u00ebssel, kuck https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key fir w\u00e9ider Instruktiounen"
+ },
+ "select": {
+ "data": {
+ "select_ip": "IP Adresse"
+ }
+ },
"user": {
"data": {
"api_key": "API Schl\u00ebssel",
diff --git a/homeassistant/components/motion_blinds/translations/nl.json b/homeassistant/components/motion_blinds/translations/nl.json
new file mode 100644
index 00000000000000..54baeb9e18d29e
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/nl.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
+ "connection_error": "Kan geen verbinding maken"
+ },
+ "error": {
+ "discovery_error": "Kan geen Motion Gateway vinden"
+ },
+ "flow_title": "Motion Blinds",
+ "step": {
+ "connect": {
+ "data": {
+ "api_key": "API-sleutel"
+ },
+ "description": "U hebt de API-sleutel van 16 tekens nodig, zie https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key voor instructies",
+ "title": "Motion Blinds"
+ },
+ "select": {
+ "data": {
+ "select_ip": "IP-adres"
+ },
+ "description": "Voer de installatie opnieuw uit als u extra Motion Gateways wilt aansluiten",
+ "title": "Selecteer de Motion Gateway waarmee u verbinding wilt maken"
+ },
+ "user": {
+ "data": {
+ "api_key": "API-sleutel",
+ "host": "IP-adres"
+ },
+ "description": "Maak verbinding met uw Motion Gateway, als het IP-adres niet is ingesteld, wordt auto-discovery gebruikt",
+ "title": "Motion Blinds"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/no.json b/homeassistant/components/motion_blinds/translations/no.json
index 9e4061506912ee..e86da7c1fc494d 100644
--- a/homeassistant/components/motion_blinds/translations/no.json
+++ b/homeassistant/components/motion_blinds/translations/no.json
@@ -5,14 +5,31 @@
"already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
"connection_error": "Tilkobling mislyktes"
},
+ "error": {
+ "discovery_error": "Kunne ikke oppdage en Motion Gateway"
+ },
"flow_title": "Motion Blinds",
"step": {
+ "connect": {
+ "data": {
+ "api_key": "API-n\u00f8kkel"
+ },
+ "description": "Du trenger API-n\u00f8kkelen med 16 tegn, se https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instruksjoner",
+ "title": ""
+ },
+ "select": {
+ "data": {
+ "select_ip": "IP adresse"
+ },
+ "description": "Kj\u00f8r oppsettet p\u00e5 nytt hvis du vil koble til flere Motion Gateways",
+ "title": "Velg Motion Gateway som du vil koble til"
+ },
"user": {
"data": {
"api_key": "API-n\u00f8kkel",
"host": "IP adresse"
},
- "description": "Du trenger API-n\u00f8kkelen med 16 tegn, se https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instruksjoner",
+ "description": "Koble til Motion Gateway. Hvis IP-adressen ikke er angitt, brukes automatisk oppdagelse",
"title": "Motion Blinds"
}
}
diff --git a/homeassistant/components/motion_blinds/translations/pl.json b/homeassistant/components/motion_blinds/translations/pl.json
index 8f73496fd1d2e2..61e9d22c3cf91f 100644
--- a/homeassistant/components/motion_blinds/translations/pl.json
+++ b/homeassistant/components/motion_blinds/translations/pl.json
@@ -5,15 +5,32 @@
"already_in_progress": "Konfiguracja jest ju\u017c w toku",
"connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
- "flow_title": "Motion Blinds",
+ "error": {
+ "discovery_error": "Nie uda\u0142o si\u0119 wykry\u0107 bramki ruchu"
+ },
+ "flow_title": "Rolety Motion",
"step": {
+ "connect": {
+ "data": {
+ "api_key": "Klucz API"
+ },
+ "description": "B\u0119dziesz potrzebowa\u0142 16-znakowego klucza API, instrukcje znajdziesz na https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key",
+ "title": "Motion Blinds"
+ },
+ "select": {
+ "data": {
+ "select_ip": "Adres IP"
+ },
+ "description": "Uruchom ponownie konfiguracj\u0119, je\u015bli chcesz pod\u0142\u0105czy\u0107 dodatkowe bramki ruchu",
+ "title": "Wybierz bram\u0119 ruchu, z kt\u00f3r\u0105 chcesz si\u0119 po\u0142\u0105czy\u0107"
+ },
"user": {
"data": {
"api_key": "Klucz API",
"host": "Adres IP"
},
- "description": "B\u0119dziesz potrzebowa\u0142 16-znakowego klucza API, instrukcje znajdziesz na https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key",
- "title": "Motion Blinds"
+ "description": "Po\u0142\u0105cz si\u0119 z bram\u0105 ruchu. Je\u015bli adres IP nie jest ustawiony, u\u017cywane jest automatyczne wykrywanie",
+ "title": "Rolety Motion"
}
}
}
diff --git a/homeassistant/components/motion_blinds/translations/pt.json b/homeassistant/components/motion_blinds/translations/pt.json
index fe188057e4616d..64ccd6061d2e06 100644
--- a/homeassistant/components/motion_blinds/translations/pt.json
+++ b/homeassistant/components/motion_blinds/translations/pt.json
@@ -5,12 +5,25 @@
"already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer",
"connection_error": "Falha na liga\u00e7\u00e3o"
},
+ "flow_title": "Cortinas Motion",
"step": {
+ "connect": {
+ "data": {
+ "api_key": "API Key"
+ },
+ "title": "Cortinas Motion"
+ },
+ "select": {
+ "data": {
+ "select_ip": "Endere\u00e7o IP"
+ }
+ },
"user": {
"data": {
"api_key": "API Key",
"host": "Endere\u00e7o IP"
- }
+ },
+ "title": "Cortinas Motion"
}
}
}
diff --git a/homeassistant/components/motion_blinds/translations/ru.json b/homeassistant/components/motion_blinds/translations/ru.json
index 1a249a4fab86a0..ae2d3229c208ca 100644
--- a/homeassistant/components/motion_blinds/translations/ru.json
+++ b/homeassistant/components/motion_blinds/translations/ru.json
@@ -5,14 +5,31 @@
"already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
"connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
},
+ "error": {
+ "discovery_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0448\u043b\u044e\u0437 Motion."
+ },
"flow_title": "Motion Blinds",
"step": {
+ "connect": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API"
+ },
+ "description": "\u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.",
+ "title": "Motion Blinds"
+ },
+ "select": {
+ "data": {
+ "select_ip": "IP-\u0430\u0434\u0440\u0435\u0441"
+ },
+ "description": "\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0435\u0449\u0451 \u0440\u0430\u0437, \u0435\u0441\u043b\u0438 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u0448\u043b\u044e\u0437",
+ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 Motion"
+ },
"user": {
"data": {
"api_key": "\u041a\u043b\u044e\u0447 API",
"host": "IP-\u0430\u0434\u0440\u0435\u0441"
},
- "description": "\u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.",
+ "description": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u043b\u044e\u0437\u0443 Motion. \u0414\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0448\u043b\u044e\u0437\u0430, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u0443\u0441\u0442\u044b\u043c.",
"title": "Motion Blinds"
}
}
diff --git a/homeassistant/components/motion_blinds/translations/tr.json b/homeassistant/components/motion_blinds/translations/tr.json
index 545a3547ffcd16..194608780c9853 100644
--- a/homeassistant/components/motion_blinds/translations/tr.json
+++ b/homeassistant/components/motion_blinds/translations/tr.json
@@ -1,14 +1,30 @@
{
"config": {
"abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor",
"connection_error": "Ba\u011flanma hatas\u0131"
},
+ "flow_title": "Hareketli Panjurlar",
"step": {
+ "connect": {
+ "data": {
+ "api_key": "API Anahtar\u0131"
+ }
+ },
+ "select": {
+ "data": {
+ "select_ip": "\u0130p Adresi"
+ },
+ "title": "Ba\u011flamak istedi\u011finiz Hareket A\u011f Ge\u00e7idini se\u00e7in"
+ },
"user": {
"data": {
"api_key": "API Anahtar\u0131",
"host": "IP adresi"
- }
+ },
+ "description": "Motion Gateway'inize ba\u011flan\u0131n, IP adresi ayarlanmad\u0131ysa, otomatik ke\u015fif kullan\u0131l\u0131r",
+ "title": "Hareketli Panjurlar"
}
}
}
diff --git a/homeassistant/components/motion_blinds/translations/uk.json b/homeassistant/components/motion_blinds/translations/uk.json
new file mode 100644
index 00000000000000..99ccb60dc6cce9
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/uk.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "connection_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "error": {
+ "discovery_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0438\u044f\u0432\u0438\u0442\u0438 Motion Gateway"
+ },
+ "flow_title": "Motion Blinds",
+ "step": {
+ "connect": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API"
+ },
+ "description": "\u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API, \u0434\u0438\u0432. https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0439",
+ "title": "Motion Blinds"
+ },
+ "select": {
+ "data": {
+ "select_ip": "IP-\u0430\u0434\u0440\u0435\u0441\u0430"
+ },
+ "description": "\u0417\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443 \u0449\u0435 \u0440\u0430\u0437, \u044f\u043a\u0449\u043e \u0432\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 Motion Gateway",
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c Motion Gateway, \u044f\u043a\u0438\u0439 \u0432\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438"
+ },
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430"
+ },
+ "description": "\u041f\u0440\u043e \u0442\u0435, \u044f\u043a \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API, \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0457 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.",
+ "title": "Motion Blinds"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/zh-Hant.json b/homeassistant/components/motion_blinds/translations/zh-Hant.json
index 37925ca6288189..0f2f9881ebd09c 100644
--- a/homeassistant/components/motion_blinds/translations/zh-Hant.json
+++ b/homeassistant/components/motion_blinds/translations/zh-Hant.json
@@ -5,14 +5,31 @@
"already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"connection_error": "\u9023\u7dda\u5931\u6557"
},
+ "error": {
+ "discovery_error": "\u63a2\u7d22 Motion \u9598\u9053\u5668\u5931\u6557"
+ },
"flow_title": "Motion Blinds",
"step": {
+ "connect": {
+ "data": {
+ "api_key": "API \u5bc6\u9470"
+ },
+ "description": "\u5c07\u9700\u8981\u8f38\u5165 16 \u4f4d\u5b57\u5143 API \u5bc6\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002",
+ "title": "Motion Blinds"
+ },
+ "select": {
+ "data": {
+ "select_ip": "IP \u4f4d\u5740"
+ },
+ "description": "\u5047\u5982\u6b32\u9023\u7dda\u81f3\u5176\u4ed6 Motion \u9598\u9053\u5668\uff0c\u8acb\u518d\u57f7\u884c\u4e00\u6b21\u8a2d\u5b9a\u6b65\u9a5f",
+ "title": "\u9078\u64c7\u6240\u8981\u9023\u7dda\u7684 Motion \u7db2\u95dc"
+ },
"user": {
"data": {
"api_key": "API \u5bc6\u9470",
"host": "IP \u4f4d\u5740"
},
- "description": "\u5c07\u9700\u8981\u8f38\u5165 16 \u4f4d\u5b57\u5143 API \u5bc6\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002",
+ "description": "\u9023\u7dda\u81f3 Motion \u9598\u9053\u5668\uff0c\u5047\u5982\u672a\u63d0\u4f9b IP \u4f4d\u5740\uff0c\u5c07\u4f7f\u7528\u81ea\u52d5\u63a2\u7d22",
"title": "Motion Blinds"
}
}
diff --git a/homeassistant/components/mpd/manifest.json b/homeassistant/components/mpd/manifest.json
index 12ce5b61b74bc9..a11b9fedd801c6 100644
--- a/homeassistant/components/mpd/manifest.json
+++ b/homeassistant/components/mpd/manifest.json
@@ -2,6 +2,6 @@
"domain": "mpd",
"name": "Music Player Daemon (MPD)",
"documentation": "https://www.home-assistant.io/integrations/mpd",
- "requirements": ["python-mpd2==3.0.3"],
+ "requirements": ["python-mpd2==3.0.4"],
"codeowners": ["@fabaff"]
}
diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py
index 6685347b3e38cb..adb4bf0e810a85 100644
--- a/homeassistant/components/mpd/media_player.py
+++ b/homeassistant/components/mpd/media_player.py
@@ -1,4 +1,5 @@
"""Support to interact with a Music Player Daemon."""
+from contextlib import suppress
from datetime import timedelta
import hashlib
import logging
@@ -129,10 +130,8 @@ async def _connect(self):
def _disconnect(self):
"""Disconnect from MPD."""
- try:
+ with suppress(mpd.ConnectionError):
self._client.disconnect()
- except mpd.ConnectionError:
- pass
self._is_connected = False
self._status = None
@@ -281,20 +280,22 @@ async def async_get_media_image(self):
try:
response = await self._client.readpicture(file)
except mpd.CommandError as error:
- _LOGGER.warning(
- "Retrieving artwork through `readpicture` command failed: %s",
- error,
- )
+ if error.errno is not mpd.FailureResponseCode.NO_EXIST:
+ _LOGGER.warning(
+ "Retrieving artwork through `readpicture` command failed: %s",
+ error,
+ )
# read artwork contained in the media directory (cover.{jpg,png,tiff,bmp}) if none is embedded
if can_albumart and not response:
try:
response = await self._client.albumart(file)
except mpd.CommandError as error:
- _LOGGER.warning(
- "Retrieving artwork through `albumart` command failed: %s",
- error,
- )
+ if error.errno is not mpd.FailureResponseCode.NO_EXIST:
+ _LOGGER.warning(
+ "Retrieving artwork through `albumart` command failed: %s",
+ error,
+ )
if not response:
return None, None
diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index 788f8d1957ed80..ce2d413e1b69c7 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -1,4 +1,6 @@
"""Support for MQTT message handling."""
+from __future__ import annotations
+
import asyncio
from functools import lru_cache, partial, wraps
import inspect
@@ -8,7 +10,7 @@
import os
import ssl
import time
-from typing import Any, Callable, List, Optional, Union
+from typing import Any, Callable, Union
import uuid
import attr
@@ -19,6 +21,7 @@
from homeassistant.components import websocket_api
from homeassistant.const import (
CONF_CLIENT_ID,
+ CONF_DISCOVERY,
CONF_PASSWORD,
CONF_PAYLOAD,
CONF_PORT,
@@ -28,7 +31,6 @@
EVENT_HOMEASSISTANT_STARTED,
EVENT_HOMEASSISTANT_STOP,
)
-from homeassistant.const import CONF_UNIQUE_ID # noqa: F401
from homeassistant.core import CoreState, Event, HassJob, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, Unauthorized
from homeassistant.helpers import config_validation as cv, event, template
@@ -40,7 +42,6 @@
from homeassistant.util.logging import catch_log_exception
# Loading the config flow file will register the flow
-from . import config_flow # noqa: F401 pylint: disable=unused-import
from . import debug_info, discovery
from .const import (
ATTR_PAYLOAD,
@@ -49,7 +50,6 @@
ATTR_TOPIC,
CONF_BIRTH_MESSAGE,
CONF_BROKER,
- CONF_DISCOVERY,
CONF_QOS,
CONF_RETAIN,
CONF_STATE_TOPIC,
@@ -312,7 +312,7 @@ async def async_subscribe(
topic: str,
msg_callback: MessageCallbackType,
qos: int = DEFAULT_QOS,
- encoding: Optional[str] = "utf-8",
+ encoding: str | None = "utf-8",
):
"""Subscribe to an MQTT topic.
@@ -387,7 +387,7 @@ async def _async_setup_discovery(
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
"""Start the MQTT protocol service."""
- conf: Optional[ConfigType] = config.get(DOMAIN)
+ conf: ConfigType | None = config.get(DOMAIN)
websocket_api.async_register_command(hass, websocket_subscribe)
websocket_api.async_register_command(hass, websocket_remove_device)
@@ -554,7 +554,7 @@ def __init__(
self.hass = hass
self.config_entry = config_entry
self.conf = conf
- self.subscriptions: List[Subscription] = []
+ self.subscriptions: list[Subscription] = []
self.connected = False
self._ha_started = asyncio.Event()
self._last_subscribe = time.time()
@@ -670,7 +670,7 @@ def init_client(self):
will_message = None
if will_message is not None:
- self._mqttc.will_set( # pylint: disable=no-value-for-parameter
+ self._mqttc.will_set(
topic=will_message.topic,
payload=will_message.payload,
qos=will_message.qos,
@@ -732,7 +732,7 @@ async def async_subscribe(
topic: str,
msg_callback: MessageCallbackType,
qos: int,
- encoding: Optional[str] = None,
+ encoding: str | None = None,
) -> Callable[[], None]:
"""Set up a subscription to a topic with the provided qos.
@@ -835,7 +835,7 @@ def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code: int) -> None:
async def publish_birth_message(birth_message):
await self._ha_started.wait() # Wait for Home Assistant to start
await self._discovery_cooldown() # Wait for MQTT discovery to cool down
- await self.async_publish( # pylint: disable=no-value-for-parameter
+ await self.async_publish(
topic=birth_message.topic,
payload=birth_message.payload,
qos=birth_message.qos,
@@ -931,7 +931,9 @@ async def _wait_for_mid(self, mid):
try:
await asyncio.wait_for(self._pending_operations[mid].wait(), TIMEOUT_ACK)
except asyncio.TimeoutError:
- _LOGGER.error("Timed out waiting for mid %s", mid)
+ _LOGGER.warning(
+ "No ACK from MQTT server in %s seconds (mid: %s)", TIMEOUT_ACK, mid
+ )
finally:
del self._pending_operations[mid]
@@ -1054,7 +1056,6 @@ async def forward_messages(mqttmsg: Message):
@callback
def async_subscribe_connection_status(hass, connection_status_callback):
"""Subscribe to MQTT connection changes."""
-
connection_status_callback_job = HassJob(connection_status_callback)
async def connected():
diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py
index 4b209f6f364b0c..78d0434e4128bb 100644
--- a/homeassistant/components/mqtt/abbreviations.py
+++ b/homeassistant/components/mqtt/abbreviations.py
@@ -24,6 +24,7 @@
"bat_lev_tpl": "battery_level_template",
"chrg_t": "charging_topic",
"chrg_tpl": "charging_template",
+ "clrm": "color_mode",
"clr_temp_cmd_t": "color_temp_command_topic",
"clr_temp_stat_t": "color_temp_state_topic",
"clr_temp_tpl": "color_temp_template",
@@ -42,6 +43,7 @@
"dev_cla": "device_class",
"dock_t": "docked_topic",
"dock_tpl": "docked_template",
+ "en": "enabled_by_default",
"err_t": "error_topic",
"err_tpl": "error_template",
"fanspd_t": "fan_speed_topic",
@@ -86,8 +88,13 @@
"on_cmd_type": "on_command_type",
"opt": "optimistic",
"osc_cmd_t": "oscillation_command_topic",
+ "osc_cmd_tpl": "oscillation_command_template",
"osc_stat_t": "oscillation_state_topic",
"osc_val_tpl": "oscillation_value_template",
+ "pct_cmd_t": "percentage_command_topic",
+ "pct_cmd_tpl": "percentage_command_template",
+ "pct_stat_t": "percentage_state_topic",
+ "pct_val_tpl": "percentage_value_template",
"pl": "payload",
"pl_arm_away": "payload_arm_away",
"pl_arm_home": "payload_arm_home",
@@ -124,6 +131,11 @@
"pow_cmd_t": "power_command_topic",
"pow_stat_t": "power_state_topic",
"pow_stat_tpl": "power_state_template",
+ "pr_mode_cmd_t": "preset_mode_command_topic",
+ "pr_mode_cmd_tpl": "preset_mode_command_template",
+ "pr_mode_stat_t": "preset_mode_state_topic",
+ "pr_mode_val_tpl": "preset_mode_value_template",
+ "pr_modes": "preset_modes",
"r_tpl": "red_template",
"ret": "retain",
"rgb_cmd_tpl": "rgb_command_template",
@@ -136,8 +148,11 @@
"set_pos_tpl": "set_position_template",
"set_pos_t": "set_position_topic",
"pos_t": "position_topic",
+ "pos_tpl": "position_template",
"spd_cmd_t": "speed_command_topic",
"spd_stat_t": "speed_state_topic",
+ "spd_rng_min": "speed_range_min",
+ "spd_rng_max": "speed_range_max",
"spd_val_tpl": "speed_value_template",
"spds": "speeds",
"src_type": "source_type",
@@ -147,6 +162,7 @@
"stat_on": "state_on",
"stat_open": "state_open",
"stat_opening": "state_opening",
+ "stat_stopped": "state_stopped",
"stat_locked": "state_locked",
"stat_unlocked": "state_unlocked",
"stat_t": "state_topic",
@@ -154,6 +170,7 @@
"stat_val_tpl": "state_value_template",
"stype": "subtype",
"sup_feat": "supported_features",
+ "sup_clrm": "supported_color_modes",
"swing_mode_cmd_tpl": "swing_mode_command_template",
"swing_mode_cmd_t": "swing_mode_command_topic",
"swing_mode_stat_tpl": "swing_mode_state_template",
@@ -173,6 +190,7 @@
"temp_unit": "temperature_unit",
"tilt_clsd_val": "tilt_closed_value",
"tilt_cmd_t": "tilt_command_topic",
+ "tilt_cmd_tpl": "tilt_command_template",
"tilt_inv_stat": "tilt_invert_state",
"tilt_max": "tilt_max",
"tilt_min": "tilt_min",
@@ -200,4 +218,5 @@
"mf": "manufacturer",
"mdl": "model",
"sw": "sw_version",
+ "sa": "suggested_area",
}
diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py
index 38fec57607e714..0f10e91e41ca9c 100644
--- a/homeassistant/components/mqtt/alarm_control_panel.py
+++ b/homeassistant/components/mqtt/alarm_control_panel.py
@@ -14,9 +14,7 @@
)
from homeassistant.const import (
CONF_CODE,
- CONF_DEVICE,
CONF_NAME,
- CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
@@ -44,13 +42,7 @@
)
from .. import mqtt
from .debug_info import log_messages
-from .mixins import (
- MQTT_AVAILABILITY_SCHEMA,
- MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- MQTT_JSON_ATTRS_SCHEMA,
- MqttEntity,
- async_setup_entry_helper,
-)
+from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper
_LOGGER = logging.getLogger(__name__)
@@ -70,34 +62,28 @@
DEFAULT_ARM_CUSTOM_BYPASS = "ARM_CUSTOM_BYPASS"
DEFAULT_DISARM = "DISARM"
DEFAULT_NAME = "MQTT Alarm"
-PLATFORM_SCHEMA = (
- mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_CODE): cv.string,
- vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
- vol.Optional(CONF_CODE_DISARM_REQUIRED, default=True): cv.boolean,
- vol.Optional(
- CONF_COMMAND_TEMPLATE, default=DEFAULT_COMMAND_TEMPLATE
- ): cv.template,
- vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
- vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
- vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
- vol.Optional(CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT): cv.string,
- vol.Optional(
- CONF_PAYLOAD_ARM_CUSTOM_BYPASS, default=DEFAULT_ARM_CUSTOM_BYPASS
- ): cv.string,
- vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
- vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
- vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
- vol.Optional(CONF_UNIQUE_ID): cv.string,
- vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
- }
- )
- .extend(MQTT_AVAILABILITY_SCHEMA.schema)
- .extend(MQTT_JSON_ATTRS_SCHEMA.schema)
-)
+PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_CODE): cv.string,
+ vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
+ vol.Optional(CONF_CODE_DISARM_REQUIRED, default=True): cv.boolean,
+ vol.Optional(
+ CONF_COMMAND_TEMPLATE, default=DEFAULT_COMMAND_TEMPLATE
+ ): cv.template,
+ vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
+ vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
+ vol.Optional(CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT): cv.string,
+ vol.Optional(
+ CONF_PAYLOAD_ARM_CUSTOM_BYPASS, default=DEFAULT_ARM_CUSTOM_BYPASS
+ ): cv.string,
+ vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
+ vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
+ vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
+ }
+).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
async def async_setup_platform(
@@ -110,7 +96,6 @@ async def async_setup_platform(
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT alarm control panel dynamically through MQTT discovery."""
-
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
@@ -139,7 +124,6 @@ def config_schema():
return PLATFORM_SCHEMA
def _setup_from_config(self, config):
- self._config = config
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = self.hass
@@ -187,11 +171,6 @@ def message_received(msg):
},
)
- @property
- def name(self):
- """Return the name of the device."""
- return self._config[CONF_NAME]
-
@property
def state(self):
"""Return the state of the device."""
diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py
index d965401cef42e2..fbd5e7535c519a 100644
--- a/homeassistant/components/mqtt/binary_sensor.py
+++ b/homeassistant/components/mqtt/binary_sensor.py
@@ -11,13 +11,11 @@
BinarySensorEntity,
)
from homeassistant.const import (
- CONF_DEVICE,
CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE,
CONF_NAME,
CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON,
- CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import callback
@@ -32,9 +30,7 @@
from .. import mqtt
from .debug_info import log_messages
from .mixins import (
- MQTT_AVAILABILITY_SCHEMA,
- MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- MQTT_JSON_ATTRS_SCHEMA,
+ MQTT_ENTITY_COMMON_SCHEMA,
MqttAvailability,
MqttEntity,
async_setup_entry_helper,
@@ -49,23 +45,17 @@
DEFAULT_FORCE_UPDATE = False
CONF_EXPIRE_AFTER = "expire_after"
-PLATFORM_SCHEMA = (
- mqtt.MQTT_RO_PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
- vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
- vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_OFF_DELAY): cv.positive_int,
- vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
- vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
- vol.Optional(CONF_UNIQUE_ID): cv.string,
- }
- )
- .extend(MQTT_AVAILABILITY_SCHEMA.schema)
- .extend(MQTT_JSON_ATTRS_SCHEMA.schema)
-)
+PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
+ vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
+ vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_OFF_DELAY): cv.positive_int,
+ vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
+ vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
+ }
+).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
async def async_setup_platform(
@@ -78,7 +68,6 @@ async def async_setup_platform(
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT binary sensor dynamically through MQTT discovery."""
-
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
@@ -114,7 +103,6 @@ def config_schema():
return PLATFORM_SCHEMA
def _setup_from_config(self, config):
- self._config = config
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = self.hass
@@ -214,17 +202,11 @@ def state_message_received(msg):
@callback
def _value_is_expired(self, *_):
"""Triggered when value is expired."""
-
self._expiration_trigger = None
self._expired = True
self.async_write_ha_state()
- @property
- def name(self):
- """Return the name of the binary sensor."""
- return self._config[CONF_NAME]
-
@property
def is_on(self):
"""Return true if the binary sensor is on."""
@@ -244,7 +226,6 @@ def force_update(self):
def available(self) -> bool:
"""Return true if the device is available and value has not expired."""
expire_after = self._config.get(CONF_EXPIRE_AFTER)
- # pylint: disable=no-member
return MqttAvailability.available.fget(self) and (
expire_after is None or not self._expired
)
diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py
index 21fcb9276dd40c..0a1a35b2ddd4a4 100644
--- a/homeassistant/components/mqtt/camera.py
+++ b/homeassistant/components/mqtt/camera.py
@@ -1,12 +1,11 @@
"""Camera that loads a picture from an MQTT topic."""
import functools
-import logging
import voluptuous as vol
from homeassistant.components import camera
from homeassistant.components.camera import Camera
-from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_UNIQUE_ID
+from homeassistant.const import CONF_NAME
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.reload import async_setup_reload_service
@@ -15,31 +14,17 @@
from . import CONF_QOS, DOMAIN, PLATFORMS, subscription
from .. import mqtt
from .debug_info import log_messages
-from .mixins import (
- MQTT_AVAILABILITY_SCHEMA,
- MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- MQTT_JSON_ATTRS_SCHEMA,
- MqttEntity,
- async_setup_entry_helper,
-)
-
-_LOGGER = logging.getLogger(__name__)
+from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper
CONF_TOPIC = "topic"
DEFAULT_NAME = "MQTT Camera"
-PLATFORM_SCHEMA = (
- mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
- vol.Optional(CONF_UNIQUE_ID): cv.string,
- }
- )
- .extend(MQTT_AVAILABILITY_SCHEMA.schema)
- .extend(MQTT_JSON_ATTRS_SCHEMA.schema)
-)
+PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
+ }
+).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
async def async_setup_platform(
@@ -52,7 +37,6 @@ async def async_setup_platform(
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT camera dynamically through MQTT discovery."""
-
setup = functools.partial(
_async_setup_entity, async_add_entities, config_entry=config_entry
)
@@ -81,9 +65,6 @@ def config_schema():
"""Return the config schema."""
return PLATFORM_SCHEMA
- def _setup_from_config(self, config):
- self._config = config
-
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
@@ -109,8 +90,3 @@ def message_received(msg):
async def async_camera_image(self):
"""Return image response."""
return self._last_image
-
- @property
- def name(self):
- """Return the name of this camera."""
- return self._config[CONF_NAME]
diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py
index 15c7c916eeb599..8ab7a9ca3cfb66 100644
--- a/homeassistant/components/mqtt/climate.py
+++ b/homeassistant/components/mqtt/climate.py
@@ -36,10 +36,10 @@
)
from homeassistant.const import (
ATTR_TEMPERATURE,
- CONF_DEVICE,
CONF_NAME,
+ CONF_PAYLOAD_OFF,
+ CONF_PAYLOAD_ON,
CONF_TEMPERATURE_UNIT,
- CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
PRECISION_HALVES,
PRECISION_TENTHS,
@@ -61,13 +61,7 @@
)
from .. import mqtt
from .debug_info import log_messages
-from .mixins import (
- MQTT_AVAILABILITY_SCHEMA,
- MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- MQTT_JSON_ATTRS_SCHEMA,
- MqttEntity,
- async_setup_entry_helper,
-)
+from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper
_LOGGER = logging.getLogger(__name__)
@@ -98,8 +92,6 @@
CONF_MODE_LIST = "modes"
CONF_MODE_STATE_TEMPLATE = "mode_state_template"
CONF_MODE_STATE_TOPIC = "mode_state_topic"
-CONF_PAYLOAD_OFF = "payload_off"
-CONF_PAYLOAD_ON = "payload_on"
CONF_POWER_COMMAND_TOPIC = "power_command_topic"
CONF_POWER_STATE_TEMPLATE = "power_state_template"
CONF_POWER_STATE_TOPIC = "power_state_topic"
@@ -178,90 +170,84 @@
)
SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema)
-PLATFORM_SCHEMA = (
- SCHEMA_BASE.extend(
- {
- vol.Optional(CONF_AUX_COMMAND_TOPIC): mqtt.valid_publish_topic,
- vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template,
- vol.Optional(CONF_AUX_STATE_TOPIC): mqtt.valid_subscribe_topic,
- vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
- vol.Optional(CONF_AWAY_MODE_STATE_TEMPLATE): cv.template,
- vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
- vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template,
- vol.Optional(CONF_CURRENT_TEMP_TOPIC): mqtt.valid_subscribe_topic,
- vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- vol.Optional(CONF_FAN_MODE_COMMAND_TEMPLATE): cv.template,
- vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
- vol.Optional(
- CONF_FAN_MODE_LIST,
- default=[FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH],
- ): cv.ensure_list,
- vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template,
- vol.Optional(CONF_FAN_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
- vol.Optional(CONF_HOLD_COMMAND_TEMPLATE): cv.template,
- vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic,
- vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template,
- vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic,
- vol.Optional(CONF_HOLD_LIST, default=list): cv.ensure_list,
- vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template,
- vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
- vol.Optional(
- CONF_MODE_LIST,
- default=[
- HVAC_MODE_AUTO,
- HVAC_MODE_OFF,
- HVAC_MODE_COOL,
- HVAC_MODE_HEAT,
- HVAC_MODE_DRY,
- HVAC_MODE_FAN_ONLY,
- ],
- ): cv.ensure_list,
- vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template,
- vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string,
- vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string,
- vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic,
- vol.Optional(CONF_POWER_STATE_TEMPLATE): cv.template,
- vol.Optional(CONF_POWER_STATE_TOPIC): mqtt.valid_subscribe_topic,
- vol.Optional(CONF_PRECISION): vol.In(
- [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]
- ),
- vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
- vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean,
- vol.Optional(CONF_ACTION_TEMPLATE): cv.template,
- vol.Optional(CONF_ACTION_TOPIC): mqtt.valid_subscribe_topic,
- vol.Optional(CONF_SWING_MODE_COMMAND_TEMPLATE): cv.template,
- vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
- vol.Optional(
- CONF_SWING_MODE_LIST, default=[STATE_ON, HVAC_MODE_OFF]
- ): cv.ensure_list,
- vol.Optional(CONF_SWING_MODE_STATE_TEMPLATE): cv.template,
- vol.Optional(CONF_SWING_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
- vol.Optional(CONF_TEMP_INITIAL, default=21): cv.positive_int,
- vol.Optional(CONF_TEMP_MIN, default=DEFAULT_MIN_TEMP): vol.Coerce(float),
- vol.Optional(CONF_TEMP_MAX, default=DEFAULT_MAX_TEMP): vol.Coerce(float),
- vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float),
- vol.Optional(CONF_TEMP_COMMAND_TEMPLATE): cv.template,
- vol.Optional(CONF_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic,
- vol.Optional(CONF_TEMP_HIGH_COMMAND_TEMPLATE): cv.template,
- vol.Optional(CONF_TEMP_HIGH_COMMAND_TOPIC): mqtt.valid_publish_topic,
- vol.Optional(CONF_TEMP_HIGH_STATE_TOPIC): mqtt.valid_subscribe_topic,
- vol.Optional(CONF_TEMP_HIGH_STATE_TEMPLATE): cv.template,
- vol.Optional(CONF_TEMP_LOW_COMMAND_TEMPLATE): cv.template,
- vol.Optional(CONF_TEMP_LOW_COMMAND_TOPIC): mqtt.valid_publish_topic,
- vol.Optional(CONF_TEMP_LOW_STATE_TEMPLATE): cv.template,
- vol.Optional(CONF_TEMP_LOW_STATE_TOPIC): mqtt.valid_subscribe_topic,
- vol.Optional(CONF_TEMP_STATE_TEMPLATE): cv.template,
- vol.Optional(CONF_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic,
- vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit,
- vol.Optional(CONF_UNIQUE_ID): cv.string,
- vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
- }
- )
- .extend(MQTT_AVAILABILITY_SCHEMA.schema)
- .extend(MQTT_JSON_ATTRS_SCHEMA.schema)
-)
+PLATFORM_SCHEMA = SCHEMA_BASE.extend(
+ {
+ vol.Optional(CONF_AUX_COMMAND_TOPIC): mqtt.valid_publish_topic,
+ vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template,
+ vol.Optional(CONF_AUX_STATE_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
+ vol.Optional(CONF_AWAY_MODE_STATE_TEMPLATE): cv.template,
+ vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template,
+ vol.Optional(CONF_CURRENT_TEMP_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_FAN_MODE_COMMAND_TEMPLATE): cv.template,
+ vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
+ vol.Optional(
+ CONF_FAN_MODE_LIST,
+ default=[FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH],
+ ): cv.ensure_list,
+ vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template,
+ vol.Optional(CONF_FAN_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_HOLD_COMMAND_TEMPLATE): cv.template,
+ vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic,
+ vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template,
+ vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_HOLD_LIST, default=list): cv.ensure_list,
+ vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template,
+ vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
+ vol.Optional(
+ CONF_MODE_LIST,
+ default=[
+ HVAC_MODE_AUTO,
+ HVAC_MODE_OFF,
+ HVAC_MODE_COOL,
+ HVAC_MODE_HEAT,
+ HVAC_MODE_DRY,
+ HVAC_MODE_FAN_ONLY,
+ ],
+ ): cv.ensure_list,
+ vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template,
+ vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string,
+ vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string,
+ vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic,
+ vol.Optional(CONF_POWER_STATE_TEMPLATE): cv.template,
+ vol.Optional(CONF_POWER_STATE_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_PRECISION): vol.In(
+ [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]
+ ),
+ vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
+ vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean,
+ vol.Optional(CONF_ACTION_TEMPLATE): cv.template,
+ vol.Optional(CONF_ACTION_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_SWING_MODE_COMMAND_TEMPLATE): cv.template,
+ vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
+ vol.Optional(
+ CONF_SWING_MODE_LIST, default=[STATE_ON, HVAC_MODE_OFF]
+ ): cv.ensure_list,
+ vol.Optional(CONF_SWING_MODE_STATE_TEMPLATE): cv.template,
+ vol.Optional(CONF_SWING_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_TEMP_INITIAL, default=21): cv.positive_int,
+ vol.Optional(CONF_TEMP_MIN, default=DEFAULT_MIN_TEMP): vol.Coerce(float),
+ vol.Optional(CONF_TEMP_MAX, default=DEFAULT_MAX_TEMP): vol.Coerce(float),
+ vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float),
+ vol.Optional(CONF_TEMP_COMMAND_TEMPLATE): cv.template,
+ vol.Optional(CONF_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic,
+ vol.Optional(CONF_TEMP_HIGH_COMMAND_TEMPLATE): cv.template,
+ vol.Optional(CONF_TEMP_HIGH_COMMAND_TOPIC): mqtt.valid_publish_topic,
+ vol.Optional(CONF_TEMP_HIGH_STATE_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_TEMP_HIGH_STATE_TEMPLATE): cv.template,
+ vol.Optional(CONF_TEMP_LOW_COMMAND_TEMPLATE): cv.template,
+ vol.Optional(CONF_TEMP_LOW_COMMAND_TOPIC): mqtt.valid_publish_topic,
+ vol.Optional(CONF_TEMP_LOW_STATE_TEMPLATE): cv.template,
+ vol.Optional(CONF_TEMP_LOW_STATE_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_TEMP_STATE_TEMPLATE): cv.template,
+ vol.Optional(CONF_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit,
+ vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
+ }
+).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
async def async_setup_platform(
@@ -274,7 +260,6 @@ async def async_setup_platform(
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT climate device dynamically through MQTT discovery."""
-
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
@@ -322,7 +307,6 @@ async def async_added_to_hass(self):
def _setup_from_config(self, config):
"""(Re)Setup the entity."""
- self._config = config
self._topic = {key: config.get(key) for key in TOPIC_KEYS}
# set to None in non-optimistic mode
@@ -564,11 +548,6 @@ def handle_hold_mode_received(msg):
self.hass, self._sub_state, topics
)
- @property
- def name(self):
- """Return the name of the climate device."""
- return self._config[CONF_NAME]
-
@property
def temperature_unit(self):
"""Return the unit of measurement."""
diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py
index 5c4016437a62d2..5e5b8c54cf2493 100644
--- a/homeassistant/components/mqtt/config_flow.py
+++ b/homeassistant/components/mqtt/config_flow.py
@@ -1,12 +1,12 @@
"""Config flow for MQTT."""
from collections import OrderedDict
-import logging
import queue
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
+ CONF_DISCOVERY,
CONF_HOST,
CONF_PASSWORD,
CONF_PAYLOAD,
@@ -22,19 +22,17 @@
ATTR_TOPIC,
CONF_BIRTH_MESSAGE,
CONF_BROKER,
- CONF_DISCOVERY,
CONF_WILL_MESSAGE,
DATA_MQTT_CONFIG,
DEFAULT_BIRTH,
DEFAULT_DISCOVERY,
DEFAULT_WILL,
+ DOMAIN,
)
from .util import MQTT_WILL_BIRTH_SCHEMA
-_LOGGER = logging.getLogger(__name__)
-
-@config_entries.HANDLERS.register("mqtt")
+@config_entries.HANDLERS.register(DOMAIN)
class FlowHandler(config_entries.ConfigFlow):
"""Handle a config flow."""
diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py
index 3e56ab6caf93f7..6c334eca311a28 100644
--- a/homeassistant/components/mqtt/const.py
+++ b/homeassistant/components/mqtt/const.py
@@ -11,7 +11,6 @@
CONF_BROKER = "broker"
CONF_BIRTH_MESSAGE = "birth_message"
-CONF_DISCOVERY = "discovery"
CONF_QOS = ATTR_QOS
CONF_RETAIN = ATTR_RETAIN
CONF_STATE_TOPIC = "state_topic"
diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py
index dc2cba0efab6ee..010f751dad47c8 100644
--- a/homeassistant/components/mqtt/cover.py
+++ b/homeassistant/components/mqtt/cover.py
@@ -20,11 +20,9 @@
CoverEntity,
)
from homeassistant.const import (
- CONF_DEVICE,
CONF_DEVICE_CLASS,
CONF_NAME,
CONF_OPTIMISTIC,
- CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
STATE_CLOSED,
STATE_CLOSING,
@@ -48,20 +46,16 @@
)
from .. import mqtt
from .debug_info import log_messages
-from .mixins import (
- MQTT_AVAILABILITY_SCHEMA,
- MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- MQTT_JSON_ATTRS_SCHEMA,
- MqttEntity,
- async_setup_entry_helper,
-)
+from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper
_LOGGER = logging.getLogger(__name__)
CONF_GET_POSITION_TOPIC = "position_topic"
-CONF_SET_POSITION_TEMPLATE = "set_position_template"
+CONF_GET_POSITION_TEMPLATE = "position_template"
CONF_SET_POSITION_TOPIC = "set_position_topic"
+CONF_SET_POSITION_TEMPLATE = "set_position_template"
CONF_TILT_COMMAND_TOPIC = "tilt_command_topic"
+CONF_TILT_COMMAND_TEMPLATE = "tilt_command_template"
CONF_TILT_STATUS_TOPIC = "tilt_status_topic"
CONF_TILT_STATUS_TEMPLATE = "tilt_status_template"
@@ -74,6 +68,7 @@
CONF_STATE_CLOSING = "state_closing"
CONF_STATE_OPEN = "state_open"
CONF_STATE_OPENING = "state_opening"
+CONF_STATE_STOPPED = "state_stopped"
CONF_TILT_CLOSED_POSITION = "tilt_closed_value"
CONF_TILT_INVERT_STATE = "tilt_invert_state"
CONF_TILT_MAX = "tilt_max"
@@ -92,6 +87,7 @@
DEFAULT_POSITION_CLOSED = 0
DEFAULT_POSITION_OPEN = 100
DEFAULT_RETAIN = False
+DEFAULT_STATE_STOPPED = "stopped"
DEFAULT_TILT_CLOSED_POSITION = 0
DEFAULT_TILT_INVERT_STATE = False
DEFAULT_TILT_MAX = 100
@@ -115,8 +111,27 @@ def validate_options(value):
"""
if CONF_SET_POSITION_TOPIC in value and CONF_GET_POSITION_TOPIC not in value:
raise vol.Invalid(
- "set_position_topic must be set together with position_topic."
+ "'set_position_topic' must be set together with 'position_topic'."
+ )
+
+ if (
+ CONF_GET_POSITION_TOPIC in value
+ and CONF_STATE_TOPIC not in value
+ and CONF_VALUE_TEMPLATE in value
+ ):
+ _LOGGER.warning(
+ "Using 'value_template' for 'position_topic' is deprecated "
+ "and will be removed from Home Assistant in version 2021.6, "
+ "please replace it with 'position_template'"
+ )
+
+ if CONF_TILT_INVERT_STATE in value:
+ _LOGGER.warning(
+ "'tilt_invert_state' is deprecated "
+ "and will be removed from Home Assistant in version 2021.6, "
+ "please invert tilt using 'tilt_min' & 'tilt_max'"
)
+
return value
@@ -124,7 +139,6 @@ def validate_options(value):
mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
- vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_GET_POSITION_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
@@ -143,14 +157,13 @@ def validate_options(value):
vol.Optional(CONF_STATE_CLOSING, default=STATE_CLOSING): cv.string,
vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string,
vol.Optional(CONF_STATE_OPENING, default=STATE_OPENING): cv.string,
+ vol.Optional(CONF_STATE_STOPPED, default=DEFAULT_STATE_STOPPED): cv.string,
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(
CONF_TILT_CLOSED_POSITION, default=DEFAULT_TILT_CLOSED_POSITION
): int,
vol.Optional(CONF_TILT_COMMAND_TOPIC): mqtt.valid_publish_topic,
- vol.Optional(
- CONF_TILT_INVERT_STATE, default=DEFAULT_TILT_INVERT_STATE
- ): cv.boolean,
+ vol.Optional(CONF_TILT_INVERT_STATE): cv.boolean,
vol.Optional(CONF_TILT_MAX, default=DEFAULT_TILT_MAX): int,
vol.Optional(CONF_TILT_MIN, default=DEFAULT_TILT_MIN): int,
vol.Optional(
@@ -161,12 +174,11 @@ def validate_options(value):
): cv.boolean,
vol.Optional(CONF_TILT_STATUS_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_TILT_STATUS_TEMPLATE): cv.template,
- vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
+ vol.Optional(CONF_GET_POSITION_TEMPLATE): cv.template,
+ vol.Optional(CONF_TILT_COMMAND_TEMPLATE): cv.template,
}
- )
- .extend(MQTT_AVAILABILITY_SCHEMA.schema)
- .extend(MQTT_JSON_ATTRS_SCHEMA.schema),
+ ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema),
validate_options,
)
@@ -181,7 +193,6 @@ async def async_setup_platform(
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT cover dynamically through MQTT discovery."""
-
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
@@ -215,19 +226,28 @@ def config_schema():
return PLATFORM_SCHEMA
def _setup_from_config(self, config):
- self._config = config
self._optimistic = config[CONF_OPTIMISTIC] or (
config.get(CONF_STATE_TOPIC) is None
and config.get(CONF_GET_POSITION_TOPIC) is None
)
self._tilt_optimistic = config[CONF_TILT_STATE_OPTIMISTIC]
- template = self._config.get(CONF_VALUE_TEMPLATE)
- if template is not None:
- template.hass = self.hass
+ value_template = self._config.get(CONF_VALUE_TEMPLATE)
+ if value_template is not None:
+ value_template.hass = self.hass
+
set_position_template = self._config.get(CONF_SET_POSITION_TEMPLATE)
if set_position_template is not None:
set_position_template.hass = self.hass
+
+ get_position_template = self._config.get(CONF_GET_POSITION_TEMPLATE)
+ if get_position_template is not None:
+ get_position_template.hass = self.hass
+
+ set_tilt_template = self._config.get(CONF_TILT_COMMAND_TEMPLATE)
+ if set_tilt_template is not None:
+ set_tilt_template.hass = self.hass
+
tilt_status_template = self._config.get(CONF_TILT_STATUS_TEMPLATE)
if tilt_status_template is not None:
tilt_status_template.hass = self.hass
@@ -247,36 +267,58 @@ def tilt_message_received(msg):
payload
)
- if payload.isnumeric() and (
+ if not payload.isnumeric():
+ _LOGGER.warning("Payload '%s' is not numeric", payload)
+ elif (
self._config[CONF_TILT_MIN]
<= int(payload)
<= self._config[CONF_TILT_MAX]
+ or self._config[CONF_TILT_MAX]
+ <= int(payload)
+ <= self._config[CONF_TILT_MIN]
):
-
level = self.find_percentage_in_range(float(payload))
self._tilt_value = level
self.async_write_ha_state()
+ else:
+ _LOGGER.warning(
+ "Payload '%s' is out of range, must be between '%s' and '%s' inclusive",
+ payload,
+ self._config[CONF_TILT_MIN],
+ self._config[CONF_TILT_MAX],
+ )
@callback
@log_messages(self.hass, self.entity_id)
def state_message_received(msg):
"""Handle new MQTT state messages."""
payload = msg.payload
- template = self._config.get(CONF_VALUE_TEMPLATE)
- if template is not None:
- payload = template.async_render_with_possible_json_value(payload)
-
- if payload == self._config[CONF_STATE_OPEN]:
- self._state = STATE_OPEN
+ value_template = self._config.get(CONF_VALUE_TEMPLATE)
+ if value_template is not None:
+ payload = value_template.async_render_with_possible_json_value(payload)
+
+ if payload == self._config[CONF_STATE_STOPPED]:
+ if self._config.get(CONF_GET_POSITION_TOPIC) is not None:
+ self._state = (
+ STATE_CLOSED
+ if self._position == DEFAULT_POSITION_CLOSED
+ else STATE_OPEN
+ )
+ else:
+ self._state = (
+ STATE_CLOSED if self._state == STATE_CLOSING else STATE_OPEN
+ )
elif payload == self._config[CONF_STATE_OPENING]:
self._state = STATE_OPENING
- elif payload == self._config[CONF_STATE_CLOSED]:
- self._state = STATE_CLOSED
elif payload == self._config[CONF_STATE_CLOSING]:
self._state = STATE_CLOSING
+ elif payload == self._config[CONF_STATE_OPEN]:
+ self._state = STATE_OPEN
+ elif payload == self._config[CONF_STATE_CLOSED]:
+ self._state = STATE_CLOSED
else:
_LOGGER.warning(
- "Payload is not supported (e.g. open, closed, opening, closing): %s",
+ "Payload is not supported (e.g. open, closed, opening, closing, stopped): %s",
payload,
)
return
@@ -286,9 +328,16 @@ def state_message_received(msg):
@callback
@log_messages(self.hass, self.entity_id)
def position_message_received(msg):
- """Handle new MQTT state messages."""
+ """Handle new MQTT position messages."""
payload = msg.payload
- template = self._config.get(CONF_VALUE_TEMPLATE)
+
+ template = self._config.get(CONF_GET_POSITION_TEMPLATE)
+
+ # To be removed in 2021.6:
+ # allow using `value_template` as position template if no `state_topic`
+ if template is None and self._config.get(CONF_STATE_TOPIC) is None:
+ template = self._config.get(CONF_VALUE_TEMPLATE)
+
if template is not None:
payload = template.async_render_with_possible_json_value(payload)
@@ -297,13 +346,14 @@ def position_message_received(msg):
float(payload), COVER_PAYLOAD
)
self._position = percentage_payload
- self._state = (
- STATE_CLOSED
- if percentage_payload == DEFAULT_POSITION_CLOSED
- else STATE_OPEN
- )
+ if self._config.get(CONF_STATE_TOPIC) is None:
+ self._state = (
+ STATE_CLOSED
+ if percentage_payload == DEFAULT_POSITION_CLOSED
+ else STATE_OPEN
+ )
else:
- _LOGGER.warning("Payload is not integer within range: %s", payload)
+ _LOGGER.warning("Payload '%s' is not numeric", payload)
return
self.async_write_ha_state()
@@ -313,13 +363,18 @@ def position_message_received(msg):
"msg_callback": position_message_received,
"qos": self._config[CONF_QOS],
}
- elif self._config.get(CONF_STATE_TOPIC):
+
+ if self._config.get(CONF_STATE_TOPIC):
topics["state_topic"] = {
"topic": self._config.get(CONF_STATE_TOPIC),
"msg_callback": state_message_received,
"qos": self._config[CONF_QOS],
}
- else:
+
+ if (
+ self._config.get(CONF_GET_POSITION_TOPIC) is None
+ and self._config.get(CONF_STATE_TOPIC) is None
+ ):
# Force into optimistic mode.
self._optimistic = True
@@ -342,11 +397,6 @@ def assumed_state(self):
"""Return true if we do optimistic updates."""
return self._optimistic
- @property
- def name(self):
- """Return the name of the cover."""
- return self._config[CONF_NAME]
-
@property
def is_closed(self):
"""Return true if the cover is closed or None if the status is unknown."""
@@ -488,28 +538,32 @@ async def async_close_cover_tilt(self, **kwargs):
async def async_set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
- position = kwargs[ATTR_TILT_POSITION]
-
- # The position needs to be between min and max
- level = self.find_in_range_from_percent(position)
+ set_tilt_template = self._config.get(CONF_TILT_COMMAND_TEMPLATE)
+ tilt = kwargs[ATTR_TILT_POSITION]
+ percentage_tilt = tilt
+ tilt = self.find_in_range_from_percent(tilt)
+ if set_tilt_template is not None:
+ tilt = set_tilt_template.async_render(parse_result=False, **kwargs)
mqtt.async_publish(
self.hass,
self._config.get(CONF_TILT_COMMAND_TOPIC),
- level,
+ tilt,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
)
+ if self._tilt_optimistic:
+ self._tilt_value = percentage_tilt
+ self.async_write_ha_state()
async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
set_position_template = self._config.get(CONF_SET_POSITION_TEMPLATE)
position = kwargs[ATTR_POSITION]
percentage_position = position
+ position = self.find_in_range_from_percent(position, COVER_PAYLOAD)
if set_position_template is not None:
position = set_position_template.async_render(parse_result=False, **kwargs)
- else:
- position = self.find_in_range_from_percent(position, COVER_PAYLOAD)
mqtt.async_publish(
self.hass,
@@ -557,7 +611,7 @@ def find_percentage_in_range(self, position, range_type=TILT_PAYLOAD):
max_percent = 100
min_percent = 0
position_percentage = min(max(position_percentage, min_percent), max_percent)
- if range_type == TILT_PAYLOAD and self._config[CONF_TILT_INVERT_STATE]:
+ if range_type == TILT_PAYLOAD and self._config.get(CONF_TILT_INVERT_STATE):
return 100 - position_percentage
return position_percentage
@@ -581,6 +635,6 @@ def find_in_range_from_percent(self, percentage, range_type=TILT_PAYLOAD):
position = round(current_range * (percentage / 100.0))
position += offset
- if range_type == TILT_PAYLOAD and self._config[CONF_TILT_INVERT_STATE]:
+ if range_type == TILT_PAYLOAD and self._config.get(CONF_TILT_INVERT_STATE):
position = max_range - position + offset
return position
diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py
index a3c56652253696..52aeb20e3aa4bc 100644
--- a/homeassistant/components/mqtt/debug_info.py
+++ b/homeassistant/components/mqtt/debug_info.py
@@ -1,7 +1,6 @@
"""Helper to handle a set of topics to subscribe to."""
from collections import deque
from functools import wraps
-import logging
from typing import Any
from homeassistant.helpers.typing import HomeAssistantType
@@ -9,8 +8,6 @@
from .const import ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC
from .models import MessageCallbackType
-_LOGGER = logging.getLogger(__name__)
-
DATA_MQTT_DEBUG_INFO = "mqtt_debug_info"
STORED_MESSAGES = 10
diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py
index d3e1f33421df30..50d6a6e4d19587 100644
--- a/homeassistant/components/mqtt/device_automation.py
+++ b/homeassistant/components/mqtt/device_automation.py
@@ -1,6 +1,5 @@
"""Provides device automations for MQTT."""
import functools
-import logging
import voluptuous as vol
@@ -10,8 +9,6 @@
from .. import mqtt
from .mixins import async_setup_entry_helper
-_LOGGER = logging.getLogger(__name__)
-
AUTOMATION_TYPE_TRIGGER = "trigger"
AUTOMATION_TYPES = [AUTOMATION_TYPE_TRIGGER]
AUTOMATION_TYPES_SCHEMA = vol.In(AUTOMATION_TYPES)
diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py
index 8b51b9fac0e0ca..d6688636bb2aa7 100644
--- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py
+++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py
@@ -1,6 +1,5 @@
"""Support for tracking MQTT enabled devices identified through discovery."""
import functools
-import logging
import voluptuous as vol
@@ -11,10 +10,7 @@
ATTR_GPS_ACCURACY,
ATTR_LATITUDE,
ATTR_LONGITUDE,
- CONF_DEVICE,
- CONF_ICON,
CONF_NAME,
- CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
STATE_HOME,
STATE_NOT_HOME,
@@ -26,40 +22,24 @@
from ... import mqtt
from ..const import CONF_QOS, CONF_STATE_TOPIC
from ..debug_info import log_messages
-from ..mixins import (
- MQTT_AVAILABILITY_SCHEMA,
- MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- MQTT_JSON_ATTRS_SCHEMA,
- MqttEntity,
- async_setup_entry_helper,
-)
-
-_LOGGER = logging.getLogger(__name__)
+from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper
CONF_PAYLOAD_HOME = "payload_home"
CONF_PAYLOAD_NOT_HOME = "payload_not_home"
CONF_SOURCE_TYPE = "source_type"
-PLATFORM_SCHEMA_DISCOVERY = (
- mqtt.MQTT_RO_PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- vol.Optional(CONF_ICON): cv.icon,
- vol.Optional(CONF_NAME): cv.string,
- vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string,
- vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string,
- vol.Optional(CONF_SOURCE_TYPE): vol.In(SOURCE_TYPES),
- vol.Optional(CONF_UNIQUE_ID): cv.string,
- }
- )
- .extend(MQTT_AVAILABILITY_SCHEMA.schema)
- .extend(MQTT_JSON_ATTRS_SCHEMA.schema)
-)
+PLATFORM_SCHEMA_DISCOVERY = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_NAME): cv.string,
+ vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string,
+ vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string,
+ vol.Optional(CONF_SOURCE_TYPE): vol.In(SOURCE_TYPES),
+ }
+).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
async def async_setup_entry_from_discovery(hass, config_entry, async_add_entities):
"""Set up MQTT device tracker dynamically through MQTT discovery."""
-
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
@@ -91,8 +71,6 @@ def config_schema():
def _setup_from_config(self, config):
"""(Re)Setup the entity."""
- self._config = config
-
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = self.hass
@@ -129,39 +107,34 @@ def message_received(msg):
},
)
- @property
- def icon(self):
- """Return the icon of the device."""
- return self._config.get(CONF_ICON)
-
@property
def latitude(self):
- """Return latitude if provided in device_state_attributes or None."""
+ """Return latitude if provided in extra_state_attributes or None."""
if (
- self.device_state_attributes is not None
- and ATTR_LATITUDE in self.device_state_attributes
+ self.extra_state_attributes is not None
+ and ATTR_LATITUDE in self.extra_state_attributes
):
- return self.device_state_attributes[ATTR_LATITUDE]
+ return self.extra_state_attributes[ATTR_LATITUDE]
return None
@property
def location_accuracy(self):
- """Return location accuracy if provided in device_state_attributes or None."""
+ """Return location accuracy if provided in extra_state_attributes or None."""
if (
- self.device_state_attributes is not None
- and ATTR_GPS_ACCURACY in self.device_state_attributes
+ self.extra_state_attributes is not None
+ and ATTR_GPS_ACCURACY in self.extra_state_attributes
):
- return self.device_state_attributes[ATTR_GPS_ACCURACY]
+ return self.extra_state_attributes[ATTR_GPS_ACCURACY]
return None
@property
def longitude(self):
- """Return longitude if provided in device_state_attributes or None."""
+ """Return longitude if provided in extra_state_attributes or None."""
if (
- self.device_state_attributes is not None
- and ATTR_LONGITUDE in self.device_state_attributes
+ self.extra_state_attributes is not None
+ and ATTR_LONGITUDE in self.extra_state_attributes
):
- return self.device_state_attributes[ATTR_LONGITUDE]
+ return self.extra_state_attributes[ATTR_LONGITUDE]
return None
@property
@@ -169,11 +142,6 @@ def location_name(self):
"""Return a location name for the current location of the device."""
return self._location_name
- @property
- def name(self):
- """Return the name of the device tracker."""
- return self._config.get(CONF_NAME)
-
@property
def source_type(self):
"""Return the source type, eg gps or router, of the device."""
diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py
index 6a04fd48049753..1e058162bc3fbb 100644
--- a/homeassistant/components/mqtt/device_trigger.py
+++ b/homeassistant/components/mqtt/device_trigger.py
@@ -1,6 +1,8 @@
"""Provides device automations for MQTT."""
+from __future__ import annotations
+
import logging
-from typing import Callable, List, Optional
+from typing import Callable
import attr
import voluptuous as vol
@@ -13,6 +15,7 @@
CONF_DOMAIN,
CONF_PLATFORM,
CONF_TYPE,
+ CONF_VALUE_TEMPLATE,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
@@ -66,10 +69,11 @@
{
vol.Required(CONF_AUTOMATION_TYPE): str,
vol.Required(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_PAYLOAD, default=None): vol.Any(None, cv.string),
- vol.Required(CONF_TYPE): cv.string,
vol.Required(CONF_SUBTYPE): cv.string,
+ vol.Required(CONF_TOPIC): cv.string,
+ vol.Required(CONF_TYPE): cv.string,
+ vol.Optional(CONF_VALUE_TEMPLATE, default=None): vol.Any(None, cv.string),
},
validate_device_has_at_least_one_identifier,
)
@@ -83,18 +87,22 @@ class TriggerInstance:
action: AutomationActionType = attr.ib()
automation_info: dict = attr.ib()
- trigger: "Trigger" = attr.ib()
- remove: Optional[CALLBACK_TYPE] = attr.ib(default=None)
+ trigger: Trigger = attr.ib()
+ remove: CALLBACK_TYPE | None = attr.ib(default=None)
async def async_attach_trigger(self):
"""Attach MQTT trigger."""
mqtt_config = {
+ mqtt_trigger.CONF_PLATFORM: mqtt.DOMAIN,
mqtt_trigger.CONF_TOPIC: self.trigger.topic,
mqtt_trigger.CONF_ENCODING: DEFAULT_ENCODING,
mqtt_trigger.CONF_QOS: self.trigger.qos,
}
if self.trigger.payload:
mqtt_config[CONF_PAYLOAD] = self.trigger.payload
+ if self.trigger.value_template:
+ mqtt_config[CONF_VALUE_TEMPLATE] = self.trigger.value_template
+ mqtt_config = mqtt_trigger.TRIGGER_SCHEMA(mqtt_config)
if self.remove:
self.remove()
@@ -119,7 +127,8 @@ class Trigger:
subtype: str = attr.ib()
topic: str = attr.ib()
type: str = attr.ib()
- trigger_instances: List[TriggerInstance] = attr.ib(factory=list)
+ value_template: str = attr.ib()
+ trigger_instances: list[TriggerInstance] = attr.ib(factory=list)
async def add_trigger(self, action, automation_info):
"""Add MQTT trigger."""
@@ -151,6 +160,7 @@ async def update_trigger(self, config, discovery_hash, remove_signal):
self.qos = config[CONF_QOS]
topic_changed = self.topic != config[CONF_TOPIC]
self.topic = config[CONF_TOPIC]
+ self.value_template = config[CONF_VALUE_TEMPLATE]
# Unsubscribe+subscribe if this trigger is in use and topic has changed
# If topic is same unsubscribe+subscribe will execute in the wrong order
@@ -243,6 +253,7 @@ async def discovery_update(payload):
payload=config[CONF_PAYLOAD],
qos=config[CONF_QOS],
remove_signal=remove_signal,
+ value_template=config[CONF_VALUE_TEMPLATE],
)
else:
await hass.data[DEVICE_TRIGGERS][discovery_id].update_trigger(
@@ -276,7 +287,7 @@ async def async_device_removed(hass: HomeAssistant, device_id: str):
)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers for MQTT devices."""
triggers = []
@@ -308,7 +319,6 @@ async def async_attach_trigger(
"""Attach a trigger."""
if DEVICE_TRIGGERS not in hass.data:
hass.data[DEVICE_TRIGGERS] = {}
- config = TRIGGER_SCHEMA(config)
device_id = config[CONF_DEVICE_ID]
discovery_id = config[CONF_DISCOVERY_ID]
@@ -323,6 +333,7 @@ async def async_attach_trigger(
topic=None,
payload=None,
qos=None,
+ value_template=None,
)
return await hass.data[DEVICE_TRIGGERS][discovery_id].add_trigger(
action, automation_info
diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py
index f96180b098240f..395480a041d9f2 100644
--- a/homeassistant/components/mqtt/fan.py
+++ b/homeassistant/components/mqtt/fan.py
@@ -6,28 +6,37 @@
from homeassistant.components import fan
from homeassistant.components.fan import (
+ ATTR_OSCILLATING,
+ ATTR_PERCENTAGE,
+ ATTR_PRESET_MODE,
ATTR_SPEED,
SPEED_HIGH,
SPEED_LOW,
SPEED_MEDIUM,
SPEED_OFF,
SUPPORT_OSCILLATE,
+ SUPPORT_PRESET_MODE,
SUPPORT_SET_SPEED,
FanEntity,
+ speed_list_without_preset_modes,
)
from homeassistant.const import (
- CONF_DEVICE,
CONF_NAME,
CONF_OPTIMISTIC,
CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON,
CONF_STATE,
- CONF_UNIQUE_ID,
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+from homeassistant.util.percentage import (
+ ordered_list_item_to_percentage,
+ percentage_to_ordered_list_item,
+ percentage_to_ranged_value,
+ ranged_value_to_percentage,
+)
from . import (
CONF_COMMAND_TOPIC,
@@ -40,23 +49,28 @@
)
from .. import mqtt
from .debug_info import log_messages
-from .mixins import (
- MQTT_AVAILABILITY_SCHEMA,
- MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- MQTT_JSON_ATTRS_SCHEMA,
- MqttEntity,
- async_setup_entry_helper,
-)
-
-_LOGGER = logging.getLogger(__name__)
+from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper
CONF_STATE_VALUE_TEMPLATE = "state_value_template"
+CONF_COMMAND_TEMPLATE = "command_template"
+CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic"
+CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic"
+CONF_PERCENTAGE_VALUE_TEMPLATE = "percentage_value_template"
+CONF_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template"
+CONF_SPEED_RANGE_MIN = "speed_range_min"
+CONF_SPEED_RANGE_MAX = "speed_range_max"
+CONF_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic"
+CONF_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic"
+CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template"
+CONF_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template"
+CONF_PRESET_MODES_LIST = "preset_modes"
CONF_SPEED_STATE_TOPIC = "speed_state_topic"
CONF_SPEED_COMMAND_TOPIC = "speed_command_topic"
CONF_SPEED_VALUE_TEMPLATE = "speed_value_template"
CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic"
CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic"
CONF_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template"
+CONF_OSCILLATION_COMMAND_TEMPLATE = "oscillation_command_template"
CONF_PAYLOAD_OSCILLATION_ON = "payload_oscillation_on"
CONF_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off"
CONF_PAYLOAD_OFF_SPEED = "payload_off_speed"
@@ -69,21 +83,74 @@
DEFAULT_PAYLOAD_ON = "ON"
DEFAULT_PAYLOAD_OFF = "OFF"
DEFAULT_OPTIMISTIC = False
+DEFAULT_SPEED_RANGE_MIN = 1
+DEFAULT_SPEED_RANGE_MAX = 100
OSCILLATE_ON_PAYLOAD = "oscillate_on"
OSCILLATE_OFF_PAYLOAD = "oscillate_off"
-OSCILLATION = "oscillation"
-PLATFORM_SCHEMA = (
+_LOGGER = logging.getLogger(__name__)
+
+
+def valid_fan_speed_configuration(config):
+ """Validate that the fan speed configuration is valid, throws if it isn't."""
+ if config.get(CONF_SPEED_COMMAND_TOPIC) and not speed_list_without_preset_modes(
+ config.get(CONF_SPEED_LIST)
+ ):
+ raise ValueError("No valid speeds configured")
+ return config
+
+
+def valid_speed_range_configuration(config):
+ """Validate that the fan speed_range configuration is valid, throws if it isn't."""
+ if config.get(CONF_SPEED_RANGE_MIN) == 0:
+ raise ValueError("speed_range_min must be > 0")
+ if config.get(CONF_SPEED_RANGE_MIN) >= config.get(CONF_SPEED_RANGE_MAX):
+ raise ValueError("speed_range_max must be > speed_range_min")
+ return config
+
+
+PLATFORM_SCHEMA = vol.All(
+ # CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, CONF_STATE_VALUE_TEMPLATE, CONF_SPEED_LIST and
+ # Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF,
+ # are deprecated, support will be removed after a quarter (2021.7)
+ cv.deprecated(CONF_PAYLOAD_HIGH_SPEED),
+ cv.deprecated(CONF_PAYLOAD_LOW_SPEED),
+ cv.deprecated(CONF_PAYLOAD_MEDIUM_SPEED),
+ cv.deprecated(CONF_SPEED_LIST),
+ cv.deprecated(CONF_SPEED_COMMAND_TOPIC),
+ cv.deprecated(CONF_SPEED_STATE_TOPIC),
+ cv.deprecated(CONF_SPEED_VALUE_TEMPLATE),
mqtt.MQTT_RW_PLATFORM_SCHEMA.extend(
{
- vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
+ vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_OSCILLATION_COMMAND_TOPIC): mqtt.valid_publish_topic,
+ vol.Optional(CONF_OSCILLATION_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_OSCILLATION_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_OSCILLATION_VALUE_TEMPLATE): cv.template,
+ vol.Optional(CONF_PERCENTAGE_COMMAND_TOPIC): mqtt.valid_publish_topic,
+ vol.Optional(CONF_PERCENTAGE_COMMAND_TEMPLATE): cv.template,
+ vol.Optional(CONF_PERCENTAGE_STATE_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_PERCENTAGE_VALUE_TEMPLATE): cv.template,
+ # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together
+ vol.Inclusive(
+ CONF_PRESET_MODE_COMMAND_TOPIC, "preset_modes"
+ ): mqtt.valid_publish_topic,
+ vol.Inclusive(
+ CONF_PRESET_MODES_LIST, "preset_modes", default=[]
+ ): cv.ensure_list,
+ vol.Optional(CONF_PRESET_MODE_COMMAND_TEMPLATE): cv.template,
+ vol.Optional(CONF_PRESET_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_PRESET_MODE_VALUE_TEMPLATE): cv.template,
+ vol.Optional(
+ CONF_SPEED_RANGE_MIN, default=DEFAULT_SPEED_RANGE_MIN
+ ): cv.positive_int,
+ vol.Optional(
+ CONF_SPEED_RANGE_MAX, default=DEFAULT_SPEED_RANGE_MAX
+ ): cv.positive_int,
vol.Optional(CONF_PAYLOAD_HIGH_SPEED, default=SPEED_HIGH): cv.string,
vol.Optional(CONF_PAYLOAD_LOW_SPEED, default=SPEED_LOW): cv.string,
vol.Optional(CONF_PAYLOAD_MEDIUM_SPEED, default=SPEED_MEDIUM): cv.string,
@@ -104,11 +171,10 @@
vol.Optional(CONF_SPEED_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_SPEED_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template,
- vol.Optional(CONF_UNIQUE_ID): cv.string,
}
- )
- .extend(MQTT_AVAILABILITY_SCHEMA.schema)
- .extend(MQTT_JSON_ATTRS_SCHEMA.schema)
+ ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema),
+ valid_fan_speed_configuration,
+ valid_speed_range_configuration,
)
@@ -122,7 +188,6 @@ async def async_setup_platform(
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT fan dynamically through MQTT discovery."""
-
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
@@ -142,15 +207,21 @@ class MqttFan(MqttEntity, FanEntity):
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the MQTT fan."""
self._state = False
+ # self._speed will be removed after a quarter (2021.7)
self._speed = None
+ self._percentage = None
+ self._preset_mode = None
self._oscillation = None
self._supported_features = 0
self._topic = None
self._payload = None
- self._templates = None
+ self._value_templates = None
+ self._command_templates = None
self._optimistic = None
self._optimistic_oscillation = None
+ self._optimistic_percentage = None
+ self._optimistic_preset_mode = None
self._optimistic_speed = None
MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
@@ -162,38 +233,91 @@ def config_schema():
def _setup_from_config(self, config):
"""(Re)Setup the entity."""
- self._config = config
+ self._speed_range = (
+ config.get(CONF_SPEED_RANGE_MIN),
+ config.get(CONF_SPEED_RANGE_MAX),
+ )
self._topic = {
key: config.get(key)
for key in (
CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC,
+ CONF_PERCENTAGE_STATE_TOPIC,
+ CONF_PERCENTAGE_COMMAND_TOPIC,
+ CONF_PRESET_MODE_STATE_TOPIC,
+ CONF_PRESET_MODE_COMMAND_TOPIC,
CONF_SPEED_STATE_TOPIC,
CONF_SPEED_COMMAND_TOPIC,
CONF_OSCILLATION_STATE_TOPIC,
CONF_OSCILLATION_COMMAND_TOPIC,
)
}
- self._templates = {
+ self._value_templates = {
CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
+ ATTR_PERCENTAGE: config.get(CONF_PERCENTAGE_VALUE_TEMPLATE),
+ ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_VALUE_TEMPLATE),
+ # ATTR_SPEED is deprecated in the schema, support will be removed after a quarter (2021.7)
ATTR_SPEED: config.get(CONF_SPEED_VALUE_TEMPLATE),
- OSCILLATION: config.get(CONF_OSCILLATION_VALUE_TEMPLATE),
+ ATTR_OSCILLATING: config.get(CONF_OSCILLATION_VALUE_TEMPLATE),
+ }
+ self._command_templates = {
+ CONF_STATE: config.get(CONF_COMMAND_TEMPLATE),
+ ATTR_PERCENTAGE: config.get(CONF_PERCENTAGE_COMMAND_TEMPLATE),
+ ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_COMMAND_TEMPLATE),
+ ATTR_OSCILLATING: config.get(CONF_OSCILLATION_COMMAND_TEMPLATE),
}
self._payload = {
"STATE_ON": config[CONF_PAYLOAD_ON],
"STATE_OFF": config[CONF_PAYLOAD_OFF],
"OSCILLATE_ON_PAYLOAD": config[CONF_PAYLOAD_OSCILLATION_ON],
"OSCILLATE_OFF_PAYLOAD": config[CONF_PAYLOAD_OSCILLATION_OFF],
+ # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7)
"SPEED_LOW": config[CONF_PAYLOAD_LOW_SPEED],
"SPEED_MEDIUM": config[CONF_PAYLOAD_MEDIUM_SPEED],
"SPEED_HIGH": config[CONF_PAYLOAD_HIGH_SPEED],
"SPEED_OFF": config[CONF_PAYLOAD_OFF_SPEED],
}
+ # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7)
+ self._feature_legacy_speeds = not self._topic[CONF_SPEED_COMMAND_TOPIC] is None
+ if self._feature_legacy_speeds:
+ self._legacy_speeds_list = config[CONF_SPEED_LIST]
+ self._legacy_speeds_list_no_off = speed_list_without_preset_modes(
+ self._legacy_speeds_list
+ )
+ else:
+ self._legacy_speeds_list = []
+
+ self._feature_percentage = CONF_PERCENTAGE_COMMAND_TOPIC in config
+ self._feature_preset_mode = CONF_PRESET_MODE_COMMAND_TOPIC in config
+ if self._feature_preset_mode:
+ self._speeds_list = speed_list_without_preset_modes(
+ self._legacy_speeds_list + config[CONF_PRESET_MODES_LIST]
+ )
+ self._preset_modes = (
+ self._legacy_speeds_list + config[CONF_PRESET_MODES_LIST]
+ )
+ else:
+ self._speeds_list = speed_list_without_preset_modes(
+ self._legacy_speeds_list
+ )
+ self._preset_modes = []
+
+ if not self._speeds_list or self._feature_percentage:
+ self._speed_count = 100
+ else:
+ self._speed_count = len(self._speeds_list)
+
optimistic = config[CONF_OPTIMISTIC]
self._optimistic = optimistic or self._topic[CONF_STATE_TOPIC] is None
self._optimistic_oscillation = (
optimistic or self._topic[CONF_OSCILLATION_STATE_TOPIC] is None
)
+ self._optimistic_percentage = (
+ optimistic or self._topic[CONF_PERCENTAGE_STATE_TOPIC] is None
+ )
+ self._optimistic_preset_mode = (
+ optimistic or self._topic[CONF_PRESET_MODE_STATE_TOPIC] is None
+ )
self._optimistic_speed = (
optimistic or self._topic[CONF_SPEED_STATE_TOPIC] is None
)
@@ -203,16 +327,22 @@ def _setup_from_config(self, config):
self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None
and SUPPORT_OSCILLATE
)
- self._supported_features |= (
- self._topic[CONF_SPEED_COMMAND_TOPIC] is not None and SUPPORT_SET_SPEED
- )
-
- for key, tpl in list(self._templates.items()):
- if tpl is None:
- self._templates[key] = lambda value: value
- else:
- tpl.hass = self.hass
- self._templates[key] = tpl.async_render_with_possible_json_value
+ if self._feature_preset_mode and self._speeds_list:
+ self._supported_features |= SUPPORT_SET_SPEED
+ if self._feature_percentage:
+ self._supported_features |= SUPPORT_SET_SPEED
+ if self._feature_legacy_speeds:
+ self._supported_features |= SUPPORT_SET_SPEED
+ if self._feature_preset_mode:
+ self._supported_features |= SUPPORT_PRESET_MODE
+
+ for tpl_dict in [self._command_templates, self._value_templates]:
+ for key, tpl in tpl_dict.items():
+ if tpl is None:
+ tpl_dict[key] = lambda value: value
+ else:
+ tpl.hass = self.hass
+ tpl_dict[key] = tpl.async_render_with_possible_json_value
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
@@ -222,7 +352,7 @@ async def _subscribe_topics(self):
@log_messages(self.hass, self.entity_id)
def state_received(msg):
"""Handle new received MQTT message."""
- payload = self._templates[CONF_STATE](msg.payload)
+ payload = self._value_templates[CONF_STATE](msg.payload)
if payload == self._payload["STATE_ON"]:
self._state = True
elif payload == self._payload["STATE_OFF"]:
@@ -236,19 +366,103 @@ def state_received(msg):
"qos": self._config[CONF_QOS],
}
+ @callback
+ @log_messages(self.hass, self.entity_id)
+ def percentage_received(msg):
+ """Handle new received MQTT message for the percentage."""
+ numeric_val_str = self._value_templates[ATTR_PERCENTAGE](msg.payload)
+ try:
+ percentage = ranged_value_to_percentage(
+ self._speed_range, int(numeric_val_str)
+ )
+ except ValueError:
+ _LOGGER.warning(
+ "'%s' received on topic %s is not a valid speed within the speed range",
+ msg.payload,
+ msg.topic,
+ )
+ return
+ if percentage < 0 or percentage > 100:
+ _LOGGER.warning(
+ "'%s' received on topic %s is not a valid speed within the speed range",
+ msg.payload,
+ msg.topic,
+ )
+ return
+ self._percentage = percentage
+ self.async_write_ha_state()
+
+ if self._topic[CONF_PERCENTAGE_STATE_TOPIC] is not None:
+ topics[CONF_PERCENTAGE_STATE_TOPIC] = {
+ "topic": self._topic[CONF_PERCENTAGE_STATE_TOPIC],
+ "msg_callback": percentage_received,
+ "qos": self._config[CONF_QOS],
+ }
+ self._percentage = None
+
+ @callback
+ @log_messages(self.hass, self.entity_id)
+ def preset_mode_received(msg):
+ """Handle new received MQTT message for preset mode."""
+ preset_mode = self._value_templates[ATTR_PRESET_MODE](msg.payload)
+ if preset_mode not in self.preset_modes:
+ _LOGGER.warning(
+ "'%s' received on topic %s is not a valid preset mode",
+ msg.payload,
+ msg.topic,
+ )
+ return
+
+ self._preset_mode = preset_mode
+ if not self._implemented_percentage and (preset_mode in self.speed_list):
+ self._percentage = ordered_list_item_to_percentage(
+ self.speed_list, preset_mode
+ )
+ self.async_write_ha_state()
+
+ if self._topic[CONF_PRESET_MODE_STATE_TOPIC] is not None:
+ topics[CONF_PRESET_MODE_STATE_TOPIC] = {
+ "topic": self._topic[CONF_PRESET_MODE_STATE_TOPIC],
+ "msg_callback": preset_mode_received,
+ "qos": self._config[CONF_QOS],
+ }
+ self._preset_mode = None
+
+ # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7)
@callback
@log_messages(self.hass, self.entity_id)
def speed_received(msg):
"""Handle new received MQTT message for the speed."""
- payload = self._templates[ATTR_SPEED](msg.payload)
- if payload == self._payload["SPEED_LOW"]:
- self._speed = SPEED_LOW
- elif payload == self._payload["SPEED_MEDIUM"]:
- self._speed = SPEED_MEDIUM
- elif payload == self._payload["SPEED_HIGH"]:
- self._speed = SPEED_HIGH
- elif payload == self._payload["SPEED_OFF"]:
- self._speed = SPEED_OFF
+ speed_payload = self._value_templates[ATTR_SPEED](msg.payload)
+ if speed_payload == self._payload["SPEED_LOW"]:
+ speed = SPEED_LOW
+ elif speed_payload == self._payload["SPEED_MEDIUM"]:
+ speed = SPEED_MEDIUM
+ elif speed_payload == self._payload["SPEED_HIGH"]:
+ speed = SPEED_HIGH
+ elif speed_payload == self._payload["SPEED_OFF"]:
+ speed = SPEED_OFF
+ else:
+ speed = None
+
+ if speed and speed in self._legacy_speeds_list:
+ self._speed = speed
+ else:
+ _LOGGER.warning(
+ "'%s' received on topic %s is not a valid speed",
+ msg.payload,
+ msg.topic,
+ )
+ return
+
+ if not self._implemented_percentage:
+ if speed in self._speeds_list:
+ self._percentage = ordered_list_item_to_percentage(
+ self._speeds_list, speed
+ )
+ elif speed == SPEED_OFF:
+ self._percentage = 0
+
self.async_write_ha_state()
if self._topic[CONF_SPEED_STATE_TOPIC] is not None:
@@ -263,7 +477,7 @@ def speed_received(msg):
@log_messages(self.hass, self.entity_id)
def oscillation_received(msg):
"""Handle new received MQTT message for the oscillation."""
- payload = self._templates[OSCILLATION](msg.payload)
+ payload = self._value_templates[ATTR_OSCILLATING](msg.payload)
if payload == self._payload["OSCILLATE_ON_PAYLOAD"]:
self._oscillation = True
elif payload == self._payload["OSCILLATE_OFF_PAYLOAD"]:
@@ -293,14 +507,41 @@ def is_on(self):
return self._state
@property
- def name(self) -> str:
- """Get entity name."""
- return self._config[CONF_NAME]
+ def _implemented_percentage(self):
+ """Return true if percentage has been implemented."""
+ return self._feature_percentage
+
+ @property
+ def _implemented_preset_mode(self):
+ """Return true if preset_mode has been implemented."""
+ return self._feature_preset_mode
+
+ # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7)
+ @property
+ def _implemented_speed(self):
+ """Return true if speed has been implemented."""
+ return self._feature_legacy_speeds
+
+ @property
+ def percentage(self):
+ """Return the current percentage."""
+ return self._percentage
+ @property
+ def preset_mode(self):
+ """Return the current preset _mode."""
+ return self._preset_mode
+
+ @property
+ def preset_modes(self) -> list:
+ """Get the list of available preset modes."""
+ return self._preset_modes
+
+ # The speed_list property is deprecated in the schema, support will be removed after a quarter (2021.7)
@property
def speed_list(self) -> list:
"""Get the list of available speeds."""
- return self._config[CONF_SPEED_LIST]
+ return self._speeds_list
@property
def supported_features(self) -> int:
@@ -312,18 +553,17 @@ def speed(self):
"""Return the current speed."""
return self._speed
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports or 100 if percentage is supported."""
+ return self._speed_count
+
@property
def oscillating(self):
"""Return the oscillation state."""
return self._oscillation
- #
- # The fan entity model has changed to use percentages and preset_modes
- # instead of speeds.
- #
- # Please review
- # https://developers.home-assistant.io/docs/core/entity/fan/
- #
+ # The speed attribute deprecated in the schema, support will be removed after a quarter (2021.7)
async def async_turn_on(
self,
speed: str = None,
@@ -335,14 +575,20 @@ async def async_turn_on(
This method is a coroutine.
"""
+ mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_ON"])
mqtt.async_publish(
self.hass,
self._topic[CONF_COMMAND_TOPIC],
- self._payload["STATE_ON"],
+ mqtt_payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
)
- if speed:
+ if percentage:
+ await self.async_set_percentage(percentage)
+ if preset_mode:
+ await self.async_set_preset_mode(preset_mode)
+ # The speed attribute deprecated in the schema, support will be removed after a quarter (2021.7)
+ if speed and not percentage and not preset_mode:
await self.async_set_speed(speed)
if self._optimistic:
self._state = True
@@ -353,10 +599,11 @@ async def async_turn_off(self, **kwargs) -> None:
This method is a coroutine.
"""
+ mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_OFF"])
mqtt.async_publish(
self.hass,
self._topic[CONF_COMMAND_TOPIC],
- self._payload["STATE_OFF"],
+ mqtt_payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
)
@@ -364,32 +611,112 @@ async def async_turn_off(self, **kwargs) -> None:
self._state = False
self.async_write_ha_state()
- async def async_set_speed(self, speed: str) -> None:
- """Set the speed of the fan.
+ async def async_set_percentage(self, percentage: int) -> None:
+ """Set the percentage of the fan.
This method is a coroutine.
"""
- if speed == SPEED_LOW:
- mqtt_payload = self._payload["SPEED_LOW"]
- elif speed == SPEED_MEDIUM:
- mqtt_payload = self._payload["SPEED_MEDIUM"]
- elif speed == SPEED_HIGH:
- mqtt_payload = self._payload["SPEED_HIGH"]
- elif speed == SPEED_OFF:
- mqtt_payload = self._payload["SPEED_OFF"]
- else:
- raise ValueError(f"{speed} is not a valid fan speed")
+ percentage_payload = int(
+ percentage_to_ranged_value(self._speed_range, percentage)
+ )
+ mqtt_payload = self._command_templates[ATTR_PERCENTAGE](percentage_payload)
+ if self._implemented_preset_mode:
+ if percentage:
+ await self.async_set_preset_mode(
+ preset_mode=percentage_to_ordered_list_item(
+ self.speed_list, percentage
+ )
+ )
+ # Legacy are deprecated in the schema, support will be removed after a quarter (2021.7)
+ elif self._feature_legacy_speeds and (
+ SPEED_OFF in self._legacy_speeds_list
+ ):
+ await self.async_set_preset_mode(SPEED_OFF)
+ # Legacy are deprecated in the schema, support will be removed after a quarter (2021.7)
+ elif self._feature_legacy_speeds:
+ if percentage:
+ await self.async_set_speed(
+ percentage_to_ordered_list_item(
+ self._legacy_speeds_list_no_off,
+ percentage,
+ )
+ )
+ elif SPEED_OFF in self._legacy_speeds_list:
+ await self.async_set_speed(SPEED_OFF)
+
+ if self._implemented_percentage:
+ mqtt.async_publish(
+ self.hass,
+ self._topic[CONF_PERCENTAGE_COMMAND_TOPIC],
+ mqtt_payload,
+ self._config[CONF_QOS],
+ self._config[CONF_RETAIN],
+ )
+
+ if self._optimistic_percentage:
+ self._percentage = percentage
+ self.async_write_ha_state()
+
+ async def async_set_preset_mode(self, preset_mode: str) -> None:
+ """Set the preset mode of the fan.
+
+ This method is a coroutine.
+ """
+ if preset_mode not in self.preset_modes:
+ _LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
+ return
+ # Legacy are deprecated in the schema, support will be removed after a quarter (2021.7)
+ if preset_mode in self._legacy_speeds_list:
+ await self.async_set_speed(speed=preset_mode)
+ if not self._implemented_percentage and preset_mode in self.speed_list:
+ self._percentage = ordered_list_item_to_percentage(
+ self.speed_list, preset_mode
+ )
+ mqtt_payload = self._command_templates[ATTR_PRESET_MODE](preset_mode)
mqtt.async_publish(
self.hass,
- self._topic[CONF_SPEED_COMMAND_TOPIC],
+ self._topic[CONF_PRESET_MODE_COMMAND_TOPIC],
mqtt_payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
)
- if self._optimistic_speed:
- self._speed = speed
+ if self._optimistic_preset_mode:
+ self._preset_mode = preset_mode
+ self.async_write_ha_state()
+
+ # async_set_speed is deprecated, support will be removed after a quarter (2021.7)
+ async def async_set_speed(self, speed: str) -> None:
+ """Set the speed of the fan.
+
+ This method is a coroutine.
+ """
+ speed_payload = None
+ if self._feature_legacy_speeds:
+ if speed == SPEED_LOW:
+ speed_payload = self._payload["SPEED_LOW"]
+ elif speed == SPEED_MEDIUM:
+ speed_payload = self._payload["SPEED_MEDIUM"]
+ elif speed == SPEED_HIGH:
+ speed_payload = self._payload["SPEED_HIGH"]
+ elif speed == SPEED_OFF:
+ speed_payload = self._payload["SPEED_OFF"]
+ else:
+ _LOGGER.warning("'%s'is not a valid speed", speed)
+ return
+
+ if speed_payload:
+ mqtt.async_publish(
+ self.hass,
+ self._topic[CONF_SPEED_COMMAND_TOPIC],
+ speed_payload,
+ self._config[CONF_QOS],
+ self._config[CONF_RETAIN],
+ )
+
+ if self._optimistic_speed and speed_payload:
+ self._speed = speed
self.async_write_ha_state()
async def async_oscillate(self, oscillating: bool) -> None:
@@ -397,15 +724,19 @@ async def async_oscillate(self, oscillating: bool) -> None:
This method is a coroutine.
"""
- if oscillating is False:
- payload = self._payload["OSCILLATE_OFF_PAYLOAD"]
+ if oscillating:
+ mqtt_payload = self._command_templates[ATTR_OSCILLATING](
+ self._payload["OSCILLATE_ON_PAYLOAD"]
+ )
else:
- payload = self._payload["OSCILLATE_ON_PAYLOAD"]
+ mqtt_payload = self._command_templates[ATTR_OSCILLATING](
+ self._payload["OSCILLATE_OFF_PAYLOAD"]
+ )
mqtt.async_publish(
self.hass,
self._topic[CONF_OSCILLATION_COMMAND_TOPIC],
- payload,
+ mqtt_payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
)
diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py
index e780332d093c43..95a0cde52f4551 100644
--- a/homeassistant/components/mqtt/light/__init__.py
+++ b/homeassistant/components/mqtt/light/__init__.py
@@ -1,12 +1,12 @@
"""Support for MQTT lights."""
import functools
-import logging
import voluptuous as vol
from homeassistant.components import light
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.reload import async_setup_reload_service
-from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+from homeassistant.helpers.typing import ConfigType
from .. import DOMAIN, PLATFORMS
from ..mixins import async_setup_entry_helper
@@ -15,8 +15,6 @@
from .schema_json import PLATFORM_SCHEMA_JSON, async_setup_entity_json
from .schema_template import PLATFORM_SCHEMA_TEMPLATE, async_setup_entity_template
-_LOGGER = logging.getLogger(__name__)
-
def validate_mqtt_light(value):
"""Validate MQTT light schema."""
@@ -34,7 +32,7 @@ def validate_mqtt_light(value):
async def async_setup_platform(
- hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None
+ hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None
):
"""Set up MQTT light through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
@@ -43,7 +41,6 @@ async def async_setup_platform(
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT light dynamically through MQTT discovery."""
-
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py
index 04f01ea0d3a278..9c4b0f3a3e3a3a 100644
--- a/homeassistant/components/mqtt/light/schema_basic.py
+++ b/homeassistant/components/mqtt/light/schema_basic.py
@@ -17,12 +17,10 @@
LightEntity,
)
from homeassistant.const import (
- CONF_DEVICE,
CONF_NAME,
CONF_OPTIMISTIC,
CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON,
- CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
STATE_ON,
)
@@ -34,12 +32,7 @@
from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription
from ... import mqtt
from ..debug_info import log_messages
-from ..mixins import (
- MQTT_AVAILABILITY_SCHEMA,
- MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- MQTT_JSON_ATTRS_SCHEMA,
- MqttEntity,
-)
+from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity
from .schema import MQTT_LIGHT_SCHEMA_SCHEMA
_LOGGER = logging.getLogger(__name__)
@@ -110,7 +103,6 @@
vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_COLOR_TEMP_VALUE_TEMPLATE): cv.template,
- vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
vol.Optional(CONF_EFFECT_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EFFECT_STATE_TOPIC): mqtt.valid_subscribe_topic,
@@ -132,7 +124,6 @@
vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template,
- vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(
CONF_WHITE_VALUE_SCALE, default=DEFAULT_WHITE_VALUE_SCALE
@@ -144,8 +135,7 @@
vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template,
}
)
- .extend(MQTT_AVAILABILITY_SCHEMA.schema)
- .extend(MQTT_JSON_ATTRS_SCHEMA.schema)
+ .extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
.extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema)
)
@@ -194,8 +184,6 @@ def config_schema():
def _setup_from_config(self, config):
"""(Re)Setup the entity."""
- self._config = config
-
topic = {
key: config.get(key)
for key in (
@@ -540,11 +528,6 @@ def white_value(self):
return white_value
return None
- @property
- def name(self):
- """Return the name of the device if any."""
- return self._config[CONF_NAME]
-
@property
def is_on(self):
"""Return true if device is on."""
@@ -617,9 +600,8 @@ async def async_turn_on(self, **kwargs):
# If brightness is being used instead of an on command, make sure
# there is a brightness input. Either set the brightness to our
# saved value or the maximum value if this is the first call
- elif on_command_type == "brightness":
- if ATTR_BRIGHTNESS not in kwargs:
- kwargs[ATTR_BRIGHTNESS] = self._brightness if self._brightness else 255
+ elif on_command_type == "brightness" and ATTR_BRIGHTNESS not in kwargs:
+ kwargs[ATTR_BRIGHTNESS] = self._brightness if self._brightness else 255
if ATTR_HS_COLOR in kwargs and self._topic[CONF_RGB_COMMAND_TOPIC] is not None:
diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py
index c6622578a6f8e5..aaf12f3362f424 100644
--- a/homeassistant/components/mqtt/light/schema_json.py
+++ b/homeassistant/components/mqtt/light/schema_json.py
@@ -1,4 +1,5 @@
"""Support for MQTT JSON lights."""
+from contextlib import suppress
import json
import logging
@@ -6,12 +7,23 @@
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
+ ATTR_COLOR_MODE,
ATTR_COLOR_TEMP,
ATTR_EFFECT,
ATTR_FLASH,
ATTR_HS_COLOR,
+ ATTR_RGB_COLOR,
+ ATTR_RGBW_COLOR,
+ ATTR_RGBWW_COLOR,
ATTR_TRANSITION,
ATTR_WHITE_VALUE,
+ ATTR_XY_COLOR,
+ COLOR_MODE_COLOR_TEMP,
+ COLOR_MODE_HS,
+ COLOR_MODE_RGB,
+ COLOR_MODE_RGBW,
+ COLOR_MODE_RGBWW,
+ COLOR_MODE_XY,
FLASH_LONG,
FLASH_SHORT,
SUPPORT_BRIGHTNESS,
@@ -21,17 +33,18 @@
SUPPORT_FLASH,
SUPPORT_TRANSITION,
SUPPORT_WHITE_VALUE,
+ VALID_COLOR_MODES,
LightEntity,
+ valid_supported_color_modes,
)
from homeassistant.const import (
CONF_BRIGHTNESS,
CONF_COLOR_TEMP,
- CONF_DEVICE,
CONF_EFFECT,
+ CONF_HS,
CONF_NAME,
CONF_OPTIMISTIC,
CONF_RGB,
- CONF_UNIQUE_ID,
CONF_WHITE_VALUE,
CONF_XY,
STATE_ON,
@@ -45,12 +58,7 @@
from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription
from ... import mqtt
from ..debug_info import log_messages
-from ..mixins import (
- MQTT_AVAILABILITY_SCHEMA,
- MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- MQTT_JSON_ATTRS_SCHEMA,
- MqttEntity,
-)
+from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity
from .schema import MQTT_LIGHT_SCHEMA_SCHEMA
from .schema_basic import CONF_BRIGHTNESS_SCALE
@@ -59,6 +67,7 @@
DOMAIN = "mqtt_json"
DEFAULT_BRIGHTNESS = False
+DEFAULT_COLOR_MODE = False
DEFAULT_COLOR_TEMP = False
DEFAULT_EFFECT = False
DEFAULT_FLASH_TIME_LONG = 10
@@ -71,25 +80,37 @@
DEFAULT_HS = False
DEFAULT_BRIGHTNESS_SCALE = 255
+CONF_COLOR_MODE = "color_mode"
+CONF_SUPPORTED_COLOR_MODES = "supported_color_modes"
+
CONF_EFFECT_LIST = "effect_list"
CONF_FLASH_TIME_LONG = "flash_time_long"
CONF_FLASH_TIME_SHORT = "flash_time_short"
-CONF_HS = "hs"
CONF_MAX_MIREDS = "max_mireds"
CONF_MIN_MIREDS = "min_mireds"
-# Stealing some of these from the base MQTT configs.
-PLATFORM_SCHEMA_JSON = (
+
+def valid_color_configuration(config):
+ """Test color_mode is not combined with deprecated config."""
+ deprecated = {CONF_COLOR_TEMP, CONF_HS, CONF_RGB, CONF_WHITE_VALUE, CONF_XY}
+ if config[CONF_COLOR_MODE] and any(config.get(key) for key in deprecated):
+ raise vol.Invalid(f"color_mode must not be combined with any of {deprecated}")
+ return config
+
+
+PLATFORM_SCHEMA_JSON = vol.All(
mqtt.MQTT_RW_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean,
vol.Optional(
CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE
): vol.All(vol.Coerce(int), vol.Range(min=1)),
+ vol.Inclusive(
+ CONF_COLOR_MODE, "color_mode", default=DEFAULT_COLOR_MODE
+ ): cv.boolean,
vol.Optional(CONF_COLOR_TEMP, default=DEFAULT_COLOR_TEMP): cv.boolean,
- vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
vol.Optional(CONF_EFFECT, default=DEFAULT_EFFECT): cv.boolean,
vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(
@@ -109,14 +130,19 @@
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean,
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
- vol.Optional(CONF_UNIQUE_ID): cv.string,
+ vol.Inclusive(CONF_SUPPORTED_COLOR_MODES, "color_mode"): vol.All(
+ cv.ensure_list,
+ [vol.In(VALID_COLOR_MODES)],
+ vol.Unique(),
+ valid_supported_color_modes,
+ ),
vol.Optional(CONF_WHITE_VALUE, default=DEFAULT_WHITE_VALUE): cv.boolean,
vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean,
- }
+ },
)
- .extend(MQTT_AVAILABILITY_SCHEMA.schema)
- .extend(MQTT_JSON_ATTRS_SCHEMA.schema)
- .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema)
+ .extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
+ .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema),
+ valid_color_configuration,
)
@@ -138,11 +164,16 @@ def __init__(self, config, config_entry, discovery_data):
self._topic = None
self._optimistic = False
self._brightness = None
+ self._color_mode = None
self._color_temp = None
self._effect = None
+ self._flash_times = None
self._hs = None
+ self._rgb = None
+ self._rgbw = None
+ self._rgbww = None
self._white_value = None
- self._flash_times = None
+ self._xy = None
MqttEntity.__init__(self, None, config, config_entry, discovery_data)
@@ -153,8 +184,6 @@ def config_schema():
def _setup_from_config(self, config):
"""(Re)Setup the entity."""
- self._config = config
-
self._topic = {
key: config.get(key) for key in (CONF_STATE_TOPIC, CONF_COMMAND_TOPIC)
}
@@ -167,13 +196,92 @@ def _setup_from_config(self, config):
}
self._supported_features = SUPPORT_TRANSITION | SUPPORT_FLASH
- self._supported_features |= config[CONF_RGB] and SUPPORT_COLOR
- 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_EFFECT] and SUPPORT_EFFECT
- self._supported_features |= config[CONF_WHITE_VALUE] and SUPPORT_WHITE_VALUE
- self._supported_features |= config[CONF_XY] and SUPPORT_COLOR
- self._supported_features |= config[CONF_HS] and SUPPORT_COLOR
+ 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_WHITE_VALUE] and SUPPORT_WHITE_VALUE
+ self._supported_features |= config[CONF_XY] and SUPPORT_COLOR
+
+ def _update_color(self, values):
+ if not self._config[CONF_COLOR_MODE]:
+ # Deprecated color handling
+ try:
+ red = int(values["color"]["r"])
+ green = int(values["color"]["g"])
+ blue = int(values["color"]["b"])
+ self._hs = color_util.color_RGB_to_hs(red, green, blue)
+ except KeyError:
+ pass
+ except ValueError:
+ _LOGGER.warning("Invalid RGB color value received")
+ return
+
+ try:
+ x_color = float(values["color"]["x"])
+ y_color = float(values["color"]["y"])
+ self._hs = color_util.color_xy_to_hs(x_color, y_color)
+ except KeyError:
+ pass
+ except ValueError:
+ _LOGGER.warning("Invalid XY color value received")
+ return
+
+ try:
+ hue = float(values["color"]["h"])
+ saturation = float(values["color"]["s"])
+ self._hs = (hue, saturation)
+ except KeyError:
+ pass
+ except ValueError:
+ _LOGGER.warning("Invalid HS color value received")
+ return
+ else:
+ color_mode = values["color_mode"]
+ if not self._supports_color_mode(color_mode):
+ _LOGGER.warning("Invalid color mode received")
+ return
+ try:
+ if color_mode == COLOR_MODE_COLOR_TEMP:
+ self._color_temp = int(values["color_temp"])
+ self._color_mode = COLOR_MODE_COLOR_TEMP
+ elif color_mode == COLOR_MODE_HS:
+ hue = float(values["color"]["h"])
+ saturation = float(values["color"]["s"])
+ self._color_mode = COLOR_MODE_HS
+ self._hs = (hue, saturation)
+ elif color_mode == COLOR_MODE_RGB:
+ r = int(values["color"]["r"]) # pylint: disable=invalid-name
+ g = int(values["color"]["g"]) # pylint: disable=invalid-name
+ b = int(values["color"]["b"]) # pylint: disable=invalid-name
+ self._color_mode = COLOR_MODE_RGB
+ self._rgb = (r, g, b)
+ elif color_mode == COLOR_MODE_RGBW:
+ r = int(values["color"]["r"]) # pylint: disable=invalid-name
+ g = int(values["color"]["g"]) # pylint: disable=invalid-name
+ b = int(values["color"]["b"]) # pylint: disable=invalid-name
+ w = int(values["color"]["w"]) # pylint: disable=invalid-name
+ self._color_mode = COLOR_MODE_RGBW
+ self._rgbw = (r, g, b, w)
+ elif color_mode == COLOR_MODE_RGBWW:
+ r = int(values["color"]["r"]) # pylint: disable=invalid-name
+ g = int(values["color"]["g"]) # pylint: disable=invalid-name
+ b = int(values["color"]["b"]) # pylint: disable=invalid-name
+ c = int(values["color"]["c"]) # pylint: disable=invalid-name
+ w = int(values["color"]["w"]) # pylint: disable=invalid-name
+ self._color_mode = COLOR_MODE_RGBWW
+ self._rgbww = (r, g, b, c, w)
+ elif color_mode == COLOR_MODE_XY:
+ x = float(values["color"]["x"]) # pylint: disable=invalid-name
+ y = float(values["color"]["y"]) # pylint: disable=invalid-name
+ self._color_mode = COLOR_MODE_XY
+ self._xy = (x, y)
+ except (KeyError, ValueError):
+ _LOGGER.warning("Invalid or incomplete color value received")
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
@@ -190,37 +298,14 @@ def state_received(msg):
elif values["state"] == "OFF":
self._state = False
- if self._supported_features and SUPPORT_COLOR:
- try:
- red = int(values["color"]["r"])
- green = int(values["color"]["g"])
- blue = int(values["color"]["b"])
-
- self._hs = color_util.color_RGB_to_hs(red, green, blue)
- except KeyError:
- pass
- except ValueError:
- _LOGGER.warning("Invalid RGB color value received")
-
- try:
- x_color = float(values["color"]["x"])
- y_color = float(values["color"]["y"])
-
- self._hs = color_util.color_xy_to_hs(x_color, y_color)
- except KeyError:
- pass
- except ValueError:
- _LOGGER.warning("Invalid XY color value received")
-
- try:
- hue = float(values["color"]["h"])
- saturation = float(values["color"]["s"])
+ if self._supported_features and SUPPORT_COLOR and "color" in values:
+ if values["color"] is None:
+ self._hs = None
+ else:
+ self._update_color(values)
- self._hs = (hue, saturation)
- except KeyError:
- pass
- except ValueError:
- _LOGGER.warning("Invalid HS color value received")
+ if self._config[CONF_COLOR_MODE] and "color_mode" in values:
+ self._update_color(values)
if self._supported_features and SUPPORT_BRIGHTNESS:
try:
@@ -234,19 +319,24 @@ def state_received(msg):
except (TypeError, ValueError):
_LOGGER.warning("Invalid brightness value received")
- if self._supported_features and SUPPORT_COLOR_TEMP:
+ if (
+ self._supported_features
+ and SUPPORT_COLOR_TEMP
+ and not self._config[CONF_COLOR_MODE]
+ ):
try:
- self._color_temp = int(values["color_temp"])
+ if values["color_temp"] is None:
+ self._color_temp = None
+ else:
+ self._color_temp = int(values["color_temp"])
except KeyError:
pass
except ValueError:
_LOGGER.warning("Invalid color temp value received")
if self._supported_features and SUPPORT_EFFECT:
- try:
+ with suppress(KeyError):
self._effect = values["effect"]
- except KeyError:
- pass
if self._supported_features and SUPPORT_WHITE_VALUE:
try:
@@ -273,16 +363,17 @@ def state_received(msg):
if self._optimistic and last_state:
self._state = last_state.state == STATE_ON
- if last_state.attributes.get(ATTR_BRIGHTNESS):
- self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS)
- if last_state.attributes.get(ATTR_HS_COLOR):
- self._hs = last_state.attributes.get(ATTR_HS_COLOR)
- if last_state.attributes.get(ATTR_COLOR_TEMP):
- self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP)
- if last_state.attributes.get(ATTR_EFFECT):
- self._effect = last_state.attributes.get(ATTR_EFFECT)
- if last_state.attributes.get(ATTR_WHITE_VALUE):
- self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE)
+ last_attributes = last_state.attributes
+ self._brightness = last_attributes.get(ATTR_BRIGHTNESS, self._brightness)
+ self._color_mode = last_attributes.get(ATTR_COLOR_MODE, self._color_mode)
+ self._color_temp = last_attributes.get(ATTR_COLOR_TEMP, self._color_temp)
+ self._effect = last_attributes.get(ATTR_EFFECT, self._effect)
+ self._hs = last_attributes.get(ATTR_HS_COLOR, self._hs)
+ self._rgb = last_attributes.get(ATTR_RGB_COLOR, self._rgb)
+ self._rgbw = last_attributes.get(ATTR_RGBW_COLOR, self._rgbw)
+ self._rgbww = last_attributes.get(ATTR_RGBWW_COLOR, self._rgbww)
+ self._white_value = last_attributes.get(ATTR_WHITE_VALUE, self._white_value)
+ self._xy = last_attributes.get(ATTR_XY_COLOR, self._xy)
@property
def brightness(self):
@@ -319,16 +410,31 @@ def hs_color(self):
"""Return the hs color value."""
return self._hs
+ @property
+ def rgb_color(self):
+ """Return the hs color value."""
+ return self._rgb
+
+ @property
+ def rgbw_color(self):
+ """Return the hs color value."""
+ return self._rgbw
+
+ @property
+ def rgbww_color(self):
+ """Return the hs color value."""
+ return self._rgbww
+
+ @property
+ def xy_color(self):
+ """Return the hs color value."""
+ return self._xy
+
@property
def white_value(self):
"""Return the white property."""
return self._white_value
- @property
- def name(self):
- """Return the name of the device if any."""
- return self._config[CONF_NAME]
-
@property
def is_on(self):
"""Return true if device is on."""
@@ -339,11 +445,45 @@ def assumed_state(self):
"""Return true if we do optimistic updates."""
return self._optimistic
+ @property
+ def color_mode(self):
+ """Return current color mode."""
+ return self._color_mode
+
+ @property
+ def supported_color_modes(self):
+ """Flag supported color modes."""
+ return self._config.get(CONF_SUPPORTED_COLOR_MODES)
+
@property
def supported_features(self):
"""Flag supported features."""
return self._supported_features
+ def _set_flash_and_transition(self, message, **kwargs):
+ if ATTR_TRANSITION in kwargs:
+ message["transition"] = kwargs[ATTR_TRANSITION]
+
+ if ATTR_FLASH in kwargs:
+ flash = kwargs.get(ATTR_FLASH)
+
+ if flash == FLASH_LONG:
+ message["flash"] = self._flash_times[CONF_FLASH_TIME_LONG]
+ elif flash == FLASH_SHORT:
+ message["flash"] = self._flash_times[CONF_FLASH_TIME_SHORT]
+
+ def _scale_rgbxx(self, rgbxx, kwargs):
+ # If there's a brightness topic set, we don't want to scale the
+ # RGBxx values given using the brightness.
+ if self._config[CONF_BRIGHTNESS]:
+ brightness = 255
+ else:
+ brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
+ return tuple(round(i / 255 * brightness) for i in rgbxx)
+
+ def _supports_color_mode(self, color_mode):
+ return self.supported_color_modes and color_mode in self.supported_color_modes
+
async def async_turn_on(self, **kwargs):
"""Turn the device on.
@@ -383,16 +523,53 @@ async def async_turn_on(self, **kwargs):
self._hs = kwargs[ATTR_HS_COLOR]
should_update = True
- if ATTR_FLASH in kwargs:
- flash = kwargs.get(ATTR_FLASH)
+ if ATTR_HS_COLOR in kwargs and self._supports_color_mode(COLOR_MODE_HS):
+ hs_color = kwargs[ATTR_HS_COLOR]
+ message["color"] = {"h": hs_color[0], "s": hs_color[1]}
+ if self._optimistic:
+ self._color_mode = COLOR_MODE_HS
+ self._hs = hs_color
+ should_update = True
- if flash == FLASH_LONG:
- message["flash"] = self._flash_times[CONF_FLASH_TIME_LONG]
- elif flash == FLASH_SHORT:
- message["flash"] = self._flash_times[CONF_FLASH_TIME_SHORT]
+ if ATTR_RGB_COLOR in kwargs and self._supports_color_mode(COLOR_MODE_RGB):
+ rgb = self._scale_rgbxx(kwargs[ATTR_RGB_COLOR], kwargs)
+ message["color"] = {"r": rgb[0], "g": rgb[1], "b": rgb[2]}
+ if self._optimistic:
+ self._color_mode = COLOR_MODE_RGB
+ self._rgb = rgb
+ should_update = True
- if ATTR_TRANSITION in kwargs:
- message["transition"] = kwargs[ATTR_TRANSITION]
+ if ATTR_RGBW_COLOR in kwargs and self._supports_color_mode(COLOR_MODE_RGBW):
+ rgb = self._scale_rgbxx(kwargs[ATTR_RGBW_COLOR], kwargs)
+ message["color"] = {"r": rgb[0], "g": rgb[1], "b": rgb[2], "w": rgb[3]}
+ if self._optimistic:
+ self._color_mode = COLOR_MODE_RGBW
+ self._rgbw = rgb
+ should_update = True
+
+ if ATTR_RGBWW_COLOR in kwargs and self._supports_color_mode(COLOR_MODE_RGBWW):
+ rgb = self._scale_rgbxx(kwargs[ATTR_RGBWW_COLOR], kwargs)
+ message["color"] = {
+ "r": rgb[0],
+ "g": rgb[1],
+ "b": rgb[2],
+ "c": rgb[3],
+ "w": rgb[4],
+ }
+ if self._optimistic:
+ self._color_mode = COLOR_MODE_RGBWW
+ self._rgbww = rgb
+ should_update = True
+
+ if ATTR_XY_COLOR in kwargs and self._supports_color_mode(COLOR_MODE_XY):
+ xy = kwargs[ATTR_XY_COLOR] # pylint: disable=invalid-name
+ message["color"] = {"x": xy[0], "y": xy[1]}
+ if self._optimistic:
+ self._color_mode = COLOR_MODE_XY
+ self._xy = xy
+ should_update = True
+
+ self._set_flash_and_transition(message, **kwargs)
if ATTR_BRIGHTNESS in kwargs and self._config[CONF_BRIGHTNESS]:
brightness_normalized = kwargs[ATTR_BRIGHTNESS] / DEFAULT_BRIGHTNESS_SCALE
@@ -452,8 +629,7 @@ async def async_turn_off(self, **kwargs):
"""
message = {"state": "OFF"}
- if ATTR_TRANSITION in kwargs:
- message["transition"] = kwargs[ATTR_TRANSITION]
+ self._set_flash_and_transition(message, **kwargs)
mqtt.async_publish(
self.hass,
diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py
index e696e99552e862..7c0266265db5c9 100644
--- a/homeassistant/components/mqtt/light/schema_template.py
+++ b/homeassistant/components/mqtt/light/schema_template.py
@@ -21,10 +21,9 @@
LightEntity,
)
from homeassistant.const import (
- CONF_DEVICE,
CONF_NAME,
CONF_OPTIMISTIC,
- CONF_UNIQUE_ID,
+ CONF_STATE_TEMPLATE,
STATE_OFF,
STATE_ON,
)
@@ -36,12 +35,7 @@
from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription
from ... import mqtt
from ..debug_info import log_messages
-from ..mixins import (
- MQTT_AVAILABILITY_SCHEMA,
- MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- MQTT_JSON_ATTRS_SCHEMA,
- MqttEntity,
-)
+from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity
from .schema import MQTT_LIGHT_SCHEMA_SCHEMA
_LOGGER = logging.getLogger(__name__)
@@ -62,7 +56,6 @@
CONF_MAX_MIREDS = "max_mireds"
CONF_MIN_MIREDS = "min_mireds"
CONF_RED_TEMPLATE = "red_template"
-CONF_STATE_TEMPLATE = "state_template"
CONF_WHITE_VALUE_TEMPLATE = "white_value_template"
PLATFORM_SCHEMA_TEMPLATE = (
@@ -73,7 +66,6 @@
vol.Optional(CONF_COLOR_TEMP_TEMPLATE): cv.template,
vol.Required(CONF_COMMAND_OFF_TEMPLATE): cv.template,
vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template,
- vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EFFECT_TEMPLATE): cv.template,
vol.Optional(CONF_GREEN_TEMPLATE): cv.template,
@@ -83,12 +75,10 @@
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_RED_TEMPLATE): cv.template,
vol.Optional(CONF_STATE_TEMPLATE): cv.template,
- vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template,
}
)
- .extend(MQTT_AVAILABILITY_SCHEMA.schema)
- .extend(MQTT_JSON_ATTRS_SCHEMA.schema)
+ .extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
.extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema)
)
@@ -127,8 +117,6 @@ def config_schema():
def _setup_from_config(self, config):
"""(Re)Setup the entity."""
- self._config = config
-
self._topics = {
key: config.get(key) for key in (CONF_STATE_TOPIC, CONF_COMMAND_TOPIC)
}
@@ -299,11 +287,6 @@ def white_value(self):
"""Return the white property."""
return self._white_value
- @property
- def name(self):
- """Return the name of the entity."""
- return self._config[CONF_NAME]
-
@property
def is_on(self):
"""Return True if entity is on."""
@@ -383,7 +366,7 @@ async def async_turn_on(self, **kwargs):
values["flash"] = kwargs.get(ATTR_FLASH)
if ATTR_TRANSITION in kwargs:
- values["transition"] = int(kwargs[ATTR_TRANSITION])
+ values["transition"] = kwargs[ATTR_TRANSITION]
mqtt.async_publish(
self.hass,
@@ -408,7 +391,7 @@ async def async_turn_off(self, **kwargs):
self._state = False
if ATTR_TRANSITION in kwargs:
- values["transition"] = int(kwargs[ATTR_TRANSITION])
+ values["transition"] = kwargs[ATTR_TRANSITION]
mqtt.async_publish(
self.hass,
@@ -434,7 +417,7 @@ def supported_features(self):
and self._templates[CONF_GREEN_TEMPLATE] is not None
and self._templates[CONF_BLUE_TEMPLATE] is not None
):
- features = features | SUPPORT_COLOR
+ features = features | SUPPORT_COLOR | SUPPORT_BRIGHTNESS
if self._config.get(CONF_EFFECT_LIST) is not None:
features = features | SUPPORT_EFFECT
if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None:
diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py
index b08f8f8bb4397b..cdfa51015480ac 100644
--- a/homeassistant/components/mqtt/lock.py
+++ b/homeassistant/components/mqtt/lock.py
@@ -1,18 +1,11 @@
"""Support for MQTT locks."""
import functools
-import logging
import voluptuous as vol
from homeassistant.components import lock
from homeassistant.components.lock import LockEntity
-from homeassistant.const import (
- CONF_DEVICE,
- CONF_NAME,
- CONF_OPTIMISTIC,
- CONF_UNIQUE_ID,
- CONF_VALUE_TEMPLATE,
-)
+from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import async_setup_reload_service
@@ -29,15 +22,7 @@
)
from .. import mqtt
from .debug_info import log_messages
-from .mixins import (
- MQTT_AVAILABILITY_SCHEMA,
- MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- MQTT_JSON_ATTRS_SCHEMA,
- MqttEntity,
- async_setup_entry_helper,
-)
-
-_LOGGER = logging.getLogger(__name__)
+from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper
CONF_PAYLOAD_LOCK = "payload_lock"
CONF_PAYLOAD_UNLOCK = "payload_unlock"
@@ -52,26 +37,16 @@
DEFAULT_STATE_LOCKED = "LOCKED"
DEFAULT_STATE_UNLOCKED = "UNLOCKED"
-PLATFORM_SCHEMA = (
- mqtt.MQTT_RW_PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
- vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string,
- vol.Optional(
- CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK
- ): cv.string,
- vol.Optional(CONF_STATE_LOCKED, default=DEFAULT_STATE_LOCKED): cv.string,
- vol.Optional(
- CONF_STATE_UNLOCKED, default=DEFAULT_STATE_UNLOCKED
- ): cv.string,
- vol.Optional(CONF_UNIQUE_ID): cv.string,
- }
- )
- .extend(MQTT_AVAILABILITY_SCHEMA.schema)
- .extend(MQTT_JSON_ATTRS_SCHEMA.schema)
-)
+PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
+ vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string,
+ vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string,
+ vol.Optional(CONF_STATE_LOCKED, default=DEFAULT_STATE_LOCKED): cv.string,
+ vol.Optional(CONF_STATE_UNLOCKED, default=DEFAULT_STATE_UNLOCKED): cv.string,
+ }
+).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
async def async_setup_platform(
@@ -84,7 +59,6 @@ async def async_setup_platform(
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT lock dynamically through MQTT discovery."""
-
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
@@ -115,8 +89,6 @@ def config_schema():
def _setup_from_config(self, config):
"""(Re)Setup the entity."""
- self._config = config
-
self._optimistic = config[CONF_OPTIMISTIC]
value_template = self._config.get(CONF_VALUE_TEMPLATE)
@@ -157,11 +129,6 @@ def message_received(msg):
},
)
- @property
- def name(self):
- """Return the name of the lock."""
- return self._config[CONF_NAME]
-
@property
def is_locked(self):
"""Return true if lock is locked."""
diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json
index 4d44090a4e3559..9de3b07184487c 100644
--- a/homeassistant/components/mqtt/manifest.json
+++ b/homeassistant/components/mqtt/manifest.json
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/mqtt",
"requirements": ["paho-mqtt==1.5.1"],
"dependencies": ["http"],
- "codeowners": ["@home-assistant/core", "@emontnemery"]
+ "codeowners": ["@emontnemery"]
}
diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py
index 1ab2054b355f41..9b1c7a9fb219d9 100644
--- a/homeassistant/components/mqtt/mixins.py
+++ b/homeassistant/components/mqtt/mixins.py
@@ -1,12 +1,13 @@
"""MQTT component mixins and helpers."""
+from __future__ import annotations
+
from abc import abstractmethod
import json
import logging
-from typing import Optional
import voluptuous as vol
-from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_UNIQUE_ID
+from homeassistant.const import CONF_DEVICE, CONF_ICON, CONF_NAME, CONF_UNIQUE_ID
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import (
@@ -51,6 +52,7 @@
CONF_AVAILABILITY = "availability"
CONF_AVAILABILITY_MODE = "availability_mode"
CONF_AVAILABILITY_TOPIC = "availability_topic"
+CONF_ENABLED_BY_DEFAULT = "enabled_by_default"
CONF_PAYLOAD_AVAILABLE = "payload_available"
CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available"
CONF_JSON_ATTRS_TOPIC = "json_attributes_topic"
@@ -63,6 +65,7 @@
CONF_SW_VERSION = "sw_version"
CONF_VIA_DEVICE = "via_device"
CONF_DEPRECATED_VIA_HUB = "via_hub"
+CONF_SUGGESTED_AREA = "suggested_area"
MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema(
{
@@ -129,15 +132,20 @@ def validate_device_has_at_least_one_identifier(value: ConfigType) -> ConfigType
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_SW_VERSION): cv.string,
vol.Optional(CONF_VIA_DEVICE): cv.string,
+ vol.Optional(CONF_SUGGESTED_AREA): cv.string,
}
),
validate_device_has_at_least_one_identifier,
)
-MQTT_JSON_ATTRS_SCHEMA = vol.Schema(
+MQTT_ENTITY_COMMON_SCHEMA = MQTT_AVAILABILITY_SCHEMA.extend(
{
+ vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
+ vol.Optional(CONF_ENABLED_BY_DEFAULT, default=True): cv.boolean,
+ vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template,
+ vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)
@@ -226,7 +234,7 @@ async def async_will_remove_from_hass(self):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attributes
@@ -347,7 +355,7 @@ async def cleanup_device_registry(hass, device_id):
if (
device_id
and not hass.helpers.entity_registry.async_entries_for_device(
- entity_registry, device_id, include_disabled_entities=True
+ entity_registry, device_id, include_disabled_entities=False
)
and not await device_trigger.async_get_triggers(hass, device_id)
and not tag.async_has_tags(hass, device_id)
@@ -387,7 +395,7 @@ async def _async_remove_state_and_registry_entry(self) -> None:
entity_registry.async_remove(self.entity_id)
await cleanup_device_registry(self.hass, entity_entry.device_id)
else:
- await self.async_remove()
+ await self.async_remove(force_remove=True)
async def discovery_callback(payload):
"""Handle discovery update."""
@@ -488,13 +496,16 @@ def device_info_from_config(config):
if CONF_VIA_DEVICE in config:
info["via_device"] = (DOMAIN, config[CONF_VIA_DEVICE])
+ if CONF_SUGGESTED_AREA in config:
+ info["suggested_area"] = config[CONF_SUGGESTED_AREA]
+
return info
class MqttEntityDeviceInfo(Entity):
"""Mixin used for mqtt platforms that support the device registry."""
- def __init__(self, device_config: Optional[ConfigType], config_entry=None) -> None:
+ def __init__(self, device_config: ConfigType | None, config_entry=None) -> None:
"""Initialize the device mixin."""
self._device_config = device_config
self._config_entry = config_entry
@@ -527,11 +538,12 @@ class MqttEntity(
def __init__(self, hass, config, config_entry, discovery_data):
"""Init the MQTT Entity."""
self.hass = hass
+ self._config = config
self._unique_id = config.get(CONF_UNIQUE_ID)
self._sub_state = None
# Load config
- self._setup_from_config(config)
+ self._setup_from_config(self._config)
# Initialize mixin classes
MqttAttributes.__init__(self, config)
@@ -547,7 +559,8 @@ async def async_added_to_hass(self):
async def discovery_update(self, discovery_payload):
"""Handle updated discovery message."""
config = self.config_schema()(discovery_payload)
- self._setup_from_config(config)
+ self._config = config
+ self._setup_from_config(self._config)
await self.attributes_discovery_update(config)
await self.availability_discovery_update(config)
await self.device_info_discovery_update(config)
@@ -575,6 +588,21 @@ def _setup_from_config(self, config):
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
+ @property
+ def entity_registry_enabled_default(self) -> bool:
+ """Return if the entity should be enabled when first added to the entity registry."""
+ return self._config[CONF_ENABLED_BY_DEFAULT]
+
+ @property
+ def icon(self):
+ """Return icon of the entity if any."""
+ return self._config.get(CONF_ICON)
+
+ @property
+ def name(self):
+ """Return the name of the device if any."""
+ return self._config.get(CONF_NAME)
+
@property
def should_poll(self):
"""No polling needed."""
diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py
index 202f457372c10c..7cdafeef98de86 100644
--- a/homeassistant/components/mqtt/models.py
+++ b/homeassistant/components/mqtt/models.py
@@ -1,6 +1,8 @@
"""Modesl used by multiple MQTT modules."""
+from __future__ import annotations
+
import datetime as dt
-from typing import Callable, Optional, Union
+from typing import Callable, Union
import attr
@@ -15,8 +17,8 @@ class Message:
payload: PublishPayloadType = attr.ib()
qos: int = attr.ib()
retain: bool = attr.ib()
- subscribed_topic: Optional[str] = attr.ib(default=None)
- timestamp: Optional[dt.datetime] = attr.ib(default=None)
+ subscribed_topic: str | None = attr.ib(default=None)
+ timestamp: dt.datetime | None = attr.ib(default=None)
MessageCallbackType = Callable[[Message], None]
diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py
index 159f466f7ae801..e7839f8e483bdf 100644
--- a/homeassistant/components/mqtt/number.py
+++ b/homeassistant/components/mqtt/number.py
@@ -6,13 +6,7 @@
from homeassistant.components import number
from homeassistant.components.number import NumberEntity
-from homeassistant.const import (
- CONF_DEVICE,
- CONF_ICON,
- CONF_NAME,
- CONF_OPTIMISTIC,
- CONF_UNIQUE_ID,
-)
+from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.reload import async_setup_reload_service
@@ -28,33 +22,21 @@
subscription,
)
from .. import mqtt
+from .const import CONF_RETAIN
from .debug_info import log_messages
-from .mixins import (
- MQTT_AVAILABILITY_SCHEMA,
- MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- MQTT_JSON_ATTRS_SCHEMA,
- MqttEntity,
- async_setup_entry_helper,
-)
+from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "MQTT Number"
DEFAULT_OPTIMISTIC = False
-PLATFORM_SCHEMA = (
- mqtt.MQTT_RW_PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- vol.Optional(CONF_ICON): cv.icon,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
- vol.Optional(CONF_UNIQUE_ID): cv.string,
- }
- )
- .extend(MQTT_AVAILABILITY_SCHEMA.schema)
- .extend(MQTT_JSON_ATTRS_SCHEMA.schema)
-)
+PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
+ }
+).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
async def async_setup_platform(
@@ -67,7 +49,6 @@ async def async_setup_platform(
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT number dynamically through MQTT discovery."""
-
setup = functools.partial(
_async_setup_entity, async_add_entities, config_entry=config_entry
)
@@ -90,7 +71,6 @@ def __init__(self, config, config_entry, discovery_data):
self._current_number = None
self._optimistic = config.get(CONF_OPTIMISTIC)
- self._unique_id = config.get(CONF_UNIQUE_ID)
NumberEntity.__init__(self)
MqttEntity.__init__(self, None, config, config_entry, discovery_data)
@@ -100,9 +80,6 @@ def config_schema():
"""Return the config schema."""
return PLATFORM_SCHEMA
- def _setup_from_config(self, config):
- self._config = config
-
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
@@ -110,7 +87,6 @@ async def _subscribe_topics(self):
@log_messages(self.hass, self.entity_id)
def message_received(msg):
"""Handle new MQTT messages."""
-
try:
if msg.payload.decode("utf-8").isnumeric():
self._current_number = int(msg.payload)
@@ -149,7 +125,6 @@ def value(self):
async def async_set_value(self, value: float) -> None:
"""Update the current value."""
-
current_number = value
if value.is_integer():
@@ -164,19 +139,10 @@ async def async_set_value(self, value: float) -> None:
self._config[CONF_COMMAND_TOPIC],
current_number,
self._config[CONF_QOS],
+ self._config[CONF_RETAIN],
)
- @property
- def name(self):
- """Return the name of this number."""
- return self._config[CONF_NAME]
-
@property
def assumed_state(self):
"""Return true if we do optimistic updates."""
return self._optimistic
-
- @property
- def icon(self):
- """Return the icon."""
- return self._config.get(CONF_ICON)
diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py
index 908f4bafd30b86..c6d9140af616a0 100644
--- a/homeassistant/components/mqtt/scene.py
+++ b/homeassistant/components/mqtt/scene.py
@@ -1,6 +1,5 @@
"""Support for MQTT scenes."""
import functools
-import logging
import voluptuous as vol
@@ -20,8 +19,6 @@
async_setup_entry_helper,
)
-_LOGGER = logging.getLogger(__name__)
-
DEFAULT_NAME = "MQTT Scene"
DEFAULT_RETAIN = False
@@ -47,7 +44,6 @@ async def async_setup_platform(
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT scene dynamically through MQTT discovery."""
-
setup = functools.partial(
_async_setup_entity, async_add_entities, config_entry=config_entry
)
diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py
index 3f79ab1bafe664..65c9e0550e0ba2 100644
--- a/homeassistant/components/mqtt/sensor.py
+++ b/homeassistant/components/mqtt/sensor.py
@@ -1,26 +1,22 @@
"""Support for MQTT sensors."""
+from __future__ import annotations
+
from datetime import timedelta
import functools
-import logging
-from typing import Optional
import voluptuous as vol
from homeassistant.components import sensor
-from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA
+from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, SensorEntity
from homeassistant.const import (
- CONF_DEVICE,
CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE,
- CONF_ICON,
CONF_NAME,
- CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
@@ -30,36 +26,25 @@
from .. import mqtt
from .debug_info import log_messages
from .mixins import (
- MQTT_AVAILABILITY_SCHEMA,
- MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- MQTT_JSON_ATTRS_SCHEMA,
+ MQTT_ENTITY_COMMON_SCHEMA,
MqttAvailability,
MqttEntity,
async_setup_entry_helper,
)
-_LOGGER = logging.getLogger(__name__)
-
CONF_EXPIRE_AFTER = "expire_after"
DEFAULT_NAME = "MQTT Sensor"
DEFAULT_FORCE_UPDATE = False
-PLATFORM_SCHEMA = (
- mqtt.MQTT_RO_PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
- vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
- vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
- vol.Optional(CONF_ICON): cv.icon,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_UNIQUE_ID): cv.string,
- vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
- }
- )
- .extend(MQTT_AVAILABILITY_SCHEMA.schema)
- .extend(MQTT_JSON_ATTRS_SCHEMA.schema)
-)
+PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
+ vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
+ vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
+ }
+).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
async def async_setup_platform(
@@ -72,7 +57,6 @@ async def async_setup_platform(
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT sensors dynamically through MQTT discovery."""
-
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
@@ -86,7 +70,7 @@ async def _async_setup_entity(
async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)])
-class MqttSensor(MqttEntity, Entity):
+class MqttSensor(MqttEntity, SensorEntity):
"""Representation of a sensor that can be updated using MQTT."""
def __init__(self, hass, config, config_entry, discovery_data):
@@ -109,7 +93,6 @@ def config_schema():
def _setup_from_config(self, config):
"""(Re)Setup the entity."""
- self._config = config
template = self._config.get(CONF_VALUE_TEMPLATE)
if template is not None:
template.hass = self.hass
@@ -167,11 +150,6 @@ def _value_is_expired(self, *_):
self._expired = True
self.async_write_ha_state()
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._config[CONF_NAME]
-
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
@@ -188,12 +166,7 @@ def state(self):
return self._state
@property
- def icon(self):
- """Return the icon."""
- return self._config.get(CONF_ICON)
-
- @property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the device class of the sensor."""
return self._config.get(CONF_DEVICE_CLASS)
@@ -201,7 +174,6 @@ def device_class(self) -> Optional[str]:
def available(self) -> bool:
"""Return true if the device is available and value has not expired."""
expire_after = self._config.get(CONF_EXPIRE_AFTER)
- # pylint: disable=no-member
return MqttAvailability.available.fget(self) and (
expire_after is None or not self._expired
)
diff --git a/homeassistant/components/mqtt/services.yaml b/homeassistant/components/mqtt/services.yaml
index 04dce23f5def94..21d3915628a153 100644
--- a/homeassistant/components/mqtt/services.yaml
+++ b/homeassistant/components/mqtt/services.yaml
@@ -1,40 +1,79 @@
# Describes the format for available MQTT services
publish:
+ name: Publish
description: Publish a message to an MQTT topic.
fields:
topic:
+ name: Topic
description: Topic to publish payload.
+ required: true
example: /homeassistant/hello
+ selector:
+ text:
payload:
+ name: Payload
description: Payload to publish.
example: This is great
+ selector:
+ text:
payload_template:
- description: Template to render as payload value. Ignored if payload given.
+ name: Payload Template
+ description:
+ Template to render as payload value. Ignored if payload given.
+ advanced: true
example: "{{ states('sensor.temperature') }}"
+ selector:
+ object:
qos:
+ name: QoS
description: Quality of Service to use.
+ advanced: true
example: 2
values:
- 0
- 1
- 2
default: 0
+ selector:
+ select:
+ options:
+ - "0"
+ - "1"
+ - "2"
retain:
+ name: Retain
description: If message should have the retain flag set.
- example: true
default: false
+ example: true
+ selector:
+ boolean:
dump:
- description: Dump messages on a topic selector to the 'mqtt_dump.txt' file in your config folder.
+ name: Dump
+ description:
+ Dump messages on a topic selector to the 'mqtt_dump.txt' file in your
+ configuration folder.
fields:
topic:
+ name: Topic
description: topic to listen to
example: "OpenZWave/#"
+ selector:
+ text:
duration:
+ name: Duration
description: how long we should listen for messages in seconds
example: 5
default: 5
+ selector:
+ number:
+ min: 1
+ max: 300
+ step: 1
+ unit_of_measurement: "seconds"
+ mode: slider
reload:
- description: Reload all mqtt entities from yaml.
+ name: Reload
+ description: Reload all MQTT entities from YAML.
diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json
index 8c3db8e5b61eb0..2edbc86eb8c77f 100644
--- a/homeassistant/components/mqtt/strings.json
+++ b/homeassistant/components/mqtt/strings.json
@@ -12,8 +12,8 @@
}
},
"hassio_confirm": {
- "title": "MQTT Broker via Hass.io add-on",
- "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the Hass.io add-on {addon}?",
+ "title": "MQTT Broker via Home Assistant add-on",
+ "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the add-on {addon}?",
"data": {
"discovery": "Enable discovery"
}
diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py
index c61c30c922e998..e6c99c09fd5a44 100644
--- a/homeassistant/components/mqtt/subscription.py
+++ b/homeassistant/components/mqtt/subscription.py
@@ -1,6 +1,7 @@
"""Helper to handle a set of topics to subscribe to."""
-import logging
-from typing import Any, Callable, Dict, Optional
+from __future__ import annotations
+
+from typing import Any, Callable
import attr
@@ -12,8 +13,6 @@
from .const import DEFAULT_QOS
from .models import MessageCallbackType
-_LOGGER = logging.getLogger(__name__)
-
@attr.s(slots=True)
class EntitySubscription:
@@ -22,7 +21,7 @@ class EntitySubscription:
hass: HomeAssistantType = attr.ib()
topic: str = attr.ib()
message_callback: MessageCallbackType = attr.ib()
- unsubscribe_callback: Optional[Callable[[], None]] = attr.ib()
+ unsubscribe_callback: Callable[[], None] | None = attr.ib()
qos: int = attr.ib(default=0)
encoding: str = attr.ib(default="utf-8")
@@ -65,8 +64,8 @@ def _should_resubscribe(self, other):
@bind_hass
async def async_subscribe_topics(
hass: HomeAssistantType,
- new_state: Optional[Dict[str, EntitySubscription]],
- topics: Dict[str, Any],
+ new_state: dict[str, EntitySubscription] | None,
+ topics: dict[str, Any],
):
"""(Re)Subscribe to a set of MQTT topics.
diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py
index d6d476b680d2da..2b272b0f9be986 100644
--- a/homeassistant/components/mqtt/switch.py
+++ b/homeassistant/components/mqtt/switch.py
@@ -1,19 +1,15 @@
"""Support for MQTT switches."""
import functools
-import logging
import voluptuous as vol
from homeassistant.components import switch
from homeassistant.components.switch import SwitchEntity
from homeassistant.const import (
- CONF_DEVICE,
- CONF_ICON,
CONF_NAME,
CONF_OPTIMISTIC,
CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON,
- CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
STATE_ON,
)
@@ -34,15 +30,7 @@
)
from .. import mqtt
from .debug_info import log_messages
-from .mixins import (
- MQTT_AVAILABILITY_SCHEMA,
- MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- MQTT_JSON_ATTRS_SCHEMA,
- MqttEntity,
- async_setup_entry_helper,
-)
-
-_LOGGER = logging.getLogger(__name__)
+from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper
DEFAULT_NAME = "MQTT Switch"
DEFAULT_PAYLOAD_ON = "ON"
@@ -51,23 +39,16 @@
CONF_STATE_ON = "state_on"
CONF_STATE_OFF = "state_off"
-PLATFORM_SCHEMA = (
- mqtt.MQTT_RW_PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- vol.Optional(CONF_ICON): cv.icon,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
- vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
- vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
- vol.Optional(CONF_STATE_OFF): cv.string,
- vol.Optional(CONF_STATE_ON): cv.string,
- vol.Optional(CONF_UNIQUE_ID): cv.string,
- }
- )
- .extend(MQTT_AVAILABILITY_SCHEMA.schema)
- .extend(MQTT_JSON_ATTRS_SCHEMA.schema)
-)
+PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
+ vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
+ vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
+ vol.Optional(CONF_STATE_OFF): cv.string,
+ vol.Optional(CONF_STATE_ON): cv.string,
+ }
+).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
async def async_setup_platform(
@@ -80,7 +61,6 @@ async def async_setup_platform(
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT switch dynamically through MQTT discovery."""
-
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
@@ -114,8 +94,6 @@ def config_schema():
def _setup_from_config(self, config):
"""(Re)Setup the entity."""
- self._config = config
-
state_on = config.get(CONF_STATE_ON)
self._state_on = state_on if state_on else config[CONF_PAYLOAD_ON]
@@ -167,11 +145,6 @@ def state_message_received(msg):
if last_state:
self._state = last_state.state == STATE_ON
- @property
- def name(self):
- """Return the name of the switch."""
- return self._config[CONF_NAME]
-
@property
def is_on(self):
"""Return true if device is on."""
@@ -182,11 +155,6 @@ def assumed_state(self):
"""Return true if we do optimistic updates."""
return self._optimistic
- @property
- def icon(self):
- """Return the icon."""
- return self._config.get(CONF_ICON)
-
async def async_turn_on(self, **kwargs):
"""Turn the device on.
diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py
index b691c5cf8ce3a3..4960ff50fb594d 100644
--- a/homeassistant/components/mqtt/tag.py
+++ b/homeassistant/components/mqtt/tag.py
@@ -45,7 +45,6 @@
async def async_setup_entry(hass, config_entry):
"""Set up MQTT tag scan dynamically through MQTT discovery."""
-
setup = functools.partial(async_setup_tag, hass, config_entry=config_entry)
await async_setup_entry_helper(hass, "tag", setup, PLATFORM_SCHEMA)
diff --git a/homeassistant/components/mqtt/translations/bg.json b/homeassistant/components/mqtt/translations/bg.json
index 7a470b7427264e..96343a7f87abf7 100644
--- a/homeassistant/components/mqtt/translations/bg.json
+++ b/homeassistant/components/mqtt/translations/bg.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e\u0442\u043e \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430"
},
- "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435 \u0441 MQTT \u0431\u0440\u043e\u043a\u0435\u0440\u0430 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 {addon}?",
- "title": "MQTT \u0431\u0440\u043e\u043a\u0435\u0440 \u0447\u0440\u0435\u0437 Hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430"
+ "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435 \u0441 MQTT \u0431\u0440\u043e\u043a\u0435\u0440\u0430 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 {addon}?",
+ "title": "MQTT \u0431\u0440\u043e\u043a\u0435\u0440 \u0447\u0440\u0435\u0437 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430"
}
}
}
diff --git a/homeassistant/components/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json
index f72ee30cdcfc3a..8a314f33d9448a 100644
--- a/homeassistant/components/mqtt/translations/ca.json
+++ b/homeassistant/components/mqtt/translations/ca.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "Habilitar descobriment autom\u00e0tic"
},
- "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb el broker MQTT proporcionat pel complement de Hass.io: {addon}?",
- "title": "Broker MQTT a trav\u00e9s del complement de Hass.io"
+ "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb el broker MQTT proporcionat pel complement {addon}?",
+ "title": "Broker MQTT via complement de Home Assistant"
}
}
},
diff --git a/homeassistant/components/mqtt/translations/cs.json b/homeassistant/components/mqtt/translations/cs.json
index 325e8dde09831c..9876e2509b3773 100644
--- a/homeassistant/components/mqtt/translations/cs.json
+++ b/homeassistant/components/mqtt/translations/cs.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "Povolit automatick\u00e9 vyhled\u00e1v\u00e1n\u00ed za\u0159\u00edzen\u00ed"
},
- "description": "Chcete nakonfigurovat Home Assistant pro p\u0159ipojen\u00ed k MQTT poskytovan\u00e9mu dopl\u0148kem {addon} z Hass.io?",
- "title": "MQTT Broker prost\u0159ednictv\u00edm dopl\u0148ku Hass.io"
+ "description": "Chcete nakonfigurovat Home Assistant pro p\u0159ipojen\u00ed k MQTT poskytovan\u00e9mu dopl\u0148kem {addon} z Supervisor?",
+ "title": "MQTT Broker prost\u0159ednictv\u00edm dopl\u0148ku Supervisor"
}
}
},
@@ -38,13 +38,13 @@
"turn_on": "Zapnout"
},
"trigger_type": {
- "button_double_press": "Dvakr\u00e1t stisknuto \"{subtype}\"",
+ "button_double_press": "\"{subtype}\" stisknuto dvakr\u00e1t",
"button_long_release": "Uvoln\u011bno \"{subtype}\" po dlouh\u00e9m stisku",
- "button_quadruple_press": "\u010cty\u0159ikr\u00e1t stisknuto \"{subtype}\"",
- "button_quintuple_press": "P\u011btkr\u00e1t stisknuto \"{subtype}\"",
- "button_short_press": "Stiknuto \"{subtype}\"",
+ "button_quadruple_press": "\"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t",
+ "button_quintuple_press": "\"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t",
+ "button_short_press": "\"{subtype}\" stisknuto",
"button_short_release": "Uvoln\u011bno \"{subtype}\"",
- "button_triple_press": "T\u0159ikr\u00e1t stisknuto \"{subtype}\""
+ "button_triple_press": "\"{subtype}\" stisknuto t\u0159ikr\u00e1t"
}
},
"options": {
diff --git a/homeassistant/components/mqtt/translations/da.json b/homeassistant/components/mqtt/translations/da.json
index 7ff0f2b0a7020a..9b853a2dae2ea3 100644
--- a/homeassistant/components/mqtt/translations/da.json
+++ b/homeassistant/components/mqtt/translations/da.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "Aktiv\u00e9r opdagelse"
},
- "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til MQTT-brokeren, der leveres af hass.io-tilf\u00f8jelsen {addon}?",
- "title": "MQTT-broker via Hass.io-tilf\u00f8jelse"
+ "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til MQTT-brokeren, der leveres af Supervisor-tilf\u00f8jelsen {addon}?",
+ "title": "MQTT-broker via Supervisor-tilf\u00f8jelse"
}
}
},
diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json
index a92886eb0c6a5b..4b57249eb382e3 100644
--- a/homeassistant/components/mqtt/translations/de.json
+++ b/homeassistant/components/mqtt/translations/de.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Nur eine einzige Konfiguration von MQTT ist zul\u00e4ssig."
+ "single_instance_allowed": "Bereits konfiguriert. Es ist nur eine Konfiguration m\u00f6glich."
},
"error": {
- "cannot_connect": "Es konnte keine Verbindung zum Broker hergestellt werden."
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"step": {
"broker": {
@@ -21,8 +21,8 @@
"data": {
"discovery": "Suche aktivieren"
},
- "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem MQTT-Broker herstellt, der vom Hass.io Add-on {addon} bereitgestellt wird?",
- "title": "MQTT Broker per Hass.io add-on"
+ "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem MQTT-Broker herstellt, der vom Supervisor Add-on {addon} bereitgestellt wird?",
+ "title": "MQTT Broker per Supervisor add-on"
}
}
},
@@ -59,12 +59,15 @@
"password": "Passwort",
"port": "Port",
"username": "Benutzername"
- }
+ },
+ "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein."
},
"options": {
"data": {
+ "discovery": "Erkennung aktivieren",
"will_enable": "Letzten Willen aktivieren"
- }
+ },
+ "description": "Bitte die MQTT-Einstellungen ausw\u00e4hlen."
}
}
}
diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json
index 362e51b440537d..c8d24b78fb7864 100644
--- a/homeassistant/components/mqtt/translations/en.json
+++ b/homeassistant/components/mqtt/translations/en.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "Enable discovery"
},
- "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the Hass.io add-on {addon}?",
- "title": "MQTT Broker via Hass.io add-on"
+ "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the add-on {addon}?",
+ "title": "MQTT Broker via Home Assistant add-on"
}
}
},
diff --git a/homeassistant/components/mqtt/translations/es-419.json b/homeassistant/components/mqtt/translations/es-419.json
index d2ddc6691d158e..a69be795f77089 100644
--- a/homeassistant/components/mqtt/translations/es-419.json
+++ b/homeassistant/components/mqtt/translations/es-419.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "Habilitar descubrimiento"
},
- "description": "\u00bfDesea configurar el Asistente del Hogar para que se conecte al broker MQTT proporcionado por el complemento hass.io {addon}?",
- "title": "MQTT Broker a trav\u00e9s del complemento Hass.io"
+ "description": "\u00bfDesea configurar el Asistente del Hogar para que se conecte al broker MQTT proporcionado por el complemento Supervisor {addon}?",
+ "title": "MQTT Broker a trav\u00e9s del complemento Supervisor"
}
}
},
diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json
index d36cfbc9694b24..70107efa269121 100644
--- a/homeassistant/components/mqtt/translations/es.json
+++ b/homeassistant/components/mqtt/translations/es.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "Habilitar descubrimiento"
},
- "description": "\u00bfQuieres configurar Home Assistant para conectar con el broker de MQTT proporcionado por el complemento Hass.io {addon}?",
- "title": "MQTT Broker a trav\u00e9s del complemento Hass.io"
+ "description": "\u00bfQuieres configurar Home Assistant para conectar con el broker de MQTT proporcionado por el complemento Supervisor {addon}?",
+ "title": "MQTT Broker a trav\u00e9s del complemento Supervisor"
}
}
},
diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json
index 53d6d391e8f8df..4bc267450bb872 100644
--- a/homeassistant/components/mqtt/translations/et.json
+++ b/homeassistant/components/mqtt/translations/et.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "Luba automaatne avastamine"
},
- "description": "Kas soovite seadistada Home Assistanti \u00fchenduse loomiseks Hass.io lisandmooduli {addon} pakutava MQTT vahendajaga?",
- "title": "MQTT vahendaja Hass.io pistikprogrammi kaudu"
+ "description": "Kas soovid seadistada Home Assistanti \u00fchenduse loomiseks lisandmooduli {addon} pakutava MQTT vahendajaga?",
+ "title": "MQTT vahendaja Home Assistanti lisandmooduli abil"
}
}
},
@@ -78,7 +78,7 @@
"will_retain": "L\u00f5petamisteate j\u00e4\u00e4dvustamine",
"will_topic": "L\u00f5petamisteade"
},
- "description": "Valige MQTT s\u00e4tted."
+ "description": "Vali MQTT s\u00e4tted."
}
}
}
diff --git a/homeassistant/components/mqtt/translations/fr.json b/homeassistant/components/mqtt/translations/fr.json
index 574db2d2faf57f..6ee3788725d863 100644
--- a/homeassistant/components/mqtt/translations/fr.json
+++ b/homeassistant/components/mqtt/translations/fr.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "Activer la d\u00e9couverte"
},
- "description": "Vous voulez configurer Home Assistant pour vous connecter au broker MQTT fourni par l\u2019Add-on hass.io {addon} ?",
- "title": "MQTT Broker via le module compl\u00e9mentaire Hass.io"
+ "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte au courtier MQTT fourni par le module compl\u00e9mentaire Hass.io {addon} ?",
+ "title": "Courtier MQTT via le module compl\u00e9mentaire Hass.io"
}
}
},
diff --git a/homeassistant/components/mqtt/translations/hu.json b/homeassistant/components/mqtt/translations/hu.json
index 8cc6aa0857fcb2..f265789d777dda 100644
--- a/homeassistant/components/mqtt/translations/hu.json
+++ b/homeassistant/components/mqtt/translations/hu.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Csak egyetlen MQTT konfigur\u00e1ci\u00f3 megengedett."
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
},
"error": {
- "cannot_connect": "Nem siker\u00fclt csatlakozni a br\u00f3kerhez."
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
"step": {
"broker": {
@@ -21,8 +21,8 @@
"data": {
"discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se"
},
- "description": "Be szeretn\u00e9d konfigru\u00e1lni, hogy a Home Assistant a(z) {addon} Hass.io add-on \u00e1ltal biztos\u00edtott MQTT br\u00f3kerhez csatlakozzon?",
- "title": "MQTT Br\u00f3ker a Hass.io b\u0151v\u00edtm\u00e9nnyel"
+ "description": "Be szeretn\u00e9d konfigru\u00e1lni, hogy a Home Assistant a(z) {addon} Supervisor add-on \u00e1ltal biztos\u00edtott MQTT br\u00f3kerhez csatlakozzon?",
+ "title": "MQTT Br\u00f3ker a Supervisor b\u0151v\u00edtm\u00e9nnyel"
}
}
},
@@ -47,5 +47,20 @@
"button_short_release": "\"{subtype}\" felengedve",
"button_triple_press": "\"{subtype}\" tripla kattint\u00e1s"
}
+ },
+ "options": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "Br\u00f3ker",
+ "password": "Jelsz\u00f3",
+ "port": "Port",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/translations/id.json b/homeassistant/components/mqtt/translations/id.json
index e21052a501fe07..2a3171456c86bc 100644
--- a/homeassistant/components/mqtt/translations/id.json
+++ b/homeassistant/components/mqtt/translations/id.json
@@ -1,20 +1,72 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Hanya satu konfigurasi MQTT yang diizinkan."
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
},
"error": {
- "cannot_connect": "Tidak dapat terhubung ke broker."
+ "cannot_connect": "Gagal terhubung"
},
"step": {
"broker": {
"data": {
"broker": "Broker",
- "password": "Kata sandi",
+ "discovery": "Aktifkan penemuan",
+ "password": "Kata Sandi",
"port": "Port",
- "username": "Nama pengguna"
+ "username": "Nama Pengguna"
},
- "description": "Harap masukkan informasi koneksi dari broker MQTT Anda."
+ "description": "Masukkan informasi koneksi broker MQTT Anda."
+ },
+ "hassio_confirm": {
+ "data": {
+ "discovery": "Aktifkan penemuan"
+ },
+ "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke broker MQTT yang disediakan oleh add-on Supervisor {addon}?",
+ "title": "MQTT Broker via add-on Supervisor"
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "Tombol pertama",
+ "button_2": "Tombol kedua",
+ "button_3": "Tombol ketiga",
+ "button_4": "Tombol keempat",
+ "button_5": "Tombol kelima",
+ "button_6": "Tombol keenam",
+ "turn_off": "Matikan",
+ "turn_on": "Nyalakan"
+ },
+ "trigger_type": {
+ "button_double_press": "\"{subtype}\" diklik dua kali",
+ "button_long_press": "\"{subtype}\" terus ditekan",
+ "button_long_release": "\"{subtype}\" dilepaskan setelah ditekan lama",
+ "button_quadruple_press": "\"{subtype}\" diklik empat kali",
+ "button_quintuple_press": "\"{subtype}\" diklik lima kali",
+ "button_short_press": "\"{subtype}\" ditekan",
+ "button_short_release": "\"{subtype}\" dilepas",
+ "button_triple_press": "\"{subtype}\" diklik tiga kali"
+ }
+ },
+ "options": {
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "Broker",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "username": "Nama Pengguna"
+ },
+ "description": "Masukkan informasi koneksi broker MQTT Anda."
+ },
+ "options": {
+ "data": {
+ "discovery": "Aktifkan penemuan"
+ },
+ "description": "Pilih opsi MQTT."
}
}
}
diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json
index 845d0efabc787a..a7cad033cdbedd 100644
--- a/homeassistant/components/mqtt/translations/it.json
+++ b/homeassistant/components/mqtt/translations/it.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "Attiva l'individuazione"
},
- "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dal componente aggiuntivo di Hass.io: {addon}?",
- "title": "Broker MQTT tramite il componente aggiuntivo di Hass.io"
+ "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dal componente aggiuntivo: {addon}?",
+ "title": "Broker MQTT tramite il componente aggiuntivo di Home Assistant"
}
}
},
diff --git a/homeassistant/components/mqtt/translations/ko.json b/homeassistant/components/mqtt/translations/ko.json
index f713d564438695..dccd49b2ef3ebf 100644
--- a/homeassistant/components/mqtt/translations/ko.json
+++ b/homeassistant/components/mqtt/translations/ko.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\ud558\ub098\uc758 MQTT \ube0c\ub85c\ucee4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"error": {
- "cannot_connect": "MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"broker": {
@@ -21,8 +21,8 @@
"data": {
"discovery": "\uae30\uae30 \uac80\uc0c9 \ud65c\uc131\ud654"
},
- "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ub41c MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
- "title": "Hass.io \uc560\ub4dc\uc628\uc758 MQTT \ube0c\ub85c\ucee4"
+ "description": "{addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "Home Assistant \uc560\ub4dc\uc628\uc758 MQTT \ube0c\ub85c\ucee4"
}
}
},
@@ -38,21 +38,21 @@
"turn_on": "\ucf1c\uae30"
},
"trigger_type": {
- "button_double_press": "\"{subtype}\" \uc774 \ub450 \ubc88 \ub20c\ub9b4 \ub54c",
- "button_long_press": "\"{subtype}\" \uc774 \uacc4\uc18d \ub20c\ub824\uc9c8 \ub54c",
- "button_long_release": "\"{subtype}\" \uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c",
- "button_quadruple_press": "\"{subtype}\" \uc774 \ub124 \ubc88 \ub20c\ub9b4 \ub54c",
- "button_quintuple_press": "\"{subtype}\" \uc774 \ub2e4\uc12f \ubc88 \ub20c\ub9b4 \ub54c",
- "button_short_press": "\"{subtype}\" \uc774 \ub20c\ub9b4 \ub54c",
- "button_short_release": "\"{subtype}\" \uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c",
- "button_triple_press": "\"{subtype}\" \uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c"
+ "button_double_press": "\"{subtype}\"\uc774(\uac00) \ub450 \ubc88 \ub20c\ub838\uc744 \ub54c",
+ "button_long_press": "\"{subtype}\"\uc774(\uac00) \uacc4\uc18d \ub20c\ub838\uc744 \ub54c",
+ "button_long_release": "\"{subtype}\"\uc774(\uac00) \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \ub5bc\uc600\uc744 \ub54c",
+ "button_quadruple_press": "\"{subtype}\"\uc774(\uac00) \ub124 \ubc88 \ub20c\ub838\uc744 \ub54c",
+ "button_quintuple_press": "\"{subtype}\"\uc774(\uac00) \ub2e4\uc12f \ubc88 \ub20c\ub838\uc744 \ub54c",
+ "button_short_press": "\"{subtype}\"\uc774(\uac00) \ub20c\ub838\uc744 \ub54c",
+ "button_short_release": "\"{subtype}\"\uc5d0\uc11c \uc190\uc744 \ub5bc\uc5c8\uc744 \ub54c",
+ "button_triple_press": "\"{subtype}\"\uc774(\uac00) \uc138 \ubc88 \ub20c\ub838\uc744 \ub54c"
}
},
"options": {
"error": {
"bad_birth": "Birth \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"bad_will": "Will \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "cannot_connect": "MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"broker": {
@@ -66,11 +66,13 @@
},
"options": {
"data": {
+ "birth_enable": "Birth \uba54\uc2dc\uc9c0 \ud65c\uc131\ud654\ud558\uae30",
"birth_payload": "Birth \uba54\uc2dc\uc9c0 \ud398\uc774\ub85c\ub4dc",
"birth_qos": "Birth \uba54\uc2dc\uc9c0 QoS",
"birth_retain": "Birth \uba54\uc2dc\uc9c0 \ub9ac\ud14c\uc778",
"birth_topic": "Birth \uba54\uc2dc\uc9c0 \ud1a0\ud53d",
- "discovery": "\uc7a5\uce58 \uac80\uc0c9 \ud65c\uc131\ud654",
+ "discovery": "\uae30\uae30 \uac80\uc0c9 \ud65c\uc131\ud654\ud558\uae30",
+ "will_enable": "Will \uba54\uc2dc\uc9c0 \ud65c\uc131\ud654\ud558\uae30",
"will_payload": "Will \uba54\uc2dc\uc9c0 \ud398\uc774\ub85c\ub4dc",
"will_qos": "Will \uba54\uc2dc\uc9c0 QoS",
"will_retain": "Will \uba54\uc2dc\uc9c0 \ub9ac\ud14c\uc778",
diff --git a/homeassistant/components/mqtt/translations/lb.json b/homeassistant/components/mqtt/translations/lb.json
index 88a2664cd37a3f..fd9cd351858067 100644
--- a/homeassistant/components/mqtt/translations/lb.json
+++ b/homeassistant/components/mqtt/translations/lb.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "Entdeckung aktiv\u00e9ieren"
},
- "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam MQTT broker ze verbannen dee vum hass.io add-on {addon} bereet gestallt g\u00ebtt?",
- "title": "MQTT Broker via Hass.io add-on"
+ "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam MQTT broker ze verbannen dee vum Supervisor add-on {addon} bereet gestallt g\u00ebtt?",
+ "title": "MQTT Broker via Supervisor add-on"
}
}
},
diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json
index a0ab0e497dabfc..b56ef2413d77ea 100644
--- a/homeassistant/components/mqtt/translations/nl.json
+++ b/homeassistant/components/mqtt/translations/nl.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van MQTT is toegestaan."
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
},
"error": {
- "cannot_connect": "Kan geen verbinding maken met de broker."
+ "cannot_connect": "Kan geen verbinding maken"
},
"step": {
"broker": {
@@ -21,8 +21,8 @@
"data": {
"discovery": "Detectie inschakelen"
},
- "description": "Wilt u Home Assistant configureren om verbinding te maken met de MQTT-broker die wordt aangeboden door de hass.io add-on {addon} ?",
- "title": "MQTTT Broker via Hass.io add-on"
+ "description": "Wilt u Home Assistant configureren om verbinding te maken met de MQTT-broker die wordt aangeboden door de add-on {addon}?",
+ "title": "MQTT Broker via Home Assistant add-on"
}
}
},
@@ -50,11 +50,14 @@
},
"options": {
"error": {
+ "bad_birth": "Ongeldig birth topic",
+ "bad_will": "Ongeldig will topic",
"cannot_connect": "Kon niet verbinden"
},
"step": {
"broker": {
"data": {
+ "broker": "Broker",
"password": "Wachtwoord",
"port": "Poort",
"username": "Gebruikersnaam"
@@ -63,9 +66,19 @@
},
"options": {
"data": {
+ "birth_enable": "Geboortebericht inschakelen",
"birth_payload": "Birth message payload",
- "birth_topic": "Birth message onderwerp"
- }
+ "birth_qos": "Birth message QoS",
+ "birth_retain": "Birth message behouden",
+ "birth_topic": "Birth message onderwerp",
+ "discovery": "Discovery inschakelen",
+ "will_enable": "Will message inschakelen",
+ "will_payload": "Will message payload",
+ "will_qos": "Will message QoS",
+ "will_retain": "Will message behouden",
+ "will_topic": "Will message topic"
+ },
+ "description": "Selecteer MQTT-opties."
}
}
}
diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json
index 2a9372b3fb0578..44792075813de7 100644
--- a/homeassistant/components/mqtt/translations/no.json
+++ b/homeassistant/components/mqtt/translations/no.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "Aktiver oppdagelse"
},
- "description": "Vil du konfigurere Home Assistant til \u00e5 koble til en MQTT megler som er levert av Hass.io-tillegget {addon}?",
- "title": "MQTT megler via Hass.io tillegg"
+ "description": "Vil du konfigurere Home Assistant for \u00e5 koble til MQTT-megleren levert av tillegget {addon} ?",
+ "title": "MQTT Broker via Home Assistant-tillegg"
}
}
},
diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json
index ce41d059b244b9..17ea7407f3c2f0 100644
--- a/homeassistant/components/mqtt/translations/pl.json
+++ b/homeassistant/components/mqtt/translations/pl.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "W\u0142\u0105cz wykrywanie"
},
- "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z po\u015brednikiem MQTT dostarczonym przez dodatek Hass.io {addon}?",
- "title": "Po\u015brednik MQTT za po\u015brednictwem dodatku Hass.io"
+ "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z po\u015brednikiem MQTT dostarczonym przez dodatek {addon}?",
+ "title": "Po\u015brednik MQTT przez dodatek Home Assistant"
}
}
},
@@ -38,14 +38,14 @@
"turn_on": "w\u0142\u0105cznik"
},
"trigger_type": {
- "button_double_press": "\"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty",
- "button_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y",
- "button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu",
- "button_quadruple_press": "\"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty",
- "button_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty",
- "button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty",
- "button_short_release": "\"{subtype}\" zostanie zwolniony",
- "button_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty"
+ "button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty",
+ "button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y",
+ "button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu",
+ "button_quadruple_press": "przycisk \"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty",
+ "button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty",
+ "button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty",
+ "button_short_release": "przycisk \"{subtype}\" zostanie zwolniony",
+ "button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty"
}
},
"options": {
diff --git a/homeassistant/components/mqtt/translations/pt-BR.json b/homeassistant/components/mqtt/translations/pt-BR.json
index de739963cae894..ef9fad14440547 100644
--- a/homeassistant/components/mqtt/translations/pt-BR.json
+++ b/homeassistant/components/mqtt/translations/pt-BR.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "Ativar descoberta"
},
- "description": "Deseja configurar o Home Assistant para se conectar ao broker MQTT fornecido pelo complemento hass.io {addon}?",
- "title": "MQTT Broker via add-on Hass.io"
+ "description": "Deseja configurar o Home Assistant para se conectar ao broker MQTT fornecido pelo complemento Supervisor {addon}?",
+ "title": "MQTT Broker via add-on Supervisor"
}
}
},
diff --git a/homeassistant/components/mqtt/translations/pt.json b/homeassistant/components/mqtt/translations/pt.json
index 606997038b20e2..209c33cf1657bd 100644
--- a/homeassistant/components/mqtt/translations/pt.json
+++ b/homeassistant/components/mqtt/translations/pt.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "Ativar descoberta"
},
- "description": "Deseja configurar o Home Assistant para se ligar ao broker MQTT fornecido pelo add-on hass.io {addon}?",
- "title": "MQTT Broker atrav\u00e9s do add-on Hass.io"
+ "description": "Deseja configurar o Home Assistant para se ligar ao broker MQTT fornecido pelo add-on Supervisor {addon}?",
+ "title": "MQTT Broker atrav\u00e9s do add-on Supervisor"
}
}
},
diff --git a/homeassistant/components/mqtt/translations/ro.json b/homeassistant/components/mqtt/translations/ro.json
index 2292b58d01da67..a98818be937a8f 100644
--- a/homeassistant/components/mqtt/translations/ro.json
+++ b/homeassistant/components/mqtt/translations/ro.json
@@ -22,7 +22,7 @@
"discovery": "Activa\u021bi descoperirea"
},
"description": "Dori\u021bi s\u0103 configura\u021bi Home Assistant pentru a se conecta la brokerul MQTT furnizat de addon-ul {addon} ?",
- "title": "MQTT Broker, prin intermediul Hass.io add-on"
+ "title": "MQTT Broker, prin intermediul Supervisor add-on"
}
}
}
diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json
index 0079481d6f2541..8ff5a13138cbaa 100644
--- a/homeassistant/components/mqtt/translations/ru.json
+++ b/homeassistant/components/mqtt/translations/ru.json
@@ -13,7 +13,7 @@
"discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT."
},
@@ -21,8 +21,8 @@
"data": {
"discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432"
},
- "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?",
- "title": "\u0411\u0440\u043e\u043a\u0435\u0440 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)"
+ "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?",
+ "title": "\u0411\u0440\u043e\u043a\u0435\u0440 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)"
}
}
},
@@ -60,7 +60,7 @@
"broker": "\u0411\u0440\u043e\u043a\u0435\u0440",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT."
},
diff --git a/homeassistant/components/mqtt/translations/sl.json b/homeassistant/components/mqtt/translations/sl.json
index afd2f3b80007cd..9f16209d524bb3 100644
--- a/homeassistant/components/mqtt/translations/sl.json
+++ b/homeassistant/components/mqtt/translations/sl.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "Omogo\u010di odkrivanje"
},
- "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo s posrednikom MQTT, ki ga ponuja dodatek Hass.io {addon} ?",
- "title": "MQTT Broker prek dodatka Hass.io"
+ "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo s posrednikom MQTT, ki ga ponuja dodatek Supervisor {addon} ?",
+ "title": "MQTT Broker prek dodatka Supervisor"
}
}
},
diff --git a/homeassistant/components/mqtt/translations/sv.json b/homeassistant/components/mqtt/translations/sv.json
index c74979bb6bba2f..b3088ca49a9799 100644
--- a/homeassistant/components/mqtt/translations/sv.json
+++ b/homeassistant/components/mqtt/translations/sv.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "Aktivera uppt\u00e4ckt"
},
- "description": "Vill du konfigurera Home Assistant att ansluta till den MQTT-broker som tillhandah\u00e5lls av Hass.io-till\u00e4gget \"{addon}\"?",
- "title": "MQTT Broker via Hass.io till\u00e4gg"
+ "description": "Vill du konfigurera Home Assistant att ansluta till den MQTT-broker som tillhandah\u00e5lls av Supervisor-till\u00e4gget \"{addon}\"?",
+ "title": "MQTT Broker via Supervisor till\u00e4gg"
}
}
},
diff --git a/homeassistant/components/mqtt/translations/tr.json b/homeassistant/components/mqtt/translations/tr.json
index 1b73b94d5a4337..86dce2b6ea4b6e 100644
--- a/homeassistant/components/mqtt/translations/tr.json
+++ b/homeassistant/components/mqtt/translations/tr.json
@@ -1,11 +1,52 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
"step": {
+ "broker": {
+ "data": {
+ "password": "Parola",
+ "port": "Port"
+ }
+ },
"hassio_confirm": {
"data": {
"discovery": "Ke\u015ffetmeyi etkinle\u015ftir"
}
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "turn_off": "Kapat",
+ "turn_on": "A\u00e7"
+ },
+ "trigger_type": {
+ "button_double_press": "\" {subtype} \" \u00e7ift t\u0131kland\u0131",
+ "button_long_press": "\" {subtype} \" s\u00fcrekli olarak bas\u0131ld\u0131",
+ "button_quadruple_press": "\" {subtype} \" d\u00f6rt kez t\u0131kland\u0131",
+ "button_quintuple_press": "\" {subtype} \" be\u015fli t\u0131kland\u0131",
+ "button_short_press": "\" {subtype} \" bas\u0131ld\u0131",
+ "button_short_release": "\" {subtype} \" yay\u0131nland\u0131",
+ "button_triple_press": "\" {subtype} \" \u00fc\u00e7 kez t\u0131kland\u0131"
+ }
+ },
+ "options": {
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "password": "Parola",
+ "port": "Port",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/translations/uk.json b/homeassistant/components/mqtt/translations/uk.json
index 747d190a56d6a9..b8cbab32b14fd4 100644
--- a/homeassistant/components/mqtt/translations/uk.json
+++ b/homeassistant/components/mqtt/translations/uk.json
@@ -1,26 +1,84 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\u0414\u043e\u0437\u0432\u043e\u043b\u0435\u043d\u043e \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e MQTT."
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
},
"error": {
- "cannot_connect": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0431\u0440\u043e\u043a\u0435\u0440\u0430."
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
},
"step": {
"broker": {
"data": {
"broker": "\u0411\u0440\u043e\u043a\u0435\u0440",
- "discovery": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u043f\u043e\u0448\u0443\u043a",
+ "discovery": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0410\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
"username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
},
- "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0432\u0430\u0448\u043e\u0433\u043e \u0431\u0440\u043e\u043a\u0435\u0440\u0430 MQTT."
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0437 \u0432\u0430\u0448\u0438\u043c \u0431\u0440\u043e\u043a\u0435\u0440\u043e\u043c MQTT."
},
"hassio_confirm": {
"data": {
- "discovery": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u043f\u043e\u0448\u0443\u043a"
- }
+ "discovery": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0410\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432"
+ },
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0431\u0440\u043e\u043a\u0435\u0440\u0430 MQTT (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor \"{addon}\")?",
+ "title": "\u0411\u0440\u043e\u043a\u0435\u0440 MQTT (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor)"
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button_1": "\u041f\u0435\u0440\u0448\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_2": "\u0414\u0440\u0443\u0433\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_3": "\u0422\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_5": "\u041f'\u044f\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_6": "\u0428\u043e\u0441\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "turn_off": "\u0412\u0438\u043c\u043a\u043d\u0443\u0442\u0438",
+ "turn_on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438"
+ },
+ "trigger_type": {
+ "button_double_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0438",
+ "button_long_press": "{subtype} \u0434\u043e\u0432\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430",
+ "button_long_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f",
+ "button_quadruple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0447\u043e\u0442\u0438\u0440\u0438 \u0440\u0430\u0437\u0438",
+ "button_quintuple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u043f'\u044f\u0442\u044c \u0440\u0430\u0437\u0456\u0432",
+ "button_short_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430",
+ "button_short_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f",
+ "button_triple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0438"
+ }
+ },
+ "options": {
+ "error": {
+ "bad_birth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f.",
+ "bad_will": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "\u0411\u0440\u043e\u043a\u0435\u0440",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0437 \u0432\u0430\u0448\u0438\u043c \u0431\u0440\u043e\u043a\u0435\u0440\u043e\u043c MQTT."
+ },
+ "options": {
+ "data": {
+ "birth_enable": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u0438 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f",
+ "birth_payload": "\u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0442\u043e\u043f\u0456\u043a\u0430 \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f",
+ "birth_qos": "QoS \u0442\u043e\u043f\u0456\u043a\u0430 \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f",
+ "birth_retain": "\u0417\u0431\u0435\u0440\u0456\u0433\u0430\u0442\u0438 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f",
+ "birth_topic": "\u0422\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f (LWT)",
+ "discovery": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f",
+ "will_enable": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u0438 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f",
+ "will_payload": "\u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0442\u043e\u043f\u0456\u043a\u0430 \u043f\u0440\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f",
+ "will_qos": "QoS \u0442\u043e\u043f\u0456\u043a\u0430 \u043f\u0440\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f",
+ "will_retain": "\u0417\u0431\u0435\u0440\u0456\u0433\u0430\u0442\u0438 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f",
+ "will_topic": "\u0422\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f (LWT)"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0438\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 MQTT."
}
}
}
diff --git a/homeassistant/components/mqtt/translations/zh-Hans.json b/homeassistant/components/mqtt/translations/zh-Hans.json
index 63ceded5654159..97356ed44d4202 100644
--- a/homeassistant/components/mqtt/translations/zh-Hans.json
+++ b/homeassistant/components/mqtt/translations/zh-Hans.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "\u542f\u7528\u53d1\u73b0"
},
- "description": "\u662f\u5426\u8981\u914d\u7f6e Home Assistant \u8fde\u63a5\u5230 Hass.io \u52a0\u8f7d\u9879 {addon} \u63d0\u4f9b\u7684 MQTT \u670d\u52a1\u5668\uff1f",
- "title": "\u6765\u81ea Hass.io \u52a0\u8f7d\u9879\u7684 MQTT \u670d\u52a1\u5668"
+ "description": "\u662f\u5426\u8981\u914d\u7f6e Home Assistant \u8fde\u63a5\u5230 Supervisor \u52a0\u8f7d\u9879 {addon} \u63d0\u4f9b\u7684 MQTT \u670d\u52a1\u5668\uff1f",
+ "title": "\u6765\u81ea Supervisor \u52a0\u8f7d\u9879\u7684 MQTT \u670d\u52a1\u5668"
}
}
},
diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json
index bfb27361889f38..e24474ed7b6aef 100644
--- a/homeassistant/components/mqtt/translations/zh-Hant.json
+++ b/homeassistant/components/mqtt/translations/zh-Hant.json
@@ -21,8 +21,8 @@
"data": {
"discovery": "\u958b\u555f\u641c\u5c0b"
},
- "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Hass.io \u9644\u52a0\u6574\u5408 {addon} \u4e4b MQTT broker\uff1f",
- "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6 MQTT Broker"
+ "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 MQTT broker\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f",
+ "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 MQTT Broker"
}
}
},
diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py
index 1c96b3de266a91..34c47aec791291 100644
--- a/homeassistant/components/mqtt/trigger.py
+++ b/homeassistant/components/mqtt/trigger.py
@@ -1,11 +1,13 @@
"""Offer MQTT listening automation rules."""
+from contextlib import suppress
import json
+import logging
import voluptuous as vol
-from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM
+from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM, CONF_VALUE_TEMPLATE
from homeassistant.core import HassJob, callback
-import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers import config_validation as cv, template
from .. import mqtt
@@ -20,8 +22,9 @@
TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_PLATFORM): mqtt.DOMAIN,
- vol.Required(CONF_TOPIC): mqtt.util.valid_subscribe_topic,
- vol.Optional(CONF_PAYLOAD): cv.string,
+ vol.Required(CONF_TOPIC): mqtt.util.valid_subscribe_topic_template,
+ vol.Optional(CONF_PAYLOAD): cv.template,
+ vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All(
vol.Coerce(int), vol.In([0, 1, 2])
@@ -29,34 +32,65 @@
}
)
+_LOGGER = logging.getLogger(__name__)
+
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
topic = config[CONF_TOPIC]
- payload = config.get(CONF_PAYLOAD)
+ wanted_payload = config.get(CONF_PAYLOAD)
+ value_template = config.get(CONF_VALUE_TEMPLATE)
encoding = config[CONF_ENCODING] or None
qos = config[CONF_QOS]
job = HassJob(action)
+ variables = None
+ if automation_info:
+ variables = automation_info.get("variables")
+
+ template.attach(hass, wanted_payload)
+ if wanted_payload:
+ wanted_payload = wanted_payload.async_render(
+ variables, limited=True, parse_result=False
+ )
+
+ template.attach(hass, topic)
+ if isinstance(topic, template.Template):
+ topic = topic.async_render(variables, limited=True, parse_result=False)
+ topic = mqtt.util.valid_subscribe_topic(topic)
+
+ template.attach(hass, value_template)
@callback
def mqtt_automation_listener(mqttmsg):
"""Listen for MQTT messages."""
- if payload is None or payload == mqttmsg.payload:
+ payload = mqttmsg.payload
+
+ if value_template is not None:
+ payload = value_template.async_render_with_possible_json_value(
+ payload,
+ error_value=None,
+ )
+
+ if wanted_payload is None or wanted_payload == payload:
data = {
"platform": "mqtt",
"topic": mqttmsg.topic,
"payload": mqttmsg.payload,
"qos": mqttmsg.qos,
"description": f"mqtt topic {mqttmsg.topic}",
+ "id": trigger_id,
}
- try:
+ with suppress(ValueError):
data["payload_json"] = json.loads(mqttmsg.payload)
- except ValueError:
- pass
hass.async_run_hass_job(job, {"trigger": data})
+ _LOGGER.debug(
+ "Attaching MQTT trigger for topic: '%s', payload: '%s'", topic, wanted_payload
+ )
+
remove = await mqtt.async_subscribe(
hass, topic, mqtt_automation_listener, encoding=encoding, qos=qos
)
diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py
index 651fe48fe3d37c..b8fca50a153c06 100644
--- a/homeassistant/components/mqtt/util.py
+++ b/homeassistant/components/mqtt/util.py
@@ -4,7 +4,7 @@
import voluptuous as vol
from homeassistant.const import CONF_PAYLOAD
-from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers import config_validation as cv, template
from .const import (
ATTR_PAYLOAD,
@@ -61,6 +61,16 @@ def valid_subscribe_topic(value: Any) -> str:
return value
+def valid_subscribe_topic_template(value: Any) -> template.Template:
+ """Validate either a jinja2 template or a valid MQTT subscription topic."""
+ tpl = template.Template(value)
+
+ if tpl.is_static:
+ valid_subscribe_topic(value)
+
+ return tpl
+
+
def valid_publish_topic(value: Any) -> str:
"""Validate that we can publish using this MQTT topic."""
value = valid_topic(value)
diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py
index e580e874993366..85fd1247381ecd 100644
--- a/homeassistant/components/mqtt/vacuum/__init__.py
+++ b/homeassistant/components/mqtt/vacuum/__init__.py
@@ -1,6 +1,5 @@
"""Support for MQTT vacuums."""
import functools
-import logging
import voluptuous as vol
@@ -13,8 +12,6 @@
from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy
from .schema_state import PLATFORM_SCHEMA_STATE, async_setup_entity_state
-_LOGGER = logging.getLogger(__name__)
-
def validate_mqtt_vacuum(value):
"""Validate MQTT vacuum schema."""
@@ -35,7 +32,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT vacuum dynamically through MQTT discovery."""
-
setup = functools.partial(
_async_setup_entity, async_add_entities, config_entry=config_entry
)
diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py
index e7be64be6ae3cf..f0f00a72bb4e73 100644
--- a/homeassistant/components/mqtt/vacuum/schema_legacy.py
+++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py
@@ -1,6 +1,5 @@
"""Support for Legacy MQTT vacuum."""
import json
-import logging
import voluptuous as vol
@@ -18,12 +17,7 @@
SUPPORT_TURN_ON,
VacuumEntity,
)
-from homeassistant.const import (
- ATTR_SUPPORTED_FEATURES,
- CONF_DEVICE,
- CONF_NAME,
- CONF_UNIQUE_ID,
-)
+from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.icon import icon_for_battery_level
@@ -31,16 +25,9 @@
from .. import subscription
from ... import mqtt
from ..debug_info import log_messages
-from ..mixins import (
- MQTT_AVAILABILITY_SCHEMA,
- MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- MQTT_JSON_ATTRS_SCHEMA,
- MqttEntity,
-)
+from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity
from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services
-_LOGGER = logging.getLogger(__name__)
-
SERVICE_TO_STRING = {
SUPPORT_TURN_ON: "turn_on",
SUPPORT_TURN_OFF: "turn_off",
@@ -120,7 +107,6 @@
vol.Inclusive(CONF_CHARGING_TOPIC, "charging"): mqtt.valid_publish_topic,
vol.Inclusive(CONF_CLEANING_TEMPLATE, "cleaning"): cv.template,
vol.Inclusive(CONF_CLEANING_TOPIC, "cleaning"): mqtt.valid_publish_topic,
- vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
vol.Inclusive(CONF_DOCKED_TEMPLATE, "docked"): cv.template,
vol.Inclusive(CONF_DOCKED_TOPIC, "docked"): mqtt.valid_publish_topic,
vol.Inclusive(CONF_ERROR_TEMPLATE, "error"): cv.template,
@@ -155,13 +141,11 @@
vol.Optional(
CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS
): vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]),
- vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(mqtt.CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
}
)
- .extend(MQTT_AVAILABILITY_SCHEMA.schema)
- .extend(MQTT_JSON_ATTRS_SCHEMA.schema)
+ .extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
.extend(MQTT_VACUUM_SCHEMA.schema)
)
@@ -195,7 +179,6 @@ def config_schema():
return PLATFORM_SCHEMA_LEGACY
def _setup_from_config(self, config):
- self._name = config[CONF_NAME]
supported_feature_strings = config[CONF_SUPPORTED_FEATURES]
self._supported_features = strings_to_services(
supported_feature_strings, STRING_TO_SERVICE
@@ -341,11 +324,6 @@ def message_received(msg):
},
)
- @property
- def name(self):
- """Return the name of the vacuum."""
- return self._name
-
@property
def is_on(self):
"""Return true if vacuum is on."""
@@ -377,7 +355,6 @@ def battery_icon(self):
No need to check SUPPORT_BATTERY, this won't be called if battery_level is None.
"""
-
return icon_for_battery_level(
battery_level=self.battery_level, charging=self._charging
)
diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py
index c754ba1604a368..37a12d33df644e 100644
--- a/homeassistant/components/mqtt/vacuum/schema_state.py
+++ b/homeassistant/components/mqtt/vacuum/schema_state.py
@@ -1,6 +1,5 @@
"""Support for a State MQTT vacuum."""
import json
-import logging
import voluptuous as vol
@@ -23,28 +22,16 @@
SUPPORT_STOP,
StateVacuumEntity,
)
-from homeassistant.const import (
- ATTR_SUPPORTED_FEATURES,
- CONF_DEVICE,
- CONF_NAME,
- CONF_UNIQUE_ID,
-)
+from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription
from ... import mqtt
from ..debug_info import log_messages
-from ..mixins import (
- MQTT_AVAILABILITY_SCHEMA,
- MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- MQTT_JSON_ATTRS_SCHEMA,
- MqttEntity,
-)
+from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity
from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services
-_LOGGER = logging.getLogger(__name__)
-
SERVICE_TO_STRING = {
SUPPORT_START: "start",
SUPPORT_PAUSE: "pause",
@@ -116,7 +103,6 @@
PLATFORM_SCHEMA_STATE = (
mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
{
- vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All(
cv.ensure_list, [cv.string]
),
@@ -139,13 +125,11 @@
vol.Optional(
CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS
): vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]),
- vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
}
)
- .extend(MQTT_AVAILABILITY_SCHEMA.schema)
- .extend(MQTT_JSON_ATTRS_SCHEMA.schema)
+ .extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
.extend(MQTT_VACUUM_SCHEMA.schema)
)
@@ -174,8 +158,6 @@ def config_schema():
return PLATFORM_SCHEMA_STATE
def _setup_from_config(self, config):
- self._config = config
- self._name = config[CONF_NAME]
supported_feature_strings = config[CONF_SUPPORTED_FEATURES]
self._supported_features = strings_to_services(
supported_feature_strings, STRING_TO_SERVICE
@@ -222,11 +204,6 @@ def state_message_received(msg):
self.hass, self._sub_state, topics
)
- @property
- def name(self):
- """Return the name of the vacuum."""
- return self._name
-
@property
def state(self):
"""Return state of vacuum."""
diff --git a/homeassistant/components/mqtt_eventstream/__init__.py b/homeassistant/components/mqtt_eventstream/__init__.py
index 5a5f3b3c74d560..328b9395eeac3f 100644
--- a/homeassistant/components/mqtt_eventstream/__init__.py
+++ b/homeassistant/components/mqtt_eventstream/__init__.py
@@ -60,13 +60,13 @@ def _event_publisher(event):
# Filter out the events that were triggered by publishing
# to the MQTT topic, or you will end up in an infinite loop.
- if event.event_type == EVENT_CALL_SERVICE:
- if (
- event.data.get("domain") == mqtt.DOMAIN
- and event.data.get("service") == mqtt.SERVICE_PUBLISH
- and event.data[ATTR_SERVICE_DATA].get("topic") == pub_topic
- ):
- return
+ if (
+ event.event_type == EVENT_CALL_SERVICE
+ and event.data.get("domain") == mqtt.DOMAIN
+ and event.data.get("service") == mqtt.SERVICE_PUBLISH
+ and event.data[ATTR_SERVICE_DATA].get("topic") == pub_topic
+ ):
+ return
event_info = {"event_type": event.event_type, "event_data": event.data}
msg = json.dumps(event_info, cls=JSONEncoder)
diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py
index 0d07133b39653f..e446ab8ba7a096 100644
--- a/homeassistant/components/mqtt_room/sensor.py
+++ b/homeassistant/components/mqtt_room/sensor.py
@@ -7,20 +7,24 @@
from homeassistant.components import mqtt
from homeassistant.components.mqtt import CONF_STATE_TOPIC
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import ATTR_ID, CONF_NAME, CONF_TIMEOUT, STATE_NOT_HOME
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.const import (
+ ATTR_DEVICE_ID,
+ ATTR_ID,
+ CONF_DEVICE_ID,
+ CONF_NAME,
+ CONF_TIMEOUT,
+ STATE_NOT_HOME,
+)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import dt, slugify
_LOGGER = logging.getLogger(__name__)
-ATTR_DEVICE_ID = "device_id"
ATTR_DISTANCE = "distance"
ATTR_ROOM = "room"
-CONF_DEVICE_ID = "device_id"
CONF_AWAY_TIMEOUT = "away_timeout"
DEFAULT_AWAY_TIMEOUT = 0
@@ -66,7 +70,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
-class MQTTRoomSensor(Entity):
+class MQTTRoomSensor(SensorEntity):
"""Representation of a room sensor that is updated via MQTT."""
def __init__(self, name, state_topic, device_id, timeout, consider_home):
@@ -130,7 +134,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_DISTANCE: self._distance}
diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py
new file mode 100644
index 00000000000000..541c6075cc3bfd
--- /dev/null
+++ b/homeassistant/components/mullvad/__init__.py
@@ -0,0 +1,64 @@
+"""The Mullvad VPN integration."""
+import asyncio
+from datetime import timedelta
+import logging
+
+import async_timeout
+from mullvad_api import MullvadAPI
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import update_coordinator
+
+from .const import DOMAIN
+
+PLATFORMS = ["binary_sensor"]
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+ """Set up the Mullvad VPN integration."""
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: dict):
+ """Set up Mullvad VPN integration."""
+
+ async def async_get_mullvad_api_data():
+ with async_timeout.timeout(10):
+ api = await hass.async_add_executor_job(MullvadAPI)
+ return api.data
+
+ coordinator = update_coordinator.DataUpdateCoordinator(
+ hass,
+ logging.getLogger(__name__),
+ name=DOMAIN,
+ update_method=async_get_mullvad_api_data,
+ update_interval=timedelta(minutes=1),
+ )
+ await coordinator.async_config_entry_first_refresh()
+
+ hass.data[DOMAIN] = coordinator
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+
+ if unload_ok:
+ del hass.data[DOMAIN]
+
+ return unload_ok
diff --git a/homeassistant/components/mullvad/binary_sensor.py b/homeassistant/components/mullvad/binary_sensor.py
new file mode 100644
index 00000000000000..1a91bba2038db4
--- /dev/null
+++ b/homeassistant/components/mullvad/binary_sensor.py
@@ -0,0 +1,52 @@
+"""Setup Mullvad VPN Binary Sensors."""
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
+ BinarySensorEntity,
+)
+from homeassistant.const import CONF_DEVICE_CLASS, CONF_ID, CONF_NAME
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import DOMAIN
+
+BINARY_SENSORS = (
+ {
+ CONF_ID: "mullvad_exit_ip",
+ CONF_NAME: "Mullvad Exit IP",
+ CONF_DEVICE_CLASS: DEVICE_CLASS_CONNECTIVITY,
+ },
+)
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Defer sensor setup to the shared sensor module."""
+ coordinator = hass.data[DOMAIN]
+
+ async_add_entities(
+ MullvadBinarySensor(coordinator, sensor) for sensor in BINARY_SENSORS
+ )
+
+
+class MullvadBinarySensor(CoordinatorEntity, BinarySensorEntity):
+ """Represents a Mullvad binary sensor."""
+
+ def __init__(self, coordinator, sensor):
+ """Initialize the Mullvad binary sensor."""
+ super().__init__(coordinator)
+ self.id = sensor[CONF_ID]
+ self._name = sensor[CONF_NAME]
+ self._device_class = sensor[CONF_DEVICE_CLASS]
+
+ @property
+ def device_class(self):
+ """Return the device class for this binary sensor."""
+ return self._device_class
+
+ @property
+ def name(self):
+ """Return the name for this binary sensor."""
+ return self._name
+
+ @property
+ def is_on(self):
+ """Return the state for this binary sensor."""
+ return self.coordinator.data[self.id]
diff --git a/homeassistant/components/mullvad/config_flow.py b/homeassistant/components/mullvad/config_flow.py
new file mode 100644
index 00000000000000..50f67d10e257b2
--- /dev/null
+++ b/homeassistant/components/mullvad/config_flow.py
@@ -0,0 +1,35 @@
+"""Config flow for Mullvad VPN integration."""
+import logging
+
+from mullvad_api import MullvadAPI, MullvadAPIError
+
+from homeassistant import config_entries
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Mullvad VPN."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ if self.hass.config_entries.async_entries(DOMAIN):
+ return self.async_abort(reason="already_configured")
+
+ errors = {}
+ if user_input is not None:
+ try:
+ await self.hass.async_add_executor_job(MullvadAPI)
+ except MullvadAPIError:
+ errors["base"] = "cannot_connect"
+ except Exception: # pylint: disable=broad-except
+ errors["base"] = "unknown"
+ else:
+ return self.async_create_entry(title="Mullvad VPN", data=user_input)
+
+ return self.async_show_form(step_id="user", errors=errors)
diff --git a/homeassistant/components/mullvad/const.py b/homeassistant/components/mullvad/const.py
new file mode 100644
index 00000000000000..4e3be28782ce63
--- /dev/null
+++ b/homeassistant/components/mullvad/const.py
@@ -0,0 +1,3 @@
+"""Constants for the Mullvad VPN integration."""
+
+DOMAIN = "mullvad"
diff --git a/homeassistant/components/mullvad/manifest.json b/homeassistant/components/mullvad/manifest.json
new file mode 100644
index 00000000000000..1a440240d7e1cf
--- /dev/null
+++ b/homeassistant/components/mullvad/manifest.json
@@ -0,0 +1,12 @@
+{
+ "domain": "mullvad",
+ "name": "Mullvad VPN",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/mullvad",
+ "requirements": [
+ "mullvad-api==1.0.0"
+ ],
+ "codeowners": [
+ "@meichthys"
+ ]
+}
diff --git a/homeassistant/components/mullvad/strings.json b/homeassistant/components/mullvad/strings.json
new file mode 100644
index 00000000000000..7910a40ec35e4f
--- /dev/null
+++ b/homeassistant/components/mullvad/strings.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "step": {
+ "user": {
+ "description": "Set up the Mullvad VPN integration?"
+ }
+ }
+ }
+}
diff --git a/homeassistant/components/mullvad/translations/bg.json b/homeassistant/components/mullvad/translations/bg.json
new file mode 100644
index 00000000000000..a84e1c3bfdf358
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/bg.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "error": {
+ "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/mullvad/translations/ca.json b/homeassistant/components/mullvad/translations/ca.json
new file mode 100644
index 00000000000000..f81781cbc0fa43
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/ca.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "password": "Contrasenya",
+ "username": "Nom d'usuari"
+ },
+ "description": "Vols configurar la integraci\u00f3 Mullvad VPN?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/cs.json b/homeassistant/components/mullvad/translations/cs.json
new file mode 100644
index 00000000000000..0f02cd974c207c
--- /dev/null
+++ b/homeassistant/components/mullvad/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",
+ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hostitel",
+ "password": "Heslo",
+ "username": "U\u017eivatelsk\u00e9 jm\u00e9no"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/de.json b/homeassistant/components/mullvad/translations/de.json
new file mode 100644
index 00000000000000..6014a9155c81b8
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/de.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Passwort",
+ "username": "Benutzername"
+ },
+ "description": "Mullvad VPN Integration einrichten?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/el.json b/homeassistant/components/mullvad/translations/el.json
new file mode 100644
index 00000000000000..6f19f0039eddaf
--- /dev/null
+++ b/homeassistant/components/mullvad/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 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af"
+ },
+ "error": {
+ "cannot_connect": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2",
+ "invalid_auth": "\u0386\u03ba\u03c5\u03c1\u03b7 \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7",
+ "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u03a0\u03ac\u03c1\u03bf\u03c7\u03bf\u03c2",
+ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2",
+ "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7"
+ },
+ "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 Mullvad VPN;"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/en.json b/homeassistant/components/mullvad/translations/en.json
new file mode 100644
index 00000000000000..fcfa89ef0829ee
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/en.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Password",
+ "username": "Username"
+ },
+ "description": "Set up the Mullvad VPN integration?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/es.json b/homeassistant/components/mullvad/translations/es.json
new file mode 100644
index 00000000000000..579726b061e520
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/es.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositivo ya est\u00e1 configurado"
+ },
+ "error": {
+ "cannot_connect": "Fallo al conectar",
+ "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida",
+ "unknown": "Error inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrase\u00f1a",
+ "username": "Usuario"
+ },
+ "description": "\u00bfConfigurar la integraci\u00f3n VPN de Mullvad?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/et.json b/homeassistant/components/mullvad/translations/et.json
new file mode 100644
index 00000000000000..671d18a2cd344c
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/et.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendumine nurjus",
+ "invalid_auth": "Tuvastamise viga",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Salas\u00f5na",
+ "username": "Kasutajanimi"
+ },
+ "description": "Kas seadistada Mullvad VPN sidumine?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/fr.json b/homeassistant/components/mullvad/translations/fr.json
new file mode 100644
index 00000000000000..1a8b10de809cfd
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/fr.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "invalid_auth": "Authentification invalide",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "H\u00f4te",
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur"
+ },
+ "description": "Configurez l'int\u00e9gration VPN Mullvad?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/he.json b/homeassistant/components/mullvad/translations/he.json
new file mode 100644
index 00000000000000..7f60f15d598ac0
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/he.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8"
+ },
+ "error": {
+ "cannot_connect": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
+ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05ea\u05e7\u05d9\u05df",
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/hu.json b/homeassistant/components/mullvad/translations/hu.json
new file mode 100644
index 00000000000000..0abcc301f0c854
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/hu.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/id.json b/homeassistant/components/mullvad/translations/id.json
new file mode 100644
index 00000000000000..a5409549f192d5
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/id.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "description": "Siapkan integrasi VPN Mullvad?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/it.json b/homeassistant/components/mullvad/translations/it.json
new file mode 100644
index 00000000000000..47cd8290f215ad
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/it.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Password",
+ "username": "Nome utente"
+ },
+ "description": "Configurare l'integrazione VPN Mullvad?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/ko.json b/homeassistant/components/mullvad/translations/ko.json
new file mode 100644
index 00000000000000..fd9134b977c4f2
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/ko.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ },
+ "description": "Mullvad VPN \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/nl.json b/homeassistant/components/mullvad/translations/nl.json
new file mode 100644
index 00000000000000..aa4d80ac71dd67
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/nl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Wachtwoord",
+ "username": "Gebruikersnaam"
+ },
+ "description": "De Mullvad VPN-integratie instellen?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/no.json b/homeassistant/components/mullvad/translations/no.json
new file mode 100644
index 00000000000000..d33f26404452be
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/no.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_auth": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Vert",
+ "password": "Passord",
+ "username": "Brukernavn"
+ },
+ "description": "Sette opp Mullvad VPN-integrasjon?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/pl.json b/homeassistant/components/mullvad/translations/pl.json
new file mode 100644
index 00000000000000..f5aca4e092c9a3
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/pl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP",
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ },
+ "description": "Skonfigurowa\u0107 integracj\u0119 Mullvad VPN?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/pt.json b/homeassistant/components/mullvad/translations/pt.json
new file mode 100644
index 00000000000000..561c8d77287efa
--- /dev/null
+++ b/homeassistant/components/mullvad/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",
+ "unknown": "Erro inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Servidor",
+ "password": "Palavra-passe",
+ "username": "Nome de Utilizador"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/ru.json b/homeassistant/components/mullvad/translations/ru.json
new file mode 100644
index 00000000000000..af2ecd321f00d6
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/ru.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Mullvad VPN."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/tr.json b/homeassistant/components/mullvad/translations/tr.json
new file mode 100644
index 00000000000000..0f3ddabfc4f44b
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/tr.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/zh-Hans.json b/homeassistant/components/mullvad/translations/zh-Hans.json
new file mode 100644
index 00000000000000..acb02a7d0f6303
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/zh-Hans.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u5b8c\u6210\u914d\u7f6e"
+ },
+ "error": {
+ "cannot_connect": "\u8fde\u63a5\u5931\u8d25",
+ "invalid_auth": "\u9a8c\u8bc1\u5931\u8d25",
+ "unknown": "\u9884\u671f\u5916\u7684\u9519\u8bef"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u5bc6\u7801",
+ "username": "\u7528\u6237\u540d"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/zh-Hant.json b/homeassistant/components/mullvad/translations/zh-Hant.json
new file mode 100644
index 00000000000000..d78c36b72d77cd
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/zh-Hant.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "password": "\u5bc6\u78bc",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ },
+ "description": "\u8a2d\u5b9a Mullvad VPN \u6574\u5408\uff1f"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py
index 2ceca024a6f1ea..16b061a3346cd7 100644
--- a/homeassistant/components/mvglive/sensor.py
+++ b/homeassistant/components/mvglive/sensor.py
@@ -6,10 +6,9 @@
import MVGLive
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, TIME_MINUTES
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -78,7 +77,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class MVGLiveSensor(Entity):
+class MVGLiveSensor(SensorEntity):
"""Implementation of an MVG Live sensor."""
def __init__(
@@ -114,7 +113,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
dep = self.data.departures
if not dep:
diff --git a/homeassistant/components/my/__init__.py b/homeassistant/components/my/__init__.py
new file mode 100644
index 00000000000000..8cc725cb9a566b
--- /dev/null
+++ b/homeassistant/components/my/__init__.py
@@ -0,0 +1,12 @@
+"""Support for my.home-assistant.io redirect service."""
+
+DOMAIN = "my"
+URL_PATH = "_my_redirect"
+
+
+async def async_setup(hass, config):
+ """Register hidden _my_redirect panel."""
+ hass.components.frontend.async_register_built_in_panel(
+ DOMAIN, frontend_url_path=URL_PATH
+ )
+ return True
diff --git a/homeassistant/components/my/manifest.json b/homeassistant/components/my/manifest.json
new file mode 100644
index 00000000000000..3b9e253f353279
--- /dev/null
+++ b/homeassistant/components/my/manifest.json
@@ -0,0 +1,7 @@
+{
+ "domain": "my",
+ "name": "My Home Assistant",
+ "documentation": "https://www.home-assistant.io/integrations/my",
+ "dependencies": ["frontend"],
+ "codeowners": ["@home-assistant/core"]
+}
diff --git a/homeassistant/components/mychevy/sensor.py b/homeassistant/components/mychevy/sensor.py
index 413118451856df..18b5e95d8389cf 100644
--- a/homeassistant/components/mychevy/sensor.py
+++ b/homeassistant/components/mychevy/sensor.py
@@ -1,10 +1,9 @@
"""Support for MyChevy sensors."""
import logging
-from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity
from homeassistant.const import PERCENTAGE
from homeassistant.core import callback
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.util import slugify
@@ -46,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors)
-class MyChevyStatus(Entity):
+class MyChevyStatus(SensorEntity):
"""A string representing the charge mode."""
_name = "MyChevy Status"
@@ -109,7 +108,7 @@ def should_poll(self):
return False
-class EVSensor(Entity):
+class EVSensor(SensorEntity):
"""Base EVSensor class.
The only real difference between sensors is which units and what
@@ -160,6 +159,8 @@ def async_update_callback(self):
"""Update state."""
if self._car is not None:
self._state = getattr(self._car, self._attr, None)
+ if self._unit_of_measurement == "miles":
+ self._state = round(self._state)
for attr in self._extra_attrs:
self._state_attributes[attr] = getattr(self._car, attr)
self.async_write_ha_state()
@@ -170,7 +171,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return all the state attributes."""
return self._state_attributes
diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py
index 959000da3b3abd..b25751d7270db7 100644
--- a/homeassistant/components/myq/__init__.py
+++ b/homeassistant/components/myq/__init__.py
@@ -11,7 +11,7 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, PLATFORMS, UPDATE_INTERVAL
@@ -40,19 +40,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
except MyQError as err:
raise ConfigEntryNotReady from err
+ # Called by DataUpdateCoordinator, allows to capture any MyQError exceptions and to throw an HASS UpdateFailed
+ # exception instead, preventing traceback in HASS logs.
+ async def async_update_data():
+ try:
+ return await myq.update_device_info()
+ except MyQError as err:
+ raise UpdateFailed(str(err)) from err
+
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name="myq devices",
- update_method=myq.update_device_info,
+ update_method=async_update_data,
update_interval=timedelta(seconds=UPDATE_INTERVAL),
)
hass.data[DOMAIN][entry.entry_id] = {MYQ_GATEWAY: myq, MYQ_COORDINATOR: coordinator}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -63,8 +71,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/myq/binary_sensor.py b/homeassistant/components/myq/binary_sensor.py
index 57bd2451d2ab2c..e3832458b9bd2c 100644
--- a/homeassistant/components/myq/binary_sensor.py
+++ b/homeassistant/components/myq/binary_sensor.py
@@ -1,7 +1,5 @@
"""Support for MyQ gateways."""
from pymyq.const import (
- DEVICE_FAMILY as MYQ_DEVICE_FAMILY,
- DEVICE_FAMILY_GATEWAY as MYQ_DEVICE_FAMILY_GATEWAY,
DEVICE_STATE as MYQ_DEVICE_STATE,
DEVICE_STATE_ONLINE as MYQ_DEVICE_STATE_ONLINE,
KNOWN_MODELS,
@@ -25,9 +23,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities = []
- for device in myq.devices.values():
- if device.device_json[MYQ_DEVICE_FAMILY] == MYQ_DEVICE_FAMILY_GATEWAY:
- entities.append(MyQBinarySensorEntity(coordinator, device))
+ for device in myq.gateways.values():
+ entities.append(MyQBinarySensorEntity(coordinator, device))
async_add_entities(entities, True)
diff --git a/homeassistant/components/myq/config_flow.py b/homeassistant/components/myq/config_flow.py
index 352283607d819f..17c98195a4ed84 100644
--- a/homeassistant/components/myq/config_flow.py
+++ b/homeassistant/components/myq/config_flow.py
@@ -9,7 +9,7 @@
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import aiohttp_client
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/myq/const.py b/homeassistant/components/myq/const.py
index 9251bce7447ea3..6189b1601eaa1f 100644
--- a/homeassistant/components/myq/const.py
+++ b/homeassistant/components/myq/const.py
@@ -1,9 +1,9 @@
"""The MyQ integration."""
-from pymyq.device import (
- STATE_CLOSED as MYQ_STATE_CLOSED,
- STATE_CLOSING as MYQ_STATE_CLOSING,
- STATE_OPEN as MYQ_STATE_OPEN,
- STATE_OPENING as MYQ_STATE_OPENING,
+from pymyq.garagedoor import (
+ STATE_CLOSED as MYQ_COVER_STATE_CLOSED,
+ STATE_CLOSING as MYQ_COVER_STATE_CLOSING,
+ STATE_OPEN as MYQ_COVER_STATE_OPEN,
+ STATE_OPENING as MYQ_COVER_STATE_OPENING,
)
from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING
@@ -13,10 +13,10 @@
PLATFORMS = ["cover", "binary_sensor"]
MYQ_TO_HASS = {
- MYQ_STATE_CLOSED: STATE_CLOSED,
- MYQ_STATE_CLOSING: STATE_CLOSING,
- MYQ_STATE_OPEN: STATE_OPEN,
- MYQ_STATE_OPENING: STATE_OPENING,
+ MYQ_COVER_STATE_CLOSED: STATE_CLOSED,
+ MYQ_COVER_STATE_CLOSING: STATE_CLOSING,
+ MYQ_COVER_STATE_OPEN: STATE_OPEN,
+ MYQ_COVER_STATE_OPENING: STATE_OPENING,
}
MYQ_GATEWAY = "myq_gateway"
@@ -24,7 +24,7 @@
# myq has some ratelimits in place
# and 61 seemed to be work every time
-UPDATE_INTERVAL = 61
+UPDATE_INTERVAL = 15
# Estimated time it takes myq to start transition from one
# state to the next.
diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py
index 6fef6b25bab22c..e26a969e724bde 100644
--- a/homeassistant/components/myq/cover.py
+++ b/homeassistant/components/myq/cover.py
@@ -1,14 +1,14 @@
"""Support for MyQ-Enabled Garage Doors."""
-import time
+import logging
from pymyq.const import (
DEVICE_STATE as MYQ_DEVICE_STATE,
DEVICE_STATE_ONLINE as MYQ_DEVICE_STATE_ONLINE,
- DEVICE_TYPE as MYQ_DEVICE_TYPE,
DEVICE_TYPE_GATE as MYQ_DEVICE_TYPE_GATE,
KNOWN_MODELS,
MANUFACTURER,
)
+from pymyq.errors import MyQError
from homeassistant.components.cover import (
DEVICE_CLASS_GARAGE,
@@ -17,19 +17,12 @@
SUPPORT_OPEN,
CoverEntity,
)
-from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING
-from homeassistant.core import callback
-from homeassistant.helpers.event import async_call_later
+from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING
from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from .const import (
- DOMAIN,
- MYQ_COORDINATOR,
- MYQ_GATEWAY,
- MYQ_TO_HASS,
- TRANSITION_COMPLETE_DURATION,
- TRANSITION_START_DURATION,
-)
+from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, MYQ_TO_HASS
+
+_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -50,13 +43,11 @@ def __init__(self, coordinator, device):
"""Initialize with API object, device id."""
super().__init__(coordinator)
self._device = device
- self._last_action_timestamp = 0
- self._scheduled_transition_update = None
@property
def device_class(self):
"""Define this cover as a garage door."""
- device_type = self._device.device_json.get(MYQ_DEVICE_TYPE)
+ device_type = self._device.device_type
if device_type is not None and device_type == MYQ_DEVICE_TYPE_GATE:
return DEVICE_CLASS_GATE
return DEVICE_CLASS_GARAGE
@@ -87,6 +78,11 @@ def is_closing(self):
"""Return if the cover is closing or not."""
return MYQ_TO_HASS.get(self._device.state) == STATE_CLOSING
+ @property
+ def is_open(self):
+ """Return if the cover is opening or not."""
+ return MYQ_TO_HASS.get(self._device.state) == STATE_OPEN
+
@property
def is_opening(self):
"""Return if the cover is opening or not."""
@@ -104,37 +100,48 @@ def unique_id(self):
async def async_close_cover(self, **kwargs):
"""Issue close command to cover."""
- self._last_action_timestamp = time.time()
- await self._device.close()
- self._async_schedule_update_for_transition()
+ if self.is_closing or self.is_closed:
+ return
+
+ try:
+ wait_task = await self._device.close(wait_for_state=False)
+ except MyQError as err:
+ _LOGGER.error(
+ "Closing of cover %s failed with error: %s", self._device.name, str(err)
+ )
+
+ return
+
+ # Write closing state to HASS
+ self.async_write_ha_state()
+
+ if not await wait_task:
+ _LOGGER.error("Closing of cover %s failed", self._device.name)
+
+ # Write final state to HASS
+ self.async_write_ha_state()
async def async_open_cover(self, **kwargs):
"""Issue open command to cover."""
- self._last_action_timestamp = time.time()
- await self._device.open()
- self._async_schedule_update_for_transition()
+ if self.is_opening or self.is_open:
+ return
- @callback
- def _async_schedule_update_for_transition(self):
+ try:
+ wait_task = await self._device.open(wait_for_state=False)
+ except MyQError as err:
+ _LOGGER.error(
+ "Opening of cover %s failed with error: %s", self._device.name, str(err)
+ )
+ return
+
+ # Write opening state to HASS
self.async_write_ha_state()
- # Cancel any previous updates
- if self._scheduled_transition_update:
- self._scheduled_transition_update()
-
- # Schedule an update for when we expect the transition
- # to be completed so the garage door or gate does not
- # seem like its closing or opening for a long time
- self._scheduled_transition_update = async_call_later(
- self.hass,
- TRANSITION_COMPLETE_DURATION,
- self._async_complete_schedule_update,
- )
+ if not await wait_task:
+ _LOGGER.error("Opening of cover %s failed", self._device.name)
- async def _async_complete_schedule_update(self, _):
- """Update status of the cover via coordinator."""
- self._scheduled_transition_update = None
- await self.coordinator.async_request_refresh()
+ # Write final state to HASS
+ self.async_write_ha_state()
@property
def device_info(self):
@@ -152,22 +159,8 @@ def device_info(self):
device_info["via_device"] = (DOMAIN, self._device.parent_device_id)
return device_info
- @callback
- def _async_consume_update(self):
- if time.time() - self._last_action_timestamp <= TRANSITION_START_DURATION:
- # If we just started a transition we need
- # to prevent a bouncy state
- return
-
- self.async_write_ha_state()
-
async def async_added_to_hass(self):
"""Subscribe to updates."""
self.async_on_remove(
- self.coordinator.async_add_listener(self._async_consume_update)
+ self.coordinator.async_add_listener(self.async_write_ha_state)
)
-
- async def async_will_remove_from_hass(self):
- """Undo subscription."""
- if self._scheduled_transition_update:
- self._scheduled_transition_update()
diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json
index aba2f24b5bd1ec..2098480af523be 100644
--- a/homeassistant/components/myq/manifest.json
+++ b/homeassistant/components/myq/manifest.json
@@ -2,7 +2,7 @@
"domain": "myq",
"name": "MyQ",
"documentation": "https://www.home-assistant.io/integrations/myq",
- "requirements": ["pymyq==2.0.14"],
+ "requirements": ["pymyq==3.0.4"],
"codeowners": ["@bdraco"],
"config_flow": true,
"homekit": {
diff --git a/homeassistant/components/myq/translations/de.json b/homeassistant/components/myq/translations/de.json
index d5c890e41699e6..fafa38c7817646 100644
--- a/homeassistant/components/myq/translations/de.json
+++ b/homeassistant/components/myq/translations/de.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "MyQ ist bereits konfiguriert"
+ "already_configured": "Der Dienst ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
diff --git a/homeassistant/components/myq/translations/he.json b/homeassistant/components/myq/translations/he.json
new file mode 100644
index 00000000000000..ac90b3264eab33
--- /dev/null
+++ b/homeassistant/components/myq/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/myq/translations/hu.json b/homeassistant/components/myq/translations/hu.json
index dee4ed9ee0fa4d..9c5b90e744753a 100644
--- a/homeassistant/components/myq/translations/hu.json
+++ b/homeassistant/components/myq/translations/hu.json
@@ -1,5 +1,13 @@
{
"config": {
+ "abort": {
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/myq/translations/id.json b/homeassistant/components/myq/translations/id.json
new file mode 100644
index 00000000000000..2cc790d15e0da8
--- /dev/null
+++ b/homeassistant/components/myq/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "title": "Hubungkan ke MyQ Gateway"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/myq/translations/ko.json b/homeassistant/components/myq/translations/ko.json
index 31e3f8646e629f..23ba2eecea7022 100644
--- a/homeassistant/components/myq/translations/ko.json
+++ b/homeassistant/components/myq/translations/ko.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "MyQ \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
diff --git a/homeassistant/components/myq/translations/nl.json b/homeassistant/components/myq/translations/nl.json
index fd6310cce6a735..65df320a544ef4 100644
--- a/homeassistant/components/myq/translations/nl.json
+++ b/homeassistant/components/myq/translations/nl.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "MyQ is al geconfigureerd"
+ "already_configured": "Service is al geconfigureerd"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
diff --git a/homeassistant/components/myq/translations/ru.json b/homeassistant/components/myq/translations/ru.json
index daa3148beef481..c88db7d696094a 100644
--- a/homeassistant/components/myq/translations/ru.json
+++ b/homeassistant/components/myq/translations/ru.json
@@ -5,14 +5,14 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "MyQ"
}
diff --git a/homeassistant/components/myq/translations/sv.json b/homeassistant/components/myq/translations/sv.json
index 1243ca600f0d83..da06f32aa924b8 100644
--- a/homeassistant/components/myq/translations/sv.json
+++ b/homeassistant/components/myq/translations/sv.json
@@ -1,6 +1,10 @@
{
"config": {
+ "abort": {
+ "already_configured": "Tj\u00e4nsten har redan konfigurerats"
+ },
"error": {
+ "cannot_connect": "Anslutningen misslyckades",
"invalid_auth": "Ogiltig autentisering",
"unknown": "Ov\u00e4ntat fel"
},
@@ -9,7 +13,8 @@
"data": {
"password": "L\u00f6senord",
"username": "Anv\u00e4ndarnamn"
- }
+ },
+ "title": "Anslut till MyQ Gateway"
}
}
}
diff --git a/homeassistant/components/myq/translations/tr.json b/homeassistant/components/myq/translations/tr.json
new file mode 100644
index 00000000000000..7347d18bc34283
--- /dev/null
+++ b/homeassistant/components/myq/translations/tr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ },
+ "title": "MyQ A\u011f Ge\u00e7idine ba\u011flan\u0131n"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/myq/translations/uk.json b/homeassistant/components/myq/translations/uk.json
new file mode 100644
index 00000000000000..12f8406de12b02
--- /dev/null
+++ b/homeassistant/components/myq/translations/uk.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "MyQ"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py
index 43e398b142fd2e..c9ad496762dce0 100644
--- a/homeassistant/components/mysensors/__init__.py
+++ b/homeassistant/components/mysensors/__init__.py
@@ -1,12 +1,25 @@
"""Connect to a MySensors gateway via pymysensors API."""
+from __future__ import annotations
+
+import asyncio
+from functools import partial
import logging
+from typing import Callable
+from mysensors import BaseAsyncGateway
import voluptuous as vol
+from homeassistant import config_entries
+from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN
from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_topic
+from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_OPTIMISTIC
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.discovery import async_load_platform
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from .const import (
ATTR_DEVICES,
@@ -22,16 +35,24 @@
CONF_TOPIC_OUT_PREFIX,
CONF_VERSION,
DOMAIN,
+ MYSENSORS_DISCOVERY,
MYSENSORS_GATEWAYS,
+ MYSENSORS_ON_UNLOAD,
+ PLATFORMS_WITH_ENTRY_SUPPORT,
+ DevId,
+ SensorType,
)
-from .device import get_mysensors_devices
-from .gateway import finish_setup, get_mysensors_gateway, setup_gateways
+from .device import MySensorsDevice, MySensorsEntity, get_mysensors_devices
+from .gateway import finish_setup, get_mysensors_gateway, gw_stop, setup_gateway
+from .helpers import on_unload
_LOGGER = logging.getLogger(__name__)
CONF_DEBUG = "debug"
CONF_NODE_NAME = "name"
+DATA_HASS_CONFIG = "hass_config"
+
DEFAULT_BAUD_RATE = 115200
DEFAULT_TCP_PORT = 5003
DEFAULT_VERSION = "1.4"
@@ -81,29 +102,38 @@ def validator(config):
NODE_SCHEMA = vol.Schema({cv.positive_int: {vol.Required(CONF_NODE_NAME): cv.string}})
-GATEWAY_SCHEMA = {
- vol.Required(CONF_DEVICE): cv.string,
- vol.Optional(CONF_PERSISTENCE_FILE): vol.All(cv.string, is_persistence_file),
- vol.Optional(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE): cv.positive_int,
- vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port,
- vol.Optional(CONF_TOPIC_IN_PREFIX): valid_subscribe_topic,
- vol.Optional(CONF_TOPIC_OUT_PREFIX): valid_publish_topic,
- vol.Optional(CONF_NODES, default={}): NODE_SCHEMA,
-}
+GATEWAY_SCHEMA = vol.Schema(
+ vol.All(
+ deprecated(CONF_NODES),
+ {
+ vol.Required(CONF_DEVICE): cv.string,
+ vol.Optional(CONF_PERSISTENCE_FILE): vol.All(
+ cv.string, is_persistence_file
+ ),
+ vol.Optional(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE): cv.positive_int,
+ vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port,
+ vol.Optional(CONF_TOPIC_IN_PREFIX): valid_subscribe_topic,
+ vol.Optional(CONF_TOPIC_OUT_PREFIX): valid_publish_topic,
+ vol.Optional(CONF_NODES, default={}): NODE_SCHEMA,
+ },
+ )
+)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
vol.All(
deprecated(CONF_DEBUG),
+ deprecated(CONF_OPTIMISTIC),
+ deprecated(CONF_PERSISTENCE),
{
vol.Required(CONF_GATEWAYS): vol.All(
cv.ensure_list, has_all_unique_files, [GATEWAY_SCHEMA]
),
- vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
- vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean,
vol.Optional(CONF_RETAIN, default=True): cv.boolean,
vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string,
+ vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
+ vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean,
},
)
)
@@ -112,69 +142,170 @@ def validator(config):
)
-async def async_setup(hass, config):
+async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
"""Set up the MySensors component."""
- gateways = await setup_gateways(hass, config)
+ hass.data[DOMAIN] = {DATA_HASS_CONFIG: config}
+
+ if DOMAIN not in config or bool(hass.config_entries.async_entries(DOMAIN)):
+ return True
+
+ config = config[DOMAIN]
+ user_inputs = [
+ {
+ CONF_DEVICE: gw[CONF_DEVICE],
+ CONF_BAUD_RATE: gw[CONF_BAUD_RATE],
+ CONF_TCP_PORT: gw[CONF_TCP_PORT],
+ CONF_TOPIC_OUT_PREFIX: gw.get(CONF_TOPIC_OUT_PREFIX, ""),
+ CONF_TOPIC_IN_PREFIX: gw.get(CONF_TOPIC_IN_PREFIX, ""),
+ CONF_RETAIN: config[CONF_RETAIN],
+ CONF_VERSION: config[CONF_VERSION],
+ CONF_PERSISTENCE_FILE: gw.get(CONF_PERSISTENCE_FILE)
+ # nodes config ignored at this time. renaming nodes can now be done from the frontend.
+ }
+ for gw in config[CONF_GATEWAYS]
+ ]
+ user_inputs = [
+ {k: v for k, v in userinput.items() if v is not None}
+ for userinput in user_inputs
+ ]
- if not gateways:
- _LOGGER.error("No devices could be setup as gateways, check your configuration")
+ # there is an actual configuration in configuration.yaml, so we have to process it
+ for user_input in user_inputs:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data=user_input,
+ )
+ )
+
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+ """Set up an instance of the MySensors integration.
+
+ Every instance has a connection to exactly one Gateway.
+ """
+ gateway = await setup_gateway(hass, entry)
+
+ if not gateway:
+ _LOGGER.error("Gateway setup failed for %s", entry.data)
return False
- hass.data[MYSENSORS_GATEWAYS] = gateways
+ if MYSENSORS_GATEWAYS not in hass.data[DOMAIN]:
+ hass.data[DOMAIN][MYSENSORS_GATEWAYS] = {}
+ hass.data[DOMAIN][MYSENSORS_GATEWAYS][entry.entry_id] = gateway
- hass.async_create_task(finish_setup(hass, config, gateways))
+ # Connect notify discovery as that integration doesn't support entry forwarding.
+ # Allow loading device tracker platform via discovery
+ # until refactor to config entry is done.
+
+ for platform in (DEVICE_TRACKER_DOMAIN, NOTIFY_DOMAIN):
+ load_discovery_platform = partial(
+ async_load_platform,
+ hass,
+ platform,
+ DOMAIN,
+ hass_config=hass.data[DOMAIN][DATA_HASS_CONFIG],
+ )
+
+ await on_unload(
+ hass,
+ entry.entry_id,
+ async_dispatcher_connect(
+ hass,
+ MYSENSORS_DISCOVERY.format(entry.entry_id, platform),
+ load_discovery_platform,
+ ),
+ )
+
+ async def finish() -> None:
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ for platform in PLATFORMS_WITH_ENTRY_SUPPORT
+ ]
+ )
+ await finish_setup(hass, entry, gateway)
+
+ hass.async_create_task(finish())
return True
-def _get_mysensors_name(gateway, node_id, child_id):
- """Return a name for a node child."""
- node_name = f"{gateway.sensors[node_id].sketch_name} {node_id}"
- node_name = next(
- (
- node[CONF_NODE_NAME]
- for conf_id, node in gateway.nodes_config.items()
- if node.get(CONF_NODE_NAME) is not None and conf_id == node_id
- ),
- node_name,
+async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+ """Remove an instance of the MySensors integration."""
+
+ gateway = get_mysensors_gateway(hass, entry.entry_id)
+
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS_WITH_ENTRY_SUPPORT
+ ]
+ )
)
- return f"{node_name} {child_id}"
+ if not unload_ok:
+ return False
+
+ key = MYSENSORS_ON_UNLOAD.format(entry.entry_id)
+ if key in hass.data[DOMAIN]:
+ for fnct in hass.data[DOMAIN][key]:
+ fnct()
+
+ hass.data[DOMAIN].pop(key)
+
+ del hass.data[DOMAIN][MYSENSORS_GATEWAYS][entry.entry_id]
+
+ await gw_stop(hass, entry, gateway)
+ return True
@callback
def setup_mysensors_platform(
- hass,
- domain,
- discovery_info,
- device_class,
- device_args=None,
- async_add_entities=None,
-):
- """Set up a MySensors platform."""
- # Only act if called via MySensors by discovery event.
- # Otherwise gateway is not set up.
- if not discovery_info:
- return None
+ hass: HomeAssistant,
+ domain: str, # hass platform name
+ discovery_info: dict[str, list[DevId]],
+ device_class: type[MySensorsDevice] | dict[SensorType, type[MySensorsEntity]],
+ device_args: (
+ None | tuple
+ ) = None, # extra arguments that will be given to the entity constructor
+ async_add_entities: Callable | None = None,
+) -> list[MySensorsDevice] | None:
+ """Set up a MySensors platform.
+
+ Sets up a bunch of instances of a single platform that is supported by this integration.
+ The function is given a list of device ids, each one describing an instance to set up.
+ The function is also given a class.
+ A new instance of the class is created for every device id, and the device id is given to the constructor of the class
+ """
if device_args is None:
device_args = ()
- new_devices = []
- new_dev_ids = discovery_info[ATTR_DEVICES]
+ new_devices: list[MySensorsDevice] = []
+ new_dev_ids: list[DevId] = discovery_info[ATTR_DEVICES]
for dev_id in new_dev_ids:
- devices = get_mysensors_devices(hass, domain)
+ devices: dict[DevId, MySensorsDevice] = get_mysensors_devices(hass, domain)
if dev_id in devices:
+ _LOGGER.debug(
+ "Skipping setup of %s for platform %s as it already exists",
+ dev_id,
+ domain,
+ )
continue
gateway_id, node_id, child_id, value_type = dev_id
- gateway = get_mysensors_gateway(hass, gateway_id)
+ gateway: BaseAsyncGateway | None = get_mysensors_gateway(hass, gateway_id)
if not gateway:
+ _LOGGER.warning("Skipping setup of %s, no gateway found", dev_id)
continue
device_class_copy = device_class
if isinstance(device_class, dict):
child = gateway.sensors[node_id].children[child_id]
s_type = gateway.const.Presentation(child.type).name
device_class_copy = device_class[s_type]
- name = _get_mysensors_name(gateway, node_id, child_id)
- args_copy = (*device_args, gateway, node_id, child_id, name, value_type)
+ args_copy = (*device_args, gateway_id, gateway, node_id, child_id, value_type)
devices[dev_id] = device_class_copy(*args_copy)
new_devices.append(devices[dev_id])
if new_devices:
diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py
index 4ec3c6e0abd65c..c4e12d170c01ec 100644
--- a/homeassistant/components/mysensors/binary_sensor.py
+++ b/homeassistant/components/mysensors/binary_sensor.py
@@ -1,4 +1,6 @@
"""Support for MySensors binary sensors."""
+from typing import Callable
+
from homeassistant.components import mysensors
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_MOISTURE,
@@ -10,7 +12,13 @@
DOMAIN,
BinarySensorEntity,
)
+from homeassistant.components.mysensors import on_unload
+from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_ON
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.typing import HomeAssistantType
SENSORS = {
"S_DOOR": "door",
@@ -24,14 +32,30 @@
}
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the mysensors platform for binary sensors."""
- mysensors.setup_mysensors_platform(
+async def async_setup_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
+):
+ """Set up this platform for a specific ConfigEntry(==Gateway)."""
+
+ @callback
+ def async_discover(discovery_info):
+ """Discover and add a MySensors binary_sensor."""
+ mysensors.setup_mysensors_platform(
+ hass,
+ DOMAIN,
+ discovery_info,
+ MySensorsBinarySensor,
+ async_add_entities=async_add_entities,
+ )
+
+ await on_unload(
hass,
- DOMAIN,
- discovery_info,
- MySensorsBinarySensor,
- async_add_entities=async_add_entities,
+ config_entry,
+ async_dispatcher_connect(
+ hass,
+ MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
+ async_discover,
+ ),
)
diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py
index c318ccf7ec614f..b1916fc4ed104c 100644
--- a/homeassistant/components/mysensors/climate.py
+++ b/homeassistant/components/mysensors/climate.py
@@ -1,4 +1,6 @@
"""MySensors platform that offers a Climate (MySensors-HVAC) component."""
+from typing import Callable
+
from homeassistant.components import mysensors
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
@@ -13,7 +15,12 @@
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
+from homeassistant.components.mysensors import on_unload
+from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.typing import HomeAssistantType
DICT_HA_TO_MYS = {
HVAC_MODE_AUTO: "AutoChangeOver",
@@ -32,14 +39,29 @@
OPERATION_LIST = [HVAC_MODE_OFF, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT]
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the mysensors climate."""
- mysensors.setup_mysensors_platform(
+async def async_setup_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
+):
+ """Set up this platform for a specific ConfigEntry(==Gateway)."""
+
+ async def async_discover(discovery_info):
+ """Discover and add a MySensors climate."""
+ mysensors.setup_mysensors_platform(
+ hass,
+ DOMAIN,
+ discovery_info,
+ MySensorsHVAC,
+ async_add_entities=async_add_entities,
+ )
+
+ await on_unload(
hass,
- DOMAIN,
- discovery_info,
- MySensorsHVAC,
- async_add_entities=async_add_entities,
+ config_entry,
+ async_dispatcher_connect(
+ hass,
+ MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
+ async_discover,
+ ),
)
@@ -62,15 +84,10 @@ def supported_features(self):
features = features | SUPPORT_TARGET_TEMPERATURE
return features
- @property
- def assumed_state(self):
- """Return True if unable to access real state of entity."""
- return self.gateway.optimistic
-
@property
def temperature_unit(self):
"""Return the unit of measurement."""
- return TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT
+ return TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT
@property
def current_temperature(self):
@@ -159,7 +176,7 @@ async def async_set_temperature(self, **kwargs):
self.gateway.set_child_value(
self.node_id, self.child_id, value_type, value, ack=1
)
- if self.gateway.optimistic:
+ if self.assumed_state:
# Optimistically assume that device has changed state
self._values[value_type] = value
self.async_write_ha_state()
@@ -170,7 +187,7 @@ async def async_set_fan_mode(self, fan_mode):
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan_mode, ack=1
)
- if self.gateway.optimistic:
+ if self.assumed_state:
# Optimistically assume that device has changed state
self._values[set_req.V_HVAC_SPEED] = fan_mode
self.async_write_ha_state()
@@ -184,7 +201,7 @@ async def async_set_hvac_mode(self, hvac_mode):
DICT_HA_TO_MYS[hvac_mode],
ack=1,
)
- if self.gateway.optimistic:
+ if self.assumed_state:
# Optimistically assume that device has changed state
self._values[self.value_type] = hvac_mode
self.async_write_ha_state()
diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py
new file mode 100644
index 00000000000000..bdf1b9392a82e1
--- /dev/null
+++ b/homeassistant/components/mysensors/config_flow.py
@@ -0,0 +1,338 @@
+"""Config flow for MySensors."""
+from __future__ import annotations
+
+from contextlib import suppress
+import logging
+import os
+from typing import Any
+
+from awesomeversion import (
+ AwesomeVersion,
+ AwesomeVersionStrategy,
+ AwesomeVersionStrategyException,
+)
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_topic
+from homeassistant.components.mysensors import (
+ CONF_DEVICE,
+ DEFAULT_BAUD_RATE,
+ DEFAULT_TCP_PORT,
+ is_persistence_file,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import callback
+import homeassistant.helpers.config_validation as cv
+
+from . import CONF_RETAIN, CONF_VERSION, DEFAULT_VERSION
+from .const import (
+ CONF_BAUD_RATE,
+ CONF_GATEWAY_TYPE,
+ CONF_GATEWAY_TYPE_ALL,
+ CONF_GATEWAY_TYPE_MQTT,
+ CONF_GATEWAY_TYPE_SERIAL,
+ CONF_GATEWAY_TYPE_TCP,
+ CONF_PERSISTENCE_FILE,
+ CONF_TCP_PORT,
+ CONF_TOPIC_IN_PREFIX,
+ CONF_TOPIC_OUT_PREFIX,
+ DOMAIN,
+ ConfGatewayType,
+)
+from .gateway import MQTT_COMPONENT, is_serial_port, is_socket_address, try_connect
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def _get_schema_common(user_input: dict[str, str]) -> dict:
+ """Create a schema with options common to all gateway types."""
+ schema = {
+ vol.Required(
+ CONF_VERSION,
+ default="",
+ description={
+ "suggested_value": user_input.get(CONF_VERSION, DEFAULT_VERSION)
+ },
+ ): str,
+ vol.Optional(CONF_PERSISTENCE_FILE): str,
+ }
+ return schema
+
+
+def _validate_version(version: str) -> dict[str, str]:
+ """Validate a version string from the user."""
+ version_okay = False
+ with suppress(AwesomeVersionStrategyException):
+ version_okay = bool(
+ AwesomeVersion.ensure_strategy(
+ version,
+ [AwesomeVersionStrategy.SIMPLEVER, AwesomeVersionStrategy.SEMVER],
+ )
+ )
+
+ if version_okay:
+ return {}
+ return {CONF_VERSION: "invalid_version"}
+
+
+def _is_same_device(
+ gw_type: ConfGatewayType, user_input: dict[str, str], entry: ConfigEntry
+):
+ """Check if another ConfigDevice is actually the same as user_input.
+
+ This function only compares addresses and tcp ports, so it is possible to fool it with tricks like port forwarding.
+ """
+ if entry.data[CONF_DEVICE] != user_input[CONF_DEVICE]:
+ return False
+ if gw_type == CONF_GATEWAY_TYPE_TCP:
+ return entry.data[CONF_TCP_PORT] == user_input[CONF_TCP_PORT]
+ if gw_type == CONF_GATEWAY_TYPE_MQTT:
+ entry_topics = {
+ entry.data[CONF_TOPIC_IN_PREFIX],
+ entry.data[CONF_TOPIC_OUT_PREFIX],
+ }
+ return (
+ user_input.get(CONF_TOPIC_IN_PREFIX) in entry_topics
+ or user_input.get(CONF_TOPIC_OUT_PREFIX) in entry_topics
+ )
+ return True
+
+
+class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow."""
+
+ def __init__(self) -> None:
+ """Set up config flow."""
+ self._gw_type: str | None = None
+
+ async def async_step_import(self, user_input: dict[str, str] | None = None):
+ """Import a config entry.
+
+ This method is called by async_setup and it has already
+ prepared the dict to be compatible with what a user would have
+ entered from the frontend.
+ Therefore we process it as though it came from the frontend.
+ """
+ if user_input[CONF_DEVICE] == MQTT_COMPONENT:
+ user_input[CONF_GATEWAY_TYPE] = CONF_GATEWAY_TYPE_MQTT
+ else:
+ try:
+ await self.hass.async_add_executor_job(
+ is_serial_port, user_input[CONF_DEVICE]
+ )
+ except vol.Invalid:
+ user_input[CONF_GATEWAY_TYPE] = CONF_GATEWAY_TYPE_TCP
+ else:
+ user_input[CONF_GATEWAY_TYPE] = CONF_GATEWAY_TYPE_SERIAL
+
+ result: dict[str, Any] = await self.async_step_user(user_input=user_input)
+ if result["type"] == "form":
+ return self.async_abort(reason=next(iter(result["errors"].values())))
+ return result
+
+ async def async_step_user(self, user_input: dict[str, str] | None = None):
+ """Create a config entry from frontend user input."""
+ schema = {vol.Required(CONF_GATEWAY_TYPE): vol.In(CONF_GATEWAY_TYPE_ALL)}
+ schema = vol.Schema(schema)
+
+ if user_input is not None:
+ gw_type = self._gw_type = user_input[CONF_GATEWAY_TYPE]
+ input_pass = user_input if CONF_DEVICE in user_input else None
+ if gw_type == CONF_GATEWAY_TYPE_MQTT:
+ return await self.async_step_gw_mqtt(input_pass)
+ if gw_type == CONF_GATEWAY_TYPE_TCP:
+ return await self.async_step_gw_tcp(input_pass)
+ if gw_type == CONF_GATEWAY_TYPE_SERIAL:
+ return await self.async_step_gw_serial(input_pass)
+
+ return self.async_show_form(step_id="user", data_schema=schema)
+
+ async def async_step_gw_serial(self, user_input: dict[str, str] | None = None):
+ """Create config entry for a serial gateway."""
+ errors = {}
+ if user_input is not None:
+ errors.update(
+ await self.validate_common(CONF_GATEWAY_TYPE_SERIAL, errors, user_input)
+ )
+ if not errors:
+ return self._async_create_entry(user_input)
+
+ user_input = user_input or {}
+ schema = _get_schema_common(user_input)
+ schema[
+ vol.Required(
+ CONF_BAUD_RATE,
+ default=user_input.get(CONF_BAUD_RATE, DEFAULT_BAUD_RATE),
+ )
+ ] = cv.positive_int
+ schema[
+ vol.Required(
+ CONF_DEVICE, default=user_input.get(CONF_DEVICE, "/dev/ttyACM0")
+ )
+ ] = str
+
+ schema = vol.Schema(schema)
+ return self.async_show_form(
+ step_id="gw_serial", data_schema=schema, errors=errors
+ )
+
+ async def async_step_gw_tcp(self, user_input: dict[str, str] | None = None):
+ """Create a config entry for a tcp gateway."""
+ errors = {}
+ if user_input is not None:
+ if CONF_TCP_PORT in user_input:
+ port: int = user_input[CONF_TCP_PORT]
+ if not (0 < port <= 65535):
+ errors[CONF_TCP_PORT] = "port_out_of_range"
+
+ errors.update(
+ await self.validate_common(CONF_GATEWAY_TYPE_TCP, errors, user_input)
+ )
+ if not errors:
+ return self._async_create_entry(user_input)
+
+ user_input = user_input or {}
+ schema = _get_schema_common(user_input)
+ schema[
+ vol.Required(CONF_DEVICE, default=user_input.get(CONF_DEVICE, "127.0.0.1"))
+ ] = str
+ # Don't use cv.port as that would show a slider *facepalm*
+ schema[
+ vol.Optional(
+ CONF_TCP_PORT, default=user_input.get(CONF_TCP_PORT, DEFAULT_TCP_PORT)
+ )
+ ] = vol.Coerce(int)
+
+ schema = vol.Schema(schema)
+ return self.async_show_form(step_id="gw_tcp", data_schema=schema, errors=errors)
+
+ def _check_topic_exists(self, topic: str) -> bool:
+ for other_config in self.hass.config_entries.async_entries(DOMAIN):
+ if topic == other_config.data.get(
+ CONF_TOPIC_IN_PREFIX
+ ) or topic == other_config.data.get(CONF_TOPIC_OUT_PREFIX):
+ return True
+ return False
+
+ async def async_step_gw_mqtt(self, user_input: dict[str, str] | None = None):
+ """Create a config entry for a mqtt gateway."""
+ errors = {}
+ if user_input is not None:
+ user_input[CONF_DEVICE] = MQTT_COMPONENT
+
+ try:
+ valid_subscribe_topic(user_input[CONF_TOPIC_IN_PREFIX])
+ except vol.Invalid:
+ errors[CONF_TOPIC_IN_PREFIX] = "invalid_subscribe_topic"
+ else:
+ if self._check_topic_exists(user_input[CONF_TOPIC_IN_PREFIX]):
+ errors[CONF_TOPIC_IN_PREFIX] = "duplicate_topic"
+
+ try:
+ valid_publish_topic(user_input[CONF_TOPIC_OUT_PREFIX])
+ except vol.Invalid:
+ errors[CONF_TOPIC_OUT_PREFIX] = "invalid_publish_topic"
+ if not errors:
+ if (
+ user_input[CONF_TOPIC_IN_PREFIX]
+ == user_input[CONF_TOPIC_OUT_PREFIX]
+ ):
+ errors[CONF_TOPIC_OUT_PREFIX] = "same_topic"
+ elif self._check_topic_exists(user_input[CONF_TOPIC_OUT_PREFIX]):
+ errors[CONF_TOPIC_OUT_PREFIX] = "duplicate_topic"
+
+ errors.update(
+ await self.validate_common(CONF_GATEWAY_TYPE_MQTT, errors, user_input)
+ )
+ if not errors:
+ return self._async_create_entry(user_input)
+
+ user_input = user_input or {}
+ schema = _get_schema_common(user_input)
+ schema[
+ vol.Required(CONF_RETAIN, default=user_input.get(CONF_RETAIN, True))
+ ] = bool
+ schema[
+ vol.Required(
+ CONF_TOPIC_IN_PREFIX, default=user_input.get(CONF_TOPIC_IN_PREFIX, "")
+ )
+ ] = str
+ schema[
+ vol.Required(
+ CONF_TOPIC_OUT_PREFIX, default=user_input.get(CONF_TOPIC_OUT_PREFIX, "")
+ )
+ ] = str
+
+ schema = vol.Schema(schema)
+ return self.async_show_form(
+ step_id="gw_mqtt", data_schema=schema, errors=errors
+ )
+
+ @callback
+ def _async_create_entry(
+ self, user_input: dict[str, str] | None = None
+ ) -> dict[str, Any]:
+ """Create the config entry."""
+ return self.async_create_entry(
+ title=f"{user_input[CONF_DEVICE]}",
+ data={**user_input, CONF_GATEWAY_TYPE: self._gw_type},
+ )
+
+ def _normalize_persistence_file(self, path: str) -> str:
+ return os.path.realpath(os.path.normcase(self.hass.config.path(path)))
+
+ async def validate_common(
+ self,
+ gw_type: ConfGatewayType,
+ errors: dict[str, str],
+ user_input: dict[str, str] | None = None,
+ ) -> dict[str, str]:
+ """Validate parameters common to all gateway types."""
+ if user_input is not None:
+ errors.update(_validate_version(user_input.get(CONF_VERSION)))
+
+ if gw_type != CONF_GATEWAY_TYPE_MQTT:
+ if gw_type == CONF_GATEWAY_TYPE_TCP:
+ verification_func = is_socket_address
+ else:
+ verification_func = is_serial_port
+
+ try:
+ await self.hass.async_add_executor_job(
+ verification_func, user_input.get(CONF_DEVICE)
+ )
+ except vol.Invalid:
+ errors[CONF_DEVICE] = (
+ "invalid_ip"
+ if gw_type == CONF_GATEWAY_TYPE_TCP
+ else "invalid_serial"
+ )
+ if CONF_PERSISTENCE_FILE in user_input:
+ try:
+ is_persistence_file(user_input[CONF_PERSISTENCE_FILE])
+ except vol.Invalid:
+ errors[CONF_PERSISTENCE_FILE] = "invalid_persistence_file"
+ else:
+ real_persistence_path = self._normalize_persistence_file(
+ user_input[CONF_PERSISTENCE_FILE]
+ )
+ for other_entry in self.hass.config_entries.async_entries(DOMAIN):
+ if CONF_PERSISTENCE_FILE not in other_entry.data:
+ continue
+ if real_persistence_path == self._normalize_persistence_file(
+ other_entry.data[CONF_PERSISTENCE_FILE]
+ ):
+ errors[CONF_PERSISTENCE_FILE] = "duplicate_persistence_file"
+ break
+
+ for other_entry in self.hass.config_entries.async_entries(DOMAIN):
+ if _is_same_device(gw_type, user_input, other_entry):
+ errors["base"] = "already_configured"
+ break
+
+ # if no errors so far, try to connect
+ if not errors and not await try_connect(self.hass, user_input):
+ errors["base"] = "cannot_connect"
+
+ return errors
diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py
index ccb646eb47eec6..7a9027d9b724eb 100644
--- a/homeassistant/components/mysensors/const.py
+++ b/homeassistant/components/mysensors/const.py
@@ -1,33 +1,70 @@
"""MySensors constants."""
-from collections import defaultdict
+from __future__ import annotations
-ATTR_DEVICES = "devices"
-
-CONF_BAUD_RATE = "baud_rate"
-CONF_DEVICE = "device"
-CONF_GATEWAYS = "gateways"
-CONF_NODES = "nodes"
-CONF_PERSISTENCE = "persistence"
-CONF_PERSISTENCE_FILE = "persistence_file"
-CONF_RETAIN = "retain"
-CONF_TCP_PORT = "tcp_port"
-CONF_TOPIC_IN_PREFIX = "topic_in_prefix"
-CONF_TOPIC_OUT_PREFIX = "topic_out_prefix"
-CONF_VERSION = "version"
-
-DOMAIN = "mysensors"
-MYSENSORS_GATEWAY_READY = "mysensors_gateway_ready_{}"
-MYSENSORS_GATEWAYS = "mysensors_gateways"
-PLATFORM = "platform"
-SCHEMA = "schema"
-CHILD_CALLBACK = "mysensors_child_callback_{}_{}_{}_{}"
-NODE_CALLBACK = "mysensors_node_callback_{}_{}"
-TYPE = "type"
-UPDATE_DELAY = 0.1
-
-SERVICE_SEND_IR_CODE = "send_ir_code"
-
-BINARY_SENSOR_TYPES = {
+from collections import defaultdict
+from typing import Literal, Tuple
+
+ATTR_DEVICES: str = "devices"
+ATTR_GATEWAY_ID: str = "gateway_id"
+
+CONF_BAUD_RATE: str = "baud_rate"
+CONF_DEVICE: str = "device"
+CONF_GATEWAYS: str = "gateways"
+CONF_NODES: str = "nodes"
+CONF_PERSISTENCE: str = "persistence"
+CONF_PERSISTENCE_FILE: str = "persistence_file"
+CONF_RETAIN: str = "retain"
+CONF_TCP_PORT: str = "tcp_port"
+CONF_TOPIC_IN_PREFIX: str = "topic_in_prefix"
+CONF_TOPIC_OUT_PREFIX: str = "topic_out_prefix"
+CONF_VERSION: str = "version"
+CONF_GATEWAY_TYPE: str = "gateway_type"
+ConfGatewayType = Literal["Serial", "TCP", "MQTT"]
+CONF_GATEWAY_TYPE_SERIAL: ConfGatewayType = "Serial"
+CONF_GATEWAY_TYPE_TCP: ConfGatewayType = "TCP"
+CONF_GATEWAY_TYPE_MQTT: ConfGatewayType = "MQTT"
+CONF_GATEWAY_TYPE_ALL: list[str] = [
+ CONF_GATEWAY_TYPE_MQTT,
+ CONF_GATEWAY_TYPE_SERIAL,
+ CONF_GATEWAY_TYPE_TCP,
+]
+
+
+DOMAIN: str = "mysensors"
+MYSENSORS_GATEWAY_START_TASK: str = "mysensors_gateway_start_task_{}"
+MYSENSORS_GATEWAYS: str = "mysensors_gateways"
+PLATFORM: str = "platform"
+SCHEMA: str = "schema"
+CHILD_CALLBACK: str = "mysensors_child_callback_{}_{}_{}_{}"
+NODE_CALLBACK: str = "mysensors_node_callback_{}_{}"
+MYSENSORS_DISCOVERY = "mysensors_discovery_{}_{}"
+MYSENSORS_ON_UNLOAD = "mysensors_on_unload_{}"
+TYPE: str = "type"
+UPDATE_DELAY: float = 0.1
+
+SERVICE_SEND_IR_CODE: str = "send_ir_code"
+
+SensorType = str
+# S_DOOR, S_MOTION, S_SMOKE, ...
+
+ValueType = str
+# V_TRIPPED, V_ARMED, V_STATUS, V_PERCENTAGE, ...
+
+GatewayId = str
+# a unique id generated by config_flow.py and stored in the ConfigEntry as the entry id.
+#
+# Gateway may be fetched by giving the gateway id to get_mysensors_gateway()
+
+DevId = Tuple[GatewayId, int, int, int]
+# describes the backend of a hass entity. Contents are: GatewayId, node_id, child_id, v_type as int
+#
+# The string version of v_type can be looked up in the enum gateway.const.SetReq of the appropriate BaseAsyncGateway
+# Home Assistant Entities are quite limited and only ever do one thing.
+# MySensors Nodes have multiple child_ids each with a s_type several associated v_types
+# The MySensors integration brings these together by creating an entity for every v_type of every child_id of every node.
+# The DevId tuple perfectly captures this.
+
+BINARY_SENSOR_TYPES: dict[SensorType, set[ValueType]] = {
"S_DOOR": {"V_TRIPPED"},
"S_MOTION": {"V_TRIPPED"},
"S_SMOKE": {"V_TRIPPED"},
@@ -38,21 +75,23 @@
"S_MOISTURE": {"V_TRIPPED"},
}
-CLIMATE_TYPES = {"S_HVAC": {"V_HVAC_FLOW_STATE"}}
+CLIMATE_TYPES: dict[SensorType, set[ValueType]] = {"S_HVAC": {"V_HVAC_FLOW_STATE"}}
-COVER_TYPES = {"S_COVER": {"V_DIMMER", "V_PERCENTAGE", "V_LIGHT", "V_STATUS"}}
+COVER_TYPES: dict[SensorType, set[ValueType]] = {
+ "S_COVER": {"V_DIMMER", "V_PERCENTAGE", "V_LIGHT", "V_STATUS"}
+}
-DEVICE_TRACKER_TYPES = {"S_GPS": {"V_POSITION"}}
+DEVICE_TRACKER_TYPES: dict[SensorType, set[ValueType]] = {"S_GPS": {"V_POSITION"}}
-LIGHT_TYPES = {
+LIGHT_TYPES: dict[SensorType, set[ValueType]] = {
"S_DIMMER": {"V_DIMMER", "V_PERCENTAGE"},
"S_RGB_LIGHT": {"V_RGB"},
"S_RGBW_LIGHT": {"V_RGBW"},
}
-NOTIFY_TYPES = {"S_INFO": {"V_TEXT"}}
+NOTIFY_TYPES: dict[SensorType, set[ValueType]] = {"S_INFO": {"V_TEXT"}}
-SENSOR_TYPES = {
+SENSOR_TYPES: dict[SensorType, set[ValueType]] = {
"S_SOUND": {"V_LEVEL"},
"S_VIBRATION": {"V_LEVEL"},
"S_MOISTURE": {"V_LEVEL"},
@@ -80,7 +119,7 @@
"S_DUST": {"V_DUST_LEVEL", "V_LEVEL"},
}
-SWITCH_TYPES = {
+SWITCH_TYPES: dict[SensorType, set[ValueType]] = {
"S_LIGHT": {"V_LIGHT"},
"S_BINARY": {"V_STATUS"},
"S_DOOR": {"V_ARMED"},
@@ -97,7 +136,7 @@
}
-PLATFORM_TYPES = {
+PLATFORM_TYPES: dict[str, dict[SensorType, set[ValueType]]] = {
"binary_sensor": BINARY_SENSOR_TYPES,
"climate": CLIMATE_TYPES,
"cover": COVER_TYPES,
@@ -108,13 +147,19 @@
"switch": SWITCH_TYPES,
}
-FLAT_PLATFORM_TYPES = {
+FLAT_PLATFORM_TYPES: dict[tuple[str, SensorType], set[ValueType]] = {
(platform, s_type_name): v_type_name
for platform, platform_types in PLATFORM_TYPES.items()
for s_type_name, v_type_name in platform_types.items()
}
-TYPE_TO_PLATFORMS = defaultdict(list)
+TYPE_TO_PLATFORMS: dict[SensorType, list[str]] = defaultdict(list)
+
for platform, platform_types in PLATFORM_TYPES.items():
for s_type_name in platform_types:
TYPE_TO_PLATFORMS[s_type_name].append(platform)
+
+PLATFORMS_WITH_ENTRY_SUPPORT = set(PLATFORM_TYPES.keys()) - {
+ "notify",
+ "device_tracker",
+}
diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py
index f2ede69793fde0..33393f08defab1 100644
--- a/homeassistant/components/mysensors/cover.py
+++ b/homeassistant/components/mysensors/cover.py
@@ -1,35 +1,97 @@
"""Support for MySensors covers."""
+from enum import Enum, unique
+import logging
+from typing import Callable
+
from homeassistant.components import mysensors
from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverEntity
+from homeassistant.components.mysensors import on_unload
+from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.typing import HomeAssistantType
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@unique
+class CoverState(Enum):
+ """An enumeration of the standard cover states."""
+
+ OPEN = 0
+ OPENING = 1
+ CLOSING = 2
+ CLOSED = 3
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the mysensors platform for covers."""
- mysensors.setup_mysensors_platform(
+async def async_setup_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
+):
+ """Set up this platform for a specific ConfigEntry(==Gateway)."""
+
+ async def async_discover(discovery_info):
+ """Discover and add a MySensors cover."""
+ mysensors.setup_mysensors_platform(
+ hass,
+ DOMAIN,
+ discovery_info,
+ MySensorsCover,
+ async_add_entities=async_add_entities,
+ )
+
+ await on_unload(
hass,
- DOMAIN,
- discovery_info,
- MySensorsCover,
- async_add_entities=async_add_entities,
+ config_entry.entry_id,
+ async_dispatcher_connect(
+ hass,
+ MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
+ async_discover,
+ ),
)
class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
"""Representation of the value of a MySensors Cover child node."""
- @property
- def assumed_state(self):
- """Return True if unable to access real state of entity."""
- return self.gateway.optimistic
+ def get_cover_state(self):
+ """Return a CoverState enum representing the state of the cover."""
+ set_req = self.gateway.const.SetReq
+ v_up = self._values.get(set_req.V_UP) == STATE_ON
+ v_down = self._values.get(set_req.V_DOWN) == STATE_ON
+ v_stop = self._values.get(set_req.V_STOP) == STATE_ON
+
+ # If a V_DIMMER or V_PERCENTAGE is available, that is the amount
+ # the cover is open. Otherwise, use 0 or 100 based on the V_LIGHT
+ # or V_STATUS.
+ amount = 100
+ if set_req.V_DIMMER in self._values:
+ amount = self._values.get(set_req.V_DIMMER)
+ else:
+ amount = 100 if self._values.get(set_req.V_LIGHT) == STATE_ON else 0
+
+ if amount == 0:
+ return CoverState.CLOSED
+ if v_up and not v_down and not v_stop:
+ return CoverState.OPENING
+ if not v_up and v_down and not v_stop:
+ return CoverState.CLOSING
+ return CoverState.OPEN
@property
def is_closed(self):
- """Return True if cover is closed."""
- set_req = self.gateway.const.SetReq
- if set_req.V_DIMMER in self._values:
- return self._values.get(set_req.V_DIMMER) == 0
- return self._values.get(set_req.V_LIGHT) == STATE_OFF
+ """Return True if the cover is closed."""
+ return self.get_cover_state() == CoverState.CLOSED
+
+ @property
+ def is_closing(self):
+ """Return True if the cover is closing."""
+ return self.get_cover_state() == CoverState.CLOSING
+
+ @property
+ def is_opening(self):
+ """Return True if the cover is opening."""
+ return self.get_cover_state() == CoverState.OPENING
@property
def current_cover_position(self):
@@ -46,7 +108,7 @@ async def async_open_cover(self, **kwargs):
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_UP, 1, ack=1
)
- if self.gateway.optimistic:
+ if self.assumed_state:
# Optimistically assume that cover has changed state.
if set_req.V_DIMMER in self._values:
self._values[set_req.V_DIMMER] = 100
@@ -60,7 +122,7 @@ async def async_close_cover(self, **kwargs):
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_DOWN, 1, ack=1
)
- if self.gateway.optimistic:
+ if self.assumed_state:
# Optimistically assume that cover has changed state.
if set_req.V_DIMMER in self._values:
self._values[set_req.V_DIMMER] = 0
@@ -75,7 +137,7 @@ async def async_set_cover_position(self, **kwargs):
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_DIMMER, position, ack=1
)
- if self.gateway.optimistic:
+ if self.assumed_state:
# Optimistically assume that cover has changed state.
self._values[set_req.V_DIMMER] = position
self.async_write_ha_state()
diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py
index 9c1c4b54367646..4e770f70bf0c98 100644
--- a/homeassistant/components/mysensors/device.py
+++ b/homeassistant/components/mysensors/device.py
@@ -1,13 +1,28 @@
"""Handle MySensors devices."""
+from __future__ import annotations
+
from functools import partial
import logging
+from typing import Any
+
+from mysensors import BaseAsyncGateway, Sensor
+from mysensors.sensor import ChildSensor
from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
-from .const import CHILD_CALLBACK, NODE_CALLBACK, UPDATE_DELAY
+from .const import (
+ CHILD_CALLBACK,
+ CONF_DEVICE,
+ DOMAIN,
+ NODE_CALLBACK,
+ PLATFORM_TYPES,
+ UPDATE_DELAY,
+ DevId,
+ GatewayId,
+)
_LOGGER = logging.getLogger(__name__)
@@ -19,36 +34,97 @@
MYSENSORS_PLATFORM_DEVICES = "mysensors_devices_{}"
-def get_mysensors_devices(hass, domain):
- """Return MySensors devices for a platform."""
- if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data:
- hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] = {}
- return hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)]
-
-
class MySensorsDevice:
"""Representation of a MySensors device."""
- def __init__(self, gateway, node_id, child_id, name, value_type):
+ def __init__(
+ self,
+ gateway_id: GatewayId,
+ gateway: BaseAsyncGateway,
+ node_id: int,
+ child_id: int,
+ value_type: int,
+ ):
"""Set up the MySensors device."""
- self.gateway = gateway
- self.node_id = node_id
- self.child_id = child_id
- self._name = name
- self.value_type = value_type
- child = gateway.sensors[node_id].children[child_id]
- self.child_type = child.type
+ self.gateway_id: GatewayId = gateway_id
+ self.gateway: BaseAsyncGateway = gateway
+ self.node_id: int = node_id
+ self.child_id: int = child_id
+ self.value_type: int = value_type # value_type as int. string variant can be looked up in gateway consts
+ self.child_type = self._child.type
self._values = {}
self._update_scheduled = False
self.hass = None
+ @property
+ def dev_id(self) -> DevId:
+ """Return the DevId of this device.
+
+ It is used to route incoming MySensors messages to the correct device/entity.
+ """
+ return self.gateway_id, self.node_id, self.child_id, self.value_type
+
+ @property
+ def _logger(self):
+ return logging.getLogger(f"{__name__}.{self.name}")
+
+ async def async_will_remove_from_hass(self):
+ """Remove this entity from home assistant."""
+ for platform in PLATFORM_TYPES:
+ platform_str = MYSENSORS_PLATFORM_DEVICES.format(platform)
+ if platform_str in self.hass.data[DOMAIN]:
+ platform_dict = self.hass.data[DOMAIN][platform_str]
+ if self.dev_id in platform_dict:
+ del platform_dict[self.dev_id]
+ self._logger.debug(
+ "deleted %s from platform %s", self.dev_id, platform
+ )
+
+ @property
+ def _node(self) -> Sensor:
+ return self.gateway.sensors[self.node_id]
+
+ @property
+ def _child(self) -> ChildSensor:
+ return self._node.children[self.child_id]
+
+ @property
+ def sketch_name(self) -> str:
+ """Return the name of the sketch running on the whole node (will be the same for several entities!)."""
+ return self._node.sketch_name
+
+ @property
+ def sketch_version(self) -> str:
+ """Return the version of the sketch running on the whole node (will be the same for several entities!)."""
+ return self._node.sketch_version
+
+ @property
+ def node_name(self) -> str:
+ """Name of the whole node (will be the same for several entities!)."""
+ return f"{self.sketch_name} {self.node_id}"
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique ID for use in home assistant."""
+ return f"{self.gateway_id}-{self.node_id}-{self.child_id}-{self.value_type}"
+
+ @property
+ def device_info(self) -> dict[str, Any] | None:
+ """Return a dict that allows home assistant to puzzle all entities belonging to a node together."""
+ return {
+ "identifiers": {(DOMAIN, f"{self.gateway_id}-{self.node_id}")},
+ "name": self.node_name,
+ "manufacturer": DOMAIN,
+ "sw_version": self.sketch_version,
+ }
+
@property
def name(self):
"""Return the name of this entity."""
- return self._name
+ return f"{self.node_name} {self.child_id}"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id]
@@ -57,9 +133,12 @@ def device_state_attributes(self):
ATTR_HEARTBEAT: node.heartbeat,
ATTR_CHILD_ID: self.child_id,
ATTR_DESCRIPTION: child.description,
- ATTR_DEVICE: self.gateway.device,
ATTR_NODE_ID: self.node_id,
}
+ # This works when we are actually an Entity (i.e. all platforms except device_tracker)
+ if hasattr(self, "platform"):
+ # pylint: disable=no-member
+ attr[ATTR_DEVICE] = self.platform.config_entry.data[CONF_DEVICE]
set_req = self.gateway.const.SetReq
@@ -76,7 +155,7 @@ async def async_update(self):
for value_type, value in child.values.items():
_LOGGER.debug(
"Entity update: %s: value_type %s, value = %s",
- self._name,
+ self.name,
value_type,
value,
)
@@ -85,6 +164,9 @@ async def async_update(self):
set_req.V_LIGHT,
set_req.V_LOCK_STATUS,
set_req.V_TRIPPED,
+ set_req.V_UP,
+ set_req.V_DOWN,
+ set_req.V_STOP,
):
self._values[value_type] = STATE_ON if int(value) == 1 else STATE_OFF
elif value_type == set_req.V_DIMMER:
@@ -116,6 +198,13 @@ async def update():
self.hass.loop.call_later(UPDATE_DELAY, delayed_update)
+def get_mysensors_devices(hass, domain: str) -> dict[DevId, MySensorsDevice]:
+ """Return MySensors devices for a hass platform name."""
+ if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data[DOMAIN]:
+ hass.data[DOMAIN][MYSENSORS_PLATFORM_DEVICES.format(domain)] = {}
+ return hass.data[DOMAIN][MYSENSORS_PLATFORM_DEVICES.format(domain)]
+
+
class MySensorsEntity(MySensorsDevice, Entity):
"""Representation of a MySensors entity."""
@@ -135,17 +224,17 @@ async def _async_update_callback(self):
async def async_added_to_hass(self):
"""Register update callback."""
- gateway_id = id(self.gateway)
- dev_id = gateway_id, self.node_id, self.child_id, self.value_type
self.async_on_remove(
async_dispatcher_connect(
- self.hass, CHILD_CALLBACK.format(*dev_id), self.async_update_callback
+ self.hass,
+ CHILD_CALLBACK.format(*self.dev_id),
+ self.async_update_callback,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
- NODE_CALLBACK.format(gateway_id, self.node_id),
+ NODE_CALLBACK.format(self.gateway_id, self.node_id),
self.async_update_callback,
)
)
diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py
index 1bf1e072cebfd2..068029af9602fe 100644
--- a/homeassistant/components/mysensors/device_tracker.py
+++ b/homeassistant/components/mysensors/device_tracker.py
@@ -1,12 +1,20 @@
"""Support for tracking MySensors devices."""
from homeassistant.components import mysensors
from homeassistant.components.device_tracker import DOMAIN
+from homeassistant.components.mysensors import DevId, on_unload
+from homeassistant.components.mysensors.const import ATTR_GATEWAY_ID, GatewayId
from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import slugify
-async def async_setup_scanner(hass, config, async_see, discovery_info=None):
+async def async_setup_scanner(
+ hass: HomeAssistantType, config, async_see, discovery_info=None
+):
"""Set up the MySensors device scanner."""
+ if not discovery_info:
+ return False
+
new_devices = mysensors.setup_mysensors_platform(
hass,
DOMAIN,
@@ -18,17 +26,25 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
return False
for device in new_devices:
- gateway_id = id(device.gateway)
- dev_id = (gateway_id, device.node_id, device.child_id, device.value_type)
- async_dispatcher_connect(
+ gateway_id: GatewayId = discovery_info[ATTR_GATEWAY_ID]
+ dev_id: DevId = (gateway_id, device.node_id, device.child_id, device.value_type)
+ await on_unload(
hass,
- mysensors.const.CHILD_CALLBACK.format(*dev_id),
- device.async_update_callback,
+ gateway_id,
+ async_dispatcher_connect(
+ hass,
+ mysensors.const.CHILD_CALLBACK.format(*dev_id),
+ device.async_update_callback,
+ ),
)
- async_dispatcher_connect(
+ await on_unload(
hass,
- mysensors.const.NODE_CALLBACK.format(gateway_id, device.node_id),
- device.async_update_callback,
+ gateway_id,
+ async_dispatcher_connect(
+ hass,
+ mysensors.const.NODE_CALLBACK.format(gateway_id, device.node_id),
+ device.async_update_callback,
+ ),
)
return True
@@ -37,7 +53,7 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
class MySensorsDeviceScanner(mysensors.device.MySensorsDevice):
"""Represent a MySensors scanner."""
- def __init__(self, hass, async_see, *args):
+ def __init__(self, hass: HomeAssistantType, async_see, *args):
"""Set up instance."""
super().__init__(*args)
self.async_see = async_see
@@ -56,5 +72,5 @@ async def _async_update_callback(self):
host_name=self.name,
gps=(latitude, longitude),
battery=node.battery_level,
- attributes=self.device_state_attributes,
+ attributes=self.extra_state_attributes,
)
diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py
index f9450b798ace1e..6cf8e7d7383176 100644
--- a/homeassistant/components/mysensors/gateway.py
+++ b/homeassistant/components/mysensors/gateway.py
@@ -1,25 +1,26 @@
"""Handle MySensors gateways."""
+from __future__ import annotations
+
import asyncio
from collections import defaultdict
import logging
import socket
import sys
+from typing import Any, Callable, Coroutine
import async_timeout
-from mysensors import mysensors
+from mysensors import BaseAsyncGateway, Message, Sensor, mysensors
import voluptuous as vol
-from homeassistant.const import CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP
-from homeassistant.core import callback
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import EVENT_HOMEASSISTANT_STOP
+from homeassistant.core import Event, callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.setup import async_setup_component
+from homeassistant.helpers.typing import HomeAssistantType
from .const import (
CONF_BAUD_RATE,
CONF_DEVICE,
- CONF_GATEWAYS,
- CONF_NODES,
- CONF_PERSISTENCE,
CONF_PERSISTENCE_FILE,
CONF_RETAIN,
CONF_TCP_PORT,
@@ -27,15 +28,21 @@
CONF_TOPIC_OUT_PREFIX,
CONF_VERSION,
DOMAIN,
- MYSENSORS_GATEWAY_READY,
+ MYSENSORS_GATEWAY_START_TASK,
MYSENSORS_GATEWAYS,
+ GatewayId,
)
from .handler import HANDLERS
-from .helpers import discover_mysensors_platform, validate_child, validate_node
+from .helpers import (
+ discover_mysensors_platform,
+ on_unload,
+ validate_child,
+ validate_node,
+)
_LOGGER = logging.getLogger(__name__)
-GATEWAY_READY_TIMEOUT = 15.0
+GATEWAY_READY_TIMEOUT = 20.0
MQTT_COMPONENT = "mqtt"
@@ -58,48 +65,107 @@ def is_socket_address(value):
raise vol.Invalid("Device is not a valid domain name or ip address") from err
-def get_mysensors_gateway(hass, gateway_id):
- """Return MySensors gateway."""
- if MYSENSORS_GATEWAYS not in hass.data:
- hass.data[MYSENSORS_GATEWAYS] = {}
- gateways = hass.data.get(MYSENSORS_GATEWAYS)
- return gateways.get(gateway_id)
-
+async def try_connect(hass: HomeAssistantType, user_input: dict[str, str]) -> bool:
+ """Try to connect to a gateway and report if it worked."""
+ if user_input[CONF_DEVICE] == MQTT_COMPONENT:
+ return True # dont validate mqtt. mqtt gateways dont send ready messages :(
+ try:
+ gateway_ready = asyncio.Event()
+
+ def on_conn_made(_: BaseAsyncGateway) -> None:
+ gateway_ready.set()
+
+ gateway: BaseAsyncGateway | None = await _get_gateway(
+ hass,
+ device=user_input[CONF_DEVICE],
+ version=user_input[CONF_VERSION],
+ event_callback=lambda _: None,
+ persistence_file=None,
+ baud_rate=user_input.get(CONF_BAUD_RATE),
+ tcp_port=user_input.get(CONF_TCP_PORT),
+ topic_in_prefix=None,
+ topic_out_prefix=None,
+ retain=False,
+ persistence=False,
+ )
+ if gateway is None:
+ return False
+ gateway.on_conn_made = on_conn_made
-async def setup_gateways(hass, config):
- """Set up all gateways."""
- conf = config[DOMAIN]
- gateways = {}
+ connect_task = None
+ try:
+ connect_task = asyncio.create_task(gateway.start())
+ with async_timeout.timeout(GATEWAY_READY_TIMEOUT):
+ await gateway_ready.wait()
+ return True
+ except asyncio.TimeoutError:
+ _LOGGER.info("Try gateway connect failed with timeout")
+ return False
+ finally:
+ if connect_task is not None and not connect_task.done():
+ connect_task.cancel()
+ asyncio.create_task(gateway.stop())
+ except OSError as err:
+ _LOGGER.info("Try gateway connect failed with exception", exc_info=err)
+ return False
- for index, gateway_conf in enumerate(conf[CONF_GATEWAYS]):
- persistence_file = gateway_conf.get(
- CONF_PERSISTENCE_FILE,
- hass.config.path(f"mysensors{index + 1}.pickle"),
- )
- ready_gateway = await _get_gateway(hass, config, gateway_conf, persistence_file)
- if ready_gateway is not None:
- gateways[id(ready_gateway)] = ready_gateway
- return gateways
+def get_mysensors_gateway(
+ hass: HomeAssistantType, gateway_id: GatewayId
+) -> BaseAsyncGateway | None:
+ """Return the Gateway for a given GatewayId."""
+ if MYSENSORS_GATEWAYS not in hass.data[DOMAIN]:
+ hass.data[DOMAIN][MYSENSORS_GATEWAYS] = {}
+ gateways = hass.data[DOMAIN].get(MYSENSORS_GATEWAYS)
+ return gateways.get(gateway_id)
-async def _get_gateway(hass, config, gateway_conf, persistence_file):
+async def setup_gateway(
+ hass: HomeAssistantType, entry: ConfigEntry
+) -> BaseAsyncGateway | None:
+ """Set up the Gateway for the given ConfigEntry."""
+
+ ready_gateway = await _get_gateway(
+ hass,
+ device=entry.data[CONF_DEVICE],
+ version=entry.data[CONF_VERSION],
+ event_callback=_gw_callback_factory(hass, entry.entry_id),
+ persistence_file=entry.data.get(
+ CONF_PERSISTENCE_FILE, f"mysensors_{entry.entry_id}.json"
+ ),
+ baud_rate=entry.data.get(CONF_BAUD_RATE),
+ tcp_port=entry.data.get(CONF_TCP_PORT),
+ topic_in_prefix=entry.data.get(CONF_TOPIC_IN_PREFIX),
+ topic_out_prefix=entry.data.get(CONF_TOPIC_OUT_PREFIX),
+ retain=entry.data.get(CONF_RETAIN, False),
+ )
+ return ready_gateway
+
+
+async def _get_gateway(
+ hass: HomeAssistantType,
+ device: str,
+ version: str,
+ event_callback: Callable[[Message], None],
+ persistence_file: str | None = None,
+ baud_rate: int | None = None,
+ tcp_port: int | None = None,
+ topic_in_prefix: str | None = None,
+ topic_out_prefix: str | None = None,
+ retain: bool = False,
+ persistence: bool = True, # old persistence option has been deprecated. kwarg is here so we can run try_connect() without persistence
+) -> BaseAsyncGateway | None:
"""Return gateway after setup of the gateway."""
- conf = config[DOMAIN]
- persistence = conf[CONF_PERSISTENCE]
- version = conf[CONF_VERSION]
- device = gateway_conf[CONF_DEVICE]
- baud_rate = gateway_conf[CONF_BAUD_RATE]
- tcp_port = gateway_conf[CONF_TCP_PORT]
- in_prefix = gateway_conf.get(CONF_TOPIC_IN_PREFIX, "")
- out_prefix = gateway_conf.get(CONF_TOPIC_OUT_PREFIX, "")
+ if persistence_file is not None:
+ # interpret relative paths to be in hass config folder. absolute paths will be left as they are
+ persistence_file = hass.config.path(persistence_file)
if device == MQTT_COMPONENT:
- if not await async_setup_component(hass, MQTT_COMPONENT, config):
- return None
+ # what is the purpose of this?
+ # if not await async_setup_component(hass, MQTT_COMPONENT, entry):
+ # return None
mqtt = hass.components.mqtt
- retain = conf[CONF_RETAIN]
def pub_callback(topic, payload, qos, retain):
"""Call MQTT publish function."""
@@ -118,8 +184,8 @@ def internal_callback(msg):
gateway = mysensors.AsyncMQTTGateway(
pub_callback,
sub_callback,
- in_prefix=in_prefix,
- out_prefix=out_prefix,
+ in_prefix=topic_in_prefix,
+ out_prefix=topic_out_prefix,
retain=retain,
loop=hass.loop,
event_callback=None,
@@ -154,25 +220,23 @@ def internal_callback(msg):
)
except vol.Invalid:
# invalid ip address
+ _LOGGER.error("Connect failed: Invalid device %s", device)
return None
- gateway.metric = hass.config.units.is_metric
- gateway.optimistic = conf[CONF_OPTIMISTIC]
- gateway.device = device
- gateway.event_callback = _gw_callback_factory(hass, config)
- gateway.nodes_config = gateway_conf[CONF_NODES]
+ gateway.event_callback = event_callback
if persistence:
await gateway.start_persistence()
return gateway
-async def finish_setup(hass, hass_config, gateways):
+async def finish_setup(
+ hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway
+):
"""Load any persistent devices and platforms and start gateway."""
discover_tasks = []
start_tasks = []
- for gateway in gateways.values():
- discover_tasks.append(_discover_persistent_devices(hass, hass_config, gateway))
- start_tasks.append(_gw_start(hass, gateway))
+ discover_tasks.append(_discover_persistent_devices(hass, entry, gateway))
+ start_tasks.append(_gw_start(hass, entry, gateway))
if discover_tasks:
# Make sure all devices and platforms are loaded before gateway start.
await asyncio.wait(discover_tasks)
@@ -180,71 +244,99 @@ async def finish_setup(hass, hass_config, gateways):
await asyncio.wait(start_tasks)
-async def _discover_persistent_devices(hass, hass_config, gateway):
+async def _discover_persistent_devices(
+ hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway
+):
"""Discover platforms for devices loaded via persistence file."""
tasks = []
new_devices = defaultdict(list)
for node_id in gateway.sensors:
if not validate_node(gateway, node_id):
continue
- node = gateway.sensors[node_id]
- for child in node.children.values():
- validated = validate_child(gateway, node_id, child)
+ node: Sensor = gateway.sensors[node_id]
+ for child in node.children.values(): # child is of type ChildSensor
+ validated = validate_child(entry.entry_id, gateway, node_id, child)
for platform, dev_ids in validated.items():
new_devices[platform].extend(dev_ids)
+ _LOGGER.debug("discovering persistent devices: %s", new_devices)
for platform, dev_ids in new_devices.items():
- tasks.append(discover_mysensors_platform(hass, hass_config, platform, dev_ids))
+ discover_mysensors_platform(hass, entry.entry_id, platform, dev_ids)
if tasks:
await asyncio.wait(tasks)
-async def _gw_start(hass, gateway):
+async def gw_stop(hass, entry: ConfigEntry, gateway: BaseAsyncGateway):
+ """Stop the gateway."""
+ connect_task = hass.data[DOMAIN].pop(
+ MYSENSORS_GATEWAY_START_TASK.format(entry.entry_id), None
+ )
+ if connect_task is not None and not connect_task.done():
+ connect_task.cancel()
+ await gateway.stop()
+
+
+async def _gw_start(
+ hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway
+):
"""Start the gateway."""
- # Don't use hass.async_create_task to avoid holding up setup indefinitely.
- connect_task = hass.loop.create_task(gateway.start())
+ gateway_ready = asyncio.Event()
- @callback
- def gw_stop(event):
- """Trigger to stop the gateway."""
- hass.async_create_task(gateway.stop())
- if not connect_task.done():
- connect_task.cancel()
-
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop)
- if gateway.device == "mqtt":
+ def gateway_connected(_: BaseAsyncGateway):
+ gateway_ready.set()
+
+ gateway.on_conn_made = gateway_connected
+ # Don't use hass.async_create_task to avoid holding up setup indefinitely.
+ hass.data[DOMAIN][
+ MYSENSORS_GATEWAY_START_TASK.format(entry.entry_id)
+ ] = asyncio.create_task(
+ gateway.start()
+ ) # store the connect task so it can be cancelled in gw_stop
+
+ async def stop_this_gw(_: Event):
+ await gw_stop(hass, entry, gateway)
+
+ await on_unload(
+ hass,
+ entry.entry_id,
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_this_gw),
+ )
+
+ if entry.data[CONF_DEVICE] == MQTT_COMPONENT:
# Gatways connected via mqtt doesn't send gateway ready message.
return
- gateway_ready = asyncio.Future()
- gateway_ready_key = MYSENSORS_GATEWAY_READY.format(id(gateway))
- hass.data[gateway_ready_key] = gateway_ready
-
try:
with async_timeout.timeout(GATEWAY_READY_TIMEOUT):
- await gateway_ready
+ await gateway_ready.wait()
except asyncio.TimeoutError:
_LOGGER.warning(
- "Gateway %s not ready after %s secs so continuing with setup",
- gateway.device,
+ "Gateway %s not connected after %s secs so continuing with setup",
+ entry.data[CONF_DEVICE],
GATEWAY_READY_TIMEOUT,
)
- finally:
- hass.data.pop(gateway_ready_key, None)
-def _gw_callback_factory(hass, hass_config):
+def _gw_callback_factory(
+ hass: HomeAssistantType, gateway_id: GatewayId
+) -> Callable[[Message], None]:
"""Return a new callback for the gateway."""
@callback
- def mysensors_callback(msg):
- """Handle messages from a MySensors gateway."""
+ def mysensors_callback(msg: Message):
+ """Handle messages from a MySensors gateway.
+
+ All MySenors messages are received here.
+ The messages are passed to handler functions depending on their type.
+ """
_LOGGER.debug("Node update: node %s child %s", msg.node_id, msg.child_id)
msg_type = msg.gateway.const.MessageType(msg.type)
- msg_handler = HANDLERS.get(msg_type.name)
+ msg_handler: Callable[
+ [Any, GatewayId, Message], Coroutine[None]
+ ] = HANDLERS.get(msg_type.name)
if msg_handler is None:
return
- hass.async_create_task(msg_handler(hass, hass_config, msg))
+ hass.async_create_task(msg_handler(hass, gateway_id, msg))
return mysensors_callback
diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py
index b5b8b511aee110..d21140701f97c2 100644
--- a/homeassistant/components/mysensors/handler.py
+++ b/homeassistant/components/mysensors/handler.py
@@ -1,9 +1,14 @@
"""Handle MySensors messages."""
+from __future__ import annotations
+
+from mysensors import Message
+
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import decorator
-from .const import CHILD_CALLBACK, MYSENSORS_GATEWAY_READY, NODE_CALLBACK
+from .const import CHILD_CALLBACK, NODE_CALLBACK, DevId, GatewayId
from .device import get_mysensors_devices
from .helpers import discover_mysensors_platform, validate_set_msg
@@ -11,75 +16,77 @@
@HANDLERS.register("set")
-async def handle_set(hass, hass_config, msg):
+async def handle_set(
+ hass: HomeAssistantType, gateway_id: GatewayId, msg: Message
+) -> None:
"""Handle a mysensors set message."""
- validated = validate_set_msg(msg)
- _handle_child_update(hass, hass_config, validated)
+ validated = validate_set_msg(gateway_id, msg)
+ _handle_child_update(hass, gateway_id, validated)
@HANDLERS.register("internal")
-async def handle_internal(hass, hass_config, msg):
+async def handle_internal(
+ hass: HomeAssistantType, gateway_id: GatewayId, msg: Message
+) -> None:
"""Handle a mysensors internal message."""
internal = msg.gateway.const.Internal(msg.sub_type)
handler = HANDLERS.get(internal.name)
if handler is None:
return
- await handler(hass, hass_config, msg)
+ await handler(hass, gateway_id, msg)
@HANDLERS.register("I_BATTERY_LEVEL")
-async def handle_battery_level(hass, hass_config, msg):
+async def handle_battery_level(
+ hass: HomeAssistantType, gateway_id: GatewayId, msg: Message
+) -> None:
"""Handle an internal battery level message."""
- _handle_node_update(hass, msg)
+ _handle_node_update(hass, gateway_id, msg)
@HANDLERS.register("I_HEARTBEAT_RESPONSE")
-async def handle_heartbeat(hass, hass_config, msg):
+async def handle_heartbeat(
+ hass: HomeAssistantType, gateway_id: GatewayId, msg: Message
+) -> None:
"""Handle an heartbeat."""
- _handle_node_update(hass, msg)
+ _handle_node_update(hass, gateway_id, msg)
@HANDLERS.register("I_SKETCH_NAME")
-async def handle_sketch_name(hass, hass_config, msg):
+async def handle_sketch_name(
+ hass: HomeAssistantType, gateway_id: GatewayId, msg: Message
+) -> None:
"""Handle an internal sketch name message."""
- _handle_node_update(hass, msg)
+ _handle_node_update(hass, gateway_id, msg)
@HANDLERS.register("I_SKETCH_VERSION")
-async def handle_sketch_version(hass, hass_config, msg):
+async def handle_sketch_version(
+ hass: HomeAssistantType, gateway_id: GatewayId, msg: Message
+) -> None:
"""Handle an internal sketch version message."""
- _handle_node_update(hass, msg)
-
-
-@HANDLERS.register("I_GATEWAY_READY")
-async def handle_gateway_ready(hass, hass_config, msg):
- """Handle an internal gateway ready message.
-
- Set asyncio future result if gateway is ready.
- """
- gateway_ready = hass.data.get(MYSENSORS_GATEWAY_READY.format(id(msg.gateway)))
- if gateway_ready is None or gateway_ready.cancelled():
- return
- gateway_ready.set_result(True)
+ _handle_node_update(hass, gateway_id, msg)
@callback
-def _handle_child_update(hass, hass_config, validated):
+def _handle_child_update(
+ hass: HomeAssistantType, gateway_id: GatewayId, validated: dict[str, list[DevId]]
+):
"""Handle a child update."""
- signals = []
+ signals: list[str] = []
# Update all platforms for the device via dispatcher.
# Add/update entity for validated children.
for platform, dev_ids in validated.items():
devices = get_mysensors_devices(hass, platform)
- new_dev_ids = []
+ new_dev_ids: list[DevId] = []
for dev_id in dev_ids:
if dev_id in devices:
signals.append(CHILD_CALLBACK.format(*dev_id))
else:
new_dev_ids.append(dev_id)
if new_dev_ids:
- discover_mysensors_platform(hass, hass_config, platform, new_dev_ids)
+ discover_mysensors_platform(hass, gateway_id, platform, new_dev_ids)
for signal in set(signals):
# Only one signal per device is needed.
# A device can have multiple platforms, ie multiple schemas.
@@ -87,7 +94,7 @@ def _handle_child_update(hass, hass_config, validated):
@callback
-def _handle_node_update(hass, msg):
+def _handle_node_update(hass: HomeAssistantType, gateway_id: GatewayId, msg: Message):
"""Handle a node update."""
- signal = NODE_CALLBACK.format(id(msg.gateway), msg.node_id)
+ signal = NODE_CALLBACK.format(gateway_id, msg.node_id)
async_dispatcher_send(hass, signal)
diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py
index 20b266e550e619..0d18b243520d54 100644
--- a/homeassistant/components/mysensors/helpers.py
+++ b/homeassistant/components/mysensors/helpers.py
@@ -1,78 +1,131 @@
"""Helper functions for mysensors package."""
+from __future__ import annotations
+
from collections import defaultdict
+from enum import IntEnum
import logging
+from typing import Callable, DefaultDict
+from mysensors import BaseAsyncGateway, Message
+from mysensors.sensor import ChildSensor
import voluptuous as vol
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
-from homeassistant.core import callback
-from homeassistant.helpers import discovery
+from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.decorator import Registry
-from .const import ATTR_DEVICES, DOMAIN, FLAT_PLATFORM_TYPES, TYPE_TO_PLATFORMS
+from .const import (
+ ATTR_DEVICES,
+ ATTR_GATEWAY_ID,
+ DOMAIN,
+ FLAT_PLATFORM_TYPES,
+ MYSENSORS_DISCOVERY,
+ MYSENSORS_ON_UNLOAD,
+ TYPE_TO_PLATFORMS,
+ DevId,
+ GatewayId,
+ SensorType,
+ ValueType,
+)
_LOGGER = logging.getLogger(__name__)
SCHEMAS = Registry()
+async def on_unload(
+ hass: HomeAssistantType, entry: ConfigEntry | GatewayId, fnct: Callable
+) -> None:
+ """Register a callback to be called when entry is unloaded.
+
+ This function is used by platforms to cleanup after themselves.
+ """
+ if isinstance(entry, GatewayId):
+ uniqueid = entry
+ else:
+ uniqueid = entry.entry_id
+ key = MYSENSORS_ON_UNLOAD.format(uniqueid)
+ if key not in hass.data[DOMAIN]:
+ hass.data[DOMAIN][key] = []
+ hass.data[DOMAIN][key].append(fnct)
+
+
@callback
-def discover_mysensors_platform(hass, hass_config, platform, new_devices):
+def discover_mysensors_platform(
+ hass: HomeAssistant, gateway_id: GatewayId, platform: str, new_devices: list[DevId]
+) -> None:
"""Discover a MySensors platform."""
- task = hass.async_create_task(
- discovery.async_load_platform(
- hass,
- platform,
- DOMAIN,
- {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN},
- hass_config,
- )
+ _LOGGER.debug("Discovering platform %s with devIds: %s", platform, new_devices)
+ async_dispatcher_send(
+ hass,
+ MYSENSORS_DISCOVERY.format(gateway_id, platform),
+ {
+ ATTR_DEVICES: new_devices,
+ CONF_NAME: DOMAIN,
+ ATTR_GATEWAY_ID: gateway_id,
+ },
)
- return task
-def default_schema(gateway, child, value_type_name):
+def default_schema(
+ gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
+) -> vol.Schema:
"""Return a default validation schema for value types."""
schema = {value_type_name: cv.string}
return get_child_schema(gateway, child, value_type_name, schema)
@SCHEMAS.register(("light", "V_DIMMER"))
-def light_dimmer_schema(gateway, child, value_type_name):
+def light_dimmer_schema(
+ gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
+) -> vol.Schema:
"""Return a validation schema for V_DIMMER."""
schema = {"V_DIMMER": cv.string, "V_LIGHT": cv.string}
return get_child_schema(gateway, child, value_type_name, schema)
@SCHEMAS.register(("light", "V_PERCENTAGE"))
-def light_percentage_schema(gateway, child, value_type_name):
+def light_percentage_schema(
+ gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
+) -> vol.Schema:
"""Return a validation schema for V_PERCENTAGE."""
schema = {"V_PERCENTAGE": cv.string, "V_STATUS": cv.string}
return get_child_schema(gateway, child, value_type_name, schema)
@SCHEMAS.register(("light", "V_RGB"))
-def light_rgb_schema(gateway, child, value_type_name):
+def light_rgb_schema(
+ gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
+) -> vol.Schema:
"""Return a validation schema for V_RGB."""
schema = {"V_RGB": cv.string, "V_STATUS": cv.string}
return get_child_schema(gateway, child, value_type_name, schema)
@SCHEMAS.register(("light", "V_RGBW"))
-def light_rgbw_schema(gateway, child, value_type_name):
+def light_rgbw_schema(
+ gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
+) -> vol.Schema:
"""Return a validation schema for V_RGBW."""
schema = {"V_RGBW": cv.string, "V_STATUS": cv.string}
return get_child_schema(gateway, child, value_type_name, schema)
@SCHEMAS.register(("switch", "V_IR_SEND"))
-def switch_ir_send_schema(gateway, child, value_type_name):
+def switch_ir_send_schema(
+ gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
+) -> vol.Schema:
"""Return a validation schema for V_IR_SEND."""
schema = {"V_IR_SEND": cv.string, "V_LIGHT": cv.string}
return get_child_schema(gateway, child, value_type_name, schema)
-def get_child_schema(gateway, child, value_type_name, schema):
+def get_child_schema(
+ gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType, schema
+) -> vol.Schema:
"""Return a child schema."""
set_req = gateway.const.SetReq
child_schema = child.get_schema(gateway.protocol_version)
@@ -88,7 +141,9 @@ def get_child_schema(gateway, child, value_type_name, schema):
return schema
-def invalid_msg(gateway, child, value_type_name):
+def invalid_msg(
+ gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
+):
"""Return a message for an invalid child during schema validation."""
pres = gateway.const.Presentation
set_req = gateway.const.SetReq
@@ -97,15 +152,15 @@ def invalid_msg(gateway, child, value_type_name):
)
-def validate_set_msg(msg):
+def validate_set_msg(gateway_id: GatewayId, msg: Message) -> dict[str, list[DevId]]:
"""Validate a set message."""
if not validate_node(msg.gateway, msg.node_id):
return {}
child = msg.gateway.sensors[msg.node_id].children[msg.child_id]
- return validate_child(msg.gateway, msg.node_id, child, msg.sub_type)
+ return validate_child(gateway_id, msg.gateway, msg.node_id, child, msg.sub_type)
-def validate_node(gateway, node_id):
+def validate_node(gateway: BaseAsyncGateway, node_id: int) -> bool:
"""Validate a node."""
if gateway.sensors[node_id].sketch_name is None:
_LOGGER.debug("Node %s is missing sketch name", node_id)
@@ -113,31 +168,39 @@ def validate_node(gateway, node_id):
return True
-def validate_child(gateway, node_id, child, value_type=None):
- """Validate a child."""
- validated = defaultdict(list)
- pres = gateway.const.Presentation
- set_req = gateway.const.SetReq
- child_type_name = next(
+def validate_child(
+ gateway_id: GatewayId,
+ gateway: BaseAsyncGateway,
+ node_id: int,
+ child: ChildSensor,
+ value_type: int | None = None,
+) -> DefaultDict[str, list[DevId]]:
+ """Validate a child. Returns a dict mapping hass platform names to list of DevId."""
+ validated: DefaultDict[str, list[DevId]] = defaultdict(list)
+ pres: IntEnum = gateway.const.Presentation
+ set_req: IntEnum = gateway.const.SetReq
+ child_type_name: SensorType | None = next(
(member.name for member in pres if member.value == child.type), None
)
- value_types = {value_type} if value_type else {*child.values}
- value_type_names = {
+ value_types: set[int] = {value_type} if value_type else {*child.values}
+ value_type_names: set[ValueType] = {
member.name for member in set_req if member.value in value_types
}
- platforms = TYPE_TO_PLATFORMS.get(child_type_name, [])
+ platforms: list[str] = TYPE_TO_PLATFORMS.get(child_type_name, [])
if not platforms:
_LOGGER.warning("Child type %s is not supported", child.type)
return validated
for platform in platforms:
- platform_v_names = FLAT_PLATFORM_TYPES[platform, child_type_name]
- v_names = platform_v_names & value_type_names
+ platform_v_names: set[ValueType] = FLAT_PLATFORM_TYPES[
+ platform, child_type_name
+ ]
+ v_names: set[ValueType] = platform_v_names & value_type_names
if not v_names:
- child_value_names = {
+ child_value_names: set[ValueType] = {
member.name for member in set_req if member.value in child.values
}
- v_names = platform_v_names & child_value_names
+ v_names: set[ValueType] = platform_v_names & child_value_names
for v_name in v_names:
child_schema_gen = SCHEMAS.get((platform, v_name), default_schema)
@@ -153,7 +216,12 @@ def validate_child(gateway, node_id, child, value_type=None):
exc,
)
continue
- dev_id = id(gateway), node_id, child.id, set_req[v_name].value
+ dev_id: DevId = (
+ gateway_id,
+ node_id,
+ child.id,
+ set_req[v_name].value,
+ )
validated[platform].append(dev_id)
return validated
diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py
index ffbcba6f032971..f90f9c5c81c77b 100644
--- a/homeassistant/components/mysensors/light.py
+++ b/homeassistant/components/mysensors/light.py
@@ -1,4 +1,6 @@
"""Support for MySensors lights."""
+from typing import Callable
+
from homeassistant.components import mysensors
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -10,27 +12,47 @@
SUPPORT_WHITE_VALUE,
LightEntity,
)
+from homeassistant.components.mysensors import on_unload
+from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.typing import HomeAssistantType
import homeassistant.util.color as color_util
from homeassistant.util.color import rgb_hex_to_rgb_list
SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the mysensors platform for lights."""
+async def async_setup_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
+):
+ """Set up this platform for a specific ConfigEntry(==Gateway)."""
device_class_map = {
"S_DIMMER": MySensorsLightDimmer,
"S_RGB_LIGHT": MySensorsLightRGB,
"S_RGBW_LIGHT": MySensorsLightRGBW,
}
- mysensors.setup_mysensors_platform(
+
+ async def async_discover(discovery_info):
+ """Discover and add a MySensors light."""
+ mysensors.setup_mysensors_platform(
+ hass,
+ DOMAIN,
+ discovery_info,
+ device_class_map,
+ async_add_entities=async_add_entities,
+ )
+
+ await on_unload(
hass,
- DOMAIN,
- discovery_info,
- device_class_map,
- async_add_entities=async_add_entities,
+ config_entry,
+ async_dispatcher_connect(
+ hass,
+ MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
+ async_discover,
+ ),
)
@@ -60,11 +82,6 @@ def white_value(self):
"""Return the white value of this light between 0..255."""
return self._white
- @property
- def assumed_state(self):
- """Return true if unable to access real state of entity."""
- return self.gateway.optimistic
-
@property
def is_on(self):
"""Return true if device is on."""
@@ -80,7 +97,7 @@ def _turn_on_light(self):
self.node_id, self.child_id, set_req.V_LIGHT, 1, ack=1
)
- if self.gateway.optimistic:
+ if self.assumed_state:
# optimistically assume that light has changed state
self._state = True
self._values[set_req.V_LIGHT] = STATE_ON
@@ -102,7 +119,7 @@ def _turn_on_dimmer(self, **kwargs):
self.node_id, self.child_id, set_req.V_DIMMER, percent, ack=1
)
- if self.gateway.optimistic:
+ if self.assumed_state:
# optimistically assume that light has changed state
self._brightness = brightness
self._values[set_req.V_DIMMER] = percent
@@ -135,7 +152,7 @@ def _turn_on_rgb_and_w(self, hex_template, **kwargs):
self.node_id, self.child_id, self.value_type, hex_color, ack=1
)
- if self.gateway.optimistic:
+ if self.assumed_state:
# optimistically assume that light has changed state
self._hs = color_util.color_RGB_to_hs(*rgb)
self._white = white
@@ -145,7 +162,7 @@ async def async_turn_off(self, **kwargs):
"""Turn the device off."""
value_type = self.gateway.const.SetReq.V_LIGHT
self.gateway.set_child_value(self.node_id, self.child_id, value_type, 0, ack=1)
- if self.gateway.optimistic:
+ if self.assumed_state:
# optimistically assume that light has changed state
self._state = False
self._values[value_type] = STATE_OFF
@@ -188,7 +205,7 @@ async def async_turn_on(self, **kwargs):
"""Turn the device on."""
self._turn_on_light()
self._turn_on_dimmer(**kwargs)
- if self.gateway.optimistic:
+ if self.assumed_state:
self.async_write_ha_state()
async def async_update(self):
@@ -214,7 +231,7 @@ async def async_turn_on(self, **kwargs):
self._turn_on_light()
self._turn_on_dimmer(**kwargs)
self._turn_on_rgb_and_w("%02x%02x%02x", **kwargs)
- if self.gateway.optimistic:
+ if self.assumed_state:
self.async_write_ha_state()
async def async_update(self):
@@ -241,5 +258,5 @@ async def async_turn_on(self, **kwargs):
self._turn_on_light()
self._turn_on_dimmer(**kwargs)
self._turn_on_rgb_and_w("%02x%02x%02x%02x", **kwargs)
- if self.gateway.optimistic:
+ if self.assumed_state:
self.async_write_ha_state()
diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json
index afeeb5d57cc99b..c7d439dedc44dd 100644
--- a/homeassistant/components/mysensors/manifest.json
+++ b/homeassistant/components/mysensors/manifest.json
@@ -2,7 +2,8 @@
"domain": "mysensors",
"name": "MySensors",
"documentation": "https://www.home-assistant.io/integrations/mysensors",
- "requirements": ["pymysensors==0.18.0"],
+ "requirements": ["pymysensors==0.21.0"],
"after_dependencies": ["mqtt"],
- "codeowners": ["@MartinHjelmare"]
+ "codeowners": ["@MartinHjelmare", "@functionpointer"],
+ "config_flow": true
}
diff --git a/homeassistant/components/mysensors/notify.py b/homeassistant/components/mysensors/notify.py
index 99e731762df5c8..50fca55ab39703 100644
--- a/homeassistant/components/mysensors/notify.py
+++ b/homeassistant/components/mysensors/notify.py
@@ -5,6 +5,9 @@
async def async_get_service(hass, config, discovery_info=None):
"""Get the MySensors notification service."""
+ if not discovery_info:
+ return None
+
new_devices = mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsNotificationDevice
)
diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py
index bab6bf3fc402e8..a62318aea53ecc 100644
--- a/homeassistant/components/mysensors/sensor.py
+++ b/homeassistant/components/mysensors/sensor.py
@@ -1,6 +1,11 @@
"""Support for MySensors sensors."""
+from typing import Callable
+
from homeassistant.components import mysensors
-from homeassistant.components.sensor import DOMAIN
+from homeassistant.components.mysensors import on_unload
+from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY
+from homeassistant.components.sensor import DOMAIN, SensorEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONDUCTIVITY,
DEGREE,
@@ -18,6 +23,8 @@
VOLT,
VOLUME_CUBIC_METERS,
)
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.typing import HomeAssistantType
SENSORS = {
"V_TEMP": [None, "mdi:thermometer"],
@@ -54,18 +61,33 @@
}
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the MySensors platform for sensors."""
- mysensors.setup_mysensors_platform(
+async def async_setup_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
+):
+ """Set up this platform for a specific ConfigEntry(==Gateway)."""
+
+ async def async_discover(discovery_info):
+ """Discover and add a MySensors sensor."""
+ mysensors.setup_mysensors_platform(
+ hass,
+ DOMAIN,
+ discovery_info,
+ MySensorsSensor,
+ async_add_entities=async_add_entities,
+ )
+
+ await on_unload(
hass,
- DOMAIN,
- discovery_info,
- MySensorsSensor,
- async_add_entities=async_add_entities,
+ config_entry,
+ async_dispatcher_connect(
+ hass,
+ MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
+ async_discover,
+ ),
)
-class MySensorsSensor(mysensors.device.MySensorsEntity):
+class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity):
"""Representation of a MySensors Sensor child node."""
@property
@@ -105,7 +127,7 @@ def _get_sensor_type(self):
pres = self.gateway.const.Presentation
set_req = self.gateway.const.SetReq
SENSORS[set_req.V_TEMP.name][0] = (
- TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT
+ TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT
)
sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None])
if isinstance(sensor_type, dict):
diff --git a/homeassistant/components/mysensors/strings.json b/homeassistant/components/mysensors/strings.json
new file mode 100644
index 00000000000000..43a68f61e247a0
--- /dev/null
+++ b/homeassistant/components/mysensors/strings.json
@@ -0,0 +1,79 @@
+{
+ "title": "MySensors",
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "gateway_type": "Gateway type"
+ },
+ "description": "Choose connection method to the gateway"
+ },
+ "gw_tcp": {
+ "description": "Ethernet gateway setup",
+ "data": {
+ "device": "IP address of the gateway",
+ "tcp_port": "port",
+ "version": "MySensors version",
+ "persistence_file": "persistence file (leave empty to auto-generate)"
+ }
+ },
+ "gw_serial": {
+ "description": "Serial gateway setup",
+ "data": {
+ "device": "Serial port",
+ "baud_rate": "baud rate",
+ "version": "MySensors version",
+ "persistence_file": "persistence file (leave empty to auto-generate)"
+ }
+ },
+ "gw_mqtt": {
+ "description": "MQTT gateway setup",
+ "data": {
+ "retain": "mqtt retain",
+ "topic_in_prefix": "prefix for input topics (topic_in_prefix)",
+ "topic_out_prefix": "prefix for output topics (topic_out_prefix)",
+ "version": "MySensors version",
+ "persistence_file": "persistence file (leave empty to auto-generate)"
+ }
+ }
+ },
+ "error": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "invalid_subscribe_topic": "Invalid subscribe topic",
+ "invalid_publish_topic": "Invalid publish topic",
+ "duplicate_topic": "Topic already in use",
+ "same_topic": "Subscribe and publish topics are the same",
+ "invalid_port": "Invalid port number",
+ "invalid_persistence_file": "Invalid persistence file",
+ "duplicate_persistence_file": "Persistence file already in use",
+ "invalid_ip": "Invalid IP address",
+ "invalid_serial": "Invalid serial port",
+ "invalid_device": "Invalid device",
+ "invalid_version": "Invalid MySensors version",
+ "not_a_number": "Please enter a number",
+ "port_out_of_range": "Port number must be at least 1 and at most 65535",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "invalid_subscribe_topic": "Invalid subscribe topic",
+ "invalid_publish_topic": "Invalid publish topic",
+ "duplicate_topic": "Topic already in use",
+ "same_topic": "Subscribe and publish topics are the same",
+ "invalid_port": "Invalid port number",
+ "invalid_persistence_file": "Invalid persistence file",
+ "duplicate_persistence_file": "Persistence file already in use",
+ "invalid_ip": "Invalid IP address",
+ "invalid_serial": "Invalid serial port",
+ "invalid_device": "Invalid device",
+ "invalid_version": "Invalid MySensors version",
+ "not_a_number": "Please enter a number",
+ "port_out_of_range": "Port number must be at least 1 and at most 65535",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ }
+ }
+}
diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py
index 0da8bfe7030f57..14911e11090fb4 100644
--- a/homeassistant/components/mysensors/switch.py
+++ b/homeassistant/components/mysensors/switch.py
@@ -1,4 +1,6 @@
"""Support for MySensors switches."""
+from typing import Callable
+
import voluptuous as vol
from homeassistant.components import mysensors
@@ -6,7 +8,11 @@
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
import homeassistant.helpers.config_validation as cv
-from .const import DOMAIN as MYSENSORS_DOMAIN, SERVICE_SEND_IR_CODE
+from . import on_unload
+from ...config_entries import ConfigEntry
+from ...helpers.dispatcher import async_dispatcher_connect
+from ...helpers.typing import HomeAssistantType
+from .const import DOMAIN as MYSENSORS_DOMAIN, MYSENSORS_DISCOVERY, SERVICE_SEND_IR_CODE
ATTR_IR_CODE = "V_IR_SEND"
@@ -15,8 +21,10 @@
)
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the mysensors platform for switches."""
+async def async_setup_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
+):
+ """Set up this platform for a specific ConfigEntry(==Gateway)."""
device_class_map = {
"S_DOOR": MySensorsSwitch,
"S_MOTION": MySensorsSwitch,
@@ -32,13 +40,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"S_MOISTURE": MySensorsSwitch,
"S_WATER_QUALITY": MySensorsSwitch,
}
- mysensors.setup_mysensors_platform(
- hass,
- DOMAIN,
- discovery_info,
- device_class_map,
- async_add_entities=async_add_entities,
- )
+
+ async def async_discover(discovery_info):
+ """Discover and add a MySensors switch."""
+ mysensors.setup_mysensors_platform(
+ hass,
+ DOMAIN,
+ discovery_info,
+ device_class_map,
+ async_add_entities=async_add_entities,
+ )
async def async_send_ir_code_service(service):
"""Set IR code as device state attribute."""
@@ -71,15 +82,20 @@ async def async_send_ir_code_service(service):
schema=SEND_IR_CODE_SERVICE_SCHEMA,
)
+ await on_unload(
+ hass,
+ config_entry,
+ async_dispatcher_connect(
+ hass,
+ MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
+ async_discover,
+ ),
+ )
+
class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity):
"""Representation of the value of a MySensors Switch child node."""
- @property
- def assumed_state(self):
- """Return True if unable to access real state of entity."""
- return self.gateway.optimistic
-
@property
def current_power_w(self):
"""Return the current power usage in W."""
@@ -96,7 +112,7 @@ async def async_turn_on(self, **kwargs):
self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, 1, ack=1
)
- if self.gateway.optimistic:
+ if self.assumed_state:
# Optimistically assume that switch has changed state
self._values[self.value_type] = STATE_ON
self.async_write_ha_state()
@@ -106,7 +122,7 @@ async def async_turn_off(self, **kwargs):
self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, 0, ack=1
)
- if self.gateway.optimistic:
+ if self.assumed_state:
# Optimistically assume that switch has changed state
self._values[self.value_type] = STATE_OFF
self.async_write_ha_state()
@@ -137,7 +153,7 @@ async def async_turn_on(self, **kwargs):
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_LIGHT, 1, ack=1
)
- if self.gateway.optimistic:
+ if self.assumed_state:
# Optimistically assume that switch has changed state
self._values[self.value_type] = self._ir_code
self._values[set_req.V_LIGHT] = STATE_ON
@@ -151,7 +167,7 @@ async def async_turn_off(self, **kwargs):
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_LIGHT, 0, ack=1
)
- if self.gateway.optimistic:
+ if self.assumed_state:
# Optimistically assume that switch has changed state
self._values[set_req.V_LIGHT] = STATE_OFF
self.async_write_ha_state()
diff --git a/homeassistant/components/mysensors/translations/bg.json b/homeassistant/components/mysensors/translations/bg.json
new file mode 100644
index 00000000000000..854e88b38b9f3c
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/bg.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430"
+ },
+ "error": {
+ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430"
+ },
+ "step": {
+ "gw_serial": {
+ "data": {
+ "device": "\u0421\u0435\u0440\u0438\u0435\u043d \u043f\u043e\u0440\u0442"
+ }
+ },
+ "gw_tcp": {
+ "data": {
+ "tcp_port": "\u043f\u043e\u0440\u0442"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/translations/ca.json b/homeassistant/components/mysensors/translations/ca.json
new file mode 100644
index 00000000000000..844d9e51da1de9
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/ca.json
@@ -0,0 +1,79 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "duplicate_persistence_file": "Fitxer de persist\u00e8ncia ja en \u00fas",
+ "duplicate_topic": "Topic ja en \u00fas",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "invalid_device": "Dispositiu no v\u00e0lid",
+ "invalid_ip": "Adre\u00e7a IP inv\u00e0lida",
+ "invalid_persistence_file": "Fitxer de persist\u00e8ncia inv\u00e0lid",
+ "invalid_port": "N\u00famero de port inv\u00e0lid",
+ "invalid_publish_topic": "Topic de publicaci\u00f3 inv\u00e0lid",
+ "invalid_serial": "Port s\u00e8rie inv\u00e0lid",
+ "invalid_subscribe_topic": "Topic de subscripci\u00f3 inv\u00e0lid",
+ "invalid_version": "Versi\u00f3 de MySensors inv\u00e0lida",
+ "not_a_number": "Introdueix un n\u00famero",
+ "port_out_of_range": "El n\u00famero de port ha d'estar entre 1 i 65535",
+ "same_topic": "Els topics de publicaci\u00f3 i subscripci\u00f3 son els mateixos",
+ "unknown": "Error inesperat"
+ },
+ "error": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "duplicate_persistence_file": "Fitxer de persist\u00e8ncia ja en \u00fas",
+ "duplicate_topic": "Topic ja en \u00fas",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "invalid_device": "Dispositiu no v\u00e0lid",
+ "invalid_ip": "Adre\u00e7a IP inv\u00e0lida",
+ "invalid_persistence_file": "Fitxer de persist\u00e8ncia inv\u00e0lid",
+ "invalid_port": "N\u00famero de port inv\u00e0lid",
+ "invalid_publish_topic": "Topic de publicaci\u00f3 inv\u00e0lid",
+ "invalid_serial": "Port s\u00e8rie inv\u00e0lid",
+ "invalid_subscribe_topic": "Topic de subscripci\u00f3 inv\u00e0lid",
+ "invalid_version": "Versi\u00f3 de MySensors inv\u00e0lida",
+ "not_a_number": "Introdueix un n\u00famero",
+ "port_out_of_range": "El n\u00famero de port ha d'estar entre 1 i 65535",
+ "same_topic": "Els topics de publicaci\u00f3 i subscripci\u00f3 son els mateixos",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "gw_mqtt": {
+ "data": {
+ "persistence_file": "fitxer de persist\u00e8ncia (deixa-ho buit per generar-lo autom\u00e0ticament)",
+ "retain": "retenci\u00f3 mqtt",
+ "topic_in_prefix": "prefix per als topics d'entrada (topic_in_prefix)",
+ "topic_out_prefix": "prefix per als topics de sortida (topic_out_prefix)",
+ "version": "Versi\u00f3 de MySensors"
+ },
+ "description": "Configuraci\u00f3 de passarel\u00b7la MQTT"
+ },
+ "gw_serial": {
+ "data": {
+ "baud_rate": "Velocitat, en baudis",
+ "device": "Port s\u00e8rie",
+ "persistence_file": "fitxer de persist\u00e8ncia (deixa-ho buit per generar-lo autom\u00e0ticament)",
+ "version": "Versi\u00f3 de MySensors"
+ },
+ "description": "Configuraci\u00f3 de passarel\u00b7la s\u00e8rie"
+ },
+ "gw_tcp": {
+ "data": {
+ "device": "Adre\u00e7a IP de la passarel\u00b7la",
+ "persistence_file": "fitxer de persist\u00e8ncia (deixa-ho buit per generar-lo autom\u00e0ticament)",
+ "tcp_port": "port",
+ "version": "Versi\u00f3 de MySensors"
+ },
+ "description": "Configuraci\u00f3 de passarel\u00b7la Ethernet"
+ },
+ "user": {
+ "data": {
+ "gateway_type": "Tipus de passarel\u00b7la"
+ },
+ "description": "Tria el m\u00e8tode de connexi\u00f3 a la passarel\u00b7la"
+ }
+ }
+ },
+ "title": "MySensors"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/translations/cs.json b/homeassistant/components/mysensors/translations/cs.json
new file mode 100644
index 00000000000000..abe47f046ff64b
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/cs.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "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",
+ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
+ },
+ "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",
+ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
+ }
+ },
+ "title": "MySensors"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/translations/de.json b/homeassistant/components/mysensors/translations/de.json
new file mode 100644
index 00000000000000..d05e2bdb47b61a
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/de.json
@@ -0,0 +1,58 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "invalid_device": "Ung\u00fcltiges Ger\u00e4t",
+ "invalid_ip": "Ung\u00fcltige IP-Adresse",
+ "invalid_serial": "Ung\u00fcltiger Serieller Port",
+ "invalid_version": "Ung\u00fcltige MySensors Version",
+ "not_a_number": "Bitte eine Nummer eingeben",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "error": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "invalid_device": "Ung\u00fcltiges Ger\u00e4t",
+ "invalid_ip": "Ung\u00fcltige IP-Adresse",
+ "invalid_serial": "Ung\u00fcltiger Serieller Port",
+ "invalid_version": "Ung\u00fcltige MySensors Version",
+ "not_a_number": "Bitte eine Nummer eingeben",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "gw_mqtt": {
+ "data": {
+ "retain": "MQTT behalten",
+ "version": "MySensors Version"
+ },
+ "description": "MQTT-Gateway einrichten"
+ },
+ "gw_serial": {
+ "data": {
+ "baud_rate": "Baudrate",
+ "device": "Serielle Schnittstelle",
+ "version": "MySensors Version"
+ },
+ "description": "Einrichtung des seriellen Gateways"
+ },
+ "gw_tcp": {
+ "data": {
+ "device": "IP-Adresse des Gateways",
+ "tcp_port": "Port",
+ "version": "MySensors Version"
+ },
+ "description": "Einrichtung des Ethernet-Gateways"
+ },
+ "user": {
+ "data": {
+ "gateway_type": "Gateway-Typ"
+ },
+ "description": "Verbindungsmethode zum Gateway w\u00e4hlen"
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/translations/en.json b/homeassistant/components/mysensors/translations/en.json
new file mode 100644
index 00000000000000..63af85488f0a09
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/en.json
@@ -0,0 +1,79 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured",
+ "cannot_connect": "Failed to connect",
+ "duplicate_persistence_file": "Persistence file already in use",
+ "duplicate_topic": "Topic already in use",
+ "invalid_auth": "Invalid authentication",
+ "invalid_device": "Invalid device",
+ "invalid_ip": "Invalid IP address",
+ "invalid_persistence_file": "Invalid persistence file",
+ "invalid_port": "Invalid port number",
+ "invalid_publish_topic": "Invalid publish topic",
+ "invalid_serial": "Invalid serial port",
+ "invalid_subscribe_topic": "Invalid subscribe topic",
+ "invalid_version": "Invalid MySensors version",
+ "not_a_number": "Please enter a number",
+ "port_out_of_range": "Port number must be at least 1 and at most 65535",
+ "same_topic": "Subscribe and publish topics are the same",
+ "unknown": "Unexpected error"
+ },
+ "error": {
+ "already_configured": "Device is already configured",
+ "cannot_connect": "Failed to connect",
+ "duplicate_persistence_file": "Persistence file already in use",
+ "duplicate_topic": "Topic already in use",
+ "invalid_auth": "Invalid authentication",
+ "invalid_device": "Invalid device",
+ "invalid_ip": "Invalid IP address",
+ "invalid_persistence_file": "Invalid persistence file",
+ "invalid_port": "Invalid port number",
+ "invalid_publish_topic": "Invalid publish topic",
+ "invalid_serial": "Invalid serial port",
+ "invalid_subscribe_topic": "Invalid subscribe topic",
+ "invalid_version": "Invalid MySensors version",
+ "not_a_number": "Please enter a number",
+ "port_out_of_range": "Port number must be at least 1 and at most 65535",
+ "same_topic": "Subscribe and publish topics are the same",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "gw_mqtt": {
+ "data": {
+ "persistence_file": "persistence file (leave empty to auto-generate)",
+ "retain": "mqtt retain",
+ "topic_in_prefix": "prefix for input topics (topic_in_prefix)",
+ "topic_out_prefix": "prefix for output topics (topic_out_prefix)",
+ "version": "MySensors version"
+ },
+ "description": "MQTT gateway setup"
+ },
+ "gw_serial": {
+ "data": {
+ "baud_rate": "baud rate",
+ "device": "Serial port",
+ "persistence_file": "persistence file (leave empty to auto-generate)",
+ "version": "MySensors version"
+ },
+ "description": "Serial gateway setup"
+ },
+ "gw_tcp": {
+ "data": {
+ "device": "IP address of the gateway",
+ "persistence_file": "persistence file (leave empty to auto-generate)",
+ "tcp_port": "port",
+ "version": "MySensors version"
+ },
+ "description": "Ethernet gateway setup"
+ },
+ "user": {
+ "data": {
+ "gateway_type": "Gateway type"
+ },
+ "description": "Choose connection method to the gateway"
+ }
+ }
+ },
+ "title": "MySensors"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/translations/es.json b/homeassistant/components/mysensors/translations/es.json
new file mode 100644
index 00000000000000..2a4b30910d17de
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/es.json
@@ -0,0 +1,75 @@
+{
+ "config": {
+ "abort": {
+ "duplicate_persistence_file": "Archivo de persistencia ya en uso",
+ "duplicate_topic": "Tema ya en uso",
+ "invalid_device": "Dispositivo no v\u00e1lido",
+ "invalid_ip": "Direcci\u00f3n IP no v\u00e1lida",
+ "invalid_persistence_file": "Archivo de persistencia no v\u00e1lido",
+ "invalid_port": "N\u00famero de puerto no v\u00e1lido",
+ "invalid_publish_topic": "Tema de publicaci\u00f3n no v\u00e1lido",
+ "invalid_serial": "Puerto serie no v\u00e1lido",
+ "invalid_subscribe_topic": "Tema de suscripci\u00f3n no v\u00e1lido",
+ "invalid_version": "Versi\u00f3n inv\u00e1lida de MySensors",
+ "not_a_number": "Por favor, introduzca un n\u00famero",
+ "port_out_of_range": "El n\u00famero de puerto debe ser como m\u00ednimo 1 y como m\u00e1ximo 65535",
+ "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos"
+ },
+ "error": {
+ "already_configured": "El dispositivo ya est\u00e1 configurado",
+ "cannot_connect": "No se pudo conectar",
+ "duplicate_persistence_file": "Archivo de persistencia ya en uso",
+ "duplicate_topic": "Tema ya en uso",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
+ "invalid_device": "Dispositivo no v\u00e1lido",
+ "invalid_ip": "Direcci\u00f3n IP no v\u00e1lida",
+ "invalid_persistence_file": "Archivo de persistencia no v\u00e1lido",
+ "invalid_port": "N\u00famero de puerto no v\u00e1lido",
+ "invalid_publish_topic": "Tema de publicaci\u00f3n no v\u00e1lido",
+ "invalid_serial": "Puerto serie no v\u00e1lido",
+ "invalid_subscribe_topic": "Tema de suscripci\u00f3n no v\u00e1lido",
+ "invalid_version": "Versi\u00f3n no v\u00e1lida de MySensors",
+ "not_a_number": "Por favor, introduce un n\u00famero",
+ "port_out_of_range": "El n\u00famero de puerto debe ser como m\u00ednimo 1 y como m\u00e1ximo 65535",
+ "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos",
+ "unknown": "Error inesperado"
+ },
+ "step": {
+ "gw_mqtt": {
+ "data": {
+ "persistence_file": "archivo de persistencia (d\u00e9jelo vac\u00edo para que se genere autom\u00e1ticamente)",
+ "retain": "retener mqtt",
+ "topic_in_prefix": "prefijo para los temas de entrada (topic_in_prefix)",
+ "topic_out_prefix": "prefijo para los temas de salida (topic_out_prefix)",
+ "version": "Versi\u00f3n de MySensors"
+ },
+ "description": "Configuraci\u00f3n del gateway MQTT"
+ },
+ "gw_serial": {
+ "data": {
+ "baud_rate": "tasa de baudios",
+ "device": "Puerto serie",
+ "persistence_file": "archivo de persistencia (d\u00e9jelo vac\u00edo para que se genere autom\u00e1ticamente)",
+ "version": "Versi\u00f3n de MySensors"
+ },
+ "description": "Configuraci\u00f3n de la pasarela en serie"
+ },
+ "gw_tcp": {
+ "data": {
+ "device": "Direcci\u00f3n IP de la pasarela",
+ "persistence_file": "archivo de persistencia (d\u00e9jelo vac\u00edo para que se genere autom\u00e1ticamente)",
+ "tcp_port": "Puerto",
+ "version": "Versi\u00f3n de MySensores"
+ },
+ "description": "Configuraci\u00f3n de la pasarela Ethernet"
+ },
+ "user": {
+ "data": {
+ "gateway_type": "Tipo de pasarela"
+ },
+ "description": "Elija el m\u00e9todo de conexi\u00f3n con la pasarela"
+ }
+ }
+ },
+ "title": "MySensors"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/translations/et.json b/homeassistant/components/mysensors/translations/et.json
new file mode 100644
index 00000000000000..0682610be97fef
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/et.json
@@ -0,0 +1,79 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "cannot_connect": "\u00dchendamine nurjus",
+ "duplicate_persistence_file": "P\u00fcsivusfail on juba kasutusel",
+ "duplicate_topic": "Teema on juba kasutusel",
+ "invalid_auth": "Vigane autentimine",
+ "invalid_device": "Sobimatu seade",
+ "invalid_ip": "Sobimatu IP-aadress",
+ "invalid_persistence_file": "Sobimatu p\u00fcsivusfail",
+ "invalid_port": "Lubamatu pordinumber",
+ "invalid_publish_topic": "Kehtetu avaldamisteema",
+ "invalid_serial": "Sobimatu jadaport",
+ "invalid_subscribe_topic": "Kehtetu tellimisteema",
+ "invalid_version": "Sobimatu MySensors versioon",
+ "not_a_number": "Sisesta number",
+ "port_out_of_range": "Pordi number peab olema v\u00e4hemalt 1 ja k\u00f5ige rohkem 65535",
+ "same_topic": "Tellimise ja avaldamise teemad kattuvad",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "error": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "cannot_connect": "\u00dchendamine nurjus",
+ "duplicate_persistence_file": "P\u00fcsivusfail on juba kasutusel",
+ "duplicate_topic": "Teema on juba kasutusel",
+ "invalid_auth": "Vigane autentimine",
+ "invalid_device": "Sobimatu seade",
+ "invalid_ip": "Sobimatu IP-aadress",
+ "invalid_persistence_file": "Sobimatu p\u00fcsivusfail",
+ "invalid_port": "Lubamatu pordinumber",
+ "invalid_publish_topic": "Kehtetu avaldamisteema",
+ "invalid_serial": "Sobimatu jadaport",
+ "invalid_subscribe_topic": "Kehtetu tellimisteema",
+ "invalid_version": "Sobimatu MySensors versioon",
+ "not_a_number": "Sisesta number",
+ "port_out_of_range": "Pordi number peab olema v\u00e4hemalt 1 ja k\u00f5ige rohkem 65535",
+ "same_topic": "Tellimise ja avaldamise teemad kattuvad",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "gw_mqtt": {
+ "data": {
+ "persistence_file": "p\u00fcsivusfail (j\u00e4ta automaatse genereerimise jaoks t\u00fchjaks)",
+ "retain": "mqtt oleku s\u00e4ilitamine",
+ "topic_in_prefix": "sisendteemade eesliide (topic_in_prefix)",
+ "topic_out_prefix": "v\u00e4ljunditeemade eesliide (topic_out_prefix)",
+ "version": "MySensors versioon"
+ },
+ "description": "MQTT-l\u00fc\u00fcsi seadistamine"
+ },
+ "gw_serial": {
+ "data": {
+ "baud_rate": "andmeedastuskiirus",
+ "device": "Jadaport",
+ "persistence_file": "p\u00fcsivusfail (j\u00e4ta automaatse genereerimise jaoks t\u00fchjaks)",
+ "version": "MySensors versioon"
+ },
+ "description": "Jadal\u00fc\u00fcsi h\u00e4\u00e4lestus"
+ },
+ "gw_tcp": {
+ "data": {
+ "device": "L\u00fc\u00fcsi IP-aadress",
+ "persistence_file": "p\u00fcsivusfail (j\u00e4ta automaatse genereerimise jaoks t\u00fchjaks)",
+ "tcp_port": "port",
+ "version": "MySensors versioon"
+ },
+ "description": "Etherneti l\u00fc\u00fcsi seadistamine"
+ },
+ "user": {
+ "data": {
+ "gateway_type": "L\u00fc\u00fcsi t\u00fc\u00fcp"
+ },
+ "description": "Vali l\u00fc\u00fcsi \u00fchendusviis"
+ }
+ }
+ },
+ "title": "MySensors"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/translations/fr.json b/homeassistant/components/mysensors/translations/fr.json
new file mode 100644
index 00000000000000..00f9831c035b3f
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/fr.json
@@ -0,0 +1,79 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
+ "cannot_connect": "\u00c9chec de connexion",
+ "duplicate_persistence_file": "Fichier de persistance d\u00e9j\u00e0 utilis\u00e9",
+ "duplicate_topic": "Sujet d\u00e9j\u00e0 utilis\u00e9",
+ "invalid_auth": "Authentification invalide",
+ "invalid_device": "Appareil non valide",
+ "invalid_ip": "Adresse IP non valide",
+ "invalid_persistence_file": "Fichier de persistance non valide",
+ "invalid_port": "Num\u00e9ro de port non valide",
+ "invalid_publish_topic": "Sujet de publication non valide",
+ "invalid_serial": "Port s\u00e9rie non valide",
+ "invalid_subscribe_topic": "Sujet d'abonnement non valide",
+ "invalid_version": "Version de MySensors non valide",
+ "not_a_number": "Veuillez saisir un nombre",
+ "port_out_of_range": "Le num\u00e9ro de port doit \u00eatre au moins 1 et au plus 65535",
+ "same_topic": "Les sujets de souscription et de publication sont identiques",
+ "unknown": "Erreur inattendue"
+ },
+ "error": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
+ "cannot_connect": "\u00c9chec de connexion",
+ "duplicate_persistence_file": "Fichier de persistance d\u00e9j\u00e0 utilis\u00e9",
+ "duplicate_topic": "Sujet d\u00e9j\u00e0 utilis\u00e9",
+ "invalid_auth": "Authentification invalide",
+ "invalid_device": "Appareil non valide",
+ "invalid_ip": "Adresse IP non valide",
+ "invalid_persistence_file": "Fichier de persistance non valide",
+ "invalid_port": "Num\u00e9ro de port non valide",
+ "invalid_publish_topic": "Sujet de publication non valide",
+ "invalid_serial": "Port s\u00e9rie non valide",
+ "invalid_subscribe_topic": "Sujet d'abonnement non valide",
+ "invalid_version": "Version de MySensors non valide",
+ "not_a_number": "Veuillez saisir un nombre",
+ "port_out_of_range": "Le num\u00e9ro de port doit \u00eatre au moins 1 et au plus 65535",
+ "same_topic": "Les sujets de souscription et de publication sont identiques",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "gw_mqtt": {
+ "data": {
+ "persistence_file": "fichier de persistance (laissez vide pour g\u00e9n\u00e9rer automatiquement)",
+ "retain": "mqtt conserver",
+ "topic_in_prefix": "pr\u00e9fixe pour les sujets d\u2019entr\u00e9e (topic_in_prefix)",
+ "topic_out_prefix": "pr\u00e9fixe pour les sujets de sortie (topic_out_prefix)",
+ "version": "Version de MySensors"
+ },
+ "description": "Configuration de la passerelle MQTT"
+ },
+ "gw_serial": {
+ "data": {
+ "baud_rate": "d\u00e9bit en bauds",
+ "device": "Port s\u00e9rie",
+ "persistence_file": "fichier de persistance (laissez vide pour g\u00e9n\u00e9rer automatiquement)",
+ "version": "Version de MySensors"
+ },
+ "description": "Configuration de la passerelle s\u00e9rie"
+ },
+ "gw_tcp": {
+ "data": {
+ "device": "Adresse IP de la passerelle",
+ "persistence_file": "fichier de persistance (laisser vide pour g\u00e9n\u00e9rer automatiquement)",
+ "tcp_port": "port",
+ "version": "Version de MySensors"
+ },
+ "description": "Configuration de la passerelle Ethernet"
+ },
+ "user": {
+ "data": {
+ "gateway_type": "Type de passerelle"
+ },
+ "description": "Choisissez la m\u00e9thode de connexion \u00e0 la passerelle"
+ }
+ }
+ },
+ "title": "MySensors"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/translations/hu.json b/homeassistant/components/mysensors/translations/hu.json
new file mode 100644
index 00000000000000..fefe3fd4b6c290
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/hu.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "invalid_device": "\u00c9rv\u00e9nytelen eszk\u00f6z",
+ "not_a_number": "Adj meg egy sz\u00e1mot.",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "error": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "invalid_serial": "\u00c9rv\u00e9nytelen soros port",
+ "invalid_version": "\u00c9rv\u00e9nytelen MySensors verzi\u00f3",
+ "port_out_of_range": "A portsz\u00e1mnak legal\u00e1bb 1-nek \u00e9s legfeljebb 65535-nek kell lennie",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "gw_mqtt": {
+ "data": {
+ "version": "MySensors verzi\u00f3"
+ }
+ },
+ "gw_serial": {
+ "data": {
+ "version": "MySensors verzi\u00f3"
+ }
+ },
+ "gw_tcp": {
+ "data": {
+ "tcp_port": "port",
+ "version": "MySensors verzi\u00f3"
+ }
+ }
+ }
+ },
+ "title": "MySensors"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/translations/id.json b/homeassistant/components/mysensors/translations/id.json
new file mode 100644
index 00000000000000..e982250b09c03e
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/id.json
@@ -0,0 +1,75 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung",
+ "duplicate_persistence_file": "File persistensi sudah digunakan",
+ "duplicate_topic": "Topik sudah digunakan",
+ "invalid_auth": "Autentikasi tidak valid",
+ "invalid_device": "Perangkat tidak valid",
+ "invalid_ip": "Alamat IP tidak valid",
+ "invalid_persistence_file": "File persistensi tidak valid",
+ "invalid_port": "Nomor port tidak valid",
+ "invalid_serial": "Port serial tidak valid",
+ "invalid_subscribe_topic": "Topik langganan tidak valid",
+ "invalid_version": "Versi MySensors tidak valid",
+ "not_a_number": "Masukkan angka",
+ "port_out_of_range": "Nilai port minimal 1 dan maksimal 65535",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "error": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung",
+ "duplicate_persistence_file": "File persistensi sudah digunakan",
+ "duplicate_topic": "Topik sudah digunakan",
+ "invalid_auth": "Autentikasi tidak valid",
+ "invalid_device": "Perangkat tidak valid",
+ "invalid_ip": "Alamat IP tidak valid",
+ "invalid_persistence_file": "File persistensi tidak valid",
+ "invalid_port": "Nomor port tidak valid",
+ "invalid_serial": "Port serial tidak valid",
+ "invalid_subscribe_topic": "Topik langganan tidak valid",
+ "invalid_version": "Versi MySensors tidak valid",
+ "not_a_number": "Masukkan angka",
+ "port_out_of_range": "Nilai port minimal 1 dan maksimal 65535",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "gw_mqtt": {
+ "data": {
+ "persistence_file": "file persistensi (kosongkan untuk dihasilkan otomatis)",
+ "retain": "mqtt retain",
+ "topic_in_prefix": "prefiks untuk topik input (topic_in_prefix)",
+ "topic_out_prefix": "prefiks untuk topik output (topic_out_prefix)",
+ "version": "Versi MySensors"
+ },
+ "description": "Penyiapan gateway MQTT"
+ },
+ "gw_serial": {
+ "data": {
+ "baud_rate": "tingkat baud",
+ "device": "Port serial",
+ "persistence_file": "file persistensi (kosongkan untuk dihasilkan otomatis)",
+ "version": "Versi MySensors"
+ },
+ "description": "Penyiapan gateway serial"
+ },
+ "gw_tcp": {
+ "data": {
+ "device": "Alamat IP gateway",
+ "persistence_file": "file persistensi (kosongkan untuk dihasilkan otomatis)",
+ "tcp_port": "port",
+ "version": "Versi MySensors"
+ },
+ "description": "Pengaturan gateway Ethernet"
+ },
+ "user": {
+ "data": {
+ "gateway_type": "Jenis gateway"
+ },
+ "description": "Pilih metode koneksi ke gateway"
+ }
+ }
+ },
+ "title": "MySensors"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/translations/it.json b/homeassistant/components/mysensors/translations/it.json
new file mode 100644
index 00000000000000..f256ddb95eb274
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/it.json
@@ -0,0 +1,79 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "cannot_connect": "Impossibile connettersi",
+ "duplicate_persistence_file": "File di persistenza gi\u00e0 in uso",
+ "duplicate_topic": "Argomento gi\u00e0 in uso",
+ "invalid_auth": "Autenticazione non valida",
+ "invalid_device": "Dispositivo non valido",
+ "invalid_ip": "Indirizzo IP non valido",
+ "invalid_persistence_file": "File di persistenza non valido",
+ "invalid_port": "Numero di porta non valido",
+ "invalid_publish_topic": "Argomento di pubblicazione non valido",
+ "invalid_serial": "Porta seriale non valida",
+ "invalid_subscribe_topic": "Argomento di sottoscrizione non valido",
+ "invalid_version": "Versione di MySensors non valida",
+ "not_a_number": "Per favore inserisci un numero",
+ "port_out_of_range": "Il numero di porta deve essere almeno 1 e al massimo 65535",
+ "same_topic": "Gli argomenti di sottoscrizione e pubblicazione sono gli stessi",
+ "unknown": "Errore imprevisto"
+ },
+ "error": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "cannot_connect": "Impossibile connettersi",
+ "duplicate_persistence_file": "File di persistenza gi\u00e0 in uso",
+ "duplicate_topic": "Argomento gi\u00e0 in uso",
+ "invalid_auth": "Autenticazione non valida",
+ "invalid_device": "Dispositivo non valido",
+ "invalid_ip": "Indirizzo IP non valido",
+ "invalid_persistence_file": "File di persistenza non valido",
+ "invalid_port": "Numero di porta non valido",
+ "invalid_publish_topic": "Argomento di pubblicazione non valido",
+ "invalid_serial": "Porta seriale non valida",
+ "invalid_subscribe_topic": "Argomento di sottoscrizione non valido",
+ "invalid_version": "Versione di MySensors non valida",
+ "not_a_number": "Per favore inserisci un numero",
+ "port_out_of_range": "Il numero di porta deve essere almeno 1 e al massimo 65535",
+ "same_topic": "Gli argomenti di sottoscrizione e pubblicazione sono gli stessi",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "gw_mqtt": {
+ "data": {
+ "persistence_file": "file di persistenza (lasciare vuoto per generare automaticamente)",
+ "retain": "mqtt conserva",
+ "topic_in_prefix": "prefisso per argomenti di input (topic_in_prefix)",
+ "topic_out_prefix": "prefisso per argomenti di output (topic_out_prefix)",
+ "version": "Versione MySensors"
+ },
+ "description": "Configurazione del gateway MQTT"
+ },
+ "gw_serial": {
+ "data": {
+ "baud_rate": "velocit\u00e0 di trasmissione",
+ "device": "Porta seriale",
+ "persistence_file": "file di persistenza (lasciare vuoto per generare automaticamente)",
+ "version": "Versione MySensors"
+ },
+ "description": "Configurazione del gateway seriale"
+ },
+ "gw_tcp": {
+ "data": {
+ "device": "Indirizzo IP del gateway",
+ "persistence_file": "file di persistenza (lasciare vuoto per generare automaticamente)",
+ "tcp_port": "porta",
+ "version": "Versione MySensors"
+ },
+ "description": "Configurazione del gateway Ethernet"
+ },
+ "user": {
+ "data": {
+ "gateway_type": "Tipo di gateway"
+ },
+ "description": "Scegli il metodo di connessione al gateway"
+ }
+ }
+ },
+ "title": "MySensors"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/translations/ko.json b/homeassistant/components/mysensors/translations/ko.json
new file mode 100644
index 00000000000000..e57f60aafbffa6
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/ko.json
@@ -0,0 +1,79 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "duplicate_persistence_file": "\uc9c0\uc18d\uc131 \ud30c\uc77c\uc774 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4",
+ "duplicate_topic": "\ud1a0\ud53d\uc774 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_device": "\uae30\uae30\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_ip": "IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_persistence_file": "\uc9c0\uc18d\uc131 \ud30c\uc77c\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_port": "\ud3ec\ud2b8 \ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_publish_topic": "\ubc1c\ud589 \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_serial": "\uc2dc\ub9ac\uc5bc \ud3ec\ud2b8\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_subscribe_topic": "\uad6c\ub3c5 \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_version": "MySensors \ubc84\uc804\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "not_a_number": "\uc22b\uc790\ub85c \uc785\ub825\ud574\uc8fc\uc138\uc694",
+ "port_out_of_range": "\ud3ec\ud2b8 \ubc88\ud638\ub294 1 \uc774\uc0c1 65535 \uc774\ud558\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4",
+ "same_topic": "\uad6c\ub3c5 \ubc0f \ubc1c\ud589 \ud1a0\ud53d\uc740 \ub3d9\uc77c\ud569\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "duplicate_persistence_file": "\uc9c0\uc18d\uc131 \ud30c\uc77c\uc774 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4",
+ "duplicate_topic": "\ud1a0\ud53d\uc774 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_device": "\uae30\uae30\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_ip": "IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_persistence_file": "\uc9c0\uc18d\uc131 \ud30c\uc77c\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_port": "\ud3ec\ud2b8 \ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_publish_topic": "\ubc1c\ud589 \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_serial": "\uc2dc\ub9ac\uc5bc \ud3ec\ud2b8\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_subscribe_topic": "\uad6c\ub3c5 \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_version": "MySensors \ubc84\uc804\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "not_a_number": "\uc22b\uc790\ub85c \uc785\ub825\ud574\uc8fc\uc138\uc694",
+ "port_out_of_range": "\ud3ec\ud2b8 \ubc88\ud638\ub294 1 \uc774\uc0c1 65535 \uc774\ud558\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4",
+ "same_topic": "\uad6c\ub3c5 \ubc0f \ubc1c\ud589 \ud1a0\ud53d\uc740 \ub3d9\uc77c\ud569\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "gw_mqtt": {
+ "data": {
+ "persistence_file": "\uc9c0\uc18d\uc131 \ud30c\uc77c(\uc790\ub3d9\uc73c\ub85c \uc0dd\uc131\ud558\ub824\uba74 \ube44\uc6cc\ub450\uc138\uc694)",
+ "retain": "mqtt retain",
+ "topic_in_prefix": "\uc785\ub825 \ud1a0\ud53d\uc758 \uc811\ub450\uc0ac (topic_in_prefix)",
+ "topic_out_prefix": "\ucd9c\ub825 \ud1a0\ud53d\uc758 \uc811\ub450\uc0ac (topic_out_prefix)",
+ "version": "MySensors \ubc84\uc804"
+ },
+ "description": "MQTT \uac8c\uc774\ud2b8\uc6e8\uc774 \uc124\uc815"
+ },
+ "gw_serial": {
+ "data": {
+ "baud_rate": "\uc804\uc1a1 \uc18d\ub3c4",
+ "device": "\uc2dc\ub9ac\uc5bc \ud3ec\ud2b8",
+ "persistence_file": "\uc9c0\uc18d\uc131 \ud30c\uc77c(\uc790\ub3d9\uc73c\ub85c \uc0dd\uc131\ud558\ub824\uba74 \ube44\uc6cc\ub450\uc138\uc694)",
+ "version": "MySensors \ubc84\uc804"
+ },
+ "description": "\uc2dc\ub9ac\uc5bc \uac8c\uc774\ud2b8\uc6e8\uc774 \uc124\uc815"
+ },
+ "gw_tcp": {
+ "data": {
+ "device": "\uac8c\uc774\ud2b8\uc6e8\uc774\uc758 IP \uc8fc\uc18c",
+ "persistence_file": "\uc9c0\uc18d\uc131 \ud30c\uc77c(\uc790\ub3d9\uc73c\ub85c \uc0dd\uc131\ud558\ub824\uba74 \ube44\uc6cc\ub450\uc138\uc694)",
+ "tcp_port": "\ud3ec\ud2b8",
+ "version": "MySensors \ubc84\uc804"
+ },
+ "description": "\uc774\ub354\ub137 \uac8c\uc774\ud2b8\uc6e8\uc774 \uc124\uc815"
+ },
+ "user": {
+ "data": {
+ "gateway_type": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uc720\ud615"
+ },
+ "description": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uc5f0\uacb0 \ubc29\ubc95\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694"
+ }
+ }
+ },
+ "title": "MySensors"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/translations/nl.json b/homeassistant/components/mysensors/translations/nl.json
new file mode 100644
index 00000000000000..49ddf987cef645
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/nl.json
@@ -0,0 +1,79 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd",
+ "cannot_connect": "Kan geen verbinding maken",
+ "duplicate_persistence_file": "Persistentiebestand al in gebruik",
+ "duplicate_topic": "Topic is al in gebruik",
+ "invalid_auth": "Ongeldige authenticatie",
+ "invalid_device": "Ongeldig apparaat",
+ "invalid_ip": "Ongeldig IP-adres",
+ "invalid_persistence_file": "Ongeldig persistentiebestand",
+ "invalid_port": "Ongeldig poortnummer",
+ "invalid_publish_topic": "Ongeldig publiceer topic",
+ "invalid_serial": "Ongeldige seri\u00eble poort",
+ "invalid_subscribe_topic": "Ongeldig abonneeronderwerp",
+ "invalid_version": "Ongeldige MySensors-versie",
+ "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",
+ "unknown": "Onverwachte fout"
+ },
+ "error": {
+ "already_configured": "Apparaat is al geconfigureerd",
+ "cannot_connect": "Kan geen verbinding maken",
+ "duplicate_persistence_file": "Persistentiebestand al in gebruik",
+ "duplicate_topic": "Topic is al in gebruik",
+ "invalid_auth": "Ongeldige authenticatie",
+ "invalid_device": "Ongeldig apparaat",
+ "invalid_ip": "Ongeldig IP-adres",
+ "invalid_persistence_file": "Ongeldig persistentiebestand",
+ "invalid_port": "Ongeldig poortnummer",
+ "invalid_publish_topic": "ongeldig publiceer topic",
+ "invalid_serial": "Ongeldige seri\u00eble poort",
+ "invalid_subscribe_topic": "Ongeldig abonneer topic",
+ "invalid_version": "Ongeldige MySensors-versie",
+ "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",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "gw_mqtt": {
+ "data": {
+ "persistence_file": "persistentiebestand (leeg laten om automatisch te genereren)",
+ "retain": "mqtt behouden",
+ "topic_in_prefix": "prefix voor inkomende topics (topic_in_prefix)",
+ "topic_out_prefix": "prefix voor uitgaande topics (topic_out_prefix)",
+ "version": "MySensors-versie"
+ },
+ "description": "MQTT-gateway instellen"
+ },
+ "gw_serial": {
+ "data": {
+ "baud_rate": "baudrate",
+ "device": "Seri\u00eble poort",
+ "persistence_file": "persistentiebestand (leeg laten om automatisch te genereren)",
+ "version": "MySensors-versie"
+ },
+ "description": "Seri\u00eble gateway setup"
+ },
+ "gw_tcp": {
+ "data": {
+ "device": "IP-adres van de gateway",
+ "persistence_file": "persistentiebestand (leeg laten om automatisch te genereren)",
+ "tcp_port": "Poort",
+ "version": "MySensors-versie"
+ },
+ "description": "Ethernet gateway instellen"
+ },
+ "user": {
+ "data": {
+ "gateway_type": "Gateway type"
+ },
+ "description": "Kies de verbindingsmethode met de gateway"
+ }
+ }
+ },
+ "title": "MySensors"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/translations/no.json b/homeassistant/components/mysensors/translations/no.json
new file mode 100644
index 00000000000000..9d028260a7686a
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/no.json
@@ -0,0 +1,79 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert",
+ "cannot_connect": "Tilkobling mislyktes",
+ "duplicate_persistence_file": "Persistensfil allerede i bruk",
+ "duplicate_topic": "Emnet er allerede i bruk",
+ "invalid_auth": "Ugyldig godkjenning",
+ "invalid_device": "Ugyldig enhet",
+ "invalid_ip": "Ugyldig IP-adresse",
+ "invalid_persistence_file": "Ugyldig utholdenhetsfil",
+ "invalid_port": "Ugyldig portnummer",
+ "invalid_publish_topic": "Ugyldig publiseringsemne",
+ "invalid_serial": "Ugyldig serieport",
+ "invalid_subscribe_topic": "Ugyldig abonnementsemne",
+ "invalid_version": "Ugyldig MySensors-versjon",
+ "not_a_number": "Vennligst skriv inn et nummer",
+ "port_out_of_range": "Portnummer m\u00e5 v\u00e6re minst 1 og maksimalt 65535",
+ "same_topic": "Abonner og publiser emner er de samme",
+ "unknown": "Uventet feil"
+ },
+ "error": {
+ "already_configured": "Enheten er allerede konfigurert",
+ "cannot_connect": "Tilkobling mislyktes",
+ "duplicate_persistence_file": "Persistensfil allerede i bruk",
+ "duplicate_topic": "Emnet er allerede i bruk",
+ "invalid_auth": "Ugyldig godkjenning",
+ "invalid_device": "Ugyldig enhet",
+ "invalid_ip": "Ugyldig IP-adresse",
+ "invalid_persistence_file": "Ugyldig utholdenhetsfil",
+ "invalid_port": "Ugyldig portnummer",
+ "invalid_publish_topic": "Ugyldig publiseringsemne",
+ "invalid_serial": "Ugyldig serieport",
+ "invalid_subscribe_topic": "Ugyldig abonnementsemne",
+ "invalid_version": "Ugyldig MySensors-versjon",
+ "not_a_number": "Vennligst skriv inn et nummer",
+ "port_out_of_range": "Portnummer m\u00e5 v\u00e6re minst 1 og maksimalt 65535",
+ "same_topic": "Abonner og publiser emner er de samme",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "gw_mqtt": {
+ "data": {
+ "persistence_file": "Persistensfil (la den v\u00e6re tom for automatisk generering)",
+ "retain": "mqtt beholde",
+ "topic_in_prefix": "prefiks for input-emner (topic_in_prefix)",
+ "topic_out_prefix": "prefiks for utgangstemaer (topic_out_prefix)",
+ "version": "MySensors versjon"
+ },
+ "description": "MQTT gateway-oppsett"
+ },
+ "gw_serial": {
+ "data": {
+ "baud_rate": "Overf\u00f8ringshastighet",
+ "device": "Seriell port",
+ "persistence_file": "persistensfil (la den v\u00e6re tom for automatisk generering)",
+ "version": "MySensors versjon"
+ },
+ "description": "Seriell gatewayoppsett"
+ },
+ "gw_tcp": {
+ "data": {
+ "device": "IP-adressen til gatewayen",
+ "persistence_file": "persistensfil (la den v\u00e6re tom for automatisk generering)",
+ "tcp_port": "Port",
+ "version": "MySensors versjon"
+ },
+ "description": "Ethernet gateway-oppsett"
+ },
+ "user": {
+ "data": {
+ "gateway_type": ""
+ },
+ "description": "Velg tilkoblingsmetode til gatewayen"
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/translations/pl.json b/homeassistant/components/mysensors/translations/pl.json
new file mode 100644
index 00000000000000..fa67ffe4030422
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/pl.json
@@ -0,0 +1,79 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "duplicate_persistence_file": "Plik danych z sensora jest ju\u017c w u\u017cyciu",
+ "duplicate_topic": "Temat jest ju\u017c w u\u017cyciu",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "invalid_device": "Nieprawid\u0142owe urz\u0105dzenie",
+ "invalid_ip": "Nieprawid\u0142owy adres IP",
+ "invalid_persistence_file": "Nieprawid\u0142owy plik danych z sensora",
+ "invalid_port": "Nieprawid\u0142owy numer portu",
+ "invalid_publish_topic": "Nieprawid\u0142owy temat \"publish\"",
+ "invalid_serial": "Nieprawid\u0142owy port szeregowy",
+ "invalid_subscribe_topic": "Nieprawid\u0142owy temat \"subscribe\"",
+ "invalid_version": "Nieprawid\u0142owa wersja MySensors",
+ "not_a_number": "Prosz\u0119 wpisa\u0107 numer",
+ "port_out_of_range": "Numer portu musi by\u0107 pomi\u0119dzy 1 a 65535",
+ "same_topic": "Tematy \"subscribe\" i \"publish\" s\u0105 takie same",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "error": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "duplicate_persistence_file": "Plik danych z sensora jest ju\u017c w u\u017cyciu",
+ "duplicate_topic": "Temat jest ju\u017c w u\u017cyciu",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "invalid_device": "Nieprawid\u0142owe urz\u0105dzenie",
+ "invalid_ip": "Nieprawid\u0142owy adres IP",
+ "invalid_persistence_file": "Nieprawid\u0142owy plik danych z sensora",
+ "invalid_port": "Nieprawid\u0142owy numer portu",
+ "invalid_publish_topic": "Nieprawid\u0142owy temat \"publish\"",
+ "invalid_serial": "Nieprawid\u0142owy port szeregowy",
+ "invalid_subscribe_topic": "Nieprawid\u0142owy temat \"subscribe\"",
+ "invalid_version": "Nieprawid\u0142owa wersja MySensors",
+ "not_a_number": "Prosz\u0119 wpisa\u0107 numer",
+ "port_out_of_range": "Numer portu musi by\u0107 pomi\u0119dzy 1 a 65535",
+ "same_topic": "Tematy \"subscribe\" i \"publish\" s\u0105 takie same",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "gw_mqtt": {
+ "data": {
+ "persistence_file": "plik danych z sensora (pozostaw puste. aby wygenerowa\u0107 automatycznie)",
+ "retain": "flaga \"retain\" dla mqtt",
+ "topic_in_prefix": "prefix tematu wej\u015bciowego (topic_in_prefix)",
+ "topic_out_prefix": "prefix tematu wyj\u015bciowego (topic_out_prefix)",
+ "version": "Wersja MySensors"
+ },
+ "description": "Konfiguracja bramki MQTT"
+ },
+ "gw_serial": {
+ "data": {
+ "baud_rate": "szybko\u015b\u0107 transmisji (baud rate)",
+ "device": "Port szeregowy",
+ "persistence_file": "plik danych z sensora (pozostaw puste. aby wygenerowa\u0107 automatycznie)",
+ "version": "Wersja MySensors"
+ },
+ "description": "Konfiguracja bramki szeregowej"
+ },
+ "gw_tcp": {
+ "data": {
+ "device": "Adres IP bramki",
+ "persistence_file": "plik danych z sensora (pozostaw puste. aby wygenerowa\u0107 automatycznie)",
+ "tcp_port": "port",
+ "version": "Wersja MySensors"
+ },
+ "description": "Konfiguracja bramki LAN"
+ },
+ "user": {
+ "data": {
+ "gateway_type": "Typ bramki"
+ },
+ "description": "Wybierz metod\u0119 po\u0142\u0105czenia z bramk\u0105"
+ }
+ }
+ },
+ "title": "MySensors"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/translations/ru.json b/homeassistant/components/mysensors/translations/ru.json
new file mode 100644
index 00000000000000..6267970901761a
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/ru.json
@@ -0,0 +1,79 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "duplicate_persistence_file": "\u042d\u0442\u043e\u0442 \u0444\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.",
+ "duplicate_topic": "\u042d\u0442\u043e\u0442 \u0442\u043e\u043f\u0438\u043a \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\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_device": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.",
+ "invalid_ip": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441.",
+ "invalid_persistence_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439.",
+ "invalid_port": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430.",
+ "invalid_publish_topic": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u0434\u043b\u044f \u043f\u0443\u0431\u043b\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "invalid_serial": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442.",
+ "invalid_subscribe_topic": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u0434\u043b\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438.",
+ "invalid_version": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f MySensors.",
+ "not_a_number": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0447\u0438\u0441\u043b\u043e.",
+ "port_out_of_range": "\u041d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043e\u0442 1 \u0434\u043e 65535.",
+ "same_topic": "\u0422\u043e\u043f\u0438\u043a\u0438 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438 \u0438 \u043f\u0443\u0431\u043b\u0438\u043a\u0430\u0446\u0438\u0438 \u0441\u043e\u0432\u043f\u0430\u0434\u0430\u044e\u0442.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "error": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "duplicate_persistence_file": "\u042d\u0442\u043e\u0442 \u0444\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.",
+ "duplicate_topic": "\u042d\u0442\u043e\u0442 \u0442\u043e\u043f\u0438\u043a \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\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_device": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.",
+ "invalid_ip": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441.",
+ "invalid_persistence_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439.",
+ "invalid_port": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430.",
+ "invalid_publish_topic": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u0434\u043b\u044f \u043f\u0443\u0431\u043b\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "invalid_serial": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442.",
+ "invalid_subscribe_topic": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u0434\u043b\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438.",
+ "invalid_version": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f MySensors.",
+ "not_a_number": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0447\u0438\u0441\u043b\u043e.",
+ "port_out_of_range": "\u041d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043e\u0442 1 \u0434\u043e 65535.",
+ "same_topic": "\u0422\u043e\u043f\u0438\u043a\u0438 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438 \u0438 \u043f\u0443\u0431\u043b\u0438\u043a\u0430\u0446\u0438\u0438 \u0441\u043e\u0432\u043f\u0430\u0434\u0430\u044e\u0442.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "gw_mqtt": {
+ "data": {
+ "persistence_file": "\u0424\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f)",
+ "retain": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f MQTT",
+ "topic_in_prefix": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0442\u043e\u043f\u0438\u043a\u043e\u0432 (topic_in_prefix)",
+ "topic_out_prefix": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441 \u0434\u043b\u044f \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0442\u043e\u043f\u0438\u043a\u043e\u0432 (topic_out_prefix)",
+ "version": "\u0412\u0435\u0440\u0441\u0438\u044f MySensors"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 MQTT"
+ },
+ "gw_serial": {
+ "data": {
+ "baud_rate": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0434\u0430\u043d\u043d\u044b\u0445",
+ "device": "\u041f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442",
+ "persistence_file": "\u0424\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f)",
+ "version": "\u0412\u0435\u0440\u0441\u0438\u044f MySensors"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0448\u043b\u044e\u0437\u0430"
+ },
+ "gw_tcp": {
+ "data": {
+ "device": "IP-\u0430\u0434\u0440\u0435\u0441 \u0448\u043b\u044e\u0437\u0430",
+ "persistence_file": "\u0424\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f)",
+ "tcp_port": "\u041f\u043e\u0440\u0442",
+ "version": "\u0412\u0435\u0440\u0441\u0438\u044f MySensors"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 Ethernet"
+ },
+ "user": {
+ "data": {
+ "gateway_type": "\u0422\u0438\u043f \u0448\u043b\u044e\u0437\u0430"
+ },
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0448\u043b\u044e\u0437\u0443"
+ }
+ }
+ },
+ "title": "MySensors"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/translations/zh-Hant.json b/homeassistant/components/mysensors/translations/zh-Hant.json
new file mode 100644
index 00000000000000..d0067c2d0ced0d
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/zh-Hant.json
@@ -0,0 +1,79 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "duplicate_persistence_file": "Persistence \u6a94\u6848\u5df2\u4f7f\u7528\u4e2d",
+ "duplicate_topic": "\u4e3b\u984c\u5df2\u4f7f\u7528\u4e2d",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "invalid_device": "\u88dd\u7f6e\u7121\u6548",
+ "invalid_ip": "IP \u4f4d\u5740\u7121\u6548",
+ "invalid_persistence_file": "Persistence \u6a94\u6848\u7121\u6548",
+ "invalid_port": "\u901a\u8a0a\u57e0\u865f\u78bc\u7121\u6548",
+ "invalid_publish_topic": "\u767c\u5e03\u4e3b\u984c\u7121\u6548",
+ "invalid_serial": "\u5e8f\u5217\u57e0\u7121\u6548",
+ "invalid_subscribe_topic": "\u8a02\u95b1\u4e3b\u984c\u7121\u6548",
+ "invalid_version": "MySensors \u7248\u672c\u7121\u6548",
+ "not_a_number": "\u8acb\u8f38\u5165\u865f\u78bc",
+ "port_out_of_range": "\u8acb\u8f38\u5165\u4ecb\u65bc 1 \u81f3 65535 \u4e4b\u9593\u7684\u865f\u78bc",
+ "same_topic": "\u8a02\u95b1\u8207\u767c\u4f48\u4e3b\u984c\u76f8\u540c",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "error": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "duplicate_persistence_file": "Persistence \u6a94\u6848\u5df2\u4f7f\u7528\u4e2d",
+ "duplicate_topic": "\u4e3b\u984c\u5df2\u4f7f\u7528\u4e2d",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "invalid_device": "\u88dd\u7f6e\u7121\u6548",
+ "invalid_ip": "IP \u4f4d\u5740\u7121\u6548",
+ "invalid_persistence_file": "Persistence \u6a94\u6848\u7121\u6548",
+ "invalid_port": "\u901a\u8a0a\u57e0\u865f\u78bc\u7121\u6548",
+ "invalid_publish_topic": "\u767c\u5e03\u4e3b\u984c\u7121\u6548",
+ "invalid_serial": "\u5e8f\u5217\u57e0\u7121\u6548",
+ "invalid_subscribe_topic": "\u8a02\u95b1\u4e3b\u984c\u7121\u6548",
+ "invalid_version": "MySensors \u7248\u672c\u7121\u6548",
+ "not_a_number": "\u8acb\u8f38\u5165\u865f\u78bc",
+ "port_out_of_range": "\u8acb\u8f38\u5165\u4ecb\u65bc 1 \u81f3 65535 \u4e4b\u9593\u7684\u865f\u78bc",
+ "same_topic": "\u8a02\u95b1\u8207\u767c\u4f48\u4e3b\u984c\u76f8\u540c",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "gw_mqtt": {
+ "data": {
+ "persistence_file": "Persistence \u6a94\u6848\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u81ea\u52d5\u7522\u751f\uff09",
+ "retain": "mqtt retain",
+ "topic_in_prefix": "\u8f38\u5165\u4e3b\u984c\u524d\u7db4\uff08topic_in_prefix\uff09",
+ "topic_out_prefix": "\u8f38\u51fa\u4e3b\u984c\u524d\u7db4\uff08topic_out_prefix\uff09",
+ "version": "MySensors \u7248\u672c"
+ },
+ "description": "MQTT \u9598\u9053\u5668\u8a2d\u5b9a"
+ },
+ "gw_serial": {
+ "data": {
+ "baud_rate": "\u50b3\u8f38\u7387",
+ "device": "\u5e8f\u5217\u57e0",
+ "persistence_file": "Persistence \u6a94\u6848\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u81ea\u52d5\u7522\u751f\uff09",
+ "version": "MySensors \u7248\u672c"
+ },
+ "description": "\u9598\u9053\u5668\u8a0a\u5217\u57e0\u8a2d\u5b9a"
+ },
+ "gw_tcp": {
+ "data": {
+ "device": "\u7db2\u95dc IP \u4f4d\u5740",
+ "persistence_file": "Persistence \u6a94\u6848\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u81ea\u52d5\u7522\u751f\uff09",
+ "tcp_port": "\u901a\u8a0a\u57e0",
+ "version": "MySensors \u7248\u672c"
+ },
+ "description": "\u9598\u9053\u5668\u4e59\u592a\u7db2\u8def\u8a2d\u5b9a"
+ },
+ "user": {
+ "data": {
+ "gateway_type": "\u9598\u9053\u5668\u985e\u578b"
+ },
+ "description": "\u9078\u64c7\u9598\u9053\u5668\u9023\u7dda\u65b9\u5f0f"
+ }
+ }
+ },
+ "title": "MySensors"
+}
\ No newline at end of file
diff --git a/homeassistant/components/mystrom/light.py b/homeassistant/components/mystrom/light.py
index 510245ea8592de..145bdadcc2a4f6 100644
--- a/homeassistant/components/mystrom/light.py
+++ b/homeassistant/components/mystrom/light.py
@@ -145,7 +145,7 @@ async def async_turn_off(self, **kwargs):
try:
await self._bulb.set_off()
except MyStromConnectionError:
- _LOGGER.warning("myStrom bulb not online")
+ _LOGGER.warning("The myStrom bulb not online")
async def async_update(self):
"""Fetch new state data for this light."""
diff --git a/homeassistant/components/n26/__init__.py b/homeassistant/components/n26/__init__.py
index f8379cb310f188..b1e83cd5311300 100644
--- a/homeassistant/components/n26/__init__.py
+++ b/homeassistant/components/n26/__init__.py
@@ -36,7 +36,7 @@
extra=vol.ALLOW_EXTRA,
)
-N26_COMPONENTS = ["sensor", "switch"]
+PLATFORMS = ["sensor", "switch"]
def setup(hass, config):
@@ -65,9 +65,9 @@ def setup(hass, config):
hass.data[DOMAIN] = {}
hass.data[DOMAIN][DATA] = api_data_list
- # Load components for supported devices
- for component in N26_COMPONENTS:
- load_platform(hass, component, DOMAIN, {}, config)
+ # Load platforms for supported devices
+ for platform in PLATFORMS:
+ load_platform(hass, platform, DOMAIN, {}, config)
return True
diff --git a/homeassistant/components/n26/sensor.py b/homeassistant/components/n26/sensor.py
index b9a8b21f9d0c96..98d86194b86e0f 100644
--- a/homeassistant/components/n26/sensor.py
+++ b/homeassistant/components/n26/sensor.py
@@ -1,5 +1,5 @@
"""Support for N26 bank account sensors."""
-from homeassistant.helpers.entity import Entity
+from homeassistant.components.sensor import SensorEntity
from . import DEFAULT_SCAN_INTERVAL, DOMAIN, timestamp_ms_to_date
from .const import DATA
@@ -43,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensor_entities)
-class N26Account(Entity):
+class N26Account(SensorEntity):
"""Sensor for a N26 balance account.
A balance account contains an amount of money (=balance). The amount may
@@ -86,7 +86,7 @@ def unit_of_measurement(self) -> str:
return self._data.balance.get("currency")
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Additional attributes of the sensor."""
attributes = {
ATTR_IBAN: self._data.balance.get("iban"),
@@ -117,7 +117,7 @@ def icon(self) -> str:
return ICON_ACCOUNT
-class N26Card(Entity):
+class N26Card(SensorEntity):
"""Sensor for a N26 card."""
def __init__(self, api_data, card) -> None:
@@ -147,7 +147,7 @@ def state(self) -> float:
return self._card["status"]
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Additional attributes of the sensor."""
attributes = {
"apple_pay_eligible": self._card.get("applePayEligible"),
@@ -186,7 +186,7 @@ def icon(self) -> str:
return ICON_CARD
-class N26Space(Entity):
+class N26Space(SensorEntity):
"""Sensor for a N26 space."""
def __init__(self, api_data, space) -> None:
@@ -220,7 +220,7 @@ def unit_of_measurement(self) -> str:
return self._space["balance"]["currency"]
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Additional attributes of the sensor."""
goal_value = ""
if "goal" in self._space:
diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py
index 782b8735e3ae21..e7f83c66efac93 100644
--- a/homeassistant/components/nad/media_player.py
+++ b/homeassistant/components/nad/media_player.py
@@ -163,7 +163,7 @@ def source(self):
@property
def source_list(self):
"""List of available input sources."""
- return sorted(list(self._reverse_mapping))
+ return sorted(self._reverse_mapping)
@property
def available(self):
diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py
index ed1e4877a31b13..a6f453ce2aa17e 100644
--- a/homeassistant/components/nanoleaf/light.py
+++ b/homeassistant/components/nanoleaf/light.py
@@ -62,14 +62,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
token = ""
if discovery_info is not None:
host = discovery_info["host"]
- name = discovery_info["hostname"]
+ name = None
+ device_id = discovery_info["properties"]["id"]
+
# if device already exists via config, skip discovery setup
if host in hass.data[DATA_NANOLEAF]:
return
_LOGGER.info("Discovered a new Nanoleaf: %s", discovery_info)
conf = load_json(hass.config.path(CONFIG_FILE))
- if conf.get(host, {}).get("token"):
- token = conf[host]["token"]
+ if host in conf and device_id not in conf:
+ conf[device_id] = conf.pop(host)
+ save_json(hass.config.path(CONFIG_FILE), conf)
+ token = conf.get(device_id, {}).get("token", "")
else:
host = config[CONF_HOST]
name = config[CONF_NAME]
@@ -94,11 +98,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
nanoleaf_light.token = token
try:
- nanoleaf_light.available
+ info = nanoleaf_light.info
except Unavailable:
_LOGGER.error("Could not connect to Nanoleaf Light: %s on %s", name, host)
return
+ if name is None:
+ name = info.name
+
hass.data[DATA_NANOLEAF][host] = nanoleaf_light
add_entities([NanoleafLight(nanoleaf_light, name)], True)
@@ -108,6 +115,7 @@ class NanoleafLight(LightEntity):
def __init__(self, light, name):
"""Initialize an Nanoleaf light."""
+ self._unique_id = light.serialNo
self._available = True
self._brightness = None
self._color_temp = None
@@ -157,6 +165,11 @@ def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
return 833
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self._unique_id
+
@property
def name(self):
"""Return the display name of this light."""
diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json
index 6d953335a34644..1f0fbf80983c62 100644
--- a/homeassistant/components/nanoleaf/manifest.json
+++ b/homeassistant/components/nanoleaf/manifest.json
@@ -2,6 +2,6 @@
"domain": "nanoleaf",
"name": "Nanoleaf",
"documentation": "https://www.home-assistant.io/integrations/nanoleaf",
- "requirements": ["pynanoleaf==0.0.5"],
+ "requirements": ["pynanoleaf==0.1.0"],
"codeowners": []
}
diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py
index 1d9d3de4f89a80..bb0db8ebd8559a 100644
--- a/homeassistant/components/neato/__init__.py
+++ b/homeassistant/components/neato/__init__.py
@@ -103,9 +103,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
hass.data[NEATO_LOGIN] = hub
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
diff --git a/homeassistant/components/neato/api.py b/homeassistant/components/neato/api.py
index 931d7cdb712be1..31988fc175e73f 100644
--- a/homeassistant/components/neato/api.py
+++ b/homeassistant/components/neato/api.py
@@ -1,14 +1,11 @@
"""API for Neato Botvac bound to Home Assistant OAuth."""
from asyncio import run_coroutine_threadsafe
-import logging
import pybotvac
from homeassistant import config_entries, core
from homeassistant.helpers import config_entry_oauth2_flow
-_LOGGER = logging.getLogger(__name__)
-
class ConfigEntryAuth(pybotvac.OAuthSession):
"""Provide Neato Botvac authentication tied to an OAuth2 based config entry."""
diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py
index 1698a1d944a6e6..9a2f47bcfa361a 100644
--- a/homeassistant/components/neato/camera.py
+++ b/homeassistant/components/neato/camera.py
@@ -102,7 +102,7 @@ def update(self):
self._image = image.read()
self._image_url = image_url
- self._generated_at = (map_data["generated_at"].strip("Z")).replace("T", " ")
+ self._generated_at = map_data["generated_at"]
self._available = True
@property
@@ -126,7 +126,7 @@ def device_info(self):
return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}}
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the vacuum cleaner."""
data = {}
diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py
index 449de72b158908..3f7f7831f54b46 100644
--- a/homeassistant/components/neato/config_flow.py
+++ b/homeassistant/components/neato/config_flow.py
@@ -1,6 +1,7 @@
"""Config flow for Neato Botvac."""
+from __future__ import annotations
+
import logging
-from typing import Optional
import voluptuous as vol
@@ -8,11 +9,8 @@
from homeassistant.const import CONF_TOKEN
from homeassistant.helpers import config_entry_oauth2_flow
-# pylint: disable=unused-import
from .const import NEATO_DOMAIN
-_LOGGER = logging.getLogger(__name__)
-
class OAuth2FlowHandler(
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=NEATO_DOMAIN
@@ -27,7 +25,7 @@ def logger(self) -> logging.Logger:
"""Return logger."""
return logging.getLogger(__name__)
- async def async_step_user(self, user_input: Optional[dict] = None) -> dict:
+ async def async_step_user(self, user_input: dict | None = None) -> dict:
"""Create an entry for the flow."""
current_entries = self._async_current_entries()
if current_entries and CONF_TOKEN in current_entries[0].data:
@@ -40,9 +38,7 @@ async def async_step_reauth(self, data) -> dict:
"""Perform reauth upon migration of old entries."""
return await self.async_step_reauth_confirm()
- async def async_step_reauth_confirm(
- self, user_input: Optional[dict] = None
- ) -> dict:
+ async def async_step_reauth_confirm(self, user_input: dict | None = None) -> dict:
"""Confirm reauth upon migration of old entries."""
if user_input is None:
return self.async_show_form(
diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py
index 248e455b6da62f..0cd8fb932ce0b0 100644
--- a/homeassistant/components/neato/const.py
+++ b/homeassistant/components/neato/const.py
@@ -119,6 +119,7 @@
"nav_backdrop_frontbump": "Clear my path",
"nav_backdrop_leftbump": "Clear my path",
"nav_backdrop_wheelextended": "Clear my path",
+ "nav_floorplan_zone_path_blocked": "Clear my path",
"nav_mag_sensor": "Clear my path",
"nav_no_exit": "Clear my path",
"nav_no_movement": "Clear my path",
diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py
index 50af42e7007bff..83add4ff3f7de6 100644
--- a/homeassistant/components/neato/sensor.py
+++ b/homeassistant/components/neato/sensor.py
@@ -4,9 +4,8 @@
from pybotvac.exceptions import NeatoRobotException
-from homeassistant.components.sensor import DEVICE_CLASS_BATTERY
+from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, SensorEntity
from homeassistant.const import PERCENTAGE
-from homeassistant.helpers.entity import Entity
from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES
@@ -31,7 +30,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
async_add_entities(dev, True)
-class NeatoSensor(Entity):
+class NeatoSensor(SensorEntity):
"""Neato sensor."""
def __init__(self, neato, robot):
diff --git a/homeassistant/components/neato/translations/de.json b/homeassistant/components/neato/translations/de.json
index 94fcd3c4cb2b54..272191a42216d4 100644
--- a/homeassistant/components/neato/translations/de.json
+++ b/homeassistant/components/neato/translations/de.json
@@ -4,7 +4,7 @@
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
"authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
- "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte beachte die Dokumentation.",
+ "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.",
"no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler sind [im Hilfebereich]({docs_url}) zu finden",
"reauth_successful": "Die erneute Authentifizierung war erfolgreich"
},
@@ -20,7 +20,7 @@
"title": "W\u00e4hle die Authentifizierungsmethode"
},
"reauth_confirm": {
- "title": "Wollen Sie mit der Einrichtung beginnen?"
+ "title": "M\u00f6chten Sie mit der Einrichtung beginnen?"
},
"user": {
"data": {
diff --git a/homeassistant/components/neato/translations/fr.json b/homeassistant/components/neato/translations/fr.json
index 69f2186c54c6e6..26b97e83c0bf55 100644
--- a/homeassistant/components/neato/translations/fr.json
+++ b/homeassistant/components/neato/translations/fr.json
@@ -2,7 +2,11 @@
"config": {
"abort": {
"already_configured": "D\u00e9j\u00e0 configur\u00e9",
- "invalid_auth": "Authentification invalide"
+ "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.",
+ "invalid_auth": "Authentification invalide",
+ "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation ",
+ "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )",
+ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi"
},
"create_entry": {
"default": "Voir [Documentation Neato]({docs_url})."
@@ -12,6 +16,12 @@
"unknown": "Erreur inattendue"
},
"step": {
+ "pick_implementation": {
+ "title": "S\u00e9lectionner une m\u00e9thode d'authentification"
+ },
+ "reauth_confirm": {
+ "title": "Voulez-vous commencer la configuration ?"
+ },
"user": {
"data": {
"password": "Mot de passe",
@@ -22,5 +32,6 @@
"title": "Informations compte Neato"
}
}
- }
+ },
+ "title": "Neato Botvac"
}
\ No newline at end of file
diff --git a/homeassistant/components/neato/translations/he.json b/homeassistant/components/neato/translations/he.json
new file mode 100644
index 00000000000000..6f4191da70d538
--- /dev/null
+++ b/homeassistant/components/neato/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/neato/translations/hu.json b/homeassistant/components/neato/translations/hu.json
index f2fd30f323efe4..3cb6ffd33641b1 100644
--- a/homeassistant/components/neato/translations/hu.json
+++ b/homeassistant/components/neato/translations/hu.json
@@ -1,15 +1,26 @@
{
"config": {
"abort": {
- "already_configured": "M\u00e1r konfigur\u00e1lva van",
- "reauth_successful": "Az \u00fajb\u00f3li azonos\u00edt\u00e1s sikeres"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.",
+ "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
},
"create_entry": {
- "default": "L\u00e1sd: [Neato dokument\u00e1ci\u00f3] ( {docs_url} )."
+ "default": "Sikeres hiteles\u00edt\u00e9s"
+ },
+ "error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"step": {
+ "pick_implementation": {
+ "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert"
+ },
"reauth_confirm": {
- "title": "El akarja kezdeni a be\u00e1ll\u00edt\u00e1st?"
+ "title": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?"
},
"user": {
"data": {
@@ -17,9 +28,10 @@
"username": "Felhaszn\u00e1l\u00f3n\u00e9v",
"vendor": "Sz\u00e1ll\u00edt\u00f3"
},
- "description": "L\u00e1sd: [Neato dokument\u00e1ci\u00f3] ( {docs_url} ).",
+ "description": "L\u00e1sd: [Neato dokument\u00e1ci\u00f3]({docs_url}).",
"title": "Neato Fi\u00f3kinform\u00e1ci\u00f3"
}
}
- }
+ },
+ "title": "Neato Botvac"
}
\ No newline at end of file
diff --git a/homeassistant/components/neato/translations/id.json b/homeassistant/components/neato/translations/id.json
new file mode 100644
index 00000000000000..17eee515787ba2
--- /dev/null
+++ b/homeassistant/components/neato/translations/id.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.",
+ "invalid_auth": "Autentikasi tidak valid",
+ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.",
+ "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})",
+ "reauth_successful": "Autentikasi ulang berhasil"
+ },
+ "create_entry": {
+ "default": "Berhasil diautentikasi"
+ },
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Pilih Metode Autentikasi"
+ },
+ "reauth_confirm": {
+ "title": "Ingin memulai penyiapan?"
+ },
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna",
+ "vendor": "Vendor"
+ },
+ "description": "Baca [dokumentasi Neato]({docs_url}).",
+ "title": "Info Akun Neato"
+ }
+ }
+ },
+ "title": "Neato Botvac"
+}
\ No newline at end of file
diff --git a/homeassistant/components/neato/translations/it.json b/homeassistant/components/neato/translations/it.json
index 100237c33e6dad..b559c23bb1a827 100644
--- a/homeassistant/components/neato/translations/it.json
+++ b/homeassistant/components/neato/translations/it.json
@@ -2,14 +2,14 @@
"config": {
"abort": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
- "authorize_url_timeout": "Timeout nella generazione dell'URL di autorizzazione.",
+ "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.",
"invalid_auth": "Autenticazione non valida",
- "missing_configuration": "Questo componente non \u00e8 configurato. Per favore segui la documentazione.",
- "no_url_available": "Nessun URL disponibile. Per altre informazioni su questo errore, [controlla la sezione di aiuto]({docs_url})",
- "reauth_successful": "Ri-autenticazione completata con successo"
+ "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.",
+ "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})",
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
},
"create_entry": {
- "default": "Autenticato con successo"
+ "default": "Autenticazione riuscita"
},
"error": {
"invalid_auth": "Autenticazione non valida",
@@ -17,10 +17,10 @@
},
"step": {
"pick_implementation": {
- "title": "Scegli un metodo di autenticazione"
+ "title": "Scegli il metodo di autenticazione"
},
"reauth_confirm": {
- "title": "Vuoi cominciare la configurazione?"
+ "title": "Vuoi iniziare la configurazione?"
},
"user": {
"data": {
diff --git a/homeassistant/components/neato/translations/ko.json b/homeassistant/components/neato/translations/ko.json
index 00d1ae3b4677c0..d08000871ea8f5 100644
--- a/homeassistant/components/neato/translations/ko.json
+++ b/homeassistant/components/neato/translations/ko.json
@@ -1,12 +1,27 @@
{
"config": {
"abort": {
- "already_configured": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
+ "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
},
"create_entry": {
- "default": "[Neato \uc124\uba85\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
+ "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
+ "pick_implementation": {
+ "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30"
+ },
+ "reauth_confirm": {
+ "title": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ },
"user": {
"data": {
"password": "\ube44\ubc00\ubc88\ud638",
@@ -17,5 +32,6 @@
"title": "Neato \uacc4\uc815 \uc815\ubcf4"
}
}
- }
+ },
+ "title": "Neato Botvac"
}
\ No newline at end of file
diff --git a/homeassistant/components/neato/translations/lb.json b/homeassistant/components/neato/translations/lb.json
index 44d8e4f68119c9..adc42ae840dcc0 100644
--- a/homeassistant/components/neato/translations/lb.json
+++ b/homeassistant/components/neato/translations/lb.json
@@ -2,7 +2,10 @@
"config": {
"abort": {
"already_configured": "Apparat ass scho konfigur\u00e9iert",
- "invalid_auth": "Ong\u00eblteg Authentifikatioun"
+ "authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL.",
+ "invalid_auth": "Ong\u00eblteg Authentifikatioun",
+ "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.",
+ "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich"
},
"create_entry": {
"default": "Kuckt [Neato Dokumentatioun]({docs_url})."
@@ -12,6 +15,12 @@
"unknown": "Onerwaarte Feeler"
},
"step": {
+ "pick_implementation": {
+ "title": "Authentifikatiouns Method auswielen"
+ },
+ "reauth_confirm": {
+ "title": "Soll den Ariichtungs Prozess gestart ginn?"
+ },
"user": {
"data": {
"password": "Passwuert",
@@ -22,5 +31,6 @@
"title": "Neato Kont Informatiounen"
}
}
- }
+ },
+ "title": "Neato Botvac"
}
\ No newline at end of file
diff --git a/homeassistant/components/neato/translations/nl.json b/homeassistant/components/neato/translations/nl.json
index 26e5a647b1d8c5..d03bc1d216a767 100644
--- a/homeassistant/components/neato/translations/nl.json
+++ b/homeassistant/components/neato/translations/nl.json
@@ -1,17 +1,27 @@
{
"config": {
"abort": {
- "already_configured": "Al geconfigureerd",
- "invalid_auth": "Ongeldige authenticatie"
+ "already_configured": "Apparaat is al geconfigureerd",
+ "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
+ "invalid_auth": "Ongeldige authenticatie",
+ "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.",
+ "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})",
+ "reauth_successful": "Herauthenticatie was succesvol"
},
"create_entry": {
- "default": "Zie [Neato-documentatie] ({docs_url})."
+ "default": "Succesvol geauthenticeerd"
},
"error": {
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
"step": {
+ "pick_implementation": {
+ "title": "Kies een authenticatie methode"
+ },
+ "reauth_confirm": {
+ "title": "Wil je beginnen met instellen?"
+ },
"user": {
"data": {
"password": "Wachtwoord",
@@ -22,5 +32,6 @@
"title": "Neato-account info"
}
}
- }
+ },
+ "title": "Neato Botvac"
}
\ No newline at end of file
diff --git a/homeassistant/components/neato/translations/pt.json b/homeassistant/components/neato/translations/pt.json
index 0672c9af33f7d2..48e73c763f0034 100644
--- a/homeassistant/components/neato/translations/pt.json
+++ b/homeassistant/components/neato/translations/pt.json
@@ -2,13 +2,26 @@
"config": {
"abort": {
"already_configured": "O dispositivo j\u00e1 est\u00e1 configurado",
- "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida"
+ "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o",
+ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida",
+ "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.",
+ "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})",
+ "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida"
+ },
+ "create_entry": {
+ "default": "Autenticado com sucesso"
},
"error": {
"invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida",
"unknown": "Erro inesperado"
},
"step": {
+ "pick_implementation": {
+ "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o"
+ },
+ "reauth_confirm": {
+ "title": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?"
+ },
"user": {
"data": {
"password": "Palavra-passe",
diff --git a/homeassistant/components/neato/translations/ru.json b/homeassistant/components/neato/translations/ru.json
index 30ea15c60c37bc..25bb616a63837f 100644
--- a/homeassistant/components/neato/translations/ru.json
+++ b/homeassistant/components/neato/translations/ru.json
@@ -3,7 +3,7 @@
"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.",
"authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.",
"no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.",
"reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
@@ -12,7 +12,7 @@
"default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
},
"error": {
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
@@ -25,7 +25,7 @@
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f",
"vendor": "\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c"
},
"description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.",
diff --git a/homeassistant/components/neato/translations/tr.json b/homeassistant/components/neato/translations/tr.json
new file mode 100644
index 00000000000000..53a8e0503cb460
--- /dev/null
+++ b/homeassistant/components/neato/translations/tr.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu"
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in"
+ },
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ },
+ "title": "Neato Hesap Bilgisi"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/neato/translations/uk.json b/homeassistant/components/neato/translations/uk.json
new file mode 100644
index 00000000000000..58b56a52f6c4e3
--- /dev/null
+++ b/homeassistant/components/neato/translations/uk.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.",
+ "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e"
+ },
+ "create_entry": {
+ "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e."
+ },
+ "error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ },
+ "reauth_confirm": {
+ "title": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?"
+ },
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430",
+ "vendor": "\u0412\u0438\u0440\u043e\u0431\u043d\u0438\u043a"
+ },
+ "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0438\u0445 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u044c.",
+ "title": "Neato"
+ }
+ }
+ },
+ "title": "Neato Botvac"
+}
\ No newline at end of file
diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py
index ce4156244b7c07..e0b3c7b779f034 100644
--- a/homeassistant/components/neato/vacuum.py
+++ b/homeassistant/components/neato/vacuum.py
@@ -202,8 +202,8 @@ def update(self):
return
mapdata = self._mapdata[self._robot_serial]["maps"][0]
- self._clean_time_start = (mapdata["start_at"].strip("Z")).replace("T", " ")
- self._clean_time_stop = (mapdata["end_at"].strip("Z")).replace("T", " ")
+ self._clean_time_start = mapdata["start_at"]
+ self._clean_time_stop = mapdata["end_at"]
self._clean_area = mapdata["cleaned_area"]
self._clean_susp_charge_count = mapdata["suspended_cleaning_charging_count"]
self._clean_susp_time = mapdata["time_in_suspended_cleaning"]
@@ -284,7 +284,7 @@ def unique_id(self):
return self._robot_serial
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the vacuum cleaner."""
data = {}
diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py
index 3d15e3c4d9b355..de8a85f44fd30d 100644
--- a/homeassistant/components/nederlandse_spoorwegen/sensor.py
+++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py
@@ -7,11 +7,10 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -94,7 +93,7 @@ def valid_stations(stations, given_stations):
return True
-class NSDepartureSensor(Entity):
+class NSDepartureSensor(SensorEntity):
"""Implementation of a NS Departure Sensor."""
def __init__(self, nsapi, name, departure, heading, via, time):
@@ -124,7 +123,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if not self._trips:
return
diff --git a/homeassistant/components/nello/lock.py b/homeassistant/components/nello/lock.py
index 61241660847332..93e63b05da9f22 100644
--- a/homeassistant/components/nello/lock.py
+++ b/homeassistant/components/nello/lock.py
@@ -48,7 +48,7 @@ def is_locked(self):
return True
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
return self._device_attrs
diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py
index 85e591707ade38..cd3f6ed9ed3361 100644
--- a/homeassistant/components/nest/__init__.py
+++ b/homeassistant/components/nest/__init__.py
@@ -198,9 +198,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None)
hass.data[DOMAIN][DATA_SUBSCRIBER] = subscriber
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -211,14 +211,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
if DATA_SDM not in entry.data:
# Legacy API
return True
-
+ _LOGGER.debug("Stopping nest subscriber")
subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER]
subscriber.stop_async()
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py
index aa8e100059abc0..ce6ff897a2fac0 100644
--- a/homeassistant/components/nest/camera_sdm.py
+++ b/homeassistant/components/nest/camera_sdm.py
@@ -1,8 +1,8 @@
"""Support for Google Nest SDM Cameras."""
+from __future__ import annotations
import datetime
import logging
-from typing import Optional
from google_nest_sdm.camera_traits import (
CameraEventImageTrait,
@@ -74,7 +74,7 @@ def should_poll(self) -> bool:
return False
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID."""
# The API "name" field is a unique device identifier.
return f"{self._device.name}-camera"
@@ -145,7 +145,13 @@ async def _handle_stream_refresh(self, now):
_LOGGER.debug("Failed to extend stream: %s", err)
# Next attempt to catch a url will get a new one
self._stream = None
+ if self.stream:
+ self.stream.stop()
+ self.stream = None
return
+ # Update the stream worker with the latest valid url
+ if self.stream:
+ self.stream.update_source(self._stream.rtsp_stream_url)
self._schedule_stream_refresh()
async def async_will_remove_from_hass(self):
diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py
index 6413b2e0dfe089..e02ebcd2dee587 100644
--- a/homeassistant/components/nest/climate_sdm.py
+++ b/homeassistant/components/nest/climate_sdm.py
@@ -1,6 +1,5 @@
"""Support for Google Nest SDM climate devices."""
-
-from typing import Optional
+from __future__ import annotations
from google_nest_sdm.device import Device
from google_nest_sdm.device_traits import FanTrait, TemperatureTrait
@@ -75,6 +74,8 @@
}
FAN_INV_MODE_MAP = {v: k for k, v in FAN_MODE_MAP.items()}
+MAX_FAN_DURATION = 43200 # 15 hours is the max in the SDM API
+
async def async_setup_sdm_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
@@ -109,7 +110,7 @@ def should_poll(self) -> bool:
return False
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID."""
# The API "name" field is a unique device identifier.
return self._device.name
@@ -179,9 +180,11 @@ def target_temperature_low(self):
@property
def _target_temperature_trait(self):
"""Return the correct trait with a target temp depending on mode."""
- if self.preset_mode == PRESET_ECO:
- if ThermostatEcoTrait.NAME in self._device.traits:
- return self._device.traits[ThermostatEcoTrait.NAME]
+ if (
+ self.preset_mode == PRESET_ECO
+ and ThermostatEcoTrait.NAME in self._device.traits
+ ):
+ return self._device.traits[ThermostatEcoTrait.NAME]
if ThermostatTemperatureSetpointTrait.NAME in self._device.traits:
return self._device.traits[ThermostatTemperatureSetpointTrait.NAME]
return None
@@ -322,4 +325,7 @@ async def async_set_fan_mode(self, fan_mode):
if fan_mode not in self.fan_modes:
raise ValueError(f"Unsupported fan_mode '{fan_mode}'")
trait = self._device.traits[FanTrait.NAME]
- await trait.set_timer(FAN_INV_MODE_MAP[fan_mode])
+ duration = None
+ if fan_mode != FAN_OFF:
+ duration = MAX_FAN_DURATION
+ await trait.set_timer(FAN_INV_MODE_MAP[fan_mode], duration=duration)
diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py
index 36b0da239a98ad..fd5eef34c7d388 100644
--- a/homeassistant/components/nest/config_flow.py
+++ b/homeassistant/components/nest/config_flow.py
@@ -11,12 +11,12 @@
new api via NestFlowHandler.register_sdm_api, the custom methods just
invoke the AbstractOAuth2FlowHandler methods.
"""
+from __future__ import annotations
import asyncio
from collections import OrderedDict
import logging
import os
-from typing import Dict
import async_timeout
import voluptuous as vol
@@ -98,7 +98,7 @@ def logger(self) -> logging.Logger:
return logging.getLogger(__name__)
@property
- def extra_authorize_data(self) -> Dict[str, str]:
+ def extra_authorize_data(self) -> dict[str, str]:
"""Extra data that needs to be appended to the authorize url."""
return {
"scope": " ".join(SDM_SCOPES),
diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py
index e5bd7ea1ca8a60..d59ec05c503750 100644
--- a/homeassistant/components/nest/device_trigger.py
+++ b/homeassistant/components/nest/device_trigger.py
@@ -1,6 +1,5 @@
"""Provides device automations for Nest."""
-import logging
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -17,8 +16,6 @@
from .const import DATA_SUBSCRIBER, DOMAIN
from .events import DEVICE_TRAIT_TRIGGER_MAP, NEST_EVENT
-_LOGGER = logging.getLogger(__name__)
-
DEVICE = "device"
TRIGGER_TYPES = set(DEVICE_TRAIT_TRIGGER_MAP.values())
@@ -42,7 +39,7 @@ async def async_get_nest_device_id(hass: HomeAssistant, device_id: str) -> str:
async def async_get_device_trigger_types(
hass: HomeAssistant, nest_device_id: str
-) -> List[str]:
+) -> list[str]:
"""List event triggers supported for a Nest device."""
# All devices should have already been loaded so any failures here are
# "shouldn't happen" cases
@@ -61,7 +58,7 @@ async def async_get_device_trigger_types(
return trigger_types
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers for a Nest device."""
nest_device_id = await async_get_nest_device_id(hass, device_id)
if not nest_device_id:
@@ -85,7 +82,6 @@ async def async_attach_trigger(
automation_info: dict,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
- config = TRIGGER_SCHEMA(config)
event_config = event_trigger.TRIGGER_SCHEMA(
{
event_trigger.CONF_PLATFORM: "event",
diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py
index 218b01fd71b38d..60faa90e8b4625 100644
--- a/homeassistant/components/nest/legacy/__init__.py
+++ b/homeassistant/components/nest/legacy/__init__.py
@@ -28,6 +28,8 @@
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
+PLATFORMS = ["climate", "camera", "sensor", "binary_sensor"]
+
# Configuration for the legacy nest API
SERVICE_CANCEL_ETA = "cancel_eta"
SERVICE_SET_ETA = "set_eta"
@@ -131,9 +133,9 @@ async def async_setup_legacy_entry(hass, entry):
if not await hass.async_add_executor_job(hass.data[DATA_NEST].initialize):
return False
- for component in "climate", "camera", "sensor", "binary_sensor":
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
def validate_structures(target_structures):
@@ -361,11 +363,6 @@ def name(self):
"""Return the name of the nest, if any."""
return self._name
- @property
- def unit_of_measurement(self):
- """Return the unit the value is expressed in."""
- return self._unit
-
@property
def should_poll(self):
"""Do not need poll thanks using Nest streaming API."""
diff --git a/homeassistant/components/nest/legacy/sensor.py b/homeassistant/components/nest/legacy/sensor.py
index 34f525ca7a626b..53d9c8244669ef 100644
--- a/homeassistant/components/nest/legacy/sensor.py
+++ b/homeassistant/components/nest/legacy/sensor.py
@@ -1,6 +1,7 @@
"""Support for Nest Thermostat sensors for the legacy API."""
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
CONF_MONITORED_CONDITIONS,
CONF_SENSORS,
@@ -149,9 +150,14 @@ def get_sensors():
async_add_entities(await hass.async_add_executor_job(get_sensors), True)
-class NestBasicSensor(NestSensorDevice):
+class NestBasicSensor(NestSensorDevice, SensorEntity):
"""Representation a basic Nest sensor."""
+ @property
+ def unit_of_measurement(self):
+ """Return the unit the value is expressed in."""
+ return self._unit
+
@property
def state(self):
"""Return the state of the sensor."""
@@ -179,7 +185,7 @@ def update(self):
self._state = getattr(self.device, self.variable)
-class NestTempSensor(NestSensorDevice):
+class NestTempSensor(NestSensorDevice, SensorEntity):
"""Representation of a Nest Temperature sensor."""
@property
@@ -187,6 +193,11 @@ def state(self):
"""Return the state of the sensor."""
return self._state
+ @property
+ def unit_of_measurement(self):
+ """Return the unit the value is expressed in."""
+ return self._unit
+
@property
def device_class(self):
"""Return the device class of the sensor."""
diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json
index f9bc135693d4d4..734261d9b08611 100644
--- a/homeassistant/components/nest/manifest.json
+++ b/homeassistant/components/nest/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"dependencies": ["ffmpeg", "http"],
"documentation": "https://www.home-assistant.io/integrations/nest",
- "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.2.9"],
+ "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.2.12"],
"codeowners": ["@allenporter"],
"quality_scale": "platinum",
"dhcp": [{"macaddress":"18B430*"}]
diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py
index 52490f41f8691e..06e2b68d7cf602 100644
--- a/homeassistant/components/nest/sensor_sdm.py
+++ b/homeassistant/components/nest/sensor_sdm.py
@@ -1,12 +1,13 @@
"""Support for Google Nest SDM sensors."""
+from __future__ import annotations
import logging
-from typing import Optional
from google_nest_sdm.device import Device
from google_nest_sdm.device_traits import HumidityTrait, TemperatureTrait
from google_nest_sdm.exceptions import GoogleNestException
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
@@ -15,7 +16,6 @@
TEMP_CELSIUS,
)
from homeassistant.exceptions import PlatformNotReady
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
from .const import DATA_SUBSCRIBER, DOMAIN
@@ -53,7 +53,7 @@ async def async_setup_sdm_entry(
async_add_entities(entities)
-class SensorBase(Entity):
+class SensorBase(SensorEntity):
"""Representation of a dynamically updated Sensor."""
def __init__(self, device: Device):
@@ -67,7 +67,7 @@ def should_poll(self) -> bool:
return False
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID."""
# The API "name" field is a unique device identifier.
return f"{self._device.name}-{self.device_class}"
@@ -113,7 +113,7 @@ class HumiditySensor(SensorBase):
"""Representation of a Humidity Sensor."""
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID."""
# The API returns the identifier under the name field.
return f"{self._device.name}-humidity"
diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json
index 6ce529621aab9b..26ec49c0d75f55 100644
--- a/homeassistant/components/nest/strings.json
+++ b/homeassistant/components/nest/strings.json
@@ -17,7 +17,7 @@
},
"link": {
"title": "Link Nest Account",
- "description": "To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided pin code below.",
+ "description": "To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided PIN code below.",
"data": {
"code": "[%key:common::config_flow::data::pin%]"
}
diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json
index 08d8cb974546e9..bc19d5c2c7c1d5 100644
--- a/homeassistant/components/nest/translations/ca.json
+++ b/homeassistant/components/nest/translations/ca.json
@@ -30,7 +30,7 @@
"data": {
"code": "Codi PIN"
},
- "description": "Per enlla\u00e7ar el teu compte de Nest, [autoritza el teu compte]({url}). \n\nDespr\u00e9s de l'autoritzaci\u00f3, copia i enganxa el codi pin que es mostra a sota.",
+ "description": "Per enlla\u00e7ar el teu compte de Nest, [autoritza el teu compte]({url}). \n\nDespr\u00e9s de l'autoritzaci\u00f3, copia i enganxa el codi PIN que es mostra a sota.",
"title": "Enlla\u00e7 amb el compte de Nest"
},
"pick_implementation": {
diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json
index 2bc328ff8f6659..3925b7537b220a 100644
--- a/homeassistant/components/nest/translations/de.json
+++ b/homeassistant/components/nest/translations/de.json
@@ -2,34 +2,43 @@
"config": {
"abort": {
"authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL",
- "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL",
- "reauth_successful": "Neuathentifizierung erfolgreich",
+ "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.",
+ "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.",
+ "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich",
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.",
"unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten"
},
+ "create_entry": {
+ "default": "Erfolgreich authentifiziert"
+ },
"error": {
"internal_error": "Ein interner Fehler ist aufgetreten",
"invalid_pin": "Ung\u00fcltiger PIN-Code",
"timeout": "Ein zeit\u00fcberschreitungs Fehler ist aufgetreten",
- "unknown": "Ein unbekannter Fehler ist aufgetreten"
+ "unknown": "Unerwarteter Fehler"
},
"step": {
"init": {
"data": {
"flow_impl": "Anbieter"
},
- "description": "W\u00e4hlen, \u00fcber welchen Authentifizierungsanbieter du dich bei Nest authentifizieren m\u00f6chtest.",
+ "description": "W\u00e4hle die Authentifizierungsmethode",
"title": "Authentifizierungsanbieter"
},
"link": {
"data": {
- "code": "PIN Code"
+ "code": "PIN-Code"
},
"description": "[Autorisiere dein Konto] ( {url} ), um deinen Nest-Account zu verkn\u00fcpfen.\n\n F\u00fcge anschlie\u00dfend den erhaltenen PIN Code hier ein.",
"title": "Nest-Konto verkn\u00fcpfen"
},
+ "pick_implementation": {
+ "title": "W\u00e4hle die Authentifizierungsmethode"
+ },
"reauth_confirm": {
"description": "Die Nest-Integration muss das Konto neu authentifizieren",
- "title": "Integration neu authentifizieren"
+ "title": "Integration erneut authentifizieren"
}
}
},
diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json
index 6693c2e5614e32..d7b000d921fe27 100644
--- a/homeassistant/components/nest/translations/en.json
+++ b/homeassistant/components/nest/translations/en.json
@@ -7,7 +7,7 @@
"no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})",
"reauth_successful": "Re-authentication was successful",
"single_instance_allowed": "Already configured. Only a single configuration possible.",
- "unknown_authorize_url_generation": "Unknown error generating an authorize url."
+ "unknown_authorize_url_generation": "Unknown error generating an authorize URL."
},
"create_entry": {
"default": "Successfully authenticated"
@@ -30,7 +30,7 @@
"data": {
"code": "PIN Code"
},
- "description": "To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided pin code below.",
+ "description": "To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided PIN code below.",
"title": "Link Nest Account"
},
"pick_implementation": {
diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json
index 7d22dfd96bf21c..1319bc2ca4b794 100644
--- a/homeassistant/components/nest/translations/et.json
+++ b/homeassistant/components/nest/translations/et.json
@@ -30,7 +30,7 @@
"data": {
"code": "PIN kood"
},
- "description": "Nest-i konto linkimiseks [authorize your account] ({url}).\n\nP\u00e4rast autoriseerimist kopeeri allolev PIN kood.",
+ "description": "Nest-i konto linkimiseks [authorize your account] ({url}).\n\nP\u00e4rast autoriseerimist kopeeri ja kleebi allolev PIN kood.",
"title": "Lingi Nesti konto"
},
"pick_implementation": {
diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json
index be006913f652b9..ce716fb3083bc0 100644
--- a/homeassistant/components/nest/translations/fr.json
+++ b/homeassistant/components/nest/translations/fr.json
@@ -5,7 +5,9 @@
"authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.",
"missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.",
"no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )",
- "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
+ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi",
+ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.",
+ "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation."
},
"create_entry": {
"default": "Authentification r\u00e9ussie"
@@ -28,12 +30,24 @@
"data": {
"code": "Code PIN"
},
- "description": "Pour associer votre compte Nest, [autorisez votre compte]({url}). \n\n Apr\u00e8s autorisation, copiez-collez le code PIN fourni ci-dessous.",
+ "description": "Pour associer votre compte Nest, [autorisez votre compte]({url}). \n\n Apr\u00e8s autorisation, copiez-collez le code NIP fourni ci-dessous.",
"title": "Lier un compte Nest"
},
"pick_implementation": {
"title": "S\u00e9lectionner une m\u00e9thode d'authentification"
+ },
+ "reauth_confirm": {
+ "description": "L'int\u00e9gration Nest doit r\u00e9-authentifier votre compte",
+ "title": "R\u00e9-authentifier l'int\u00e9gration"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "camera_motion": "Mouvement d\u00e9tect\u00e9",
+ "camera_person": "Personne d\u00e9tect\u00e9e",
+ "camera_sound": "Son d\u00e9tect\u00e9",
+ "doorbell_chime": "Sonnette enfonc\u00e9e"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json
index 47334c4aa622dd..9400ea7875c6cf 100644
--- a/homeassistant/components/nest/translations/hu.json
+++ b/homeassistant/components/nest/translations/hu.json
@@ -3,34 +3,42 @@
"abort": {
"authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.",
"authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.",
- "reauth_successful": "Az \u00fajb\u00f3li azonos\u00edt\u00e1s sikeres"
+ "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.",
+ "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.",
+ "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n."
},
"create_entry": {
- "default": "Sikeres autentik\u00e1ci\u00f3"
+ "default": "Sikeres hiteles\u00edt\u00e9s"
},
"error": {
"internal_error": "Bels\u0151 hiba t\u00f6rt\u00e9nt a k\u00f3d valid\u00e1l\u00e1s\u00e1n\u00e1l",
- "invalid_pin": "\u00c9rv\u00e9nytelen ",
+ "invalid_pin": "\u00c9rv\u00e9nytelen PIN-k\u00f3d",
"timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n.",
- "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n"
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"step": {
"init": {
"data": {
"flow_impl": "Szolg\u00e1ltat\u00f3"
},
- "description": "V\u00e1laszd ki, hogy melyik hiteles\u00edt\u00e9si szolg\u00e1ltat\u00f3n\u00e1l szeretn\u00e9d hiteles\u00edteni a Nestet.",
+ "description": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert",
"title": "Hiteles\u00edt\u00e9si Szolg\u00e1ltat\u00f3"
},
"link": {
"data": {
"code": "PIN-k\u00f3d"
},
- "description": "A Nest-fi\u00f3k \u00f6sszekapcsol\u00e1s\u00e1hoz [enged\u00e9lyezze fi\u00f3kj\u00e1t] ( {url} ). \n\n Az enged\u00e9lyez\u00e9s ut\u00e1n m\u00e1solja be az al\u00e1bbi PIN k\u00f3dot.",
+ "description": "A Nest-fi\u00f3k \u00f6sszekapcsol\u00e1s\u00e1hoz [enged\u00e9lyezze fi\u00f3kj\u00e1t]({url}). \n\nAz enged\u00e9lyez\u00e9s ut\u00e1n m\u00e1solja be az PIN k\u00f3dot.",
"title": "Nest fi\u00f3k \u00f6sszekapcsol\u00e1sa"
},
+ "pick_implementation": {
+ "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert"
+ },
"reauth_confirm": {
- "title": "Integr\u00e1ci\u00f3 \u00fajb\u00f3li azonos\u00edt\u00e1sa"
+ "description": "A Nest integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie a fi\u00f3kodat",
+ "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se"
}
}
},
diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json
index 4fc229c0d5308f..757c53e2866221 100644
--- a/homeassistant/components/nest/translations/id.json
+++ b/homeassistant/components/nest/translations/id.json
@@ -2,28 +2,52 @@
"config": {
"abort": {
"authorize_url_fail": "Kesalahan tidak dikenal terjadi ketika menghasilkan URL otorisasi.",
- "authorize_url_timeout": "Waktu tunggu menghasilkan otorisasi url telah habis."
+ "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.",
+ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.",
+ "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})",
+ "reauth_successful": "Autentikasi ulang berhasil",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.",
+ "unknown_authorize_url_generation": "Kesalahan tidak dikenal ketika menghasilkan URL otorisasi."
+ },
+ "create_entry": {
+ "default": "Berhasil diautentikasi"
},
"error": {
- "internal_error": "Kesalahan Internal memvalidasi kode",
- "timeout": "Waktu tunggu memvalidasi kode telah habis.",
- "unknown": "Error tidak diketahui saat memvalidasi kode"
+ "internal_error": "Kesalahan internal saat memvalidasi kode",
+ "invalid_pin": "Invalid Kode PIN",
+ "timeout": "Tenggang waktu memvalidasi kode telah habis.",
+ "unknown": "Kesalahan yang tidak diharapkan"
},
"step": {
"init": {
"data": {
"flow_impl": "Penyedia"
},
- "description": "Pilih melalui penyedia autentikasi mana yang ingin Anda autentikasi dengan Nest.",
- "title": "Penyedia Otentikasi"
+ "description": "Pilih Metode Autentikasi",
+ "title": "Penyedia Autentikasi"
},
"link": {
"data": {
"code": "Kode PIN"
},
- "description": "Untuk menautkan akun Nest Anda, [beri kuasa akun Anda] ( {url} ). \n\n Setelah otorisasi, salin-tempel kode pin yang disediakan di bawah ini.",
- "title": "Hubungkan Akun Nest"
+ "description": "Untuk menautkan akun Nest Anda, [otorisasi akun Anda]({url}).\n\nSetelah otorisasi, salin dan tempel kode PIN yang disediakan di bawah ini.",
+ "title": "Tautkan Akun Nest"
+ },
+ "pick_implementation": {
+ "title": "Pilih Metode Autentikasi"
+ },
+ "reauth_confirm": {
+ "description": "Integrasi Nest perlu mengautentikasi ulang akun Anda",
+ "title": "Autentikasi Ulang Integrasi"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "camera_motion": "Gerakan terdeteksi",
+ "camera_person": "Orang terdeteksi",
+ "camera_sound": "Suara terdeteksi",
+ "doorbell_chime": "Bel pintu ditekan"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json
index 958eaea039a4cd..c6e62db314d7fa 100644
--- a/homeassistant/components/nest/translations/it.json
+++ b/homeassistant/components/nest/translations/it.json
@@ -5,7 +5,7 @@
"authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.",
"missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.",
"no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})",
- "reauth_successful": "Riautenticato con successo",
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente",
"single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.",
"unknown_authorize_url_generation": "Errore sconosciuto durante la generazione di un URL di autorizzazione."
},
@@ -30,7 +30,7 @@
"data": {
"code": "Codice PIN"
},
- "description": "Per collegare l'account Nido, [autorizzare l'account]({url}).\n\nDopo l'autorizzazione, copia-incolla il codice PIN fornito di seguito.",
+ "description": "Per collegare il tuo account Nest, [autorizza il tuo account] ({url}). \n\nDopo l'autorizzazione, copia e incolla il codice PIN fornito di seguito.",
"title": "Collega un account Nest"
},
"pick_implementation": {
@@ -38,7 +38,7 @@
},
"reauth_confirm": {
"description": "L'integrazione di Nest deve autenticare nuovamente il tuo account",
- "title": "Autentica nuovamente l'integrazione"
+ "title": "Autenticare nuovamente l'integrazione"
}
}
},
diff --git a/homeassistant/components/nest/translations/ko.json b/homeassistant/components/nest/translations/ko.json
index 798f191e34adf2..f8d6de2244a859 100644
--- a/homeassistant/components/nest/translations/ko.json
+++ b/homeassistant/components/nest/translations/ko.json
@@ -1,29 +1,53 @@
{
"config": {
"abort": {
- "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
- "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "authorize_url_fail": "\uc778\uc99d URL\uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
+ "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
+ "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "unknown_authorize_url_generation": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4."
+ },
+ "create_entry": {
+ "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
"internal_error": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \ub0b4\ubd80 \uc624\ub958 \ubc1c\uc0dd",
+ "invalid_pin": "PIN \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"timeout": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac \uc2dc\uac04 \ucd08\uacfc",
- "unknown": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958 \ubc1c\uc0dd"
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"init": {
"data": {
"flow_impl": "\uacf5\uae09\uc790"
},
- "description": "Nest \ub97c \uc778\uc99d\ud558\uae30 \uc704\ud55c \uc778\uc99d \uacf5\uae09\uc790\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.",
+ "description": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30",
"title": "\uc778\uc99d \uacf5\uae09\uc790"
},
"link": {
"data": {
"code": "PIN \ucf54\ub4dc"
},
- "description": "Nest \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74, [\uacc4\uc815 \uc5f0\uacb0 \uc2b9\uc778]({url}) \uc744 \ud574\uc8fc\uc138\uc694.\n\n\uc2b9\uc778 \ud6c4, \uc544\ub798\uc758 PIN \ucf54\ub4dc\ub97c \ubcf5\uc0ac\ud558\uc5ec \ubd99\uc5ec\ub123\uc73c\uc138\uc694.",
+ "description": "Nest \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74 [\uacc4\uc815 \uc5f0\uacb0 \uc2b9\uc778]({url})\uc744 \ud574\uc8fc\uc138\uc694.\n\n\uc2b9\uc778 \ud6c4 \uc544\ub798\uc5d0 \uc81c\uacf5\ub41c PIN \ucf54\ub4dc\ub97c \ubcf5\uc0ac\ud558\uc5ec \ubd99\uc5ec \ub123\uc5b4\uc8fc\uc138\uc694.",
"title": "Nest \uacc4\uc815 \uc5f0\uacb0\ud558\uae30"
+ },
+ "pick_implementation": {
+ "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30"
+ },
+ "reauth_confirm": {
+ "description": "Nest \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \uacc4\uc815\uc744 \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4",
+ "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "camera_motion": "\uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud588\uc744 \ub54c",
+ "camera_person": "\uc0ac\ub78c\uc744 \uac10\uc9c0\ud588\uc744 \ub54c",
+ "camera_sound": "\uc18c\ub9ac\ub97c \uac10\uc9c0\ud588\uc744 \ub54c",
+ "doorbell_chime": "\ucd08\uc778\uc885\uc774 \ub20c\ub838\uc744 \ub54c"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nest/translations/lb.json b/homeassistant/components/nest/translations/lb.json
index 1f0115a429b8bd..612d1f302589d6 100644
--- a/homeassistant/components/nest/translations/lb.json
+++ b/homeassistant/components/nest/translations/lb.json
@@ -4,7 +4,12 @@
"authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.",
"authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.",
"missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.",
- "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech."
+ "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich",
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech.",
+ "unknown_authorize_url_generation": "Onbekannte Feeler beim erstellen vun der Authorisatiouns URL."
+ },
+ "create_entry": {
+ "default": "Erfollegr\u00e4ich authentifiz\u00e9iert"
},
"error": {
"internal_error": "Interne Feeler beim valid\u00e9ieren vum Code",
diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json
index 931b8aa770e74f..b4a965f4955749 100644
--- a/homeassistant/components/nest/translations/nl.json
+++ b/homeassistant/components/nest/translations/nl.json
@@ -2,27 +2,43 @@
"config": {
"abort": {
"authorize_url_fail": "Onbekende fout bij het genereren van een autoriseer-URL.",
- "authorize_url_timeout": "Toestemming voor het genereren van autoriseer-url."
+ "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
+ "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.",
+ "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})",
+ "reauth_successful": "Herauthenticatie was succesvol",
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.",
+ "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL."
+ },
+ "create_entry": {
+ "default": "Succesvol geauthenticeerd"
},
"error": {
"internal_error": "Interne foutvalidatiecode",
+ "invalid_pin": "Ongeldige PIN-code",
"timeout": "Time-out validatie van code",
- "unknown": "Onbekende foutvalidatiecode"
+ "unknown": "Onverwachte fout"
},
"step": {
"init": {
"data": {
"flow_impl": "Leverancier"
},
- "description": "Kies met welke authenticatieleverancier u wilt verifi\u00ebren met Nest.",
+ "description": "Kies een authenticatie methode",
"title": "Authenticatieleverancier"
},
"link": {
"data": {
- "code": "Pincode"
+ "code": "PIN-code"
},
"description": "Als je je Nest-account wilt koppelen, [autoriseer je account] ( {url} ). \n\nNa autorisatie, kopieer en plak de voorziene pincode hieronder.",
"title": "Koppel Nest-account"
+ },
+ "pick_implementation": {
+ "title": "Kies een authenticatie methode"
+ },
+ "reauth_confirm": {
+ "description": "De Nest-integratie moet je account opnieuw verifi\u00ebren",
+ "title": "Verifieer de integratie opnieuw"
}
}
},
diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json
index dfaf33b3969d68..d6b6c89bcaa14b 100644
--- a/homeassistant/components/nest/translations/no.json
+++ b/homeassistant/components/nest/translations/no.json
@@ -1,13 +1,13 @@
{
"config": {
"abort": {
- "authorize_url_fail": "Ukjent feil ved generering av godkjenningsadresse",
+ "authorize_url_fail": "Ukjent feil under generering av en autoriserings-URL.",
"authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse",
"missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen",
"no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})",
"reauth_successful": "Godkjenning p\u00e5 nytt var vellykket",
"single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.",
- "unknown_authorize_url_generation": "Ukjent feil ved generering av godkjenningsadresse"
+ "unknown_authorize_url_generation": "Ukjent feil under generering av en autoriserings-URL."
},
"create_entry": {
"default": "Vellykket godkjenning"
@@ -30,7 +30,7 @@
"data": {
"code": "PIN kode"
},
- "description": "For \u00e5 koble din Nest-konto m\u00e5 du [bekrefte kontoen din]({url}). \n\nEtter bekreftelse, kopier og lim inn den oppgitte PIN koden nedenfor.",
+ "description": "For \u00e5 koble til Nest-kontoen din, [autoriser kontoen din] ( {url} ). \n\n Etter autorisasjon, kopier og lim inn den angitte PIN-koden nedenfor.",
"title": "Koble til Nest konto"
},
"pick_implementation": {
diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json
index 63e45df12fa994..d1147e03afc316 100644
--- a/homeassistant/components/nest/translations/pl.json
+++ b/homeassistant/components/nest/translations/pl.json
@@ -5,6 +5,7 @@
"authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji",
"missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.",
"no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})",
+ "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119",
"single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.",
"unknown_authorize_url_generation": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji"
},
@@ -34,6 +35,10 @@
},
"pick_implementation": {
"title": "Wybierz metod\u0119 uwierzytelniania"
+ },
+ "reauth_confirm": {
+ "description": "Integracja Nest wymaga ponownego uwierzytelnienia Twojego konta",
+ "title": "Ponownie uwierzytelnij integracj\u0119"
}
}
},
diff --git a/homeassistant/components/nest/translations/pt.json b/homeassistant/components/nest/translations/pt.json
index 6da647ac29bfef..33ff857af7e6c8 100644
--- a/homeassistant/components/nest/translations/pt.json
+++ b/homeassistant/components/nest/translations/pt.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.",
- "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.",
+ "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o",
"missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.",
"no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})",
"single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.",
@@ -36,5 +36,10 @@
"title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "camera_motion": "Movimento detectado"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json
index 4f2e8952566ff2..0763b68a1becf4 100644
--- a/homeassistant/components/nest/translations/ru.json
+++ b/homeassistant/components/nest/translations/ru.json
@@ -30,7 +30,7 @@
"data": {
"code": "PIN-\u043a\u043e\u0434"
},
- "description": "[\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest. \n\n \u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u043f\u0438\u043d-\u043a\u043e\u0434.",
+ "description": "[\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest. \n\n \u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 PIN-\u043a\u043e\u0434.",
"title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest"
},
"pick_implementation": {
@@ -38,7 +38,7 @@
},
"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 Nest",
- "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f"
+ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f"
}
}
},
diff --git a/homeassistant/components/nest/translations/sv.json b/homeassistant/components/nest/translations/sv.json
index 0abe4d75fac17d..cddb9e2fe79a47 100644
--- a/homeassistant/components/nest/translations/sv.json
+++ b/homeassistant/components/nest/translations/sv.json
@@ -25,5 +25,10 @@
"title": "L\u00e4nka Nest-konto"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "camera_motion": "R\u00f6relse uppt\u00e4ckt"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nest/translations/tr.json b/homeassistant/components/nest/translations/tr.json
index 484cdaff6ec4a2..003c1ccc0c22d2 100644
--- a/homeassistant/components/nest/translations/tr.json
+++ b/homeassistant/components/nest/translations/tr.json
@@ -1,9 +1,20 @@
{
+ "config": {
+ "abort": {
+ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu",
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.",
+ "unknown_authorize_url_generation": "Yetkilendirme url'si olu\u015fturulurken bilinmeyen hata."
+ },
+ "error": {
+ "unknown": "Beklenmeyen hata"
+ }
+ },
"device_automation": {
"trigger_type": {
"camera_motion": "Hareket alg\u0131land\u0131",
"camera_person": "Ki\u015fi alg\u0131land\u0131",
- "camera_sound": "Ses alg\u0131land\u0131"
+ "camera_sound": "Ses alg\u0131land\u0131",
+ "doorbell_chime": "Kap\u0131 zili bas\u0131ld\u0131"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nest/translations/uk.json b/homeassistant/components/nest/translations/uk.json
new file mode 100644
index 00000000000000..f2869a76f4258a
--- /dev/null
+++ b/homeassistant/components/nest/translations/uk.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.",
+ "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.",
+ "unknown_authorize_url_generation": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457."
+ },
+ "create_entry": {
+ "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e."
+ },
+ "error": {
+ "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0456\u0448\u043d\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 \u043a\u043e\u0434\u0443.",
+ "invalid_pin": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 PIN-\u043a\u043e\u0434.",
+ "timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 \u043a\u043e\u0434\u0443.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457",
+ "title": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ },
+ "link": {
+ "data": {
+ "code": "PIN-\u043a\u043e\u0434"
+ },
+ "description": "[\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0449\u043e\u0431 \u043f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u0441\u0432\u043e\u044e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 Nest. \n \n\u041f\u0456\u0441\u043b\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457 \u0441\u043a\u043e\u043f\u0456\u044e\u0439\u0442\u0435 \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 PIN-\u043a\u043e\u0434.",
+ "title": "\u041f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 Nest"
+ },
+ "pick_implementation": {
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ },
+ "reauth_confirm": {
+ "description": "\u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Nest",
+ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e"
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "camera_motion": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0440\u0443\u0445",
+ "camera_person": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c \u043b\u044e\u0434\u0438\u043d\u0438",
+ "camera_sound": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0437\u0432\u0443\u043a",
+ "doorbell_chime": "\u041d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430 \u0434\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u0434\u0437\u0432\u0456\u043d\u043a\u0430"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py
index cdbd34991f2473..b9b04a08febbe7 100644
--- a/homeassistant/components/netatmo/__init__.py
+++ b/homeassistant/components/netatmo/__init__.py
@@ -43,7 +43,7 @@
OAUTH2_TOKEN,
)
from .data_handler import NetatmoDataHandler
-from .webhook import handle_webhook
+from .webhook import async_handle_webhook
_LOGGER = logging.getLogger(__name__)
@@ -111,9 +111,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
await data_handler.async_setup()
hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] = data_handler
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
async def unregister_webhook(_):
@@ -157,18 +157,20 @@ async def register_webhook(event):
try:
webhook_register(
- hass, DOMAIN, "Netatmo", entry.data[CONF_WEBHOOK_ID], handle_webhook
+ hass,
+ DOMAIN,
+ "Netatmo",
+ entry.data[CONF_WEBHOOK_ID],
+ async_handle_webhook,
)
async def handle_event(event):
"""Handle webhook events."""
if event["data"]["push_type"] == "webhook_activation":
if activation_listener is not None:
- _LOGGER.debug("sub called")
activation_listener()
if activation_timeout is not None:
- _LOGGER.debug("Unsub called")
activation_timeout()
activation_listener = async_dispatcher_connect(
@@ -205,15 +207,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
await hass.async_add_executor_job(
hass.data[DOMAIN][entry.entry_id][AUTH].dropwebhook
)
- _LOGGER.info("Unregister Netatmo webhook.")
+ _LOGGER.info("Unregister Netatmo webhook")
await hass.data[DOMAIN][entry.entry_id][DATA_HANDLER].async_cleanup()
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py
index 6e55b884d4d19d..5445231282cced 100644
--- a/homeassistant/components/netatmo/camera.py
+++ b/homeassistant/components/netatmo/camera.py
@@ -7,6 +7,7 @@
from homeassistant.components.camera import SUPPORT_STREAM, Camera
from homeassistant.core import callback
+from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -49,15 +50,17 @@ async def async_setup_entry(hass, entry, async_add_entities):
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
+ await data_handler.register_data_class(
+ CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None
+ )
+
+ if CAMERA_DATA_CLASS_NAME not in data_handler.data:
+ raise PlatformNotReady
+
async def get_entities():
"""Retrieve Netatmo entities."""
- await data_handler.register_data_class(
- CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None
- )
-
- data = data_handler.data
- if not data.get(CAMERA_DATA_CLASS_NAME):
+ if not data_handler.data.get(CAMERA_DATA_CLASS_NAME):
return []
data_class = data_handler.data[CAMERA_DATA_CLASS_NAME]
@@ -94,24 +97,25 @@ async def get_entities():
async_add_entities(await get_entities(), True)
+ await data_handler.unregister_data_class(CAMERA_DATA_CLASS_NAME, None)
+
platform = entity_platform.current_platform.get()
- if data_handler.data[CAMERA_DATA_CLASS_NAME] is not None:
- platform.async_register_entity_service(
- SERVICE_SET_PERSONS_HOME,
- {vol.Required(ATTR_PERSONS): vol.All(cv.ensure_list, [cv.string])},
- "_service_set_persons_home",
- )
- platform.async_register_entity_service(
- SERVICE_SET_PERSON_AWAY,
- {vol.Optional(ATTR_PERSON): cv.string},
- "_service_set_person_away",
- )
- platform.async_register_entity_service(
- SERVICE_SET_CAMERA_LIGHT,
- {vol.Required(ATTR_CAMERA_LIGHT_MODE): vol.In(CAMERA_LIGHT_MODES)},
- "_service_set_camera_light",
- )
+ platform.async_register_entity_service(
+ SERVICE_SET_PERSONS_HOME,
+ {vol.Required(ATTR_PERSONS): vol.All(cv.ensure_list, [cv.string])},
+ "_service_set_persons_home",
+ )
+ platform.async_register_entity_service(
+ SERVICE_SET_PERSON_AWAY,
+ {vol.Optional(ATTR_PERSON): cv.string},
+ "_service_set_person_away",
+ )
+ platform.async_register_entity_service(
+ SERVICE_SET_CAMERA_LIGHT,
+ {vol.Required(ATTR_CAMERA_LIGHT_MODE): vol.In(CAMERA_LIGHT_MODES)},
+ "_service_set_camera_light",
+ )
class NetatmoCamera(NetatmoBase, Camera):
@@ -213,7 +217,7 @@ def camera_image(self):
return response.content
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the Netatmo-specific camera state attributes."""
return {
"id": self._id,
@@ -346,7 +350,7 @@ def _service_set_person_away(self, **kwargs):
def _service_set_camera_light(self, **kwargs):
"""Service to set light mode."""
mode = kwargs.get(ATTR_CAMERA_LIGHT_MODE)
- _LOGGER.debug("Turn camera '%s' %s", self._name, mode)
+ _LOGGER.debug("Turn %s camera light for '%s'", mode, self._name)
self._data.set_state(
home_id=self._home_id,
camera_id=self._id,
diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py
index dee8d3b668df3b..9993b4efac2e7c 100644
--- a/homeassistant/components/netatmo/climate.py
+++ b/homeassistant/components/netatmo/climate.py
@@ -1,7 +1,9 @@
"""Support for Netatmo Smart thermostats."""
+from __future__ import annotations
+
import logging
-from typing import List, Optional
+import pyatmo
import voluptuous as vol
from homeassistant.components.climate import ClimateEntity
@@ -25,18 +27,22 @@
TEMP_CELSIUS,
)
from homeassistant.core import callback
+from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv, entity_platform
+from homeassistant.helpers.device_registry import async_get_registry
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (
ATTR_HEATING_POWER_REQUEST,
ATTR_SCHEDULE_NAME,
ATTR_SELECTED_SCHEDULE,
+ DATA_DEVICE_IDS,
DATA_HANDLER,
DATA_HOMES,
DATA_SCHEDULES,
DOMAIN,
EVENT_TYPE_CANCEL_SET_POINT,
+ EVENT_TYPE_SCHEDULE,
EVENT_TYPE_SET_POINT,
EVENT_TYPE_THERM_MODE,
MANUFACTURER,
@@ -79,6 +85,7 @@
STATE_NETATMO_AWAY: PRESET_AWAY,
STATE_NETATMO_OFF: STATE_NETATMO_OFF,
STATE_NETATMO_MANUAL: STATE_NETATMO_MANUAL,
+ STATE_NETATMO_HOME: PRESET_SCHEDULE,
}
HVAC_MAP_NETATMO = {
@@ -109,18 +116,18 @@ async def async_setup_entry(hass, entry, async_add_entities):
)
home_data = data_handler.data.get(HOMEDATA_DATA_CLASS_NAME)
- if not home_data:
- return
+ if HOMEDATA_DATA_CLASS_NAME not in data_handler.data:
+ raise PlatformNotReady
async def get_entities():
"""Retrieve Netatmo entities."""
entities = []
for home_id in get_all_home_ids(home_data):
- _LOGGER.debug("Setting up home %s ...", home_id)
+ _LOGGER.debug("Setting up home %s", home_id)
for room_id in home_data.rooms[home_id].keys():
room_name = home_data.rooms[home_id][room_id]["name"]
- _LOGGER.debug("Setting up room %s (%s) ...", room_name, room_id)
+ _LOGGER.debug("Setting up room %s (%s)", room_name, room_id)
signal_name = f"{HOMESTATUS_DATA_CLASS_NAME}-{home_id}"
await data_handler.register_data_class(
HOMESTATUS_DATA_CLASS_NAME, signal_name, None, home_id=home_id
@@ -149,6 +156,8 @@ async def get_entities():
async_add_entities(await get_entities(), True)
+ await data_handler.unregister_data_class(HOMEDATA_DATA_CLASS_NAME, None)
+
platform = entity_platform.current_platform.get()
if home_data is not None:
@@ -228,6 +237,7 @@ async def async_added_to_hass(self) -> None:
EVENT_TYPE_SET_POINT,
EVENT_TYPE_THERM_MODE,
EVENT_TYPE_CANCEL_SET_POINT,
+ EVENT_TYPE_SCHEDULE,
):
self._listeners.append(
async_dispatcher_connect(
@@ -237,11 +247,23 @@ async def async_added_to_hass(self) -> None:
)
)
+ registry = await async_get_registry(self.hass)
+ device = registry.async_get_device({(DOMAIN, self._id)}, set())
+ self.hass.data[DOMAIN][DATA_DEVICE_IDS][self._home_id] = device.id
+
async def handle_event(self, event):
"""Handle webhook events."""
data = event["data"]
- if not data.get("home"):
+ if self._home_id != data["home_id"]:
+ return
+
+ if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data:
+ self._selected_schedule = self.hass.data[DOMAIN][DATA_SCHEDULES][
+ self._home_id
+ ].get(data["schedule_id"])
+ self.async_write_ha_state()
+ self.data_handler.async_force_update(self._home_status_class)
return
home = data["home"]
@@ -258,35 +280,37 @@ async def handle_event(self, event):
self._target_temperature = self._away_temperature
elif self._preset == PRESET_SCHEDULE:
self.async_update_callback()
+ self.data_handler.async_force_update(self._home_status_class)
self.async_write_ha_state()
return
- if not home.get("rooms"):
- return
-
- for room in home["rooms"]:
- if data["event_type"] == EVENT_TYPE_SET_POINT:
- if self._id == room["id"]:
- if room["therm_setpoint_mode"] == STATE_NETATMO_OFF:
- self._hvac_mode = HVAC_MODE_OFF
- elif room["therm_setpoint_mode"] == STATE_NETATMO_MAX:
+ for room in home.get("rooms", []):
+ if data["event_type"] == EVENT_TYPE_SET_POINT and self._id == room["id"]:
+ if room["therm_setpoint_mode"] == STATE_NETATMO_OFF:
+ self._hvac_mode = HVAC_MODE_OFF
+ self._preset = STATE_NETATMO_OFF
+ self._target_temperature = 0
+ elif room["therm_setpoint_mode"] == STATE_NETATMO_MAX:
+ self._hvac_mode = HVAC_MODE_HEAT
+ self._preset = PRESET_MAP_NETATMO[PRESET_BOOST]
+ self._target_temperature = DEFAULT_MAX_TEMP
+ elif room["therm_setpoint_mode"] == STATE_NETATMO_MANUAL:
+ self._hvac_mode = HVAC_MODE_HEAT
+ self._target_temperature = room["therm_setpoint_temperature"]
+ else:
+ self._target_temperature = room["therm_setpoint_temperature"]
+ if self._target_temperature == DEFAULT_MAX_TEMP:
self._hvac_mode = HVAC_MODE_HEAT
- self._target_temperature = DEFAULT_MAX_TEMP
- elif room["therm_setpoint_mode"] == STATE_NETATMO_MANUAL:
- self._hvac_mode = HVAC_MODE_HEAT
- self._target_temperature = room["therm_setpoint_temperature"]
- else:
- self._target_temperature = room["therm_setpoint_temperature"]
- if self._target_temperature == DEFAULT_MAX_TEMP:
- self._hvac_mode = HVAC_MODE_HEAT
- self.async_write_ha_state()
- break
-
- elif data["event_type"] == EVENT_TYPE_CANCEL_SET_POINT:
- if self._id == room["id"]:
- self.async_update_callback()
- self.async_write_ha_state()
- break
+ self.async_write_ha_state()
+ return
+
+ if (
+ data["event_type"] == EVENT_TYPE_CANCEL_SET_POINT
+ and self._id == room["id"]
+ ):
+ self.async_update_callback()
+ self.async_write_ha_state()
+ return
@property
def supported_features(self):
@@ -309,7 +333,7 @@ def target_temperature(self):
return self._target_temperature
@property
- def target_temperature_step(self) -> Optional[float]:
+ def target_temperature_step(self) -> float | None:
"""Return the supported step of target temperature."""
return PRECISION_HALVES
@@ -324,7 +348,7 @@ def hvac_modes(self):
return self._operation_list
@property
- def hvac_action(self) -> Optional[str]:
+ def hvac_action(self) -> str | None:
"""Return the current running hvac operation if supported."""
if self._model == NA_THERM and self._boilerstatus is not None:
return CURRENT_HVAC_MAP_NETATMO[self._boilerstatus]
@@ -346,6 +370,9 @@ def set_hvac_mode(self, hvac_mode: str) -> None:
def set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
+ if self.hvac_mode == HVAC_MODE_OFF:
+ self.turn_on()
+
if self.target_temperature == 0:
self._home_status.set_room_thermpoint(
self._id,
@@ -386,12 +413,12 @@ def set_preset_mode(self, preset_mode: str) -> None:
self.async_write_ha_state()
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp."""
return self._preset
@property
- def preset_modes(self) -> Optional[List[str]]:
+ def preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes."""
return SUPPORT_PRESET
@@ -405,7 +432,7 @@ def set_temperature(self, **kwargs):
self.async_write_ha_state()
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the thermostat."""
attr = {}
@@ -556,7 +583,9 @@ def _service_set_schedule(self, **kwargs):
schedule_id = sid
if not schedule_id:
- _LOGGER.error("You passed an invalid schedule")
+ _LOGGER.error(
+ "%s is not a invalid schedule", kwargs.get(ATTR_SCHEDULE_NAME)
+ )
return
self._data.switch_home_schedule(home_id=self._home_id, schedule_id=schedule_id)
@@ -567,8 +596,13 @@ def _service_set_schedule(self, **kwargs):
schedule_id,
)
+ @property
+ def device_info(self):
+ """Return the device info for the thermostat."""
+ return {**super().device_info, "suggested_area": self._room_data["name"]}
+
-def interpolate(batterylevel, module_type):
+def interpolate(batterylevel: int, module_type: str) -> int:
"""Interpolate battery level depending on device type."""
na_battery_levels = {
NA_THERM: {
@@ -610,7 +644,7 @@ def interpolate(batterylevel, module_type):
return int(pct)
-def get_all_home_ids(home_data):
+def get_all_home_ids(home_data: pyatmo.HomeData) -> list[str]:
"""Get all the home ids returned by NetAtmo API."""
if home_data is None:
return []
diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py
index ed1c5f0a8801dc..b0a312fa1f3913 100644
--- a/homeassistant/components/netatmo/const.py
+++ b/homeassistant/components/netatmo/const.py
@@ -4,21 +4,36 @@
DOMAIN = "netatmo"
MANUFACTURER = "Netatmo"
+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": "Relay",
- "NATherm1": "Smart Thermostat",
- "NRV": "Smart Radiator Valves",
- "NACamera": "Smart Indoor Camera",
- "NOC": "Smart Outdoor Camera",
- "NSD": "Smart Smoke Alarm",
- "NACamDoorTag": "Smart Door and Window Sensors",
- "NHC": "Smart Indoor Air Quality Monitor",
- "NAMain": "Smart Home Weather station – indoor module",
- "NAModule1": "Smart Home Weather station – outdoor module",
- "NAModule4": "Smart Additional Indoor module",
- "NAModule3": "Smart Rain Gauge",
- "NAModule2": "Smart Anemometer",
- "public": "Public Weather stations",
+ "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,
}
AUTH = "netatmo_auth"
@@ -56,9 +71,7 @@
DEFAULT_DISCOVERY = True
DEFAULT_WEBHOOKS = False
-ATTR_ID = "id"
ATTR_PSEUDO = "pseudo"
-ATTR_NAME = "name"
ATTR_EVENT_TYPE = "event_type"
ATTR_HEATING_POWER_REQUEST = "heating_power_request"
ATTR_HOME_ID = "home_id"
@@ -77,12 +90,67 @@
SERVICE_SET_PERSONS_HOME = "set_persons_home"
SERVICE_SET_PERSON_AWAY = "set_person_away"
+# Climate events
+EVENT_TYPE_SET_POINT = "set_point"
EVENT_TYPE_CANCEL_SET_POINT = "cancel_set_point"
+EVENT_TYPE_THERM_MODE = "therm_mode"
+EVENT_TYPE_SCHEDULE = "schedule"
+# Camera events
EVENT_TYPE_LIGHT_MODE = "light_mode"
+EVENT_TYPE_CAMERA_OUTDOOR = "outdoor"
+EVENT_TYPE_CAMERA_ANIMAL = "animal"
+EVENT_TYPE_CAMERA_HUMAN = "human"
+EVENT_TYPE_CAMERA_VEHICLE = "vehicle"
+EVENT_TYPE_CAMERA_MOVEMENT = "movement"
+EVENT_TYPE_CAMERA_PERSON = "person"
+EVENT_TYPE_CAMERA_PERSON_AWAY = "person_away"
+# Door tags
+EVENT_TYPE_DOOR_TAG_SMALL_MOVE = "tag_small_move"
+EVENT_TYPE_DOOR_TAG_BIG_MOVE = "tag_big_move"
+EVENT_TYPE_DOOR_TAG_OPEN = "tag_open"
EVENT_TYPE_OFF = "off"
EVENT_TYPE_ON = "on"
-EVENT_TYPE_SET_POINT = "set_point"
-EVENT_TYPE_THERM_MODE = "therm_mode"
+EVENT_TYPE_ALARM_STARTED = "alarm_started"
+
+OUTDOOR_CAMERA_TRIGGERS = [
+ EVENT_TYPE_CAMERA_ANIMAL,
+ EVENT_TYPE_CAMERA_HUMAN,
+ EVENT_TYPE_CAMERA_OUTDOOR,
+ EVENT_TYPE_CAMERA_VEHICLE,
+]
+INDOOR_CAMERA_TRIGGERS = [
+ EVENT_TYPE_CAMERA_MOVEMENT,
+ EVENT_TYPE_CAMERA_PERSON,
+ EVENT_TYPE_CAMERA_PERSON_AWAY,
+ EVENT_TYPE_ALARM_STARTED,
+]
+DOOR_TAG_TRIGGERS = [
+ EVENT_TYPE_DOOR_TAG_SMALL_MOVE,
+ EVENT_TYPE_DOOR_TAG_BIG_MOVE,
+ EVENT_TYPE_DOOR_TAG_OPEN,
+]
+CLIMATE_TRIGGERS = [
+ EVENT_TYPE_SET_POINT,
+ EVENT_TYPE_CANCEL_SET_POINT,
+ EVENT_TYPE_THERM_MODE,
+]
+EVENT_ID_MAP = {
+ EVENT_TYPE_CAMERA_MOVEMENT: "device_id",
+ EVENT_TYPE_CAMERA_PERSON: "device_id",
+ EVENT_TYPE_CAMERA_PERSON_AWAY: "device_id",
+ EVENT_TYPE_CAMERA_ANIMAL: "device_id",
+ EVENT_TYPE_CAMERA_HUMAN: "device_id",
+ EVENT_TYPE_CAMERA_OUTDOOR: "device_id",
+ EVENT_TYPE_CAMERA_VEHICLE: "device_id",
+ EVENT_TYPE_DOOR_TAG_SMALL_MOVE: "device_id",
+ EVENT_TYPE_DOOR_TAG_BIG_MOVE: "device_id",
+ EVENT_TYPE_DOOR_TAG_OPEN: "device_id",
+ EVENT_TYPE_LIGHT_MODE: "device_id",
+ EVENT_TYPE_ALARM_STARTED: "device_id",
+ EVENT_TYPE_CANCEL_SET_POINT: "room_id",
+ EVENT_TYPE_SET_POINT: "room_id",
+ EVENT_TYPE_THERM_MODE: "home_id",
+}
MODE_LIGHT_ON = "on"
MODE_LIGHT_OFF = "off"
diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py
index 9bc4b197f1b77b..6982a651a45435 100644
--- a/homeassistant/components/netatmo/data_handler.py
+++ b/homeassistant/components/netatmo/data_handler.py
@@ -1,16 +1,18 @@
"""The Netatmo data handler."""
+from __future__ import annotations
+
from collections import deque
from datetime import timedelta
from functools import partial
from itertools import islice
import logging
from time import time
-from typing import Deque, Dict, List
+from typing import Deque
import pyatmo
from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import CALLBACK_TYPE, HomeAssistant
+from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import async_track_time_interval
@@ -55,8 +57,8 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry):
"""Initialize self."""
self.hass = hass
self._auth = hass.data[DOMAIN][entry.entry_id][AUTH]
- self.listeners: List[CALLBACK_TYPE] = []
- self._data_classes: Dict = {}
+ self.listeners: list[CALLBACK_TYPE] = []
+ self._data_classes: dict = {}
self.data = {}
self._queue: Deque = deque()
self._webhook: bool = False
@@ -96,6 +98,12 @@ async def async_update(self, event_time):
self._queue.rotate(BATCH_SIZE)
+ @callback
+ def async_force_update(self, data_class_entry):
+ """Prioritize data retrieval for given data class entry."""
+ self._data_classes[data_class_entry][NEXT_SCAN] = time()
+ self._queue.rotate(-(self._queue.index(self._data_classes[data_class_entry])))
+
async def async_cleanup(self):
"""Clean up the Netatmo data handler."""
for listener in self.listeners:
@@ -113,7 +121,7 @@ async def handle_event(self, event):
elif event["data"]["push_type"] == "NACamera-connection":
_LOGGER.debug("%s camera reconnected", MANUFACTURER)
- self._data_classes[CAMERA_DATA_CLASS_NAME][NEXT_SCAN] = time()
+ self.async_force_update(CAMERA_DATA_CLASS_NAME)
async def async_fetch_data(self, data_class, data_class_entry, **kwargs):
"""Fetch data and notify."""
@@ -129,7 +137,11 @@ async def async_fetch_data(self, data_class, data_class_entry, **kwargs):
if update_callback:
update_callback()
- except (pyatmo.NoDevice, pyatmo.ApiError) as err:
+ except pyatmo.NoDevice as err:
+ _LOGGER.debug(err)
+ self.data[data_class_entry] = None
+
+ except pyatmo.ApiError as err:
_LOGGER.debug(err)
async def register_data_class(
diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py
new file mode 100644
index 00000000000000..d6085ec06ec1f6
--- /dev/null
+++ b/homeassistant/components/netatmo/device_trigger.py
@@ -0,0 +1,153 @@
+"""Provides device automations for Netatmo."""
+from __future__ import annotations
+
+import voluptuous as vol
+
+from homeassistant.components.automation import AutomationActionType
+from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.components.device_automation.exceptions import (
+ InvalidDeviceAutomationConfig,
+)
+from homeassistant.components.homeassistant.triggers import event as event_trigger
+from homeassistant.const import (
+ ATTR_DEVICE_ID,
+ CONF_DEVICE_ID,
+ CONF_DOMAIN,
+ CONF_ENTITY_ID,
+ CONF_PLATFORM,
+ CONF_TYPE,
+)
+from homeassistant.core import CALLBACK_TYPE, HomeAssistant
+from homeassistant.helpers import config_validation as cv, entity_registry
+from homeassistant.helpers.typing import ConfigType
+
+from . import DOMAIN
+from .climate import STATE_NETATMO_AWAY, STATE_NETATMO_HG, STATE_NETATMO_SCHEDULE
+from .const import (
+ CLIMATE_TRIGGERS,
+ EVENT_TYPE_THERM_MODE,
+ INDOOR_CAMERA_TRIGGERS,
+ MODEL_NACAMERA,
+ MODEL_NATHERM1,
+ MODEL_NOC,
+ MODEL_NRV,
+ NETATMO_EVENT,
+ OUTDOOR_CAMERA_TRIGGERS,
+)
+
+CONF_SUBTYPE = "subtype"
+
+DEVICES = {
+ MODEL_NACAMERA: INDOOR_CAMERA_TRIGGERS,
+ MODEL_NOC: OUTDOOR_CAMERA_TRIGGERS,
+ MODEL_NATHERM1: CLIMATE_TRIGGERS,
+ MODEL_NRV: CLIMATE_TRIGGERS,
+}
+
+SUBTYPES = {
+ EVENT_TYPE_THERM_MODE: [
+ STATE_NETATMO_SCHEDULE,
+ STATE_NETATMO_HG,
+ STATE_NETATMO_AWAY,
+ ]
+}
+
+TRIGGER_TYPES = OUTDOOR_CAMERA_TRIGGERS + INDOOR_CAMERA_TRIGGERS + CLIMATE_TRIGGERS
+
+TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_ENTITY_ID): cv.entity_id,
+ vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
+ vol.Optional(CONF_SUBTYPE): str,
+ }
+)
+
+
+async def async_validate_trigger_config(hass, config):
+ """Validate config."""
+ config = TRIGGER_SCHEMA(config)
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get(config[CONF_DEVICE_ID])
+
+ trigger = config[CONF_TYPE]
+
+ if (
+ not device
+ or device.model not in DEVICES
+ or trigger not in DEVICES[device.model]
+ ):
+ raise InvalidDeviceAutomationConfig(f"Unsupported model {device.model}")
+
+ return config
+
+
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
+ """List device triggers for Netatmo devices."""
+ registry = await entity_registry.async_get_registry(hass)
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ triggers = []
+
+ for entry in entity_registry.async_entries_for_device(registry, device_id):
+ device = device_registry.async_get(device_id)
+
+ for trigger in DEVICES.get(device.model, []):
+ if trigger in SUBTYPES:
+ for subtype in SUBTYPES[trigger]:
+ triggers.append(
+ {
+ CONF_PLATFORM: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: trigger,
+ CONF_SUBTYPE: subtype,
+ }
+ )
+ else:
+ triggers.append(
+ {
+ CONF_PLATFORM: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_ENTITY_ID: entry.entity_id,
+ CONF_TYPE: trigger,
+ }
+ )
+
+ return triggers
+
+
+async def async_attach_trigger(
+ hass: HomeAssistant,
+ config: ConfigType,
+ action: AutomationActionType,
+ automation_info: dict,
+) -> CALLBACK_TYPE:
+ """Attach a trigger."""
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get(config[CONF_DEVICE_ID])
+
+ if not device:
+ return
+
+ if device.model not in DEVICES:
+ return
+
+ event_config = {
+ event_trigger.CONF_PLATFORM: "event",
+ event_trigger.CONF_EVENT_TYPE: NETATMO_EVENT,
+ event_trigger.CONF_EVENT_DATA: {
+ "type": config[CONF_TYPE],
+ ATTR_DEVICE_ID: config[ATTR_DEVICE_ID],
+ },
+ }
+ if config[CONF_TYPE] in SUBTYPES:
+ event_config[event_trigger.CONF_EVENT_DATA]["data"] = {
+ "mode": config[CONF_SUBTYPE]
+ }
+
+ event_config = event_trigger.TRIGGER_SCHEMA(event_config)
+ return await event_trigger.async_attach_trigger(
+ hass, event_config, action, automation_info, platform_type="device"
+ )
diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py
index dc8bf3f1fc8c01..08744c462e8402 100644
--- a/homeassistant/components/netatmo/light.py
+++ b/homeassistant/components/netatmo/light.py
@@ -31,18 +31,19 @@ async def async_setup_entry(hass, entry, async_add_entities):
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
+ await data_handler.register_data_class(
+ CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None
+ )
+
+ if CAMERA_DATA_CLASS_NAME not in data_handler.data:
+ raise PlatformNotReady
+
async def get_entities():
"""Retrieve Netatmo entities."""
- await data_handler.register_data_class(
- CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None
- )
entities = []
all_cameras = []
- if CAMERA_DATA_CLASS_NAME not in data_handler.data:
- raise PlatformNotReady
-
try:
for home in data_handler.data[CAMERA_DATA_CLASS_NAME].cameras.values():
for camera in home.values():
@@ -67,6 +68,8 @@ async def get_entities():
async_add_entities(await get_entities(), True)
+ await data_handler.unregister_data_class(CAMERA_DATA_CLASS_NAME, None)
+
class NetatmoLight(NetatmoBase, LightEntity):
"""Representation of a Netatmo Presence camera light."""
diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py
index 6375c46d394f51..db00df5129f257 100644
--- a/homeassistant/components/netatmo/media_source.py
+++ b/homeassistant/components/netatmo/media_source.py
@@ -1,8 +1,9 @@
"""Netatmo Media Source Implementation."""
+from __future__ import annotations
+
import datetime as dt
import logging
import re
-from typing import Optional, Tuple
from homeassistant.components.media_player.const import (
MEDIA_CLASS_DIRECTORY,
@@ -53,7 +54,7 @@ async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
return PlayMedia(url, MIME_TYPE)
async def async_browse_media(
- self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES
+ self, item: MediaSourceItem, media_types: tuple[str] = MEDIA_MIME_TYPES
) -> BrowseMediaSource:
"""Return media."""
try:
@@ -156,7 +157,7 @@ def remove_html_tags(text):
@callback
def async_parse_identifier(
item: MediaSourceItem,
-) -> Tuple[str, str, Optional[int]]:
+) -> tuple[str, str, int | None]:
"""Parse identifier."""
if not item.identifier:
return "events", "", None
diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py
index d0753613555ed9..e41b873bdc4d41 100644
--- a/homeassistant/components/netatmo/netatmo_entity_base.py
+++ b/homeassistant/components/netatmo/netatmo_entity_base.py
@@ -1,11 +1,12 @@
"""Base class for Netatmo entities."""
+from __future__ import annotations
+
import logging
-from typing import Dict, List
from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.helpers.entity import Entity
-from .const import DOMAIN, MANUFACTURER, MODELS, SIGNAL_NAME
+from .const import DATA_DEVICE_IDS, DOMAIN, MANUFACTURER, MODELS, SIGNAL_NAME
from .data_handler import NetatmoDataHandler
_LOGGER = logging.getLogger(__name__)
@@ -17,8 +18,8 @@ class NetatmoBase(Entity):
def __init__(self, data_handler: NetatmoDataHandler) -> None:
"""Set up Netatmo entity base."""
self.data_handler = data_handler
- self._data_classes: List[Dict] = []
- self._listeners: List[CALLBACK_TYPE] = []
+ self._data_classes: list[dict] = []
+ self._listeners: list[CALLBACK_TYPE] = []
self._device_name = None
self._id = None
@@ -58,6 +59,10 @@ async def async_added_to_hass(self) -> None:
await self.data_handler.unregister_data_class(signal_name, None)
+ registry = await self.hass.helpers.device_registry.async_get_registry()
+ device = registry.async_get_device({(DOMAIN, self._id)}, set())
+ self.hass.data[DOMAIN][DATA_DEVICE_IDS][self._id] = device.id
+
self.async_update_callback()
async def async_will_remove_from_hass(self):
diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py
index f848444481811d..4c6facb3eca161 100644
--- a/homeassistant/components/netatmo/sensor.py
+++ b/homeassistant/components/netatmo/sensor.py
@@ -1,6 +1,7 @@
"""Support for the Netatmo Weather Service."""
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_LATITUDE,
@@ -8,6 +9,7 @@
CONCENTRATION_PARTS_PER_MILLION,
DEGREE,
DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_CO2,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_SIGNAL_STRENGTH,
@@ -20,6 +22,7 @@
TEMP_CELSIUS,
)
from homeassistant.core import HomeAssistant, callback
+from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.device_registry import async_entries_for_config_entry
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
@@ -51,7 +54,7 @@
SENSOR_TYPES = {
"temperature": ["Temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE, True],
"temp_trend": ["Temperature trend", None, "mdi:trending-up", None, False],
- "co2": ["CO2", CONCENTRATION_PARTS_PER_MILLION, "mdi:molecule-co2", None, True],
+ "co2": ["CO2", CONCENTRATION_PARTS_PER_MILLION, None, DEVICE_CLASS_CO2, True],
"pressure": ["Pressure", PRESSURE_MBAR, None, DEVICE_CLASS_PRESSURE, True],
"pressure_trend": ["Pressure trend", None, "mdi:trending-up", None, False],
"noise": ["Noise", "dB", "mdi:volume-high", None, True],
@@ -129,14 +132,25 @@ async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the Netatmo weather and homecoach platform."""
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
+ await data_handler.register_data_class(
+ WEATHERSTATION_DATA_CLASS_NAME, WEATHERSTATION_DATA_CLASS_NAME, None
+ )
+ await data_handler.register_data_class(
+ HOMECOACH_DATA_CLASS_NAME, HOMECOACH_DATA_CLASS_NAME, None
+ )
+
async def find_entities(data_class_name):
"""Find all entities."""
- await data_handler.register_data_class(data_class_name, data_class_name, None)
+ if data_class_name not in data_handler.data:
+ raise PlatformNotReady
all_module_infos = {}
data = data_handler.data
- if not data.get(data_class_name):
+ if data_class_name not in data:
+ return []
+
+ if data[data_class_name] is None:
return []
data_class = data[data_class_name]
@@ -174,6 +188,8 @@ async def find_entities(data_class_name):
NetatmoSensor(data_handler, data_class_name, module, condition)
)
+ await data_handler.unregister_data_class(data_class_name, None)
+
return entities
for data_class_name in [
@@ -245,7 +261,7 @@ async def async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) ->
async_dispatcher_send(hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}")
-class NetatmoSensor(NetatmoBase):
+class NetatmoSensor(NetatmoBase, SensorEntity):
"""Implementation of a Netatmo sensor."""
def __init__(self, data_handler, data_class_name, module_info, sensor_type):
@@ -474,7 +490,7 @@ def process_wifi(strength):
return "Full"
-class NetatmoPublicSensor(NetatmoBase):
+class NetatmoPublicSensor(NetatmoBase, SensorEntity):
"""Represent a single sensor in a Netatmo."""
def __init__(self, data_handler, area, sensor_type):
@@ -521,7 +537,7 @@ def device_class(self):
return self._device_class
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the attributes of the device."""
attrs = {}
@@ -625,7 +641,7 @@ def async_update_callback(self):
elif self.type == "guststrength":
data = self._data.get_latest_gust_strengths()
- if not data:
+ if data is None:
if self._state is None:
return
_LOGGER.debug(
@@ -634,8 +650,8 @@ def async_update_callback(self):
self._state = None
return
- values = [x for x in data.values() if x is not None]
- if self._mode == "avg":
- self._state = round(sum(values) / len(values), 1)
- elif self._mode == "max":
- self._state = max(values)
+ if values := [x for x in data.values() if x is not None]:
+ if self._mode == "avg":
+ self._state = round(sum(values) / len(values), 1)
+ elif self._mode == "max":
+ self._state = max(values)
diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml
index 459ef23b0e0dbe..06f56d084c640b 100644
--- a/homeassistant/components/netatmo/services.yaml
+++ b/homeassistant/components/netatmo/services.yaml
@@ -1,46 +1,79 @@
# Describes the format for available Netatmo services
set_camera_light:
- description: Set the camera light mode.
+ name: Set camera light mode
+ description: Sets the light mode for a Netatmo Outdoor camera light.
+ target:
+ entity:
+ integration: netatmo
+ domain: light
fields:
camera_light_mode:
+ name: Camera light mode
description: Outdoor camera light mode (on/off/auto)
example: auto
- entity_id:
- description: Entity id of the camera.
- example: camera.netatmo_entrance
+ required: true
+ selector:
+ select:
+ options:
+ - "on"
+ - "off"
+ - "auto"
set_schedule:
- description: Set the heating schedule.
+ name: Set heating schedule
+ description:
+ Set the heating schedule for Netatmo climate device. The schedule name must
+ match a schedule configured at Netatmo.
+ target:
+ entity:
+ integration: netatmo
+ domain: climate
fields:
schedule_name:
description: Schedule name
example: Standard
- entity_id:
- description: Entity id of the climate device.
- example: climate.netatmo_livingroom
+ required: true
+ selector:
+ text:
set_persons_home:
- description: Set a list of persons as at home. Person's name must match a name known by the Welcome Camera.
+ name: Set persons at home
+ description:
+ Set a list of persons as at home. Person's name must match a name known by
+ the Netatmo Indoor (Welcome) Camera.
+ target:
+ entity:
+ integration: netatmo
+ domain: camera
fields:
persons:
description: List of names
example: Bob
- entity_id:
- description: Entity id of the camera.
- example: camera.netatmo_entrance
+ required: true
+ selector:
+ text:
set_person_away:
- description: Set a person away. If no person is set the home will be marked as empty. Person's name must match a name known by the Welcome Camera.
+ name: Set person away
+ description:
+ Set a person as away. If no person is set the home will be marked as empty.
+ Person's name must match a name known by the Netatmo Indoor (Welcome)
+ Camera.
+ target:
+ entity:
+ integration: netatmo
+ domain: camera
fields:
person:
description: Person's name (optional)
example: Bob
- entity_id:
- description: Entity id of the camera.
- example: camera.netatmo_entrance
+ selector:
+ text:
register_webhook:
- description: Register webhook
+ name: Register webhook
+ description: Register the webhook to the Netatmo backend.
unregister_webhook:
- description: Unregister webhook
+ name: Unregister webhook
+ description: Unregister the webhook from the Netatmo backend.
diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json
index 60fdab5f22c5f7..c65001b2e8f90b 100644
--- a/homeassistant/components/netatmo/strings.json
+++ b/homeassistant/components/netatmo/strings.json
@@ -39,5 +39,27 @@
"title": "Netatmo public weather sensor"
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "away",
+ "schedule": "schedule",
+ "hg": "frost guard"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} turned off",
+ "turned_on": "{entity_name} turned on",
+ "human": "{entity_name} detected a human",
+ "movement": "{entity_name} detected movement",
+ "person": "{entity_name} detected a person",
+ "person_away": "{entity_name} detected a person has left",
+ "animal": "{entity_name} detected an animal",
+ "outdoor": "{entity_name} detected an outdoor event",
+ "vehicle": "{entity_name} detected a vehicle",
+ "alarm_started": "{entity_name} detected an alarm",
+ "set_point": "Target temperature {entity_name} set manually",
+ "cancel_set_point": "{entity_name} has resumed its schedule",
+ "therm_mode": "{entity_name} switched to \"{subtype}\""
+ }
}
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/netatmo/translations/ca.json b/homeassistant/components/netatmo/translations/ca.json
index a6b8b5c2b82443..809223a04ae3af 100644
--- a/homeassistant/components/netatmo/translations/ca.json
+++ b/homeassistant/components/netatmo/translations/ca.json
@@ -15,6 +15,28 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "a fora",
+ "hg": "protecci\u00f3 contra gelades",
+ "schedule": "programaci\u00f3"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name} ha detectat una alarma",
+ "animal": "{entity_name} ha detectat un animal",
+ "cancel_set_point": "{entity_name} ha repr\u00e8s la programaci\u00f3",
+ "human": "{entity_name} ha detectat un hum\u00e0",
+ "movement": "{entity_name} ha detectat moviment",
+ "outdoor": "{entity_name} ha detectat un esdeveniment a fora",
+ "person": "{entity_name} ha detectat una persona",
+ "person_away": "{entity_name} ha detectat una marxant",
+ "set_point": "Temperatura objectiu {entity_name} configurada manualment",
+ "therm_mode": "{entity_name} ha canviar a \"{subtype}\"",
+ "turned_off": "{entity_name} s'ha apagat",
+ "turned_on": "{entity_name} s'ha engegat",
+ "vehicle": "{entity_name} ha detectat un vehicle"
+ }
+ },
"options": {
"step": {
"public_weather": {
diff --git a/homeassistant/components/netatmo/translations/de.json b/homeassistant/components/netatmo/translations/de.json
index 30cfba6dfed9a3..dccb58577487e5 100644
--- a/homeassistant/components/netatmo/translations/de.json
+++ b/homeassistant/components/netatmo/translations/de.json
@@ -1,8 +1,10 @@
{
"config": {
"abort": {
- "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Autorisierungs-URL.",
- "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation."
+ "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.",
+ "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.",
+ "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).",
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"create_entry": {
"default": "Erfolgreich authentifiziert."
@@ -13,6 +15,28 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "Abwesenheit",
+ "hg": "Frostw\u00e4chter",
+ "schedule": "Zeitplan"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name} hat einen Alarm erkannt",
+ "animal": "{entity_name} hat ein Tier erkannt",
+ "cancel_set_point": "{entity_name} hat seinen Zeitplan wieder aufgenommen",
+ "human": "{entity_name} hat einen Menschen erkannt",
+ "movement": "{entity_name} hat eine Bewegung erkannt",
+ "outdoor": "{entity_name} hat ein Ereignis im Freien erkannt",
+ "person": "{entity_name} hat eine Person erkannt",
+ "person_away": "{entity_name} hat erkannt, dass eine Person gegangen ist",
+ "set_point": "Solltemperatur {entity_name} manuell eingestellt",
+ "therm_mode": "{entity_name} wechselte zu \"{subtype}\"",
+ "turned_off": "{entity_name} ausgeschaltet",
+ "turned_on": "{entity_name} eingeschaltet",
+ "vehicle": "{entity_name} hat ein Fahrzeug erkannt"
+ }
+ },
"options": {
"step": {
"public_weather": {
@@ -28,6 +52,7 @@
},
"public_weather_areas": {
"data": {
+ "new_area": "Bereichsname",
"weather_areas": "Wettergebiete"
},
"description": "Konfiguriere \u00f6ffentliche Wettersensoren.",
diff --git a/homeassistant/components/netatmo/translations/el.json b/homeassistant/components/netatmo/translations/el.json
new file mode 100644
index 00000000000000..03a1530be9b74d
--- /dev/null
+++ b/homeassistant/components/netatmo/translations/el.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "\u03b5\u03ba\u03c4\u03cc\u03c2",
+ "hg": "\u03c0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c3\u03af\u03b1 \u03c0\u03b1\u03b3\u03b5\u03c4\u03bf\u03cd",
+ "schedule": "\u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name} \u03b5\u03bd\u03c4\u03cc\u03c0\u03b9\u03c3\u03b5 \u03ad\u03bd\u03b1\u03bd \u03c3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03cc",
+ "animal": "{entity_name} \u03b5\u03bd\u03c4\u03cc\u03c0\u03b9\u03c3\u03b5 \u03ad\u03bd\u03b1 \u03b6\u03ce\u03bf",
+ "cancel_set_point": "\u03a4\u03bf {entity_name} \u03c3\u03c5\u03bd\u03ad\u03c7\u03b9\u03c3\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03ac \u03c4\u03bf\u03c5",
+ "human": "{entity_name} \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ad\u03bd\u03b1\u03c2 \u03ac\u03bd\u03b8\u03c1\u03c9\u03c0\u03bf\u03c2",
+ "movement": "{entity_name} \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ba\u03af\u03bd\u03b7\u03c3\u03b7",
+ "outdoor": "\u03a4\u03bf {entity_name} \u03b5\u03bd\u03c4\u03cc\u03c0\u03b9\u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd \u03b5\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03bf\u03cd \u03c7\u03ce\u03c1\u03bf\u03c5",
+ "person": "{entity_name} \u03b5\u03bd\u03c4\u03cc\u03c0\u03b9\u03c3\u03b5 \u03ad\u03bd\u03b1 \u03ac\u03c4\u03bf\u03bc\u03bf",
+ "person_away": "{entity_name} \u03b5\u03bd\u03c4\u03cc\u03c0\u03b9\u03c3\u03b5 \u03cc\u03c4\u03b9 \u03ad\u03bd\u03b1 \u03ac\u03c4\u03bf\u03bc\u03bf \u03ad\u03c7\u03b5\u03b9 \u03c6\u03cd\u03b3\u03b5\u03b9"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/netatmo/translations/en.json b/homeassistant/components/netatmo/translations/en.json
index e31d801b7a0da2..7e230374720a09 100644
--- a/homeassistant/components/netatmo/translations/en.json
+++ b/homeassistant/components/netatmo/translations/en.json
@@ -15,6 +15,28 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "away",
+ "hg": "frost guard",
+ "schedule": "schedule"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name} detected an alarm",
+ "animal": "{entity_name} detected an animal",
+ "cancel_set_point": "{entity_name} has resumed its schedule",
+ "human": "{entity_name} detected a human",
+ "movement": "{entity_name} detected movement",
+ "outdoor": "{entity_name} detected an outdoor event",
+ "person": "{entity_name} detected a person",
+ "person_away": "{entity_name} detected a person has left",
+ "set_point": "Target temperature {entity_name} set manually",
+ "therm_mode": "{entity_name} switched to \"{subtype}\"",
+ "turned_off": "{entity_name} turned off",
+ "turned_on": "{entity_name} turned on",
+ "vehicle": "{entity_name} detected a vehicle"
+ }
+ },
"options": {
"step": {
"public_weather": {
diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json
index 556fe2626d491b..b1159c1dd9d306 100644
--- a/homeassistant/components/netatmo/translations/es.json
+++ b/homeassistant/components/netatmo/translations/es.json
@@ -15,6 +15,28 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "fuera",
+ "hg": "protector contra las heladas",
+ "schedule": "Horario"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name} ha detectado una alarma",
+ "animal": "{entity_name} ha detectado un animal",
+ "cancel_set_point": "{entity_name} ha reanudado su programaci\u00f3n",
+ "human": "{entity_name} ha detectado una persona",
+ "movement": "{entity_name} ha detectado movimiento",
+ "outdoor": "{entity_name} ha detectado un evento en el exterior",
+ "person": "{entity_name} ha detectado una persona",
+ "person_away": "{entity_name} ha detectado que una persona se ha ido",
+ "set_point": "Temperatura objetivo {entity_name} fijada manualmente",
+ "therm_mode": "{entity_name} cambi\u00f3 a \" {subtype} \"",
+ "turned_off": "{entity_name} desactivado",
+ "turned_on": "{entity_name} activado",
+ "vehicle": "{entity_name} ha detectado un veh\u00edculo"
+ }
+ },
"options": {
"step": {
"public_weather": {
diff --git a/homeassistant/components/netatmo/translations/et.json b/homeassistant/components/netatmo/translations/et.json
index 99e062b38422b6..8725eb48016a13 100644
--- a/homeassistant/components/netatmo/translations/et.json
+++ b/homeassistant/components/netatmo/translations/et.json
@@ -15,6 +15,28 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "eemal",
+ "hg": "k\u00fclmumiskaitse",
+ "schedule": "ajastus"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name} tuvastas h\u00e4ire",
+ "animal": "{entity_name} tuvastas looma",
+ "cancel_set_point": "{entity_name} on oma ajakava j\u00e4tkanud",
+ "human": "{entity_name} tuvastas inimese",
+ "movement": "{entity_name} tuvastas liikumise",
+ "outdoor": "{entity_name} tuvastas v\u00e4lise s\u00fcndmuse",
+ "person": "{entity_name} tuvastas isiku",
+ "person_away": "{entity_name} tuvastas inimese eemaldumise",
+ "set_point": "Sihttemperatuur {entity_name} on k\u00e4sitsi m\u00e4\u00e4ratud",
+ "therm_mode": "{entity_name} l\u00fclitus olekusse {subtype}.",
+ "turned_off": "{entity_name} l\u00fclitus v\u00e4lja",
+ "turned_on": "{entity_name} l\u00fclitus sisse",
+ "vehicle": "{entity_name} tuvastas s\u00f5iduki"
+ }
+ },
"options": {
"step": {
"public_weather": {
diff --git a/homeassistant/components/netatmo/translations/fr.json b/homeassistant/components/netatmo/translations/fr.json
index fe8fc74d273473..6c294d467abb6c 100644
--- a/homeassistant/components/netatmo/translations/fr.json
+++ b/homeassistant/components/netatmo/translations/fr.json
@@ -15,6 +15,28 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "absent",
+ "hg": "garde-gel",
+ "schedule": "horaire"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name} a d\u00e9tect\u00e9 une alarme",
+ "animal": "{entity_name} a d\u00e9tect\u00e9 un animal",
+ "cancel_set_point": "{entity_name} a repris son programme",
+ "human": "{entity_name} a d\u00e9tect\u00e9 une personne",
+ "movement": "{entity_name} a d\u00e9tect\u00e9 un mouvement",
+ "outdoor": "{entity_name} a d\u00e9tect\u00e9 un \u00e9v\u00e9nement ext\u00e9rieur",
+ "person": "{entity_name} a d\u00e9tect\u00e9 une personne",
+ "person_away": "{entity_name} a d\u00e9tect\u00e9 qu\u2019une personne est partie",
+ "set_point": "Temp\u00e9rature cible {entity_name} d\u00e9finie manuellement",
+ "therm_mode": "{entity_name} est pass\u00e9 \u00e0 \"{subtype}\"",
+ "turned_off": "{entity_name} d\u00e9sactiv\u00e9",
+ "turned_on": "{entity_name} activ\u00e9",
+ "vehicle": "{entity_name} a d\u00e9tect\u00e9 un v\u00e9hicule"
+ }
+ },
"options": {
"step": {
"public_weather": {
diff --git a/homeassistant/components/netatmo/translations/he.json b/homeassistant/components/netatmo/translations/he.json
new file mode 100644
index 00000000000000..54bef84c30a4a2
--- /dev/null
+++ b/homeassistant/components/netatmo/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "device_automation": {
+ "trigger_type": {
+ "animal": "\u05d6\u05d9\u05d4\u05d4 \u05d1\u05e2\u05dc-\u05d7\u05d9\u05d9\u05dd",
+ "human": "\u05d6\u05d9\u05d4\u05d4 \u05d0\u05d3\u05dd",
+ "movement": "\u05d6\u05d9\u05d4\u05d4 \u05ea\u05e0\u05d5\u05e2\u05d4",
+ "turned_off": "\u05db\u05d1\u05d4",
+ "turned_on": "\u05e0\u05d3\u05dc\u05e7"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/netatmo/translations/hu.json b/homeassistant/components/netatmo/translations/hu.json
index cae1f6d20c0393..b4979396eeb081 100644
--- a/homeassistant/components/netatmo/translations/hu.json
+++ b/homeassistant/components/netatmo/translations/hu.json
@@ -2,7 +2,9 @@
"config": {
"abort": {
"authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.",
- "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t."
+ "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.",
+ "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
},
"create_entry": {
"default": "Sikeres hiteles\u00edt\u00e9s"
@@ -12,5 +14,40 @@
"title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert"
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "t\u00e1vol"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name} riaszt\u00e1st \u00e9szlelt",
+ "animal": "{entity_name} \u00e9szlelt egy \u00e1llatot",
+ "cancel_set_point": "{entity_name} folytatta \u00fctemez\u00e9s\u00e9t",
+ "human": "{entity_name} embert \u00e9szlelt",
+ "movement": "{entity_name} mozg\u00e1st \u00e9szlelt",
+ "outdoor": "{entity_name} k\u00fclt\u00e9ri esem\u00e9nyt \u00e9szlelt",
+ "person": "{entity_name} szem\u00e9lyt \u00e9szlelt",
+ "person_away": "{entity_name} \u00e9szlelte, hogy egy szem\u00e9ly t\u00e1vozott",
+ "set_point": "A(z) {entity_name} c\u00e9lh\u0151m\u00e9rs\u00e9klet manu\u00e1lisan lett be\u00e1ll\u00edtva",
+ "therm_mode": "{entity_name} \u00e1tv\u00e1ltott erre: \"{subtype}\"",
+ "turned_off": "{entity_name} ki lett kapcsolva",
+ "turned_on": "{entity_name} be lett kapcsolva",
+ "vehicle": "{entity_name} j\u00e1rm\u0171vet \u00e9szlelt"
+ }
+ },
+ "options": {
+ "step": {
+ "public_weather": {
+ "data": {
+ "area_name": "A ter\u00fclet neve"
+ }
+ },
+ "public_weather_areas": {
+ "data": {
+ "new_area": "Ter\u00fclet neve",
+ "weather_areas": "Id\u0151j\u00e1r\u00e1si ter\u00fcletek"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/netatmo/translations/id.json b/homeassistant/components/netatmo/translations/id.json
new file mode 100644
index 00000000000000..6812d45816b492
--- /dev/null
+++ b/homeassistant/components/netatmo/translations/id.json
@@ -0,0 +1,64 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.",
+ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.",
+ "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "create_entry": {
+ "default": "Berhasil diautentikasi"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Pilih Metode Autentikasi"
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "keluar",
+ "schedule": "jadwal"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name} mendeteksi alarm",
+ "animal": "{entity_name} mendeteksi binatang",
+ "cancel_set_point": "{entity_name} telah melanjutkan jadwalnya",
+ "human": "{entity_name} mendeteksi manusia",
+ "movement": "{entity_name} mendeteksi gerakan",
+ "outdoor": "{entity_name} mendeteksi peristiwa luar ruangan",
+ "person": "{entity_name} mendeteksi seseorang",
+ "person_away": "{entity_name} mendeteksi seseorang telah pergi",
+ "set_point": "Suhu target {entity_name} disetel secara manual",
+ "therm_mode": "{entity_name} beralih ke \"{subtype}\"",
+ "turned_off": "{entity_name} dimatikan",
+ "turned_on": "{entity_name} dinyalakan",
+ "vehicle": "{entity_name} mendeteksi kendaraan"
+ }
+ },
+ "options": {
+ "step": {
+ "public_weather": {
+ "data": {
+ "area_name": "Nama area",
+ "lat_ne": "Lintang Pojok Timur Laut",
+ "lat_sw": "Lintang Pojok Barat Daya",
+ "lon_ne": "Bujur Pojok Timur Laut",
+ "lon_sw": "Bujur Pojok Barat Daya",
+ "mode": "Perhitungan",
+ "show_on_map": "Tampilkan di peta"
+ },
+ "description": "Konfigurasikan sensor cuaca publik untuk suatu area.",
+ "title": "Sensor cuaca publik Netatmo"
+ },
+ "public_weather_areas": {
+ "data": {
+ "new_area": "Nama area",
+ "weather_areas": "Area cuaca"
+ },
+ "description": "Konfigurasikan sensor cuaca publik.",
+ "title": "Sensor cuaca publik Netatmo"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/netatmo/translations/it.json b/homeassistant/components/netatmo/translations/it.json
index 46c2d7d2721814..152f7d47597ac8 100644
--- a/homeassistant/components/netatmo/translations/it.json
+++ b/homeassistant/components/netatmo/translations/it.json
@@ -15,6 +15,28 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "Fuori casa",
+ "hg": "protezione antigelo",
+ "schedule": "programma"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name} ha rilevato un allarme",
+ "animal": "{entity_name} ha rilevato un animale",
+ "cancel_set_point": "{entity_name} ha ripreso il suo programma",
+ "human": "{entity_name} ha rilevato un essere umano",
+ "movement": "{entity_name} ha rilevato un movimento",
+ "outdoor": "{entity_name} ha rilevato un evento all'esterno",
+ "person": "{entity_name} ha rilevato una persona",
+ "person_away": "{entity_name} ha rilevato che una persona \u00e8 uscita",
+ "set_point": "{entity_name} temperatura desiderata impostata manualmente",
+ "therm_mode": "{entity_name} \u00e8 passato a \"{subtype}\"",
+ "turned_off": "{entity_name} disattivato",
+ "turned_on": "{entity_name} attivato",
+ "vehicle": "{entity_name} ha rilevato un veicolo"
+ }
+ },
"options": {
"step": {
"public_weather": {
diff --git a/homeassistant/components/netatmo/translations/ko.json b/homeassistant/components/netatmo/translations/ko.json
index 8165941f0d8fa1..fee370ce219f1e 100644
--- a/homeassistant/components/netatmo/translations/ko.json
+++ b/homeassistant/components/netatmo/translations/ko.json
@@ -3,7 +3,8 @@
"abort": {
"authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
- "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})"
+ "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"create_entry": {
"default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
@@ -14,6 +15,28 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "\uc678\ucd9c",
+ "hg": "\ub3d9\ud30c \ubc29\uc9c0",
+ "schedule": "\uc77c\uc815"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name}\uc774(\uac00) \uc54c\ub78c\uc744 \uac10\uc9c0\ud588\uc744 \ub54c",
+ "animal": "{entity_name}\uc774(\uac00) \ub3d9\ubb3c\uc744 \uac10\uc9c0\ud588\uc744 \ub54c",
+ "cancel_set_point": "{entity_name}\uc774(\uac00) \uc77c\uc815\uc744 \uc7ac\uac1c\ud560 \ub54c",
+ "human": "{entity_name}\uc774(\uac00) \uc0ac\ub78c\uc744 \uac10\uc9c0\ud588\uc744 \ub54c",
+ "movement": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud588\uc744 \ub54c",
+ "outdoor": "{entity_name}\uc774(\uac00) \uc2e4\uc678\uc758 \uc774\ubca4\ud2b8\ub97c \uac10\uc9c0\ud588\uc744 \ub54c",
+ "person": "{entity_name}\uc774(\uac00) \uc0ac\ub78c\uc744 \uac10\uc9c0\ud588\uc744 \ub54c",
+ "person_away": "{entity_name}\uc774(\uac00) \uc0ac\ub78c\uc774 \ub5a0\ub0ac\uc74c\uc744 \uac10\uc9c0\ud588\uc744 \ub54c",
+ "set_point": "{entity_name}\uc758 \ubaa9\ud45c \uc628\ub3c4\uac00 \uc218\ub3d9\uc73c\ub85c \uc124\uc815\ub418\uc5c8\uc744 \ub54c",
+ "therm_mode": "{entity_name}\uc774(\uac00) {subtype}(\uc73c)\ub85c \uc804\ud658\ub418\uc5c8\uc744 \ub54c",
+ "turned_off": "{entity_name}\uc774(\uac00) \uaebc\uc84c\uc744 \ub54c",
+ "turned_on": "{entity_name}\uc774(\uac00) \ucf1c\uc84c\uc744 \ub54c",
+ "vehicle": "{entity_name}\uc774(\uac00) \ucc28\ub7c9\uc744 \uac10\uc9c0\ud588\uc744 \ub54c"
+ }
+ },
"options": {
"step": {
"public_weather": {
@@ -26,16 +49,16 @@
"mode": "\uacc4\uc0b0\ud558\uae30",
"show_on_map": "\uc9c0\ub3c4\uc5d0 \ud45c\uc2dc\ud558\uae30"
},
- "description": "\uc9c0\uc5ed\uc5d0 \ub300\ud55c \uacf5\uc6a9 \ub0a0\uc528 \uc13c\uc11c\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.",
- "title": "Netamo \uacf5\uc6a9 \ub0a0\uc528 \uc13c\uc11c"
+ "description": "\uc9c0\uc5ed\uc5d0 \ub300\ud55c \uacf5\uacf5 \uae30\uc0c1 \uc13c\uc11c\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.",
+ "title": "Netamo \uacf5\uacf5 \uae30\uc0c1 \uc13c\uc11c"
},
"public_weather_areas": {
"data": {
"new_area": "\uc9c0\uc5ed \uc774\ub984",
"weather_areas": "\ub0a0\uc528 \uc9c0\uc5ed"
},
- "description": "\uacf5\uc6a9 \ub0a0\uc528 \uc13c\uc11c\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.",
- "title": "Netamo \uacf5\uc6a9 \ub0a0\uc528 \uc13c\uc11c"
+ "description": "\uacf5\uacf5 \uae30\uc0c1 \uc13c\uc11c\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.",
+ "title": "Netamo \uacf5\uacf5 \uae30\uc0c1 \uc13c\uc11c"
}
}
}
diff --git a/homeassistant/components/netatmo/translations/nl.json b/homeassistant/components/netatmo/translations/nl.json
index eab1d9741ad136..dc811b63534245 100644
--- a/homeassistant/components/netatmo/translations/nl.json
+++ b/homeassistant/components/netatmo/translations/nl.json
@@ -2,10 +2,12 @@
"config": {
"abort": {
"authorize_url_timeout": "Time-out genereren autorisatie-URL.",
- "missing_configuration": "Het component is niet geconfigureerd. Volg de documentatie."
+ "missing_configuration": "Het component is niet geconfigureerd. Volg de documentatie.",
+ "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})",
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
},
"create_entry": {
- "default": "Succesvol geauthenticeerd met Netatmo."
+ "default": "Succesvol geauthenticeerd"
},
"step": {
"pick_implementation": {
@@ -13,17 +15,50 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "afwezig",
+ "hg": "vorstbescherming",
+ "schedule": "schema"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name} heeft een alarm gedetecteerd",
+ "animal": "{entity_name} heeft een dier gedetecteerd",
+ "cancel_set_point": "{entity_name} heeft zijn schema hervat",
+ "human": "{entity_name} heeft een mens gedetecteerd",
+ "movement": "{entity_name} heeft beweging gedetecteerd",
+ "outdoor": "{entity_name} heeft een buitengebeurtenis gedetecteerd",
+ "person": "{entity_name} heeft een persoon gedetecteerd",
+ "person_away": "{entity_name} heeft gedetecteerd dat een persoon is vertrokken",
+ "set_point": "Doeltemperatuur {entity_name} handmatig ingesteld",
+ "therm_mode": "{entity_name} is overgeschakeld naar \"{subtype}\"",
+ "turned_off": "{entity_name} uitgeschakeld",
+ "turned_on": "{entity_name} ingeschakeld",
+ "vehicle": "{entity_name} heeft een voertuig gedetecteerd"
+ }
+ },
"options": {
"step": {
"public_weather": {
"data": {
"area_name": "Naam van het gebied",
+ "lat_ne": "Breedtegraad Noordoostelijke hoek",
+ "lat_sw": "Breedtegraad Zuidwestelijke hoek",
+ "lon_ne": "Lengtegraad Noordoostelijke hoek",
+ "lon_sw": "Lengtegraad Zuidwestelijke hoek",
"mode": "Berekening",
"show_on_map": "Toon op kaart"
- }
+ },
+ "description": "Configureer een openbare weersensor voor een gebied.",
+ "title": "Netatmo openbare weersensor"
},
"public_weather_areas": {
- "description": "Configureer openbare weersensoren."
+ "data": {
+ "new_area": "Naam van het gebied",
+ "weather_areas": "Weersgebieden"
+ },
+ "description": "Configureer openbare weersensoren.",
+ "title": "Netatmo openbare weersensor"
}
}
}
diff --git a/homeassistant/components/netatmo/translations/no.json b/homeassistant/components/netatmo/translations/no.json
index 387dbe7b26cf3c..9e3e24d5771b66 100644
--- a/homeassistant/components/netatmo/translations/no.json
+++ b/homeassistant/components/netatmo/translations/no.json
@@ -15,6 +15,28 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "borte",
+ "hg": "frostvakt",
+ "schedule": "Tidsplan"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name} oppdaget en alarm",
+ "animal": "{entity_name} oppdaget et dyr",
+ "cancel_set_point": "{entity_name} har gjenopptatt tidsplanen",
+ "human": "{entity_name} oppdaget et menneske",
+ "movement": "{entity_name} oppdaget bevegelse",
+ "outdoor": "{entity_name} oppdaget en utend\u00f8rs hendelse",
+ "person": "{entity_name} oppdaget en person",
+ "person_away": "{entity_name} oppdaget at en person har forlatt",
+ "set_point": "M\u00e5ltemperatur {entity_name} angis manuelt",
+ "therm_mode": "{entity_name} byttet til \"{subtype}\"",
+ "turned_off": "{entity_name} sl\u00e5tt av",
+ "turned_on": "{entity_name} sl\u00e5tt p\u00e5",
+ "vehicle": "{entity_name} oppdaget et kj\u00f8ret\u00f8y"
+ }
+ },
"options": {
"step": {
"public_weather": {
diff --git a/homeassistant/components/netatmo/translations/pl.json b/homeassistant/components/netatmo/translations/pl.json
index b41689a9f8cdaa..449e09bfa3a08c 100644
--- a/homeassistant/components/netatmo/translations/pl.json
+++ b/homeassistant/components/netatmo/translations/pl.json
@@ -15,6 +15,28 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "poza domem",
+ "hg": "ochrona przed mrozem",
+ "schedule": "harmonogram"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name} wykryje alarm",
+ "animal": "{entity_name} wykryje zwierz\u0119",
+ "cancel_set_point": "{entity_name} wznowi sw\u00f3j harmonogram",
+ "human": "{entity_name} wykryje cz\u0142owieka",
+ "movement": "{entity_name} wykryje ruch",
+ "outdoor": "{entity_name} wykryje zdarzenie zewn\u0119trzne",
+ "person": "{entity_name} wykryje osob\u0119",
+ "person_away": "{entity_name} wykryje, \u017ce osoba wysz\u0142a",
+ "set_point": "temperatura docelowa {entity_name} zosta\u0142a ustawiona r\u0119cznie",
+ "therm_mode": "{entity_name} prze\u0142\u0105czy\u0142(a) si\u0119 na \u201e{subtype}\u201d",
+ "turned_off": "{entity_name} zostanie wy\u0142\u0105czony",
+ "turned_on": "{entity_name} zostanie w\u0142\u0105czony",
+ "vehicle": "{entity_name} wykryje pojazd"
+ }
+ },
"options": {
"step": {
"public_weather": {
diff --git a/homeassistant/components/netatmo/translations/ru.json b/homeassistant/components/netatmo/translations/ru.json
index c9be7e60825004..b25e0843967487 100644
--- a/homeassistant/components/netatmo/translations/ru.json
+++ b/homeassistant/components/netatmo/translations/ru.json
@@ -15,6 +15,28 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "\u043d\u0435 \u0434\u043e\u043c\u0430",
+ "hg": "\u0437\u0430\u0449\u0438\u0442\u0430 \u043e\u0442 \u0437\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u0438\u044f",
+ "schedule": "\u0440\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0435"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u0440\u0435\u0432\u043e\u0433\u0443",
+ "animal": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0436\u0438\u0432\u043e\u0442\u043d\u043e\u0435",
+ "cancel_set_point": "{entity_name} \u0432\u043e\u0437\u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442 \u0441\u0432\u043e\u0435 \u0440\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0435",
+ "human": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0447\u0435\u043b\u043e\u0432\u0435\u043a\u0430",
+ "movement": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435",
+ "outdoor": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043d\u0430 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0432\u043e\u0437\u0434\u0443\u0445\u0435",
+ "person": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0435\u0440\u0441\u043e\u043d\u0443",
+ "person_away": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442, \u0447\u0442\u043e \u043f\u0435\u0440\u0441\u043e\u043d\u0430 \u0443\u0448\u043b\u0430",
+ "set_point": "\u0426\u0435\u043b\u0435\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 {entity_name} \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0430 \u0432\u0440\u0443\u0447\u043d\u0443\u044e",
+ "therm_mode": "{entity_name} \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u043d\u0430 \"{subtype}\"",
+ "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f",
+ "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f",
+ "vehicle": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u043e\u0435 \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e"
+ }
+ },
"options": {
"step": {
"public_weather": {
diff --git a/homeassistant/components/netatmo/translations/tr.json b/homeassistant/components/netatmo/translations/tr.json
new file mode 100644
index 00000000000000..69646be2292b1b
--- /dev/null
+++ b/homeassistant/components/netatmo/translations/tr.json
@@ -0,0 +1,48 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "uzakta",
+ "hg": "donma korumas\u0131",
+ "schedule": "Zamanlama"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name} bir alarm alg\u0131lad\u0131",
+ "animal": "{entity_name} bir hayvan tespit etti",
+ "cancel_set_point": "{entity_name} zamanlamas\u0131na devam etti",
+ "human": "{entity_name} bir insan alg\u0131lad\u0131",
+ "movement": "{entity_name} hareket alg\u0131lad\u0131",
+ "turned_off": "{entity_name} kapat\u0131ld\u0131",
+ "turned_on": "{entity_name} a\u00e7\u0131ld\u0131"
+ }
+ },
+ "options": {
+ "step": {
+ "public_weather": {
+ "data": {
+ "area_name": "Alan\u0131n ad\u0131",
+ "lat_ne": "Enlem Kuzey-Do\u011fu k\u00f6\u015fesi",
+ "lat_sw": "Enlem G\u00fcney-Bat\u0131 k\u00f6\u015fesi",
+ "lon_ne": "Boylam Kuzey-Do\u011fu k\u00f6\u015fesi",
+ "lon_sw": "Boylam G\u00fcney-Bat\u0131 k\u00f6\u015fesi",
+ "mode": "Hesaplama",
+ "show_on_map": "Haritada g\u00f6ster"
+ },
+ "description": "Bir alan i\u00e7in genel hava durumu sens\u00f6r\u00fc yap\u0131land\u0131r\u0131n.",
+ "title": "Netatmo genel hava durumu sens\u00f6r\u00fc"
+ },
+ "public_weather_areas": {
+ "data": {
+ "new_area": "Alan ad\u0131",
+ "weather_areas": "Hava alanlar\u0131"
+ },
+ "description": "Genel hava durumu sens\u00f6rlerini yap\u0131land\u0131r\u0131n.",
+ "title": "Netatmo genel hava durumu sens\u00f6r\u00fc"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/netatmo/translations/uk.json b/homeassistant/components/netatmo/translations/uk.json
new file mode 100644
index 00000000000000..b8c439edfde4fc
--- /dev/null
+++ b/homeassistant/components/netatmo/translations/uk.json
@@ -0,0 +1,43 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.",
+ "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "create_entry": {
+ "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e."
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "public_weather": {
+ "data": {
+ "area_name": "\u041d\u0430\u0437\u0432\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u0456",
+ "lat_ne": "\u0428\u0438\u0440\u043e\u0442\u0430 (\u043f\u0456\u0432\u043d\u0456\u0447\u043d\u043e-\u0441\u0445\u0456\u0434\u043d\u0438\u0439 \u043a\u0443\u0442)",
+ "lat_sw": "\u0428\u0438\u0440\u043e\u0442\u0430 (\u044e\u0433\u043e-\u0437\u0430\u043f\u0430\u0434\u043d\u044b\u0439 \u0443\u0433\u043e\u043b)",
+ "lon_ne": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430 (\u043f\u0456\u0432\u043d\u0456\u0447\u043d\u043e-\u0441\u0445\u0456\u0434\u043d\u0438\u0439 \u043a\u0443\u0442)",
+ "lon_sw": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430 (\u043f\u0456\u0432\u0434\u0435\u043d\u043d\u043e-\u0437\u0430\u0445\u0456\u0434\u043d\u0438\u0439 \u043a\u0443\u0442)",
+ "mode": "\u0420\u043e\u0437\u0440\u0430\u0445\u0443\u043d\u043e\u043a",
+ "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043d\u0430 \u043c\u0430\u043f\u0456"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0437\u0430\u0433\u0430\u043b\u044c\u043d\u043e\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u043f\u043e\u0433\u043e\u0434\u0438 \u0434\u043b\u044f \u043e\u0431\u043b\u0430\u0441\u0442\u0456",
+ "title": "\u0417\u0430\u0433\u0430\u043b\u044c\u043d\u043e\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u043f\u043e\u0433\u043e\u0434\u0438 Netatmo"
+ },
+ "public_weather_areas": {
+ "data": {
+ "new_area": "\u041d\u0430\u0437\u0432\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u0456",
+ "weather_areas": "\u041f\u043e\u0433\u043e\u0434\u043d\u0456 \u043e\u0431\u043b\u0430\u0441\u0442\u0456"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u0430\u0433\u0430\u043b\u044c\u043d\u043e\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u0434\u0430\u0442\u0447\u0438\u043a\u0456\u0432 \u043f\u043e\u0433\u043e\u0434\u0438",
+ "title": "\u0417\u0430\u0433\u0430\u043b\u044c\u043d\u043e\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u043f\u043e\u0433\u043e\u0434\u0438 Netatmo"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/netatmo/translations/zh-Hans.json b/homeassistant/components/netatmo/translations/zh-Hans.json
new file mode 100644
index 00000000000000..0e8f01ed8f724a
--- /dev/null
+++ b/homeassistant/components/netatmo/translations/zh-Hans.json
@@ -0,0 +1,16 @@
+{
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "\u79bb\u5f00",
+ "schedule": "\u65e5\u7a0b"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name} \u68c0\u6d4b\u5230\u4e00\u4e2a\u95f9\u949f",
+ "animal": "{entity_name} \u68c0\u6d4b\u5230\u4e00\u4e2a\u52a8\u7269",
+ "human": "{entity_name} \u68c0\u6d4b\u5230\u4e00\u4e2a\u4eba",
+ "outdoor": "{entity_name} \u68c0\u6d4b\u5230\u4e00\u4e2a\u51fa\u95e8\u4e8b\u4ef6",
+ "person": "{entity_name} \u68c0\u6d4b\u5230\u4e00\u4e2a\u4eba",
+ "person_away": "{entity_name} \u68c0\u6d4b\u5230\u4e00\u4e2a\u4eba\u79bb\u5f00\u4e86"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/netatmo/translations/zh-Hant.json b/homeassistant/components/netatmo/translations/zh-Hant.json
index e396deabb68c0b..e62836f9a7e4ad 100644
--- a/homeassistant/components/netatmo/translations/zh-Hant.json
+++ b/homeassistant/components/netatmo/translations/zh-Hant.json
@@ -15,6 +15,28 @@
}
}
},
+ "device_automation": {
+ "trigger_subtype": {
+ "away": "\u96e2\u5bb6",
+ "hg": "\u9632\u51cd\u6a21\u5f0f",
+ "schedule": "\u6392\u7a0b"
+ },
+ "trigger_type": {
+ "alarm_started": "{entity_name}\u5075\u6e2c\u5230\u8b66\u5831",
+ "animal": "{entity_name}\u5075\u6e2c\u5230\u52d5\u7269",
+ "cancel_set_point": "{entity_name}\u5df2\u6062\u5fa9\u5176\u6392\u7a0b",
+ "human": "{entity_name}\u5075\u6e2c\u5230\u4eba\u985e",
+ "movement": "{entity_name}\u5075\u6e2c\u5230\u52d5\u4f5c",
+ "outdoor": "{entity_name}\u5075\u6e2c\u5230\u6236\u5916\u52d5\u4f5c",
+ "person": "{entity_name}\u5075\u6e2c\u5230\u4eba\u54e1",
+ "person_away": "{entity_name}\u5075\u6e2c\u5230\u4eba\u54e1\u5df2\u96e2\u958b",
+ "set_point": "\u624b\u52d5\u8a2d\u5b9a{entity_name}\u76ee\u6a19\u6eab\u5ea6",
+ "therm_mode": "{entity_name}\u5207\u63db\u81f3 \"{subtype}\"",
+ "turned_off": "{entity_name}\u5df2\u95dc\u9589",
+ "turned_on": "{entity_name}\u5df2\u958b\u555f",
+ "vehicle": "{entity_name}\u5075\u6e2c\u5230\u8eca\u8f1b"
+ }
+ },
"options": {
"step": {
"public_weather": {
diff --git a/homeassistant/components/netatmo/webhook.py b/homeassistant/components/netatmo/webhook.py
index 582fce8985c608..54db95e9aa0a27 100644
--- a/homeassistant/components/netatmo/webhook.py
+++ b/homeassistant/components/netatmo/webhook.py
@@ -1,31 +1,31 @@
"""The Netatmo integration."""
import logging
-from homeassistant.core import callback
+from homeassistant.const import ATTR_DEVICE_ID, ATTR_ID, ATTR_NAME
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import (
ATTR_EVENT_TYPE,
ATTR_FACE_URL,
- ATTR_ID,
ATTR_IS_KNOWN,
- ATTR_NAME,
ATTR_PERSONS,
+ DATA_DEVICE_IDS,
DATA_PERSONS,
DEFAULT_PERSON,
DOMAIN,
+ EVENT_ID_MAP,
NETATMO_EVENT,
)
_LOGGER = logging.getLogger(__name__)
-EVENT_TYPE_MAP = {
+SUBEVENT_TYPE_MAP = {
"outdoor": "",
"therm_mode": "",
}
-async def handle_webhook(hass, webhook_id, request):
+async def async_handle_webhook(hass, webhook_id, request):
"""Handle webhook callback."""
try:
data = await request.json()
@@ -37,17 +37,16 @@ async def handle_webhook(hass, webhook_id, request):
event_type = data.get(ATTR_EVENT_TYPE)
- if event_type in EVENT_TYPE_MAP:
+ if event_type in SUBEVENT_TYPE_MAP:
async_send_event(hass, event_type, data)
- for event_data in data.get(EVENT_TYPE_MAP[event_type], []):
+ for event_data in data.get(SUBEVENT_TYPE_MAP[event_type], []):
async_evaluate_event(hass, event_data)
else:
async_evaluate_event(hass, data)
-@callback
def async_evaluate_event(hass, event_data):
"""Evaluate events from webhook."""
event_type = event_data.get(ATTR_EVENT_TYPE)
@@ -65,18 +64,30 @@ def async_evaluate_event(hass, event_data):
async_send_event(hass, event_type, person_event_data)
else:
- _LOGGER.debug("%s: %s", event_type, event_data)
async_send_event(hass, event_type, event_data)
-@callback
def async_send_event(hass, event_type, data):
"""Send events."""
- hass.bus.async_fire(
- event_type=NETATMO_EVENT, event_data={"type": event_type, "data": data}
- )
+ _LOGGER.debug("%s: %s", event_type, data)
async_dispatcher_send(
hass,
f"signal-{DOMAIN}-webhook-{event_type}",
{"type": event_type, "data": data},
)
+
+ event_data = {
+ "type": event_type,
+ "data": data,
+ }
+
+ if event_type in EVENT_ID_MAP:
+ data_device_id = data[EVENT_ID_MAP[event_type]]
+ event_data[ATTR_DEVICE_ID] = hass.data[DOMAIN][DATA_DEVICE_IDS].get(
+ data_device_id
+ )
+
+ hass.bus.async_fire(
+ event_type=NETATMO_EVENT,
+ event_data=event_data,
+ )
diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py
index 64c8d789fc78e2..21e4cd1b00503c 100644
--- a/homeassistant/components/netdata/sensor.py
+++ b/homeassistant/components/netdata/sensor.py
@@ -6,7 +6,7 @@
from netdata.exceptions import NetdataError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_HOST,
CONF_ICON,
@@ -18,7 +18,6 @@
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -97,7 +96,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(dev, True)
-class NetdataSensor(Entity):
+class NetdataSensor(SensorEntity):
"""Implementation of a Netdata sensor."""
def __init__(self, netdata, name, sensor, sensor_name, element, icon, unit, invert):
@@ -146,7 +145,7 @@ async def async_update(self):
)
-class NetdataAlarms(Entity):
+class NetdataAlarms(SensorEntity):
"""Implementation of a Netdata alarm sensor."""
def __init__(self, netdata, name, host, port):
diff --git a/homeassistant/components/netgear_lte/sensor.py b/homeassistant/components/netgear_lte/sensor.py
index abbedf79d64efe..c8f07301e98fa7 100644
--- a/homeassistant/components/netgear_lte/sensor.py
+++ b/homeassistant/components/netgear_lte/sensor.py
@@ -1,5 +1,5 @@
"""Support for Netgear LTE sensors."""
-from homeassistant.components.sensor import DOMAIN
+from homeassistant.components.sensor import DOMAIN, SensorEntity
from homeassistant.exceptions import PlatformNotReady
from . import CONF_MONITORED_CONDITIONS, DATA_KEY, LTEEntity
@@ -33,7 +33,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info)
async_add_entities(sensors)
-class LTESensor(LTEEntity):
+class LTESensor(LTEEntity, SensorEntity):
"""Base LTE sensor entity."""
@property
diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py
index 3c1482844ad4ef..a254d06fc06a26 100644
--- a/homeassistant/components/netio/switch.py
+++ b/homeassistant/components/netio/switch.py
@@ -169,7 +169,7 @@ def update(self):
self.netio.update()
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return optional state attributes."""
return {
ATTR_TOTAL_CONSUMPTION_KWH: self.cumulated_consumption_kwh,
diff --git a/homeassistant/components/neurio_energy/sensor.py b/homeassistant/components/neurio_energy/sensor.py
index 264d7508347b96..2bc17fbecb2565 100644
--- a/homeassistant/components/neurio_energy/sensor.py
+++ b/homeassistant/components/neurio_energy/sensor.py
@@ -6,10 +6,9 @@
import requests.exceptions
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_API_KEY, ENERGY_KILO_WATT_HOUR, POWER_WATT
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
import homeassistant.util.dt as dt_util
@@ -123,7 +122,7 @@ def get_daily_usage(self):
self._daily_usage = round(kwh, 2)
-class NeurioEnergy(Entity):
+class NeurioEnergy(SensorEntity):
"""Implementation of a Neurio energy sensor."""
def __init__(self, data, name, sensor_type, update_call):
diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py
index fc2ef7fef354a6..4dde2084400114 100644
--- a/homeassistant/components/nexia/__init__.py
+++ b/homeassistant/components/nexia/__init__.py
@@ -80,9 +80,9 @@ async def _async_update_data():
UPDATE_COORDINATOR: coordinator,
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -93,8 +93,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py
index 2b3f7de44893ea..aff3711cdaef6f 100644
--- a/homeassistant/components/nexia/climate.py
+++ b/homeassistant/components/nexia/climate.py
@@ -334,12 +334,19 @@ def set_temperature(self, **kwargs):
new_cool_temp = min_temp + deadband
# Check that we're within the deadband range, fix it if we're not
- if new_heat_temp and new_heat_temp != cur_heat_temp:
- if new_cool_temp - new_heat_temp < deadband:
- new_cool_temp = new_heat_temp + deadband
- if new_cool_temp and new_cool_temp != cur_cool_temp:
- if new_cool_temp - new_heat_temp < deadband:
- new_heat_temp = new_cool_temp - deadband
+ if (
+ new_heat_temp
+ and new_heat_temp != cur_heat_temp
+ and new_cool_temp - new_heat_temp < deadband
+ ):
+ new_cool_temp = new_heat_temp + deadband
+
+ if (
+ new_cool_temp
+ and new_cool_temp != cur_cool_temp
+ and new_cool_temp - new_heat_temp < deadband
+ ):
+ new_heat_temp = new_cool_temp - deadband
self._zone.set_heat_cool_temp(
heat_temperature=new_heat_temp,
@@ -354,9 +361,9 @@ def is_aux_heat(self):
return self._thermostat.is_emergency_heat_active()
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
- data = super().device_state_attributes
+ data = super().extra_state_attributes
data[ATTR_ZONE_STATUS] = self._zone.get_status()
diff --git a/homeassistant/components/nexia/config_flow.py b/homeassistant/components/nexia/config_flow.py
index 87850145077f17..e68564706faf2c 100644
--- a/homeassistant/components/nexia/config_flow.py
+++ b/homeassistant/components/nexia/config_flow.py
@@ -8,7 +8,7 @@
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import DOMAIN
from .util import is_invalid_auth_code
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/nexia/entity.py b/homeassistant/components/nexia/entity.py
index 7820ebb6216539..fc69c7ef38980a 100644
--- a/homeassistant/components/nexia/entity.py
+++ b/homeassistant/components/nexia/entity.py
@@ -33,7 +33,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
@@ -83,10 +83,12 @@ def __init__(self, coordinator, zone, name, unique_id):
def device_info(self):
"""Return the device_info of the device."""
data = super().device_info
+ zone_name = self._zone.get_name()
data.update(
{
"identifiers": {(DOMAIN, self._zone.zone_id)},
- "name": self._zone.get_name(),
+ "name": zone_name,
+ "suggested_area": zone_name,
"via_device": (DOMAIN, self._zone.thermostat.thermostat_id),
}
)
diff --git a/homeassistant/components/nexia/scene.py b/homeassistant/components/nexia/scene.py
index d3a6691d59ec11..495a8fb4d3a792 100644
--- a/homeassistant/components/nexia/scene.py
+++ b/homeassistant/components/nexia/scene.py
@@ -41,9 +41,9 @@ def __init__(self, coordinator, automation):
self._automation = automation
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the scene specific state attributes."""
- data = super().device_state_attributes
+ data = super().extra_state_attributes
data[ATTR_DESCRIPTION] = self._automation.description
return data
diff --git a/homeassistant/components/nexia/sensor.py b/homeassistant/components/nexia/sensor.py
index eff15d443bcc7c..a14931e41eeb25 100644
--- a/homeassistant/components/nexia/sensor.py
+++ b/homeassistant/components/nexia/sensor.py
@@ -2,6 +2,7 @@
from nexia.const import UNIT_CELSIUS
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
@@ -149,7 +150,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities, True)
-class NexiaThermostatSensor(NexiaThermostatEntity):
+class NexiaThermostatSensor(NexiaThermostatEntity, SensorEntity):
"""Provides Nexia thermostat sensor support."""
def __init__(
@@ -196,7 +197,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
-class NexiaThermostatZoneSensor(NexiaThermostatZoneEntity):
+class NexiaThermostatZoneSensor(NexiaThermostatZoneEntity, SensorEntity):
"""Nexia Zone Sensor Support."""
def __init__(
diff --git a/homeassistant/components/nexia/translations/de.json b/homeassistant/components/nexia/translations/de.json
index 0ff4da3b2e1dd8..f2220f828e833b 100644
--- a/homeassistant/components/nexia/translations/de.json
+++ b/homeassistant/components/nexia/translations/de.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Dieses Nexia Home ist bereits konfiguriert"
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
diff --git a/homeassistant/components/nexia/translations/he.json b/homeassistant/components/nexia/translations/he.json
new file mode 100644
index 00000000000000..ac90b3264eab33
--- /dev/null
+++ b/homeassistant/components/nexia/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nexia/translations/hu.json b/homeassistant/components/nexia/translations/hu.json
index dee4ed9ee0fa4d..7dedf459484fa1 100644
--- a/homeassistant/components/nexia/translations/hu.json
+++ b/homeassistant/components/nexia/translations/hu.json
@@ -1,11 +1,20 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
"step": {
"user": {
"data": {
"password": "Jelsz\u00f3",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
- }
+ },
+ "title": "Csatlakoz\u00e1s a mynexia.com-hoz"
}
}
}
diff --git a/homeassistant/components/nexia/translations/id.json b/homeassistant/components/nexia/translations/id.json
new file mode 100644
index 00000000000000..e6900bdffa1757
--- /dev/null
+++ b/homeassistant/components/nexia/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "title": "Hubungkan ke mynexia.com"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nexia/translations/ko.json b/homeassistant/components/nexia/translations/ko.json
index a918de60fe63d5..4bd1589e4e8d80 100644
--- a/homeassistant/components/nexia/translations/ko.json
+++ b/homeassistant/components/nexia/translations/ko.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "nexia home \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
@@ -14,7 +14,7 @@
"password": "\ube44\ubc00\ubc88\ud638",
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
},
- "title": "mynexia.com \uc5d0 \uc5f0\uacb0\ud558\uae30"
+ "title": "mynexia.com\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/nexia/translations/nl.json b/homeassistant/components/nexia/translations/nl.json
index d718c78d7af78c..faa19d3b63c05a 100644
--- a/homeassistant/components/nexia/translations/nl.json
+++ b/homeassistant/components/nexia/translations/nl.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Deze nexia-woning is al geconfigureerd"
+ "already_configured": "Apparaat is al geconfigureerd"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
diff --git a/homeassistant/components/nexia/translations/ru.json b/homeassistant/components/nexia/translations/ru.json
index a19d16e3a7e696..74ec08ec2cfef4 100644
--- a/homeassistant/components/nexia/translations/ru.json
+++ b/homeassistant/components/nexia/translations/ru.json
@@ -5,14 +5,14 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a mynexia.com"
}
diff --git a/homeassistant/components/nexia/translations/sv.json b/homeassistant/components/nexia/translations/sv.json
index 9cfd620ac7329c..60044361f658d8 100644
--- a/homeassistant/components/nexia/translations/sv.json
+++ b/homeassistant/components/nexia/translations/sv.json
@@ -10,7 +10,8 @@
"data": {
"password": "L\u00f6senord",
"username": "Anv\u00e4ndarnamn"
- }
+ },
+ "title": "Anslut till mynexia.com"
}
}
}
diff --git a/homeassistant/components/nexia/translations/tr.json b/homeassistant/components/nexia/translations/tr.json
new file mode 100644
index 00000000000000..47f3d931c46fb8
--- /dev/null
+++ b/homeassistant/components/nexia/translations/tr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ },
+ "title": "Mynexia.com'a ba\u011flan\u0131n"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nexia/translations/uk.json b/homeassistant/components/nexia/translations/uk.json
new file mode 100644
index 00000000000000..8cb2aec836a9e3
--- /dev/null
+++ b/homeassistant/components/nexia/translations/uk.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e mynexia.com"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py
index 2b5da2a97fac65..67d0a4a81d7627 100644
--- a/homeassistant/components/nextbus/sensor.py
+++ b/homeassistant/components/nextbus/sensor.py
@@ -5,10 +5,9 @@
from py_nextbus import NextBusClient
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, DEVICE_CLASS_TIMESTAMP
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util.dt import utc_from_timestamp
_LOGGER = logging.getLogger(__name__)
@@ -104,7 +103,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([NextBusDepartureSensor(client, agency, route, stop, name)], True)
-class NextBusDepartureSensor(Entity):
+class NextBusDepartureSensor(SensorEntity):
"""Sensor class that displays upcoming NextBus times.
To function, this requires knowing the agency tag as well as the tags for
@@ -156,7 +155,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return additional state attributes."""
return self._attributes
diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py
index 1a773040980781..efa5b2e2f32cfc 100644
--- a/homeassistant/components/nextcloud/__init__.py
+++ b/homeassistant/components/nextcloud/__init__.py
@@ -17,7 +17,7 @@
_LOGGER = logging.getLogger(__name__)
DOMAIN = "nextcloud"
-NEXTCLOUD_COMPONENTS = ("sensor", "binary_sensor")
+PLATFORMS = ("sensor", "binary_sensor")
SCAN_INTERVAL = timedelta(seconds=60)
# Validate user configuration
@@ -116,8 +116,8 @@ def nextcloud_update(event_time):
# Update sensors on time interval
track_time_interval(hass, nextcloud_update, conf[CONF_SCAN_INTERVAL])
- for component in NEXTCLOUD_COMPONENTS:
- discovery.load_platform(hass, component, DOMAIN, {}, config)
+ for platform in PLATFORMS:
+ discovery.load_platform(hass, platform, DOMAIN, {}, config)
return True
diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py
index e0be9dde69e6a2..5cd02f124e94fb 100644
--- a/homeassistant/components/nextcloud/sensor.py
+++ b/homeassistant/components/nextcloud/sensor.py
@@ -1,5 +1,5 @@
"""Summary data from Nextcoud."""
-from homeassistant.helpers.entity import Entity
+from homeassistant.components.sensor import SensorEntity
from . import DOMAIN, SENSORS
@@ -15,7 +15,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class NextcloudSensor(Entity):
+class NextcloudSensor(SensorEntity):
"""Represents a Nextcloud sensor."""
def __init__(self, item):
diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py
index 4c8ab756ddf42d..dfaaf28048e804 100644
--- a/homeassistant/components/nightscout/__init__.py
+++ b/homeassistant/components/nightscout/__init__.py
@@ -48,9 +48,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
entry_type="service",
)
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -61,8 +61,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/nightscout/config_flow.py b/homeassistant/components/nightscout/config_flow.py
index 3000d652e46f87..2b91395d37733f 100644
--- a/homeassistant/components/nightscout/config_flow.py
+++ b/homeassistant/components/nightscout/config_flow.py
@@ -9,7 +9,7 @@
from homeassistant import config_entries, exceptions
from homeassistant.const import CONF_API_KEY, CONF_URL
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import DOMAIN
from .utils import hash_from_url
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/nightscout/const.py b/homeassistant/components/nightscout/const.py
index 4bb96a94c2971f..7e47f7ff49d4a2 100644
--- a/homeassistant/components/nightscout/const.py
+++ b/homeassistant/components/nightscout/const.py
@@ -3,6 +3,5 @@
DOMAIN = "nightscout"
ATTR_DEVICE = "device"
-ATTR_DATE = "date"
ATTR_DELTA = "delta"
ATTR_DIRECTION = "direction"
diff --git a/homeassistant/components/nightscout/sensor.py b/homeassistant/components/nightscout/sensor.py
index f4ff14d7b2a7ea..ea2ea549cec470 100644
--- a/homeassistant/components/nightscout/sensor.py
+++ b/homeassistant/components/nightscout/sensor.py
@@ -1,17 +1,21 @@
"""Support for Nightscout sensors."""
+from __future__ import annotations
+
from asyncio import TimeoutError as AsyncIOTimeoutError
from datetime import timedelta
import logging
-from typing import Callable, List
+from typing import Callable
from aiohttp import ClientError
from py_nightscout import Api as NightscoutAPI
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import ATTR_DATE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
-from .const import ATTR_DATE, ATTR_DELTA, ATTR_DEVICE, ATTR_DIRECTION, DOMAIN
+from .const import ATTR_DELTA, ATTR_DEVICE, ATTR_DIRECTION, DOMAIN
SCAN_INTERVAL = timedelta(minutes=1)
@@ -23,14 +27,14 @@
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up the Glucose Sensor."""
api = hass.data[DOMAIN][entry.entry_id]
async_add_entities([NightscoutSensor(api, "Blood Sugar", entry.unique_id)], True)
-class NightscoutSensor(Entity):
+class NightscoutSensor(SensorEntity):
"""Implementation of a Nightscout sensor."""
def __init__(self, api: NightscoutAPI, name, unique_id):
@@ -114,6 +118,6 @@ def _parse_icon(self) -> str:
return switcher.get(self._attributes[ATTR_DIRECTION], "mdi:cloud-question")
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attributes
diff --git a/homeassistant/components/nightscout/translations/de.json b/homeassistant/components/nightscout/translations/de.json
index 8581b04099d695..510d57ce45f4c4 100644
--- a/homeassistant/components/nightscout/translations/de.json
+++ b/homeassistant/components/nightscout/translations/de.json
@@ -1,12 +1,18 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
"error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"flow_title": "Nightscout",
"step": {
"user": {
"data": {
+ "api_key": "API-Schl\u00fcssel",
"url": "URL"
}
}
diff --git a/homeassistant/components/nightscout/translations/et.json b/homeassistant/components/nightscout/translations/et.json
index 1e77907f2af652..0d00cebb6a5f8e 100644
--- a/homeassistant/components/nightscout/translations/et.json
+++ b/homeassistant/components/nightscout/translations/et.json
@@ -15,7 +15,7 @@
"api_key": "API v\u00f5ti",
"url": ""
},
- "description": "- URL: NightScout eksemplari aadress. St: https://myhomeassistant.duckdns.org:5423\n - API v\u00f5ti (valikuline): kasutage ainult siis kui teie eksemplar on kaitstud (auth_default_roles! = readable).",
+ "description": "- URL: NightScout eksemplari aadress. St: https://myhomeassistant.duckdns.org:5423\n - API v\u00f5ti (valikuline): kasuta ainult siis kui eksemplar on kaitstud (auth_default_roles! = readable).",
"title": "Sisesta oma Nightscouti serveri teave."
}
}
diff --git a/homeassistant/components/nightscout/translations/hu.json b/homeassistant/components/nightscout/translations/hu.json
index 3b2d79a34a77e2..459a879e82cc63 100644
--- a/homeassistant/components/nightscout/translations/hu.json
+++ b/homeassistant/components/nightscout/translations/hu.json
@@ -2,6 +2,20 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "flow_title": "Nightscout",
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API kulcs",
+ "url": "URL"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nightscout/translations/id.json b/homeassistant/components/nightscout/translations/id.json
new file mode 100644
index 00000000000000..75496084bc4fa2
--- /dev/null
+++ b/homeassistant/components/nightscout/translations/id.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "flow_title": "Nightscout",
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kunci API",
+ "url": "URL"
+ },
+ "description": "- URL: alamat instans nightscout Anda, misalnya https://myhomeassistant.duckdns.org:5423\n- Kunci API (Opsional): Hanya gunakan jika instans Anda dilindungi (auth_default_roles != dapat dibaca).",
+ "title": "Masukkan informasi server Nightscout Anda."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nightscout/translations/ko.json b/homeassistant/components/nightscout/translations/ko.json
index 0235c446e75454..1146e6926e3f46 100644
--- a/homeassistant/components/nightscout/translations/ko.json
+++ b/homeassistant/components/nightscout/translations/ko.json
@@ -4,7 +4,20 @@
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "flow_title": "Nightscout",
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API \ud0a4",
+ "url": "URL \uc8fc\uc18c"
+ },
+ "description": "- URL: Nightscout \uc778\uc2a4\ud134\uc2a4\uc758 \uc8fc\uc18c. \uc608: https://myhomeassistant.duckdns.org:5423\n- API \ud0a4 (\uc120\ud0dd \uc0ac\ud56d): \uc778\uc2a4\ud134\uc2a4\uac00 \ubcf4\ud638\ub41c \uacbd\uc6b0\uc5d0\ub9cc \uc0ac\uc6a9\ud574\uc8fc\uc138\uc694 (auth_default_roles != readable).",
+ "title": "Nightscout \uc11c\ubc84 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694."
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nightscout/translations/nl.json b/homeassistant/components/nightscout/translations/nl.json
index 208299fd4427ff..a9b81e9403e547 100644
--- a/homeassistant/components/nightscout/translations/nl.json
+++ b/homeassistant/components/nightscout/translations/nl.json
@@ -4,15 +4,19 @@
"already_configured": "Apparaat is al geconfigureerd"
},
"error": {
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
+ "flow_title": "Nightscout",
"step": {
"user": {
"data": {
"api_key": "API-sleutel",
"url": "URL"
- }
+ },
+ "description": "- URL: het adres van uw nightscout instantie. Bijv.: https://myhomeassistant.duckdns.org:5423\n- API-sleutel (optioneel): Alleen gebruiken als uw instantie beveiligd is (auth_default_roles != readable).",
+ "title": "Voer uw Nightscout-serverinformatie in."
}
}
}
diff --git a/homeassistant/components/nightscout/translations/no.json b/homeassistant/components/nightscout/translations/no.json
index db7b8f811cae88..d68fe45c684137 100644
--- a/homeassistant/components/nightscout/translations/no.json
+++ b/homeassistant/components/nightscout/translations/no.json
@@ -15,7 +15,7 @@
"api_key": "API-n\u00f8kkel",
"url": "URL"
},
- "description": "- URL: adressen til din nattscout-forekomst. Dvs: https://myhomeassistant.duckdns.org:5423 \n - API-n\u00f8kkel (valgfritt): Bruk bare hvis forekomsten din er beskyttet (auth_default_roles! = Lesbar).",
+ "description": "- URL: Adressen til din nattscout-forekomst. F. Eks: https://myhomeassistant.duckdns.org:5423 \n- API-n\u00f8kkel (valgfritt): Bruk bare hvis forekomsten din er beskyttet (auth_default_roles! = readable).",
"title": "Skriv inn informasjon om Nightscout-serveren."
}
}
diff --git a/homeassistant/components/nightscout/translations/ru.json b/homeassistant/components/nightscout/translations/ru.json
index 738c4dfa9a33d0..c7688973c1bc77 100644
--- a/homeassistant/components/nightscout/translations/ru.json
+++ b/homeassistant/components/nightscout/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"flow_title": "Nightscout",
diff --git a/homeassistant/components/nightscout/translations/tr.json b/homeassistant/components/nightscout/translations/tr.json
index 585aace899de32..95f36a4d124aa0 100644
--- a/homeassistant/components/nightscout/translations/tr.json
+++ b/homeassistant/components/nightscout/translations/tr.json
@@ -1,11 +1,18 @@
{
"config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
"error": {
- "cannot_connect": "Ba\u011flan\u0131lamad\u0131"
+ "cannot_connect": "Ba\u011flan\u0131lamad\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
},
+ "flow_title": "Nightscout",
"step": {
"user": {
"data": {
+ "api_key": "API Anahtar\u0131",
"url": "URL"
}
}
diff --git a/homeassistant/components/nightscout/translations/uk.json b/homeassistant/components/nightscout/translations/uk.json
new file mode 100644
index 00000000000000..6504b00eb883e0
--- /dev/null
+++ b/homeassistant/components/nightscout/translations/uk.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "flow_title": "Nightscout",
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430"
+ },
+ "description": "- URL: \u0430\u0434\u0440\u0435\u0441\u0430 \u0412\u0430\u0448\u043e\u0433\u043e Nightscout. \u041d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: https://myhomeassistant.duckdns.org:5423\n - \u041a\u043b\u044e\u0447 API (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e): \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435, \u043b\u0438\u0448\u0435 \u044f\u043a\u0449\u043e \u0412\u0430\u0448 Nightcout \u0437\u0430\u0445\u0438\u0449\u0435\u043d\u0438\u0439 (auth_default_roles != readable).",
+ "title": "Nightscout"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nilu/air_quality.py b/homeassistant/components/nilu/air_quality.py
index 8e851592de3807..d6fcad3ac7eddd 100644
--- a/homeassistant/components/nilu/air_quality.py
+++ b/homeassistant/components/nilu/air_quality.py
@@ -175,7 +175,7 @@ def attribution(self) -> str:
return ATTRIBUTION
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return other details about the sensor state."""
return self._attrs
diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py
index 26689e5cb0a9f2..24adf2237199a0 100644
--- a/homeassistant/components/nissan_leaf/__init__.py
+++ b/homeassistant/components/nissan_leaf/__init__.py
@@ -7,7 +7,7 @@
from pycarwings2 import CarwingsError, Session
import voluptuous as vol
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_OK
+from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME, HTTP_OK
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
@@ -34,7 +34,6 @@
CONF_INTERVAL = "update_interval"
CONF_CHARGING_INTERVAL = "update_interval_charging"
CONF_CLIMATE_INTERVAL = "update_interval_climate"
-CONF_REGION = "region"
CONF_VALID_REGIONS = ["NNA", "NE", "NCI", "NMA", "NML"]
CONF_FORCE_MILES = "force_miles"
@@ -82,7 +81,7 @@
extra=vol.ALLOW_EXTRA,
)
-LEAF_COMPONENTS = ["sensor", "switch", "binary_sensor"]
+PLATFORMS = ["sensor", "switch", "binary_sensor"]
SIGNAL_UPDATE_LEAF = "nissan_leaf_update"
@@ -95,7 +94,7 @@
def setup(hass, config):
- """Set up the Nissan Leaf component."""
+ """Set up the Nissan Leaf integration."""
async def async_handle_update(service):
"""Handle service to update leaf data from Nissan servers."""
@@ -136,7 +135,7 @@ async def async_handle_start_charge(service):
def setup_leaf(car_config):
"""Set up a car."""
- _LOGGER.debug("Logging into You+Nissan...")
+ _LOGGER.debug("Logging into You+Nissan")
username = car_config[CONF_USERNAME]
password = car_config[CONF_PASSWORD]
@@ -171,8 +170,8 @@ def setup_leaf(car_config):
data_store = LeafDataStore(hass, leaf, car_config)
hass.data[DATA_LEAF][leaf.vin] = data_store
- for component in LEAF_COMPONENTS:
- load_platform(hass, component, DOMAIN, {}, car_config)
+ for platform in PLATFORMS:
+ load_platform(hass, platform, DOMAIN, {}, car_config)
async_track_point_in_utc_time(
hass, data_store.async_update_data, utcnow() + INITIAL_UPDATE
@@ -272,7 +271,6 @@ def get_next_interval(self):
async def async_refresh_data(self, now):
"""Refresh the leaf data and update the datastore."""
-
if self.request_in_progress:
_LOGGER.debug("Refresh currently in progress for %s", self.leaf.nickname)
return
@@ -336,7 +334,6 @@ def _extract_start_date(battery_info):
async def async_get_battery(self):
"""Request battery update from Nissan servers."""
-
try:
# Request battery update from the car
_LOGGER.debug("Requesting battery update, %s", self.leaf.vin)
@@ -388,7 +385,6 @@ async def async_get_battery(self):
async def async_get_climate(self):
"""Request climate data from Nissan servers."""
-
try:
return await self.hass.async_add_executor_job(
self.leaf.get_latest_hvac_status
@@ -454,7 +450,7 @@ def log_registration(self):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return default attributes for Nissan leaf entities."""
return {
"next_update": self.car.next_update,
diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py
index 368db17ab4b95a..936d607a84ee23 100644
--- a/homeassistant/components/nissan_leaf/sensor.py
+++ b/homeassistant/components/nissan_leaf/sensor.py
@@ -1,6 +1,7 @@
"""Battery Charge and Range Support for the Nissan Leaf."""
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE
from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.util.distance import LENGTH_KILOMETERS, LENGTH_MILES
@@ -35,7 +36,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(devices, True)
-class LeafBatterySensor(LeafEntity):
+class LeafBatterySensor(LeafEntity, SensorEntity):
"""Nissan Leaf Battery Sensor."""
@property
@@ -65,7 +66,7 @@ def icon(self):
return icon_for_battery_level(battery_level=self.state, charging=chargestate)
-class LeafRangeSensor(LeafEntity):
+class LeafRangeSensor(LeafEntity, SensorEntity):
"""Nissan Leaf Range Sensor."""
def __init__(self, car, ac_on):
diff --git a/homeassistant/components/nissan_leaf/switch.py b/homeassistant/components/nissan_leaf/switch.py
index d95d3e4ed39c23..2b8d557c2dd56b 100644
--- a/homeassistant/components/nissan_leaf/switch.py
+++ b/homeassistant/components/nissan_leaf/switch.py
@@ -37,9 +37,9 @@ def log_registration(self):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return climate control attributes."""
- attrs = super().device_state_attributes
+ attrs = super().extra_state_attributes
attrs["updated_on"] = self.car.last_climate_response
return attrs
diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py
index 0a8c177b08cf48..69c65873e513ea 100644
--- a/homeassistant/components/nmap_tracker/device_tracker.py
+++ b/homeassistant/components/nmap_tracker/device_tracker.py
@@ -12,13 +12,12 @@
PLATFORM_SCHEMA,
DeviceScanner,
)
-from homeassistant.const import CONF_HOSTS
+from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
-CONF_EXCLUDE = "exclude"
# Interval in minutes to exclude devices from a scan while they are home
CONF_HOME_INTERVAL = "home_interval"
CONF_OPTIONS = "scan_options"
@@ -90,7 +89,7 @@ def _update_info(self):
Returns boolean if scanning successful.
"""
- _LOGGER.debug("Scanning...")
+ _LOGGER.debug("Scanning")
scanner = PortScanner()
diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py
index bdf4658434c4d1..32e4fd87e2917f 100644
--- a/homeassistant/components/nmbs/sensor.py
+++ b/homeassistant/components/nmbs/sensor.py
@@ -4,7 +4,7 @@
from pyrail import iRail
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_LATITUDE,
@@ -14,7 +14,6 @@
TIME_MINUTES,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -88,7 +87,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class NMBSLiveBoard(Entity):
+class NMBSLiveBoard(SensorEntity):
"""Get the next train from a station's liveboard."""
def __init__(self, api_client, live_station, station_from, station_to):
@@ -126,7 +125,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the sensor attributes if data is available."""
if self._state is None or not self._attrs:
return None
@@ -164,7 +163,7 @@ def update(self):
)
-class NMBSSensor(Entity):
+class NMBSSensor(SensorEntity):
"""Get the the total travel time for a given connection."""
def __init__(
@@ -202,7 +201,7 @@ def icon(self):
return "mdi:train"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return sensor attributes if data is available."""
if self._state is None or not self._attrs:
return None
diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py
index a0453e3acb1883..e637e9531733bc 100644
--- a/homeassistant/components/noaa_tides/sensor.py
+++ b/homeassistant/components/noaa_tides/sensor.py
@@ -2,11 +2,11 @@
from datetime import datetime, timedelta
import logging
-import noaa_coops as coops # pylint: disable=import-error
+import noaa_coops as coops
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_NAME,
@@ -15,7 +15,6 @@
)
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -72,7 +71,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([noaa_sensor], True)
-class NOAATidesAndCurrentsSensor(Entity):
+class NOAATidesAndCurrentsSensor(SensorEntity):
"""Representation of a NOAA Tides and Currents sensor."""
def __init__(self, name, station_id, timezone, unit_system, station):
@@ -90,7 +89,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of this device."""
attr = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
if self.data is None:
diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py
index 8e6c13260e5b8d..788f900ef70be6 100644
--- a/homeassistant/components/norway_air/air_quality.py
+++ b/homeassistant/components/norway_air/air_quality.py
@@ -80,7 +80,7 @@ def attribution(self) -> str:
return ATTRIBUTION
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return other details about the sensor state."""
return {
"level": self._api.data.get("level"),
diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py
index d3439baf4fb403..118579fb0c0c7b 100644
--- a/homeassistant/components/notify/__init__.py
+++ b/homeassistant/components/notify/__init__.py
@@ -1,21 +1,24 @@
"""Provides functionality to notify people."""
+from __future__ import annotations
+
import asyncio
from functools import partial
import logging
-from typing import Any, Dict, Optional
+from typing import Any, cast
import voluptuous as vol
import homeassistant.components.persistent_notification as pn
-from homeassistant.const import CONF_NAME, CONF_PLATFORM
-from homeassistant.core import ServiceCall
+from homeassistant.const import CONF_DESCRIPTION, CONF_NAME, CONF_PLATFORM
+from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform, discovery
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.typing import HomeAssistantType
-from homeassistant.loader import bind_hass
-from homeassistant.setup import async_prepare_setup_platform
+from homeassistant.helpers.service import async_set_service_schema
+from homeassistant.loader import async_get_integration, bind_hass
+from homeassistant.setup import async_prepare_setup_platform, async_start_setup
from homeassistant.util import slugify
+from homeassistant.util.yaml import load_yaml
# mypy: allow-untyped-defs, no-check-untyped-defs
@@ -41,6 +44,8 @@
NOTIFY_SERVICES = "notify_services"
+CONF_FIELDS = "fields"
+
PLATFORM_SCHEMA = vol.Schema(
{vol.Required(CONF_PLATFORM): cv.string, vol.Optional(CONF_NAME): cv.string},
extra=vol.ALLOW_EXTRA,
@@ -64,7 +69,7 @@
@bind_hass
-async def async_reload(hass: HomeAssistantType, integration_name: str) -> None:
+async def async_reload(hass: HomeAssistant, integration_name: str) -> None:
"""Register notify services for an integration."""
if not _async_integration_has_notify_services(hass, integration_name):
return
@@ -78,7 +83,7 @@ async def async_reload(hass: HomeAssistantType, integration_name: str) -> None:
@bind_hass
-async def async_reset_platform(hass: HomeAssistantType, integration_name: str) -> None:
+async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None:
"""Unregister notify services for an integration."""
if not _async_integration_has_notify_services(hass, integration_name):
return
@@ -94,7 +99,7 @@ async def async_reset_platform(hass: HomeAssistantType, integration_name: str) -
def _async_integration_has_notify_services(
- hass: HomeAssistantType, integration_name: str
+ hass: HomeAssistant, integration_name: str
) -> bool:
"""Determine if an integration has notify services registered."""
if (
@@ -109,9 +114,13 @@ def _async_integration_has_notify_services(
class BaseNotificationService:
"""An abstract class for notification services."""
- hass: Optional[HomeAssistantType] = None
+ # While not purely typed, it makes typehinting more useful for us
+ # and removes the need for constant None checks or asserts.
+ # Ignore types: https://github.com/PyCQA/pylint/issues/3167
+ hass: HomeAssistant = None # type: ignore
+
# Name => target
- registered_targets: Dict[str, str]
+ registered_targets: dict[str, str]
def send_message(self, message, **kwargs):
"""Send a message.
@@ -125,7 +134,9 @@ async def async_send_message(self, message: Any, **kwargs: Any) -> None:
kwargs can contain ATTR_TITLE to specify a title.
"""
- await self.hass.async_add_executor_job(partial(self.send_message, message, **kwargs)) # type: ignore
+ await self.hass.async_add_executor_job(
+ partial(self.send_message, message, **kwargs)
+ )
async def _async_notify_message_service(self, service: ServiceCall) -> None:
"""Handle sending notification message service calls."""
@@ -150,7 +161,7 @@ async def _async_notify_message_service(self, service: ServiceCall) -> None:
async def async_setup(
self,
- hass: HomeAssistantType,
+ hass: HomeAssistant,
service_name: str,
target_service_name_prefix: str,
) -> None:
@@ -161,10 +172,15 @@ async def async_setup(
self._target_service_name_prefix = target_service_name_prefix
self.registered_targets = {}
+ # Load service descriptions from notify/services.yaml
+ integration = await async_get_integration(hass, DOMAIN)
+ services_yaml = integration.file_path / "services.yaml"
+ self.services_dict = cast(
+ dict, await hass.async_add_executor_job(load_yaml, str(services_yaml))
+ )
+
async def async_register_services(self) -> None:
"""Create or update the notify services."""
- assert self.hass
-
if hasattr(self, "targets"):
stale_targets = set(self.registered_targets)
@@ -185,6 +201,13 @@ async def async_register_services(self) -> None:
self._async_notify_message_service,
schema=NOTIFY_SERVICE_SCHEMA,
)
+ # Register the service description
+ service_desc = {
+ CONF_NAME: f"Send a notification via {target_name}",
+ CONF_DESCRIPTION: f"Sends a notification message using the {target_name} integration.",
+ CONF_FIELDS: self.services_dict[SERVICE_NOTIFY][CONF_FIELDS],
+ }
+ async_set_service_schema(self.hass, DOMAIN, target_name, service_desc)
for stale_target_name in stale_targets:
del self.registered_targets[stale_target_name]
@@ -203,10 +226,16 @@ async def async_register_services(self) -> None:
schema=NOTIFY_SERVICE_SCHEMA,
)
+ # Register the service description
+ service_desc = {
+ CONF_NAME: f"Send a notification with {self._service_name}",
+ CONF_DESCRIPTION: f"Sends a notification message using the {self._service_name} service.",
+ CONF_FIELDS: self.services_dict[SERVICE_NOTIFY][CONF_FIELDS],
+ }
+ async_set_service_schema(self.hass, DOMAIN, self._service_name, service_desc)
+
async def async_unregister_services(self) -> None:
"""Unregister the notify services."""
- assert self.hass
-
if self.registered_targets:
remove_targets = set(self.registered_targets)
for remove_target_name in remove_targets:
@@ -260,47 +289,52 @@ async def async_setup_platform(
_LOGGER.error("Unknown notification service specified")
return
- _LOGGER.info("Setting up %s.%s", DOMAIN, integration_name)
- notify_service = None
- try:
- if hasattr(platform, "async_get_service"):
- notify_service = await platform.async_get_service(
- hass, p_config, discovery_info
- )
- elif hasattr(platform, "get_service"):
- notify_service = await hass.async_add_executor_job(
- platform.get_service, hass, p_config, discovery_info
- )
- else:
- raise HomeAssistantError("Invalid notify platform.")
-
- if notify_service is None:
- # Platforms can decide not to create a service based
- # on discovery data.
- if discovery_info is None:
- _LOGGER.error(
- "Failed to initialize notification service %s", integration_name
+ full_name = f"{DOMAIN}.{integration_name}"
+ _LOGGER.info("Setting up %s", full_name)
+ with async_start_setup(hass, [full_name]):
+ notify_service = None
+ try:
+ if hasattr(platform, "async_get_service"):
+ notify_service = await platform.async_get_service(
+ hass, p_config, discovery_info
+ )
+ elif hasattr(platform, "get_service"):
+ notify_service = await hass.async_add_executor_job(
+ platform.get_service, hass, p_config, discovery_info
)
+ else:
+ raise HomeAssistantError("Invalid notify platform.")
+
+ if notify_service is None:
+ # Platforms can decide not to create a service based
+ # on discovery data.
+ if discovery_info is None:
+ _LOGGER.error(
+ "Failed to initialize notification service %s",
+ integration_name,
+ )
+ return
+
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Error setting up platform %s", integration_name)
return
- except Exception: # pylint: disable=broad-except
- _LOGGER.exception("Error setting up platform %s", integration_name)
- return
-
- if discovery_info is None:
- discovery_info = {}
+ if discovery_info is None:
+ discovery_info = {}
- conf_name = p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME)
- target_service_name_prefix = conf_name or integration_name
- service_name = slugify(conf_name or SERVICE_NOTIFY)
+ conf_name = p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME)
+ target_service_name_prefix = conf_name or integration_name
+ service_name = slugify(conf_name or SERVICE_NOTIFY)
- await notify_service.async_setup(hass, service_name, target_service_name_prefix)
- await notify_service.async_register_services()
+ await notify_service.async_setup(
+ hass, service_name, target_service_name_prefix
+ )
+ await notify_service.async_register_services()
- hass.data[NOTIFY_SERVICES].setdefault(integration_name, []).append(
- notify_service
- )
- hass.config.components.add(f"{DOMAIN}.{integration_name}")
+ hass.data[NOTIFY_SERVICES].setdefault(integration_name, []).append(
+ notify_service
+ )
+ hass.config.components.add(f"{DOMAIN}.{integration_name}")
return True
@@ -312,7 +346,7 @@ async def async_setup_platform(
)
setup_tasks = [
- async_setup_platform(integration_name, p_config)
+ asyncio.create_task(async_setup_platform(integration_name, p_config))
for integration_name, p_config in config_per_platform(config, DOMAIN)
]
diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml
index 8c75c94e34ab37..f6918b6c09c6b9 100644
--- a/homeassistant/components/notify/services.yaml
+++ b/homeassistant/components/notify/services.yaml
@@ -1,22 +1,37 @@
# Describes the format for available notification services
notify:
- description: Send a notification.
+ name: Send a notification
+ description: Sends a notification message to selected notify platforms.
fields:
message:
+ name: Message
description: Message body of the notification.
example: The garage door has been open for 10 minutes.
+ selector:
+ text:
title:
+ name: Title
description: Optional title for your notification.
example: "Your Garage Door Friend"
+ selector:
+ text:
target:
- description: An array of targets to send the notification to. Optional depending on the platform.
+ description:
+ An array of targets to send the notification to. Optional depending on
+ the platform.
example: platform specific
data:
- description: Extended information for notification. Optional depending on the platform.
+ name: Data
+ description:
+ Extended information for notification. Optional depending on the
+ platform.
example: platform specific
+ selector:
+ object:
persistent_notification:
+ name: Send a persistent notification
description: Sends a notification to the visible in the front-end.
fields:
message:
@@ -27,10 +42,16 @@ persistent_notification:
example: "Your Garage Door Friend"
apns_register:
- description: Registers a device to receive push notifications.
+ name: Register APNS device
+ description:
+ Registers a device to receive push notifications via APNS (Apple Push
+ Notification Service).
fields:
push_id:
- description: The device token, a 64 character hex string (256 bits). The device token is provided to you by your client app, which receives the token after registering itself with the remote notification service.
+ description:
+ The device token, a 64 character hex string (256 bits). The device token
+ is provided to you by your client app, which receives the token after
+ registering itself with the remote notification service.
example: "72f2a8633655c5ce574fdc9b2b34ff8abdfc3b739b6ceb7a9ff06c1cbbf99f62"
name:
description: A friendly name for the device (optional).
diff --git a/homeassistant/components/notify/translations/hu.json b/homeassistant/components/notify/translations/hu.json
index b5c88047f66c42..18413724b53e7e 100644
--- a/homeassistant/components/notify/translations/hu.json
+++ b/homeassistant/components/notify/translations/hu.json
@@ -1,3 +1,3 @@
{
- "title": "\u00c9rtes\u00edt"
+ "title": "\u00c9rtes\u00edt\u00e9sek"
}
\ No newline at end of file
diff --git a/homeassistant/components/notify/translations/id.json b/homeassistant/components/notify/translations/id.json
index 723b49fe6af4db..0ee3a77f9c13da 100644
--- a/homeassistant/components/notify/translations/id.json
+++ b/homeassistant/components/notify/translations/id.json
@@ -1,3 +1,3 @@
{
- "title": "Pemberitahuan"
+ "title": "Notifikasi"
}
\ No newline at end of file
diff --git a/homeassistant/components/notify/translations/nl.json b/homeassistant/components/notify/translations/nl.json
index 409692f72277eb..52a24cc9efdd85 100644
--- a/homeassistant/components/notify/translations/nl.json
+++ b/homeassistant/components/notify/translations/nl.json
@@ -1,3 +1,3 @@
{
- "title": "Notificeer"
+ "title": "Meldingen"
}
\ No newline at end of file
diff --git a/homeassistant/components/notify/translations/uk.json b/homeassistant/components/notify/translations/uk.json
index 86821a3e50f7f3..d87752255d58c1 100644
--- a/homeassistant/components/notify/translations/uk.json
+++ b/homeassistant/components/notify/translations/uk.json
@@ -1,3 +1,3 @@
{
- "title": "\u041f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f"
+ "title": "\u0421\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u043d\u044f"
}
\ No newline at end of file
diff --git a/homeassistant/components/notify_events/notify.py b/homeassistant/components/notify_events/notify.py
index 23df01a128b78c..ce7c353badb3ab 100644
--- a/homeassistant/components/notify_events/notify.py
+++ b/homeassistant/components/notify_events/notify.py
@@ -28,6 +28,8 @@
ATTR_FILE_KIND_FILE = "file"
ATTR_FILE_KIND_IMAGE = "image"
+ATTR_TOKEN = "token"
+
_LOGGER = logging.getLogger(__name__)
@@ -114,7 +116,12 @@ def prepare_message(self, message, data) -> Message:
def send_message(self, message, **kwargs):
"""Send a message."""
+ token = self.token
data = kwargs.get(ATTR_DATA) or {}
msg = self.prepare_message(message, data)
- msg.send(self.token)
+
+ if data.get(ATTR_TOKEN, "").trim():
+ token = data[ATTR_TOKEN]
+
+ msg.send(token)
diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py
index c2cbdb85289fb0..ca0ccf08c89749 100644
--- a/homeassistant/components/notion/__init__.py
+++ b/homeassistant/components/notion/__init__.py
@@ -97,7 +97,7 @@ async def async_update():
update_method=async_update,
)
- await coordinator.async_refresh()
+ await coordinator.async_config_entry_first_refresh()
for platform in PLATFORMS:
hass.async_create_task(
@@ -180,7 +180,7 @@ def device_class(self) -> str:
return self._device_class
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return the state attributes."""
return self._attrs
diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py
index 86c51e3c13c81f..425357c3105c34 100644
--- a/homeassistant/components/notion/config_flow.py
+++ b/homeassistant/components/notion/config_flow.py
@@ -7,7 +7,7 @@
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import aiohttp_client
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import DOMAIN
class NotionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py
index 978e0aac46abf5..4f034408fe2d99 100644
--- a/homeassistant/components/notion/sensor.py
+++ b/homeassistant/components/notion/sensor.py
@@ -1,6 +1,7 @@
"""Support for Notion sensors."""
from typing import Callable
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import TEMP_CELSIUS
from homeassistant.core import HomeAssistant, callback
@@ -42,7 +43,7 @@ async def async_setup_entry(
async_add_entities(sensor_list)
-class NotionSensor(NotionEntity):
+class NotionSensor(NotionEntity, SensorEntity):
"""Define a Notion sensor."""
def __init__(
diff --git a/homeassistant/components/notion/translations/de.json b/homeassistant/components/notion/translations/de.json
index f322826c45b2ab..0b421911aa7dea 100644
--- a/homeassistant/components/notion/translations/de.json
+++ b/homeassistant/components/notion/translations/de.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Dieser Benutzername wird bereits benutzt."
+ "already_configured": "Konto wurde bereits konfiguriert"
},
"error": {
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"no_devices": "Keine Ger\u00e4te im Konto gefunden"
},
"step": {
diff --git a/homeassistant/components/notion/translations/he.json b/homeassistant/components/notion/translations/he.json
new file mode 100644
index 00000000000000..3007c0e968c1dc
--- /dev/null
+++ b/homeassistant/components/notion/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/notion/translations/hu.json b/homeassistant/components/notion/translations/hu.json
index 6bf20925535f06..b4d57f83bb3e54 100644
--- a/homeassistant/components/notion/translations/hu.json
+++ b/homeassistant/components/notion/translations/hu.json
@@ -1,6 +1,10 @@
{
"config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"no_devices": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a fi\u00f3kban"
},
"step": {
diff --git a/homeassistant/components/notion/translations/id.json b/homeassistant/components/notion/translations/id.json
new file mode 100644
index 00000000000000..35ee7a295442ee
--- /dev/null
+++ b/homeassistant/components/notion/translations/id.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi"
+ },
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid",
+ "no_devices": "Tidak ada perangkat yang ditemukan di akun"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "title": "Isi informasi Anda"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/notion/translations/ko.json b/homeassistant/components/notion/translations/ko.json
index 323ea126445223..b5c7cadbe9b3a5 100644
--- a/homeassistant/components/notion/translations/ko.json
+++ b/homeassistant/components/notion/translations/ko.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4."
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"no_devices": "\uacc4\uc815\uc5d0 \ub4f1\ub85d\ub41c \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4"
},
"step": {
diff --git a/homeassistant/components/notion/translations/nl.json b/homeassistant/components/notion/translations/nl.json
index 4b6597725efa92..acb42046c9053e 100644
--- a/homeassistant/components/notion/translations/nl.json
+++ b/homeassistant/components/notion/translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Deze gebruikersnaam is al in gebruik."
+ "already_configured": "Account is al geconfigureerd"
},
"error": {
"invalid_auth": "Ongeldige authenticatie",
@@ -11,7 +11,7 @@
"user": {
"data": {
"password": "Wachtwoord",
- "username": "Gebruikersnaam/E-mailadres"
+ "username": "Gebruikersnaam"
},
"title": "Vul uw gegevens informatie"
}
diff --git a/homeassistant/components/notion/translations/ru.json b/homeassistant/components/notion/translations/ru.json
index 678eff742b513d..4b9a45bbf3f8d2 100644
--- a/homeassistant/components/notion/translations/ru.json
+++ b/homeassistant/components/notion/translations/ru.json
@@ -4,14 +4,14 @@
"already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant."
},
"error": {
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e."
},
"step": {
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "Notion"
}
diff --git a/homeassistant/components/notion/translations/tr.json b/homeassistant/components/notion/translations/tr.json
index 8966b79df1b5c4..f89e3fb75338dd 100644
--- a/homeassistant/components/notion/translations/tr.json
+++ b/homeassistant/components/notion/translations/tr.json
@@ -1,7 +1,19 @@
{
"config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
"error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
"no_devices": "Hesapta cihaz bulunamad\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/notion/translations/uk.json b/homeassistant/components/notion/translations/uk.json
new file mode 100644
index 00000000000000..6dc969c3609592
--- /dev/null
+++ b/homeassistant/components/notion/translations/uk.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "no_devices": "\u041d\u0435\u043c\u0430\u0454 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432, \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0438\u0445 \u0437 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u043c \u0437\u0430\u043f\u0438\u0441\u043e\u043c."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "Notion"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py
index b6c0d1a5d9bd0c..6c8061294e9fa9 100644
--- a/homeassistant/components/nsw_fuel_station/sensor.py
+++ b/homeassistant/components/nsw_fuel_station/sensor.py
@@ -1,15 +1,15 @@
"""Sensor platform to display the current fuel prices at a NSW fuel station."""
+from __future__ import annotations
+
import datetime
import logging
-from typing import Optional
from nsw_fuel import FuelCheckClient, FuelCheckError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CURRENCY_CENT, VOLUME_LITERS
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -145,7 +145,7 @@ def get_station_name(self) -> str:
return self._station_name
-class StationPriceSensor(Entity):
+class StationPriceSensor(SensorEntity):
"""Implementation of a sensor that reports the fuel price for a station."""
def __init__(self, station_data: StationPriceData, fuel_type: str):
@@ -159,7 +159,7 @@ def name(self) -> str:
return f"{self._station_data.get_station_name()} {self._fuel_type}"
@property
- def state(self) -> Optional[float]:
+ def state(self) -> float | None:
"""Return the state of the sensor."""
price_info = self._station_data.for_fuel_type(self._fuel_type)
if price_info:
@@ -168,7 +168,7 @@ def state(self) -> Optional[float]:
return None
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return the state attributes of the device."""
return {
ATTR_STATION_ID: self._station_data.station_id,
diff --git a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py
index c8eda3690efbc4..08e62e6c6a3768 100644
--- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py
+++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py
@@ -1,7 +1,8 @@
"""Support for NSW Rural Fire Service Feeds."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Optional
from aio_geojson_nsw_rfs_incidents import NswRuralFireServiceIncidentsFeedManager
import voluptuous as vol
@@ -210,7 +211,7 @@ async def async_will_remove_from_hass(self) -> None:
@callback
def _delete_callback(self):
"""Remove this entity."""
- self.hass.async_create_task(self.async_remove())
+ self.hass.async_create_task(self.async_remove(force_remove=True))
@callback
def _update_callback(self):
@@ -259,22 +260,22 @@ def source(self) -> str:
return SOURCE
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the entity."""
return self._name
@property
- def distance(self) -> Optional[float]:
+ def distance(self) -> float | None:
"""Return distance value of this external event."""
return self._distance
@property
- def latitude(self) -> Optional[float]:
+ def latitude(self) -> float | None:
"""Return latitude value of this external event."""
return self._latitude
@property
- def longitude(self) -> Optional[float]:
+ def longitude(self) -> float | None:
"""Return longitude value of this external event."""
return self._longitude
@@ -284,7 +285,7 @@ def unit_of_measurement(self):
return LENGTH_KILOMETERS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
for key, value in (
diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py
index 2f51ae377a5200..9fe4764e1afecd 100644
--- a/homeassistant/components/nuheat/__init__.py
+++ b/homeassistant/components/nuheat/__init__.py
@@ -80,9 +80,9 @@ async def _async_update_data():
hass.data[DOMAIN][entry.entry_id] = (thermostat, coordinator)
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -93,8 +93,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py
index e8f21fc89c2c87..35000dd21fac79 100644
--- a/homeassistant/components/nuheat/climate.py
+++ b/homeassistant/components/nuheat/climate.py
@@ -291,4 +291,5 @@ def device_info(self):
"name": self._thermostat.room,
"model": "nVent Signature",
"manufacturer": MANUFACTURER,
+ "suggested_area": self._thermostat.room,
}
diff --git a/homeassistant/components/nuheat/config_flow.py b/homeassistant/components/nuheat/config_flow.py
index 40de3844620a03..2cbe1105e97e9a 100644
--- a/homeassistant/components/nuheat/config_flow.py
+++ b/homeassistant/components/nuheat/config_flow.py
@@ -13,8 +13,7 @@
HTTP_INTERNAL_SERVER_ERROR,
)
-from .const import CONF_SERIAL_NUMBER
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import CONF_SERIAL_NUMBER, DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/nuheat/translations/de.json b/homeassistant/components/nuheat/translations/de.json
index 52c30681efcad4..8599f7fe1b5a99 100644
--- a/homeassistant/components/nuheat/translations/de.json
+++ b/homeassistant/components/nuheat/translations/de.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Der Thermostat ist bereits konfiguriert"
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"invalid_thermostat": "Die Seriennummer des Thermostats ist ung\u00fcltig.",
"unknown": "Unerwarteter Fehler"
diff --git a/homeassistant/components/nuheat/translations/he.json b/homeassistant/components/nuheat/translations/he.json
new file mode 100644
index 00000000000000..ac90b3264eab33
--- /dev/null
+++ b/homeassistant/components/nuheat/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuheat/translations/hu.json b/homeassistant/components/nuheat/translations/hu.json
index 8c523b72e04ce1..e6e7174e325229 100644
--- a/homeassistant/components/nuheat/translations/hu.json
+++ b/homeassistant/components/nuheat/translations/hu.json
@@ -1,8 +1,13 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
- "cannot_connect": "A csatlakoz\u00e1s nem siker\u00fclt, pr\u00f3b\u00e1lkozzon \u00fajra",
- "invalid_thermostat": "A termoszt\u00e1t sorozatsz\u00e1ma \u00e9rv\u00e9nytelen."
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "invalid_thermostat": "A termoszt\u00e1t sorozatsz\u00e1ma \u00e9rv\u00e9nytelen.",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"step": {
"user": {
diff --git a/homeassistant/components/nuheat/translations/id.json b/homeassistant/components/nuheat/translations/id.json
new file mode 100644
index 00000000000000..69041c7755d40c
--- /dev/null
+++ b/homeassistant/components/nuheat/translations/id.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "invalid_thermostat": "Nomor seri termostat tidak valid.",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "serial_number": "Nomor seri termostat.",
+ "username": "Nama Pengguna"
+ },
+ "description": "Anda harus mendapatkan nomor seri atau ID numerik termostat Anda dengan masuk ke https://MyNuHeat.com dan memilih termostat Anda.",
+ "title": "Hubungkan ke NuHeat"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuheat/translations/ko.json b/homeassistant/components/nuheat/translations/ko.json
index 1476e8beb0dada..b926d1f5269ee4 100644
--- a/homeassistant/components/nuheat/translations/ko.json
+++ b/homeassistant/components/nuheat/translations/ko.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\uc628\ub3c4 \uc870\uc808\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"invalid_thermostat": "\uc628\ub3c4 \uc870\uc808\uae30\uc758 \uc2dc\ub9ac\uc5bc \ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
@@ -16,8 +16,8 @@
"serial_number": "\uc628\ub3c4 \uc870\uc808\uae30\uc758 \uc2dc\ub9ac\uc5bc \ubc88\ud638",
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
},
- "description": "https://MyNuHeat.com \uc5d0 \ub85c\uadf8\uc778\ud558\uace0 \uc628\ub3c4 \uc870\uc808\uae30\ub97c \uc120\ud0dd\ud558\uc5ec \uc628\ub3c4 \uc870\uc808\uae30\uc758 \uc2dc\ub9ac\uc5bc \ubc88\ud638 \ub610\ub294 \ub610\ub294 ID \ub97c \uc5bb\uc5b4\uc57c \ud569\ub2c8\ub2e4.",
- "title": "NuHeat \uc5d0 \uc5f0\uacb0\ud558\uae30"
+ "description": "https://MyNuHeat.com \uc5d0 \ub85c\uadf8\uc778\ud558\uace0 \uc628\ub3c4 \uc870\uc808\uae30\ub97c \uc120\ud0dd\ud558\uc5ec \uc628\ub3c4 \uc870\uc808\uae30\uc758 \uc2dc\ub9ac\uc5bc \ubc88\ud638 \ub610\ub294 \ub610\ub294 ID\ub97c \uc5bb\uc5b4\uc57c \ud569\ub2c8\ub2e4.",
+ "title": "NuHeat\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/nuheat/translations/nl.json b/homeassistant/components/nuheat/translations/nl.json
index edf3ad17ff45c4..d7672832db051b 100644
--- a/homeassistant/components/nuheat/translations/nl.json
+++ b/homeassistant/components/nuheat/translations/nl.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "De thermostaat is al geconfigureerd"
+ "already_configured": "Apparaat is al geconfigureerd"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"invalid_thermostat": "Het serienummer van de thermostaat is ongeldig.",
"unknown": "Onverwachte fout"
diff --git a/homeassistant/components/nuheat/translations/ru.json b/homeassistant/components/nuheat/translations/ru.json
index 09e74c0e4cbf8a..7c90b8615640e0 100644
--- a/homeassistant/components/nuheat/translations/ru.json
+++ b/homeassistant/components/nuheat/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"invalid_thermostat": "\u0421\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
@@ -14,7 +14,7 @@
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"serial_number": "\u0421\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u0438\u043b\u0438 ID \u0412\u0430\u0448\u0435\u0433\u043e \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430, \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 https://MyNuHeat.com.",
"title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443"
diff --git a/homeassistant/components/nuheat/translations/sv.json b/homeassistant/components/nuheat/translations/sv.json
index 9cfd620ac7329c..327bdf8c4caa6f 100644
--- a/homeassistant/components/nuheat/translations/sv.json
+++ b/homeassistant/components/nuheat/translations/sv.json
@@ -3,14 +3,17 @@
"error": {
"cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen",
"invalid_auth": "Ogiltig autentisering",
+ "invalid_thermostat": "Termostatens serienummer \u00e4r ogiltigt.",
"unknown": "Ov\u00e4ntat fel"
},
"step": {
"user": {
"data": {
"password": "L\u00f6senord",
+ "serial_number": "Termostatens serienummer.",
"username": "Anv\u00e4ndarnamn"
- }
+ },
+ "description": "F\u00e5 tillg\u00e5ng till din termostats serienummer eller ID genom att logga in p\u00e5 https://MyNuHeat.com och v\u00e4lja din termostat."
}
}
}
diff --git a/homeassistant/components/nuheat/translations/tr.json b/homeassistant/components/nuheat/translations/tr.json
new file mode 100644
index 00000000000000..5123f1c7d9af15
--- /dev/null
+++ b/homeassistant/components/nuheat/translations/tr.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "invalid_thermostat": "Termostat seri numaras\u0131 ge\u00e7ersiz.",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "serial_number": "Termostat\u0131n seri numaras\u0131.",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuheat/translations/uk.json b/homeassistant/components/nuheat/translations/uk.json
new file mode 100644
index 00000000000000..21be3968eb733d
--- /dev/null
+++ b/homeassistant/components/nuheat/translations/uk.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "invalid_thermostat": "\u0421\u0435\u0440\u0456\u0439\u043d\u0438\u0439 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430 \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "serial_number": "\u0421\u0435\u0440\u0456\u0439\u043d\u0438\u0439 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u0439 \u043d\u043e\u043c\u0435\u0440 \u0430\u0431\u043e ID \u0412\u0430\u0448\u043e\u0433\u043e \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430, \u043d\u0430 \u0441\u0430\u0439\u0442\u0456 https://MyNuHeat.com.",
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuimo_controller/__init__.py b/homeassistant/components/nuimo_controller/__init__.py
deleted file mode 100644
index 013c2caf23d6c4..00000000000000
--- a/homeassistant/components/nuimo_controller/__init__.py
+++ /dev/null
@@ -1,190 +0,0 @@
-"""Support for Nuimo device over Bluetooth LE."""
-import logging
-import threading
-import time
-
-# pylint: disable=import-error
-from nuimo import NuimoController, NuimoDiscoveryManager
-import voluptuous as vol
-
-from homeassistant.const import CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP
-import homeassistant.helpers.config_validation as cv
-
-_LOGGER = logging.getLogger(__name__)
-
-DOMAIN = "nuimo_controller"
-EVENT_NUIMO = "nuimo_input"
-
-DEFAULT_NAME = "None"
-
-CONFIG_SCHEMA = vol.Schema(
- {
- DOMAIN: vol.Schema(
- {
- vol.Optional(CONF_MAC): cv.string,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- }
- )
- },
- extra=vol.ALLOW_EXTRA,
-)
-
-SERVICE_NUIMO = "led_matrix"
-DEFAULT_INTERVAL = 2.0
-
-SERVICE_NUIMO_SCHEMA = vol.Schema(
- {
- vol.Required("matrix"): cv.string,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional("interval", default=DEFAULT_INTERVAL): float,
- }
-)
-
-DEFAULT_ADAPTER = "hci0"
-
-
-def setup(hass, config):
- """Set up the Nuimo component."""
- conf = config[DOMAIN]
- mac = conf.get(CONF_MAC)
- name = conf.get(CONF_NAME)
- NuimoThread(hass, mac, name).start()
- return True
-
-
-class NuimoLogger:
- """Handle Nuimo Controller event callbacks."""
-
- def __init__(self, hass, name):
- """Initialize Logger object."""
- self._hass = hass
- self._name = name
-
- def received_gesture_event(self, event):
- """Input Event received."""
- _LOGGER.debug(
- "Received event: name=%s, gesture_id=%s,value=%s",
- event.name,
- event.gesture,
- event.value,
- )
- self._hass.bus.fire(
- EVENT_NUIMO, {"type": event.name, "value": event.value, "name": self._name}
- )
-
-
-class NuimoThread(threading.Thread):
- """Manage one Nuimo controller."""
-
- def __init__(self, hass, mac, name):
- """Initialize thread object."""
- super().__init__()
- self._hass = hass
- self._mac = mac
- self._name = name
- self._hass_is_running = True
- self._nuimo = None
- hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
-
- def run(self):
- """Set up the connection or be idle."""
- while self._hass_is_running:
- if not self._nuimo or not self._nuimo.is_connected():
- self._attach()
- self._connect()
- else:
- time.sleep(1)
-
- if self._nuimo:
- self._nuimo.disconnect()
- self._nuimo = None
-
- def stop(self, event):
- """Terminate Thread by unsetting flag."""
- _LOGGER.debug("Stopping thread for Nuimo %s", self._mac)
- self._hass_is_running = False
-
- def _attach(self):
- """Create a Nuimo object from MAC address or discovery."""
-
- if self._nuimo:
- self._nuimo.disconnect()
- self._nuimo = None
-
- if self._mac:
- self._nuimo = NuimoController(self._mac)
- else:
- nuimo_manager = NuimoDiscoveryManager(
- bluetooth_adapter=DEFAULT_ADAPTER, delegate=DiscoveryLogger()
- )
- nuimo_manager.start_discovery()
- # Were any Nuimos found?
- if not nuimo_manager.nuimos:
- _LOGGER.debug("No Nuimo devices detected")
- return
- # Take the first Nuimo found.
- self._nuimo = nuimo_manager.nuimos[0]
- self._mac = self._nuimo.addr
-
- def _connect(self):
- """Build up connection and set event delegator and service."""
- if not self._nuimo:
- return
-
- try:
- self._nuimo.connect()
- _LOGGER.debug("Connected to %s", self._mac)
- except RuntimeError as error:
- _LOGGER.error("Could not connect to %s: %s", self._mac, error)
- time.sleep(1)
- return
-
- nuimo_event_delegate = NuimoLogger(self._hass, self._name)
- self._nuimo.set_delegate(nuimo_event_delegate)
-
- def handle_write_matrix(call):
- """Handle led matrix service."""
- matrix = call.data.get("matrix", None)
- name = call.data.get(CONF_NAME, DEFAULT_NAME)
- interval = call.data.get("interval", DEFAULT_INTERVAL)
- if self._name == name and matrix:
- self._nuimo.write_matrix(matrix, interval)
-
- self._hass.services.register(
- DOMAIN, SERVICE_NUIMO, handle_write_matrix, schema=SERVICE_NUIMO_SCHEMA
- )
-
- self._nuimo.write_matrix(HOMEASSIST_LOGO, 2.0)
-
-
-# must be 9x9 matrix
-HOMEASSIST_LOGO = (
- " . "
- + " ... "
- + " ..... "
- + " ....... "
- + "..... ..."
- + " ....... "
- + " .. .... "
- + " .. .... "
- + "........."
-)
-
-
-class DiscoveryLogger:
- """Handle Nuimo Discovery callbacks."""
-
- # pylint: disable=no-self-use
- def discovery_started(self):
- """Discovery started."""
- _LOGGER.info("Started discovery")
-
- # pylint: disable=no-self-use
- def discovery_finished(self):
- """Discovery finished."""
- _LOGGER.info("Finished discovery")
-
- # pylint: disable=no-self-use
- def controller_added(self, nuimo):
- """Return that a controller was found."""
- _LOGGER.info("Added Nuimo: %s", nuimo)
diff --git a/homeassistant/components/nuimo_controller/manifest.json b/homeassistant/components/nuimo_controller/manifest.json
deleted file mode 100644
index dddd4a975231b7..00000000000000
--- a/homeassistant/components/nuimo_controller/manifest.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "domain": "nuimo_controller",
- "name": "Nuimo controller",
- "documentation": "https://www.home-assistant.io/integrations/nuimo_controller",
- "requirements": ["--only-binary=all nuimo==0.1.0"],
- "codeowners": []
-}
diff --git a/homeassistant/components/nuimo_controller/services.yaml b/homeassistant/components/nuimo_controller/services.yaml
deleted file mode 100644
index d98659caa8b11f..00000000000000
--- a/homeassistant/components/nuimo_controller/services.yaml
+++ /dev/null
@@ -1,17 +0,0 @@
-led_matrix:
- description: Sends an LED Matrix to your display
- fields:
- matrix:
- description: "A string representation of the matrix to be displayed. See the SDK documentation for more info: https://github.com/getSenic/nuimo-linux-python#write-to-nuimos-led-matrix"
- example: "........
- 0000000.
- .000000.
- ..00000.
- .0.0000.
- .00.000.
- .000000.
- .000000.
- ........"
- interval:
- description: Display interval in seconds
- example: 0.5
diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py
index 627cf20b16b416..a96cda070772bb 100644
--- a/homeassistant/components/nuki/__init__.py
+++ b/homeassistant/components/nuki/__init__.py
@@ -1,50 +1,65 @@
"""The nuki component."""
+import asyncio
from datetime import timedelta
import logging
-import voluptuous as vol
+import async_timeout
+from pynuki import NukiBridge
+from pynuki.bridge import InvalidCredentialsException
+from requests.exceptions import RequestException
-from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
-from homeassistant.config_entries import SOURCE_IMPORT
+from homeassistant import exceptions
+from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN
-import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+ UpdateFailed,
+)
-from .const import DEFAULT_PORT, DOMAIN
+from .const import (
+ DATA_BRIDGE,
+ DATA_COORDINATOR,
+ DATA_LOCKS,
+ DATA_OPENERS,
+ DEFAULT_TIMEOUT,
+ DOMAIN,
+ ERROR_STATES,
+)
_LOGGER = logging.getLogger(__name__)
-NUKI_PLATFORMS = ["lock"]
+PLATFORMS = ["binary_sensor", "lock"]
UPDATE_INTERVAL = timedelta(seconds=30)
-NUKI_SCHEMA = vol.Schema(
- vol.All(
- {
- vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
- vol.Required(CONF_TOKEN): cv.string,
- },
- )
-)
-CONFIG_SCHEMA = vol.Schema(
- {DOMAIN: vol.Schema(NUKI_SCHEMA)},
- extra=vol.ALLOW_EXTRA,
-)
+def _get_bridge_devices(bridge):
+ return bridge.locks, bridge.openers
+
+
+def _update_devices(devices):
+ for device in devices:
+ for level in (False, True):
+ try:
+ device.update(level)
+ except RequestException:
+ continue
+
+ if device.state not in ERROR_STATES:
+ break
async def async_setup(hass, config):
"""Set up the Nuki component."""
hass.data.setdefault(DOMAIN, {})
- _LOGGER.debug("Config: %s", config)
- for platform in NUKI_PLATFORMS:
+ for platform in PLATFORMS:
confs = config.get(platform)
if confs is None:
continue
for conf in confs:
- _LOGGER.debug("Conf: %s", conf)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
@@ -56,8 +71,98 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, entry):
"""Set up the Nuki entry."""
- hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, LOCK_DOMAIN)
+
+ hass.data.setdefault(DOMAIN, {})
+
+ try:
+ bridge = await hass.async_add_executor_job(
+ NukiBridge,
+ entry.data[CONF_HOST],
+ entry.data[CONF_TOKEN],
+ entry.data[CONF_PORT],
+ True,
+ DEFAULT_TIMEOUT,
+ )
+
+ locks, openers = await hass.async_add_executor_job(_get_bridge_devices, bridge)
+ except InvalidCredentialsException:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data
+ )
+ )
+ return False
+ except RequestException as err:
+ raise exceptions.ConfigEntryNotReady from err
+
+ async def async_update_data():
+ """Fetch data from Nuki bridge."""
+ try:
+ # Note: asyncio.TimeoutError and aiohttp.ClientError are already
+ # handled by the data update coordinator.
+ async with async_timeout.timeout(10):
+ await hass.async_add_executor_job(_update_devices, locks + openers)
+ except InvalidCredentialsException as err:
+ raise UpdateFailed(f"Invalid credentials for Bridge: {err}") from err
+ except RequestException as err:
+ raise UpdateFailed(f"Error communicating with Bridge: {err}") from err
+
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ # Name of the data. For logging purposes.
+ name="nuki devices",
+ update_method=async_update_data,
+ # Polling interval. Will only be polled if there are subscribers.
+ update_interval=UPDATE_INTERVAL,
)
+ hass.data[DOMAIN][entry.entry_id] = {
+ DATA_COORDINATOR: coordinator,
+ DATA_BRIDGE: bridge,
+ DATA_LOCKS: locks,
+ DATA_OPENERS: openers,
+ }
+
+ # Fetch initial data so we have data when entities subscribe
+ await coordinator.async_refresh()
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
return True
+
+
+async def async_unload_entry(hass, entry):
+ """Unload the Nuki entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
+
+
+class NukiEntity(CoordinatorEntity):
+ """An entity using CoordinatorEntity.
+
+ The CoordinatorEntity class provides:
+ should_poll
+ async_update
+ async_added_to_hass
+ available
+
+ """
+
+ def __init__(self, coordinator, nuki_device):
+ """Pass coordinator to CoordinatorEntity."""
+ super().__init__(coordinator)
+ self._nuki_device = nuki_device
diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py
new file mode 100644
index 00000000000000..37641dbf15a98f
--- /dev/null
+++ b/homeassistant/components/nuki/binary_sensor.py
@@ -0,0 +1,73 @@
+"""Doorsensor Support for the Nuki Lock."""
+
+import logging
+
+from pynuki import STATE_DOORSENSOR_OPENED
+
+from homeassistant.components.binary_sensor import DEVICE_CLASS_DOOR, BinarySensorEntity
+
+from . import NukiEntity
+from .const import ATTR_NUKI_ID, DATA_COORDINATOR, DATA_LOCKS, DOMAIN as NUKI_DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up the Nuki lock binary sensor."""
+ data = hass.data[NUKI_DOMAIN][entry.entry_id]
+ coordinator = data[DATA_COORDINATOR]
+
+ entities = []
+
+ for lock in data[DATA_LOCKS]:
+ if lock.is_door_sensor_activated:
+ entities.extend([NukiDoorsensorEntity(coordinator, lock)])
+
+ async_add_entities(entities)
+
+
+class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity):
+ """Representation of a Nuki Lock Doorsensor."""
+
+ @property
+ def name(self):
+ """Return the name of the lock."""
+ return self._nuki_device.name
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique ID."""
+ return f"{self._nuki_device.nuki_id}_doorsensor"
+
+ @property
+ def extra_state_attributes(self):
+ """Return the device specific state attributes."""
+ data = {
+ ATTR_NUKI_ID: self._nuki_device.nuki_id,
+ }
+ return data
+
+ @property
+ def available(self):
+ """Return true if door sensor is present and activated."""
+ return super().available and self._nuki_device.is_door_sensor_activated
+
+ @property
+ def door_sensor_state(self):
+ """Return the state of the door sensor."""
+ return self._nuki_device.door_sensor_state
+
+ @property
+ def door_sensor_state_name(self):
+ """Return the state name of the door sensor."""
+ return self._nuki_device.door_sensor_state_name
+
+ @property
+ def is_on(self):
+ """Return true if the door is open."""
+ return self.door_sensor_state == STATE_DOORSENSOR_OPENED
+
+ @property
+ def device_class(self):
+ """Return the class of this device, from component DEVICE_CLASSES."""
+ return DEVICE_CLASS_DOOR
diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py
index 9af74cb4423645..7a98ad2f00d032 100644
--- a/homeassistant/components/nuki/config_flow.py
+++ b/homeassistant/components/nuki/config_flow.py
@@ -7,13 +7,10 @@
import voluptuous as vol
from homeassistant import config_entries, exceptions
+from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN
-from .const import ( # pylint: disable=unused-import
- DEFAULT_PORT,
- DEFAULT_TIMEOUT,
- DOMAIN,
-)
+from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -25,6 +22,8 @@
}
)
+REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): str})
+
async def validate_input(hass, data):
"""Validate the user input allows us to connect.
@@ -54,6 +53,11 @@ async def validate_input(hass, data):
class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Nuki config flow."""
+ def __init__(self):
+ """Initialize the Nuki config flow."""
+ self.discovery_schema = {}
+ self._data = {}
+
async def async_step_import(self, user_input=None):
"""Handle a flow initiated by import."""
return await self.async_step_validate(user_input)
@@ -62,7 +66,67 @@ async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
return await self.async_step_validate(user_input)
- async def async_step_validate(self, user_input):
+ async def async_step_dhcp(self, discovery_info: dict):
+ """Prepare configuration for a DHCP discovered Nuki bridge."""
+ await self.async_set_unique_id(int(discovery_info.get(HOSTNAME)[12:], 16))
+
+ self._abort_if_unique_id_configured()
+
+ self.discovery_schema = vol.Schema(
+ {
+ vol.Required(CONF_HOST, default=discovery_info[IP_ADDRESS]): str,
+ vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
+ vol.Required(CONF_TOKEN): str,
+ }
+ )
+
+ return await self.async_step_validate()
+
+ async def async_step_reauth(self, data):
+ """Perform reauth upon an API authentication error."""
+ self._data = data
+
+ return await self.async_step_reauth_confirm()
+
+ async def async_step_reauth_confirm(self, user_input=None):
+ """Dialog that inform the user that reauth is required."""
+ errors = {}
+ if user_input is None:
+ return self.async_show_form(
+ step_id="reauth_confirm", data_schema=REAUTH_SCHEMA
+ )
+
+ conf = {
+ CONF_HOST: self._data[CONF_HOST],
+ CONF_PORT: self._data[CONF_PORT],
+ CONF_TOKEN: user_input[CONF_TOKEN],
+ }
+
+ try:
+ info = await validate_input(self.hass, conf)
+ except CannotConnect:
+ errors["base"] = "cannot_connect"
+ except InvalidAuth:
+ errors["base"] = "invalid_auth"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+
+ if not errors:
+ existing_entry = await self.async_set_unique_id(info["ids"]["hardwareId"])
+ if existing_entry:
+ self.hass.config_entries.async_update_entry(existing_entry, data=conf)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(existing_entry.entry_id)
+ )
+ return self.async_abort(reason="reauth_successful")
+ errors["base"] = "unknown"
+
+ return self.async_show_form(
+ step_id="reauth_confirm", data_schema=REAUTH_SCHEMA, errors=errors
+ )
+
+ async def async_step_validate(self, user_input=None):
"""Handle init step of a flow."""
errors = {}
@@ -84,8 +148,9 @@ async def async_step_validate(self, user_input):
title=info["ids"]["hardwareId"], data=user_input
)
+ data_schema = self.discovery_schema or USER_SCHEMA
return self.async_show_form(
- step_id="user", data_schema=USER_SCHEMA, errors=errors
+ step_id="user", data_schema=data_schema, errors=errors
)
diff --git a/homeassistant/components/nuki/const.py b/homeassistant/components/nuki/const.py
index 07ef49ebd88723..da12a3a074ddb5 100644
--- a/homeassistant/components/nuki/const.py
+++ b/homeassistant/components/nuki/const.py
@@ -1,6 +1,19 @@
"""Constants for Nuki."""
DOMAIN = "nuki"
+# Attributes
+ATTR_BATTERY_CRITICAL = "battery_critical"
+ATTR_NUKI_ID = "nuki_id"
+ATTR_UNLATCH = "unlatch"
+
+# Data
+DATA_BRIDGE = "nuki_bridge_data"
+DATA_LOCKS = "nuki_locks_data"
+DATA_OPENERS = "nuki_openers_data"
+DATA_COORDINATOR = "nuki_coordinator"
+
# Defaults
DEFAULT_PORT = 8080
DEFAULT_TIMEOUT = 20
+
+ERROR_STATES = (0, 254, 255)
diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py
index fe024405908b15..bd5d58ed42ae1e 100644
--- a/homeassistant/components/nuki/lock.py
+++ b/homeassistant/components/nuki/lock.py
@@ -1,31 +1,28 @@
"""Nuki.io lock platform."""
from abc import ABC, abstractmethod
-from datetime import timedelta
import logging
-from pynuki import NukiBridge
-from requests.exceptions import RequestException
import voluptuous as vol
from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockEntity
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN
from homeassistant.helpers import config_validation as cv, entity_platform
-from .const import DEFAULT_PORT, DEFAULT_TIMEOUT
+from . import NukiEntity
+from .const import (
+ ATTR_BATTERY_CRITICAL,
+ ATTR_NUKI_ID,
+ ATTR_UNLATCH,
+ DATA_COORDINATOR,
+ DATA_LOCKS,
+ DATA_OPENERS,
+ DEFAULT_PORT,
+ DOMAIN as NUKI_DOMAIN,
+ ERROR_STATES,
+)
_LOGGER = logging.getLogger(__name__)
-ATTR_BATTERY_CRITICAL = "battery_critical"
-ATTR_NUKI_ID = "nuki_id"
-ATTR_UNLATCH = "unlatch"
-
-MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=5)
-MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
-
-NUKI_DATA = "nuki"
-
-ERROR_STATES = (0, 254, 255)
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
@@ -42,26 +39,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
-async def async_setup_entry(hass, config_entry, async_add_entities):
+async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the Nuki lock platform."""
- config = config_entry.data
- _LOGGER.debug("Config: %s", config)
-
- def get_entities():
- bridge = NukiBridge(
- config[CONF_HOST],
- config[CONF_TOKEN],
- config[CONF_PORT],
- True,
- DEFAULT_TIMEOUT,
- )
-
- entities = [NukiLockEntity(lock) for lock in bridge.locks]
- entities.extend([NukiOpenerEntity(opener) for opener in bridge.openers])
- return entities
-
- entities = await hass.async_add_executor_job(get_entities)
+ data = hass.data[NUKI_DOMAIN][entry.entry_id]
+ coordinator = data[DATA_COORDINATOR]
+ entities = [NukiLockEntity(coordinator, lock) for lock in data[DATA_LOCKS]]
+ entities.extend(
+ [NukiOpenerEntity(coordinator, opener) for opener in data[DATA_OPENERS]]
+ )
async_add_entities(entities)
platform = entity_platform.current_platform.get()
@@ -76,14 +62,9 @@ def get_entities():
)
-class NukiDeviceEntity(LockEntity, ABC):
+class NukiDeviceEntity(NukiEntity, LockEntity, ABC):
"""Representation of a Nuki device."""
- def __init__(self, nuki_device):
- """Initialize the lock."""
- self._nuki_device = nuki_device
- self._available = nuki_device.state not in ERROR_STATES
-
@property
def name(self):
"""Return the name of the lock."""
@@ -100,7 +81,7 @@ def is_locked(self):
"""Return true if lock is locked."""
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
data = {
ATTR_BATTERY_CRITICAL: self._nuki_device.battery_critical,
@@ -116,22 +97,7 @@ def supported_features(self):
@property
def available(self) -> bool:
"""Return True if entity is available."""
- return self._available
-
- def update(self):
- """Update the nuki lock properties."""
- for level in (False, True):
- try:
- self._nuki_device.update(aggressive=level)
- except RequestException:
- _LOGGER.warning("Network issues detect with %s", self.name)
- self._available = False
- continue
-
- # If in error state, we force an update and repoll data
- self._available = self._nuki_device.state not in ERROR_STATES
- if self._available:
- break
+ return super().available and self._nuki_device.state not in ERROR_STATES
@abstractmethod
def lock(self, **kwargs):
diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json
index 9385821845ad30..8500a3c90aa069 100644
--- a/homeassistant/components/nuki/manifest.json
+++ b/homeassistant/components/nuki/manifest.json
@@ -2,7 +2,8 @@
"domain": "nuki",
"name": "Nuki",
"documentation": "https://www.home-assistant.io/integrations/nuki",
- "requirements": ["pynuki==1.3.8"],
+ "requirements": ["pynuki==1.4.1"],
"codeowners": ["@pschmitt", "@pvizeli", "@pree"],
- "config_flow": true
+ "config_flow": true,
+ "dhcp": [{ "hostname": "nuki_bridge_*" }]
}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/strings.json b/homeassistant/components/nuki/strings.json
index 9e1e4f5e5ab248..3f6de25122a872 100644
--- a/homeassistant/components/nuki/strings.json
+++ b/homeassistant/components/nuki/strings.json
@@ -7,12 +7,22 @@
"port": "[%key:common::config_flow::data::port%]",
"token": "[%key:common::config_flow::data::access_token%]"
}
+ },
+ "reauth_confirm": {
+ "title": "[%key:common::config_flow::title::reauth%]",
+ "description": "The Nuki integration needs to re-authenticate with your bridge.",
+ "data": {
+ "token": "[%key:common::config_flow::data::access_token%]"
+ }
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "abort": {
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/bg.json b/homeassistant/components/nuki/translations/bg.json
new file mode 100644
index 00000000000000..4983c9a14b265d
--- /dev/null
+++ b/homeassistant/components/nuki/translations/bg.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "port": "\u041f\u043e\u0440\u0442"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/ca.json b/homeassistant/components/nuki/translations/ca.json
new file mode 100644
index 00000000000000..e7b149349db987
--- /dev/null
+++ b/homeassistant/components/nuki/translations/ca.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "token": "Token d'acc\u00e9s"
+ },
+ "description": "La integraci\u00f3 Nuki ha de tornar a autenticar-se amb la passarel\u00b7la d'enlla\u00e7.",
+ "title": "Reautenticaci\u00f3 de la integraci\u00f3"
+ },
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "port": "Port",
+ "token": "Token d'acc\u00e9s"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/cs.json b/homeassistant/components/nuki/translations/cs.json
new file mode 100644
index 00000000000000..349c92805cf8f1
--- /dev/null
+++ b/homeassistant/components/nuki/translations/cs.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
+ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
+ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hostitel",
+ "port": "Port",
+ "token": "P\u0159\u00edstupov\u00fd token"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/de.json b/homeassistant/components/nuki/translations/de.json
new file mode 100644
index 00000000000000..ae1322d7641be5
--- /dev/null
+++ b/homeassistant/components/nuki/translations/de.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "token": "Zugangstoken"
+ },
+ "title": "Integration erneut authentifizieren"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port",
+ "token": "Zugangstoken"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/en.json b/homeassistant/components/nuki/translations/en.json
index 70ae9c6a1fe171..99c43859eb0ae9 100644
--- a/homeassistant/components/nuki/translations/en.json
+++ b/homeassistant/components/nuki/translations/en.json
@@ -1,16 +1,26 @@
{
"config": {
+ "abort": {
+ "reauth_successful": "Re-authentication was successful"
+ },
"error": {
"cannot_connect": "Failed to connect",
- "invalid_auth": "Could not login with provided token",
- "unknown": "Unknown error"
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
},
"step": {
+ "reauth_confirm": {
+ "data": {
+ "token": "Access Token"
+ },
+ "description": "The Nuki integration needs to re-authenticate with your bridge.",
+ "title": "Reauthenticate Integration"
+ },
"user": {
"data": {
- "token": "Access Token",
"host": "Host",
- "port": "Port"
+ "port": "Port",
+ "token": "Access Token"
}
}
}
diff --git a/homeassistant/components/nuki/translations/es.json b/homeassistant/components/nuki/translations/es.json
new file mode 100644
index 00000000000000..8def4e2780d2ef
--- /dev/null
+++ b/homeassistant/components/nuki/translations/es.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "No se pudo conectar",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
+ "unknown": "Error inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Puerto",
+ "token": "Token de acceso"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/et.json b/homeassistant/components/nuki/translations/et.json
new file mode 100644
index 00000000000000..e587458bbf0a84
--- /dev/null
+++ b/homeassistant/components/nuki/translations/et.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Taastuvastamine \u00f5nnestus"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_auth": "Vigane autentimine",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "token": "Juurdep\u00e4\u00e4sut\u00f5end"
+ },
+ "description": "Nuki sidumise peab sillaga uuesti autentima.",
+ "title": "Taastuvasta sidumine"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port",
+ "token": "Juurdep\u00e4\u00e4sut\u00f5end"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/fr.json b/homeassistant/components/nuki/translations/fr.json
new file mode 100644
index 00000000000000..035c07325766cd
--- /dev/null
+++ b/homeassistant/components/nuki/translations/fr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u00c9chec de la connexion ",
+ "invalid_auth": "Authentification invalide ",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hote",
+ "port": "Port",
+ "token": "jeton d'acc\u00e8s"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/hu.json b/homeassistant/components/nuki/translations/hu.json
new file mode 100644
index 00000000000000..4f0b1a29738370
--- /dev/null
+++ b/homeassistant/components/nuki/translations/hu.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "port": "Port",
+ "token": "Hozz\u00e1f\u00e9r\u00e9si token"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/id.json b/homeassistant/components/nuki/translations/id.json
new file mode 100644
index 00000000000000..d9e5e1de2c31af
--- /dev/null
+++ b/homeassistant/components/nuki/translations/id.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port",
+ "token": "Token Akses"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/it.json b/homeassistant/components/nuki/translations/it.json
new file mode 100644
index 00000000000000..eaf0a8e52e4e2b
--- /dev/null
+++ b/homeassistant/components/nuki/translations/it.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "token": "Token di accesso"
+ },
+ "description": "L'integrazione Nuki deve essere nuovamente autenticata con il tuo bridge.",
+ "title": "Autenticare nuovamente l'integrazione"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Porta",
+ "token": "Token di accesso"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/ko.json b/homeassistant/components/nuki/translations/ko.json
new file mode 100644
index 00000000000000..3015596e7d4678
--- /dev/null
+++ b/homeassistant/components/nuki/translations/ko.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "token": "\uc561\uc138\uc2a4 \ud1a0\ud070"
+ },
+ "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30"
+ },
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "port": "\ud3ec\ud2b8",
+ "token": "\uc561\uc138\uc2a4 \ud1a0\ud070"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/nl.json b/homeassistant/components/nuki/translations/nl.json
new file mode 100644
index 00000000000000..3bfa8f60b7021c
--- /dev/null
+++ b/homeassistant/components/nuki/translations/nl.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Herauthenticatie was succesvol"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "token": "Toegangstoken"
+ },
+ "description": "De Nuki integratie moet opnieuw authenticeren met je bridge.",
+ "title": "Verifieer de integratie opnieuw"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Poort",
+ "token": "Toegangstoken"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/no.json b/homeassistant/components/nuki/translations/no.json
new file mode 100644
index 00000000000000..1ae4eb03624421
--- /dev/null
+++ b/homeassistant/components/nuki/translations/no.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_auth": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "token": "Tilgangstoken"
+ },
+ "description": "Nuki-integrasjonen m\u00e5 godkjennes p\u00e5 nytt med broen din.",
+ "title": "Godkjenne integrering p\u00e5 nytt"
+ },
+ "user": {
+ "data": {
+ "host": "Vert",
+ "port": "Port",
+ "token": "Tilgangstoken"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/pl.json b/homeassistant/components/nuki/translations/pl.json
new file mode 100644
index 00000000000000..c51a431cfe7e77
--- /dev/null
+++ b/homeassistant/components/nuki/translations/pl.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "token": "Token dost\u0119pu"
+ },
+ "description": "Integracja Nuki wymaga ponownego uwierzytelnienia z Twoim mostkiem.",
+ "title": "Ponownie uwierzytelnij integracj\u0119"
+ },
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP",
+ "port": "Port",
+ "token": "Token dost\u0119pu"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/ru.json b/homeassistant/components/nuki/translations/ru.json
new file mode 100644
index 00000000000000..a39f1429e140cd
--- /dev/null
+++ b/homeassistant/components/nuki/translations/ru.json
@@ -0,0 +1,28 @@
+{
+ "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_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430"
+ },
+ "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 \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0430 Nuki.",
+ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442",
+ "token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/tr.json b/homeassistant/components/nuki/translations/tr.json
new file mode 100644
index 00000000000000..ba6a496fa4c049
--- /dev/null
+++ b/homeassistant/components/nuki/translations/tr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port",
+ "token": "Eri\u015fim Belirteci"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/zh-Hant.json b/homeassistant/components/nuki/translations/zh-Hant.json
new file mode 100644
index 00000000000000..fb486faced1a64
--- /dev/null
+++ b/homeassistant/components/nuki/translations/zh-Hant.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "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"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "token": "\u5b58\u53d6\u6b0a\u6756"
+ },
+ "description": "Nuki \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49 Bridge\u3002",
+ "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408"
+ },
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "port": "\u901a\u8a0a\u57e0",
+ "token": "\u5b58\u53d6\u6b0a\u6756"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/numato/manifest.json b/homeassistant/components/numato/manifest.json
index 4b7dcd9e372eef..6138f401ec2647 100644
--- a/homeassistant/components/numato/manifest.json
+++ b/homeassistant/components/numato/manifest.json
@@ -2,6 +2,6 @@
"domain": "numato",
"name": "Numato USB GPIO Expander",
"documentation": "https://www.home-assistant.io/integrations/numato",
- "requirements": ["numato-gpio==0.8.0"],
+ "requirements": ["numato-gpio==0.10.0"],
"codeowners": ["@clssn"]
}
diff --git a/homeassistant/components/numato/sensor.py b/homeassistant/components/numato/sensor.py
index e268d32a29399a..19372de5258b4e 100644
--- a/homeassistant/components/numato/sensor.py
+++ b/homeassistant/components/numato/sensor.py
@@ -3,8 +3,8 @@
from numato_gpio import NumatoGpioError
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_ID, CONF_NAME, CONF_SENSORS
-from homeassistant.helpers.entity import Entity
from . import (
CONF_DEVICES,
@@ -58,7 +58,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class NumatoGpioAdc(Entity):
+class NumatoGpioAdc(SensorEntity):
"""Represents an ADC port of a Numato USB GPIO expander."""
def __init__(self, name, device_id, port, src_range, dst_range, dst_unit, api):
diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py
index 31a0bcd776280c..e61398f6582f3d 100644
--- a/homeassistant/components/number/__init__.py
+++ b/homeassistant/components/number/__init__.py
@@ -1,8 +1,10 @@
"""Component to allow numeric input for platforms."""
+from __future__ import annotations
+
from abc import abstractmethod
from datetime import timedelta
import logging
-from typing import Any, Dict
+from typing import Any
import voluptuous as vol
@@ -66,7 +68,7 @@ class NumberEntity(Entity):
"""Representation of a Number entity."""
@property
- def capability_attributes(self) -> Dict[str, Any]:
+ def capability_attributes(self) -> dict[str, Any]:
"""Return capability attributes."""
return {
ATTR_MIN: self.min_value,
@@ -110,5 +112,4 @@ def set_value(self, value: float) -> None:
async def async_set_value(self, value: float) -> None:
"""Set new value."""
- assert self.hass is not None
await self.hass.async_add_executor_job(self.set_value, value)
diff --git a/homeassistant/components/number/device_action.py b/homeassistant/components/number/device_action.py
index c22ba720e374e8..77b36b49f20a79 100644
--- a/homeassistant/components/number/device_action.py
+++ b/homeassistant/components/number/device_action.py
@@ -1,5 +1,7 @@
"""Provides device actions for Number."""
-from typing import Any, Dict, List, Optional
+from __future__ import annotations
+
+from typing import Any
import voluptuous as vol
@@ -27,10 +29,10 @@
)
-async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device actions for Number."""
registry = await entity_registry.async_get_registry(hass)
- actions: List[Dict[str, Any]] = []
+ actions: list[dict[str, Any]] = []
# Get all the integrations entities for this device
for entry in entity_registry.async_entries_for_device(registry, device_id):
@@ -50,14 +52,9 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
async def async_call_action_from_config(
- hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context]
+ hass: HomeAssistant, config: dict, variables: dict, context: Context | None
) -> None:
"""Execute a device action."""
- config = ACTION_SCHEMA(config)
-
- if config[CONF_TYPE] != ATYP_SET_VALUE:
- return
-
await hass.services.async_call(
DOMAIN,
const.SERVICE_SET_VALUE,
@@ -72,11 +69,6 @@ async def async_call_action_from_config(
async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> dict:
"""List action capabilities."""
- action_type = config[CONF_TYPE]
-
- if action_type != ATYP_SET_VALUE:
- return {}
-
fields = {vol.Required(const.ATTR_VALUE): vol.Coerce(float)}
return {"extra_fields": vol.Schema(fields)}
diff --git a/homeassistant/components/number/reproduce_state.py b/homeassistant/components/number/reproduce_state.py
index 611744e3191eaa..4364dffe1e8531 100644
--- a/homeassistant/components/number/reproduce_state.py
+++ b/homeassistant/components/number/reproduce_state.py
@@ -1,7 +1,9 @@
"""Reproduce a Number entity state."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import Context, State
@@ -16,8 +18,8 @@ async def _async_reproduce_state(
hass: HomeAssistantType,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -50,8 +52,8 @@ async def async_reproduce_states(
hass: HomeAssistantType,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce multiple Number states."""
# Reproduce states in parallel.
diff --git a/homeassistant/components/number/services.yaml b/homeassistant/components/number/services.yaml
index d18416f9974275..a684fef7d5ddd2 100644
--- a/homeassistant/components/number/services.yaml
+++ b/homeassistant/components/number/services.yaml
@@ -1,11 +1,13 @@
# Describes the format for available Number entity services
set_value:
+ name: Set
description: Set the value of a Number entity.
+ target:
fields:
- entity_id:
- description: Entity ID of the Number to set the new value.
- example: number.volume
value:
+ name: Value
description: The target value the entity should be set to.
example: 42
+ selector:
+ text:
diff --git a/homeassistant/components/number/translations/ca.json b/homeassistant/components/number/translations/ca.json
new file mode 100644
index 00000000000000..0058f01aac0f4f
--- /dev/null
+++ b/homeassistant/components/number/translations/ca.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "Estableix el valor de {entity_name}"
+ }
+ },
+ "title": "N\u00famero"
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/translations/cs.json b/homeassistant/components/number/translations/cs.json
new file mode 100644
index 00000000000000..a6810f08c61416
--- /dev/null
+++ b/homeassistant/components/number/translations/cs.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "Nastavit hodnotu pro {entity_name}"
+ }
+ },
+ "title": "\u010c\u00edslo"
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/translations/de.json b/homeassistant/components/number/translations/de.json
new file mode 100644
index 00000000000000..3ef9a0358a461e
--- /dev/null
+++ b/homeassistant/components/number/translations/de.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "Wert f\u00fcr {entity_name} setzen"
+ }
+ },
+ "title": "Nummer"
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/translations/en.json b/homeassistant/components/number/translations/en.json
new file mode 100644
index 00000000000000..4e3fe6536b3451
--- /dev/null
+++ b/homeassistant/components/number/translations/en.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "Set value for {entity_name}"
+ }
+ },
+ "title": "Number"
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/translations/es.json b/homeassistant/components/number/translations/es.json
new file mode 100644
index 00000000000000..e709346849e403
--- /dev/null
+++ b/homeassistant/components/number/translations/es.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "Establecer valor para {entity_name}"
+ }
+ },
+ "title": "N\u00famero"
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/translations/et.json b/homeassistant/components/number/translations/et.json
new file mode 100644
index 00000000000000..36958c0fc77c4a
--- /dev/null
+++ b/homeassistant/components/number/translations/et.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "Olemi {entity_name} v\u00e4\u00e4rtuse m\u00e4\u00e4ramine"
+ }
+ },
+ "title": "Number"
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/translations/fr.json b/homeassistant/components/number/translations/fr.json
new file mode 100644
index 00000000000000..9f49c3fb96224d
--- /dev/null
+++ b/homeassistant/components/number/translations/fr.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "D\u00e9finir la valeur de {entity_name}"
+ }
+ },
+ "title": "Nombre"
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/translations/hu.json b/homeassistant/components/number/translations/hu.json
new file mode 100644
index 00000000000000..296b9b750a7077
--- /dev/null
+++ b/homeassistant/components/number/translations/hu.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "{entity_name} \u00e9rt\u00e9k\u00e9nek be\u00e1ll\u00edt\u00e1sa"
+ }
+ },
+ "title": "Sz\u00e1m"
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/translations/id.json b/homeassistant/components/number/translations/id.json
new file mode 100644
index 00000000000000..6e928cb51cf18d
--- /dev/null
+++ b/homeassistant/components/number/translations/id.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "Tetapkan nilai untuk {entity_name}"
+ }
+ },
+ "title": "Bilangan"
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/translations/it.json b/homeassistant/components/number/translations/it.json
new file mode 100644
index 00000000000000..135467cea9bf9f
--- /dev/null
+++ b/homeassistant/components/number/translations/it.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "Imposta il valore per {entity_name}"
+ }
+ },
+ "title": "Numero"
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/translations/ko.json b/homeassistant/components/number/translations/ko.json
new file mode 100644
index 00000000000000..9c642931408745
--- /dev/null
+++ b/homeassistant/components/number/translations/ko.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "{entity_name}\uc758 \uac12 \uc124\uc815\ud558\uae30"
+ }
+ },
+ "title": "\uc22b\uc790"
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/translations/nl.json b/homeassistant/components/number/translations/nl.json
new file mode 100644
index 00000000000000..f9a1c6b60a9374
--- /dev/null
+++ b/homeassistant/components/number/translations/nl.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "Stel waarde in voor {entity_name}"
+ }
+ },
+ "title": "Nummer"
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/translations/no.json b/homeassistant/components/number/translations/no.json
new file mode 100644
index 00000000000000..ad82c4ac6d1119
--- /dev/null
+++ b/homeassistant/components/number/translations/no.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "Angi verdi for {entity_name}"
+ }
+ },
+ "title": "Nummer"
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/translations/pl.json b/homeassistant/components/number/translations/pl.json
new file mode 100644
index 00000000000000..93d5dd045990f1
--- /dev/null
+++ b/homeassistant/components/number/translations/pl.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "ustaw warto\u015b\u0107 dla {entity_name}"
+ }
+ },
+ "title": "Number"
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/translations/ru.json b/homeassistant/components/number/translations/ru.json
new file mode 100644
index 00000000000000..5e250b4e2db8c6
--- /dev/null
+++ b/homeassistant/components/number/translations/ru.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u043b\u044f {entity_name}"
+ }
+ },
+ "title": "\u0427\u0438\u0441\u043b\u043e"
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/translations/tr.json b/homeassistant/components/number/translations/tr.json
new file mode 100644
index 00000000000000..dfdbd90531752a
--- /dev/null
+++ b/homeassistant/components/number/translations/tr.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "{entity_name} i\u00e7in de\u011fer ayarlay\u0131n"
+ }
+ },
+ "title": "Numara"
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/translations/zh-Hans.json b/homeassistant/components/number/translations/zh-Hans.json
new file mode 100644
index 00000000000000..de9720ed77acaa
--- /dev/null
+++ b/homeassistant/components/number/translations/zh-Hans.json
@@ -0,0 +1,7 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "\u8bbe\u7f6e {entity_name} \u7684\u503c"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/number/translations/zh-Hant.json b/homeassistant/components/number/translations/zh-Hant.json
new file mode 100644
index 00000000000000..d36f751682d7ab
--- /dev/null
+++ b/homeassistant/components/number/translations/zh-Hant.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "set_value": "{entity_name} \u8a2d\u5b9a\u503c"
+ }
+ },
+ "title": "\u865f\u78bc"
+}
\ No newline at end of file
diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py
index 5669b8a5c3bb63..be86ca5951c31a 100644
--- a/homeassistant/components/nut/__init__.py
+++ b/homeassistant/components/nut/__init__.py
@@ -74,7 +74,7 @@ async def async_update_data():
)
# Fetch initial data so we have data when entities subscribe
- await coordinator.async_refresh()
+ await coordinator.async_config_entry_first_refresh()
status = data.status
if not status:
@@ -101,9 +101,9 @@ async def async_update_data():
UNDO_UPDATE_LISTENER: undo_listener,
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -178,8 +178,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py
index 8a868d7bb39bbd..07e135b8ebdf68 100644
--- a/homeassistant/components/nut/config_flow.py
+++ b/homeassistant/components/nut/config_flow.py
@@ -6,6 +6,7 @@
from homeassistant import config_entries, core, exceptions
from homeassistant.const import (
CONF_ALIAS,
+ CONF_BASE,
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
@@ -21,12 +22,12 @@
DEFAULT_HOST,
DEFAULT_PORT,
DEFAULT_SCAN_INTERVAL,
+ DOMAIN,
KEY_STATUS,
KEY_STATUS_DISPLAY,
SENSOR_NAME,
SENSOR_TYPES,
)
-from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
@@ -129,7 +130,6 @@ async def async_step_zeroconf(self, discovery_info):
"""Prepare configuration for a discovered nut device."""
self.discovery_info = discovery_info
await self._async_handle_discovery_without_unique_id()
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {
CONF_PORT: discovery_info.get(CONF_PORT, DEFAULT_PORT),
CONF_HOST: discovery_info[CONF_HOST],
@@ -212,10 +212,10 @@ async def _async_validate_or_error(self, config):
try:
info = await validate_input(self.hass, config)
except CannotConnect:
- errors["base"] = "cannot_connect"
+ errors[CONF_BASE] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
- errors["base"] = "unknown"
+ errors[CONF_BASE] = "unknown"
return info, errors
@staticmethod
@@ -242,7 +242,17 @@ async def async_step_init(self, user_input=None):
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
)
- info = await validate_input(self.hass, self.config_entry.data)
+ errors = {}
+ try:
+ info = await validate_input(self.hass, self.config_entry.data)
+ except CannotConnect:
+ errors[CONF_BASE] = "cannot_connect"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors[CONF_BASE] = "unknown"
+
+ if errors:
+ return self.async_show_form(step_id="abort", errors=errors)
base_schema = _resource_schema_base(info["available_resources"], resources)
base_schema[
@@ -250,10 +260,13 @@ async def async_step_init(self, user_input=None):
] = cv.positive_int
return self.async_show_form(
- step_id="init",
- data_schema=vol.Schema(base_schema),
+ step_id="init", data_schema=vol.Schema(base_schema), errors=errors
)
+ async def async_step_abort(self, user_input=None):
+ """Abort options flow."""
+ return self.async_create_entry(title="", data=self.config_entry.options)
+
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py
index cc70b33f7633b1..890ac3697dd6b4 100644
--- a/homeassistant/components/nut/const.py
+++ b/homeassistant/components/nut/const.py
@@ -1,8 +1,10 @@
"""The nut component."""
from homeassistant.components.sensor import (
DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
+ DEVICE_CLASS_VOLTAGE,
)
from homeassistant.const import (
ELECTRICAL_CURRENT_AMPERE,
@@ -45,7 +47,7 @@
"ups.temperature": [
"UPS Temperature",
TEMP_CELSIUS,
- "mdi:thermometer",
+ None,
DEVICE_CLASS_TEMPERATURE,
],
"ups.load": ["Load", PERCENTAGE, "mdi:gauge", None],
@@ -83,13 +85,13 @@
"ups.realpower": [
"Current Real Power",
POWER_WATT,
- "mdi:flash",
+ None,
DEVICE_CLASS_POWER,
],
"ups.realpower.nominal": [
"Nominal Real Power",
POWER_WATT,
- "mdi:flash",
+ None,
DEVICE_CLASS_POWER,
],
"ups.beeper.status": ["Beeper Status", "", "mdi:information-outline", None],
@@ -102,7 +104,7 @@
"battery.charge": [
"Battery Charge",
PERCENTAGE,
- "mdi:gauge",
+ None,
DEVICE_CLASS_BATTERY,
],
"battery.charge.low": ["Low Battery Setpoint", PERCENTAGE, "mdi:gauge", None],
@@ -119,10 +121,15 @@
None,
],
"battery.charger.status": ["Charging Status", "", "mdi:information-outline", None],
- "battery.voltage": ["Battery Voltage", VOLT, "mdi:flash", None],
- "battery.voltage.nominal": ["Nominal Battery Voltage", VOLT, "mdi:flash", None],
- "battery.voltage.low": ["Low Battery Voltage", VOLT, "mdi:flash", None],
- "battery.voltage.high": ["High Battery Voltage", VOLT, "mdi:flash", None],
+ "battery.voltage": ["Battery Voltage", VOLT, None, DEVICE_CLASS_VOLTAGE],
+ "battery.voltage.nominal": [
+ "Nominal Battery Voltage",
+ VOLT,
+ None,
+ DEVICE_CLASS_VOLTAGE,
+ ],
+ "battery.voltage.low": ["Low Battery Voltage", VOLT, None, DEVICE_CLASS_VOLTAGE],
+ "battery.voltage.high": ["High Battery Voltage", VOLT, None, DEVICE_CLASS_VOLTAGE],
"battery.capacity": ["Battery Capacity", "Ah", "mdi:flash", None],
"battery.current": [
"Battery Current",
@@ -139,7 +146,7 @@
"battery.temperature": [
"Battery Temperature",
TEMP_CELSIUS,
- "mdi:thermometer",
+ None,
DEVICE_CLASS_TEMPERATURE,
],
"battery.runtime": ["Battery Runtime", TIME_SECONDS, "mdi:timer-outline", None],
@@ -177,16 +184,21 @@
"mdi:information-outline",
None,
],
- "input.transfer.low": ["Low Voltage Transfer", VOLT, "mdi:flash", None],
- "input.transfer.high": ["High Voltage Transfer", VOLT, "mdi:flash", None],
+ "input.transfer.low": ["Low Voltage Transfer", VOLT, None, DEVICE_CLASS_VOLTAGE],
+ "input.transfer.high": ["High Voltage Transfer", VOLT, None, DEVICE_CLASS_VOLTAGE],
"input.transfer.reason": [
"Voltage Transfer Reason",
"",
"mdi:information-outline",
None,
],
- "input.voltage": ["Input Voltage", VOLT, "mdi:flash", None],
- "input.voltage.nominal": ["Nominal Input Voltage", VOLT, "mdi:flash", None],
+ "input.voltage": ["Input Voltage", VOLT, None, DEVICE_CLASS_VOLTAGE],
+ "input.voltage.nominal": [
+ "Nominal Input Voltage",
+ VOLT,
+ None,
+ DEVICE_CLASS_VOLTAGE,
+ ],
"input.frequency": ["Input Line Frequency", FREQUENCY_HERTZ, "mdi:flash", None],
"input.frequency.nominal": [
"Nominal Input Line Frequency",
@@ -207,8 +219,13 @@
"mdi:flash",
None,
],
- "output.voltage": ["Output Voltage", VOLT, "mdi:flash", None],
- "output.voltage.nominal": ["Nominal Output Voltage", VOLT, "mdi:flash", None],
+ "output.voltage": ["Output Voltage", VOLT, None, DEVICE_CLASS_VOLTAGE],
+ "output.voltage.nominal": [
+ "Nominal Output Voltage",
+ VOLT,
+ None,
+ DEVICE_CLASS_VOLTAGE,
+ ],
"output.frequency": ["Output Frequency", FREQUENCY_HERTZ, "mdi:flash", None],
"output.frequency.nominal": [
"Nominal Output Frequency",
@@ -216,6 +233,18 @@
"mdi:flash",
None,
],
+ "ambient.humidity": [
+ "Ambient Humidity",
+ PERCENTAGE,
+ None,
+ DEVICE_CLASS_HUMIDITY,
+ ],
+ "ambient.temperature": [
+ "Ambient Temperature",
+ TEMP_CELSIUS,
+ None,
+ DEVICE_CLASS_TEMPERATURE,
+ ],
}
STATE_TYPES = {
diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py
index f4fbbdef932001..2e3826935fe20d 100644
--- a/homeassistant/components/nut/sensor.py
+++ b/homeassistant/components/nut/sensor.py
@@ -1,6 +1,7 @@
"""Provides a sensor to track various status aspects of a UPS."""
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ATTR_STATE, CONF_RESOURCES, STATE_UNKNOWN
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -67,7 +68,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
)
else:
- _LOGGER.warning(
+ _LOGGER.info(
"Sensor type: %s does not appear in the NUT status "
"output, cannot add",
sensor_type,
@@ -76,7 +77,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities, True)
-class NUTSensor(CoordinatorEntity):
+class NUTSensor(CoordinatorEntity, SensorEntity):
"""Representation of a sensor entity for NUT status values."""
def __init__(
@@ -160,7 +161,7 @@ def unit_of_measurement(self):
return self._unit
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the sensor attributes."""
return {ATTR_STATE: _format_display_state(self._data.status)}
diff --git a/homeassistant/components/nut/strings.json b/homeassistant/components/nut/strings.json
index 1b71280b6a95c8..97e637fdcb35c2 100644
--- a/homeassistant/components/nut/strings.json
+++ b/homeassistant/components/nut/strings.json
@@ -41,6 +41,10 @@
"scan_interval": "Scan Interval (seconds)"
}
}
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
}
}
}
diff --git a/homeassistant/components/nut/translations/ca.json b/homeassistant/components/nut/translations/ca.json
index fa7b728e115272..fc8b5b719a029f 100644
--- a/homeassistant/components/nut/translations/ca.json
+++ b/homeassistant/components/nut/translations/ca.json
@@ -33,6 +33,10 @@
}
},
"options": {
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "unknown": "Error inesperat"
+ },
"step": {
"init": {
"data": {
diff --git a/homeassistant/components/nut/translations/de.json b/homeassistant/components/nut/translations/de.json
index 793ab5bfa7c102..990f21523b6093 100644
--- a/homeassistant/components/nut/translations/de.json
+++ b/homeassistant/components/nut/translations/de.json
@@ -4,7 +4,7 @@
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"unknown": "Unerwarteter Fehler"
},
"step": {
@@ -33,6 +33,10 @@
}
},
"options": {
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "unknown": "Unerwarteter Fehler"
+ },
"step": {
"init": {
"data": {
diff --git a/homeassistant/components/nut/translations/en.json b/homeassistant/components/nut/translations/en.json
index 2e5db79d81c328..3d57189f7a5deb 100644
--- a/homeassistant/components/nut/translations/en.json
+++ b/homeassistant/components/nut/translations/en.json
@@ -33,6 +33,10 @@
}
},
"options": {
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "unknown": "Unexpected error"
+ },
"step": {
"init": {
"data": {
diff --git a/homeassistant/components/nut/translations/et.json b/homeassistant/components/nut/translations/et.json
index 27745bfeeae378..83d793eb22c4ce 100644
--- a/homeassistant/components/nut/translations/et.json
+++ b/homeassistant/components/nut/translations/et.json
@@ -33,6 +33,10 @@
}
},
"options": {
+ "error": {
+ "cannot_connect": "\u00dchendus nurjus",
+ "unknown": "Tundmatu viga"
+ },
"step": {
"init": {
"data": {
diff --git a/homeassistant/components/nut/translations/fr.json b/homeassistant/components/nut/translations/fr.json
index d91bd1e4070f9b..35739689425f84 100644
--- a/homeassistant/components/nut/translations/fr.json
+++ b/homeassistant/components/nut/translations/fr.json
@@ -33,6 +33,10 @@
}
},
"options": {
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "unknown": "Erreur inattendue"
+ },
"step": {
"init": {
"data": {
diff --git a/homeassistant/components/nut/translations/he.json b/homeassistant/components/nut/translations/he.json
new file mode 100644
index 00000000000000..ac90b3264eab33
--- /dev/null
+++ b/homeassistant/components/nut/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nut/translations/hu.json b/homeassistant/components/nut/translations/hu.json
index 1ca56c7684f069..a7bad455dc3837 100644
--- a/homeassistant/components/nut/translations/hu.json
+++ b/homeassistant/components/nut/translations/hu.json
@@ -1,5 +1,12 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
"step": {
"user": {
"data": {
@@ -10,5 +17,11 @@
}
}
}
+ },
+ "options": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nut/translations/id.json b/homeassistant/components/nut/translations/id.json
new file mode 100644
index 00000000000000..fc23e34fe8e0d8
--- /dev/null
+++ b/homeassistant/components/nut/translations/id.json
@@ -0,0 +1,50 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "resources": {
+ "data": {
+ "resources": "Sumber Daya"
+ },
+ "title": "Pilih Sumber Daya untuk Dipantau"
+ },
+ "ups": {
+ "data": {
+ "alias": "Alias",
+ "resources": "Sumber Daya"
+ },
+ "title": "Pilih UPS untuk Dipantau"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "username": "Nama Pengguna"
+ },
+ "title": "Hubungkan ke server NUT"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "resources": "Sumber Daya",
+ "scan_interval": "Interval Pindai (detik)"
+ },
+ "description": "Pilih Sumber Daya Sensor."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nut/translations/it.json b/homeassistant/components/nut/translations/it.json
index 440cb421504a47..cb8949fca641b0 100644
--- a/homeassistant/components/nut/translations/it.json
+++ b/homeassistant/components/nut/translations/it.json
@@ -33,6 +33,10 @@
}
},
"options": {
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "unknown": "Errore imprevisto"
+ },
"step": {
"init": {
"data": {
diff --git a/homeassistant/components/nut/translations/ko.json b/homeassistant/components/nut/translations/ko.json
index 81fe8d88a90e61..a5680f4ed48887 100644
--- a/homeassistant/components/nut/translations/ko.json
+++ b/homeassistant/components/nut/translations/ko.json
@@ -4,7 +4,7 @@
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
@@ -33,13 +33,17 @@
}
},
"options": {
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
"step": {
"init": {
"data": {
"resources": "\ub9ac\uc18c\uc2a4",
"scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ucd08)"
},
- "description": "\uc13c\uc11c \ub9ac\uc18c\uc2a4 \uc120\ud0dd"
+ "description": "\uc13c\uc11c \ub9ac\uc18c\uc2a4\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694."
}
}
}
diff --git a/homeassistant/components/nut/translations/nl.json b/homeassistant/components/nut/translations/nl.json
index 5e4acf3574d986..d90b75b4bccbe6 100644
--- a/homeassistant/components/nut/translations/nl.json
+++ b/homeassistant/components/nut/translations/nl.json
@@ -4,7 +4,7 @@
"already_configured": "Apparaat is al geconfigureerd"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Kan geen verbinding maken",
"unknown": "Onverwachte fout"
},
"step": {
@@ -33,6 +33,10 @@
}
},
"options": {
+ "error": {
+ "cannot_connect": "Verbinden mislukt",
+ "unknown": "Onverwachte fout"
+ },
"step": {
"init": {
"data": {
diff --git a/homeassistant/components/nut/translations/no.json b/homeassistant/components/nut/translations/no.json
index 0af64ad4fa46c1..887d3b6d30af7a 100644
--- a/homeassistant/components/nut/translations/no.json
+++ b/homeassistant/components/nut/translations/no.json
@@ -33,6 +33,10 @@
}
},
"options": {
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "unknown": "Uventet feil"
+ },
"step": {
"init": {
"data": {
diff --git a/homeassistant/components/nut/translations/pl.json b/homeassistant/components/nut/translations/pl.json
index 240b32bfe192c9..2686a98e69779f 100644
--- a/homeassistant/components/nut/translations/pl.json
+++ b/homeassistant/components/nut/translations/pl.json
@@ -33,6 +33,10 @@
}
},
"options": {
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
"step": {
"init": {
"data": {
diff --git a/homeassistant/components/nut/translations/pt.json b/homeassistant/components/nut/translations/pt.json
index a856ef0aeede81..f5e8690e3839c6 100644
--- a/homeassistant/components/nut/translations/pt.json
+++ b/homeassistant/components/nut/translations/pt.json
@@ -22,5 +22,11 @@
}
}
}
+ },
+ "options": {
+ "error": {
+ "cannot_connect": "Falha na liga\u00e7\u00e3o",
+ "unknown": "Erro inesperado"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nut/translations/ru.json b/homeassistant/components/nut/translations/ru.json
index 7a3f1c9b47e0fb..071a7d0f09c82c 100644
--- a/homeassistant/components/nut/translations/ru.json
+++ b/homeassistant/components/nut/translations/ru.json
@@ -26,13 +26,17 @@
"host": "\u0425\u043e\u0441\u0442",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 NUT"
}
}
},
"options": {
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
"step": {
"init": {
"data": {
diff --git a/homeassistant/components/nut/translations/tr.json b/homeassistant/components/nut/translations/tr.json
new file mode 100644
index 00000000000000..b383d7656191ff
--- /dev/null
+++ b/homeassistant/components/nut/translations/tr.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "resources": {
+ "data": {
+ "resources": "Kaynaklar"
+ }
+ },
+ "ups": {
+ "data": {
+ "alias": "Takma ad"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "password": "Parola",
+ "port": "Port",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "resources": "Kaynaklar"
+ },
+ "description": "Sens\u00f6r Kaynaklar\u0131'n\u0131 se\u00e7in."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nut/translations/uk.json b/homeassistant/components/nut/translations/uk.json
new file mode 100644
index 00000000000000..b25fe854560190
--- /dev/null
+++ b/homeassistant/components/nut/translations/uk.json
@@ -0,0 +1,46 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "resources": {
+ "data": {
+ "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438"
+ },
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0440\u0435\u0441\u0443\u0440\u0441\u0438 \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443"
+ },
+ "ups": {
+ "data": {
+ "alias": "\u041f\u0441\u0435\u0432\u0434\u043e\u043d\u0456\u043c",
+ "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438"
+ },
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c UPS \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 NUT"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438",
+ "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0440\u0435\u0441\u0443\u0440\u0441\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nut/translations/zh-Hans.json b/homeassistant/components/nut/translations/zh-Hans.json
index a5f4ff11f09ef8..91522c7f609f39 100644
--- a/homeassistant/components/nut/translations/zh-Hans.json
+++ b/homeassistant/components/nut/translations/zh-Hans.json
@@ -1,11 +1,22 @@
{
"config": {
"step": {
+ "resources": {
+ "data": {
+ "resources": "\u8d44\u6e90"
+ }
+ },
"user": {
"data": {
"username": "\u7528\u6237\u540d"
}
}
}
+ },
+ "options": {
+ "error": {
+ "cannot_connect": "\u8fde\u63a5\u5931\u8d25",
+ "unknown": "\u4e0d\u5728\u9884\u671f\u5185\u7684\u9519\u8bef"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nut/translations/zh-Hant.json b/homeassistant/components/nut/translations/zh-Hant.json
index 7c65e836f9e067..822d2e785f22ce 100644
--- a/homeassistant/components/nut/translations/zh-Hant.json
+++ b/homeassistant/components/nut/translations/zh-Hant.json
@@ -33,6 +33,10 @@
}
},
"options": {
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
"step": {
"init": {
"data": {
diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py
index a0958be8d9e55c..9cdf17fa264acb 100644
--- a/homeassistant/components/nws/__init__.py
+++ b/homeassistant/components/nws/__init__.py
@@ -1,8 +1,10 @@
"""The National Weather Service integration."""
+from __future__ import annotations
+
import asyncio
import datetime
import logging
-from typing import Awaitable, Callable, Optional
+from typing import Awaitable, Callable
from pynws import SimpleNWS
@@ -26,7 +28,7 @@
_LOGGER = logging.getLogger(__name__)
-PLATFORMS = ["weather"]
+PLATFORMS = ["sensor", "weather"]
DEFAULT_SCAN_INTERVAL = datetime.timedelta(minutes=10)
FAILED_SCAN_INTERVAL = datetime.timedelta(minutes=1)
@@ -58,8 +60,8 @@ def __init__(
name: str,
update_interval: datetime.timedelta,
failed_update_interval: datetime.timedelta,
- update_method: Optional[Callable[[], Awaitable]] = None,
- request_refresh_debouncer: Optional[debounce.Debouncer] = None,
+ update_method: Callable[[], Awaitable] | None = None,
+ request_refresh_debouncer: debounce.Debouncer | None = None,
):
"""Initialize NWS coordinator."""
super().__init__(
@@ -157,9 +159,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
await coordinator_forecast.async_refresh()
await coordinator_forecast_hourly.async_refresh()
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -169,8 +171,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/nws/config_flow.py b/homeassistant/components/nws/config_flow.py
index 12ab09abaae6c6..cfe43f3a528943 100644
--- a/homeassistant/components/nws/config_flow.py
+++ b/homeassistant/components/nws/config_flow.py
@@ -11,7 +11,7 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import base_unique_id
-from .const import CONF_STATION, DOMAIN # pylint:disable=unused-import
+from .const import CONF_STATION, DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py
index 574ad6925ac906..f82a70ea4e0ff2 100644
--- a/homeassistant/components/nws/const.py
+++ b/homeassistant/components/nws/const.py
@@ -1,4 +1,6 @@
"""Constants for National Weather Service Integration."""
+from datetime import timedelta
+
from homeassistant.components.weather import (
ATTR_CONDITION_CLOUDY,
ATTR_CONDITION_EXCEPTIONAL,
@@ -14,6 +16,21 @@
ATTR_CONDITION_WINDY,
ATTR_CONDITION_WINDY_VARIANT,
)
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ DEGREE,
+ DEVICE_CLASS_HUMIDITY,
+ DEVICE_CLASS_PRESSURE,
+ DEVICE_CLASS_TEMPERATURE,
+ LENGTH_METERS,
+ LENGTH_MILES,
+ PERCENTAGE,
+ PRESSURE_INHG,
+ PRESSURE_PA,
+ SPEED_KILOMETERS_PER_HOUR,
+ SPEED_MILES_PER_HOUR,
+ TEMP_CELSIUS,
+)
DOMAIN = "nws"
@@ -23,6 +40,11 @@
ATTR_FORECAST_DETAILED_DESCRIPTION = "detailed_description"
ATTR_FORECAST_DAYTIME = "daytime"
+ATTR_ICON = "icon"
+ATTR_LABEL = "label"
+ATTR_UNIT = "unit"
+ATTR_UNIT_CONVERT = "unit_convert"
+ATTR_UNIT_CONVERT_METHOD = "unit_convert_method"
CONDITION_CLASSES = {
ATTR_CONDITION_EXCEPTIONAL: [
@@ -35,7 +57,7 @@
"Hot",
"Cold",
],
- ATTR_CONDITION_SNOWY: ["Snow", "Sleet", "Blizzard"],
+ ATTR_CONDITION_SNOWY: ["Snow", "Sleet", "Snow/sleet", "Blizzard"],
ATTR_CONDITION_SNOWY_RAINY: [
"Rain/snow",
"Rain/sleet",
@@ -75,3 +97,86 @@
COORDINATOR_OBSERVATION = "coordinator_observation"
COORDINATOR_FORECAST = "coordinator_forecast"
COORDINATOR_FORECAST_HOURLY = "coordinator_forecast_hourly"
+
+OBSERVATION_VALID_TIME = timedelta(minutes=20)
+FORECAST_VALID_TIME = timedelta(minutes=45)
+
+SENSOR_TYPES = {
+ "dewpoint": {
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
+ ATTR_ICON: None,
+ ATTR_LABEL: "Dew Point",
+ ATTR_UNIT: TEMP_CELSIUS,
+ ATTR_UNIT_CONVERT: TEMP_CELSIUS,
+ },
+ "temperature": {
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
+ ATTR_ICON: None,
+ ATTR_LABEL: "Temperature",
+ ATTR_UNIT: TEMP_CELSIUS,
+ ATTR_UNIT_CONVERT: TEMP_CELSIUS,
+ },
+ "windChill": {
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
+ ATTR_ICON: None,
+ ATTR_LABEL: "Wind Chill",
+ ATTR_UNIT: TEMP_CELSIUS,
+ ATTR_UNIT_CONVERT: TEMP_CELSIUS,
+ },
+ "heatIndex": {
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
+ ATTR_ICON: None,
+ ATTR_LABEL: "Heat Index",
+ ATTR_UNIT: TEMP_CELSIUS,
+ ATTR_UNIT_CONVERT: TEMP_CELSIUS,
+ },
+ "relativeHumidity": {
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
+ ATTR_ICON: None,
+ ATTR_LABEL: "Relative Humidity",
+ ATTR_UNIT: PERCENTAGE,
+ ATTR_UNIT_CONVERT: PERCENTAGE,
+ },
+ "windSpeed": {
+ ATTR_DEVICE_CLASS: None,
+ ATTR_ICON: "mdi:weather-windy",
+ ATTR_LABEL: "Wind Speed",
+ ATTR_UNIT: SPEED_KILOMETERS_PER_HOUR,
+ ATTR_UNIT_CONVERT: SPEED_MILES_PER_HOUR,
+ },
+ "windGust": {
+ ATTR_DEVICE_CLASS: None,
+ ATTR_ICON: "mdi:weather-windy",
+ ATTR_LABEL: "Wind Gust",
+ ATTR_UNIT: SPEED_KILOMETERS_PER_HOUR,
+ ATTR_UNIT_CONVERT: SPEED_MILES_PER_HOUR,
+ },
+ "windDirection": {
+ ATTR_DEVICE_CLASS: None,
+ ATTR_ICON: "mdi:compass-rose",
+ ATTR_LABEL: "Wind Direction",
+ ATTR_UNIT: DEGREE,
+ ATTR_UNIT_CONVERT: DEGREE,
+ },
+ "barometricPressure": {
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
+ ATTR_ICON: None,
+ ATTR_LABEL: "Barometric Pressure",
+ ATTR_UNIT: PRESSURE_PA,
+ ATTR_UNIT_CONVERT: PRESSURE_INHG,
+ },
+ "seaLevelPressure": {
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
+ ATTR_ICON: None,
+ ATTR_LABEL: "Sea Level Pressure",
+ ATTR_UNIT: PRESSURE_PA,
+ ATTR_UNIT_CONVERT: PRESSURE_INHG,
+ },
+ "visibility": {
+ ATTR_DEVICE_CLASS: None,
+ ATTR_ICON: "mdi:eye",
+ ATTR_LABEL: "Visibility",
+ ATTR_UNIT: LENGTH_METERS,
+ ATTR_UNIT_CONVERT: LENGTH_MILES,
+ },
+}
diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py
new file mode 100644
index 00000000000000..bff5cdca589549
--- /dev/null
+++ b/homeassistant/components/nws/sensor.py
@@ -0,0 +1,156 @@
+"""Sensors for National Weather Service (NWS)."""
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.const import (
+ ATTR_ATTRIBUTION,
+ ATTR_DEVICE_CLASS,
+ CONF_LATITUDE,
+ CONF_LONGITUDE,
+ LENGTH_KILOMETERS,
+ LENGTH_METERS,
+ LENGTH_MILES,
+ PERCENTAGE,
+ PRESSURE_INHG,
+ PRESSURE_PA,
+ SPEED_MILES_PER_HOUR,
+ TEMP_CELSIUS,
+)
+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 . import base_unique_id
+from .const import (
+ ATTR_ICON,
+ ATTR_LABEL,
+ ATTR_UNIT,
+ ATTR_UNIT_CONVERT,
+ ATTRIBUTION,
+ CONF_STATION,
+ COORDINATOR_OBSERVATION,
+ DOMAIN,
+ NWS_DATA,
+ OBSERVATION_VALID_TIME,
+ SENSOR_TYPES,
+)
+
+PARALLEL_UPDATES = 0
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up the NWS weather platform."""
+ hass_data = hass.data[DOMAIN][entry.entry_id]
+ station = entry.data[CONF_STATION]
+
+ entities = []
+ for sensor_type, sensor_data in SENSOR_TYPES.items():
+ if hass.config.units.is_metric:
+ unit = sensor_data[ATTR_UNIT]
+ else:
+ unit = sensor_data[ATTR_UNIT_CONVERT]
+ entities.append(
+ NWSSensor(
+ entry.data,
+ hass_data,
+ sensor_type,
+ station,
+ sensor_data[ATTR_LABEL],
+ sensor_data[ATTR_ICON],
+ sensor_data[ATTR_DEVICE_CLASS],
+ unit,
+ ),
+ )
+
+ async_add_entities(entities, False)
+
+
+class NWSSensor(CoordinatorEntity, SensorEntity):
+ """An NWS Sensor Entity."""
+
+ def __init__(
+ self,
+ entry_data,
+ hass_data,
+ sensor_type,
+ station,
+ label,
+ icon,
+ device_class,
+ unit,
+ ):
+ """Initialise the platform with a data instance."""
+ super().__init__(hass_data[COORDINATOR_OBSERVATION])
+ self._nws = hass_data[NWS_DATA]
+ self._latitude = entry_data[CONF_LATITUDE]
+ self._longitude = entry_data[CONF_LONGITUDE]
+ self._type = sensor_type
+ self._station = station
+ self._label = label
+ self._icon = icon
+ self._device_class = device_class
+ self._unit = unit
+
+ @property
+ def state(self):
+ """Return the state."""
+ value = self._nws.observation.get(self._type)
+ if value is None:
+ return None
+ if self._unit == SPEED_MILES_PER_HOUR:
+ return round(convert_distance(value, LENGTH_KILOMETERS, LENGTH_MILES))
+ if self._unit == LENGTH_MILES:
+ return round(convert_distance(value, LENGTH_METERS, LENGTH_MILES))
+ if self._unit == PRESSURE_INHG:
+ return round(convert_pressure(value, PRESSURE_PA, PRESSURE_INHG), 2)
+ if self._unit == TEMP_CELSIUS:
+ return round(value, 1)
+ if self._unit == PERCENTAGE:
+ return round(value)
+ return value
+
+ @property
+ def icon(self):
+ """Return the icon."""
+ return self._icon
+
+ @property
+ def device_class(self):
+ """Return the device class."""
+ return self._device_class
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit the value is expressed in."""
+ return self._unit
+
+ @property
+ def device_state_attributes(self):
+ """Return the attribution."""
+ return {ATTR_ATTRIBUTION: ATTRIBUTION}
+
+ @property
+ def name(self):
+ """Return the name of the station."""
+ return f"{self._station} {self._label}"
+
+ @property
+ def unique_id(self):
+ """Return a unique_id for this entity."""
+ return f"{base_unique_id(self._latitude, self._longitude)}_{self._type}"
+
+ @property
+ def available(self):
+ """Return if state is available."""
+ if self.coordinator.last_update_success_time:
+ last_success_time = (
+ utcnow() - self.coordinator.last_update_success_time
+ < OBSERVATION_VALID_TIME
+ )
+ else:
+ last_success_time = False
+ return self.coordinator.last_update_success or last_success_time
+
+ @property
+ def entity_registry_enabled_default(self) -> bool:
+ """Return if the entity should be enabled when first added to the entity registry."""
+ return False
diff --git a/homeassistant/components/nws/translations/de.json b/homeassistant/components/nws/translations/de.json
index 1461d86b2e5665..3d409bf885b4b3 100644
--- a/homeassistant/components/nws/translations/de.json
+++ b/homeassistant/components/nws/translations/de.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ "already_configured": "Der Dienst ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"unknown": "Unerwarteter Fehler"
},
"step": {
diff --git a/homeassistant/components/nws/translations/he.json b/homeassistant/components/nws/translations/he.json
new file mode 100644
index 00000000000000..4c49313d97741a
--- /dev/null
+++ b/homeassistant/components/nws/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nws/translations/hu.json b/homeassistant/components/nws/translations/hu.json
index 5f4f7bb8bee402..1d674cacc7e0c1 100644
--- a/homeassistant/components/nws/translations/hu.json
+++ b/homeassistant/components/nws/translations/hu.json
@@ -1,9 +1,18 @@
{
"config": {
+ "abort": {
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
"step": {
"user": {
"data": {
- "api_key": "API kulcs"
+ "api_key": "API kulcs",
+ "latitude": "Sz\u00e9less\u00e9g",
+ "longitude": "Hossz\u00fas\u00e1g"
}
}
}
diff --git a/homeassistant/components/nws/translations/id.json b/homeassistant/components/nws/translations/id.json
new file mode 100644
index 00000000000000..3c92ebaed64031
--- /dev/null
+++ b/homeassistant/components/nws/translations/id.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kunci API",
+ "latitude": "Lintang",
+ "longitude": "Bujur",
+ "station": "Kode stasiun METAR"
+ },
+ "description": "Jika kode stasiun METAR tidak ditentukan, informasi lintang dan bujur akan digunakan untuk menemukan stasiun terdekat. Untuk saat ini, Kunci API bisa berupa nilai sebarang. Disarankan untuk menggunakan alamat email yang valid.",
+ "title": "Hubungkan ke National Weather Service"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nws/translations/ko.json b/homeassistant/components/nws/translations/ko.json
index 552099c7193756..6cf4ca0c9c592e 100644
--- a/homeassistant/components/nws/translations/ko.json
+++ b/homeassistant/components/nws/translations/ko.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
@@ -15,7 +15,7 @@
"longitude": "\uacbd\ub3c4",
"station": "METAR \uc2a4\ud14c\uc774\uc158 \ucf54\ub4dc"
},
- "description": "METAR \uc2a4\ud14c\uc774\uc158 \ucf54\ub4dc\ub97c \uc9c0\uc815\ud558\uc9c0 \uc54a\uc73c\uba74 \uac00\uae4c\uc6b4 \uc2a4\ud14c\uc774\uc158\uc744 \ucc3e\ub294\ub370 \uc704\ub3c4\uc640 \uacbd\ub3c4\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.",
+ "description": "METAR \uc2a4\ud14c\uc774\uc158 \ucf54\ub4dc\uac00 \uc9c0\uc815\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc704\ub3c4 \ubc0f \uacbd\ub3c4\uac00 \uac00\uc7a5 \uac00\uae4c\uc6b4 \uc2a4\ud14c\uc774\uc158\uc744 \ucc3e\ub294 \ub370 \uc0ac\uc6a9\ub429\ub2c8\ub2e4. \ud604\uc7ac API Key\ub294 \uc544\ubb34 \ud0a4\ub098 \ub123\uc5b4\ub3c4 \uc0c1\uad00\uc5c6\uc2b5\ub2c8\ub2e4\ub9cc, \uc62c\ubc14\ub978 \uc774\uba54\uc77c \uc8fc\uc18c\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4.",
"title": "\ubbf8\uad6d \uae30\uc0c1\uccad\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
diff --git a/homeassistant/components/nws/translations/nl.json b/homeassistant/components/nws/translations/nl.json
index b74e6db96a21a7..5332f43f4c73f9 100644
--- a/homeassistant/components/nws/translations/nl.json
+++ b/homeassistant/components/nws/translations/nl.json
@@ -1,21 +1,21 @@
{
"config": {
"abort": {
- "already_configured": "Apparaat is al geconfigureerd"
+ "already_configured": "Service is al geconfigureerd"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Kan geen verbinding maken",
"unknown": "Onverwachte fout"
},
"step": {
"user": {
"data": {
- "api_key": "API-sleutel (e-mail)",
+ "api_key": "API-sleutel",
"latitude": "Breedtegraad",
"longitude": "Lengtegraad",
"station": "METAR-zendercode"
},
- "description": "Als er geen METAR-zendercode is opgegeven, worden de lengte- en breedtegraad gebruikt om het dichtstbijzijnde station te vinden.",
+ "description": "Als er geen METAR-stationscode is opgegeven, worden de lengte- en breedtegraad gebruikt om het dichtstbijzijnde station te vinden. Voorlopig kan een API-sleutel van alles zijn. Het wordt aanbevolen om een geldig e-mailadres te gebruiken.",
"title": "Maak verbinding met de National Weather Service"
}
}
diff --git a/homeassistant/components/nws/translations/tr.json b/homeassistant/components/nws/translations/tr.json
new file mode 100644
index 00000000000000..8f51593aedbacf
--- /dev/null
+++ b/homeassistant/components/nws/translations/tr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Anahtar\u0131",
+ "latitude": "Enlem",
+ "longitude": "Boylam"
+ },
+ "description": "Bir METAR istasyon kodu belirtilmezse, en yak\u0131n istasyonu bulmak i\u00e7in enlem ve boylam kullan\u0131lacakt\u0131r. \u015eimdilik bir API Anahtar\u0131 herhangi bir \u015fey olabilir. Ge\u00e7erli bir e-posta adresi kullanman\u0131z tavsiye edilir."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nws/translations/uk.json b/homeassistant/components/nws/translations/uk.json
new file mode 100644
index 00000000000000..1e6886540ae897
--- /dev/null
+++ b/homeassistant/components/nws/translations/uk.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
+ "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430",
+ "station": "\u041a\u043e\u0434 \u0441\u0442\u0430\u043d\u0446\u0456\u0457 METAR"
+ },
+ "description": "\u042f\u043a\u0449\u043e \u043a\u043e\u0434 \u0441\u0442\u0430\u043d\u0446\u0456\u0457 METAR \u043d\u0435 \u0432\u043a\u0430\u0437\u0430\u043d\u043e, \u0434\u043b\u044f \u043f\u043e\u0448\u0443\u043a\u0443 \u043d\u0430\u0439\u0431\u043b\u0438\u0436\u0447\u043e\u0457 \u0441\u0442\u0430\u043d\u0446\u0456\u0457 \u0431\u0443\u0434\u0443\u0442\u044c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0448\u0438\u0440\u043e\u0442\u0430 \u0456 \u0434\u043e\u0432\u0433\u043e\u0442\u0430. \u041d\u0430 \u0434\u0430\u043d\u0438\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u043a\u043b\u044e\u0447 API \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0431\u0443\u0434\u044c-\u044f\u043a\u0438\u043c. \u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0454\u0442\u044c\u0441\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u0456\u044e\u0447\u0443 \u0430\u0434\u0440\u0435\u0441\u0443 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438.",
+ "title": "National Weather Service"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py
index 34c2909188f1a2..c84d1b78ea2b75 100644
--- a/homeassistant/components/nws/weather.py
+++ b/homeassistant/components/nws/weather.py
@@ -1,7 +1,4 @@
"""Support for NWS weather service."""
-from datetime import timedelta
-import logging
-
from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_SUNNY,
@@ -43,17 +40,14 @@
COORDINATOR_OBSERVATION,
DAYNIGHT,
DOMAIN,
+ FORECAST_VALID_TIME,
HOURLY,
NWS_DATA,
+ OBSERVATION_VALID_TIME,
)
-_LOGGER = logging.getLogger(__name__)
-
PARALLEL_UPDATES = 0
-OBSERVATION_VALID_TIME = timedelta(minutes=20)
-FORECAST_VALID_TIME = timedelta(minutes=45)
-
def convert_condition(time, weather):
"""
diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py
index 2db3531f879555..058ac6c5795269 100644
--- a/homeassistant/components/nx584/binary_sensor.py
+++ b/homeassistant/components/nx584/binary_sensor.py
@@ -105,7 +105,7 @@ def is_on(self):
return self._zone["state"]
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {"zone_number": self._zone["number"]}
diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py
index eba5eb58baf712..48abe597f5aeb0 100644
--- a/homeassistant/components/nzbget/__init__.py
+++ b/homeassistant/components/nzbget/__init__.py
@@ -13,7 +13,6 @@
CONF_SSL,
CONF_USERNAME,
)
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -95,10 +94,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
options=entry.options,
)
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
undo_listener = entry.add_update_listener(_async_update_listener)
@@ -107,9 +103,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
DATA_UNDO_UPDATE_LISTENER: undo_listener,
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
_async_register_services(hass, coordinator)
@@ -122,8 +118,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py
index f593eeb0729c4c..a352c4df6ed471 100644
--- a/homeassistant/components/nzbget/config_flow.py
+++ b/homeassistant/components/nzbget/config_flow.py
@@ -1,6 +1,8 @@
"""Config flow for NZBGet."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict, Optional
+from typing import Any
import voluptuous as vol
@@ -24,14 +26,14 @@
DEFAULT_SCAN_INTERVAL,
DEFAULT_SSL,
DEFAULT_VERIFY_SSL,
+ DOMAIN,
)
-from .const import DOMAIN # pylint: disable=unused-import
from .coordinator import NZBGetAPI, NZBGetAPIException
_LOGGER = logging.getLogger(__name__)
-def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]:
+def validate_input(hass: HomeAssistantType, data: dict) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
@@ -63,8 +65,8 @@ def async_get_options_flow(config_entry):
return NZBGetOptionsFlowHandler(config_entry)
async def async_step_import(
- self, user_input: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, user_input: ConfigType | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initiated by configuration file."""
if CONF_SCAN_INTERVAL in user_input:
user_input[CONF_SCAN_INTERVAL] = user_input[CONF_SCAN_INTERVAL].seconds
@@ -72,8 +74,8 @@ async def async_step_import(
return await self.async_step_user(user_input)
async def async_step_user(
- self, user_input: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, user_input: ConfigType | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initiated by the user."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
@@ -127,7 +129,7 @@ def __init__(self, config_entry):
"""Initialize options flow."""
self.config_entry = config_entry
- async def async_step_init(self, user_input: Optional[ConfigType] = None):
+ async def async_step_init(self, user_input: ConfigType | None = None):
"""Manage NZBGet options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py
index b4133e7550d985..54a88c89f5311e 100644
--- a/homeassistant/components/nzbget/sensor.py
+++ b/homeassistant/components/nzbget/sensor.py
@@ -1,8 +1,11 @@
"""Monitor the NZBGet API."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Callable, List, Optional
+from typing import Callable
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_NAME,
@@ -41,7 +44,7 @@
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up NZBGet sensor based on a config entry."""
coordinator: NZBGetDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
@@ -64,7 +67,7 @@ async def async_setup_entry(
async_add_entities(sensors)
-class NZBGetSensor(NZBGetEntity):
+class NZBGetSensor(NZBGetEntity, SensorEntity):
"""Representation of a NZBGet sensor."""
def __init__(
@@ -74,7 +77,7 @@ def __init__(
entry_name: str,
sensor_type: str,
sensor_name: str,
- unit_of_measurement: Optional[str] = None,
+ unit_of_measurement: str | None = None,
):
"""Initialize a new NZBGet sensor."""
self._sensor_type = sensor_type
diff --git a/homeassistant/components/nzbget/switch.py b/homeassistant/components/nzbget/switch.py
index c4ceaab5ded353..4f0eae17c23dc1 100644
--- a/homeassistant/components/nzbget/switch.py
+++ b/homeassistant/components/nzbget/switch.py
@@ -1,5 +1,7 @@
"""Support for NZBGet switches."""
-from typing import Callable, List
+from __future__ import annotations
+
+from typing import Callable
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
@@ -15,7 +17,7 @@
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up NZBGet sensor based on a config entry."""
coordinator: NZBGetDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
diff --git a/homeassistant/components/nzbget/translations/de.json b/homeassistant/components/nzbget/translations/de.json
index 018f3870c58693..529eff3d9a2a53 100644
--- a/homeassistant/components/nzbget/translations/de.json
+++ b/homeassistant/components/nzbget/translations/de.json
@@ -1,8 +1,12 @@
{
"config": {
"abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.",
"unknown": "Unerwarteter Fehler"
},
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
"flow_title": "NZBGet: {name}",
"step": {
"user": {
@@ -11,7 +15,9 @@
"name": "Name",
"password": "Passwort",
"port": "Port",
- "username": "Benutzername"
+ "ssl": "Nutzt ein SSL-Zertifikat",
+ "username": "Benutzername",
+ "verify_ssl": "SSL-Zertifikat verfizieren"
},
"title": "Mit NZBGet verbinden"
}
diff --git a/homeassistant/components/nzbget/translations/hu.json b/homeassistant/components/nzbget/translations/hu.json
new file mode 100644
index 00000000000000..9eee6cc3be60dc
--- /dev/null
+++ b/homeassistant/components/nzbget/translations/hu.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "flow_title": "NZBGet: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "name": "N\u00e9v",
+ "password": "Jelsz\u00f3",
+ "port": "Port",
+ "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v",
+ "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se"
+ },
+ "title": "Csatlakoz\u00e1s az NZBGet-hez"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g (m\u00e1sodpercben)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nzbget/translations/id.json b/homeassistant/components/nzbget/translations/id.json
new file mode 100644
index 00000000000000..af096f4ef5fd53
--- /dev/null
+++ b/homeassistant/components/nzbget/translations/id.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "flow_title": "NZBGet: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nama",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "ssl": "Menggunakan sertifikat SSL",
+ "username": "Nama Pengguna",
+ "verify_ssl": "Verifikasi sertifikat SSL"
+ },
+ "title": "Hubungkan ke NZBGet"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Frekuensi pembaruan (dalam detik)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nzbget/translations/ko.json b/homeassistant/components/nzbget/translations/ko.json
index dd53d52a236fc1..0de5bbb3dd80e0 100644
--- a/homeassistant/components/nzbget/translations/ko.json
+++ b/homeassistant/components/nzbget/translations/ko.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub428. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.",
- "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328"
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
"flow_title": "NZBGet : {name}",
"step": {
@@ -13,13 +13,13 @@
"data": {
"host": "\ud638\uc2a4\ud2b8",
"name": "\uc774\ub984",
- "password": "\uc554\ud638",
+ "password": "\ube44\ubc00\ubc88\ud638",
"port": "\ud3ec\ud2b8",
- "ssl": "NZBGet\uc740 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.",
- "username": "\uc0ac\uc6a9\uc790\uba85",
- "verify_ssl": "NZBGet\uc740 \uc801\uc808\ud55c \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4."
+ "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984",
+ "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778"
},
- "title": "NZBGet\uc5d0 \uc5f0\uacb0"
+ "title": "NZBGet\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
},
@@ -27,7 +27,7 @@
"step": {
"init": {
"data": {
- "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 (\ucd08)"
+ "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4 (\ucd08)"
}
}
}
diff --git a/homeassistant/components/nzbget/translations/nl.json b/homeassistant/components/nzbget/translations/nl.json
index f5f1bfd39ed9f1..89d58d14292ae5 100644
--- a/homeassistant/components/nzbget/translations/nl.json
+++ b/homeassistant/components/nzbget/translations/nl.json
@@ -11,6 +11,7 @@
"step": {
"user": {
"data": {
+ "host": "Host",
"name": "Naam",
"password": "Wachtwoord",
"port": "Poort",
diff --git a/homeassistant/components/nzbget/translations/ru.json b/homeassistant/components/nzbget/translations/ru.json
index e4f0a44fbcc58a..4c5d73795270d8 100644
--- a/homeassistant/components/nzbget/translations/ru.json
+++ b/homeassistant/components/nzbget/translations/ru.json
@@ -16,7 +16,7 @@
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
"ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
- "username": "\u041b\u043e\u0433\u0438\u043d",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f",
"verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL"
},
"title": "NZBGet"
diff --git a/homeassistant/components/nzbget/translations/tr.json b/homeassistant/components/nzbget/translations/tr.json
new file mode 100644
index 00000000000000..63b6c489018479
--- /dev/null
+++ b/homeassistant/components/nzbget/translations/tr.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.",
+ "unknown": "Beklenmeyen hata"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "port": "Port",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "G\u00fcncelle\u015ftirme s\u0131kl\u0131\u011f\u0131 (saniye)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nzbget/translations/uk.json b/homeassistant/components/nzbget/translations/uk.json
new file mode 100644
index 00000000000000..eba15cca19c175
--- /dev/null
+++ b/homeassistant/components/nzbget/translations/uk.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "flow_title": "NZBGet: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430",
+ "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL"
+ },
+ "title": "NZBGet"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py
index 4bf6b395d5f58e..71af8dacba2e9d 100644
--- a/homeassistant/components/oasa_telematics/sensor.py
+++ b/homeassistant/components/oasa_telematics/sensor.py
@@ -6,10 +6,9 @@
import oasatelematics
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TIMESTAMP
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -52,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([OASATelematicsSensor(data, stop_id, route_id, name)], True)
-class OASATelematicsSensor(Entity):
+class OASATelematicsSensor(SensorEntity):
"""Implementation of the OASA Telematics sensor."""
def __init__(self, data, stop_id, route_id, name):
@@ -79,7 +78,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
params = {}
if self._times is not None:
diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py
index c105b91971da26..639b9eb332f9ce 100644
--- a/homeassistant/components/obihai/sensor.py
+++ b/homeassistant/components/obihai/sensor.py
@@ -5,7 +5,7 @@
from pyobihai import PyObihai
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
@@ -13,7 +13,6 @@
DEVICE_CLASS_TIMESTAMP,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -69,7 +68,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors)
-class ObihaiServiceSensors(Entity):
+class ObihaiServiceSensors(SensorEntity):
"""Get the status of each Obihai Lines."""
def __init__(self, pyobihai, serial, service_name):
@@ -147,9 +146,8 @@ def update(self):
services = self._pyobihai.get_line_state()
- if services is not None:
- if self._service_name in services:
- self._state = services.get(self._service_name)
+ if services is not None and self._service_name in services:
+ self._state = services.get(self._service_name)
call_direction = self._pyobihai.get_call_direction()
diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py
index 6f178f2657844b..918f0258f782ef 100644
--- a/homeassistant/components/octoprint/__init__.py
+++ b/homeassistant/components/octoprint/__init__.py
@@ -6,7 +6,6 @@
import requests
import voluptuous as vol
-from homeassistant.components.discovery import SERVICE_OCTOPRINT
from homeassistant.const import (
CONF_API_KEY,
CONF_BINARY_SENSORS,
@@ -22,7 +21,6 @@
TEMP_CELSIUS,
TIME_SECONDS,
)
-from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
from homeassistant.util import slugify as util_slugify
@@ -132,12 +130,6 @@ def setup(hass, config):
printers = hass.data[DOMAIN] = {}
success = False
- def device_discovered(service, info):
- """Get called when an Octoprint server has been discovered."""
- _LOGGER.debug("Found an Octoprint server: %s", info)
-
- discovery.listen(hass, SERVICE_OCTOPRINT, device_discovered)
-
if DOMAIN not in config:
# Skip the setup if there is no configuration present
return True
@@ -220,14 +212,12 @@ def get(self, endpoint):
now = time.time()
if endpoint == "job":
last_time = self.job_last_reading[1]
- if last_time is not None:
- if now - last_time < 30.0:
- return self.job_last_reading[0]
+ if last_time is not None and now - last_time < 30.0:
+ return self.job_last_reading[0]
elif endpoint == "printer":
last_time = self.printer_last_reading[1]
- if last_time is not None:
- if now - last_time < 30.0:
- return self.printer_last_reading[0]
+ if last_time is not None and now - last_time < 30.0:
+ return self.printer_last_reading[0]
url = self.api_url + endpoint
try:
@@ -308,8 +298,7 @@ def get_value_from_json(json_dict, sensor_type, group, tool):
return json_dict[group][sensor_type]
- if tool is not None:
- if sensor_type in json_dict[group][tool]:
- return json_dict[group][tool][sensor_type]
+ if tool is not None and sensor_type in json_dict[group][tool]:
+ return json_dict[group][tool][sensor_type]
return None
diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py
index 921f355edbe223..16f6efce00447e 100644
--- a/homeassistant/components/octoprint/sensor.py
+++ b/homeassistant/components/octoprint/sensor.py
@@ -3,8 +3,8 @@
import requests
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import PERCENTAGE, TEMP_CELSIUS
-from homeassistant.helpers.entity import Entity
from . import DOMAIN as COMPONENT_DOMAIN, SENSOR_TYPES
@@ -25,18 +25,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
octoprint_api = hass.data[COMPONENT_DOMAIN][base_url]
tools = octoprint_api.get_tools()
- if "Temperatures" in monitored_conditions:
- if not tools:
- hass.components.persistent_notification.create(
- "Your printer appears to be offline.
"
- "If you do not want to have your printer on
"
- " at all times, and you would like to monitor
"
- "temperatures, please add
"
- "bed and/or number_of_tools to your configuration
"
- "and restart.",
- title=NOTIFICATION_TITLE,
- notification_id=NOTIFICATION_ID,
- )
+ if "Temperatures" in monitored_conditions and not tools:
+ hass.components.persistent_notification.create(
+ "Your printer appears to be offline.
"
+ "If you do not want to have your printer on
"
+ " at all times, and you would like to monitor
"
+ "temperatures, please add
"
+ "bed and/or number_of_tools to your configuration
"
+ "and restart.",
+ title=NOTIFICATION_TITLE,
+ notification_id=NOTIFICATION_ID,
+ )
devices = []
types = ["actual", "target"]
@@ -71,7 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(devices, True)
-class OctoPrintSensor(Entity):
+class OctoPrintSensor(SensorEntity):
"""Representation of an OctoPrint sensor."""
def __init__(
diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py
index 56a3cc0655667f..b53c35e17b5d6b 100644
--- a/homeassistant/components/ohmconnect/sensor.py
+++ b/homeassistant/components/ohmconnect/sensor.py
@@ -6,16 +6,13 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.const import CONF_ID, CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
-CONF_ID = "id"
-
DEFAULT_NAME = "OhmConnect Status"
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
@@ -36,7 +33,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([OhmconnectSensor(name, ohmid)], True)
-class OhmconnectSensor(Entity):
+class OhmconnectSensor(SensorEntity):
"""Representation of a OhmConnect sensor."""
def __init__(self, name, ohmid):
@@ -58,7 +55,7 @@ def state(self):
return "Inactive"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {"Address": self._data.get("address"), "ID": self._ohmid}
diff --git a/homeassistant/components/ombi/__init__.py b/homeassistant/components/ombi/__init__.py
index dcd8f2641612b0..5db46658eb1589 100644
--- a/homeassistant/components/ombi/__init__.py
+++ b/homeassistant/components/ombi/__init__.py
@@ -5,6 +5,7 @@
import voluptuous as vol
from homeassistant.const import (
+ ATTR_NAME,
CONF_API_KEY,
CONF_HOST,
CONF_PASSWORD,
@@ -15,7 +16,6 @@
import homeassistant.helpers.config_validation as cv
from .const import (
- ATTR_NAME,
ATTR_SEASON,
CONF_URLBASE,
DEFAULT_PORT,
diff --git a/homeassistant/components/ombi/const.py b/homeassistant/components/ombi/const.py
index 42b58e7f50d631..784b46a99b7e0f 100644
--- a/homeassistant/components/ombi/const.py
+++ b/homeassistant/components/ombi/const.py
@@ -1,5 +1,4 @@
"""Support for Ombi."""
-ATTR_NAME = "name"
ATTR_SEASON = "season"
CONF_URLBASE = "urlbase"
diff --git a/homeassistant/components/ombi/sensor.py b/homeassistant/components/ombi/sensor.py
index 2a2f50532b4e23..8c08b026b282e4 100644
--- a/homeassistant/components/ombi/sensor.py
+++ b/homeassistant/components/ombi/sensor.py
@@ -4,7 +4,7 @@
from pyombi import OmbiError
-from homeassistant.helpers.entity import Entity
+from homeassistant.components.sensor import SensorEntity
from .const import DOMAIN, SENSOR_TYPES
@@ -31,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class OmbiSensor(Entity):
+class OmbiSensor(SensorEntity):
"""Representation of an Ombi sensor."""
def __init__(self, label, sensor_type, ombi, icon):
diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py
index ff4dd93a0e1537..e5a545e480688c 100644
--- a/homeassistant/components/omnilogic/__init__.py
+++ b/homeassistant/components/omnilogic/__init__.py
@@ -56,19 +56,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
name="Omnilogic",
polling_interval=polling_interval,
)
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = {
COORDINATOR: coordinator,
OMNI_API: api,
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -79,8 +76,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/omnilogic/common.py b/homeassistant/components/omnilogic/common.py
index 791d81b6757a53..6f7ee6e5eb574f 100644
--- a/homeassistant/components/omnilogic/common.py
+++ b/homeassistant/components/omnilogic/common.py
@@ -141,7 +141,7 @@ def icon(self):
return self._icon
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the attributes."""
return self._attrs
diff --git a/homeassistant/components/omnilogic/config_flow.py b/homeassistant/components/omnilogic/config_flow.py
index 641ec5a8d94bfd..f8dffaeda44bb9 100644
--- a/homeassistant/components/omnilogic/config_flow.py
+++ b/homeassistant/components/omnilogic/config_flow.py
@@ -9,7 +9,7 @@
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
-from .const import CONF_SCAN_INTERVAL, DOMAIN # pylint:disable=unused-import
+from .const import CONF_SCAN_INTERVAL, DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/omnilogic/manifest.json b/homeassistant/components/omnilogic/manifest.json
index d999a34f0761ed..2b2a4a9fe3d318 100644
--- a/homeassistant/components/omnilogic/manifest.json
+++ b/homeassistant/components/omnilogic/manifest.json
@@ -3,6 +3,6 @@
"name": "Hayward Omnilogic",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/omnilogic",
- "requirements": ["omnilogic==0.4.2"],
+ "requirements": ["omnilogic==0.4.3"],
"codeowners": ["@oliver84","@djtimca","@gentoosu"]
}
diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py
index e1b4b387a46062..25457224e9f007 100644
--- a/homeassistant/components/omnilogic/sensor.py
+++ b/homeassistant/components/omnilogic/sensor.py
@@ -1,5 +1,5 @@
"""Definition and setup of the Omnilogic Sensors for Home Assistant."""
-from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE
+from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE, SensorEntity
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
MASS_GRAMS,
@@ -59,7 +59,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
async_add_entities(entities)
-class OmnilogicSensor(OmniLogicEntity):
+class OmnilogicSensor(OmniLogicEntity, SensorEntity):
"""Defines an Omnilogic sensor entity."""
def __init__(
diff --git a/homeassistant/components/omnilogic/translations/de.json b/homeassistant/components/omnilogic/translations/de.json
index 382156757010d5..85de80d3dfab85 100644
--- a/homeassistant/components/omnilogic/translations/de.json
+++ b/homeassistant/components/omnilogic/translations/de.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"step": {
@@ -12,5 +16,14 @@
}
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "polling_interval": "Abfrageintervall (in Sekunden)"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/hu.json b/homeassistant/components/omnilogic/translations/hu.json
new file mode 100644
index 00000000000000..129bb041b42fbd
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/hu.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "polling_interval": "Lek\u00e9rdez\u00e9si id\u0151k\u00f6z (m\u00e1sodpercben)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/id.json b/homeassistant/components/omnilogic/translations/id.json
new file mode 100644
index 00000000000000..ed19cc68cf8609
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/id.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "polling_interval": "Interval polling (dalam detik)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/ko.json b/homeassistant/components/omnilogic/translations/ko.json
index 5389207cdda909..0f3df64c00f782 100644
--- a/homeassistant/components/omnilogic/translations/ko.json
+++ b/homeassistant/components/omnilogic/translations/ko.json
@@ -1,18 +1,18 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c. \ud558\ub098\uc758 \uc124\uc815\ub9cc \uac00\ub2a5\ud568."
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"error": {
- "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
- "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d",
- "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
"data": {
- "password": "\uc554\ud638",
- "username": "\uc0ac\uc6a9\uc790\uba85"
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
}
}
}
@@ -21,7 +21,7 @@
"step": {
"init": {
"data": {
- "polling_interval": "\ud3f4\ub9c1 \uac04\uaca9(\ucd08)"
+ "polling_interval": "\ud3f4\ub9c1 \uac04\uaca9 (\ucd08)"
}
}
}
diff --git a/homeassistant/components/omnilogic/translations/ru.json b/homeassistant/components/omnilogic/translations/ru.json
index 9040654e58cebb..5b00efefa1a209 100644
--- a/homeassistant/components/omnilogic/translations/ru.json
+++ b/homeassistant/components/omnilogic/translations/ru.json
@@ -5,14 +5,14 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
}
}
}
diff --git a/homeassistant/components/omnilogic/translations/tr.json b/homeassistant/components/omnilogic/translations/tr.json
new file mode 100644
index 00000000000000..ab93b71de844f9
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/omnilogic/translations/uk.json b/homeassistant/components/omnilogic/translations/uk.json
new file mode 100644
index 00000000000000..21ebf6f4fafa26
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/uk.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "polling_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/onboarding/__init__.py b/homeassistant/components/onboarding/__init__.py
index bedfa703a9b0d2..e383e4e32c459a 100644
--- a/homeassistant/components/onboarding/__init__.py
+++ b/homeassistant/components/onboarding/__init__.py
@@ -4,10 +4,17 @@
from homeassistant.loader import bind_hass
from . import views
-from .const import DOMAIN, STEP_CORE_CONFIG, STEP_INTEGRATION, STEP_USER, STEPS
+from .const import (
+ DOMAIN,
+ STEP_ANALYTICS,
+ STEP_CORE_CONFIG,
+ STEP_INTEGRATION,
+ STEP_USER,
+ STEPS,
+)
STORAGE_KEY = DOMAIN
-STORAGE_VERSION = 3
+STORAGE_VERSION = 4
class OnboadingStorage(Store):
@@ -20,6 +27,8 @@ async def _async_migrate_func(self, old_version, old_data):
old_data["done"].append(STEP_INTEGRATION)
if old_version < 3:
old_data["done"].append(STEP_CORE_CONFIG)
+ if old_version < 4:
+ old_data["done"].append(STEP_ANALYTICS)
return old_data
diff --git a/homeassistant/components/onboarding/const.py b/homeassistant/components/onboarding/const.py
index bf350a200dee63..5a771f524ac4ae 100644
--- a/homeassistant/components/onboarding/const.py
+++ b/homeassistant/components/onboarding/const.py
@@ -3,7 +3,8 @@
STEP_USER = "user"
STEP_CORE_CONFIG = "core_config"
STEP_INTEGRATION = "integration"
+STEP_ANALYTICS = "analytics"
-STEPS = [STEP_USER, STEP_CORE_CONFIG, STEP_INTEGRATION]
+STEPS = [STEP_USER, STEP_CORE_CONFIG, STEP_ANALYTICS, STEP_INTEGRATION]
DEFAULT_AREAS = ("living_room", "kitchen", "bedroom")
diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json
index e2fb8e084b83b8..06c9946b5c9b64 100644
--- a/homeassistant/components/onboarding/manifest.json
+++ b/homeassistant/components/onboarding/manifest.json
@@ -6,6 +6,7 @@
"hassio"
],
"dependencies": [
+ "analytics",
"auth",
"http",
"person"
diff --git a/homeassistant/components/onboarding/translations/id.json b/homeassistant/components/onboarding/translations/id.json
index 33e8a88a9ae0cd..472630cacf6de8 100644
--- a/homeassistant/components/onboarding/translations/id.json
+++ b/homeassistant/components/onboarding/translations/id.json
@@ -1,5 +1,7 @@
{
"area": {
- "kitchen": "Dapur"
+ "bedroom": "Kamar Tidur",
+ "kitchen": "Dapur",
+ "living_room": "Ruang Keluarga"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/onboarding/translations/uk.json b/homeassistant/components/onboarding/translations/uk.json
new file mode 100644
index 00000000000000..595726cbd34bb8
--- /dev/null
+++ b/homeassistant/components/onboarding/translations/uk.json
@@ -0,0 +1,7 @@
+{
+ "area": {
+ "bedroom": "\u0421\u043f\u0430\u043b\u044c\u043d\u044f",
+ "kitchen": "\u041a\u0443\u0445\u043d\u044f",
+ "living_room": "\u0412\u0456\u0442\u0430\u043b\u044c\u043d\u044f"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py
index 1d5528688dd636..dec806428455dd 100644
--- a/homeassistant/components/onboarding/views.py
+++ b/homeassistant/components/onboarding/views.py
@@ -14,6 +14,7 @@
from .const import (
DEFAULT_AREAS,
DOMAIN,
+ STEP_ANALYTICS,
STEP_CORE_CONFIG,
STEP_INTEGRATION,
STEP_USER,
@@ -27,6 +28,7 @@ async def async_setup(hass, data, store):
hass.http.register_view(UserOnboardingView(data, store))
hass.http.register_view(CoreConfigOnboardingView(data, store))
hass.http.register_view(IntegrationOnboardingView(data, store))
+ hass.http.register_view(AnalyticsOnboardingView(data, store))
class OnboardingView(HomeAssistantView):
@@ -217,6 +219,28 @@ async def post(self, request, data):
return self.json({"auth_code": auth_code})
+class AnalyticsOnboardingView(_BaseOnboardingView):
+ """View to finish analytics onboarding step."""
+
+ url = "/api/onboarding/analytics"
+ name = "api:onboarding:analytics"
+ step = STEP_ANALYTICS
+
+ async def post(self, request):
+ """Handle finishing analytics step."""
+ hass = request.app["hass"]
+
+ async with self._lock:
+ if self._async_is_done():
+ return self.json_message(
+ "Analytics config step already done", HTTP_FORBIDDEN
+ )
+
+ await self._async_mark_done(hass)
+
+ return self.json({})
+
+
@callback
def _async_get_hass_provider(hass):
"""Get the Home Assistant auth provider."""
diff --git a/homeassistant/components/ondilo_ico/__init__.py b/homeassistant/components/ondilo_ico/__init__.py
index 69538c5e8b384a..4dac83815ba8e6 100644
--- a/homeassistant/components/ondilo_ico/__init__.py
+++ b/homeassistant/components/ondilo_ico/__init__.py
@@ -35,9 +35,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN][entry.entry_id] = api.OndiloClient(hass, entry, implementation)
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -48,8 +48,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/ondilo_ico/config_flow.py b/homeassistant/components/ondilo_ico/config_flow.py
index c6a164e913b120..74c668a3d2c8e9 100644
--- a/homeassistant/components/ondilo_ico/config_flow.py
+++ b/homeassistant/components/ondilo_ico/config_flow.py
@@ -7,8 +7,6 @@
from .const import DOMAIN
from .oauth_impl import OndiloOauth2Implementation
-_LOGGER = logging.getLogger(__name__)
-
class OAuth2FlowHandler(
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py
index b34ee4eae35c44..3af2bb7c326127 100644
--- a/homeassistant/components/ondilo_ico/sensor.py
+++ b/homeassistant/components/ondilo_ico/sensor.py
@@ -4,6 +4,7 @@
from ondilo import OndiloError
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
DEVICE_CLASS_BATTERY,
@@ -83,7 +84,7 @@ async def async_update_data():
async_add_entities(entities)
-class OndiloICO(CoordinatorEntity):
+class OndiloICO(CoordinatorEntity, SensorEntity):
"""Representation of a Sensor."""
def __init__(
diff --git a/homeassistant/components/ondilo_ico/translations/ca.json b/homeassistant/components/ondilo_ico/translations/ca.json
new file mode 100644
index 00000000000000..77453bda398928
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/ca.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.",
+ "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3."
+ },
+ "create_entry": {
+ "default": "Autenticaci\u00f3 exitosa"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3"
+ }
+ }
+ },
+ "title": "Ondilo ICO"
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/cs.json b/homeassistant/components/ondilo_ico/translations/cs.json
new file mode 100644
index 00000000000000..bcb8849839caab
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/cs.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el",
+ "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace."
+ },
+ "create_entry": {
+ "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Vyberte metodu ov\u011b\u0159en\u00ed"
+ }
+ }
+ },
+ "title": "Ondilo ICO"
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/de.json b/homeassistant/components/ondilo_ico/translations/de.json
new file mode 100644
index 00000000000000..ad11cefde66ec9
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/de.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.",
+ "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen."
+ },
+ "create_entry": {
+ "default": "Erfolgreich authentifiziert"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "W\u00e4hle die Authentifizierungsmethode"
+ }
+ }
+ },
+ "title": "Ondilo ICO"
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/en.json b/homeassistant/components/ondilo_ico/translations/en.json
index e3849fc17a3aab..c88a152ef81fcf 100644
--- a/homeassistant/components/ondilo_ico/translations/en.json
+++ b/homeassistant/components/ondilo_ico/translations/en.json
@@ -12,5 +12,6 @@
"title": "Pick Authentication Method"
}
}
- }
+ },
+ "title": "Ondilo ICO"
}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/es.json b/homeassistant/components/ondilo_ico/translations/es.json
new file mode 100644
index 00000000000000..2394c610796eaa
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/es.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.",
+ "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n."
+ },
+ "create_entry": {
+ "default": "Autenticado correctamente"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n"
+ }
+ }
+ },
+ "title": "Ondilo ICO"
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/et.json b/homeassistant/components/ondilo_ico/translations/et.json
new file mode 100644
index 00000000000000..132e9849cf104f
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/et.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp",
+ "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni."
+ },
+ "create_entry": {
+ "default": "Tuvastamine \u00f5nnestus"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Vali tuvastusmeetod"
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/fr.json b/homeassistant/components/ondilo_ico/translations/fr.json
new file mode 100644
index 00000000000000..c05fc0caaa6222
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/fr.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.",
+ "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation."
+ },
+ "create_entry": {
+ "default": "Authentification r\u00e9ussie"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "S\u00e9lectionner une m\u00e9thode d'authentification"
+ }
+ }
+ },
+ "title": "Ondilo ICO"
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/hu.json b/homeassistant/components/ondilo_ico/translations/hu.json
new file mode 100644
index 00000000000000..cae1f6d20c0393
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/hu.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.",
+ "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t."
+ },
+ "create_entry": {
+ "default": "Sikeres hiteles\u00edt\u00e9s"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/id.json b/homeassistant/components/ondilo_ico/translations/id.json
new file mode 100644
index 00000000000000..1227a6d6689dc4
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/id.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.",
+ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi."
+ },
+ "create_entry": {
+ "default": "Berhasil diautentikasi"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Pilih Metode Autentikasi"
+ }
+ }
+ },
+ "title": "Ondilo ICO"
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/it.json b/homeassistant/components/ondilo_ico/translations/it.json
new file mode 100644
index 00000000000000..cd75684a4372ab
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/it.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.",
+ "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione."
+ },
+ "create_entry": {
+ "default": "Autenticazione riuscita"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Scegli il metodo di autenticazione"
+ }
+ }
+ },
+ "title": "Ondilo ICO"
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/ko.json b/homeassistant/components/ondilo_ico/translations/ko.json
new file mode 100644
index 00000000000000..88f3d678171bde
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/ko.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694."
+ },
+ "create_entry": {
+ "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30"
+ }
+ }
+ },
+ "title": "Ondilo ICO"
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/lb.json b/homeassistant/components/ondilo_ico/translations/lb.json
new file mode 100644
index 00000000000000..d9a5cc7482a6ce
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/lb.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL.",
+ "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun."
+ },
+ "create_entry": {
+ "default": "Erfollegr\u00e4ich authentifiz\u00e9iert"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/nl.json b/homeassistant/components/ondilo_ico/translations/nl.json
new file mode 100644
index 00000000000000..8a91dff086f642
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/nl.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
+ "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen."
+ },
+ "create_entry": {
+ "default": "Succesvol geauthenticeerd"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Kies een authenticatie methode"
+ }
+ }
+ },
+ "title": "Ondilo ICO"
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/no.json b/homeassistant/components/ondilo_ico/translations/no.json
new file mode 100644
index 00000000000000..4a06b93d045de5
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/no.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse",
+ "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen"
+ },
+ "create_entry": {
+ "default": "Vellykket godkjenning"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Velg godkjenningsmetode"
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/pl.json b/homeassistant/components/ondilo_ico/translations/pl.json
new file mode 100644
index 00000000000000..f3aa08a250f83e
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/pl.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji",
+ "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105."
+ },
+ "create_entry": {
+ "default": "Pomy\u015blnie uwierzytelniono"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Wybierz metod\u0119 uwierzytelniania"
+ }
+ }
+ },
+ "title": "Ondilo ICO"
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/ru.json b/homeassistant/components/ondilo_ico/translations/ru.json
new file mode 100644
index 00000000000000..56bb2d342b7576
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/ru.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
+ "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438."
+ },
+ "create_entry": {
+ "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438"
+ }
+ }
+ },
+ "title": "Ondilo ICO"
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/tr.json b/homeassistant/components/ondilo_ico/translations/tr.json
new file mode 100644
index 00000000000000..9672275736570a
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/tr.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "step": {
+ "pick_implementation": {
+ "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in"
+ }
+ }
+ },
+ "title": "Ondilo ICO"
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/uk.json b/homeassistant/components/ondilo_ico/translations/uk.json
new file mode 100644
index 00000000000000..31e5834b027aa2
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/uk.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438."
+ },
+ "create_entry": {
+ "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e."
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ }
+ }
+ },
+ "title": "Ondilo ICO"
+}
\ No newline at end of file
diff --git a/homeassistant/components/ondilo_ico/translations/zh-Hant.json b/homeassistant/components/ondilo_ico/translations/zh-Hant.json
new file mode 100644
index 00000000000000..ea1902b3295535
--- /dev/null
+++ b/homeassistant/components/ondilo_ico/translations/zh-Hant.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002",
+ "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002"
+ },
+ "create_entry": {
+ "default": "\u5df2\u6210\u529f\u8a8d\u8b49"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f"
+ }
+ }
+ },
+ "title": "Ondilo ICO"
+}
\ No newline at end of file
diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py
index 6d64478aa7217b..e5a214ce8a4a3b 100644
--- a/homeassistant/components/onewire/__init__.py
+++ b/homeassistant/components/onewire/__init__.py
@@ -1,13 +1,17 @@
"""The 1-Wire component."""
import asyncio
+import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.typing import HomeAssistantType
-from .const import DOMAIN, SUPPORTED_PLATFORMS
+from .const import DOMAIN, PLATFORMS
from .onewirehub import CannotConnect, OneWireHub
+_LOGGER = logging.getLogger(__name__)
+
async def async_setup(hass, config):
"""Set up 1-Wire integrations."""
@@ -26,10 +30,43 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry):
hass.data[DOMAIN][config_entry.unique_id] = onewirehub
- for component in SUPPORTED_PLATFORMS:
- hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ async def cleanup_registry() -> None:
+ # Get registries
+ device_registry, entity_registry = await asyncio.gather(
+ hass.helpers.device_registry.async_get_registry(),
+ hass.helpers.entity_registry.async_get_registry(),
+ )
+ # Generate list of all device entries
+ registry_devices = [
+ entry.id
+ for entry in dr.async_entries_for_config_entry(
+ device_registry, config_entry.entry_id
+ )
+ ]
+ # Remove devices that don't belong to any entity
+ for device_id in registry_devices:
+ if not er.async_entries_for_device(
+ entity_registry, device_id, include_disabled_entities=True
+ ):
+ _LOGGER.debug(
+ "Removing device `%s` because it does not have any entities",
+ device_id,
+ )
+ device_registry.async_remove_device(device_id)
+
+ async def start_platforms() -> None:
+ """Start platforms and cleanup devices."""
+ # wait until all required platforms are ready
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ for platform in PLATFORMS
+ ]
)
+ await cleanup_registry()
+
+ hass.async_create_task(start_platforms())
+
return True
@@ -38,8 +75,8 @@ async def async_unload_entry(hass: HomeAssistantType, config_entry: ConfigEntry)
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in SUPPORTED_PLATFORMS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py
index 9ad4d5347f0cc0..fbb1d5debefeb3 100644
--- a/homeassistant/components/onewire/config_flow.py
+++ b/homeassistant/components/onewire/config_flow.py
@@ -5,7 +5,7 @@
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
from homeassistant.helpers.typing import HomeAssistantType
-from .const import ( # pylint: disable=unused-import
+from .const import (
CONF_MOUNT_DIR,
CONF_TYPE_OWFS,
CONF_TYPE_OWSERVER,
diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py
index e68039078e9df1..54b18f7c9053a4 100644
--- a/homeassistant/components/onewire/const.py
+++ b/homeassistant/components/onewire/const.py
@@ -61,7 +61,7 @@
SWITCH_TYPE_PIO: [None, None],
}
-SUPPORTED_PLATFORMS = [
+PLATFORMS = [
BINARY_SENSOR_DOMAIN,
SENSOR_DOMAIN,
SWITCH_DOMAIN,
diff --git a/homeassistant/components/onewire/onewire_entities.py b/homeassistant/components/onewire/onewire_entities.py
index 9238bb5d32c3f5..10c2b0c24a77b8 100644
--- a/homeassistant/components/onewire/onewire_entities.py
+++ b/homeassistant/components/onewire/onewire_entities.py
@@ -1,6 +1,8 @@
"""Support for 1-Wire entities."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from pyownet import protocol
@@ -43,32 +45,27 @@ def __init__(
self._unique_id = unique_id or device_file
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the entity."""
return self._name
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the class of this device."""
return self._device_class
@property
- def unit_of_measurement(self) -> Optional[str]:
- """Return the unit the value is expressed in."""
- return self._unit_of_measurement
-
- @property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the entity."""
return {"device_file": self._device_file, "raw_value": self._value_raw}
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID."""
return self._unique_id
@property
- def device_info(self) -> Optional[Dict[str, Any]]:
+ def device_info(self) -> dict[str, Any] | None:
"""Return device specific attributes."""
return self._device_info
@@ -85,9 +82,9 @@ def __init__(
self,
device_id: str,
device_name: str,
- device_info: Dict[str, Any],
+ device_info: dict[str, Any],
entity_path: str,
- entity_specs: Dict[str, Any],
+ entity_specs: dict[str, Any],
owproxy: protocol._Proxy,
):
"""Initialize the sensor."""
diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py
index 4888383fa423fd..b3a5be0a1ca775 100644
--- a/homeassistant/components/onewire/sensor.py
+++ b/homeassistant/components/onewire/sensor.py
@@ -1,4 +1,7 @@
"""Support for 1-Wire environment sensors."""
+from __future__ import annotations
+
+import asyncio
from glob import glob
import logging
import os
@@ -6,7 +9,7 @@
from pi1wire import InvalidCRCException, UnsupportResponseException
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
import homeassistant.helpers.config_validation as cv
@@ -264,9 +267,8 @@ def get_entities(onewirehub: OneWireHub, config):
"""Get a list of entities."""
entities = []
device_names = {}
- if CONF_NAMES in config:
- if isinstance(config[CONF_NAMES], dict):
- device_names = config[CONF_NAMES]
+ if CONF_NAMES in config and isinstance(config[CONF_NAMES], dict):
+ device_names = config[CONF_NAMES]
conf_type = config[CONF_TYPE]
# We have an owserver on a remote(or local) host/port
@@ -394,7 +396,16 @@ def get_entities(onewirehub: OneWireHub, config):
return entities
-class OneWireProxySensor(OneWireProxyEntity):
+class OneWireSensor(OneWireBaseEntity, SensorEntity):
+ """Mixin for sensor specific attributes."""
+
+ @property
+ def unit_of_measurement(self) -> str | None:
+ """Return the unit the value is expressed in."""
+ return self._unit_of_measurement
+
+
+class OneWireProxySensor(OneWireProxyEntity, OneWireSensor):
"""Implementation of a 1-Wire sensor connected through owserver."""
@property
@@ -403,7 +414,7 @@ def state(self) -> StateType:
return self._state
-class OneWireDirectSensor(OneWireBaseEntity):
+class OneWireDirectSensor(OneWireSensor):
"""Implementation of a 1-Wire sensor directly connected to RPI GPIO."""
def __init__(self, name, device_file, device_info, owsensor):
@@ -416,11 +427,31 @@ def state(self) -> StateType:
"""Return the state of the entity."""
return self._state
- def update(self):
+ async def get_temperature(self):
+ """Get the latest data from the device."""
+ attempts = 1
+ while True:
+ try:
+ return await self.hass.async_add_executor_job(
+ self._owsensor.get_temperature
+ )
+ except UnsupportResponseException as ex:
+ _LOGGER.debug(
+ "Cannot read from sensor %s (retry attempt %s): %s",
+ self._device_file,
+ attempts,
+ ex,
+ )
+ await asyncio.sleep(0.2)
+ attempts += 1
+ if attempts > 10:
+ raise
+
+ async def async_update(self):
"""Get the latest data from the device."""
value = None
try:
- self._value_raw = self._owsensor.get_temperature()
+ self._value_raw = await self.get_temperature()
value = round(float(self._value_raw), 1)
except (
FileNotFoundError,
@@ -431,7 +462,7 @@ def update(self):
self._state = value
-class OneWireOWFSSensor(OneWireBaseEntity): # pragma: no cover
+class OneWireOWFSSensor(OneWireSensor): # pragma: no cover
"""Implementation of a 1-Wire sensor through owfs.
This part of the implementation does not conform to policy regarding 3rd-party libraries, and will not longer be updated.
diff --git a/homeassistant/components/onewire/translations/de.json b/homeassistant/components/onewire/translations/de.json
index 3cc9f9cfc68f75..2b0630db22c85e 100644
--- a/homeassistant/components/onewire/translations/de.json
+++ b/homeassistant/components/onewire/translations/de.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
"error": {
"cannot_connect": "Verbindung fehlgeschlagen",
"invalid_path": "Verzeichnis nicht gefunden."
@@ -9,12 +12,14 @@
"data": {
"host": "Host",
"port": "Port"
- }
+ },
+ "title": "owserver-Details einstellen"
},
"user": {
"data": {
"type": "Verbindungstyp"
- }
+ },
+ "title": "1-Wire einrichten"
}
}
}
diff --git a/homeassistant/components/onewire/translations/hu.json b/homeassistant/components/onewire/translations/hu.json
index 8ac8f6d3b03c42..662475dde2c085 100644
--- a/homeassistant/components/onewire/translations/hu.json
+++ b/homeassistant/components/onewire/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
"invalid_path": "A k\u00f6nyvt\u00e1r nem tal\u00e1lhat\u00f3."
@@ -7,14 +10,15 @@
"step": {
"owserver": {
"data": {
- "host": "Gazdag\u00e9p",
+ "host": "Hoszt",
"port": "Port"
}
},
"user": {
"data": {
"type": "Kapcsolat t\u00edpusa"
- }
+ },
+ "title": "A 1-Wire be\u00e1ll\u00edt\u00e1sa"
}
}
}
diff --git a/homeassistant/components/onewire/translations/id.json b/homeassistant/components/onewire/translations/id.json
new file mode 100644
index 00000000000000..5de8e2eee3e6c3
--- /dev/null
+++ b/homeassistant/components/onewire/translations/id.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_path": "Direktori tidak ditemukan."
+ },
+ "step": {
+ "owserver": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ },
+ "title": "Tetapkan detail owserver"
+ },
+ "user": {
+ "data": {
+ "type": "Jenis koneksi"
+ },
+ "title": "Siapkan 1-Wire"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/onewire/translations/ko.json b/homeassistant/components/onewire/translations/ko.json
new file mode 100644
index 00000000000000..038be16108ab12
--- /dev/null
+++ b/homeassistant/components/onewire/translations/ko.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_path": "\ub514\ub809\ud130\ub9ac\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "owserver": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "port": "\ud3ec\ud2b8"
+ },
+ "title": "owserver \uc138\ubd80 \uc815\ubcf4 \uc124\uc815\ud558\uae30"
+ },
+ "user": {
+ "data": {
+ "type": "\uc5f0\uacb0 \uc720\ud615"
+ },
+ "title": "1-Wire \uc124\uc815\ud558\uae30"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/onewire/translations/nl.json b/homeassistant/components/onewire/translations/nl.json
index ae155ccf2c23d2..77ac79c15975bf 100644
--- a/homeassistant/components/onewire/translations/nl.json
+++ b/homeassistant/components/onewire/translations/nl.json
@@ -4,10 +4,15 @@
"already_configured": "Apparaat is al geconfigureerd"
},
"error": {
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_path": "Directory niet gevonden."
},
"step": {
"owserver": {
+ "data": {
+ "host": "Host",
+ "port": "Poort"
+ },
"title": "Owserver-details instellen"
},
"user": {
diff --git a/homeassistant/components/onewire/translations/tr.json b/homeassistant/components/onewire/translations/tr.json
new file mode 100644
index 00000000000000..f59da2ab7e74d6
--- /dev/null
+++ b/homeassistant/components/onewire/translations/tr.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_path": "Dizin bulunamad\u0131."
+ },
+ "step": {
+ "owserver": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ }
+ },
+ "user": {
+ "data": {
+ "type": "Ba\u011flant\u0131 t\u00fcr\u00fc"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/onewire/translations/uk.json b/homeassistant/components/onewire/translations/uk.json
new file mode 100644
index 00000000000000..9c9705d2993ce0
--- /dev/null
+++ b/homeassistant/components/onewire/translations/uk.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_path": "\u041a\u0430\u0442\u0430\u043b\u043e\u0433 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e."
+ },
+ "step": {
+ "owserver": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e owserver"
+ },
+ "user": {
+ "data": {
+ "type": "\u0422\u0438\u043f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f"
+ },
+ "title": "1-Wire"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py
index ac8cfa5e4b6956..2e4b6eff6da399 100644
--- a/homeassistant/components/onkyo/media_player.py
+++ b/homeassistant/components/onkyo/media_player.py
@@ -1,6 +1,7 @@
"""Support for Onkyo Receivers."""
+from __future__ import annotations
+
import logging
-from typing import List
import eiscp
from eiscp import eISCP
@@ -56,7 +57,7 @@
| SUPPORT_PLAY_MEDIA
)
-KNOWN_HOSTS: List[str] = []
+KNOWN_HOSTS: list[str] = []
DEFAULT_SOURCES = {
"tv": "TV",
"bd": "Bluray",
@@ -118,15 +119,20 @@
SERVICE_SELECT_HDMI_OUTPUT = "onkyo_select_hdmi_output"
-def _parse_onkyo_tuple(tup):
- """Parse a tuple returned from the eiscp library."""
- if len(tup) < 2:
+def _parse_onkyo_payload(payload):
+ """Parse a payload returned from the eiscp library."""
+ if isinstance(payload, bool):
+ # command not supported by the device
+ return False
+
+ if len(payload) < 2:
+ # no value
return None
- if isinstance(tup[1], str):
- return tup[1].split(",")
+ if isinstance(payload[1], str):
+ return payload[1].split(",")
- return tup[1]
+ return payload[1]
def _tuple_get(tup, index, default=None):
@@ -267,6 +273,8 @@ def __init__(
self._reverse_mapping = {value: key for key, value in sources.items()}
self._attributes = {}
self._hdmi_out_supported = True
+ self._audio_info_supported = True
+ self._video_info_supported = True
def command(self, command):
"""Run an eiscp command and catch connection errors."""
@@ -309,12 +317,14 @@ def update(self):
else:
hdmi_out_raw = []
preset_raw = self.command("preset query")
- audio_information_raw = self.command("audio-information query")
- video_information_raw = self.command("video-information query")
+ if self._audio_info_supported:
+ audio_information_raw = self.command("audio-information query")
+ if self._video_info_supported:
+ video_information_raw = self.command("video-information query")
if not (volume_raw and mute_raw and current_source_raw):
return
- sources = _parse_onkyo_tuple(current_source_raw)
+ sources = _parse_onkyo_payload(current_source_raw)
for source in sources:
if source in self._source_mapping:
@@ -383,7 +393,7 @@ def source_list(self):
return self._source_list
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
return self._attributes
@@ -441,7 +451,11 @@ def select_output(self, output):
self.command(f"hdmi-output-selector={output}")
def _parse_audio_information(self, audio_information_raw):
- values = _parse_onkyo_tuple(audio_information_raw)
+ values = _parse_onkyo_payload(audio_information_raw)
+ if values is False:
+ self._audio_info_supported = False
+ return
+
if values:
info = {
"format": _tuple_get(values, 1),
@@ -456,7 +470,11 @@ def _parse_audio_information(self, audio_information_raw):
self._attributes.pop(ATTR_AUDIO_INFORMATION, None)
def _parse_video_information(self, video_information_raw):
- values = _parse_onkyo_tuple(video_information_raw)
+ values = _parse_onkyo_payload(video_information_raw)
+ if values is False:
+ self._video_info_supported = False
+ return
+
if values:
info = {
"input_resolution": _tuple_get(values, 1),
diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py
index b332b7a795ac99..0eb39064db7cd7 100644
--- a/homeassistant/components/onvif/__init__.py
+++ b/homeassistant/components/onvif/__init__.py
@@ -88,9 +88,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
if device.capabilities.events:
platforms += ["binary_sensor", "sensor"]
- for component in platforms:
+ for platform in platforms:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.async_stop)
@@ -111,8 +111,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
return all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in platforms
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in platforms
]
)
)
diff --git a/homeassistant/components/onvif/binary_sensor.py b/homeassistant/components/onvif/binary_sensor.py
index 9b5469ee0d0cb0..680f92efd2bc5d 100644
--- a/homeassistant/components/onvif/binary_sensor.py
+++ b/homeassistant/components/onvif/binary_sensor.py
@@ -1,5 +1,5 @@
"""Support for ONVIF binary sensors."""
-from typing import Optional
+from __future__ import annotations
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.core import callback
@@ -55,7 +55,7 @@ def name(self) -> str:
return self.device.events.get_uid(self.uid).name
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the class of this device, from component DEVICE_CLASSES."""
return self.device.events.get_uid(self.uid).device_class
diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py
index 273640ab6b5371..0de9362795323e 100644
--- a/homeassistant/components/onvif/config_flow.py
+++ b/homeassistant/components/onvif/config_flow.py
@@ -1,6 +1,7 @@
"""Config flow for ONVIF."""
+from __future__ import annotations
+
from pprint import pformat
-from typing import List
from urllib.parse import urlparse
from onvif.exceptions import ONVIFError
@@ -21,7 +22,6 @@
)
from homeassistant.core import callback
-# pylint: disable=unused-import
from .const import (
CONF_DEVICE_ID,
CONF_RTSP_TRANSPORT,
@@ -36,7 +36,7 @@
CONF_MANUAL_INPUT = "Manually configure ONVIF device"
-def wsdiscovery() -> List[Service]:
+def wsdiscovery() -> list[Service]:
"""Get ONVIF Profile S devices from network."""
discovery = WSDiscovery(ttl=4)
discovery.start()
@@ -49,7 +49,7 @@ def wsdiscovery() -> List[Service]:
async def async_discovery(hass) -> bool:
"""Return if there are devices that can be discovered."""
- LOGGER.debug("Starting ONVIF discovery...")
+ LOGGER.debug("Starting ONVIF discovery")
services = await hass.async_add_executor_job(wsdiscovery)
devices = []
diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py
index 84761a4777f890..826ff4b1a29822 100644
--- a/homeassistant/components/onvif/device.py
+++ b/homeassistant/components/onvif/device.py
@@ -1,8 +1,10 @@
"""ONVIF device abstraction."""
+from __future__ import annotations
+
import asyncio
+from contextlib import suppress
import datetime as dt
import os
-from typing import List
from httpx import RequestError
import onvif
@@ -50,7 +52,7 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry = None):
self.info: DeviceInfo = DeviceInfo()
self.capabilities: Capabilities = Capabilities()
- self.profiles: List[Profile] = []
+ self.profiles: list[Profile] = []
self.max_resolution: int = 0
self._dt_diff_seconds: int = 0
@@ -240,29 +242,23 @@ async def async_get_device_info(self) -> DeviceInfo:
async def async_get_capabilities(self):
"""Obtain information about the available services on the device."""
snapshot = False
- try:
+ with suppress(ONVIFError, Fault, RequestError):
media_service = self.device.create_media_service()
media_capabilities = await media_service.GetServiceCapabilities()
snapshot = media_capabilities and media_capabilities.SnapshotUri
- except (ONVIFError, Fault, RequestError):
- pass
pullpoint = False
- try:
+ with suppress(ONVIFError, Fault, RequestError):
pullpoint = await self.events.async_start()
- except (ONVIFError, Fault):
- pass
ptz = False
- try:
+ with suppress(ONVIFError, Fault, RequestError):
self.device.get_definition("ptz")
ptz = True
- except ONVIFError:
- pass
return Capabilities(snapshot, pullpoint, ptz)
- async def async_get_profiles(self) -> List[Profile]:
+ async def async_get_profiles(self) -> list[Profile]:
"""Obtain media profiles for this device."""
media_service = self.device.create_media_service()
result = await media_service.GetProfiles()
@@ -438,7 +434,7 @@ async def async_perform_ptz(
await ptz_service.Stop(req)
except ONVIFError as err:
if "Bad Request" in err.reason:
- LOGGER.warning("Device '%s' doesn't support PTZ.", self.name)
+ LOGGER.warning("Device '%s' doesn't support PTZ", self.name)
else:
LOGGER.error("Error trying to perform PTZ action: %s", err)
diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py
index eaf23236042be6..76b18d729a8f00 100644
--- a/homeassistant/components/onvif/event.py
+++ b/homeassistant/components/onvif/event.py
@@ -1,7 +1,10 @@
"""ONVIF event abstraction."""
+from __future__ import annotations
+
import asyncio
+from contextlib import suppress
import datetime as dt
-from typing import Callable, Dict, List, Optional, Set
+from typing import Callable
from httpx import RemoteProtocolError, TransportError
from onvif import ONVIFCamera, ONVIFService
@@ -34,14 +37,14 @@ def __init__(self, hass: HomeAssistant, device: ONVIFCamera, unique_id: str):
self.started: bool = False
self._subscription: ONVIFService = None
- self._events: Dict[str, Event] = {}
- self._listeners: List[CALLBACK_TYPE] = []
- self._unsub_refresh: Optional[CALLBACK_TYPE] = None
+ self._events: dict[str, Event] = {}
+ self._listeners: list[CALLBACK_TYPE] = []
+ self._unsub_refresh: CALLBACK_TYPE | None = None
super().__init__()
@property
- def platforms(self) -> Set[str]:
+ def platforms(self) -> set[str]:
"""Return platforms to setup."""
return {event.platform for event in self._events.values()}
@@ -84,10 +87,8 @@ async def async_start(self) -> bool:
# Initialize events
pullpoint = self.device.create_pullpoint_service()
- try:
+ with suppress(*SUBSCRIPTION_ERRORS):
await pullpoint.SetSynchronizationPoint()
- except SUBSCRIPTION_ERRORS:
- pass
response = await pullpoint.PullMessages(
{"MessageLimit": 100, "Timeout": dt.timedelta(seconds=5)}
)
@@ -117,10 +118,9 @@ async def async_restart(self, _now: dt = None) -> None:
return
if self._subscription:
- try:
+ # Suppressed. The subscription may no longer exist.
+ with suppress(*SUBSCRIPTION_ERRORS):
await self._subscription.Unsubscribe()
- except SUBSCRIPTION_ERRORS:
- pass # Ignored. The subscription may no longer exist.
self._subscription = None
try:
@@ -130,7 +130,7 @@ async def async_restart(self, _now: dt = None) -> None:
if not restarted:
LOGGER.warning(
- "Failed to restart ONVIF PullPoint subscription for '%s'. Retrying...",
+ "Failed to restart ONVIF PullPoint subscription for '%s'. Retrying",
self.unique_id,
)
# Try again in a minute
@@ -229,6 +229,6 @@ def get_uid(self, uid) -> Event:
"""Retrieve event for given id."""
return self._events[uid]
- def get_platform(self, platform) -> List[Event]:
+ def get_platform(self, platform) -> list[Event]:
"""Retrieve events for given platform."""
return [event for event in self._events.values() if event.platform == platform]
diff --git a/homeassistant/components/onvif/models.py b/homeassistant/components/onvif/models.py
index 2a129d3bc44427..feda891f7729b8 100644
--- a/homeassistant/components/onvif/models.py
+++ b/homeassistant/components/onvif/models.py
@@ -1,6 +1,8 @@
"""ONVIF models."""
+from __future__ import annotations
+
from dataclasses import dataclass
-from typing import Any, List
+from typing import Any
@dataclass
@@ -37,7 +39,7 @@ class PTZ:
continuous: bool
relative: bool
absolute: bool
- presets: List[str] = None
+ presets: list[str] = None
@dataclass
diff --git a/homeassistant/components/onvif/parsers.py b/homeassistant/components/onvif/parsers.py
index cad2b3c8cab5ca..9574d44edeae2a 100644
--- a/homeassistant/components/onvif/parsers.py
+++ b/homeassistant/components/onvif/parsers.py
@@ -387,3 +387,25 @@ async def async_parse_last_clock_sync(uid: str, msg) -> Event:
)
except (AttributeError, KeyError, ValueError):
return None
+
+
+@PARSERS.register("tns1:RecordingConfig/JobState")
+# pylint: disable=protected-access
+async def async_parse_jobstate(uid: str, msg) -> Event:
+ """Handle parsing event message.
+
+ Topic: tns1:RecordingConfig/JobState*
+ """
+
+ try:
+ source = msg.Message._value_1.Source.SimpleItem[0].Value
+ return Event(
+ f"{uid}_{msg.Topic._value_1}_{source}",
+ f"{source} JobState",
+ "binary_sensor",
+ None,
+ None,
+ msg.Message._value_1.Data.SimpleItem[0].Value == "Active",
+ )
+ except (AttributeError, KeyError):
+ return None
diff --git a/homeassistant/components/onvif/sensor.py b/homeassistant/components/onvif/sensor.py
index b1d7ff7986cc46..1c5766e39697e0 100644
--- a/homeassistant/components/onvif/sensor.py
+++ b/homeassistant/components/onvif/sensor.py
@@ -1,6 +1,7 @@
"""Support for ONVIF binary sensors."""
-from typing import Optional, Union
+from __future__ import annotations
+from homeassistant.components.sensor import SensorEntity
from homeassistant.core import callback
from .base import ONVIFBaseEntity
@@ -33,7 +34,7 @@ def async_check_entities():
return True
-class ONVIFSensor(ONVIFBaseEntity):
+class ONVIFSensor(ONVIFBaseEntity, SensorEntity):
"""Representation of a ONVIF sensor event."""
def __init__(self, uid, device):
@@ -43,7 +44,7 @@ def __init__(self, uid, device):
super().__init__(device)
@property
- def state(self) -> Union[None, str, int, float]:
+ def state(self) -> None | str | int | float:
"""Return the state of the entity."""
return self.device.events.get_uid(self.uid).value
@@ -53,12 +54,12 @@ def name(self):
return self.device.events.get_uid(self.uid).name
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the class of this device, from component DEVICE_CLASSES."""
return self.device.events.get_uid(self.uid).device_class
@property
- def unit_of_measurement(self) -> Optional[str]:
+ def unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of this entity, if any."""
return self.device.events.get_uid(self.uid).unit_of_measurement
diff --git a/homeassistant/components/onvif/translations/de.json b/homeassistant/components/onvif/translations/de.json
index 25984ecf3e17c6..5289f6479cc050 100644
--- a/homeassistant/components/onvif/translations/de.json
+++ b/homeassistant/components/onvif/translations/de.json
@@ -1,12 +1,15 @@
{
"config": {
"abort": {
- "already_configured": "Das ONVIF-Ger\u00e4t ist bereits konfiguriert.",
- "already_in_progress": "Der Konfigurationsfluss f\u00fcr das ONVIF-Ger\u00e4t wird bereits ausgef\u00fchrt.",
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
"no_h264": "Es waren keine H264-Streams verf\u00fcgbar. \u00dcberpr\u00fcfen Sie die Profilkonfiguration auf Ihrem Ger\u00e4t.",
"no_mac": "Die eindeutige ID f\u00fcr das ONVIF-Ger\u00e4t konnte nicht konfiguriert werden.",
"onvif_error": "Fehler beim Einrichten des ONVIF-Ger\u00e4ts. \u00dcberpr\u00fcfen Sie die Protokolle auf weitere Informationen."
},
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
"step": {
"auth": {
"data": {
diff --git a/homeassistant/components/onvif/translations/he.json b/homeassistant/components/onvif/translations/he.json
index a3203bfedb3380..ecfa1afaab23d5 100644
--- a/homeassistant/components/onvif/translations/he.json
+++ b/homeassistant/components/onvif/translations/he.json
@@ -1,6 +1,12 @@
{
"config": {
"step": {
+ "auth": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ },
"configure_profile": {
"data": {
"include": "\u05e6\u05d5\u05e8 \u05d9\u05e9\u05d5\u05ea \u05de\u05e6\u05dc\u05de\u05d4"
diff --git a/homeassistant/components/onvif/translations/hu.json b/homeassistant/components/onvif/translations/hu.json
index c9c3f1379847f0..61a6cfb056ef46 100644
--- a/homeassistant/components/onvif/translations/hu.json
+++ b/homeassistant/components/onvif/translations/hu.json
@@ -1,5 +1,12 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
+ "no_h264": "Nem voltak el\u00e9rhet\u0151 H264 streamek. Ellen\u0151rizd a profil konfigur\u00e1ci\u00f3j\u00e1t a k\u00e9sz\u00fcl\u00e9ken.",
+ "no_mac": "Nem siker\u00fclt konfigur\u00e1lni az egyedi azonos\u00edt\u00f3t az ONVIF eszk\u00f6zh\u00f6z.",
+ "onvif_error": "Hiba t\u00f6rt\u00e9nt az ONVIF eszk\u00f6z be\u00e1ll\u00edt\u00e1sakor. Tov\u00e1bbi inform\u00e1ci\u00f3k\u00e9rt ellen\u0151rizd a napl\u00f3kat."
+ },
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
@@ -8,7 +15,8 @@
"data": {
"password": "Jelsz\u00f3",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
- }
+ },
+ "title": "Hiteles\u00edt\u00e9s konfigur\u00e1l\u00e1sa"
},
"configure_profile": {
"data": {
@@ -17,11 +25,19 @@
"description": "L\u00e9trehozza a(z) {profile} f\u00e9nyk\u00e9pez\u0151g\u00e9p entit\u00e1s\u00e1t {resolution} felbont\u00e1ssal?",
"title": "Profilok konfigur\u00e1l\u00e1sa"
},
+ "device": {
+ "data": {
+ "host": "V\u00e1laszd ki a felfedezett ONVIF eszk\u00f6zt"
+ },
+ "title": "ONVIF eszk\u00f6z kiv\u00e1laszt\u00e1sa"
+ },
"manual_input": {
"data": {
"host": "Hoszt",
+ "name": "N\u00e9v",
"port": "Port"
- }
+ },
+ "title": "ONVIF eszk\u00f6z konfigur\u00e1l\u00e1sa"
},
"user": {
"description": "A k\u00fcld\u00e9s gombra kattintva olyan ONVIF-eszk\u00f6z\u00f6ket keres\u00fcnk a h\u00e1l\u00f3zat\u00e1ban, amelyek t\u00e1mogatj\u00e1k az S profilt.\n\nEgyes gy\u00e1rt\u00f3k alap\u00e9rtelmez\u00e9s szerint elkezdt\u00e9k letiltani az ONVIF-et. Ellen\u0151rizze, hogy az ONVIF enged\u00e9lyezve van-e a kamera konfigur\u00e1ci\u00f3j\u00e1ban."
diff --git a/homeassistant/components/onvif/translations/id.json b/homeassistant/components/onvif/translations/id.json
new file mode 100644
index 00000000000000..3ed50ae63c40de
--- /dev/null
+++ b/homeassistant/components/onvif/translations/id.json
@@ -0,0 +1,59 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "no_h264": "Tidak ada aliran H264 yang tersedia. Periksa konfigurasi profil di perangkat Anda.",
+ "no_mac": "Tidak dapat mengonfigurasi ID unik untuk perangkat ONVIF.",
+ "onvif_error": "Terjadi kesalahan saat menyiapkan perangkat ONVIF. Periksa log untuk informasi lebih lanjut."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "title": "Konfigurasikan autentikasi"
+ },
+ "configure_profile": {
+ "data": {
+ "include": "Buat entitas kamera"
+ },
+ "description": "Buat entitas kamera untuk {profile} dengan {resolution}?",
+ "title": "Konfigurasikan Profil"
+ },
+ "device": {
+ "data": {
+ "host": "Pilih perangkat ONVIF yang ditemukan"
+ },
+ "title": "Pilih perangkat ONVIF"
+ },
+ "manual_input": {
+ "data": {
+ "host": "Host",
+ "name": "Nama",
+ "port": "Port"
+ },
+ "title": "Konfigurasikan perangkat ONVIF"
+ },
+ "user": {
+ "description": "Dengan mengklik kirim, kami akan mencari perangkat ONVIF pada jaringan Anda yang mendukung Profil S.\n\nBeberapa produsen mulai menonaktifkan ONVIF secara default. Pastikan ONVIF diaktifkan dalam konfigurasi kamera Anda.",
+ "title": "Penyiapan perangkat ONVIF"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "onvif_devices": {
+ "data": {
+ "extra_arguments": "Argumen FFMPEG ekstra",
+ "rtsp_transport": "Mekanisme transport RTSP"
+ },
+ "title": "Opsi Perangkat ONVIF"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/onvif/translations/ko.json b/homeassistant/components/onvif/translations/ko.json
index 3b992e8d35fa9a..173cdb88512df5 100644
--- a/homeassistant/components/onvif/translations/ko.json
+++ b/homeassistant/components/onvif/translations/ko.json
@@ -1,12 +1,15 @@
{
"config": {
"abort": {
- "already_configured": "ONVIF \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "already_in_progress": "ONVIF \uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.",
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
"no_h264": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c H264 \uc2a4\ud2b8\ub9bc\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uae30\uae30\uc5d0\uc11c \ud504\ub85c\ud544 \uad6c\uc131\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
"no_mac": "ONVIF \uae30\uae30\uc758 \uace0\uc720 ID \ub97c \uad6c\uc131\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
"onvif_error": "ONVIF \uae30\uae30 \uc124\uc815 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 \ub85c\uadf8\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694."
},
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
"step": {
"auth": {
"data": {
diff --git a/homeassistant/components/onvif/translations/nl.json b/homeassistant/components/onvif/translations/nl.json
index 4d76e939af023b..e1fdac8256e59a 100644
--- a/homeassistant/components/onvif/translations/nl.json
+++ b/homeassistant/components/onvif/translations/nl.json
@@ -52,7 +52,7 @@
"extra_arguments": "Extra FFMPEG argumenten",
"rtsp_transport": "RTSP-transportmechanisme"
},
- "title": "[%%] Apparaatopties"
+ "title": "ONVIF-apparaatopties"
}
}
}
diff --git a/homeassistant/components/onvif/translations/ru.json b/homeassistant/components/onvif/translations/ru.json
index 823dd8eb7fd2dd..6b486bb62e2b68 100644
--- a/homeassistant/components/onvif/translations/ru.json
+++ b/homeassistant/components/onvif/translations/ru.json
@@ -14,7 +14,7 @@
"auth": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f"
},
diff --git a/homeassistant/components/onvif/translations/tr.json b/homeassistant/components/onvif/translations/tr.json
index 4e3ad18a60d685..683dfbe7b92615 100644
--- a/homeassistant/components/onvif/translations/tr.json
+++ b/homeassistant/components/onvif/translations/tr.json
@@ -1,8 +1,42 @@
{
"config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
"step": {
+ "auth": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ },
+ "configure_profile": {
+ "data": {
+ "include": "Kamera varl\u0131\u011f\u0131 olu\u015ftur"
+ },
+ "title": "Profilleri Yap\u0131land\u0131r"
+ },
+ "device": {
+ "data": {
+ "host": "Ke\u015ffedilen ONVIF cihaz\u0131n\u0131 se\u00e7in"
+ },
+ "title": "ONVIF cihaz\u0131n\u0131 se\u00e7in"
+ },
+ "manual_input": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "name": "Ad",
+ "port": "Port"
+ },
+ "title": "ONVIF cihaz\u0131n\u0131 yap\u0131land\u0131r\u0131n"
+ },
"user": {
- "description": "G\u00f6nder d\u00fc\u011fmesine t\u0131klad\u0131\u011f\u0131n\u0131zda, Profil S'yi destekleyen ONVIF cihazlar\u0131 i\u00e7in a\u011f\u0131n\u0131zda arama yapaca\u011f\u0131z. \n\n Baz\u0131 \u00fcreticiler varsay\u0131lan olarak ONVIF'i devre d\u0131\u015f\u0131 b\u0131rakmaya ba\u015flad\u0131. L\u00fctfen kameran\u0131z\u0131n yap\u0131land\u0131rmas\u0131nda ONVIF'in etkinle\u015ftirildi\u011finden emin olun."
+ "description": "G\u00f6nder d\u00fc\u011fmesine t\u0131klad\u0131\u011f\u0131n\u0131zda, Profil S'yi destekleyen ONVIF cihazlar\u0131 i\u00e7in a\u011f\u0131n\u0131zda arama yapaca\u011f\u0131z. \n\n Baz\u0131 \u00fcreticiler varsay\u0131lan olarak ONVIF'i devre d\u0131\u015f\u0131 b\u0131rakmaya ba\u015flad\u0131. L\u00fctfen kameran\u0131z\u0131n yap\u0131land\u0131rmas\u0131nda ONVIF'in etkinle\u015ftirildi\u011finden emin olun.",
+ "title": "ONVIF cihaz kurulumu"
}
}
},
diff --git a/homeassistant/components/onvif/translations/uk.json b/homeassistant/components/onvif/translations/uk.json
new file mode 100644
index 00000000000000..82a816add044e3
--- /dev/null
+++ b/homeassistant/components/onvif/translations/uk.json
@@ -0,0 +1,59 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "no_h264": "\u041d\u0435\u043c\u0430\u0454 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u043f\u043e\u0442\u043e\u043a\u0456\u0432 H264. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043d\u0430 \u0441\u0432\u043e\u0454\u043c\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457.",
+ "no_mac": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0443\u043d\u0456\u043a\u0430\u043b\u044c\u043d\u0438\u0439 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0434\u043b\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e.",
+ "onvif_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043b\u043e\u0433\u0438 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "\u0423\u0432\u0456\u0439\u0442\u0438"
+ },
+ "configure_profile": {
+ "data": {
+ "include": "\u0421\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043e\u0431'\u0454\u043a\u0442 \u043a\u0430\u043c\u0435\u0440\u0438"
+ },
+ "description": "\u0421\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043e\u0431'\u0454\u043a\u0442 \u043a\u0430\u043c\u0435\u0440\u0438 \u0434\u043b\u044f {profile} \u0437 \u0440\u043e\u0437\u0434\u0456\u043b\u044c\u043d\u043e\u044e \u0437\u0434\u0430\u0442\u043d\u0456\u0441\u0442\u044e {resolution}?",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u043e\u0444\u0456\u043b\u0456\u0432"
+ },
+ "device": {
+ "data": {
+ "host": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 ONVIF"
+ },
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 ONVIF"
+ },
+ "manual_input": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e ONVIF"
+ },
+ "user": {
+ "description": "\u041a\u043e\u043b\u0438 \u0412\u0438 \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041d\u0430\u0434\u0456\u0441\u043b\u0430\u0442\u0438, \u043f\u043e\u0447\u043d\u0435\u0442\u044c\u0441\u044f \u043f\u043e\u0448\u0443\u043a \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 ONVIF, \u044f\u043a\u0456 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u044e\u0442\u044c Profile S. \n\n\u0414\u0435\u044f\u043a\u0456 \u0432\u0438\u0440\u043e\u0431\u043d\u0438\u043a\u0438 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0437\u0430 \u0443\u043c\u043e\u0432\u0447\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0430\u044e\u0442\u044c ONVIF. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e ONVIF \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e \u0432 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u0445 \u0412\u0430\u0448\u043e\u0457 \u043a\u0430\u043c\u0435\u0440\u0438.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e ONVIF"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "onvif_devices": {
+ "data": {
+ "extra_arguments": "\u0414\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u0438 FFMPEG",
+ "rtsp_transport": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u0438\u0439 \u043c\u0435\u0445\u0430\u043d\u0456\u0437\u043c RTSP"
+ },
+ "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e ONVIF"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py
index ee913070a11f26..d098edba5b2f80 100644
--- a/homeassistant/components/openalpr_local/image_processing.py
+++ b/homeassistant/components/openalpr_local/image_processing.py
@@ -99,7 +99,7 @@ def device_class(self):
return "alpr"
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
return {ATTR_PLATES: self.plates, ATTR_VEHICLES: self.vehicles}
diff --git a/homeassistant/components/opencv/image_processing.py b/homeassistant/components/opencv/image_processing.py
index 028d6eacf249c3..bf63ec0bfff675 100644
--- a/homeassistant/components/opencv/image_processing.py
+++ b/homeassistant/components/opencv/image_processing.py
@@ -152,7 +152,7 @@ def state(self):
return self._total_matches
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
return {ATTR_MATCHES: self._matches, ATTR_TOTAL_MATCHES: self._total_matches}
diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json
index 24b84e305e7c25..a0294a7aa49ce5 100644
--- a/homeassistant/components/opencv/manifest.json
+++ b/homeassistant/components/opencv/manifest.json
@@ -2,6 +2,6 @@
"domain": "opencv",
"name": "OpenCV",
"documentation": "https://www.home-assistant.io/integrations/opencv",
- "requirements": ["numpy==1.19.2", "opencv-python-headless==4.3.0.36"],
+ "requirements": ["numpy==1.20.2", "opencv-python-headless==4.3.0.36"],
"codeowners": []
}
diff --git a/homeassistant/components/openerz/sensor.py b/homeassistant/components/openerz/sensor.py
index 9a5bf3a981329c..33305b677def1d 100644
--- a/homeassistant/components/openerz/sensor.py
+++ b/homeassistant/components/openerz/sensor.py
@@ -4,9 +4,9 @@
from openerz_api.main import OpenERZConnector
import voluptuous as vol
+from homeassistant.components.sensor import SensorEntity
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
-from homeassistant.helpers.entity import Entity
SCAN_INTERVAL = timedelta(hours=12)
@@ -29,7 +29,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([OpenERZSensor(api_connector, config.get(CONF_NAME))], True)
-class OpenERZSensor(Entity):
+class OpenERZSensor(SensorEntity):
"""Representation of a Sensor."""
def __init__(self, api_connector, name):
diff --git a/homeassistant/components/openevse/sensor.py b/homeassistant/components/openevse/sensor.py
index e0f21f6946db1b..d7d4149e26d080 100644
--- a/homeassistant/components/openevse/sensor.py
+++ b/homeassistant/components/openevse/sensor.py
@@ -5,7 +5,7 @@
from requests import RequestException
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_HOST,
CONF_MONITORED_VARIABLES,
@@ -14,7 +14,6 @@
TIME_MINUTES,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -52,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(dev, True)
-class OpenEVSESensor(Entity):
+class OpenEVSESensor(SensorEntity):
"""Implementation of an OpenEVSE sensor."""
def __init__(self, sensor_type, charger):
diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py
index 9846e305291b0b..8474cdab131426 100644
--- a/homeassistant/components/openexchangerates/sensor.py
+++ b/homeassistant/components/openexchangerates/sensor.py
@@ -5,7 +5,7 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_API_KEY,
@@ -15,7 +15,6 @@
HTTP_OK,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -58,7 +57,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([OpenexchangeratesSensor(rest, name, quote)], True)
-class OpenexchangeratesSensor(Entity):
+class OpenexchangeratesSensor(SensorEntity):
"""Representation of an Open Exchange Rates sensor."""
def __init__(self, rest, name, quote):
@@ -79,7 +78,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return other attributes of the sensor."""
attr = self.rest.data
attr[ATTR_ATTRIBUTION] = ATTRIBUTION
diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py
index cf6825c867bda6..154cb4df3ae09f 100644
--- a/homeassistant/components/opengarage/cover.py
+++ b/homeassistant/components/opengarage/cover.py
@@ -92,7 +92,7 @@ def __init__(self, name, open_garage, device_id):
self._open_garage = open_garage
self._state = None
self._state_before_move = None
- self._device_state_attributes = {}
+ self._extra_state_attributes = {}
self._available = True
self._device_id = device_id
@@ -107,9 +107,9 @@ def available(self):
return self._available
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
- return self._device_state_attributes
+ return self._extra_state_attributes
@property
def is_closed(self):
@@ -154,11 +154,11 @@ async def async_update(self):
_LOGGER.debug("%s status: %s", self._name, self._state)
if status.get("rssi") is not None:
- self._device_state_attributes[ATTR_SIGNAL_STRENGTH] = status.get("rssi")
+ self._extra_state_attributes[ATTR_SIGNAL_STRENGTH] = status.get("rssi")
if status.get("dist") is not None:
- self._device_state_attributes[ATTR_DISTANCE_SENSOR] = status.get("dist")
+ self._extra_state_attributes[ATTR_DISTANCE_SENSOR] = status.get("dist")
if self._state is not None:
- self._device_state_attributes[ATTR_DOOR_STATE] = self._state
+ self._extra_state_attributes[ATTR_DOOR_STATE] = self._state
self._available = True
diff --git a/homeassistant/components/openhardwaremonitor/sensor.py b/homeassistant/components/openhardwaremonitor/sensor.py
index 115366dac66a53..70d0d36176c620 100644
--- a/homeassistant/components/openhardwaremonitor/sensor.py
+++ b/homeassistant/components/openhardwaremonitor/sensor.py
@@ -5,11 +5,10 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from homeassistant.util.dt import utcnow
@@ -44,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(data.devices, True)
-class OpenHardwareMonitorDevice(Entity):
+class OpenHardwareMonitorDevice(SensorEntity):
"""Device used to display information from OpenHardwareMonitor."""
def __init__(self, data, name, path, unit_of_measurement):
@@ -73,8 +72,8 @@ def state(self):
return self.value
@property
- def state_attributes(self):
- """Return the state attributes of the sun."""
+ def extra_state_attributes(self):
+ """Return the state attributes of the entity."""
return self.attributes
@classmethod
diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py
index f9dbd4272ba184..270eb22ebdaf00 100644
--- a/homeassistant/components/openhome/media_player.py
+++ b/homeassistant/components/openhome/media_player.py
@@ -139,7 +139,7 @@ def turn_off(self):
def play_media(self, media_type, media_id, **kwargs):
"""Send the play_media command to the media player."""
- if not media_type == MEDIA_TYPE_MUSIC:
+ if media_type != MEDIA_TYPE_MUSIC:
_LOGGER.error(
"Invalid media type %s. Only %s is supported",
media_type,
diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py
index 06132e83e8896e..122388b85b7588 100644
--- a/homeassistant/components/opensky/sensor.py
+++ b/homeassistant/components/opensky/sensor.py
@@ -4,7 +4,7 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_LATITUDE,
@@ -17,7 +17,6 @@
LENGTH_METERS,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import distance as util_distance, location as util_location
CONF_ALTITUDE = "altitude"
@@ -87,7 +86,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
)
-class OpenSkySensor(Entity):
+class OpenSkySensor(SensorEntity):
"""Open Sky Network Sensor."""
def __init__(self, hass, name, latitude, longitude, radius, altitude):
@@ -174,7 +173,7 @@ def update(self):
self._previously_tracked = currently_tracked
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: OPENSKY_ATTRIBUTION}
diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py
index cc08ec3da6997f..8686997e7481a6 100644
--- a/homeassistant/components/opentherm_gw/__init__.py
+++ b/homeassistant/components/opentherm_gw/__init__.py
@@ -39,6 +39,8 @@
CONF_CLIMATE,
CONF_FLOOR_TEMP,
CONF_PRECISION,
+ CONF_READ_PRECISION,
+ CONF_SET_PRECISION,
DATA_GATEWAYS,
DATA_OPENTHERM_GW,
DOMAIN,
@@ -94,6 +96,17 @@ async def async_setup_entry(hass, config_entry):
gateway = OpenThermGatewayDevice(hass, config_entry)
hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] = gateway
+ if config_entry.options.get(CONF_PRECISION):
+ migrate_options = dict(config_entry.options)
+ migrate_options.update(
+ {
+ CONF_READ_PRECISION: config_entry.options[CONF_PRECISION],
+ CONF_SET_PRECISION: config_entry.options[CONF_PRECISION],
+ }
+ )
+ del migrate_options[CONF_PRECISION]
+ hass.config_entries.async_update_entry(config_entry, options=migrate_options)
+
config_entry.add_update_listener(options_updated)
# Schedule directly on the loop to avoid blocking HA startup.
diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py
index 8ec536e73317bb..294088ee608f6f 100644
--- a/homeassistant/components/opentherm_gw/climate.py
+++ b/homeassistant/components/opentherm_gw/climate.py
@@ -28,7 +28,14 @@
from homeassistant.helpers.entity import async_generate_entity_id
from . import DOMAIN
-from .const import CONF_FLOOR_TEMP, CONF_PRECISION, DATA_GATEWAYS, DATA_OPENTHERM_GW
+from .const import (
+ CONF_FLOOR_TEMP,
+ CONF_READ_PRECISION,
+ CONF_SET_PRECISION,
+ CONF_TEMPORARY_OVRD_MODE,
+ DATA_GATEWAYS,
+ DATA_OPENTHERM_GW,
+)
_LOGGER = logging.getLogger(__name__)
@@ -61,7 +68,9 @@ def __init__(self, gw_dev, options):
)
self.friendly_name = gw_dev.name
self.floor_temp = options.get(CONF_FLOOR_TEMP, DEFAULT_FLOOR_TEMP)
- self.temp_precision = options.get(CONF_PRECISION)
+ self.temp_read_precision = options.get(CONF_READ_PRECISION)
+ self.temp_set_precision = options.get(CONF_SET_PRECISION)
+ self.temporary_ovrd_mode = options.get(CONF_TEMPORARY_OVRD_MODE, True)
self._available = False
self._current_operation = None
self._current_temperature = None
@@ -79,7 +88,9 @@ def __init__(self, gw_dev, options):
def update_options(self, entry):
"""Update climate entity options."""
self.floor_temp = entry.options[CONF_FLOOR_TEMP]
- self.temp_precision = entry.options[CONF_PRECISION]
+ self.temp_read_precision = entry.options[CONF_READ_PRECISION]
+ self.temp_set_precision = entry.options[CONF_SET_PRECISION]
+ self.temporary_ovrd_mode = entry.options[CONF_TEMPORARY_OVRD_MODE]
self.async_write_ha_state()
async def async_added_to_hass(self):
@@ -178,8 +189,8 @@ def unique_id(self):
@property
def precision(self):
"""Return the precision of the system."""
- if self.temp_precision is not None and self.temp_precision != 0:
- return self.temp_precision
+ if self.temp_read_precision:
+ return self.temp_read_precision
if self.hass.config.units.temperature_unit == TEMP_CELSIUS:
return PRECISION_HALVES
return PRECISION_WHOLE
@@ -234,7 +245,11 @@ def target_temperature(self):
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
- return self.precision
+ if self.temp_set_precision:
+ return self.temp_set_precision
+ if self.hass.config.units.temperature_unit == TEMP_CELSIUS:
+ return PRECISION_HALVES
+ return PRECISION_WHOLE
@property
def preset_mode(self):
@@ -259,7 +274,7 @@ async def async_set_temperature(self, **kwargs):
if temp == self.target_temperature:
return
self._new_target_temperature = await self._gateway.gateway.set_target_temp(
- temp
+ temp, self.temporary_ovrd_mode
)
self.async_write_ha_state()
diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py
index 8da530bebda5a7..aa764b7ae9e115 100644
--- a/homeassistant/components/opentherm_gw/config_flow.py
+++ b/homeassistant/components/opentherm_gw/config_flow.py
@@ -19,7 +19,12 @@
import homeassistant.helpers.config_validation as cv
from . import DOMAIN
-from .const import CONF_FLOOR_TEMP, CONF_PRECISION
+from .const import (
+ CONF_FLOOR_TEMP,
+ CONF_READ_PRECISION,
+ CONF_SET_PRECISION,
+ CONF_TEMPORARY_OVRD_MODE,
+)
class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@@ -121,14 +126,29 @@ async def async_step_init(self, user_input=None):
data_schema=vol.Schema(
{
vol.Optional(
- CONF_PRECISION,
- default=self.config_entry.options.get(CONF_PRECISION, 0),
+ CONF_READ_PRECISION,
+ default=self.config_entry.options.get(CONF_READ_PRECISION, 0),
): vol.All(
vol.Coerce(float),
vol.In(
[0, PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]
),
),
+ vol.Optional(
+ CONF_SET_PRECISION,
+ default=self.config_entry.options.get(CONF_SET_PRECISION, 0),
+ ): vol.All(
+ vol.Coerce(float),
+ vol.In(
+ [0, PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]
+ ),
+ ),
+ vol.Optional(
+ CONF_TEMPORARY_OVRD_MODE,
+ default=self.config_entry.options.get(
+ CONF_TEMPORARY_OVRD_MODE, True
+ ),
+ ): bool,
vol.Optional(
CONF_FLOOR_TEMP,
default=self.config_entry.options.get(CONF_FLOOR_TEMP, False),
diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py
index 2c3e2f7071d831..09713a69e5484a 100644
--- a/homeassistant/components/opentherm_gw/const.py
+++ b/homeassistant/components/opentherm_gw/const.py
@@ -19,6 +19,9 @@
CONF_CLIMATE = "climate"
CONF_FLOOR_TEMP = "floor_temperature"
CONF_PRECISION = "precision"
+CONF_READ_PRECISION = "read_precision"
+CONF_SET_PRECISION = "set_precision"
+CONF_TEMPORARY_OVRD_MODE = "temporary_override_mode"
DATA_GATEWAYS = "gateways"
DATA_OPENTHERM_GW = "opentherm_gw"
diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json
index 066cee61c050be..baa02dc3f4604c 100644
--- a/homeassistant/components/opentherm_gw/manifest.json
+++ b/homeassistant/components/opentherm_gw/manifest.json
@@ -2,7 +2,7 @@
"domain": "opentherm_gw",
"name": "OpenTherm Gateway",
"documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
- "requirements": ["pyotgw==1.0b1"],
+ "requirements": ["pyotgw==1.1b1"],
"codeowners": ["@mvn23"],
"config_flow": true
}
diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py
index 4a20aa651cd49e..1d9904ea59fd6d 100644
--- a/homeassistant/components/opentherm_gw/sensor.py
+++ b/homeassistant/components/opentherm_gw/sensor.py
@@ -2,11 +2,11 @@
import logging
from pprint import pformat
-from homeassistant.components.sensor import ENTITY_ID_FORMAT
+from homeassistant.components.sensor import ENTITY_ID_FORMAT, SensorEntity
from homeassistant.const import CONF_ID
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity, async_generate_entity_id
+from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.entity_registry import async_get_registry
from . import DOMAIN
@@ -77,7 +77,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors)
-class OpenThermSensor(Entity):
+class OpenThermSensor(SensorEntity):
"""Representation of an OpenTherm Gateway sensor."""
def __init__(self, gw_dev, var, source, device_class, unit, friendly_name_format):
diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json
index 306529e7be1329..ed9cf05cae89d4 100644
--- a/homeassistant/components/opentherm_gw/strings.json
+++ b/homeassistant/components/opentherm_gw/strings.json
@@ -22,7 +22,9 @@
"description": "Options for the OpenTherm Gateway",
"data": {
"floor_temperature": "Floor Temperature",
- "precision": "Precision"
+ "read_precision": "Read Precision",
+ "set_precision": "Set Precision",
+ "temporary_override_mode": "Temporary Setpoint Override Mode"
}
}
}
diff --git a/homeassistant/components/opentherm_gw/translations/ca.json b/homeassistant/components/opentherm_gw/translations/ca.json
index 1da9bbb584e827..3f38055fb8ca40 100644
--- a/homeassistant/components/opentherm_gw/translations/ca.json
+++ b/homeassistant/components/opentherm_gw/translations/ca.json
@@ -21,7 +21,10 @@
"init": {
"data": {
"floor_temperature": "Temperatura de la planta",
- "precision": "Precisi\u00f3"
+ "precision": "Precisi\u00f3",
+ "read_precision": "Llegeix precisi\u00f3",
+ "set_precision": "Defineix precisi\u00f3",
+ "temporary_override_mode": "Mode de sobreescriptura temporal"
},
"description": "Opcions del la passarel\u00b7la d'enlla\u00e7 d'OpenTherm"
}
diff --git a/homeassistant/components/opentherm_gw/translations/de.json b/homeassistant/components/opentherm_gw/translations/de.json
index 6e8d02bc7923c4..36b76592945016 100644
--- a/homeassistant/components/opentherm_gw/translations/de.json
+++ b/homeassistant/components/opentherm_gw/translations/de.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "already_configured": "Gateway bereits konfiguriert",
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
"cannot_connect": "Verbindung fehlgeschlagen",
"id_exists": "Gateway-ID ist bereits vorhanden"
},
diff --git a/homeassistant/components/opentherm_gw/translations/el.json b/homeassistant/components/opentherm_gw/translations/el.json
new file mode 100644
index 00000000000000..f15bc7bdc0e25b
--- /dev/null
+++ b/homeassistant/components/opentherm_gw/translations/el.json
@@ -0,0 +1,12 @@
+{
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "read_precision": "\u0394\u03b9\u03ac\u03b2\u03b1\u03c3\u03b5 \u03c4\u03b7\u03bd \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1",
+ "set_precision": "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1\u03c2"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/translations/en.json b/homeassistant/components/opentherm_gw/translations/en.json
index 9d74a168bae7eb..a4e9eb664b2231 100644
--- a/homeassistant/components/opentherm_gw/translations/en.json
+++ b/homeassistant/components/opentherm_gw/translations/en.json
@@ -21,7 +21,10 @@
"init": {
"data": {
"floor_temperature": "Floor Temperature",
- "precision": "Precision"
+ "precision": "Precision",
+ "read_precision": "Read Precision",
+ "set_precision": "Set Precision",
+ "temporary_override_mode": "Temporary Setpoint Override Mode"
},
"description": "Options for the OpenTherm Gateway"
}
diff --git a/homeassistant/components/opentherm_gw/translations/es-419.json b/homeassistant/components/opentherm_gw/translations/es-419.json
index 935be180777e78..3b3a32987be6c3 100644
--- a/homeassistant/components/opentherm_gw/translations/es-419.json
+++ b/homeassistant/components/opentherm_gw/translations/es-419.json
@@ -20,7 +20,9 @@
"init": {
"data": {
"floor_temperature": "Temperatura del piso",
- "precision": "Precisi\u00f3n"
+ "precision": "Precisi\u00f3n",
+ "read_precision": "Leer precisi\u00f3n",
+ "set_precision": "Establecer precisi\u00f3n"
},
"description": "Opciones para OpenTherm Gateway"
}
diff --git a/homeassistant/components/opentherm_gw/translations/es.json b/homeassistant/components/opentherm_gw/translations/es.json
index 44b6c6dfabcb4b..7a85b685e891d6 100644
--- a/homeassistant/components/opentherm_gw/translations/es.json
+++ b/homeassistant/components/opentherm_gw/translations/es.json
@@ -21,7 +21,9 @@
"init": {
"data": {
"floor_temperature": "Temperatura del suelo",
- "precision": "Precisi\u00f3n"
+ "precision": "Precisi\u00f3n",
+ "read_precision": "Leer precisi\u00f3n",
+ "set_precision": "Establecer precisi\u00f3n"
},
"description": "Opciones para OpenTherm Gateway"
}
diff --git a/homeassistant/components/opentherm_gw/translations/et.json b/homeassistant/components/opentherm_gw/translations/et.json
index 4ab500e5531f6f..9aef362a6a0d0d 100644
--- a/homeassistant/components/opentherm_gw/translations/et.json
+++ b/homeassistant/components/opentherm_gw/translations/et.json
@@ -21,7 +21,10 @@
"init": {
"data": {
"floor_temperature": "P\u00f5randa temperatuur",
- "precision": "T\u00e4psus"
+ "precision": "T\u00e4psus",
+ "read_precision": "Lugemi t\u00e4psus",
+ "set_precision": "M\u00e4\u00e4ra lugemi t\u00e4psus",
+ "temporary_override_mode": "Ajutine seadepunkti alistamine"
},
"description": "OpenTherm Gateway suvandid"
}
diff --git a/homeassistant/components/opentherm_gw/translations/fr.json b/homeassistant/components/opentherm_gw/translations/fr.json
index f060503ea23d14..7cc5b4ef848908 100644
--- a/homeassistant/components/opentherm_gw/translations/fr.json
+++ b/homeassistant/components/opentherm_gw/translations/fr.json
@@ -21,7 +21,9 @@
"init": {
"data": {
"floor_temperature": "Temp\u00e9rature du sol",
- "precision": "Pr\u00e9cision"
+ "precision": "Pr\u00e9cision",
+ "read_precision": "Pr\u00e9cision de lecture",
+ "set_precision": "D\u00e9finir la pr\u00e9cision"
},
"description": "Options pour la passerelle OpenTherm"
}
diff --git a/homeassistant/components/opentherm_gw/translations/hu.json b/homeassistant/components/opentherm_gw/translations/hu.json
index c3dd3f1020679e..9ca79a3ccddd51 100644
--- a/homeassistant/components/opentherm_gw/translations/hu.json
+++ b/homeassistant/components/opentherm_gw/translations/hu.json
@@ -1,7 +1,8 @@
{
"config": {
"error": {
- "already_configured": "Az \u00e1tj\u00e1r\u00f3 m\u00e1r konfigur\u00e1lva van",
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
"id_exists": "Az \u00e1tj\u00e1r\u00f3 azonos\u00edt\u00f3ja m\u00e1r l\u00e9tezik"
},
"step": {
@@ -20,7 +21,8 @@
"init": {
"data": {
"floor_temperature": "Padl\u00f3 h\u0151m\u00e9rs\u00e9klete",
- "precision": "Pontoss\u00e1g"
+ "precision": "Pontoss\u00e1g",
+ "temporary_override_mode": "Ideiglenes be\u00e1ll\u00edt\u00e1s fel\u00fclb\u00edr\u00e1l\u00e1si m\u00f3dja"
}
}
}
diff --git a/homeassistant/components/opentherm_gw/translations/id.json b/homeassistant/components/opentherm_gw/translations/id.json
new file mode 100644
index 00000000000000..7c7624c3dfe07b
--- /dev/null
+++ b/homeassistant/components/opentherm_gw/translations/id.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung",
+ "id_exists": "ID gateway sudah ada"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "device": "Jalur atau URL",
+ "id": "ID",
+ "name": "Nama"
+ },
+ "title": "Gateway OpenTherm"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "Suhu Lantai",
+ "precision": "Tingkat Presisi"
+ },
+ "description": "Pilihan untuk Gateway OpenTherm"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/translations/it.json b/homeassistant/components/opentherm_gw/translations/it.json
index df1c36cd8d599d..a082cd87586f3e 100644
--- a/homeassistant/components/opentherm_gw/translations/it.json
+++ b/homeassistant/components/opentherm_gw/translations/it.json
@@ -21,7 +21,10 @@
"init": {
"data": {
"floor_temperature": "Temperatura del pavimento",
- "precision": "Precisione"
+ "precision": "Precisione",
+ "read_precision": "Leggi la precisione",
+ "set_precision": "Imposta la precisione",
+ "temporary_override_mode": "Modalit\u00e0 di esclusione temporanea del setpoint"
},
"description": "Opzioni per OpenTherm Gateway"
}
diff --git a/homeassistant/components/opentherm_gw/translations/ko.json b/homeassistant/components/opentherm_gw/translations/ko.json
index eece1492002b56..658fd24348e38e 100644
--- a/homeassistant/components/opentherm_gw/translations/ko.json
+++ b/homeassistant/components/opentherm_gw/translations/ko.json
@@ -1,7 +1,8 @@
{
"config": {
"error": {
- "already_configured": "OpenTherm Gateway \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"id_exists": "OpenTherm Gateway id \uac00 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4"
},
"step": {
@@ -20,7 +21,10 @@
"init": {
"data": {
"floor_temperature": "\uc628\ub3c4 \uc18c\uc218\uc810 \ubc84\ub9bc",
- "precision": "\uc815\ubc00\ub3c4"
+ "precision": "\uc815\ubc00\ub3c4",
+ "read_precision": "\uc77d\uae30 \uc815\ubc00\ub3c4",
+ "set_precision": "\uc815\ubc00\ub3c4 \uc124\uc815\ud558\uae30",
+ "temporary_override_mode": "\uc784\uc2dc \uc124\uc815\uac12 \uc7ac\uc815\uc758 \ubaa8\ub4dc"
},
"description": "OpenTherm Gateway \uc635\uc158"
}
diff --git a/homeassistant/components/opentherm_gw/translations/nl.json b/homeassistant/components/opentherm_gw/translations/nl.json
index 7c9c89381e8e6e..5a4d868e81efc9 100644
--- a/homeassistant/components/opentherm_gw/translations/nl.json
+++ b/homeassistant/components/opentherm_gw/translations/nl.json
@@ -2,6 +2,7 @@
"config": {
"error": {
"already_configured": "Gateway al geconfigureerd",
+ "cannot_connect": "Kan geen verbinding maken",
"id_exists": "Gateway id bestaat al"
},
"step": {
@@ -20,7 +21,10 @@
"init": {
"data": {
"floor_temperature": "Vloertemperatuur",
- "precision": "Precisie"
+ "precision": "Precisie",
+ "read_precision": "Lees Precisie",
+ "set_precision": "Precisie instellen",
+ "temporary_override_mode": "Tijdelijke setpoint-overschrijvingsmodus"
},
"description": "Opties voor de OpenTherm Gateway"
}
diff --git a/homeassistant/components/opentherm_gw/translations/no.json b/homeassistant/components/opentherm_gw/translations/no.json
index 76118924e0a2f6..8d82d3c6106b1e 100644
--- a/homeassistant/components/opentherm_gw/translations/no.json
+++ b/homeassistant/components/opentherm_gw/translations/no.json
@@ -21,7 +21,10 @@
"init": {
"data": {
"floor_temperature": "Etasje Temperatur",
- "precision": "Presisjon"
+ "precision": "Presisjon",
+ "read_precision": "Les presisjon",
+ "set_precision": "Angi presisjon",
+ "temporary_override_mode": "Midlertidig overstyringsmodus for settpunkt"
},
"description": "Alternativer for OpenTherm Gateway"
}
diff --git a/homeassistant/components/opentherm_gw/translations/pl.json b/homeassistant/components/opentherm_gw/translations/pl.json
index 3fe12393a149b3..dc06752e404282 100644
--- a/homeassistant/components/opentherm_gw/translations/pl.json
+++ b/homeassistant/components/opentherm_gw/translations/pl.json
@@ -21,7 +21,9 @@
"init": {
"data": {
"floor_temperature": "Zaokr\u0105glanie warto\u015bci w d\u00f3\u0142",
- "precision": "Precyzja"
+ "precision": "Precyzja",
+ "read_precision": "Odczytaj precyzj\u0119",
+ "set_precision": "Ustaw precyzj\u0119"
},
"description": "Opcje dla bramki OpenTherm"
}
diff --git a/homeassistant/components/opentherm_gw/translations/ru.json b/homeassistant/components/opentherm_gw/translations/ru.json
index e63bfb58d9582c..3b10be1166a909 100644
--- a/homeassistant/components/opentherm_gw/translations/ru.json
+++ b/homeassistant/components/opentherm_gw/translations/ru.json
@@ -21,7 +21,10 @@
"init": {
"data": {
"floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u043e\u043b\u0430",
- "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c"
+ "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c",
+ "read_precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0447\u0442\u0435\u043d\u0438\u044f",
+ "set_precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438",
+ "temporary_override_mode": "\u0420\u0435\u0436\u0438\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0430\u0432\u043a\u0438"
},
"description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0430 Opentherm"
}
diff --git a/homeassistant/components/opentherm_gw/translations/tr.json b/homeassistant/components/opentherm_gw/translations/tr.json
new file mode 100644
index 00000000000000..507b71ede5b270
--- /dev/null
+++ b/homeassistant/components/opentherm_gw/translations/tr.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "device": "Yol veya URL"
+ },
+ "title": "OpenTherm A\u011f Ge\u00e7idi"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/translations/uk.json b/homeassistant/components/opentherm_gw/translations/uk.json
new file mode 100644
index 00000000000000..af7699271136b0
--- /dev/null
+++ b/homeassistant/components/opentherm_gw/translations/uk.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "id_exists": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0448\u043b\u044e\u0437\u0443 \u0432\u0436\u0435 \u0456\u0441\u043d\u0443\u0454."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "device": "\u0428\u043b\u044f\u0445 \u0430\u0431\u043e URL-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440",
+ "name": "\u041d\u0430\u0437\u0432\u0430"
+ },
+ "title": "OpenTherm"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u0456\u0434\u043b\u043e\u0433\u0438",
+ "precision": "\u0422\u043e\u0447\u043d\u0456\u0441\u0442\u044c"
+ },
+ "description": "\u0414\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0443 Opentherm"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/translations/zh-Hant.json b/homeassistant/components/opentherm_gw/translations/zh-Hant.json
index ea138287c78359..8273eb1de983c3 100644
--- a/homeassistant/components/opentherm_gw/translations/zh-Hant.json
+++ b/homeassistant/components/opentherm_gw/translations/zh-Hant.json
@@ -21,7 +21,10 @@
"init": {
"data": {
"floor_temperature": "\u6a13\u5c64\u6eab\u5ea6",
- "precision": "\u6e96\u78ba\u5ea6"
+ "precision": "\u6e96\u78ba\u5ea6",
+ "read_precision": "\u8b80\u53d6\u7cbe\u6e96\u5ea6",
+ "set_precision": "\u8a2d\u5b9a\u7cbe\u6e96\u5ea6",
+ "temporary_override_mode": "\u81e8\u6642 Setpoint \u8986\u84cb\u6a21\u5f0f"
},
"description": "OpenTherm \u9598\u9053\u5668\u9078\u9805"
}
diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py
index ce75365771d4e1..aeefe435845d93 100644
--- a/homeassistant/components/openuv/__init__.py
+++ b/homeassistant/components/openuv/__init__.py
@@ -69,9 +69,9 @@ async def async_setup_entry(hass, config_entry):
LOGGER.error("Config entry failed: %s", err)
raise ConfigEntryNotReady from err
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
@_verify_domain_control
@@ -110,8 +110,8 @@ async def async_unload_entry(hass, config_entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -187,7 +187,7 @@ def available(self) -> bool:
return self._available
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attrs
diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py
index 9dfb053ff01b8d..172b0ed3c443a7 100644
--- a/homeassistant/components/openuv/config_flow.py
+++ b/homeassistant/components/openuv/config_flow.py
@@ -12,7 +12,7 @@
)
from homeassistant.helpers import aiohttp_client, config_validation as cv
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import DOMAIN
CONFIG_SCHEMA = vol.Schema(
{
diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py
index b9c73023c1124b..654a89cfcf96b4 100644
--- a/homeassistant/components/openuv/sensor.py
+++ b/homeassistant/components/openuv/sensor.py
@@ -1,4 +1,5 @@
"""Support for OpenUV sensors."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import TIME_MINUTES, UV_INDEX
from homeassistant.core import callback
from homeassistant.util.dt import as_local, parse_datetime
@@ -87,7 +88,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
async_add_entities(sensors, True)
-class OpenUvSensor(OpenUvEntity):
+class OpenUvSensor(OpenUvEntity, SensorEntity):
"""Define a binary sensor for OpenUV."""
def __init__(self, openuv, sensor_type, name, icon, unit, entry_id):
diff --git a/homeassistant/components/openuv/translations/de.json b/homeassistant/components/openuv/translations/de.json
index fae3f0f062095b..88f9e69a5b6502 100644
--- a/homeassistant/components/openuv/translations/de.json
+++ b/homeassistant/components/openuv/translations/de.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Diese Koordinaten sind bereits registriert."
+ "already_configured": "Standort ist bereits konfiguriert"
},
"error": {
"invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel"
diff --git a/homeassistant/components/openuv/translations/hu.json b/homeassistant/components/openuv/translations/hu.json
index 3a6f6a8ae9183a..b5c0e5ec6081b9 100644
--- a/homeassistant/components/openuv/translations/hu.json
+++ b/homeassistant/components/openuv/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "A hely m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
"invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs"
},
diff --git a/homeassistant/components/openuv/translations/id.json b/homeassistant/components/openuv/translations/id.json
index 4d0edd93ea9625..5075ec2c965b8f 100644
--- a/homeassistant/components/openuv/translations/id.json
+++ b/homeassistant/components/openuv/translations/id.json
@@ -1,15 +1,18 @@
{
"config": {
+ "abort": {
+ "already_configured": "Lokasi sudah dikonfigurasi"
+ },
"error": {
"invalid_api_key": "Kunci API tidak valid"
},
"step": {
"user": {
"data": {
- "api_key": "Kunci API OpenUV",
+ "api_key": "Kunci API",
"elevation": "Ketinggian",
"latitude": "Lintang",
- "longitude": "Garis bujur"
+ "longitude": "Bujur"
},
"title": "Isi informasi Anda"
}
diff --git a/homeassistant/components/openuv/translations/ko.json b/homeassistant/components/openuv/translations/ko.json
index 480b745fe36031..114be1df692360 100644
--- a/homeassistant/components/openuv/translations/ko.json
+++ b/homeassistant/components/openuv/translations/ko.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\uc88c\ud45c\uac12\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
"invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
diff --git a/homeassistant/components/openuv/translations/nl.json b/homeassistant/components/openuv/translations/nl.json
index 118e8f05141ebf..d7287b99ddf5c6 100644
--- a/homeassistant/components/openuv/translations/nl.json
+++ b/homeassistant/components/openuv/translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Deze co\u00f6rdinaten zijn al geregistreerd."
+ "already_configured": "Locatie is al geconfigureerd."
},
"error": {
"invalid_api_key": "Ongeldige API-sleutel"
@@ -9,7 +9,7 @@
"step": {
"user": {
"data": {
- "api_key": "OpenUV API-Sleutel",
+ "api_key": "API-sleutel",
"elevation": "Hoogte",
"latitude": "Breedtegraad",
"longitude": "Lengtegraad"
diff --git a/homeassistant/components/openuv/translations/tr.json b/homeassistant/components/openuv/translations/tr.json
new file mode 100644
index 00000000000000..241c588f691865
--- /dev/null
+++ b/homeassistant/components/openuv/translations/tr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Anahtar\u0131",
+ "latitude": "Enlem",
+ "longitude": "Boylam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openuv/translations/uk.json b/homeassistant/components/openuv/translations/uk.json
index fef350a3f3cfd8..bd29fe692e1b3b 100644
--- a/homeassistant/components/openuv/translations/uk.json
+++ b/homeassistant/components/openuv/translations/uk.json
@@ -1,13 +1,20 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435."
+ },
+ "error": {
+ "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API"
+ },
"step": {
"user": {
"data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
"elevation": "\u0412\u0438\u0441\u043e\u0442\u0430",
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
"longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430"
},
- "title": "\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u0432\u0430\u0448\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e"
+ "title": "OpenUV"
}
}
}
diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py
index 4754c4b2eff5cc..f6d47d1dcae01f 100644
--- a/homeassistant/components/openweathermap/__init__.py
+++ b/homeassistant/components/openweathermap/__init__.py
@@ -14,10 +14,8 @@
CONF_NAME,
)
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
from .const import (
- COMPONENTS,
CONF_LANGUAGE,
CONFIG_FLOW_VERSION,
DOMAIN,
@@ -25,6 +23,7 @@
ENTRY_WEATHER_COORDINATOR,
FORECAST_MODE_FREE_DAILY,
FORECAST_MODE_ONECALL_DAILY,
+ PLATFORMS,
UPDATE_LISTENER,
)
from .weather_update_coordinator import WeatherUpdateCoordinator
@@ -54,10 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
owm, latitude, longitude, forecast_mode, hass
)
- await weather_coordinator.async_refresh()
-
- if not weather_coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await weather_coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = {
@@ -65,9 +61,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
ENTRY_WEATHER_COORDINATOR: weather_coordinator,
}
- for component in COMPONENTS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
update_listener = config_entry.add_update_listener(async_update_options)
@@ -108,8 +104,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in COMPONENTS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/openweathermap/abstract_owm_sensor.py b/homeassistant/components/openweathermap/abstract_owm_sensor.py
index 809d2c2e572b64..30a21a057f0fba 100644
--- a/homeassistant/components/openweathermap/abstract_owm_sensor.py
+++ b/homeassistant/components/openweathermap/abstract_owm_sensor.py
@@ -1,12 +1,12 @@
"""Abstraction form OWM sensors."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import ATTRIBUTION, SENSOR_DEVICE_CLASS, SENSOR_NAME, SENSOR_UNIT
-class AbstractOpenWeatherMapSensor(Entity):
+class AbstractOpenWeatherMapSensor(SensorEntity):
"""Abstract class for an OpenWeatherMap sensor."""
def __init__(
@@ -57,7 +57,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/openweathermap/config_flow.py b/homeassistant/components/openweathermap/config_flow.py
index 2c2070141d51c7..7be4fe795aca1f 100644
--- a/homeassistant/components/openweathermap/config_flow.py
+++ b/homeassistant/components/openweathermap/config_flow.py
@@ -20,10 +20,10 @@
DEFAULT_FORECAST_MODE,
DEFAULT_LANGUAGE,
DEFAULT_NAME,
+ DOMAIN,
FORECAST_MODES,
LANGUAGES,
)
-from .const import DOMAIN # pylint:disable=unused-import
class OpenWeatherMapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py
index c70afa9cab06a4..36d38ff4688f9c 100644
--- a/homeassistant/components/openweathermap/const.py
+++ b/homeassistant/components/openweathermap/const.py
@@ -16,6 +16,7 @@
ATTR_CONDITION_WINDY_VARIANT,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_PRESSURE,
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
@@ -34,6 +35,7 @@
PRESSURE_HPA,
SPEED_METERS_PER_SECOND,
TEMP_CELSIUS,
+ UV_INDEX,
)
DOMAIN = "openweathermap"
@@ -45,9 +47,12 @@
ENTRY_NAME = "name"
ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
ATTR_API_PRECIPITATION = "precipitation"
+ATTR_API_PRECIPITATION_KIND = "precipitation_kind"
ATTR_API_DATETIME = "datetime"
+ATTR_API_DEW_POINT = "dew_point"
ATTR_API_WEATHER = "weather"
ATTR_API_TEMPERATURE = "temperature"
+ATTR_API_FEELS_LIKE_TEMPERATURE = "feels_like_temperature"
ATTR_API_WIND_SPEED = "wind_speed"
ATTR_API_WIND_BEARING = "wind_bearing"
ATTR_API_HUMIDITY = "humidity"
@@ -56,13 +61,14 @@
ATTR_API_CLOUDS = "clouds"
ATTR_API_RAIN = "rain"
ATTR_API_SNOW = "snow"
+ATTR_API_UV_INDEX = "uv_index"
ATTR_API_WEATHER_CODE = "weather_code"
ATTR_API_FORECAST = "forecast"
SENSOR_NAME = "sensor_name"
SENSOR_UNIT = "sensor_unit"
SENSOR_DEVICE_CLASS = "sensor_device_class"
UPDATE_LISTENER = "update_listener"
-COMPONENTS = ["sensor", "weather"]
+PLATFORMS = ["sensor", "weather"]
FORECAST_MODE_HOURLY = "hourly"
FORECAST_MODE_DAILY = "daily"
@@ -79,7 +85,9 @@
MONITORED_CONDITIONS = [
ATTR_API_WEATHER,
+ ATTR_API_DEW_POINT,
ATTR_API_TEMPERATURE,
+ ATTR_API_FEELS_LIKE_TEMPERATURE,
ATTR_API_WIND_SPEED,
ATTR_API_WIND_BEARING,
ATTR_API_HUMIDITY,
@@ -87,12 +95,15 @@
ATTR_API_CLOUDS,
ATTR_API_RAIN,
ATTR_API_SNOW,
+ ATTR_API_PRECIPITATION_KIND,
+ ATTR_API_UV_INDEX,
ATTR_API_CONDITION,
ATTR_API_WEATHER_CODE,
]
FORECAST_MONITORED_CONDITIONS = [
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_PRESSURE,
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
@@ -183,11 +194,21 @@
}
WEATHER_SENSOR_TYPES = {
ATTR_API_WEATHER: {SENSOR_NAME: "Weather"},
+ ATTR_API_DEW_POINT: {
+ SENSOR_NAME: "Dew Point",
+ SENSOR_UNIT: TEMP_CELSIUS,
+ SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
+ },
ATTR_API_TEMPERATURE: {
SENSOR_NAME: "Temperature",
SENSOR_UNIT: TEMP_CELSIUS,
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
},
+ ATTR_API_FEELS_LIKE_TEMPERATURE: {
+ SENSOR_NAME: "Feels like temperature",
+ SENSOR_UNIT: TEMP_CELSIUS,
+ SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
+ },
ATTR_API_WIND_SPEED: {
SENSOR_NAME: "Wind speed",
SENSOR_UNIT: SPEED_METERS_PER_SECOND,
@@ -206,12 +227,24 @@
ATTR_API_CLOUDS: {SENSOR_NAME: "Cloud coverage", SENSOR_UNIT: PERCENTAGE},
ATTR_API_RAIN: {SENSOR_NAME: "Rain", SENSOR_UNIT: LENGTH_MILLIMETERS},
ATTR_API_SNOW: {SENSOR_NAME: "Snow", SENSOR_UNIT: LENGTH_MILLIMETERS},
+ ATTR_API_PRECIPITATION_KIND: {SENSOR_NAME: "Precipitation kind"},
+ ATTR_API_UV_INDEX: {
+ SENSOR_NAME: "UV Index",
+ SENSOR_UNIT: UV_INDEX,
+ },
ATTR_API_CONDITION: {SENSOR_NAME: "Condition"},
ATTR_API_WEATHER_CODE: {SENSOR_NAME: "Weather Code"},
}
FORECAST_SENSOR_TYPES = {
ATTR_FORECAST_CONDITION: {SENSOR_NAME: "Condition"},
- ATTR_FORECAST_PRECIPITATION: {SENSOR_NAME: "Precipitation"},
+ ATTR_FORECAST_PRECIPITATION: {
+ SENSOR_NAME: "Precipitation",
+ SENSOR_UNIT: LENGTH_MILLIMETERS,
+ },
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: {
+ SENSOR_NAME: "Precipitation probability",
+ SENSOR_UNIT: PERCENTAGE,
+ },
ATTR_FORECAST_PRESSURE: {SENSOR_NAME: "Pressure"},
ATTR_FORECAST_TEMP: {
SENSOR_NAME: "Temperature",
diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json
index e355e2e4752e49..27cda9fb26dcd2 100644
--- a/homeassistant/components/openweathermap/manifest.json
+++ b/homeassistant/components/openweathermap/manifest.json
@@ -3,6 +3,6 @@
"name": "OpenWeatherMap",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/openweathermap",
- "requirements": ["pyowm==3.1.1"],
+ "requirements": ["pyowm==3.2.0"],
"codeowners": ["@fabaff", "@freekode", "@nzapponi"]
}
diff --git a/homeassistant/components/openweathermap/translations/de.json b/homeassistant/components/openweathermap/translations/de.json
index 239b47e2d3e0c1..cac601b71d32cd 100644
--- a/homeassistant/components/openweathermap/translations/de.json
+++ b/homeassistant/components/openweathermap/translations/de.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "already_configured": "Standort ist bereits konfiguriert"
+ },
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel"
},
"step": {
"user": {
diff --git a/homeassistant/components/openweathermap/translations/et.json b/homeassistant/components/openweathermap/translations/et.json
index 26c688482fd9d3..e548c07236ee80 100644
--- a/homeassistant/components/openweathermap/translations/et.json
+++ b/homeassistant/components/openweathermap/translations/et.json
@@ -17,7 +17,7 @@
"mode": "Re\u017eiim",
"name": "Sidumise nimi"
},
- "description": "Seadistage OpenWeatherMapi sidumine. API-v\u00f5tme loomiseks minge aadressile https://openweathermap.org/appid",
+ "description": "Seadista OpenWeatherMapi sidumine. API-v\u00f5tme loomiseks mine aadressile https://openweathermap.org/appid",
"title": "OpenWeatherMap"
}
}
diff --git a/homeassistant/components/openweathermap/translations/he.json b/homeassistant/components/openweathermap/translations/he.json
new file mode 100644
index 00000000000000..4c49313d97741a
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/translations/hu.json b/homeassistant/components/openweathermap/translations/hu.json
new file mode 100644
index 00000000000000..2fd2f0acc7a354
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/hu.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A hely m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API kulcs",
+ "language": "Nyelv",
+ "latitude": "Sz\u00e9less\u00e9g",
+ "longitude": "Hossz\u00fas\u00e1g",
+ "mode": "M\u00f3d",
+ "name": "Az integr\u00e1ci\u00f3 neve"
+ },
+ "description": "Az OpenWeatherMap integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa. Az API kulcs l\u00e9trehoz\u00e1s\u00e1hoz menj az https://openweathermap.org/appid oldalra",
+ "title": "OpenWeatherMap"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "Nyelv",
+ "mode": "M\u00f3d"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/translations/id.json b/homeassistant/components/openweathermap/translations/id.json
new file mode 100644
index 00000000000000..61d2713f42a094
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/id.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Lokasi sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_api_key": "Kunci API tidak valid"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kunci API",
+ "language": "Bahasa",
+ "latitude": "Lintang",
+ "longitude": "Bujur",
+ "mode": "Mode",
+ "name": "Nama integrasi"
+ },
+ "description": "Siapkan integrasi OpenWeatherMap. Untuk membuat kunci API, buka https://openweathermap.org/appid",
+ "title": "OpenWeatherMap"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "Bahasa",
+ "mode": "Mode"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/translations/ko.json b/homeassistant/components/openweathermap/translations/ko.json
index 9560f447250a15..d2f5d9aa1230b9 100644
--- a/homeassistant/components/openweathermap/translations/ko.json
+++ b/homeassistant/components/openweathermap/translations/ko.json
@@ -1,19 +1,23 @@
{
"config": {
"abort": {
- "already_configured": "\uc774\ub7ec\ud55c \uc88c\ud45c\uc5d0 \ub300\ud55c OpenWeatherMap \ud1b5\ud569\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "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": "OpenWeatherMap API \ud0a4",
+ "api_key": "API \ud0a4",
"language": "\uc5b8\uc5b4",
"latitude": "\uc704\ub3c4",
"longitude": "\uacbd\ub3c4",
"mode": "\ubaa8\ub4dc",
- "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uba85"
+ "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc774\ub984"
},
- "description": "OpenWeatherMap \ud1b5\ud569\uc744 \uc124\uc815\ud558\uc138\uc694. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://openweathermap.org/appid\ub85c \uc774\ub3d9\ud558\uc2ed\uc2dc\uc624.",
+ "description": "OpenWeatherMap \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://openweathermap.org/appid \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694",
"title": "OpenWeatherMap"
}
}
diff --git a/homeassistant/components/openweathermap/translations/tr.json b/homeassistant/components/openweathermap/translations/tr.json
new file mode 100644
index 00000000000000..0f845a4df73453
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/tr.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Anahtar\u0131",
+ "latitude": "Enlem",
+ "longitude": "Boylam",
+ "mode": "Mod"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "mode": "Mod"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/translations/uk.json b/homeassistant/components/openweathermap/translations/uk.json
new file mode 100644
index 00000000000000..7a39cfa078e829
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/uk.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "language": "\u041c\u043e\u0432\u0430",
+ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
+ "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430",
+ "mode": "\u0420\u0435\u0436\u0438\u043c",
+ "name": "\u041d\u0430\u0437\u0432\u0430"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 OpenWeatherMap. \u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043d\u0430 https://openweathermap.org/appid.",
+ "title": "OpenWeatherMap"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "language": "\u041c\u043e\u0432\u0430",
+ "mode": "\u0420\u0435\u0436\u0438\u043c"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py
index 93db4ca26d8125..20cc71da72573b 100644
--- a/homeassistant/components/openweathermap/weather_update_coordinator.py
+++ b/homeassistant/components/openweathermap/weather_update_coordinator.py
@@ -10,6 +10,7 @@
ATTR_CONDITION_SUNNY,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_PRESSURE,
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
@@ -24,12 +25,16 @@
from .const import (
ATTR_API_CLOUDS,
ATTR_API_CONDITION,
+ ATTR_API_DEW_POINT,
+ ATTR_API_FEELS_LIKE_TEMPERATURE,
ATTR_API_FORECAST,
ATTR_API_HUMIDITY,
+ ATTR_API_PRECIPITATION_KIND,
ATTR_API_PRESSURE,
ATTR_API_RAIN,
ATTR_API_SNOW,
ATTR_API_TEMPERATURE,
+ ATTR_API_UV_INDEX,
ATTR_API_WEATHER,
ATTR_API_WEATHER_CODE,
ATTR_API_WIND_BEARING,
@@ -114,6 +119,10 @@ def _convert_weather_response(self, weather_response):
return {
ATTR_API_TEMPERATURE: current_weather.temperature("celsius").get("temp"),
+ ATTR_API_FEELS_LIKE_TEMPERATURE: current_weather.temperature("celsius").get(
+ "feels_like"
+ ),
+ ATTR_API_DEW_POINT: self._fmt_dewpoint(current_weather.dewpoint),
ATTR_API_PRESSURE: current_weather.pressure.get("press"),
ATTR_API_HUMIDITY: current_weather.humidity,
ATTR_API_WIND_BEARING: current_weather.wind().get("deg"),
@@ -121,8 +130,12 @@ def _convert_weather_response(self, weather_response):
ATTR_API_CLOUDS: current_weather.clouds,
ATTR_API_RAIN: self._get_rain(current_weather.rain),
ATTR_API_SNOW: self._get_snow(current_weather.snow),
+ ATTR_API_PRECIPITATION_KIND: self._calc_precipitation_kind(
+ current_weather.rain, current_weather.snow
+ ),
ATTR_API_WEATHER: current_weather.detailed_status,
ATTR_API_CONDITION: self._get_condition(current_weather.weather_code),
+ ATTR_API_UV_INDEX: current_weather.uvi,
ATTR_API_WEATHER_CODE: current_weather.weather_code,
ATTR_API_FORECAST: forecast_weather,
}
@@ -139,10 +152,15 @@ def _get_forecast_from_weather_response(self, weather_response):
def _convert_forecast(self, entry):
forecast = {
- ATTR_FORECAST_TIME: dt.utc_from_timestamp(entry.reference_time("unix")),
+ ATTR_FORECAST_TIME: dt.utc_from_timestamp(
+ entry.reference_time("unix")
+ ).isoformat(),
ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(
entry.rain, entry.snow
),
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: (
+ round(entry.precipitation_probability * 100)
+ ),
ATTR_FORECAST_PRESSURE: entry.pressure.get("press"),
ATTR_FORECAST_WIND_SPEED: entry.wind().get("speed"),
ATTR_FORECAST_WIND_BEARING: entry.wind().get("deg"),
@@ -160,40 +178,55 @@ def _convert_forecast(self, entry):
return forecast
+ @staticmethod
+ def _fmt_dewpoint(dewpoint):
+ if dewpoint is not None:
+ return round(dewpoint / 100, 1)
+ return None
+
@staticmethod
def _get_rain(rain):
"""Get rain data from weather data."""
if "all" in rain:
- return round(rain["all"], 0)
+ return round(rain["all"], 2)
if "1h" in rain:
- return round(rain["1h"], 0)
- return "not raining"
+ return round(rain["1h"], 2)
+ return 0
@staticmethod
def _get_snow(snow):
"""Get snow data from weather data."""
if snow:
if "all" in snow:
- return round(snow["all"], 0)
+ return round(snow["all"], 2)
if "1h" in snow:
- return round(snow["1h"], 0)
- return "not snowing"
- return "not snowing"
+ return round(snow["1h"], 2)
+ return 0
@staticmethod
def _calc_precipitation(rain, snow):
"""Calculate the precipitation."""
rain_value = 0
- if WeatherUpdateCoordinator._get_rain(rain) != "not raining":
+ if WeatherUpdateCoordinator._get_rain(rain) != 0:
rain_value = WeatherUpdateCoordinator._get_rain(rain)
snow_value = 0
- if WeatherUpdateCoordinator._get_snow(snow) != "not snowing":
+ if WeatherUpdateCoordinator._get_snow(snow) != 0:
snow_value = WeatherUpdateCoordinator._get_snow(snow)
- if round(rain_value + snow_value, 1) == 0:
- return None
- return round(rain_value + snow_value, 1)
+ return round(rain_value + snow_value, 2)
+
+ @staticmethod
+ def _calc_precipitation_kind(rain, snow):
+ """Determine the precipitation kind."""
+ if WeatherUpdateCoordinator._get_rain(rain) != 0:
+ if WeatherUpdateCoordinator._get_snow(snow) != 0:
+ return "Snow and Rain"
+ return "Rain"
+
+ if WeatherUpdateCoordinator._get_snow(snow) != 0:
+ return "Snow"
+ return "None"
def _get_condition(self, weather_code, timestamp=None):
"""Get weather condition from weather data."""
diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py
index d6620ed39e5839..063c0c6169d3be 100644
--- a/homeassistant/components/oru/sensor.py
+++ b/homeassistant/components/oru/sensor.py
@@ -5,10 +5,9 @@
from oru import Meter, MeterError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ENERGY_KILO_WATT_HOUR
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -39,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_LOGGER.debug("Oru meter_number = %s", meter_number)
-class CurrentEnergyUsageSensor(Entity):
+class CurrentEnergyUsageSensor(SensorEntity):
"""Representation of the sensor."""
def __init__(self, meter):
diff --git a/homeassistant/components/orvibo/switch.py b/homeassistant/components/orvibo/switch.py
index a03d9fc5ff0d8a..f7a16036f009f3 100644
--- a/homeassistant/components/orvibo/switch.py
+++ b/homeassistant/components/orvibo/switch.py
@@ -44,7 +44,7 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None):
switch_conf = config.get(CONF_SWITCHES, [config])
if config.get(CONF_DISCOVERY):
- _LOGGER.info("Discovering S20 switches ...")
+ _LOGGER.info("Discovering S20 switches")
switch_data.update(discover())
for switch in switch_conf:
diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py
index 49c32da69bccc3..e01ad9704886a3 100644
--- a/homeassistant/components/osramlightify/light.py
+++ b/homeassistant/components/osramlightify/light.py
@@ -269,7 +269,7 @@ def unique_id(self):
return self._unique_id
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
return self._device_attributes
diff --git a/homeassistant/components/osramlightify/manifest.json b/homeassistant/components/osramlightify/manifest.json
index dfe71d4b9e1ec2..80cfeff6e12bd1 100644
--- a/homeassistant/components/osramlightify/manifest.json
+++ b/homeassistant/components/osramlightify/manifest.json
@@ -2,6 +2,6 @@
"domain": "osramlightify",
"name": "Osramlightify",
"documentation": "https://www.home-assistant.io/integrations/osramlightify",
- "requirements": ["lightify==1.0.7.2"],
+ "requirements": ["lightify==1.0.7.3"],
"codeowners": []
}
diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py
index 9f23f5ae6faa2e..7aee9d99208d11 100644
--- a/homeassistant/components/otp/sensor.py
+++ b/homeassistant/components/otp/sensor.py
@@ -4,11 +4,10 @@
import pyotp
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, CONF_TOKEN
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
DEFAULT_NAME = "OTP Sensor"
@@ -34,7 +33,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
# Only TOTP supported at the moment, HOTP might be added later
-class TOTPSensor(Entity):
+class TOTPSensor(SensorEntity):
"""Representation of a TOTP sensor."""
def __init__(self, name, token):
diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py
index 0130ba30c3046c..98ed42ea10e428 100644
--- a/homeassistant/components/ovo_energy/__init__.py
+++ b/homeassistant/components/ovo_energy/__init__.py
@@ -1,14 +1,16 @@
"""Support for OVO Energy."""
+from __future__ import annotations
+
from datetime import datetime, timedelta
import logging
-from typing import Any, Dict
+from typing import Any
import aiohttp
import async_timeout
from ovoenergy import OVODailyUsage
from ovoenergy.ovoenergy import OVOEnergy
-from homeassistant.config_entries import ConfigEntry
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
@@ -44,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
if not authenticated:
hass.async_create_task(
hass.config_entries.flow.async_init(
- DOMAIN, context={"source": "reauth"}, data=entry.data
+ DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data
)
)
return False
@@ -61,7 +63,7 @@ async def async_update_data() -> OVODailyUsage:
if not authenticated:
hass.async_create_task(
hass.config_entries.flow.async_init(
- DOMAIN, context={"source": "reauth"}, data=entry.data
+ DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data
)
)
raise UpdateFailed("Not authenticated with OVO Energy")
@@ -84,7 +86,7 @@ async def async_update_data() -> OVODailyUsage:
}
# Fetch initial data so we have data when entities subscribe
- await coordinator.async_refresh()
+ await coordinator.async_config_entry_first_refresh()
# Setup components
hass.async_create_task(
@@ -148,7 +150,7 @@ class OVOEnergyDeviceEntity(OVOEnergyEntity):
"""Defines a OVO Energy device entity."""
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about this OVO Energy instance."""
return {
"identifiers": {(DOMAIN, self._client.account_id)},
diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py
index f395415d89e104..f65b8007ecb3e8 100644
--- a/homeassistant/components/ovo_energy/config_flow.py
+++ b/homeassistant/components/ovo_energy/config_flow.py
@@ -7,7 +7,7 @@
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import DOMAIN
REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
USER_SCHEMA = vol.Schema(
@@ -62,7 +62,6 @@ async def async_step_reauth(self, user_input):
if user_input and user_input.get(CONF_USERNAME):
self.username = user_input[CONF_USERNAME]
- # pylint: disable=no-member
self.context["title_placeholders"] = {CONF_USERNAME: self.username}
if user_input is not None and user_input.get(CONF_PASSWORD) is not None:
diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py
index 2f2e1b8dd50d92..d03f7c49f96aaf 100644
--- a/homeassistant/components/ovo_energy/sensor.py
+++ b/homeassistant/components/ovo_energy/sensor.py
@@ -4,6 +4,7 @@
from ovoenergy import OVODailyUsage
from ovoenergy.ovoenergy import OVOEnergy
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@@ -53,7 +54,7 @@ async def async_setup_entry(
async_add_entities(entities, True)
-class OVOEnergySensor(OVOEnergyDeviceEntity):
+class OVOEnergySensor(OVOEnergyDeviceEntity, SensorEntity):
"""Defines a OVO Energy sensor."""
def __init__(
@@ -100,7 +101,7 @@ def state(self) -> str:
return usage.electricity[-1].consumption
@property
- def device_state_attributes(self) -> object:
+ def extra_state_attributes(self) -> object:
"""Return the attributes of the sensor."""
usage: OVODailyUsage = self.coordinator.data
if usage is None or not usage.electricity:
@@ -135,7 +136,7 @@ def state(self) -> str:
return usage.gas[-1].consumption
@property
- def device_state_attributes(self) -> object:
+ def extra_state_attributes(self) -> object:
"""Return the attributes of the sensor."""
usage: OVODailyUsage = self.coordinator.data
if usage is None or not usage.gas:
@@ -171,7 +172,7 @@ def state(self) -> str:
return usage.electricity[-1].cost.amount
@property
- def device_state_attributes(self) -> object:
+ def extra_state_attributes(self) -> object:
"""Return the attributes of the sensor."""
usage: OVODailyUsage = self.coordinator.data
if usage is None or not usage.electricity:
@@ -207,7 +208,7 @@ def state(self) -> str:
return usage.gas[-1].cost.amount
@property
- def device_state_attributes(self) -> object:
+ def extra_state_attributes(self) -> object:
"""Return the attributes of the sensor."""
usage: OVODailyUsage = self.coordinator.data
if usage is None or not usage.gas:
diff --git a/homeassistant/components/ovo_energy/translations/de.json b/homeassistant/components/ovo_energy/translations/de.json
index 3bd083e48394bd..a86f39a614cf2b 100644
--- a/homeassistant/components/ovo_energy/translations/de.json
+++ b/homeassistant/components/ovo_energy/translations/de.json
@@ -1,19 +1,25 @@
{
"config": {
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
},
+ "flow_title": "OVO Energy: {username}",
"step": {
"reauth": {
"data": {
"password": "Passwort"
- }
+ },
+ "description": "Die Authentifizierung f\u00fcr OVO Energy ist fehlgeschlagen. Bitte geben Sie Ihre aktuellen Anmeldedaten ein.",
+ "title": "Erneute Authentifizierung"
},
"user": {
"data": {
"password": "Passwort",
"username": "Benutzername"
- }
+ },
+ "title": "Ovo Energy Account hinzuf\u00fcgen"
}
}
}
diff --git a/homeassistant/components/ovo_energy/translations/fr.json b/homeassistant/components/ovo_energy/translations/fr.json
index 86719e87df4fce..9be6b4d3c114cc 100644
--- a/homeassistant/components/ovo_energy/translations/fr.json
+++ b/homeassistant/components/ovo_energy/translations/fr.json
@@ -1,11 +1,19 @@
{
"config": {
"error": {
- "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9",
+ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9",
"cannot_connect": "\u00c9chec de connexion",
"invalid_auth": "Authentification invalide"
},
+ "flow_title": "OVO Energy: {username}",
"step": {
+ "reauth": {
+ "data": {
+ "password": "Mot de passe"
+ },
+ "description": "L'authentification a \u00e9chou\u00e9 pour OVO Energy. Veuillez saisir vos informations d'identification actuelles.",
+ "title": "R\u00e9authentification"
+ },
"user": {
"data": {
"password": "Mot de passe",
diff --git a/homeassistant/components/ovo_energy/translations/hu.json b/homeassistant/components/ovo_energy/translations/hu.json
index c4091388b35acb..7bfd337cce5d05 100644
--- a/homeassistant/components/ovo_energy/translations/hu.json
+++ b/homeassistant/components/ovo_energy/translations/hu.json
@@ -2,11 +2,23 @@
"config": {
"error": {
"already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
- "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
},
+ "flow_title": "OVO Energy: {username}",
"step": {
"reauth": {
+ "data": {
+ "password": "Jelsz\u00f3"
+ },
"title": "\u00dajrahiteles\u00edt\u00e9s"
+ },
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "title": "OVO Energy azonos\u00edt\u00f3 megad\u00e1sa"
}
}
}
diff --git a/homeassistant/components/ovo_energy/translations/id.json b/homeassistant/components/ovo_energy/translations/id.json
new file mode 100644
index 00000000000000..05c38f244e7db7
--- /dev/null
+++ b/homeassistant/components/ovo_energy/translations/id.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Akun sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "flow_title": "OVO Energy: {username}",
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Kata Sandi"
+ },
+ "description": "Autentikasi gagal untuk OVO Energy. Masukkan kredensial Anda saat ini.",
+ "title": "Autentikasi ulang"
+ },
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "description": "Siapkan instans OVO Energy untuk mengakses data penggunaan energi Anda.",
+ "title": "Tambahkan Akun OVO Energy"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ovo_energy/translations/ko.json b/homeassistant/components/ovo_energy/translations/ko.json
index 26372afc28eb85..4ca904725b4559 100644
--- a/homeassistant/components/ovo_energy/translations/ko.json
+++ b/homeassistant/components/ovo_energy/translations/ko.json
@@ -1,10 +1,25 @@
{
"config": {
"error": {
- "already_configured": "\uacc4\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
+ "flow_title": "OVO Energy: {username}",
"step": {
+ "reauth": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638"
+ },
+ "description": "OVO Energy\uc5d0 \ub300\ud55c \uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \ud604\uc7ac \uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "\uc7ac\uc778\uc99d"
+ },
"user": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ },
+ "description": "\uc5d0\ub108\uc9c0 \uc0ac\uc6a9\ub7c9\uc5d0 \uc811\uadfc\ud558\ub824\uba74 OVO Energy \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694.",
"title": "OVO Energy \uacc4\uc815 \ucd94\uac00\ud558\uae30"
}
}
diff --git a/homeassistant/components/ovo_energy/translations/lb.json b/homeassistant/components/ovo_energy/translations/lb.json
index 0e007924b6a3dc..b27b7d9702c357 100644
--- a/homeassistant/components/ovo_energy/translations/lb.json
+++ b/homeassistant/components/ovo_energy/translations/lb.json
@@ -6,6 +6,11 @@
"invalid_auth": "Ong\u00eblteg Authentifikatioun"
},
"step": {
+ "reauth": {
+ "data": {
+ "password": "Passwuert"
+ }
+ },
"user": {
"data": {
"password": "Passwuert",
diff --git a/homeassistant/components/ovo_energy/translations/nl.json b/homeassistant/components/ovo_energy/translations/nl.json
index daa12f9e569a39..d598e17d93b591 100644
--- a/homeassistant/components/ovo_energy/translations/nl.json
+++ b/homeassistant/components/ovo_energy/translations/nl.json
@@ -2,17 +2,25 @@
"config": {
"error": {
"already_configured": "Account is al geconfigureerd",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie"
},
+ "flow_title": "OVO Energy: {username}",
"step": {
"reauth": {
+ "data": {
+ "password": "Wachtwoord"
+ },
+ "description": "Authenticatie mislukt voor OVO Energy. Voer uw huidige inloggegevens in.",
"title": "Opnieuw verifi\u00ebren"
},
"user": {
"data": {
"password": "Wachtwoord",
"username": "Gebruikersnaam"
- }
+ },
+ "description": "Stel een OVO Energy instance in om toegang te krijgen tot je energieverbruik.",
+ "title": "Voeg OVO Energie Account toe"
}
}
}
diff --git a/homeassistant/components/ovo_energy/translations/ru.json b/homeassistant/components/ovo_energy/translations/ru.json
index dd422bac01f6cb..89eb632102f585 100644
--- a/homeassistant/components/ovo_energy/translations/ru.json
+++ b/homeassistant/components/ovo_energy/translations/ru.json
@@ -3,7 +3,7 @@
"error": {
"already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.",
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"flow_title": "OVO Energy: {username}",
"step": {
@@ -17,7 +17,7 @@
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 OVO Energy.",
"title": "OVO Energy"
diff --git a/homeassistant/components/ovo_energy/translations/tr.json b/homeassistant/components/ovo_energy/translations/tr.json
index f3784f6de87fcd..714daac3253ccd 100644
--- a/homeassistant/components/ovo_energy/translations/tr.json
+++ b/homeassistant/components/ovo_energy/translations/tr.json
@@ -1,5 +1,10 @@
{
"config": {
+ "error": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
"flow_title": "OVO Enerji: {username}",
"step": {
"reauth": {
@@ -8,6 +13,12 @@
},
"description": "OVO Energy i\u00e7in kimlik do\u011frulama ba\u015far\u0131s\u0131z oldu. L\u00fctfen mevcut kimlik bilgilerinizi girin.",
"title": "Yeniden kimlik do\u011frulama"
+ },
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
}
}
}
diff --git a/homeassistant/components/ovo_energy/translations/uk.json b/homeassistant/components/ovo_energy/translations/uk.json
new file mode 100644
index 00000000000000..8a5f8e2a8ba756
--- /dev/null
+++ b/homeassistant/components/ovo_energy/translations/uk.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "flow_title": "OVO Energy: {username}",
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "description": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457. \u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448\u0456 \u043f\u043e\u0442\u043e\u0447\u043d\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.",
+ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f"
+ },
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 OVO Energy.",
+ "title": "OVO Energy"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py
index f0838b510ec8b3..7074312594abca 100644
--- a/homeassistant/components/owntracks/config_flow.py
+++ b/homeassistant/components/owntracks/config_flow.py
@@ -4,7 +4,7 @@
from homeassistant import config_entries
from homeassistant.const import CONF_WEBHOOK_ID
-from .const import DOMAIN # noqa pylint: disable=unused-import
+from .const import DOMAIN
from .helper import supports_encryption
CONF_SECRET = "secret"
diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py
index 8a8fdc52fb1b79..d50e5b9c414064 100644
--- a/homeassistant/components/owntracks/device_tracker.py
+++ b/homeassistant/components/owntracks/device_tracker.py
@@ -74,7 +74,7 @@ def battery_level(self):
return self._data.get("battery")
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific attributes."""
return self._data.get("attributes")
diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py
index 3a4aac6bfd1bde..bd01284329b9ed 100644
--- a/homeassistant/components/owntracks/messages.py
+++ b/homeassistant/components/owntracks/messages.py
@@ -304,7 +304,7 @@ async def async_handle_waypoint(hass, name_base, waypoint):
if hass.states.get(entity_id) is not None:
return
- zone = zone_comp.Zone(
+ zone = zone_comp.Zone.from_yaml(
{
zone_comp.CONF_NAME: pretty_name,
zone_comp.CONF_LATITUDE: lat,
@@ -313,7 +313,6 @@ async def async_handle_waypoint(hass, name_base, waypoint):
zone_comp.CONF_ICON: zone_comp.ICON_IMPORT,
zone_comp.CONF_PASSIVE: False,
},
- False,
)
zone.hass = hass
zone.entity_id = entity_id
diff --git a/homeassistant/components/owntracks/translations/de.json b/homeassistant/components/owntracks/translations/de.json
index 9d832cc264a040..0bc533c04690c5 100644
--- a/homeassistant/components/owntracks/translations/de.json
+++ b/homeassistant/components/owntracks/translations/de.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
"create_entry": {
"default": "\n\n\u00d6ffnen unter Android [die OwnTracks-App]({android_url}) und gehe zu {android_url} - > Verbindung. \u00c4nder die folgenden Einstellungen: \n - Modus: Privates HTTP \n - Host: {webhook_url} \n - Identifizierung: \n - Benutzername: `''` \n - Ger\u00e4te-ID: `''` \n\n\u00d6ffnen unter iOS [die OwnTracks-App]({ios_url}) und tippe auf das Symbol (i) oben links - > Einstellungen. \u00c4nder die folgenden Einstellungen: \n - Modus: HTTP \n - URL: {webhook_url} \n - Aktivieren Sie die Authentifizierung \n - UserID: `''`\n\n {secret} \n \n Weitere Informationen findest du in der [Dokumentation]({docs_url})."
},
diff --git a/homeassistant/components/owntracks/translations/et.json b/homeassistant/components/owntracks/translations/et.json
index 16ef569d9a4617..2ee171365a4ce2 100644
--- a/homeassistant/components/owntracks/translations/et.json
+++ b/homeassistant/components/owntracks/translations/et.json
@@ -4,7 +4,7 @@
"single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
},
"create_entry": {
- "default": "\n\nAva Android seadmes [rakendus OwnTracks] ( {android_url} ), mine eelistustele - > \u00fchendus. Muuda j\u00e4rgmisi seadeid:\n - Re\u017eiim: privaatne HTTP\n - Host: {webhook_url}\n - Identifitseerimine:\n - kasutajanimi: \" \"\n - seadme ID: \" \"\n\n IOS-is ava rakendus [OwnTracks] ( {ios_url} ), puuduta vasakus \u00fclanurgas ikooni (i) - > seaded. Muuda j\u00e4rgmisi seadeid:\n - Re\u017eiim: HTTP\n - URL: {webhook_url}\n - L\u00fclitage autentimine sisse\n - UserID: \" \" \"\n\n {secret}\n\n Lisateavet leiad [dokumentatsioonist] ( {docs_url} )."
+ "default": "\n\nAva Android seadmes [rakendus OwnTracks] ( {android_url} ), mine eelistustele - > \u00fchendus. Muuda j\u00e4rgmisi seadeid:\n - Re\u017eiim: privaatne HTTP\n - Host: {webhook_url}\n - Identifitseerimine:\n - kasutajanimi: \" \"\n - seadme ID: \" \"\n\n IOS-is ava rakendus [OwnTracks] ( {ios_url} ), puuduta vasakus \u00fclanurgas ikooni (i) - > seaded. Muuda j\u00e4rgmisi seadeid:\n - Re\u017eiim: HTTP\n - URL: {webhook_url}\n - L\u00fclita autentimine sisse\n - UserID: \" \" \"\n\n {secret}\n\n Lisateavet leiad [dokumentatsioonist] ( {docs_url} )."
},
"step": {
"user": {
diff --git a/homeassistant/components/owntracks/translations/hu.json b/homeassistant/components/owntracks/translations/hu.json
index b6bb7593906f33..f103fc9bbe1cd6 100644
--- a/homeassistant/components/owntracks/translations/hu.json
+++ b/homeassistant/components/owntracks/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
"create_entry": {
"default": "\n\nAndroidon, nyisd meg [az OwnTracks appot]({android_url}), menj a preferences -> connectionre. V\u00e1ltoztasd meg a al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\niOS-en, nyisd meg [az OwnTracks appot]({ios_url}), kattints az (i) ikonra bal oldalon fel\u00fcl -> settings. V\u00e1ltoztasd meg az al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\nN\u00e9zd meg [a dokument\u00e1ci\u00f3t]({docs_url}) tov\u00e1bbi inform\u00e1ci\u00f3k\u00e9rt."
},
diff --git a/homeassistant/components/owntracks/translations/id.json b/homeassistant/components/owntracks/translations/id.json
new file mode 100644
index 00000000000000..890afaa099cb46
--- /dev/null
+++ b/homeassistant/components/owntracks/translations/id.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "create_entry": {
+ "default": "\n\nDi Android, buka [aplikasi OwnTracks]({android_url}), buka preferensi -> koneksi. Ubah setelan berikut ini:\n - Mode: HTTP Pribadi\n - Host: {webhook_url}\n - Identifikasi:\n - Nama pengguna: `''`\n - ID Perangkat: `''`\n\nDi iOS, buka [aplikasi OwnTracks]({ios_url}), ketuk ikon (i) di pojok kiri atas -> pengaturan. Ubah setelan berikut ini:\n - Mode: HTTP\n - URL: {webhook_url}\n - Aktifkan autentikasi\n - UserID: `''`\n\n{secret}\n\nLihat [dokumentasi]({docs_url}) untuk informasi lebih lanjut."
+ },
+ "step": {
+ "user": {
+ "description": "Yakin ingin menyiapkan OwnTracks?",
+ "title": "Siapkan OwnTracks"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/owntracks/translations/ko.json b/homeassistant/components/owntracks/translations/ko.json
index 3cde37528c24fe..a8dd94b7e165c0 100644
--- a/homeassistant/components/owntracks/translations/ko.json
+++ b/homeassistant/components/owntracks/translations/ko.json
@@ -1,11 +1,14 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
"create_entry": {
- "default": "\n\nAndroid \uc778 \uacbd\uc6b0, [OwnTracks \uc571]({android_url}) \uc744 \uc5f4\uace0 preferences -> connection \uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \ub2e4\uc74c\uacfc \uac19\uc774 \uc124\uc815\ud574\uc8fc\uc138\uc694:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\niOS \uc778 \uacbd\uc6b0, [OwnTracks \uc571]({ios_url}) \uc744 \uc5f4\uace0 \uc67c\ucabd \uc0c1\ub2e8\uc758 (i) \uc544\uc774\ucf58\uc744 \ud0ed\ud558\uc5ec \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \ub2e4\uc74c\uacfc \uac19\uc774 \uc124\uc815\ud574\uc8fc\uc138\uc694:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `''`\n\n{secret} \n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
+ "default": "\n\nAndroid\uc778 \uacbd\uc6b0, [OwnTracks \uc571]({android_url})\uc744 \uc5f4\uace0 preferences -> connection\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \ub2e4\uc74c\uacfc \uac19\uc774 \uc124\uc815\ud574\uc8fc\uc138\uc694:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\niOS \uc778 \uacbd\uc6b0, [OwnTracks \uc571]({ios_url})\uc744 \uc5f4\uace0 \uc67c\ucabd \uc0c1\ub2e8\uc758 (i) \uc544\uc774\ucf58\uc744 \ud0ed\ud558\uc5ec \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \ub2e4\uc74c\uacfc \uac19\uc774 \uc124\uc815\ud574\uc8fc\uc138\uc694:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `''`\n\n{secret} \n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
},
"step": {
"user": {
- "description": "OwnTracks \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "description": "OwnTracks\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "OwnTracks \uc124\uc815\ud558\uae30"
}
}
diff --git a/homeassistant/components/owntracks/translations/tr.json b/homeassistant/components/owntracks/translations/tr.json
new file mode 100644
index 00000000000000..a152eb194683cb
--- /dev/null
+++ b/homeassistant/components/owntracks/translations/tr.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/owntracks/translations/uk.json b/homeassistant/components/owntracks/translations/uk.json
index f1f3186424297b..e6a6fc26068e90 100644
--- a/homeassistant/components/owntracks/translations/uk.json
+++ b/homeassistant/components/owntracks/translations/uk.json
@@ -1,5 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "create_entry": {
+ "default": "\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u0456\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0456 Android, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({android_url}), \u043f\u043e\u0442\u0456\u043c preferences - > connection. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: Private HTTP\n- Host: {webhook_url}\n- Identification:\n- Username: ``\n- Device ID: `` \n\n\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 iOS, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({ios_url}), \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0456\u0432\u043e\u043c\u0443 \u0432\u0435\u0440\u0445\u043d\u044c\u043e\u043c\u0443 \u043a\u0443\u0442\u043a\u0443 - > settings. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: HTTP\n- URL: {webhook_url}\n- Turn on authentication\n- UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457."
+ },
"step": {
"user": {
"description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 OwnTracks?",
diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py
index a75c05416dcefb..ace71e4af81c28 100644
--- a/homeassistant/components/ozw/__init__.py
+++ b/homeassistant/components/ozw/__init__.py
@@ -1,5 +1,6 @@
"""The ozw integration."""
import asyncio
+from contextlib import suppress
import json
import logging
@@ -267,8 +268,8 @@ def async_receive_message(msg):
async def start_platforms():
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_setup(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ for platform in PLATFORMS
]
)
if entry.data.get(CONF_USE_ADDON):
@@ -280,10 +281,8 @@ async def async_stop_mqtt_client(event=None):
Do not unsubscribe the manager topic.
"""
mqtt_client_task.cancel()
- try:
+ with suppress(asyncio.CancelledError):
await mqtt_client_task
- except asyncio.CancelledError:
- pass
ozw_data[DATA_UNSUBSCRIBE].append(
hass.bus.async_listen_once(
@@ -310,8 +309,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/ozw/climate.py b/homeassistant/components/ozw/climate.py
index a74fd869f0f443..e403c4f5517b71 100644
--- a/homeassistant/components/ozw/climate.py
+++ b/homeassistant/components/ozw/climate.py
@@ -1,7 +1,8 @@
"""Support for Z-Wave climate devices."""
+from __future__ import annotations
+
from enum import IntEnum
import logging
-from typing import Optional, Tuple
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity
from homeassistant.components.climate.const import (
@@ -239,12 +240,12 @@ def target_temperature(self):
return self._current_mode_setpoint_values[0].value
@property
- def target_temperature_low(self) -> Optional[float]:
+ def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach."""
return self._current_mode_setpoint_values[0].value
@property
- def target_temperature_high(self) -> Optional[float]:
+ def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach."""
return self._current_mode_setpoint_values[1].value
@@ -308,9 +309,9 @@ async def async_set_preset_mode(self, preset_mode):
self.values.mode.send_value(preset_mode_value)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the optional state attributes."""
- data = super().device_state_attributes
+ data = super().extra_state_attributes
if self.values.fan_action:
data[ATTR_FAN_ACTION] = self.values.fan_action.value
if self.values.valve_position:
@@ -333,7 +334,7 @@ def supported_features(self):
support |= SUPPORT_PRESET_MODE
return support
- def _get_current_mode_setpoint_values(self) -> Tuple:
+ def _get_current_mode_setpoint_values(self) -> tuple:
"""Return a tuple of current setpoint Z-Wave value(s)."""
if not self.values.mode:
setpoint_names = ("setpoint_heating",)
diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py
index 00917c0609c932..2546a2e0aff662 100644
--- a/homeassistant/components/ozw/config_flow.py
+++ b/homeassistant/components/ozw/config_flow.py
@@ -7,8 +7,7 @@
from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow
-from .const import CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON, DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/ozw/entity.py b/homeassistant/components/ozw/entity.py
index 9c494a514e000a..305601a2333e0b 100644
--- a/homeassistant/components/ozw/entity.py
+++ b/homeassistant/components/ozw/entity.py
@@ -209,7 +209,7 @@ def device_info(self):
return device_info
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
return {const.ATTR_NODE_ID: self.values.primary.node.node_id}
@@ -268,7 +268,7 @@ async def _delete_callback(self, values_id):
if not self.values:
return # race condition: delete already requested
if values_id == self.values.values_id:
- await self.async_remove()
+ await self.async_remove(force_remove=True)
def create_device_name(node: OZWNode):
diff --git a/homeassistant/components/ozw/fan.py b/homeassistant/components/ozw/fan.py
index b4054207d0f170..505959dd3431c5 100644
--- a/homeassistant/components/ozw/fan.py
+++ b/homeassistant/components/ozw/fan.py
@@ -1,5 +1,4 @@
"""Support for Z-Wave fans."""
-import logging
import math
from homeassistant.components.fan import (
@@ -10,6 +9,7 @@
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import (
+ int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@@ -17,8 +17,6 @@
from .const import DATA_UNSUBSCRIBE, DOMAIN
from .entity import ZWaveDeviceEntity
-_LOGGER = logging.getLogger(__name__)
-
SUPPORTED_FEATURES = SUPPORT_SET_SPEED
SPEED_RANGE = (1, 99) # off is not included
@@ -75,6 +73,11 @@ def percentage(self):
"""
return ranged_value_to_percentage(SPEED_RANGE, self.values.primary.value)
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return int_states_in_range(SPEED_RANGE)
+
@property
def supported_features(self):
"""Flag supported features."""
diff --git a/homeassistant/components/ozw/manifest.json b/homeassistant/components/ozw/manifest.json
index 984e3f9c51afc7..a1409fd79a81ec 100644
--- a/homeassistant/components/ozw/manifest.json
+++ b/homeassistant/components/ozw/manifest.json
@@ -7,8 +7,7 @@
"python-openzwave-mqtt[mqtt-client]==1.4.0"
],
"after_dependencies": [
- "mqtt",
- "zwave"
+ "mqtt"
],
"codeowners": [
"@cgarwood",
diff --git a/homeassistant/components/ozw/sensor.py b/homeassistant/components/ozw/sensor.py
index 5bd0d1c482f16e..3c3d4c3ca36b1a 100644
--- a/homeassistant/components/ozw/sensor.py
+++ b/homeassistant/components/ozw/sensor.py
@@ -12,6 +12,7 @@
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DOMAIN as SENSOR_DOMAIN,
+ SensorEntity,
)
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import callback
@@ -57,7 +58,7 @@ def async_add_sensor(value):
)
-class ZwaveSensorBase(ZWaveDeviceEntity):
+class ZwaveSensorBase(ZWaveDeviceEntity, SensorEntity):
"""Basic Representation of a Z-Wave sensor."""
@property
@@ -149,9 +150,9 @@ def state(self):
return self.values.primary.value["Selected_id"]
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
- attributes = super().device_state_attributes
+ attributes = super().extra_state_attributes
# add the value's label as property
attributes["label"] = self.values.primary.value["Selected"]
return attributes
diff --git a/homeassistant/components/ozw/translations/ca.json b/homeassistant/components/ozw/translations/ca.json
index 4553589e36d7e5..835c16eb449562 100644
--- a/homeassistant/components/ozw/translations/ca.json
+++ b/homeassistant/components/ozw/translations/ca.json
@@ -26,7 +26,7 @@
"data": {
"use_addon": "Utilitza el complement OpenZWave Supervisor"
},
- "description": "Voleu utilitzar el complement OpenZWave Supervisor?",
+ "description": "Vols utilitzar el complement Supervisor d'OpenZWave?",
"title": "Selecciona el m\u00e8tode de connexi\u00f3"
},
"start_addon": {
diff --git a/homeassistant/components/ozw/translations/de.json b/homeassistant/components/ozw/translations/de.json
index 70eaaaf18df410..c58c55c49adb0b 100644
--- a/homeassistant/components/ozw/translations/de.json
+++ b/homeassistant/components/ozw/translations/de.json
@@ -1,9 +1,16 @@
{
"config": {
"abort": {
+ "addon_info_failed": "Fehler beim Abrufen von OpenZWave Add-on Informationen.",
+ "addon_install_failed": "Installation des OpenZWave Add-ons fehlgeschlagen.",
+ "addon_set_config_failed": "Setzen der OpenZWave Konfiguration fehlgeschlagen.",
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
"already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
- "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet"
+ "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet",
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
+ "error": {
+ "addon_start_failed": "Fehler beim Starten des OpenZWave Add-ons. \u00dcberpr\u00fcfe die Konfiguration."
},
"progress": {
"install_addon": "Bitte warten, bis die Installation des OpenZWave-Add-Ons abgeschlossen ist. Dies kann einige Minuten dauern."
@@ -14,6 +21,20 @@
},
"install_addon": {
"title": "Die Installation des OpenZWave-Add-On wurde gestartet"
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Verwende das OpenZWave Supervisor Add-on"
+ },
+ "description": "M\u00f6chtest du das OpenZWave Supervisor Add-on verwenden?",
+ "title": "Verbindungstyp ausw\u00e4hlen"
+ },
+ "start_addon": {
+ "data": {
+ "network_key": "Netzwerk-Schl\u00fcssel",
+ "usb_path": "USB-Ger\u00e4te-Pfad"
+ },
+ "title": "Gib die Konfiguration des OpenZWave Add-ons ein"
}
}
}
diff --git a/homeassistant/components/ozw/translations/fr.json b/homeassistant/components/ozw/translations/fr.json
index c4ea835d86c4c1..5e408b7b807215 100644
--- a/homeassistant/components/ozw/translations/fr.json
+++ b/homeassistant/components/ozw/translations/fr.json
@@ -1,28 +1,32 @@
{
"config": {
"abort": {
- "addon_info_failed": "Impossible d\u2019obtenir des informations de l'add-on OpenZWave.",
- "addon_install_failed": "\u00c9chec de l\u2019installation de l'add-on OpenZWave.",
+ "addon_info_failed": "Impossible d'obtenir les informations sur le module compl\u00e9mentaire OpenZWave.",
+ "addon_install_failed": "\u00c9chec de l'installation du module compl\u00e9mentaire OpenZWave.",
"addon_set_config_failed": "\u00c9chec de la configuration OpenZWave.",
"already_configured": "Cet appareil est d\u00e9j\u00e0 configur\u00e9",
+ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours",
"mqtt_required": "L'int\u00e9gration MQTT n'est pas configur\u00e9e",
"single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
},
"error": {
- "addon_start_failed": "\u00c9chec du d\u00e9marrage de l'add-on OpenZWave. V\u00e9rifiez la configuration."
+ "addon_start_failed": "\u00c9chec du d\u00e9marrage du module compl\u00e9mentaire OpenZWave. V\u00e9rifiez la configuration."
},
"progress": {
"install_addon": "Veuillez patienter pendant que l'installation du module OpenZWave se termine. Cela peut prendre plusieurs minutes."
},
"step": {
"hassio_confirm": {
- "title": "Configurer l\u2019int\u00e9gration OpenZWave avec l\u2019add-on OpenZWave"
+ "title": "Configurer l'int\u00e9gration d'OpenZWave avec le module compl\u00e9mentaire OpenZWave"
+ },
+ "install_addon": {
+ "title": "L'installation du module compl\u00e9mentaire OpenZWave a commenc\u00e9"
},
"on_supervisor": {
"data": {
- "use_addon": "Utiliser l'add-on OpenZWave Supervisor"
+ "use_addon": "Utilisez le module compl\u00e9mentaire OpenZWave du Supervisor"
},
- "description": "Souhaitez-vous utiliser l'add-on OpenZWave Supervisor ?",
+ "description": "Voulez-vous utiliser le module compl\u00e9mentaire OpenZWave du Supervisor?",
"title": "S\u00e9lectionner la m\u00e9thode de connexion"
},
"start_addon": {
@@ -30,7 +34,7 @@
"network_key": "Cl\u00e9 r\u00e9seau",
"usb_path": "Chemin du p\u00e9riph\u00e9rique USB"
},
- "title": "Entrez dans la configuration de l'add-on OpenZWave"
+ "title": "Entrez dans la configuration du module compl\u00e9mentaire OpenZWave"
}
}
}
diff --git a/homeassistant/components/ozw/translations/hu.json b/homeassistant/components/ozw/translations/hu.json
index e4c864d9fd3491..6c2c6c22f55e0b 100644
--- a/homeassistant/components/ozw/translations/hu.json
+++ b/homeassistant/components/ozw/translations/hu.json
@@ -4,7 +4,9 @@
"addon_info_failed": "Nem siker\u00fclt bet\u00f6lteni az OpenZWave kieg\u00e9sz\u00edt\u0151 inform\u00e1ci\u00f3kat.",
"addon_install_failed": "Nem siker\u00fclt telep\u00edteni az OpenZWave b\u0151v\u00edtm\u00e9nyt.",
"addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani az OpenZWave konfigur\u00e1ci\u00f3t.",
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
},
"error": {
"addon_start_failed": "Nem siker\u00fclt elind\u00edtani az OpenZWave b\u0151v\u00edtm\u00e9nyt. Ellen\u0151rizze a konfigur\u00e1ci\u00f3t."
@@ -12,14 +14,15 @@
"step": {
"on_supervisor": {
"data": {
- "use_addon": "Haszn\u00e1lja az OpenZWave adminisztr\u00e1tori b\u0151v\u00edtm\u00e9nyt"
+ "use_addon": "Haszn\u00e1ld az OpenZWave Supervisor b\u0151v\u00edtm\u00e9nyt"
},
- "description": "Szeretn\u00e9 haszn\u00e1lni az OpenZWave adminisztr\u00e1tori b\u0151v\u00edtm\u00e9nyt?",
- "title": "V\u00e1lassza ki a csatlakoz\u00e1si m\u00f3dot"
+ "description": "Szeretn\u00e9d haszn\u00e1lni az OpenZWave Supervisor b\u0151v\u00edtm\u00e9nyt?",
+ "title": "V\u00e1laszd ki a csatlakoz\u00e1si m\u00f3dot"
},
"start_addon": {
"data": {
- "network_key": "H\u00e1l\u00f3zati kulcs"
+ "network_key": "H\u00e1l\u00f3zati kulcs",
+ "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat"
}
}
}
diff --git a/homeassistant/components/ozw/translations/id.json b/homeassistant/components/ozw/translations/id.json
new file mode 100644
index 00000000000000..ef47e12f12d37c
--- /dev/null
+++ b/homeassistant/components/ozw/translations/id.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "addon_info_failed": "Gagal mendapatkan info add-on OpenZWave.",
+ "addon_install_failed": "Gagal menginstal add-on OpenZWave.",
+ "addon_set_config_failed": "Gagal menyetel konfigurasi OpenZWave.",
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "mqtt_required": "Integrasi MQTT belum disiapkan",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "addon_start_failed": "Gagal memulai add-on OpenZWave. Periksa konfigurasi."
+ },
+ "progress": {
+ "install_addon": "Harap tunggu hingga penginstalan add-on OpenZWave selesai. Ini bisa memakan waktu beberapa saat."
+ },
+ "step": {
+ "hassio_confirm": {
+ "title": "Siapkan integrasi OpenZWave dengan add-on OpenZWave"
+ },
+ "install_addon": {
+ "title": "Instalasi add-on OpenZWave telah dimulai"
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Gunakan add-on Supervisor OpenZWave"
+ },
+ "description": "Ingin menggunakan add-on Supervisor OpenZWave?",
+ "title": "Pilih metode koneksi"
+ },
+ "start_addon": {
+ "data": {
+ "network_key": "Kunci Jaringan",
+ "usb_path": "Jalur Perangkat USB"
+ },
+ "title": "Masukkan konfigurasi add-on OpenZWave"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ozw/translations/ko.json b/homeassistant/components/ozw/translations/ko.json
index 98b965d5dd23a5..f6dddf5c96abdd 100644
--- a/homeassistant/components/ozw/translations/ko.json
+++ b/homeassistant/components/ozw/translations/ko.json
@@ -1,7 +1,41 @@
{
"config": {
"abort": {
- "mqtt_required": "MQTT \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4"
+ "addon_info_failed": "OpenZWave \uc560\ub4dc\uc628\uc758 \uc815\ubcf4\ub97c \uac00\uc838\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.",
+ "addon_install_failed": "OpenZWave \uc560\ub4dc\uc628\uc744 \uc124\uce58\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.",
+ "addon_set_config_failed": "OpenZWave \uad6c\uc131\uc744 \uc124\uc815\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.",
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
+ "mqtt_required": "MQTT \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "addon_start_failed": "OpenZWave \uc560\ub4dc\uc628\uc744 \uc2dc\uc791\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uad6c\uc131 \ub0b4\uc6a9\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694."
+ },
+ "progress": {
+ "install_addon": "Openzwave \uc560\ub4dc\uc628\uc758 \uc124\uce58\uac00 \uc644\ub8cc\ub418\ub294 \ub3d9\uc548 \uc7a0\uc2dc \uae30\ub2e4\ub824\uc8fc\uc138\uc694. \uba87 \ubd84 \uc815\ub3c4 \uac78\ub9b4 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "step": {
+ "hassio_confirm": {
+ "title": "OpenZWave \uc560\ub4dc\uc628\uc73c\ub85c OpenZWave \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc124\uc815\ud558\uae30"
+ },
+ "install_addon": {
+ "title": "Openzwave \uc560\ub4dc\uc628 \uc124\uce58\uac00 \uc2dc\uc791\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "OpenZWave Supervisor \uc560\ub4dc\uc628\uc744 \uc0ac\uc6a9\ud558\uae30"
+ },
+ "description": "OpenZWave Supervisor \uc560\ub4dc\uc628\uc744 \uc0ac\uc6a9\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "\uc5f0\uacb0 \ubc29\ubc95 \uc120\ud0dd\ud558\uae30"
+ },
+ "start_addon": {
+ "data": {
+ "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4",
+ "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c"
+ },
+ "title": "OpenZWave \uc560\ub4dc\uc628\uc758 \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ozw/translations/lb.json b/homeassistant/components/ozw/translations/lb.json
index f97f026d38b92d..33de9a44953b76 100644
--- a/homeassistant/components/ozw/translations/lb.json
+++ b/homeassistant/components/ozw/translations/lb.json
@@ -1,8 +1,17 @@
{
"config": {
"abort": {
+ "already_configured": "Apparat ass scho konfigur\u00e9iert",
+ "already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang",
"mqtt_required": "MQTT Integratioun ass net ageriicht",
"single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
+ },
+ "step": {
+ "start_addon": {
+ "data": {
+ "network_key": "Netzwierk Schl\u00ebssel"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ozw/translations/nl.json b/homeassistant/components/ozw/translations/nl.json
index 4497654e7f37be..7392f9b63eb07d 100644
--- a/homeassistant/components/ozw/translations/nl.json
+++ b/homeassistant/components/ozw/translations/nl.json
@@ -1,8 +1,41 @@
{
"config": {
"abort": {
- "mqtt_required": "De [%%] integratie is niet ingesteld",
+ "addon_info_failed": "Mislukt om OpenZWave add-on info te krijgen.",
+ "addon_install_failed": "De installatie van de OpenZWave add-on is mislukt.",
+ "addon_set_config_failed": "Mislukt om OpenZWave configuratie in te stellen.",
+ "already_configured": "Apparaat is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
+ "mqtt_required": "De MQTT-integratie is niet ingesteld",
"single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk."
+ },
+ "error": {
+ "addon_start_failed": "Het starten van de OpenZWave-add-on is mislukt. Controleer de configuratie."
+ },
+ "progress": {
+ "install_addon": "Wacht even terwijl de installatie van de OpenZWave add-on wordt voltooid. Dit kan enkele minuten duren."
+ },
+ "step": {
+ "hassio_confirm": {
+ "title": "OpenZWave integratie instellen met de OpenZWave add-on"
+ },
+ "install_addon": {
+ "title": "De OpenZWave add-on installatie is gestart"
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Gebruik de OpenZWave Supervisor add-on"
+ },
+ "description": "Wilt u de OpenZWave Supervisor add-on gebruiken?",
+ "title": "Selecteer een verbindingsmethode"
+ },
+ "start_addon": {
+ "data": {
+ "network_key": "Netwerksleutel",
+ "usb_path": "USB-apparaatpad"
+ },
+ "title": "Voer de OpenZWave add-on configuratie in"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ozw/translations/no.json b/homeassistant/components/ozw/translations/no.json
index 89563ff3533798..652e28fe3fcb4f 100644
--- a/homeassistant/components/ozw/translations/no.json
+++ b/homeassistant/components/ozw/translations/no.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "addon_info_failed": "Kunne ikke hente OpenZWave-tilleggsinfo",
- "addon_install_failed": "Kunne ikke installere OpenZWave-tillegget",
+ "addon_info_failed": "Kunne ikke hente informasjon om OpenZWave-tillegg",
+ "addon_install_failed": "Kunne ikke installere OpenZWave-tillegg",
"addon_set_config_failed": "Kunne ikke angi OpenZWave-konfigurasjon",
"already_configured": "Enheten er allerede konfigurert",
"already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
@@ -10,23 +10,23 @@
"single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"error": {
- "addon_start_failed": "Kunne ikke starte OpenZWave-tillegget. Sjekk konfigurasjonen."
+ "addon_start_failed": "Kunne ikke starte OpenZWave-tillegg. Sjekk konfigurasjonen."
},
"progress": {
- "install_addon": "Vent mens OpenZWave-tilleggsinstallasjonen er ferdig. Dette kan ta flere minutter."
+ "install_addon": "Vent mens installasjonen av OpenZWave-tillegg er ferdig. Dette kan ta flere minutter."
},
"step": {
"hassio_confirm": {
- "title": "Sett opp OpenZWave-integrasjon med OpenZWave-tillegget"
+ "title": "Sett opp OpenZWave-integrasjon med OpenZWave-tillegg"
},
"install_addon": {
- "title": "Installasjonen av tilleggsprogrammet OpenZWave har startet"
+ "title": "Installasjonen av OpenZWave-tillegg har startet"
},
"on_supervisor": {
"data": {
- "use_addon": "Bruk OpenZWave Supervisor-tillegget"
+ "use_addon": "Bruk OpenZWave Supervisor-tillegg"
},
- "description": "\u00d8nsker du \u00e5 bruke OpenZWave Supervisor-tillegget?",
+ "description": "\u00d8nsker du \u00e5 bruke OpenZWave Supervisor-tillegg?",
"title": "Velg tilkoblingsmetode"
},
"start_addon": {
@@ -34,7 +34,7 @@
"network_key": "Nettverksn\u00f8kkel",
"usb_path": "USB enhetsbane"
},
- "title": "Angi OpenZWave-tilleggskonfigurasjonen"
+ "title": "Angi konfigurasjon for OpenZWave-tillegg"
}
}
}
diff --git a/homeassistant/components/ozw/translations/tr.json b/homeassistant/components/ozw/translations/tr.json
index d0a70d57752f47..99eda8b8311ddc 100644
--- a/homeassistant/components/ozw/translations/tr.json
+++ b/homeassistant/components/ozw/translations/tr.json
@@ -2,7 +2,12 @@
"config": {
"abort": {
"addon_info_failed": "OpenZWave eklenti bilgileri al\u0131namad\u0131.",
- "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ "addon_install_failed": "OpenZWave eklentisi y\u00fcklenemedi.",
+ "addon_set_config_failed": "OpenZWave yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.",
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor",
+ "mqtt_required": "MQTT entegrasyonu kurulmam\u0131\u015f",
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
},
"progress": {
"install_addon": "OpenZWave eklenti kurulumu bitene kadar l\u00fctfen bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir."
@@ -10,6 +15,18 @@
"step": {
"install_addon": {
"title": "OpenZWave eklenti kurulumu ba\u015flad\u0131"
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "OpenZWave Supervisor eklentisini kullan\u0131n"
+ },
+ "description": "OpenZWave Supervisor eklentisini kullanmak istiyor musunuz?",
+ "title": "Ba\u011flant\u0131 y\u00f6ntemini se\u00e7in"
+ },
+ "start_addon": {
+ "data": {
+ "network_key": "A\u011f Anahtar\u0131"
+ }
}
}
}
diff --git a/homeassistant/components/ozw/translations/uk.json b/homeassistant/components/ozw/translations/uk.json
new file mode 100644
index 00000000000000..f8fb161aa1c4f6
--- /dev/null
+++ b/homeassistant/components/ozw/translations/uk.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "addon_info_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f OpenZWave.",
+ "addon_install_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0438 OpenZWave.",
+ "addon_set_config_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e OpenZWave.",
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "mqtt_required": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f MQTT \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0430.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "addon_start_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 OpenZWave. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "progress": {
+ "install_addon": "\u0417\u0430\u0447\u0435\u043a\u0430\u0439\u0442\u0435, \u043f\u043e\u043a\u0438 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f OpenZWave. \u0426\u0435 \u043c\u043e\u0436\u0435 \u0437\u0430\u0439\u043d\u044f\u0442\u0438 \u043a\u0456\u043b\u044c\u043a\u0430 \u0445\u0432\u0438\u043b\u0438\u043d."
+ },
+ "step": {
+ "hassio_confirm": {
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f OpenZWave"
+ },
+ "install_addon": {
+ "title": "\u0420\u043e\u0437\u043f\u043e\u0447\u0430\u0442\u043e \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f Open'Wave"
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Supervisor OpenZWave"
+ },
+ "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Supervisor OpenZWave?",
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f"
+ },
+ "start_addon": {
+ "data": {
+ "network_key": "\u041a\u043b\u044e\u0447 \u043c\u0435\u0440\u0435\u0436\u0456",
+ "usb_path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ },
+ "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 Open'Wave"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ozw/translations/zh-Hant.json b/homeassistant/components/ozw/translations/zh-Hant.json
index f9ed5469da0579..37ab2ea9c9e5ff 100644
--- a/homeassistant/components/ozw/translations/zh-Hant.json
+++ b/homeassistant/components/ozw/translations/zh-Hant.json
@@ -1,32 +1,32 @@
{
"config": {
"abort": {
- "addon_info_failed": "\u53d6\u5f97 OpenZWave add-on \u8cc7\u8a0a\u5931\u6557\u3002",
- "addon_install_failed": "OpenZWave add-on \u5b89\u88dd\u5931\u6557\u3002",
- "addon_set_config_failed": "OpenZWave add-on \u8a2d\u5b9a\u5931\u6557\u3002",
+ "addon_info_failed": "\u53d6\u5f97 OpenZWave \u9644\u52a0\u5143\u4ef6\u8cc7\u8a0a\u5931\u6557\u3002",
+ "addon_install_failed": "OpenZWave \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5931\u6557\u3002",
+ "addon_set_config_failed": "OpenZWave a\u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u5931\u6557\u3002",
"already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a",
"single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002"
},
"error": {
- "addon_start_failed": "OpenZWave add-on \u555f\u52d5\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u3002"
+ "addon_start_failed": "OpenZWave \u9644\u52a0\u5143\u4ef6\u555f\u52d5\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u3002"
},
"progress": {
- "install_addon": "\u8acb\u7a0d\u7b49 OpenZWave add-on \u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002"
+ "install_addon": "\u8acb\u7a0d\u7b49 OpenZWave \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002"
},
"step": {
"hassio_confirm": {
- "title": "\u4ee5 OpenZWave add-on \u8a2d\u5b9a OpenZwave \u6574\u5408"
+ "title": "\u4ee5 OpenZWave \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a OpenZwave \u6574\u5408"
},
"install_addon": {
- "title": "OpenZWave add-on \u5b89\u88dd\u5df2\u555f\u52d5"
+ "title": "OpenZWave \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5df2\u555f\u52d5"
},
"on_supervisor": {
"data": {
- "use_addon": "\u4f7f\u7528 OpenZWave Supervisor add-on"
+ "use_addon": "\u4f7f\u7528 OpenZWave Supervisor \u9644\u52a0\u5143\u4ef6"
},
- "description": "\u662f\u5426\u8981\u4f7f\u7528 OpenZWave Supervisor add-on\uff1f",
+ "description": "\u662f\u5426\u8981\u4f7f\u7528 OpenZWave Supervisor \u9644\u52a0\u5143\u4ef6\uff1f",
"title": "\u9078\u64c7\u9023\u7dda\u985e\u578b"
},
"start_addon": {
@@ -34,7 +34,7 @@
"network_key": "\u7db2\u8def\u5bc6\u9470",
"usb_path": "USB \u88dd\u7f6e\u8def\u5f91"
},
- "title": "\u8acb\u8f38\u5165 OpenZWave \u8a2d\u5b9a\u3002"
+ "title": "\u8acb\u8f38\u5165 OpenZWave \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u3002"
}
}
}
diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py
index 3305c935890e0d..67cf07dc433389 100644
--- a/homeassistant/components/panasonic_viera/__init__.py
+++ b/homeassistant/components/panasonic_viera/__init__.py
@@ -8,6 +8,7 @@
import voluptuous as vol
from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN
+from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON
import homeassistant.helpers.config_validation as cv
@@ -46,7 +47,7 @@
extra=vol.ALLOW_EXTRA,
)
-PLATFORMS = [MEDIA_PLAYER_DOMAIN]
+PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN]
async def async_setup(hass, config):
@@ -93,7 +94,7 @@ async def async_setup_entry(hass, config_entry):
unique_id = config_entry.unique_id
if device_info is None:
_LOGGER.error(
- "Couldn't gather device info. Please restart Home Assistant with your TV turned on and connected to your network."
+ "Couldn't gather device info; Please restart Home Assistant with your TV turned on and connected to your network"
)
else:
unique_id = device_info[ATTR_UDN]
@@ -103,9 +104,9 @@ async def async_setup_entry(hass, config_entry):
data={**config, ATTR_DEVICE_INFO: device_info},
)
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return True
@@ -116,8 +117,8 @@ async def async_unload_entry(hass, config_entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -219,6 +220,7 @@ async def async_turn_off(self):
"""Turn off the TV."""
if self.state != STATE_OFF:
await self.async_send_key(Keys.power)
+ self.state = STATE_OFF
await self.async_update()
async def async_set_mute(self, enable):
diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py
index b39d3a1d3c8c6c..50a030b91dadfc 100644
--- a/homeassistant/components/panasonic_viera/config_flow.py
+++ b/homeassistant/components/panasonic_viera/config_flow.py
@@ -9,7 +9,7 @@
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT
-from .const import ( # pylint: disable=unused-import
+from .const import (
ATTR_DEVICE_INFO,
ATTR_FRIENDLY_NAME,
ATTR_UDN,
diff --git a/homeassistant/components/panasonic_viera/remote.py b/homeassistant/components/panasonic_viera/remote.py
new file mode 100644
index 00000000000000..8f3fab80215bfa
--- /dev/null
+++ b/homeassistant/components/panasonic_viera/remote.py
@@ -0,0 +1,90 @@
+"""Remote control support for Panasonic Viera TV."""
+import logging
+
+from homeassistant.components.remote import RemoteEntity
+from homeassistant.const import CONF_NAME, STATE_ON
+
+from .const import (
+ ATTR_DEVICE_INFO,
+ ATTR_MANUFACTURER,
+ ATTR_MODEL_NUMBER,
+ ATTR_REMOTE,
+ ATTR_UDN,
+ DEFAULT_MANUFACTURER,
+ DEFAULT_MODEL_NUMBER,
+ DOMAIN,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Panasonic Viera TV Remote from a config entry."""
+
+ config = config_entry.data
+
+ remote = hass.data[DOMAIN][config_entry.entry_id][ATTR_REMOTE]
+ name = config[CONF_NAME]
+ device_info = config[ATTR_DEVICE_INFO]
+
+ async_add_entities([PanasonicVieraRemoteEntity(remote, name, device_info)])
+
+
+class PanasonicVieraRemoteEntity(RemoteEntity):
+ """Representation of a Panasonic Viera TV Remote."""
+
+ def __init__(self, remote, name, device_info):
+ """Initialize the entity."""
+ # Save a reference to the imported class
+ self._remote = remote
+ self._name = name
+ self._device_info = device_info
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of the device."""
+ if self._device_info is None:
+ return None
+ return self._device_info[ATTR_UDN]
+
+ @property
+ def device_info(self):
+ """Return device specific attributes."""
+ if self._device_info is None:
+ return None
+ return {
+ "name": self._name,
+ "identifiers": {(DOMAIN, self._device_info[ATTR_UDN])},
+ "manufacturer": self._device_info.get(
+ ATTR_MANUFACTURER, DEFAULT_MANUFACTURER
+ ),
+ "model": self._device_info.get(ATTR_MODEL_NUMBER, DEFAULT_MODEL_NUMBER),
+ }
+
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return self._name
+
+ @property
+ def available(self):
+ """Return True if the device is available."""
+ return self._remote.available
+
+ @property
+ def is_on(self):
+ """Return true if device is on."""
+ return self._remote.state == STATE_ON
+
+ async def async_turn_on(self, **kwargs):
+ """Turn the device on."""
+ await self._remote.async_turn_on(context=self._context)
+
+ async def async_turn_off(self, **kwargs):
+ """Turn the device off."""
+ await self._remote.async_turn_off()
+
+ async def async_send_command(self, command, **kwargs):
+ """Send a command to one device."""
+ for cmd in command:
+ await self._remote.async_send_key(cmd)
diff --git a/homeassistant/components/panasonic_viera/translations/de.json b/homeassistant/components/panasonic_viera/translations/de.json
index 4b2c14be9d6185..71090830714681 100644
--- a/homeassistant/components/panasonic_viera/translations/de.json
+++ b/homeassistant/components/panasonic_viera/translations/de.json
@@ -1,20 +1,20 @@
{
"config": {
"abort": {
- "already_configured": "Dieser Panasonic Viera TV ist bereits konfiguriert.",
- "cannot_connect": "Verbindungsfehler",
- "unknown": "Ein unbekannter Fehler ist aufgetreten. Weitere Informationen finden Sie in den Logs."
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "unknown": "Unerwarteter Fehler"
},
"error": {
- "cannot_connect": "Verbindungsfehler",
- "invalid_pin_code": "Der von Ihnen eingegebene PIN-Code war ung\u00fcltig"
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_pin_code": "Der eingegebene PIN-Code war ung\u00fcltig"
},
"step": {
"pairing": {
"data": {
- "pin": "PIN"
+ "pin": "PIN-Code"
},
- "description": "Geben Sie die auf Ihrem Fernseher angezeigte PIN ein",
+ "description": "Gib den auf deinem TV angezeigten PIN-Code ein",
"title": "Kopplung"
},
"user": {
@@ -22,7 +22,7 @@
"host": "IP-Adresse",
"name": "Name"
},
- "description": "Geben Sie die IP-Adresse Ihres Panasonic Viera TV ein",
+ "description": "Gib die IP-Adresse deines Panasonic Viera TV ein",
"title": "Richten Sie Ihr Fernsehger\u00e4t ein"
}
}
diff --git a/homeassistant/components/panasonic_viera/translations/hu.json b/homeassistant/components/panasonic_viera/translations/hu.json
index fbf7f49be6a51d..cfc0be387d00bf 100644
--- a/homeassistant/components/panasonic_viera/translations/hu.json
+++ b/homeassistant/components/panasonic_viera/translations/hu.json
@@ -1,16 +1,28 @@
{
"config": {
"abort": {
- "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"error": {
- "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_pin_code": "A megadott PIN-k\u00f3d \u00e9rv\u00e9nytelen volt"
},
"step": {
+ "pairing": {
+ "data": {
+ "pin": "PIN-k\u00f3d"
+ },
+ "description": "Add meg a TV-k\u00e9sz\u00fcl\u00e9ken megjelen\u0151 PIN-k\u00f3dot",
+ "title": "P\u00e1ros\u00edt\u00e1s"
+ },
"user": {
"data": {
- "host": "IP c\u00edm"
- }
+ "host": "IP c\u00edm",
+ "name": "N\u00e9v"
+ },
+ "description": "Add meg a Panasonic Viera TV-hez tartoz\u00f3 IP c\u00edmet"
}
}
}
diff --git a/homeassistant/components/panasonic_viera/translations/id.json b/homeassistant/components/panasonic_viera/translations/id.json
new file mode 100644
index 00000000000000..4f9c6e3d432481
--- /dev/null
+++ b/homeassistant/components/panasonic_viera/translations/id.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_pin_code": "Kode PIN Anda masukkan tidak valid"
+ },
+ "step": {
+ "pairing": {
+ "data": {
+ "pin": "Kode PIN"
+ },
+ "description": "Masukkan Kode PIN yang ditampilkan di TV Anda",
+ "title": "Memasangkan"
+ },
+ "user": {
+ "data": {
+ "host": "Alamat IP",
+ "name": "Nama"
+ },
+ "description": "Masukkan Alamat IP TV Panasonic Viera Anda",
+ "title": "Siapkan TV Anda"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/panasonic_viera/translations/ko.json b/homeassistant/components/panasonic_viera/translations/ko.json
index fc2fd7827ab50c..4eb136c11494a7 100644
--- a/homeassistant/components/panasonic_viera/translations/ko.json
+++ b/homeassistant/components/panasonic_viera/translations/ko.json
@@ -1,18 +1,20 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 Panasonic Viera TV \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 \ub85c\uadf8\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694"
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_pin_code": "\uc785\ub825\ud55c PIN \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
"pairing": {
"data": {
- "pin": "PIN"
+ "pin": "PIN \ucf54\ub4dc"
},
- "description": "TV \uc5d0 \ud45c\uc2dc\ub41c PIN \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694",
+ "description": "TV\uc5d0 \ud45c\uc2dc\ub41c PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694",
"title": "\ud398\uc5b4\ub9c1\ud558\uae30"
},
"user": {
@@ -20,7 +22,7 @@
"host": "IP \uc8fc\uc18c",
"name": "\uc774\ub984"
},
- "description": "Panasonic Viera TV \uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694",
+ "description": "Panasonic Viera TV\uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694",
"title": "TV \uc124\uc815\ud558\uae30"
}
}
diff --git a/homeassistant/components/panasonic_viera/translations/nl.json b/homeassistant/components/panasonic_viera/translations/nl.json
index 96757370bed841..fa63892730e7ca 100644
--- a/homeassistant/components/panasonic_viera/translations/nl.json
+++ b/homeassistant/components/panasonic_viera/translations/nl.json
@@ -1,18 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "Deze Panasonic Viera TV is al geconfigureerd.",
+ "already_configured": "Apparaat is al geconfigureerd",
"cannot_connect": "Kan geen verbinding maken",
- "unknown": "Er is een onbekende fout opgetreden. Controleer de logs voor meer informatie."
+ "unknown": "Onverwachte fout"
},
"error": {
"cannot_connect": "Kan geen verbinding maken",
- "invalid_pin_code": "De ingevoerde pincode is ongeldig"
+ "invalid_pin_code": "De PIN-code die u hebt ingevoerd is ongeldig"
},
"step": {
"pairing": {
"data": {
- "pin": "PIN"
+ "pin": "PIN-code"
},
"description": "Voer de PIN-code in die op uw TV wordt weergegeven",
"title": "Koppelen"
@@ -22,7 +22,7 @@
"host": "IP-adres",
"name": "Naam"
},
- "description": "Voer het IP-adres van uw Panasonic Viera TV in",
+ "description": "Voer de IP-adres van uw Panasonic Viera TV in.",
"title": "Uw tv instellen"
}
}
diff --git a/homeassistant/components/panasonic_viera/translations/tr.json b/homeassistant/components/panasonic_viera/translations/tr.json
new file mode 100644
index 00000000000000..d0e573fdcf95e2
--- /dev/null
+++ b/homeassistant/components/panasonic_viera/translations/tr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0130p Adresi"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/panasonic_viera/translations/uk.json b/homeassistant/components/panasonic_viera/translations/uk.json
new file mode 100644
index 00000000000000..9722b19ece9cb8
--- /dev/null
+++ b/homeassistant/components/panasonic_viera/translations/uk.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_pin_code": "\u0412\u0432\u0435\u0434\u0435\u043d\u0438\u0439 PIN-\u043a\u043e\u0434 \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439."
+ },
+ "step": {
+ "pairing": {
+ "data": {
+ "pin": "PIN-\u043a\u043e\u0434"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434 , \u0449\u043e \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430",
+ "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f"
+ },
+ "user": {
+ "data": {
+ "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "name": "\u041d\u0430\u0437\u0432\u0430"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Panasonic Viera",
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/pencom/switch.py b/homeassistant/components/pencom/switch.py
index 7f193bc09a1da2..5621846e4967a4 100644
--- a/homeassistant/components/pencom/switch.py
+++ b/homeassistant/components/pencom/switch.py
@@ -93,6 +93,6 @@ def update(self):
self._state = self._hub.get(self._board, self._addr)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return supported attributes."""
return {"board": self._board, "addr": self._addr}
diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py
index 5f08f79dc00d75..05d52cf7830e3f 100644
--- a/homeassistant/components/persistent_notification/__init__.py
+++ b/homeassistant/components/persistent_notification/__init__.py
@@ -1,7 +1,9 @@
"""Support for displaying persistent notifications."""
+from __future__ import annotations
+
from collections import OrderedDict
import logging
-from typing import Any, Mapping, MutableMapping, Optional
+from typing import Any, Mapping, MutableMapping
import voluptuous as vol
@@ -11,6 +13,7 @@
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
+from homeassistant.helpers.template import Template
from homeassistant.loader import bind_hass
from homeassistant.util import slugify
import homeassistant.util.dt as dt_util
@@ -35,8 +38,8 @@
SCHEMA_SERVICE_CREATE = vol.Schema(
{
- vol.Required(ATTR_MESSAGE): cv.template,
- vol.Optional(ATTR_TITLE): cv.template,
+ vol.Required(ATTR_MESSAGE): vol.Any(cv.dynamic_template, cv.string),
+ vol.Optional(ATTR_TITLE): vol.Any(cv.dynamic_template, cv.string),
vol.Optional(ATTR_NOTIFICATION_ID): cv.string,
}
)
@@ -70,8 +73,8 @@ def dismiss(hass, notification_id):
def async_create(
hass: HomeAssistant,
message: str,
- title: Optional[str] = None,
- notification_id: Optional[str] = None,
+ title: str | None = None,
+ notification_id: str | None = None,
) -> None:
"""Generate a notification."""
data = {
@@ -118,22 +121,24 @@ def create_service(call):
attr = {}
if title is not None:
- try:
- title.hass = hass
- title = title.async_render(parse_result=False)
- except TemplateError as ex:
- _LOGGER.error("Error rendering title %s: %s", title, ex)
- title = title.template
+ if isinstance(title, Template):
+ try:
+ title.hass = hass
+ title = title.async_render(parse_result=False)
+ except TemplateError as ex:
+ _LOGGER.error("Error rendering title %s: %s", title, ex)
+ title = title.template
attr[ATTR_TITLE] = title
attr[ATTR_FRIENDLY_NAME] = title
- try:
- message.hass = hass
- message = message.async_render(parse_result=False)
- except TemplateError as ex:
- _LOGGER.error("Error rendering message %s: %s", message, ex)
- message = message.template
+ if isinstance(message, Template):
+ try:
+ message.hass = hass
+ message = message.async_render(parse_result=False)
+ except TemplateError as ex:
+ _LOGGER.error("Error rendering message %s: %s", message, ex)
+ message = message.template
attr[ATTR_MESSAGE] = message
diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py
index d0c0e9eccc81dd..1eb9d4eda7a401 100644
--- a/homeassistant/components/person/__init__.py
+++ b/homeassistant/components/person/__init__.py
@@ -1,6 +1,8 @@
"""Support for tracking people."""
+from __future__ import annotations
+
import logging
-from typing import List, Optional, cast
+from typing import cast
import voluptuous as vol
@@ -171,7 +173,7 @@ def __init__(
super().__init__(store, logger, id_manager)
self.yaml_collection = yaml_collection
- async def _async_load_data(self) -> Optional[dict]:
+ async def _async_load_data(self) -> dict | None:
"""Load the data.
A past bug caused onboarding to create invalid person objects.
@@ -257,7 +259,7 @@ async def _validate_user_id(self, user_id):
raise ValueError("User already taken")
-async def filter_yaml_data(hass: HomeAssistantType, persons: List[dict]) -> List[dict]:
+async def filter_yaml_data(hass: HomeAssistantType, persons: list[dict]) -> list[dict]:
"""Validate YAML data that we can't validate via schema."""
filtered = []
person_invalid_user = []
@@ -265,16 +267,15 @@ async def filter_yaml_data(hass: HomeAssistantType, persons: List[dict]) -> List
for person_conf in persons:
user_id = person_conf.get(CONF_USER_ID)
- if user_id is not None:
- if await hass.auth.async_get_user(user_id) is None:
- _LOGGER.error(
- "Invalid user_id detected for person %s",
- person_conf[collection.CONF_ID],
- )
- person_invalid_user.append(
- f"- Person {person_conf[CONF_NAME]} (id: {person_conf[collection.CONF_ID]}) points at invalid user {user_id}"
- )
- continue
+ if user_id is not None and await hass.auth.async_get_user(user_id) is None:
+ _LOGGER.error(
+ "Invalid user_id detected for person %s",
+ person_conf[collection.CONF_ID],
+ )
+ person_invalid_user.append(
+ f"- Person {person_conf[CONF_NAME]} (id: {person_conf[collection.CONF_ID]}) points at invalid user {user_id}"
+ )
+ continue
filtered.append(person_conf)
@@ -306,14 +307,12 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
yaml_collection,
)
- collection.attach_entity_component_collection(
- entity_component, yaml_collection, lambda conf: Person(conf, False)
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, entity_component, yaml_collection, Person
)
- collection.attach_entity_component_collection(
- entity_component, storage_collection, lambda conf: Person(conf, True)
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, entity_component, storage_collection, Person.from_yaml
)
- collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection)
- collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection)
await yaml_collection.async_load(
await filter_yaml_data(hass, config.get(DOMAIN, []))
@@ -358,10 +357,10 @@ async def async_reload_yaml(call: ServiceCall):
class Person(RestoreEntity):
"""Represent a tracked person."""
- def __init__(self, config, editable):
+ def __init__(self, config):
"""Set up person."""
self._config = config
- self._editable = editable
+ self.editable = True
self._latitude = None
self._longitude = None
self._gps_accuracy = None
@@ -369,13 +368,20 @@ def __init__(self, config, editable):
self._state = None
self._unsub_track_device = None
+ @classmethod
+ def from_yaml(cls, config):
+ """Return entity instance initialized from yaml storage."""
+ person = cls(config)
+ person.editable = False
+ return person
+
@property
def name(self):
"""Return the name of the entity."""
return self._config[CONF_NAME]
@property
- def entity_picture(self) -> Optional[str]:
+ def entity_picture(self) -> str | None:
"""Return entity picture."""
return self._config.get(CONF_PICTURE)
@@ -393,9 +399,9 @@ def state(self):
return self._state
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the person."""
- data = {ATTR_EDITABLE: self._editable, ATTR_ID: self.unique_id}
+ data = {ATTR_EDITABLE: self.editable, ATTR_ID: self.unique_id}
if self._latitude is not None:
data[ATTR_LATITUDE] = self._latitude
if self._longitude is not None:
@@ -517,7 +523,7 @@ def ws_list_person(
)
-def _get_latest(prev: Optional[State], curr: State):
+def _get_latest(prev: State | None, curr: State):
"""Get latest state."""
if prev is None or curr.last_updated > prev.last_updated:
return curr
diff --git a/homeassistant/components/person/significant_change.py b/homeassistant/components/person/significant_change.py
new file mode 100644
index 00000000000000..680b9194144aa6
--- /dev/null
+++ b/homeassistant/components/person/significant_change.py
@@ -0,0 +1,23 @@
+"""Helper to test significant Person state changes."""
+from __future__ import annotations
+
+from typing import Any
+
+from homeassistant.core import HomeAssistant, callback
+
+
+@callback
+def async_check_significant_change(
+ hass: HomeAssistant,
+ old_state: str,
+ old_attrs: dict,
+ new_state: str,
+ new_attrs: dict,
+ **kwargs: Any,
+) -> bool | None:
+ """Test if state significantly changed."""
+
+ if new_state != old_state:
+ return True
+
+ return False
diff --git a/homeassistant/components/person/translations/id.json b/homeassistant/components/person/translations/id.json
index 2be3be8476a683..1535bb2bd50c4c 100644
--- a/homeassistant/components/person/translations/id.json
+++ b/homeassistant/components/person/translations/id.json
@@ -1,7 +1,7 @@
{
"state": {
"_": {
- "home": "Di rumah",
+ "home": "Di Rumah",
"not_home": "Keluar"
}
},
diff --git a/homeassistant/components/person/translations/uk.json b/homeassistant/components/person/translations/uk.json
index 0dba7914da07c8..5e6b186e38ccf7 100644
--- a/homeassistant/components/person/translations/uk.json
+++ b/homeassistant/components/person/translations/uk.json
@@ -2,7 +2,7 @@
"state": {
"_": {
"home": "\u0412\u0434\u043e\u043c\u0430",
- "not_home": "\u0412\u0456\u0434\u0441\u0443\u0442\u043d\u0456\u0439"
+ "not_home": "\u041d\u0435 \u0432\u0434\u043e\u043c\u0430"
}
},
"title": "\u041b\u044e\u0434\u0438\u043d\u0430"
diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py
index 4b011c9f207b74..7be5efeaf2fd7b 100644
--- a/homeassistant/components/philips_js/__init__.py
+++ b/homeassistant/components/philips_js/__init__.py
@@ -1 +1,174 @@
-"""The philips_js component."""
+"""The Philips TV integration."""
+from __future__ import annotations
+
+import asyncio
+from datetime import timedelta
+import logging
+from typing import Any, Callable
+
+from haphilipsjs import ConnectionFailure, PhilipsTV
+
+from homeassistant.components.automation import AutomationActionType
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ CONF_API_VERSION,
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_USERNAME,
+)
+from homeassistant.core import CALLBACK_TYPE, Context, HassJob, HomeAssistant, callback
+from homeassistant.helpers.debounce import Debouncer
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+
+from .const import DOMAIN
+
+PLATFORMS = ["media_player", "remote"]
+
+LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+ """Set up the Philips TV component."""
+ hass.data[DOMAIN] = {}
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up Philips TV from a config entry."""
+
+ tvapi = PhilipsTV(
+ entry.data[CONF_HOST],
+ entry.data[CONF_API_VERSION],
+ username=entry.data.get(CONF_USERNAME),
+ password=entry.data.get(CONF_PASSWORD),
+ )
+
+ coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi)
+
+ await coordinator.async_refresh()
+ hass.data[DOMAIN][entry.entry_id] = coordinator
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
+
+
+class PluggableAction:
+ """A pluggable action handler."""
+
+ def __init__(self, update: Callable[[], None]):
+ """Initialize."""
+ self._update = update
+ self._actions: dict[Any, AutomationActionType] = {}
+
+ def __bool__(self):
+ """Return if we have something attached."""
+ return bool(self._actions)
+
+ @callback
+ def async_attach(self, action: AutomationActionType, variables: dict[str, Any]):
+ """Attach a device trigger for turn on."""
+
+ @callback
+ def _remove():
+ del self._actions[_remove]
+ self._update()
+
+ job = HassJob(action)
+
+ self._actions[_remove] = (job, variables)
+ self._update()
+
+ return _remove
+
+ async def async_run(self, hass: HomeAssistantType, context: Context | None = None):
+ """Run all turn on triggers."""
+ for job, variables in self._actions.values():
+ hass.async_run_hass_job(job, variables, context)
+
+
+class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]):
+ """Coordinator to update data."""
+
+ def __init__(self, hass, api: PhilipsTV) -> None:
+ """Set up the coordinator."""
+ self.api = api
+ self._notify_future: asyncio.Task | None = None
+
+ @callback
+ def _update_listeners():
+ for update_callback in self._listeners:
+ update_callback()
+
+ self.turn_on = PluggableAction(_update_listeners)
+
+ super().__init__(
+ hass,
+ LOGGER,
+ name=DOMAIN,
+ update_interval=timedelta(seconds=30),
+ request_refresh_debouncer=Debouncer(
+ hass, LOGGER, cooldown=2.0, immediate=False
+ ),
+ )
+
+ async def _notify_task(self):
+ while self.api.on and self.api.notify_change_supported:
+ if await self.api.notifyChange(130):
+ self.async_set_updated_data(None)
+
+ @callback
+ def _async_notify_stop(self):
+ if self._notify_future:
+ self._notify_future.cancel()
+ self._notify_future = None
+
+ @callback
+ def _async_notify_schedule(self):
+ if (
+ (self._notify_future is None or self._notify_future.done())
+ and self.api.on
+ and self.api.notify_change_supported
+ ):
+ self._notify_future = asyncio.create_task(self._notify_task())
+
+ @callback
+ def async_remove_listener(self, update_callback: CALLBACK_TYPE) -> None:
+ """Remove data update."""
+ super().async_remove_listener(update_callback)
+ if not self._listeners:
+ self._async_notify_stop()
+
+ @callback
+ def _async_stop_refresh(self, event: asyncio.Event) -> None:
+ super()._async_stop_refresh(event)
+ self._async_notify_stop()
+
+ @callback
+ async def _async_update_data(self):
+ """Fetch the latest data from the source."""
+ try:
+ await self.api.update()
+ self._async_notify_schedule()
+ except ConnectionFailure:
+ pass
diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py
new file mode 100644
index 00000000000000..8f0bcd161fcb81
--- /dev/null
+++ b/homeassistant/components/philips_js/config_flow.py
@@ -0,0 +1,159 @@
+"""Config flow for Philips TV integration."""
+from __future__ import annotations
+
+import platform
+from typing import Any
+
+from haphilipsjs import ConnectionFailure, PairingFailure, PhilipsTV
+import voluptuous as vol
+
+from homeassistant import config_entries, core
+from homeassistant.const import (
+ CONF_API_VERSION,
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PIN,
+ CONF_USERNAME,
+)
+
+from . import LOGGER
+from .const import CONF_SYSTEM, CONST_APP_ID, CONST_APP_NAME, DOMAIN
+
+
+async def validate_input(
+ hass: core.HomeAssistant, host: str, api_version: int
+) -> tuple[dict, PhilipsTV]:
+ """Validate the user input allows us to connect."""
+ hub = PhilipsTV(host, api_version)
+
+ await hub.getSystem()
+ await hub.setTransport(hub.secured_transport)
+
+ if not hub.system:
+ raise ConnectionFailure("System data is empty")
+
+ return hub
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Philips TV."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ def __init__(self) -> None:
+ """Initialize flow."""
+ super().__init__()
+ self._current = {}
+ self._hub: PhilipsTV | None = None
+ self._pair_state: Any = None
+
+ async def async_step_import(self, conf: dict) -> dict:
+ """Import a configuration from config.yaml."""
+ for entry in self._async_current_entries():
+ if entry.data[CONF_HOST] == conf[CONF_HOST]:
+ return self.async_abort(reason="already_configured")
+
+ return await self.async_step_user(
+ {
+ CONF_HOST: conf[CONF_HOST],
+ CONF_API_VERSION: conf[CONF_API_VERSION],
+ }
+ )
+
+ async def _async_create_current(self):
+
+ system = self._current[CONF_SYSTEM]
+ return self.async_create_entry(
+ title=f"{system['name']} ({system['serialnumber']})",
+ data=self._current,
+ )
+
+ async def async_step_pair(self, user_input: dict | None = None) -> dict:
+ """Attempt to pair with device."""
+ assert self._hub
+
+ errors = {}
+ schema = vol.Schema(
+ {
+ vol.Required(CONF_PIN): str,
+ }
+ )
+
+ if not user_input:
+ try:
+ self._pair_state = await self._hub.pairRequest(
+ CONST_APP_ID,
+ CONST_APP_NAME,
+ platform.node(),
+ platform.system(),
+ "native",
+ )
+ except PairingFailure as exc:
+ LOGGER.debug(exc)
+ return self.async_abort(
+ reason="pairing_failure",
+ description_placeholders={"error_id": exc.data.get("error_id")},
+ )
+ return self.async_show_form(
+ step_id="pair", data_schema=schema, errors=errors
+ )
+
+ try:
+ username, password = await self._hub.pairGrant(
+ self._pair_state, user_input[CONF_PIN]
+ )
+ except PairingFailure as exc:
+ LOGGER.debug(exc)
+ if exc.data.get("error_id") == "INVALID_PIN":
+ errors[CONF_PIN] = "invalid_pin"
+ return self.async_show_form(
+ step_id="pair", data_schema=schema, errors=errors
+ )
+
+ return self.async_abort(
+ reason="pairing_failure",
+ description_placeholders={"error_id": exc.data.get("error_id")},
+ )
+
+ self._current[CONF_USERNAME] = username
+ self._current[CONF_PASSWORD] = password
+ return await self._async_create_current()
+
+ async def async_step_user(self, user_input: dict | None = None) -> dict:
+ """Handle the initial step."""
+ errors = {}
+ if user_input:
+ self._current = user_input
+ try:
+ hub = await validate_input(
+ self.hass, user_input[CONF_HOST], user_input[CONF_API_VERSION]
+ )
+ except ConnectionFailure as exc:
+ LOGGER.error(exc)
+ errors["base"] = "cannot_connect"
+ except Exception: # pylint: disable=broad-except
+ LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+ else:
+
+ await self.async_set_unique_id(hub.system["serialnumber"])
+ self._abort_if_unique_id_configured()
+
+ self._current[CONF_SYSTEM] = hub.system
+ self._current[CONF_API_VERSION] = hub.api_version
+ self._hub = hub
+
+ if hub.pairing_type == "digest_auth_pairing":
+ return await self.async_step_pair()
+ return await self._async_create_current()
+
+ schema = vol.Schema(
+ {
+ vol.Required(CONF_HOST, default=self._current.get(CONF_HOST)): str,
+ vol.Required(
+ CONF_API_VERSION, default=self._current.get(CONF_API_VERSION, 1)
+ ): vol.In([1, 5, 6]),
+ }
+ )
+ return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
diff --git a/homeassistant/components/philips_js/const.py b/homeassistant/components/philips_js/const.py
new file mode 100644
index 00000000000000..5769a8979ced25
--- /dev/null
+++ b/homeassistant/components/philips_js/const.py
@@ -0,0 +1,7 @@
+"""The Philips TV constants."""
+
+DOMAIN = "philips_js"
+CONF_SYSTEM = "system"
+
+CONST_APP_ID = "homeassistant.io"
+CONST_APP_NAME = "Home Assistant"
diff --git a/homeassistant/components/philips_js/device_trigger.py b/homeassistant/components/philips_js/device_trigger.py
new file mode 100644
index 00000000000000..77782fc641cba0
--- /dev/null
+++ b/homeassistant/components/philips_js/device_trigger.py
@@ -0,0 +1,69 @@
+"""Provides device automations for control of device."""
+from __future__ import annotations
+
+import voluptuous as vol
+
+from homeassistant.components.automation import AutomationActionType
+from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
+from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
+from homeassistant.core import CALLBACK_TYPE, HomeAssistant
+from homeassistant.helpers.device_registry import DeviceRegistry, async_get_registry
+from homeassistant.helpers.typing import ConfigType
+
+from . import PhilipsTVDataUpdateCoordinator
+from .const import DOMAIN
+
+TRIGGER_TYPE_TURN_ON = "turn_on"
+
+TRIGGER_TYPES = {TRIGGER_TYPE_TURN_ON}
+TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
+ }
+)
+
+
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
+ """List device triggers for device."""
+ triggers = []
+ triggers.append(
+ {
+ CONF_PLATFORM: "device",
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_TYPE: TRIGGER_TYPE_TURN_ON,
+ }
+ )
+
+ return triggers
+
+
+async def async_attach_trigger(
+ hass: HomeAssistant,
+ config: ConfigType,
+ action: AutomationActionType,
+ automation_info: dict,
+) -> CALLBACK_TYPE | None:
+ """Attach a trigger."""
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
+ registry: DeviceRegistry = await async_get_registry(hass)
+ if config[CONF_TYPE] == TRIGGER_TYPE_TURN_ON:
+ variables = {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": config[CONF_DEVICE_ID],
+ "description": f"philips_js '{config[CONF_TYPE]}' event",
+ "id": trigger_id,
+ }
+ }
+
+ device = registry.async_get(config[CONF_DEVICE_ID])
+ for config_entry_id in device.config_entries:
+ coordinator: PhilipsTVDataUpdateCoordinator = hass.data[DOMAIN].get(
+ config_entry_id
+ )
+ if coordinator:
+ return coordinator.turn_on.async_attach(action, variables)
+
+ return None
diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json
index 74473827424f3f..ad591ad330bdc4 100644
--- a/homeassistant/components/philips_js/manifest.json
+++ b/homeassistant/components/philips_js/manifest.json
@@ -2,6 +2,11 @@
"domain": "philips_js",
"name": "Philips TV",
"documentation": "https://www.home-assistant.io/integrations/philips_js",
- "requirements": ["ha-philipsjs==0.0.8"],
- "codeowners": ["@elupus"]
-}
+ "requirements": [
+ "ha-philipsjs==2.3.2"
+ ],
+ "codeowners": [
+ "@elupus"
+ ],
+ "config_flow": true
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py
index 7ccec14406ae80..7376d34e308af0 100644
--- a/homeassistant/components/philips_js/media_player.py
+++ b/homeassistant/components/philips_js/media_player.py
@@ -1,25 +1,34 @@
"""Media Player component to integrate TVs exposing the Joint Space API."""
-from datetime import timedelta
-import logging
+from __future__ import annotations
-from haphilipsjs import PhilipsTV
+from typing import Any
+
+from haphilipsjs import ConnectionFailure
import voluptuous as vol
+from homeassistant import config_entries
from homeassistant.components.media_player import (
+ DEVICE_CLASS_TV,
PLATFORM_SCHEMA,
BrowseMedia,
MediaPlayerEntity,
)
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,
SUPPORT_BROWSE_MEDIA,
SUPPORT_NEXT_TRACK,
+ SUPPORT_PAUSE,
+ SUPPORT_PLAY,
SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK,
SUPPORT_SELECT_SOURCE,
+ SUPPORT_STOP,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE,
@@ -34,11 +43,13 @@
STATE_OFF,
STATE_ON,
)
+from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.event import call_later, track_time_interval
-from homeassistant.helpers.script import Script
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
-_LOGGER = logging.getLogger(__name__)
+from . import LOGGER as _LOGGER, PhilipsTVDataUpdateCoordinator
+from .const import CONF_SYSTEM, DOMAIN
SUPPORT_PHILIPS_JS = (
SUPPORT_TURN_OFF
@@ -50,28 +61,30 @@
| SUPPORT_PREVIOUS_TRACK
| SUPPORT_PLAY_MEDIA
| SUPPORT_BROWSE_MEDIA
+ | SUPPORT_PLAY
+ | SUPPORT_PAUSE
+ | SUPPORT_STOP
)
CONF_ON_ACTION = "turn_on_action"
-DEFAULT_NAME = "Philips TV"
-DEFAULT_API_VERSION = "1"
-DEFAULT_SCAN_INTERVAL = 30
-
-DELAY_ACTION_DEFAULT = 2.0
-DELAY_ACTION_ON = 10.0
-
-PREFIX_SEPARATOR = ": "
-PREFIX_SOURCE = "Input"
-PREFIX_CHANNEL = "Channel"
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): cv.string,
- vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
- }
+DEFAULT_API_VERSION = 1
+
+PLATFORM_SCHEMA = vol.All(
+ cv.deprecated(CONF_HOST),
+ cv.deprecated(CONF_NAME),
+ cv.deprecated(CONF_API_VERSION),
+ cv.deprecated(CONF_ON_ACTION),
+ PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_HOST): cv.string,
+ vol.Remove(CONF_NAME): cv.string,
+ vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): vol.Coerce(
+ int
+ ),
+ vol.Remove(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
+ }
+ ),
)
@@ -79,77 +92,85 @@ def _inverted(data):
return {v: k for k, v in data.items()}
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Philips TV platform."""
- name = config.get(CONF_NAME)
- host = config.get(CONF_HOST)
- api_version = config.get(CONF_API_VERSION)
- turn_on_action = config.get(CONF_ON_ACTION)
-
- tvapi = PhilipsTV(host, api_version)
- domain = __name__.split(".")[-2]
- on_script = Script(hass, turn_on_action, name, domain) if turn_on_action else None
-
- add_entities([PhilipsTVMediaPlayer(tvapi, name, on_script)])
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data=config,
+ )
+ )
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType,
+ config_entry: config_entries.ConfigEntry,
+ async_add_entities,
+):
+ """Set up the configuration entry."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]
+ async_add_entities(
+ [
+ PhilipsTVMediaPlayer(
+ coordinator,
+ config_entry.data[CONF_SYSTEM],
+ config_entry.unique_id,
+ )
+ ]
+ )
-class PhilipsTVMediaPlayer(MediaPlayerEntity):
+class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
"""Representation of a Philips TV exposing the JointSpace API."""
- def __init__(self, tv: PhilipsTV, name: str, on_script: Script):
+ def __init__(
+ self,
+ coordinator: PhilipsTVDataUpdateCoordinator,
+ system: dict[str, Any],
+ unique_id: str,
+ ):
"""Initialize the Philips TV."""
- self._tv = tv
- self._name = name
+ self._tv = coordinator.api
+ self._coordinator = coordinator
self._sources = {}
self._channels = {}
- self._on_script = on_script
self._supports = SUPPORT_PHILIPS_JS
- if self._on_script:
- self._supports |= SUPPORT_TURN_ON
- self._update_task = None
-
- def _update_soon(self, delay):
+ self._system = system
+ self._unique_id = unique_id
+ self._state = STATE_OFF
+ self._media_content_type: str | None = None
+ self._media_content_id: str | None = None
+ self._media_title: str | None = None
+ self._media_channel: str | None = None
+
+ super().__init__(coordinator)
+ self._update_from_coordinator()
+
+ async def _async_update_soon(self):
"""Reschedule update task."""
- if self._update_task:
- self._update_task()
- self._update_task = None
-
- self.schedule_update_ha_state(force_refresh=False)
-
- def update_forced(event_time):
- self.schedule_update_ha_state(force_refresh=True)
-
- def update_and_restart(event_time):
- update_forced(event_time)
- self._update_task = track_time_interval(
- self.hass, update_forced, timedelta(seconds=DEFAULT_SCAN_INTERVAL)
- )
-
- call_later(self.hass, delay, update_and_restart)
-
- async def async_added_to_hass(self):
- """Start running updates once we are added to hass."""
- await self.hass.async_add_executor_job(self._update_soon, 0)
+ self.async_write_ha_state()
+ await self.coordinator.async_request_refresh()
@property
def name(self):
"""Return the device name."""
- return self._name
-
- @property
- def should_poll(self):
- """Device should be polled."""
- return False
+ return self._system["name"]
@property
def supported_features(self):
"""Flag media player features that are supported."""
- return self._supports
+ supports = self._supports
+ if self._coordinator.turn_on or (
+ self._tv.on and self._tv.powerstate is not None
+ ):
+ supports |= SUPPORT_TURN_ON
+ return supports
@property
def state(self):
"""Get the device state. An exception means OFF state."""
- if self._tv.on:
+ if self._tv.on and (self._tv.powerstate == "On" or self._tv.powerstate is None):
return STATE_ON
return STATE_OFF
@@ -163,22 +184,12 @@ def source_list(self):
"""List of available input sources."""
return list(self._sources.values())
- def select_source(self, source):
+ async def async_select_source(self, source):
"""Set the input source."""
- data = source.split(PREFIX_SEPARATOR, 1)
- if data[0] == PREFIX_SOURCE: # Legacy way to set source
- source_id = _inverted(self._sources).get(data[1])
- if source_id:
- self._tv.setSource(source_id)
- elif data[0] == PREFIX_CHANNEL: # Legacy way to set channel
- channel_id = _inverted(self._channels).get(data[1])
- if channel_id:
- self._tv.setChannel(channel_id)
- else:
- source_id = _inverted(self._sources).get(source)
- if source_id:
- self._tv.setSource(source_id)
- self._update_soon(DELAY_ACTION_DEFAULT)
+ source_id = _inverted(self._sources).get(source)
+ if source_id:
+ await self._tv.setSource(source_id)
+ await self._async_update_soon()
@property
def volume_level(self):
@@ -190,134 +201,378 @@ def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._tv.muted
- def turn_on(self):
+ async def async_turn_on(self):
"""Turn on the device."""
- if self._on_script:
- self._on_script.run(context=self._context)
- self._update_soon(DELAY_ACTION_ON)
+ if self._tv.on and self._tv.powerstate:
+ await self._tv.setPowerState("On")
+ self._state = STATE_ON
+ else:
+ await self._coordinator.turn_on.async_run(self.hass, self._context)
+ await self._async_update_soon()
- def turn_off(self):
+ async def async_turn_off(self):
"""Turn off the device."""
- self._tv.sendKey("Standby")
- self._tv.on = False
- self._update_soon(DELAY_ACTION_DEFAULT)
+ await self._tv.sendKey("Standby")
+ self._state = STATE_OFF
+ await self._async_update_soon()
- def volume_up(self):
+ async def async_volume_up(self):
"""Send volume up command."""
- self._tv.sendKey("VolumeUp")
- self._update_soon(DELAY_ACTION_DEFAULT)
+ await self._tv.sendKey("VolumeUp")
+ await self._async_update_soon()
- def volume_down(self):
+ async def async_volume_down(self):
"""Send volume down command."""
- self._tv.sendKey("VolumeDown")
- self._update_soon(DELAY_ACTION_DEFAULT)
+ await self._tv.sendKey("VolumeDown")
+ await self._async_update_soon()
- def mute_volume(self, mute):
+ async def async_mute_volume(self, mute):
"""Send mute command."""
- self._tv.setVolume(None, mute)
- self._update_soon(DELAY_ACTION_DEFAULT)
+ if self._tv.muted != mute:
+ await self._tv.sendKey("Mute")
+ await self._async_update_soon()
+ else:
+ _LOGGER.debug("Ignoring request when already in expected state")
- def set_volume_level(self, volume):
+ async def async_set_volume_level(self, volume):
"""Set volume level, range 0..1."""
- self._tv.setVolume(volume, self._tv.muted)
- self._update_soon(DELAY_ACTION_DEFAULT)
+ await self._tv.setVolume(volume, self._tv.muted)
+ await self._async_update_soon()
- def media_previous_track(self):
+ async def async_media_previous_track(self):
"""Send rewind command."""
- self._tv.sendKey("Previous")
- self._update_soon(DELAY_ACTION_DEFAULT)
+ await self._tv.sendKey("Previous")
+ await self._async_update_soon()
- def media_next_track(self):
+ async def async_media_next_track(self):
"""Send fast forward command."""
- self._tv.sendKey("Next")
- self._update_soon(DELAY_ACTION_DEFAULT)
+ await self._tv.sendKey("Next")
+ await self._async_update_soon()
+
+ async def async_media_play_pause(self):
+ """Send pause command to media player."""
+ if self._tv.quirk_playpause_spacebar:
+ await self._tv.sendUnicode(" ")
+ else:
+ await self._tv.sendKey("PlayPause")
+ await self._async_update_soon()
+
+ async def async_media_play(self):
+ """Send pause command to media player."""
+ await self._tv.sendKey("Play")
+ await self._async_update_soon()
+
+ async def async_media_pause(self):
+ """Send play command to media player."""
+ await self._tv.sendKey("Pause")
+ await self._async_update_soon()
+
+ async def async_media_stop(self):
+ """Send play command to media player."""
+ await self._tv.sendKey("Stop")
+ await self._async_update_soon()
@property
def media_channel(self):
"""Get current channel if it's a channel."""
- if self.media_content_type == MEDIA_TYPE_CHANNEL:
- return self._channels.get(self._tv.channel_id)
- return None
+ return self._media_channel
@property
def media_title(self):
"""Title of current playing media."""
- if self.media_content_type == MEDIA_TYPE_CHANNEL:
- return self._channels.get(self._tv.channel_id)
- return self._sources.get(self._tv.source_id)
+ return self._media_title
@property
def media_content_type(self):
"""Return content type of playing media."""
- if self._tv.source_id == "tv" or self._tv.source_id == "11":
- return MEDIA_TYPE_CHANNEL
- if self._tv.source_id is None and self._tv.channels:
- return MEDIA_TYPE_CHANNEL
- return None
+ return self._media_content_type
@property
def media_content_id(self):
"""Content type of current playing media."""
- if self.media_content_type == MEDIA_TYPE_CHANNEL:
- return self._channels.get(self._tv.channel_id)
+ return self._media_content_id
+
+ @property
+ 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,
+ ):
+ return self.get_browse_image_url(
+ self._media_content_type, self._media_content_id, media_image_id=None
+ )
return None
@property
- def device_state_attributes(self):
- """Return the state attributes."""
- return {"channel_list": list(self._channels.values())}
+ def app_id(self):
+ """ID of the current running app."""
+ return self._tv.application_id
+
+ @property
+ def app_name(self):
+ """Name of the current running app."""
+ app = self._tv.applications.get(self._tv.application_id)
+ if app:
+ return app.get("label")
+
+ @property
+ def device_class(self):
+ """Return the device class."""
+ return DEVICE_CLASS_TV
+
+ @property
+ def unique_id(self):
+ """Return unique identifier if known."""
+ return self._unique_id
- def play_media(self, media_type, media_id, **kwargs):
+ @property
+ def device_info(self):
+ """Return a device description for device registry."""
+ return {
+ "name": self._system["name"],
+ "identifiers": {
+ (DOMAIN, self._unique_id),
+ },
+ "model": self._system.get("model"),
+ "manufacturer": "Philips",
+ "sw_version": self._system.get("softwareversion"),
+ }
+
+ async def async_play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
_LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id)
if media_type == MEDIA_TYPE_CHANNEL:
- channel_id = _inverted(self._channels).get(media_id)
+ list_id, _, channel_id = media_id.partition("/")
if channel_id:
- self._tv.setChannel(channel_id)
- self._update_soon(DELAY_ACTION_DEFAULT)
+ 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:
+ app = self._tv.applications.get(media_id)
+ if app:
+ await self._tv.setApplication(app["intent"])
+ await self._async_update_soon()
+ else:
+ _LOGGER.error("Unable to find application <%s>", media_id)
else:
_LOGGER.error("Unsupported media type <%s>", media_type)
- async def async_browse_media(self, media_content_type=None, media_content_id=None):
- """Implement the websocket media browsing helper."""
- if media_content_id not in (None, ""):
- raise BrowseError(
- f"Media not found: {media_content_type} / {media_content_id}"
- )
+ async def async_browse_media_channels(self, expanded):
+ """Return channel media objects."""
+ if expanded:
+ children = [
+ BrowseMedia(
+ title=channel.get("name", f"Channel: {channel_id}"),
+ media_class=MEDIA_CLASS_CHANNEL,
+ media_content_id=f"alltv/{channel_id}",
+ media_content_type=MEDIA_TYPE_CHANNEL,
+ can_play=True,
+ can_expand=False,
+ )
+ for channel_id, channel in self._tv.channels.items()
+ ]
+ else:
+ children = None
return BrowseMedia(
title="Channels",
media_class=MEDIA_CLASS_DIRECTORY,
- media_content_id="",
+ media_content_id="channels",
media_content_type=MEDIA_TYPE_CHANNELS,
+ children_media_class=MEDIA_CLASS_CHANNEL,
can_play=False,
can_expand=True,
- children=[
+ children=children,
+ )
+
+ async def async_browse_media_favorites(self, list_id, expanded):
+ """Return channel media objects."""
+ if expanded:
+ favorites = await self._tv.getFavoriteList(list_id)
+ if favorites:
+
+ def get_name(channel):
+ channel_data = self._tv.channels.get(str(channel["ccid"]))
+ if channel_data:
+ return channel_data["name"]
+ return f"Channel: {channel['ccid']}"
+
+ children = [
+ BrowseMedia(
+ title=get_name(channel),
+ media_class=MEDIA_CLASS_CHANNEL,
+ media_content_id=f"{list_id}/{channel['ccid']}",
+ media_content_type=MEDIA_TYPE_CHANNEL,
+ can_play=True,
+ can_expand=False,
+ )
+ for channel in favorites
+ ]
+ else:
+ children = None
+ else:
+ children = None
+
+ favorite = self._tv.favorite_lists[list_id]
+ return BrowseMedia(
+ title=favorite.get("name", f"Favorites {list_id}"),
+ media_class=MEDIA_CLASS_DIRECTORY,
+ media_content_id=f"favorites/{list_id}",
+ media_content_type=MEDIA_TYPE_CHANNELS,
+ children_media_class=MEDIA_CLASS_CHANNEL,
+ can_play=False,
+ can_expand=True,
+ children=children,
+ )
+
+ async def async_browse_media_applications(self, expanded):
+ """Return application media objects."""
+ if expanded:
+ children = [
BrowseMedia(
- title=channel,
- media_class=MEDIA_CLASS_CHANNEL,
- media_content_id=channel,
- media_content_type=MEDIA_TYPE_CHANNEL,
+ title=application["label"],
+ media_class=MEDIA_CLASS_APP,
+ media_content_id=application_id,
+ media_content_type=MEDIA_TYPE_APP,
can_play=True,
can_expand=False,
+ thumbnail=self.get_browse_image_url(
+ MEDIA_TYPE_APP, application_id, media_image_id=None
+ ),
)
- for channel in self._channels.values()
+ for application_id, application in self._tv.applications.items()
+ ]
+ else:
+ children = None
+
+ return BrowseMedia(
+ title="Applications",
+ media_class=MEDIA_CLASS_DIRECTORY,
+ media_content_id="applications",
+ media_content_type=MEDIA_TYPE_APPS,
+ children_media_class=MEDIA_CLASS_APP,
+ can_play=False,
+ can_expand=True,
+ children=children,
+ )
+
+ async def async_browse_media_favorite_lists(self, expanded):
+ """Return favorite media objects."""
+ if self._tv.favorite_lists and expanded:
+ children = [
+ await self.async_browse_media_favorites(list_id, False)
+ for list_id in self._tv.favorite_lists
+ ]
+ else:
+ children = None
+
+ return BrowseMedia(
+ title="Favorites",
+ media_class=MEDIA_CLASS_DIRECTORY,
+ media_content_id="favorite_lists",
+ media_content_type=MEDIA_TYPE_CHANNELS,
+ children_media_class=MEDIA_CLASS_CHANNEL,
+ can_play=False,
+ can_expand=True,
+ children=children,
+ )
+
+ async def async_browse_media_root(self):
+ """Return root media objects."""
+
+ return BrowseMedia(
+ title="Library",
+ media_class=MEDIA_CLASS_DIRECTORY,
+ media_content_id="",
+ media_content_type="",
+ can_play=False,
+ can_expand=True,
+ children=[
+ await self.async_browse_media_channels(False),
+ await self.async_browse_media_applications(False),
+ await self.async_browse_media_favorite_lists(False),
],
)
- def update(self):
- """Get the latest data and update device state."""
- self._tv.update()
+ async def async_browse_media(self, media_content_type=None, media_content_id=None):
+ """Implement the websocket media browsing helper."""
+ if not self._tv.on:
+ raise BrowseError("Can't browse when tv is turned off")
+
+ if media_content_id in (None, ""):
+ return await self.async_browse_media_root()
+ path = media_content_id.partition("/")
+ if path[0] == "channels":
+ return await self.async_browse_media_channels(True)
+ if path[0] == "applications":
+ return await self.async_browse_media_applications(True)
+ if path[0] == "favorite_lists":
+ return await self.async_browse_media_favorite_lists(True)
+ if path[0] == "favorites":
+ return await self.async_browse_media_favorites(path[2], True)
+
+ raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}")
+
+ async def async_get_browse_image(
+ self, media_content_type, media_content_id, media_image_id=None
+ ):
+ """Serve album art. Returns (content, content_type)."""
+ try:
+ if media_content_type == MEDIA_TYPE_APP and media_content_id:
+ return await self._tv.getApplicationIcon(media_content_id)
+ if media_content_type == MEDIA_TYPE_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):
+ """Serve album art. Returns (content, content_type)."""
+ return await self.async_get_browse_image(
+ self.media_content_type, self.media_content_id, None
+ )
+
+ @callback
+ def _update_from_coordinator(self):
+
+ if self._tv.on:
+ if self._tv.powerstate in ("Standby", "StandbyKeep"):
+ self._state = STATE_OFF
+ else:
+ self._state = STATE_ON
+ else:
+ self._state = STATE_OFF
self._sources = {
srcid: source.get("name") or f"Source {srcid}"
for srcid, source in (self._tv.sources or {}).items()
}
- self._channels = {
- chid: channel.get("name") or f"Channel {chid}"
- for chid, channel in (self._tv.channels or {}).items()
- }
+ if self._tv.channel_active:
+ self._media_content_type = MEDIA_TYPE_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_id = self._tv.application_id
+ self._media_title = self._tv.applications.get(
+ self._tv.application_id, {}
+ ).get("label")
+ self._media_channel = None
+ else:
+ self._media_content_type = None
+ self._media_content_id = None
+ self._media_title = self._sources.get(self._tv.source_id)
+ self._media_channel = None
+
+ @callback
+ def _handle_coordinator_update(self) -> None:
+ """Handle updated data from the coordinator."""
+ self._update_from_coordinator()
+ super()._handle_coordinator_update()
diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py
new file mode 100644
index 00000000000000..f4d34904f1b532
--- /dev/null
+++ b/homeassistant/components/philips_js/remote.py
@@ -0,0 +1,107 @@
+"""Remote control support for Apple TV."""
+
+import asyncio
+
+from haphilipsjs.typing import SystemType
+
+from homeassistant.components.remote import (
+ ATTR_DELAY_SECS,
+ ATTR_NUM_REPEATS,
+ DEFAULT_DELAY_SECS,
+ RemoteEntity,
+)
+
+from . import LOGGER, PhilipsTVDataUpdateCoordinator
+from .const import CONF_SYSTEM, DOMAIN
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the configuration entry."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]
+ async_add_entities(
+ [
+ PhilipsTVRemote(
+ coordinator,
+ config_entry.data[CONF_SYSTEM],
+ config_entry.unique_id,
+ )
+ ]
+ )
+
+
+class PhilipsTVRemote(RemoteEntity):
+ """Device that sends commands."""
+
+ def __init__(
+ self,
+ coordinator: PhilipsTVDataUpdateCoordinator,
+ system: SystemType,
+ unique_id: str,
+ ):
+ """Initialize the Philips TV."""
+ self._tv = coordinator.api
+ self._coordinator = coordinator
+ self._system = system
+ self._unique_id = unique_id
+
+ @property
+ def name(self):
+ """Return the device name."""
+ return self._system["name"]
+
+ @property
+ def is_on(self):
+ """Return true if device is on."""
+ return bool(
+ self._tv.on and (self._tv.powerstate == "On" or self._tv.powerstate is None)
+ )
+
+ @property
+ def should_poll(self):
+ """No polling needed for Apple TV."""
+ return False
+
+ @property
+ def unique_id(self):
+ """Return unique identifier if known."""
+ return self._unique_id
+
+ @property
+ def device_info(self):
+ """Return a device description for device registry."""
+ return {
+ "name": self._system["name"],
+ "identifiers": {
+ (DOMAIN, self._unique_id),
+ },
+ "model": self._system.get("model"),
+ "manufacturer": "Philips",
+ "sw_version": self._system.get("softwareversion"),
+ }
+
+ async def async_turn_on(self, **kwargs):
+ """Turn the device on."""
+ if self._tv.on and self._tv.powerstate:
+ await self._tv.setPowerState("On")
+ else:
+ await self._coordinator.turn_on.async_run(self.hass, self._context)
+ self.async_write_ha_state()
+
+ async def async_turn_off(self, **kwargs):
+ """Turn the device off."""
+ if self._tv.on:
+ await self._tv.sendKey("Standby")
+ self.async_write_ha_state()
+ else:
+ LOGGER.debug("Tv was already turned off")
+
+ async def async_send_command(self, command, **kwargs):
+ """Send a command to one device."""
+ num_repeats = kwargs[ATTR_NUM_REPEATS]
+ delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
+
+ for _ in range(num_repeats):
+ for single_command in command:
+ LOGGER.debug("Sending command %s", single_command)
+ await self._tv.sendKey(single_command)
+ await asyncio.sleep(delay)
diff --git a/homeassistant/components/philips_js/strings.json b/homeassistant/components/philips_js/strings.json
new file mode 100644
index 00000000000000..5c8f08eff6a261
--- /dev/null
+++ b/homeassistant/components/philips_js/strings.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "[%key:common::config_flow::data::host%]",
+ "api_version": "API Version"
+ }
+ },
+ "pair": {
+ "title": "Pair",
+ "description": "Enter the PIN displayed on your TV",
+ "data":{
+ "pin": "[%key:common::config_flow::data::pin%]"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]",
+ "pairing_failure": "Unable to pair: {error_id}",
+ "invalid_pin": "Invalid PIN"
+},
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "Device is requested to turn on"
+ }
+ }
+}
diff --git a/homeassistant/components/philips_js/translations/bg.json b/homeassistant/components/philips_js/translations/bg.json
new file mode 100644
index 00000000000000..eb502f5a135e22
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/bg.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "invalid_pin": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u041f\u0418\u041d",
+ "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/philips_js/translations/ca.json b/homeassistant/components/philips_js/translations/ca.json
new file mode 100644
index 00000000000000..b94faccd615c58
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/ca.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_pin": "PIN inv\u00e0lid",
+ "pairing_failure": "No s'ha pogut vincular: {error_id}",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "pair": {
+ "data": {
+ "pin": "Codi PIN"
+ },
+ "description": "Introdueix el PIN que es mostra al televisor",
+ "title": "Vinculaci\u00f3"
+ },
+ "user": {
+ "data": {
+ "api_version": "Versi\u00f3 de l'API",
+ "host": "Amfitri\u00f3"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "Es demani que el dispositiu s'engegui"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/cs.json b/homeassistant/components/philips_js/translations/cs.json
new file mode 100644
index 00000000000000..57ba2a219b6433
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/cs.json
@@ -0,0 +1,24 @@
+{
+ "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": {
+ "pair": {
+ "data": {
+ "pin": "PIN k\u00f3d"
+ }
+ },
+ "user": {
+ "data": {
+ "api_version": "Verze API",
+ "host": "Hostitel"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/de.json b/homeassistant/components/philips_js/translations/de.json
new file mode 100644
index 00000000000000..6288e9fb5c4bae
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/de.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_pin": "Ung\u00fcltige PIN",
+ "pairing_failure": "Fehler beim Koppeln: {error_id}",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "pair": {
+ "data": {
+ "pin": "PIN-Code"
+ },
+ "description": "Gib die auf deinem Fernseher angezeigten PIN ein"
+ },
+ "user": {
+ "data": {
+ "api_version": "API-Version",
+ "host": "Host"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "Ger\u00e4t wird zum Einschalten aufgefordert"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/el.json b/homeassistant/components/philips_js/translations/el.json
new file mode 100644
index 00000000000000..94f6b540bd0e4e
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/el.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "pair": {
+ "data": {
+ "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN"
+ },
+ "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf PIN \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2",
+ "title": "\u03a3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/en.json b/homeassistant/components/philips_js/translations/en.json
new file mode 100644
index 00000000000000..ea254a3873d79f
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/en.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_pin": "Invalid PIN",
+ "pairing_failure": "Unable to pair: {error_id}",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "pair": {
+ "data": {
+ "pin": "PIN Code"
+ },
+ "description": "Enter the PIN displayed on your TV",
+ "title": "Pair"
+ },
+ "user": {
+ "data": {
+ "api_version": "API Version",
+ "host": "Host"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "Device is requested to turn on"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/es.json b/homeassistant/components/philips_js/translations/es.json
new file mode 100644
index 00000000000000..c8d34e9ea9d6ec
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/es.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "error": {
+ "invalid_pin": "PIN no v\u00e1lido",
+ "pairing_failure": "No se ha podido emparejar: {error_id}"
+ },
+ "step": {
+ "pair": {
+ "description": "Introduzca el PIN que se muestra en el televisor",
+ "title": "Par"
+ },
+ "user": {
+ "data": {
+ "api_version": "Versi\u00f3n del API",
+ "host": "Host"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "Se solicita al dispositivo que se encienda"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/et.json b/homeassistant/components/philips_js/translations/et.json
new file mode 100644
index 00000000000000..4a5bf9fe6e9f6f
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/et.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_pin": "Vale PIN kood",
+ "pairing_failure": "Sidumine nurjus: {error_id}",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "pair": {
+ "data": {
+ "pin": "PIN kood"
+ },
+ "description": "Sisesta teleris kuvatav PIN-kood",
+ "title": "Paarita"
+ },
+ "user": {
+ "data": {
+ "api_version": "API versioon",
+ "host": "Host"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "Seadmel palutakse sisse l\u00fclituda"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/fr.json b/homeassistant/components/philips_js/translations/fr.json
new file mode 100644
index 00000000000000..eb16bb922714a6
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/fr.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "invalid_pin": "NIP invalide",
+ "pairing_failure": "Association impossible: {error_id}",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "pair": {
+ "data": {
+ "pin": "Code PIN"
+ },
+ "description": "Entrez le code PIN affich\u00e9 sur votre t\u00e9l\u00e9viseur",
+ "title": "Appairer"
+ },
+ "user": {
+ "data": {
+ "api_version": "Version de l'API",
+ "host": "H\u00f4te"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "Il a \u00e9t\u00e9 demand\u00e9 \u00e0 l'appareil de s'allumer"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/he.json b/homeassistant/components/philips_js/translations/he.json
new file mode 100644
index 00000000000000..04648fe58451ef
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/he.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "pairing_failure": "\u05e6\u05d9\u05de\u05d5\u05d3 \u05e0\u05db\u05e9\u05dc"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/hu.json b/homeassistant/components/philips_js/translations/hu.json
new file mode 100644
index 00000000000000..f7ce3f708b0ab2
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/hu.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_pin": "\u00c9rv\u00e9nytelen PIN",
+ "pairing_failure": "Nem lehet p\u00e1ros\u00edtani: {error_id}",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "pair": {
+ "data": {
+ "pin": "PIN-k\u00f3d"
+ },
+ "description": "\u00cdrd be a t\u00e9v\u00e9n megjelen\u0151 PIN-k\u00f3dot",
+ "title": "P\u00e1ros\u00edt\u00e1s"
+ },
+ "user": {
+ "data": {
+ "api_version": "API Verzi\u00f3",
+ "host": "Hoszt"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/id.json b/homeassistant/components/philips_js/translations/id.json
new file mode 100644
index 00000000000000..633cfdd633e697
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/id.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_pin": "PIN tidak valid",
+ "pairing_failure": "Tidak dapat memasangkan: {error_id}",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_version": "Versi API",
+ "host": "Host"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "Perangkat diminta untuk dinyalakan"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/it.json b/homeassistant/components/philips_js/translations/it.json
new file mode 100644
index 00000000000000..6ff668dbea8b75
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/it.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_pin": "PIN non valido",
+ "pairing_failure": "Impossibile eseguire l'associazione: {error_id}",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "pair": {
+ "data": {
+ "pin": "Codice PIN"
+ },
+ "description": "Inserire il PIN visualizzato sul televisore",
+ "title": "Associa"
+ },
+ "user": {
+ "data": {
+ "api_version": "Versione API",
+ "host": "Host"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "Si richiede l'accensione del dispositivo"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/ko.json b/homeassistant/components/philips_js/translations/ko.json
new file mode 100644
index 00000000000000..04ba5eff601d6e
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/ko.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_pin": "PIN\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "pairing_failure": "\ud398\uc5b4\ub9c1\uc744 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4: {error_id}",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "pair": {
+ "data": {
+ "pin": "PIN \ucf54\ub4dc"
+ },
+ "description": "TV\uc5d0 \ud45c\uc2dc\ub41c PIN \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694",
+ "title": "\ud398\uc5b4\ub9c1\ud558\uae30"
+ },
+ "user": {
+ "data": {
+ "api_version": "API \ubc84\uc804",
+ "host": "\ud638\uc2a4\ud2b8"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "\uae30\uae30\uac00 \ucf1c\uc9c0\ub3c4\ub85d \uc694\uccad\ub418\uc5c8\uc744 \ub54c"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/nl.json b/homeassistant/components/philips_js/translations/nl.json
new file mode 100644
index 00000000000000..34497d285fa6e9
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/nl.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_pin": "Ongeldige pincode",
+ "pairing_failure": "Kan niet koppelen: {error_id}",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "pair": {
+ "data": {
+ "pin": "PIN-code"
+ },
+ "description": "Voer de pincode in die op uw tv wordt weergegeven",
+ "title": "Koppel"
+ },
+ "user": {
+ "data": {
+ "api_version": "API Versie",
+ "host": "Host"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "Apparaat wordt gevraagd om in te schakelen"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/no.json b/homeassistant/components/philips_js/translations/no.json
new file mode 100644
index 00000000000000..5b1df7c8e8bb00
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/no.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_pin": "Ugyldig PIN",
+ "pairing_failure": "Kan ikke parre: {error_id}",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "pair": {
+ "data": {
+ "pin": "PIN kode"
+ },
+ "description": "Angi PIN-koden som vises p\u00e5 TV-en",
+ "title": "Par"
+ },
+ "user": {
+ "data": {
+ "api_version": "API-versjon",
+ "host": "Vert"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "Enheten blir bedt om \u00e5 sl\u00e5 p\u00e5"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/pl.json b/homeassistant/components/philips_js/translations/pl.json
new file mode 100644
index 00000000000000..a89b6136ff8299
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/pl.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_pin": "Nieprawid\u0142owy kod PIN",
+ "pairing_failure": "Nie mo\u017cna sparowa\u0107: {error_id}",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "pair": {
+ "data": {
+ "pin": "Kod PIN"
+ },
+ "description": "Wprowad\u017a kod PIN wy\u015bwietlony na Twoim telewizorze",
+ "title": "Paruj"
+ },
+ "user": {
+ "data": {
+ "api_version": "Wersja API",
+ "host": "Nazwa hosta lub adres IP"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "Urz\u0105dzenie zostanie poproszone o w\u0142\u0105czenie"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/pt.json b/homeassistant/components/philips_js/translations/pt.json
new file mode 100644
index 00000000000000..4646fcae7dc081
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/pt.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "error": {
+ "invalid_pin": "PIN inv\u00e1lido"
+ },
+ "step": {
+ "pair": {
+ "data": {
+ "pin": "C\u00f3digo PIN"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/ru.json b/homeassistant/components/philips_js/translations/ru.json
new file mode 100644
index 00000000000000..df3dfd4b6f61b9
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/ru.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434.",
+ "pairing_failure": "\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: {error_id}.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "pair": {
+ "data": {
+ "pin": "PIN-\u043a\u043e\u0434"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u044b\u0439 \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430",
+ "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435"
+ },
+ "user": {
+ "data": {
+ "api_version": "\u0412\u0435\u0440\u0441\u0438\u044f API",
+ "host": "\u0425\u043e\u0441\u0442"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/zh-Hans.json b/homeassistant/components/philips_js/translations/zh-Hans.json
new file mode 100644
index 00000000000000..1353d8d122507b
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/zh-Hans.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "invalid_pin": "\u65e0\u6548\u7684PIN\u7801"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/zh-Hant.json b/homeassistant/components/philips_js/translations/zh-Hant.json
new file mode 100644
index 00000000000000..7ae9c8893d557a
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/zh-Hant.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_pin": "PIN \u78bc\u7121\u6548",
+ "pairing_failure": "\u7121\u6cd5\u914d\u5c0d\uff1a{error_id}",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "pair": {
+ "data": {
+ "pin": "PIN \u78bc"
+ },
+ "description": "\u8f38\u5165\u96fb\u8996\u986f\u793a\u4e4b PIN \u78bc",
+ "title": "\u914d\u5c0d"
+ },
+ "user": {
+ "data": {
+ "api_version": "API \u7248\u672c",
+ "host": "\u4e3b\u6a5f\u7aef"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "turn_on": "\u88dd\u7f6e\u5fc5\u9808\u70ba\u958b\u555f\u72c0\u614b"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py b/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py
index 61cedb8f063271..bdec7714eef310 100644
--- a/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py
+++ b/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py
@@ -1,5 +1,5 @@
"""Support for binary sensor using RPi GPIO."""
-from pi4ioe5v9xxxx import pi4ioe5v9xxxx # pylint: disable=import-error
+from pi4ioe5v9xxxx import pi4ioe5v9xxxx
import voluptuous as vol
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
diff --git a/homeassistant/components/pi4ioe5v9xxxx/switch.py b/homeassistant/components/pi4ioe5v9xxxx/switch.py
index 81de76c086c226..85bde509070618 100644
--- a/homeassistant/components/pi4ioe5v9xxxx/switch.py
+++ b/homeassistant/components/pi4ioe5v9xxxx/switch.py
@@ -1,5 +1,5 @@
"""Allows to configure a switch using RPi GPIO."""
-from pi4ioe5v9xxxx import pi4ioe5v9xxxx # pylint: disable=import-error
+from pi4ioe5v9xxxx import pi4ioe5v9xxxx
import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py
index 2d540d936e5bf7..bc486a0c9014ec 100644
--- a/homeassistant/components/pi_hole/__init__.py
+++ b/homeassistant/components/pi_hole/__init__.py
@@ -112,7 +112,7 @@ async def async_update_data():
try:
await api.get_data()
except HoleError as err:
- raise UpdateFailed(f"Failed to communicating with API: {err}") from err
+ raise UpdateFailed(f"Failed to communicate with API: {err}") from err
coordinator = DataUpdateCoordinator(
hass,
diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py
index a7d4b387b1c87e..60d53c4f904b89 100644
--- a/homeassistant/components/pi_hole/config_flow.py
+++ b/homeassistant/components/pi_hole/config_flow.py
@@ -6,7 +6,7 @@
import voluptuous as vol
from homeassistant import config_entries
-from homeassistant.components.pi_hole.const import ( # pylint: disable=unused-import
+from homeassistant.components.pi_hole.const import (
CONF_LOCATION,
CONF_STATISTICS_ONLY,
DEFAULT_LOCATION,
diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py
index 2f5873b14c1c50..517e8cfcf17f4d 100644
--- a/homeassistant/components/pi_hole/sensor.py
+++ b/homeassistant/components/pi_hole/sensor.py
@@ -1,5 +1,6 @@
"""Support for getting statistical data from a Pi-hole system."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_NAME
from . import PiHoleEntity
@@ -30,7 +31,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
async_add_entities(sensors, True)
-class PiHoleSensor(PiHoleEntity):
+class PiHoleSensor(PiHoleEntity, SensorEntity):
"""Representation of a Pi-hole sensor."""
def __init__(self, api, coordinator, name, sensor_name, server_unique_id):
@@ -73,6 +74,6 @@ def state(self):
return self.api.data[self._condition]
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the Pi-hole."""
return {ATTR_BLOCKED_DOMAINS: self.api.data["domains_being_blocked"]}
diff --git a/homeassistant/components/pi_hole/translations/ca.json b/homeassistant/components/pi_hole/translations/ca.json
index 37d4e890ef4a5e..eb15fa7bf97b8d 100644
--- a/homeassistant/components/pi_hole/translations/ca.json
+++ b/homeassistant/components/pi_hole/translations/ca.json
@@ -7,6 +7,11 @@
"cannot_connect": "Ha fallat la connexi\u00f3"
},
"step": {
+ "api_key": {
+ "data": {
+ "api_key": "Clau API"
+ }
+ },
"user": {
"data": {
"api_key": "Clau API",
@@ -15,6 +20,7 @@
"name": "Nom",
"port": "Port",
"ssl": "Utilitza un certificat SSL",
+ "statistics_only": "Nom\u00e9s les estad\u00edstiques",
"verify_ssl": "Verifica el certificat SSL"
}
}
diff --git a/homeassistant/components/pi_hole/translations/cs.json b/homeassistant/components/pi_hole/translations/cs.json
index a9057ceabab9e4..fa90fbdb2a005f 100644
--- a/homeassistant/components/pi_hole/translations/cs.json
+++ b/homeassistant/components/pi_hole/translations/cs.json
@@ -7,6 +7,11 @@
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit"
},
"step": {
+ "api_key": {
+ "data": {
+ "api_key": "Kl\u00ed\u010d API"
+ }
+ },
"user": {
"data": {
"api_key": "Kl\u00ed\u010d API",
diff --git a/homeassistant/components/pi_hole/translations/de.json b/homeassistant/components/pi_hole/translations/de.json
index f74c5acb6352f0..6d9518490d55ea 100644
--- a/homeassistant/components/pi_hole/translations/de.json
+++ b/homeassistant/components/pi_hole/translations/de.json
@@ -4,17 +4,23 @@
"already_configured": "Service ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung konnte nicht hergestellt werden"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"step": {
+ "api_key": {
+ "data": {
+ "api_key": "API-Schl\u00fcssel"
+ }
+ },
"user": {
"data": {
- "api_key": "API-Schl\u00fcssel (optional)",
+ "api_key": "API-Schl\u00fcssel",
"host": "Host",
"location": "Org",
"name": "Name",
"port": "Port",
- "ssl": "SSL verwenden",
+ "ssl": "Nutzt ein SSL-Zertifikat",
+ "statistics_only": "Nur Statistiken",
"verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen"
}
}
diff --git a/homeassistant/components/pi_hole/translations/en.json b/homeassistant/components/pi_hole/translations/en.json
index 858e7c230ac085..9053a70c18fe48 100644
--- a/homeassistant/components/pi_hole/translations/en.json
+++ b/homeassistant/components/pi_hole/translations/en.json
@@ -7,6 +7,11 @@
"cannot_connect": "Failed to connect"
},
"step": {
+ "api_key": {
+ "data": {
+ "api_key": "API Key"
+ }
+ },
"user": {
"data": {
"api_key": "API Key",
@@ -15,6 +20,7 @@
"name": "Name",
"port": "Port",
"ssl": "Uses an SSL certificate",
+ "statistics_only": "Statistics Only",
"verify_ssl": "Verify SSL certificate"
}
}
diff --git a/homeassistant/components/pi_hole/translations/es.json b/homeassistant/components/pi_hole/translations/es.json
index 48708d6810421e..35597af49f291e 100644
--- a/homeassistant/components/pi_hole/translations/es.json
+++ b/homeassistant/components/pi_hole/translations/es.json
@@ -7,6 +7,11 @@
"cannot_connect": "No se pudo conectar"
},
"step": {
+ "api_key": {
+ "data": {
+ "api_key": "Clave API"
+ }
+ },
"user": {
"data": {
"api_key": "Clave API",
@@ -15,6 +20,7 @@
"name": "Nombre",
"port": "Puerto",
"ssl": "Usar SSL",
+ "statistics_only": "S\u00f3lo las estad\u00edsticas",
"verify_ssl": "Verificar certificado SSL"
}
}
diff --git a/homeassistant/components/pi_hole/translations/et.json b/homeassistant/components/pi_hole/translations/et.json
index c68d52c0c107a3..4ff0fdd0ba850e 100644
--- a/homeassistant/components/pi_hole/translations/et.json
+++ b/homeassistant/components/pi_hole/translations/et.json
@@ -7,6 +7,11 @@
"cannot_connect": "\u00dchendamine nurjus"
},
"step": {
+ "api_key": {
+ "data": {
+ "api_key": "API v\u00f5ti"
+ }
+ },
"user": {
"data": {
"api_key": "API v\u00f5ti",
@@ -15,6 +20,7 @@
"name": "Nimi",
"port": "",
"ssl": "Kasuatb SSL serti",
+ "statistics_only": "Ainult statistika",
"verify_ssl": "Kontrolli SSL sertifikaati"
}
}
diff --git a/homeassistant/components/pi_hole/translations/fr.json b/homeassistant/components/pi_hole/translations/fr.json
index 1ccc5ac7d761f9..152fb0f3def637 100644
--- a/homeassistant/components/pi_hole/translations/fr.json
+++ b/homeassistant/components/pi_hole/translations/fr.json
@@ -7,6 +7,11 @@
"cannot_connect": "Connexion impossible"
},
"step": {
+ "api_key": {
+ "data": {
+ "api_key": "Clef d'API"
+ }
+ },
"user": {
"data": {
"api_key": "Cl\u00e9 d'API",
@@ -15,6 +20,7 @@
"name": "Nom",
"port": "Port",
"ssl": "Utiliser SSL",
+ "statistics_only": "Statistiques uniquement",
"verify_ssl": "V\u00e9rifier le certificat SSL"
}
}
diff --git a/homeassistant/components/pi_hole/translations/hu.json b/homeassistant/components/pi_hole/translations/hu.json
index f1bd9a106bc930..a8f8563da41f0b 100644
--- a/homeassistant/components/pi_hole/translations/hu.json
+++ b/homeassistant/components/pi_hole/translations/hu.json
@@ -7,10 +7,21 @@
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
"step": {
+ "api_key": {
+ "data": {
+ "api_key": "API kulcs"
+ }
+ },
"user": {
"data": {
+ "api_key": "API kulcs",
"host": "Hoszt",
- "port": "Port"
+ "location": "Elhelyezked\u00e9s",
+ "name": "N\u00e9v",
+ "port": "Port",
+ "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata",
+ "statistics_only": "Csak statisztik\u00e1k",
+ "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se"
}
}
}
diff --git a/homeassistant/components/pi_hole/translations/id.json b/homeassistant/components/pi_hole/translations/id.json
new file mode 100644
index 00000000000000..c38c0f5c9bbad7
--- /dev/null
+++ b/homeassistant/components/pi_hole/translations/id.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "api_key": {
+ "data": {
+ "api_key": "Kunci API"
+ }
+ },
+ "user": {
+ "data": {
+ "api_key": "Kunci API",
+ "host": "Host",
+ "location": "Lokasi",
+ "name": "Nama",
+ "port": "Port",
+ "ssl": "Menggunakan sertifikat SSL",
+ "statistics_only": "Hanya Statistik",
+ "verify_ssl": "Verifikasi sertifikat SSL"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/pi_hole/translations/it.json b/homeassistant/components/pi_hole/translations/it.json
index 34590ee77bb14d..7d355caf985622 100644
--- a/homeassistant/components/pi_hole/translations/it.json
+++ b/homeassistant/components/pi_hole/translations/it.json
@@ -7,6 +7,11 @@
"cannot_connect": "Impossibile connettersi"
},
"step": {
+ "api_key": {
+ "data": {
+ "api_key": "Chiave API"
+ }
+ },
"user": {
"data": {
"api_key": "Chiave API",
@@ -15,6 +20,7 @@
"name": "Nome",
"port": "Porta",
"ssl": "Utilizza un certificato SSL",
+ "statistics_only": "Solo Statistiche",
"verify_ssl": "Verificare il certificato SSL"
}
}
diff --git a/homeassistant/components/pi_hole/translations/ko.json b/homeassistant/components/pi_hole/translations/ko.json
index 4653cc8564dec1..d79878d8a42ce5 100644
--- a/homeassistant/components/pi_hole/translations/ko.json
+++ b/homeassistant/components/pi_hole/translations/ko.json
@@ -7,6 +7,11 @@
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
+ "api_key": {
+ "data": {
+ "api_key": "API \ud0a4"
+ }
+ },
"user": {
"data": {
"api_key": "API \ud0a4",
@@ -14,8 +19,9 @@
"location": "\uc704\uce58",
"name": "\uc774\ub984",
"port": "\ud3ec\ud2b8",
- "ssl": "SSL \uc0ac\uc6a9",
- "verify_ssl": "SSL \uc778\uc99d\uc11c \uac80\uc99d"
+ "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9",
+ "statistics_only": "\ud1b5\uacc4 \uc804\uc6a9",
+ "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778"
}
}
}
diff --git a/homeassistant/components/pi_hole/translations/nl.json b/homeassistant/components/pi_hole/translations/nl.json
index 24da024acaefa3..156a248a80bd53 100644
--- a/homeassistant/components/pi_hole/translations/nl.json
+++ b/homeassistant/components/pi_hole/translations/nl.json
@@ -7,6 +7,11 @@
"cannot_connect": "Kon niet verbinden"
},
"step": {
+ "api_key": {
+ "data": {
+ "api_key": "API-sleutel"
+ }
+ },
"user": {
"data": {
"api_key": "API-sleutel",
@@ -15,6 +20,7 @@
"name": "Naam",
"port": "Poort",
"ssl": "Maakt gebruik van een SSL-certificaat",
+ "statistics_only": "Alleen statistieken",
"verify_ssl": "SSL-certificaat verifi\u00ebren"
}
}
diff --git a/homeassistant/components/pi_hole/translations/no.json b/homeassistant/components/pi_hole/translations/no.json
index 71c815ecd36dd4..7d005fa65163bf 100644
--- a/homeassistant/components/pi_hole/translations/no.json
+++ b/homeassistant/components/pi_hole/translations/no.json
@@ -7,6 +7,11 @@
"cannot_connect": "Tilkobling mislyktes"
},
"step": {
+ "api_key": {
+ "data": {
+ "api_key": "API-n\u00f8kkel"
+ }
+ },
"user": {
"data": {
"api_key": "API-n\u00f8kkel",
@@ -15,6 +20,7 @@
"name": "Navn",
"port": "Port",
"ssl": "Bruker et SSL-sertifikat",
+ "statistics_only": "Bare statistikk",
"verify_ssl": "Verifisere SSL-sertifikat"
}
}
diff --git a/homeassistant/components/pi_hole/translations/pl.json b/homeassistant/components/pi_hole/translations/pl.json
index add788ef916027..ee4b6eadd87a27 100644
--- a/homeassistant/components/pi_hole/translations/pl.json
+++ b/homeassistant/components/pi_hole/translations/pl.json
@@ -7,6 +7,11 @@
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
"step": {
+ "api_key": {
+ "data": {
+ "api_key": "Klucz API"
+ }
+ },
"user": {
"data": {
"api_key": "Klucz API",
@@ -15,6 +20,7 @@
"name": "Nazwa",
"port": "Port",
"ssl": "Certyfikat SSL",
+ "statistics_only": "Tylko statystyki",
"verify_ssl": "Weryfikacja certyfikatu SSL"
}
}
diff --git a/homeassistant/components/pi_hole/translations/ru.json b/homeassistant/components/pi_hole/translations/ru.json
index eb3cfa62c623ec..eed9596c90746f 100644
--- a/homeassistant/components/pi_hole/translations/ru.json
+++ b/homeassistant/components/pi_hole/translations/ru.json
@@ -7,6 +7,11 @@
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
},
"step": {
+ "api_key": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API"
+ }
+ },
"user": {
"data": {
"api_key": "\u041a\u043b\u044e\u0447 API",
@@ -15,6 +20,7 @@
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
"port": "\u041f\u043e\u0440\u0442",
"ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
+ "statistics_only": "\u0422\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430",
"verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL"
}
}
diff --git a/homeassistant/components/pi_hole/translations/tr.json b/homeassistant/components/pi_hole/translations/tr.json
new file mode 100644
index 00000000000000..a14e020d360277
--- /dev/null
+++ b/homeassistant/components/pi_hole/translations/tr.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "api_key": {
+ "data": {
+ "api_key": "API Anahtar\u0131"
+ }
+ },
+ "user": {
+ "data": {
+ "api_key": "API Anahtar\u0131",
+ "host": "Ana Bilgisayar",
+ "location": "Konum",
+ "port": "Port",
+ "statistics_only": "Yaln\u0131zca \u0130statistikler"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/pi_hole/translations/uk.json b/homeassistant/components/pi_hole/translations/uk.json
new file mode 100644
index 00000000000000..93413f9abff057
--- /dev/null
+++ b/homeassistant/components/pi_hole/translations/uk.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "host": "\u0425\u043e\u0441\u0442",
+ "location": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f",
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "port": "\u041f\u043e\u0440\u0442",
+ "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL",
+ "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/pi_hole/translations/zh-Hant.json b/homeassistant/components/pi_hole/translations/zh-Hant.json
index 1cea5a87f4b5f1..1527b48f580457 100644
--- a/homeassistant/components/pi_hole/translations/zh-Hant.json
+++ b/homeassistant/components/pi_hole/translations/zh-Hant.json
@@ -7,6 +7,11 @@
"cannot_connect": "\u9023\u7dda\u5931\u6557"
},
"step": {
+ "api_key": {
+ "data": {
+ "api_key": "API \u5bc6\u9470"
+ }
+ },
"user": {
"data": {
"api_key": "API \u5bc6\u9470",
@@ -15,6 +20,7 @@
"name": "\u540d\u7a31",
"port": "\u901a\u8a0a\u57e0",
"ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49",
+ "statistics_only": "\u50c5\u7d71\u8a08\u8cc7\u8a0a",
"verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49"
}
}
diff --git a/homeassistant/components/pilight/sensor.py b/homeassistant/components/pilight/sensor.py
index e8c7b4bd4b6673..97458acd5fc622 100644
--- a/homeassistant/components/pilight/sensor.py
+++ b/homeassistant/components/pilight/sensor.py
@@ -4,10 +4,9 @@
import voluptuous as vol
from homeassistant.components import pilight
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, CONF_PAYLOAD, CONF_UNIT_OF_MEASUREMENT
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -39,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
)
-class PilightSensor(Entity):
+class PilightSensor(SensorEntity):
"""Representation of a sensor that can be updated using Pilight."""
def __init__(self, hass, name, variable, payload, unit_of_measurement):
diff --git a/homeassistant/components/ping/__init__.py b/homeassistant/components/ping/__init__.py
index 19207ada1b78b1..726bb212574db0 100644
--- a/homeassistant/components/ping/__init__.py
+++ b/homeassistant/components/ping/__init__.py
@@ -1,13 +1,26 @@
"""The ping component."""
+from __future__ import annotations
+
+import logging
+
+from icmplib import SocketPermissionError, ping as icmp_ping
from homeassistant.core import callback
+from homeassistant.helpers.reload import async_setup_reload_service
+
+from .const import DEFAULT_START_ID, DOMAIN, MAX_PING_ID, PING_ID, PING_PRIVS, PLATFORMS
+
+_LOGGER = logging.getLogger(__name__)
-DOMAIN = "ping"
-PLATFORMS = ["binary_sensor"]
-PING_ID = "ping_id"
-DEFAULT_START_ID = 129
-MAX_PING_ID = 65534
+async def async_setup(hass, config):
+ """Set up the template integration."""
+ await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
+ hass.data[DOMAIN] = {
+ PING_PRIVS: await hass.async_add_executor_job(_can_use_icmp_lib_with_privilege),
+ PING_ID: DEFAULT_START_ID,
+ }
+ return True
@callback
@@ -16,8 +29,7 @@ def async_get_next_ping_id(hass):
Must be called in async
"""
- current_id = hass.data.setdefault(DOMAIN, {}).get(PING_ID, DEFAULT_START_ID)
-
+ current_id = hass.data[DOMAIN][PING_ID]
if current_id == MAX_PING_ID:
next_id = DEFAULT_START_ID
else:
@@ -26,3 +38,23 @@ def async_get_next_ping_id(hass):
hass.data[DOMAIN][PING_ID] = next_id
return next_id
+
+
+def _can_use_icmp_lib_with_privilege() -> None | bool:
+ """Verify we can create a raw socket."""
+ try:
+ icmp_ping("127.0.0.1", count=0, timeout=0, privileged=True)
+ except SocketPermissionError:
+ try:
+ icmp_ping("127.0.0.1", count=0, timeout=0, privileged=False)
+ except SocketPermissionError:
+ _LOGGER.debug(
+ "Cannot use icmplib because privileges are insufficient to create the socket"
+ )
+ return None
+ else:
+ _LOGGER.debug("Using icmplib in privileged=False mode")
+ return False
+ else:
+ _LOGGER.debug("Using icmplib in privileged=True mode")
+ return True
diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py
index 98c36c01d987eb..9ae891d598b82e 100644
--- a/homeassistant/components/ping/binary_sensor.py
+++ b/homeassistant/components/ping/binary_sensor.py
@@ -1,13 +1,16 @@
"""Tracks the latency of a host by sending ICMP echo requests (ping)."""
+from __future__ import annotations
+
import asyncio
+from contextlib import suppress
from datetime import timedelta
from functools import partial
import logging
import re
import sys
-from typing import Any, Dict
+from typing import Any
-from icmplib import SocketPermissionError, ping as icmp_ping
+from icmplib import NameLookupError, ping as icmp_ping
import voluptuous as vol
from homeassistant.components.binary_sensor import (
@@ -15,12 +18,12 @@
PLATFORM_SCHEMA,
BinarySensorEntity,
)
-from homeassistant.const import CONF_HOST, CONF_NAME
+from homeassistant.const import CONF_HOST, CONF_NAME, STATE_ON
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.reload import setup_reload_service
+from homeassistant.helpers.restore_state import RestoreEntity
-from . import DOMAIN, PLATFORMS, async_get_next_ping_id
-from .const import PING_TIMEOUT
+from . import async_get_next_ping_id
+from .const import DOMAIN, ICMP_TIMEOUT, PING_PRIVS, PING_TIMEOUT
_LOGGER = logging.getLogger(__name__)
@@ -60,32 +63,30 @@
)
-def setup_platform(hass, config, add_entities, discovery_info=None) -> None:
+async def async_setup_platform(
+ hass, config, async_add_entities, discovery_info=None
+) -> None:
"""Set up the Ping Binary sensor."""
- setup_reload_service(hass, DOMAIN, PLATFORMS)
-
host = config[CONF_HOST]
count = config[CONF_PING_COUNT]
name = config.get(CONF_NAME, f"{DEFAULT_NAME} {host}")
-
- try:
- # Verify we can create a raw socket, or
- # fallback to using a subprocess
- icmp_ping("127.0.0.1", count=0, timeout=0)
- ping_cls = PingDataICMPLib
- except SocketPermissionError:
+ privileged = hass.data[DOMAIN][PING_PRIVS]
+ if privileged is None:
ping_cls = PingDataSubProcess
+ else:
+ ping_cls = PingDataICMPLib
- ping_data = ping_cls(hass, host, count)
-
- add_entities([PingBinarySensor(name, ping_data)], True)
+ async_add_entities(
+ [PingBinarySensor(name, ping_cls(hass, host, count, privileged))]
+ )
-class PingBinarySensor(BinarySensorEntity):
+class PingBinarySensor(RestoreEntity, BinarySensorEntity):
"""Representation of a Ping Binary sensor."""
def __init__(self, name: str, ping) -> None:
"""Initialize the Ping Binary sensor."""
+ self._available = False
self._name = name
self._ping = ping
@@ -94,6 +95,11 @@ def name(self) -> str:
"""Return the name of the device."""
return self._name
+ @property
+ def available(self) -> str:
+ """Return if we have done the first ping."""
+ return self._available
+
@property
def device_class(self) -> str:
"""Return the class of this sensor."""
@@ -102,10 +108,10 @@ def device_class(self) -> str:
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
- return self._ping.available
+ return self._ping.is_alive
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the ICMP checo request."""
if self._ping.data is not False:
return {
@@ -118,6 +124,28 @@ def device_state_attributes(self) -> Dict[str, Any]:
async def async_update(self) -> None:
"""Get the latest data."""
await self._ping.async_update()
+ self._available = True
+
+ async def async_added_to_hass(self):
+ """Restore previous state on restart to avoid blocking startup."""
+ await super().async_added_to_hass()
+
+ last_state = await self.async_get_last_state()
+ if last_state is not None:
+ self._available = True
+
+ if last_state is None or last_state.state != STATE_ON:
+ self._ping.data = False
+ return
+
+ attributes = last_state.attributes
+ self._ping.is_alive = True
+ self._ping.data = {
+ "min": attributes[ATTR_ROUND_TRIP_TIME_AVG],
+ "max": attributes[ATTR_ROUND_TRIP_TIME_MAX],
+ "avg": attributes[ATTR_ROUND_TRIP_TIME_MDEV],
+ "mdev": attributes[ATTR_ROUND_TRIP_TIME_MIN],
+ }
class PingData:
@@ -129,26 +157,37 @@ def __init__(self, hass, host, count) -> None:
self._ip_address = host
self._count = count
self.data = {}
- self.available = False
+ self.is_alive = False
class PingDataICMPLib(PingData):
"""The Class for handling the data retrieval using icmplib."""
+ def __init__(self, hass, host, count, privileged) -> None:
+ """Initialize the data object."""
+ super().__init__(hass, host, count)
+ self._privileged = privileged
+
async def async_update(self) -> None:
"""Retrieve the latest details from the host."""
_LOGGER.debug("ping address: %s", self._ip_address)
- data = await self.hass.async_add_executor_job(
- partial(
- icmp_ping,
- self._ip_address,
- count=self._count,
- timeout=1,
- id=async_get_next_ping_id(self.hass),
+ try:
+ data = await self.hass.async_add_executor_job(
+ partial(
+ icmp_ping,
+ self._ip_address,
+ count=self._count,
+ timeout=ICMP_TIMEOUT,
+ id=async_get_next_ping_id(self.hass),
+ privileged=self._privileged,
+ )
)
- )
- self.available = data.is_alive
- if not self.available:
+ except NameLookupError:
+ self.is_alive = False
+ return
+
+ self.is_alive = data.is_alive
+ if not self.is_alive:
self.data = False
return
@@ -163,7 +202,7 @@ async def async_update(self) -> None:
class PingDataSubProcess(PingData):
"""The Class for handling the data retrieval using the ping binary."""
- def __init__(self, hass, host, count) -> None:
+ def __init__(self, hass, host, count, privileged) -> None:
"""Initialize the data object."""
super().__init__(hass, host, count)
if sys.platform == "win32":
@@ -240,10 +279,8 @@ async def async_ping(self):
self._count + PING_TIMEOUT,
)
if pinger:
- try:
+ with suppress(TypeError):
await pinger.kill()
- except TypeError:
- pass
del pinger
return False
@@ -253,4 +290,4 @@ async def async_ping(self):
async def async_update(self) -> None:
"""Retrieve the latest details from the host."""
self.data = await self.async_ping()
- self.available = bool(self.data)
+ self.is_alive = bool(self.data)
diff --git a/homeassistant/components/ping/const.py b/homeassistant/components/ping/const.py
index 89b93c84169fc9..62fca9123ba2e8 100644
--- a/homeassistant/components/ping/const.py
+++ b/homeassistant/components/ping/const.py
@@ -1,4 +1,21 @@
"""Tracks devices by sending a ICMP echo request (ping)."""
+# The ping binary and icmplib timeouts are not the same
+# timeout. ping is an overall timeout, icmplib is the
+# time since the data was sent.
+
+# ping binary
PING_TIMEOUT = 3
+
+# icmplib timeout
+ICMP_TIMEOUT = 1
+
PING_ATTEMPTS_COUNT = 3
+
+DOMAIN = "ping"
+PLATFORMS = ["binary_sensor"]
+
+PING_ID = "ping_id"
+PING_PRIVS = "ping_privs"
+DEFAULT_START_ID = 129
+MAX_PING_ID = 65534
diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py
index 4139438bcfeff1..a6b75a9245b76d 100644
--- a/homeassistant/components/ping/device_tracker.py
+++ b/homeassistant/components/ping/device_tracker.py
@@ -1,10 +1,12 @@
"""Tracks devices by sending a ICMP echo request (ping)."""
+import asyncio
from datetime import timedelta
+from functools import partial
import logging
import subprocess
import sys
-from icmplib import SocketPermissionError, ping as icmp_ping
+from icmplib import multiping
import voluptuous as vol
from homeassistant import const, util
@@ -15,16 +17,18 @@
SOURCE_TYPE_ROUTER,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.util.async_ import run_callback_threadsafe
+from homeassistant.helpers.event import async_track_point_in_utc_time
+from homeassistant.util.async_ import gather_with_concurrency
from homeassistant.util.process import kill_subprocess
from . import async_get_next_ping_id
-from .const import PING_ATTEMPTS_COUNT, PING_TIMEOUT
+from .const import DOMAIN, ICMP_TIMEOUT, PING_ATTEMPTS_COUNT, PING_PRIVS, PING_TIMEOUT
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
CONF_PING_COUNT = "count"
+CONCURRENT_PING_LIMIT = 6
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -37,16 +41,16 @@
class HostSubProcess:
"""Host object with ping detection."""
- def __init__(self, ip_address, dev_id, hass, config):
+ def __init__(self, ip_address, dev_id, hass, config, privileged):
"""Initialize the Host pinger."""
self.hass = hass
self.ip_address = ip_address
self.dev_id = dev_id
self._count = config[CONF_PING_COUNT]
if sys.platform == "win32":
- self._ping_cmd = ["ping", "-n", "1", "-w", "1000", self.ip_address]
+ self._ping_cmd = ["ping", "-n", "1", "-w", "1000", ip_address]
else:
- self._ping_cmd = ["ping", "-n", "-q", "-c1", "-W1", self.ip_address]
+ self._ping_cmd = ["ping", "-n", "-q", "-c1", "-W1", ip_address]
def ping(self):
"""Send an ICMP echo request and return True if success."""
@@ -63,86 +67,83 @@ def ping(self):
except subprocess.CalledProcessError:
return False
- def update(self, see):
+ def update(self) -> bool:
"""Update device state by sending one or more ping messages."""
failed = 0
while failed < self._count: # check more times if host is unreachable
if self.ping():
- see(dev_id=self.dev_id, source_type=SOURCE_TYPE_ROUTER)
return True
failed += 1
_LOGGER.debug("No response from %s failed=%d", self.ip_address, failed)
+ return False
-class HostICMPLib:
- """Host object with ping detection."""
-
- def __init__(self, ip_address, dev_id, hass, config):
- """Initialize the Host pinger."""
- self.hass = hass
- self.ip_address = ip_address
- self.dev_id = dev_id
- self._count = config[CONF_PING_COUNT]
-
- def ping(self):
- """Send an ICMP echo request and return True if success."""
- next_id = run_callback_threadsafe(
- self.hass.loop, async_get_next_ping_id, self.hass
- ).result()
-
- return icmp_ping(
- self.ip_address, count=PING_ATTEMPTS_COUNT, timeout=1, id=next_id
- ).is_alive
-
- def update(self, see):
- """Update device state by sending one or more ping messages."""
- if self.ping():
- see(dev_id=self.dev_id, source_type=SOURCE_TYPE_ROUTER)
- return True
-
- _LOGGER.debug(
- "No response from %s (%s) failed=%d",
- self.ip_address,
- self.dev_id,
- PING_ATTEMPTS_COUNT,
- )
-
-
-def setup_scanner(hass, config, see, discovery_info=None):
+async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up the Host objects and return the update function."""
- try:
- # Verify we can create a raw socket, or
- # fallback to using a subprocess
- icmp_ping("127.0.0.1", count=0, timeout=0)
- host_cls = HostICMPLib
- except SocketPermissionError:
- host_cls = HostSubProcess
-
- hosts = [
- host_cls(ip, dev_id, hass, config)
- for (dev_id, ip) in config[const.CONF_HOSTS].items()
- ]
+ privileged = hass.data[DOMAIN][PING_PRIVS]
+ ip_to_dev_id = {ip: dev_id for (dev_id, ip) in config[const.CONF_HOSTS].items()}
interval = config.get(
CONF_SCAN_INTERVAL,
- timedelta(seconds=len(hosts) * config[CONF_PING_COUNT]) + SCAN_INTERVAL,
+ timedelta(seconds=len(ip_to_dev_id) * config[CONF_PING_COUNT]) + SCAN_INTERVAL,
)
_LOGGER.debug(
"Started ping tracker with interval=%s on hosts: %s",
interval,
- ",".join([host.ip_address for host in hosts]),
+ ",".join(ip_to_dev_id.keys()),
)
- def update_interval(now):
- """Update all the hosts on every interval time."""
+ if privileged is None:
+ hosts = [
+ HostSubProcess(ip, dev_id, hass, config, privileged)
+ for (dev_id, ip) in config[const.CONF_HOSTS].items()
+ ]
+
+ async def async_update(now):
+ """Update all the hosts on every interval time."""
+ results = await gather_with_concurrency(
+ CONCURRENT_PING_LIMIT,
+ *[hass.async_add_executor_job(host.update) for host in hosts],
+ )
+ await asyncio.gather(
+ *[
+ async_see(dev_id=host.dev_id, source_type=SOURCE_TYPE_ROUTER)
+ for idx, host in enumerate(hosts)
+ if results[idx]
+ ]
+ )
+
+ else:
+
+ async def async_update(now):
+ """Update all the hosts on every interval time."""
+ responses = await hass.async_add_executor_job(
+ partial(
+ multiping,
+ ip_to_dev_id.keys(),
+ count=PING_ATTEMPTS_COUNT,
+ timeout=ICMP_TIMEOUT,
+ privileged=privileged,
+ id=async_get_next_ping_id(hass),
+ )
+ )
+ _LOGGER.debug("Multiping responses: %s", responses)
+ await asyncio.gather(
+ *[
+ async_see(dev_id=dev_id, source_type=SOURCE_TYPE_ROUTER)
+ for idx, dev_id in enumerate(ip_to_dev_id.values())
+ if responses[idx].is_alive
+ ]
+ )
+
+ async def _async_update_interval(now):
try:
- for host in hosts:
- host.update(see)
+ await async_update(now)
finally:
- hass.helpers.event.track_point_in_utc_time(
- update_interval, util.dt.utcnow() + interval
+ async_track_point_in_utc_time(
+ hass, _async_update_interval, util.dt.utcnow() + interval
)
- update_interval(None)
+ await _async_update_interval(None)
return True
diff --git a/homeassistant/components/ping/manifest.json b/homeassistant/components/ping/manifest.json
index 258a75caa02332..0995478760866f 100644
--- a/homeassistant/components/ping/manifest.json
+++ b/homeassistant/components/ping/manifest.json
@@ -3,6 +3,6 @@
"name": "Ping (ICMP)",
"documentation": "https://www.home-assistant.io/integrations/ping",
"codeowners": [],
- "requirements": ["icmplib==2.0"],
+ "requirements": ["icmplib==2.1.1"],
"quality_scale": "internal"
}
diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py
index b365c7e0081bed..2ec6028f9f94c9 100644
--- a/homeassistant/components/plaato/__init__.py
+++ b/homeassistant/components/plaato/__init__.py
@@ -1,11 +1,34 @@
-"""Support for Plaato Airlock."""
+"""Support for Plaato devices."""
+
+import asyncio
+from datetime import timedelta
import logging
from aiohttp import web
+from pyplaato.models.airlock import PlaatoAirlock
+from pyplaato.plaato import (
+ ATTR_ABV,
+ ATTR_BATCH_VOLUME,
+ ATTR_BPM,
+ ATTR_BUBBLES,
+ ATTR_CO2_VOLUME,
+ ATTR_DEVICE_ID,
+ ATTR_DEVICE_NAME,
+ ATTR_OG,
+ ATTR_SG,
+ ATTR_TEMP,
+ ATTR_TEMP_UNIT,
+ ATTR_VOLUME_UNIT,
+ Plaato,
+ PlaatoDeviceType,
+)
import voluptuous as vol
from homeassistant.components.sensor import DOMAIN as SENSOR
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
+ CONF_SCAN_INTERVAL,
+ CONF_TOKEN,
CONF_WEBHOOK_ID,
HTTP_OK,
TEMP_CELSIUS,
@@ -13,31 +36,32 @@
VOLUME_GALLONS,
VOLUME_LITERS,
)
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers import aiohttp_client
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
-
-from .const import DOMAIN
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+
+from .const import (
+ CONF_DEVICE_NAME,
+ CONF_DEVICE_TYPE,
+ CONF_USE_WEBHOOK,
+ COORDINATOR,
+ DEFAULT_SCAN_INTERVAL,
+ DEVICE,
+ DEVICE_ID,
+ DEVICE_NAME,
+ DEVICE_TYPE,
+ DOMAIN,
+ PLATFORMS,
+ SENSOR_DATA,
+ UNDO_UPDATE_LISTENER,
+)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["webhook"]
-PLAATO_DEVICE_SENSORS = "sensors"
-PLAATO_DEVICE_ATTRS = "attrs"
-
-ATTR_DEVICE_ID = "device_id"
-ATTR_DEVICE_NAME = "device_name"
-ATTR_TEMP_UNIT = "temp_unit"
-ATTR_VOLUME_UNIT = "volume_unit"
-ATTR_BPM = "bpm"
-ATTR_TEMP = "temp"
-ATTR_SG = "sg"
-ATTR_OG = "og"
-ATTR_BUBBLES = "bubbles"
-ATTR_ABV = "abv"
-ATTR_CO2_VOLUME = "co2_volume"
-ATTR_BATCH_VOLUME = "batch_volume"
-
SENSOR_UPDATE = f"{DOMAIN}_sensor_update"
SENSOR_DATA_KEY = f"{DOMAIN}.{SENSOR}"
@@ -60,31 +84,122 @@
)
-async def async_setup(hass, hass_config):
+async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Plaato component."""
+ hass.data.setdefault(DOMAIN, {})
return True
-async def async_setup_entry(hass, entry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Configure based on config entry."""
- if DOMAIN not in hass.data:
- hass.data[DOMAIN] = {}
- webhook_id = entry.data[CONF_WEBHOOK_ID]
- hass.components.webhook.async_register(DOMAIN, "Plaato", webhook_id, handle_webhook)
+ use_webhook = entry.data[CONF_USE_WEBHOOK]
- hass.async_create_task(hass.config_entries.async_forward_entry_setup(entry, SENSOR))
+ if use_webhook:
+ async_setup_webhook(hass, entry)
+ else:
+ await async_setup_coordinator(hass, entry)
+
+ for platform in PLATFORMS:
+ if entry.options.get(platform, True):
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
return True
-async def async_unload_entry(hass, entry):
+@callback
+def async_setup_webhook(hass: HomeAssistant, entry: ConfigEntry):
+ """Init webhook based on config entry."""
+ webhook_id = entry.data[CONF_WEBHOOK_ID]
+ device_name = entry.data[CONF_DEVICE_NAME]
+
+ _set_entry_data(entry, hass)
+
+ hass.components.webhook.async_register(
+ DOMAIN, f"{DOMAIN}.{device_name}", webhook_id, handle_webhook
+ )
+
+
+async def async_setup_coordinator(hass: HomeAssistant, entry: ConfigEntry):
+ """Init auth token based on config entry."""
+ auth_token = entry.data[CONF_TOKEN]
+ device_type = entry.data[CONF_DEVICE_TYPE]
+
+ if entry.options.get(CONF_SCAN_INTERVAL):
+ update_interval = timedelta(minutes=entry.options[CONF_SCAN_INTERVAL])
+ else:
+ update_interval = timedelta(minutes=DEFAULT_SCAN_INTERVAL)
+
+ coordinator = PlaatoCoordinator(hass, auth_token, device_type, update_interval)
+ await coordinator.async_config_entry_first_refresh()
+
+ _set_entry_data(entry, hass, coordinator, auth_token)
+
+ for platform in PLATFORMS:
+ if entry.options.get(platform, True):
+ coordinator.platforms.append(platform)
+
+
+def _set_entry_data(entry, hass, coordinator=None, device_id=None):
+ device = {
+ DEVICE_NAME: entry.data[CONF_DEVICE_NAME],
+ DEVICE_TYPE: entry.data[CONF_DEVICE_TYPE],
+ DEVICE_ID: device_id,
+ }
+
+ hass.data[DOMAIN][entry.entry_id] = {
+ COORDINATOR: coordinator,
+ DEVICE: device,
+ SENSOR_DATA: None,
+ UNDO_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener),
+ }
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
- hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
- hass.data[SENSOR_DATA_KEY]()
+ use_webhook = entry.data[CONF_USE_WEBHOOK]
+ hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
- await hass.config_entries.async_forward_entry_unload(entry, SENSOR)
- return True
+ if use_webhook:
+ return await async_unload_webhook(hass, entry)
+
+ return await async_unload_coordinator(hass, entry)
+
+
+async def async_unload_webhook(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload webhook based entry."""
+ if entry.data[CONF_WEBHOOK_ID] is not None:
+ hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
+ return await async_unload_platforms(hass, entry, PLATFORMS)
+
+
+async def async_unload_coordinator(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload auth token based entry."""
+ coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]
+ return await async_unload_platforms(hass, entry, coordinator.platforms)
+
+
+async def async_unload_platforms(hass: HomeAssistant, entry: ConfigEntry, platforms):
+ """Unload platforms."""
+ unloaded = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in platforms
+ ]
+ )
+ )
+ if unloaded:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unloaded
+
+
+async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry):
+ """Handle options update."""
+ await hass.config_entries.async_reload(entry.entry_id)
async def handle_webhook(hass, webhook_id, request):
@@ -96,31 +211,9 @@ async def handle_webhook(hass, webhook_id, request):
return
device_id = _device_id(data)
+ sensor_data = PlaatoAirlock.from_web_hook(data)
- attrs = {
- ATTR_DEVICE_NAME: data.get(ATTR_DEVICE_NAME),
- ATTR_DEVICE_ID: data.get(ATTR_DEVICE_ID),
- ATTR_TEMP_UNIT: data.get(ATTR_TEMP_UNIT),
- ATTR_VOLUME_UNIT: data.get(ATTR_VOLUME_UNIT),
- }
-
- sensors = {
- ATTR_TEMP: data.get(ATTR_TEMP),
- ATTR_BPM: data.get(ATTR_BPM),
- ATTR_SG: data.get(ATTR_SG),
- ATTR_OG: data.get(ATTR_OG),
- ATTR_ABV: data.get(ATTR_ABV),
- ATTR_CO2_VOLUME: data.get(ATTR_CO2_VOLUME),
- ATTR_BATCH_VOLUME: data.get(ATTR_BATCH_VOLUME),
- ATTR_BUBBLES: data.get(ATTR_BUBBLES),
- }
-
- hass.data[DOMAIN][device_id] = {
- PLAATO_DEVICE_ATTRS: attrs,
- PLAATO_DEVICE_SENSORS: sensors,
- }
-
- async_dispatcher_send(hass, SENSOR_UPDATE, device_id)
+ async_dispatcher_send(hass, SENSOR_UPDATE, *(device_id, sensor_data))
return web.Response(text=f"Saving status for {device_id}", status=HTTP_OK)
@@ -128,3 +221,35 @@ async def handle_webhook(hass, webhook_id, request):
def _device_id(data):
"""Return name of device sensor."""
return f"{data.get(ATTR_DEVICE_NAME)}_{data.get(ATTR_DEVICE_ID)}"
+
+
+class PlaatoCoordinator(DataUpdateCoordinator):
+ """Class to manage fetching data from the API."""
+
+ def __init__(
+ self,
+ hass,
+ auth_token,
+ device_type: PlaatoDeviceType,
+ update_interval: timedelta,
+ ):
+ """Initialize."""
+ self.api = Plaato(auth_token=auth_token)
+ self.hass = hass
+ self.device_type = device_type
+ self.platforms = []
+
+ super().__init__(
+ hass,
+ _LOGGER,
+ name=DOMAIN,
+ update_interval=update_interval,
+ )
+
+ async def _async_update_data(self):
+ """Update data via library."""
+ data = await self.api.get_data(
+ session=aiohttp_client.async_get_clientsession(self.hass),
+ device_type=self.device_type,
+ )
+ return data
diff --git a/homeassistant/components/plaato/binary_sensor.py b/homeassistant/components/plaato/binary_sensor.py
new file mode 100644
index 00000000000000..27150692d6f0e8
--- /dev/null
+++ b/homeassistant/components/plaato/binary_sensor.py
@@ -0,0 +1,54 @@
+"""Support for Plaato Airlock sensors."""
+
+import logging
+
+from pyplaato.plaato import PlaatoKeg
+
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_OPENING,
+ DEVICE_CLASS_PROBLEM,
+ BinarySensorEntity,
+)
+
+from .const import CONF_USE_WEBHOOK, COORDINATOR, DOMAIN
+from .entity import PlaatoEntity
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Plaato from a config entry."""
+
+ if config_entry.data[CONF_USE_WEBHOOK]:
+ return
+
+ coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
+ async_add_entities(
+ PlaatoBinarySensor(
+ hass.data[DOMAIN][config_entry.entry_id],
+ sensor_type,
+ coordinator,
+ )
+ for sensor_type in coordinator.data.binary_sensors
+ )
+
+
+class PlaatoBinarySensor(PlaatoEntity, BinarySensorEntity):
+ """Representation of a Binary Sensor."""
+
+ @property
+ def is_on(self):
+ """Return true if the binary sensor is on."""
+ if self._coordinator is not None:
+ return self._coordinator.data.binary_sensors.get(self._sensor_type)
+ return False
+
+ @property
+ def device_class(self):
+ """Return the class of this device, from component DEVICE_CLASSES."""
+ if self._coordinator is None:
+ return None
+ if self._sensor_type is PlaatoKeg.Pins.LEAK_DETECTION:
+ return DEVICE_CLASS_PROBLEM
+ if self._sensor_type is PlaatoKeg.Pins.POURING:
+ return DEVICE_CLASS_OPENING
diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py
index 3c616c822fb984..2cb1f4ce3261b0 100644
--- a/homeassistant/components/plaato/config_flow.py
+++ b/homeassistant/components/plaato/config_flow.py
@@ -1,10 +1,223 @@
-"""Config flow for GPSLogger."""
-from homeassistant.helpers import config_entry_flow
+"""Config flow for Plaato."""
+import logging
-from .const import DOMAIN
+from pyplaato.plaato import PlaatoDeviceType
+import voluptuous as vol
-config_entry_flow.register_webhook_flow(
+from homeassistant import config_entries
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_WEBHOOK_ID
+from homeassistant.core import callback
+import homeassistant.helpers.config_validation as cv
+
+from .const import (
+ CONF_CLOUDHOOK,
+ CONF_DEVICE_NAME,
+ CONF_DEVICE_TYPE,
+ CONF_USE_WEBHOOK,
+ DEFAULT_SCAN_INTERVAL,
+ DOCS_URL,
DOMAIN,
- "Webhook",
- {"docs_url": "https://www.home-assistant.io/integrations/plaato/"},
+ PLACEHOLDER_DEVICE_NAME,
+ PLACEHOLDER_DEVICE_TYPE,
+ PLACEHOLDER_DOCS_URL,
+ PLACEHOLDER_WEBHOOK_URL,
)
+
+_LOGGER = logging.getLogger(__package__)
+
+
+class PlaatoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handles a Plaato config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ def __init__(self):
+ """Initialize."""
+ self._init_info = {}
+
+ async def async_step_user(self, user_input=None):
+ """Handle user step."""
+
+ if user_input is not None:
+ self._init_info[CONF_DEVICE_TYPE] = PlaatoDeviceType(
+ user_input[CONF_DEVICE_TYPE]
+ )
+ self._init_info[CONF_DEVICE_NAME] = user_input[CONF_DEVICE_NAME]
+
+ return await self.async_step_api_method()
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ CONF_DEVICE_NAME,
+ default=self._init_info.get(CONF_DEVICE_NAME, None),
+ ): str,
+ vol.Required(
+ CONF_DEVICE_TYPE,
+ default=self._init_info.get(CONF_DEVICE_TYPE, None),
+ ): vol.In(list(PlaatoDeviceType)),
+ }
+ ),
+ )
+
+ async def async_step_api_method(self, user_input=None):
+ """Handle device type step."""
+
+ device_type = self._init_info[CONF_DEVICE_TYPE]
+
+ if user_input is not None:
+ token = user_input.get(CONF_TOKEN, None)
+ use_webhook = user_input.get(CONF_USE_WEBHOOK, False)
+
+ if not token and not use_webhook:
+ errors = {"base": PlaatoConfigFlow._get_error(device_type)}
+ return await self._show_api_method_form(device_type, errors)
+
+ self._init_info[CONF_USE_WEBHOOK] = use_webhook
+ self._init_info[CONF_TOKEN] = token
+ return await self.async_step_webhook()
+
+ return await self._show_api_method_form(device_type)
+
+ async def async_step_webhook(self, user_input=None):
+ """Validate config step."""
+
+ use_webhook = self._init_info[CONF_USE_WEBHOOK]
+
+ if use_webhook and user_input is None:
+ webhook_id, webhook_url, cloudhook = await self._get_webhook_id()
+ self._init_info[CONF_WEBHOOK_ID] = webhook_id
+ self._init_info[CONF_CLOUDHOOK] = cloudhook
+
+ return self.async_show_form(
+ step_id="webhook",
+ description_placeholders={
+ PLACEHOLDER_WEBHOOK_URL: webhook_url,
+ PLACEHOLDER_DOCS_URL: DOCS_URL,
+ },
+ )
+
+ return await self._async_create_entry()
+
+ async def _async_create_entry(self):
+ """Create the entry step."""
+
+ webhook_id = self._init_info.get(CONF_WEBHOOK_ID, None)
+ auth_token = self._init_info[CONF_TOKEN]
+ device_name = self._init_info[CONF_DEVICE_NAME]
+ device_type = self._init_info[CONF_DEVICE_TYPE]
+
+ unique_id = auth_token if auth_token else webhook_id
+
+ await self.async_set_unique_id(unique_id)
+ self._abort_if_unique_id_configured()
+
+ return self.async_create_entry(
+ title=device_type.name,
+ data=self._init_info,
+ description_placeholders={
+ PLACEHOLDER_DEVICE_TYPE: device_type.name,
+ PLACEHOLDER_DEVICE_NAME: device_name,
+ },
+ )
+
+ async def _show_api_method_form(
+ self, device_type: PlaatoDeviceType, errors: dict = None
+ ):
+ data_schema = vol.Schema({vol.Optional(CONF_TOKEN, default=""): str})
+
+ if device_type == PlaatoDeviceType.Airlock:
+ data_schema = data_schema.extend(
+ {vol.Optional(CONF_USE_WEBHOOK, default=False): bool}
+ )
+
+ return self.async_show_form(
+ step_id="api_method",
+ data_schema=data_schema,
+ errors=errors,
+ description_placeholders={PLACEHOLDER_DEVICE_TYPE: device_type.name},
+ )
+
+ async def _get_webhook_id(self):
+ """Generate webhook ID."""
+ webhook_id = self.hass.components.webhook.async_generate_id()
+ if self.hass.components.cloud.async_active_subscription():
+ webhook_url = await self.hass.components.cloud.async_create_cloudhook(
+ webhook_id
+ )
+ cloudhook = True
+ else:
+ webhook_url = self.hass.components.webhook.async_generate_url(webhook_id)
+ cloudhook = False
+
+ return webhook_id, webhook_url, cloudhook
+
+ @staticmethod
+ def _get_error(device_type: PlaatoDeviceType):
+ if device_type == PlaatoDeviceType.Airlock:
+ return "no_api_method"
+ return "no_auth_token"
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for this handler."""
+ return PlaatoOptionsFlowHandler(config_entry)
+
+
+class PlaatoOptionsFlowHandler(config_entries.OptionsFlow):
+ """Handle Plaato options."""
+
+ def __init__(self, config_entry: ConfigEntry):
+ """Initialize domain options flow."""
+ super().__init__()
+
+ self._config_entry = config_entry
+
+ async def async_step_init(self, user_input=None):
+ """Manage the options."""
+ use_webhook = self._config_entry.data.get(CONF_USE_WEBHOOK, False)
+ if use_webhook:
+ return await self.async_step_webhook()
+
+ return await self.async_step_user()
+
+ async def async_step_user(self, user_input=None):
+ """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="user",
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_SCAN_INTERVAL,
+ default=self._config_entry.options.get(
+ CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
+ ),
+ ): cv.positive_int
+ }
+ ),
+ )
+
+ async def async_step_webhook(self, user_input=None):
+ """Manage the options for webhook device."""
+ if user_input is not None:
+ return self.async_create_entry(title="", data=user_input)
+
+ webhook_id = self._config_entry.data.get(CONF_WEBHOOK_ID, None)
+ webhook_url = (
+ ""
+ if webhook_id is None
+ else self.hass.components.webhook.async_generate_url(webhook_id)
+ )
+
+ return self.async_show_form(
+ step_id="webhook",
+ description_placeholders={PLACEHOLDER_WEBHOOK_URL: webhook_url},
+ )
diff --git a/homeassistant/components/plaato/const.py b/homeassistant/components/plaato/const.py
index cbe8fcd2b6dc43..1700b803775d6a 100644
--- a/homeassistant/components/plaato/const.py
+++ b/homeassistant/components/plaato/const.py
@@ -1,3 +1,36 @@
-"""Const for GPSLogger."""
+"""Const for Plaato."""
+from datetime import timedelta
DOMAIN = "plaato"
+PLAATO_DEVICE_SENSORS = "sensors"
+PLAATO_DEVICE_ATTRS = "attrs"
+SENSOR_SIGNAL = f"{DOMAIN}_%s_%s"
+
+CONF_USE_WEBHOOK = "use_webhook"
+CONF_DEVICE_TYPE = "device_type"
+CONF_DEVICE_NAME = "device_name"
+CONF_CLOUDHOOK = "cloudhook"
+PLACEHOLDER_WEBHOOK_URL = "webhook_url"
+PLACEHOLDER_DOCS_URL = "docs_url"
+PLACEHOLDER_DEVICE_TYPE = "device_type"
+PLACEHOLDER_DEVICE_NAME = "device_name"
+DOCS_URL = "https://www.home-assistant.io/integrations/plaato/"
+PLATFORMS = ["sensor", "binary_sensor"]
+SENSOR_DATA = "sensor_data"
+COORDINATOR = "coordinator"
+DEVICE = "device"
+DEVICE_NAME = "device_name"
+DEVICE_TYPE = "device_type"
+DEVICE_ID = "device_id"
+UNDO_UPDATE_LISTENER = "undo_update_listener"
+DEFAULT_SCAN_INTERVAL = 5
+MIN_UPDATE_INTERVAL = timedelta(minutes=1)
+
+DEVICE_STATE_ATTRIBUTES = {
+ "beer_name": "beer_name",
+ "keg_date": "keg_date",
+ "mode": "mode",
+ "original_gravity": "original_gravity",
+ "final_gravity": "final_gravity",
+ "alcohol_by_volume": "alcohol_by_volume",
+}
diff --git a/homeassistant/components/plaato/entity.py b/homeassistant/components/plaato/entity.py
new file mode 100644
index 00000000000000..a28dfefb56727d
--- /dev/null
+++ b/homeassistant/components/plaato/entity.py
@@ -0,0 +1,109 @@
+"""PlaatoEntity class."""
+from pyplaato.models.device import PlaatoDevice
+
+from homeassistant.helpers import entity
+
+from .const import (
+ DEVICE,
+ DEVICE_ID,
+ DEVICE_NAME,
+ DEVICE_STATE_ATTRIBUTES,
+ DEVICE_TYPE,
+ DOMAIN,
+ SENSOR_DATA,
+ SENSOR_SIGNAL,
+)
+
+
+class PlaatoEntity(entity.Entity):
+ """Representation of a Plaato Entity."""
+
+ def __init__(self, data, sensor_type, coordinator=None):
+ """Initialize the sensor."""
+ self._coordinator = coordinator
+ self._entry_data = data
+ self._sensor_type = sensor_type
+ self._device_id = data[DEVICE][DEVICE_ID]
+ self._device_type = data[DEVICE][DEVICE_TYPE]
+ self._device_name = data[DEVICE][DEVICE_NAME]
+ self._state = 0
+
+ @property
+ def _attributes(self) -> dict:
+ return PlaatoEntity._to_snake_case(self._sensor_data.attributes)
+
+ @property
+ def _sensor_name(self) -> str:
+ return self._sensor_data.get_sensor_name(self._sensor_type)
+
+ @property
+ def _sensor_data(self) -> PlaatoDevice:
+ if self._coordinator:
+ return self._coordinator.data
+ return self._entry_data[SENSOR_DATA]
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return f"{DOMAIN} {self._device_type} {self._device_name} {self._sensor_name}".title()
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of this sensor."""
+ return f"{self._device_id}_{self._sensor_type}"
+
+ @property
+ def device_info(self):
+ """Get device info."""
+ device_info = {
+ "identifiers": {(DOMAIN, self._device_id)},
+ "name": self._device_name,
+ "manufacturer": "Plaato",
+ "model": self._device_type,
+ }
+
+ if self._sensor_data.firmware_version != "":
+ device_info["sw_version"] = self._sensor_data.firmware_version
+
+ return device_info
+
+ @property
+ def extra_state_attributes(self):
+ """Return the state attributes of the monitored installation."""
+ if self._attributes:
+ return {
+ attr_key: self._attributes[plaato_key]
+ for attr_key, plaato_key in DEVICE_STATE_ATTRIBUTES.items()
+ if plaato_key in self._attributes
+ and self._attributes[plaato_key] is not None
+ }
+
+ @property
+ def available(self):
+ """Return if sensor is available."""
+ if self._coordinator is not None:
+ return self._coordinator.last_update_success
+ return True
+
+ @property
+ def should_poll(self):
+ """Return the polling state."""
+ return False
+
+ async def async_added_to_hass(self):
+ """When entity is added to hass."""
+ if self._coordinator is not None:
+ self.async_on_remove(
+ self._coordinator.async_add_listener(self.async_write_ha_state)
+ )
+ else:
+ self.async_on_remove(
+ self.hass.helpers.dispatcher.async_dispatcher_connect(
+ SENSOR_SIGNAL % (self._device_id, self._sensor_type),
+ self.async_write_ha_state,
+ )
+ )
+
+ @staticmethod
+ def _to_snake_case(dictionary: dict):
+ return {k.lower().replace(" ", "_"): v for k, v in dictionary.items()}
diff --git a/homeassistant/components/plaato/manifest.json b/homeassistant/components/plaato/manifest.json
index 29e104b13ed394..e3291e5a229d24 100644
--- a/homeassistant/components/plaato/manifest.json
+++ b/homeassistant/components/plaato/manifest.json
@@ -1,8 +1,10 @@
{
"domain": "plaato",
- "name": "Plaato Airlock",
+ "name": "Plaato",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/plaato",
"dependencies": ["webhook"],
- "codeowners": ["@JohNan"]
+ "after_dependencies": ["cloud"],
+ "codeowners": ["@JohNan"],
+ "requirements": ["pyplaato==0.0.15"]
}
diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py
index 3f8034698fd843..9af16a1cacd08d 100644
--- a/homeassistant/components/plaato/sensor.py
+++ b/homeassistant/components/plaato/sensor.py
@@ -1,164 +1,85 @@
"""Support for Plaato Airlock sensors."""
+from __future__ import annotations
-import logging
+from pyplaato.models.device import PlaatoDevice
+from pyplaato.plaato import PlaatoKeg
-from homeassistant.const import PERCENTAGE
+from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE, SensorEntity
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
-from homeassistant.helpers.entity import Entity
-from . import (
- ATTR_ABV,
- ATTR_BATCH_VOLUME,
- ATTR_BPM,
- ATTR_CO2_VOLUME,
- ATTR_TEMP,
- ATTR_TEMP_UNIT,
- ATTR_VOLUME_UNIT,
- DOMAIN as PLAATO_DOMAIN,
- PLAATO_DEVICE_ATTRS,
- PLAATO_DEVICE_SENSORS,
- SENSOR_DATA_KEY,
- SENSOR_UPDATE,
+from . import ATTR_TEMP, SENSOR_UPDATE
+from ...core import callback
+from .const import (
+ CONF_USE_WEBHOOK,
+ COORDINATOR,
+ DEVICE,
+ DEVICE_ID,
+ DOMAIN,
+ SENSOR_DATA,
+ SENSOR_SIGNAL,
)
-
-_LOGGER = logging.getLogger(__name__)
+from .entity import PlaatoEntity
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Plaato sensor."""
-async def async_setup_entry(hass, config_entry, async_add_entities):
+async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Plaato from a config entry."""
- devices = {}
-
- def get_device(device_id):
- """Get a device."""
- return hass.data[PLAATO_DOMAIN].get(device_id, False)
+ entry_data = hass.data[DOMAIN][entry.entry_id]
- def get_device_sensors(device_id):
- """Get device sensors."""
- return hass.data[PLAATO_DOMAIN].get(device_id).get(PLAATO_DEVICE_SENSORS)
-
- async def _update_sensor(device_id):
+ @callback
+ async def _async_update_from_webhook(device_id, sensor_data: PlaatoDevice):
"""Update/Create the sensors."""
- if device_id not in devices and get_device(device_id):
- entities = []
- sensors = get_device_sensors(device_id)
-
- for sensor_type in sensors:
- entities.append(PlaatoSensor(device_id, sensor_type))
-
- devices[device_id] = entities
-
- async_add_entities(entities, True)
+ entry_data[SENSOR_DATA] = sensor_data
+
+ if device_id != entry_data[DEVICE][DEVICE_ID]:
+ entry_data[DEVICE][DEVICE_ID] = device_id
+ async_add_entities(
+ [
+ PlaatoSensor(entry_data, sensor_type)
+ for sensor_type in sensor_data.sensors
+ ]
+ )
else:
- for entity in devices[device_id]:
- async_dispatcher_send(hass, f"{PLAATO_DOMAIN}_{entity.unique_id}")
-
- hass.data[SENSOR_DATA_KEY] = async_dispatcher_connect(
- hass, SENSOR_UPDATE, _update_sensor
- )
-
- return True
-
-
-class PlaatoSensor(Entity):
- """Representation of a Sensor."""
-
- def __init__(self, device_id, sensor_type):
- """Initialize the sensor."""
- self._device_id = device_id
- self._type = sensor_type
- self._state = 0
- self._name = f"{device_id} {sensor_type}"
- self._attributes = None
+ for sensor_type in sensor_data.sensors:
+ async_dispatcher_send(hass, SENSOR_SIGNAL % (device_id, sensor_type))
+
+ if entry.data[CONF_USE_WEBHOOK]:
+ async_dispatcher_connect(hass, SENSOR_UPDATE, _async_update_from_webhook)
+ else:
+ coordinator = entry_data[COORDINATOR]
+ async_add_entities(
+ PlaatoSensor(entry_data, sensor_type, coordinator)
+ for sensor_type in coordinator.data.sensors
+ )
- @property
- def name(self):
- """Return the name of the sensor."""
- return f"{PLAATO_DOMAIN} {self._name}"
- @property
- def unique_id(self):
- """Return the unique ID of this sensor."""
- return f"{self._device_id}_{self._type}"
+class PlaatoSensor(PlaatoEntity, SensorEntity):
+ """Representation of a Plaato Sensor."""
@property
- def device_info(self):
- """Get device info."""
- return {
- "identifiers": {(PLAATO_DOMAIN, self._device_id)},
- "name": self._device_id,
- "manufacturer": "Plaato",
- "model": "Airlock",
- }
-
- def get_sensors(self):
- """Get device sensors."""
- return (
- self.hass.data[PLAATO_DOMAIN]
- .get(self._device_id)
- .get(PLAATO_DEVICE_SENSORS, False)
- )
-
- def get_sensors_unit_of_measurement(self, sensor_type):
- """Get unit of measurement for sensor of type."""
- return (
- self.hass.data[PLAATO_DOMAIN]
- .get(self._device_id)
- .get(PLAATO_DEVICE_ATTRS, [])
- .get(sensor_type, "")
- )
+ def device_class(self) -> str | None:
+ """Return the class of this device, from component DEVICE_CLASSES."""
+ if (
+ self._coordinator is not None
+ and self._sensor_type == PlaatoKeg.Pins.TEMPERATURE
+ ):
+ return DEVICE_CLASS_TEMPERATURE
+ if self._sensor_type == ATTR_TEMP:
+ return DEVICE_CLASS_TEMPERATURE
+ return None
@property
def state(self):
"""Return the state of the sensor."""
- sensors = self.get_sensors()
- if sensors is False:
- _LOGGER.debug("Device with name %s has no sensors", self.name)
- return 0
-
- if self._type == ATTR_ABV:
- return round(sensors.get(self._type), 2)
- if self._type == ATTR_TEMP:
- return round(sensors.get(self._type), 1)
- if self._type == ATTR_CO2_VOLUME:
- return round(sensors.get(self._type), 2)
- return sensors.get(self._type)
-
- @property
- def device_state_attributes(self):
- """Return the state attributes of the monitored installation."""
- if self._attributes is not None:
- return self._attributes
+ return self._sensor_data.sensors.get(self._sensor_type)
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
- if self._type == ATTR_TEMP:
- return self.get_sensors_unit_of_measurement(ATTR_TEMP_UNIT)
- if self._type == ATTR_BATCH_VOLUME or self._type == ATTR_CO2_VOLUME:
- return self.get_sensors_unit_of_measurement(ATTR_VOLUME_UNIT)
- if self._type == ATTR_BPM:
- return "bpm"
- if self._type == ATTR_ABV:
- return PERCENTAGE
-
- return ""
-
- @property
- def should_poll(self):
- """Return the polling state."""
- return False
-
- async def async_added_to_hass(self):
- """Register callbacks."""
- self.async_on_remove(
- self.hass.helpers.dispatcher.async_dispatcher_connect(
- f"{PLAATO_DOMAIN}_{self.unique_id}", self.async_write_ha_state
- )
- )
+ return self._sensor_data.get_unit_of_measurement(self._sensor_type)
diff --git a/homeassistant/components/plaato/strings.json b/homeassistant/components/plaato/strings.json
index 087cee136833f2..85bc39a8d83fcd 100644
--- a/homeassistant/components/plaato/strings.json
+++ b/homeassistant/components/plaato/strings.json
@@ -2,16 +2,53 @@
"config": {
"step": {
"user": {
- "title": "Set up the Plaato Webhook",
- "description": "[%key:common::config_flow::description::confirm_setup%]"
+ "title": "Set up the Plaato devices",
+ "description": "[%key:common::config_flow::description::confirm_setup%]",
+ "data": {
+ "device_name": "Name your device",
+ "device_type": "Type of Plaato device"
+ }
+ },
+ "api_method": {
+ "title": "Select API method",
+ "description": "To be able to query the API an `auth_token` is required which can be obtained by following [these](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instructions\n\n Selected device: **{device_type}** \n\nIf you rather use the built in webhook method (Airlock only) please check the box below and leave Auth Token blank",
+ "data": {
+ "use_webhook": "Use webhook",
+ "token": "Paste Auth Token here"
+ }
+ },
+ "webhook": {
+ "title": "Webhook to use",
+ "description": "To send events to Home Assistant, you will need to setup the webhook feature in Plaato Airlock.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details."
}
},
+ "error": {
+ "invalid_webhook_device": "You have selected a device that does not support sending data to a webhook. It is only available for the Airlock",
+ "no_auth_token": "You need to add an auth token",
+ "no_api_method": "You need to add an auth token or select webhook"
+ },
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
- "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]"
+ "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"create_entry": {
- "default": "To send events to Home Assistant, you will need to setup the webhook feature in Plaato Airlock.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details."
+ "default": "Your Plaato {device_type} with name **{device_name}** was successfully setup!"
+ }
+ },
+ "options": {
+ "step": {
+ "webhook": {
+ "title": "Options for Plaato Airlock",
+ "description": "Webhook info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n"
+ },
+ "user": {
+ "title": "Options for Plaato",
+ "description": "Set the update interval (minutes)",
+ "data": {
+ "update_interval": "Update interval (minutes)"
+ }
+ }
}
}
}
diff --git a/homeassistant/components/plaato/translations/ca.json b/homeassistant/components/plaato/translations/ca.json
index 1dbe125d50d376..c4669b219ab760 100644
--- a/homeassistant/components/plaato/translations/ca.json
+++ b/homeassistant/components/plaato/translations/ca.json
@@ -1,16 +1,53 @@
{
"config": {
"abort": {
+ "already_configured": "El compte ja ha estat configurat",
"single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.",
"webhook_not_internet_accessible": "La teva inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per poder rebre missatges webhook."
},
"create_entry": {
- "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar l'opci\u00f3 webhook de Plaato Airlock.\n\nCompleta la seg\u00fcent informaci\u00f3:\n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls."
+ "default": "El dispositiu Plaato {device_type} amb nom **{device_name}** s'ha configurat correctament!"
+ },
+ "error": {
+ "invalid_webhook_device": "Has seleccionat un dispositiu que no admet l'enviament de dades a un webhook. Nom\u00e9s est\u00e0 disponible per a Airlock",
+ "no_api_method": "Has d'afegir un token d'autenticaci\u00f3 o seleccionar webhook",
+ "no_auth_token": "Has d'afegir un token d'autenticaci\u00f3"
},
"step": {
+ "api_method": {
+ "data": {
+ "token": "Enganxa el token d'autenticaci\u00f3 aqu\u00ed",
+ "use_webhook": "Utilitza webhook"
+ },
+ "description": "Per poder consultar l'API, cal un `auth_token` que es pot obtenir seguint aquestes [instruccions](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token)\n\n Dispositiu seleccionat: **{device_type}** \n\n Si prefereixes utilitzar el m\u00e8tode webhook integrat (nom\u00e9s per Airlock), marca la casella seg\u00fcent i deixa el token d'autenticaci\u00f3 en blanc",
+ "title": "Selecciona el m\u00e8tode API"
+ },
"user": {
+ "data": {
+ "device_name": "Posa un nom al dispositiu",
+ "device_type": "Tipus de dispositiu Plaato"
+ },
"description": "Vols comen\u00e7ar la configuraci\u00f3?",
- "title": "Configuraci\u00f3 del Webhook de Plaato"
+ "title": "Configura dispositius Plaato"
+ },
+ "webhook": {
+ "description": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar la opci\u00f3 webhook de Plaato Airlock.\n\nCompleta la seg\u00fcent informaci\u00f3:\n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls.",
+ "title": "Webhook a utilitzar"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "update_interval": "Interval d'actualitzaci\u00f3 (minuts)"
+ },
+ "description": "Estableix l'interval d'actualitzaci\u00f3 (minuts)",
+ "title": "Opcions de Plaato"
+ },
+ "webhook": {
+ "description": "Informaci\u00f3 del webhook: \n\n - URL: `{webhook_url}`\n - M\u00e8tode: POST\n\n",
+ "title": "Opcions de Plaato Airlock"
}
}
}
diff --git a/homeassistant/components/plaato/translations/cs.json b/homeassistant/components/plaato/translations/cs.json
index 582a3e3a18086b..4d736d1c695a4a 100644
--- a/homeassistant/components/plaato/translations/cs.json
+++ b/homeassistant/components/plaato/translations/cs.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "already_configured": "\u00da\u010det je ji\u017e nastaven",
"single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.",
"webhook_not_internet_accessible": "V\u00e1\u0161 Home Assistant mus\u00ed b\u00fdt p\u0159\u00edstupn\u00fd z internetu, aby mohl p\u0159ij\u00edmat zpr\u00e1vy webhook."
},
diff --git a/homeassistant/components/plaato/translations/de.json b/homeassistant/components/plaato/translations/de.json
index f97fe4875f7835..9a092ef4fa6230 100644
--- a/homeassistant/components/plaato/translations/de.json
+++ b/homeassistant/components/plaato/translations/de.json
@@ -1,12 +1,50 @@
{
"config": {
+ "abort": {
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.",
+ "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen."
+ },
"create_entry": {
"default": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in Plaato Airlock konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})."
},
+ "error": {
+ "invalid_webhook_device": "Du hast ein Ger\u00e4t gew\u00e4hlt, das das Senden von Daten an einen Webhook nicht unterst\u00fctzt. Es ist nur f\u00fcr die Airlock verf\u00fcgbar",
+ "no_api_method": "Du musst ein Authentifizierungstoken hinzuf\u00fcgen oder ein Webhook ausw\u00e4hlen",
+ "no_auth_token": "Du musst ein Authentifizierungstoken hinzuf\u00fcgen"
+ },
"step": {
+ "api_method": {
+ "data": {
+ "use_webhook": "Webhook verwenden"
+ },
+ "title": "API-Methode ausw\u00e4hlen"
+ },
"user": {
- "description": "Soll Plaato Airlock wirklich eingerichtet werden?",
+ "data": {
+ "device_name": "Benenne dein Ger\u00e4t",
+ "device_type": "Art des Plaato-Ger\u00e4ts"
+ },
+ "description": "M\u00f6chten Sie mit der Einrichtung beginnen?",
"title": "Plaato Webhook einrichten"
+ },
+ "webhook": {
+ "description": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in Plaato Airlock konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "update_interval": "Aktualisierungsintervall (Minuten)"
+ },
+ "description": "Aktualisierungsintervall einrichten (Minuten)",
+ "title": "Optionen f\u00fcr Plaato"
+ },
+ "webhook": {
+ "description": "Webhook-Informationen:\n\n- URL: `{webhook_url}`\n- Methode: POST\n\n",
+ "title": "Optionen f\u00fcr Plaato Airlock"
}
}
}
diff --git a/homeassistant/components/plaato/translations/en.json b/homeassistant/components/plaato/translations/en.json
index 6f25c15583c2b8..1217eb53d6eac8 100644
--- a/homeassistant/components/plaato/translations/en.json
+++ b/homeassistant/components/plaato/translations/en.json
@@ -1,16 +1,53 @@
{
"config": {
"abort": {
+ "already_configured": "Account is already configured",
"single_instance_allowed": "Already configured. Only a single configuration possible.",
"webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages."
},
"create_entry": {
- "default": "To send events to Home Assistant, you will need to setup the webhook feature in Plaato Airlock.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details."
+ "default": "Your Plaato {device_type} with name **{device_name}** was successfully setup!"
+ },
+ "error": {
+ "invalid_webhook_device": "You have selected a device that does not support sending data to a webhook. It is only available for the Airlock",
+ "no_api_method": "You need to add an auth token or select webhook",
+ "no_auth_token": "You need to add an auth token"
},
"step": {
+ "api_method": {
+ "data": {
+ "token": "Paste Auth Token here",
+ "use_webhook": "Use webhook"
+ },
+ "description": "To be able to query the API an `auth_token` is required which can be obtained by following [these](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instructions\n\n Selected device: **{device_type}** \n\nIf you rather use the built in webhook method (Airlock only) please check the box below and leave Auth Token blank",
+ "title": "Select API method"
+ },
"user": {
+ "data": {
+ "device_name": "Name your device",
+ "device_type": "Type of Plaato device"
+ },
"description": "Do you want to start set up?",
- "title": "Set up the Plaato Webhook"
+ "title": "Set up the Plaato devices"
+ },
+ "webhook": {
+ "description": "To send events to Home Assistant, you will need to setup the webhook feature in Plaato Airlock.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details.",
+ "title": "Webhook to use"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "update_interval": "Update interval (minutes)"
+ },
+ "description": "Set the update interval (minutes)",
+ "title": "Options for Plaato"
+ },
+ "webhook": {
+ "description": "Webhook info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n",
+ "title": "Options for Plaato Airlock"
}
}
}
diff --git a/homeassistant/components/plaato/translations/es.json b/homeassistant/components/plaato/translations/es.json
index 0f030e56b4eb0c..e0b6c767043acf 100644
--- a/homeassistant/components/plaato/translations/es.json
+++ b/homeassistant/components/plaato/translations/es.json
@@ -1,16 +1,53 @@
{
"config": {
"abort": {
+ "already_configured": "La cuenta ya ha sido configurada",
"single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.",
"webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook."
},
"create_entry": {
"default": "Para enviar eventos a Home Assistant, necesitar\u00e1s configurar la funci\u00f3n de webhook en Plaato Airlock.\n\nCompleta la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST\n\nEcha un vistazo a [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles."
},
+ "error": {
+ "invalid_webhook_device": "Has seleccionado un dispositivo que no admite el env\u00edo de datos a un webhook. Solo est\u00e1 disponible para Airlock",
+ "no_api_method": "Necesitas a\u00f1adir un token de autenticaci\u00f3n o seleccionar un webhook",
+ "no_auth_token": "Es necesario a\u00f1adir un token de autenticaci\u00f3n"
+ },
"step": {
+ "api_method": {
+ "data": {
+ "token": "Pega el token de autenticaci\u00f3n aqu\u00ed",
+ "use_webhook": "Usar webhook"
+ },
+ "description": "Para poder consultar la API se necesita un `auth_token` que puede obtenerse siguiendo [estas](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instrucciones\n\n Dispositivo seleccionado: **{device_type}** \n\nSi prefiere utilizar el m\u00e9todo de webhook incorporado (s\u00f3lo Airlock), marque la casilla siguiente y deje en blanco el Auth Token",
+ "title": "Selecciona el m\u00e9todo API"
+ },
"user": {
+ "data": {
+ "device_name": "Nombre de su dispositivo",
+ "device_type": "Tipo de dispositivo Plaato"
+ },
"description": "\u00bfEst\u00e1s seguro de que quieres configurar el Airlock de Plaato?",
"title": "Configurar el webhook de Plaato"
+ },
+ "webhook": {
+ "description": "Para enviar eventos a Home Assistant, deber\u00e1 configurar la funci\u00f3n de webhook en Plaato Airlock. \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n Consulte [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s detalles.",
+ "title": "Webhook a utilizar"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "update_interval": "Intervalo de actualizaci\u00f3n (minutos)"
+ },
+ "description": "Intervalo de actualizaci\u00f3n (minutos)",
+ "title": "Opciones de Plaato"
+ },
+ "webhook": {
+ "description": "Informaci\u00f3n de webhook: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n",
+ "title": "Opciones para Plaato Airlock"
}
}
}
diff --git a/homeassistant/components/plaato/translations/et.json b/homeassistant/components/plaato/translations/et.json
index 75c7a2182ef739..fb4325581dd37e 100644
--- a/homeassistant/components/plaato/translations/et.json
+++ b/homeassistant/components/plaato/translations/et.json
@@ -1,16 +1,53 @@
{
"config": {
"abort": {
+ "already_configured": "Kasutaja on juba seadistatud",
"single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.",
"webhook_not_internet_accessible": "Veebikonksu s\u00f5numite vastuv\u00f5tmiseks peab Home Assistant olema Interneti kaudu juurdep\u00e4\u00e4setav."
},
"create_entry": {
- "default": "S\u00fcndmuste saatmiseks Home Assistantile pead seadistama Plaatoo Airlock'i veebihaagi. \n\n Sisesta j\u00e4rgmine teave: \n\n - URL: \" {webhook_url} \" \n - Meetod: POST \n \n Lisateavet leiad [documentation] ( {docs_url} )."
+ "default": "{device_type} Plaato seade nimega **{device_name}** on edukalt seadistatud!"
+ },
+ "error": {
+ "invalid_webhook_device": "Oled valinud seadme mis ei toeta andmete saatmist veebihaagile. See on saadaval ainult Airlocki jaoks",
+ "no_api_method": "Pead lisama autentimisloa v\u00f5i valima veebihaagi",
+ "no_auth_token": "Pead lisama autentimisloa"
},
"step": {
+ "api_method": {
+ "data": {
+ "token": "Aseta Auth Token siia",
+ "use_webhook": "Kasuta veebihaaki"
+ },
+ "description": "API p\u00e4ringu esitamiseks on vajalik \"auth_token\", mille saad j\u00e4rgides [neid] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) juhiseid \n\n Valitud seade: ** {device_type} ** \n\n Kui kasutad pigem sisseehitatud veebihaagi meetodit (ainult Airlock), m\u00e4rgi palun allolev ruut ja j\u00e4ta Auth Token t\u00fchjaks",
+ "title": "Vali API meetod"
+ },
"user": {
+ "data": {
+ "device_name": "Pane oma seadmele nimi",
+ "device_type": "Plaato seadme t\u00fc\u00fcp"
+ },
"description": "Kas alustan seadistamist?",
- "title": "Plaato Webhooki seadistamine"
+ "title": "Plaato seadmete h\u00e4\u00e4lestamine"
+ },
+ "webhook": {
+ "description": "S\u00fcndmuste saatmiseks Home Assistanti pead seadistama Plaato Airlocki veebihaagi. \n\n Sisesta j\u00e4rgmine teave: \n\n - URL: \" {webhook_url} \"\n - Meetod: POST \n\n Lisateavet leiad [dokumentatsioonist] ( {docs_url} ).",
+ "title": "Kasutatav veebihaak"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "update_interval": "V\u00e4rskendamise intervall (minutites)"
+ },
+ "description": "M\u00e4\u00e4ra v\u00e4rskendamise intervall (minutites)",
+ "title": "Plaato valikud"
+ },
+ "webhook": {
+ "description": "Veebihaagi teave: \n\n - URL: `{webhook_url}`\n - Meetod: POST\n\n",
+ "title": "Plaato Airlocki valikud"
}
}
}
diff --git a/homeassistant/components/plaato/translations/fr.json b/homeassistant/components/plaato/translations/fr.json
index bc442a04c60188..ab3c01144ddf3d 100644
--- a/homeassistant/components/plaato/translations/fr.json
+++ b/homeassistant/components/plaato/translations/fr.json
@@ -1,16 +1,53 @@
{
"config": {
"abort": {
+ "already_configured": "Le compte est d\u00e9ja configur\u00e9",
"single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.",
"webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook."
},
"create_entry": {
"default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer la fonction Webhook dans Plaato Airlock. \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n\n Voir [la documentation] ( {docs_url} ) pour plus de d\u00e9tails."
},
+ "error": {
+ "invalid_webhook_device": "Vous avez s\u00e9lectionn\u00e9 un appareil qui ne prend pas en charge l'envoi de donn\u00e9es vers un webhook. Il n'est disponible que pour le sas",
+ "no_api_method": "Vous devez ajouter un jeton d'authentification ou s\u00e9lectionner un webhook",
+ "no_auth_token": "Vous devez ajouter un jeton d'authentification"
+ },
"step": {
+ "api_method": {
+ "data": {
+ "token": "Collez le jeton d'authentification ici",
+ "use_webhook": "Utiliser le webhook"
+ },
+ "description": "Pour pouvoir interroger l'API, un \u00abauth_token\u00bb est n\u00e9cessaire. Il peut \u00eatre obtenu en suivant [ces] instructions (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) \n\n Appareil s\u00e9lectionn\u00e9: ** {device_type} ** \n\n Si vous pr\u00e9f\u00e9rez utiliser la m\u00e9thode Webhook int\u00e9gr\u00e9e (Airlock uniquement), veuillez cocher la case ci-dessous et laisser le jeton d'authentification vide",
+ "title": "S\u00e9lectionnez la m\u00e9thode API"
+ },
"user": {
+ "data": {
+ "device_name": "Nommez votre appareil",
+ "device_type": "Type d'appareil Plaato"
+ },
"description": "\u00cates-vous s\u00fbr de vouloir installer le Plaato Airlock ?",
"title": "Configurer le Webhook Plaato"
+ },
+ "webhook": {
+ "description": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer la fonction Webhook dans Plaato Airlock. \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n\n Voir [la documentation] ( {docs_url} ) pour plus de d\u00e9tails.",
+ "title": "Webhook \u00e0 utiliser"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "update_interval": "Intervalle de mise \u00e0 jour (minutes)"
+ },
+ "description": "D\u00e9finir l'intervalle de mise \u00e0 jour (minutes)",
+ "title": "Options pour Plaato"
+ },
+ "webhook": {
+ "description": "Informations sur le webhook: \n\n - URL: ` {webhook_url} `\n - M\u00e9thode: POST \n\n",
+ "title": "Options pour Plaato Airlock"
}
}
}
diff --git a/homeassistant/components/plaato/translations/hu.json b/homeassistant/components/plaato/translations/hu.json
index 76229e86224903..8347b5d2f98ce8 100644
--- a/homeassistant/components/plaato/translations/hu.json
+++ b/homeassistant/components/plaato/translations/hu.json
@@ -1,7 +1,22 @@
{
"config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.",
+ "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz."
+ },
"create_entry": {
- "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Plaato Airlock-ban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} )."
+ "default": "A Plaato {device_type} **{device_name}** n\u00e9vvel sikeresen telep\u00edtve lett!"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "device_name": "Eszk\u00f6z neve",
+ "device_type": "A Plaato eszk\u00f6z t\u00edpusa"
+ },
+ "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?",
+ "title": "A Plaato eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/plaato/translations/id.json b/homeassistant/components/plaato/translations/id.json
new file mode 100644
index 00000000000000..989bb38bcaf0f5
--- /dev/null
+++ b/homeassistant/components/plaato/translations/id.json
@@ -0,0 +1,54 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.",
+ "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook."
+ },
+ "create_entry": {
+ "default": "Plaato {device_type} dengan nama **{device_name}** berhasil disiapkan!"
+ },
+ "error": {
+ "invalid_webhook_device": "Anda telah memilih perangkat yang tidak mendukung pengiriman data ke webhook. Ini hanya tersedia untuk Airlock",
+ "no_api_method": "Anda perlu menambahkan token auth atau memilih webhook",
+ "no_auth_token": "Anda perlu menambahkan token autentikasi"
+ },
+ "step": {
+ "api_method": {
+ "data": {
+ "token": "Tempel Token Auth di sini",
+ "use_webhook": "Gunakan webhook"
+ },
+ "description": "Untuk dapat melakukan kueri API diperlukan 'auth_token'. Nilai token dapat diperoleh dengan mengikuti [petunjuk ini](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token)\n\nPerangkat yang dipilih: **{device_type}** \n\nJika Anda lebih memilih untuk menggunakan metode webhook bawaan (hanya Airlock), centang centang kotak di bawah ini dan kosongkan nilai Auth Token'",
+ "title": "Pilih metode API"
+ },
+ "user": {
+ "data": {
+ "device_name": "Nama perangkat Anda",
+ "device_type": "Jenis perangkat Plaato"
+ },
+ "description": "Ingin memulai penyiapan?",
+ "title": "Siapkan perangkat Plaato"
+ },
+ "webhook": {
+ "description": "Untuk mengirim event ke Home Assistant, Anda harus menyiapkan fitur webhook di Plaato Airlock.\n\nIsi info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nBaca [dokumentasi]({docs_url}) tentang detail lebih lanjut.",
+ "title": "Webhook untuk digunakan"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "update_interval": "Interval pembaruan (menit)"
+ },
+ "description": "Atur interval pembaruan (menit)",
+ "title": "Opsi untuk Plaato"
+ },
+ "webhook": {
+ "description": "Info webhook:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n",
+ "title": "Opsi untuk Plaato Airlock"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plaato/translations/it.json b/homeassistant/components/plaato/translations/it.json
index ad289fa758f4b5..acd2fcfa3f4185 100644
--- a/homeassistant/components/plaato/translations/it.json
+++ b/homeassistant/components/plaato/translations/it.json
@@ -1,16 +1,53 @@
{
"config": {
"abort": {
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato",
"single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.",
"webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook."
},
"create_entry": {
- "default": "Per inviare eventi a Home Assistant, dovrai impostare la funzione webhook in Plaato Airlock. \n\n Inserisci le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Metodo: POST \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli."
+ "default": "Il tuo Plaato {device_type} con nome **{device_name}** \u00e8 stato configurato con successo!"
+ },
+ "error": {
+ "invalid_webhook_device": "Hai selezionato un dispositivo che non supporta l'invio di dati a un webhook. \u00c8 disponibile solo per Airlock",
+ "no_api_method": "Devi aggiungere un token di autenticazione o selezionare webhook",
+ "no_auth_token": "\u00c8 necessario aggiungere un token di autorizzazione"
},
"step": {
+ "api_method": {
+ "data": {
+ "token": "Incolla il token di autenticazione qui",
+ "use_webhook": "Usa webhook"
+ },
+ "description": "Per poter interrogare l'API \u00e8 necessario un `auth_token` che pu\u00f2 essere ottenuto seguendo [queste] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) istruzioni \n\n Dispositivo selezionato: **{device_type}** \n\n Se preferisci utilizzare il metodo webhook integrato (solo Airlock), seleziona la casella sottostante e lascia vuoto il token di autenticazione",
+ "title": "Seleziona il metodo API"
+ },
"user": {
+ "data": {
+ "device_name": "Assegna un nome al dispositivo",
+ "device_type": "Tipo di dispositivo Plaato"
+ },
"description": "Vuoi iniziare la configurazione?",
- "title": "Configura il webhook di Plaato"
+ "title": "Imposta i dispositivi Plaato"
+ },
+ "webhook": {
+ "description": "Per inviare eventi a Home Assistant, dovrai configurare la funzione webhook in Plaato Airlock. \n\n Compila le seguenti informazioni: \n\n - URL: \"{webhook_url}\"\n - Metodo: POST \n\n Vedere [la documentazione] ({docs_url}) per ulteriori dettagli.",
+ "title": "Webhook da utilizzare"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "update_interval": "Intervallo di aggiornamento (minuti)"
+ },
+ "description": "Imposta l'intervallo di aggiornamento (minuti)",
+ "title": "Opzioni per Plaato"
+ },
+ "webhook": {
+ "description": "Informazioni webhook:\n\n- URL: \"{webhook_url}\"\n- Metodo: POST\n\n",
+ "title": "Opzioni per Plaato Airlock"
}
}
}
diff --git a/homeassistant/components/plaato/translations/ko.json b/homeassistant/components/plaato/translations/ko.json
index 6eeb6a9c06150d..fb75bbb7d7cbcc 100644
--- a/homeassistant/components/plaato/translations/ko.json
+++ b/homeassistant/components/plaato/translations/ko.json
@@ -1,12 +1,53 @@
{
"config": {
+ "abort": {
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4."
+ },
"create_entry": {
- "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Plaato Airlock \uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4.\n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
+ "default": "**{device_name}**\uc758 Plaato {device_type}\uc774(\uac00) \uc131\uacf5\uc801\uc73c\ub85c \uc124\uc815\ub418\uc5c8\uc2b5\ub2c8\ub2e4!"
+ },
+ "error": {
+ "invalid_webhook_device": "\uc6f9 \ud6c5\uc73c\ub85c \ub370\uc774\ud130 \uc804\uc1a1\uc744 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 \uae30\uae30\ub97c \uc120\ud0dd\ud588\uc2b5\ub2c8\ub2e4. Airlock\uc5d0\uc11c\ub9cc \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4",
+ "no_api_method": "\uc778\uc99d \ud1a0\ud070\uc744 \ucd94\uac00\ud558\uac70\ub098 \uc6f9 \ud6c5\uc744 \uc120\ud0dd\ud574\uc57c \ud569\ub2c8\ub2e4",
+ "no_auth_token": "\uc778\uc99d \ud1a0\ud070\uc744 \ucd94\uac00\ud574\uc57c \ud569\ub2c8\ub2e4"
},
+ "step": {
+ "api_method": {
+ "data": {
+ "token": "\uc5ec\uae30\uc5d0 \uc778\uc99d \ud1a0\ud070\uc744 \ubd99\uc5ec \ub123\uc5b4\uc8fc\uc138\uc694",
+ "use_webhook": "\uc6f9 \ud6c5 \uc0ac\uc6a9\ud558\uae30"
+ },
+ "description": "API\ub97c \ucffc\ub9ac \ud558\ub824\uba74 [\uc548\ub0b4 \uc9c0\uce68](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token)\uc5d0 \ub530\ub77c \uc5bb\uc744 \uc218 \uc788\ub294 'auth_token'\uc774 \ud544\uc694\ud569\ub2c8\ub2e4\n\n\uc120\ud0dd\ud55c \uae30\uae30: **{device_type}**\n\n\ub0b4\uc7a5\ub41c \uc6f9 \ud6c5 \ubc29\uc2dd(Airlock \uc804\uc6a9)\uc744 \uc0ac\uc6a9\ud558\ub824\ub294 \uacbd\uc6b0 \uc544\ub798 \ud655\uc778\ub780\uc744 \uc120\ud0dd\ud558\uace0 Auth Token\uc744 \ube44\uc6cc\ub450\uc138\uc694",
+ "title": "API \ubc29\uc2dd \uc120\ud0dd\ud558\uae30"
+ },
+ "user": {
+ "data": {
+ "device_name": "\uae30\uae30 \uc774\ub984",
+ "device_type": "Plaato \uae30\uae30 \uc720\ud615"
+ },
+ "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "Plaato \uae30\uae30 \uc124\uc815\ud558\uae30"
+ },
+ "webhook": {
+ "description": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Plaato Airlock\uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4.\n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
+ "title": "\uc0ac\uc6a9\ud560 \uc6f9 \ud6c5"
+ }
+ }
+ },
+ "options": {
"step": {
"user": {
- "description": "Plaato Airlock \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
- "title": "Plaato \uc6f9 \ud6c5 \uc124\uc815\ud558\uae30"
+ "data": {
+ "update_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 (\ubd84)"
+ },
+ "description": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 \uc124\uc815\ud558\uae30 (\ubd84)",
+ "title": "Plaato \uc635\uc158"
+ },
+ "webhook": {
+ "description": "\uc6f9 \ud6c5 \uc815\ubcf4:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n",
+ "title": "Plaato Airlock \uc635\uc158"
}
}
}
diff --git a/homeassistant/components/plaato/translations/nl.json b/homeassistant/components/plaato/translations/nl.json
index 6545a659427805..d50763d0e1acaf 100644
--- a/homeassistant/components/plaato/translations/nl.json
+++ b/homeassistant/components/plaato/translations/nl.json
@@ -1,15 +1,53 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk."
+ "already_configured": "Account is al geconfigureerd",
+ "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.",
+ "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen."
},
"create_entry": {
- "default": "Om evenementen naar de Home Assistant te sturen, moet u de webhook-functie instellen in Plaato Airlock. \n\n Vul de volgende info in: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n\n Zie [de documentatie] ( {docs_url} ) voor meer informatie."
+ "default": "Uw Plaato {device_type} met naam **{device_name}** is succesvol ingesteld!"
},
+ "error": {
+ "invalid_webhook_device": "U heeft een apparaat geselecteerd dat het verzenden van gegevens naar een webhook niet ondersteunt. Het is alleen beschikbaar voor de Airlock",
+ "no_api_method": "U moet een verificatie token toevoegen of selecteer een webhook",
+ "no_auth_token": "U moet een verificatie token toevoegen"
+ },
+ "step": {
+ "api_method": {
+ "data": {
+ "token": "Plak hier de verificatie-token",
+ "use_webhook": "Webhook gebruiken"
+ },
+ "description": "Om de API te kunnenopvragen is een `auth_token` nodig, die kan worden verkregen door [deze] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instructies te volgen\n\n Geselecteerd apparaat: **{device_type}** \n\nIndien u liever de ingebouwde webhook methode gebruikt (alleen Airlock) vink dan het vakje hieronder aan en laat Auth Token leeg",
+ "title": "Selecteer API-methode"
+ },
+ "user": {
+ "data": {
+ "device_name": "Geef uw apparaat een naam",
+ "device_type": "Type Plaato-apparaat"
+ },
+ "description": "Wil je beginnen met instellen?",
+ "title": "Stel de Plaato-apparaten in"
+ },
+ "webhook": {
+ "description": "Om evenementen naar de Home Assistant te sturen, moet u de webhook-functie instellen in Plaato Airlock. \n\n Vul de volgende info in: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n\n Zie [de documentatie] ({docs_url}) voor meer informatie.",
+ "title": "Webhook om te gebruiken"
+ }
+ }
+ },
+ "options": {
"step": {
"user": {
- "description": "Weet u zeker dat u de Plaato-airlock wilt instellen?",
- "title": "Stel de Plaato Webhook in"
+ "data": {
+ "update_interval": "Update-interval (minuten)"
+ },
+ "description": "Stel het update-interval in (minuten)",
+ "title": "Opties voor Plaato"
+ },
+ "webhook": {
+ "description": "Webhook-informatie: \n\n - URL: ' {webhook_url} '\n - Methode: POST\n\n",
+ "title": "Opties voor Plaato Airlock"
}
}
}
diff --git a/homeassistant/components/plaato/translations/no.json b/homeassistant/components/plaato/translations/no.json
index 1e2da1bfb12661..7039662468ef2b 100644
--- a/homeassistant/components/plaato/translations/no.json
+++ b/homeassistant/components/plaato/translations/no.json
@@ -1,16 +1,53 @@
{
"config": {
"abort": {
+ "already_configured": "Kontoen er allerede konfigurert",
"single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.",
"webhook_not_internet_accessible": "Home Assistant forekomsten din m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta webhook meldinger"
},
"create_entry": {
- "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Plaato Airlock. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer."
+ "default": "Plaato {device_type} med navnet **{device_name}** ble konfigurert!"
+ },
+ "error": {
+ "invalid_webhook_device": "Du har valgt en enhet som ikke st\u00f8tter sending av data til en webhook. Den er bare tilgjengelig for luftsluse",
+ "no_api_method": "Du m\u00e5 legge til et godkjenningstoken eller velge webhook",
+ "no_auth_token": "Du m\u00e5 legge til et godkjenningstoken"
},
"step": {
+ "api_method": {
+ "data": {
+ "token": "Lim inn Auth Token her",
+ "use_webhook": "Bruk webhook"
+ },
+ "description": "For \u00e5 kunne s\u00f8ke p\u00e5 API-en kreves det en `auth_token` som kan oppn\u00e5s ved \u00e5 f\u00f8lge [disse] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instruksjonene \n\n Valgt enhet: **{device_type}** \n\n Hvis du heller bruker den innebygde webhook-metoden (kun luftsperre), vennligst merk av i ruten nedenfor og la Auth Token v\u00e6re tom.",
+ "title": "Velg API-metode"
+ },
"user": {
+ "data": {
+ "device_name": "Navngi enheten din",
+ "device_type": "Type Platon-enhet"
+ },
"description": "Vil du starte oppsettet?",
- "title": "Sett opp Plaato Webhook"
+ "title": "Sett opp Plaato-enhetene"
+ },
+ "webhook": {
+ "description": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Plaato Airlock. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer.",
+ "title": "Webhook \u00e5 bruke"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "update_interval": "Oppdateringsintervall (minutter)"
+ },
+ "description": "Still inn oppdateringsintervallet (minutter)",
+ "title": "Alternativer for Plaato"
+ },
+ "webhook": {
+ "description": "Webhook info:\n\n- URL-adresse: {webhook_url}\n- Metode: POST\n\n",
+ "title": "Alternativer for Plaato Airlock"
}
}
}
diff --git a/homeassistant/components/plaato/translations/pl.json b/homeassistant/components/plaato/translations/pl.json
index 1f7c8141aa5686..ddfb779ea2e7d9 100644
--- a/homeassistant/components/plaato/translations/pl.json
+++ b/homeassistant/components/plaato/translations/pl.json
@@ -1,16 +1,53 @@
{
"config": {
"abort": {
+ "already_configured": "Konto jest ju\u017c skonfigurowane",
"single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.",
"webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook"
},
"create_entry": {
- "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistanta, musisz skonfigurowa\u0107 webhook w Plaato Airlock. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y."
+ "default": "Tw\u00f3j {device_type} Plaato o nazwie **{device_name}** zosta\u0142o pomy\u015blnie skonfigurowane!"
+ },
+ "error": {
+ "invalid_webhook_device": "Wybra\u0142e\u015b urz\u0105dzenie, kt\u00f3re nie obs\u0142uguje wysy\u0142ania danych do webhooka. Opcja dost\u0119pna tylko w areometrze Airlock",
+ "no_api_method": "Musisz doda\u0107 token uwierzytelniania lub wybra\u0107 webhook",
+ "no_auth_token": "Musisz doda\u0107 token autoryzacji"
},
"step": {
+ "api_method": {
+ "data": {
+ "token": "Wklej token autoryzacji",
+ "use_webhook": "U\u017cyj webhook"
+ },
+ "description": "Aby m\u00f3c przesy\u0142a\u0107 zapytania do API, wymagany jest \u201etoken autoryzacji\u201d, kt\u00f3ry mo\u017cna uzyska\u0107, post\u0119puj\u0105c zgodnie z [t\u0105] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instrukcj\u0105\n\nWybrane urz\u0105dzenie: **{device_type}** \n\nJe\u015bli wolisz u\u017cywa\u0107 wbudowanej metody webhook (tylko areomierz Airlock), zaznacz poni\u017csze pole i pozostaw token autoryzacji pusty",
+ "title": "Wybierz metod\u0119 API"
+ },
"user": {
+ "data": {
+ "device_name": "Nazwij swoje urz\u0105dzenie",
+ "device_type": "Rodzaj urz\u0105dzenia Plaato"
+ },
"description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?",
- "title": "Konfiguracja Plaato Webhook"
+ "title": "Konfiguracja urz\u0105dze\u0144 Plaato"
+ },
+ "webhook": {
+ "description": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistanta, musisz skonfigurowa\u0107 webhook w Plaato Airlock. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y.",
+ "title": "Webhook do u\u017cycia"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "update_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (w minutach)"
+ },
+ "description": "Ustaw cz\u0119stotliwo\u015bci aktualizacji (w minutach)",
+ "title": "Opcje dla Plaato"
+ },
+ "webhook": {
+ "description": "Informacje o webhooku: \n\n - URL: `{webhook_url}`\n - Metoda: POST \n\n",
+ "title": "Opcje dla areomierza Plaato Airlock"
}
}
}
diff --git a/homeassistant/components/plaato/translations/ru.json b/homeassistant/components/plaato/translations/ru.json
index 99e28ac9e04746..99e1bf94e0d9de 100644
--- a/homeassistant/components/plaato/translations/ru.json
+++ b/homeassistant/components/plaato/translations/ru.json
@@ -1,15 +1,52 @@
{
"config": {
"abort": {
+ "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.",
"single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.",
"webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439."
},
"create_entry": {
- "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Plaato Airlock.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438."
+ "default": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Plaato {device_type} **{device_name}** \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
+ },
+ "error": {
+ "invalid_webhook_device": "\u0412\u044b \u0432\u044b\u0431\u0440\u0430\u043b\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 Webhook. \u042d\u0442\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0430.",
+ "no_api_method": "\u041d\u0443\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0438\u043b\u0438 \u0432\u044b\u0431\u0440\u0430\u0442\u044c Webhook.",
+ "no_auth_token": "\u041d\u0443\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"step": {
+ "api_method": {
+ "data": {
+ "token": "\u0412\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441\u044e\u0434\u0430",
+ "use_webhook": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c Webhook"
+ },
+ "description": "\u0427\u0442\u043e\u0431\u044b \u0438\u043c\u0435\u0442\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0442\u044c API, \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f `auth_token`, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c, \u0441\u043b\u0435\u0434\u0443\u044f [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) \n\n\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e: **{device_type}** \n\n\u0415\u0441\u043b\u0438 \u0412\u044b \u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0438\u0442\u0430\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0439 Webhook (\u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f Airlock), \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 \u0444\u043b\u0430\u0436\u043e\u043a \u043d\u0438\u0436\u0435 \u0438 \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u0442\u043e\u043a\u0435\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0443\u0441\u0442\u044b\u043c.",
+ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 API"
+ },
"user": {
+ "data": {
+ "device_name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430",
+ "device_type": "\u0422\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Plaato"
+ },
"description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Plaato"
+ },
+ "webhook": {
+ "description": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Plaato Airlock.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.",
+ "title": "Webhook"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "update_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)"
+ },
+ "description": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)",
+ "title": "Plaato"
+ },
+ "webhook": {
+ "description": "Webhook:\n\n- URL: `{webhook_url}`\n- Method: POST",
"title": "Plaato Airlock"
}
}
diff --git a/homeassistant/components/plaato/translations/tr.json b/homeassistant/components/plaato/translations/tr.json
new file mode 100644
index 00000000000000..1f21b08ec81157
--- /dev/null
+++ b/homeassistant/components/plaato/translations/tr.json
@@ -0,0 +1,43 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.",
+ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir."
+ },
+ "error": {
+ "no_auth_token": "Bir kimlik do\u011frulama jetonu eklemeniz gerekiyor"
+ },
+ "step": {
+ "api_method": {
+ "data": {
+ "use_webhook": "Webhook kullan"
+ },
+ "title": "API y\u00f6ntemini se\u00e7in"
+ },
+ "user": {
+ "data": {
+ "device_name": "Cihaz\u0131n\u0131z\u0131 adland\u0131r\u0131n",
+ "device_type": "Plaato cihaz\u0131n\u0131n t\u00fcr\u00fc"
+ }
+ },
+ "webhook": {
+ "title": "Webhook kullanmak i\u00e7in"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "update_interval": "G\u00fcncelle\u015ftirme aral\u0131\u011f\u0131 (dakika)"
+ },
+ "description": "G\u00fcncelleme aral\u0131\u011f\u0131n\u0131 ayarlay\u0131n (dakika)",
+ "title": "Plaato i\u00e7in se\u00e7enekler"
+ },
+ "webhook": {
+ "title": "Plaato Airlock i\u00e7in se\u00e7enekler"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plaato/translations/uk.json b/homeassistant/components/plaato/translations/uk.json
new file mode 100644
index 00000000000000..a4f7de7c6be4ea
--- /dev/null
+++ b/homeassistant/components/plaato/translations/uk.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.",
+ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c."
+ },
+ "create_entry": {
+ "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0432\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f Plaato Airlock. \n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST \n\n \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457."
+ },
+ "step": {
+ "user": {
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?",
+ "title": "Plaato Airlock"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plaato/translations/zh-Hant.json b/homeassistant/components/plaato/translations/zh-Hant.json
index aec745ea38bc4c..26d7b728771b72 100644
--- a/homeassistant/components/plaato/translations/zh-Hant.json
+++ b/homeassistant/components/plaato/translations/zh-Hant.json
@@ -1,16 +1,53 @@
{
"config": {
"abort": {
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002",
"webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002"
},
"create_entry": {
- "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Plaato Airlock \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002"
+ "default": "\u540d\u7a31\u70ba **{device_name}** \u7684 Plaato {device_type} \u5df2\u6210\u529f\u8a2d\u5b9a\uff01"
+ },
+ "error": {
+ "invalid_webhook_device": "\u6240\u9078\u64c7\u7684\u88dd\u7f6e\u4e0d\u652f\u63f4\u50b3\u9001\u8cc7\u6599\u81f3 Webhook\u3001AirLock \u50c5\u652f\u63f4\u6b64\u985e\u578b",
+ "no_api_method": "\u9700\u8981\u65b0\u589e\u6388\u6b0a\u6b0a\u6756\u6216\u9078\u64c7 Webhook",
+ "no_auth_token": "\u9700\u8981\u65b0\u589e\u6388\u6b0a\u6b0a\u6756"
},
"step": {
+ "api_method": {
+ "data": {
+ "token": "\u65bc\u6b64\u8cbc\u4e0a\u6388\u6b0a\u6b0a\u6756",
+ "use_webhook": "\u4f7f\u7528 Webhook"
+ },
+ "description": "\u9700\u8981\u6388\u6b0a\u5bc6\u8981 `auth_token` \u65b9\u80fd\u67e5\u8a62 API\u3002\u7372\u5f97\u7684\u65b9\u6cd5\u8acb [\u53c3\u95b1](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) \u6559\u5b78\n\n\u9078\u64c7\u7684\u88dd\u7f6e\uff1a**{device_type}** \n\n\u5047\u5982\u9078\u64c7\u5167\u5efa Webhook \u65b9\u6cd5\uff08Airlock \u552f\u4e00\u652f\u63f4\uff09\uff0c\u8acb\u6aa2\u67e5\u4e0b\u65b9\u6838\u9078\u76d2\u4e26\u78ba\u5b9a\u4fdd\u6301\u6388\u6b0a\u6b0a\u6756\u6b04\u4f4d\u7a7a\u767d",
+ "title": "\u9078\u64c7 API \u65b9\u5f0f"
+ },
"user": {
+ "data": {
+ "device_name": "\u88dd\u7f6e\u540d\u7a31",
+ "device_type": "Plaato \u88dd\u7f6e\u985e\u578b"
+ },
"description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f",
- "title": "\u8a2d\u5b9a Plaato Webhook"
+ "title": "\u8a2d\u5b9a Plaato \u88dd\u7f6e"
+ },
+ "webhook": {
+ "description": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Plaato Airlock \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002",
+ "title": "\u4f7f\u7528\u4e4b Webhook"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "update_interval": "\u66f4\u65b0\u983b\u7387\uff08\u5206\uff09"
+ },
+ "description": "\u8a2d\u5b9a\u66f4\u65b0\u983b\u7387\uff08\u5206\uff09",
+ "title": "Plaato \u9078\u9805"
+ },
+ "webhook": {
+ "description": "Webhook \u8a0a\u606f\uff1a\n\n- URL\uff1a`{webhook_url}`\n- \u65b9\u5f0f\uff1aPOST\n\n",
+ "title": "Plaato Airlock \u9078\u9805"
}
}
}
diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py
index 2d8291875609bb..290993959b3fc0 100644
--- a/homeassistant/components/plant/__init__.py
+++ b/homeassistant/components/plant/__init__.py
@@ -1,5 +1,6 @@
"""Support for monitoring plants."""
from collections import deque
+from contextlib import suppress
from datetime import datetime, timedelta
import logging
@@ -324,12 +325,10 @@ def _load_history_from_db(self):
for state in states:
# filter out all None, NaN and "unknown" states
# only keep real values
- try:
+ with suppress(ValueError):
self._brightness_history.add_measurement(
int(state.state), state.last_updated
)
- except ValueError:
- pass
_LOGGER.debug("Initializing from database completed")
@property
@@ -348,7 +347,7 @@ def state(self):
return self._state
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the attributes of the entity.
Provide the individual measurements from the
diff --git a/homeassistant/components/plant/translations/id.json b/homeassistant/components/plant/translations/id.json
index 519964be2787af..5378edc405f116 100644
--- a/homeassistant/components/plant/translations/id.json
+++ b/homeassistant/components/plant/translations/id.json
@@ -1,9 +1,9 @@
{
"state": {
"_": {
- "ok": "OK",
- "problem": "Masalah"
+ "ok": "Oke",
+ "problem": "Bermasalah"
}
},
- "title": "Tanaman"
+ "title": "Monitor Tanaman"
}
\ No newline at end of file
diff --git a/homeassistant/components/plant/translations/uk.json b/homeassistant/components/plant/translations/uk.json
index 3204c42a714b9d..25f24b43b80ff3 100644
--- a/homeassistant/components/plant/translations/uk.json
+++ b/homeassistant/components/plant/translations/uk.json
@@ -1,8 +1,8 @@
{
"state": {
"_": {
- "ok": "\u0422\u0410\u041a",
- "problem": "\u0425\u0430\u043b\u0435\u043f\u0430"
+ "ok": "\u041e\u041a",
+ "problem": "\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430"
}
},
"title": "\u0420\u043e\u0441\u043b\u0438\u043d\u0430"
diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py
index 6b403150e9c35a..137c0524bac59a 100644
--- a/homeassistant/components/plex/__init__.py
+++ b/homeassistant/components/plex/__init__.py
@@ -7,7 +7,6 @@
from plexapi.gdm import GDM
from plexwebsocket import (
SIGNAL_CONNECTION_STATE,
- SIGNAL_DATA,
STATE_CONNECTED,
STATE_DISCONNECTED,
STATE_STOPPED,
@@ -25,9 +24,13 @@
)
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import device_registry as dev_reg, entity_registry as ent_reg
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.debounce import Debouncer
-from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.dispatcher import (
+ async_dispatcher_connect,
+ async_dispatcher_send,
+)
from .const import (
CONF_SERVER,
@@ -39,6 +42,7 @@
PLATFORMS,
PLATFORMS_COMPLETED,
PLEX_SERVER_CONFIG,
+ PLEX_UPDATE_LIBRARY_SIGNAL,
PLEX_UPDATE_PLATFORMS_SIGNAL,
SERVERS,
WEBSOCKETS,
@@ -61,12 +65,16 @@ async def async_setup(hass, config):
gdm = hass.data[PLEX_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(
hass,
_LOGGER,
cooldown=10,
immediate=True,
- function=partial(gdm.scan, scan_for_clients=True),
+ function=gdm_scan,
).async_call
return True
@@ -145,26 +153,22 @@ async def async_setup_entry(hass, entry):
entry.add_update_listener(async_options_updated)
- async def async_update_plex():
- await hass.data[PLEX_DOMAIN][GDM_DEBOUNCER]()
- await plex_server.async_update_platforms()
-
unsub = async_dispatcher_connect(
hass,
PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id),
- async_update_plex,
+ plex_server.async_update_platforms,
)
hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, [])
hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)
@callback
- def plex_websocket_callback(signal, data, error):
+ def plex_websocket_callback(msgtype, data, error):
"""Handle callbacks from plexwebsocket library."""
- if signal == SIGNAL_CONNECTION_STATE:
+ if msgtype == SIGNAL_CONNECTION_STATE:
if data == STATE_CONNECTED:
_LOGGER.debug("Websocket to %s successful", entry.data[CONF_SERVER])
- hass.async_create_task(async_update_plex())
+ hass.async_create_task(plex_server.async_update_platforms())
elif data == STATE_DISCONNECTED:
_LOGGER.debug(
"Websocket to %s disconnected, retrying", entry.data[CONF_SERVER]
@@ -178,14 +182,22 @@ def plex_websocket_callback(signal, data, error):
)
hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
- elif signal == SIGNAL_DATA:
+ elif msgtype == "playing":
hass.async_create_task(plex_server.async_update_session(data))
+ elif msgtype == "status":
+ if data["StatusNotification"][0]["title"] == "Library scan complete":
+ async_dispatcher_send(
+ hass,
+ PLEX_UPDATE_LIBRARY_SIGNAL.format(server_id),
+ )
session = async_get_clientsession(hass)
+ subscriptions = ["playing", "status"]
verify_ssl = server_config.get(CONF_VERIFY_SSL)
websocket = PlexWebsocket(
plex_server.plex_server,
plex_websocket_callback,
+ subscriptions=subscriptions,
session=session,
verify_ssl=verify_ssl,
)
@@ -210,6 +222,8 @@ def close_websocket_session(_):
)
task.add_done_callback(partial(start_websocket_session, platform))
+ async_cleanup_plex_devices(hass, entry)
+
def get_plex_account(plex_server):
try:
return plex_server.account
@@ -250,3 +264,30 @@ async def async_options_updated(hass, entry):
# Guard incomplete setup during reauth flows
if server_id in hass.data[PLEX_DOMAIN][SERVERS]:
hass.data[PLEX_DOMAIN][SERVERS][server_id].options = entry.options
+
+
+@callback
+def async_cleanup_plex_devices(hass, entry):
+ """Clean up old and invalid devices from the registry."""
+ device_registry = dev_reg.async_get(hass)
+ entity_registry = ent_reg.async_get(hass)
+
+ device_entries = hass.helpers.device_registry.async_entries_for_config_entry(
+ device_registry, entry.entry_id
+ )
+
+ for device_entry in device_entries:
+ if (
+ len(
+ hass.helpers.entity_registry.async_entries_for_device(
+ entity_registry, device_entry.id, include_disabled_entities=True
+ )
+ )
+ == 0
+ ):
+ _LOGGER.debug(
+ "Removing orphaned device: %s / %s",
+ device_entry.name,
+ device_entry.identifiers,
+ )
+ device_registry.async_remove_device(device_entry.id)
diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py
index f177412e7ec26f..d1fa5684cf5f23 100644
--- a/homeassistant/components/plex/config_flow.py
+++ b/homeassistant/components/plex/config_flow.py
@@ -27,7 +27,7 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.network import get_url
-from .const import ( # pylint: disable=unused-import
+from .const import (
AUTH_CALLBACK_NAME,
AUTH_CALLBACK_PATH,
AUTOMATIC_SETUP_STRING,
@@ -230,10 +230,7 @@ async def async_step_server_validate(self, server_config):
}
entry = await self.async_set_unique_id(server_id)
- if (
- self.context[CONF_SOURCE] # pylint: disable=no-member
- == config_entries.SOURCE_REAUTH
- ):
+ if self.context[CONF_SOURCE] == config_entries.SOURCE_REAUTH:
self.hass.config_entries.async_update_entry(entry, data=data)
_LOGGER.debug("Updated config entry for %s", plex_server.friendly_name)
await self.hass.config_entries.async_reload(entry.entry_id)
@@ -243,7 +240,7 @@ async def async_step_server_validate(self, server_config):
_LOGGER.debug("Valid config created for %s", plex_server.friendly_name)
- return self.async_create_entry(title=plex_server.friendly_name, data=data)
+ return self.async_create_entry(title=url, data=data)
async def async_step_select_server(self, user_input=None):
"""Use selected Plex server."""
@@ -280,7 +277,7 @@ async def async_step_integration_discovery(self, discovery_info):
self._abort_if_unique_id_configured()
host = f"{discovery_info['from'][0]}:{discovery_info['data']['Port']}"
name = discovery_info["data"]["Name"]
- self.context["title_placeholders"] = { # pylint: disable=no-member
+ self.context["title_placeholders"] = {
"host": host,
"name": name,
}
diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py
index eec433202e45f4..e247f7a5db7b29 100644
--- a/homeassistant/components/plex/const.py
+++ b/homeassistant/components/plex/const.py
@@ -4,6 +4,7 @@
DOMAIN = "plex"
NAME_FORMAT = "Plex ({})"
COMMON_PLAYERS = ["Plex Web"]
+TRANSIENT_DEVICE_MODELS = ["Plex Web", "Plex for Sonos"]
DEFAULT_PORT = 32400
DEFAULT_SSL = False
@@ -26,6 +27,7 @@
PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal.{}"
PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL = "plex_update_session_signal.{}"
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}"
+PLEX_UPDATE_LIBRARY_SIGNAL = "plex_update_libraries_signal.{}"
PLEX_UPDATE_PLATFORMS_SIGNAL = "plex_update_platforms_signal.{}"
PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal.{}"
diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json
index f0f1e09a15c475..e0e62d7150bf06 100644
--- a/homeassistant/components/plex/manifest.json
+++ b/homeassistant/components/plex/manifest.json
@@ -4,9 +4,9 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/plex",
"requirements": [
- "plexapi==4.3.0",
+ "plexapi==4.5.1",
"plexauth==0.0.6",
- "plexwebsocket==0.0.12"
+ "plexwebsocket==0.0.13"
],
"dependencies": ["http"],
"codeowners": ["@jjlawren"]
diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py
index cfc5a12d6c5398..f3f92880c44c6d 100644
--- a/homeassistant/components/plex/media_browser.py
+++ b/homeassistant/components/plex/media_browser.py
@@ -153,7 +153,7 @@ def build_item_response(payload):
title = entity.plex_server.friendly_name
elif media_content_type == "library":
library_or_section = entity.plex_server.library.sectionByID(
- media_content_id
+ int(media_content_id)
)
title = library_or_section.title
try:
@@ -193,7 +193,7 @@ def build_item_response(payload):
return server_payload(entity.plex_server)
if media_content_type == "library":
- return library_payload(media_content_id)
+ return library_payload(int(media_content_id))
except UnknownMediaType as err:
raise BrowseError(
@@ -223,7 +223,7 @@ def library_section_payload(section):
return BrowseMedia(
title=section.title,
media_class=MEDIA_CLASS_DIRECTORY,
- media_content_id=section.key,
+ media_content_id=str(section.key),
media_content_type="library",
can_play=False,
can_expand=True,
diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py
index 1a57186bd9b7e2..650ed2c89b081f 100644
--- a/homeassistant/components/plex/media_player.py
+++ b/homeassistant/components/plex/media_player.py
@@ -41,6 +41,7 @@
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL,
PLEX_UPDATE_SENSOR_SIGNAL,
SERVERS,
+ TRANSIENT_DEVICE_MODELS,
)
from .media_browser import browse_media
@@ -522,7 +523,7 @@ def play_media(self, media_type, media_id, **kwargs):
_LOGGER.error("Timed out playing on %s", self.name)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the scene state attributes."""
attributes = {}
for attr in [
@@ -544,6 +545,15 @@ def device_info(self):
if self.machine_identifier is None:
return None
+ if self.device_product in TRANSIENT_DEVICE_MODELS:
+ return {
+ "identifiers": {(PLEX_DOMAIN, "plex.tv-clients")},
+ "name": "Plex Client Service",
+ "manufacturer": "Plex",
+ "model": "Plex Clients",
+ "entry_type": "service",
+ }
+
return {
"identifiers": {(PLEX_DOMAIN, self.machine_identifier)},
"manufacturer": self.device_platform or "Plex",
diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py
index 7633c5deaa86b8..731d5bbc7dbca2 100644
--- a/homeassistant/components/plex/models.py
+++ b/homeassistant/components/plex/models.py
@@ -70,7 +70,7 @@ def update_media(self, media):
self.media_library_title = "Live TV"
else:
self.media_library_title = (
- media.section().title if media.section() is not None else ""
+ media.section().title if media.librarySectionID is not None else ""
)
if media.type == "episode":
diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py
index 8c3733a7450c06..95ba0a65ef0872 100644
--- a/homeassistant/components/plex/sensor.py
+++ b/homeassistant/components/plex/sensor.py
@@ -1,19 +1,39 @@
"""Support for Plex media server monitoring."""
import logging
+from plexapi.exceptions import NotFound
+
+from homeassistant.components.sensor import SensorEntity
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from .const import (
CONF_SERVER_IDENTIFIER,
- DISPATCHERS,
DOMAIN as PLEX_DOMAIN,
NAME_FORMAT,
+ PLEX_UPDATE_LIBRARY_SIGNAL,
PLEX_UPDATE_SENSOR_SIGNAL,
SERVERS,
)
+LIBRARY_ATTRIBUTE_TYPES = {
+ "artist": ["artist", "album"],
+ "photo": ["photoalbum"],
+ "show": ["show", "season"],
+}
+
+LIBRARY_PRIMARY_LIBTYPE = {
+ "show": "episode",
+ "artist": "track",
+}
+
+LIBRARY_ICON_LOOKUP = {
+ "artist": "mdi:music",
+ "movie": "mdi:movie",
+ "photo": "mdi:image",
+ "show": "mdi:television",
+}
+
_LOGGER = logging.getLogger(__name__)
@@ -21,11 +41,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Plex sensor from a config entry."""
server_id = config_entry.data[CONF_SERVER_IDENTIFIER]
plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id]
- sensor = PlexSensor(hass, plexserver)
- async_add_entities([sensor])
+ sensors = [PlexSensor(hass, plexserver)]
+
+ def create_library_sensors():
+ """Create Plex library sensors with sync calls."""
+ for library in plexserver.library.sections():
+ sensors.append(PlexLibrarySectionSensor(hass, plexserver, library))
+ await hass.async_add_executor_job(create_library_sensors)
+ async_add_entities(sensors)
-class PlexSensor(Entity):
+
+class PlexSensor(SensorEntity):
"""Representation of a Plex now playing sensor."""
def __init__(self, hass, plex_server):
@@ -45,12 +72,13 @@ def __init__(self, hass, plex_server):
async def async_added_to_hass(self):
"""Run when about to be added to hass."""
server_id = self._server.machine_identifier
- unsub = async_dispatcher_connect(
- self.hass,
- PLEX_UPDATE_SENSOR_SIGNAL.format(server_id),
- self.async_refresh_sensor,
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ PLEX_UPDATE_SENSOR_SIGNAL.format(server_id),
+ self.async_refresh_sensor,
+ )
)
- self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)
async def _async_refresh_sensor(self):
"""Set instance object and trigger an entity state update."""
@@ -89,7 +117,7 @@ def icon(self):
return "mdi:plex"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._server.sensor_attributes
@@ -103,6 +131,117 @@ def device_info(self):
"identifiers": {(PLEX_DOMAIN, self._server.machine_identifier)},
"manufacturer": "Plex",
"model": "Plex Media Server",
- "name": "Activity Sensor",
+ "name": self._server.friendly_name,
+ "sw_version": self._server.version,
+ }
+
+
+class PlexLibrarySectionSensor(SensorEntity):
+ """Representation of a Plex library section sensor."""
+
+ def __init__(self, hass, plex_server, plex_library_section):
+ """Initialize the sensor."""
+ self._server = plex_server
+ self.server_name = plex_server.friendly_name
+ self.server_id = plex_server.machine_identifier
+ self.library_section = plex_library_section
+ self.library_type = plex_library_section.type
+ self._name = f"{self.server_name} Library - {plex_library_section.title}"
+ self._unique_id = f"library-{self.server_id}-{plex_library_section.uuid}"
+ self._state = None
+ self._available = True
+ self._attributes = {}
+
+ async def async_added_to_hass(self):
+ """Run when about to be added to hass."""
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ PLEX_UPDATE_LIBRARY_SIGNAL.format(self.server_id),
+ self.async_refresh_sensor,
+ )
+ )
+ await self.async_refresh_sensor()
+
+ async def async_refresh_sensor(self):
+ """Update state and attributes for the library sensor."""
+ _LOGGER.debug("Refreshing library sensor for '%s'", self.name)
+ try:
+ await self.hass.async_add_executor_job(self._update_state_and_attrs)
+ self._available = True
+ except NotFound:
+ self._available = False
+ self.async_write_ha_state()
+
+ def _update_state_and_attrs(self):
+ """Update library sensor state with sync calls."""
+ primary_libtype = LIBRARY_PRIMARY_LIBTYPE.get(
+ self.library_type, self.library_type
+ )
+
+ self._state = self.library_section.totalViewSize(
+ libtype=primary_libtype, includeCollections=False
+ )
+ for libtype in LIBRARY_ATTRIBUTE_TYPES.get(self.library_type, []):
+ self._attributes[f"{libtype}s"] = self.library_section.totalViewSize(
+ libtype=libtype, includeCollections=False
+ )
+
+ @property
+ def available(self):
+ """Return the availability of the client."""
+ return self._available
+
+ @property
+ def entity_registry_enabled_default(self):
+ """Return if sensor should be enabled by default."""
+ return False
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def unique_id(self):
+ """Return the id of this plex client."""
+ return self._unique_id
+
+ @property
+ def should_poll(self):
+ """Return True if entity has to be polled for state."""
+ return False
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit this state is expressed in."""
+ return "Items"
+
+ @property
+ def icon(self):
+ """Return the icon of the sensor."""
+ return LIBRARY_ICON_LOOKUP.get(self.library_type, "mdi:plex")
+
+ @property
+ def extra_state_attributes(self):
+ """Return the state attributes."""
+ return self._attributes
+
+ @property
+ def device_info(self):
+ """Return a device description for device registry."""
+ if self.unique_id is None:
+ return None
+
+ return {
+ "identifiers": {(PLEX_DOMAIN, self.server_id)},
+ "manufacturer": "Plex",
+ "model": "Plex Media Server",
+ "name": self.server_name,
"sw_version": self._server.version,
}
diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py
index 1baceb78ff1e45..d4bd4b09ef26bc 100644
--- a/homeassistant/components/plex/server.py
+++ b/homeassistant/components/plex/server.py
@@ -34,6 +34,7 @@
DEBOUNCE_TIMEOUT,
DEFAULT_VERIFY_SSL,
DOMAIN,
+ GDM_DEBOUNCER,
GDM_SCANNER,
PLAYER_SOURCE,
PLEX_NEW_MP_SIGNAL,
@@ -189,7 +190,7 @@ def _update_plexdirect_hostname():
_connect_with_url()
except requests.exceptions.SSLError as error:
while error and not isinstance(error, ssl.SSLCertVerificationError):
- error = error.__context__ # pylint: disable=no-member
+ error = error.__context__
if isinstance(error, ssl.SSLCertVerificationError):
domain = urlparse(self._url).netloc.split(":")[0]
if domain.endswith("plex.direct") and error.args[0].startswith(
@@ -213,21 +214,27 @@ def _update_plexdirect_hostname():
try:
system_accounts = self._plex_server.systemAccounts()
+ shared_users = self.account.users() if self.account else []
except Unauthorized:
_LOGGER.warning(
"Plex account has limited permissions, shared account filtering will not be available"
)
else:
- self._accounts = [
- account.name for account in system_accounts if account.name
- ]
+ self._accounts = []
+ for user in shared_users:
+ for shared_server in user.servers:
+ if shared_server.machineIdentifier == self.machine_identifier:
+ self._accounts.append(user.title)
+
_LOGGER.debug("Linked accounts: %s", self.accounts)
- owner_account = [
- account.name for account in system_accounts if account.accountID == 1
- ]
+ owner_account = next(
+ (account.name for account in system_accounts if account.accountID == 1),
+ None,
+ )
if owner_account:
- self._owner_username = owner_account[0]
+ self._owner_username = owner_account
+ self._accounts.append(owner_account)
_LOGGER.debug("Server owner found: '%s'", self._owner_username)
self._version = self._plex_server.version
@@ -250,11 +257,7 @@ def async_refresh_entity(self, machine_identifier, device, session, source):
async def async_update_session(self, payload):
"""Process a session payload received from a websocket callback."""
- try:
- session_payload = payload["PlaySessionStateNotification"][0]
- except KeyError:
- await self.async_update_platforms()
- return
+ session_payload = payload["PlaySessionStateNotification"][0]
state = session_payload["state"]
if state == "buffering":
@@ -317,6 +320,8 @@ async def _async_update_platforms(self):
"""Update the platform entities."""
_LOGGER.debug("Updating devices")
+ await self.hass.data[DOMAIN][GDM_DEBOUNCER]()
+
available_clients = {}
ignored_clients = set()
new_clients = set()
@@ -360,17 +365,20 @@ def process_device(source, device):
PLAYER_SOURCE, source
)
- if device.machineIdentifier not in ignored_clients:
- if self.option_ignore_plexweb_clients and device.product == "Plex Web":
- ignored_clients.add(device.machineIdentifier)
- if device.machineIdentifier not in self._known_clients:
- _LOGGER.debug(
- "Ignoring %s %s: %s",
- "Plex Web",
- source,
- device.machineIdentifier,
- )
- return
+ if (
+ device.machineIdentifier not in ignored_clients
+ and self.option_ignore_plexweb_clients
+ and device.product == "Plex Web"
+ ):
+ ignored_clients.add(device.machineIdentifier)
+ if device.machineIdentifier not in self._known_clients:
+ _LOGGER.debug(
+ "Ignoring %s %s: %s",
+ "Plex Web",
+ source,
+ device.machineIdentifier,
+ )
+ return
if device.machineIdentifier not in (
self._created_clients | ignored_clients | new_clients
@@ -389,9 +397,10 @@ def connect_to_client(source, baseurl, machine_identifier, name="Unknown"):
client = PlexClient(
server=self._plex_server,
baseurl=baseurl,
+ identifier=machine_identifier,
token=self._plex_server.createToken(),
)
- except requests.exceptions.ConnectionError:
+ except (NotFound, requests.exceptions.ConnectionError):
_LOGGER.error(
"Direct client connection failed, will try again: %s (%s)",
name,
@@ -412,9 +421,11 @@ def connect_to_resource(resource):
"""Connect to a plex.tv resource and return a Plex client."""
try:
client = resource.connect(timeout=3)
- _LOGGER.debug("plex.tv resource connection successful: %s", client)
+ _LOGGER.debug("Resource connection successful to plex.tv: %s", client)
except NotFound:
- _LOGGER.error("plex.tv resource connection failed: %s", resource.name)
+ _LOGGER.error(
+ "Resource connection failed to plex.tv: %s", resource.name
+ )
else:
client.proxyThroughServer(value=False, server=self._plex_server)
self._client_device_cache[client.machineIdentifier] = client
diff --git a/homeassistant/components/plex/translations/de.json b/homeassistant/components/plex/translations/de.json
index 961ad4b3ed6e3e..2ba14e65f85f01 100644
--- a/homeassistant/components/plex/translations/de.json
+++ b/homeassistant/components/plex/translations/de.json
@@ -3,9 +3,10 @@
"abort": {
"all_configured": "Alle verkn\u00fcpften Server sind bereits konfiguriert",
"already_configured": "Dieser Plex-Server ist bereits konfiguriert",
- "already_in_progress": "Plex wird konfiguriert",
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich",
"token_request_timeout": "Zeit\u00fcberschreitung beim Erhalt des Tokens",
- "unknown": "Aus unbekanntem Grund fehlgeschlagen"
+ "unknown": "Unerwarteter Fehler"
},
"error": {
"faulty_credentials": "Autorisierung fehlgeschlagen, Token \u00fcberpr\u00fcfen",
diff --git a/homeassistant/components/plex/translations/hu.json b/homeassistant/components/plex/translations/hu.json
index cfccf5c83e6949..9168f070609f48 100644
--- a/homeassistant/components/plex/translations/hu.json
+++ b/homeassistant/components/plex/translations/hu.json
@@ -3,26 +3,30 @@
"abort": {
"all_configured": "Az \u00f6sszes \u00f6sszekapcsolt szerver m\u00e1r konfigur\u00e1lva van",
"already_configured": "Ez a Plex szerver m\u00e1r konfigur\u00e1lva van",
- "already_in_progress": "A Plex konfigur\u00e1l\u00e1sa folyamatban van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt",
"token_request_timeout": "Token k\u00e9r\u00e9sre sz\u00e1nt id\u0151 lej\u00e1rt",
- "unknown": "Ismeretlen okb\u00f3l nem siker\u00fclt"
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"error": {
"faulty_credentials": "A hiteles\u00edt\u00e9s sikertelen",
"no_servers": "Nincs szerver csatlakoztatva a fi\u00f3khoz",
"not_found": "A Plex szerver nem tal\u00e1lhat\u00f3"
},
+ "flow_title": "{name} ({host})",
"step": {
"manual_setup": {
"data": {
"host": "Hoszt",
"port": "Port",
- "ssl": "Haszn\u00e1ljon SSL-t"
+ "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata",
+ "token": "Token (opcion\u00e1lis)",
+ "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se"
}
},
"select_server": {
"data": {
- "server": "szerver"
+ "server": "Szerver"
},
"description": "T\u00f6bb szerver el\u00e9rhet\u0151, v\u00e1lasszon egyet:",
"title": "Plex-kiszolg\u00e1l\u00f3 kiv\u00e1laszt\u00e1sa"
@@ -39,6 +43,7 @@
"step": {
"plex_mp_settings": {
"data": {
+ "ignore_plex_web_clients": "Plex Web kliensek figyelmen k\u00edv\u00fcl hagy\u00e1sa",
"use_episode_art": "Haszn\u00e1lja az epiz\u00f3d bor\u00edt\u00f3j\u00e1t"
},
"description": "Plex media lej\u00e1tsz\u00f3k be\u00e1ll\u00edt\u00e1sai"
diff --git a/homeassistant/components/plex/translations/id.json b/homeassistant/components/plex/translations/id.json
new file mode 100644
index 00000000000000..7c596835e03c07
--- /dev/null
+++ b/homeassistant/components/plex/translations/id.json
@@ -0,0 +1,62 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "Semua server tertaut sudah dikonfigurasi",
+ "already_configured": "Server Plex ini sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "reauth_successful": "Autentikasi ulang berhasil",
+ "token_request_timeout": "Tenggang waktu pengambilan token habis",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "error": {
+ "faulty_credentials": "Otorisasi gagal, verifikasi Token",
+ "host_or_token": "Harus menyediakan setidaknya satu Host atau Token",
+ "no_servers": "Tidak ada server yang ditautkan ke akun Plex",
+ "not_found": "Server Plex tidak ditemukan",
+ "ssl_error": "Masalah sertifikat SSL"
+ },
+ "flow_title": "{name} ({host})",
+ "step": {
+ "manual_setup": {
+ "data": {
+ "host": "Host",
+ "port": "Port",
+ "ssl": "Menggunakan sertifikat SSL",
+ "token": "Token (Opsional)",
+ "verify_ssl": "Verifikasi sertifikat SSL"
+ },
+ "title": "Konfigurasi Plex Manual"
+ },
+ "select_server": {
+ "data": {
+ "server": "Server"
+ },
+ "description": "Beberapa server tersedia, pilih satu:",
+ "title": "Pilih server Plex"
+ },
+ "user": {
+ "description": "Lanjutkan [plex.tv](https://plex.tv) untuk menautkan server Plex.",
+ "title": "Server Media Plex"
+ },
+ "user_advanced": {
+ "data": {
+ "setup_method": "Metode penyiapan"
+ },
+ "title": "Server Media Plex"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "plex_mp_settings": {
+ "data": {
+ "ignore_new_shared_users": "Abaikan pengguna baru yang dikelola/berbagi",
+ "ignore_plex_web_clients": "Abaikan klien Plex Web",
+ "monitored_users": "Pengguna yang dipantau",
+ "use_episode_art": "Gunakan sampul episode"
+ },
+ "description": "Opsi untuk Pemutar Media Plex"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/translations/it.json b/homeassistant/components/plex/translations/it.json
index f470c1bc1639cc..f1ec23e2736bca 100644
--- a/homeassistant/components/plex/translations/it.json
+++ b/homeassistant/components/plex/translations/it.json
@@ -4,7 +4,7 @@
"all_configured": "Tutti i server collegati sono gi\u00e0 configurati",
"already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato",
"already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
- "reauth_successful": "La riautenticazione ha avuto successo",
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente",
"token_request_timeout": "Timeout per l'ottenimento del token",
"unknown": "Errore imprevisto"
},
diff --git a/homeassistant/components/plex/translations/ko.json b/homeassistant/components/plex/translations/ko.json
index 7c461fe1673211..d6b0c6a23419c5 100644
--- a/homeassistant/components/plex/translations/ko.json
+++ b/homeassistant/components/plex/translations/ko.json
@@ -3,9 +3,10 @@
"abort": {
"all_configured": "\uc774\ubbf8 \uad6c\uc131\ub41c \ubaa8\ub4e0 \uc5f0\uacb0\ub41c \uc11c\ubc84",
"already_configured": "\uc774 Plex \uc11c\ubc84\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "already_in_progress": "Plex \ub97c \uad6c\uc131 \uc911\uc785\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4",
"token_request_timeout": "\ud1a0\ud070 \ud68d\ub4dd \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4",
- "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc774\uc720\ub85c \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4"
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
"faulty_credentials": "\uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \ud1a0\ud070\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694",
@@ -20,9 +21,9 @@
"data": {
"host": "\ud638\uc2a4\ud2b8",
"port": "\ud3ec\ud2b8",
- "ssl": "SSL \uc0ac\uc6a9",
+ "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9",
"token": "\ud1a0\ud070 (\uc120\ud0dd \uc0ac\ud56d)",
- "verify_ssl": "SSL \uc778\uc99d\uc11c \uac80\uc99d"
+ "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778"
},
"title": "Plex \uc9c1\uc811 \uad6c\uc131\ud558\uae30"
},
@@ -34,7 +35,7 @@
"title": "Plex \uc11c\ubc84 \uc120\ud0dd\ud558\uae30"
},
"user": {
- "description": "Plex \uc11c\ubc84\ub97c \uc5f0\uacb0\ud558\ub824\uba74 [plex.tv](https://plex.tv) \ub85c \uacc4\uc18d \uc9c4\ud589\ud574\uc8fc\uc138\uc694.",
+ "description": "Plex \uc11c\ubc84\ub97c \uc5f0\uacb0\ud558\ub824\uba74 [plex.tv](https://plex.tv)\ub85c \uacc4\uc18d \uc9c4\ud589\ud574\uc8fc\uc138\uc694.",
"title": "Plex \ubbf8\ub514\uc5b4 \uc11c\ubc84"
},
"user_advanced": {
diff --git a/homeassistant/components/plex/translations/nl.json b/homeassistant/components/plex/translations/nl.json
index 00c2b30c490ee7..a196555e7ea56d 100644
--- a/homeassistant/components/plex/translations/nl.json
+++ b/homeassistant/components/plex/translations/nl.json
@@ -3,14 +3,15 @@
"abort": {
"all_configured": "Alle gekoppelde servers zijn al geconfigureerd",
"already_configured": "Deze Plex-server is al geconfigureerd",
- "already_in_progress": "Plex wordt geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
+ "reauth_successful": "Herauthenticatie was succesvol",
"token_request_timeout": "Time-out verkrijgen van token",
- "unknown": "Mislukt om onbekende reden"
+ "unknown": "Onverwachte fout"
},
"error": {
- "faulty_credentials": "Autorisatie mislukt",
+ "faulty_credentials": "Autorisatie mislukt, controleer token",
"host_or_token": "Moet ten minste \u00e9\u00e9n host of token verstrekken.",
- "no_servers": "Geen servers gekoppeld aan account",
+ "no_servers": "Geen servers gekoppeld aan Plex account",
"not_found": "Plex-server niet gevonden",
"ssl_error": "SSL-certificaatprobleem"
},
diff --git a/homeassistant/components/plex/translations/tr.json b/homeassistant/components/plex/translations/tr.json
new file mode 100644
index 00000000000000..93f8cc85eaeee9
--- /dev/null
+++ b/homeassistant/components/plex/translations/tr.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Bu Plex sunucusu zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor",
+ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "manual_setup": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ }
+ },
+ "user": {
+ "title": "Plex Medya Sunucusu"
+ },
+ "user_advanced": {
+ "data": {
+ "setup_method": "Kurulum y\u00f6ntemi"
+ },
+ "title": "Plex Medya Sunucusu"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/translations/uk.json b/homeassistant/components/plex/translations/uk.json
new file mode 100644
index 00000000000000..20351cf735abfc
--- /dev/null
+++ b/homeassistant/components/plex/translations/uk.json
@@ -0,0 +1,62 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "\u0412\u0441\u0456 \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0456 \u0441\u0435\u0440\u0432\u0435\u0440\u0438 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0456.",
+ "already_configured": "\u0426\u0435\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e",
+ "token_request_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0442\u043e\u043a\u0435\u043d\u0430.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "error": {
+ "faulty_credentials": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0422\u043e\u043a\u0435\u043d.",
+ "host_or_token": "\u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u043a\u0430\u0437\u0430\u0442\u0438 \u0425\u043e\u0441\u0442 \u0430\u0431\u043e \u0422\u043e\u043a\u0435\u043d.",
+ "no_servers": "\u041d\u0435\u043c\u0430\u0454 \u0441\u0435\u0440\u0432\u0435\u0440\u0456\u0432, \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0438\u0445 \u0437 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u043c \u0437\u0430\u043f\u0438\u0441\u043e\u043c.",
+ "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e.",
+ "ssl_error": "\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u0437 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u043c."
+ },
+ "flow_title": "{name} ({host})",
+ "step": {
+ "manual_setup": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442",
+ "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL",
+ "token": "\u0422\u043e\u043a\u0435\u043d (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)",
+ "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL"
+ },
+ "title": "\u0420\u0443\u0447\u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Plex"
+ },
+ "select_server": {
+ "data": {
+ "server": "\u0421\u0435\u0440\u0432\u0435\u0440"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0434\u0438\u043d \u0437 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u0456\u0432:",
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 Plex"
+ },
+ "user": {
+ "description": "\u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043d\u0430 [plex.tv](https://plex.tv), \u0449\u043e\u0431 \u043f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0434\u043e Home Assistant.",
+ "title": "Plex Media Server"
+ },
+ "user_advanced": {
+ "data": {
+ "setup_method": "\u0421\u043f\u043e\u0441\u0456\u0431 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f"
+ },
+ "title": "Plex Media Server"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "plex_mp_settings": {
+ "data": {
+ "ignore_new_shared_users": "\u0406\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438 \u043d\u043e\u0432\u0438\u0445 \u043a\u0435\u0440\u043e\u0432\u0430\u043d\u0438\u0445 / \u0437\u0430\u0433\u0430\u043b\u044c\u043d\u0438\u0445 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0456\u0432",
+ "ignore_plex_web_clients": "\u0406\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438 \u0432\u0435\u0431-\u043a\u043b\u0456\u0454\u043d\u0442\u0438 Plex",
+ "monitored_users": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u043d\u0456 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0456",
+ "use_episode_art": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u043e\u0431\u043a\u043b\u0430\u0434\u0438\u043d\u043a\u0438 \u0435\u043f\u0456\u0437\u043e\u0434\u0456\u0432"
+ },
+ "description": "\u0414\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/translations/zh-Hant.json b/homeassistant/components/plex/translations/zh-Hant.json
index 137b953a145745..7f19fa0d035441 100644
--- a/homeassistant/components/plex/translations/zh-Hant.json
+++ b/homeassistant/components/plex/translations/zh-Hant.json
@@ -5,12 +5,12 @@
"already_configured": "Plex \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f",
- "token_request_timeout": "\u53d6\u5f97\u5bc6\u9470\u903e\u6642",
+ "token_request_timeout": "\u53d6\u5f97\u6b0a\u6756\u903e\u6642",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"error": {
- "faulty_credentials": "\u9a57\u8b49\u5931\u6557\u3001\u78ba\u8a8d\u5bc6\u9470",
- "host_or_token": "\u5fc5\u9808\u81f3\u5c11\u63d0\u4f9b\u4e3b\u6a5f\u7aef\u6216\u5bc6\u9470",
+ "faulty_credentials": "\u9a57\u8b49\u5931\u6557\u3001\u78ba\u8a8d\u6b0a\u6756",
+ "host_or_token": "\u5fc5\u9808\u81f3\u5c11\u63d0\u4f9b\u4e3b\u6a5f\u7aef\u6216\u6b0a\u6756",
"no_servers": "Plex \u5e33\u865f\u672a\u7d81\u5b9a\u4efb\u4f55\u4f3a\u670d\u5668",
"not_found": "\u627e\u4e0d\u5230 Plex \u4f3a\u670d\u5668",
"ssl_error": "SSL \u8a8d\u8b49\u554f\u984c"
@@ -22,7 +22,7 @@
"host": "\u4e3b\u6a5f\u7aef",
"port": "\u901a\u8a0a\u57e0",
"ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49",
- "token": "\u5bc6\u9470\uff08\u9078\u9805\uff09",
+ "token": "\u6b0a\u6756\uff08\u9078\u9805\uff09",
"verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49"
},
"title": "Plex \u624b\u52d5\u8a2d\u5b9a"
diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py
index 825d27d59bb6d1..023ffa3de701cb 100644
--- a/homeassistant/components/plugwise/binary_sensor.py
+++ b/homeassistant/components/plugwise/binary_sensor.py
@@ -143,7 +143,7 @@ def __init__(self, api, coordinator, name, dev_id, binary_sensor):
self._attributes = {}
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attributes
diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py
index c8a2191963ee8e..3efd6dbc3ca778 100644
--- a/homeassistant/components/plugwise/climate.py
+++ b/homeassistant/components/plugwise/climate.py
@@ -124,7 +124,7 @@ def supported_features(self):
return SUPPORT_FLAGS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
attributes = {}
if self._schema_names:
diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py
index e0d2262773704d..e17c85a79788d5 100644
--- a/homeassistant/components/plugwise/config_flow.py
+++ b/homeassistant/components/plugwise/config_flow.py
@@ -19,12 +19,7 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import DiscoveryInfoType
-from .const import ( # pylint:disable=unused-import
- DEFAULT_PORT,
- DEFAULT_SCAN_INTERVAL,
- DOMAIN,
- ZEROCONF_MAP,
-)
+from .const import DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN, ZEROCONF_MAP
_LOGGER = logging.getLogger(__name__)
@@ -102,7 +97,6 @@ async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType):
_version = _properties.get("version", "n/a")
_name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {
CONF_HOST: discovery_info[CONF_HOST],
CONF_PORT: discovery_info.get(CONF_PORT, DEFAULT_PORT),
@@ -152,7 +146,6 @@ async def async_step_user_gateway(self, user_input=None):
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
-
# PLACEHOLDER USB vs Gateway Logic
return await self.async_step_user_gateway()
diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py
index c6ef43af602667..fb8911d6fc7955 100644
--- a/homeassistant/components/plugwise/const.py
+++ b/homeassistant/components/plugwise/const.py
@@ -22,7 +22,6 @@
DEFAULT_TIMEOUT = 60
# Configuration directives
-CONF_BASE = "base"
CONF_GAS = "gas"
CONF_MAX_TEMP = "max_temp"
CONF_MIN_TEMP = "min_temp"
diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py
index a0bf23986bdb6f..70a4a822431b7f 100644
--- a/homeassistant/components/plugwise/gateway.py
+++ b/homeassistant/components/plugwise/gateway.py
@@ -1,9 +1,9 @@
"""Plugwise platform for Home Assistant Core."""
+from __future__ import annotations
import asyncio
from datetime import timedelta
import logging
-from typing import Dict
import async_timeout
from plugwise.exceptions import (
@@ -103,16 +103,12 @@ async def async_update_data():
update_interval=update_interval,
)
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
api.get_all_devices()
- if entry.unique_id is None:
- if api.smile_version[0] != "1.8.0":
- hass.config_entries.async_update_entry(entry, unique_id=api.smile_hostname)
+ if entry.unique_id is None and api.smile_version[0] != "1.8.0":
+ hass.config_entries.async_update_entry(entry, unique_id=api.smile_hostname)
undo_listener = entry.add_update_listener(_update_listener)
@@ -139,9 +135,9 @@ async def async_update_data():
if single_master_thermostat is None:
platforms = SENSOR_PLATFORMS
- for component in platforms:
+ for platform in platforms:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -160,8 +156,8 @@ async def async_unload_entry_gw(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS_GATEWAY
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS_GATEWAY
]
)
)
@@ -201,9 +197,8 @@ def name(self):
return self._name
@property
- def device_info(self) -> Dict[str, any]:
+ def device_info(self) -> dict[str, any]:
"""Return the device information."""
-
device_information = {
"identifiers": {(DOMAIN, self._dev_id)},
"name": self._entity_name,
diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py
index f57ff2b2a91b21..4152f9fdabddbf 100644
--- a/homeassistant/components/plugwise/sensor.py
+++ b/homeassistant/components/plugwise/sensor.py
@@ -2,6 +2,7 @@
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_ILLUMINANCE,
@@ -17,7 +18,6 @@
VOLUME_CUBIC_METERS,
)
from homeassistant.core import callback
-from homeassistant.helpers.entity import Entity
from .const import (
COOL_ICON,
@@ -236,7 +236,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities, True)
-class SmileSensor(SmileGateway):
+class SmileSensor(SmileGateway, SensorEntity):
"""Represent Smile Sensors."""
def __init__(self, api, coordinator, name, dev_id, sensor):
@@ -282,7 +282,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
-class PwThermostatSensor(SmileSensor, Entity):
+class PwThermostatSensor(SmileSensor):
"""Thermostat (or generic) sensor devices."""
def __init__(self, api, coordinator, name, dev_id, sensor, sensor_type):
@@ -311,7 +311,7 @@ def _async_process_data(self):
self.async_write_ha_state()
-class PwAuxDeviceSensor(SmileSensor, Entity):
+class PwAuxDeviceSensor(SmileSensor):
"""Auxiliary Device Sensors."""
def __init__(self, api, coordinator, name, dev_id, sensor):
@@ -348,7 +348,7 @@ def _async_process_data(self):
self.async_write_ha_state()
-class PwPowerSensor(SmileSensor, Entity):
+class PwPowerSensor(SmileSensor):
"""Power sensor entities."""
def __init__(self, api, coordinator, name, dev_id, sensor, sensor_type, model):
diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json
index 2282e3584fc99c..4d01be82b6a5d0 100644
--- a/homeassistant/components/plugwise/translations/de.json
+++ b/homeassistant/components/plugwise/translations/de.json
@@ -4,7 +4,8 @@
"already_configured": "Service ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"flow_title": "Smile: {name}",
@@ -13,10 +14,13 @@
"data": {
"flow_type": "Verbindungstyp"
},
- "description": "Details"
+ "description": "Details",
+ "title": "Plugwise Typ"
},
"user_gateway": {
"data": {
+ "host": "IP-Adresse",
+ "password": "Smile ID",
"port": "Port"
},
"description": "Bitte eingeben"
diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json
index 2fdc3502571439..f89c850913627c 100644
--- a/homeassistant/components/plugwise/translations/fr.json
+++ b/homeassistant/components/plugwise/translations/fr.json
@@ -21,7 +21,8 @@
"data": {
"host": "Adresse IP",
"password": "ID Smile",
- "port": "Port"
+ "port": "Port",
+ "username": "Nom d'utilisateur de sourire"
},
"description": "Veuillez saisir :",
"title": "Se connecter \u00e0 Smile"
diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json
index 1dcdb7fe5afb52..d6d9012c21ac35 100644
--- a/homeassistant/components/plugwise/translations/hu.json
+++ b/homeassistant/components/plugwise/translations/hu.json
@@ -1,13 +1,31 @@
{
"config": {
+ "abort": {
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "flow_title": "Smile: {name}",
"step": {
"user": {
"data": {
"flow_type": "Kapcsolat t\u00edpusa"
- }
+ },
+ "description": "Term\u00e9k:",
+ "title": "Plugwise t\u00edpus"
},
"user_gateway": {
- "description": "K\u00e9rj\u00fck, adja meg"
+ "data": {
+ "host": "IP c\u00edm",
+ "password": "Smile azonos\u00edt\u00f3",
+ "port": "Port",
+ "username": "Smile Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "description": "K\u00e9rj\u00fck, adja meg",
+ "title": "Csatlakoz\u00e1s a Smile-hoz"
}
}
}
diff --git a/homeassistant/components/plugwise/translations/id.json b/homeassistant/components/plugwise/translations/id.json
new file mode 100644
index 00000000000000..9047bf477bd7c7
--- /dev/null
+++ b/homeassistant/components/plugwise/translations/id.json
@@ -0,0 +1,42 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "flow_title": "Smile: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "flow_type": "Jenis koneksi"
+ },
+ "description": "Produk:",
+ "title": "Jenis Plugwise"
+ },
+ "user_gateway": {
+ "data": {
+ "host": "Alamat IP",
+ "password": "ID Smile",
+ "port": "Port",
+ "username": "Nama Pengguna Smile"
+ },
+ "description": "Masukkan",
+ "title": "Hubungkan ke Smile"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Interval Pindai (detik)"
+ },
+ "description": "Sesuaikan Opsi Plugwise"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/ko.json b/homeassistant/components/plugwise/translations/ko.json
index 4af12098b7f33b..7d856d1fe2873d 100644
--- a/homeassistant/components/plugwise/translations/ko.json
+++ b/homeassistant/components/plugwise/translations/ko.json
@@ -1,18 +1,31 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 Smile \uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694",
- "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. 8\uc790\uc758 Smile ID \ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"flow_title": "Smile: {name}",
"step": {
"user": {
- "description": "\uc138\ubd80 \uc815\ubcf4",
- "title": "Smile \uc5d0 \uc5f0\uacb0\ud558\uae30"
+ "data": {
+ "flow_type": "\uc5f0\uacb0 \uc720\ud615"
+ },
+ "description": "\uc81c\ud488:",
+ "title": "Plugwise \uc720\ud615"
+ },
+ "user_gateway": {
+ "data": {
+ "host": "IP \uc8fc\uc18c",
+ "password": "Smile ID",
+ "port": "\ud3ec\ud2b8",
+ "username": "Smile \uc0ac\uc6a9\uc790 \uc774\ub984"
+ },
+ "description": "\uc785\ub825\ud574\uc8fc\uc138\uc694",
+ "title": "Smile\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
},
@@ -22,7 +35,7 @@
"data": {
"scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ucd08)"
},
- "description": "Plugwise \uc635\uc158 \uc870\uc815"
+ "description": "Plugwise \uc635\uc158 \uc870\uc815\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/plugwise/translations/lb.json b/homeassistant/components/plugwise/translations/lb.json
index 4ce9f8b0145c97..a3618bc911eb92 100644
--- a/homeassistant/components/plugwise/translations/lb.json
+++ b/homeassistant/components/plugwise/translations/lb.json
@@ -21,7 +21,8 @@
"data": {
"host": "IP Adress",
"password": "Smile ID",
- "port": "Port"
+ "port": "Port",
+ "username": "Smile Benotzernumm"
},
"title": "Mam Smile verbannen"
}
diff --git a/homeassistant/components/plugwise/translations/nl.json b/homeassistant/components/plugwise/translations/nl.json
index 001bbbe6d5e57f..af77f6f15e14d3 100644
--- a/homeassistant/components/plugwise/translations/nl.json
+++ b/homeassistant/components/plugwise/translations/nl.json
@@ -4,24 +4,27 @@
"already_configured": "De service is al geconfigureerd"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
- "invalid_auth": "Ongeldige authenticatie, controleer de 8 karakters van uw Smile-ID",
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
+ "flow_title": "Glimlach: {name}",
"step": {
"user": {
"data": {
"flow_type": "Verbindingstype"
},
- "description": "Details",
- "title": "Maak verbinding met de Smile"
+ "description": "Product:",
+ "title": "Plugwise type"
},
"user_gateway": {
"data": {
"host": "IP-adres",
"password": "Smile-ID",
- "port": "Poort"
+ "port": "Poort",
+ "username": "Smile Gebruikersnaam"
},
+ "description": "Voer in",
"title": "Maak verbinding met de Smile"
}
}
@@ -31,7 +34,8 @@
"init": {
"data": {
"scan_interval": "Scaninterval (seconden)"
- }
+ },
+ "description": "Plugwise opties aanpassen"
}
}
}
diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json
index 8a59d492e66ac3..f027ebfc772472 100644
--- a/homeassistant/components/plugwise/translations/ru.json
+++ b/homeassistant/components/plugwise/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"flow_title": "Smile: {name}",
@@ -22,7 +22,7 @@
"host": "IP-\u0430\u0434\u0440\u0435\u0441",
"password": "Smile ID",
"port": "\u041f\u043e\u0440\u0442",
- "username": "\u041b\u043e\u0433\u0438\u043d Smile"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f Smile"
},
"description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435:",
"title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Smile"
diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json
index d25f1975cf7745..60d6b1f92be30e 100644
--- a/homeassistant/components/plugwise/translations/tr.json
+++ b/homeassistant/components/plugwise/translations/tr.json
@@ -1,10 +1,23 @@
{
"config": {
+ "abort": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "flow_title": "Smile: {name}",
"step": {
"user_gateway": {
"data": {
+ "host": "\u0130p Adresi",
+ "password": "G\u00fcl\u00fcmseme Kimli\u011fi",
+ "port": "Port",
"username": "Smile Kullan\u0131c\u0131 Ad\u0131"
- }
+ },
+ "description": "L\u00fctfen girin"
}
}
}
diff --git a/homeassistant/components/plugwise/translations/uk.json b/homeassistant/components/plugwise/translations/uk.json
new file mode 100644
index 00000000000000..6c6f54612b1701
--- /dev/null
+++ b/homeassistant/components/plugwise/translations/uk.json
@@ -0,0 +1,42 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "flow_title": "Smile: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "flow_type": "\u0422\u0438\u043f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f"
+ },
+ "description": "\u041f\u0440\u043e\u0434\u0443\u043a\u0442:",
+ "title": "\u0422\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Plugwise"
+ },
+ "user_gateway": {
+ "data": {
+ "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "password": "Smile ID",
+ "port": "\u041f\u043e\u0440\u0442",
+ "username": "\u041b\u043e\u0433\u0456\u043d Smile"
+ },
+ "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c:",
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Smile"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Plugwise"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py
index 2a7ce4497bb4bc..aeabe8634f8599 100644
--- a/homeassistant/components/plum_lightpad/__init__.py
+++ b/homeassistant/components/plum_lightpad/__init__.py
@@ -38,7 +38,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
conf = config[DOMAIN]
- _LOGGER.info("Found Plum Lightpad configuration in config, importing...")
+ _LOGGER.info("Found Plum Lightpad configuration in config, importing")
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
@@ -67,9 +67,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = plum
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
def cleanup(event):
diff --git a/homeassistant/components/plum_lightpad/config_flow.py b/homeassistant/components/plum_lightpad/config_flow.py
index c6261307d135fb..40432810cc5fa2 100644
--- a/homeassistant/components/plum_lightpad/config_flow.py
+++ b/homeassistant/components/plum_lightpad/config_flow.py
@@ -1,6 +1,8 @@
"""Config flow for Plum Lightpad."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from aiohttp import ContentTypeError
from requests.exceptions import ConnectTimeout, HTTPError
@@ -10,7 +12,7 @@
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.typing import ConfigType
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import DOMAIN
from .utils import load_plum
_LOGGER = logging.getLogger(__name__)
@@ -34,8 +36,8 @@ def _show_form(self, errors=None):
)
async def async_step_user(
- self, user_input: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, user_input: ConfigType | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initialized by the user or redirected to by import."""
if not user_input:
return self._show_form()
@@ -58,7 +60,7 @@ async def async_step_user(
)
async def async_step_import(
- self, import_config: Optional[ConfigType]
- ) -> Dict[str, Any]:
+ self, import_config: ConfigType | None
+ ) -> dict[str, Any]:
"""Import a config entry from configuration.yaml."""
return await self.async_step_user(import_config)
diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py
index feacec4492b8b3..90558eb252380c 100644
--- a/homeassistant/components/plum_lightpad/light.py
+++ b/homeassistant/components/plum_lightpad/light.py
@@ -1,6 +1,8 @@
"""Support for Plum Lightpad lights."""
+from __future__ import annotations
+
import asyncio
-from typing import Callable, List
+from typing import Callable
from plumlightpad import Plum
@@ -23,7 +25,7 @@
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity]], None],
+ async_add_entities: Callable[[list[Entity]], None],
) -> None:
"""Set up Plum Lightpad dimmer lights and glow rings."""
diff --git a/homeassistant/components/plum_lightpad/translations/de.json b/homeassistant/components/plum_lightpad/translations/de.json
index accee16a6f5ce8..c94bf9aadabc10 100644
--- a/homeassistant/components/plum_lightpad/translations/de.json
+++ b/homeassistant/components/plum_lightpad/translations/de.json
@@ -1,7 +1,10 @@
{
"config": {
+ "abort": {
+ "already_configured": "Konto wurde bereits konfiguriert"
+ },
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"step": {
"user": {
diff --git a/homeassistant/components/plum_lightpad/translations/hu.json b/homeassistant/components/plum_lightpad/translations/hu.json
index 436e8b1fb7dd75..3a82e0a241ed2a 100644
--- a/homeassistant/components/plum_lightpad/translations/hu.json
+++ b/homeassistant/components/plum_lightpad/translations/hu.json
@@ -2,6 +2,17 @@
"config": {
"abort": {
"already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "E-mail"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/plum_lightpad/translations/id.json b/homeassistant/components/plum_lightpad/translations/id.json
new file mode 100644
index 00000000000000..0d2f98d1faa927
--- /dev/null
+++ b/homeassistant/components/plum_lightpad/translations/id.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Email"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plum_lightpad/translations/ko.json b/homeassistant/components/plum_lightpad/translations/ko.json
index 008177f1cec336..be71285cbb7354 100644
--- a/homeassistant/components/plum_lightpad/translations/ko.json
+++ b/homeassistant/components/plum_lightpad/translations/ko.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
diff --git a/homeassistant/components/plum_lightpad/translations/nl.json b/homeassistant/components/plum_lightpad/translations/nl.json
index 7f0f85b7326aa7..8410cabbbb92c1 100644
--- a/homeassistant/components/plum_lightpad/translations/nl.json
+++ b/homeassistant/components/plum_lightpad/translations/nl.json
@@ -9,7 +9,8 @@
"step": {
"user": {
"data": {
- "password": "Wachtwoord"
+ "password": "Wachtwoord",
+ "username": "E-mail"
}
}
}
diff --git a/homeassistant/components/plum_lightpad/translations/tr.json b/homeassistant/components/plum_lightpad/translations/tr.json
new file mode 100644
index 00000000000000..f0dab20775fbc4
--- /dev/null
+++ b/homeassistant/components/plum_lightpad/translations/tr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "E-posta"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plum_lightpad/translations/uk.json b/homeassistant/components/plum_lightpad/translations/uk.json
new file mode 100644
index 00000000000000..96b14f793751d5
--- /dev/null
+++ b/homeassistant/components/plum_lightpad/translations/uk.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/pocketcasts/sensor.py b/homeassistant/components/pocketcasts/sensor.py
index 19f7e26543894c..55ae4a524fc1ca 100644
--- a/homeassistant/components/pocketcasts/sensor.py
+++ b/homeassistant/components/pocketcasts/sensor.py
@@ -5,10 +5,9 @@
from pycketcasts import pocketcasts
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -37,7 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return False
-class PocketCastsSensor(Entity):
+class PocketCastsSensor(SensorEntity):
"""Representation of a pocket casts sensor."""
def __init__(self, api):
diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py
index 13dce13c0dacea..e5c209004de14a 100644
--- a/homeassistant/components/point/__init__.py
+++ b/homeassistant/components/point/__init__.py
@@ -39,6 +39,8 @@
DATA_CONFIG_ENTRY_LOCK = "point_config_entry_lock"
CONFIG_ENTRY_IS_SETUP = "point_config_entry_is_setup"
+PLATFORMS = ["binary_sensor", "sensor"]
+
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
@@ -137,8 +139,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
session = hass.data[DOMAIN].pop(entry.entry_id)
await session.remove_webhook()
- for component in ("binary_sensor", "sensor"):
- await hass.config_entries.async_forward_entry_unload(entry, component)
+ for platform in PLATFORMS:
+ await hass.config_entries.async_forward_entry_unload(entry, platform)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)
@@ -186,18 +188,18 @@ async def _sync(self):
async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY)
return
- async def new_device(device_id, component):
+ async def new_device(device_id, platform):
"""Load new device."""
- config_entries_key = f"{component}.{DOMAIN}"
+ config_entries_key = f"{platform}.{DOMAIN}"
async with self._hass.data[DATA_CONFIG_ENTRY_LOCK]:
if config_entries_key not in self._hass.data[CONFIG_ENTRY_IS_SETUP]:
await self._hass.config_entries.async_forward_entry_setup(
- self._config_entry, component
+ self._config_entry, platform
)
self._hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key)
async_dispatcher_send(
- self._hass, POINT_DISCOVERY_NEW.format(component, DOMAIN), device_id
+ self._hass, POINT_DISCOVERY_NEW.format(platform, DOMAIN), device_id
)
self._is_available = True
@@ -207,8 +209,8 @@ async def new_device(device_id, component):
self._known_homes.add(home_id)
for device in self._client.devices:
if device.device_id not in self._known_devices:
- for component in ("sensor", "binary_sensor"):
- await new_device(device.device_id, component)
+ for platform in PLATFORMS:
+ await new_device(device.device_id, platform)
self._known_devices.add(device.device_id)
async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY)
@@ -294,7 +296,7 @@ def device_id(self):
return self._id
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return status of device."""
attrs = self.device.device_status
attrs["last_heard_from"] = as_local(self.last_update).strftime(
diff --git a/homeassistant/components/point/binary_sensor.py b/homeassistant/components/point/binary_sensor.py
index d82ecd096ee2c8..0bcd2a33a2ed76 100644
--- a/homeassistant/components/point/binary_sensor.py
+++ b/homeassistant/components/point/binary_sensor.py
@@ -1,8 +1,16 @@
"""Support for Minut Point binary sensors."""
import logging
+from pypoint import EVENTS
+
from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_COLD,
DEVICE_CLASS_CONNECTIVITY,
+ DEVICE_CLASS_HEAT,
+ DEVICE_CLASS_MOISTURE,
+ DEVICE_CLASS_MOTION,
+ DEVICE_CLASS_SOUND,
DOMAIN,
BinarySensorEntity,
)
@@ -14,37 +22,22 @@
_LOGGER = logging.getLogger(__name__)
-EVENTS = {
- "battery": ("battery_low", ""), # On means low, Off means normal
- "button_press": ( # On means the button was pressed, Off means normal
- "short_button_press",
- "",
- ),
- "cold": ( # On means cold, Off means normal
- "temperature_low",
- "temperature_risen_normal",
- ),
- "connectivity": ( # On means connected, Off means disconnected
- "device_online",
- "device_offline",
- ),
- "dry": ( # On means too dry, Off means normal
- "humidity_low",
- "humidity_risen_normal",
- ),
- "heat": ( # On means hot, Off means normal
- "temperature_high",
- "temperature_dropped_normal",
- ),
- "moisture": ( # On means wet, Off means dry
- "humidity_high",
- "humidity_dropped_normal",
- ),
- "sound": ( # On means sound detected, Off means no sound (clear)
- "avg_sound_high",
- "sound_level_dropped_normal",
- ),
- "tamper": ("tamper", ""), # On means the point was removed or attached
+
+DEVICES = {
+ "alarm": {"icon": "mdi:alarm-bell"},
+ "battery": {"device_class": DEVICE_CLASS_BATTERY},
+ "button_press": {"icon": "mdi:gesture-tap-button"},
+ "cold": {"device_class": DEVICE_CLASS_COLD},
+ "connectivity": {"device_class": DEVICE_CLASS_CONNECTIVITY},
+ "dry": {"icon": "mdi:water"},
+ "glass": {"icon": "mdi:window-closed-variant"},
+ "heat": {"device_class": DEVICE_CLASS_HEAT},
+ "moisture": {"device_class": DEVICE_CLASS_MOISTURE},
+ "motion": {"device_class": DEVICE_CLASS_MOTION},
+ "noise": {"icon": "mdi:volume-high"},
+ "sound": {"device_class": DEVICE_CLASS_SOUND},
+ "tamper_old": {"icon": "mdi:shield-alert"},
+ "tamper": {"icon": "mdi:shield-alert"},
}
@@ -56,8 +49,9 @@ async def async_discover_sensor(device_id):
client = hass.data[POINT_DOMAIN][config_entry.entry_id]
async_add_entities(
(
- MinutPointBinarySensor(client, device_id, device_class)
- for device_class in EVENTS
+ MinutPointBinarySensor(client, device_id, device_name)
+ for device_name in DEVICES
+ if device_name in EVENTS
),
True,
)
@@ -70,12 +64,16 @@ async def async_discover_sensor(device_id):
class MinutPointBinarySensor(MinutPointEntity, BinarySensorEntity):
"""The platform class required by Home Assistant."""
- def __init__(self, point_client, device_id, device_class):
+ def __init__(self, point_client, device_id, device_name):
"""Initialize the binary sensor."""
- super().__init__(point_client, device_id, device_class)
-
+ super().__init__(
+ point_client,
+ device_id,
+ DEVICES[device_name].get("device_class"),
+ )
+ self._device_name = device_name
self._async_unsub_hook_dispatcher_connect = None
- self._events = EVENTS[device_class]
+ self._events = EVENTS[device_name]
self._is_on = None
async def async_added_to_hass(self):
@@ -124,3 +122,18 @@ def is_on(self):
# connectivity is the other way around.
return not self._is_on
return self._is_on
+
+ @property
+ def name(self):
+ """Return the display name of this device."""
+ return f"{self._name} {self._device_name.capitalize()}"
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend, if any."""
+ return DEVICES[self._device_name].get("icon")
+
+ @property
+ def unique_id(self):
+ """Return the unique id of the sensor."""
+ return f"point.{self._id}-{self._device_name}"
diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py
index aaefc45bc9cc68..1f3cf2a751d084 100644
--- a/homeassistant/components/point/config_flow.py
+++ b/homeassistant/components/point/config_flow.py
@@ -40,7 +40,7 @@ def register_flow_implementation(hass, domain, client_id, client_secret):
}
-@config_entries.HANDLERS.register("point")
+@config_entries.HANDLERS.register(DOMAIN)
class PointFlowHandler(config_entries.ConfigFlow):
"""Handle a config flow."""
diff --git a/homeassistant/components/point/manifest.json b/homeassistant/components/point/manifest.json
index 6c25cdef91a2aa..899e5615b40617 100644
--- a/homeassistant/components/point/manifest.json
+++ b/homeassistant/components/point/manifest.json
@@ -3,7 +3,7 @@
"name": "Minut Point",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/point",
- "requirements": ["pypoint==2.0.0"],
+ "requirements": ["pypoint==2.1.0"],
"dependencies": ["webhook", "http"],
"codeowners": ["@fredrike"],
"quality_scale": "gold"
diff --git a/homeassistant/components/point/sensor.py b/homeassistant/components/point/sensor.py
index 87f9a8ab2fe2f8..338ed275f50fa0 100644
--- a/homeassistant/components/point/sensor.py
+++ b/homeassistant/components/point/sensor.py
@@ -1,7 +1,7 @@
"""Support for Minut Point sensors."""
import logging
-from homeassistant.components.sensor import DOMAIN
+from homeassistant.components.sensor import DOMAIN, SensorEntity
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PRESSURE,
@@ -47,7 +47,7 @@ async def async_discover_sensor(device_id):
)
-class MinutPointSensor(MinutPointEntity):
+class MinutPointSensor(MinutPointEntity, SensorEntity):
"""The platform class required by Home Assistant."""
def __init__(self, point_client, device_id, device_class):
diff --git a/homeassistant/components/point/translations/de.json b/homeassistant/components/point/translations/de.json
index 8ee83eab727631..c36c7a44b51aa7 100644
--- a/homeassistant/components/point/translations/de.json
+++ b/homeassistant/components/point/translations/de.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_setup": "Du kannst nur ein Point-Konto konfigurieren.",
+ "already_setup": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.",
"authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL.",
"authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.",
"external_setup": "Pointt erfolgreich von einem anderen Flow konfiguriert.",
- "no_flows": "Du m\u00fcsst Point konfigurieren, bevor du dich damit authentifizieren kannst. [Bitte lese die Anweisungen] (https://www.home-assistant.io/components/point/).",
+ "no_flows": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.",
"unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten"
},
"create_entry": {
@@ -17,15 +17,15 @@
},
"step": {
"auth": {
- "description": "Folge dem Link unten und Akzeptiere Zugriff auf dei Minut-Konto. Kehre dann zur\u00fcck und dr\u00fccke unten auf Senden . \n\n [Link]({authorization_url})",
+ "description": "Folge dem Link unten und **Best\u00e4tige** den Zugriff auf dein Minut-Konto. Kehre dann zur\u00fcck und dr\u00fccke unten auf **Senden**. \n\n [Link]({authorization_url})",
"title": "Point authentifizieren"
},
"user": {
"data": {
"flow_impl": "Anbieter"
},
- "description": "W\u00e4hle \u00fcber welchen Authentifizierungsanbieter du sich mit Point authentifizieren m\u00f6chtest.",
- "title": "Authentifizierungsanbieter"
+ "description": "M\u00f6chten Sie mit der Einrichtung beginnen?",
+ "title": "W\u00e4hle die Authentifizierungsmethode"
}
}
}
diff --git a/homeassistant/components/point/translations/en.json b/homeassistant/components/point/translations/en.json
index 50e9e4f3ce04bf..685a16cbbf5c8a 100644
--- a/homeassistant/components/point/translations/en.json
+++ b/homeassistant/components/point/translations/en.json
@@ -6,7 +6,7 @@
"authorize_url_timeout": "Timeout generating authorize URL.",
"external_setup": "Point successfully configured from another flow.",
"no_flows": "The component is not configured. Please follow the documentation.",
- "unknown_authorize_url_generation": "Unknown error generating an authorize url."
+ "unknown_authorize_url_generation": "Unknown error generating an authorize URL."
},
"create_entry": {
"default": "Successfully authenticated"
diff --git a/homeassistant/components/point/translations/fr.json b/homeassistant/components/point/translations/fr.json
index 141af3545ba491..ab9cd7af34e8b3 100644
--- a/homeassistant/components/point/translations/fr.json
+++ b/homeassistant/components/point/translations/fr.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.",
"authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.",
"external_setup": "Point correctement configur\u00e9 \u00e0 partir d\u2019un autre flux.",
- "no_flows": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation."
+ "no_flows": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.",
+ "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation."
},
"create_entry": {
"default": "Authentification r\u00e9ussie"
diff --git a/homeassistant/components/point/translations/hu.json b/homeassistant/components/point/translations/hu.json
index c31e2c55e6aff3..7f4346a6ea9e62 100644
--- a/homeassistant/components/point/translations/hu.json
+++ b/homeassistant/components/point/translations/hu.json
@@ -4,7 +4,8 @@
"already_setup": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.",
"authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.",
"authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.",
- "no_flows": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t."
+ "no_flows": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.",
+ "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n."
},
"create_entry": {
"default": "Sikeres hiteles\u00edt\u00e9s"
@@ -15,7 +16,7 @@
},
"step": {
"auth": {
- "description": "K\u00e9rlek k\u00f6vesd az al\u00e1bbi linket \u00e9s a Fogadd el a hozz\u00e1f\u00e9r\u00e9st a Minut fi\u00f3kj\u00e1hoz, majd t\u00e9rj vissza \u00e9s nyomd meg a K\u00fcld\u00e9s gombot. \n\n [Link] ( {authorization_url} )",
+ "description": "K\u00e9rlek k\u00f6vesd az al\u00e1bbi linket \u00e9s a **Fogadd el** a hozz\u00e1f\u00e9r\u00e9st a Minut fi\u00f3kj\u00e1hoz, majd t\u00e9rj vissza \u00e9s nyomd meg a **K\u00fcld\u00e9s ** gombot. \n\n [Link]({authorization_url})",
"title": "Point hiteles\u00edt\u00e9se"
},
"user": {
diff --git a/homeassistant/components/point/translations/id.json b/homeassistant/components/point/translations/id.json
new file mode 100644
index 00000000000000..868321d74693f8
--- /dev/null
+++ b/homeassistant/components/point/translations/id.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.",
+ "authorize_url_fail": "Kesalahan tidak dikenal terjadi ketika menghasilkan URL otorisasi.",
+ "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.",
+ "external_setup": "Point berhasil dikonfigurasi dari alur konfigurasi lainnya.",
+ "no_flows": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.",
+ "unknown_authorize_url_generation": "Kesalahan tidak dikenal ketika menghasilkan URL otorisasi."
+ },
+ "create_entry": {
+ "default": "Berhasil diautentikasi"
+ },
+ "error": {
+ "follow_link": "Buka tautan dan autentikasi sebelum menekan Kirim",
+ "no_token": "Token akses tidak valid"
+ },
+ "step": {
+ "auth": {
+ "description": "Buka tautan di bawah ini dan **Terima** akses ke akun Minut Anda, lalu kembali dan tekan tombol **Kirim** di bawah ini.\n\n[Tautan] ({authorization_url})",
+ "title": "Autentikasi Point"
+ },
+ "user": {
+ "data": {
+ "flow_impl": "Penyedia"
+ },
+ "description": "Ingin memulai penyiapan?",
+ "title": "Pilih Metode Autentikasi"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/point/translations/ko.json b/homeassistant/components/point/translations/ko.json
index 6ea58e95834a24..5813dbba137264 100644
--- a/homeassistant/components/point/translations/ko.json
+++ b/homeassistant/components/point/translations/ko.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "already_setup": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.",
- "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
+ "already_setup": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "authorize_url_fail": "\uc778\uc99d URL\uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
"authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "external_setup": "Point \uac00 \ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "no_flows": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694."
+ "external_setup": "Point\uac00 \ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "no_flows": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
+ "unknown_authorize_url_generation": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4."
},
"create_entry": {
"default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
diff --git a/homeassistant/components/point/translations/nl.json b/homeassistant/components/point/translations/nl.json
index a257ba3e1115db..8447ac6bbb2c35 100644
--- a/homeassistant/components/point/translations/nl.json
+++ b/homeassistant/components/point/translations/nl.json
@@ -1,30 +1,31 @@
{
"config": {
"abort": {
- "already_setup": "U kunt alleen een Point-account configureren.",
+ "already_setup": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.",
"authorize_url_fail": "Onbekende fout bij het genereren van een autoriseer-URL.",
"authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
"external_setup": "Punt succesvol geconfigureerd vanuit een andere stroom.",
- "no_flows": "U moet Point configureren voordat u zich ermee kunt verifi\u00ebren. [Gelieve de instructies te lezen](https://www.home-assistant.io/components/nest/)."
+ "no_flows": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.",
+ "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL."
},
"create_entry": {
- "default": "Succesvol geverifieerd met Minut voor uw Point appara(a)t(en)"
+ "default": "Succesvol geauthenticeerd"
},
"error": {
"follow_link": "Volg de link en verifieer voordat je op Verzenden klikt",
- "no_token": "Niet geverifieerd met Minut"
+ "no_token": "Ongeldig toegangstoken"
},
"step": {
"auth": {
- "description": "Ga naar onderstaande link en Accepteer toegang tot je Minut account, kom dan hier terug en klik op Verzenden hier onder.\n\n[Link]({authorization_url})",
+ "description": "Ga naar onderstaande link en **Accepteer** toegang tot je Minut account, kom dan hier terug en klik op **Verzenden** hier onder.\n\n[Link]({authorization_url})",
"title": "Verificatie Point"
},
"user": {
"data": {
"flow_impl": "Leverancier"
},
- "description": "Kies met welke authenticatieprovider u wilt authenticeren met Point.",
- "title": "Authenticatieleverancier"
+ "description": "Wil je beginnen met instellen?",
+ "title": "Kies een authenticatie methode"
}
}
}
diff --git a/homeassistant/components/point/translations/no.json b/homeassistant/components/point/translations/no.json
index d0d0b9114fb555..a72a8083f6fb10 100644
--- a/homeassistant/components/point/translations/no.json
+++ b/homeassistant/components/point/translations/no.json
@@ -2,11 +2,11 @@
"config": {
"abort": {
"already_setup": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.",
- "authorize_url_fail": "Ukjent feil ved generering av godkjenningsadresse",
+ "authorize_url_fail": "Ukjent feil under generering av en autoriserings-URL.",
"authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse",
"external_setup": "Punktet er konfigurert fra en annen flyt.",
"no_flows": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen",
- "unknown_authorize_url_generation": "Ukjent feil ved generering av godkjenningsadresse"
+ "unknown_authorize_url_generation": "Ukjent feil under generering av en autoriserings-URL."
},
"create_entry": {
"default": "Vellykket godkjenning"
diff --git a/homeassistant/components/point/translations/tr.json b/homeassistant/components/point/translations/tr.json
new file mode 100644
index 00000000000000..5a4849fad0726e
--- /dev/null
+++ b/homeassistant/components/point/translations/tr.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.",
+ "unknown_authorize_url_generation": "Yetkilendirme url'si olu\u015fturulurken bilinmeyen hata."
+ },
+ "error": {
+ "no_token": "Eri\u015fim Belirteci"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/point/translations/uk.json b/homeassistant/components/point/translations/uk.json
new file mode 100644
index 00000000000000..6b66a39a291767
--- /dev/null
+++ b/homeassistant/components/point/translations/uk.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.",
+ "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "external_setup": "Point \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439 \u0437 \u0456\u043d\u0448\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0443.",
+ "no_flows": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.",
+ "unknown_authorize_url_generation": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457."
+ },
+ "create_entry": {
+ "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e."
+ },
+ "error": {
+ "follow_link": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c \u0456 \u043f\u0440\u043e\u0439\u0434\u0456\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e, \u043f\u0435\u0440\u0448 \u043d\u0456\u0436 \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0438 \"\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438\".",
+ "no_token": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443."
+ },
+ "step": {
+ "auth": {
+ "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 [\u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c]({authorization_url}) \u0456 ** \u0414\u043e\u0437\u0432\u043e\u043b\u044c\u0442\u0435 ** \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0432\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Minut, \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0435\u0440\u043d\u0456\u0442\u044c\u0441\u044f \u0441\u044e\u0434\u0438 \u0442\u0430 \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **.",
+ "title": "Minut Point"
+ },
+ "user": {
+ "data": {
+ "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440"
+ },
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?",
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/point/translations/zh-Hant.json b/homeassistant/components/point/translations/zh-Hant.json
index 710d363f771383..2bb1a8fc23901e 100644
--- a/homeassistant/components/point/translations/zh-Hant.json
+++ b/homeassistant/components/point/translations/zh-Hant.json
@@ -13,7 +13,7 @@
},
"error": {
"follow_link": "\u8acb\u65bc\u50b3\u9001\u524d\uff0c\u5148\u4f7f\u7528\u9023\u7d50\u4e26\u9032\u884c\u8a8d\u8b49\u3002",
- "no_token": "\u5b58\u53d6\u5bc6\u9470\u7121\u6548"
+ "no_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548"
},
"step": {
"auth": {
diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py
index 315816f4e1c107..cfc2abb031642a 100644
--- a/homeassistant/components/poolsense/__init__.py
+++ b/homeassistant/components/poolsense/__init__.py
@@ -10,7 +10,6 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
@@ -49,16 +48,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
coordinator = PoolSenseDataUpdateCoordinator(hass, entry)
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = coordinator
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -69,8 +65,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -118,7 +114,7 @@ async def _async_update_data(self):
try:
data = await self.poolsense.get_poolsense_data()
except (PoolSenseError) as error:
- _LOGGER.error("PoolSense query did not complete.")
+ _LOGGER.error("PoolSense query did not complete")
raise UpdateFailed(error) from error
return data
diff --git a/homeassistant/components/poolsense/config_flow.py b/homeassistant/components/poolsense/config_flow.py
index b9c73ace3fc32d..653ba026ebf651 100644
--- a/homeassistant/components/poolsense/config_flow.py
+++ b/homeassistant/components/poolsense/config_flow.py
@@ -8,7 +8,7 @@
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers import aiohttp_client
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/poolsense/sensor.py b/homeassistant/components/poolsense/sensor.py
index a64cc0aef61e57..ca79fde6b08262 100644
--- a/homeassistant/components/poolsense/sensor.py
+++ b/homeassistant/components/poolsense/sensor.py
@@ -1,4 +1,5 @@
"""Sensor platform for the PoolSense sensor."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_EMAIL,
@@ -8,7 +9,6 @@
PERCENTAGE,
TEMP_CELSIUS,
)
-from homeassistant.helpers.entity import Entity
from . import PoolSenseEntity
from .const import ATTRIBUTION, DOMAIN
@@ -79,7 +79,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors_list, False)
-class PoolSenseSensor(PoolSenseEntity, Entity):
+class PoolSenseSensor(PoolSenseEntity, SensorEntity):
"""Sensor representing poolsense data."""
@property
@@ -108,6 +108,6 @@ def unit_of_measurement(self):
return SENSORS[self.info_type]["unit"]
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/poolsense/translations/de.json b/homeassistant/components/poolsense/translations/de.json
index c7dfe6d02b211f..dc569c2d9ad8d9 100644
--- a/homeassistant/components/poolsense/translations/de.json
+++ b/homeassistant/components/poolsense/translations/de.json
@@ -3,13 +3,17 @@
"abort": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
+ "error": {
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
+ },
"step": {
"user": {
"data": {
"email": "E-Mail",
"password": "Passwort"
},
- "description": "Wollen Sie mit der Einrichtung beginnen?"
+ "description": "M\u00f6chten Sie mit der Einrichtung beginnen?",
+ "title": ""
}
}
}
diff --git a/homeassistant/components/poolsense/translations/hu.json b/homeassistant/components/poolsense/translations/hu.json
index 3b2d79a34a77e2..80562b34e2830f 100644
--- a/homeassistant/components/poolsense/translations/hu.json
+++ b/homeassistant/components/poolsense/translations/hu.json
@@ -2,6 +2,19 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-mail",
+ "password": "Jelsz\u00f3"
+ },
+ "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?",
+ "title": "PoolSense"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/poolsense/translations/id.json b/homeassistant/components/poolsense/translations/id.json
new file mode 100644
index 00000000000000..6e40f5f0925367
--- /dev/null
+++ b/homeassistant/components/poolsense/translations/id.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Email",
+ "password": "Kata Sandi"
+ },
+ "description": "Ingin memulai penyiapan?",
+ "title": "PoolSense"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/poolsense/translations/ko.json b/homeassistant/components/poolsense/translations/ko.json
index 42a6654592f00f..ec8c7dfc90f60e 100644
--- a/homeassistant/components/poolsense/translations/ko.json
+++ b/homeassistant/components/poolsense/translations/ko.json
@@ -12,7 +12,7 @@
"email": "\uc774\uba54\uc77c",
"password": "\ube44\ubc00\ubc88\ud638"
},
- "description": "[%key:common::config_flow::description%]",
+ "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "PoolSense"
}
}
diff --git a/homeassistant/components/poolsense/translations/nl.json b/homeassistant/components/poolsense/translations/nl.json
index 7482a0bbe7c64d..f88d14e297a9e7 100644
--- a/homeassistant/components/poolsense/translations/nl.json
+++ b/homeassistant/components/poolsense/translations/nl.json
@@ -9,8 +9,11 @@
"step": {
"user": {
"data": {
+ "email": "E-mail",
"password": "Wachtwoord"
- }
+ },
+ "description": "Wil je beginnen met instellen?",
+ "title": "PoolSense"
}
}
}
diff --git a/homeassistant/components/poolsense/translations/ru.json b/homeassistant/components/poolsense/translations/ru.json
index 3687b75a6f7373..09c94368cdabb9 100644
--- a/homeassistant/components/poolsense/translations/ru.json
+++ b/homeassistant/components/poolsense/translations/ru.json
@@ -4,7 +4,7 @@
"already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
},
"error": {
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"step": {
"user": {
diff --git a/homeassistant/components/poolsense/translations/tr.json b/homeassistant/components/poolsense/translations/tr.json
new file mode 100644
index 00000000000000..1e2e9d0c5b8de4
--- /dev/null
+++ b/homeassistant/components/poolsense/translations/tr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-posta",
+ "password": "Parola"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/poolsense/translations/uk.json b/homeassistant/components/poolsense/translations/uk.json
new file mode 100644
index 00000000000000..6ac3b97f74167d
--- /dev/null
+++ b/homeassistant/components/poolsense/translations/uk.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?",
+ "title": "PoolSense"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py
index 54b7310b7adb32..ceec56aa05a940 100644
--- a/homeassistant/components/powerwall/__init__.py
+++ b/homeassistant/components/powerwall/__init__.py
@@ -4,10 +4,15 @@
import logging
import requests
-from tesla_powerwall import MissingAttributeError, Powerwall, PowerwallUnreachableError
+from tesla_powerwall import (
+ AccessDeniedError,
+ MissingAttributeError,
+ Powerwall,
+ PowerwallUnreachableError,
+)
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_IP_ADDRESS
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
+from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry
@@ -93,11 +98,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.data[DOMAIN].setdefault(entry_id, {})
http_session = requests.Session()
+
+ password = entry.data.get(CONF_PASSWORD)
power_wall = Powerwall(entry.data[CONF_IP_ADDRESS], http_session=http_session)
try:
- await hass.async_add_executor_job(power_wall.detect_and_pin_version)
- await hass.async_add_executor_job(_fetch_powerwall_data, power_wall)
- powerwall_data = await hass.async_add_executor_job(call_base_info, power_wall)
+ powerwall_data = await hass.async_add_executor_job(
+ _login_and_fetch_base_info, power_wall, password
+ )
except PowerwallUnreachableError as err:
http_session.close()
raise ConfigEntryNotReady from err
@@ -105,6 +112,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
http_session.close()
await _async_handle_api_changed_error(hass, err)
return False
+ except AccessDeniedError as err:
+ _LOGGER.debug("Authentication failed", exc_info=err)
+ http_session.close()
+ _async_start_reauth(hass, entry)
+ return False
await _migrate_old_unique_ids(hass, entry_id, powerwall_data)
@@ -112,22 +124,20 @@ async def async_update_data():
"""Fetch data from API endpoint."""
# Check if we had an error before
_LOGGER.debug("Checking if update failed")
- if not hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED]:
- _LOGGER.debug("Updating data")
- try:
- return await hass.async_add_executor_job(
- _fetch_powerwall_data, power_wall
- )
- except PowerwallUnreachableError as err:
- raise UpdateFailed("Unable to fetch data from powerwall") from err
- except MissingAttributeError as err:
- await _async_handle_api_changed_error(hass, err)
- hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED] = True
- # Returns the cached data. This data can also be None
- return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data
- else:
+ if hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED]:
return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data
+ _LOGGER.debug("Updating data")
+ try:
+ return await _async_update_powerwall_data(hass, entry, power_wall)
+ except AccessDeniedError:
+ if password is None:
+ raise
+
+ # If the session expired, relogin, and try again
+ await hass.async_add_executor_job(power_wall.login, "", password)
+ return await _async_update_powerwall_data(hass, entry, power_wall)
+
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
@@ -146,16 +156,50 @@ async def async_update_data():
}
)
- await coordinator.async_refresh()
+ await coordinator.async_config_entry_first_refresh()
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
+async def _async_update_powerwall_data(
+ hass: HomeAssistant, entry: ConfigEntry, power_wall: Powerwall
+):
+ """Fetch updated powerwall data."""
+ try:
+ return await hass.async_add_executor_job(_fetch_powerwall_data, power_wall)
+ except PowerwallUnreachableError as err:
+ raise UpdateFailed("Unable to fetch data from powerwall") from err
+ except MissingAttributeError as err:
+ await _async_handle_api_changed_error(hass, err)
+ hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED] = True
+ # Returns the cached data. This data can also be None
+ return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data
+
+
+def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry):
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_REAUTH},
+ data=entry.data,
+ )
+ )
+ _LOGGER.error("Password is no longer valid. Please reauthenticate")
+
+
+def _login_and_fetch_base_info(power_wall: Powerwall, password: str):
+ """Login to the powerwall and fetch the base info."""
+ if password is not None:
+ power_wall.login("", password)
+ power_wall.detect_and_pin_version()
+ return call_base_info(power_wall)
+
+
def call_base_info(power_wall):
"""Wrap powerwall properties to be a callable."""
serial_numbers = power_wall.get_serial_numbers()
@@ -184,8 +228,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py
index 37ee2730bb49d1..579c916a15ada3 100644
--- a/homeassistant/components/powerwall/config_flow.py
+++ b/homeassistant/components/powerwall/config_flow.py
@@ -1,19 +1,32 @@
"""Config flow for Tesla Powerwall integration."""
import logging
-from tesla_powerwall import MissingAttributeError, Powerwall, PowerwallUnreachableError
+from tesla_powerwall import (
+ AccessDeniedError,
+ MissingAttributeError,
+ Powerwall,
+ PowerwallUnreachableError,
+)
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
from homeassistant.components.dhcp import IP_ADDRESS
-from homeassistant.const import CONF_IP_ADDRESS
+from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
from homeassistant.core import callback
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
+def _login_and_fetch_site_info(power_wall: Powerwall, password: str):
+ """Login to the powerwall and fetch the base info."""
+ if password is not None:
+ power_wall.login("", password)
+ power_wall.detect_and_pin_version()
+ return power_wall.get_site_info()
+
+
async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.
@@ -21,12 +34,12 @@ async def validate_input(hass: core.HomeAssistant, data):
"""
power_wall = Powerwall(data[CONF_IP_ADDRESS])
+ password = data[CONF_PASSWORD]
try:
- await hass.async_add_executor_job(power_wall.detect_and_pin_version)
- site_info = await hass.async_add_executor_job(power_wall.get_site_info)
- except PowerwallUnreachableError as err:
- raise CannotConnect from err
+ site_info = await hass.async_add_executor_job(
+ _login_and_fetch_site_info, power_wall, password
+ )
except MissingAttributeError as err:
# Only log the exception without the traceback
_LOGGER.error(str(err))
@@ -52,7 +65,6 @@ async def async_step_dhcp(self, dhcp_discovery):
return self.async_abort(reason="already_configured")
self.ip_address = dhcp_discovery[IP_ADDRESS]
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {CONF_IP_ADDRESS: self.ip_address}
return await self.async_step_user()
@@ -62,27 +74,44 @@ async def async_step_user(self, user_input=None):
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)
- except CannotConnect:
- errors["base"] = "cannot_connect"
+ except PowerwallUnreachableError:
+ errors[CONF_IP_ADDRESS] = "cannot_connect"
except WrongVersion:
errors["base"] = "wrong_version"
+ except AccessDeniedError:
+ errors[CONF_PASSWORD] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
- if "base" not in errors:
- await self.async_set_unique_id(user_input[CONF_IP_ADDRESS])
- self._abort_if_unique_id_configured()
+ if not errors:
+ existing_entry = await self.async_set_unique_id(
+ user_input[CONF_IP_ADDRESS]
+ )
+ if existing_entry:
+ self.hass.config_entries.async_update_entry(
+ existing_entry, data=user_input
+ )
+ await self.hass.config_entries.async_reload(existing_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title=info["title"], data=user_input)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
- {vol.Required(CONF_IP_ADDRESS, default=self.ip_address): str}
+ {
+ vol.Required(CONF_IP_ADDRESS, default=self.ip_address): str,
+ vol.Optional(CONF_PASSWORD): str,
+ }
),
errors=errors,
)
+ async def async_step_reauth(self, data):
+ """Handle configuration by re-auth."""
+ self.ip_address = data[CONF_IP_ADDRESS]
+ return await self.async_step_user()
+
@callback
def _async_ip_address_already_configured(self, ip_address):
"""See if we already have an entry matching the ip_address."""
@@ -92,9 +121,5 @@ def _async_ip_address_already_configured(self, ip_address):
return False
-class CannotConnect(exceptions.HomeAssistantError):
- """Error to indicate we cannot connect."""
-
-
class WrongVersion(exceptions.HomeAssistantError):
"""Error to indicate the powerwall uses a software version we cannot interact with."""
diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json
index 6b7b147d3c5d35..40d0a6c50fe117 100644
--- a/homeassistant/components/powerwall/manifest.json
+++ b/homeassistant/components/powerwall/manifest.json
@@ -3,7 +3,7 @@
"name": "Tesla Powerwall",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/powerwall",
- "requirements": ["tesla-powerwall==0.3.3"],
+ "requirements": ["tesla-powerwall==0.3.5"],
"codeowners": ["@bdraco", "@jrester"],
"dhcp": [
{"hostname":"1118431-*","macaddress":"88DA1A*"},
diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py
index 5026d2fb35736b..36f803e66d7a27 100644
--- a/homeassistant/components/powerwall/sensor.py
+++ b/homeassistant/components/powerwall/sensor.py
@@ -3,6 +3,7 @@
from tesla_powerwall import MeterType
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER, PERCENTAGE
from .const import (
@@ -59,7 +60,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities, True)
-class PowerWallChargeSensor(PowerWallEntity):
+class PowerWallChargeSensor(PowerWallEntity, SensorEntity):
"""Representation of an Powerwall charge sensor."""
@property
@@ -88,7 +89,7 @@ def state(self):
return round(self.coordinator.data[POWERWALL_API_CHARGE])
-class PowerWallEnergySensor(PowerWallEntity):
+class PowerWallEnergySensor(PowerWallEntity, SensorEntity):
"""Representation of an Powerwall Energy sensor."""
def __init__(
@@ -136,7 +137,7 @@ def state(self):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
meter = self.coordinator.data[POWERWALL_API_METERS].get_meter(self._meter)
return {
diff --git a/homeassistant/components/powerwall/strings.json b/homeassistant/components/powerwall/strings.json
index ac0d9568154166..5deacd6a8f9d1c 100644
--- a/homeassistant/components/powerwall/strings.json
+++ b/homeassistant/components/powerwall/strings.json
@@ -4,18 +4,22 @@
"step": {
"user": {
"title": "Connect to the powerwall",
+ "description": "The password is usually the last 5 characters of the serial number for Backup Gateway and can be found in the Tesla app or the last 5 characters of the password found inside the door for Backup Gateway 2.",
"data": {
- "ip_address": "[%key:common::config_flow::data::ip%]"
+ "ip_address": "[%key:common::config_flow::data::ip%]",
+ "password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"wrong_version": "Your powerwall uses a software version that is not supported. Please consider upgrading or reporting this issue so it can be resolved.",
- "unknown": "[%key:common::config_flow::error::unknown%]"
+ "unknown": "[%key:common::config_flow::error::unknown%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"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/powerwall/translations/bg.json b/homeassistant/components/powerwall/translations/bg.json
new file mode 100644
index 00000000000000..cef3726d759676
--- /dev/null
+++ b/homeassistant/components/powerwall/translations/bg.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u0430"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/powerwall/translations/ca.json b/homeassistant/components/powerwall/translations/ca.json
index 2c9becb17959e6..8016cd123710a2 100644
--- a/homeassistant/components/powerwall/translations/ca.json
+++ b/homeassistant/components/powerwall/translations/ca.json
@@ -1,18 +1,23 @@
{
"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",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"unknown": "Error inesperat",
"wrong_version": "El teu Powerwall utilitza una versi\u00f3 de programari no compatible. L'hauries d'actualitzar o informar d'aquest problema perqu\u00e8 sigui solucionat."
},
+ "flow_title": "Tesla Powerwall ({ip_address})",
"step": {
"user": {
"data": {
- "ip_address": "Adre\u00e7a IP"
+ "ip_address": "Adre\u00e7a IP",
+ "password": "Contrasenya"
},
+ "description": "La contrasenya normalment s\u00f3n els darrers cinc car\u00e0cters del n\u00famero de s\u00e8rie de la pasarel\u00b7la (backup gateway) i es pot trobar a l'aplicaci\u00f3 de Tesla. Tamb\u00e9 s\u00f3n els darrers 5 car\u00e0cters de la contrasenya que es troba a l'interior de la tapa de la pasarel\u00b7la vers\u00f3 2 (backup gateway 2).",
"title": "Connexi\u00f3 amb el Powerwall"
}
}
diff --git a/homeassistant/components/powerwall/translations/cs.json b/homeassistant/components/powerwall/translations/cs.json
index b64eabcf33bae6..d6e5cd5904b004 100644
--- a/homeassistant/components/powerwall/translations/cs.json
+++ b/homeassistant/components/powerwall/translations/cs.json
@@ -1,17 +1,21 @@
{
"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",
+ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
"unknown": "Neo\u010dek\u00e1van\u00e1 chyba",
"wrong_version": "Powerwall pou\u017e\u00edv\u00e1 verzi softwaru, kter\u00e1 nen\u00ed podporov\u00e1na. Zva\u017ete upgrade nebo nahlaste probl\u00e9m, aby mohl b\u00fdt vy\u0159e\u0161en."
},
+ "flow_title": "Tesla Powerwall ({ip_address})",
"step": {
"user": {
"data": {
- "ip_address": "IP adresa"
+ "ip_address": "IP adresa",
+ "password": "Heslo"
},
"title": "P\u0159ipojen\u00ed k powerwall"
}
diff --git a/homeassistant/components/powerwall/translations/de.json b/homeassistant/components/powerwall/translations/de.json
index f5317e3046ae74..c916152637370b 100644
--- a/homeassistant/components/powerwall/translations/de.json
+++ b/homeassistant/components/powerwall/translations/de.json
@@ -1,16 +1,21 @@
{
"config": {
"abort": {
- "already_configured": "Die Powerwall ist bereits konfiguriert"
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
- "unknown": "Unerwarteter Fehler"
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler",
+ "wrong_version": "Deine Powerwall verwendet eine Softwareversion, die nicht unterst\u00fctzt wird. Bitte ziehe ein Upgrade in Betracht oder melde dieses Problem, damit es behoben werden kann."
},
+ "flow_title": "Tesla Powerwall ({ip_address})",
"step": {
"user": {
"data": {
- "ip_address": "IP-Adresse"
+ "ip_address": "IP-Adresse",
+ "password": "Passwort"
},
"title": "Stellen Sie eine Verbindung zur Powerwall her"
}
diff --git a/homeassistant/components/powerwall/translations/en.json b/homeassistant/components/powerwall/translations/en.json
index ac0d9568154166..06fc09804d92ce 100644
--- a/homeassistant/components/powerwall/translations/en.json
+++ b/homeassistant/components/powerwall/translations/en.json
@@ -1,21 +1,25 @@
{
- "config": {
- "flow_title": "Tesla Powerwall ({ip_address})",
- "step": {
- "user": {
- "title": "Connect to the powerwall",
- "data": {
- "ip_address": "[%key:common::config_flow::data::ip%]"
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured",
+ "reauth_successful": "Re-authentication was successful"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error",
+ "wrong_version": "Your powerwall uses a software version that is not supported. Please consider upgrading or reporting this issue so it can be resolved."
+ },
+ "flow_title": "Tesla Powerwall ({ip_address})",
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "IP Address",
+ "password": "Password"
+ },
+ "description": "The password is usually the last 5 characters of the serial number for Backup Gateway and can be found in the Tesla app or the last 5 characters of the password found inside the door for Backup Gateway 2.",
+ "title": "Connect to the powerwall"
+ }
}
- }
- },
- "error": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "wrong_version": "Your powerwall uses a software version that is not supported. Please consider upgrading or reporting this issue so it can be resolved.",
- "unknown": "[%key:common::config_flow::error::unknown%]"
- },
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
- }
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json
index 76835e814809f8..81e3edab38772d 100644
--- a/homeassistant/components/powerwall/translations/es.json
+++ b/homeassistant/components/powerwall/translations/es.json
@@ -8,11 +8,13 @@
"unknown": "Error inesperado",
"wrong_version": "Tu powerwall utiliza una versi\u00f3n de software que no es compatible. Considera actualizar o informar de este problema para que pueda resolverse."
},
+ "flow_title": "Powerwall de Tesla ({ip_address})",
"step": {
"user": {
"data": {
"ip_address": "Direcci\u00f3n IP"
},
+ "description": "La contrase\u00f1a suele ser los \u00faltimos 5 caracteres del n\u00famero de serie del Backup Gateway y se puede encontrar en la aplicaci\u00f3n Telsa; o los \u00faltimos 5 caracteres de la contrase\u00f1a que se encuentran dentro de la puerta del Backup Gateway 2.",
"title": "Conectarse al powerwall"
}
}
diff --git a/homeassistant/components/powerwall/translations/et.json b/homeassistant/components/powerwall/translations/et.json
index 996b0ea4b309b7..8811b87031699a 100644
--- a/homeassistant/components/powerwall/translations/et.json
+++ b/homeassistant/components/powerwall/translations/et.json
@@ -1,18 +1,23 @@
{
"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": "\u00dchenduse loomine nurjus. Proovi uuesti",
+ "invalid_auth": "Vigane autentimine",
"unknown": "Ootamatu t\u00f5rge",
- "wrong_version": "Teie Powerwall kasutab tarkvaraversiooni, mida ei toetata. Kaaluge tarkvara uuendamist v\u00f5i probleemist teavitamist, et see saaks lahendatud."
+ "wrong_version": "Powerwall kasutab tarkvaraversiooni, mida ei toetata. Kaaluge tarkvara uuendamist v\u00f5i probleemist teavitamist, et see saaks lahendatud."
},
+ "flow_title": "Tesla Powerwall ( {ip_address} )",
"step": {
"user": {
"data": {
- "ip_address": "IP aadress"
+ "ip_address": "IP aadress",
+ "password": "Salas\u00f5na"
},
+ "description": "Parool on tavaliselt Backup Gateway seerianumbri viimased 5 t\u00e4hem\u00e4rki ja selle leiad Tesla rakendusest v\u00f5i Backup Gateway 2 luugilt leitud parooli viimased 5 m\u00e4rki.",
"title": "Powerwalliga \u00fchendamine"
}
}
diff --git a/homeassistant/components/powerwall/translations/fr.json b/homeassistant/components/powerwall/translations/fr.json
index 3ddc6634557258..3bfd70cd44c421 100644
--- a/homeassistant/components/powerwall/translations/fr.json
+++ b/homeassistant/components/powerwall/translations/fr.json
@@ -1,18 +1,23 @@
{
"config": {
"abort": {
- "already_configured": "Le Powerwall est d\u00e9j\u00e0 configur\u00e9"
+ "already_configured": "Le Powerwall est d\u00e9j\u00e0 configur\u00e9",
+ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi"
},
"error": {
"cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer",
+ "invalid_auth": "Authentification invalide",
"unknown": "Erreur inattendue",
"wrong_version": "Votre Powerwall utilise une version logicielle qui n'est pas prise en charge. Veuillez envisager de mettre \u00e0 niveau ou de signaler ce probl\u00e8me afin qu'il puisse \u00eatre r\u00e9solu."
},
+ "flow_title": "Tesla Powerwall ( {ip_address} )",
"step": {
"user": {
"data": {
- "ip_address": "Adresse IP"
+ "ip_address": "Adresse IP",
+ "password": "Mot de passe"
},
+ "description": "Le mot de passe est g\u00e9n\u00e9ralement les 5 derniers caract\u00e8res du num\u00e9ro de s\u00e9rie de Backup Gateway et peut \u00eatre trouv\u00e9 dans l\u2019application Tesla ou les 5 derniers caract\u00e8res du mot de passe trouv\u00e9 \u00e0 l\u2019int\u00e9rieur de la porte pour la passerelle de Backup Gateway 2.",
"title": "Connectez-vous au Powerwall"
}
}
diff --git a/homeassistant/components/powerwall/translations/hu.json b/homeassistant/components/powerwall/translations/hu.json
index 7cc0ceafac116a..a2b8af1337003c 100644
--- a/homeassistant/components/powerwall/translations/hu.json
+++ b/homeassistant/components/powerwall/translations/hu.json
@@ -1,9 +1,20 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "flow_title": "Tesla Powerwall ({ip_address})",
"step": {
"user": {
"data": {
- "ip_address": "IP-c\u00edm"
+ "ip_address": "IP c\u00edm",
+ "password": "Jelsz\u00f3"
}
}
}
diff --git a/homeassistant/components/powerwall/translations/id.json b/homeassistant/components/powerwall/translations/id.json
new file mode 100644
index 00000000000000..a5ae5f5e9794be
--- /dev/null
+++ b/homeassistant/components/powerwall/translations/id.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "reauth_successful": "Autentikasi ulang berhasil"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan",
+ "wrong_version": "Powerwall Anda menggunakan versi perangkat lunak yang tidak didukung. Pertimbangkan untuk memutakhirkan atau melaporkan masalah ini agar dapat diatasi."
+ },
+ "flow_title": "Tesla Powerwall ({ip_address})",
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "Alamat IP",
+ "password": "Kata Sandi"
+ },
+ "description": "Kata sandi umumnya adalah 5 karakter terakhir dari nomor seri untuk Backup Gateway dan dapat ditemukan di aplikasi Tesla atau 5 karakter terakhir kata sandi yang ditemukan di dalam pintu untuk Backup Gateway 2.",
+ "title": "Hubungkan ke powerwall"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/powerwall/translations/it.json b/homeassistant/components/powerwall/translations/it.json
index 422a28b6936958..48cd7c04743df6 100644
--- a/homeassistant/components/powerwall/translations/it.json
+++ b/homeassistant/components/powerwall/translations/it.json
@@ -1,18 +1,23 @@
{
"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",
+ "invalid_auth": "Autenticazione non valida",
"unknown": "Errore imprevisto",
"wrong_version": "Il tuo powerwall utilizza una versione del software non supportata. Si prega di considerare l'aggiornamento o la segnalazione di questo problema in modo che possa essere risolto."
},
+ "flow_title": "Tesla Powerwall ({ip_address})",
"step": {
"user": {
"data": {
- "ip_address": "Indirizzo IP"
+ "ip_address": "Indirizzo IP",
+ "password": "Password"
},
+ "description": "La password di solito \u00e8 costituita dagli ultimi 5 caratteri del numero di serie per il Backup Gateway e pu\u00f2 essere trovata nell'app Tesla; oppure dagli ultimi 5 caratteri della password trovata all'interno della porta per il Backup Gateway 2.",
"title": "Connessione al Powerwall"
}
}
diff --git a/homeassistant/components/powerwall/translations/ko.json b/homeassistant/components/powerwall/translations/ko.json
index 9ba7004899f7ca..d1dd99bbb7a103 100644
--- a/homeassistant/components/powerwall/translations/ko.json
+++ b/homeassistant/components/powerwall/translations/ko.json
@@ -1,19 +1,24 @@
{
"config": {
"abort": {
- "already_configured": "powerwall \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4",
"wrong_version": "Powerwall \uc774 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \ubc84\uc804\uc758 \uc18c\ud504\ud2b8\uc6e8\uc5b4\ub97c \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4. \uc774 \ubb38\uc81c\ub97c \ud574\uacb0\ud558\ub824\uba74 \uc5c5\uadf8\ub808\uc774\ub4dc\ud558\uac70\ub098 \uc774 \ub0b4\uc6a9\uc744 \uc54c\ub824\uc8fc\uc138\uc694."
},
+ "flow_title": "Tesla Powerwall ({ip_address})",
"step": {
"user": {
"data": {
- "ip_address": "IP \uc8fc\uc18c"
+ "ip_address": "IP \uc8fc\uc18c",
+ "password": "\ube44\ubc00\ubc88\ud638"
},
- "title": "powerwall \uc5d0 \uc5f0\uacb0\ud558\uae30"
+ "description": "\ube44\ubc00\ubc88\ud638\ub294 \uc77c\ubc18\uc801\uc73c\ub85c \ubc31\uc5c5 \uac8c\uc774\ud2b8\uc6e8\uc774 \uc2dc\ub9ac\uc5bc \ubc88\ud638\uc758 \ub9c8\uc9c0\ub9c9 5\uc790\ub9ac\uc774\uba70 Tesla \uc571 \ub610\ub294 \ubc31\uc5c5 \uac8c\uc774\ud2b8\uc6e8\uc774 2\uc758 \ub3c4\uc5b4 \ub0b4\ubd80\uc5d0 \uc788\ub294 \ub9c8\uc9c0\ub9c9 5\uc790\ub9ac \ube44\ubc00\ubc88\ud638\uc5d0\uc11c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "title": "powerwall\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/powerwall/translations/nl.json b/homeassistant/components/powerwall/translations/nl.json
index f77cc864813d07..5149f391669388 100644
--- a/homeassistant/components/powerwall/translations/nl.json
+++ b/homeassistant/components/powerwall/translations/nl.json
@@ -1,18 +1,23 @@
{
"config": {
"abort": {
- "already_configured": "De powerwall is al geconfigureerd"
+ "already_configured": "Apparaat is al geconfigureerd",
+ "reauth_successful": "Herauthenticatie was succesvol"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout",
"wrong_version": "Uw powerwall gebruikt een softwareversie die niet wordt ondersteund. Overweeg om dit probleem te upgraden of te melden, zodat het kan worden opgelost."
},
+ "flow_title": "Tesla Powerwall ({ip_adres})",
"step": {
"user": {
"data": {
- "ip_address": "IP-adres"
+ "ip_address": "IP-adres",
+ "password": "Wachtwoord"
},
+ "description": "Het wachtwoord is meestal de laatste 5 tekens van het serienummer voor Backup Gateway en is te vinden in de Tesla-app of de laatste 5 tekens van het wachtwoord aan de binnenkant van de deur voor Backup Gateway 2.",
"title": "Maak verbinding met de powerwall"
}
}
diff --git a/homeassistant/components/powerwall/translations/no.json b/homeassistant/components/powerwall/translations/no.json
index 7ddab100358730..00b77e1456611b 100644
--- a/homeassistant/components/powerwall/translations/no.json
+++ b/homeassistant/components/powerwall/translations/no.json
@@ -1,18 +1,23 @@
{
"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",
+ "invalid_auth": "Ugyldig godkjenning",
"unknown": "Uventet feil",
"wrong_version": "Powerwall bruker en programvareversjon som ikke st\u00f8ttes. Vennligst vurder \u00e5 oppgradere eller rapportere dette problemet, s\u00e5 det kan l\u00f8ses."
},
+ "flow_title": "",
"step": {
"user": {
"data": {
- "ip_address": "IP adresse"
+ "ip_address": "IP adresse",
+ "password": "Passord"
},
+ "description": "Passordet er vanligvis de siste 5 tegnene i serienummeret for Backup Gateway, og kan bli funnet i Tesla-appen eller de siste 5 tegnene i passordet som er funnet inne i d\u00f8ren til Backup Gateway 2.",
"title": "Koble til powerwall"
}
}
diff --git a/homeassistant/components/powerwall/translations/pl.json b/homeassistant/components/powerwall/translations/pl.json
index 8532066608ceb4..272f28df3b99b3 100644
--- a/homeassistant/components/powerwall/translations/pl.json
+++ b/homeassistant/components/powerwall/translations/pl.json
@@ -1,18 +1,23 @@
{
"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",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
"unknown": "Nieoczekiwany b\u0142\u0105d",
"wrong_version": "Powerwall u\u017cywa wersji oprogramowania, kt\u00f3ra nie jest obs\u0142ugiwana. Rozwa\u017c uaktualnienie lub zg\u0142oszenie tego problemu, aby mo\u017cna go by\u0142o rozwi\u0105za\u0107."
},
+ "flow_title": "Tesla UPS ({ip_address})",
"step": {
"user": {
"data": {
- "ip_address": "Adres IP"
+ "ip_address": "Adres IP",
+ "password": "Has\u0142o"
},
+ "description": "Has\u0142o to zazwyczaj 5 ostatnich znak\u00f3w numeru seryjnego Backup Gateway i mo\u017cna je znale\u017a\u0107 w aplikacji Tesla; lub ostatnie 5 znak\u00f3w has\u0142a na wewn\u0119trznej stronie drzwiczek Backup Gateway 2.",
"title": "Po\u0142\u0105czenie z Powerwall"
}
}
diff --git a/homeassistant/components/powerwall/translations/ru.json b/homeassistant/components/powerwall/translations/ru.json
index a8713bcd04ae31..f79b62c2c78dc6 100644
--- a/homeassistant/components/powerwall/translations/ru.json
+++ b/homeassistant/components/powerwall/translations/ru.json
@@ -1,18 +1,23 @@
{
"config": {
"abort": {
- "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.",
"wrong_version": "\u0412\u0430\u0448 powerwall \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0432\u0435\u0440\u0441\u0438\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u043e\u0433\u043e \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0441\u043e\u043e\u0431\u0449\u0438\u0442\u0435 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0435, \u0447\u0442\u043e\u0431\u044b \u0435\u0435 \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0440\u0435\u0448\u0438\u0442\u044c."
},
+ "flow_title": "Tesla Powerwall ({ip_address})",
"step": {
"user": {
"data": {
- "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441"
+ "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
},
+ "description": "\u041f\u0430\u0440\u043e\u043b\u044c \u043e\u0431\u044b\u0447\u043d\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u043e\u0431\u043e\u0439 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 5 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432 \u0441\u0435\u0440\u0438\u0439\u043d\u043e\u0433\u043e \u043d\u043e\u043c\u0435\u0440\u0430 \u0434\u043b\u044f Backup Gateway, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 Telsa; \u0438\u043b\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 5 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432 \u043f\u0430\u0440\u043e\u043b\u044f, \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u043d\u0443\u0442\u0440\u0438 Backup Gateway 2.",
"title": "Tesla Powerwall"
}
}
diff --git a/homeassistant/components/powerwall/translations/tr.json b/homeassistant/components/powerwall/translations/tr.json
new file mode 100644
index 00000000000000..dd09a83a78c5ba
--- /dev/null
+++ b/homeassistant/components/powerwall/translations/tr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ },
+ "flow_title": "Tesla Powerwall ( {ip_address} )",
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "\u0130p Adresi"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/powerwall/translations/uk.json b/homeassistant/components/powerwall/translations/uk.json
new file mode 100644
index 00000000000000..9b397138c5272c
--- /dev/null
+++ b/homeassistant/components/powerwall/translations/uk.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430",
+ "wrong_version": "\u0412\u0430\u0448 Powerwall \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454 \u0432\u0435\u0440\u0441\u0456\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043d\u043e\u0433\u043e \u0437\u0430\u0431\u0435\u0437\u043f\u0435\u0447\u0435\u043d\u043d\u044f, \u044f\u043a\u0430 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0440\u043e\u0437\u0433\u043b\u044f\u043d\u044c\u0442\u0435 \u043c\u043e\u0436\u043b\u0438\u0432\u0456\u0441\u0442\u044c \u043f\u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0430\u0431\u043e \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u0442\u0435 \u043f\u0440\u043e \u0446\u044e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443, \u0449\u043e\u0431 \u0457\u0457 \u043c\u043e\u0436\u043d\u0430 \u0431\u0443\u043b\u043e \u0432\u0438\u0440\u0456\u0448\u0438\u0442\u0438."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430"
+ },
+ "title": "Tesla Powerwall"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/powerwall/translations/zh-Hant.json b/homeassistant/components/powerwall/translations/zh-Hant.json
index 45edbf2d88ed37..44e79e935cdf1d 100644
--- a/homeassistant/components/powerwall/translations/zh-Hant.json
+++ b/homeassistant/components/powerwall/translations/zh-Hant.json
@@ -1,18 +1,23 @@
{
"config": {
"abort": {
- "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ "already_configured": "\u88dd\u7f6e\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_version": "\u4e0d\u652f\u63f4\u60a8\u6240\u4f7f\u7528\u7684 Powerwall \u7248\u672c\u3002\u8acb\u8003\u616e\u9032\u884c\u5347\u7d1a\u6216\u56de\u5831\u6b64\u554f\u984c\u3001\u4ee5\u671f\u554f\u984c\u53ef\u4ee5\u7372\u5f97\u89e3\u6c7a\u3002"
},
+ "flow_title": "Tesla Powerwall ({ip_address})",
"step": {
"user": {
"data": {
- "ip_address": "IP \u4f4d\u5740"
+ "ip_address": "IP \u4f4d\u5740",
+ "password": "\u5bc6\u78bc"
},
+ "description": "\u5bc6\u78bc\u901a\u5e38\u70ba\u81f3\u5c11\u5099\u4efd\u9598\u9053\u5668\u5e8f\u865f\u7684\u6700\u5f8c\u4e94\u78bc\uff0c\u4e26\u4e14\u80fd\u5920\u65bc Telsa App \u4e2d\n\u627e\u5230\u3002\u6216\u8005\u70ba\u5099\u4efd\u9598\u9053\u5668 2 \u9580\u5167\u5074\u627e\u5230\u7684\u5bc6\u78bc\u6700\u5f8c\u4e94\u78bc\u3002",
"title": "\u9023\u7dda\u81f3 Powerwall"
}
}
diff --git a/homeassistant/components/profiler/config_flow.py b/homeassistant/components/profiler/config_flow.py
index 9a71b64a1790c1..259c300239cf2e 100644
--- a/homeassistant/components/profiler/config_flow.py
+++ b/homeassistant/components/profiler/config_flow.py
@@ -3,8 +3,7 @@
from homeassistant import config_entries
-from .const import DEFAULT_NAME
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import DEFAULT_NAME, DOMAIN
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/profiler/translations/de.json b/homeassistant/components/profiler/translations/de.json
new file mode 100644
index 00000000000000..7137cd2ee4e106
--- /dev/null
+++ b/homeassistant/components/profiler/translations/de.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
+ "step": {
+ "user": {
+ "description": "M\u00f6chtest du mit der Einrichtung beginnen?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/profiler/translations/hu.json b/homeassistant/components/profiler/translations/hu.json
index bbdd2e5b536f4d..c5d28903888626 100644
--- a/homeassistant/components/profiler/translations/hu.json
+++ b/homeassistant/components/profiler/translations/hu.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "single_instance_allowed": "M\u00e1r konfigur\u00e1lva. Csak egyetlen konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
},
"step": {
"user": {
- "description": "El akarja kezdeni a be\u00e1ll\u00edt\u00e1st?"
+ "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?"
}
}
}
diff --git a/homeassistant/components/profiler/translations/id.json b/homeassistant/components/profiler/translations/id.json
new file mode 100644
index 00000000000000..3521bb95412b03
--- /dev/null
+++ b/homeassistant/components/profiler/translations/id.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "step": {
+ "user": {
+ "description": "Ingin memulai penyiapan?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/profiler/translations/ko.json b/homeassistant/components/profiler/translations/ko.json
new file mode 100644
index 00000000000000..251c9777b6c18f
--- /dev/null
+++ b/homeassistant/components/profiler/translations/ko.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "step": {
+ "user": {
+ "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/profiler/translations/nl.json b/homeassistant/components/profiler/translations/nl.json
index 703ac8614c49d4..8690611b1c90ff 100644
--- a/homeassistant/components/profiler/translations/nl.json
+++ b/homeassistant/components/profiler/translations/nl.json
@@ -2,6 +2,11 @@
"config": {
"abort": {
"single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk."
+ },
+ "step": {
+ "user": {
+ "description": "Wil je beginnen met instellen?"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/profiler/translations/tr.json b/homeassistant/components/profiler/translations/tr.json
new file mode 100644
index 00000000000000..a152eb194683cb
--- /dev/null
+++ b/homeassistant/components/profiler/translations/tr.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/profiler/translations/uk.json b/homeassistant/components/profiler/translations/uk.json
new file mode 100644
index 00000000000000..5594895456e987
--- /dev/null
+++ b/homeassistant/components/profiler/translations/uk.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "step": {
+ "user": {
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/progettihwsw/__init__.py b/homeassistant/components/progettihwsw/__init__.py
index 02418c963d4cfa..7597b2ff1a2927 100644
--- a/homeassistant/components/progettihwsw/__init__.py
+++ b/homeassistant/components/progettihwsw/__init__.py
@@ -30,9 +30,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
# Check board validation again to load new values to API.
await hass.data[DOMAIN][entry.entry_id].check_board()
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -43,8 +43,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/progettihwsw/translations/de.json b/homeassistant/components/progettihwsw/translations/de.json
index 2e5bed4b668af3..0f773e03c1ded4 100644
--- a/homeassistant/components/progettihwsw/translations/de.json
+++ b/homeassistant/components/progettihwsw/translations/de.json
@@ -1,7 +1,10 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"unknown": "Unerwarteter Fehler"
},
"step": {
diff --git a/homeassistant/components/progettihwsw/translations/hu.json b/homeassistant/components/progettihwsw/translations/hu.json
index 3b2d79a34a77e2..f6f6e2c15b7849 100644
--- a/homeassistant/components/progettihwsw/translations/hu.json
+++ b/homeassistant/components/progettihwsw/translations/hu.json
@@ -2,6 +2,32 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "relay_modes": {
+ "data": {
+ "relay_1": "Rel\u00e9 1",
+ "relay_10": "Rel\u00e9 10",
+ "relay_11": "Rel\u00e9 11",
+ "relay_12": "Rel\u00e9 12",
+ "relay_13": "Rel\u00e9 13",
+ "relay_14": "Rel\u00e9 14",
+ "relay_15": "Rel\u00e9 15",
+ "relay_16": "Rel\u00e9 16",
+ "relay_2": "Rel\u00e9 2",
+ "relay_3": "Rel\u00e9 3"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "port": "Port"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/progettihwsw/translations/id.json b/homeassistant/components/progettihwsw/translations/id.json
new file mode 100644
index 00000000000000..0aa69cad9588a1
--- /dev/null
+++ b/homeassistant/components/progettihwsw/translations/id.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "relay_modes": {
+ "data": {
+ "relay_1": "Relai 1",
+ "relay_10": "Relai 10",
+ "relay_11": "Relai 11",
+ "relay_12": "Relai 12",
+ "relay_13": "Relai 13",
+ "relay_14": "Relai 14",
+ "relay_15": "Relai 15",
+ "relay_16": "Relai 16",
+ "relay_2": "Relai 2",
+ "relay_3": "Relai 3",
+ "relay_4": "Relai 4",
+ "relay_5": "Relai 5",
+ "relay_6": "Relai 6",
+ "relay_7": "Relai 7",
+ "relay_8": "Relai 8",
+ "relay_9": "Relai 9"
+ },
+ "title": "Siapkan relai"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ },
+ "title": "Siapkan papan"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/progettihwsw/translations/ko.json b/homeassistant/components/progettihwsw/translations/ko.json
index 02fca81481141b..9324783f5cbe2d 100644
--- a/homeassistant/components/progettihwsw/translations/ko.json
+++ b/homeassistant/components/progettihwsw/translations/ko.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "\uc7a5\uce58\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
- "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"relay_modes": {
@@ -27,14 +27,14 @@
"relay_8": "\ub9b4\ub808\uc774 8",
"relay_9": "\ub9b4\ub808\uc774 9"
},
- "title": "\ub9b4\ub808\uc774 \uc124\uc815"
+ "title": "\ub9b4\ub808\uc774 \uc124\uc815\ud558\uae30"
},
"user": {
"data": {
"host": "\ud638\uc2a4\ud2b8",
"port": "\ud3ec\ud2b8"
},
- "title": "\ubcf4\ub4dc \uc124\uc815"
+ "title": "\ubcf4\ub4dc \uc124\uc815\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/progettihwsw/translations/nl.json b/homeassistant/components/progettihwsw/translations/nl.json
index ba10aee5ea20cc..64eb0d1271758f 100644
--- a/homeassistant/components/progettihwsw/translations/nl.json
+++ b/homeassistant/components/progettihwsw/translations/nl.json
@@ -9,13 +9,32 @@
},
"step": {
"relay_modes": {
+ "data": {
+ "relay_1": "Relais 1",
+ "relay_10": "Relais 10",
+ "relay_11": "Relais 11",
+ "relay_12": "Relais 12",
+ "relay_13": "Relais 13",
+ "relay_14": "Relais 14",
+ "relay_15": "Relais 15",
+ "relay_16": "Relais 16",
+ "relay_2": "Relais 2",
+ "relay_3": "Relais 3",
+ "relay_4": "Relais 4",
+ "relay_5": "Relais 5",
+ "relay_6": "Relais 6",
+ "relay_7": "Relais 7",
+ "relay_8": "Relais 8",
+ "relay_9": "Relais 9"
+ },
"title": "Stel relais in"
},
"user": {
"data": {
"host": "Host",
"port": "Poort"
- }
+ },
+ "title": "Stel het bord in"
}
}
}
diff --git a/homeassistant/components/progettihwsw/translations/tr.json b/homeassistant/components/progettihwsw/translations/tr.json
new file mode 100644
index 00000000000000..1d3d77584dd907
--- /dev/null
+++ b/homeassistant/components/progettihwsw/translations/tr.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "relay_modes": {
+ "data": {
+ "relay_1": "R\u00f6le 1",
+ "relay_10": "R\u00f6le 10",
+ "relay_11": "R\u00f6le 11",
+ "relay_12": "R\u00f6le 12",
+ "relay_13": "R\u00f6le 13",
+ "relay_14": "R\u00f6le 14",
+ "relay_15": "R\u00f6le 15",
+ "relay_16": "R\u00f6le 16",
+ "relay_2": "R\u00f6le 2",
+ "relay_3": "R\u00f6le 3",
+ "relay_4": "R\u00f6le 4",
+ "relay_5": "R\u00f6le 5",
+ "relay_6": "R\u00f6le 6",
+ "relay_7": "R\u00f6le 7",
+ "relay_8": "R\u00f6le 8",
+ "relay_9": "R\u00f6le 9"
+ },
+ "title": "R\u00f6leleri kur"
+ },
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ },
+ "title": "Panoyu kur"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/progettihwsw/translations/uk.json b/homeassistant/components/progettihwsw/translations/uk.json
new file mode 100644
index 00000000000000..7918db8e1584f2
--- /dev/null
+++ b/homeassistant/components/progettihwsw/translations/uk.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "relay_modes": {
+ "data": {
+ "relay_1": "\u0420\u0435\u043b\u0435 1",
+ "relay_10": "\u0420\u0435\u043b\u0435 10",
+ "relay_11": "\u0420\u0435\u043b\u0435 11",
+ "relay_12": "\u0420\u0435\u043b\u0435 12",
+ "relay_13": "\u0420\u0435\u043b\u0435 13",
+ "relay_14": "\u0420\u0435\u043b\u0435 14",
+ "relay_15": "\u0420\u0435\u043b\u0435 15",
+ "relay_16": "\u0420\u0435\u043b\u0435 16",
+ "relay_2": "\u0420\u0435\u043b\u0435 2",
+ "relay_3": "\u0420\u0435\u043b\u0435 3",
+ "relay_4": "\u0420\u0435\u043b\u0435 4",
+ "relay_5": "\u0420\u0435\u043b\u0435 5",
+ "relay_6": "\u0420\u0435\u043b\u0435 6",
+ "relay_7": "\u0420\u0435\u043b\u0435 7",
+ "relay_8": "\u0420\u0435\u043b\u0435 8",
+ "relay_9": "\u0420\u0435\u043b\u0435 9"
+ },
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0440\u0435\u043b\u0435"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043b\u0430\u0442\u0438"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py
index 5dff4725ea0d42..a293642038e427 100644
--- a/homeassistant/components/proliphix/climate.py
+++ b/homeassistant/components/proliphix/climate.py
@@ -84,7 +84,7 @@ def precision(self):
return PRECISION_TENTHS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
return {ATTR_FAN: self._pdp.fan_state}
diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py
index bd9a6e35276df6..b253daf559ec36 100644
--- a/homeassistant/components/prometheus/__init__.py
+++ b/homeassistant/components/prometheus/__init__.py
@@ -16,12 +16,12 @@
from homeassistant.components.humidifier.const import (
ATTR_AVAILABLE_MODES,
ATTR_HUMIDITY,
- ATTR_MODE,
)
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_DEVICE_CLASS,
ATTR_FRIENDLY_NAME,
+ ATTR_MODE,
ATTR_TEMPERATURE,
ATTR_UNIT_OF_MEASUREMENT,
CONTENT_TYPE_TEXT_PLAIN,
@@ -29,6 +29,7 @@
PERCENTAGE,
STATE_ON,
STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
@@ -154,9 +155,11 @@ def handle_event(self, event):
if not self._filter(state.entity_id):
return
+ ignored_states = (STATE_UNAVAILABLE, STATE_UNKNOWN)
+
handler = f"_handle_{domain}"
- if hasattr(self, handler) and state.state != STATE_UNAVAILABLE:
+ if hasattr(self, handler) and state.state not in ignored_states:
getattr(self, handler)(state)
labels = self._labels(state)
@@ -168,9 +171,9 @@ def handle_event(self, event):
entity_available = self._metric(
"entity_available",
self.prometheus_cli.Gauge,
- "Entity is available (not in the unavailable state)",
+ "Entity is available (not in the unavailable or unknown state)",
)
- entity_available.labels(**labels).set(float(state.state != STATE_UNAVAILABLE))
+ entity_available.labels(**labels).set(float(state.state not in ignored_states))
last_updated_time_seconds = self._metric(
"last_updated_time_seconds",
diff --git a/homeassistant/components/prowl/notify.py b/homeassistant/components/prowl/notify.py
index 5d2efe766077b8..802679ab03d11d 100644
--- a/homeassistant/components/prowl/notify.py
+++ b/homeassistant/components/prowl/notify.py
@@ -48,6 +48,8 @@ async def async_send_message(self, message, **kwargs):
"description": message,
"priority": data["priority"] if data and "priority" in data else 0,
}
+ if data and data.get("url"):
+ payload["url"] = data["url"]
_LOGGER.debug("Attempting call Prowl service at %s", url)
session = async_get_clientsession(self._hass)
diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py
index 6df0f50b72019d..de9d6247f9f61a 100644
--- a/homeassistant/components/proximity/__init__.py
+++ b/homeassistant/components/proximity/__init__.py
@@ -148,7 +148,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_DIR_OF_TRAVEL: self.dir_of_travel, ATTR_NEAREST: self.nearest}
diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py
index 2f42ca8fe9ecd4..a149c8b6034fdb 100644
--- a/homeassistant/components/proxmoxve/__init__.py
+++ b/homeassistant/components/proxmoxve/__init__.py
@@ -185,12 +185,12 @@ def poll_api() -> dict:
hass.data[DOMAIN][COORDINATOR] = coordinator
# Fetch initial data
- await coordinator.async_refresh()
+ await coordinator.async_config_entry_first_refresh()
- for component in PLATFORMS:
+ for platform in PLATFORMS:
await hass.async_create_task(
hass.helpers.discovery.async_load_platform(
- component, DOMAIN, {"config": config}, config
+ platform, DOMAIN, {"config": config}, config
)
)
diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py
index 014766b532e95c..1151c2ec33299f 100644
--- a/homeassistant/components/proxmoxve/binary_sensor.py
+++ b/homeassistant/components/proxmoxve/binary_sensor.py
@@ -1,13 +1,9 @@
"""Binary sensor to read Proxmox VE data."""
-import logging
-
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import COORDINATOR, DOMAIN, ProxmoxEntity
-_LOGGER = logging.getLogger(__name__)
-
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up binary sensors."""
diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py
index c3f7151431a394..8fda507ace21d7 100644
--- a/homeassistant/components/proxy/camera.py
+++ b/homeassistant/components/proxy/camera.py
@@ -77,7 +77,7 @@ def _precheck_image(image, opts):
if imgfmt not in ("PNG", "JPEG"):
_LOGGER.warning("Image is of unsupported type: %s", imgfmt)
raise ValueError()
- if not img.mode == "RGB":
+ if img.mode != "RGB":
img = img.convert("RGB")
return img
diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json
index 65d8d21fc0cf12..86f3d23d308775 100644
--- a/homeassistant/components/proxy/manifest.json
+++ b/homeassistant/components/proxy/manifest.json
@@ -2,6 +2,6 @@
"domain": "proxy",
"name": "Camera Proxy",
"documentation": "https://www.home-assistant.io/integrations/proxy",
- "requirements": ["pillow==8.1.0"],
+ "requirements": ["pillow==8.1.2"],
"codeowners": []
}
diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py
index 390637c26a35fa..11d271be543a0d 100644
--- a/homeassistant/components/ps4/__init__.py
+++ b/homeassistant/components/ps4/__init__.py
@@ -25,7 +25,7 @@
from homeassistant.util import location
from homeassistant.util.json import load_json, save_json
-from .config_flow import PlayStation4FlowHandler # noqa: pylint: disable=unused-import
+from .config_flow import PlayStation4FlowHandler # noqa: F401
from .const import ATTR_MEDIA_IMAGE_URL, COMMANDS, DOMAIN, GAMES_FILE, PS4_DATA
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py
index 24a1589db0d9ca..be77ea04f1cae1 100644
--- a/homeassistant/components/ps4/media_player.py
+++ b/homeassistant/components/ps4/media_player.py
@@ -1,5 +1,6 @@
"""Support for PlayStation 4 consoles."""
import asyncio
+from contextlib import suppress
import logging
from pyps4_2ndscreen.errors import NotReady, PSDataIncomplete
@@ -142,10 +143,8 @@ async def async_update(self):
and not self._ps4.is_standby
and self._ps4.is_available
):
- try:
+ with suppress(NotReady):
await self._ps4.async_connect()
- except NotReady:
- pass
# Try to ensure correct status is set on startup for device info.
if self._ps4.ddp_protocol is None:
diff --git a/homeassistant/components/ps4/translations/de.json b/homeassistant/components/ps4/translations/de.json
index 5dd638a717cb1c..d5aa867f1db369 100644
--- a/homeassistant/components/ps4/translations/de.json
+++ b/homeassistant/components/ps4/translations/de.json
@@ -1,15 +1,16 @@
{
"config": {
"abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
"credential_error": "Fehler beim Abrufen der Anmeldeinformationen.",
"no_devices_found": "Es wurden keine PlayStation 4 im Netzwerk gefunden.",
"port_987_bind_error": "Konnte sich nicht an Port 987 binden. Weitere Informationen findest du in der [Dokumentation] (https://www.home-assistant.io/components/ps4/).",
"port_997_bind_error": "Bind to Port 997 nicht m\u00f6glich. Weitere Informationen findest du in der [Dokumentation](https://www.home-assistant.io/components/ps4/)"
},
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"credential_timeout": "Zeit\u00fcberschreitung beim Warten auf den Anmeldedienst. Klicken zum Neustarten auf Senden.",
- "login_failed": "Fehler beim Koppeln mit PlayStation 4. \u00dcberpr\u00fcfe, ob die PIN korrekt ist.",
+ "login_failed": "Fehler beim Koppeln mit der PlayStation 4. \u00dcberpr\u00fcfe, ob der PIN-Code korrekt ist.",
"no_ipaddress": "Gib die IP-Adresse der PlayStation 4 ein, die konfiguriert werden soll."
},
"step": {
@@ -19,7 +20,7 @@
},
"link": {
"data": {
- "code": "PIN",
+ "code": "PIN-Code",
"ip_address": "IP-Adresse",
"name": "Name",
"region": "Region"
diff --git a/homeassistant/components/ps4/translations/hu.json b/homeassistant/components/ps4/translations/hu.json
index c3bcabb0d3a111..bb677b21700a77 100644
--- a/homeassistant/components/ps4/translations/hu.json
+++ b/homeassistant/components/ps4/translations/hu.json
@@ -1,8 +1,12 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton"
+ },
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
- "login_failed": "Nem siker\u00fclt p\u00e1ros\u00edtani a PlayStation 4-gyel. Ellen\u0151rizze, hogy a helyes-e."
+ "login_failed": "Nem siker\u00fclt p\u00e1ros\u00edtani a PlayStation 4-gyel. Ellen\u0151rizze, hogy a PIN-k\u00f3d helyes-e."
},
"step": {
"creds": {
@@ -10,14 +14,15 @@
},
"link": {
"data": {
- "code": "PIN",
- "ip_address": "IP-c\u00edm",
+ "code": "PIN-k\u00f3d",
+ "ip_address": "IP c\u00edm",
"name": "N\u00e9v",
"region": "R\u00e9gi\u00f3"
}
},
"mode": {
"data": {
+ "ip_address": "IP c\u00edm (Hagyd \u00fcresen az Automatikus Felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz).",
"mode": "Konfigur\u00e1ci\u00f3s m\u00f3d"
},
"title": "PlayStation 4"
diff --git a/homeassistant/components/ps4/translations/id.json b/homeassistant/components/ps4/translations/id.json
new file mode 100644
index 00000000000000..aab31564e162cf
--- /dev/null
+++ b/homeassistant/components/ps4/translations/id.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "credential_error": "Terjadi kesalahan saat mengambil kredensial.",
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan",
+ "port_987_bind_error": "Tidak dapat mengaitkan ke port 987. Baca [dokumentasi] (https://www.home-assistant.io/components/ps4/) untuk info lebih lanjut.",
+ "port_997_bind_error": "Tidak dapat mengaitkan ke port 997. Baca [dokumentasi] (https://www.home-assistant.io/components/ps4/) untuk info lebih lanjut."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "credential_timeout": "Tenggang waktu layanan kredensial habis. Tekan kirim untuk memulai kembali.",
+ "login_failed": "Gagal memasangkan dengan PlayStation 4. Pastikan Kode PIN sudah benar.",
+ "no_ipaddress": "Masukkan Alamat IP perangkat PlayStation 4 yang dikonfigurasi."
+ },
+ "step": {
+ "creds": {
+ "description": "Kredensial diperlukan. Tekan 'Kirim' dan kemudian di Aplikasi Layar ke-2 PS4, segarkan perangkat dan pilih perangkat 'Home-Assistant' untuk melanjutkan.",
+ "title": "PlayStation 4"
+ },
+ "link": {
+ "data": {
+ "code": "Kode PIN",
+ "ip_address": "Alamat IP",
+ "name": "Nama",
+ "region": "Wilayah"
+ },
+ "description": "Masukkan informasi PlayStation 4 Anda. Untuk Kode PIN, buka 'Pengaturan' di konsol PlayStation 4 Anda. Kemudian buka 'Pengaturan Koneksi Aplikasi Seluler' dan pilih 'Tambah Perangkat'. Masukkan Kode PIN yang ditampilkan. Lihat [dokumentasi] (https://www.home-assistant.io/components/ps4/) untuk info lebih lanjut.",
+ "title": "PlayStation 4"
+ },
+ "mode": {
+ "data": {
+ "ip_address": "Alamat IP (Kosongkan jika menggunakan Penemuan Otomatis).",
+ "mode": "Mode Konfigurasi"
+ },
+ "description": "Pilih mode untuk konfigurasi. Alamat IP dapat dikosongkan jika memilih Penemuan Otomatis karena perangkat akan ditemukan secara otomatis.",
+ "title": "PlayStation 4"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ps4/translations/ko.json b/homeassistant/components/ps4/translations/ko.json
index 0e62a64d1c62ae..129927a9babd25 100644
--- a/homeassistant/components/ps4/translations/ko.json
+++ b/homeassistant/components/ps4/translations/ko.json
@@ -1,29 +1,31 @@
{
"config": {
"abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"credential_error": "\uc790\uaca9 \uc99d\uba85\uc744 \uac00\uc838\uc624\ub294 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
- "no_devices_found": "PlayStation 4 \uae30\uae30\ub97c \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
- "port_987_bind_error": "\ud3ec\ud2b8 987 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
- "port_997_bind_error": "\ud3ec\ud2b8 997 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "port_987_bind_error": "987 \ud3ec\ud2b8\uc5d0 \ubc14\uc778\ub529\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c](https://www.home-assistant.io/components/ps4/)\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
+ "port_997_bind_error": "997 \ud3ec\ud2b8\uc5d0 \ubc14\uc778\ub529\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c](https://www.home-assistant.io/components/ps4/)\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
},
"error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"credential_timeout": "\uc790\uaca9 \uc99d\uba85 \uc11c\ube44\uc2a4 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud655\uc778\uc744 \ud074\ub9ad\ud558\uc5ec \ub2e4\uc2dc \uc2dc\uc791\ud574\uc8fc\uc138\uc694.",
- "login_failed": "PlayStation 4 \uc640 \ud398\uc5b4\ub9c1\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. PIN \uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
- "no_ipaddress": "\uad6c\uc131\ud558\uace0\uc790 \ud558\ub294 PlayStation 4 \uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694."
+ "login_failed": "PlayStation 4 \uc640 \ud398\uc5b4\ub9c1\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. PIN \ucf54\ub4dc\uac00 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
+ "no_ipaddress": "\uad6c\uc131\ud560 PlayStation 4\uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694."
},
"step": {
"creds": {
- "description": "\uc790\uaca9 \uc99d\uba85\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. '\ud655\uc778'\uc744 \ud074\ub9ad\ud55c \ub2e4\uc74c PS4 \uc138\ucee8\ub4dc \uc2a4\ud06c\ub9b0 \uc571\uc5d0\uc11c \uae30\uae30\ub97c \uc0c8\ub85c \uace0\uce68\ud558\uace0 'Home-Assistant' \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.",
+ "description": "\uc790\uaca9 \uc99d\uba85\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. '\ud655\uc778'\uc744 \ud074\ub9ad\ud55c \ub2e4\uc74c PS4 \uc138\ucee8\ub4dc \uc2a4\ud06c\ub9b0 \uc571\uc5d0\uc11c \uae30\uae30\ub97c \uc0c8\ub85c\uace0\uce68 \ud558\uace0 'Home-Assistant' \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.",
"title": "PlayStation 4"
},
"link": {
"data": {
- "code": "PIN",
+ "code": "PIN \ucf54\ub4dc",
"ip_address": "IP \uc8fc\uc18c",
"name": "\uc774\ub984",
"region": "\uc9c0\uc5ed"
},
- "description": "PlayStation 4 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. 'PIN' \uc744 \ud655\uc778\ud558\ub824\uba74, PlayStation 4 \ucf58\uc194\uc5d0\uc11c '\uc124\uc815' \uc73c\ub85c \uc774\ub3d9\ud55c \ub4a4 '\ubaa8\ubc14\uc77c \uc571 \uc811\uc18d \uc124\uc815' \uc73c\ub85c \uc774\ub3d9\ud558\uc5ec '\uae30\uae30 \ub4f1\ub85d\ud558\uae30' \ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ud654\uba74\uc5d0 \ud45c\uc2dc\ub41c 8\uc790\ub9ac \uc22b\uc790\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
+ "description": "PlayStation 4 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. PIN \ucf54\ub4dc\ub97c \ud655\uc778\ud558\ub824\uba74, PlayStation 4 \ucf58\uc194\uc5d0\uc11c '\uc124\uc815'\uc73c\ub85c \uc774\ub3d9\ud55c \ub4a4 '\ubaa8\ubc14\uc77c \uc571 \uc811\uc18d \uc124\uc815'\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec '\uae30\uae30 \ub4f1\ub85d\ud558\uae30'\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ud654\uba74\uc5d0 \ud45c\uc2dc\ub41c PIN \ucf54\ub4dc(\uc744)\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \ucd94\uac00 \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c](https://www.home-assistant.io/components/ps4/)\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
"title": "PlayStation 4"
},
"mode": {
@@ -31,7 +33,7 @@
"ip_address": "IP \uc8fc\uc18c (\uc790\ub3d9 \uac80\uc0c9\uc744 \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0 \ube44\uc6cc\ub450\uc138\uc694)",
"mode": "\uad6c\uc131 \ubaa8\ub4dc"
},
- "description": "\uad6c\uc131 \ubaa8\ub4dc\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc790\ub3d9 \uac80\uc0c9\uc744 \uc120\ud0dd\ud558\uba74 \uae30\uae30\uac00 \uc790\ub3d9\uc73c\ub85c \uac80\uc0c9\ub418\ubbc0\ub85c IP \uc8fc\uc18c \ud544\ub4dc\ub294 \ube44\uc6cc\ub450\uc154\ub3c4 \ub429\ub2c8\ub2e4.",
+ "description": "\uad6c\uc131\ud560 \ubaa8\ub4dc\ub97c \uc120\ud0dd\ud569\ub2c8\ub2e4. \uc790\ub3d9 \uac80\uc0c9\uc744 \uc120\ud0dd\ud558\uba74 \uae30\uae30\uac00 \uc790\ub3d9\uc73c\ub85c \uac80\uc0c9\ub418\ubbc0\ub85c IP \uc8fc\uc18c \ud544\ub4dc\ub294 \ube44\uc6cc\ub450\uc154\ub3c4 \ub429\ub2c8\ub2e4.",
"title": "PlayStation 4"
}
}
diff --git a/homeassistant/components/ps4/translations/nl.json b/homeassistant/components/ps4/translations/nl.json
index d86240b2c0a273..db59c7ab30ab5b 100644
--- a/homeassistant/components/ps4/translations/nl.json
+++ b/homeassistant/components/ps4/translations/nl.json
@@ -3,14 +3,15 @@
"abort": {
"already_configured": "Apparaat is al geconfigureerd",
"credential_error": "Fout bij ophalen van inloggegevens.",
- "no_devices_found": "Geen PlayStation 4 apparaten gevonden op het netwerk.",
+ "no_devices_found": "Geen apparaten gevonden op het netwerk",
"port_987_bind_error": "Kon niet binden aan poort 987. Raadpleeg de [documentatie] (https://www.home-assistant.io/components/ps4/) voor meer informatie.",
"port_997_bind_error": "Kon niet binden aan poort 997. Raadpleeg de [documentatie] (https://www.home-assistant.io/components/ps4/) voor aanvullende informatie."
},
"error": {
+ "cannot_connect": "Kan geen verbinding maken",
"credential_timeout": "Time-out van inlog service. Druk op Submit om opnieuw te starten.",
- "login_failed": "Kan niet koppelen met PlayStation 4. Controleer of de pincode juist is.",
- "no_ipaddress": "Voer het IP-adres in van de PlayStation 4 die je wilt configureren."
+ "login_failed": "Kan Playstation 4 niet koppelen. Controleer of de PIN-code correct is.",
+ "no_ipaddress": "Voer het IP-adres in van de PlayStation 4 die u wilt configureren."
},
"step": {
"creds": {
@@ -19,20 +20,20 @@
},
"link": {
"data": {
- "code": "PIN",
+ "code": "PIN-code",
"ip_address": "IP-adres",
"name": "Naam",
"region": "Regio"
},
- "description": "Voer je PlayStation 4-informatie in. Ga voor 'PIN' naar 'Instellingen' op je PlayStation 4-console. Navigeer vervolgens naar 'Verbindingsinstellingen mobiele app' en selecteer 'Apparaat toevoegen'. Voer de pincode in die wordt weergegeven. Raadpleeg de [documentatie] (https://www.home-assistant.io/components/ps4/) voor meer informatie.",
+ "description": "Voer je PlayStation 4-informatie in. Voor PIN-code navigeer je naar 'Instellingen' op je PlayStation 4-console. Ga vervolgens naar 'Verbindingsinstellingen mobiele app' en selecteer 'Apparaat toevoegen'. Voer de PIN-code in die wordt weergegeven. Raadpleeg de [documentatie](https://www.home-assistant.io/components/ps4/) voor meer informatie.",
"title": "PlayStation 4"
},
"mode": {
"data": {
- "ip_address": "IP-adres (leeg laten als u Auto Discovery gebruikt).",
+ "ip_address": "IP-adres (leeg laten indien Auto Discovery wordt gebruikt).",
"mode": "Configuratiemodus"
},
- "description": "Selecteer modus voor configuratie. Het veld IP-adres kan leeg blijven als Auto Discovery wordt geselecteerd, omdat apparaten automatisch worden gedetecteerd.",
+ "description": "Selecteer modus voor configuratie. Het veld IP-adres kan leeg worden gelaten als Auto Discovery wordt geselecteerd, omdat apparaten dan automatisch worden ontdekt.",
"title": "PlayStation 4"
}
}
diff --git a/homeassistant/components/ps4/translations/tr.json b/homeassistant/components/ps4/translations/tr.json
new file mode 100644
index 00000000000000..4e3e0b53445e79
--- /dev/null
+++ b/homeassistant/components/ps4/translations/tr.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "link": {
+ "data": {
+ "ip_address": "\u0130p Adresi"
+ }
+ },
+ "mode": {
+ "data": {
+ "ip_address": "\u0130p Adresi (Otomatik Bulma kullan\u0131l\u0131yorsa bo\u015f b\u0131rak\u0131n)."
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ps4/translations/uk.json b/homeassistant/components/ps4/translations/uk.json
new file mode 100644
index 00000000000000..696a46bf8d7f68
--- /dev/null
+++ b/homeassistant/components/ps4/translations/uk.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "credential_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0445 \u0434\u0430\u043d\u0438\u0445.",
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.",
+ "port_987_bind_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u043f\u043e\u0440\u0442\u043e\u043c 987. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/components/ps4/).",
+ "port_997_bind_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u043f\u043e\u0440\u0442\u043e\u043c 997. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/components/ps4/)."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "credential_timeout": "\u0427\u0430\u0441 \u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u0438\u043d\u0443\u0432. \u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **, \u0449\u043e\u0431 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0438 \u0441\u043f\u0440\u043e\u0431\u0443.",
+ "login_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043f\u0430\u0440\u0443 \u0437 PlayStation 4. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e PIN-\u043a\u043e\u0434 \u0432\u0432\u0435\u0434\u0435\u043d\u0438\u0439 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e.",
+ "no_ipaddress": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441\u0443 PlayStation 4."
+ },
+ "step": {
+ "creds": {
+ "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **, \u0430 \u043f\u043e\u0442\u0456\u043c \u0432 \u0434\u043e\u0434\u0430\u0442\u043a\u0443 'PS4 Second Screen' \u043e\u043d\u043e\u0432\u0456\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u0456 \u0432\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 'Home-Assistant'.",
+ "title": "PlayStation 4"
+ },
+ "link": {
+ "data": {
+ "code": "PIN-\u043a\u043e\u0434",
+ "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "region": "\u0420\u0435\u0433\u0456\u043e\u043d"
+ },
+ "description": "\u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f PIN-\u043a\u043e\u0434\u0443 \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0434\u043e \u043f\u0443\u043d\u043a\u0442\u0443 ** \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f ** \u043d\u0430 \u043a\u043e\u043d\u0441\u043e\u043b\u0456 PlayStation 4. \u041f\u043e\u0442\u0456\u043c \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 ** \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043c\u043e\u0431\u0456\u043b\u044c\u043d\u043e\u0433\u043e \u0434\u043e\u0434\u0430\u0442\u043a\u0430 ** \u0456 \u0432\u0438\u0431\u0435\u0440\u0456\u0442\u044c ** \u0414\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 ** . \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/components/ps4/) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457.",
+ "title": "PlayStation 4"
+ },
+ "mode": {
+ "data": {
+ "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430 (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c \u043f\u0440\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u0456 \u0440\u0435\u0436\u0438\u043c\u0443 \u0430\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f)",
+ "mode": "\u0420\u0435\u0436\u0438\u043c"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u041f\u043e\u043b\u0435 'IP-\u0430\u0434\u0440\u0435\u0441\u0430' \u043c\u043e\u0436\u043d\u0430 \u0437\u0430\u043b\u0438\u0448\u0438\u0442\u0438 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c, \u044f\u043a\u0449\u043e \u0432\u0438\u0431\u0440\u0430\u043d\u043e 'Auto Discovery', \u043e\u0441\u043a\u0456\u043b\u044c\u043a\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0431\u0443\u0434\u0443\u0442\u044c \u0434\u043e\u0434\u0430\u043d\u0456 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e.",
+ "title": "PlayStation 4"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/pulseaudio_loopback/switch.py b/homeassistant/components/pulseaudio_loopback/switch.py
index 9c27ab4e0277c1..260fbe65c1b168 100644
--- a/homeassistant/components/pulseaudio_loopback/switch.py
+++ b/homeassistant/components/pulseaudio_loopback/switch.py
@@ -73,7 +73,7 @@ def _get_module_idx(self):
self._pa_svr.connect()
for module in self._pa_svr.module_list():
- if not module.name == "module-loopback":
+ if module.name != "module-loopback":
continue
if f"sink={self._sink_name}" not in module.argument:
diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py
index 31f2f88dac7223..ff0ac45c1393dc 100644
--- a/homeassistant/components/push/camera.py
+++ b/homeassistant/components/push/camera.py
@@ -175,7 +175,7 @@ def motion_detection_enabled(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
name: value
diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py
index ff18e86aad992c..4f8ec6a17001e8 100644
--- a/homeassistant/components/pushbullet/sensor.py
+++ b/homeassistant/components/pushbullet/sensor.py
@@ -5,10 +5,9 @@
from pushbullet import InvalidKeyError, Listener, PushBullet
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -52,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(devices)
-class PushBulletNotificationSensor(Entity):
+class PushBulletNotificationSensor(SensorEntity):
"""Representation of a Pushbullet Sensor."""
def __init__(self, pb, element):
@@ -85,7 +84,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return all known attributes of the sensor."""
return self._state_attributes
diff --git a/homeassistant/components/pushsafer/notify.py b/homeassistant/components/pushsafer/notify.py
index 12735764b4ba5f..3337af0f8b062d 100644
--- a/homeassistant/components/pushsafer/notify.py
+++ b/homeassistant/components/pushsafer/notify.py
@@ -15,7 +15,7 @@
PLATFORM_SCHEMA,
BaseNotificationService,
)
-from homeassistant.const import HTTP_OK
+from homeassistant.const import ATTR_ICON, HTTP_OK
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -28,7 +28,6 @@
# Top level attributes in 'data'
ATTR_SOUND = "sound"
ATTR_VIBRATION = "vibration"
-ATTR_ICON = "icon"
ATTR_ICONCOLOR = "iconcolor"
ATTR_URL = "url"
ATTR_URLTITLE = "urltitle"
@@ -94,7 +93,7 @@ def send_message(self, message="", **kwargs):
_LOGGER.debug("Loading image from file %s", local_path)
picture1_encoded = self.load_from_file(local_path)
else:
- _LOGGER.warning("missing url or local_path for picture1")
+ _LOGGER.warning("Missing url or local_path for picture1")
else:
_LOGGER.debug("picture1 is not specified")
@@ -144,7 +143,7 @@ def load_from_url(self, url=None, username=None, password=None, auth=None):
else:
response = requests.get(url, timeout=CONF_TIMEOUT)
return self.get_base64(response.content, response.headers["content-type"])
- _LOGGER.warning("url not found in param")
+ _LOGGER.warning("No url was found in param")
return None
diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py
index 32d33f19e80f29..eb461061dcc1ad 100644
--- a/homeassistant/components/pvoutput/sensor.py
+++ b/homeassistant/components/pvoutput/sensor.py
@@ -6,7 +6,7 @@
import voluptuous as vol
from homeassistant.components.rest.data import RestData
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_DATE,
ATTR_TEMPERATURE,
@@ -17,7 +17,6 @@
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
_ENDPOINT = "http://pvoutput.org/service/r2/getstatus.jsp"
@@ -64,7 +63,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([PvoutputSensor(rest, name)])
-class PvoutputSensor(Entity):
+class PvoutputSensor(SensorEntity):
"""Representation of a PVOutput sensor."""
def __init__(self, rest, name):
@@ -100,7 +99,7 @@ def state(self):
return None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the monitored installation."""
if self.pvcoutput is not None:
return {
diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py
index a9b53c970bd49f..5fe65e3dc65f63 100644
--- a/homeassistant/components/pvpc_hourly_pricing/sensor.py
+++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py
@@ -1,11 +1,13 @@
"""Sensor to collect the reference daily prices of electricity ('PVPC') in Spain."""
+from __future__ import annotations
+
import logging
from random import randint
-from typing import Optional
from aiopvpc import PVPCData
from homeassistant import config_entries
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_NAME, CURRENCY_EURO, ENERGY_KILO_WATT_HOUR
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -41,7 +43,7 @@ async def async_setup_entry(
)
-class ElecPriceSensor(RestoreEntity):
+class ElecPriceSensor(RestoreEntity, SensorEntity):
"""Class to hold the prices of electricity as a sensor."""
unit_of_measurement = UNIT
@@ -92,7 +94,7 @@ async def async_added_to_hass(self):
self.update_current_price(dt_util.utcnow())
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID."""
return self._unique_id
@@ -112,7 +114,7 @@ def available(self) -> bool:
return self._pvpc_data.state_available
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._pvpc_data.attributes
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/de.json b/homeassistant/components/pvpc_hourly_pricing/translations/de.json
index 8e3e9b68e42b5d..1b5c4d37658777 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/de.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/de.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Die Integration ist bereits mit einem vorhandenen Sensor mit diesem Tarif konfiguriert"
+ "already_configured": "Der Dienst ist bereits konfiguriert"
},
"step": {
"user": {
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json
new file mode 100644
index 00000000000000..f5301e874eae05
--- /dev/null
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/id.json b/homeassistant/components/pvpc_hourly_pricing/translations/id.json
new file mode 100644
index 00000000000000..8601c31fda0c28
--- /dev/null
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/id.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Nama Sensor",
+ "tariff": "Tarif kontrak (1, 2, atau 3 periode)"
+ },
+ "description": "Sensor ini menggunakan API resmi untuk mendapatkan [harga listrik per jam (PVPC)](https://www.esios.ree.es/es/pvpc) di Spanyol.\nUntuk penjelasan yang lebih tepat, kunjungi [dokumen integrasi](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nPilih tarif kontrak berdasarkan jumlah periode penagihan per hari:\n- 1 periode: normal\n- 2 periode: diskriminasi (tarif per malam)\n- 3 periode: mobil listrik (tarif per malam 3 periode)",
+ "title": "Pemilihan tarif"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ko.json b/homeassistant/components/pvpc_hourly_pricing/translations/ko.json
index 35ac17a8bb8ff4..c44d121796104b 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/ko.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/ko.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \uc774\ubbf8 \ud574\ub2f9 \uc694\uae08\uc81c \uc13c\uc11c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
@@ -9,7 +9,7 @@
"name": "\uc13c\uc11c \uc774\ub984",
"tariff": "\uacc4\uc57d \uc694\uae08\uc81c (1, 2 \ub610\ub294 3 \uad6c\uac04)"
},
- "description": "\uc774 \uc13c\uc11c\ub294 \uacf5\uc2dd API \ub97c \uc0ac\uc6a9\ud558\uc5ec \uc2a4\ud398\uc778\uc758 [\uc2dc\uac04\ub2f9 \uc804\uae30 \uc694\uae08 (PVPC)](https://www.esios.ree.es/es/pvpc) \uc744 \uac00\uc838\uc635\ub2c8\ub2e4.\n\ubcf4\ub2e4 \uc790\uc138\ud55c \uc124\uba85\uc740 [\uc548\ub0b4](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.\n\n1\uc77c\ub2f9 \uccad\uad6c \uad6c\uac04\uc5d0 \ub530\ub77c \uacc4\uc57d \uc694\uae08\uc81c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.\n - 1 \uad6c\uac04: \uc77c\ubc18 \uc694\uae08\uc81c\n - 2 \uad6c\uac04: \ucc28\ub4f1 \uc694\uae08\uc81c (\uc57c\uac04 \uc694\uae08) \n - 3 \uad6c\uac04: \uc804\uae30\uc790\ub3d9\ucc28 (3 \uad6c\uac04 \uc57c\uac04 \uc694\uae08)",
+ "description": "\uc774 \uc13c\uc11c\ub294 \uacf5\uc2dd API\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc2a4\ud398\uc778\uc758 [\uc2dc\uac04\ub2f9 \uc804\uae30 \uc694\uae08 (PVPC)](https://www.esios.ree.es/es/pvpc) \uc744 \uac00\uc838\uc635\ub2c8\ub2e4.\n\ubcf4\ub2e4 \uc790\uc138\ud55c \uc124\uba85\uc740 [\uad00\ub828 \ubb38\uc11c](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.\n\n1\uc77c\ub2f9 \uccad\uad6c \uad6c\uac04\uc5d0 \ub530\ub77c \uacc4\uc57d \uc694\uae08\uc81c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.\n - 1 \uad6c\uac04: \uc77c\ubc18 \uc694\uae08\uc81c\n - 2 \uad6c\uac04: \ucc28\ub4f1 \uc694\uae08\uc81c (\uc57c\uac04 \uc694\uae08) \n - 3 \uad6c\uac04: \uc804\uae30\uc790\ub3d9\ucc28 (3 \uad6c\uac04 \uc57c\uac04 \uc694\uae08)",
"title": "\uc694\uae08\uc81c \uc120\ud0dd"
}
}
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json
index 3abffdf5bc0f24..5048ed498dfa7c 100644
--- a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Integratie is al geconfigureerd met een bestaande sensor met dat tarief"
+ "already_configured": "Service is al geconfigureerd"
},
"step": {
"user": {
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/tr.json b/homeassistant/components/pvpc_hourly_pricing/translations/tr.json
new file mode 100644
index 00000000000000..394f876401beb0
--- /dev/null
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/tr.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Sens\u00f6r Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/uk.json b/homeassistant/components/pvpc_hourly_pricing/translations/uk.json
new file mode 100644
index 00000000000000..da2136d7765e00
--- /dev/null
+++ b/homeassistant/components/pvpc_hourly_pricing/translations/uk.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "tariff": "\u041a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u043d\u0438\u0439 \u0442\u0430\u0440\u0438\u0444 (1, 2 \u0430\u0431\u043e 3 \u043f\u0435\u0440\u0456\u043e\u0434\u0438)"
+ },
+ "description": "\u0426\u0435\u0439 \u0441\u0435\u043d\u0441\u043e\u0440 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454 \u043e\u0444\u0456\u0446\u0456\u0439\u043d\u0438\u0439 API \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f [\u043f\u043e\u0433\u043e\u0434\u0438\u043d\u043d\u043e\u0457 \u0446\u0456\u043d\u0438 \u0437\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u0435\u043d\u0435\u0440\u0433\u0456\u044e (PVPC)] (https://www.esios.ree.es/es/pvpc) \u0432 \u0406\u0441\u043f\u0430\u043d\u0456\u0457.\n\u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u0435\u0442\u0430\u043b\u044c\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044c \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0430\u0440\u0438\u0444, \u0437\u0430\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u0439 \u043d\u0430 \u043a\u0456\u043b\u044c\u043a\u043e\u0441\u0442\u0456 \u0440\u043e\u0437\u0440\u0430\u0445\u0443\u043d\u043a\u043e\u0432\u0438\u0445 \u043f\u0435\u0440\u0456\u043e\u0434\u0456\u0432 \u0432 \u0434\u0435\u043d\u044c:\n- 1 \u043f\u0435\u0440\u0456\u043e\u0434: normal\n- 2 \u043f\u0435\u0440\u0456\u043e\u0434\u0438: discrimination (nightly rate)\n- 3 \u043f\u0435\u0440\u0456\u043e\u0434\u0438: electric car (nightly rate of 3 periods)",
+ "title": "\u0412\u0438\u0431\u0456\u0440 \u0442\u0430\u0440\u0438\u0444\u0443"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py
index 6539479d2cd08a..c439d5181be756 100644
--- a/homeassistant/components/pyload/sensor.py
+++ b/homeassistant/components/pyload/sensor.py
@@ -6,7 +6,7 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_HOST,
CONF_MONITORED_VARIABLES,
@@ -19,7 +19,6 @@
DATA_RATE_MEGABYTES_PER_SECOND,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -77,7 +76,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(devices, True)
-class PyLoadSensor(Entity):
+class PyLoadSensor(SensorEntity):
"""Representation of a pyLoad sensor."""
def __init__(self, api, sensor_type, client_name):
diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py
index bed159cae6bf9b..2051b32b63f2ab 100644
--- a/homeassistant/components/python_script/__init__.py
+++ b/homeassistant/components/python_script/__init__.py
@@ -19,7 +19,7 @@
)
import voluptuous as vol
-from homeassistant.const import SERVICE_RELOAD
+from homeassistant.const import CONF_DESCRIPTION, CONF_NAME, SERVICE_RELOAD
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.service import async_set_service_schema
from homeassistant.loader import bind_hass
@@ -71,6 +71,8 @@
"get_age",
}
+CONF_FIELDS = "fields"
+
class ScriptError(HomeAssistantError):
"""When a script error occurs."""
@@ -125,8 +127,9 @@ def python_script_service_handler(call):
hass.services.register(DOMAIN, name, python_script_service_handler)
service_desc = {
- "description": services_dict.get(name, {}).get("description", ""),
- "fields": services_dict.get(name, {}).get("fields", {}),
+ CONF_NAME: services_dict.get(name, {}).get("name", name),
+ CONF_DESCRIPTION: services_dict.get(name, {}).get("description", ""),
+ CONF_FIELDS: services_dict.get(name, {}).get("fields", {}),
}
async_set_service_schema(hass, DOMAIN, name, service_desc)
diff --git a/homeassistant/components/python_script/services.yaml b/homeassistant/components/python_script/services.yaml
index 835f6402481435..e9f860f1a6289f 100644
--- a/homeassistant/components/python_script/services.yaml
+++ b/homeassistant/components/python_script/services.yaml
@@ -1,4 +1,5 @@
# Describes the format for available python_script services
reload:
+ name: Reload
description: Reload all available python_scripts
diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py
index cd67355d88379d..251407099b159e 100644
--- a/homeassistant/components/qbittorrent/sensor.py
+++ b/homeassistant/components/qbittorrent/sensor.py
@@ -5,7 +5,7 @@
from requests.exceptions import RequestException
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_NAME,
CONF_PASSWORD,
@@ -16,7 +16,6 @@
)
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -71,7 +70,7 @@ def format_speed(speed):
return round(kb_spd, 2 if kb_spd < 0.1 else 1)
-class QBittorrentSensor(Entity):
+class QBittorrentSensor(SensorEntity):
"""Representation of an qBittorrent sensor."""
def __init__(self, sensor_type, qbittorrent_client, client_name, exception):
diff --git a/homeassistant/components/qld_bushfire/geo_location.py b/homeassistant/components/qld_bushfire/geo_location.py
index 8efb1a32705eaa..669e9d1e884cfa 100644
--- a/homeassistant/components/qld_bushfire/geo_location.py
+++ b/homeassistant/components/qld_bushfire/geo_location.py
@@ -1,7 +1,8 @@
"""Support for Queensland Bushfire Alert Feeds."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Optional
from georss_qld_bushfire_alert_client import QldBushfireAlertFeedManager
import voluptuous as vol
@@ -167,7 +168,7 @@ def _delete_callback(self):
"""Remove this entity."""
self._remove_signal_delete()
self._remove_signal_update()
- self.hass.async_create_task(self.async_remove())
+ self.hass.async_create_task(self.async_remove(force_remove=True))
@callback
def _update_callback(self):
@@ -209,22 +210,22 @@ def source(self) -> str:
return SOURCE
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the entity."""
return self._name
@property
- def distance(self) -> Optional[float]:
+ def distance(self) -> float | None:
"""Return distance value of this external event."""
return self._distance
@property
- def latitude(self) -> Optional[float]:
+ def latitude(self) -> float | None:
"""Return latitude value of this external event."""
return self._latitude
@property
- def longitude(self) -> Optional[float]:
+ def longitude(self) -> float | None:
"""Return longitude value of this external event."""
return self._longitude
@@ -234,7 +235,7 @@ def unit_of_measurement(self):
return LENGTH_KILOMETERS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
for key, value in (
diff --git a/homeassistant/components/qnap/manifest.json b/homeassistant/components/qnap/manifest.json
index b83d37f0d3c25b..29750683abf054 100644
--- a/homeassistant/components/qnap/manifest.json
+++ b/homeassistant/components/qnap/manifest.json
@@ -2,6 +2,6 @@
"domain": "qnap",
"name": "QNAP",
"documentation": "https://www.home-assistant.io/integrations/qnap",
- "requirements": ["qnapstats==0.3.0"],
+ "requirements": ["qnapstats==0.3.1"],
"codeowners": ["@colinodell"]
}
diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py
index 11faba0f210ba7..5759713e80c444 100644
--- a/homeassistant/components/qnap/sensor.py
+++ b/homeassistant/components/qnap/sensor.py
@@ -5,7 +5,7 @@
from qnapstats import QNAPStats
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_NAME,
CONF_HOST,
@@ -23,7 +23,6 @@
)
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -200,7 +199,7 @@ def update(self):
_LOGGER.exception("Failed to fetch QNAP stats from the NAS")
-class QNAPSensor(Entity):
+class QNAPSensor(SensorEntity):
"""Base class for a QNAP sensor."""
def __init__(self, api, variable, variable_info, monitor_device=None):
@@ -268,7 +267,7 @@ def state(self):
return round(used / total * 100)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._api.data:
data = self._api.data["system_stats"]["memory"]
@@ -294,7 +293,7 @@ def state(self):
return round_nicely(data["rx"] / 1024 / 1024)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._api.data:
data = self._api.data["system_stats"]["nics"][self.monitor_device]
@@ -322,7 +321,7 @@ def state(self):
return int(self._api.data["system_stats"]["system"]["temp_c"])
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._api.data:
data = self._api.data["system_stats"]
@@ -360,7 +359,7 @@ def name(self):
return f"{server_name} {self.var_name} (Drive {self.monitor_device})"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._api.data:
data = self._api.data["smart_drive_health"][self.monitor_device]
@@ -394,7 +393,7 @@ def state(self):
return round(used_gb / total_gb * 100)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._api.data:
data = self._api.data["volumes"][self.monitor_device]
diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json
index b16eace14fdc26..bd574af0297d45 100644
--- a/homeassistant/components/qrcode/manifest.json
+++ b/homeassistant/components/qrcode/manifest.json
@@ -2,6 +2,6 @@
"domain": "qrcode",
"name": "QR Code",
"documentation": "https://www.home-assistant.io/integrations/qrcode",
- "requirements": ["pillow==8.1.0", "pyzbar==0.1.7"],
+ "requirements": ["pillow==8.1.2", "pyzbar==0.1.7"],
"codeowners": []
}
diff --git a/homeassistant/components/qvr_pro/camera.py b/homeassistant/components/qvr_pro/camera.py
index 9dd8e3c4f20ba1..2f4353063d1149 100644
--- a/homeassistant/components/qvr_pro/camera.py
+++ b/homeassistant/components/qvr_pro/camera.py
@@ -82,7 +82,7 @@ def brand(self):
return self._brand
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Get the state attributes."""
attrs = {"qvr_guid": self.guid}
diff --git a/homeassistant/components/qwikswitch/sensor.py b/homeassistant/components/qwikswitch/sensor.py
index 53cf68ccdbad9e..f6d0ce7ec28956 100644
--- a/homeassistant/components/qwikswitch/sensor.py
+++ b/homeassistant/components/qwikswitch/sensor.py
@@ -3,6 +3,7 @@
from pyqwikswitch.qwikswitch import SENSORS
+from homeassistant.components.sensor import SensorEntity
from homeassistant.core import callback
from . import DOMAIN as QWIKSWITCH, QSEntity
@@ -21,7 +22,7 @@ async def async_setup_platform(hass, _, add_entities, discovery_info=None):
add_entities(devs)
-class QSSensor(QSEntity):
+class QSSensor(QSEntity, SensorEntity):
"""Sensor based on a Qwikswitch relay/dimmer module."""
_val = None
diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py
index 672ff272344cd5..30015dcf8c1de0 100644
--- a/homeassistant/components/rachio/__init__.py
+++ b/homeassistant/components/rachio/__init__.py
@@ -21,7 +21,7 @@
_LOGGER = logging.getLogger(__name__)
-SUPPORTED_DOMAINS = ["switch", "binary_sensor"]
+PLATFORMS = ["switch", "binary_sensor"]
CONFIG_SCHEMA = cv.deprecated(DOMAIN)
@@ -39,8 +39,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in SUPPORTED_DOMAINS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -99,13 +99,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
webhook_url,
)
- # Enable component
+ # Enable platform
hass.data[DOMAIN][entry.entry_id] = person
async_register_webhook(hass, webhook_id, entry.entry_id)
- for component in SUPPORTED_DOMAINS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py
index 63e5bd56954aed..5719dd810660f1 100644
--- a/homeassistant/components/rachio/config_flow.py
+++ b/homeassistant/components/rachio/config_flow.py
@@ -12,11 +12,11 @@
from .const import (
CONF_MANUAL_RUN_MINS,
DEFAULT_MANUAL_RUN_MINS,
+ DOMAIN,
KEY_ID,
KEY_STATUS,
KEY_USERNAME,
)
-from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py
index c9de7eea7d44bb..a6ed596db04216 100644
--- a/homeassistant/components/rachio/device.py
+++ b/homeassistant/components/rachio/device.py
@@ -1,6 +1,7 @@
"""Adapter to wrap the rachiopy api for home assistant."""
+from __future__ import annotations
+
import logging
-from typing import Optional
import voluptuous as vol
@@ -239,7 +240,7 @@ def list_zones(self, include_disabled=False) -> list:
# Only enabled zones
return [z for z in self._zones if z[KEY_ENABLED]]
- def get_zone(self, zone_id) -> Optional[dict]:
+ def get_zone(self, zone_id) -> dict | None:
"""Return the zone with the given ID."""
for zone in self.list_zones(include_disabled=True):
if zone[KEY_ID] == zone_id:
diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py
index 8009d79b2240cc..8d87b688aa47e7 100644
--- a/homeassistant/components/rachio/switch.py
+++ b/homeassistant/components/rachio/switch.py
@@ -1,12 +1,13 @@
"""Integration with the Rachio Iro sprinkler system controller."""
from abc import abstractmethod
+from contextlib import suppress
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.components.switch import SwitchEntity
-from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.const import ATTR_ENTITY_ID, ATTR_ID
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_platform
@@ -67,7 +68,6 @@
_LOGGER = logging.getLogger(__name__)
ATTR_DURATION = "duration"
-ATTR_ID = "id"
ATTR_PERCENT = "percent"
ATTR_SCHEDULE_SUMMARY = "Summary"
ATTR_SCHEDULE_ENABLED = "Enabled"
@@ -389,7 +389,7 @@ def entity_picture(self):
return self._entity_picture
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return the optional state attributes."""
props = {ATTR_ZONE_NUMBER: self._zone_number, ATTR_ZONE_SUMMARY: self._summary}
if self._shade_type:
@@ -495,7 +495,7 @@ def icon(self) -> str:
return "mdi:water" if self.schedule_is_enabled else "mdi:water-off"
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return the optional state attributes."""
return {
ATTR_SCHEDULE_SUMMARY: self._summary,
@@ -526,7 +526,7 @@ def turn_off(self, **kwargs) -> None:
def _async_handle_update(self, *args, **kwargs) -> None:
"""Handle incoming webhook schedule data."""
# Schedule ID not passed when running individual zones, so we catch that error
- try:
+ with suppress(KeyError):
if args[0][KEY_SCHEDULE_ID] == self._schedule_id:
if args[0][KEY_SUBTYPE] in [SUBTYPE_SCHEDULE_STARTED]:
self._state = True
@@ -535,8 +535,6 @@ def _async_handle_update(self, *args, **kwargs) -> None:
SUBTYPE_SCHEDULE_COMPLETED,
]:
self._state = False
- except KeyError:
- pass
self.async_write_ha_state()
diff --git a/homeassistant/components/rachio/translations/de.json b/homeassistant/components/rachio/translations/de.json
index e6a4d73cde1ab7..9acd92ce40de22 100644
--- a/homeassistant/components/rachio/translations/de.json
+++ b/homeassistant/components/rachio/translations/de.json
@@ -4,7 +4,7 @@
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
@@ -13,7 +13,7 @@
"data": {
"api_key": "API-Schl\u00fcssel"
},
- "description": "Sie ben\u00f6tigen den API-Schl\u00fcssel von https://app.rach.io/. W\u00e4hlen Sie \"Kontoeinstellungen\" und klicken Sie dann auf \"API-SCHL\u00dcSSEL ERHALTEN\".",
+ "description": "Du ben\u00f6tigst den API-Schl\u00fcssel von https://app.rach.io/. Gehe in die Einstellungen und klicke auf \"API-SCHL\u00dcSSEL ANFORDERN\".",
"title": "Stellen Sie eine Verbindung zu Ihrem Rachio-Ger\u00e4t her"
}
}
@@ -22,7 +22,7 @@
"step": {
"init": {
"data": {
- "manual_run_mins": "Wie lange, in Minuten, um eine Station einzuschalten, wenn der Schalter aktiviert ist."
+ "manual_run_mins": "Wie viele Minuten es laufen soll, wenn ein Zonen-Schalter aktiviert wird"
}
}
}
diff --git a/homeassistant/components/rachio/translations/hu.json b/homeassistant/components/rachio/translations/hu.json
index 5f4f7bb8bee402..570dd27b5d99ac 100644
--- a/homeassistant/components/rachio/translations/hu.json
+++ b/homeassistant/components/rachio/translations/hu.json
@@ -1,5 +1,13 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/rachio/translations/id.json b/homeassistant/components/rachio/translations/id.json
new file mode 100644
index 00000000000000..6630273424833d
--- /dev/null
+++ b/homeassistant/components/rachio/translations/id.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kunci API"
+ },
+ "description": "Anda akan memerlukan Kunci API dari https://app.rach.io/. Buka Settings, lalu klik 'GET API KEY'.",
+ "title": "Hubungkan ke perangkat Rachio Anda"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "manual_run_mins": "Durasi dalam menit yang akan dijalankan saat mengaktifkan sakelar zona"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rachio/translations/ko.json b/homeassistant/components/rachio/translations/ko.json
index 2f5724c7af1b6b..e0c7a3a13a34dc 100644
--- a/homeassistant/components/rachio/translations/ko.json
+++ b/homeassistant/components/rachio/translations/ko.json
@@ -4,7 +4,7 @@
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
@@ -13,7 +13,7 @@
"data": {
"api_key": "API \ud0a4"
},
- "description": "https://app.rach.io/ \uc758 API \ud0a4\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. \uacc4\uc815 \uc124\uc815\uc744 \uc120\ud0dd\ud55c \ub2e4\uc74c 'GET API KEY ' \ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694.",
+ "description": "https://app.rach.io/ \uc758 API \ud0a4\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. Settings\ub85c \uc774\ub3d9\ud55c \ub2e4\uc74c 'GET API KEY '\ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694.",
"title": "Rachio \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
@@ -22,7 +22,7 @@
"step": {
"init": {
"data": {
- "manual_run_mins": "\uc2a4\uc704\uce58\uac00 \ud65c\uc131\ud654\ub41c \uacbd\uc6b0 \uc2a4\ud14c\uc774\uc158\uc744 \ucf1c\ub294 \uc2dc\uac04(\ubd84) \uc785\ub2c8\ub2e4."
+ "manual_run_mins": "\uad6c\uc5ed \uc2a4\uc704\uce58\ub97c \ud65c\uc131\ud654\ud560 \ub54c \uc2e4\ud589\ud560 \uc2dc\uac04 (\ubd84)"
}
}
}
diff --git a/homeassistant/components/rachio/translations/nl.json b/homeassistant/components/rachio/translations/nl.json
index 2173c768185bc4..7071401a1672df 100644
--- a/homeassistant/components/rachio/translations/nl.json
+++ b/homeassistant/components/rachio/translations/nl.json
@@ -4,14 +4,14 @@
"already_configured": "Apparaat is al geconfigureerd"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
"step": {
"user": {
"data": {
- "api_key": "De API-sleutel voor het Rachio-account."
+ "api_key": "API-sleutel"
},
"description": "U heeft de API-sleutel nodig van https://app.rach.io/. Selecteer 'Accountinstellingen en klik vervolgens op' GET API KEY '.",
"title": "Maak verbinding met uw Rachio-apparaat"
@@ -22,7 +22,7 @@
"step": {
"init": {
"data": {
- "manual_run_mins": "Hoe lang, in minuten, om een station in te schakelen wanneer de schakelaar is ingeschakeld."
+ "manual_run_mins": "Looptijd in minuten bij activering van een zoneschakelaar"
}
}
}
diff --git a/homeassistant/components/rachio/translations/ru.json b/homeassistant/components/rachio/translations/ru.json
index 53cd98387facf9..52248b8d6860b1 100644
--- a/homeassistant/components/rachio/translations/ru.json
+++ b/homeassistant/components/rachio/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
diff --git a/homeassistant/components/rachio/translations/sv.json b/homeassistant/components/rachio/translations/sv.json
index c2da7a1c01d7f4..4932b17ebfa49d 100644
--- a/homeassistant/components/rachio/translations/sv.json
+++ b/homeassistant/components/rachio/translations/sv.json
@@ -12,7 +12,8 @@
"user": {
"data": {
"api_key": "API nyckel"
- }
+ },
+ "title": "Anslut till Rachio-enheten"
}
}
}
diff --git a/homeassistant/components/rachio/translations/tr.json b/homeassistant/components/rachio/translations/tr.json
new file mode 100644
index 00000000000000..8bbc4eb1e49db7
--- /dev/null
+++ b/homeassistant/components/rachio/translations/tr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Anahtar\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rachio/translations/uk.json b/homeassistant/components/rachio/translations/uk.json
new file mode 100644
index 00000000000000..af5d7cd39d97a7
--- /dev/null
+++ b/homeassistant/components/rachio/translations/uk.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API"
+ },
+ "description": "\u0414\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d \u043a\u043b\u044e\u0447 API \u0437 \u0441\u0430\u0439\u0442\u0443 https://app.rach.io/. \u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0432 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f, \u0430 \u043f\u043e\u0442\u0456\u043c \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c 'GET API KEY'.",
+ "title": "Rachio"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "manual_run_mins": "\u0422\u0440\u0438\u0432\u0430\u043b\u0456\u0441\u0442\u044c \u0440\u043e\u0431\u043e\u0442\u0438 \u043f\u0440\u0438 \u0430\u043a\u0442\u0438\u0432\u0430\u0446\u0456\u0457 \u043f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u0447\u0430 \u0437\u043e\u043d\u0438 (\u0432 \u0445\u0432\u0438\u043b\u0438\u043d\u0430\u0445)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rachio/webhooks.py b/homeassistant/components/rachio/webhooks.py
index c175117efcb344..94c79a1504f062 100644
--- a/homeassistant/components/rachio/webhooks.py
+++ b/homeassistant/components/rachio/webhooks.py
@@ -1,7 +1,4 @@
"""Webhooks used by rachio."""
-
-import logging
-
from aiohttp import web
from homeassistant.const import URL_API
@@ -80,9 +77,6 @@
}
-_LOGGER = logging.getLogger(__name__)
-
-
@callback
def async_register_webhook(hass, webhook_id, entry_id):
"""Register a webhook."""
diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py
index 27365271014789..542ff285261f58 100644
--- a/homeassistant/components/radarr/sensor.py
+++ b/homeassistant/components/radarr/sensor.py
@@ -7,7 +7,7 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_API_KEY,
CONF_HOST,
@@ -26,7 +26,6 @@
HTTP_OK,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -95,7 +94,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([RadarrSensor(hass, config, sensor) for sensor in conditions], True)
-class RadarrSensor(Entity):
+class RadarrSensor(SensorEntity):
"""Implementation of the Radarr sensor."""
def __init__(self, hass, conf, sensor_type):
@@ -144,7 +143,7 @@ def unit_of_measurement(self):
return self._unit
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
attributes = {}
if self.type == "upcoming":
diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py
index f09ef95170f68d..aad6bf3989ef35 100644
--- a/homeassistant/components/radiotherm/climate.py
+++ b/homeassistant/components/radiotherm/climate.py
@@ -181,7 +181,7 @@ def precision(self):
return PRECISION_HALVES
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
return {ATTR_FAN_ACTION: self._fstate}
@@ -372,6 +372,6 @@ def set_preset_mode(self, preset_mode):
self.device.program_mode = PRESET_MODE_TO_CODE[preset_mode]
else:
_LOGGER.error(
- "preset_mode %s not in PRESET_MODES",
+ "Preset_mode %s not in PRESET_MODES",
preset_mode,
)
diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py
index 83c358c480b0e7..d8334470b60a11 100644
--- a/homeassistant/components/rainbird/__init__.py
+++ b/homeassistant/components/rainbird/__init__.py
@@ -16,7 +16,7 @@
CONF_ZONES = "zones"
-SUPPORTED_PLATFORMS = [switch.DOMAIN, sensor.DOMAIN, binary_sensor.DOMAIN]
+PLATFORMS = [switch.DOMAIN, sensor.DOMAIN, binary_sensor.DOMAIN]
_LOGGER = logging.getLogger(__name__)
@@ -80,7 +80,7 @@ def _setup_controller(hass, controller_config, config):
return False
hass.data[DATA_RAINBIRD].append(controller)
_LOGGER.debug("Rain Bird Controller %d set to: %s", position, server)
- for platform in SUPPORTED_PLATFORMS:
+ for platform in PLATFORMS:
discovery.load_platform(
hass,
platform,
diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py
index 501566de6823bc..2c542dc12a9af8 100644
--- a/homeassistant/components/rainbird/sensor.py
+++ b/homeassistant/components/rainbird/sensor.py
@@ -3,7 +3,7 @@
from pyrainbird import RainbirdController
-from homeassistant.helpers.entity import Entity
+from homeassistant.components.sensor import SensorEntity
from . import (
DATA_RAINBIRD,
@@ -28,7 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
)
-class RainBirdSensor(Entity):
+class RainBirdSensor(SensorEntity):
"""A sensor implementation for Rain Bird device."""
def __init__(self, controller: RainbirdController, sensor_type):
diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py
index bccd4d2986ca69..7acb9740616570 100644
--- a/homeassistant/components/rainbird/switch.py
+++ b/homeassistant/components/rainbird/switch.py
@@ -78,7 +78,7 @@ def __init__(self, controller: RainbirdController, zone, time, name):
self._attributes = {ATTR_DURATION: self._duration, "zone": self._zone}
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return state attributes."""
return self._attributes
diff --git a/homeassistant/components/raincloud/__init__.py b/homeassistant/components/raincloud/__init__.py
index 5955ef67168745..51e9f5de5cecfc 100644
--- a/homeassistant/components/raincloud/__init__.py
+++ b/homeassistant/components/raincloud/__init__.py
@@ -161,12 +161,7 @@ def _update_callback(self):
self.schedule_update_ha_state(True)
@property
- def unit_of_measurement(self):
- """Return the units of measurement."""
- return UNIT_OF_MEASUREMENT_MAP.get(self._sensor_type)
-
- @property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION, "identifier": self.data.serial}
diff --git a/homeassistant/components/raincloud/sensor.py b/homeassistant/components/raincloud/sensor.py
index b819b51365e49c..ee8f68734ade26 100644
--- a/homeassistant/components/raincloud/sensor.py
+++ b/homeassistant/components/raincloud/sensor.py
@@ -3,12 +3,18 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_MONITORED_CONDITIONS
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.icon import icon_for_battery_level
-from . import DATA_RAINCLOUD, ICON_MAP, SENSORS, RainCloudEntity
+from . import (
+ DATA_RAINCLOUD,
+ ICON_MAP,
+ SENSORS,
+ UNIT_OF_MEASUREMENT_MAP,
+ RainCloudEntity,
+)
_LOGGER = logging.getLogger(__name__)
@@ -38,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return True
-class RainCloudSensor(RainCloudEntity):
+class RainCloudSensor(RainCloudEntity, SensorEntity):
"""A sensor implementation for raincloud device."""
@property
@@ -46,6 +52,11 @@ def state(self):
"""Return the state of the sensor."""
return self._state
+ @property
+ def unit_of_measurement(self):
+ """Return the units of measurement."""
+ return UNIT_OF_MEASUREMENT_MAP.get(self._sensor_type)
+
def update(self):
"""Get the latest data and updates the states."""
_LOGGER.debug("Updating RainCloud sensor: %s", self._name)
diff --git a/homeassistant/components/raincloud/switch.py b/homeassistant/components/raincloud/switch.py
index d6733412cac9ac..d15f5c7c0475bf 100644
--- a/homeassistant/components/raincloud/switch.py
+++ b/homeassistant/components/raincloud/switch.py
@@ -83,7 +83,7 @@ def update(self):
self._state = self.data.auto_watering
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py
index 99751e63f5b231..d333f9437f19ea 100644
--- a/homeassistant/components/rainforest_eagle/sensor.py
+++ b/homeassistant/components/rainforest_eagle/sensor.py
@@ -7,14 +7,13 @@
from uEagle import Eagle as LegacyReader
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_IP_ADDRESS,
DEVICE_CLASS_POWER,
ENERGY_KILO_WATT_HOUR,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
CONF_CLOUD_ID = "cloud_id"
@@ -56,14 +55,18 @@ def hwtest(cloud_id, install_code, ip_address):
response = reader.get_network_info()
# Branch to test if target is Legacy Model
- if "NetworkInfo" in response:
- if response["NetworkInfo"].get("ModelId", None) == "Z109-EAGLE":
- return reader
+ if (
+ "NetworkInfo" in response
+ and response["NetworkInfo"].get("ModelId", None) == "Z109-EAGLE"
+ ):
+ return reader
# Branch to test if target is Eagle-200 Model
- if "Response" in response:
- if response["Response"].get("Command", None) == "get_network_info":
- return EagleReader(ip_address, cloud_id, install_code)
+ if (
+ "Response" in response
+ and response["Response"].get("Command", None) == "get_network_info"
+ ):
+ return EagleReader(ip_address, cloud_id, install_code)
# Catch-all if hardware ID tests fail
raise ValueError("Couldn't determine device model.")
@@ -95,7 +98,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors)
-class EagleSensor(Entity):
+class EagleSensor(SensorEntity):
"""Implementation of the Rainforest Eagle-200 sensor."""
def __init__(self, eagle_data, sensor_type, name, unit):
@@ -160,7 +163,7 @@ def get_state(self, sensor_type):
return state
-class LeagleReader(LegacyReader):
+class LeagleReader(LegacyReader, SensorEntity):
"""Wraps uEagle to make it behave like eagle_reader, offering update()."""
def update(self):
diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py
index 98fbdbcf4015e9..e71e8a1f6d2c15 100644
--- a/homeassistant/components/rainmachine/__init__.py
+++ b/homeassistant/components/rainmachine/__init__.py
@@ -155,9 +155,9 @@ async def async_update(api_category: str) -> dict:
await asyncio.gather(*controller_init_tasks)
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
hass.data[DOMAIN][DATA_LISTENER] = entry.add_update_listener(async_reload_entry)
@@ -170,8 +170,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -225,7 +225,7 @@ def device_info(self) -> dict:
}
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return the state attributes."""
return self._attrs
diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py
index 80540491ee714d..e076a1055766cc 100644
--- a/homeassistant/components/rainmachine/config_flow.py
+++ b/homeassistant/components/rainmachine/config_flow.py
@@ -8,12 +8,7 @@
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client, config_validation as cv
-from .const import ( # pylint: disable=unused-import
- CONF_ZONE_RUN_TIME,
- DEFAULT_PORT,
- DEFAULT_ZONE_RUN,
- DOMAIN,
-)
+from .const import CONF_ZONE_RUN_TIME, DEFAULT_PORT, DEFAULT_ZONE_RUN, DOMAIN
DATA_SCHEMA = vol.Schema(
{
diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py
index 4533397fb54c73..20912809cb1a44 100644
--- a/homeassistant/components/rainmachine/sensor.py
+++ b/homeassistant/components/rainmachine/sensor.py
@@ -4,6 +4,7 @@
from regenmaschine.controller import Controller
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import TEMP_CELSIUS, VOLUME_CUBIC_METERS
from homeassistant.core import HomeAssistant, callback
@@ -108,7 +109,7 @@ def async_get_sensor(api_category: str) -> partial:
)
-class RainMachineSensor(RainMachineEntity):
+class RainMachineSensor(RainMachineEntity, SensorEntity):
"""Define a general RainMachine sensor."""
def __init__(
diff --git a/homeassistant/components/rainmachine/translations/de.json b/homeassistant/components/rainmachine/translations/de.json
index 92df52bb1481ec..511d85b36b6c4f 100644
--- a/homeassistant/components/rainmachine/translations/de.json
+++ b/homeassistant/components/rainmachine/translations/de.json
@@ -1,7 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Dieser RainMachine-Kontroller ist bereits konfiguriert."
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
},
"step": {
"user": {
diff --git a/homeassistant/components/rainmachine/translations/he.json b/homeassistant/components/rainmachine/translations/he.json
new file mode 100644
index 00000000000000..3007c0e968c1dc
--- /dev/null
+++ b/homeassistant/components/rainmachine/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rainmachine/translations/hu.json b/homeassistant/components/rainmachine/translations/hu.json
index 44e24519ca2e48..48718980e2efe4 100644
--- a/homeassistant/components/rainmachine/translations/hu.json
+++ b/homeassistant/components/rainmachine/translations/hu.json
@@ -1,5 +1,11 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
+ },
"step": {
"user": {
"data": {
@@ -16,7 +22,8 @@
"init": {
"data": {
"zone_run_time": "Alap\u00e9rtelmezett z\u00f3nafut\u00e1si id\u0151 (m\u00e1sodpercben)"
- }
+ },
+ "title": "RainMachine konfigur\u00e1l\u00e1sa"
}
}
}
diff --git a/homeassistant/components/rainmachine/translations/id.json b/homeassistant/components/rainmachine/translations/id.json
new file mode 100644
index 00000000000000..482ffb752785fc
--- /dev/null
+++ b/homeassistant/components/rainmachine/translations/id.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "Nama Host atau Alamat IP",
+ "password": "Kata Sandi",
+ "port": "Port"
+ },
+ "title": "Isi informasi Anda"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "zone_run_time": "Waktu berjalan zona default (dalam detik)"
+ },
+ "title": "Konfigurasikan RainMachine"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rainmachine/translations/ko.json b/homeassistant/components/rainmachine/translations/ko.json
index e1c78ae82479a5..0e38f4c4dfab07 100644
--- a/homeassistant/components/rainmachine/translations/ko.json
+++ b/homeassistant/components/rainmachine/translations/ko.json
@@ -1,7 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 RainMachine \ucee8\ud2b8\ub864\ub7ec\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
@@ -13,5 +16,15 @@
"title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4 \uc785\ub825\ud558\uae30"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "zone_run_time": "\uae30\ubcf8 \uc9c0\uc5ed \uc2e4\ud589 \uc2dc\uac04 (\ucd08)"
+ },
+ "title": "RainMachine \uad6c\uc131\ud558\uae30"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/rainmachine/translations/nl.json b/homeassistant/components/rainmachine/translations/nl.json
index adaa8cb5f308b4..8b767ced6c0a43 100644
--- a/homeassistant/components/rainmachine/translations/nl.json
+++ b/homeassistant/components/rainmachine/translations/nl.json
@@ -1,7 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Deze RainMachine controller is al geconfigureerd."
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "invalid_auth": "Ongeldige authenticatie"
},
"step": {
"user": {
@@ -13,5 +16,15 @@
"title": "Vul uw gegevens in"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "zone_run_time": "Standaardlooptijd van de zone (in seconden)"
+ },
+ "title": "Configureer RainMachine"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/rainmachine/translations/ru.json b/homeassistant/components/rainmachine/translations/ru.json
index 08ce690d22f327..8502b66aff7e5f 100644
--- a/homeassistant/components/rainmachine/translations/ru.json
+++ b/homeassistant/components/rainmachine/translations/ru.json
@@ -4,7 +4,7 @@
"already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
},
"error": {
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"step": {
"user": {
diff --git a/homeassistant/components/rainmachine/translations/tr.json b/homeassistant/components/rainmachine/translations/tr.json
index 20f74cae994549..80cfc05e568eaa 100644
--- a/homeassistant/components/rainmachine/translations/tr.json
+++ b/homeassistant/components/rainmachine/translations/tr.json
@@ -1,4 +1,21 @@
{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "Ana makine ad\u0131 veya IP adresi",
+ "password": "Parola",
+ "port": "Port"
+ }
+ }
+ }
+ },
"options": {
"step": {
"init": {
diff --git a/homeassistant/components/rainmachine/translations/uk.json b/homeassistant/components/rainmachine/translations/uk.json
new file mode 100644
index 00000000000000..ff8d7089cecb83
--- /dev/null
+++ b/homeassistant/components/rainmachine/translations/uk.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "\u0414\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "title": "RainMachine"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "zone_run_time": "\u0427\u0430\u0441 \u0440\u043e\u0431\u043e\u0442\u0438 \u0437\u043e\u043d\u0438 \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
+ },
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f RainMachine"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/random/sensor.py b/homeassistant/components/random/sensor.py
index 58d996bc6ec913..6465b828be1cfe 100644
--- a/homeassistant/components/random/sensor.py
+++ b/homeassistant/components/random/sensor.py
@@ -3,7 +3,7 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_MAXIMUM,
CONF_MINIMUM,
@@ -11,7 +11,6 @@
CONF_UNIT_OF_MEASUREMENT,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
ATTR_MAXIMUM = "maximum"
ATTR_MINIMUM = "minimum"
@@ -42,7 +41,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([RandomSensor(name, minimum, maximum, unit)], True)
-class RandomSensor(Entity):
+class RandomSensor(SensorEntity):
"""Representation of a Random number sensor."""
def __init__(self, name, minimum, maximum, unit_of_measurement):
@@ -74,7 +73,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the attributes of the sensor."""
return {ATTR_MAXIMUM: self._maximum, ATTR_MINIMUM: self._minimum}
diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py
index 0600d73d8a1bf7..2e6f780c74928c 100644
--- a/homeassistant/components/recollect_waste/__init__.py
+++ b/homeassistant/components/recollect_waste/__init__.py
@@ -1,14 +1,14 @@
"""The ReCollect Waste integration."""
+from __future__ import annotations
+
import asyncio
from datetime import date, timedelta
-from typing import List
from aiorecollect.client import Client, PickupEvent
from aiorecollect.errors import RecollectError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data[CONF_PLACE_ID], entry.data[CONF_SERVICE_ID], session=session
)
- async def async_get_pickup_events() -> List[PickupEvent]:
+ async def async_get_pickup_events() -> list[PickupEvent]:
"""Get the next pickup."""
try:
return await client.async_get_pickup_events(
@@ -54,16 +54,13 @@ async def async_get_pickup_events() -> List[PickupEvent]:
update_method=async_get_pickup_events,
)
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = coordinator
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
hass.data[DOMAIN][DATA_LISTENER][entry.entry_id] = entry.add_update_listener(
@@ -83,8 +80,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/recollect_waste/config_flow.py b/homeassistant/components/recollect_waste/config_flow.py
index 8e208f57cc6c78..62b42e2bddf6d0 100644
--- a/homeassistant/components/recollect_waste/config_flow.py
+++ b/homeassistant/components/recollect_waste/config_flow.py
@@ -1,5 +1,5 @@
"""Config flow for ReCollect Waste integration."""
-from typing import Optional
+from __future__ import annotations
from aiorecollect.client import Client
from aiorecollect.errors import RecollectError
@@ -10,12 +10,7 @@
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
-from .const import ( # pylint:disable=unused-import
- CONF_PLACE_ID,
- CONF_SERVICE_ID,
- DOMAIN,
- LOGGER,
-)
+from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DOMAIN, LOGGER
DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_PLACE_ID): str, vol.Required(CONF_SERVICE_ID): str}
@@ -83,7 +78,7 @@ def __init__(self, entry: config_entries.ConfigEntry):
"""Initialize."""
self._entry = entry
- async def async_step_init(self, user_input: Optional[dict] = None):
+ async def async_step_init(self, user_input: dict | None = None):
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json
index dc8a85ce2aacd2..4e7568a3fff30a 100644
--- a/homeassistant/components/recollect_waste/manifest.json
+++ b/homeassistant/components/recollect_waste/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/recollect_waste",
"requirements": [
- "aiorecollect==1.0.1"
+ "aiorecollect==1.0.4"
],
"codeowners": [
"@bachya"
diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py
index d66c2aae0e4c81..b95f1d6e8fa212 100644
--- a/homeassistant/components/recollect_waste/sensor.py
+++ b/homeassistant/components/recollect_waste/sensor.py
@@ -1,18 +1,26 @@
"""Support for ReCollect Waste sensors."""
-from typing import Callable, List
+from __future__ import annotations
+
+from typing import Callable
from aiorecollect.client import PickupType
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_FRIENDLY_NAME
+from homeassistant.const import (
+ ATTR_ATTRIBUTION,
+ CONF_FRIENDLY_NAME,
+ CONF_NAME,
+ DEVICE_CLASS_TIMESTAMP,
+)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
+from homeassistant.util.dt import as_utc
from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN, LOGGER
@@ -23,9 +31,6 @@
DEFAULT_ATTRIBUTION = "Pickup data provided by ReCollect Waste"
DEFAULT_NAME = "recollect_waste"
-DEFAULT_ICON = "mdi:trash-can-outline"
-
-CONF_NAME = "name"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -38,8 +43,8 @@
@callback
def async_get_pickup_type_names(
- entry: ConfigEntry, pickup_types: List[PickupType]
-) -> List[str]:
+ entry: ConfigEntry, pickup_types: list[PickupType]
+) -> list[str]:
"""Return proper pickup type names from their associated objects."""
return [
t.friendly_name
@@ -57,8 +62,8 @@ async def async_setup_platform(
):
"""Import Recollect Waste configuration from YAML."""
LOGGER.warning(
- "Loading ReCollect Waste via platform setup is deprecated. "
- "Please remove it from your configuration."
+ "Loading ReCollect Waste via platform setup is deprecated; "
+ "Please remove it from your configuration"
)
hass.async_create_task(
hass.config_entries.flow.async_init(
@@ -77,7 +82,7 @@ async def async_setup_entry(
async_add_entities([ReCollectWasteSensor(coordinator, entry)])
-class ReCollectWasteSensor(CoordinatorEntity):
+class ReCollectWasteSensor(CoordinatorEntity, SensorEntity):
"""ReCollect Waste Sensor."""
def __init__(self, coordinator: DataUpdateCoordinator, entry: ConfigEntry) -> None:
@@ -88,14 +93,14 @@ def __init__(self, coordinator: DataUpdateCoordinator, entry: ConfigEntry) -> No
self._state = None
@property
- def device_state_attributes(self) -> dict:
- """Return the state attributes."""
- return self._attributes
+ def device_class(self) -> dict:
+ """Return the device class."""
+ return DEVICE_CLASS_TIMESTAMP
@property
- def icon(self) -> str:
- """Icon to use in the frontend."""
- return DEFAULT_ICON
+ def extra_state_attributes(self) -> dict:
+ """Return the state attributes."""
+ return self._attributes
@property
def name(self) -> str:
@@ -128,9 +133,8 @@ def update_from_latest_data(self) -> None:
"""Update the state."""
pickup_event = self.coordinator.data[0]
next_pickup_event = self.coordinator.data[1]
- next_date = str(next_pickup_event.date)
- self._state = pickup_event.date
+ self._state = as_utc(pickup_event.date).isoformat()
self._attributes.update(
{
ATTR_PICKUP_TYPES: async_get_pickup_type_names(
@@ -140,6 +144,6 @@ def update_from_latest_data(self) -> None:
ATTR_NEXT_PICKUP_TYPES: async_get_pickup_type_names(
self._entry, next_pickup_event.pickup_types
),
- ATTR_NEXT_PICKUP_DATE: next_date,
+ ATTR_NEXT_PICKUP_DATE: as_utc(next_pickup_event.date).isoformat(),
}
)
diff --git a/homeassistant/components/recollect_waste/translations/de.json b/homeassistant/components/recollect_waste/translations/de.json
new file mode 100644
index 00000000000000..7cbcea1b25e301
--- /dev/null
+++ b/homeassistant/components/recollect_waste/translations/de.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "place_id": "Platz-ID",
+ "service_id": "Dienst-ID"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "Recollect Waste konfigurieren"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recollect_waste/translations/es.json b/homeassistant/components/recollect_waste/translations/es.json
index 2fdeb991bfde40..69a39d435ebbe1 100644
--- a/homeassistant/components/recollect_waste/translations/es.json
+++ b/homeassistant/components/recollect_waste/translations/es.json
@@ -20,7 +20,8 @@
"init": {
"data": {
"friendly_name": "Utilizar nombres descriptivos para los tipos de recogida (cuando sea posible)"
- }
+ },
+ "title": "Configurar la recogida de residuos"
}
}
}
diff --git a/homeassistant/components/recollect_waste/translations/fr.json b/homeassistant/components/recollect_waste/translations/fr.json
new file mode 100644
index 00000000000000..dc62c8f520a384
--- /dev/null
+++ b/homeassistant/components/recollect_waste/translations/fr.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9ja configur\u00e9 "
+ },
+ "error": {
+ "invalid_place_or_service_id": "ID de lieu ou de service non valide"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "place_id": "Identifiant de lieu",
+ "service_id": "ID de service"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "friendly_name": "Utilisez des noms conviviaux pour les types de ramassage (si possible)"
+ },
+ "title": "Configurer Recollect Waste"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recollect_waste/translations/hu.json b/homeassistant/components/recollect_waste/translations/hu.json
index 112c8cb83858b9..3222f50be02af5 100644
--- a/homeassistant/components/recollect_waste/translations/hu.json
+++ b/homeassistant/components/recollect_waste/translations/hu.json
@@ -14,5 +14,12 @@
}
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "Recollect Waste konfigur\u00e1l\u00e1sa"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/recollect_waste/translations/id.json b/homeassistant/components/recollect_waste/translations/id.json
new file mode 100644
index 00000000000000..1c0656810dd7ec
--- /dev/null
+++ b/homeassistant/components/recollect_waste/translations/id.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "invalid_place_or_service_id": "Place atau Service ID tidak valid"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "place_id": "Place ID",
+ "service_id": "Service ID"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "friendly_name": "Gunakan nama alias untuk jenis pengambilan (jika memungkinkan)"
+ },
+ "title": "Konfigurasikan Recollect Waste"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recollect_waste/translations/ko.json b/homeassistant/components/recollect_waste/translations/ko.json
new file mode 100644
index 00000000000000..422843c67f232b
--- /dev/null
+++ b/homeassistant/components/recollect_waste/translations/ko.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "invalid_place_or_service_id": "\uc7a5\uc18c \ub610\ub294 \uc11c\ube44\uc2a4 ID\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "place_id": "\uc7a5\uc18c ID",
+ "service_id": "\uc11c\ube44\uc2a4 ID"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "friendly_name": "\uc218\uac70 \uc720\ud615\uc5d0 \uce5c\uc219\ud55c \uc774\ub984\uc744 \uc0ac\uc6a9\ud558\uae30 (\uac00\ub2a5\ud55c \uacbd\uc6b0)"
+ },
+ "title": "Recollect Waste \uad6c\uc131\ud558\uae30"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recollect_waste/translations/lb.json b/homeassistant/components/recollect_waste/translations/lb.json
new file mode 100644
index 00000000000000..4e312bb0f23485
--- /dev/null
+++ b/homeassistant/components/recollect_waste/translations/lb.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparat ass scho konfigur\u00e9iert"
+ },
+ "error": {
+ "invalid_place_or_service_id": "Ong\u00eblteg Place oder Service ID"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "place_id": "Place ID",
+ "service_id": "Service ID"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recollect_waste/translations/nl.json b/homeassistant/components/recollect_waste/translations/nl.json
new file mode 100644
index 00000000000000..eec63605267ba2
--- /dev/null
+++ b/homeassistant/components/recollect_waste/translations/nl.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "invalid_place_or_service_id": "Ongeldige plaats of service-ID"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "place_id": "Plaats-ID",
+ "service_id": "Service-ID"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "friendly_name": "Gebruik vriendelijke namen voor afhaaltypes (indien mogelijk)"
+ },
+ "title": "Configureer Recollect Waste"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recollect_waste/translations/pl.json b/homeassistant/components/recollect_waste/translations/pl.json
index 013d0028790658..cc0342e93d75da 100644
--- a/homeassistant/components/recollect_waste/translations/pl.json
+++ b/homeassistant/components/recollect_waste/translations/pl.json
@@ -14,5 +14,15 @@
}
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "friendly_name": "U\u017cywaj przyjaznych nazw dla typu odbioru (je\u015bli to mo\u017cliwe)"
+ },
+ "title": "Konfiguracja Recollect Waste"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/recollect_waste/translations/tr.json b/homeassistant/components/recollect_waste/translations/tr.json
new file mode 100644
index 00000000000000..5307276a71d3a3
--- /dev/null
+++ b/homeassistant/components/recollect_waste/translations/tr.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recollect_waste/translations/uk.json b/homeassistant/components/recollect_waste/translations/uk.json
new file mode 100644
index 00000000000000..db47699f1bacf0
--- /dev/null
+++ b/homeassistant/components/recollect_waste/translations/uk.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "invalid_place_or_service_id": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 ID \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f \u0430\u0431\u043e \u0441\u043b\u0443\u0436\u0431\u0438."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "place_id": "ID \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f",
+ "service_id": "ID \u0441\u043b\u0443\u0436\u0431\u0438"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "friendly_name": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0437\u0440\u043e\u0437\u0443\u043c\u0456\u043b\u0456 \u0456\u043c\u0435\u043d\u0430 \u0434\u043b\u044f \u0442\u0438\u043f\u0456\u0432 \u0432\u0438\u0431\u043e\u0440\u0443 (\u044f\u043a\u0449\u043e \u043c\u043e\u0436\u043b\u0438\u0432\u043e)"
+ },
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Recollect Waste"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py
index 0f8a5ae7f8f915..f93d965a4b9d05 100644
--- a/homeassistant/components/recorder/__init__.py
+++ b/homeassistant/components/recorder/__init__.py
@@ -1,13 +1,15 @@
"""Support for recording details."""
+from __future__ import annotations
+
import asyncio
-from collections import namedtuple
import concurrent.futures
from datetime import datetime
import logging
import queue
+import sqlite3
import threading
import time
-from typing import Any, Callable, List, Optional
+from typing import Any, Callable, NamedTuple
from sqlalchemy import create_engine, event as sqlalchemy_event, exc, select
from sqlalchemy.orm import scoped_session, sessionmaker
@@ -37,21 +39,32 @@
from . import migration, purge
from .const import CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, DOMAIN, SQLITE_URL_PREFIX
from .models import Base, Events, RecorderRuns, States
-from .util import session_scope, validate_or_move_away_sqlite_database
+from .util import (
+ dburl_to_path,
+ move_away_broken_database,
+ session_scope,
+ validate_or_move_away_sqlite_database,
+)
_LOGGER = logging.getLogger(__name__)
SERVICE_PURGE = "purge"
+SERVICE_ENABLE = "enable"
+SERVICE_DISABLE = "disable"
ATTR_KEEP_DAYS = "keep_days"
ATTR_REPACK = "repack"
+ATTR_APPLY_FILTER = "apply_filter"
SERVICE_PURGE_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_KEEP_DAYS): cv.positive_int,
vol.Optional(ATTR_REPACK, default=False): cv.boolean,
+ vol.Optional(ATTR_APPLY_FILTER, default=False): cv.boolean,
}
)
+SERVICE_ENABLE_SCHEMA = vol.Schema({})
+SERVICE_DISABLE_SCHEMA = vol.Schema({})
DEFAULT_URL = "sqlite:///{hass_config_path}"
DEFAULT_DB_FILE = "home-assistant_v2.db"
@@ -114,7 +127,7 @@
)
-def run_information(hass, point_in_time: Optional[datetime] = None):
+def run_information(hass, point_in_time: datetime | None = None):
"""Return information about current run.
There is also the run that covers point_in_time.
@@ -127,7 +140,7 @@ def run_information(hass, point_in_time: Optional[datetime] = None):
return run_information_with_session(session, point_in_time)
-def run_information_from_instance(hass, point_in_time: Optional[datetime] = None):
+def run_information_from_instance(hass, point_in_time: datetime | None = None):
"""Return information about current run from the existing instance.
Does not query the database for older runs.
@@ -138,7 +151,7 @@ def run_information_from_instance(hass, point_in_time: Optional[datetime] = None
return ins.run_info
-def run_information_with_session(session, point_in_time: Optional[datetime] = None):
+def run_information_with_session(session, point_in_time: datetime | None = None):
"""Return information about current run from the database."""
recorder_runs = RecorderRuns
@@ -193,10 +206,32 @@ async def async_handle_purge_service(service):
DOMAIN, SERVICE_PURGE, async_handle_purge_service, schema=SERVICE_PURGE_SCHEMA
)
+ async def async_handle_enable_sevice(service):
+ instance.set_enable(True)
+
+ hass.services.async_register(
+ DOMAIN, SERVICE_ENABLE, async_handle_enable_sevice, schema=SERVICE_ENABLE_SCHEMA
+ )
+
+ async def async_handle_disable_service(service):
+ instance.set_enable(False)
+
+ hass.services.async_register(
+ DOMAIN,
+ SERVICE_DISABLE,
+ async_handle_disable_service,
+ schema=SERVICE_DISABLE_SCHEMA,
+ )
+
return await instance.async_db_ready
-PurgeTask = namedtuple("PurgeTask", ["keep_days", "repack"])
+class PurgeTask(NamedTuple):
+ """Object to store information about purge task."""
+
+ keep_days: int
+ repack: bool
+ apply_filter: bool
class WaitTask:
@@ -216,7 +251,7 @@ def __init__(
db_max_retries: int,
db_retry_wait: int,
entity_filter: Callable[[str], bool],
- exclude_t: List[str],
+ exclude_t: list[str],
db_integrity_check: bool,
) -> None:
"""Initialize the recorder."""
@@ -247,55 +282,42 @@ def __init__(
self._pending_expunge = []
self.event_session = None
self.get_session = None
- self._completed_database_setup = False
+ self._completed_database_setup = None
+
+ self.enabled = True
+
+ def set_enable(self, enable):
+ """Enable or disable recording events and states."""
+ self.enabled = enable
@callback
def async_initialize(self):
"""Initialize the recorder."""
- self.hass.bus.async_listen(MATCH_ALL, self.event_listener)
+ self.hass.bus.async_listen(
+ MATCH_ALL, self.event_listener, event_filter=self._async_event_filter
+ )
+
+ @callback
+ def _async_event_filter(self, event):
+ """Filter events."""
+ if event.event_type in self.exclude_t:
+ return False
+
+ entity_id = event.data.get(ATTR_ENTITY_ID)
+ return bool(entity_id is None or self.entity_filter(entity_id))
def do_adhoc_purge(self, **kwargs):
"""Trigger an adhoc purge retaining keep_days worth of data."""
keep_days = kwargs.get(ATTR_KEEP_DAYS, self.keep_days)
repack = kwargs.get(ATTR_REPACK)
+ apply_filter = kwargs.get(ATTR_APPLY_FILTER)
- self.queue.put(PurgeTask(keep_days, repack))
+ self.queue.put(PurgeTask(keep_days, repack, apply_filter))
def run(self):
"""Start processing events to save."""
- tries = 1
- connected = False
- while not connected and tries <= self.db_max_retries:
- if tries != 1:
- time.sleep(self.db_retry_wait)
- try:
- self._setup_connection()
- migration.migrate_schema(self)
- self._setup_run()
- connected = True
- _LOGGER.debug("Connected to recorder database")
- except Exception as err: # pylint: disable=broad-except
- _LOGGER.error(
- "Error during connection setup: %s (retrying in %s seconds)",
- err,
- self.db_retry_wait,
- )
- tries += 1
-
- if not connected:
-
- @callback
- def connection_failed():
- """Connect failed tasks."""
- self.async_db_ready.set_result(False)
- persistent_notification.async_create(
- self.hass,
- "The recorder could not start, please check the log",
- "Recorder",
- )
-
- self.hass.add_job(connection_failed)
+ if not self._setup_recorder():
return
shutdown_task = object()
@@ -333,6 +355,9 @@ def notify_hass_started(event):
# If shutdown happened before Home Assistant finished starting
if result is shutdown_task:
+ # Make sure we cleanly close the run if
+ # we restart before startup finishes
+ self._shutdown()
return
# Start periodic purge
@@ -341,198 +366,244 @@ def notify_hass_started(event):
@callback
def async_purge(now):
"""Trigger the purge."""
- self.queue.put(PurgeTask(self.keep_days, repack=False))
+ self.queue.put(
+ PurgeTask(self.keep_days, repack=False, apply_filter=False)
+ )
# Purge every night at 4:12am
self.hass.helpers.event.track_time_change(
async_purge, hour=4, minute=12, second=0
)
- self.event_session = self.get_session()
- self.event_session.expire_on_commit = False
+ _LOGGER.debug("Recorder processing the queue")
# Use a session for the event read loop
# with a commit every time the event time
# has changed. This reduces the disk io.
while True:
event = self.queue.get()
+
if event is None:
- self._close_run()
- self._close_connection()
+ self._shutdown()
return
- if isinstance(event, PurgeTask):
- # Schedule a new purge task if this one didn't finish
- if not purge.purge_old_data(self, event.keep_days, event.repack):
- self.queue.put(PurgeTask(event.keep_days, event.repack))
- continue
- if isinstance(event, WaitTask):
- self._queue_watch.set()
- continue
- if event.event_type == EVENT_TIME_CHANGED:
- self._keepalive_count += 1
- if self._keepalive_count >= KEEPALIVE_TIME:
- self._keepalive_count = 0
- self._send_keep_alive()
- if self.commit_interval:
- self._timechanges_seen += 1
- if self._timechanges_seen >= self.commit_interval:
- self._timechanges_seen = 0
- self._commit_event_session_or_retry()
- continue
- if event.event_type in self.exclude_t:
- continue
-
- entity_id = event.data.get(ATTR_ENTITY_ID)
- if entity_id is not None:
- if not self.entity_filter(entity_id):
- continue
+ self._process_one_event(event)
+
+ def _setup_recorder(self) -> bool:
+ """Create schema and connect to the database."""
+ tries = 1
+
+ while tries <= self.db_max_retries:
try:
- if event.event_type == EVENT_STATE_CHANGED:
- dbevent = Events.from_event(event, event_data="{}")
- else:
- dbevent = Events.from_event(event)
- dbevent.created = event.time_fired
- self.event_session.add(dbevent)
+ self._setup_connection()
+ migration.migrate_schema(self)
+ self._setup_run()
+ except Exception as err: # pylint: disable=broad-except
+ _LOGGER.exception(
+ "Error during connection setup to %s: %s (retrying in %s seconds)",
+ self.db_url,
+ err,
+ self.db_retry_wait,
+ )
+ else:
+ _LOGGER.debug("Connected to recorder database")
+ self._open_event_session()
+ return True
+
+ tries += 1
+ time.sleep(self.db_retry_wait)
+
+ @callback
+ def connection_failed():
+ """Connect failed tasks."""
+ self.async_db_ready.set_result(False)
+ persistent_notification.async_create(
+ self.hass,
+ "The recorder could not start, please check the log",
+ "Recorder",
+ )
+
+ self.hass.add_job(connection_failed)
+ return False
+
+ def _process_one_event(self, event):
+ """Process one event."""
+ if isinstance(event, PurgeTask):
+ # Schedule a new purge task if this one didn't finish
+ if not purge.purge_old_data(
+ self, event.keep_days, event.repack, event.apply_filter
+ ):
+ self.queue.put(
+ PurgeTask(event.keep_days, event.repack, event.apply_filter)
+ )
+ return
+ if isinstance(event, WaitTask):
+ self._queue_watch.set()
+ return
+ if event.event_type == EVENT_TIME_CHANGED:
+ self._keepalive_count += 1
+ if self._keepalive_count >= KEEPALIVE_TIME:
+ self._keepalive_count = 0
+ self._send_keep_alive()
+ if self.commit_interval:
+ self._timechanges_seen += 1
+ if self._timechanges_seen >= self.commit_interval:
+ self._timechanges_seen = 0
+ self._commit_event_session_or_recover()
+ return
+
+ if not self.enabled:
+ return
+
+ try:
+ if event.event_type == EVENT_STATE_CHANGED:
+ dbevent = Events.from_event(event, event_data="{}")
+ else:
+ dbevent = Events.from_event(event)
+ dbevent.created = event.time_fired
+ self.event_session.add(dbevent)
+ except (TypeError, ValueError):
+ _LOGGER.warning("Event is not JSON serializable: %s", event)
+ return
+ except Exception as err: # pylint: disable=broad-except
+ # Must catch the exception to prevent the loop from collapsing
+ _LOGGER.exception("Error adding event: %s", err)
+ return
+
+ if event.event_type == EVENT_STATE_CHANGED:
+ try:
+ dbstate = States.from_event(event)
+ has_new_state = event.data.get("new_state")
+ if dbstate.entity_id in self._old_states:
+ old_state = self._old_states.pop(dbstate.entity_id)
+ if old_state.state_id:
+ dbstate.old_state_id = old_state.state_id
+ else:
+ dbstate.old_state = old_state
+ if not has_new_state:
+ dbstate.state = None
+ dbstate.event = dbevent
+ dbstate.created = event.time_fired
+ self.event_session.add(dbstate)
+ if has_new_state:
+ self._old_states[dbstate.entity_id] = dbstate
+ self._pending_expunge.append(dbstate)
except (TypeError, ValueError):
- _LOGGER.warning("Event is not JSON serializable: %s", event)
+ _LOGGER.warning(
+ "State is not JSON serializable: %s",
+ event.data.get("new_state"),
+ )
except Exception as err: # pylint: disable=broad-except
# Must catch the exception to prevent the loop from collapsing
- _LOGGER.exception("Error adding event: %s", err)
-
- if dbevent and event.event_type == EVENT_STATE_CHANGED:
- try:
- dbstate = States.from_event(event)
- has_new_state = event.data.get("new_state")
- if dbstate.entity_id in self._old_states:
- old_state = self._old_states.pop(dbstate.entity_id)
- if old_state.state_id:
- dbstate.old_state_id = old_state.state_id
- else:
- dbstate.old_state = old_state
- if not has_new_state:
- dbstate.state = None
- dbstate.event = dbevent
- dbstate.created = event.time_fired
- self.event_session.add(dbstate)
- if has_new_state:
- self._old_states[dbstate.entity_id] = dbstate
- self._pending_expunge.append(dbstate)
- except (TypeError, ValueError):
- _LOGGER.warning(
- "State is not JSON serializable: %s",
- event.data.get("new_state"),
- )
- except Exception as err: # pylint: disable=broad-except
- # Must catch the exception to prevent the loop from collapsing
- _LOGGER.exception("Error adding state change: %s", err)
-
- # If they do not have a commit interval
- # than we commit right away
- if not self.commit_interval:
- self._commit_event_session_or_retry()
+ _LOGGER.exception("Error adding state change: %s", err)
- def _send_keep_alive(self):
+ # If they do not have a commit interval
+ # than we commit right away
+ if not self.commit_interval:
+ self._commit_event_session_or_recover()
+
+ def _commit_event_session_or_recover(self):
+ """Commit changes to the database and recover if the database fails when possible."""
try:
- _LOGGER.debug("Sending keepalive")
- self.event_session.connection().scalar(select([1]))
+ self._commit_event_session_or_retry()
return
+ except exc.DatabaseError as err:
+ if isinstance(err.__cause__, sqlite3.DatabaseError):
+ _LOGGER.exception(
+ "Unrecoverable sqlite3 database corruption detected: %s", err
+ )
+ self._handle_sqlite_corruption()
+ return
+ _LOGGER.exception("Unexpected error saving events: %s", err)
except Exception as err: # pylint: disable=broad-except
# Must catch the exception to prevent the loop from collapsing
- _LOGGER.error(
- "Error in database connectivity during keepalive: %s",
- err,
- )
- self._reopen_event_session()
+ _LOGGER.exception("Unexpected error saving events: %s", err)
+
+ self._reopen_event_session()
+ return
def _commit_event_session_or_retry(self):
tries = 1
while tries <= self.db_max_retries:
- if tries != 1:
- time.sleep(self.db_retry_wait)
-
try:
self._commit_event_session()
return
except (exc.InternalError, exc.OperationalError) as err:
if err.connection_invalidated:
- _LOGGER.error(
- "Database connection invalidated: %s. "
- "(retrying in %s seconds)",
- err,
- self.db_retry_wait,
- )
+ message = "Database connection invalidated"
else:
- _LOGGER.error(
- "Error in database connectivity during commit: %s. "
- "(retrying in %s seconds)",
- err,
- self.db_retry_wait,
- )
+ message = "Error in database connectivity during commit"
+ _LOGGER.error(
+ "%s: Error executing query: %s. (retrying in %s seconds)",
+ message,
+ err,
+ self.db_retry_wait,
+ )
+ if tries == self.db_max_retries:
+ raise
+
tries += 1
+ time.sleep(self.db_retry_wait)
- except Exception as err: # pylint: disable=broad-except
- # Must catch the exception to prevent the loop from collapsing
- _LOGGER.exception("Error saving events: %s", err)
- return
+ def _commit_event_session(self):
+ self._commits_without_expire += 1
- _LOGGER.error(
- "Error in database update. Could not save " "after %d tries. Giving up",
- tries,
- )
- self._reopen_event_session()
+ if self._pending_expunge:
+ self.event_session.flush()
+ for dbstate in self._pending_expunge:
+ # Expunge the state so its not expired
+ # until we use it later for dbstate.old_state
+ if dbstate in self.event_session:
+ self.event_session.expunge(dbstate)
+ self._pending_expunge = []
+ self.event_session.commit()
+
+ # Expire is an expensive operation (frequently more expensive
+ # than the flush and commit itself) so we only
+ # do it after EXPIRE_AFTER_COMMITS commits
+ if self._commits_without_expire == EXPIRE_AFTER_COMMITS:
+ self._commits_without_expire = 0
+ self.event_session.expire_all()
+
+ def _handle_sqlite_corruption(self):
+ """Handle the sqlite3 database being corrupt."""
+ self._close_connection()
+ move_away_broken_database(dburl_to_path(self.db_url))
+ self._setup_recorder()
def _reopen_event_session(self):
- try:
- self.event_session.rollback()
- except Exception as err: # pylint: disable=broad-except
- # Must catch the exception to prevent the loop from collapsing
- _LOGGER.exception("Error while rolling back event session: %s", err)
+ """Rollback the event session and reopen it after a failure."""
+ self._old_states = {}
try:
+ self.event_session.rollback()
self.event_session.close()
except Exception as err: # pylint: disable=broad-except
# Must catch the exception to prevent the loop from collapsing
- _LOGGER.exception("Error while closing event session: %s", err)
+ _LOGGER.exception(
+ "Error while rolling back and closing the event session: %s", err
+ )
+ self._open_event_session()
+
+ def _open_event_session(self):
+ """Open the event session."""
try:
self.event_session = self.get_session()
self.event_session.expire_on_commit = False
except Exception as err: # pylint: disable=broad-except
- # Must catch the exception to prevent the loop from collapsing
_LOGGER.exception("Error while creating new event session: %s", err)
- def _commit_event_session(self):
- self._commits_without_expire += 1
-
+ def _send_keep_alive(self):
try:
- if self._pending_expunge:
- self.event_session.flush()
- for dbstate in self._pending_expunge:
- # Expunge the state so its not expired
- # until we use it later for dbstate.old_state
- if dbstate in self.event_session:
- self.event_session.expunge(dbstate)
- self._pending_expunge = []
- self.event_session.commit()
- except exc.IntegrityError as err:
+ _LOGGER.debug("Sending keepalive")
+ self.event_session.connection().scalar(select([1]))
+ return
+ except Exception as err: # pylint: disable=broad-except
_LOGGER.error(
- "Integrity error executing query (database likely deleted out from under us): %s",
+ "Error in database connectivity during keepalive: %s",
err,
)
- self.event_session.rollback()
- self._old_states = {}
- raise
- except Exception as err:
- _LOGGER.error("Error executing query: %s", err)
- self.event_session.rollback()
- raise
-
- # Expire is an expensive operation (frequently more expensive
- # than the flush and commit itself) so we only
- # do it after EXPIRE_AFTER_COMMITS commits
- if self._commits_without_expire == EXPIRE_AFTER_COMMITS:
- self._commits_without_expire = 0
- self.event_session.expire_all()
+ self._reopen_event_session()
@callback
def event_listener(self, event):
@@ -558,6 +629,7 @@ def block_till_done(self):
def _setup_connection(self):
"""Ensure database is ready to fly."""
kwargs = {}
+ self._completed_database_setup = False
def setup_recorder_connection(dbapi_connection, connection_record):
"""Dbapi specific connection settings."""
@@ -590,9 +662,7 @@ def setup_recorder_connection(dbapi_connection, connection_record):
else:
kwargs["echo"] = False
- if self.db_url != SQLITE_URL_PREFIX and self.db_url.startswith(
- SQLITE_URL_PREFIX
- ):
+ if self._using_file_sqlite:
with self.hass.timeout.freeze(DOMAIN):
#
# Here we run an sqlite3 quick_check. In the majority
@@ -615,6 +685,13 @@ def setup_recorder_connection(dbapi_connection, connection_record):
Base.metadata.create_all(self.engine)
self.get_session = scoped_session(sessionmaker(bind=self.engine))
+ @property
+ def _using_file_sqlite(self):
+ """Short version to check if we are using sqlite3 as a file."""
+ return self.db_url != SQLITE_URL_PREFIX and self.db_url.startswith(
+ SQLITE_URL_PREFIX
+ )
+
def _close_connection(self):
"""Close the connection."""
self.engine.dispose()
@@ -639,12 +716,18 @@ def _setup_run(self):
session.flush()
session.expunge(self.run_info)
- def _close_run(self):
+ def _shutdown(self):
"""Save end time for current run."""
if self.event_session is not None:
self.run_info.end = dt_util.utcnow()
self.event_session.add(self.run_info)
- self._commit_event_session_or_retry()
- self.event_session.close()
+ try:
+ self._commit_event_session_or_retry()
+ self.event_session.close()
+ except Exception as err: # pylint: disable=broad-except
+ _LOGGER.exception(
+ "Error saving the event session during shutdown: %s", err
+ )
self.run_info = None
+ self._close_connection()
diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py
index a2b5ffc6f2a786..026628a32dfc0b 100644
--- a/homeassistant/components/recorder/const.py
+++ b/homeassistant/components/recorder/const.py
@@ -5,3 +5,6 @@
DOMAIN = "recorder"
CONF_DB_INTEGRITY_CHECK = "db_integrity_check"
+
+# The maximum number of rows (events) we purge in one delete statement
+MAX_ROWS_TO_PURGE = 1000
diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json
index 67d3bdd0f5bf8c..a7e5eb0814d797 100644
--- a/homeassistant/components/recorder/manifest.json
+++ b/homeassistant/components/recorder/manifest.json
@@ -2,7 +2,7 @@
"domain": "recorder",
"name": "Recorder",
"documentation": "https://www.home-assistant.io/integrations/recorder",
- "requirements": ["sqlalchemy==1.3.22"],
+ "requirements": ["sqlalchemy==1.3.23"],
"codeowners": [],
"quality_scale": "internal"
}
diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py
index 4501b25385e0c0..fa93f6155617c5 100644
--- a/homeassistant/components/recorder/migration.py
+++ b/homeassistant/components/recorder/migration.py
@@ -3,7 +3,12 @@
from sqlalchemy import ForeignKeyConstraint, MetaData, Table, text
from sqlalchemy.engine import reflection
-from sqlalchemy.exc import InternalError, OperationalError, SQLAlchemyError
+from sqlalchemy.exc import (
+ InternalError,
+ OperationalError,
+ ProgrammingError,
+ SQLAlchemyError,
+)
from sqlalchemy.schema import AddConstraint, DropConstraint
from .const import DOMAIN
@@ -69,7 +74,7 @@ def _create_index(engine, table_name, index_name):
)
try:
index.create(engine)
- except OperationalError as err:
+ except (InternalError, ProgrammingError, OperationalError) as err:
lower_err_str = str(err).lower()
if "already exists" not in lower_err_str and "duplicate" not in lower_err_str:
@@ -78,13 +83,6 @@ def _create_index(engine, table_name, index_name):
_LOGGER.warning(
"Index %s already exists on %s, continuing", index_name, table_name
)
- except InternalError as err:
- if "duplicate" not in str(err).lower():
- raise
-
- _LOGGER.warning(
- "Index %s already exists on %s, continuing", index_name, table_name
- )
_LOGGER.debug("Finished creating %s", index_name)
@@ -206,6 +204,65 @@ def _add_columns(engine, table_name, columns_def):
)
+def _modify_columns(engine, table_name, columns_def):
+ """Modify columns in a table."""
+ if engine.dialect.name == "sqlite":
+ _LOGGER.debug(
+ "Skipping to modify columns %s in table %s; "
+ "Modifying column length in SQLite is unnecessary, "
+ "it does not impose any length restrictions",
+ ", ".join(column.split(" ")[0] for column in columns_def),
+ table_name,
+ )
+ return
+
+ _LOGGER.warning(
+ "Modifying columns %s in table %s. Note: this can take several "
+ "minutes on large databases and slow computers. Please "
+ "be patient!",
+ ", ".join(column.split(" ")[0] for column in columns_def),
+ table_name,
+ )
+
+ if engine.dialect.name == "postgresql":
+ columns_def = [
+ "ALTER {column} TYPE {type}".format(
+ **dict(zip(["column", "type"], col_def.split(" ", 1)))
+ )
+ for col_def in columns_def
+ ]
+ elif engine.dialect.name == "mssql":
+ columns_def = [f"ALTER COLUMN {col_def}" for col_def in columns_def]
+ else:
+ columns_def = [f"MODIFY {col_def}" for col_def in columns_def]
+
+ try:
+ engine.execute(
+ text(
+ "ALTER TABLE {table} {columns_def}".format(
+ table=table_name, columns_def=", ".join(columns_def)
+ )
+ )
+ )
+ return
+ except (InternalError, OperationalError):
+ _LOGGER.info("Unable to use quick column modify. Modifying 1 by 1")
+
+ for column_def in columns_def:
+ try:
+ engine.execute(
+ text(
+ "ALTER TABLE {table} {column_def}".format(
+ table=table_name, column_def=column_def
+ )
+ )
+ )
+ except (InternalError, OperationalError):
+ _LOGGER.exception(
+ "Could not modify column %s in table %s", column_def, table_name
+ )
+
+
def _update_states_table_with_foreign_key_options(engine):
"""Add the options to foreign key constraints."""
inspector = reflection.Inspector.from_engine(engine)
@@ -323,6 +380,26 @@ def _apply_update(engine, new_version, old_version):
elif new_version == 11:
_create_index(engine, "states", "ix_states_old_state_id")
_update_states_table_with_foreign_key_options(engine)
+ elif new_version == 12:
+ if engine.dialect.name == "mysql":
+ _modify_columns(engine, "events", ["event_data LONGTEXT"])
+ _modify_columns(engine, "states", ["attributes LONGTEXT"])
+ elif new_version == 13:
+ if engine.dialect.name == "mysql":
+ _modify_columns(
+ engine, "events", ["time_fired DATETIME(6)", "created DATETIME(6)"]
+ )
+ _modify_columns(
+ engine,
+ "states",
+ [
+ "last_changed DATETIME(6)",
+ "last_updated DATETIME(6)",
+ "created DATETIME(6)",
+ ],
+ )
+ elif new_version == 14:
+ _modify_columns(engine, "events", ["event_type VARCHAR(64)"])
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 9481e954bde8ff..3459da309eebf7 100644
--- a/homeassistant/components/recorder/models.py
+++ b/homeassistant/components/recorder/models.py
@@ -13,10 +13,12 @@
Text,
distinct,
)
+from sqlalchemy.dialects import mysql
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm.session import Session
+from homeassistant.const import MAX_LENGTH_EVENT_TYPE
from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id
from homeassistant.helpers.json import JSONEncoder
import homeassistant.util.dt as dt_util
@@ -25,7 +27,7 @@
# pylint: disable=invalid-name
Base = declarative_base()
-SCHEMA_VERSION = 11
+SCHEMA_VERSION = 14
_LOGGER = logging.getLogger(__name__)
@@ -38,6 +40,10 @@
ALL_TABLES = [TABLE_STATES, TABLE_EVENTS, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES]
+DATETIME_TYPE = DateTime(timezone=True).with_variant(
+ mysql.DATETIME(timezone=True, fsp=6), "mysql"
+)
+
class Events(Base): # type: ignore
"""Event history data."""
@@ -48,11 +54,11 @@ class Events(Base): # type: ignore
}
__tablename__ = TABLE_EVENTS
event_id = Column(Integer, primary_key=True)
- event_type = Column(String(32))
- event_data = Column(Text)
+ event_type = Column(String(MAX_LENGTH_EVENT_TYPE))
+ event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql"))
origin = Column(String(32))
- time_fired = Column(DateTime(timezone=True), index=True)
- created = Column(DateTime(timezone=True), default=dt_util.utcnow)
+ time_fired = Column(DATETIME_TYPE, index=True)
+ created = Column(DATETIME_TYPE, default=dt_util.utcnow)
context_id = Column(String(36), index=True)
context_user_id = Column(String(36), index=True)
context_parent_id = Column(String(36), index=True)
@@ -63,6 +69,15 @@ class Events(Base): # type: ignore
Index("ix_events_event_type_time_fired", "event_type", "time_fired"),
)
+ def __repr__(self) -> str:
+ """Return string representation of instance for debugging."""
+ return (
+ f""
+ )
+
@staticmethod
def from_event(event, event_data=None):
"""Create an event database object from a native event."""
@@ -109,15 +124,15 @@ class States(Base): # type: ignore
domain = Column(String(64))
entity_id = Column(String(255))
state = Column(String(255))
- attributes = Column(Text)
+ attributes = Column(Text().with_variant(mysql.LONGTEXT, "mysql"))
event_id = Column(
Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True
)
- last_changed = Column(DateTime(timezone=True), default=dt_util.utcnow)
- last_updated = Column(DateTime(timezone=True), default=dt_util.utcnow, index=True)
- created = Column(DateTime(timezone=True), default=dt_util.utcnow)
+ last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow)
+ last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True)
+ created = Column(DATETIME_TYPE, default=dt_util.utcnow)
old_state_id = Column(
- Integer, ForeignKey("states.state_id", ondelete="SET NULL"), index=True
+ Integer, ForeignKey("states.state_id", ondelete="NO ACTION"), index=True
)
event = relationship("Events", uselist=False)
old_state = relationship("States", remote_side=[state_id])
@@ -128,6 +143,17 @@ class States(Base): # type: ignore
Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"),
)
+ def __repr__(self) -> str:
+ """Return string representation of instance for debugging."""
+ return (
+ f""
+ )
+
@staticmethod
def from_event(event):
"""Create object from a state_changed event."""
@@ -184,6 +210,19 @@ class RecorderRuns(Base): # type: ignore
__table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),)
+ def __repr__(self) -> str:
+ """Return string representation of instance for debugging."""
+ end = (
+ f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None
+ )
+ return (
+ f""
+ )
+
def entity_ids(self, point_in_time=None):
"""Return the entity ids that existed in this run.
@@ -218,6 +257,15 @@ class SchemaChanges(Base): # type: ignore
schema_version = Column(Integer)
changed = Column(DateTime(timezone=True), default=dt_util.utcnow)
+ def __repr__(self) -> str:
+ """Return string representation of instance for debugging."""
+ return (
+ f""
+ )
+
def process_timestamp(ts):
"""Process a timestamp into datetime object."""
diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py
index 43e84785f7d4f2..ef626a744c4fab 100644
--- a/homeassistant/components/recorder/purge.py
+++ b/homeassistant/components/recorder/purge.py
@@ -1,88 +1,59 @@
"""Purge old data helper."""
-from datetime import timedelta
+from __future__ import annotations
+
+from datetime import datetime, timedelta
import logging
import time
+from typing import TYPE_CHECKING
from sqlalchemy.exc import OperationalError, SQLAlchemyError
+from sqlalchemy.orm.session import Session
+from sqlalchemy.sql.expression import distinct
import homeassistant.util.dt as dt_util
+from .const import MAX_ROWS_TO_PURGE
from .models import Events, RecorderRuns, States
-from .util import execute, session_scope
+from .repack import repack_database
+from .util import session_scope
+
+if TYPE_CHECKING:
+ from . import Recorder
_LOGGER = logging.getLogger(__name__)
-def purge_old_data(instance, purge_days: int, repack: bool) -> bool:
+def purge_old_data(
+ instance: Recorder, purge_days: int, repack: bool, apply_filter: bool = False
+) -> bool:
"""Purge events and states older than purge_days ago.
Cleans up an timeframe of an hour, based on the oldest record.
"""
purge_before = dt_util.utcnow() - timedelta(days=purge_days)
- _LOGGER.debug("Purging states and events before target %s", purge_before)
-
+ _LOGGER.debug(
+ "Purging states and events before target %s",
+ purge_before.isoformat(sep=" ", timespec="seconds"),
+ )
try:
- with session_scope(session=instance.get_session()) as session:
- # Purge a max of 1 hour, based on the oldest states or events record
- batch_purge_before = purge_before
-
- query = session.query(States).order_by(States.last_updated.asc()).limit(1)
- states = execute(query, to_native=True, validate_entity_ids=False)
- if states:
- batch_purge_before = min(
- batch_purge_before,
- states[0].last_updated + timedelta(hours=1),
- )
-
- query = session.query(Events).order_by(Events.time_fired.asc()).limit(1)
- events = execute(query, to_native=True)
- if events:
- batch_purge_before = min(
- batch_purge_before,
- events[0].time_fired + timedelta(hours=1),
- )
-
- _LOGGER.debug("Purging states and events before %s", batch_purge_before)
-
- deleted_rows = (
- session.query(States)
- .filter(States.last_updated < batch_purge_before)
- .delete(synchronize_session=False)
- )
- _LOGGER.debug("Deleted %s states", deleted_rows)
-
- deleted_rows = (
- session.query(Events)
- .filter(Events.time_fired < batch_purge_before)
- .delete(synchronize_session=False)
- )
- _LOGGER.debug("Deleted %s events", deleted_rows)
-
- # If states or events purging isn't processing the purge_before yet,
- # return false, as we are not done yet.
- if batch_purge_before != purge_before:
+ with session_scope(session=instance.get_session()) as session: # type: ignore
+ # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record
+ event_ids = _select_event_ids_to_purge(session, purge_before)
+ state_ids = _select_state_ids_to_purge(session, purge_before, event_ids)
+ if state_ids:
+ _purge_state_ids(session, state_ids)
+ if event_ids:
+ _purge_event_ids(session, event_ids)
+ # If states or events purging isn't processing the purge_before yet,
+ # return false, as we are not done yet.
_LOGGER.debug("Purging hasn't fully completed yet")
return False
-
- # Recorder runs is small, no need to batch run it
- deleted_rows = (
- session.query(RecorderRuns)
- .filter(RecorderRuns.start < purge_before)
- .filter(RecorderRuns.run_id != instance.run_info.run_id)
- .delete(synchronize_session=False)
- )
- _LOGGER.debug("Deleted %s recorder_runs", deleted_rows)
-
+ if apply_filter and _purge_filtered_data(instance, session) is False:
+ _LOGGER.debug("Cleanup filtered data hasn't fully completed yet")
+ return False
+ _purge_old_recorder_runs(instance, session, purge_before)
if repack:
- # Execute sqlite or postgresql vacuum command to free up space on disk
- if instance.engine.driver in ("pysqlite", "postgresql"):
- _LOGGER.debug("Vacuuming SQL DB to free space")
- instance.engine.execute("VACUUM")
- # Optimize mysql / mariadb tables to free up space on disk
- elif instance.engine.driver in ("mysqldb", "pymysql"):
- _LOGGER.debug("Optimizing SQL DB to free space")
- instance.engine.execute("OPTIMIZE TABLE states, events, recorder_runs")
-
+ repack_database(instance)
except OperationalError as err:
# Retry when one of the following MySQL errors occurred:
# 1205: Lock wait timeout exceeded; try restarting transaction
@@ -101,3 +72,144 @@ def purge_old_data(instance, purge_days: int, repack: bool) -> bool:
except SQLAlchemyError as err:
_LOGGER.warning("Error purging history: %s", err)
return True
+
+
+def _select_event_ids_to_purge(session: Session, purge_before: datetime) -> list[int]:
+ """Return a list of event ids to purge."""
+ events = (
+ session.query(Events.event_id)
+ .filter(Events.time_fired < purge_before)
+ .limit(MAX_ROWS_TO_PURGE)
+ .all()
+ )
+ _LOGGER.debug("Selected %s event ids to remove", len(events))
+ return [event.event_id for event in events]
+
+
+def _select_state_ids_to_purge(
+ session: Session, purge_before: datetime, event_ids: list[int]
+) -> list[int]:
+ """Return a list of state ids to purge."""
+ if not event_ids:
+ return []
+ states = (
+ session.query(States.state_id)
+ .filter(States.last_updated < purge_before)
+ .filter(States.event_id.in_(event_ids))
+ .all()
+ )
+ _LOGGER.debug("Selected %s state ids to remove", len(states))
+ return [state.state_id for state in states]
+
+
+def _purge_state_ids(session: Session, state_ids: list[int]) -> None:
+ """Disconnect states and delete by state id."""
+
+ # Update old_state_id to NULL before deleting to ensure
+ # the delete does not fail due to a foreign key constraint
+ # since some databases (MSSQL) cannot do the ON DELETE SET NULL
+ # for us.
+ disconnected_rows = (
+ session.query(States)
+ .filter(States.old_state_id.in_(state_ids))
+ .update({"old_state_id": None}, synchronize_session=False)
+ )
+ _LOGGER.debug("Updated %s states to remove old_state_id", disconnected_rows)
+
+ deleted_rows = (
+ session.query(States)
+ .filter(States.state_id.in_(state_ids))
+ .delete(synchronize_session=False)
+ )
+ _LOGGER.debug("Deleted %s states", deleted_rows)
+
+
+def _purge_event_ids(session: Session, event_ids: list[int]) -> None:
+ """Delete by event id."""
+ deleted_rows = (
+ session.query(Events)
+ .filter(Events.event_id.in_(event_ids))
+ .delete(synchronize_session=False)
+ )
+ _LOGGER.debug("Deleted %s events", deleted_rows)
+
+
+def _purge_old_recorder_runs(
+ instance: Recorder, session: Session, purge_before: datetime
+) -> None:
+ """Purge all old recorder runs."""
+ # Recorder runs is small, no need to batch run it
+ deleted_rows = (
+ session.query(RecorderRuns)
+ .filter(RecorderRuns.start < purge_before)
+ .filter(RecorderRuns.run_id != instance.run_info.run_id)
+ .delete(synchronize_session=False)
+ )
+ _LOGGER.debug("Deleted %s recorder_runs", deleted_rows)
+
+
+def _purge_filtered_data(instance: Recorder, session: Session) -> bool:
+ """Remove filtered states and events that shouldn't be in the database."""
+ _LOGGER.debug("Cleanup filtered data")
+
+ # Check if excluded entity_ids are in database
+ excluded_entity_ids: list[str] = [
+ entity_id
+ for (entity_id,) in session.query(distinct(States.entity_id)).all()
+ if not instance.entity_filter(entity_id)
+ ]
+ if len(excluded_entity_ids) > 0:
+ _purge_filtered_states(session, excluded_entity_ids)
+ return False
+
+ # Check if excluded event_types are in database
+ excluded_event_types: list[str] = [
+ event_type
+ for (event_type,) in session.query(distinct(Events.event_type)).all()
+ if event_type in instance.exclude_t
+ ]
+ if len(excluded_event_types) > 0:
+ _purge_filtered_events(session, excluded_event_types)
+ return False
+
+ return True
+
+
+def _purge_filtered_states(session: Session, excluded_entity_ids: list[str]) -> None:
+ """Remove filtered states and linked events."""
+ state_ids: list[int]
+ event_ids: list[int | None]
+ state_ids, event_ids = zip(
+ *(
+ session.query(States.state_id, States.event_id)
+ .filter(States.entity_id.in_(excluded_entity_ids))
+ .limit(MAX_ROWS_TO_PURGE)
+ .all()
+ )
+ )
+ event_ids = [id_ for id_ in event_ids if id_ is not None]
+ _LOGGER.debug(
+ "Selected %s state_ids to remove that should be filtered", len(state_ids)
+ )
+ _purge_state_ids(session, state_ids)
+ _purge_event_ids(session, event_ids) # type: ignore # type of event_ids already narrowed to 'list[int]'
+
+
+def _purge_filtered_events(session: Session, excluded_event_types: list[str]) -> None:
+ """Remove filtered events and linked states."""
+ events: list[Events] = (
+ session.query(Events.event_id)
+ .filter(Events.event_type.in_(excluded_event_types))
+ .limit(MAX_ROWS_TO_PURGE)
+ .all()
+ )
+ event_ids: list[int] = [event.event_id for event in events]
+ _LOGGER.debug(
+ "Selected %s event_ids to remove that should be filtered", len(event_ids)
+ )
+ states: list[States] = (
+ session.query(States.state_id).filter(States.event_id.in_(event_ids)).all()
+ )
+ state_ids: list[int] = [state.state_id for state in states]
+ _purge_state_ids(session, state_ids)
+ _purge_event_ids(session, event_ids)
diff --git a/homeassistant/components/recorder/repack.py b/homeassistant/components/recorder/repack.py
new file mode 100644
index 00000000000000..68d7d5954c92b6
--- /dev/null
+++ b/homeassistant/components/recorder/repack.py
@@ -0,0 +1,35 @@
+"""Purge repack helper."""
+from __future__ import annotations
+
+import logging
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from . import Recorder
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def repack_database(instance: Recorder) -> None:
+ """Repack based on engine type."""
+
+ # Execute sqlite command to free up space on disk
+ if instance.engine.dialect.name == "sqlite":
+ _LOGGER.debug("Vacuuming SQL DB to free space")
+ instance.engine.execute("VACUUM")
+ return
+
+ # Execute postgresql vacuum command to free up space on disk
+ if instance.engine.dialect.name == "postgresql":
+ _LOGGER.debug("Vacuuming SQL DB to free space")
+ with instance.engine.connect().execution_options(
+ isolation_level="AUTOCOMMIT"
+ ) as conn:
+ conn.execute("VACUUM")
+ return
+
+ # Optimize mysql / mariadb tables to free up space on disk
+ if instance.engine.dialect.name == "mysql":
+ _LOGGER.debug("Optimizing SQL DB to free space")
+ instance.engine.execute("OPTIMIZE TABLE states, events, recorder_runs")
+ return
diff --git a/homeassistant/components/recorder/services.yaml b/homeassistant/components/recorder/services.yaml
index 512807c9f69427..2c4f35b5e7a29f 100644
--- a/homeassistant/components/recorder/services.yaml
+++ b/homeassistant/components/recorder/services.yaml
@@ -1,11 +1,40 @@
# Describes the format for available recorder services
purge:
- description: Start purge task - delete events and states older than x days, according to keep_days service data.
+ name: Purge
+ description: Start purge task - to clean up old data from your database.
fields:
keep_days:
- description: Number of history days to keep in database after purge. Value >= 0.
+ name: Days to keep
+ description: Number of history days to keep in database after purge.
example: 2
+ selector:
+ number:
+ min: 0
+ max: 365
+ step: 1
+ unit_of_measurement: days
+ mode: slider
+
repack:
- description: Attempt to save disk space by rewriting the entire database file.
+ name: Repack
+ description:
+ Attempt to save disk space by rewriting the entire database file.
+ example: true
+ default: false
+ selector:
+ boolean:
+
+ apply_filter:
+ name: Apply filter
+ description: Apply entity_id and event_type filter in addition to time based purge.
example: true
+ default: false
+ selector:
+ boolean:
+
+disable:
+ description: Stop the recording of events and state changes
+
+enabled:
+ description: Start the recording of events and state changes
diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py
index abf14268687357..c17fb33d365ed2 100644
--- a/homeassistant/components/recorder/util.py
+++ b/homeassistant/components/recorder/util.py
@@ -1,4 +1,7 @@
"""SQLAlchemy util functions."""
+from __future__ import annotations
+
+from collections.abc import Generator
from contextlib import contextmanager
from datetime import timedelta
import logging
@@ -6,7 +9,9 @@
import time
from sqlalchemy.exc import OperationalError, SQLAlchemyError
+from sqlalchemy.orm.session import Session
+from homeassistant.helpers.typing import HomeAssistantType
import homeassistant.util.dt as dt_util
from .const import CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, SQLITE_URL_PREFIX
@@ -25,7 +30,9 @@
@contextmanager
-def session_scope(*, hass=None, session=None):
+def session_scope(
+ *, hass: HomeAssistantType | None = None, session: Session | None = None
+) -> Generator[Session, None, None]:
"""Provide a transactional scope around a series of operations."""
if session is None and hass is not None:
session = hass.data[DATA_INSTANCE].get_session()
@@ -112,19 +119,24 @@ def execute(qry, to_native=False, validate_entity_ids=True):
def validate_or_move_away_sqlite_database(dburl: str, db_integrity_check: bool) -> bool:
"""Ensure that the database is valid or move it away."""
- dbpath = dburl[len(SQLITE_URL_PREFIX) :]
+ dbpath = dburl_to_path(dburl)
if not os.path.exists(dbpath):
# Database does not exist yet, this is OK
return True
if not validate_sqlite_database(dbpath, db_integrity_check):
- _move_away_broken_database(dbpath)
+ move_away_broken_database(dbpath)
return False
return True
+def dburl_to_path(dburl):
+ """Convert the db url into a filesystem path."""
+ return dburl[len(SQLITE_URL_PREFIX) :]
+
+
def last_run_was_recently_clean(cursor):
"""Verify the last recorder run was recently clean."""
@@ -163,7 +175,7 @@ def validate_sqlite_database(dbpath: str, db_integrity_check: bool) -> bool:
run_checks_on_open_db(dbpath, conn.cursor(), db_integrity_check)
conn.close()
except sqlite3.DatabaseError:
- _LOGGER.exception("The database at %s is corrupt or malformed.", dbpath)
+ _LOGGER.exception("The database at %s is corrupt or malformed", dbpath)
return False
return True
@@ -171,7 +183,10 @@ def validate_sqlite_database(dbpath: str, db_integrity_check: bool) -> bool:
def run_checks_on_open_db(dbpath, cursor, db_integrity_check):
"""Run checks that will generate a sqlite3 exception if there is corruption."""
- if basic_sanity_check(cursor) and last_run_was_recently_clean(cursor):
+ sanity_check_passed = basic_sanity_check(cursor)
+ last_run_was_clean = last_run_was_recently_clean(cursor)
+
+ if sanity_check_passed and last_run_was_clean:
_LOGGER.debug(
"The quick_check will be skipped as the system was restarted cleanly and passed the basic sanity check"
)
@@ -187,13 +202,25 @@ def run_checks_on_open_db(dbpath, cursor, db_integrity_check):
)
return
- _LOGGER.debug(
+ if not sanity_check_passed:
+ _LOGGER.warning(
+ "The database sanity check failed to validate the sqlite3 database at %s",
+ dbpath,
+ )
+
+ if not last_run_was_clean:
+ _LOGGER.warning(
+ "The system could not validate that the sqlite3 database at %s was shutdown cleanly",
+ dbpath,
+ )
+
+ _LOGGER.info(
"A quick_check is being performed on the sqlite3 database at %s", dbpath
)
cursor.execute("PRAGMA QUICK_CHECK")
-def _move_away_broken_database(dbfile: str) -> None:
+def move_away_broken_database(dbfile: str) -> None:
"""Move away a broken sqlite3 database."""
isotime = dt_util.utcnow().isoformat()
diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json
index fc3356b310c047..252052ac5c2211 100644
--- a/homeassistant/components/reddit/manifest.json
+++ b/homeassistant/components/reddit/manifest.json
@@ -2,6 +2,6 @@
"domain": "reddit",
"name": "Reddit",
"documentation": "https://www.home-assistant.io/integrations/reddit",
- "requirements": ["praw==7.1.0"],
+ "requirements": ["praw==7.1.4"],
"codeowners": []
}
diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py
index 0fe4e87f863429..a88de916009e0b 100644
--- a/homeassistant/components/reddit/sensor.py
+++ b/homeassistant/components/reddit/sensor.py
@@ -5,8 +5,9 @@
import praw
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
+ ATTR_ID,
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_MAXIMUM,
@@ -14,14 +15,12 @@
CONF_USERNAME,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
CONF_SORT_BY = "sort_by"
CONF_SUBREDDITS = "subreddits"
-ATTR_ID = "id"
ATTR_BODY = "body"
ATTR_COMMENTS_NUMBER = "comms_num"
ATTR_CREATED = "created"
@@ -82,7 +81,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class RedditSensor(Entity):
+class RedditSensor(SensorEntity):
"""Representation of a Reddit sensor."""
def __init__(self, reddit, subreddit: str, limit: int, sort_by: str):
@@ -105,7 +104,7 @@ def state(self):
return len(self._subreddit_data)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_SUBREDDIT: self._subreddit,
diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py
index 30d57a3d9dcaa2..78b713c286ca20 100644
--- a/homeassistant/components/rejseplanen/sensor.py
+++ b/homeassistant/components/rejseplanen/sensor.py
@@ -4,6 +4,7 @@
For more info on the API see:
https://help.rejseplanen.dk/hc/en-us/articles/214174465-Rejseplanen-s-API
"""
+from contextlib import suppress
from datetime import datetime, timedelta
import logging
from operator import itemgetter
@@ -11,10 +12,9 @@
import rjpl
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, TIME_MINUTES
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -87,7 +87,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
)
-class RejseplanenTransportSensor(Entity):
+class RejseplanenTransportSensor(SensorEntity):
"""Implementation of Rejseplanen transport sensor."""
def __init__(self, data, stop_id, route, direction, name):
@@ -110,7 +110,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if not self._times:
return {ATTR_STOP_ID: self._stop_id, ATTR_ATTRIBUTION: ATTRIBUTION}
@@ -148,10 +148,8 @@ def update(self):
if not self._times:
self._state = None
else:
- try:
+ with suppress(TypeError):
self._state = self._times[0][ATTR_DUE_IN]
- except TypeError:
- pass
class PublicTransportData:
diff --git a/homeassistant/components/remember_the_milk/manifest.json b/homeassistant/components/remember_the_milk/manifest.json
index f03f88023ae335..8ce8cb98e5bfaa 100644
--- a/homeassistant/components/remember_the_milk/manifest.json
+++ b/homeassistant/components/remember_the_milk/manifest.json
@@ -2,7 +2,7 @@
"domain": "remember_the_milk",
"name": "Remember The Milk",
"documentation": "https://www.home-assistant.io/integrations/remember_the_milk",
- "requirements": ["RtmAPI==0.7.2", "httplib2==0.18.1"],
+ "requirements": ["RtmAPI==0.7.2", "httplib2==0.19.0"],
"dependencies": ["configurator"],
"codeowners": []
}
diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py
index 44a318988b2e24..ecde6f67b67c55 100644
--- a/homeassistant/components/remote/__init__.py
+++ b/homeassistant/components/remote/__init__.py
@@ -1,13 +1,16 @@
"""Support to interface with universal remote control devices."""
+from __future__ import annotations
+
from datetime import timedelta
import functools as ft
import logging
-from typing import Any, Iterable, cast
+from typing import Any, Iterable, cast, final
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
+ ATTR_COMMAND,
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
@@ -29,7 +32,8 @@
_LOGGER = logging.getLogger(__name__)
ATTR_ACTIVITY = "activity"
-ATTR_COMMAND = "command"
+ATTR_ACTIVITY_LIST = "activity_list"
+ATTR_CURRENT_ACTIVITY = "current_activity"
ATTR_COMMAND_TYPE = "command_type"
ATTR_DEVICE = "device"
ATTR_NUM_REPEATS = "num_repeats"
@@ -56,6 +60,7 @@
SUPPORT_LEARN_COMMAND = 1
SUPPORT_DELETE_COMMAND = 2
+SUPPORT_ACTIVITY = 4
REMOTE_SERVICE_ACTIVITY_SCHEMA = make_entity_service_schema(
{vol.Optional(ATTR_ACTIVITY): cv.string}
@@ -136,20 +141,41 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo
class RemoteEntity(ToggleEntity):
- """Representation of a remote."""
+ """Base class for remote entities."""
@property
def supported_features(self) -> int:
"""Flag supported features."""
return 0
+ @property
+ def current_activity(self) -> str | None:
+ """Active activity."""
+ return None
+
+ @property
+ def activity_list(self) -> list[str] | None:
+ """List of available activities."""
+ return None
+
+ @final
+ @property
+ def state_attributes(self) -> dict[str, Any] | None:
+ """Return optional state attributes."""
+ if not self.supported_features & SUPPORT_ACTIVITY:
+ return None
+
+ return {
+ ATTR_ACTIVITY_LIST: self.activity_list,
+ ATTR_CURRENT_ACTIVITY: self.current_activity,
+ }
+
def send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send commands to a device."""
raise NotImplementedError()
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send commands to a device."""
- assert self.hass is not None
await self.hass.async_add_executor_job(
ft.partial(self.send_command, command, **kwargs)
)
@@ -160,7 +186,6 @@ def learn_command(self, **kwargs: Any) -> None:
async def async_learn_command(self, **kwargs: Any) -> None:
"""Learn a command from a device."""
- assert self.hass is not None
await self.hass.async_add_executor_job(ft.partial(self.learn_command, **kwargs))
def delete_command(self, **kwargs: Any) -> None:
@@ -169,7 +194,6 @@ def delete_command(self, **kwargs: Any) -> None:
async def async_delete_command(self, **kwargs: Any) -> None:
"""Delete commands from the database."""
- assert self.hass is not None
await self.hass.async_add_executor_job(
ft.partial(self.delete_command, **kwargs)
)
diff --git a/homeassistant/components/remote/device_action.py b/homeassistant/components/remote/device_action.py
index aa819f3eb46812..aa34eb33224c20 100644
--- a/homeassistant/components/remote/device_action.py
+++ b/homeassistant/components/remote/device_action.py
@@ -1,5 +1,5 @@
"""Provides device actions for remotes."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -25,6 +25,6 @@ async def async_call_action_from_config(
)
-async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device actions."""
return await toggle_entity.async_get_actions(hass, device_id, DOMAIN)
diff --git a/homeassistant/components/remote/device_condition.py b/homeassistant/components/remote/device_condition.py
index 06c7bec89d48fe..ed200fd5579c03 100644
--- a/homeassistant/components/remote/device_condition.py
+++ b/homeassistant/components/remote/device_condition.py
@@ -1,5 +1,5 @@
"""Provides device conditions for remotes."""
-from typing import Dict, List
+from __future__ import annotations
import voluptuous as vol
@@ -28,7 +28,7 @@ def async_condition_from_config(
async def async_get_conditions(
hass: HomeAssistant, device_id: str
-) -> List[Dict[str, str]]:
+) -> list[dict[str, str]]:
"""List device conditions."""
return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN)
diff --git a/homeassistant/components/remote/device_trigger.py b/homeassistant/components/remote/device_trigger.py
index 5919e8c61ba43c..d8437604f6dc0d 100644
--- a/homeassistant/components/remote/device_trigger.py
+++ b/homeassistant/components/remote/device_trigger.py
@@ -1,5 +1,5 @@
"""Provides device triggers for remotes."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -28,7 +28,7 @@ async def async_attach_trigger(
)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers."""
return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN)
diff --git a/homeassistant/components/remote/reproduce_state.py b/homeassistant/components/remote/reproduce_state.py
index 4e1f426c57b0ee..b42a0bdc611a71 100644
--- a/homeassistant/components/remote/reproduce_state.py
+++ b/homeassistant/components/remote/reproduce_state.py
@@ -1,7 +1,9 @@
"""Reproduce an Remote state."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -24,8 +26,8 @@ async def _async_reproduce_state(
hass: HomeAssistantType,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -60,8 +62,8 @@ async def async_reproduce_states(
hass: HomeAssistantType,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Remote states."""
await asyncio.gather(
diff --git a/homeassistant/components/remote/translations/hu.json b/homeassistant/components/remote/translations/hu.json
index fa0bf3fee904ad..39ce5f17a12ef9 100644
--- a/homeassistant/components/remote/translations/hu.json
+++ b/homeassistant/components/remote/translations/hu.json
@@ -1,4 +1,19 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "{entity_name} be/kikapcsol\u00e1sa",
+ "turn_off": "{entity_name} kikapcsol\u00e1sa",
+ "turn_on": "{entity_name} bekapcsol\u00e1sa"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} ki van kapcsolva",
+ "is_on": "{entity_name} be van kapcsolva"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} ki lett kapcsolva",
+ "turned_on": "{entity_name} be lett kapcsolva"
+ }
+ },
"state": {
"_": {
"off": "Ki",
diff --git a/homeassistant/components/remote/translations/id.json b/homeassistant/components/remote/translations/id.json
index e824cafff4eceb..09552be40d49d6 100644
--- a/homeassistant/components/remote/translations/id.json
+++ b/homeassistant/components/remote/translations/id.json
@@ -1,8 +1,23 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Nyala/matikan {entity_name}",
+ "turn_off": "Matikan {entity_name}",
+ "turn_on": "Nyalakan {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} mati",
+ "is_on": "{entity_name} nyala"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} dimatikan",
+ "turned_on": "{entity_name} dinyalakan"
+ }
+ },
"state": {
"_": {
- "off": "Off",
- "on": "On"
+ "off": "Mati",
+ "on": "Nyala"
}
},
"title": "Daring"
diff --git a/homeassistant/components/remote/translations/ko.json b/homeassistant/components/remote/translations/ko.json
index bd055e21f5bb73..a89c4b36f35d7d 100644
--- a/homeassistant/components/remote/translations/ko.json
+++ b/homeassistant/components/remote/translations/ko.json
@@ -1,17 +1,17 @@
{
"device_automation": {
"action_type": {
- "toggle": "{entity_name} \ud1a0\uae00",
- "turn_off": "{entity_name} \ub044\uae30",
- "turn_on": "{entity_name} \ucf1c\uae30"
+ "toggle": "{entity_name}\uc744(\ub97c) \ud1a0\uae00\ud558\uae30",
+ "turn_off": "{entity_name}\uc744(\ub97c) \ub044\uae30",
+ "turn_on": "{entity_name}\uc744(\ub97c) \ucf1c\uae30"
},
"condition_type": {
- "is_off": "{entity_name} \uc774 \uaebc\uc838 \uc788\uc73c\uba74",
- "is_on": "{entity_name} \uc774 \ucf1c\uc838 \uc788\uc73c\uba74"
+ "is_off": "{entity_name}\uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74",
+ "is_on": "{entity_name}\uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74"
},
"trigger_type": {
- "turned_off": "{entity_name} \uaebc\uc9d0",
- "turned_on": "{entity_name} \ucf1c\uc9d0"
+ "turned_off": "{entity_name}\uc774(\uac00) \uaebc\uc84c\uc744 \ub54c",
+ "turned_on": "{entity_name}\uc774(\uac00) \ucf1c\uc84c\uc744 \ub54c"
}
},
"state": {
diff --git a/homeassistant/components/remote/translations/tr.json b/homeassistant/components/remote/translations/tr.json
index cdc40c6268bb56..5359c99a78a28e 100644
--- a/homeassistant/components/remote/translations/tr.json
+++ b/homeassistant/components/remote/translations/tr.json
@@ -1,4 +1,14 @@
{
+ "device_automation": {
+ "action_type": {
+ "turn_off": "{entity_name} kapat",
+ "turn_on": "{entity_name} a\u00e7\u0131n"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} kapat\u0131ld\u0131",
+ "turned_on": "{entity_name} a\u00e7\u0131ld\u0131"
+ }
+ },
"state": {
"_": {
"off": "Kapal\u0131",
diff --git a/homeassistant/components/remote/translations/uk.json b/homeassistant/components/remote/translations/uk.json
index 2feda4928e5847..1f275f5f2ebe87 100644
--- a/homeassistant/components/remote/translations/uk.json
+++ b/homeassistant/components/remote/translations/uk.json
@@ -1,5 +1,14 @@
{
"device_automation": {
+ "action_type": {
+ "toggle": "{entity_name}: \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u0438",
+ "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438",
+ "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456"
+ },
"trigger_type": {
"turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
"turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u043e"
diff --git a/homeassistant/components/remote/translations/zh-Hans.json b/homeassistant/components/remote/translations/zh-Hans.json
index ba1344fbb744af..f6c509d4a0861f 100644
--- a/homeassistant/components/remote/translations/zh-Hans.json
+++ b/homeassistant/components/remote/translations/zh-Hans.json
@@ -1,7 +1,9 @@
{
"device_automation": {
"action_type": {
- "turn_off": "\u5173\u95ed {entity_name}"
+ "toggle": "\u5207\u6362 {entity_name} \u5f00\u5173",
+ "turn_off": "\u5173\u95ed {entity_name}",
+ "turn_on": "\u6253\u5f00 {entity_name}"
},
"condition_type": {
"is_off": "{entity_name} \u5df2\u5173\u95ed",
diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py
index e342b2d341ed97..77a3c51e9cf177 100644
--- a/homeassistant/components/repetier/sensor.py
+++ b/homeassistant/components/repetier/sensor.py
@@ -3,10 +3,10 @@
import logging
import time
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_TIMESTAMP
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from . import REPETIER_API, SENSOR_TYPES, UPDATE_SIGNAL
@@ -46,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(entities, True)
-class RepetierSensor(Entity):
+class RepetierSensor(SensorEntity):
"""Class to create and populate a Repetier Sensor."""
def __init__(self, api, temp_id, name, printer_id, sensor_type):
@@ -66,7 +66,7 @@ def available(self) -> bool:
return self._available
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return sensor attributes."""
return self._attributes
diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py
index 69bc61723418f6..8b9390bb1c922d 100644
--- a/homeassistant/components/rest/__init__.py
+++ b/homeassistant/components/rest/__init__.py
@@ -1,4 +1,173 @@
"""The rest component."""
-DOMAIN = "rest"
+import asyncio
+import logging
+
+import httpx
+import voluptuous as vol
+
+from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.const import (
+ CONF_AUTHENTICATION,
+ CONF_HEADERS,
+ CONF_METHOD,
+ CONF_PARAMS,
+ CONF_PASSWORD,
+ CONF_PAYLOAD,
+ CONF_RESOURCE,
+ CONF_RESOURCE_TEMPLATE,
+ CONF_SCAN_INTERVAL,
+ CONF_TIMEOUT,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+ HTTP_DIGEST_AUTHENTICATION,
+ SERVICE_RELOAD,
+)
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers import discovery
+from homeassistant.helpers.entity_component import (
+ DEFAULT_SCAN_INTERVAL,
+ EntityComponent,
+)
+from homeassistant.helpers.reload import async_reload_integration_platforms
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+
+from .const import COORDINATOR, DOMAIN, PLATFORM_IDX, REST, REST_DATA, REST_IDX
+from .data import RestData
+from .schema import CONFIG_SCHEMA # noqa: F401
+
+_LOGGER = logging.getLogger(__name__)
+
PLATFORMS = ["binary_sensor", "notify", "sensor", "switch"]
+COORDINATOR_AWARE_PLATFORMS = [SENSOR_DOMAIN, BINARY_SENSOR_DOMAIN]
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+ """Set up the rest platforms."""
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ _async_setup_shared_data(hass)
+
+ async def reload_service_handler(service):
+ """Remove all user-defined groups and load new ones from config."""
+ conf = await component.async_prepare_reload()
+ if conf is None:
+ return
+ await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS)
+ _async_setup_shared_data(hass)
+ await _async_process_config(hass, conf)
+
+ hass.services.async_register(
+ DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({})
+ )
+
+ return await _async_process_config(hass, config)
+
+
+@callback
+def _async_setup_shared_data(hass: HomeAssistant):
+ """Create shared data for platform config and rest coordinators."""
+ hass.data[DOMAIN] = {key: [] for key in [REST_DATA, *COORDINATOR_AWARE_PLATFORMS]}
+
+
+async def _async_process_config(hass, config) -> bool:
+ """Process rest configuration."""
+ if DOMAIN not in config:
+ return True
+
+ refresh_tasks = []
+ load_tasks = []
+ for rest_idx, conf in enumerate(config[DOMAIN]):
+ scan_interval = conf.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
+ resource_template = conf.get(CONF_RESOURCE_TEMPLATE)
+ rest = create_rest_data_from_config(hass, conf)
+ coordinator = _rest_coordinator(hass, rest, resource_template, scan_interval)
+ refresh_tasks.append(coordinator.async_refresh())
+ hass.data[DOMAIN][REST_DATA].append({REST: rest, COORDINATOR: coordinator})
+
+ for platform_domain in COORDINATOR_AWARE_PLATFORMS:
+ if platform_domain not in conf:
+ continue
+
+ for platform_conf in conf[platform_domain]:
+ hass.data[DOMAIN][platform_domain].append(platform_conf)
+ platform_idx = len(hass.data[DOMAIN][platform_domain]) - 1
+
+ load = discovery.async_load_platform(
+ hass,
+ platform_domain,
+ DOMAIN,
+ {REST_IDX: rest_idx, PLATFORM_IDX: platform_idx},
+ config,
+ )
+ load_tasks.append(load)
+
+ if refresh_tasks:
+ await asyncio.gather(*refresh_tasks)
+
+ if load_tasks:
+ await asyncio.gather(*load_tasks)
+
+ return True
+
+
+async def async_get_config_and_coordinator(hass, platform_domain, discovery_info):
+ """Get the config and coordinator for the platform from discovery."""
+ shared_data = hass.data[DOMAIN][REST_DATA][discovery_info[REST_IDX]]
+ conf = hass.data[DOMAIN][platform_domain][discovery_info[PLATFORM_IDX]]
+ coordinator = shared_data[COORDINATOR]
+ rest = shared_data[REST]
+ if rest.data is None:
+ await coordinator.async_request_refresh()
+ return conf, coordinator, rest
+
+
+def _rest_coordinator(hass, rest, resource_template, update_interval):
+ """Wrap a DataUpdateCoordinator around the rest object."""
+ if resource_template:
+
+ async def _async_refresh_with_resource_template():
+ rest.set_url(resource_template.async_render(parse_result=False))
+ await rest.async_update()
+
+ update_method = _async_refresh_with_resource_template
+ else:
+ update_method = rest.async_update
+
+ return DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name="rest data",
+ update_method=update_method,
+ update_interval=update_interval,
+ )
+
+
+def create_rest_data_from_config(hass, config):
+ """Create RestData from config."""
+ resource = config.get(CONF_RESOURCE)
+ resource_template = config.get(CONF_RESOURCE_TEMPLATE)
+ method = config.get(CONF_METHOD)
+ payload = config.get(CONF_PAYLOAD)
+ verify_ssl = config.get(CONF_VERIFY_SSL)
+ username = config.get(CONF_USERNAME)
+ password = config.get(CONF_PASSWORD)
+ headers = config.get(CONF_HEADERS)
+ params = config.get(CONF_PARAMS)
+ timeout = config.get(CONF_TIMEOUT)
+
+ if resource_template is not None:
+ resource_template.hass = hass
+ resource = resource_template.async_render(parse_result=False)
+
+ if username and password:
+ if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
+ auth = httpx.DigestAuth(username, password)
+ else:
+ auth = (username, password)
+ else:
+ auth = None
+
+ return RestData(
+ hass, method, resource, auth, headers, params, payload, verify_ssl, timeout
+ )
diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py
index 49c10354c5138b..a90c5bd7c77060 100644
--- a/homeassistant/components/rest/binary_sensor.py
+++ b/homeassistant/components/rest/binary_sensor.py
@@ -1,64 +1,27 @@
"""Support for RESTful binary sensors."""
-import httpx
import voluptuous as vol
from homeassistant.components.binary_sensor import (
- DEVICE_CLASSES_SCHEMA,
+ DOMAIN as BINARY_SENSOR_DOMAIN,
PLATFORM_SCHEMA,
BinarySensorEntity,
)
from homeassistant.const import (
- CONF_AUTHENTICATION,
CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE,
- CONF_HEADERS,
- CONF_METHOD,
CONF_NAME,
- CONF_PARAMS,
- CONF_PASSWORD,
- CONF_PAYLOAD,
CONF_RESOURCE,
CONF_RESOURCE_TEMPLATE,
- CONF_TIMEOUT,
- CONF_USERNAME,
CONF_VALUE_TEMPLATE,
- CONF_VERIFY_SSL,
- HTTP_BASIC_AUTHENTICATION,
- HTTP_DIGEST_AUTHENTICATION,
)
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.reload import async_setup_reload_service
-
-from . import DOMAIN, PLATFORMS
-from .data import DEFAULT_TIMEOUT, RestData
-
-DEFAULT_METHOD = "GET"
-DEFAULT_NAME = "REST Binary Sensor"
-DEFAULT_VERIFY_SSL = True
-DEFAULT_FORCE_UPDATE = False
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Exclusive(CONF_RESOURCE, CONF_RESOURCE): cv.url,
- vol.Exclusive(CONF_RESOURCE_TEMPLATE, CONF_RESOURCE): cv.template,
- vol.Optional(CONF_AUTHENTICATION): vol.In(
- [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]
- ),
- vol.Optional(CONF_HEADERS): {cv.string: cv.string},
- vol.Optional(CONF_PARAMS): {cv.string: cv.string},
- vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(["POST", "GET"]),
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_PAYLOAD): cv.string,
- vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
- vol.Optional(CONF_USERNAME): cv.string,
- vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
- vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
- vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
- vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
- }
-)
+
+from . import async_get_config_and_coordinator, create_rest_data_from_config
+from .entity import RestEntity
+from .schema import BINARY_SENSOR_SCHEMA, RESOURCE_SCHEMA
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **BINARY_SENSOR_SCHEMA})
PLATFORM_SCHEMA = vol.All(
cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), PLATFORM_SCHEMA
@@ -67,51 +30,36 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the REST binary sensor."""
-
- await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
-
- name = config.get(CONF_NAME)
- resource = config.get(CONF_RESOURCE)
- resource_template = config.get(CONF_RESOURCE_TEMPLATE)
- method = config.get(CONF_METHOD)
- payload = config.get(CONF_PAYLOAD)
- verify_ssl = config.get(CONF_VERIFY_SSL)
- timeout = config.get(CONF_TIMEOUT)
- username = config.get(CONF_USERNAME)
- password = config.get(CONF_PASSWORD)
- headers = config.get(CONF_HEADERS)
- params = config.get(CONF_PARAMS)
- device_class = config.get(CONF_DEVICE_CLASS)
- value_template = config.get(CONF_VALUE_TEMPLATE)
- force_update = config.get(CONF_FORCE_UPDATE)
-
- if resource_template is not None:
- resource_template.hass = hass
- resource = resource_template.async_render(parse_result=False)
-
- if value_template is not None:
- value_template.hass = hass
-
- if username and password:
- if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
- auth = httpx.DigestAuth(username, password)
- else:
- auth = (username, password)
+ # Must update the sensor now (including fetching the rest resource) to
+ # ensure it's updating its state.
+ if discovery_info is not None:
+ conf, coordinator, rest = await async_get_config_and_coordinator(
+ hass, BINARY_SENSOR_DOMAIN, discovery_info
+ )
else:
- auth = None
-
- rest = RestData(
- hass, method, resource, auth, headers, params, payload, verify_ssl, timeout
- )
- await rest.async_update()
+ conf = config
+ coordinator = None
+ rest = create_rest_data_from_config(hass, conf)
+ await rest.async_update(log_errors=False)
if rest.data is None:
+ if rest.last_exception:
+ raise PlatformNotReady from rest.last_exception
raise PlatformNotReady
+ name = conf.get(CONF_NAME)
+ device_class = conf.get(CONF_DEVICE_CLASS)
+ value_template = conf.get(CONF_VALUE_TEMPLATE)
+ force_update = conf.get(CONF_FORCE_UPDATE)
+ resource_template = conf.get(CONF_RESOURCE_TEMPLATE)
+
+ if value_template is not None:
+ value_template.hass = hass
+
async_add_entities(
[
RestBinarySensor(
- hass,
+ coordinator,
rest,
name,
device_class,
@@ -123,12 +71,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
-class RestBinarySensor(BinarySensorEntity):
+class RestBinarySensor(RestEntity, BinarySensorEntity):
"""Representation of a REST binary sensor."""
def __init__(
self,
- hass,
+ coordinator,
rest,
name,
device_class,
@@ -137,36 +85,23 @@ def __init__(
resource_template,
):
"""Initialize a REST binary sensor."""
- self._hass = hass
- self.rest = rest
- self._name = name
- self._device_class = device_class
+ super().__init__(
+ coordinator, rest, name, device_class, resource_template, force_update
+ )
self._state = False
self._previous_data = None
self._value_template = value_template
- self._force_update = force_update
- self._resource_template = resource_template
-
- @property
- def name(self):
- """Return the name of the binary sensor."""
- return self._name
-
- @property
- def device_class(self):
- """Return the class of this sensor."""
- return self._device_class
-
- @property
- def available(self):
- """Return the availability of this sensor."""
- return self.rest.data is not None
+ self._is_on = None
@property
def is_on(self):
"""Return true if the binary sensor is on."""
+ return self._is_on
+
+ def _update_from_rest_data(self):
+ """Update state from the rest data."""
if self.rest.data is None:
- return False
+ self._is_on = False
response = self.rest.data
@@ -176,20 +111,8 @@ def is_on(self):
)
try:
- return bool(int(response))
+ self._is_on = bool(int(response))
except ValueError:
- return {"true": True, "on": True, "open": True, "yes": True}.get(
+ self._is_on = {"true": True, "on": True, "open": True, "yes": True}.get(
response.lower(), False
)
-
- @property
- def force_update(self):
- """Force update."""
- return self._force_update
-
- async def async_update(self):
- """Get the latest data from REST API and updates the state."""
- if self._resource_template is not None:
- self.rest.set_url(self._resource_template.async_render(parse_result=False))
-
- await self.rest.async_update()
diff --git a/homeassistant/components/rest/const.py b/homeassistant/components/rest/const.py
new file mode 100644
index 00000000000000..5fd32d8fba774f
--- /dev/null
+++ b/homeassistant/components/rest/const.py
@@ -0,0 +1,22 @@
+"""The rest component constants."""
+
+DOMAIN = "rest"
+
+DEFAULT_METHOD = "GET"
+DEFAULT_VERIFY_SSL = True
+DEFAULT_FORCE_UPDATE = False
+
+DEFAULT_BINARY_SENSOR_NAME = "REST Binary Sensor"
+DEFAULT_SENSOR_NAME = "REST Sensor"
+CONF_JSON_ATTRS = "json_attributes"
+CONF_JSON_ATTRS_PATH = "json_attributes_path"
+
+REST_IDX = "rest_idx"
+PLATFORM_IDX = "platform_idx"
+
+COORDINATOR = "coordinator"
+REST = "rest"
+
+REST_DATA = "rest_data"
+
+METHODS = ["POST", "GET"]
diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py
index dd2e29616c7680..8b03bcfb128763 100644
--- a/homeassistant/components/rest/data.py
+++ b/homeassistant/components/rest/data.py
@@ -37,13 +37,14 @@ def __init__(
self._verify_ssl = verify_ssl
self._async_client = None
self.data = None
+ self.last_exception = None
self.headers = None
def set_url(self, url):
"""Set url."""
self._resource = url
- async def async_update(self):
+ async def async_update(self, log_errors=True):
"""Get the latest data from REST service with provided method."""
if not self._async_client:
self._async_client = get_async_client(
@@ -64,6 +65,10 @@ async def async_update(self):
self.data = response.text
self.headers = response.headers
except httpx.RequestError as ex:
- _LOGGER.error("Error fetching data: %s failed with %s", self._resource, ex)
+ if log_errors:
+ _LOGGER.error(
+ "Error fetching data: %s failed with %s", self._resource, ex
+ )
+ self.last_exception = ex
self.data = None
self.headers = None
diff --git a/homeassistant/components/rest/entity.py b/homeassistant/components/rest/entity.py
new file mode 100644
index 00000000000000..acfe5a2dfc5c1e
--- /dev/null
+++ b/homeassistant/components/rest/entity.py
@@ -0,0 +1,89 @@
+"""The base entity for the rest component."""
+
+from abc import abstractmethod
+from typing import Any
+
+from homeassistant.core import callback
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+
+from .data import RestData
+
+
+class RestEntity(Entity):
+ """A class for entities using DataUpdateCoordinator or rest data directly."""
+
+ def __init__(
+ self,
+ coordinator: DataUpdateCoordinator[Any],
+ rest: RestData,
+ name,
+ device_class,
+ resource_template,
+ force_update,
+ ) -> None:
+ """Create the entity that may have a coordinator."""
+ self.coordinator = coordinator
+ self.rest = rest
+ self._name = name
+ self._device_class = device_class
+ self._resource_template = resource_template
+ self._force_update = force_update
+ super().__init__()
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @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
+
+ @property
+ def should_poll(self) -> bool:
+ """Poll only if we do noty have a coordinator."""
+ return not self.coordinator
+
+ @property
+ def available(self):
+ """Return the availability of this sensor."""
+ if self.coordinator and not self.coordinator.last_update_success:
+ return False
+ return self.rest.data is not None
+
+ async def async_added_to_hass(self) -> None:
+ """When entity is added to hass."""
+ await super().async_added_to_hass()
+ self._update_from_rest_data()
+ if self.coordinator:
+ self.async_on_remove(
+ self.coordinator.async_add_listener(self._handle_coordinator_update)
+ )
+
+ @callback
+ def _handle_coordinator_update(self) -> None:
+ """Handle updated data from the coordinator."""
+ self._update_from_rest_data()
+ self.async_write_ha_state()
+
+ async def async_update(self):
+ """Get the latest data from REST API and update the state."""
+ if self.coordinator:
+ await self.coordinator.async_request_refresh()
+ return
+
+ if self._resource_template is not None:
+ self.rest.set_url(self._resource_template.async_render(parse_result=False))
+ await self.rest.async_update()
+ self._update_from_rest_data()
+
+ @abstractmethod
+ def _update_from_rest_data(self):
+ """Update state from the rest data."""
diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py
index f15df428640328..198e5b06c52865 100644
--- a/homeassistant/components/rest/notify.py
+++ b/homeassistant/components/rest/notify.py
@@ -29,11 +29,8 @@
HTTP_OK,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.reload import setup_reload_service
from homeassistant.helpers.template import Template
-from . import DOMAIN, PLATFORMS
-
CONF_DATA = "data"
CONF_DATA_TEMPLATE = "data_template"
CONF_MESSAGE_PARAMETER_NAME = "message_param_name"
@@ -73,8 +70,6 @@
def get_service(hass, config, discovery_info=None):
"""Get the RESTful notification service."""
- setup_reload_service(hass, DOMAIN, PLATFORMS)
-
resource = config.get(CONF_RESOURCE)
method = config.get(CONF_METHOD)
headers = config.get(CONF_HEADERS)
diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py
new file mode 100644
index 00000000000000..bedd02d272a438
--- /dev/null
+++ b/homeassistant/components/rest/schema.py
@@ -0,0 +1,99 @@
+"""The rest component schemas."""
+
+import voluptuous as vol
+
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
+ DOMAIN as BINARY_SENSOR_DOMAIN,
+)
+from homeassistant.components.sensor import (
+ DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
+ DOMAIN as SENSOR_DOMAIN,
+)
+from homeassistant.const import (
+ CONF_AUTHENTICATION,
+ CONF_DEVICE_CLASS,
+ CONF_FORCE_UPDATE,
+ CONF_HEADERS,
+ CONF_METHOD,
+ CONF_NAME,
+ CONF_PARAMS,
+ CONF_PASSWORD,
+ CONF_PAYLOAD,
+ CONF_RESOURCE,
+ CONF_RESOURCE_TEMPLATE,
+ CONF_SCAN_INTERVAL,
+ CONF_TIMEOUT,
+ CONF_UNIT_OF_MEASUREMENT,
+ CONF_USERNAME,
+ CONF_VALUE_TEMPLATE,
+ CONF_VERIFY_SSL,
+ HTTP_BASIC_AUTHENTICATION,
+ HTTP_DIGEST_AUTHENTICATION,
+)
+import homeassistant.helpers.config_validation as cv
+
+from .const import (
+ CONF_JSON_ATTRS,
+ CONF_JSON_ATTRS_PATH,
+ DEFAULT_BINARY_SENSOR_NAME,
+ DEFAULT_FORCE_UPDATE,
+ DEFAULT_METHOD,
+ DEFAULT_SENSOR_NAME,
+ DEFAULT_VERIFY_SSL,
+ DOMAIN,
+ METHODS,
+)
+from .data import DEFAULT_TIMEOUT
+
+RESOURCE_SCHEMA = {
+ vol.Exclusive(CONF_RESOURCE, CONF_RESOURCE): cv.url,
+ vol.Exclusive(CONF_RESOURCE_TEMPLATE, CONF_RESOURCE): cv.template,
+ vol.Optional(CONF_AUTHENTICATION): vol.In(
+ [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]
+ ),
+ vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}),
+ vol.Optional(CONF_PARAMS): vol.Schema({cv.string: cv.string}),
+ vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(METHODS),
+ vol.Optional(CONF_USERNAME): cv.string,
+ vol.Optional(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_PAYLOAD): cv.string,
+ vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
+ vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
+}
+
+SENSOR_SCHEMA = {
+ vol.Optional(CONF_NAME, default=DEFAULT_SENSOR_NAME): cv.string,
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
+ vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
+ vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv,
+ vol.Optional(CONF_JSON_ATTRS_PATH): cv.string,
+ vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
+ vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
+}
+
+BINARY_SENSOR_SCHEMA = {
+ vol.Optional(CONF_NAME, default=DEFAULT_BINARY_SENSOR_NAME): cv.string,
+ vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
+ vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
+ vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
+}
+
+
+COMBINED_SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_SCAN_INTERVAL): cv.time_period,
+ **RESOURCE_SCHEMA,
+ vol.Optional(SENSOR_DOMAIN): vol.All(
+ cv.ensure_list, [vol.Schema(SENSOR_SCHEMA)]
+ ),
+ vol.Optional(BINARY_SENSOR_DOMAIN): vol.All(
+ cv.ensure_list, [vol.Schema(BINARY_SENSOR_SCHEMA)]
+ ),
+ }
+)
+
+CONFIG_SCHEMA = vol.Schema(
+ {DOMAIN: vol.All(cv.ensure_list, [COMBINED_SCHEMA])},
+ extra=vol.ALLOW_EXTRA,
+)
diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py
index 85d79b6b33172a..7727b5f09ab46f 100644
--- a/homeassistant/components/rest/sensor.py
+++ b/homeassistant/components/rest/sensor.py
@@ -3,76 +3,35 @@
import logging
from xml.parsers.expat import ExpatError
-import httpx
from jsonpath import jsonpath
import voluptuous as vol
import xmltodict
-from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA
+from homeassistant.components.sensor import (
+ DOMAIN as SENSOR_DOMAIN,
+ PLATFORM_SCHEMA,
+ SensorEntity,
+)
from homeassistant.const import (
- CONF_AUTHENTICATION,
CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE,
- CONF_HEADERS,
- CONF_METHOD,
CONF_NAME,
- CONF_PARAMS,
- CONF_PASSWORD,
- CONF_PAYLOAD,
CONF_RESOURCE,
CONF_RESOURCE_TEMPLATE,
- CONF_TIMEOUT,
CONF_UNIT_OF_MEASUREMENT,
- CONF_USERNAME,
CONF_VALUE_TEMPLATE,
- CONF_VERIFY_SSL,
- HTTP_BASIC_AUTHENTICATION,
- HTTP_DIGEST_AUTHENTICATION,
)
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.reload import async_setup_reload_service
-from . import DOMAIN, PLATFORMS
-from .data import DEFAULT_TIMEOUT, RestData
+from . import async_get_config_and_coordinator, create_rest_data_from_config
+from .const import CONF_JSON_ATTRS, CONF_JSON_ATTRS_PATH
+from .entity import RestEntity
+from .schema import RESOURCE_SCHEMA, SENSOR_SCHEMA
_LOGGER = logging.getLogger(__name__)
-DEFAULT_METHOD = "GET"
-DEFAULT_NAME = "REST Sensor"
-DEFAULT_VERIFY_SSL = True
-DEFAULT_FORCE_UPDATE = False
-
-
-CONF_JSON_ATTRS = "json_attributes"
-CONF_JSON_ATTRS_PATH = "json_attributes_path"
-METHODS = ["POST", "GET"]
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Exclusive(CONF_RESOURCE, CONF_RESOURCE): cv.url,
- vol.Exclusive(CONF_RESOURCE_TEMPLATE, CONF_RESOURCE): cv.template,
- vol.Optional(CONF_AUTHENTICATION): vol.In(
- [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]
- ),
- vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}),
- vol.Optional(CONF_PARAMS): vol.Schema({cv.string: cv.string}),
- vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv,
- vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(METHODS),
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_PAYLOAD): cv.string,
- vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
- vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
- vol.Optional(CONF_USERNAME): cv.string,
- vol.Optional(CONF_JSON_ATTRS_PATH): cv.string,
- vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
- vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
- vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
- vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
- }
-)
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **SENSOR_SCHEMA})
PLATFORM_SCHEMA = vol.All(
cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), PLATFORM_SCHEMA
@@ -81,55 +40,39 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the RESTful sensor."""
- await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
-
- name = config.get(CONF_NAME)
- resource = config.get(CONF_RESOURCE)
- resource_template = config.get(CONF_RESOURCE_TEMPLATE)
- method = config.get(CONF_METHOD)
- payload = config.get(CONF_PAYLOAD)
- verify_ssl = config.get(CONF_VERIFY_SSL)
- username = config.get(CONF_USERNAME)
- password = config.get(CONF_PASSWORD)
- headers = config.get(CONF_HEADERS)
- params = config.get(CONF_PARAMS)
- unit = config.get(CONF_UNIT_OF_MEASUREMENT)
- device_class = config.get(CONF_DEVICE_CLASS)
- value_template = config.get(CONF_VALUE_TEMPLATE)
- json_attrs = config.get(CONF_JSON_ATTRS)
- json_attrs_path = config.get(CONF_JSON_ATTRS_PATH)
- force_update = config.get(CONF_FORCE_UPDATE)
- timeout = config.get(CONF_TIMEOUT)
-
- if value_template is not None:
- value_template.hass = hass
-
- if resource_template is not None:
- resource_template.hass = hass
- resource = resource_template.async_render(parse_result=False)
-
- if username and password:
- if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
- auth = httpx.DigestAuth(username, password)
- else:
- auth = (username, password)
+ # Must update the sensor now (including fetching the rest resource) to
+ # ensure it's updating its state.
+ if discovery_info is not None:
+ conf, coordinator, rest = await async_get_config_and_coordinator(
+ hass, SENSOR_DOMAIN, discovery_info
+ )
else:
- auth = None
- rest = RestData(
- hass, method, resource, auth, headers, params, payload, verify_ssl, timeout
- )
-
- await rest.async_update()
+ conf = config
+ coordinator = None
+ rest = create_rest_data_from_config(hass, conf)
+ await rest.async_update(log_errors=False)
if rest.data is None:
+ if rest.last_exception:
+ raise PlatformNotReady from rest.last_exception
raise PlatformNotReady
- # Must update the sensor now (including fetching the rest resource) to
- # ensure it's updating its state.
+ name = conf.get(CONF_NAME)
+ unit = conf.get(CONF_UNIT_OF_MEASUREMENT)
+ device_class = conf.get(CONF_DEVICE_CLASS)
+ json_attrs = conf.get(CONF_JSON_ATTRS)
+ json_attrs_path = conf.get(CONF_JSON_ATTRS_PATH)
+ value_template = conf.get(CONF_VALUE_TEMPLATE)
+ force_update = conf.get(CONF_FORCE_UPDATE)
+ resource_template = conf.get(CONF_RESOURCE_TEMPLATE)
+
+ if value_template is not None:
+ value_template.hass = hass
+
async_add_entities(
[
RestSensor(
- hass,
+ coordinator,
rest,
name,
unit,
@@ -144,12 +87,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
-class RestSensor(Entity):
+class RestSensor(RestEntity, SensorEntity):
"""Implementation of a REST sensor."""
def __init__(
self,
- hass,
+ coordinator,
rest,
name,
unit_of_measurement,
@@ -161,60 +104,30 @@ def __init__(
json_attrs_path,
):
"""Initialize the REST sensor."""
- self._hass = hass
- self.rest = rest
- self._name = name
+ super().__init__(
+ coordinator, rest, name, device_class, resource_template, force_update
+ )
self._state = None
self._unit_of_measurement = unit_of_measurement
- self._device_class = device_class
self._value_template = value_template
self._json_attrs = json_attrs
self._attributes = None
- self._force_update = force_update
- self._resource_template = resource_template
self._json_attrs_path = json_attrs_path
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._unit_of_measurement
- @property
- def device_class(self):
- """Return the class of this sensor."""
- return self._device_class
-
- @property
- def available(self):
- """Return if the sensor data are available."""
- return self.rest.data is not None
-
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
- def force_update(self):
- """Force update."""
- return self._force_update
-
- async def async_update(self):
- """Get the latest data from REST API and update the state."""
- if self._resource_template is not None:
- self.rest.set_url(self._resource_template.async_render(parse_result=False))
-
- await self.rest.async_update()
- self._update_from_rest_data()
-
- async def async_added_to_hass(self):
- """Ensure the data from the initial update is reflected in the state."""
- self._update_from_rest_data()
+ def extra_state_attributes(self):
+ """Return the state attributes."""
+ return self._attributes
def _update_from_rest_data(self):
"""Update state from the rest data."""
@@ -273,8 +186,3 @@ def _update_from_rest_data(self):
)
self._state = value
-
- @property
- def device_state_attributes(self):
- """Return the state attributes."""
- return self._attributes
diff --git a/homeassistant/components/rest/services.yaml b/homeassistant/components/rest/services.yaml
index 06baa8734f2979..7e324670134a28 100644
--- a/homeassistant/components/rest/services.yaml
+++ b/homeassistant/components/rest/services.yaml
@@ -1,2 +1,2 @@
reload:
- description: Reload all rest entities and notify services.
+ description: Reload all rest entities and notify services
diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py
index ea480d549f3ac2..e8ae1dee0158b6 100644
--- a/homeassistant/components/rest/switch.py
+++ b/homeassistant/components/rest/switch.py
@@ -22,12 +22,8 @@
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.reload import async_setup_reload_service
-
-from . import DOMAIN, PLATFORMS
_LOGGER = logging.getLogger(__name__)
-
CONF_BODY_OFF = "body_off"
CONF_BODY_ON = "body_on"
CONF_IS_ON_TEMPLATE = "is_on_template"
@@ -65,9 +61,6 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the RESTful switch."""
-
- await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
-
body_off = config.get(CONF_BODY_OFF)
body_on = config.get(CONF_BODY_ON)
is_on_template = config.get(CONF_IS_ON_TEMPLATE)
diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py
index 116b24642134be..c78b0c6f944f5e 100644
--- a/homeassistant/components/rflink/__init__.py
+++ b/homeassistant/components/rflink/__init__.py
@@ -10,7 +10,9 @@
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_STATE,
CONF_COMMAND,
+ CONF_DEVICE_ID,
CONF_HOST,
CONF_PORT,
EVENT_HOMEASSISTANT_STOP,
@@ -26,30 +28,31 @@
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.restore_state import RestoreEntity
+from .utils import brightness_to_rflink
+
_LOGGER = logging.getLogger(__name__)
ATTR_EVENT = "event"
-ATTR_STATE = "state"
CONF_ALIASES = "aliases"
CONF_GROUP_ALIASES = "group_aliases"
CONF_GROUP = "group"
CONF_NOGROUP_ALIASES = "nogroup_aliases"
CONF_DEVICE_DEFAULTS = "device_defaults"
-CONF_DEVICE_ID = "device_id"
-CONF_DEVICES = "devices"
CONF_AUTOMATIC_ADD = "automatic_add"
CONF_FIRE_EVENT = "fire_event"
CONF_IGNORE_DEVICES = "ignore_devices"
CONF_RECONNECT_INTERVAL = "reconnect_interval"
CONF_SIGNAL_REPETITIONS = "signal_repetitions"
CONF_WAIT_FOR_ACK = "wait_for_ack"
+CONF_KEEPALIVE_IDLE = "tcp_keepalive_idle_timer"
DATA_DEVICE_REGISTER = "rflink_device_register"
DATA_ENTITY_LOOKUP = "rflink_entity_lookup"
DATA_ENTITY_GROUP_LOOKUP = "rflink_entity_group_only_lookup"
DEFAULT_RECONNECT_INTERVAL = 10
DEFAULT_SIGNAL_REPETITIONS = 1
+DEFAULT_TCP_KEEPALIVE_IDLE_TIMER = 3600
CONNECTION_TIMEOUT = 10
EVENT_BUTTON_PRESSED = "button_pressed"
@@ -66,6 +69,7 @@
SIGNAL_AVAILABILITY = "rflink_device_available"
SIGNAL_HANDLE_EVENT = "rflink_handle_event_{}"
+SIGNAL_EVENT = "rflink_event"
TMP_ENTITY = "tmp.{}"
@@ -85,6 +89,9 @@
vol.Required(CONF_PORT): vol.Any(cv.port, cv.string),
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_WAIT_FOR_ACK, default=True): cv.boolean,
+ vol.Optional(
+ CONF_KEEPALIVE_IDLE, default=DEFAULT_TCP_KEEPALIVE_IDLE_TIMER
+ ): int,
vol.Optional(
CONF_RECONNECT_INTERVAL, default=DEFAULT_RECONNECT_INTERVAL
): int,
@@ -136,6 +143,15 @@ async def async_send_command(call):
)
):
_LOGGER.error("Failed Rflink command for %s", str(call.data))
+ else:
+ async_dispatcher_send(
+ hass,
+ SIGNAL_EVENT,
+ {
+ EVENT_KEY_ID: call.data.get(CONF_DEVICE_ID),
+ EVENT_KEY_COMMAND: call.data.get(CONF_COMMAND),
+ },
+ )
hass.services.async_register(
DOMAIN, SERVICE_SEND_COMMAND, async_send_command, schema=SEND_COMMAND_SCHEMA
@@ -199,6 +215,29 @@ def event_callback(event):
# TCP port when host configured, otherwise serial port
port = config[DOMAIN][CONF_PORT]
+ keepalive_idle_timer = None
+ # TCP KeepAlive only if this is TCP based connection (not serial)
+ if host is not None:
+ # TCP KEEPALIVE will be enabled if value > 0
+ keepalive_idle_timer = config[DOMAIN][CONF_KEEPALIVE_IDLE]
+ if keepalive_idle_timer < 0:
+ _LOGGER.error(
+ "A bogus TCP Keepalive IDLE timer was provided (%d secs), "
+ "it will be disabled. "
+ "Recommended values: 60-3600 (seconds)",
+ keepalive_idle_timer,
+ )
+ keepalive_idle_timer = None
+ elif keepalive_idle_timer == 0:
+ keepalive_idle_timer = None
+ elif keepalive_idle_timer <= 30:
+ _LOGGER.warning(
+ "A very short TCP Keepalive IDLE timer was provided (%d secs) "
+ "and may produce unexpected disconnections from RFlink device."
+ " Recommended values: 60-3600 (seconds)",
+ keepalive_idle_timer,
+ )
+
@callback
def reconnect(exc=None):
"""Schedule reconnect after connection has been unexpectedly lost."""
@@ -209,7 +248,7 @@ def reconnect(exc=None):
# If HA is not stopping, initiate new connection
if hass.state != CoreState.stopping:
- _LOGGER.warning("disconnected from Rflink, reconnecting")
+ _LOGGER.warning("Disconnected from Rflink, reconnecting")
hass.async_create_task(connect())
async def connect():
@@ -223,6 +262,7 @@ async def connect():
connection = create_rflink_connection(
port=port,
host=host,
+ keepalive=keepalive_idle_timer,
event_callback=event_callback,
disconnect_callback=reconnect,
loop=hass.loop,
@@ -265,6 +305,7 @@ async def connect():
_LOGGER.info("Connected to Rflink")
hass.async_create_task(connect())
+ async_dispatcher_connect(hass, SIGNAL_EVENT, event_callback)
return True
@@ -470,7 +511,7 @@ async def _async_handle_command(self, command, *args):
elif command == "dim":
# convert brightness to rflink dim level
- cmd = str(int(args[0] / 17))
+ cmd = str(brightness_to_rflink(args[0]))
self._state = True
elif command == "toggle":
diff --git a/homeassistant/components/rflink/binary_sensor.py b/homeassistant/components/rflink/binary_sensor.py
index dd16343898d8bb..77a8a522f6594b 100644
--- a/homeassistant/components/rflink/binary_sensor.py
+++ b/homeassistant/components/rflink/binary_sensor.py
@@ -6,11 +6,16 @@
PLATFORM_SCHEMA,
BinarySensorEntity,
)
-from homeassistant.const import CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, CONF_NAME
+from homeassistant.const import (
+ CONF_DEVICE_CLASS,
+ CONF_DEVICES,
+ CONF_FORCE_UPDATE,
+ CONF_NAME,
+)
import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.event as evt
-from . import CONF_ALIASES, CONF_DEVICES, RflinkDevice
+from . import CONF_ALIASES, RflinkDevice
CONF_OFF_DELAY = "off_delay"
DEFAULT_FORCE_UPDATE = False
diff --git a/homeassistant/components/rflink/cover.py b/homeassistant/components/rflink/cover.py
index 5eacce3afa83c4..2e6837d21ea6e2 100644
--- a/homeassistant/components/rflink/cover.py
+++ b/homeassistant/components/rflink/cover.py
@@ -4,14 +4,13 @@
import voluptuous as vol
from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity
-from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_OPEN
+from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_TYPE, STATE_OPEN
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity
from . import (
CONF_ALIASES,
CONF_DEVICE_DEFAULTS,
- CONF_DEVICES,
CONF_FIRE_EVENT,
CONF_GROUP,
CONF_GROUP_ALIASES,
diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py
index 6d63e12378d2db..5a0d67661797c5 100644
--- a/homeassistant/components/rflink/light.py
+++ b/homeassistant/components/rflink/light.py
@@ -1,5 +1,6 @@
"""Support for Rflink lights."""
import logging
+import re
import voluptuous as vol
@@ -9,14 +10,13 @@
SUPPORT_BRIGHTNESS,
LightEntity,
)
-from homeassistant.const import CONF_NAME, CONF_TYPE
+from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_TYPE
import homeassistant.helpers.config_validation as cv
from . import (
CONF_ALIASES,
CONF_AUTOMATIC_ADD,
CONF_DEVICE_DEFAULTS,
- CONF_DEVICES,
CONF_FIRE_EVENT,
CONF_GROUP,
CONF_GROUP_ALIASES,
@@ -28,6 +28,7 @@
EVENT_KEY_ID,
SwitchableRflinkDevice,
)
+from .utils import brightness_to_rflink, rflink_to_brightness
_LOGGER = logging.getLogger(__name__)
@@ -184,30 +185,39 @@ async def async_turn_on(self, **kwargs):
"""Turn the device on."""
if ATTR_BRIGHTNESS in kwargs:
# rflink only support 16 brightness levels
- self._brightness = int(kwargs[ATTR_BRIGHTNESS] / 17) * 17
+ self._brightness = rflink_to_brightness(
+ brightness_to_rflink(kwargs[ATTR_BRIGHTNESS])
+ )
# Turn on light at the requested dim level
await self._async_handle_command("dim", self._brightness)
+ def _handle_event(self, event):
+ """Adjust state if Rflink picks up a remote command for this device."""
+ self.cancel_queued_send_commands()
+
+ command = event["command"]
+ if command in ["on", "allon"]:
+ self._state = True
+ elif command in ["off", "alloff"]:
+ self._state = False
+ # dimmable device accept 'set_level=(0-15)' commands
+ elif re.search("^set_level=(0?[0-9]|1[0-5])$", command, re.IGNORECASE):
+ self._brightness = rflink_to_brightness(int(command.split("=")[1]))
+ self._state = True
+
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._brightness
- @property
- def device_state_attributes(self):
- """Return the device state attributes."""
- if self._brightness is None:
- return {}
- return {ATTR_BRIGHTNESS: self._brightness}
-
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_BRIGHTNESS
-class HybridRflinkLight(SwitchableRflinkDevice, LightEntity):
+class HybridRflinkLight(DimmableRflinkLight, LightEntity):
"""Rflink light device that sends out both dim and on/off commands.
Used for protocols which support lights that are not exclusively on/off
@@ -222,52 +232,14 @@ class HybridRflinkLight(SwitchableRflinkDevice, LightEntity):
Which results in a nice house disco :)
"""
- _brightness = 255
-
- async def async_added_to_hass(self):
- """Restore RFLink light brightness attribute."""
- await super().async_added_to_hass()
-
- old_state = await self.async_get_last_state()
- if (
- old_state is not None
- and old_state.attributes.get(ATTR_BRIGHTNESS) is not None
- ):
- # restore also brightness in dimmables devices
- self._brightness = int(old_state.attributes[ATTR_BRIGHTNESS])
-
async def async_turn_on(self, **kwargs):
"""Turn the device on and set dim level."""
- if ATTR_BRIGHTNESS in kwargs:
- # rflink only support 16 brightness levels
- self._brightness = int(kwargs[ATTR_BRIGHTNESS] / 17) * 17
-
- # if receiver supports dimming this will turn on the light
- # at the requested dim level
- await self._async_handle_command("dim", self._brightness)
-
+ await super().async_turn_on(**kwargs)
# if the receiving device does not support dimlevel this
# will ensure it is turned on when full brightness is set
- if self._brightness == 255:
+ if self.brightness == 255:
await self._async_handle_command("turn_on")
- @property
- def brightness(self):
- """Return the brightness of this light between 0..255."""
- return self._brightness
-
- @property
- def device_state_attributes(self):
- """Return the device state attributes."""
- if self._brightness is None:
- return {}
- return {ATTR_BRIGHTNESS: self._brightness}
-
- @property
- def supported_features(self):
- """Flag supported features."""
- return SUPPORT_BRIGHTNESS
-
class ToggleRflinkLight(SwitchableRflinkDevice, LightEntity):
"""Rflink light device which sends out only 'on' commands.
diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json
index cdcfe97c21918d..ebd1fb5afdca52 100644
--- a/homeassistant/components/rflink/manifest.json
+++ b/homeassistant/components/rflink/manifest.json
@@ -2,6 +2,8 @@
"domain": "rflink",
"name": "RFLink",
"documentation": "https://www.home-assistant.io/integrations/rflink",
- "requirements": ["rflink==0.0.55"],
- "codeowners": []
+ "requirements": ["rflink==0.0.58"],
+ "codeowners": [
+ "@javicalle"
+ ]
}
diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py
index 2c27477e6c6440..497c9b8cee615c 100644
--- a/homeassistant/components/rflink/sensor.py
+++ b/homeassistant/components/rflink/sensor.py
@@ -2,10 +2,12 @@
from rflink.parser import PACKET_FIELDS, UNITS
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
+ CONF_DEVICES,
CONF_NAME,
+ CONF_SENSOR_TYPE,
CONF_UNIT_OF_MEASUREMENT,
)
import homeassistant.helpers.config_validation as cv
@@ -14,7 +16,6 @@
from . import (
CONF_ALIASES,
CONF_AUTOMATIC_ADD,
- CONF_DEVICES,
DATA_DEVICE_REGISTER,
DATA_ENTITY_LOOKUP,
EVENT_KEY_ID,
@@ -32,8 +33,6 @@
"temperature": "mdi:thermometer",
}
-CONF_SENSOR_TYPE = "sensor_type"
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_AUTOMATIC_ADD, default=True): cv.boolean,
@@ -99,7 +98,7 @@ async def add_new_device(event):
hass.data[DATA_DEVICE_REGISTER][EVENT_KEY_SENSOR] = add_new_device
-class RflinkSensor(RflinkDevice):
+class RflinkSensor(RflinkDevice, SensorEntity):
"""Representation of a Rflink sensor."""
def __init__(
diff --git a/homeassistant/components/rflink/switch.py b/homeassistant/components/rflink/switch.py
index 77e1f821bad087..8f84286a616caf 100644
--- a/homeassistant/components/rflink/switch.py
+++ b/homeassistant/components/rflink/switch.py
@@ -2,13 +2,12 @@
import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
-from homeassistant.const import CONF_NAME
+from homeassistant.const import CONF_DEVICES, CONF_NAME
import homeassistant.helpers.config_validation as cv
from . import (
CONF_ALIASES,
CONF_DEVICE_DEFAULTS,
- CONF_DEVICES,
CONF_FIRE_EVENT,
CONF_GROUP,
CONF_GROUP_ALIASES,
diff --git a/homeassistant/components/rflink/utils.py b/homeassistant/components/rflink/utils.py
new file mode 100644
index 00000000000000..9738d9f74facb1
--- /dev/null
+++ b/homeassistant/components/rflink/utils.py
@@ -0,0 +1,11 @@
+"""RFLink integration utils."""
+
+
+def brightness_to_rflink(brightness: int) -> int:
+ """Convert 0-255 brightness to RFLink dim level (0-15)."""
+ return int(brightness / 17)
+
+
+def rflink_to_brightness(dim_level: int) -> int:
+ """Convert RFLink dim level (0-15) to 0-255 brightness."""
+ return int(dim_level * 17)
diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py
index 067ffeb5313567..d23a3e4e6ffb06 100644
--- a/homeassistant/components/rfxtrx/__init__.py
+++ b/homeassistant/components/rfxtrx/__init__.py
@@ -3,6 +3,7 @@
import binascii
from collections import OrderedDict
import copy
+import functools
import logging
import RFXtrx as rfxtrxmod
@@ -150,7 +151,7 @@ def _ensure_device(value):
extra=vol.ALLOW_EXTRA,
)
-DOMAINS = ["switch", "sensor", "light", "binary_sensor", "cover"]
+PLATFORMS = ["switch", "sensor", "light", "binary_sensor", "cover"]
async def async_setup(hass, config):
@@ -201,9 +202,9 @@ async def async_setup_entry(hass, entry: config_entries.ConfigEntry):
)
return False
- for domain in DOMAINS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, domain)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -214,8 +215,8 @@ async def async_unload_entry(hass, entry: config_entries.ConfigEntry):
if not all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in DOMAINS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
):
@@ -427,7 +428,7 @@ def find_possible_pt2262_device(device_ids, device_id):
if size is not None:
size = len(dev_id) - size - 1
_LOGGER.info(
- "rfxtrx: found possible device %s for %s "
+ "Found possible device %s for %s "
"with the following configuration:\n"
"data_bits=%d\n"
"command_on=0x%s\n"
@@ -488,7 +489,8 @@ async def async_added_to_hass(self):
self.async_on_remove(
self.hass.helpers.dispatcher.async_dispatcher_connect(
- f"{DOMAIN}_{CONF_REMOVE_DEVICE}_{self._device_id}", self.async_remove
+ f"{DOMAIN}_{CONF_REMOVE_DEVICE}_{self._device_id}",
+ functools.partial(self.async_remove, force_remove=True),
)
)
@@ -503,7 +505,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
if not self._event:
return None
diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py
index 5eeb9b38411ff4..da4d6447e76cb1 100644
--- a/homeassistant/components/rfxtrx/config_flow.py
+++ b/homeassistant/components/rfxtrx/config_flow.py
@@ -344,7 +344,9 @@ async def _async_replace_device(self, replace_device):
new_device_id = "_".join(x for x in new_device_data[CONF_DEVICE_ID])
entity_registry = await async_get_entity_registry(self.hass)
- entity_entries = async_entries_for_device(entity_registry, old_device)
+ entity_entries = async_entries_for_device(
+ entity_registry, old_device, include_disabled_entities=True
+ )
entity_migration_map = {}
for entry in entity_entries:
unique_id = entry.unique_id
diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py
index c897e164119667..72cd9f6bbf604f 100644
--- a/homeassistant/components/rfxtrx/sensor.py
+++ b/homeassistant/components/rfxtrx/sensor.py
@@ -8,6 +8,7 @@
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_TEMPERATURE,
+ SensorEntity,
)
from homeassistant.const import (
CONF_DEVICES,
@@ -129,7 +130,7 @@ def sensor_update(event, device_id):
connect_auto_add(hass, discovery_info, sensor_update)
-class RfxtrxSensor(RfxtrxEntity):
+class RfxtrxSensor(RfxtrxEntity, SensorEntity):
"""Representation of a RFXtrx sensor."""
def __init__(self, device, device_id, data_type, event=None):
diff --git a/homeassistant/components/rfxtrx/translations/ca.json b/homeassistant/components/rfxtrx/translations/ca.json
index 6c4e920df02e52..d7db4107e3bb2e 100644
--- a/homeassistant/components/rfxtrx/translations/ca.json
+++ b/homeassistant/components/rfxtrx/translations/ca.json
@@ -64,7 +64,8 @@
"off_delay": "Retard OFF",
"off_delay_enabled": "Activa el retard OFF",
"replace_device": "Selecciona el dispositiu a substituir",
- "signal_repetitions": "Nombre de repeticions del senyal"
+ "signal_repetitions": "Nombre de repeticions del senyal",
+ "venetian_blind_mode": "Mode persiana veneciana"
},
"title": "Configuraci\u00f3 de les opcions del dispositiu"
}
diff --git a/homeassistant/components/rfxtrx/translations/de.json b/homeassistant/components/rfxtrx/translations/de.json
index 1979a10cb8a6fd..0f3837f3a595de 100644
--- a/homeassistant/components/rfxtrx/translations/de.json
+++ b/homeassistant/components/rfxtrx/translations/de.json
@@ -2,13 +2,17 @@
"config": {
"abort": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert. Nur eine Konfiguration m\u00f6glich.",
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"step": {
"setup_network": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ },
"title": "Verbindungsadresse ausw\u00e4hlen"
},
"setup_serial": {
@@ -18,6 +22,9 @@
"title": "Ger\u00e4t"
},
"setup_serial_manual_path": {
+ "data": {
+ "device": "USB-Ger\u00e4te-Pfad"
+ },
"title": "Pfad"
},
"user": {
@@ -30,12 +37,34 @@
},
"options": {
"error": {
+ "already_configured_device": "Ger\u00e4t ist bereits konfiguriert",
+ "invalid_event_code": "Ung\u00fcltiger Ereigniscode",
+ "invalid_input_2262_off": "Ung\u00fcltige Eingabe f\u00fcr Befehl \"aus\"",
+ "invalid_input_2262_on": "Ung\u00fcltige Eingabe f\u00fcr Befehl \"an\"",
+ "invalid_input_off_delay": "Ung\u00fcltige Eingabe f\u00fcr Ausschaltverz\u00f6gerung",
"unknown": "Unerwarteter Fehler"
},
"step": {
"prompt_options": {
"data": {
- "debug": "Debugging aktivieren"
+ "automatic_add": "Automatisches Hinzuf\u00fcgen aktivieren",
+ "debug": "Debugging aktivieren",
+ "device": "Zu konfigurierendes Ger\u00e4t ausw\u00e4hlen",
+ "remove_device": "Zu l\u00f6schendes Ger\u00e4t ausw\u00e4hlen"
+ },
+ "title": "Rfxtrx Optionen"
+ },
+ "set_device_options": {
+ "data": {
+ "command_off": "Datenbitwert f\u00fcr den Befehl \"aus\"",
+ "command_on": "Datenbitwert f\u00fcr den Befehl \"ein\"",
+ "data_bit": "Anzahl der Datenbits",
+ "fire_event": "Ger\u00e4teereignis aktivieren",
+ "off_delay": "Ausschaltverz\u00f6gerung",
+ "off_delay_enabled": "Ausschaltverz\u00f6gerung aktivieren",
+ "replace_device": "W\u00e4hle ein Ger\u00e4t aus, das ersetzt werden soll",
+ "signal_repetitions": "Anzahl der Signalwiederholungen",
+ "venetian_blind_mode": "Jalousie-Modus"
}
}
}
diff --git a/homeassistant/components/rfxtrx/translations/en.json b/homeassistant/components/rfxtrx/translations/en.json
index 2d73ac568100c0..5e3f551e0cf95c 100644
--- a/homeassistant/components/rfxtrx/translations/en.json
+++ b/homeassistant/components/rfxtrx/translations/en.json
@@ -64,8 +64,8 @@
"off_delay": "Off delay",
"off_delay_enabled": "Enable off delay",
"replace_device": "Select device to replace",
- "venetian_blind_mode": "Venetian blind mode (tilt by: US - long press, EU - short press)",
- "signal_repetitions": "Number of signal repetitions"
+ "signal_repetitions": "Number of signal repetitions",
+ "venetian_blind_mode": "Venetian blind mode"
},
"title": "Configure device options"
}
diff --git a/homeassistant/components/rfxtrx/translations/es.json b/homeassistant/components/rfxtrx/translations/es.json
index 86bad8f096f698..c1c4d72735cdf4 100644
--- a/homeassistant/components/rfxtrx/translations/es.json
+++ b/homeassistant/components/rfxtrx/translations/es.json
@@ -64,7 +64,8 @@
"off_delay": "Retraso de apagado",
"off_delay_enabled": "Activar retardo de apagado",
"replace_device": "Seleccione el dispositivo que desea reemplazar",
- "signal_repetitions": "N\u00famero de repeticiones de la se\u00f1al"
+ "signal_repetitions": "N\u00famero de repeticiones de la se\u00f1al",
+ "venetian_blind_mode": "Modo de persiana veneciana"
},
"title": "Configurar las opciones del dispositivo"
}
diff --git a/homeassistant/components/rfxtrx/translations/et.json b/homeassistant/components/rfxtrx/translations/et.json
index 1ade1f112c2b48..662664b445480e 100644
--- a/homeassistant/components/rfxtrx/translations/et.json
+++ b/homeassistant/components/rfxtrx/translations/et.json
@@ -64,7 +64,8 @@
"off_delay": "V\u00e4ljal\u00fclitamise viivitus",
"off_delay_enabled": "Luba v\u00e4ljal\u00fclitusviivitus",
"replace_device": "Vali asendav seade",
- "signal_repetitions": "Signaali korduste arv"
+ "signal_repetitions": "Signaali korduste arv",
+ "venetian_blind_mode": "Ribikardinate juhtimine"
},
"title": "Seadista seadme valikud"
}
diff --git a/homeassistant/components/rfxtrx/translations/fr.json b/homeassistant/components/rfxtrx/translations/fr.json
index baf8d0f5148678..8794b3913f190c 100644
--- a/homeassistant/components/rfxtrx/translations/fr.json
+++ b/homeassistant/components/rfxtrx/translations/fr.json
@@ -5,9 +5,13 @@
"cannot_connect": "\u00c9chec de connexion"
},
"error": {
- "cannot_connect": "\u00c9chec de connexion"
+ "cannot_connect": "\u00c9chec de connexion",
+ "one": "Vide",
+ "other": "Vide"
},
"step": {
+ "one": "Vide",
+ "other": "Vide",
"setup_network": {
"data": {
"host": "H\u00f4te",
@@ -35,6 +39,7 @@
}
}
},
+ "one": "Vide",
"options": {
"error": {
"already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
@@ -64,10 +69,12 @@
"off_delay": "D\u00e9lai d'arr\u00eat",
"off_delay_enabled": "Activer le d\u00e9lai d'arr\u00eat",
"replace_device": "S\u00e9lectionnez l'appareil \u00e0 remplacer",
- "signal_repetitions": "Nombre de r\u00e9p\u00e9titions du signal"
+ "signal_repetitions": "Nombre de r\u00e9p\u00e9titions du signal",
+ "venetian_blind_mode": "Mode store v\u00e9nitien"
},
"title": "Configurer les options de l'appareil"
}
}
- }
+ },
+ "other": "Vide"
}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json
index 4e04ab16ce2c60..20ef3db6171884 100644
--- a/homeassistant/components/rfxtrx/translations/hu.json
+++ b/homeassistant/components/rfxtrx/translations/hu.json
@@ -1,9 +1,19 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
"step": {
+ "setup_network": {
+ "data": {
+ "host": "Hoszt",
+ "port": "Port"
+ }
+ },
"setup_serial": {
"data": {
"device": "Eszk\u00f6z kiv\u00e1laszt\u00e1sa"
@@ -11,13 +21,18 @@
"title": "Eszk\u00f6z"
},
"setup_serial_manual_path": {
+ "data": {
+ "device": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat"
+ },
"title": "El\u00e9r\u00e9si \u00fat"
}
}
},
"options": {
"error": {
- "invalid_event_code": "\u00c9rv\u00e9nytelen esem\u00e9nyk\u00f3d"
+ "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "invalid_event_code": "\u00c9rv\u00e9nytelen esem\u00e9nyk\u00f3d",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"step": {
"prompt_options": {
diff --git a/homeassistant/components/rfxtrx/translations/id.json b/homeassistant/components/rfxtrx/translations/id.json
new file mode 100644
index 00000000000000..9836d252c6818c
--- /dev/null
+++ b/homeassistant/components/rfxtrx/translations/id.json
@@ -0,0 +1,73 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.",
+ "cannot_connect": "Gagal terhubung"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ },
+ "title": "Pilih alamat koneksi"
+ },
+ "setup_serial": {
+ "data": {
+ "device": "Pilih perangkat"
+ },
+ "title": "Perangkat"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "device": "Jalur Perangkat USB"
+ },
+ "title": "Jalur"
+ },
+ "user": {
+ "data": {
+ "type": "Jenis koneksi"
+ },
+ "title": "Pilih jenis koneksi"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "already_configured_device": "Perangkat sudah dikonfigurasi",
+ "invalid_event_code": "Kode event tidak valid",
+ "invalid_input_2262_off": "Masukan tidak valid untuk perintah mematikan",
+ "invalid_input_2262_on": "Masukan tidak valid untuk perintah menyalakan",
+ "invalid_input_off_delay": "Input tidak valid untuk penundaan mematikan",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "prompt_options": {
+ "data": {
+ "automatic_add": "Aktifkan penambahan otomatis",
+ "debug": "Aktifkan debugging",
+ "device": "Pilih perangkat untuk dikonfigurasi",
+ "event_code": "Masukkan kode event untuk ditambahkan",
+ "remove_device": "Pilih perangkat yang akan dihapus"
+ },
+ "title": "Opsi Rfxtrx"
+ },
+ "set_device_options": {
+ "data": {
+ "command_off": "Nilai bit data untuk perintah mematikan",
+ "command_on": "Nilai bit data untuk perintah menyalakan",
+ "data_bit": "Jumlah bit data",
+ "fire_event": "Aktifkan event perangkat",
+ "off_delay": "Penundaan mematikan",
+ "off_delay_enabled": "Aktifkan penundaan mematikan",
+ "replace_device": "Pilih perangkat yang akan diganti",
+ "signal_repetitions": "Jumlah pengulangan sinyal"
+ },
+ "title": "Konfigurasi opsi perangkat"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/it.json b/homeassistant/components/rfxtrx/translations/it.json
index ff705fdd0a2285..938c471e992a63 100644
--- a/homeassistant/components/rfxtrx/translations/it.json
+++ b/homeassistant/components/rfxtrx/translations/it.json
@@ -64,7 +64,8 @@
"off_delay": "Ritardo di spegnimento",
"off_delay_enabled": "Attivare il ritardo di spegnimento",
"replace_device": "Selezionare il dispositivo da sostituire",
- "signal_repetitions": "Numero di ripetizioni del segnale"
+ "signal_repetitions": "Numero di ripetizioni del segnale",
+ "venetian_blind_mode": "Modalit\u00e0 veneziana"
},
"title": "Configurare le opzioni del dispositivo"
}
diff --git a/homeassistant/components/rfxtrx/translations/ko.json b/homeassistant/components/rfxtrx/translations/ko.json
index aa8512da2850af..891926083dd479 100644
--- a/homeassistant/components/rfxtrx/translations/ko.json
+++ b/homeassistant/components/rfxtrx/translations/ko.json
@@ -1,7 +1,74 @@
{
"config": {
"abort": {
- "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "port": "\ud3ec\ud2b8"
+ },
+ "title": "\uc5f0\uacb0 \uc8fc\uc18c \uc120\ud0dd\ud558\uae30"
+ },
+ "setup_serial": {
+ "data": {
+ "device": "\uae30\uae30 \uc120\ud0dd\ud558\uae30"
+ },
+ "title": "\uae30\uae30"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "device": "USB \uc7a5\uce58 \uacbd\ub85c"
+ },
+ "title": "\uacbd\ub85c"
+ },
+ "user": {
+ "data": {
+ "type": "\uc5f0\uacb0 \uc720\ud615"
+ },
+ "title": "\uc5f0\uacb0 \uc720\ud615 \uc120\ud0dd\ud558\uae30"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_event_code": "\uc774\ubca4\ud2b8 \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_input_2262_off": "\ub044\uae30 \uba85\ub839\uc5d0 \ub300\ud55c \uc785\ub825\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_input_2262_on": "\ucf1c\uae30 \uba85\ub839\uc5d0 \ub300\ud55c \uc785\ub825\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_input_off_delay": "\uc790\ub3d9 \uaebc\uc9c4 \uc0c1\ud0dc\uc5d0 \ub300\ud55c \uc785\ub825\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "prompt_options": {
+ "data": {
+ "automatic_add": "\uc790\ub3d9 \ucd94\uac00 \ud65c\uc131\ud654\ud558\uae30",
+ "debug": "\ub514\ubc84\uae45 \ud65c\uc131\ud654\ud558\uae30",
+ "device": "\uad6c\uc131\ud560 \uae30\uae30 \uc120\ud0dd\ud558\uae30",
+ "event_code": "\ucd94\uac00\ud560 \uc774\ubca4\ud2b8 \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694",
+ "remove_device": "\uc0ad\uc81c\ud560 \uae30\uae30 \uc120\ud0dd\ud558\uae30"
+ },
+ "title": "Rfxtrx \uc635\uc158"
+ },
+ "set_device_options": {
+ "data": {
+ "command_off": "\ub044\uae30 \uba85\ub839\uc5d0 \ub300\ud55c \ub370\uc774\ud130 \ube44\ud2b8 \uac12",
+ "command_on": "\ucf1c\uae30 \uba85\ub839\uc5d0 \ub300\ud55c \ub370\uc774\ud130 \ube44\ud2b8 \uac12",
+ "data_bit": "\ub370\uc774\ud130 \ube44\ud2b8 \uc218",
+ "fire_event": "\uae30\uae30 \uc774\ubca4\ud2b8 \ud65c\uc131\ud654\ud558\uae30",
+ "off_delay": "\uc790\ub3d9 \uaebc\uc9c4 \uc0c1\ud0dc",
+ "off_delay_enabled": "\uc790\ub3d9 \uaebc\uc9c4 \uc0c1\ud0dc(Off Delay) \ud65c\uc131\ud654\ud558\uae30",
+ "replace_device": "\uad50\uccb4\ud560 \uae30\uae30 \uc120\ud0dd\ud558\uae30",
+ "signal_repetitions": "\uc2e0\ud638 \ubc18\ubcf5 \ud69f\uc218",
+ "venetian_blind_mode": "\ubca0\ub124\uc2dc\uc548 \ube14\ub77c\uc778\ub4dc \ubaa8\ub4dc"
+ },
+ "title": "\uae30\uae30 \uc635\uc158 \uad6c\uc131\ud558\uae30"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/nl.json b/homeassistant/components/rfxtrx/translations/nl.json
index 0dc56206f668f8..1d22751ceed7ff 100644
--- a/homeassistant/components/rfxtrx/translations/nl.json
+++ b/homeassistant/components/rfxtrx/translations/nl.json
@@ -10,10 +10,23 @@
"step": {
"setup_network": {
"data": {
+ "host": "Host",
"port": "Poort"
},
"title": "Selecteer verbindingsadres"
},
+ "setup_serial": {
+ "data": {
+ "device": "Selecteer apparaat"
+ },
+ "title": "Apparaat"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "device": "USB-apparaatpad"
+ },
+ "title": "Pad"
+ },
"user": {
"data": {
"type": "Verbindingstype"
@@ -25,7 +38,37 @@
"options": {
"error": {
"already_configured_device": "Apparaat is al geconfigureerd",
+ "invalid_event_code": "Ongeldige gebeurteniscode",
+ "invalid_input_2262_off": "Ongeldige invoer voor commando uit",
+ "invalid_input_2262_on": "Ongeldige invoer voor commando aan",
+ "invalid_input_off_delay": "Ongeldige invoer voor uitschakelvertraging",
"unknown": "Onverwachte fout"
+ },
+ "step": {
+ "prompt_options": {
+ "data": {
+ "automatic_add": "Schakel automatisch toevoegen in",
+ "debug": "Foutopsporing inschakelen",
+ "device": "Selecteer het apparaat om te configureren",
+ "event_code": "Voer de gebeurteniscode in om toe te voegen",
+ "remove_device": "Apparaat selecteren dat u wilt verwijderen"
+ },
+ "title": "Rfxtrx-opties"
+ },
+ "set_device_options": {
+ "data": {
+ "command_off": "Waarde gegevensbits voor commando uit",
+ "command_on": "Waarde gegevensbits voor commando aan",
+ "data_bit": "Aantal databits",
+ "fire_event": "Schakel apparaatgebeurtenis in",
+ "off_delay": "Uitschakelvertraging",
+ "off_delay_enabled": "Schakel uitschakelvertraging in",
+ "replace_device": "Selecteer apparaat dat u wilt vervangen",
+ "signal_repetitions": "Aantal signaalherhalingen",
+ "venetian_blind_mode": "Venetiaanse jaloezie modus"
+ },
+ "title": "Configureer apparaatopties"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/no.json b/homeassistant/components/rfxtrx/translations/no.json
index 752136dac7fe70..3eb9c9b83df52b 100644
--- a/homeassistant/components/rfxtrx/translations/no.json
+++ b/homeassistant/components/rfxtrx/translations/no.json
@@ -64,7 +64,8 @@
"off_delay": "Av forsinkelse",
"off_delay_enabled": "Aktiver av forsinkelse",
"replace_device": "Velg enheten du vil erstatte",
- "signal_repetitions": "Antall signalrepetisjoner"
+ "signal_repetitions": "Antall signalrepetisjoner",
+ "venetian_blind_mode": "Persiennemodus"
},
"title": "Konfigurer enhetsalternativer"
}
diff --git a/homeassistant/components/rfxtrx/translations/pl.json b/homeassistant/components/rfxtrx/translations/pl.json
index bf17d6c5166392..e0e69b2a64a037 100644
--- a/homeassistant/components/rfxtrx/translations/pl.json
+++ b/homeassistant/components/rfxtrx/translations/pl.json
@@ -75,7 +75,8 @@
"off_delay": "Op\u00f3\u017anienie stanu \"off\"",
"off_delay_enabled": "W\u0142\u0105cz op\u00f3\u017anienie stanu \"off\"",
"replace_device": "Wybierz urz\u0105dzenie do zast\u0105pienia",
- "signal_repetitions": "Liczba powt\u00f3rze\u0144 sygna\u0142u"
+ "signal_repetitions": "Liczba powt\u00f3rze\u0144 sygna\u0142u",
+ "venetian_blind_mode": "Tryb \u017caluzji weneckich"
},
"title": "Konfiguracja opcji urz\u0105dzenia"
}
diff --git a/homeassistant/components/rfxtrx/translations/ru.json b/homeassistant/components/rfxtrx/translations/ru.json
index 361b051ac2ce4d..5a635766d3f379 100644
--- a/homeassistant/components/rfxtrx/translations/ru.json
+++ b/homeassistant/components/rfxtrx/translations/ru.json
@@ -64,7 +64,8 @@
"off_delay": "\u0417\u0430\u0434\u0435\u0440\u0436\u043a\u0430 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f",
"off_delay_enabled": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0437\u0430\u0434\u0435\u0440\u0436\u043a\u0443 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f",
"replace_device": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u0437\u0430\u043c\u0435\u043d\u044b",
- "signal_repetitions": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u043e\u0432 \u0441\u0438\u0433\u043d\u0430\u043b\u0430"
+ "signal_repetitions": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u043e\u0432 \u0441\u0438\u0433\u043d\u0430\u043b\u0430",
+ "venetian_blind_mode": "\u0420\u0435\u0436\u0438\u043c \u0432\u0435\u043d\u0435\u0446\u0438\u0430\u043d\u0441\u043a\u0438\u0445 \u0436\u0430\u043b\u044e\u0437\u0438"
},
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430"
}
diff --git a/homeassistant/components/rfxtrx/translations/tr.json b/homeassistant/components/rfxtrx/translations/tr.json
new file mode 100644
index 00000000000000..1c3ad8b9e05f86
--- /dev/null
+++ b/homeassistant/components/rfxtrx/translations/tr.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.",
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "set_device_options": {
+ "data": {
+ "venetian_blind_mode": "Jaluzi modu"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/uk.json b/homeassistant/components/rfxtrx/translations/uk.json
new file mode 100644
index 00000000000000..1b0938b8b70887
--- /dev/null
+++ b/homeassistant/components/rfxtrx/translations/uk.json
@@ -0,0 +1,74 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "setup_network": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f"
+ },
+ "setup_serial": {
+ "data": {
+ "device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439"
+ },
+ "title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439"
+ },
+ "setup_serial_manual_path": {
+ "data": {
+ "device": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ },
+ "title": "\u0428\u043b\u044f\u0445"
+ },
+ "user": {
+ "data": {
+ "type": "\u0422\u0438\u043f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f"
+ },
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "already_configured_device": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "invalid_event_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434 \u043f\u043e\u0434\u0456\u0457.",
+ "invalid_input_2262_off": "\u041d\u0435\u0432\u0456\u0440\u043d\u0456 \u0434\u0430\u043d\u0456 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0438 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f.",
+ "invalid_input_2262_on": "\u041d\u0435\u0432\u0456\u0440\u043d\u0456 \u0434\u0430\u043d\u0456 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0438 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f.",
+ "invalid_input_off_delay": "\u041d\u0435\u0432\u0456\u0440\u043d\u0456 \u0434\u0430\u043d\u0456 \u0434\u043b\u044f \u0437\u0430\u0442\u0440\u0438\u043c\u043a\u0438 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "prompt_options": {
+ "data": {
+ "automatic_add": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f",
+ "debug": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u043d\u0430\u043b\u0430\u0433\u043e\u0434\u0436\u0435\u043d\u043d\u044f",
+ "device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f",
+ "event_code": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0456\u0457",
+ "remove_device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0434\u043b\u044f \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043d\u044f"
+ },
+ "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438"
+ },
+ "set_device_options": {
+ "data": {
+ "command_off": "\u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0431\u0456\u0442\u0456\u0432 \u0434\u0430\u043d\u0438\u0445 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0438 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f",
+ "command_on": "\u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0431\u0456\u0442\u0456\u0432 \u0434\u0430\u043d\u0438\u0445 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0438 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f",
+ "data_bit": "\u041a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0431\u0456\u0442\u0456\u0432 \u0434\u0430\u043d\u0438\u0445",
+ "fire_event": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u043f\u043e\u0434\u0456\u0457 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e",
+ "off_delay": "\u0417\u0430\u0442\u0440\u0438\u043c\u043a\u0430 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f",
+ "off_delay_enabled": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0437\u0430\u0442\u0440\u0438\u043c\u043a\u0443 \u0432\u0438\u043c\u0438\u043a\u0430\u043d\u043d\u044f",
+ "replace_device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0434\u043b\u044f \u0437\u0430\u043c\u0456\u043d\u0438",
+ "signal_repetitions": "\u041a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0432 \u0441\u0438\u0433\u043d\u0430\u043b\u0443",
+ "venetian_blind_mode": "\u0420\u0435\u0436\u0438\u043c \u0436\u0430\u043b\u044e\u0437\u0456"
+ },
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/zh-Hant.json b/homeassistant/components/rfxtrx/translations/zh-Hant.json
index 3da2e5f5384221..24e5ee56d76103 100644
--- a/homeassistant/components/rfxtrx/translations/zh-Hant.json
+++ b/homeassistant/components/rfxtrx/translations/zh-Hant.json
@@ -64,7 +64,8 @@
"off_delay": "\u5ef6\u9072",
"off_delay_enabled": "\u958b\u555f\u5ef6\u9072",
"replace_device": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u53d6\u4ee3",
- "signal_repetitions": "\u8a0a\u865f\u91cd\u8907\u6b21\u6578"
+ "signal_repetitions": "\u8a0a\u865f\u91cd\u8907\u6b21\u6578",
+ "venetian_blind_mode": "\u767e\u8449\u7a97\u6a21\u5f0f"
},
"title": "\u8a2d\u5b9a\u88dd\u7f6e\u9078\u9805"
}
diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py
index ed22575bcccdde..f5211ac54c03c4 100644
--- a/homeassistant/components/ring/__init__.py
+++ b/homeassistant/components/ring/__init__.py
@@ -1,10 +1,11 @@
"""Support for Ring Doorbell/Chimes."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
from functools import partial
import logging
from pathlib import Path
-from typing import Optional
from oauthlib.oauth2 import AccessDeniedError
import requests
@@ -99,9 +100,9 @@ def token_updater(token):
),
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
if hass.services.has_service(DOMAIN, "update"):
@@ -126,8 +127,8 @@ async def async_unload_entry(hass, entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -187,7 +188,7 @@ def async_remove_listener(self, update_callback):
self._unsub_interval()
self._unsub_interval = None
- async def async_refresh_all(self, _now: Optional[int] = None) -> None:
+ async def async_refresh_all(self, _now: int | None = None) -> None:
"""Time to update."""
if not self.listeners:
return
diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py
index bbfbbf1690e0d8..18ce87e722e0d0 100644
--- a/homeassistant/components/ring/binary_sensor.py
+++ b/homeassistant/components/ring/binary_sensor.py
@@ -111,9 +111,9 @@ def unique_id(self):
return self._unique_id
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
- attrs = super().device_state_attributes
+ attrs = super().extra_state_attributes
if self._active_alert is None:
return attrs
diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py
index bd5950b81a9fa6..8f827aee7d28ee 100644
--- a/homeassistant/components/ring/camera.py
+++ b/homeassistant/components/ring/camera.py
@@ -93,7 +93,7 @@ def unique_id(self):
return self._device.id
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py
index 87b2026882c188..a23a08b2a54099 100644
--- a/homeassistant/components/ring/config_flow.py
+++ b/homeassistant/components/ring/config_flow.py
@@ -7,7 +7,7 @@
from homeassistant import config_entries, const, core, exceptions
-from . import DOMAIN # pylint: disable=unused-import
+from . import DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/ring/entity.py b/homeassistant/components/ring/entity.py
index 6eb87cb8f9b023..7a1c8ae7bdfaeb 100644
--- a/homeassistant/components/ring/entity.py
+++ b/homeassistant/components/ring/entity.py
@@ -38,7 +38,7 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py
index 0a1cc85230f9fe..a2b9e2300dc759 100644
--- a/homeassistant/components/ring/sensor.py
+++ b/homeassistant/components/ring/sensor.py
@@ -1,7 +1,11 @@
"""This component provides HA sensor support for Ring Door Bell/Chimes."""
-from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.const import (
+ DEVICE_CLASS_TIMESTAMP,
+ PERCENTAGE,
+ SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
+)
from homeassistant.core import callback
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level
from . import DOMAIN
@@ -32,7 +36,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors)
-class RingSensor(RingEntityMixin, Entity):
+class RingSensor(RingEntityMixin, SensorEntity):
"""A sensor implementation for Ring device."""
def __init__(self, config_entry_id, device, sensor_type):
@@ -180,9 +184,9 @@ def state(self):
return self._latest_event["created_at"].isoformat()
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
- attrs = super().device_state_attributes
+ attrs = super().extra_state_attributes
if self._latest_event:
attrs["created_at"] = self._latest_event["created_at"]
@@ -210,7 +214,7 @@ def device_state_attributes(self):
None,
"history",
None,
- "timestamp",
+ DEVICE_CLASS_TIMESTAMP,
HistoryRingSensor,
],
"last_ding": [
@@ -219,7 +223,7 @@ def device_state_attributes(self):
None,
"history",
"ding",
- "timestamp",
+ DEVICE_CLASS_TIMESTAMP,
HistoryRingSensor,
],
"last_motion": [
@@ -228,7 +232,7 @@ def device_state_attributes(self):
None,
"history",
"motion",
- "timestamp",
+ DEVICE_CLASS_TIMESTAMP,
HistoryRingSensor,
],
"volume": [
diff --git a/homeassistant/components/ring/translations/he.json b/homeassistant/components/ring/translations/he.json
new file mode 100644
index 00000000000000..ac90b3264eab33
--- /dev/null
+++ b/homeassistant/components/ring/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ring/translations/hu.json b/homeassistant/components/ring/translations/hu.json
index fba6b944222a3b..8e6e94eb2d63eb 100644
--- a/homeassistant/components/ring/translations/hu.json
+++ b/homeassistant/components/ring/translations/hu.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
},
"error": {
- "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s.",
- "unknown": "V\u00e1ratlan hiba"
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"step": {
"2fa": {
diff --git a/homeassistant/components/ring/translations/id.json b/homeassistant/components/ring/translations/id.json
new file mode 100644
index 00000000000000..198833299384fe
--- /dev/null
+++ b/homeassistant/components/ring/translations/id.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "Kode autentikasi dua faktor"
+ },
+ "title": "Autentikasi dua faktor"
+ },
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "title": "Masuk dengan akun Ring"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ring/translations/ru.json b/homeassistant/components/ring/translations/ru.json
index fb8c22c39af644..bc07db24007d4c 100644
--- a/homeassistant/components/ring/translations/ru.json
+++ b/homeassistant/components/ring/translations/ru.json
@@ -4,7 +4,7 @@
"already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
},
"error": {
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
@@ -17,7 +17,7 @@
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "Ring"
}
diff --git a/homeassistant/components/ring/translations/tr.json b/homeassistant/components/ring/translations/tr.json
new file mode 100644
index 00000000000000..caba385d7fa10f
--- /dev/null
+++ b/homeassistant/components/ring/translations/tr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ring/translations/uk.json b/homeassistant/components/ring/translations/uk.json
new file mode 100644
index 00000000000000..8d40cdf0d23f59
--- /dev/null
+++ b/homeassistant/components/ring/translations/uk.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "\u041a\u043e\u0434 \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ },
+ "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f"
+ },
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "Ring"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ring/translations/zh-Hant.json b/homeassistant/components/ring/translations/zh-Hant.json
index 5eb31bfa7927d4..9f3c91e2a7c490 100644
--- a/homeassistant/components/ring/translations/zh-Hant.json
+++ b/homeassistant/components/ring/translations/zh-Hant.json
@@ -10,9 +10,9 @@
"step": {
"2fa": {
"data": {
- "2fa": "\u96d9\u91cd\u9a57\u8b49\u78bc"
+ "2fa": "\u96d9\u91cd\u8a8d\u8b49\u78bc"
},
- "title": "\u96d9\u91cd\u9a57\u8b49"
+ "title": "\u96d9\u91cd\u8a8d\u8b49"
},
"user": {
"data": {
diff --git a/homeassistant/components/ripple/sensor.py b/homeassistant/components/ripple/sensor.py
index ab0da77b173b04..f36e2c58ec81ca 100644
--- a/homeassistant/components/ripple/sensor.py
+++ b/homeassistant/components/ripple/sensor.py
@@ -4,10 +4,9 @@
from pyripple import get_balance
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
ATTRIBUTION = "Data provided by ripple.com"
@@ -31,7 +30,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([RippleSensor(name, address)], True)
-class RippleSensor(Entity):
+class RippleSensor(SensorEntity):
"""Representation of an Ripple.com sensor."""
def __init__(self, name, address):
@@ -57,7 +56,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py
index 685fee43adfd77..eec30553870f93 100644
--- a/homeassistant/components/risco/__init__.py
+++ b/homeassistant/components/risco/__init__.py
@@ -47,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
scan_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
coordinator = RiscoDataUpdateCoordinator(hass, risco, scan_interval)
- await coordinator.async_refresh()
+ await coordinator.async_config_entry_first_refresh()
events_coordinator = RiscoEventsDataUpdateCoordinator(
hass, risco, entry.entry_id, 60
)
@@ -63,8 +63,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
async def start_platforms():
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_setup(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ for platform in PLATFORMS
]
)
await events_coordinator.async_refresh()
@@ -79,8 +79,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/risco/binary_sensor.py b/homeassistant/components/risco/binary_sensor.py
index ba01b70686b3ba..ba32429c154b02 100644
--- a/homeassistant/components/risco/binary_sensor.py
+++ b/homeassistant/components/risco/binary_sensor.py
@@ -60,7 +60,7 @@ def unique_id(self):
return binary_sensor_unique_id(self._risco, self._zone_id)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {"zone_id": self._zone_id, "bypassed": self._zone.bypassed}
diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py
index 507e3943f2dd0f..76b6105df016fa 100644
--- a/homeassistant/components/risco/config_flow.py
+++ b/homeassistant/components/risco/config_flow.py
@@ -23,9 +23,9 @@
CONF_HA_STATES_TO_RISCO,
CONF_RISCO_STATES_TO_HA,
DEFAULT_OPTIONS,
+ DOMAIN,
RISCO_STATES,
)
-from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/risco/sensor.py b/homeassistant/components/risco/sensor.py
index 43d763a35fa7e2..b39655949b2038 100644
--- a/homeassistant/components/risco/sensor.py
+++ b/homeassistant/components/risco/sensor.py
@@ -1,5 +1,6 @@
"""Sensor for Risco Events."""
from homeassistant.components.binary_sensor import DOMAIN as BS_DOMAIN
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_TIMESTAMP
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -42,7 +43,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors)
-class RiscoSensor(CoordinatorEntity):
+class RiscoSensor(CoordinatorEntity, SensorEntity):
"""Sensor for Risco events."""
def __init__(self, coordinator, category_id, excludes, name, entry_id) -> None:
@@ -94,7 +95,7 @@ def state(self):
return self._event.time
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""State attributes."""
if self._event is None:
return None
diff --git a/homeassistant/components/risco/translations/de.json b/homeassistant/components/risco/translations/de.json
index ad863f7ff792e0..36d808bd6dec77 100644
--- a/homeassistant/components/risco/translations/de.json
+++ b/homeassistant/components/risco/translations/de.json
@@ -1,14 +1,18 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"step": {
"user": {
"data": {
"password": "Passwort",
- "pin": "PIN Code",
+ "pin": "PIN-Code",
"username": "Benutzername"
}
}
@@ -16,6 +20,12 @@
},
"options": {
"step": {
+ "init": {
+ "data": {
+ "code_arm_required": "PIN-Code zum Entsperren vorgeben",
+ "code_disarm_required": "PIN-Code zum Entsperren vorgeben"
+ }
+ },
"risco_to_ha": {
"data": {
"A": "Gruppe A",
diff --git a/homeassistant/components/risco/translations/et.json b/homeassistant/components/risco/translations/et.json
index 9bd35ec22db58f..c57d5d73fecb67 100644
--- a/homeassistant/components/risco/translations/et.json
+++ b/homeassistant/components/risco/translations/et.json
@@ -27,7 +27,7 @@
"armed_home": "Valves kodus",
"armed_night": "Valves \u00f6ine"
},
- "description": "Valige millisesse olekusse l\u00fcltub Risco alarm kui valvestada Home Assistant",
+ "description": "Vali millisesse olekusse l\u00fcltub Risco alarm kui valvestada Home Assistant",
"title": "Lisa Risco olekud Home Assistanti olekutesse"
},
"init": {
diff --git a/homeassistant/components/risco/translations/hu.json b/homeassistant/components/risco/translations/hu.json
index 3b2d79a34a77e2..ee57b9488dc073 100644
--- a/homeassistant/components/risco/translations/hu.json
+++ b/homeassistant/components/risco/translations/hu.json
@@ -2,6 +2,27 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "pin": "PIN-k\u00f3d",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "Be\u00e1ll\u00edt\u00e1sok konfigur\u00e1l\u00e1sa"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/risco/translations/id.json b/homeassistant/components/risco/translations/id.json
new file mode 100644
index 00000000000000..eef1f00ffa6c65
--- /dev/null
+++ b/homeassistant/components/risco/translations/id.json
@@ -0,0 +1,55 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "pin": "Kode PIN",
+ "username": "Nama Pengguna"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "ha_to_risco": {
+ "data": {
+ "armed_away": "Diaktifkan untuk Keluar",
+ "armed_custom_bypass": "Diaktifkan Khusus",
+ "armed_home": "Diaktifkan untuk Di Rumah",
+ "armed_night": "Diaktifkan untuk Malam"
+ },
+ "description": "Pilih status mana yang akan disetel pada Risco ketika mengaktifkan alarm Home Assistant",
+ "title": "Petakan status Home Assistant ke status Risco"
+ },
+ "init": {
+ "data": {
+ "code_arm_required": "Membutuhkan Kode PIN untuk mengaktifkan",
+ "code_disarm_required": "Membutuhkan Kode PIN untuk menonaktifkan",
+ "scan_interval": "Seberapa sering untuk mengambil dari Risco (dalam detik)"
+ },
+ "title": "Konfigurasi opsi"
+ },
+ "risco_to_ha": {
+ "data": {
+ "A": "Grup A",
+ "B": "Grup B",
+ "C": "Grup C",
+ "D": "Grup D",
+ "arm": "Diaktifkan (Keluar)",
+ "partial_arm": "Diaktifkan Sebagian (Di Rumah)"
+ },
+ "description": "Pilih status mana untuk masing-masing status yang akan dilaporkan oleh Home Assistant ke Risco",
+ "title": "Petakan status Risco ke status Home Assistant"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/risco/translations/ko.json b/homeassistant/components/risco/translations/ko.json
index 37d9a61307b062..6ceaedbc6dcdd0 100644
--- a/homeassistant/components/risco/translations/ko.json
+++ b/homeassistant/components/risco/translations/ko.json
@@ -1,30 +1,42 @@
{
"config": {
"abort": {
- "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
- "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d",
- "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "pin": "PIN \ucf54\ub4dc",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ }
+ }
}
},
"options": {
"step": {
"ha_to_risco": {
"data": {
- "armed_away": "\uacbd\ube44\uc911(\uc678\ucd9c)",
- "armed_custom_bypass": "\uacbd\ube44\uc911(\uc0ac\uc6a9\uc790 \uc6b0\ud68c)",
- "armed_home": "\uc9d1\uc548 \uacbd\ube44\uc911",
- "armed_night": "\uc57c\uac04 \uacbd\ube44\uc911"
+ "armed_away": "\uacbd\ube44 \uc911(\uc678\ucd9c)",
+ "armed_custom_bypass": "\uacbd\ube44 \uc911 (\uc0ac\uc6a9\uc790 \uc6b0\ud68c)",
+ "armed_home": "\uacbd\ube44 \uc911 (\uc7ac\uc2e4)",
+ "armed_night": "\uc57c\uac04 \uacbd\ube44 \uc911"
},
- "description": "Home Assistant \uc54c\ub78c\uc744 \ud65c\uc131\ud654 \ud560 \ub54c Risco \uc54c\ub78c\uc758 \uc0c1\ud0dc\ub97c \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624.",
- "title": "Home Assistant \uc0c1\ud0dc\ub97c Risco \uc0c1\ud0dc\ub85c \ub9e4\ud551"
+ "description": "Home Assistant \uc54c\ub78c\uc744 \uc124\uc815\ud560 \ub54c Risco \uc54c\ub78c\uc744 \uc124\uc815\ud560 \uc0c1\ud0dc\ub97c \uc120\ud0dd\ud558\uae30",
+ "title": "Home Assistant \uc0c1\ud0dc\ub97c Risco \uc0c1\ud0dc\uc5d0 \ub9e4\ud551\ud558\uae30"
},
"init": {
"data": {
+ "code_arm_required": "\uc124\uc815\ud558\ub824\uba74 PIN \ucf54\ub4dc\uac00 \ud544\uc694\ud569\ub2c8\ub2e4",
+ "code_disarm_required": "\ud574\uc81c\ud558\ub824\uba74 PIN \ucf54\ub4dc\uac00 \ud544\uc694\ud569\ub2c8\ub2e4",
"scan_interval": "Risco\ub97c \ud3f4\ub9c1\ud558\ub294 \ube48\ub3c4 (\ucd08)"
- }
+ },
+ "title": "\uc635\uc158 \uad6c\uc131\ud558\uae30"
},
"risco_to_ha": {
"data": {
@@ -32,11 +44,11 @@
"B": "\uadf8\ub8f9 B",
"C": "\uadf8\ub8f9 C",
"D": "\uadf8\ub8f9 D",
- "arm": "\uacbd\ube44\uc911(\uc678\ucd9c)",
- "partial_arm": "\ubd80\ubd84 \uacbd\ube44 \uc124\uc815 (\uc7ac\uc2e4)"
+ "arm": "\uacbd\ube44 \uc911 (\uc678\ucd9c)",
+ "partial_arm": "\ubd80\ubd84 \uacbd\ube44 \uc911 (\uc7ac\uc2e4)"
},
- "description": "Risco\uc5d0\uc11c\ubcf4\uace0\ud558\ub294 \ubaa8\ub4e0 \uc0c1\ud0dc\uc5d0 \ub300\ud574 Home Assistant \uc54c\ub78c\uc774 \ubcf4\uace0 \ud560 \uc0c1\ud0dc\ub97c \uc120\ud0dd\ud569\ub2c8\ub2e4.",
- "title": "Risco \uc0c1\ud0dc\ub97c \ud648 \uc5b4\uc2dc\uc2a4\ud134\ud2b8 \uc0c1\ud0dc\uc5d0 \ub9e4\ud551"
+ "description": "Risco\uac00 \ubcf4\uace0\ud558\ub294 \ubaa8\ub4e0 \uc0c1\ud0dc\uc5d0 \ub300\ud574 Home Assistant \uc54c\ub78c\uc774 \ubcf4\uace0\ud560 \uc0c1\ud0dc\ub97c \uc120\ud0dd\ud558\uae30",
+ "title": "Risco \uc0c1\ud0dc\ub97c Home Assistant \uc0c1\ud0dc\uc5d0 \ub9e4\ud551\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/risco/translations/lb.json b/homeassistant/components/risco/translations/lb.json
index 197dd78c4033b0..ae136cb184305b 100644
--- a/homeassistant/components/risco/translations/lb.json
+++ b/homeassistant/components/risco/translations/lb.json
@@ -39,7 +39,9 @@
"A": "Grupp A",
"B": "Grupp B",
"C": "Grupp C",
- "D": "Grupp D"
+ "D": "Grupp D",
+ "arm": "Aktiv\u00e9iert (\u00cbNNERWEE)",
+ "partial_arm": "Deelweis Aktiv\u00e9iert (DOHEEM)"
}
}
}
diff --git a/homeassistant/components/risco/translations/nl.json b/homeassistant/components/risco/translations/nl.json
index 34bcb4ab98a491..5267b164f3f756 100644
--- a/homeassistant/components/risco/translations/nl.json
+++ b/homeassistant/components/risco/translations/nl.json
@@ -26,12 +26,15 @@
"armed_custom_bypass": "Ingeschakeld met overbrugging(en)",
"armed_home": "Ingeschakeld thuis",
"armed_night": "Ingeschakeld nacht"
- }
+ },
+ "description": "Selecteer in welke staat u uw Risco-alarm wilt instellen wanneer u het Home Assistant-alarm inschakelt",
+ "title": "Wijs Home Assistant-staten toe aan Risco-staten"
},
"init": {
"data": {
- "code_arm_required": "Pincode vereist om in te schakelen",
- "code_disarm_required": "Pincode vereist om uit te schakelen"
+ "code_arm_required": "PIN-code vereist om in te schakelen",
+ "code_disarm_required": "PIN-code vereist om uit te schakelen",
+ "scan_interval": "Polling-interval (in seconden)"
},
"title": "Configureer opties"
},
@@ -40,8 +43,12 @@
"A": "Groep A",
"B": "Groep B",
"C": "Groep C",
- "D": "Groep D"
- }
+ "D": "Groep D",
+ "arm": "Ingeschakeld (AFWEZIG)",
+ "partial_arm": "Gedeeltelijk ingeschakeld (AANWEZIG)"
+ },
+ "description": "Selecteer welke staat uw Home Assistant alarm zal melden voor elke staat gemeld door Risco",
+ "title": "Wijs Risco-staten toe aan Home Assistant-staten"
}
}
}
diff --git a/homeassistant/components/risco/translations/pl.json b/homeassistant/components/risco/translations/pl.json
index ef7ed9f13e0944..b39c2cde23b307 100644
--- a/homeassistant/components/risco/translations/pl.json
+++ b/homeassistant/components/risco/translations/pl.json
@@ -34,7 +34,7 @@
"data": {
"code_arm_required": "Wymagaj kodu PIN do uzbrojenia",
"code_disarm_required": "Wymagaj kodu PIN do rozbrojenia",
- "scan_interval": "Cz\u0119stotliwo\u015b\u0107 od\u015bwie\u017cania (w sekundach)"
+ "scan_interval": "Jak cz\u0119sto odpytywa\u0107 Risco (w sekundach)"
},
"title": "Opcje"
},
diff --git a/homeassistant/components/risco/translations/ru.json b/homeassistant/components/risco/translations/ru.json
index 3fd1fd567f9a89..200c38fb213d67 100644
--- a/homeassistant/components/risco/translations/ru.json
+++ b/homeassistant/components/risco/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
@@ -13,7 +13,7 @@
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"pin": "PIN-\u043a\u043e\u0434",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
}
}
}
diff --git a/homeassistant/components/risco/translations/tr.json b/homeassistant/components/risco/translations/tr.json
new file mode 100644
index 00000000000000..02a3b505f84b8b
--- /dev/null
+++ b/homeassistant/components/risco/translations/tr.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "Se\u00e7enekleri yap\u0131land\u0131r\u0131n"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/risco/translations/uk.json b/homeassistant/components/risco/translations/uk.json
new file mode 100644
index 00000000000000..53b64344f2edd4
--- /dev/null
+++ b/homeassistant/components/risco/translations/uk.json
@@ -0,0 +1,55 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "pin": "PIN-\u043a\u043e\u0434",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "ha_to_risco": {
+ "data": {
+ "armed_away": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u041d\u0435 \u0432\u0434\u043e\u043c\u0430)",
+ "armed_custom_bypass": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 \u0437 \u0432\u0438\u043d\u044f\u0442\u043a\u0430\u043c\u0438",
+ "armed_home": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u0412\u0434\u043e\u043c\u0430)",
+ "armed_night": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u043d\u0456\u0447)"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Risco \u043f\u0440\u0438 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u0456 \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Home Assistant",
+ "title": "\u0417\u0456\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044f \u0441\u0442\u0430\u043d\u0456\u0432 Home Assistant \u0456 Risco"
+ },
+ "init": {
+ "data": {
+ "code_arm_required": "\u0412\u0438\u043c\u0430\u0433\u0430\u0442\u0438 PIN-\u043a\u043e\u0434 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0443",
+ "code_disarm_required": "\u0412\u0438\u043c\u0430\u0433\u0430\u0442\u0438 PIN-\u043a\u043e\u0434 \u0434\u043b\u044f \u0437\u043d\u044f\u0442\u0442\u044f \u0437 \u043e\u0445\u043e\u0440\u043e\u043d\u0438",
+ "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
+ },
+ "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438"
+ },
+ "risco_to_ha": {
+ "data": {
+ "A": "\u0413\u0440\u0443\u043f\u0430 \u0410",
+ "B": "\u0413\u0440\u0443\u043f\u0430 B",
+ "C": "\u0413\u0440\u0443\u043f\u0430 C",
+ "D": "\u0413\u0440\u0443\u043f\u0430 D",
+ "arm": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (AWAY)",
+ "partial_arm": "\u0427\u0430\u0441\u0442\u043a\u043e\u0432\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430 (STAY)"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Home Assistant \u043f\u0440\u0438 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u0456 \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Risco",
+ "title": "\u0417\u0456\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044f \u0441\u0442\u0430\u043d\u0456\u0432 Home Assistant \u0456 Risco"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py
new file mode 100644
index 00000000000000..f2fd13a9ef4516
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/__init__.py
@@ -0,0 +1,61 @@
+"""The Rituals Perfume Genie integration."""
+import asyncio
+import logging
+
+from aiohttp.client_exceptions import ClientConnectorError
+from pyrituals import Account
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+
+from .const import ACCOUNT_HASH, DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+EMPTY_CREDENTIALS = ""
+
+PLATFORMS = ["switch"]
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+ """Set up the Rituals Perfume Genie component."""
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up Rituals Perfume Genie from a config entry."""
+ session = async_get_clientsession(hass)
+ account = Account(EMPTY_CREDENTIALS, EMPTY_CREDENTIALS, session)
+ account.data = {ACCOUNT_HASH: entry.data.get(ACCOUNT_HASH)}
+
+ try:
+ await account.get_devices()
+ except ClientConnectorError as ex:
+ raise ConfigEntryNotReady from ex
+
+ hass.data.setdefault(DOMAIN, {})[entry.entry_id] = account
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
diff --git a/homeassistant/components/rituals_perfume_genie/config_flow.py b/homeassistant/components/rituals_perfume_genie/config_flow.py
new file mode 100644
index 00000000000000..7bd75cdbbc0899
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/config_flow.py
@@ -0,0 +1,60 @@
+"""Config flow for Rituals Perfume Genie integration."""
+import logging
+
+from aiohttp import ClientResponseError
+from pyrituals import Account, AuthenticationException
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+
+from .const import ACCOUNT_HASH, DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+DATA_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_EMAIL): str,
+ vol.Required(CONF_PASSWORD): str,
+ }
+)
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Rituals Perfume Genie."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ if user_input is None:
+ return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
+
+ errors = {}
+
+ session = async_get_clientsession(self.hass)
+ account = Account(user_input[CONF_EMAIL], user_input[CONF_PASSWORD], session)
+
+ try:
+ await account.authenticate()
+ except ClientResponseError:
+ errors["base"] = "cannot_connect"
+ except AuthenticationException:
+ errors["base"] = "invalid_auth"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+ else:
+ await self.async_set_unique_id(account.data[CONF_EMAIL])
+ self._abort_if_unique_id_configured()
+
+ return self.async_create_entry(
+ title=account.data[CONF_EMAIL],
+ data={ACCOUNT_HASH: account.data[ACCOUNT_HASH]},
+ )
+
+ return self.async_show_form(
+ step_id="user", data_schema=DATA_SCHEMA, errors=errors
+ )
diff --git a/homeassistant/components/rituals_perfume_genie/const.py b/homeassistant/components/rituals_perfume_genie/const.py
new file mode 100644
index 00000000000000..075d79ec8de9e8
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/const.py
@@ -0,0 +1,5 @@
+"""Constants for the Rituals Perfume Genie integration."""
+
+DOMAIN = "rituals_perfume_genie"
+
+ACCOUNT_HASH = "account_hash"
diff --git a/homeassistant/components/rituals_perfume_genie/manifest.json b/homeassistant/components/rituals_perfume_genie/manifest.json
new file mode 100644
index 00000000000000..8be7e98b939895
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/manifest.json
@@ -0,0 +1,12 @@
+{
+ "domain": "rituals_perfume_genie",
+ "name": "Rituals Perfume Genie",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/rituals_perfume_genie",
+ "requirements": [
+ "pyrituals==0.0.2"
+ ],
+ "codeowners": [
+ "@milanmeu"
+ ]
+}
diff --git a/homeassistant/components/rituals_perfume_genie/strings.json b/homeassistant/components/rituals_perfume_genie/strings.json
new file mode 100644
index 00000000000000..8824923c3138bf
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/strings.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "Connect to your Rituals account",
+ "data": {
+ "email": "[%key:common::config_flow::data::email%]",
+ "password": "[%key:common::config_flow::data::password%]"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ }
+ }
+}
diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py
new file mode 100644
index 00000000000000..bc8e2b5e175995
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/switch.py
@@ -0,0 +1,104 @@
+"""Support for Rituals Perfume Genie switches."""
+from datetime import timedelta
+import logging
+
+import aiohttp
+
+from homeassistant.components.switch import SwitchEntity
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+SCAN_INTERVAL = timedelta(seconds=30)
+
+ON_STATE = "1"
+AVAILABLE_STATE = 1
+
+MANUFACTURER = "Rituals Cosmetics"
+MODEL = "Diffuser"
+ICON = "mdi:fan"
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the diffuser switch."""
+ account = hass.data[DOMAIN][config_entry.entry_id]
+ diffusers = await account.get_devices()
+
+ entities = []
+ for diffuser in diffusers:
+ entities.append(DiffuserSwitch(diffuser))
+
+ async_add_entities(entities, True)
+
+
+class DiffuserSwitch(SwitchEntity):
+ """Representation of a diffuser switch."""
+
+ def __init__(self, diffuser):
+ """Initialize the switch."""
+ self._diffuser = diffuser
+ self._available = True
+
+ @property
+ def device_info(self):
+ """Return information about the device."""
+ return {
+ "name": self._diffuser.data["hub"]["attributes"]["roomnamec"],
+ "identifiers": {(DOMAIN, self._diffuser.data["hub"]["hublot"])},
+ "manufacturer": MANUFACTURER,
+ "model": MODEL,
+ "sw_version": self._diffuser.data["hub"]["sensors"]["versionc"],
+ }
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of the device."""
+ return self._diffuser.data["hub"]["hublot"]
+
+ @property
+ def available(self):
+ """Return if the device is available."""
+ return self._available
+
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return self._diffuser.data["hub"]["attributes"]["roomnamec"]
+
+ @property
+ def icon(self):
+ """Return the icon of the device."""
+ return ICON
+
+ @property
+ def extra_state_attributes(self):
+ """Return the device state attributes."""
+ attributes = {
+ "fan_speed": self._diffuser.data["hub"]["attributes"]["speedc"],
+ "room_size": self._diffuser.data["hub"]["attributes"]["roomc"],
+ }
+ return attributes
+
+ @property
+ def is_on(self):
+ """If the device is currently on or off."""
+ return self._diffuser.data["hub"]["attributes"]["fanc"] == ON_STATE
+
+ async def async_turn_on(self, **kwargs):
+ """Turn the device on."""
+ await self._diffuser.turn_on()
+
+ async def async_turn_off(self, **kwargs):
+ """Turn the device off."""
+ await self._diffuser.turn_off()
+
+ async def async_update(self):
+ """Update the data of the device."""
+ try:
+ await self._diffuser.update_data()
+ except aiohttp.ClientError:
+ self._available = False
+ _LOGGER.error("Unable to retrieve data from rituals.sense-company.com")
+ else:
+ self._available = self._diffuser.data["hub"]["status"] == AVAILABLE_STATE
diff --git a/homeassistant/components/rituals_perfume_genie/translations/bg.json b/homeassistant/components/rituals_perfume_genie/translations/bg.json
new file mode 100644
index 00000000000000..cef3726d759676
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/bg.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u0430"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/translations/ca.json b/homeassistant/components/rituals_perfume_genie/translations/ca.json
new file mode 100644
index 00000000000000..d7abccc2c25bc0
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/ca.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Correu electr\u00f2nic",
+ "password": "Contrasenya"
+ },
+ "title": "Connexi\u00f3 amb el teu compte Rituals"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/translations/de.json b/homeassistant/components/rituals_perfume_genie/translations/de.json
new file mode 100644
index 00000000000000..67b8ed59e0b6b3
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/de.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-Mail",
+ "password": "Passwort"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/translations/en.json b/homeassistant/components/rituals_perfume_genie/translations/en.json
new file mode 100644
index 00000000000000..21207b1e7ed34a
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/en.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Email",
+ "password": "Password"
+ },
+ "title": "Connect to your Rituals account"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/translations/es.json b/homeassistant/components/rituals_perfume_genie/translations/es.json
new file mode 100644
index 00000000000000..bdb1933eaf7176
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/es.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositivo ya est\u00e1 configurado"
+ },
+ "error": {
+ "cannot_connect": "No se pudo conectar",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
+ "unknown": "Error inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "email",
+ "password": "Contrase\u00f1a"
+ },
+ "title": "Con\u00e9ctese a su cuenta de Rituals"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/translations/et.json b/homeassistant/components/rituals_perfume_genie/translations/et.json
new file mode 100644
index 00000000000000..17720a597924eb
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/et.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_auth": "Vigane autentimine",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-posti aadress",
+ "password": "Salas\u00f5na"
+ },
+ "title": "Loo \u00fchendus oma Ritualsi kontoga"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/translations/fr.json b/homeassistant/components/rituals_perfume_genie/translations/fr.json
new file mode 100644
index 00000000000000..2a1fb9c8bb8a89
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/fr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion",
+ "invalid_auth": "Authentification invalide",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Email",
+ "password": "Mot de passe"
+ },
+ "title": "Connectez-vous \u00e0 votre compte Rituals"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/translations/hu.json b/homeassistant/components/rituals_perfume_genie/translations/hu.json
new file mode 100644
index 00000000000000..4ecaf2ba0d0b0d
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/hu.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-mail",
+ "password": "Jelsz\u00f3"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/translations/id.json b/homeassistant/components/rituals_perfume_genie/translations/id.json
new file mode 100644
index 00000000000000..91d931005cf91f
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Email",
+ "password": "Kata Sandi"
+ },
+ "title": "Hubungkan ke akun Ritual Anda"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/translations/it.json b/homeassistant/components/rituals_perfume_genie/translations/it.json
new file mode 100644
index 00000000000000..6dfb2230285cbf
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/it.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-mail",
+ "password": "Password"
+ },
+ "title": "Collegati al tuo account Rituals"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/translations/ko.json b/homeassistant/components/rituals_perfume_genie/translations/ko.json
new file mode 100644
index 00000000000000..489e4f5b806475
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/ko.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "\uc774\uba54\uc77c",
+ "password": "\ube44\ubc00\ubc88\ud638"
+ },
+ "title": "Rituals \uacc4\uc815\uc5d0 \uc5f0\uacb0\ud558\uae30"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/translations/nl.json b/homeassistant/components/rituals_perfume_genie/translations/nl.json
new file mode 100644
index 00000000000000..432079cac257fc
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/nl.json
@@ -0,0 +1,21 @@
+{
+ "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": {
+ "email": "E-mail",
+ "password": "Wachtwoord"
+ },
+ "title": "Verbind met uw Rituals account"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/translations/no.json b/homeassistant/components/rituals_perfume_genie/translations/no.json
new file mode 100644
index 00000000000000..2ffc9bc91af8bd
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/no.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_auth": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-post",
+ "password": "Passord"
+ },
+ "title": "Koble til Rituals-kontoen din"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/translations/pl.json b/homeassistant/components/rituals_perfume_genie/translations/pl.json
new file mode 100644
index 00000000000000..9e8c9839cbe962
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/pl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Adres e-mail",
+ "password": "Has\u0142o"
+ },
+ "title": "Po\u0142\u0105czenie z kontem Rituals"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/translations/pt.json b/homeassistant/components/rituals_perfume_genie/translations/pt.json
new file mode 100644
index 00000000000000..e3b78cd8e42fc8
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/pt.json
@@ -0,0 +1,20 @@
+{
+ "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": {
+ "email": "Email",
+ "password": "Palavra-passe"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/translations/ru.json b/homeassistant/components/rituals_perfume_genie/translations/ru.json
new file mode 100644
index 00000000000000..afbf1da0e4693c
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/ru.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Rituals"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json b/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json
new file mode 100644
index 00000000000000..c91a500edd81fb
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "\u96fb\u5b50\u90f5\u4ef6",
+ "password": "\u5bc6\u78bc"
+ },
+ "title": "\u9023\u7dda\u81f3 Rituals \u5e33\u865f"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json
index 595e2d4834ae3f..68f895cb2b8855 100644
--- a/homeassistant/components/rmvtransport/manifest.json
+++ b/homeassistant/components/rmvtransport/manifest.json
@@ -3,7 +3,7 @@
"name": "RMV",
"documentation": "https://www.home-assistant.io/integrations/rmvtransport",
"requirements": [
- "PyRMVtransport==0.2.10"
+ "PyRMVtransport==0.3.1"
],
"codeowners": [
"@cgtobi"
diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py
index eb053de8950257..d85dae533032dd 100644
--- a/homeassistant/components/rmvtransport/sensor.py
+++ b/homeassistant/components/rmvtransport/sensor.py
@@ -4,14 +4,16 @@
import logging
from RMVtransport import RMVtransport
-from RMVtransport.rmvtransport import RMVtransportApiConnectionError
+from RMVtransport.rmvtransport import (
+ RMVtransportApiConnectionError,
+ RMVtransportDataError,
+)
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, TIME_MINUTES
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_TIMEOUT, TIME_MINUTES
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -25,7 +27,6 @@
CONF_PRODUCTS = "products"
CONF_TIME_OFFSET = "time_offset"
CONF_MAX_JOURNEYS = "max_journeys"
-CONF_TIMEOUT = "timeout"
DEFAULT_NAME = "RMV Journey"
@@ -102,7 +103,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(sensors)
-class RMVDepartureSensor(Entity):
+class RMVDepartureSensor(SensorEntity):
"""Implementation of an RMV departure sensor."""
def __init__(
@@ -149,7 +150,7 @@ def state(self):
return self._state
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
try:
return {
@@ -230,7 +231,7 @@ async def async_update(self):
max_journeys=50,
)
- except RMVtransportApiConnectionError:
+ except (RMVtransportApiConnectionError, RMVtransportDataError):
self.departures = []
_LOGGER.warning("Could not retrieve data from rmv.de")
return
diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py
index af2e0ee946fe2b..4a349265459562 100644
--- a/homeassistant/components/roku/__init__.py
+++ b/homeassistant/components/roku/__init__.py
@@ -1,8 +1,10 @@
"""Support for Roku."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
import logging
-from typing import Any, Dict
+from typing import Any
from rokuecp import Roku, RokuConnectionError, RokuError
from rokuecp.models import Device
@@ -11,7 +13,6 @@
from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_NAME, CONF_HOST
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import HomeAssistantType
@@ -27,6 +28,7 @@
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_SOFTWARE_VERSION,
+ ATTR_SUGGESTED_AREA,
DOMAIN,
)
@@ -37,7 +39,7 @@
_LOGGER = logging.getLogger(__name__)
-async def async_setup(hass: HomeAssistantType, config: Dict) -> bool:
+async def async_setup(hass: HomeAssistantType, config: dict) -> bool:
"""Set up the Roku integration."""
hass.data.setdefault(DOMAIN, {})
return True
@@ -46,16 +48,13 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool:
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
"""Set up Roku from a config entry."""
coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = coordinator
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -66,8 +65,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -150,7 +149,7 @@ def name(self) -> str:
return self._name
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about this Roku device."""
if self._device_id is None:
return None
@@ -161,4 +160,5 @@ def device_info(self) -> Dict[str, Any]:
ATTR_MANUFACTURER: self.coordinator.data.info.brand,
ATTR_MODEL: self.coordinator.data.info.model_name,
ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version,
+ ATTR_SUGGESTED_AREA: self.coordinator.data.info.device_location,
}
diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py
index f8e9034292c1d6..8424850fe6c6db 100644
--- a/homeassistant/components/roku/config_flow.py
+++ b/homeassistant/components/roku/config_flow.py
@@ -1,6 +1,8 @@
"""Config flow for Roku."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from urllib.parse import urlparse
from rokuecp import Roku, RokuError
@@ -17,7 +19,7 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import HomeAssistantType
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import DOMAIN
DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
@@ -27,7 +29,7 @@
_LOGGER = logging.getLogger(__name__)
-async def validate_input(hass: HomeAssistantType, data: Dict) -> Dict:
+async def validate_input(hass: HomeAssistantType, data: dict) -> dict:
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
@@ -53,7 +55,7 @@ def __init__(self):
self.discovery_info = {}
@callback
- def _show_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
+ def _show_form(self, errors: dict | None = None) -> dict[str, Any]:
"""Show the form to the user."""
return self.async_show_form(
step_id="user",
@@ -61,9 +63,7 @@ def _show_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
errors=errors or {},
)
- async def async_step_user(
- self, user_input: Optional[Dict] = None
- ) -> Dict[str, Any]:
+ async def async_step_user(self, user_input: dict | None = None) -> dict[str, Any]:
"""Handle a flow initialized by the user."""
if not user_input:
return self._show_form()
@@ -109,15 +109,14 @@ async def async_step_homekit(self, discovery_info):
updates={CONF_HOST: discovery_info[CONF_HOST]},
)
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update({"title_placeholders": {"name": info["title"]}})
self.discovery_info.update({CONF_NAME: info["title"]})
return await self.async_step_discovery_confirm()
async def async_step_ssdp(
- self, discovery_info: Optional[Dict] = None
- ) -> Dict[str, Any]:
+ self, discovery_info: dict | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initialized by discovery."""
host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname
name = discovery_info[ATTR_UPNP_FRIENDLY_NAME]
@@ -126,7 +125,6 @@ async def async_step_ssdp(
await self.async_set_unique_id(serial_number)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update({"title_placeholders": {"name": name}})
self.discovery_info.update({CONF_HOST: host, CONF_NAME: name})
@@ -143,10 +141,9 @@ async def async_step_ssdp(
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(
- self, user_input: Optional[Dict] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict | None = None
+ ) -> dict[str, Any]:
"""Handle user-confirmation of discovered device."""
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
if user_input is None:
return self.async_show_form(
step_id="discovery_confirm",
diff --git a/homeassistant/components/roku/const.py b/homeassistant/components/roku/const.py
index 4abbd9e109aca6..dc458c88cd0f97 100644
--- a/homeassistant/components/roku/const.py
+++ b/homeassistant/components/roku/const.py
@@ -7,6 +7,7 @@
ATTR_MANUFACTURER = "manufacturer"
ATTR_MODEL = "model"
ATTR_SOFTWARE_VERSION = "sw_version"
+ATTR_SUGGESTED_AREA = "suggested_area"
# Default Values
DEFAULT_PORT = 8060
diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json
index f1509edb6fbbc5..981a9b080777d4 100644
--- a/homeassistant/components/roku/manifest.json
+++ b/homeassistant/components/roku/manifest.json
@@ -2,7 +2,7 @@
"domain": "roku",
"name": "Roku",
"documentation": "https://www.home-assistant.io/integrations/roku",
- "requirements": ["rokuecp==0.6.0"],
+ "requirements": ["rokuecp==0.8.1"],
"homekit": {
"models": [
"3810X",
diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py
index e50c28d0a43e52..6fee53595ac823 100644
--- a/homeassistant/components/roku/media_player.py
+++ b/homeassistant/components/roku/media_player.py
@@ -1,6 +1,7 @@
"""Support for the Roku media player."""
+from __future__ import annotations
+
import logging
-from typing import List, Optional
import voluptuous as vol
@@ -100,7 +101,7 @@ def unique_id(self) -> str:
return self._unique_id
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the class of this device."""
if self.coordinator.data.info.device_type == "tv":
return DEVICE_CLASS_TV
@@ -230,7 +231,7 @@ def source(self) -> str:
return None
@property
- def source_list(self) -> List:
+ def source_list(self) -> list:
"""List of available input sources."""
return ["Home"] + sorted(app.name for app in self.coordinator.data.apps)
diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py
index 3fcd2ee1a34032..da57866757866d 100644
--- a/homeassistant/components/roku/remote.py
+++ b/homeassistant/components/roku/remote.py
@@ -1,5 +1,7 @@
"""Support for the Roku remote."""
-from typing import Callable, List
+from __future__ import annotations
+
+from typing import Callable
from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity
from homeassistant.config_entries import ConfigEntry
@@ -12,7 +14,7 @@
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
- async_add_entities: Callable[[List, bool], None],
+ async_add_entities: Callable[[list, bool], None],
) -> bool:
"""Load Roku remote based on a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
@@ -56,7 +58,7 @@ async def async_turn_off(self, **kwargs) -> None:
await self.coordinator.async_request_refresh()
@roku_exception_handler
- async def async_send_command(self, command: List, **kwargs) -> None:
+ async def async_send_command(self, command: list, **kwargs) -> None:
"""Send a command to one device."""
num_repeats = kwargs[ATTR_NUM_REPEATS]
diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json
index 55b533d4f1cfc6..3523615ff33a44 100644
--- a/homeassistant/components/roku/strings.json
+++ b/homeassistant/components/roku/strings.json
@@ -19,6 +19,7 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
}
diff --git a/homeassistant/components/roku/translations/ca.json b/homeassistant/components/roku/translations/ca.json
index e9ab61575b5203..b60b8f83eb9d38 100644
--- a/homeassistant/components/roku/translations/ca.json
+++ b/homeassistant/components/roku/translations/ca.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "El dispositiu ja est\u00e0 configurat",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
"unknown": "Error inesperat"
},
"error": {
@@ -9,6 +10,10 @@
},
"flow_title": "Roku: {name}",
"step": {
+ "discovery_confirm": {
+ "description": "Vols configurar {name}?",
+ "title": "Roku"
+ },
"ssdp_confirm": {
"description": "Vols configurar {name}?",
"title": "Roku"
diff --git a/homeassistant/components/roku/translations/cs.json b/homeassistant/components/roku/translations/cs.json
index 7a83973a6f7856..6914a519285fe4 100644
--- a/homeassistant/components/roku/translations/cs.json
+++ b/homeassistant/components/roku/translations/cs.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno",
+ "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1",
"unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
},
"error": {
@@ -9,6 +10,10 @@
},
"flow_title": "Roku: {name}",
"step": {
+ "discovery_confirm": {
+ "description": "Chcete nastavit {name}?",
+ "title": "Roku"
+ },
"ssdp_confirm": {
"description": "Chcete nastavit {name}?",
"title": "Roku"
diff --git a/homeassistant/components/roku/translations/de.json b/homeassistant/components/roku/translations/de.json
index 9899aeba427e4f..152161cb27fbfb 100644
--- a/homeassistant/components/roku/translations/de.json
+++ b/homeassistant/components/roku/translations/de.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "Das Ger\u00e4t ist bereits konfiguriert",
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
"unknown": "Unerwarteter Fehler"
},
"error": {
@@ -9,6 +10,10 @@
},
"flow_title": "Roku: {name}",
"step": {
+ "discovery_confirm": {
+ "description": "M\u00f6chtest du {name} einrichten?",
+ "title": "Roku"
+ },
"ssdp_confirm": {
"data": {
"one": "eins",
diff --git a/homeassistant/components/roku/translations/en.json b/homeassistant/components/roku/translations/en.json
index 6facd1f3a7c26b..2b54cafe8909a1 100644
--- a/homeassistant/components/roku/translations/en.json
+++ b/homeassistant/components/roku/translations/en.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "Device is already configured",
+ "already_in_progress": "Configuration flow is already in progress",
"unknown": "Unexpected error"
},
"error": {
@@ -9,6 +10,10 @@
},
"flow_title": "Roku: {name}",
"step": {
+ "discovery_confirm": {
+ "description": "Do you want to set up {name}?",
+ "title": "Roku"
+ },
"ssdp_confirm": {
"description": "Do you want to set up {name}?",
"title": "Roku"
diff --git a/homeassistant/components/roku/translations/es.json b/homeassistant/components/roku/translations/es.json
index 78fb25809271dd..95e42643379c1f 100644
--- a/homeassistant/components/roku/translations/es.json
+++ b/homeassistant/components/roku/translations/es.json
@@ -9,6 +9,10 @@
},
"flow_title": "Roku: {name}",
"step": {
+ "discovery_confirm": {
+ "description": "\u00bfQuieres configurar {name} ?",
+ "title": "Roku"
+ },
"ssdp_confirm": {
"description": "\u00bfQuieres configurar {name}?",
"title": "Roku"
diff --git a/homeassistant/components/roku/translations/et.json b/homeassistant/components/roku/translations/et.json
index e4869d044c8d4b..17bce39f5dfca8 100644
--- a/homeassistant/components/roku/translations/et.json
+++ b/homeassistant/components/roku/translations/et.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "already_in_progress": "Seadistamine on juba k\u00e4imas",
"unknown": "Tundmatu viga"
},
"error": {
@@ -9,6 +10,10 @@
},
"flow_title": "",
"step": {
+ "discovery_confirm": {
+ "description": "Kas soovid seadistada {name}?",
+ "title": ""
+ },
"ssdp_confirm": {
"description": "Kas soovid seadistada {name}?",
"title": ""
diff --git a/homeassistant/components/roku/translations/fr.json b/homeassistant/components/roku/translations/fr.json
index 7aba1ef0489fd6..b3dc08a7dc86d9 100644
--- a/homeassistant/components/roku/translations/fr.json
+++ b/homeassistant/components/roku/translations/fr.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
+ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours",
"unknown": "Erreur inattendue"
},
"error": {
@@ -9,6 +10,14 @@
},
"flow_title": "Roku: {name}",
"step": {
+ "discovery_confirm": {
+ "data": {
+ "one": "Vide",
+ "other": "Vide"
+ },
+ "description": "Voulez-vous configurer {name} ?",
+ "title": "Roku"
+ },
"ssdp_confirm": {
"data": {
"one": "Vide",
diff --git a/homeassistant/components/roku/translations/hu.json b/homeassistant/components/roku/translations/hu.json
index 8b4e9f9d54d114..5485d9e00ce465 100644
--- a/homeassistant/components/roku/translations/hu.json
+++ b/homeassistant/components/roku/translations/hu.json
@@ -2,12 +2,26 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
- "unknown": "V\u00e1ratlan hiba"
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
+ "flow_title": "Roku: {name}",
"step": {
+ "discovery_confirm": {
+ "data": {
+ "one": "Egy",
+ "other": "Egy\u00e9b"
+ },
+ "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a(z) {name}-t?",
+ "title": "Roku"
+ },
+ "ssdp_confirm": {
+ "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?",
+ "title": "Roku"
+ },
"user": {
"data": {
"host": "Hoszt"
diff --git a/homeassistant/components/roku/translations/id.json b/homeassistant/components/roku/translations/id.json
new file mode 100644
index 00000000000000..0e60de9b61f790
--- /dev/null
+++ b/homeassistant/components/roku/translations/id.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "flow_title": "Roku: {name}",
+ "step": {
+ "discovery_confirm": {
+ "description": "Ingin menyiapkan {name}?",
+ "title": "Roku"
+ },
+ "ssdp_confirm": {
+ "description": "Ingin menyiapkan {name}?",
+ "title": "Roku"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Masukkan informasi Roku Anda."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/roku/translations/it.json b/homeassistant/components/roku/translations/it.json
index 007be91d15532c..3c11aa4d8ae12e 100644
--- a/homeassistant/components/roku/translations/it.json
+++ b/homeassistant/components/roku/translations/it.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"unknown": "Errore imprevisto"
},
"error": {
@@ -9,6 +10,10 @@
},
"flow_title": "Roku: {name}",
"step": {
+ "discovery_confirm": {
+ "description": "Vuoi configurare {name}?",
+ "title": "Roku"
+ },
"ssdp_confirm": {
"description": "Vuoi impostare {name}?",
"title": "Roku"
diff --git a/homeassistant/components/roku/translations/ko.json b/homeassistant/components/roku/translations/ko.json
index 054c1674884f8a..cb127234601533 100644
--- a/homeassistant/components/roku/translations/ko.json
+++ b/homeassistant/components/roku/translations/ko.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
@@ -9,8 +10,12 @@
},
"flow_title": "Roku: {name}",
"step": {
+ "discovery_confirm": {
+ "description": "{name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "Roku"
+ },
"ssdp_confirm": {
- "description": "{name} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "description": "{name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "Roku"
},
"user": {
diff --git a/homeassistant/components/roku/translations/lb.json b/homeassistant/components/roku/translations/lb.json
index 3aa8e5fa642fe2..04ad814c6b4d4e 100644
--- a/homeassistant/components/roku/translations/lb.json
+++ b/homeassistant/components/roku/translations/lb.json
@@ -9,6 +9,10 @@
},
"flow_title": "Roku: {name}",
"step": {
+ "discovery_confirm": {
+ "description": "Soll {name} konfigur\u00e9iert ginn?",
+ "title": "Roku"
+ },
"ssdp_confirm": {
"description": "Soll {name} konfigur\u00e9iert ginn?",
"title": "Roku"
diff --git a/homeassistant/components/roku/translations/nl.json b/homeassistant/components/roku/translations/nl.json
index 6b0927178fe5d5..daecee2f1dc4e7 100644
--- a/homeassistant/components/roku/translations/nl.json
+++ b/homeassistant/components/roku/translations/nl.json
@@ -1,14 +1,23 @@
{
"config": {
"abort": {
- "already_configured": "Roku-apparaat is al geconfigureerd",
+ "already_configured": "Apparaat is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
"unknown": "Onverwachte fout"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw"
+ "cannot_connect": "Kan geen verbinding maken"
},
"flow_title": "Roku: {name}",
"step": {
+ "discovery_confirm": {
+ "data": {
+ "one": "Een",
+ "other": "Ander"
+ },
+ "description": "Wilt u {name} instellen?",
+ "title": "Roku"
+ },
"ssdp_confirm": {
"data": {
"one": "Leeg",
@@ -19,7 +28,7 @@
},
"user": {
"data": {
- "host": "Host- of IP-adres"
+ "host": "Host"
},
"description": "Voer uw Roku-informatie in."
}
diff --git a/homeassistant/components/roku/translations/no.json b/homeassistant/components/roku/translations/no.json
index 029220b58593ab..e7dc663b8f800e 100644
--- a/homeassistant/components/roku/translations/no.json
+++ b/homeassistant/components/roku/translations/no.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "Enheten er allerede konfigurert",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
"unknown": "Uventet feil"
},
"error": {
@@ -9,6 +10,10 @@
},
"flow_title": "",
"step": {
+ "discovery_confirm": {
+ "description": "Vil du konfigurere {name}?",
+ "title": ""
+ },
"ssdp_confirm": {
"description": "Vil du sette opp {name} ?",
"title": ""
diff --git a/homeassistant/components/roku/translations/pl.json b/homeassistant/components/roku/translations/pl.json
index 3231d6c4bb7367..1a570c6434776c 100644
--- a/homeassistant/components/roku/translations/pl.json
+++ b/homeassistant/components/roku/translations/pl.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "already_in_progress": "Konfiguracja jest ju\u017c w toku",
"unknown": "Nieoczekiwany b\u0142\u0105d"
},
"error": {
@@ -9,6 +10,16 @@
},
"flow_title": "Roku: {name}",
"step": {
+ "discovery_confirm": {
+ "data": {
+ "few": "kilka",
+ "many": "wiele",
+ "one": "jeden",
+ "other": "inne"
+ },
+ "description": "Czy chcesz skonfigurowa\u0107 {name}?",
+ "title": "Roku"
+ },
"ssdp_confirm": {
"data": {
"few": "kilka",
diff --git a/homeassistant/components/roku/translations/ru.json b/homeassistant/components/roku/translations/ru.json
index b5dcddbe555417..c3ae135ed76ae2 100644
--- a/homeassistant/components/roku/translations/ru.json
+++ b/homeassistant/components/roku/translations/ru.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "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.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"error": {
@@ -9,6 +10,10 @@
},
"flow_title": "Roku: {name}",
"step": {
+ "discovery_confirm": {
+ "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?",
+ "title": "Roku"
+ },
"ssdp_confirm": {
"description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?",
"title": "Roku"
diff --git a/homeassistant/components/roku/translations/sv.json b/homeassistant/components/roku/translations/sv.json
index 6d6f9223466dd2..524e7753548a21 100644
--- a/homeassistant/components/roku/translations/sv.json
+++ b/homeassistant/components/roku/translations/sv.json
@@ -2,6 +2,18 @@
"config": {
"abort": {
"unknown": "Ov\u00e4ntat fel"
+ },
+ "flow_title": "Roku: {name}",
+ "step": {
+ "ssdp_confirm": {
+ "description": "Vill du konfigurera {name}?",
+ "title": "Roku"
+ },
+ "user": {
+ "data": {
+ "host": "V\u00e4rd"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/roku/translations/tr.json b/homeassistant/components/roku/translations/tr.json
new file mode 100644
index 00000000000000..0dca1a028b283e
--- /dev/null
+++ b/homeassistant/components/roku/translations/tr.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "unknown": "Beklenmeyen hata"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "discovery_confirm": {
+ "description": "{name} kurmak istiyor musunuz?",
+ "title": "Roku"
+ },
+ "ssdp_confirm": {
+ "description": "{name} kurmak istiyor musunuz?"
+ },
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/roku/translations/uk.json b/homeassistant/components/roku/translations/uk.json
new file mode 100644
index 00000000000000..b7db8875f8e0b8
--- /dev/null
+++ b/homeassistant/components/roku/translations/uk.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "flow_title": "Roku: {name}",
+ "step": {
+ "discovery_confirm": {
+ "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name}?",
+ "title": "Roku"
+ },
+ "ssdp_confirm": {
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name}?",
+ "title": "Roku"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e Roku."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/roku/translations/zh-Hant.json b/homeassistant/components/roku/translations/zh-Hant.json
index cfa3a4aa3b4ed7..429c03a991ea83 100644
--- a/homeassistant/components/roku/translations/zh-Hant.json
+++ b/homeassistant/components/roku/translations/zh-Hant.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"error": {
@@ -9,6 +10,10 @@
},
"flow_title": "Roku\uff1a{name}",
"step": {
+ "discovery_confirm": {
+ "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f",
+ "title": "Roku"
+ },
"ssdp_confirm": {
"description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f",
"title": "Roku"
diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py
index 63deead73071c0..6de775e1d997ff 100644
--- a/homeassistant/components/roomba/__init__.py
+++ b/homeassistant/components/roomba/__init__.py
@@ -6,18 +6,9 @@
from roombapy import Roomba, RoombaConnectionError
from homeassistant import exceptions
-from homeassistant.const import CONF_HOST, CONF_PASSWORD
-
-from .const import (
- BLID,
- COMPONENTS,
- CONF_BLID,
- CONF_CONTINUOUS,
- CONF_DELAY,
- CONF_NAME,
- DOMAIN,
- ROOMBA_SESSION,
-)
+from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_NAME, CONF_PASSWORD
+
+from .const import BLID, CONF_BLID, CONF_CONTINUOUS, DOMAIN, PLATFORMS, ROOMBA_SESSION
_LOGGER = logging.getLogger(__name__)
@@ -60,9 +51,9 @@ async def async_setup_entry(hass, config_entry):
BLID: config_entry.data[CONF_BLID],
}
- for component in COMPONENTS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
if not config_entry.update_listeners:
@@ -114,8 +105,8 @@ async def async_unload_entry(hass, config_entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in COMPONENTS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/roomba/braava.py b/homeassistant/components/roomba/braava.py
index 1a3d106bf8086a..90298078e4238a 100644
--- a/homeassistant/components/roomba/braava.py
+++ b/homeassistant/components/roomba/braava.py
@@ -116,9 +116,9 @@ async def async_set_fan_speed(self, fan_speed, **kwargs):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
- state_attrs = super().device_state_attributes
+ state_attrs = super().extra_state_attributes
# Get Braava state
state = self.vacuum_state
diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py
index 4b0b76b44c9129..45c2d8b9a1bdca 100644
--- a/homeassistant/components/roomba/config_flow.py
+++ b/homeassistant/components/roomba/config_flow.py
@@ -9,31 +9,30 @@
from homeassistant import config_entries, core
from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS
-from homeassistant.const import CONF_HOST, CONF_PASSWORD
+from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_NAME, CONF_PASSWORD
from homeassistant.core import callback
from . import CannotConnect, async_connect_or_timeout, async_disconnect_or_timeout
from .const import (
CONF_BLID,
CONF_CONTINUOUS,
- CONF_DELAY,
- CONF_NAME,
DEFAULT_CONTINUOUS,
DEFAULT_DELAY,
+ DOMAIN,
ROOMBA_SESSION,
)
-from .const import DOMAIN # pylint:disable=unused-import
ROOMBA_DISCOVERY_LOCK = "roomba_discovery_lock"
+ALL_ATTEMPTS = 2
+HOST_ATTEMPTS = 6
+ROOMBA_WAKE_TIME = 6
DEFAULT_OPTIONS = {CONF_CONTINUOUS: DEFAULT_CONTINUOUS, CONF_DELAY: DEFAULT_DELAY}
MAX_NUM_DEVICES_TO_DISCOVER = 25
AUTH_HELP_URL_KEY = "auth_help_url"
-AUTH_HELP_URL_VALUE = (
- "https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials"
-)
+AUTH_HELP_URL_VALUE = "https://www.home-assistant.io/integrations/roomba/#manually-retrieving-your-credentials"
async def validate_input(hass: core.HomeAssistant, data):
@@ -45,11 +44,13 @@ async def validate_input(hass: core.HomeAssistant, data):
address=data[CONF_HOST],
blid=data[CONF_BLID],
password=data[CONF_PASSWORD],
- continuous=data[CONF_CONTINUOUS],
+ continuous=False,
delay=data[CONF_DELAY],
)
info = await async_connect_or_timeout(hass, roomba)
+ if info:
+ await async_disconnect_or_timeout(hass, roomba)
return {
ROOMBA_SESSION: info[ROOMBA_SESSION],
@@ -82,18 +83,26 @@ async def async_step_dhcp(self, dhcp_discovery):
if self._async_host_already_configured(dhcp_discovery[IP_ADDRESS]):
return self.async_abort(reason="already_configured")
- if not dhcp_discovery[HOSTNAME].startswith("iRobot-"):
+ if not dhcp_discovery[HOSTNAME].startswith(("irobot-", "roomba-")):
return self.async_abort(reason="not_irobot_device")
- blid = _async_blid_from_hostname(dhcp_discovery[HOSTNAME])
- await self.async_set_unique_id(blid)
- self._abort_if_unique_id_configured(
- updates={CONF_HOST: dhcp_discovery[IP_ADDRESS]}
- )
-
self.host = dhcp_discovery[IP_ADDRESS]
- self.blid = blid
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
+ self.blid = _async_blid_from_hostname(dhcp_discovery[HOSTNAME])
+ await self.async_set_unique_id(self.blid)
+ self._abort_if_unique_id_configured(updates={CONF_HOST: self.host})
+
+ # Because the hostname is so long some sources may
+ # truncate the hostname since it will be longer than
+ # the valid allowed length. If we already have a flow
+ # going for a longer hostname we abort so the user
+ # does not see two flows if discovery fails.
+ for progress in self._async_in_progress():
+ flow_unique_id = progress["context"]["unique_id"]
+ if flow_unique_id.startswith(self.blid):
+ return self.async_abort(reason="short_blid")
+ if self.blid.startswith(flow_unique_id):
+ self.hass.config_entries.flow.async_abort(progress["flow_id"])
+
self.context["title_placeholders"] = {"host": self.host, "name": self.blid}
return await self.async_step_user()
@@ -121,10 +130,8 @@ async def async_step_user(self, user_input=None):
return await self._async_start_link()
already_configured = self._async_current_ids(False)
- discovery = _async_get_roomba_discovery()
- async with self.hass.data.setdefault(ROOMBA_DISCOVERY_LOCK, asyncio.Lock()):
- devices = await self.hass.async_add_executor_job(discovery.get_all)
+ devices = await _async_discover_roombas(self.hass, self.host)
if devices:
# Find already configured hosts
@@ -133,14 +140,14 @@ async def async_step_user(self, user_input=None):
for device in devices
if device.blid not in already_configured
}
- if self.host and self.host in self.discovered_robots:
- # From discovery
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
- self.context["title_placeholders"] = {
- "host": self.host,
- "name": self.discovered_robots[self.host].robot_name,
- }
- return await self._async_start_link()
+
+ if self.host and self.host in self.discovered_robots:
+ # From discovery
+ self.context["title_placeholders"] = {
+ "host": self.host,
+ "name": self.discovered_robots[self.host].robot_name,
+ }
+ return await self._async_start_link()
if not self.discovered_robots:
return await self.async_step_manual()
@@ -184,7 +191,7 @@ async def async_step_manual(self, user_input=None):
return self.async_abort(reason="already_configured")
self.host = user_input[CONF_HOST]
- self.blid = user_input[CONF_BLID]
+ self.blid = user_input[CONF_BLID].upper()
await self.async_set_unique_id(self.blid, raise_on_progress=False)
self._abort_if_unique_id_configured()
return await self.async_step_link()
@@ -201,11 +208,11 @@ async def async_step_link(self, user_input=None):
description_placeholders={CONF_NAME: self.name or self.blid},
)
+ roomba_pw = RoombaPassword(self.host)
+
try:
- password = await self.hass.async_add_executor_job(
- RoombaPassword(self.host).get_password
- )
- except ConnectionRefusedError:
+ password = await self.hass.async_add_executor_job(roomba_pw.get_password)
+ except (OSError, ConnectionRefusedError):
return await self.async_step_link_manual()
if not password:
@@ -224,7 +231,6 @@ async def async_step_link(self, user_input=None):
except CannotConnect:
return self.async_abort(reason="cannot_connect")
- await async_disconnect_or_timeout(self.hass, info[ROOMBA_SESSION])
self.name = info[CONF_NAME]
return self.async_create_entry(title=self.name, data=config)
@@ -246,7 +252,6 @@ async def async_step_link_manual(self, user_input=None):
errors = {"base": "cannot_connect"}
if not errors:
- await async_disconnect_or_timeout(self.hass, info[ROOMBA_SESSION])
return self.async_create_entry(title=info[CONF_NAME], data=config)
return self.async_show_form(
@@ -309,4 +314,41 @@ def _async_get_roomba_discovery():
@callback
def _async_blid_from_hostname(hostname):
"""Extract the blid from the hostname."""
- return hostname.split("-")[1].split(".")[0]
+ return hostname.split("-")[1].split(".")[0].upper()
+
+
+async def _async_discover_roombas(hass, host):
+ discovered_hosts = set()
+ devices = []
+ discover_lock = hass.data.setdefault(ROOMBA_DISCOVERY_LOCK, asyncio.Lock())
+ discover_attempts = HOST_ATTEMPTS if host else ALL_ATTEMPTS
+
+ for attempt in range(discover_attempts + 1):
+ async with discover_lock:
+ discovery = _async_get_roomba_discovery()
+ try:
+ if host:
+ discovered = [
+ await hass.async_add_executor_job(discovery.get, host)
+ ]
+ else:
+ discovered = await hass.async_add_executor_job(discovery.get_all)
+ except OSError:
+ # Socket temporarily unavailable
+ await asyncio.sleep(ROOMBA_WAKE_TIME * attempt)
+ continue
+ else:
+ for device in discovered:
+ if device.ip in discovered_hosts:
+ continue
+ discovered_hosts.add(device.ip)
+ devices.append(device)
+ finally:
+ discovery.server_socket.close()
+
+ if host and host in discovered_hosts:
+ return devices
+
+ await asyncio.sleep(ROOMBA_WAKE_TIME)
+
+ return devices
diff --git a/homeassistant/components/roomba/const.py b/homeassistant/components/roomba/const.py
index 06684e63bdc238..0509cd92116278 100644
--- a/homeassistant/components/roomba/const.py
+++ b/homeassistant/components/roomba/const.py
@@ -1,10 +1,8 @@
"""The roomba constants."""
DOMAIN = "roomba"
-COMPONENTS = ["sensor", "binary_sensor", "vacuum"]
+PLATFORMS = ["sensor", "binary_sensor", "vacuum"]
CONF_CERT = "certificate"
CONF_CONTINUOUS = "continuous"
-CONF_DELAY = "delay"
-CONF_NAME = "name"
CONF_BLID = "blid"
DEFAULT_CERT = "/etc/ssl/certs/ca-certificates.crt"
DEFAULT_CONTINUOUS = True
diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py
index 7dd045a1137337..9d6a0f5cafc531 100644
--- a/homeassistant/components/roomba/irobot_base.py
+++ b/homeassistant/components/roomba/irobot_base.py
@@ -168,7 +168,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
state = self.vacuum_state
diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json
index 5ceb44ff780bd8..d1858a46fdc9d5 100644
--- a/homeassistant/components/roomba/manifest.json
+++ b/homeassistant/components/roomba/manifest.json
@@ -5,5 +5,15 @@
"documentation": "https://www.home-assistant.io/integrations/roomba",
"requirements": ["roombapy==1.6.2"],
"codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"],
- "dhcp": [{"hostname":"irobot-*","macaddress":"501479*"}]
+ "dhcp": [
+ {
+ "hostname" : "irobot-*",
+ "macaddress" : "501479*"
+ },
+ {
+ "hostname" : "roomba-*",
+ "macaddress" : "80A589*"
+ }
+ ]
}
+
diff --git a/homeassistant/components/roomba/roomba.py b/homeassistant/components/roomba/roomba.py
index 0a9aec0b60832c..5f960aeaae0e33 100644
--- a/homeassistant/components/roomba/roomba.py
+++ b/homeassistant/components/roomba/roomba.py
@@ -23,9 +23,9 @@ class RoombaVacuum(IRobotVacuum):
"""Basic Roomba robot (without carpet boost)."""
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
- state_attrs = super().device_state_attributes
+ state_attrs = super().extra_state_attributes
# Get bin state
bin_raw_state = self.vacuum_state.get("bin", {})
diff --git a/homeassistant/components/roomba/sensor.py b/homeassistant/components/roomba/sensor.py
index f2d08f087720c9..4a99d9f71af188 100644
--- a/homeassistant/components/roomba/sensor.py
+++ b/homeassistant/components/roomba/sensor.py
@@ -1,4 +1,5 @@
"""Sensor for checking the battery level of Roomba."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.components.vacuum import STATE_DOCKED
from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE
from homeassistant.helpers.icon import icon_for_battery_level
@@ -16,7 +17,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities([roomba_vac], True)
-class RoombaBattery(IRobotEntity):
+class RoombaBattery(IRobotEntity, SensorEntity):
"""Class to hold Roomba Sensor basic info."""
@property
diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json
index 512e27a758e36d..16371041a15a65 100644
--- a/homeassistant/components/roomba/strings.json
+++ b/homeassistant/components/roomba/strings.json
@@ -3,7 +3,7 @@
"flow_title": "iRobot {name} ({host})",
"step": {
"init": {
- "title": "Automaticlly connect to the device",
+ "title": "Automatically connect to the device",
"description": "Select a Roomba or Braava.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
@@ -11,7 +11,7 @@
},
"manual": {
"title": "Manually connect to the device",
- "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-`. Please follow the steps outlined in the documentation at: {auth_help_url}",
+ "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-` or `Roomba-`. Please follow the steps outlined in the documentation at: {auth_help_url}",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"blid": "BLID"
@@ -19,11 +19,11 @@
},
"link": {
"title": "Retrieve Password",
- "description": "Press and hold the Home button on {name} until the device generates a sound (about two seconds)."
+ "description": "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 retrivied 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 follow the steps outlined in the documentation at: {auth_help_url}",
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
@@ -35,7 +35,8 @@
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "not_irobot_device": "Discovered device is not an iRobot device"
+ "not_irobot_device": "Discovered device is not an iRobot device",
+ "short_blid": "The BLID was truncated"
}
},
"options": {
diff --git a/homeassistant/components/roomba/translations/bg.json b/homeassistant/components/roomba/translations/bg.json
new file mode 100644
index 00000000000000..10f778472e6321
--- /dev/null
+++ b/homeassistant/components/roomba/translations/bg.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "link_manual": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u0430"
+ },
+ "title": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/roomba/translations/ca.json b/homeassistant/components/roomba/translations/ca.json
index af358678144949..3bdf842df9b66b 100644
--- a/homeassistant/components/roomba/translations/ca.json
+++ b/homeassistant/components/roomba/translations/ca.json
@@ -1,9 +1,42 @@
{
"config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "not_irobot_device": "El dispositiu descobert no \u00e9s un dispositiu iRobot",
+ "short_blid": "El BLID s'ha truncat"
+ },
"error": {
"cannot_connect": "Ha fallat la connexi\u00f3"
},
+ "flow_title": "iRobot {name} ({host})",
"step": {
+ "init": {
+ "data": {
+ "host": "Amfitri\u00f3"
+ },
+ "description": "Selecciona un/a Roomba o Braava.",
+ "title": "Connexi\u00f3 autom\u00e0tica amb el dispositiu"
+ },
+ "link": {
+ "description": "Mant\u00e9 premut el bot\u00f3 d'inici a {name} fins que el dispositiu emeti un so (aproximadament dos segons) despr\u00e9s, envia en els seg\u00fcents 30 segons.",
+ "title": "Recupera la contrasenya"
+ },
+ "link_manual": {
+ "data": {
+ "password": "Contrasenya"
+ },
+ "description": "No s'ha pogut obtenir la contrasenya del dispositiu autom\u00e0ticament. Segueix els passos de la seg\u00fcent documentaci\u00f3: {auth_help_url}",
+ "title": "Introdueix contrasenya"
+ },
+ "manual": {
+ "data": {
+ "blid": "BLID",
+ "host": "Amfitri\u00f3"
+ },
+ "description": "No s'ha descobert cap Roomba ni cap Braava a la teva xarxa. El BLID \u00e9s la part del nom d'amfitri\u00f3 del dispositiu despr\u00e9s de `iRobot-` o `Roomba-`. Segueix els passos de la documentaci\u00f3 seg\u00fcent: {auth_help_url}",
+ "title": "Connecta't al dispositiu manualment"
+ },
"user": {
"data": {
"blid": "BLID",
diff --git a/homeassistant/components/roomba/translations/cs.json b/homeassistant/components/roomba/translations/cs.json
index fdf4aff22c90a2..d94d39f8136371 100644
--- a/homeassistant/components/roomba/translations/cs.json
+++ b/homeassistant/components/roomba/translations/cs.json
@@ -1,9 +1,29 @@
{
"config": {
+ "abort": {
+ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno",
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit"
+ },
"error": {
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit"
},
+ "flow_title": "iRobot {name} ({host})",
"step": {
+ "init": {
+ "data": {
+ "host": "Hostitel"
+ }
+ },
+ "link_manual": {
+ "data": {
+ "password": "Heslo"
+ }
+ },
+ "manual": {
+ "data": {
+ "host": "Hostitel"
+ }
+ },
"user": {
"data": {
"delay": "Zpo\u017ed\u011bn\u00ed",
diff --git a/homeassistant/components/roomba/translations/de.json b/homeassistant/components/roomba/translations/de.json
index 2f6ef37d13c179..780d406bcafbc6 100644
--- a/homeassistant/components/roomba/translations/de.json
+++ b/homeassistant/components/roomba/translations/de.json
@@ -1,9 +1,40 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "not_irobot_device": "Das erkannte Ger\u00e4t ist kein iRobot-Ger\u00e4t"
+ },
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
+ "flow_title": "iRobot {name} ({host})",
"step": {
+ "init": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "W\u00e4hle einen Roomba oder Braava aus.",
+ "title": "Automatisch mit dem Ger\u00e4t verbinden"
+ },
+ "link": {
+ "description": "Halte die Home-Taste von {name} gedr\u00fcckt, bis das Ger\u00e4t einen Ton erzeugt (ca. zwei Sekunden).",
+ "title": "Passwort abrufen"
+ },
+ "link_manual": {
+ "data": {
+ "password": "Passwort"
+ },
+ "description": "Das Passwort konnte nicht automatisch vom Ger\u00e4t abgerufen werden. Bitte die in der Dokumentation beschriebenen Schritte unter {auth_help_url} befolgen",
+ "title": "Passwort eingeben"
+ },
+ "manual": {
+ "data": {
+ "blid": "BLID",
+ "host": "Host"
+ },
+ "title": "Manuell mit dem Ger\u00e4t verbinden"
+ },
"user": {
"data": {
"blid": "BLID",
diff --git a/homeassistant/components/roomba/translations/en.json b/homeassistant/components/roomba/translations/en.json
index 276ab4a92a2e4d..5cc06f0cb5d368 100644
--- a/homeassistant/components/roomba/translations/en.json
+++ b/homeassistant/components/roomba/translations/en.json
@@ -1,51 +1,63 @@
{
- "config": {
- "flow_title": "iRobot {name} ({host})",
- "step": {
- "init": {
- "title": "Automaticlly connect to the device",
- "description": "Select a Roomba or Braava.",
- "data": {
- "host": "[%key:common::config_flow::data::host%]"
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured",
+ "cannot_connect": "Failed to connect",
+ "not_irobot_device": "Discovered device is not an iRobot device",
+ "short_blid": "The BLID was truncated"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect"
+ },
+ "flow_title": "iRobot {name} ({host})",
+ "step": {
+ "init": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Select a Roomba or Braava.",
+ "title": "Automatically connect to the device"
+ },
+ "link": {
+ "description": "Press and hold the Home button on {name} until the device generates a sound (about two seconds), then submit within 30 seconds.",
+ "title": "Retrieve Password"
+ },
+ "link_manual": {
+ "data": {
+ "password": "Password"
+ },
+ "description": "The password could not be retrieved from the device automatically. Please follow the steps outlined in the documentation at: {auth_help_url}",
+ "title": "Enter Password"
+ },
+ "manual": {
+ "data": {
+ "blid": "BLID",
+ "host": "Host"
+ },
+ "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-` or `Roomba-`. Please follow the steps outlined in the documentation at: {auth_help_url}",
+ "title": "Manually connect to the device"
+ },
+ "user": {
+ "data": {
+ "blid": "BLID",
+ "continuous": "Continuous",
+ "delay": "Delay",
+ "host": "Host",
+ "password": "Password"
+ },
+ "description": "Currently retrieving the BLID and password is a manual process. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
+ "title": "Connect to the device"
+ }
}
- },
- "manual": {
- "title": "Manually connect to the device",
- "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-`. Please follow the steps outlined in the documentation at: {auth_help_url}",
- "data": {
- "host": "[%key:common::config_flow::data::host%]",
- "blid": "BLID"
- }
- },
- "link": {
- "title": "Retrieve Password",
- "description": "Press and hold the Home button on {name} until the device generates a sound (about two seconds)."
- },
- "link_manual": {
- "title": "Enter Password",
- "description": "The password could not be retrivied from the device automatically. Please follow the steps outlined in the documentation at: {auth_help_url}",
- "data": {
- "password": "[%key:common::config_flow::data::password%]"
- }
- }
- },
- "error": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
- "abort": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "not_irobot_device": "Discovered device not an iRobot device"
- }
- },
- "options": {
- "step": {
- "init": {
- "data": {
- "continuous": "Continuous",
- "delay": "Delay"
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "continuous": "Continuous",
+ "delay": "Delay"
+ }
+ }
}
- }
}
- }
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/roomba/translations/es.json b/homeassistant/components/roomba/translations/es.json
index a49022c2d3d39d..29f0b47a655eb2 100644
--- a/homeassistant/components/roomba/translations/es.json
+++ b/homeassistant/components/roomba/translations/es.json
@@ -1,9 +1,41 @@
{
"config": {
+ "abort": {
+ "already_configured": "El dispositivo ya est\u00e1 configurado",
+ "cannot_connect": "No se pudo conectar",
+ "not_irobot_device": "El dispositivo descubierto no es un dispositivo iRobot"
+ },
"error": {
"cannot_connect": "No se pudo conectar"
},
+ "flow_title": "iRobot {name} ({host})",
"step": {
+ "init": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Selecciona una Roomba o Braava.",
+ "title": "Conectar autom\u00e1ticamente con el dispositivo"
+ },
+ "link": {
+ "description": "Mant\u00e9n pulsado el bot\u00f3n Inicio en {name} hasta que el dispositivo genere un sonido (aproximadamente dos segundos).",
+ "title": "Recuperar la contrase\u00f1a"
+ },
+ "link_manual": {
+ "data": {
+ "password": "Contrase\u00f1a"
+ },
+ "description": "No se pudo recuperar la contrase\u00f1a desde el dispositivo de forma autom\u00e1tica. Por favor, sigue los pasos descritos en la documentaci\u00f3n en: {auth_help_url}",
+ "title": "Escribe la contrase\u00f1a"
+ },
+ "manual": {
+ "data": {
+ "blid": "BLID",
+ "host": "Host"
+ },
+ "description": "No se ha descubierto ning\u00fan dispositivo Roomba ni Braava en tu red. El BLID es la parte del nombre de host del dispositivo despu\u00e9s de 'iRobot-'. Por favor, sigue los pasos descritos en la documentaci\u00f3n en: {auth_help_url}",
+ "title": "Conectar manualmente con el dispositivo"
+ },
"user": {
"data": {
"blid": "BLID",
diff --git a/homeassistant/components/roomba/translations/et.json b/homeassistant/components/roomba/translations/et.json
index 92da58fa146329..7943df95b4f2d6 100644
--- a/homeassistant/components/roomba/translations/et.json
+++ b/homeassistant/components/roomba/translations/et.json
@@ -1,9 +1,42 @@
{
"config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "cannot_connect": "\u00dchendamine nurjus",
+ "not_irobot_device": "Leitud seade ei ole iRoboti seade",
+ "short_blid": "BLID'i k\u00e4rbiti"
+ },
"error": {
"cannot_connect": "\u00dchendamine nurjus"
},
+ "flow_title": "iRobot {name} ( {host} )",
"step": {
+ "init": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Vali Roomba v\u00f5i Braava seade.",
+ "title": "\u00dchenda seadmega automaatselt"
+ },
+ "link": {
+ "description": "Vajuta ja hoia all seadme {name} nuppu Home kuni seade teeb piiksu (umbes kaks sekundit), edasta 30 sekundi jooksul.",
+ "title": "Hangi salas\u00f5na"
+ },
+ "link_manual": {
+ "data": {
+ "password": "Salas\u00f5na"
+ },
+ "description": "Salas\u00f5na ei \u00f5nnestunud seadmest automaatselt hankida. J\u00e4rgi dokumentatsioonis toodud juhiseid: {auth_help_url}",
+ "title": "Sisesta salas\u00f5na"
+ },
+ "manual": {
+ "data": {
+ "blid": "",
+ "host": "Host"
+ },
+ "description": "V\u00f5rgus ei tuvastatud \u00fchtegi Roomba ega Braava seadet. BLID on seadme hostinime osa p\u00e4rast 'iRobot-` v\u00f5i 'Roomba-'. J\u00e4rgi dokumentatsioonis toodud juhiseid: {auth_help_url}",
+ "title": "\u00dchenda seadmega k\u00e4sitsi"
+ },
"user": {
"data": {
"blid": "",
diff --git a/homeassistant/components/roomba/translations/fr.json b/homeassistant/components/roomba/translations/fr.json
index 1ec97dd3842e08..1f0e0b029c0c4a 100644
--- a/homeassistant/components/roomba/translations/fr.json
+++ b/homeassistant/components/roomba/translations/fr.json
@@ -1,9 +1,42 @@
{
"config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ",
+ "cannot_connect": "Echec de connection",
+ "not_irobot_device": "L'appareil d\u00e9couvert n'est pas un appareil iRobot",
+ "short_blid": "La BLID a \u00e9t\u00e9 tronqu\u00e9"
+ },
"error": {
"cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer"
},
+ "flow_title": "iRobot {name} ( {host} )",
"step": {
+ "init": {
+ "data": {
+ "host": "H\u00f4te"
+ },
+ "description": "S\u00e9lectionnez un Roomba ou un Braava.",
+ "title": "Se connecter automatiquement \u00e0 l'appareil"
+ },
+ "link": {
+ "description": "Appuyez sur le bouton Accueil et maintenez-le enfonc\u00e9 jusqu'\u00e0 ce que l'appareil \u00e9mette un son (environ deux secondes).",
+ "title": "R\u00e9cup\u00e9rer le mot de passe"
+ },
+ "link_manual": {
+ "data": {
+ "password": "Mot de passe"
+ },
+ "description": "Le mot de passe n'a pas pu \u00eatre r\u00e9cup\u00e9r\u00e9 automatiquement \u00e0 partir de l'appareil. Veuillez suivre les \u00e9tapes d\u00e9crites dans la documentation \u00e0 {auth_help_url}",
+ "title": "Entrer le mot de passe"
+ },
+ "manual": {
+ "data": {
+ "blid": "BLID",
+ "host": "H\u00f4te"
+ },
+ "description": "Aucun Roomba ou Braava d\u00e9couvert sur votre r\u00e9seau. Le BLID est la partie du nom d'h\u00f4te du p\u00e9riph\u00e9rique apr\u00e8s `iRobot-`. Veuillez suivre les \u00e9tapes d\u00e9crites dans la documentation \u00e0 {auth_help_url}",
+ "title": "Se connecter manuellement \u00e0 l'appareil"
+ },
"user": {
"data": {
"blid": "BLID",
diff --git a/homeassistant/components/roomba/translations/he.json b/homeassistant/components/roomba/translations/he.json
new file mode 100644
index 00000000000000..3007c0e968c1dc
--- /dev/null
+++ b/homeassistant/components/roomba/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json
index 357ca74746dfa5..931671f92d210d 100644
--- a/homeassistant/components/roomba/translations/hu.json
+++ b/homeassistant/components/roomba/translations/hu.json
@@ -1,10 +1,52 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "not_irobot_device": "A felfedezett eszk\u00f6z nem iRobot eszk\u00f6z",
+ "short_blid": "fel lett oldva"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "flow_title": "iRobot {name} ({host})",
"step": {
+ "init": {
+ "data": {
+ "host": "Hoszt"
+ },
+ "title": "Automatikus csatlakoz\u00e1s az eszk\u00f6zh\u00f6z"
+ },
+ "link_manual": {
+ "data": {
+ "password": "Jelsz\u00f3"
+ },
+ "title": "Jelsz\u00f3 megad\u00e1sa"
+ },
+ "manual": {
+ "data": {
+ "blid": "BLID",
+ "host": "Hoszt"
+ },
+ "title": "Manu\u00e1lis csatlakoz\u00e1s az eszk\u00f6zh\u00f6z"
+ },
"user": {
"data": {
+ "blid": "BLID",
+ "delay": "K\u00e9sleltet\u00e9s",
"host": "Hoszt",
"password": "Jelsz\u00f3"
+ },
+ "title": "Csatlakoz\u00e1s az eszk\u00f6zh\u00f6z"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "continuous": "Folyamatos",
+ "delay": "K\u00e9sleltet\u00e9s"
}
}
}
diff --git a/homeassistant/components/roomba/translations/id.json b/homeassistant/components/roomba/translations/id.json
new file mode 100644
index 00000000000000..3afe75ae09da8e
--- /dev/null
+++ b/homeassistant/components/roomba/translations/id.json
@@ -0,0 +1,62 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung",
+ "not_irobot_device": "Perangkat yang ditemukan bukan perangkat iRobot"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "flow_title": "iRobot {name} ({host})",
+ "step": {
+ "init": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Pilih Roomba atau Braava.",
+ "title": "Sambungkan secara otomatis ke perangkat"
+ },
+ "link": {
+ "description": "Tekan dan tahan tombol Home pada {name} hingga perangkat mengeluarkan suara (sekitar dua detik).",
+ "title": "Ambil Kata Sandi"
+ },
+ "link_manual": {
+ "data": {
+ "password": "Kata Sandi"
+ },
+ "description": "Kata sandi tidak dapat diambil dari perangkat secara otomatis. Ikuti langkah-langkah yang diuraikan dalam dokumentasi di: {auth_help_url}",
+ "title": "Masukkan Kata Sandi"
+ },
+ "manual": {
+ "data": {
+ "blid": "BLID",
+ "host": "Host"
+ },
+ "description": "Tidak ada Roomba atau Braava yang ditemukan di jaringan Anda. BLID adalah bagian dari nama host perangkat setelah `iRobot-`. Ikuti langkah-langkah yang diuraikan dalam dokumentasi di: {auth_help_url}",
+ "title": "Hubungkan ke perangkat secara manual"
+ },
+ "user": {
+ "data": {
+ "blid": "BLID",
+ "continuous": "Terus menerus",
+ "delay": "Tunda",
+ "host": "Host",
+ "password": "Kata Sandi"
+ },
+ "description": "Saat ini proses mengambil BLID dan kata sandi merupakan proses manual. Iikuti langkah-langkah yang diuraikan dalam dokumentasi di: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
+ "title": "Hubungkan ke perangkat"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "continuous": "Terus menerus",
+ "delay": "Tunda"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/roomba/translations/it.json b/homeassistant/components/roomba/translations/it.json
index d109aa8bcc099c..5e2e2b47141b1d 100644
--- a/homeassistant/components/roomba/translations/it.json
+++ b/homeassistant/components/roomba/translations/it.json
@@ -1,9 +1,42 @@
{
"config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "cannot_connect": "Impossibile connettersi",
+ "not_irobot_device": "Il dispositivo rilevato non \u00e8 un dispositivo iRobot",
+ "short_blid": "Il BLID \u00e8 stato troncato"
+ },
"error": {
"cannot_connect": "Impossibile connettersi"
},
+ "flow_title": "iRobot {name} ({host})",
"step": {
+ "init": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Seleziona un Roomba o un Braava.",
+ "title": "Connettiti automaticamente al dispositivo"
+ },
+ "link": {
+ "description": "Tieni premuto il pulsante Home su {name} fino a quando il dispositivo non genera un suono (circa due secondi), quindi invialo entro 30 secondi.",
+ "title": "Recupera password"
+ },
+ "link_manual": {
+ "data": {
+ "password": "Password"
+ },
+ "description": "La password non pu\u00f2 essere recuperata automaticamente dal dispositivo. Segui le istruzioni indicate sulla documentazione a: {auth_help_url}",
+ "title": "Inserisci la password"
+ },
+ "manual": {
+ "data": {
+ "blid": "BLID",
+ "host": "Host"
+ },
+ "description": "Nessun Roomba o Braava sono stati rilevati all'interno della tua rete. Il BLID \u00e8 la porzione del nome host del dispositivo dopo `iRobot-` o `Roomba-`. Segui i passaggi descritti nella documentazione all'indirizzo: {auth_help_url}",
+ "title": "Connettiti manualmente al dispositivo"
+ },
"user": {
"data": {
"blid": "BLID",
diff --git a/homeassistant/components/roomba/translations/ko.json b/homeassistant/components/roomba/translations/ko.json
index ebf9056c0379ec..bb33287c9b83e1 100644
--- a/homeassistant/components/roomba/translations/ko.json
+++ b/homeassistant/components/roomba/translations/ko.json
@@ -1,9 +1,42 @@
{
"config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "not_irobot_device": "\ubc1c\uacac\ub41c \uae30\uae30\ub294 \uc544\uc774\ub85c\ubd07 \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4",
+ "short_blid": "BLID\uac00 \uc798\ub838\uc2b5\ub2c8\ub2e4"
+ },
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694."
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
+ "flow_title": "\uc544\uc774\ub85c\ubd07: {name} ({host})",
"step": {
+ "init": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8"
+ },
+ "description": "\ub8f8\ubc14 \ub610\ub294 \ube0c\ub77c\ubc14\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.",
+ "title": "\uae30\uae30\uc5d0 \uc790\ub3d9\uc73c\ub85c \uc5f0\uacb0\ud558\uae30"
+ },
+ "link": {
+ "description": "\uae30\uae30\uc5d0\uc11c \uc18c\ub9ac\uac00 \ub0a0 \ub54c\uae4c\uc9c0 {name}\uc758 \ud648 \ubc84\ud2bc\uc744 \uae38\uac8c \ub204\ub978 \ub2e4\uc74c(\uc57d 2\ucd08) 30\ucd08 \uc774\ub0b4\uc5d0 \ud655\uc778 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.",
+ "title": "\ube44\ubc00\ubc88\ud638 \uac00\uc838\uc624\uae30"
+ },
+ "link_manual": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638"
+ },
+ "description": "\uae30\uae30\uc5d0\uc11c \ube44\ubc00\ubc88\ud638\ub97c \uc790\ub3d9\uc73c\ub85c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uad00\ub828 \ubb38\uc11c\uc5d0 \ub098\uc640 \uc788\ub294 {auth_help_url} \ub2e8\uacc4\ub97c \ub530\ub77c\uc8fc\uc138\uc694.",
+ "title": "\ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694"
+ },
+ "manual": {
+ "data": {
+ "blid": "BLID",
+ "host": "\ud638\uc2a4\ud2b8"
+ },
+ "description": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ub8f8\ubc14 \ub610\ub294 \ube0c\ub77c\ubc14\uac00 \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. BLID\ub294 `iRobot-` \ub610\ub294 `Roomba-` \ub4a4\uc5d0 \uc788\ub294 \uae30\uae30 \ud638\uc2a4\ud2b8 \uc774\ub984\uc758 \uc77c\ubd80\uc785\ub2c8\ub2e4. \uad00\ub828 \ubb38\uc11c\uc5d0 \uc124\uba85\ub41c \ub2e8\uacc4\ub97c \ub530\ub77c\uc8fc\uc138\uc694: {auth_help_url}",
+ "title": "\uae30\uae30\uc5d0 \uc218\ub3d9\uc73c\ub85c \uc5f0\uacb0\ud558\uae30"
+ },
"user": {
"data": {
"blid": "BLID",
@@ -12,7 +45,7 @@
"host": "\ud638\uc2a4\ud2b8",
"password": "\ube44\ubc00\ubc88\ud638"
},
- "description": "\ud604\uc7ac BLID \ubc0f \ube44\ubc00\ubc88\ud638\ub294 \uc218\ub3d9\uc73c\ub85c \uac00\uc838\uc640\uc57c\ud569\ub2c8\ub2e4. \ub2e4\uc74c \ubb38\uc11c\uc5d0 \uc124\uba85\ub41c \uc808\ucc28\ub97c \ub530\ub77c \uc124\uc815\ud574\uc8fc\uc138\uc694: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
+ "description": "\ud604\uc7ac BLID \ubc0f \ube44\ubc00\ubc88\ud638\ub294 \uc218\ub3d9\uc73c\ub85c \uac00\uc838\uc640\uc57c\ud569\ub2c8\ub2e4. \ub2e4\uc74c \uad00\ub828 \ubb38\uc11c\uc5d0 \ub098\uc640 \uc788\ub294 \ub2e8\uacc4\ub97c \ub530\ub77c\uc8fc\uc138\uc694: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
"title": "\uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
diff --git a/homeassistant/components/roomba/translations/lb.json b/homeassistant/components/roomba/translations/lb.json
index d3fc631f5dfedd..5578a7710dba71 100644
--- a/homeassistant/components/roomba/translations/lb.json
+++ b/homeassistant/components/roomba/translations/lb.json
@@ -1,9 +1,36 @@
{
"config": {
+ "abort": {
+ "cannot_connect": "Feeler beim verbannen"
+ },
"error": {
"cannot_connect": "Feeler beim verbannen"
},
+ "flow_title": "iRobot {name} ({host})",
"step": {
+ "init": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Ee Roomba oder Bravaa auswielen.",
+ "title": "Automatesch mam Apparat verbannen"
+ },
+ "link": {
+ "title": "Passwuert ausliesen"
+ },
+ "link_manual": {
+ "data": {
+ "password": "Passwuert"
+ },
+ "title": "Passwuert aginn"
+ },
+ "manual": {
+ "data": {
+ "blid": "BLID",
+ "host": "Host"
+ },
+ "title": "Manuell mam Apparat verbannen"
+ },
"user": {
"data": {
"blid": "BLID",
diff --git a/homeassistant/components/roomba/translations/nl.json b/homeassistant/components/roomba/translations/nl.json
index f5268bdf799471..2af6e13b13f34e 100644
--- a/homeassistant/components/roomba/translations/nl.json
+++ b/homeassistant/components/roomba/translations/nl.json
@@ -1,15 +1,48 @@
{
"config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd",
+ "cannot_connect": "Kan geen verbinding maken",
+ "not_irobot_device": "Het gevonden apparaat is geen iRobot-apparaat",
+ "short_blid": "De BLID is afgekapt"
+ },
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw"
+ "cannot_connect": "Kan geen verbinding maken"
},
+ "flow_title": "iRobot {name} ({host})",
"step": {
+ "init": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Kies een Roomba of Braava.",
+ "title": "Automatisch verbinding maken met het apparaat"
+ },
+ "link": {
+ "description": "Houd de Home-knop op {name} ingedrukt totdat het apparaat een geluid genereert (ongeveer twee seconden).",
+ "title": "Wachtwoord opvragen"
+ },
+ "link_manual": {
+ "data": {
+ "password": "Wachtwoord"
+ },
+ "description": "Het wachtwoord kon niet automatisch van het apparaat worden opgehaald. Volg de stappen zoals beschreven in de documentatie op: {auth_help_url}",
+ "title": "Voer wachtwoord in"
+ },
+ "manual": {
+ "data": {
+ "blid": "BLID",
+ "host": "Host"
+ },
+ "description": "Er is geen Roomba of Braava ontdekt op uw netwerk. De BLID is het gedeelte van de hostnaam van het apparaat na `iRobot-`. Volg de stappen die worden beschreven in de documentatie op: {auth_help_url}",
+ "title": "Handmatig verbinding maken met het apparaat"
+ },
"user": {
"data": {
"blid": "BLID",
"continuous": "Doorlopend",
"delay": "Vertraging",
- "host": "Hostnaam of IP-adres",
+ "host": "Host",
"password": "Wachtwoord"
},
"description": "Het ophalen van de BLID en het wachtwoord is momenteel een handmatig proces. Volg de stappen in de documentatie op: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
diff --git a/homeassistant/components/roomba/translations/no.json b/homeassistant/components/roomba/translations/no.json
index adf13cb57af879..4f051cfde3ff2b 100644
--- a/homeassistant/components/roomba/translations/no.json
+++ b/homeassistant/components/roomba/translations/no.json
@@ -1,9 +1,42 @@
{
"config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert",
+ "cannot_connect": "Tilkobling mislyktes",
+ "not_irobot_device": "Oppdaget enhet er ikke en iRobot-enhet",
+ "short_blid": "BLID ble avkortet"
+ },
"error": {
"cannot_connect": "Tilkobling mislyktes"
},
+ "flow_title": "",
"step": {
+ "init": {
+ "data": {
+ "host": "Vert"
+ },
+ "description": "Velg en Roomba eller Braava",
+ "title": "Koble automatisk til enheten"
+ },
+ "link": {
+ "description": "Trykk og hold nede Hjem-knappen p\u00e5 {name} til enheten genererer en lyd (omtrent to sekunder), og send deretter innen 30 sekunder.",
+ "title": "Hent passord"
+ },
+ "link_manual": {
+ "data": {
+ "password": "Passord"
+ },
+ "description": "Passordet kan ikke hentes fra enheten automatisk. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: {auth_help_url}",
+ "title": "Skriv inn passord"
+ },
+ "manual": {
+ "data": {
+ "blid": "",
+ "host": "Vert"
+ },
+ "description": "Ingen Roomba eller Braava har blitt oppdaget i nettverket ditt. BLID er delen av enhetens vertsnavn etter `iRobot-` eller `Roomba-`. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: {auth_help_url}",
+ "title": "Koble til enheten manuelt"
+ },
"user": {
"data": {
"blid": "Blid",
diff --git a/homeassistant/components/roomba/translations/pl.json b/homeassistant/components/roomba/translations/pl.json
index b2a4ab89cbecdc..863023f321b664 100644
--- a/homeassistant/components/roomba/translations/pl.json
+++ b/homeassistant/components/roomba/translations/pl.json
@@ -1,9 +1,42 @@
{
"config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "not_irobot_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem iRobot",
+ "short_blid": "BLID zosta\u0142 obci\u0119ty"
+ },
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
+ "flow_title": "iRobot {name} ({host})",
"step": {
+ "init": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP"
+ },
+ "description": "Wybierz Roomb\u0119 lub Braava",
+ "title": "Po\u0142\u0105cz si\u0119 automatycznie z urz\u0105dzeniem"
+ },
+ "link": {
+ "description": "Naci\u015bnij i przytrzymaj przycisk Home na {name} a\u017c urz\u0105dzenie wygeneruje d\u017awi\u0119k (oko\u0142o dwie sekundy), a nast\u0119pnie prze\u015blij w ci\u0105gu 30 sekund.",
+ "title": "Odzyskiwanie has\u0142a"
+ },
+ "link_manual": {
+ "data": {
+ "password": "Has\u0142o"
+ },
+ "description": "Nie mo\u017cna automatycznie pobra\u0107 has\u0142a z urz\u0105dzenia. Post\u0119puj zgodnie z instrukcjami podanymi w dokumentacji pod adresem: {auth_help_url}",
+ "title": "Wprowad\u017a has\u0142o"
+ },
+ "manual": {
+ "data": {
+ "blid": "BLID",
+ "host": "Nazwa hosta lub adres IP"
+ },
+ "description": "W Twojej sieci nie wykryto urz\u0105dzenia Roomba ani Braava. BLID to cz\u0119\u015b\u0107 nazwy hosta urz\u0105dzenia po `iRobot-` lub 'Roomba-'. Post\u0119puj zgodnie z instrukcjami podanymi w dokumentacji pod adresem: {auth_help_url}",
+ "title": "R\u0119czne po\u0142\u0105czenie z urz\u0105dzeniem"
+ },
"user": {
"data": {
"blid": "BLID",
diff --git a/homeassistant/components/roomba/translations/pt.json b/homeassistant/components/roomba/translations/pt.json
index 0156fd48a62666..6036e870e6c05d 100644
--- a/homeassistant/components/roomba/translations/pt.json
+++ b/homeassistant/components/roomba/translations/pt.json
@@ -1,9 +1,16 @@
{
"config": {
+ "abort": {
+ "not_irobot_device": "O dispositivo descoberto n\u00e3o \u00e9 um dispositivo iRobot"
+ },
"error": {
"cannot_connect": "Falha ao conectar, tente novamente"
},
+ "flow_title": "iRobot {name} ({host})",
"step": {
+ "link": {
+ "title": "Recuperar Palavra-passe"
+ },
"user": {
"data": {
"continuous": "Cont\u00ednuo",
diff --git a/homeassistant/components/roomba/translations/ru.json b/homeassistant/components/roomba/translations/ru.json
index ee1192f69ec63c..26e5c61faf3327 100644
--- a/homeassistant/components/roomba/translations/ru.json
+++ b/homeassistant/components/roomba/translations/ru.json
@@ -1,9 +1,42 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "not_irobot_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 iRobot.",
+ "short_blid": "BLID \u0431\u044b\u043b \u0443\u043a\u0430\u0437\u0430\u043d \u043d\u0435 \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e."
+ },
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
},
+ "flow_title": "iRobot {name} ({host})",
"step": {
+ "init": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u044b\u043b\u0435\u0441\u043e\u0441 \u0438\u0437 \u043c\u043e\u0434\u0435\u043b\u0435\u0439 Roomba \u0438\u043b\u0438 Braava.",
+ "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443"
+ },
+ "link": {
+ "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0438 \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0439\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 Home \u043d\u0430 {name}, \u043f\u043e\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0438\u0437\u0434\u0430\u0441\u0442 \u0437\u0432\u0443\u043a (\u043e\u043a\u043e\u043b\u043e \u0434\u0432\u0443\u0445 \u0441\u0435\u043a\u0443\u043d\u0434). \u0417\u0430\u0442\u0435\u043c \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 30 \u0441\u0435\u043a\u0443\u043d\u0434 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\".",
+ "title": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u043f\u0430\u0440\u043e\u043b\u044f"
+ },
+ "link_manual": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "description": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439: {auth_help_url}.",
+ "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "manual": {
+ "data": {
+ "blid": "BLID",
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "description": "\u0412 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u043f\u044b\u043b\u0435\u0441\u043e\u0441\u043e\u0432 Roomba \u0438\u043b\u0438 Braava. BLID - \u044d\u0442\u043e \u0447\u0430\u0441\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0433\u043e \u0438\u043c\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043f\u043e\u0441\u043b\u0435 `iRobot-` \u0438\u043b\u0438 `Roomba-`. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439: {auth_help_url}.",
+ "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 \u0432\u0440\u0443\u0447\u043d\u0443\u044e"
+ },
"user": {
"data": {
"blid": "BLID",
diff --git a/homeassistant/components/roomba/translations/tr.json b/homeassistant/components/roomba/translations/tr.json
new file mode 100644
index 00000000000000..3d85144c1883cb
--- /dev/null
+++ b/homeassistant/components/roomba/translations/tr.json
@@ -0,0 +1,60 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "not_irobot_device": "Bulunan cihaz bir iRobot cihaz\u0131 de\u011fil"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "flow_title": "iRobot {name} ( {host} )",
+ "step": {
+ "init": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ },
+ "description": "Roomba veya Braava'y\u0131 se\u00e7in.",
+ "title": "Cihaza otomatik olarak ba\u011flan"
+ },
+ "link": {
+ "description": "Cihaz bir ses olu\u015fturana kadar (yakla\u015f\u0131k iki saniye) {name} \u00fczerindeki Ana Sayfa d\u00fc\u011fmesini bas\u0131l\u0131 tutun.",
+ "title": "\u015eifre Al"
+ },
+ "link_manual": {
+ "data": {
+ "password": "\u015eifre"
+ },
+ "description": "Parola ayg\u0131ttan otomatik olarak al\u0131namad\u0131. L\u00fctfen belgelerde belirtilen ad\u0131mlar\u0131 izleyin: {auth_help_url}",
+ "title": "\u015eifre Girin"
+ },
+ "manual": {
+ "data": {
+ "blid": "BLID",
+ "host": "Ana Bilgisayar"
+ },
+ "title": "Cihaza manuel olarak ba\u011flan\u0131n"
+ },
+ "user": {
+ "data": {
+ "continuous": "S\u00fcrekli",
+ "delay": "Gecikme",
+ "host": "Ana Bilgisayar",
+ "password": "Parola"
+ },
+ "description": "\u015eu anda BLID ve parola alma manuel bir i\u015flemdir. L\u00fctfen a\u015fa\u011f\u0131daki belgelerde belirtilen ad\u0131mlar\u0131 izleyin: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
+ "title": "Cihaza ba\u011flan\u0131n"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "continuous": "S\u00fcrekli",
+ "delay": "Gecikme"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/roomba/translations/uk.json b/homeassistant/components/roomba/translations/uk.json
new file mode 100644
index 00000000000000..833a35f62f3a38
--- /dev/null
+++ b/homeassistant/components/roomba/translations/uk.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "blid": "BLID",
+ "continuous": "\u0411\u0435\u0437\u043f\u0435\u0440\u0435\u0440\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c",
+ "delay": "\u0417\u0430\u0442\u0440\u0438\u043c\u043a\u0430 (\u0441\u0435\u043a.)",
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438, \u0449\u043e\u0431 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u044f\u043a \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 BLID \u0456 \u043f\u0430\u0440\u043e\u043b\u044c:\nhttps://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "continuous": "\u0411\u0435\u0437\u043f\u0435\u0440\u0435\u0440\u0432\u043d\u043e",
+ "delay": "\u0417\u0430\u0442\u0440\u0438\u043c\u043a\u0430"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/roomba/translations/zh-Hant.json b/homeassistant/components/roomba/translations/zh-Hant.json
index 932e5cadd7567d..830258ff2b654b 100644
--- a/homeassistant/components/roomba/translations/zh-Hant.json
+++ b/homeassistant/components/roomba/translations/zh-Hant.json
@@ -1,9 +1,42 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "not_irobot_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e iRobot \u88dd\u7f6e",
+ "short_blid": "BLID \u906d\u622a\u77ed"
+ },
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557"
},
+ "flow_title": "iRobot {name} ({host})",
"step": {
+ "init": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef"
+ },
+ "description": "\u9078\u64c7 Roomba \u6216 Braava\u3002",
+ "title": "\u81ea\u52d5\u9023\u7dda\u81f3\u88dd\u7f6e"
+ },
+ "link": {
+ "description": "\u8acb\u6309\u4f4f {name} \u4e0a\u7684 Home \u9375\u76f4\u5230\u88dd\u7f6e\u767c\u51fa\u8072\u97f3\uff08\u7d04\u5169\u79d2\uff09\uff0c\u7136\u5f8c\u65bc 30 \u79d2\u5167\u50b3\u9001\u3002",
+ "title": "\u91cd\u7f6e\u5bc6\u78bc"
+ },
+ "link_manual": {
+ "data": {
+ "password": "\u5bc6\u78bc"
+ },
+ "description": "\u5bc6\u78bc\u53ef\u81ea\u52d5\u81ea\u88dd\u7f6e\u4e0a\u53d6\u5f97\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1a{auth_help_url}",
+ "title": "\u8f38\u5165\u5bc6\u78bc"
+ },
+ "manual": {
+ "data": {
+ "blid": "BLID",
+ "host": "\u4e3b\u6a5f\u7aef"
+ },
+ "description": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Roomba \u6216 Braava\u3002BLID \u88dd\u7f6e\u65bc\u4e3b\u6a5f\u7aef\u7684\u90e8\u5206\u540d\u7a31\u70ba `iRobot-` \u6216 `Roomba-` \u958b\u982d\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1a{auth_help_url}",
+ "title": "\u624b\u52d5\u9023\u7dda\u81f3\u88dd\u7f6e"
+ },
"user": {
"data": {
"blid": "BLID",
diff --git a/homeassistant/components/roon/config_flow.py b/homeassistant/components/roon/config_flow.py
index c6ed443698186c..b1db9e39a25a04 100644
--- a/homeassistant/components/roon/config_flow.py
+++ b/homeassistant/components/roon/config_flow.py
@@ -8,7 +8,7 @@
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_API_KEY, CONF_HOST
-from .const import ( # pylint: disable=unused-import
+from .const import (
AUTHENTICATE_TIMEOUT,
CONF_ROON_ID,
DEFAULT_NAME,
diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json
index 0d5d0c131ae491..e4c4a25dcb540e 100644
--- a/homeassistant/components/roon/manifest.json
+++ b/homeassistant/components/roon/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/roon",
"requirements": [
- "roonapi==0.0.31"
+ "roonapi==0.0.32"
],
"codeowners": [
"@pavoni"
diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py
index 8abcba189daf8e..773028da2d3073 100644
--- a/homeassistant/components/roon/media_player.py
+++ b/homeassistant/components/roon/media_player.py
@@ -175,7 +175,7 @@ def device_info(self):
"name": self.name,
"manufacturer": "RoonLabs",
"model": dev_model,
- "via_hub": (DOMAIN, self._server.roon_id),
+ "via_device": (DOMAIN, self._server.roon_id),
}
def update_data(self, player_data=None):
@@ -465,7 +465,7 @@ def turn_off(self):
return
for source in self.player_data["source_controls"]:
- if source["supports_standby"] and not source["status"] == "indeterminate":
+ if source["supports_standby"] and source["status"] != "indeterminate":
self._server.roonapi.standby(self.output_id, source["control_key"])
return
diff --git a/homeassistant/components/roon/server.py b/homeassistant/components/roon/server.py
index d5b8d81c2aa5f4..83b620e176ea81 100644
--- a/homeassistant/components/roon/server.py
+++ b/homeassistant/components/roon/server.py
@@ -141,17 +141,6 @@ async def async_update_players(self):
async_dispatcher_send(self.hass, "roon_media_player", player_data)
self.offline_devices.add(dev_id)
- async def async_update_playlists(self):
- """Store lists in memory with all playlists - could be used by a custom lovelace card."""
- all_playlists = []
- roon_playlists = self.roonapi.playlists()
- if roon_playlists and "items" in roon_playlists:
- all_playlists += [item["title"] for item in roon_playlists["items"]]
- roon_playlists = self.roonapi.internet_radio()
- if roon_playlists and "items" in roon_playlists:
- all_playlists += [item["title"] for item in roon_playlists["items"]]
- self.all_playlists = all_playlists
-
async def async_create_player_data(self, zone, output):
"""Create player object dict by combining zone with output."""
new_dict = zone.copy()
diff --git a/homeassistant/components/roon/translations/ca.json b/homeassistant/components/roon/translations/ca.json
index 3a1de2208b6033..ef32dd00e75f86 100644
--- a/homeassistant/components/roon/translations/ca.json
+++ b/homeassistant/components/roon/translations/ca.json
@@ -17,7 +17,7 @@
"data": {
"host": "Amfitri\u00f3"
},
- "description": "Introdueix el nom d'amfitri\u00f3 o la IP del servidor Roon"
+ "description": "No s'ha pogut descobrir el servidor Roon, introdueix el nom d'amfitri\u00f3 o la IP."
}
}
}
diff --git a/homeassistant/components/roon/translations/cs.json b/homeassistant/components/roon/translations/cs.json
index a15e75066a9865..fd01ed1cd25777 100644
--- a/homeassistant/components/roon/translations/cs.json
+++ b/homeassistant/components/roon/translations/cs.json
@@ -16,8 +16,7 @@
"user": {
"data": {
"host": "Hostitel"
- },
- "description": "Zadejte pros\u00edm n\u00e1zev hostitele nebo IP adresu va\u0161eho Roon serveru."
+ }
}
}
}
diff --git a/homeassistant/components/roon/translations/de.json b/homeassistant/components/roon/translations/de.json
index 9918e38670acde..4416589a23ee45 100644
--- a/homeassistant/components/roon/translations/de.json
+++ b/homeassistant/components/roon/translations/de.json
@@ -1,8 +1,19 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
"error": {
"duplicate_entry": "Dieser Host wurde bereits hinzugef\u00fcgt.",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/roon/translations/en.json b/homeassistant/components/roon/translations/en.json
index 99f2b65bd13c21..b763fbb1e0c1ed 100644
--- a/homeassistant/components/roon/translations/en.json
+++ b/homeassistant/components/roon/translations/en.json
@@ -17,7 +17,7 @@
"data": {
"host": "Host"
},
- "description": "Please enter your Roon server Hostname or IP."
+ "description": "Could not discover Roon server, please enter your the Hostname or IP."
}
}
}
diff --git a/homeassistant/components/roon/translations/et.json b/homeassistant/components/roon/translations/et.json
index dfe3ad53f48d69..e29b1ccc6c6d1c 100644
--- a/homeassistant/components/roon/translations/et.json
+++ b/homeassistant/components/roon/translations/et.json
@@ -17,7 +17,7 @@
"data": {
"host": ""
},
- "description": "Sisesta oma Rooni serveri hostinimi v\u00f5i IP."
+ "description": "Rooni serverit ei leitud. Sisesta oma Rooni serveri hostinimi v\u00f5i IP."
}
}
}
diff --git a/homeassistant/components/roon/translations/hu.json b/homeassistant/components/roon/translations/hu.json
index 3b2d79a34a77e2..6ea0aa2b25c3ab 100644
--- a/homeassistant/components/roon/translations/hu.json
+++ b/homeassistant/components/roon/translations/hu.json
@@ -2,6 +2,18 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "duplicate_entry": "Ez a hoszt m\u00e1r konfigur\u00e1lva van.",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/roon/translations/id.json b/homeassistant/components/roon/translations/id.json
new file mode 100644
index 00000000000000..bfd70955ac86b8
--- /dev/null
+++ b/homeassistant/components/roon/translations/id.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "duplicate_entry": "Host ini telah ditambahkan.",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "link": {
+ "description": "Anda harus mengotorisasi Home Assistant di Roon. Setelah Anda mengeklik kirim, buka aplikasi Roon Core, buka Pengaturan dan aktifkan HomeAssistant pada tab Ekstensi.",
+ "title": "Otorisasi HomeAssistant di Roon"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Tidak dapat menemukan server Roon, masukkan Nama Host atau IP Anda."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/roon/translations/it.json b/homeassistant/components/roon/translations/it.json
index 5f63482c3c3937..e0450af9d39198 100644
--- a/homeassistant/components/roon/translations/it.json
+++ b/homeassistant/components/roon/translations/it.json
@@ -17,7 +17,7 @@
"data": {
"host": "Host"
},
- "description": "Inserisci il nome host o l'IP del tuo server Roon."
+ "description": "Impossibile individuare il server Roon, inserire l'hostname o l'IP."
}
}
}
diff --git a/homeassistant/components/roon/translations/ko.json b/homeassistant/components/roon/translations/ko.json
index ae483b0b09880c..b1e0fee4089211 100644
--- a/homeassistant/components/roon/translations/ko.json
+++ b/homeassistant/components/roon/translations/ko.json
@@ -10,14 +10,14 @@
},
"step": {
"link": {
- "description": "Roon\uc5d0\uc11c \ud648 \uc5b4\uc2dc\uc2a4\ud134\ud2b8\ub97c \uc778\uc99d\ud574\uc57c\ud569\ub2c8\ub2e4. \uc81c\ucd9c\uc744 \ud074\ub9ad \ud55c \ud6c4 Roon Core \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \uc124\uc815\uc744 \uc5f4\uace0 \ud655\uc7a5 \ud0ed\uc5d0\uc11c HomeAssistant\ub97c \ud65c\uc131\ud654\ud569\ub2c8\ub2e4.",
- "title": "Roon\uc5d0\uc11c HomeAssistant \uc778\uc99d"
+ "description": "Roon\uc5d0\uc11c Home Assistant\ub97c \uc778\uc99d\ud574\uc8fc\uc5b4\uc57c \ud569\ub2c8\ub2e4. \ud655\uc778\uc744 \ud074\ub9ad\ud55c \ud6c4 Roon Core \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \uc124\uc815\uc744 \uc5f4\uace0 \ud655\uc7a5 \ud0ed\uc5d0\uc11c Home Assistant\ub97c \ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694.",
+ "title": "Roon\uc5d0\uc11c HomeAssistant \uc778\uc99d\ud558\uae30"
},
"user": {
"data": {
"host": "\ud638\uc2a4\ud2b8"
},
- "description": "Roon \uc11c\ubc84 Hostname \ub610\ub294 IP\ub97c \uc785\ub825\ud558\uc2ed\uc2dc\uc624."
+ "description": "Roon \uc11c\ubc84\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694."
}
}
}
diff --git a/homeassistant/components/roon/translations/no.json b/homeassistant/components/roon/translations/no.json
index 9067e2c6f5331a..e872e03a69d20c 100644
--- a/homeassistant/components/roon/translations/no.json
+++ b/homeassistant/components/roon/translations/no.json
@@ -17,7 +17,7 @@
"data": {
"host": "Vert"
},
- "description": "Vennligst skriv inn Roon-serverens vertsnavn eller IP."
+ "description": "Kunne ikke oppdage Roon-serveren. Angi vertsnavnet eller IP-adressen."
}
}
}
diff --git a/homeassistant/components/roon/translations/pl.json b/homeassistant/components/roon/translations/pl.json
index e63c5f6b55c675..d763fc12bd26ad 100644
--- a/homeassistant/components/roon/translations/pl.json
+++ b/homeassistant/components/roon/translations/pl.json
@@ -17,7 +17,7 @@
"data": {
"host": "Nazwa hosta lub adres IP"
},
- "description": "Wprowad\u017a nazw\u0119 hosta lub adres IP swojego serwera Roon."
+ "description": "Nie wykryto serwera Roon, wprowad\u017a nazw\u0119 hosta lub adres IP."
}
}
}
diff --git a/homeassistant/components/roon/translations/ru.json b/homeassistant/components/roon/translations/ru.json
index abfbea2ccde294..c01006d6269663 100644
--- a/homeassistant/components/roon/translations/ru.json
+++ b/homeassistant/components/roon/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"duplicate_entry": "\u042d\u0442\u043e\u0442 \u0445\u043e\u0441\u0442 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
@@ -17,7 +17,7 @@
"data": {
"host": "\u0425\u043e\u0441\u0442"
},
- "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Roon"
+ "description": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 Roon, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441."
}
}
}
diff --git a/homeassistant/components/roon/translations/tr.json b/homeassistant/components/roon/translations/tr.json
new file mode 100644
index 00000000000000..97241919c9b5f1
--- /dev/null
+++ b/homeassistant/components/roon/translations/tr.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "duplicate_entry": "Bu ana bilgisayar zaten eklendi.",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "link": {
+ "description": "Roon'da HomeAssistant\u0131 yetkilendirmelisiniz. G\u00f6nder'e t\u0131klad\u0131ktan sonra, Roon Core uygulamas\u0131na gidin, Ayarlar'\u0131 a\u00e7\u0131n ve Uzant\u0131lar sekmesinde HomeAssistant'\u0131 etkinle\u015ftirin.",
+ "title": "Roon'da HomeAssistant'\u0131 Yetkilendirme"
+ },
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/roon/translations/uk.json b/homeassistant/components/roon/translations/uk.json
new file mode 100644
index 00000000000000..91a530787ae37b
--- /dev/null
+++ b/homeassistant/components/roon/translations/uk.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "duplicate_entry": "\u0426\u0435\u0439 \u0445\u043e\u0441\u0442 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0438\u0439.",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "link": {
+ "description": "\u041f\u0456\u0441\u043b\u044f \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f \u043a\u043d\u043e\u043f\u043a\u0438 \u00ab\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438\u00bb \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0432 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Roon Core, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u00ab\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u00bb \u0456 \u0443\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c HomeAssistant \u043d\u0430 \u0432\u043a\u043b\u0430\u0434\u0446\u0456 \u00ab\u0420\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u043d\u044f\u00bb.",
+ "title": "Roon"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043d\u0430\u0437\u0432\u0443 \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Roon"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/roon/translations/zh-Hant.json b/homeassistant/components/roon/translations/zh-Hant.json
index f34bce445f74a9..39099753f39ed7 100644
--- a/homeassistant/components/roon/translations/zh-Hant.json
+++ b/homeassistant/components/roon/translations/zh-Hant.json
@@ -17,7 +17,7 @@
"data": {
"host": "\u4e3b\u6a5f\u7aef"
},
- "description": "\u8acb\u8f38\u5165 Roon \u4f3a\u670d\u5668\u4e3b\u6a5f\u540d\u7a31\u6216 IP\u3002"
+ "description": "\u627e\u4e0d\u5230 Roon \u4f3a\u670d\u5668\uff0c\u8acb\u8f38\u5165\u4e3b\u6a5f\u540d\u7a31\u6216 IP\u3002"
}
}
}
diff --git a/homeassistant/components/route53/__init__.py b/homeassistant/components/route53/__init__.py
index 1061b7979ba0a9..a2b6a854c62e10 100644
--- a/homeassistant/components/route53/__init__.py
+++ b/homeassistant/components/route53/__init__.py
@@ -1,7 +1,8 @@
"""Update the IP addresses of your Route53 DNS records."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import List
import boto3
import requests
@@ -77,7 +78,7 @@ def _update_route53(
aws_secret_access_key: str,
zone: str,
domain: str,
- records: List[str],
+ records: list[str],
ttl: int,
):
_LOGGER.debug("Starting update for zone %s", zone)
diff --git a/homeassistant/components/rova/manifest.json b/homeassistant/components/rova/manifest.json
index a4ba931da43dd7..b3635b39f38c3a 100644
--- a/homeassistant/components/rova/manifest.json
+++ b/homeassistant/components/rova/manifest.json
@@ -2,6 +2,6 @@
"domain": "rova",
"name": "ROVA",
"documentation": "https://www.home-assistant.io/integrations/rova",
- "requirements": ["rova==0.1.0"],
+ "requirements": ["rova==0.2.1"],
"codeowners": []
}
diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py
index a2dafae931704c..13f8fffb8d1d5e 100644
--- a/homeassistant/components/rova/sensor.py
+++ b/homeassistant/components/rova/sensor.py
@@ -7,14 +7,13 @@
from rova.rova import Rova
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_MONITORED_CONDITIONS,
CONF_NAME,
DEVICE_CLASS_TIMESTAMP,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
# Config for rova requests.
@@ -80,7 +79,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(entities, True)
-class RovaSensor(Entity):
+class RovaSensor(SensorEntity):
"""Representation of a Rova sensor."""
def __init__(self, platform_name, sensor_key, data_service):
diff --git a/homeassistant/components/rpi_gpio/binary_sensor.py b/homeassistant/components/rpi_gpio/binary_sensor.py
index a2461f52db9e5a..318b29131b607e 100644
--- a/homeassistant/components/rpi_gpio/binary_sensor.py
+++ b/homeassistant/components/rpi_gpio/binary_sensor.py
@@ -1,4 +1,7 @@
"""Support for binary sensor using RPi GPIO."""
+
+import asyncio
+
import voluptuous as vol
from homeassistant.components import rpi_gpio
@@ -32,7 +35,6 @@
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Raspberry PI GPIO devices."""
-
setup_reload_service(hass, DOMAIN, PLATFORMS)
pull_mode = config.get(CONF_PULL_MODE)
@@ -53,6 +55,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class RPiGPIOBinarySensor(BinarySensorEntity):
"""Represent a binary sensor that uses Raspberry Pi GPIO."""
+ async def async_read_gpio(self):
+ """Read state from GPIO."""
+ await asyncio.sleep(float(self._bouncetime) / 1000)
+ self._state = await self.hass.async_add_executor_job(
+ rpi_gpio.read_input, self._port
+ )
+ self.async_write_ha_state()
+
def __init__(self, name, port, pull_mode, bouncetime, invert_logic):
"""Initialize the RPi binary sensor."""
self._name = name or DEVICE_DEFAULT_NAME
@@ -64,12 +74,11 @@ def __init__(self, name, port, pull_mode, bouncetime, invert_logic):
rpi_gpio.setup_input(self._port, self._pull_mode)
- def read_gpio(port):
- """Read state from GPIO."""
- self._state = rpi_gpio.read_input(self._port)
- self.schedule_update_ha_state()
+ def edge_detected(port):
+ """Edge detection handler."""
+ self.hass.add_job(self.async_read_gpio)
- rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime)
+ rpi_gpio.edge_detect(self._port, edge_detected, self._bouncetime)
@property
def should_poll(self):
diff --git a/homeassistant/components/rpi_gpio/cover.py b/homeassistant/components/rpi_gpio/cover.py
index 032796fe55b603..15eae3b4b077ac 100644
--- a/homeassistant/components/rpi_gpio/cover.py
+++ b/homeassistant/components/rpi_gpio/cover.py
@@ -5,13 +5,12 @@
from homeassistant.components import rpi_gpio
from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity
-from homeassistant.const import CONF_NAME
+from homeassistant.const import CONF_COVERS, CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import setup_reload_service
from . import DOMAIN, PLATFORMS
-CONF_COVERS = "covers"
CONF_RELAY_PIN = "relay_pin"
CONF_RELAY_TIME = "relay_time"
CONF_STATE_PIN = "state_pin"
@@ -49,7 +48,6 @@
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the RPi cover platform."""
-
setup_reload_service(hass, DOMAIN, PLATFORMS)
relay_time = config.get(CONF_RELAY_TIME)
diff --git a/homeassistant/components/rpi_gpio/manifest.json b/homeassistant/components/rpi_gpio/manifest.json
index 523d98dfdb752a..1a73c736d04c30 100644
--- a/homeassistant/components/rpi_gpio/manifest.json
+++ b/homeassistant/components/rpi_gpio/manifest.json
@@ -2,6 +2,6 @@
"domain": "rpi_gpio",
"name": "Raspberry Pi GPIO",
"documentation": "https://www.home-assistant.io/integrations/rpi_gpio",
- "requirements": ["RPi.GPIO==0.7.0"],
+ "requirements": ["RPi.GPIO==0.7.1a4"],
"codeowners": []
}
diff --git a/homeassistant/components/rpi_gpio/switch.py b/homeassistant/components/rpi_gpio/switch.py
index d556d8f035419d..3fba7b4b2cb0a4 100644
--- a/homeassistant/components/rpi_gpio/switch.py
+++ b/homeassistant/components/rpi_gpio/switch.py
@@ -28,7 +28,6 @@
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Raspberry PI GPIO devices."""
-
setup_reload_service(hass, DOMAIN, PLATFORMS)
invert_logic = config.get(CONF_INVERT_LOGIC)
diff --git a/homeassistant/components/rpi_power/config_flow.py b/homeassistant/components/rpi_power/config_flow.py
index 9924ebf0440cf2..b635972f43fe59 100644
--- a/homeassistant/components/rpi_power/config_flow.py
+++ b/homeassistant/components/rpi_power/config_flow.py
@@ -1,5 +1,7 @@
"""Config flow for Raspberry Pi Power Supply Checker."""
-from typing import Any, Dict, Optional
+from __future__ import annotations
+
+from typing import Any
from rpi_bad_power import new_under_voltage
@@ -31,8 +33,8 @@ def __init__(self) -> None:
)
async def async_step_onboarding(
- self, data: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, data: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initialized by onboarding."""
has_devices = await self._discovery_function(self.hass)
diff --git a/homeassistant/components/rpi_power/translations/de.json b/homeassistant/components/rpi_power/translations/de.json
new file mode 100644
index 00000000000000..1a00c87b985362
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/de.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Die f\u00fcr diese Komponente ben\u00f6tigte Systemklasse konnte nicht gefunden werden. Stellen Sie sicher, dass Ihr Kernel aktuell ist und die Hardware unterst\u00fctzt wird",
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
+ "step": {
+ "confirm": {
+ "description": "M\u00f6chten Sie mit der Einrichtung beginnen?"
+ }
+ }
+ },
+ "title": "Raspberry Pi Stromversorgungspr\u00fcfer"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/et.json b/homeassistant/components/rpi_power/translations/et.json
index fdf32414ba7c58..ec8475d2dac4a7 100644
--- a/homeassistant/components/rpi_power/translations/et.json
+++ b/homeassistant/components/rpi_power/translations/et.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "no_devices_found": "Ei leia selle komponendi jaoks vajalikku s\u00fcsteemiklassi. Veenduge, et teie kernel on v\u00e4rske ja riistvara on toetatud",
+ "no_devices_found": "Ei leia selle komponendi jaoks vajalikku s\u00fcsteemiklassi. Veendu, et kernel on v\u00e4rske ja riistvara on toetatud",
"single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
},
"step": {
diff --git a/homeassistant/components/rpi_power/translations/hu.json b/homeassistant/components/rpi_power/translations/hu.json
new file mode 100644
index 00000000000000..2d1c0811286072
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/hu.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
+ "step": {
+ "confirm": {
+ "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?"
+ }
+ }
+ },
+ "title": "Raspberry Pi Power Supply Checker"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/id.json b/homeassistant/components/rpi_power/translations/id.json
new file mode 100644
index 00000000000000..f9fcfa6c97a626
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/id.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Tidak dapat menemukan kelas sistem yang diperlukan untuk komponen ini, pastikan kernel Anda cukup terbaru dan perangkat kerasnya didukung",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "step": {
+ "confirm": {
+ "description": "Ingin memulai penyiapan?"
+ }
+ }
+ },
+ "title": "Pemeriksa Catu Daya Raspberry Pi"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/ko.json b/homeassistant/components/rpi_power/translations/ko.json
index b9a9a1be643c50..e8423b7d7339f1 100644
--- a/homeassistant/components/rpi_power/translations/ko.json
+++ b/homeassistant/components/rpi_power/translations/ko.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "\uc774 \uad6c\uc131 \uc694\uc18c\uc5d0 \ud544\uc694\ud55c \uc2dc\uc2a4\ud15c \ud074\ub798\uc2a4\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucee4\ub110\uc774 \ucd5c\uc2e0\uc774\uace0 \ud558\ub4dc\uc6e8\uc5b4\uac00 \uc9c0\uc6d0\ub418\ub294\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624.",
- "single_instance_allowed": "\uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c. \ud558\ub098\uc758 \uc124\uc815\ub9cc \uac00\ub2a5\ud568."
+ "no_devices_found": "\uc774 \uad6c\uc131 \uc694\uc18c\uc5d0 \ud544\uc694\ud55c \uc2dc\uc2a4\ud15c \ud074\ub798\uc2a4\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucee4\ub110\uc774 \ucd5c\uc2e0 \uc0c1\ud0dc\uc774\uace0 \ud558\ub4dc\uc6e8\uc5b4\uac00 \uc9c0\uc6d0\ub418\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/rpi_power/translations/lb.json b/homeassistant/components/rpi_power/translations/lb.json
index 3e145432bae03d..e4bb73893399ab 100644
--- a/homeassistant/components/rpi_power/translations/lb.json
+++ b/homeassistant/components/rpi_power/translations/lb.json
@@ -3,6 +3,11 @@
"abort": {
"no_devices_found": "Kann d\u00e9i Systemklass fir d\u00ebs noutwendeg Komponent net fannen, stell s\u00e9cher dass de Kernel rezent ass an d'Hardware \u00ebnnerst\u00ebtzt g\u00ebtt.",
"single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech."
+ },
+ "step": {
+ "confirm": {
+ "description": "Soll den Ariichtungs Prozess gestart ginn?"
+ }
}
},
"title": "Raspberry Pi Netzdeel Checker"
diff --git a/homeassistant/components/rpi_power/translations/nl.json b/homeassistant/components/rpi_power/translations/nl.json
index 72f9ff82ba4c9f..5529aa39f2006f 100644
--- a/homeassistant/components/rpi_power/translations/nl.json
+++ b/homeassistant/components/rpi_power/translations/nl.json
@@ -1,7 +1,13 @@
{
"config": {
"abort": {
+ "no_devices_found": "Kan de systeemklasse die nodig is voor dit onderdeel niet vinden, controleer of uw kernel recent is en of de hardware ondersteund wordt",
"single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk."
+ },
+ "step": {
+ "confirm": {
+ "description": "Wil je beginnen met instellen?"
+ }
}
},
"title": "Raspberry Pi Voeding Checker"
diff --git a/homeassistant/components/rpi_power/translations/tr.json b/homeassistant/components/rpi_power/translations/tr.json
new file mode 100644
index 00000000000000..f1dfcf16667708
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/tr.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "step": {
+ "confirm": {
+ "description": "Kuruluma ba\u015flamak ister misiniz?"
+ }
+ }
+ },
+ "title": "Raspberry Pi G\u00fc\u00e7 Kayna\u011f\u0131 Denetleyicisi"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_power/translations/uk.json b/homeassistant/components/rpi_power/translations/uk.json
new file mode 100644
index 00000000000000..b60160e1c4ec3a
--- /dev/null
+++ b/homeassistant/components/rpi_power/translations/uk.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u043d\u0430\u0439\u0442\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u0438\u0439 \u043a\u043b\u0430\u0441, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0438\u0439 \u0434\u043b\u044f \u0440\u043e\u0431\u043e\u0442\u0438 \u0446\u044c\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0443 \u0412\u0430\u0441 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u043d\u0430\u0439\u043d\u043e\u0432\u0456\u0448\u0435 \u044f\u0434\u0440\u043e \u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0435 \u043e\u0431\u043b\u0430\u0434\u043d\u0430\u043d\u043d\u044f.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?"
+ }
+ }
+ },
+ "title": "Raspberry Pi power supply checker"
+}
\ No newline at end of file
diff --git a/homeassistant/components/rpi_rf/switch.py b/homeassistant/components/rpi_rf/switch.py
index 78c2153a7b3902..a374300a264589 100644
--- a/homeassistant/components/rpi_rf/switch.py
+++ b/homeassistant/components/rpi_rf/switch.py
@@ -6,7 +6,12 @@
import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
-from homeassistant.const import CONF_NAME, CONF_SWITCHES, EVENT_HOMEASSISTANT_STOP
+from homeassistant.const import (
+ CONF_NAME,
+ CONF_PROTOCOL,
+ CONF_SWITCHES,
+ EVENT_HOMEASSISTANT_STOP,
+)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -14,7 +19,6 @@
CONF_CODE_OFF = "code_off"
CONF_CODE_ON = "code_on"
CONF_GPIO = "gpio"
-CONF_PROTOCOL = "protocol"
CONF_PULSELENGTH = "pulselength"
CONF_SIGNAL_REPETITIONS = "signal_repetitions"
@@ -41,7 +45,6 @@
)
-# pylint: disable=no-member
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Find and return switches controlled by a generic RF device via GPIO."""
rpi_rf = importlib.import_module("rpi_rf")
diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py
index 3976d8985cdbc7..4c02f49d86a331 100644
--- a/homeassistant/components/rtorrent/sensor.py
+++ b/homeassistant/components/rtorrent/sensor.py
@@ -4,7 +4,7 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_MONITORED_VARIABLES,
CONF_NAME,
@@ -14,7 +14,6 @@
)
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -75,7 +74,7 @@ def format_speed(speed):
return round(kb_spd, 2 if kb_spd < 0.1 else 1)
-class RTorrentSensor(Entity):
+class RTorrentSensor(SensorEntity):
"""Representation of an rtorrent sensor."""
def __init__(self, sensor_type, rtorrent_client, client_name):
diff --git a/homeassistant/components/ruckus_unleashed/__init__.py b/homeassistant/components/ruckus_unleashed/__init__.py
index ed38f5a2a3a3c4..2eb4f143131396 100644
--- a/homeassistant/components/ruckus_unleashed/__init__.py
+++ b/homeassistant/components/ruckus_unleashed/__init__.py
@@ -47,9 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = RuckusUnleashedDataUpdateCoordinator(hass, ruckus=ruckus)
- await coordinator.async_refresh()
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
system_info = await hass.async_add_executor_job(ruckus.system_info)
@@ -84,8 +82,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/ruckus_unleashed/config_flow.py b/homeassistant/components/ruckus_unleashed/config_flow.py
index 098ffba708ccc8..26be0e5bed97a0 100644
--- a/homeassistant/components/ruckus_unleashed/config_flow.py
+++ b/homeassistant/components/ruckus_unleashed/config_flow.py
@@ -8,11 +8,7 @@
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
-from .const import ( # pylint:disable=unused-import
- API_SERIAL,
- API_SYSTEM_OVERVIEW,
- DOMAIN,
-)
+from .const import API_SERIAL, API_SYSTEM_OVERVIEW, DOMAIN
_LOGGER = logging.getLogger(__package__)
diff --git a/homeassistant/components/ruckus_unleashed/device_tracker.py b/homeassistant/components/ruckus_unleashed/device_tracker.py
index 955e0581393f48..90a848b663b3ac 100644
--- a/homeassistant/components/ruckus_unleashed/device_tracker.py
+++ b/homeassistant/components/ruckus_unleashed/device_tracker.py
@@ -1,5 +1,5 @@
"""Support for Ruckus Unleashed devices."""
-from typing import Optional
+from __future__ import annotations
from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER
from homeassistant.components.device_tracker.config_entry import ScannerEntity
@@ -67,14 +67,17 @@ def restore_entities(registry, coordinator, entry, async_add_entities, tracked):
missing = []
for entity in registry.entities.values():
- if entity.config_entry_id == entry.entry_id and entity.platform == DOMAIN:
- if entity.unique_id not in coordinator.data[API_CLIENTS]:
- missing.append(
- RuckusUnleashedDevice(
- coordinator, entity.unique_id, entity.original_name
- )
+ if (
+ entity.config_entry_id == entry.entry_id
+ and entity.platform == DOMAIN
+ and entity.unique_id not in coordinator.data[API_CLIENTS]
+ ):
+ missing.append(
+ RuckusUnleashedDevice(
+ coordinator, entity.unique_id, entity.original_name
)
- tracked.add(entity.unique_id)
+ )
+ tracked.add(entity.unique_id)
if missing:
async_add_entities(missing)
@@ -115,7 +118,7 @@ def source_type(self) -> str:
return SOURCE_TYPE_ROUTER
@property
- def device_info(self) -> Optional[dict]:
+ def device_info(self) -> dict | None:
"""Return the device information."""
if self.is_connected:
return {
diff --git a/homeassistant/components/ruckus_unleashed/translations/de.json b/homeassistant/components/ruckus_unleashed/translations/de.json
index ae15ec058b5185..625c7372347a61 100644
--- a/homeassistant/components/ruckus_unleashed/translations/de.json
+++ b/homeassistant/components/ruckus_unleashed/translations/de.json
@@ -4,12 +4,14 @@
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"step": {
"user": {
"data": {
+ "host": "Host",
"password": "Passwort",
"username": "Benutzername"
}
diff --git a/homeassistant/components/ruckus_unleashed/translations/he.json b/homeassistant/components/ruckus_unleashed/translations/he.json
new file mode 100644
index 00000000000000..6ef580c7d8d482
--- /dev/null
+++ b/homeassistant/components/ruckus_unleashed/translations/he.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u05de\u05d0\u05e8\u05d7",
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ruckus_unleashed/translations/hu.json b/homeassistant/components/ruckus_unleashed/translations/hu.json
index c1a23478ac4d6a..0abcc301f0c854 100644
--- a/homeassistant/components/ruckus_unleashed/translations/hu.json
+++ b/homeassistant/components/ruckus_unleashed/translations/hu.json
@@ -1,13 +1,17 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
- "unknown": "V\u00e1ratlan hiba"
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"step": {
"user": {
"data": {
- "host": "Gazdag\u00e9p",
+ "host": "Hoszt",
"password": "Jelsz\u00f3",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
}
diff --git a/homeassistant/components/ruckus_unleashed/translations/id.json b/homeassistant/components/ruckus_unleashed/translations/id.json
new file mode 100644
index 00000000000000..ed8fde321061cf
--- /dev/null
+++ b/homeassistant/components/ruckus_unleashed/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ruckus_unleashed/translations/ko.json b/homeassistant/components/ruckus_unleashed/translations/ko.json
new file mode 100644
index 00000000000000..9ba063c37ddf46
--- /dev/null
+++ b/homeassistant/components/ruckus_unleashed/translations/ko.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ruckus_unleashed/translations/nl.json b/homeassistant/components/ruckus_unleashed/translations/nl.json
index 7482a0bbe7c64d..8ad15260b0de5c 100644
--- a/homeassistant/components/ruckus_unleashed/translations/nl.json
+++ b/homeassistant/components/ruckus_unleashed/translations/nl.json
@@ -4,12 +4,16 @@
"already_configured": "Apparaat is al geconfigureerd"
},
"error": {
- "invalid_auth": "Ongeldige authenticatie"
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
},
"step": {
"user": {
"data": {
- "password": "Wachtwoord"
+ "host": "Host",
+ "password": "Wachtwoord",
+ "username": "Gebruikersnaam"
}
}
}
diff --git a/homeassistant/components/ruckus_unleashed/translations/ru.json b/homeassistant/components/ruckus_unleashed/translations/ru.json
index 6f71ee41376162..9b02cafd466691 100644
--- a/homeassistant/components/ruckus_unleashed/translations/ru.json
+++ b/homeassistant/components/ruckus_unleashed/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
@@ -13,7 +13,7 @@
"data": {
"host": "\u0425\u043e\u0441\u0442",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
}
}
}
diff --git a/homeassistant/components/ruckus_unleashed/translations/tr.json b/homeassistant/components/ruckus_unleashed/translations/tr.json
new file mode 100644
index 00000000000000..40c9c39b967721
--- /dev/null
+++ b/homeassistant/components/ruckus_unleashed/translations/tr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ruckus_unleashed/translations/uk.json b/homeassistant/components/ruckus_unleashed/translations/uk.json
new file mode 100644
index 00000000000000..2df11f744559db
--- /dev/null
+++ b/homeassistant/components/ruckus_unleashed/translations/uk.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py
index 5e437c41b23dbf..c0930f2c114280 100644
--- a/homeassistant/components/sabnzbd/sensor.py
+++ b/homeassistant/components/sabnzbd/sensor.py
@@ -1,6 +1,6 @@
"""Support for monitoring an SABnzbd NZB client."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from . import DATA_SABNZBD, SENSOR_TYPES, SIGNAL_SABNZBD_UPDATED
@@ -18,7 +18,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
-class SabnzbdSensor(Entity):
+class SabnzbdSensor(SensorEntity):
"""Representation of an SABnzbd sensor."""
def __init__(self, sensor_type, sabnzbd_api_data, client_name):
diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py
index 1e7b3dd506107d..f1def71cc641ad 100644
--- a/homeassistant/components/saj/sensor.py
+++ b/homeassistant/components/saj/sensor.py
@@ -5,7 +5,7 @@
import pysaj
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_HOST,
CONF_NAME,
@@ -26,7 +26,6 @@
from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_call_later
_LOGGER = logging.getLogger(__name__)
@@ -104,18 +103,18 @@ async def async_saj():
for sensor in hass_sensors:
state_unknown = False
- if not values:
- # SAJ inverters are powered by DC via solar panels and thus are
- # offline after the sun has set. If a sensor resets on a daily
- # basis like "today_yield", this reset won't happen automatically.
- # Code below checks if today > day when sensor was last updated
- # and if so: set state to None.
- # Sensors with live values like "temperature" or "current_power"
- # will also be reset to None.
- if (sensor.per_day_basis and date.today() > sensor.date_updated) or (
- not sensor.per_day_basis and not sensor.per_total_basis
- ):
- state_unknown = True
+ # SAJ inverters are powered by DC via solar panels and thus are
+ # offline after the sun has set. If a sensor resets on a daily
+ # basis like "today_yield", this reset won't happen automatically.
+ # Code below checks if today > day when sensor was last updated
+ # and if so: set state to None.
+ # Sensors with live values like "temperature" or "current_power"
+ # will also be reset to None.
+ if not values and (
+ (sensor.per_day_basis and date.today() > sensor.date_updated)
+ or (not sensor.per_day_basis and not sensor.per_total_basis)
+ ):
+ state_unknown = True
sensor.async_update_values(unknown_state=state_unknown)
return values
@@ -160,7 +159,7 @@ def remove_listener():
return remove_listener
-class SAJsensor(Entity):
+class SAJsensor(SensorEntity):
"""Representation of a SAJ sensor."""
def __init__(self, serialnumber, pysaj_sensor, inverter_name=None):
diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py
index 7a7fa26f922584..209b89f541af60 100644
--- a/homeassistant/components/samsungtv/config_flow.py
+++ b/homeassistant/components/samsungtv/config_flow.py
@@ -21,7 +21,6 @@
CONF_TOKEN,
)
-# pylint:disable=unused-import
from .bridge import SamsungTVBridge
from .const import (
CONF_MANUFACTURER,
@@ -51,8 +50,6 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
-
def __init__(self):
"""Initialize flow."""
self._host = None
diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json
index 5584d2dd452bdc..08dc4d0c04974a 100644
--- a/homeassistant/components/samsungtv/manifest.json
+++ b/homeassistant/components/samsungtv/manifest.json
@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/samsungtv",
"requirements": [
"samsungctl[websocket]==0.7.1",
- "samsungtvws==1.4.0"
+ "samsungtvws==1.6.0"
],
"ssdp": [
{
diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py
index 6e406f60ec442d..a4b61369f99a7e 100644
--- a/homeassistant/components/samsungtv/media_player.py
+++ b/homeassistant/components/samsungtv/media_player.py
@@ -18,6 +18,7 @@
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_STEP,
)
+from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.const import (
CONF_HOST,
CONF_ID,
@@ -124,7 +125,7 @@ def access_denied(self):
self.hass.add_job(
self.hass.config_entries.flow.async_init(
DOMAIN,
- context={"source": "reauth"},
+ context={"source": SOURCE_REAUTH},
data=self._config_entry.data,
)
)
diff --git a/homeassistant/components/samsungtv/translations/de.json b/homeassistant/components/samsungtv/translations/de.json
index e33542676300e3..3ba569c87db987 100644
--- a/homeassistant/components/samsungtv/translations/de.json
+++ b/homeassistant/components/samsungtv/translations/de.json
@@ -2,9 +2,9 @@
"config": {
"abort": {
"already_configured": "Dieser Samsung TV ist bereits konfiguriert",
- "already_in_progress": "Der Konfigurationsablauf f\u00fcr Samsung TV wird bereits ausgef\u00fchrt.",
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
"auth_missing": "Home Assistant ist nicht berechtigt, eine Verbindung zu diesem Samsung TV herzustellen. \u00dcberpr\u00fcfe die Einstellungen deines Fernsehger\u00e4ts, um Home Assistant zu autorisieren.",
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"not_supported": "Dieses Samsung TV-Ger\u00e4t wird derzeit nicht unterst\u00fctzt."
},
"flow_title": "Samsung TV: {model}",
diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json
index ca42aff331ad2e..5c517c78d69682 100644
--- a/homeassistant/components/samsungtv/translations/hu.json
+++ b/homeassistant/components/samsungtv/translations/hu.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Ez a Samsung TV m\u00e1r konfigur\u00e1lva van.",
- "already_in_progress": "A Samsung TV konfigur\u00e1l\u00e1sa m\u00e1r folyamatban van.",
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
"auth_missing": "A Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizd a TV be\u00e1ll\u00edt\u00e1sait a Home Assistant enged\u00e9lyez\u00e9s\u00e9hez.",
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
"not_supported": "Ez a Samsung TV k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott."
diff --git a/homeassistant/components/samsungtv/translations/id.json b/homeassistant/components/samsungtv/translations/id.json
new file mode 100644
index 00000000000000..7d0f5982a657c5
--- /dev/null
+++ b/homeassistant/components/samsungtv/translations/id.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "auth_missing": "Home Assistant tidak diizinkan untuk tersambung ke TV Samsung ini. Periksa setelan TV Anda untuk mengotorisasi Home Assistant.",
+ "cannot_connect": "Gagal terhubung",
+ "not_supported": "Perangkat TV Samsung ini saat ini tidak didukung."
+ },
+ "flow_title": "TV Samsung: {model}",
+ "step": {
+ "confirm": {
+ "description": "Apakah Anda ingin menyiapkan TV Samsung {model}? Jika Anda belum pernah menyambungkan Home Assistant sebelumnya, Anda akan melihat dialog di TV yang meminta otorisasi. Konfigurasi manual untuk TV ini akan ditimpa.",
+ "title": "TV Samsung"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nama"
+ },
+ "description": "Masukkan informasi TV Samsung Anda. Jika Anda belum pernah menyambungkan Home Assistant sebelumnya, Anda akan melihat dialog di TV yang meminta otorisasi."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/samsungtv/translations/ko.json b/homeassistant/components/samsungtv/translations/ko.json
index 1c7e0b29808b71..7efb88bf7eb3c8 100644
--- a/homeassistant/components/samsungtv/translations/ko.json
+++ b/homeassistant/components/samsungtv/translations/ko.json
@@ -1,15 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 \uc0bc\uc131 TV \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "already_in_progress": "\uc0bc\uc131 TV \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.",
- "auth_missing": "Home Assistant \uac00 \ud574\ub2f9 \uc0bc\uc131 TV \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc788\ub294 \uad8c\ud55c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. TV \uc124\uc815\uc744 \ud655\uc778\ud558\uc5ec Home Assistant \ub97c \uc2b9\uc778\ud574\uc8fc\uc138\uc694.",
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
+ "auth_missing": "Home Assistant\uac00 \ud574\ub2f9 \uc0bc\uc131 TV\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc788\ub294 \uad8c\ud55c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. TV \uc124\uc815\uc744 \ud655\uc778\ud558\uc5ec Home Assistant\ub97c \uc2b9\uc778\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"not_supported": "\uc774 \uc0bc\uc131 TV \ubaa8\ub378\uc740 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4."
},
"flow_title": "\uc0bc\uc131 TV: {model}",
"step": {
"confirm": {
- "description": "\uc0bc\uc131 TV {model} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? Home Assistant \ub97c \uc5f0\uacb0 \ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV \uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4. \uc774 TV \uc758 \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ub41c \ub0b4\uc6a9\uc744 \ub36e\uc5b4\uc501\ub2c8\ub2e4.",
+ "description": "{model} \uc0bc\uc131 TV\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? Home Assistant\ub97c \uc5f0\uacb0\ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV\uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4. \uc774 TV\uc758 \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ub41c \ub0b4\uc6a9\uc740 \ub36e\uc5b4\uc50c\uc6cc\uc9d1\ub2c8\ub2e4.",
"title": "\uc0bc\uc131 TV"
},
"user": {
@@ -17,7 +18,7 @@
"host": "\ud638\uc2a4\ud2b8",
"name": "\uc774\ub984"
},
- "description": "\uc0bc\uc131 TV \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. Home Assistant \ub97c \uc5f0\uacb0 \ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV \uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4."
+ "description": "\uc0bc\uc131 TV \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. Home Assistant\ub97c \uc5f0\uacb0 \ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV\uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4."
}
}
}
diff --git a/homeassistant/components/samsungtv/translations/nl.json b/homeassistant/components/samsungtv/translations/nl.json
index d1e2a9abaa22bb..2a6cca466ea783 100644
--- a/homeassistant/components/samsungtv/translations/nl.json
+++ b/homeassistant/components/samsungtv/translations/nl.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Deze Samsung TV is al geconfigureerd.",
- "already_in_progress": "Samsung TV configuratie is al in uitvoering.",
+ "already_configured": "Apparaat is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
"auth_missing": "Home Assistant is niet geautoriseerd om verbinding te maken met deze Samsung TV.",
"cannot_connect": "Kan geen verbinding maken",
"not_supported": "Deze Samsung TV wordt momenteel niet ondersteund."
@@ -15,7 +15,7 @@
},
"user": {
"data": {
- "host": "Hostnaam of IP-adres",
+ "host": "Host",
"name": "Naam"
},
"description": "Voer uw Samsung TV informatie in. Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt."
diff --git a/homeassistant/components/samsungtv/translations/tr.json b/homeassistant/components/samsungtv/translations/tr.json
index 50e6b21d120d67..6b3900e9aa5651 100644
--- a/homeassistant/components/samsungtv/translations/tr.json
+++ b/homeassistant/components/samsungtv/translations/tr.json
@@ -4,13 +4,17 @@
"already_configured": "Bu Samsung TV zaten ayarlanm\u0131\u015f.",
"already_in_progress": "Samsung TV ayar\u0131 zaten s\u00fcr\u00fcyor.",
"auth_missing": "Home Assistant'\u0131n bu Samsung TV'ye ba\u011flanma izni yok. Home Assistant'\u0131 yetkilendirmek i\u00e7in l\u00fctfen TV'nin ayarlar\u0131n\u0131 kontrol et.",
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
"not_supported": "Bu Samsung TV cihaz\u0131 \u015fu anda desteklenmiyor."
},
"flow_title": "Samsung TV: {model}",
"step": {
+ "confirm": {
+ "title": "Samsung TV"
+ },
"user": {
"data": {
- "host": "Host veya IP adresi",
+ "host": "Ana Bilgisayar",
"name": "Ad"
},
"description": "Samsung TV bilgilerini gir. Daha \u00f6nce hi\u00e7 Home Assistant'a ba\u011flamad\u0131ysan, TV'nde izin isteyen bir pencere g\u00f6receksindir."
diff --git a/homeassistant/components/samsungtv/translations/uk.json b/homeassistant/components/samsungtv/translations/uk.json
new file mode 100644
index 00000000000000..83bb18e76f172c
--- /dev/null
+++ b/homeassistant/components/samsungtv/translations/uk.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "auth_missing": "Home Assistant \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u0438\u0439 \u0434\u043b\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "not_supported": "\u0426\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 \u0432 \u0434\u0430\u043d\u0438\u0439 \u0447\u0430\u0441 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f."
+ },
+ "flow_title": "Samsung TV: {model}",
+ "step": {
+ "confirm": {
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Samsung {model}? \u042f\u043a\u0449\u043e \u0446\u0435\u0439 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0440\u0430\u043d\u0456\u0448\u0435 \u043d\u0435 \u0431\u0443\u0432 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0434\u043e Home Assistant, \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 \u043c\u0430\u0454 \u0437'\u044f\u0432\u0438\u0442\u0438\u0441\u044f \u0441\u043f\u043b\u0438\u0432\u0430\u044e\u0447\u0435 \u0432\u0456\u043a\u043d\u043e \u0456\u0437 \u0437\u0430\u043f\u0438\u0442\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457. \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430, \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0456 \u0432\u0440\u0443\u0447\u043d\u0443, \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0430\u043d\u0456.",
+ "title": "\u0422\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Samsung"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Samsung. \u042f\u043a\u0449\u043e \u0446\u0435\u0439 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0440\u0430\u043d\u0456\u0448\u0435 \u043d\u0435 \u0431\u0443\u0432 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0434\u043e Home Assistant, \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 \u043c\u0430\u0454 \u0437'\u044f\u0432\u0438\u0442\u0438\u0441\u044f \u0441\u043f\u043b\u0438\u0432\u0430\u044e\u0447\u0435 \u0432\u0456\u043a\u043d\u043e \u0456\u0437 \u0437\u0430\u043f\u0438\u0442\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py
index 245d21770ee603..e11934c61c3804 100644
--- a/homeassistant/components/scene/__init__.py
+++ b/homeassistant/components/scene/__init__.py
@@ -1,8 +1,10 @@
"""Allow users to set and activate scenes."""
+from __future__ import annotations
+
import functools as ft
import importlib
import logging
-from typing import Any, Optional
+from typing import Any
import voluptuous as vol
@@ -94,7 +96,7 @@ def should_poll(self) -> bool:
return False
@property
- def state(self) -> Optional[str]:
+ def state(self) -> str | None:
"""Return the state of the scene."""
return STATE
@@ -104,7 +106,6 @@ def activate(self, **kwargs: Any) -> None:
async def async_activate(self, **kwargs: Any) -> None:
"""Activate scene. Try to get entities into requested state."""
- assert self.hass
task = self.hass.async_add_job(ft.partial(self.activate, **kwargs))
if task:
await task
diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml
index 29fa11e9367197..9d07460379c930 100644
--- a/homeassistant/components/scene/services.yaml
+++ b/homeassistant/components/scene/services.yaml
@@ -1,53 +1,83 @@
# Describes the format for available scene services
turn_on:
+ name: Activate
description: Activate a scene.
+ target:
fields:
transition:
+ name: Transition
description:
Transition duration in seconds it takes to bring devices to the state
defined in the scene.
example: 2.5
- entity_id:
- description: Name(s) of scenes to turn on
- example: "scene.romantic"
+ selector:
+ number:
+ min: 0
+ max: 300
+ step: 1
+ unit_of_measurement: seconds
+ mode: slider
reload:
- description: Reload the scene configuration
+ name: Reload
+ description: Reload the scene configuration.
apply:
- description:
- Activate a scene. Takes same data as the entities field from a single scene
- in the config.
+ name: Apply
+ description: Activate a scene with configuration.
fields:
- transition:
- description:
- Transition duration in seconds it takes to bring devices to the state
- defined in the scene.
- example: 2.5
entities:
+ name: Entities state
description: The entities and the state that they need to be.
+ required: true
example:
light.kitchen: "on"
light.ceiling:
state: "on"
brightness: 80
+ selector:
+ object:
+ transition:
+ name: Transition
+ description:
+ Transition duration in seconds it takes to bring devices to the state
+ defined in the scene.
+ example: 2.5
+ selector:
+ number:
+ min: 0
+ max: 300
+ step: 1
+ unit_of_measurement: seconds
+ mode: slider
create:
+ name: Create
description: Creates a new scene.
fields:
scene_id:
+ name: Scene entity ID
description: The entity_id of the new scene.
+ required: true
example: all_lights
+ selector:
+ text:
entities:
+ name: Entities state
description: The entities to control with the scene.
example:
light.tv_back_light: "on"
light.ceiling:
state: "on"
brightness: 200
+ selector:
+ object:
snapshot_entities:
+ name: Snapshot entities
description: The entities of which a snapshot is to be taken
example:
- light.ceiling
- light.kitchen
+ selector:
+ object:
diff --git a/homeassistant/components/scene/translations/id.json b/homeassistant/components/scene/translations/id.json
index 65f2adf73253bc..827c0c81f3801f 100644
--- a/homeassistant/components/scene/translations/id.json
+++ b/homeassistant/components/scene/translations/id.json
@@ -1,3 +1,3 @@
{
- "title": "Adegan"
+ "title": "Scene"
}
\ No newline at end of file
diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py
index e8b6fcfd2c3f21..3bf070a7d792a0 100644
--- a/homeassistant/components/scrape/sensor.py
+++ b/homeassistant/components/scrape/sensor.py
@@ -6,7 +6,7 @@
import voluptuous as vol
from homeassistant.components.rest.data import RestData
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_AUTHENTICATION,
CONF_HEADERS,
@@ -22,7 +22,6 @@
)
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -89,7 +88,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
-class ScrapeSensor(Entity):
+class ScrapeSensor(SensorEntity):
"""Representation of a web scrape sensor."""
def __init__(self, rest, name, select, attr, index, value_template, unit):
diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py
new file mode 100644
index 00000000000000..11f71cfd337112
--- /dev/null
+++ b/homeassistant/components/screenlogic/__init__.py
@@ -0,0 +1,203 @@
+"""The Screenlogic integration."""
+import asyncio
+from collections import defaultdict
+from datetime import timedelta
+import logging
+
+from screenlogicpy import ScreenLogicError, ScreenLogicGateway
+from screenlogicpy.const import (
+ EQUIPMENT,
+ SL_GATEWAY_IP,
+ SL_GATEWAY_NAME,
+ SL_GATEWAY_PORT,
+)
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import device_registry as dr
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+ UpdateFailed,
+)
+
+from .config_flow import async_discover_gateways_by_unique_id, name_for_mac
+from .const import DEFAULT_SCAN_INTERVAL, DISCOVERED_GATEWAYS, DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORMS = ["switch", "sensor", "binary_sensor", "climate"]
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+ """Set up the Screenlogic component."""
+ domain_data = hass.data[DOMAIN] = {}
+ domain_data[DISCOVERED_GATEWAYS] = await async_discover_gateways_by_unique_id(hass)
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up Screenlogic from a config entry."""
+ mac = entry.unique_id
+ # Attempt to re-discover named gateway to follow IP changes
+ discovered_gateways = hass.data[DOMAIN][DISCOVERED_GATEWAYS]
+ if mac in discovered_gateways:
+ connect_info = discovered_gateways[mac]
+ else:
+ _LOGGER.warning("Gateway rediscovery failed")
+ # Static connection defined or fallback from discovery
+ connect_info = {
+ SL_GATEWAY_NAME: name_for_mac(mac),
+ SL_GATEWAY_IP: entry.data[CONF_IP_ADDRESS],
+ SL_GATEWAY_PORT: entry.data[CONF_PORT],
+ }
+
+ try:
+ gateway = ScreenLogicGateway(**connect_info)
+ except ScreenLogicError as ex:
+ _LOGGER.error("Error while connecting to the gateway %s: %s", connect_info, ex)
+ raise ConfigEntryNotReady from ex
+
+ # The api library uses a shared socket connection and does not handle concurrent
+ # requests very well.
+ api_lock = asyncio.Lock()
+
+ coordinator = ScreenlogicDataUpdateCoordinator(
+ hass, config_entry=entry, gateway=gateway, api_lock=api_lock
+ )
+
+ device_data = defaultdict(list)
+
+ await coordinator.async_config_entry_first_refresh()
+
+ for circuit in coordinator.data["circuits"]:
+ device_data["switch"].append(circuit)
+
+ for sensor in coordinator.data["sensors"]:
+ if sensor == "chem_alarm":
+ device_data["binary_sensor"].append(sensor)
+ else:
+ if coordinator.data["sensors"][sensor]["value"] != 0:
+ device_data["sensor"].append(sensor)
+
+ for pump in coordinator.data["pumps"]:
+ if (
+ coordinator.data["pumps"][pump]["data"] != 0
+ and "currentWatts" in coordinator.data["pumps"][pump]
+ ):
+ device_data["pump"].append(pump)
+
+ for body in coordinator.data["bodies"]:
+ device_data["body"].append(body)
+
+ hass.data[DOMAIN][entry.entry_id] = {
+ "coordinator": coordinator,
+ "devices": device_data,
+ "listener": entry.add_update_listener(async_update_listener),
+ }
+
+ for component in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, component)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, component)
+ for component in PLATFORMS
+ ]
+ )
+ )
+ hass.data[DOMAIN][entry.entry_id]["listener"]()
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
+
+
+async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry):
+ """Handle options update."""
+ await hass.config_entries.async_reload(entry.entry_id)
+
+
+class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator):
+ """Class to manage the data update for the Screenlogic component."""
+
+ def __init__(self, hass, *, config_entry, gateway, api_lock):
+ """Initialize the Screenlogic Data Update Coordinator."""
+ self.config_entry = config_entry
+ self.gateway = gateway
+ self.api_lock = api_lock
+ self.screenlogic_data = {}
+ interval = timedelta(
+ seconds=config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
+ )
+ super().__init__(
+ hass,
+ _LOGGER,
+ name=DOMAIN,
+ update_interval=interval,
+ )
+
+ async def _async_update_data(self):
+ """Fetch data from the Screenlogic gateway."""
+ try:
+ async with self.api_lock:
+ await self.hass.async_add_executor_job(self.gateway.update)
+ except ScreenLogicError as error:
+ raise UpdateFailed(error) from error
+ return self.gateway.get_data()
+
+
+class ScreenlogicEntity(CoordinatorEntity):
+ """Base class for all ScreenLogic entities."""
+
+ def __init__(self, coordinator, data_key):
+ """Initialize of the entity."""
+ super().__init__(coordinator)
+ self._data_key = data_key
+
+ @property
+ def mac(self):
+ """Mac address."""
+ return self.coordinator.config_entry.unique_id
+
+ @property
+ def unique_id(self):
+ """Entity Unique ID."""
+ return f"{self.mac}_{self._data_key}"
+
+ @property
+ def config_data(self):
+ """Shortcut for config data."""
+ return self.coordinator.data["config"]
+
+ @property
+ def gateway(self):
+ """Return the gateway."""
+ return self.coordinator.gateway
+
+ @property
+ def gateway_name(self):
+ """Return the configured name of the gateway."""
+ return self.gateway.name
+
+ @property
+ def device_info(self):
+ """Return device information for the controller."""
+ controller_type = self.config_data["controller_type"]
+ hardware_type = self.config_data["hardware_type"]
+ return {
+ "connections": {(dr.CONNECTION_NETWORK_MAC, self.mac)},
+ "name": self.gateway_name,
+ "manufacturer": "Pentair",
+ "model": EQUIPMENT.CONTROLLER_HARDWARE[controller_type][hardware_type],
+ }
diff --git a/homeassistant/components/screenlogic/binary_sensor.py b/homeassistant/components/screenlogic/binary_sensor.py
new file mode 100644
index 00000000000000..0001223030a68a
--- /dev/null
+++ b/homeassistant/components/screenlogic/binary_sensor.py
@@ -0,0 +1,52 @@
+"""Support for a ScreenLogic Binary Sensor."""
+import logging
+
+from screenlogicpy.const import DEVICE_TYPE, ON_OFF
+
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_PROBLEM,
+ BinarySensorEntity,
+)
+
+from . import ScreenlogicEntity
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = {DEVICE_TYPE.ALARM: DEVICE_CLASS_PROBLEM}
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up entry."""
+ entities = []
+ data = hass.data[DOMAIN][config_entry.entry_id]
+ coordinator = data["coordinator"]
+
+ for binary_sensor in data["devices"]["binary_sensor"]:
+ entities.append(ScreenLogicBinarySensor(coordinator, binary_sensor))
+ async_add_entities(entities)
+
+
+class ScreenLogicBinarySensor(ScreenlogicEntity, BinarySensorEntity):
+ """Representation of a ScreenLogic binary sensor entity."""
+
+ @property
+ def name(self):
+ """Return the sensor name."""
+ return f"{self.gateway_name} {self.sensor['name']}"
+
+ @property
+ def device_class(self):
+ """Return the device class."""
+ device_class = self.sensor.get("device_type")
+ return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_class)
+
+ @property
+ def is_on(self) -> bool:
+ """Determine if the sensor is on."""
+ return self.sensor["value"] == ON_OFF.ON
+
+ @property
+ def sensor(self):
+ """Shortcut to access the sensor data."""
+ return self.coordinator.data["sensors"][self._data_key]
diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py
new file mode 100644
index 00000000000000..b50879bfd499a4
--- /dev/null
+++ b/homeassistant/components/screenlogic/climate.py
@@ -0,0 +1,220 @@
+"""Support for a ScreenLogic heating device."""
+import logging
+
+from screenlogicpy.const import EQUIPMENT, HEAT_MODE
+
+from homeassistant.components.climate import ClimateEntity
+from homeassistant.components.climate.const import (
+ ATTR_PRESET_MODE,
+ CURRENT_HVAC_HEAT,
+ CURRENT_HVAC_IDLE,
+ CURRENT_HVAC_OFF,
+ HVAC_MODE_HEAT,
+ HVAC_MODE_OFF,
+ SUPPORT_PRESET_MODE,
+ SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers.restore_state import RestoreEntity
+
+from . import ScreenlogicEntity
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+SUPPORTED_FEATURES = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
+
+SUPPORTED_MODES = [HVAC_MODE_OFF, HVAC_MODE_HEAT]
+
+SUPPORTED_PRESETS = [
+ HEAT_MODE.SOLAR,
+ HEAT_MODE.SOLAR_PREFERRED,
+ HEAT_MODE.HEATER,
+]
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up entry."""
+ entities = []
+ data = hass.data[DOMAIN][config_entry.entry_id]
+ coordinator = data["coordinator"]
+
+ for body in data["devices"]["body"]:
+ entities.append(ScreenLogicClimate(coordinator, body))
+ async_add_entities(entities)
+
+
+class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity):
+ """Represents a ScreenLogic climate entity."""
+
+ def __init__(self, coordinator, body):
+ """Initialize a ScreenLogic climate entity."""
+ super().__init__(coordinator, body)
+ self._configured_heat_modes = []
+ # Is solar listed as available equipment?
+ if self.coordinator.data["config"]["equipment_flags"] & EQUIPMENT.FLAG_SOLAR:
+ self._configured_heat_modes.extend(
+ [HEAT_MODE.SOLAR, HEAT_MODE.SOLAR_PREFERRED]
+ )
+ self._configured_heat_modes.append(HEAT_MODE.HEATER)
+ self._last_preset = None
+
+ @property
+ def name(self) -> str:
+ """Name of the heater."""
+ ent_name = self.body["heat_status"]["name"]
+ return f"{self.gateway_name} {ent_name}"
+
+ @property
+ def min_temp(self) -> float:
+ """Minimum allowed temperature."""
+ return self.body["min_set_point"]["value"]
+
+ @property
+ def max_temp(self) -> float:
+ """Maximum allowed temperature."""
+ return self.body["max_set_point"]["value"]
+
+ @property
+ def current_temperature(self) -> float:
+ """Return water temperature."""
+ return self.body["last_temperature"]["value"]
+
+ @property
+ def target_temperature(self) -> float:
+ """Target temperature."""
+ return self.body["heat_set_point"]["value"]
+
+ @property
+ def temperature_unit(self) -> str:
+ """Return the unit of measurement."""
+ if self.config_data["is_celcius"]["value"] == 1:
+ return TEMP_CELSIUS
+ return TEMP_FAHRENHEIT
+
+ @property
+ def hvac_mode(self) -> str:
+ """Return the current hvac mode."""
+ if self.body["heat_mode"]["value"] > 0:
+ return HVAC_MODE_HEAT
+ return HVAC_MODE_OFF
+
+ @property
+ def hvac_modes(self):
+ """Return th supported hvac modes."""
+ return SUPPORTED_MODES
+
+ @property
+ def hvac_action(self) -> str:
+ """Return the current action of the heater."""
+ if self.body["heat_status"]["value"] > 0:
+ return CURRENT_HVAC_HEAT
+ if self.hvac_mode == HVAC_MODE_HEAT:
+ return CURRENT_HVAC_IDLE
+ return CURRENT_HVAC_OFF
+
+ @property
+ def preset_mode(self) -> str:
+ """Return current/last preset mode."""
+ if self.hvac_mode == HVAC_MODE_OFF:
+ return HEAT_MODE.NAME_FOR_NUM[self._last_preset]
+ return HEAT_MODE.NAME_FOR_NUM[self.body["heat_mode"]["value"]]
+
+ @property
+ def preset_modes(self):
+ """All available presets."""
+ return [
+ HEAT_MODE.NAME_FOR_NUM[mode_num] for mode_num in self._configured_heat_modes
+ ]
+
+ @property
+ def supported_features(self):
+ """Supported features of the heater."""
+ return SUPPORTED_FEATURES
+
+ async def async_set_temperature(self, **kwargs) -> None:
+ """Change the setpoint of the heater."""
+ if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
+ raise ValueError(f"Expected attribute {ATTR_TEMPERATURE}")
+
+ async with self.coordinator.api_lock:
+ success = await self.hass.async_add_executor_job(
+ self.gateway.set_heat_temp, int(self._data_key), int(temperature)
+ )
+
+ if success:
+ await self.coordinator.async_request_refresh()
+ else:
+ raise HomeAssistantError(
+ f"Failed to set_temperature {temperature} on body {self.body['body_type']['value']}"
+ )
+
+ async def async_set_hvac_mode(self, hvac_mode) -> None:
+ """Set the operation mode."""
+ if hvac_mode == HVAC_MODE_OFF:
+ mode = HEAT_MODE.OFF
+ else:
+ mode = HEAT_MODE.NUM_FOR_NAME[self.preset_mode]
+
+ async with self.coordinator.api_lock:
+ success = await self.hass.async_add_executor_job(
+ self.gateway.set_heat_mode, int(self._data_key), int(mode)
+ )
+
+ if success:
+ await self.coordinator.async_request_refresh()
+ else:
+ raise HomeAssistantError(
+ f"Failed to set_hvac_mode {mode} on body {self.body['body_type']['value']}"
+ )
+
+ async def async_set_preset_mode(self, preset_mode: str) -> None:
+ """Set the preset mode."""
+ _LOGGER.debug("Setting last_preset to %s", HEAT_MODE.NUM_FOR_NAME[preset_mode])
+ self._last_preset = mode = HEAT_MODE.NUM_FOR_NAME[preset_mode]
+ if self.hvac_mode == HVAC_MODE_OFF:
+ return
+
+ async with self.coordinator.api_lock:
+ success = await self.hass.async_add_executor_job(
+ self.gateway.set_heat_mode, int(self._data_key), int(mode)
+ )
+
+ if success:
+ await self.coordinator.async_request_refresh()
+ else:
+ raise HomeAssistantError(
+ f"Failed to set_preset_mode {mode} on body {self.body['body_type']['value']}"
+ )
+
+ async def async_added_to_hass(self):
+ """Run when entity is about to be added."""
+ await super().async_added_to_hass()
+
+ _LOGGER.debug("Startup last preset is %s", self._last_preset)
+ if self._last_preset is not None:
+ return
+ prev_state = await self.async_get_last_state()
+ if (
+ prev_state is not None
+ and prev_state.attributes.get(ATTR_PRESET_MODE) is not None
+ ):
+ _LOGGER.debug(
+ "Startup setting last_preset to %s from prev_state",
+ HEAT_MODE.NUM_FOR_NAME[prev_state.attributes.get(ATTR_PRESET_MODE)],
+ )
+ self._last_preset = HEAT_MODE.NUM_FOR_NAME[
+ prev_state.attributes.get(ATTR_PRESET_MODE)
+ ]
+ else:
+ _LOGGER.debug(
+ "Startup setting last_preset to default (%s)",
+ self._configured_heat_modes[0],
+ )
+ self._last_preset = self._configured_heat_modes[0]
+
+ @property
+ def body(self):
+ """Shortcut to access body data."""
+ return self.coordinator.data["bodies"][self._data_key]
diff --git a/homeassistant/components/screenlogic/config_flow.py b/homeassistant/components/screenlogic/config_flow.py
new file mode 100644
index 00000000000000..4f388722117129
--- /dev/null
+++ b/homeassistant/components/screenlogic/config_flow.py
@@ -0,0 +1,217 @@
+"""Config flow for ScreenLogic."""
+import logging
+
+from screenlogicpy import ScreenLogicError, discover
+from screenlogicpy.const import SL_GATEWAY_IP, SL_GATEWAY_NAME, SL_GATEWAY_PORT
+from screenlogicpy.requests import login
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS
+from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL
+from homeassistant.core import callback
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.device_registry import format_mac
+
+from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, MIN_SCAN_INTERVAL
+
+_LOGGER = logging.getLogger(__name__)
+
+GATEWAY_SELECT_KEY = "selected_gateway"
+GATEWAY_MANUAL_ENTRY = "manual"
+
+PENTAIR_OUI = "00-C0-33"
+
+
+async def async_discover_gateways_by_unique_id(hass):
+ """Discover gateways and return a dict of them by unique id."""
+ discovered_gateways = {}
+ try:
+ hosts = await hass.async_add_executor_job(discover)
+ _LOGGER.debug("Discovered hosts: %s", hosts)
+ except ScreenLogicError as ex:
+ _LOGGER.debug(ex)
+ return discovered_gateways
+
+ for host in hosts:
+ mac = _extract_mac_from_name(host[SL_GATEWAY_NAME])
+ discovered_gateways[mac] = host
+
+ _LOGGER.debug("Discovered gateways: %s", discovered_gateways)
+ return discovered_gateways
+
+
+def _extract_mac_from_name(name):
+ return format_mac(f"{PENTAIR_OUI}-{name.split(':')[1].strip()}")
+
+
+def short_mac(mac):
+ """Short version of the mac as seen in the app."""
+ return "-".join(mac.split(":")[3:]).upper()
+
+
+def name_for_mac(mac):
+ """Derive the gateway name from the mac."""
+ return f"Pentair: {short_mac(mac)}"
+
+
+async def async_get_mac_address(hass, ip_address, port):
+ """Connect to a screenlogic gateway and return the mac address."""
+ connected_socket = await hass.async_add_executor_job(
+ login.create_socket,
+ ip_address,
+ port,
+ )
+ if not connected_socket:
+ raise ScreenLogicError("Unknown socket error")
+ return await hass.async_add_executor_job(login.gateway_connect, connected_socket)
+
+
+class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Config flow to setup screen logic devices."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ def __init__(self):
+ """Initialize ScreenLogic ConfigFlow."""
+ self.discovered_gateways = {}
+ self.discovered_ip = None
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for ScreenLogic."""
+ return ScreenLogicOptionsFlowHandler(config_entry)
+
+ async def async_step_user(self, user_input=None):
+ """Handle the start of the config flow."""
+ self.discovered_gateways = await async_discover_gateways_by_unique_id(self.hass)
+ return await self.async_step_gateway_select()
+
+ async def async_step_dhcp(self, dhcp_discovery):
+ """Handle dhcp discovery."""
+ mac = _extract_mac_from_name(dhcp_discovery[HOSTNAME])
+ await self.async_set_unique_id(mac)
+ self._abort_if_unique_id_configured(
+ updates={CONF_IP_ADDRESS: dhcp_discovery[IP_ADDRESS]}
+ )
+ self.discovered_ip = dhcp_discovery[IP_ADDRESS]
+ self.context["title_placeholders"] = {"name": dhcp_discovery[HOSTNAME]}
+ return await self.async_step_gateway_entry()
+
+ async def async_step_gateway_select(self, user_input=None):
+ """Handle the selection of a discovered ScreenLogic gateway."""
+ existing = self._async_current_ids()
+ unconfigured_gateways = {
+ mac: gateway[SL_GATEWAY_NAME]
+ for mac, gateway in self.discovered_gateways.items()
+ if mac not in existing
+ }
+
+ if not unconfigured_gateways:
+ return await self.async_step_gateway_entry()
+
+ errors = {}
+ if user_input is not None:
+ if user_input[GATEWAY_SELECT_KEY] == GATEWAY_MANUAL_ENTRY:
+ return await self.async_step_gateway_entry()
+
+ mac = user_input[GATEWAY_SELECT_KEY]
+ selected_gateway = self.discovered_gateways[mac]
+ await self.async_set_unique_id(mac, raise_on_progress=False)
+ self._abort_if_unique_id_configured()
+ return self.async_create_entry(
+ title=name_for_mac(mac),
+ data={
+ CONF_IP_ADDRESS: selected_gateway[SL_GATEWAY_IP],
+ CONF_PORT: selected_gateway[SL_GATEWAY_PORT],
+ },
+ )
+
+ return self.async_show_form(
+ step_id="gateway_select",
+ data_schema=vol.Schema(
+ {
+ vol.Required(GATEWAY_SELECT_KEY): vol.In(
+ {
+ **unconfigured_gateways,
+ GATEWAY_MANUAL_ENTRY: "Manually configure a ScreenLogic gateway",
+ }
+ )
+ }
+ ),
+ errors=errors,
+ description_placeholders={},
+ )
+
+ async def async_step_gateway_entry(self, user_input=None):
+ """Handle the manual entry of a ScreenLogic gateway."""
+ errors = {}
+ ip_address = self.discovered_ip
+ port = 80
+
+ if user_input is not None:
+ ip_address = user_input[CONF_IP_ADDRESS]
+ port = user_input[CONF_PORT]
+ try:
+ mac = format_mac(
+ await async_get_mac_address(self.hass, ip_address, port)
+ )
+ except ScreenLogicError as ex:
+ _LOGGER.debug(ex)
+ errors[CONF_IP_ADDRESS] = "cannot_connect"
+
+ if not errors:
+ await self.async_set_unique_id(mac, raise_on_progress=False)
+ self._abort_if_unique_id_configured()
+ return self.async_create_entry(
+ title=name_for_mac(mac),
+ data={
+ CONF_IP_ADDRESS: ip_address,
+ CONF_PORT: port,
+ },
+ )
+
+ return self.async_show_form(
+ step_id="gateway_entry",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_IP_ADDRESS, default=ip_address): str,
+ vol.Required(CONF_PORT, default=port): int,
+ }
+ ),
+ errors=errors,
+ description_placeholders={},
+ )
+
+
+class ScreenLogicOptionsFlowHandler(config_entries.OptionsFlow):
+ """Handles the options for the ScreenLogic integration."""
+
+ def __init__(self, config_entry: config_entries.ConfigEntry):
+ """Init the screen logic options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(self, user_input=None):
+ """Manage the options."""
+ if user_input is not None:
+ return self.async_create_entry(
+ title=self.config_entry.title, data=user_input
+ )
+
+ current_interval = self.config_entry.options.get(
+ CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
+ )
+ return self.async_show_form(
+ step_id="init",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ CONF_SCAN_INTERVAL,
+ default=current_interval,
+ ): vol.All(cv.positive_int, vol.Clamp(min=MIN_SCAN_INTERVAL))
+ }
+ ),
+ description_placeholders={"gateway_name": self.config_entry.title},
+ )
diff --git a/homeassistant/components/screenlogic/const.py b/homeassistant/components/screenlogic/const.py
new file mode 100644
index 00000000000000..d777dc6ddc5031
--- /dev/null
+++ b/homeassistant/components/screenlogic/const.py
@@ -0,0 +1,7 @@
+"""Constants for the ScreenLogic integration."""
+
+DOMAIN = "screenlogic"
+DEFAULT_SCAN_INTERVAL = 30
+MIN_SCAN_INTERVAL = 10
+
+DISCOVERED_GATEWAYS = "_discovered_gateways"
diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json
new file mode 100644
index 00000000000000..ab3d08a0702d48
--- /dev/null
+++ b/homeassistant/components/screenlogic/manifest.json
@@ -0,0 +1,11 @@
+{
+ "domain": "screenlogic",
+ "name": "Pentair ScreenLogic",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/screenlogic",
+ "requirements": ["screenlogicpy==0.2.1"],
+ "codeowners": [
+ "@dieselrabbit"
+ ],
+ "dhcp": [{"hostname":"pentair: *","macaddress":"00C033*"}]
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py
new file mode 100644
index 00000000000000..38bde2afd760ea
--- /dev/null
+++ b/homeassistant/components/screenlogic/sensor.py
@@ -0,0 +1,105 @@
+"""Support for a ScreenLogic Sensor."""
+import logging
+
+from screenlogicpy.const import DEVICE_TYPE
+
+from homeassistant.components.sensor import (
+ DEVICE_CLASS_POWER,
+ DEVICE_CLASS_TEMPERATURE,
+ SensorEntity,
+)
+
+from . import ScreenlogicEntity
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+PUMP_SENSORS = ("currentWatts", "currentRPM", "currentGPM")
+
+SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = {
+ DEVICE_TYPE.TEMPERATURE: DEVICE_CLASS_TEMPERATURE,
+ DEVICE_TYPE.ENERGY: DEVICE_CLASS_POWER,
+}
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up entry."""
+ entities = []
+ data = hass.data[DOMAIN][config_entry.entry_id]
+ coordinator = data["coordinator"]
+ # Generic sensors
+ for sensor in data["devices"]["sensor"]:
+ entities.append(ScreenLogicSensor(coordinator, sensor))
+ # Pump sensors
+ for pump in data["devices"]["pump"]:
+ for pump_key in PUMP_SENSORS:
+ entities.append(ScreenLogicPumpSensor(coordinator, pump, pump_key))
+
+ async_add_entities(entities)
+
+
+class ScreenLogicSensor(ScreenlogicEntity, SensorEntity):
+ """Representation of a ScreenLogic sensor entity."""
+
+ @property
+ def name(self):
+ """Name of the sensor."""
+ return f"{self.gateway_name} {self.sensor['name']}"
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return self.sensor.get("unit")
+
+ @property
+ def device_class(self):
+ """Device class of the sensor."""
+ device_class = self.sensor.get("device_type")
+ return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_class)
+
+ @property
+ def state(self):
+ """State of the sensor."""
+ value = self.sensor["value"]
+ return (value - 1) if "supply" in self._data_key else value
+
+ @property
+ def sensor(self):
+ """Shortcut to access the sensor data."""
+ return self.coordinator.data["sensors"][self._data_key]
+
+
+class ScreenLogicPumpSensor(ScreenlogicEntity, SensorEntity):
+ """Representation of a ScreenLogic pump sensor entity."""
+
+ def __init__(self, coordinator, pump, key):
+ """Initialize of the pump sensor."""
+ super().__init__(coordinator, f"{key}_{pump}")
+ self._pump_id = pump
+ self._key = key
+
+ @property
+ def name(self):
+ """Return the pump sensor name."""
+ return f"{self.gateway_name} {self.pump_sensor['name']}"
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return self.pump_sensor.get("unit")
+
+ @property
+ def device_class(self):
+ """Return the device class."""
+ device_class = self.pump_sensor.get("device_type")
+ return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_class)
+
+ @property
+ def state(self):
+ """State of the pump sensor."""
+ return self.pump_sensor["value"]
+
+ @property
+ def pump_sensor(self):
+ """Shortcut to access the pump sensor data."""
+ return self.coordinator.data["pumps"][self._pump_id][self._key]
diff --git a/homeassistant/components/screenlogic/strings.json b/homeassistant/components/screenlogic/strings.json
new file mode 100644
index 00000000000000..155eeb3043e1ac
--- /dev/null
+++ b/homeassistant/components/screenlogic/strings.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "flow_title": "ScreenLogic {name}",
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ },
+ "step": {
+ "gateway_entry": {
+ "title": "ScreenLogic",
+ "description": "Enter your ScreenLogic Gateway information.",
+ "data": {
+ "ip_address": "[%key:common::config_flow::data::ip%]",
+ "port": "[%key:common::config_flow::data::port%]"
+ }
+ },
+ "gateway_select": {
+ "title": "ScreenLogic",
+ "description": "The following ScreenLogic gateways were discovered. Please select one to configure, or choose to manually configure a ScreenLogic gateway.",
+ "data": {
+ "selected_gateway": "Gateway"
+ }
+ }
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ }
+ },
+ "options":{
+ "step": {
+ "init": {
+ "title": "ScreenLogic",
+ "description": "Specify settings for {gateway_name}",
+ "data": {
+ "scan_interval": "Seconds between scans"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/switch.py b/homeassistant/components/screenlogic/switch.py
new file mode 100644
index 00000000000000..e0077b1d62dc6b
--- /dev/null
+++ b/homeassistant/components/screenlogic/switch.py
@@ -0,0 +1,63 @@
+"""Support for a ScreenLogic 'circuit' switch."""
+import logging
+
+from screenlogicpy.const import ON_OFF
+
+from homeassistant.components.switch import SwitchEntity
+
+from . import ScreenlogicEntity
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up entry."""
+ entities = []
+ data = hass.data[DOMAIN][config_entry.entry_id]
+ coordinator = data["coordinator"]
+
+ for switch in data["devices"]["switch"]:
+ entities.append(ScreenLogicSwitch(coordinator, switch))
+ async_add_entities(entities)
+
+
+class ScreenLogicSwitch(ScreenlogicEntity, SwitchEntity):
+ """ScreenLogic switch entity."""
+
+ @property
+ def name(self):
+ """Get the name of the switch."""
+ return f"{self.gateway_name} {self.circuit['name']}"
+
+ @property
+ def is_on(self) -> bool:
+ """Get whether the switch is in on state."""
+ return self.circuit["value"] == 1
+
+ async def async_turn_on(self, **kwargs) -> None:
+ """Send the ON command."""
+ return await self._async_set_circuit(ON_OFF.ON)
+
+ async def async_turn_off(self, **kwargs) -> None:
+ """Send the OFF command."""
+ return await self._async_set_circuit(ON_OFF.OFF)
+
+ async def _async_set_circuit(self, circuit_value) -> None:
+ async with self.coordinator.api_lock:
+ success = await self.hass.async_add_executor_job(
+ self.gateway.set_circuit, self._data_key, circuit_value
+ )
+
+ if success:
+ _LOGGER.debug("Turn %s %s", self._data_key, circuit_value)
+ await self.coordinator.async_request_refresh()
+ else:
+ _LOGGER.warning(
+ "Failed to set_circuit %s %s", self._data_key, circuit_value
+ )
+
+ @property
+ def circuit(self):
+ """Shortcut to access the circuit."""
+ return self.coordinator.data["circuits"][self._data_key]
diff --git a/homeassistant/components/screenlogic/translations/ca.json b/homeassistant/components/screenlogic/translations/ca.json
new file mode 100644
index 00000000000000..68bfad1ff94578
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/ca.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3"
+ },
+ "flow_title": "ScreenLogic {name}",
+ "step": {
+ "gateway_entry": {
+ "data": {
+ "ip_address": "Adre\u00e7a IP",
+ "port": "Port"
+ },
+ "description": "Introdueix la informaci\u00f3 de la passarel\u00b7la ScreenLogic.",
+ "title": "ScreenLogic"
+ },
+ "gateway_select": {
+ "data": {
+ "selected_gateway": "Passarel\u00b7la"
+ },
+ "description": "S'han descobert les seg\u00fcents passarel\u00b7les ScreenLogic. Tria'n una per configurar-la o escull configurar manualment una passarel\u00b7la ScreenLogic.",
+ "title": "ScreenLogic"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Segons entre escanejos"
+ },
+ "description": "Especifica la configuraci\u00f3 de {gateway_name}",
+ "title": "ScreenLogic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/translations/cs.json b/homeassistant/components/screenlogic/translations/cs.json
new file mode 100644
index 00000000000000..a3fa8b759f8d4c
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/cs.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno"
+ },
+ "error": {
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit"
+ },
+ "flow_title": "ScreenLogic {name}",
+ "step": {
+ "gateway_entry": {
+ "data": {
+ "ip_address": "IP adresa",
+ "port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/translations/de.json b/homeassistant/components/screenlogic/translations/de.json
new file mode 100644
index 00000000000000..6afe42e37ee1b6
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/de.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
+ "flow_title": "ScreenLogic {name}",
+ "step": {
+ "gateway_entry": {
+ "data": {
+ "ip_address": "IP-Adresse",
+ "port": "Port"
+ },
+ "description": "Gib deine ScreenLogic Gateway-Informationen ein.",
+ "title": "ScreenLogic"
+ },
+ "gateway_select": {
+ "data": {
+ "selected_gateway": ""
+ },
+ "description": "Die folgenden ScreenLogic-Gateways wurden erkannt. Bitte w\u00e4hle eines aus, um es zu konfigurieren oder w\u00e4hle ein ScreenLogic-Gateway zum manuellen Konfigurieren.",
+ "title": "ScreenLogic"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Sekunden zwischen den Scans"
+ },
+ "description": "Einstellungen f\u00fcr {gateway_name} angeben",
+ "title": "ScreenLogic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/translations/el.json b/homeassistant/components/screenlogic/translations/el.json
new file mode 100644
index 00000000000000..26906ac3b29059
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/el.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af"
+ },
+ "error": {
+ "cannot_connect": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
+ },
+ "flow_title": "ScreenLogic {name}",
+ "step": {
+ "gateway_entry": {
+ "data": {
+ "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP",
+ "port": "\u0398\u03cd\u03c1\u03b1"
+ },
+ "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03b1\u03c2 \u03c4\u03b7\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2 ScreenLogic.",
+ "title": "ScreenLogic"
+ },
+ "gateway_select": {
+ "data": {
+ "selected_gateway": "\u03a0\u03cd\u03bb\u03b7"
+ },
+ "description": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b1\u03bd \u03bf\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c0\u03cd\u03bb\u03b5\u03c2 ScreenLogic. \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03af\u03b1 \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ae \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 ScreenLogic \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1.",
+ "title": "ScreenLogic"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\u0394\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c3\u03b1\u03c1\u03ce\u03c3\u03b5\u03c9\u03bd"
+ },
+ "description": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {gateway_name}",
+ "title": "ScreenLogic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/translations/en.json b/homeassistant/components/screenlogic/translations/en.json
new file mode 100644
index 00000000000000..2572fdf38fa190
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/en.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect"
+ },
+ "flow_title": "ScreenLogic {name}",
+ "step": {
+ "gateway_entry": {
+ "data": {
+ "ip_address": "IP Address",
+ "port": "Port"
+ },
+ "description": "Enter your ScreenLogic Gateway information.",
+ "title": "ScreenLogic"
+ },
+ "gateway_select": {
+ "data": {
+ "selected_gateway": "Gateway"
+ },
+ "description": "The following ScreenLogic gateways were discovered. Please select one to configure, or choose to manually configure a ScreenLogic gateway.",
+ "title": "ScreenLogic"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Seconds between scans"
+ },
+ "description": "Specify settings for {gateway_name}",
+ "title": "ScreenLogic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/translations/es-419.json b/homeassistant/components/screenlogic/translations/es-419.json
new file mode 100644
index 00000000000000..4e12c0c2a91745
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/es-419.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "flow_title": "ScreenLogic {name}",
+ "step": {
+ "gateway_entry": {
+ "data": {
+ "ip_address": "Direcci\u00f3n IP",
+ "port": "Puerto"
+ },
+ "description": "Introduzca la informaci\u00f3n de su portal ScreenLogic.",
+ "title": "ScreenLogic"
+ },
+ "gateway_select": {
+ "data": {
+ "selected_gateway": "Portal"
+ },
+ "description": "Se descubrieron los siguientes portales ScreenLogic. Seleccione uno para configurarlo, \u00f3 elija configurar manualmente el portal ScreenLogic.",
+ "title": "ScreenLogic"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Segundos entre escaneos"
+ },
+ "description": "Especificar la configuraci\u00f3n para {gateway_name}",
+ "title": "ScreenLogic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/translations/es.json b/homeassistant/components/screenlogic/translations/es.json
new file mode 100644
index 00000000000000..8e9513d4f7530e
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/es.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "flow_title": "ScreenLogic {name}",
+ "step": {
+ "gateway_entry": {
+ "description": "Introduzca la informaci\u00f3n de su ScreenLogic Gateway.",
+ "title": "ScreenLogic"
+ },
+ "gateway_select": {
+ "data": {
+ "selected_gateway": "Puerta de enlace"
+ },
+ "description": "Se han descubierto las siguientes puertas de enlace ScreenLogic. Seleccione una para configurarla o elija configurar manualmente una puerta de enlace ScreenLogic.",
+ "title": "ScreenLogic"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Segundos entre exploraciones"
+ },
+ "description": "Especificar la configuraci\u00f3n de {gateway_name}",
+ "title": "ScreenLogic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/translations/et.json b/homeassistant/components/screenlogic/translations/et.json
new file mode 100644
index 00000000000000..cf2cf19418fcbd
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/et.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus"
+ },
+ "flow_title": "ScreenLogic {name}",
+ "step": {
+ "gateway_entry": {
+ "data": {
+ "ip_address": "IP aadress",
+ "port": "Port"
+ },
+ "description": "Sisesta oma ScreenLogic Gateway teave.",
+ "title": ""
+ },
+ "gateway_select": {
+ "data": {
+ "selected_gateway": "L\u00fc\u00fcs"
+ },
+ "description": "Avastati j\u00e4rgmised ScreenLogicu l\u00fc\u00fcsid. Vali seadistatav l\u00fc\u00fcs v\u00f5i seadista ScreenLogicu l\u00fc\u00fcs k\u00e4sitsi.",
+ "title": ""
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "P\u00e4ringute vahe sekundites"
+ },
+ "description": "M\u00e4\u00e4ra {gateway_name} s\u00e4tted",
+ "title": ""
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/translations/fr.json b/homeassistant/components/screenlogic/translations/fr.json
new file mode 100644
index 00000000000000..efd9740ac31274
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/fr.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de connexion"
+ },
+ "flow_title": "ScreenLogic {nom}",
+ "step": {
+ "gateway_entry": {
+ "data": {
+ "ip_address": "Adresse IP",
+ "port": "Port"
+ },
+ "description": "Entrez vos informations de passerelle ScreenLogic.",
+ "title": "ScreenLogic"
+ },
+ "gateway_select": {
+ "data": {
+ "selected_gateway": "Passerelle"
+ },
+ "description": "Les passerelles ScreenLogic suivantes ont \u00e9t\u00e9 d\u00e9couvertes. S\u2019il vous pla\u00eet s\u00e9lectionner un \u00e0 configurer, ou choisissez de configurer manuellement une passerelle ScreenLogic.",
+ "title": "ScreenLogic"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Secondes entre les scans"
+ },
+ "description": "Sp\u00e9cifiez les param\u00e8tres pour {gateway_name}",
+ "title": "ScreenLogic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/translations/hu.json b/homeassistant/components/screenlogic/translations/hu.json
new file mode 100644
index 00000000000000..59e48fda273298
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/hu.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "flow_title": "ScreenLogic {name}",
+ "step": {
+ "gateway_entry": {
+ "data": {
+ "ip_address": "IP c\u00edm",
+ "port": "Port"
+ },
+ "description": "Add meg a ScreenLogic Gateway adatait.",
+ "title": "ScreenLogic"
+ },
+ "gateway_select": {
+ "data": {
+ "selected_gateway": "Gateway"
+ },
+ "title": "ScreenLogic"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Szkennel\u00e9sek k\u00f6z\u00f6tti m\u00e1sodpercek"
+ },
+ "description": "{gateway_name} be\u00e1ll\u00edt\u00e1sainak megad\u00e1sa",
+ "title": "ScreenLogic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/translations/it.json b/homeassistant/components/screenlogic/translations/it.json
new file mode 100644
index 00000000000000..8fc3c346c0f425
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/it.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi"
+ },
+ "flow_title": "ScreenLogic {name}",
+ "step": {
+ "gateway_entry": {
+ "data": {
+ "ip_address": "Indirizzo IP",
+ "port": "Porta"
+ },
+ "description": "Inserisci le informazioni del tuo gateway ScreenLogic.",
+ "title": "ScreenLogic"
+ },
+ "gateway_select": {
+ "data": {
+ "selected_gateway": "Gateway"
+ },
+ "description": "Sono stati individuati i gateway ScreenLogic seguenti. Selezionarne uno da configurare oppure scegliere di configurare manualmente un gateway ScreenLogic.",
+ "title": "ScreenLogic"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Secondi tra le scansioni"
+ },
+ "description": "Specifica le impostazioni per {gateway_name}",
+ "title": "ScreenLogic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/translations/ko.json b/homeassistant/components/screenlogic/translations/ko.json
new file mode 100644
index 00000000000000..94ddca6830a4e7
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/ko.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "flow_title": "ScreenLogic {name}",
+ "step": {
+ "gateway_entry": {
+ "data": {
+ "ip_address": "IP \uc8fc\uc18c",
+ "port": "\ud3ec\ud2b8"
+ },
+ "description": "ScreenLogic \uac8c\uc774\ud2b8\uc6e8\uc774 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "ScreenLogic"
+ },
+ "gateway_select": {
+ "data": {
+ "selected_gateway": "\uac8c\uc774\ud2b8\uc6e8\uc774"
+ },
+ "description": "\ub2e4\uc74c ScreenLogic \uac8c\uc774\ud2b8\uc6e8\uc774\uac00 \uac80\uc0c9\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uad6c\uc131\ud558\uac70\ub098 \uc218\ub3d9\uc73c\ub85c ScreenLogic \uac8c\uc774\ud2b8\uc6e8\uc774\ub85c \uad6c\uc131\ud560 \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.",
+ "title": "ScreenLogic"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ucd08)"
+ },
+ "description": "{gateway_name}\uc5d0 \ub300\ud55c \uc124\uc815 \uc9c0\uc815\ud558\uae30",
+ "title": "ScreenLogic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/translations/nl.json b/homeassistant/components/screenlogic/translations/nl.json
new file mode 100644
index 00000000000000..7c752e0ae4d753
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/nl.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken"
+ },
+ "flow_title": "ScreenLogic {name}",
+ "step": {
+ "gateway_entry": {
+ "data": {
+ "ip_address": "IP-adres",
+ "port": "Poort"
+ },
+ "description": "Voer uw ScreenLogic Gateway informatie in.",
+ "title": "ScreenLogic"
+ },
+ "gateway_select": {
+ "data": {
+ "selected_gateway": "Gateway"
+ },
+ "description": "De volgende ScreenLogic gateways werden ontdekt. Selecteer er een om te configureren, of kies ervoor om handmatig een ScreenLogic gateway te configureren.",
+ "title": "ScreenLogic"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Seconden tussen scans"
+ },
+ "description": "Geef instellingen op voor {gateway_name}",
+ "title": "ScreenLogic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/translations/no.json b/homeassistant/components/screenlogic/translations/no.json
new file mode 100644
index 00000000000000..0ca4827514a0f5
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/no.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes"
+ },
+ "flow_title": "ScreenLogic {name}",
+ "step": {
+ "gateway_entry": {
+ "data": {
+ "ip_address": "IP adresse",
+ "port": "Port"
+ },
+ "description": "Skriv inn din ScreenLogic Gateway-informasjon.",
+ "title": "ScreenLogic"
+ },
+ "gateway_select": {
+ "data": {
+ "selected_gateway": "Gateway"
+ },
+ "description": "F\u00f8lgende ScreenLogic-gateways ble oppdaget. Velg en \u00e5 konfigurere, eller velg \u00e5 konfigurere en ScreenLogic gateway manuelt.",
+ "title": "ScreenLogic"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Sekunder mellom skanninger"
+ },
+ "description": "Angi innstillinger for {gateway_name}",
+ "title": "ScreenLogic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/translations/pl.json b/homeassistant/components/screenlogic/translations/pl.json
new file mode 100644
index 00000000000000..64e2573ddb0a94
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/pl.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ },
+ "flow_title": "ScreenLogic {name}",
+ "step": {
+ "gateway_entry": {
+ "data": {
+ "ip_address": "Adres IP",
+ "port": "Port"
+ },
+ "description": "Wprowad\u017a informacje o bramce ScreenLogic.",
+ "title": "ScreenLogic"
+ },
+ "gateway_select": {
+ "data": {
+ "selected_gateway": "Bramka"
+ },
+ "description": "Wykryto nast\u0119puj\u0105ce bramki ScreenLogic. Wybierz jedn\u0105 do skonfigurowania lub wybierz opcj\u0119 r\u0119cznej konfiguracji bramki ScreenLogic.",
+ "title": "ScreenLogic"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji"
+ },
+ "description": "Okre\u015bl ustawienia dla {gateway_name}",
+ "title": "ScreenLogic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/translations/ru.json b/homeassistant/components/screenlogic/translations/ru.json
new file mode 100644
index 00000000000000..a657b7360c790d
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/ru.json
@@ -0,0 +1,39 @@
+{
+ "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."
+ },
+ "flow_title": "ScreenLogic {name}",
+ "step": {
+ "gateway_entry": {
+ "data": {
+ "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0448\u043b\u044e\u0437\u0435 ScreenLogic.",
+ "title": "ScreenLogic"
+ },
+ "gateway_select": {
+ "data": {
+ "selected_gateway": "\u0428\u043b\u044e\u0437"
+ },
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043e\u0434\u0438\u043d \u0438\u0437 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0445 \u0448\u043b\u044e\u0437\u043e\u0432 ScreenLogic \u0438\u043b\u0438 \u0443\u043a\u0430\u0436\u0438\u0442\u0435 \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.",
+ "title": "ScreenLogic"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
+ },
+ "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043b\u044f {gateway_name}",
+ "title": "ScreenLogic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/screenlogic/translations/zh-Hant.json b/homeassistant/components/screenlogic/translations/zh-Hant.json
new file mode 100644
index 00000000000000..40ca94fd779a3d
--- /dev/null
+++ b/homeassistant/components/screenlogic/translations/zh-Hant.json
@@ -0,0 +1,39 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
+ },
+ "flow_title": "ScreenLogic {name}",
+ "step": {
+ "gateway_entry": {
+ "data": {
+ "ip_address": "IP \u4f4d\u5740",
+ "port": "\u901a\u8a0a\u57e0"
+ },
+ "description": "\u8f38\u5165 ScreenLogic \u9598\u9053\u5668\u8cc7\u8a0a\u3002",
+ "title": "ScreenLogic"
+ },
+ "gateway_select": {
+ "data": {
+ "selected_gateway": "\u9598\u9053\u5668"
+ },
+ "description": "\u641c\u5c0b\u5230\u4ee5\u4e0b ScreenLogic \u9598\u9053\u5668\uff0c\u8acb\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u9598\u9053\u5668\u3001\u6216\u9032\u884c\u624b\u52d5\u8a2d\u5b9a\u3002",
+ "title": "ScreenLogic"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\u6383\u63cf\u9593\u9694\u79d2\u6578"
+ },
+ "description": "{gateway_name} \u7279\u5b9a\u8a2d\u5b9a",
+ "title": "ScreenLogic"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py
index eab30e01ee2cd7..8f2e0743f77d27 100644
--- a/homeassistant/components/script/__init__.py
+++ b/homeassistant/components/script/__init__.py
@@ -1,16 +1,22 @@
"""Support for scripts."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import List
import voluptuous as vol
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_MODE,
ATTR_NAME,
CONF_ALIAS,
+ CONF_DEFAULT,
+ CONF_DESCRIPTION,
CONF_ICON,
CONF_MODE,
+ CONF_NAME,
+ CONF_SELECTOR,
CONF_SEQUENCE,
CONF_VARIABLES,
SERVICE_RELOAD,
@@ -27,16 +33,19 @@
from homeassistant.helpers.script import (
ATTR_CUR,
ATTR_MAX,
- ATTR_MODE,
CONF_MAX,
CONF_MAX_EXCEEDED,
SCRIPT_MODE_SINGLE,
Script,
make_script_schema,
)
+from homeassistant.helpers.selector import validate_selector
from homeassistant.helpers.service import async_set_service_schema
+from homeassistant.helpers.trace import trace_get, trace_path
from homeassistant.loader import bind_hass
+from .trace import trace_script
+
_LOGGER = logging.getLogger(__name__)
DOMAIN = "script"
@@ -45,9 +54,10 @@
ATTR_LAST_TRIGGERED = "last_triggered"
ATTR_VARIABLES = "variables"
-CONF_DESCRIPTION = "description"
+CONF_ADVANCED = "advanced"
CONF_EXAMPLE = "example"
CONF_FIELDS = "fields"
+CONF_REQUIRED = "required"
ENTITY_ID_FORMAT = DOMAIN + ".{}"
@@ -63,8 +73,13 @@
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
vol.Optional(CONF_FIELDS, default={}): {
cv.string: {
+ vol.Optional(CONF_ADVANCED, default=False): cv.boolean,
+ vol.Optional(CONF_DEFAULT): cv.match_all,
vol.Optional(CONF_DESCRIPTION): cv.string,
vol.Optional(CONF_EXAMPLE): cv.string,
+ vol.Optional(CONF_NAME): cv.string,
+ vol.Optional(CONF_REQUIRED, default=False): cv.boolean,
+ vol.Optional(CONF_SELECTOR): validate_selector,
}
},
},
@@ -89,7 +104,7 @@ def is_on(hass, entity_id):
@callback
-def scripts_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]:
+def scripts_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]:
"""Return all scripts that reference the entity."""
if DOMAIN not in hass.data:
return []
@@ -104,7 +119,7 @@ def scripts_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]:
@callback
-def entities_in_script(hass: HomeAssistant, entity_id: str) -> List[str]:
+def entities_in_script(hass: HomeAssistant, entity_id: str) -> list[str]:
"""Return all entities in script."""
if DOMAIN not in hass.data:
return []
@@ -120,7 +135,7 @@ def entities_in_script(hass: HomeAssistant, entity_id: str) -> List[str]:
@callback
-def scripts_with_device(hass: HomeAssistant, device_id: str) -> List[str]:
+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 []
@@ -135,7 +150,7 @@ def scripts_with_device(hass: HomeAssistant, device_id: str) -> List[str]:
@callback
-def devices_in_script(hass: HomeAssistant, entity_id: str) -> List[str]:
+def devices_in_script(hass: HomeAssistant, entity_id: str) -> list[str]:
"""Return all devices in script."""
if DOMAIN not in hass.data:
return []
@@ -150,6 +165,37 @@ def devices_in_script(hass: HomeAssistant, entity_id: str) -> List[str]:
return list(script_entity.script.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
+ ]
+
+
+@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]
+
+ script_entity = component.get_entity(entity_id)
+
+ if script_entity is None:
+ return []
+
+ return list(script_entity.script.referenced_areas)
+
+
async def async_setup(hass, config):
"""Load the scripts from the configuration."""
hass.data[DOMAIN] = component = EntityComponent(_LOGGER, DOMAIN, hass)
@@ -181,7 +227,10 @@ async def turn_off_service(service):
return
await asyncio.wait(
- [script_entity.async_turn_off() for script_entity in script_entities]
+ [
+ asyncio.create_task(script_entity.async_turn_off())
+ for script_entity in script_entities
+ ]
)
async def toggle_service(service):
@@ -217,7 +266,7 @@ async def service_handler(service):
)
script_entities = [
- ScriptEntity(hass, object_id, cfg)
+ ScriptEntity(hass, object_id, cfg, cfg.raw_config)
for object_id, cfg in config.get(DOMAIN, {}).items()
]
@@ -238,6 +287,7 @@ async def service_handler(service):
# Register the service description
service_desc = {
+ CONF_NAME: script_entity.name,
CONF_DESCRIPTION: cfg[CONF_DESCRIPTION],
CONF_FIELDS: cfg[CONF_FIELDS],
}
@@ -249,7 +299,7 @@ class ScriptEntity(ToggleEntity):
icon = None
- def __init__(self, hass, object_id, cfg):
+ def __init__(self, hass, object_id, cfg, raw_config):
"""Initialize the script."""
self.object_id = object_id
self.icon = cfg.get(CONF_ICON)
@@ -268,6 +318,7 @@ def __init__(self, hass, object_id, cfg):
variables=cfg.get(CONF_VARIABLES),
)
self._changed = asyncio.Event()
+ self._raw_config = raw_config
@property
def should_poll(self):
@@ -280,7 +331,7 @@ def name(self):
return self.script.name
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attrs = {
ATTR_LAST_TRIGGERED: self.script.last_triggered,
@@ -305,7 +356,11 @@ def async_change_listener(self):
self._changed.set()
async def async_turn_on(self, **kwargs):
- """Turn the script on."""
+ """Run the script.
+
+ Depending on the script's run mode, this may do nothing, restart the script or
+ fire an additional parallel run.
+ """
variables = kwargs.get("variables")
context = kwargs.get("context")
wait = kwargs.get("wait", True)
@@ -315,7 +370,7 @@ async def async_turn_on(self, **kwargs):
{ATTR_NAME: self.script.name, ATTR_ENTITY_ID: self.entity_id},
context=context,
)
- coro = self.script.async_run(variables, context)
+ coro = self._async_run(variables, context)
if wait:
await coro
return
@@ -327,8 +382,20 @@ async def async_turn_on(self, **kwargs):
self.hass.async_create_task(coro)
await self._changed.wait()
+ async def _async_run(self, variables, context):
+ with trace_script(
+ self.hass, self.object_id, self._raw_config, context
+ ) as script_trace:
+ # Prepare tracing the execution of the script's sequence
+ script_trace.set_trace(trace_get())
+ with trace_path("sequence"):
+ return await self.script.async_run(variables, context)
+
async def async_turn_off(self, **kwargs):
- """Turn script off."""
+ """Stop running the script.
+
+ If multiple runs are in progress, all will be stopped.
+ """
await self.script.async_stop()
async def async_will_remove_from_hass(self):
diff --git a/homeassistant/components/script/config.py b/homeassistant/components/script/config.py
index 3860a4d0119d3b..5da8bec5a8743b 100644
--- a/homeassistant/components/script/config.py
+++ b/homeassistant/components/script/config.py
@@ -25,8 +25,21 @@ async def async_validate_config_item(hass, config, full_config=None):
return config
+class ScriptConfig(dict):
+ """Dummy class to allow adding attributes."""
+
+ raw_config = None
+
+
async def _try_async_validate_config_item(hass, object_id, config, full_config=None):
"""Validate config item."""
+ raw_config = None
+ try:
+ raw_config = dict(config)
+ except ValueError:
+ # Invalid config
+ pass
+
try:
cv.slug(object_id)
config = await async_validate_config_item(hass, config, full_config)
@@ -34,6 +47,8 @@ async def _try_async_validate_config_item(hass, object_id, config, full_config=N
async_log_exception(ex, DOMAIN, full_config or config, hass)
return None
+ config = ScriptConfig(config)
+ config.raw_config = raw_config
return config
diff --git a/homeassistant/components/script/manifest.json b/homeassistant/components/script/manifest.json
index b9d333ce553ae7..ab14889a60cf1b 100644
--- a/homeassistant/components/script/manifest.json
+++ b/homeassistant/components/script/manifest.json
@@ -2,6 +2,7 @@
"domain": "script",
"name": "Scripts",
"documentation": "https://www.home-assistant.io/integrations/script",
+ "dependencies": ["trace"],
"codeowners": [
"@home-assistant/core"
],
diff --git a/homeassistant/components/script/services.yaml b/homeassistant/components/script/services.yaml
index 1347f760b546cf..b772b80a1d2055 100644
--- a/homeassistant/components/script/services.yaml
+++ b/homeassistant/components/script/services.yaml
@@ -1,25 +1,20 @@
# Describes the format for available python_script services
reload:
+ name: Reload
description: Reload all the available scripts
turn_on:
+ name: Turn on
description: Turn on script
- fields:
- entity_id:
- description: Name(s) of script to be turned on.
- example: "script.arrive_home"
+ target:
turn_off:
+ name: Turn off
description: Turn off script
- fields:
- entity_id:
- description: Name(s) of script to be turned off.
- example: "script.arrive_home"
+ target:
toggle:
+ name: Toggle
description: Toggle script
- fields:
- entity_id:
- description: Name(s) of script to be toggled.
- example: "script.arrive_home"
+ target:
diff --git a/homeassistant/components/script/trace.py b/homeassistant/components/script/trace.py
new file mode 100644
index 00000000000000..a8053feaa1e4a0
--- /dev/null
+++ b/homeassistant/components/script/trace.py
@@ -0,0 +1,39 @@
+"""Trace support for script."""
+from __future__ import annotations
+
+from contextlib import contextmanager
+from typing import Any
+
+from homeassistant.components.trace import ActionTrace, async_store_trace
+from homeassistant.core import Context
+
+
+class ScriptTrace(ActionTrace):
+ """Container for automation trace."""
+
+ def __init__(
+ self,
+ item_id: str,
+ config: dict[str, Any],
+ context: Context,
+ ):
+ """Container for automation trace."""
+ key = ("script", item_id)
+ super().__init__(key, config, None, context)
+
+
+@contextmanager
+def trace_script(hass, item_id, config, context):
+ """Trace execution of a script."""
+ trace = ScriptTrace(item_id, config, context)
+ async_store_trace(hass, trace)
+
+ try:
+ yield trace
+ except Exception as ex:
+ if item_id:
+ trace.set_error(ex)
+ raise ex
+ finally:
+ if item_id:
+ trace.finished()
diff --git a/homeassistant/components/script/translations/id.json b/homeassistant/components/script/translations/id.json
index 8b23be94861f1e..cac38736ea7d1e 100644
--- a/homeassistant/components/script/translations/id.json
+++ b/homeassistant/components/script/translations/id.json
@@ -1,8 +1,8 @@
{
"state": {
"_": {
- "off": "Off",
- "on": "On"
+ "off": "Mati",
+ "on": "Nyala"
}
},
"title": "Skrip"
diff --git a/homeassistant/components/script/translations/uk.json b/homeassistant/components/script/translations/uk.json
index bfff0258c6643c..ee494e264ae40a 100644
--- a/homeassistant/components/script/translations/uk.json
+++ b/homeassistant/components/script/translations/uk.json
@@ -5,5 +5,5 @@
"on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e"
}
},
- "title": "\u0421\u0446\u0435\u043d\u0430\u0440\u0456\u0439"
+ "title": "\u0421\u043a\u0440\u0438\u043f\u0442"
}
\ No newline at end of file
diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py
index 81e33aa24b53cf..3198f40720b3b7 100644
--- a/homeassistant/components/search/__init__.py
+++ b/homeassistant/components/search/__init__.py
@@ -19,7 +19,6 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True
-@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required("type"): "search/related",
@@ -38,12 +37,13 @@ async def async_setup(hass: HomeAssistant, config: dict):
vol.Required("item_id"): str,
}
)
-async def websocket_search_related(hass, connection, msg):
+@callback
+def websocket_search_related(hass, connection, msg):
"""Handle search."""
searcher = Searcher(
hass,
- await device_registry.async_get_registry(hass),
- await entity_registry.async_get_registry(hass),
+ device_registry.async_get(hass),
+ entity_registry.async_get(hass),
)
connection.send_result(
msg["id"], searcher.async_search(msg["item_type"], msg["item_id"])
@@ -127,6 +127,12 @@ def _resolve_area(self, area_id) -> None:
):
self._add_or_resolve("entity", entity_entry.entity_id)
+ for entity_id in script.scripts_with_area(self.hass, area_id):
+ self._add_or_resolve("entity", entity_id)
+
+ for entity_id in automation.automations_with_area(self.hass, area_id):
+ self._add_or_resolve("entity", entity_id)
+
@callback
def _resolve_device(self, device_id) -> None:
"""Resolve a device."""
@@ -198,6 +204,9 @@ def _resolve_automation(self, automation_entity_id) -> None:
for device in automation.devices_in_automation(self.hass, automation_entity_id):
self._add_or_resolve("device", device)
+ for area in automation.areas_in_automation(self.hass, automation_entity_id):
+ self._add_or_resolve("area", area)
+
@callback
def _resolve_script(self, script_entity_id) -> None:
"""Resolve a script.
@@ -210,6 +219,9 @@ def _resolve_script(self, script_entity_id) -> None:
for device in script.devices_in_script(self.hass, script_entity_id):
self._add_or_resolve("device", device)
+ for area in script.areas_in_script(self.hass, script_entity_id):
+ self._add_or_resolve("area", area)
+
@callback
def _resolve_group(self, group_entity_id) -> None:
"""Resolve a group.
diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py
index e116fb0e8611ee..165920dd8e5b75 100644
--- a/homeassistant/components/season/sensor.py
+++ b/homeassistant/components/season/sensor.py
@@ -6,10 +6,9 @@
import voluptuous as vol
from homeassistant import util
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, CONF_TYPE
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util.dt import utcnow
_LOGGER = logging.getLogger(__name__)
@@ -109,7 +108,7 @@ def get_season(date, hemisphere, season_tracking_type):
return HEMISPHERE_SEASON_SWAP.get(season)
-class Season(Entity):
+class Season(SensorEntity):
"""Representation of the current season."""
def __init__(self, hass, hemisphere, season_tracking_type, name):
diff --git a/homeassistant/components/season/translations/sensor.id.json b/homeassistant/components/season/translations/sensor.id.json
new file mode 100644
index 00000000000000..fd28dca59012c7
--- /dev/null
+++ b/homeassistant/components/season/translations/sensor.id.json
@@ -0,0 +1,16 @@
+{
+ "state": {
+ "season__season": {
+ "autumn": "Musim gugur",
+ "spring": "Musim semi",
+ "summer": "Musim panas",
+ "winter": "Musim dingin"
+ },
+ "season__season__": {
+ "autumn": "Musim gugur",
+ "spring": "Musim semi",
+ "summer": "Musim panas",
+ "winter": "Musim dingin"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/season/translations/sensor.uk.json b/homeassistant/components/season/translations/sensor.uk.json
index 2c694e287b1566..fa79d3cff07f51 100644
--- a/homeassistant/components/season/translations/sensor.uk.json
+++ b/homeassistant/components/season/translations/sensor.uk.json
@@ -1,5 +1,11 @@
{
"state": {
+ "season__season": {
+ "autumn": "\u041e\u0441\u0456\u043d\u044c",
+ "spring": "\u0412\u0435\u0441\u043d\u0430",
+ "summer": "\u041b\u0456\u0442\u043e",
+ "winter": "\u0417\u0438\u043c\u0430"
+ },
"season__season__": {
"autumn": "\u041e\u0441\u0456\u043d\u044c",
"spring": "\u0412\u0435\u0441\u043d\u0430",
diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json
index 1a93040837a631..21ebcd828c2f6a 100644
--- a/homeassistant/components/sendgrid/manifest.json
+++ b/homeassistant/components/sendgrid/manifest.json
@@ -2,6 +2,6 @@
"domain": "sendgrid",
"name": "SendGrid",
"documentation": "https://www.home-assistant.io/integrations/sendgrid",
- "requirements": ["sendgrid==6.5.0"],
+ "requirements": ["sendgrid==6.6.0"],
"codeowners": []
}
diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py
index 114c64c390b938..1689d8c48341d9 100644
--- a/homeassistant/components/sense/__init__.py
+++ b/homeassistant/components/sense/__init__.py
@@ -14,6 +14,7 @@
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
@@ -96,7 +97,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
password = entry_data[CONF_PASSWORD]
timeout = entry_data[CONF_TIMEOUT]
- gateway = ASyncSenseable(api_timeout=timeout, wss_timeout=timeout)
+ client_session = async_get_clientsession(hass)
+
+ gateway = ASyncSenseable(
+ api_timeout=timeout, wss_timeout=timeout, client_session=client_session
+ )
gateway.rate_limit = ACTIVE_UPDATE_RATE
try:
@@ -134,9 +139,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
SENSE_DISCOVERED_DEVICES_DATA: sense_discovered_devices,
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
async def async_sense_update(_):
@@ -164,8 +169,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/sense/binary_sensor.py b/homeassistant/components/sense/binary_sensor.py
index bc06721ae5e38b..ae5e4fc95bc914 100644
--- a/homeassistant/components/sense/binary_sensor.py
+++ b/homeassistant/components/sense/binary_sensor.py
@@ -101,7 +101,7 @@ def old_unique_id(self):
return self._id
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/sense/config_flow.py b/homeassistant/components/sense/config_flow.py
index f8b8ede6a4c5fc..4f88834eacad2c 100644
--- a/homeassistant/components/sense/config_flow.py
+++ b/homeassistant/components/sense/config_flow.py
@@ -6,9 +6,9 @@
from homeassistant import config_entries, core
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, SENSE_TIMEOUT_EXCEPTIONS
-from .const import DOMAIN # pylint:disable=unused-import; pylint:disable=unused-import
+from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, SENSE_TIMEOUT_EXCEPTIONS
_LOGGER = logging.getLogger(__name__)
@@ -27,8 +27,11 @@ async def validate_input(hass: core.HomeAssistant, data):
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
timeout = data[CONF_TIMEOUT]
+ client_session = async_get_clientsession(hass)
- gateway = ASyncSenseable(api_timeout=timeout, wss_timeout=timeout)
+ gateway = ASyncSenseable(
+ api_timeout=timeout, wss_timeout=timeout, client_session=client_session
+ )
gateway.rate_limit = ACTIVE_UPDATE_RATE
await gateway.authenticate(data[CONF_EMAIL], data[CONF_PASSWORD])
diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json
index bd132f1f983021..57028ccb395a69 100644
--- a/homeassistant/components/sense/manifest.json
+++ b/homeassistant/components/sense/manifest.json
@@ -2,7 +2,7 @@
"domain": "sense",
"name": "Sense",
"documentation": "https://www.home-assistant.io/integrations/sense",
- "requirements": ["sense_energy==0.8.1"],
+ "requirements": ["sense_energy==0.9.0"],
"codeowners": ["@kbickar"],
"config_flow": true,
"dhcp": [{"hostname":"sense-*","macaddress":"009D6B*"}, {"hostname":"sense-*","macaddress":"DCEFCA*"}]
diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py
index 25fa5943bd5fef..0af64f3f17d713 100644
--- a/homeassistant/components/sense/sensor.py
+++ b/homeassistant/components/sense/sensor.py
@@ -1,4 +1,5 @@
"""Support for monitoring a Sense energy sensor."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
DEVICE_CLASS_POWER,
@@ -8,7 +9,6 @@
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from .const import (
ACTIVE_NAME,
@@ -118,7 +118,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(devices)
-class SenseActiveSensor(Entity):
+class SenseActiveSensor(SensorEntity):
"""Implementation of a Sense energy sensor."""
def __init__(
@@ -163,7 +163,7 @@ def unit_of_measurement(self):
return POWER_WATT
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
@@ -207,7 +207,7 @@ def _async_update_from_data(self):
self.async_write_ha_state()
-class SenseVoltageSensor(Entity):
+class SenseVoltageSensor(SensorEntity):
"""Implementation of a Sense energy voltage sensor."""
def __init__(
@@ -247,7 +247,7 @@ def unit_of_measurement(self):
return VOLT
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
@@ -287,7 +287,7 @@ def _async_update_from_data(self):
self.async_write_ha_state()
-class SenseTrendsSensor(Entity):
+class SenseTrendsSensor(SensorEntity):
"""Implementation of a Sense energy sensor."""
def __init__(
@@ -333,7 +333,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
@@ -370,7 +370,7 @@ async def async_added_to_hass(self):
self.async_on_remove(self._coordinator.async_add_listener(self._async_update))
-class SenseEnergyDevice(Entity):
+class SenseEnergyDevice(SensorEntity):
"""Implementation of a Sense energy device."""
def __init__(self, sense_devices_data, device, sense_monitor_id):
@@ -415,7 +415,7 @@ def unit_of_measurement(self):
return POWER_WATT
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/sense/translations/de.json b/homeassistant/components/sense/translations/de.json
index de9e6877f25a27..9d4845ece79fa1 100644
--- a/homeassistant/components/sense/translations/de.json
+++ b/homeassistant/components/sense/translations/de.json
@@ -4,7 +4,7 @@
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
diff --git a/homeassistant/components/sense/translations/he.json b/homeassistant/components/sense/translations/he.json
new file mode 100644
index 00000000000000..3007c0e968c1dc
--- /dev/null
+++ b/homeassistant/components/sense/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sense/translations/hu.json b/homeassistant/components/sense/translations/hu.json
index 0085d9ea9c4b7d..4ecaf2ba0d0b0d 100644
--- a/homeassistant/components/sense/translations/hu.json
+++ b/homeassistant/components/sense/translations/hu.json
@@ -1,5 +1,13 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/sense/translations/id.json b/homeassistant/components/sense/translations/id.json
new file mode 100644
index 00000000000000..8d0d996e510279
--- /dev/null
+++ b/homeassistant/components/sense/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Email",
+ "password": "Kata Sandi"
+ },
+ "title": "Hubungkan ke Sense Energy Monitor Anda"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sense/translations/ko.json b/homeassistant/components/sense/translations/ko.json
index 26545db739a54d..269d8a76feaa6e 100644
--- a/homeassistant/components/sense/translations/ko.json
+++ b/homeassistant/components/sense/translations/ko.json
@@ -4,7 +4,7 @@
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
@@ -14,7 +14,7 @@
"email": "\uc774\uba54\uc77c",
"password": "\ube44\ubc00\ubc88\ud638"
},
- "title": "Sense Energy Monitor \uc5d0 \uc5f0\uacb0\ud558\uae30"
+ "title": "Sense Energy Monitor\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/sense/translations/nl.json b/homeassistant/components/sense/translations/nl.json
index ee9e61b5a38b35..df64e83da16028 100644
--- a/homeassistant/components/sense/translations/nl.json
+++ b/homeassistant/components/sense/translations/nl.json
@@ -4,14 +4,14 @@
"already_configured": "Apparaat is al geconfigureerd"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
"step": {
"user": {
"data": {
- "email": "E-mailadres",
+ "email": "E-mail",
"password": "Wachtwoord"
},
"title": "Maak verbinding met uw Sense Energy Monitor"
diff --git a/homeassistant/components/sense/translations/ru.json b/homeassistant/components/sense/translations/ru.json
index 74be3049a750be..0bb299e22084f3 100644
--- a/homeassistant/components/sense/translations/ru.json
+++ b/homeassistant/components/sense/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
diff --git a/homeassistant/components/sense/translations/tr.json b/homeassistant/components/sense/translations/tr.json
new file mode 100644
index 00000000000000..0e335265325683
--- /dev/null
+++ b/homeassistant/components/sense/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-posta",
+ "password": "Parola"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sense/translations/uk.json b/homeassistant/components/sense/translations/uk.json
new file mode 100644
index 00000000000000..8eac9c9d4abebf
--- /dev/null
+++ b/homeassistant/components/sense/translations/uk.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "title": "Sense Energy Monitor"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sensehat/sensor.py b/homeassistant/components/sensehat/sensor.py
index 67beb021d899bb..6ba00baae773a8 100644
--- a/homeassistant/components/sensehat/sensor.py
+++ b/homeassistant/components/sensehat/sensor.py
@@ -6,7 +6,7 @@
from sense_hat import SenseHat
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_DISPLAY_OPTIONS,
CONF_NAME,
@@ -14,7 +14,6 @@
TEMP_CELSIUS,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -68,7 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(dev, True)
-class SenseHatSensor(Entity):
+class SenseHatSensor(SensorEntity):
"""Representation of a Sense HAT sensor."""
def __init__(self, data, sensor_types):
diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py
index 26276650752f5a..10ceaa39a38b19 100644
--- a/homeassistant/components/sensibo/climate.py
+++ b/homeassistant/components/sensibo/climate.py
@@ -191,7 +191,7 @@ def state(self):
return self._external_state or super().state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {"battery": self.current_battery}
diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py
index 464620197492d8..0012b1a3aa28cd 100644
--- a/homeassistant/components/sensor/__init__.py
+++ b/homeassistant/components/sensor/__init__.py
@@ -7,6 +7,8 @@
from homeassistant.const import (
DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_CO,
+ DEVICE_CLASS_CO2,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_HUMIDITY,
@@ -23,6 +25,7 @@
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
)
+from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
# mypy: allow-untyped-defs, no-check-untyped-defs
@@ -36,6 +39,8 @@
SCAN_INTERVAL = timedelta(seconds=30)
DEVICE_CLASSES = [
DEVICE_CLASS_BATTERY, # % of battery that is left
+ DEVICE_CLASS_CO, # ppm (parts per million) Carbon Monoxide gas concentration
+ DEVICE_CLASS_CO2, # ppm (parts per million) Carbon Dioxide gas concentration
DEVICE_CLASS_CURRENT, # current (A)
DEVICE_CLASS_ENERGY, # energy (kWh, Wh)
DEVICE_CLASS_HUMIDITY, # % of humidity in the air
@@ -70,3 +75,7 @@ async def async_setup_entry(hass, entry):
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)
+
+
+class SensorEntity(Entity):
+ """Base class for sensor entities."""
diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py
index a9d44f2f860f53..4d3d8a4b477348 100644
--- a/homeassistant/components/sensor/device_condition.py
+++ b/homeassistant/components/sensor/device_condition.py
@@ -1,5 +1,5 @@
"""Provides device conditions for sensors."""
-from typing import Dict, List
+from __future__ import annotations
import voluptuous as vol
@@ -14,6 +14,8 @@
CONF_ENTITY_ID,
CONF_TYPE,
DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_CO,
+ DEVICE_CLASS_CO2,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_HUMIDITY,
@@ -23,7 +25,6 @@
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_TEMPERATURE,
- DEVICE_CLASS_TIMESTAMP,
DEVICE_CLASS_VOLTAGE,
)
from homeassistant.core import HomeAssistant, callback
@@ -41,6 +42,8 @@
DEVICE_CLASS_NONE = "none"
CONF_IS_BATTERY_LEVEL = "is_battery_level"
+CONF_IS_CO = "is_carbon_monoxide"
+CONF_IS_CO2 = "is_carbon_dioxide"
CONF_IS_CURRENT = "is_current"
CONF_IS_ENERGY = "is_energy"
CONF_IS_HUMIDITY = "is_humidity"
@@ -50,12 +53,13 @@
CONF_IS_PRESSURE = "is_pressure"
CONF_IS_SIGNAL_STRENGTH = "is_signal_strength"
CONF_IS_TEMPERATURE = "is_temperature"
-CONF_IS_TIMESTAMP = "is_timestamp"
CONF_IS_VOLTAGE = "is_voltage"
CONF_IS_VALUE = "is_value"
ENTITY_CONDITIONS = {
DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_IS_BATTERY_LEVEL}],
+ DEVICE_CLASS_CO: [{CONF_TYPE: CONF_IS_CO}],
+ DEVICE_CLASS_CO2: [{CONF_TYPE: CONF_IS_CO2}],
DEVICE_CLASS_CURRENT: [{CONF_TYPE: CONF_IS_CURRENT}],
DEVICE_CLASS_ENERGY: [{CONF_TYPE: CONF_IS_ENERGY}],
DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_IS_HUMIDITY}],
@@ -65,7 +69,6 @@
DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_IS_PRESSURE}],
DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_IS_SIGNAL_STRENGTH}],
DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_IS_TEMPERATURE}],
- DEVICE_CLASS_TIMESTAMP: [{CONF_TYPE: CONF_IS_TIMESTAMP}],
DEVICE_CLASS_VOLTAGE: [{CONF_TYPE: CONF_IS_VOLTAGE}],
DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_VALUE}],
}
@@ -77,6 +80,8 @@
vol.Required(CONF_TYPE): vol.In(
[
CONF_IS_BATTERY_LEVEL,
+ CONF_IS_CO,
+ CONF_IS_CO2,
CONF_IS_CURRENT,
CONF_IS_ENERGY,
CONF_IS_HUMIDITY,
@@ -86,7 +91,6 @@
CONF_IS_PRESSURE,
CONF_IS_SIGNAL_STRENGTH,
CONF_IS_TEMPERATURE,
- CONF_IS_TIMESTAMP,
CONF_IS_VOLTAGE,
CONF_IS_VALUE,
]
@@ -101,9 +105,9 @@
async def async_get_conditions(
hass: HomeAssistant, device_id: str
-) -> List[Dict[str, str]]:
+) -> list[dict[str, str]]:
"""List device conditions."""
- conditions: List[Dict[str, str]] = []
+ conditions: list[dict[str, str]] = []
entity_registry = await async_get_registry(hass)
entries = [
entry
diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py
index 86dda53cd2bf28..0bca1e299d6336 100644
--- a/homeassistant/components/sensor/device_trigger.py
+++ b/homeassistant/components/sensor/device_trigger.py
@@ -17,6 +17,8 @@
CONF_FOR,
CONF_TYPE,
DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_CO,
+ DEVICE_CLASS_CO2,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_HUMIDITY,
@@ -26,7 +28,6 @@
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_TEMPERATURE,
- DEVICE_CLASS_TIMESTAMP,
DEVICE_CLASS_VOLTAGE,
)
from homeassistant.helpers import config_validation as cv
@@ -39,6 +40,8 @@
DEVICE_CLASS_NONE = "none"
CONF_BATTERY_LEVEL = "battery_level"
+CONF_CO = "carbon_monoxide"
+CONF_CO2 = "carbon_dioxide"
CONF_CURRENT = "current"
CONF_ENERGY = "energy"
CONF_HUMIDITY = "humidity"
@@ -48,12 +51,13 @@
CONF_PRESSURE = "pressure"
CONF_SIGNAL_STRENGTH = "signal_strength"
CONF_TEMPERATURE = "temperature"
-CONF_TIMESTAMP = "timestamp"
CONF_VOLTAGE = "voltage"
CONF_VALUE = "value"
ENTITY_TRIGGERS = {
DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BATTERY_LEVEL}],
+ DEVICE_CLASS_CO: [{CONF_TYPE: CONF_CO}],
+ DEVICE_CLASS_CO2: [{CONF_TYPE: CONF_CO2}],
DEVICE_CLASS_CURRENT: [{CONF_TYPE: CONF_CURRENT}],
DEVICE_CLASS_ENERGY: [{CONF_TYPE: CONF_ENERGY}],
DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_HUMIDITY}],
@@ -63,7 +67,6 @@
DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_PRESSURE}],
DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_SIGNAL_STRENGTH}],
DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_TEMPERATURE}],
- DEVICE_CLASS_TIMESTAMP: [{CONF_TYPE: CONF_TIMESTAMP}],
DEVICE_CLASS_VOLTAGE: [{CONF_TYPE: CONF_VOLTAGE}],
DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_VALUE}],
}
@@ -76,6 +79,8 @@
vol.Required(CONF_TYPE): vol.In(
[
CONF_BATTERY_LEVEL,
+ CONF_CO,
+ CONF_CO2,
CONF_CURRENT,
CONF_ENERGY,
CONF_HUMIDITY,
@@ -85,7 +90,6 @@
CONF_PRESSURE,
CONF_SIGNAL_STRENGTH,
CONF_TEMPERATURE,
- CONF_TIMESTAMP,
CONF_VOLTAGE,
CONF_VALUE,
]
diff --git a/homeassistant/components/sensor/group.py b/homeassistant/components/sensor/group.py
index 4741f8a3b548cd..2ac081496cdd90 100644
--- a/homeassistant/components/sensor/group.py
+++ b/homeassistant/components/sensor/group.py
@@ -2,13 +2,12 @@
from homeassistant.components.group import GroupIntegrationRegistry
-from homeassistant.core import callback
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import HomeAssistant, callback
@callback
def async_describe_on_off_states(
- hass: HomeAssistantType, registry: GroupIntegrationRegistry
+ hass: HomeAssistant, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.exclude_domain()
diff --git a/homeassistant/components/sensor/significant_change.py b/homeassistant/components/sensor/significant_change.py
index 2c281c0a046acd..cda80991242957 100644
--- a/homeassistant/components/sensor/significant_change.py
+++ b/homeassistant/components/sensor/significant_change.py
@@ -1,5 +1,7 @@
"""Helper to test significant sensor state changes."""
-from typing import Any, Optional, Union
+from __future__ import annotations
+
+from typing import Any
from homeassistant.const import (
ATTR_DEVICE_CLASS,
@@ -19,7 +21,7 @@ def async_check_significant_change(
new_state: str,
new_attrs: dict,
**kwargs: Any,
-) -> Optional[bool]:
+) -> bool | None:
"""Test if state significantly changed."""
device_class = new_attrs.get(ATTR_DEVICE_CLASS)
@@ -28,7 +30,7 @@ def async_check_significant_change(
if device_class == DEVICE_CLASS_TEMPERATURE:
if new_attrs.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_FAHRENHEIT:
- change: Union[float, int] = 1
+ change: float | int = 1
else:
change = 0.5
diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json
index 76ea9efabc331a..efe5366cfecf0d 100644
--- a/homeassistant/components/sensor/strings.json
+++ b/homeassistant/components/sensor/strings.json
@@ -3,13 +3,14 @@
"device_automation": {
"condition_type": {
"is_battery_level": "Current {entity_name} battery level",
+ "is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level",
+ "is_carbon_dioxide": "Current {entity_name} carbon dioxide concentration level",
"is_humidity": "Current {entity_name} humidity",
"is_illuminance": "Current {entity_name} illuminance",
"is_power": "Current {entity_name} power",
"is_pressure": "Current {entity_name} pressure",
"is_signal_strength": "Current {entity_name} signal strength",
"is_temperature": "Current {entity_name} temperature",
- "is_timestamp": "Current {entity_name} timestamp",
"is_current": "Current {entity_name} current",
"is_energy": "Current {entity_name} energy",
"is_power_factor": "Current {entity_name} power factor",
@@ -18,13 +19,14 @@
},
"trigger_type": {
"battery_level": "{entity_name} battery level changes",
+ "carbon_monoxide": "{entity_name} carbon monoxide concentration changes",
+ "carbon_dioxide": "{entity_name} carbon dioxide concentration changes",
"humidity": "{entity_name} humidity changes",
"illuminance": "{entity_name} illuminance changes",
"power": "{entity_name} power changes",
"pressure": "{entity_name} pressure changes",
"signal_strength": "{entity_name} signal strength changes",
"temperature": "{entity_name} temperature changes",
- "timestamp": "{entity_name} timestamp changes",
"current": "{entity_name} current changes",
"energy": "{entity_name} energy changes",
"power_factor": "{entity_name} power factor changes",
diff --git a/homeassistant/components/sensor/translations/ca.json b/homeassistant/components/sensor/translations/ca.json
index b351aed3049c03..e0cfb5faa50334 100644
--- a/homeassistant/components/sensor/translations/ca.json
+++ b/homeassistant/components/sensor/translations/ca.json
@@ -2,6 +2,8 @@
"device_automation": {
"condition_type": {
"is_battery_level": "Nivell de bateria actual de {entity_name}",
+ "is_carbon_dioxide": "Concentraci\u00f3 actual de di\u00f2xid de carboni de {entity_name}",
+ "is_carbon_monoxide": "Concentraci\u00f3 actual de mon\u00f2xid de carboni de {entity_name}",
"is_current": "Intensitat actual de {entity_name}",
"is_energy": "Energia actual de {entity_name}",
"is_humidity": "Humitat actual de {entity_name}",
@@ -17,6 +19,8 @@
},
"trigger_type": {
"battery_level": "Canvia el nivell de bateria de {entity_name}",
+ "carbon_dioxide": "Canvia la concentraci\u00f3 de di\u00f2xid de carboni de {entity_name}",
+ "carbon_monoxide": "Canvia la concentraci\u00f3 de mon\u00f2xid de carboni de {entity_name}",
"current": "Canvia la intensitat de {entity_name}",
"energy": "Canvia l'energia de {entity_name}",
"humidity": "Canvia la humitat de {entity_name}",
diff --git a/homeassistant/components/sensor/translations/de.json b/homeassistant/components/sensor/translations/de.json
index 0f72b344982e06..bb7c197f0e82a8 100644
--- a/homeassistant/components/sensor/translations/de.json
+++ b/homeassistant/components/sensor/translations/de.json
@@ -2,6 +2,8 @@
"device_automation": {
"condition_type": {
"is_battery_level": "{entity_name} Batteriestand",
+ "is_carbon_dioxide": "Aktuelle {entity_name} Kohlenstoffdioxid-Konzentration",
+ "is_carbon_monoxide": "Aktuelle {entity_name} Kohlenstoffmonoxid-Konzentration",
"is_humidity": "{entity_name} Feuchtigkeit",
"is_illuminance": "Aktuelle {entity_name} Helligkeit",
"is_power": "Aktuelle {entity_name} Leistung",
@@ -13,6 +15,8 @@
},
"trigger_type": {
"battery_level": "{entity_name} Batteriestatus\u00e4nderungen",
+ "carbon_dioxide": "{entity_name} Kohlenstoffdioxid-Konzentrations\u00e4nderung",
+ "carbon_monoxide": "{entity_name} Kohlenstoffmonoxid-Konzentrations\u00e4nderung",
"humidity": "{entity_name} Feuchtigkeits\u00e4nderungen",
"illuminance": "{entity_name} Helligkeits\u00e4nderungen",
"power": "{entity_name} Leistungs\u00e4nderungen",
diff --git a/homeassistant/components/sensor/translations/en.json b/homeassistant/components/sensor/translations/en.json
index ae0c32df57445c..e32ae845c1c7bd 100644
--- a/homeassistant/components/sensor/translations/en.json
+++ b/homeassistant/components/sensor/translations/en.json
@@ -2,6 +2,8 @@
"device_automation": {
"condition_type": {
"is_battery_level": "Current {entity_name} battery level",
+ "is_carbon_dioxide": "Current {entity_name} carbon dioxide concentration level",
+ "is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level",
"is_current": "Current {entity_name} current",
"is_energy": "Current {entity_name} energy",
"is_humidity": "Current {entity_name} humidity",
@@ -17,6 +19,8 @@
},
"trigger_type": {
"battery_level": "{entity_name} battery level changes",
+ "carbon_dioxide": "{entity_name} carbon dioxide concentration changes",
+ "carbon_monoxide": "{entity_name} carbon monoxide concentration changes",
"current": "{entity_name} current changes",
"energy": "{entity_name} energy changes",
"humidity": "{entity_name} humidity changes",
diff --git a/homeassistant/components/sensor/translations/es.json b/homeassistant/components/sensor/translations/es.json
index b2db3151abf203..b810a3f0eb11cf 100644
--- a/homeassistant/components/sensor/translations/es.json
+++ b/homeassistant/components/sensor/translations/es.json
@@ -2,6 +2,8 @@
"device_automation": {
"condition_type": {
"is_battery_level": "Nivel de bater\u00eda actual de {entity_name}",
+ "is_carbon_dioxide": "Nivel actual de concentraci\u00f3n de di\u00f3xido de carbono {entity_name}",
+ "is_carbon_monoxide": "Nivel actual de concentraci\u00f3n de mon\u00f3xido de carbono {entity_name}",
"is_current": "Corriente actual de {entity_name}",
"is_energy": "Energ\u00eda actual de {entity_name}",
"is_humidity": "Humedad actual de {entity_name}",
@@ -17,6 +19,8 @@
},
"trigger_type": {
"battery_level": "Cambios de nivel de bater\u00eda de {entity_name}",
+ "carbon_dioxide": "{entity_name} cambios en la concentraci\u00f3n de di\u00f3xido de carbono",
+ "carbon_monoxide": "{entity_name} cambios en la concentraci\u00f3n de mon\u00f3xido de carbono",
"current": "Cambio de corriente en {entity_name}",
"energy": "Cambio de energ\u00eda en {entity_name}",
"humidity": "Cambios de humedad de {entity_name}",
diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json
index 450f5b60537c93..55b1fa48a8f0b9 100644
--- a/homeassistant/components/sensor/translations/et.json
+++ b/homeassistant/components/sensor/translations/et.json
@@ -2,6 +2,8 @@
"device_automation": {
"condition_type": {
"is_battery_level": "Praegune {entity_name} aku tase",
+ "is_carbon_dioxide": "{entity_name} praegune s\u00fcsihappegaasi tase",
+ "is_carbon_monoxide": "{entity_name} praegune vingugaasi tase",
"is_current": "Praegune {entity_name} voolutugevus",
"is_energy": "Praegune {entity_name} v\u00f5imsus",
"is_humidity": "Praegune {entity_name} niiskus",
@@ -17,6 +19,8 @@
},
"trigger_type": {
"battery_level": "{entity_name} aku tase muutub",
+ "carbon_dioxide": "{entity_name} s\u00fcsihappegaasi tase muutus",
+ "carbon_monoxide": "{entity_name} vingugaasi tase muutus",
"current": "{entity_name} voolutugevus muutub",
"energy": "{entity_name} v\u00f5imsus muutub",
"humidity": "{entity_name} niiskus muutub",
diff --git a/homeassistant/components/sensor/translations/fr.json b/homeassistant/components/sensor/translations/fr.json
index 4705d28b5c3219..ef88b7aceccb15 100644
--- a/homeassistant/components/sensor/translations/fr.json
+++ b/homeassistant/components/sensor/translations/fr.json
@@ -2,6 +2,8 @@
"device_automation": {
"condition_type": {
"is_battery_level": "Niveau de la batterie de {entity_name}",
+ "is_carbon_dioxide": "Niveau actuel de concentration de dioxyde de carbone {entity_name}",
+ "is_carbon_monoxide": "Niveau actuel de concentration de monoxyde de carbone {entity_name}",
"is_current": "Courant actuel pour {entity_name}",
"is_energy": "\u00c9nergie actuelle pour {entity_name}",
"is_humidity": "Humidit\u00e9 de {entity_name}",
@@ -17,6 +19,8 @@
},
"trigger_type": {
"battery_level": "{entity_name} modification du niveau de batterie",
+ "carbon_dioxide": "{entity_name} changements de concentration de dioxyde de carbone",
+ "carbon_monoxide": "{entity_name} changements de concentration de monoxyde de carbone",
"current": "{entity_name} changement de courant",
"energy": "{entity_name} changement d'\u00e9nergie",
"humidity": "{entity_name} modification de l'humidit\u00e9",
diff --git a/homeassistant/components/sensor/translations/hu.json b/homeassistant/components/sensor/translations/hu.json
index 7be96984451bc6..98e817fc1644d6 100644
--- a/homeassistant/components/sensor/translations/hu.json
+++ b/homeassistant/components/sensor/translations/hu.json
@@ -2,6 +2,8 @@
"device_automation": {
"condition_type": {
"is_battery_level": "{entity_name} aktu\u00e1lis akku szintje",
+ "is_carbon_dioxide": "Jelenlegi {entity_name} sz\u00e9n-dioxid koncentr\u00e1ci\u00f3 szint",
+ "is_carbon_monoxide": "Jelenlegi {entity_name} sz\u00e9n-monoxid koncentr\u00e1ci\u00f3 szint",
"is_humidity": "{entity_name} aktu\u00e1lis p\u00e1ratartalma",
"is_illuminance": "{entity_name} aktu\u00e1lis megvil\u00e1g\u00edt\u00e1sa",
"is_power": "{entity_name} aktu\u00e1lis teljes\u00edtm\u00e9nye",
@@ -13,6 +15,8 @@
},
"trigger_type": {
"battery_level": "{entity_name} akku szintje v\u00e1ltozik",
+ "carbon_dioxide": "{entity_name} sz\u00e9n-dioxid koncentr\u00e1ci\u00f3ja megv\u00e1ltozik",
+ "carbon_monoxide": "{entity_name} sz\u00e9n-monoxid koncentr\u00e1ci\u00f3ja megv\u00e1ltozik",
"humidity": "{entity_name} p\u00e1ratartalma v\u00e1ltozik",
"illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1sa v\u00e1ltozik",
"power": "{entity_name} teljes\u00edtm\u00e9nye v\u00e1ltozik",
@@ -20,7 +24,8 @@
"signal_strength": "{entity_name} jeler\u0151ss\u00e9ge v\u00e1ltozik",
"temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klete v\u00e1ltozik",
"timestamp": "{entity_name} id\u0151b\u00e9lyege v\u00e1ltozik",
- "value": "{entity_name} \u00e9rt\u00e9ke v\u00e1ltozik"
+ "value": "{entity_name} \u00e9rt\u00e9ke v\u00e1ltozik",
+ "voltage": "{entity_name} fesz\u00fclts\u00e9ge v\u00e1ltozik"
}
},
"state": {
diff --git a/homeassistant/components/sensor/translations/id.json b/homeassistant/components/sensor/translations/id.json
index e2d0cdb057dfd5..d43c2304428ebc 100644
--- a/homeassistant/components/sensor/translations/id.json
+++ b/homeassistant/components/sensor/translations/id.json
@@ -1,8 +1,44 @@
{
+ "device_automation": {
+ "condition_type": {
+ "is_battery_level": "Level baterai {entity_name} saat ini",
+ "is_carbon_dioxide": "Level konsentasi karbondioksida {entity_name} saat ini",
+ "is_carbon_monoxide": "Level konsentasi karbonmonoksida {entity_name} saat ini",
+ "is_current": "Arus {entity_name} saat ini",
+ "is_energy": "Energi {entity_name} saat ini",
+ "is_humidity": "Kelembaban {entity_name} saat ini",
+ "is_illuminance": "Pencahayaan {entity_name} saat ini",
+ "is_power": "Daya {entity_name} saat ini",
+ "is_power_factor": "Faktor daya {entity_name} saat ini",
+ "is_pressure": "Tekanan {entity_name} saat ini",
+ "is_signal_strength": "Kekuatan sinyal {entity_name} saat ini",
+ "is_temperature": "Suhu {entity_name} saat ini",
+ "is_timestamp": "Stempel waktu {entity_name} saat ini",
+ "is_value": "Nilai {entity_name} saat ini",
+ "is_voltage": "Tegangan {entity_name} saat ini"
+ },
+ "trigger_type": {
+ "battery_level": "Perubahan level baterai {entity_name}",
+ "carbon_dioxide": "Perubahan konsentrasi karbondioksida {entity_name}",
+ "carbon_monoxide": "Perubahan konsentrasi karbonmonoksida {entity_name}",
+ "current": "Perubahan arus {entity_name}",
+ "energy": "Perubahan energi {entity_name}",
+ "humidity": "Perubahan kelembaban {entity_name}",
+ "illuminance": "Perubahan pencahayaan {entity_name}",
+ "power": "Perubahan daya {entity_name}",
+ "power_factor": "Perubahan faktor daya {entity_name}",
+ "pressure": "Perubahan tekanan {entity_name}",
+ "signal_strength": "Perubahan kekuatan sinyal {entity_name}",
+ "temperature": "Perubahan suhu {entity_name}",
+ "timestamp": "Perubahan stempel waktu {entity_name}",
+ "value": "Perubahan nilai {entity_name}",
+ "voltage": "Perubahan tegangan {entity_name}"
+ }
+ },
"state": {
"_": {
- "off": "Off",
- "on": "On"
+ "off": "Mati",
+ "on": "Nyala"
}
},
"title": "Sensor"
diff --git a/homeassistant/components/sensor/translations/it.json b/homeassistant/components/sensor/translations/it.json
index 84a8b2773a50e5..c8b4a2f9b9cd17 100644
--- a/homeassistant/components/sensor/translations/it.json
+++ b/homeassistant/components/sensor/translations/it.json
@@ -2,6 +2,8 @@
"device_automation": {
"condition_type": {
"is_battery_level": "Livello della batteria attuale di {entity_name}",
+ "is_carbon_dioxide": "Livello di concentrazione di anidride carbonica attuale in {entity_name}",
+ "is_carbon_monoxide": "Livello attuale di concentrazione di monossido di carbonio in {entity_name}",
"is_current": "Corrente attuale di {entity_name}",
"is_energy": "Energia attuale di {entity_name}",
"is_humidity": "Umidit\u00e0 attuale di {entity_name}",
@@ -17,6 +19,8 @@
},
"trigger_type": {
"battery_level": "variazioni del livello di batteria di {entity_name} ",
+ "carbon_dioxide": "Variazioni della concentrazione di anidride carbonica di {entity_name}",
+ "carbon_monoxide": "Variazioni nella concentrazione di monossido di carbonio di {entity_name}",
"current": "variazioni di corrente di {entity_name}",
"energy": "variazioni di energia di {entity_name}",
"humidity": "variazioni di umidit\u00e0 di {entity_name} ",
diff --git a/homeassistant/components/sensor/translations/ko.json b/homeassistant/components/sensor/translations/ko.json
index 92fcd5d37a2673..d8e99874822c50 100644
--- a/homeassistant/components/sensor/translations/ko.json
+++ b/homeassistant/components/sensor/translations/ko.json
@@ -1,26 +1,38 @@
{
"device_automation": {
"condition_type": {
- "is_battery_level": "\ud604\uc7ac {entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 ~ \uc774\uba74",
- "is_humidity": "\ud604\uc7ac {entity_name} \uc2b5\ub3c4\uac00 ~ \uc774\uba74",
- "is_illuminance": "\ud604\uc7ac {entity_name} \uc870\ub3c4\uac00 ~ \uc774\uba74",
- "is_power": "\ud604\uc7ac {entity_name} \uc18c\ube44 \uc804\ub825\uc774 ~ \uc774\uba74",
- "is_pressure": "\ud604\uc7ac {entity_name} \uc555\ub825\uc774 ~ \uc774\uba74",
- "is_signal_strength": "\ud604\uc7ac {entity_name} \uc2e0\ud638 \uac15\ub3c4\uac00 ~ \uc774\uba74",
- "is_temperature": "\ud604\uc7ac {entity_name} \uc628\ub3c4\uac00 ~ \uc774\uba74",
- "is_timestamp": "\ud604\uc7ac {entity_name} \uc2dc\uac01\uc774 ~ \uc774\uba74",
- "is_value": "\ud604\uc7ac {entity_name} \uac12\uc774 ~ \uc774\uba74"
+ "is_battery_level": "\ud604\uc7ac {entity_name}\uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 ~ \uc774\uba74",
+ "is_carbon_dioxide": "\ud604\uc7ac {entity_name}\uc758 \uc774\uc0b0\ud654\ud0c4\uc18c \ub18d\ub3c4 \uc218\uc900\uc774 ~ \uc774\uba74",
+ "is_carbon_monoxide": "\ud604\uc7ac {entity_name}\uc758 \uc77c\uc0b0\ud654\ud0c4\uc18c \ub18d\ub3c4 \uc218\uc900\uc774 ~ \uc774\uba74",
+ "is_current": "\ud604\uc7ac {entity_name}\uc758 \uc804\ub958\uac00 ~ \uc774\uba74",
+ "is_energy": "\ud604\uc7ac {entity_name}\uc758 \uc5d0\ub108\uc9c0\uac00 ~ \uc774\uba74",
+ "is_humidity": "\ud604\uc7ac {entity_name}\uc758 \uc2b5\ub3c4\uac00 ~ \uc774\uba74",
+ "is_illuminance": "\ud604\uc7ac {entity_name}\uc758 \uc870\ub3c4\uac00 ~ \uc774\uba74",
+ "is_power": "\ud604\uc7ac {entity_name}\uc758 \uc18c\ube44 \uc804\ub825\uc774 ~ \uc774\uba74",
+ "is_power_factor": "\ud604\uc7ac {entity_name}\uc758 \uc5ed\ub960\uc774 ~ \uc774\uba74",
+ "is_pressure": "\ud604\uc7ac {entity_name}\uc758 \uc555\ub825\uc774 ~ \uc774\uba74",
+ "is_signal_strength": "\ud604\uc7ac {entity_name}\uc758 \uc2e0\ud638 \uac15\ub3c4\uac00 ~ \uc774\uba74",
+ "is_temperature": "\ud604\uc7ac {entity_name}\uc758 \uc628\ub3c4\uac00 ~ \uc774\uba74",
+ "is_timestamp": "\ud604\uc7ac {entity_name}\uc758 \uc2dc\uac01\uc774 ~ \uc774\uba74",
+ "is_value": "\ud604\uc7ac {entity_name}\uc758 \uac12\uc774 ~ \uc774\uba74",
+ "is_voltage": "\ud604\uc7ac {entity_name}\uc758 \uc804\uc555\uc774 ~ \uc774\uba74"
},
"trigger_type": {
- "battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubc14\ub014 \ub54c",
- "humidity": "{entity_name} \uc2b5\ub3c4\uac00 \ubc14\ub014 \ub54c",
- "illuminance": "{entity_name} \uc870\ub3c4\uac00 \ubc14\ub014 \ub54c",
- "power": "{entity_name} \uc18c\ube44 \uc804\ub825\uc774 \ubc14\ub014 \ub54c",
- "pressure": "{entity_name} \uc555\ub825\uc774 \ubc14\ub014 \ub54c",
- "signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4\uac00 \ubc14\ub014 \ub54c",
- "temperature": "{entity_name} \uc628\ub3c4\uac00 \ubc14\ub014 \ub54c",
- "timestamp": "{entity_name} \uc2dc\uac01\uc774 \ubc14\ub014 \ub54c",
- "value": "{entity_name} \uac12\uc774 \ubc14\ub014 \ub54c"
+ "battery_level": "{entity_name}\uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubcc0\ud560 \ub54c",
+ "carbon_dioxide": "{entity_name}\uc758 \uc774\uc0b0\ud654\ud0c4\uc18c \ub18d\ub3c4\uac00 \ubcc0\ud560 \ub54c",
+ "carbon_monoxide": "{entity_name}\uc758 \uc77c\uc0b0\ud654\ud0c4\uc18c \ub18d\ub3c4\uac00 \ubcc0\ud560 \ub54c",
+ "current": "{entity_name}\uc758 \uc804\ub958\uac00 \ubcc0\ud560 \ub54c",
+ "energy": "{entity_name}\uc758 \uc5d0\ub108\uc9c0\uac00 \ubcc0\ud560 \ub54c",
+ "humidity": "{entity_name}\uc758 \uc2b5\ub3c4\uac00 \ubcc0\ud560 \ub54c",
+ "illuminance": "{entity_name}\uc758 \uc870\ub3c4\uac00 \ubcc0\ud560 \ub54c",
+ "power": "{entity_name}\uc758 \uc18c\ube44 \uc804\ub825\uc774 \ubcc0\ud560 \ub54c",
+ "power_factor": "{entity_name}\uc758 \uc5ed\ub960\uc774 \ubcc0\ud560 \ub54c",
+ "pressure": "{entity_name}\uc758 \uc555\ub825\uc774 \ubcc0\ud560 \ub54c",
+ "signal_strength": "{entity_name}\uc758 \uc2e0\ud638 \uac15\ub3c4\uac00 \ubcc0\ud560 \ub54c",
+ "temperature": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ubcc0\ud560 \ub54c",
+ "timestamp": "{entity_name}\uc758 \uc2dc\uac01\uc774 \ubcc0\ud560 \ub54c",
+ "value": "{entity_name}\uc758 \uac12\uc774 \ubcc0\ud560 \ub54c",
+ "voltage": "{entity_name}\uc758 \uc804\uc555\uc774 \ubcc0\ud560 \ub54c"
}
},
"state": {
diff --git a/homeassistant/components/sensor/translations/nl.json b/homeassistant/components/sensor/translations/nl.json
index 869599296d583f..411ebf3cefd537 100644
--- a/homeassistant/components/sensor/translations/nl.json
+++ b/homeassistant/components/sensor/translations/nl.json
@@ -2,6 +2,8 @@
"device_automation": {
"condition_type": {
"is_battery_level": "Huidige batterijniveau {entity_name}",
+ "is_carbon_dioxide": "Huidig niveau {entity_name} kooldioxideconcentratie",
+ "is_carbon_monoxide": "Huidig niveau {entity_name} koolmonoxideconcentratie",
"is_current": "Huidige {entity_name} stroom",
"is_energy": "Huidige {entity_name} energie",
"is_humidity": "Huidige {entity_name} vochtigheidsgraad",
@@ -17,16 +19,20 @@
},
"trigger_type": {
"battery_level": "{entity_name} batterijniveau gewijzigd",
+ "carbon_dioxide": "{entity_name} kooldioxideconcentratie gewijzigd",
+ "carbon_monoxide": "{entity_name} koolmonoxideconcentratie gewijzigd",
"current": "{entity_name} huidige wijzigingen",
"energy": "{entity_name} energieveranderingen",
"humidity": "{entity_name} vochtigheidsgraad gewijzigd",
"illuminance": "{entity_name} verlichtingssterkte gewijzigd",
"power": "{entity_name} vermogen gewijzigd",
+ "power_factor": "{entity_name} power factor verandert",
"pressure": "{entity_name} druk gewijzigd",
"signal_strength": "{entity_name} signaalsterkte gewijzigd",
"temperature": "{entity_name} temperatuur gewijzigd",
"timestamp": "{entity_name} tijdstip gewijzigd",
- "value": "{entity_name} waarde gewijzigd"
+ "value": "{entity_name} waarde gewijzigd",
+ "voltage": "{entity_name} voltage verandert"
}
},
"state": {
diff --git a/homeassistant/components/sensor/translations/no.json b/homeassistant/components/sensor/translations/no.json
index b3e8dc199f6ee4..3662356d15ecfe 100644
--- a/homeassistant/components/sensor/translations/no.json
+++ b/homeassistant/components/sensor/translations/no.json
@@ -2,6 +2,8 @@
"device_automation": {
"condition_type": {
"is_battery_level": "Gjeldende {entity_name} batteriniv\u00e5",
+ "is_carbon_dioxide": "Gjeldende {entity_name} karbondioksidkonsentrasjonsniv\u00e5",
+ "is_carbon_monoxide": "Gjeldende {entity_name} karbonmonoksid konsentrasjonsniv\u00e5",
"is_current": "Gjeldende {entity_name} str\u00f8m",
"is_energy": "Gjeldende {entity_name} effekt",
"is_humidity": "Gjeldende {entity_name} fuktighet",
@@ -17,6 +19,8 @@
},
"trigger_type": {
"battery_level": "{entity_name} batteriniv\u00e5 endres",
+ "carbon_dioxide": "{entity_name} endringer i konsentrasjonen av karbondioksid",
+ "carbon_monoxide": "{entity_name} endringer i konsentrasjonen av karbonmonoksid",
"current": "{entity_name} gjeldende endringer",
"energy": "{entity_name} effektendringer",
"humidity": "{entity_name} fuktighets endringer",
diff --git a/homeassistant/components/sensor/translations/pl.json b/homeassistant/components/sensor/translations/pl.json
index a8fb604cc530cd..a2baa380174467 100644
--- a/homeassistant/components/sensor/translations/pl.json
+++ b/homeassistant/components/sensor/translations/pl.json
@@ -2,6 +2,8 @@
"device_automation": {
"condition_type": {
"is_battery_level": "obecny poziom na\u0142adowania baterii {entity_name}",
+ "is_carbon_dioxide": "Bie\u017c\u0105cy poziom st\u0119\u017cenia dwutlenku w\u0119gla w {entity_name}",
+ "is_carbon_monoxide": "Bie\u017c\u0105cy poziom st\u0119\u017cenia tlenku w\u0119gla w {entity_name}",
"is_current": "obecne nat\u0119\u017cenie pr\u0105du {entity_name}",
"is_energy": "obecna energia {entity_name}",
"is_humidity": "obecna wilgotno\u015b\u0107 {entity_name}",
@@ -17,6 +19,8 @@
},
"trigger_type": {
"battery_level": "zmieni si\u0119 poziom baterii {entity_name}",
+ "carbon_dioxide": "Zmiana st\u0119\u017cenie dwutlenku w\u0119gla w {entity_name}",
+ "carbon_monoxide": "Zmiana st\u0119\u017cenia tlenku w\u0119gla w {entity_name}",
"current": "zmieni si\u0119 nat\u0119\u017cenie pr\u0105du w {entity_name}",
"energy": "zmieni si\u0119 energia {entity_name}",
"humidity": "zmieni si\u0119 wilgotno\u015b\u0107 {entity_name}",
diff --git a/homeassistant/components/sensor/translations/ru.json b/homeassistant/components/sensor/translations/ru.json
index ae84c843bc3f22..ae0a0997dd6721 100644
--- a/homeassistant/components/sensor/translations/ru.json
+++ b/homeassistant/components/sensor/translations/ru.json
@@ -2,6 +2,8 @@
"device_automation": {
"condition_type": {
"is_battery_level": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435",
+ "is_carbon_dioxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0443\u0433\u043b\u0435\u043a\u0438\u0441\u043b\u043e\u0433\u043e \u0433\u0430\u0437\u0430",
+ "is_carbon_monoxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0443\u0433\u0430\u0440\u043d\u043e\u0433\u043e \u0433\u0430\u0437\u0430",
"is_current": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0438\u043b\u044b \u0442\u043e\u043a\u0430",
"is_energy": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438",
"is_humidity": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435",
@@ -17,6 +19,8 @@
},
"trigger_type": {
"battery_level": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435",
+ "carbon_dioxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435",
+ "carbon_monoxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435",
"current": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0438\u043b\u044b \u0442\u043e\u043a\u0430",
"energy": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438",
"humidity": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435",
diff --git a/homeassistant/components/sensor/translations/tr.json b/homeassistant/components/sensor/translations/tr.json
index 3bf1ba6f3682f0..feca40991eee3a 100644
--- a/homeassistant/components/sensor/translations/tr.json
+++ b/homeassistant/components/sensor/translations/tr.json
@@ -1,4 +1,31 @@
{
+ "device_automation": {
+ "condition_type": {
+ "is_current": "Mevcut {entity_name} ak\u0131m\u0131",
+ "is_energy": "Mevcut {entity_name} enerjisi",
+ "is_power_factor": "Mevcut {entity_name} g\u00fc\u00e7 fakt\u00f6r\u00fc",
+ "is_signal_strength": "Mevcut {entity_name} sinyal g\u00fcc\u00fc",
+ "is_temperature": "Mevcut {entity_name} s\u0131cakl\u0131\u011f\u0131",
+ "is_timestamp": "Mevcut {entity_name} zaman damgas\u0131",
+ "is_value": "Mevcut {entity_name} de\u011feri",
+ "is_voltage": "Mevcut {entity_name} voltaj\u0131"
+ },
+ "trigger_type": {
+ "battery_level": "{entity_name} pil seviyesi de\u011fi\u015fiklikleri",
+ "current": "{entity_name} ak\u0131m de\u011fi\u015fiklikleri",
+ "energy": "{entity_name} enerji de\u011fi\u015fiklikleri",
+ "humidity": "{entity_name} nem de\u011fi\u015fiklikleri",
+ "illuminance": "{entity_name} ayd\u0131nlatma de\u011fi\u015fiklikleri",
+ "power": "{entity_name} g\u00fc\u00e7 de\u011fi\u015fiklikleri",
+ "power_factor": "{entity_name} g\u00fc\u00e7 fakt\u00f6r\u00fc de\u011fi\u015fiklikleri",
+ "pressure": "{entity_name} bas\u0131n\u00e7 de\u011fi\u015fiklikleri",
+ "signal_strength": "{entity_name} sinyal g\u00fcc\u00fc de\u011fi\u015fiklikleri",
+ "temperature": "{entity_name} s\u0131cakl\u0131k de\u011fi\u015fiklikleri",
+ "timestamp": "{entity_name} zaman damgas\u0131 de\u011fi\u015fiklikleri",
+ "value": "{entity_name} de\u011fer de\u011fi\u015fiklikleri",
+ "voltage": "{entity_name} voltaj de\u011fi\u015fiklikleri"
+ }
+ },
"state": {
"_": {
"off": "Kapal\u0131",
diff --git a/homeassistant/components/sensor/translations/uk.json b/homeassistant/components/sensor/translations/uk.json
index 391415409f5bd1..9e6148c3b8c636 100644
--- a/homeassistant/components/sensor/translations/uk.json
+++ b/homeassistant/components/sensor/translations/uk.json
@@ -1,7 +1,34 @@
{
"device_automation": {
"condition_type": {
- "is_battery_level": "\u041f\u043e\u0442\u043e\u0447\u043d\u0438\u0439 \u0440\u0456\u0432\u0435\u043d\u044c \u0437\u0430\u0440\u044f\u0434\u0443 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430 {entity_name}"
+ "is_battery_level": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "is_current": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0441\u0438\u043b\u0438 \u0441\u0442\u0440\u0443\u043c\u0443",
+ "is_energy": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u043e\u0442\u0443\u0436\u043d\u043e\u0441\u0442\u0456",
+ "is_humidity": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "is_illuminance": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "is_power": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "is_power_factor": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043a\u043e\u0435\u0444\u0456\u0446\u0456\u0454\u043d\u0442\u0430 \u043f\u043e\u0442\u0443\u0436\u043d\u043e\u0441\u0442\u0456",
+ "is_pressure": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "is_signal_strength": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "is_temperature": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "is_timestamp": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "is_value": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "is_voltage": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043d\u0430\u043f\u0440\u0443\u0433\u0438"
+ },
+ "trigger_type": {
+ "battery_level": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "current": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0441\u0438\u043b\u0438 \u0441\u0442\u0440\u0443\u043c\u0443",
+ "energy": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u043e\u0442\u0443\u0436\u043d\u043e\u0441\u0442\u0456",
+ "humidity": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "illuminance": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "power": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "power_factor": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u043a\u043e\u0435\u0444\u0456\u0446\u0456\u0454\u043d\u0442 \u043f\u043e\u0442\u0443\u0436\u043d\u043e\u0441\u0442\u0456",
+ "pressure": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "signal_strength": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "temperature": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "timestamp": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "value": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f",
+ "voltage": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043d\u0430\u043f\u0440\u0443\u0433\u0438"
}
},
"state": {
@@ -10,5 +37,5 @@
"on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e"
}
},
- "title": "\u0414\u0430\u0442\u0447\u0438\u043a"
+ "title": "\u0421\u0435\u043d\u0441\u043e\u0440"
}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/translations/zh-Hant.json b/homeassistant/components/sensor/translations/zh-Hant.json
index 56350e501ef585..ff84d8b2790a40 100644
--- a/homeassistant/components/sensor/translations/zh-Hant.json
+++ b/homeassistant/components/sensor/translations/zh-Hant.json
@@ -2,6 +2,8 @@
"device_automation": {
"condition_type": {
"is_battery_level": "\u76ee\u524d{entity_name}\u96fb\u91cf",
+ "is_carbon_dioxide": "\u76ee\u524d {entity_name} \u4e8c\u6c27\u5316\u78b3\u6fc3\u5ea6\u72c0\u614b",
+ "is_carbon_monoxide": "\u76ee\u524d {entity_name} \u4e00\u6c27\u5316\u78b3\u6fc3\u5ea6\u72c0\u614b",
"is_current": "\u76ee\u524d{entity_name}\u96fb\u6d41",
"is_energy": "\u76ee\u524d{entity_name}\u96fb\u529b",
"is_humidity": "\u76ee\u524d{entity_name}\u6fd5\u5ea6",
@@ -17,6 +19,8 @@
},
"trigger_type": {
"battery_level": "{entity_name}\u96fb\u91cf\u8b8a\u66f4",
+ "carbon_dioxide": "{entity_name} \u4e8c\u6c27\u5316\u78b3\u6fc3\u5ea6\u8b8a\u5316",
+ "carbon_monoxide": "{entity_name} \u4e00\u6c27\u5316\u78b3\u6fc3\u5ea6\u8b8a\u5316",
"current": "\u76ee\u524d{entity_name}\u96fb\u6d41\u8b8a\u66f4",
"energy": "\u76ee\u524d{entity_name}\u96fb\u529b\u8b8a\u66f4",
"humidity": "{entity_name}\u6fd5\u5ea6\u8b8a\u66f4",
diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py
index 6be02b9ba5edba..c58d7bcd1a8e29 100644
--- a/homeassistant/components/sentry/__init__.py
+++ b/homeassistant/components/sentry/__init__.py
@@ -1,6 +1,7 @@
"""The sentry integration."""
+from __future__ import annotations
+
import re
-from typing import Dict, Union
import sentry_sdk
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
@@ -126,8 +127,8 @@ def process_before_send(
options,
channel: str,
huuid: str,
- system_info: Dict[str, Union[bool, str]],
- custom_components: Dict[str, Integration],
+ system_info: dict[str, bool | str],
+ custom_components: dict[str, Integration],
event,
hint,
):
diff --git a/homeassistant/components/sentry/config_flow.py b/homeassistant/components/sentry/config_flow.py
index a308423f40ba46..b294fa46236660 100644
--- a/homeassistant/components/sentry/config_flow.py
+++ b/homeassistant/components/sentry/config_flow.py
@@ -2,7 +2,7 @@
from __future__ import annotations
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from sentry_sdk.utils import BadDsn, Dsn
import voluptuous as vol
@@ -10,7 +10,7 @@
from homeassistant import config_entries
from homeassistant.core import callback
-from .const import ( # pylint: disable=unused-import
+from .const import (
CONF_DSN,
CONF_ENVIRONMENT,
CONF_EVENT_CUSTOM_COMPONENTS,
@@ -47,8 +47,8 @@ def async_get_options_flow(
return SentryOptionsFlow(config_entry)
async def async_step_user(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle a user config flow."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
@@ -78,8 +78,8 @@ def __init__(self, config_entry: config_entries.ConfigEntry):
self.config_entry = config_entry
async def async_step_init(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Manage Sentry options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json
index da5294b9258ae9..04735d98687493 100644
--- a/homeassistant/components/sentry/manifest.json
+++ b/homeassistant/components/sentry/manifest.json
@@ -3,6 +3,6 @@
"name": "Sentry",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sentry",
- "requirements": ["sentry-sdk==0.19.5"],
+ "requirements": ["sentry-sdk==1.0.0"],
"codeowners": ["@dcramer", "@frenck"]
}
diff --git a/homeassistant/components/sentry/translations/de.json b/homeassistant/components/sentry/translations/de.json
index c36bbf258b08f7..8fbcfc1eaa2cc7 100644
--- a/homeassistant/components/sentry/translations/de.json
+++ b/homeassistant/components/sentry/translations/de.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
"error": {
"bad_dsn": "Ung\u00fcltiger DSN",
"unknown": "Unerwarteter Fehler"
diff --git a/homeassistant/components/sentry/translations/hu.json b/homeassistant/components/sentry/translations/hu.json
index 64ee672a02f91b..055c881717724e 100644
--- a/homeassistant/components/sentry/translations/hu.json
+++ b/homeassistant/components/sentry/translations/hu.json
@@ -1,8 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
"error": {
"bad_dsn": "\u00c9rv\u00e9nytelen DSN",
- "unknown": "V\u00e1ratlan hiba"
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"step": {
"user": {
diff --git a/homeassistant/components/sentry/translations/id.json b/homeassistant/components/sentry/translations/id.json
new file mode 100644
index 00000000000000..fbd94fa656505e
--- /dev/null
+++ b/homeassistant/components/sentry/translations/id.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "bad_dsn": "DSN tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "dsn": "DSN"
+ },
+ "description": "Masukkan DSN Sentry Anda",
+ "title": "Sentry"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "environment": "Nama opsional untuk lingkungan.",
+ "event_custom_components": "Kirim event dari komponen khusus",
+ "event_handled": "Kirim event yang ditangani",
+ "event_third_party_packages": "Kirim event dari paket pihak ketiga",
+ "tracing": "Aktifkan pelacakan kinerja",
+ "tracing_sample_rate": "Laju sampel pelacakan; antara 0,0 dan 1,0 (1,0 = 100%)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sentry/translations/ko.json b/homeassistant/components/sentry/translations/ko.json
index 963695dabfd782..92e26b30c42e93 100644
--- a/homeassistant/components/sentry/translations/ko.json
+++ b/homeassistant/components/sentry/translations/ko.json
@@ -1,11 +1,17 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
"error": {
"bad_dsn": "DSN \uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
+ "data": {
+ "dsn": "DSN"
+ },
"description": "Sentry DSN \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694",
"title": "Sentry"
}
@@ -15,14 +21,14 @@
"step": {
"init": {
"data": {
- "environment": "\ud658\uacbd\uc758 \uc120\ud0dd\uc801 \uba85\uce6d",
+ "environment": "\ud658\uacbd\uc5d0 \ub300\ud55c \uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)",
"event_custom_components": "\uc0ac\uc6a9\uc790 \uc9c0\uc815 \uad6c\uc131 \uc694\uc18c\uc5d0\uc11c \uc774\ubca4\ud2b8 \ubcf4\ub0b4\uae30",
"event_handled": "\ucc98\ub9ac\ub41c \uc774\ubca4\ud2b8 \ubcf4\ub0b4\uae30",
- "event_third_party_packages": "\uc368\ub4dc\ud30c\ud2f0 \ud328\ud0a4\uc9c0\uc5d0\uc11c \uc774\ubca4\ud2b8 \ubcf4\ub0b4\uae30",
- "logging_event_level": "Log level Sentry\ub294 \ub2e4\uc74c\uc5d0 \ub300\ud55c \uc774\ubca4\ud2b8\ub97c \ub4f1\ub85d\ud569\ub2c8\ub2e4.",
- "logging_level": "Log level sentry\ub294 \ub2e4\uc74c\uc5d0 \ub300\ud55c \ub85c\uadf8\ub97c \ube0c\ub808\ub4dc \ud06c\ub7fc\uc73c\ub85c \uae30\ub85d\ud569\ub2c8\ub2e4.",
- "tracing": "\uc131\ub2a5 \ucd94\uc801 \ud65c\uc131\ud654",
- "tracing_sample_rate": "\uc0d8\ud50c\ub9c1 \uc18d\ub3c4 \ucd94\uc801; 0.0\uc5d0\uc11c 1.0 \uc0ac\uc774 (1.0 = 100 %)"
+ "event_third_party_packages": "\uc11c\ub4dc \ud30c\ud2f0 \ud328\ud0a4\uc9c0\uc5d0\uc11c \uc774\ubca4\ud2b8 \ubcf4\ub0b4\uae30",
+ "logging_event_level": "\ub85c\uadf8 \ub808\ubca8 Sentry\ub294 \ub2e4\uc74c \ub85c\uadf8 \uc218\uc900\uc5d0 \ub300\ud55c \uc774\ubca4\ud2b8\ub97c \ub4f1\ub85d\ud569\ub2c8\ub2e4",
+ "logging_level": "\ub85c\uadf8 \ub808\ubca8 Sentry\ub294 \ub2e4\uc74c \ub85c\uadf8 \uc218\uc900\uc5d0 \ub300\ud55c \ub85c\uadf8\ub97c \ube0c\ub808\ub4dc\ud06c\ub7fc\uc73c\ub85c \uae30\ub85d\ud569\ub2c8\ub2e4",
+ "tracing": "\uc131\ub2a5 \ucd94\uc801 \ud65c\uc131\ud654\ud558\uae30",
+ "tracing_sample_rate": "\ucd94\uc801 \uc0d8\ud50c \uc18d\ub3c4; 0.0\uc5d0\uc11c 1.0 \uc0ac\uc774 (1.0 = 100%)"
}
}
}
diff --git a/homeassistant/components/sentry/translations/nl.json b/homeassistant/components/sentry/translations/nl.json
index 37437dfe8368fa..53f54ac1968409 100644
--- a/homeassistant/components/sentry/translations/nl.json
+++ b/homeassistant/components/sentry/translations/nl.json
@@ -9,9 +9,28 @@
},
"step": {
"user": {
+ "data": {
+ "dsn": "DSN"
+ },
"description": "Voer uw Sentry DSN in",
"title": "Sentry"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "environment": "Optionele naam van de omgeving.",
+ "event_custom_components": "Gebeurtenissen verzenden vanuit aangepaste onderdelen",
+ "event_handled": "Stuur afgehandelde gebeurtenissen",
+ "event_third_party_packages": "Gebeurtenissen verzenden vanuit pakketten van derden",
+ "logging_event_level": "Het logniveau waarvoor Sentry een gebeurtenis registreert",
+ "logging_level": "Het logniveau Sentry zal logs opnemen als broodkruimels voor",
+ "tracing": "Schakel prestatietracering in",
+ "tracing_sample_rate": "Tracering van de steekproefsnelheid; tussen 0,0 en 1,0 (1,0 = 100%)"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/sentry/translations/tr.json b/homeassistant/components/sentry/translations/tr.json
new file mode 100644
index 00000000000000..4dab23fbd949f9
--- /dev/null
+++ b/homeassistant/components/sentry/translations/tr.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "error": {
+ "bad_dsn": "Ge\u00e7ersiz DSN",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "dsn": "DSN"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "environment": "Ortam\u0131n iste\u011fe ba\u011fl\u0131 ad\u0131."
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sentry/translations/uk.json b/homeassistant/components/sentry/translations/uk.json
new file mode 100644
index 00000000000000..01da0308851d37
--- /dev/null
+++ b/homeassistant/components/sentry/translations/uk.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "bad_dsn": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 DSN.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "dsn": "DSN"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448 DSN Sentry",
+ "title": "Sentry"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "environment": "\u041d\u0430\u0437\u0432\u0430",
+ "event_custom_components": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u0438 \u043f\u043e\u0434\u0456\u0457 \u0437 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0446\u044c\u043a\u0438\u0445 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0456\u0432",
+ "event_handled": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u0438 \u043e\u0431\u0440\u043e\u0431\u043b\u0435\u043d\u0456 \u043f\u043e\u0434\u0456\u0457",
+ "event_third_party_packages": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u0438 \u043f\u043e\u0434\u0456\u0457 \u0437 \u0441\u0442\u043e\u0440\u043e\u043d\u043d\u0456\u0445 \u043f\u0430\u043a\u0435\u0442\u0456\u0432",
+ "logging_event_level": "\u0417\u0430\u043f\u0438\u0441\u0443\u0432\u0430\u0442\u0438 \u0436\u0443\u0440\u043d\u0430\u043b\u0438 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u0438\u0445 \u043f\u043e\u0434\u0456\u0439",
+ "logging_level": "\u0417\u0430\u043f\u0438\u0441\u0443\u0432\u0430\u0442\u0438 \u0436\u0443\u0440\u043d\u0430\u043b\u0438 \u0443 \u0432\u0438\u0433\u043b\u044f\u0434\u0456 \u043d\u0430\u0432\u0456\u0433\u0430\u0446\u0456\u0439\u043d\u0438\u0445 \u043b\u0430\u043d\u0446\u044e\u0436\u043a\u0456\u0432",
+ "tracing": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0456",
+ "tracing_sample_rate": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u0434\u0438\u0441\u043a\u0440\u0435\u0442\u0438\u0437\u0430\u0446\u0456\u0457 \u0442\u0440\u0430\u0441\u0443\u0432\u0430\u043d\u043d\u044f; \u0432\u0456\u0434 0,0 \u0434\u043e 1,0 (1,0 = 100%)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sentry/translations/zh-Hant.json b/homeassistant/components/sentry/translations/zh-Hant.json
index b73a2e57f1a3bc..aae10144a661f3 100644
--- a/homeassistant/components/sentry/translations/zh-Hant.json
+++ b/homeassistant/components/sentry/translations/zh-Hant.json
@@ -26,7 +26,7 @@
"event_handled": "\u50b3\u9001\u5df2\u8655\u7406\u4e8b\u4ef6",
"event_third_party_packages": "\u50b3\u9001\u7b2c\u4e09\u65b9\u5c01\u5305\u4e8b\u4ef6",
"logging_event_level": "\u65e5\u8a8c\u7b49\u7d1a\u76e3\u63a7\u5c07\u6703\u8a3b\u518a\u4e8b\u4ef6\u70ba",
- "logging_level": "\u65e5\u8a8c\u7b49\u7d1a\u76e3\u63a7\u5c07\u6703\u7d00\u9304\u4e8b\u4ef6\u70ba\u6a94\u6848\u5c0e\u822a\u70ba",
+ "logging_level": "\u65e5\u8a8c\u7b49\u7d1a\u76e3\u63a7\u5c07\u6703\u65e5\u8a8c\u4e8b\u4ef6\u70ba\u6a94\u6848\u5c0e\u822a\u70ba",
"tracing": "\u958b\u555f\u6548\u80fd\u8ffd\u8e64",
"tracing_sample_rate": "\u8ffd\u8e64\u63a1\u6a23\u7bc4\u570d\uff0c\u4ecb\u65bc 0.0 \u53ca 1.0 \u4e4b\u9593\uff081.0 = 100%\uff09"
}
diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py
index e0bf23a251405e..1e73ae9ac8303d 100644
--- a/homeassistant/components/serial/sensor.py
+++ b/homeassistant/components/serial/sensor.py
@@ -7,11 +7,10 @@
import serial_asyncio
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -103,7 +102,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([sensor], True)
-class SerialSensor(Entity):
+class SerialSensor(SensorEntity):
"""Representation of a Serial sensor."""
def __init__(
@@ -241,7 +240,7 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the attributes of the entity (if any JSON present)."""
return self._attributes
diff --git a/homeassistant/components/serial_pm/sensor.py b/homeassistant/components/serial_pm/sensor.py
index 2e7604ee97db1f..b81c60e0a19d29 100644
--- a/homeassistant/components/serial_pm/sensor.py
+++ b/homeassistant/components/serial_pm/sensor.py
@@ -4,10 +4,9 @@
from pmsensor import serial_pm as pm
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -56,7 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(dev)
-class ParticulateMatterSensor(Entity):
+class ParticulateMatterSensor(SensorEntity):
"""Representation of an Particulate matter sensor."""
def __init__(self, pmDataCollector, name, pmname):
diff --git a/homeassistant/components/sesame/lock.py b/homeassistant/components/sesame/lock.py
index 0ad3d87a1715f0..acd71b7c9e74af 100644
--- a/homeassistant/components/sesame/lock.py
+++ b/homeassistant/components/sesame/lock.py
@@ -7,6 +7,7 @@
from homeassistant.components.lock import PLATFORM_SCHEMA, LockEntity
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
+ ATTR_DEVICE_ID,
CONF_API_KEY,
STATE_LOCKED,
STATE_UNLOCKED,
@@ -14,7 +15,6 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
-ATTR_DEVICE_ID = "device_id"
ATTR_SERIAL_NO = "serial"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string})
@@ -86,7 +86,7 @@ def update(self) -> None:
self._responsive = status["responsive"]
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return the state attributes."""
return {
ATTR_DEVICE_ID: self._device_id,
diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json
index 01e0275feeb31d..13f3cf22506b94 100644
--- a/homeassistant/components/seven_segments/manifest.json
+++ b/homeassistant/components/seven_segments/manifest.json
@@ -2,6 +2,6 @@
"domain": "seven_segments",
"name": "Seven Segments OCR",
"documentation": "https://www.home-assistant.io/integrations/seven_segments",
- "requirements": ["pillow==8.1.0"],
+ "requirements": ["pillow==8.1.2"],
"codeowners": ["@fabaff"]
}
diff --git a/homeassistant/components/seventeentrack/manifest.json b/homeassistant/components/seventeentrack/manifest.json
index 2cec9dea954add..427882de91a557 100644
--- a/homeassistant/components/seventeentrack/manifest.json
+++ b/homeassistant/components/seventeentrack/manifest.json
@@ -2,6 +2,6 @@
"domain": "seventeentrack",
"name": "17TRACK",
"documentation": "https://www.home-assistant.io/integrations/seventeentrack",
- "requirements": ["py17track==2.2.2"],
+ "requirements": ["py17track==3.2.1"],
"codeowners": ["@bachya"]
}
diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py
index 94efe9b98c7ddf..e856f71b008db7 100644
--- a/homeassistant/components/seventeentrack/sensor.py
+++ b/homeassistant/components/seventeentrack/sensor.py
@@ -6,24 +6,24 @@
from py17track.errors import SeventeenTrackError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
+ ATTR_FRIENDLY_NAME,
ATTR_LOCATION,
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
CONF_USERNAME,
)
from homeassistant.helpers import aiohttp_client, config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_call_later
from homeassistant.util import Throttle, slugify
_LOGGER = logging.getLogger(__name__)
ATTR_DESTINATION_COUNTRY = "destination_country"
-ATTR_FRIENDLY_NAME = "friendly_name"
ATTR_INFO_TEXT = "info_text"
+ATTR_TIMESTAMP = "timestamp"
ATTR_ORIGIN_COUNTRY = "origin_country"
ATTR_PACKAGES = "packages"
ATTR_PACKAGE_TYPE = "package_type"
@@ -65,9 +65,9 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Configure the platform and add the sensors."""
- websession = aiohttp_client.async_get_clientsession(hass)
+ session = aiohttp_client.async_get_clientsession(hass)
- client = SeventeenTrackClient(websession)
+ client = SeventeenTrackClient(session=session)
try:
login_result = await client.profile.login(
@@ -89,11 +89,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
scan_interval,
config[CONF_SHOW_ARCHIVED],
config[CONF_SHOW_DELIVERED],
+ str(hass.config.time_zone),
)
await data.async_update()
-class SeventeenTrackSummarySensor(Entity):
+class SeventeenTrackSummarySensor(SensorEntity):
"""Define a summary sensor."""
def __init__(self, data, status, initial_state):
@@ -109,7 +110,7 @@ def available(self):
return self._state is not None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
return self._attrs
@@ -151,6 +152,7 @@ async def async_update(self):
{
ATTR_FRIENDLY_NAME: package.friendly_name,
ATTR_INFO_TEXT: package.info_text,
+ ATTR_TIMESTAMP: package.timestamp,
ATTR_STATUS: package.status,
ATTR_LOCATION: package.location,
ATTR_TRACKING_NUMBER: package.tracking_number,
@@ -163,7 +165,7 @@ async def async_update(self):
self._state = self._data.summary.get(self._status)
-class SeventeenTrackPackageSensor(Entity):
+class SeventeenTrackPackageSensor(SensorEntity):
"""Define an individual package sensor."""
def __init__(self, data, package):
@@ -172,6 +174,7 @@ def __init__(self, data, package):
ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION,
ATTR_DESTINATION_COUNTRY: package.destination_country,
ATTR_INFO_TEXT: package.info_text,
+ ATTR_TIMESTAMP: package.timestamp,
ATTR_LOCATION: package.location,
ATTR_ORIGIN_COUNTRY: package.origin_country,
ATTR_PACKAGE_TYPE: package.package_type,
@@ -190,7 +193,7 @@ def available(self):
return self._data.packages.get(self._tracking_number) is not None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
return self._attrs
@@ -237,14 +240,18 @@ async def async_update(self):
return
self._attrs.update(
- {ATTR_INFO_TEXT: package.info_text, ATTR_LOCATION: package.location}
+ {
+ ATTR_INFO_TEXT: package.info_text,
+ ATTR_TIMESTAMP: package.timestamp,
+ ATTR_LOCATION: package.location,
+ }
)
self._state = package.status
self._friendly_name = package.friendly_name
async def _remove(self, *_):
"""Remove entity itself."""
- await self.async_remove()
+ await self.async_remove(force_remove=True)
reg = await self.hass.helpers.entity_registry.async_get_registry()
entity_id = reg.async_get_entity_id(
@@ -277,7 +284,13 @@ class SeventeenTrackData:
"""Define a data handler for 17track.net."""
def __init__(
- self, client, async_add_entities, scan_interval, show_archived, show_delivered
+ self,
+ client,
+ async_add_entities,
+ scan_interval,
+ show_archived,
+ show_delivered,
+ timezone,
):
"""Initialize."""
self._async_add_entities = async_add_entities
@@ -287,6 +300,7 @@ def __init__(
self.account_id = client.profile.account_id
self.packages = {}
self.show_delivered = show_delivered
+ self.timezone = timezone
self.summary = {}
self.async_update = Throttle(self._scan_interval)(self._async_update)
@@ -297,7 +311,7 @@ async def _async_update(self):
try:
packages = await self._client.profile.packages(
- show_archived=self._show_archived
+ show_archived=self._show_archived, tz=self.timezone
)
_LOGGER.debug("New package data received: %s", packages)
diff --git a/homeassistant/components/sharkiq/__init__.py b/homeassistant/components/sharkiq/__init__.py
index b7684f988553da..94b6a8f2e3bfd0 100644
--- a/homeassistant/components/sharkiq/__init__.py
+++ b/homeassistant/components/sharkiq/__init__.py
@@ -1,6 +1,7 @@
"""Shark IQ Integration."""
import asyncio
+from contextlib import suppress
import async_timeout
from sharkiqpy import (
@@ -14,7 +15,7 @@
from homeassistant import exceptions
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from .const import _LOGGER, API_TIMEOUT, COMPONENTS, DOMAIN
+from .const import _LOGGER, API_TIMEOUT, DOMAIN, PLATFORMS
from .update_coordinator import SharkIqUpdateCoordinator
@@ -59,20 +60,17 @@ async def async_setup_entry(hass, config_entry):
raise exceptions.ConfigEntryNotReady from exc
shark_vacs = await ayla_api.async_get_devices(False)
- device_names = ", ".join([d.name for d in shark_vacs])
+ device_names = ", ".join(d.name for d in shark_vacs)
_LOGGER.debug("Found %d Shark IQ device(s): %s", len(shark_vacs), device_names)
coordinator = SharkIqUpdateCoordinator(hass, config_entry, ayla_api, shark_vacs)
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise exceptions.ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][config_entry.entry_id] = coordinator
- for component in COMPONENTS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return True
@@ -81,11 +79,10 @@ async def async_setup_entry(hass, config_entry):
async def async_disconnect_or_timeout(coordinator: SharkIqUpdateCoordinator):
"""Disconnect to vacuum."""
_LOGGER.debug("Disconnecting from Ayla Api")
- with async_timeout.timeout(5):
- try:
- await coordinator.ayla_api.async_sign_out()
- except (SharkIqAuthError, SharkIqAuthExpiringError, SharkIqNotAuthedError):
- pass
+ with async_timeout.timeout(5), suppress(
+ SharkIqAuthError, SharkIqAuthExpiringError, SharkIqNotAuthedError
+ ):
+ await coordinator.ayla_api.async_sign_out()
async def async_update_options(hass, config_entry):
@@ -98,17 +95,15 @@ async def async_unload_entry(hass, config_entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in COMPONENTS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
if unload_ok:
domain_data = hass.data[DOMAIN][config_entry.entry_id]
- try:
+ with suppress(SharkIqAuthError):
await async_disconnect_or_timeout(coordinator=domain_data)
- except SharkIqAuthError:
- pass
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok
diff --git a/homeassistant/components/sharkiq/config_flow.py b/homeassistant/components/sharkiq/config_flow.py
index 9d2e80b8ec60e8..046aaee7df5117 100644
--- a/homeassistant/components/sharkiq/config_flow.py
+++ b/homeassistant/components/sharkiq/config_flow.py
@@ -1,7 +1,7 @@
"""Config flow for Shark IQ integration."""
+from __future__ import annotations
import asyncio
-from typing import Dict, Optional
import aiohttp
import async_timeout
@@ -11,7 +11,7 @@
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from .const import _LOGGER, DOMAIN # pylint:disable=unused-import
+from .const import _LOGGER, DOMAIN
SHARKIQ_SCHEMA = vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
@@ -62,7 +62,7 @@ async def _async_validate_input(self, user_input):
errors["base"] = "unknown"
return info, errors
- async def async_step_user(self, user_input: Optional[Dict] = None):
+ async def async_step_user(self, user_input: dict | None = None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
@@ -76,7 +76,7 @@ async def async_step_user(self, user_input: Optional[Dict] = None):
step_id="user", data_schema=SHARKIQ_SCHEMA, errors=errors
)
- async def async_step_reauth(self, user_input: Optional[dict] = None):
+ async def async_step_reauth(self, user_input: dict | None = None):
"""Handle re-auth if login is invalid."""
errors = {}
diff --git a/homeassistant/components/sharkiq/const.py b/homeassistant/components/sharkiq/const.py
index e0feb306f77882..8f4c56b65dba74 100644
--- a/homeassistant/components/sharkiq/const.py
+++ b/homeassistant/components/sharkiq/const.py
@@ -5,7 +5,7 @@
_LOGGER = logging.getLogger(__package__)
API_TIMEOUT = 20
-COMPONENTS = ["vacuum"]
+PLATFORMS = ["vacuum"]
DOMAIN = "sharkiq"
SHARK = "Shark"
UPDATE_INTERVAL = timedelta(seconds=30)
diff --git a/homeassistant/components/sharkiq/translations/de.json b/homeassistant/components/sharkiq/translations/de.json
index 2294960d6f261d..8a6f9b14747b5b 100644
--- a/homeassistant/components/sharkiq/translations/de.json
+++ b/homeassistant/components/sharkiq/translations/de.json
@@ -1,11 +1,14 @@
{
"config": {
"abort": {
- "cannot_connect": "Verbindungsfehler",
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich",
"unknown": "Unerwarteter Fehler"
},
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"step": {
diff --git a/homeassistant/components/sharkiq/translations/fr.json b/homeassistant/components/sharkiq/translations/fr.json
index 5f05292ec2a8bb..6fa3ba7707cfb4 100644
--- a/homeassistant/components/sharkiq/translations/fr.json
+++ b/homeassistant/components/sharkiq/translations/fr.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9",
+ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9",
"cannot_connect": "\u00c9chec de connexion",
"reauth_successful": "Jeton d'acc\u00e8s mis \u00e0 jour avec succ\u00e8s",
"unknown": "Erreur inattendue"
diff --git a/homeassistant/components/sharkiq/translations/hu.json b/homeassistant/components/sharkiq/translations/hu.json
new file mode 100644
index 00000000000000..b765ad68a3ff43
--- /dev/null
+++ b/homeassistant/components/sharkiq/translations/hu.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sharkiq/translations/id.json b/homeassistant/components/sharkiq/translations/id.json
new file mode 100644
index 00000000000000..e3d8b4a8ed24c5
--- /dev/null
+++ b/homeassistant/components/sharkiq/translations/id.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung",
+ "reauth_successful": "Autentikasi ulang berhasil",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sharkiq/translations/it.json b/homeassistant/components/sharkiq/translations/it.json
index 4f7940cb5fc070..cfba2066bfca6c 100644
--- a/homeassistant/components/sharkiq/translations/it.json
+++ b/homeassistant/components/sharkiq/translations/it.json
@@ -3,7 +3,7 @@
"abort": {
"already_configured": "L'account \u00e8 gi\u00e0 configurato",
"cannot_connect": "Impossibile connettersi",
- "reauth_successful": "La riautenticazione ha avuto successo",
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente",
"unknown": "Errore imprevisto"
},
"error": {
diff --git a/homeassistant/components/sharkiq/translations/ko.json b/homeassistant/components/sharkiq/translations/ko.json
index 92649031534ab6..04f400212f19fe 100644
--- a/homeassistant/components/sharkiq/translations/ko.json
+++ b/homeassistant/components/sharkiq/translations/ko.json
@@ -1,26 +1,27 @@
{
"config": {
"abort": {
- "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
- "reauth_successful": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc131\uacf5\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328",
- "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d",
- "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"reauth": {
"data": {
- "password": "\uc554\ud638",
- "username": "\uc0ac\uc6a9\uc790\uba85"
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
}
},
"user": {
"data": {
- "password": "\uc554\ud638",
- "username": "\uc0ac\uc6a9\uc790\uba85"
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
}
}
}
diff --git a/homeassistant/components/sharkiq/translations/nl.json b/homeassistant/components/sharkiq/translations/nl.json
index 96c10f3e2f0812..3acfdbdf0747cd 100644
--- a/homeassistant/components/sharkiq/translations/nl.json
+++ b/homeassistant/components/sharkiq/translations/nl.json
@@ -3,6 +3,7 @@
"abort": {
"already_configured": "Account is al geconfigureerd",
"cannot_connect": "Kan geen verbinding maken",
+ "reauth_successful": "Herauthenticatie was succesvol",
"unknown": "Onverwachte fout"
},
"error": {
diff --git a/homeassistant/components/sharkiq/translations/ru.json b/homeassistant/components/sharkiq/translations/ru.json
index 80af08a8958837..0e91c64c7d41e6 100644
--- a/homeassistant/components/sharkiq/translations/ru.json
+++ b/homeassistant/components/sharkiq/translations/ru.json
@@ -8,20 +8,20 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
"reauth": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
}
},
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
}
}
}
diff --git a/homeassistant/components/sharkiq/translations/tr.json b/homeassistant/components/sharkiq/translations/tr.json
new file mode 100644
index 00000000000000..c82f1e8bf051d7
--- /dev/null
+++ b/homeassistant/components/sharkiq/translations/tr.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu",
+ "unknown": "Beklenmeyen hata"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sharkiq/translations/uk.json b/homeassistant/components/sharkiq/translations/uk.json
new file mode 100644
index 00000000000000..0f78c62fa7ecea
--- /dev/null
+++ b/homeassistant/components/sharkiq/translations/uk.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sharkiq/update_coordinator.py b/homeassistant/components/sharkiq/update_coordinator.py
index eff18064dcc3b0..73f4093739a49f 100644
--- a/homeassistant/components/sharkiq/update_coordinator.py
+++ b/homeassistant/components/sharkiq/update_coordinator.py
@@ -1,7 +1,7 @@
"""Data update coordinator for shark iq vacuums."""
+from __future__ import annotations
import asyncio
-from typing import Dict, List, Set
from async_timeout import timeout
from sharkiqpy import (
@@ -12,7 +12,7 @@
SharkIqVacuum,
)
-from homeassistant.config_entries import ConfigEntry
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -27,11 +27,11 @@ def __init__(
hass: HomeAssistant,
config_entry: ConfigEntry,
ayla_api: AylaApi,
- shark_vacs: List[SharkIqVacuum],
+ shark_vacs: list[SharkIqVacuum],
) -> None:
"""Set up the SharkIqUpdateCoordinator class."""
self.ayla_api = ayla_api
- self.shark_vacs: Dict[str, SharkIqVacuum] = {
+ self.shark_vacs: dict[str, SharkIqVacuum] = {
sharkiq.serial_number: sharkiq for sharkiq in shark_vacs
}
self._config_entry = config_entry
@@ -40,7 +40,7 @@ def __init__(
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL)
@property
- def online_dsns(self) -> Set[str]:
+ def online_dsns(self) -> set[str]:
"""Get the set of all online DSNs."""
return self._online_dsns
@@ -76,7 +76,7 @@ async def _async_update_data(self) -> bool:
) as err:
_LOGGER.debug("Bad auth state. Attempting re-auth", exc_info=err)
flow_context = {
- "source": "reauth",
+ "source": SOURCE_REAUTH,
"unique_id": self._config_entry.unique_id,
}
@@ -99,7 +99,7 @@ async def _async_update_data(self) -> bool:
_LOGGER.debug("Matching flow found")
raise UpdateFailed(err) from err
- except Exception as err: # pylint: disable=broad-except
+ except Exception as err:
_LOGGER.exception("Unexpected error updating SharkIQ")
raise UpdateFailed(err) from err
diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py
index 9684dde45e64de..eed41fb14387c8 100644
--- a/homeassistant/components/sharkiq/vacuum.py
+++ b/homeassistant/components/sharkiq/vacuum.py
@@ -1,8 +1,8 @@
"""Shark IQ Wrapper."""
-
+from __future__ import annotations
import logging
-from typing import Dict, Iterable, Optional
+from typing import Iterable
from sharkiqpy import OperatingModes, PowerModes, Properties, SharkIqVacuum
@@ -69,7 +69,7 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Shark IQ vacuum cleaner."""
coordinator: SharkIqUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
- devices: Iterable["SharkIqVacuum"] = coordinator.shark_vacs.values()
+ devices: Iterable[SharkIqVacuum] = coordinator.shark_vacs.values()
device_names = [d.name for d in devices]
_LOGGER.debug(
"Found %d Shark IQ device(s): %s",
@@ -118,7 +118,7 @@ def model(self) -> str:
return self.sharkiq.oem_model_number
@property
- def device_info(self) -> Dict:
+ def device_info(self) -> dict:
"""Device info dictionary."""
return {
"identifiers": {(DOMAIN, self.serial_number)},
@@ -136,30 +136,30 @@ def supported_features(self) -> int:
return SUPPORT_SHARKIQ
@property
- def is_docked(self) -> Optional[bool]:
+ def is_docked(self) -> bool | None:
"""Is vacuum docked."""
return self.sharkiq.get_property_value(Properties.DOCKED_STATUS)
@property
- def error_code(self) -> Optional[int]:
+ def error_code(self) -> int | None:
"""Return the last observed error code (or None)."""
return self.sharkiq.error_code
@property
- def error_message(self) -> Optional[str]:
+ def error_message(self) -> str | None:
"""Return the last observed error message (or None)."""
if not self.error_code:
return None
return self.sharkiq.error_text
@property
- def operating_mode(self) -> Optional[str]:
+ def operating_mode(self) -> str | None:
"""Operating mode.."""
op_mode = self.sharkiq.get_property_value(Properties.OPERATING_MODE)
return OPERATING_STATE_MAP.get(op_mode)
@property
- def recharging_to_resume(self) -> Optional[int]:
+ def recharging_to_resume(self) -> int | None:
"""Return True if vacuum set to recharge and resume cleaning."""
return self.sharkiq.get_property_value(Properties.RECHARGING_TO_RESUME)
@@ -240,12 +240,12 @@ def fan_speed_list(self):
# Various attributes we want to expose
@property
- def recharge_resume(self) -> Optional[bool]:
+ def recharge_resume(self) -> bool | None:
"""Recharge and resume mode active."""
return self.sharkiq.get_property_value(Properties.RECHARGE_RESUME)
@property
- def rssi(self) -> Optional[int]:
+ def rssi(self) -> int | None:
"""Get the WiFi RSSI."""
return self.sharkiq.get_property_value(Properties.RSSI)
@@ -255,7 +255,7 @@ def low_light(self):
return self.sharkiq.get_property_value(Properties.LOW_LIGHT_MISSION)
@property
- def device_state_attributes(self) -> Dict:
+ def extra_state_attributes(self) -> dict:
"""Return a dictionary of device state attributes specific to sharkiq."""
data = {
ATTR_ERROR_CODE: self.error_code,
diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py
index 1173d0477abb95..089dc36b1a8222 100644
--- a/homeassistant/components/shell_command/__init__.py
+++ b/homeassistant/components/shell_command/__init__.py
@@ -1,5 +1,6 @@
"""Expose regular shell commands as services."""
import asyncio
+from contextlib import suppress
import logging
import shlex
@@ -87,10 +88,8 @@ async def async_service_handler(service: ServiceCall) -> None:
"Timed out running command: `%s`, after: %ss", cmd, COMMAND_TIMEOUT
)
if process:
- try:
+ with suppress(TypeError):
await process.kill()
- except TypeError:
- pass
del process
return
diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py
index d2df03b44a55e8..be87e2556eb803 100644
--- a/homeassistant/components/shelly/__init__.py
+++ b/homeassistant/components/shelly/__init__.py
@@ -2,7 +2,6 @@
import asyncio
from datetime import timedelta
import logging
-from socket import gethostbyname
import aioshelly
import async_timeout
@@ -17,12 +16,7 @@
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers import (
- aiohttp_client,
- device_registry,
- singleton,
- update_coordinator,
-)
+from homeassistant.helpers import aiohttp_client, device_registry, update_coordinator
from .const import (
AIOSHELLY_DEVICE_TIMEOUT_SEC,
@@ -32,36 +26,23 @@
BATTERY_DEVICES_WITH_PERMANENT_CONNECTION,
COAP,
DATA_CONFIG_ENTRY,
+ DEVICE,
DOMAIN,
EVENT_SHELLY_CLICK,
INPUTS_EVENTS_DICT,
- POLLING_TIMEOUT_MULTIPLIER,
+ POLLING_TIMEOUT_SEC,
REST,
REST_SENSORS_UPDATE_INTERVAL,
SLEEP_PERIOD_MULTIPLIER,
UPDATE_PERIOD_MULTIPLIER,
)
-from .utils import get_device_name
+from .utils import get_coap_context, get_device_name, get_device_sleep_period
PLATFORMS = ["binary_sensor", "cover", "light", "sensor", "switch"]
+SLEEPING_PLATFORMS = ["binary_sensor", "sensor"]
_LOGGER = logging.getLogger(__name__)
-@singleton.singleton("shelly_coap")
-async def get_coap_context(hass):
- """Get CoAP context to be used in all Shelly devices."""
- context = aioshelly.COAP()
- await context.initialize()
-
- @callback
- def shutdown_listener(ev):
- context.close()
-
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_listener)
-
- return context
-
-
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Shelly component."""
hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}}
@@ -70,12 +51,13 @@ async def async_setup(hass: HomeAssistant, config: dict):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Shelly from a config entry."""
- temperature_unit = "C" if hass.config.units.is_metric else "F"
+ hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = {}
+ hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None
- ip_address = await hass.async_add_executor_job(gethostbyname, entry.data[CONF_HOST])
+ temperature_unit = "C" if hass.config.units.is_metric else "F"
options = aioshelly.ConnectionOptions(
- ip_address,
+ entry.data[CONF_HOST],
entry.data.get(CONF_USERNAME),
entry.data.get(CONF_PASSWORD),
temperature_unit,
@@ -83,33 +65,82 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
coap_context = await get_coap_context(hass)
- try:
- async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
- device = await aioshelly.Device.create(
- aiohttp_client.async_get_clientsession(hass),
- coap_context,
- options,
- )
- except (asyncio.TimeoutError, OSError) as err:
- raise ConfigEntryNotReady from err
+ device = await aioshelly.Device.create(
+ aiohttp_client.async_get_clientsession(hass),
+ coap_context,
+ options,
+ False,
+ )
- hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = {}
- coap_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
+ dev_reg = await device_registry.async_get_registry(hass)
+ identifier = (DOMAIN, entry.unique_id)
+ device_entry = dev_reg.async_get_device(identifiers={identifier}, connections=set())
+ if device_entry and entry.entry_id not in device_entry.config_entries:
+ device_entry = None
+
+ sleep_period = entry.data.get("sleep_period")
+
+ @callback
+ def _async_device_online(_):
+ _LOGGER.debug("Device %s is online, resuming setup", entry.title)
+ hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None
+
+ if sleep_period is None:
+ data = {**entry.data}
+ data["sleep_period"] = get_device_sleep_period(device.settings)
+ data["model"] = device.settings["device"]["type"]
+ hass.config_entries.async_update_entry(entry, data=data)
+
+ hass.async_create_task(async_device_setup(hass, entry, device))
+
+ if sleep_period == 0:
+ # Not a sleeping device, finish setup
+ _LOGGER.debug("Setting up online device %s", entry.title)
+ try:
+ async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
+ await device.initialize(True)
+ except (asyncio.TimeoutError, OSError) as err:
+ raise ConfigEntryNotReady from err
+
+ await async_device_setup(hass, entry, device)
+ elif sleep_period is None or device_entry is None:
+ # Need to get sleep info or first time sleeping device setup, wait for device
+ hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = device
+ _LOGGER.debug(
+ "Setup for device %s will resume when device is online", entry.title
+ )
+ device.subscribe_updates(_async_device_online)
+ await device.coap_request("s")
+ else:
+ # Restore sensors for sleeping device
+ _LOGGER.debug("Setting up offline device %s", entry.title)
+ await async_device_setup(hass, entry, device)
+
+ return True
+
+
+async def async_device_setup(
+ hass: HomeAssistant, entry: ConfigEntry, device: aioshelly.Device
+):
+ """Set up a device that is online."""
+ device_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
COAP
] = ShellyDeviceWrapper(hass, entry, device)
- await coap_wrapper.async_setup()
+ await device_wrapper.async_setup()
+
+ platforms = SLEEPING_PLATFORMS
- hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
- REST
- ] = ShellyDeviceRestWrapper(hass, device)
+ if not entry.data.get("sleep_period"):
+ hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
+ REST
+ ] = ShellyDeviceRestWrapper(hass, device)
+ platforms = PLATFORMS
- for component in PLATFORMS:
+ for platform in platforms:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
- return True
-
class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
"""Wrapper for a Shelly device with Home Assistant specific functions."""
@@ -117,43 +148,40 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
def __init__(self, hass, entry, device: aioshelly.Device):
"""Initialize the Shelly device wrapper."""
self.device_id = None
- sleep_mode = device.settings.get("sleep_mode")
-
- if sleep_mode:
- sleep_period = sleep_mode["period"]
- if sleep_mode["unit"] == "h":
- sleep_period *= 60 # hours to minutes
+ sleep_period = entry.data["sleep_period"]
- update_interval = (
- SLEEP_PERIOD_MULTIPLIER * sleep_period * 60
- ) # minutes to seconds
+ if sleep_period:
+ update_interval = SLEEP_PERIOD_MULTIPLIER * sleep_period
else:
update_interval = (
UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"]
)
+ device_name = get_device_name(device) if device.initialized else entry.title
super().__init__(
hass,
_LOGGER,
- name=get_device_name(device),
+ name=device_name,
update_interval=timedelta(seconds=update_interval),
)
self.hass = hass
self.entry = entry
self.device = device
- self.device.subscribe_updates(self.async_set_updated_data)
-
- self._async_remove_input_events_handler = self.async_add_listener(
- self._async_input_events_handler
+ self._async_remove_device_updates_handler = self.async_add_listener(
+ self._async_device_updates_handler
)
- self._last_input_events_count = dict()
+ self._last_input_events_count = {}
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop)
@callback
- def _async_input_events_handler(self):
- """Handle device input events."""
+ def _async_device_updates_handler(self):
+ """Handle device updates."""
+ if not self.device.initialized:
+ return
+
+ # Check for input events
for block in self.device.blocks:
if (
"inputEvent" not in block.sensor_ids
@@ -192,13 +220,13 @@ def _async_input_events_handler(self):
async def _async_update_data(self):
"""Fetch data."""
+ if self.entry.data.get("sleep_period"):
+ # Sleeping device, no point polling it, just mark it unavailable
+ raise update_coordinator.UpdateFailed("Sleeping device did not update")
_LOGGER.debug("Polling Shelly Device - %s", self.name)
try:
- async with async_timeout.timeout(
- POLLING_TIMEOUT_MULTIPLIER
- * self.device.settings["coiot"]["update_period"]
- ):
+ async with async_timeout.timeout(POLLING_TIMEOUT_SEC):
return await self.device.update()
except OSError as err:
raise update_coordinator.UpdateFailed("Error fetching data") from err
@@ -206,18 +234,17 @@ async def _async_update_data(self):
@property
def model(self):
"""Model of the device."""
- return self.device.settings["device"]["type"]
+ return self.entry.data["model"]
@property
def mac(self):
"""Mac address of the device."""
- return self.device.settings["device"]["mac"]
+ return self.entry.unique_id
async def async_setup(self):
"""Set up the wrapper."""
-
dev_reg = await device_registry.async_get_registry(self.hass)
- model_type = self.device.settings["device"]["type"]
+ sw_version = self.device.settings["fw"] if self.device.initialized else ""
entry = dev_reg.async_get_or_create(
config_entry_id=self.entry.entry_id,
name=self.name,
@@ -225,15 +252,16 @@ async def async_setup(self):
# This is duplicate but otherwise via_device can't work
identifiers={(DOMAIN, self.mac)},
manufacturer="Shelly",
- model=aioshelly.MODEL_NAMES.get(model_type, model_type),
- sw_version=self.device.settings["fw"],
+ model=aioshelly.MODEL_NAMES.get(self.model, self.model),
+ sw_version=sw_version,
)
self.device_id = entry.id
+ self.device.subscribe_updates(self.async_set_updated_data)
def shutdown(self):
"""Shutdown the wrapper."""
self.device.shutdown()
- self._async_remove_input_events_handler()
+ self._async_remove_device_updates_handler()
@callback
def _handle_ha_stop(self, _):
@@ -282,11 +310,23 @@ def mac(self):
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
+ device = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id].get(DEVICE)
+ if device is not None:
+ # If device is present, device wrapper is not setup yet
+ device.shutdown()
+ return True
+
+ platforms = SLEEPING_PLATFORMS
+
+ if not entry.data.get("sleep_period"):
+ hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][REST] = None
+ platforms = PLATFORMS
+
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in platforms
]
)
)
diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py
index d53f089054a20a..385b3b30c36dd0 100644
--- a/homeassistant/components/shelly/binary_sensor.py
+++ b/homeassistant/components/shelly/binary_sensor.py
@@ -9,6 +9,7 @@
DEVICE_CLASS_PROBLEM,
DEVICE_CLASS_SMOKE,
DEVICE_CLASS_VIBRATION,
+ STATE_ON,
BinarySensorEntity,
)
@@ -17,6 +18,7 @@
RestAttributeDescription,
ShellyBlockAttributeEntity,
ShellyRestAttributeEntity,
+ ShellySleepingBlockAttributeEntity,
async_setup_entry_attribute_entities,
async_setup_entry_rest,
)
@@ -45,7 +47,7 @@
name="Gas",
device_class=DEVICE_CLASS_GAS,
value=lambda value: value in ["mild", "heavy"],
- device_state_attributes=lambda block: {"detected": block.gas},
+ extra_state_attributes=lambda block: {"detected": block.gas},
),
("sensor", "smoke"): BlockAttributeDescription(
name="Smoke", device_class=DEVICE_CLASS_SMOKE
@@ -71,6 +73,11 @@
default_enabled=False,
removal_condition=is_momentary_input,
),
+ ("sensor", "extInput"): BlockAttributeDescription(
+ name="External Input",
+ device_class=DEVICE_CLASS_POWER,
+ default_enabled=False,
+ ),
("sensor", "motion"): BlockAttributeDescription(
name="Motion", device_class=DEVICE_CLASS_MOTION
),
@@ -84,11 +91,11 @@
default_enabled=False,
),
"fwupdate": RestAttributeDescription(
- name="Firmware update",
+ name="Firmware Update",
icon="mdi:update",
value=lambda status, _: status["update"]["has_update"],
default_enabled=False,
- device_state_attributes=lambda status: {
+ extra_state_attributes=lambda status: {
"latest_stable_version": status["update"]["new_version"],
"installed_version": status["update"]["old_version"],
},
@@ -98,13 +105,25 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up sensors for device."""
- await async_setup_entry_attribute_entities(
- hass, config_entry, async_add_entities, SENSORS, ShellyBinarySensor
- )
-
- await async_setup_entry_rest(
- hass, config_entry, async_add_entities, REST_SENSORS, ShellyRestBinarySensor
- )
+ if config_entry.data["sleep_period"]:
+ await async_setup_entry_attribute_entities(
+ hass,
+ config_entry,
+ async_add_entities,
+ SENSORS,
+ ShellySleepingBinarySensor,
+ )
+ else:
+ await async_setup_entry_attribute_entities(
+ hass, config_entry, async_add_entities, SENSORS, ShellyBinarySensor
+ )
+ await async_setup_entry_rest(
+ hass,
+ config_entry,
+ async_add_entities,
+ REST_SENSORS,
+ ShellyRestBinarySensor,
+ )
class ShellyBinarySensor(ShellyBlockAttributeEntity, BinarySensorEntity):
@@ -123,3 +142,17 @@ class ShellyRestBinarySensor(ShellyRestAttributeEntity, BinarySensorEntity):
def is_on(self):
"""Return true if REST sensor state is on."""
return bool(self.attribute_value)
+
+
+class ShellySleepingBinarySensor(
+ ShellySleepingBlockAttributeEntity, BinarySensorEntity
+):
+ """Represent a shelly sleeping binary sensor."""
+
+ @property
+ def is_on(self):
+ """Return true if sensor state is on."""
+ if self.block is not None:
+ return bool(self.attribute_value)
+
+ return self.last_state == STATE_ON
diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py
index b47c76cbb7a2cc..73c231086eff5b 100644
--- a/homeassistant/components/shelly/config_flow.py
+++ b/homeassistant/components/shelly/config_flow.py
@@ -1,7 +1,6 @@
"""Config flow for Shelly integration."""
import asyncio
import logging
-from socket import gethostbyname
import aiohttp
import aioshelly
@@ -17,9 +16,8 @@
)
from homeassistant.helpers import aiohttp_client
-from . import get_coap_context
-from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, DOMAIN
+from .utils import get_coap_context, get_device_sleep_period
_LOGGER = logging.getLogger(__name__)
@@ -33,10 +31,9 @@ async def validate_input(hass: core.HomeAssistant, host, data):
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
- ip_address = await hass.async_add_executor_job(gethostbyname, host)
options = aioshelly.ConnectionOptions(
- ip_address, data.get(CONF_USERNAME), data.get(CONF_PASSWORD)
+ host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD)
)
coap_context = await get_coap_context(hass)
@@ -53,6 +50,8 @@ async def validate_input(hass: core.HomeAssistant, host, data):
return {
"title": device.settings["name"],
"hostname": device.settings["device"]["hostname"],
+ "sleep_period": get_device_sleep_period(device.settings),
+ "model": device.settings["device"]["type"],
}
@@ -63,6 +62,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
host = None
info = None
+ device_info = None
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
@@ -95,7 +95,11 @@ async def async_step_user(self, user_input=None):
else:
return self.async_create_entry(
title=device_info["title"] or device_info["hostname"],
- data=user_input,
+ data={
+ **user_input,
+ "sleep_period": device_info["sleep_period"],
+ "model": device_info["model"],
+ },
)
return self.async_show_form(
@@ -121,7 +125,12 @@ async def async_step_credentials(self, user_input=None):
else:
return self.async_create_entry(
title=device_info["title"] or device_info["hostname"],
- data={**user_input, CONF_HOST: self.host},
+ data={
+ **user_input,
+ CONF_HOST: self.host,
+ "sleep_period": device_info["sleep_period"],
+ "model": device_info["model"],
+ },
)
else:
user_input = {}
@@ -149,31 +158,35 @@ async def async_step_zeroconf(self, zeroconf_info):
await self.async_set_unique_id(info["mac"])
self._abort_if_unique_id_configured({CONF_HOST: zeroconf_info["host"]})
self.host = zeroconf_info["host"]
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
+
self.context["title_placeholders"] = {
"name": zeroconf_info.get("name", "").split(".")[0]
}
+
+ if info["auth"]:
+ return await self.async_step_credentials()
+
+ try:
+ self.device_info = await validate_input(self.hass, self.host, {})
+ except HTTP_CONNECT_ERRORS:
+ return self.async_abort(reason="cannot_connect")
+
return await self.async_step_confirm_discovery()
async def async_step_confirm_discovery(self, user_input=None):
"""Handle discovery confirm."""
errors = {}
if user_input is not None:
- if self.info["auth"]:
- return await self.async_step_credentials()
+ return self.async_create_entry(
+ title=self.device_info["title"] or self.device_info["hostname"],
+ data={
+ "host": self.host,
+ "sleep_period": self.device_info["sleep_period"],
+ "model": self.device_info["model"],
+ },
+ )
- try:
- device_info = await validate_input(self.hass, self.host, {})
- except HTTP_CONNECT_ERRORS:
- errors["base"] = "cannot_connect"
- except Exception: # pylint: disable=broad-except
- _LOGGER.exception("Unexpected exception")
- errors["base"] = "unknown"
- else:
- return self.async_create_entry(
- title=device_info["title"] or device_info["hostname"],
- data={"host": self.host},
- )
+ self._set_confirm_only()
return self.async_show_form(
step_id="confirm_discovery",
diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py
index a5922d0b9c0071..4fda656e7b4463 100644
--- a/homeassistant/components/shelly/const.py
+++ b/homeassistant/components/shelly/const.py
@@ -2,11 +2,12 @@
COAP = "coap"
DATA_CONFIG_ENTRY = "config_entry"
+DEVICE = "device"
DOMAIN = "shelly"
REST = "rest"
-# Used to calculate the timeout in "_async_update_data" used for polling data from devices.
-POLLING_TIMEOUT_MULTIPLIER = 1.2
+# Used in "_async_update_data" as timeout for polling data from devices.
+POLLING_TIMEOUT_SEC = 18
# Refresh interval for REST sensors
REST_SENSORS_UPDATE_INTERVAL = 60
@@ -73,5 +74,5 @@
# Kelvin value for colorTemp
KELVIN_MAX_VALUE = 6500
-KELVIN_MIN_VALUE = 2700
-KELVIN_MIN_VALUE_SHBLB_1 = 3000
+KELVIN_MIN_VALUE_WHITE = 2700
+KELVIN_MIN_VALUE_COLOR = 3000
diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py
index 6caa7d5132c320..0438e5fe6b7e9d 100644
--- a/homeassistant/components/shelly/cover.py
+++ b/homeassistant/components/shelly/cover.py
@@ -3,6 +3,7 @@
from homeassistant.components.cover import (
ATTR_POSITION,
+ DEVICE_CLASS_SHUTTER,
SUPPORT_CLOSE,
SUPPORT_OPEN,
SUPPORT_SET_POSITION,
@@ -75,6 +76,11 @@ def supported_features(self):
"""Flag supported features."""
return self._supported_features
+ @property
+ def device_class(self) -> str:
+ """Return the class of the device."""
+ return DEVICE_CLASS_SHUTTER
+
async def async_close_cover(self, **kwargs):
"""Close cover."""
self.control_result = await self.block.set_state(go="close")
diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py
index f6cdfaee19f2be..b7cf11209490dc 100644
--- a/homeassistant/components/shelly/device_trigger.py
+++ b/homeassistant/components/shelly/device_trigger.py
@@ -1,5 +1,5 @@
"""Provides device triggers for Shelly."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -60,7 +60,7 @@ async def async_validate_trigger_config(hass, config):
)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers for Shelly devices."""
triggers = []
@@ -92,18 +92,15 @@ async def async_attach_trigger(
automation_info: dict,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
- config = TRIGGER_SCHEMA(config)
- event_config = event_trigger.TRIGGER_SCHEMA(
- {
- event_trigger.CONF_PLATFORM: CONF_EVENT,
- event_trigger.CONF_EVENT_TYPE: EVENT_SHELLY_CLICK,
- event_trigger.CONF_EVENT_DATA: {
- ATTR_DEVICE_ID: config[CONF_DEVICE_ID],
- ATTR_CHANNEL: INPUTS_EVENTS_SUBTYPES[config[CONF_SUBTYPE]],
- ATTR_CLICK_TYPE: config[CONF_TYPE],
- },
- }
- )
+ event_config = {
+ event_trigger.CONF_PLATFORM: CONF_EVENT,
+ event_trigger.CONF_EVENT_TYPE: EVENT_SHELLY_CLICK,
+ event_trigger.CONF_EVENT_DATA: {
+ ATTR_DEVICE_ID: config[CONF_DEVICE_ID],
+ ATTR_CHANNEL: INPUTS_EVENTS_SUBTYPES[config[CONF_SUBTYPE]],
+ ATTR_CLICK_TYPE: config[CONF_TYPE],
+ },
+ }
event_config = event_trigger.TRIGGER_SCHEMA(event_config)
return await event_trigger.async_attach_trigger(
hass, event_config, action, automation_info, platform_type="device"
diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py
index b4df2d486f8976..48d37312225c3f 100644
--- a/homeassistant/components/shelly/entity.py
+++ b/homeassistant/components/shelly/entity.py
@@ -1,24 +1,51 @@
"""Shelly entity helper."""
+from __future__ import annotations
+
from dataclasses import dataclass
-from typing import Any, Callable, Optional, Union
+import logging
+from typing import Any, Callable
import aioshelly
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback
-from homeassistant.helpers import device_registry, entity, update_coordinator
+from homeassistant.helpers import (
+ device_registry,
+ entity,
+ entity_registry,
+ update_coordinator,
+)
+from homeassistant.helpers.restore_state import RestoreEntity
from . import ShellyDeviceRestWrapper, ShellyDeviceWrapper
from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN, REST
from .utils import async_remove_shelly_entity, get_entity_name
+_LOGGER = logging.getLogger(__name__)
+
async def async_setup_entry_attribute_entities(
hass, config_entry, async_add_entities, sensors, sensor_class
):
- """Set up entities for block attributes."""
+ """Set up entities for attributes."""
wrapper: ShellyDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id
][COAP]
+
+ if wrapper.device.initialized:
+ await async_setup_block_attribute_entities(
+ hass, async_add_entities, wrapper, sensors, sensor_class
+ )
+ else:
+ await async_restore_block_attribute_entities(
+ hass, config_entry, async_add_entities, wrapper, sensor_class
+ )
+
+
+async def async_setup_block_attribute_entities(
+ hass, async_add_entities, wrapper, sensors, sensor_class
+):
+ """Set up entities for block attributes."""
blocks = []
for block in wrapper.device.blocks:
@@ -36,9 +63,7 @@ async def async_setup_entry_attribute_entities(
wrapper.device.settings, block
):
domain = sensor_class.__module__.split(".")[-1]
- unique_id = sensor_class(
- wrapper, block, sensor_id, description
- ).unique_id
+ unique_id = f"{wrapper.mac}-{block.description}-{sensor_id}"
await async_remove_shelly_entity(hass, domain, unique_id)
else:
blocks.append((block, sensor_id, description))
@@ -54,6 +79,39 @@ async def async_setup_entry_attribute_entities(
)
+async def async_restore_block_attribute_entities(
+ hass, config_entry, async_add_entities, wrapper, sensor_class
+):
+ """Restore block attributes entities."""
+ entities = []
+
+ ent_reg = await entity_registry.async_get_registry(hass)
+ entries = entity_registry.async_entries_for_config_entry(
+ ent_reg, config_entry.entry_id
+ )
+
+ domain = sensor_class.__module__.split(".")[-1]
+
+ for entry in entries:
+ if entry.domain != domain:
+ continue
+
+ attribute = entry.unique_id.split("-")[-1]
+ description = BlockAttributeDescription(
+ name="",
+ icon=entry.original_icon,
+ unit=entry.unit_of_measurement,
+ device_class=entry.device_class,
+ )
+
+ entities.append(sensor_class(wrapper, None, attribute, description, entry))
+
+ if not entities:
+ return
+
+ async_add_entities(entities)
+
+
async def async_setup_entry_rest(
hass, config_entry, async_add_entities, sensors, sensor_class
):
@@ -86,17 +144,15 @@ class BlockAttributeDescription:
name: str
# Callable = lambda attr_info: unit
- icon: Optional[str] = None
- unit: Union[None, str, Callable[[dict], str]] = None
+ icon: str | None = None
+ unit: None | str | Callable[[dict], str] = None
value: Callable[[Any], Any] = lambda val: val
- device_class: Optional[str] = None
+ device_class: str | None = None
default_enabled: bool = True
- available: Optional[Callable[[aioshelly.Block], bool]] = None
+ available: Callable[[aioshelly.Block], bool] | None = None
# Callable (settings, block), return true if entity should be removed
- removal_condition: Optional[Callable[[dict, aioshelly.Block], bool]] = None
- device_state_attributes: Optional[
- Callable[[aioshelly.Block], Optional[dict]]
- ] = None
+ removal_condition: Callable[[dict, aioshelly.Block], bool] | None = None
+ extra_state_attributes: Callable[[aioshelly.Block], dict | None] | None = None
@dataclass
@@ -104,12 +160,12 @@ class RestAttributeDescription:
"""Class to describe a REST sensor."""
name: str
- icon: Optional[str] = None
- unit: Optional[str] = None
+ icon: str | None = None
+ unit: str | None = None
value: Callable[[dict, Any], Any] = None
- device_class: Optional[str] = None
+ device_class: str | None = None
default_enabled: bool = True
- device_state_attributes: Optional[Callable[[dict], Optional[dict]]] = None
+ extra_state_attributes: Callable[[dict], dict | None] | None = None
class ShellyBlockEntity(entity.Entity):
@@ -163,7 +219,7 @@ def _update_callback(self):
class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
- """Switch that controls a relay block on Shelly devices."""
+ """Helper class to represent a block attribute."""
def __init__(
self,
@@ -176,12 +232,11 @@ def __init__(
super().__init__(wrapper, block)
self.attribute = attribute
self.description = description
- self.info = block.info(attribute)
unit = self.description.unit
if callable(unit):
- unit = unit(self.info)
+ unit = unit(block.info(attribute))
self._unit = unit
self._unique_id = f"{super().unique_id}-{self.attribute}"
@@ -212,11 +267,6 @@ def attribute_value(self):
return self.description.value(value)
- @property
- def unit_of_measurement(self):
- """Return unit of sensor."""
- return self._unit
-
@property
def device_class(self):
"""Device class of sensor."""
@@ -238,12 +288,12 @@ def available(self):
return self.description.available(self.block)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
- if self.description.device_state_attributes is None:
+ if self.description.extra_state_attributes is None:
return None
- return self.description.device_state_attributes(self.block)
+ return self.description.extra_state_attributes(self.block)
class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity):
@@ -293,11 +343,6 @@ def attribute_value(self):
)
return self._last_value
- @property
- def unit_of_measurement(self):
- """Return unit of sensor."""
- return self.description.unit
-
@property
def device_class(self):
"""Device class of sensor."""
@@ -314,9 +359,73 @@ def unique_id(self):
return f"{self.wrapper.mac}-{self.attribute}"
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return the state attributes."""
- if self.description.device_state_attributes is None:
+ if self.description.extra_state_attributes is None:
return None
- return self.description.device_state_attributes(self.wrapper.device.status)
+ return self.description.extra_state_attributes(self.wrapper.device.status)
+
+
+class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEntity):
+ """Represent a shelly sleeping block attribute entity."""
+
+ # pylint: disable=super-init-not-called
+ def __init__(
+ self,
+ wrapper: ShellyDeviceWrapper,
+ block: aioshelly.Block,
+ attribute: str,
+ description: BlockAttributeDescription,
+ entry: ConfigEntry | None = None,
+ ) -> None:
+ """Initialize the sleeping sensor."""
+ self.last_state = None
+ self.wrapper = wrapper
+ self.attribute = attribute
+ self.block = block
+ self.description = description
+ self._unit = self.description.unit
+
+ if block is not None:
+ if callable(self._unit):
+ self._unit = self._unit(block.info(attribute))
+
+ self._unique_id = f"{self.wrapper.mac}-{block.description}-{attribute}"
+ self._name = get_entity_name(
+ self.wrapper.device, block, self.description.name
+ )
+ else:
+ self._unique_id = entry.unique_id
+ self._name = entry.original_name
+
+ async def async_added_to_hass(self):
+ """Handle entity which will be added."""
+ await super().async_added_to_hass()
+
+ last_state = await self.async_get_last_state()
+
+ if last_state is not None:
+ self.last_state = last_state.state
+
+ @callback
+ def _update_callback(self):
+ """Handle device update."""
+ if self.block is not None or not self.wrapper.device.initialized:
+ super()._update_callback()
+ return
+
+ _, entity_block, entity_sensor = self.unique_id.split("-")
+
+ for block in self.wrapper.device.blocks:
+ if block.description != entity_block:
+ continue
+
+ for sensor_id in block.sensor_ids:
+ if sensor_id != entity_sensor:
+ continue
+
+ self.block = block
+ _LOGGER.debug("Entity %s attached to block", self.name)
+ super()._update_callback()
+ return
diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py
index 0c91ddc1088c4f..a9e137968758a9 100644
--- a/homeassistant/components/shelly/light.py
+++ b/homeassistant/components/shelly/light.py
@@ -1,5 +1,5 @@
"""Light for Shelly."""
-from typing import Optional, Tuple
+from __future__ import annotations
from aioshelly import Block
@@ -28,20 +28,13 @@
DATA_CONFIG_ENTRY,
DOMAIN,
KELVIN_MAX_VALUE,
- KELVIN_MIN_VALUE,
- KELVIN_MIN_VALUE_SHBLB_1,
+ KELVIN_MIN_VALUE_COLOR,
+ KELVIN_MIN_VALUE_WHITE,
)
from .entity import ShellyBlockEntity
from .utils import async_remove_shelly_entity
-def min_kelvin(model: str):
- """Kelvin (min) for colorTemp."""
- if model in ["SHBLB-1"]:
- return KELVIN_MIN_VALUE_SHBLB_1
- return KELVIN_MIN_VALUE
-
-
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up lights for device."""
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP]
@@ -76,6 +69,8 @@ def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None:
self.control_result = None
self.mode_result = None
self._supported_features = 0
+ self._min_kelvin = KELVIN_MIN_VALUE_WHITE
+ self._max_kelvin = KELVIN_MAX_VALUE
if hasattr(block, "brightness") or hasattr(block, "gain"):
self._supported_features |= SUPPORT_BRIGHTNESS
@@ -85,6 +80,7 @@ def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None:
self._supported_features |= SUPPORT_WHITE_VALUE
if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"):
self._supported_features |= SUPPORT_COLOR
+ self._min_kelvin = KELVIN_MIN_VALUE_COLOR
@property
def supported_features(self) -> int:
@@ -100,7 +96,7 @@ def is_on(self) -> bool:
return self.block.output
@property
- def mode(self) -> Optional[str]:
+ def mode(self) -> str | None:
"""Return the color mode of the light."""
if self.mode_result:
return self.mode_result["mode"]
@@ -118,7 +114,7 @@ def mode(self) -> Optional[str]:
return "white"
@property
- def brightness(self) -> Optional[int]:
+ def brightness(self) -> int:
"""Brightness of light."""
if self.mode == "color":
if self.control_result:
@@ -133,7 +129,7 @@ def brightness(self) -> Optional[int]:
return int(brightness / 100 * 255)
@property
- def white_value(self) -> Optional[int]:
+ def white_value(self) -> int:
"""White value of light."""
if self.control_result:
white = self.control_result["white"]
@@ -142,7 +138,7 @@ def white_value(self) -> Optional[int]:
return int(white)
@property
- def hs_color(self) -> Optional[Tuple[float, float]]:
+ def hs_color(self) -> tuple[float, float]:
"""Return the hue and saturation color value of light."""
if self.mode == "white":
return color_RGB_to_hs(255, 255, 255)
@@ -158,7 +154,7 @@ def hs_color(self) -> Optional[Tuple[float, float]]:
return color_RGB_to_hs(red, green, blue)
@property
- def color_temp(self) -> Optional[float]:
+ def color_temp(self) -> int | None:
"""Return the CT color value in mireds."""
if self.mode == "color":
return None
@@ -168,25 +164,28 @@ def color_temp(self) -> Optional[float]:
else:
color_temp = self.block.colorTemp
- # If you set DUO to max mireds in Shelly app, 2700K,
- # It reports 0 temp
- if color_temp == 0:
- return min_kelvin(self.wrapper.model)
+ color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp))
return int(color_temperature_kelvin_to_mired(color_temp))
@property
- def min_mireds(self) -> Optional[float]:
+ def min_mireds(self) -> int:
"""Return the coldest color_temp that this light supports."""
- return color_temperature_kelvin_to_mired(KELVIN_MAX_VALUE)
+ return int(color_temperature_kelvin_to_mired(self._max_kelvin))
@property
- def max_mireds(self) -> Optional[float]:
+ def max_mireds(self) -> int:
"""Return the warmest color_temp that this light supports."""
- return color_temperature_kelvin_to_mired(min_kelvin(self.wrapper.model))
+ return int(color_temperature_kelvin_to_mired(self._min_kelvin))
async def async_turn_on(self, **kwargs) -> None:
"""Turn on light."""
+ if self.block.type == "relay":
+ self.control_result = await self.block.set_state(turn="on")
+ self.async_write_ha_state()
+ return
+
+ set_mode = None
params = {"turn": "on"}
if ATTR_BRIGHTNESS in kwargs:
tmp_brightness = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100)
@@ -196,27 +195,26 @@ async def async_turn_on(self, **kwargs) -> None:
params["brightness"] = tmp_brightness
if ATTR_COLOR_TEMP in kwargs:
color_temp = color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP])
- color_temp = min(
- KELVIN_MAX_VALUE, max(min_kelvin(self.wrapper.model), color_temp)
- )
+ color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp))
# Color temperature change - used only in white mode, switch device mode to white
- if self.mode == "color":
- self.mode_result = await self.wrapper.device.switch_light_mode("white")
- params["red"] = params["green"] = params["blue"] = 255
+ set_mode = "white"
+ params["red"] = params["green"] = params["blue"] = 255
params["temp"] = int(color_temp)
- elif ATTR_HS_COLOR in kwargs:
+ if ATTR_HS_COLOR in kwargs:
red, green, blue = color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
# Color channels change - used only in color mode, switch device mode to color
- if self.mode == "white":
- self.mode_result = await self.wrapper.device.switch_light_mode("color")
+ set_mode = "color"
params["red"] = red
params["green"] = green
params["blue"] = blue
- elif ATTR_WHITE_VALUE in kwargs:
+ if ATTR_WHITE_VALUE in kwargs:
# White channel change - used only in color mode, switch device mode device to color
- if self.mode == "white":
- self.mode_result = await self.wrapper.device.switch_light_mode("color")
+ set_mode = "color"
params["white"] = int(kwargs[ATTR_WHITE_VALUE])
+
+ if set_mode and self.mode != set_mode:
+ self.mode_result = await self.wrapper.device.switch_light_mode(set_mode)
+
self.control_result = await self.block.set_state(**params)
self.async_write_ha_state()
diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json
index 923bcdced34e02..1ae274d6dfd856 100644
--- a/homeassistant/components/shelly/manifest.json
+++ b/homeassistant/components/shelly/manifest.json
@@ -3,7 +3,7 @@
"name": "Shelly",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/shelly",
- "requirements": ["aioshelly==0.5.3"],
+ "requirements": ["aioshelly==0.6.2"],
"zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }],
"codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"]
}
diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py
index b92b90c1b46a41..b6d3bc2dbff3e8 100644
--- a/homeassistant/components/shelly/sensor.py
+++ b/homeassistant/components/shelly/sensor.py
@@ -1,5 +1,6 @@
"""Sensor for Shelly."""
from homeassistant.components import sensor
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
DEGREE,
@@ -18,6 +19,7 @@
RestAttributeDescription,
ShellyBlockAttributeEntity,
ShellyRestAttributeEntity,
+ ShellySleepingBlockAttributeEntity,
async_setup_entry_attribute_entities,
async_setup_entry_rest,
)
@@ -126,10 +128,7 @@
("sensor", "concentration"): BlockAttributeDescription(
name="Gas Concentration",
unit=CONCENTRATION_PARTS_PER_MILLION,
- value=lambda value: value,
icon="mdi:gauge",
- # "sensorOp" is "normal" when the Shelly Gas is working properly and taking measurements.
- available=lambda block: block.sensorOp == "normal",
),
("sensor", "extTemp"): BlockAttributeDescription(
name="Temperature",
@@ -148,13 +147,17 @@
unit=LIGHT_LUX,
device_class=sensor.DEVICE_CLASS_ILLUMINANCE,
),
- ("sensor", "tilt"): BlockAttributeDescription(name="Tilt", unit=DEGREE),
+ ("sensor", "tilt"): BlockAttributeDescription(
+ name="Tilt",
+ unit=DEGREE,
+ icon="mdi:angle-acute",
+ ),
("relay", "totalWorkTime"): BlockAttributeDescription(
- name="Lamp life",
+ name="Lamp Life",
unit=PERCENTAGE,
icon="mdi:progress-wrench",
value=lambda value: round(100 - (value / 3600 / SHAIR_MAX_WORK_HOURS), 1),
- device_state_attributes=lambda block: {
+ extra_state_attributes=lambda block: {
"Operational hours": round(block.totalWorkTime / 3600, 1)
},
),
@@ -164,6 +167,12 @@
value=lambda value: round(value, 1),
device_class=sensor.DEVICE_CLASS_VOLTAGE,
),
+ ("sensor", "sensorOp"): BlockAttributeDescription(
+ name="Operation",
+ icon="mdi:cog-transfer",
+ value=lambda value: value,
+ extra_state_attributes=lambda block: {"self_test": block.selfTest},
+ ),
}
REST_SENSORS = {
@@ -185,15 +194,20 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up sensors for device."""
- await async_setup_entry_attribute_entities(
- hass, config_entry, async_add_entities, SENSORS, ShellySensor
- )
- await async_setup_entry_rest(
- hass, config_entry, async_add_entities, REST_SENSORS, ShellyRestSensor
- )
+ if config_entry.data["sleep_period"]:
+ await async_setup_entry_attribute_entities(
+ hass, config_entry, async_add_entities, SENSORS, ShellySleepingSensor
+ )
+ else:
+ await async_setup_entry_attribute_entities(
+ hass, config_entry, async_add_entities, SENSORS, ShellySensor
+ )
+ await async_setup_entry_rest(
+ hass, config_entry, async_add_entities, REST_SENSORS, ShellyRestSensor
+ )
-class ShellySensor(ShellyBlockAttributeEntity):
+class ShellySensor(ShellyBlockAttributeEntity, SensorEntity):
"""Represent a shelly sensor."""
@property
@@ -201,11 +215,38 @@ def state(self):
"""Return value of sensor."""
return self.attribute_value
+ @property
+ def unit_of_measurement(self):
+ """Return unit of sensor."""
+ return self._unit
-class ShellyRestSensor(ShellyRestAttributeEntity):
+
+class ShellyRestSensor(ShellyRestAttributeEntity, SensorEntity):
"""Represent a shelly REST sensor."""
@property
def state(self):
"""Return value of sensor."""
return self.attribute_value
+
+ @property
+ def unit_of_measurement(self):
+ """Return unit of sensor."""
+ return self.description.unit
+
+
+class ShellySleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
+ """Represent a shelly sleeping sensor."""
+
+ @property
+ def state(self):
+ """Return value of sensor."""
+ if self.block is not None:
+ return self.attribute_value
+
+ return self.last_state
+
+ @property
+ def unit_of_measurement(self):
+ """Return unit of sensor."""
+ return self._unit
diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json
index 341328801cc3c1..85a1fa87d0c070 100644
--- a/homeassistant/components/shelly/strings.json
+++ b/homeassistant/components/shelly/strings.json
@@ -3,7 +3,7 @@
"flow_title": "{name}",
"step": {
"user": {
- "description": "Before set up, battery-powered devices must be woken up by pressing the button on the device.",
+ "description": "Before set up, battery-powered devices must be woken up, you can now wake the device up using a button on it.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
@@ -15,7 +15,7 @@
}
},
"confirm_discovery": {
- "description": "Do you want to set up the {model} at {host}?\n\nBefore set up, battery-powered devices must be woken up by pressing the button on the device."
+ "description": "Do you want to set up the {model} at {host}?\n\nBattery-powered devices that are password protected must be woken up before continuing with setting up.\nBattery-powered devices that are not password protected will be added when the device wakes up, you can now manually wake the device up using a button on it or wait for the next data update from the device."
}
},
"error": {
diff --git a/homeassistant/components/shelly/translations/bg.json b/homeassistant/components/shelly/translations/bg.json
new file mode 100644
index 00000000000000..c856929a5e1f4a
--- /dev/null
+++ b/homeassistant/components/shelly/translations/bg.json
@@ -0,0 +1,10 @@
+{
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "\u0411\u0443\u0442\u043e\u043d",
+ "button1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d",
+ "button2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d",
+ "button3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/ca.json b/homeassistant/components/shelly/translations/ca.json
index 2bf17c2ba77c28..13cc79ac3d8af8 100644
--- a/homeassistant/components/shelly/translations/ca.json
+++ b/homeassistant/components/shelly/translations/ca.json
@@ -12,7 +12,7 @@
"flow_title": "{name}",
"step": {
"confirm_discovery": {
- "description": "Vols configurar el {model} a {host}? \n\nAbans de configurar-lo, els dispositius amb bateria s'han de desperar prement el bot\u00f3 del dispositiu."
+ "description": "Vols configurar el {model} a {host}? \n\nAbans de configurar-lo, els dispositius amb bateria protegits amb contrasenya s'han de desperar prement el bot\u00f3 del dispositiu.\nEls dispositius que no tinguin contrasenya s'afegiran tan bon punt es despertin. Ja pots despertar el dispositiu manualment mitjan\u00e7ant el bot\u00f3 o esperar a la seg\u00fcent transmissi\u00f3 de dades del dispositiu."
},
"credentials": {
"data": {
@@ -24,8 +24,24 @@
"data": {
"host": "Amfitri\u00f3"
},
- "description": "Abans de configurar-lo, els dispositius amb bateria s'han de desperar prement el bot\u00f3 del dispositiu."
+ "description": "Abans de configurar-lo, els dispositius amb bateria s'han de desperar, ja pots clicar el bot\u00f3 del dispositiu per a despertar-lo."
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "Bot\u00f3",
+ "button1": "Primer bot\u00f3",
+ "button2": "Segon bot\u00f3",
+ "button3": "Tercer bot\u00f3"
+ },
+ "trigger_type": {
+ "double": "{subtype} clicat dues vegades",
+ "long": "{subtype} clicat durant una estona",
+ "long_single": "{subtype} clicat durant una estona i despr\u00e9s r\u00e0pid",
+ "single": "{subtype} clicat una vegada",
+ "single_long": "{subtype} clicat r\u00e0pid i, despr\u00e9s, durant una estona",
+ "triple": "{subtype} clicat tres vegades"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/cs.json b/homeassistant/components/shelly/translations/cs.json
index 41ca338ab9e6ab..afdfe7c8f56ab7 100644
--- a/homeassistant/components/shelly/translations/cs.json
+++ b/homeassistant/components/shelly/translations/cs.json
@@ -27,5 +27,21 @@
"description": "P\u0159ed nastaven\u00edm mus\u00ed b\u00fdt za\u0159\u00edzen\u00ed nap\u00e1jen\u00e9 z baterie probuzeno stisknut\u00edm tla\u010d\u00edtka na dan\u00e9m za\u0159\u00edzen\u00ed."
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "Tla\u010d\u00edtko",
+ "button1": "Prvn\u00ed tla\u010d\u00edtko",
+ "button2": "Druh\u00e9 tla\u010d\u00edtko",
+ "button3": "T\u0159et\u00ed tla\u010d\u00edtko"
+ },
+ "trigger_type": {
+ "double": "\"{subtype}\" stisknuto dvakr\u00e1t",
+ "long": "\"{subtype}\" stisknuto dlouze",
+ "long_single": "\"{subtype}\" stisknuto dlouze a pak jednou",
+ "single": "\"{subtype}\" stisknuto jednou",
+ "single_long": "\"{subtype}\" stisknuto jednou a pak dlouze",
+ "triple": "\"{subtype}\" stisknuto t\u0159ikr\u00e1t"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/da.json b/homeassistant/components/shelly/translations/da.json
new file mode 100644
index 00000000000000..08631bc39e132e
--- /dev/null
+++ b/homeassistant/components/shelly/translations/da.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "Knap",
+ "button1": "F\u00f8rste knap",
+ "button2": "Anden knap",
+ "button3": "Tredje knap"
+ },
+ "trigger_type": {
+ "double": "{subtype} dobbelt klik",
+ "long": "{subtype} langt klik",
+ "long_single": "{subtype} langt klik og derefter enkelt klik",
+ "single": "{subtype} enkelt klik",
+ "single_long": "{subtype} enkelt klik og derefter langt klik"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json
index 74d0f831c8bd33..9d78d362c99c75 100644
--- a/homeassistant/components/shelly/translations/de.json
+++ b/homeassistant/components/shelly/translations/de.json
@@ -1,7 +1,12 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "unsupported_firmware": "Das Ger\u00e4t verwendet eine nicht unterst\u00fctzte Firmware-Version."
+ },
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"flow_title": "Shelly: {name}",
@@ -15,8 +20,25 @@
"user": {
"data": {
"host": "Host"
- }
+ },
+ "description": "Vor der Einrichtung m\u00fcssen batteriebetriebene Ger\u00e4te durch Dr\u00fccken der Taste am Ger\u00e4t aufgeweckt werden."
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "Taste",
+ "button1": "Erste Taste",
+ "button2": "Zweite Taste",
+ "button3": "Dritte Taste"
+ },
+ "trigger_type": {
+ "double": "{subtype} zweifach bet\u00e4tigt",
+ "long": "{subtype} gehalten",
+ "long_single": "{subtype} gehalten und dann einfach bet\u00e4tigt",
+ "single": "{subtype} einfach bet\u00e4tigt",
+ "single_long": "{subtype} einfach bet\u00e4tigt und dann gehalten",
+ "triple": "{subtype} dreifach bet\u00e4tigt"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json
index a1fa6b72598eb1..b60d9dfbe3eebe 100644
--- a/homeassistant/components/shelly/translations/en.json
+++ b/homeassistant/components/shelly/translations/en.json
@@ -12,7 +12,7 @@
"flow_title": "{name}",
"step": {
"confirm_discovery": {
- "description": "Do you want to set up the {model} at {host}?\n\nBefore set up, battery-powered devices must be woken up by pressing the button on the device."
+ "description": "Do you want to set up the {model} at {host}?\n\nBattery-powered devices that are password protected must be woken up before continuing with setting up.\nBattery-powered devices that are not password protected will be added when the device wakes up, you can now manually wake the device up using a button on it or wait for the next data update from the device."
},
"credentials": {
"data": {
@@ -24,24 +24,24 @@
"data": {
"host": "Host"
},
- "description": "Before set up, battery-powered devices must be woken up by pressing the button on the device."
+ "description": "Before set up, battery-powered devices must be woken up, you can now wake the device up using a button on it."
}
}
},
"device_automation": {
"trigger_subtype": {
"button": "Button",
- "button1": "First button",
+ "button1": "First button",
"button2": "Second button",
"button3": "Third button"
},
- "trigger_type": {
- "single": "{subtype} single clicked",
- "double": "{subtype} double clicked",
- "triple": "{subtype} triple clicked",
- "long":" {subtype} long clicked",
- "single_long": "{subtype} single clicked and then long clicked",
- "long_single": "{subtype} long clicked and then single clicked"
+ "trigger_type": {
+ "double": "{subtype} double clicked",
+ "long": " {subtype} long clicked",
+ "long_single": "{subtype} long clicked and then single clicked",
+ "single": "{subtype} single clicked",
+ "single_long": "{subtype} single clicked and then long clicked",
+ "triple": "{subtype} triple clicked"
}
}
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json
index 38c72f21dca04b..09cc3f51378445 100644
--- a/homeassistant/components/shelly/translations/es.json
+++ b/homeassistant/components/shelly/translations/es.json
@@ -27,5 +27,21 @@
"description": "Antes de configurarlo, el dispositivo que funciona con bater\u00eda debe despertarse presionando el bot\u00f3n del dispositivo."
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "Bot\u00f3n",
+ "button1": "Primer bot\u00f3n",
+ "button2": "Segundo bot\u00f3n",
+ "button3": "Tercer bot\u00f3n"
+ },
+ "trigger_type": {
+ "double": "Pulsaci\u00f3n doble de {subtype}",
+ "long": "Pulsaci\u00f3n larga de {subtype}",
+ "long_single": "Pulsaci\u00f3n larga de {subtype} seguida de una pulsaci\u00f3n simple",
+ "single": "Pulsaci\u00f3n simple de {subtype}",
+ "single_long": "Pulsaci\u00f3n simple de {subtype} seguida de una pulsaci\u00f3n larga",
+ "triple": "Pulsaci\u00f3n triple de {subtype}"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/et.json b/homeassistant/components/shelly/translations/et.json
index 12a662f6560a76..7059ce6b3d3f27 100644
--- a/homeassistant/components/shelly/translations/et.json
+++ b/homeassistant/components/shelly/translations/et.json
@@ -12,7 +12,7 @@
"flow_title": "",
"step": {
"confirm_discovery": {
- "description": "Kas soovid seadistada {model} saidil {host} ?\n\n Enne seadistamist tuleb akutoitega seade \u00e4ratada vajutades seadmel nuppu."
+ "description": "Kas soovid seadistada seadet {model} saidil {host} ? \n\n Enne seadistamise j\u00e4tkamist tuleb parooliga kaitstud akutoitega seadmed \u00e4ratada.\n Patareitoitega seadmed, mis pole parooliga kaitstud, lisatakse seadme \u00e4rkamisel. N\u00fc\u00fcd saad seadme k\u00e4sitsi \u00fcles \u00e4ratada, kasutades sellel olevat nuppu v\u00f5i oodata seadme j\u00e4rgmist andmete v\u00e4rskendamist."
},
"credentials": {
"data": {
@@ -27,5 +27,21 @@
"description": "Enne seadistamist tuleb akutoitega seade \u00e4ratada vajutades seadme nuppu."
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "Nupp",
+ "button1": "Esimene nupp",
+ "button2": "Teine nupp",
+ "button3": "Kolmas nupp"
+ },
+ "trigger_type": {
+ "double": "Nuppu {subtype} topeltkl\u00f5psati",
+ "long": "Nuppu \"{subtype}\" hoiti all",
+ "long_single": "Nuppu {subtype} hoiti all ja seej\u00e4rel kl\u00f5psati",
+ "single": "Nuppu {subtype} kl\u00f5psati",
+ "single_long": "Nuppu {subtype} kl\u00f5psati \u00fcks kord ja seej\u00e4rel hoiti all",
+ "triple": "Nuppu {subtype} kl\u00f5psati kolm korda"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/fr.json b/homeassistant/components/shelly/translations/fr.json
index e40da9f5e681fa..e4bdc99db1ec32 100644
--- a/homeassistant/components/shelly/translations/fr.json
+++ b/homeassistant/components/shelly/translations/fr.json
@@ -12,7 +12,7 @@
"flow_title": "Shelly: {name}",
"step": {
"confirm_discovery": {
- "description": "Voulez-vous configurer le {model} \u00e0 {host}?"
+ "description": "Voulez-vous configurer le {model} \u00e0 {host}?\n\nLes appareils aliment\u00e9s par batterie prot\u00e9g\u00e9s par mot de passe doivent \u00eatre r\u00e9veill\u00e9s avant de continuer \u00e0 s\u2019installer.\nLes appareils aliment\u00e9s par batterie qui ne sont pas prot\u00e9g\u00e9s par mot de passe seront ajout\u00e9s lorsque l\u2019appareil se r\u00e9veillera, vous pouvez maintenant r\u00e9veiller manuellement l\u2019appareil \u00e0 l\u2019aide d\u2019un bouton dessus ou attendre la prochaine mise \u00e0 jour des donn\u00e9es de l\u2019appareil."
},
"credentials": {
"data": {
@@ -24,8 +24,24 @@
"data": {
"host": "H\u00f4te"
},
- "description": "Avant la configuration, l'appareil aliment\u00e9 par batterie doit \u00eatre r\u00e9veill\u00e9 en appuyant sur le bouton de l'appareil."
+ "description": "Avant la configuration, les appareils aliment\u00e9s par batterie doivent \u00eatre r\u00e9veill\u00e9s, vous pouvez maintenant r\u00e9veiller l'appareil \u00e0 l'aide d'un bouton dessus."
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "Bouton",
+ "button1": "Premier bouton",
+ "button2": "Deuxi\u00e8me bouton",
+ "button3": "Troisi\u00e8me bouton"
+ },
+ "trigger_type": {
+ "double": "{subtype} double-cliqu\u00e9",
+ "long": " {sous-type} long cliqu\u00e9",
+ "long_single": "{subtype} clic long et simple clic",
+ "single": "{subtype} simple clic",
+ "single_long": "{subtype} simple clic, puis un clic long",
+ "triple": "{subtype} cliqu\u00e9 trois fois"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/hu.json b/homeassistant/components/shelly/translations/hu.json
index 3b2d79a34a77e2..2c8f468aaed7f8 100644
--- a/homeassistant/components/shelly/translations/hu.json
+++ b/homeassistant/components/shelly/translations/hu.json
@@ -1,7 +1,44 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "unsupported_firmware": "Az eszk\u00f6z nem t\u00e1mogatott firmware verzi\u00f3t haszn\u00e1l."
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "credentials": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Hoszt"
+ },
+ "description": "A be\u00e1ll\u00edt\u00e1s el\u0151tt az akkumul\u00e1toros eszk\u00f6z\u00f6ket fel kell \u00e9breszteni, most egy rajta l\u00e9v\u0151 gombbal fel\u00e9bresztheted az eszk\u00f6zt."
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "Gomb",
+ "button1": "Els\u0151 gomb",
+ "button2": "M\u00e1sodik gomb",
+ "button3": "Harmadik gomb"
+ },
+ "trigger_type": {
+ "double": "{subtype} dupla kattint\u00e1s",
+ "long": "{subtype} hosszan nyomva",
+ "long_single": "{subtype} hosszan nyomva, majd egy kattint\u00e1s",
+ "single": "{subtype} egy kattint\u00e1s",
+ "single_long": "{subtype} egy kattint\u00e1s, majd hosszan nyomva",
+ "triple": "{subtype} tripla kattint\u00e1s"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/id.json b/homeassistant/components/shelly/translations/id.json
new file mode 100644
index 00000000000000..606ee473805dad
--- /dev/null
+++ b/homeassistant/components/shelly/translations/id.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "unsupported_firmware": "Perangkat menggunakan versi firmware yang tidak didukung."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "confirm_discovery": {
+ "description": "Ingin menyiapkan {model} di {host}?\n\nPerangkat bertenaga baterai yang dilindungi kata sandi harus dibangunkan sebelum melanjutkan penyiapan.\nPerangkat bertenaga baterai yang tidak dilindungi kata sandi akan ditambahkan ketika perangkat bangun. Anda dapat membangunkan perangkat secara manual menggunakan tombol di atasnya sekarang juga atau menunggu pembaruan data berikutnya dari perangkat."
+ },
+ "credentials": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Sebelum menyiapkan, perangkat bertenaga baterai harus dibangunkan. Anda dapat membangunkan perangkat menggunakan tombol di atasnya sekarang."
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "Tombol",
+ "button1": "Tombol pertama",
+ "button2": "Tombol kedua",
+ "button3": "Tombol ketiga"
+ },
+ "trigger_type": {
+ "double": "{subtype} diklik dua kali",
+ "long": "{subtype} diklik lama",
+ "long_single": "{subtype} diklik lama kemudian diklik sekali",
+ "single": "{subtype} diklik sekali",
+ "single_long": "{subtype} diklik sekali kemudian diklik lama",
+ "triple": "{subtype} diklik tiga kali"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/it.json b/homeassistant/components/shelly/translations/it.json
index 61f2f8ccd09661..051cf88dc38ef8 100644
--- a/homeassistant/components/shelly/translations/it.json
+++ b/homeassistant/components/shelly/translations/it.json
@@ -12,7 +12,7 @@
"flow_title": "{name}",
"step": {
"confirm_discovery": {
- "description": "Vuoi impostare {model} su {host} ?\n\n Prima della configurazione, i dispositivi alimentati a batteria devono essere riattivati premendo il pulsante sul dispositivo."
+ "description": "Vuoi impostare {model} su {host}? \n\nI dispositivi alimentati a batteria protetti da password devono essere riattivati prima di continuare con la configurazione.\nI dispositivi alimentati a batteria che non sono protetti da password verranno aggiunti quando il dispositivo si riattiver\u00e0, ora puoi riattivare manualmente il dispositivo utilizzando un pulsante su di esso o attendere il prossimo aggiornamento dei dati dal dispositivo."
},
"credentials": {
"data": {
@@ -24,8 +24,24 @@
"data": {
"host": "Host"
},
- "description": "Prima della configurazione, i dispositivi alimentati a batteria devono essere riattivati premendo il pulsante sul dispositivo."
+ "description": "Prima della configurazione, i dispositivi alimentati a batteria devono essere riattivati, ora puoi riattivare il dispositivo utilizzando un pulsante su di esso."
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "Pulsante",
+ "button1": "Primo pulsante",
+ "button2": "Secondo pulsante",
+ "button3": "Terzo pulsante"
+ },
+ "trigger_type": {
+ "double": "{subtype} premuto due volte",
+ "long": "{subtype} premuto a lungo",
+ "long_single": "{subtype} premuto a lungo e poi singolarmente",
+ "single": "{subtype} premuto singolarmente",
+ "single_long": "{subtype} premuto singolarmente e poi a lungo",
+ "triple": "{subtype} premuto tre volte"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/ko.json b/homeassistant/components/shelly/translations/ko.json
index 5fb84e0ac90486..d4af42485788cf 100644
--- a/homeassistant/components/shelly/translations/ko.json
+++ b/homeassistant/components/shelly/translations/ko.json
@@ -1,18 +1,47 @@
{
"config": {
"abort": {
- "unsupported_firmware": "\uc774 \uc7a5\uce58\ub294 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \ud38c\uc6e8\uc5b4 \ubc84\uc804\uc744 \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unsupported_firmware": "\uae30\uae30\uac00 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \ud38c\uc6e8\uc5b4 \ubc84\uc804\uc744 \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4."
},
"error": {
- "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d"
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
+ "flow_title": "{name}",
"step": {
+ "confirm_discovery": {
+ "description": "{host}\uc5d0\uc11c {model}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?\n\n\ube44\ubc00\ubc88\ud638\ub85c \ubcf4\ud638\ub41c \ubc30\ud130\ub9ac \ubc29\uc2dd \uae30\uae30\ub294 \uc124\uc815\ud558\uae30 \uc804\uc5d0 \uc808\uc804 \ubaa8\ub4dc\ub97c \ud574\uc81c\ud574\uc57c \ud569\ub2c8\ub2e4.\n\ube44\ubc00\ubc88\ud638\ub85c \ubcf4\ud638\ub418\uc9c0 \uc54a\ub294 \ubc30\ud130\ub9ac \ubc29\uc2dd \uae30\uae30\ub294 \uae30\uae30\uc758 \uc808\uc804 \ubaa8\ub4dc\uac00 \ud574\uc81c\ub420 \ub54c \ucd94\uac00\ub418\uba70, \uae30\uae30\uc758 \ubc84\ud2bc\uc744 \uc0ac\uc6a9\ud558\uc5ec \uc218\ub3d9\uc73c\ub85c \uae30\uae30\ub97c \uc808\uc804 \ud574\uc81c\uc2dc\ud0a4\uac70\ub098 \uae30\uae30\uc5d0\uc11c \ub2e4\uc74c \ub370\uc774\ud130\ub97c \uc5c5\ub370\uc774\ud2b8\ud560 \ub54c\uae4c\uc9c0 \uae30\ub2e4\ub9b4 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
"credentials": {
"data": {
- "password": "\uc554\ud638",
- "username": "\uc0ac\uc6a9\uc790\uba85"
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
}
+ },
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8"
+ },
+ "description": "\uc124\uc815\ud558\uae30 \uc804\uc5d0 \ubc30\ud130\ub9ac\ub85c \uc791\ub3d9\ub418\ub294 \uae30\uae30\ub294 \uc808\uc804 \ubaa8\ub4dc\uac00 \ud574\uc81c\ub418\uc5b4 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uae30\uae30\uc758 \ubc84\ud2bc\uc744 \uc0ac\uc6a9\ud558\uc5ec \uc808\uc804 \ubaa8\ub4dc\ub97c \ud574\uc81c\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "\ubc84\ud2bc",
+ "button1": "\uccab \ubc88\uc9f8 \ubc84\ud2bc",
+ "button2": "\ub450 \ubc88\uc9f8 \ubc84\ud2bc",
+ "button3": "\uc138 \ubc88\uc9f8 \ubc84\ud2bc"
+ },
+ "trigger_type": {
+ "double": "\"{subtype}\"\uc774(\uac00) \ub450 \ubc88 \ub20c\ub838\uc744 \ub54c",
+ "long": "\"{subtype}\"\uc774(\uac00) \uae38\uac8c \ub20c\ub838\uc744 \ub54c",
+ "long_single": "\"{subtype}\"\uc774(\uac00) \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc9e7\uac8c \ub20c\ub838\uc744 \ub54c",
+ "single": "\"{subtype}\"\uc774(\uac00) \uc9e7\uac8c \ub20c\ub838\uc744 \ub54c",
+ "single_long": "\"{subtype}\"\uc774(\uac00) \uc9e7\uac8c \ub20c\ub838\ub2e4\uac00 \uae38\uac8c \ub20c\ub838\uc744 \ub54c",
+ "triple": "\"{subtype}\"\uc774(\uac00) \uc138 \ubc88 \ub20c\ub838\uc744 \ub54c"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/lb.json b/homeassistant/components/shelly/translations/lb.json
index b358c1c728269f..e6c5d8330c674e 100644
--- a/homeassistant/components/shelly/translations/lb.json
+++ b/homeassistant/components/shelly/translations/lb.json
@@ -27,5 +27,13 @@
"description": "Virum ariichten muss dat Batterie bedriwwen Ger\u00e4t aktiv\u00e9iert ginn andeems de Kn\u00e4ppchen um Apparat gedr\u00e9ckt g\u00ebtt."
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "Kn\u00e4ppchen",
+ "button1": "\u00c9ischte Kn\u00e4ppchen",
+ "button2": "Zweete Kn\u00e4ppchen",
+ "button3": "Dr\u00ebtte Kn\u00e4ppchen"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/nl.json b/homeassistant/components/shelly/translations/nl.json
index 75a2d2771d668b..571fb9b4339752 100644
--- a/homeassistant/components/shelly/translations/nl.json
+++ b/homeassistant/components/shelly/translations/nl.json
@@ -1,14 +1,15 @@
{
"config": {
"abort": {
- "already_configured": "Apparaat is al geconfigureerd"
+ "already_configured": "Apparaat is al geconfigureerd",
+ "unsupported_firmware": "Het apparaat gebruikt een niet-ondersteunde firmwareversie."
},
"error": {
"cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
- "flow_title": "Shelly: {name}",
+ "flow_title": "{name}",
"step": {
"confirm_discovery": {
"description": "Wilt u het {model} bij {host} instellen? Voordat apparaten op batterijen kunnen worden ingesteld, moet het worden gewekt door op de knop op het apparaat te drukken."
@@ -16,14 +17,31 @@
"credentials": {
"data": {
"password": "Wachtwoord",
- "username": "Benutzername"
+ "username": "Gebruikersnaam"
}
},
"user": {
"data": {
"host": "Host"
- }
+ },
+ "description": "Slapende (op batterij werkende) apparaten moeten wakker zijn wanneer deze apparaten opgezet worden. Je kunt deze apparaten nu wakker maken door op de knop erop te drukken"
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "Knop",
+ "button1": "Eerste knop",
+ "button2": "Tweede knop",
+ "button3": "Derde knop"
+ },
+ "trigger_type": {
+ "double": "{subtype} dubbel geklikt",
+ "long": "{subtype} lang geklikt",
+ "long_single": "{subtype} lang geklikt en daarna \u00e9\u00e9n keer geklikt",
+ "single": "{subtype} enkel geklikt",
+ "single_long": "{subtype} een keer geklikt en daarna lang geklikt",
+ "triple": "{subtype} driemaal geklikt"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/no.json b/homeassistant/components/shelly/translations/no.json
index 705c494a4c11b3..90cfe3ca906581 100644
--- a/homeassistant/components/shelly/translations/no.json
+++ b/homeassistant/components/shelly/translations/no.json
@@ -12,7 +12,7 @@
"flow_title": "{name}",
"step": {
"confirm_discovery": {
- "description": "Vil du konfigurere {model} p\u00e5 {host} ?\n\n F\u00f8r du setter opp, m\u00e5 batteridrevne enheter vekkes ved \u00e5 trykke p\u00e5 knappen p\u00e5 enheten."
+ "description": "Vil du konfigurere {model} p\u00e5 {host} ? \n\n Batteridrevne enheter som er passordbeskyttet, m\u00e5 vekkes f\u00f8r du fortsetter med konfigurasjonen.\n Batteridrevne enheter som ikke er passordbeskyttet, blir lagt til n\u00e5r enheten v\u00e5kner, du kan n\u00e5 vekke enheten manuelt med en knapp p\u00e5 den eller vente p\u00e5 neste dataoppdatering fra enheten."
},
"credentials": {
"data": {
@@ -24,8 +24,24 @@
"data": {
"host": "Vert"
},
- "description": "F\u00f8r du setter opp, m\u00e5 batteridrevne enheter vekkes ved \u00e5 trykke p\u00e5 knappen p\u00e5 enheten."
+ "description": "F\u00f8r du setter opp, m\u00e5 batteridrevne enheter vekkes, du kan n\u00e5 vekke enheten med en knapp p\u00e5 den."
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "Knapp",
+ "button1": "F\u00f8rste knapp",
+ "button2": "Andre knapp",
+ "button3": "Tredje knapp"
+ },
+ "trigger_type": {
+ "double": "{subtype} dobbeltklikket",
+ "long": "{subtype} lenge klikket",
+ "long_single": "{subtype} lengre klikk og deretter et enkeltklikk",
+ "single": "{subtype} enkeltklikket",
+ "single_long": "{subtype} enkeltklikket og deretter et lengre klikk",
+ "triple": "{subtype} trippelklikket"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json
index ebf6041d4bac8f..b0c4dd11b1b12b 100644
--- a/homeassistant/components/shelly/translations/pl.json
+++ b/homeassistant/components/shelly/translations/pl.json
@@ -12,7 +12,7 @@
"flow_title": "{name}",
"step": {
"confirm_discovery": {
- "description": "Czy chcesz skonfigurowa\u0107 {model} ({host})?\n\nPrzed skonfigurowaniem urz\u0105dzenia zasilane bateryjnie nale\u017cy, wybudzi\u0107 naciskaj\u0105c przycisk na urz\u0105dzeniu."
+ "description": "Czy chcesz skonfigurowa\u0107 {model} ({host})?\n\nUrz\u0105dzenia zasilane bateryjnie, z ustawionym has\u0142em, nale\u017cy wybudzi\u0107 przed konfiguracj\u0105.\nUrz\u0105dzenia zasilane bateryjnie, bez ustawionego has\u0142a, zostan\u0105 dodane, gdy si\u0119 wybudz\u0105. Mo\u017cesz r\u0119cznie wybudzi\u0107 urz\u0105dzenie przyciskiem na obudowie lub poczeka\u0107 na aktualizacj\u0119 danych z urz\u0105dzenia."
},
"credentials": {
"data": {
@@ -24,8 +24,24 @@
"data": {
"host": "Nazwa hosta lub adres IP"
},
- "description": "Przed skonfigurowaniem urz\u0105dzenia zasilane bateryjnie nale\u017cy, wybudzi\u0107 naciskaj\u0105c przycisk na urz\u0105dzeniu."
+ "description": "Przed skonfigurowaniem urz\u0105dzenia zasilane bateryjnie nale\u017cy, wybudzi\u0107 naciskaj\u0105c przycisk na obudowie."
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "przycisk",
+ "button1": "pierwszy",
+ "button2": "drugi",
+ "button3": "trzeci"
+ },
+ "trigger_type": {
+ "double": "przycisk \"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty",
+ "long": "przycisk \"{subtype}\" zostanie d\u0142ugo naci\u015bni\u0119ty",
+ "long_single": "przycisk \"{subtype}\" zostanie d\u0142ugo naci\u015bni\u0119ty, a nast\u0119pnie pojedynczo naci\u015bni\u0119ty",
+ "single": "przycisk \"{subtype}\" zostanie pojedynczo naci\u015bni\u0119ty",
+ "single_long": "przycisk \"{subtype}\" pojedynczo naci\u015bni\u0119ty, a nast\u0119pnie d\u0142ugo naci\u015bni\u0119ty",
+ "triple": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/ru.json b/homeassistant/components/shelly/translations/ru.json
index 508a189b849771..9996e347e96a91 100644
--- a/homeassistant/components/shelly/translations/ru.json
+++ b/homeassistant/components/shelly/translations/ru.json
@@ -6,18 +6,18 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"flow_title": "{name}",
"step": {
"confirm_discovery": {
- "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 {model} ({host}) ?\n\n\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0449\u0438\u0435 \u043e\u0442 \u0431\u0430\u0442\u0430\u0440\u0435\u0438, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u0432\u0435\u0441\u0442\u0438 \u0438\u0437 \u0441\u043f\u044f\u0449\u0435\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0430, \u043d\u0430\u0436\u0430\u0432 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435."
+ "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 {model} ({host})?\n\n\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0442 \u043e\u0442 \u0431\u0430\u0442\u0430\u0440\u0435\u0438 \u0438 \u0437\u0430\u0449\u0438\u0449\u0435\u043d\u044b \u043f\u0430\u0440\u043e\u043b\u0435\u043c, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442\u044c, \u043f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443.\n\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0442 \u043e\u0442 \u0431\u0430\u0442\u0430\u0440\u0435\u0438 \u0438 \u043d\u0435 \u0437\u0430\u0449\u0438\u0449\u0435\u043d\u044b \u043f\u0430\u0440\u043e\u043b\u0435\u043c, \u0431\u0443\u0434\u0443\u0442 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u044b, \u043a\u043e\u0433\u0434\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u044b\u0439\u0434\u0435\u0442 \u0438\u0437 \u0441\u043f\u044f\u0449\u0435\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0430. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0436\u0430\u0442\u044c \u043d\u0430 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435, \u0442\u0435\u043c \u0441\u0430\u043c\u044b\u043c \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0432 \u0435\u0433\u043e, \u043b\u0438\u0431\u043e \u0434\u043e\u0436\u0434\u0430\u0442\u044c\u0441\u044f \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0433\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430."
},
"credentials": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
}
},
"user": {
@@ -27,5 +27,21 @@
"description": "\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0449\u0438\u0435 \u043e\u0442 \u0431\u0430\u0442\u0430\u0440\u0435\u0438, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u0432\u0435\u0441\u0442\u0438 \u0438\u0437 \u0441\u043f\u044f\u0449\u0435\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0430, \u043d\u0430\u0436\u0430\u0432 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435."
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "\u041a\u043d\u043e\u043f\u043a\u0430",
+ "button1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430"
+ },
+ "trigger_type": {
+ "double": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430",
+ "long": "{subtype} \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430",
+ "long_single": "{subtype} \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430 \u0438 \u0437\u0430\u0442\u0435\u043c \u043d\u0430\u0436\u0430\u0442\u0430 \u043e\u0434\u0438\u043d \u0440\u0430\u0437",
+ "single": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u043e\u0434\u0438\u043d \u0440\u0430\u0437",
+ "single_long": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u0438 \u0437\u0430\u0442\u0435\u043c \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430",
+ "triple": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/tr.json b/homeassistant/components/shelly/translations/tr.json
new file mode 100644
index 00000000000000..f577c73787f521
--- /dev/null
+++ b/homeassistant/components/shelly/translations/tr.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "credentials": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ }
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "D\u00fc\u011fme",
+ "button1": "\u0130lk d\u00fc\u011fme",
+ "button2": "\u0130kinci d\u00fc\u011fme",
+ "button3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme"
+ },
+ "trigger_type": {
+ "double": "{subtype} \u00e7ift t\u0131kland\u0131",
+ "long": "{subtype} uzun t\u0131kland\u0131",
+ "long_single": "{subtype} uzun t\u0131kland\u0131 ve ard\u0131ndan tek t\u0131kland\u0131",
+ "single": "{subtype} tek t\u0131kland\u0131",
+ "triple": "{subtype} \u00fc\u00e7 kez t\u0131kland\u0131"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/uk.json b/homeassistant/components/shelly/translations/uk.json
new file mode 100644
index 00000000000000..7ad70b0f0da915
--- /dev/null
+++ b/homeassistant/components/shelly/translations/uk.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "unsupported_firmware": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454 \u043d\u0435\u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0443 \u0432\u0435\u0440\u0441\u0456\u044e \u043c\u0456\u043a\u0440\u043e\u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0438."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "flow_title": "{name}",
+ "step": {
+ "confirm_discovery": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 {model} ({host})? \n\n\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457, \u0449\u043e \u043f\u0440\u0430\u0446\u044e\u044e\u0442\u044c \u0432\u0456\u0434 \u0431\u0430\u0442\u0430\u0440\u0435\u0457, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u0438\u0432\u0435\u0441\u0442\u0438 \u0437\u0456 \u0441\u043f\u043b\u044f\u0447\u043e\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0443, \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0432\u0448\u0438 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457."
+ },
+ "credentials": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "description": "\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457, \u0449\u043e \u043f\u0440\u0430\u0446\u044e\u044e\u0442\u044c \u0432\u0456\u0434 \u0431\u0430\u0442\u0430\u0440\u0435\u0457, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u0438\u0432\u0435\u0441\u0442\u0438 \u0437\u0456 \u0441\u043f\u043b\u044f\u0447\u043e\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0443, \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0432\u0448\u0438 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457."
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "\u041a\u043d\u043e\u043f\u043a\u0430",
+ "button1": "\u041f\u0435\u0440\u0448\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button2": "\u0414\u0440\u0443\u0433\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button3": "\u0422\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0430"
+ },
+ "trigger_type": {
+ "double": "{subtype} \u043f\u043e\u0434\u0432\u0456\u0439\u043d\u0438\u0439 \u043a\u043b\u0456\u043a",
+ "long": "{subtype} \u0434\u043e\u0432\u0433\u0438\u0439 \u043a\u043b\u0456\u043a",
+ "long_single": "{subtype} \u0434\u043e\u0432\u0433\u0438\u0439 \u043a\u043b\u0456\u043a, \u0430 \u043f\u043e\u0442\u0456\u043c \u043e\u0434\u0438\u043d \u043a\u043b\u0456\u043a",
+ "single": "{subtype} \u043e\u0434\u0438\u043d\u0430\u0440\u043d\u0438\u0439 \u043a\u043b\u0456\u043a",
+ "single_long": "{subtype} \u043e\u0434\u0438\u043d\u0430\u0440\u043d\u0438\u0439 \u043a\u043b\u0456\u043a, \u043f\u043e\u0442\u0456\u043c \u0434\u043e\u0432\u0433\u0438\u0439 \u043a\u043b\u0456\u043a",
+ "triple": "{subtype} \u043f\u043e\u0442\u0440\u0456\u0439\u043d\u0438\u0439 \u043a\u043b\u0456\u043a"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/zh-Hant.json b/homeassistant/components/shelly/translations/zh-Hant.json
index bf0150523b3e39..abc0b627423e9b 100644
--- a/homeassistant/components/shelly/translations/zh-Hant.json
+++ b/homeassistant/components/shelly/translations/zh-Hant.json
@@ -12,7 +12,7 @@
"flow_title": "{name}",
"step": {
"confirm_discovery": {
- "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4f4d\u65bc {host} \u7684 {model}\uff1f\n\n\u958b\u59cb\u8a2d\u5b9a\u524d\uff0c\u5fc5\u9808\u6309\u4e0b\u88dd\u7f6e\u4e0a\u7684\u6309\u9215\u4ee5\u559a\u9192\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\u3002"
+ "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4f4d\u65bc {host} \u7684 {model}\uff1f\n\n\u958b\u59cb\u8a2d\u5b9a\u524d\uff0c\u5fc5\u9808\u6309\u4e0b\u88dd\u7f6e\u4e0a\u7684\u6309\u9215\u4ee5\u559a\u9192\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\u3002\n\u4e0d\u5177\u5bc6\u78bc\u4fdd\u8b77\u7684\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\uff0c\u53ef\u4ee5\u65bc\u559a\u9192\u5f8c\u65b0\u589e\u3002\u53ef\u4ee5\u4f7f\u7528\u88dd\u7f6e\u4e0a\u7684\u6309\u9215\u6216\u7b49\u5f85\u88dd\u7f6e\u4e0b\u4e00\u6b21\u8cc7\u6599\u66f4\u65b0\u6642\u9032\u884c\u624b\u52d5\u559a\u9192\u3002"
},
"credentials": {
"data": {
@@ -27,5 +27,21 @@
"description": "\u958b\u59cb\u8a2d\u5b9a\u524d\uff0c\u5fc5\u9808\u6309\u4e0b\u88dd\u7f6e\u4e0a\u7684\u6309\u9215\u4ee5\u559a\u9192\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\u3002"
}
}
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "button": "\u6309\u9215",
+ "button1": "\u7b2c\u4e00\u500b\u6309\u9215",
+ "button2": "\u7b2c\u4e8c\u500b\u6309\u9215",
+ "button3": "\u7b2c\u4e09\u500b\u6309\u9215"
+ },
+ "trigger_type": {
+ "double": "{subtype} \u96d9\u64ca",
+ "long": "{subtype} \u9577\u6309",
+ "long_single": "{subtype} \u9577\u6309\u5f8c\u55ae\u64ca",
+ "single": "{subtype} \u55ae\u64ca",
+ "single_long": "{subtype} \u55ae\u64ca\u5f8c\u9577\u6309",
+ "triple": "{subtype} \u4e09\u9023\u64ca"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py
index 97d8bda609bc4a..126491f65c1303 100644
--- a/homeassistant/components/shelly/utils.py
+++ b/homeassistant/components/shelly/utils.py
@@ -1,13 +1,14 @@
"""Shelly helpers functions."""
+from __future__ import annotations
from datetime import timedelta
import logging
-from typing import List, Optional, Tuple
import aioshelly
-from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
-from homeassistant.core import HomeAssistant
+from homeassistant.const import EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers import singleton
from homeassistant.util.dt import parse_datetime, utcnow
from .const import (
@@ -66,7 +67,7 @@ def get_number_of_channels(device: aioshelly.Device, block: aioshelly.Block) ->
def get_entity_name(
device: aioshelly.Device,
block: aioshelly.Block,
- description: Optional[str] = None,
+ description: str | None = None,
) -> str:
"""Naming for switch and sensors."""
channel_name = get_device_channel_name(device, block)
@@ -110,7 +111,7 @@ def get_device_channel_name(
def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool:
"""Return true if input button settings is set to a momentary type."""
# Shelly Button type is fixed to momentary and no btn_type
- if settings["device"]["type"] == "SHBTN-1":
+ if settings["device"]["type"] in ("SHBTN-1", "SHBTN-2"):
return True
button = settings.get("relays") or settings.get("lights") or settings.get("inputs")
@@ -142,7 +143,7 @@ def get_device_uptime(status: dict, last_uptime: str) -> str:
def get_input_triggers(
device: aioshelly.Device, block: aioshelly.Block
-) -> List[Tuple[str, str]]:
+) -> list[tuple[str, str]]:
"""Return list of input triggers for block."""
if "inputEvent" not in block.sensor_ids or "inputEventCnt" not in block.sensor_ids:
return []
@@ -157,7 +158,7 @@ def get_input_triggers(
else:
subtype = f"button{int(block.channel)+1}"
- if device.settings["device"]["type"] == "SHBTN-1":
+ if device.settings["device"]["type"] in ("SHBTN-1", "SHBTN-2"):
trigger_types = SHBTN_1_INPUTS_EVENTS_TYPES
elif device.settings["device"]["type"] == "SHIX3-1":
trigger_types = SHIX3_1_INPUTS_EVENTS_TYPES
@@ -176,9 +177,36 @@ def get_device_wrapper(hass: HomeAssistant, device_id: str):
return None
for config_entry in hass.data[DOMAIN][DATA_CONFIG_ENTRY]:
- wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry][COAP]
+ wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get(COAP)
- if wrapper.device_id == device_id:
+ if wrapper and wrapper.device_id == device_id:
return wrapper
return None
+
+
+@singleton.singleton("shelly_coap")
+async def get_coap_context(hass):
+ """Get CoAP context to be used in all Shelly devices."""
+ context = aioshelly.COAP()
+ await context.initialize()
+
+ @callback
+ def shutdown_listener(ev):
+ context.close()
+
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_listener)
+
+ return context
+
+
+def get_device_sleep_period(settings: dict) -> int:
+ """Return the device sleep period in seconds or 0 for non sleeping devices."""
+ sleep_period = 0
+
+ if settings.get("sleep_mode", False):
+ sleep_period = settings["sleep_mode"]["period"]
+ if settings["sleep_mode"]["unit"] == "h":
+ sleep_period *= 60 # hours to minutes
+
+ return sleep_period * 60 # minutes to seconds
diff --git a/homeassistant/components/shodan/sensor.py b/homeassistant/components/shodan/sensor.py
index d2a6a28fbe4004..fa0fc2d3906874 100644
--- a/homeassistant/components/shodan/sensor.py
+++ b/homeassistant/components/shodan/sensor.py
@@ -5,10 +5,9 @@
import shodan
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -47,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([ShodanSensor(data, name)], True)
-class ShodanSensor(Entity):
+class ShodanSensor(SensorEntity):
"""Representation of the Shodan sensor."""
def __init__(self, data, name):
@@ -78,7 +77,7 @@ def icon(self):
return ICON
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py
index 1831f894cecab3..841865cd759b35 100644
--- a/homeassistant/components/shopping_list/__init__.py
+++ b/homeassistant/components/shopping_list/__init__.py
@@ -7,25 +7,28 @@
from homeassistant import config_entries
from homeassistant.components import http, websocket_api
from homeassistant.components.http.data_validator import RequestDataValidator
-from homeassistant.const import HTTP_BAD_REQUEST, HTTP_NOT_FOUND
+from homeassistant.const import ATTR_NAME, HTTP_BAD_REQUEST, HTTP_NOT_FOUND
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json, save_json
from .const import DOMAIN
-ATTR_NAME = "name"
+ATTR_COMPLETE = "complete"
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({DOMAIN: {}}, extra=vol.ALLOW_EXTRA)
EVENT = "shopping_list_updated"
-ITEM_UPDATE_SCHEMA = vol.Schema({"complete": bool, ATTR_NAME: str})
+ITEM_UPDATE_SCHEMA = vol.Schema({ATTR_COMPLETE: bool, ATTR_NAME: str})
PERSISTENCE = ".shopping_list.json"
SERVICE_ADD_ITEM = "add_item"
SERVICE_COMPLETE_ITEM = "complete_item"
-
+SERVICE_INCOMPLETE_ITEM = "incomplete_item"
+SERVICE_COMPLETE_ALL = "complete_all"
+SERVICE_INCOMPLETE_ALL = "incomplete_all"
SERVICE_ITEM_SCHEMA = vol.Schema({vol.Required(ATTR_NAME): vol.Any(None, cv.string)})
+SERVICE_LIST_SCHEMA = vol.Schema({})
WS_TYPE_SHOPPING_LIST_ITEMS = "shopping_list/items"
WS_TYPE_SHOPPING_LIST_ADD_ITEM = "shopping_list/items/add"
@@ -92,6 +95,27 @@ async def complete_item_service(call):
else:
await data.async_update(item["id"], {"name": name, "complete": True})
+ async def incomplete_item_service(call):
+ """Mark the item provided via `name` as incomplete."""
+ data = hass.data[DOMAIN]
+ name = call.data.get(ATTR_NAME)
+ if name is None:
+ return
+ try:
+ item = [item for item in data.items if item["name"] == name][0]
+ except IndexError:
+ _LOGGER.error("Restoring of item failed: %s cannot be found", name)
+ else:
+ await data.async_update(item["id"], {"name": name, "complete": False})
+
+ async def complete_all_service(call):
+ """Mark all items in the list as complete."""
+ await data.async_update_list({"complete": True})
+
+ async def incomplete_all_service(call):
+ """Mark all items in the list as incomplete."""
+ await data.async_update_list({"complete": False})
+
data = hass.data[DOMAIN] = ShoppingData(hass)
await data.async_load()
@@ -101,6 +125,24 @@ async def complete_item_service(call):
hass.services.async_register(
DOMAIN, SERVICE_COMPLETE_ITEM, complete_item_service, schema=SERVICE_ITEM_SCHEMA
)
+ hass.services.async_register(
+ DOMAIN,
+ SERVICE_INCOMPLETE_ITEM,
+ incomplete_item_service,
+ schema=SERVICE_ITEM_SCHEMA,
+ )
+ hass.services.async_register(
+ DOMAIN,
+ SERVICE_COMPLETE_ALL,
+ complete_all_service,
+ schema=SERVICE_LIST_SCHEMA,
+ )
+ hass.services.async_register(
+ DOMAIN,
+ SERVICE_INCOMPLETE_ALL,
+ incomplete_all_service,
+ schema=SERVICE_LIST_SCHEMA,
+ )
hass.http.register_view(ShoppingListView)
hass.http.register_view(CreateShoppingListItemView)
@@ -165,6 +207,13 @@ async def async_clear_completed(self):
self.items = [itm for itm in self.items if not itm["complete"]]
await self.hass.async_add_executor_job(self.save)
+ async def async_update_list(self, info):
+ """Update all items in the list."""
+ for item in self.items:
+ item.update(info)
+ await self.hass.async_add_executor_job(self.save)
+ return self.items
+
@callback
def async_reorder(self, item_ids):
"""Reorder items."""
diff --git a/homeassistant/components/shopping_list/services.yaml b/homeassistant/components/shopping_list/services.yaml
index 04457e2abec4e2..73540210232b8a 100644
--- a/homeassistant/components/shopping_list/services.yaml
+++ b/homeassistant/components/shopping_list/services.yaml
@@ -1,12 +1,36 @@
add_item:
- description: Adds an item to the shopping list.
+ name: Add item
+ description: Add an item to the shopping list.
fields:
name:
+ name: Name
description: The name of the item to add.
+ required: true
example: Beer
+ selector:
+ text:
+
complete_item:
- description: Marks an item as completed in the shopping list. It does not remove the item.
+ name: Complete item
+ description: Mark an item as completed in the shopping list.
fields:
name:
- description: The name of the item to mark as completed.
+ name: Name
+ description: The name of the item to mark as completed (without removing).
+ required: true
example: Beer
+ selector:
+ text:
+
+incomplete_item:
+ description: Marks an item as incomplete in the shopping list.
+ fields:
+ name:
+ description: The name of the item to mark as incomplete.
+ example: Beer
+
+complete_all:
+ description: Marks all items as completed in the shopping list. It does not remove the items.
+
+incomplete_all:
+ description: Marks all items as incomplete in the shopping list.
diff --git a/homeassistant/components/shopping_list/translations/de.json b/homeassistant/components/shopping_list/translations/de.json
index d2d6a42fe249a8..68372e9f4ac8df 100644
--- a/homeassistant/components/shopping_list/translations/de.json
+++ b/homeassistant/components/shopping_list/translations/de.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Die Einkaufsliste ist bereits konfiguriert."
+ "already_configured": "Der Dienst ist bereits konfiguriert"
},
"step": {
"user": {
diff --git a/homeassistant/components/shopping_list/translations/hu.json b/homeassistant/components/shopping_list/translations/hu.json
index 4a093bea379c31..5f092963da3d66 100644
--- a/homeassistant/components/shopping_list/translations/hu.json
+++ b/homeassistant/components/shopping_list/translations/hu.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "A bev\u00e1s\u00e1rl\u00f3lista m\u00e1r konfigur\u00e1lva van."
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
},
"step": {
"user": {
diff --git a/homeassistant/components/shopping_list/translations/id.json b/homeassistant/components/shopping_list/translations/id.json
new file mode 100644
index 00000000000000..0efa42d0782433
--- /dev/null
+++ b/homeassistant/components/shopping_list/translations/id.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi"
+ },
+ "step": {
+ "user": {
+ "description": "Ingin mengonfigurasi daftar belanja?",
+ "title": "Daftar Belanja"
+ }
+ }
+ },
+ "title": "Daftar Belanja"
+}
\ No newline at end of file
diff --git a/homeassistant/components/shopping_list/translations/ko.json b/homeassistant/components/shopping_list/translations/ko.json
index 247fa8d9f4d42e..a576567a87f3b4 100644
--- a/homeassistant/components/shopping_list/translations/ko.json
+++ b/homeassistant/components/shopping_list/translations/ko.json
@@ -1,14 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "\uc7a5\ubcf4\uae30\ubaa9\ub85d\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
- "description": "\uc7a5\ubcf4\uae30\ubaa9\ub85d\uc744 \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
- "title": "\uc7a5\ubcf4\uae30\ubaa9\ub85d"
+ "description": "\uc7a5\ubcf4\uae30 \ubaa9\ub85d\uc744 \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "\uc7a5\ubcf4\uae30 \ubaa9\ub85d"
}
}
},
- "title": "\uc7a5\ubcf4\uae30\ubaa9\ub85d"
+ "title": "\uc7a5\ubcf4\uae30 \ubaa9\ub85d"
}
\ No newline at end of file
diff --git a/homeassistant/components/shopping_list/translations/nl.json b/homeassistant/components/shopping_list/translations/nl.json
index de6045dd81bc29..e8de5fbae1dd42 100644
--- a/homeassistant/components/shopping_list/translations/nl.json
+++ b/homeassistant/components/shopping_list/translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "De Shopping List is al geconfigureerd."
+ "already_configured": "Service is al geconfigureerd"
},
"step": {
"user": {
diff --git a/homeassistant/components/shopping_list/translations/sv.json b/homeassistant/components/shopping_list/translations/sv.json
new file mode 100644
index 00000000000000..0202ae3a53fa23
--- /dev/null
+++ b/homeassistant/components/shopping_list/translations/sv.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Tj\u00e4nsten har redan konfigurerats"
+ },
+ "step": {
+ "user": {
+ "description": "Vill du konfigurera ink\u00f6pslistan?",
+ "title": "Ink\u00f6pslista"
+ }
+ }
+ },
+ "title": "Ink\u00f6pslista"
+}
\ No newline at end of file
diff --git a/homeassistant/components/shopping_list/translations/tr.json b/homeassistant/components/shopping_list/translations/tr.json
new file mode 100644
index 00000000000000..d139d2f6399971
--- /dev/null
+++ b/homeassistant/components/shopping_list/translations/tr.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "step": {
+ "user": {
+ "description": "Al\u0131\u015fveri\u015f listesini yap\u0131land\u0131rmak istiyor musunuz?",
+ "title": "Al\u0131\u015fveri\u015f listesi"
+ }
+ }
+ },
+ "title": "Al\u0131\u015fveri\u015f listesi"
+}
\ No newline at end of file
diff --git a/homeassistant/components/shopping_list/translations/uk.json b/homeassistant/components/shopping_list/translations/uk.json
new file mode 100644
index 00000000000000..b73bd6c702a048
--- /dev/null
+++ b/homeassistant/components/shopping_list/translations/uk.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant."
+ },
+ "step": {
+ "user": {
+ "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u043a\u0443\u043f\u043e\u043a?",
+ "title": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u043a\u0443\u043f\u043e\u043a"
+ }
+ }
+ },
+ "title": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u043a\u0443\u043f\u043e\u043a"
+}
\ No newline at end of file
diff --git a/homeassistant/components/sht31/sensor.py b/homeassistant/components/sht31/sensor.py
index 277039b3ba626a..fd5506ee513f32 100644
--- a/homeassistant/components/sht31/sensor.py
+++ b/homeassistant/components/sht31/sensor.py
@@ -7,7 +7,7 @@
from Adafruit_SHT31 import SHT31
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_MONITORED_CONDITIONS,
CONF_NAME,
@@ -16,7 +16,6 @@
TEMP_CELSIUS,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.temperature import display_temp
from homeassistant.util import Throttle
@@ -93,7 +92,7 @@ def update(self):
self.humidity = humidity
-class SHTSensor(Entity):
+class SHTSensor(SensorEntity):
"""An abstract SHTSensor, can be either temperature or humidity."""
def __init__(self, sensor, name):
diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py
index 1de3cfeb8a0913..75c2a4f0f63132 100644
--- a/homeassistant/components/sigfox/sensor.py
+++ b/homeassistant/components/sigfox/sensor.py
@@ -7,10 +7,9 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, HTTP_OK, HTTP_UNAUTHORIZED
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -109,7 +108,7 @@ def devices(self):
return self._devices
-class SigfoxDevice(Entity):
+class SigfoxDevice(SensorEntity):
"""Class for single sigfox device."""
def __init__(self, device_id, auth, name):
@@ -155,6 +154,6 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return other details about the last message."""
return self._message_data
diff --git a/homeassistant/components/sighthound/image_processing.py b/homeassistant/components/sighthound/image_processing.py
index e15fab1aaa3139..fa636eb757f47c 100644
--- a/homeassistant/components/sighthound/image_processing.py
+++ b/homeassistant/components/sighthound/image_processing.py
@@ -170,7 +170,7 @@ def unit_of_measurement(self):
return ATTR_PEOPLE
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the attributes."""
if not self._last_detection:
return {}
diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json
index 99902b8dd363cf..0cab5b45b848f1 100644
--- a/homeassistant/components/sighthound/manifest.json
+++ b/homeassistant/components/sighthound/manifest.json
@@ -2,6 +2,6 @@
"domain": "sighthound",
"name": "Sighthound",
"documentation": "https://www.home-assistant.io/integrations/sighthound",
- "requirements": ["pillow==8.1.0", "simplehound==0.3"],
+ "requirements": ["pillow==8.1.2", "simplehound==0.3"],
"codeowners": ["@robmarkcole"]
}
diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py
index 1d10153415741d..5a83dec69f0673 100644
--- a/homeassistant/components/simplepush/notify.py
+++ b/homeassistant/components/simplepush/notify.py
@@ -8,13 +8,12 @@
PLATFORM_SCHEMA,
BaseNotificationService,
)
-from homeassistant.const import CONF_PASSWORD
+from homeassistant.const import CONF_EVENT, CONF_PASSWORD
import homeassistant.helpers.config_validation as cv
ATTR_ENCRYPTED = "encrypted"
CONF_DEVICE_KEY = "device_key"
-CONF_EVENT = "event"
CONF_SALT = "salt"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
@@ -44,7 +43,6 @@ def __init__(self, config):
def send_message(self, message="", **kwargs):
"""Send a message to a Simplepush user."""
-
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
if self._password:
diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py
index 89f5c40b1ff74f..485284b32937bd 100644
--- a/homeassistant/components/simplisafe/__init__.py
+++ b/homeassistant/components/simplisafe/__init__.py
@@ -10,10 +10,10 @@
EVENT_CONNECTION_LOST,
EVENT_CONNECTION_RESTORED,
EVENT_DOORBELL_DETECTED,
- EVENT_ENTRY_DETECTED,
+ EVENT_ENTRY_DELAY,
EVENT_LOCK_LOCKED,
EVENT_LOCK_UNLOCKED,
- EVENT_MOTION_DETECTED,
+ EVENT_SECRET_ALERT_TRIGGERED,
)
import voluptuous as vol
@@ -71,7 +71,7 @@
DEFAULT_SOCKET_MIN_RETRY = 15
-SUPPORTED_PLATFORMS = (
+PLATFORMS = (
"alarm_control_panel",
"binary_sensor",
"lock",
@@ -82,8 +82,8 @@
WEBSOCKET_EVENTS_TO_TRIGGER_HASS_EVENT = [
EVENT_CAMERA_MOTION_DETECTED,
EVENT_DOORBELL_DETECTED,
- EVENT_ENTRY_DETECTED,
- EVENT_MOTION_DETECTED,
+ EVENT_ENTRY_DELAY,
+ EVENT_SECRET_ALERT_TRIGGERED,
]
ATTR_CATEGORY = "category"
@@ -219,7 +219,7 @@ async def async_setup_entry(hass, config_entry):
)
await simplisafe.async_init()
- for platform in SUPPORTED_PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
@@ -327,8 +327,8 @@ async def async_unload_entry(hass, entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in SUPPORTED_PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -634,7 +634,7 @@ def device_info(self):
return self._device_info
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attrs
diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py
index 7634f1cce86f8c..8f394890ad4eec 100644
--- a/homeassistant/components/simplisafe/alarm_control_panel.py
+++ b/homeassistant/components/simplisafe/alarm_control_panel.py
@@ -163,6 +163,7 @@ async def async_alarm_disarm(self, code=None):
return
self._state = STATE_ALARM_DISARMED
+ self.async_write_ha_state()
async def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
@@ -178,6 +179,7 @@ async def async_alarm_arm_home(self, code=None):
return
self._state = STATE_ALARM_ARMED_HOME
+ self.async_write_ha_state()
async def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
@@ -193,6 +195,7 @@ async def async_alarm_arm_away(self, code=None):
return
self._state = STATE_ALARM_ARMING
+ self.async_write_ha_state()
@callback
def async_update_from_rest_api(self):
diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py
index f17a2ce2e4cb6b..09e0b96e742109 100644
--- a/homeassistant/components/simplisafe/config_flow.py
+++ b/homeassistant/components/simplisafe/config_flow.py
@@ -13,7 +13,7 @@
from homeassistant.helpers import aiohttp_client
from . import async_get_client_id
-from .const import DOMAIN, LOGGER # pylint: disable=unused-import
+from .const import DOMAIN, LOGGER
FULL_DATA_SCHEMA = vol.Schema(
{
diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py
index 08ffb82d24fcae..a4d823efe386a6 100644
--- a/homeassistant/components/simplisafe/lock.py
+++ b/homeassistant/components/simplisafe/lock.py
@@ -55,6 +55,9 @@ async def async_lock(self, **kwargs):
LOGGER.error('Error while locking "%s": %s', self._lock.name, err)
return
+ self._is_locked = True
+ self.async_write_ha_state()
+
async def async_unlock(self, **kwargs):
"""Unlock the lock."""
try:
@@ -63,6 +66,9 @@ async def async_unlock(self, **kwargs):
LOGGER.error('Error while unlocking "%s": %s', self._lock.name, err)
return
+ self._is_locked = False
+ self.async_write_ha_state()
+
@callback
def async_update_from_rest_api(self):
"""Update the entity with the provided REST API data."""
diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json
index a502a7908f087c..45deb938b59c27 100644
--- a/homeassistant/components/simplisafe/manifest.json
+++ b/homeassistant/components/simplisafe/manifest.json
@@ -3,6 +3,6 @@
"name": "SimpliSafe",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
- "requirements": ["simplisafe-python==9.6.2"],
+ "requirements": ["simplisafe-python==9.6.9"],
"codeowners": ["@bachya"]
}
diff --git a/homeassistant/components/simplisafe/sensor.py b/homeassistant/components/simplisafe/sensor.py
index 7e927ee942e140..9f93a6f9e87a56 100644
--- a/homeassistant/components/simplisafe/sensor.py
+++ b/homeassistant/components/simplisafe/sensor.py
@@ -1,6 +1,7 @@
"""Support for SimpliSafe freeze sensor."""
from simplipy.entity import EntityTypes
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT
from homeassistant.core import callback
@@ -25,7 +26,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
async_add_entities(sensors)
-class SimplisafeFreezeSensor(SimpliSafeBaseSensor):
+class SimplisafeFreezeSensor(SimpliSafeBaseSensor, SensorEntity):
"""Define a SimpliSafe freeze sensor entity."""
def __init__(self, simplisafe, system, sensor):
diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json
index ab05cf649d8334..5914e8f680c83b 100644
--- a/homeassistant/components/simplisafe/translations/de.json
+++ b/homeassistant/components/simplisafe/translations/de.json
@@ -1,17 +1,21 @@
{
"config": {
"abort": {
- "already_configured": "Dieses SimpliSafe-Konto wird bereits verwendet."
+ "already_configured": "Dieses SimpliSafe-Konto wird bereits verwendet.",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
},
"error": {
"identifier_exists": "Konto bereits registriert",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"step": {
"reauth_confirm": {
"data": {
"password": "Passwort"
- }
+ },
+ "description": "Dein Zugriffstoken ist abgelaufen oder wurde widerrufen. Gib dein Passwort ein, um dein Konto erneut zu verkn\u00fcpfen.",
+ "title": "Integration erneut authentifizieren"
},
"user": {
"data": {
diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json
index b98a121046a507..7b6e317b9228df 100644
--- a/homeassistant/components/simplisafe/translations/et.json
+++ b/homeassistant/components/simplisafe/translations/et.json
@@ -12,7 +12,7 @@
},
"step": {
"mfa": {
- "description": "Kontrollige oma e-posti: link SimpliSafe-lt. P\u00e4rast lingi kontrollimist naase siia, et viia l\u00f5pule sidumise installimine.",
+ "description": "Kontrolli oma e-posti: link SimpliSafe-lt. P\u00e4rast lingi kontrollimist naase siia, et viia l\u00f5pule sidumise installimine.",
"title": "SimpliSafe mitmeastmeline autentimine"
},
"reauth_confirm": {
diff --git a/homeassistant/components/simplisafe/translations/he.json b/homeassistant/components/simplisafe/translations/he.json
new file mode 100644
index 00000000000000..3007c0e968c1dc
--- /dev/null
+++ b/homeassistant/components/simplisafe/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json
index 7b989246de1738..8a2deedc5341af 100644
--- a/homeassistant/components/simplisafe/translations/hu.json
+++ b/homeassistant/components/simplisafe/translations/hu.json
@@ -1,11 +1,23 @@
{
"config": {
+ "abort": {
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
+ },
"error": {
- "identifier_exists": "Fi\u00f3k m\u00e1r regisztr\u00e1lva van"
+ "identifier_exists": "Fi\u00f3k m\u00e1r regisztr\u00e1lva van",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"step": {
+ "reauth_confirm": {
+ "data": {
+ "password": "Jelsz\u00f3"
+ },
+ "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se"
+ },
"user": {
"data": {
+ "code": "K\u00f3d (a Home Assistant felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9n haszn\u00e1latos)",
"password": "Jelsz\u00f3",
"username": "E-mail"
},
diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json
new file mode 100644
index 00000000000000..512d6a38405f5d
--- /dev/null
+++ b/homeassistant/components/simplisafe/translations/id.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun SimpliSafe ini sudah digunakan.",
+ "reauth_successful": "Autentikasi ulang berhasil"
+ },
+ "error": {
+ "identifier_exists": "Akun sudah terdaftar",
+ "invalid_auth": "Autentikasi tidak valid",
+ "still_awaiting_mfa": "Masih menunggu pengeklikan dari email MFA",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "mfa": {
+ "description": "Periksa email Anda untuk mendapatkan tautan dari SimpliSafe. Setelah memverifikasi tautan, kembali ke sini untuk menyelesaikan instalasi integrasi.",
+ "title": "Autentikasi Multi-Faktor SimpliSafe"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "Kata Sandi"
+ },
+ "description": "Token akses Anda telah kedaluwarsa atau dicabut. Masukkan kata sandi Anda untuk menautkan kembali akun Anda.",
+ "title": "Autentikasi Ulang Integrasi"
+ },
+ "user": {
+ "data": {
+ "code": "Kode (digunakan di antarmuka Home Assistant)",
+ "password": "Kata Sandi",
+ "username": "Email"
+ },
+ "title": "Isi informasi Anda."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "code": "Kode (digunakan di antarmuka Home Assistant)"
+ },
+ "title": "Konfigurasikan SimpliSafe"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json
index fdd69b39efc0a1..b5ce2a2670204f 100644
--- a/homeassistant/components/simplisafe/translations/it.json
+++ b/homeassistant/components/simplisafe/translations/it.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Questo account SimpliSafe \u00e8 gi\u00e0 in uso.",
- "reauth_successful": "La riautenticazione ha avuto successo"
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
},
"error": {
"identifier_exists": "Account gi\u00e0 registrato",
@@ -20,7 +20,7 @@
"password": "Password"
},
"description": "Il token di accesso \u00e8 scaduto o \u00e8 stato revocato. Inserisci la tua password per ricollegare il tuo account.",
- "title": "Reautenticare l'integrazione"
+ "title": "Autenticare nuovamente l'integrazione"
},
"user": {
"data": {
diff --git a/homeassistant/components/simplisafe/translations/ko.json b/homeassistant/components/simplisafe/translations/ko.json
index 57ba4a88fc16cc..194fa6cecf48b2 100644
--- a/homeassistant/components/simplisafe/translations/ko.json
+++ b/homeassistant/components/simplisafe/translations/ko.json
@@ -1,12 +1,27 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 SimpliSafe \uacc4\uc815\uc740 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4."
+ "already_configured": "\uc774 SimpliSafe \uacc4\uc815\uc740 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4.",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
- "identifier_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "identifier_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "still_awaiting_mfa": "\uc544\uc9c1 \ub2e4\ub2e8\uacc4 \uc778\uc99d(MFA) \uc774\uba54\uc77c\uc758 \ub9c1\ud06c \ud074\ub9ad\uc744 \uae30\ub2e4\ub9ac\uace0\uc788\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
+ "mfa": {
+ "description": "\uc774\uba54\uc77c\uc5d0\uc11c SimpliSafe\uc758 \ub9c1\ud06c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694. \ub9c1\ud06c\ub97c \ud655\uc778\ud55c \ud6c4 \uc5ec\uae30\ub85c \ub3cc\uc544\uc640 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc124\uce58\ub97c \uc644\ub8cc\ud574\uc8fc\uc138\uc694.",
+ "title": "SimpliSafe \ub2e4\ub2e8\uacc4 \uc778\uc99d"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638"
+ },
+ "description": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \ub9cc\ub8cc\ub418\uc5c8\uac70\ub098 \ud574\uc9c0\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uacc4\uc815\uc744 \ub2e4\uc2dc \uc5f0\uacb0\ud558\ub824\uba74 \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30"
+ },
"user": {
"data": {
"code": "\ucf54\ub4dc (Home Assistant UI \uc5d0\uc11c \uc0ac\uc6a9\ub428)",
diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json
index b285b288525a1d..7a6d9c5c4e32bb 100644
--- a/homeassistant/components/simplisafe/translations/nl.json
+++ b/homeassistant/components/simplisafe/translations/nl.json
@@ -1,25 +1,32 @@
{
"config": {
"abort": {
- "already_configured": "Dit SimpliSafe-account is al in gebruik."
+ "already_configured": "Dit SimpliSafe-account is al in gebruik.",
+ "reauth_successful": "Herauthenticatie was succesvol"
},
"error": {
"identifier_exists": "Account bestaat al",
"invalid_auth": "Ongeldige authenticatie",
+ "still_awaiting_mfa": "Wacht nog steeds op MFA-e-mailklik",
"unknown": "Onverwachte fout"
},
"step": {
+ "mfa": {
+ "description": "Controleer uw e-mail voor een link van SimpliSafe. Nadat u de link hebt geverifieerd, gaat u hier terug om de installatie van de integratie te voltooien.",
+ "title": "SimpliSafe Multi-Factor Authenticatie"
+ },
"reauth_confirm": {
"data": {
"password": "Wachtwoord"
},
- "description": "Uw toegangstoken is verlopen of ingetrokken. Voer uw wachtwoord in om uw account opnieuw te koppelen."
+ "description": "Uw toegangstoken is verlopen of ingetrokken. Voer uw wachtwoord in om uw account opnieuw te koppelen.",
+ "title": "Verifieer de integratie opnieuw"
},
"user": {
"data": {
"code": "Code (gebruikt in Home Assistant)",
"password": "Wachtwoord",
- "username": "E-mailadres"
+ "username": "E-mail"
},
"title": "Vul uw gegevens in"
}
diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json
index 94b0e6a0975d73..bcfffc575336fa 100644
--- a/homeassistant/components/simplisafe/translations/ru.json
+++ b/homeassistant/components/simplisafe/translations/ru.json
@@ -6,7 +6,7 @@
},
"error": {
"identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"still_awaiting_mfa": "\u041e\u0436\u0438\u0434\u0430\u043d\u0438\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u043f\u043e \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u0435.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
@@ -20,7 +20,7 @@
"password": "\u041f\u0430\u0440\u043e\u043b\u044c"
},
"description": "\u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438\u0441\u0442\u0435\u043a \u0438\u043b\u0438 \u0431\u044b\u043b \u0430\u043d\u043d\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043d\u043e\u0432\u043e \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c.",
- "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f"
+ "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": {
diff --git a/homeassistant/components/simplisafe/translations/sv.json b/homeassistant/components/simplisafe/translations/sv.json
index a1bfb4400be850..a4e8e0520733b6 100644
--- a/homeassistant/components/simplisafe/translations/sv.json
+++ b/homeassistant/components/simplisafe/translations/sv.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Det h\u00e4r SimpliSafe-kontot har redan konfigurerats."
+ },
"error": {
"identifier_exists": "Kontot \u00e4r redan registrerat"
},
diff --git a/homeassistant/components/simplisafe/translations/tr.json b/homeassistant/components/simplisafe/translations/tr.json
index ec84b1b7c1c51b..94506fb426bba8 100644
--- a/homeassistant/components/simplisafe/translations/tr.json
+++ b/homeassistant/components/simplisafe/translations/tr.json
@@ -1,6 +1,26 @@
{
"config": {
+ "abort": {
+ "already_configured": "Bu SimpliSafe hesab\u0131 zaten kullan\u0131mda.",
+ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu"
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "still_awaiting_mfa": "Hala MFA e-posta t\u0131klamas\u0131 bekleniyor",
+ "unknown": "Beklenmeyen hata"
+ },
"step": {
+ "mfa": {
+ "description": "SimpliSafe'den bir ba\u011flant\u0131 i\u00e7in e-postan\u0131z\u0131 kontrol edin. Ba\u011flant\u0131y\u0131 do\u011frulad\u0131ktan sonra, entegrasyonun kurulumunu tamamlamak i\u00e7in buraya geri d\u00f6n\u00fcn.",
+ "title": "SimpliSafe \u00c7ok Fakt\u00f6rl\u00fc Kimlik Do\u011frulama"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "Parola"
+ },
+ "description": "Eri\u015fim kodunuzun s\u00fcresi doldu veya iptal edildi. Hesab\u0131n\u0131z\u0131 yeniden ba\u011flamak i\u00e7in parolan\u0131z\u0131 girin.",
+ "title": "Entegrasyonu Yeniden Do\u011frula"
+ },
"user": {
"data": {
"password": "Parola",
diff --git a/homeassistant/components/simplisafe/translations/uk.json b/homeassistant/components/simplisafe/translations/uk.json
index 376fb4468dbf48..0a51f129e5fd26 100644
--- a/homeassistant/components/simplisafe/translations/uk.json
+++ b/homeassistant/components/simplisafe/translations/uk.json
@@ -1,18 +1,45 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e"
+ },
+ "error": {
+ "identifier_exists": "\u041e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u043e.",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "still_awaiting_mfa": "\u041e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f, \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u043e \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0456\u0439 \u043f\u043e\u0448\u0442\u0456.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
"step": {
+ "mfa": {
+ "description": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0441\u0432\u043e\u044e \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0443 \u043f\u043e\u0448\u0442\u0443 \u043d\u0430 \u043d\u0430\u044f\u0432\u043d\u0456\u0441\u0442\u044c \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0432\u0456\u0434 SimpliSafe. \u041f\u0456\u0441\u043b\u044f \u0442\u043e\u0433\u043e \u044f\u043a \u0432\u0456\u0434\u043a\u0440\u0438\u0454\u0442\u0435 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f, \u043f\u043e\u0432\u0435\u0440\u043d\u0456\u0442\u044c\u0441\u044f \u0441\u044e\u0434\u0438, \u0449\u043e\u0431 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457.",
+ "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f SimpliSafe"
+ },
"reauth_confirm": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c"
- }
+ },
+ "description": "\u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0437\u0430\u043a\u0456\u043d\u0447\u0438\u0432\u0441\u044f \u0430\u0431\u043e \u0431\u0443\u0432 \u0430\u043d\u0443\u043b\u044c\u043e\u0432\u0430\u043d\u0438\u0439. \u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c, \u0449\u043e\u0431 \u0437\u0430\u043d\u043e\u0432\u043e \u043f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441.",
+ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e"
},
"user": {
"data": {
+ "code": "\u041a\u043e\u0434 (\u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 Home Assistant)",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438"
},
"title": "\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u0432\u0430\u0448\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "code": "\u041a\u043e\u0434 (\u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 Home Assistant)"
+ },
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f SimpliSafe"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json
index ad5323d3957d6f..27064ed1055a38 100644
--- a/homeassistant/components/simplisafe/translations/zh-Hant.json
+++ b/homeassistant/components/simplisafe/translations/zh-Hant.json
@@ -19,7 +19,7 @@
"data": {
"password": "\u5bc6\u78bc"
},
- "description": "\u5b58\u53d6\u5bc6\u9470\u5df2\u7d93\u904e\u671f\u6216\u53d6\u6d88\uff0c\u8acb\u8f38\u5165\u5bc6\u78bc\u4ee5\u91cd\u65b0\u9023\u7d50\u5e33\u865f\u3002",
+ "description": "\u5b58\u53d6\u6b0a\u6756\u5df2\u7d93\u904e\u671f\u6216\u53d6\u6d88\uff0c\u8acb\u8f38\u5165\u5bc6\u78bc\u4ee5\u91cd\u65b0\u9023\u7d50\u5e33\u865f\u3002",
"title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408"
},
"user": {
diff --git a/homeassistant/components/simulated/sensor.py b/homeassistant/components/simulated/sensor.py
index 7f484b712c1790..3fe7aedfbb0940 100644
--- a/homeassistant/components/simulated/sensor.py
+++ b/homeassistant/components/simulated/sensor.py
@@ -5,10 +5,9 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
CONF_AMP = "amplitude"
@@ -67,7 +66,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([sensor], True)
-class SimulatedSensor(Entity):
+class SimulatedSensor(SensorEntity):
"""Class for simulated sensor."""
def __init__(
@@ -137,7 +136,7 @@ def unit_of_measurement(self):
return self._unit
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return other details about the sensor state."""
return {
"amplitude": self._amp,
diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py
index 9b759327dcacb7..3fdd2e55b0d8b1 100644
--- a/homeassistant/components/skybeacon/sensor.py
+++ b/homeassistant/components/skybeacon/sensor.py
@@ -8,7 +8,7 @@
from pygatt.exceptions import BLEError, NotConnectedError, NotificationTimeout
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_MAC,
CONF_NAME,
@@ -18,7 +18,6 @@
TEMP_CELSIUS,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -47,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Skybeacon sensor."""
name = config.get(CONF_NAME)
mac = config.get(CONF_MAC)
- _LOGGER.debug("Setting up...")
+ _LOGGER.debug("Setting up")
mon = Monitor(hass, mac, name)
add_entities([SkybeaconTemp(name, mon)])
@@ -62,7 +61,7 @@ def monitor_stop(_service_or_event):
mon.start()
-class SkybeaconHumid(Entity):
+class SkybeaconHumid(SensorEntity):
"""Representation of a Skybeacon humidity sensor."""
def __init__(self, name, mon):
@@ -86,12 +85,12 @@ def unit_of_measurement(self):
return PERCENTAGE
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {ATTR_DEVICE: "SKYBEACON", ATTR_MODEL: 1}
-class SkybeaconTemp(Entity):
+class SkybeaconTemp(SensorEntity):
"""Representation of a Skybeacon temperature sensor."""
def __init__(self, name, mon):
@@ -115,12 +114,12 @@ def unit_of_measurement(self):
return TEMP_CELSIUS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {ATTR_DEVICE: "SKYBEACON", ATTR_MODEL: 1}
-class Monitor(threading.Thread):
+class Monitor(threading.Thread, SensorEntity):
"""Connection handling."""
def __init__(self, hass, mac, name):
diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py
index c1c9d76314c901..2acb729d767995 100644
--- a/homeassistant/components/skybell/__init__.py
+++ b/homeassistant/components/skybell/__init__.py
@@ -82,7 +82,7 @@ def update(self):
self._device.refresh()
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py
index 512731ab355ebf..7e075fba38a371 100644
--- a/homeassistant/components/skybell/binary_sensor.py
+++ b/homeassistant/components/skybell/binary_sensor.py
@@ -14,7 +14,7 @@
from . import DEFAULT_ENTITY_NAMESPACE, DOMAIN as SKYBELL_DOMAIN, SkybellDevice
-SCAN_INTERVAL = timedelta(seconds=5)
+SCAN_INTERVAL = timedelta(seconds=10)
# Sensor types: Name, device_class, event
SENSOR_TYPES = {
@@ -76,9 +76,9 @@ def device_class(self):
return self._device_class
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
- attrs = super().device_state_attributes
+ attrs = super().extra_state_attributes
attrs["event_date"] = self._event.get("createdAt")
diff --git a/homeassistant/components/skybell/sensor.py b/homeassistant/components/skybell/sensor.py
index 09a7400a035c23..8dc13814c678d4 100644
--- a/homeassistant/components/skybell/sensor.py
+++ b/homeassistant/components/skybell/sensor.py
@@ -3,7 +3,7 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS
import homeassistant.helpers.config_validation as cv
@@ -38,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class SkybellSensor(SkybellDevice):
+class SkybellSensor(SkybellDevice, SensorEntity):
"""A sensor implementation for Skybell devices."""
def __init__(self, device, sensor_type):
diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py
index 985f59a67156db..f1e293773bd3a2 100644
--- a/homeassistant/components/slack/notify.py
+++ b/homeassistant/components/slack/notify.py
@@ -4,7 +4,7 @@
import asyncio
import logging
import os
-from typing import Any, List, Optional, TypedDict
+from typing import Any, TypedDict
from urllib.parse import urlparse
from aiohttp import BasicAuth, FormData
@@ -20,7 +20,7 @@
PLATFORM_SCHEMA,
BaseNotificationService,
)
-from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_USERNAME
+from homeassistant.const import ATTR_ICON, CONF_API_KEY, CONF_ICON, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client, config_validation as cv
import homeassistant.helpers.template as template
@@ -35,7 +35,6 @@
ATTR_BLOCKS = "blocks"
ATTR_BLOCKS_TEMPLATE = "blocks_template"
ATTR_FILE = "file"
-ATTR_ICON = "icon"
ATTR_PASSWORD = "password"
ATTR_PATH = "path"
ATTR_URL = "url"
@@ -106,14 +105,14 @@ class MessageT(TypedDict, total=False):
username: str # Optional key
icon_url: str # Optional key
icon_emoji: str # Optional key
- blocks: List[Any] # Optional key
+ blocks: list[Any] # Optional key
async def async_get_service(
hass: HomeAssistantType,
config: ConfigType,
- discovery_info: Optional[DiscoveryInfoType] = None,
-) -> Optional[SlackNotificationService]:
+ discovery_info: DiscoveryInfoType | None = None,
+) -> SlackNotificationService | None:
"""Set up the Slack notification service."""
session = aiohttp_client.async_get_clientsession(hass)
client = WebClient(token=config[CONF_API_KEY], run_async=True, session=session)
@@ -147,7 +146,7 @@ def _async_get_filename_from_url(url: str) -> str:
@callback
-def _async_sanitize_channel_names(channel_list: List[str]) -> List[str]:
+def _async_sanitize_channel_names(channel_list: list[str]) -> list[str]:
"""Remove any # symbols from a channel list."""
return [channel.lstrip("#") for channel in channel_list]
@@ -174,8 +173,8 @@ def __init__(
hass: HomeAssistantType,
client: WebClient,
default_channel: str,
- username: Optional[str],
- icon: Optional[str],
+ username: str | None,
+ icon: str | None,
) -> None:
"""Initialize."""
self._client = client
@@ -187,9 +186,9 @@ def __init__(
async def _async_send_local_file_message(
self,
path: str,
- targets: List[str],
+ targets: list[str],
message: str,
- title: Optional[str],
+ title: str | None,
) -> None:
"""Upload a local file (with message) to Slack."""
if not self._hass.config.is_allowed_path(path):
@@ -213,12 +212,12 @@ async def _async_send_local_file_message(
async def _async_send_remote_file_message(
self,
url: str,
- targets: List[str],
+ targets: list[str],
message: str,
- title: Optional[str],
+ title: str | None,
*,
- username: Optional[str] = None,
- password: Optional[str] = None,
+ username: str | None = None,
+ password: str | None = None,
) -> None:
"""Upload a remote file (with message) to Slack.
@@ -263,13 +262,13 @@ async def _async_send_remote_file_message(
async def _async_send_text_only_message(
self,
- targets: List[str],
+ targets: list[str],
message: str,
- title: Optional[str],
+ title: str | None,
*,
- username: Optional[str] = None,
- icon: Optional[str] = None,
- blocks: Optional[Any] = None,
+ username: str | None = None,
+ icon: str | None = None,
+ blocks: Any | None = None,
) -> None:
"""Send a text-only message."""
message_dict: MessageT = {"link_names": True, "text": message}
diff --git a/homeassistant/components/sleepiq/sensor.py b/homeassistant/components/sleepiq/sensor.py
index ae48c059bb88bf..8f5c17dad8941a 100644
--- a/homeassistant/components/sleepiq/sensor.py
+++ b/homeassistant/components/sleepiq/sensor.py
@@ -1,4 +1,6 @@
"""Support for SleepIQ sensors."""
+from homeassistant.components.sensor import SensorEntity
+
from . import SleepIQSensor
from .const import DOMAIN, SENSOR_TYPES, SIDES, SLEEP_NUMBER
@@ -21,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(dev)
-class SleepNumberSensor(SleepIQSensor):
+class SleepNumberSensor(SleepIQSensor, SensorEntity):
"""Implementation of a SleepIQ sensor."""
def __init__(self, sleepiq_data, bed_id, side):
diff --git a/homeassistant/components/slide/cover.py b/homeassistant/components/slide/cover.py
index 470cf9e5a1f0b6..9e925af3391137 100644
--- a/homeassistant/components/slide/cover.py
+++ b/homeassistant/components/slide/cover.py
@@ -55,7 +55,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
return {ATTR_ID: self._id}
diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py
index 119d9a366d6ef9..2290f3a330f9ff 100644
--- a/homeassistant/components/sma/sensor.py
+++ b/homeassistant/components/sma/sensor.py
@@ -5,12 +5,13 @@
import pysma
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PATH,
CONF_SCAN_INTERVAL,
+ CONF_SENSORS,
CONF_SSL,
CONF_VERIFY_SSL,
EVENT_HOMEASSISTANT_STOP,
@@ -18,7 +19,6 @@
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
_LOGGER = logging.getLogger(__name__)
@@ -27,7 +27,6 @@
CONF_FACTOR = "factor"
CONF_GROUP = "group"
CONF_KEY = "key"
-CONF_SENSORS = "sensors"
CONF_UNIT = "unit"
GROUPS = ["user", "installer"]
@@ -86,7 +85,6 @@ def _check_sensor_schema(conf):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up SMA WebConnect sensor."""
-
# Check config again during load - dependency available
config = _check_sensor_schema(config)
@@ -170,7 +168,7 @@ async def async_sma(event):
async_track_time_interval(hass, async_sma, interval)
-class SMAsensor(Entity):
+class SMAsensor(SensorEntity):
"""Representation of a SMA sensor."""
def __init__(self, pysma_sensor, sub_sensors):
@@ -197,7 +195,7 @@ def unit_of_measurement(self):
return self._sensor.unit
@property
- def device_state_attributes(self): # Can be remove from 0.99
+ def extra_state_attributes(self): # Can be remove from 0.99
"""Return the state attributes of the sensor."""
return self._attr
diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py
index aed67c5c1676b8..f803f38b8ea038 100644
--- a/homeassistant/components/smappee/__init__.py
+++ b/homeassistant/components/smappee/__init__.py
@@ -21,7 +21,7 @@
CONF_SERIALNUMBER,
DOMAIN,
MIN_TIME_BETWEEN_UPDATES,
- SMAPPEE_PLATFORMS,
+ PLATFORMS,
TOKEN_URL,
)
@@ -92,9 +92,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.data[DOMAIN][entry.entry_id] = SmappeeBase(hass, smappee)
- for component in SMAPPEE_PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -105,8 +105,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in SMAPPEE_PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/smappee/config_flow.py b/homeassistant/components/smappee/config_flow.py
index c6d208626e4c3d..450874b3f35ae0 100644
--- a/homeassistant/components/smappee/config_flow.py
+++ b/homeassistant/components/smappee/config_flow.py
@@ -56,7 +56,6 @@ async def async_step_zeroconf(self, discovery_info):
if self.is_cloud_device_already_added():
return self.async_abort(reason="already_configured_device")
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update(
{
CONF_IP_ADDRESS: discovery_info["host"],
@@ -76,7 +75,6 @@ async def async_step_zeroconf_confirm(self, user_input=None):
return self.async_abort(reason="already_configured_device")
if user_input is None:
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
serialnumber = self.context.get(CONF_SERIALNUMBER)
return self.async_show_form(
step_id="zeroconf_confirm",
@@ -84,7 +82,6 @@ async def async_step_zeroconf_confirm(self, user_input=None):
errors=errors,
)
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
ip_address = self.context.get(CONF_IP_ADDRESS)
serial_number = self.context.get(CONF_SERIALNUMBER)
diff --git a/homeassistant/components/smappee/const.py b/homeassistant/components/smappee/const.py
index 2c69f1ccb961bd..fc059509ced65d 100644
--- a/homeassistant/components/smappee/const.py
+++ b/homeassistant/components/smappee/const.py
@@ -12,7 +12,7 @@
ENV_CLOUD = "cloud"
ENV_LOCAL = "local"
-SMAPPEE_PLATFORMS = ["binary_sensor", "sensor", "switch"]
+PLATFORMS = ["binary_sensor", "sensor", "switch"]
SUPPORTED_LOCAL_DEVICES = ("Smappee1", "Smappee2")
diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json
index ddbff4e77381e2..a6dda75ac723d8 100644
--- a/homeassistant/components/smappee/manifest.json
+++ b/homeassistant/components/smappee/manifest.json
@@ -5,7 +5,7 @@
"documentation": "https://www.home-assistant.io/integrations/smappee",
"dependencies": ["http"],
"requirements": [
- "pysmappee==0.2.16"
+ "pysmappee==0.2.17"
],
"codeowners": [
"@bsmappee"
diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py
index 41041c1a0023b2..43483dbdb1ecb5 100644
--- a/homeassistant/components/smappee/sensor.py
+++ b/homeassistant/components/smappee/sensor.py
@@ -1,6 +1,6 @@
"""Support for monitoring a Smappee energy sensor."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_WATT_HOUR, POWER_WATT, VOLT
-from homeassistant.helpers.entity import Entity
from .const import DOMAIN
@@ -239,7 +239,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities, True)
-class SmappeeSensor(Entity):
+class SmappeeSensor(SensorEntity):
"""Implementation of a Smappee sensor."""
def __init__(self, smappee_base, service_location, sensor, attributes):
diff --git a/homeassistant/components/smappee/translations/de.json b/homeassistant/components/smappee/translations/de.json
index a609492f4285ae..15fd8d6cd22da1 100644
--- a/homeassistant/components/smappee/translations/de.json
+++ b/homeassistant/components/smappee/translations/de.json
@@ -1,7 +1,11 @@
{
"config": {
"abort": {
- "cannot_connect": "Verbindungsfehler"
+ "already_configured_device": "Ger\u00e4t ist bereits konfiguriert",
+ "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.",
+ "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})."
},
"flow_title": "Smappee: {name}",
"step": {
@@ -9,6 +13,14 @@
"data": {
"environment": "Umgebung"
}
+ },
+ "local": {
+ "data": {
+ "host": "Host"
+ }
+ },
+ "pick_implementation": {
+ "title": "W\u00e4hle die Authentifizierungsmethode"
}
}
}
diff --git a/homeassistant/components/smappee/translations/hu.json b/homeassistant/components/smappee/translations/hu.json
index 4258cfb0912ffb..15bfd4dc5d2175 100644
--- a/homeassistant/components/smappee/translations/hu.json
+++ b/homeassistant/components/smappee/translations/hu.json
@@ -2,7 +2,21 @@
"config": {
"abort": {
"already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
- "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.",
+ "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz."
+ },
+ "flow_title": "Smappee: {name}",
+ "step": {
+ "local": {
+ "data": {
+ "host": "Hoszt"
+ }
+ },
+ "pick_implementation": {
+ "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/smappee/translations/id.json b/homeassistant/components/smappee/translations/id.json
new file mode 100644
index 00000000000000..b72200c34ca7da
--- /dev/null
+++ b/homeassistant/components/smappee/translations/id.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_device": "Perangkat sudah dikonfigurasi",
+ "already_configured_local_device": "Perangkat lokal sudah dikonfigurasi. Hapus perangkat tersebut terlebih dahulu sebelum mengonfigurasi perangkat awan.",
+ "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.",
+ "cannot_connect": "Gagal terhubung",
+ "invalid_mdns": "Perangkat tidak didukung untuk integrasi Smappee.",
+ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.",
+ "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})"
+ },
+ "flow_title": "Smappee: {name}",
+ "step": {
+ "environment": {
+ "data": {
+ "environment": "Lingkungan"
+ },
+ "description": "Siapkan Smappee Anda untuk diintegrasikan dengan Home Assistant."
+ },
+ "local": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Masukkan host untuk memulai integrasi lokal Smappee"
+ },
+ "pick_implementation": {
+ "title": "Pilih Metode Autentikasi"
+ },
+ "zeroconf_confirm": {
+ "description": "Ingin menambahkan perangkat Smappee dengan nomor seri `{serialnumber}` ke Home Assistant?",
+ "title": "Peranti Smappee yang ditemukan"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smappee/translations/ko.json b/homeassistant/components/smappee/translations/ko.json
index b3e37ee6d01ed0..b2f0e5880c59a2 100644
--- a/homeassistant/components/smappee/translations/ko.json
+++ b/homeassistant/components/smappee/translations/ko.json
@@ -1,19 +1,34 @@
{
"config": {
"abort": {
- "already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_configured_local_device": "\ub85c\uceec \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4. \ud074\ub77c\uc6b0\ub4dc \uae30\uae30\ub97c \uad6c\uc131\ud558\uae30 \uc804\uc5d0 \uc774\ub7ec\ud55c \uae30\uae30\ub97c \uba3c\uc800 \uc81c\uac70\ud574\uc8fc\uc138\uc694.",
+ "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_mdns": "Smappee \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0\uc11c \uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 \uae30\uae30\uc785\ub2c8\ub2e4.",
"missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
- "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})"
+ "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694."
},
+ "flow_title": "Smappee: {name}",
"step": {
+ "environment": {
+ "data": {
+ "environment": "\ud658\uacbd"
+ },
+ "description": "Home Assistant\uc5d0 Smappee \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4."
+ },
"local": {
"data": {
"host": "\ud638\uc2a4\ud2b8"
- }
+ },
+ "description": "Smappee \ub85c\uceec \uc5f0\ub3d9\uc744 \uc2dc\uc791\ud560 \ud638\uc2a4\ud2b8\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694"
},
"pick_implementation": {
"title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30"
+ },
+ "zeroconf_confirm": {
+ "description": "\uc2dc\ub9ac\uc5bc \ubc88\ud638 `{serialnumber}`\uc758 Smappee \uae30\uae30\ub97c Home Assistant\uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "\ubc1c\uacac\ub41c Smpappee \uae30\uae30"
}
}
}
diff --git a/homeassistant/components/smappee/translations/nl.json b/homeassistant/components/smappee/translations/nl.json
index 86f4a40c6f9765..66ede5e8c14e66 100644
--- a/homeassistant/components/smappee/translations/nl.json
+++ b/homeassistant/components/smappee/translations/nl.json
@@ -3,14 +3,32 @@
"abort": {
"already_configured_device": "Apparaat is al geconfigureerd",
"already_configured_local_device": "Lokale apparaten zijn al geconfigureerd. Verwijder deze eerst voordat u een cloudapparaat configureert.",
- "cannot_connect": "Kan geen verbinding maken"
+ "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_mdns": "Niet-ondersteund apparaat voor de Smappee-integratie.",
+ "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.",
+ "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})"
},
+ "flow_title": "Smappee: {name}",
"step": {
+ "environment": {
+ "data": {
+ "environment": "Omgeving"
+ },
+ "description": "Stel uw Smappee in om te integreren met Home Assistant."
+ },
"local": {
"data": {
"host": "Host"
},
"description": "Voer de host in om de lokale Smappee-integratie te starten"
+ },
+ "pick_implementation": {
+ "title": "Kies een authenticatie methode"
+ },
+ "zeroconf_confirm": {
+ "description": "Wilt u het Smappee apparaat met serienummer `{serialnumber}` toevoegen aan Home Assistant?",
+ "title": "Ontdekt Smappee apparaat"
}
}
}
diff --git a/homeassistant/components/smappee/translations/tr.json b/homeassistant/components/smappee/translations/tr.json
new file mode 100644
index 00000000000000..4ba8a4da9a6ca5
--- /dev/null
+++ b/homeassistant/components/smappee/translations/tr.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_configured_local_device": "Yerel ayg\u0131t (lar) zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. L\u00fctfen bir bulut cihaz\u0131n\u0131 yap\u0131land\u0131rmadan \u00f6nce bunlar\u0131 kald\u0131r\u0131n.",
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_mdns": "Smappee entegrasyonu i\u00e7in desteklenmeyen cihaz."
+ },
+ "flow_title": "Smappee: {name}",
+ "step": {
+ "environment": {
+ "data": {
+ "environment": "\u00c7evre"
+ }
+ },
+ "local": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ }
+ },
+ "zeroconf_confirm": {
+ "title": "Smappee cihaz\u0131 bulundu"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smappee/translations/uk.json b/homeassistant/components/smappee/translations/uk.json
new file mode 100644
index 00000000000000..a268fa82eacd3f
--- /dev/null
+++ b/homeassistant/components/smappee/translations/uk.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_device": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_configured_local_device": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435 \u0434\u043b\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432. \u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u0457\u0445 \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0445\u043c\u0430\u0440\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e.",
+ "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_mdns": "\u041d\u0435\u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439.",
+ "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.",
+ "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443."
+ },
+ "flow_title": "Smappee: {name}",
+ "step": {
+ "environment": {
+ "data": {
+ "environment": "\u041e\u0442\u043e\u0447\u0435\u043d\u043d\u044f"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Smappee."
+ },
+ "local": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u0445\u043e\u0441\u0442\u0430, \u0449\u043e\u0431 \u043f\u043e\u0447\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u0437 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0438\u043c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c Smappee"
+ },
+ "pick_implementation": {
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ },
+ "zeroconf_confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Smappee \u0437 \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serialnumber}`?",
+ "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Smappee"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py
index 7b1c6cfa9b7173..3180248fcd1060 100644
--- a/homeassistant/components/smart_meter_texas/__init__.py
+++ b/homeassistant/components/smart_meter_texas/__init__.py
@@ -83,9 +83,9 @@ async def async_update_data():
asyncio.create_task(coordinator.async_refresh())
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -122,8 +122,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/smart_meter_texas/config_flow.py b/homeassistant/components/smart_meter_texas/config_flow.py
index 211957dac9dfef..9f6df058cc7fb3 100644
--- a/homeassistant/components/smart_meter_texas/config_flow.py
+++ b/homeassistant/components/smart_meter_texas/config_flow.py
@@ -14,7 +14,7 @@
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import aiohttp_client
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/smart_meter_texas/sensor.py b/homeassistant/components/smart_meter_texas/sensor.py
index e65fbdcb53164b..54e5969f8ea218 100644
--- a/homeassistant/components/smart_meter_texas/sensor.py
+++ b/homeassistant/components/smart_meter_texas/sensor.py
@@ -1,6 +1,7 @@
"""Support for Smart Meter Texas sensors."""
from smart_meter_texas import Meter
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_ADDRESS, ENERGY_KILO_WATT_HOUR
from homeassistant.core import callback
from homeassistant.helpers.restore_state import RestoreEntity
@@ -29,7 +30,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
-class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity):
+class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity, SensorEntity):
"""Representation of an Smart Meter Texas sensor."""
def __init__(self, meter: Meter, coordinator: DataUpdateCoordinator):
@@ -65,7 +66,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
attributes = {
METER_NUMBER: self.meter.meter,
diff --git a/homeassistant/components/smart_meter_texas/translations/de.json b/homeassistant/components/smart_meter_texas/translations/de.json
index 382156757010d5..0eee2778d05365 100644
--- a/homeassistant/components/smart_meter_texas/translations/de.json
+++ b/homeassistant/components/smart_meter_texas/translations/de.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"step": {
diff --git a/homeassistant/components/smart_meter_texas/translations/hu.json b/homeassistant/components/smart_meter_texas/translations/hu.json
index 3b2d79a34a77e2..fd8db27da5efd0 100644
--- a/homeassistant/components/smart_meter_texas/translations/hu.json
+++ b/homeassistant/components/smart_meter_texas/translations/hu.json
@@ -2,6 +2,19 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/smart_meter_texas/translations/id.json b/homeassistant/components/smart_meter_texas/translations/id.json
new file mode 100644
index 00000000000000..4a84db42a14f2a
--- /dev/null
+++ b/homeassistant/components/smart_meter_texas/translations/id.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smart_meter_texas/translations/ru.json b/homeassistant/components/smart_meter_texas/translations/ru.json
index 9fe75df9c3f8ac..aef0fdff54e603 100644
--- a/homeassistant/components/smart_meter_texas/translations/ru.json
+++ b/homeassistant/components/smart_meter_texas/translations/ru.json
@@ -5,14 +5,14 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
}
}
}
diff --git a/homeassistant/components/smart_meter_texas/translations/tr.json b/homeassistant/components/smart_meter_texas/translations/tr.json
new file mode 100644
index 00000000000000..6ed28a58c793c2
--- /dev/null
+++ b/homeassistant/components/smart_meter_texas/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smart_meter_texas/translations/uk.json b/homeassistant/components/smart_meter_texas/translations/uk.json
new file mode 100644
index 00000000000000..49bceaa3f6ea94
--- /dev/null
+++ b/homeassistant/components/smart_meter_texas/translations/uk.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py
index c826b5d8f4d2b0..c259ef71aab961 100644
--- a/homeassistant/components/smarthab/__init__.py
+++ b/homeassistant/components/smarthab/__init__.py
@@ -13,7 +13,7 @@
DOMAIN = "smarthab"
DATA_HUB = "hub"
-COMPONENTS = ["light", "cover"]
+PLATFORMS = ["light", "cover"]
_LOGGER = logging.getLogger(__name__)
@@ -69,9 +69,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
# Pass hub object to child platforms
hass.data[DOMAIN][entry.entry_id] = {DATA_HUB: hub}
- for component in COMPONENTS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -83,8 +83,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
result = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in COMPONENTS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/smarthab/config_flow.py b/homeassistant/components/smarthab/config_flow.py
index 68f0460a4ccd96..9a4ec3ef3250b4 100644
--- a/homeassistant/components/smarthab/config_flow.py
+++ b/homeassistant/components/smarthab/config_flow.py
@@ -7,7 +7,6 @@
from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
-# pylint: disable=unused-import
from . import DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/smarthab/translations/de.json b/homeassistant/components/smarthab/translations/de.json
index 2c76c4d56db9a0..18bb2c77047dc1 100644
--- a/homeassistant/components/smarthab/translations/de.json
+++ b/homeassistant/components/smarthab/translations/de.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler"
},
"step": {
diff --git a/homeassistant/components/smarthab/translations/hu.json b/homeassistant/components/smarthab/translations/hu.json
index b40828cc764d8a..222c95bba16b57 100644
--- a/homeassistant/components/smarthab/translations/hu.json
+++ b/homeassistant/components/smarthab/translations/hu.json
@@ -1,7 +1,15 @@
{
"config": {
+ "error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
"step": {
"user": {
+ "data": {
+ "email": "E-mail",
+ "password": "Jelsz\u00f3"
+ },
"description": "Technikai okokb\u00f3l ne felejtsen el m\u00e1sodlagos fi\u00f3kot haszn\u00e1lni a Home Assistant be\u00e1ll\u00edt\u00e1s\u00e1hoz. A SmartHab alkalmaz\u00e1sb\u00f3l l\u00e9trehozhat egyet."
}
}
diff --git a/homeassistant/components/smarthab/translations/id.json b/homeassistant/components/smarthab/translations/id.json
new file mode 100644
index 00000000000000..7a776eac304f53
--- /dev/null
+++ b/homeassistant/components/smarthab/translations/id.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid",
+ "service": "Terjadi kesalahan saat mencoba menjangkau SmartHab. Layanan mungkin sedang mengalami gangguan. Periksa koneksi Anda.",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Email",
+ "password": "Kata Sandi"
+ },
+ "description": "Untuk alasan teknis, pastikan untuk menggunakan akun sekunder khusus untuk penyiapan Home Assistant Anda. Anda dapat membuatnya dari aplikasi SmartHab.",
+ "title": "Siapkan SmartHab"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarthab/translations/ko.json b/homeassistant/components/smarthab/translations/ko.json
index 4b15acd2f3837c..1641555b412f48 100644
--- a/homeassistant/components/smarthab/translations/ko.json
+++ b/homeassistant/components/smarthab/translations/ko.json
@@ -1,7 +1,9 @@
{
"config": {
"error": {
- "service": "SmartHab \uc5d0 \uc811\uc18d\ud558\ub294 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc11c\ube44\uc2a4\uac00 \ub2e4\uc6b4\ub418\uc5c8\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc5f0\uacb0\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694."
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "service": "SmartHab \uc5d0 \uc811\uc18d\ud558\ub294 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc11c\ube44\uc2a4\uac00 \ub2e4\uc6b4\ub418\uc5c8\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc5f0\uacb0\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
@@ -9,7 +11,7 @@
"email": "\uc774\uba54\uc77c",
"password": "\ube44\ubc00\ubc88\ud638"
},
- "description": "\uae30\uc220\uc801\uc778 \uc774\uc720\ub85c Home Assistant \uc124\uc815\uacfc \uad00\ub828\ub41c \ubcf4\uc870 \uacc4\uc815\uc744 \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. SmartHab \uc751\uc6a9 \ud504\ub85c\uadf8\ub7a8\uc5d0\uc11c \uc0dd\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "description": "\uae30\uc220\uc801\uc778 \uc774\uc720\ub85c Home Assistant \uc124\uc815\uacfc \uad00\ub828\ub41c \ubcf4\uc870 \uacc4\uc815\uc744 \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. SmartHab \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc5d0\uc11c \uc0dd\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
"title": "SmartHab \uc124\uce58\ud558\uae30"
}
}
diff --git a/homeassistant/components/smarthab/translations/nl.json b/homeassistant/components/smarthab/translations/nl.json
index 9dabac8aa55d9a..31a02ae2b9771d 100644
--- a/homeassistant/components/smarthab/translations/nl.json
+++ b/homeassistant/components/smarthab/translations/nl.json
@@ -1,14 +1,18 @@
{
"config": {
"error": {
+ "invalid_auth": "Ongeldige authenticatie",
"service": "Fout bij het bereiken van SmartHab. De service is mogelijk uitgevallen. Controleer uw verbinding.",
"unknown": "Onverwachte fout"
},
"step": {
"user": {
"data": {
+ "email": "E-mail",
"password": "Wachtwoord"
- }
+ },
+ "description": "Om technische redenen moet u een tweede account gebruiken dat specifiek is voor uw Home Assistant-installatie. U kunt er een aanmaken vanuit de SmartHab-toepassing.",
+ "title": "Stel SmartHab in"
}
}
}
diff --git a/homeassistant/components/smarthab/translations/ru.json b/homeassistant/components/smarthab/translations/ru.json
index cea090f51d2a27..45e3698034f683 100644
--- a/homeassistant/components/smarthab/translations/ru.json
+++ b/homeassistant/components/smarthab/translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"service": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a SmartHab. \u0421\u0435\u0440\u0432\u0438\u0441 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
diff --git a/homeassistant/components/smarthab/translations/tr.json b/homeassistant/components/smarthab/translations/tr.json
new file mode 100644
index 00000000000000..98da6384f8d2e4
--- /dev/null
+++ b/homeassistant/components/smarthab/translations/tr.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-posta",
+ "password": "Parola"
+ },
+ "title": "SmartHab'\u0131 kurun"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarthab/translations/uk.json b/homeassistant/components/smarthab/translations/uk.json
new file mode 100644
index 00000000000000..036ec0a78d4d84
--- /dev/null
+++ b/homeassistant/components/smarthab/translations/uk.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "service": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0441\u043f\u0440\u043e\u0431\u0456 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e SmartHab. \u0421\u0435\u0440\u0432\u0456\u0441 \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "description": "\u0417 \u0442\u0435\u0445\u043d\u0456\u0447\u043d\u0438\u0445 \u043f\u0440\u0438\u0447\u0438\u043d \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0438\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0434\u043b\u044f Home Assistant. \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u0457\u0457 \u0432 \u0434\u043e\u0434\u0430\u0442\u043a\u0443 SmartHab.",
+ "title": "SmartHab"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py
index d184a3ca6ce20f..77ef913c629cf5 100644
--- a/homeassistant/components/smartthings/__init__.py
+++ b/homeassistant/components/smartthings/__init__.py
@@ -36,8 +36,8 @@
DATA_MANAGER,
DOMAIN,
EVENT_BUTTON,
+ PLATFORMS,
SIGNAL_SMARTTHINGS_UPDATE,
- SUPPORTED_PLATFORMS,
TOKEN_REFRESH_INTERVAL,
)
from .smartapp import (
@@ -184,9 +184,9 @@ async def retrieve_device_status(device):
)
return False
- for component in SUPPORTED_PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -213,8 +213,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
broker.disconnect()
tasks = [
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in SUPPORTED_PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
return all(await asyncio.gather(*tasks))
@@ -293,11 +293,13 @@ def _assign_capabilities(self, devices: Iterable):
for device in devices:
capabilities = device.capabilities.copy()
slots = {}
- for platform_name in SUPPORTED_PLATFORMS:
- platform = importlib.import_module(f".{platform_name}", self.__module__)
- if not hasattr(platform, "get_capabilities"):
+ for platform in PLATFORMS:
+ platform_module = importlib.import_module(
+ f".{platform}", self.__module__
+ )
+ if not hasattr(platform_module, "get_capabilities"):
continue
- assigned = platform.get_capabilities(capabilities)
+ assigned = platform_module.get_capabilities(capabilities)
if not assigned:
continue
# Draw-down capabilities and set slot assignment
@@ -305,7 +307,7 @@ def _assign_capabilities(self, devices: Iterable):
if capability not in capabilities:
continue
capabilities.remove(capability)
- slots[capability] = platform_name
+ slots[capability] = platform
assignments[device.device_id] = slots
return assignments
diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py
index 41e915d5c955f4..dd4c1e2928c5de 100644
--- a/homeassistant/components/smartthings/binary_sensor.py
+++ b/homeassistant/components/smartthings/binary_sensor.py
@@ -1,5 +1,7 @@
"""Support for binary sensors through the SmartThings cloud API."""
-from typing import Optional, Sequence
+from __future__ import annotations
+
+from typing import Sequence
from pysmartthings import Attribute, Capability
@@ -52,7 +54,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors)
-def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]:
+def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None:
"""Return all capabilities supported if minimum required are present."""
return [
capability for capability in CAPABILITY_TO_ATTRIB if capability in capabilities
diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py
index 6ce872cdac7ab8..76c168fbc381ee 100644
--- a/homeassistant/components/smartthings/climate.py
+++ b/homeassistant/components/smartthings/climate.py
@@ -1,8 +1,10 @@
"""Support for climate devices through the SmartThings cloud API."""
+from __future__ import annotations
+
import asyncio
from collections.abc import Iterable
import logging
-from typing import Optional, Sequence
+from typing import Sequence
from pysmartthings import Attribute, Capability
@@ -103,7 +105,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities, True)
-def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]:
+def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None:
"""Return all capabilities supported if minimum required are present."""
supported = [
Capability.air_conditioner_mode,
@@ -274,7 +276,7 @@ def fan_modes(self):
return self._device.status.supported_thermostat_fan_modes
@property
- def hvac_action(self) -> Optional[str]:
+ def hvac_action(self) -> str | None:
"""Return the current running hvac operation if supported."""
return OPERATING_STATE_TO_ACTION.get(
self._device.status.thermostat_operating_state
@@ -415,7 +417,7 @@ def current_temperature(self):
return self._device.status.temperature
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""
Return device specific state attributes.
diff --git a/homeassistant/components/smartthings/config_flow.py b/homeassistant/components/smartthings/config_flow.py
index e320c335a0820b..f6cca7e02763bd 100644
--- a/homeassistant/components/smartthings/config_flow.py
+++ b/homeassistant/components/smartthings/config_flow.py
@@ -16,7 +16,6 @@
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-# pylint: disable=unused-import
from .const import (
APP_OAUTH_CLIENT_NAME,
APP_OAUTH_SCOPES,
diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py
index 03188411f07bd7..a7aa9066dd2f74 100644
--- a/homeassistant/components/smartthings/const.py
+++ b/homeassistant/components/smartthings/const.py
@@ -31,7 +31,7 @@
# Ordered 'specific to least-specific platform' in order for capabilities
# to be drawn-down and represented by the most appropriate platform.
-SUPPORTED_PLATFORMS = [
+PLATFORMS = [
"climate",
"fan",
"light",
diff --git a/homeassistant/components/smartthings/cover.py b/homeassistant/components/smartthings/cover.py
index ddc52ec3f6c9ca..8fff4ebbdfa51b 100644
--- a/homeassistant/components/smartthings/cover.py
+++ b/homeassistant/components/smartthings/cover.py
@@ -1,5 +1,7 @@
"""Support for covers through the SmartThings cloud API."""
-from typing import Optional, Sequence
+from __future__ import annotations
+
+from typing import Sequence
from pysmartthings import Attribute, Capability
@@ -46,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
-def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]:
+def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None:
"""Return all capabilities supported if minimum required are present."""
min_required = [
Capability.door_control,
@@ -147,7 +149,7 @@ def device_class(self):
return self._device_class
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Get additional state attributes."""
return self._state_attrs
diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py
index b09bfe0ad46c2b..167f3a38edf7bd 100644
--- a/homeassistant/components/smartthings/fan.py
+++ b/homeassistant/components/smartthings/fan.py
@@ -1,11 +1,14 @@
"""Support for fans through the SmartThings cloud API."""
+from __future__ import annotations
+
import math
-from typing import Optional, Sequence
+from typing import Sequence
from pysmartthings import Capability
from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity
from homeassistant.util.percentage import (
+ int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@@ -28,7 +31,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
-def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]:
+def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None:
"""Return all capabilities supported if minimum required are present."""
supported = [Capability.switch, Capability.fan_speed]
# Must have switch and fan_speed
@@ -75,10 +78,15 @@ def is_on(self) -> bool:
return self._device.status.switch
@property
- def percentage(self) -> str:
+ def percentage(self) -> int:
"""Return the current speed percentage."""
return ranged_value_to_percentage(SPEED_RANGE, self._device.status.fan_speed)
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return int_states_in_range(SPEED_RANGE)
+
@property
def supported_features(self) -> int:
"""Flag supported features."""
diff --git a/homeassistant/components/smartthings/light.py b/homeassistant/components/smartthings/light.py
index 1e4161abd0f756..de678f255fa6e4 100644
--- a/homeassistant/components/smartthings/light.py
+++ b/homeassistant/components/smartthings/light.py
@@ -1,6 +1,8 @@
"""Support for lights through the SmartThings cloud API."""
+from __future__ import annotations
+
import asyncio
-from typing import Optional, Sequence
+from typing import Sequence
from pysmartthings import Capability
@@ -34,7 +36,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
-def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]:
+def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None:
"""Return all capabilities supported if minimum required are present."""
supported = [
Capability.switch,
diff --git a/homeassistant/components/smartthings/lock.py b/homeassistant/components/smartthings/lock.py
index d6b615b47a74f9..2cd0b283cca861 100644
--- a/homeassistant/components/smartthings/lock.py
+++ b/homeassistant/components/smartthings/lock.py
@@ -1,5 +1,7 @@
"""Support for locks through the SmartThings cloud API."""
-from typing import Optional, Sequence
+from __future__ import annotations
+
+from typing import Sequence
from pysmartthings import Attribute, Capability
@@ -31,7 +33,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
-def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]:
+def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None:
"""Return all capabilities supported if minimum required are present."""
if Capability.lock in capabilities:
return [Capability.lock]
@@ -57,7 +59,7 @@ def is_locked(self):
return self._device.status.lock == ST_STATE_LOCKED
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
state_attrs = {}
status = self._device.status.attributes[Attribute.lock]
diff --git a/homeassistant/components/smartthings/scene.py b/homeassistant/components/smartthings/scene.py
index 11ee6dc83e1853..e3d93c663fa3c6 100644
--- a/homeassistant/components/smartthings/scene.py
+++ b/homeassistant/components/smartthings/scene.py
@@ -24,7 +24,7 @@ async def async_activate(self, **kwargs: Any) -> None:
await self._scene.execute()
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Get attributes about the state."""
return {
"icon": self._scene.icon,
diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py
index 835c4168f07f7f..86377e32e23940 100644
--- a/homeassistant/components/smartthings/sensor.py
+++ b/homeassistant/components/smartthings/sensor.py
@@ -1,9 +1,12 @@
"""Support for sensors through the SmartThings cloud API."""
+from __future__ import annotations
+
from collections import namedtuple
-from typing import Optional, Sequence
+from typing import Sequence
from pysmartthings import Attribute, Capability
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
AREA_SQUARE_METERS,
CONCENTRATION_PARTS_PER_MILLION,
@@ -297,14 +300,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors)
-def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]:
+def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None:
"""Return all capabilities supported if minimum required are present."""
return [
capability for capability in CAPABILITY_TO_SENSORS if capability in capabilities
]
-class SmartThingsSensor(SmartThingsEntity):
+class SmartThingsSensor(SmartThingsEntity, SensorEntity):
"""Define a SmartThings Sensor."""
def __init__(
@@ -344,7 +347,7 @@ def unit_of_measurement(self):
return UNITS.get(unit, unit) if unit else self._default_unit
-class SmartThingsThreeAxisSensor(SmartThingsEntity):
+class SmartThingsThreeAxisSensor(SmartThingsEntity, SensorEntity):
"""Define a SmartThings Three Axis Sensor."""
def __init__(self, device, index):
diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py
index ff70648ddcf0d6..d8bcd4554154b6 100644
--- a/homeassistant/components/smartthings/switch.py
+++ b/homeassistant/components/smartthings/switch.py
@@ -1,5 +1,7 @@
"""Support for switches through the SmartThings cloud API."""
-from typing import Optional, Sequence
+from __future__ import annotations
+
+from typing import Sequence
from pysmartthings import Attribute, Capability
@@ -21,7 +23,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
-def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]:
+def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None:
"""Return all capabilities supported if minimum required are present."""
# Must be able to be turned on/off.
if Capability.switch in capabilities:
diff --git a/homeassistant/components/smartthings/translations/et.json b/homeassistant/components/smartthings/translations/et.json
index 04cd0d70218bf9..18d6076898db4c 100644
--- a/homeassistant/components/smartthings/translations/et.json
+++ b/homeassistant/components/smartthings/translations/et.json
@@ -19,7 +19,7 @@
"data": {
"access_token": "Juurdep\u00e4\u00e4sut\u00f5end"
},
- "description": "Sisesta SmartThingsi [isiklik juurdep\u00e4\u00e4suluba] ( {token_url} ), mis on loodud vastavalt [juhistele] ( {component_url} ). Seda kasutatakse Home Assistanti sidumise loomiseks teie SmartThingsi kontol.",
+ "description": "Sisesta SmartThingsi [isiklik juurdep\u00e4\u00e4suluba] ( {token_url} ), mis on loodud vastavalt [juhistele] ( {component_url} ). Seda kasutatakse Home Assistanti sidumise loomiseks SmartThingsi kontol.",
"title": "Sisesta isiklik juurdep\u00e4\u00e4suluba (PAT)"
},
"select_location": {
diff --git a/homeassistant/components/smartthings/translations/hu.json b/homeassistant/components/smartthings/translations/hu.json
index 5cbe1d086bc084..17c0a1a1b042f5 100644
--- a/homeassistant/components/smartthings/translations/hu.json
+++ b/homeassistant/components/smartthings/translations/hu.json
@@ -12,11 +12,15 @@
"data": {
"access_token": "Hozz\u00e1f\u00e9r\u00e9si token"
},
- "description": "K\u00e9rj\u00fck, adjon meg egy SmartThings [Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si tokent] ( {token_url} ), amelyet az [utas\u00edt\u00e1sok] ( {component_url} ) alapj\u00e1n hoztak l\u00e9tre. Ezt haszn\u00e1ljuk a Home Assistant integr\u00e1ci\u00f3j\u00e1nak l\u00e9trehoz\u00e1s\u00e1hoz a SmartThings-fi\u00f3kban."
+ "description": "K\u00e9rj\u00fck, adjon meg egy SmartThings [Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si tokent]({token_url}), amelyet az [utas\u00edt\u00e1sok]({component_url}) alapj\u00e1n hoztak l\u00e9tre. Ezt haszn\u00e1ljuk a Home Assistant integr\u00e1ci\u00f3j\u00e1nak l\u00e9trehoz\u00e1s\u00e1hoz a SmartThings-fi\u00f3kban."
+ },
+ "select_location": {
+ "data": {
+ "location_id": "Elhelyezked\u00e9s"
+ }
},
"user": {
- "description": "K\u00e9rlek add meg a SmartThings [Personal Access Tokent]({token_url}), amit az [instrukci\u00f3k] ({component_url}) alapj\u00e1n hozt\u00e1l l\u00e9tre.",
- "title": "Adja meg a szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si Tokent"
+ "title": "Callback URL meger\u0151s\u00edt\u00e9se"
}
}
}
diff --git a/homeassistant/components/smartthings/translations/id.json b/homeassistant/components/smartthings/translations/id.json
new file mode 100644
index 00000000000000..77846487d85b5c
--- /dev/null
+++ b/homeassistant/components/smartthings/translations/id.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "invalid_webhook_url": "Home Assistant tidak dikonfigurasi dengan benar untuk menerima pembaruan dari SmartThings. URL webhook tidak valid:\n> {webhook_url}\n\nPerbarui konfigurasi Anda sesuai [petunjuk]({component_url}), kemudian mulai ulang Home Assistant, dan coba kembali.",
+ "no_available_locations": "Tidak ada SmartThings Location untuk disiapkan di Home Assistant."
+ },
+ "error": {
+ "app_setup_error": "Tidak dapat menyiapkan SmartApp. Coba lagi.",
+ "token_forbidden": "Token tidak memiliki cakupan OAuth yang diperlukan.",
+ "token_invalid_format": "Token harus dalam format UID/GUID",
+ "token_unauthorized": "Token tidak valid atau tidak lagi diotorisasi.",
+ "webhook_error": "SmartThings tidak dapat memvalidasi URL webhook. Pastikan URL webhook dapat dijangkau dari internet, lalu coba lagi."
+ },
+ "step": {
+ "authorize": {
+ "title": "Otorisasi Home Assistant"
+ },
+ "pat": {
+ "data": {
+ "access_token": "Token Akses"
+ },
+ "description": "Masukkan [Token Akses Pribadi]({token_url}) SmartThings yang telah dibuat sesuai [petunjuk]({component_url}). Ini akan digunakan untuk membuat integrasi Home Assistant dalam akun SmartThings Anda.",
+ "title": "Masukkan Token Akses Pribadi"
+ },
+ "select_location": {
+ "data": {
+ "location_id": "Lokasi"
+ },
+ "description": "Pilih SmartThings Location yang ingin ditambahkan ke Home Assistant. Kami akan membuka jendela baru dan meminta Anda untuk masuk dan mengotorisasi instalasi integrasi Home Assistant ke Location yang dipilih.",
+ "title": "Pilih Location"
+ },
+ "user": {
+ "description": "SmartThings akan dikonfigurasi untuk mengirim pembaruan push ke Home Assistant di:\n > {webhook_url} \n\nJika ini tidak benar, perbarui konfigurasi Anda, mulai ulang Home Assistant, dan coba lagi.",
+ "title": "Konfirmasikan URL Panggilan Balik"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smartthings/translations/ko.json b/homeassistant/components/smartthings/translations/ko.json
index 0ee2813665181f..dc02f25451b637 100644
--- a/homeassistant/components/smartthings/translations/ko.json
+++ b/homeassistant/components/smartthings/translations/ko.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "invalid_webhook_url": "Home Assistant \uac00 SmartThings \uc5d0\uc11c \uc5c5\ub370\uc774\ud2b8\ub97c \uc218\uc2e0\ud558\ub3c4\ub85d \uc62c\ubc14\ub974\uac8c \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc6f9 \ud6c5 URL \uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4:\n> {webhook_url} \n\n[\uc548\ub0b4]({component_url}) \ub97c \ucc38\uace0\ud558\uc5ec \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud558\uace0 Home Assistant \ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
- "no_available_locations": "Home Assistant \uc5d0\uc11c \uc124\uc815\ud560 \uc218 \uc788\ub294 SmartThings \uc704\uce58\uac00 \uc5c6\uc2b5\ub2c8\ub2e4."
+ "invalid_webhook_url": "Home Assistant\uac00 SmartThings\uc5d0\uc11c \uc5c5\ub370\uc774\ud2b8\ub97c \uc218\uc2e0\ud558\ub3c4\ub85d \uc62c\ubc14\ub974\uac8c \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc6f9 \ud6c5 URL\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4:\n> {webhook_url} \n\n[\uc548\ub0b4]({component_url})\ub97c \ucc38\uace0\ud558\uc5ec \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud558\uace0 Home Assistant\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "no_available_locations": "Home Assistant\uc5d0\uc11c \uc124\uc815\ud560 \uc218 \uc788\ub294 SmartThings \uc704\uce58\uac00 \uc5c6\uc2b5\ub2c8\ub2e4."
},
"error": {
"app_setup_error": "SmartApp \uc744 \uc124\uc815\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
@@ -19,18 +19,18 @@
"data": {
"access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070"
},
- "description": "[\uc548\ub0b4]({component_url}) \uc5d0 \ub530\ub77c \uc0dd\uc131\ub41c SmartThings [\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070]({token_url}) \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. SmartThings \uacc4\uc815\uc5d0\uc11c Home Assistant \uc5f0\ub3d9\uc744 \ub9cc\ub4dc\ub294\ub370 \uc0ac\uc6a9\ub429\ub2c8\ub2e4.",
- "title": "\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070 \uc785\ub825\ud558\uae30"
+ "description": "[\uc548\ub0b4]({component_url})\uc5d0 \ub530\ub77c \uc0dd\uc131\ub41c SmartThings [\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070]({token_url})\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. SmartThings \uacc4\uc815\uc5d0\uc11c Home Assistant \uc5f0\ub3d9\uc744 \ub9cc\ub4dc\ub294\ub370 \uc0ac\uc6a9\ub429\ub2c8\ub2e4.",
+ "title": "\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694"
},
"select_location": {
"data": {
"location_id": "\uc704\uce58"
},
- "description": "Home Assistant \uc5d0 \ucd94\uac00\ud558\ub824\ub294 SmartThings \uc704\uce58\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc0c8\ub86d\uac8c \uc5f4\ub9b0 \ub85c\uadf8\uc778 \ucc3d\uc5d0\uc11c \ub85c\uadf8\uc778\uc744 \ud558\uba74 \uc120\ud0dd\ud55c \uc704\uce58\uc5d0 Home Assistant \uc5f0\ub3d9\uc744 \uc2b9\uc778\ud558\ub77c\ub294 \uba54\uc2dc\uc9c0\uac00 \ud45c\uc2dc\ub429\ub2c8\ub2e4.",
+ "description": "Home Assistant\uc5d0 \ucd94\uac00\ud558\ub824\ub294 SmartThings \uc704\uce58\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc0c8\ub86d\uac8c \uc5f4\ub9b0 \ub85c\uadf8\uc778 \ucc3d\uc5d0\uc11c \ub85c\uadf8\uc778\uc744 \ud558\uba74 \uc120\ud0dd\ud55c \uc704\uce58\uc5d0 Home Assistant \uc5f0\ub3d9\uc744 \uc2b9\uc778\ud558\ub77c\ub294 \uba54\uc2dc\uc9c0\uac00 \ud45c\uc2dc\ub429\ub2c8\ub2e4.",
"title": "\uc704\uce58 \uc120\ud0dd\ud558\uae30"
},
"user": {
- "description": "SmartThings \ub294 \uc544\ub798\uc758 \uc6f9 \ud6c5 \uc8fc\uc18c\ub85c Home Assistant \uc5d0 \ud478\uc2dc \uc5c5\ub370\uc774\ud2b8\ub97c \ubcf4\ub0b4\ub3c4\ub85d \uad6c\uc131\ub429\ub2c8\ub2e4. \n > {webhook_url} \n\n\uc774 \uad6c\uc131\uc774 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc73c\uba74 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud558\uace0 Home Assistant \ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "description": "SmartThings\ub294 \uc544\ub798\uc758 \uc6f9 \ud6c5 \uc8fc\uc18c\ub85c Home Assistant\uc5d0 \ud478\uc2dc \uc5c5\ub370\uc774\ud2b8\ub97c \ubcf4\ub0b4\ub3c4\ub85d \uad6c\uc131\ub429\ub2c8\ub2e4. \n > {webhook_url} \n\n\uc774 \uad6c\uc131\uc774 \uc62c\ubc14\ub974\uc9c0 \uc54a\ub2e4\uba74 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud558\uace0 Home Assistant\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
"title": "\ucf5c\ubc31 URL \ud655\uc778\ud558\uae30"
}
}
diff --git a/homeassistant/components/smartthings/translations/nl.json b/homeassistant/components/smartthings/translations/nl.json
index a77ad40f0caa98..6c141b624d28e3 100644
--- a/homeassistant/components/smartthings/translations/nl.json
+++ b/homeassistant/components/smartthings/translations/nl.json
@@ -9,7 +9,7 @@
"token_forbidden": "Het token heeft niet de vereiste OAuth-scopes.",
"token_invalid_format": "Het token moet de UID/GUID-indeling hebben",
"token_unauthorized": "Het token is ongeldig of niet langer geautoriseerd.",
- "webhook_error": "SmartThings kon het in 'base_url` geconfigureerde endpoint niet goedkeuren. Lees de componentvereisten door."
+ "webhook_error": "SmartThings kan de webhook URL niet valideren. Zorg ervoor dat de webhook URL bereikbaar is vanaf het internet en probeer het opnieuw."
},
"step": {
"authorize": {
@@ -30,8 +30,8 @@
"title": "Locatie selecteren"
},
"user": {
- "description": "Voer een SmartThings [Personal Access Token]({token_url}) in die is aangemaakt volgens de [instructies]({component_url}).",
- "title": "Persoonlijk toegangstoken invoeren"
+ "description": "SmartThings zal worden geconfigureerd om push updates te sturen naar Home Assistant op:\n> {webhook_url}\n\nAls dit niet correct is, werk dan uw configuratie bij, start Home Assistant opnieuw op en probeer het opnieuw.",
+ "title": "Bevestig Callback URL"
}
}
}
diff --git a/homeassistant/components/smartthings/translations/tr.json b/homeassistant/components/smartthings/translations/tr.json
new file mode 100644
index 00000000000000..5e7463c1c7443e
--- /dev/null
+++ b/homeassistant/components/smartthings/translations/tr.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "webhook_error": "SmartThings, webhook URL'sini do\u011frulayamad\u0131. L\u00fctfen webhook URL'sinin internetten eri\u015filebilir oldu\u011fundan emin olun ve tekrar deneyin."
+ },
+ "step": {
+ "pat": {
+ "data": {
+ "access_token": "Eri\u015fim Belirteci"
+ }
+ },
+ "select_location": {
+ "title": "Konum Se\u00e7in"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smartthings/translations/uk.json b/homeassistant/components/smartthings/translations/uk.json
new file mode 100644
index 00000000000000..6f8a0ed47446ec
--- /dev/null
+++ b/homeassistant/components/smartthings/translations/uk.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "invalid_webhook_url": "Webhook URL, \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u0439 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u044c \u0432\u0456\u0434 SmartThings, \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439:\n > {webhook_url} \n\n\u041e\u043d\u043e\u0432\u0456\u0442\u044c \u0432\u0430\u0448\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u043d\u043e \u0434\u043e [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0439] ({component_url}), \u0430 \u043f\u0456\u0441\u043b\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0443 Home Assistant \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.",
+ "no_available_locations": "\u041d\u0435\u043c\u0430\u0454 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u0456\u0441\u0446\u044c \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f SmartThings."
+ },
+ "error": {
+ "app_setup_error": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 SmartApp. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.",
+ "token_forbidden": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435 \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u0438\u0439 \u0434\u043b\u044f OAuth.",
+ "token_invalid_format": "\u0422\u043e\u043a\u0435\u043d \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 UID / GUID.",
+ "token_unauthorized": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439 \u0430\u0431\u043e \u0431\u0456\u043b\u044c\u0448\u0435 \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u0438\u0439.",
+ "webhook_error": "SmartThings \u043d\u0435 \u043c\u043e\u0436\u0435 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0438\u0442\u0438 Webhook URL. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u0439 Webhook URL \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0456\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0456 \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437."
+ },
+ "step": {
+ "authorize": {
+ "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f Home Assistant"
+ },
+ "pat": {
+ "data": {
+ "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c [\u041e\u0441\u043e\u0431\u0438\u0441\u0442\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 SmartThings] ({token_url}), \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u0438\u0439 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u043d\u043e \u0434\u043e [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0457] ({component_url}).",
+ "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443"
+ },
+ "select_location": {
+ "data": {
+ "location_id": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u0456\u0441\u0446\u0435 \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f SmartThings, \u044f\u043a\u0438\u0439 \u0432\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u0432 Home Assistant. \u041f\u0456\u0441\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u0432\u0456\u0434\u043a\u0440\u0438\u0454\u0442\u044c\u0441\u044f \u043d\u043e\u0432\u0435 \u0432\u0456\u043a\u043d\u043e, \u0434\u0435 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0431\u0443\u0434\u0435 \u0443\u0432\u0456\u0439\u0442\u0438 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0442\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 Home Assistant \u0432 \u043e\u0431\u0440\u0430\u043d\u043e\u043c\u0443 \u043c\u0456\u0441\u0446\u0456 \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f.",
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f"
+ },
+ "user": {
+ "description": "SmartThings \u0431\u0443\u0434\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439 \u0434\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 push-\u043e\u043d\u043e\u0432\u043b\u0435\u043d\u044c \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e:\n> {webhook_url} \n\n\u042f\u043a\u0449\u043e \u0446\u0435 \u043d\u0435 \u0442\u0430\u043a, \u043e\u043d\u043e\u0432\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e, \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c Home Assistant \u0456 \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.",
+ "title": "\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f Callback URL"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smartthings/translations/zh-Hant.json b/homeassistant/components/smartthings/translations/zh-Hant.json
index d9a17e460582c3..88360c756781a6 100644
--- a/homeassistant/components/smartthings/translations/zh-Hant.json
+++ b/homeassistant/components/smartthings/translations/zh-Hant.json
@@ -6,9 +6,9 @@
},
"error": {
"app_setup_error": "\u7121\u6cd5\u8a2d\u5b9a SmartApp\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002",
- "token_forbidden": "\u5bc6\u9470\u4e0d\u5177\u6240\u9700\u7684 OAuth \u7bc4\u570d\u3002",
- "token_invalid_format": "\u5bc6\u9470\u5fc5\u9808\u70ba UID/GUID \u683c\u5f0f",
- "token_unauthorized": "\u5bc6\u9470\u7121\u6548\u6216\u4e0d\u518d\u5177\u6709\u6388\u6b0a\u3002",
+ "token_forbidden": "\u6b0a\u6756\u4e0d\u5177\u6240\u9700\u7684 OAuth \u7bc4\u570d\u3002",
+ "token_invalid_format": "\u6b0a\u6756\u5fc5\u9808\u70ba UID/GUID \u683c\u5f0f",
+ "token_unauthorized": "\u6b0a\u6756\u7121\u6548\u6216\u4e0d\u518d\u5177\u6709\u6388\u6b0a\u3002",
"webhook_error": "SmartThings \u7121\u6cd5\u8a8d\u8b49 Webhook URL\u3002\u8acb\u78ba\u8a8d Webhook URL \u53ef\u7531\u7db2\u8def\u5b58\u53d6\u5f8c\u518d\u8a66\u4e00\u6b21\u3002"
},
"step": {
@@ -17,10 +17,10 @@
},
"pat": {
"data": {
- "access_token": "\u5b58\u53d6\u5bc6\u9470"
+ "access_token": "\u5b58\u53d6\u6b0a\u6756"
},
- "description": "\u8acb\u8f38\u5165\u8ddf\u96a8\u6b64[\u6559\u5b78]({component_url}) \u6240\u5efa\u7acb\u7684 SmartThings [\u500b\u4eba\u5b58\u53d6\u5bc6\u9470]({token_url})\u3002\u5c07\u4f7f\u7528 SmartThings \u5e33\u865f\u65b0\u589e Home Assistant \u6574\u5408\u3002",
- "title": "\u8f38\u5165\u500b\u4eba\u5b58\u53d6\u5bc6\u9470"
+ "description": "\u8acb\u8f38\u5165\u8ddf\u96a8\u6b64[\u6559\u5b78]({component_url}) \u6240\u5efa\u7acb\u7684 SmartThings [\u500b\u4eba\u5b58\u53d6\u6b0a\u6756]({token_url})\u3002\u5c07\u4f7f\u7528 SmartThings \u5e33\u865f\u65b0\u589e Home Assistant \u6574\u5408\u3002",
+ "title": "\u8f38\u5165\u500b\u4eba\u5b58\u53d6\u6b0a\u6756"
},
"select_location": {
"data": {
diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py
new file mode 100644
index 00000000000000..457af4b7bc0d9e
--- /dev/null
+++ b/homeassistant/components/smarttub/__init__.py
@@ -0,0 +1,54 @@
+"""SmartTub integration."""
+import asyncio
+import logging
+
+from .const import DOMAIN, SMARTTUB_CONTROLLER
+from .controller import SmartTubController
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORMS = ["binary_sensor", "climate", "light", "sensor", "switch"]
+
+
+async def async_setup(hass, config):
+ """Set up smarttub component."""
+
+ hass.data.setdefault(DOMAIN, {})
+
+ return True
+
+
+async def async_setup_entry(hass, entry):
+ """Set up a smarttub config entry."""
+
+ controller = SmartTubController(hass)
+ hass.data[DOMAIN][entry.entry_id] = {
+ SMARTTUB_CONTROLLER: controller,
+ }
+
+ if not await controller.async_setup_entry(entry):
+ return False
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass, entry):
+ """Remove a smarttub config entry."""
+ if not all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ ):
+ return False
+
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return True
diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py
new file mode 100644
index 00000000000000..bbeece366551e6
--- /dev/null
+++ b/homeassistant/components/smarttub/binary_sensor.py
@@ -0,0 +1,92 @@
+"""Platform for binary sensor integration."""
+import logging
+
+from smarttub import SpaReminder
+
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
+ DEVICE_CLASS_PROBLEM,
+ BinarySensorEntity,
+)
+
+from .const import ATTR_REMINDERS, DOMAIN, SMARTTUB_CONTROLLER
+from .entity import SmartTubEntity, SmartTubSensorBase
+
+_LOGGER = logging.getLogger(__name__)
+
+# whether the reminder has been snoozed (bool)
+ATTR_REMINDER_SNOOZED = "snoozed"
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up binary sensor entities for the binary sensors in the tub."""
+
+ controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER]
+
+ entities = []
+ for spa in controller.spas:
+ entities.append(SmartTubOnline(controller.coordinator, spa))
+ entities.extend(
+ SmartTubReminder(controller.coordinator, spa, reminder)
+ for reminder in controller.coordinator.data[spa.id][ATTR_REMINDERS].values()
+ )
+
+ async_add_entities(entities)
+
+
+class SmartTubOnline(SmartTubSensorBase, BinarySensorEntity):
+ """A binary sensor indicating whether the spa is currently online (connected to the cloud)."""
+
+ def __init__(self, coordinator, spa):
+ """Initialize the entity."""
+ super().__init__(coordinator, spa, "Online", "online")
+
+ @property
+ def is_on(self) -> bool:
+ """Return true if the binary sensor is on."""
+ return self._state is True
+
+ @property
+ def device_class(self) -> str:
+ """Return the device class for this entity."""
+ return DEVICE_CLASS_CONNECTIVITY
+
+
+class SmartTubReminder(SmartTubEntity, BinarySensorEntity):
+ """Reminders for maintenance actions."""
+
+ def __init__(self, coordinator, spa, reminder):
+ """Initialize the entity."""
+ super().__init__(
+ coordinator,
+ spa,
+ f"{reminder.name.title()} Reminder",
+ )
+ self.reminder_id = reminder.id
+
+ @property
+ def unique_id(self):
+ """Return a unique id for this sensor."""
+ return f"{self.spa.id}-reminder-{self.reminder_id}"
+
+ @property
+ def reminder(self) -> SpaReminder:
+ """Return the underlying SpaReminder object for this entity."""
+ return self.coordinator.data[self.spa.id]["reminders"][self.reminder_id]
+
+ @property
+ def is_on(self) -> bool:
+ """Return whether the specified maintenance action needs to be taken."""
+ return self.reminder.remaining_days == 0
+
+ @property
+ def extra_state_attributes(self):
+ """Return the state attributes."""
+ return {
+ ATTR_REMINDER_SNOOZED: self.reminder.snoozed,
+ }
+
+ @property
+ def device_class(self) -> str:
+ """Return the device class for this entity."""
+ return DEVICE_CLASS_PROBLEM
diff --git a/homeassistant/components/smarttub/climate.py b/homeassistant/components/smarttub/climate.py
new file mode 100644
index 00000000000000..be564c84a94ea8
--- /dev/null
+++ b/homeassistant/components/smarttub/climate.py
@@ -0,0 +1,142 @@
+"""Platform for climate integration."""
+import logging
+
+from smarttub import Spa
+
+from homeassistant.components.climate import ClimateEntity
+from homeassistant.components.climate.const import (
+ CURRENT_HVAC_HEAT,
+ CURRENT_HVAC_IDLE,
+ HVAC_MODE_HEAT,
+ PRESET_ECO,
+ PRESET_NONE,
+ SUPPORT_PRESET_MODE,
+ SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
+from homeassistant.util.temperature import convert as convert_temperature
+
+from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, SMARTTUB_CONTROLLER
+from .entity import SmartTubEntity
+
+_LOGGER = logging.getLogger(__name__)
+
+PRESET_DAY = "day"
+
+PRESET_MODES = {
+ Spa.HeatMode.AUTO: PRESET_NONE,
+ Spa.HeatMode.ECONOMY: PRESET_ECO,
+ Spa.HeatMode.DAY: PRESET_DAY,
+}
+
+HEAT_MODES = {v: k for k, v in PRESET_MODES.items()}
+
+HVAC_ACTIONS = {
+ "OFF": CURRENT_HVAC_IDLE,
+ "ON": CURRENT_HVAC_HEAT,
+}
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up climate entity for the thermostat in the tub."""
+
+ controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER]
+
+ entities = [
+ SmartTubThermostat(controller.coordinator, spa) for spa in controller.spas
+ ]
+
+ async_add_entities(entities)
+
+
+class SmartTubThermostat(SmartTubEntity, ClimateEntity):
+ """The target water temperature for the spa."""
+
+ def __init__(self, coordinator, spa):
+ """Initialize the entity."""
+ super().__init__(coordinator, spa, "Thermostat")
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement used by the platform."""
+ return TEMP_CELSIUS
+
+ @property
+ def hvac_action(self):
+ """Return the current running hvac operation."""
+ return HVAC_ACTIONS.get(self.spa_status.heater)
+
+ @property
+ def hvac_modes(self):
+ """Return the list of available hvac operation modes."""
+ return [HVAC_MODE_HEAT]
+
+ @property
+ def hvac_mode(self):
+ """Return the current hvac mode.
+
+ SmartTub devices don't seem to have the option of disabling the heater,
+ so this is always HVAC_MODE_HEAT.
+ """
+ return HVAC_MODE_HEAT
+
+ async def async_set_hvac_mode(self, hvac_mode: str):
+ """Set new target hvac mode.
+
+ As with hvac_mode, we don't really have an option here.
+ """
+ if hvac_mode == HVAC_MODE_HEAT:
+ return
+ raise NotImplementedError(hvac_mode)
+
+ @property
+ def min_temp(self):
+ """Return the minimum temperature."""
+ min_temp = DEFAULT_MIN_TEMP
+ return convert_temperature(min_temp, TEMP_CELSIUS, self.temperature_unit)
+
+ @property
+ def max_temp(self):
+ """Return the maximum temperature."""
+ max_temp = DEFAULT_MAX_TEMP
+ return convert_temperature(max_temp, TEMP_CELSIUS, self.temperature_unit)
+
+ @property
+ def supported_features(self):
+ """Return the set of supported features.
+
+ Only target temperature is supported.
+ """
+ return SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE
+
+ @property
+ def preset_mode(self):
+ """Return the current preset mode."""
+ return PRESET_MODES[self.spa_status.heat_mode]
+
+ @property
+ def preset_modes(self):
+ """Return the available preset modes."""
+ return list(PRESET_MODES.values())
+
+ @property
+ def current_temperature(self):
+ """Return the current water temperature."""
+ return self.spa_status.water.temperature
+
+ @property
+ def target_temperature(self):
+ """Return the target water temperature."""
+ return self.spa_status.set_temperature
+
+ async def async_set_temperature(self, **kwargs):
+ """Set new target temperature."""
+ temperature = kwargs[ATTR_TEMPERATURE]
+ await self.spa.set_temperature(temperature)
+ await self.coordinator.async_refresh()
+
+ async def async_set_preset_mode(self, preset_mode: str):
+ """Activate the specified preset mode."""
+ heat_mode = HEAT_MODES[preset_mode]
+ await self.spa.set_heat_mode(heat_mode)
+ await self.coordinator.async_refresh()
diff --git a/homeassistant/components/smarttub/config_flow.py b/homeassistant/components/smarttub/config_flow.py
new file mode 100644
index 00000000000000..d3349060a07615
--- /dev/null
+++ b/homeassistant/components/smarttub/config_flow.py
@@ -0,0 +1,53 @@
+"""Config flow to configure the SmartTub integration."""
+import logging
+
+from smarttub import LoginFailed
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
+
+from .const import DOMAIN
+from .controller import SmartTubController
+
+DATA_SCHEMA = vol.Schema(
+ {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str}
+)
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class SmartTubConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """SmartTub configuration flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initiated by the user."""
+ errors = {}
+
+ if user_input is None:
+ return self.async_show_form(
+ step_id="user", data_schema=DATA_SCHEMA, errors=errors
+ )
+
+ controller = SmartTubController(self.hass)
+ try:
+ account = await controller.login(
+ user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
+ )
+ except LoginFailed:
+ errors["base"] = "invalid_auth"
+ return self.async_show_form(
+ step_id="user", data_schema=DATA_SCHEMA, errors=errors
+ )
+
+ existing_entry = await self.async_set_unique_id(account.id)
+ if existing_entry:
+ self.hass.config_entries.async_update_entry(existing_entry, data=user_input)
+ await self.hass.config_entries.async_reload(existing_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
+
+ return self.async_create_entry(title=user_input[CONF_EMAIL], data=user_input)
diff --git a/homeassistant/components/smarttub/const.py b/homeassistant/components/smarttub/const.py
new file mode 100644
index 00000000000000..23bd8bd8ec0da3
--- /dev/null
+++ b/homeassistant/components/smarttub/const.py
@@ -0,0 +1,27 @@
+"""smarttub constants."""
+
+DOMAIN = "smarttub"
+
+EVENT_SMARTTUB = "smarttub"
+
+SMARTTUB_CONTROLLER = "smarttub_controller"
+
+SCAN_INTERVAL = 60
+
+POLLING_TIMEOUT = 10
+API_TIMEOUT = 5
+
+DEFAULT_MIN_TEMP = 18.5
+DEFAULT_MAX_TEMP = 40
+
+# the device doesn't remember any state for the light, so we have to choose a
+# mode (smarttub.SpaLight.LightMode) when turning it on. There is no white
+# mode.
+DEFAULT_LIGHT_EFFECT = "purple"
+# default to 50% brightness
+DEFAULT_LIGHT_BRIGHTNESS = 128
+
+ATTR_LIGHTS = "lights"
+ATTR_PUMPS = "pumps"
+ATTR_REMINDERS = "reminders"
+ATTR_STATUS = "status"
diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py
new file mode 100644
index 00000000000000..8139c72ab6e954
--- /dev/null
+++ b/homeassistant/components/smarttub/controller.py
@@ -0,0 +1,132 @@
+"""Interface to the SmartTub API."""
+
+import asyncio
+from datetime import timedelta
+import logging
+
+from aiohttp import client_exceptions
+import async_timeout
+from smarttub import APIError, LoginFailed, SmartTub
+from smarttub.api import Account
+
+from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import device_registry as dr
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import (
+ ATTR_LIGHTS,
+ ATTR_PUMPS,
+ ATTR_REMINDERS,
+ ATTR_STATUS,
+ DOMAIN,
+ POLLING_TIMEOUT,
+ SCAN_INTERVAL,
+)
+from .helpers import get_spa_name
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class SmartTubController:
+ """Interface between Home Assistant and the SmartTub API."""
+
+ def __init__(self, hass):
+ """Initialize an interface to SmartTub."""
+ self._hass = hass
+ self._account = None
+ self.spas = set()
+ self._spa_devices = {}
+
+ self.coordinator = None
+
+ async def async_setup_entry(self, entry):
+ """Perform initial setup.
+
+ Authenticate, query static state, set up polling, and otherwise make
+ ready for normal operations .
+ """
+
+ try:
+ self._account = await self.login(
+ entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD]
+ )
+ except LoginFailed:
+ # credentials were changed or invalidated, we need new ones
+
+ return False
+ except (
+ asyncio.TimeoutError,
+ client_exceptions.ClientOSError,
+ client_exceptions.ServerDisconnectedError,
+ client_exceptions.ContentTypeError,
+ ) as err:
+ raise ConfigEntryNotReady from err
+
+ self.spas = await self._account.get_spas()
+
+ self.coordinator = DataUpdateCoordinator(
+ self._hass,
+ _LOGGER,
+ name=DOMAIN,
+ update_method=self.async_update_data,
+ update_interval=timedelta(seconds=SCAN_INTERVAL),
+ )
+
+ await self.coordinator.async_refresh()
+
+ await self.async_register_devices(entry)
+
+ return True
+
+ async def async_update_data(self):
+ """Query the API and return the new state."""
+
+ data = {}
+ try:
+ async with async_timeout.timeout(POLLING_TIMEOUT):
+ for spa in self.spas:
+ data[spa.id] = await self._get_spa_data(spa)
+ except APIError as err:
+ raise UpdateFailed(err) from err
+
+ return data
+
+ async def _get_spa_data(self, spa):
+ status, pumps, lights, reminders = await asyncio.gather(
+ spa.get_status(),
+ spa.get_pumps(),
+ spa.get_lights(),
+ spa.get_reminders(),
+ )
+ return {
+ ATTR_STATUS: status,
+ ATTR_PUMPS: {pump.id: pump for pump in pumps},
+ ATTR_LIGHTS: {light.zone: light for light in lights},
+ ATTR_REMINDERS: {reminder.id: reminder for reminder in reminders},
+ }
+
+ async def async_register_devices(self, entry):
+ """Register devices with the device registry for all spas."""
+ device_registry = await dr.async_get_registry(self._hass)
+ for spa in self.spas:
+ device = device_registry.async_get_or_create(
+ config_entry_id=entry.entry_id,
+ identifiers={(DOMAIN, spa.id)},
+ manufacturer=spa.brand,
+ name=get_spa_name(spa),
+ model=spa.model,
+ )
+ self._spa_devices[spa.id] = device
+
+ async def login(self, email, password) -> Account:
+ """Retrieve the account corresponding to the specified email and password.
+
+ Returns None if the credentials are invalid.
+ """
+
+ api = SmartTub(async_get_clientsession(self._hass))
+
+ await api.login(email, password)
+ return await api.get_account()
diff --git a/homeassistant/components/smarttub/entity.py b/homeassistant/components/smarttub/entity.py
new file mode 100644
index 00000000000000..8be956a2b70e9d
--- /dev/null
+++ b/homeassistant/components/smarttub/entity.py
@@ -0,0 +1,71 @@
+"""SmartTub integration."""
+import logging
+
+import smarttub
+
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+)
+
+from .const import DOMAIN
+from .helpers import get_spa_name
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class SmartTubEntity(CoordinatorEntity):
+ """Base class for SmartTub entities."""
+
+ def __init__(
+ self, coordinator: DataUpdateCoordinator, spa: smarttub.Spa, entity_type
+ ):
+ """Initialize the entity.
+
+ Given a spa id and a short name for the entity, we provide basic device
+ info, name, unique id, etc. for all derived entities.
+ """
+
+ super().__init__(coordinator)
+ self.spa = spa
+ self._entity_type = entity_type
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique id for the entity."""
+ return f"{self.spa.id}-{self._entity_type}"
+
+ @property
+ def device_info(self) -> str:
+ """Return device info."""
+ return {
+ "identifiers": {(DOMAIN, self.spa.id)},
+ "manufacturer": self.spa.brand,
+ "model": self.spa.model,
+ }
+
+ @property
+ def name(self) -> str:
+ """Return the name of the entity."""
+ spa_name = get_spa_name(self.spa)
+ return f"{spa_name} {self._entity_type}"
+
+ @property
+ def spa_status(self) -> smarttub.SpaState:
+ """Retrieve the result of Spa.get_status()."""
+
+ return self.coordinator.data[self.spa.id].get("status")
+
+
+class SmartTubSensorBase(SmartTubEntity):
+ """Base class for SmartTub sensors."""
+
+ def __init__(self, coordinator, spa, sensor_name, attr_name):
+ """Initialize the entity."""
+ super().__init__(coordinator, spa, sensor_name)
+ self._attr_name = attr_name
+
+ @property
+ def _state(self):
+ """Retrieve the underlying state from the spa."""
+ return getattr(self.spa_status, self._attr_name)
diff --git a/homeassistant/components/smarttub/helpers.py b/homeassistant/components/smarttub/helpers.py
new file mode 100644
index 00000000000000..a6f2d09c38f18b
--- /dev/null
+++ b/homeassistant/components/smarttub/helpers.py
@@ -0,0 +1,8 @@
+"""Helper functions for SmartTub integration."""
+
+import smarttub
+
+
+def get_spa_name(spa: smarttub.Spa) -> str:
+ """Return the name of the specified spa."""
+ return f"{spa.brand} {spa.model}"
diff --git a/homeassistant/components/smarttub/light.py b/homeassistant/components/smarttub/light.py
new file mode 100644
index 00000000000000..57acf583415146
--- /dev/null
+++ b/homeassistant/components/smarttub/light.py
@@ -0,0 +1,142 @@
+"""Platform for light integration."""
+import logging
+
+from smarttub import SpaLight
+
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS,
+ ATTR_EFFECT,
+ EFFECT_COLORLOOP,
+ SUPPORT_BRIGHTNESS,
+ SUPPORT_EFFECT,
+ LightEntity,
+)
+
+from .const import (
+ ATTR_LIGHTS,
+ DEFAULT_LIGHT_BRIGHTNESS,
+ DEFAULT_LIGHT_EFFECT,
+ DOMAIN,
+ SMARTTUB_CONTROLLER,
+)
+from .entity import SmartTubEntity
+from .helpers import get_spa_name
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up entities for any lights in the tub."""
+
+ controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER]
+
+ entities = [
+ SmartTubLight(controller.coordinator, light)
+ for spa in controller.spas
+ for light in controller.coordinator.data[spa.id][ATTR_LIGHTS].values()
+ ]
+
+ async_add_entities(entities)
+
+
+class SmartTubLight(SmartTubEntity, LightEntity):
+ """A light on a spa."""
+
+ def __init__(self, coordinator, light):
+ """Initialize the entity."""
+ super().__init__(coordinator, light.spa, "light")
+ self.light_zone = light.zone
+
+ @property
+ def light(self) -> SpaLight:
+ """Return the underlying SpaLight object for this entity."""
+ return self.coordinator.data[self.spa.id]["lights"][self.light_zone]
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique ID for this light entity."""
+ return f"{super().unique_id}-{self.light_zone}"
+
+ @property
+ def name(self) -> str:
+ """Return a name for this light entity."""
+ spa_name = get_spa_name(self.spa)
+ return f"{spa_name} Light {self.light_zone}"
+
+ @property
+ def brightness(self):
+ """Return the brightness of this light between 0..255."""
+
+ # SmartTub intensity is 0..100
+ return self._smarttub_to_hass_brightness(self.light.intensity)
+
+ @staticmethod
+ def _smarttub_to_hass_brightness(intensity):
+ if intensity in (0, 1):
+ return 0
+ return round(intensity * 255 / 100)
+
+ @staticmethod
+ def _hass_to_smarttub_brightness(brightness):
+ return round(brightness * 100 / 255)
+
+ @property
+ def is_on(self):
+ """Return true if the light is on."""
+ return self.light.mode != SpaLight.LightMode.OFF
+
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ return SUPPORT_BRIGHTNESS | SUPPORT_EFFECT
+
+ @property
+ def effect(self):
+ """Return the current effect."""
+ mode = self.light.mode.name.lower()
+ if mode in self.effect_list:
+ return mode
+ return None
+
+ @property
+ def effect_list(self):
+ """Return the list of supported effects."""
+ effects = [
+ effect
+ for effect in map(self._light_mode_to_effect, SpaLight.LightMode)
+ if effect is not None
+ ]
+
+ return effects
+
+ @staticmethod
+ def _light_mode_to_effect(light_mode: SpaLight.LightMode):
+ if light_mode == SpaLight.LightMode.OFF:
+ return None
+ if light_mode == SpaLight.LightMode.HIGH_SPEED_COLOR_WHEEL:
+ return EFFECT_COLORLOOP
+
+ return light_mode.name.lower()
+
+ @staticmethod
+ def _effect_to_light_mode(effect):
+ if effect == EFFECT_COLORLOOP:
+ return SpaLight.LightMode.HIGH_SPEED_COLOR_WHEEL
+
+ return SpaLight.LightMode[effect.upper()]
+
+ async def async_turn_on(self, **kwargs):
+ """Turn the light on."""
+
+ mode = self._effect_to_light_mode(kwargs.get(ATTR_EFFECT, DEFAULT_LIGHT_EFFECT))
+ intensity = self._hass_to_smarttub_brightness(
+ kwargs.get(ATTR_BRIGHTNESS, DEFAULT_LIGHT_BRIGHTNESS)
+ )
+
+ await self.light.set_mode(mode, intensity)
+ await self.coordinator.async_request_refresh()
+
+ async def async_turn_off(self, **kwargs):
+ """Turn the light off."""
+ await self.light.set_mode(SpaLight.LightMode.OFF, 0)
+ await self.coordinator.async_request_refresh()
diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json
new file mode 100644
index 00000000000000..2425268e05c75b
--- /dev/null
+++ b/homeassistant/components/smarttub/manifest.json
@@ -0,0 +1,12 @@
+{
+ "domain": "smarttub",
+ "name": "SmartTub",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/smarttub",
+ "dependencies": [],
+ "codeowners": ["@mdz"],
+ "requirements": [
+ "python-smarttub==0.0.19"
+ ],
+ "quality_scale": "platinum"
+}
diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py
new file mode 100644
index 00000000000000..ea803c9862b8a3
--- /dev/null
+++ b/homeassistant/components/smarttub/sensor.py
@@ -0,0 +1,107 @@
+"""Platform for sensor integration."""
+from enum import Enum
+import logging
+
+from homeassistant.components.sensor import SensorEntity
+
+from .const import DOMAIN, SMARTTUB_CONTROLLER
+from .entity import SmartTubSensorBase
+
+_LOGGER = logging.getLogger(__name__)
+
+# the desired duration, in hours, of the cycle
+ATTR_DURATION = "duration"
+ATTR_CYCLE_LAST_UPDATED = "cycle_last_updated"
+ATTR_MODE = "mode"
+# the hour of the day at which to start the cycle (0-23)
+ATTR_START_HOUR = "start_hour"
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up sensor entities for the sensors in the tub."""
+
+ controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER]
+
+ entities = []
+ for spa in controller.spas:
+ entities.extend(
+ [
+ SmartTubSensor(controller.coordinator, spa, "State", "state"),
+ SmartTubSensor(
+ controller.coordinator, spa, "Flow Switch", "flow_switch"
+ ),
+ SmartTubSensor(controller.coordinator, spa, "Ozone", "ozone"),
+ SmartTubSensor(controller.coordinator, spa, "UV", "uv"),
+ SmartTubSensor(
+ controller.coordinator, spa, "Blowout Cycle", "blowout_cycle"
+ ),
+ SmartTubSensor(
+ controller.coordinator, spa, "Cleanup Cycle", "cleanup_cycle"
+ ),
+ SmartTubPrimaryFiltrationCycle(controller.coordinator, spa),
+ SmartTubSecondaryFiltrationCycle(controller.coordinator, spa),
+ ]
+ )
+
+ async_add_entities(entities)
+
+
+class SmartTubSensor(SmartTubSensorBase, SensorEntity):
+ """Generic class for SmartTub status sensors."""
+
+ @property
+ def state(self) -> str:
+ """Return the current state of the sensor."""
+ if isinstance(self._state, Enum):
+ return self._state.name.lower()
+ return self._state.lower()
+
+
+class SmartTubPrimaryFiltrationCycle(SmartTubSensor):
+ """The primary filtration cycle."""
+
+ def __init__(self, coordinator, spa):
+ """Initialize the entity."""
+ super().__init__(
+ coordinator, spa, "Primary Filtration Cycle", "primary_filtration"
+ )
+
+ @property
+ def state(self) -> str:
+ """Return the current state of the sensor."""
+ return self._state.status.name.lower()
+
+ @property
+ def extra_state_attributes(self):
+ """Return the state attributes."""
+ state = self._state
+ return {
+ ATTR_DURATION: state.duration,
+ ATTR_CYCLE_LAST_UPDATED: state.last_updated.isoformat(),
+ ATTR_MODE: state.mode.name.lower(),
+ ATTR_START_HOUR: state.start_hour,
+ }
+
+
+class SmartTubSecondaryFiltrationCycle(SmartTubSensor):
+ """The secondary filtration cycle."""
+
+ def __init__(self, coordinator, spa):
+ """Initialize the entity."""
+ super().__init__(
+ coordinator, spa, "Secondary Filtration Cycle", "secondary_filtration"
+ )
+
+ @property
+ def state(self) -> str:
+ """Return the current state of the sensor."""
+ return self._state.status.name.lower()
+
+ @property
+ def extra_state_attributes(self):
+ """Return the state attributes."""
+ state = self._state
+ return {
+ ATTR_CYCLE_LAST_UPDATED: state.last_updated.isoformat(),
+ ATTR_MODE: state.mode.name.lower(),
+ }
diff --git a/homeassistant/components/smarttub/strings.json b/homeassistant/components/smarttub/strings.json
new file mode 100644
index 00000000000000..8ba888a9ffb3a1
--- /dev/null
+++ b/homeassistant/components/smarttub/strings.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "Login",
+ "description": "Enter your SmartTub email address and password to login",
+ "data": {
+ "email": "[%key:common::config_flow::data::email%]",
+ "password": "[%key:common::config_flow::data::password%]"
+ }
+ }
+ },
+ "error": {
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ }
+ }
+}
diff --git a/homeassistant/components/smarttub/switch.py b/homeassistant/components/smarttub/switch.py
new file mode 100644
index 00000000000000..26239df9dff799
--- /dev/null
+++ b/homeassistant/components/smarttub/switch.py
@@ -0,0 +1,82 @@
+"""Platform for switch integration."""
+import logging
+
+import async_timeout
+from smarttub import SpaPump
+
+from homeassistant.components.switch import SwitchEntity
+
+from .const import API_TIMEOUT, ATTR_PUMPS, DOMAIN, SMARTTUB_CONTROLLER
+from .entity import SmartTubEntity
+from .helpers import get_spa_name
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up switch entities for the pumps on the tub."""
+
+ controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER]
+
+ entities = [
+ SmartTubPump(controller.coordinator, pump)
+ for spa in controller.spas
+ for pump in controller.coordinator.data[spa.id][ATTR_PUMPS].values()
+ ]
+
+ async_add_entities(entities)
+
+
+class SmartTubPump(SmartTubEntity, SwitchEntity):
+ """A pump on a spa."""
+
+ def __init__(self, coordinator, pump: SpaPump):
+ """Initialize the entity."""
+ super().__init__(coordinator, pump.spa, "pump")
+ self.pump_id = pump.id
+ self.pump_type = pump.type
+
+ @property
+ def pump(self) -> SpaPump:
+ """Return the underlying SpaPump object for this entity."""
+ return self.coordinator.data[self.spa.id]["pumps"][self.pump_id]
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique ID for this pump entity."""
+ return f"{super().unique_id}-{self.pump_id}"
+
+ @property
+ def name(self) -> str:
+ """Return a name for this pump entity."""
+ spa_name = get_spa_name(self.spa)
+ if self.pump_type == SpaPump.PumpType.CIRCULATION:
+ return f"{spa_name} Circulation Pump"
+ if self.pump_type == SpaPump.PumpType.JET:
+ return f"{spa_name} Jet {self.pump_id}"
+ return f"{spa_name} pump {self.pump_id}"
+
+ @property
+ def is_on(self) -> bool:
+ """Return True if the pump is on."""
+ return self.pump.state != SpaPump.PumpState.OFF
+
+ async def async_turn_on(self, **kwargs) -> None:
+ """Turn the pump on."""
+
+ # the API only supports toggling
+ if not self.is_on:
+ await self.async_toggle()
+
+ async def async_turn_off(self, **kwargs) -> None:
+ """Turn the pump off."""
+
+ # the API only supports toggling
+ if self.is_on:
+ await self.async_toggle()
+
+ async def async_toggle(self, **kwargs) -> None:
+ """Toggle the pump on or off."""
+ async with async_timeout.timeout(API_TIMEOUT):
+ await self.pump.toggle()
+ await self.coordinator.async_request_refresh()
diff --git a/homeassistant/components/smarttub/translations/bg.json b/homeassistant/components/smarttub/translations/bg.json
new file mode 100644
index 00000000000000..05ef3ed780e750
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/bg.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "error": {
+ "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"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarttub/translations/ca.json b/homeassistant/components/smarttub/translations/ca.json
new file mode 100644
index 00000000000000..6d882abeee6583
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/ca.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament"
+ },
+ "error": {
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Correu electr\u00f2nic",
+ "password": "Contrasenya"
+ },
+ "description": "Introdueix el correu electr\u00f2nic i la contrasenya de SmartTub per iniciar sessi\u00f3",
+ "title": "Inici de sessi\u00f3"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarttub/translations/cs.json b/homeassistant/components/smarttub/translations/cs.json
new file mode 100644
index 00000000000000..6be2df92286601
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/cs.json
@@ -0,0 +1,21 @@
+{
+ "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": {
+ "email": "E-mail",
+ "password": "Heslo"
+ },
+ "title": "P\u0159ihl\u00e1\u0161en\u00ed"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarttub/translations/de.json b/homeassistant/components/smarttub/translations/de.json
new file mode 100644
index 00000000000000..8e608193b81f71
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/de.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
+ },
+ "error": {
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-Mail",
+ "password": "Passwort"
+ },
+ "description": "Gib deine SmartTub E-Mail-Adresse und Passwort f\u00fcr die Anmeldung ein",
+ "title": "Anmeldung"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarttub/translations/en.json b/homeassistant/components/smarttub/translations/en.json
new file mode 100644
index 00000000000000..4cf930918875b4
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/en.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured",
+ "reauth_successful": "Re-authentication was successful"
+ },
+ "error": {
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Email",
+ "password": "Password"
+ },
+ "description": "Enter your SmartTub email address and password to login",
+ "title": "Login"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarttub/translations/es.json b/homeassistant/components/smarttub/translations/es.json
new file mode 100644
index 00000000000000..df5b4122bc4449
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/es.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "description": "Introduzca su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a de SmartTub para iniciar sesi\u00f3n",
+ "title": "Inicio de sesi\u00f3n"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarttub/translations/et.json b/homeassistant/components/smarttub/translations/et.json
new file mode 100644
index 00000000000000..676edee158421f
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/et.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "reauth_successful": "Taastuvastamine \u00f5nnestus"
+ },
+ "error": {
+ "invalid_auth": "Vigane autentimine",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-posti aadress",
+ "password": "Salas\u00f5na"
+ },
+ "description": "Sisselogimiseks sisesta oma SmartTubi e-posti aadress ja salas\u00f5na",
+ "title": "Sisselogimine"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarttub/translations/fr.json b/homeassistant/components/smarttub/translations/fr.json
new file mode 100644
index 00000000000000..15dfa04fc7876f
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/fr.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
+ "reauth_successful": "La r\u00e9-authentification a \u00e9t\u00e9 un succ\u00e8s"
+ },
+ "error": {
+ "invalid_auth": "Authentification invalide",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Email",
+ "password": "Mot de passe"
+ },
+ "description": "Entrez votre adresse e-mail et votre mot de passe SmartTub pour vous connecter",
+ "title": "Connexion"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarttub/translations/hu.json b/homeassistant/components/smarttub/translations/hu.json
new file mode 100644
index 00000000000000..666ff85e321b32
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/hu.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
+ },
+ "error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-mail",
+ "password": "Jelsz\u00f3"
+ },
+ "description": "Add meg SmartTub e-mail c\u00edmet \u00e9s jelsz\u00f3t a bejelentkez\u00e9shez",
+ "title": "Bejelentkez\u00e9s"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarttub/translations/id.json b/homeassistant/components/smarttub/translations/id.json
new file mode 100644
index 00000000000000..c1de3aa0453058
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/id.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "reauth_successful": "Autentikasi ulang berhasil"
+ },
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Email",
+ "password": "Kata Sandi"
+ },
+ "description": "Masukkan alamat email dan kata sandi SmartTub Anda untuk masuk",
+ "title": "Masuk"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarttub/translations/it.json b/homeassistant/components/smarttub/translations/it.json
new file mode 100644
index 00000000000000..64aed0996f3fe2
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/it.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
+ },
+ "error": {
+ "invalid_auth": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-mail",
+ "password": "Password"
+ },
+ "description": "Inserisci il tuo indirizzo e-mail e la password SmartTub per accedere",
+ "title": "Accesso"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarttub/translations/ko.json b/homeassistant/components/smarttub/translations/ko.json
new file mode 100644
index 00000000000000..2ab844cd967f39
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/ko.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "\uc774\uba54\uc77c",
+ "password": "\ube44\ubc00\ubc88\ud638"
+ },
+ "description": "\ub85c\uadf8\uc778\ud558\ub824\uba74 SmartTub \uc774\uba54\uc77c \uc8fc\uc18c\uc640 \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694",
+ "title": "\ub85c\uadf8\uc778"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarttub/translations/nl.json b/homeassistant/components/smarttub/translations/nl.json
new file mode 100644
index 00000000000000..7ef935d8cee182
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/nl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd",
+ "reauth_successful": "Herauthenticatie was succesvol"
+ },
+ "error": {
+ "invalid_auth": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-mail",
+ "password": "Wachtwoord"
+ },
+ "description": "Voer uw SmartTub-e-mailadres en wachtwoord in om in te loggen",
+ "title": "Inloggen"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarttub/translations/no.json b/homeassistant/components/smarttub/translations/no.json
new file mode 100644
index 00000000000000..7f1c5982d28adc
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/no.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert",
+ "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket"
+ },
+ "error": {
+ "invalid_auth": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "E-post",
+ "password": "Passord"
+ },
+ "description": "Skriv inn din SmartTub e-postadresse og passord for \u00e5 logge p\u00e5",
+ "title": "P\u00e5logging"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarttub/translations/pl.json b/homeassistant/components/smarttub/translations/pl.json
new file mode 100644
index 00000000000000..2c3f097d6d003a
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/pl.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119"
+ },
+ "error": {
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Adres e-mail",
+ "password": "Has\u0142o"
+ },
+ "description": "Wprowad\u017a sw\u00f3j adres e-mail SmartTub oraz has\u0142o, aby si\u0119 zalogowa\u0107",
+ "title": "Logowanie"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarttub/translations/pt.json b/homeassistant/components/smarttub/translations/pt.json
new file mode 100644
index 00000000000000..414ca7ddf82f11
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/pt.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado",
+ "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida"
+ },
+ "error": {
+ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida",
+ "unknown": "Erro inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "Email",
+ "password": "Palavra-passe"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarttub/translations/ru.json b/homeassistant/components/smarttub/translations/ru.json
new file mode 100644
index 00000000000000..44f27877d93642
--- /dev/null
+++ b/homeassistant/components/smarttub/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.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
+ },
+ "error": {
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "description": "\u0414\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c SmartTub.",
+ "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarttub/translations/zh-Hant.json b/homeassistant/components/smarttub/translations/zh-Hant.json
new file mode 100644
index 00000000000000..9491e7d2f2570f
--- /dev/null
+++ b/homeassistant/components/smarttub/translations/zh-Hant.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f"
+ },
+ "error": {
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "email": "\u96fb\u5b50\u90f5\u4ef6",
+ "password": "\u5bc6\u78bc"
+ },
+ "description": "\u8acb\u8f38\u5165\u767b\u5165 SmartTub \u4e4b Email \u5730\u5740\u8207\u5bc6\u78bc\u3002",
+ "title": "\u767b\u5165"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarty/__init__.py b/homeassistant/components/smarty/__init__.py
index 22987673005b32..72d5071882fad4 100644
--- a/homeassistant/components/smarty/__init__.py
+++ b/homeassistant/components/smarty/__init__.py
@@ -59,12 +59,12 @@ def setup(hass, config):
def poll_device_update(event_time):
"""Update Smarty device."""
- _LOGGER.debug("Updating Smarty device...")
+ _LOGGER.debug("Updating Smarty device")
if smarty.update():
- _LOGGER.debug("Update success...")
+ _LOGGER.debug("Update success")
dispatcher_send(hass, SIGNAL_UPDATE_SMARTY)
else:
- _LOGGER.debug("Update failed...")
+ _LOGGER.debug("Update failed")
track_time_interval(hass, poll_device_update, timedelta(seconds=30))
diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py
index 40c244944cefc7..481d2e56d3d597 100644
--- a/homeassistant/components/smarty/fan.py
+++ b/homeassistant/components/smarty/fan.py
@@ -1,26 +1,24 @@
"""Platform to control a Salda Smarty XP/XV ventilation unit."""
import logging
+import math
-from homeassistant.components.fan import (
- SPEED_HIGH,
- SPEED_LOW,
- SPEED_MEDIUM,
- SPEED_OFF,
- SUPPORT_SET_SPEED,
- FanEntity,
-)
+from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity
from homeassistant.core import callback
+from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.util.percentage import (
+ int_states_in_range,
+ percentage_to_ranged_value,
+ ranged_value_to_percentage,
+)
from . import DOMAIN, SIGNAL_UPDATE_SMARTY
_LOGGER = logging.getLogger(__name__)
-SPEED_LIST = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
-
-SPEED_MAPPING = {1: SPEED_LOW, 2: SPEED_MEDIUM, 3: SPEED_HIGH}
-SPEED_TO_MODE = {v: k for k, v in SPEED_MAPPING.items()}
+DEFAULT_ON_PERCENTAGE = 66
+SPEED_RANGE = (1, 3) # off is not included
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
@@ -37,8 +35,7 @@ class SmartyFan(FanEntity):
def __init__(self, name, smarty):
"""Initialize the entity."""
self._name = name
- self._speed = SPEED_OFF
- self._state = None
+ self._smarty_fan_speed = 0
self._smarty = smarty
@property
@@ -61,76 +58,64 @@ def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_SET_SPEED
- @property
- def speed_list(self):
- """List of available fan modes."""
- return SPEED_LIST
-
@property
def is_on(self):
"""Return state of the fan."""
- return self._state
+ return bool(self._smarty_fan_speed)
+
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return int_states_in_range(SPEED_RANGE)
@property
- def speed(self) -> str:
- """Return speed of the fan."""
- return self._speed
-
- def set_speed(self, speed: str) -> None:
- """Set the speed of the fan."""
- _LOGGER.debug("Set the fan speed to %s", speed)
- if speed == SPEED_OFF:
+ def percentage(self) -> int:
+ """Return speed percentage of the fan."""
+ if self._smarty_fan_speed == 0:
+ return 0
+ return ranged_value_to_percentage(SPEED_RANGE, self._smarty_fan_speed)
+
+ def set_percentage(self, percentage: int) -> None:
+ """Set the speed percentage of the fan."""
+ _LOGGER.debug("Set the fan percentage to %s", percentage)
+ if percentage == 0:
self.turn_off()
- else:
- self._smarty.set_fan_speed(SPEED_TO_MODE.get(speed))
- self._speed = speed
- self._state = True
-
- #
- # The fan entity model has changed to use percentages and preset_modes
- # instead of speeds.
- #
- # Please review
- # https://developers.home-assistant.io/docs/core/entity/fan/
- #
+ return
+
+ fan_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
+ if not self._smarty.set_fan_speed(fan_speed):
+ raise HomeAssistantError(
+ f"Failed to set the fan speed percentage to {percentage}"
+ )
+
+ self._smarty_fan_speed = fan_speed
+ self.schedule_update_ha_state()
+
def turn_on(self, speed=None, percentage=None, preset_mode=None, **kwargs):
"""Turn on the fan."""
_LOGGER.debug("Turning on fan. Speed is %s", speed)
- if speed is None:
- if self._smarty.turn_on(SPEED_TO_MODE.get(self._speed)):
- self._state = True
- self._speed = SPEED_MEDIUM
- else:
- if self._smarty.set_fan_speed(SPEED_TO_MODE.get(speed)):
- self._speed = speed
- self._state = True
-
- self.schedule_update_ha_state()
+ self.set_percentage(percentage or DEFAULT_ON_PERCENTAGE)
def turn_off(self, **kwargs):
"""Turn off the fan."""
_LOGGER.debug("Turning off fan")
- if self._smarty.turn_off():
- self._state = False
+ if not self._smarty.turn_off():
+ raise HomeAssistantError("Failed to turn off the fan")
+ self._smarty_fan_speed = 0
self.schedule_update_ha_state()
async def async_added_to_hass(self):
"""Call to update fan."""
- async_dispatcher_connect(self.hass, SIGNAL_UPDATE_SMARTY, self._update_callback)
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass, SIGNAL_UPDATE_SMARTY, self._update_callback
+ )
+ )
@callback
def _update_callback(self):
"""Call update method."""
- self.async_schedule_update_ha_state(True)
-
- def update(self):
- """Update state."""
_LOGGER.debug("Updating state")
- result = self._smarty.fan_speed
- if result:
- self._speed = SPEED_MAPPING[result]
- _LOGGER.debug("Speed is %s, Mode is %s", self._speed, result)
- self._state = True
- else:
- self._state = False
+ self._smarty_fan_speed = self._smarty.fan_speed
+ self.async_write_ha_state()
diff --git a/homeassistant/components/smarty/sensor.py b/homeassistant/components/smarty/sensor.py
index f5cd1fbb404dcc..b958185f9bdec6 100644
--- a/homeassistant/components/smarty/sensor.py
+++ b/homeassistant/components/smarty/sensor.py
@@ -3,6 +3,7 @@
import datetime as dt
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
@@ -10,7 +11,6 @@
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
from . import DOMAIN, SIGNAL_UPDATE_SMARTY
@@ -35,7 +35,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(sensors, True)
-class SmartySensor(Entity):
+class SmartySensor(SensorEntity):
"""Representation of a Smarty Sensor."""
def __init__(
diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py
index 8853680af338dd..a8cfdba5be5555 100644
--- a/homeassistant/components/smhi/config_flow.py
+++ b/homeassistant/components/smhi/config_flow.py
@@ -53,31 +53,32 @@ async def async_step_user(self, user_input=None):
# If hass config has the location set and is a valid coordinate the
# default location is set as default values in the form
- if not smhi_locations(self.hass):
- if await self._homeassistant_location_exists():
- return await self._show_config_form(
- name=HOME_LOCATION_NAME,
- latitude=self.hass.config.latitude,
- longitude=self.hass.config.longitude,
- )
+ if (
+ not smhi_locations(self.hass)
+ and await self._homeassistant_location_exists()
+ ):
+ return await self._show_config_form(
+ name=HOME_LOCATION_NAME,
+ latitude=self.hass.config.latitude,
+ longitude=self.hass.config.longitude,
+ )
return await self._show_config_form()
async def _homeassistant_location_exists(self) -> bool:
"""Return true if default location is set and is valid."""
- if self.hass.config.latitude != 0.0 and self.hass.config.longitude != 0.0:
- # Return true if valid location
- if await self._check_location(
+ # Return true if valid location
+ return (
+ self.hass.config.latitude != 0.0
+ and self.hass.config.longitude != 0.0
+ and await self._check_location(
self.hass.config.longitude, self.hass.config.latitude
- ):
- return True
- return False
+ )
+ )
def _name_in_configuration_exists(self, name: str) -> bool:
"""Return True if name exists in configuration."""
- if name in smhi_locations(self.hass):
- return True
- return False
+ return name in smhi_locations(self.hass)
async def _show_config_form(
self, name: str = None, latitude: str = None, longitude: str = None
@@ -97,7 +98,6 @@ async def _show_config_form(
async def _check_location(self, longitude: str, latitude: str) -> bool:
"""Return true if location is ok."""
-
try:
session = aiohttp_client.async_get_clientsession(self.hass)
smhi_api = Smhi(longitude, latitude, session=session)
diff --git a/homeassistant/components/smhi/translations/he.json b/homeassistant/components/smhi/translations/he.json
new file mode 100644
index 00000000000000..4c49313d97741a
--- /dev/null
+++ b/homeassistant/components/smhi/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smhi/translations/id.json b/homeassistant/components/smhi/translations/id.json
new file mode 100644
index 00000000000000..8d5d95f183ec83
--- /dev/null
+++ b/homeassistant/components/smhi/translations/id.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "Nama sudah ada",
+ "wrong_location": "Hanya untuk lokasi di Swedia"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Lintang",
+ "longitude": "Bujur",
+ "name": "Nama"
+ },
+ "title": "Lokasi di Swedia"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smhi/translations/uk.json b/homeassistant/components/smhi/translations/uk.json
new file mode 100644
index 00000000000000..24af32172baf37
--- /dev/null
+++ b/homeassistant/components/smhi/translations/uk.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "\u0426\u044f \u043d\u0430\u0437\u0432\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f.",
+ "wrong_location": "\u0422\u0456\u043b\u044c\u043a\u0438 \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0456\u0457."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
+ "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430",
+ "name": "\u041d\u0430\u0437\u0432\u0430"
+ },
+ "title": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432 \u0428\u0432\u0435\u0446\u0456\u0457"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py
index c13982ee15db41..86cdf72e65c487 100644
--- a/homeassistant/components/smhi/weather.py
+++ b/homeassistant/components/smhi/weather.py
@@ -1,8 +1,9 @@
"""Support for the Swedish weather institute weather service."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
import logging
-from typing import Dict, List
import aiohttp
import async_timeout
@@ -210,7 +211,7 @@ def attribution(self) -> str:
return "Swedish weather institute (SMHI)"
@property
- def forecast(self) -> List:
+ def forecast(self) -> list:
"""Return the forecast."""
if self._forecasts is None or len(self._forecasts) < 2:
return None
@@ -235,7 +236,7 @@ def forecast(self) -> List:
return data
@property
- def device_state_attributes(self) -> Dict:
+ def extra_state_attributes(self) -> dict:
"""Return SMHI specific attributes."""
if self.cloudiness:
return {ATTR_SMHI_CLOUDINESS: self.cloudiness}
diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py
index 8752dfd90da8a1..c4fdb38ebaa633 100644
--- a/homeassistant/components/sms/__init__.py
+++ b/homeassistant/components/sms/__init__.py
@@ -46,9 +46,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
if not gateway:
return False
hass.data[DOMAIN][SMS_GATEWAY] = gateway
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -59,8 +59,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/sms/config_flow.py b/homeassistant/components/sms/config_flow.py
index 52f3a403ed1481..9546fba07739b0 100644
--- a/homeassistant/components/sms/config_flow.py
+++ b/homeassistant/components/sms/config_flow.py
@@ -1,13 +1,13 @@
"""Config flow for SMS integration."""
import logging
-import gammu # pylint: disable=import-error, no-member
+import gammu # pylint: disable=import-error
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_DEVICE
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import DOMAIN
from .gateway import create_sms_gateway
_LOGGER = logging.getLogger(__name__)
@@ -27,7 +27,7 @@ async def get_imei_from_config(hass: core.HomeAssistant, data):
raise CannotConnect
try:
imei = await gateway.get_imei_async()
- except gammu.GSMError as err: # pylint: disable=no-member
+ except gammu.GSMError as err:
raise CannotConnect from err
finally:
await gateway.terminate_async()
diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py
index 9bdfbad0f5597f..51667ef8f776e3 100644
--- a/homeassistant/components/sms/gateway.py
+++ b/homeassistant/components/sms/gateway.py
@@ -1,10 +1,8 @@
"""The sms gateway to interact with a GSM modem."""
import logging
-import gammu # pylint: disable=import-error, no-member
-from gammu.asyncworker import ( # pylint: disable=import-error, no-member
- GammuAsyncWorker,
-)
+import gammu # pylint: disable=import-error
+from gammu.asyncworker import GammuAsyncWorker # pylint: disable=import-error
from homeassistant.core import callback
@@ -45,7 +43,7 @@ def sms_callback(self, state_machine, callback_type, callback_data):
)
entries = self.get_and_delete_all_sms(state_machine)
_LOGGER.debug("SMS entries:%s", entries)
- data = list()
+ data = []
for entry in entries:
decoded_entry = gammu.DecodeSMS(entry)
@@ -80,7 +78,7 @@ def get_and_delete_all_sms(self, state_machine, force=False):
start_remaining = remaining
# Get all sms
start = True
- entries = list()
+ entries = []
all_parts = -1
all_parts_arrived = False
_LOGGER.debug("Start remaining:%i", start_remaining)
@@ -165,6 +163,6 @@ async def create_sms_gateway(config, hass):
gateway = Gateway(worker, hass)
await gateway.init_async()
return gateway
- except gammu.GSMError as exc: # pylint: disable=no-member
+ except gammu.GSMError as exc:
_LOGGER.error("Failed to initialize, error %s", exc)
return None
diff --git a/homeassistant/components/sms/notify.py b/homeassistant/components/sms/notify.py
index f030409b6cace7..04964c15878ea9 100644
--- a/homeassistant/components/sms/notify.py
+++ b/homeassistant/components/sms/notify.py
@@ -1,7 +1,7 @@
"""Support for SMS notification services."""
import logging
-import gammu # pylint: disable=import-error, no-member
+import gammu # pylint: disable=import-error
import voluptuous as vol
from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService
@@ -51,8 +51,8 @@ async def async_send_message(self, message="", **kwargs):
}
try:
# Encode messages
- encoded = gammu.EncodeSMS(smsinfo) # pylint: disable=no-member
- except gammu.GSMError as exc: # pylint: disable=no-member
+ encoded = gammu.EncodeSMS(smsinfo)
+ except gammu.GSMError as exc:
_LOGGER.error("Encoding message %s failed: %s", message, exc)
return
@@ -64,5 +64,5 @@ async def async_send_message(self, message="", **kwargs):
try:
# Actually send the message
await self.gateway.send_sms_async(encoded_message)
- except gammu.GSMError as exc: # pylint: disable=no-member
+ except gammu.GSMError as exc:
_LOGGER.error("Sending to %s failed: %s", self.number, exc)
diff --git a/homeassistant/components/sms/sensor.py b/homeassistant/components/sms/sensor.py
index eaad395eaa6910..fc2310426e31eb 100644
--- a/homeassistant/components/sms/sensor.py
+++ b/homeassistant/components/sms/sensor.py
@@ -1,10 +1,10 @@
"""Support for SMS dongle sensor."""
import logging
-import gammu # pylint: disable=import-error, no-member
+import gammu # pylint: disable=import-error
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_SIGNAL_STRENGTH, SIGNAL_STRENGTH_DECIBELS
-from homeassistant.helpers.entity import Entity
from .const import DOMAIN, SMS_GATEWAY
@@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities, True)
-class GSMSignalSensor(Entity):
+class GSMSignalSensor(SensorEntity):
"""Implementation of a GSM Signal sensor."""
def __init__(
@@ -71,11 +71,11 @@ async def async_update(self):
"""Get the latest data from the modem."""
try:
self._state = await self._gateway.get_signal_quality_async()
- except gammu.GSMError as exc: # pylint: disable=no-member
+ except gammu.GSMError as exc:
_LOGGER.error("Failed to read signal quality: %s", exc)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the sensor attributes."""
return self._state
diff --git a/homeassistant/components/sms/translations/de.json b/homeassistant/components/sms/translations/de.json
index 1252313a438e23..3c5cc3c0490bf0 100644
--- a/homeassistant/components/sms/translations/de.json
+++ b/homeassistant/components/sms/translations/de.json
@@ -1,14 +1,19 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"unknown": "Unerwarteter Fehler"
},
"step": {
"user": {
"data": {
"device": "Ger\u00e4t"
- }
+ },
+ "title": "Verbinden mit dem Modem"
}
}
}
diff --git a/homeassistant/components/sms/translations/hu.json b/homeassistant/components/sms/translations/hu.json
index 3b2d79a34a77e2..6fa524b18ab936 100644
--- a/homeassistant/components/sms/translations/hu.json
+++ b/homeassistant/components/sms/translations/hu.json
@@ -1,7 +1,20 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "device": "Eszk\u00f6z"
+ },
+ "title": "Csatlakoz\u00e1s a modemhez"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/sms/translations/id.json b/homeassistant/components/sms/translations/id.json
new file mode 100644
index 00000000000000..63ebb088521c66
--- /dev/null
+++ b/homeassistant/components/sms/translations/id.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "device": "Perangkat"
+ },
+ "title": "Hubungkan ke modem"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sms/translations/ko.json b/homeassistant/components/sms/translations/ko.json
index 13c043ef6353e9..5ead95c1a27ea2 100644
--- a/homeassistant/components/sms/translations/ko.json
+++ b/homeassistant/components/sms/translations/ko.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4."
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"error": {
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
diff --git a/homeassistant/components/sms/translations/nl.json b/homeassistant/components/sms/translations/nl.json
index 75dd593982a24e..ddcc54d239f50b 100644
--- a/homeassistant/components/sms/translations/nl.json
+++ b/homeassistant/components/sms/translations/nl.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Apparaat is al geconfigureerd"
+ "already_configured": "Apparaat is al geconfigureerd",
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
},
"error": {
"cannot_connect": "Kon niet verbinden",
diff --git a/homeassistant/components/sms/translations/tr.json b/homeassistant/components/sms/translations/tr.json
new file mode 100644
index 00000000000000..1ef2efb8121651
--- /dev/null
+++ b/homeassistant/components/sms/translations/tr.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "title": "Modeme ba\u011flan\u0131n"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sms/translations/uk.json b/homeassistant/components/sms/translations/uk.json
new file mode 100644
index 00000000000000..be271a2b6e4896
--- /dev/null
+++ b/homeassistant/components/sms/translations/uk.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439"
+ },
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/snapcast/manifest.json b/homeassistant/components/snapcast/manifest.json
index 4e65b60280b8d9..43fbbeb8808fa6 100644
--- a/homeassistant/components/snapcast/manifest.json
+++ b/homeassistant/components/snapcast/manifest.json
@@ -2,6 +2,6 @@
"domain": "snapcast",
"name": "Snapcast",
"documentation": "https://www.home-assistant.io/integrations/snapcast",
- "requirements": ["snapcast==2.1.1"],
+ "requirements": ["snapcast==2.1.2"],
"codeowners": []
}
diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py
index ab4b2415034eba..e1c5b7d875b61a 100644
--- a/homeassistant/components/snapcast/media_player.py
+++ b/homeassistant/components/snapcast/media_player.py
@@ -164,7 +164,7 @@ def source_list(self):
return list(self._group.streams_by_name().keys())
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
name = f"{self._group.friendly_name} {GROUP_SUFFIX}"
return {"friendly_name": name}
@@ -261,7 +261,7 @@ def state(self):
return STATE_OFF
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
state_attrs = {}
if self.latency is not None:
diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py
index a60183a1a0fca2..7de2bfb91e2b4c 100644
--- a/homeassistant/components/snmp/sensor.py
+++ b/homeassistant/components/snmp/sensor.py
@@ -15,7 +15,7 @@
)
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_HOST,
CONF_NAME,
@@ -26,7 +26,6 @@
STATE_UNKNOWN,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from .const import (
CONF_ACCEPT_ERRORS,
@@ -139,7 +138,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([SnmpSensor(data, name, unit, value_template)], True)
-class SnmpSensor(Entity):
+class SnmpSensor(SensorEntity):
"""Representation of a SNMP sensor."""
def __init__(self, data, name, unit_of_measurement, value_template):
diff --git a/homeassistant/components/sochain/sensor.py b/homeassistant/components/sochain/sensor.py
index 8f70447133982a..1f735da4995ae8 100644
--- a/homeassistant/components/sochain/sensor.py
+++ b/homeassistant/components/sochain/sensor.py
@@ -4,11 +4,10 @@
from pysochain import ChainSo
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
ATTRIBUTION = "Data provided by chain.so"
@@ -40,7 +39,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([SochainSensor(name, network.upper(), chainso)], True)
-class SochainSensor(Entity):
+class SochainSensor(SensorEntity):
"""Representation of a Sochain sensor."""
def __init__(self, name, unit_of_measurement, chainso):
@@ -69,7 +68,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/socialblade/__init__.py b/homeassistant/components/socialblade/__init__.py
deleted file mode 100644
index c497d99d32cafe..00000000000000
--- a/homeassistant/components/socialblade/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The socialblade component."""
diff --git a/homeassistant/components/socialblade/manifest.json b/homeassistant/components/socialblade/manifest.json
deleted file mode 100644
index d73e76869476c6..00000000000000
--- a/homeassistant/components/socialblade/manifest.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "domain": "socialblade",
- "name": "Social Blade",
- "documentation": "https://www.home-assistant.io/integrations/socialblade",
- "requirements": ["socialbladeclient==0.5"],
- "codeowners": []
-}
diff --git a/homeassistant/components/socialblade/sensor.py b/homeassistant/components/socialblade/sensor.py
deleted file mode 100644
index 3d53e76a27a125..00000000000000
--- a/homeassistant/components/socialblade/sensor.py
+++ /dev/null
@@ -1,85 +0,0 @@
-"""Support for Social Blade."""
-from datetime import timedelta
-import logging
-
-import socialbladeclient
-import voluptuous as vol
-
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME
-from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.entity import Entity
-from homeassistant.util import Throttle
-
-_LOGGER = logging.getLogger(__name__)
-
-CHANNEL_ID = "channel_id"
-
-DEFAULT_NAME = "Social Blade"
-
-MIN_TIME_BETWEEN_UPDATES = timedelta(hours=2)
-
-SUBSCRIBERS = "subscribers"
-
-TOTAL_VIEWS = "total_views"
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CHANNEL_ID): cv.string,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- }
-)
-
-
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Social Blade sensor."""
- social_blade = SocialBladeSensor(config[CHANNEL_ID], config[CONF_NAME])
-
- social_blade.update()
- if social_blade.valid_channel_id is False:
- return
-
- add_entities([social_blade])
-
-
-class SocialBladeSensor(Entity):
- """Representation of a Social Blade Sensor."""
-
- def __init__(self, case, name):
- """Initialize the Social Blade sensor."""
- self._state = None
- self.channel_id = case
- self._attributes = None
- self.valid_channel_id = None
- self._name = name
-
- @property
- def name(self):
- """Return the name."""
- return self._name
-
- @property
- def state(self):
- """Return the state."""
- return self._state
-
- @property
- def device_state_attributes(self):
- """Return the state attributes."""
- if self._attributes:
- return self._attributes
-
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
- def update(self):
- """Get the latest data from Social Blade."""
-
- try:
- data = socialbladeclient.get_data(self.channel_id)
- self._attributes = {TOTAL_VIEWS: data[TOTAL_VIEWS]}
- self._state = data[SUBSCRIBERS]
- self.valid_channel_id = True
-
- except (ValueError, IndexError):
- _LOGGER.error("Unable to find valid channel ID")
- self.valid_channel_id = False
- self._attributes = None
diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py
index bafc6b67f1c828..f01226bcb458be 100644
--- a/homeassistant/components/solaredge/__init__.py
+++ b/homeassistant/components/solaredge/__init__.py
@@ -1,28 +1,35 @@
-"""The solaredge component."""
+"""The solaredge integration."""
+from __future__ import annotations
+
+from typing import Any
+
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_NAME
+from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.typing import HomeAssistantType
from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN
CONFIG_SCHEMA = vol.Schema(
- {
- DOMAIN: vol.Schema(
- {
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Required(CONF_API_KEY): cv.string,
- vol.Required(CONF_SITE_ID): cv.string,
- }
- )
- },
+ vol.All(
+ cv.deprecated(DOMAIN),
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Required(CONF_API_KEY): cv.string,
+ vol.Required(CONF_SITE_ID): cv.string,
+ }
+ )
+ },
+ ),
extra=vol.ALLOW_EXTRA,
)
-async def async_setup(hass, config):
+async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Platform setup, do nothing."""
if DOMAIN not in config:
return True
@@ -35,7 +42,7 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Load the saved entities."""
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "sensor")
diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py
index 49c265b4221b21..eecd11d7b128fc 100644
--- a/homeassistant/components/solaredge/config_flow.py
+++ b/homeassistant/components/solaredge/config_flow.py
@@ -1,4 +1,8 @@
"""Config flow for the SolarEdge platform."""
+from __future__ import annotations
+
+from typing import Any
+
from requests.exceptions import ConnectTimeout, HTTPError
import solaredge
import voluptuous as vol
@@ -30,13 +34,11 @@ def __init__(self) -> None:
"""Initialize the config flow."""
self._errors = {}
- def _site_in_configuration_exists(self, site_id) -> bool:
+ def _site_in_configuration_exists(self, site_id: str) -> bool:
"""Return True if site_id exists in configuration."""
- if site_id in solaredge_entries(self.hass):
- return True
- return False
+ return site_id in solaredge_entries(self.hass)
- def _check_site(self, site_id, api_key) -> bool:
+ def _check_site(self, site_id: str, api_key: str) -> bool:
"""Check if we can connect to the soleredge api service."""
api = solaredge.Solaredge(api_key)
try:
@@ -52,7 +54,9 @@ def _check_site(self, site_id, api_key) -> bool:
return False
return True
- async def async_step_user(self, user_input=None):
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Step when user initializes a integration."""
self._errors = {}
if user_input is not None:
@@ -71,11 +75,7 @@ async def async_step_user(self, user_input=None):
)
else:
- user_input = {}
- user_input[CONF_NAME] = DEFAULT_NAME
- user_input[CONF_SITE_ID] = ""
- user_input[CONF_API_KEY] = ""
-
+ user_input = {CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: "", CONF_API_KEY: ""}
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
@@ -90,7 +90,9 @@ async def async_step_user(self, user_input=None):
errors=self._errors,
)
- async def async_step_import(self, user_input=None):
+ async def async_step_import(
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Import a config entry."""
if self._site_in_configuration_exists(user_input[CONF_SITE_ID]):
return self.async_abort(reason="already_configured")
diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json
index f0a620021ad58f..5cfe773d98c0b5 100644
--- a/homeassistant/components/solaredge/manifest.json
+++ b/homeassistant/components/solaredge/manifest.json
@@ -4,6 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/solaredge",
"requirements": ["solaredge==0.0.2", "stringcase==1.2.0"],
"config_flow": true,
- "codeowners": [],
- "dhcp": [{"hostname":"target","macaddress":"002702*"}]
+ "codeowners": ["@frenck"],
+ "dhcp": [{ "hostname": "target", "macaddress": "002702*" }]
}
diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py
index e3e59676bf5f97..b93a84a77fb615 100644
--- a/homeassistant/components/solaredge/sensor.py
+++ b/homeassistant/components/solaredge/sensor.py
@@ -1,14 +1,26 @@
"""Support for SolarEdge Monitoring API."""
-from datetime import date, datetime
+from __future__ import annotations
+
+from abc import abstractmethod
+from datetime import date, datetime, timedelta
import logging
+from typing import Any, Callable, Iterable
from requests.exceptions import ConnectTimeout, HTTPError
-import solaredge
+from solaredge import Solaredge
from stringcase import snakecase
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.entity import Entity
-from homeassistant.util import Throttle
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+ UpdateFailed,
+)
from .const import (
CONF_SITE_ID,
@@ -23,10 +35,14 @@
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass, entry, async_add_entities):
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: Callable[[Iterable[Entity]], None],
+) -> None:
"""Add an solarEdge entry."""
# Add the needed sensors to hass
- api = solaredge.Solaredge(entry.data[CONF_API_KEY])
+ api = Solaredge(entry.data[CONF_API_KEY])
# Check if api can be reached and site is active
try:
@@ -37,14 +53,20 @@ async def async_setup_entry(hass, entry, async_add_entities):
_LOGGER.error("SolarEdge site is not active")
return
_LOGGER.debug("Credentials correct and site is active")
- except KeyError:
+ except KeyError as ex:
_LOGGER.error("Missing details data in SolarEdge response")
- return
- except (ConnectTimeout, HTTPError):
+ raise ConfigEntryNotReady from ex
+ except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Could not retrieve details from SolarEdge API")
- return
+ raise ConfigEntryNotReady from ex
+
+ sensor_factory = SolarEdgeSensorFactory(
+ hass, entry.title, entry.data[CONF_SITE_ID], api
+ )
+ for service in sensor_factory.all_services:
+ service.async_setup()
+ await service.coordinator.async_refresh()
- sensor_factory = SolarEdgeSensorFactory(entry.title, entry.data[CONF_SITE_ID], api)
entities = []
for sensor_key in SENSOR_TYPES:
sensor = sensor_factory.create_sensor(sensor_key)
@@ -56,17 +78,26 @@ async def async_setup_entry(hass, entry, async_add_entities):
class SolarEdgeSensorFactory:
"""Factory which creates sensors based on the sensor_key."""
- def __init__(self, platform_name, site_id, api):
+ def __init__(
+ self, hass: HomeAssistant, platform_name: str, site_id: str, api: Solaredge
+ ) -> None:
"""Initialize the factory."""
self.platform_name = platform_name
- details = SolarEdgeDetailsDataService(api, site_id)
- overview = SolarEdgeOverviewDataService(api, site_id)
- inventory = SolarEdgeInventoryDataService(api, site_id)
- flow = SolarEdgePowerFlowDataService(api, site_id)
- energy = SolarEdgeEnergyDetailsService(api, site_id)
+ details = SolarEdgeDetailsDataService(hass, api, site_id)
+ overview = SolarEdgeOverviewDataService(hass, api, site_id)
+ inventory = SolarEdgeInventoryDataService(hass, api, site_id)
+ flow = SolarEdgePowerFlowDataService(hass, api, site_id)
+ energy = SolarEdgeEnergyDetailsService(hass, api, site_id)
- self.services = {"site_details": (SolarEdgeDetailsSensor, details)}
+ self.all_services = (details, overview, inventory, flow, energy)
+
+ self.services: dict[
+ str,
+ tuple[
+ type[SolarEdgeSensor | SolarEdgeOverviewSensor], SolarEdgeDataService
+ ],
+ ] = {"site_details": (SolarEdgeDetailsSensor, details)}
for key in [
"lifetime_energy",
@@ -95,82 +126,70 @@ def __init__(self, platform_name, site_id, api):
]:
self.services[key] = (SolarEdgeEnergyDetailsSensor, energy)
- def create_sensor(self, sensor_key):
+ def create_sensor(self, sensor_key: str) -> SolarEdgeSensor:
"""Create and return a sensor based on the sensor_key."""
sensor_class, service = self.services[sensor_key]
return sensor_class(self.platform_name, sensor_key, service)
-class SolarEdgeSensor(Entity):
+class SolarEdgeSensor(CoordinatorEntity, SensorEntity):
"""Abstract class for a solaredge sensor."""
- def __init__(self, platform_name, sensor_key, data_service):
+ def __init__(
+ self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService
+ ) -> None:
"""Initialize the sensor."""
+ super().__init__(data_service.coordinator)
self.platform_name = platform_name
self.sensor_key = sensor_key
self.data_service = data_service
- self._state = None
-
- self._unit_of_measurement = SENSOR_TYPES[self.sensor_key][2]
- self._icon = SENSOR_TYPES[self.sensor_key][3]
+ @property
+ def unit_of_measurement(self) -> str | None:
+ """Return the unit of measurement."""
+ return SENSOR_TYPES[self.sensor_key][2]
@property
- def name(self):
+ def name(self) -> str:
"""Return the name."""
return "{} ({})".format(self.platform_name, SENSOR_TYPES[self.sensor_key][1])
@property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return self._unit_of_measurement
-
- @property
- def icon(self):
+ def icon(self) -> str | None:
"""Return the sensor icon."""
- return self._icon
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
+ return SENSOR_TYPES[self.sensor_key][3]
class SolarEdgeOverviewSensor(SolarEdgeSensor):
"""Representation of an SolarEdge Monitoring API overview sensor."""
- def __init__(self, platform_name, sensor_key, data_service):
+ def __init__(
+ self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService
+ ) -> None:
"""Initialize the overview sensor."""
super().__init__(platform_name, sensor_key, data_service)
self._json_key = SENSOR_TYPES[self.sensor_key][0]
- def update(self):
- """Get the latest data from the sensor and update the state."""
- self.data_service.update()
- self._state = self.data_service.data.get(self._json_key)
+ @property
+ def state(self) -> str | None:
+ """Return the state of the sensor."""
+ return self.data_service.data.get(self._json_key)
class SolarEdgeDetailsSensor(SolarEdgeSensor):
"""Representation of an SolarEdge Monitoring API details sensor."""
- def __init__(self, platform_name, sensor_key, data_service):
- """Initialize the details sensor."""
- super().__init__(platform_name, sensor_key, data_service)
-
- self._attributes = {}
-
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
- return self._attributes
+ return self.data_service.attributes
- def update(self):
- """Get the latest details and update state and attributes."""
- self.data_service.update()
- self._state = self.data_service.data
- self._attributes = self.data_service.attributes
+ @property
+ def state(self) -> str | None:
+ """Return the state of the sensor."""
+ return self.data_service.data
class SolarEdgeInventorySensor(SolarEdgeSensor):
@@ -182,18 +201,15 @@ def __init__(self, platform_name, sensor_key, data_service):
self._json_key = SENSOR_TYPES[self.sensor_key][0]
- self._attributes = {}
-
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
- return self._attributes
+ return self.data_service.attributes.get(self._json_key)
- def update(self):
- """Get the latest inventory data and update state and attributes."""
- self.data_service.update()
- self._state = self.data_service.data.get(self._json_key)
- self._attributes = self.data_service.attributes.get(self._json_key)
+ @property
+ def state(self) -> str | None:
+ """Return the state of the sensor."""
+ return self.data_service.data.get(self._json_key)
class SolarEdgeEnergyDetailsSensor(SolarEdgeSensor):
@@ -205,76 +221,83 @@ def __init__(self, platform_name, sensor_key, data_service):
self._json_key = SENSOR_TYPES[self.sensor_key][0]
- self._attributes = {}
-
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
- return self._attributes
+ return self.data_service.attributes.get(self._json_key)
- def update(self):
- """Get the latest inventory data and update state and attributes."""
- self.data_service.update()
- self._state = self.data_service.data.get(self._json_key)
- self._attributes = self.data_service.attributes.get(self._json_key)
- self._unit_of_measurement = self.data_service.unit
+ @property
+ def state(self) -> str | None:
+ """Return the state of the sensor."""
+ return self.data_service.data.get(self._json_key)
+
+ @property
+ def unit_of_measurement(self) -> str | None:
+ """Return the unit of measurement."""
+ return self.data_service.unit
class SolarEdgePowerFlowSensor(SolarEdgeSensor):
"""Representation of an SolarEdge Monitoring API power flow sensor."""
- def __init__(self, platform_name, sensor_key, data_service):
+ def __init__(
+ self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService
+ ) -> None:
"""Initialize the power flow sensor."""
super().__init__(platform_name, sensor_key, data_service)
self._json_key = SENSOR_TYPES[self.sensor_key][0]
- self._attributes = {}
+ @property
+ def device_class(self) -> str:
+ """Device Class."""
+ return DEVICE_CLASS_POWER
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
- return self._attributes
+ return self.data_service.attributes.get(self._json_key)
@property
- def device_class(self):
- """Device Class."""
- return DEVICE_CLASS_POWER
+ def state(self) -> str | None:
+ """Return the state of the sensor."""
+ return self.data_service.data.get(self._json_key)
- def update(self):
- """Get the latest inventory data and update state and attributes."""
- self.data_service.update()
- self._state = self.data_service.data.get(self._json_key)
- self._attributes = self.data_service.attributes.get(self._json_key)
- self._unit_of_measurement = self.data_service.unit
+ @property
+ def unit_of_measurement(self) -> str | None:
+ """Return the unit of measurement."""
+ return self.data_service.unit
class SolarEdgeStorageLevelSensor(SolarEdgeSensor):
"""Representation of an SolarEdge Monitoring API storage level sensor."""
- def __init__(self, platform_name, sensor_key, data_service):
+ def __init__(
+ self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService
+ ) -> None:
"""Initialize the storage level sensor."""
super().__init__(platform_name, sensor_key, data_service)
self._json_key = SENSOR_TYPES[self.sensor_key][0]
@property
- def device_class(self):
+ def device_class(self) -> str:
"""Return the device_class of the device."""
return DEVICE_CLASS_BATTERY
- def update(self):
- """Get the latest inventory data and update state and attributes."""
- self.data_service.update()
+ @property
+ def state(self) -> str | None:
+ """Return the state of the sensor."""
attr = self.data_service.attributes.get(self._json_key)
if attr and "soc" in attr:
- self._state = attr["soc"]
+ return attr["soc"]
+ return None
class SolarEdgeDataService:
"""Get and update the latest data."""
- def __init__(self, api, site_id):
+ def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None:
"""Initialize the data object."""
self.api = api
self.site_id = site_id
@@ -282,22 +305,49 @@ def __init__(self, api, site_id):
self.data = {}
self.attributes = {}
+ self.hass = hass
+ self.coordinator = None
+
+ @callback
+ def async_setup(self) -> None:
+ """Coordinator creation."""
+ self.coordinator = DataUpdateCoordinator(
+ self.hass,
+ _LOGGER,
+ name=str(self),
+ update_method=self.async_update_data,
+ update_interval=self.update_interval,
+ )
+
+ @property
+ @abstractmethod
+ def update_interval(self) -> timedelta:
+ """Update interval."""
+
+ @abstractmethod
+ def update(self) -> None:
+ """Update data in executor."""
+
+ async def async_update_data(self) -> None:
+ """Update data."""
+ await self.hass.async_add_executor_job(self.update)
+
class SolarEdgeOverviewDataService(SolarEdgeDataService):
"""Get and update the latest overview data."""
- @Throttle(OVERVIEW_UPDATE_DELAY)
- def update(self):
+ @property
+ def update_interval(self) -> timedelta:
+ """Update interval."""
+ return OVERVIEW_UPDATE_DELAY
+
+ def update(self) -> None:
"""Update the data from the SolarEdge Monitoring API."""
try:
data = self.api.get_overview(self.site_id)
overview = data["overview"]
- except KeyError:
- _LOGGER.error("Missing overview data, skipping update")
- return
- except (ConnectTimeout, HTTPError):
- _LOGGER.error("Could not retrieve data, skipping update")
- return
+ except KeyError as ex:
+ raise UpdateFailed("Missing overview data, skipping update") from ex
self.data = {}
@@ -316,25 +366,25 @@ def update(self):
class SolarEdgeDetailsDataService(SolarEdgeDataService):
"""Get and update the latest details data."""
- def __init__(self, api, site_id):
+ def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None:
"""Initialize the details data service."""
- super().__init__(api, site_id)
+ super().__init__(hass, api, site_id)
self.data = None
- @Throttle(DETAILS_UPDATE_DELAY)
- def update(self):
+ @property
+ def update_interval(self) -> timedelta:
+ """Update interval."""
+ return DETAILS_UPDATE_DELAY
+
+ def update(self) -> None:
"""Update the data from the SolarEdge Monitoring API."""
try:
data = self.api.get_details(self.site_id)
details = data["details"]
- except KeyError:
- _LOGGER.error("Missing details data, skipping update")
- return
- except (ConnectTimeout, HTTPError):
- _LOGGER.error("Could not retrieve data, skipping update")
- return
+ except KeyError as ex:
+ raise UpdateFailed("Missing details data, skipping update") from ex
self.data = None
self.attributes = {}
@@ -362,18 +412,18 @@ def update(self):
class SolarEdgeInventoryDataService(SolarEdgeDataService):
"""Get and update the latest inventory data."""
- @Throttle(INVENTORY_UPDATE_DELAY)
- def update(self):
+ @property
+ def update_interval(self) -> timedelta:
+ """Update interval."""
+ return INVENTORY_UPDATE_DELAY
+
+ def update(self) -> None:
"""Update the data from the SolarEdge Monitoring API."""
try:
data = self.api.get_inventory(self.site_id)
inventory = data["Inventory"]
- except KeyError:
- _LOGGER.error("Missing inventory data, skipping update")
- return
- except (ConnectTimeout, HTTPError):
- _LOGGER.error("Could not retrieve data, skipping update")
- return
+ except KeyError as ex:
+ raise UpdateFailed("Missing inventory data, skipping update") from ex
self.data = {}
self.attributes = {}
@@ -388,14 +438,18 @@ def update(self):
class SolarEdgeEnergyDetailsService(SolarEdgeDataService):
"""Get and update the latest power flow data."""
- def __init__(self, api, site_id):
+ def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None:
"""Initialize the power flow data service."""
- super().__init__(api, site_id)
+ super().__init__(hass, api, site_id)
self.unit = None
- @Throttle(ENERGY_DETAILS_DELAY)
- def update(self):
+ @property
+ def update_interval(self) -> timedelta:
+ """Update interval."""
+ return ENERGY_DETAILS_DELAY
+
+ def update(self) -> None:
"""Update the data from the SolarEdge Monitoring API."""
try:
now = datetime.now()
@@ -409,12 +463,8 @@ def update(self):
time_unit="DAY",
)
energy_details = data["energyDetails"]
- except KeyError:
- _LOGGER.error("Missing power flow data, skipping update")
- return
- except (ConnectTimeout, HTTPError):
- _LOGGER.error("Could not retrieve data, skipping update")
- return
+ except KeyError as ex:
+ raise UpdateFailed("Missing power flow data, skipping update") from ex
if "meters" not in energy_details:
_LOGGER.debug(
@@ -449,24 +499,24 @@ def update(self):
class SolarEdgePowerFlowDataService(SolarEdgeDataService):
"""Get and update the latest power flow data."""
- def __init__(self, api, site_id):
+ def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None:
"""Initialize the power flow data service."""
- super().__init__(api, site_id)
+ super().__init__(hass, api, site_id)
self.unit = None
- @Throttle(POWER_FLOW_UPDATE_DELAY)
- def update(self):
+ @property
+ def update_interval(self) -> timedelta:
+ """Update interval."""
+ return POWER_FLOW_UPDATE_DELAY
+
+ def update(self) -> None:
"""Update the data from the SolarEdge Monitoring API."""
try:
data = self.api.get_current_power_flow(self.site_id)
power_flow = data["siteCurrentPowerFlow"]
- except KeyError:
- _LOGGER.error("Missing power flow data, skipping update")
- return
- except (ConnectTimeout, HTTPError):
- _LOGGER.error("Could not retrieve data, skipping update")
- return
+ except KeyError as ex:
+ raise UpdateFailed("Missing power flow data, skipping update") from ex
power_from = []
power_to = []
diff --git a/homeassistant/components/solaredge/translations/fr.json b/homeassistant/components/solaredge/translations/fr.json
index f8aec1fa2308f7..3eea6678d03e40 100644
--- a/homeassistant/components/solaredge/translations/fr.json
+++ b/homeassistant/components/solaredge/translations/fr.json
@@ -1,10 +1,15 @@
{
"config": {
"abort": {
+ "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ",
"site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9"
},
"error": {
- "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9"
+ "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ",
+ "could_not_connect": "Impossible de se connecter \u00e0 l'API solaredge",
+ "invalid_api_key": "Cl\u00e9 API invalide",
+ "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9",
+ "site_not_active": "The site n'est pas actif"
},
"step": {
"user": {
diff --git a/homeassistant/components/solaredge/translations/hu.json b/homeassistant/components/solaredge/translations/hu.json
index 31890269925447..8479c90f595b6f 100644
--- a/homeassistant/components/solaredge/translations/hu.json
+++ b/homeassistant/components/solaredge/translations/hu.json
@@ -1,6 +1,11 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs",
"site_not_active": "Az oldal nem akt\u00edv"
},
"step": {
diff --git a/homeassistant/components/solaredge/translations/id.json b/homeassistant/components/solaredge/translations/id.json
new file mode 100644
index 00000000000000..41c94755af7480
--- /dev/null
+++ b/homeassistant/components/solaredge/translations/id.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "site_exists": "Nilai site_id ini sudah dikonfigurasi"
+ },
+ "error": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "could_not_connect": "Tidak dapat terhubung ke API solaredge",
+ "invalid_api_key": "Kunci API tidak valid",
+ "site_exists": "Nilai site_id ini sudah dikonfigurasi",
+ "site_not_active": "Situs tidak aktif"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kunci API",
+ "name": "Nama instalasi ini",
+ "site_id": "Nilai site_id SolarEdge"
+ },
+ "title": "Tentukan parameter API untuk instalasi ini"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/translations/ko.json b/homeassistant/components/solaredge/translations/ko.json
index eb2d8c42a14cd7..ce3ed2a767d847 100644
--- a/homeassistant/components/solaredge/translations/ko.json
+++ b/homeassistant/components/solaredge/translations/ko.json
@@ -1,10 +1,15 @@
{
"config": {
"abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"site_exists": "\uc774 site_id \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "site_exists": "\uc774 site_id \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "could_not_connect": "SolarEdge API\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "site_exists": "\uc774 site_id \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "site_not_active": "\uc0ac\uc774\ud2b8\uac00 \ud65c\uc131\ud654\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
diff --git a/homeassistant/components/solaredge/translations/lb.json b/homeassistant/components/solaredge/translations/lb.json
index 4f2f698a6ca751..709a57f070b868 100644
--- a/homeassistant/components/solaredge/translations/lb.json
+++ b/homeassistant/components/solaredge/translations/lb.json
@@ -1,10 +1,13 @@
{
"config": {
"abort": {
+ "already_configured": "Apparat ass scho konfigur\u00e9iert",
"site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert"
},
"error": {
- "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert"
+ "already_configured": "Apparat ass scho konfigur\u00e9iert",
+ "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert",
+ "site_not_active": "De Site ass net aktiv"
},
"step": {
"user": {
diff --git a/homeassistant/components/solaredge/translations/nl.json b/homeassistant/components/solaredge/translations/nl.json
index 4b468218410628..24e1716dd571ba 100644
--- a/homeassistant/components/solaredge/translations/nl.json
+++ b/homeassistant/components/solaredge/translations/nl.json
@@ -1,15 +1,20 @@
{
"config": {
"abort": {
+ "already_configured": "Apparaat is al geconfigureerd",
"site_exists": "Deze site_id is al geconfigureerd"
},
"error": {
- "site_exists": "Deze site_id is al geconfigureerd"
+ "already_configured": "Apparaat is al geconfigureerd",
+ "could_not_connect": "Kon geen verbinding maken met de solaredge API",
+ "invalid_api_key": "Ongeldige API-sleutel",
+ "site_exists": "Deze site_id is al geconfigureerd",
+ "site_not_active": "De site is niet actief"
},
"step": {
"user": {
"data": {
- "api_key": "De API-sleutel voor deze site",
+ "api_key": "API-sleutel",
"name": "De naam van deze installatie",
"site_id": "De SolarEdge site-id"
},
diff --git a/homeassistant/components/solaredge/translations/tr.json b/homeassistant/components/solaredge/translations/tr.json
index 5307276a71d3a3..b8159be58b48c4 100644
--- a/homeassistant/components/solaredge/translations/tr.json
+++ b/homeassistant/components/solaredge/translations/tr.json
@@ -2,6 +2,19 @@
"config": {
"abort": {
"already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "could_not_connect": "Solaredge API'ye ba\u011flan\u0131lamad\u0131",
+ "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131",
+ "site_not_active": "Site aktif de\u011fil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Anahtar\u0131"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/translations/uk.json b/homeassistant/components/solaredge/translations/uk.json
new file mode 100644
index 00000000000000..5ad67d8768001b
--- /dev/null
+++ b/homeassistant/components/solaredge/translations/uk.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "site_exists": "\u0426\u0435\u0439 site_id \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439."
+ },
+ "error": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "could_not_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 API Solaredge.",
+ "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API",
+ "site_exists": "\u0426\u0435\u0439 site_id \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439.",
+ "site_not_active": "\u0421\u0430\u0439\u0442 \u043d\u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u0438\u0439."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "site_id": "site-id"
+ },
+ "title": "SolarEdge"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py
index 59b0a5e88560d8..441a1c39e08b3a 100644
--- a/homeassistant/components/solaredge_local/sensor.py
+++ b/homeassistant/components/solaredge_local/sensor.py
@@ -1,4 +1,5 @@
"""Support for SolarEdge-local Monitoring API."""
+from contextlib import suppress
from copy import deepcopy
from datetime import timedelta
import logging
@@ -8,7 +9,7 @@
from solaredge_local import SolarEdge
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_IP_ADDRESS,
CONF_NAME,
@@ -21,7 +22,6 @@
VOLT,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
DOMAIN = "solaredge_local"
@@ -231,7 +231,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(entities, True)
-class SolarEdgeSensor(Entity):
+class SolarEdgeSensor(SensorEntity):
"""Representation of an SolarEdge Monitoring API sensor."""
def __init__(self, platform_name, data, json_key, name, unit, icon, attr):
@@ -257,7 +257,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._attr:
try:
@@ -351,19 +351,15 @@ def update(self):
self.info["optimizers"] = status.optimizersStatus.total
self.info["invertertemperature"] = INVERTER_MODES[status.status]
- try:
+ with suppress(IndexError):
if status.metersList[1]:
self.data["currentPowerimport"] = status.metersList[1].currentPower
self.data["totalEnergyimport"] = status.metersList[1].totalEnergy
- except IndexError:
- pass
- try:
+ with suppress(IndexError):
if status.metersList[0]:
self.data["currentPowerexport"] = status.metersList[0].currentPower
self.data["totalEnergyexport"] = status.metersList[0].totalEnergy
- except IndexError:
- pass
if maintenance.system.name:
self.data["optimizertemperature"] = round(statistics.mean(temperature), 2)
diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py
index 6073d12815b55b..85a1531090daf6 100644
--- a/homeassistant/components/solarlog/sensor.py
+++ b/homeassistant/components/solarlog/sensor.py
@@ -5,8 +5,8 @@
from requests.exceptions import HTTPError, Timeout
from sunwatcher.solarlog.solarlog import SolarLog
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_HOST
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from .const import DOMAIN, SCAN_INTERVAL, SENSOR_TYPES
@@ -55,7 +55,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
return True
-class SolarlogSensor(Entity):
+class SolarlogSensor(SensorEntity):
"""Representation of a Sensor."""
def __init__(self, entry_id, device_name, sensor_key, data):
diff --git a/homeassistant/components/solarlog/translations/de.json b/homeassistant/components/solarlog/translations/de.json
index 58e691b733d181..008e10586819fa 100644
--- a/homeassistant/components/solarlog/translations/de.json
+++ b/homeassistant/components/solarlog/translations/de.json
@@ -5,7 +5,7 @@
},
"error": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
- "cannot_connect": "Verbindung fehlgeschlagen. \u00dcberpr\u00fcfe die Host-Adresse"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"step": {
"user": {
diff --git a/homeassistant/components/solarlog/translations/hu.json b/homeassistant/components/solarlog/translations/hu.json
index 3fa8a9620a017f..dd0ea8033ae16a 100644
--- a/homeassistant/components/solarlog/translations/hu.json
+++ b/homeassistant/components/solarlog/translations/hu.json
@@ -1,7 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
"step": {
"user": {
diff --git a/homeassistant/components/solarlog/translations/id.json b/homeassistant/components/solarlog/translations/id.json
new file mode 100644
index 00000000000000..3ce222f9c2a879
--- /dev/null
+++ b/homeassistant/components/solarlog/translations/id.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Prefiks yang akan digunakan untuk sensor Solar-Log Anda"
+ },
+ "title": "Tentukan koneksi Solar-Log Anda"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solarlog/translations/ko.json b/homeassistant/components/solarlog/translations/ko.json
index 66c6a2177d4eff..22002c52cef36e 100644
--- a/homeassistant/components/solarlog/translations/ko.json
+++ b/homeassistant/components/solarlog/translations/ko.json
@@ -5,7 +5,7 @@
},
"error": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8 \uc8fc\uc18c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694."
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
diff --git a/homeassistant/components/solarlog/translations/nl.json b/homeassistant/components/solarlog/translations/nl.json
index 8ccf5e626c7632..6e526802d8ab9d 100644
--- a/homeassistant/components/solarlog/translations/nl.json
+++ b/homeassistant/components/solarlog/translations/nl.json
@@ -5,12 +5,12 @@
},
"error": {
"already_configured": "Apparaat is al geconfigureerd",
- "cannot_connect": "Verbinding mislukt, controleer het host-adres"
+ "cannot_connect": "Kan geen verbinding maken"
},
"step": {
"user": {
"data": {
- "host": "De hostnaam of het IP-adres van uw Solar-Log apparaat",
+ "host": "Host",
"name": "Het voorvoegsel dat moet worden gebruikt voor uw Solar-Log sensoren"
},
"title": "Definieer uw Solar-Log verbinding"
diff --git a/homeassistant/components/solarlog/translations/tr.json b/homeassistant/components/solarlog/translations/tr.json
new file mode 100644
index 00000000000000..a11d3815eed8ca
--- /dev/null
+++ b/homeassistant/components/solarlog/translations/tr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solarlog/translations/uk.json b/homeassistant/components/solarlog/translations/uk.json
new file mode 100644
index 00000000000000..f4fca695032917
--- /dev/null
+++ b/homeassistant/components/solarlog/translations/uk.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041f\u0440\u0435\u0444\u0456\u043a\u0441, \u044f\u043a\u0438\u0439 \u0431\u0443\u0434\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0434\u043b\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432 Solar-Log"
+ },
+ "title": "Solar-Log"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json
index 232715ebe18e7b..90bfd8e61840d6 100644
--- a/homeassistant/components/solax/manifest.json
+++ b/homeassistant/components/solax/manifest.json
@@ -2,6 +2,6 @@
"domain": "solax",
"name": "SolaX Power",
"documentation": "https://www.home-assistant.io/integrations/solax",
- "requirements": ["solax==0.2.5"],
+ "requirements": ["solax==0.2.6"],
"codeowners": ["@squishykid"]
}
diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py
index bca507c4391cc4..e47f5c578027ee 100644
--- a/homeassistant/components/solax/sensor.py
+++ b/homeassistant/components/solax/sensor.py
@@ -6,11 +6,10 @@
from solax.inverter import InverterError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
DEFAULT_PORT = 80
@@ -73,7 +72,7 @@ async def async_refresh(self, now=None):
sensor.async_schedule_update_ha_state()
-class Inverter(Entity):
+class Inverter(SensorEntity):
"""Class for a sensor."""
def __init__(self, uid, serial, key, unit):
diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py
index bd5695cb7ecf05..3f15199c162bed 100644
--- a/homeassistant/components/soma/__init__.py
+++ b/homeassistant/components/soma/__init__.py
@@ -24,7 +24,7 @@
extra=vol.ALLOW_EXTRA,
)
-SOMA_COMPONENTS = ["cover", "sensor"]
+PLATFORMS = ["cover", "sensor"]
async def async_setup(hass, config):
@@ -50,9 +50,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
devices = await hass.async_add_executor_job(hass.data[DOMAIN][API].list_devices)
hass.data[DOMAIN][DEVICES] = devices["shades"]
- for component in SOMA_COMPONENTS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -63,8 +63,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in SOMA_COMPONENTS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/soma/sensor.py b/homeassistant/components/soma/sensor.py
index 9430a929e1e972..436a92a1087c86 100644
--- a/homeassistant/components/soma/sensor.py
+++ b/homeassistant/components/soma/sensor.py
@@ -4,8 +4,8 @@
from requests import RequestException
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from . import DEVICES, SomaEntity
@@ -26,7 +26,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
-class SomaSensor(SomaEntity, Entity):
+class SomaSensor(SomaEntity, SensorEntity):
"""Representation of a Soma cover device."""
@property
diff --git a/homeassistant/components/soma/strings.json b/homeassistant/components/soma/strings.json
index 8ab7335dd5c63f..a31b404dad72df 100644
--- a/homeassistant/components/soma/strings.json
+++ b/homeassistant/components/soma/strings.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_setup": "You can only configure one Soma account.",
- "authorize_url_timeout": "Timeout generating authorize url.",
+ "authorize_url_timeout": "Timeout generating authorize URL.",
"missing_configuration": "The Soma component is not configured. Please follow the documentation.",
"result_error": "SOMA Connect responded with error status.",
"connection_error": "Failed to connect to SOMA Connect."
diff --git a/homeassistant/components/soma/translations/cs.json b/homeassistant/components/soma/translations/cs.json
index 5a27562df71f71..ba1261c1100915 100644
--- a/homeassistant/components/soma/translations/cs.json
+++ b/homeassistant/components/soma/translations/cs.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_setup": "M\u016f\u017eete nastavit pouze jeden \u00fa\u010det Soma.",
- "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el",
+ "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el.",
"connection_error": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed SOMA Connect se nezda\u0159ilo.",
"missing_configuration": "Integrace Soma nen\u00ed nastavena. Postupujte podle dokumentace.",
"result_error": "SOMA Connect odpov\u011bd\u011blo chybov\u00fdm stavem."
diff --git a/homeassistant/components/soma/translations/en.json b/homeassistant/components/soma/translations/en.json
index 6f28ee53ae26b6..fb5d17ac59de3d 100644
--- a/homeassistant/components/soma/translations/en.json
+++ b/homeassistant/components/soma/translations/en.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_setup": "You can only configure one Soma account.",
- "authorize_url_timeout": "Timeout generating authorize url.",
+ "authorize_url_timeout": "Timeout generating authorize URL.",
"connection_error": "Failed to connect to SOMA Connect.",
"missing_configuration": "The Soma component is not configured. Please follow the documentation.",
"result_error": "SOMA Connect responded with error status."
diff --git a/homeassistant/components/soma/translations/hu.json b/homeassistant/components/soma/translations/hu.json
index 82ec28ff4d7905..d013cb49fdf945 100644
--- a/homeassistant/components/soma/translations/hu.json
+++ b/homeassistant/components/soma/translations/hu.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.",
"connection_error": "Nem siker\u00fclt csatlakozni a SOMA Connecthez.",
"missing_configuration": "A Soma \u00f6sszetev\u0151 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.",
"result_error": "A SOMA Connect hiba\u00e1llapottal v\u00e1laszolt."
@@ -15,7 +16,7 @@
"port": "Port"
},
"description": "K\u00e9rj\u00fck, adja meg a SOMA Connect csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sait.",
- "title": "SOMA csatlakoz\u00e1s"
+ "title": "SOMA Connect"
}
}
}
diff --git a/homeassistant/components/soma/translations/id.json b/homeassistant/components/soma/translations/id.json
new file mode 100644
index 00000000000000..d512bd467976e9
--- /dev/null
+++ b/homeassistant/components/soma/translations/id.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Anda hanya dapat mengonfigurasi satu akun Soma.",
+ "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.",
+ "connection_error": "Gagal menyambungkan ke SOMA Connect.",
+ "missing_configuration": "Komponen Soma tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.",
+ "result_error": "SOMA Connect merespons dengan status kesalahan."
+ },
+ "create_entry": {
+ "default": "Berhasil mengautentikasi dengan Soma."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ },
+ "description": "Masukkan pengaturan koneksi SOMA Connect Anda.",
+ "title": "SOMA Connect"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/soma/translations/it.json b/homeassistant/components/soma/translations/it.json
index 0119fca7388ebf..237ce347cb0873 100644
--- a/homeassistant/components/soma/translations/it.json
+++ b/homeassistant/components/soma/translations/it.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_setup": "\u00c8 possibile configurare un solo account Soma.",
- "authorize_url_timeout": "Timeout durante la generazione dell'URL di autorizzazione.",
+ "authorize_url_timeout": "Tempo scaduto nella generazione dell'URL di autorizzazione.",
"connection_error": "Impossibile connettersi a SOMA Connect.",
"missing_configuration": "Il componente Soma non \u00e8 configurato. Si prega di seguire la documentazione.",
"result_error": "SOMA Connect ha risposto con stato di errore."
diff --git a/homeassistant/components/soma/translations/ko.json b/homeassistant/components/soma/translations/ko.json
index b987c7b2b73e7a..13a6fb03e83da4 100644
--- a/homeassistant/components/soma/translations/ko.json
+++ b/homeassistant/components/soma/translations/ko.json
@@ -2,13 +2,13 @@
"config": {
"abort": {
"already_setup": "\ud558\ub098\uc758 Soma \uacc4\uc815\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
- "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "connection_error": "SOMA Connect \uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.",
+ "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "connection_error": "SOMA Connect\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.",
"missing_configuration": "Soma \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
"result_error": "SOMA Connect \uac00 \uc624\ub958 \uc0c1\ud0dc\ub85c \uc751\ub2f5\ud588\uc2b5\ub2c8\ub2e4."
},
"create_entry": {
- "default": "Soma \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "default": "Soma\ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
},
"step": {
"user": {
diff --git a/homeassistant/components/soma/translations/no.json b/homeassistant/components/soma/translations/no.json
index f9b64dc848377b..a399f430329b51 100644
--- a/homeassistant/components/soma/translations/no.json
+++ b/homeassistant/components/soma/translations/no.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_setup": "Du kan bare konfigurere \u00e9n Soma-konto.",
- "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse",
+ "authorize_url_timeout": "Tidsavbrudd genererer godkjennelses-URL.",
"connection_error": "Kunne ikke koble til SOMA Connect.",
"missing_configuration": "Soma-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.",
"result_error": "SOMA Connect svarte med feilstatus."
diff --git a/homeassistant/components/soma/translations/tr.json b/homeassistant/components/soma/translations/tr.json
new file mode 100644
index 00000000000000..21a477c75a79c4
--- /dev/null
+++ b/homeassistant/components/soma/translations/tr.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/soma/translations/uk.json b/homeassistant/components/soma/translations/uk.json
new file mode 100644
index 00000000000000..0ec98301d62c93
--- /dev/null
+++ b/homeassistant/components/soma/translations/uk.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435.",
+ "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "connection_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 SOMA Connect.",
+ "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Soma \u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.",
+ "result_error": "SOMA Connect \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0432 \u0437\u0456 \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u043c \u043f\u043e\u043c\u0438\u043b\u043a\u0438."
+ },
+ "create_entry": {
+ "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e SOMA Connect.",
+ "title": "SOMA Connect"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py
index 2fc83ea71de5a0..9d67675f10e285 100644
--- a/homeassistant/components/somfy/__init__.py
+++ b/homeassistant/components/somfy/__init__.py
@@ -9,7 +9,7 @@
from homeassistant.components.somfy import config_flow
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
+from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_OPTIMISTIC
from homeassistant.core import callback
from homeassistant.helpers import (
config_entry_oauth2_flow,
@@ -25,7 +25,7 @@
)
from . import api
-from .const import API, CONF_OPTIMISTIC, COORDINATOR, DOMAIN
+from .const import API, COORDINATOR, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -48,7 +48,7 @@
extra=vol.ALLOW_EXTRA,
)
-SOMFY_COMPONENTS = ["climate", "cover", "sensor", "switch"]
+PLATFORMS = ["climate", "cover", "sensor", "switch"]
async def async_setup(hass, config):
@@ -108,7 +108,7 @@ async def _update_all_devices():
)
data[COORDINATOR] = coordinator
- await coordinator.async_refresh()
+ await coordinator.async_config_entry_first_refresh()
if all(not bool(device.states) for device in coordinator.data.values()):
_LOGGER.debug(
@@ -134,9 +134,9 @@ async def _update_all_devices():
model=hub.type,
)
- for component in SOMFY_COMPONENTS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -147,8 +147,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
hass.data[DOMAIN].pop(API, None)
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in SOMFY_COMPONENTS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
return True
@@ -188,7 +188,7 @@ def device_info(self):
"identifiers": {(DOMAIN, self.unique_id)},
"name": self.name,
"model": self.device.type,
- "via_hub": (DOMAIN, self.device.parent_id),
+ "via_device": (DOMAIN, self.device.parent_id),
# For the moment, Somfy only returns their own device.
"manufacturer": "Somfy",
}
diff --git a/homeassistant/components/somfy/api.py b/homeassistant/components/somfy/api.py
index a679af06d730db..43db2c29060982 100644
--- a/homeassistant/components/somfy/api.py
+++ b/homeassistant/components/somfy/api.py
@@ -1,6 +1,7 @@
"""API for Somfy bound to Home Assistant OAuth."""
+from __future__ import annotations
+
from asyncio import run_coroutine_threadsafe
-from typing import Dict, Union
from pymfy.api import somfy_api
@@ -27,7 +28,7 @@ def __init__(
def refresh_tokens(
self,
- ) -> Dict[str, Union[str, int]]:
+ ) -> dict[str, str | int]:
"""Refresh and return new Somfy tokens using Home Assistant OAuth2 session."""
run_coroutine_threadsafe(
self.session.async_ensure_token_valid(), self.hass.loop
diff --git a/homeassistant/components/somfy/climate.py b/homeassistant/components/somfy/climate.py
index 00a2738f4fefe1..66602aea3e6acd 100644
--- a/homeassistant/components/somfy/climate.py
+++ b/homeassistant/components/somfy/climate.py
@@ -1,6 +1,5 @@
"""Support for Somfy Thermostat."""
-
-from typing import List, Optional
+from __future__ import annotations
from pymfy.api.devices.category import Category
from pymfy.api.devices.thermostat import (
@@ -48,7 +47,6 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Somfy climate platform."""
-
domain_data = hass.data[DOMAIN]
coordinator = domain_data[COORDINATOR]
api = domain_data[API]
@@ -126,7 +124,7 @@ def hvac_mode(self) -> str:
return HVAC_MODES_MAPPING.get(self._climate.get_hvac_state())
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes.
HEAT and COOL mode are exclusive. End user has to enable a mode manually within the Somfy application.
@@ -145,13 +143,13 @@ def set_hvac_mode(self, hvac_mode: str) -> None:
)
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return the current preset mode."""
mode = self._climate.get_target_mode()
return PRESETS_MAPPING.get(mode)
@property
- def preset_modes(self) -> Optional[List[str]]:
+ def preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes."""
return list(PRESETS_MAPPING.values())
diff --git a/homeassistant/components/somfy/const.py b/homeassistant/components/somfy/const.py
index aca93be66cb1ae..128d6eb76bb9f1 100644
--- a/homeassistant/components/somfy/const.py
+++ b/homeassistant/components/somfy/const.py
@@ -3,4 +3,3 @@
DOMAIN = "somfy"
COORDINATOR = "coordinator"
API = "api"
-CONF_OPTIMISTIC = "optimistic"
diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py
index e7308558127032..d227bc31227e8a 100644
--- a/homeassistant/components/somfy/cover.py
+++ b/homeassistant/components/somfy/cover.py
@@ -18,11 +18,11 @@
SUPPORT_STOP_TILT,
CoverEntity,
)
-from homeassistant.const import STATE_CLOSED, STATE_OPEN
+from homeassistant.const import CONF_OPTIMISTIC, STATE_CLOSED, STATE_OPEN
from homeassistant.helpers.restore_state import RestoreEntity
from . import SomfyEntity
-from .const import API, CONF_OPTIMISTIC, COORDINATOR, DOMAIN
+from .const import API, COORDINATOR, DOMAIN
BLIND_DEVICE_CATEGORIES = {Category.INTERIOR_BLIND.value, Category.EXTERIOR_BLIND.value}
SHUTTER_DEVICE_CATEGORIES = {Category.EXTERIOR_BLIND.value}
@@ -35,7 +35,6 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Somfy cover platform."""
-
domain_data = hass.data[DOMAIN]
coordinator = domain_data[COORDINATOR]
api = domain_data[API]
diff --git a/homeassistant/components/somfy/sensor.py b/homeassistant/components/somfy/sensor.py
index 1becc929adc533..34283a1271c092 100644
--- a/homeassistant/components/somfy/sensor.py
+++ b/homeassistant/components/somfy/sensor.py
@@ -3,6 +3,7 @@
from pymfy.api.devices.category import Category
from pymfy.api.devices.thermostat import Thermostat
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE
from . import SomfyEntity
@@ -13,7 +14,6 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Somfy sensor platform."""
-
domain_data = hass.data[DOMAIN]
coordinator = domain_data[COORDINATOR]
api = domain_data[API]
@@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors)
-class SomfyThermostatBatterySensor(SomfyEntity):
+class SomfyThermostatBatterySensor(SomfyEntity, SensorEntity):
"""Representation of a Somfy thermostat battery."""
def __init__(self, coordinator, device_id, api):
diff --git a/homeassistant/components/somfy/switch.py b/homeassistant/components/somfy/switch.py
index 14328953367dcd..66eef99d6b5abb 100644
--- a/homeassistant/components/somfy/switch.py
+++ b/homeassistant/components/somfy/switch.py
@@ -10,7 +10,6 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Somfy switch platform."""
-
domain_data = hass.data[DOMAIN]
coordinator = domain_data[COORDINATOR]
api = domain_data[API]
diff --git a/homeassistant/components/somfy/translations/de.json b/homeassistant/components/somfy/translations/de.json
index 6b76e2f61befcc..29a959f48ce140 100644
--- a/homeassistant/components/somfy/translations/de.json
+++ b/homeassistant/components/somfy/translations/de.json
@@ -2,10 +2,12 @@
"config": {
"abort": {
"authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.",
- "missing_configuration": "Die Somfy-Komponente ist nicht konfiguriert. Folge bitte der Dokumentation."
+ "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.",
+ "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).",
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"create_entry": {
- "default": "Erfolgreich mit Somfy authentifiziert."
+ "default": "Erfolgreich authentifiziert"
},
"step": {
"pick_implementation": {
diff --git a/homeassistant/components/somfy/translations/hu.json b/homeassistant/components/somfy/translations/hu.json
index 86927570c85836..ce4e94b3399566 100644
--- a/homeassistant/components/somfy/translations/hu.json
+++ b/homeassistant/components/somfy/translations/hu.json
@@ -1,11 +1,17 @@
{
"config": {
+ "abort": {
+ "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.",
+ "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.",
+ "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
"create_entry": {
- "default": "Sikeres autentik\u00e1ci\u00f3"
+ "default": "Sikeres hiteles\u00edt\u00e9s"
},
"step": {
"pick_implementation": {
- "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert"
+ "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert"
}
}
}
diff --git a/homeassistant/components/somfy/translations/id.json b/homeassistant/components/somfy/translations/id.json
new file mode 100644
index 00000000000000..2d229de00d579f
--- /dev/null
+++ b/homeassistant/components/somfy/translations/id.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.",
+ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.",
+ "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "create_entry": {
+ "default": "Berhasil diautentikasi"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Pilih Metode Autentikasi"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/ko.json b/homeassistant/components/somfy/translations/ko.json
index 5119670f766d83..568c8d051163da 100644
--- a/homeassistant/components/somfy/translations/ko.json
+++ b/homeassistant/components/somfy/translations/ko.json
@@ -1,17 +1,17 @@
{
"config": {
"abort": {
- "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "missing_configuration": "Somfy \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
- "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})",
- "single_instance_allowed": "\uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c. \ud558\ub098\uc758 \uc124\uc815\ub9cc \uac00\ub2a5\ud568."
+ "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
+ "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"create_entry": {
- "default": "Somfy \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
"pick_implementation": {
- "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd"
+ "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/somfy/translations/nl.json b/homeassistant/components/somfy/translations/nl.json
index 423dbb6a2bb8e9..94305c7ae6f975 100644
--- a/homeassistant/components/somfy/translations/nl.json
+++ b/homeassistant/components/somfy/translations/nl.json
@@ -2,15 +2,16 @@
"config": {
"abort": {
"authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
- "missing_configuration": "Het Somfy-component is niet geconfigureerd. Gelieve de documentatie te volgen.",
+ "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.",
+ "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})",
"single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk."
},
"create_entry": {
- "default": "Succesvol geverifieerd met Somfy."
+ "default": "Succesvol geauthenticeerd"
},
"step": {
"pick_implementation": {
- "title": "Kies de authenticatie methode"
+ "title": "Kies een authenticatie methode"
}
}
}
diff --git a/homeassistant/components/somfy/translations/tr.json b/homeassistant/components/somfy/translations/tr.json
new file mode 100644
index 00000000000000..a152eb194683cb
--- /dev/null
+++ b/homeassistant/components/somfy/translations/tr.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/uk.json b/homeassistant/components/somfy/translations/uk.json
new file mode 100644
index 00000000000000..ebf7e41044eef6
--- /dev/null
+++ b/homeassistant/components/somfy/translations/uk.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.",
+ "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "create_entry": {
+ "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e."
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py
index d15ea029530eef..40240306dc473f 100644
--- a/homeassistant/components/somfy_mylink/__init__.py
+++ b/homeassistant/components/somfy_mylink/__init__.py
@@ -23,7 +23,7 @@
DEFAULT_PORT,
DOMAIN,
MYLINK_STATUS,
- SOMFY_MYLINK_COMPONENTS,
+ PLATFORMS,
)
CONFIG_OPTIONS = (CONF_DEFAULT_REVERSE, CONF_ENTITY_CONFIG)
@@ -101,13 +101,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
if not mylink_status or "error" in mylink_status:
_LOGGER.error(
- "mylink failed to setup because of an error: %s",
+ "Somfy Mylink failed to setup because of an error: %s",
mylink_status.get("error", {}).get(
"message", "Empty response from mylink device"
),
)
return False
+ if "result" not in mylink_status:
+ raise ConfigEntryNotReady("The Somfy MyLink device returned an empty result")
+
_async_migrate_entity_config(hass, entry, mylink_status)
undo_listener = entry.add_update_listener(_async_update_listener)
@@ -118,9 +121,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
UNDO_UPDATE_LISTENER: undo_listener,
}
- for component in SOMFY_MYLINK_COMPONENTS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -179,8 +182,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in SOMFY_MYLINK_COMPONENTS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py
index ce69d265b55c61..d1a1e19609ad38 100644
--- a/homeassistant/components/somfy_mylink/config_flow.py
+++ b/homeassistant/components/somfy_mylink/config_flow.py
@@ -19,9 +19,9 @@
CONF_TARGET_ID,
CONF_TARGET_NAME,
DEFAULT_PORT,
+ DOMAIN,
MYLINK_STATUS,
)
-from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
@@ -72,7 +72,6 @@ async def async_step_dhcp(self, dhcp_discovery):
self.host = dhcp_discovery[HOSTNAME]
self.mac = formatted_mac
self.ip_address = dhcp_discovery[IP_ADDRESS]
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {"ip": self.ip_address, "mac": self.mac}
return await self.async_step_user()
diff --git a/homeassistant/components/somfy_mylink/const.py b/homeassistant/components/somfy_mylink/const.py
index a7cbf864cd958e..bf58ee1af929a9 100644
--- a/homeassistant/components/somfy_mylink/const.py
+++ b/homeassistant/components/somfy_mylink/const.py
@@ -14,6 +14,6 @@
MYLINK_STATUS = "mylink_status"
DOMAIN = "somfy_mylink"
-SOMFY_MYLINK_COMPONENTS = ["cover"]
+PLATFORMS = ["cover"]
MANUFACTURER = "Somfy"
diff --git a/homeassistant/components/somfy_mylink/manifest.json b/homeassistant/components/somfy_mylink/manifest.json
index a7be33583d2784..a71661f57f4458 100644
--- a/homeassistant/components/somfy_mylink/manifest.json
+++ b/homeassistant/components/somfy_mylink/manifest.json
@@ -5,7 +5,7 @@
"requirements": [
"somfy-mylink-synergy==1.0.6"
],
- "codeowners": ["@bdraco"],
+ "codeowners": [],
"config_flow": true,
"dhcp": [{
"hostname":"somfy_*", "macaddress":"B8B7F1*"
diff --git a/homeassistant/components/somfy_mylink/translations/bg.json b/homeassistant/components/somfy_mylink/translations/bg.json
new file mode 100644
index 00000000000000..4983c9a14b265d
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/bg.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "port": "\u041f\u043e\u0440\u0442"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/ca.json b/homeassistant/components/somfy_mylink/translations/ca.json
new file mode 100644
index 00000000000000..93ae58ca2bfe6b
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/ca.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "flow_title": "Somfy MyLink {mac} ({ip})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "port": "Port",
+ "system_id": "ID del sistema"
+ },
+ "description": "L'ID de sistema es pot obtenir des de l'aplicaci\u00f3 MyLink dins de Integraci\u00f3, seleccionant qualsevol servei que no sigui al n\u00favol."
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "Ha fallat la connexi\u00f3"
+ },
+ "step": {
+ "entity_config": {
+ "data": {
+ "reverse": "La coberta est\u00e0 invertida"
+ },
+ "description": "Opcions de configuraci\u00f3 de `{entity_id}`",
+ "title": "Configura l'entitat"
+ },
+ "init": {
+ "data": {
+ "default_reverse": "Estat d'inversi\u00f3 predeterminat per a cobertes sense configurar",
+ "entity_id": "Configura una entitat espec\u00edfica.",
+ "target_id": "Opcions de configuraci\u00f3 de la coberta."
+ },
+ "title": "Configura opcions de MyLink"
+ },
+ "target_config": {
+ "data": {
+ "reverse": "La coberta est\u00e0 invertida"
+ },
+ "description": "Opcions de configuraci\u00f3 de `{target_name}`",
+ "title": "Configura coberta MyLink"
+ }
+ }
+ },
+ "title": "Somfy MyLink"
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/cs.json b/homeassistant/components/somfy_mylink/translations/cs.json
new file mode 100644
index 00000000000000..71e05b51544574
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/cs.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno"
+ },
+ "error": {
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
+ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
+ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
+ },
+ "flow_title": "Somfy MyLink {mac} ({ip})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hostitel",
+ "port": "Port"
+ }
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/de.json b/homeassistant/components/somfy_mylink/translations/de.json
new file mode 100644
index 00000000000000..4382ccd4a0c0f1
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/de.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "flow_title": "Somfy MyLink {mac} ({ip})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port",
+ "system_id": "System-ID"
+ }
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
+ "step": {
+ "entity_config": {
+ "description": "Optionen f\u00fcr `{entity_id}` konfigurieren",
+ "title": "Entit\u00e4t konfigurieren"
+ },
+ "init": {
+ "data": {
+ "entity_id": "Konfiguriere eine bestimmte Entit\u00e4t."
+ },
+ "title": "MyLink-Optionen konfigurieren"
+ },
+ "target_config": {
+ "description": "Konfiguriere die Optionen f\u00fcr `{target_name}`",
+ "title": "MyLink-Cover konfigurieren"
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/en.json b/homeassistant/components/somfy_mylink/translations/en.json
index ca3d83e402b35b..13115b36e5c26e 100644
--- a/homeassistant/components/somfy_mylink/translations/en.json
+++ b/homeassistant/components/somfy_mylink/translations/en.json
@@ -1,44 +1,53 @@
{
- "title": "Somfy MyLink",
- "config": {
- "flow_title": "Somfy MyLink {mac} ({ip})",
- "step": {
- "user": {
- "description": "The System ID can be obtained in the MyLink app under Integration by selecting any non-Cloud service.",
- "data": {
- "host": "[%key:common::config_flow::data::host%]",
- "port": "[%key:common::config_flow::data::port%]",
- "system_id": "System ID"
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "flow_title": "Somfy MyLink {mac} ({ip})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port",
+ "system_id": "System ID"
+ },
+ "description": "The System ID can be obtained in the MyLink app under Integration by selecting any non-Cloud service."
+ }
}
- }
},
- "error": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
- "unknown": "[%key:common::config_flow::error::unknown%]"
- },
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
- }
- },
- "options": {
- "abort": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
- },
- "step": {
- "init": {
- "title": "Configure MyLink Options",
- "data": {
- "target_id": "Configure options for a cover."
+ "options": {
+ "abort": {
+ "cannot_connect": "Failed to connect"
+ },
+ "step": {
+ "entity_config": {
+ "data": {
+ "reverse": "Cover is reversed"
+ },
+ "description": "Configure options for `{entity_id}`",
+ "title": "Configure Entity"
+ },
+ "init": {
+ "data": {
+ "default_reverse": "Default reversal status for unconfigured covers",
+ "entity_id": "Configure a specific entity.",
+ "target_id": "Configure options for a cover."
+ },
+ "title": "Configure MyLink Options"
+ },
+ "target_config": {
+ "data": {
+ "reverse": "Cover is reversed"
+ },
+ "description": "Configure options for `{target_name}`",
+ "title": "Configure MyLink Cover"
+ }
}
- },
- "target_config": {
- "title": "Configure MyLink Cover",
- "description": "Configure options for `{target_name}`",
- "data": {
- "reverse": "Cover is reversed"
- }
- }
- }
- }
+ },
+ "title": "Somfy MyLink"
}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/es.json b/homeassistant/components/somfy_mylink/translations/es.json
new file mode 100644
index 00000000000000..40d82a4522a89c
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/es.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositivo ya est\u00e1 configurado"
+ },
+ "error": {
+ "cannot_connect": "No se pudo conectar",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
+ "unknown": "Error inesperado"
+ },
+ "flow_title": "Somfy MyLink {mac} ({ip})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Puerto",
+ "system_id": "ID del sistema"
+ },
+ "description": "El ID del sistema se puede obtener en la aplicaci\u00f3n MyLink en Integraci\u00f3n seleccionando cualquier servicio que no sea de la nube."
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "No se pudo conectar"
+ },
+ "step": {
+ "entity_config": {
+ "data": {
+ "reverse": "La cubierta est\u00e1 invertida"
+ },
+ "description": "Configurar opciones para `{entity_id}`",
+ "title": "Configurar entidad"
+ },
+ "init": {
+ "data": {
+ "default_reverse": "Estado de inversi\u00f3n predeterminado para cubiertas no configuradas",
+ "entity_id": "Configurar una entidad espec\u00edfica.",
+ "target_id": "Configurar opciones para una cubierta."
+ },
+ "title": "Configurar opciones de MyLink"
+ },
+ "target_config": {
+ "data": {
+ "reverse": "La cubierta est\u00e1 invertida"
+ },
+ "description": "Configurar opciones para `{target_name}`",
+ "title": "Configurar la cubierta MyLink"
+ }
+ }
+ },
+ "title": "Somfy MyLink"
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/et.json b/homeassistant/components/somfy_mylink/translations/et.json
new file mode 100644
index 00000000000000..6d965220d7efc6
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/et.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_auth": "Vigane autentimine",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "flow_title": "Somfy MyLink {mac} ( {ip} )",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port",
+ "system_id": "S\u00fcsteemi ID"
+ },
+ "description": "S\u00fcsteemi ID saab rakenduse MyLink sidumise alt valides mis tahes mitte- pilveteenuse."
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "\u00dchendamine nurjus"
+ },
+ "step": {
+ "entity_config": {
+ "data": {
+ "reverse": "(Akna)kate t\u00f6\u00f6tab vastupidi"
+ },
+ "description": "Olemi {entity_id} suvandite seadmine",
+ "title": "Seadista olem"
+ },
+ "init": {
+ "data": {
+ "default_reverse": "Seadistamata (akna)katete vaikep\u00f6\u00f6rduse olek",
+ "entity_id": "Seadista konkreetne olem.",
+ "target_id": "Seadista (akna)katte suvandid"
+ },
+ "title": "Seadista MyLinki suvandid"
+ },
+ "target_config": {
+ "data": {
+ "reverse": "(Akna)kate liigub vastupidi"
+ },
+ "description": "Seadme `{target_name}` suvandite seadmine",
+ "title": "Seadista MyLink Cover"
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/fr.json b/homeassistant/components/somfy_mylink/translations/fr.json
new file mode 100644
index 00000000000000..bee2ea3ba1361e
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/fr.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'appareil est d\u00e9ja configur\u00e9 "
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de la connexion ",
+ "invalid_auth": "Authentification invalide ",
+ "unknown": "Erreur inattendue"
+ },
+ "flow_title": "Somfy MyLink {mac} ( {ip} )",
+ "step": {
+ "user": {
+ "data": {
+ "host": "H\u00f4te",
+ "port": "Port",
+ "system_id": "ID syst\u00e8me"
+ },
+ "description": "L'ID syst\u00e8me peut \u00eatre obtenu dans l'application MyLink sous Int\u00e9gration en s\u00e9lectionnant n'importe quel service non cloud."
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "Echec de connection"
+ },
+ "step": {
+ "entity_config": {
+ "data": {
+ "reverse": "La couverture est invers\u00e9e"
+ },
+ "description": "Configurer les options pour \u00ab {entity_id} \u00bb",
+ "title": "Configurez une entit\u00e9 sp\u00e9cifique"
+ },
+ "init": {
+ "data": {
+ "default_reverse": "Statut d'inversion par d\u00e9faut pour les couvertures non configur\u00e9es",
+ "entity_id": "Configurez une entit\u00e9 sp\u00e9cifique.",
+ "target_id": "Configurez les options pour la couverture."
+ },
+ "title": "Configurer les options MyLink"
+ },
+ "target_config": {
+ "data": {
+ "reverse": "La couverture est invers\u00e9e"
+ },
+ "description": "Configurer les options pour \u00ab {target_name} \u00bb",
+ "title": "Configurer la couverture MyLink"
+ }
+ }
+ },
+ "title": "Somfy MyLink"
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/he.json b/homeassistant/components/somfy_mylink/translations/he.json
new file mode 100644
index 00000000000000..9af5985ac45883
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/he.json
@@ -0,0 +1,9 @@
+{
+ "options": {
+ "step": {
+ "init": {
+ "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc MyLink"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/hu.json b/homeassistant/components/somfy_mylink/translations/hu.json
new file mode 100644
index 00000000000000..08d0db14866e8f
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/hu.json
@@ -0,0 +1,42 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "flow_title": "Somfy MyLink {mac} ({ip})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "port": "Port"
+ }
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "step": {
+ "entity_config": {
+ "title": "Entit\u00e1s konfigur\u00e1l\u00e1sa"
+ },
+ "init": {
+ "data": {
+ "target_id": "Az \u00e1rny\u00e9kol\u00f3 be\u00e1ll\u00edt\u00e1sainak konfigur\u00e1l\u00e1sa."
+ },
+ "title": "Mylink be\u00e1ll\u00edt\u00e1sok konfigur\u00e1l\u00e1sa"
+ },
+ "target_config": {
+ "description": "A(z) `{target_name}` be\u00e1ll\u00edt\u00e1sainak konfigur\u00e1l\u00e1sa",
+ "title": "MyLink \u00e1rny\u00e9kol\u00f3 konfigur\u00e1l\u00e1sa"
+ }
+ }
+ },
+ "title": "Somfy MyLink"
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/id.json b/homeassistant/components/somfy_mylink/translations/id.json
new file mode 100644
index 00000000000000..0203ae421e2ce6
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/id.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "flow_title": "Somfy MyLink {mac} ({ip})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port",
+ "system_id": "ID Sistem"
+ },
+ "description": "ID Sistem dapat diperoleh di aplikasi MyLink di bawah bagian Integrasi dengan memilih layanan non-Cloud."
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "entity_config": {
+ "data": {
+ "reverse": "Penutup dibalik"
+ },
+ "description": "Konfigurasikan opsi untuk `{entity_id}`",
+ "title": "Konfigurasikan Entitas"
+ },
+ "init": {
+ "data": {
+ "default_reverse": "Status pembalikan baku untuk penutup yang belum dikonfigurasi",
+ "entity_id": "Konfigurasikan entitas tertentu.",
+ "target_id": "Konfigurasikan opsi untuk penutup."
+ },
+ "title": "Konfigurasikan Opsi MyLink"
+ },
+ "target_config": {
+ "data": {
+ "reverse": "Penutup dibalik"
+ },
+ "description": "Konfigurasikan opsi untuk `{target_name}`",
+ "title": "Konfigurasikan Cover MyLink"
+ }
+ }
+ },
+ "title": "Somfy MyLink"
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/it.json b/homeassistant/components/somfy_mylink/translations/it.json
new file mode 100644
index 00000000000000..ce049782c43364
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/it.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_auth": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "flow_title": "Somfy MyLink {mac} ({ip})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Porta",
+ "system_id": "ID sistema"
+ },
+ "description": "L'ID sistema pu\u00f2 essere ottenuto nell'app MyLink alla voce Integrazione selezionando qualsiasi servizio non-Cloud."
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "Impossibile connettersi"
+ },
+ "step": {
+ "entity_config": {
+ "data": {
+ "reverse": "La tapparella \u00e8 invertita"
+ },
+ "description": "Configura le opzioni per `{entity_id}`",
+ "title": "Configura entit\u00e0"
+ },
+ "init": {
+ "data": {
+ "default_reverse": "Stato d'inversione predefinito per le tapparelle non configurate",
+ "entity_id": "Configura un'entit\u00e0 specifica.",
+ "target_id": "Configura opzioni per una tapparella"
+ },
+ "title": "Configura le opzioni MyLink"
+ },
+ "target_config": {
+ "data": {
+ "reverse": "La tapparella \u00e8 invertita"
+ },
+ "description": "Configura le opzioni per `{target_name}`",
+ "title": "Configura tapparelle MyLink"
+ }
+ }
+ },
+ "title": "Somfy MyLink"
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/ko.json b/homeassistant/components/somfy_mylink/translations/ko.json
new file mode 100644
index 00000000000000..b099d3d6ed842d
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/ko.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "flow_title": "Somfy MyLink: {mac} ({ip})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "port": "\ud3ec\ud2b8",
+ "system_id": "\uc2dc\uc2a4\ud15c ID"
+ },
+ "description": "\uc2dc\uc2a4\ud15c ID\ub294 MyLink \uc571\uc758 Integration\uc5d0\uc11c non-Cloud service\ub97c \uc120\ud0dd\ud558\uc5ec \uc5bb\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "entity_config": {
+ "data": {
+ "reverse": "\uc5ec\ub2eb\uc774\uac00 \ubc18\uc804\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "description": "`{entity_id}`\uc5d0 \ub300\ud55c \uc635\uc158 \uad6c\uc131\ud558\uae30",
+ "title": "\uad6c\uc131\uc694\uc18c \uad6c\uc131\ud558\uae30"
+ },
+ "init": {
+ "data": {
+ "default_reverse": "\uad6c\uc131\ub418\uc9c0 \uc54a\uc740 \uc5ec\ub2eb\uc774\uc5d0 \ub300\ud55c \uae30\ubcf8 \ubc18\uc804 \uc0c1\ud0dc",
+ "entity_id": "\ud2b9\uc815 \uad6c\uc131\uc694\uc18c\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.",
+ "target_id": "\uc5ec\ub2eb\uc774\uc5d0 \ub300\ud55c \uc635\uc158 \uad6c\uc131\ud558\uae30"
+ },
+ "title": "MyLink \uc635\uc158 \uad6c\uc131\ud558\uae30"
+ },
+ "target_config": {
+ "data": {
+ "reverse": "\uc5ec\ub2eb\uc774\uac00 \ubc18\uc804\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "description": "`{target_name}`\uc5d0 \ub300\ud55c \uc635\uc158 \uad6c\uc131\ud558\uae30",
+ "title": "MyLink \uc5ec\ub2eb\uc774 \uad6c\uc131\ud558\uae30"
+ }
+ }
+ },
+ "title": "Somfy MyLink"
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/lb.json b/homeassistant/components/somfy_mylink/translations/lb.json
new file mode 100644
index 00000000000000..efaba3ab497400
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/lb.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparat ass scho konfigur\u00e9iert"
+ },
+ "error": {
+ "cannot_connect": "Feeler beim verbannen",
+ "invalid_auth": "Ong\u00eblteg Authentifikatioun",
+ "unknown": "Onerwaarte Feeler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port",
+ "system_id": "System ID"
+ }
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "Feeler beim verbannen"
+ }
+ },
+ "title": "Somfy MyLink"
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/nl.json b/homeassistant/components/somfy_mylink/translations/nl.json
new file mode 100644
index 00000000000000..b900e46bee4d97
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/nl.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
+ },
+ "flow_title": "Somfy MyLink {mac} ({ip})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Poort",
+ "system_id": "Systeem-ID"
+ },
+ "description": "De systeem-id kan worden verkregen in de MyLink app onder Integratie door een niet-Cloud service te selecteren."
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "Kan geen verbinding maken"
+ },
+ "step": {
+ "entity_config": {
+ "data": {
+ "reverse": "Cover is omgekeerd"
+ },
+ "description": "Configureer opties voor `{entity_id}`",
+ "title": "Entiteit configureren"
+ },
+ "init": {
+ "data": {
+ "default_reverse": "Standaard omkeerstatus voor niet-geconfigureerde covers",
+ "entity_id": "Configureer een specifieke entiteit.",
+ "target_id": "Configureer opties voor een cover."
+ },
+ "title": "Configureer MyLink-opties"
+ },
+ "target_config": {
+ "data": {
+ "reverse": "Cover is omgekeerd"
+ },
+ "description": "Configureer opties voor ' {target_name} '",
+ "title": "Configureer MyLink Cover"
+ }
+ }
+ },
+ "title": "Somfy MyLink"
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/no.json b/homeassistant/components/somfy_mylink/translations/no.json
new file mode 100644
index 00000000000000..5b9b6608c256fa
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/no.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_auth": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "flow_title": "",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Vert",
+ "port": "Port",
+ "system_id": ""
+ },
+ "description": "System-ID-en kan f\u00e5s i MyLink-appen under Integrasjon ved \u00e5 velge en hvilken som helst ikke-Cloud-tjeneste."
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "Tilkobling mislyktes"
+ },
+ "step": {
+ "entity_config": {
+ "data": {
+ "reverse": "Rullegardinet reverseres"
+ },
+ "description": "Konfigurer alternativer for \"{entity_id}\"",
+ "title": "Konfigurer enhet"
+ },
+ "init": {
+ "data": {
+ "default_reverse": "Standard tilbakef\u00f8ringsstatus for ukonfigurerte rullegardiner",
+ "entity_id": "Konfigurer en bestemt enhet.",
+ "target_id": "Konfigurer alternativer for et rullgardin"
+ },
+ "title": "Konfigurere MyLink-alternativer"
+ },
+ "target_config": {
+ "data": {
+ "reverse": "Rullegardinet reverseres"
+ },
+ "description": "Konfigurer alternativer for \"{target_name}\"",
+ "title": "Konfigurer MyLink-deksel"
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/pl.json b/homeassistant/components/somfy_mylink/translations/pl.json
new file mode 100644
index 00000000000000..7e49ecb2bcaa27
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/pl.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "flow_title": "Somfy MyLink {mac} ({ip})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP",
+ "port": "Port",
+ "system_id": "Identyfikator systemu"
+ },
+ "description": "Identyfikator systemu mo\u017cna uzyska\u0107 w aplikacji MyLink w sekcji Integracja, wybieraj\u0105c dowoln\u0105 us\u0142ug\u0119 spoza chmury."
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ },
+ "step": {
+ "entity_config": {
+ "data": {
+ "reverse": "Roleta/pokrywa jest odwr\u00f3cona"
+ },
+ "description": "Konfiguracja opcji dla \"{entity_id}\"",
+ "title": "Konfigurowanie encji"
+ },
+ "init": {
+ "data": {
+ "default_reverse": "Domy\u015blny stan odwr\u00f3cenia nieskonfigurowanych rolet/pokryw",
+ "entity_id": "Skonfiguruj okre\u015blon\u0105 encj\u0119.",
+ "target_id": "Konfiguracja opcji rolety"
+ },
+ "title": "Konfiguracja opcji MyLink"
+ },
+ "target_config": {
+ "data": {
+ "reverse": "Roleta/pokrywa jest odwr\u00f3cona"
+ },
+ "description": "Konfiguracja opcji dla \"{target_name}\"",
+ "title": "Konfiguracja rolety MyLink"
+ }
+ }
+ },
+ "title": "Somfy MyLink"
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/ru.json b/homeassistant/components/somfy_mylink/translations/ru.json
new file mode 100644
index 00000000000000..7c98166433571c
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/ru.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "flow_title": "Somfy MyLink {mac} ({ip})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442",
+ "system_id": "System ID"
+ },
+ "description": "System ID \u043c\u043e\u0436\u043d\u043e \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 MyLink \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u00ab\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u00bb, \u0432\u044b\u0431\u0440\u0430\u0432 \u043b\u044e\u0431\u0443\u044e \u043d\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u0443\u044e \u0441\u043b\u0443\u0436\u0431\u0443."
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
+ },
+ "step": {
+ "entity_config": {
+ "data": {
+ "reverse": "\u0418\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0434\u043b\u044f \u0448\u0442\u043e\u0440 \u0438 \u0436\u0430\u043b\u044e\u0437\u0438"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0434\u043b\u044f `{entity_id}`",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u0430"
+ },
+ "init": {
+ "data": {
+ "default_reverse": "\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0438\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0434\u043b\u044f \u0448\u0442\u043e\u0440 \u0438 \u0436\u0430\u043b\u044e\u0437\u0438",
+ "entity_id": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430",
+ "target_id": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0448\u0442\u043e\u0440 \u0438\u043b\u0438 \u0436\u0430\u043b\u044e\u0437\u0438."
+ },
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 MyLink"
+ },
+ "target_config": {
+ "data": {
+ "reverse": "\u0418\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0434\u043b\u044f \u0448\u0442\u043e\u0440 \u0438 \u0436\u0430\u043b\u044e\u0437\u0438"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0434\u043b\u044f `{target_name}`",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 MyLink Cover"
+ }
+ }
+ },
+ "title": "Somfy MyLink"
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/tr.json b/homeassistant/components/somfy_mylink/translations/tr.json
new file mode 100644
index 00000000000000..29530b65659cfb
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/tr.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "flow_title": "Somfy MyLink {mac} ( {ip} )",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port",
+ "system_id": "Sistem ID"
+ },
+ "description": "Sistem Kimli\u011fi, MyLink uygulamas\u0131nda Entegrasyon alt\u0131nda Bulut d\u0131\u015f\u0131 herhangi bir hizmet se\u00e7ilerek elde edilebilir."
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "entity_config": {
+ "data": {
+ "reverse": "Kapak ters \u00e7evrildi"
+ },
+ "description": "'{entity_id}' i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n",
+ "title": "Varl\u0131\u011f\u0131 Yap\u0131land\u0131r"
+ },
+ "init": {
+ "data": {
+ "default_reverse": "Yap\u0131land\u0131r\u0131lmam\u0131\u015f kapaklar i\u00e7in varsay\u0131lan geri alma durumu",
+ "entity_id": "Belirli bir varl\u0131\u011f\u0131 yap\u0131land\u0131r\u0131n.",
+ "target_id": "Kapak i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n."
+ },
+ "title": "MyLink Se\u00e7eneklerini Yap\u0131land\u0131r\u0131n"
+ },
+ "target_config": {
+ "data": {
+ "reverse": "Kapak ters \u00e7evrildi"
+ },
+ "description": "'{target_name}' i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n",
+ "title": "MyLink Kapa\u011f\u0131n\u0131 Yap\u0131land\u0131r\u0131n"
+ }
+ }
+ },
+ "title": "Somfy MyLink"
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/uk.json b/homeassistant/components/somfy_mylink/translations/uk.json
new file mode 100644
index 00000000000000..2d251531340042
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/uk.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442",
+ "system_id": "System ID"
+ },
+ "description": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0441\u0438\u0441\u0442\u0435\u043c\u0438 \u043c\u043e\u0436\u043d\u0430 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0432 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0456 MyLink \u0443 \u0440\u043e\u0437\u0434\u0456\u043b\u0456 \u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f, \u0432\u0438\u0431\u0440\u0430\u0432\u0448\u0438 \u0431\u0443\u0434\u044c-\u044f\u043a\u0443 \u043d\u0435\u0445\u043c\u0430\u0440\u043d\u0443 \u0441\u043b\u0443\u0436\u0431\u0443."
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "entity_config": {
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u0434\u043b\u044f \"{entity_id}\"",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c"
+ },
+ "init": {
+ "data": {
+ "entity_id": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u043f\u0435\u0446\u0438\u0444\u0456\u0447\u043d\u043e\u0457 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456."
+ },
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0435\u0439 MyLink"
+ }
+ }
+ },
+ "title": "Somfy MyLink"
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy_mylink/translations/zh-Hant.json b/homeassistant/components/somfy_mylink/translations/zh-Hant.json
new file mode 100644
index 00000000000000..2abb6a64f7c8ab
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/zh-Hant.json
@@ -0,0 +1,53 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "flow_title": "Somfy MyLink {mac} ({ip})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "port": "\u901a\u8a0a\u57e0",
+ "system_id": "\u7cfb\u7d71 ID"
+ },
+ "description": "\u7cfb\u7d71 ID \u53ef\u4ee5\u65bc\u6574\u5408\u5167\u7684 MyLink app \u9078\u64c7\u975e\u96f2\u7aef\u670d\u52d9\u4e2d\u627e\u5230\u3002"
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
+ },
+ "step": {
+ "entity_config": {
+ "data": {
+ "reverse": "\u7a97\u7c3e\u53cd\u5411"
+ },
+ "description": "`{entity_id}` \u8a2d\u5b9a\u9078\u9805",
+ "title": "\u8a2d\u5b9a\u5be6\u9ad4"
+ },
+ "init": {
+ "data": {
+ "default_reverse": "\u672a\u8a2d\u5b9a\u7a97\u7c3e\u9810\u8a2d\u70ba\u53cd\u5411",
+ "entity_id": "\u8a2d\u5b9a\u7279\u5b9a\u5be6\u9ad4\u3002",
+ "target_id": "\u7a97\u7c3e\u8a2d\u5b9a\u9078\u9805\u3002"
+ },
+ "title": "MyLink \u8a2d\u5b9a\u9078\u9805"
+ },
+ "target_config": {
+ "data": {
+ "reverse": "\u7a97\u7c3e\u53cd\u5411"
+ },
+ "description": "`{target_name}` \u8a2d\u5b9a\u9078\u9805",
+ "title": "\u8a2d\u5b9a MyLink \u7a97\u7c3e"
+ }
+ }
+ },
+ "title": "Somfy MyLink"
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py
index 3e2a5498c553c2..946d9b1e047604 100644
--- a/homeassistant/components/sonarr/__init__.py
+++ b/homeassistant/components/sonarr/__init__.py
@@ -1,8 +1,10 @@
"""The Sonarr component."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
import logging
-from typing import Any, Dict
+from typing import Any
from sonarr import Sonarr, SonarrAccessRestricted, SonarrError
@@ -40,7 +42,7 @@
_LOGGER = logging.getLogger(__name__)
-async def async_setup(hass: HomeAssistantType, config: Dict) -> bool:
+async def async_setup(hass: HomeAssistantType, config: dict) -> bool:
"""Set up the Sonarr component."""
hass.data.setdefault(DOMAIN, {})
return True
@@ -84,9 +86,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
DATA_UNDO_UPDATE_LISTENER: undo_listener,
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -97,8 +99,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -164,7 +166,7 @@ def entity_registry_enabled_default(self) -> bool:
return self._enabled_default
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about the application."""
if self._device_id is None:
return None
diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py
index fc11790356a431..fd7315585dcba5 100644
--- a/homeassistant/components/sonarr/config_flow.py
+++ b/homeassistant/components/sonarr/config_flow.py
@@ -1,6 +1,8 @@
"""Config flow for Sonarr."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from sonarr import Sonarr, SonarrAccessRestricted, SonarrError
import voluptuous as vol
@@ -27,13 +29,13 @@
DEFAULT_UPCOMING_DAYS,
DEFAULT_VERIFY_SSL,
DEFAULT_WANTED_MAX_ITEMS,
+ DOMAIN,
)
-from .const import DOMAIN # pylint: disable=unused-import
_LOGGER = logging.getLogger(__name__)
-async def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]:
+async def validate_input(hass: HomeAssistantType, data: dict) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
@@ -73,9 +75,7 @@ def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return SonarrOptionsFlowHandler(config_entry)
- async def async_step_reauth(
- self, data: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ async def async_step_reauth(self, data: ConfigType | None = None) -> dict[str, Any]:
"""Handle configuration by re-auth."""
self._reauth = True
self._entry_data = dict(data)
@@ -84,8 +84,8 @@ async def async_step_reauth(
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
- self, user_input: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, user_input: ConfigType | None = None
+ ) -> dict[str, Any]:
"""Confirm reauth dialog."""
if user_input is None:
return self.async_show_form(
@@ -98,8 +98,8 @@ async def async_step_reauth_confirm(
return await self.async_step_user()
async def async_step_user(
- self, user_input: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, user_input: ConfigType | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initiated by the user."""
errors = {}
@@ -138,7 +138,7 @@ async def async_step_user(
async def _async_reauth_update_entry(
self, entry_id: str, data: dict
- ) -> Dict[str, Any]:
+ ) -> dict[str, Any]:
"""Update existing config entry."""
entry = self.hass.config_entries.async_get_entry(entry_id)
self.hass.config_entries.async_update_entry(entry, data=data)
@@ -146,7 +146,7 @@ async def _async_reauth_update_entry(
return self.async_abort(reason="reauth_successful")
- def _get_user_data_schema(self) -> Dict[str, Any]:
+ def _get_user_data_schema(self) -> dict[str, Any]:
"""Get the data schema to display user form."""
if self._reauth:
return {vol.Required(CONF_API_KEY): str}
@@ -174,7 +174,7 @@ def __init__(self, config_entry):
"""Initialize options flow."""
self.config_entry = config_entry
- async def async_step_init(self, user_input: Optional[ConfigType] = None):
+ async def async_step_init(self, user_input: ConfigType | None = None):
"""Manage Sonarr options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py
index 8a625846744533..3446130433e817 100644
--- a/homeassistant/components/sonarr/sensor.py
+++ b/homeassistant/components/sonarr/sensor.py
@@ -1,10 +1,13 @@
"""Support for Sonarr sensors."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Any, Callable, Dict, List, Optional
+from typing import Any, Callable
from sonarr import Sonarr, SonarrConnectionError, SonarrError
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import DATA_GIGABYTES
from homeassistant.helpers.entity import Entity
@@ -20,7 +23,7 @@
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up Sonarr sensors based on a config entry."""
options = entry.options
@@ -63,7 +66,7 @@ async def handler(self, *args, **kwargs):
return handler
-class SonarrSensor(SonarrEntity):
+class SonarrSensor(SonarrEntity, SensorEntity):
"""Implementation of the Sonarr sensor."""
def __init__(
@@ -75,7 +78,7 @@ def __init__(
icon: str,
key: str,
name: str,
- unit_of_measurement: Optional[str] = None,
+ unit_of_measurement: str | None = None,
) -> None:
"""Initialize Sonarr sensor."""
self._unit_of_measurement = unit_of_measurement
@@ -131,7 +134,7 @@ async def async_update(self) -> None:
self._commands = await self.sonarr.commands()
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the entity."""
attrs = {}
@@ -172,7 +175,7 @@ async def async_update(self) -> None:
self._total_free = sum([disk.free for disk in self._disks])
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the entity."""
attrs = {}
@@ -217,7 +220,7 @@ async def async_update(self) -> None:
self._queue = await self.sonarr.queue()
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the entity."""
attrs = {}
@@ -258,7 +261,7 @@ async def async_update(self) -> None:
self._items = await self.sonarr.series()
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the entity."""
attrs = {}
@@ -301,7 +304,7 @@ async def async_update(self) -> None:
)
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the entity."""
attrs = {}
@@ -323,7 +326,7 @@ def __init__(self, sonarr: Sonarr, entry_id: str, max_items: int = 10) -> None:
"""Initialize Sonarr Wanted sensor."""
self._max_items = max_items
self._results = None
- self._total: Optional[int] = None
+ self._total: int | None = None
super().__init__(
sonarr=sonarr,
@@ -342,7 +345,7 @@ async def async_update(self) -> None:
self._total = self._results.total
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the entity."""
attrs = {}
@@ -354,6 +357,6 @@ def device_state_attributes(self) -> Optional[Dict[str, Any]]:
return attrs
@property
- def state(self) -> Optional[int]:
+ def state(self) -> int | None:
"""Return the state of the sensor."""
return self._total
diff --git a/homeassistant/components/sonarr/translations/de.json b/homeassistant/components/sonarr/translations/de.json
index 3abc6b45ef39a9..b4ceeeb43b7674 100644
--- a/homeassistant/components/sonarr/translations/de.json
+++ b/homeassistant/components/sonarr/translations/de.json
@@ -1,19 +1,38 @@
{
"config": {
"abort": {
+ "already_configured": "Der Dienst ist bereits konfiguriert",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich",
"unknown": "Unerwateter Fehler"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen"
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
},
"flow_title": "Sonarr: {name}",
"step": {
+ "reauth_confirm": {
+ "description": "Die Sonarr-Integration muss manuell mit der Sonarr-API, die unter {host} gehostet wird, neu authentifiziert werden",
+ "title": "Integration erneut authentifizieren"
+ },
"user": {
"data": {
"api_key": "API Schl\u00fcssel",
"base_path": "Pfad zur API",
"host": "Host",
- "port": "Port"
+ "port": "Port",
+ "ssl": "Verwendet ein SSL-Zertifikat",
+ "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "upcoming_days": "Anzahl der anzuzeigenden Tage",
+ "wanted_max_items": "Maximale Anzahl der anzuzeigenden gesuchten Elemente"
}
}
}
diff --git a/homeassistant/components/sonarr/translations/et.json b/homeassistant/components/sonarr/translations/et.json
index 957b3c74eae0f4..c95b2e9dc88d90 100644
--- a/homeassistant/components/sonarr/translations/et.json
+++ b/homeassistant/components/sonarr/translations/et.json
@@ -13,7 +13,7 @@
"step": {
"reauth_confirm": {
"description": "Sonarr-i sidumine tuleb k\u00e4sitsi taastuvastada Sonarr API abil: {host}",
- "title": "Autentige uuesti Sonarriga"
+ "title": "Autendi Sonarriga uuesti"
},
"user": {
"data": {
diff --git a/homeassistant/components/sonarr/translations/hu.json b/homeassistant/components/sonarr/translations/hu.json
index f5301e874eae05..e3fa0b5ff21928 100644
--- a/homeassistant/components/sonarr/translations/hu.json
+++ b/homeassistant/components/sonarr/translations/hu.json
@@ -1,7 +1,39 @@
{
"config": {
"abort": {
- "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
+ },
+ "flow_title": "Sonarr: {name}",
+ "step": {
+ "reauth_confirm": {
+ "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se"
+ },
+ "user": {
+ "data": {
+ "api_key": "API kulcs",
+ "base_path": "El\u00e9r\u00e9si \u00fat az API-hoz",
+ "host": "Hoszt",
+ "port": "Port",
+ "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata",
+ "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "upcoming_days": "A megjelen\u00edteni k\u00edv\u00e1nt k\u00f6vetkez\u0151 napok sz\u00e1ma",
+ "wanted_max_items": "A megjelen\u00edteni k\u00edv\u00e1nt elemek maxim\u00e1lis sz\u00e1ma"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/sonarr/translations/id.json b/homeassistant/components/sonarr/translations/id.json
new file mode 100644
index 00000000000000..ffaf1d226047f6
--- /dev/null
+++ b/homeassistant/components/sonarr/translations/id.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi",
+ "reauth_successful": "Autentikasi ulang berhasil",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "flow_title": "Sonarr: {name}",
+ "step": {
+ "reauth_confirm": {
+ "description": "Integrasi Sonarr perlu diautentikasi ulang secara manual dengan API Sonarr yang dihosting di: {host}",
+ "title": "Autentikasi Ulang Integrasi"
+ },
+ "user": {
+ "data": {
+ "api_key": "Kunci API",
+ "base_path": "Jalur ke API",
+ "host": "Host",
+ "port": "Port",
+ "ssl": "Menggunakan sertifikat SSL",
+ "verify_ssl": "Verifikasi sertifikat SSL"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "upcoming_days": "Jumlah hari mendatang untuk ditampilkan",
+ "wanted_max_items": "Jumlah maksimal item yang diinginkan untuk ditampilkan"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonarr/translations/it.json b/homeassistant/components/sonarr/translations/it.json
index 1a383201ab1c31..71a4cca729e1ce 100644
--- a/homeassistant/components/sonarr/translations/it.json
+++ b/homeassistant/components/sonarr/translations/it.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Il servizio \u00e8 gi\u00e0 configurato",
- "reauth_successful": "La riautenticazione ha avuto successo",
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente",
"unknown": "Errore imprevisto"
},
"error": {
@@ -13,7 +13,7 @@
"step": {
"reauth_confirm": {
"description": "L'integrazione di Sonarr deve essere nuovamente autenticata manualmente con l'API Sonarr ospitata su: {host}",
- "title": "Reautenticare l'integrazione"
+ "title": "Autenticare nuovamente l'integrazione"
},
"user": {
"data": {
diff --git a/homeassistant/components/sonarr/translations/ko.json b/homeassistant/components/sonarr/translations/ko.json
index fe650991778f8d..fbff72e46f20ae 100644
--- a/homeassistant/components/sonarr/translations/ko.json
+++ b/homeassistant/components/sonarr/translations/ko.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
@@ -10,14 +11,18 @@
},
"flow_title": "Sonarr: {name}",
"step": {
+ "reauth_confirm": {
+ "description": "Sonarr \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 {host}\uc5d0\uc11c \ud638\uc2a4\ud305\ub418\ub294 Sonarr API\ub85c \uc218\ub3d9\uc73c\ub85c \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4",
+ "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30"
+ },
"user": {
"data": {
"api_key": "API \ud0a4",
"base_path": "API \uacbd\ub85c",
"host": "\ud638\uc2a4\ud2b8",
"port": "\ud3ec\ud2b8",
- "ssl": "Sonarr \ub294 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4",
- "verify_ssl": "Sonarr \ub294 \uc62c\ubc14\ub978 \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4"
+ "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9",
+ "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778"
}
}
}
diff --git a/homeassistant/components/sonarr/translations/nl.json b/homeassistant/components/sonarr/translations/nl.json
index 58db7f57dd4f28..3203f3ac1281fb 100644
--- a/homeassistant/components/sonarr/translations/nl.json
+++ b/homeassistant/components/sonarr/translations/nl.json
@@ -2,16 +2,23 @@
"config": {
"abort": {
"already_configured": "Service is al geconfigureerd",
+ "reauth_successful": "Herauthenticatie was succesvol",
"unknown": "Onverwachte fout"
},
"error": {
"cannot_connect": "Kon niet verbinden",
"invalid_auth": "Ongeldige authenticatie"
},
+ "flow_title": "Sonarr: {name}",
"step": {
+ "reauth_confirm": {
+ "description": "De Sonarr-integratie moet handmatig opnieuw worden geverifieerd met de Sonarr-API die wordt gehost op: {host}",
+ "title": "Verifieer de integratie opnieuw"
+ },
"user": {
"data": {
"api_key": "API-sleutel",
+ "base_path": "Pad naar API",
"host": "Host",
"port": "Poort",
"ssl": "Maakt gebruik van een SSL-certificaat",
@@ -19,5 +26,15 @@
}
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "upcoming_days": "Aantal komende dagen om weer te geven",
+ "wanted_max_items": "Maximaal aantal gewenste items dat moet worden weergegeven"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/sonarr/translations/ru.json b/homeassistant/components/sonarr/translations/ru.json
index 1b6345d4563694..6bbb204b69e3f2 100644
--- a/homeassistant/components/sonarr/translations/ru.json
+++ b/homeassistant/components/sonarr/translations/ru.json
@@ -7,13 +7,13 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"flow_title": "Sonarr: {name}",
"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 Sonarr \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: {host}",
- "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f"
+ "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": {
diff --git a/homeassistant/components/sonarr/translations/tr.json b/homeassistant/components/sonarr/translations/tr.json
new file mode 100644
index 00000000000000..eadf010004587c
--- /dev/null
+++ b/homeassistant/components/sonarr/translations/tr.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu",
+ "unknown": "Beklenmeyen hata"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Anahtar\u0131",
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonarr/translations/uk.json b/homeassistant/components/sonarr/translations/uk.json
new file mode 100644
index 00000000000000..0b6b7acf26decb
--- /dev/null
+++ b/homeassistant/components/sonarr/translations/uk.json
@@ -0,0 +1,40 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "flow_title": "Sonarr: {name}",
+ "step": {
+ "reauth_confirm": {
+ "description": "\u041f\u043e\u0442\u0440\u0456\u0431\u043d\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e API Sonarr \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e: {host}",
+ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e"
+ },
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "base_path": "\u0428\u043b\u044f\u0445 \u0434\u043e API",
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442",
+ "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL",
+ "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "upcoming_days": "\u041a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u043c\u0430\u0439\u0431\u0443\u0442\u043d\u0456\u0445 \u0434\u043d\u0456\u0432 \u0434\u043b\u044f \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f",
+ "wanted_max_items": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u043a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0435\u043b\u0435\u043c\u0435\u043d\u0442\u0456\u0432 \u0434\u043b\u044f \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/songpal/config_flow.py b/homeassistant/components/songpal/config_flow.py
index 9acbedd11c7806..22f9f0932ed445 100644
--- a/homeassistant/components/songpal/config_flow.py
+++ b/homeassistant/components/songpal/config_flow.py
@@ -1,6 +1,7 @@
"""Config flow to configure songpal component."""
+from __future__ import annotations
+
import logging
-from typing import Optional
from urllib.parse import urlparse
from songpal import Device, SongpalException
@@ -11,7 +12,7 @@
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.core import callback
-from .const import CONF_ENDPOINT, DOMAIN # pylint: disable=unused-import
+from .const import CONF_ENDPOINT, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -34,7 +35,7 @@ class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self):
"""Initialize the flow."""
- self.conf: Optional[SongpalConfig] = None
+ self.conf: SongpalConfig | None = None
async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
@@ -114,7 +115,6 @@ async def async_step_ssdp(self, discovery_info):
if "videoScreen" in service_types:
return self.async_abort(reason="not_songpal_device")
- # pylint: disable=no-member
self.context["title_placeholders"] = {
CONF_NAME: friendly_name,
CONF_HOST: parsed_url.hostname,
diff --git a/homeassistant/components/songpal/translations/hu.json b/homeassistant/components/songpal/translations/hu.json
index cd4c501ecf79f1..aa55862c0aad27 100644
--- a/homeassistant/components/songpal/translations/hu.json
+++ b/homeassistant/components/songpal/translations/hu.json
@@ -1,12 +1,17 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "not_songpal_device": "Nem Songpal eszk\u00f6z"
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
+ "flow_title": "Sony Songpal {name} ({host})",
"step": {
+ "init": {
+ "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?"
+ },
"user": {
"data": {
"endpoint": "V\u00e9gpont"
diff --git a/homeassistant/components/songpal/translations/id.json b/homeassistant/components/songpal/translations/id.json
new file mode 100644
index 00000000000000..2b8149661bc4a2
--- /dev/null
+++ b/homeassistant/components/songpal/translations/id.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "not_songpal_device": "Bukan perangkat Songpal"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "flow_title": "Sony Songpal {name} ({host})",
+ "step": {
+ "init": {
+ "description": "Ingin menyiapkan {name} ({host})?"
+ },
+ "user": {
+ "data": {
+ "endpoint": "Titik Akhir"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/songpal/translations/ko.json b/homeassistant/components/songpal/translations/ko.json
index abe7f9b384cf21..987f5fa76a3ebe 100644
--- a/homeassistant/components/songpal/translations/ko.json
+++ b/homeassistant/components/songpal/translations/ko.json
@@ -10,7 +10,7 @@
"flow_title": "Sony Songpal: {name} ({host})",
"step": {
"init": {
- "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ "description": "{name} ({host})\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
},
"user": {
"data": {
diff --git a/homeassistant/components/songpal/translations/tr.json b/homeassistant/components/songpal/translations/tr.json
new file mode 100644
index 00000000000000..ab90d4b1067d7c
--- /dev/null
+++ b/homeassistant/components/songpal/translations/tr.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "endpoint": "Biti\u015f noktas\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/songpal/translations/uk.json b/homeassistant/components/songpal/translations/uk.json
new file mode 100644
index 00000000000000..893077a826db7b
--- /dev/null
+++ b/homeassistant/components/songpal/translations/uk.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "not_songpal_device": "\u0426\u0435 \u043d\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Songpal."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "flow_title": "Sony Songpal {name} ({host})",
+ "step": {
+ "init": {
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} ({host})?"
+ },
+ "user": {
+ "data": {
+ "endpoint": "\u041a\u0456\u043d\u0446\u0435\u0432\u0430 \u0442\u043e\u0447\u043a\u0430"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py
index da397b3e5e71ef..63d5745da21a75 100644
--- a/homeassistant/components/sonos/const.py
+++ b/homeassistant/components/sonos/const.py
@@ -1,4 +1,20 @@
"""Const for Sonos."""
+from homeassistant.components.media_player.const import (
+ MEDIA_CLASS_ALBUM,
+ MEDIA_CLASS_ARTIST,
+ MEDIA_CLASS_COMPOSER,
+ MEDIA_CLASS_CONTRIBUTING_ARTIST,
+ MEDIA_CLASS_GENRE,
+ MEDIA_CLASS_PLAYLIST,
+ MEDIA_CLASS_TRACK,
+ MEDIA_TYPE_ALBUM,
+ MEDIA_TYPE_ARTIST,
+ MEDIA_TYPE_COMPOSER,
+ MEDIA_TYPE_CONTRIBUTING_ARTIST,
+ MEDIA_TYPE_GENRE,
+ MEDIA_TYPE_PLAYLIST,
+ MEDIA_TYPE_TRACK,
+)
DOMAIN = "sonos"
DATA_SONOS = "sonos_media_player"
@@ -10,3 +26,98 @@
SONOS_ALBUM_ARTIST = "album_artists"
SONOS_TRACKS = "tracks"
SONOS_COMPOSER = "composers"
+
+EXPANDABLE_MEDIA_TYPES = [
+ MEDIA_TYPE_ALBUM,
+ MEDIA_TYPE_ARTIST,
+ MEDIA_TYPE_COMPOSER,
+ MEDIA_TYPE_GENRE,
+ MEDIA_TYPE_PLAYLIST,
+ SONOS_ALBUM,
+ SONOS_ALBUM_ARTIST,
+ SONOS_ARTIST,
+ SONOS_GENRE,
+ SONOS_COMPOSER,
+ SONOS_PLAYLISTS,
+]
+
+SONOS_TO_MEDIA_CLASSES = {
+ SONOS_ALBUM: MEDIA_CLASS_ALBUM,
+ SONOS_ALBUM_ARTIST: MEDIA_CLASS_ARTIST,
+ SONOS_ARTIST: MEDIA_CLASS_CONTRIBUTING_ARTIST,
+ SONOS_COMPOSER: MEDIA_CLASS_COMPOSER,
+ SONOS_GENRE: MEDIA_CLASS_GENRE,
+ SONOS_PLAYLISTS: MEDIA_CLASS_PLAYLIST,
+ SONOS_TRACKS: MEDIA_CLASS_TRACK,
+ "object.container.album.musicAlbum": MEDIA_CLASS_ALBUM,
+ "object.container.genre.musicGenre": MEDIA_CLASS_PLAYLIST,
+ "object.container.person.composer": MEDIA_CLASS_PLAYLIST,
+ "object.container.person.musicArtist": MEDIA_CLASS_ARTIST,
+ "object.container.playlistContainer.sameArtist": MEDIA_CLASS_ARTIST,
+ "object.container.playlistContainer": MEDIA_CLASS_PLAYLIST,
+ "object.item.audioItem.musicTrack": MEDIA_CLASS_TRACK,
+}
+
+SONOS_TO_MEDIA_TYPES = {
+ SONOS_ALBUM: MEDIA_TYPE_ALBUM,
+ SONOS_ALBUM_ARTIST: MEDIA_TYPE_ARTIST,
+ SONOS_ARTIST: MEDIA_TYPE_CONTRIBUTING_ARTIST,
+ SONOS_COMPOSER: MEDIA_TYPE_COMPOSER,
+ SONOS_GENRE: MEDIA_TYPE_GENRE,
+ SONOS_PLAYLISTS: MEDIA_TYPE_PLAYLIST,
+ SONOS_TRACKS: MEDIA_TYPE_TRACK,
+ "object.container.album.musicAlbum": MEDIA_TYPE_ALBUM,
+ "object.container.genre.musicGenre": MEDIA_TYPE_PLAYLIST,
+ "object.container.person.composer": MEDIA_TYPE_PLAYLIST,
+ "object.container.person.musicArtist": MEDIA_TYPE_ARTIST,
+ "object.container.playlistContainer.sameArtist": MEDIA_TYPE_ARTIST,
+ "object.container.playlistContainer": MEDIA_TYPE_PLAYLIST,
+ "object.item.audioItem.musicTrack": MEDIA_TYPE_TRACK,
+}
+
+MEDIA_TYPES_TO_SONOS = {
+ MEDIA_TYPE_ALBUM: SONOS_ALBUM,
+ MEDIA_TYPE_ARTIST: SONOS_ALBUM_ARTIST,
+ MEDIA_TYPE_CONTRIBUTING_ARTIST: SONOS_ARTIST,
+ MEDIA_TYPE_COMPOSER: SONOS_COMPOSER,
+ MEDIA_TYPE_GENRE: SONOS_GENRE,
+ MEDIA_TYPE_PLAYLIST: SONOS_PLAYLISTS,
+ MEDIA_TYPE_TRACK: SONOS_TRACKS,
+}
+
+SONOS_TYPES_MAPPING = {
+ "A:ALBUM": SONOS_ALBUM,
+ "A:ALBUMARTIST": SONOS_ALBUM_ARTIST,
+ "A:ARTIST": SONOS_ARTIST,
+ "A:COMPOSER": SONOS_COMPOSER,
+ "A:GENRE": SONOS_GENRE,
+ "A:PLAYLISTS": SONOS_PLAYLISTS,
+ "A:TRACKS": SONOS_TRACKS,
+ "object.container.album.musicAlbum": SONOS_ALBUM,
+ "object.container.genre.musicGenre": SONOS_GENRE,
+ "object.container.person.composer": SONOS_COMPOSER,
+ "object.container.person.musicArtist": SONOS_ALBUM_ARTIST,
+ "object.container.playlistContainer.sameArtist": SONOS_ARTIST,
+ "object.container.playlistContainer": SONOS_PLAYLISTS,
+ "object.item.audioItem.musicTrack": SONOS_TRACKS,
+}
+
+LIBRARY_TITLES_MAPPING = {
+ "A:ALBUM": "Albums",
+ "A:ALBUMARTIST": "Artists",
+ "A:ARTIST": "Contributing Artists",
+ "A:COMPOSER": "Composers",
+ "A:GENRE": "Genres",
+ "A:PLAYLISTS": "Playlists",
+ "A:TRACKS": "Tracks",
+}
+
+PLAYABLE_MEDIA_TYPES = [
+ MEDIA_TYPE_ALBUM,
+ MEDIA_TYPE_ARTIST,
+ MEDIA_TYPE_COMPOSER,
+ MEDIA_TYPE_CONTRIBUTING_ARTIST,
+ MEDIA_TYPE_GENRE,
+ MEDIA_TYPE_PLAYLIST,
+ MEDIA_TYPE_TRACK,
+]
diff --git a/homeassistant/components/sonos/exception.py b/homeassistant/components/sonos/exception.py
new file mode 100644
index 00000000000000..3d5a1230bcbad3
--- /dev/null
+++ b/homeassistant/components/sonos/exception.py
@@ -0,0 +1,6 @@
+"""Sonos specific exceptions."""
+from homeassistant.components.media_player.errors import BrowseError
+
+
+class UnknownMediaType(BrowseError):
+ """Unknown media type."""
diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json
index 1852f9c3849234..f66e25e3d27184 100644
--- a/homeassistant/components/sonos/manifest.json
+++ b/homeassistant/components/sonos/manifest.json
@@ -3,7 +3,7 @@
"name": "Sonos",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sonos",
- "requirements": ["pysonos==0.0.37"],
+ "requirements": ["pysonos==0.0.42"],
"after_dependencies": ["plex"],
"ssdp": [
{
diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py
new file mode 100644
index 00000000000000..179fc62e0cc195
--- /dev/null
+++ b/homeassistant/components/sonos/media_browser.py
@@ -0,0 +1,215 @@
+"""Support for media browsing."""
+from contextlib import suppress
+import logging
+import urllib.parse
+
+from homeassistant.components.media_player import BrowseMedia
+from homeassistant.components.media_player.const import (
+ MEDIA_CLASS_DIRECTORY,
+ MEDIA_TYPE_ALBUM,
+)
+from homeassistant.components.media_player.errors import BrowseError
+
+from .const import (
+ EXPANDABLE_MEDIA_TYPES,
+ LIBRARY_TITLES_MAPPING,
+ MEDIA_TYPES_TO_SONOS,
+ PLAYABLE_MEDIA_TYPES,
+ SONOS_ALBUM,
+ SONOS_ALBUM_ARTIST,
+ SONOS_GENRE,
+ SONOS_TO_MEDIA_CLASSES,
+ SONOS_TO_MEDIA_TYPES,
+ SONOS_TRACKS,
+ SONOS_TYPES_MAPPING,
+)
+from .exception import UnknownMediaType
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def build_item_response(media_library, payload, get_thumbnail_url=None):
+ """Create response payload for the provided media query."""
+ if payload["search_type"] == MEDIA_TYPE_ALBUM and payload["idstring"].startswith(
+ ("A:GENRE", "A:COMPOSER")
+ ):
+ payload["idstring"] = "A:ALBUMARTIST/" + "/".join(
+ payload["idstring"].split("/")[2:]
+ )
+
+ media = media_library.browse_by_idstring(
+ MEDIA_TYPES_TO_SONOS[payload["search_type"]],
+ payload["idstring"],
+ full_album_art_uri=True,
+ max_items=0,
+ )
+
+ if media is None:
+ return
+
+ thumbnail = None
+ title = None
+
+ # Fetch album info for titles and thumbnails
+ # Can't be extracted from track info
+ if (
+ payload["search_type"] == MEDIA_TYPE_ALBUM
+ and media[0].item_class == "object.item.audioItem.musicTrack"
+ ):
+ item = get_media(media_library, payload["idstring"], SONOS_ALBUM_ARTIST)
+ title = getattr(item, "title", None)
+ thumbnail = get_thumbnail_url(SONOS_ALBUM_ARTIST, payload["idstring"])
+
+ if not title:
+ try:
+ title = urllib.parse.unquote(payload["idstring"].split("/")[1])
+ except IndexError:
+ title = LIBRARY_TITLES_MAPPING[payload["idstring"]]
+
+ try:
+ media_class = SONOS_TO_MEDIA_CLASSES[
+ MEDIA_TYPES_TO_SONOS[payload["search_type"]]
+ ]
+ except KeyError:
+ _LOGGER.debug("Unknown media type received %s", payload["search_type"])
+ return None
+
+ children = []
+ for item in media:
+ with suppress(UnknownMediaType):
+ children.append(item_payload(item, get_thumbnail_url))
+
+ return BrowseMedia(
+ title=title,
+ thumbnail=thumbnail,
+ media_class=media_class,
+ media_content_id=payload["idstring"],
+ media_content_type=payload["search_type"],
+ children=children,
+ can_play=can_play(payload["search_type"]),
+ can_expand=can_expand(payload["search_type"]),
+ )
+
+
+def item_payload(item, get_thumbnail_url=None):
+ """
+ Create response payload for a single media item.
+
+ Used by async_browse_media.
+ """
+ media_type = get_media_type(item)
+ try:
+ media_class = SONOS_TO_MEDIA_CLASSES[media_type]
+ except KeyError as err:
+ _LOGGER.debug("Unknown media type received %s", media_type)
+ raise UnknownMediaType from err
+
+ content_id = get_content_id(item)
+ thumbnail = None
+ if getattr(item, "album_art_uri", None):
+ thumbnail = get_thumbnail_url(media_class, content_id)
+
+ return BrowseMedia(
+ title=item.title,
+ thumbnail=thumbnail,
+ media_class=media_class,
+ media_content_id=content_id,
+ media_content_type=SONOS_TO_MEDIA_TYPES[media_type],
+ can_play=can_play(item.item_class),
+ can_expand=can_expand(item),
+ )
+
+
+def library_payload(media_library, get_thumbnail_url=None):
+ """
+ Create response payload to describe contents of a specific library.
+
+ Used by async_browse_media.
+ """
+ if not media_library.browse_by_idstring(
+ "tracks",
+ "",
+ max_items=1,
+ ):
+ raise BrowseError("Local library not found")
+
+ children = []
+ for item in media_library.browse():
+ with suppress(UnknownMediaType):
+ children.append(item_payload(item, get_thumbnail_url))
+
+ return BrowseMedia(
+ title="Music Library",
+ media_class=MEDIA_CLASS_DIRECTORY,
+ media_content_id="library",
+ media_content_type="library",
+ can_play=False,
+ can_expand=True,
+ children=children,
+ )
+
+
+def get_media_type(item):
+ """Extract media type of item."""
+ if item.item_class == "object.item.audioItem.musicTrack":
+ return SONOS_TRACKS
+
+ if (
+ item.item_class == "object.container.album.musicAlbum"
+ and SONOS_TYPES_MAPPING.get(item.item_id.split("/")[0])
+ in [
+ SONOS_ALBUM_ARTIST,
+ SONOS_GENRE,
+ ]
+ ):
+ return SONOS_TYPES_MAPPING[item.item_class]
+
+ return SONOS_TYPES_MAPPING.get(item.item_id.split("/")[0], item.item_class)
+
+
+def can_play(item):
+ """
+ Test if playable.
+
+ Used by async_browse_media.
+ """
+ return SONOS_TO_MEDIA_TYPES.get(item) in PLAYABLE_MEDIA_TYPES
+
+
+def can_expand(item):
+ """
+ Test if expandable.
+
+ Used by async_browse_media.
+ """
+ if isinstance(item, str):
+ return SONOS_TYPES_MAPPING.get(item) in EXPANDABLE_MEDIA_TYPES
+
+ if SONOS_TO_MEDIA_TYPES.get(item.item_class) in EXPANDABLE_MEDIA_TYPES:
+ return True
+
+ return SONOS_TYPES_MAPPING.get(item.item_id) in EXPANDABLE_MEDIA_TYPES
+
+
+def get_content_id(item):
+ """Extract content id or uri."""
+ if item.item_class == "object.item.audioItem.musicTrack":
+ return item.get_uri()
+ return item.item_id
+
+
+def get_media(media_library, item_id, search_type):
+ """Fetch media/album."""
+ search_type = MEDIA_TYPES_TO_SONOS.get(search_type, search_type)
+
+ if not item_id.startswith("A:ALBUM") and search_type == SONOS_ALBUM:
+ item_id = "A:ALBUMARTIST/" + "/".join(item_id.split("/")[2:])
+
+ for item in media_library.browse_by_idstring(
+ search_type,
+ "/".join(item_id.split("/")[:-1]),
+ full_album_art_uri=True,
+ max_items=0,
+ ):
+ if item.item_id == item_id:
+ return item
diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py
index 9d89bdf68f84d0..3ee458ec9db42b 100644
--- a/homeassistant/components/sonos/media_player.py
+++ b/homeassistant/components/sonos/media_player.py
@@ -1,42 +1,38 @@
"""Support to interface with Sonos players."""
+from __future__ import annotations
+
import asyncio
+from contextlib import suppress
import datetime
import functools as ft
import logging
import socket
+from typing import Any, Callable, Coroutine
import urllib.parse
import async_timeout
import pysonos
-from pysonos import alarms
+from pysonos import alarms, events_asyncio
from pysonos.core import (
+ MUSIC_SRC_LINE_IN,
+ MUSIC_SRC_RADIO,
+ MUSIC_SRC_TV,
PLAY_MODE_BY_MEANING,
PLAY_MODES,
- PLAYING_LINE_IN,
- PLAYING_RADIO,
- PLAYING_TV,
+ SoCo,
)
+from pysonos.data_structures import DidlFavorite
+from pysonos.events_base import Event, SubscriptionBase
from pysonos.exceptions import SoCoException, SoCoUPnPException
import pysonos.music_library
import pysonos.snapshot
import voluptuous as vol
-from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity
+from homeassistant.components.media_player import MediaPlayerEntity
from homeassistant.components.media_player.const import (
ATTR_MEDIA_ENQUEUE,
- MEDIA_CLASS_ALBUM,
- MEDIA_CLASS_ARTIST,
- MEDIA_CLASS_COMPOSER,
- MEDIA_CLASS_CONTRIBUTING_ARTIST,
- MEDIA_CLASS_DIRECTORY,
- MEDIA_CLASS_GENRE,
- MEDIA_CLASS_PLAYLIST,
- MEDIA_CLASS_TRACK,
MEDIA_TYPE_ALBUM,
MEDIA_TYPE_ARTIST,
- MEDIA_TYPE_COMPOSER,
- MEDIA_TYPE_CONTRIBUTING_ARTIST,
- MEDIA_TYPE_GENRE,
MEDIA_TYPE_MUSIC,
MEDIA_TYPE_PLAYLIST,
MEDIA_TYPE_TRACK,
@@ -61,35 +57,35 @@
from homeassistant.components.media_player.errors import BrowseError
from homeassistant.components.plex.const import PLEX_URI_SCHEME
from homeassistant.components.plex.services import play_on_sonos
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_TIME,
+ CONF_HOSTS,
EVENT_HOMEASSISTANT_STOP,
STATE_IDLE,
STATE_PAUSED,
STATE_PLAYING,
)
-from homeassistant.core import ServiceCall, callback
+from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv, entity_platform, service
import homeassistant.helpers.device_registry as dr
+from homeassistant.helpers.network import is_internal_request
from homeassistant.util.dt import utcnow
-from . import CONF_ADVERTISE_ADDR, CONF_HOSTS, CONF_INTERFACE_ADDR
+from . import CONF_ADVERTISE_ADDR, CONF_INTERFACE_ADDR
from .const import (
DATA_SONOS,
DOMAIN as SONOS_DOMAIN,
- SONOS_ALBUM,
- SONOS_ALBUM_ARTIST,
- SONOS_ARTIST,
- SONOS_COMPOSER,
- SONOS_GENRE,
- SONOS_PLAYLISTS,
- SONOS_TRACKS,
+ MEDIA_TYPES_TO_SONOS,
+ PLAYABLE_MEDIA_TYPES,
)
+from .media_browser import build_item_response, get_media, library_payload
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = 10
DISCOVERY_INTERVAL = 60
+SEEN_EXPIRE_TIME = 3.5 * DISCOVERY_INTERVAL
SUPPORT_SONOS = (
SUPPORT_BROWSE_MEDIA
@@ -111,101 +107,6 @@
SOURCE_LINEIN = "Line-in"
SOURCE_TV = "TV"
-EXPANDABLE_MEDIA_TYPES = [
- MEDIA_TYPE_ALBUM,
- MEDIA_TYPE_ARTIST,
- MEDIA_TYPE_COMPOSER,
- MEDIA_TYPE_GENRE,
- MEDIA_TYPE_PLAYLIST,
- SONOS_ALBUM,
- SONOS_ALBUM_ARTIST,
- SONOS_ARTIST,
- SONOS_GENRE,
- SONOS_COMPOSER,
- SONOS_PLAYLISTS,
-]
-
-SONOS_TO_MEDIA_CLASSES = {
- SONOS_ALBUM: MEDIA_CLASS_ALBUM,
- SONOS_ALBUM_ARTIST: MEDIA_CLASS_ARTIST,
- SONOS_ARTIST: MEDIA_CLASS_CONTRIBUTING_ARTIST,
- SONOS_COMPOSER: MEDIA_CLASS_COMPOSER,
- SONOS_GENRE: MEDIA_CLASS_GENRE,
- SONOS_PLAYLISTS: MEDIA_CLASS_PLAYLIST,
- SONOS_TRACKS: MEDIA_CLASS_TRACK,
- "object.container.album.musicAlbum": MEDIA_CLASS_ALBUM,
- "object.container.genre.musicGenre": MEDIA_CLASS_PLAYLIST,
- "object.container.person.composer": MEDIA_CLASS_PLAYLIST,
- "object.container.person.musicArtist": MEDIA_CLASS_ARTIST,
- "object.container.playlistContainer.sameArtist": MEDIA_CLASS_ARTIST,
- "object.container.playlistContainer": MEDIA_CLASS_PLAYLIST,
- "object.item.audioItem.musicTrack": MEDIA_CLASS_TRACK,
-}
-
-SONOS_TO_MEDIA_TYPES = {
- SONOS_ALBUM: MEDIA_TYPE_ALBUM,
- SONOS_ALBUM_ARTIST: MEDIA_TYPE_ARTIST,
- SONOS_ARTIST: MEDIA_TYPE_CONTRIBUTING_ARTIST,
- SONOS_COMPOSER: MEDIA_TYPE_COMPOSER,
- SONOS_GENRE: MEDIA_TYPE_GENRE,
- SONOS_PLAYLISTS: MEDIA_TYPE_PLAYLIST,
- SONOS_TRACKS: MEDIA_TYPE_TRACK,
- "object.container.album.musicAlbum": MEDIA_TYPE_ALBUM,
- "object.container.genre.musicGenre": MEDIA_TYPE_PLAYLIST,
- "object.container.person.composer": MEDIA_TYPE_PLAYLIST,
- "object.container.person.musicArtist": MEDIA_TYPE_ARTIST,
- "object.container.playlistContainer.sameArtist": MEDIA_TYPE_ARTIST,
- "object.container.playlistContainer": MEDIA_TYPE_PLAYLIST,
- "object.item.audioItem.musicTrack": MEDIA_TYPE_TRACK,
-}
-
-MEDIA_TYPES_TO_SONOS = {
- MEDIA_TYPE_ALBUM: SONOS_ALBUM,
- MEDIA_TYPE_ARTIST: SONOS_ALBUM_ARTIST,
- MEDIA_TYPE_CONTRIBUTING_ARTIST: SONOS_ARTIST,
- MEDIA_TYPE_COMPOSER: SONOS_COMPOSER,
- MEDIA_TYPE_GENRE: SONOS_GENRE,
- MEDIA_TYPE_PLAYLIST: SONOS_PLAYLISTS,
- MEDIA_TYPE_TRACK: SONOS_TRACKS,
-}
-
-SONOS_TYPES_MAPPING = {
- "A:ALBUM": SONOS_ALBUM,
- "A:ALBUMARTIST": SONOS_ALBUM_ARTIST,
- "A:ARTIST": SONOS_ARTIST,
- "A:COMPOSER": SONOS_COMPOSER,
- "A:GENRE": SONOS_GENRE,
- "A:PLAYLISTS": SONOS_PLAYLISTS,
- "A:TRACKS": SONOS_TRACKS,
- "object.container.album.musicAlbum": SONOS_ALBUM,
- "object.container.genre.musicGenre": SONOS_GENRE,
- "object.container.person.composer": SONOS_COMPOSER,
- "object.container.person.musicArtist": SONOS_ALBUM_ARTIST,
- "object.container.playlistContainer.sameArtist": SONOS_ARTIST,
- "object.container.playlistContainer": SONOS_PLAYLISTS,
- "object.item.audioItem.musicTrack": SONOS_TRACKS,
-}
-
-LIBRARY_TITLES_MAPPING = {
- "A:ALBUM": "Albums",
- "A:ALBUMARTIST": "Artists",
- "A:ARTIST": "Contributing Artists",
- "A:COMPOSER": "Composers",
- "A:GENRE": "Genres",
- "A:PLAYLISTS": "Playlists",
- "A:TRACKS": "Tracks",
-}
-
-PLAYABLE_MEDIA_TYPES = [
- MEDIA_TYPE_ALBUM,
- MEDIA_TYPE_ARTIST,
- MEDIA_TYPE_COMPOSER,
- MEDIA_TYPE_CONTRIBUTING_ARTIST,
- MEDIA_TYPE_GENRE,
- MEDIA_TYPE_PLAYLIST,
- MEDIA_TYPE_TRACK,
-]
-
REPEAT_TO_SONOS = {
REPEAT_MODE_OFF: False,
REPEAT_MODE_ALL: True,
@@ -244,42 +145,34 @@
UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None}
-class UnknownMediaType(BrowseError):
- """Unknown media type."""
-
-
class SonosData:
"""Storage class for platform global data."""
- def __init__(self):
+ def __init__(self) -> None:
"""Initialize the data."""
- self.entities = []
- self.discovered = []
+ self.entities: list[SonosEntity] = []
+ self.discovered: list[str] = []
self.topology_condition = asyncio.Condition()
self.discovery_thread = None
self.hosts_heartbeat = None
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the Sonos platform. Obsolete."""
- _LOGGER.error(
- "Loading Sonos by media_player platform configuration is no longer supported"
- )
-
-
-async def async_setup_entry(hass, config_entry, async_add_entities):
+async def async_setup_entry(
+ hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable
+) -> None:
"""Set up Sonos from a config entry."""
if DATA_SONOS not in hass.data:
hass.data[DATA_SONOS] = SonosData()
config = hass.data[SONOS_DOMAIN].get("media_player", {})
_LOGGER.debug("Reached async_setup_entry, config=%s", config)
+ pysonos.config.EVENTS_MODULE = events_asyncio
advertise_addr = config.get(CONF_ADVERTISE_ADDR)
if advertise_addr:
pysonos.config.EVENT_ADVERTISE_IP = advertise_addr
- def _stop_discovery(event):
+ def _stop_discovery(event: Event) -> None:
data = hass.data[DATA_SONOS]
if data.discovery_thread:
data.discovery_thread.stop()
@@ -288,11 +181,11 @@ def _stop_discovery(event):
data.hosts_heartbeat()
data.hosts_heartbeat = None
- def _discovery(now=None):
+ def _discovery(now: datetime.datetime | None = None) -> None:
"""Discover players from network or configuration."""
hosts = config.get(CONF_HOSTS)
- def _discovered_player(soco):
+ def _discovered_player(soco: SoCo) -> None:
"""Handle a (re)discovered player."""
try:
_LOGGER.debug("Reached _discovered_player, soco=%s", soco)
@@ -305,7 +198,7 @@ def _discovered_player(soco):
entity = _get_entity_from_soco_uid(hass, soco.uid)
if entity and (entity.soco == soco or not entity.available):
_LOGGER.debug("Seen %s", entity)
- hass.add_job(entity.async_seen(soco))
+ hass.add_job(entity.async_seen(soco)) # type: ignore
except SoCoException as ex:
_LOGGER.debug("SoCoException, ex=%s", ex)
@@ -336,6 +229,7 @@ def _discovered_player(soco):
interval=DISCOVERY_INTERVAL,
interface_addr=config.get(CONF_INTERFACE_ADDR),
)
+ hass.data[DATA_SONOS].discovery_thread.name = "Sonos-Discovery"
_LOGGER.debug("Adding discovery job")
hass.async_add_executor_job(_discovery)
@@ -344,31 +238,35 @@ def _discovered_player(soco):
platform = entity_platform.current_platform.get()
@service.verify_domain_control(hass, SONOS_DOMAIN)
- async def async_service_handle(service_call: ServiceCall):
+ async def async_service_handle(service_call: ServiceCall) -> None:
"""Handle dispatched services."""
+ assert platform is not None
entities = await platform.async_extract_from_service(service_call)
if not entities:
return
+ for entity in entities:
+ assert isinstance(entity, SonosEntity)
+
if service_call.service == SERVICE_JOIN:
master = platform.entities.get(service_call.data[ATTR_MASTER])
if master:
- await SonosEntity.join_multi(hass, master, entities)
+ await SonosEntity.join_multi(hass, master, entities) # type: ignore[arg-type]
else:
_LOGGER.error(
"Invalid master specified for join service: %s",
service_call.data[ATTR_MASTER],
)
elif service_call.service == SERVICE_UNJOIN:
- await SonosEntity.unjoin_multi(hass, entities)
+ await SonosEntity.unjoin_multi(hass, entities) # type: ignore[arg-type]
elif service_call.service == SERVICE_SNAPSHOT:
await SonosEntity.snapshot_multi(
- hass, entities, service_call.data[ATTR_WITH_GROUP]
+ hass, entities, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type]
)
elif service_call.service == SERVICE_RESTORE:
await SonosEntity.restore_multi(
- hass, entities, service_call.data[ATTR_WITH_GROUP]
+ hass, entities, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type]
)
hass.services.async_register(
@@ -397,7 +295,7 @@ async def async_service_handle(service_call: ServiceCall):
SONOS_DOMAIN, SERVICE_RESTORE, async_service_handle, join_unjoin_schema
)
- platform.async_register_entity_service(
+ platform.async_register_entity_service( # type: ignore
SERVICE_SET_TIMER,
{
vol.Required(ATTR_SLEEP_TIME): vol.All(
@@ -407,9 +305,9 @@ async def async_service_handle(service_call: ServiceCall):
"set_sleep_timer",
)
- platform.async_register_entity_service(SERVICE_CLEAR_TIMER, {}, "clear_sleep_timer")
+ platform.async_register_entity_service(SERVICE_CLEAR_TIMER, {}, "clear_sleep_timer") # type: ignore
- platform.async_register_entity_service(
+ platform.async_register_entity_service( # type: ignore
SERVICE_UPDATE_ALARM,
{
vol.Required(ATTR_ALARM_ID): cv.positive_int,
@@ -421,7 +319,7 @@ async def async_service_handle(service_call: ServiceCall):
"set_alarm",
)
- platform.async_register_entity_service(
+ platform.async_register_entity_service( # type: ignore
SERVICE_SET_OPTION,
{
vol.Optional(ATTR_NIGHT_SOUND): cv.boolean,
@@ -431,50 +329,36 @@ async def async_service_handle(service_call: ServiceCall):
"set_option",
)
- platform.async_register_entity_service(
+ platform.async_register_entity_service( # type: ignore
SERVICE_PLAY_QUEUE,
{vol.Optional(ATTR_QUEUE_POSITION): cv.positive_int},
"play_queue",
)
- platform.async_register_entity_service(
+ platform.async_register_entity_service( # type: ignore
SERVICE_REMOVE_FROM_QUEUE,
{vol.Optional(ATTR_QUEUE_POSITION): cv.positive_int},
"remove_from_queue",
)
-class _ProcessSonosEventQueue:
- """Queue like object for dispatching sonos events."""
-
- def __init__(self, handler):
- """Initialize Sonos event queue."""
- self._handler = handler
-
- def put(self, item, block=True, timeout=None):
- """Process event."""
- try:
- self._handler(item)
- except SoCoException as ex:
- _LOGGER.warning("Error calling %s: %s", self._handler, ex)
-
-
-def _get_entity_from_soco_uid(hass, uid):
+def _get_entity_from_soco_uid(hass: HomeAssistant, uid: str) -> SonosEntity | None:
"""Return SonosEntity from SoCo uid."""
- for entity in hass.data[DATA_SONOS].entities:
+ entities: list[SonosEntity] = hass.data[DATA_SONOS].entities
+ for entity in entities:
if uid == entity.unique_id:
return entity
return None
-def soco_error(errorcodes=None):
+def soco_error(errorcodes: list[str] | None = None) -> Callable:
"""Filter out specified UPnP errors from logs and avoid exceptions."""
- def decorator(funct):
+ def decorator(funct: Callable) -> Callable:
"""Decorate functions."""
@ft.wraps(funct)
- def wrapper(*args, **kwargs):
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
"""Wrap for all soco UPnP exception."""
try:
return funct(*args, **kwargs)
@@ -489,11 +373,11 @@ def wrapper(*args, **kwargs):
return decorator
-def soco_coordinator(funct):
+def soco_coordinator(funct: Callable) -> Callable:
"""Call function on coordinator."""
@ft.wraps(funct)
- def wrapper(entity, *args, **kwargs):
+ def wrapper(entity: SonosEntity, *args: Any, **kwargs: Any) -> Any:
"""Wrap for call to coordinator."""
if entity.is_coordinator:
return funct(entity, *args, **kwargs)
@@ -502,85 +386,82 @@ def wrapper(entity, *args, **kwargs):
return wrapper
-def _timespan_secs(timespan):
+def _timespan_secs(timespan: str | None) -> None | float:
"""Parse a time-span into number of seconds."""
if timespan in UNAVAILABLE_VALUES:
return None
+ assert timespan is not None
return sum(60 ** x[0] * int(x[1]) for x in enumerate(reversed(timespan.split(":"))))
class SonosEntity(MediaPlayerEntity):
"""Representation of a Sonos entity."""
- def __init__(self, player):
+ def __init__(self, player: SoCo) -> None:
"""Initialize the Sonos entity."""
- self._subscriptions = []
- self._poll_timer = None
- self._seen_timer = None
+ self._subscriptions: list[SubscriptionBase] = []
+ self._poll_timer: Callable | None = None
+ self._seen_timer: Callable | None = None
self._volume_increment = 2
- self._unique_id = player.uid
- self._player = player
- self._player_volume = None
- self._player_muted = None
- self._play_mode = None
- self._coordinator = None
- self._sonos_group = [self]
- self._status = None
- self._uri = None
+ self._unique_id: str = player.uid
+ self._player: SoCo = player
+ self._player_volume: int | None = None
+ self._player_muted: bool | None = None
+ self._play_mode: str | None = None
+ self._coordinator: SonosEntity | None = None
+ self._sonos_group: list[SonosEntity] = [self]
+ self._status: str | None = None
+ self._uri: str | None = None
self._media_library = pysonos.music_library.MusicLibrary(self.soco)
- self._media_duration = None
- self._media_position = None
- self._media_position_updated_at = None
- self._media_image_url = None
- self._media_channel = None
- self._media_artist = None
- self._media_album_name = None
- self._media_title = None
- self._queue_position = None
- self._night_sound = None
- self._speech_enhance = None
- self._source_name = None
- self._favorites = []
- self._soco_snapshot = None
- self._snapshot_group = None
+ self._media_duration: float | None = None
+ self._media_position: float | None = None
+ self._media_position_updated_at: datetime.datetime | None = None
+ self._media_image_url: str | None = None
+ self._media_channel: str | None = None
+ self._media_artist: str | None = None
+ self._media_album_name: str | None = None
+ self._media_title: str | None = None
+ self._queue_position: int | None = None
+ self._night_sound: bool | None = None
+ self._speech_enhance: bool | None = None
+ self._source_name: str | None = None
+ self._favorites: list[DidlFavorite] = []
+ self._soco_snapshot: pysonos.snapshot.Snapshot | None = None
+ self._snapshot_group: list[SonosEntity] | None = None
# Set these early since device_info() needs them
- speaker_info = self.soco.get_speaker_info(True)
- self._name = speaker_info["zone_name"]
- self._model = speaker_info["model_name"]
- self._sw_version = speaker_info["software_version"]
- self._mac_address = speaker_info["mac_address"]
+ speaker_info: dict = self.soco.get_speaker_info(True)
+ self._name: str = speaker_info["zone_name"]
+ self._model: str = speaker_info["model_name"]
+ self._sw_version: str = speaker_info["software_version"]
+ self._mac_address: str = speaker_info["mac_address"]
- async def async_added_to_hass(self):
+ async def async_added_to_hass(self) -> None:
"""Subscribe sonos events."""
await self.async_seen(self.soco)
self.hass.data[DATA_SONOS].entities.append(self)
- def _rebuild_groups():
- """Build the current group topology."""
- for entity in self.hass.data[DATA_SONOS].entities:
- entity.update_groups()
-
- self.hass.async_add_executor_job(_rebuild_groups)
+ for entity in self.hass.data[DATA_SONOS].entities:
+ await entity.create_update_groups_coro()
@property
- def unique_id(self):
+ def unique_id(self) -> str:
"""Return a unique ID."""
return self._unique_id
- def __hash__(self):
+ def __hash__(self) -> int:
"""Return a hash of self."""
return hash(self.unique_id)
@property
- def name(self):
+ def name(self) -> str:
"""Return the name of the entity."""
return self._name
@property
- def device_info(self):
+ def device_info(self) -> dict:
"""Return information about the device."""
return {
"identifiers": {(SONOS_DOMAIN, self._unique_id)},
@@ -589,11 +470,12 @@ def device_info(self):
"sw_version": self._sw_version,
"connections": {(dr.CONNECTION_NETWORK_MAC, self._mac_address)},
"manufacturer": "Sonos",
+ "suggested_area": self._name,
}
- @property
+ @property # type: ignore[misc]
@soco_coordinator
- def state(self):
+ def state(self) -> str:
"""Return the state of the entity."""
if self._status in (
"PAUSED_PLAYBACK",
@@ -609,23 +491,24 @@ def state(self):
return STATE_IDLE
@property
- def is_coordinator(self):
+ def is_coordinator(self) -> bool:
"""Return true if player is a coordinator."""
return self._coordinator is None
@property
- def soco(self):
+ def soco(self) -> SoCo:
"""Return soco object."""
return self._player
@property
- def coordinator(self):
+ def coordinator(self) -> SoCo:
"""Return coordinator of this player."""
return self._coordinator
- async def async_seen(self, player):
+ async def async_seen(self, player: SoCo) -> None:
"""Record that this player was seen right now."""
was_available = self.available
+ _LOGGER.debug("Async seen: %s, was_available: %s", player, was_available)
self._player = player
@@ -633,7 +516,7 @@ async def async_seen(self, player):
self._seen_timer()
self._seen_timer = self.hass.helpers.event.async_call_later(
- 2.5 * DISCOVERY_INTERVAL, self.async_unseen
+ SEEN_EXPIRE_TIME, self.async_unseen
)
if was_available:
@@ -643,15 +526,15 @@ async def async_seen(self, player):
self.update, datetime.timedelta(seconds=SCAN_INTERVAL)
)
- done = await self.hass.async_add_executor_job(self._attach_player)
+ done = await self._async_attach_player()
if not done:
+ assert self._seen_timer is not None
self._seen_timer()
- self.async_unseen()
+ await self.async_unseen()
self.async_write_ha_state()
- @callback
- def async_unseen(self, now=None):
+ async def async_unseen(self, now: datetime.datetime | None = None) -> None:
"""Make this player unavailable when it was not seen recently."""
self._seen_timer = None
@@ -659,11 +542,8 @@ def async_unseen(self, now=None):
self._poll_timer()
self._poll_timer = None
- def _unsub(subscriptions):
- for subscription in subscriptions:
- subscription.unsubscribe()
-
- self.hass.async_add_executor_job(_unsub, self._subscriptions)
+ for subscription in self._subscriptions:
+ await subscription.unsubscribe()
self._subscriptions = []
@@ -674,12 +554,12 @@ def available(self) -> bool:
"""Return True if entity is available."""
return self._seen_timer is not None
- def _clear_media_position(self):
+ def _clear_media_position(self) -> None:
"""Clear the media_position."""
self._media_position = None
self._media_position_updated_at = None
- def _set_favorites(self):
+ def _set_favorites(self) -> None:
"""Set available favorites."""
self._favorites = []
for fav in self.soco.music_library.get_sonos_favorites():
@@ -691,36 +571,48 @@ def _set_favorites(self):
# Skip unknown types
_LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex)
- def _attach_player(self):
+ def _attach_player(self) -> None:
+ """Get basic information and add event subscriptions."""
+ self._play_mode = self.soco.play_mode
+ self.update_volume()
+ self._set_favorites()
+
+ async def _async_attach_player(self) -> bool:
"""Get basic information and add event subscriptions."""
try:
- self._play_mode = self.soco.play_mode
- self.update_volume()
- self._set_favorites()
+ await self.hass.async_add_executor_job(self._attach_player)
player = self.soco
- def subscribe(sonos_service, action):
- """Add a subscription to a pysonos service."""
- queue = _ProcessSonosEventQueue(action)
- sub = sonos_service.subscribe(auto_renew=True, event_queue=queue)
- self._subscriptions.append(sub)
+ if self._subscriptions:
+ raise RuntimeError(
+ f"Attempted to attach subscriptions to player: {player} "
+ f"when existing subscriptions exist: {self._subscriptions}"
+ )
- subscribe(player.avTransport, self.update_media)
- subscribe(player.renderingControl, self.update_volume)
- subscribe(player.zoneGroupTopology, self.update_groups)
- subscribe(player.contentDirectory, self.update_content)
+ await self._subscribe(player.avTransport, self.async_update_media)
+ await self._subscribe(player.renderingControl, self.async_update_volume)
+ await self._subscribe(player.zoneGroupTopology, self.async_update_groups)
+ await self._subscribe(player.contentDirectory, self.async_update_content)
return True
except SoCoException as ex:
_LOGGER.warning("Could not connect %s: %s", self.entity_id, ex)
return False
+ async def _subscribe(
+ self, target: SubscriptionBase, sub_callback: Callable
+ ) -> None:
+ """Create a sonos subscription."""
+ subscription = await target.subscribe(auto_renew=True)
+ subscription.callback = sub_callback
+ self._subscriptions.append(subscription)
+
@property
- def should_poll(self):
+ def should_poll(self) -> bool:
"""Return that we should not be polled (we handle that internally)."""
return False
- def update(self, now=None):
+ def update(self, now: datetime.datetime | None = None) -> None:
"""Retrieve latest state."""
try:
self.update_groups()
@@ -730,7 +622,12 @@ def update(self, now=None):
except SoCoException:
pass
- def update_media(self, event=None):
+ @callback
+ def async_update_media(self, event: Event | None = None) -> None:
+ """Update information about currently playing media."""
+ self.hass.async_add_executor_job(self.update_media, event)
+
+ def update_media(self, event: Event | None = None) -> None:
"""Update information about currently playing media."""
variables = event and event.variables
@@ -758,12 +655,16 @@ def update_media(self, event=None):
update_position = new_status != self._status
self._status = new_status
- track_uri = variables["current_track_uri"] if variables else None
- whats_playing = self.soco.whats_playing(track_uri)
+ if variables:
+ track_uri = variables["current_track_uri"]
+ music_source = self.soco.music_source_from_uri(track_uri)
+ else:
+ # This causes a network round-trip so we avoid it when possible
+ music_source = self.soco.music_source
- if whats_playing == PLAYING_TV:
+ if music_source == MUSIC_SRC_TV:
self.update_media_linein(SOURCE_TV)
- elif whats_playing == PLAYING_LINE_IN:
+ elif music_source == MUSIC_SRC_LINE_IN:
self.update_media_linein(SOURCE_LINEIN)
else:
track_info = self.soco.get_current_track_info()
@@ -775,8 +676,8 @@ def update_media(self, event=None):
self._media_album_name = track_info.get("album")
self._media_title = track_info.get("title")
- if whats_playing == PLAYING_RADIO:
- self.update_media_radio(variables, track_info)
+ if music_source == MUSIC_SRC_RADIO:
+ self.update_media_radio(variables)
else:
self.update_media_music(update_position, track_info)
@@ -788,14 +689,14 @@ def update_media(self, event=None):
if coordinator and coordinator.unique_id == self.unique_id:
entity.schedule_update_ha_state()
- def update_media_linein(self, source):
+ def update_media_linein(self, source: str) -> None:
"""Update state when playing from line-in/tv."""
self._clear_media_position()
self._media_title = source
self._source_name = source
- def update_media_radio(self, variables, track_info):
+ def update_media_radio(self, variables: dict) -> None:
"""Update state when streaming radio."""
self._clear_media_position()
@@ -816,8 +717,12 @@ def update_media_radio(self, variables, track_info):
uri_meta_data, pysonos.data_structures.DidlAudioBroadcast
) and (
self.state != STATE_PLAYING
- or self.soco.is_radio_uri(self._media_title)
- or self._media_title in self._uri
+ or self.soco.music_source_from_uri(self._media_title) == MUSIC_SRC_RADIO
+ or (
+ isinstance(self._media_title, str)
+ and isinstance(self._uri, str)
+ and self._media_title in self._uri
+ )
):
self._media_title = uri_meta_data.title
except (TypeError, KeyError, AttributeError):
@@ -832,7 +737,7 @@ def update_media_radio(self, variables, track_info):
if fav.reference.get_uri() == media_info["uri"]:
self._source_name = fav.title
- def update_media_music(self, update_media_position, track_info):
+ def update_media_music(self, update_media_position: bool, track_info: dict) -> None:
"""Update state when playing music tracks."""
self._media_duration = _timespan_secs(track_info.get("duration"))
current_position = _timespan_secs(track_info.get("position"))
@@ -844,8 +749,9 @@ def update_media_music(self, update_media_position, track_info):
# position jumped?
if current_position is not None and self._media_position is not None:
if self.state == STATE_PLAYING:
- time_diff = utcnow() - self._media_position_updated_at
- time_diff = time_diff.total_seconds()
+ assert self._media_position_updated_at is not None
+ time_delta = utcnow() - self._media_position_updated_at
+ time_diff = time_delta.total_seconds()
else:
time_diff = 0
@@ -862,43 +768,58 @@ def update_media_music(self, update_media_position, track_info):
self._media_image_url = track_info.get("album_art")
- playlist_position = int(track_info.get("playlist_position"))
+ playlist_position = int(track_info.get("playlist_position")) # type: ignore
if playlist_position > 0:
self._queue_position = playlist_position - 1
- def update_volume(self, event=None):
+ @callback
+ def async_update_volume(self, event: Event) -> None:
"""Update information about currently volume settings."""
- if event:
- variables = event.variables
+ variables = event.variables
- if "volume" in variables:
- self._player_volume = int(variables["volume"]["Master"])
+ if "volume" in variables:
+ self._player_volume = int(variables["volume"]["Master"])
- if "mute" in variables:
- self._player_muted = variables["mute"]["Master"] == "1"
+ if "mute" in variables:
+ self._player_muted = variables["mute"]["Master"] == "1"
- if "night_mode" in variables:
- self._night_sound = variables["night_mode"] == "1"
+ if "night_mode" in variables:
+ self._night_sound = variables["night_mode"] == "1"
- if "dialog_level" in variables:
- self._speech_enhance = variables["dialog_level"] == "1"
+ if "dialog_level" in variables:
+ self._speech_enhance = variables["dialog_level"] == "1"
- self.schedule_update_ha_state()
- else:
- self._player_volume = self.soco.volume
- self._player_muted = self.soco.mute
- self._night_sound = self.soco.night_mode
- self._speech_enhance = self.soco.dialog_mode
+ self.async_write_ha_state()
+
+ def update_volume(self) -> None:
+ """Update information about currently volume settings."""
+ self._player_volume = self.soco.volume
+ self._player_muted = self.soco.mute
+ self._night_sound = self.soco.night_mode
+ self._speech_enhance = self.soco.dialog_mode
- def update_groups(self, event=None):
+ def update_groups(self, event: Event | None = None) -> None:
"""Handle callback for topology change event."""
+ coro = self.create_update_groups_coro(event)
+ if coro:
+ self.hass.add_job(coro) # type: ignore
- def _get_soco_group():
+ @callback
+ def async_update_groups(self, event: Event | None = None) -> None:
+ """Handle callback for topology change event."""
+ coro = self.create_update_groups_coro(event)
+ if coro:
+ self.hass.async_add_job(coro) # type: ignore
+
+ def create_update_groups_coro(self, event: Event | None = None) -> Coroutine | None:
+ """Handle callback for topology change event."""
+
+ def _get_soco_group() -> list[str]:
"""Ask SoCo cache for existing topology."""
coordinator_uid = self.unique_id
slave_uids = []
- try:
+ with suppress(SoCoException):
if self.soco.group and self.soco.group.coordinator:
coordinator_uid = self.soco.group.coordinator.uid
slave_uids = [
@@ -906,21 +827,20 @@ def _get_soco_group():
for p in self.soco.group.members
if p.uid != coordinator_uid
]
- except SoCoException:
- pass
return [coordinator_uid] + slave_uids
- async def _async_extract_group(event):
+ async def _async_extract_group(event: Event) -> list[str]:
"""Extract group layout from a topology event."""
group = event and event.zone_player_uui_ds_in_group
if group:
+ assert isinstance(group, str)
return group.split(",")
return await self.hass.async_add_executor_job(_get_soco_group)
@callback
- def _async_regroup(group):
+ def _async_regroup(group: list[str]) -> None:
"""Rebuild internal group layout."""
sonos_group = []
for uid in group:
@@ -940,7 +860,7 @@ def _async_regroup(group):
slave._sonos_group = sonos_group
slave.async_schedule_update_ha_state()
- async def _async_handle_group_event(event):
+ async def _async_handle_group_event(event: Event) -> None:
"""Get async lock and handle event."""
if event and self._poll_timer:
# Cancel poll timer since we do receive events
@@ -956,136 +876,136 @@ async def _async_handle_group_event(event):
self.hass.data[DATA_SONOS].topology_condition.notify_all()
if event and not hasattr(event, "zone_player_uui_ds_in_group"):
- return
+ return None
- self.hass.add_job(_async_handle_group_event(event))
+ return _async_handle_group_event(event)
- def update_content(self, event=None):
+ @callback
+ def async_update_content(self, event: Event | None = None) -> None:
"""Update information about available content."""
if event and "favorites_update_id" in event.variables:
- self._set_favorites()
- self.schedule_update_ha_state()
+ self.hass.async_add_job(self._set_favorites)
+ self.async_write_ha_state()
@property
- def volume_level(self):
+ def volume_level(self) -> float | None:
"""Volume level of the media player (0..1)."""
- if self._player_volume is None:
- return None
- return self._player_volume / 100
+ return self._player_volume and self._player_volume / 100
@property
- def is_volume_muted(self):
+ def is_volume_muted(self) -> bool | None:
"""Return true if volume is muted."""
return self._player_muted
- @property
+ @property # type: ignore[misc]
@soco_coordinator
- def shuffle(self):
+ def shuffle(self) -> str | None:
"""Shuffling state."""
- return PLAY_MODES[self._play_mode][0]
+ shuffle: str = PLAY_MODES[self._play_mode][0]
+ return shuffle
- @property
+ @property # type: ignore[misc]
@soco_coordinator
- def repeat(self):
+ def repeat(self) -> str | None:
"""Return current repeat mode."""
sonos_repeat = PLAY_MODES[self._play_mode][1]
return SONOS_TO_REPEAT[sonos_repeat]
- @property
+ @property # type: ignore[misc]
@soco_coordinator
- def media_content_id(self):
+ def media_content_id(self) -> str | None:
"""Content id of current playing media."""
return self._uri
@property
- def media_content_type(self):
+ def media_content_type(self) -> str:
"""Content type of current playing media."""
return MEDIA_TYPE_MUSIC
- @property
+ @property # type: ignore[misc]
@soco_coordinator
- def media_duration(self):
+ def media_duration(self) -> float | None:
"""Duration of current playing media in seconds."""
return self._media_duration
- @property
+ @property # type: ignore[misc]
@soco_coordinator
- def media_position(self):
+ def media_position(self) -> float | None:
"""Position of current playing media in seconds."""
return self._media_position
- @property
+ @property # type: ignore[misc]
@soco_coordinator
- def media_position_updated_at(self):
+ def media_position_updated_at(self) -> datetime.datetime | None:
"""When was the position of the current playing media valid."""
return self._media_position_updated_at
- @property
+ @property # type: ignore[misc]
@soco_coordinator
- def media_image_url(self):
+ def media_image_url(self) -> str | None:
"""Image url of current playing media."""
return self._media_image_url or None
- @property
+ @property # type: ignore[misc]
@soco_coordinator
- def media_channel(self):
+ def media_channel(self) -> str | None:
"""Channel currently playing."""
return self._media_channel or None
- @property
+ @property # type: ignore[misc]
@soco_coordinator
- def media_artist(self):
+ def media_artist(self) -> str | None:
"""Artist of current playing media, music track only."""
return self._media_artist or None
- @property
+ @property # type: ignore[misc]
@soco_coordinator
- def media_album_name(self):
+ def media_album_name(self) -> str | None:
"""Album name of current playing media, music track only."""
return self._media_album_name or None
- @property
+ @property # type: ignore[misc]
@soco_coordinator
- def media_title(self):
+ def media_title(self) -> str | None:
"""Title of current playing media."""
return self._media_title or None
- @property
+ @property # type: ignore[misc]
@soco_coordinator
- def queue_position(self):
+ def queue_position(self) -> int | None:
"""If playing local queue return the position in the queue else None."""
return self._queue_position
- @property
+ @property # type: ignore[misc]
@soco_coordinator
- def source(self):
+ def source(self) -> str | None:
"""Name of the current input source."""
return self._source_name or None
- @property
+ @property # type: ignore[misc]
@soco_coordinator
- def supported_features(self):
+ def supported_features(self) -> int:
"""Flag media player features that are supported."""
return SUPPORT_SONOS
@soco_error()
- def volume_up(self):
+ def volume_up(self) -> None:
"""Volume up media player."""
self._player.volume += self._volume_increment
@soco_error()
- def volume_down(self):
+ def volume_down(self) -> None:
"""Volume down media player."""
self._player.volume -= self._volume_increment
@soco_error()
- def set_volume_level(self, volume):
+ def set_volume_level(self, volume: str) -> None:
"""Set volume level, range 0..1."""
self.soco.volume = str(int(volume * 100))
@soco_error(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
- def set_shuffle(self, shuffle):
+ def set_shuffle(self, shuffle: str) -> None:
"""Enable/Disable shuffle mode."""
sonos_shuffle = shuffle
sonos_repeat = PLAY_MODES[self._play_mode][1]
@@ -1093,20 +1013,20 @@ def set_shuffle(self, shuffle):
@soco_error(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
- def set_repeat(self, repeat):
+ def set_repeat(self, repeat: str) -> None:
"""Set repeat mode."""
sonos_shuffle = PLAY_MODES[self._play_mode][0]
sonos_repeat = REPEAT_TO_SONOS[repeat]
self.soco.play_mode = PLAY_MODE_BY_MEANING[(sonos_shuffle, sonos_repeat)]
@soco_error()
- def mute_volume(self, mute):
+ def mute_volume(self, mute: bool) -> None:
"""Mute (true) or unmute (false) media player."""
self.soco.mute = mute
@soco_error()
@soco_coordinator
- def select_source(self, source):
+ def select_source(self, source: str) -> None:
"""Select input source."""
if source == SOURCE_LINEIN:
self.soco.switch_to_line_in()
@@ -1117,16 +1037,19 @@ def select_source(self, source):
if len(fav) == 1:
src = fav.pop()
uri = src.reference.get_uri()
- if self.soco.is_radio_uri(uri):
+ if self.soco.music_source_from_uri(uri) in [
+ MUSIC_SRC_RADIO,
+ MUSIC_SRC_LINE_IN,
+ ]:
self.soco.play_uri(uri, title=source)
else:
self.soco.clear_queue()
self.soco.add_to_queue(src.reference)
self.soco.play_from_queue(0)
- @property
+ @property # type: ignore[misc]
@soco_coordinator
- def source_list(self):
+ def source_list(self) -> list[str]:
"""List of available input sources."""
sources = [fav.title for fav in self._favorites]
@@ -1142,49 +1065,49 @@ def source_list(self):
@soco_error(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
- def media_play(self):
+ def media_play(self) -> None:
"""Send play command."""
self.soco.play()
@soco_error(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
- def media_stop(self):
+ def media_stop(self) -> None:
"""Send stop command."""
self.soco.stop()
@soco_error(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
- def media_pause(self):
+ def media_pause(self) -> None:
"""Send pause command."""
self.soco.pause()
@soco_error(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
- def media_next_track(self):
+ def media_next_track(self) -> None:
"""Send next track command."""
self.soco.next()
@soco_error(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
- def media_previous_track(self):
+ def media_previous_track(self) -> None:
"""Send next track command."""
self.soco.previous()
@soco_error(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
- def media_seek(self, position):
+ def media_seek(self, position: str) -> None:
"""Send seek command."""
self.soco.seek(str(datetime.timedelta(seconds=int(position))))
@soco_error()
@soco_coordinator
- def clear_playlist(self):
+ def clear_playlist(self) -> None:
"""Clear players playlist."""
self.soco.clear_queue()
@soco_error()
@soco_coordinator
- 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.
@@ -1197,12 +1120,12 @@ def play_media(self, media_type, media_id, **kwargs):
"""
if media_id and media_id.startswith(PLEX_URI_SCHEME):
media_id = media_id[len(PLEX_URI_SCHEME) :]
- play_on_sonos(self.hass, media_type, media_id, self.name)
+ play_on_sonos(self.hass, media_type, media_id, self.name) # type: ignore[no-untyped-call]
elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK):
if kwargs.get(ATTR_MEDIA_ENQUEUE):
try:
- if self.soco.is_spotify_uri(media_id):
- self.soco.add_spotify_uri_to_queue(media_id)
+ if self.soco.is_service_uri(media_id):
+ self.soco.add_service_uri_to_queue(media_id)
else:
self.soco.add_uri_to_queue(media_id)
except SoCoUPnPException:
@@ -1213,15 +1136,15 @@ def play_media(self, media_type, media_id, **kwargs):
media_id,
)
else:
- if self.soco.is_spotify_uri(media_id):
+ if self.soco.is_service_uri(media_id):
self.soco.clear_queue()
- self.soco.add_spotify_uri_to_queue(media_id)
+ self.soco.add_service_uri_to_queue(media_id)
self.soco.play_from_queue(0)
else:
self.soco.play_uri(media_id)
elif media_type == MEDIA_TYPE_PLAYLIST:
if media_id.startswith("S:"):
- item = get_media(self._media_library, media_id, media_type)
+ item = get_media(self._media_library, media_id, media_type) # type: ignore[no-untyped-call]
self.soco.play_uri(item.get_uri())
return
try:
@@ -1233,7 +1156,7 @@ def play_media(self, media_type, media_id, **kwargs):
except StopIteration:
_LOGGER.error('Could not find a Sonos playlist named "%s"', media_id)
elif media_type in PLAYABLE_MEDIA_TYPES:
- item = get_media(self._media_library, media_id, media_type)
+ item = get_media(self._media_library, media_id, media_type) # type: ignore[no-untyped-call]
if not item:
_LOGGER.error('Could not find "%s" in the library', media_id)
@@ -1244,7 +1167,7 @@ def play_media(self, media_type, media_id, **kwargs):
_LOGGER.error('Sonos does not support a media type of "%s"', media_type)
@soco_error()
- def join(self, slaves):
+ def join(self, slaves: list[SonosEntity]) -> list[SonosEntity]:
"""Form a group with other players."""
if self._coordinator:
self.unjoin()
@@ -1263,23 +1186,27 @@ def join(self, slaves):
return group
@staticmethod
- async def join_multi(hass, master, entities):
+ async def join_multi(
+ hass: HomeAssistant, master: SonosEntity, entities: list[SonosEntity]
+ ) -> None:
"""Form a group with other players."""
async with hass.data[DATA_SONOS].topology_condition:
- group = await hass.async_add_executor_job(master.join, entities)
+ group: list[SonosEntity] = await hass.async_add_executor_job(
+ master.join, entities
+ )
await SonosEntity.wait_for_groups(hass, [group])
@soco_error()
- def unjoin(self):
+ def unjoin(self) -> None:
"""Unjoin the player from a group."""
self.soco.unjoin()
self._coordinator = None
@staticmethod
- async def unjoin_multi(hass, entities):
+ async def unjoin_multi(hass: HomeAssistant, entities: list[SonosEntity]) -> None:
"""Unjoin several players from their group."""
- def _unjoin_all(entities):
+ def _unjoin_all(entities: list[SonosEntity]) -> None:
"""Sync helper."""
# Unjoin slaves first to prevent inheritance of queues
coordinators = [e for e in entities if e.is_coordinator]
@@ -1293,7 +1220,7 @@ def _unjoin_all(entities):
await SonosEntity.wait_for_groups(hass, [[e] for e in entities])
@soco_error()
- def snapshot(self, with_group):
+ def snapshot(self, with_group: bool) -> None:
"""Snapshot the state of a player."""
self._soco_snapshot = pysonos.snapshot.Snapshot(self.soco)
self._soco_snapshot.snapshot()
@@ -1303,30 +1230,33 @@ def snapshot(self, with_group):
self._snapshot_group = None
@staticmethod
- async def snapshot_multi(hass, entities, with_group):
+ async def snapshot_multi(
+ hass: HomeAssistant, entities: list[SonosEntity], with_group: bool
+ ) -> None:
"""Snapshot all the entities and optionally their groups."""
# pylint: disable=protected-access
- def _snapshot_all(entities):
+ def _snapshot_all(entities: list[SonosEntity]) -> None:
"""Sync helper."""
for entity in entities:
entity.snapshot(with_group)
# Find all affected players
- entities = set(entities)
+ entities_set = set(entities)
if with_group:
- for entity in list(entities):
- entities.update(entity._sonos_group)
+ for entity in list(entities_set):
+ entities_set.update(entity._sonos_group)
async with hass.data[DATA_SONOS].topology_condition:
- await hass.async_add_executor_job(_snapshot_all, entities)
+ await hass.async_add_executor_job(_snapshot_all, entities_set)
@soco_error()
- def restore(self):
+ def restore(self) -> None:
"""Restore a snapshotted state to a player."""
try:
+ assert self._soco_snapshot is not None
self._soco_snapshot.restore()
- except (TypeError, AttributeError, SoCoException) as ex:
+ except (TypeError, AssertionError, AttributeError, SoCoException) as ex:
# Can happen if restoring a coordinator onto a current slave
_LOGGER.warning("Error on restore %s: %s", self.entity_id, ex)
@@ -1334,11 +1264,15 @@ def restore(self):
self._snapshot_group = None
@staticmethod
- async def restore_multi(hass, entities, with_group):
+ async def restore_multi(
+ hass: HomeAssistant, entities: list[SonosEntity], with_group: bool
+ ) -> None:
"""Restore snapshots for all the entities."""
# pylint: disable=protected-access
- def _restore_groups(entities, with_group):
+ def _restore_groups(
+ entities: list[SonosEntity], with_group: bool
+ ) -> list[list[SonosEntity]]:
"""Pause all current coordinators and restore groups."""
for entity in (e for e in entities if e.is_coordinator):
if entity.state == STATE_PLAYING:
@@ -1354,13 +1288,14 @@ def _restore_groups(entities, with_group):
# Bring back the original group topology
for entity in (e for e in entities if e._snapshot_group):
+ assert entity._snapshot_group is not None
if entity._snapshot_group[0] == entity:
entity.join(entity._snapshot_group)
groups.append(entity._snapshot_group.copy())
return groups
- def _restore_players(entities):
+ def _restore_players(entities: list[SonosEntity]) -> None:
"""Restore state of all players."""
for entity in (e for e in entities if not e.is_coordinator):
entity.restore()
@@ -1369,26 +1304,29 @@ def _restore_players(entities):
entity.restore()
# Find all affected players
- entities = {e for e in entities if e._soco_snapshot}
+ entities_set = {e for e in entities if e._soco_snapshot}
if with_group:
- for entity in [e for e in entities if e._snapshot_group]:
- entities.update(entity._snapshot_group)
+ for entity in [e for e in entities_set if e._snapshot_group]:
+ assert entity._snapshot_group is not None
+ entities_set.update(entity._snapshot_group)
async with hass.data[DATA_SONOS].topology_condition:
groups = await hass.async_add_executor_job(
- _restore_groups, entities, with_group
+ _restore_groups, entities_set, with_group
)
await SonosEntity.wait_for_groups(hass, groups)
- await hass.async_add_executor_job(_restore_players, entities)
+ await hass.async_add_executor_job(_restore_players, entities_set)
@staticmethod
- async def wait_for_groups(hass, groups):
+ async def wait_for_groups(
+ hass: HomeAssistant, groups: list[list[SonosEntity]]
+ ) -> None:
"""Wait until all groups are present, or timeout."""
# pylint: disable=protected-access
- def _test_groups(groups):
+ def _test_groups(groups: list[list[SonosEntity]]) -> bool:
"""Return whether all groups exist now."""
for group in groups:
coordinator = group[0]
@@ -1416,21 +1354,26 @@ def _test_groups(groups):
@soco_error()
@soco_coordinator
- def set_sleep_timer(self, sleep_time):
+ def set_sleep_timer(self, sleep_time: int) -> None:
"""Set the timer on the player."""
self.soco.set_sleep_timer(sleep_time)
@soco_error()
@soco_coordinator
- def clear_sleep_timer(self):
+ def clear_sleep_timer(self) -> None:
"""Clear the timer on the player."""
self.soco.set_sleep_timer(None)
@soco_error()
@soco_coordinator
def set_alarm(
- self, alarm_id, time=None, volume=None, enabled=None, include_linked_zones=None
- ):
+ self,
+ alarm_id: int,
+ time: datetime.datetime | None = None,
+ volume: float | None = None,
+ enabled: bool | None = None,
+ include_linked_zones: bool | None = None,
+ ) -> None:
"""Set the alarm clock on the player."""
alarm = None
for one_alarm in alarms.get_alarms(self.soco):
@@ -1438,7 +1381,7 @@ def set_alarm(
if one_alarm._alarm_id == str(alarm_id):
alarm = one_alarm
if alarm is None:
- _LOGGER.warning("did not find alarm with id %s", alarm_id)
+ _LOGGER.warning("Did not find alarm with id %s", alarm_id)
return
if time is not None:
alarm.start_time = time
@@ -1451,7 +1394,12 @@ def set_alarm(
alarm.save()
@soco_error()
- def set_option(self, night_sound=None, speech_enhance=None, status_light=None):
+ def set_option(
+ self,
+ night_sound: bool | None = None,
+ speech_enhance: bool | None = None,
+ status_light: bool | None = None,
+ ) -> None:
"""Modify playback options."""
if night_sound is not None and self._night_sound is not None:
self.soco.night_mode = night_sound
@@ -1463,20 +1411,22 @@ def set_option(self, night_sound=None, speech_enhance=None, status_light=None):
self.soco.status_light = status_light
@soco_error()
- def play_queue(self, queue_position=0):
+ def play_queue(self, queue_position: int = 0) -> None:
"""Start playing the queue."""
self.soco.play_from_queue(queue_position)
@soco_error()
@soco_coordinator
- def remove_from_queue(self, queue_position=0):
+ def remove_from_queue(self, queue_position: int = 0) -> None:
"""Remove item from the queue."""
self.soco.remove_from_queue(queue_position)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return entity specific state attributes."""
- attributes = {ATTR_SONOS_GROUP: [e.entity_id for e in self._sonos_group]}
+ attributes: dict[str, Any] = {
+ ATTR_SONOS_GROUP: [e.entity_id for e in self._sonos_group]
+ }
if self._night_sound is not None:
attributes[ATTR_NIGHT_SOUND] = self._night_sound
@@ -1489,11 +1439,58 @@ def device_state_attributes(self):
return attributes
- async def async_browse_media(self, media_content_type=None, media_content_id=None):
+ async def async_get_browse_image(
+ self,
+ media_content_type: str | None,
+ media_content_id: str | None,
+ media_image_id: str | None = None,
+ ) -> tuple[None | str, None | str]:
+ """Fetch media browser image to serve via proxy."""
+ if (
+ media_content_type in [MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST]
+ and media_content_id
+ ):
+ item = await self.hass.async_add_executor_job(
+ get_media,
+ self._media_library,
+ media_content_id,
+ MEDIA_TYPES_TO_SONOS[media_content_type],
+ )
+ image_url = getattr(item, "album_art_uri", None)
+ if image_url:
+ result = await self._async_fetch_image(image_url) # type: ignore[no-untyped-call]
+ return result # type: ignore
+
+ return (None, None)
+
+ async def async_browse_media(
+ self, media_content_type: str | None = None, media_content_id: str | None = None
+ ) -> Any:
"""Implement the websocket media browsing helper."""
+ is_internal = is_internal_request(self.hass)
+
+ def _get_thumbnail_url(
+ media_content_type: str,
+ media_content_id: str,
+ media_image_id: str | None = None,
+ ) -> str | None:
+ if is_internal:
+ item = get_media( # type: ignore[no-untyped-call]
+ self._media_library,
+ media_content_id,
+ media_content_type,
+ )
+ return getattr(item, "album_art_uri", None) # type: ignore[no-any-return]
+
+ return self.get_browse_image_url(
+ media_content_type,
+ urllib.parse.quote_plus(media_content_id),
+ media_image_id,
+ )
+
if media_content_type in [None, "library"]:
return await self.hass.async_add_executor_job(
- library_payload, self._media_library
+ library_payload, self._media_library, _get_thumbnail_url
)
payload = {
@@ -1501,195 +1498,10 @@ async def async_browse_media(self, media_content_type=None, media_content_id=Non
"idstring": media_content_id,
}
response = await self.hass.async_add_executor_job(
- build_item_response, self._media_library, payload
+ build_item_response, self._media_library, payload, _get_thumbnail_url
)
if response is None:
raise BrowseError(
f"Media not found: {media_content_type} / {media_content_id}"
)
return response
-
-
-def build_item_response(media_library, payload):
- """Create response payload for the provided media query."""
- if payload["search_type"] == MEDIA_TYPE_ALBUM and payload["idstring"].startswith(
- ("A:GENRE", "A:COMPOSER")
- ):
- payload["idstring"] = "A:ALBUMARTIST/" + "/".join(
- payload["idstring"].split("/")[2:]
- )
-
- media = media_library.browse_by_idstring(
- MEDIA_TYPES_TO_SONOS[payload["search_type"]],
- payload["idstring"],
- full_album_art_uri=True,
- max_items=0,
- )
-
- if media is None:
- return
-
- thumbnail = None
- title = None
-
- # Fetch album info for titles and thumbnails
- # Can't be extracted from track info
- if (
- payload["search_type"] == MEDIA_TYPE_ALBUM
- and media[0].item_class == "object.item.audioItem.musicTrack"
- ):
- item = get_media(media_library, payload["idstring"], SONOS_ALBUM_ARTIST)
- title = getattr(item, "title", None)
- thumbnail = getattr(item, "album_art_uri", media[0].album_art_uri)
-
- if not title:
- try:
- title = urllib.parse.unquote(payload["idstring"].split("/")[1])
- except IndexError:
- title = LIBRARY_TITLES_MAPPING[payload["idstring"]]
-
- try:
- media_class = SONOS_TO_MEDIA_CLASSES[
- MEDIA_TYPES_TO_SONOS[payload["search_type"]]
- ]
- except KeyError:
- _LOGGER.debug("Unknown media type received %s", payload["search_type"])
- return None
-
- children = []
- for item in media:
- try:
- children.append(item_payload(item))
- except UnknownMediaType:
- pass
-
- return BrowseMedia(
- title=title,
- thumbnail=thumbnail,
- media_class=media_class,
- media_content_id=payload["idstring"],
- media_content_type=payload["search_type"],
- children=children,
- can_play=can_play(payload["search_type"]),
- can_expand=can_expand(payload["search_type"]),
- )
-
-
-def item_payload(item):
- """
- Create response payload for a single media item.
-
- Used by async_browse_media.
- """
- media_type = get_media_type(item)
- try:
- media_class = SONOS_TO_MEDIA_CLASSES[media_type]
- except KeyError as err:
- _LOGGER.debug("Unknown media type received %s", media_type)
- raise UnknownMediaType from err
- return BrowseMedia(
- title=item.title,
- thumbnail=getattr(item, "album_art_uri", None),
- media_class=media_class,
- media_content_id=get_content_id(item),
- media_content_type=SONOS_TO_MEDIA_TYPES[media_type],
- can_play=can_play(item.item_class),
- can_expand=can_expand(item),
- )
-
-
-def library_payload(media_library):
- """
- Create response payload to describe contents of a specific library.
-
- Used by async_browse_media.
- """
- if not media_library.browse_by_idstring(
- "tracks",
- "",
- max_items=1,
- ):
- raise BrowseError("Local library not found")
-
- children = []
- for item in media_library.browse():
- try:
- children.append(item_payload(item))
- except UnknownMediaType:
- pass
-
- return BrowseMedia(
- title="Music Library",
- media_class=MEDIA_CLASS_DIRECTORY,
- media_content_id="library",
- media_content_type="library",
- can_play=False,
- can_expand=True,
- children=children,
- )
-
-
-def get_media_type(item):
- """Extract media type of item."""
- if item.item_class == "object.item.audioItem.musicTrack":
- return SONOS_TRACKS
-
- if (
- item.item_class == "object.container.album.musicAlbum"
- and SONOS_TYPES_MAPPING.get(item.item_id.split("/")[0])
- in [
- SONOS_ALBUM_ARTIST,
- SONOS_GENRE,
- ]
- ):
- return SONOS_TYPES_MAPPING[item.item_class]
-
- return SONOS_TYPES_MAPPING.get(item.item_id.split("/")[0], item.item_class)
-
-
-def can_play(item):
- """
- Test if playable.
-
- Used by async_browse_media.
- """
- return SONOS_TO_MEDIA_TYPES.get(item) in PLAYABLE_MEDIA_TYPES
-
-
-def can_expand(item):
- """
- Test if expandable.
-
- Used by async_browse_media.
- """
- if isinstance(item, str):
- return SONOS_TYPES_MAPPING.get(item) in EXPANDABLE_MEDIA_TYPES
-
- if SONOS_TO_MEDIA_TYPES.get(item.item_class) in EXPANDABLE_MEDIA_TYPES:
- return True
-
- return SONOS_TYPES_MAPPING.get(item.item_id) in EXPANDABLE_MEDIA_TYPES
-
-
-def get_content_id(item):
- """Extract content id or uri."""
- if item.item_class == "object.item.audioItem.musicTrack":
- return item.get_uri()
- return item.item_id
-
-
-def get_media(media_library, item_id, search_type):
- """Fetch media/album."""
- search_type = MEDIA_TYPES_TO_SONOS.get(search_type, search_type)
-
- if not item_id.startswith("A:ALBUM") and search_type == SONOS_ALBUM:
- item_id = "A:ALBUMARTIST/" + "/".join(item_id.split("/")[2:])
-
- for item in media_library.browse_by_idstring(
- search_type,
- "/".join(item_id.split("/")[:-1]),
- full_album_art_uri=True,
- max_items=0,
- ):
- if item.item_id == item_id:
- return item
diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml
index 8a35e9a7790667..99b430e46806a6 100644
--- a/homeassistant/components/sonos/services.yaml
+++ b/homeassistant/components/sonos/services.yaml
@@ -1,9 +1,15 @@
join:
+ name: Join group
description: Group player together.
fields:
master:
- description: Entity ID of the player that should become the coordinator of the group.
+ description:
+ Entity ID of the player that should become the coordinator of the group.
example: "media_player.living_room_sonos"
+ selector:
+ entity:
+ integration: sonos
+ domain: media_player
entity_id:
description: Name(s) of entities that will join the master.
example: "media_player.living_room_sonos"
@@ -13,6 +19,7 @@ join:
domain: media_player
unjoin:
+ name: Unjoin group
description: Unjoin the player from a group.
fields:
entity_id:
@@ -24,6 +31,7 @@ unjoin:
domain: media_player
snapshot:
+ name: Snapshot
description: Take a snapshot of the media player.
fields:
entity_id:
@@ -38,6 +46,7 @@ snapshot:
example: "true"
restore:
+ name: Restore
description: Restore a snapshot of the media player.
fields:
entity_id:
@@ -52,6 +61,7 @@ restore:
example: "true"
set_sleep_timer:
+ name: Set timer
description: Set a Sonos timer.
fields:
entity_id:
@@ -64,8 +74,16 @@ set_sleep_timer:
sleep_time:
description: Number of seconds to set the timer.
example: "900"
+ selector:
+ number:
+ min: 0
+ max: 3600
+ step: 1
+ unit_of_measurement: seconds
+ mode: slider
clear_sleep_timer:
+ name: Clear timer
description: Clear a Sonos timer.
fields:
entity_id:
@@ -77,6 +95,7 @@ clear_sleep_timer:
domain: media_player
set_option:
+ name: Set option
description: Set Sonos sound options.
fields:
entity_id:
@@ -89,15 +108,22 @@ set_option:
night_sound:
description: Enable Night Sound mode
example: "true"
+ selector:
+ boolean:
speech_enhance:
description: Enable Speech Enhancement mode
example: "true"
+ selector:
+ boolean:
status_light:
description: Enable Status (LED) Light
example: "true"
+ selector:
+ boolean:
play_queue:
- description: Starts playing the queue from the first item.
+ name: Play queue
+ description: Start playing the queue from the first item.
fields:
entity_id:
description: Name(s) of entities that will start playing.
@@ -109,8 +135,14 @@ play_queue:
queue_position:
description: Position of the song in the queue to start playing from.
example: "0"
+ selector:
+ number:
+ min: 0
+ max: 100000000
+ mode: box
remove_from_queue:
+ name: Remove from queue
description: Removes an item from the queue.
fields:
entity_id:
@@ -123,8 +155,14 @@ remove_from_queue:
queue_position:
description: Position in the queue to remove.
example: "0"
+ selector:
+ number:
+ min: 0
+ max: 100000000
+ mode: box
update_alarm:
+ name: Update alarm
description: Updates an alarm with new time and volume settings.
fields:
alarm_id:
diff --git a/homeassistant/components/sonos/translations/de.json b/homeassistant/components/sonos/translations/de.json
index 93b25cf0b9781b..5d66c16811671d 100644
--- a/homeassistant/components/sonos/translations/de.json
+++ b/homeassistant/components/sonos/translations/de.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"no_devices_found": "Keine Sonos Ger\u00e4te im Netzwerk gefunden.",
- "single_instance_allowed": "Nur eine einzige Konfiguration von Sonos ist notwendig."
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/sonos/translations/hu.json b/homeassistant/components/sonos/translations/hu.json
index aa10087a884e9a..2123ec520f7474 100644
--- a/homeassistant/components/sonos/translations/hu.json
+++ b/homeassistant/components/sonos/translations/hu.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Nem tal\u00e1lhat\u00f3k Sonos eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton.",
- "single_instance_allowed": "Csak egyetlen Sonos konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges."
+ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/sonos/translations/id.json b/homeassistant/components/sonos/translations/id.json
index ef88cab58142db..145e2775e4a1ff 100644
--- a/homeassistant/components/sonos/translations/id.json
+++ b/homeassistant/components/sonos/translations/id.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "no_devices_found": "Tidak ada perangkat Sonos yang ditemukan pada jaringan.",
- "single_instance_allowed": "Hanya satu konfigurasi Sonos yang diperlukan."
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
},
"step": {
"confirm": {
- "description": "Apakah Anda ingin mengatur Sonos?"
+ "description": "Ingin menyiapkan Sonos?"
}
}
}
diff --git a/homeassistant/components/sonos/translations/ko.json b/homeassistant/components/sonos/translations/ko.json
index c92b50a0f83933..f85f3c5cab402f 100644
--- a/homeassistant/components/sonos/translations/ko.json
+++ b/homeassistant/components/sonos/translations/ko.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "no_devices_found": "Sonos \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
- "single_instance_allowed": "\ud558\ub098\uc758 Sonos \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"step": {
"confirm": {
- "description": "Sonos \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ "description": "Sonos\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
}
}
}
diff --git a/homeassistant/components/sonos/translations/nl.json b/homeassistant/components/sonos/translations/nl.json
index e52111fc50f6e2..42298f0b4f7dc1 100644
--- a/homeassistant/components/sonos/translations/nl.json
+++ b/homeassistant/components/sonos/translations/nl.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Geen Sonos-apparaten gevonden op het netwerk.",
- "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van Sonos nodig."
+ "no_devices_found": "Geen apparaten gevonden op het netwerk",
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/sonos/translations/tr.json b/homeassistant/components/sonos/translations/tr.json
new file mode 100644
index 00000000000000..42bd46ce7c0e35
--- /dev/null
+++ b/homeassistant/components/sonos/translations/tr.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "step": {
+ "confirm": {
+ "description": "Sonos'u kurmak istiyor musunuz?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonos/translations/uk.json b/homeassistant/components/sonos/translations/uk.json
new file mode 100644
index 00000000000000..aff6c9f59b1799
--- /dev/null
+++ b/homeassistant/components/sonos/translations/uk.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Sonos?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sony_projector/switch.py b/homeassistant/components/sony_projector/switch.py
index 723478ac34b6e9..935b33cc5df86e 100644
--- a/homeassistant/components/sony_projector/switch.py
+++ b/homeassistant/components/sony_projector/switch.py
@@ -65,7 +65,7 @@ def is_on(self):
return self._state
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return state attributes."""
return self._attributes
diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py
index 83c8192ccb279a..1b07f01e92a7c2 100644
--- a/homeassistant/components/soundtouch/media_player.py
+++ b/homeassistant/components/soundtouch/media_player.py
@@ -440,7 +440,7 @@ def add_zone_slave(self, slaves):
self._device.add_zone_slave([slave.device for slave in slaves])
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return entity specific state attributes."""
attributes = {}
diff --git a/homeassistant/components/spaceapi/__init__.py b/homeassistant/components/spaceapi/__init__.py
index 571e0ab62f377c..66583050b206f5 100644
--- a/homeassistant/components/spaceapi/__init__.py
+++ b/homeassistant/components/spaceapi/__init__.py
@@ -1,4 +1,6 @@
"""Support for the SpaceAPI."""
+from contextlib import suppress
+
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
@@ -6,6 +8,7 @@
ATTR_ENTITY_ID,
ATTR_ICON,
ATTR_LOCATION,
+ ATTR_NAME,
ATTR_STATE,
ATTR_UNIT_OF_MEASUREMENT,
CONF_ADDRESS,
@@ -35,7 +38,6 @@
ATTR_ISSUE_REPORT_CHANNELS = "issue_report_channels"
ATTR_LASTCHANGE = "lastchange"
ATTR_LOGO = "logo"
-ATTR_NAME = "name"
ATTR_OPEN = "open"
ATTR_SENSORS = "sensors"
ATTR_SPACE = "space"
@@ -287,13 +289,11 @@ def get(self, request):
else:
state = {ATTR_OPEN: "null", ATTR_LASTCHANGE: 0}
- try:
+ with suppress(KeyError):
state[ATTR_ICON] = {
ATTR_OPEN: spaceapi["state"][CONF_ICON_OPEN],
ATTR_CLOSE: spaceapi["state"][CONF_ICON_CLOSED],
}
- except KeyError:
- pass
data = {
ATTR_API: SPACEAPI_VERSION,
@@ -306,40 +306,26 @@ def get(self, request):
ATTR_URL: spaceapi[CONF_URL],
}
- try:
+ with suppress(KeyError):
data[ATTR_CAM] = spaceapi[CONF_CAM]
- except KeyError:
- pass
- try:
+ with suppress(KeyError):
data[ATTR_SPACEFED] = spaceapi[CONF_SPACEFED]
- except KeyError:
- pass
- try:
+ with suppress(KeyError):
data[ATTR_STREAM] = spaceapi[CONF_STREAM]
- except KeyError:
- pass
- try:
+ with suppress(KeyError):
data[ATTR_FEEDS] = spaceapi[CONF_FEEDS]
- except KeyError:
- pass
- try:
+ with suppress(KeyError):
data[ATTR_CACHE] = spaceapi[CONF_CACHE]
- except KeyError:
- pass
- try:
+ with suppress(KeyError):
data[ATTR_PROJECTS] = spaceapi[CONF_PROJECTS]
- except KeyError:
- pass
- try:
+ with suppress(KeyError):
data[ATTR_RADIO_SHOW] = spaceapi[CONF_RADIO_SHOW]
- except KeyError:
- pass
if is_sensors is not None:
sensors = {}
diff --git a/homeassistant/components/speedtestdotnet/config_flow.py b/homeassistant/components/speedtestdotnet/config_flow.py
index 5ff405b59a839a..84d63ebc33bd13 100644
--- a/homeassistant/components/speedtestdotnet/config_flow.py
+++ b/homeassistant/components/speedtestdotnet/config_flow.py
@@ -13,8 +13,8 @@
DEFAULT_NAME,
DEFAULT_SCAN_INTERVAL,
DEFAULT_SERVER,
+ DOMAIN,
)
-from .const import DOMAIN # pylint: disable=unused-import
class SpeedTestFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/speedtestdotnet/manifest.json b/homeassistant/components/speedtestdotnet/manifest.json
index d230f03f954d29..f2e2a2196c98b3 100644
--- a/homeassistant/components/speedtestdotnet/manifest.json
+++ b/homeassistant/components/speedtestdotnet/manifest.json
@@ -3,6 +3,8 @@
"name": "Speedtest.net",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/speedtestdotnet",
- "requirements": ["speedtest-cli==2.1.2"],
+ "requirements": [
+ "speedtest-cli==2.1.3"
+ ],
"codeowners": ["@rohankapoorcom", "@engrbm87"]
}
diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py
index 5607d2570c9797..c49a5691cec953 100644
--- a/homeassistant/components/speedtestdotnet/sensor.py
+++ b/homeassistant/components/speedtestdotnet/sensor.py
@@ -1,4 +1,5 @@
"""Support for Speedtest.net internet speed testing sensor."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.core import callback
from homeassistant.helpers.restore_state import RestoreEntity
@@ -30,7 +31,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities)
-class SpeedtestSensor(CoordinatorEntity, RestoreEntity):
+class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity):
"""Implementation of a speedtest.net sensor."""
def __init__(self, coordinator, sensor_type):
@@ -67,7 +68,7 @@ def icon(self):
return ICON
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if not self.coordinator.data:
return None
diff --git a/homeassistant/components/speedtestdotnet/translations/de.json b/homeassistant/components/speedtestdotnet/translations/de.json
index 56b1a91a89e4e8..f2635c19f033cb 100644
--- a/homeassistant/components/speedtestdotnet/translations/de.json
+++ b/homeassistant/components/speedtestdotnet/translations/de.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "wrong_server_id": "Server ID ist ung\u00fcltig"
+ "single_instance_allowed": "Bereits konfiguriert. Es ist nur eine Konfiguration m\u00f6glich.",
+ "wrong_server_id": "Server-ID ist ung\u00fcltig"
},
"step": {
"user": {
- "description": "Einrichtung beginnen?"
+ "description": "M\u00f6chten Sie mit der Einrichtung beginnen?"
}
}
},
@@ -13,7 +14,9 @@
"step": {
"init": {
"data": {
- "manual": "Automatische Updates deaktivieren"
+ "manual": "Automatische Updates deaktivieren",
+ "scan_interval": "Aktualisierungsfrequenz (Minuten)",
+ "server_name": "Testserver ausw\u00e4hlen"
}
}
}
diff --git a/homeassistant/components/speedtestdotnet/translations/hu.json b/homeassistant/components/speedtestdotnet/translations/hu.json
new file mode 100644
index 00000000000000..ec08c711e1d374
--- /dev/null
+++ b/homeassistant/components/speedtestdotnet/translations/hu.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.",
+ "wrong_server_id": "A szerver azonos\u00edt\u00f3 \u00e9rv\u00e9nytelen"
+ },
+ "step": {
+ "user": {
+ "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "server_name": "V\u00e1laszd ki a teszt szervert"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/speedtestdotnet/translations/id.json b/homeassistant/components/speedtestdotnet/translations/id.json
new file mode 100644
index 00000000000000..24e78609380bdc
--- /dev/null
+++ b/homeassistant/components/speedtestdotnet/translations/id.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.",
+ "wrong_server_id": "ID server tidak valid"
+ },
+ "step": {
+ "user": {
+ "description": "Ingin memulai penyiapan?"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "manual": "Nonaktifkan pembaruan otomatis",
+ "scan_interval": "Frekuensi pembaruan (menit)",
+ "server_name": "Pilih server uji"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/speedtestdotnet/translations/ko.json b/homeassistant/components/speedtestdotnet/translations/ko.json
index ede64fa053119d..e3b652083174f6 100644
--- a/homeassistant/components/speedtestdotnet/translations/ko.json
+++ b/homeassistant/components/speedtestdotnet/translations/ko.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "wrong_server_id": "\uc11c\ubc84 ID \uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "wrong_server_id": "\uc11c\ubc84 ID\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
- "description": "SpeedTest \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
}
}
},
diff --git a/homeassistant/components/speedtestdotnet/translations/nl.json b/homeassistant/components/speedtestdotnet/translations/nl.json
index 0c0c184b5fe582..5de8460fd77e14 100644
--- a/homeassistant/components/speedtestdotnet/translations/nl.json
+++ b/homeassistant/components/speedtestdotnet/translations/nl.json
@@ -3,6 +3,22 @@
"abort": {
"single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.",
"wrong_server_id": "Server-ID is niet geldig"
+ },
+ "step": {
+ "user": {
+ "description": "Wil je beginnen met instellen?"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "manual": "Automatische updaten uitschakelen",
+ "scan_interval": "Update frequentie (minuten)",
+ "server_name": "Selecteer testserver"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/speedtestdotnet/translations/tr.json b/homeassistant/components/speedtestdotnet/translations/tr.json
new file mode 100644
index 00000000000000..b13be7c5e0cb7d
--- /dev/null
+++ b/homeassistant/components/speedtestdotnet/translations/tr.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.",
+ "wrong_server_id": "Sunucu kimli\u011fi ge\u00e7erli de\u011fil"
+ },
+ "step": {
+ "user": {
+ "description": "Kuruluma ba\u015flamak ister misiniz?"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "manual": "Otomatik g\u00fcncellemeyi devre d\u0131\u015f\u0131 b\u0131rak\u0131n",
+ "scan_interval": "G\u00fcncelleme s\u0131kl\u0131\u011f\u0131 (dakika)",
+ "server_name": "Test sunucusunu se\u00e7in"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/speedtestdotnet/translations/uk.json b/homeassistant/components/speedtestdotnet/translations/uk.json
new file mode 100644
index 00000000000000..89ef24440d13e4
--- /dev/null
+++ b/homeassistant/components/speedtestdotnet/translations/uk.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.",
+ "wrong_server_id": "\u041d\u0435\u043f\u0440\u0438\u043f\u0443\u0441\u0442\u0438\u043c\u0438\u0439 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430."
+ },
+ "step": {
+ "user": {
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "manual": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f",
+ "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0443 \u0445\u0432\u0438\u043b\u0438\u043d\u0430\u0445)",
+ "server_name": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0443\u0432\u0430\u043d\u043d\u044f"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/spider/__init__.py b/homeassistant/components/spider/__init__.py
index b0c34ae5a08100..d9ccdfd248a130 100644
--- a/homeassistant/components/spider/__init__.py
+++ b/homeassistant/components/spider/__init__.py
@@ -66,9 +66,9 @@ async def async_setup_entry(hass, entry):
hass.data[DOMAIN][entry.entry_id] = api
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -79,8 +79,8 @@ async def async_unload_entry(hass, entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/spider/translations/de.json b/homeassistant/components/spider/translations/de.json
index 6f39806287630f..c57e55e9d2ea1b 100644
--- a/homeassistant/components/spider/translations/de.json
+++ b/homeassistant/components/spider/translations/de.json
@@ -1,5 +1,12 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
+ "error": {
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/spider/translations/hu.json b/homeassistant/components/spider/translations/hu.json
new file mode 100644
index 00000000000000..9639cfe6367d56
--- /dev/null
+++ b/homeassistant/components/spider/translations/hu.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
+ "error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "title": "Bejelentkez\u00e9s mijn.ithodaalderop.nl fi\u00f3kkal"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/spider/translations/id.json b/homeassistant/components/spider/translations/id.json
new file mode 100644
index 00000000000000..2ea038fdcdd76b
--- /dev/null
+++ b/homeassistant/components/spider/translations/id.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "title": "Masuk dengan akun mijn.ithodaalderop.nl"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/spider/translations/ko.json b/homeassistant/components/spider/translations/ko.json
index 1f08b96ee10939..5c72b30726af9c 100644
--- a/homeassistant/components/spider/translations/ko.json
+++ b/homeassistant/components/spider/translations/ko.json
@@ -1,8 +1,20 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
"error": {
- "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d",
- "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec"
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ },
+ "title": "mijn.ithodaalderop.nl \uacc4\uc815\uc73c\ub85c \ub85c\uadf8\uc778\ud558\uae30"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/spider/translations/nl.json b/homeassistant/components/spider/translations/nl.json
index f0b4ddf59a9ea8..373d203aed7e64 100644
--- a/homeassistant/components/spider/translations/nl.json
+++ b/homeassistant/components/spider/translations/nl.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
+ },
"error": {
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
@@ -9,7 +12,8 @@
"data": {
"password": "Wachtwoord",
"username": "Gebruikersnaam"
- }
+ },
+ "title": "Aanmelden met mijn.ithodaalderop.nl account"
}
}
}
diff --git a/homeassistant/components/spider/translations/ru.json b/homeassistant/components/spider/translations/ru.json
index 983f2b94361b41..08d70e4406539e 100644
--- a/homeassistant/components/spider/translations/ru.json
+++ b/homeassistant/components/spider/translations/ru.json
@@ -4,14 +4,14 @@
"single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e."
},
"error": {
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "\u0412\u0445\u043e\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 mijn.ithodaalderop.nl"
}
diff --git a/homeassistant/components/spider/translations/tr.json b/homeassistant/components/spider/translations/tr.json
new file mode 100644
index 00000000000000..9bcc6bb1c41c70
--- /dev/null
+++ b/homeassistant/components/spider/translations/tr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/spider/translations/uk.json b/homeassistant/components/spider/translations/uk.json
new file mode 100644
index 00000000000000..b8be2a1488791e
--- /dev/null
+++ b/homeassistant/components/spider/translations/uk.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "\u0412\u0445\u0456\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 mijn.ithodaalderop.nl"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/spotcrime/sensor.py b/homeassistant/components/spotcrime/sensor.py
index 30aa80b5e7da5b..72a6fec84e97eb 100644
--- a/homeassistant/components/spotcrime/sensor.py
+++ b/homeassistant/components/spotcrime/sensor.py
@@ -6,7 +6,7 @@
import spotcrime
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_LATITUDE,
@@ -20,7 +20,6 @@
CONF_RADIUS,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify
CONF_DAYS = "days"
@@ -66,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
)
-class SpotCrimeSensor(Entity):
+class SpotCrimeSensor(SensorEntity):
"""Representation of a Spot Crime Sensor."""
def __init__(
@@ -103,7 +102,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attributes
diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py
index e28e1fcf315a34..e36491670f5f6d 100644
--- a/homeassistant/components/spotify/__init__.py
+++ b/homeassistant/components/spotify/__init__.py
@@ -6,7 +6,7 @@
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.components.spotify import config_flow
-from homeassistant.config_entries import ConfigEntry
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.const import ATTR_CREDENTIALS, CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
@@ -87,7 +87,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
- context={"source": "reauth"},
+ context={"source": SOURCE_REAUTH},
data=entry.data,
)
)
diff --git a/homeassistant/components/spotify/config_flow.py b/homeassistant/components/spotify/config_flow.py
index ac6e101d4fe0c1..d0fb73e18bdb3e 100644
--- a/homeassistant/components/spotify/config_flow.py
+++ b/homeassistant/components/spotify/config_flow.py
@@ -1,6 +1,8 @@
"""Config flow for Spotify."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from spotipy import Spotify
import voluptuous as vol
@@ -24,7 +26,7 @@ class SpotifyFlowHandler(
def __init__(self) -> None:
"""Instantiate config flow."""
super().__init__()
- self.entry: Optional[Dict[str, Any]] = None
+ self.entry: dict[str, Any] | None = None
@property
def logger(self) -> logging.Logger:
@@ -32,11 +34,11 @@ def logger(self) -> logging.Logger:
return logging.getLogger(__name__)
@property
- def extra_authorize_data(self) -> Dict[str, Any]:
+ def extra_authorize_data(self) -> dict[str, Any]:
"""Extra data that needs to be appended to the authorize url."""
return {"scope": ",".join(SPOTIFY_SCOPES)}
- async def async_oauth_create_entry(self, data: Dict[str, Any]) -> Dict[str, Any]:
+ async def async_oauth_create_entry(self, data: dict[str, Any]) -> dict[str, Any]:
"""Create an entry for Spotify."""
spotify = Spotify(auth=data["token"]["access_token"])
@@ -58,12 +60,11 @@ async def async_oauth_create_entry(self, data: Dict[str, Any]) -> Dict[str, Any]
return self.async_create_entry(title=name, data=data)
- async def async_step_reauth(self, entry: Dict[str, Any]) -> Dict[str, Any]:
+ async def async_step_reauth(self, entry: dict[str, Any]) -> dict[str, Any]:
"""Perform reauth upon migration of old entries."""
if entry:
self.entry = entry
- assert self.hass
persistent_notification.async_create(
self.hass,
f"Spotify integration for account {entry['id']} needs to be re-authenticated. Please go to the integrations page to re-configure it.",
@@ -74,8 +75,8 @@ async def async_step_reauth(self, entry: Dict[str, Any]) -> Dict[str, Any]:
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Confirm reauth dialog."""
if user_input is None:
return self.async_show_form(
@@ -85,7 +86,6 @@ async def async_step_reauth_confirm(
errors={},
)
- assert self.hass
persistent_notification.async_dismiss(self.hass, "spotify_reauth")
return await self.async_step_pick_implementation(
diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json
index c4d7378c060522..bd92217e9cf4d8 100644
--- a/homeassistant/components/spotify/manifest.json
+++ b/homeassistant/components/spotify/manifest.json
@@ -2,7 +2,7 @@
"domain": "spotify",
"name": "Spotify",
"documentation": "https://www.home-assistant.io/integrations/spotify",
- "requirements": ["spotipy==2.16.1"],
+ "requirements": ["spotipy==2.17.1"],
"zeroconf": ["_spotify-connect._tcp.local."],
"dependencies": ["http"],
"codeowners": ["@frenck"],
diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py
index e4450e7a3068a1..0a291582a30ae0 100644
--- a/homeassistant/components/spotify/media_player.py
+++ b/homeassistant/components/spotify/media_player.py
@@ -1,9 +1,11 @@
"""Support for interacting with Spotify Connect."""
+from __future__ import annotations
+
from asyncio import run_coroutine_threadsafe
import datetime as dt
from datetime import timedelta
import logging
-from typing import Any, Callable, Dict, List, Optional
+from typing import Any, Callable
import requests
from spotipy import Spotify, SpotifyException
@@ -50,6 +52,7 @@
STATE_PLAYING,
)
from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from homeassistant.helpers.entity import Entity
from homeassistant.util.dt import utc_from_timestamp
@@ -185,7 +188,7 @@ class UnknownMediaType(BrowseError):
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up Spotify based on a config entry."""
spotify = SpotifyMediaPlayer(
@@ -210,8 +213,12 @@ def wrapper(self, *args, **kwargs):
result = func(self, *args, **kwargs)
self.player_available = True
return result
- except (SpotifyException, requests.RequestException):
+ except requests.RequestException:
+ self.player_available = False
+ except SpotifyException as exc:
self.player_available = False
+ if exc.reason == "NO_ACTIVE_DEVICE":
+ raise HomeAssistantError("No active playback device found") from None
return wrapper
@@ -237,9 +244,9 @@ def __init__(
SPOTIFY_SCOPES
)
- self._currently_playing: Optional[dict] = {}
- self._devices: Optional[List[dict]] = []
- self._playlist: Optional[dict] = None
+ self._currently_playing: dict | None = {}
+ self._devices: list[dict] | None = []
+ self._playlist: dict | None = None
self._spotify: Spotify = None
self.player_available = False
@@ -265,7 +272,7 @@ def unique_id(self) -> str:
return self._id
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about this entity."""
if self._me is not None:
model = self._me["product"]
@@ -278,7 +285,7 @@ def device_info(self) -> Dict[str, Any]:
}
@property
- def state(self) -> Optional[str]:
+ def state(self) -> str | None:
"""Return the playback state."""
if not self._currently_playing:
return STATE_IDLE
@@ -287,44 +294,44 @@ def state(self) -> Optional[str]:
return STATE_PAUSED
@property
- def volume_level(self) -> Optional[float]:
+ def volume_level(self) -> float | None:
"""Return the device volume."""
return self._currently_playing.get("device", {}).get("volume_percent", 0) / 100
@property
- def media_content_id(self) -> Optional[str]:
+ def media_content_id(self) -> str | None:
"""Return the media URL."""
item = self._currently_playing.get("item") or {}
return item.get("uri")
@property
- def media_content_type(self) -> Optional[str]:
+ def media_content_type(self) -> str | None:
"""Return the media type."""
return MEDIA_TYPE_MUSIC
@property
- def media_duration(self) -> Optional[int]:
+ def media_duration(self) -> int | None:
"""Duration of current playing media in seconds."""
if self._currently_playing.get("item") is None:
return None
return self._currently_playing["item"]["duration_ms"] / 1000
@property
- def media_position(self) -> Optional[str]:
+ def media_position(self) -> str | None:
"""Position of current playing media in seconds."""
if not self._currently_playing:
return None
return self._currently_playing["progress_ms"] / 1000
@property
- def media_position_updated_at(self) -> Optional[dt.datetime]:
+ def media_position_updated_at(self) -> dt.datetime | None:
"""When was the position of the current playing media valid."""
if not self._currently_playing:
return None
return utc_from_timestamp(self._currently_playing["timestamp"] / 1000)
@property
- def media_image_url(self) -> Optional[str]:
+ def media_image_url(self) -> str | None:
"""Return the media image URL."""
if (
self._currently_playing.get("item") is None
@@ -339,13 +346,13 @@ def media_image_remotely_accessible(self) -> bool:
return False
@property
- def media_title(self) -> Optional[str]:
+ def media_title(self) -> str | None:
"""Return the media title."""
item = self._currently_playing.get("item") or {}
return item.get("name")
@property
- def media_artist(self) -> Optional[str]:
+ def media_artist(self) -> str | None:
"""Return the media artist."""
if self._currently_playing.get("item") is None:
return None
@@ -354,14 +361,14 @@ def media_artist(self) -> Optional[str]:
)
@property
- def media_album_name(self) -> Optional[str]:
+ def media_album_name(self) -> str | None:
"""Return the media album."""
if self._currently_playing.get("item") is None:
return None
return self._currently_playing["item"]["album"]["name"]
@property
- def media_track(self) -> Optional[int]:
+ def media_track(self) -> int | None:
"""Track number of current playing media, music track only."""
item = self._currently_playing.get("item") or {}
return item.get("track_number")
@@ -374,12 +381,12 @@ def media_playlist(self):
return self._playlist["name"]
@property
- def source(self) -> Optional[str]:
+ def source(self) -> str | None:
"""Return the current playback device."""
return self._currently_playing.get("device", {}).get("name")
@property
- def source_list(self) -> Optional[List[str]]:
+ def source_list(self) -> list[str] | None:
"""Return a list of source devices."""
if not self._devices:
return None
@@ -391,7 +398,7 @@ def shuffle(self) -> bool:
return bool(self._currently_playing.get("shuffle_state"))
@property
- def repeat(self) -> Optional[str]:
+ def repeat(self) -> str | None:
"""Return current repeat mode."""
repeat_state = self._currently_playing.get("repeat_state")
return REPEAT_MODE_MAPPING_TO_HA.get(repeat_state)
@@ -621,7 +628,7 @@ def build_item_response(spotify, user, payload):
try:
item_id = item["id"]
except KeyError:
- _LOGGER.debug("Missing id for media item: %s", item)
+ _LOGGER.debug("Missing ID for media item: %s", item)
continue
media_item.children.append(
BrowseMedia(
@@ -677,7 +684,7 @@ def item_payload(item):
media_type = item["type"]
media_id = item["uri"]
except KeyError as err:
- _LOGGER.debug("Missing type or uri for media item: %s", item)
+ _LOGGER.debug("Missing type or URI for media item: %s", item)
raise MissingMediaInformation from err
try:
diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json
index 74df79c4d78fcb..f775e5df85d4ce 100644
--- a/homeassistant/components/spotify/strings.json
+++ b/homeassistant/components/spotify/strings.json
@@ -10,7 +10,7 @@
}
},
"abort": {
- "authorize_url_timeout": "Timeout generating authorize url.",
+ "authorize_url_timeout": "Timeout generating authorize URL.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"missing_configuration": "The Spotify integration is not configured. Please follow the documentation.",
"reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication."
diff --git a/homeassistant/components/spotify/translations/cs.json b/homeassistant/components/spotify/translations/cs.json
index f8f122e63e2c18..69cd1b1623ada0 100644
--- a/homeassistant/components/spotify/translations/cs.json
+++ b/homeassistant/components/spotify/translations/cs.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el",
+ "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el.",
"missing_configuration": "Integrace Spotify nen\u00ed nastavena. Postupujte podle dokumentace.",
"no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})"
},
diff --git a/homeassistant/components/spotify/translations/de.json b/homeassistant/components/spotify/translations/de.json
index bfd393bbbb8600..281803ec66ed96 100644
--- a/homeassistant/components/spotify/translations/de.json
+++ b/homeassistant/components/spotify/translations/de.json
@@ -2,14 +2,18 @@
"config": {
"abort": {
"authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.",
- "missing_configuration": "Die Spotify-Integration ist nicht konfiguriert. Bitte folgen Sie der Dokumentation."
+ "missing_configuration": "Die Spotify-Integration ist nicht konfiguriert. Bitte folgen Sie der Dokumentation.",
+ "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})."
},
"create_entry": {
"default": "Erfolgreich mit Spotify authentifiziert."
},
"step": {
"pick_implementation": {
- "title": "Authentifizierungsmethode ausw\u00e4hlen"
+ "title": "W\u00e4hle die Authentifizierungsmethode"
+ },
+ "reauth_confirm": {
+ "title": "Integration erneut authentifizieren"
}
}
},
diff --git a/homeassistant/components/spotify/translations/en.json b/homeassistant/components/spotify/translations/en.json
index 73ea219105bd75..7136e5a8e710ba 100644
--- a/homeassistant/components/spotify/translations/en.json
+++ b/homeassistant/components/spotify/translations/en.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "authorize_url_timeout": "Timeout generating authorize url.",
+ "authorize_url_timeout": "Timeout generating authorize URL.",
"missing_configuration": "The Spotify integration is not configured. Please follow the documentation.",
"no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})",
"reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication."
diff --git a/homeassistant/components/spotify/translations/et.json b/homeassistant/components/spotify/translations/et.json
index 01583d1b0f0d37..c5cee44acca879 100644
--- a/homeassistant/components/spotify/translations/et.json
+++ b/homeassistant/components/spotify/translations/et.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"authorize_url_timeout": "Kinnituse URLi ajal\u00f5pp",
- "missing_configuration": "Spotify sidumine pole h\u00e4\u00e4lestatud. Palun j\u00e4rgige dokumentatsiooni.",
+ "missing_configuration": "Spotify sidumine pole h\u00e4\u00e4lestatud. Palun j\u00e4rgi dokumentatsiooni.",
"no_url_available": "URL pole saadaval. Rohkem teavet [check the help section]({docs_url})",
"reauth_account_mismatch": "Spotify konto mida autenditi ei vasta kontole mis vajas uuesti autentimist."
},
diff --git a/homeassistant/components/spotify/translations/fr.json b/homeassistant/components/spotify/translations/fr.json
index f4f6566e88dfea..d6b5838feb5e8e 100644
--- a/homeassistant/components/spotify/translations/fr.json
+++ b/homeassistant/components/spotify/translations/fr.json
@@ -18,5 +18,10 @@
"title": "R\u00e9-authentifier avec Spotify"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "Point de terminaison de l'API Spotify accessible"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/hu.json b/homeassistant/components/spotify/translations/hu.json
index fb0dc0f8a1f826..060aeffe8bdfb0 100644
--- a/homeassistant/components/spotify/translations/hu.json
+++ b/homeassistant/components/spotify/translations/hu.json
@@ -2,15 +2,24 @@
"config": {
"abort": {
"authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.",
- "missing_configuration": "A Spotify integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t."
+ "missing_configuration": "A Spotify integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.",
+ "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz."
},
"create_entry": {
"default": "A Spotify sikeresen hiteles\u00edtett."
},
"step": {
"pick_implementation": {
- "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert"
+ "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert"
+ },
+ "reauth_confirm": {
+ "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "A Spotify API v\u00e9gpont el\u00e9rhet\u0151"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/id.json b/homeassistant/components/spotify/translations/id.json
new file mode 100644
index 00000000000000..f75f4159a96e05
--- /dev/null
+++ b/homeassistant/components/spotify/translations/id.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.",
+ "missing_configuration": "Integrasi Spotify tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.",
+ "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})",
+ "reauth_account_mismatch": "Akun Spotify yang digunakan untuk autentikasi tidak cocok dengan akun yang memerlukan autentikasi ulang."
+ },
+ "create_entry": {
+ "default": "Berhasil mengautentikasi dengan Spotify."
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Pilih Metode Autentikasi"
+ },
+ "reauth_confirm": {
+ "description": "Integrasi Spotify perlu diautentikasi ulang ke Spotify untuk akun: {account}",
+ "title": "Autentikasi Ulang Integrasi"
+ }
+ }
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "Titik akhir API Spotify dapat dijangkau"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/it.json b/homeassistant/components/spotify/translations/it.json
index 6911d38be0020b..28d821c81f144c 100644
--- a/homeassistant/components/spotify/translations/it.json
+++ b/homeassistant/components/spotify/translations/it.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione",
+ "authorize_url_timeout": "Tempo scaduto nella generazione dell'URL di autorizzazione.",
"missing_configuration": "L'integrazione di Spotify non \u00e8 configurata. Si prega di seguire la documentazione.",
"no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})",
"reauth_account_mismatch": "L'account Spotify con cui si \u00e8 autenticati non corrisponde all'account necessario per la ri-autenticazione."
@@ -15,7 +15,7 @@
},
"reauth_confirm": {
"description": "L'integrazione di Spotify deve essere nuovamente autenticata con Spotify per l'account: {account}",
- "title": "Reautenticare l'integrazione"
+ "title": "Autenticare nuovamente l'integrazione"
}
}
},
diff --git a/homeassistant/components/spotify/translations/ko.json b/homeassistant/components/spotify/translations/ko.json
index a12162bb4fb629..926231a1de3171 100644
--- a/homeassistant/components/spotify/translations/ko.json
+++ b/homeassistant/components/spotify/translations/ko.json
@@ -1,22 +1,27 @@
{
"config": {
"abort": {
- "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"missing_configuration": "Spotify \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
- "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})",
- "reauth_account_mismatch": "\uc778\uc99d\ub41c Spotify \uacc4\uc815\uc740 \uc7ac\uc778\uc99d\uc774 \ud544\uc694\ud55c \uacc4\uc815\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4."
+ "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
+ "reauth_account_mismatch": "\uc778\uc99d \ubc1b\uc740 Spotify \uacc4\uc815\uc774 \uc7ac\uc778\uc99d\ud574\uc57c \ud558\ub294 \uacc4\uc815\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4."
},
"create_entry": {
- "default": "Spotify \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "default": "Spotify\ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
},
"step": {
"pick_implementation": {
"title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30"
},
"reauth_confirm": {
- "description": "Spotify \ud1b5\ud569\uc740 \uacc4\uc815 {account} \ub300\ud574 Spotify\ub85c \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c\ud569\ub2c8\ub2e4.",
- "title": "Spotify\ub85c \uc7ac \uc778\uc99d"
+ "description": "Spotify \uad6c\uc131\uc694\uc18c\ub294 {account} \uacc4\uc815\uc5d0 \ub300\ud574 Spotify\ub97c \uc0ac\uc6a9\ud558\uc5ec \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4.",
+ "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "Spotify API \uc5d4\ub4dc \ud3ec\uc778\ud2b8 \uc5f0\uacb0"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/lb.json b/homeassistant/components/spotify/translations/lb.json
index d7b5dcec0be4d6..92e323d6c4d7c4 100644
--- a/homeassistant/components/spotify/translations/lb.json
+++ b/homeassistant/components/spotify/translations/lb.json
@@ -17,5 +17,10 @@
"title": "Integratioun re-authentifiz\u00e9ieren"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "Spotify API Endpunkt ereechbar"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/nl.json b/homeassistant/components/spotify/translations/nl.json
index bdc86919f7466b..0d1e63fde5d070 100644
--- a/homeassistant/components/spotify/translations/nl.json
+++ b/homeassistant/components/spotify/translations/nl.json
@@ -11,12 +11,17 @@
},
"step": {
"pick_implementation": {
- "title": "Kies Authenticatiemethode"
+ "title": "Kies een authenticatie methode"
},
"reauth_confirm": {
"description": "De Spotify integratie moet opnieuw worden geverifieerd met Spotify voor account: {account}",
- "title": "Verifieer opnieuw met Spotify"
+ "title": "Verifieer de integratie opnieuw"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "Spotify API-eindpunt is bereikbaar"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/no.json b/homeassistant/components/spotify/translations/no.json
index 8e2ec3d36c0f95..54e3ca1f8b4b79 100644
--- a/homeassistant/components/spotify/translations/no.json
+++ b/homeassistant/components/spotify/translations/no.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse",
+ "authorize_url_timeout": "Tidsavbrudd genererer godkjennelses-URL.",
"missing_configuration": "Spotify-integrasjonen er ikke konfigurert. F\u00f8lg dokumentasjonen.",
"no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})",
"reauth_account_mismatch": "Spotify-kontoen som er godkjent samsvarer ikke med kontoen som trenger godkjenning p\u00e5 nytt"
diff --git a/homeassistant/components/spotify/translations/pl.json b/homeassistant/components/spotify/translations/pl.json
index f9e6f42921458b..52028d4d368522 100644
--- a/homeassistant/components/spotify/translations/pl.json
+++ b/homeassistant/components/spotify/translations/pl.json
@@ -21,7 +21,7 @@
},
"system_health": {
"info": {
- "api_endpoint_reachable": "Dost\u0119pno\u015b\u0107 punktu ko\u0144cowego API Spotify"
+ "api_endpoint_reachable": "Punkt ko\u0144cowy Spotify API osi\u0105galny"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/ru.json b/homeassistant/components/spotify/translations/ru.json
index 722cb125169224..bac888937a5b96 100644
--- a/homeassistant/components/spotify/translations/ru.json
+++ b/homeassistant/components/spotify/translations/ru.json
@@ -15,7 +15,7 @@
},
"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 \u0432 Spotify \u0434\u043b\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438: {account}",
- "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f"
+ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f"
}
}
},
diff --git a/homeassistant/components/spotify/translations/uk.json b/homeassistant/components/spotify/translations/uk.json
new file mode 100644
index 00000000000000..fda84b310a5c24
--- /dev/null
+++ b/homeassistant/components/spotify/translations/uk.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "missing_configuration": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f Spotify \u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044c \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0454\u044e.",
+ "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.",
+ "reauth_account_mismatch": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u043e\u0432\u0430\u043d\u0438\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u043d\u0435 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0454 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u0443, \u0449\u043e \u0432\u0438\u043c\u0430\u0433\u0430\u0454 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457."
+ },
+ "create_entry": {
+ "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e."
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ },
+ "reauth_confirm": {
+ "description": "\u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u0432 Spotify \u0434\u043b\u044f \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443: {account}",
+ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e"
+ }
+ }
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e API Spotify"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json
index 3b21d32b11078f..7418eb095da785 100644
--- a/homeassistant/components/sql/manifest.json
+++ b/homeassistant/components/sql/manifest.json
@@ -2,6 +2,6 @@
"domain": "sql",
"name": "SQL",
"documentation": "https://www.home-assistant.io/integrations/sql",
- "requirements": ["sqlalchemy==1.3.22"],
+ "requirements": ["sqlalchemy==1.3.23"],
"codeowners": ["@dgomes"]
}
diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py
index 670f5e66146bd3..b90ce2f8e594b8 100644
--- a/homeassistant/components/sql/sensor.py
+++ b/homeassistant/components/sql/sensor.py
@@ -2,16 +2,16 @@
import datetime
import decimal
import logging
+import re
import sqlalchemy
from sqlalchemy.orm import scoped_session, sessionmaker
import voluptuous as vol
from homeassistant.components.recorder import CONF_DB_URL, DEFAULT_DB_FILE, DEFAULT_URL
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -19,6 +19,13 @@
CONF_QUERIES = "queries"
CONF_QUERY = "query"
+DB_URL_RE = re.compile("//.*:.*@")
+
+
+def redact_credentials(data):
+ """Redact credentials from string data."""
+ return DB_URL_RE.sub("//****:****@", data)
+
def validate_sql_select(value):
"""Validate that value is a SQL SELECT query."""
@@ -48,6 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if not db_url:
db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE))
+ sess = None
try:
engine = sqlalchemy.create_engine(db_url)
sessmaker = scoped_session(sessionmaker(bind=engine))
@@ -57,10 +65,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
sess.execute("SELECT 1;")
except sqlalchemy.exc.SQLAlchemyError as err:
- _LOGGER.error("Couldn't connect using %s DB_URL: %s", db_url, err)
+ _LOGGER.error(
+ "Couldn't connect using %s DB_URL: %s",
+ redact_credentials(db_url),
+ redact_credentials(str(err)),
+ )
return
finally:
- sess.close()
+ if sess:
+ sess.close()
queries = []
@@ -90,7 +103,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(queries, True)
-class SQLSensor(Entity):
+class SQLSensor(SensorEntity):
"""Representation of an SQL sensor."""
def __init__(self, name, sessmaker, query, column, unit, value_template):
@@ -120,7 +133,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attributes
@@ -148,7 +161,11 @@ def update(self):
value = str(value)
self._attributes[key] = value
except sqlalchemy.exc.SQLAlchemyError as err:
- _LOGGER.error("Error executing query %s: %s", self._query, err)
+ _LOGGER.error(
+ "Error executing query %s: %s",
+ self._query,
+ redact_credentials(str(err)),
+ )
return
finally:
sess.close()
diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py
index e298bee7b07839..f276daac56ac10 100644
--- a/homeassistant/components/squeezebox/__init__.py
+++ b/homeassistant/components/squeezebox/__init__.py
@@ -11,11 +11,6 @@
_LOGGER = logging.getLogger(__name__)
-async def async_setup(hass: HomeAssistant, config: dict):
- """Set up the Logitech Squeezebox component."""
- return True
-
-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Logitech Squeezebox from a config entry."""
hass.async_create_task(
diff --git a/homeassistant/components/squeezebox/config_flow.py b/homeassistant/components/squeezebox/config_flow.py
index f5ed6073104a56..adfa5895b7d850 100644
--- a/homeassistant/components/squeezebox/config_flow.py
+++ b/homeassistant/components/squeezebox/config_flow.py
@@ -15,7 +15,6 @@
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-# pylint: disable=unused-import
from .const import DEFAULT_PORT, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -80,7 +79,7 @@ def _discovery_callback(server):
return
self.discovery_info = {
CONF_HOST: server.host,
- CONF_PORT: server.port,
+ CONF_PORT: int(server.port),
"uuid": server.uuid,
}
_LOGGER.debug("Discovered server: %s", self.discovery_info)
@@ -182,7 +181,6 @@ async def async_step_discovery(self, discovery_info):
# update schema with suggested values from discovery
self.data_schema = _base_schema(discovery_info)
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update({"title_placeholders": {"host": discovery_info[CONF_HOST]}})
return await self.async_step_edit()
diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py
index b87695dd159b50..c57f95266ff5b9 100644
--- a/homeassistant/components/squeezebox/media_player.py
+++ b/homeassistant/components/squeezebox/media_player.py
@@ -265,7 +265,7 @@ def __init__(self, player):
self._remove_dispatcher = None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device-specific attributes."""
squeezebox_attr = {
attr: getattr(self, attr)
diff --git a/homeassistant/components/squeezebox/translations/de.json b/homeassistant/components/squeezebox/translations/de.json
index 667bf6dbd128c8..c64e1ae3a1cdd4 100644
--- a/homeassistant/components/squeezebox/translations/de.json
+++ b/homeassistant/components/squeezebox/translations/de.json
@@ -1,15 +1,25 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "no_server_found": "Kein LMS-Server gefunden."
+ },
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "no_server_found": "Konnte den Server nicht automatisch entdecken.",
+ "unknown": "Unerwarteter Fehler"
},
+ "flow_title": "Logitech Squeezebox",
"step": {
"edit": {
"data": {
+ "host": "Host",
"password": "Passwort",
"port": "Port",
"username": "Benutzername"
- }
+ },
+ "title": "Verbindungsinformationen bearbeiten"
},
"user": {
"data": {
diff --git a/homeassistant/components/squeezebox/translations/hu.json b/homeassistant/components/squeezebox/translations/hu.json
index 3b2d79a34a77e2..216badd15c68ac 100644
--- a/homeassistant/components/squeezebox/translations/hu.json
+++ b/homeassistant/components/squeezebox/translations/hu.json
@@ -1,7 +1,30 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "no_server_found": "Nem tal\u00e1lhat\u00f3 LMS szerver."
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "no_server_found": "Nem siker\u00fclt automatikusan felfedezni a szervert.",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "flow_title": "Logitech Squeezebox: {host}",
+ "step": {
+ "edit": {
+ "data": {
+ "host": "Hoszt",
+ "password": "Jelsz\u00f3",
+ "port": "Port",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Hoszt"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/squeezebox/translations/id.json b/homeassistant/components/squeezebox/translations/id.json
new file mode 100644
index 00000000000000..764c356ba8475c
--- /dev/null
+++ b/homeassistant/components/squeezebox/translations/id.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "no_server_found": "Tidak ada server LMS yang ditemukan."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "no_server_found": "Tidak dapat menemukan server secara otomatis.",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "flow_title": "Logitech Squeezebox: {host}",
+ "step": {
+ "edit": {
+ "data": {
+ "host": "Host",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "username": "Nama Pengguna"
+ },
+ "title": "Edit informasi koneksi"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/squeezebox/translations/nl.json b/homeassistant/components/squeezebox/translations/nl.json
index d023a3e96d19d3..f60fc640db2b8f 100644
--- a/homeassistant/components/squeezebox/translations/nl.json
+++ b/homeassistant/components/squeezebox/translations/nl.json
@@ -1,11 +1,13 @@
{
"config": {
"abort": {
- "already_configured": "Apparaat is al geconfigureerd"
+ "already_configured": "Apparaat is al geconfigureerd",
+ "no_server_found": "Geen LMS server gevonden."
},
"error": {
"cannot_connect": "Kon niet verbinden",
"invalid_auth": "Ongeldige authenticatie",
+ "no_server_found": "Kan server niet automatisch vinden.",
"unknown": "Onverwachte fout"
},
"flow_title": "Logitech Squeezebox: {host}",
diff --git a/homeassistant/components/squeezebox/translations/ru.json b/homeassistant/components/squeezebox/translations/ru.json
index 789fff313d884c..3b144adc04eb8b 100644
--- a/homeassistant/components/squeezebox/translations/ru.json
+++ b/homeassistant/components/squeezebox/translations/ru.json
@@ -6,7 +6,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"no_server_found": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
@@ -17,7 +17,7 @@
"host": "\u0425\u043e\u0441\u0442",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438"
},
diff --git a/homeassistant/components/squeezebox/translations/tr.json b/homeassistant/components/squeezebox/translations/tr.json
new file mode 100644
index 00000000000000..ff249aafa14853
--- /dev/null
+++ b/homeassistant/components/squeezebox/translations/tr.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "edit": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "password": "Parola",
+ "port": "Port",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/squeezebox/translations/uk.json b/homeassistant/components/squeezebox/translations/uk.json
new file mode 100644
index 00000000000000..50cd135f6f3d41
--- /dev/null
+++ b/homeassistant/components/squeezebox/translations/uk.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "no_server_found": "\u0421\u0435\u0440\u0432\u0435\u0440 LMS \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "no_server_found": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432\u0438\u044f\u0432\u0438\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "flow_title": "Logitech Squeezebox: {host}",
+ "step": {
+ "edit": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "\u0406\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044f \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/config_flow.py b/homeassistant/components/srp_energy/config_flow.py
index b65b93e0108d8c..51b6f80bb3372e 100644
--- a/homeassistant/components/srp_energy/config_flow.py
+++ b/homeassistant/components/srp_energy/config_flow.py
@@ -7,11 +7,7 @@
from homeassistant import config_entries
from homeassistant.const import CONF_ID, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
-from .const import ( # pylint:disable=unused-import
- CONF_IS_TOU,
- DEFAULT_NAME,
- SRP_ENERGY_DOMAIN,
-)
+from .const import CONF_IS_TOU, DEFAULT_NAME, SRP_ENERGY_DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py
index 36a8798b05bfc4..6973c58600e18e 100644
--- a/homeassistant/components/srp_energy/sensor.py
+++ b/homeassistant/components/srp_energy/sensor.py
@@ -5,8 +5,8 @@
import async_timeout
from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, ENERGY_KILO_WATT_HOUR
-from homeassistant.helpers import entity
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
@@ -71,7 +71,7 @@ async def async_update_data():
async_add_entities([SrpEntity(coordinator)])
-class SrpEntity(entity.Entity):
+class SrpEntity(SensorEntity):
"""Implementation of a Srp Energy Usage sensor."""
def __init__(self, coordinator):
@@ -122,7 +122,7 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if not self.coordinator.data:
return None
diff --git a/homeassistant/components/srp_energy/translations/de.json b/homeassistant/components/srp_energy/translations/de.json
index 23fe89c73b4d1f..302233d29234b8 100644
--- a/homeassistant/components/srp_energy/translations/de.json
+++ b/homeassistant/components/srp_energy/translations/de.json
@@ -1,14 +1,21 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen"
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Anmeldung",
+ "unknown": "Unerwarteter Fehler"
},
"step": {
"user": {
"data": {
- "password": "Passwort"
+ "password": "Passwort",
+ "username": "Benutzername"
}
}
}
- }
+ },
+ "title": "SRP Energy"
}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/es.json b/homeassistant/components/srp_energy/translations/es.json
index de15bb805514dd..849c5019d3b5f5 100644
--- a/homeassistant/components/srp_energy/translations/es.json
+++ b/homeassistant/components/srp_energy/translations/es.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n."
+ "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n."
},
"error": {
"cannot_connect": "No se pudo conectar",
"invalid_account": "El ID de la cuenta debe ser un n\u00famero de 9 d\u00edgitos",
- "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida",
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"unknown": "Error inesperado"
},
"step": {
@@ -15,7 +15,7 @@
"id": "ID de la cuenta",
"is_tou": "Es el plan de tiempo de uso",
"password": "Contrase\u00f1a",
- "username": "Nombre de usuario"
+ "username": "Usuario"
}
}
}
diff --git a/homeassistant/components/srp_energy/translations/fr.json b/homeassistant/components/srp_energy/translations/fr.json
new file mode 100644
index 00000000000000..b9b33cfa9306ab
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/fr.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "D\u00e9ja configur\u00e9. Seulement une seule configuration est possible "
+ },
+ "error": {
+ "cannot_connect": "\u00c9chec de la connexion ",
+ "invalid_account": "L'ID de compte doit \u00eatre un num\u00e9ro \u00e0 9 chiffres",
+ "invalid_auth": "Authentification invalide ",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Identifiant de compte",
+ "is_tou": "Est le plan de temps d'utilisation",
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur "
+ }
+ }
+ }
+ },
+ "title": "\u00c9nergie SRP"
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/hu.json b/homeassistant/components/srp_energy/translations/hu.json
index f46e17923ad735..0c3bdf29389f37 100644
--- a/homeassistant/components/srp_energy/translations/hu.json
+++ b/homeassistant/components/srp_energy/translations/hu.json
@@ -1,11 +1,22 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
"step": {
"user": {
"data": {
- "id": "A fi\u00f3k azonos\u00edt\u00f3ja"
+ "id": "A fi\u00f3k azonos\u00edt\u00f3ja",
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
}
}
}
- }
+ },
+ "title": "SRP Energy"
}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/id.json b/homeassistant/components/srp_energy/translations/id.json
new file mode 100644
index 00000000000000..fefcbff2ecb6b5
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/id.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_account": "ID akun harus terdiri dari 9 angka",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "ID akun",
+ "is_tou": "Dalam Paket Time of Use",
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ }
+ }
+ }
+ },
+ "title": "SRP Energy"
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/ko.json b/homeassistant/components/srp_energy/translations/ko.json
new file mode 100644
index 00000000000000..b329cf1f2b1536
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/ko.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_account": "\uacc4\uc815 ID\ub294 9\uc790\ub9ac \uc22b\uc790\uc5ec\uc57c \ud569\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "\uacc4\uc815 ID",
+ "is_tou": "\uacc4\uc2dc\ubcc4 \uc694\uae08\uc81c",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ }
+ }
+ }
+ },
+ "title": "SRP Energy"
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/lb.json b/homeassistant/components/srp_energy/translations/lb.json
new file mode 100644
index 00000000000000..1affdcc31e6f18
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/lb.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech."
+ },
+ "error": {
+ "cannot_connect": "Feeler beim verbannen",
+ "invalid_auth": "Ong\u00eblteg Authentifikatioun",
+ "unknown": "Onerwaarte Feeler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwuert",
+ "username": "Benotzernumm"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/nl.json b/homeassistant/components/srp_energy/translations/nl.json
new file mode 100644
index 00000000000000..91bdc3592b673b
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/nl.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_account": "Account-ID moet een 9-cijferig nummer zijn",
+ "invalid_auth": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Account ID",
+ "is_tou": "Is tijd van gebruik plan",
+ "password": "Wachtwoord",
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ },
+ "title": "SRP Energy"
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/ru.json b/homeassistant/components/srp_energy/translations/ru.json
index 3fcbace37dfe90..a492fa7dfb233e 100644
--- a/homeassistant/components/srp_energy/translations/ru.json
+++ b/homeassistant/components/srp_energy/translations/ru.json
@@ -6,7 +6,7 @@
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"invalid_account": "ID \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c 9-\u0437\u043d\u0430\u0447\u043d\u044b\u043c \u0447\u0438\u0441\u043b\u043e\u043c.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
@@ -15,7 +15,7 @@
"id": "ID \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430",
"is_tou": "\u041f\u043b\u0430\u043d \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
}
}
}
diff --git a/homeassistant/components/srp_energy/translations/tr.json b/homeassistant/components/srp_energy/translations/tr.json
index 1b08426f631ac4..ead8238d82c116 100644
--- a/homeassistant/components/srp_energy/translations/tr.json
+++ b/homeassistant/components/srp_energy/translations/tr.json
@@ -1,7 +1,13 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
"error": {
- "invalid_account": "Hesap kimli\u011fi 9 haneli bir say\u0131 olmal\u0131d\u0131r"
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_account": "Hesap kimli\u011fi 9 haneli bir say\u0131 olmal\u0131d\u0131r",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
},
"step": {
"user": {
diff --git a/homeassistant/components/srp_energy/translations/uk.json b/homeassistant/components/srp_energy/translations/uk.json
new file mode 100644
index 00000000000000..5267aa2a5757f1
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/uk.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_account": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 \u043c\u0430\u0454 \u0431\u0443\u0442\u0438 9-\u0437\u043d\u0430\u0447\u043d\u0438\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c.",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443",
+ "is_tou": "\u041f\u043b\u0430\u043d \u0447\u0430\u0441\u0443 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ }
+ }
+ }
+ },
+ "title": "SRP Energy"
+}
\ No newline at end of file
diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py
index e962c141bef146..8cad4a74bf89b8 100644
--- a/homeassistant/components/ssdp/__init__.py
+++ b/homeassistant/components/ssdp/__init__.py
@@ -1,14 +1,16 @@
"""The SSDP integration."""
import asyncio
from datetime import timedelta
-import itertools
import logging
+from typing import Any, Mapping
import aiohttp
+from async_upnp_client.search import async_search
from defusedxml import ElementTree
from netdisco import ssdp, util
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
+from homeassistant.core import callback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.loader import async_get_ssdp
@@ -51,12 +53,6 @@ async def initialize(_):
return True
-def _run_ssdp_scans():
- _LOGGER.debug("Scanning")
- # Run 3 times as packets can get lost
- return itertools.chain.from_iterable([ssdp.scan() for _ in range(3)])
-
-
class Scanner:
"""Class to manage SSDP scanning."""
@@ -64,25 +60,38 @@ def __init__(self, hass, integration_matchers):
"""Initialize class."""
self.hass = hass
self.seen = set()
+ self._entries = []
self._integration_matchers = integration_matchers
self._description_cache = {}
+ async def _on_ssdp_response(self, data: Mapping[str, Any]) -> None:
+ """Process an ssdp response."""
+ self.async_store_entry(
+ ssdp.UPNPEntry({key.lower(): item for key, item in data.items()})
+ )
+
+ @callback
+ def async_store_entry(self, entry):
+ """Save an entry for later processing."""
+ self._entries.append(entry)
+
async def async_scan(self, _):
"""Scan for new entries."""
- entries = await self.hass.async_add_executor_job(_run_ssdp_scans)
- await self._process_entries(entries)
+ await async_search(async_callback=self._on_ssdp_response)
+ await self._process_entries()
# We clear the cache after each run. We track discovered entries
# so will never need a description twice.
self._description_cache.clear()
+ self._entries.clear()
- async def _process_entries(self, entries):
+ async def _process_entries(self):
"""Process SSDP entries."""
entries_to_process = []
unseen_locations = set()
- for entry in entries:
+ for entry in self._entries:
key = (entry.st, entry.location)
if key in self.seen:
@@ -171,13 +180,13 @@ async def _fetch_description(self, xml_location):
session = self.hass.helpers.aiohttp_client.async_get_clientsession()
try:
resp = await session.get(xml_location, timeout=5)
- xml = await resp.text()
+ xml = await resp.text(errors="replace")
# Samsung Smart TV sometimes returns an empty document the
# first time. Retry once.
if not xml:
resp = await session.get(xml_location, timeout=5)
- xml = await resp.text()
+ xml = await resp.text(errors="replace")
except (aiohttp.ClientError, asyncio.TimeoutError) as err:
_LOGGER.debug("Error fetching %s: %s", xml_location, err)
return {}
diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json
index ed20ae9ead6c9f..938ad979daf7b2 100644
--- a/homeassistant/components/ssdp/manifest.json
+++ b/homeassistant/components/ssdp/manifest.json
@@ -2,7 +2,7 @@
"domain": "ssdp",
"name": "Simple Service Discovery Protocol (SSDP)",
"documentation": "https://www.home-assistant.io/integrations/ssdp",
- "requirements": ["defusedxml==0.6.0", "netdisco==2.8.2"],
+ "requirements": ["defusedxml==0.6.0", "netdisco==2.8.2", "async-upnp-client==0.16.0"],
"after_dependencies": ["zeroconf"],
"codeowners": []
}
diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py
index 392dbff9e03db6..2eb729721d3fca 100644
--- a/homeassistant/components/starline/__init__.py
+++ b/homeassistant/components/starline/__init__.py
@@ -2,12 +2,12 @@
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import Config, HomeAssistant
+from homeassistant.const import CONF_SCAN_INTERVAL
+from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .account import StarlineAccount
from .const import (
- CONF_SCAN_INTERVAL,
CONF_SCAN_OBD_INTERVAL,
DEFAULT_SCAN_INTERVAL,
DEFAULT_SCAN_OBD_INTERVAL,
@@ -19,11 +19,6 @@
)
-async def async_setup(hass: HomeAssistant, config: Config) -> bool:
- """Set up configured StarLine."""
- return True
-
-
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up the StarLine device from a config entry."""
account = StarlineAccount(hass, config_entry)
diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py
index 7452253019b23a..3f82b816cd517a 100644
--- a/homeassistant/components/starline/account.py
+++ b/homeassistant/components/starline/account.py
@@ -1,6 +1,8 @@
"""StarLine Account."""
+from __future__ import annotations
+
from datetime import datetime, timedelta
-from typing import Any, Callable, Dict, Optional
+from typing import Any, Callable
from starline import StarlineApi, StarlineDevice
@@ -29,8 +31,8 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry):
self._config_entry: ConfigEntry = config_entry
self._update_interval: int = DEFAULT_SCAN_INTERVAL
self._update_obd_interval: int = DEFAULT_SCAN_OBD_INTERVAL
- self._unsubscribe_auto_updater: Optional[Callable] = None
- self._unsubscribe_auto_obd_updater: Optional[Callable] = None
+ self._unsubscribe_auto_updater: Callable | None = None
+ self._unsubscribe_auto_obd_updater: Callable | None = None
self._api: StarlineApi = StarlineApi(
config_entry.data[DATA_USER_ID], config_entry.data[DATA_SLNET_TOKEN]
)
@@ -114,7 +116,7 @@ def set_update_obd_interval(self, interval: int) -> None:
def unload(self):
"""Unload StarLine API."""
- _LOGGER.debug("Unloading StarLine API.")
+ _LOGGER.debug("Unloading StarLine API")
if self._unsubscribe_auto_updater is not None:
self._unsubscribe_auto_updater()
self._unsubscribe_auto_updater = None
@@ -123,7 +125,7 @@ def unload(self):
self._unsubscribe_auto_obd_updater = None
@staticmethod
- def device_info(device: StarlineDevice) -> Dict[str, Any]:
+ def device_info(device: StarlineDevice) -> dict[str, Any]:
"""Device information for entities."""
return {
"identifiers": {(DOMAIN, device.device_id)},
@@ -134,7 +136,7 @@ def device_info(device: StarlineDevice) -> Dict[str, Any]:
}
@staticmethod
- def gps_attrs(device: StarlineDevice) -> Dict[str, Any]:
+ def gps_attrs(device: StarlineDevice) -> dict[str, Any]:
"""Attributes for device tracker."""
return {
"updated": datetime.utcfromtimestamp(device.position["ts"]).isoformat(),
@@ -142,7 +144,7 @@ def gps_attrs(device: StarlineDevice) -> Dict[str, Any]:
}
@staticmethod
- def balance_attrs(device: StarlineDevice) -> Dict[str, Any]:
+ def balance_attrs(device: StarlineDevice) -> dict[str, Any]:
"""Attributes for balance sensor."""
return {
"operator": device.balance.get("operator"),
@@ -151,7 +153,7 @@ def balance_attrs(device: StarlineDevice) -> Dict[str, Any]:
}
@staticmethod
- def gsm_attrs(device: StarlineDevice) -> Dict[str, Any]:
+ def gsm_attrs(device: StarlineDevice) -> dict[str, Any]:
"""Attributes for GSM sensor."""
return {
"raw": device.gsm_level,
@@ -161,7 +163,7 @@ def gsm_attrs(device: StarlineDevice) -> Dict[str, Any]:
}
@staticmethod
- def engine_attrs(device: StarlineDevice) -> Dict[str, Any]:
+ def engine_attrs(device: StarlineDevice) -> dict[str, Any]:
"""Attributes for engine switch."""
return {
"autostart": device.car_state.get("r_start"),
@@ -169,6 +171,6 @@ def engine_attrs(device: StarlineDevice) -> Dict[str, Any]:
}
@staticmethod
- def errors_attrs(device: StarlineDevice) -> Dict[str, Any]:
+ def errors_attrs(device: StarlineDevice) -> dict[str, Any]:
"""Attributes for errors sensor."""
return {"errors": device.errors.get("errors")}
diff --git a/homeassistant/components/starline/config_flow.py b/homeassistant/components/starline/config_flow.py
index d6e8d6f98ead9f..9f8e03392103d6 100644
--- a/homeassistant/components/starline/config_flow.py
+++ b/homeassistant/components/starline/config_flow.py
@@ -1,5 +1,5 @@
"""Config flow to configure StarLine component."""
-from typing import Optional
+from __future__ import annotations
from starline import StarlineAuth
import voluptuous as vol
@@ -7,7 +7,7 @@
from homeassistant import config_entries, core
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from .const import ( # pylint: disable=unused-import
+from .const import (
_LOGGER,
CONF_APP_ID,
CONF_APP_SECRET,
@@ -32,11 +32,11 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self):
"""Initialize flow."""
- self._app_id: Optional[str] = None
- self._app_secret: Optional[str] = None
- self._username: Optional[str] = None
- self._password: Optional[str] = None
- self._mfa_code: Optional[str] = None
+ self._app_id: str | None = None
+ self._app_secret: str | None = None
+ self._username: str | None = None
+ self._password: str | None = None
+ self._mfa_code: str | None = None
self._app_code = None
self._app_token = None
diff --git a/homeassistant/components/starline/const.py b/homeassistant/components/starline/const.py
index 89ea0873aa1a6d..488a5cb9e0f447 100644
--- a/homeassistant/components/starline/const.py
+++ b/homeassistant/components/starline/const.py
@@ -11,7 +11,6 @@
CONF_MFA_CODE = "mfa_code"
CONF_CAPTCHA_CODE = "captcha_code"
-CONF_SCAN_INTERVAL = "scan_interval"
DEFAULT_SCAN_INTERVAL = 180 # in seconds
CONF_SCAN_OBD_INTERVAL = "scan_obd_interval"
DEFAULT_SCAN_OBD_INTERVAL = 10800 # 3 hours in seconds
diff --git a/homeassistant/components/starline/device_tracker.py b/homeassistant/components/starline/device_tracker.py
index 6f202bbae52c47..59b9f5b4f95d39 100644
--- a/homeassistant/components/starline/device_tracker.py
+++ b/homeassistant/components/starline/device_tracker.py
@@ -26,7 +26,7 @@ def __init__(self, account: StarlineAccount, device: StarlineDevice):
super().__init__(account, device, "location", "Location")
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific attributes."""
return self._account.gps_attrs(self._device)
diff --git a/homeassistant/components/starline/entity.py b/homeassistant/components/starline/entity.py
index 5db4d369f5e742..9b81481b9d1c30 100644
--- a/homeassistant/components/starline/entity.py
+++ b/homeassistant/components/starline/entity.py
@@ -1,5 +1,7 @@
"""StarLine base entity."""
-from typing import Callable, Optional
+from __future__ import annotations
+
+from typing import Callable
from homeassistant.helpers.entity import Entity
@@ -17,7 +19,7 @@ def __init__(
self._device = device
self._key = key
self._name = name
- self._unsubscribe_api: Optional[Callable] = None
+ self._unsubscribe_api: Callable | None = None
@property
def should_poll(self):
diff --git a/homeassistant/components/starline/lock.py b/homeassistant/components/starline/lock.py
index 56cd8686186a59..f19fa4896bae4e 100644
--- a/homeassistant/components/starline/lock.py
+++ b/homeassistant/components/starline/lock.py
@@ -8,7 +8,6 @@
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the StarLine lock."""
-
account: StarlineAccount = hass.data[DOMAIN][entry.entry_id]
entities = []
for device in account.api.devices.values():
@@ -32,7 +31,7 @@ def available(self):
return super().available and self._device.online
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the lock.
Possible dictionary keys:
diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py
index 8aba1b54269aed..29deacee428600 100644
--- a/homeassistant/components/starline/sensor.py
+++ b/homeassistant/components/starline/sensor.py
@@ -1,5 +1,5 @@
"""Reads vehicle status from StarLine API."""
-from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE
+from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE, SensorEntity
from homeassistant.const import (
LENGTH_KILOMETERS,
PERCENTAGE,
@@ -7,7 +7,6 @@
VOLT,
VOLUME_LITERS,
)
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level, icon_for_signal_level
from .account import StarlineAccount, StarlineDevice
@@ -38,7 +37,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
async_add_entities(entities)
-class StarlineSensor(StarlineEntity, Entity):
+class StarlineSensor(StarlineEntity, SensorEntity):
"""Representation of a StarLine sensor."""
def __init__(
@@ -109,7 +108,7 @@ def device_class(self):
return self._device_class
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
if self._key == "balance":
return self._account.balance_attrs(self._device)
diff --git a/homeassistant/components/starline/switch.py b/homeassistant/components/starline/switch.py
index c50a7bb4973784..b3214390a448d2 100644
--- a/homeassistant/components/starline/switch.py
+++ b/homeassistant/components/starline/switch.py
@@ -53,7 +53,7 @@ def available(self):
return super().available and self._device.online
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the switch."""
if self._key == "ign":
return self._account.engine_attrs(self._device)
diff --git a/homeassistant/components/starline/translations/he.json b/homeassistant/components/starline/translations/he.json
new file mode 100644
index 00000000000000..535427982322a0
--- /dev/null
+++ b/homeassistant/components/starline/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "auth_user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/starline/translations/hu.json b/homeassistant/components/starline/translations/hu.json
index 9d544eb0337890..71895e80ad5b38 100644
--- a/homeassistant/components/starline/translations/hu.json
+++ b/homeassistant/components/starline/translations/hu.json
@@ -11,7 +11,7 @@
"app_id": "App ID",
"app_secret": "Titok"
},
- "description": "Alkalmaz\u00e1s azonos\u00edt\u00f3ja \u00e9s titkos k\u00f3dja a StarLine fejleszt\u0151i fi\u00f3kb\u00f3l ",
+ "description": "Alkalmaz\u00e1s azonos\u00edt\u00f3ja \u00e9s titkos k\u00f3dja a [StarLine fejleszt\u0151i fi\u00f3kb\u00f3l](https://my.starline.ru/developer)",
"title": "Alkalmaz\u00e1si hiteles\u00edt\u0151 adatok"
},
"auth_captcha": {
diff --git a/homeassistant/components/starline/translations/id.json b/homeassistant/components/starline/translations/id.json
new file mode 100644
index 00000000000000..5a0afdba7ef11d
--- /dev/null
+++ b/homeassistant/components/starline/translations/id.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "error": {
+ "error_auth_app": "ID aplikasi atau kode rahasia salah",
+ "error_auth_mfa": "Kode salah",
+ "error_auth_user": "Nama pengguna atau kata sandi salah"
+ },
+ "step": {
+ "auth_app": {
+ "data": {
+ "app_id": "ID Aplikasi",
+ "app_secret": "Kode Rahasia"
+ },
+ "description": "ID Aplikasi dan kode rahasia dari [akun pengembang StarLine] (https://my.starline.ru/developer)",
+ "title": "Kredensial aplikasi"
+ },
+ "auth_captcha": {
+ "data": {
+ "captcha_code": "Kode dari gambar"
+ },
+ "description": "{captcha_img}",
+ "title": "Captcha"
+ },
+ "auth_mfa": {
+ "data": {
+ "mfa_code": "Kode SMS"
+ },
+ "description": "Masukkan kode yang dikirimkan ke ponsel {phone_number}",
+ "title": "Autentikasi Dua Faktor"
+ },
+ "auth_user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "description": "Email dan kata sandi akun StarLine",
+ "title": "Kredensial pengguna"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/starline/translations/ru.json b/homeassistant/components/starline/translations/ru.json
index ea6833f58423d9..a89fc3b15d5d5c 100644
--- a/homeassistant/components/starline/translations/ru.json
+++ b/homeassistant/components/starline/translations/ru.json
@@ -3,7 +3,7 @@
"error": {
"error_auth_app": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043e\u0434.",
"error_auth_mfa": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434.",
- "error_auth_user": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c."
+ "error_auth_user": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c."
},
"step": {
"auth_app": {
@@ -31,7 +31,7 @@
"auth_user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 StarLine",
"title": "\u0423\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
diff --git a/homeassistant/components/starline/translations/tr.json b/homeassistant/components/starline/translations/tr.json
new file mode 100644
index 00000000000000..9d52f589e98735
--- /dev/null
+++ b/homeassistant/components/starline/translations/tr.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "error": {
+ "error_auth_user": "Yanl\u0131\u015f kullan\u0131c\u0131 ad\u0131 ya da parola"
+ },
+ "step": {
+ "auth_app": {
+ "title": "Uygulama kimlik bilgileri"
+ },
+ "auth_captcha": {
+ "data": {
+ "captcha_code": "G\u00f6r\u00fcnt\u00fcden kod"
+ },
+ "description": "{captcha_img}",
+ "title": "Captcha"
+ },
+ "auth_mfa": {
+ "data": {
+ "mfa_code": "SMS kodu"
+ },
+ "description": "{phone_number} telefona g\u00f6nderilen kodu girin",
+ "title": "\u0130ki fakt\u00f6rl\u00fc yetkilendirme"
+ },
+ "auth_user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ },
+ "description": "StarLine hesab\u0131 e-postas\u0131 ve parolas\u0131"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/starline/translations/uk.json b/homeassistant/components/starline/translations/uk.json
new file mode 100644
index 00000000000000..8a263044284f6a
--- /dev/null
+++ b/homeassistant/components/starline/translations/uk.json
@@ -0,0 +1,41 @@
+{
+ "config": {
+ "error": {
+ "error_auth_app": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0434\u043e\u0434\u0430\u0442\u043a\u0430 \u0430\u0431\u043e \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u0438\u0439 \u043a\u043e\u0434.",
+ "error_auth_mfa": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434.",
+ "error_auth_user": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043b\u043e\u0433\u0456\u043d \u0430\u0431\u043e \u043f\u0430\u0440\u043e\u043b\u044c."
+ },
+ "step": {
+ "auth_app": {
+ "data": {
+ "app_id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0434\u043e\u0434\u0430\u0442\u043a\u0430",
+ "app_secret": "\u0421\u0435\u043a\u0440\u0435\u0442\u043d\u0438\u0439 \u043a\u043e\u0434"
+ },
+ "description": "ID \u0434\u043e\u0434\u0430\u0442\u043a\u0430 \u0456 \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u0438\u0439 \u043a\u043e\u0434 [\u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 \u0440\u043e\u0437\u0440\u043e\u0431\u043d\u0438\u043a\u0430 StarLine] (https://my.starline.ru/developer)",
+ "title": "\u041e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456 \u0434\u043e\u0434\u0430\u0442\u043a\u0430"
+ },
+ "auth_captcha": {
+ "data": {
+ "captcha_code": "\u041a\u043e\u0434 \u0437 \u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f"
+ },
+ "description": "{captcha_img}",
+ "title": "CAPTCHA"
+ },
+ "auth_mfa": {
+ "data": {
+ "mfa_code": "\u041a\u043e\u0434 \u0437 SMS"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434, \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0439 \u043d\u0430 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0443 {phone_number}",
+ "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f"
+ },
+ "auth_user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 StarLine",
+ "title": "\u041e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/starline/translations/zh-Hant.json b/homeassistant/components/starline/translations/zh-Hant.json
index 81a65ac0405a01..722c5daaad4a3a 100644
--- a/homeassistant/components/starline/translations/zh-Hant.json
+++ b/homeassistant/components/starline/translations/zh-Hant.json
@@ -26,7 +26,7 @@
"mfa_code": "\u7c21\u8a0a\u5bc6\u78bc"
},
"description": "\u8f38\u5165\u50b3\u9001\u81f3 {phone_number} \u7684\u9a57\u8b49\u78bc",
- "title": "\u96d9\u91cd\u9a57\u8b49"
+ "title": "\u96d9\u91cd\u8a8d\u8b49"
},
"auth_user": {
"data": {
diff --git a/homeassistant/components/starlingbank/sensor.py b/homeassistant/components/starlingbank/sensor.py
index 20fa646ce41bc1..77f5ab307cb699 100644
--- a/homeassistant/components/starlingbank/sensor.py
+++ b/homeassistant/components/starlingbank/sensor.py
@@ -5,10 +5,9 @@
from starlingbank import StarlingAccount
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -62,7 +61,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(sensors, True)
-class StarlingBalanceSensor(Entity):
+class StarlingBalanceSensor(SensorEntity):
"""Representation of a Starling balance sensor."""
def __init__(self, starling_account, account_name, balance_data_type):
diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py
index f6867d832125db..661e00ed494ee8 100644
--- a/homeassistant/components/startca/sensor.py
+++ b/homeassistant/components/startca/sensor.py
@@ -7,7 +7,7 @@
import voluptuous as vol
import xmltodict
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_API_KEY,
CONF_MONITORED_VARIABLES,
@@ -18,7 +18,6 @@
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -75,7 +74,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(sensors, True)
-class StartcaSensor(Entity):
+class StartcaSensor(SensorEntity):
"""Representation of Start.ca Bandwidth sensor."""
def __init__(self, startcadata, sensor_type, name):
diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py
index 11cddc88c878da..e32ae0debafee4 100644
--- a/homeassistant/components/statistics/sensor.py
+++ b/homeassistant/components/statistics/sensor.py
@@ -7,7 +7,7 @@
from homeassistant.components.recorder.models import States
from homeassistant.components.recorder.util import execute, session_scope
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
CONF_ENTITY_ID,
@@ -18,7 +18,6 @@
)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import (
async_track_point_in_utc_time,
async_track_state_change_event,
@@ -85,7 +84,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
return True
-class StatisticsSensor(Entity):
+class StatisticsSensor(SensorEntity):
"""Representation of a Statistics sensor."""
def __init__(self, entity_id, name, sampling_size, max_age, precision):
@@ -184,7 +183,7 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
if not self.is_binary:
return {
diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py
index dbe83177537dfd..45ae1a6c70a8d5 100644
--- a/homeassistant/components/steam_online/sensor.py
+++ b/homeassistant/components/steam_online/sensor.py
@@ -6,11 +6,10 @@
import steam
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_API_KEY
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_time_interval
from homeassistant.util.dt import utc_from_timestamp
@@ -71,7 +70,7 @@ def do_update(time):
track_time_interval(hass, do_update, BASE_INTERVAL)
-class SteamSensor(Entity):
+class SteamSensor(SensorEntity):
"""A class for the Steam account."""
def __init__(self, account, steamod):
@@ -194,7 +193,7 @@ def _get_last_online(self):
return None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attr = {}
if self._game is not None:
diff --git a/homeassistant/components/stiebel_eltron/climate.py b/homeassistant/components/stiebel_eltron/climate.py
index d8c32575b1703a..5ae7a9230f750f 100644
--- a/homeassistant/components/stiebel_eltron/climate.py
+++ b/homeassistant/components/stiebel_eltron/climate.py
@@ -96,7 +96,7 @@ def update(self):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
return {"filter_alarm": self._filter_alarm}
diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py
index a1c36e9a10ebb7..033af78560c341 100644
--- a/homeassistant/components/stookalert/binary_sensor.py
+++ b/homeassistant/components/stookalert/binary_sensor.py
@@ -57,7 +57,7 @@ def __init__(self, name, api_handler):
self._api_handler = api_handler
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the attribute(s) of the sensor."""
state_attr = {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py
index c7d1dad4835e5a..0d91b63844e75e 100644
--- a/homeassistant/components/stream/__init__.py
+++ b/homeassistant/components/stream/__init__.py
@@ -1,46 +1,58 @@
-"""Provide functionality to stream video source."""
+"""Provide functionality to stream video source.
+
+Components use create_stream with a stream source (e.g. an rtsp url) to create
+a new Stream object. Stream manages:
+ - Background work to fetch and decode a stream
+ - Desired output formats
+ - Home Assistant URLs for viewing a stream
+ - Access tokens for URLs for viewing a stream
+
+A Stream consists of a background worker, and one or more output formats each
+with their own idle timeout managed by the stream component. When an output
+format is no longer in use, the stream component will expire it. When there
+are no active output formats, the background worker is shut down and access
+tokens are expired. Alternatively, a Stream can be configured with keepalive
+to always keep workers active.
+"""
import logging
+import re
import secrets
import threading
+import time
from types import MappingProxyType
-import voluptuous as vol
-
-from homeassistant.const import CONF_FILENAME, EVENT_HOMEASSISTANT_STOP
+from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
-import homeassistant.helpers.config_validation as cv
-from homeassistant.loader import bind_hass
from .const import (
ATTR_ENDPOINTS,
ATTR_STREAMS,
- CONF_DURATION,
- CONF_LOOKBACK,
- CONF_STREAM_SOURCE,
DOMAIN,
MAX_SEGMENTS,
- SERVICE_RECORD,
+ OUTPUT_IDLE_TIMEOUT,
+ STREAM_RESTART_INCREMENT,
+ STREAM_RESTART_RESET_TIME,
)
-from .core import PROVIDERS
+from .core import PROVIDERS, IdleTimer
from .hls import async_setup_hls
_LOGGER = logging.getLogger(__name__)
-STREAM_SERVICE_SCHEMA = vol.Schema({vol.Required(CONF_STREAM_SOURCE): cv.string})
+STREAM_SOURCE_RE = re.compile("//.*:.*@")
+
+
+def redact_credentials(data):
+ """Redact credentials from string data."""
+ return STREAM_SOURCE_RE.sub("//****:****@", data)
-SERVICE_RECORD_SCHEMA = STREAM_SERVICE_SCHEMA.extend(
- {
- vol.Required(CONF_FILENAME): cv.string,
- vol.Optional(CONF_DURATION, default=30): int,
- vol.Optional(CONF_LOOKBACK, default=0): int,
- }
-)
+def create_stream(hass, stream_source, options=None):
+ """Create a stream with the specified identfier based on the source url.
-@bind_hass
-def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=None):
- """Set up stream with token."""
+ The stream_source is typically an rtsp url and options are passed into
+ pyav / ffmpeg as options.
+ """
if DOMAIN not in hass.config.components:
raise HomeAssistantError("Stream integration is not set up.")
@@ -55,25 +67,9 @@ def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=N
**options,
}
- try:
- streams = hass.data[DOMAIN][ATTR_STREAMS]
- stream = streams.get(stream_source)
- if not stream:
- stream = Stream(hass, stream_source, options=options, keepalive=keepalive)
- streams[stream_source] = stream
- else:
- # Update keepalive option on existing stream
- stream.keepalive = keepalive
-
- # Add provider
- stream.add_provider(fmt)
-
- if not stream.access_token:
- stream.access_token = secrets.token_hex()
- stream.start()
- return hass.data[DOMAIN][ATTR_ENDPOINTS][fmt].format(stream.access_token)
- except Exception as err:
- raise HomeAssistantError("Unable to get stream") from err
+ stream = Stream(hass, stream_source, options=options)
+ hass.data[DOMAIN][ATTR_STREAMS].append(stream)
+ return stream
async def async_setup(hass, config):
@@ -88,7 +84,7 @@ async def async_setup(hass, config):
hass.data[DOMAIN] = {}
hass.data[DOMAIN][ATTR_ENDPOINTS] = {}
- hass.data[DOMAIN][ATTR_STREAMS] = {}
+ hass.data[DOMAIN][ATTR_STREAMS] = []
# Setup HLS
hls_endpoint = async_setup_hls(hass)
@@ -100,88 +96,142 @@ async def async_setup(hass, config):
@callback
def shutdown(event):
"""Stop all stream workers."""
- for stream in hass.data[DOMAIN][ATTR_STREAMS].values():
+ for stream in hass.data[DOMAIN][ATTR_STREAMS]:
stream.keepalive = False
stream.stop()
_LOGGER.info("Stopped stream workers")
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown)
- async def async_record(call):
- """Call record stream service handler."""
- await async_handle_record_service(hass, call)
-
- hass.services.async_register(
- DOMAIN, SERVICE_RECORD, async_record, schema=SERVICE_RECORD_SCHEMA
- )
-
return True
class Stream:
"""Represents a single stream."""
- def __init__(self, hass, source, options=None, keepalive=False):
+ def __init__(self, hass, source, options=None):
"""Initialize a stream."""
self.hass = hass
self.source = source
self.options = options
- self.keepalive = keepalive
+ self.keepalive = False
self.access_token = None
self._thread = None
- self._thread_quit = None
+ self._thread_quit = threading.Event()
self._outputs = {}
+ self._fast_restart_once = False
if self.options is None:
self.options = {}
- @property
+ def endpoint_url(self, fmt):
+ """Start the stream and returns a url for the output format."""
+ if fmt not in self._outputs:
+ raise ValueError(f"Stream is not configured for format '{fmt}'")
+ if not self.access_token:
+ self.access_token = secrets.token_hex()
+ return self.hass.data[DOMAIN][ATTR_ENDPOINTS][fmt].format(self.access_token)
+
def outputs(self):
"""Return a copy of the stream outputs."""
# A copy is returned so the caller can iterate through the outputs
# without concern about self._outputs being modified from another thread.
return MappingProxyType(self._outputs.copy())
- def add_provider(self, fmt):
+ def add_provider(self, fmt, timeout=OUTPUT_IDLE_TIMEOUT):
"""Add provider output stream."""
if not self._outputs.get(fmt):
- provider = PROVIDERS[fmt](self)
+
+ @callback
+ def idle_callback():
+ if (not self.keepalive or fmt == "recorder") and fmt in self._outputs:
+ self.remove_provider(self._outputs[fmt])
+ self.check_idle()
+
+ provider = PROVIDERS[fmt](
+ self.hass, IdleTimer(self.hass, timeout, idle_callback)
+ )
self._outputs[fmt] = provider
return self._outputs[fmt]
def remove_provider(self, provider):
"""Remove provider output stream."""
if provider.name in self._outputs:
+ self._outputs[provider.name].cleanup()
del self._outputs[provider.name]
- self.check_idle()
if not self._outputs:
self.stop()
def check_idle(self):
"""Reset access token if all providers are idle."""
- if all([p.idle for p in self._outputs.values()]):
+ if all(p.idle for p in self._outputs.values()):
self.access_token = None
def start(self):
"""Start a stream."""
- # Keep import here so that we can import stream integration without installing reqs
- # pylint: disable=import-outside-toplevel
- from .worker import stream_worker
-
if self._thread is None or not self._thread.is_alive():
if self._thread is not None:
# The thread must have crashed/exited. Join to clean up the
# previous thread.
self._thread.join(timeout=0)
- self._thread_quit = threading.Event()
+ self._thread_quit.clear()
self._thread = threading.Thread(
name="stream_worker",
- target=stream_worker,
- args=(self.hass, self, self._thread_quit),
+ target=self._run_worker,
)
self._thread.start()
- _LOGGER.info("Started stream: %s", self.source)
+ _LOGGER.info("Started stream: %s", redact_credentials(str(self.source)))
+
+ def update_source(self, new_source):
+ """Restart the stream with a new stream source."""
+ _LOGGER.debug("Updating stream source %s", new_source)
+ self.source = new_source
+ self._fast_restart_once = True
+ self._thread_quit.set()
+
+ def _run_worker(self):
+ """Handle consuming streams and restart keepalive streams."""
+ # Keep import here so that we can import stream integration without installing reqs
+ # pylint: disable=import-outside-toplevel
+ from .worker import SegmentBuffer, stream_worker
+
+ segment_buffer = SegmentBuffer(self.outputs)
+ wait_timeout = 0
+ while not self._thread_quit.wait(timeout=wait_timeout):
+ start_time = time.time()
+ stream_worker(self.source, self.options, segment_buffer, self._thread_quit)
+ segment_buffer.discontinuity()
+ if not self.keepalive or self._thread_quit.is_set():
+ if self._fast_restart_once:
+ # The stream source is updated, restart without any delay.
+ self._fast_restart_once = False
+ self._thread_quit.clear()
+ continue
+ break
+ # To avoid excessive restarts, wait before restarting
+ # As the required recovery time may be different for different setups, start
+ # with trying a short wait_timeout and increase it on each reconnection attempt.
+ # Reset the wait_timeout after the worker has been up for several minutes
+ if time.time() - start_time > STREAM_RESTART_RESET_TIME:
+ wait_timeout = 0
+ wait_timeout += STREAM_RESTART_INCREMENT
+ _LOGGER.debug(
+ "Restarting stream worker in %d seconds: %s",
+ wait_timeout,
+ self.source,
+ )
+ self._worker_finished()
+
+ def _worker_finished(self):
+ """Schedule cleanup of all outputs."""
+
+ @callback
+ def remove_outputs():
+ for provider in self.outputs().values():
+ self.remove_provider(provider)
+
+ self.hass.loop.call_soon_threadsafe(remove_outputs)
def stop(self):
"""Remove outputs and access token."""
@@ -197,42 +247,31 @@ def _stop(self):
self._thread_quit.set()
self._thread.join()
self._thread = None
- _LOGGER.info("Stopped stream: %s", self.source)
-
-
-async def async_handle_record_service(hass, call):
- """Handle save video service calls."""
- stream_source = call.data[CONF_STREAM_SOURCE]
- video_path = call.data[CONF_FILENAME]
- duration = call.data[CONF_DURATION]
- lookback = call.data[CONF_LOOKBACK]
-
- # Check for file access
- if not hass.config.is_allowed_path(video_path):
- raise HomeAssistantError(f"Can't write {video_path}, no access to path!")
-
- # Check for active stream
- streams = hass.data[DOMAIN][ATTR_STREAMS]
- stream = streams.get(stream_source)
- if not stream:
- stream = Stream(hass, stream_source)
- streams[stream_source] = stream
-
- # Add recorder
- recorder = stream.outputs.get("recorder")
- if recorder:
- raise HomeAssistantError(f"Stream already recording to {recorder.video_path}!")
-
- recorder = stream.add_provider("recorder")
- recorder.video_path = video_path
- recorder.timeout = duration
-
- stream.start()
-
- # Take advantage of lookback
- hls = stream.outputs.get("hls")
- if lookback > 0 and hls:
- num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS)
- # Wait for latest segment, then add the lookback
- await hls.recv()
- recorder.prepend(list(hls.get_segment())[-num_segments:])
+ _LOGGER.info("Stopped stream: %s", redact_credentials(str(self.source)))
+
+ async def async_record(self, video_path, duration=30, lookback=5):
+ """Make a .mp4 recording from a provided stream."""
+
+ # Check for file access
+ if not self.hass.config.is_allowed_path(video_path):
+ raise HomeAssistantError(f"Can't write {video_path}, no access to path!")
+
+ # Add recorder
+ recorder = self.outputs().get("recorder")
+ if recorder:
+ raise HomeAssistantError(
+ f"Stream already recording to {recorder.video_path}!"
+ )
+ recorder = self.add_provider("recorder", timeout=duration)
+ recorder.video_path = video_path
+
+ self.start()
+ _LOGGER.debug("Started a stream recording of %s seconds", duration)
+
+ # Take advantage of lookback
+ hls = self.outputs().get("hls")
+ if lookback > 0 and hls:
+ num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS)
+ # Wait for latest segment, then add the lookback
+ await hls.recv()
+ recorder.prepend(list(hls.get_segment())[-num_segments:])
diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py
index 181808e549ed66..a2557286cf1753 100644
--- a/homeassistant/components/stream/const.py
+++ b/homeassistant/components/stream/const.py
@@ -1,21 +1,21 @@
"""Constants for Stream component."""
DOMAIN = "stream"
-CONF_STREAM_SOURCE = "stream_source"
-CONF_LOOKBACK = "lookback"
-CONF_DURATION = "duration"
-
ATTR_ENDPOINTS = "endpoints"
ATTR_STREAMS = "streams"
-ATTR_KEEPALIVE = "keepalive"
-
-SERVICE_RECORD = "record"
OUTPUT_FORMATS = ["hls"]
+SEGMENT_CONTAINER_FORMAT = "mp4" # format for segments
+RECORDER_CONTAINER_FORMAT = "mp4" # format for recorder output
+AUDIO_CODECS = {"aac", "mp3"}
+
FORMAT_CONTENT_TYPE = {"hls": "application/vnd.apple.mpegurl"}
-MAX_SEGMENTS = 3 # Max number of segments to keep around
+OUTPUT_IDLE_TIMEOUT = 300 # Idle timeout due to inactivity
+
+NUM_PLAYLIST_SEGMENTS = 3 # Number of segments to use in HLS playlist
+MAX_SEGMENTS = 4 # Max number of segments to keep around
MIN_SEGMENT_DURATION = 1.5 # Each segment is at least this many seconds
PACKETS_TO_WAIT_FOR_AUDIO = 20 # Some streams have an audio stream with no audio
diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py
index 5158ba185b13fb..076eb3596d7a33 100644
--- a/homeassistant/components/stream/core.py
+++ b/homeassistant/components/stream/core.py
@@ -1,18 +1,20 @@
"""Provides core stream functionality."""
+from __future__ import annotations
+
import asyncio
from collections import deque
import io
-from typing import Any, Callable, List
+from typing import Any, Callable
from aiohttp import web
import attr
from homeassistant.components.http import HomeAssistantView
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.event import async_call_later
from homeassistant.util.decorator import Registry
-from .const import ATTR_STREAMS, DOMAIN, MAX_SEGMENTS
+from .const import ATTR_STREAMS, DOMAIN
PROVIDERS = Registry()
@@ -34,20 +36,64 @@ class Segment:
sequence: int = attr.ib()
segment: io.BytesIO = attr.ib()
duration: float = attr.ib()
+ # For detecting discontinuities across stream restarts
+ stream_id: int = attr.ib(default=0)
+
+
+class IdleTimer:
+ """Invoke a callback after an inactivity timeout.
+
+ The IdleTimer invokes the callback after some timeout has passed. The awake() method
+ resets the internal alarm, extending the inactivity time.
+ """
+
+ def __init__(
+ self, hass: HomeAssistant, timeout: int, idle_callback: Callable[[], None]
+ ):
+ """Initialize IdleTimer."""
+ self._hass = hass
+ self._timeout = timeout
+ self._callback = idle_callback
+ self._unsub = None
+ self.idle = False
+
+ def start(self):
+ """Start the idle timer if not already started."""
+ self.idle = False
+ if self._unsub is None:
+ self._unsub = async_call_later(self._hass, self._timeout, self.fire)
+
+ def awake(self):
+ """Keep the idle time alive by resetting the timeout."""
+ self.idle = False
+ # Reset idle timeout
+ self.clear()
+ self._unsub = async_call_later(self._hass, self._timeout, self.fire)
+
+ def clear(self):
+ """Clear and disable the timer if it has not already fired."""
+ if self._unsub is not None:
+ self._unsub()
+
+ def fire(self, _now=None):
+ """Invoke the idle timeout callback, called when the alarm fires."""
+ self.idle = True
+ self._unsub = None
+ self._callback()
class StreamOutput:
"""Represents a stream output."""
- def __init__(self, stream, timeout: int = 300) -> None:
+ def __init__(
+ self, hass: HomeAssistant, idle_timer: IdleTimer, deque_maxlen: int = None
+ ) -> None:
"""Initialize a stream output."""
- self.idle = False
- self.timeout = timeout
- self._stream = stream
+ self._hass = hass
+ self._idle_timer = idle_timer
self._cursor = None
self._event = asyncio.Event()
- self._segments = deque(maxlen=MAX_SEGMENTS)
- self._unsub = None
+ self._segments = deque(maxlen=deque_maxlen)
@property
def name(self) -> str:
@@ -55,27 +101,12 @@ def name(self) -> str:
return None
@property
- def format(self) -> str:
- """Return container format."""
- return None
-
- @property
- def audio_codecs(self) -> str:
- """Return desired audio codecs."""
- return None
-
- @property
- def video_codecs(self) -> tuple:
- """Return desired video codecs."""
- return None
-
- @property
- def container_options(self) -> Callable[[int], dict]:
- """Return Callable which takes a sequence number and returns container options."""
- return None
+ def idle(self) -> bool:
+ """Return True if the output is idle."""
+ return self._idle_timer.idle
@property
- def segments(self) -> List[int]:
+ def segments(self) -> list[int]:
"""Return current sequence from segments."""
return [s.sequence for s in self._segments]
@@ -90,11 +121,7 @@ def target_duration(self) -> int:
def get_segment(self, sequence: int = None) -> Any:
"""Retrieve a specific segment, or the whole list."""
- self.idle = False
- # Reset idle timeout
- if self._unsub is not None:
- self._unsub()
- self._unsub = async_call_later(self._stream.hass, self.timeout, self._timeout)
+ self._idle_timer.awake()
if not sequence:
return self._segments
@@ -119,43 +146,22 @@ async def recv(self) -> Segment:
def put(self, segment: Segment) -> None:
"""Store output."""
- self._stream.hass.loop.call_soon_threadsafe(self._async_put, segment)
+ self._hass.loop.call_soon_threadsafe(self._async_put, segment)
@callback
def _async_put(self, segment: Segment) -> None:
"""Store output from event loop."""
# Start idle timeout when we start receiving data
- if self._unsub is None:
- self._unsub = async_call_later(
- self._stream.hass, self.timeout, self._timeout
- )
-
- if segment is None:
- self._event.set()
- # Cleanup provider
- if self._unsub is not None:
- self._unsub()
- self.cleanup()
- return
-
+ self._idle_timer.start()
self._segments.append(segment)
self._event.set()
self._event.clear()
- @callback
- def _timeout(self, _now=None):
- """Handle stream timeout."""
- self._unsub = None
- if self._stream.keepalive:
- self.idle = True
- self._stream.check_idle()
- else:
- self.cleanup()
-
def cleanup(self):
"""Handle cleanup."""
- self._segments = deque(maxlen=MAX_SEGMENTS)
- self._stream.remove_provider(self)
+ self._event.set()
+ self._idle_timer.clear()
+ self._segments = deque(maxlen=self._segments.maxlen)
class StreamView(HomeAssistantView):
@@ -174,11 +180,7 @@ async def get(self, request, token, sequence=None):
hass = request.app["hass"]
stream = next(
- (
- s
- for s in hass.data[DOMAIN][ATTR_STREAMS].values()
- if s.access_token == token
- ),
+ (s for s in hass.data[DOMAIN][ATTR_STREAMS] if s.access_token == token),
None,
)
diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py
index 2b305442b808cb..4909bbf95a3452 100644
--- a/homeassistant/components/stream/hls.py
+++ b/homeassistant/components/stream/hls.py
@@ -1,13 +1,12 @@
"""Provide functionality to stream HLS."""
import io
-from typing import Callable
from aiohttp import web
from homeassistant.core import callback
-from .const import FORMAT_CONTENT_TYPE
-from .core import PROVIDERS, StreamOutput, StreamView
+from .const import FORMAT_CONTENT_TYPE, MAX_SEGMENTS, NUM_PLAYLIST_SEGMENTS
+from .core import PROVIDERS, HomeAssistant, IdleTimer, StreamOutput, StreamView
from .fmp4utils import get_codec_string, get_init, get_m4s
@@ -51,9 +50,8 @@ async def handle(self, request, stream, sequence):
track = stream.add_provider("hls")
stream.start()
# Wait for a segment to be ready
- if not track.segments:
- if not await track.recv():
- return web.HTTPNotFound()
+ if not track.segments and not await track.recv():
+ return web.HTTPNotFound()
headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]}
return web.Response(body=self.render(track).encode("utf-8"), headers=headers)
@@ -77,21 +75,27 @@ def render_preamble(track):
@staticmethod
def render_playlist(track):
"""Render playlist."""
- segments = track.segments
+ segments = list(track.get_segment())[-NUM_PLAYLIST_SEGMENTS:]
if not segments:
return []
- playlist = ["#EXT-X-MEDIA-SEQUENCE:{}".format(segments[0])]
+ playlist = [
+ "#EXT-X-MEDIA-SEQUENCE:{}".format(segments[0].sequence),
+ "#EXT-X-DISCONTINUITY-SEQUENCE:{}".format(segments[0].stream_id),
+ ]
- for sequence in segments:
- segment = track.get_segment(sequence)
+ last_stream_id = segments[0].stream_id
+ for segment in segments:
+ if last_stream_id != segment.stream_id:
+ playlist.append("#EXT-X-DISCONTINUITY")
playlist.extend(
[
"#EXTINF:{:.04f},".format(float(segment.duration)),
f"./segment/{segment.sequence}.m4s",
]
)
+ last_stream_id = segment.stream_id
return playlist
@@ -105,9 +109,8 @@ async def handle(self, request, stream, sequence):
track = stream.add_provider("hls")
stream.start()
# Wait for a segment to be ready
- if not track.segments:
- if not await track.recv():
- return web.HTTPNotFound()
+ if not track.segments and not await track.recv():
+ return web.HTTPNotFound()
headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]}
return web.Response(body=self.render(track).encode("utf-8"), headers=headers)
@@ -153,32 +156,11 @@ async def handle(self, request, stream, sequence):
class HlsStreamOutput(StreamOutput):
"""Represents HLS Output formats."""
+ def __init__(self, hass: HomeAssistant, idle_timer: IdleTimer) -> None:
+ """Initialize recorder output."""
+ super().__init__(hass, idle_timer, deque_maxlen=MAX_SEGMENTS)
+
@property
def name(self) -> str:
"""Return provider name."""
return "hls"
-
- @property
- def format(self) -> str:
- """Return container format."""
- return "mp4"
-
- @property
- def audio_codecs(self) -> str:
- """Return desired audio codecs."""
- return {"aac", "mp3"}
-
- @property
- def video_codecs(self) -> tuple:
- """Return desired video codecs."""
- return {"hevc", "h264"}
-
- @property
- def container_options(self) -> Callable[[int], dict]:
- """Return Callable which takes a sequence number and returns container options."""
- return lambda sequence: {
- # Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970
- "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont",
- "avoid_negative_ts": "make_non_negative",
- "fragment_index": str(sequence),
- }
diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json
index 3d194bdf0d4e7c..400b50eae04590 100644
--- a/homeassistant/components/stream/manifest.json
+++ b/homeassistant/components/stream/manifest.json
@@ -2,8 +2,8 @@
"domain": "stream",
"name": "Stream",
"documentation": "https://www.home-assistant.io/integrations/stream",
- "requirements": ["av==8.0.2"],
+ "requirements": ["av==8.0.3"],
"dependencies": ["http"],
- "codeowners": ["@hunterjm", "@uvjustin"],
+ "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"],
"quality_scale": "internal"
}
diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py
index cf923de85c2eb6..01a8ca9ea6b49a 100644
--- a/homeassistant/components/stream/recorder.py
+++ b/homeassistant/components/stream/recorder.py
@@ -1,14 +1,17 @@
"""Provide functionality to record stream."""
+from __future__ import annotations
+
import logging
import os
import threading
-from typing import List
+from typing import Deque
import av
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
-from .core import PROVIDERS, Segment, StreamOutput
+from .const import RECORDER_CONTAINER_FORMAT, SEGMENT_CONTAINER_FORMAT
+from .core import PROVIDERS, IdleTimer, Segment, StreamOutput
_LOGGER = logging.getLogger(__name__)
@@ -18,51 +21,83 @@ def async_setup_recorder(hass):
"""Only here so Provider Registry works."""
-def recorder_save_worker(file_out: str, segments: List[Segment], container_format: str):
+def recorder_save_worker(file_out: str, segments: Deque[Segment]):
"""Handle saving stream."""
+
+ if not segments:
+ _LOGGER.error("Recording failed to capture anything")
+ return
+
if not os.path.exists(os.path.dirname(file_out)):
os.makedirs(os.path.dirname(file_out), exist_ok=True)
- first_pts = {"video": None, "audio": None}
- output = av.open(file_out, "w", format=container_format)
+ pts_adjuster = {"video": None, "audio": None}
+ output = None
output_v = None
output_a = None
- # Get first_pts values from first segment
- if len(segments) > 0:
- segment = segments[0]
- source = av.open(segment.segment, "r", format=container_format)
- source_v = source.streams.video[0]
- first_pts["video"] = source_v.start_time
- if len(source.streams.audio) > 0:
- source_a = source.streams.audio[0]
- first_pts["audio"] = int(
- source_v.start_time * source_v.time_base / source_a.time_base
- )
- source.close()
+ last_stream_id = None
+ # The running duration of processed segments. Note that this is in av.time_base
+ # units which seem to be defined inversely to how stream time_bases are defined
+ running_duration = 0
+ last_sequence = float("-inf")
for segment in segments:
+ # Because the stream_worker is in a different thread from the record service,
+ # the lookback segments may still have some overlap with the recorder segments
+ if segment.sequence <= last_sequence:
+ continue
+ last_sequence = segment.sequence
+
# Open segment
- source = av.open(segment.segment, "r", format=container_format)
+ source = av.open(segment.segment, "r", format=SEGMENT_CONTAINER_FORMAT)
source_v = source.streams.video[0]
- # Add output streams
+ source_a = source.streams.audio[0] if len(source.streams.audio) > 0 else None
+
+ # Create output on first segment
+ if not output:
+ output = av.open(
+ file_out,
+ "w",
+ format=RECORDER_CONTAINER_FORMAT,
+ container_options={
+ "video_track_timescale": str(int(1 / source_v.time_base))
+ },
+ )
+
+ # Add output streams if necessary
if not output_v:
output_v = output.add_stream(template=source_v)
context = output_v.codec_context
context.flags |= "GLOBAL_HEADER"
- if not output_a and len(source.streams.audio) > 0:
- source_a = source.streams.audio[0]
+ if source_a and not output_a:
output_a = output.add_stream(template=source_a)
+ # Recalculate pts adjustments on first segment and on any discontinuity
+ # We are assuming time base is the same across all discontinuities
+ if last_stream_id != segment.stream_id:
+ last_stream_id = segment.stream_id
+ pts_adjuster["video"] = int(
+ (running_duration - source.start_time)
+ / (av.time_base * source_v.time_base)
+ )
+ if source_a:
+ pts_adjuster["audio"] = int(
+ (running_duration - source.start_time)
+ / (av.time_base * source_a.time_base)
+ )
+
# Remux video
for packet in source.demux():
if packet.dts is None:
continue
- packet.pts -= first_pts[packet.stream.type]
- packet.dts -= first_pts[packet.stream.type]
+ packet.pts += pts_adjuster[packet.stream.type]
+ packet.dts += pts_adjuster[packet.stream.type]
packet.stream = output_v if packet.stream.type == "video" else output_a
output.mux(packet)
+ running_duration += source.duration - source.start_time
+
source.close()
output.close()
@@ -72,43 +107,19 @@ def recorder_save_worker(file_out: str, segments: List[Segment], container_forma
class RecorderOutput(StreamOutput):
"""Represents HLS Output formats."""
- def __init__(self, stream, timeout: int = 30) -> None:
+ def __init__(self, hass: HomeAssistant, idle_timer: IdleTimer) -> None:
"""Initialize recorder output."""
- super().__init__(stream, timeout)
+ super().__init__(hass, idle_timer)
self.video_path = None
- self._segments = []
@property
def name(self) -> str:
"""Return provider name."""
return "recorder"
- @property
- def format(self) -> str:
- """Return container format."""
- return "mp4"
-
- @property
- def audio_codecs(self) -> str:
- """Return desired audio codec."""
- return {"aac", "mp3"}
-
- @property
- def video_codecs(self) -> tuple:
- """Return desired video codecs."""
- return {"hevc", "h264"}
-
- def prepend(self, segments: List[Segment]) -> None:
+ def prepend(self, segments: list[Segment]) -> None:
"""Prepend segments to existing list."""
- own_segments = self.segments
- segments = [s for s in segments if s.sequence not in own_segments]
- self._segments = segments + self._segments
-
- @callback
- def _timeout(self, _now=None):
- """Handle recorder timeout."""
- self._unsub = None
- self.cleanup()
+ self._segments.extendleft(reversed(segments))
def cleanup(self):
"""Write recording and clean up."""
@@ -116,9 +127,8 @@ def cleanup(self):
thread = threading.Thread(
name="recorder_save_worker",
target=recorder_save_worker,
- args=(self.video_path, self._segments, self.format),
+ args=(self.video_path, self._segments),
)
thread.start()
- self._segments = []
- self._stream.remove_provider(self)
+ super().cleanup()
diff --git a/homeassistant/components/stream/services.yaml b/homeassistant/components/stream/services.yaml
deleted file mode 100644
index a8652335bf1a20..00000000000000
--- a/homeassistant/components/stream/services.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-record:
- description: Make a .mp4 recording from a provided stream.
- fields:
- stream_source:
- description: The input source for the stream.
- example: "rtsp://my.stream.feed:554"
- filename:
- description: The file name string.
- example: "/tmp/my_stream.mp4"
- duration:
- description: "Target recording length (in seconds). Default: 30"
- example: 30
- lookback:
- description: "Target lookback period (in seconds) to include in addition to duration. Only available if there is currently an active HLS stream for stream_source. Default: 0"
- example: 5
diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py
index cccbfd1b48babd..cd4528b3088e21 100644
--- a/homeassistant/components/stream/worker.py
+++ b/homeassistant/components/stream/worker.py
@@ -2,17 +2,17 @@
from collections import deque
import io
import logging
-import time
import av
+from . import redact_credentials
from .const import (
+ AUDIO_CODECS,
MAX_MISSING_DTS,
MAX_TIMESTAMP_GAP,
MIN_SEGMENT_DURATION,
PACKETS_TO_WAIT_FOR_AUDIO,
- STREAM_RESTART_INCREMENT,
- STREAM_RESTART_RESET_TIME,
+ SEGMENT_CONTAINER_FORMAT,
STREAM_TIMEOUT,
)
from .core import Segment, StreamBuffer
@@ -20,19 +20,20 @@
_LOGGER = logging.getLogger(__name__)
-def create_stream_buffer(stream_output, video_stream, audio_stream, sequence):
+def create_stream_buffer(video_stream, audio_stream, sequence):
"""Create a new StreamBuffer."""
segment = io.BytesIO()
- container_options = (
- stream_output.container_options(sequence)
- if stream_output.container_options
- else {}
- )
+ container_options = {
+ # Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970
+ "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets",
+ "avoid_negative_ts": "disabled",
+ "fragment_index": str(sequence),
+ }
output = av.open(
segment,
mode="w",
- format=stream_output.format,
+ format=SEGMENT_CONTAINER_FORMAT,
container_options={
"video_track_timescale": str(int(1 / video_stream.time_base)),
**container_options,
@@ -41,46 +42,93 @@ def create_stream_buffer(stream_output, video_stream, audio_stream, sequence):
vstream = output.add_stream(template=video_stream)
# Check if audio is requested
astream = None
- if audio_stream and audio_stream.name in stream_output.audio_codecs:
+ if audio_stream and audio_stream.name in AUDIO_CODECS:
astream = output.add_stream(template=audio_stream)
return StreamBuffer(segment, output, vstream, astream)
-def stream_worker(hass, stream, quit_event):
- """Handle consuming streams and restart keepalive streams."""
+class SegmentBuffer:
+ """Buffer for writing a sequence of packets to the output as a segment."""
- wait_timeout = 0
- while not quit_event.wait(timeout=wait_timeout):
- start_time = time.time()
- try:
- _stream_worker_internal(hass, stream, quit_event)
- except av.error.FFmpegError: # pylint: disable=c-extension-no-member
- _LOGGER.exception("Stream connection failed: %s", stream.source)
- if not stream.keepalive or quit_event.is_set():
- break
- # To avoid excessive restarts, wait before restarting
- # As the required recovery time may be different for different setups, start
- # with trying a short wait_timeout and increase it on each reconnection attempt.
- # Reset the wait_timeout after the worker has been up for several minutes
- if time.time() - start_time > STREAM_RESTART_RESET_TIME:
- wait_timeout = 0
- wait_timeout += STREAM_RESTART_INCREMENT
- _LOGGER.debug(
- "Restarting stream worker in %d seconds: %s",
- wait_timeout,
- stream.source,
+ def __init__(self, outputs_callback) -> None:
+ """Initialize SegmentBuffer."""
+ self._stream_id = 0
+ self._video_stream = None
+ self._audio_stream = None
+ self._outputs_callback = outputs_callback
+ # Each element is a StreamOutput
+ self._outputs = []
+ self._sequence = 0
+ self._segment_start_pts = None
+ self._stream_buffer = None
+
+ def set_streams(self, video_stream, audio_stream):
+ """Initialize output buffer with streams from container."""
+ self._video_stream = video_stream
+ self._audio_stream = audio_stream
+
+ def reset(self, video_pts):
+ """Initialize a new stream segment."""
+ # Keep track of the number of segments we've processed
+ self._sequence += 1
+ self._segment_start_pts = video_pts
+
+ # Fetch the latest StreamOutputs, which may have changed since the
+ # worker started.
+ self._outputs = self._outputs_callback().values()
+ self._stream_buffer = create_stream_buffer(
+ self._video_stream, self._audio_stream, self._sequence
+ )
+
+ def mux_packet(self, packet):
+ """Mux a packet to the appropriate StreamBuffers."""
+
+ # Check for end of segment
+ if packet.stream == self._video_stream and packet.is_keyframe:
+ duration = (packet.pts - self._segment_start_pts) * packet.time_base
+ if duration >= MIN_SEGMENT_DURATION:
+ # Save segment to outputs
+ self.flush(duration)
+
+ # Reinitialize
+ self.reset(packet.pts)
+
+ # Mux the packet
+ if packet.stream == self._video_stream:
+ packet.stream = self._stream_buffer.vstream
+ self._stream_buffer.output.mux(packet)
+ elif packet.stream == self._audio_stream:
+ packet.stream = self._stream_buffer.astream
+ self._stream_buffer.output.mux(packet)
+
+ def flush(self, duration):
+ """Create a segment from the buffered packets and write to output."""
+ self._stream_buffer.output.close()
+ segment = Segment(
+ self._sequence, self._stream_buffer.segment, duration, self._stream_id
)
+ for stream_output in self._outputs:
+ stream_output.put(segment)
+ def discontinuity(self):
+ """Mark the stream as having been restarted."""
+ # Preserving sequence and stream_id here keep the HLS playlist logic
+ # simple to check for discontinuity at output time, and to determine
+ # the discontinuity sequence number.
+ self._stream_id += 1
-def _stream_worker_internal(hass, stream, quit_event):
+ def close(self):
+ """Close stream buffer."""
+ self._stream_buffer.output.close()
+
+
+def stream_worker(source, options, segment_buffer, quit_event):
"""Handle consuming streams."""
try:
- container = av.open(
- stream.source, options=stream.options, timeout=STREAM_TIMEOUT
- )
+ container = av.open(source, options=options, timeout=STREAM_TIMEOUT)
except av.AVError:
- _LOGGER.error("Error opening stream %s", stream.source)
+ _LOGGER.error("Error opening stream %s", redact_credentials(str(source)))
return
try:
video_stream = container.streams.video[0]
@@ -106,10 +154,6 @@ def _stream_worker_internal(hass, stream, quit_event):
last_dts = {video_stream: float("-inf"), audio_stream: float("-inf")}
# Keep track of consecutive packets without a dts to detect end of stream.
missing_dts = 0
- # Holds the buffers for each stream provider
- outputs = None
- # Keep track of the number of segments we've processed
- sequence = 0
# The video pts at the beginning of the segment
segment_start_pts = None
# Because of problems 1 and 2 below, we need to store the first few packets and replay them
@@ -165,6 +209,16 @@ def peek_first_pts():
missing_dts += 1
continue
if packet.stream == audio_stream:
+ # detect ADTS AAC and disable audio
+ if audio_stream.codec.name == "aac" and packet.size > 2:
+ with memoryview(packet) as packet_view:
+ if packet_view[0] == 0xFF and packet_view[1] & 0xF0 == 0xF0:
+ _LOGGER.warning(
+ "ADTS AAC detected - disabling audio stream"
+ )
+ container_packets = container.demux(video_stream)
+ audio_stream = None
+ continue
found_audio = True
elif (
segment_start_pts is None
@@ -183,54 +237,15 @@ def peek_first_pts():
_LOGGER.error(
"Error demuxing stream while finding first packet: %s", str(ex)
)
- finalize_stream()
return False
return True
- def initialize_segment(video_pts):
- """Reset some variables and initialize outputs for each segment."""
- nonlocal outputs, sequence, segment_start_pts
- # Clear outputs and increment sequence
- outputs = {}
- sequence += 1
- segment_start_pts = video_pts
- for stream_output in stream.outputs.values():
- if video_stream.name not in stream_output.video_codecs:
- continue
- buffer = create_stream_buffer(
- stream_output, video_stream, audio_stream, sequence
- )
- outputs[stream_output.name] = (
- buffer,
- {video_stream: buffer.vstream, audio_stream: buffer.astream},
- )
-
- def mux_video_packet(packet):
- # mux packets to each buffer
- for buffer, output_streams in outputs.values():
- # Assign the packet to the new stream & mux
- packet.stream = output_streams[video_stream]
- buffer.output.mux(packet)
-
- def mux_audio_packet(packet):
- # almost the same as muxing video but add extra check
- for buffer, output_streams in outputs.values():
- # Assign the packet to the new stream & mux
- if output_streams.get(audio_stream):
- packet.stream = output_streams[audio_stream]
- buffer.output.mux(packet)
-
- def finalize_stream():
- if not stream.keepalive:
- # End of stream, clear listeners and stop thread
- for fmt in stream.outputs:
- stream.outputs[fmt].put(None)
-
if not peek_first_pts():
container.close()
return
- initialize_segment(segment_start_pts)
+ segment_buffer.set_streams(video_stream, audio_stream)
+ segment_buffer.reset(segment_start_pts)
while not quit_event.is_set():
try:
@@ -249,7 +264,6 @@ def finalize_stream():
missing_dts = 0
except (av.AVError, StopIteration) as ex:
_LOGGER.error("Error demuxing stream: %s", str(ex))
- finalize_stream()
break
# Discard packet if dts is not monotonic
@@ -263,38 +277,16 @@ def finalize_stream():
last_dts[packet.stream],
packet.dts,
)
- finalize_stream()
break
continue
- # Check for end of segment
- if packet.stream == video_stream and packet.is_keyframe:
- segment_duration = (packet.pts - segment_start_pts) * packet.time_base
- if segment_duration >= MIN_SEGMENT_DURATION:
- # Save segment to outputs
- for fmt, (buffer, _) in outputs.items():
- buffer.output.close()
- if stream.outputs.get(fmt):
- stream.outputs[fmt].put(
- Segment(
- sequence,
- buffer.segment,
- segment_duration,
- ),
- )
-
- # Reinitialize
- initialize_segment(packet.pts)
-
# Update last_dts processed
last_dts[packet.stream] = packet.dts
- # mux packets
- if packet.stream == video_stream:
- mux_video_packet(packet) # mutates packet timestamps
- else:
- mux_audio_packet(packet) # mutates packet timestamps
+
+ # Mux packets, and possibly write a segment to the output stream.
+ # This mutates packet timestamps and stream
+ segment_buffer.mux_packet(packet)
# Close stream
- for buffer, _ in outputs.values():
- buffer.output.close()
+ segment_buffer.close()
container.close()
diff --git a/homeassistant/components/streamlabswater/__init__.py b/homeassistant/components/streamlabswater/__init__.py
index 336e92358ee667..0a49e128c2f7dc 100644
--- a/homeassistant/components/streamlabswater/__init__.py
+++ b/homeassistant/components/streamlabswater/__init__.py
@@ -17,7 +17,7 @@
AWAY_MODE_AWAY = "away"
AWAY_MODE_HOME = "home"
-STREAMLABSWATER_COMPONENTS = ["sensor", "binary_sensor"]
+PLATFORMS = ["sensor", "binary_sensor"]
CONF_LOCATION_ID = "location_id"
@@ -39,7 +39,7 @@
def setup(hass, config):
- """Set up the streamlabs water component."""
+ """Set up the streamlabs water integration."""
conf = config[DOMAIN]
api_key = conf.get(CONF_API_KEY)
@@ -74,8 +74,8 @@ def setup(hass, config):
"location_name": location_name,
}
- for component in STREAMLABSWATER_COMPONENTS:
- discovery.load_platform(hass, component, DOMAIN, {}, config)
+ for platform in PLATFORMS:
+ discovery.load_platform(hass, platform, DOMAIN, {}, config)
def set_away_mode(service):
"""Set the StreamLabsWater Away Mode."""
diff --git a/homeassistant/components/streamlabswater/sensor.py b/homeassistant/components/streamlabswater/sensor.py
index e7168f8ec0b536..ba722d0a4f2843 100644
--- a/homeassistant/components/streamlabswater/sensor.py
+++ b/homeassistant/components/streamlabswater/sensor.py
@@ -2,9 +2,9 @@
from datetime import timedelta
+from homeassistant.components.sensor import SensorEntity
from homeassistant.components.streamlabswater import DOMAIN as STREAMLABSWATER_DOMAIN
from homeassistant.const import VOLUME_GALLONS
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
DEPENDENCIES = ["streamlabswater"]
@@ -67,7 +67,7 @@ def get_yearly_usage(self):
return self._this_year
-class StreamLabsDailyUsage(Entity):
+class StreamLabsDailyUsage(SensorEntity):
"""Monitors the daily water usage."""
def __init__(self, location_name, streamlabs_usage_data):
diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py
index 43ef01a497e30f..5c45e5e3d44571 100644
--- a/homeassistant/components/stt/__init__.py
+++ b/homeassistant/components/stt/__init__.py
@@ -1,8 +1,9 @@
"""Provide functionality to STT."""
+from __future__ import annotations
+
from abc import ABC, abstractmethod
import asyncio
import logging
-from typing import Dict, List, Optional
from aiohttp import StreamReader, web
from aiohttp.hdrs import istr
@@ -62,7 +63,7 @@ async def async_setup_platform(p_type, p_config=None, discovery_info=None):
return
setup_tasks = [
- async_setup_platform(p_type, p_config)
+ asyncio.create_task(async_setup_platform(p_type, p_config))
for p_type, p_config in config_per_platform(config, DOMAIN)
]
@@ -96,44 +97,44 @@ class SpeechMetadata:
class SpeechResult:
"""Result of audio Speech."""
- text: Optional[str] = attr.ib()
+ text: str | None = attr.ib()
result: SpeechResultState = attr.ib()
class Provider(ABC):
"""Represent a single STT provider."""
- hass: Optional[HomeAssistantType] = None
- name: Optional[str] = None
+ hass: HomeAssistantType | None = None
+ name: str | None = None
@property
@abstractmethod
- def supported_languages(self) -> List[str]:
+ def supported_languages(self) -> list[str]:
"""Return a list of supported languages."""
@property
@abstractmethod
- def supported_formats(self) -> List[AudioFormats]:
+ def supported_formats(self) -> list[AudioFormats]:
"""Return a list of supported formats."""
@property
@abstractmethod
- def supported_codecs(self) -> List[AudioCodecs]:
+ def supported_codecs(self) -> list[AudioCodecs]:
"""Return a list of supported codecs."""
@property
@abstractmethod
- def supported_bit_rates(self) -> List[AudioBitRates]:
+ def supported_bit_rates(self) -> list[AudioBitRates]:
"""Return a list of supported bit rates."""
@property
@abstractmethod
- def supported_sample_rates(self) -> List[AudioSampleRates]:
+ def supported_sample_rates(self) -> list[AudioSampleRates]:
"""Return a list of supported sample rates."""
@property
@abstractmethod
- def supported_channels(self) -> List[AudioChannels]:
+ def supported_channels(self) -> list[AudioChannels]:
"""Return a list of supported channels."""
@abstractmethod
@@ -167,12 +168,12 @@ class SpeechToTextView(HomeAssistantView):
url = "/api/stt/{provider}"
name = "api:stt:provider"
- def __init__(self, providers: Dict[str, Provider]) -> None:
+ def __init__(self, providers: dict[str, Provider]) -> None:
"""Initialize a tts view."""
self.providers = providers
@staticmethod
- def _metadata_from_header(request: web.Request) -> Optional[SpeechMetadata]:
+ def _metadata_from_header(request: web.Request) -> SpeechMetadata | None:
"""Extract metadata from header.
X-Speech-Content: format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=1; language=de_de
diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py
new file mode 100644
index 00000000000000..04f6111167105f
--- /dev/null
+++ b/homeassistant/components/subaru/__init__.py
@@ -0,0 +1,173 @@
+"""The Subaru integration."""
+import asyncio
+from datetime import timedelta
+import logging
+import time
+
+from subarulink import Controller as SubaruAPI, InvalidCredentials, SubaruException
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import aiohttp_client
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import (
+ CONF_COUNTRY,
+ CONF_UPDATE_ENABLED,
+ COORDINATOR_NAME,
+ DOMAIN,
+ ENTRY_CONTROLLER,
+ ENTRY_COORDINATOR,
+ ENTRY_VEHICLES,
+ FETCH_INTERVAL,
+ PLATFORMS,
+ UPDATE_INTERVAL,
+ VEHICLE_API_GEN,
+ VEHICLE_HAS_EV,
+ VEHICLE_HAS_REMOTE_SERVICE,
+ VEHICLE_HAS_REMOTE_START,
+ VEHICLE_HAS_SAFETY_SERVICE,
+ VEHICLE_LAST_UPDATE,
+ VEHICLE_NAME,
+ VEHICLE_VIN,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup(hass, base_config):
+ """Do nothing since this integration does not support configuration.yml setup."""
+ hass.data.setdefault(DOMAIN, {})
+ return True
+
+
+async def async_setup_entry(hass, entry):
+ """Set up Subaru from a config entry."""
+ config = entry.data
+ websession = aiohttp_client.async_get_clientsession(hass)
+ try:
+ controller = SubaruAPI(
+ websession,
+ config[CONF_USERNAME],
+ config[CONF_PASSWORD],
+ config[CONF_DEVICE_ID],
+ config[CONF_PIN],
+ None,
+ config[CONF_COUNTRY],
+ update_interval=UPDATE_INTERVAL,
+ fetch_interval=FETCH_INTERVAL,
+ )
+ _LOGGER.debug("Using subarulink %s", controller.version)
+ await controller.connect()
+ except InvalidCredentials:
+ _LOGGER.error("Invalid account")
+ return False
+ except SubaruException as err:
+ raise ConfigEntryNotReady(err.message) from err
+
+ vehicle_info = {}
+ for vin in controller.get_vehicles():
+ vehicle_info[vin] = get_vehicle_info(controller, vin)
+
+ async def async_update_data():
+ """Fetch data from API endpoint."""
+ try:
+ return await refresh_subaru_data(entry, vehicle_info, controller)
+ except SubaruException as err:
+ raise UpdateFailed(err.message) from err
+
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name=COORDINATOR_NAME,
+ update_method=async_update_data,
+ update_interval=timedelta(seconds=FETCH_INTERVAL),
+ )
+
+ await coordinator.async_refresh()
+
+ hass.data[DOMAIN][entry.entry_id] = {
+ ENTRY_CONTROLLER: controller,
+ ENTRY_COORDINATOR: coordinator,
+ ENTRY_VEHICLES: vehicle_info,
+ }
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+ return unload_ok
+
+
+async def refresh_subaru_data(config_entry, vehicle_info, controller):
+ """
+ Refresh local data with data fetched via Subaru API.
+
+ Subaru API calls assume a server side vehicle context
+ Data fetch/update must be done for each vehicle
+ """
+ data = {}
+
+ for vehicle in vehicle_info.values():
+ vin = vehicle[VEHICLE_VIN]
+
+ # Active subscription required
+ if not vehicle[VEHICLE_HAS_SAFETY_SERVICE]:
+ continue
+
+ # Optionally send an "update" remote command to vehicle (throttled with update_interval)
+ if config_entry.options.get(CONF_UPDATE_ENABLED, False):
+ await update_subaru(vehicle, controller)
+
+ # Fetch data from Subaru servers
+ await controller.fetch(vin, force=True)
+
+ # Update our local data that will go to entity states
+ received_data = await controller.get_data(vin)
+ if received_data:
+ data[vin] = received_data
+
+ return data
+
+
+async def update_subaru(vehicle, controller):
+ """Commands remote vehicle update (polls the vehicle to update subaru API cache)."""
+ cur_time = time.time()
+ last_update = vehicle[VEHICLE_LAST_UPDATE]
+
+ if cur_time - last_update > controller.get_update_interval():
+ await controller.update(vehicle[VEHICLE_VIN], force=True)
+ vehicle[VEHICLE_LAST_UPDATE] = cur_time
+
+
+def get_vehicle_info(controller, vin):
+ """Obtain vehicle identifiers and capabilities."""
+ info = {
+ VEHICLE_VIN: vin,
+ VEHICLE_NAME: controller.vin_to_name(vin),
+ VEHICLE_HAS_EV: controller.get_ev_status(vin),
+ VEHICLE_API_GEN: controller.get_api_gen(vin),
+ VEHICLE_HAS_REMOTE_START: controller.get_res_status(vin),
+ VEHICLE_HAS_REMOTE_SERVICE: controller.get_remote_status(vin),
+ VEHICLE_HAS_SAFETY_SERVICE: controller.get_safety_status(vin),
+ VEHICLE_LAST_UPDATE: 0,
+ }
+ return info
diff --git a/homeassistant/components/subaru/config_flow.py b/homeassistant/components/subaru/config_flow.py
new file mode 100644
index 00000000000000..91ed8ad421418f
--- /dev/null
+++ b/homeassistant/components/subaru/config_flow.py
@@ -0,0 +1,157 @@
+"""Config flow for Subaru integration."""
+from datetime import datetime
+import logging
+
+from subarulink import (
+ Controller as SubaruAPI,
+ InvalidCredentials,
+ InvalidPIN,
+ SubaruException,
+)
+from subarulink.const import COUNTRY_CAN, COUNTRY_USA
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME
+from homeassistant.core import callback
+from homeassistant.helpers import aiohttp_client, config_validation as cv
+
+from .const import CONF_COUNTRY, CONF_UPDATE_ENABLED, DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+PIN_SCHEMA = vol.Schema({vol.Required(CONF_PIN): str})
+
+
+class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Subaru."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ def __init__(self):
+ """Initialize config flow."""
+ self.config_data = {CONF_PIN: None}
+ self.controller = None
+
+ async def async_step_user(self, user_input=None):
+ """Handle the start of the config flow."""
+ error = None
+
+ if user_input:
+ if user_input[CONF_USERNAME] in [
+ entry.data[CONF_USERNAME] for entry in self._async_current_entries()
+ ]:
+ return self.async_abort(reason="already_configured")
+
+ try:
+ await self.validate_login_creds(user_input)
+ except InvalidCredentials:
+ error = {"base": "invalid_auth"}
+ except SubaruException as ex:
+ _LOGGER.error("Unable to communicate with Subaru API: %s", ex.message)
+ return self.async_abort(reason="cannot_connect")
+ else:
+ if self.controller.is_pin_required():
+ return await self.async_step_pin()
+ return self.async_create_entry(
+ title=user_input[CONF_USERNAME], data=self.config_data
+ )
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ CONF_USERNAME,
+ default=user_input.get(CONF_USERNAME) if user_input else "",
+ ): str,
+ vol.Required(
+ CONF_PASSWORD,
+ default=user_input.get(CONF_PASSWORD) if user_input else "",
+ ): str,
+ vol.Required(
+ CONF_COUNTRY,
+ default=user_input.get(CONF_COUNTRY)
+ if user_input
+ else COUNTRY_USA,
+ ): vol.In([COUNTRY_CAN, COUNTRY_USA]),
+ }
+ ),
+ errors=error,
+ )
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for this handler."""
+ return OptionsFlowHandler(config_entry)
+
+ async def validate_login_creds(self, data):
+ """Validate the user input allows us to connect.
+
+ data: contains values provided by the user.
+ """
+ websession = aiohttp_client.async_get_clientsession(self.hass)
+ now = datetime.now()
+ if not data.get(CONF_DEVICE_ID):
+ data[CONF_DEVICE_ID] = int(now.timestamp())
+ date = now.strftime("%Y-%m-%d")
+ device_name = "Home Assistant: Added " + date
+
+ self.controller = SubaruAPI(
+ websession,
+ username=data[CONF_USERNAME],
+ password=data[CONF_PASSWORD],
+ device_id=data[CONF_DEVICE_ID],
+ pin=None,
+ device_name=device_name,
+ country=data[CONF_COUNTRY],
+ )
+ _LOGGER.debug(
+ "Setting up first time connection to Subaru API. This may take up to 20 seconds"
+ )
+ if await self.controller.connect():
+ _LOGGER.debug("Successfully authenticated and authorized with Subaru API")
+ self.config_data.update(data)
+
+ async def async_step_pin(self, user_input=None):
+ """Handle second part of config flow, if required."""
+ error = None
+ if user_input and self.controller.update_saved_pin(user_input[CONF_PIN]):
+ try:
+ vol.Match(r"[0-9]{4}")(user_input[CONF_PIN])
+ await self.controller.test_pin()
+ except vol.Invalid:
+ error = {"base": "bad_pin_format"}
+ except InvalidPIN:
+ error = {"base": "incorrect_pin"}
+ else:
+ _LOGGER.debug("PIN successfully tested")
+ self.config_data.update(user_input)
+ return self.async_create_entry(
+ title=self.config_data[CONF_USERNAME], data=self.config_data
+ )
+ return self.async_show_form(step_id="pin", data_schema=PIN_SCHEMA, errors=error)
+
+
+class OptionsFlowHandler(config_entries.OptionsFlow):
+ """Handle a option flow for Subaru."""
+
+ def __init__(self, config_entry: config_entries.ConfigEntry):
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(self, user_input=None):
+ """Handle options flow."""
+ if user_input is not None:
+ return self.async_create_entry(title="", data=user_input)
+
+ data_schema = vol.Schema(
+ {
+ vol.Required(
+ CONF_UPDATE_ENABLED,
+ default=self.config_entry.options.get(CONF_UPDATE_ENABLED, False),
+ ): cv.boolean,
+ }
+ )
+ return self.async_show_form(step_id="init", data_schema=data_schema)
diff --git a/homeassistant/components/subaru/const.py b/homeassistant/components/subaru/const.py
new file mode 100644
index 00000000000000..cada29edd3a726
--- /dev/null
+++ b/homeassistant/components/subaru/const.py
@@ -0,0 +1,42 @@
+"""Constants for the Subaru integration."""
+
+DOMAIN = "subaru"
+FETCH_INTERVAL = 300
+UPDATE_INTERVAL = 7200
+CONF_UPDATE_ENABLED = "update_enabled"
+CONF_COUNTRY = "country"
+
+# entry fields
+ENTRY_CONTROLLER = "controller"
+ENTRY_COORDINATOR = "coordinator"
+ENTRY_VEHICLES = "vehicles"
+
+# update coordinator name
+COORDINATOR_NAME = "subaru_data"
+
+# info fields
+VEHICLE_VIN = "vin"
+VEHICLE_NAME = "display_name"
+VEHICLE_HAS_EV = "is_ev"
+VEHICLE_API_GEN = "api_gen"
+VEHICLE_HAS_REMOTE_START = "has_res"
+VEHICLE_HAS_REMOTE_SERVICE = "has_remote"
+VEHICLE_HAS_SAFETY_SERVICE = "has_safety"
+VEHICLE_LAST_UPDATE = "last_update"
+VEHICLE_STATUS = "status"
+
+
+API_GEN_1 = "g1"
+API_GEN_2 = "g2"
+MANUFACTURER = "Subaru Corp."
+
+PLATFORMS = [
+ "sensor",
+]
+
+ICONS = {
+ "Avg Fuel Consumption": "mdi:leaf",
+ "EV Range": "mdi:ev-station",
+ "Odometer": "mdi:road-variant",
+ "Range": "mdi:gas-station",
+}
diff --git a/homeassistant/components/subaru/entity.py b/homeassistant/components/subaru/entity.py
new file mode 100644
index 00000000000000..559feeea303ca1
--- /dev/null
+++ b/homeassistant/components/subaru/entity.py
@@ -0,0 +1,34 @@
+"""Base class for all Subaru Entities."""
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import DOMAIN, MANUFACTURER, VEHICLE_NAME, VEHICLE_VIN
+
+
+class SubaruEntity(CoordinatorEntity):
+ """Representation of a Subaru Entity."""
+
+ def __init__(self, vehicle_info, coordinator):
+ """Initialize the Subaru Entity."""
+ super().__init__(coordinator)
+ self.car_name = vehicle_info[VEHICLE_NAME]
+ self.vin = vehicle_info[VEHICLE_VIN]
+ self.entity_type = "entity"
+
+ @property
+ def name(self):
+ """Return name."""
+ return f"{self.car_name} {self.entity_type}"
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique ID."""
+ return f"{self.vin}_{self.entity_type}"
+
+ @property
+ def device_info(self):
+ """Return the device_info of the device."""
+ return {
+ "identifiers": {(DOMAIN, self.vin)},
+ "name": self.car_name,
+ "manufacturer": MANUFACTURER,
+ }
diff --git a/homeassistant/components/subaru/manifest.json b/homeassistant/components/subaru/manifest.json
new file mode 100644
index 00000000000000..7a918c59f74721
--- /dev/null
+++ b/homeassistant/components/subaru/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "subaru",
+ "name": "Subaru",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/subaru",
+ "requirements": ["subarulink==0.3.12"],
+ "codeowners": ["@G-Two"]
+}
diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py
new file mode 100644
index 00000000000000..3994c9c6124fd8
--- /dev/null
+++ b/homeassistant/components/subaru/sensor.py
@@ -0,0 +1,279 @@
+"""Support for Subaru sensors."""
+import subarulink.const as sc
+
+from homeassistant.components.sensor import DEVICE_CLASSES, SensorEntity
+from homeassistant.const import (
+ DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_PRESSURE,
+ DEVICE_CLASS_TEMPERATURE,
+ DEVICE_CLASS_TIMESTAMP,
+ DEVICE_CLASS_VOLTAGE,
+ LENGTH_KILOMETERS,
+ LENGTH_MILES,
+ PERCENTAGE,
+ PRESSURE_HPA,
+ TEMP_CELSIUS,
+ TIME_MINUTES,
+ VOLT,
+ VOLUME_GALLONS,
+ VOLUME_LITERS,
+)
+from homeassistant.util.distance import convert as dist_convert
+from homeassistant.util.unit_system import (
+ IMPERIAL_SYSTEM,
+ LENGTH_UNITS,
+ PRESSURE_UNITS,
+ TEMPERATURE_UNITS,
+)
+from homeassistant.util.volume import convert as vol_convert
+
+from .const import (
+ API_GEN_2,
+ DOMAIN,
+ ENTRY_COORDINATOR,
+ ENTRY_VEHICLES,
+ ICONS,
+ VEHICLE_API_GEN,
+ VEHICLE_HAS_EV,
+ VEHICLE_HAS_SAFETY_SERVICE,
+ VEHICLE_STATUS,
+)
+from .entity import SubaruEntity
+
+L_PER_GAL = vol_convert(1, VOLUME_GALLONS, VOLUME_LITERS)
+KM_PER_MI = dist_convert(1, LENGTH_MILES, LENGTH_KILOMETERS)
+
+# Fuel Economy Constants
+FUEL_CONSUMPTION_L_PER_100KM = "L/100km"
+FUEL_CONSUMPTION_MPG = "mi/gal"
+FUEL_CONSUMPTION_UNITS = [FUEL_CONSUMPTION_L_PER_100KM, FUEL_CONSUMPTION_MPG]
+
+SENSOR_TYPE = "type"
+SENSOR_CLASS = "class"
+SENSOR_FIELD = "field"
+SENSOR_UNITS = "units"
+
+# Sensor data available to "Subaru Safety Plus" subscribers with Gen1 or Gen2 vehicles
+SAFETY_SENSORS = [
+ {
+ SENSOR_TYPE: "Odometer",
+ SENSOR_CLASS: None,
+ SENSOR_FIELD: sc.ODOMETER,
+ SENSOR_UNITS: LENGTH_KILOMETERS,
+ },
+]
+
+# Sensor data available to "Subaru Safety Plus" subscribers with Gen2 vehicles
+API_GEN_2_SENSORS = [
+ {
+ SENSOR_TYPE: "Avg Fuel Consumption",
+ SENSOR_CLASS: None,
+ SENSOR_FIELD: sc.AVG_FUEL_CONSUMPTION,
+ SENSOR_UNITS: FUEL_CONSUMPTION_L_PER_100KM,
+ },
+ {
+ SENSOR_TYPE: "Range",
+ SENSOR_CLASS: None,
+ SENSOR_FIELD: sc.DIST_TO_EMPTY,
+ SENSOR_UNITS: LENGTH_KILOMETERS,
+ },
+ {
+ SENSOR_TYPE: "Tire Pressure FL",
+ SENSOR_CLASS: DEVICE_CLASS_PRESSURE,
+ SENSOR_FIELD: sc.TIRE_PRESSURE_FL,
+ SENSOR_UNITS: PRESSURE_HPA,
+ },
+ {
+ SENSOR_TYPE: "Tire Pressure FR",
+ SENSOR_CLASS: DEVICE_CLASS_PRESSURE,
+ SENSOR_FIELD: sc.TIRE_PRESSURE_FR,
+ SENSOR_UNITS: PRESSURE_HPA,
+ },
+ {
+ SENSOR_TYPE: "Tire Pressure RL",
+ SENSOR_CLASS: DEVICE_CLASS_PRESSURE,
+ SENSOR_FIELD: sc.TIRE_PRESSURE_RL,
+ SENSOR_UNITS: PRESSURE_HPA,
+ },
+ {
+ SENSOR_TYPE: "Tire Pressure RR",
+ SENSOR_CLASS: DEVICE_CLASS_PRESSURE,
+ SENSOR_FIELD: sc.TIRE_PRESSURE_RR,
+ SENSOR_UNITS: PRESSURE_HPA,
+ },
+ {
+ SENSOR_TYPE: "External Temp",
+ SENSOR_CLASS: DEVICE_CLASS_TEMPERATURE,
+ SENSOR_FIELD: sc.EXTERNAL_TEMP,
+ SENSOR_UNITS: TEMP_CELSIUS,
+ },
+ {
+ SENSOR_TYPE: "12V Battery Voltage",
+ SENSOR_CLASS: DEVICE_CLASS_VOLTAGE,
+ SENSOR_FIELD: sc.BATTERY_VOLTAGE,
+ SENSOR_UNITS: VOLT,
+ },
+]
+
+# Sensor data available to "Subaru Safety Plus" subscribers with PHEV vehicles
+EV_SENSORS = [
+ {
+ SENSOR_TYPE: "EV Range",
+ SENSOR_CLASS: None,
+ SENSOR_FIELD: sc.EV_DISTANCE_TO_EMPTY,
+ SENSOR_UNITS: LENGTH_MILES,
+ },
+ {
+ SENSOR_TYPE: "EV Battery Level",
+ SENSOR_CLASS: DEVICE_CLASS_BATTERY,
+ SENSOR_FIELD: sc.EV_STATE_OF_CHARGE_PERCENT,
+ SENSOR_UNITS: PERCENTAGE,
+ },
+ {
+ SENSOR_TYPE: "EV Time to Full Charge",
+ SENSOR_CLASS: DEVICE_CLASS_TIMESTAMP,
+ SENSOR_FIELD: sc.EV_TIME_TO_FULLY_CHARGED,
+ SENSOR_UNITS: TIME_MINUTES,
+ },
+]
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the Subaru sensors by config_entry."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id][ENTRY_COORDINATOR]
+ vehicle_info = hass.data[DOMAIN][config_entry.entry_id][ENTRY_VEHICLES]
+ entities = []
+ for vin in vehicle_info:
+ entities.extend(create_vehicle_sensors(vehicle_info[vin], coordinator))
+ async_add_entities(entities, True)
+
+
+def create_vehicle_sensors(vehicle_info, coordinator):
+ """Instantiate all available sensors for the vehicle."""
+ sensors_to_add = []
+ if vehicle_info[VEHICLE_HAS_SAFETY_SERVICE]:
+ sensors_to_add.extend(SAFETY_SENSORS)
+
+ if vehicle_info[VEHICLE_API_GEN] == API_GEN_2:
+ sensors_to_add.extend(API_GEN_2_SENSORS)
+
+ if vehicle_info[VEHICLE_HAS_EV]:
+ sensors_to_add.extend(EV_SENSORS)
+
+ return [
+ SubaruSensor(
+ vehicle_info,
+ coordinator,
+ s[SENSOR_TYPE],
+ s[SENSOR_CLASS],
+ s[SENSOR_FIELD],
+ s[SENSOR_UNITS],
+ )
+ for s in sensors_to_add
+ ]
+
+
+class SubaruSensor(SubaruEntity, SensorEntity):
+ """Class for Subaru sensors."""
+
+ def __init__(
+ self, vehicle_info, coordinator, entity_type, sensor_class, data_field, api_unit
+ ):
+ """Initialize the sensor."""
+ super().__init__(vehicle_info, coordinator)
+ self.hass_type = "sensor"
+ self.current_value = None
+ self.entity_type = entity_type
+ self.sensor_class = sensor_class
+ self.data_field = data_field
+ self.api_unit = api_unit
+
+ @property
+ def device_class(self):
+ """Return the class of this device, from component DEVICE_CLASSES."""
+ if self.sensor_class in DEVICE_CLASSES:
+ return self.sensor_class
+ return None
+
+ @property
+ def icon(self):
+ """Return the icon of the sensor."""
+ if not self.device_class:
+ return ICONS.get(self.entity_type)
+ return None
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ self.current_value = self.get_current_value()
+
+ if self.current_value is None:
+ return None
+
+ if self.api_unit in TEMPERATURE_UNITS:
+ return round(
+ self.hass.config.units.temperature(self.current_value, self.api_unit), 1
+ )
+
+ if self.api_unit in LENGTH_UNITS:
+ return round(
+ self.hass.config.units.length(self.current_value, self.api_unit), 1
+ )
+
+ if (
+ self.api_unit in PRESSURE_UNITS
+ and self.hass.config.units == IMPERIAL_SYSTEM
+ ):
+ return round(
+ self.hass.config.units.pressure(self.current_value, self.api_unit),
+ 1,
+ )
+
+ if (
+ self.api_unit in FUEL_CONSUMPTION_UNITS
+ and self.hass.config.units == IMPERIAL_SYSTEM
+ ):
+ return round((100.0 * L_PER_GAL) / (KM_PER_MI * self.current_value), 1)
+
+ return self.current_value
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit_of_measurement of the device."""
+ if self.api_unit in TEMPERATURE_UNITS:
+ return self.hass.config.units.temperature_unit
+
+ if self.api_unit in LENGTH_UNITS:
+ return self.hass.config.units.length_unit
+
+ if self.api_unit in PRESSURE_UNITS:
+ if self.hass.config.units == IMPERIAL_SYSTEM:
+ return self.hass.config.units.pressure_unit
+ return PRESSURE_HPA
+
+ if self.api_unit in FUEL_CONSUMPTION_UNITS:
+ if self.hass.config.units == IMPERIAL_SYSTEM:
+ return FUEL_CONSUMPTION_MPG
+ return FUEL_CONSUMPTION_L_PER_100KM
+
+ return self.api_unit
+
+ @property
+ def available(self):
+ """Return if entity is available."""
+ last_update_success = super().available
+ if last_update_success and self.vin not in self.coordinator.data:
+ return False
+ return last_update_success
+
+ def get_current_value(self):
+ """Get raw value from the coordinator."""
+ value = self.coordinator.data[self.vin][VEHICLE_STATUS].get(self.data_field)
+ if value in sc.BAD_SENSOR_VALUES:
+ value = None
+ if isinstance(value, str):
+ if "." in value:
+ value = float(value)
+ else:
+ value = int(value)
+ return value
diff --git a/homeassistant/components/subaru/strings.json b/homeassistant/components/subaru/strings.json
new file mode 100644
index 00000000000000..ea9df082f3a8df
--- /dev/null
+++ b/homeassistant/components/subaru/strings.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "Subaru Starlink Configuration",
+ "description": "Please enter your MySubaru credentials\nNOTE: Initial setup may take up to 30 seconds",
+ "data": {
+ "username": "[%key:common::config_flow::data::username%]",
+ "password": "[%key:common::config_flow::data::password%]",
+ "country": "Select country"
+ }
+ },
+ "pin": {
+ "title": "Subaru Starlink Configuration",
+ "description": "Please enter your MySubaru PIN\nNOTE: All vehicles in account must have the same PIN",
+ "data": {
+ "pin": "PIN"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "incorrect_pin": "Incorrect PIN",
+ "bad_pin_format": "PIN should be 4 digits"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ }
+ },
+
+ "options": {
+ "step": {
+ "init": {
+ "title": "Subaru Starlink Options",
+ "description": "When enabled, vehicle polling will send a remote command to your vehicle every 2 hours to obtain new sensor data. Without vehicle polling, new sensor data is only received when the vehicle automatically pushes data (normally after engine shutdown).",
+ "data": {
+ "update_enabled": "Enable vehicle polling"
+ }
+ }
+ }
+ }
+}
diff --git a/homeassistant/components/subaru/translations/bg.json b/homeassistant/components/subaru/translations/bg.json
new file mode 100644
index 00000000000000..c3dc8345ecd9e8
--- /dev/null
+++ b/homeassistant/components/subaru/translations/bg.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "country": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0434\u044a\u0440\u0436\u0430\u0432\u0430",
+ "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/subaru/translations/ca.json b/homeassistant/components/subaru/translations/ca.json
new file mode 100644
index 00000000000000..51cd44b4ce2b1b
--- /dev/null
+++ b/homeassistant/components/subaru/translations/ca.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El compte ja ha estat configurat",
+ "cannot_connect": "Ha fallat la connexi\u00f3"
+ },
+ "error": {
+ "bad_pin_format": "El PIN ha de tenir 4 d\u00edgits",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "incorrect_pin": "PIN incorrecte",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "PIN"
+ },
+ "description": "Introdueix el teu PIN de MySubaru\nNOTA: tots els vehicles associats a un compte han de tenir el mateix PIN",
+ "title": "Configuraci\u00f3 de Subaru Starlink"
+ },
+ "user": {
+ "data": {
+ "country": "Selecciona un pa\u00eds",
+ "password": "Contrasenya",
+ "username": "Nom d'usuari"
+ },
+ "description": "Introdueix les teves credencials de MySubaru\nNOTA: la primera configuraci\u00f3 pot tardar fins a 30 segons",
+ "title": "Configuraci\u00f3 de Subaru Starlink"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "update_enabled": "Activa el sondeig de vehicle"
+ },
+ "description": "Quan estigui activat, el sondeig de vehicle enviar\u00e0 una ordre al vehicle cada 2 hores per tal d'obtenir noves dades. Sense el sondeig de vehicle, les noves dades nom\u00e9s es rebr\u00e0n quan el vehicle envia autom\u00e0ticament les dades (normalment despr\u00e9s de l'aturada del motor).",
+ "title": "Opcions de Subaru Starlink"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/subaru/translations/cs.json b/homeassistant/components/subaru/translations/cs.json
new file mode 100644
index 00000000000000..ee3bf7347ca256
--- /dev/null
+++ b/homeassistant/components/subaru/translations/cs.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u00da\u010det je ji\u017e nastaven",
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit"
+ },
+ "error": {
+ "bad_pin_format": "PIN by m\u011bl m\u00edt 4 \u010d\u00edslice",
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
+ "incorrect_pin": "Nespr\u00e1vn\u00fd PIN",
+ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
+ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "PIN k\u00f3d"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "Heslo",
+ "username": "U\u017eivatelsk\u00e9 jm\u00e9no"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/subaru/translations/de.json b/homeassistant/components/subaru/translations/de.json
new file mode 100644
index 00000000000000..9c4a0bdf535efe
--- /dev/null
+++ b/homeassistant/components/subaru/translations/de.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
+ "error": {
+ "bad_pin_format": "Die PIN sollte 4-stellig sein",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "incorrect_pin": "Falsche PIN",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "PIN"
+ },
+ "description": "Bitte gib deinen MySubaru-PIN ein\nHINWEIS: Alle Fahrzeuge im Konto m\u00fcssen dieselbe PIN haben"
+ },
+ "user": {
+ "data": {
+ "country": "Land ausw\u00e4hlen",
+ "password": "Passwort",
+ "username": "Benutzername"
+ },
+ "description": "Bitte gib deine MySubaru-Anmeldedaten ein\nHINWEIS: Die Ersteinrichtung kann bis zu 30 Sekunden dauern"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/subaru/translations/el.json b/homeassistant/components/subaru/translations/el.json
new file mode 100644
index 00000000000000..17ede1af4e74d3
--- /dev/null
+++ b/homeassistant/components/subaru/translations/el.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "error": {
+ "unknown": "\u0391\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1"
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "PIN"
+ },
+ "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Subaru Starlink"
+ },
+ "user": {
+ "data": {
+ "country": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c7\u03ce\u03c1\u03b1\u03c2",
+ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2",
+ "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7"
+ },
+ "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03ac \u03c3\u03b1\u03c2 \u03c4\u03bf\u03c5 MySubaru\n\u03a3\u0397\u039c\u0395\u0399\u03a9\u03a3\u0397: \u0397 \u03b1\u03c1\u03c7\u03b9\u03ba\u03ae \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03bd\u03b4\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c1\u03ba\u03ad\u03c3\u03b5\u03b9 \u03ad\u03c9\u03c2 \u03ba\u03b1\u03b9 30 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1",
+ "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Subaru Starlink"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "update_enabled": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03bf\u03c7\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2"
+ },
+ "description": "\u038c\u03c4\u03b1\u03bd \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af, \u03b7 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7 \u03bf\u03c7\u03b7\u03bc\u03ac\u03c4\u03c9\u03bd \u03b8\u03b1 \u03c3\u03c4\u03ad\u03bb\u03bd\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae \u03c3\u03c4\u03bf \u03cc\u03c7\u03b7\u03bc\u03ac \u03c3\u03b1\u03c2 \u03ba\u03ac\u03b8\u03b5 2 \u03ce\u03c1\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c0\u03bf\u03ba\u03c4\u03ae\u03c3\u03b5\u03b9 \u03c4\u03b1 \u03bd\u03ad\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c4\u03c9\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03c9\u03bd. \u03a7\u03c9\u03c1\u03af\u03c2 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7 \u03bf\u03c7\u03b7\u03bc\u03ac\u03c4\u03c9\u03bd, \u03c4\u03b1 \u03bd\u03ad\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03c9\u03bd \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03cc\u03bd\u03bf \u03cc\u03c4\u03b1\u03bd \u03c4\u03bf \u03cc\u03c7\u03b7\u03bc\u03b1 \u03c9\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 (\u03ba\u03b1\u03bd\u03bf\u03bd\u03b9\u03ba\u03ac \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc \u03c4\u03b7 \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03bc\u03b7\u03c7\u03b1\u03bd\u03ce\u03bd).",
+ "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 Subaru Starlink"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/subaru/translations/en.json b/homeassistant/components/subaru/translations/en.json
new file mode 100644
index 00000000000000..ea15ff0055206c
--- /dev/null
+++ b/homeassistant/components/subaru/translations/en.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Account is already configured",
+ "cannot_connect": "Failed to connect"
+ },
+ "error": {
+ "bad_pin_format": "PIN should be 4 digits",
+ "cannot_connect": "Failed to connect",
+ "incorrect_pin": "Incorrect PIN",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "PIN"
+ },
+ "description": "Please enter your MySubaru PIN\nNOTE: All vehicles in account must have the same PIN",
+ "title": "Subaru Starlink Configuration"
+ },
+ "user": {
+ "data": {
+ "country": "Select country",
+ "password": "Password",
+ "username": "Username"
+ },
+ "description": "Please enter your MySubaru credentials\nNOTE: Initial setup may take up to 30 seconds",
+ "title": "Subaru Starlink Configuration"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "update_enabled": "Enable vehicle polling"
+ },
+ "description": "When enabled, vehicle polling will send a remote command to your vehicle every 2 hours to obtain new sensor data. Without vehicle polling, new sensor data is only received when the vehicle automatically pushes data (normally after engine shutdown).",
+ "title": "Subaru Starlink Options"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/subaru/translations/es.json b/homeassistant/components/subaru/translations/es.json
new file mode 100644
index 00000000000000..deccc23c75dbb2
--- /dev/null
+++ b/homeassistant/components/subaru/translations/es.json
@@ -0,0 +1,37 @@
+{
+ "config": {
+ "error": {
+ "bad_pin_format": "El PIN debe tener 4 d\u00edgitos",
+ "incorrect_pin": "PIN incorrecto"
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "PIN"
+ },
+ "description": "Por favor, introduzca su PIN de MySubaru\nNOTA: Todos los veh\u00edculos de la cuenta deben tener el mismo PIN",
+ "title": "Configuraci\u00f3n de Subaru Starlink"
+ },
+ "user": {
+ "data": {
+ "country": "Seleccionar pa\u00eds",
+ "password": "Contrase\u00f1a",
+ "username": "Nombre de usuario"
+ },
+ "description": "Por favor, introduzca sus credenciales de MySubaru\nNOTA: La configuraci\u00f3n inicial puede tardar hasta 30 segundos",
+ "title": "Configuraci\u00f3n de Subaru Starlink"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "update_enabled": "Habilitar el sondeo de veh\u00edculos"
+ },
+ "description": "Cuando est\u00e1 habilitado, el sondeo de veh\u00edculos enviar\u00e1 un comando remoto a su veh\u00edculo cada 2 horas para obtener nuevos datos del sensor. Sin sondeo del veh\u00edculo, los nuevos datos del sensor solo se reciben cuando el veh\u00edculo env\u00eda datos autom\u00e1ticamente (normalmente despu\u00e9s de apagar el motor).",
+ "title": "Opciones de Subaru Starlink"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/subaru/translations/et.json b/homeassistant/components/subaru/translations/et.json
new file mode 100644
index 00000000000000..30dd690f84939f
--- /dev/null
+++ b/homeassistant/components/subaru/translations/et.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kasutaja on juba seadistatud",
+ "cannot_connect": "\u00dchendamine nurjus"
+ },
+ "error": {
+ "bad_pin_format": "PIN-kood peaks olema 4-kohaline",
+ "cannot_connect": "\u00dchendamine nurjus",
+ "incorrect_pin": "Vale PIN-kood",
+ "invalid_auth": "Vigane autentimine",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "PIN"
+ },
+ "description": "Sisesta oma MySubaru PIN-kood\n M\u00c4RKUS. K\u00f5igil kontol olevatel s\u00f5idukitel peab olema sama PIN-kood",
+ "title": "Subaru Starlinki konfiguratsioon"
+ },
+ "user": {
+ "data": {
+ "country": "Vali riik",
+ "password": "Salas\u00f5na",
+ "username": "Kasutajanimi"
+ },
+ "description": "Sisesta oma MySubaru mandaat\n M\u00c4RKUS. Esmane seadistamine v\u00f5ib v\u00f5tta kuni 30 sekundit",
+ "title": "Subaru Starlinki konfiguratsioon"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "update_enabled": "Luba s\u00f5iduki k\u00fcsitlus"
+ },
+ "description": "Kui see on lubatud, saadetakse k\u00fcsitlus ts\u00f5idukile iga kahe tunni j\u00e4rel, et saada uusi anduriandmeid. Ilma s\u00f5iduki valimiseta saadakse uusi anduriandmeid ainult siis, kui s\u00f5iduk automaatselt andmeid edastab (tavaliselt p\u00e4rast mootori seiskamist).",
+ "title": "Subaru Starlinki valikud"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/subaru/translations/fr.json b/homeassistant/components/subaru/translations/fr.json
new file mode 100644
index 00000000000000..25544534297718
--- /dev/null
+++ b/homeassistant/components/subaru/translations/fr.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9",
+ "cannot_connect": "\u00c9chec de connexion"
+ },
+ "error": {
+ "bad_pin_format": "Le code PIN doit \u00eatre compos\u00e9 de 4 chiffres",
+ "cannot_connect": "\u00c9chec de connexion",
+ "incorrect_pin": "PIN incorrect",
+ "invalid_auth": "Authentification invalide",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "PIN"
+ },
+ "description": "Veuillez entrer votre NIP MySubaru\nREMARQUE : Tous les v\u00e9hicules en compte doivent avoir le m\u00eame NIP",
+ "title": "Configuration de Subaru Starlink"
+ },
+ "user": {
+ "data": {
+ "country": "Choisissez le pays",
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur"
+ },
+ "description": "Veuillez saisir vos identifiants MySubaru\n REMARQUE: la configuration initiale peut prendre jusqu'\u00e0 30 secondes",
+ "title": "Configuration de Subaru Starlink"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "update_enabled": "Activer l'interrogation des v\u00e9hicules"
+ },
+ "description": "Lorsqu'elle est activ\u00e9e, l'interrogation du v\u00e9hicule enverra une commande \u00e0 distance \u00e0 votre v\u00e9hicule toutes les 2 heures pour obtenir de nouvelles donn\u00e9es de capteur. Sans interrogation du v\u00e9hicule, les nouvelles donn\u00e9es de capteur ne sont re\u00e7ues que lorsque le v\u00e9hicule pousse automatiquement les donn\u00e9es (normalement apr\u00e8s l'arr\u00eat du moteur).",
+ "title": "Options de Subaru Starlink"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/subaru/translations/hu.json b/homeassistant/components/subaru/translations/hu.json
new file mode 100644
index 00000000000000..d92ca24b7a1e06
--- /dev/null
+++ b/homeassistant/components/subaru/translations/hu.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "error": {
+ "bad_pin_format": "A PIN-nek 4 sz\u00e1mjegy\u0171nek kell lennie",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "incorrect_pin": "Helytelen PIN",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "PIN"
+ },
+ "title": "Subaru Starlink konfigur\u00e1ci\u00f3"
+ },
+ "user": {
+ "data": {
+ "country": "V\u00e1lassz orsz\u00e1got",
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ },
+ "title": "Subaru Starlink konfigur\u00e1ci\u00f3"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "Subaru Starlink be\u00e1ll\u00edt\u00e1sok"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/subaru/translations/id.json b/homeassistant/components/subaru/translations/id.json
new file mode 100644
index 00000000000000..1ae1506fe099ce
--- /dev/null
+++ b/homeassistant/components/subaru/translations/id.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung"
+ },
+ "error": {
+ "bad_pin_format": "PIN harus terdiri dari 4 angka",
+ "cannot_connect": "Gagal terhubung",
+ "incorrect_pin": "PIN salah",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "PIN"
+ },
+ "description": "Masukkan PIN MySubaru Anda\nCATATAN: Semua kendaraan dalam akun harus memiliki PIN yang sama",
+ "title": "Konfigurasi Subaru Starlink"
+ },
+ "user": {
+ "data": {
+ "country": "Pilih negara",
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "description": "Masukkan kredensial MySubaru Anda\nCATATAN: Penyiapan awal mungkin memerlukan waktu hingga 30 detik",
+ "title": "Konfigurasi Subaru Starlink"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "update_enabled": "Aktifkan polling kendaraan"
+ },
+ "description": "Ketika diaktifkan, polling kendaraan akan mengirim perintah jarak jauh ke kendaraan Anda setiap 2 jam untuk mendapatkan data sensor baru. Tanpa polling kendaraan, data sensor baru hanya diterima ketika kendaraan mengirimkan data secara otomatis (umumnya setelah mesin dimatikan).",
+ "title": "Opsi Subaru Starlink"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/subaru/translations/it.json b/homeassistant/components/subaru/translations/it.json
new file mode 100644
index 00000000000000..c585834f12ba98
--- /dev/null
+++ b/homeassistant/components/subaru/translations/it.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato",
+ "cannot_connect": "Impossibile connettersi"
+ },
+ "error": {
+ "bad_pin_format": "Il PIN deve essere di 4 cifre",
+ "cannot_connect": "Impossibile connettersi",
+ "incorrect_pin": "PIN errato",
+ "invalid_auth": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "PIN"
+ },
+ "description": "Inserisci il tuo PIN MySubaru\nNOTA: tutti i veicoli nell'account devono avere lo stesso PIN",
+ "title": "Configurazione Subaru Starlink"
+ },
+ "user": {
+ "data": {
+ "country": "Seleziona il paese",
+ "password": "Password",
+ "username": "Nome utente"
+ },
+ "description": "Inserisci le tue credenziali MySubaru\nNOTA: la configurazione iniziale pu\u00f2 richiedere fino a 30 secondi",
+ "title": "Configurazione Subaru Starlink"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "update_enabled": "Abilita la verifica ciclica del veicolo"
+ },
+ "description": "Quando abilitata, la verifica ciclica del veicolo invier\u00e0 un comando remoto d'interrogazione al tuo veicolo ogni 2 ore per ottenere nuovi dati del sensore. Senza l'interrogazione del veicolo i nuovi dati del sensore verranno ricevuti solo quando il veicolo invier\u00e0 automaticamente i dati (normalmente dopo lo spegnimento del motore).",
+ "title": "Opzioni Subaru Starlink"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/subaru/translations/ko.json b/homeassistant/components/subaru/translations/ko.json
new file mode 100644
index 00000000000000..8fe12309812498
--- /dev/null
+++ b/homeassistant/components/subaru/translations/ko.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "bad_pin_format": "PIN\uc740 4\uc790\ub9ac\uc5ec\uc57c \ud569\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "incorrect_pin": "PIN\uc774 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "PIN"
+ },
+ "description": "MySubaru PIN\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694\n\ucc38\uace0: \uacc4\uc815\uc758 \ubaa8\ub4e0 \ucc28\ub7c9\uc740 \ub3d9\uc77c\ud55c PIN \uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4",
+ "title": "Subaru Starlink \uad6c\uc131"
+ },
+ "user": {
+ "data": {
+ "country": "\uad6d\uac00 \uc120\ud0dd\ud558\uae30",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ },
+ "description": "MySubaru \uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694\n\ucc38\uace0: \ucd08\uae30 \uc124\uc815\uc5d0\ub294 \ucd5c\ub300 30\ucd08 \uc815\ub3c4 \uac78\ub9b4 \uc218 \uc788\uc2b5\ub2c8\ub2e4",
+ "title": "Subaru Starlink \uad6c\uc131"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "update_enabled": "\ucc28\ub7c9 \ud3f4\ub9c1 \ud65c\uc131\ud654\ud558\uae30"
+ },
+ "description": "\ud65c\uc131\ud654\ub418\uba74 \ucc28\ub7c9 \ud3f4\ub9c1\uc740 \ucc28\ub7c9\uc5d0 2\uc2dc\uac04\ub9c8\ub2e4 \uc6d0\uaca9 \uba85\ub839\uc744 \uc804\uc1a1\ud558\uc5ec \uc0c8\ub85c\uc6b4 \uc13c\uc11c \ub370\uc774\ud130\ub97c \ubc1b\uc544\uc635\ub2c8\ub2e4. \ucc28\ub7c9 \ud3f4\ub9c1\uc774 \uc5c6\uc73c\uba74 \uc0c8\ub85c\uc6b4 \uc13c\uc11c \ub370\uc774\ud130\ub294 \ucc28\ub7c9\uc774 \uc790\ub3d9\uc73c\ub85c \ub370\uc774\ud130\ub97c \ubcf4\ub0bc \ub54c\ub9cc \uc218\uc2e0\ub429\ub2c8\ub2e4(\uc77c\ubc18\uc801\uc73c\ub85c \uc5d4\uc9c4\uc774 \uaebc\uc9c4 \ud6c4).",
+ "title": "Subaru Starlink \uc635\uc158"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/subaru/translations/nl.json b/homeassistant/components/subaru/translations/nl.json
new file mode 100644
index 00000000000000..339c990ca0bb68
--- /dev/null
+++ b/homeassistant/components/subaru/translations/nl.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Account is al geconfigureerd",
+ "cannot_connect": "Kan geen verbinding maken"
+ },
+ "error": {
+ "bad_pin_format": "De pincode moet uit 4 cijfers bestaan",
+ "cannot_connect": "Kan geen verbinding maken",
+ "incorrect_pin": "Onjuiste PIN",
+ "invalid_auth": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "PIN"
+ },
+ "description": "Voer uw MySubaru-pincode in\n OPMERKING: Alle voertuigen in een account moeten dezelfde pincode hebben",
+ "title": "Subaru Starlink Configuratie"
+ },
+ "user": {
+ "data": {
+ "country": "Selecteer land",
+ "password": "Wachtwoord",
+ "username": "Gebruikersnaam"
+ },
+ "description": "Voer uw MySubaru inloggegevens in\nOPMERKING: De eerste installatie kan tot 30 seconden duren",
+ "title": "Subaru Starlink-configuratie"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "update_enabled": "Voertuigpeiling inschakelen"
+ },
+ "description": "Wanneer deze optie is ingeschakeld, zal voertuigpeiling om de 2 uur een opdracht op afstand naar uw voertuig sturen om nieuwe sensorgegevens te verkrijgen. Zonder voertuigpeiling worden nieuwe sensorgegevens alleen ontvangen wanneer het voertuig automatisch gegevens doorstuurt (normaal gesproken na het uitschakelen van de motor).",
+ "title": "Subaru Starlink-opties"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/subaru/translations/no.json b/homeassistant/components/subaru/translations/no.json
new file mode 100644
index 00000000000000..25b0f7bec295b2
--- /dev/null
+++ b/homeassistant/components/subaru/translations/no.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kontoen er allerede konfigurert",
+ "cannot_connect": "Tilkobling mislyktes"
+ },
+ "error": {
+ "bad_pin_format": "PIN-koden skal best\u00e5 av fire sifre",
+ "cannot_connect": "Tilkobling mislyktes",
+ "incorrect_pin": "Feil PIN",
+ "invalid_auth": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "PIN"
+ },
+ "description": "Vennligst skriv inn MySubaru PIN-koden\n MERKNAD: Alle kj\u00f8ret\u00f8yer som er kontoen m\u00e5 ha samme PIN-kode",
+ "title": "Subaru Starlink-konfigurasjon"
+ },
+ "user": {
+ "data": {
+ "country": "Velg land",
+ "password": "Passord",
+ "username": "Brukernavn"
+ },
+ "description": "Vennligst skriv inn MySubaru-legitimasjonen din\n MERK: F\u00f8rste oppsett kan ta opptil 30 sekunder",
+ "title": "Subaru Starlink-konfigurasjon"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "update_enabled": "Aktiver polling av kj\u00f8ret\u00f8y"
+ },
+ "description": "N\u00e5r dette er aktivert, sender polling av kj\u00f8ret\u00f8y en fjernkommando til kj\u00f8ret\u00f8yet annenhver time for \u00e5 skaffe nye sensordata. Uten kj\u00f8ret\u00f8yoppm\u00e5ling mottas nye sensordata bare n\u00e5r kj\u00f8ret\u00f8yet automatisk skyver data (normalt etter motorstans).",
+ "title": "Subaru Starlink alternativer"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/subaru/translations/pl.json b/homeassistant/components/subaru/translations/pl.json
new file mode 100644
index 00000000000000..99415cdeea710f
--- /dev/null
+++ b/homeassistant/components/subaru/translations/pl.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto jest ju\u017c skonfigurowane",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ },
+ "error": {
+ "bad_pin_format": "PIN powinien sk\u0142ada\u0107 si\u0119 z 4 cyfr",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "incorrect_pin": "Nieprawid\u0142owy PIN",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "PIN"
+ },
+ "description": "Wprowad\u017a sw\u00f3j PIN dla MySubaru\nUWAGA: Wszystkie pojazdy na koncie musz\u0105 mie\u0107 ten sam kod PIN",
+ "title": "Konfiguracja Subaru Starlink"
+ },
+ "user": {
+ "data": {
+ "country": "Wybierz kraj",
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ },
+ "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce MySubaru\nUWAGA: Pocz\u0105tkowa konfiguracja mo\u017ce zaj\u0105\u0107 do 30 sekund",
+ "title": "Konfiguracja Subaru Starlink"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "update_enabled": "W\u0142\u0105cz odpytywanie pojazdu"
+ },
+ "description": "Po w\u0142\u0105czeniu, odpytywanie pojazdu b\u0119dzie co 2 godziny wysy\u0142a\u0107 zdalne polecenie do pojazdu w celu uzyskania nowych danych z czujnika. Bez odpytywania pojazdu, nowe dane z czujnika s\u0105 odbierane tylko wtedy, gdy pojazd automatycznie przesy\u0142a dane (zwykle po wy\u0142\u0105czeniu silnika).",
+ "title": "Opcje Subaru Starlink"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/subaru/translations/pt.json b/homeassistant/components/subaru/translations/pt.json
new file mode 100644
index 00000000000000..7f3e1ec8e3b6fb
--- /dev/null
+++ b/homeassistant/components/subaru/translations/pt.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Conta j\u00e1 configurada",
+ "cannot_connect": "Falha na liga\u00e7\u00e3o"
+ },
+ "error": {
+ "cannot_connect": "Falha na liga\u00e7\u00e3o",
+ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida",
+ "unknown": "Erro inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Palavra-passe",
+ "username": "Nome de Utilizador"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/subaru/translations/ru.json b/homeassistant/components/subaru/translations/ru.json
new file mode 100644
index 00000000000000..c414ebb385fece
--- /dev/null
+++ b/homeassistant/components/subaru/translations/ru.json
@@ -0,0 +1,44 @@
+{
+ "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.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
+ },
+ "error": {
+ "bad_pin_format": "PIN-\u043a\u043e\u0434 \u0434\u043e\u043b\u0436\u0435\u043d \u0441\u043e\u0441\u0442\u043e\u044f\u0442\u044c \u0438\u0437 4 \u0446\u0438\u0444\u0440.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "incorrect_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "PIN-\u043a\u043e\u0434"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434 MySubaru.\n\u0412\u0441\u0435 \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u0438 \u0432 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u043c\u0435\u0442\u044c \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u044b\u0439 PIN-\u043a\u043e\u0434.",
+ "title": "Subaru Starlink"
+ },
+ "user": {
+ "data": {
+ "country": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0440\u0430\u043d\u0443",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 MySubaru.\n\u041f\u0435\u0440\u0432\u043e\u043d\u0430\u0447\u0430\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u0434\u043e 30 \u0441\u0435\u043a\u0443\u043d\u0434.",
+ "title": "Subaru Starlink"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "update_enabled": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u043f\u0440\u043e\u0441 \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u0435\u0439"
+ },
+ "description": "\u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d, Home Assistant \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u043a\u043e\u043c\u0430\u043d\u0434\u0443 \u043d\u0430 \u0412\u0430\u0448 \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u044c \u043a\u0430\u0436\u0434\u044b\u0435 2 \u0447\u0430\u0441\u0430 \u0434\u043b\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u043d\u043e\u0432\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445. \u0411\u0435\u0437 \u043e\u043f\u0440\u043e\u0441\u0430 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u043e\u0433\u043e \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430 \u0434\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0431\u0443\u0434\u0443\u0442 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0433\u0434\u0430 \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u044c \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e \u0438\u043d\u0438\u0446\u0438\u0438\u0440\u0443\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 (\u043e\u0431\u044b\u0447\u043d\u043e \u043f\u043e\u0441\u043b\u0435 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0434\u0432\u0438\u0433\u0430\u0442\u0435\u043b\u044f).",
+ "title": "Subaru Starlink"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/subaru/translations/zh-Hant.json b/homeassistant/components/subaru/translations/zh-Hant.json
new file mode 100644
index 00000000000000..22eaa589fe2e79
--- /dev/null
+++ b/homeassistant/components/subaru/translations/zh-Hant.json
@@ -0,0 +1,44 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
+ },
+ "error": {
+ "bad_pin_format": "PIN \u78bc\u61c9\u8a72\u70ba 4 \u4f4d\u6578\u5b57",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "incorrect_pin": "PIN \u78bc\u932f\u8aa4",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "pin": {
+ "data": {
+ "pin": "PIN"
+ },
+ "description": "\u8acb\u8f38\u5165 MySubaru PIN \u78bc\n\u6ce8\u610f\uff1a\u6240\u4ee5\u5e33\u865f\u5167\u8eca\u8f1b\u90fd\u5fc5\u9808\u4f7f\u7528\u76f8\u540c PIN \u78bc",
+ "title": "Subaru Starlink \u8a2d\u5b9a"
+ },
+ "user": {
+ "data": {
+ "country": "\u9078\u64c7\u570b\u5bb6",
+ "password": "\u5bc6\u78bc",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ },
+ "description": "\u8acb\u8f38\u5165 MySubaru \u8a8d\u8b49\n\u6ce8\u610f\uff1a\u555f\u59cb\u8a2d\u5b9a\u5927\u7d04\u9700\u8981 30 \u79d2",
+ "title": "Subaru Starlink \u8a2d\u5b9a"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "update_enabled": "\u958b\u555f\u8eca\u8f1b\u8cc7\u6599\u4e0b\u8f09"
+ },
+ "description": "\u958b\u555f\u5f8c\uff0c\u5c07\u6703\u6bcf 2 \u5c0f\u6642\u50b3\u9001\u9060\u7aef\u547d\u4ee4\u81f3\u8eca\u8f1b\u4ee5\u7372\u5f97\u6700\u65b0\u50b3\u611f\u5668\u8cc7\u6599\u3002\u5982\u679c\u6c92\u6709\u958b\u555f\uff0c\u50b3\u611f\u5668\u65b0\u8cc7\u6599\u50c5\u6703\u65bc\u8eca\u8f1b\u81ea\u52d5\u63a8\u9001\u8cc7\u6599\u6642\u63a5\u6536\uff08\u901a\u5e38\u70ba\u5f15\u64ce\u7184\u706b\u4e4b\u5f8c\uff09\u3002",
+ "title": "Subaru Starlink \u9078\u9805"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py
index 3bca348429807f..7170e0b8a67392 100644
--- a/homeassistant/components/suez_water/sensor.py
+++ b/homeassistant/components/suez_water/sensor.py
@@ -6,10 +6,9 @@
from pysuez.client import PySuezError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, VOLUME_LITERS
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
CONF_COUNTER_ID = "counter_id"
@@ -47,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([SuezSensor(client)], True)
-class SuezSensor(Entity):
+class SuezSensor(SensorEntity):
"""Representation of a Sensor."""
def __init__(self, client):
@@ -73,7 +72,7 @@ def unit_of_measurement(self):
return VOLUME_LITERS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attributes
diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py
index 2d921da4a46816..489eab6b5be190 100644
--- a/homeassistant/components/sun/__init__.py
+++ b/homeassistant/components/sun/__init__.py
@@ -92,6 +92,7 @@ def __init__(self, hass):
"""Initialize the sun."""
self.hass = hass
self.location = None
+ self.elevation = 0.0
self._state = self.next_rising = self.next_setting = None
self.next_dawn = self.next_dusk = None
self.next_midnight = self.next_noon = None
@@ -100,10 +101,11 @@ def __init__(self, hass):
self._next_change = None
def update_location(_event):
- location = get_astral_location(self.hass)
+ location, elevation = get_astral_location(self.hass)
if location == self.location:
return
self.location = location
+ self.elevation = elevation
self.update_events()
update_location(None)
@@ -124,7 +126,7 @@ def state(self):
return STATE_BELOW_HORIZON
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sun."""
return {
STATE_ATTR_NEXT_DAWN: self.next_dawn.isoformat(),
@@ -140,7 +142,7 @@ def state_attributes(self):
def _check_event(self, utc_point_in_time, sun_event, before):
next_utc = get_location_astral_event_next(
- self.location, sun_event, utc_point_in_time
+ self.location, self.elevation, sun_event, utc_point_in_time
)
if next_utc < self._next_change:
self._next_change = next_utc
@@ -169,7 +171,7 @@ def update_events(self, now=None):
)
self.location.solar_depression = -10
self._check_event(utc_point_in_time, "dawn", PHASE_SMALL_DAY)
- self.next_noon = self._check_event(utc_point_in_time, "solar_noon", None)
+ self.next_noon = self._check_event(utc_point_in_time, "noon", None)
self._check_event(utc_point_in_time, "dusk", PHASE_DAY)
self.next_setting = self._check_event(
utc_point_in_time, SUN_EVENT_SUNSET, PHASE_SMALL_DAY
@@ -180,9 +182,7 @@ def update_events(self, now=None):
self._check_event(utc_point_in_time, "dusk", PHASE_NAUTICAL_TWILIGHT)
self.location.solar_depression = "astronomical"
self._check_event(utc_point_in_time, "dusk", PHASE_ASTRONOMICAL_TWILIGHT)
- self.next_midnight = self._check_event(
- utc_point_in_time, "solar_midnight", None
- )
+ self.next_midnight = self._check_event(utc_point_in_time, "midnight", None)
self.location.solar_depression = "civil"
# if the event was solar midday or midnight, phase will now
@@ -190,7 +190,7 @@ def update_events(self, now=None):
# even in the day at the poles, so we can't rely on it.
# Need to calculate phase if next is noon or midnight
if self.phase is None:
- elevation = self.location.solar_elevation(self._next_change)
+ elevation = self.location.solar_elevation(self._next_change, self.elevation)
if elevation >= 10:
self.phase = PHASE_DAY
elif elevation >= 0:
@@ -222,9 +222,11 @@ def update_sun_position(self, now=None):
"""Calculate the position of the sun."""
# Grab current time in case system clock changed since last time we ran.
utc_point_in_time = dt_util.utcnow()
- self.solar_azimuth = round(self.location.solar_azimuth(utc_point_in_time), 2)
+ self.solar_azimuth = round(
+ self.location.solar_azimuth(utc_point_in_time, self.elevation), 2
+ )
self.solar_elevation = round(
- self.location.solar_elevation(utc_point_in_time), 2
+ self.location.solar_elevation(utc_point_in_time, self.elevation), 2
)
_LOGGER.debug(
diff --git a/homeassistant/components/sun/translations/id.json b/homeassistant/components/sun/translations/id.json
index 374da4e0db9293..df6c960e67d0e0 100644
--- a/homeassistant/components/sun/translations/id.json
+++ b/homeassistant/components/sun/translations/id.json
@@ -2,7 +2,7 @@
"state": {
"_": {
"above_horizon": "Terbit",
- "below_horizon": "Tenggelam"
+ "below_horizon": "Terbenam"
}
},
"title": "Matahari"
diff --git a/homeassistant/components/sun/translations/pl.json b/homeassistant/components/sun/translations/pl.json
index fb90b9bd232b84..1f00babd1fd07a 100644
--- a/homeassistant/components/sun/translations/pl.json
+++ b/homeassistant/components/sun/translations/pl.json
@@ -1,7 +1,7 @@
{
"state": {
"_": {
- "above_horizon": "powy\u017cej horyzontu",
+ "above_horizon": "nad horyzontem",
"below_horizon": "poni\u017cej horyzontu"
}
},
diff --git a/homeassistant/components/sun/trigger.py b/homeassistant/components/sun/trigger.py
index 779d3078f13da8..d2b6f6de560705 100644
--- a/homeassistant/components/sun/trigger.py
+++ b/homeassistant/components/sun/trigger.py
@@ -26,6 +26,7 @@
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for events based on configuration."""
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
event = config.get(CONF_EVENT)
offset = config.get(CONF_OFFSET)
description = event
@@ -44,6 +45,7 @@ def call_action():
"event": event,
"offset": offset,
"description": description,
+ "id": trigger_id,
}
},
)
diff --git a/homeassistant/components/supervisord/sensor.py b/homeassistant/components/supervisord/sensor.py
index 8e1d6f89eea196..f701df2d6c3bb3 100644
--- a/homeassistant/components/supervisord/sensor.py
+++ b/homeassistant/components/supervisord/sensor.py
@@ -4,10 +4,9 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_URL
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -36,7 +35,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
)
-class SupervisorProcessSensor(Entity):
+class SupervisorProcessSensor(SensorEntity):
"""Representation of a supervisor-monitored process."""
def __init__(self, info, server):
@@ -61,7 +60,7 @@ def available(self):
return self._available
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_DESCRIPTION: self._info.get("description"),
diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py
index 084811c8fa05fc..5ebd6d6ca481d0 100644
--- a/homeassistant/components/supla/__init__.py
+++ b/homeassistant/components/supla/__init__.py
@@ -1,7 +1,8 @@
"""Support for Supla devices."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Optional
import async_timeout
from asyncpysupla import SuplaAPI
@@ -180,7 +181,7 @@ def unique_id(self) -> str:
)
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the device."""
return self.channel_data["caption"]
diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py
index 8ba6809ee0502a..4a65931d3f04aa 100644
--- a/homeassistant/components/surepetcare/__init__.py
+++ b/homeassistant/components/surepetcare/__init__.py
@@ -1,6 +1,8 @@
"""Support for Sure Petcare cat/pet flaps."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict, List
+from typing import Any
from surepy import (
MESTART_RESOURCE,
@@ -185,12 +187,12 @@ async def handle_set_lock_state(call):
class SurePetcareAPI:
"""Define a generic Sure Petcare object."""
- def __init__(self, hass, surepy: SurePetcare, ids: List[Dict[str, Any]]) -> None:
+ def __init__(self, hass, surepy: SurePetcare, ids: list[dict[str, Any]]) -> None:
"""Initialize the Sure Petcare object."""
self.hass = hass
self.surepy = surepy
self.ids = ids
- self.states: Dict[str, Any] = {}
+ self.states: dict[str, Any] = {}
async def async_update(self, arg: Any = None) -> None:
"""Refresh Sure Petcare data."""
diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py
index 2a624b580acffd..e96a5eaf35e591 100644
--- a/homeassistant/components/surepetcare/binary_sensor.py
+++ b/homeassistant/components/surepetcare/binary_sensor.py
@@ -1,7 +1,9 @@
"""Support for Sure PetCare Flaps/Pets binary sensors."""
+from __future__ import annotations
+
from datetime import datetime
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from surepy import SureLocationID, SurepyProduct
@@ -71,8 +73,8 @@ def __init__(
self._device_class = device_class
self._spc: SurePetcareAPI = spc
- self._spc_data: Dict[str, Any] = self._spc.states[self._sure_type].get(self._id)
- self._state: Dict[str, Any] = {}
+ self._spc_data: dict[str, Any] = self._spc.states[self._sure_type].get(self._id)
+ self._state: dict[str, Any] = {}
# cover special case where a device has no name set
if "name" in self._spc_data:
@@ -85,7 +87,7 @@ def __init__(
self._async_unsub_dispatcher_connect = None
@property
- def is_on(self) -> Optional[bool]:
+ def is_on(self) -> bool | None:
"""Return true if entity is on/unlocked."""
return bool(self._state)
@@ -151,7 +153,7 @@ def is_on(self) -> bool:
return self.available
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the device."""
attributes = None
if self._state:
@@ -179,7 +181,7 @@ def is_on(self) -> bool:
return False
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the device."""
attributes = None
if self._state:
@@ -232,7 +234,7 @@ def is_on(self) -> bool:
return self.available
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the device."""
attributes = None
if self._state:
diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py
index e2d3d0708675ea..0a49781767b203 100644
--- a/homeassistant/components/surepetcare/sensor.py
+++ b/homeassistant/components/surepetcare/sensor.py
@@ -1,9 +1,12 @@
"""Support for Sure PetCare Flaps/Pets sensors."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from surepy import SureLockStateID, SurepyProduct
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_VOLTAGE,
CONF_ID,
@@ -13,7 +16,6 @@
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from . import SurePetcareAPI
from .const import (
@@ -52,7 +54,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(entities, True)
-class SurePetcareSensor(Entity):
+class SurePetcareSensor(SensorEntity):
"""A binary sensor implementation for Sure Petcare Entities."""
def __init__(self, _id: int, sure_type: SurepyProduct, spc: SurePetcareAPI):
@@ -62,8 +64,8 @@ def __init__(self, _id: int, sure_type: SurepyProduct, spc: SurePetcareAPI):
self._sure_type = sure_type
self._spc = spc
- self._spc_data: Dict[str, Any] = self._spc.states[self._sure_type].get(self._id)
- self._state: Dict[str, Any] = {}
+ self._spc_data: dict[str, Any] = self._spc.states[self._sure_type].get(self._id)
+ self._state: dict[str, Any] = {}
self._name = (
f"{self._sure_type.name.capitalize()} "
@@ -120,12 +122,12 @@ class Flap(SurePetcareSensor):
"""Sure Petcare Flap."""
@property
- def state(self) -> Optional[int]:
+ def state(self) -> int | None:
"""Return battery level in percent."""
return SureLockStateID(self._state["locking"]["mode"]).name.capitalize()
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the device."""
attributes = None
if self._state:
@@ -143,9 +145,9 @@ def name(self) -> str:
return f"{self._name} Battery Level"
@property
- def state(self) -> Optional[int]:
+ def state(self) -> int | None:
"""Return battery level in percent."""
- battery_percent: Optional[int]
+ battery_percent: int | None
try:
per_battery_voltage = self._state["battery"] / 4
voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW
@@ -166,7 +168,7 @@ def device_class(self) -> str:
return DEVICE_CLASS_BATTERY
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return state attributes."""
attributes = None
if self._state:
diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py
index 61423312b2a073..47a8d3e55899d2 100644
--- a/homeassistant/components/swiss_hydrological_data/sensor.py
+++ b/homeassistant/components/swiss_hydrological_data/sensor.py
@@ -5,10 +5,9 @@
from swisshydrodata import SwissHydroData
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -83,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(entities, True)
-class SwissHydrologicalDataSensor(Entity):
+class SwissHydrologicalDataSensor(SensorEntity):
"""Implementation of a Swiss hydrological sensor."""
def __init__(self, hydro_data, station, condition):
@@ -119,7 +118,7 @@ def state(self):
return None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
attrs = {}
diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py
index 2c7fb483eff72a..a971524c22b2f2 100644
--- a/homeassistant/components/swiss_public_transport/sensor.py
+++ b/homeassistant/components/swiss_public_transport/sensor.py
@@ -6,11 +6,10 @@
from opendata_transport.exceptions import OpendataTransportError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -68,7 +67,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([SwissPublicTransportSensor(opendata, start, destination, name)])
-class SwissPublicTransportSensor(Entity):
+class SwissPublicTransportSensor(SensorEntity):
"""Implementation of an Swiss public transport sensor."""
def __init__(self, opendata, start, destination, name):
@@ -94,7 +93,7 @@ def state(self):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._opendata is None:
return
diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py
index 5662212c9e8d90..1332aa4618986e 100644
--- a/homeassistant/components/swisscom/device_tracker.py
+++ b/homeassistant/components/swisscom/device_tracker.py
@@ -1,4 +1,5 @@
"""Support for Swisscom routers (Internet-Box)."""
+from contextlib import suppress
import logging
from aiohttp.hdrs import CONTENT_TYPE
@@ -97,13 +98,11 @@ def get_swisscom_data(self):
return devices
for device in request.json()["status"]:
- try:
+ with suppress(KeyError, requests.exceptions.RequestException):
devices[device["Key"]] = {
"ip": device["IPAddress"],
"mac": device["PhysAddress"],
"host": device["Name"],
"status": device["Active"],
}
- except (KeyError, requests.exceptions.RequestException):
- pass
return devices
diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py
index 1d9b54a0424b45..c585fdc22d30a6 100644
--- a/homeassistant/components/switch/__init__.py
+++ b/homeassistant/components/switch/__init__.py
@@ -1,6 +1,7 @@
"""Component to interface with switches that can be controlled remotely."""
from datetime import timedelta
import logging
+from typing import final
import voluptuous as vol
@@ -79,7 +80,7 @@ async def async_unload_entry(hass, entry):
class SwitchEntity(ToggleEntity):
- """Representation of a switch."""
+ """Base class for switch entities."""
@property
def current_power_w(self):
@@ -96,6 +97,7 @@ def is_standby(self):
"""Return true if device is in standby."""
return None
+ @final
@property
def state_attributes(self):
"""Return the optional state attributes."""
diff --git a/homeassistant/components/switch/device_action.py b/homeassistant/components/switch/device_action.py
index a50131f094c281..0f3890d329ff3a 100644
--- a/homeassistant/components/switch/device_action.py
+++ b/homeassistant/components/switch/device_action.py
@@ -1,5 +1,5 @@
"""Provides device actions for switches."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -25,6 +25,6 @@ async def async_call_action_from_config(
)
-async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device actions."""
return await toggle_entity.async_get_actions(hass, device_id, DOMAIN)
diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py
index c928deef01abe6..15c2e54d193254 100644
--- a/homeassistant/components/switch/device_condition.py
+++ b/homeassistant/components/switch/device_condition.py
@@ -1,5 +1,5 @@
"""Provides device conditions for switches."""
-from typing import Dict, List
+from __future__ import annotations
import voluptuous as vol
@@ -28,7 +28,7 @@ def async_condition_from_config(
async def async_get_conditions(
hass: HomeAssistant, device_id: str
-) -> List[Dict[str, str]]:
+) -> list[dict[str, str]]:
"""List device conditions."""
return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN)
diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py
index cb5d5f7aa0e3c4..15b700d9eb59ef 100644
--- a/homeassistant/components/switch/device_trigger.py
+++ b/homeassistant/components/switch/device_trigger.py
@@ -1,5 +1,5 @@
"""Provides device triggers for switches."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -28,7 +28,7 @@ async def async_attach_trigger(
)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers."""
return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN)
diff --git a/homeassistant/components/switch/group.py b/homeassistant/components/switch/group.py
index 1636054663dc69..234883ffd5a041 100644
--- a/homeassistant/components/switch/group.py
+++ b/homeassistant/components/switch/group.py
@@ -3,13 +3,12 @@
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OFF, STATE_ON
-from homeassistant.core import callback
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import HomeAssistant, callback
@callback
def async_describe_on_off_states(
- hass: HomeAssistantType, registry: GroupIntegrationRegistry
+ hass: HomeAssistant, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_ON}, STATE_OFF)
diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py
index 5128a49d8b7c17..e21548105225b4 100644
--- a/homeassistant/components/switch/light.py
+++ b/homeassistant/components/switch/light.py
@@ -1,5 +1,7 @@
"""Light support for switch entities."""
-from typing import Any, Callable, Optional, Sequence, cast
+from __future__ import annotations
+
+from typing import Any, Callable, Sequence, cast
import voluptuous as vol
@@ -12,15 +14,11 @@
STATE_ON,
STATE_UNAVAILABLE,
)
-from homeassistant.core import CALLBACK_TYPE, callback
+from homeassistant.core import HomeAssistant, State, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_state_change_event
-from homeassistant.helpers.typing import (
- ConfigType,
- DiscoveryInfoType,
- HomeAssistantType,
-)
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
@@ -35,10 +33,10 @@
async def async_setup_platform(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
config: ConfigType,
- async_add_entities: Callable[[Sequence[Entity], bool], None],
- discovery_info: Optional[DiscoveryInfoType] = None,
+ async_add_entities: Callable[[Sequence[Entity]], None],
+ discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Initialize Light Switch platform."""
@@ -53,8 +51,7 @@ async def async_setup_platform(
config[CONF_ENTITY_ID],
unique_id,
)
- ],
- True,
+ ]
)
@@ -66,9 +63,7 @@ def __init__(self, name: str, switch_entity_id: str, unique_id: str) -> None:
self._name = name
self._switch_entity_id = switch_entity_id
self._unique_id = unique_id
- self._is_on = False
- self._available = False
- self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None
+ self._switch_state: State | None = None
@property
def name(self) -> str:
@@ -78,12 +73,16 @@ def name(self) -> str:
@property
def is_on(self) -> bool:
"""Return true if light switch is on."""
- return self._is_on
+ assert self._switch_state is not None
+ return self._switch_state.state == STATE_ON
@property
def available(self) -> bool:
"""Return true if light switch is on."""
- return self._available
+ return (
+ self._switch_state is not None
+ and self._switch_state.state != STATE_UNAVAILABLE
+ )
@property
def should_poll(self) -> bool:
@@ -117,33 +116,18 @@ async def async_turn_off(self, **kwargs):
context=self._context,
)
- async def async_update(self):
- """Query the switch in this light switch and determine the state."""
- switch_state = self.hass.states.get(self._switch_entity_id)
-
- if switch_state is None:
- self._available = False
- return
-
- self._is_on = switch_state.state == STATE_ON
- self._available = switch_state.state != STATE_UNAVAILABLE
-
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
+ self._switch_state = self.hass.states.get(self._switch_entity_id)
@callback
def async_state_changed_listener(*_: Any) -> None:
"""Handle child updates."""
- self.async_schedule_update_ha_state(True)
+ self._switch_state = self.hass.states.get(self._switch_entity_id)
+ self.async_write_ha_state()
- assert self.hass is not None
- self._async_unsub_state_changed = async_track_state_change_event(
- self.hass, [self._switch_entity_id], async_state_changed_listener
+ self.async_on_remove(
+ async_track_state_change_event(
+ self.hass, [self._switch_entity_id], async_state_changed_listener
+ )
)
-
- async def async_will_remove_from_hass(self):
- """Handle removal from Home Assistant."""
- if self._async_unsub_state_changed is not None:
- self._async_unsub_state_changed()
- self._async_unsub_state_changed = None
- self._available = False
diff --git a/homeassistant/components/switch/reproduce_state.py b/homeassistant/components/switch/reproduce_state.py
index 0527f558f35673..94fd836631bb13 100644
--- a/homeassistant/components/switch/reproduce_state.py
+++ b/homeassistant/components/switch/reproduce_state.py
@@ -1,7 +1,9 @@
"""Reproduce an Switch state."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -10,8 +12,7 @@
STATE_OFF,
STATE_ON,
)
-from homeassistant.core import Context, State
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import Context, HomeAssistant, State
from . import DOMAIN
@@ -21,11 +22,11 @@
async def _async_reproduce_state(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -57,11 +58,11 @@ async def _async_reproduce_state(
async def async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Switch states."""
await asyncio.gather(
diff --git a/homeassistant/components/switch/services.yaml b/homeassistant/components/switch/services.yaml
index 74dda2ddf4f0e4..de45995797fe7c 100644
--- a/homeassistant/components/switch/services.yaml
+++ b/homeassistant/components/switch/services.yaml
@@ -1,22 +1,13 @@
# Describes the format for available switch services
turn_on:
- description: Turn a switch on.
- fields:
- entity_id:
- description: Name(s) of entities to turn on
- example: "switch.living_room"
+ description: Turn a switch on
+ target:
turn_off:
- description: Turn a switch off.
- fields:
- entity_id:
- description: Name(s) of entities to turn off.
- example: "switch.living_room"
+ description: Turn a switch off
+ target:
toggle:
- description: Toggles a switch state.
- fields:
- entity_id:
- description: Name(s) of entities to toggle.
- example: "switch.living_room"
+ description: Toggles a switch state
+ target:
diff --git a/homeassistant/components/switch/significant_change.py b/homeassistant/components/switch/significant_change.py
index f4dcddc3f340dc..231085a3eeff8b 100644
--- a/homeassistant/components/switch/significant_change.py
+++ b/homeassistant/components/switch/significant_change.py
@@ -1,5 +1,7 @@
"""Helper to test significant Switch state changes."""
-from typing import Any, Optional
+from __future__ import annotations
+
+from typing import Any
from homeassistant.core import HomeAssistant, callback
@@ -12,6 +14,6 @@ def async_check_significant_change(
new_state: str,
new_attrs: dict,
**kwargs: Any,
-) -> Optional[bool]:
+) -> bool | None:
"""Test if state significantly changed."""
return old_state != new_state
diff --git a/homeassistant/components/switch/translations/id.json b/homeassistant/components/switch/translations/id.json
index 891b1b00681ce2..070d272aa4344a 100644
--- a/homeassistant/components/switch/translations/id.json
+++ b/homeassistant/components/switch/translations/id.json
@@ -1,8 +1,23 @@
{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Nyala/matikan {entity_name}",
+ "turn_off": "Matikan {entity_name}",
+ "turn_on": "Nyalakan {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} mati",
+ "is_on": "{entity_name} nyala"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} dimatikan",
+ "turned_on": "{entity_name} dinyalakan"
+ }
+ },
"state": {
"_": {
- "off": "Off",
- "on": "On"
+ "off": "Mati",
+ "on": "Nyala"
}
},
"title": "Sakelar"
diff --git a/homeassistant/components/switch/translations/ko.json b/homeassistant/components/switch/translations/ko.json
index 1779f3e1f648b3..6a3417efeb67d7 100644
--- a/homeassistant/components/switch/translations/ko.json
+++ b/homeassistant/components/switch/translations/ko.json
@@ -1,17 +1,17 @@
{
"device_automation": {
"action_type": {
- "toggle": "{entity_name} \ud1a0\uae00",
- "turn_off": "{entity_name} \ub044\uae30",
- "turn_on": "{entity_name} \ucf1c\uae30"
+ "toggle": "{entity_name}\uc744(\ub97c) \ud1a0\uae00\ud558\uae30",
+ "turn_off": "{entity_name}\uc744(\ub97c) \ub044\uae30",
+ "turn_on": "{entity_name}\uc744(\ub97c) \ucf1c\uae30"
},
"condition_type": {
- "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74",
- "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74"
+ "is_off": "{entity_name}\uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74",
+ "is_on": "{entity_name}\uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74"
},
"trigger_type": {
- "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c",
- "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c"
+ "turned_off": "{entity_name}\uc774(\uac00) \uaebc\uc84c\uc744 \ub54c",
+ "turned_on": "{entity_name}\uc774(\uac00) \ucf1c\uc84c\uc744 \ub54c"
}
},
"state": {
diff --git a/homeassistant/components/switch/translations/uk.json b/homeassistant/components/switch/translations/uk.json
index bee9eb957d530f..26b85b3a87397f 100644
--- a/homeassistant/components/switch/translations/uk.json
+++ b/homeassistant/components/switch/translations/uk.json
@@ -1,5 +1,14 @@
{
"device_automation": {
+ "action_type": {
+ "toggle": "{entity_name}: \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u0438",
+ "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438",
+ "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456",
+ "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456"
+ },
"trigger_type": {
"turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
"turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e"
diff --git a/homeassistant/components/switch/translations/zh-Hans.json b/homeassistant/components/switch/translations/zh-Hans.json
index a18455aec6a862..afe9f58db8f9d8 100644
--- a/homeassistant/components/switch/translations/zh-Hans.json
+++ b/homeassistant/components/switch/translations/zh-Hans.json
@@ -7,7 +7,7 @@
},
"condition_type": {
"is_off": "{entity_name} \u5df2\u5173\u95ed",
- "is_on": "{entity_name} \u5df2\u5f00\u542f"
+ "is_on": "{entity_name} \u5df2\u6253\u5f00"
},
"trigger_type": {
"turned_off": "{entity_name} \u88ab\u5173\u95ed",
diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py
index 3dd931abe49a5a..cff1a0d0edc4d5 100644
--- a/homeassistant/components/switchbot/switch.py
+++ b/homeassistant/components/switchbot/switch.py
@@ -1,7 +1,9 @@
"""Support for Switchbot."""
-from typing import Any, Dict
+from __future__ import annotations
-# pylint: disable=import-error, no-member
+from typing import Any
+
+# pylint: disable=import-error
import switchbot
import voluptuous as vol
@@ -86,6 +88,6 @@ def name(self) -> str:
return self._name
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
return {"last_run_success": self._last_run_success}
diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py
index 244ed708cc7bc9..8d39182dcc32f4 100644
--- a/homeassistant/components/switcher_kis/__init__.py
+++ b/homeassistant/components/switcher_kis/__init__.py
@@ -1,14 +1,15 @@
"""Home Assistant Switcher Component."""
+from __future__ import annotations
+
from asyncio import QueueEmpty, TimeoutError as Asyncio_TimeoutError, wait_for
from datetime import datetime, timedelta
import logging
-from typing import Dict, Optional
from aioswitcher.bridge import SwitcherV2Bridge
import voluptuous as vol
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
-from homeassistant.const import EVENT_HOMEASSISTANT_STOP
+from homeassistant.const import CONF_DEVICE_ID, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
@@ -20,7 +21,6 @@
DOMAIN = "switcher_kis"
-CONF_DEVICE_ID = "device_id"
CONF_DEVICE_PASSWORD = "device_password"
CONF_PHONE_ID = "phone_id"
@@ -46,9 +46,8 @@
)
-async def async_setup(hass: HomeAssistantType, config: Dict) -> bool:
+async def async_setup(hass: HomeAssistantType, config: dict) -> bool:
"""Set up the switcher component."""
-
phone_id = config[DOMAIN][CONF_PHONE_ID]
device_id = config[DOMAIN][CONF_DEVICE_ID]
device_password = config[DOMAIN][CONF_DEVICE_PASSWORD]
@@ -74,7 +73,7 @@ async def async_stop_bridge(event: EventType) -> None:
hass.async_create_task(async_load_platform(hass, SWITCH_DOMAIN, DOMAIN, {}, config))
@callback
- def device_updates(timestamp: Optional[datetime]) -> None:
+ def device_updates(timestamp: datetime | None) -> None:
"""Use for updating the device data from the queue."""
if v2bridge.running:
try:
diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py
index 5e75a0e6090f8c..61297142716cb9 100644
--- a/homeassistant/components/switcher_kis/switch.py
+++ b/homeassistant/components/switcher_kis/switch.py
@@ -1,5 +1,7 @@
"""Home Assistant Switcher Component Switch platform."""
-from typing import Callable, Dict
+from __future__ import annotations
+
+from typing import Callable
from aioswitcher.api import SwitcherV2Api
from aioswitcher.api.messages import SwitcherV2ControlResponseMSG
@@ -52,9 +54,9 @@
async def async_setup_platform(
hass: HomeAssistantType,
- config: Dict,
+ config: dict,
async_add_entities: Callable,
- discovery_info: Dict,
+ discovery_info: dict,
) -> None:
"""Set up the switcher platform for the switch component."""
if discovery_info is None:
@@ -62,7 +64,6 @@ async def async_setup_platform(
async def async_set_auto_off_service(entity, service_call: ServiceCallType) -> None:
"""Use for handling setting device auto-off service calls."""
-
async with SwitcherV2Api(
hass.loop,
device_data.ip_addr,
@@ -76,7 +77,6 @@ async def async_turn_on_with_timer_service(
entity, service_call: ServiceCallType
) -> None:
"""Use for handling turning device on with a timer service calls."""
-
async with SwitcherV2Api(
hass.loop,
device_data.ip_addr,
@@ -133,7 +133,6 @@ def unique_id(self) -> str:
@property
def is_on(self) -> bool:
"""Return True if entity is on."""
-
return self._state == SWITCHER_STATE_ON
@property
@@ -142,9 +141,8 @@ def current_power_w(self) -> int:
return self._device_data.power_consumption
@property
- def device_state_attributes(self) -> Dict:
+ def extra_state_attributes(self) -> dict:
"""Return the optional state attributes."""
-
attribs = {}
for prop, attr in DEVICE_PROPERTIES_TO_HA_ATTRIBUTES.items():
@@ -157,7 +155,6 @@ def device_state_attributes(self) -> Dict:
@property
def available(self) -> bool:
"""Return True if entity is available."""
-
return self._state in [SWITCHER_STATE_ON, SWITCHER_STATE_OFF]
async def async_added_to_hass(self) -> None:
@@ -178,17 +175,16 @@ async def async_update_data(self, device_data: SwitcherV2Device) -> None:
self._state = self._device_data.state
self.async_write_ha_state()
- async def async_turn_on(self, **kwargs: Dict) -> None:
+ async def async_turn_on(self, **kwargs: dict) -> None:
"""Turn the entity on."""
await self._control_device(True)
- async def async_turn_off(self, **kwargs: Dict) -> None:
+ async def async_turn_off(self, **kwargs: dict) -> None:
"""Turn the entity off."""
await self._control_device(False)
async def _control_device(self, send_on: bool) -> None:
"""Turn the entity on or off."""
-
response: SwitcherV2ControlResponseMSG = None
async with SwitcherV2Api(
self.hass.loop,
diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py
index a052b9051a1f69..24b54537dc839e 100644
--- a/homeassistant/components/switchmate/switch.py
+++ b/homeassistant/components/switchmate/switch.py
@@ -1,7 +1,7 @@
"""Support for Switchmate."""
from datetime import timedelta
-# pylint: disable=import-error, no-member
+# pylint: disable=import-error
import switchmate
import voluptuous as vol
diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py
index 83d32eb9b47e83..293680151ffc08 100644
--- a/homeassistant/components/syncthru/__init__.py
+++ b/homeassistant/components/syncthru/__init__.py
@@ -1,7 +1,7 @@
"""The syncthru component."""
+from __future__ import annotations
import logging
-from typing import Set, Tuple
from pysyncthru import SyncThru
@@ -65,12 +65,12 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo
return True
-def device_identifiers(printer: SyncThru) -> Set[Tuple[str, str]]:
+def device_identifiers(printer: SyncThru) -> set[tuple[str, str]]:
"""Get device identifiers for device registry."""
return {(DOMAIN, printer.serial_number())}
-def device_connections(printer: SyncThru) -> Set[Tuple[str, str]]:
+def device_connections(printer: SyncThru) -> set[tuple[str, str]]:
"""Get device connections for device registry."""
connections = set()
try:
diff --git a/homeassistant/components/syncthru/config_flow.py b/homeassistant/components/syncthru/config_flow.py
index cbdd46b4a6ae64..cb0243f98fcd29 100644
--- a/homeassistant/components/syncthru/config_flow.py
+++ b/homeassistant/components/syncthru/config_flow.py
@@ -12,7 +12,6 @@
from homeassistant.const import CONF_NAME, CONF_URL
from homeassistant.helpers import aiohttp_client
-# pylint: disable=unused-import # for DOMAIN https://github.com/PyCQA/pylint/issues/3202
from .const import DEFAULT_MODEL, DEFAULT_NAME_TEMPLATE, DOMAIN
@@ -62,10 +61,7 @@ async def async_step_ssdp(self, discovery_info):
# Remove trailing " (ip)" if present for consistency with user driven config
self.name = re.sub(r"\s+\([\d.]+\)\s*$", "", self.name)
- # https://github.com/PyCQA/pylint/issues/3167
- self.context["title_placeholders"] = { # pylint: disable=no-member
- CONF_NAME: self.name
- }
+ self.context["title_placeholders"] = {CONF_NAME: self.name}
return await self.async_step_confirm()
async def async_step_confirm(self, user_input=None):
diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py
index 639ec3ac6cb9d2..8277bd69467a02 100644
--- a/homeassistant/components/syncthru/sensor.py
+++ b/homeassistant/components/syncthru/sensor.py
@@ -5,11 +5,10 @@
from pysyncthru import SYNCTHRU_STATE_HUMAN, SyncThru
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_NAME, CONF_RESOURCE, CONF_URL, PERCENTAGE
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from . import device_identifiers
from .const import DEFAULT_MODEL, DEFAULT_NAME_TEMPLATE, DOMAIN
@@ -41,7 +40,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"""Set up the SyncThru component."""
_LOGGER.warning(
"Loading syncthru via platform config is deprecated and no longer "
- "necessary as of 0.113. Please remove it from your configuration YAML."
+ "necessary as of 0.113; Please remove it from your configuration YAML"
)
hass.async_create_task(
hass.config_entries.flow.async_init(
@@ -81,7 +80,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(devices, True)
-class SyncThruSensor(Entity):
+class SyncThruSensor(SensorEntity):
"""Implementation of an abstract Samsung Printer sensor platform."""
def __init__(self, syncthru, name):
@@ -121,7 +120,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
return self._attributes
diff --git a/homeassistant/components/syncthru/strings.json b/homeassistant/components/syncthru/strings.json
index 0164fdf6ddc2fe..67f50e84a983ed 100644
--- a/homeassistant/components/syncthru/strings.json
+++ b/homeassistant/components/syncthru/strings.json
@@ -12,7 +12,7 @@
"step": {
"confirm": {
"data": {
- "name": "[%key:component::syncthru::config::step::user::data::name%]",
+ "name": "[%key:common::config_flow::data::name%]",
"url": "[%key:component::syncthru::config::step::user::data::url%]"
}
},
diff --git a/homeassistant/components/syncthru/translations/hu.json b/homeassistant/components/syncthru/translations/hu.json
index 3b2d79a34a77e2..227b759ffaf163 100644
--- a/homeassistant/components/syncthru/translations/hu.json
+++ b/homeassistant/components/syncthru/translations/hu.json
@@ -2,6 +2,23 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "invalid_url": "\u00c9rv\u00e9nytelen URL"
+ },
+ "step": {
+ "confirm": {
+ "data": {
+ "name": "N\u00e9v",
+ "url": "Webes fel\u00fclet URL-je"
+ }
+ },
+ "user": {
+ "data": {
+ "name": "N\u00e9v",
+ "url": "Webes fel\u00fclet URL-je"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/syncthru/translations/id.json b/homeassistant/components/syncthru/translations/id.json
new file mode 100644
index 00000000000000..54d5e6f5c969e2
--- /dev/null
+++ b/homeassistant/components/syncthru/translations/id.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "invalid_url": "URL tidak valid",
+ "syncthru_not_supported": "Perangkat tidak mendukung SyncThru",
+ "unknown_state": "Status printer tidak diketahui, verifikasi URL dan konektivitas jaringan"
+ },
+ "flow_title": "Printer Samsung SyncThru: {name}",
+ "step": {
+ "confirm": {
+ "data": {
+ "name": "Nama",
+ "url": "URL antarmuka web"
+ }
+ },
+ "user": {
+ "data": {
+ "name": "Nama",
+ "url": "URL antarmuka web"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/syncthru/translations/nl.json b/homeassistant/components/syncthru/translations/nl.json
index 349b4b2818e69c..d86e9fa2087968 100644
--- a/homeassistant/components/syncthru/translations/nl.json
+++ b/homeassistant/components/syncthru/translations/nl.json
@@ -4,13 +4,22 @@
"already_configured": "Apparaat is al geconfigureerd"
},
"error": {
+ "invalid_url": "Ongeldige URL",
+ "syncthru_not_supported": "Apparaat ondersteunt SyncThru niet",
"unknown_state": "Printerstatus onbekend, controleer URL en netwerkconnectiviteit"
},
"flow_title": "Samsung SyncThru Printer: {name}",
"step": {
+ "confirm": {
+ "data": {
+ "name": "Naam",
+ "url": "Webinterface URL"
+ }
+ },
"user": {
"data": {
- "name": "Naam"
+ "name": "Naam",
+ "url": "Webinterface URL"
}
}
}
diff --git a/homeassistant/components/syncthru/translations/tr.json b/homeassistant/components/syncthru/translations/tr.json
new file mode 100644
index 00000000000000..942457958f820e
--- /dev/null
+++ b/homeassistant/components/syncthru/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "step": {
+ "confirm": {
+ "data": {
+ "url": "Web aray\u00fcz\u00fc URL'si"
+ }
+ },
+ "user": {
+ "data": {
+ "name": "Ad",
+ "url": "Web aray\u00fcz\u00fc URL'si"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/syncthru/translations/uk.json b/homeassistant/components/syncthru/translations/uk.json
new file mode 100644
index 00000000000000..74cccc7ef5a24a
--- /dev/null
+++ b/homeassistant/components/syncthru/translations/uk.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "invalid_url": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430 URL-\u0430\u0434\u0440\u0435\u0441\u0430.",
+ "syncthru_not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454 SyncThru.",
+ "unknown_state": "\u0421\u0442\u0430\u043d \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043d\u0435\u0432\u0456\u0434\u043e\u043c\u0438\u0439, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 URL-\u0430\u0434\u0440\u0435\u0441\u0443 \u0442\u0430 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043c\u0435\u0440\u0435\u0436\u0456."
+ },
+ "flow_title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 Samsung SyncThru: {name}",
+ "step": {
+ "confirm": {
+ "data": {
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432\u0435\u0431-\u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443"
+ }
+ },
+ "user": {
+ "data": {
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432\u0435\u0431-\u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/synology/__init__.py b/homeassistant/components/synology/__init__.py
deleted file mode 100644
index 0ab4b45e298646..00000000000000
--- a/homeassistant/components/synology/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The synology component."""
diff --git a/homeassistant/components/synology/camera.py b/homeassistant/components/synology/camera.py
deleted file mode 100644
index 4417f72918d0dc..00000000000000
--- a/homeassistant/components/synology/camera.py
+++ /dev/null
@@ -1,143 +0,0 @@
-"""Support for Synology Surveillance Station Cameras."""
-from functools import partial
-import logging
-
-import requests
-from synology.surveillance_station import SurveillanceStation
-import voluptuous as vol
-
-from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
-from homeassistant.const import (
- CONF_NAME,
- CONF_PASSWORD,
- CONF_TIMEOUT,
- CONF_URL,
- CONF_USERNAME,
- CONF_VERIFY_SSL,
- CONF_WHITELIST,
-)
-from homeassistant.helpers.aiohttp_client import (
- async_aiohttp_proxy_web,
- async_get_clientsession,
-)
-import homeassistant.helpers.config_validation as cv
-
-_LOGGER = logging.getLogger(__name__)
-
-DEFAULT_NAME = "Synology Camera"
-DEFAULT_TIMEOUT = 5
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- vol.Required(CONF_URL): cv.string,
- vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
- vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
- vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
- }
-)
-
-
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up a Synology IP Camera."""
- _LOGGER.warning(
- "The Synology integration is deprecated."
- " Please use the Synology DSM integration"
- " (https://www.home-assistant.io/integrations/synology_dsm/) instead."
- " This integration will be removed in version 0.118.0."
- )
-
- verify_ssl = config.get(CONF_VERIFY_SSL)
- timeout = config.get(CONF_TIMEOUT)
-
- try:
- surveillance = await hass.async_add_executor_job(
- partial(
- SurveillanceStation,
- config.get(CONF_URL),
- config.get(CONF_USERNAME),
- config.get(CONF_PASSWORD),
- verify_ssl=verify_ssl,
- timeout=timeout,
- )
- )
- except (requests.exceptions.RequestException, ValueError):
- _LOGGER.exception("Error when initializing SurveillanceStation")
- return False
-
- cameras = surveillance.get_all_cameras()
-
- # add cameras
- devices = []
- for camera in cameras:
- if not config[CONF_WHITELIST] or camera.name in config[CONF_WHITELIST]:
- device = SynologyCamera(surveillance, camera.camera_id, verify_ssl)
- devices.append(device)
-
- async_add_entities(devices)
-
-
-class SynologyCamera(Camera):
- """An implementation of a Synology NAS based IP camera."""
-
- def __init__(self, surveillance, camera_id, verify_ssl):
- """Initialize a Synology Surveillance Station camera."""
- super().__init__()
- self._surveillance = surveillance
- self._camera_id = camera_id
- self._verify_ssl = verify_ssl
- self._camera = self._surveillance.get_camera(camera_id)
- self._motion_setting = self._surveillance.get_motion_setting(camera_id)
- self.is_streaming = self._camera.is_enabled
-
- def camera_image(self):
- """Return bytes of camera image."""
- return self._surveillance.get_camera_image(self._camera_id)
-
- async def handle_async_mjpeg_stream(self, request):
- """Return a MJPEG stream image response directly from the camera."""
- streaming_url = self._camera.video_stream_url
-
- websession = async_get_clientsession(self.hass, self._verify_ssl)
- stream_coro = websession.get(streaming_url)
-
- return await async_aiohttp_proxy_web(self.hass, request, stream_coro)
-
- @property
- def name(self):
- """Return the name of this device."""
- return self._camera.name
-
- @property
- def is_recording(self):
- """Return true if the device is recording."""
- return self._camera.is_recording
-
- @property
- def should_poll(self):
- """Update the recording state periodically."""
- return True
-
- def update(self):
- """Update the status of the camera."""
- self._surveillance.update()
- self._camera = self._surveillance.get_camera(self._camera.camera_id)
- self._motion_setting = self._surveillance.get_motion_setting(
- self._camera.camera_id
- )
- self.is_streaming = self._camera.is_enabled
-
- @property
- def motion_detection_enabled(self):
- """Return the camera motion detection status."""
- return self._motion_setting.is_enabled
-
- def enable_motion_detection(self):
- """Enable motion detection in the camera."""
- self._surveillance.enable_motion_detection(self._camera_id)
-
- def disable_motion_detection(self):
- """Disable motion detection in camera."""
- self._surveillance.disable_motion_detection(self._camera_id)
diff --git a/homeassistant/components/synology/manifest.json b/homeassistant/components/synology/manifest.json
deleted file mode 100644
index a29dccc2a78229..00000000000000
--- a/homeassistant/components/synology/manifest.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "domain": "synology",
- "name": "Synology",
- "documentation": "https://www.home-assistant.io/integrations/synology",
- "requirements": ["py-synology==0.2.0"],
- "codeowners": []
-}
diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py
index 06696865d03a1c..16b531b9ee3df2 100644
--- a/homeassistant/components/synology_dsm/__init__.py
+++ b/homeassistant/components/synology_dsm/__init__.py
@@ -1,9 +1,11 @@
"""The Synology DSM component."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
import logging
-from typing import Dict
+import async_timeout
from synology_dsm import SynologyDSM
from synology_dsm.api.core.security import SynoCoreSecurity
from synology_dsm.api.core.system import SynoCoreSystem
@@ -14,6 +16,7 @@
from synology_dsm.api.storage.storage import SynoStorage
from synology_dsm.api.surveillance_station import SynoSurveillanceStation
from synology_dsm.exceptions import (
+ SynologyDSMAPIErrorException,
SynologyDSMLoginFailedException,
SynologyDSMRequestException,
)
@@ -37,17 +40,20 @@
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.dispatcher import (
- async_dispatcher_connect,
- async_dispatcher_send,
-)
-from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+ UpdateFailed,
+)
from .const import (
+ CONF_DEVICE_TOKEN,
CONF_SERIAL,
CONF_VOLUMES,
+ COORDINATOR_CAMERAS,
+ COORDINATOR_CENTRAL,
+ COORDINATOR_SWITCHES,
DEFAULT_SCAN_INTERVAL,
DEFAULT_USE_SSL,
DEFAULT_VERIFY_SSL,
@@ -65,7 +71,7 @@
STORAGE_DISK_SENSORS,
STORAGE_VOL_SENSORS,
SYNO_API,
- TEMP_SENSORS_KEYS,
+ SYSTEM_LOADED,
UNDO_UPDATE_LISTENER,
UTILISATION_SENSORS,
)
@@ -185,15 +191,16 @@ def _async_migrator(entity_entry: entity_registry.RegistryEntry):
try:
await api.async_setup()
except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err:
- _LOGGER.debug("async_setup_entry - Unable to connect to DSM: %s", err)
+ _LOGGER.debug(
+ "Unable to connect to DSM '%s' during setup: %s", entry.unique_id, err
+ )
raise ConfigEntryNotReady from err
- undo_listener = entry.add_update_listener(_async_update_listener)
-
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.unique_id] = {
+ UNDO_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener),
SYNO_API: api,
- UNDO_UPDATE_LISTENER: undo_listener,
+ SYSTEM_LOADED: True,
}
# Services
@@ -206,6 +213,79 @@ def _async_migrator(entity_entry: entity_registry.RegistryEntry):
entry, data={**entry.data, CONF_MAC: network.macs}
)
+ async def async_coordinator_update_data_cameras():
+ """Fetch all camera data from api."""
+ if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]:
+ raise UpdateFailed("System not fully loaded")
+
+ if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis:
+ return None
+
+ surveillance_station = api.surveillance_station
+
+ try:
+ async with async_timeout.timeout(10):
+ await hass.async_add_executor_job(surveillance_station.update)
+ except SynologyDSMAPIErrorException as err:
+ raise UpdateFailed(f"Error communicating with API: {err}") from err
+
+ return {
+ "cameras": {
+ camera.id: camera for camera in surveillance_station.get_all_cameras()
+ }
+ }
+
+ async def async_coordinator_update_data_central():
+ """Fetch all device and sensor data from api."""
+ try:
+ await api.async_update()
+ except Exception as err:
+ raise UpdateFailed(f"Error communicating with API: {err}") from err
+ return None
+
+ async def async_coordinator_update_data_switches():
+ """Fetch all switch data from api."""
+ if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]:
+ raise UpdateFailed("System not fully loaded")
+ if SynoSurveillanceStation.HOME_MODE_API_KEY not in api.dsm.apis:
+ return None
+
+ surveillance_station = api.surveillance_station
+
+ return {
+ "switches": {
+ "home_mode": await hass.async_add_executor_job(
+ surveillance_station.get_home_mode_status
+ )
+ }
+ }
+
+ hass.data[DOMAIN][entry.unique_id][COORDINATOR_CAMERAS] = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name=f"{entry.unique_id}_cameras",
+ update_method=async_coordinator_update_data_cameras,
+ update_interval=timedelta(seconds=30),
+ )
+
+ hass.data[DOMAIN][entry.unique_id][COORDINATOR_CENTRAL] = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name=f"{entry.unique_id}_central",
+ update_method=async_coordinator_update_data_central,
+ update_interval=timedelta(
+ minutes=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
+ ),
+ )
+
+ hass.data[DOMAIN][entry.unique_id][COORDINATOR_SWITCHES] = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name=f"{entry.unique_id}_switches",
+ update_method=async_coordinator_update_data_switches,
+ update_interval=timedelta(seconds=30),
+ )
+
for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
@@ -254,23 +334,22 @@ async def service_handler(call: ServiceCall):
serial = next(iter(dsm_devices))
else:
_LOGGER.error(
- "service_handler - more than one DSM configured, must specify one of serials %s",
+ "More than one DSM configured, must specify one of serials %s",
sorted(dsm_devices),
)
return
if not dsm_device:
- _LOGGER.error(
- "service_handler - DSM with specified serial %s not found", serial
- )
+ _LOGGER.error("DSM with specified serial %s not found", serial)
return
_LOGGER.debug("%s DSM with serial %s", call.service, serial)
dsm_api = dsm_device[SYNO_API]
+ dsm_device[SYSTEM_LOADED] = False
if call.service == SERVICE_REBOOT:
await dsm_api.async_reboot()
elif call.service == SERVICE_SHUTDOWN:
- await dsm_api.system.shutdown()
+ await dsm_api.async_shutdown()
for service in SERVICES:
hass.services.async_register(DOMAIN, service, service_handler)
@@ -305,13 +384,6 @@ def __init__(self, hass: HomeAssistantType, entry: ConfigEntry):
self._with_upgrade = True
self._with_utilisation = True
- self._unsub_dispatcher = None
-
- @property
- def signal_sensor_update(self) -> str:
- """Event specific per Synology DSM entry to signal updates in sensors."""
- return f"{DOMAIN}-{self.information.serial}-sensor-update"
-
async def async_setup(self):
"""Start interacting with the NAS."""
self.dsm = SynologyDSM(
@@ -322,32 +394,29 @@ async def async_setup(self):
self._entry.data[CONF_SSL],
self._entry.data[CONF_VERIFY_SSL],
timeout=self._entry.options.get(CONF_TIMEOUT),
- device_token=self._entry.data.get("device_token"),
+ device_token=self._entry.data.get(CONF_DEVICE_TOKEN),
)
await self._hass.async_add_executor_job(self.dsm.login)
+ # check if surveillance station is used
self._with_surveillance_station = bool(
self.dsm.apis.get(SynoSurveillanceStation.CAMERA_API_KEY)
)
+ _LOGGER.debug(
+ "State of Surveillance_station during setup of '%s': %s",
+ self._entry.unique_id,
+ self._with_surveillance_station,
+ )
self._async_setup_api_requests()
await self._hass.async_add_executor_job(self._fetch_device_configuration)
await self.async_update()
- self._unsub_dispatcher = async_track_time_interval(
- self._hass,
- self.async_update,
- timedelta(
- minutes=self._entry.options.get(
- CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
- )
- ),
- )
-
@callback
def subscribe(self, api_key, unique_id):
- """Subscribe an entity from API fetches."""
+ """Subscribe an entity to API fetches."""
+ _LOGGER.debug("Subscribe new entity: %s", unique_id)
if api_key not in self._fetching_entities:
self._fetching_entities[api_key] = set()
self._fetching_entities[api_key].add(unique_id)
@@ -355,7 +424,10 @@ def subscribe(self, api_key, unique_id):
@callback
def unsubscribe() -> None:
"""Unsubscribe an entity from API fetches (when disable)."""
+ _LOGGER.debug("Unsubscribe entity: %s", unique_id)
self._fetching_entities[api_key].remove(unique_id)
+ if len(self._fetching_entities[api_key]) == 0:
+ self._fetching_entities.pop(api_key)
return unsubscribe
@@ -364,14 +436,20 @@ def _async_setup_api_requests(self):
"""Determine if we should fetch each API, if one entity needs it."""
# Entities not added yet, fetch all
if not self._fetching_entities:
+ _LOGGER.debug(
+ "Entities not added yet, fetch all for '%s'", self._entry.unique_id
+ )
return
+ # surveillance_station is updated by own coordinator
+ self.dsm.reset(self.surveillance_station)
+
# Determine if we should fetch an API
+ self._with_system = bool(self.dsm.apis.get(SynoCoreSystem.API_KEY))
self._with_security = bool(
self._fetching_entities.get(SynoCoreSecurity.API_KEY)
)
self._with_storage = bool(self._fetching_entities.get(SynoStorage.API_KEY))
- self._with_system = bool(self._fetching_entities.get(SynoCoreSystem.API_KEY))
self._with_upgrade = bool(self._fetching_entities.get(SynoCoreUpgrade.API_KEY))
self._with_utilisation = bool(
self._fetching_entities.get(SynoCoreUtilization.API_KEY)
@@ -379,37 +457,45 @@ def _async_setup_api_requests(self):
self._with_information = bool(
self._fetching_entities.get(SynoDSMInformation.API_KEY)
)
- self._with_surveillance_station = bool(
- self._fetching_entities.get(SynoSurveillanceStation.CAMERA_API_KEY)
- ) or bool(
- self._fetching_entities.get(SynoSurveillanceStation.HOME_MODE_API_KEY)
- )
# Reset not used API, information is not reset since it's used in device_info
if not self._with_security:
+ _LOGGER.debug(
+ "Disable security api from being updated for '%s'",
+ self._entry.unique_id,
+ )
self.dsm.reset(self.security)
self.security = None
if not self._with_storage:
+ _LOGGER.debug(
+ "Disable storage api from being updatedf or '%s'", self._entry.unique_id
+ )
self.dsm.reset(self.storage)
self.storage = None
if not self._with_system:
+ _LOGGER.debug(
+ "Disable system api from being updated for '%s'", self._entry.unique_id
+ )
self.dsm.reset(self.system)
self.system = None
if not self._with_upgrade:
+ _LOGGER.debug(
+ "Disable upgrade api from being updated for '%s'", self._entry.unique_id
+ )
self.dsm.reset(self.upgrade)
self.upgrade = None
if not self._with_utilisation:
+ _LOGGER.debug(
+ "Disable utilisation api from being updated for '%s'",
+ self._entry.unique_id,
+ )
self.dsm.reset(self.utilisation)
self.utilisation = None
- if not self._with_surveillance_station:
- self.dsm.reset(self.surveillance_station)
- self.surveillance_station = None
-
def _fetch_device_configuration(self):
"""Fetch initial device config."""
self.information = self.dsm.information
@@ -417,43 +503,68 @@ def _fetch_device_configuration(self):
self.network.update()
if self._with_security:
+ _LOGGER.debug("Enable security api updates for '%s'", self._entry.unique_id)
self.security = self.dsm.security
if self._with_storage:
+ _LOGGER.debug("Enable storage api updates for '%s'", self._entry.unique_id)
self.storage = self.dsm.storage
if self._with_upgrade:
+ _LOGGER.debug("Enable upgrade api updates for '%s'", self._entry.unique_id)
self.upgrade = self.dsm.upgrade
if self._with_system:
+ _LOGGER.debug("Enable system api updates for '%s'", self._entry.unique_id)
self.system = self.dsm.system
if self._with_utilisation:
+ _LOGGER.debug(
+ "Enable utilisation api updates for '%s'", self._entry.unique_id
+ )
self.utilisation = self.dsm.utilisation
if self._with_surveillance_station:
+ _LOGGER.debug(
+ "Enable surveillance_station api updates for '%s'",
+ self._entry.unique_id,
+ )
self.surveillance_station = self.dsm.surveillance_station
async def async_reboot(self):
"""Reboot NAS."""
- if not self.system:
- _LOGGER.debug("async_reboot - System API not ready: %s", self)
- return
- await self._hass.async_add_executor_job(self.system.reboot)
+ try:
+ await self._hass.async_add_executor_job(self.system.reboot)
+ except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err:
+ _LOGGER.error(
+ "Reboot of '%s' not possible, please try again later",
+ self._entry.unique_id,
+ )
+ _LOGGER.debug("Exception:%s", err)
async def async_shutdown(self):
"""Shutdown NAS."""
- if not self.system:
- _LOGGER.debug("async_shutdown - System API not ready: %s", self)
- return
- await self._hass.async_add_executor_job(self.system.shutdown)
+ try:
+ await self._hass.async_add_executor_job(self.system.shutdown)
+ except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err:
+ _LOGGER.error(
+ "Shutdown of '%s' not possible, please try again later",
+ self._entry.unique_id,
+ )
+ _LOGGER.debug("Exception:%s", err)
async def async_unload(self):
"""Stop interacting with the NAS and prepare for removal from hass."""
- self._unsub_dispatcher()
+ try:
+ await self._hass.async_add_executor_job(self.dsm.logout)
+ except (SynologyDSMAPIErrorException, SynologyDSMRequestException) as err:
+ _LOGGER.debug(
+ "Logout from '%s' not possible:%s", self._entry.unique_id, err
+ )
async def async_update(self, now=None):
"""Update function for updating API information."""
+ _LOGGER.debug("Start data update for '%s'", self._entry.unique_id)
self._async_setup_api_requests()
try:
await self._hass.async_add_executor_job(
@@ -461,25 +572,29 @@ async def async_update(self, now=None):
)
except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err:
_LOGGER.warning(
- "async_update - connection error during update, fallback by reloading the entry"
+ "Connection error during update, fallback by reloading the entry"
+ )
+ _LOGGER.debug(
+ "Connection error during update of '%s' with exception: %s",
+ self._entry.unique_id,
+ err,
)
- _LOGGER.debug("async_update - exception: %s", err)
await self._hass.config_entries.async_reload(self._entry.entry_id)
return
- async_dispatcher_send(self._hass, self.signal_sensor_update)
-class SynologyDSMEntity(Entity):
+class SynologyDSMBaseEntity(CoordinatorEntity):
"""Representation of a Synology NAS entry."""
def __init__(
self,
api: SynoApi,
entity_type: str,
- entity_info: Dict[str, str],
+ entity_info: dict[str, str],
+ coordinator: DataUpdateCoordinator,
):
"""Initialize the Synology DSM entity."""
- super().__init__()
+ super().__init__(coordinator)
self._api = api
self._api_key = entity_type.split(":")[0]
@@ -506,25 +621,18 @@ def icon(self) -> str:
"""Return the icon."""
return self._icon
- @property
- def unit_of_measurement(self) -> str:
- """Return the unit the value is expressed in."""
- if self.entity_type in TEMP_SENSORS_KEYS:
- return self.hass.config.units.temperature_unit
- return self._unit
-
@property
def device_class(self) -> str:
"""Return the class of this device."""
return self._class
@property
- def device_state_attributes(self) -> Dict[str, any]:
+ def extra_state_attributes(self) -> dict[str, any]:
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
@property
- def device_info(self) -> Dict[str, any]:
+ def device_info(self) -> dict[str, any]:
"""Return the device information."""
return {
"identifiers": {(DOMAIN, self._api.information.serial)},
@@ -539,41 +647,25 @@ def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
return self._enable_default
- @property
- def should_poll(self) -> bool:
- """No polling needed."""
- return False
-
- async def async_update(self):
- """Only used by the generic entity update service."""
- if not self.enabled:
- return
-
- await self._api.async_update()
-
async def async_added_to_hass(self):
- """Register state update callback."""
- self.async_on_remove(
- async_dispatcher_connect(
- self.hass, self._api.signal_sensor_update, self.async_write_ha_state
- )
- )
-
+ """Register entity for updates from API."""
self.async_on_remove(self._api.subscribe(self._api_key, self.unique_id))
+ await super().async_added_to_hass()
-class SynologyDSMDeviceEntity(SynologyDSMEntity):
+class SynologyDSMDeviceEntity(SynologyDSMBaseEntity):
"""Representation of a Synology NAS disk or volume entry."""
def __init__(
self,
api: SynoApi,
entity_type: str,
- entity_info: Dict[str, str],
+ entity_info: dict[str, str],
+ coordinator: DataUpdateCoordinator,
device_id: str = None,
):
"""Initialize the Synology DSM disk or volume entity."""
- super().__init__(api, entity_type, entity_info)
+ super().__init__(api, entity_type, entity_info, coordinator)
self._device_id = device_id
self._device_name = None
self._device_manufacturer = None
@@ -610,7 +702,7 @@ def available(self) -> bool:
return bool(self._api.storage)
@property
- def device_info(self) -> Dict[str, any]:
+ def device_info(self) -> dict[str, any]:
"""Return the device information."""
return {
"identifiers": {(DOMAIN, self._api.information.serial, self._device_id)},
diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py
index 69f217a4b4e9c0..fb8ed5a23cdb5a 100644
--- a/homeassistant/components/synology_dsm/binary_sensor.py
+++ b/homeassistant/components/synology_dsm/binary_sensor.py
@@ -1,13 +1,14 @@
"""Support for Synology DSM binary sensors."""
-from typing import Dict
+from __future__ import annotations
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DISKS
from homeassistant.helpers.typing import HomeAssistantType
-from . import SynologyDSMDeviceEntity, SynologyDSMEntity
+from . import SynologyDSMBaseEntity, SynologyDSMDeviceEntity
from .const import (
+ COORDINATOR_CENTRAL,
DOMAIN,
SECURITY_BINARY_SENSORS,
STORAGE_DISK_BINARY_SENSORS,
@@ -21,18 +22,20 @@ async def async_setup_entry(
) -> None:
"""Set up the Synology NAS binary sensor."""
- api = hass.data[DOMAIN][entry.unique_id][SYNO_API]
+ data = hass.data[DOMAIN][entry.unique_id]
+ api = data[SYNO_API]
+ coordinator = data[COORDINATOR_CENTRAL]
entities = [
SynoDSMSecurityBinarySensor(
- api, sensor_type, SECURITY_BINARY_SENSORS[sensor_type]
+ api, sensor_type, SECURITY_BINARY_SENSORS[sensor_type], coordinator
)
for sensor_type in SECURITY_BINARY_SENSORS
]
entities += [
SynoDSMUpgradeBinarySensor(
- api, sensor_type, UPGRADE_BINARY_SENSORS[sensor_type]
+ api, sensor_type, UPGRADE_BINARY_SENSORS[sensor_type], coordinator
)
for sensor_type in UPGRADE_BINARY_SENSORS
]
@@ -42,7 +45,11 @@ async def async_setup_entry(
for disk in entry.data.get(CONF_DISKS, api.storage.disks_ids):
entities += [
SynoDSMStorageBinarySensor(
- api, sensor_type, STORAGE_DISK_BINARY_SENSORS[sensor_type], disk
+ api,
+ sensor_type,
+ STORAGE_DISK_BINARY_SENSORS[sensor_type],
+ coordinator,
+ disk,
)
for sensor_type in STORAGE_DISK_BINARY_SENSORS
]
@@ -50,7 +57,7 @@ async def async_setup_entry(
async_add_entities(entities)
-class SynoDSMSecurityBinarySensor(SynologyDSMEntity, BinarySensorEntity):
+class SynoDSMSecurityBinarySensor(SynologyDSMBaseEntity, BinarySensorEntity):
"""Representation a Synology Security binary sensor."""
@property
@@ -64,7 +71,7 @@ def available(self) -> bool:
return bool(self._api.security)
@property
- def device_state_attributes(self) -> Dict[str, str]:
+ def extra_state_attributes(self) -> dict[str, str]:
"""Return security checks details."""
return self._api.security.status_by_check
@@ -78,7 +85,7 @@ def is_on(self) -> bool:
return getattr(self._api.storage, self.entity_type)(self._device_id)
-class SynoDSMUpgradeBinarySensor(SynologyDSMEntity, BinarySensorEntity):
+class SynoDSMUpgradeBinarySensor(SynologyDSMBaseEntity, BinarySensorEntity):
"""Representation a Synology Upgrade binary sensor."""
@property
diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py
index 1dfd8ff945bd98..67052543569e37 100644
--- a/homeassistant/components/synology_dsm/camera.py
+++ b/homeassistant/components/synology_dsm/camera.py
@@ -1,15 +1,22 @@
"""Support for Synology DSM cameras."""
-from typing import Dict
+from __future__ import annotations
+
+import logging
from synology_dsm.api.surveillance_station import SynoSurveillanceStation
-from synology_dsm.api.surveillance_station.camera import SynoCamera
+from synology_dsm.exceptions import (
+ SynologyDSMAPIErrorException,
+ SynologyDSMRequestException,
+)
from homeassistant.components.camera import SUPPORT_STREAM, Camera
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
-from . import SynoApi, SynologyDSMEntity
+from . import SynoApi, SynologyDSMBaseEntity
from .const import (
+ COORDINATOR_CAMERAS,
DOMAIN,
ENTITY_CLASS,
ENTITY_ENABLE,
@@ -19,50 +26,72 @@
SYNO_API,
)
+_LOGGER = logging.getLogger(__name__)
+
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
) -> None:
- """Set up the Synology NAS binary sensor."""
+ """Set up the Synology NAS cameras."""
- api = hass.data[DOMAIN][entry.unique_id][SYNO_API]
+ data = hass.data[DOMAIN][entry.unique_id]
+ api = data[SYNO_API]
if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis:
return
- surveillance_station = api.surveillance_station
- await hass.async_add_executor_job(surveillance_station.update)
- cameras = surveillance_station.get_all_cameras()
- entities = [SynoDSMCamera(api, camera) for camera in cameras]
+ # initial data fetch
+ coordinator = data[COORDINATOR_CAMERAS]
+ await coordinator.async_refresh()
- async_add_entities(entities)
+ async_add_entities(
+ SynoDSMCamera(api, coordinator, camera_id)
+ for camera_id in coordinator.data["cameras"]
+ )
-class SynoDSMCamera(SynologyDSMEntity, Camera):
+class SynoDSMCamera(SynologyDSMBaseEntity, Camera):
"""Representation a Synology camera."""
- def __init__(self, api: SynoApi, camera: SynoCamera):
+ def __init__(
+ self, api: SynoApi, coordinator: DataUpdateCoordinator, camera_id: int
+ ):
"""Initialize a Synology camera."""
super().__init__(
api,
- f"{SynoSurveillanceStation.CAMERA_API_KEY}:{camera.id}",
+ f"{SynoSurveillanceStation.CAMERA_API_KEY}:{camera_id}",
{
- ENTITY_NAME: camera.name,
+ ENTITY_NAME: coordinator.data["cameras"][camera_id].name,
+ ENTITY_ENABLE: coordinator.data["cameras"][camera_id].is_enabled,
ENTITY_CLASS: None,
ENTITY_ICON: None,
- ENTITY_ENABLE: True,
ENTITY_UNIT: None,
},
+ coordinator,
)
- self._camera = camera
+ Camera.__init__(self)
+
+ self._camera_id = camera_id
+ self._api = api
+
+ @property
+ def camera_data(self):
+ """Camera data."""
+ return self.coordinator.data["cameras"][self._camera_id]
@property
- def device_info(self) -> Dict[str, any]:
+ def device_info(self) -> dict[str, any]:
"""Return the device information."""
return {
- "identifiers": {(DOMAIN, self._api.information.serial, self._camera.id)},
- "name": self._camera.name,
- "model": self._camera.model,
+ "identifiers": {
+ (
+ DOMAIN,
+ self._api.information.serial,
+ self.camera_data.id,
+ )
+ },
+ "name": self.camera_data.name,
+ "model": self.camera_data.model,
"via_device": (
DOMAIN,
self._api.information.serial,
@@ -73,7 +102,7 @@ def device_info(self) -> Dict[str, any]:
@property
def available(self) -> bool:
"""Return the availability of the camera."""
- return self._camera.is_enabled
+ return self.camera_data.is_enabled and self.coordinator.last_update_success
@property
def supported_features(self) -> int:
@@ -83,29 +112,57 @@ def supported_features(self) -> int:
@property
def is_recording(self):
"""Return true if the device is recording."""
- return self._camera.is_recording
+ return self.camera_data.is_recording
@property
def motion_detection_enabled(self):
"""Return the camera motion detection status."""
- return self._camera.is_motion_detection_enabled
+ return self.camera_data.is_motion_detection_enabled
def camera_image(self) -> bytes:
"""Return bytes of camera image."""
+ _LOGGER.debug(
+ "SynoDSMCamera.camera_image(%s)",
+ self.camera_data.name,
+ )
if not self.available:
return None
- return self._api.surveillance_station.get_camera_image(self._camera.id)
+ try:
+ return self._api.surveillance_station.get_camera_image(self._camera_id)
+ except (
+ SynologyDSMAPIErrorException,
+ SynologyDSMRequestException,
+ ConnectionRefusedError,
+ ) as err:
+ _LOGGER.debug(
+ "SynoDSMCamera.camera_image(%s) - Exception:%s",
+ self.camera_data.name,
+ err,
+ )
+ return None
async def stream_source(self) -> str:
"""Return the source of the stream."""
+ _LOGGER.debug(
+ "SynoDSMCamera.stream_source(%s)",
+ self.camera_data.name,
+ )
if not self.available:
return None
- return self._camera.live_view.rtsp
+ return self.camera_data.live_view.rtsp
def enable_motion_detection(self):
"""Enable motion detection in the camera."""
- self._api.surveillance_station.enable_motion_detection(self._camera.id)
+ _LOGGER.debug(
+ "SynoDSMCamera.enable_motion_detection(%s)",
+ self.camera_data.name,
+ )
+ self._api.surveillance_station.enable_motion_detection(self._camera_id)
def disable_motion_detection(self):
"""Disable motion detection in camera."""
- self._api.surveillance_station.disable_motion_detection(self._camera.id)
+ _LOGGER.debug(
+ "SynoDSMCamera.disable_motion_detection(%s)",
+ self.camera_data.name,
+ )
+ self._api.surveillance_station.disable_motion_detection(self._camera_id)
diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py
index 5a1ab53b3f765f..b3b26e892a8800 100644
--- a/homeassistant/components/synology_dsm/config_flow.py
+++ b/homeassistant/components/synology_dsm/config_flow.py
@@ -31,6 +31,7 @@
import homeassistant.helpers.config_validation as cv
from .const import (
+ CONF_DEVICE_TOKEN,
CONF_VOLUMES,
DEFAULT_PORT,
DEFAULT_PORT_SSL,
@@ -38,8 +39,8 @@
DEFAULT_TIMEOUT,
DEFAULT_USE_SSL,
DEFAULT_VERIFY_SSL,
+ DOMAIN,
)
-from .const import DOMAIN # pylint: disable=unused-import
_LOGGER = logging.getLogger(__name__)
@@ -180,7 +181,7 @@ async def async_step_user(self, user_input=None):
CONF_MAC: api.network.macs,
}
if otp_code:
- config_data["device_token"] = api.device_token
+ config_data[CONF_DEVICE_TOKEN] = api.device_token
if user_input.get(CONF_DISKS):
config_data[CONF_DISKS] = user_input[CONF_DISKS]
if user_input.get(CONF_VOLUMES):
@@ -208,7 +209,6 @@ async def async_step_ssdp(self, discovery_info):
CONF_NAME: friendly_name,
CONF_HOST: parsed_url.hostname,
}
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = self.discovered_conf
return await self.async_step_user()
diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py
index ba1a8034223a8c..ba1aa393c8594c 100644
--- a/homeassistant/components/synology_dsm/const.py
+++ b/homeassistant/components/synology_dsm/const.py
@@ -19,6 +19,10 @@
DOMAIN = "synology_dsm"
PLATFORMS = ["binary_sensor", "camera", "sensor", "switch"]
+COORDINATOR_CAMERAS = "coordinator_cameras"
+COORDINATOR_CENTRAL = "coordinator_central"
+COORDINATOR_SWITCHES = "coordinator_switches"
+SYSTEM_LOADED = "system_loaded"
# Entry keys
SYNO_API = "syno_api"
@@ -27,6 +31,7 @@
# Configuration
CONF_SERIAL = "serial"
CONF_VOLUMES = "volumes"
+CONF_DEVICE_TOKEN = "device_token"
DEFAULT_USE_SSL = True
DEFAULT_VERIFY_SSL = False
@@ -36,6 +41,7 @@
DEFAULT_SCAN_INTERVAL = 15 # min
DEFAULT_TIMEOUT = 10 # sec
+ENTITY_UNIT_LOAD = "load"
ENTITY_NAME = "name"
ENTITY_UNIT = "unit"
@@ -94,50 +100,50 @@
# Sensors
UTILISATION_SENSORS = {
f"{SynoCoreUtilization.API_KEY}:cpu_other_load": {
- ENTITY_NAME: "CPU Load (Other)",
+ ENTITY_NAME: "CPU Utilization (Other)",
ENTITY_UNIT: PERCENTAGE,
ENTITY_ICON: "mdi:chip",
ENTITY_CLASS: None,
ENTITY_ENABLE: False,
},
f"{SynoCoreUtilization.API_KEY}:cpu_user_load": {
- ENTITY_NAME: "CPU Load (User)",
+ ENTITY_NAME: "CPU Utilization (User)",
ENTITY_UNIT: PERCENTAGE,
ENTITY_ICON: "mdi:chip",
ENTITY_CLASS: None,
ENTITY_ENABLE: True,
},
f"{SynoCoreUtilization.API_KEY}:cpu_system_load": {
- ENTITY_NAME: "CPU Load (System)",
+ ENTITY_NAME: "CPU Utilization (System)",
ENTITY_UNIT: PERCENTAGE,
ENTITY_ICON: "mdi:chip",
ENTITY_CLASS: None,
ENTITY_ENABLE: False,
},
f"{SynoCoreUtilization.API_KEY}:cpu_total_load": {
- ENTITY_NAME: "CPU Load (Total)",
+ ENTITY_NAME: "CPU Utilization (Total)",
ENTITY_UNIT: PERCENTAGE,
ENTITY_ICON: "mdi:chip",
ENTITY_CLASS: None,
ENTITY_ENABLE: True,
},
f"{SynoCoreUtilization.API_KEY}:cpu_1min_load": {
- ENTITY_NAME: "CPU Load (1 min)",
- ENTITY_UNIT: PERCENTAGE,
+ ENTITY_NAME: "CPU Load Average (1 min)",
+ ENTITY_UNIT: ENTITY_UNIT_LOAD,
ENTITY_ICON: "mdi:chip",
ENTITY_CLASS: None,
ENTITY_ENABLE: False,
},
f"{SynoCoreUtilization.API_KEY}:cpu_5min_load": {
- ENTITY_NAME: "CPU Load (5 min)",
- ENTITY_UNIT: PERCENTAGE,
+ ENTITY_NAME: "CPU Load Average (5 min)",
+ ENTITY_UNIT: ENTITY_UNIT_LOAD,
ENTITY_ICON: "mdi:chip",
ENTITY_CLASS: None,
ENTITY_ENABLE: True,
},
f"{SynoCoreUtilization.API_KEY}:cpu_15min_load": {
- ENTITY_NAME: "CPU Load (15 min)",
- ENTITY_UNIT: PERCENTAGE,
+ ENTITY_NAME: "CPU Load Average (15 min)",
+ ENTITY_UNIT: ENTITY_UNIT_LOAD,
ENTITY_ICON: "mdi:chip",
ENTITY_CLASS: None,
ENTITY_ENABLE: True,
diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json
index 45cad8acfc2455..4da44942b4ff52 100644
--- a/homeassistant/components/synology_dsm/manifest.json
+++ b/homeassistant/components/synology_dsm/manifest.json
@@ -2,7 +2,7 @@
"domain": "synology_dsm",
"name": "Synology DSM",
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
- "requirements": ["synologydsm-api==1.0.1"],
+ "requirements": ["synologydsm-api==1.0.2"],
"codeowners": ["@hacf-fr", "@Quentame", "@mib1185"],
"config_flow": true,
"ssdp": [
diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py
index 31013451682570..22f41601e7baa2 100644
--- a/homeassistant/components/synology_dsm/sensor.py
+++ b/homeassistant/components/synology_dsm/sensor.py
@@ -1,7 +1,9 @@
"""Support for Synology DSM sensors."""
+from __future__ import annotations
+
from datetime import timedelta
-from typing import Dict
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_DISKS,
@@ -13,12 +15,15 @@
)
from homeassistant.helpers.temperature import display_temp
from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util.dt import utcnow
-from . import SynoApi, SynologyDSMDeviceEntity, SynologyDSMEntity
+from . import SynoApi, SynologyDSMBaseEntity, SynologyDSMDeviceEntity
from .const import (
CONF_VOLUMES,
+ COORDINATOR_CENTRAL,
DOMAIN,
+ ENTITY_UNIT_LOAD,
INFORMATION_SENSORS,
STORAGE_DISK_SENSORS,
STORAGE_VOL_SENSORS,
@@ -33,10 +38,14 @@ async def async_setup_entry(
) -> None:
"""Set up the Synology NAS Sensor."""
- api = hass.data[DOMAIN][entry.unique_id][SYNO_API]
+ data = hass.data[DOMAIN][entry.unique_id]
+ api = data[SYNO_API]
+ coordinator = data[COORDINATOR_CENTRAL]
entities = [
- SynoDSMUtilSensor(api, sensor_type, UTILISATION_SENSORS[sensor_type])
+ SynoDSMUtilSensor(
+ api, sensor_type, UTILISATION_SENSORS[sensor_type], coordinator
+ )
for sensor_type in UTILISATION_SENSORS
]
@@ -45,7 +54,11 @@ async def async_setup_entry(
for volume in entry.data.get(CONF_VOLUMES, api.storage.volumes_ids):
entities += [
SynoDSMStorageSensor(
- api, sensor_type, STORAGE_VOL_SENSORS[sensor_type], volume
+ api,
+ sensor_type,
+ STORAGE_VOL_SENSORS[sensor_type],
+ coordinator,
+ volume,
)
for sensor_type in STORAGE_VOL_SENSORS
]
@@ -55,20 +68,37 @@ async def async_setup_entry(
for disk in entry.data.get(CONF_DISKS, api.storage.disks_ids):
entities += [
SynoDSMStorageSensor(
- api, sensor_type, STORAGE_DISK_SENSORS[sensor_type], disk
+ api,
+ sensor_type,
+ STORAGE_DISK_SENSORS[sensor_type],
+ coordinator,
+ disk,
)
for sensor_type in STORAGE_DISK_SENSORS
]
entities += [
- SynoDSMInfoSensor(api, sensor_type, INFORMATION_SENSORS[sensor_type])
+ SynoDSMInfoSensor(
+ api, sensor_type, INFORMATION_SENSORS[sensor_type], coordinator
+ )
for sensor_type in INFORMATION_SENSORS
]
async_add_entities(entities)
-class SynoDSMUtilSensor(SynologyDSMEntity):
+class SynoDSMSensor(SynologyDSMBaseEntity):
+ """Mixin for sensor specific attributes."""
+
+ @property
+ def unit_of_measurement(self) -> str:
+ """Return the unit the value is expressed in."""
+ if self.entity_type in TEMP_SENSORS_KEYS:
+ return self.hass.config.units.temperature_unit
+ return self._unit
+
+
+class SynoDSMUtilSensor(SynoDSMSensor, SensorEntity):
"""Representation a Synology Utilisation sensor."""
@property
@@ -88,6 +118,10 @@ def state(self):
if self._unit == DATA_RATE_KILOBYTES_PER_SECOND:
return round(attr / 1024.0, 1)
+ # CPU load average
+ if self._unit == ENTITY_UNIT_LOAD:
+ return round(attr / 100, 2)
+
return attr
@property
@@ -96,7 +130,7 @@ def available(self) -> bool:
return bool(self._api.utilisation)
-class SynoDSMStorageSensor(SynologyDSMDeviceEntity):
+class SynoDSMStorageSensor(SynologyDSMDeviceEntity, SynoDSMSensor, SensorEntity):
"""Representation a Synology Storage sensor."""
@property
@@ -117,12 +151,18 @@ def state(self):
return attr
-class SynoDSMInfoSensor(SynologyDSMEntity):
+class SynoDSMInfoSensor(SynoDSMSensor, SensorEntity):
"""Representation a Synology information sensor."""
- def __init__(self, api: SynoApi, entity_type: str, entity_info: Dict[str, str]):
+ def __init__(
+ self,
+ api: SynoApi,
+ entity_type: str,
+ entity_info: dict[str, str],
+ coordinator: DataUpdateCoordinator,
+ ):
"""Initialize the Synology SynoDSMInfoSensor entity."""
- super().__init__(api, entity_type, entity_info)
+ super().__init__(api, entity_type, entity_info, coordinator)
self._previous_uptime = None
self._last_boot = None
diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py
index ee29c9f2692801..f9883b0c9162f2 100644
--- a/homeassistant/components/synology_dsm/switch.py
+++ b/homeassistant/components/synology_dsm/switch.py
@@ -1,14 +1,19 @@
"""Support for Synology DSM switch."""
-from typing import Dict
+from __future__ import annotations
+
+import logging
from synology_dsm.api.surveillance_station import SynoSurveillanceStation
from homeassistant.components.switch import ToggleEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+
+from . import SynoApi, SynologyDSMBaseEntity
+from .const import COORDINATOR_SWITCHES, DOMAIN, SURVEILLANCE_SWITCH, SYNO_API
-from . import SynoApi, SynologyDSMEntity
-from .const import DOMAIN, SURVEILLANCE_SWITCH, SYNO_API
+_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
@@ -16,16 +21,21 @@ async def async_setup_entry(
) -> None:
"""Set up the Synology NAS switch."""
- api = hass.data[DOMAIN][entry.unique_id][SYNO_API]
+ data = hass.data[DOMAIN][entry.unique_id]
+ api = data[SYNO_API]
entities = []
if SynoSurveillanceStation.INFO_API_KEY in api.dsm.apis:
info = await hass.async_add_executor_job(api.dsm.surveillance_station.get_info)
version = info["data"]["CMSMinVersion"]
+
+ # initial data fetch
+ coordinator = data[COORDINATOR_SWITCHES]
+ await coordinator.async_refresh()
entities += [
SynoDSMSurveillanceHomeModeToggle(
- api, sensor_type, SURVEILLANCE_SWITCH[sensor_type], version
+ api, sensor_type, SURVEILLANCE_SWITCH[sensor_type], version, coordinator
)
for sensor_type in SURVEILLANCE_SWITCH
]
@@ -33,46 +43,52 @@ async def async_setup_entry(
async_add_entities(entities, True)
-class SynoDSMSurveillanceHomeModeToggle(SynologyDSMEntity, ToggleEntity):
+class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, ToggleEntity):
"""Representation a Synology Surveillance Station Home Mode toggle."""
def __init__(
- self, api: SynoApi, entity_type: str, entity_info: Dict[str, str], version: str
+ self,
+ api: SynoApi,
+ entity_type: str,
+ entity_info: dict[str, str],
+ version: str,
+ coordinator: DataUpdateCoordinator,
):
"""Initialize a Synology Surveillance Station Home Mode."""
super().__init__(
api,
entity_type,
entity_info,
+ coordinator,
)
self._version = version
- self._state = None
@property
def is_on(self) -> bool:
"""Return the state."""
- if self.entity_type == "home_mode":
- return self._state
- return None
-
- @property
- def should_poll(self) -> bool:
- """No polling needed."""
- return True
-
- async def async_update(self):
- """Update the toggle state."""
- self._state = await self.hass.async_add_executor_job(
- self._api.surveillance_station.get_home_mode_status
- )
+ return self.coordinator.data["switches"][self.entity_type]
- def turn_on(self, **kwargs) -> None:
+ async def async_turn_on(self, **kwargs) -> None:
"""Turn on Home mode."""
- self._api.surveillance_station.set_home_mode(True)
+ _LOGGER.debug(
+ "SynoDSMSurveillanceHomeModeToggle.turn_on(%s)",
+ self._api.information.serial,
+ )
+ await self.hass.async_add_executor_job(
+ self._api.dsm.surveillance_station.set_home_mode, True
+ )
+ await self.coordinator.async_request_refresh()
- def turn_off(self, **kwargs) -> None:
+ async def async_turn_off(self, **kwargs) -> None:
"""Turn off Home mode."""
- self._api.surveillance_station.set_home_mode(False)
+ _LOGGER.debug(
+ "SynoDSMSurveillanceHomeModeToggle.turn_off(%s)",
+ self._api.information.serial,
+ )
+ await self.hass.async_add_executor_job(
+ self._api.dsm.surveillance_station.set_home_mode, False
+ )
+ await self.coordinator.async_request_refresh()
@property
def available(self) -> bool:
@@ -80,7 +96,7 @@ def available(self) -> bool:
return bool(self._api.surveillance_station)
@property
- def device_info(self) -> Dict[str, any]:
+ def device_info(self) -> dict[str, any]:
"""Return the device information."""
return {
"identifiers": {
diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json
index 303321ea94c6ad..f0d274c3bfe47c 100644
--- a/homeassistant/components/synology_dsm/translations/de.json
+++ b/homeassistant/components/synology_dsm/translations/de.json
@@ -1,13 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "Host bereits konfiguriert"
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"missing_data": "Fehlende Daten: Bitte versuchen Sie es sp\u00e4ter noch einmal oder eine andere Konfiguration",
"otp_failed": "Die zweistufige Authentifizierung ist fehlgeschlagen. Versuchen Sie es erneut mit einem neuen Code",
- "unknown": "Unbekannter Fehler: Bitte \u00fcberpr\u00fcfen Sie die Protokolle, um weitere Details zu erhalten"
+ "unknown": "Unerwarteter Fehler"
},
"flow_title": "Synology DSM {name} ({host})",
"step": {
@@ -21,7 +22,7 @@
"data": {
"password": "Passwort",
"port": "Port",
- "ssl": "Verwenden Sie SSL/TLS, um eine Verbindung zu Ihrem NAS herzustellen",
+ "ssl": "Verwendet ein SSL-Zertifikat",
"username": "Benutzername",
"verify_ssl": "SSL Zertifikat verifizieren"
},
@@ -33,7 +34,7 @@
"host": "Host",
"password": "Passwort",
"port": "Port",
- "ssl": "Verwenden Sie SSL/TLS, um eine Verbindung zu Ihrem NAS herzustellen",
+ "ssl": "Verwendet ein SSL-Zertifikat",
"username": "Benutzername",
"verify_ssl": "SSL Zertifikat verifizieren"
},
diff --git a/homeassistant/components/synology_dsm/translations/he.json b/homeassistant/components/synology_dsm/translations/he.json
index 98b3a2214d746f..8135fba13e030e 100644
--- a/homeassistant/components/synology_dsm/translations/he.json
+++ b/homeassistant/components/synology_dsm/translations/he.json
@@ -1,4 +1,20 @@
{
+ "config": {
+ "step": {
+ "link": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ },
"options": {
"step": {
"init": {
diff --git a/homeassistant/components/synology_dsm/translations/hu.json b/homeassistant/components/synology_dsm/translations/hu.json
index 29e520de432d6e..c26fd349f06646 100644
--- a/homeassistant/components/synology_dsm/translations/hu.json
+++ b/homeassistant/components/synology_dsm/translations/hu.json
@@ -1,23 +1,40 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
- "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
+ "flow_title": "Synology DSM {name} ({host})",
"step": {
+ "2sa": {
+ "data": {
+ "otp_code": "K\u00f3d"
+ }
+ },
"link": {
"data": {
"password": "Jelsz\u00f3",
"port": "Port",
- "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
- }
+ "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v",
+ "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se"
+ },
+ "title": "Synology DSM"
},
"user": {
"data": {
"host": "Hoszt",
"password": "Jelsz\u00f3",
"port": "Port",
- "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
- }
+ "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v",
+ "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se"
+ },
+ "title": "Synology DSM"
}
}
}
diff --git a/homeassistant/components/synology_dsm/translations/id.json b/homeassistant/components/synology_dsm/translations/id.json
new file mode 100644
index 00000000000000..e614c2578d4c1a
--- /dev/null
+++ b/homeassistant/components/synology_dsm/translations/id.json
@@ -0,0 +1,55 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "missing_data": "Data tidak tersedia: coba lagi nanti atau konfigurasikan lainnya",
+ "otp_failed": "Autentikasi dua langkah gagal, coba lagi dengan kode sandi baru",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "flow_title": "Synology DSM {name} ({host})",
+ "step": {
+ "2sa": {
+ "data": {
+ "otp_code": "Kode"
+ },
+ "title": "Synology DSM: autentikasi dua langkah"
+ },
+ "link": {
+ "data": {
+ "password": "Kata Sandi",
+ "port": "Port",
+ "ssl": "Menggunakan sertifikat SSL",
+ "username": "Nama Pengguna",
+ "verify_ssl": "Verifikasi sertifikat SSL"
+ },
+ "description": "Ingin menyiapkan {name} ({host})?",
+ "title": "Synology DSM"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "ssl": "Menggunakan sertifikat SSL",
+ "username": "Nama Pengguna",
+ "verify_ssl": "Verifikasi sertifikat SSL"
+ },
+ "title": "Synology DSM"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Interval pemindaian dalam menit",
+ "timeout": "Tenggang waktu (detik)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/synology_dsm/translations/ko.json b/homeassistant/components/synology_dsm/translations/ko.json
index 6989f6515a1e4e..da61e46731e12b 100644
--- a/homeassistant/components/synology_dsm/translations/ko.json
+++ b/homeassistant/components/synology_dsm/translations/ko.json
@@ -1,12 +1,14 @@
{
"config": {
"abort": {
- "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"missing_data": "\ub204\ub77d\ub41c \ub370\uc774\ud130: \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud558\uac70\ub098 \ub2e4\ub978 \uad6c\uc131\uc744 \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694",
"otp_failed": "2\ub2e8\uacc4 \uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \uc0c8\ub85c\uc6b4 \ud328\uc2a4 \ucf54\ub4dc\ub85c \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694",
- "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 \ub85c\uadf8\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694"
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"flow_title": "Synology DSM: {name} ({host})",
"step": {
@@ -20,10 +22,11 @@
"data": {
"password": "\ube44\ubc00\ubc88\ud638",
"port": "\ud3ec\ud2b8",
- "ssl": "SSL/TLS \ub97c \uc0ac\uc6a9\ud558\uc5ec NAS \uc5d0 \uc5f0\uacb0",
- "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984",
+ "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778"
},
- "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "description": "{name} ({host})\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "Synology DSM"
},
"user": {
@@ -31,8 +34,9 @@
"host": "\ud638\uc2a4\ud2b8",
"password": "\ube44\ubc00\ubc88\ud638",
"port": "\ud3ec\ud2b8",
- "ssl": "SSL/TLS \ub97c \uc0ac\uc6a9\ud558\uc5ec NAS \uc5d0 \uc5f0\uacb0",
- "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984",
+ "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778"
},
"title": "Synology DSM"
}
@@ -42,7 +46,7 @@
"step": {
"init": {
"data": {
- "scan_interval": "\uc2a4\uce94 \uac04\uaca9(\ubd84)",
+ "scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ubd84)",
"timeout": "\uc81c\ud55c \uc2dc\uac04 (\ucd08)"
}
}
diff --git a/homeassistant/components/synology_dsm/translations/nl.json b/homeassistant/components/synology_dsm/translations/nl.json
index d4932064a60057..be8d7d4534851e 100644
--- a/homeassistant/components/synology_dsm/translations/nl.json
+++ b/homeassistant/components/synology_dsm/translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Host is al geconfigureerd."
+ "already_configured": "Apparaat is al geconfigureerd"
},
"error": {
"cannot_connect": "Kan geen verbinding maken",
@@ -21,8 +21,8 @@
"link": {
"data": {
"password": "Wachtwoord",
- "port": "Poort (optioneel)",
- "ssl": "Gebruik SSL/TLS om verbinding te maken met uw NAS",
+ "port": "Poort",
+ "ssl": "Gebruik een SSL-certificaat",
"username": "Gebruikersnaam",
"verify_ssl": "Controleer het SSL-certificaat"
},
@@ -33,8 +33,8 @@
"data": {
"host": "Host",
"password": "Wachtwoord",
- "port": "Poort (optioneel)",
- "ssl": "Gebruik SSL/TLS om verbinding te maken met uw NAS",
+ "port": "Poort",
+ "ssl": "Gebruik een SSL-certificaat",
"username": "Gebruikersnaam",
"verify_ssl": "Controleer het SSL-certificaat"
},
diff --git a/homeassistant/components/synology_dsm/translations/ru.json b/homeassistant/components/synology_dsm/translations/ru.json
index ed3c2eea0a8ab0..98a7522b27d4c2 100644
--- a/homeassistant/components/synology_dsm/translations/ru.json
+++ b/homeassistant/components/synology_dsm/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"missing_data": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0435: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u0443\u044e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.",
"otp_failed": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u0441 \u043d\u043e\u0432\u044b\u043c \u043f\u0430\u0440\u043e\u043b\u0435\u043c.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
@@ -23,7 +23,7 @@
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
"ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
- "username": "\u041b\u043e\u0433\u0438\u043d",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f",
"verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL"
},
"description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?",
@@ -35,7 +35,7 @@
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
"ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
- "username": "\u041b\u043e\u0433\u0438\u043d",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f",
"verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL"
},
"title": "Synology DSM"
diff --git a/homeassistant/components/synology_dsm/translations/tr.json b/homeassistant/components/synology_dsm/translations/tr.json
index a7598bb343842a..681d85d2ef545e 100644
--- a/homeassistant/components/synology_dsm/translations/tr.json
+++ b/homeassistant/components/synology_dsm/translations/tr.json
@@ -1,15 +1,31 @@
{
"config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
"step": {
"link": {
"data": {
+ "password": "Parola",
+ "port": "Port",
+ "username": "Kullan\u0131c\u0131 Ad\u0131",
"verify_ssl": "SSL sertifikalar\u0131n\u0131 do\u011frula"
}
},
"user": {
"data": {
+ "host": "Ana Bilgisayar",
+ "password": "Parola",
+ "port": "Port",
+ "username": "Kullan\u0131c\u0131 Ad\u0131",
"verify_ssl": "SSL sertifikalar\u0131n\u0131 do\u011frula"
- }
+ },
+ "title": "Synology DSM"
}
}
}
diff --git a/homeassistant/components/synology_dsm/translations/uk.json b/homeassistant/components/synology_dsm/translations/uk.json
new file mode 100644
index 00000000000000..4d80350989fd68
--- /dev/null
+++ b/homeassistant/components/synology_dsm/translations/uk.json
@@ -0,0 +1,55 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "missing_data": "\u0412\u0456\u0434\u0441\u0443\u0442\u043d\u0456 \u0434\u0430\u043d\u0456: \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0442\u044c \u0441\u043f\u0440\u043e\u0431\u0443 \u043f\u0456\u0437\u043d\u0456\u0448\u0435 \u0430\u0431\u043e \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0456\u043d\u0448\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.",
+ "otp_failed": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437 \u0437 \u043d\u043e\u0432\u0438\u043c \u043f\u0430\u0440\u043e\u043b\u0435\u043c.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "flow_title": "Synology DSM {name} ({host})",
+ "step": {
+ "2sa": {
+ "data": {
+ "otp_code": "\u041a\u043e\u0434"
+ },
+ "title": "Synology DSM: \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f"
+ },
+ "link": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430",
+ "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL"
+ },
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} ({host})?",
+ "title": "Synology DSM"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430",
+ "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL"
+ },
+ "title": "Synology DSM"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0456\u0436 \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f\u043c\u0438 (\u0445\u0432.)",
+ "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/synology_dsm/translations/zh-Hant.json b/homeassistant/components/synology_dsm/translations/zh-Hant.json
index d5e78faf91c950..19a231ccd60526 100644
--- a/homeassistant/components/synology_dsm/translations/zh-Hant.json
+++ b/homeassistant/components/synology_dsm/translations/zh-Hant.json
@@ -7,7 +7,7 @@
"cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"missing_data": "\u7f3a\u5c11\u8cc7\u6599\uff1a\u8acb\u7a0d\u5f8c\u91cd\u8a66\u6216\u4f7f\u7528\u5176\u4ed6\u8a2d\u5b9a",
- "otp_failed": "\u5169\u6b65\u9a5f\u9a57\u8b49\u5931\u6557\uff0c\u8acb\u91cd\u65b0\u53d6\u5f97\u4ee3\u78bc\u5f8c\u91cd\u8a66",
+ "otp_failed": "\u96d9\u91cd\u8a8d\u8b49\u5931\u6557\uff0c\u8acb\u91cd\u65b0\u53d6\u5f97\u4ee3\u78bc\u5f8c\u91cd\u8a66",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"flow_title": "\u7fa4\u6689 DSM {name} ({host})",
@@ -16,7 +16,7 @@
"data": {
"otp_code": "\u4ee3\u78bc"
},
- "title": "Synology DSM\uff1a\u96d9\u91cd\u9a57\u8b49"
+ "title": "Synology DSM\uff1a\u96d9\u91cd\u8a8d\u8b49"
},
"link": {
"data": {
diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py
index c53cd9da1a5453..2ad4863dbec3dd 100644
--- a/homeassistant/components/system_health/__init__.py
+++ b/homeassistant/components/system_health/__init__.py
@@ -1,9 +1,11 @@
"""Support for System health ."""
+from __future__ import annotations
+
import asyncio
import dataclasses
from datetime import datetime
import logging
-from typing import Awaitable, Callable, Dict, Optional
+from typing import Awaitable, Callable
import aiohttp
import async_timeout
@@ -27,14 +29,14 @@
def async_register_info(
hass: HomeAssistant,
domain: str,
- info_callback: Callable[[HomeAssistant], Dict],
+ info_callback: Callable[[HomeAssistant], dict],
):
"""Register an info callback.
Deprecated.
"""
_LOGGER.warning(
- "system_health.async_register_info is deprecated. Add a system_health platform instead."
+ "Calling system_health.async_register_info is deprecated; Add a system_health platform instead"
)
hass.data.setdefault(DOMAIN, {})
SystemHealthRegistration(hass, domain).async_register_info(info_callback)
@@ -58,7 +60,7 @@ async def _register_system_health_platform(hass, integration_domain, platform):
async def get_integration_info(
- hass: HomeAssistant, registration: "SystemHealthRegistration"
+ hass: HomeAssistant, registration: SystemHealthRegistration
):
"""Get integration system health."""
try:
@@ -89,10 +91,10 @@ def _format_value(val):
@websocket_api.async_response
@websocket_api.websocket_command({vol.Required("type"): "system_health/info"})
async def handle_info(
- hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: Dict
+ hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
):
"""Handle an info request via a subscription."""
- registrations: Dict[str, SystemHealthRegistration] = hass.data[DOMAIN]
+ registrations: dict[str, SystemHealthRegistration] = hass.data[DOMAIN]
data = {}
pending_info = {}
@@ -187,14 +189,14 @@ class SystemHealthRegistration:
hass: HomeAssistant
domain: str
- info_callback: Optional[Callable[[HomeAssistant], Awaitable[Dict]]] = None
- manage_url: Optional[str] = None
+ info_callback: Callable[[HomeAssistant], Awaitable[dict]] | None = None
+ manage_url: str | None = None
@callback
def async_register_info(
self,
- info_callback: Callable[[HomeAssistant], Awaitable[Dict]],
- manage_url: Optional[str] = None,
+ info_callback: Callable[[HomeAssistant], Awaitable[dict]],
+ manage_url: str | None = None,
):
"""Register an info callback."""
self.info_callback = info_callback
@@ -203,7 +205,7 @@ def async_register_info(
async def async_check_can_reach_url(
- hass: HomeAssistant, url: str, more_info: Optional[str] = None
+ hass: HomeAssistant, url: str, more_info: str | None = None
) -> str:
"""Test if the url can be reached."""
session = aiohttp_client.async_get_clientsession(hass)
diff --git a/homeassistant/components/system_health/translations/id.json b/homeassistant/components/system_health/translations/id.json
new file mode 100644
index 00000000000000..309a6dd38d08d9
--- /dev/null
+++ b/homeassistant/components/system_health/translations/id.json
@@ -0,0 +1,3 @@
+{
+ "title": "Kesehatan Sistem"
+}
\ No newline at end of file
diff --git a/homeassistant/components/system_health/translations/uk.json b/homeassistant/components/system_health/translations/uk.json
index 267fcb83a61646..61f30782f04999 100644
--- a/homeassistant/components/system_health/translations/uk.json
+++ b/homeassistant/components/system_health/translations/uk.json
@@ -1,3 +1,3 @@
{
- "title": "\u0411\u0435\u0437\u043f\u0435\u043a\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0438"
+ "title": "\u0421\u0442\u0430\u043d \u0441\u0438\u0441\u0442\u0435\u043c\u0438"
}
\ No newline at end of file
diff --git a/homeassistant/components/system_log/services.yaml b/homeassistant/components/system_log/services.yaml
index 2545d47c82532d..a762c31f2054d5 100644
--- a/homeassistant/components/system_log/services.yaml
+++ b/homeassistant/components/system_log/services.yaml
@@ -1,15 +1,36 @@
clear:
+ name: Clear all
description: Clear all log entries.
write:
+ name: Write
description: Write log entry.
fields:
message:
- description: Message to log. [Required]
+ name: Message
+ description: Message to log.
+ required: true
example: Something went wrong
+ selector:
+ text:
level:
- description: "Log level: debug, info, warning, error, critical. Defaults to 'error'."
+ name: Level
+ description: "Log level: debug, info, warning, error, critical."
+ default: error
example: debug
+ selector:
+ select:
+ options:
+ - "debug"
+ - "info"
+ - "warning"
+ - "error"
+ - "critical"
logger:
- description: Logger name under which to log the message. Defaults to 'system_log.external'.
+ name: Logger
+ description:
+ Logger name under which to log the message. Defaults to
+ 'system_log.external'.
example: mycomponent.myplatform
+ selector:
+ text:
diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py
index ce856c04c64a26..94f747014a46d4 100644
--- a/homeassistant/components/systemmonitor/sensor.py
+++ b/homeassistant/components/systemmonitor/sensor.py
@@ -1,31 +1,46 @@
"""Support for monitoring the local system."""
+from __future__ import annotations
+
+import asyncio
+from dataclasses import dataclass
+import datetime
+from functools import lru_cache
import logging
import os
import socket
import sys
+from typing import Any, Callable, cast
import psutil
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_RESOURCES,
+ CONF_SCAN_INTERVAL,
CONF_TYPE,
DATA_GIBIBYTES,
DATA_MEBIBYTES,
DATA_RATE_MEGABYTES_PER_SECOND,
+ DEVICE_CLASS_TIMESTAMP,
+ EVENT_HOMEASSISTANT_STOP,
PERCENTAGE,
STATE_OFF,
STATE_ON,
TEMP_CELSIUS,
)
+from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.dispatcher import (
+ async_dispatcher_connect,
+ async_dispatcher_send,
+)
+from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL
+from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
import homeassistant.util.dt as dt_util
-# mypy: allow-untyped-defs, no-check-untyped-defs
-
_LOGGER = logging.getLogger(__name__)
CONF_ARG = "arg"
@@ -35,71 +50,80 @@
else:
CPU_ICON = "mdi:cpu-32-bit"
+SENSOR_TYPE_NAME = 0
+SENSOR_TYPE_UOM = 1
+SENSOR_TYPE_ICON = 2
+SENSOR_TYPE_DEVICE_CLASS = 3
+SENSOR_TYPE_MANDATORY_ARG = 4
+
+SIGNAL_SYSTEMMONITOR_UPDATE = "systemmonitor_update"
+
# Schema: [name, unit of measurement, icon, device class, flag if mandatory arg]
-SENSOR_TYPES = {
- "disk_free": ["Disk free", DATA_GIBIBYTES, "mdi:harddisk", None, False],
- "disk_use": ["Disk use", DATA_GIBIBYTES, "mdi:harddisk", None, False],
- "disk_use_percent": [
+SENSOR_TYPES: dict[str, tuple[str, str | None, str | None, str | None, bool]] = {
+ "disk_free": ("Disk free", DATA_GIBIBYTES, "mdi:harddisk", None, False),
+ "disk_use": ("Disk use", DATA_GIBIBYTES, "mdi:harddisk", None, False),
+ "disk_use_percent": (
"Disk use (percent)",
PERCENTAGE,
"mdi:harddisk",
None,
False,
- ],
- "ipv4_address": ["IPv4 address", "", "mdi:server-network", None, True],
- "ipv6_address": ["IPv6 address", "", "mdi:server-network", None, True],
- "last_boot": ["Last boot", "", "mdi:clock", "timestamp", False],
- "load_15m": ["Load (15m)", " ", CPU_ICON, None, False],
- "load_1m": ["Load (1m)", " ", CPU_ICON, None, False],
- "load_5m": ["Load (5m)", " ", CPU_ICON, None, False],
- "memory_free": ["Memory free", DATA_MEBIBYTES, "mdi:memory", None, False],
- "memory_use": ["Memory use", DATA_MEBIBYTES, "mdi:memory", None, False],
- "memory_use_percent": [
+ ),
+ "ipv4_address": ("IPv4 address", "", "mdi:server-network", None, True),
+ "ipv6_address": ("IPv6 address", "", "mdi:server-network", None, True),
+ "last_boot": ("Last boot", None, "mdi:clock", DEVICE_CLASS_TIMESTAMP, False),
+ "load_15m": ("Load (15m)", " ", CPU_ICON, None, False),
+ "load_1m": ("Load (1m)", " ", CPU_ICON, None, False),
+ "load_5m": ("Load (5m)", " ", CPU_ICON, None, False),
+ "memory_free": ("Memory free", DATA_MEBIBYTES, "mdi:memory", None, False),
+ "memory_use": ("Memory use", DATA_MEBIBYTES, "mdi:memory", None, False),
+ "memory_use_percent": (
"Memory use (percent)",
PERCENTAGE,
"mdi:memory",
None,
False,
- ],
- "network_in": ["Network in", DATA_MEBIBYTES, "mdi:server-network", None, True],
- "network_out": ["Network out", DATA_MEBIBYTES, "mdi:server-network", None, True],
- "packets_in": ["Packets in", " ", "mdi:server-network", None, True],
- "packets_out": ["Packets out", " ", "mdi:server-network", None, True],
- "throughput_network_in": [
+ ),
+ "network_in": ("Network in", DATA_MEBIBYTES, "mdi:server-network", None, True),
+ "network_out": ("Network out", DATA_MEBIBYTES, "mdi:server-network", None, True),
+ "packets_in": ("Packets in", " ", "mdi:server-network", None, True),
+ "packets_out": ("Packets out", " ", "mdi:server-network", None, True),
+ "throughput_network_in": (
"Network throughput in",
DATA_RATE_MEGABYTES_PER_SECOND,
"mdi:server-network",
None,
True,
- ],
- "throughput_network_out": [
+ ),
+ "throughput_network_out": (
"Network throughput out",
DATA_RATE_MEGABYTES_PER_SECOND,
"mdi:server-network",
+ None,
True,
- ],
- "process": ["Process", " ", CPU_ICON, None, True],
- "processor_use": ["Processor use (percent)", PERCENTAGE, CPU_ICON, None, False],
- "processor_temperature": [
+ ),
+ "process": ("Process", " ", CPU_ICON, None, True),
+ "processor_use": ("Processor use (percent)", PERCENTAGE, CPU_ICON, None, False),
+ "processor_temperature": (
"Processor temperature",
TEMP_CELSIUS,
CPU_ICON,
None,
False,
- ],
- "swap_free": ["Swap free", DATA_MEBIBYTES, "mdi:harddisk", None, False],
- "swap_use": ["Swap use", DATA_MEBIBYTES, "mdi:harddisk", None, False],
- "swap_use_percent": ["Swap use (percent)", PERCENTAGE, "mdi:harddisk", None, False],
+ ),
+ "swap_free": ("Swap free", DATA_MEBIBYTES, "mdi:harddisk", None, False),
+ "swap_use": ("Swap use", DATA_MEBIBYTES, "mdi:harddisk", None, False),
+ "swap_use_percent": ("Swap use (percent)", PERCENTAGE, "mdi:harddisk", None, False),
}
-def check_required_arg(value):
+def check_required_arg(value: Any) -> Any:
"""Validate that the required "arg" for the sensor types that need it are set."""
for sensor in value:
sensor_type = sensor[CONF_TYPE]
sensor_arg = sensor.get(CONF_ARG)
- if sensor_arg is None and SENSOR_TYPES[sensor_type][4]:
+ if sensor_arg is None and SENSOR_TYPES[sensor_type][SENSOR_TYPE_MANDATORY_ARG]:
raise vol.RequiredFieldInvalid(
f"Mandatory 'arg' is missing for sensor type '{sensor_type}'."
)
@@ -155,195 +179,335 @@ def check_required_arg(value):
"radeon 1",
"soc-thermal 1",
"soc_thermal 1",
+ "Tctl",
+ "cpu0-thermal",
]
-def setup_platform(hass, config, add_entities, discovery_info=None):
+@dataclass
+class SensorData:
+ """Data for a sensor."""
+
+ argument: Any
+ state: str | None
+ value: Any | None
+ update_time: datetime.datetime | None
+ last_exception: BaseException | None
+
+
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: Callable,
+ discovery_info: Any | None = None,
+) -> None:
"""Set up the system monitor sensors."""
- dev = []
+ entities = []
+ sensor_registry: dict[str, SensorData] = {}
+
for resource in config[CONF_RESOURCES]:
+ type_ = resource[CONF_TYPE]
# Initialize the sensor argument if none was provided.
# For disk monitoring default to "/" (root) to prevent runtime errors, if argument was not specified.
if CONF_ARG not in resource:
+ argument = ""
if resource[CONF_TYPE].startswith("disk_"):
- resource[CONF_ARG] = "/"
- else:
- resource[CONF_ARG] = ""
+ argument = "/"
+ else:
+ argument = resource[CONF_ARG]
# Verify if we can retrieve CPU / processor temperatures.
# If not, do not create the entity and add a warning to the log
- if resource[CONF_TYPE] == "processor_temperature":
- if SystemMonitorSensor.read_cpu_temperature() is None:
- _LOGGER.warning("Cannot read CPU / processor temperature information.")
- continue
+ if (
+ type_ == "processor_temperature"
+ and await hass.async_add_executor_job(_read_cpu_temperature) is None
+ ):
+ _LOGGER.warning("Cannot read CPU / processor temperature information")
+ continue
+
+ sensor_registry[type_] = SensorData(argument, None, None, None, None)
+ entities.append(SystemMonitorSensor(sensor_registry, type_, argument))
+
+ scan_interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
+ await async_setup_sensor_registry_updates(hass, sensor_registry, scan_interval)
+
+ async_add_entities(entities)
- dev.append(SystemMonitorSensor(resource[CONF_TYPE], resource[CONF_ARG]))
- add_entities(dev, True)
+async def async_setup_sensor_registry_updates(
+ hass: HomeAssistant,
+ sensor_registry: dict[str, SensorData],
+ scan_interval: datetime.timedelta,
+) -> None:
+ """Update the registry and create polling."""
+
+ _update_lock = asyncio.Lock()
+
+ def _update_sensors() -> None:
+ """Update sensors and store the result in the registry."""
+ for type_, data in sensor_registry.items():
+ try:
+ state, value, update_time = _update(type_, data)
+ except Exception as ex: # pylint: disable=broad-except
+ _LOGGER.exception("Error updating sensor: %s", type_)
+ data.last_exception = ex
+ else:
+ data.state = state
+ data.value = value
+ data.update_time = update_time
+ data.last_exception = None
+
+ # Only fetch these once per iteration as we use the same
+ # data source multiple times in _update
+ _disk_usage.cache_clear()
+ _swap_memory.cache_clear()
+ _virtual_memory.cache_clear()
+ _net_io_counters.cache_clear()
+ _net_if_addrs.cache_clear()
+ _getloadavg.cache_clear()
+
+ async def _async_update_data(*_: Any) -> None:
+ """Update all sensors in one executor jump."""
+ if _update_lock.locked():
+ _LOGGER.warning(
+ "Updating systemmonitor took longer than the scheduled update interval %s",
+ scan_interval,
+ )
+ return
+
+ async with _update_lock:
+ await hass.async_add_executor_job(_update_sensors)
+ async_dispatcher_send(hass, SIGNAL_SYSTEMMONITOR_UPDATE)
+ polling_remover = async_track_time_interval(hass, _async_update_data, scan_interval)
-class SystemMonitorSensor(Entity):
+ @callback
+ def _async_stop_polling(*_: Any) -> None:
+ polling_remover()
+
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_polling)
+
+ await _async_update_data()
+
+
+class SystemMonitorSensor(SensorEntity):
"""Implementation of a system monitor sensor."""
- def __init__(self, sensor_type, argument=""):
+ def __init__(
+ self,
+ sensor_registry: dict[str, SensorData],
+ sensor_type: str,
+ argument: str = "",
+ ) -> None:
"""Initialize the sensor."""
- self._name = "{} {}".format(SENSOR_TYPES[sensor_type][0], argument)
- self._unique_id = slugify(f"{sensor_type}_{argument}")
- self.argument = argument
- self.type = sensor_type
- self._state = None
- self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
- self._available = True
- if sensor_type in ["throughput_network_out", "throughput_network_in"]:
- self._last_value = None
- self._last_update_time = None
+ self._type: str = sensor_type
+ self._name: str = f"{self.sensor_type[SENSOR_TYPE_NAME]} {argument}".rstrip()
+ self._unique_id: str = slugify(f"{sensor_type}_{argument}")
+ self._sensor_registry = sensor_registry
@property
- def name(self):
+ def name(self) -> str:
"""Return the name of the sensor."""
- return self._name.rstrip()
+ return self._name
@property
- def unique_id(self):
+ def unique_id(self) -> str:
"""Return the unique ID."""
return self._unique_id
@property
- def device_class(self):
+ def device_class(self) -> str | None:
"""Return the class of this sensor."""
- return SENSOR_TYPES[self.type][3]
+ return self.sensor_type[SENSOR_TYPE_DEVICE_CLASS] # type: ignore[no-any-return]
@property
- def icon(self):
+ def icon(self) -> str | None:
"""Icon to use in the frontend, if any."""
- return SENSOR_TYPES[self.type][2]
+ return self.sensor_type[SENSOR_TYPE_ICON] # type: ignore[no-any-return]
@property
- def state(self):
+ def state(self) -> str | None:
"""Return the state of the device."""
- return self._state
+ return self.data.state
@property
- def unit_of_measurement(self):
+ def unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of this entity, if any."""
- return self._unit_of_measurement
+ return self.sensor_type[SENSOR_TYPE_UOM] # type: ignore[no-any-return]
@property
- def available(self):
+ def available(self) -> bool:
"""Return True if entity is available."""
- return self._available
-
- def update(self):
- """Get the latest system information."""
- if self.type == "disk_use_percent":
- self._state = psutil.disk_usage(self.argument).percent
- elif self.type == "disk_use":
- self._state = round(psutil.disk_usage(self.argument).used / 1024 ** 3, 1)
- elif self.type == "disk_free":
- self._state = round(psutil.disk_usage(self.argument).free / 1024 ** 3, 1)
- elif self.type == "memory_use_percent":
- self._state = psutil.virtual_memory().percent
- elif self.type == "memory_use":
- virtual_memory = psutil.virtual_memory()
- self._state = round(
- (virtual_memory.total - virtual_memory.available) / 1024 ** 2, 1
+ return self.data.last_exception is None
+
+ @property
+ def should_poll(self) -> bool:
+ """Entity does not poll."""
+ return False
+
+ @property
+ def sensor_type(self) -> list:
+ """Return sensor type data for the sensor."""
+ return SENSOR_TYPES[self._type] # type: ignore
+
+ @property
+ def data(self) -> SensorData:
+ """Return registry entry for the data."""
+ return self._sensor_registry[self._type]
+
+ async def async_added_to_hass(self) -> None:
+ """When entity is added to hass."""
+ await super().async_added_to_hass()
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass, SIGNAL_SYSTEMMONITOR_UPDATE, self.async_write_ha_state
)
- elif self.type == "memory_free":
- self._state = round(psutil.virtual_memory().available / 1024 ** 2, 1)
- elif self.type == "swap_use_percent":
- self._state = psutil.swap_memory().percent
- elif self.type == "swap_use":
- self._state = round(psutil.swap_memory().used / 1024 ** 2, 1)
- elif self.type == "swap_free":
- self._state = round(psutil.swap_memory().free / 1024 ** 2, 1)
- elif self.type == "processor_use":
- self._state = round(psutil.cpu_percent(interval=None))
- elif self.type == "processor_temperature":
- self._state = self.read_cpu_temperature()
- elif self.type == "process":
- for proc in psutil.process_iter():
- try:
- if self.argument == proc.name():
- self._state = STATE_ON
- return
- except psutil.NoSuchProcess as err:
- _LOGGER.warning(
- "Failed to load process with ID: %s, old name: %s",
- err.pid,
- err.name,
- )
- self._state = STATE_OFF
- elif self.type == "network_out" or self.type == "network_in":
- counters = psutil.net_io_counters(pernic=True)
- if self.argument in counters:
- counter = counters[self.argument][IO_COUNTER[self.type]]
- self._state = round(counter / 1024 ** 2, 1)
- else:
- self._state = None
- elif self.type == "packets_out" or self.type == "packets_in":
- counters = psutil.net_io_counters(pernic=True)
- if self.argument in counters:
- self._state = counters[self.argument][IO_COUNTER[self.type]]
- else:
- self._state = None
- elif (
- self.type == "throughput_network_out"
- or self.type == "throughput_network_in"
- ):
- counters = psutil.net_io_counters(pernic=True)
- if self.argument in counters:
- counter = counters[self.argument][IO_COUNTER[self.type]]
- now = dt_util.utcnow()
- if self._last_value and self._last_value < counter:
- self._state = round(
- (counter - self._last_value)
- / 1000 ** 2
- / (now - self._last_update_time).seconds,
- 3,
- )
- else:
- self._state = None
- self._last_update_time = now
- self._last_value = counter
- else:
- self._state = None
- elif self.type == "ipv4_address" or self.type == "ipv6_address":
- addresses = psutil.net_if_addrs()
- if self.argument in addresses:
- for addr in addresses[self.argument]:
- if addr.family == IF_ADDRS_FAMILY[self.type]:
- self._state = addr.address
+ )
+
+
+def _update(
+ type_: str, data: SensorData
+) -> tuple[str | None, str | None, datetime.datetime | None]:
+ """Get the latest system information."""
+ state = None
+ value = None
+ update_time = None
+
+ if type_ == "disk_use_percent":
+ state = _disk_usage(data.argument).percent
+ elif type_ == "disk_use":
+ state = round(_disk_usage(data.argument).used / 1024 ** 3, 1)
+ elif type_ == "disk_free":
+ state = round(_disk_usage(data.argument).free / 1024 ** 3, 1)
+ elif type_ == "memory_use_percent":
+ state = _virtual_memory().percent
+ elif type_ == "memory_use":
+ virtual_memory = _virtual_memory()
+ state = round((virtual_memory.total - virtual_memory.available) / 1024 ** 2, 1)
+ elif type_ == "memory_free":
+ state = round(_virtual_memory().available / 1024 ** 2, 1)
+ elif type_ == "swap_use_percent":
+ state = _swap_memory().percent
+ elif type_ == "swap_use":
+ state = round(_swap_memory().used / 1024 ** 2, 1)
+ elif type_ == "swap_free":
+ state = round(_swap_memory().free / 1024 ** 2, 1)
+ elif type_ == "processor_use":
+ state = round(psutil.cpu_percent(interval=None))
+ elif type_ == "processor_temperature":
+ state = _read_cpu_temperature()
+ elif type_ == "process":
+ state = STATE_OFF
+ for proc in psutil.process_iter():
+ try:
+ if data.argument == proc.name():
+ state = STATE_ON
+ break
+ except psutil.NoSuchProcess as err:
+ _LOGGER.warning(
+ "Failed to load process with ID: %s, old name: %s",
+ err.pid,
+ err.name,
+ )
+ elif type_ in ["network_out", "network_in"]:
+ counters = _net_io_counters()
+ if data.argument in counters:
+ counter = counters[data.argument][IO_COUNTER[type_]]
+ state = round(counter / 1024 ** 2, 1)
+ else:
+ state = None
+ elif type_ in ["packets_out", "packets_in"]:
+ counters = _net_io_counters()
+ if data.argument in counters:
+ state = counters[data.argument][IO_COUNTER[type_]]
+ else:
+ state = None
+ elif type_ in ["throughput_network_out", "throughput_network_in"]:
+ counters = _net_io_counters()
+ if data.argument in counters:
+ counter = counters[data.argument][IO_COUNTER[type_]]
+ now = dt_util.utcnow()
+ if data.value and data.value < counter:
+ state = round(
+ (counter - data.value)
+ / 1000 ** 2
+ / (now - (data.update_time or now)).seconds,
+ 3,
+ )
else:
- self._state = None
- elif self.type == "last_boot":
- # Only update on initial setup
- if self._state is None:
- self._state = dt_util.as_local(
- dt_util.utc_from_timestamp(psutil.boot_time())
- ).isoformat()
- elif self.type == "load_1m":
- self._state = round(os.getloadavg()[0], 2)
- elif self.type == "load_5m":
- self._state = round(os.getloadavg()[1], 2)
- elif self.type == "load_15m":
- self._state = round(os.getloadavg()[2], 2)
-
- @staticmethod
- def read_cpu_temperature():
- """Attempt to read CPU / processor temperature."""
- temps = psutil.sensors_temperatures()
-
- for name, entries in temps.items():
- i = 1
- for entry in entries:
- # In case the label is empty (e.g. on Raspberry PI 4),
- # construct it ourself here based on the sensor key name.
- if not entry.label:
- _label = f"{name} {i}"
- else:
- _label = entry.label
-
- if _label in CPU_SENSOR_PREFIXES:
- return round(entry.current, 1)
-
- i += 1
+ state = None
+ update_time = now
+ value = counter
+ else:
+ state = None
+ elif type_ in ["ipv4_address", "ipv6_address"]:
+ addresses = _net_if_addrs()
+ if data.argument in addresses:
+ for addr in addresses[data.argument]:
+ if addr.family == IF_ADDRS_FAMILY[type_]:
+ state = addr.address
+ else:
+ state = None
+ elif type_ == "last_boot":
+ # Only update on initial setup
+ if data.state is None:
+ state = dt_util.utc_from_timestamp(psutil.boot_time()).isoformat()
+ else:
+ state = data.state
+ elif type_ == "load_1m":
+ state = round(_getloadavg()[0], 2)
+ elif type_ == "load_5m":
+ state = round(_getloadavg()[1], 2)
+ elif type_ == "load_15m":
+ state = round(_getloadavg()[2], 2)
+
+ return state, value, update_time
+
+
+# When we drop python 3.8 support these can be switched to
+# @cache https://docs.python.org/3.9/library/functools.html#functools.cache
+@lru_cache(maxsize=None)
+def _disk_usage(path: str) -> Any:
+ return psutil.disk_usage(path)
+
+
+@lru_cache(maxsize=None)
+def _swap_memory() -> Any:
+ return psutil.swap_memory()
+
+
+@lru_cache(maxsize=None)
+def _virtual_memory() -> Any:
+ return psutil.virtual_memory()
+
+
+@lru_cache(maxsize=None)
+def _net_io_counters() -> Any:
+ return psutil.net_io_counters(pernic=True)
+
+
+@lru_cache(maxsize=None)
+def _net_if_addrs() -> Any:
+ return psutil.net_if_addrs()
+
+
+@lru_cache(maxsize=None)
+def _getloadavg() -> tuple[float, float, float]:
+ return os.getloadavg()
+
+
+def _read_cpu_temperature() -> float | None:
+ """Attempt to read CPU / processor temperature."""
+ temps = psutil.sensors_temperatures()
+
+ for name, entries in temps.items():
+ for i, entry in enumerate(entries, start=1):
+ # In case the label is empty (e.g. on Raspberry PI 4),
+ # construct it ourself here based on the sensor key name.
+ _label = f"{name} {i}" if not entry.label else entry.label
+ # check both name and label because some systems embed cpu# in the
+ # name, which makes label not match because label adds cpu# at end.
+ if _label in CPU_SENSOR_PREFIXES or name in CPU_SENSOR_PREFIXES:
+ return cast(float, round(entry.current, 1))
+
+ return None
diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py
index e88fb4c60b8267..094465d38aaaba 100644
--- a/homeassistant/components/tado/__init__.py
+++ b/homeassistant/components/tado/__init__.py
@@ -31,10 +31,10 @@
_LOGGER = logging.getLogger(__name__)
-TADO_COMPONENTS = ["binary_sensor", "sensor", "climate", "water_heater"]
+PLATFORMS = ["binary_sensor", "sensor", "climate", "water_heater"]
-MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
-SCAN_INTERVAL = timedelta(seconds=15)
+MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4)
+SCAN_INTERVAL = timedelta(minutes=5)
CONFIG_SCHEMA = cv.deprecated(DOMAIN)
@@ -92,9 +92,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
UPDATE_LISTENER: update_listener,
}
- for component in TADO_COMPONENTS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -118,8 +118,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in TADO_COMPONENTS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -144,11 +144,13 @@ def __init__(self, hass, username, password, fallback):
self._fallback = fallback
self.home_id = None
+ self.home_name = None
self.tado = None
self.zones = None
self.devices = None
self.data = {
"device": {},
+ "weather": {},
"zone": {},
}
@@ -164,7 +166,9 @@ def setup(self):
# Load zones and devices
self.zones = self.tado.getZones()
self.devices = self.tado.getDevices()
- self.home_id = self.tado.getMe()["homes"][0]["id"]
+ tado_home = self.tado.getMe()["homes"][0]
+ self.home_id = tado_home["id"]
+ self.home_name = tado_home["name"]
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
@@ -173,6 +177,11 @@ def update(self):
self.update_sensor("device", device["shortSerialNo"])
for zone in self.zones:
self.update_sensor("zone", zone["id"])
+ self.data["weather"] = self.tado.getWeather()
+ dispatcher_send(
+ self.hass,
+ SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "weather", "data"),
+ )
def update_sensor(self, sensor_type, sensor):
"""Update the internal data from Tado."""
diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py
index 1acefdb4c1689b..9f68aa8a4e7758 100644
--- a/homeassistant/components/tado/binary_sensor.py
+++ b/homeassistant/components/tado/binary_sensor.py
@@ -183,6 +183,7 @@ def __init__(self, tado, zone_name, zone_id, zone_variable):
self._unique_id = f"{zone_variable} {zone_id} {tado.home_id}"
self._state = None
+ self._state_attributes = None
self._tado_zone_data = None
async def async_added_to_hass(self):
@@ -229,6 +230,11 @@ def device_class(self):
return DEVICE_CLASS_POWER
return None
+ @property
+ def extra_state_attributes(self):
+ """Return the state attributes."""
+ return self._state_attributes
+
@callback
def _async_update_callback(self):
"""Update and write state."""
@@ -244,13 +250,17 @@ def _async_update_zone_data(self):
return
if self.zone_variable == "power":
- self._state = self._tado_zone_data.power
+ self._state = self._tado_zone_data.power == "ON"
elif self.zone_variable == "link":
- self._state = self._tado_zone_data.link
+ self._state = self._tado_zone_data.link == "ONLINE"
elif self.zone_variable == "overlay":
self._state = self._tado_zone_data.overlay_active
+ if self._tado_zone_data.overlay_active:
+ self._state_attributes = {
+ "termination": self._tado_zone_data.overlay_termination_type
+ }
elif self.zone_variable == "early start":
self._state = self._tado_zone_data.preparation
@@ -260,3 +270,4 @@ def _async_update_zone_data(self):
self._tado_zone_data.open_window
or self._tado_zone_data.open_window_detected
)
+ self._state_attributes = self._tado_zone_data.open_window_attr
diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py
index 9547617a36b5b4..b86eb08b1b0075 100644
--- a/homeassistant/components/tado/climate.py
+++ b/homeassistant/components/tado/climate.py
@@ -462,7 +462,7 @@ def swing_modes(self):
return None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return temperature offset."""
return self._tado_zone_temp_offset
diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py
index 6c1f06b2626775..5f97212abf34f3 100644
--- a/homeassistant/components/tado/config_flow.py
+++ b/homeassistant/components/tado/config_flow.py
@@ -9,8 +9,7 @@
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
-from .const import CONF_FALLBACK, UNIQUE_ID
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import CONF_FALLBACK, DOMAIN, UNIQUE_ID
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py
index 6e009df7ca29cc..2c86fa2d6423f1 100644
--- a/homeassistant/components/tado/const.py
+++ b/homeassistant/components/tado/const.py
@@ -48,6 +48,21 @@
DATA = "data"
UPDATE_TRACK = "update_track"
+# Weather
+CONDITIONS_MAP = {
+ "clear-night": {"NIGHT_CLEAR"},
+ "cloudy": {"CLOUDY", "CLOUDY_MOSTLY", "NIGHT_CLOUDY"},
+ "fog": {"FOGGY"},
+ "hail": {"HAIL", "RAIN_HAIL"},
+ "lightning": {"THUNDERSTORM"},
+ "partlycloudy": {"CLOUDY_PARTLY"},
+ "rainy": {"DRIZZLE", "RAIN", "SCATTERED_RAIN"},
+ "snowy": {"FREEZING", "SCATTERED_SNOW", "SNOW"},
+ "snowy-rainy": {"RAIN_SNOW", "SCATTERED_RAIN_SNOW"},
+ "sunny": {"SUN"},
+ "windy": {"WIND"},
+}
+
# Types
TYPE_AIR_CONDITIONING = "AIR_CONDITIONING"
TYPE_HEATING = "HEATING"
@@ -149,6 +164,7 @@
DEFAULT_NAME = "Tado"
+TADO_HOME = "Home"
TADO_ZONE = "Zone"
UPDATE_LISTENER = "update_listener"
diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py
index 8de938af985797..afa8bc6a604a03 100644
--- a/homeassistant/components/tado/device_tracker.py
+++ b/homeassistant/components/tado/device_tracker.py
@@ -131,11 +131,10 @@ async def _async_update_info(self):
# Find devices that have geofencing enabled, and are currently at home.
for mobile_device in tado_json:
- if mobile_device.get("location"):
- if mobile_device["location"]["atHome"]:
- device_id = mobile_device["id"]
- device_name = mobile_device["name"]
- last_results.append(Device(device_id, device_name))
+ if mobile_device.get("location") and mobile_device["location"]["atHome"]:
+ device_id = mobile_device["id"]
+ device_name = mobile_device["name"]
+ last_results.append(Device(device_id, device_name))
self.last_results = last_results
diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py
index e9fefe2848bb4a..270d6f1e9115bf 100644
--- a/homeassistant/components/tado/entity.py
+++ b/homeassistant/components/tado/entity.py
@@ -1,7 +1,7 @@
"""Base class for Tado entity."""
from homeassistant.helpers.entity import Entity
-from .const import DEFAULT_NAME, DOMAIN, TADO_ZONE
+from .const import DEFAULT_NAME, DOMAIN, TADO_HOME, TADO_ZONE
class TadoDeviceEntity(Entity):
@@ -32,6 +32,26 @@ def should_poll(self):
return False
+class TadoHomeEntity(Entity):
+ """Base implementation for Tado home."""
+
+ def __init__(self, tado):
+ """Initialize a Tado home."""
+ super().__init__()
+ self.home_name = tado.home_name
+ self.home_id = tado.home_id
+
+ @property
+ def device_info(self):
+ """Return the device_info of the device."""
+ return {
+ "identifiers": {(DOMAIN, self.home_id)},
+ "name": self.home_name,
+ "manufacturer": DEFAULT_NAME,
+ "model": TADO_HOME,
+ }
+
+
class TadoZoneEntity(Entity):
"""Base implementation for Tado zone."""
@@ -50,6 +70,7 @@ def device_info(self):
"name": self.zone_name,
"manufacturer": DEFAULT_NAME,
"model": TADO_ZONE,
+ "suggested_area": self.zone_name,
}
@property
diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json
index 9b166027df3c06..27c7ecff4116d9 100644
--- a/homeassistant/components/tado/manifest.json
+++ b/homeassistant/components/tado/manifest.json
@@ -3,7 +3,7 @@
"name": "Tado",
"documentation": "https://www.home-assistant.io/integrations/tado",
"requirements": ["python-tado==0.10.0"],
- "codeowners": ["@michaelarnauts", "@bdraco"],
+ "codeowners": ["@michaelarnauts", "@bdraco", "@noltari"],
"config_flow": true,
"homekit": {
"models": ["tado", "AC02"]
diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py
index 6613de82bff332..87d2170eb7514d 100644
--- a/homeassistant/components/tado/sensor.py
+++ b/homeassistant/components/tado/sensor.py
@@ -1,6 +1,7 @@
"""Support for Tado sensors for each zone."""
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
@@ -10,9 +11,9 @@
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from .const import (
+ CONDITIONS_MAP,
DATA,
DOMAIN,
SIGNAL_TADO_UPDATE_RECEIVED,
@@ -20,10 +21,16 @@
TYPE_HEATING,
TYPE_HOT_WATER,
)
-from .entity import TadoZoneEntity
+from .entity import TadoHomeEntity, TadoZoneEntity
_LOGGER = logging.getLogger(__name__)
+HOME_SENSORS = {
+ "outdoor temperature",
+ "solar percentage",
+ "weather condition",
+}
+
ZONE_SENSORS = {
TYPE_HEATING: [
"temperature",
@@ -41,6 +48,14 @@
}
+def format_condition(condition: str) -> str:
+ """Return condition from dict CONDITIONS_MAP."""
+ for key, value in CONDITIONS_MAP.items():
+ if condition in value:
+ return key
+ return condition
+
+
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
):
@@ -50,6 +65,9 @@ async def async_setup_entry(
zones = tado.zones
entities = []
+ # Create home sensors
+ entities.extend([TadoHomeSensor(tado, variable) for variable in HOME_SENSORS])
+
# Create zone sensors
for zone in zones:
zone_type = zone["type"]
@@ -68,7 +86,112 @@ async def async_setup_entry(
async_add_entities(entities, True)
-class TadoZoneSensor(TadoZoneEntity, Entity):
+class TadoHomeSensor(TadoHomeEntity, SensorEntity):
+ """Representation of a Tado Sensor."""
+
+ def __init__(self, tado, home_variable):
+ """Initialize of the Tado Sensor."""
+ super().__init__(tado)
+ self._tado = tado
+
+ self.home_variable = home_variable
+
+ self._unique_id = f"{home_variable} {tado.home_id}"
+
+ self._state = None
+ self._state_attributes = None
+ self._tado_weather_data = self._tado.data["weather"]
+
+ async def async_added_to_hass(self):
+ """Register for sensor updates."""
+
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ SIGNAL_TADO_UPDATE_RECEIVED.format(
+ self._tado.home_id, "weather", "data"
+ ),
+ self._async_update_callback,
+ )
+ )
+ self._async_update_home_data()
+
+ @property
+ def unique_id(self):
+ """Return the unique id."""
+ return self._unique_id
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return f"{self._tado.home_name} {self.home_variable}"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def extra_state_attributes(self):
+ """Return the state attributes."""
+ return self._state_attributes
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ if self.home_variable == "temperature":
+ return TEMP_CELSIUS
+ if self.home_variable == "solar percentage":
+ return PERCENTAGE
+ if self.home_variable == "weather condition":
+ return None
+
+ @property
+ def device_class(self):
+ """Return the device class."""
+ if self.home_variable == "outdoor temperature":
+ return DEVICE_CLASS_TEMPERATURE
+ return None
+
+ @callback
+ def _async_update_callback(self):
+ """Update and write state."""
+ self._async_update_home_data()
+ self.async_write_ha_state()
+
+ @callback
+ def _async_update_home_data(self):
+ """Handle update callbacks."""
+ try:
+ self._tado_weather_data = self._tado.data["weather"]
+ except KeyError:
+ return
+
+ if self.home_variable == "outdoor temperature":
+ self._state = self.hass.config.units.temperature(
+ self._tado_weather_data["outsideTemperature"]["celsius"],
+ TEMP_CELSIUS,
+ )
+ self._state_attributes = {
+ "time": self._tado_weather_data["outsideTemperature"]["timestamp"],
+ }
+
+ elif self.home_variable == "solar percentage":
+ self._state = self._tado_weather_data["solarIntensity"]["percentage"]
+ self._state_attributes = {
+ "time": self._tado_weather_data["solarIntensity"]["timestamp"],
+ }
+
+ elif self.home_variable == "weather condition":
+ self._state = format_condition(
+ self._tado_weather_data["weatherState"]["value"]
+ )
+ self._state_attributes = {
+ "time": self._tado_weather_data["weatherState"]["timestamp"]
+ }
+
+
+class TadoZoneSensor(TadoZoneEntity, SensorEntity):
"""Representation of a tado Sensor."""
def __init__(self, tado, zone_name, zone_id, zone_variable):
@@ -114,7 +237,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._state_attributes
diff --git a/homeassistant/components/tado/translations/de.json b/homeassistant/components/tado/translations/de.json
index ffab091f726d7e..9dc410b670e546 100644
--- a/homeassistant/components/tado/translations/de.json
+++ b/homeassistant/components/tado/translations/de.json
@@ -4,7 +4,7 @@
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"no_homes": "Es sind keine Standorte mit diesem Tado-Konto verkn\u00fcpft.",
"unknown": "Unerwarteter Fehler"
@@ -15,7 +15,7 @@
"password": "Passwort",
"username": "Benutzername"
},
- "title": "Stellen Sie eine Verbindung zu Ihrem Tado-Konto her"
+ "title": "Stellen eine Verbindung zu deinem Tado-Konto her"
}
}
},
@@ -23,10 +23,10 @@
"step": {
"init": {
"data": {
- "fallback": "Aktivieren Sie den Fallback-Modus."
+ "fallback": "Aktivieren den Fallback-Modus."
},
"description": "Der Fallback-Modus wechselt beim n\u00e4chsten Zeitplanwechsel nach dem manuellen Anpassen einer Zone zu Smart Schedule.",
- "title": "Passen Sie die Tado-Optionen an."
+ "title": "Passe die Tado-Optionen an."
}
}
}
diff --git a/homeassistant/components/tado/translations/he.json b/homeassistant/components/tado/translations/he.json
new file mode 100644
index 00000000000000..ac90b3264eab33
--- /dev/null
+++ b/homeassistant/components/tado/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tado/translations/hu.json b/homeassistant/components/tado/translations/hu.json
index dee4ed9ee0fa4d..fd8db27da5efd0 100644
--- a/homeassistant/components/tado/translations/hu.json
+++ b/homeassistant/components/tado/translations/hu.json
@@ -1,5 +1,13 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/tado/translations/id.json b/homeassistant/components/tado/translations/id.json
new file mode 100644
index 00000000000000..bdbfa19cb6d6ab
--- /dev/null
+++ b/homeassistant/components/tado/translations/id.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "no_homes": "Tidak ada rumah yang ditautkan ke akun Tado ini.",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "title": "Hubungkan ke akun Tado Anda"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "fallback": "Aktifkan mode alternatif."
+ },
+ "title": "Sesuaikan opsi Tado."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tado/translations/ko.json b/homeassistant/components/tado/translations/ko.json
index 8982b68829baf0..29603d8f6d2bd0 100644
--- a/homeassistant/components/tado/translations/ko.json
+++ b/homeassistant/components/tado/translations/ko.json
@@ -4,7 +4,7 @@
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"no_homes": "\uc774 Tado \uacc4\uc815\uc5d0 \uc5f0\uacb0\ub41c \uc9d1\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
@@ -25,7 +25,7 @@
"data": {
"fallback": "\ub300\uccb4 \ubaa8\ub4dc\ub97c \ud65c\uc131\ud654\ud569\ub2c8\ub2e4."
},
- "description": "\uc601\uc5ed\uc744 \uc218\ub3d9\uc73c\ub85c \uc804\ud658\ud558\uba74 \ub300\uccb4 \ubaa8\ub4dc\ub294 \ub2e4\uc74c \uc77c\uc815\uc744 \uc2a4\ub9c8\ud2b8 \uc77c\uc815\uc73c\ub85c \uc804\ud658\ud569\ub2c8\ub2e4.",
+ "description": "\ub300\uccb4 \ubaa8\ub4dc\ub294 \uc9c0\uc5ed\uc744 \uc218\ub3d9\uc73c\ub85c \uc870\uc815\ud55c \ud6c4 \ub2e4\uc74c \uc77c\uc815 \uc804\ud658\uc2dc \uc2a4\ub9c8\ud2b8 \uc77c\uc815\uc73c\ub85c \uc804\ud658\ub429\ub2c8\ub2e4.",
"title": "Tado \uc635\uc158 \uc870\uc815\ud558\uae30"
}
}
diff --git a/homeassistant/components/tado/translations/nl.json b/homeassistant/components/tado/translations/nl.json
index 3cdadf0f54e1c6..3b6d914b71c49b 100644
--- a/homeassistant/components/tado/translations/nl.json
+++ b/homeassistant/components/tado/translations/nl.json
@@ -4,7 +4,7 @@
"already_configured": "Apparaat is al geconfigureerd"
},
"error": {
- "cannot_connect": "Verbinding mislukt, probeer het opnieuw",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"no_homes": "Er zijn geen huizen gekoppeld aan dit tado-account.",
"unknown": "Onverwachte fout"
diff --git a/homeassistant/components/tado/translations/ru.json b/homeassistant/components/tado/translations/ru.json
index 8ffb14edc0efc2..18e9ddff67b914 100644
--- a/homeassistant/components/tado/translations/ru.json
+++ b/homeassistant/components/tado/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"no_homes": "\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0434\u043e\u043c\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
@@ -13,7 +13,7 @@
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "Tado"
}
diff --git a/homeassistant/components/tado/translations/tr.json b/homeassistant/components/tado/translations/tr.json
new file mode 100644
index 00000000000000..09ffbf8a7d14f7
--- /dev/null
+++ b/homeassistant/components/tado/translations/tr.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "fallback": "Geri d\u00f6n\u00fc\u015f modunu etkinle\u015ftirin."
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tado/translations/uk.json b/homeassistant/components/tado/translations/uk.json
new file mode 100644
index 00000000000000..f1dcf4d575ba83
--- /dev/null
+++ b/homeassistant/components/tado/translations/uk.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "no_homes": "\u0427\u0438 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0431\u0443\u0434\u0438\u043d\u043a\u0456\u0432, \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0438\u0445 \u0437 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u043c \u0437\u0430\u043f\u0438\u0441\u043e\u043c.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "Tado"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "fallback": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c Fallback"
+ },
+ "description": "\u0420\u0435\u0436\u0438\u043c Fallback \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043d\u0430 Smart Schedule \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u043e\u0433\u043e \u0440\u0430\u0437\u0443 \u043f\u0456\u0441\u043b\u044f \u0440\u0443\u0447\u043d\u043e\u0433\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u043e\u043d\u0438.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Tado"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py
index 6c385181aaad2d..6dcf2ec9a4df8c 100644
--- a/homeassistant/components/tag/__init__.py
+++ b/homeassistant/components/tag/__init__.py
@@ -1,6 +1,7 @@
"""The Tag integration."""
+from __future__ import annotations
+
import logging
-import typing
import uuid
import voluptuous as vol
@@ -63,7 +64,7 @@ class TagStorageCollection(collection.StorageCollection):
CREATE_SCHEMA = vol.Schema(CREATE_FIELDS)
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
- async def _process_create_data(self, data: typing.Dict) -> typing.Dict:
+ async def _process_create_data(self, data: dict) -> dict:
"""Validate the config is valid."""
data = self.CREATE_SCHEMA(data)
if not data[TAG_ID]:
@@ -74,11 +75,11 @@ async def _process_create_data(self, data: typing.Dict) -> typing.Dict:
return data
@callback
- def _get_suggested_id(self, info: typing.Dict) -> str:
+ def _get_suggested_id(self, info: dict) -> str:
"""Suggest an ID based on the config."""
return info[TAG_ID]
- async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict:
+ async def _update_data(self, data: dict, update_data: dict) -> dict:
"""Return a new updated data object."""
data = {**data, **self.UPDATE_SCHEMA(update_data)}
# make last_scanned JSON serializeable
diff --git a/homeassistant/components/tag/translations/hu.json b/homeassistant/components/tag/translations/hu.json
new file mode 100644
index 00000000000000..edea7ba32ef265
--- /dev/null
+++ b/homeassistant/components/tag/translations/hu.json
@@ -0,0 +1,3 @@
+{
+ "title": "C\u00edmke"
+}
\ No newline at end of file
diff --git a/homeassistant/components/tag/translations/id.json b/homeassistant/components/tag/translations/id.json
new file mode 100644
index 00000000000000..fdac700612daf4
--- /dev/null
+++ b/homeassistant/components/tag/translations/id.json
@@ -0,0 +1,3 @@
+{
+ "title": "Tag"
+}
\ No newline at end of file
diff --git a/homeassistant/components/tag/translations/nl.json b/homeassistant/components/tag/translations/nl.json
new file mode 100644
index 00000000000000..fdac700612daf4
--- /dev/null
+++ b/homeassistant/components/tag/translations/nl.json
@@ -0,0 +1,3 @@
+{
+ "title": "Tag"
+}
\ No newline at end of file
diff --git a/homeassistant/components/tag/translations/uk.json b/homeassistant/components/tag/translations/uk.json
new file mode 100644
index 00000000000000..fdac700612daf4
--- /dev/null
+++ b/homeassistant/components/tag/translations/uk.json
@@ -0,0 +1,3 @@
+{
+ "title": "Tag"
+}
\ No newline at end of file
diff --git a/homeassistant/components/tag/trigger.py b/homeassistant/components/tag/trigger.py
index 9803bd56afec2b..4f6dd89a2520cf 100644
--- a/homeassistant/components/tag/trigger.py
+++ b/homeassistant/components/tag/trigger.py
@@ -18,6 +18,7 @@
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for tag_scanned events based on configuration."""
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
tag_ids = set(config[TAG_ID])
device_ids = set(config[DEVICE_ID]) if DEVICE_ID in config else None
@@ -37,6 +38,7 @@ async def handle_event(event):
"platform": DOMAIN,
"event": event,
"description": "Tag scanned",
+ "id": trigger_id,
}
},
event.context,
diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py
index d75ccaec414318..8db7b23a8ce25e 100644
--- a/homeassistant/components/tahoma/__init__.py
+++ b/homeassistant/components/tahoma/__init__.py
@@ -31,7 +31,7 @@
extra=vol.ALLOW_EXTRA,
)
-TAHOMA_COMPONENTS = ["binary_sensor", "cover", "lock", "scene", "sensor", "switch"]
+PLATFORMS = ["binary_sensor", "cover", "lock", "scene", "sensor", "switch"]
TAHOMA_TYPES = {
"io:AwningValanceIOComponent": "cover",
@@ -73,7 +73,7 @@
def setup(hass, config):
- """Activate Tahoma component."""
+ """Set up Tahoma integration."""
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
@@ -111,14 +111,14 @@ def setup(hass, config):
for scene in scenes:
hass.data[DOMAIN]["scenes"].append(scene)
- for component in TAHOMA_COMPONENTS:
- discovery.load_platform(hass, component, DOMAIN, {}, config)
+ for platform in PLATFORMS:
+ discovery.load_platform(hass, platform, DOMAIN, {}, config)
return True
def map_tahoma_device(tahoma_device):
- """Map Tahoma device types to Home Assistant components."""
+ """Map Tahoma device types to Home Assistant platforms."""
return TAHOMA_TYPES.get(tahoma_device.type)
@@ -137,7 +137,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
return {"tahoma_device_id": self.tahoma_device.url}
diff --git a/homeassistant/components/tahoma/binary_sensor.py b/homeassistant/components/tahoma/binary_sensor.py
index af06bf5ca4c77f..c094601346962a 100644
--- a/homeassistant/components/tahoma/binary_sensor.py
+++ b/homeassistant/components/tahoma/binary_sensor.py
@@ -57,10 +57,10 @@ def icon(self):
return self._icon
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
attr = {}
- super_attr = super().device_state_attributes
+ super_attr = super().extra_state_attributes
if super_attr is not None:
attr.update(super_attr)
diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py
index 2eec9160811de7..a02f21fb5e1409 100644
--- a/homeassistant/components/tahoma/cover.py
+++ b/homeassistant/components/tahoma/cover.py
@@ -194,10 +194,10 @@ def device_class(self):
return TAHOMA_DEVICE_CLASSES.get(self.tahoma_device.type)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
attr = {}
- super_attr = super().device_state_attributes
+ super_attr = super().extra_state_attributes
if super_attr is not None:
attr.update(super_attr)
diff --git a/homeassistant/components/tahoma/lock.py b/homeassistant/components/tahoma/lock.py
index 93d82bffc99501..3d160cd95b3909 100644
--- a/homeassistant/components/tahoma/lock.py
+++ b/homeassistant/components/tahoma/lock.py
@@ -78,12 +78,12 @@ def is_locked(self):
return self._lock_status == STATE_LOCKED
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the lock state attributes."""
attr = {
ATTR_BATTERY_LEVEL: self._battery_level,
}
- super_attr = super().device_state_attributes
+ super_attr = super().extra_state_attributes
if super_attr is not None:
attr.update(super_attr)
return attr
diff --git a/homeassistant/components/tahoma/scene.py b/homeassistant/components/tahoma/scene.py
index 1d53b65d5d5695..3cfa26a5f8912f 100644
--- a/homeassistant/components/tahoma/scene.py
+++ b/homeassistant/components/tahoma/scene.py
@@ -37,6 +37,6 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the scene."""
return {"tahoma_scene_oid": self.tahoma_scene.oid}
diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py
index fb1129cfa0edc1..47e6d3004143ec 100644
--- a/homeassistant/components/tahoma/sensor.py
+++ b/homeassistant/components/tahoma/sensor.py
@@ -2,8 +2,8 @@
from datetime import timedelta
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ATTR_BATTERY_LEVEL, LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS
-from homeassistant.helpers.entity import Entity
from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice
@@ -25,7 +25,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(devices, True)
-class TahomaSensor(TahomaDevice, Entity):
+class TahomaSensor(TahomaDevice, SensorEntity):
"""Representation of a Tahoma Sensor."""
def __init__(self, tahoma_device, controller):
@@ -110,10 +110,10 @@ def update(self):
_LOGGER.debug("Update %s, value: %d", self._name, self.current_value)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
attr = {}
- super_attr = super().device_state_attributes
+ super_attr = super().extra_state_attributes
if super_attr is not None:
attr.update(super_attr)
diff --git a/homeassistant/components/tahoma/switch.py b/homeassistant/components/tahoma/switch.py
index 808f80d8cfaf53..2ea68b93e6b799 100644
--- a/homeassistant/components/tahoma/switch.py
+++ b/homeassistant/components/tahoma/switch.py
@@ -105,10 +105,10 @@ def is_on(self):
return bool(self._state == STATE_ON)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
attr = {}
- super_attr = super().device_state_attributes
+ super_attr = super().extra_state_attributes
if super_attr is not None:
attr.update(super_attr)
diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py
index ab1cc8b23da42d..379819cf65ebb1 100644
--- a/homeassistant/components/tank_utility/sensor.py
+++ b/homeassistant/components/tank_utility/sensor.py
@@ -7,10 +7,9 @@
from tank_utility import auth, device as tank_monitor
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_DEVICES, CONF_EMAIL, CONF_PASSWORD, PERCENTAGE
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -62,7 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(all_sensors, True)
-class TankUtilitySensor(Entity):
+class TankUtilitySensor(SensorEntity):
"""Representation of a Tank Utility sensor."""
def __init__(self, email, password, token, device):
@@ -97,7 +96,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the attributes of the device."""
return self._attributes
diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py
index 6985072b065e30..5c1898e02a98a4 100644
--- a/homeassistant/components/tankerkoenig/sensor.py
+++ b/homeassistant/components/tankerkoenig/sensor.py
@@ -2,6 +2,7 @@
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_LATITUDE,
@@ -79,7 +80,7 @@ async def async_update_data():
async_add_entities(entities)
-class FuelPriceSensor(CoordinatorEntity):
+class FuelPriceSensor(CoordinatorEntity, SensorEntity):
"""Contains prices for fuel in a given station."""
def __init__(self, fuel_type, station, coordinator, name, show_on_map):
@@ -125,7 +126,7 @@ def unique_id(self) -> str:
return f"{self._station_id}_{self._fuel_type}"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the attributes of the device."""
data = self.coordinator.data[self._station_id]
diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py
index c0ebae7695eb84..83baae9c19c563 100644
--- a/homeassistant/components/tasmota/__init__.py
+++ b/homeassistant/components/tasmota/__init__.py
@@ -24,7 +24,6 @@
EVENT_DEVICE_REGISTRY_UPDATED,
async_entries_for_config_entry,
)
-from homeassistant.helpers.typing import HomeAssistantType
from . import device_automation, discovery
from .const import (
@@ -38,11 +37,6 @@
_LOGGER = logging.getLogger(__name__)
-async def async_setup(hass: HomeAssistantType, config: dict):
- """Set up the Tasmota component."""
- return True
-
-
async def async_setup_entry(hass, entry):
"""Set up Tasmota from a config entry."""
websocket_api.async_register_command(hass, websocket_remove_device)
@@ -92,8 +86,8 @@ async def start_platforms():
await device_automation.async_setup_entry(hass, entry)
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_setup(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ for platform in PLATFORMS
]
)
@@ -113,8 +107,8 @@ async def async_unload_entry(hass, entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -128,8 +122,8 @@ async def async_unload_entry(hass, entry):
for unsub in hass.data[DATA_UNSUB]:
unsub()
hass.data.pop(DATA_REMOVE_DISCOVER_COMPONENT.format("device_automation"))()
- for component in PLATFORMS:
- hass.data.pop(DATA_REMOVE_DISCOVER_COMPONENT.format(component))()
+ for platform in PLATFORMS:
+ hass.data.pop(DATA_REMOVE_DISCOVER_COMPONENT.format(platform))()
# deattach device triggers
device_registry = await hass.helpers.device_registry.async_get_registry()
diff --git a/homeassistant/components/tasmota/config_flow.py b/homeassistant/components/tasmota/config_flow.py
index 5d39fa024382f0..320b4ff2448736 100644
--- a/homeassistant/components/tasmota/config_flow.py
+++ b/homeassistant/components/tasmota/config_flow.py
@@ -4,11 +4,7 @@
from homeassistant import config_entries
from homeassistant.components.mqtt import valid_subscribe_topic
-from .const import ( # pylint:disable=unused-import
- CONF_DISCOVERY_PREFIX,
- DEFAULT_PREFIX,
- DOMAIN,
-)
+from .const import CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX, DOMAIN
class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py
index 463b1c65a98a1c..ae4a528efc6667 100644
--- a/homeassistant/components/tasmota/device_trigger.py
+++ b/homeassistant/components/tasmota/device_trigger.py
@@ -1,6 +1,8 @@
"""Provides device automations for Tasmota."""
+from __future__ import annotations
+
import logging
-from typing import Callable, List, Optional
+from typing import Callable
import attr
from hatasmota.trigger import TasmotaTrigger
@@ -46,8 +48,8 @@ class TriggerInstance:
action: AutomationActionType = attr.ib()
automation_info: dict = attr.ib()
- trigger: "Trigger" = attr.ib()
- remove: Optional[CALLBACK_TYPE] = attr.ib(default=None)
+ trigger: Trigger = attr.ib()
+ remove: CALLBACK_TYPE | None = attr.ib(default=None)
async def async_attach_trigger(self):
"""Attach event trigger."""
@@ -85,7 +87,7 @@ class Trigger:
subtype: str = attr.ib()
tasmota_trigger: TasmotaTrigger = attr.ib()
type: str = attr.ib()
- trigger_instances: List[TriggerInstance] = attr.ib(factory=list)
+ trigger_instances: list[TriggerInstance] = attr.ib(factory=list)
async def add_trigger(self, action, automation_info):
"""Add Tasmota trigger."""
@@ -238,7 +240,7 @@ async def async_remove_triggers(hass: HomeAssistant, device_id: str):
device_trigger.remove_update_signal()
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers for a Tasmota device."""
triggers = []
@@ -271,7 +273,6 @@ async def async_attach_trigger(
"""Attach a device trigger."""
if DEVICE_TRIGGERS not in hass.data:
hass.data[DEVICE_TRIGGERS] = {}
- config = TRIGGER_SCHEMA(config)
device_id = config[CONF_DEVICE_ID]
discovery_id = config[CONF_DISCOVERY_ID]
diff --git a/homeassistant/components/tasmota/fan.py b/homeassistant/components/tasmota/fan.py
index 1d7aa9f38cb813..876d1a4cf60410 100644
--- a/homeassistant/components/tasmota/fan.py
+++ b/homeassistant/components/tasmota/fan.py
@@ -6,19 +6,20 @@
from homeassistant.components.fan import FanEntity
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.util.percentage import (
+ ordered_list_item_to_percentage,
+ percentage_to_ordered_list_item,
+)
from .const import DATA_REMOVE_DISCOVER_COMPONENT
from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW
from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate
-HA_TO_TASMOTA_SPEED_MAP = {
- fan.SPEED_OFF: tasmota_const.FAN_SPEED_OFF,
- fan.SPEED_LOW: tasmota_const.FAN_SPEED_LOW,
- fan.SPEED_MEDIUM: tasmota_const.FAN_SPEED_MEDIUM,
- fan.SPEED_HIGH: tasmota_const.FAN_SPEED_HIGH,
-}
-
-TASMOTA_TO_HA_SPEED_MAP = {v: k for k, v in HA_TO_TASMOTA_SPEED_MAP.items()}
+ORDERED_NAMED_FAN_SPEEDS = [
+ tasmota_const.FAN_SPEED_LOW,
+ tasmota_const.FAN_SPEED_MEDIUM,
+ tasmota_const.FAN_SPEED_HIGH,
+] # off is not included
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -56,42 +57,45 @@ def __init__(self, **kwds):
)
@property
- def speed(self):
- """Return the current speed."""
- return TASMOTA_TO_HA_SPEED_MAP.get(self._state)
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return len(ORDERED_NAMED_FAN_SPEEDS)
@property
- def speed_list(self):
- """Get the list of available speeds."""
- return list(HA_TO_TASMOTA_SPEED_MAP)
+ def percentage(self):
+ """Return the current speed percentage."""
+ if self._state is None:
+ return None
+ if self._state == 0:
+ return 0
+ return ordered_list_item_to_percentage(ORDERED_NAMED_FAN_SPEEDS, self._state)
@property
def supported_features(self):
"""Flag supported features."""
return fan.SUPPORT_SET_SPEED
- async def async_set_speed(self, speed):
+ async def async_set_percentage(self, percentage):
"""Set the speed of the fan."""
- if speed not in HA_TO_TASMOTA_SPEED_MAP:
- raise ValueError(f"Unsupported speed {speed}")
- if speed == fan.SPEED_OFF:
+ if percentage == 0:
await self.async_turn_off()
else:
- self._tasmota_entity.set_speed(HA_TO_TASMOTA_SPEED_MAP[speed])
-
- #
- # The fan entity model has changed to use percentages and preset_modes
- # instead of speeds.
- #
- # Please review
- # https://developers.home-assistant.io/docs/core/entity/fan/
- #
+ tasmota_speed = percentage_to_ordered_list_item(
+ ORDERED_NAMED_FAN_SPEEDS, percentage
+ )
+ self._tasmota_entity.set_speed(tasmota_speed)
+
async def async_turn_on(
self, speed=None, percentage=None, preset_mode=None, **kwargs
):
"""Turn the fan on."""
# Tasmota does not support turning a fan on with implicit speed
- await self.async_set_speed(speed or fan.SPEED_MEDIUM)
+ await self.async_set_percentage(
+ percentage
+ or ordered_list_item_to_percentage(
+ ORDERED_NAMED_FAN_SPEEDS, tasmota_const.FAN_SPEED_MEDIUM
+ )
+ )
async def async_turn_off(self, **kwargs):
"""Turn the fan off."""
diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json
index bd48cae8e5944a..17e72a57ce65d7 100644
--- a/homeassistant/components/tasmota/manifest.json
+++ b/homeassistant/components/tasmota/manifest.json
@@ -3,7 +3,7 @@
"name": "Tasmota",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tasmota",
- "requirements": ["hatasmota==0.2.7"],
+ "requirements": ["hatasmota==0.2.9"],
"dependencies": ["mqtt"],
"mqtt": ["tasmota/discovery/#"],
"codeowners": ["@emontnemery"]
diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py
index 17a6e2a35c2fcb..432fc2266f3adc 100644
--- a/homeassistant/components/tasmota/sensor.py
+++ b/homeassistant/components/tasmota/sensor.py
@@ -1,14 +1,16 @@
"""Support for Tasmota sensors."""
-from typing import Optional
+from __future__ import annotations
from hatasmota import const as hc, status_sensor
from homeassistant.components import sensor
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_CO2,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_POWER,
@@ -37,7 +39,6 @@
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from .const import DATA_REMOVE_DISCOVER_COMPONENT
from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW
@@ -52,7 +53,7 @@
hc.SENSOR_APPARENT_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER},
hc.SENSOR_BATTERY: {DEVICE_CLASS: DEVICE_CLASS_BATTERY},
hc.SENSOR_CCT: {ICON: "mdi:temperature-kelvin"},
- hc.SENSOR_CO2: {ICON: "mdi:molecule-co2"},
+ hc.SENSOR_CO2: {DEVICE_CLASS: DEVICE_CLASS_CO2},
hc.SENSOR_COLOR_BLUE: {ICON: "mdi:palette"},
hc.SENSOR_COLOR_GREEN: {ICON: "mdi:palette"},
hc.SENSOR_COLOR_RED: {ICON: "mdi:palette"},
@@ -144,7 +145,7 @@ async def async_discover_sensor(tasmota_entity, discovery_hash):
)
-class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, Entity):
+class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity):
"""Representation of a Tasmota sensor."""
def __init__(self, **kwds):
@@ -162,7 +163,7 @@ def state_updated(self, state, **kwargs):
self.async_write_ha_state()
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the device class of the sensor."""
class_or_icon = SENSOR_DEVICE_CLASS_ICON_MAP.get(
self._tasmota_entity.quantity, {}
@@ -192,6 +193,11 @@ def state(self):
return self._state.isoformat()
return self._state
+ @property
+ def force_update(self):
+ """Force update."""
+ return True
+
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
diff --git a/homeassistant/components/tasmota/translations/de.json b/homeassistant/components/tasmota/translations/de.json
new file mode 100644
index 00000000000000..308747088395bd
--- /dev/null
+++ b/homeassistant/components/tasmota/translations/de.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
+ "step": {
+ "config": {
+ "description": "Bitte die Tasmota-Konfiguration einstellen.",
+ "title": "Tasmota"
+ },
+ "confirm": {
+ "description": "M\u00f6chtest du Tasmota einrichten?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tasmota/translations/hu.json b/homeassistant/components/tasmota/translations/hu.json
index c76efd0e898325..4461f2a2b71a44 100644
--- a/homeassistant/components/tasmota/translations/hu.json
+++ b/homeassistant/components/tasmota/translations/hu.json
@@ -1,8 +1,15 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
"step": {
"config": {
+ "description": "Add meg a Tasmota konfigur\u00e1ci\u00f3t.",
"title": "Tasmota"
+ },
+ "confirm": {
+ "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Tasmota-t?"
}
}
}
diff --git a/homeassistant/components/tasmota/translations/id.json b/homeassistant/components/tasmota/translations/id.json
new file mode 100644
index 00000000000000..a11acf50390f10
--- /dev/null
+++ b/homeassistant/components/tasmota/translations/id.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "invalid_discovery_topic": "Prefiks topik penemuan tidak valid."
+ },
+ "step": {
+ "config": {
+ "data": {
+ "discovery_prefix": "Prefiks topik penemuan"
+ },
+ "description": "Masukkan konfigurasi Tasmota.",
+ "title": "Tasmota"
+ },
+ "confirm": {
+ "description": "Ingin menyiapkan Tasmota?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tasmota/translations/ko.json b/homeassistant/components/tasmota/translations/ko.json
new file mode 100644
index 00000000000000..45cac13f622034
--- /dev/null
+++ b/homeassistant/components/tasmota/translations/ko.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "invalid_discovery_topic": "\uac80\uc0c9 \ud1a0\ud53d \uc811\ub450\uc0ac\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "config": {
+ "data": {
+ "discovery_prefix": "\uac80\uc0c9 \ud1a0\ud53d \uc811\ub450\uc0ac"
+ },
+ "description": "Tasmota \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "Tasmota"
+ },
+ "confirm": {
+ "description": "Tasmota\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tasmota/translations/nl.json b/homeassistant/components/tasmota/translations/nl.json
index 3b0cc5c1ca8e21..c099d376920550 100644
--- a/homeassistant/components/tasmota/translations/nl.json
+++ b/homeassistant/components/tasmota/translations/nl.json
@@ -3,8 +3,14 @@
"abort": {
"single_instance_allowed": "Is al geconfigureerd. Er is maar een configuratie mogelijk"
},
+ "error": {
+ "invalid_discovery_topic": "Ongeldig onderwerpvoorvoegsel voor ontdekken"
+ },
"step": {
"config": {
+ "data": {
+ "discovery_prefix": "Discovery-onderwerpvoorvoegsel"
+ },
"description": "Vul de Tasmota gegevens in",
"title": "Tasmota"
},
diff --git a/homeassistant/components/tasmota/translations/tr.json b/homeassistant/components/tasmota/translations/tr.json
new file mode 100644
index 00000000000000..a559d0911ee0f5
--- /dev/null
+++ b/homeassistant/components/tasmota/translations/tr.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "step": {
+ "config": {
+ "description": "L\u00fctfen Tasmota yap\u0131land\u0131rmas\u0131n\u0131 girin.",
+ "title": "Tasmota"
+ },
+ "confirm": {
+ "description": "Tasmota'y\u0131 kurmak istiyor musunuz?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tasmota/translations/uk.json b/homeassistant/components/tasmota/translations/uk.json
new file mode 100644
index 00000000000000..6639a9c9626eb8
--- /dev/null
+++ b/homeassistant/components/tasmota/translations/uk.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "invalid_discovery_topic": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043f\u0440\u0435\u0444\u0456\u043a\u0441 \u0442\u0435\u043c\u0438 \u0430\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f."
+ },
+ "step": {
+ "config": {
+ "data": {
+ "discovery_prefix": "\u041f\u0440\u0435\u0444\u0456\u043a\u0441 \u0442\u0435\u043c\u0438 \u0430\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Tasmota.",
+ "title": "Tasmota"
+ },
+ "confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Tasmota?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py
index ed96bb62ace041..c50efb00ed7960 100644
--- a/homeassistant/components/tautulli/sensor.py
+++ b/homeassistant/components/tautulli/sensor.py
@@ -4,7 +4,7 @@
from pytautulli import Tautulli
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_API_KEY,
CONF_HOST,
@@ -18,7 +18,6 @@
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
CONF_MONITORED_USERS = "monitored_users"
@@ -72,7 +71,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(sensor, True)
-class TautulliSensor(Entity):
+class TautulliSensor(SensorEntity):
"""Representation of a Tautulli sensor."""
def __init__(self, tautulli, name, monitored_conditions, users):
@@ -130,7 +129,7 @@ def unit_of_measurement(self):
return "Watching"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return attributes for the sensor."""
return self._attributes
diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py
index 9b7e1539fb4114..54cf4d120f197d 100644
--- a/homeassistant/components/tcp/sensor.py
+++ b/homeassistant/components/tcp/sensor.py
@@ -5,7 +5,7 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_HOST,
CONF_NAME,
@@ -17,7 +17,6 @@
)
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -48,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([TcpSensor(hass, config)])
-class TcpSensor(Entity):
+class TcpSensor(SensorEntity):
"""Implementation of a TCP socket based sensor."""
required = ()
diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py
index 3ea26286b18c03..d618ca9c2cf6bd 100644
--- a/homeassistant/components/ted5000/sensor.py
+++ b/homeassistant/components/ted5000/sensor.py
@@ -1,4 +1,5 @@
"""Support gathering ted5000 information."""
+from contextlib import suppress
from datetime import timedelta
import logging
@@ -6,10 +7,9 @@
import voluptuous as vol
import xmltodict
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, POWER_WATT, VOLT
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -49,7 +49,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return True
-class Ted5000Sensor(Entity):
+class Ted5000Sensor(SensorEntity):
"""Implementation of a Ted5000 sensor."""
def __init__(self, gateway, name, mtu, unit):
@@ -74,10 +74,8 @@ def unit_of_measurement(self):
@property
def state(self):
"""Return the state of the resources."""
- try:
+ with suppress(KeyError):
return self._gateway.data[self._mtu][self._unit]
- except KeyError:
- pass
def update(self):
"""Get the latest data from REST API."""
diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py
index b6ca78816156a5..86bf4c24407765 100644
--- a/homeassistant/components/telegram_bot/__init__.py
+++ b/homeassistant/components/telegram_bot/__init__.py
@@ -330,7 +330,7 @@ def _render_template_attr(data, attribute):
attribute_templ = data.get(attribute)
if attribute_templ:
if any(
- [isinstance(attribute_templ, vtype) for vtype in [float, int, str]]
+ isinstance(attribute_templ, vtype) for vtype in [float, int, str]
):
data[attribute] = attribute_templ
else:
diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py
index f772e2411e50c4..7fd6cb24efdc04 100644
--- a/homeassistant/components/telegram_bot/webhooks.py
+++ b/homeassistant/components/telegram_bot/webhooks.py
@@ -42,7 +42,7 @@ async def async_setup_platform(hass, config):
if (last_error_date is not None) and (isinstance(last_error_date, int)):
last_error_date = dt.datetime.fromtimestamp(last_error_date)
_LOGGER.info(
- "telegram webhook last_error_date: %s. Status: %s",
+ "Telegram webhook last_error_date: %s. Status: %s",
last_error_date,
current_status,
)
diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py
index ae98a5d85047c0..70cc884881457d 100644
--- a/homeassistant/components/tellduslive/__init__.py
+++ b/homeassistant/components/tellduslive/__init__.py
@@ -12,7 +12,6 @@
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_call_later
-from . import config_flow # noqa: F401
from .const import (
CONF_HOST,
DOMAIN,
@@ -124,8 +123,8 @@ async def async_unload_entry(hass, config_entry):
interval_tracker()
await asyncio.wait(
[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in hass.data.pop(CONFIG_ENTRY_IS_SETUP)
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in hass.data.pop(CONFIG_ENTRY_IS_SETUP)
]
)
del hass.data[DOMAIN]
diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py
index aabbf88ee1c5b9..33a02cd1f16ff0 100644
--- a/homeassistant/components/tellduslive/config_flow.py
+++ b/homeassistant/components/tellduslive/config_flow.py
@@ -29,7 +29,7 @@
_LOGGER = logging.getLogger(__name__)
-@config_entries.HANDLERS.register("tellduslive")
+@config_entries.HANDLERS.register(DOMAIN)
class FlowHandler(config_entries.ConfigFlow):
"""Handle a config flow."""
diff --git a/homeassistant/components/tellduslive/entry.py b/homeassistant/components/tellduslive/entry.py
index 851823385dc494..4453622b21e126 100644
--- a/homeassistant/components/tellduslive/entry.py
+++ b/homeassistant/components/tellduslive/entry.py
@@ -81,7 +81,7 @@ def available(self):
return self._client.is_available(self.device_id)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attrs = {}
if self._battery_level:
diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py
index 1b06fd6ed97bb8..a86b487afd2d81 100644
--- a/homeassistant/components/tellduslive/sensor.py
+++ b/homeassistant/components/tellduslive/sensor.py
@@ -1,5 +1,6 @@
"""Support for Tellstick Net/Telstick Live sensors."""
from homeassistant.components import sensor, tellduslive
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
@@ -71,7 +72,7 @@ async def async_discover_sensor(device_id):
)
-class TelldusLiveSensor(TelldusLiveEntity):
+class TelldusLiveSensor(TelldusLiveEntity, SensorEntity):
"""Representation of a Telldus Live sensor."""
@property
diff --git a/homeassistant/components/tellduslive/translations/de.json b/homeassistant/components/tellduslive/translations/de.json
index a1f6f595a046d0..098ad9c17be221 100644
--- a/homeassistant/components/tellduslive/translations/de.json
+++ b/homeassistant/components/tellduslive/translations/de.json
@@ -4,9 +4,12 @@
"already_configured": "Dienst ist bereits konfiguriert",
"authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL",
"authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.",
- "unknown": "Unbekannter Fehler ist aufgetreten",
+ "unknown": "Unerwarteter Fehler",
"unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten"
},
+ "error": {
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
+ },
"step": {
"auth": {
"description": "So verkn\u00fcpfest du dein TelldusLive-Konto: \n 1. Klicke auf den Link unten \n 2. Melde dich bei Telldus Live an \n 3. Autorisiere ** {app_name} ** (klicke auf ** Yes **). \n 4. Komme hierher zur\u00fcck und klicke auf ** SUBMIT **. \n\n [Link TelldusLive-Konto]({auth_url})",
diff --git a/homeassistant/components/tellduslive/translations/en.json b/homeassistant/components/tellduslive/translations/en.json
index 7b14df15fa8fe5..b1b9cd9ab10086 100644
--- a/homeassistant/components/tellduslive/translations/en.json
+++ b/homeassistant/components/tellduslive/translations/en.json
@@ -5,7 +5,7 @@
"authorize_url_fail": "Unknown error generating an authorize url.",
"authorize_url_timeout": "Timeout generating authorize URL.",
"unknown": "Unexpected error",
- "unknown_authorize_url_generation": "Unknown error generating an authorize url."
+ "unknown_authorize_url_generation": "Unknown error generating an authorize URL."
},
"error": {
"invalid_auth": "Invalid authentication"
diff --git a/homeassistant/components/tellduslive/translations/fr.json b/homeassistant/components/tellduslive/translations/fr.json
index cde9d9c2c68b31..ef4d7bc44dda47 100644
--- a/homeassistant/components/tellduslive/translations/fr.json
+++ b/homeassistant/components/tellduslive/translations/fr.json
@@ -4,7 +4,8 @@
"already_configured": "TelldusLive est d\u00e9j\u00e0 configur\u00e9",
"authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.",
"authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.",
- "unknown": "Une erreur inconnue s'est produite"
+ "unknown": "Une erreur inconnue s'est produite",
+ "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation."
},
"error": {
"invalid_auth": "Authentification invalide"
diff --git a/homeassistant/components/tellduslive/translations/hu.json b/homeassistant/components/tellduslive/translations/hu.json
index 748189e6427009..2e59375369dbd3 100644
--- a/homeassistant/components/tellduslive/translations/hu.json
+++ b/homeassistant/components/tellduslive/translations/hu.json
@@ -1,16 +1,20 @@
{
"config": {
"abort": {
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van",
"authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.",
- "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.",
- "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt"
+ "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt",
+ "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n."
+ },
+ "error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
},
"step": {
"user": {
"data": {
"host": "Hoszt"
},
- "description": "\u00dcres",
"title": "V\u00e1lassz v\u00e9gpontot."
}
}
diff --git a/homeassistant/components/tellduslive/translations/id.json b/homeassistant/components/tellduslive/translations/id.json
new file mode 100644
index 00000000000000..1a405e794fefb4
--- /dev/null
+++ b/homeassistant/components/tellduslive/translations/id.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi",
+ "authorize_url_fail": "Kesalahan tidak dikenal terjadi ketika menghasilkan URL otorisasi.",
+ "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.",
+ "unknown": "Kesalahan yang tidak diharapkan",
+ "unknown_authorize_url_generation": "Kesalahan tidak dikenal ketika menghasilkan URL otorisasi."
+ },
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "step": {
+ "auth": {
+ "description": "Untuk menautkan akun TelldusLive Anda:\n 1. Klik tautan di bawah ini\n 2. Masuk ke Telldus Live\n 3. Otorisasi **{app_name}** (klik **Yes**).\n 4. Kembali ke sini dan klik **KIRIM**.\n\n[Akun Link TelldusLive] ({auth_url})",
+ "title": "Autentikasi ke TelldusLive"
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "title": "Pilih titik akhir."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tellduslive/translations/ko.json b/homeassistant/components/tellduslive/translations/ko.json
index 645b7233bd75a1..6107245c088db2 100644
--- a/homeassistant/components/tellduslive/translations/ko.json
+++ b/homeassistant/components/tellduslive/translations/ko.json
@@ -1,14 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "TelldusLive \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
- "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "authorize_url_fail": "\uc778\uc99d URL\uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
+ "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4",
+ "unknown_authorize_url_generation": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
"auth": {
- "description": "TelldusLive \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74:\n 1. \ud558\ub2e8\uc758 \ub9c1\ud06c\ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694\n 2. Telldus Live \uc5d0 \ub85c\uadf8\uc778 \ud558\uc138\uc694\n 3. Authorize **{app_name}** (**Yes** \ub97c \ud074\ub9ad\ud558\uc138\uc694).\n 4. \ub2e4\uc2dc \uc5ec\uae30\ub85c \ub3cc\uc544\uc640\uc11c **\ud655\uc778**\uc744 \ud074\ub9ad\ud558\uc138\uc694.\n\n [TelldusLive \uacc4\uc815 \uc5f0\uacb0\ud558\uae30]({auth_url})",
+ "description": "TelldusLive \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74:\n 1. \ud558\ub2e8\uc758 \ub9c1\ud06c\ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694\n 2. Telldus Live\uc5d0 \ub85c\uadf8\uc778\ud574\uc8fc\uc138\uc694\n 3. **{app_name}**\uc744(\ub97c) \uc778\uc99d\ud574\uc8fc\uc138\uc694 (**Yes**\ub97c \ud074\ub9ad\ud558\uc138\uc694).\n 4. \ub2e4\uc2dc \uc5ec\uae30\ub85c \ub3cc\uc544\uc640\uc11c **\ud655\uc778**\uc744 \ud074\ub9ad\ud558\uc138\uc694.\n\n [TelldusLive \uacc4\uc815 \uc5f0\uacb0\ud558\uae30]({auth_url})",
"title": "TelldusLive \uc778\uc99d\ud558\uae30"
},
"user": {
diff --git a/homeassistant/components/tellduslive/translations/lb.json b/homeassistant/components/tellduslive/translations/lb.json
index 5e733c2294d6c3..2b809050677ce8 100644
--- a/homeassistant/components/tellduslive/translations/lb.json
+++ b/homeassistant/components/tellduslive/translations/lb.json
@@ -4,7 +4,8 @@
"already_configured": "Service ass scho konfigur\u00e9iert",
"authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.",
"authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.",
- "unknown": "Onerwaarte Feeler"
+ "unknown": "Onerwaarte Feeler",
+ "unknown_authorize_url_generation": "Onbekannte Feeler beim erstellen vun der Authorisatiouns URL."
},
"error": {
"invalid_auth": "Ong\u00eblteg Authentifikatioun"
diff --git a/homeassistant/components/tellduslive/translations/nl.json b/homeassistant/components/tellduslive/translations/nl.json
index b3874dac77ec40..c34911553ab6c4 100644
--- a/homeassistant/components/tellduslive/translations/nl.json
+++ b/homeassistant/components/tellduslive/translations/nl.json
@@ -4,7 +4,11 @@
"already_configured": "Service is al geconfigureerd",
"authorize_url_fail": "Onbekende fout bij het genereren van een autorisatie url.",
"authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
- "unknown": "Onbekende fout opgetreden"
+ "unknown": "Onverwachte fout",
+ "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL."
+ },
+ "error": {
+ "invalid_auth": "Ongeldige authenticatie"
},
"step": {
"auth": {
diff --git a/homeassistant/components/tellduslive/translations/no.json b/homeassistant/components/tellduslive/translations/no.json
index 649de0f86e4d04..563359d266a945 100644
--- a/homeassistant/components/tellduslive/translations/no.json
+++ b/homeassistant/components/tellduslive/translations/no.json
@@ -2,10 +2,10 @@
"config": {
"abort": {
"already_configured": "Tjenesten er allerede konfigurert",
- "authorize_url_fail": "Ukjent feil ved generering av godkjenningsadresse",
+ "authorize_url_fail": "Ukjent feil under generering av en autoriserings-URL.",
"authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse",
"unknown": "Uventet feil",
- "unknown_authorize_url_generation": "Ukjent feil ved generering av godkjenningsadresse"
+ "unknown_authorize_url_generation": "Ukjent feil under generering av en autoriserings-URL."
},
"error": {
"invalid_auth": "Ugyldig godkjenning"
diff --git a/homeassistant/components/tellduslive/translations/ru.json b/homeassistant/components/tellduslive/translations/ru.json
index 0fc0c2f449f0e4..95a16fa205f215 100644
--- a/homeassistant/components/tellduslive/translations/ru.json
+++ b/homeassistant/components/tellduslive/translations/ru.json
@@ -8,7 +8,7 @@
"unknown_authorize_url_generation": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438."
},
"error": {
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"step": {
"auth": {
diff --git a/homeassistant/components/tellduslive/translations/tr.json b/homeassistant/components/tellduslive/translations/tr.json
new file mode 100644
index 00000000000000..300fad68391594
--- /dev/null
+++ b/homeassistant/components/tellduslive/translations/tr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "unknown": "Beklenmeyen hata",
+ "unknown_authorize_url_generation": "Yetkilendirme url'si olu\u015fturulurken bilinmeyen hata."
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tellduslive/translations/uk.json b/homeassistant/components/tellduslive/translations/uk.json
new file mode 100644
index 00000000000000..ff7b3337bb941b
--- /dev/null
+++ b/homeassistant/components/tellduslive/translations/uk.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.",
+ "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430",
+ "unknown_authorize_url_generation": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457."
+ },
+ "error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "step": {
+ "auth": {
+ "description": "\u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0449\u043e\u0431 \u043f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u0430\u043a\u0430\u0443\u043d\u0442 Telldus Live:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043f\u043e \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044e, \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e\u043c\u0443 \u043d\u0438\u0436\u0447\u0435\n2. \u0423\u0432\u0456\u0439\u0434\u0456\u0442\u044c \u0432 Telldus Live\n3. Authorize ** {app_name} ** (\u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** Yes **).\n4. \u041f\u043e\u0432\u0435\u0440\u043d\u0456\u0442\u044c\u0441\u044f \u0441\u044e\u0434\u0438 \u0442\u0430 \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **. \n\n[\u041f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 Telldus Live]({auth_url})",
+ "title": "Telldus Live"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "description": "\u043f\u043e\u0440\u043e\u0436\u043d\u044c\u043e",
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043a\u0456\u043d\u0446\u0435\u0432\u0443 \u0442\u043e\u0447\u043a\u0443."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py
index 444cabd0180f6d..f58c5916bfb1cd 100644
--- a/homeassistant/components/tellstick/sensor.py
+++ b/homeassistant/components/tellstick/sensor.py
@@ -6,7 +6,7 @@
import tellcore.constants as tellcore_constants
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_ID,
CONF_NAME,
@@ -15,7 +15,6 @@
TEMP_CELSIUS,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -126,7 +125,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors)
-class TellstickSensor(Entity):
+class TellstickSensor(SensorEntity):
"""Representation of a Tellstick sensor."""
def __init__(self, name, tellcore_sensor, datatype, sensor_info):
diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py
index fd26b1702dce98..7edbd3ba8123fc 100644
--- a/homeassistant/components/temper/sensor.py
+++ b/homeassistant/components/temper/sensor.py
@@ -4,14 +4,17 @@
from temperusb.temper import TemperHandler
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME, TEMP_FAHRENHEIT
-from homeassistant.helpers.entity import Entity
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.const import (
+ CONF_NAME,
+ CONF_OFFSET,
+ DEVICE_DEFAULT_NAME,
+ TEMP_FAHRENHEIT,
+)
_LOGGER = logging.getLogger(__name__)
CONF_SCALE = "scale"
-CONF_OFFSET = "offset"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -26,7 +29,6 @@
def get_temper_devices():
"""Scan the Temper devices from temperusb."""
-
return TemperHandler().get_devices()
@@ -55,7 +57,7 @@ def reset_devices():
sensor.set_temper_device(device)
-class TemperSensor(Entity):
+class TemperSensor(SensorEntity):
"""Representation of a Temper temperature sensor."""
def __init__(self, temper_device, temp_unit, name, scaling):
diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py
index 079569a932462e..72a97d6eeab7a4 100644
--- a/homeassistant/components/template/__init__.py
+++ b/homeassistant/components/template/__init__.py
@@ -1,22 +1,143 @@
"""The template component."""
-from homeassistant.const import SERVICE_RELOAD
+from __future__ import annotations
+
+import asyncio
+import logging
+from typing import Callable
+
+from homeassistant import config as conf_util
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.const import EVENT_HOMEASSISTANT_START, SERVICE_RELOAD
+from homeassistant.core import CoreState, Event, callback
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers import (
+ discovery,
+ trigger as trigger_helper,
+ update_coordinator,
+)
from homeassistant.helpers.reload import async_reload_integration_platforms
+from homeassistant.loader import async_get_integration
+
+from .const import CONF_TRIGGER, DOMAIN, PLATFORMS
-from .const import DOMAIN, EVENT_TEMPLATE_RELOADED, PLATFORMS
+_LOGGER = logging.getLogger(__name__)
-async def async_setup_reload_service(hass):
- """Create the reload service for the template domain."""
+async def async_setup(hass, config):
+ """Set up the template integration."""
+ if DOMAIN in config:
+ await _process_config(hass, config)
- if hass.services.has_service(DOMAIN, SERVICE_RELOAD):
- return
+ async def _reload_config(call: Event) -> None:
+ """Reload top-level + platforms."""
+ try:
+ unprocessed_conf = await conf_util.async_hass_config_yaml(hass)
+ except HomeAssistantError as err:
+ _LOGGER.error(err)
+ return
- async def _reload_config(call):
- """Reload the template platform config."""
+ conf = await conf_util.async_process_component_config(
+ hass, unprocessed_conf, await async_get_integration(hass, DOMAIN)
+ )
+
+ if conf is None:
+ return
await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS)
- hass.bus.async_fire(EVENT_TEMPLATE_RELOADED, context=call.context)
+
+ if DOMAIN in conf:
+ await _process_config(hass, conf)
+
+ hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context)
hass.helpers.service.async_register_admin_service(
DOMAIN, SERVICE_RELOAD, _reload_config
)
+
+ return True
+
+
+async def _process_config(hass, config):
+ """Process config."""
+ coordinators: list[TriggerUpdateCoordinator] | None = hass.data.get(DOMAIN)
+
+ # Remove old ones
+ if coordinators:
+ for coordinator in coordinators:
+ coordinator.async_remove()
+
+ async def init_coordinator(hass, conf):
+ coordinator = TriggerUpdateCoordinator(hass, conf)
+ await coordinator.async_setup(conf)
+ return coordinator
+
+ hass.data[DOMAIN] = await asyncio.gather(
+ *[init_coordinator(hass, conf) for conf in config[DOMAIN]]
+ )
+
+
+class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator):
+ """Class to handle incoming data."""
+
+ REMOVE_TRIGGER = object()
+
+ def __init__(self, hass, config):
+ """Instantiate trigger data."""
+ super().__init__(hass, _LOGGER, name="Trigger Update Coordinator")
+ self.config = config
+ self._unsub_start: Callable[[], None] | None = None
+ self._unsub_trigger: Callable[[], None] | None = None
+
+ @property
+ def unique_id(self) -> str | None:
+ """Return unique ID for the entity."""
+ return self.config.get("unique_id")
+
+ @callback
+ def async_remove(self):
+ """Signal that the entities need to remove themselves."""
+ if self._unsub_start:
+ self._unsub_start()
+ if self._unsub_trigger:
+ self._unsub_trigger()
+
+ async def async_setup(self, hass_config):
+ """Set up the trigger and create entities."""
+ if self.hass.state == CoreState.running:
+ await self._attach_triggers()
+ else:
+ self._unsub_start = self.hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_START, self._attach_triggers
+ )
+
+ for platform_domain in (SENSOR_DOMAIN,):
+ self.hass.async_create_task(
+ discovery.async_load_platform(
+ self.hass,
+ platform_domain,
+ DOMAIN,
+ {"coordinator": self, "entities": self.config[platform_domain]},
+ hass_config,
+ )
+ )
+
+ async def _attach_triggers(self, start_event=None) -> None:
+ """Attach the triggers."""
+ if start_event is not None:
+ self._unsub_start = None
+
+ self._unsub_trigger = await trigger_helper.async_initialize_triggers(
+ self.hass,
+ self.config[CONF_TRIGGER],
+ self._handle_triggered,
+ DOMAIN,
+ self.name,
+ self.logger.log,
+ start_event is not None,
+ )
+
+ @callback
+ def _handle_triggered(self, run_variables, context=None):
+ self.async_set_updated_data(
+ {"run_variables": run_variables, "context": context}
+ )
diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py
index ccefab767be560..4c72c5094efcae 100644
--- a/homeassistant/components/template/alarm_control_panel.py
+++ b/homeassistant/components/template/alarm_control_panel.py
@@ -32,10 +32,8 @@
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
-from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.script import Script
-from .const import DOMAIN, PLATFORMS
from .template_entity import TemplateEntity
_LOGGER = logging.getLogger(__name__)
@@ -81,7 +79,6 @@
async def _async_create_entities(hass, config):
"""Create Template Alarm Control Panels."""
-
alarm_control_panels = []
for device, device_config in config[CONF_ALARM_CONTROL_PANELS].items():
@@ -114,8 +111,6 @@ async def _async_create_entities(hass, config):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Template Alarm Control Panels."""
-
- await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
async_add_entities(await _async_create_entities(hass, config))
diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py
index f996b91a61eb32..1088652cd0a0b6 100644
--- a/homeassistant/components/template/binary_sensor.py
+++ b/homeassistant/components/template/binary_sensor.py
@@ -22,10 +22,9 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.event import async_call_later
-from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.template import result_as_boolean
-from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS
+from .const import CONF_AVAILABILITY_TEMPLATE
from .template_entity import TemplateEntity
CONF_DELAY_ON = "delay_on"
@@ -97,8 +96,6 @@ async def _async_create_entities(hass, config):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the template binary sensors."""
-
- await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
async_add_entities(await _async_create_entities(hass, config))
@@ -141,7 +138,6 @@ def __init__(
async def async_added_to_hass(self):
"""Register callbacks."""
-
self.add_template_attribute("_state", self._template, None, self._update_state)
if self._delay_on_raw is not None:
diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py
new file mode 100644
index 00000000000000..5d1a66836f3e2f
--- /dev/null
+++ b/homeassistant/components/template/config.py
@@ -0,0 +1,128 @@
+"""Template config validator."""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.sensor import (
+ DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
+ DOMAIN as SENSOR_DOMAIN,
+)
+from homeassistant.config import async_log_exception, config_without_domain
+from homeassistant.const import (
+ CONF_DEVICE_CLASS,
+ CONF_ENTITY_PICTURE_TEMPLATE,
+ CONF_FRIENDLY_NAME,
+ CONF_FRIENDLY_NAME_TEMPLATE,
+ CONF_ICON,
+ CONF_ICON_TEMPLATE,
+ CONF_NAME,
+ CONF_SENSORS,
+ CONF_STATE,
+ CONF_UNIQUE_ID,
+ CONF_UNIT_OF_MEASUREMENT,
+ CONF_VALUE_TEMPLATE,
+)
+from homeassistant.helpers import config_validation as cv, template
+from homeassistant.helpers.trigger import async_validate_trigger_config
+
+from .const import (
+ CONF_ATTRIBUTE_TEMPLATES,
+ CONF_ATTRIBUTES,
+ CONF_AVAILABILITY,
+ CONF_AVAILABILITY_TEMPLATE,
+ CONF_PICTURE,
+ CONF_TRIGGER,
+ DOMAIN,
+)
+from .sensor import SENSOR_SCHEMA as PLATFORM_SENSOR_SCHEMA
+
+LEGACY_SENSOR = {
+ CONF_ICON_TEMPLATE: CONF_ICON,
+ CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE,
+ CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY,
+ CONF_ATTRIBUTE_TEMPLATES: CONF_ATTRIBUTES,
+ CONF_FRIENDLY_NAME_TEMPLATE: CONF_NAME,
+ CONF_FRIENDLY_NAME: CONF_NAME,
+ CONF_VALUE_TEMPLATE: CONF_STATE,
+}
+
+
+SENSOR_SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_NAME): cv.template,
+ vol.Required(CONF_STATE): cv.template,
+ vol.Optional(CONF_ICON): cv.template,
+ vol.Optional(CONF_PICTURE): cv.template,
+ vol.Optional(CONF_AVAILABILITY): cv.template,
+ vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}),
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
+ vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
+ vol.Optional(CONF_UNIQUE_ID): cv.string,
+ }
+)
+
+CONFIG_SECTION_SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_UNIQUE_ID): cv.string,
+ vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
+ vol.Optional(SENSOR_DOMAIN): vol.All(cv.ensure_list, [SENSOR_SCHEMA]),
+ vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys(PLATFORM_SENSOR_SCHEMA),
+ }
+)
+
+
+def _rewrite_legacy_to_modern_trigger_conf(cfg: dict):
+ """Rewrite a legacy to a modern trigger-basd conf."""
+ logging.getLogger(__name__).warning(
+ "The entity definition format under template: differs from the platform configuration format. See https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors"
+ )
+ sensor = list(cfg[SENSOR_DOMAIN]) if SENSOR_DOMAIN in cfg else []
+
+ for device_id, entity_cfg in cfg[CONF_SENSORS].items():
+ entity_cfg = {**entity_cfg}
+
+ for from_key, to_key in LEGACY_SENSOR.items():
+ if from_key not in entity_cfg or to_key in entity_cfg:
+ continue
+
+ val = entity_cfg.pop(from_key)
+ if isinstance(val, str):
+ val = template.Template(val)
+ entity_cfg[to_key] = val
+
+ if CONF_NAME not in entity_cfg:
+ entity_cfg[CONF_NAME] = template.Template(device_id)
+
+ sensor.append(entity_cfg)
+
+ return {**cfg, "sensor": sensor}
+
+
+async def async_validate_config(hass, config):
+ """Validate config."""
+ if DOMAIN not in config:
+ return config
+
+ config_sections = []
+
+ for cfg in cv.ensure_list(config[DOMAIN]):
+ try:
+ cfg = CONFIG_SECTION_SCHEMA(cfg)
+ cfg[CONF_TRIGGER] = await async_validate_trigger_config(
+ hass, cfg[CONF_TRIGGER]
+ )
+ except vol.Invalid as err:
+ async_log_exception(err, DOMAIN, cfg, hass)
+ continue
+
+ if CONF_TRIGGER in cfg and CONF_SENSORS in cfg:
+ cfg = _rewrite_legacy_to_modern_trigger_conf(cfg)
+
+ config_sections.append(cfg)
+
+ # Create a copy of the configuration with all config for current
+ # component removed and add validated config back in.
+ config = config_without_domain(config, DOMAIN)
+ config[DOMAIN] = config_sections
+
+ return config
diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py
index cf1ec8bc1c3aca..971d4a864c95c8 100644
--- a/homeassistant/components/template/const.py
+++ b/homeassistant/components/template/const.py
@@ -1,13 +1,13 @@
"""Constants for the Template Platform Components."""
CONF_AVAILABILITY_TEMPLATE = "availability_template"
+CONF_ATTRIBUTE_TEMPLATES = "attribute_templates"
+CONF_TRIGGER = "trigger"
DOMAIN = "template"
PLATFORM_STORAGE_KEY = "template_platforms"
-EVENT_TEMPLATE_RELOADED = "event_template_reloaded"
-
PLATFORMS = [
"alarm_control_panel",
"binary_sensor",
@@ -18,4 +18,9 @@
"sensor",
"switch",
"vacuum",
+ "weather",
]
+
+CONF_AVAILABILITY = "availability"
+CONF_ATTRIBUTES = "attributes"
+CONF_PICTURE = "picture"
diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py
index 93ffd2fd988f3f..d985473792edcd 100644
--- a/homeassistant/components/template/cover.py
+++ b/homeassistant/components/template/cover.py
@@ -20,6 +20,7 @@
CoverEntity,
)
from homeassistant.const import (
+ CONF_COVERS,
CONF_DEVICE_CLASS,
CONF_ENTITY_ID,
CONF_ENTITY_PICTURE_TEMPLATE,
@@ -37,10 +38,9 @@
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
-from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.script import Script
-from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS
+from .const import CONF_AVAILABILITY_TEMPLATE
from .template_entity import TemplateEntity
_LOGGER = logging.getLogger(__name__)
@@ -53,8 +53,6 @@
"false",
]
-CONF_COVERS = "covers"
-
CONF_POSITION_TEMPLATE = "position_template"
CONF_TILT_TEMPLATE = "tilt_template"
OPEN_ACTION = "open_cover"
@@ -161,8 +159,6 @@ async def _async_create_entities(hass, config):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Template cover."""
-
- await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
async_add_entities(await _async_create_entities(hass, config))
@@ -223,12 +219,13 @@ def __init__(
self._optimistic = optimistic or (not state_template and not position_template)
self._tilt_optimistic = tilt_optimistic or not tilt_template
self._position = None
+ self._is_opening = False
+ self._is_closing = False
self._tilt_value = None
self._unique_id = unique_id
async def async_added_to_hass(self):
"""Register callbacks."""
-
if self._template:
self.add_template_attribute(
"_position", self._template, None, self._update_state
@@ -265,6 +262,9 @@ def _update_state(self, result):
self._position = 100
else:
self._position = 0
+
+ self._is_opening = state == STATE_OPENING
+ self._is_closing = state == STATE_CLOSING
else:
_LOGGER.error(
"Received invalid cover is_on state: %s. Expected: %s",
@@ -324,6 +324,16 @@ def is_closed(self):
"""Return if the cover is closed."""
return self._position == 0
+ @property
+ def is_opening(self):
+ """Return if the cover is currently opening."""
+ return self._is_opening
+
+ @property
+ def is_closing(self):
+ """Return if the cover is currently closing."""
+ return self._is_closing
+
@property
def current_cover_position(self):
"""Return current position of cover.
diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py
index 5d01790f21af86..563a9af2849056 100644
--- a/homeassistant/components/template/fan.py
+++ b/homeassistant/components/template/fan.py
@@ -36,16 +36,16 @@
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
-from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.script import Script
-from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS
+from .const import CONF_AVAILABILITY_TEMPLATE
from .template_entity import TemplateEntity
_LOGGER = logging.getLogger(__name__)
CONF_FANS = "fans"
CONF_SPEED_LIST = "speeds"
+CONF_SPEED_COUNT = "speed_count"
CONF_PRESET_MODES = "preset_modes"
CONF_SPEED_TEMPLATE = "speed_template"
CONF_PERCENTAGE_TEMPLATE = "percentage_template"
@@ -86,6 +86,7 @@
vol.Optional(CONF_SET_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA,
+ vol.Optional(CONF_SPEED_COUNT): vol.Coerce(int),
vol.Optional(
CONF_SPEED_LIST,
default=[SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH],
@@ -126,6 +127,7 @@ async def _async_create_entities(hass, config):
set_direction_action = device_config.get(CONF_SET_DIRECTION_ACTION)
speed_list = device_config[CONF_SPEED_LIST]
+ speed_count = device_config.get(CONF_SPEED_COUNT)
preset_modes = device_config.get(CONF_PRESET_MODES)
unique_id = device_config.get(CONF_UNIQUE_ID)
@@ -148,6 +150,7 @@ async def _async_create_entities(hass, config):
set_preset_mode_action,
set_oscillating_action,
set_direction_action,
+ speed_count,
speed_list,
preset_modes,
unique_id,
@@ -159,8 +162,6 @@ async def _async_create_entities(hass, config):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the template fans."""
-
- await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
async_add_entities(await _async_create_entities(hass, config))
@@ -186,6 +187,7 @@ def __init__(
set_preset_mode_action,
set_oscillating_action,
set_direction_action,
+ speed_count,
speed_list,
preset_modes,
unique_id,
@@ -261,12 +263,20 @@ def __init__(
self._unique_id = unique_id
+ # Number of valid speeds
+ self._speed_count = speed_count
+
# List of valid speeds
self._speed_list = speed_list
# List of valid preset modes
self._preset_modes = preset_modes
+ @property
+ def _implemented_speed(self):
+ """Return true if speed has been implemented."""
+ return bool(self._set_speed_script or self._speed_template)
+
@property
def name(self):
"""Return the display name of this fan."""
@@ -282,6 +292,11 @@ def supported_features(self) -> int:
"""Flag supported features."""
return self._supported_features
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return self._speed_count or 100
+
@property
def speed_list(self) -> list:
"""Get the list of available speeds."""
@@ -511,7 +526,6 @@ def _update_speed(self, speed):
speed = str(speed)
if speed in self._speed_list:
- self._state = STATE_OFF if speed == SPEED_OFF else STATE_ON
self._speed = speed
self._percentage = self.speed_to_percentage(speed)
self._preset_mode = speed if speed in self.preset_modes else None
@@ -540,7 +554,6 @@ def _update_percentage(self, percentage):
return
if 0 <= percentage <= 100:
- self._state = STATE_OFF if percentage == 0 else STATE_ON
self._percentage = percentage
if self._speed_list:
self._speed = self.percentage_to_speed(percentage)
@@ -557,7 +570,6 @@ def _update_preset_mode(self, preset_mode):
preset_mode = str(preset_mode)
if preset_mode in self.preset_modes:
- self._state = STATE_ON
self._speed = preset_mode
self._percentage = None
self._preset_mode = preset_mode
diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py
index 42493136b48e90..e76ba42289b161 100644
--- a/homeassistant/components/template/light.py
+++ b/homeassistant/components/template/light.py
@@ -31,10 +31,9 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
from homeassistant.helpers.entity import async_generate_entity_id
-from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.script import Script
-from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS
+from .const import CONF_AVAILABILITY_TEMPLATE
from .template_entity import TemplateEntity
_LOGGER = logging.getLogger(__name__)
@@ -137,8 +136,6 @@ async def _async_create_entities(hass, config):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the template lights."""
-
- await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
async_add_entities(await _async_create_entities(hass, config))
@@ -259,7 +256,6 @@ def is_on(self):
async def async_added_to_hass(self):
"""Register callbacks."""
-
if self._template:
self.add_template_attribute(
"_state", self._template, None, self._update_state
@@ -404,7 +400,6 @@ def _update_white_value(self, white_value):
@callback
def _update_state(self, result):
"""Update the state from the template."""
-
if isinstance(result, TemplateError):
# This behavior is legacy
self._state = False
@@ -431,7 +426,6 @@ def _update_state(self, result):
@callback
def _update_temperature(self, render):
"""Update the temperature from the template."""
-
try:
if render in ("None", ""):
self._temperature = None
diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py
index 2cd8bc00266190..c4a3977a4dbd52 100644
--- a/homeassistant/components/template/lock.py
+++ b/homeassistant/components/template/lock.py
@@ -13,10 +13,9 @@
from homeassistant.core import callback
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.script import Script
-from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS
+from .const import CONF_AVAILABILITY_TEMPLATE
from .template_entity import TemplateEntity
CONF_LOCK = "lock"
@@ -60,8 +59,6 @@ async def _async_create_entities(hass, config):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the template lock."""
-
- await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
async_add_entities(await _async_create_entities(hass, config))
@@ -129,7 +126,6 @@ def _update_state(self, result):
async def async_added_to_hass(self):
"""Register callbacks."""
-
self.add_template_attribute(
"_state", self._state_template, None, self._update_state
)
diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py
index aea14884812f5c..4631a77584710d 100644
--- a/homeassistant/components/template/sensor.py
+++ b/homeassistant/components/template/sensor.py
@@ -1,35 +1,36 @@
"""Allows the creation of a sensor that breaks out state_attributes."""
-from typing import Optional
+from __future__ import annotations
import voluptuous as vol
from homeassistant.components.sensor import (
DEVICE_CLASSES_SCHEMA,
+ DOMAIN as SENSOR_DOMAIN,
ENTITY_ID_FORMAT,
PLATFORM_SCHEMA,
+ SensorEntity,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
- ATTR_FRIENDLY_NAME,
- ATTR_UNIT_OF_MEASUREMENT,
CONF_DEVICE_CLASS,
CONF_ENTITY_PICTURE_TEMPLATE,
+ CONF_FRIENDLY_NAME,
CONF_FRIENDLY_NAME_TEMPLATE,
CONF_ICON_TEMPLATE,
CONF_SENSORS,
+ CONF_STATE,
CONF_UNIQUE_ID,
+ CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import callback
from homeassistant.exceptions import TemplateError
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity, async_generate_entity_id
-from homeassistant.helpers.reload import async_setup_reload_service
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.entity import async_generate_entity_id
-from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS
+from .const import CONF_ATTRIBUTE_TEMPLATES, CONF_AVAILABILITY_TEMPLATE, CONF_TRIGGER
from .template_entity import TemplateEntity
-
-CONF_ATTRIBUTE_TEMPLATES = "attribute_templates"
+from .trigger_entity import TriggerEntity
SENSOR_SCHEMA = vol.All(
cv.deprecated(ATTR_ENTITY_ID),
@@ -43,8 +44,8 @@
vol.Optional(CONF_ATTRIBUTE_TEMPLATES, default={}): vol.Schema(
{cv.string: cv.template}
),
- vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
- vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string,
+ vol.Optional(CONF_FRIENDLY_NAME): cv.string,
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_UNIQUE_ID): cv.string,
@@ -52,14 +53,32 @@
),
)
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA)}
+
+def trigger_warning(val):
+ """Warn if a trigger is defined."""
+ if CONF_TRIGGER in val:
+ raise vol.Invalid(
+ "You can only add triggers to template entities if they are defined under `template:`. "
+ "See the template documentation for more information: https://www.home-assistant.io/integrations/template/"
+ )
+
+ return val
+
+
+PLATFORM_SCHEMA = vol.All(
+ PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_TRIGGER): cv.match_all, # to raise custom warning
+ vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA),
+ }
+ ),
+ trigger_warning,
)
-async def _async_create_entities(hass, config):
+@callback
+def _async_create_template_tracking_entities(hass, config):
"""Create the template sensors."""
-
sensors = []
for device, device_config in config[CONF_SENSORS].items():
@@ -67,11 +86,11 @@ async def _async_create_entities(hass, config):
icon_template = device_config.get(CONF_ICON_TEMPLATE)
entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE)
availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE)
- friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
+ friendly_name = device_config.get(CONF_FRIENDLY_NAME, device)
friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE)
- unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT)
+ unit_of_measurement = device_config.get(CONF_UNIT_OF_MEASUREMENT)
device_class = device_config.get(CONF_DEVICE_CLASS)
- attribute_templates = device_config[CONF_ATTRIBUTE_TEMPLATES]
+ attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {})
unique_id = device_config.get(CONF_UNIQUE_ID)
sensors.append(
@@ -96,12 +115,16 @@ async def _async_create_entities(hass, config):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the template sensors."""
-
- await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
- async_add_entities(await _async_create_entities(hass, config))
+ if discovery_info is None:
+ async_add_entities(_async_create_template_tracking_entities(hass, config))
+ else:
+ async_add_entities(
+ TriggerSensorEntity(hass, discovery_info["coordinator"], config)
+ for config in discovery_info["entities"]
+ )
-class SensorTemplate(TemplateEntity, Entity):
+class SensorTemplate(TemplateEntity, SensorEntity):
"""Representation of a Template Sensor."""
def __init__(
@@ -140,7 +163,6 @@ def __init__(
async def async_added_to_hass(self):
"""Register callbacks."""
-
self.add_template_attribute("_state", self._template, None, self._update_state)
if self._friendly_name_template is not None:
self.add_template_attribute("_name", self._friendly_name_template)
@@ -168,7 +190,7 @@ def state(self):
return self._state
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the device class of the sensor."""
return self._device_class
@@ -176,3 +198,15 @@ def device_class(self) -> Optional[str]:
def unit_of_measurement(self):
"""Return the unit_of_measurement of the device."""
return self._unit_of_measurement
+
+
+class TriggerSensorEntity(TriggerEntity, SensorEntity):
+ """Sensor entity based on trigger data."""
+
+ domain = SENSOR_DOMAIN
+ extra_template_keys = (CONF_STATE,)
+
+ @property
+ def state(self) -> str | None:
+ """Return state of the sensor."""
+ return self._rendered.get(CONF_STATE)
diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py
index 511338b5aa1171..0e083df13f4912 100644
--- a/homeassistant/components/template/switch.py
+++ b/homeassistant/components/template/switch.py
@@ -22,11 +22,10 @@
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
-from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.script import Script
-from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS
+from .const import CONF_AVAILABILITY_TEMPLATE
from .template_entity import TemplateEntity
_VALID_STATES = [STATE_ON, STATE_OFF, "true", "false"]
@@ -90,8 +89,6 @@ async def _async_create_entities(hass, config):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the template switches."""
-
- await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
async_add_entities(await _async_create_entities(hass, config))
@@ -147,7 +144,6 @@ def _update_state(self, result):
async def async_added_to_hass(self):
"""Register callbacks."""
-
if self._template is None:
# restore state after startup
diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py
index 49b0edfab0230f..f8909206dec2a4 100644
--- a/homeassistant/components/template/template_entity.py
+++ b/homeassistant/components/template/template_entity.py
@@ -1,7 +1,8 @@
"""TemplateEntity utility class."""
+from __future__ import annotations
import logging
-from typing import Any, Callable, List, Optional, Union
+from typing import Any, Callable
import voluptuous as vol
@@ -30,8 +31,8 @@ def __init__(
attribute: str,
template: Template,
validator: Callable[[Any], Any] = None,
- on_update: Optional[Callable[[Any], None]] = None,
- none_on_template_error: Optional[bool] = False,
+ on_update: Callable[[Any], None] | None = None,
+ none_on_template_error: bool | None = False,
):
"""Template attribute."""
self._entity = entity
@@ -61,10 +62,10 @@ def _default_update(self, result):
@callback
def handle_result(
self,
- event: Optional[Event],
+ event: Event | None,
template: Template,
- last_result: Union[str, None, TemplateError],
- result: Union[str, TemplateError],
+ last_result: str | None | TemplateError,
+ result: str | TemplateError,
) -> None:
"""Handle a template result event callback."""
if isinstance(result, TemplateError):
@@ -168,7 +169,7 @@ def entity_picture(self):
return self._entity_picture
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attributes
@@ -189,7 +190,7 @@ def add_template_attribute(
attribute: str,
template: Template,
validator: Callable[[Any], Any] = None,
- on_update: Optional[Callable[[Any], None]] = None,
+ on_update: Callable[[Any], None] | None = None,
none_on_template_error: bool = False,
) -> None:
"""
@@ -219,11 +220,10 @@ def add_template_attribute(
@callback
def _handle_results(
self,
- event: Optional[Event],
- updates: List[TrackTemplateResult],
+ event: Event | None,
+ updates: list[TrackTemplateResult],
) -> None:
"""Call back the results to the attributes."""
-
if event:
self.async_set_context(event.context)
diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py
index 80ad585486b294..e631950a74ac43 100644
--- a/homeassistant/components/template/trigger.py
+++ b/homeassistant/components/template/trigger.py
@@ -31,27 +31,58 @@ async def async_attach_trigger(
hass, config, action, automation_info, *, platform_type="template"
):
"""Listen for state changes based on configuration."""
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
value_template = config.get(CONF_VALUE_TEMPLATE)
value_template.hass = hass
time_delta = config.get(CONF_FOR)
template.attach(hass, time_delta)
delay_cancel = None
job = HassJob(action)
+ armed = False
+
+ # Arm at setup if the template is already false.
+ try:
+ if not result_as_boolean(
+ value_template.async_render(automation_info["variables"])
+ ):
+ armed = True
+ except exceptions.TemplateError as ex:
+ _LOGGER.warning(
+ "Error initializing 'template' trigger for '%s': %s",
+ automation_info["name"],
+ ex,
+ )
@callback
def template_listener(event, updates):
"""Listen for state changes and calls action."""
- nonlocal delay_cancel
+ nonlocal delay_cancel, armed
result = updates.pop().result
+ if isinstance(result, exceptions.TemplateError):
+ _LOGGER.warning(
+ "Error evaluating 'template' trigger for '%s': %s",
+ automation_info["name"],
+ result,
+ )
+ return
+
if delay_cancel:
# pylint: disable=not-callable
delay_cancel()
delay_cancel = None
if not result_as_boolean(result):
+ armed = True
return
+ # Only fire when previously armed.
+ if not armed:
+ return
+
+ # Fire!
+ armed = False
+
entity_id = event and event.data.get("entity_id")
from_s = event and event.data.get("old_state")
to_s = event and event.data.get("new_state")
@@ -70,6 +101,7 @@ def template_listener(event, updates):
trigger_variables = {
"for": time_delta,
"description": description,
+ "id": trigger_id,
}
@callback
diff --git a/homeassistant/components/template/trigger_entity.py b/homeassistant/components/template/trigger_entity.py
new file mode 100644
index 00000000000000..418fa976304d7c
--- /dev/null
+++ b/homeassistant/components/template/trigger_entity.py
@@ -0,0 +1,145 @@
+"""Trigger entity."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from homeassistant.const import (
+ CONF_DEVICE_CLASS,
+ CONF_ICON,
+ CONF_NAME,
+ CONF_UNIQUE_ID,
+ CONF_UNIT_OF_MEASUREMENT,
+)
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers import template, update_coordinator
+
+from . import TriggerUpdateCoordinator
+from .const import CONF_ATTRIBUTES, CONF_AVAILABILITY, CONF_PICTURE
+
+
+class TriggerEntity(update_coordinator.CoordinatorEntity):
+ """Template entity based on trigger data."""
+
+ domain = ""
+ extra_template_keys: tuple | None = None
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ coordinator: TriggerUpdateCoordinator,
+ config: dict,
+ ):
+ """Initialize the entity."""
+ super().__init__(coordinator)
+
+ entity_unique_id = config.get(CONF_UNIQUE_ID)
+
+ if entity_unique_id and coordinator.unique_id:
+ self._unique_id = f"{coordinator.unique_id}-{entity_unique_id}"
+ else:
+ self._unique_id = entity_unique_id
+
+ self._config = config
+
+ self._static_rendered = {}
+ self._to_render = []
+
+ for itm in (
+ CONF_NAME,
+ CONF_ICON,
+ CONF_PICTURE,
+ CONF_AVAILABILITY,
+ ):
+ if itm not in config:
+ continue
+
+ if config[itm].is_static:
+ self._static_rendered[itm] = config[itm].template
+ else:
+ self._to_render.append(itm)
+
+ if self.extra_template_keys is not None:
+ self._to_render.extend(self.extra_template_keys)
+
+ # We make a copy so our initial render is 'unknown' and not 'unavailable'
+ self._rendered = dict(self._static_rendered)
+
+ @property
+ def name(self):
+ """Name of the entity."""
+ return self._rendered.get(CONF_NAME)
+
+ @property
+ def unique_id(self):
+ """Return unique ID of the entity."""
+ return self._unique_id
+
+ @property
+ def device_class(self):
+ """Return device class of the entity."""
+ return self._config.get(CONF_DEVICE_CLASS)
+
+ @property
+ def unit_of_measurement(self) -> str | None:
+ """Return unit of measurement."""
+ return self._config.get(CONF_UNIT_OF_MEASUREMENT)
+
+ @property
+ def icon(self) -> str | None:
+ """Return icon."""
+ return self._rendered.get(CONF_ICON)
+
+ @property
+ def entity_picture(self) -> str | None:
+ """Return entity picture."""
+ return self._rendered.get(CONF_PICTURE)
+
+ @property
+ def available(self):
+ """Return availability of the entity."""
+ return (
+ self._rendered is not self._static_rendered
+ and
+ # Check against False so `None` is ok
+ self._rendered.get(CONF_AVAILABILITY) is not False
+ )
+
+ @property
+ def extra_state_attributes(self) -> dict[str, Any] | None:
+ """Return extra attributes."""
+ return self._rendered.get(CONF_ATTRIBUTES)
+
+ async def async_added_to_hass(self) -> None:
+ """Handle being added to Home Assistant."""
+ template.attach(self.hass, self._config)
+ await super().async_added_to_hass()
+ if self.coordinator.data is not None:
+ self._handle_coordinator_update()
+
+ @callback
+ def _handle_coordinator_update(self) -> None:
+ """Handle updated data from the coordinator."""
+ try:
+ rendered = dict(self._static_rendered)
+
+ for key in self._to_render:
+ rendered[key] = self._config[key].async_render(
+ self.coordinator.data["run_variables"], parse_result=False
+ )
+
+ if CONF_ATTRIBUTES in self._config:
+ rendered[CONF_ATTRIBUTES] = template.render_complex(
+ self._config[CONF_ATTRIBUTES],
+ self.coordinator.data["run_variables"],
+ )
+
+ self._rendered = rendered
+ except template.TemplateError as err:
+ logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error(
+ "Error rendering %s template for %s: %s", key, self.entity_id, err
+ )
+ self._rendered = self._static_rendered
+
+ self.async_set_context(self.coordinator.data["context"])
+ self.async_write_ha_state()
diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py
index 5bf8148b96e75a..ed7919d174e257 100644
--- a/homeassistant/components/template/vacuum.py
+++ b/homeassistant/components/template/vacuum.py
@@ -41,10 +41,9 @@
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
-from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.script import Script
-from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS
+from .const import CONF_AVAILABILITY_TEMPLATE
from .template_entity import TemplateEntity
_LOGGER = logging.getLogger(__name__)
@@ -147,8 +146,6 @@ async def _async_create_entities(hass, config):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the template vacuums."""
-
- await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
async_add_entities(await _async_create_entities(hass, config))
@@ -337,7 +334,6 @@ async def async_set_fan_speed(self, fan_speed, **kwargs):
async def async_added_to_hass(self):
"""Register callbacks."""
-
if self._template is not None:
self.add_template_attribute(
"_state", self._template, None, self._update_state
diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py
new file mode 100644
index 00000000000000..eabafb89803c15
--- /dev/null
+++ b/homeassistant/components/template/weather.py
@@ -0,0 +1,284 @@
+"""Template platform that aggregates meteorological data."""
+import voluptuous as vol
+
+from homeassistant.components.weather import (
+ ATTR_CONDITION_CLEAR_NIGHT,
+ ATTR_CONDITION_CLOUDY,
+ ATTR_CONDITION_EXCEPTIONAL,
+ ATTR_CONDITION_FOG,
+ ATTR_CONDITION_HAIL,
+ ATTR_CONDITION_LIGHTNING,
+ ATTR_CONDITION_LIGHTNING_RAINY,
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_POURING,
+ ATTR_CONDITION_RAINY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_CONDITION_SNOWY_RAINY,
+ ATTR_CONDITION_SUNNY,
+ ATTR_CONDITION_WINDY,
+ ATTR_CONDITION_WINDY_VARIANT,
+ ENTITY_ID_FORMAT,
+ WeatherEntity,
+)
+from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
+from homeassistant.helpers.entity import async_generate_entity_id
+
+from .template_entity import TemplateEntity
+
+CONDITION_CLASSES = {
+ ATTR_CONDITION_CLEAR_NIGHT,
+ ATTR_CONDITION_CLOUDY,
+ ATTR_CONDITION_FOG,
+ ATTR_CONDITION_HAIL,
+ ATTR_CONDITION_LIGHTNING,
+ ATTR_CONDITION_LIGHTNING_RAINY,
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_POURING,
+ ATTR_CONDITION_RAINY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_CONDITION_SNOWY_RAINY,
+ ATTR_CONDITION_SUNNY,
+ ATTR_CONDITION_WINDY,
+ ATTR_CONDITION_WINDY_VARIANT,
+ ATTR_CONDITION_EXCEPTIONAL,
+}
+
+CONF_WEATHER = "weather"
+CONF_TEMPERATURE_TEMPLATE = "temperature_template"
+CONF_HUMIDITY_TEMPLATE = "humidity_template"
+CONF_CONDITION_TEMPLATE = "condition_template"
+CONF_ATTRIBUTION_TEMPLATE = "attribution_template"
+CONF_PRESSURE_TEMPLATE = "pressure_template"
+CONF_WIND_SPEED_TEMPLATE = "wind_speed_template"
+CONF_WIND_BEARING_TEMPLATE = "wind_bearing_template"
+CONF_OZONE_TEMPLATE = "ozone_template"
+CONF_VISIBILITY_TEMPLATE = "visibility_template"
+CONF_FORECAST_TEMPLATE = "forecast_template"
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_NAME): cv.string,
+ vol.Required(CONF_CONDITION_TEMPLATE): cv.template,
+ vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template,
+ vol.Required(CONF_HUMIDITY_TEMPLATE): cv.template,
+ vol.Optional(CONF_ATTRIBUTION_TEMPLATE): cv.template,
+ vol.Optional(CONF_PRESSURE_TEMPLATE): cv.template,
+ vol.Optional(CONF_WIND_SPEED_TEMPLATE): cv.template,
+ vol.Optional(CONF_WIND_BEARING_TEMPLATE): cv.template,
+ vol.Optional(CONF_OZONE_TEMPLATE): cv.template,
+ vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template,
+ vol.Optional(CONF_FORECAST_TEMPLATE): cv.template,
+ vol.Optional(CONF_UNIQUE_ID): cv.string,
+ }
+)
+
+
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Set up the Template weather."""
+
+ name = config[CONF_NAME]
+ condition_template = config[CONF_CONDITION_TEMPLATE]
+ temperature_template = config[CONF_TEMPERATURE_TEMPLATE]
+ humidity_template = config[CONF_HUMIDITY_TEMPLATE]
+ attribution_template = config.get(CONF_ATTRIBUTION_TEMPLATE)
+ pressure_template = config.get(CONF_PRESSURE_TEMPLATE)
+ wind_speed_template = config.get(CONF_WIND_SPEED_TEMPLATE)
+ wind_bearing_template = config.get(CONF_WIND_BEARING_TEMPLATE)
+ ozone_template = config.get(CONF_OZONE_TEMPLATE)
+ visibility_template = config.get(CONF_VISIBILITY_TEMPLATE)
+ forecast_template = config.get(CONF_FORECAST_TEMPLATE)
+ unique_id = config.get(CONF_UNIQUE_ID)
+
+ async_add_entities(
+ [
+ WeatherTemplate(
+ hass,
+ name,
+ condition_template,
+ temperature_template,
+ humidity_template,
+ attribution_template,
+ pressure_template,
+ wind_speed_template,
+ wind_bearing_template,
+ ozone_template,
+ visibility_template,
+ forecast_template,
+ unique_id,
+ )
+ ]
+ )
+
+
+class WeatherTemplate(TemplateEntity, WeatherEntity):
+ """Representation of a weather condition."""
+
+ def __init__(
+ self,
+ hass,
+ name,
+ condition_template,
+ temperature_template,
+ humidity_template,
+ attribution_template,
+ pressure_template,
+ wind_speed_template,
+ wind_bearing_template,
+ ozone_template,
+ visibility_template,
+ forecast_template,
+ unique_id,
+ ):
+ """Initialize the Demo weather."""
+ super().__init__()
+
+ self._name = name
+ self._condition_template = condition_template
+ self._temperature_template = temperature_template
+ self._humidity_template = humidity_template
+ self._attribution_template = attribution_template
+ self._pressure_template = pressure_template
+ self._wind_speed_template = wind_speed_template
+ self._wind_bearing_template = wind_bearing_template
+ self._ozone_template = ozone_template
+ self._visibility_template = visibility_template
+ self._forecast_template = forecast_template
+ self._unique_id = unique_id
+
+ self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, name, hass=hass)
+
+ self._condition = None
+ self._temperature = None
+ self._humidity = None
+ self._attribution = None
+ self._pressure = None
+ self._wind_speed = None
+ self._wind_bearing = None
+ self._ozone = None
+ self._visibility = None
+ self._forecast = []
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def condition(self):
+ """Return the current condition."""
+ return self._condition
+
+ @property
+ def temperature(self):
+ """Return the temperature."""
+ return self._temperature
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement."""
+ return self.hass.config.units.temperature_unit
+
+ @property
+ def humidity(self):
+ """Return the humidity."""
+ return self._humidity
+
+ @property
+ def wind_speed(self):
+ """Return the wind speed."""
+ return self._wind_speed
+
+ @property
+ def wind_bearing(self):
+ """Return the wind bearing."""
+ return self._wind_bearing
+
+ @property
+ def ozone(self):
+ """Return the ozone level."""
+ return self._ozone
+
+ @property
+ def visibility(self):
+ """Return the visibility."""
+ return self._visibility
+
+ @property
+ def pressure(self):
+ """Return the air pressure."""
+ return self._pressure
+
+ @property
+ def forecast(self):
+ """Return the forecast."""
+ return self._forecast
+
+ @property
+ def attribution(self):
+ """Return the attribution."""
+ if self._attribution is None:
+ return "Powered by Home Assistant"
+ return self._attribution
+
+ @property
+ def unique_id(self):
+ """Return the unique id of this weather instance."""
+ return self._unique_id
+
+ async def async_added_to_hass(self):
+ """Register callbacks."""
+
+ if self._condition_template:
+ self.add_template_attribute(
+ "_condition",
+ self._condition_template,
+ lambda condition: condition if condition in CONDITION_CLASSES else None,
+ )
+ if self._temperature_template:
+ self.add_template_attribute(
+ "_temperature",
+ self._temperature_template,
+ )
+ if self._humidity_template:
+ self.add_template_attribute(
+ "_humidity",
+ self._humidity_template,
+ )
+ if self._attribution_template:
+ self.add_template_attribute(
+ "_attribution",
+ self._attribution_template,
+ )
+ if self._pressure_template:
+ self.add_template_attribute(
+ "_pressure",
+ self._pressure_template,
+ )
+ if self._wind_speed_template:
+ self.add_template_attribute(
+ "_wind_speed",
+ self._wind_speed_template,
+ )
+ if self._wind_bearing_template:
+ self.add_template_attribute(
+ "_wind_bearing",
+ self._wind_bearing_template,
+ )
+ if self._ozone_template:
+ self.add_template_attribute(
+ "_ozone",
+ self._ozone_template,
+ )
+ if self._visibility_template:
+ self.add_template_attribute(
+ "_visibility",
+ self._visibility_template,
+ )
+ if self._forecast_template:
+ self.add_template_attribute(
+ "_forecast",
+ self._forecast_template,
+ )
+ await super().async_added_to_hass()
diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py
index e387ae97afef4c..dad83005512b97 100644
--- a/homeassistant/components/tensorflow/image_processing.py
+++ b/homeassistant/components/tensorflow/image_processing.py
@@ -279,7 +279,7 @@ def state(self):
return self._total_matches
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
return {
ATTR_MATCHES: self._matches,
@@ -336,14 +336,13 @@ def process_image(self, image):
"""Process the image."""
model = self.hass.data[DOMAIN][CONF_MODEL]
if not model:
- _LOGGER.debug("Model not yet ready.")
+ _LOGGER.debug("Model not yet ready")
return
start = time.perf_counter()
try:
- import cv2 # pylint: disable=import-error, import-outside-toplevel
+ import cv2 # pylint: disable=import-outside-toplevel
- # pylint: disable=no-member
img = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED)
inp = img[:, :, [2, 1, 0]] # BGR->RGB
inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3)
diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json
index f039a14d5b3eec..84619680490c56 100644
--- a/homeassistant/components/tensorflow/manifest.json
+++ b/homeassistant/components/tensorflow/manifest.json
@@ -6,8 +6,8 @@
"tensorflow==2.3.0",
"tf-models-official==2.3.0",
"pycocotools==2.0.1",
- "numpy==1.19.2",
- "pillow==8.1.0"
+ "numpy==1.20.2",
+ "pillow==8.1.2"
],
"codeowners": []
}
diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py
index 51090d342718a1..5091d2ea102c27 100644
--- a/homeassistant/components/tesla/__init__.py
+++ b/homeassistant/components/tesla/__init__.py
@@ -5,10 +5,11 @@
import logging
import async_timeout
-from teslajsonpy import Controller as TeslaAPI, TeslaException
+from teslajsonpy import Controller as TeslaAPI
+from teslajsonpy.exceptions import IncompleteCredentials, TeslaException
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_IMPORT
+from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, ConfigEntry
from homeassistant.const import (
ATTR_BATTERY_CHARGING,
ATTR_BATTERY_LEVEL,
@@ -17,9 +18,9 @@
CONF_SCAN_INTERVAL,
CONF_TOKEN,
CONF_USERNAME,
+ HTTP_UNAUTHORIZED,
)
-from homeassistant.core import callback
-from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
@@ -28,12 +29,7 @@
)
from homeassistant.util import slugify
-from .config_flow import (
- CannotConnect,
- InvalidAuth,
- configured_instances,
- validate_input,
-)
+from .config_flow import CannotConnect, InvalidAuth, validate_input
from .const import (
CONF_WAKE_ON_START,
DATA_LISTENER,
@@ -42,7 +38,7 @@
DOMAIN,
ICONS,
MIN_SCAN_INTERVAL,
- TESLA_COMPONENTS,
+ PLATFORMS,
)
_LOGGER = logging.getLogger(__name__)
@@ -75,6 +71,16 @@ def _async_save_tokens(hass, config_entry, access_token, refresh_token):
)
+@callback
+def _async_configured_emails(hass):
+ """Return a set of configured Tesla emails."""
+ return {
+ entry.data[CONF_USERNAME]
+ for entry in hass.config_entries.async_entries(DOMAIN)
+ if CONF_USERNAME in entry.data
+ }
+
+
async def async_setup(hass, base_config):
"""Set up of Tesla component."""
@@ -95,7 +101,7 @@ def _update_entry(email, data=None, options=None):
email = config[CONF_USERNAME]
password = config[CONF_PASSWORD]
scan_interval = config[CONF_SCAN_INTERVAL]
- if email in configured_instances(hass):
+ if email in _async_configured_emails(hass):
try:
info = await validate_input(hass, config)
except (CannotConnect, InvalidAuth):
@@ -103,6 +109,8 @@ def _update_entry(email, data=None, options=None):
_update_entry(
email,
data={
+ CONF_USERNAME: email,
+ CONF_PASSWORD: password,
CONF_ACCESS_TOKEN: info[CONF_ACCESS_TOKEN],
CONF_TOKEN: info[CONF_TOKEN],
},
@@ -125,7 +133,8 @@ async def async_setup_entry(hass, config_entry):
"""Set up Tesla as config entry."""
hass.data.setdefault(DOMAIN, {})
config = config_entry.data
- websession = aiohttp_client.async_get_clientsession(hass)
+ # Because users can have multiple accounts, we always create a new session so they have separate cookies
+ websession = aiohttp_client.async_create_clientsession(hass)
email = config_entry.title
if email in hass.data[DOMAIN] and CONF_SCAN_INTERVAL in hass.data[DOMAIN][email]:
scan_interval = hass.data[DOMAIN][email][CONF_SCAN_INTERVAL]
@@ -136,6 +145,8 @@ async def async_setup_entry(hass, config_entry):
try:
controller = TeslaAPI(
websession,
+ email=config.get(CONF_USERNAME),
+ password=config.get(CONF_PASSWORD),
refresh_token=config[CONF_TOKEN],
access_token=config[CONF_ACCESS_TOKEN],
update_interval=config_entry.options.get(
@@ -147,7 +158,12 @@ async def async_setup_entry(hass, config_entry):
CONF_WAKE_ON_START, DEFAULT_WAKE_ON_START
)
)
+ except IncompleteCredentials:
+ _async_start_reauth(hass, config_entry)
+ return False
except TeslaException as ex:
+ if ex.code == HTTP_UNAUTHORIZED:
+ _async_start_reauth(hass, config_entry)
_LOGGER.error("Unable to communicate with Tesla API: %s", ex.message)
return False
_async_save_tokens(hass, config_entry, access_token, refresh_token)
@@ -162,9 +178,7 @@ async def async_setup_entry(hass, config_entry):
}
_LOGGER.debug("Connected to the Tesla API")
- await coordinator.async_refresh()
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
all_devices = controller.get_homeassistant_components()
@@ -174,10 +188,10 @@ async def async_setup_entry(hass, config_entry):
for device in all_devices:
entry_data["devices"][device.hass_type].append(device)
- for component in TESLA_COMPONENTS:
- _LOGGER.debug("Loading %s", component)
+ for platform in PLATFORMS:
+ _LOGGER.debug("Loading %s", platform)
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return True
@@ -187,8 +201,8 @@ async def async_unload_entry(hass, config_entry) -> bool:
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in TESLA_COMPONENTS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -202,6 +216,17 @@ async def async_unload_entry(hass, config_entry) -> bool:
return False
+def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry):
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_REAUTH},
+ data=entry.data,
+ )
+ )
+ _LOGGER.error("Credentials are no longer valid. Please reauthenticate")
+
+
async def update_listener(hass, config_entry):
"""Update when config_entry options update."""
controller = hass.data[DOMAIN][config_entry.entry_id]["coordinator"].controller
@@ -280,7 +305,7 @@ def icon(self):
return ICONS.get(self.tesla_device.type)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
attr = self._attributes
if self.tesla_device.has_battery():
diff --git a/homeassistant/components/tesla/climate.py b/homeassistant/components/tesla/climate.py
index 4c7ed850749dc0..81639bc3fe46c7 100644
--- a/homeassistant/components/tesla/climate.py
+++ b/homeassistant/components/tesla/climate.py
@@ -1,6 +1,7 @@
"""Support for Tesla HVAC system."""
+from __future__ import annotations
+
import logging
-from typing import List, Optional
from teslajsonpy.exceptions import UnknownPresetMode
@@ -103,7 +104,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None:
_LOGGER.error("%s", ex.message)
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp.
Requires SUPPORT_PRESET_MODE.
@@ -111,7 +112,7 @@ def preset_mode(self) -> Optional[str]:
return self.tesla_device.preset_mode
@property
- def preset_modes(self) -> Optional[List[str]]:
+ def preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes.
Requires SUPPORT_PRESET_MODE.
diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py
index debe896c9cf682..7b3060c5072a13 100644
--- a/homeassistant/components/tesla/config_flow.py
+++ b/homeassistant/components/tesla/config_flow.py
@@ -26,16 +26,6 @@
_LOGGER = logging.getLogger(__name__)
-DATA_SCHEMA = vol.Schema(
- {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
-)
-
-
-@callback
-def configured_instances(hass):
- """Return a set of configured Tesla instances."""
- return {entry.title for entry in hass.config_entries.async_entries(DOMAIN)}
-
class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Tesla."""
@@ -43,46 +33,56 @@ class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+ def __init__(self):
+ """Initialize the tesla flow."""
+ self.username = None
+
async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml."""
return await self.async_step_user(import_config)
async def async_step_user(self, user_input=None):
"""Handle the start of the config flow."""
+ errors = {}
+
+ if user_input is not None:
+ existing_entry = self._async_entry_for_username(user_input[CONF_USERNAME])
+ if (
+ existing_entry
+ and existing_entry.data[CONF_PASSWORD] == user_input[CONF_PASSWORD]
+ ):
+ return self.async_abort(reason="already_configured")
+
+ try:
+ info = await validate_input(self.hass, user_input)
+ except CannotConnect:
+ errors["base"] = "cannot_connect"
+ except InvalidAuth:
+ errors["base"] = "invalid_auth"
+
+ if not errors:
+ if existing_entry:
+ self.hass.config_entries.async_update_entry(
+ existing_entry, data=info
+ )
+ await self.hass.config_entries.async_reload(existing_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
+
+ return self.async_create_entry(
+ title=user_input[CONF_USERNAME], data=info
+ )
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=self._async_schema(),
+ errors=errors,
+ description_placeholders={},
+ )
- if not user_input:
- return self.async_show_form(
- step_id="user",
- data_schema=DATA_SCHEMA,
- errors={},
- description_placeholders={},
- )
-
- if user_input[CONF_USERNAME] in configured_instances(self.hass):
- return self.async_show_form(
- step_id="user",
- data_schema=DATA_SCHEMA,
- errors={CONF_USERNAME: "already_configured"},
- description_placeholders={},
- )
-
- try:
- info = await validate_input(self.hass, user_input)
- except CannotConnect:
- return self.async_show_form(
- step_id="user",
- data_schema=DATA_SCHEMA,
- errors={"base": "cannot_connect"},
- description_placeholders={},
- )
- except InvalidAuth:
- return self.async_show_form(
- step_id="user",
- data_schema=DATA_SCHEMA,
- errors={"base": "invalid_auth"},
- description_placeholders={},
- )
- return self.async_create_entry(title=user_input[CONF_USERNAME], data=info)
+ async def async_step_reauth(self, data):
+ """Handle configuration by re-auth."""
+ self.username = data[CONF_USERNAME]
+ return await self.async_step_user()
@staticmethod
@callback
@@ -90,6 +90,24 @@ def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)
+ @callback
+ def _async_schema(self):
+ """Fetch schema with defaults."""
+ return vol.Schema(
+ {
+ vol.Required(CONF_USERNAME, default=self.username): str,
+ vol.Required(CONF_PASSWORD): str,
+ }
+ )
+
+ @callback
+ def _async_entry_for_username(self, username):
+ """Find an existing entry for a username."""
+ for entry in self._async_current_entries():
+ if entry.data.get(CONF_USERNAME) == username:
+ return entry
+ return None
+
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a option flow for Tesla."""
@@ -129,7 +147,8 @@ async def validate_input(hass: core.HomeAssistant, data):
"""
config = {}
- websession = aiohttp_client.async_get_clientsession(hass)
+ websession = aiohttp_client.async_create_clientsession(hass)
+
try:
controller = TeslaAPI(
websession,
@@ -140,6 +159,8 @@ async def validate_input(hass: core.HomeAssistant, data):
(config[CONF_TOKEN], config[CONF_ACCESS_TOKEN]) = await controller.connect(
test_login=True
)
+ config[CONF_USERNAME] = data[CONF_USERNAME]
+ config[CONF_PASSWORD] = data[CONF_PASSWORD]
except TeslaException as ex:
if ex.code == HTTP_UNAUTHORIZED:
_LOGGER.error("Invalid credentials: %s", ex)
diff --git a/homeassistant/components/tesla/const.py b/homeassistant/components/tesla/const.py
index 2b8485c76164ef..94883e4a833b1d 100644
--- a/homeassistant/components/tesla/const.py
+++ b/homeassistant/components/tesla/const.py
@@ -5,7 +5,8 @@
DEFAULT_SCAN_INTERVAL = 660
DEFAULT_WAKE_ON_START = False
MIN_SCAN_INTERVAL = 60
-TESLA_COMPONENTS = [
+
+PLATFORMS = [
"sensor",
"lock",
"climate",
@@ -13,6 +14,7 @@
"device_tracker",
"switch",
]
+
ICONS = {
"battery sensor": "mdi:battery",
"range sensor": "mdi:gauge",
diff --git a/homeassistant/components/tesla/device_tracker.py b/homeassistant/components/tesla/device_tracker.py
index cac89d58d3a009..6813b3769e79f1 100644
--- a/homeassistant/components/tesla/device_tracker.py
+++ b/homeassistant/components/tesla/device_tracker.py
@@ -1,5 +1,5 @@
"""Support for tracking Tesla cars."""
-from typing import Optional
+from __future__ import annotations
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
from homeassistant.components.device_tracker.config_entry import TrackerEntity
@@ -25,13 +25,13 @@ class TeslaDeviceEntity(TeslaDevice, TrackerEntity):
"""A class representing a Tesla device."""
@property
- def latitude(self) -> Optional[float]:
+ def latitude(self) -> float | None:
"""Return latitude value of the device."""
location = self.tesla_device.get_location()
return self.tesla_device.get_location().get("latitude") if location else None
@property
- def longitude(self) -> Optional[float]:
+ def longitude(self) -> float | None:
"""Return longitude value of the device."""
location = self.tesla_device.get_location()
return self.tesla_device.get_location().get("longitude") if location else None
@@ -42,9 +42,9 @@ def source_type(self):
return SOURCE_TYPE_GPS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
- attr = super().device_state_attributes.copy()
+ attr = super().extra_state_attributes.copy()
location = self.tesla_device.get_location()
if location:
attr.update(
diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json
index 3679c0f74d1012..9236aae7fb6eea 100644
--- a/homeassistant/components/tesla/manifest.json
+++ b/homeassistant/components/tesla/manifest.json
@@ -3,11 +3,11 @@
"name": "Tesla",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tesla",
- "requirements": ["teslajsonpy==0.10.4"],
+ "requirements": ["teslajsonpy==0.11.5"],
"codeowners": ["@zabuldon", "@alandtse"],
"dhcp": [
- {"hostname":"tesla_*","macaddress":"4CFCAA*"},
- {"hostname":"tesla_*","macaddress":"044EAF*"},
- {"hostname":"tesla_*","macaddress":"98ED5C*"}
+ { "hostname": "tesla_*", "macaddress": "4CFCAA*" },
+ { "hostname": "tesla_*", "macaddress": "044EAF*" },
+ { "hostname": "tesla_*", "macaddress": "98ED5C*" }
]
}
diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py
index 3b66845c78621a..40c7aa8548df95 100644
--- a/homeassistant/components/tesla/sensor.py
+++ b/homeassistant/components/tesla/sensor.py
@@ -1,14 +1,13 @@
"""Support for the Tesla sensors."""
-from typing import Optional
+from __future__ import annotations
-from homeassistant.components.sensor import DEVICE_CLASSES
+from homeassistant.components.sensor import DEVICE_CLASSES, SensorEntity
from homeassistant.const import (
LENGTH_KILOMETERS,
LENGTH_MILES,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
-from homeassistant.helpers.entity import Entity
from homeassistant.util.distance import convert
from . import DOMAIN as TESLA_DOMAIN, TeslaDevice
@@ -27,7 +26,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities, True)
-class TeslaSensor(TeslaDevice, Entity):
+class TeslaSensor(TeslaDevice, SensorEntity):
"""Representation of Tesla sensors."""
def __init__(self, tesla_device, coordinator, sensor_type=None):
@@ -39,7 +38,7 @@ def __init__(self, tesla_device, coordinator, sensor_type=None):
self._unique_id = f"{super().unique_id}_{self.type}"
@property
- def state(self) -> Optional[float]:
+ def state(self) -> float | None:
"""Return the state of the sensor."""
if self.tesla_device.type == "temperature sensor":
if self.type == "outside":
@@ -58,7 +57,7 @@ def state(self) -> Optional[float]:
return self.tesla_device.get_value()
@property
- def unit_of_measurement(self) -> Optional[str]:
+ def unit_of_measurement(self) -> str | None:
"""Return the unit_of_measurement of the device."""
units = self.tesla_device.measurement
if units == "F":
@@ -72,7 +71,7 @@ def unit_of_measurement(self) -> Optional[str]:
return units
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the device_class of the device."""
return (
self.tesla_device.device_class
@@ -81,7 +80,7 @@ def device_class(self) -> Optional[str]:
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
attr = self._attributes.copy()
if self.tesla_device.type == "charging rate sensor":
diff --git a/homeassistant/components/tesla/strings.json b/homeassistant/components/tesla/strings.json
index 503124eedd4e8f..c75562528defb1 100644
--- a/homeassistant/components/tesla/strings.json
+++ b/homeassistant/components/tesla/strings.json
@@ -5,6 +5,10 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
+ "abort": {
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/tesla/translations/ca.json b/homeassistant/components/tesla/translations/ca.json
index 4d0583af408fbf..2a51c0297ae7ac 100644
--- a/homeassistant/components/tesla/translations/ca.json
+++ b/homeassistant/components/tesla/translations/ca.json
@@ -1,5 +1,9 @@
{
"config": {
+ "abort": {
+ "already_configured": "El compte ja ha estat configurat",
+ "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament"
+ },
"error": {
"already_configured": "El compte ja ha estat configurat",
"cannot_connect": "Ha fallat la connexi\u00f3",
diff --git a/homeassistant/components/tesla/translations/cs.json b/homeassistant/components/tesla/translations/cs.json
index c611b85d7348ad..9c117223d40896 100644
--- a/homeassistant/components/tesla/translations/cs.json
+++ b/homeassistant/components/tesla/translations/cs.json
@@ -1,5 +1,9 @@
{
"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": {
"already_configured": "\u00da\u010det je ji\u017e nastaven",
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
diff --git a/homeassistant/components/tesla/translations/de.json b/homeassistant/components/tesla/translations/de.json
index 09100c355c20a0..2fd964fe01320c 100644
--- a/homeassistant/components/tesla/translations/de.json
+++ b/homeassistant/components/tesla/translations/de.json
@@ -1,7 +1,13 @@
{
"config": {
+ "abort": {
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
+ },
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
},
"step": {
"user": {
@@ -18,6 +24,7 @@
"step": {
"init": {
"data": {
+ "enable_wake_on_start": "Aufwachen des Autos beim Start erzwingen",
"scan_interval": "Sekunden zwischen den Scans"
}
}
diff --git a/homeassistant/components/tesla/translations/en.json b/homeassistant/components/tesla/translations/en.json
index f2b888552b9f1a..53b213ac19b254 100644
--- a/homeassistant/components/tesla/translations/en.json
+++ b/homeassistant/components/tesla/translations/en.json
@@ -1,5 +1,9 @@
{
"config": {
+ "abort": {
+ "already_configured": "Account is already configured",
+ "reauth_successful": "Re-authentication was successful"
+ },
"error": {
"already_configured": "Account is already configured",
"cannot_connect": "Failed to connect",
diff --git a/homeassistant/components/tesla/translations/et.json b/homeassistant/components/tesla/translations/et.json
index ae427f5d1e7553..c7ceae36990e53 100644
--- a/homeassistant/components/tesla/translations/et.json
+++ b/homeassistant/components/tesla/translations/et.json
@@ -1,5 +1,9 @@
{
"config": {
+ "abort": {
+ "already_configured": "Kasutaja on juba seadistatud",
+ "reauth_successful": "Taastuvastamine \u00f5nnestus"
+ },
"error": {
"already_configured": "Konto on juba h\u00e4\u00e4lestatud",
"cannot_connect": "\u00dchendamine nurjus",
diff --git a/homeassistant/components/tesla/translations/fr.json b/homeassistant/components/tesla/translations/fr.json
index c8efc8b4fb5303..889c32a7d911f3 100644
--- a/homeassistant/components/tesla/translations/fr.json
+++ b/homeassistant/components/tesla/translations/fr.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9",
+ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi"
+ },
"error": {
- "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9",
+ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9",
"cannot_connect": "\u00c9chec de connexion",
"invalid_auth": "Authentification invalide"
},
diff --git a/homeassistant/components/tesla/translations/he.json b/homeassistant/components/tesla/translations/he.json
new file mode 100644
index 00000000000000..ac90b3264eab33
--- /dev/null
+++ b/homeassistant/components/tesla/translations/he.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tesla/translations/hu.json b/homeassistant/components/tesla/translations/hu.json
index c6553d4a595a35..a4622ce7efa15a 100644
--- a/homeassistant/components/tesla/translations/hu.json
+++ b/homeassistant/components/tesla/translations/hu.json
@@ -1,5 +1,14 @@
{
"config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
+ },
+ "error": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/tesla/translations/id.json b/homeassistant/components/tesla/translations/id.json
new file mode 100644
index 00000000000000..681504d0d42a6d
--- /dev/null
+++ b/homeassistant/components/tesla/translations/id.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi",
+ "reauth_successful": "Autentikasi ulang berhasil"
+ },
+ "error": {
+ "already_configured": "Akun sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Email"
+ },
+ "description": "Masukkan informasi Anda.",
+ "title": "Tesla - Konfigurasi"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "enable_wake_on_start": "Paksa mobil bangun saat dinyalakan",
+ "scan_interval": "Interval pemindaian dalam detik"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tesla/translations/it.json b/homeassistant/components/tesla/translations/it.json
index a316b41c29c8ee..3a137da78f1484 100644
--- a/homeassistant/components/tesla/translations/it.json
+++ b/homeassistant/components/tesla/translations/it.json
@@ -1,5 +1,9 @@
{
"config": {
+ "abort": {
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato",
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
+ },
"error": {
"already_configured": "L'account \u00e8 gi\u00e0 configurato",
"cannot_connect": "Impossibile connettersi",
diff --git a/homeassistant/components/tesla/translations/ko.json b/homeassistant/components/tesla/translations/ko.json
index 27a96518ca728d..285326f39de44b 100644
--- a/homeassistant/components/tesla/translations/ko.json
+++ b/homeassistant/components/tesla/translations/ko.json
@@ -1,5 +1,14 @@
{
"config": {
+ "abort": {
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
"step": {
"user": {
"data": {
@@ -16,7 +25,7 @@
"init": {
"data": {
"enable_wake_on_start": "\uc2dc\ub3d9 \uc2dc \ucc28\ub7c9 \uae68\uc6b0\uae30",
- "scan_interval": "\uc2a4\uce94 \uac04\uaca9(\ucd08)"
+ "scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ucd08)"
}
}
}
diff --git a/homeassistant/components/tesla/translations/nl.json b/homeassistant/components/tesla/translations/nl.json
index 9e79b35165dcd6..5655a641f96ee7 100644
--- a/homeassistant/components/tesla/translations/nl.json
+++ b/homeassistant/components/tesla/translations/nl.json
@@ -1,14 +1,19 @@
{
"config": {
+ "abort": {
+ "already_configured": "Account is al geconfigureerd",
+ "reauth_successful": "Herauthenticatie was succesvol"
+ },
"error": {
"already_configured": "Account is al geconfigureerd",
- "cannot_connect": "Kan geen verbinding maken"
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie"
},
"step": {
"user": {
"data": {
"password": "Wachtwoord",
- "username": "E-mailadres"
+ "username": "E-mail"
},
"description": "Vul alstublieft uw gegevens in.",
"title": "Tesla - Configuratie"
diff --git a/homeassistant/components/tesla/translations/no.json b/homeassistant/components/tesla/translations/no.json
index 36cceb97f9f8d2..ce70664063636c 100644
--- a/homeassistant/components/tesla/translations/no.json
+++ b/homeassistant/components/tesla/translations/no.json
@@ -1,5 +1,9 @@
{
"config": {
+ "abort": {
+ "already_configured": "Kontoen er allerede konfigurert",
+ "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket"
+ },
"error": {
"already_configured": "Kontoen er allerede konfigurert",
"cannot_connect": "Tilkobling mislyktes",
diff --git a/homeassistant/components/tesla/translations/pl.json b/homeassistant/components/tesla/translations/pl.json
index dc4144d0f6a54c..7ec634cd56c033 100644
--- a/homeassistant/components/tesla/translations/pl.json
+++ b/homeassistant/components/tesla/translations/pl.json
@@ -1,5 +1,9 @@
{
"config": {
+ "abort": {
+ "already_configured": "Konto jest ju\u017c skonfigurowane",
+ "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119"
+ },
"error": {
"already_configured": "Konto jest ju\u017c skonfigurowane",
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
diff --git a/homeassistant/components/tesla/translations/ru.json b/homeassistant/components/tesla/translations/ru.json
index 8fe167d8631b89..d62a2e1f16848b 100644
--- a/homeassistant/components/tesla/translations/ru.json
+++ b/homeassistant/components/tesla/translations/ru.json
@@ -1,9 +1,13 @@
{
"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.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
+ },
"error": {
"already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.",
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"step": {
"user": {
diff --git a/homeassistant/components/tesla/translations/tr.json b/homeassistant/components/tesla/translations/tr.json
new file mode 100644
index 00000000000000..cf0d144c1edf0d
--- /dev/null
+++ b/homeassistant/components/tesla/translations/tr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "E-posta"
+ },
+ "description": "L\u00fctfen bilgilerinizi giriniz."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tesla/translations/uk.json b/homeassistant/components/tesla/translations/uk.json
new file mode 100644
index 00000000000000..90d47ec2ff5910
--- /dev/null
+++ b/homeassistant/components/tesla/translations/uk.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u0430\u043d\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443.",
+ "title": "Tesla"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "enable_wake_on_start": "\u041f\u0440\u0438\u043c\u0443\u0441\u043e\u0432\u043e \u0440\u043e\u0437\u0431\u0443\u0434\u0438\u0442\u0438 \u043c\u0430\u0448\u0438\u043d\u0443 \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0443",
+ "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0456\u0436 \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f\u043c\u0438 (\u0441\u0435\u043a.)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tesla/translations/zh-Hant.json b/homeassistant/components/tesla/translations/zh-Hant.json
index 235c9036637769..d9b7fd4ef79584 100644
--- a/homeassistant/components/tesla/translations/zh-Hant.json
+++ b/homeassistant/components/tesla/translations/zh-Hant.json
@@ -1,5 +1,9 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f"
+ },
"error": {
"already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"cannot_connect": "\u9023\u7dda\u5931\u6557",
diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py
index 83a2fd12d24e58..86427349f3140f 100644
--- a/homeassistant/components/thermoworks_smoke/sensor.py
+++ b/homeassistant/components/thermoworks_smoke/sensor.py
@@ -11,7 +11,7 @@
import thermoworks_smoke
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
CONF_EMAIL,
@@ -21,7 +21,6 @@
TEMP_FAHRENHEIT,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -91,7 +90,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_LOGGER.error(msg)
-class ThermoworksSmokeSensor(Entity):
+class ThermoworksSmokeSensor(SensorEntity):
"""Implementation of a thermoworks smoke sensor."""
def __init__(self, sensor_type, serial, mgr):
@@ -124,7 +123,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attributes
diff --git a/homeassistant/components/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py
index eab843069f4e65..2e139eae63d33e 100644
--- a/homeassistant/components/thethingsnetwork/sensor.py
+++ b/homeassistant/components/thethingsnetwork/sensor.py
@@ -7,22 +7,25 @@
import async_timeout
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONTENT_TYPE_JSON, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.const import (
+ ATTR_DEVICE_ID,
+ ATTR_TIME,
+ CONF_DEVICE_ID,
+ CONTENT_TYPE_JSON,
+ HTTP_NOT_FOUND,
+ HTTP_UNAUTHORIZED,
+)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from . import DATA_TTN, TTN_ACCESS_KEY, TTN_APP_ID, TTN_DATA_STORAGE_URL
_LOGGER = logging.getLogger(__name__)
-ATTR_DEVICE_ID = "device_id"
ATTR_RAW = "raw"
-ATTR_TIME = "time"
DEFAULT_TIMEOUT = 10
-CONF_DEVICE_ID = "device_id"
CONF_VALUES = "values"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
@@ -55,7 +58,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(devices, True)
-class TtnDataSensor(Entity):
+class TtnDataSensor(SensorEntity):
"""Representation of a The Things Network Data Storage sensor."""
def __init__(self, ttn_data_storage, device_id, value, unit_of_measurement):
@@ -77,8 +80,8 @@ def state(self):
"""Return the state of the entity."""
if self._ttn_data_storage.data is not None:
try:
- return round(self._state[self._value], 1)
- except (KeyError, TypeError):
+ return self._state[self._value]
+ except KeyError:
return None
return None
@@ -88,7 +91,7 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
if self._ttn_data_storage.data is not None:
return {
diff --git a/homeassistant/components/thinkingcleaner/sensor.py b/homeassistant/components/thinkingcleaner/sensor.py
index 966930f1eb1610..56cf272f7d15ad 100644
--- a/homeassistant/components/thinkingcleaner/sensor.py
+++ b/homeassistant/components/thinkingcleaner/sensor.py
@@ -5,10 +5,9 @@
import voluptuous as vol
from homeassistant import util
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_HOST, PERCENTAGE
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
@@ -73,7 +72,7 @@ def update_devices():
add_entities(dev)
-class ThinkingCleanerSensor(Entity):
+class ThinkingCleanerSensor(SensorEntity):
"""Representation of a ThinkingCleaner Sensor."""
def __init__(self, tc_object, sensor_type, update_devices):
diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py
index fa05fc096870de..5bd6f77253b26c 100644
--- a/homeassistant/components/threshold/binary_sensor.py
+++ b/homeassistant/components/threshold/binary_sensor.py
@@ -144,7 +144,7 @@ def threshold_type(self):
return TYPE_UPPER
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {
ATTR_ENTITY_ID: self._entity_id,
diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py
index b4b29a84297887..fd7fc389c75c89 100644
--- a/homeassistant/components/tibber/__init__.py
+++ b/homeassistant/components/tibber/__init__.py
@@ -73,9 +73,9 @@ async def _close(event):
_LOGGER.error("Failed to login. %s", exp)
return False
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
# set up notify platform, no entry support for notify component yet,
@@ -93,8 +93,8 @@ async def async_unload_entry(hass, config_entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/tibber/config_flow.py b/homeassistant/components/tibber/config_flow.py
index 8dce1e66a5a584..2c498cc2ff55a0 100644
--- a/homeassistant/components/tibber/config_flow.py
+++ b/homeassistant/components/tibber/config_flow.py
@@ -9,7 +9,7 @@
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import DOMAIN
DATA_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json
index 652804859dafc1..108f05d5625155 100644
--- a/homeassistant/components/tibber/manifest.json
+++ b/homeassistant/components/tibber/manifest.json
@@ -2,7 +2,7 @@
"domain": "tibber",
"name": "Tibber",
"documentation": "https://www.home-assistant.io/integrations/tibber",
- "requirements": ["pyTibber==0.16.1"],
+ "requirements": ["pyTibber==0.16.2"],
"codeowners": ["@danielhiversen"],
"quality_scale": "silver",
"config_flow": true
diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py
index bb5ebe8011f42a..5ab85013a25746 100644
--- a/homeassistant/components/tibber/sensor.py
+++ b/homeassistant/components/tibber/sensor.py
@@ -6,10 +6,9 @@
import aiohttp
-from homeassistant.components.sensor import DEVICE_CLASS_POWER
+from homeassistant.components.sensor import DEVICE_CLASS_POWER, SensorEntity
from homeassistant.const import POWER_WATT
from homeassistant.exceptions import PlatformNotReady
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle, dt as dt_util
from .const import DOMAIN as TIBBER_DOMAIN, MANUFACTURER
@@ -45,7 +44,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
async_add_entities(dev, True)
-class TibberSensor(Entity):
+class TibberSensor(SensorEntity):
"""Representation of a generic Tibber sensor."""
def __init__(self, tibber_home):
@@ -54,7 +53,7 @@ def __init__(self, tibber_home):
self._last_updated = None
self._state = None
self._is_available = False
- self._device_state_attributes = {}
+ self._extra_state_attributes = {}
self._name = tibber_home.info["viewer"]["home"]["appNickname"]
if self._name is None:
self._name = tibber_home.info["viewer"]["home"]["address"].get(
@@ -63,9 +62,9 @@ def __init__(self, tibber_home):
self._spread_load_constant = randrange(3600)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
- return self._device_state_attributes
+ return self._extra_state_attributes
@property
def model(self):
@@ -121,10 +120,10 @@ async def async_update(self):
res = self._tibber_home.current_price_data()
self._state, price_level, self._last_updated = res
- self._device_state_attributes["price_level"] = price_level
+ self._extra_state_attributes["price_level"] = price_level
attrs = self._tibber_home.current_attributes()
- self._device_state_attributes.update(attrs)
+ self._extra_state_attributes.update(attrs)
self._is_available = self._state is not None
@property
@@ -165,11 +164,11 @@ async def _fetch_data(self):
except (asyncio.TimeoutError, aiohttp.ClientError):
return
data = self._tibber_home.info["viewer"]["home"]
- self._device_state_attributes["app_nickname"] = data["appNickname"]
- self._device_state_attributes["grid_company"] = data["meteringPointData"][
+ self._extra_state_attributes["app_nickname"] = data["appNickname"]
+ self._extra_state_attributes["grid_company"] = data["meteringPointData"][
"gridCompany"
]
- self._device_state_attributes["estimated_annual_consumption"] = data[
+ self._extra_state_attributes["estimated_annual_consumption"] = data[
"meteringPointData"
]["estimatedAnnualConsumption"]
@@ -197,7 +196,7 @@ async def _async_callback(self, payload):
for key, value in live_measurement.items():
if value is None:
continue
- self._device_state_attributes[key] = value
+ self._extra_state_attributes[key] = value
self.async_write_ha_state()
diff --git a/homeassistant/components/tibber/translations/de.json b/homeassistant/components/tibber/translations/de.json
index 670f57df8ba9e2..8d49c9d9e6176f 100644
--- a/homeassistant/components/tibber/translations/de.json
+++ b/homeassistant/components/tibber/translations/de.json
@@ -1,10 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Ein Tibber-Konto ist bereits konfiguriert."
+ "already_configured": "Der Dienst ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_access_token": "Ung\u00fcltiger Zugriffs-Token",
"timeout": "Zeit\u00fcberschreitung beim Verbinden mit Tibber"
},
diff --git a/homeassistant/components/tibber/translations/hu.json b/homeassistant/components/tibber/translations/hu.json
index 08a622fd238c01..6ad5902284518d 100644
--- a/homeassistant/components/tibber/translations/hu.json
+++ b/homeassistant/components/tibber/translations/hu.json
@@ -1,14 +1,20 @@
{
"config": {
+ "abort": {
+ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
- "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token"
+ "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token",
+ "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a Tibberhez val\u00f3 csatlakoz\u00e1skor"
},
"step": {
"user": {
"data": {
"access_token": "Hozz\u00e1f\u00e9r\u00e9si token"
- }
+ },
+ "description": "Add meg a hozz\u00e1f\u00e9r\u00e9si tokent a https://developer.tibber.com/settings/accesstoken c\u00edmr\u0151l",
+ "title": "Tibber"
}
}
}
diff --git a/homeassistant/components/tibber/translations/id.json b/homeassistant/components/tibber/translations/id.json
new file mode 100644
index 00000000000000..479cf83f8c7617
--- /dev/null
+++ b/homeassistant/components/tibber/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Layanan sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_access_token": "Token akses tidak valid",
+ "timeout": "Tenggang waktu terhubung ke Tibber habis"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Token Akses"
+ },
+ "description": "Masukkan token akses Anda dari https://developer.tibber.com/settings/accesstoken",
+ "title": "Tibber"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tibber/translations/ko.json b/homeassistant/components/tibber/translations/ko.json
index 1f99aa440b495c..5ba1f62e4ed137 100644
--- a/homeassistant/components/tibber/translations/ko.json
+++ b/homeassistant/components/tibber/translations/ko.json
@@ -1,9 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "Tibber \uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"timeout": "Tibber \uc5f0\uacb0 \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4."
},
diff --git a/homeassistant/components/tibber/translations/nl.json b/homeassistant/components/tibber/translations/nl.json
index 4a89639cf5070a..4a5e518f306625 100644
--- a/homeassistant/components/tibber/translations/nl.json
+++ b/homeassistant/components/tibber/translations/nl.json
@@ -4,6 +4,7 @@
"already_configured": "Service is al geconfigureerd"
},
"error": {
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_access_token": "Ongeldig toegangstoken",
"timeout": "Time-out om verbinding te maken met Tibber"
},
diff --git a/homeassistant/components/tibber/translations/tr.json b/homeassistant/components/tibber/translations/tr.json
new file mode 100644
index 00000000000000..5f8e72986b211f
--- /dev/null
+++ b/homeassistant/components/tibber/translations/tr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Eri\u015fim Belirteci"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tibber/translations/uk.json b/homeassistant/components/tibber/translations/uk.json
new file mode 100644
index 00000000000000..b1240116856403
--- /dev/null
+++ b/homeassistant/components/tibber/translations/uk.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_access_token": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443.",
+ "timeout": "\u0427\u0430\u0441 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043c\u0438\u043d\u0443\u0432."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443, \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 \u043d\u0430 \u0441\u0430\u0439\u0442\u0456 https://developer.tibber.com/settings/accesstoken",
+ "title": "Tibber"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tibber/translations/zh-Hant.json b/homeassistant/components/tibber/translations/zh-Hant.json
index ce10615a2892b6..e4d0ec10e234c2 100644
--- a/homeassistant/components/tibber/translations/zh-Hant.json
+++ b/homeassistant/components/tibber/translations/zh-Hant.json
@@ -5,15 +5,15 @@
},
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557",
- "invalid_access_token": "\u5b58\u53d6\u5bc6\u9470\u7121\u6548",
+ "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548",
"timeout": "\u9023\u7dda\u81f3 Tibber \u903e\u6642"
},
"step": {
"user": {
"data": {
- "access_token": "\u5b58\u53d6\u5bc6\u9470"
+ "access_token": "\u5b58\u53d6\u6b0a\u6756"
},
- "description": "\u8f38\u5165\u7531 https://developer.tibber.com/settings/accesstoken \u6240\u7372\u5f97\u7684\u5b58\u53d6\u5bc6\u9470",
+ "description": "\u8f38\u5165\u7531 https://developer.tibber.com/settings/accesstoken \u6240\u7372\u5f97\u7684\u5b58\u53d6\u6b0a\u6756",
"title": "Tibber"
}
}
diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py
index 205742017d3dee..48bf8177c63647 100644
--- a/homeassistant/components/tile/__init__.py
+++ b/homeassistant/components/tile/__init__.py
@@ -74,9 +74,9 @@ async def async_update_tile(tile):
await gather_with_concurrency(DEFAULT_INIT_TASK_LIMIT, *coordinator_init_tasks)
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -87,8 +87,8 @@ async def async_unload_entry(hass, entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/tile/config_flow.py b/homeassistant/components/tile/config_flow.py
index 87f58193e9d595..7cc98e7a77f417 100644
--- a/homeassistant/components/tile/config_flow.py
+++ b/homeassistant/components/tile/config_flow.py
@@ -7,7 +7,7 @@
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import aiohttp_client
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import DOMAIN
class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py
index f7cc4e1736e6cd..7571e235ef1206 100644
--- a/homeassistant/components/tile/device_tracker.py
+++ b/homeassistant/components/tile/device_tracker.py
@@ -81,7 +81,7 @@ def battery_level(self):
return None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
return self._attrs
diff --git a/homeassistant/components/tile/translations/de.json b/homeassistant/components/tile/translations/de.json
index 59f48253a18688..1c2af82aa63062 100644
--- a/homeassistant/components/tile/translations/de.json
+++ b/homeassistant/components/tile/translations/de.json
@@ -3,6 +3,9 @@
"abort": {
"already_configured": "Konto ist bereits konfiguriert"
},
+ "error": {
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/tile/translations/hu.json b/homeassistant/components/tile/translations/hu.json
new file mode 100644
index 00000000000000..c4a6e63030ce9b
--- /dev/null
+++ b/homeassistant/components/tile/translations/hu.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "E-mail"
+ },
+ "title": "Tile konfigur\u00e1l\u00e1sa"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "show_inactive": "Inakt\u00edv Tile-ok megjelen\u00edt\u00e9se"
+ },
+ "title": "Tile konfigur\u00e1l\u00e1sa"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tile/translations/id.json b/homeassistant/components/tile/translations/id.json
new file mode 100644
index 00000000000000..5b5c710594d56f
--- /dev/null
+++ b/homeassistant/components/tile/translations/id.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi"
+ },
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Email"
+ },
+ "title": "Konfigurasi Tile"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "show_inactive": "Tampilkan Tile yang tidak aktif"
+ },
+ "title": "Konfigurasi Tile"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tile/translations/ko.json b/homeassistant/components/tile/translations/ko.json
index 50ba5000a1a5c5..d592fef112cb9e 100644
--- a/homeassistant/components/tile/translations/ko.json
+++ b/homeassistant/components/tile/translations/ko.json
@@ -1,7 +1,10 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 Tile \uacc4\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
diff --git a/homeassistant/components/tile/translations/nl.json b/homeassistant/components/tile/translations/nl.json
index 26c5726868938a..236d250122a689 100644
--- a/homeassistant/components/tile/translations/nl.json
+++ b/homeassistant/components/tile/translations/nl.json
@@ -3,10 +3,14 @@
"abort": {
"already_configured": "Apparaat is al geconfigureerd"
},
+ "error": {
+ "invalid_auth": "Ongeldige authenticatie"
+ },
"step": {
"user": {
"data": {
- "password": "Wachtwoord"
+ "password": "Wachtwoord",
+ "username": "E-mail"
},
"title": "Tegel configureren"
}
diff --git a/homeassistant/components/tile/translations/ru.json b/homeassistant/components/tile/translations/ru.json
index 62d0b10857c64f..f42a4d631b04e8 100644
--- a/homeassistant/components/tile/translations/ru.json
+++ b/homeassistant/components/tile/translations/ru.json
@@ -4,7 +4,7 @@
"already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant."
},
"error": {
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"step": {
"user": {
diff --git a/homeassistant/components/tile/translations/tr.json b/homeassistant/components/tile/translations/tr.json
new file mode 100644
index 00000000000000..8a04e2f4bbff6f
--- /dev/null
+++ b/homeassistant/components/tile/translations/tr.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "E-posta"
+ },
+ "title": "Karoyu Yap\u0131land\u0131r"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "show_inactive": "Etkin Olmayan Karolar\u0131 G\u00f6ster"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tile/translations/uk.json b/homeassistant/components/tile/translations/uk.json
new file mode 100644
index 00000000000000..dc28164fd93c2d
--- /dev/null
+++ b/homeassistant/components/tile/translations/uk.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438"
+ },
+ "title": "Tile"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "show_inactive": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043d\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457"
+ },
+ "title": "Tile"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py
index 4615e9e046c680..08195e6dd3dc87 100644
--- a/homeassistant/components/time_date/sensor.py
+++ b/homeassistant/components/time_date/sensor.py
@@ -4,11 +4,10 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_DISPLAY_OPTIONS
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_point_in_utc_time
import homeassistant.util.dt as dt_util
@@ -47,7 +46,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
-class TimeDateSensor(Entity):
+class TimeDateSensor(SensorEntity):
"""Implementation of a Time and Date sensor."""
def __init__(self, hass, option_type):
diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py
index 64d651b4cd8e35..2ff408dcd819e0 100644
--- a/homeassistant/components/timer/__init__.py
+++ b/homeassistant/components/timer/__init__.py
@@ -1,7 +1,8 @@
"""Support for Timers."""
+from __future__ import annotations
+
from datetime import datetime, timedelta
import logging
-from typing import Dict, Optional
import voluptuous as vol
@@ -107,8 +108,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
yaml_collection = collection.YamlCollection(
logging.getLogger(f"{__name__}.yaml_collection"), id_manager
)
- collection.attach_entity_component_collection(
- component, yaml_collection, Timer.from_yaml
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, component, yaml_collection, Timer.from_yaml
)
storage_collection = TimerStorageCollection(
@@ -116,7 +117,9 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
logging.getLogger(f"{__name__}.storage_collection"),
id_manager,
)
- collection.attach_entity_component_collection(component, storage_collection, Timer)
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, component, storage_collection, Timer
+ )
await yaml_collection.async_load(
[{CONF_ID: id_, **cfg} for id_, cfg in config.get(DOMAIN, {}).items()]
@@ -127,9 +130,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
).async_setup(hass)
- collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection)
- collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection)
-
async def reload_service_handler(service_call: ServiceCallType) -> None:
"""Reload yaml entities."""
conf = await component.async_prepare_reload(skip_reset=True)
@@ -164,7 +164,7 @@ class TimerStorageCollection(collection.StorageCollection):
CREATE_SCHEMA = vol.Schema(CREATE_FIELDS)
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
- async def _process_create_data(self, data: Dict) -> Dict:
+ async def _process_create_data(self, data: dict) -> dict:
"""Validate the config is valid."""
data = self.CREATE_SCHEMA(data)
# make duration JSON serializeable
@@ -172,11 +172,11 @@ async def _process_create_data(self, data: Dict) -> Dict:
return data
@callback
- def _get_suggested_id(self, info: Dict) -> str:
+ def _get_suggested_id(self, info: dict) -> str:
"""Suggest an ID based on the config."""
return info[CONF_NAME]
- async def _update_data(self, data: dict, update_data: Dict) -> Dict:
+ async def _update_data(self, data: dict, update_data: dict) -> dict:
"""Return a new updated data object."""
data = {**data, **self.UPDATE_SCHEMA(update_data)}
# make duration JSON serializeable
@@ -188,18 +188,18 @@ async def _update_data(self, data: dict, update_data: Dict) -> Dict:
class Timer(RestoreEntity):
"""Representation of a timer."""
- def __init__(self, config: Dict):
+ def __init__(self, config: dict):
"""Initialize a timer."""
self._config: dict = config
self.editable: bool = True
self._state: str = STATUS_IDLE
self._duration = cv.time_period_str(config[CONF_DURATION])
- self._remaining: Optional[timedelta] = None
- self._end: Optional[datetime] = None
+ self._remaining: timedelta | None = None
+ self._end: datetime | None = None
self._listener = None
@classmethod
- def from_yaml(cls, config: Dict) -> "Timer":
+ def from_yaml(cls, config: dict) -> Timer:
"""Return entity instance initialized from yaml storage."""
timer = cls(config)
timer.entity_id = ENTITY_ID_FORMAT.format(config[CONF_ID])
@@ -232,7 +232,7 @@ def state(self):
return self._state
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attrs = {
ATTR_DURATION: _format_timedelta(self._duration),
@@ -246,7 +246,7 @@ def state_attributes(self):
return attrs
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return unique id for the entity."""
return self._config[CONF_ID]
@@ -327,7 +327,9 @@ def async_finish(self):
if self._state != STATUS_ACTIVE:
return
- self._listener = None
+ if self._listener:
+ self._listener()
+ self._listener = None
self._state = STATUS_IDLE
self._end = None
self._remaining = None
@@ -347,7 +349,7 @@ def _async_finished(self, time):
self.hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id})
self.async_write_ha_state()
- async def async_update_config(self, config: Dict) -> None:
+ async def async_update_config(self, config: dict) -> None:
"""Handle when the config is updated."""
self._config = config
self._duration = cv.time_period_str(config[CONF_DURATION])
diff --git a/homeassistant/components/timer/reproduce_state.py b/homeassistant/components/timer/reproduce_state.py
index 71abb0bfd71a5e..377f8a1dda22c3 100644
--- a/homeassistant/components/timer/reproduce_state.py
+++ b/homeassistant/components/timer/reproduce_state.py
@@ -1,7 +1,9 @@
"""Reproduce an Timer state."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import Context, State
@@ -27,8 +29,8 @@ async def _async_reproduce_state(
hass: HomeAssistantType,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -69,8 +71,8 @@ async def async_reproduce_states(
hass: HomeAssistantType,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Timer states."""
await asyncio.gather(
diff --git a/homeassistant/components/timer/services.yaml b/homeassistant/components/timer/services.yaml
index cd810c21de51fa..54175de3cf7fef 100644
--- a/homeassistant/components/timer/services.yaml
+++ b/homeassistant/components/timer/services.yaml
@@ -1,36 +1,28 @@
# Describes the format for available timer services
start:
- description: Start a timer.
-
+ name: Start
+ description: Start a timer
+ target:
fields:
- entity_id:
- description: Entity id of the timer to start. [optional]
- example: "timer.timer0"
duration:
description: Duration the timer requires to finish. [optional]
+ default: 0
example: "00:01:00 or 60"
+ selector:
+ text:
pause:
+ name: Pause
description: Pause a timer.
-
- fields:
- entity_id:
- description: Entity id of the timer to pause. [optional]
- example: "timer.timer0"
+ target:
cancel:
+ name: Cancel
description: Cancel a timer.
-
- fields:
- entity_id:
- description: Entity id of the timer to cancel. [optional]
- example: "timer.timer0"
+ target:
finish:
+ name: Finish
description: Finish a timer.
-
- fields:
- entity_id:
- description: Entity id of the timer to finish. [optional]
- example: "timer.timer0"
+ target:
diff --git a/homeassistant/components/timer/translations/uk.json b/homeassistant/components/timer/translations/uk.json
index df690bded93a51..ce937735406f91 100644
--- a/homeassistant/components/timer/translations/uk.json
+++ b/homeassistant/components/timer/translations/uk.json
@@ -1,9 +1,9 @@
{
"state": {
"_": {
- "active": "\u0430\u043a\u0442\u0438\u0432\u043d\u0438\u0439",
- "idle": "\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f",
- "paused": "\u043d\u0430 \u043f\u0430\u0443\u0437\u0456"
+ "active": "\u0410\u043a\u0442\u0438\u0432\u043d\u0438\u0439",
+ "idle": "\u041e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f",
+ "paused": "\u041f\u0440\u0438\u0437\u0443\u043f\u0438\u043d\u0435\u043d\u043e"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/tmb/sensor.py b/homeassistant/components/tmb/sensor.py
index f731b912d65c61..88471a86c27e10 100644
--- a/homeassistant/components/tmb/sensor.py
+++ b/homeassistant/components/tmb/sensor.py
@@ -6,10 +6,9 @@
from tmb import IBus
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, TIME_MINUTES
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -63,7 +62,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class TMBSensor(Entity):
+class TMBSensor(SensorEntity):
"""Implementation of a TMB line/stop Sensor."""
def __init__(self, ibus_client, stop, line, name):
@@ -101,7 +100,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the last update."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py
index fde26acf604f78..a0fed1f803218b 100644
--- a/homeassistant/components/tod/binary_sensor.py
+++ b/homeassistant/components/tod/binary_sensor.py
@@ -109,7 +109,7 @@ def next_update(self):
return self._next_update
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {
ATTR_AFTER: self.after.astimezone(self.hass.config.time_zone).isoformat(),
@@ -173,20 +173,6 @@ def _calculate_initial_boudary_time(self):
self._time_before = before_event_date
- # We are calculating the _time_after value assuming that it will happen today
- # But that is not always true, e.g. after 23:00, before 12:00 and now is 10:00
- # If _time_before and _time_after are ahead of current_datetime:
- # _time_before is set to 12:00 next day
- # _time_after is set to 23:00 today
- # current_datetime is set to 10:00 today
- if (
- self._time_after > self.current_datetime
- and self._time_before > self.current_datetime + timedelta(days=1)
- ):
- # remove one day from _time_before and _time_after
- self._time_after -= timedelta(days=1)
- self._time_before -= timedelta(days=1)
-
# Add offset to utc boundaries according to the configuration
self._time_after += self._after_offset
self._time_before += self._before_offset
diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py
index 978e58c2500192..976462c95faaa2 100644
--- a/homeassistant/components/todoist/calendar.py
+++ b/homeassistant/components/todoist/calendar.py
@@ -226,15 +226,14 @@ def handle_new_task(call):
)
-def _parse_due_date(data: dict) -> datetime:
+def _parse_due_date(data: dict, gmt_string) -> datetime:
"""Parse the due date dict into a datetime object."""
# Add time information to date only strings.
if len(data["date"]) == 10:
data["date"] += "T00:00:00"
- # If there is no timezone provided, use UTC.
- if data["timezone"] is None:
- data["date"] += "Z"
- return dt.parse_datetime(data["date"])
+ if dt.parse_datetime(data["date"]).tzinfo is None:
+ data["date"] += gmt_string
+ return dt.as_utc(dt.parse_datetime(data["date"]))
class TodoistProjectDevice(CalendarEventDevice):
@@ -246,7 +245,7 @@ def __init__(
data,
labels,
token,
- latest_task_due_date=None,
+ due_date_days=None,
whitelisted_labels=None,
whitelisted_projects=None,
):
@@ -255,7 +254,7 @@ def __init__(
data,
labels,
token,
- latest_task_due_date,
+ due_date_days,
whitelisted_labels,
whitelisted_projects,
)
@@ -285,7 +284,7 @@ async def async_get_events(self, hass, start_date, end_date):
return await self.data.async_get_events(hass, start_date, end_date)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
if self.data.event is None:
# No tasks, we don't REALLY need to show anything.
@@ -338,7 +337,7 @@ def __init__(
project_data,
labels,
api,
- latest_task_due_date=None,
+ due_date_days=None,
whitelisted_labels=None,
whitelisted_projects=None,
):
@@ -356,12 +355,12 @@ def __init__(
self.all_project_tasks = []
- # The latest date a task can be due (for making lists of everything
+ # The days a task can be due (for making lists of everything
# due today, or everything due in the next week, for example).
- if latest_task_due_date is not None:
- self._latest_due_date = dt.utcnow() + timedelta(days=latest_task_due_date)
+ if due_date_days is not None:
+ self._due_date_days = timedelta(days=due_date_days)
else:
- self._latest_due_date = None
+ self._due_date_days = None
# Only tasks with one of these labels will be included.
if whitelisted_labels is not None:
@@ -407,10 +406,12 @@ def create_todoist_task(self, data):
# Generally speaking, that means right now.
task[START] = dt.utcnow()
if data[DUE] is not None:
- task[END] = _parse_due_date(data[DUE])
+ task[END] = _parse_due_date(
+ data[DUE], self._api.state["user"]["tz_info"]["gmt_string"]
+ )
- if self._latest_due_date is not None and (
- task[END] > self._latest_due_date
+ if self._due_date_days is not None and (
+ task[END] > dt.utcnow() + self._due_date_days
):
# This task is out of range of our due date;
# it shouldn't be counted.
@@ -430,7 +431,7 @@ def create_todoist_task(self, data):
else:
# If we ask for everything due before a certain date, don't count
# things which have no due dates.
- if self._latest_due_date is not None:
+ if self._due_date_days is not None:
return None
# Define values for tasks without due dates
@@ -529,9 +530,19 @@ async def async_get_events(self, hass, start_date, end_date):
for task in project_task_data:
if task["due"] is None:
continue
- due_date = _parse_due_date(task["due"])
+ due_date = _parse_due_date(
+ task["due"], self._api.state["user"]["tz_info"]["gmt_string"]
+ )
+ midnight = dt.as_utc(
+ dt.parse_datetime(
+ due_date.strftime("%Y-%m-%d")
+ + "T00:00:00"
+ + self._api.state["user"]["tz_info"]["gmt_string"]
+ )
+ )
+
if start_date < due_date < end_date:
- if due_date.hour == 0 and due_date.minute == 0:
+ if due_date == midnight:
# If the due date has no time data, return just the date so that it
# will render correctly as an all day event on a calendar.
due_date_value = due_date.strftime("%Y-%m-%d")
diff --git a/homeassistant/components/tof/sensor.py b/homeassistant/components/tof/sensor.py
index 17b2d1010bfb82..45713dd8f7710a 100644
--- a/homeassistant/components/tof/sensor.py
+++ b/homeassistant/components/tof/sensor.py
@@ -7,10 +7,9 @@
import voluptuous as vol
from homeassistant.components import rpi_gpio
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, LENGTH_MILLIMETERS
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
CONF_I2C_ADDRESS = "i2c_address"
CONF_I2C_BUS = "i2c_bus"
@@ -65,7 +64,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(dev, True)
-class VL53L1XSensor(Entity):
+class VL53L1XSensor(SensorEntity):
"""Implementation of VL53L1X sensor."""
def __init__(self, vl53l1x_sensor, name, unit, i2c_address):
diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py
index d1753ad4d026af..87c68b5addbe5c 100644
--- a/homeassistant/components/toon/__init__.py
+++ b/homeassistant/components/toon/__init__.py
@@ -15,7 +15,6 @@
EVENT_HOMEASSISTANT_STARTED,
)
from homeassistant.core import CoreState, HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session,
@@ -27,7 +26,7 @@
from .coordinator import ToonDataUpdateCoordinator
from .oauth2 import register_oauth2_implementations
-ENTITY_COMPONENTS = {
+PLATFORMS = {
BINARY_SENSOR_DOMAIN,
CLIMATE_DOMAIN,
SENSOR_DOMAIN,
@@ -98,10 +97,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.toon.activate_agreement(
agreement_id=entry.data[CONF_AGREEMENT_ID]
)
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
@@ -119,9 +115,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
# Spin up the platforms
- for component in ENTITY_COMPONENTS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
# If Home Assistant is already in a running state, register the webhook
@@ -146,8 +142,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unload_ok = all(
await asyncio.gather(
*(
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in ENTITY_COMPONENTS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
)
)
)
diff --git a/homeassistant/components/toon/binary_sensor.py b/homeassistant/components/toon/binary_sensor.py
index fe14435f2aba5c..6651806a21c7c5 100644
--- a/homeassistant/components/toon/binary_sensor.py
+++ b/homeassistant/components/toon/binary_sensor.py
@@ -1,5 +1,5 @@
"""Support for Toon binary sensors."""
-from typing import Optional
+from __future__ import annotations
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
@@ -84,7 +84,7 @@ def device_class(self) -> str:
return BINARY_SENSOR_ENTITIES[self.key][ATTR_DEVICE_CLASS]
@property
- def is_on(self) -> Optional[bool]:
+ def is_on(self) -> bool | None:
"""Return the status of the binary sensor."""
section = getattr(
self.coordinator.data, BINARY_SENSOR_ENTITIES[self.key][ATTR_SECTION]
diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py
index ba3ef8ee807f2e..db2bed47f51615 100644
--- a/homeassistant/components/toon/climate.py
+++ b/homeassistant/components/toon/climate.py
@@ -1,5 +1,7 @@
"""Support for Toon thermostat."""
-from typing import Any, Dict, List, Optional
+from __future__ import annotations
+
+from typing import Any
from toonapi import (
ACTIVE_STATE_AWAY,
@@ -61,12 +63,12 @@ def hvac_mode(self) -> str:
return HVAC_MODE_HEAT
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes."""
return [HVAC_MODE_HEAT]
@property
- def hvac_action(self) -> Optional[str]:
+ def hvac_action(self) -> str | None:
"""Return the current running hvac operation."""
if self.coordinator.data.thermostat.heating:
return CURRENT_HVAC_HEAT
@@ -78,7 +80,7 @@ def temperature_unit(self) -> str:
return TEMP_CELSIUS
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp."""
mapping = {
ACTIVE_STATE_AWAY: PRESET_AWAY,
@@ -89,17 +91,17 @@ def preset_mode(self) -> Optional[str]:
return mapping.get(self.coordinator.data.thermostat.active_state)
@property
- def preset_modes(self) -> List[str]:
+ def preset_modes(self) -> list[str]:
"""Return a list of available preset modes."""
return [PRESET_AWAY, PRESET_COMFORT, PRESET_HOME, PRESET_SLEEP]
@property
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self.coordinator.data.thermostat.current_display_temperature
@property
- def target_temperature(self) -> Optional[float]:
+ def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
return self.coordinator.data.thermostat.current_setpoint
@@ -114,7 +116,7 @@ def max_temp(self) -> float:
return DEFAULT_MAX_TEMP
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the current state of the burner."""
return {"heating_type": self.coordinator.data.agreement.heating_type}
diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py
index d1de68ef0b8bf9..bc673d2d181b57 100644
--- a/homeassistant/components/toon/config_flow.py
+++ b/homeassistant/components/toon/config_flow.py
@@ -1,6 +1,8 @@
"""Config flow to configure the Toon component."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict, List, Optional
+from typing import Any
from toonapi import Agreement, Toon, ToonError
import voluptuous as vol
@@ -19,15 +21,15 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
DOMAIN = DOMAIN
VERSION = 2
- agreements: Optional[List[Agreement]] = None
- data: Optional[Dict[str, Any]] = None
+ agreements: list[Agreement] | None = None
+ data: dict[str, Any] | None = None
@property
def logger(self) -> logging.Logger:
"""Return logger."""
return logging.getLogger(__name__)
- async def async_oauth_create_entry(self, data: Dict[str, Any]) -> Dict[str, Any]:
+ async def async_oauth_create_entry(self, data: dict[str, Any]) -> dict[str, Any]:
"""Test connection and load up agreements."""
self.data = data
@@ -46,8 +48,8 @@ async def async_oauth_create_entry(self, data: Dict[str, Any]) -> Dict[str, Any]
return await self.async_step_agreement()
async def async_step_import(
- self, config: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, config: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Start a configuration flow based on imported data.
This step is merely here to trigger "discovery" when the `toon`
@@ -56,7 +58,6 @@ async def async_step_import(
"""
if config is not None and CONF_MIGRATE in config:
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update({CONF_MIGRATE: config[CONF_MIGRATE]})
else:
await self._async_handle_discovery_without_unique_id()
@@ -64,8 +65,8 @@ async def async_step_import(
return await self.async_step_user()
async def async_step_agreement(
- self, user_input: Dict[str, Any] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] = None
+ ) -> dict[str, Any]:
"""Select Toon agreement to add."""
if len(self.agreements) == 1:
return await self._create_entry(self.agreements[0])
@@ -86,11 +87,8 @@ async def async_step_agreement(
agreement_index = agreements_list.index(user_input[CONF_AGREEMENT])
return await self._create_entry(self.agreements[agreement_index])
- async def _create_entry(self, agreement: Agreement) -> Dict[str, Any]:
- if ( # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
- CONF_MIGRATE in self.context
- ):
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
+ async def _create_entry(self, agreement: Agreement) -> dict[str, Any]:
+ if CONF_MIGRATE in self.context:
await self.hass.config_entries.async_remove(self.context[CONF_MIGRATE])
await self.async_set_unique_id(agreement.agreement_id)
diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py
index a1afaf9f9b0dec..d1a8b70243870b 100644
--- a/homeassistant/components/toon/const.py
+++ b/homeassistant/components/toon/const.py
@@ -5,7 +5,11 @@
DEVICE_CLASS_CONNECTIVITY,
DEVICE_CLASS_PROBLEM,
)
-from homeassistant.components.sensor import DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE
+from homeassistant.components.sensor import (
+ DEVICE_CLASS_ENERGY,
+ DEVICE_CLASS_POWER,
+ DEVICE_CLASS_TEMPERATURE,
+)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ICON,
@@ -46,7 +50,7 @@
ATTR_MEASUREMENT: "boiler_module_connected",
ATTR_INVERTED: False,
ATTR_DEVICE_CLASS: DEVICE_CLASS_CONNECTIVITY,
- ATTR_ICON: "mdi:check-network-outline",
+ ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: False,
},
"thermostat_info_burner_info_1": {
@@ -184,7 +188,7 @@
ATTR_MEASUREMENT: "average",
ATTR_UNIT_OF_MEASUREMENT: POWER_WATT,
ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER,
- ATTR_ICON: "mdi:power-plug",
+ ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: False,
},
"power_average_daily": {
@@ -192,8 +196,8 @@
ATTR_SECTION: "power_usage",
ATTR_MEASUREMENT: "day_average",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
- ATTR_DEVICE_CLASS: None,
- ATTR_ICON: "mdi:power-plug",
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: False,
},
"power_daily_cost": {
@@ -210,8 +214,8 @@
ATTR_SECTION: "power_usage",
ATTR_MEASUREMENT: "day_usage",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
- ATTR_DEVICE_CLASS: None,
- ATTR_ICON: "mdi:power-plug",
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: True,
},
"power_meter_reading": {
@@ -219,8 +223,8 @@
ATTR_SECTION: "power_usage",
ATTR_MEASUREMENT: "meter_high",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
- ATTR_DEVICE_CLASS: None,
- ATTR_ICON: "mdi:power-plug",
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: False,
},
"power_meter_reading_low": {
@@ -228,8 +232,8 @@
ATTR_SECTION: "power_usage",
ATTR_MEASUREMENT: "meter_low",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
- ATTR_DEVICE_CLASS: None,
- ATTR_ICON: "mdi:power-plug",
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: False,
},
"power_value": {
@@ -238,7 +242,7 @@
ATTR_MEASUREMENT: "current",
ATTR_UNIT_OF_MEASUREMENT: POWER_WATT,
ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER,
- ATTR_ICON: "mdi:power-plug",
+ ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: True,
},
"solar_meter_reading_produced": {
@@ -246,8 +250,8 @@
ATTR_SECTION: "power_usage",
ATTR_MEASUREMENT: "meter_produced_high",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
- ATTR_DEVICE_CLASS: None,
- ATTR_ICON: "mdi:power-plug",
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: False,
},
"solar_meter_reading_low_produced": {
@@ -255,8 +259,8 @@
ATTR_SECTION: "power_usage",
ATTR_MEASUREMENT: "meter_produced_low",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
- ATTR_DEVICE_CLASS: None,
- ATTR_ICON: "mdi:power-plug",
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: False,
},
"solar_value": {
@@ -265,7 +269,7 @@
ATTR_MEASUREMENT: "current_solar",
ATTR_UNIT_OF_MEASUREMENT: POWER_WATT,
ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER,
- ATTR_ICON: "mdi:solar-power",
+ ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: True,
},
"solar_maximum": {
@@ -273,8 +277,8 @@
ATTR_SECTION: "power_usage",
ATTR_MEASUREMENT: "day_max_solar",
ATTR_UNIT_OF_MEASUREMENT: POWER_WATT,
- ATTR_DEVICE_CLASS: None,
- ATTR_ICON: "mdi:solar-power",
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER,
+ ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: True,
},
"solar_produced": {
@@ -283,7 +287,7 @@
ATTR_MEASUREMENT: "current_produced",
ATTR_UNIT_OF_MEASUREMENT: POWER_WATT,
ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER,
- ATTR_ICON: "mdi:solar-power",
+ ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: True,
},
"power_usage_day_produced_solar": {
@@ -291,8 +295,8 @@
ATTR_SECTION: "power_usage",
ATTR_MEASUREMENT: "day_produced_solar",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
- ATTR_DEVICE_CLASS: None,
- ATTR_ICON: "mdi:solar-power",
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: True,
},
"power_usage_day_to_grid_usage": {
@@ -300,8 +304,8 @@
ATTR_SECTION: "power_usage",
ATTR_MEASUREMENT: "day_to_grid_usage",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
- ATTR_DEVICE_CLASS: None,
- ATTR_ICON: "mdi:solar-power",
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: False,
},
"power_usage_day_from_grid_usage": {
@@ -309,8 +313,8 @@
ATTR_SECTION: "power_usage",
ATTR_MEASUREMENT: "day_from_grid_usage",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
- ATTR_DEVICE_CLASS: None,
- ATTR_ICON: "mdi:power-plug",
+ ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: False,
},
"solar_average_produced": {
@@ -319,7 +323,7 @@
ATTR_MEASUREMENT: "average_produced",
ATTR_UNIT_OF_MEASUREMENT: POWER_WATT,
ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER,
- ATTR_ICON: "mdi:solar-power",
+ ATTR_ICON: None,
ATTR_DEFAULT_ENABLED: False,
},
"thermostat_info_current_modulation_level": {
diff --git a/homeassistant/components/toon/coordinator.py b/homeassistant/components/toon/coordinator.py
index 359cb5b0ffb098..069bd58d922e42 100644
--- a/homeassistant/components/toon/coordinator.py
+++ b/homeassistant/components/toon/coordinator.py
@@ -1,7 +1,8 @@
"""Provides the Toon DataUpdateCoordinator."""
+from __future__ import annotations
+
import logging
import secrets
-from typing import Optional
from toonapi import Status, Toon, ToonError
@@ -50,7 +51,7 @@ def update_listeners(self) -> None:
for update_callback in self._listeners:
update_callback()
- async def register_webhook(self, event: Optional[Event] = None) -> None:
+ async def register_webhook(self, event: Event | None = None) -> None:
"""Register a webhook with Toon to get live updates."""
if CONF_WEBHOOK_ID not in self.entry.data:
data = {**self.entry.data, CONF_WEBHOOK_ID: secrets.token_hex()}
@@ -124,7 +125,7 @@ async def handle_webhook(
except ToonError as err:
_LOGGER.error("Could not process data received from Toon webhook - %s", err)
- async def unregister_webhook(self, event: Optional[Event] = None) -> None:
+ async def unregister_webhook(self, event: Event | None = None) -> None:
"""Remove / Unregister webhook for toon."""
_LOGGER.debug(
"Unregistering Toon webhook (%s)", self.entry.data[CONF_WEBHOOK_ID]
diff --git a/homeassistant/components/toon/models.py b/homeassistant/components/toon/models.py
index edcbed369a387e..8aee2fe27e1660 100644
--- a/homeassistant/components/toon/models.py
+++ b/homeassistant/components/toon/models.py
@@ -1,5 +1,7 @@
"""DataUpdate Coordinator, and base Entity and Device models for Toon."""
-from typing import Any, Dict, Optional
+from __future__ import annotations
+
+from typing import Any
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -31,7 +33,7 @@ def name(self) -> str:
return self._name
@property
- def icon(self) -> Optional[str]:
+ def icon(self) -> str | None:
"""Return the mdi icon of the entity."""
return self._icon
@@ -45,7 +47,7 @@ class ToonDisplayDeviceEntity(ToonEntity):
"""Defines a Toon display device entity."""
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about this thermostat."""
agreement = self.coordinator.data.agreement
model = agreement.display_hardware_version.rpartition("/")[0]
@@ -63,7 +65,7 @@ class ToonElectricityMeterDeviceEntity(ToonEntity):
"""Defines a Electricity Meter device entity."""
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about this entity."""
agreement_id = self.coordinator.data.agreement.agreement_id
return {
@@ -77,7 +79,7 @@ class ToonGasMeterDeviceEntity(ToonEntity):
"""Defines a Gas Meter device entity."""
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about this entity."""
agreement_id = self.coordinator.data.agreement.agreement_id
return {
@@ -91,7 +93,7 @@ class ToonWaterMeterDeviceEntity(ToonEntity):
"""Defines a Water Meter device entity."""
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about this entity."""
agreement_id = self.coordinator.data.agreement.agreement_id
return {
@@ -105,7 +107,7 @@ class ToonSolarDeviceEntity(ToonEntity):
"""Defines a Solar Device device entity."""
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about this entity."""
agreement_id = self.coordinator.data.agreement.agreement_id
return {
@@ -119,7 +121,7 @@ class ToonBoilerModuleDeviceEntity(ToonEntity):
"""Defines a Boiler Module device entity."""
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about this entity."""
agreement_id = self.coordinator.data.agreement.agreement_id
return {
@@ -134,7 +136,7 @@ class ToonBoilerDeviceEntity(ToonEntity):
"""Defines a Boiler device entity."""
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about this entity."""
agreement_id = self.coordinator.data.agreement.agreement_id
return {
diff --git a/homeassistant/components/toon/oauth2.py b/homeassistant/components/toon/oauth2.py
index e3a83583ac6f2e..7539224ebba2b2 100644
--- a/homeassistant/components/toon/oauth2.py
+++ b/homeassistant/components/toon/oauth2.py
@@ -1,5 +1,7 @@
"""OAuth2 implementations for Toon."""
-from typing import Any, Optional, cast
+from __future__ import annotations
+
+from typing import Any, cast
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow
@@ -55,7 +57,7 @@ def __init__(
client_secret: str,
name: str,
tenant_id: str,
- issuer: Optional[str] = None,
+ issuer: str | None = None,
):
"""Local Toon Oauth Implementation."""
self._name = name
diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py
index 52d3a68f2c1b8e..36f5dedde3d3f4 100644
--- a/homeassistant/components/toon/sensor.py
+++ b/homeassistant/components/toon/sensor.py
@@ -1,6 +1,7 @@
"""Support for Toon sensors."""
-from typing import Optional
+from __future__ import annotations
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -109,7 +110,7 @@ async def async_setup_entry(
async_add_entities(sensors, True)
-class ToonSensor(ToonEntity):
+class ToonSensor(ToonEntity, SensorEntity):
"""Defines a Toon sensor."""
def __init__(self, coordinator: ToonDataUpdateCoordinator, *, key: str) -> None:
@@ -132,7 +133,7 @@ def unique_id(self) -> str:
return f"{DOMAIN}_{agreement_id}_sensor_{self.key}"
@property
- def state(self) -> Optional[str]:
+ def state(self) -> str | None:
"""Return the state of the sensor."""
section = getattr(
self.coordinator.data, SENSOR_ENTITIES[self.key][ATTR_SECTION]
@@ -140,12 +141,12 @@ def state(self) -> Optional[str]:
return getattr(section, SENSOR_ENTITIES[self.key][ATTR_MEASUREMENT])
@property
- def unit_of_measurement(self) -> Optional[str]:
+ def unit_of_measurement(self) -> str | None:
"""Return the unit this state is expressed in."""
return SENSOR_ENTITIES[self.key][ATTR_UNIT_OF_MEASUREMENT]
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the device class."""
return SENSOR_ENTITIES[self.key][ATTR_DEVICE_CLASS]
diff --git a/homeassistant/components/toon/services.yaml b/homeassistant/components/toon/services.yaml
index 7afedeb4bf6696..909018f820bea7 100644
--- a/homeassistant/components/toon/services.yaml
+++ b/homeassistant/components/toon/services.yaml
@@ -1,6 +1,11 @@
update:
+ name: Update
description: Update all entities with fresh data from Toon
fields:
display:
+ name: Display
description: Toon display to update (optional)
+ advanced: true
example: eneco-001-123456
+ selector:
+ text:
diff --git a/homeassistant/components/toon/translations/de.json b/homeassistant/components/toon/translations/de.json
index d9060a719d8780..c04f3a5f4bb4ab 100644
--- a/homeassistant/components/toon/translations/de.json
+++ b/homeassistant/components/toon/translations/de.json
@@ -1,7 +1,10 @@
{
"config": {
"abort": {
+ "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.",
+ "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.",
"no_agreements": "Dieses Konto hat keine Toon-Anzeigen.",
+ "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).",
"unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten"
}
}
diff --git a/homeassistant/components/toon/translations/en.json b/homeassistant/components/toon/translations/en.json
index c64913cfb6c1c3..3351c16d8d8d63 100644
--- a/homeassistant/components/toon/translations/en.json
+++ b/homeassistant/components/toon/translations/en.json
@@ -7,7 +7,7 @@
"missing_configuration": "The component is not configured. Please follow the documentation.",
"no_agreements": "This account has no Toon displays.",
"no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})",
- "unknown_authorize_url_generation": "Unknown error generating an authorize url."
+ "unknown_authorize_url_generation": "Unknown error generating an authorize URL."
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/et.json b/homeassistant/components/toon/translations/et.json
index 7b70eae433e8a7..f93fb684b25726 100644
--- a/homeassistant/components/toon/translations/et.json
+++ b/homeassistant/components/toon/translations/et.json
@@ -18,7 +18,7 @@
"title": "Vali oma leping"
},
"pick_implementation": {
- "title": "Valige oma rentnik, kellega autentida"
+ "title": "Vali oma rentnik, kellega autentida"
}
}
}
diff --git a/homeassistant/components/toon/translations/fr.json b/homeassistant/components/toon/translations/fr.json
index caeed852d0a0e9..3fa6059a58f2c7 100644
--- a/homeassistant/components/toon/translations/fr.json
+++ b/homeassistant/components/toon/translations/fr.json
@@ -6,7 +6,8 @@
"authorize_url_timeout": "Timout de g\u00e9n\u00e9ration de l'URL d'autorisation.",
"missing_configuration": "The composant n'est pas configur\u00e9. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation.",
"no_agreements": "Ce compte n'a pas d'affichages Toon.",
- "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )"
+ "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )",
+ "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation."
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/hu.json b/homeassistant/components/toon/translations/hu.json
new file mode 100644
index 00000000000000..cd832522870c8c
--- /dev/null
+++ b/homeassistant/components/toon/translations/hu.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.",
+ "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.",
+ "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.",
+ "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/toon/translations/id.json b/homeassistant/components/toon/translations/id.json
new file mode 100644
index 00000000000000..6e9d4a76683ad0
--- /dev/null
+++ b/homeassistant/components/toon/translations/id.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perjanjian yang dipilih sudah dikonfigurasi.",
+ "authorize_url_fail": "Kesalahan tidak dikenal terjadi ketika menghasilkan URL otorisasi.",
+ "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.",
+ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.",
+ "no_agreements": "Akun ini tidak memiliki tampilan Toon.",
+ "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})",
+ "unknown_authorize_url_generation": "Kesalahan tidak dikenal ketika menghasilkan URL otorisasi."
+ },
+ "step": {
+ "agreement": {
+ "data": {
+ "agreement": "Persetujuan"
+ },
+ "description": "Pilih alamat persetujuan yang ingin ditambahkan.",
+ "title": "Pilih persetujuan Anda"
+ },
+ "pick_implementation": {
+ "title": "Pilih penyewa Anda untuk diautentikasi"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/toon/translations/ko.json b/homeassistant/components/toon/translations/ko.json
index 379058f68d1109..e36adba2ffb560 100644
--- a/homeassistant/components/toon/translations/ko.json
+++ b/homeassistant/components/toon/translations/ko.json
@@ -2,11 +2,12 @@
"config": {
"abort": {
"already_configured": "\uc120\ud0dd\ub41c \uc57d\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
+ "authorize_url_fail": "\uc778\uc99d URL\uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
"authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
"no_agreements": "\uc774 \uacc4\uc815\uc5d0\ub294 Toon \ub514\uc2a4\ud50c\ub808\uc774\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.",
- "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})"
+ "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
+ "unknown_authorize_url_generation": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4."
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/lb.json b/homeassistant/components/toon/translations/lb.json
index 6491c66673863c..e21dfb0c996c24 100644
--- a/homeassistant/components/toon/translations/lb.json
+++ b/homeassistant/components/toon/translations/lb.json
@@ -6,7 +6,8 @@
"authorize_url_timeout": "Z\u00e4itiwwerschraidung beim erstellen vun der Autorisatioun's URL.",
"missing_configuration": "Komponent ass net konfigur\u00e9iert. Folleg der Dokumentatioun.",
"no_agreements": "D\u00ebse Kont huet keen Toon Ecran.",
- "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})"
+ "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})",
+ "unknown_authorize_url_generation": "Onbekannte Feeler beim erstellen vun der Authorisatiouns URL."
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/nl.json b/homeassistant/components/toon/translations/nl.json
index 4f63d7d09dacbe..687efce4a4235c 100644
--- a/homeassistant/components/toon/translations/nl.json
+++ b/homeassistant/components/toon/translations/nl.json
@@ -3,8 +3,23 @@
"abort": {
"already_configured": "De geselecteerde overeenkomst is al geconfigureerd.",
"authorize_url_fail": "Onbekende fout bij het genereren van een autorisatie-URL.",
+ "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
+ "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.",
"no_agreements": "Dit account heeft geen Toon schermen.",
- "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout [check the help section] ( {docs_url} )"
+ "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout [check the help section] ( {docs_url} )",
+ "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL."
+ },
+ "step": {
+ "agreement": {
+ "data": {
+ "agreement": "Overeenkomst"
+ },
+ "description": "Selecteer het overeenkomstadres dat u wilt toevoegen.",
+ "title": "Kies uw overeenkomst"
+ },
+ "pick_implementation": {
+ "title": "Kies uw leverancier om mee te authenticeren"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/toon/translations/no.json b/homeassistant/components/toon/translations/no.json
index a64a64ab74e173..41246c42f0eed7 100644
--- a/homeassistant/components/toon/translations/no.json
+++ b/homeassistant/components/toon/translations/no.json
@@ -2,12 +2,12 @@
"config": {
"abort": {
"already_configured": "Den valgte avtalen er allerede konfigurert.",
- "authorize_url_fail": "Ukjent feil ved generering av godkjenningsadresse",
+ "authorize_url_fail": "Ukjent feil under generering av en autoriserings-URL.",
"authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse",
"missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen",
"no_agreements": "Denne kontoen har ingen Toon skjermer.",
"no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})",
- "unknown_authorize_url_generation": "Ukjent feil ved generering av godkjenningsadresse"
+ "unknown_authorize_url_generation": "Ukjent feil under generering av en autoriserings-URL."
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/tr.json b/homeassistant/components/toon/translations/tr.json
new file mode 100644
index 00000000000000..97765a99a7f49c
--- /dev/null
+++ b/homeassistant/components/toon/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Se\u00e7ilen anla\u015fma zaten yap\u0131land\u0131r\u0131lm\u0131\u015f.",
+ "unknown_authorize_url_generation": "Yetkilendirme url'si olu\u015fturulurken bilinmeyen hata."
+ },
+ "step": {
+ "agreement": {
+ "data": {
+ "agreement": "Anla\u015fma"
+ },
+ "description": "Eklemek istedi\u011finiz anla\u015fma adresini se\u00e7in.",
+ "title": "Anla\u015fman\u0131z\u0131 se\u00e7in"
+ },
+ "pick_implementation": {
+ "title": "Kimlik do\u011frulamak i\u00e7in kirac\u0131n\u0131z\u0131 se\u00e7in"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/toon/translations/uk.json b/homeassistant/components/toon/translations/uk.json
new file mode 100644
index 00000000000000..51aa28f3984b04
--- /dev/null
+++ b/homeassistant/components/toon/translations/uk.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041e\u0431\u0440\u0430\u043d\u0430 \u0443\u0433\u043e\u0434\u0430 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430.",
+ "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.",
+ "no_agreements": "\u0423 \u0446\u044c\u043e\u043c\u0443 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u0456 \u043d\u0435\u043c\u0430\u0454 \u0434\u0438\u0441\u043f\u043b\u0435\u0457\u0432 Toon.",
+ "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.",
+ "unknown_authorize_url_generation": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457."
+ },
+ "step": {
+ "agreement": {
+ "data": {
+ "agreement": "\u0423\u0433\u043e\u0434\u0430"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u0443\u0433\u043e\u0434\u0438, \u044f\u043a\u0443 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438.",
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0412\u0430\u0448\u0443 \u0443\u0433\u043e\u0434\u0443"
+ },
+ "pick_implementation": {
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0440\u0435\u043d\u0434\u0430\u0440\u044f \u0434\u043b\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py
index 4b52a565d873e2..156259adccb831 100644
--- a/homeassistant/components/torque/sensor.py
+++ b/homeassistant/components/torque/sensor.py
@@ -4,11 +4,10 @@
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_EMAIL, CONF_NAME, DEGREE
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
API_PATH = "/api/torque"
@@ -106,7 +105,7 @@ def get(self, request):
return "OK!"
-class TorqueSensor(Entity):
+class TorqueSensor(SensorEntity):
"""Representation of a Torque sensor."""
def __init__(self, name, unit):
diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py
index cf3f059cfb9ce6..8ef223c49a5f8f 100644
--- a/homeassistant/components/totalconnect/__init__.py
+++ b/homeassistant/components/totalconnect/__init__.py
@@ -5,67 +5,86 @@
from total_connect_client import TotalConnectClient
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
-from .const import DOMAIN
+from .const import CONF_USERCODES, DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["alarm_control_panel", "binary_sensor"]
CONFIG_SCHEMA = vol.Schema(
- {
- DOMAIN: vol.Schema(
- {
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- }
- )
- },
+ vol.All(
+ cv.deprecated(DOMAIN),
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ }
+ )
+ },
+ ),
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up by configuration file."""
- if DOMAIN not in config:
- return True
-
- hass.async_create_task(
- hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": SOURCE_IMPORT},
- data=config[DOMAIN],
- )
- )
+ hass.data.setdefault(DOMAIN, {})
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up upon config entry in user interface."""
- hass.data.setdefault(DOMAIN, {})
-
conf = entry.data
username = conf[CONF_USERNAME]
password = conf[CONF_PASSWORD]
+ if CONF_USERCODES not in conf:
+ _LOGGER.warning("No usercodes in TotalConnect configuration")
+ # should only happen for those who used UI before we added usercodes
+ await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={
+ "source": SOURCE_REAUTH,
+ },
+ data=conf,
+ )
+ return False
+
+ temp_codes = conf[CONF_USERCODES]
+ usercodes = {}
+ for code in temp_codes:
+ usercodes[int(code)] = temp_codes[code]
+
client = await hass.async_add_executor_job(
- TotalConnectClient.TotalConnectClient, username, password
+ TotalConnectClient.TotalConnectClient, username, password, usercodes
)
if not client.is_valid_credentials():
_LOGGER.error("TotalConnect authentication failed")
+ await hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={
+ "source": SOURCE_REAUTH,
+ },
+ data=conf,
+ )
+ )
+
return False
hass.data[DOMAIN][entry.entry_id] = client
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py
index d7c17a1ccff2af..c277198b683e44 100644
--- a/homeassistant/components/totalconnect/alarm_control_panel.py
+++ b/homeassistant/components/totalconnect/alarm_control_panel.py
@@ -21,8 +21,6 @@
from .const import DOMAIN
-_LOGGER = logging.getLogger(__name__)
-
async def async_setup_entry(hass, entry, async_add_entities) -> None:
"""Set up TotalConnect alarm panels based on a config entry."""
@@ -46,7 +44,7 @@ def __init__(self, name, location_id, client):
self._location_id = location_id
self._client = client
self._state = None
- self._device_state_attributes = {}
+ self._extra_state_attributes = {}
@property
def name(self):
@@ -64,9 +62,9 @@ def supported_features(self) -> int:
return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
- return self._device_state_attributes
+ return self._extra_state_attributes
def update(self):
"""Return the state of the device."""
@@ -111,7 +109,7 @@ def update(self):
state = None
self._state = state
- self._device_state_attributes = attr
+ self._extra_state_attributes = attr
def alarm_disarm(self, code=None):
"""Send disarm command."""
diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py
index e296b12fa59d4d..ef02c5d1fd3384 100644
--- a/homeassistant/components/totalconnect/binary_sensor.py
+++ b/homeassistant/components/totalconnect/binary_sensor.py
@@ -1,6 +1,4 @@
"""Interfaces with TotalConnect sensors."""
-import logging
-
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_DOOR,
DEVICE_CLASS_GAS,
@@ -10,8 +8,6 @@
from .const import DOMAIN
-_LOGGER = logging.getLogger(__name__)
-
async def async_setup_entry(hass, entry, async_add_entities) -> None:
"""Set up TotalConnect device sensors based on a config entry."""
@@ -77,7 +73,7 @@ def device_class(self):
return None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attributes = {
"zone_id": self._zone_id,
diff --git a/homeassistant/components/totalconnect/config_flow.py b/homeassistant/components/totalconnect/config_flow.py
index 2608a3c812cdbd..122b1ad88b4dd3 100644
--- a/homeassistant/components/totalconnect/config_flow.py
+++ b/homeassistant/components/totalconnect/config_flow.py
@@ -5,7 +5,11 @@
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import CONF_USERCODES, DOMAIN
+
+CONF_LOCATION = "location"
+
+PASSWORD_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@@ -13,6 +17,13 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
+ def __init__(self):
+ """Initialize the config flow."""
+ self.username = None
+ self.password = None
+ self.usercodes = {}
+ self.client = None
+
async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
errors = {}
@@ -25,14 +36,16 @@ async def async_step_user(self, user_input=None):
await self.async_set_unique_id(username)
self._abort_if_unique_id_configured()
- valid = await self.is_valid(username, password)
+ client = await self.hass.async_add_executor_job(
+ TotalConnectClient.TotalConnectClient, username, password, None
+ )
- if valid:
- # authentication success / valid
- return self.async_create_entry(
- title="Total Connect",
- data={CONF_USERNAME: username, CONF_PASSWORD: password},
- )
+ if client.is_valid_credentials():
+ # username/password valid so show user locations
+ self.username = username
+ self.password = password
+ self.client = client
+ return await self.async_step_locations()
# authentication failed / invalid
errors["base"] = "invalid_auth"
@@ -44,13 +57,101 @@ async def async_step_user(self, user_input=None):
step_id="user", data_schema=data_schema, errors=errors
)
- async def async_step_import(self, user_input):
- """Import a config entry."""
- return await self.async_step_user(user_input)
+ async def async_step_locations(self, user_entry=None):
+ """Handle the user locations and associated usercodes."""
+ errors = {}
+ if user_entry is not None:
+ for location_id in self.usercodes:
+ if self.usercodes[location_id] is None:
+ valid = await self.hass.async_add_executor_job(
+ self.client.locations[location_id].set_usercode,
+ user_entry[CONF_LOCATION],
+ )
+ if valid:
+ self.usercodes[location_id] = user_entry[CONF_LOCATION]
+ else:
+ errors[CONF_LOCATION] = "usercode"
+ break
+
+ complete = True
+ for location_id in self.usercodes:
+ if self.usercodes[location_id] is None:
+ complete = False
+
+ if not errors and complete:
+ return self.async_create_entry(
+ title="Total Connect",
+ data={
+ CONF_USERNAME: self.username,
+ CONF_PASSWORD: self.password,
+ CONF_USERCODES: self.usercodes,
+ },
+ )
+ else:
+ for location_id in self.client.locations:
+ self.usercodes[location_id] = None
+
+ # show the next location that needs a usercode
+ location_codes = {}
+ for location_id in self.usercodes:
+ if self.usercodes[location_id] is None:
+ location_codes[
+ vol.Required(
+ CONF_LOCATION,
+ default=location_id,
+ )
+ ] = str
+ break
+
+ data_schema = vol.Schema(location_codes)
+ return self.async_show_form(
+ step_id="locations",
+ data_schema=data_schema,
+ errors=errors,
+ description_placeholders={"base": "description"},
+ )
+
+ async def async_step_reauth(self, config):
+ """Perform reauth upon an authentication error or no usercode."""
+ self.username = config[CONF_USERNAME]
+ self.usercodes = config[CONF_USERCODES]
+
+ return await self.async_step_reauth_confirm()
+
+ async def async_step_reauth_confirm(self, user_input=None):
+ """Dialog that informs the user that reauth is required."""
+ errors = {}
+ if user_input is None:
+ return self.async_show_form(
+ step_id="reauth_confirm",
+ data_schema=PASSWORD_DATA_SCHEMA,
+ )
- async def is_valid(self, username="", password=""):
- """Return true if the given username and password are valid."""
client = await self.hass.async_add_executor_job(
- TotalConnectClient.TotalConnectClient, username, password
+ TotalConnectClient.TotalConnectClient,
+ self.username,
+ user_input[CONF_PASSWORD],
+ self.usercodes,
)
- return client.is_valid_credentials()
+
+ if not client.is_valid_credentials():
+ errors["base"] = "invalid_auth"
+ return self.async_show_form(
+ step_id="reauth_confirm",
+ errors=errors,
+ data_schema=PASSWORD_DATA_SCHEMA,
+ )
+
+ existing_entry = await self.async_set_unique_id(self.username)
+ new_entry = {
+ CONF_USERNAME: self.username,
+ CONF_PASSWORD: user_input[CONF_PASSWORD],
+ CONF_USERCODES: self.usercodes,
+ }
+ self.hass.config_entries.async_update_entry(existing_entry, data=new_entry)
+
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(existing_entry.entry_id)
+ )
+
+ return self.async_abort(reason="reauth_successful")
diff --git a/homeassistant/components/totalconnect/const.py b/homeassistant/components/totalconnect/const.py
index 6c19bf0a217a22..22ecd14281f3fd 100644
--- a/homeassistant/components/totalconnect/const.py
+++ b/homeassistant/components/totalconnect/const.py
@@ -1,3 +1,8 @@
"""TotalConnect constants."""
DOMAIN = "totalconnect"
+CONF_USERCODES = "usercodes"
+CONF_LOCATION = "location"
+
+# Most TotalConnect alarms will work passing '-1' as usercode
+DEFAULT_USERCODE = "-1"
diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json
index 4ec632f45779e9..8a42ca99f035df 100644
--- a/homeassistant/components/totalconnect/manifest.json
+++ b/homeassistant/components/totalconnect/manifest.json
@@ -1,8 +1,9 @@
{
"domain": "totalconnect",
- "name": "Honeywell Total Connect Alarm",
+ "name": "Total Connect",
"documentation": "https://www.home-assistant.io/integrations/totalconnect",
- "requirements": ["total_connect_client==0.55"],
+ "requirements": ["total_connect_client==0.57"],
+ "dependencies": [],
"codeowners": ["@austinmroczek"],
"config_flow": true
}
diff --git a/homeassistant/components/totalconnect/strings.json b/homeassistant/components/totalconnect/strings.json
index 7b306554b7be6f..f284e4b86da7f0 100644
--- a/homeassistant/components/totalconnect/strings.json
+++ b/homeassistant/components/totalconnect/strings.json
@@ -7,13 +7,26 @@
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
+ },
+ "locations": {
+ "title": "Location Usercodes",
+ "description": "Enter the usercode for this user at this location",
+ "data": {
+ "location": "[%key:common::config_flow::data::location%]"
+ }
+ },
+ "reauth_confirm": {
+ "title": "[%key:common::config_flow::title::reauth%]",
+ "description": "Total Connect needs to re-authenticate your account"
}
},
"error": {
- "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "usercode": "Usercode not valid for this user at this location"
},
"abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
}
}
diff --git a/homeassistant/components/totalconnect/translations/ca.json b/homeassistant/components/totalconnect/translations/ca.json
index 25dafcf7d21dbb..ce055082a2171e 100644
--- a/homeassistant/components/totalconnect/translations/ca.json
+++ b/homeassistant/components/totalconnect/translations/ca.json
@@ -1,12 +1,25 @@
{
"config": {
"abort": {
- "already_configured": "El compte ja ha estat configurat"
+ "already_configured": "El compte ja ha estat configurat",
+ "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament"
},
"error": {
- "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida"
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "usercode": "El codi d'usuari no \u00e9s v\u00e0lid per a aquest usuari en aquesta ubicaci\u00f3"
},
"step": {
+ "locations": {
+ "data": {
+ "location": "Ubicaci\u00f3"
+ },
+ "description": "Introdueix el codi d'usuari de l'usuari en aquesta ubicaci\u00f3",
+ "title": "Codis d'usuari d'ubicaci\u00f3"
+ },
+ "reauth_confirm": {
+ "description": "Total Connect ha de tornar a autenticar-se amb el teu compte",
+ "title": "Reautenticaci\u00f3 de la integraci\u00f3"
+ },
"user": {
"data": {
"password": "Contrasenya",
diff --git a/homeassistant/components/totalconnect/translations/cs.json b/homeassistant/components/totalconnect/translations/cs.json
index 60e2196b38761b..74dece0c54ef6d 100644
--- a/homeassistant/components/totalconnect/translations/cs.json
+++ b/homeassistant/components/totalconnect/translations/cs.json
@@ -1,12 +1,18 @@
{
"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": {
"invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed"
},
"step": {
+ "locations": {
+ "data": {
+ "location": "Um\u00edst\u011bn\u00ed"
+ }
+ },
"user": {
"data": {
"password": "Heslo",
diff --git a/homeassistant/components/totalconnect/translations/de.json b/homeassistant/components/totalconnect/translations/de.json
index 25069635cca458..57dbaf77364eae 100644
--- a/homeassistant/components/totalconnect/translations/de.json
+++ b/homeassistant/components/totalconnect/translations/de.json
@@ -1,9 +1,22 @@
{
"config": {
"abort": {
- "already_configured": "Konto bereits konfiguriert"
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
+ },
+ "error": {
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
},
"step": {
+ "locations": {
+ "data": {
+ "location": "Standort"
+ }
+ },
+ "reauth_confirm": {
+ "description": "Total Connect muss dein Konto neu authentifizieren",
+ "title": "Integration erneut authentifizieren"
+ },
"user": {
"data": {
"password": "Passwort",
diff --git a/homeassistant/components/totalconnect/translations/en.json b/homeassistant/components/totalconnect/translations/en.json
index f02a3eadf9ca9b..5071e623701353 100644
--- a/homeassistant/components/totalconnect/translations/en.json
+++ b/homeassistant/components/totalconnect/translations/en.json
@@ -1,12 +1,25 @@
{
"config": {
"abort": {
- "already_configured": "Account is already configured"
+ "already_configured": "Account is already configured",
+ "reauth_successful": "Re-authentication was successful"
},
"error": {
- "invalid_auth": "Invalid authentication"
+ "invalid_auth": "Invalid authentication",
+ "usercode": "Usercode not valid for this user at this location"
},
"step": {
+ "locations": {
+ "data": {
+ "location": "Location"
+ },
+ "description": "Enter the usercode for this user at this location",
+ "title": "Location Usercodes"
+ },
+ "reauth_confirm": {
+ "description": "Total Connect needs to re-authenticate your account",
+ "title": "Reauthenticate Integration"
+ },
"user": {
"data": {
"password": "Password",
diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json
index 48af1bed0f4a5b..85797fa901ee1a 100644
--- a/homeassistant/components/totalconnect/translations/es.json
+++ b/homeassistant/components/totalconnect/translations/es.json
@@ -4,9 +4,20 @@
"already_configured": "La cuenta ya ha sido configurada"
},
"error": {
- "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida"
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
+ "usercode": "El c\u00f3digo de usuario no es v\u00e1lido para este usuario en esta ubicaci\u00f3n"
},
"step": {
+ "locations": {
+ "data": {
+ "location": "Localizaci\u00f3n"
+ },
+ "description": "Ingrese el c\u00f3digo de usuario para este usuario en esta ubicaci\u00f3n",
+ "title": "C\u00f3digos de usuario de ubicaci\u00f3n"
+ },
+ "reauth_confirm": {
+ "description": "Total Connect necesita volver a autentificar tu cuenta"
+ },
"user": {
"data": {
"password": "Contrase\u00f1a",
diff --git a/homeassistant/components/totalconnect/translations/et.json b/homeassistant/components/totalconnect/translations/et.json
index 2940b7a9e65b82..3f1a15fe139925 100644
--- a/homeassistant/components/totalconnect/translations/et.json
+++ b/homeassistant/components/totalconnect/translations/et.json
@@ -1,12 +1,25 @@
{
"config": {
"abort": {
- "already_configured": "Konto on juba seadistatud"
+ "already_configured": "Konto on juba seadistatud",
+ "reauth_successful": "Taastuvastamine \u00f5nnestus"
},
"error": {
- "invalid_auth": "Tuvastamise viga"
+ "invalid_auth": "Tuvastamise viga",
+ "usercode": "Kasutajakood ei sobi selle kasutaja jaoks selles asukohas"
},
"step": {
+ "locations": {
+ "data": {
+ "location": "Asukoht"
+ },
+ "description": "Sisesta selle kasutaja kood selles asukohas",
+ "title": "Asukoha kasutajakoodid"
+ },
+ "reauth_confirm": {
+ "description": "Total Connect peab konto uuesti autentima",
+ "title": "Taastuvasta sidumine"
+ },
"user": {
"data": {
"password": "Salas\u00f5na",
diff --git a/homeassistant/components/totalconnect/translations/fr.json b/homeassistant/components/totalconnect/translations/fr.json
index 6526d0a98001c4..b46bf12796347a 100644
--- a/homeassistant/components/totalconnect/translations/fr.json
+++ b/homeassistant/components/totalconnect/translations/fr.json
@@ -1,12 +1,25 @@
{
"config": {
"abort": {
- "already_configured": "Compte d\u00e9j\u00e0 configur\u00e9"
+ "already_configured": "Compte d\u00e9j\u00e0 configur\u00e9",
+ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi"
},
"error": {
- "invalid_auth": "Authentification invalide"
+ "invalid_auth": "Authentification invalide",
+ "usercode": "Code d'utilisateur non valide pour cet utilisateur \u00e0 cet emplacement"
},
"step": {
+ "locations": {
+ "data": {
+ "location": "Emplacement"
+ },
+ "description": "Saisissez le code d'utilisateur de cet utilisateur \u00e0 cet emplacement",
+ "title": "Codes d'utilisateur de l'emplacement"
+ },
+ "reauth_confirm": {
+ "description": "Total Connect doit r\u00e9-authentifier votre compte",
+ "title": "R\u00e9-authentifier l'int\u00e9gration"
+ },
"user": {
"data": {
"password": "Mot de passe",
diff --git a/homeassistant/components/totalconnect/translations/he.json b/homeassistant/components/totalconnect/translations/he.json
new file mode 100644
index 00000000000000..ed07845a182fec
--- /dev/null
+++ b/homeassistant/components/totalconnect/translations/he.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "step": {
+ "locations": {
+ "data": {
+ "location": "\u05de\u05d9\u05e7\u05d5\u05dd"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/totalconnect/translations/hu.json b/homeassistant/components/totalconnect/translations/hu.json
index dee4ed9ee0fa4d..6002f05663576d 100644
--- a/homeassistant/components/totalconnect/translations/hu.json
+++ b/homeassistant/components/totalconnect/translations/hu.json
@@ -1,6 +1,21 @@
{
"config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
+ },
+ "error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
+ },
"step": {
+ "locations": {
+ "data": {
+ "location": "Elhelyezked\u00e9s"
+ }
+ },
+ "reauth_confirm": {
+ "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se"
+ },
"user": {
"data": {
"password": "Jelsz\u00f3",
diff --git a/homeassistant/components/totalconnect/translations/id.json b/homeassistant/components/totalconnect/translations/id.json
new file mode 100644
index 00000000000000..c1bdf6649948a2
--- /dev/null
+++ b/homeassistant/components/totalconnect/translations/id.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Akun sudah dikonfigurasi",
+ "reauth_successful": "Autentikasi ulang berhasil"
+ },
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid",
+ "usercode": "Kode pengguna tidak valid untuk pengguna ini di lokasi ini"
+ },
+ "step": {
+ "locations": {
+ "data": {
+ "location": "Lokasi"
+ },
+ "description": "Masukkan kode pengguna untuk pengguna ini di lokasi ini",
+ "title": "Lokasi Kode Pengguna"
+ },
+ "reauth_confirm": {
+ "description": "Total Connect perlu mengautentikasi ulang akun Anda",
+ "title": "Autentikasi Ulang Integrasi"
+ },
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "title": "Total Connect"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/totalconnect/translations/it.json b/homeassistant/components/totalconnect/translations/it.json
index 2a12d00f57d3d0..18ecf6483108d4 100644
--- a/homeassistant/components/totalconnect/translations/it.json
+++ b/homeassistant/components/totalconnect/translations/it.json
@@ -1,12 +1,25 @@
{
"config": {
"abort": {
- "already_configured": "L'account \u00e8 gi\u00e0 configurato"
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato",
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
},
"error": {
- "invalid_auth": "Autenticazione non valida"
+ "invalid_auth": "Autenticazione non valida",
+ "usercode": "Codice utente non valido per questo utente in questa posizione"
},
"step": {
+ "locations": {
+ "data": {
+ "location": "Posizione"
+ },
+ "description": "Immettere il codice utente per questo utente in questa posizione",
+ "title": "Codici utente posizione"
+ },
+ "reauth_confirm": {
+ "description": "Total Connect deve autenticare nuovamente il tuo account",
+ "title": "Autenticare nuovamente l'integrazione"
+ },
"user": {
"data": {
"password": "Password",
diff --git a/homeassistant/components/totalconnect/translations/ko.json b/homeassistant/components/totalconnect/translations/ko.json
index 99513a64508c9a..354522154b59de 100644
--- a/homeassistant/components/totalconnect/translations/ko.json
+++ b/homeassistant/components/totalconnect/translations/ko.json
@@ -1,9 +1,25 @@
{
"config": {
"abort": {
- "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "usercode": "\uc774 \uc704\uce58\uc758 \ud574\ub2f9 \uc0ac\uc6a9\uc790\uc5d0 \ub300\ud55c \uc0ac\uc6a9\uc790 \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
+ "locations": {
+ "data": {
+ "location": "\uc704\uce58"
+ },
+ "description": "\uc774 \uc704\uce58\uc758 \ud574\ub2f9 \uc0ac\uc6a9\uc790\uc5d0 \ub300\ud55c \uc0ac\uc6a9\uc790 \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694",
+ "title": "\uc704\uce58 \uc0ac\uc6a9\uc790 \ucf54\ub4dc"
+ },
+ "reauth_confirm": {
+ "description": "Total Connect\ub294 \uacc4\uc815\uc744 \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4",
+ "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30"
+ },
"user": {
"data": {
"password": "\ube44\ubc00\ubc88\ud638",
diff --git a/homeassistant/components/totalconnect/translations/nl.json b/homeassistant/components/totalconnect/translations/nl.json
index c72b7e368ac0ff..de20d40bee6a1e 100644
--- a/homeassistant/components/totalconnect/translations/nl.json
+++ b/homeassistant/components/totalconnect/translations/nl.json
@@ -1,12 +1,25 @@
{
"config": {
"abort": {
- "already_configured": "Account al geconfigureerd"
+ "already_configured": "Account is al geconfigureerd",
+ "reauth_successful": "Herauthenticatie was succesvol"
},
"error": {
- "invalid_auth": "Ongeldige authenticatie"
+ "invalid_auth": "Ongeldige authenticatie",
+ "usercode": "Gebruikerscode niet geldig voor deze gebruiker op deze locatie"
},
"step": {
+ "locations": {
+ "data": {
+ "location": "Locatie"
+ },
+ "description": "Voer de gebruikerscode voor deze gebruiker op deze locatie in",
+ "title": "Locatie gebruikerscodes"
+ },
+ "reauth_confirm": {
+ "description": "Total Connect moet uw account opnieuw verifi\u00ebren",
+ "title": "Verifieer de integratie opnieuw"
+ },
"user": {
"data": {
"password": "Wachtwoord",
diff --git a/homeassistant/components/totalconnect/translations/no.json b/homeassistant/components/totalconnect/translations/no.json
index e80f86696fcd3f..9c98d6ad1e7155 100644
--- a/homeassistant/components/totalconnect/translations/no.json
+++ b/homeassistant/components/totalconnect/translations/no.json
@@ -1,12 +1,25 @@
{
"config": {
"abort": {
- "already_configured": "Kontoen er allerede konfigurert"
+ "already_configured": "Kontoen er allerede konfigurert",
+ "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket"
},
"error": {
- "invalid_auth": "Ugyldig godkjenning"
+ "invalid_auth": "Ugyldig godkjenning",
+ "usercode": "Brukerkode er ikke gyldig for denne brukeren p\u00e5 dette stedet"
},
"step": {
+ "locations": {
+ "data": {
+ "location": "Plassering"
+ },
+ "description": "Angi brukerkoden for denne brukeren p\u00e5 denne plasseringen",
+ "title": "Brukerkoder for plassering"
+ },
+ "reauth_confirm": {
+ "description": "Total Connect m\u00e5 godkjenne kontoen din p\u00e5 nytt",
+ "title": "Godkjenne integrering p\u00e5 nytt"
+ },
"user": {
"data": {
"password": "Passord",
diff --git a/homeassistant/components/totalconnect/translations/pl.json b/homeassistant/components/totalconnect/translations/pl.json
index 530d632040ce35..ff2ca2351e6d25 100644
--- a/homeassistant/components/totalconnect/translations/pl.json
+++ b/homeassistant/components/totalconnect/translations/pl.json
@@ -1,12 +1,25 @@
{
"config": {
"abort": {
- "already_configured": "Konto jest ju\u017c skonfigurowane"
+ "already_configured": "Konto jest ju\u017c skonfigurowane",
+ "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119"
},
"error": {
- "invalid_auth": "Niepoprawne uwierzytelnienie"
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "usercode": "Nieprawid\u0142owy kod u\u017cytkownika dla u\u017cytkownika w tej lokalizacji"
},
"step": {
+ "locations": {
+ "data": {
+ "location": "Lokalizacja"
+ },
+ "description": "Wprowad\u017a kod u\u017cytkownika dla u\u017cytkownika w tej lokalizacji",
+ "title": "Kody lokalizacji u\u017cytkownika"
+ },
+ "reauth_confirm": {
+ "description": "Integracja Total Connect wymaga ponownego uwierzytelnienia Twojego konta",
+ "title": "Ponownie uwierzytelnij integracj\u0119"
+ },
"user": {
"data": {
"password": "Has\u0142o",
diff --git a/homeassistant/components/totalconnect/translations/pt.json b/homeassistant/components/totalconnect/translations/pt.json
index 3c17682089a267..11ac82622676ff 100644
--- a/homeassistant/components/totalconnect/translations/pt.json
+++ b/homeassistant/components/totalconnect/translations/pt.json
@@ -1,12 +1,21 @@
{
"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"
},
"step": {
+ "locations": {
+ "data": {
+ "location": "Localiza\u00e7\u00e3o"
+ }
+ },
+ "reauth_confirm": {
+ "title": "Reautenticar integra\u00e7\u00e3o"
+ },
"user": {
"data": {
"password": "Palavra-passe",
diff --git a/homeassistant/components/totalconnect/translations/ru.json b/homeassistant/components/totalconnect/translations/ru.json
index c5221b5e4ca43a..a4e48ca01d4f02 100644
--- a/homeassistant/components/totalconnect/translations/ru.json
+++ b/homeassistant/components/totalconnect/translations/ru.json
@@ -1,16 +1,29 @@
{
"config": {
"abort": {
- "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant."
+ "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
},
"error": {
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "usercode": "\u041a\u043e\u0434 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0432 \u044d\u0442\u043e\u043c \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438."
},
"step": {
+ "locations": {
+ "data": {
+ "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0432 \u044d\u0442\u043e\u043c \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438.",
+ "title": "\u041a\u043e\u0434\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
+ },
+ "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 Total Connect",
+ "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": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "Total Connect"
}
diff --git a/homeassistant/components/totalconnect/translations/tr.json b/homeassistant/components/totalconnect/translations/tr.json
new file mode 100644
index 00000000000000..f941db5ab8942d
--- /dev/null
+++ b/homeassistant/components/totalconnect/translations/tr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/totalconnect/translations/uk.json b/homeassistant/components/totalconnect/translations/uk.json
new file mode 100644
index 00000000000000..f34a279d598ddc
--- /dev/null
+++ b/homeassistant/components/totalconnect/translations/uk.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "Total Connect"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/totalconnect/translations/zh-Hant.json b/homeassistant/components/totalconnect/translations/zh-Hant.json
index c20dd4065b6de7..96921baf007dbe 100644
--- a/homeassistant/components/totalconnect/translations/zh-Hant.json
+++ b/homeassistant/components/totalconnect/translations/zh-Hant.json
@@ -1,12 +1,25 @@
{
"config": {
"abort": {
- "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f"
},
"error": {
- "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548"
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "usercode": "\u4f7f\u7528\u8005\u4ee3\u78bc\u4e0d\u652f\u63f4\u6b64\u5ea7\u6a19"
},
"step": {
+ "locations": {
+ "data": {
+ "location": "\u5ea7\u6a19"
+ },
+ "description": "\u8f38\u5165\u4f7f\u7528\u8005\u65bc\u6b64\u5ea7\u6a19\u4e4b\u4f7f\u7528\u8005\u4ee3\u78bc",
+ "title": "\u5ea7\u6a19\u4f7f\u7528\u8005\u4ee3\u78bc"
+ },
+ "reauth_confirm": {
+ "description": "Total Connect \u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u5e33\u865f",
+ "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408"
+ },
"user": {
"data": {
"password": "\u5bc6\u78bc",
diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py
index 1aca4bf7edc0d8..b9318cf3fdd217 100644
--- a/homeassistant/components/tplink/common.py
+++ b/homeassistant/components/tplink/common.py
@@ -1,6 +1,7 @@
"""Common code for tplink."""
+from __future__ import annotations
+
import logging
-from typing import List
from pyHS100 import (
Discover,
@@ -30,7 +31,7 @@ class SmartDevices:
"""Hold different kinds of devices."""
def __init__(
- self, lights: List[SmartDevice] = None, switches: List[SmartDevice] = None
+ self, lights: list[SmartDevice] = None, switches: list[SmartDevice] = None
):
"""Initialize device holder."""
self._lights = lights or []
diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py
index ceb0944efe6c39..8880373955f05f 100644
--- a/homeassistant/components/tplink/light.py
+++ b/homeassistant/components/tplink/light.py
@@ -1,9 +1,12 @@
"""Support for TPLink lights."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
import logging
+import re
import time
-from typing import Any, Dict, NamedTuple, Tuple, cast
+from typing import Any, NamedTuple, cast
from pyHS100 import SmartBulb, SmartDeviceException
@@ -58,6 +61,21 @@
MAX_ATTEMPTS = 300
SLEEP_TIME = 2
+TPLINK_KELVIN = {
+ "LB130": (2500, 9000),
+ "LB120": (2700, 6500),
+ "LB230": (2500, 9000),
+ "KB130": (2500, 9000),
+ "KL130": (2500, 9000),
+ "KL125": (2500, 6500),
+ r"KL120\(EU\)": (2700, 6500),
+ r"KL120\(US\)": (2700, 5000),
+ r"KL430\(US\)": (2500, 9000),
+}
+
+FALLBACK_MIN_COLOR = 2700
+FALLBACK_MAX_COLOR = 5000
+
async def async_setup_entry(hass: HomeAssistantType, config_entry, async_add_entities):
"""Set up lights."""
@@ -88,7 +106,7 @@ class LightState(NamedTuple):
state: bool
brightness: int
color_temp: float
- hs: Tuple[int, int]
+ hs: tuple[int, int]
def to_param(self):
"""Return a version that we can send to the bulb."""
@@ -109,7 +127,7 @@ def to_param(self):
class LightFeatures(NamedTuple):
"""Light features."""
- sysinfo: Dict[str, Any]
+ sysinfo: dict[str, Any]
mac: str
alias: str
model: str
@@ -163,7 +181,7 @@ def available(self) -> bool:
return self._is_available
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
return self._emeter_params
@@ -267,6 +285,19 @@ def supported_features(self):
"""Flag supported features."""
return self._light_features.supported_features
+ def _get_valid_temperature_range(self):
+ """Return the device-specific white temperature range (in Kelvin).
+
+ :return: White temperature range in Kelvin (minimum, maximum)
+ """
+ model = self.smartbulb.sys_info[LIGHT_SYSINFO_MODEL]
+ for obj, temp_range in TPLINK_KELVIN.items():
+ if re.match(obj, model):
+ return temp_range
+ # pyHS100 is abandoned, but some bulb definitions aren't present
+ # use "safe" values for something that advertises color temperature
+ return FALLBACK_MIN_COLOR, FALLBACK_MAX_COLOR
+
def _get_light_features(self):
"""Determine all supported features in one go."""
sysinfo = self.smartbulb.sys_info
@@ -283,9 +314,7 @@ def _get_light_features(self):
supported_features += SUPPORT_BRIGHTNESS
if sysinfo.get(LIGHT_SYSINFO_IS_VARIABLE_COLOR_TEMP):
supported_features += SUPPORT_COLOR_TEMP
- # Have to make another api request here in
- # order to not re-implement pyHS100 here
- max_range, min_range = self.smartbulb.valid_temperature_range
+ max_range, min_range = self._get_valid_temperature_range()
min_mireds = kelvin_to_mired(min_range)
max_mireds = kelvin_to_mired(max_range)
if sysinfo.get(LIGHT_SYSINFO_IS_COLOR):
@@ -318,12 +347,12 @@ def _light_state_from_params(self, light_state_params) -> LightState:
light_state_params[LIGHT_STATE_BRIGHTNESS]
)
- if light_features.supported_features & SUPPORT_COLOR_TEMP:
- if (
- light_state_params.get(LIGHT_STATE_COLOR_TEMP) is not None
- and light_state_params[LIGHT_STATE_COLOR_TEMP] != 0
- ):
- color_temp = kelvin_to_mired(light_state_params[LIGHT_STATE_COLOR_TEMP])
+ if (
+ light_features.supported_features & SUPPORT_COLOR_TEMP
+ and light_state_params.get(LIGHT_STATE_COLOR_TEMP) is not None
+ and light_state_params[LIGHT_STATE_COLOR_TEMP] != 0
+ ):
+ color_temp = kelvin_to_mired(light_state_params[LIGHT_STATE_COLOR_TEMP])
if light_features.supported_features & SUPPORT_COLOR:
hue_saturation = (
@@ -353,8 +382,8 @@ def _update_emeter(self):
or self._last_current_power_update + CURRENT_POWER_UPDATE_INTERVAL < now
):
self._last_current_power_update = now
- self._emeter_params[ATTR_CURRENT_POWER_W] = "{:.1f}".format(
- self.smartbulb.current_consumption()
+ self._emeter_params[ATTR_CURRENT_POWER_W] = round(
+ float(self.smartbulb.current_consumption()), 1
)
if (
@@ -366,11 +395,11 @@ def _update_emeter(self):
daily_statistics = self.smartbulb.get_emeter_daily()
monthly_statistics = self.smartbulb.get_emeter_monthly()
try:
- self._emeter_params[ATTR_DAILY_ENERGY_KWH] = "{:.3f}".format(
- daily_statistics[int(time.strftime("%d"))]
+ self._emeter_params[ATTR_DAILY_ENERGY_KWH] = round(
+ float(daily_statistics[int(time.strftime("%d"))]), 3
)
- self._emeter_params[ATTR_MONTHLY_ENERGY_KWH] = "{:.3f}".format(
- monthly_statistics[int(time.strftime("%m"))]
+ self._emeter_params[ATTR_MONTHLY_ENERGY_KWH] = round(
+ float(monthly_statistics[int(time.strftime("%m"))]), 3
)
except KeyError:
# device returned no daily/monthly history
diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py
index 23000fe7b59c63..11b86d6254f0c7 100644
--- a/homeassistant/components/tplink/switch.py
+++ b/homeassistant/components/tplink/switch.py
@@ -1,5 +1,6 @@
"""Support for TPLink HS100/HS110/HS200 smart switch."""
import asyncio
+from contextlib import suppress
import logging
import time
@@ -100,7 +101,7 @@ def turn_off(self, **kwargs):
self.smartplug.turn_off()
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
return self._emeter_params
@@ -137,27 +138,24 @@ def attempt_update(self, update_attempt):
if self.smartplug.has_emeter:
emeter_readings = self.smartplug.get_emeter_realtime()
- self._emeter_params[ATTR_CURRENT_POWER_W] = "{:.2f}".format(
- emeter_readings["power"]
+ self._emeter_params[ATTR_CURRENT_POWER_W] = round(
+ float(emeter_readings["power"]), 2
)
- self._emeter_params[ATTR_TOTAL_ENERGY_KWH] = "{:.3f}".format(
- emeter_readings["total"]
+ self._emeter_params[ATTR_TOTAL_ENERGY_KWH] = round(
+ float(emeter_readings["total"]), 3
)
- self._emeter_params[ATTR_VOLTAGE] = "{:.1f}".format(
- emeter_readings["voltage"]
+ self._emeter_params[ATTR_VOLTAGE] = round(
+ float(emeter_readings["voltage"]), 1
)
- self._emeter_params[ATTR_CURRENT_A] = "{:.2f}".format(
- emeter_readings["current"]
+ self._emeter_params[ATTR_CURRENT_A] = round(
+ float(emeter_readings["current"]), 2
)
emeter_statics = self.smartplug.get_emeter_daily()
- try:
- self._emeter_params[ATTR_TODAY_ENERGY_KWH] = "{:.3f}".format(
- emeter_statics[int(time.strftime("%e"))]
+ with suppress(KeyError): # Device returned no daily history
+ self._emeter_params[ATTR_TODAY_ENERGY_KWH] = round(
+ float(emeter_statics[int(time.strftime("%e"))]), 3
)
- except KeyError:
- # Device returned no daily history
- pass
return True
except (SmartDeviceException, OSError) as ex:
if update_attempt == 0:
diff --git a/homeassistant/components/tplink/translations/de.json b/homeassistant/components/tplink/translations/de.json
index 64bdfc9bf774ec..48571158085223 100644
--- a/homeassistant/components/tplink/translations/de.json
+++ b/homeassistant/components/tplink/translations/de.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"no_devices_found": "Es wurden keine TP-Link-Ger\u00e4te im Netzwerk gefunden.",
- "single_instance_allowed": "Es ist nur eine einzige Konfiguration erforderlich."
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/tplink/translations/hu.json b/homeassistant/components/tplink/translations/hu.json
new file mode 100644
index 00000000000000..ab799e90c74001
--- /dev/null
+++ b/homeassistant/components/tplink/translations/hu.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tplink/translations/id.json b/homeassistant/components/tplink/translations/id.json
new file mode 100644
index 00000000000000..66d510de4edc6c
--- /dev/null
+++ b/homeassistant/components/tplink/translations/id.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "step": {
+ "confirm": {
+ "description": "Ingin menyiapkan perangkat cerdas TP-Link?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tplink/translations/ko.json b/homeassistant/components/tplink/translations/ko.json
index dc8a6a5a8fc9ab..a1bfb59ca0719b 100644
--- a/homeassistant/components/tplink/translations/ko.json
+++ b/homeassistant/components/tplink/translations/ko.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "TP-Link \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
- "single_instance_allowed": "\ud558\ub098\uc758 TP-Link \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/tplink/translations/nl.json b/homeassistant/components/tplink/translations/nl.json
index f6a8dbbe02a2e5..362645d9f1955b 100644
--- a/homeassistant/components/tplink/translations/nl.json
+++ b/homeassistant/components/tplink/translations/nl.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Geen TP-Link apparaten gevonden op het netwerk.",
- "single_instance_allowed": "Slechts \u00e9\u00e9n configuratie is nodig."
+ "no_devices_found": "Geen apparaten gevonden op het netwerk",
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/tplink/translations/tr.json b/homeassistant/components/tplink/translations/tr.json
new file mode 100644
index 00000000000000..e8f7a5aaf6dd06
--- /dev/null
+++ b/homeassistant/components/tplink/translations/tr.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "step": {
+ "confirm": {
+ "description": "TP-Link ak\u0131ll\u0131 cihazlar\u0131 kurmak istiyor musunuz?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tplink/translations/uk.json b/homeassistant/components/tplink/translations/uk.json
new file mode 100644
index 00000000000000..cfeaf049675f2a
--- /dev/null
+++ b/homeassistant/components/tplink/translations/uk.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 TP-Link Smart Home?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py
index c19a9cdd27e895..cc598a9851b132 100644
--- a/homeassistant/components/traccar/__init__.py
+++ b/homeassistant/components/traccar/__init__.py
@@ -3,7 +3,12 @@
import voluptuous as vol
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER
-from homeassistant.const import CONF_WEBHOOK_ID, HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
+from homeassistant.const import (
+ ATTR_ID,
+ CONF_WEBHOOK_ID,
+ HTTP_OK,
+ HTTP_UNPROCESSABLE_ENTITY,
+)
from homeassistant.helpers import config_entry_flow
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -13,7 +18,6 @@
ATTR_ALTITUDE,
ATTR_BATTERY,
ATTR_BEARING,
- ATTR_ID,
ATTR_LATITUDE,
ATTR_LONGITUDE,
ATTR_SPEED,
diff --git a/homeassistant/components/traccar/const.py b/homeassistant/components/traccar/const.py
index 56c0ab5ba1d5d6..06dd368b6a3e25 100644
--- a/homeassistant/components/traccar/const.py
+++ b/homeassistant/components/traccar/const.py
@@ -12,7 +12,6 @@
ATTR_BEARING = "bearing"
ATTR_CATEGORY = "category"
ATTR_GEOFENCE = "geofence"
-ATTR_ID = "id"
ATTR_LATITUDE = "lat"
ATTR_LONGITUDE = "lon"
ATTR_MOTION = "motion"
diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py
index cdb2033951085a..d558129e323bec 100644
--- a/homeassistant/components/traccar/device_tracker.py
+++ b/homeassistant/components/traccar/device_tracker.py
@@ -345,7 +345,7 @@ def battery_level(self):
return self._battery
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific attributes."""
return self._attributes
diff --git a/homeassistant/components/traccar/strings.json b/homeassistant/components/traccar/strings.json
index d9d9fff4bd3fb2..89689ee43df9a0 100644
--- a/homeassistant/components/traccar/strings.json
+++ b/homeassistant/components/traccar/strings.json
@@ -11,7 +11,7 @@
"webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]"
},
"create_entry": {
- "default": "To send events to Home Assistant, you will need to setup the webhook feature in Traccar.\n\nUse the following url: `{webhook_url}`\n\nSee [the documentation]({docs_url}) for further details."
+ "default": "To send events to Home Assistant, you will need to setup the webhook feature in Traccar.\n\nUse the following URL: `{webhook_url}`\n\nSee [the documentation]({docs_url}) for further details."
}
}
}
diff --git a/homeassistant/components/traccar/translations/ca.json b/homeassistant/components/traccar/translations/ca.json
index 1b00aab4b3e86a..62c15e0ca20096 100644
--- a/homeassistant/components/traccar/translations/ca.json
+++ b/homeassistant/components/traccar/translations/ca.json
@@ -5,7 +5,7 @@
"webhook_not_internet_accessible": "La teva inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per poder rebre missatges webhook."
},
"create_entry": {
- "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar l'opci\u00f3 webhook de Traccar.\n\nUtilitza el seg\u00fcent enlla\u00e7: `{webhook_url}`\n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls."
+ "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar l'opci\u00f3 webhook de Traccar.\n\nUtilitza el seg\u00fcent URL: `{webhook_url}`\n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls."
},
"step": {
"user": {
diff --git a/homeassistant/components/traccar/translations/de.json b/homeassistant/components/traccar/translations/de.json
index 5d5969b2d5111f..7e253c1d05f6e0 100644
--- a/homeassistant/components/traccar/translations/de.json
+++ b/homeassistant/components/traccar/translations/de.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.",
+ "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen."
+ },
"create_entry": {
- "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}}`\n\nSiehe [die Dokumentation]( {docs_url} ) f\u00fcr weitere Details."
+ "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}`\n\nSiehe [Dokumentation]({docs_url}) f\u00fcr weitere Details."
},
"step": {
"user": {
diff --git a/homeassistant/components/traccar/translations/en.json b/homeassistant/components/traccar/translations/en.json
index 2231d53ceb8e6f..c6d7f0f189245e 100644
--- a/homeassistant/components/traccar/translations/en.json
+++ b/homeassistant/components/traccar/translations/en.json
@@ -5,7 +5,7 @@
"webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages."
},
"create_entry": {
- "default": "To send events to Home Assistant, you will need to setup the webhook feature in Traccar.\n\nUse the following url: `{webhook_url}`\n\nSee [the documentation]({docs_url}) for further details."
+ "default": "To send events to Home Assistant, you will need to setup the webhook feature in Traccar.\n\nUse the following URL: `{webhook_url}`\n\nSee [the documentation]({docs_url}) for further details."
},
"step": {
"user": {
diff --git a/homeassistant/components/traccar/translations/hu.json b/homeassistant/components/traccar/translations/hu.json
index a14c446e673fae..c4fc027d059c67 100644
--- a/homeassistant/components/traccar/translations/hu.json
+++ b/homeassistant/components/traccar/translations/hu.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.",
+ "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz."
+ },
"create_entry": {
- "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Traccar-ban. \n\n Haszn\u00e1lja a k\u00f6vetkez\u0151 URL-t: \" {webhook_url} \" \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} )."
+ "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Traccar-ban. \n\n Haszn\u00e1lja a k\u00f6vetkez\u0151 URL-t: `{webhook_url}`\n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t]({docs_url})."
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/traccar/translations/id.json b/homeassistant/components/traccar/translations/id.json
new file mode 100644
index 00000000000000..573b73570c2310
--- /dev/null
+++ b/homeassistant/components/traccar/translations/id.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.",
+ "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook."
+ },
+ "create_entry": {
+ "default": "Untuk mengirim event ke Home Assistant, Anda harus menyiapkan fitur webhook di Traccar.\n\nGunakan URL berikut: {webhook_url}`\n\nBaca [dokumentasi]({docs_url}) untuk detail lebih lanjut."
+ },
+ "step": {
+ "user": {
+ "description": "Yakin ingin menyiapkan Traccar?",
+ "title": "Siapkan Traccar"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/traccar/translations/it.json b/homeassistant/components/traccar/translations/it.json
index 6d4de5bb13d02c..8c95b3cd022f45 100644
--- a/homeassistant/components/traccar/translations/it.json
+++ b/homeassistant/components/traccar/translations/it.json
@@ -5,7 +5,7 @@
"webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook."
},
"create_entry": {
- "default": "Per inviare eventi a Home Assistant, \u00e8 necessario configurare la funzionalit\u00e0 webhook in Traccar.\n\nUtilizzare l'URL seguente: `{webhook_url}`\n\nPer ulteriori dettagli, vedere [la documentazione]({docs_url}) ."
+ "default": "Per inviare eventi a Home Assistant, \u00e8 necessario impostare la funzione webhook in Traccar.\n\nUsa il seguente URL: `{webhook_url}`.\n\nVedi [la documentazione]({docs_url}) per ulteriori dettagli."
},
"step": {
"user": {
diff --git a/homeassistant/components/traccar/translations/ko.json b/homeassistant/components/traccar/translations/ko.json
index 910281d4b38009..aa5e2a65736734 100644
--- a/homeassistant/components/traccar/translations/ko.json
+++ b/homeassistant/components/traccar/translations/ko.json
@@ -1,11 +1,15 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4."
+ },
"create_entry": {
- "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Traccar \uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c URL \uc815\ubcf4\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4: `{webhook_url}`\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
+ "default": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Traccar\uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c\uc758 URL \uc8fc\uc18c\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4: `{webhook_url}`\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
},
"step": {
"user": {
- "description": "Traccar \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "description": "Traccar\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "Traccar \uc124\uc815\ud558\uae30"
}
}
diff --git a/homeassistant/components/traccar/translations/lb.json b/homeassistant/components/traccar/translations/lb.json
index d729525200503d..9e7d16fec3f08e 100644
--- a/homeassistant/components/traccar/translations/lb.json
+++ b/homeassistant/components/traccar/translations/lb.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.",
+ "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken."
},
"create_entry": {
"default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss den Webhook Feature am Traccar ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider Informatiounen."
diff --git a/homeassistant/components/traccar/translations/nl.json b/homeassistant/components/traccar/translations/nl.json
index 251e16d07638c2..0b4563d69fcfa4 100644
--- a/homeassistant/components/traccar/translations/nl.json
+++ b/homeassistant/components/traccar/translations/nl.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk."
+ "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.",
+ "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen."
},
"create_entry": {
"default": "Voor het verzenden van gebeurtenissen naar Home Assistant, moet u de webhook-functie in Traccar instellen.\n\nGebruik de volgende URL: ' {webhook_url} '\n\nZie [de documentatie] ({docs_url}) voor meer informatie."
diff --git a/homeassistant/components/traccar/translations/no.json b/homeassistant/components/traccar/translations/no.json
index 38faa4dc1c1ca1..e2051be22b6262 100644
--- a/homeassistant/components/traccar/translations/no.json
+++ b/homeassistant/components/traccar/translations/no.json
@@ -5,7 +5,7 @@
"webhook_not_internet_accessible": "Home Assistant forekomsten din m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta webhook meldinger"
},
"create_entry": {
- "default": "Hvis du vil sende hendelser til Home Assistant, m\u00e5 du konfigurere webhook-funksjonen i Traccar.\n\nBruk f\u00f8lgende URL-adresse: `{webhook_url}`\n\nSe [dokumentasjonen]({docs_url}) for mer informasjon."
+ "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du konfigurere webhook-funksjonen i Traccar. \n\n Bruk f\u00f8lgende URL: \"{webhook_url}\" \n\n Se [dokumentasjonen] ({docs_url}) for mer informasjon."
},
"step": {
"user": {
diff --git a/homeassistant/components/traccar/translations/ru.json b/homeassistant/components/traccar/translations/ru.json
index b35b1e74e1e7e1..dc4cd2cde112e4 100644
--- a/homeassistant/components/traccar/translations/ru.json
+++ b/homeassistant/components/traccar/translations/ru.json
@@ -5,7 +5,7 @@
"webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439."
},
"create_entry": {
- "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Traccar.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438."
+ "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Traccar.\n\n\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 URL-\u0430\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438: `{webhook_url}`\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438."
},
"step": {
"user": {
diff --git a/homeassistant/components/traccar/translations/tr.json b/homeassistant/components/traccar/translations/tr.json
index 7d044949a6ed6a..9a2b1a119cd832 100644
--- a/homeassistant/components/traccar/translations/tr.json
+++ b/homeassistant/components/traccar/translations/tr.json
@@ -1,5 +1,9 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.",
+ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir."
+ },
"step": {
"user": {
"title": "Traccar'\u0131 kur"
diff --git a/homeassistant/components/traccar/translations/uk.json b/homeassistant/components/traccar/translations/uk.json
new file mode 100644
index 00000000000000..5bfb1714a79256
--- /dev/null
+++ b/homeassistant/components/traccar/translations/uk.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.",
+ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c."
+ },
+ "create_entry": {
+ "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f Traccar. \n\n\u0414\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}` \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457"
+ },
+ "step": {
+ "user": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Traccar?",
+ "title": "Traccar"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/traccar/translations/zh-Hant.json b/homeassistant/components/traccar/translations/zh-Hant.json
index 2204e7c3323c6e..ee7c75d84086ff 100644
--- a/homeassistant/components/traccar/translations/zh-Hant.json
+++ b/homeassistant/components/traccar/translations/zh-Hant.json
@@ -5,7 +5,7 @@
"webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002"
},
"create_entry": {
- "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Traccar \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u4f7f\u7528 url: `{webhook_url}`\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002"
+ "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Traccar \u5167\u8a2d\u5b9a Webhook \u529f\u80fd\u3002\n\n\u8acb\u4f7f\u7528 URL\uff1a`{webhook_url}`\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6] ({docs_url}) \u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002"
},
"step": {
"user": {
diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py
new file mode 100644
index 00000000000000..eca22a56da84ae
--- /dev/null
+++ b/homeassistant/components/trace/__init__.py
@@ -0,0 +1,131 @@
+"""Support for script and automation tracing and debugging."""
+from __future__ import annotations
+
+import datetime as dt
+from itertools import count
+from typing import Any, Deque
+
+from homeassistant.core import Context
+from homeassistant.helpers.trace import (
+ TraceElement,
+ script_execution_get,
+ trace_id_get,
+ trace_id_set,
+ trace_set_child_id,
+)
+import homeassistant.util.dt as dt_util
+
+from . import websocket_api
+from .const import DATA_TRACE, STORED_TRACES
+from .utils import LimitedSizeDict
+
+DOMAIN = "trace"
+
+
+async def async_setup(hass, config):
+ """Initialize the trace integration."""
+ hass.data[DATA_TRACE] = {}
+ websocket_api.async_setup(hass)
+ return True
+
+
+def async_store_trace(hass, trace):
+ """Store a trace if its item_id is valid."""
+ key = trace.key
+ if key[1]:
+ traces = hass.data[DATA_TRACE]
+ if key not in traces:
+ traces[key] = LimitedSizeDict(size_limit=STORED_TRACES)
+ traces[key][trace.run_id] = trace
+
+
+class ActionTrace:
+ """Base container for an script or automation trace."""
+
+ _run_ids = count(0)
+
+ def __init__(
+ self,
+ key: tuple[str, str],
+ config: dict[str, Any],
+ blueprint_inputs: dict[str, Any],
+ context: Context,
+ ):
+ """Container for script trace."""
+ self._trace: dict[str, Deque[TraceElement]] | None = None
+ self._config: dict[str, Any] = config
+ self._blueprint_inputs: dict[str, Any] = blueprint_inputs
+ self.context: Context = context
+ self._error: Exception | None = None
+ self._state: str = "running"
+ self._script_execution: str | None = None
+ self.run_id: str = str(next(self._run_ids))
+ self._timestamp_finish: dt.datetime | None = None
+ self._timestamp_start: dt.datetime = dt_util.utcnow()
+ self.key: tuple[str, str] = key
+ if trace_id_get():
+ trace_set_child_id(self.key, self.run_id)
+ trace_id_set((key, self.run_id))
+
+ def set_trace(self, trace: dict[str, Deque[TraceElement]]) -> None:
+ """Set trace."""
+ self._trace = trace
+
+ def set_error(self, ex: Exception) -> None:
+ """Set error."""
+ self._error = ex
+
+ def finished(self) -> None:
+ """Set finish time."""
+ self._timestamp_finish = dt_util.utcnow()
+ self._state = "stopped"
+ self._script_execution = script_execution_get()
+
+ def as_dict(self) -> dict[str, Any]:
+ """Return dictionary version of this ActionTrace."""
+
+ result = self.as_short_dict()
+
+ traces = {}
+ if self._trace:
+ for key, trace_list in self._trace.items():
+ traces[key] = [item.as_dict() for item in trace_list]
+
+ result.update(
+ {
+ "trace": traces,
+ "config": self._config,
+ "blueprint_inputs": self._blueprint_inputs,
+ "context": self.context,
+ }
+ )
+ if self._error is not None:
+ result["error"] = str(self._error)
+ return result
+
+ def as_short_dict(self) -> dict[str, Any]:
+ """Return a brief dictionary version of this ActionTrace."""
+
+ last_step = None
+
+ if self._trace:
+ last_step = list(self._trace)[-1]
+
+ result = {
+ "last_step": last_step,
+ "run_id": self.run_id,
+ "state": self._state,
+ "script_execution": self._script_execution,
+ "timestamp": {
+ "start": self._timestamp_start,
+ "finish": self._timestamp_finish,
+ },
+ "domain": self.key[0],
+ "item_id": self.key[1],
+ }
+ if self._error is not None:
+ result["error"] = str(self._error)
+ if last_step is not None:
+ result["last_step"] = last_step
+
+ return result
diff --git a/homeassistant/components/trace/const.py b/homeassistant/components/trace/const.py
new file mode 100644
index 00000000000000..05942d7ee4dc6f
--- /dev/null
+++ b/homeassistant/components/trace/const.py
@@ -0,0 +1,4 @@
+"""Shared constants for script and automation tracing and debugging."""
+
+DATA_TRACE = "trace"
+STORED_TRACES = 5 # Stored traces per script or automation
diff --git a/homeassistant/components/trace/manifest.json b/homeassistant/components/trace/manifest.json
new file mode 100644
index 00000000000000..cdd857d00d35d5
--- /dev/null
+++ b/homeassistant/components/trace/manifest.json
@@ -0,0 +1,9 @@
+{
+ "domain": "trace",
+ "name": "Trace",
+ "documentation": "https://www.home-assistant.io/integrations/automation",
+ "codeowners": [
+ "@home-assistant/core"
+ ],
+ "quality_scale": "internal"
+}
diff --git a/homeassistant/components/trace/utils.py b/homeassistant/components/trace/utils.py
new file mode 100644
index 00000000000000..7e804724c55640
--- /dev/null
+++ b/homeassistant/components/trace/utils.py
@@ -0,0 +1,43 @@
+"""Helpers for script and automation tracing and debugging."""
+from collections import OrderedDict
+from datetime import timedelta
+from typing import Any
+
+from homeassistant.helpers.json import JSONEncoder as HAJSONEncoder
+
+
+class LimitedSizeDict(OrderedDict):
+ """OrderedDict limited in size."""
+
+ def __init__(self, *args, **kwds):
+ """Initialize OrderedDict limited in size."""
+ self.size_limit = kwds.pop("size_limit", None)
+ OrderedDict.__init__(self, *args, **kwds)
+ self._check_size_limit()
+
+ def __setitem__(self, key, value):
+ """Set item and check dict size."""
+ OrderedDict.__setitem__(self, key, value)
+ self._check_size_limit()
+
+ def _check_size_limit(self):
+ """Check dict size and evict items in FIFO order if needed."""
+ if self.size_limit is not None:
+ while len(self) > self.size_limit:
+ self.popitem(last=False)
+
+
+class TraceJSONEncoder(HAJSONEncoder):
+ """JSONEncoder that supports Home Assistant objects and falls back to repr(o)."""
+
+ def default(self, o: Any) -> Any:
+ """Convert certain objects.
+
+ Fall back to repr(o).
+ """
+ if isinstance(o, timedelta):
+ return {"__type": str(type(o)), "total_seconds": o.total_seconds()}
+ try:
+ return super().default(o)
+ except TypeError:
+ return {"__type": str(type(o)), "repr": repr(o)}
diff --git a/homeassistant/components/trace/websocket_api.py b/homeassistant/components/trace/websocket_api.py
new file mode 100644
index 00000000000000..17f3dc7860daec
--- /dev/null
+++ b/homeassistant/components/trace/websocket_api.py
@@ -0,0 +1,300 @@
+"""Websocket API for automation."""
+import json
+
+import voluptuous as vol
+
+from homeassistant.components import websocket_api
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers.dispatcher import (
+ DATA_DISPATCHER,
+ async_dispatcher_connect,
+ async_dispatcher_send,
+)
+from homeassistant.helpers.script import (
+ SCRIPT_BREAKPOINT_HIT,
+ SCRIPT_DEBUG_CONTINUE_ALL,
+ breakpoint_clear,
+ breakpoint_clear_all,
+ breakpoint_list,
+ breakpoint_set,
+ debug_continue,
+ debug_step,
+ debug_stop,
+)
+
+from .const import DATA_TRACE
+from .utils import TraceJSONEncoder
+
+# mypy: allow-untyped-calls, allow-untyped-defs
+
+TRACE_DOMAINS = ("automation", "script")
+
+
+@callback
+def async_setup(hass: HomeAssistant) -> None:
+ """Set up the websocket API."""
+ websocket_api.async_register_command(hass, websocket_trace_get)
+ websocket_api.async_register_command(hass, websocket_trace_list)
+ websocket_api.async_register_command(hass, websocket_trace_contexts)
+ websocket_api.async_register_command(hass, websocket_breakpoint_clear)
+ websocket_api.async_register_command(hass, websocket_breakpoint_list)
+ websocket_api.async_register_command(hass, websocket_breakpoint_set)
+ websocket_api.async_register_command(hass, websocket_debug_continue)
+ websocket_api.async_register_command(hass, websocket_debug_step)
+ websocket_api.async_register_command(hass, websocket_debug_stop)
+ websocket_api.async_register_command(hass, websocket_subscribe_breakpoint_events)
+
+
+@callback
+@websocket_api.require_admin
+@websocket_api.websocket_command(
+ {
+ vol.Required("type"): "trace/get",
+ vol.Required("domain"): vol.In(TRACE_DOMAINS),
+ vol.Required("item_id"): str,
+ vol.Required("run_id"): str,
+ }
+)
+def websocket_trace_get(hass, connection, msg):
+ """Get an script or automation trace."""
+ key = (msg["domain"], msg["item_id"])
+ run_id = msg["run_id"]
+
+ try:
+ trace = hass.data[DATA_TRACE][key][run_id]
+ except KeyError:
+ connection.send_error(
+ msg["id"], websocket_api.ERR_NOT_FOUND, "The trace could not be found"
+ )
+ return
+
+ message = websocket_api.messages.result_message(msg["id"], trace)
+
+ connection.send_message(json.dumps(message, cls=TraceJSONEncoder, allow_nan=False))
+
+
+def get_debug_traces(hass, key):
+ """Return a serializable list of debug traces for an script or automation."""
+ traces = []
+
+ for trace in hass.data[DATA_TRACE].get(key, {}).values():
+ traces.append(trace.as_short_dict())
+
+ return traces
+
+
+@callback
+@websocket_api.require_admin
+@websocket_api.websocket_command(
+ {
+ vol.Required("type"): "trace/list",
+ vol.Required("domain", "id"): vol.In(TRACE_DOMAINS),
+ vol.Optional("item_id", "id"): str,
+ }
+)
+def websocket_trace_list(hass, connection, msg):
+ """Summarize script and automation traces."""
+ domain = msg["domain"]
+ key = (domain, msg["item_id"]) if "item_id" in msg else None
+
+ if not key:
+ traces = []
+ for key in hass.data[DATA_TRACE]:
+ if key[0] == domain:
+ traces.extend(get_debug_traces(hass, key))
+ else:
+ traces = get_debug_traces(hass, key)
+
+ connection.send_result(msg["id"], traces)
+
+
+@callback
+@websocket_api.require_admin
+@websocket_api.websocket_command(
+ {
+ vol.Required("type"): "trace/contexts",
+ vol.Inclusive("domain", "id"): vol.In(TRACE_DOMAINS),
+ vol.Inclusive("item_id", "id"): str,
+ }
+)
+def websocket_trace_contexts(hass, connection, msg):
+ """Retrieve contexts we have traces for."""
+ key = (msg["domain"], msg["item_id"]) if "item_id" in msg else None
+
+ if key is not None:
+ values = {key: hass.data[DATA_TRACE].get(key, {})}
+ else:
+ values = hass.data[DATA_TRACE]
+
+ contexts = {
+ trace.context.id: {"run_id": trace.run_id, "domain": key[0], "item_id": key[1]}
+ for key, traces in values.items()
+ for trace in traces.values()
+ }
+
+ connection.send_result(msg["id"], contexts)
+
+
+@callback
+@websocket_api.require_admin
+@websocket_api.websocket_command(
+ {
+ vol.Required("type"): "trace/debug/breakpoint/set",
+ vol.Required("domain"): vol.In(TRACE_DOMAINS),
+ vol.Required("item_id"): str,
+ vol.Required("node"): str,
+ vol.Optional("run_id"): str,
+ }
+)
+def websocket_breakpoint_set(hass, connection, msg):
+ """Set breakpoint."""
+ key = (msg["domain"], msg["item_id"])
+ node = msg["node"]
+ run_id = msg.get("run_id")
+
+ if (
+ SCRIPT_BREAKPOINT_HIT not in hass.data.get(DATA_DISPATCHER, {})
+ or not hass.data[DATA_DISPATCHER][SCRIPT_BREAKPOINT_HIT]
+ ):
+ raise HomeAssistantError("No breakpoint subscription")
+
+ result = breakpoint_set(hass, key, run_id, node)
+ connection.send_result(msg["id"], result)
+
+
+@callback
+@websocket_api.require_admin
+@websocket_api.websocket_command(
+ {
+ vol.Required("type"): "trace/debug/breakpoint/clear",
+ vol.Required("domain"): vol.In(TRACE_DOMAINS),
+ vol.Required("item_id"): str,
+ vol.Required("node"): str,
+ vol.Optional("run_id"): str,
+ }
+)
+def websocket_breakpoint_clear(hass, connection, msg):
+ """Clear breakpoint."""
+ key = (msg["domain"], msg["item_id"])
+ node = msg["node"]
+ run_id = msg.get("run_id")
+
+ result = breakpoint_clear(hass, key, run_id, node)
+
+ connection.send_result(msg["id"], result)
+
+
+@callback
+@websocket_api.require_admin
+@websocket_api.websocket_command({vol.Required("type"): "trace/debug/breakpoint/list"})
+def websocket_breakpoint_list(hass, connection, msg):
+ """List breakpoints."""
+ breakpoints = breakpoint_list(hass)
+ for _breakpoint in breakpoints:
+ _breakpoint["domain"], _breakpoint["item_id"] = _breakpoint.pop("key")
+
+ connection.send_result(msg["id"], breakpoints)
+
+
+@callback
+@websocket_api.require_admin
+@websocket_api.websocket_command(
+ {vol.Required("type"): "trace/debug/breakpoint/subscribe"}
+)
+def websocket_subscribe_breakpoint_events(hass, connection, msg):
+ """Subscribe to breakpoint events."""
+
+ @callback
+ def breakpoint_hit(key, run_id, node):
+ """Forward events to websocket."""
+ connection.send_message(
+ websocket_api.event_message(
+ msg["id"],
+ {
+ "domain": key[0],
+ "item_id": key[1],
+ "run_id": run_id,
+ "node": node,
+ },
+ )
+ )
+
+ remove_signal = async_dispatcher_connect(
+ hass, SCRIPT_BREAKPOINT_HIT, breakpoint_hit
+ )
+
+ @callback
+ def unsub():
+ """Unsubscribe from breakpoint events."""
+ remove_signal()
+ if (
+ SCRIPT_BREAKPOINT_HIT not in hass.data.get(DATA_DISPATCHER, {})
+ or not hass.data[DATA_DISPATCHER][SCRIPT_BREAKPOINT_HIT]
+ ):
+ breakpoint_clear_all(hass)
+ async_dispatcher_send(hass, SCRIPT_DEBUG_CONTINUE_ALL)
+
+ connection.subscriptions[msg["id"]] = unsub
+
+ connection.send_message(websocket_api.result_message(msg["id"]))
+
+
+@callback
+@websocket_api.require_admin
+@websocket_api.websocket_command(
+ {
+ vol.Required("type"): "trace/debug/continue",
+ vol.Required("domain"): vol.In(TRACE_DOMAINS),
+ vol.Required("item_id"): str,
+ vol.Required("run_id"): str,
+ }
+)
+def websocket_debug_continue(hass, connection, msg):
+ """Resume execution of halted script or automation."""
+ key = (msg["domain"], msg["item_id"])
+ run_id = msg["run_id"]
+
+ result = debug_continue(hass, key, run_id)
+
+ connection.send_result(msg["id"], result)
+
+
+@callback
+@websocket_api.require_admin
+@websocket_api.websocket_command(
+ {
+ vol.Required("type"): "trace/debug/step",
+ vol.Required("domain"): vol.In(TRACE_DOMAINS),
+ vol.Required("item_id"): str,
+ vol.Required("run_id"): str,
+ }
+)
+def websocket_debug_step(hass, connection, msg):
+ """Single step a halted script or automation."""
+ key = (msg["domain"], msg["item_id"])
+ run_id = msg["run_id"]
+
+ result = debug_step(hass, key, run_id)
+
+ connection.send_result(msg["id"], result)
+
+
+@callback
+@websocket_api.require_admin
+@websocket_api.websocket_command(
+ {
+ vol.Required("type"): "trace/debug/stop",
+ vol.Required("domain"): vol.In(TRACE_DOMAINS),
+ vol.Required("item_id"): str,
+ vol.Required("run_id"): str,
+ }
+)
+def websocket_debug_stop(hass, connection, msg):
+ """Stop a halted script or automation."""
+ key = (msg["domain"], msg["item_id"])
+ run_id = msg["run_id"]
+
+ result = debug_stop(hass, key, run_id)
+
+ connection.send_result(msg["id"], result)
diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py
index 8d82df07bbbd3f..3323c54d9c2768 100644
--- a/homeassistant/components/tradfri/__init__.py
+++ b/homeassistant/components/tradfri/__init__.py
@@ -16,7 +16,6 @@
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.util.json import load_json
-from . import config_flow # noqa: F401
from .const import (
ATTR_TRADFRI_GATEWAY,
ATTR_TRADFRI_GATEWAY_MODEL,
@@ -149,9 +148,9 @@ async def on_hass_stop(event):
sw_version=gateway_info.firmware_version,
)
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
async def async_keep_alive(now):
@@ -175,8 +174,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py
index 7947c3ad6deb6d..e02ac69de365dd 100644
--- a/homeassistant/components/tradfri/config_flow.py
+++ b/homeassistant/components/tradfri/config_flow.py
@@ -15,6 +15,7 @@
CONF_IDENTITY,
CONF_IMPORT_GROUPS,
CONF_KEY,
+ DOMAIN,
KEY_SECURITY_CODE,
)
@@ -28,7 +29,7 @@ def __init__(self, code):
self.code = code
-@config_entries.HANDLERS.register("tradfri")
+@config_entries.HANDLERS.register(DOMAIN)
class FlowHandler(config_entries.ConfigFlow):
"""Handle a config flow."""
diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py
index 72597637bd3e0b..4c7cde1dfd114f 100644
--- a/homeassistant/components/tradfri/cover.py
+++ b/homeassistant/components/tradfri/cover.py
@@ -29,7 +29,7 @@ def __init__(self, device, api, gateway_id):
self._refresh(device)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_MODEL: self._device.device_info.model_number}
diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json
index 5c6bf76a169a57..99b9dff6d22373 100644
--- a/homeassistant/components/tradfri/manifest.json
+++ b/homeassistant/components/tradfri/manifest.json
@@ -7,5 +7,5 @@
"homekit": {
"models": ["TRADFRI"]
},
- "codeowners": ["@ggravlingen"]
+ "codeowners": []
}
diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py
index c2bf640e2aa927..455ca69147d69e 100644
--- a/homeassistant/components/tradfri/sensor.py
+++ b/homeassistant/components/tradfri/sensor.py
@@ -1,5 +1,6 @@
"""Support for IKEA Tradfri sensors."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE
from .base_class import TradfriBaseDevice
@@ -25,7 +26,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(TradfriSensor(sensor, api, gateway_id) for sensor in sensors)
-class TradfriSensor(TradfriBaseDevice):
+class TradfriSensor(TradfriBaseDevice, SensorEntity):
"""The platform class required by Home Assistant."""
def __init__(self, device, api, gateway_id):
diff --git a/homeassistant/components/tradfri/translations/de.json b/homeassistant/components/tradfri/translations/de.json
index 3e55cb701d5ce0..b1ebb2aff0b279 100644
--- a/homeassistant/components/tradfri/translations/de.json
+++ b/homeassistant/components/tradfri/translations/de.json
@@ -2,10 +2,10 @@
"config": {
"abort": {
"already_configured": "Bridge ist bereits konfiguriert.",
- "already_in_progress": "Der Konfigurationsablauf f\u00fcr die Bridge wird bereits ausgef\u00fchrt."
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt"
},
"error": {
- "cannot_connect": "Verbindung zum Gateway nicht m\u00f6glich.",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_key": "Registrierung mit angegebenem Schl\u00fcssel fehlgeschlagen. Wenn dies weiterhin geschieht, starte den Gateway neu.",
"timeout": "Timeout bei der \u00dcberpr\u00fcfung des Codes."
},
diff --git a/homeassistant/components/tradfri/translations/hu.json b/homeassistant/components/tradfri/translations/hu.json
index 8be065fe7971a6..3bc4ec90e77dd0 100644
--- a/homeassistant/components/tradfri/translations/hu.json
+++ b/homeassistant/components/tradfri/translations/hu.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van."
},
"error": {
- "cannot_connect": "Nem siker\u00fclt csatlakozni a gatewayhez.",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
"invalid_key": "Nem siker\u00fclt regisztr\u00e1lni a megadott kulcs seg\u00edts\u00e9g\u00e9vel. Ha ez t\u00f6bbsz\u00f6r megt\u00f6rt\u00e9nik, pr\u00f3b\u00e1lja meg \u00fajraind\u00edtani a gatewayt.",
"timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n."
},
diff --git a/homeassistant/components/tradfri/translations/id.json b/homeassistant/components/tradfri/translations/id.json
index 0671b162e1c758..8ff5fe257ebbe5 100644
--- a/homeassistant/components/tradfri/translations/id.json
+++ b/homeassistant/components/tradfri/translations/id.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Bridge sudah dikonfigurasi"
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung"
},
"error": {
- "cannot_connect": "Tidak dapat terhubung ke gateway.",
+ "cannot_connect": "Gagal terhubung",
"invalid_key": "Gagal mendaftar dengan kunci yang disediakan. Jika ini terus terjadi, coba mulai ulang gateway.",
"timeout": "Waktu tunggu memvalidasi kode telah habis."
},
@@ -12,7 +13,7 @@
"auth": {
"data": {
"host": "Host",
- "security_code": "Kode keamanan"
+ "security_code": "Kode Keamanan"
},
"description": "Anda dapat menemukan kode keamanan di belakang gateway Anda.",
"title": "Masukkan kode keamanan"
diff --git a/homeassistant/components/tradfri/translations/ko.json b/homeassistant/components/tradfri/translations/ko.json
index caa94fa8b10e26..307ba39ceaf6bf 100644
--- a/homeassistant/components/tradfri/translations/ko.json
+++ b/homeassistant/components/tradfri/translations/ko.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "\ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "already_in_progress": "\ube0c\ub9ac\uc9c0 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_key": "\uc81c\uacf5\ub41c \ud0a4\ub85c \ub4f1\ub85d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uc774 \ubb38\uc81c\uac00 \uacc4\uc18d \ubc1c\uc0dd\ud558\uba74 \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud574\ubcf4\uc138\uc694.",
"timeout": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
},
@@ -16,7 +16,7 @@
"security_code": "\ubcf4\uc548 \ucf54\ub4dc"
},
"description": "\uac8c\uc774\ud2b8\uc6e8\uc774 \ub4b7\uba74\uc5d0\uc11c \ubcf4\uc548 \ucf54\ub4dc\ub97c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
- "title": "\ubcf4\uc548 \ucf54\ub4dc \uc785\ub825\ud558\uae30"
+ "title": "\ubcf4\uc548 \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694"
}
}
}
diff --git a/homeassistant/components/tradfri/translations/nl.json b/homeassistant/components/tradfri/translations/nl.json
index 1d0453704d0638..e70e515c4d2a50 100644
--- a/homeassistant/components/tradfri/translations/nl.json
+++ b/homeassistant/components/tradfri/translations/nl.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Bridge is al geconfigureerd.",
- "already_in_progress": "Bridge configuratie is al in volle gang."
+ "already_configured": "Apparaat is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang"
},
"error": {
- "cannot_connect": "Kan geen verbinding maken met bridge",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_key": "Mislukt om te registreren met de meegeleverde sleutel. Als dit blijft gebeuren, probeer dan de gateway opnieuw op te starten.",
"timeout": "Time-out bij validatie van code"
},
diff --git a/homeassistant/components/tradfri/translations/tr.json b/homeassistant/components/tradfri/translations/tr.json
new file mode 100644
index 00000000000000..e4483536b129cb
--- /dev/null
+++ b/homeassistant/components/tradfri/translations/tr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/translations/uk.json b/homeassistant/components/tradfri/translations/uk.json
index a163a4680e3e3c..abd25d04b6be6e 100644
--- a/homeassistant/components/tradfri/translations/uk.json
+++ b/homeassistant/components/tradfri/translations/uk.json
@@ -1,14 +1,22 @@
{
"config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454."
+ },
"error": {
- "cannot_connect": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0448\u043b\u044e\u0437\u0443."
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_key": "\u0427\u0438 \u043d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0437 \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u043c \u043a\u043b\u044e\u0447\u0435\u043c. \u042f\u043a\u0449\u043e \u0446\u0435 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u044c\u0441\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0438\u0442\u0438 \u0448\u043b\u044e\u0437.",
+ "timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 \u043a\u043e\u0434\u0443."
},
"step": {
"auth": {
"data": {
+ "host": "\u0425\u043e\u0441\u0442",
"security_code": "\u041a\u043e\u0434 \u0431\u0435\u0437\u043f\u0435\u043a\u0438"
},
- "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 \u0431\u0435\u0437\u043f\u0435\u043a\u0438"
+ "description": "\u041a\u043e\u0434 \u0431\u0435\u0437\u043f\u0435\u043a\u0438 \u043c\u043e\u0436\u043d\u0430 \u0437\u043d\u0430\u0439\u0442\u0438 \u043d\u0430 \u0437\u0430\u0434\u043d\u0456\u0439 \u043f\u0430\u043d\u0435\u043b\u0456 \u0448\u043b\u044e\u0437\u0443.",
+ "title": "IKEA TR\u00c5DFRI"
}
}
}
diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py
index 12f3cf73e50dc1..37e3bd52cdc5ce 100644
--- a/homeassistant/components/trafikverket_train/sensor.py
+++ b/homeassistant/components/trafikverket_train/sensor.py
@@ -6,7 +6,7 @@
from pytrafikverket import TrafikverketTrain
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_API_KEY,
CONF_NAME,
@@ -16,7 +16,6 @@
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -116,7 +115,7 @@ def next_departuredate(departure):
return next_weekday(today_date, WEEKDAYS.index(departure[0]))
-class TrainSensor(Entity):
+class TrainSensor(SensorEntity):
"""Contains data about a train depature."""
def __init__(self, train_api, name, from_station, to_station, weekday, time):
@@ -153,7 +152,7 @@ async def async_update(self):
self._delay_in_minutes = self._state.get_delay_time()
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._state is None:
return None
diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py
index bb1bad67f829b6..1ae090ea23161e 100644
--- a/homeassistant/components/trafikverket_weatherstation/sensor.py
+++ b/homeassistant/components/trafikverket_weatherstation/sensor.py
@@ -8,7 +8,7 @@
from pytrafikverket.trafikverket_weather import TrafikverketWeather
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_API_KEY,
@@ -24,7 +24,6 @@
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -145,7 +144,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(dev, True)
-class TrafikverketWeatherStation(Entity):
+class TrafikverketWeatherStation(SensorEntity):
"""Representation of a Trafikverket sensor."""
def __init__(self, weather_api, name, sensor_type, sensor_station):
@@ -172,7 +171,7 @@ def icon(self):
return self._icon
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of Trafikverket Weatherstation."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py
index d020bfe97450f9..cb4bcceeeeaac1 100644
--- a/homeassistant/components/transmission/__init__.py
+++ b/homeassistant/components/transmission/__init__.py
@@ -1,7 +1,8 @@
"""Support for the Transmission BitTorrent client API."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import List
import transmissionrpc
from transmissionrpc.error import TransmissionError
@@ -171,12 +172,12 @@ def __init__(self, hass, config_entry):
"""Initialize the Transmission RPC API."""
self.hass = hass
self.config_entry = config_entry
- self.tm_api = None # type: transmissionrpc.Client
- self._tm_data = None # type: TransmissionData
+ self.tm_api: transmissionrpc.Client = None
+ self._tm_data: TransmissionData = None
self.unsub_timer = None
@property
- def api(self) -> "TransmissionData":
+ def api(self) -> TransmissionData:
"""Return the TransmissionData object."""
return self._tm_data
@@ -343,14 +344,14 @@ def __init__(self, hass, config, api: transmissionrpc.Client):
"""Initialize the Transmission RPC API."""
self.hass = hass
self.config = config
- self.data = None # type: transmissionrpc.Session
- self.available = True # type: bool
- self._all_torrents = [] # type: List[transmissionrpc.Torrent]
- self._api = api # type: transmissionrpc.Client
- self._completed_torrents = [] # type: List[transmissionrpc.Torrent]
- self._session = None # type: transmissionrpc.Session
- self._started_torrents = [] # type: List[transmissionrpc.Torrent]
- self._torrents = [] # type: List[transmissionrpc.Torrent]
+ self.data: transmissionrpc.Session = None
+ self.available: bool = True
+ self._all_torrents: list[transmissionrpc.Torrent] = []
+ self._api: transmissionrpc.Client = api
+ self._completed_torrents: list[transmissionrpc.Torrent] = []
+ self._session: transmissionrpc.Session = None
+ self._started_torrents: list[transmissionrpc.Torrent] = []
+ self._torrents: list[transmissionrpc.Torrent] = []
@property
def host(self):
@@ -363,7 +364,7 @@ def signal_update(self):
return f"{DATA_UPDATED}-{self.host}"
@property
- def torrents(self) -> List[transmissionrpc.Torrent]:
+ def torrents(self) -> list[transmissionrpc.Torrent]:
"""Get the list of torrents."""
return self._torrents
@@ -397,42 +398,49 @@ def init_torrent_list(self):
def check_completed_torrent(self):
"""Get completed torrent functionality."""
+ old_completed_torrent_names = {
+ torrent.name for torrent in self._completed_torrents
+ }
+
current_completed_torrents = [
torrent for torrent in self._torrents if torrent.status == "seeding"
]
- freshly_completed_torrents = set(current_completed_torrents).difference(
- self._completed_torrents
- )
- self._completed_torrents = current_completed_torrents
- for torrent in freshly_completed_torrents:
- self.hass.bus.fire(
- EVENT_DOWNLOADED_TORRENT, {"name": torrent.name, "id": torrent.id}
- )
+ for torrent in current_completed_torrents:
+ if torrent.name not in old_completed_torrent_names:
+ self.hass.bus.fire(
+ EVENT_DOWNLOADED_TORRENT, {"name": torrent.name, "id": torrent.id}
+ )
+
+ self._completed_torrents = current_completed_torrents
def check_started_torrent(self):
"""Get started torrent functionality."""
+ old_started_torrent_names = {torrent.name for torrent in self._started_torrents}
+
current_started_torrents = [
torrent for torrent in self._torrents if torrent.status == "downloading"
]
- freshly_started_torrents = set(current_started_torrents).difference(
- self._started_torrents
- )
- self._started_torrents = current_started_torrents
- for torrent in freshly_started_torrents:
- self.hass.bus.fire(
- EVENT_STARTED_TORRENT, {"name": torrent.name, "id": torrent.id}
- )
+ for torrent in current_started_torrents:
+ if torrent.name not in old_started_torrent_names:
+ self.hass.bus.fire(
+ EVENT_STARTED_TORRENT, {"name": torrent.name, "id": torrent.id}
+ )
+
+ self._started_torrents = current_started_torrents
def check_removed_torrent(self):
"""Get removed torrent functionality."""
- freshly_removed_torrents = set(self._all_torrents).difference(self._torrents)
- self._all_torrents = self._torrents
- for torrent in freshly_removed_torrents:
- self.hass.bus.fire(
- EVENT_REMOVED_TORRENT, {"name": torrent.name, "id": torrent.id}
- )
+ current_torrent_names = {torrent.name for torrent in self._torrents}
+
+ for torrent in self._all_torrents:
+ if torrent.name not in current_torrent_names:
+ self.hass.bus.fire(
+ EVENT_REMOVED_TORRENT, {"name": torrent.name, "id": torrent.id}
+ )
+
+ self._all_torrents = self._torrents.copy()
def start_torrents(self):
"""Start all torrents."""
diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py
index 2a24b80be161bd..b00ccfc68c0fc4 100644
--- a/homeassistant/components/transmission/sensor.py
+++ b/homeassistant/components/transmission/sensor.py
@@ -1,12 +1,14 @@
"""Support for monitoring the Transmission BitTorrent client API."""
-from typing import List
+from __future__ import annotations
+
+from contextlib import suppress
from transmissionrpc.torrent import Torrent
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_NAME, DATA_RATE_MEGABYTES_PER_SECOND, STATE_IDLE
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from . import TransmissionClient
from .const import (
@@ -38,12 +40,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(dev, True)
-class TransmissionSensor(Entity):
+class TransmissionSensor(SensorEntity):
"""A base class for all Transmission sensors."""
def __init__(self, tm_client, client_name, sensor_name, sub_type=None):
"""Initialize the sensor."""
- self._tm_client = tm_client # type: TransmissionClient
+ self._tm_client: TransmissionClient = tm_client
self._client_name = client_name
self._name = sensor_name
self._sub_type = sub_type
@@ -148,7 +150,7 @@ def unit_of_measurement(self):
return "Torrents"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes, if any."""
info = _torrents_info(
torrents=self._tm_client.api.torrents,
@@ -168,7 +170,7 @@ def update(self):
self._state = len(torrents)
-def _filter_torrents(torrents: List[Torrent], statuses=None) -> List[Torrent]:
+def _filter_torrents(torrents: list[Torrent], statuses=None) -> list[Torrent]:
return [
torrent
for torrent in torrents
@@ -187,8 +189,6 @@ def _torrents_info(torrents, order, limit, statuses=None):
"status": torrent.status,
"id": torrent.id,
}
- try:
+ with suppress(ValueError):
info["eta"] = str(torrent.eta)
- except ValueError:
- pass
return infos
diff --git a/homeassistant/components/transmission/translations/de.json b/homeassistant/components/transmission/translations/de.json
index a133cd363e04a5..2355905d1f7564 100644
--- a/homeassistant/components/transmission/translations/de.json
+++ b/homeassistant/components/transmission/translations/de.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Host ist bereits konfiguriert."
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
- "cannot_connect": "Verbindung zum Host nicht m\u00f6glich",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"name_exists": "Name existiert bereits"
},
"step": {
diff --git a/homeassistant/components/transmission/translations/he.json b/homeassistant/components/transmission/translations/he.json
new file mode 100644
index 00000000000000..6f4191da70d538
--- /dev/null
+++ b/homeassistant/components/transmission/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/transmission/translations/hu.json b/homeassistant/components/transmission/translations/hu.json
index f2fd2ca79e5f62..22d4e18df5e925 100644
--- a/homeassistant/components/transmission/translations/hu.json
+++ b/homeassistant/components/transmission/translations/hu.json
@@ -1,7 +1,11 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
"error": {
- "cannot_connect": "Nem lehet csatlakozni az \u00e1llom\u00e1shoz",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik"
},
"step": {
@@ -21,6 +25,8 @@
"step": {
"init": {
"data": {
+ "limit": "Limit",
+ "order": "Sorrend",
"scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g"
}
}
diff --git a/homeassistant/components/transmission/translations/id.json b/homeassistant/components/transmission/translations/id.json
new file mode 100644
index 00000000000000..a96524f51658f0
--- /dev/null
+++ b/homeassistant/components/transmission/translations/id.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "name_exists": "Nama sudah ada"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Nama",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "username": "Nama Pengguna"
+ },
+ "title": "Siapkan Klien Transmission"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "limit": "Batas",
+ "order": "Urutan",
+ "scan_interval": "Frekuensi pembaruan"
+ },
+ "title": "Konfigurasikan opsi untuk Transmission"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/transmission/translations/ko.json b/homeassistant/components/transmission/translations/ko.json
index 7f5d67114a1cd2..898a371035043f 100644
--- a/homeassistant/components/transmission/translations/ko.json
+++ b/homeassistant/components/transmission/translations/ko.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\ud638\uc2a4\ud2b8\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4"
},
"step": {
@@ -28,7 +29,7 @@
"order": "\uc21c\uc11c",
"scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4"
},
- "title": "Transmission \uc635\uc158 \uc124\uc815\ud558\uae30"
+ "title": "Transmission\uc5d0 \ub300\ud55c \uc635\uc158 \uad6c\uc131\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/transmission/translations/nl.json b/homeassistant/components/transmission/translations/nl.json
index 8cfa9333ba486c..fcc1e05e7abe7a 100644
--- a/homeassistant/components/transmission/translations/nl.json
+++ b/homeassistant/components/transmission/translations/nl.json
@@ -1,10 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Host is al geconfigureerd."
+ "already_configured": "Apparaat is al geconfigureerd"
},
"error": {
- "cannot_connect": "Kan geen verbinding maken met host",
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
"name_exists": "Naam bestaat al"
},
"step": {
@@ -24,6 +25,8 @@
"step": {
"init": {
"data": {
+ "limit": "Limiet",
+ "order": "Bestel",
"scan_interval": "Update frequentie"
},
"title": "Configureer de opties voor Transmission"
diff --git a/homeassistant/components/transmission/translations/ru.json b/homeassistant/components/transmission/translations/ru.json
index d1fbd592f0f4db..a83e19da4001e9 100644
--- a/homeassistant/components/transmission/translations/ru.json
+++ b/homeassistant/components/transmission/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f."
},
"step": {
@@ -15,7 +15,7 @@
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "Transmission"
}
diff --git a/homeassistant/components/transmission/translations/tr.json b/homeassistant/components/transmission/translations/tr.json
new file mode 100644
index 00000000000000..cffcc65151c6d6
--- /dev/null
+++ b/homeassistant/components/transmission/translations/tr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "password": "Parola",
+ "port": "Port",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/transmission/translations/uk.json b/homeassistant/components/transmission/translations/uk.json
new file mode 100644
index 00000000000000..5bc74f7da2a677
--- /dev/null
+++ b/homeassistant/components/transmission/translations/uk.json
@@ -0,0 +1,36 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "name_exists": "\u0426\u044f \u043d\u0430\u0437\u0432\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "Transmission"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "limit": "\u041e\u0431\u043c\u0435\u0436\u0435\u043d\u043d\u044f",
+ "order": "\u041f\u043e\u0440\u044f\u0434\u043e\u043a",
+ "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f"
+ },
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Transmission"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py
index 2aa27f8c4b39a7..be76999ec3f403 100644
--- a/homeassistant/components/transport_nsw/sensor.py
+++ b/homeassistant/components/transport_nsw/sensor.py
@@ -4,7 +4,7 @@
from TransportNSW import TransportNSW
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_MODE,
@@ -13,7 +13,6 @@
TIME_MINUTES,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
ATTR_STOP_ID = "stop_id"
ATTR_ROUTE = "route"
@@ -65,7 +64,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([TransportNSWSensor(data, stop_id, name)], True)
-class TransportNSWSensor(Entity):
+class TransportNSWSensor(SensorEntity):
"""Implementation of an Transport NSW sensor."""
def __init__(self, data, stop_id, name):
@@ -87,7 +86,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._times is not None:
return {
diff --git a/homeassistant/components/travisci/sensor.py b/homeassistant/components/travisci/sensor.py
index 8a548d8cb6b5fc..94a6ba3a48f07f 100644
--- a/homeassistant/components/travisci/sensor.py
+++ b/homeassistant/components/travisci/sensor.py
@@ -6,7 +6,7 @@
from travispy.errors import TravisError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_API_KEY,
@@ -15,7 +15,6 @@
TIME_SECONDS,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -94,7 +93,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return True
-class TravisCISensor(Entity):
+class TravisCISensor(SensorEntity):
"""Representation of a Travis CI sensor."""
def __init__(self, data, repo_name, user, branch, sensor_type):
@@ -124,7 +123,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attrs = {}
attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py
index 4b4bd48bfe34c9..52a72eca8c956b 100644
--- a/homeassistant/components/trend/binary_sensor.py
+++ b/homeassistant/components/trend/binary_sensor.py
@@ -15,6 +15,7 @@
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
+ CONF_ATTRIBUTE,
CONF_DEVICE_CLASS,
CONF_ENTITY_ID,
CONF_FRIENDLY_NAME,
@@ -40,7 +41,6 @@
ATTR_SAMPLE_DURATION = "sample_duration"
ATTR_SAMPLE_COUNT = "sample_count"
-CONF_ATTRIBUTE = "attribute"
CONF_INVERT = "invert"
CONF_MAX_SAMPLES = "max_samples"
CONF_MIN_GRADIENT = "min_gradient"
@@ -66,7 +66,6 @@
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the trend sensors."""
-
setup_reload_service(hass, DOMAIN, PLATFORMS)
sensors = []
@@ -147,7 +146,7 @@ def device_class(self):
return self._device_class
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return {
ATTR_ENTITY_ID: self._entity_id,
diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json
index 88e32ce4a46f83..2bb3719fe95024 100644
--- a/homeassistant/components/trend/manifest.json
+++ b/homeassistant/components/trend/manifest.json
@@ -2,7 +2,7 @@
"domain": "trend",
"name": "Trend",
"documentation": "https://www.home-assistant.io/integrations/trend",
- "requirements": ["numpy==1.19.2"],
+ "requirements": ["numpy==1.20.2"],
"codeowners": [],
"quality_scale": "internal"
}
diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py
index d278283baaf457..e0f59c51e5a36d 100644
--- a/homeassistant/components/tts/__init__.py
+++ b/homeassistant/components/tts/__init__.py
@@ -1,4 +1,6 @@
"""Provide functionality for TTS."""
+from __future__ import annotations
+
import asyncio
import functools as ft
import hashlib
@@ -7,7 +9,7 @@
import mimetypes
import os
import re
-from typing import Dict, Optional
+from typing import cast
from aiohttp import web
import mutagen
@@ -24,6 +26,8 @@
)
from homeassistant.const import (
ATTR_ENTITY_ID,
+ CONF_DESCRIPTION,
+ CONF_NAME,
CONF_PLATFORM,
HTTP_BAD_REQUEST,
HTTP_NOT_FOUND,
@@ -33,8 +37,11 @@
from homeassistant.helpers import config_per_platform, discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.network import get_url
+from homeassistant.helpers.service import async_set_service_schema
from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.loader import async_get_integration
from homeassistant.setup import async_prepare_setup_platform
+from homeassistant.util.yaml import load_yaml
# mypy: allow-untyped-defs, no-check-untyped-defs
@@ -55,6 +62,8 @@
CONF_SERVICE_NAME = "service_name"
CONF_TIME_MEMORY = "time_memory"
+CONF_FIELDS = "fields"
+
DEFAULT_CACHE = True
DEFAULT_CACHE_DIR = "tts"
DEFAULT_TIME_MEMORY = 300
@@ -127,6 +136,13 @@ async def async_setup(hass, config):
hass.http.register_view(TextToSpeechView(tts))
hass.http.register_view(TextToSpeechUrlView(tts))
+ # Load service descriptions from tts/services.yaml
+ integration = await async_get_integration(hass, DOMAIN)
+ services_yaml = integration.file_path / "services.yaml"
+ services_dict = cast(
+ dict, await hass.async_add_executor_job(load_yaml, str(services_yaml))
+ )
+
async def async_setup_platform(p_type, p_config=None, discovery_info=None):
"""Set up a TTS platform."""
if p_config is None:
@@ -193,8 +209,16 @@ async def async_say_handle(service):
DOMAIN, service_name, async_say_handle, schema=SCHEMA_SERVICE_SAY
)
+ # Register the service description
+ service_desc = {
+ CONF_NAME: f"Say an TTS message with {p_type}",
+ CONF_DESCRIPTION: f"Say something using text-to-speech on a media player with {p_type}.",
+ CONF_FIELDS: services_dict[SERVICE_SAY][CONF_FIELDS],
+ }
+ async_set_service_schema(hass, DOMAIN, service_name, service_desc)
+
setup_tasks = [
- async_setup_platform(p_type, p_config)
+ asyncio.create_task(async_setup_platform(p_type, p_config))
for p_type, p_config in config_per_platform(config, DOMAIN)
]
@@ -221,7 +245,7 @@ async def async_clear_cache_handle(service):
return True
-def _hash_options(options: Dict) -> str:
+def _hash_options(options: dict) -> str:
"""Hashes an options dictionary."""
opts_hash = hashlib.blake2s(digest_size=5)
for key, value in sorted(options.items()):
@@ -490,8 +514,8 @@ def write_tags(filename, data, provider, message, language, options):
class Provider:
"""Represent a single TTS provider."""
- hass: Optional[HomeAssistantType] = None
- name: Optional[str] = None
+ hass: HomeAssistantType | None = None
+ name: str | None = None
@property
def default_language(self):
diff --git a/homeassistant/components/tts/services.yaml b/homeassistant/components/tts/services.yaml
index 7d1bf95572b31f..2b48dd39dee375 100644
--- a/homeassistant/components/tts/services.yaml
+++ b/homeassistant/components/tts/services.yaml
@@ -1,23 +1,43 @@
# Describes the format for available TTS services
say:
- description: Say some things on a media player.
+ name: Say an TTS message
+ description: Say something using text-to-speech on a media player.
fields:
entity_id:
+ name: Entity
description: Name(s) of media player entities.
example: "media_player.floor"
+ required: true
+ selector:
+ entity:
+ domain: media_player
message:
+ name: Message
description: Text to speak on devices.
example: "My name is hanna"
+ required: true
+ selector:
+ text:
cache:
+ name: Cache
description: Control file cache of this message.
example: "true"
+ default: false
+ selector:
+ boolean:
language:
+ name: Language
description: Language to use for speech generation.
example: "ru"
+ selector:
+ text:
options:
- description: A dictionary containing platform-specific options. Optional depending on the platform.
+ description:
+ A dictionary containing platform-specific options. Optional depending on
+ the platform.
example: platform specific
clear_cache:
- description: Remove cache files and RAM cache.
+ name: Clear TTS cache
+ description: Remove all text-to-speech cache files and RAM cache.
diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py
index 5876331ea97f26..1f16d131e39eea 100644
--- a/homeassistant/components/tuya/__init__.py
+++ b/homeassistant/components/tuya/__init__.py
@@ -240,9 +240,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(
- entry, component.split(".", 1)[0]
+ entry, platform.split(".", 1)[0]
)
- for component in hass.data[DOMAIN][ENTRY_IS_SETUP]
+ for platform in hass.data[DOMAIN][ENTRY_IS_SETUP]
]
)
)
@@ -392,7 +392,7 @@ async def _delete_callback(self, dev_id):
entity_registry.async_remove(self.entity_id)
await cleanup_device_registry(self.hass, entity_entry.device_id)
else:
- await self.async_remove()
+ await self.async_remove(force_remove=True)
@callback
def _update_callback(self):
diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py
index da851d4a7768d1..73ba69da79786b 100644
--- a/homeassistant/components/tuya/climate.py
+++ b/homeassistant/components/tuya/climate.py
@@ -1,6 +1,5 @@
"""Support for the Tuya climate devices."""
from datetime import timedelta
-import logging
from homeassistant.components.climate import (
DOMAIN as SENSOR_DOMAIN,
@@ -34,7 +33,9 @@
CONF_CURR_TEMP_DIVIDER,
CONF_MAX_TEMP,
CONF_MIN_TEMP,
+ CONF_SET_TEMP_DIVIDED,
CONF_TEMP_DIVIDER,
+ CONF_TEMP_STEP_OVERRIDE,
DOMAIN,
SIGNAL_CONFIG_ENTITY,
TUYA_DATA,
@@ -56,8 +57,6 @@
FAN_MODES = {FAN_LOW, FAN_MEDIUM, FAN_HIGH}
-_LOGGER = logging.getLogger(__name__)
-
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up tuya sensors dynamically through tuya discovery."""
@@ -106,6 +105,8 @@ def __init__(self, tuya, platform):
self.operations = [HVAC_MODE_OFF]
self._has_operation = False
self._def_hvac_mode = HVAC_MODE_AUTO
+ self._set_temp_divided = True
+ self._temp_step_override = None
self._min_temp = None
self._max_temp = None
@@ -120,6 +121,8 @@ def _process_config(self):
self._tuya.set_unit("FAHRENHEIT" if unit == TEMP_FAHRENHEIT else "CELSIUS")
self._tuya.temp_divider = config.get(CONF_TEMP_DIVIDER, 0)
self._tuya.curr_temp_divider = config.get(CONF_CURR_TEMP_DIVIDER, 0)
+ self._set_temp_divided = config.get(CONF_SET_TEMP_DIVIDED, True)
+ self._temp_step_override = config.get(CONF_TEMP_STEP_OVERRIDE)
min_temp = config.get(CONF_MIN_TEMP, 0)
max_temp = config.get(CONF_MAX_TEMP, 0)
if min_temp >= max_temp:
@@ -192,6 +195,8 @@ def target_temperature(self):
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
+ if self._temp_step_override:
+ return self._temp_step_override
return self._tuya.target_temperature_step()
@property
@@ -207,7 +212,7 @@ def fan_modes(self):
def set_temperature(self, **kwargs):
"""Set new target temperature."""
if ATTR_TEMPERATURE in kwargs:
- self._tuya.set_temperature(kwargs[ATTR_TEMPERATURE])
+ self._tuya.set_temperature(kwargs[ATTR_TEMPERATURE], self._set_temp_divided)
def set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py
index 5d22a83e03ef08..18820098a9134a 100644
--- a/homeassistant/components/tuya/config_flow.py
+++ b/homeassistant/components/tuya/config_flow.py
@@ -23,7 +23,6 @@
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-# pylint:disable=unused-import
from .const import (
CONF_BRIGHTNESS_RANGE_MODE,
CONF_COUNTRYCODE,
@@ -35,8 +34,10 @@
CONF_MIN_TEMP,
CONF_QUERY_DEVICE,
CONF_QUERY_INTERVAL,
+ CONF_SET_TEMP_DIVIDED,
CONF_SUPPORT_COLOR,
CONF_TEMP_DIVIDER,
+ CONF_TEMP_STEP_OVERRIDE,
CONF_TUYA_MAX_COLTEMP,
DEFAULT_DISCOVERY_INTERVAL,
DEFAULT_QUERY_INTERVAL,
@@ -66,6 +67,7 @@
RESULT_AUTH_FAILED = "invalid_auth"
RESULT_CONN_ERROR = "cannot_connect"
+RESULT_SINGLE_INSTANCE = "single_instance_allowed"
RESULT_SUCCESS = "success"
RESULT_LOG_MESSAGE = {
@@ -123,7 +125,7 @@ async def async_step_import(self, user_input=None):
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if self._async_current_entries():
- return self.async_abort(reason="single_instance_allowed")
+ return self.async_abort(reason=RESULT_SINGLE_INSTANCE)
errors = {}
@@ -257,7 +259,7 @@ async def async_step_init(self, user_input=None):
if self.config_entry.state != config_entries.ENTRY_STATE_LOADED:
_LOGGER.error("Tuya integration not yet loaded")
- return self.async_abort(reason="cannot_connect")
+ return self.async_abort(reason=RESULT_CONN_ERROR)
if user_input is not None:
dev_ids = user_input.get(CONF_LIST_DEVICES)
@@ -323,11 +325,14 @@ async def async_step_device(self, user_input=None, dev_ids=None):
def _get_device_schema(self, device_type, curr_conf, device):
"""Return option schema for device."""
+ if device_type != device.device_type():
+ return None
+ schema = None
if device_type == "light":
- return self._get_light_schema(curr_conf, device)
- if device_type == "climate":
- return self._get_climate_schema(curr_conf, device)
- return None
+ schema = self._get_light_schema(curr_conf, device)
+ elif device_type == "climate":
+ schema = self._get_climate_schema(curr_conf, device)
+ return schema
@staticmethod
def _get_light_schema(curr_conf, device):
@@ -374,6 +379,8 @@ def _get_climate_schema(curr_conf, device):
"""Create option schema for climate device."""
unit = device.temperature_unit()
def_unit = TEMP_FAHRENHEIT if unit == "FAHRENHEIT" else TEMP_CELSIUS
+ supported_steps = device.supported_temperature_steps()
+ default_step = device.target_temperature_step()
config_schema = vol.Schema(
{
@@ -389,6 +396,14 @@ def _get_climate_schema(curr_conf, device):
CONF_CURR_TEMP_DIVIDER,
default=curr_conf.get(CONF_CURR_TEMP_DIVIDER, 0),
): vol.All(vol.Coerce(int), vol.Clamp(min=0)),
+ vol.Optional(
+ CONF_SET_TEMP_DIVIDED,
+ default=curr_conf.get(CONF_SET_TEMP_DIVIDED, True),
+ ): bool,
+ vol.Optional(
+ CONF_TEMP_STEP_OVERRIDE,
+ default=curr_conf.get(CONF_TEMP_STEP_OVERRIDE, default_step),
+ ): vol.In(supported_steps),
vol.Optional(
CONF_MIN_TEMP,
default=curr_conf.get(CONF_MIN_TEMP, 0),
diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py
index 4f4ec342b152a2..646bcc077cfa42 100644
--- a/homeassistant/components/tuya/const.py
+++ b/homeassistant/components/tuya/const.py
@@ -10,8 +10,10 @@
CONF_MIN_TEMP = "min_temp"
CONF_QUERY_DEVICE = "query_device"
CONF_QUERY_INTERVAL = "query_interval"
+CONF_SET_TEMP_DIVIDED = "set_temp_divided"
CONF_SUPPORT_COLOR = "support_color"
CONF_TEMP_DIVIDER = "temp_divider"
+CONF_TEMP_STEP_OVERRIDE = "temp_step_override"
CONF_TUYA_MAX_COLTEMP = "tuya_max_coltemp"
DEFAULT_DISCOVERY_INTERVAL = 605
diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py
index a66e1ff92a443a..ab361c6ac31d5e 100644
--- a/homeassistant/components/tuya/fan.py
+++ b/homeassistant/components/tuya/fan.py
@@ -1,4 +1,6 @@
"""Support for Tuya fans."""
+from __future__ import annotations
+
from datetime import timedelta
from homeassistant.components.fan import (
@@ -10,6 +12,10 @@
)
from homeassistant.const import CONF_PLATFORM, STATE_OFF
from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.util.percentage import (
+ ordered_list_item_to_percentage,
+ percentage_to_ordered_list_item,
+)
from . import TuyaDevice
from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW
@@ -61,27 +67,21 @@ def __init__(self, tuya, platform):
"""Init Tuya fan device."""
super().__init__(tuya, platform)
self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id())
- self.speeds = [STATE_OFF]
+ self.speeds = []
async def async_added_to_hass(self):
"""Create fan list when add to hass."""
await super().async_added_to_hass()
self.speeds.extend(self._tuya.speed_list())
- def set_speed(self, speed: str) -> None:
- """Set the speed of the fan."""
- if speed == STATE_OFF:
+ def set_percentage(self, percentage: int) -> None:
+ """Set the speed percentage of the fan."""
+ if percentage == 0:
self.turn_off()
else:
- self._tuya.set_speed(speed)
-
- #
- # The fan entity model has changed to use percentages and preset_modes
- # instead of speeds.
- #
- # Please review
- # https://developers.home-assistant.io/docs/core/entity/fan/
- #
+ tuya_speed = percentage_to_ordered_list_item(self.speeds, percentage)
+ self._tuya.set_speed(tuya_speed)
+
def turn_on(
self,
speed: str = None,
@@ -90,8 +90,8 @@ def turn_on(
**kwargs,
) -> None:
"""Turn on the fan."""
- if speed is not None:
- self.set_speed(speed)
+ if percentage is not None:
+ self.set_percentage(percentage)
else:
self._tuya.turn_on()
@@ -103,6 +103,13 @@ def oscillate(self, oscillating) -> None:
"""Oscillate the fan."""
self._tuya.oscillate(oscillating)
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ if self.speeds is None:
+ return super().speed_count
+ return len(self.speeds)
+
@property
def oscillating(self):
"""Return current oscillating status."""
@@ -118,16 +125,13 @@ def is_on(self):
return self._tuya.state()
@property
- def speed(self) -> str:
+ def percentage(self) -> int | None:
"""Return the current speed."""
- if self.is_on:
- return self._tuya.speed()
- return STATE_OFF
-
- @property
- def speed_list(self) -> list:
- """Get the list of available speeds."""
- return self.speeds
+ if not self.is_on:
+ return 0
+ if self.speeds is None:
+ return None
+ return ordered_list_item_to_percentage(self.speeds, self._tuya.speed())
@property
def supported_features(self) -> int:
diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json
index 7481e56f00af73..e72c7c63112cce 100644
--- a/homeassistant/components/tuya/manifest.json
+++ b/homeassistant/components/tuya/manifest.json
@@ -2,7 +2,7 @@
"domain": "tuya",
"name": "Tuya",
"documentation": "https://www.home-assistant.io/integrations/tuya",
- "requirements": ["tuyaha==0.0.9"],
+ "requirements": ["tuyaha==0.0.10"],
"codeowners": ["@ollo69"],
"config_flow": true
}
diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json
index 444ff0b5c21180..23958349b66cc9 100644
--- a/homeassistant/components/tuya/strings.json
+++ b/homeassistant/components/tuya/strings.json
@@ -49,6 +49,8 @@
"unit_of_measurement": "Temperature unit used by device",
"temp_divider": "Temperature values divider (0 = use default)",
"curr_temp_divider": "Current Temperature value divider (0 = use default)",
+ "set_temp_divided": "Use divided Temperature value for set temperature command",
+ "temp_step_override": "Target Temperature step",
"min_temp": "Min target temperature (use min and max = 0 for default)",
"max_temp": "Max target temperature (use min and max = 0 for default)"
}
diff --git a/homeassistant/components/tuya/translations/ca.json b/homeassistant/components/tuya/translations/ca.json
index 908cf287eebe83..a00d9683141f1f 100644
--- a/homeassistant/components/tuya/translations/ca.json
+++ b/homeassistant/components/tuya/translations/ca.json
@@ -40,8 +40,10 @@
"max_temp": "Temperatura desitjada m\u00e0xima (utilitza min i max = 0 per defecte)",
"min_kelvin": "Temperatura del color m\u00ednima suportada, en Kelvin",
"min_temp": "Temperatura desitjada m\u00ednima (utilitza min i max = 0 per defecte)",
+ "set_temp_divided": "Utilitza el valor de temperatura dividit per a ordres de configuraci\u00f3 de temperatura",
"support_color": "For\u00e7a el suport de color",
"temp_divider": "Divisor del valor de temperatura (0 = predeterminat)",
+ "temp_step_override": "Pas de temperatura objectiu",
"tuya_max_coltemp": "Temperatura de color m\u00e0xima enviada pel dispositiu",
"unit_of_measurement": "Unitat de temperatura utilitzada pel dispositiu"
},
diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json
index 4cdcdfced79b60..c16a945200ec54 100644
--- a/homeassistant/components/tuya/translations/de.json
+++ b/homeassistant/components/tuya/translations/de.json
@@ -1,17 +1,23 @@
{
"config": {
"abort": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
"single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
+ "error": {
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
+ },
"flow_title": "Tuya Konfiguration",
"step": {
"user": {
"data": {
+ "country_code": "L\u00e4ndercode Ihres Kontos (z. B. 1 f\u00fcr USA oder 86 f\u00fcr China)",
"password": "Passwort",
+ "platform": "Die App, in der Ihr Konto registriert ist",
"username": "Benutzername"
},
- "description": "Geben Sie Ihre Tuya-Anmeldeinformationen ein.",
+ "description": "Gib deine Tuya-Anmeldeinformationen ein.",
"title": "Tuya"
}
}
@@ -23,6 +29,20 @@
"error": {
"dev_not_config": "Ger\u00e4tetyp nicht konfigurierbar",
"dev_not_found": "Ger\u00e4t nicht gefunden"
+ },
+ "step": {
+ "device": {
+ "data": {
+ "brightness_range_mode": "Vom Ger\u00e4t genutzter Helligkeitsbereich",
+ "max_kelvin": "Maximal unterst\u00fctzte Farbtemperatur in Kelvin",
+ "max_temp": "Maximale Solltemperatur (f\u00fcr Voreinstellung min und max = 0 verwenden)",
+ "min_kelvin": "Minimale unterst\u00fctzte Farbtemperatur in Kelvin",
+ "min_temp": "Minimal Solltemperatur (f\u00fcr Voreinstellung min und max = 0 verwenden)",
+ "support_color": "Farbunterst\u00fctzung erzwingen",
+ "tuya_max_coltemp": "Vom Ger\u00e4t gemeldete maximale Farbtemperatur",
+ "unit_of_measurement": "Vom Ger\u00e4t verwendete Temperatureinheit"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json
index 46756b18cb855b..7204d6072a96d0 100644
--- a/homeassistant/components/tuya/translations/en.json
+++ b/homeassistant/components/tuya/translations/en.json
@@ -40,8 +40,10 @@
"max_temp": "Max target temperature (use min and max = 0 for default)",
"min_kelvin": "Min color temperature supported in kelvin",
"min_temp": "Min target temperature (use min and max = 0 for default)",
+ "set_temp_divided": "Use divided Temperature value for set temperature command",
"support_color": "Force color support",
"temp_divider": "Temperature values divider (0 = use default)",
+ "temp_step_override": "Target Temperature step",
"tuya_max_coltemp": "Max color temperature reported by device",
"unit_of_measurement": "Temperature unit used by device"
},
diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json
index cd8da781870557..9c57a2168880b2 100644
--- a/homeassistant/components/tuya/translations/es.json
+++ b/homeassistant/components/tuya/translations/es.json
@@ -40,8 +40,10 @@
"max_temp": "Temperatura objetivo m\u00e1xima (usa m\u00edn. y m\u00e1x. = 0 por defecto)",
"min_kelvin": "Temperatura de color m\u00ednima soportada en kelvin",
"min_temp": "Temperatura objetivo m\u00ednima (usa m\u00edn. y m\u00e1x. = 0 por defecto)",
+ "set_temp_divided": "Use el valor de temperatura dividido para el comando de temperatura establecida",
"support_color": "Forzar soporte de color",
"temp_divider": "Divisor de los valores de temperatura (0 = usar valor por defecto)",
+ "temp_step_override": "Temperatura deseada",
"tuya_max_coltemp": "Temperatura de color m\u00e1xima notificada por dispositivo",
"unit_of_measurement": "Unidad de temperatura utilizada por el dispositivo"
},
diff --git a/homeassistant/components/tuya/translations/et.json b/homeassistant/components/tuya/translations/et.json
index 967b38cdb82330..48161f552b8284 100644
--- a/homeassistant/components/tuya/translations/et.json
+++ b/homeassistant/components/tuya/translations/et.json
@@ -12,9 +12,9 @@
"step": {
"user": {
"data": {
- "country_code": "Teie konto riigikood (nt 1 USA v\u00f5i 372 Eesti)",
+ "country_code": "Konto riigikood (nt 1 USA v\u00f5i 372 Eesti)",
"password": "Salas\u00f5na",
- "platform": "\u00c4pp kus teie konto registreeriti",
+ "platform": "\u00c4pp kus konto registreeriti",
"username": "Kasutajanimi"
},
"description": "Sisesta oma Tuya konto andmed.",
@@ -40,8 +40,10 @@
"max_temp": "Maksimaalne sihttemperatuur (vaikimisi kasuta min ja max = 0)",
"min_kelvin": "Minimaalne v\u00f5imalik v\u00e4rvitemperatuur (Kelvinites)",
"min_temp": "Minimaalne sihttemperatuur (vaikimisi kasuta min ja max = 0)",
+ "set_temp_divided": "M\u00e4\u00e4ratud temperatuuri k\u00e4su jaoks kasuta jagatud temperatuuri v\u00e4\u00e4rtust",
"support_color": "Luba v\u00e4rvuse juhtimine",
"temp_divider": "Temperatuuri v\u00e4\u00e4rtuse eraldaja (0 = kasuta vaikev\u00e4\u00e4rtust)",
+ "temp_step_override": "Sihttemperatuuri samm",
"tuya_max_coltemp": "Seadme teatatud maksimaalne v\u00e4rvitemperatuur",
"unit_of_measurement": "Seadme temperatuuri\u00fchik"
},
diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json
index 9ef1c325d1e7a2..1681343f3b756c 100644
--- a/homeassistant/components/tuya/translations/fr.json
+++ b/homeassistant/components/tuya/translations/fr.json
@@ -40,8 +40,10 @@
"max_temp": "Temp\u00e9rature cible maximale (utilisez min et max = 0 par d\u00e9faut)",
"min_kelvin": "Temp\u00e9rature de couleur minimale prise en charge en kelvin",
"min_temp": "Temp\u00e9rature cible minimale (utilisez min et max = 0 par d\u00e9faut)",
+ "set_temp_divided": "Utilisez la valeur de temp\u00e9rature divis\u00e9e pour la commande de temp\u00e9rature d\u00e9finie",
"support_color": "Forcer la prise en charge des couleurs",
"temp_divider": "Diviseur de valeurs de temp\u00e9rature (0 = utiliser la valeur par d\u00e9faut)",
+ "temp_step_override": "Pas de temp\u00e9rature cible",
"tuya_max_coltemp": "Temp\u00e9rature de couleur maximale rapport\u00e9e par l'appareil",
"unit_of_measurement": "Unit\u00e9 de temp\u00e9rature utilis\u00e9e par l'appareil"
},
diff --git a/homeassistant/components/tuya/translations/hu.json b/homeassistant/components/tuya/translations/hu.json
index b128be67087541..b45148f80b8d3d 100644
--- a/homeassistant/components/tuya/translations/hu.json
+++ b/homeassistant/components/tuya/translations/hu.json
@@ -2,11 +2,11 @@
"config": {
"abort": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
- "invalid_auth": "\u00c9rv\u00e9nytelen autentik\u00e1ci\u00f3",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
},
"error": {
- "invalid_auth": "\u00c9rv\u00e9nytelen autentik\u00e1ci\u00f3"
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
},
"flow_title": "Tuya konfigur\u00e1ci\u00f3",
"step": {
@@ -27,7 +27,7 @@
"cannot_connect": "A kapcsol\u00f3d\u00e1s nem siker\u00fclt"
},
"error": {
- "dev_multi_type": "A konfigur\u00e1land\u00f3 eszk\u00f6z\u00f6knek azonos t\u00edpus\u00faaknak kell lennie",
+ "dev_multi_type": "T\u00f6bb kiv\u00e1lasztott konfigur\u00e1land\u00f3 eszk\u00f6znek azonos t\u00edpus\u00fanak kell lennie",
"dev_not_config": "Ez az eszk\u00f6zt\u00edpus nem konfigur\u00e1lhat\u00f3",
"dev_not_found": "Eszk\u00f6z nem tal\u00e1lhat\u00f3"
},
@@ -45,18 +45,18 @@
"tuya_max_coltemp": "Az eszk\u00f6z \u00e1ltal megadott maxim\u00e1lis sz\u00ednh\u0151m\u00e9rs\u00e9klet",
"unit_of_measurement": "Az eszk\u00f6z \u00e1ltal haszn\u00e1lt h\u0151m\u00e9rs\u00e9kleti egys\u00e9g"
},
- "description": "Konfigur\u00e1lja a(z) {device_type} eszk\u00f6zt \" {device_name} {device_type} \" megjelen\u00edtett inform\u00e1ci\u00f3inak be\u00e1ll\u00edt\u00e1s\u00e1hoz",
- "title": "Konfigur\u00e1lja a Tuya eszk\u00f6zt"
+ "description": "Konfigur\u00e1l\u00e1si lehet\u0151s\u00e9gek a(z) {device_type} t\u00edpus\u00fa `{device_name}` eszk\u00f6z megjelen\u00edtett inform\u00e1ci\u00f3inak be\u00e1ll\u00edt\u00e1s\u00e1hoz",
+ "title": "Tuya eszk\u00f6z konfigur\u00e1l\u00e1sa"
},
"init": {
"data": {
"discovery_interval": "Felfedez\u0151 eszk\u00f6z lek\u00e9rdez\u00e9si intervalluma m\u00e1sodpercben",
- "list_devices": "V\u00e1lassza ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6z\u00f6ket, vagy hagyja \u00fcresen a konfigur\u00e1ci\u00f3 ment\u00e9s\u00e9hez",
- "query_device": "V\u00e1lassza ki azt az eszk\u00f6zt, amely a lek\u00e9rdez\u00e9si m\u00f3dszert haszn\u00e1lja a gyorsabb \u00e1llapotfriss\u00edt\u00e9shez",
+ "list_devices": "V\u00e1laszd ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6z\u00f6ket, vagy hagyd \u00fcresen a konfigur\u00e1ci\u00f3 ment\u00e9s\u00e9hez",
+ "query_device": "V\u00e1laszd ki azt az eszk\u00f6zt, amely a lek\u00e9rdez\u00e9si m\u00f3dszert haszn\u00e1lja a gyorsabb \u00e1llapotfriss\u00edt\u00e9shez",
"query_interval": "Eszk\u00f6z lek\u00e9rdez\u00e9si id\u0151k\u00f6ze m\u00e1sodpercben"
},
- "description": "Ne \u00e1ll\u00edtsa t\u00fal alacsonyra a lek\u00e9rdez\u00e9si intervallum \u00e9rt\u00e9keit, k\u00fcl\u00f6nben a h\u00edv\u00e1sok nem fognak hiba\u00fczenetet gener\u00e1lni a napl\u00f3ban",
- "title": "Konfigur\u00e1lja a Tuya be\u00e1ll\u00edt\u00e1sokat"
+ "description": "Ne \u00e1ll\u00edtsd t\u00fal alacsonyra a lek\u00e9rdez\u00e9si intervallum \u00e9rt\u00e9keit, k\u00fcl\u00f6nben a h\u00edv\u00e1sok nem fognak hiba\u00fczenetet gener\u00e1lni a napl\u00f3ban",
+ "title": "Tuya be\u00e1ll\u00edt\u00e1sok konfigur\u00e1l\u00e1sa"
}
}
}
diff --git a/homeassistant/components/tuya/translations/id.json b/homeassistant/components/tuya/translations/id.json
new file mode 100644
index 00000000000000..bb338e12752879
--- /dev/null
+++ b/homeassistant/components/tuya/translations/id.json
@@ -0,0 +1,65 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "flow_title": "Konfigurasi Tuya",
+ "step": {
+ "user": {
+ "data": {
+ "country_code": "Kode negara akun Anda (mis., 1 untuk AS atau 86 untuk China)",
+ "password": "Kata Sandi",
+ "platform": "Aplikasi tempat akun Anda mendaftar",
+ "username": "Nama Pengguna"
+ },
+ "description": "Masukkan kredensial Tuya Anda.",
+ "title": "Tuya"
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "error": {
+ "dev_multi_type": "Untuk konfigurasi sekaligus, beberapa perangkat yang dipilih harus berjenis sama",
+ "dev_not_config": "Jenis perangkat tidak dapat dikonfigurasi",
+ "dev_not_found": "Perangkat tidak ditemukan"
+ },
+ "step": {
+ "device": {
+ "data": {
+ "brightness_range_mode": "Rentang kecerahan yang digunakan oleh perangkat",
+ "curr_temp_divider": "Pembagi nilai suhu saat ini (0 = gunakan bawaan)",
+ "max_kelvin": "Suhu warna maksimal yang didukung dalam Kelvin",
+ "max_temp": "Suhu target maksimal (gunakan min dan maks = 0 untuk bawaan)",
+ "min_kelvin": "Suhu warna minimal yang didukung dalam Kelvin",
+ "min_temp": "Suhu target minimal (gunakan min dan maks = 0 untuk bawaan)",
+ "set_temp_divided": "Gunakan nilai suhu terbagi untuk mengirimkan perintah mengatur suhu",
+ "support_color": "Paksa dukungan warna",
+ "temp_divider": "Pembagi nilai suhu (0 = gunakan bawaan)",
+ "temp_step_override": "Langkah Suhu Target",
+ "tuya_max_coltemp": "Suhu warna maksimal yang dilaporkan oleh perangkat",
+ "unit_of_measurement": "Satuan suhu yang digunakan oleh perangkat"
+ },
+ "description": "Konfigurasikan opsi untuk menyesuaikan informasi yang ditampilkan untuk perangkat {device_type} `{device_name}`",
+ "title": "Konfigurasi Perangkat Tuya"
+ },
+ "init": {
+ "data": {
+ "discovery_interval": "Interval polling penemuan perangkat dalam detik",
+ "list_devices": "Pilih perangkat yang akan dikonfigurasi atau biarkan kosong untuk menyimpan konfigurasi",
+ "query_device": "Pilih perangkat yang akan menggunakan metode kueri untuk pembaruan status lebih cepat",
+ "query_interval": "Interval polling perangkat kueri dalam detik"
+ },
+ "description": "Jangan atur nilai interval polling terlalu rendah karena panggilan akan gagal menghasilkan pesan kesalahan dalam log",
+ "title": "Konfigurasikan Opsi Tuya"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tuya/translations/it.json b/homeassistant/components/tuya/translations/it.json
index 639f58349226a0..729514d35416a1 100644
--- a/homeassistant/components/tuya/translations/it.json
+++ b/homeassistant/components/tuya/translations/it.json
@@ -40,8 +40,10 @@
"max_temp": "Temperatura di destinazione massima (utilizzare min e max = 0 per impostazione predefinita)",
"min_kelvin": "Temperatura colore minima supportata in kelvin",
"min_temp": "Temperatura di destinazione minima (utilizzare min e max = 0 per impostazione predefinita)",
+ "set_temp_divided": "Utilizzare il valore temperatura diviso per impostare il comando temperatura",
"support_color": "Forza il supporto del colore",
"temp_divider": "Divisore dei valori di temperatura (0 = utilizzare il valore predefinito)",
+ "temp_step_override": "Passo della temperatura da raggiungere",
"tuya_max_coltemp": "Temperatura di colore massima riportata dal dispositivo",
"unit_of_measurement": "Unit\u00e0 di temperatura utilizzata dal dispositivo"
},
diff --git a/homeassistant/components/tuya/translations/ko.json b/homeassistant/components/tuya/translations/ko.json
index e123bc2b6f9921..afa2541e7b9e0d 100644
--- a/homeassistant/components/tuya/translations/ko.json
+++ b/homeassistant/components/tuya/translations/ko.json
@@ -1,7 +1,12 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4."
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"flow_title": "Tuya \uad6c\uc131\ud558\uae30",
"step": {
@@ -16,5 +21,45 @@
"title": "Tuya"
}
}
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "dev_multi_type": "\uc120\ud0dd\ud55c \uc5ec\ub7ec \uae30\uae30\ub97c \uad6c\uc131\ud558\ub824\uba74 \uc720\ud615\uc774 \ub3d9\uc77c\ud574\uc57c \ud569\ub2c8\ub2e4",
+ "dev_not_config": "\uae30\uae30 \uc720\ud615\uc744 \uad6c\uc131\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "dev_not_found": "\uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "device": {
+ "data": {
+ "brightness_range_mode": "\uae30\uae30\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \ubc1d\uae30 \ubc94\uc704",
+ "curr_temp_divider": "\ud604\uc7ac \uc628\ub3c4 \uac12 \ubd84\ud560 (0 = \uae30\ubcf8\uac12 \uc0ac\uc6a9)",
+ "max_kelvin": "\uce98\ube48 \ub2e8\uc704\uc758 \ucd5c\ub300 \uc0c9\uc628\ub3c4",
+ "max_temp": "\ucd5c\ub300 \ubaa9\ud45c \uc628\ub3c4 (\uae30\ubcf8\uac12\uc758 \uacbd\uc6b0 \ucd5c\uc19f\uac12 \ubc0f \ucd5c\ub313\uac12 = 0)",
+ "min_kelvin": "\uce98\ube48 \ub2e8\uc704\uc758 \ucd5c\uc18c \uc0c9\uc628\ub3c4",
+ "min_temp": "\ucd5c\uc18c \ubaa9\ud45c \uc628\ub3c4 (\uae30\ubcf8\uac12\uc758 \uacbd\uc6b0 \ucd5c\uc19f\uac12 \ubc0f \ucd5c\ub313\uac12 = 0)",
+ "set_temp_divided": "\uc124\uc815 \uc628\ub3c4 \uba85\ub839\uc5d0 \ubd84\ud560\ub41c \uc628\ub3c4 \uac12 \uc0ac\uc6a9\ud558\uae30",
+ "support_color": "\uc0c9\uc0c1 \uc9c0\uc6d0 \uac15\uc81c \uc801\uc6a9\ud558\uae30",
+ "temp_divider": "\uc628\ub3c4 \uac12 \ubd84\ud560 (0 = \uae30\ubcf8\uac12 \uc0ac\uc6a9)",
+ "temp_step_override": "\ud76c\ub9dd \uc628\ub3c4 \ub2e8\uacc4",
+ "tuya_max_coltemp": "\uae30\uae30\uc5d0\uc11c \ubcf4\uace0\ud55c \ucd5c\ub300 \uc0c9\uc628\ub3c4",
+ "unit_of_measurement": "\uae30\uae30\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \uc628\ub3c4 \ub2e8\uc704"
+ },
+ "description": "{device_type} `{device_name}` \uae30\uae30\uc5d0 \ub300\ud574 \ud45c\uc2dc\ub418\ub294 \uc815\ubcf4\ub97c \uc870\uc815\ud558\ub294 \uc635\uc158 \uad6c\uc131\ud558\uae30",
+ "title": "Tuya \uae30\uae30 \uad6c\uc131\ud558\uae30"
+ },
+ "init": {
+ "data": {
+ "discovery_interval": "\uae30\uae30 \uac80\uc0c9 \ud3f4\ub9c1 \uac04\uaca9 (\ucd08)",
+ "list_devices": "\uad6c\uc131\uc744 \uc800\uc7a5\ud558\ub824\uba74 \uad6c\uc131\ud560 \uae30\uae30\ub97c \uc120\ud0dd\ud558\uac70\ub098 \ube44\uc6cc \ub450\uc138\uc694",
+ "query_device": "\ube60\ub978 \uc0c1\ud0dc \uc5c5\ub370\uc774\ud2b8\ub97c \uc704\ud574 \ucffc\ub9ac \ubc29\ubc95\uc744 \uc0ac\uc6a9\ud560 \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694",
+ "query_interval": "\uae30\uae30 \ucffc\ub9ac \ud3f4\ub9c1 \uac04\uaca9 (\ucd08)"
+ },
+ "description": "\ud3f4\ub9c1 \uac04\uaca9 \uac12\uc744 \ub108\ubb34 \ub0ae\uac8c \uc124\uc815\ud558\uc9c0 \ub9d0\uc544 \uc8fc\uc138\uc694. \uadf8\ub807\uc9c0 \uc54a\uc73c\uba74 \ud638\ucd9c\uc5d0 \uc2e4\ud328\ud558\uace0 \ub85c\uadf8\uc5d0 \uc624\ub958 \uba54\uc2dc\uc9c0\uac00 \uc0dd\uc131\ub429\ub2c8\ub2e4.",
+ "title": "Tuya \uc635\uc158 \uad6c\uc131\ud558\uae30"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/tuya/translations/lb.json b/homeassistant/components/tuya/translations/lb.json
index 884eb328fe4af1..0000f9ef6e62a5 100644
--- a/homeassistant/components/tuya/translations/lb.json
+++ b/homeassistant/components/tuya/translations/lb.json
@@ -23,6 +23,9 @@
}
},
"options": {
+ "abort": {
+ "cannot_connect": "Feeler beim verbannen"
+ },
"error": {
"dev_multi_type": "Multiple ausgewielte Ger\u00e4ter fir ze konfigur\u00e9ieren musse vum selwechten Typ sinn",
"dev_not_config": "Typ vun Apparat net konfigur\u00e9ierbar",
diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json
index 5a0e3691d5b247..b42922822f0feb 100644
--- a/homeassistant/components/tuya/translations/nl.json
+++ b/homeassistant/components/tuya/translations/nl.json
@@ -2,8 +2,12 @@
"config": {
"abort": {
"cannot_connect": "Kan geen verbinding maken",
+ "invalid_auth": "Ongeldige authenticatie",
"single_instance_allowed": "Al geconfigureerd. Er is maar een configuratie mogelijk."
},
+ "error": {
+ "invalid_auth": "Ongeldige authenticatie"
+ },
"flow_title": "Tuya-configuratie",
"step": {
"user": {
@@ -19,8 +23,41 @@
}
},
"options": {
+ "abort": {
+ "cannot_connect": "Kan geen verbinding maken"
+ },
+ "error": {
+ "dev_multi_type": "Meerdere geselecteerde apparaten om te configureren moeten van hetzelfde type zijn",
+ "dev_not_config": "Apparaattype kan niet worden geconfigureerd",
+ "dev_not_found": "Apparaat niet gevonden"
+ },
"step": {
+ "device": {
+ "data": {
+ "brightness_range_mode": "Helderheidsbereik gebruikt door apparaat",
+ "curr_temp_divider": "Huidige temperatuurwaarde deler (0 = standaardwaarde)",
+ "max_kelvin": "Max ondersteunde kleurtemperatuur in kelvin",
+ "max_temp": "Maximale doeltemperatuur (gebruik min en max = 0 voor standaardwaarde)",
+ "min_kelvin": "Minimaal ondersteunde kleurtemperatuur in kelvin",
+ "min_temp": "Min. gewenste temperatuur (gebruik min en max = 0 voor standaard)",
+ "set_temp_divided": "Gedeelde temperatuurwaarde gebruiken voor ingestelde temperatuuropdracht",
+ "support_color": "Forceer kleurenondersteuning",
+ "temp_divider": "Temperatuurwaarde deler (0 = standaardwaarde)",
+ "temp_step_override": "Doeltemperatuur stap",
+ "tuya_max_coltemp": "Max. Kleurtemperatuur gerapporteerd door apparaat",
+ "unit_of_measurement": "Temperatuureenheid gebruikt door apparaat"
+ },
+ "description": "Configureer opties om weergegeven informatie aan te passen voor {device_type} apparaat `{device_name}`",
+ "title": "Configureer Tuya Apparaat"
+ },
"init": {
+ "data": {
+ "discovery_interval": "Polling-interval van ontdekt apparaat in seconden",
+ "list_devices": "Selecteer de te configureren apparaten of laat leeg om de configuratie op te slaan",
+ "query_device": "Selecteer apparaat dat query-methode zal gebruiken voor snellere statusupdate",
+ "query_interval": "Peilinginterval van het apparaat in seconden"
+ },
+ "description": "Stel de waarden voor het pollinginterval niet te laag in, anders zullen de oproepen geen foutmelding in het logboek genereren",
"title": "Configureer Tuya opties"
}
}
diff --git a/homeassistant/components/tuya/translations/no.json b/homeassistant/components/tuya/translations/no.json
index d0c1a3ca1882a1..d02a88f4097c1d 100644
--- a/homeassistant/components/tuya/translations/no.json
+++ b/homeassistant/components/tuya/translations/no.json
@@ -40,8 +40,10 @@
"max_temp": "Maks m\u00e5ltemperatur (bruk min og maks = 0 for standard)",
"min_kelvin": "Min fargetemperatur st\u00f8ttet i kelvin",
"min_temp": "Min m\u00e5ltemperatur (bruk min og maks = 0 for standard)",
+ "set_temp_divided": "Bruk delt temperaturverdi for innstilt temperaturkommando",
"support_color": "Tving fargest\u00f8tte",
"temp_divider": "Deler temperaturverdier (0 = bruk standard)",
+ "temp_step_override": "Trinn for m\u00e5ltemperatur",
"tuya_max_coltemp": "Maks fargetemperatur rapportert av enheten",
"unit_of_measurement": "Temperaturenhet som brukes av enheten"
},
diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json
index a24c1dbe265221..92ced00e733d17 100644
--- a/homeassistant/components/tuya/translations/pl.json
+++ b/homeassistant/components/tuya/translations/pl.json
@@ -40,8 +40,10 @@
"max_temp": "Maksymalna temperatura docelowa (u\u017cyj min i max = 0 dla warto\u015bci domy\u015blnej)",
"min_kelvin": "Minimalna obs\u0142ugiwana temperatura barwy w kelwinach",
"min_temp": "Minimalna temperatura docelowa (u\u017cyj min i max = 0 dla warto\u015bci domy\u015blnej)",
+ "set_temp_divided": "U\u017cyj podzielonej warto\u015bci temperatury dla polecenia ustawienia temperatury",
"support_color": "Wymu\u015b obs\u0142ug\u0119 kolor\u00f3w",
"temp_divider": "Dzielnik warto\u015bci temperatury (0 = u\u017cyj warto\u015bci domy\u015blnej)",
+ "temp_step_override": "Krok docelowej temperatury",
"tuya_max_coltemp": "Maksymalna temperatura barwy raportowana przez urz\u0105dzenie",
"unit_of_measurement": "Jednostka temperatury u\u017cywana przez urz\u0105dzenie"
},
@@ -53,7 +55,7 @@
"discovery_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania nowych urz\u0105dze\u0144 (w sekundach)",
"list_devices": "Wybierz urz\u0105dzenia do skonfigurowania lub pozostaw puste, aby zapisa\u0107 konfiguracj\u0119",
"query_device": "Wybierz urz\u0105dzenie, kt\u00f3re b\u0119dzie u\u017cywa\u0107 metody odpytywania w celu szybszej aktualizacji statusu",
- "query_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania odpytywanego urz\u0105dzenia (w sekundach)"
+ "query_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania odpytywanego urz\u0105dzenia w sekundach"
},
"description": "Nie ustawiaj zbyt niskich warto\u015bci skanowania, bo zako\u0144cz\u0105 si\u0119 niepowodzeniem, generuj\u0105c komunikat o b\u0142\u0119dzie w logu",
"title": "Konfiguracja opcji Tuya"
diff --git a/homeassistant/components/tuya/translations/ru.json b/homeassistant/components/tuya/translations/ru.json
index b98c6c8e9cd0c8..f40071ba400b39 100644
--- a/homeassistant/components/tuya/translations/ru.json
+++ b/homeassistant/components/tuya/translations/ru.json
@@ -2,11 +2,11 @@
"config": {
"abort": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e."
},
"error": {
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"flow_title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Tuya",
"step": {
@@ -15,7 +15,7 @@
"country_code": "\u041a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 (1 \u0434\u043b\u044f \u0421\u0428\u0410 \u0438\u043b\u0438 86 \u0434\u043b\u044f \u041a\u0438\u0442\u0430\u044f)",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"platform": "\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d \u0430\u043a\u043a\u0430\u0443\u043d\u0442",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tuya.",
"title": "Tuya"
@@ -40,8 +40,10 @@
"max_temp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0446\u0435\u043b\u0435\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 min \u0438 max = 0)",
"min_kelvin": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0446\u0432\u0435\u0442\u043e\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0432 \u043a\u0435\u043b\u044c\u0432\u0438\u043d\u0430\u0445)",
"min_temp": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0446\u0435\u043b\u0435\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 min \u0438 max = 0)",
+ "set_temp_divided": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b",
"support_color": "\u041f\u0440\u0438\u043d\u0443\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 \u0446\u0432\u0435\u0442\u0430",
"temp_divider": "\u0414\u0435\u043b\u0438\u0442\u0435\u043b\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b (0 = \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e)",
+ "temp_step_override": "\u0428\u0430\u0433 \u0446\u0435\u043b\u0435\u0432\u043e\u0439 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b",
"tuya_max_coltemp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0446\u0432\u0435\u0442\u043e\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430, \u0441\u043e\u043e\u0431\u0449\u0430\u0435\u043c\u0430\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c",
"unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c"
},
diff --git a/homeassistant/components/tuya/translations/tr.json b/homeassistant/components/tuya/translations/tr.json
index 5a4de08033cf61..2edf3276b6c503 100644
--- a/homeassistant/components/tuya/translations/tr.json
+++ b/homeassistant/components/tuya/translations/tr.json
@@ -1,11 +1,39 @@
{
+ "config": {
+ "abort": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "flow_title": "Tuya yap\u0131land\u0131rmas\u0131",
+ "step": {
+ "user": {
+ "data": {
+ "country_code": "Hesap \u00fclke kodunuz (\u00f6r. ABD i\u00e7in 1 veya \u00c7in i\u00e7in 86)",
+ "password": "Parola",
+ "platform": "Hesab\u0131n\u0131z\u0131n kay\u0131tl\u0131 oldu\u011fu uygulama",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ },
+ "description": "Tuya kimlik bilgilerinizi girin.",
+ "title": "Tuya"
+ }
+ }
+ },
"options": {
"abort": {
"cannot_connect": "Ba\u011flanma hatas\u0131"
},
+ "error": {
+ "dev_not_config": "Cihaz t\u00fcr\u00fc yap\u0131land\u0131r\u0131lamaz",
+ "dev_not_found": "Cihaz bulunamad\u0131"
+ },
"step": {
"device": {
"data": {
+ "brightness_range_mode": "Cihaz\u0131n kulland\u0131\u011f\u0131 parlakl\u0131k aral\u0131\u011f\u0131",
"max_temp": "Maksimum hedef s\u0131cakl\u0131k (varsay\u0131lan olarak min ve maks = 0 kullan\u0131n)",
"min_kelvin": "Kelvin destekli min renk s\u0131cakl\u0131\u011f\u0131",
"min_temp": "Minimum hedef s\u0131cakl\u0131k (varsay\u0131lan i\u00e7in min ve maks = 0 kullan\u0131n)",
diff --git a/homeassistant/components/tuya/translations/uk.json b/homeassistant/components/tuya/translations/uk.json
new file mode 100644
index 00000000000000..1d2709d260a0c6
--- /dev/null
+++ b/homeassistant/components/tuya/translations/uk.json
@@ -0,0 +1,63 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "flow_title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Tuya",
+ "step": {
+ "user": {
+ "data": {
+ "country_code": "\u041a\u043e\u0434 \u043a\u0440\u0430\u0457\u043d\u0438 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 (1 \u0434\u043b\u044f \u0421\u0428\u0410 \u0430\u0431\u043e 86 \u0434\u043b\u044f \u041a\u0438\u0442\u0430\u044e)",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "platform": "\u0414\u043e\u0434\u0430\u0442\u043e\u043a, \u0432 \u044f\u043a\u043e\u043c\u0443 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Tuya.",
+ "title": "Tuya"
+ }
+ }
+ },
+ "options": {
+ "abort": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "error": {
+ "dev_multi_type": "\u041a\u0456\u043b\u044c\u043a\u0430 \u043e\u0431\u0440\u0430\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0442\u0438\u043f\u0443.",
+ "dev_not_config": "\u0422\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f.",
+ "dev_not_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e."
+ },
+ "step": {
+ "device": {
+ "data": {
+ "brightness_range_mode": "\u0414\u0456\u0430\u043f\u0430\u0437\u043e\u043d \u044f\u0441\u043a\u0440\u0430\u0432\u043e\u0441\u0442\u0456, \u044f\u043a\u0438\u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c",
+ "curr_temp_divider": "\u0414\u0456\u043b\u044c\u043d\u0438\u043a \u043f\u043e\u0442\u043e\u0447\u043d\u043e\u0433\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438 (0 = \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c)",
+ "max_kelvin": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0430 \u043a\u043e\u043b\u0456\u0440\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0432 \u043a\u0435\u043b\u044c\u0432\u0456\u043d\u0430\u0445)",
+ "max_temp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u0446\u0456\u043b\u044c\u043e\u0432\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 min \u0456 max = 0)",
+ "min_kelvin": "\u041c\u0456\u043d\u0456\u043c\u0430\u043b\u044c\u043d\u0430 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0430 \u043a\u043e\u043b\u0456\u0440\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0432 \u043a\u0435\u043b\u044c\u0432\u0456\u043d\u0430\u0445)",
+ "min_temp": "\u041c\u0456\u043d\u0456\u043c\u0430\u043b\u044c\u043d\u0430 \u0446\u0456\u043b\u044c\u043e\u0432\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 min \u0456 max = 0)",
+ "support_color": "\u041f\u0440\u0438\u043c\u0443\u0441\u043e\u0432\u0430 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u043a\u0430 \u043a\u043e\u043b\u044c\u043e\u0440\u0443",
+ "temp_divider": "\u0414\u0456\u043b\u044c\u043d\u0438\u043a \u0437\u043d\u0430\u0447\u0435\u043d\u044c \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438 (0 = \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c)",
+ "tuya_max_coltemp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u043a\u043e\u043b\u0456\u0440\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430, \u044f\u043a\u0430 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u044f\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c",
+ "unit_of_measurement": "\u041e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u0443 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438, \u044f\u043a\u0430 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c"
+ },
+ "description": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u0434\u0438\u043c\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u0434\u043b\u044f {device_type} \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e '{device_name}'",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Tuya"
+ },
+ "init": {
+ "data": {
+ "discovery_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)",
+ "list_devices": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0430\u0431\u043e \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c \u0434\u043b\u044f \u0437\u0431\u0435\u0440\u0435\u0436\u0435\u043d\u043d\u044f \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457",
+ "query_device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u044f\u043a\u0438\u0439 \u0431\u0443\u0434\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430\u043f\u0438\u0442\u0443 \u0434\u043b\u044f \u0431\u0456\u043b\u044c\u0448 \u0448\u0432\u0438\u0434\u043a\u043e\u0433\u043e \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0441\u0442\u0430\u0442\u0443\u0441\u0443",
+ "query_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)"
+ },
+ "description": "\u041d\u0435 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u044e\u0439\u0442\u0435 \u0437\u0430\u043d\u0430\u0434\u0442\u043e \u043d\u0438\u0437\u044c\u043a\u0456 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0440\u0432\u0430\u043b\u0443 \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f, \u0456\u043d\u0430\u043a\u0448\u0435 \u0432\u0438\u043a\u043b\u0438\u043a\u0438 \u043d\u0435 \u0431\u0443\u0434\u0443\u0442\u044c \u0433\u0435\u043d\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u043e \u043f\u043e\u043c\u0438\u043b\u043a\u0443 \u0432 \u0436\u0443\u0440\u043d\u0430\u043b\u0456.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Tuya"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json
index 08871c3108e333..7221c86eb6370f 100644
--- a/homeassistant/components/tuya/translations/zh-Hant.json
+++ b/homeassistant/components/tuya/translations/zh-Hant.json
@@ -40,8 +40,10 @@
"max_temp": "\u6700\u9ad8\u76ee\u6a19\u8272\u6eab\uff08\u4f7f\u7528\u6700\u4f4e\u8207\u6700\u9ad8 = 0 \u4f7f\u7528\u9810\u8a2d\uff09",
"min_kelvin": "Kelvin \u652f\u63f4\u6700\u4f4e\u8272\u6eab",
"min_temp": "\u6700\u4f4e\u76ee\u6a19\u8272\u6eab\uff08\u4f7f\u7528\u6700\u4f4e\u8207\u6700\u9ad8 = 0 \u4f7f\u7528\u9810\u8a2d\uff09",
+ "set_temp_divided": "\u4f7f\u7528\u5206\u9694\u865f\u6eab\u5ea6\u503c\u4ee5\u57f7\u884c\u8a2d\u5b9a\u6eab\u5ea6\u6307\u4ee4",
"support_color": "\u5f37\u5236\u8272\u6eab\u652f\u63f4",
"temp_divider": "\u8272\u6eab\u503c\u5206\u914d\u5668\uff080 = \u4f7f\u7528\u9810\u8a2d\uff09",
+ "temp_step_override": "\u76ee\u6a19\u6eab\u5ea6\u8a2d\u5b9a",
"tuya_max_coltemp": "\u88dd\u7f6e\u56de\u5831\u6700\u9ad8\u8272\u6eab",
"unit_of_measurement": "\u88dd\u7f6e\u6240\u4f7f\u7528\u4e4b\u6eab\u5ea6\u55ae\u4f4d"
},
diff --git a/homeassistant/components/twentemilieu/__init__.py b/homeassistant/components/twentemilieu/__init__.py
index 06c2cb27f35780..f53e4463146f47 100644
--- a/homeassistant/components/twentemilieu/__init__.py
+++ b/homeassistant/components/twentemilieu/__init__.py
@@ -1,7 +1,8 @@
"""Support for Twente Milieu."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
-from typing import Optional
from twentemilieu import TwenteMilieu
import voluptuous as vol
@@ -15,11 +16,12 @@
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID
+from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
-from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+from homeassistant.helpers.typing import ConfigType
SCAN_INTERVAL = timedelta(seconds=3600)
@@ -27,9 +29,7 @@
SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_ID): cv.string})
-async def _update_twentemilieu(
- hass: HomeAssistantType, unique_id: Optional[str]
-) -> None:
+async def _update_twentemilieu(hass: HomeAssistant, unique_id: str | None) -> None:
"""Update Twente Milieu."""
if unique_id is not None:
twentemilieu = hass.data[DOMAIN].get(unique_id)
@@ -37,16 +37,15 @@ async def _update_twentemilieu(
await twentemilieu.update()
async_dispatcher_send(hass, DATA_UPDATE, unique_id)
else:
- tasks = []
- for twentemilieu in hass.data[DOMAIN].values():
- tasks.append(twentemilieu.update())
- await asyncio.wait(tasks)
+ await asyncio.wait(
+ [twentemilieu.update() for twentemilieu in hass.data[DOMAIN].values()]
+ )
for uid in hass.data[DOMAIN]:
async_dispatcher_send(hass, DATA_UPDATE, uid)
-async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Twente Milieu components."""
async def update(call) -> None:
@@ -59,7 +58,7 @@ async def update(call) -> None:
return True
-async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Twente Milieu from a config entry."""
session = async_get_clientsession(hass)
twentemilieu = TwenteMilieu(
@@ -85,7 +84,7 @@ async def _interval_update(now=None) -> None:
return True
-async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Twente Milieu config entry."""
await hass.config_entries.async_forward_entry_unload(entry, "sensor")
diff --git a/homeassistant/components/twentemilieu/config_flow.py b/homeassistant/components/twentemilieu/config_flow.py
index 76c9f33b3e9ff8..25cdd57b26d8ec 100644
--- a/homeassistant/components/twentemilieu/config_flow.py
+++ b/homeassistant/components/twentemilieu/config_flow.py
@@ -1,4 +1,8 @@
"""Config flow to configure the Twente Milieu integration."""
+from __future__ import annotations
+
+from typing import Any
+
from twentemilieu import (
TwenteMilieu,
TwenteMilieuAddressError,
@@ -7,25 +11,22 @@
import voluptuous as vol
from homeassistant import config_entries
-from homeassistant.components.twentemilieu.const import (
- CONF_HOUSE_LETTER,
- CONF_HOUSE_NUMBER,
- CONF_POST_CODE,
- DOMAIN,
-)
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_ID
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from .const import CONF_HOUSE_LETTER, CONF_HOUSE_NUMBER, CONF_POST_CODE, DOMAIN
+
-@config_entries.HANDLERS.register(DOMAIN)
-class TwenteMilieuFlowHandler(ConfigFlow):
+class TwenteMilieuFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a Twente Milieu config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
- async def _show_setup_form(self, errors=None):
+ async def _show_setup_form(
+ self, errors: dict[str, str] | None = None
+ ) -> dict[str, Any]:
"""Show the setup form to the user."""
return self.async_show_form(
step_id="user",
@@ -39,7 +40,9 @@ async def _show_setup_form(self, errors=None):
errors=errors or {},
)
- async def async_step_user(self, user_input=None):
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initiated by the user."""
if user_input is None:
return await self._show_setup_form(user_input)
@@ -70,7 +73,7 @@ async def async_step_user(self, user_input=None):
return self.async_abort(reason="already_configured")
return self.async_create_entry(
- title=unique_id,
+ title=str(unique_id),
data={
CONF_ID: unique_id,
CONF_POST_CODE: user_input[CONF_POST_CODE],
diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py
index 92194e1217235e..ad552a4b3415ab 100644
--- a/homeassistant/components/twentemilieu/sensor.py
+++ b/homeassistant/components/twentemilieu/sensor.py
@@ -1,5 +1,7 @@
"""Support for Twente Milieu sensors."""
-from typing import Any, Dict
+from __future__ import annotations
+
+from typing import Any, Callable
from twentemilieu import (
WASTE_TYPE_NON_RECYCLABLE,
@@ -10,20 +12,23 @@
TwenteMilieuConnectionError,
)
-from homeassistant.components.twentemilieu.const import DATA_UPDATE, DOMAIN
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.typing import HomeAssistantType
+
+from .const import DATA_UPDATE, DOMAIN
PARALLEL_UPDATES = 1
async def async_setup_entry(
- hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up Twente Milieu sensor based on a config entry."""
twentemilieu = hass.data[DOMAIN][entry.data[CONF_ID]]
@@ -67,7 +72,7 @@ async def async_setup_entry(
async_add_entities(sensors, True)
-class TwenteMilieuSensor(Entity):
+class TwenteMilieuSensor(SensorEntity):
"""Defines a Twente Milieu sensor."""
def __init__(
@@ -142,7 +147,7 @@ async def async_update(self) -> None:
self._state = next_pickup.date().isoformat()
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about Twente Milieu."""
return {
"identifiers": {(DOMAIN, self._unique_id)},
diff --git a/homeassistant/components/twentemilieu/services.yaml b/homeassistant/components/twentemilieu/services.yaml
index 7a5b1db301d79c..6227bad1b6de5d 100644
--- a/homeassistant/components/twentemilieu/services.yaml
+++ b/homeassistant/components/twentemilieu/services.yaml
@@ -1,6 +1,11 @@
update:
+ name: Update
description: Update all entities with fresh data from Twente Milieu
fields:
id:
+ name: ID
description: Specific unique address ID to update
+ advanced: true
example: 1300012345
+ selector:
+ text:
diff --git a/homeassistant/components/twentemilieu/translations/de.json b/homeassistant/components/twentemilieu/translations/de.json
index 27ba9bb29c7457..38cabb6c22ec37 100644
--- a/homeassistant/components/twentemilieu/translations/de.json
+++ b/homeassistant/components/twentemilieu/translations/de.json
@@ -1,7 +1,10 @@
{
"config": {
+ "abort": {
+ "already_configured": "Standort ist bereits konfiguriert"
+ },
"error": {
- "cannot_connect": "Verbindungsfehler",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_address": "Adresse nicht im Einzugsgebiet von Twente Milieu gefunden."
},
"step": {
diff --git a/homeassistant/components/twentemilieu/translations/hu.json b/homeassistant/components/twentemilieu/translations/hu.json
index 8f88f82f2e5271..df83a29ec22c8c 100644
--- a/homeassistant/components/twentemilieu/translations/hu.json
+++ b/homeassistant/components/twentemilieu/translations/hu.json
@@ -1,5 +1,11 @@
{
"config": {
+ "abort": {
+ "already_configured": "A hely m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/twentemilieu/translations/id.json b/homeassistant/components/twentemilieu/translations/id.json
new file mode 100644
index 00000000000000..38746dfd12f0c6
--- /dev/null
+++ b/homeassistant/components/twentemilieu/translations/id.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Lokasi sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_address": "Alamat tidak ditemukan di area layanan Twente Milieu."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "house_letter": "Abjad rumah/tambahan",
+ "house_number": "Nomor rumah",
+ "post_code": "Kode pos"
+ },
+ "description": "Siapkan Twente Milieu untuk memberikan informasi pengumpulan sampah di alamat Anda.",
+ "title": "Twente Milieu"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twentemilieu/translations/ko.json b/homeassistant/components/twentemilieu/translations/ko.json
index 3efc227abf7203..a27df565f1b425 100644
--- a/homeassistant/components/twentemilieu/translations/ko.json
+++ b/homeassistant/components/twentemilieu/translations/ko.json
@@ -1,6 +1,10 @@
{
"config": {
+ "abort": {
+ "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
"error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_address": "Twente Milieu \uc11c\ube44\uc2a4 \uc9c0\uc5ed\uc5d0\uc11c \uc8fc\uc18c\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
},
"step": {
diff --git a/homeassistant/components/twentemilieu/translations/nl.json b/homeassistant/components/twentemilieu/translations/nl.json
index ca5abd7e37ca59..54611aa9ab89f7 100644
--- a/homeassistant/components/twentemilieu/translations/nl.json
+++ b/homeassistant/components/twentemilieu/translations/nl.json
@@ -4,6 +4,7 @@
"already_configured": "Locatie is al geconfigureerd"
},
"error": {
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_address": "Adres niet gevonden in servicegebied Twente Milieu."
},
"step": {
diff --git a/homeassistant/components/twentemilieu/translations/tr.json b/homeassistant/components/twentemilieu/translations/tr.json
new file mode 100644
index 00000000000000..590aec1894cc3b
--- /dev/null
+++ b/homeassistant/components/twentemilieu/translations/tr.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twentemilieu/translations/uk.json b/homeassistant/components/twentemilieu/translations/uk.json
new file mode 100644
index 00000000000000..435bd79fb85feb
--- /dev/null
+++ b/homeassistant/components/twentemilieu/translations/uk.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_address": "\u0410\u0434\u0440\u0435\u0441\u0443 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0432 \u0437\u043e\u043d\u0456 \u043e\u0431\u0441\u043b\u0443\u0433\u043e\u0432\u0443\u0432\u0430\u043d\u043d\u044f Twente Milieu."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "house_letter": "\u0414\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f \u0434\u043e \u043d\u043e\u043c\u0435\u0440\u0443 \u0434\u043e\u043c\u0443",
+ "house_number": "\u041d\u043e\u043c\u0435\u0440 \u0431\u0443\u0434\u0438\u043d\u043a\u0443",
+ "post_code": "\u041f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Twente Milieu \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0432\u0438\u0432\u0435\u0437\u0435\u043d\u043d\u044f \u0441\u043c\u0456\u0442\u0442\u044f \u0437\u0430 \u0412\u0430\u0448\u043e\u044e \u0430\u0434\u0440\u0435\u0441\u043e\u044e.",
+ "title": "Twente Milieu"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twilio/translations/de.json b/homeassistant/components/twilio/translations/de.json
index 864fee4c238f22..d9f071e8ff7ad0 100644
--- a/homeassistant/components/twilio/translations/de.json
+++ b/homeassistant/components/twilio/translations/de.json
@@ -1,11 +1,15 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.",
+ "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen."
+ },
"create_entry": {
- "default": "Um Ereignisse an den Home Assistant zu senden, musst du [Webhooks mit Twilio]({twilio_url}) einrichten. \n\n F\u00fclle die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / x-www-form-urlencoded \n\nLies in der [Dokumentation]({docs_url}) wie du Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurierst."
+ "default": "Um Ereignisse an Home Assistant zu senden, musst du [Webhooks mit Twilio]({twilio_url}) einrichten. \n\n F\u00fclle die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / x-www-form-urlencoded \n\nLies in der [Dokumentation]({docs_url}), wie du Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurierst."
},
"step": {
"user": {
- "description": "M\u00f6chtest du Twilio wirklich einrichten?",
+ "description": "M\u00f6chten Sie mit der Einrichtung beginnen?",
"title": "Twilio-Webhook einrichten"
}
}
diff --git a/homeassistant/components/twilio/translations/hu.json b/homeassistant/components/twilio/translations/hu.json
index 913e3d2a7a287c..cd60890dab37c9 100644
--- a/homeassistant/components/twilio/translations/hu.json
+++ b/homeassistant/components/twilio/translations/hu.json
@@ -1,11 +1,15 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.",
+ "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz."
+ },
"create_entry": {
- "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Webhooks Twilio-val] ( {twilio_url} ) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/x-www-form-urlencoded \n\n L\u00e1sd [a dokument\u00e1ci\u00f3] ( {docs_url} ), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re."
+ "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Webhooks Twilio-val]({twilio_url}) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/x-www-form-urlencoded \n\n L\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re."
},
"step": {
"user": {
- "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Twilio-t?",
+ "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?",
"title": "A Twilio Webhook be\u00e1ll\u00edt\u00e1sa"
}
}
diff --git a/homeassistant/components/twilio/translations/id.json b/homeassistant/components/twilio/translations/id.json
new file mode 100644
index 00000000000000..be16b1d4802cdf
--- /dev/null
+++ b/homeassistant/components/twilio/translations/id.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.",
+ "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook."
+ },
+ "create_entry": {
+ "default": "Untuk mengirim event ke Home Assistant, Anda harus menyiapkan [Webhooks dengan Twilio]({twilio_url}).\n\nIsikan info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content-Type: application/x-www-form-urlencoded\n\nBaca [dokumentasi]({docs_url}) tentang cara mengonfigurasi otomasi untuk menangani data masuk."
+ },
+ "step": {
+ "user": {
+ "description": "Ingin memulai penyiapan?",
+ "title": "Siapkan Twilio Webhook"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twilio/translations/ko.json b/homeassistant/components/twilio/translations/ko.json
index b6be32e1de4190..aeb7c09474dec1 100644
--- a/homeassistant/components/twilio/translations/ko.json
+++ b/homeassistant/components/twilio/translations/ko.json
@@ -1,11 +1,15 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4."
+ },
"create_entry": {
- "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Twilio \uc6f9 \ud6c5]({twilio_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/x-www-form-urlencoded\n \nHome Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
+ "default": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Twilio \uc6f9 \ud6c5]({twilio_url})\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/x-www-form-urlencoded\n \nHome Assistant\ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
},
"step": {
"user": {
- "description": "Twilio \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "Twilio \uc6f9 \ud6c5 \uc124\uc815\ud558\uae30"
}
}
diff --git a/homeassistant/components/twilio/translations/lb.json b/homeassistant/components/twilio/translations/lb.json
index 2721402c1f3a53..7889f244c6eca8 100644
--- a/homeassistant/components/twilio/translations/lb.json
+++ b/homeassistant/components/twilio/translations/lb.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.",
+ "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken."
},
"create_entry": {
"default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, mussen [Webhooks mat Twilio]({twilio_url}) ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nLiest [Dokumentatioun]({docs_url}) w\u00e9i een Automatiounen ariicht welch eingehend Donn\u00e9\u00eb trait\u00e9ieren."
diff --git a/homeassistant/components/twilio/translations/nl.json b/homeassistant/components/twilio/translations/nl.json
index ee97ef4f6cd71a..0d5d33a727e350 100644
--- a/homeassistant/components/twilio/translations/nl.json
+++ b/homeassistant/components/twilio/translations/nl.json
@@ -1,14 +1,15 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk."
+ "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.",
+ "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen."
},
"create_entry": {
"default": "Om evenementen naar de Home Assistant te verzenden, moet u [Webhooks with Twilio] ( {twilio_url} ) instellen. \n\n Vul de volgende info in: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhoudstype: application / x-www-form-urlencoded \n\n Zie [de documentatie] ( {docs_url} ) voor informatie over het configureren van automatiseringen om binnenkomende gegevens te verwerken."
},
"step": {
"user": {
- "description": "Weet u zeker dat u Twilio wilt instellen?",
+ "description": "Wil je beginnen met instellen?",
"title": "Stel de Twilio Webhook in"
}
}
diff --git a/homeassistant/components/twilio/translations/tr.json b/homeassistant/components/twilio/translations/tr.json
new file mode 100644
index 00000000000000..84adcdf8225c43
--- /dev/null
+++ b/homeassistant/components/twilio/translations/tr.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.",
+ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twilio/translations/uk.json b/homeassistant/components/twilio/translations/uk.json
new file mode 100644
index 00000000000000..8ea0ce86a37a09
--- /dev/null
+++ b/homeassistant/components/twilio/translations/uk.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.",
+ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c."
+ },
+ "create_entry": {
+ "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f [Twilio]({twilio_url}). \n\n\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0456\u0439 \u043f\u043e \u043e\u0431\u0440\u043e\u0431\u0446\u0456 \u0434\u0430\u043d\u0438\u0445, \u0449\u043e \u043d\u0430\u0434\u0445\u043e\u0434\u044f\u0442\u044c."
+ },
+ "step": {
+ "user": {
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?",
+ "title": "Twilio"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twinkly/config_flow.py b/homeassistant/components/twinkly/config_flow.py
index f1593de5643b1a..0a9adf76e0e456 100644
--- a/homeassistant/components/twinkly/config_flow.py
+++ b/homeassistant/components/twinkly/config_flow.py
@@ -18,11 +18,9 @@
DEV_ID,
DEV_MODEL,
DEV_NAME,
+ DOMAIN,
)
-# https://github.com/PyCQA/pylint/issues/3202
-from .const import DOMAIN # pylint: disable=unused-import
-
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py
index 8de51d19d51869..4353aa2707b8fd 100644
--- a/homeassistant/components/twinkly/light.py
+++ b/homeassistant/components/twinkly/light.py
@@ -1,8 +1,9 @@
"""The Twinkly light component."""
+from __future__ import annotations
import asyncio
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from aiohttp import ClientError
@@ -84,7 +85,7 @@ def available(self) -> bool:
return self._is_available
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Id of the device."""
return self._id
@@ -104,7 +105,7 @@ def icon(self) -> str:
return "mdi:string-lights"
@property
- def device_info(self) -> Optional[Dict[str, Any]]:
+ def device_info(self) -> dict[str, Any] | None:
"""Get device specific attributes."""
return (
{
@@ -123,12 +124,12 @@ def is_on(self) -> bool:
return self._is_on
@property
- def brightness(self) -> Optional[int]:
+ def brightness(self) -> int | None:
"""Return the brightness of the light."""
return self._brightness
@property
- def state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return device specific state attributes."""
attributes = self._attributes
diff --git a/homeassistant/components/twinkly/translations/de.json b/homeassistant/components/twinkly/translations/de.json
index 2b4c70a0bad02a..c196f53262dd13 100644
--- a/homeassistant/components/twinkly/translations/de.json
+++ b/homeassistant/components/twinkly/translations/de.json
@@ -1,7 +1,10 @@
{
"config": {
+ "abort": {
+ "device_exists": "Ger\u00e4t ist bereits konfiguriert"
+ },
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"step": {
"user": {
diff --git a/homeassistant/components/twinkly/translations/fr.json b/homeassistant/components/twinkly/translations/fr.json
new file mode 100644
index 00000000000000..c26edea54eea0e
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/fr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "D\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "cannot_connect": "Connexion impossible"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Nom r\u00e9seau (ou adresse IP) de votre Twinkly"
+ },
+ "description": "Configurer votre Twinkly",
+ "title": "Twinkly"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twinkly/translations/hu.json b/homeassistant/components/twinkly/translations/hu.json
new file mode 100644
index 00000000000000..190d7e469d56f7
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/hu.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "step": {
+ "user": {
+ "title": "Twinkly"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twinkly/translations/id.json b/homeassistant/components/twinkly/translations/id.json
new file mode 100644
index 00000000000000..b4a5ba6cbfaef3
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/id.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host (atau alamat IP) perangkat twinkly Anda"
+ },
+ "description": "Siapkan string led Twinkly Anda",
+ "title": "Twinkly"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twinkly/translations/ko.json b/homeassistant/components/twinkly/translations/ko.json
new file mode 100644
index 00000000000000..b3b23991e3d5e9
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/ko.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Twinkly \uae30\uae30\uc758 \ud638\uc2a4\ud2b8 (\ub610\ub294 IP \uc8fc\uc18c)"
+ },
+ "description": "Twinkly LED \uc904 \uc870\uba85 \uc124\uc815\ud558\uae30",
+ "title": "Twinkly"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twinkly/translations/lb.json b/homeassistant/components/twinkly/translations/lb.json
new file mode 100644
index 00000000000000..2e00a8ae4dbfb8
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/lb.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "Apparat ass scho konfigur\u00e9iert"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twinkly/translations/nl.json b/homeassistant/components/twinkly/translations/nl.json
index 861ee57283cfab..97a55150447321 100644
--- a/homeassistant/components/twinkly/translations/nl.json
+++ b/homeassistant/components/twinkly/translations/nl.json
@@ -1,5 +1,11 @@
{
"config": {
+ "abort": {
+ "device_exists": "Apparaat is al geconfigureerd"
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/twinkly/translations/tr.json b/homeassistant/components/twinkly/translations/tr.json
index 14365f988bde6c..d2e7173dad3282 100644
--- a/homeassistant/components/twinkly/translations/tr.json
+++ b/homeassistant/components/twinkly/translations/tr.json
@@ -1,5 +1,11 @@
{
"config": {
+ "abort": {
+ "device_exists": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/twinkly/translations/uk.json b/homeassistant/components/twinkly/translations/uk.json
new file mode 100644
index 00000000000000..bd256d31b0328b
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/uk.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0406\u043c'\u044f \u0445\u043e\u0441\u0442\u0430 (\u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430) \u0412\u0430\u0448\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Twinkly"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u0432\u0456\u0442\u043b\u043e\u0434\u0456\u043e\u0434\u043d\u043e\u0457 \u0441\u0442\u0440\u0456\u0447\u043a\u0438 Twinkly",
+ "title": "Twinkly"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py
index 4b0196281584e4..cfabcf1045f291 100644
--- a/homeassistant/components/twitch/sensor.py
+++ b/homeassistant/components/twitch/sensor.py
@@ -5,10 +5,9 @@
from twitch import TwitchClient
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_CLIENT_ID, CONF_TOKEN
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -56,7 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([TwitchSensor(channel_id, client) for channel_id in channel_ids], True)
-class TwitchSensor(Entity):
+class TwitchSensor(SensorEntity):
"""Representation of an Twitch channel."""
def __init__(self, channel, client):
@@ -88,7 +87,7 @@ def entity_picture(self):
return self._preview
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attr = dict(self._statistics)
diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json
index acd47253b82963..297f990e9df2c6 100644
--- a/homeassistant/components/twitter/manifest.json
+++ b/homeassistant/components/twitter/manifest.json
@@ -2,6 +2,6 @@
"domain": "twitter",
"name": "Twitter",
"documentation": "https://www.home-assistant.io/integrations/twitter",
- "requirements": ["TwitterAPI==2.6.3"],
+ "requirements": ["TwitterAPI==2.6.8"],
"codeowners": []
}
diff --git a/homeassistant/components/twitter/notify.py b/homeassistant/components/twitter/notify.py
index 62e8fc17dffabb..ac7de89a61bbb7 100644
--- a/homeassistant/components/twitter/notify.py
+++ b/homeassistant/components/twitter/notify.py
@@ -201,7 +201,7 @@ def check_status_until_done(self, media_id, callback, *args):
method_override="GET",
)
if resp.status_code != HTTP_OK:
- _LOGGER.error("media processing error: %s", resp.json())
+ _LOGGER.error("Media processing error: %s", resp.json())
processing_info = resp.json()["processing_info"]
_LOGGER.debug("media processing %s status: %s", media_id, processing_info)
diff --git a/homeassistant/components/ubus/device_tracker.py b/homeassistant/components/ubus/device_tracker.py
index 4cefefc2f96b79..12c986d57bbba3 100644
--- a/homeassistant/components/ubus/device_tracker.py
+++ b/homeassistant/components/ubus/device_tracker.py
@@ -1,9 +1,9 @@
"""Support for OpenWRT (ubus) routers."""
-import json
+
import logging
import re
-import requests
+from openwrt.ubus import Ubus
import voluptuous as vol
from homeassistant.components.device_tracker import (
@@ -11,8 +11,7 @@
PLATFORM_SCHEMA,
DeviceScanner,
)
-from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, HTTP_OK
-from homeassistant.exceptions import HomeAssistantError
+from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -58,7 +57,7 @@ def decorator(self, *args, **kwargs):
"Invalid session detected."
" Trying to refresh session_id and re-run RPC"
)
- self.session_id = _get_session_id(self.url, self.username, self.password)
+ self.ubus.connect()
return func(self, *args, **kwargs)
@@ -82,10 +81,10 @@ def __init__(self, config):
self.last_results = {}
self.url = f"http://{host}/ubus"
- self.session_id = _get_session_id(self.url, self.username, self.password)
+ self.ubus = Ubus(self.url, self.username, self.password)
self.hostapd = []
self.mac2name = None
- self.success_init = self.session_id is not None
+ self.success_init = self.ubus.connect() is not None
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
@@ -119,16 +118,14 @@ def _update_info(self):
_LOGGER.info("Checking hostapd")
if not self.hostapd:
- hostapd = _req_json_rpc(self.url, self.session_id, "list", "hostapd.*", "")
+ hostapd = self.ubus.get_hostapd()
self.hostapd.extend(hostapd.keys())
self.last_results = []
results = 0
# for each access point
for hostapd in self.hostapd:
- result = _req_json_rpc(
- self.url, self.session_id, "call", hostapd, "get_clients"
- )
+ result = self.ubus.get_hostapd_clients(hostapd)
if result:
results = results + 1
@@ -151,31 +148,21 @@ def __init__(self, config):
def _generate_mac2name(self):
if self.leasefile is None:
- result = _req_json_rpc(
- self.url,
- self.session_id,
- "call",
- "uci",
- "get",
- config="dhcp",
- type="dnsmasq",
- )
+ result = self.ubus.get_uci_config("dhcp", "dnsmasq")
if result:
values = result["values"].values()
self.leasefile = next(iter(values))["leasefile"]
else:
return
- result = _req_json_rpc(
- self.url, self.session_id, "call", "file", "read", path=self.leasefile
- )
+ result = self.ubus.file_read(self.leasefile)
if result:
self.mac2name = {}
for line in result["data"].splitlines():
hosts = line.split(" ")
self.mac2name[hosts[1].upper()] = hosts[3]
else:
- # Error, handled in the _req_json_rpc
+ # Error, handled in the ubus.file_read()
return
@@ -183,7 +170,7 @@ class OdhcpdUbusDeviceScanner(UbusDeviceScanner):
"""Implement the Ubus device scanning for the odhcp DHCP server."""
def _generate_mac2name(self):
- result = _req_json_rpc(self.url, self.session_id, "call", "dhcp", "ipv4leases")
+ result = self.ubus.get_dhcp_method("ipv4leases")
if result:
self.mac2name = {}
for device in result["device"].values():
@@ -193,55 +180,5 @@ def _generate_mac2name(self):
mac = ":".join(mac[i : i + 2] for i in range(0, len(mac), 2))
self.mac2name[mac.upper()] = lease["hostname"]
else:
- # Error, handled in the _req_json_rpc
+ # Error, handled in the ubus.get_dhcp_method()
return
-
-
-def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params):
- """Perform one JSON RPC operation."""
- data = json.dumps(
- {
- "jsonrpc": "2.0",
- "id": 1,
- "method": rpcmethod,
- "params": [session_id, subsystem, method, params],
- }
- )
-
- try:
- res = requests.post(url, data=data, timeout=5)
-
- except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
- return
-
- if res.status_code == HTTP_OK:
- response = res.json()
- if "error" in response:
- if (
- "message" in response["error"]
- and response["error"]["message"] == "Access denied"
- ):
- raise PermissionError(response["error"]["message"])
- raise HomeAssistantError(response["error"]["message"])
-
- if rpcmethod == "call":
- try:
- return response["result"][1]
- except IndexError:
- return
- else:
- return response["result"]
-
-
-def _get_session_id(url, username, password):
- """Get the authentication token for the given host+username+password."""
- res = _req_json_rpc(
- url,
- "00000000000000000000000000000000",
- "call",
- "session",
- "login",
- username=username,
- password=password,
- )
- return res["ubus_rpc_session"]
diff --git a/homeassistant/components/ubus/manifest.json b/homeassistant/components/ubus/manifest.json
index af7fb50b6c49ea..68452f98f7d720 100644
--- a/homeassistant/components/ubus/manifest.json
+++ b/homeassistant/components/ubus/manifest.json
@@ -2,5 +2,6 @@
"domain": "ubus",
"name": "OpenWrt (ubus)",
"documentation": "https://www.home-assistant.io/integrations/ubus",
- "codeowners": []
+ "requirements": ["openwrt-ubus-rpc==0.0.2"],
+ "codeowners": ["@noltari"]
}
diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py
index e7c01479a96255..f5cb21edcf7907 100644
--- a/homeassistant/components/uk_transport/sensor.py
+++ b/homeassistant/components/uk_transport/sensor.py
@@ -6,10 +6,9 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_MODE, HTTP_OK, TIME_MINUTES
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
import homeassistant.util.dt as dt_util
@@ -32,9 +31,7 @@
_QUERY_SCHEME = vol.Schema(
{
- vol.Required(CONF_MODE): vol.All(
- cv.ensure_list, [vol.In(list(["bus", "train"]))]
- ),
+ vol.Required(CONF_MODE): vol.All(cv.ensure_list, [vol.In(["bus", "train"])]),
vol.Required(CONF_ORIGIN): cv.string,
vol.Required(CONF_DESTINATION): cv.string,
}
@@ -85,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class UkTransportSensor(Entity):
+class UkTransportSensor(SensorEntity):
"""
Sensor that reads the UK transport web API.
@@ -191,7 +188,7 @@ def _update(self):
self._state = None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return other details about the sensor state."""
attrs = {}
if self._data is not None:
@@ -261,7 +258,7 @@ def _update(self):
self._state = None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return other details about the sensor state."""
attrs = {}
if self._data is not None:
diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py
index 439073497a2bf4..8d24a9b642ffdf 100644
--- a/homeassistant/components/unifi/__init__.py
+++ b/homeassistant/components/unifi/__init__.py
@@ -1,11 +1,11 @@
-"""Support for devices connected to UniFi POE."""
+"""Integration to UniFi controllers and its various features."""
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
-from .config_flow import get_controller_id_from_config_entry
from .const import (
ATTR_MANUFACTURER,
+ CONF_CONTROLLER,
DOMAIN as UNIFI_DOMAIN,
LOGGER,
UNIFI_WIRELESS_CLIENTS,
@@ -29,10 +29,19 @@ async def async_setup_entry(hass, config_entry):
"""Set up the UniFi component."""
hass.data.setdefault(UNIFI_DOMAIN, {})
+ # Flat configuration was introduced with 2021.3
+ await async_flatten_entry_data(hass, config_entry)
+
controller = UniFiController(hass, config_entry)
if not await controller.async_setup():
return False
+ # Unique ID was introduced with 2021.3
+ if config_entry.unique_id is None:
+ hass.config_entries.async_update_entry(
+ config_entry, unique_id=controller.site_id
+ )
+
hass.data[UNIFI_DOMAIN][config_entry.entry_id] = controller
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller.shutdown)
@@ -60,6 +69,17 @@ async def async_unload_entry(hass, config_entry):
return await controller.async_reset()
+async def async_flatten_entry_data(hass, config_entry):
+ """Simpler configuration structure for entry data.
+
+ Keep controller key layer in case user rollbacks.
+ """
+
+ data: dict = {**config_entry.data, **config_entry.data[CONF_CONTROLLER]}
+ if config_entry.data != data:
+ hass.config_entries.async_update_entry(config_entry, data=data)
+
+
class UnifiWirelessClients:
"""Class to store clients known to be wireless.
@@ -82,21 +102,12 @@ async def async_load(self):
@callback
def get_data(self, config_entry):
"""Get data related to a specific controller."""
- controller_id = get_controller_id_from_config_entry(config_entry)
- key = config_entry.entry_id
- if controller_id in self.data:
- key = controller_id
-
- data = self.data.get(key, {"wireless_devices": []})
+ data = self.data.get(config_entry.entry_id, {"wireless_devices": []})
return set(data["wireless_devices"])
@callback
def update_data(self, data, config_entry):
"""Update data and schedule to save to file."""
- controller_id = get_controller_id_from_config_entry(config_entry)
- if controller_id in self.data:
- self.data.pop(controller_id)
-
self.data[config_entry.entry_id] = {"wireless_devices": list(data)}
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py
index f5e947c5e6fb6b..094bae0588197b 100644
--- a/homeassistant/components/unifi/config_flow.py
+++ b/homeassistant/components/unifi/config_flow.py
@@ -1,4 +1,10 @@
-"""Config flow for UniFi."""
+"""Config flow for UniFi.
+
+Provides user initiated configuration flow.
+Discovery of controllers hosted on UDM and UDM Pro devices through SSDP.
+Reauthentication when issue with credentials are reported.
+Configuration of options through options flow.
+"""
import socket
from urllib.parse import urlparse
@@ -31,11 +37,9 @@
CONF_TRACK_CLIENTS,
CONF_TRACK_DEVICES,
CONF_TRACK_WIRED_CLIENTS,
- CONTROLLER_ID,
DEFAULT_DPI_RESTRICTIONS,
DEFAULT_POE_CLIENTS,
DOMAIN as UNIFI_DOMAIN,
- LOGGER,
)
from .controller import get_controller
from .errors import AuthenticationRequired, CannotConnect
@@ -51,15 +55,6 @@
}
-@callback
-def get_controller_id_from_config_entry(config_entry):
- """Return controller with a matching bridge id."""
- return CONTROLLER_ID.format(
- host=config_entry.data[CONF_CONTROLLER][CONF_HOST],
- site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID],
- )
-
-
class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN):
"""Handle a UniFi config flow."""
@@ -75,9 +70,9 @@ def async_get_options_flow(config_entry):
def __init__(self):
"""Initialize the UniFi flow."""
self.config = {}
- self.sites = None
+ self.site_ids = {}
+ self.site_names = {}
self.reauth_config_entry = None
- self.reauth_config = {}
self.reauth_schema = {}
async def async_step_user(self, user_input=None):
@@ -86,27 +81,27 @@ async def async_step_user(self, user_input=None):
if user_input is not None:
- try:
- self.config = {
- CONF_HOST: user_input[CONF_HOST],
- CONF_USERNAME: user_input[CONF_USERNAME],
- CONF_PASSWORD: user_input[CONF_PASSWORD],
- CONF_PORT: user_input.get(CONF_PORT),
- CONF_VERIFY_SSL: user_input.get(CONF_VERIFY_SSL),
- CONF_SITE_ID: DEFAULT_SITE_ID,
- }
+ self.config = {
+ CONF_HOST: user_input[CONF_HOST],
+ CONF_USERNAME: user_input[CONF_USERNAME],
+ CONF_PASSWORD: user_input[CONF_PASSWORD],
+ CONF_PORT: user_input.get(CONF_PORT),
+ CONF_VERIFY_SSL: user_input.get(CONF_VERIFY_SSL),
+ CONF_SITE_ID: DEFAULT_SITE_ID,
+ }
- controller = await get_controller(self.hass, **self.config)
+ try:
+ controller = await get_controller(
+ self.hass,
+ host=self.config[CONF_HOST],
+ username=self.config[CONF_USERNAME],
+ password=self.config[CONF_PASSWORD],
+ port=self.config[CONF_PORT],
+ site=self.config[CONF_SITE_ID],
+ verify_ssl=self.config[CONF_VERIFY_SSL],
+ )
sites = await controller.sites()
- self.sites = {site["name"]: site["desc"] for site in sites.values()}
-
- if self.reauth_config.get(CONF_SITE_ID) in self.sites:
- return await self.async_step_site(
- {CONF_SITE_ID: self.reauth_config[CONF_SITE_ID]}
- )
-
- return await self.async_step_site()
except AuthenticationRequired:
errors["base"] = "faulty_credentials"
@@ -114,15 +109,23 @@ async def async_step_user(self, user_input=None):
except CannotConnect:
errors["base"] = "service_unavailable"
- except Exception: # pylint: disable=broad-except
- LOGGER.error(
- "Unknown error connecting with UniFi Controller at %s",
- user_input[CONF_HOST],
- )
- return self.async_abort(reason="unknown")
+ else:
+ self.site_ids = {site["_id"]: site["name"] for site in sites.values()}
+ self.site_names = {site["_id"]: site["desc"] for site in sites.values()}
+
+ if (
+ self.reauth_config_entry
+ and self.reauth_config_entry.unique_id in self.site_names
+ ):
+ return await self.async_step_site(
+ {CONF_SITE_ID: self.reauth_config_entry.unique_id}
+ )
+
+ return await self.async_step_site()
- host = self.config.get(CONF_HOST)
- if not host and await async_discover_unifi(self.hass):
+ if not (host := self.config.get(CONF_HOST, "")) and await async_discover_unifi(
+ self.hass
+ ):
host = "unifi"
data = self.reauth_schema or {
@@ -147,26 +150,19 @@ async def async_step_site(self, user_input=None):
if user_input is not None:
- self.config[CONF_SITE_ID] = user_input[CONF_SITE_ID]
- data = {CONF_CONTROLLER: self.config}
+ unique_id = user_input[CONF_SITE_ID]
+ self.config[CONF_SITE_ID] = self.site_ids[unique_id]
+ # Backwards compatible config
+ self.config[CONF_CONTROLLER] = self.config.copy()
- if self.reauth_config_entry:
- self.hass.config_entries.async_update_entry(
- self.reauth_config_entry, data=data
- )
- await self.hass.config_entries.async_reload(
- self.reauth_config_entry.entry_id
- )
- return self.async_abort(reason="reauth_successful")
+ config_entry = await self.async_set_unique_id(unique_id)
+ abort_reason = "configuration_updated"
- for config_entry in self._async_current_entries():
- controller_data = config_entry.data[CONF_CONTROLLER]
- if (
- controller_data[CONF_HOST] != self.config[CONF_HOST]
- or controller_data[CONF_SITE_ID] != self.config[CONF_SITE_ID]
- ):
- continue
+ if self.reauth_config_entry:
+ config_entry = self.reauth_config_entry
+ abort_reason = "reauth_successful"
+ if config_entry:
controller = self.hass.data.get(UNIFI_DOMAIN, {}).get(
config_entry.entry_id
)
@@ -174,47 +170,51 @@ async def async_step_site(self, user_input=None):
if controller and controller.available:
return self.async_abort(reason="already_configured")
- self.hass.config_entries.async_update_entry(config_entry, data=data)
+ self.hass.config_entries.async_update_entry(
+ config_entry, data=self.config
+ )
await self.hass.config_entries.async_reload(config_entry.entry_id)
- return self.async_abort(reason="configuration_updated")
+ return self.async_abort(reason=abort_reason)
- site_nice_name = self.sites[self.config[CONF_SITE_ID]]
- return self.async_create_entry(title=site_nice_name, data=data)
+ site_nice_name = self.site_names[unique_id]
+ return self.async_create_entry(title=site_nice_name, data=self.config)
- if len(self.sites) == 1:
- return await self.async_step_site({CONF_SITE_ID: next(iter(self.sites))})
+ if len(self.site_names) == 1:
+ return await self.async_step_site(
+ {CONF_SITE_ID: next(iter(self.site_names))}
+ )
return self.async_show_form(
step_id="site",
- data_schema=vol.Schema({vol.Required(CONF_SITE_ID): vol.In(self.sites)}),
+ data_schema=vol.Schema(
+ {vol.Required(CONF_SITE_ID): vol.In(self.site_names)}
+ ),
errors=errors,
)
async def async_step_reauth(self, config_entry: dict):
"""Trigger a reauthentication flow."""
self.reauth_config_entry = config_entry
- self.reauth_config = config_entry.data[CONF_CONTROLLER]
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {
- CONF_HOST: self.reauth_config[CONF_HOST],
+ CONF_HOST: config_entry.data[CONF_HOST],
CONF_SITE_ID: config_entry.title,
}
self.reauth_schema = {
- vol.Required(CONF_HOST, default=self.reauth_config[CONF_HOST]): str,
- vol.Required(CONF_USERNAME, default=self.reauth_config[CONF_USERNAME]): str,
+ vol.Required(CONF_HOST, default=config_entry.data[CONF_HOST]): str,
+ vol.Required(CONF_USERNAME, default=config_entry.data[CONF_USERNAME]): str,
vol.Required(CONF_PASSWORD): str,
- vol.Required(CONF_PORT, default=self.reauth_config[CONF_PORT]): int,
+ vol.Required(CONF_PORT, default=config_entry.data[CONF_PORT]): int,
vol.Required(
- CONF_VERIFY_SSL, default=self.reauth_config[CONF_VERIFY_SSL]
+ CONF_VERIFY_SSL, default=config_entry.data[CONF_VERIFY_SSL]
): bool,
}
return await self.async_step_user()
async def async_step_ssdp(self, discovery_info):
- """Handle a discovered unifi device."""
+ """Handle a discovered UniFi device."""
parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION])
model_description = discovery_info[ssdp.ATTR_UPNP_MODEL_DESCRIPTION]
mac_address = format_mac(discovery_info[ssdp.ATTR_UPNP_SERIAL])
@@ -227,12 +227,11 @@ async def async_step_ssdp(self, discovery_info):
return self.async_abort(reason="already_configured")
await self.async_set_unique_id(mac_address)
- self._abort_if_unique_id_configured(updates={CONF_HOST: self.config[CONF_HOST]})
+ self._abort_if_unique_id_configured(updates=self.config)
- # pylint: disable=no-member
self.context["title_placeholders"] = {
CONF_HOST: self.config[CONF_HOST],
- CONF_SITE_ID: "default",
+ CONF_SITE_ID: DEFAULT_SITE_ID,
}
port = MODEL_PORTS.get(model_description)
@@ -242,11 +241,9 @@ async def async_step_ssdp(self, discovery_info):
return await self.async_step_user()
def _host_already_configured(self, host):
- """See if we already have a unifi entry matching the host."""
+ """See if we already have a UniFi entry matching the host."""
for entry in self._async_current_entries():
- if not entry.data:
- continue
- if entry.data[CONF_CONTROLLER][CONF_HOST] == host:
+ if entry.data.get(CONF_HOST) == host:
return True
return False
@@ -271,7 +268,7 @@ async def async_step_init(self, user_input=None):
return await self.async_step_simple_options()
async def async_step_simple_options(self, user_input=None):
- """For simple Jack."""
+ """For users without advanced settings enabled."""
if user_input is not None:
self.options.update(user_input)
return await self._update_options()
@@ -322,7 +319,7 @@ async def async_step_device_tracker(self, user_input=None):
if "name" in wlan
}
)
- ssid_filter = {ssid: ssid for ssid in sorted(list(ssids))}
+ ssid_filter = {ssid: ssid for ssid in sorted(ssids)}
return self.async_show_form(
step_id="device_tracker",
diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py
index ba16612a903429..94e2fad35edde6 100644
--- a/homeassistant/components/unifi/const.py
+++ b/homeassistant/components/unifi/const.py
@@ -4,8 +4,6 @@
LOGGER = logging.getLogger(__package__)
DOMAIN = "unifi"
-CONTROLLER_ID = "{host}-{site}"
-
CONF_CONTROLLER = "controller"
CONF_SITE_ID = "site"
diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py
index 11e02d60a3f261..c77987bcbddf74 100644
--- a/homeassistant/components/unifi/controller.py
+++ b/homeassistant/components/unifi/controller.py
@@ -1,8 +1,9 @@
"""UniFi Controller abstraction."""
+from __future__ import annotations
+
import asyncio
from datetime import datetime, timedelta
import ssl
-from typing import Optional
from aiohttp import CookieJar
import aiounifi
@@ -28,12 +29,20 @@
from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
+from homeassistant.components.unifi.switch import BLOCK_SWITCH, POE_SWITCH
from homeassistant.config_entries import SOURCE_REAUTH
-from homeassistant.const import CONF_HOST
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+)
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.entity_registry import async_entries_for_config_entry
from homeassistant.helpers.event import async_track_time_interval
import homeassistant.util.dt as dt_util
@@ -41,7 +50,6 @@
CONF_ALLOW_BANDWIDTH_SENSORS,
CONF_ALLOW_UPTIME_SENSORS,
CONF_BLOCK_CLIENT,
- CONF_CONTROLLER,
CONF_DETECTION_TIME,
CONF_DPI_RESTRICTIONS,
CONF_IGNORE_WIRED_BUG,
@@ -51,7 +59,6 @@
CONF_TRACK_CLIENTS,
CONF_TRACK_DEVICES,
CONF_TRACK_WIRED_CLIENTS,
- CONTROLLER_ID,
DEFAULT_ALLOW_BANDWIDTH_SENSORS,
DEFAULT_ALLOW_UPTIME_SENSORS,
DEFAULT_DETECTION_TIME,
@@ -69,7 +76,7 @@
RETRY_TIMER = 15
CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1)
-SUPPORTED_PLATFORMS = [TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN]
+PLATFORMS = [TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN]
CLIENT_CONNECTED = (
WIRED_CLIENT_CONNECTED,
@@ -96,6 +103,7 @@ def __init__(self, hass, config_entry):
self.wireless_clients = None
self.listeners = []
+ self.site_id: str = ""
self._site_name = None
self._site_role = None
@@ -109,9 +117,10 @@ def __init__(self, hass, config_entry):
def load_config_entry_options(self):
"""Store attributes to avoid property call overhead since they are called frequently."""
- # Device tracker options
options = self.config_entry.options
+ # Device tracker options
+
# Config entry option to not track clients.
self.option_track_clients = options.get(
CONF_TRACK_CLIENTS, DEFAULT_TRACK_CLIENTS
@@ -157,20 +166,15 @@ def load_config_entry_options(self):
CONF_ALLOW_UPTIME_SENSORS, DEFAULT_ALLOW_UPTIME_SENSORS
)
- @property
- def controller_id(self):
- """Return the controller ID."""
- return CONTROLLER_ID.format(host=self.host, site=self.site)
-
@property
def host(self):
"""Return the host of this controller."""
- return self.config_entry.data[CONF_CONTROLLER][CONF_HOST]
+ return self.config_entry.data[CONF_HOST]
@property
def site(self):
"""Return the site of this config entry."""
- return self.config_entry.data[CONF_CONTROLLER][CONF_SITE_ID]
+ return self.config_entry.data[CONF_SITE_ID]
@property
def site_name(self):
@@ -260,25 +264,25 @@ def async_unifi_signalling_callback(self, signal, data):
@property
def signal_reachable(self) -> str:
"""Integration specific event to signal a change in connection status."""
- return f"unifi-reachable-{self.controller_id}"
+ return f"unifi-reachable-{self.config_entry.entry_id}"
@property
- def signal_update(self):
+ def signal_update(self) -> str:
"""Event specific per UniFi entry to signal new data."""
- return f"unifi-update-{self.controller_id}"
+ return f"unifi-update-{self.config_entry.entry_id}"
@property
- def signal_remove(self):
+ def signal_remove(self) -> str:
"""Event specific per UniFi entry to signal removal of entities."""
- return f"unifi-remove-{self.controller_id}"
+ return f"unifi-remove-{self.config_entry.entry_id}"
@property
- def signal_options_update(self):
+ def signal_options_update(self) -> str:
"""Event specific per UniFi entry to signal new options."""
- return f"unifi-options-{self.controller_id}"
+ return f"unifi-options-{self.config_entry.entry_id}"
@property
- def signal_heartbeat_missed(self):
+ def signal_heartbeat_missed(self) -> str:
"""Event specific per UniFi device tracker to signal new heartbeat missed."""
return "unifi-heartbeat-missed"
@@ -303,20 +307,18 @@ async def async_setup(self):
try:
self.api = await get_controller(
self.hass,
- **self.config_entry.data[CONF_CONTROLLER],
+ host=self.config_entry.data[CONF_HOST],
+ username=self.config_entry.data[CONF_USERNAME],
+ password=self.config_entry.data[CONF_PASSWORD],
+ port=self.config_entry.data[CONF_PORT],
+ site=self.config_entry.data[CONF_SITE_ID],
+ verify_ssl=self.config_entry.data[CONF_VERIFY_SSL],
async_callback=self.async_unifi_signalling_callback,
)
await self.api.initialize()
sites = await self.api.sites()
-
- for site in sites.values():
- if self.site == site["name"]:
- self._site_name = site["desc"]
- break
-
description = await self.api.site_description()
- self._site_role = description[0]["site_role"]
except CannotConnect as err:
raise ConfigEntryNotReady from err
@@ -331,21 +333,29 @@ async def async_setup(self):
)
return False
- # Restore clients that is not a part of active clients list.
+ for site in sites.values():
+ if self.site == site["name"]:
+ self.site_id = site["_id"]
+ self._site_name = site["desc"]
+ break
+
+ self._site_role = description[0]["site_role"]
+
+ # Restore clients that are not a part of active clients list.
entity_registry = await self.hass.helpers.entity_registry.async_get_registry()
- for entity in entity_registry.entities.values():
- if (
- entity.config_entry_id != self.config_entry.entry_id
- or "-" not in entity.unique_id
+ for entry in async_entries_for_config_entry(
+ entity_registry, self.config_entry.entry_id
+ ):
+ if entry.domain == TRACKER_DOMAIN:
+ mac = entry.unique_id.split("-", 1)[0]
+ elif entry.domain == SWITCH_DOMAIN and (
+ entry.unique_id.startswith(BLOCK_SWITCH)
+ or entry.unique_id.startswith(POE_SWITCH)
):
+ mac = entry.unique_id.split("-", 1)[1]
+ else:
continue
- mac = ""
- if entity.domain == TRACKER_DOMAIN:
- mac = entity.unique_id.split("-", 1)[0]
- elif entity.domain == SWITCH_DOMAIN:
- mac = entity.unique_id.split("-", 1)[1]
-
if mac in self.api.clients or mac not in self.api.clients_all:
continue
@@ -353,7 +363,7 @@ async def async_setup(self):
self.api.clients.process_raw([client.raw])
LOGGER.debug(
"Restore disconnected client %s (%s)",
- entity.entity_id,
+ entry.entity_id,
client.mac,
)
@@ -361,7 +371,7 @@ async def async_setup(self):
self.wireless_clients = wireless_clients.get_data(self.config_entry)
self.update_wireless_clients()
- for platform in SUPPORTED_PLATFORMS:
+ for platform in PLATFORMS:
self.hass.async_create_task(
self.hass.config_entries.async_forward_entry_setup(
self.config_entry, platform
@@ -380,7 +390,7 @@ async def async_setup(self):
@callback
def async_heartbeat(
- self, unique_id: str, heartbeat_expire_time: Optional[datetime] = None
+ self, unique_id: str, heartbeat_expire_time: datetime | None = None
) -> None:
"""Signal when a device has fresh home state."""
if heartbeat_expire_time is not None:
@@ -408,9 +418,8 @@ async def async_config_entry_updated(hass, config_entry) -> None:
If config entry is updated due to reauth flow
the entry might already have been reset and thus is not available.
"""
- if config_entry.entry_id not in hass.data[UNIFI_DOMAIN]:
+ if not (controller := hass.data[UNIFI_DOMAIN].get(config_entry.entry_id)):
return
- controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
controller.load_config_entry_options()
async_dispatcher_send(hass, controller.signal_options_update)
@@ -452,10 +461,18 @@ async def async_reset(self):
"""
self.api.stop_websocket()
- for platform in SUPPORTED_PLATFORMS:
- await self.hass.config_entries.async_forward_entry_unload(
- self.config_entry, platform
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ self.hass.config_entries.async_forward_entry_unload(
+ self.config_entry, platform
+ )
+ for platform in PLATFORMS
+ ]
)
+ )
+ if not unload_ok:
+ return False
for unsub_dispatcher in self.listeners:
unsub_dispatcher()
diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py
index 6a4d986d5b2ac5..9842184e2ee59e 100644
--- a/homeassistant/components/unifi/device_tracker.py
+++ b/homeassistant/components/unifi/device_tracker.py
@@ -1,4 +1,4 @@
-"""Track devices using UniFi controllers."""
+"""Track both clients and devices using UniFi controllers."""
from datetime import timedelta
from aiounifi.api import SOURCE_DATA, SOURCE_EVENT
@@ -145,6 +145,7 @@ def __init__(self, client, controller):
self.heartbeat_check = False
self._is_connected = False
+ self._controller_connection_state_changed = False
if client.last_seen:
self._is_connected = (
@@ -175,14 +176,16 @@ async def async_will_remove_from_hass(self) -> None:
@callback
def async_signal_reachable_callback(self) -> None:
"""Call when controller connection state change."""
- self.async_update_callback(controller_state_change=True)
+ self._controller_connection_state_changed = True
+ super().async_signal_reachable_callback()
- # pylint: disable=arguments-differ
@callback
- def async_update_callback(self, controller_state_change: bool = False) -> None:
+ def async_update_callback(self) -> None:
"""Update the clients state."""
- if controller_state_change:
+ if self._controller_connection_state_changed:
+ self._controller_connection_state_changed = False
+
if self.controller.available:
self.schedule_update = True
@@ -202,10 +205,13 @@ def async_update_callback(self, controller_state_change: bool = False) -> None:
elif not self.heartbeat_check:
self.schedule_update = True
- elif not self.client.event and self.client.last_updated == SOURCE_DATA:
- if self.is_wired == self.client.is_wired:
- self._is_connected = True
- self.schedule_update = True
+ elif (
+ not self.client.event
+ and self.client.last_updated == SOURCE_DATA
+ and self.is_wired == self.client.is_wired
+ ):
+ self._is_connected = True
+ self.schedule_update = True
if self.schedule_update:
self.schedule_update = False
@@ -246,7 +252,7 @@ def unique_id(self) -> str:
return f"{self.client.mac}-{self.controller.site}"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the client state attributes."""
raw = self.client.raw
@@ -304,6 +310,7 @@ def __init__(self, device, controller):
self.device = self._item
self._is_connected = device.state == 1
+ self._controller_connection_state_changed = False
self.schedule_update = False
async def async_added_to_hass(self) -> None:
@@ -325,14 +332,16 @@ async def async_will_remove_from_hass(self) -> None:
@callback
def async_signal_reachable_callback(self) -> None:
"""Call when controller connection state change."""
- self.async_update_callback(controller_state_change=True)
+ self._controller_connection_state_changed = True
+ super().async_signal_reachable_callback()
- # pylint: disable=arguments-differ
@callback
- def async_update_callback(self, controller_state_change: bool = False) -> None:
+ def async_update_callback(self) -> None:
"""Update the devices' state."""
- if controller_state_change:
+ if self._controller_connection_state_changed:
+ self._controller_connection_state_changed = False
+
if self.controller.available:
if self._is_connected:
self.schedule_update = True
@@ -415,7 +424,7 @@ async def async_update_device_registry(self) -> None:
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
if self.device.state == 0:
return {}
diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py
index c0b8cea09c2163..755d95a061bbe6 100644
--- a/homeassistant/components/unifi/sensor.py
+++ b/homeassistant/components/unifi/sensor.py
@@ -1,5 +1,12 @@
-"""Support for bandwidth sensors with UniFi clients."""
-from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP, DOMAIN
+"""Sensor platform for UniFi integration.
+
+Support for bandwidth sensors of network clients.
+Support for uptime sensors of network clients.
+"""
+
+from datetime import datetime, timedelta
+
+from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP, DOMAIN, SensorEntity
from homeassistant.const import DATA_MEGABYTES
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -72,7 +79,7 @@ def add_uptime_entities(controller, async_add_entities, clients):
async_add_entities(sensors)
-class UniFiBandwidthSensor(UniFiClient):
+class UniFiBandwidthSensor(UniFiClient, SensorEntity):
"""UniFi bandwidth sensor base class."""
DOMAIN = DOMAIN
@@ -119,7 +126,7 @@ def state(self) -> int:
return self.client.tx_bytes / 1000000
-class UniFiUpTimeSensor(UniFiClient):
+class UniFiUpTimeSensor(UniFiClient, SensorEntity):
"""UniFi uptime sensor."""
DOMAIN = DOMAIN
@@ -136,8 +143,10 @@ def name(self) -> str:
return f"{super().name} {self.TYPE.capitalize()}"
@property
- def state(self) -> int:
+ def state(self) -> datetime:
"""Return the uptime of the client."""
+ if self.client.uptime < 1000000000:
+ return (dt_util.now() - timedelta(seconds=self.client.uptime)).isoformat()
return dt_util.utc_from_timestamp(float(self.client.uptime)).isoformat()
async def options_updated(self) -> None:
diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py
index 6aa42b0d2918e5..f04acaaec872e5 100644
--- a/homeassistant/components/unifi/switch.py
+++ b/homeassistant/components/unifi/switch.py
@@ -1,4 +1,9 @@
-"""Support for devices connected to UniFi POE."""
+"""Switch platform for UniFi integration.
+
+Support for controlling power supply of clients which are powered over Ethernet (POE).
+Support for controlling network access of clients selected in option flow.
+Support for controlling deep packet inspection (DPI) restriction groups.
+"""
import logging
from typing import Any
@@ -13,6 +18,7 @@
from homeassistant.components.switch import DOMAIN, SwitchEntity
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.entity_registry import async_entries_for_config_entry
from homeassistant.helpers.restore_state import RestoreEntity
from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN
@@ -45,19 +51,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
return
# Store previously known POE control entities in case their POE are turned off.
- previously_known_poe_clients = []
+ known_poe_clients = []
entity_registry = await hass.helpers.entity_registry.async_get_registry()
- for entity in entity_registry.entities.values():
+ for entry in async_entries_for_config_entry(entity_registry, config_entry.entry_id):
- if (
- entity.config_entry_id != config_entry.entry_id
- or not entity.unique_id.startswith(POE_SWITCH)
- ):
+ if not entry.unique_id.startswith(POE_SWITCH):
+ continue
+
+ mac = entry.unique_id.replace(f"{POE_SWITCH}-", "")
+ if mac not in controller.api.clients:
continue
- mac = entity.unique_id.replace(f"{POE_SWITCH}-", "")
- if mac in controller.api.clients or mac in controller.api.clients_all:
- previously_known_poe_clients.append(entity.unique_id)
+ known_poe_clients.append(mac)
for mac in controller.option_block_clients:
if mac not in controller.api.clients and mac in controller.api.clients_all:
@@ -75,9 +80,7 @@ def items_added(
add_block_entities(controller, async_add_entities, clients)
if controller.option_poe_clients:
- add_poe_entities(
- controller, async_add_entities, clients, previously_known_poe_clients
- )
+ add_poe_entities(controller, async_add_entities, clients, known_poe_clients)
if controller.option_dpi_restrictions:
add_dpi_entities(controller, async_add_entities, dpi_groups)
@@ -86,7 +89,7 @@ def items_added(
controller.listeners.append(async_dispatcher_connect(hass, signal, items_added))
items_added()
- previously_known_poe_clients.clear()
+ known_poe_clients.clear()
@callback
@@ -106,9 +109,7 @@ def add_block_entities(controller, async_add_entities, clients):
@callback
-def add_poe_entities(
- controller, async_add_entities, clients, previously_known_poe_clients
-):
+def add_poe_entities(controller, async_add_entities, clients, known_poe_clients):
"""Add new switch entities from the controller."""
switches = []
@@ -118,10 +119,13 @@ def add_poe_entities(
if mac in controller.entities[DOMAIN][POE_SWITCH]:
continue
- poe_client_id = f"{POE_SWITCH}-{mac}"
client = controller.api.clients[mac]
- if poe_client_id not in previously_known_poe_clients and (
+ # Try to identify new clients powered by POE.
+ # Known POE clients have been created in previous HASS sessions.
+ # If port_poe is None the port does not support POE
+ # If poe_enable is False we can't know if a POE client is available for control.
+ if mac not in known_poe_clients and (
mac in controller.wireless_clients
or client.sw_mac not in devices
or not devices[client.sw_mac].ports[client.sw_port].port_poe
@@ -134,7 +138,7 @@ def add_poe_entities(
multi_clients_on_port = False
for client2 in controller.api.clients.values():
- if poe_client_id in previously_known_poe_clients:
+ if mac in known_poe_clients:
break
if (
@@ -191,18 +195,19 @@ async def async_added_to_hass(self):
"""Call when entity about to be added to Home Assistant."""
await super().async_added_to_hass()
- state = await self.async_get_last_state()
- if state is None:
+ if self.poe_mode: # POE is enabled and client in a known state
return
- if self.poe_mode is None:
- self.poe_mode = state.attributes["poe_mode"]
+ if (state := await self.async_get_last_state()) is None:
+ return
+
+ self.poe_mode = state.attributes.get("poe_mode")
if not self.client.sw_mac:
- self.client.raw["sw_mac"] = state.attributes["switch"]
+ self.client.raw["sw_mac"] = state.attributes.get("switch")
if not self.client.sw_port:
- self.client.raw["sw_port"] = state.attributes["port"]
+ self.client.raw["sw_port"] = state.attributes.get("port")
@property
def is_on(self):
@@ -213,16 +218,15 @@ def is_on(self):
def available(self):
"""Return if switch is available.
- Poe_mode None means its poe state is unknown.
+ Poe_mode None means its POE state is unknown.
Sw_mac unavailable means restored client.
"""
return (
- self.poe_mode is None
- or self.client.sw_mac
- and (
- self.controller.available
- and self.client.sw_mac in self.controller.api.devices
- )
+ self.poe_mode is not None
+ and self.controller.available
+ and self.client.sw_port
+ and self.client.sw_mac
+ and self.client.sw_mac in self.controller.api.devices
)
async def async_turn_on(self, **kwargs):
@@ -234,7 +238,7 @@ async def async_turn_off(self, **kwargs):
await self.device.async_set_port_poe_mode(self.client.sw_port, "off")
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
attributes = {
"power": self.port.poe_power,
@@ -252,15 +256,7 @@ def device(self):
@property
def port(self):
"""Shortcut to the switch port that client is connected to."""
- try:
- return self.device.ports[self.client.sw_port]
- except (AttributeError, KeyError, TypeError):
- _LOGGER.warning(
- "Entity %s reports faulty device %s or port %s",
- self.entity_id,
- self.client.sw_mac,
- self.client.sw_port,
- )
+ return self.device.ports[self.client.sw_port]
async def options_updated(self) -> None:
"""Config entry options are updated, remove entity if option is disabled."""
@@ -283,10 +279,11 @@ def __init__(self, client, controller):
@callback
def async_update_callback(self) -> None:
"""Update the clients state."""
- if self.client.last_updated == SOURCE_EVENT:
-
- if self.client.event.event in CLIENT_BLOCKED + CLIENT_UNBLOCKED:
- self._is_blocked = self.client.event.event in CLIENT_BLOCKED
+ if (
+ self.client.last_updated == SOURCE_EVENT
+ and self.client.event.event in CLIENT_BLOCKED + CLIENT_UNBLOCKED
+ ):
+ self._is_blocked = self.client.event.event in CLIENT_BLOCKED
super().async_update_callback()
diff --git a/homeassistant/components/unifi/translations/ca.json b/homeassistant/components/unifi/translations/ca.json
index a07c034fe12a4f..f1cf4a6349b17b 100644
--- a/homeassistant/components/unifi/translations/ca.json
+++ b/homeassistant/components/unifi/translations/ca.json
@@ -1,13 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "El lloc del controlador ja est\u00e0 configurat"
+ "already_configured": "El lloc del controlador ja est\u00e0 configurat",
+ "configuration_updated": "S'ha actualitzat la configuraci\u00f3.",
+ "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament"
},
"error": {
"faulty_credentials": "[%key::common::config_flow::error::invalid_auth%]",
"service_unavailable": "[%key::common::config_flow::error::cannot_connect%]",
"unknown_client_mac": "No hi ha cap client disponible en aquesta adre\u00e7a MAC"
},
+ "flow_title": "Xarxa UniFi {site} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/unifi/translations/cs.json b/homeassistant/components/unifi/translations/cs.json
index 1247a97de9dd15..0281dfbb7500c1 100644
--- a/homeassistant/components/unifi/translations/cs.json
+++ b/homeassistant/components/unifi/translations/cs.json
@@ -1,13 +1,15 @@
{
"config": {
"abort": {
- "already_configured": "Ovlada\u010d je ji\u017e nastaven"
+ "already_configured": "Ovlada\u010d je ji\u017e nastaven",
+ "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9"
},
"error": {
"faulty_credentials": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
"service_unavailable": "Nepoda\u0159ilo se p\u0159ipojit",
"unknown_client_mac": "Na t\u00e9to MAC adrese nen\u00ed dostupn\u00fd \u017e\u00e1dn\u00fd klient"
},
+ "flow_title": "UniFi s\u00ed\u0165 {site} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/unifi/translations/da.json b/homeassistant/components/unifi/translations/da.json
index 15ec878f1cee37..84dafd36e1a45c 100644
--- a/homeassistant/components/unifi/translations/da.json
+++ b/homeassistant/components/unifi/translations/da.json
@@ -7,6 +7,7 @@
"faulty_credentials": "Ugyldige legitimationsoplysninger",
"service_unavailable": "Service utilg\u00e6ngelig"
},
+ "flow_title": "UniFi-netv\u00e6rket {site} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/unifi/translations/de.json b/homeassistant/components/unifi/translations/de.json
index 626236792ea947..b1d3e495f94250 100644
--- a/homeassistant/components/unifi/translations/de.json
+++ b/homeassistant/components/unifi/translations/de.json
@@ -1,13 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "Controller-Site ist bereits konfiguriert"
+ "already_configured": "Controller-Site ist bereits konfiguriert",
+ "configuration_updated": "Konfiguration aktualisiert.",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
},
"error": {
- "faulty_credentials": "Ung\u00fcltige Anmeldeinformationen",
+ "faulty_credentials": "Ung\u00fcltige Authentifizierung",
"service_unavailable": "Verbindung fehlgeschlagen",
"unknown_client_mac": "Unter dieser MAC-Adresse ist kein Client verf\u00fcgbar."
},
+ "flow_title": "UniFi Netzwerk {site} ({host})",
"step": {
"user": {
"data": {
@@ -16,7 +19,7 @@
"port": "Port",
"site": "Site-ID",
"username": "Benutzername",
- "verify_ssl": "Controller mit ordnungsgem\u00e4ssem Zertifikat"
+ "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen"
},
"title": "UniFi-Controller einrichten"
}
@@ -51,7 +54,9 @@
},
"simple_options": {
"data": {
- "track_clients": "Netzwerk Ger\u00e4te \u00fcberwachen"
+ "block_client": "Clients mit Netzwerkzugriffskontrolle",
+ "track_clients": "Netzwerger\u00e4te \u00fcberwachen",
+ "track_devices": "Verfolgen von Netzwerkger\u00e4ten (Ubiquiti-Ger\u00e4te)"
},
"description": "Konfigurieren Sie die UniFi-Integration"
},
diff --git a/homeassistant/components/unifi/translations/en.json b/homeassistant/components/unifi/translations/en.json
index 06e8ae1eb60ab3..41507faa430c1d 100644
--- a/homeassistant/components/unifi/translations/en.json
+++ b/homeassistant/components/unifi/translations/en.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "Controller site is already configured",
+ "configuration_updated": "Configuration updated.",
"reauth_successful": "Re-authentication was successful"
},
"error": {
diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json
index 0fa4aaf2eb7a38..a5963d7019ef2e 100644
--- a/homeassistant/components/unifi/translations/es.json
+++ b/homeassistant/components/unifi/translations/es.json
@@ -1,13 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "El sitio del controlador ya est\u00e1 configurado"
+ "already_configured": "El sitio del controlador ya est\u00e1 configurado",
+ "configuration_updated": "Configuraci\u00f3n actualizada.",
+ "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente"
},
"error": {
"faulty_credentials": "Autenticaci\u00f3n no v\u00e1lida",
"service_unavailable": "Error al conectar",
"unknown_client_mac": "Ning\u00fan cliente disponible en esa direcci\u00f3n MAC"
},
+ "flow_title": "Red UniFi {site} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/unifi/translations/et.json b/homeassistant/components/unifi/translations/et.json
index 8e95da9aa5b265..e9d76520435cf1 100644
--- a/homeassistant/components/unifi/translations/et.json
+++ b/homeassistant/components/unifi/translations/et.json
@@ -1,13 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "Kontroller on juba seadistatud"
+ "already_configured": "Kontroller on juba seadistatud",
+ "configuration_updated": "Seaded on v\u00e4rskendatud.",
+ "reauth_successful": "Taastuvastamine \u00f5nnestus"
},
"error": {
"faulty_credentials": "Tuvastamine nurjus",
"service_unavailable": "\u00dchendamine nurjus",
"unknown_client_mac": "Sellel MAC-aadressil pole \u00fchtegi klienti saadaval"
},
+ "flow_title": "UniFi Network {site} ( {host} )",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/unifi/translations/fr.json b/homeassistant/components/unifi/translations/fr.json
index 6e5412ba3d2204..d750fb0cdd94e6 100644
--- a/homeassistant/components/unifi/translations/fr.json
+++ b/homeassistant/components/unifi/translations/fr.json
@@ -1,13 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "Le contr\u00f4leur est d\u00e9j\u00e0 configur\u00e9"
+ "already_configured": "Le contr\u00f4leur est d\u00e9j\u00e0 configur\u00e9",
+ "configuration_updated": "Configuration mise \u00e0 jour.",
+ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi"
},
"error": {
"faulty_credentials": "Authentification invalide",
"service_unavailable": "\u00c9chec de connexion",
"unknown_client_mac": "Aucun client disponible sur cette adresse MAC"
},
+ "flow_title": "UniFi Network {site} ( {host} )",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/unifi/translations/he.json b/homeassistant/components/unifi/translations/he.json
new file mode 100644
index 00000000000000..3007c0e968c1dc
--- /dev/null
+++ b/homeassistant/components/unifi/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/translations/hu.json b/homeassistant/components/unifi/translations/hu.json
index 2a7a43d42e9565..4602193850f226 100644
--- a/homeassistant/components/unifi/translations/hu.json
+++ b/homeassistant/components/unifi/translations/hu.json
@@ -1,9 +1,14 @@
{
"config": {
+ "abort": {
+ "configuration_updated": "A konfigur\u00e1ci\u00f3 friss\u00edtve.",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
+ },
"error": {
"faulty_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"service_unavailable": "Sikertelen csatlakoz\u00e1s"
},
+ "flow_title": "UniFi Network {site} ({host})",
"step": {
"user": {
"data": {
@@ -12,7 +17,7 @@
"port": "Port",
"site": "Site azonos\u00edt\u00f3",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v",
- "verify_ssl": "Vez\u00e9rl\u0151 megfelel\u0151 tan\u00fas\u00edtv\u00e1nnyal"
+ "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se"
},
"title": "UniFi vez\u00e9rl\u0151 be\u00e1ll\u00edt\u00e1sa"
}
@@ -23,6 +28,9 @@
"client_control": {
"description": "Konfigur\u00e1lja a klienseket\n\n Hozzon l\u00e9tre kapcsol\u00f3kat azokhoz a sorsz\u00e1mokhoz, amelyeknek vez\u00e9relni k\u00edv\u00e1nja a h\u00e1l\u00f3zati hozz\u00e1f\u00e9r\u00e9st."
},
+ "simple_options": {
+ "description": "UniFi integr\u00e1ci\u00f3 konfigur\u00e1l\u00e1sa"
+ },
"statistics_sensors": {
"data": {
"allow_bandwidth_sensors": "S\u00e1vsz\u00e9less\u00e9g-haszn\u00e1lati \u00e9rz\u00e9kel\u0151k l\u00e9trehoz\u00e1sa a h\u00e1l\u00f3zati \u00fcgyfelek sz\u00e1m\u00e1ra"
diff --git a/homeassistant/components/unifi/translations/id.json b/homeassistant/components/unifi/translations/id.json
new file mode 100644
index 00000000000000..7a707b28aa02f8
--- /dev/null
+++ b/homeassistant/components/unifi/translations/id.json
@@ -0,0 +1,69 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Situs Controller sudah dikonfigurasi",
+ "configuration_updated": "Konfigurasi diperbarui.",
+ "reauth_successful": "Autentikasi ulang berhasil"
+ },
+ "error": {
+ "faulty_credentials": "Autentikasi tidak valid",
+ "service_unavailable": "Gagal terhubung",
+ "unknown_client_mac": "Tidak ada klien yang tersedia di alamat MAC tersebut"
+ },
+ "flow_title": "UniFi Network {site} ({host})",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Kata Sandi",
+ "port": "Port",
+ "site": "ID Site",
+ "username": "Nama Pengguna",
+ "verify_ssl": "Verifikasi sertifikat SSL"
+ },
+ "title": "Siapkan UniFi Controller"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "client_control": {
+ "data": {
+ "block_client": "Klien yang dikontrol akses jaringan",
+ "dpi_restrictions": "Izinkan kontrol grup pembatasan DPI",
+ "poe_clients": "Izinkan kontrol POE klien"
+ },
+ "description": "Konfigurasikan kontrol klien \n\nBuat sakelar untuk nomor seri yang ingin dikontrol akses jaringannya.",
+ "title": "Opsi UniFi 2/3"
+ },
+ "device_tracker": {
+ "data": {
+ "detection_time": "Tenggang waktu dalam detik dari terakhir terlihat hingga dianggap sebagai keluar",
+ "ignore_wired_bug": "Nonaktifkan bug logika kabel UniFi",
+ "ssid_filter": "Pilih SSID untuk melacak klien nirkabel",
+ "track_clients": "Lacak klien jaringan",
+ "track_devices": "Lacak perangkat jaringan (perangkat Ubiquiti)",
+ "track_wired_clients": "Sertakan klien jaringan berkabel"
+ },
+ "description": "Konfigurasikan pelacakan perangkat",
+ "title": "Opsi UniFi 1/3"
+ },
+ "simple_options": {
+ "data": {
+ "block_client": "Klien yang dikontrol akses jaringan",
+ "track_clients": "Lacak klien jaringan",
+ "track_devices": "Lacak perangkat jaringan (perangkat Ubiquiti)"
+ },
+ "description": "Konfigurasikan integrasi UniFi"
+ },
+ "statistics_sensors": {
+ "data": {
+ "allow_bandwidth_sensors": "Sensor penggunaan bandwidth untuk klien jaringan",
+ "allow_uptime_sensors": "Sensor waktu kerja untuk klien jaringan"
+ },
+ "description": "Konfigurasikan sensor statistik",
+ "title": "Opsi UniFi 3/3"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json
index 79a7206923e6c1..f5311f538c1428 100644
--- a/homeassistant/components/unifi/translations/it.json
+++ b/homeassistant/components/unifi/translations/it.json
@@ -1,13 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "Il sito del Controller \u00e8 gi\u00e0 configurato"
+ "already_configured": "Il sito del Controller \u00e8 gi\u00e0 configurato",
+ "configuration_updated": "Configurazione aggiornata.",
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
},
"error": {
"faulty_credentials": "Autenticazione non valida",
"service_unavailable": "Impossibile connettersi",
"unknown_client_mac": "Nessun client disponibile su quell'indirizzo MAC"
},
+ "flow_title": "Rete UniFi {site} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/unifi/translations/ko.json b/homeassistant/components/unifi/translations/ko.json
index 94160829bad4c6..3a13b420097a39 100644
--- a/homeassistant/components/unifi/translations/ko.json
+++ b/homeassistant/components/unifi/translations/ko.json
@@ -1,13 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "\ucee8\ud2b8\ub864\ub7ec \uc0ac\uc774\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\ucee8\ud2b8\ub864\ub7ec \uc0ac\uc774\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "configuration_updated": "\uad6c\uc131\uc774 \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
"faulty_credentials": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"service_unavailable": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"unknown_client_mac": "\ud574\ub2f9 MAC \uc8fc\uc18c\uc5d0\uc11c \uc0ac\uc6a9 \uac00\ub2a5\ud55c \ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \uc5c6\uc2b5\ub2c8\ub2e4."
},
+ "flow_title": "UniFi \ub124\ud2b8\uc6cc\ud06c: {site} ({host})",
"step": {
"user": {
"data": {
@@ -16,7 +19,7 @@
"port": "\ud3ec\ud2b8",
"site": "\uc0ac\uc774\ud2b8 ID",
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984",
- "verify_ssl": "\uc62c\ubc14\ub978 \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\ub294 \ucee8\ud2b8\ub864\ub7ec"
+ "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778"
},
"title": "UniFi \ucee8\ud2b8\ub864\ub7ec \uc124\uc815\ud558\uae30"
}
@@ -26,10 +29,11 @@
"step": {
"client_control": {
"data": {
- "block_client": "\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4 \uc81c\uc5b4 \ud074\ub77c\uc774\uc5b8\ud2b8",
- "poe_clients": "\ud074\ub77c\uc774\uc5b8\ud2b8\uc758 POE \uc81c\uc5b4 \ud5c8\uc6a9"
+ "block_client": "\ub124\ud2b8\uc6cc\ud06c \uc811\uadfc \uc81c\uc5b4 \ud074\ub77c\uc774\uc5b8\ud2b8",
+ "dpi_restrictions": "DPI \uc81c\ud55c \uadf8\ub8f9\uc758 \uc81c\uc5b4 \ud5c8\uc6a9\ud558\uae30",
+ "poe_clients": "\ud074\ub77c\uc774\uc5b8\ud2b8\uc758 POE \uc81c\uc5b4 \ud5c8\uc6a9\ud558\uae30"
},
- "description": "\ud074\ub77c\uc774\uc5b8\ud2b8 \ucee8\ud2b8\ub864 \uad6c\uc131 \n\n\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4\ub97c \uc81c\uc5b4\ud558\ub824\ub294 \uc2dc\ub9ac\uc5bc \ubc88\ud638\uc5d0 \ub300\ud55c \uc2a4\uc704\uce58\ub97c \ub9cc\ub4ed\ub2c8\ub2e4.",
+ "description": "\ud074\ub77c\uc774\uc5b8\ud2b8 \ucee8\ud2b8\ub864 \uad6c\uc131 \n\n\ub124\ud2b8\uc6cc\ud06c \uc811\uadfc\uc744 \uc81c\uc5b4\ud558\ub824\ub294 \uc2dc\ub9ac\uc5bc \ubc88\ud638\uc5d0 \ub300\ud55c \uc2a4\uc704\uce58\ub97c \ub9cc\ub4ed\ub2c8\ub2e4.",
"title": "UniFi \uc635\uc158 2/3"
},
"device_tracker": {
@@ -46,7 +50,7 @@
},
"simple_options": {
"data": {
- "block_client": "\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4 \uc81c\uc5b4 \ud074\ub77c\uc774\uc5b8\ud2b8",
+ "block_client": "\ub124\ud2b8\uc6cc\ud06c \uc811\uadfc \uc81c\uc5b4 \ud074\ub77c\uc774\uc5b8\ud2b8",
"track_clients": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ucd94\uc801 \ub300\uc0c1",
"track_devices": "\ub124\ud2b8\uc6cc\ud06c \uae30\uae30 \ucd94\uc801 (Ubiquiti \uae30\uae30)"
},
@@ -55,7 +59,7 @@
"statistics_sensors": {
"data": {
"allow_bandwidth_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ub300\uc5ed\ud3ed \uc0ac\uc6a9\ub7c9 \uc13c\uc11c",
- "allow_uptime_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8\ub97c \uc704\ud55c \uac00\ub3d9 \uc2dc\uac04 \uc13c\uc11c"
+ "allow_uptime_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0 \ub300\ud55c \uac00\ub3d9 \uc2dc\uac04 \uc13c\uc11c"
},
"description": "\ud1b5\uacc4 \uc13c\uc11c \uad6c\uc131",
"title": "UniFi \uc635\uc158 3/3"
diff --git a/homeassistant/components/unifi/translations/lb.json b/homeassistant/components/unifi/translations/lb.json
index 1e4870e6ab8d43..5c939a0105a47e 100644
--- a/homeassistant/components/unifi/translations/lb.json
+++ b/homeassistant/components/unifi/translations/lb.json
@@ -8,6 +8,7 @@
"service_unavailable": "Feeler beim verbannen",
"unknown_client_mac": "Kee Cliwent mat der MAC Adress disponibel"
},
+ "flow_title": "UniFi Network {site} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json
index 4e9aa16a245259..e5e5e3a1dfba9a 100644
--- a/homeassistant/components/unifi/translations/nl.json
+++ b/homeassistant/components/unifi/translations/nl.json
@@ -1,13 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "Controller site is al geconfigureerd"
+ "already_configured": "Controller site is al geconfigureerd",
+ "configuration_updated": "Configuratie bijgewerkt.",
+ "reauth_successful": "Herauthenticatie was succesvol"
},
"error": {
- "faulty_credentials": "Foutieve gebruikersgegevens",
- "service_unavailable": "Geen service beschikbaar",
+ "faulty_credentials": "Ongeldige authenticatie",
+ "service_unavailable": "Kan geen verbinding maken",
"unknown_client_mac": "Geen client beschikbaar op dat MAC-adres"
},
+ "flow_title": "UniFi Netwerk {site} ({host})",
"step": {
"user": {
"data": {
@@ -16,7 +19,7 @@
"port": "Poort",
"site": "Site ID",
"username": "Gebruikersnaam",
- "verify_ssl": "Controller gebruik van het juiste certificaat"
+ "verify_ssl": "SSL-certificaat verifi\u00ebren"
},
"title": "Stel de UniFi-controller in"
}
@@ -27,6 +30,7 @@
"client_control": {
"data": {
"block_client": "Cli\u00ebnten met netwerktoegang",
+ "dpi_restrictions": "Sta controle van DPI-beperkingsgroepen toe",
"poe_clients": "Sta POE-controle van gebruikers toe"
},
"description": "Configureer clientbesturingen \n\n Maak schakelaars voor serienummers waarvoor u de netwerktoegang wilt beheren.",
diff --git a/homeassistant/components/unifi/translations/no.json b/homeassistant/components/unifi/translations/no.json
index 5cda9ad7ab5748..72944a9d540b8b 100644
--- a/homeassistant/components/unifi/translations/no.json
+++ b/homeassistant/components/unifi/translations/no.json
@@ -1,13 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "Kontroller nettstedet er allerede konfigurert"
+ "already_configured": "Kontroller nettstedet er allerede konfigurert",
+ "configuration_updated": "Konfigurasjonen er oppdatert.",
+ "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket"
},
"error": {
"faulty_credentials": "Ugyldig godkjenning",
"service_unavailable": "Tilkobling mislyktes",
"unknown_client_mac": "Ingen klient tilgjengelig p\u00e5 den MAC-adressen"
},
+ "flow_title": "UniFi-nettverk {site} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/unifi/translations/pl.json b/homeassistant/components/unifi/translations/pl.json
index 8ff5f1e4793163..719120eeb5ee18 100644
--- a/homeassistant/components/unifi/translations/pl.json
+++ b/homeassistant/components/unifi/translations/pl.json
@@ -1,13 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana"
+ "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana",
+ "configuration_updated": "Konfiguracja zaktualizowana",
+ "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119"
},
"error": {
"faulty_credentials": "Niepoprawne uwierzytelnienie",
"service_unavailable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"unknown_client_mac": "Brak klienta z tym adresem MAC"
},
+ "flow_title": "Sie\u0107 UniFi {site} ({host})",
"step": {
"user": {
"data": {
@@ -67,7 +70,7 @@
"allow_uptime_sensors": "Sensory czasu pracy dla klient\u00f3w sieciowych"
},
"description": "Konfiguracja sensora statystyk",
- "title": "Opcje UniFi"
+ "title": "Opcje UniFi 3/3"
}
}
}
diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json
index 789212dca17efe..769287bb9751da 100644
--- a/homeassistant/components/unifi/translations/ru.json
+++ b/homeassistant/components/unifi/translations/ru.json
@@ -1,13 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
+ "configuration_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
},
"error": {
- "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"service_unavailable": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"unknown_client_mac": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043d\u0430 \u044d\u0442\u043e\u043c MAC-\u0430\u0434\u0440\u0435\u0441\u0435."
},
+ "flow_title": "UniFi Network {site} ({host})",
"step": {
"user": {
"data": {
@@ -15,7 +18,7 @@
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
"site": "ID \u0441\u0430\u0439\u0442\u0430",
- "username": "\u041b\u043e\u0433\u0438\u043d",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f",
"verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL"
},
"title": "UniFi Controller"
diff --git a/homeassistant/components/unifi/translations/sv.json b/homeassistant/components/unifi/translations/sv.json
index eb4e0d8ee3d43d..2e4851e70ed026 100644
--- a/homeassistant/components/unifi/translations/sv.json
+++ b/homeassistant/components/unifi/translations/sv.json
@@ -24,13 +24,17 @@
},
"options": {
"step": {
+ "client_control": {
+ "title": "UniFi-inst\u00e4llningar 2/3"
+ },
"device_tracker": {
"data": {
"detection_time": "Tid i sekunder fr\u00e5n senast sett tills den anses borta",
"track_clients": "Sp\u00e5ra n\u00e4tverksklienter",
"track_devices": "Sp\u00e5ra n\u00e4tverksenheter (Ubiquiti-enheter)",
"track_wired_clients": "Inkludera tr\u00e5dbundna n\u00e4tverksklienter"
- }
+ },
+ "title": "UniFi-inst\u00e4llningar 1/3"
},
"init": {
"data": {
@@ -44,7 +48,8 @@
"statistics_sensors": {
"data": {
"allow_bandwidth_sensors": "Skapa bandbreddsanv\u00e4ndningssensorer f\u00f6r n\u00e4tverksklienter"
- }
+ },
+ "title": "UniFi-inst\u00e4llningar 2/3"
}
}
}
diff --git a/homeassistant/components/unifi/translations/tr.json b/homeassistant/components/unifi/translations/tr.json
index 903a7aaa21f7a0..c39fa08217aa3a 100644
--- a/homeassistant/components/unifi/translations/tr.json
+++ b/homeassistant/components/unifi/translations/tr.json
@@ -1,9 +1,22 @@
{
"config": {
+ "abort": {
+ "already_configured": "Denetleyici sitesi zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "configuration_updated": "Yap\u0131land\u0131rma g\u00fcncellendi.",
+ "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu"
+ },
+ "error": {
+ "faulty_credentials": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "service_unavailable": "Ba\u011flanma hatas\u0131",
+ "unknown_client_mac": "Bu MAC adresinde kullan\u0131labilir istemci yok"
+ },
+ "flow_title": "UniFi A\u011f\u0131 {site} ( {host} )",
"step": {
"user": {
"data": {
+ "host": "Ana Bilgisayar",
"password": "Parola",
+ "port": "Port",
"username": "Kullan\u0131c\u0131 ad\u0131"
}
}
diff --git a/homeassistant/components/unifi/translations/uk.json b/homeassistant/components/unifi/translations/uk.json
new file mode 100644
index 00000000000000..0f83c35840a8d7
--- /dev/null
+++ b/homeassistant/components/unifi/translations/uk.json
@@ -0,0 +1,66 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435."
+ },
+ "error": {
+ "faulty_credentials": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "service_unavailable": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "unknown_client_mac": "\u041d\u0435\u043c\u0430\u0454 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432 \u043d\u0430 \u0446\u0456\u0439 MAC-\u0430\u0434\u0440\u0435\u0441\u0456."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "site": "ID \u0441\u0430\u0439\u0442\u0443",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430",
+ "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL"
+ },
+ "title": "UniFi Controller"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "client_control": {
+ "data": {
+ "block_client": "\u041a\u043b\u0456\u0454\u043d\u0442\u0438 \u0437 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u043c \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0443",
+ "dpi_restrictions": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f \u0433\u0440\u0443\u043f\u0430\u043c\u0438 \u043e\u0431\u043c\u0435\u0436\u0435\u043d\u044c DPI",
+ "poe_clients": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 POE \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0435\u043b\u0435\u043c\u0435\u043d\u0442\u0456\u0432 \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f. \n\n\u0421\u0442\u0432\u043e\u0440\u0456\u0442\u044c \u043f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u0447\u0456 \u0434\u043b\u044f \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u0445 \u043d\u043e\u043c\u0435\u0440\u0456\u0432, \u0434\u043b\u044f \u044f\u043a\u0438\u0445 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044e\u0432\u0430\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u043c\u0435\u0440\u0435\u0436\u0456.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f UniFi. \u041a\u0440\u043e\u043a 2."
+ },
+ "device_tracker": {
+ "data": {
+ "detection_time": "\u0427\u0430\u0441 \u0432\u0456\u0434 \u043e\u0441\u0442\u0430\u043d\u043d\u044c\u043e\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0443 \u0437\u0432'\u044f\u0437\u043a\u0443 \u0437 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0437\u0430\u043a\u0456\u043d\u0447\u0435\u043d\u043d\u044e \u044f\u043a\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043e\u0442\u0440\u0438\u043c\u0430\u0454 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\".",
+ "ignore_wired_bug": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043b\u043e\u0433\u0456\u043a\u0443 \u043f\u043e\u043c\u0438\u043b\u043a\u0438 \u0434\u043b\u044f \u0434\u0440\u043e\u0442\u043e\u0432\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432 UniFi",
+ "ssid_filter": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c SSID \u0434\u043b\u044f \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u0431\u0435\u0437\u0434\u0440\u043e\u0442\u043e\u0432\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432",
+ "track_clients": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432 \u043c\u0435\u0440\u0435\u0436\u0456",
+ "track_devices": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 (\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 Ubiquiti)",
+ "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043f\u0440\u043e\u0432\u0456\u0434\u043d\u0438\u0445 \u043c\u0435\u0440\u0435\u0436\u043d\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f UniFi. \u041a\u0440\u043e\u043a 1"
+ },
+ "simple_options": {
+ "data": {
+ "block_client": "\u041a\u043b\u0456\u0454\u043d\u0442\u0438 \u0437 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u043c \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0443",
+ "track_clients": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432 \u043c\u0435\u0440\u0435\u0436\u0456",
+ "track_devices": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 (\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 Ubiquiti)"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 UniFi."
+ },
+ "statistics_sensors": {
+ "data": {
+ "allow_bandwidth_sensors": "\u0414\u0430\u0442\u0447\u0438\u043a\u0438 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u043d\u043e\u0457 \u0437\u0434\u0430\u0442\u043d\u043e\u0441\u0442\u0456 \u0434\u043b\u044f \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432",
+ "allow_uptime_sensors": "\u0421\u0435\u043d\u0441\u043e\u0440\u0438 \u0447\u0430\u0441\u0443 \u0440\u043e\u0431\u043e\u0442\u0438 \u0434\u043b\u044f \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0438",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f UniFi. \u043a\u0440\u043e\u043a 3"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/translations/zh-Hant.json b/homeassistant/components/unifi/translations/zh-Hant.json
index d87f8cf51e08e1..add0a387309e7e 100644
--- a/homeassistant/components/unifi/translations/zh-Hant.json
+++ b/homeassistant/components/unifi/translations/zh-Hant.json
@@ -1,13 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "\u63a7\u5236\u5668\u4f4d\u5740\u5df2\u7d93\u8a2d\u5b9a"
+ "already_configured": "\u63a7\u5236\u5668\u4f4d\u5740\u5df2\u7d93\u8a2d\u5b9a",
+ "configuration_updated": "\u8a2d\u5b9a\u5df2\u66f4\u65b0\u3002",
+ "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f"
},
"error": {
"faulty_credentials": "\u9a57\u8b49\u78bc\u7121\u6548",
"service_unavailable": "\u9023\u7dda\u5931\u6557",
"unknown_client_mac": "\u8a72 Mac \u4f4d\u5740\u7121\u53ef\u7528\u5ba2\u6236\u7aef"
},
+ "flow_title": "UniFi Network {site} ({host})",
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/unifi/unifi_entity_base.py b/homeassistant/components/unifi/unifi_entity_base.py
index 904348f6324b12..03c63ce4e84a8a 100644
--- a/homeassistant/components/unifi/unifi_entity_base.py
+++ b/homeassistant/components/unifi/unifi_entity_base.py
@@ -91,7 +91,7 @@ async def remove_item(self, keys: set) -> None:
entity_registry = await self.hass.helpers.entity_registry.async_get_registry()
entity_entry = entity_registry.async_get(self.entity_id)
if not entity_entry:
- await self.async_remove()
+ await self.async_remove(force_remove=True)
return
device_registry = await self.hass.helpers.device_registry.async_get_registry()
diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py
index 8fff0e80dfb872..f6e770c126f254 100644
--- a/homeassistant/components/universal/media_player.py
+++ b/homeassistant/components/universal/media_player.py
@@ -1,9 +1,15 @@
"""Combination of multiple media players for a universal controller."""
+from __future__ import annotations
+
from copy import copy
import voluptuous as vol
-from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
+from homeassistant.components.media_player import (
+ DEVICE_CLASSES_SCHEMA,
+ PLATFORM_SCHEMA,
+ MediaPlayerEntity,
+)
from homeassistant.components.media_player.const import (
ATTR_APP_ID,
ATTR_APP_NAME,
@@ -20,6 +26,7 @@
ATTR_MEDIA_PLAYLIST,
ATTR_MEDIA_POSITION,
ATTR_MEDIA_POSITION_UPDATED_AT,
+ ATTR_MEDIA_REPEAT,
ATTR_MEDIA_SEASON,
ATTR_MEDIA_SEEK_POSITION,
ATTR_MEDIA_SERIES_TITLE,
@@ -28,13 +35,23 @@
ATTR_MEDIA_TRACK,
ATTR_MEDIA_VOLUME_LEVEL,
ATTR_MEDIA_VOLUME_MUTED,
+ ATTR_SOUND_MODE,
+ ATTR_SOUND_MODE_LIST,
DOMAIN,
SERVICE_CLEAR_PLAYLIST,
SERVICE_PLAY_MEDIA,
+ SERVICE_SELECT_SOUND_MODE,
SERVICE_SELECT_SOURCE,
SUPPORT_CLEAR_PLAYLIST,
+ SUPPORT_NEXT_TRACK,
+ SUPPORT_PAUSE,
+ SUPPORT_PLAY,
+ SUPPORT_PREVIOUS_TRACK,
+ SUPPORT_REPEAT_SET,
+ SUPPORT_SELECT_SOUND_MODE,
SUPPORT_SELECT_SOURCE,
SUPPORT_SHUFFLE_SET,
+ SUPPORT_STOP,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE,
@@ -45,6 +62,7 @@
ATTR_ENTITY_ID,
ATTR_ENTITY_PICTURE,
ATTR_SUPPORTED_FEATURES,
+ CONF_DEVICE_CLASS,
CONF_NAME,
CONF_STATE,
CONF_STATE_TEMPLATE,
@@ -55,7 +73,9 @@
SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_MEDIA_SEEK,
SERVICE_MEDIA_STOP,
+ SERVICE_REPEAT_SET,
SERVICE_SHUFFLE_SET,
+ SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
SERVICE_VOLUME_DOWN,
@@ -75,13 +95,10 @@
from homeassistant.helpers.service import async_call_from_config
ATTR_ACTIVE_CHILD = "active_child"
-ATTR_DATA = "data"
CONF_ATTRS = "attributes"
CONF_CHILDREN = "children"
CONF_COMMANDS = "commands"
-CONF_SERVICE = "service"
-CONF_SERVICE_DATA = "service_data"
OFF_STATES = [STATE_IDLE, STATE_OFF, STATE_UNAVAILABLE]
@@ -96,6 +113,7 @@
vol.Optional(CONF_ATTRS, default={}): vol.Or(
cv.ensure_list(ATTRS_SCHEMA), ATTRS_SCHEMA
),
+ vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_TEMPLATE): cv.template,
},
extra=vol.REMOVE_EXTRA,
@@ -104,7 +122,6 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the universal media players."""
-
await async_setup_reload_service(hass, "universal", ["media_player"])
player = UniversalMediaPlayer(
@@ -113,6 +130,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
config.get(CONF_CHILDREN),
config.get(CONF_COMMANDS),
config.get(CONF_ATTRS),
+ config.get(CONF_DEVICE_CLASS),
config.get(CONF_STATE_TEMPLATE),
)
@@ -122,7 +140,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class UniversalMediaPlayer(MediaPlayerEntity):
"""Representation of an universal media player."""
- def __init__(self, hass, name, children, commands, attributes, state_template=None):
+ def __init__(
+ self,
+ hass,
+ name,
+ children,
+ commands,
+ attributes,
+ device_class=None,
+ state_template=None,
+ ):
"""Initialize the Universal media device."""
self.hass = hass
self._name = name
@@ -137,6 +164,7 @@ def __init__(self, hass, name, children, commands, attributes, state_template=No
self._child_state = None
self._state_template_result = None
self._state_template = state_template
+ self._device_class = device_class
async def async_added_to_hass(self):
"""Subscribe to children and template state changes."""
@@ -242,6 +270,11 @@ def should_poll(self):
"""No polling needed."""
return False
+ @property
+ def device_class(self) -> str | None:
+ """Return the class of this device."""
+ return self._device_class
+
@property
def master_state(self):
"""Return the master state for entity or None."""
@@ -382,6 +415,16 @@ def app_name(self):
"""Name of the current running app."""
return self._child_attr(ATTR_APP_NAME)
+ @property
+ def sound_mode(self):
+ """Return the current sound mode of the device."""
+ return self._override_or_child_attr(ATTR_SOUND_MODE)
+
+ @property
+ def sound_mode_list(self):
+ """List of available sound modes."""
+ return self._override_or_child_attr(ATTR_SOUND_MODE_LIST)
+
@property
def source(self):
"""Return the current input source of the device."""
@@ -392,6 +435,11 @@ def source_list(self):
"""List of available input sources."""
return self._override_or_child_attr(ATTR_INPUT_SOURCE_LIST)
+ @property
+ def repeat(self):
+ """Boolean if repeating is enabled."""
+ return self._override_or_child_attr(ATTR_MEDIA_REPEAT)
+
@property
def shuffle(self):
"""Boolean if shuffling is enabled."""
@@ -407,7 +455,23 @@ def supported_features(self):
if SERVICE_TURN_OFF in self._cmds:
flags |= SUPPORT_TURN_OFF
- if any([cmd in self._cmds for cmd in [SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN]]):
+ if SERVICE_MEDIA_PLAY_PAUSE in self._cmds:
+ flags |= SUPPORT_PLAY | SUPPORT_PAUSE
+ else:
+ if SERVICE_MEDIA_PLAY in self._cmds:
+ flags |= SUPPORT_PLAY
+ if SERVICE_MEDIA_PAUSE in self._cmds:
+ flags |= SUPPORT_PAUSE
+
+ if SERVICE_MEDIA_STOP in self._cmds:
+ flags |= SUPPORT_STOP
+
+ if SERVICE_MEDIA_NEXT_TRACK in self._cmds:
+ flags |= SUPPORT_NEXT_TRACK
+ if SERVICE_MEDIA_PREVIOUS_TRACK in self._cmds:
+ flags |= SUPPORT_PREVIOUS_TRACK
+
+ if any(cmd in self._cmds for cmd in [SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN]):
flags |= SUPPORT_VOLUME_STEP
if SERVICE_VOLUME_SET in self._cmds:
flags |= SUPPORT_VOLUME_SET
@@ -415,7 +479,10 @@ def supported_features(self):
if SERVICE_VOLUME_MUTE in self._cmds and ATTR_MEDIA_VOLUME_MUTED in self._attrs:
flags |= SUPPORT_VOLUME_MUTE
- if SERVICE_SELECT_SOURCE in self._cmds:
+ if (
+ SERVICE_SELECT_SOURCE in self._cmds
+ and ATTR_INPUT_SOURCE_LIST in self._attrs
+ ):
flags |= SUPPORT_SELECT_SOURCE
if SERVICE_CLEAR_PLAYLIST in self._cmds:
@@ -424,10 +491,19 @@ def supported_features(self):
if SERVICE_SHUFFLE_SET in self._cmds and ATTR_MEDIA_SHUFFLE in self._attrs:
flags |= SUPPORT_SHUFFLE_SET
+ if SERVICE_REPEAT_SET in self._cmds and ATTR_MEDIA_REPEAT in self._attrs:
+ flags |= SUPPORT_REPEAT_SET
+
+ if (
+ SERVICE_SELECT_SOUND_MODE in self._cmds
+ and ATTR_SOUND_MODE_LIST in self._attrs
+ ):
+ flags |= SUPPORT_SELECT_SOUND_MODE
+
return flags
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
active_child = self._child_state
return {ATTR_ACTIVE_CHILD: active_child.entity_id} if active_child else {}
@@ -502,6 +578,13 @@ async def async_media_play_pause(self):
"""Play or pause the media player."""
await self._async_call_service(SERVICE_MEDIA_PLAY_PAUSE)
+ async def async_select_sound_mode(self, sound_mode):
+ """Select sound mode."""
+ data = {ATTR_SOUND_MODE: sound_mode}
+ await self._async_call_service(
+ SERVICE_SELECT_SOUND_MODE, data, allow_override=True
+ )
+
async def async_select_source(self, source):
"""Set the input source."""
data = {ATTR_INPUT_SOURCE: source}
@@ -516,6 +599,15 @@ async def async_set_shuffle(self, shuffle):
data = {ATTR_MEDIA_SHUFFLE: shuffle}
await self._async_call_service(SERVICE_SHUFFLE_SET, data, allow_override=True)
+ async def async_set_repeat(self, repeat):
+ """Set repeat mode."""
+ data = {ATTR_MEDIA_REPEAT: repeat}
+ await self._async_call_service(SERVICE_REPEAT_SET, data, allow_override=True)
+
+ async def async_toggle(self):
+ """Toggle the power on the media player."""
+ await self._async_call_service(SERVICE_TOGGLE)
+
async def async_update(self):
"""Update state in HA."""
for child_name in self._children:
diff --git a/homeassistant/components/universal/services.yaml b/homeassistant/components/universal/services.yaml
index ed8f550275e385..8b515151fd9bae 100644
--- a/homeassistant/components/universal/services.yaml
+++ b/homeassistant/components/universal/services.yaml
@@ -1,2 +1,2 @@
reload:
- description: Reload all universal entities.
+ description: Reload all universal entities
diff --git a/homeassistant/components/upb/__init__.py b/homeassistant/components/upb/__init__.py
index f2765ff317d6de..ba9faeb1797c8c 100644
--- a/homeassistant/components/upb/__init__.py
+++ b/homeassistant/components/upb/__init__.py
@@ -3,26 +3,19 @@
import upb_lib
-from homeassistant.const import CONF_FILE_PATH, CONF_HOST
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.const import ATTR_COMMAND, CONF_FILE_PATH, CONF_HOST
+from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.typing import ConfigType
from .const import (
ATTR_ADDRESS,
ATTR_BRIGHTNESS_PCT,
- ATTR_COMMAND,
ATTR_RATE,
DOMAIN,
EVENT_UPB_SCENE_CHANGED,
)
-UPB_PLATFORMS = ["light", "scene"]
-
-
-async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
- """Set up the UPB platform."""
- return True
+PLATFORMS = ["light", "scene"]
async def async_setup_entry(hass, config_entry):
@@ -36,9 +29,9 @@ async def async_setup_entry(hass, config_entry):
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = {"upb": upb}
- for component in UPB_PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
def _element_changed(element, changeset):
@@ -71,8 +64,8 @@ async def async_unload_entry(hass, config_entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in UPB_PLATFORMS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -111,7 +104,7 @@ def should_poll(self) -> bool:
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the default attributes of the element."""
return self._element.as_dict()
diff --git a/homeassistant/components/upb/config_flow.py b/homeassistant/components/upb/config_flow.py
index 3af9999bd9fe36..c1fa31ea467c4d 100644
--- a/homeassistant/components/upb/config_flow.py
+++ b/homeassistant/components/upb/config_flow.py
@@ -1,5 +1,6 @@
"""Config flow for UPB PIM integration."""
import asyncio
+from contextlib import suppress
import logging
from urllib.parse import urlparse
@@ -10,7 +11,7 @@
from homeassistant import config_entries, exceptions
from homeassistant.const import CONF_ADDRESS, CONF_FILE_PATH, CONF_HOST, CONF_PROTOCOL
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
PROTOCOL_MAP = {"TCP": "tcp://", "Serial port": "serial://"}
@@ -43,11 +44,8 @@ def _connected_callback():
upb.connect(_connected_callback)
- try:
- with async_timeout.timeout(VALIDATE_TIMEOUT):
- await connected_event.wait()
- except asyncio.TimeoutError:
- pass
+ with suppress(asyncio.TimeoutError), async_timeout.timeout(VALIDATE_TIMEOUT):
+ await connected_event.wait()
upb.disconnect()
diff --git a/homeassistant/components/upb/const.py b/homeassistant/components/upb/const.py
index 75d754087e4397..8a2c435a70f3a4 100644
--- a/homeassistant/components/upb/const.py
+++ b/homeassistant/components/upb/const.py
@@ -10,7 +10,6 @@
ATTR_BLINK_RATE = "blink_rate"
ATTR_BRIGHTNESS = "brightness"
ATTR_BRIGHTNESS_PCT = "brightness_pct"
-ATTR_COMMAND = "command"
ATTR_RATE = "rate"
CONF_NETWORK = "network"
EVENT_UPB_SCENE_CHANGED = "upb.scene_changed"
diff --git a/homeassistant/components/upb/translations/de.json b/homeassistant/components/upb/translations/de.json
index ea6f1d3715049d..86e4d7409cfd4e 100644
--- a/homeassistant/components/upb/translations/de.json
+++ b/homeassistant/components/upb/translations/de.json
@@ -1,9 +1,12 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
"error": {
- "cannot_connect": "Fehler beim Herstellen einer Verbindung zu UPB PIM. Versuchen Sie es erneut.",
- "invalid_upb_file": "Fehlende oder ung\u00fcltige UPB UPStart-Exportdatei, \u00fcberpr\u00fcfen Sie den Namen und den Pfad der Datei.",
- "unknown": "Unerwarteter Fehler."
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_upb_file": "Fehlende oder ung\u00fcltige UPB UPStart-Exportdatei, \u00fcberpr\u00fcfe den Namen und den Pfad der Datei.",
+ "unknown": "Unerwarteter Fehler"
},
"step": {
"user": {
@@ -12,7 +15,8 @@
"file_path": "Pfad und Name der UPStart UPB-Exportdatei.",
"protocol": "Protokoll"
},
- "title": "Stellen Sie eine Verbindung zu UPB PIM her"
+ "description": "Schlie\u00dfe ein Universal Powerline Bus Powerline Interface Module (UPB PIM) an. Der Adress-String muss in der Form 'address[:port]' f\u00fcr 'TCP' vorliegen. Der Port ist optional und standardm\u00e4\u00dfig auf 2101 eingestellt. Beispiel: '192.168.1.42'. F\u00fcr das serielle Protokoll muss die Adresse die Form 'tty[:baud]' haben. Die Baudrate ist optional und standardm\u00e4\u00dfig auf 4800 eingestellt. Beispiel: '/dev/ttyS1'.",
+ "title": "Stelle eine Verbindung zu UPB PIM her"
}
}
}
diff --git a/homeassistant/components/upb/translations/hu.json b/homeassistant/components/upb/translations/hu.json
new file mode 100644
index 00000000000000..b09f497a0e4235
--- /dev/null
+++ b/homeassistant/components/upb/translations/hu.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "protocol": "Protokoll"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upb/translations/id.json b/homeassistant/components/upb/translations/id.json
new file mode 100644
index 00000000000000..3e875de7b19727
--- /dev/null
+++ b/homeassistant/components/upb/translations/id.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_upb_file": "File ekspor UPB UPStart tidak ada atau tidak valid, periksa nama dan jalur berkas.",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "address": "Alamat (lihat deskripsi di atas)",
+ "file_path": "Jalur dan nama file ekspor UPStart UPB.",
+ "protocol": "Protokol"
+ },
+ "description": "Hubungkan Universal Powerline Bus Powerline Interface Module (UPB PIM). String alamat harus dalam format 'address[:port]' untuk 'tcp'. Nilai port ini opsional dan nilai bakunya adalah 2101. Contoh: '192.168.1.42'. Untuk protokol serial, alamat harus dalam format 'tty[:baud]'. Nilai baud ini opsional dan nilai bakunya adalah 4800. Contoh: '/dev/ttyS1'.",
+ "title": "Hubungkan ke UPB PIM"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upb/translations/ko.json b/homeassistant/components/upb/translations/ko.json
index 48cc545d87b4ae..aedb6e06c41f1a 100644
--- a/homeassistant/components/upb/translations/ko.json
+++ b/homeassistant/components/upb/translations/ko.json
@@ -1,9 +1,12 @@
{
"config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
"error": {
- "cannot_connect": "UPB PIM \uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_upb_file": "UPB UPStart \ub0b4\ubcf4\ub0b4\uae30 \ud30c\uc77c\uc774 \uc5c6\uac70\ub098 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud30c\uc77c \uc774\ub984\uacfc \uacbd\ub85c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694.",
- "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4."
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
@@ -12,8 +15,8 @@
"file_path": "UPStart UPB \ub0b4\ubcf4\ub0b4\uae30 \ud30c\uc77c\uc758 \uacbd\ub85c \ubc0f \uc774\ub984",
"protocol": "\ud504\ub85c\ud1a0\ucf5c"
},
- "description": "\ubc94\uc6a9 \ud30c\uc6cc\ub77c\uc778 \ubc84\uc2a4 \ud30c\uc6cc\ub77c\uc778 \uc778\ud130\ud398\uc774\uc2a4 \ubaa8\ub4c8 (UPB PIM) \uc744 \uc5f0\uacb0\ud574\uc8fc\uc138\uc694. \uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 'tcp' \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 'address[:port]' \ud615\uc2dd\uc785\ub2c8\ub2e4. \ud3ec\ud2b8\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 2101 \uc785\ub2c8\ub2e4. \uc608: '192.168.1.42'. \uc2dc\ub9ac\uc5bc \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 \uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 'tty[:baud]' \ud615\uc2dd\uc785\ub2c8\ub2e4. \ubcf4(baud)\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 4800 \uc785\ub2c8\ub2e4. \uc608: '/dev/ttyS1'.",
- "title": "UPB PIM \uc5d0 \uc5f0\uacb0\ud558\uae30"
+ "description": "\ubc94\uc6a9 \ud30c\uc6cc\ub77c\uc778 \ubc84\uc2a4 \ud30c\uc6cc\ub77c\uc778 \uc778\ud130\ud398\uc774\uc2a4 \ubaa8\ub4c8 (UPB PIM) \uc744 \uc5f0\uacb0\ud574\uc8fc\uc138\uc694. \uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 'tcp' \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 'address[:port]' \ud615\uc2dd\uc785\ub2c8\ub2e4. \ud3ec\ud2b8\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 2101 \uc785\ub2c8\ub2e4. \uc608: '192.168.1.42'. \uc2dc\ub9ac\uc5bc \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 \uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 'tty[:baud]' \ud615\uc2dd\uc785\ub2c8\ub2e4. \uc804\uc1a1 \uc18d\ub3c4\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 4800 \uc785\ub2c8\ub2e4. \uc608: '/dev/ttyS1'.",
+ "title": "UPB PIM\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/upb/translations/pt.json b/homeassistant/components/upb/translations/pt.json
index ae100e458450fc..657ce03e544fea 100644
--- a/homeassistant/components/upb/translations/pt.json
+++ b/homeassistant/components/upb/translations/pt.json
@@ -4,6 +4,7 @@
"already_configured": "O dispositivo j\u00e1 est\u00e1 configurado"
},
"error": {
+ "cannot_connect": "Falha na liga\u00e7\u00e3o",
"unknown": "Erro inesperado"
}
}
diff --git a/homeassistant/components/upb/translations/tr.json b/homeassistant/components/upb/translations/tr.json
new file mode 100644
index 00000000000000..818531fcaa059f
--- /dev/null
+++ b/homeassistant/components/upb/translations/tr.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upb/translations/uk.json b/homeassistant/components/upb/translations/uk.json
new file mode 100644
index 00000000000000..062503848a8d0c
--- /dev/null
+++ b/homeassistant/components/upb/translations/uk.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_upb_file": "\u0412\u0456\u0434\u0441\u0443\u0442\u043d\u0456\u0439 \u0430\u0431\u043e \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439 \u0444\u0430\u0439\u043b \u0435\u043a\u0441\u043f\u043e\u0440\u0442\u0443 UPB UPStart, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0456\u043c'\u044f \u0456 \u0448\u043b\u044f\u0445 \u0434\u043e \u0444\u0430\u0439\u043b\u0443.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "address": "\u0410\u0434\u0440\u0435\u0441\u0430 (\u0434\u0438\u0432. \u043e\u043f\u0438\u0441 \u0432\u0438\u0449\u0435)",
+ "file_path": "\u0428\u043b\u044f\u0445 \u0456 \u0456\u043c'\u044f \u0444\u0430\u0439\u043b\u0443 \u0435\u043a\u0441\u043f\u043e\u0440\u0442\u0443 UPStart UPB.",
+ "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b"
+ },
+ "description": "\u0420\u044f\u0434\u043e\u043a \u0430\u0434\u0440\u0435\u0441\u0438 \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'address[:port]' \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'tcp' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: '192.168.1.42'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'port' \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u043d \u0434\u043e\u0440\u0456\u0432\u043d\u044e\u0454 2101. \u0414\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'serial' \u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u043e\u0432\u0438\u043d\u043d\u0430 \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'tty[:baud]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: '/dev/ttyS1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'baud' \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u043d \u0434\u043e\u0440\u0456\u0432\u043d\u044e\u0454 4800.",
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e UPB PIM"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upc_connect/device_tracker.py b/homeassistant/components/upc_connect/device_tracker.py
index d68311a879341a..2a2a1b3798bb86 100644
--- a/homeassistant/components/upc_connect/device_tracker.py
+++ b/homeassistant/components/upc_connect/device_tracker.py
@@ -1,6 +1,7 @@
"""Support for UPC ConnectBox router."""
+from __future__ import annotations
+
import logging
-from typing import List, Optional
from connect_box import ConnectBox
from connect_box.exceptions import ConnectBoxError, ConnectBoxLoginError
@@ -57,7 +58,7 @@ def __init__(self, connect_box: ConnectBox):
"""Initialize the scanner."""
self.connect_box: ConnectBox = connect_box
- async def async_scan_devices(self) -> List[str]:
+ async def async_scan_devices(self) -> list[str]:
"""Scan for new devices and return a list with found device IDs."""
try:
await self.connect_box.async_get_devices()
@@ -66,7 +67,7 @@ async def async_scan_devices(self) -> List[str]:
return [device.mac for device in self.connect_box.devices]
- async def async_get_device_name(self, device: str) -> Optional[str]:
+ async def async_get_device_name(self, device: str) -> str | None:
"""Get the device name (the name of the wireless device not used)."""
for connected_device in self.connect_box.devices:
if (
diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py
index 5acf9e364bc5cf..c118f12954d29f 100644
--- a/homeassistant/components/upcloud/__init__.py
+++ b/homeassistant/components/upcloud/__init__.py
@@ -1,9 +1,10 @@
"""Support for UpCloud."""
+from __future__ import annotations
import dataclasses
from datetime import timedelta
import logging
-from typing import Dict, List
+from typing import Dict
import requests.exceptions
import upcloud_api
@@ -40,7 +41,6 @@
ATTR_CORE_NUMBER = "core_number"
ATTR_HOSTNAME = "hostname"
ATTR_MEMORY_AMOUNT = "memory_amount"
-ATTR_STATE = "state"
ATTR_TITLE = "title"
ATTR_UUID = "uuid"
ATTR_ZONE = "zone"
@@ -92,7 +92,7 @@ def __init__(
hass, _LOGGER, name=f"{username}@UpCloud", update_interval=update_interval
)
self.cloud_manager = cloud_manager
- self.unsub_handlers: List[CALLBACK_TYPE] = []
+ self.unsub_handlers: list[CALLBACK_TYPE] = []
async def async_update_config(self, config_entry: ConfigEntry) -> None:
"""Handle config update."""
@@ -100,7 +100,7 @@ async def async_update_config(self, config_entry: ConfigEntry) -> None:
seconds=config_entry.options[CONF_SCAN_INTERVAL]
)
- async def _async_update_data(self) -> Dict[str, upcloud_api.Server]:
+ async def _async_update_data(self) -> dict[str, upcloud_api.Server]:
return {
x.uuid: x
for x in await self.hass.async_add_executor_job(
@@ -113,10 +113,10 @@ async def _async_update_data(self) -> Dict[str, upcloud_api.Server]:
class UpCloudHassData:
"""Home Assistant UpCloud runtime data."""
- coordinators: Dict[str, UpCloudDataUpdateCoordinator] = dataclasses.field(
+ coordinators: dict[str, UpCloudDataUpdateCoordinator] = dataclasses.field(
default_factory=dict
)
- scan_interval_migrations: Dict[str, int] = dataclasses.field(default_factory=dict)
+ scan_interval_migrations: dict[str, int] = dataclasses.field(default_factory=dict)
async def async_setup(hass: HomeAssistantType, config) -> bool:
@@ -127,7 +127,7 @@ async def async_setup(hass: HomeAssistantType, config) -> bool:
_LOGGER.warning(
"Loading upcloud via top level config is deprecated and no longer "
- "necessary as of 0.117. Please remove it from your YAML configuration."
+ "necessary as of 0.117; Please remove it from your YAML configuration"
)
hass.async_create_task(
hass.config_entries.flow.async_init(
@@ -207,9 +207,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry)
)
# Call the UpCloud API to refresh data
- await coordinator.async_request_refresh()
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
# Listen to config entry updates
coordinator.unsub_handlers.append(
@@ -297,7 +295,7 @@ def device_class(self):
return DEFAULT_COMPONENT_DEVICE_CLASS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the UpCloud server."""
return {
x: getattr(self._server, x, None)
diff --git a/homeassistant/components/upcloud/config_flow.py b/homeassistant/components/upcloud/config_flow.py
index d7f1680c80f986..1a39b1898970bd 100644
--- a/homeassistant/components/upcloud/config_flow.py
+++ b/homeassistant/components/upcloud/config_flow.py
@@ -10,7 +10,6 @@
from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
from homeassistant.core import callback
-# pylint: disable=unused-import # for DOMAIN https://github.com/PyCQA/pylint/issues/3202
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/upcloud/manifest.json b/homeassistant/components/upcloud/manifest.json
index 3a85f847c9d92e..f161e273bc3614 100644
--- a/homeassistant/components/upcloud/manifest.json
+++ b/homeassistant/components/upcloud/manifest.json
@@ -3,6 +3,6 @@
"name": "UpCloud",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/upcloud",
- "requirements": ["upcloud-api==0.4.5"],
+ "requirements": ["upcloud-api==1.0.1"],
"codeowners": ["@scop"]
}
diff --git a/homeassistant/components/upcloud/translations/de.json b/homeassistant/components/upcloud/translations/de.json
index 76bbc70569017a..ee1802f1d38132 100644
--- a/homeassistant/components/upcloud/translations/de.json
+++ b/homeassistant/components/upcloud/translations/de.json
@@ -1,7 +1,8 @@
{
"config": {
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
},
"step": {
"user": {
diff --git a/homeassistant/components/upcloud/translations/hu.json b/homeassistant/components/upcloud/translations/hu.json
index 7a7de0633a71d5..2bb28c6c3bcbd5 100644
--- a/homeassistant/components/upcloud/translations/hu.json
+++ b/homeassistant/components/upcloud/translations/hu.json
@@ -2,7 +2,7 @@
"config": {
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
- "invalid_auth": "\u00c9rv\u00e9nytelen autentik\u00e1ci\u00f3"
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
},
"step": {
"user": {
diff --git a/homeassistant/components/upcloud/translations/id.json b/homeassistant/components/upcloud/translations/id.json
new file mode 100644
index 00000000000000..4ff6a8c7d92c23
--- /dev/null
+++ b/homeassistant/components/upcloud/translations/id.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Interval pembaruan (dalam detik, minimal 30)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upcloud/translations/ko.json b/homeassistant/components/upcloud/translations/ko.json
new file mode 100644
index 00000000000000..24b4ab0a446a73
--- /dev/null
+++ b/homeassistant/components/upcloud/translations/ko.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 (\ucd08, \ucd5c\uc19f\uac12 30)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upcloud/translations/nl.json b/homeassistant/components/upcloud/translations/nl.json
index 783032a1da0be5..f9ba7d2f696a3b 100644
--- a/homeassistant/components/upcloud/translations/nl.json
+++ b/homeassistant/components/upcloud/translations/nl.json
@@ -1,12 +1,23 @@
{
"config": {
"error": {
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie"
},
"step": {
"user": {
"data": {
- "password": "Wachtwoord"
+ "password": "Wachtwoord",
+ "username": "Gebruikersnaam"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Update-interval in seconden, minimaal 30"
}
}
}
diff --git a/homeassistant/components/upcloud/translations/ru.json b/homeassistant/components/upcloud/translations/ru.json
index ced4097a7e2719..8b0377e9c34c72 100644
--- a/homeassistant/components/upcloud/translations/ru.json
+++ b/homeassistant/components/upcloud/translations/ru.json
@@ -2,13 +2,13 @@
"config": {
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"step": {
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
}
}
}
diff --git a/homeassistant/components/upcloud/translations/tr.json b/homeassistant/components/upcloud/translations/tr.json
new file mode 100644
index 00000000000000..f1840698493b58
--- /dev/null
+++ b/homeassistant/components/upcloud/translations/tr.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upcloud/translations/uk.json b/homeassistant/components/upcloud/translations/uk.json
new file mode 100644
index 00000000000000..bf8781c1eb2a4a
--- /dev/null
+++ b/homeassistant/components/upcloud/translations/uk.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445, \u043c\u0456\u043d\u0456\u043c\u0443\u043c 30)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py
index 13497da82902db..9e3c504e4be567 100644
--- a/homeassistant/components/updater/__init__.py
+++ b/homeassistant/components/updater/__init__.py
@@ -1,11 +1,10 @@
"""Support to check for available updates."""
import asyncio
from datetime import timedelta
-from distutils.version import StrictVersion
import logging
import async_timeout
-from distro import linux_distribution # pylint: disable=import-error
+from awesomeversion import AwesomeVersion
import voluptuous as vol
from homeassistant.const import __version__ as current_version
@@ -23,20 +22,22 @@
DOMAIN = "updater"
-UPDATER_URL = "https://updater.home-assistant.io/"
+UPDATER_URL = "https://www.home-assistant.io/version.json"
+
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: {
- vol.Optional(CONF_REPORTING, default=True): cv.boolean,
- vol.Optional(CONF_COMPONENT_REPORTING, default=False): cv.boolean,
+ vol.Optional(CONF_REPORTING): cv.boolean,
+ vol.Optional(CONF_COMPONENT_REPORTING): cv.boolean,
}
},
extra=vol.ALLOW_EXTRA,
)
RESPONSE_SCHEMA = vol.Schema(
- {vol.Required("version"): cv.string, vol.Required("release-notes"): cv.url}
+ {vol.Required("current_version"): cv.string, vol.Required("release_notes"): cv.url},
+ extra=vol.REMOVE_EXTRA,
)
@@ -52,30 +53,27 @@ def __init__(self, update_available: bool, newest_version: str, release_notes: s
async def async_setup(hass, config):
"""Set up the updater component."""
- if "dev" in current_version:
- # This component only makes sense in release versions
- _LOGGER.info("Running on 'dev', only analytics will be submitted")
-
conf = config.get(DOMAIN, {})
- if conf.get(CONF_REPORTING):
- huuid = await hass.helpers.instance_id.async_get()
- else:
- huuid = None
- include_components = conf.get(CONF_COMPONENT_REPORTING)
+ for option in (CONF_COMPONENT_REPORTING, CONF_REPORTING):
+ if option in conf:
+ _LOGGER.warning(
+ "Analytics reporting with the option '%s' "
+ "is deprecated and you should remove that from your configuration. "
+ "The analytics part of this integration has moved to the new 'analytics' integration",
+ option,
+ )
async def check_new_version() -> Updater:
"""Check if a new version is available and report if one is."""
- newest, release_notes = await get_newest_version(
- hass, huuid, include_components
- )
-
- _LOGGER.debug("Fetched version %s: %s", newest, release_notes)
-
# Skip on dev
if "dev" in current_version:
return Updater(False, "", "")
+ newest, release_notes = await get_newest_version(hass)
+
+ _LOGGER.debug("Fetched version %s: %s", newest, release_notes)
+
# Load data from Supervisor
if hass.components.hassio.is_hassio():
core_info = hass.components.hassio.get_core_info()
@@ -83,16 +81,16 @@ async def check_new_version() -> Updater:
# Validate version
update_available = False
- if StrictVersion(newest) > StrictVersion(current_version):
+ if AwesomeVersion(newest) > AwesomeVersion(current_version):
_LOGGER.debug(
"The latest available version of Home Assistant is %s", newest
)
update_available = True
- elif StrictVersion(newest) == StrictVersion(current_version):
+ elif AwesomeVersion(newest) == AwesomeVersion(current_version):
_LOGGER.debug(
"You are on the latest version (%s) of Home Assistant", newest
)
- elif StrictVersion(newest) < StrictVersion(current_version):
+ elif AwesomeVersion(newest) < AwesomeVersion(current_version):
_LOGGER.debug(
"Local version (%s) is newer than the latest available version (%s)",
current_version,
@@ -121,34 +119,12 @@ async def check_new_version() -> Updater:
return True
-async def get_newest_version(hass, huuid, include_components):
+async def get_newest_version(hass):
"""Get the newest Home Assistant version."""
- if huuid:
- info_object = await hass.helpers.system_info.async_get_system_info()
-
- if include_components:
- info_object["components"] = list(hass.config.components)
-
- linux_dist = await hass.async_add_executor_job(linux_distribution, False)
- info_object["distribution"] = linux_dist[0]
- info_object["os_version"] = linux_dist[1]
-
- info_object["huuid"] = huuid
- else:
- info_object = {}
-
session = async_get_clientsession(hass)
with async_timeout.timeout(30):
- req = await session.post(UPDATER_URL, json=info_object)
-
- _LOGGER.info(
- (
- "Submitted analytics to Home Assistant servers. "
- "Information submitted includes %s"
- ),
- info_object,
- )
+ req = await session.get(UPDATER_URL)
try:
res = await req.json()
@@ -159,7 +135,7 @@ async def get_newest_version(hass, huuid, include_components):
try:
res = RESPONSE_SCHEMA(res)
- return res["version"], res["release-notes"]
+ return res["current_version"], res["release_notes"]
except vol.Invalid as err:
raise update_coordinator.UpdateFailed(
f"Got unexpected response: {err}"
diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py
index 36e05513d43788..93d1902999239b 100644
--- a/homeassistant/components/updater/binary_sensor.py
+++ b/homeassistant/components/updater/binary_sensor.py
@@ -35,7 +35,7 @@ def is_on(self) -> bool:
return self.coordinator.data.update_available
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return the optional state attributes."""
if not self.coordinator.data:
return None
diff --git a/homeassistant/components/updater/translations/zh-Hant.json b/homeassistant/components/updater/translations/zh-Hant.json
index 31188faa135654..23c1b069fc186c 100644
--- a/homeassistant/components/updater/translations/zh-Hant.json
+++ b/homeassistant/components/updater/translations/zh-Hant.json
@@ -1,3 +1,3 @@
{
- "title": "\u66f4\u65b0\u7248\u672c"
+ "title": "\u66f4\u65b0\u5668"
}
\ No newline at end of file
diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py
index 7b46037d99df16..d5be0757cf3c0b 100644
--- a/homeassistant/components/upnp/__init__.py
+++ b/homeassistant/components/upnp/__init__.py
@@ -13,6 +13,7 @@
from .const import (
CONF_LOCAL_IP,
+ CONFIG_ENTRY_HOSTNAME,
CONFIG_ENTRY_ST,
CONFIG_ENTRY_UDN,
DISCOVERY_LOCATION,
@@ -31,7 +32,13 @@
NOTIFICATION_TITLE = "UPnP/IGD Setup"
CONFIG_SCHEMA = vol.Schema(
- {DOMAIN: vol.Schema({vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string)})},
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string),
+ },
+ )
+ },
extra=vol.ALLOW_EXTRA,
)
@@ -115,6 +122,16 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry)
unique_id=device.unique_id,
)
+ # Ensure entry has a hostname, for older entries.
+ if (
+ CONFIG_ENTRY_HOSTNAME not in config_entry.data
+ or config_entry.data[CONFIG_ENTRY_HOSTNAME] != device.hostname
+ ):
+ hass.config_entries.async_update_entry(
+ entry=config_entry,
+ data={CONFIG_ENTRY_HOSTNAME: device.hostname, **config_entry.data},
+ )
+
# Create device registry entry.
device_registry = await dr.async_get_registry(hass)
device_registry.async_get_or_create(
diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py
index 41c56dddb2921f..1cbaf9318572bb 100644
--- a/homeassistant/components/upnp/config_flow.py
+++ b/homeassistant/components/upnp/config_flow.py
@@ -1,6 +1,8 @@
"""Config flow for UPNP."""
+from __future__ import annotations
+
from datetime import timedelta
-from typing import Any, Mapping, Optional
+from typing import Any, Mapping
import voluptuous as vol
@@ -10,10 +12,12 @@
from homeassistant.core import callback
from .const import (
+ CONFIG_ENTRY_HOSTNAME,
CONFIG_ENTRY_SCAN_INTERVAL,
CONFIG_ENTRY_ST,
CONFIG_ENTRY_UDN,
DEFAULT_SCAN_INTERVAL,
+ DISCOVERY_HOSTNAME,
DISCOVERY_LOCATION,
DISCOVERY_NAME,
DISCOVERY_ST,
@@ -53,7 +57,7 @@ def __init__(self) -> None:
self._discoveries: Mapping = None
async def async_step_user(
- self, user_input: Optional[Mapping] = None
+ self, user_input: Mapping | None = None
) -> Mapping[str, Any]:
"""Handle a flow start."""
_LOGGER.debug("async_step_user: user_input: %s", user_input)
@@ -109,9 +113,7 @@ async def async_step_user(
data_schema=data_schema,
)
- async def async_step_import(
- self, import_info: Optional[Mapping]
- ) -> Mapping[str, Any]:
+ async def async_step_import(self, import_info: Mapping | None) -> Mapping[str, Any]:
"""Import a new UPnP/IGD device as a config entry.
This flow is triggered by `async_setup`. If no device has been
@@ -177,13 +179,24 @@ async def async_step_ssdp(self, discovery_info: Mapping) -> Mapping[str, Any]:
discovery = await Device.async_supplement_discovery(self.hass, discovery)
unique_id = discovery[DISCOVERY_UNIQUE_ID]
await self.async_set_unique_id(unique_id)
- self._abort_if_unique_id_configured()
+ self._abort_if_unique_id_configured(
+ updates={CONFIG_ENTRY_HOSTNAME: discovery[DISCOVERY_HOSTNAME]}
+ )
+
+ # Handle devices changing their UDN, only allow a single
+ existing_entries = self.hass.config_entries.async_entries(DOMAIN)
+ for config_entry in existing_entries:
+ entry_hostname = config_entry.data.get(CONFIG_ENTRY_HOSTNAME)
+ if entry_hostname == discovery[DISCOVERY_HOSTNAME]:
+ _LOGGER.debug(
+ "Found existing config_entry with same hostname, discovery ignored"
+ )
+ return self.async_abort(reason="discovery_ignored")
# Store discovery.
self._discoveries = [discovery]
# Ensure user recognizable.
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {
"name": discovery[DISCOVERY_NAME],
}
@@ -191,7 +204,7 @@ async def async_step_ssdp(self, discovery_info: Mapping) -> Mapping[str, Any]:
return await self.async_step_ssdp_confirm()
async def async_step_ssdp_confirm(
- self, user_input: Optional[Mapping] = None
+ self, user_input: Mapping | None = None
) -> Mapping[str, Any]:
"""Confirm integration via SSDP."""
_LOGGER.debug("async_step_ssdp_confirm: user_input: %s", user_input)
@@ -223,6 +236,7 @@ async def _async_create_entry_from_discovery(
data = {
CONFIG_ENTRY_UDN: discovery[DISCOVERY_UDN],
CONFIG_ENTRY_ST: discovery[DISCOVERY_ST],
+ CONFIG_ENTRY_HOSTNAME: discovery[DISCOVERY_HOSTNAME],
}
return self.async_create_entry(title=title, data=data)
diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py
index 4ccf6d3d7ea4bc..6575139c4a4e0c 100644
--- a/homeassistant/components/upnp/const.py
+++ b/homeassistant/components/upnp/const.py
@@ -21,6 +21,7 @@
DATA_RATE_PACKETS_PER_SECOND = f"{DATA_PACKETS}/{TIME_SECONDS}"
KIBIBYTE = 1024
UPDATE_INTERVAL = timedelta(seconds=30)
+DISCOVERY_HOSTNAME = "hostname"
DISCOVERY_LOCATION = "location"
DISCOVERY_NAME = "name"
DISCOVERY_ST = "st"
@@ -30,4 +31,5 @@
CONFIG_ENTRY_SCAN_INTERVAL = "scan_interval"
CONFIG_ENTRY_ST = "st"
CONFIG_ENTRY_UDN = "udn"
+CONFIG_ENTRY_HOSTNAME = "hostname"
DEFAULT_SCAN_INTERVAL = timedelta(seconds=30).seconds
diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py
index 39fd09089b414b..034496ec02817d 100644
--- a/homeassistant/components/upnp/device.py
+++ b/homeassistant/components/upnp/device.py
@@ -3,7 +3,8 @@
import asyncio
from ipaddress import IPv4Address
-from typing import List, Mapping
+from typing import Mapping
+from urllib.parse import urlparse
from async_upnp_client import UpnpFactory
from async_upnp_client.aiohttp import AiohttpSessionRequester
@@ -17,6 +18,7 @@
BYTES_RECEIVED,
BYTES_SENT,
CONF_LOCAL_IP,
+ DISCOVERY_HOSTNAME,
DISCOVERY_LOCATION,
DISCOVERY_NAME,
DISCOVERY_ST,
@@ -40,7 +42,7 @@ def __init__(self, igd_device):
self._igd_device: IgdDevice = igd_device
@classmethod
- async def async_discover(cls, hass: HomeAssistantType) -> List[Mapping]:
+ async def async_discover(cls, hass: HomeAssistantType) -> list[Mapping]:
"""Discover UPnP/IGD devices."""
_LOGGER.debug("Discovering UPnP/IGD devices")
local_ip = None
@@ -66,10 +68,10 @@ async def async_supplement_discovery(
cls, hass: HomeAssistantType, discovery: Mapping
) -> Mapping:
"""Get additional data from device and supplement discovery."""
- device = await Device.async_create_device(hass, discovery[DISCOVERY_LOCATION])
+ location = discovery[DISCOVERY_LOCATION]
+ device = await Device.async_create_device(hass, location)
discovery[DISCOVERY_NAME] = device.name
-
- # Set unique_id.
+ discovery[DISCOVERY_HOSTNAME] = device.hostname
discovery[DISCOVERY_UNIQUE_ID] = discovery[DISCOVERY_USN]
return discovery
@@ -126,6 +128,13 @@ def unique_id(self) -> str:
"""Get the unique id."""
return self.usn
+ @property
+ def hostname(self) -> str:
+ """Get the hostname."""
+ url = self._igd_device.device.device_url
+ parsed = urlparse(url)
+ return parsed.hostname
+
def __str__(self) -> str:
"""Get string representation."""
return f"IGD Device: {self.name}/{self.udn}::{self.device_type}"
diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json
index fe3a7b169dc09d..feecdb00b18709 100644
--- a/homeassistant/components/upnp/manifest.json
+++ b/homeassistant/components/upnp/manifest.json
@@ -3,7 +3,7 @@
"name": "UPnP",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/upnp",
- "requirements": ["async-upnp-client==0.14.13"],
+ "requirements": ["async-upnp-client==0.16.0"],
"codeowners": ["@StevenLooman"],
"ssdp": [
{
diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py
index 59205f49667ca8..0e95b6106a399d 100644
--- a/homeassistant/components/upnp/sensor.py
+++ b/homeassistant/components/upnp/sensor.py
@@ -1,7 +1,10 @@
"""Support for UPnP/IGD Sensors."""
+from __future__ import annotations
+
from datetime import timedelta
from typing import Any, Mapping
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import DATA_BYTES, DATA_RATE_KIBIBYTES_PER_SECOND
from homeassistant.helpers import device_registry as dr
@@ -115,7 +118,7 @@ async def async_setup_entry(
async_add_entities(sensors, True)
-class UpnpSensor(CoordinatorEntity):
+class UpnpSensor(CoordinatorEntity, SensorEntity):
"""Base class for UPnP/IGD sensors."""
def __init__(
@@ -176,7 +179,7 @@ class RawUpnpSensor(UpnpSensor):
"""Representation of a UPnP/IGD sensor."""
@property
- def state(self) -> str:
+ def state(self) -> str | None:
"""Return the state of the device."""
device_value_key = self._sensor_type["device_value_key"]
value = self.coordinator.data[device_value_key]
@@ -214,7 +217,7 @@ def _has_overflowed(self, current_value) -> bool:
return current_value < self._last_value
@property
- def state(self) -> str:
+ def state(self) -> str | None:
"""Return the state of the device."""
# Can't calculate any derivative if we have only one value.
device_value_key = self._sensor_type["device_value_key"]
diff --git a/homeassistant/components/upnp/translations/de.json b/homeassistant/components/upnp/translations/de.json
index 972f8da40751dd..8a5f4fefadffcc 100644
--- a/homeassistant/components/upnp/translations/de.json
+++ b/homeassistant/components/upnp/translations/de.json
@@ -16,6 +16,7 @@
},
"user": {
"data": {
+ "scan_interval": "Aktualisierungsintervall (Sekunden, mindestens 30)",
"usn": "Ger\u00e4t"
}
}
diff --git a/homeassistant/components/upnp/translations/hu.json b/homeassistant/components/upnp/translations/hu.json
index 66320386a89dbf..8b50de71f746a3 100644
--- a/homeassistant/components/upnp/translations/hu.json
+++ b/homeassistant/components/upnp/translations/hu.json
@@ -1,12 +1,20 @@
{
"config": {
"abort": {
- "already_configured": "Az UPnP / IGD m\u00e1r konfigur\u00e1l\u00e1sra ker\u00fclt",
- "no_devices_found": "Nincsenek UPnPIGD eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton."
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton"
},
"error": {
"one": "hiba",
"other": ""
+ },
+ "flow_title": "UPnP/IGD: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "usn": "Eszk\u00f6z"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/upnp/translations/id.json b/homeassistant/components/upnp/translations/id.json
new file mode 100644
index 00000000000000..463e61f271c6e4
--- /dev/null
+++ b/homeassistant/components/upnp/translations/id.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "incomplete_discovery": "Proses penemuan tidak selesai",
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan"
+ },
+ "flow_title": "UPnP/IGD: {name}",
+ "step": {
+ "ssdp_confirm": {
+ "description": "Ingin menyiapkan perangkat UPnP/IGD ini?"
+ },
+ "user": {
+ "data": {
+ "scan_interval": "Interval pembaruan (dalam detik, minimal 30)",
+ "usn": "Perangkat"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upnp/translations/ko.json b/homeassistant/components/upnp/translations/ko.json
index 1a7b52049309a2..ab80ceb9caa82e 100644
--- a/homeassistant/components/upnp/translations/ko.json
+++ b/homeassistant/components/upnp/translations/ko.json
@@ -1,9 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "UPnP/IGD \uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4",
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"incomplete_discovery": "\uae30\uae30 \uac80\uc0c9\uc774 \uc644\uc804\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4",
- "no_devices_found": "UPnP/IGD \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4."
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
},
"flow_title": "UPnP/IGD: {name}",
"step": {
@@ -12,7 +12,7 @@
},
"user": {
"data": {
- "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 (\ucd08, \ucd5c\uc18c\uac12 30)",
+ "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 (\ucd08, \ucd5c\uc19f\uac12 30)",
"usn": "\uae30\uae30"
}
}
diff --git a/homeassistant/components/upnp/translations/nl.json b/homeassistant/components/upnp/translations/nl.json
index 3d2c628fcbb55f..331d5850fc4d79 100644
--- a/homeassistant/components/upnp/translations/nl.json
+++ b/homeassistant/components/upnp/translations/nl.json
@@ -1,9 +1,9 @@
{
"config": {
"abort": {
- "already_configured": "UPnP/IGD is al geconfigureerd",
+ "already_configured": "Apparaat is al geconfigureerd",
"incomplete_discovery": "Onvolledige ontdekking",
- "no_devices_found": "Geen UPnP/IGD apparaten gevonden op het netwerk."
+ "no_devices_found": "Geen apparaten gevonden op het netwerk"
},
"error": {
"one": "Een",
@@ -11,8 +11,12 @@
},
"flow_title": "UPnP/IGD: {name}",
"step": {
+ "init": {
+ "one": "Leeg",
+ "other": "Leeg"
+ },
"ssdp_confirm": {
- "description": "Wilt u [%%] instellen?"
+ "description": "Wilt u dit UPnP/IGD-apparaat instellen?"
},
"user": {
"data": {
diff --git a/homeassistant/components/upnp/translations/ro.json b/homeassistant/components/upnp/translations/ro.json
index ceb1c19131abe1..2fd83a0b371bf4 100644
--- a/homeassistant/components/upnp/translations/ro.json
+++ b/homeassistant/components/upnp/translations/ro.json
@@ -7,6 +7,13 @@
"few": "",
"one": "Unul",
"other": ""
+ },
+ "step": {
+ "init": {
+ "few": "Pu\u021bine",
+ "one": "Unul",
+ "other": "Altele"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/upnp/translations/tr.json b/homeassistant/components/upnp/translations/tr.json
new file mode 100644
index 00000000000000..2715f66e090919
--- /dev/null
+++ b/homeassistant/components/upnp/translations/tr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "flow_title": "UPnP / IGD: {name}",
+ "step": {
+ "ssdp_confirm": {
+ "description": "Bu UPnP / IGD cihaz\u0131n\u0131 kurmak istiyor musunuz?"
+ },
+ "user": {
+ "data": {
+ "scan_interval": "G\u00fcncelleme aral\u0131\u011f\u0131 (saniye, minimum 30)",
+ "usn": "Cihaz"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upnp/translations/uk.json b/homeassistant/components/upnp/translations/uk.json
index 0b8747f902ec7f..905958eeca9706 100644
--- a/homeassistant/components/upnp/translations/uk.json
+++ b/homeassistant/components/upnp/translations/uk.json
@@ -1,7 +1,21 @@
{
"config": {
"abort": {
- "already_configured": "UPnP/IGD \u0432\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0454\u043d\u043e"
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "incomplete_discovery": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043f\u0440\u043e\u0446\u0435\u0441.",
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456."
+ },
+ "flow_title": "UPnP/IGD: {name}",
+ "step": {
+ "ssdp_confirm": {
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0446\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 UPnP / IGD?"
+ },
+ "user": {
+ "data": {
+ "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445, \u043c\u0456\u043d\u0456\u043c\u0443\u043c 30)",
+ "usn": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py
index 8363d2da2cbef7..7e79c2fbb5ea0d 100644
--- a/homeassistant/components/uptime/sensor.py
+++ b/homeassistant/components/uptime/sensor.py
@@ -2,10 +2,13 @@
import voluptuous as vol
-from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP, PLATFORM_SCHEMA
+from homeassistant.components.sensor import (
+ DEVICE_CLASS_TIMESTAMP,
+ PLATFORM_SCHEMA,
+ SensorEntity,
+)
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
DEFAULT_NAME = "Uptime"
@@ -30,7 +33,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([UptimeSensor(name)], True)
-class UptimeSensor(Entity):
+class UptimeSensor(SensorEntity):
"""Representation of an uptime sensor."""
def __init__(self, name):
diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py
index e31d8b44b100fa..6c0bb63c70fac4 100644
--- a/homeassistant/components/uptimerobot/binary_sensor.py
+++ b/homeassistant/components/uptimerobot/binary_sensor.py
@@ -75,7 +75,7 @@ def device_class(self):
return DEVICE_CLASS_CONNECTIVITY
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the binary sensor."""
return {ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_TARGET: self._target}
diff --git a/homeassistant/components/uscis/sensor.py b/homeassistant/components/uscis/sensor.py
index 2e15a4b14226a0..bd261aba4fbad9 100644
--- a/homeassistant/components/uscis/sensor.py
+++ b/homeassistant/components/uscis/sensor.py
@@ -5,10 +5,9 @@
import uscisstatus
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -33,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_LOGGER.error("Setup USCIS Sensor Fail check if your Case ID is Valid")
-class UscisSensor(Entity):
+class UscisSensor(SensorEntity):
"""USCIS Sensor will check case status on daily basis."""
MIN_TIME_BETWEEN_UPDATES = timedelta(hours=24)
@@ -60,7 +59,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attributes
diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py
index 40a544a2e21a79..eefa2ed1d0d627 100644
--- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py
+++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py
@@ -1,7 +1,8 @@
"""Support for U.S. Geological Survey Earthquake Hazards Program Feeds."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Optional
from geojson_client.usgs_earthquake_hazards_program_feed import (
UsgsEarthquakeHazardsProgramFeedManager,
@@ -11,6 +12,7 @@
from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent
from homeassistant.const import (
ATTR_ATTRIBUTION,
+ ATTR_TIME,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_RADIUS,
@@ -30,7 +32,6 @@
ATTR_MAGNITUDE = "magnitude"
ATTR_PLACE = "place"
ATTR_STATUS = "status"
-ATTR_TIME = "time"
ATTR_TYPE = "type"
ATTR_UPDATED = "updated"
@@ -210,7 +211,7 @@ def _delete_callback(self):
"""Remove this entity."""
self._remove_signal_delete()
self._remove_signal_update()
- self.hass.async_create_task(self.async_remove())
+ self.hass.async_create_task(self.async_remove(force_remove=True))
@callback
def _update_callback(self):
@@ -255,22 +256,22 @@ def source(self) -> str:
return SOURCE
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the entity."""
return self._name
@property
- def distance(self) -> Optional[float]:
+ def distance(self) -> float | None:
"""Return distance value of this external event."""
return self._distance
@property
- def latitude(self) -> Optional[float]:
+ def latitude(self) -> float | None:
"""Return latitude value of this external event."""
return self._latitude
@property
- def longitude(self) -> Optional[float]:
+ def longitude(self) -> float | None:
"""Return longitude value of this external event."""
return self._longitude
@@ -280,7 +281,7 @@ def unit_of_measurement(self):
return DEFAULT_UNIT_OF_MEASUREMENT
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
for key, value in (
diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py
index 24bfd77f762fa4..5442cd583e2fc2 100644
--- a/homeassistant/components/utility_meter/__init__.py
+++ b/homeassistant/components/utility_meter/__init__.py
@@ -165,7 +165,7 @@ def state(self):
return self._current_tariff
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_TARIFFS: self._tariffs}
diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py
index e8d551ba280fa0..d28819a38cb225 100644
--- a/homeassistant/components/utility_meter/sensor.py
+++ b/homeassistant/components/utility_meter/sensor.py
@@ -5,6 +5,7 @@
import voluptuous as vol
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
CONF_NAME,
@@ -102,7 +103,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
-class UtilityMeterSensor(RestoreEntity):
+class UtilityMeterSensor(RestoreEntity, SensorEntity):
"""Representation of an utility meter sensor."""
def __init__(
@@ -324,7 +325,7 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
state_attr = {
ATTR_SOURCE_ID: self._sensor_source_id,
diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py
index 4f5cfa3907ed1a..6bbd868a8bdeaa 100644
--- a/homeassistant/components/uvc/camera.py
+++ b/homeassistant/components/uvc/camera.py
@@ -1,4 +1,6 @@
"""Support for Ubiquiti's UVC cameras."""
+from __future__ import annotations
+
from datetime import datetime
import logging
import re
@@ -8,15 +10,15 @@
import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
-from homeassistant.const import CONF_PORT, CONF_SSL
+from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_SSL
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
+from homeassistant.util.dt import utc_from_timestamp
_LOGGER = logging.getLogger(__name__)
CONF_NVR = "nvr"
CONF_KEY = "key"
-CONF_PASSWORD = "password"
DEFAULT_PASSWORD = "ubnt"
DEFAULT_PORT = 7080
@@ -111,9 +113,9 @@ def supported_features(self):
return 0
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the camera state attributes."""
- attr = super().state_attributes
+ attr = {}
if self.motion_detection_enabled:
attr["last_recording_start_time"] = timestamp_ms_to_date(
self._caminfo["lastRecordingStartTime"]
@@ -124,14 +126,12 @@ def state_attributes(self):
def is_recording(self):
"""Return true if the camera is recording."""
recording_state = "DISABLED"
- if "recordingIndicator" in self._caminfo.keys():
+ if "recordingIndicator" in self._caminfo:
recording_state = self._caminfo["recordingIndicator"]
- return (
- self._caminfo["recordingSettings"]["fullTimeRecordEnabled"]
- or recording_state == "MOTION_INPROGRESS"
- or recording_state == "MOTION_FINISHED"
- )
+ return self._caminfo["recordingSettings"][
+ "fullTimeRecordEnabled"
+ ] or recording_state in ["MOTION_INPROGRESS", "MOTION_FINISHED"]
@property
def motion_detection_enabled(self):
@@ -196,10 +196,8 @@ def _login(self):
def camera_image(self):
"""Return the image of this camera."""
-
- if not self._camera:
- if not self._login():
- return
+ if not self._camera and not self._login():
+ return
def _get_image(retry=True):
try:
@@ -256,7 +254,8 @@ def update(self):
self._caminfo = self._nvr.get_camera(self._uuid)
-def timestamp_ms_to_date(epoch_ms) -> datetime or None:
+def timestamp_ms_to_date(epoch_ms: int) -> datetime | None:
"""Convert millisecond timestamp to datetime."""
if epoch_ms:
- return datetime.fromtimestamp(epoch_ms / 1000)
+ return utc_from_timestamp(epoch_ms / 1000)
+ return None
diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py
index ca5caec7ac10fe..d8803931f382a0 100644
--- a/homeassistant/components/vacuum/__init__.py
+++ b/homeassistant/components/vacuum/__init__.py
@@ -2,6 +2,7 @@
from datetime import timedelta
from functools import partial
import logging
+from typing import final
import voluptuous as vol
@@ -271,6 +272,7 @@ def battery_icon(self):
battery_level=self.battery_level, charging=charging
)
+ @final
@property
def state_attributes(self):
"""Return the state attributes of the vacuum cleaner."""
diff --git a/homeassistant/components/vacuum/device_action.py b/homeassistant/components/vacuum/device_action.py
index ed25289da10ede..2308882469e6c9 100644
--- a/homeassistant/components/vacuum/device_action.py
+++ b/homeassistant/components/vacuum/device_action.py
@@ -1,5 +1,5 @@
"""Provides device automations for Vacuum."""
-from typing import List, Optional
+from __future__ import annotations
import voluptuous as vol
@@ -26,7 +26,7 @@
)
-async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device actions for Vacuum devices."""
registry = await entity_registry.async_get_registry(hass)
actions = []
@@ -57,7 +57,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
async def async_call_action_from_config(
- hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context]
+ hass: HomeAssistant, config: dict, variables: dict, context: Context | None
) -> None:
"""Execute a device action."""
config = ACTION_SCHEMA(config)
diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py
index cb17505f6e1022..4803ebdb988543 100644
--- a/homeassistant/components/vacuum/device_condition.py
+++ b/homeassistant/components/vacuum/device_condition.py
@@ -1,5 +1,5 @@
"""Provide the device automations for Vacuum."""
-from typing import Dict, List
+from __future__ import annotations
import voluptuous as vol
@@ -30,7 +30,7 @@
async def async_get_conditions(
hass: HomeAssistant, device_id: str
-) -> List[Dict[str, str]]:
+) -> list[dict[str, str]]:
"""List device conditions for Vacuum devices."""
registry = await entity_registry.async_get_registry(hass)
conditions = []
diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py
index 29fc5628b22794..d5c596b209a108 100644
--- a/homeassistant/components/vacuum/device_trigger.py
+++ b/homeassistant/components/vacuum/device_trigger.py
@@ -1,5 +1,5 @@
"""Provides device automations for Vacuum."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -10,6 +10,7 @@
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
+ CONF_FOR,
CONF_PLATFORM,
CONF_TYPE,
)
@@ -17,7 +18,7 @@
from homeassistant.helpers import config_validation as cv, entity_registry
from homeassistant.helpers.typing import ConfigType
-from . import DOMAIN, STATE_CLEANING, STATE_DOCKED, STATES
+from . import DOMAIN, STATE_CLEANING, STATE_DOCKED
TRIGGER_TYPES = {"cleaning", "docked"}
@@ -25,11 +26,12 @@
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
+ vol.Optional(CONF_FOR): cv.positive_time_period_dict,
}
)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers for Vacuum devices."""
registry = await entity_registry.async_get_registry(hass)
triggers = []
@@ -39,28 +41,29 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
if entry.domain != DOMAIN:
continue
- triggers.append(
+ triggers += [
{
CONF_PLATFORM: "device",
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "cleaning",
+ CONF_TYPE: trigger,
}
- )
- triggers.append(
- {
- CONF_PLATFORM: "device",
- CONF_DEVICE_ID: device_id,
- CONF_DOMAIN: DOMAIN,
- CONF_ENTITY_ID: entry.entity_id,
- CONF_TYPE: "docked",
- }
- )
+ for trigger in TRIGGER_TYPES
+ ]
return triggers
+async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict:
+ """List trigger capabilities."""
+ return {
+ "extra_fields": vol.Schema(
+ {vol.Optional(CONF_FOR): cv.positive_time_period_dict}
+ )
+ }
+
+
async def async_attach_trigger(
hass: HomeAssistant,
config: ConfigType,
@@ -68,21 +71,18 @@ async def async_attach_trigger(
automation_info: dict,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
- config = TRIGGER_SCHEMA(config)
-
if config[CONF_TYPE] == "cleaning":
- from_state = [state for state in STATES if state != STATE_CLEANING]
to_state = STATE_CLEANING
else:
- from_state = [state for state in STATES if state != STATE_DOCKED]
to_state = STATE_DOCKED
state_config = {
CONF_PLATFORM: "state",
CONF_ENTITY_ID: config[CONF_ENTITY_ID],
- state_trigger.CONF_FROM: from_state,
state_trigger.CONF_TO: to_state,
}
+ if CONF_FOR in config:
+ state_config[CONF_FOR] = config[CONF_FOR]
state_config = state_trigger.TRIGGER_SCHEMA(state_config)
return await state_trigger.async_attach_trigger(
hass, state_config, action, automation_info, platform_type="device"
diff --git a/homeassistant/components/vacuum/reproduce_state.py b/homeassistant/components/vacuum/reproduce_state.py
index 48aa9615f1ef16..38958bd47908a1 100644
--- a/homeassistant/components/vacuum/reproduce_state.py
+++ b/homeassistant/components/vacuum/reproduce_state.py
@@ -1,7 +1,9 @@
"""Reproduce an Vacuum state."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -44,8 +46,8 @@ async def _async_reproduce_state(
hass: HomeAssistantType,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -99,8 +101,8 @@ async def async_reproduce_states(
hass: HomeAssistantType,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Vacuum states."""
# Reproduce states in parallel.
diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml
index 3287eafe7f2852..e0064bc475bba4 100644
--- a/homeassistant/components/vacuum/services.yaml
+++ b/homeassistant/components/vacuum/services.yaml
@@ -1,87 +1,80 @@
# Describes the format for available vacuum services
turn_on:
+ name: Turn on
description: Start a new cleaning task.
- fields:
- entity_id:
- description: Name of the vacuum entity.
- example: "vacuum.xiaomi_vacuum_cleaner"
+ target:
turn_off:
+ name: Turn off
description: Stop the current cleaning task and return to home.
- fields:
- entity_id:
- description: Name of the vacuum entity.
- example: "vacuum.xiaomi_vacuum_cleaner"
+ target:
stop:
+ name: Stop
description: Stop the current cleaning task.
- fields:
- entity_id:
- description: Name of the vacuum entity.
- example: "vacuum.xiaomi_vacuum_cleaner"
+ target:
locate:
+ name: Locate
description: Locate the vacuum cleaner robot.
- fields:
- entity_id:
- description: Name of the vacuum entity.
- example: "vacuum.xiaomi_vacuum_cleaner"
+ target:
start_pause:
+ name: Start/Pause
description: Start, pause, or resume the cleaning task.
- fields:
- entity_id:
- description: Name of the vacuum entity.
- example: "vacuum.xiaomi_vacuum_cleaner"
+ target:
start:
+ name: Start
description: Start or resume the cleaning task.
- fields:
- entity_id:
- description: Name of the vacuum entity.
- example: "vacuum.xiaomi_vacuum_cleaner"
+ target:
pause:
+ name: Pause
description: Pause the cleaning task.
- fields:
- entity_id:
- description: Name of the vacuum entity.
- example: "vacuum.xiaomi_vacuum_cleaner"
+ target:
return_to_base:
+ name: Return to base
description: Tell the vacuum cleaner to return to its dock.
- fields:
- entity_id:
- description: Name of the vacuum entity.
- example: "vacuum.xiaomi_vacuum_cleaner"
+ target:
clean_spot:
+ name: Clean spot
description: Tell the vacuum cleaner to do a spot clean-up.
- fields:
- entity_id:
- description: Name of the vacuum entity.
- example: "vacuum.xiaomi_vacuum_cleaner"
+ target:
send_command:
+ name: Send command
description: Send a raw command to the vacuum cleaner.
+ target:
fields:
- entity_id:
- description: Name of the vacuum entity.
- example: "vacuum.xiaomi_vacuum_cleaner"
command:
+ name: Command
description: Command to execute.
+ required: true
example: "set_dnd_timer"
+ selector:
+ text:
params:
+ name: Parameters
description: Parameters for the command.
example: '{ "key": "value" }'
+ selector:
+ object:
set_fan_speed:
+ name: Set fan speed
description: Set the fan speed of the vacuum cleaner.
+ target:
fields:
- entity_id:
- description: Name of the vacuum entity.
- example: "vacuum.xiaomi_vacuum_cleaner"
fan_speed:
- description: Platform dependent vacuum cleaner fan speed, with speed steps, like 'medium' or by percentage, between 0 and 100.
+ name: Fan speed
+ description:
+ Platform dependent vacuum cleaner fan speed, with speed steps, like
+ 'medium' or by percentage, between 0 and 100.
+ required: true
example: "low"
+ selector:
+ text:
diff --git a/homeassistant/components/vacuum/translations/de.json b/homeassistant/components/vacuum/translations/de.json
index be137a5566bd1b..8de386b3506e18 100644
--- a/homeassistant/components/vacuum/translations/de.json
+++ b/homeassistant/components/vacuum/translations/de.json
@@ -18,7 +18,7 @@
"cleaning": "Reinigen",
"docked": "Angedockt",
"error": "Fehler",
- "idle": "Standby",
+ "idle": "Unt\u00e4tig",
"off": "Aus",
"on": "An",
"paused": "Pausiert",
diff --git a/homeassistant/components/vacuum/translations/id.json b/homeassistant/components/vacuum/translations/id.json
index a9827363d5ec19..5fc888515fe6f4 100644
--- a/homeassistant/components/vacuum/translations/id.json
+++ b/homeassistant/components/vacuum/translations/id.json
@@ -1,14 +1,28 @@
{
+ "device_automation": {
+ "action_type": {
+ "clean": "Perintahkan {entity_name} untuk bersih-bersih",
+ "dock": "Kembalikan {entity_name} ke dok"
+ },
+ "condition_type": {
+ "is_cleaning": "{entity_name} sedang membersihkan",
+ "is_docked": "{entity_name} berlabuh di dok"
+ },
+ "trigger_type": {
+ "cleaning": "{entity_name} mulai membersihkan",
+ "docked": "{entity_name} berlabuh"
+ }
+ },
"state": {
"_": {
"cleaning": "Membersihkan",
"docked": "Berlabuh",
"error": "Kesalahan",
"idle": "Siaga",
- "off": "Padam",
+ "off": "Mati",
"on": "Nyala",
- "paused": "Dijeda",
- "returning": "Kembali ke dock"
+ "paused": "Jeda",
+ "returning": "Kembali ke dok"
}
},
"title": "Vakum"
diff --git a/homeassistant/components/vacuum/translations/ko.json b/homeassistant/components/vacuum/translations/ko.json
index 0e59d5157eb750..52e1e1bd36b68b 100644
--- a/homeassistant/components/vacuum/translations/ko.json
+++ b/homeassistant/components/vacuum/translations/ko.json
@@ -1,16 +1,16 @@
{
"device_automation": {
"action_type": {
- "clean": "{entity_name} \uc744(\ub97c) \uccad\uc18c\uc2dc\ud0a4\uae30",
- "dock": "{entity_name} \uc744(\ub97c) \ucda9\uc804\uc2a4\ud14c\uc774\uc158\uc73c\ub85c \ubcf5\uadc0\uc2dc\ud0a4\uae30"
+ "clean": "{entity_name}\uc744(\ub97c) \uccad\uc18c\uc2dc\ud0a4\uae30",
+ "dock": "{entity_name}\uc744(\ub97c) \ucda9\uc804\uc2a4\ud14c\uc774\uc158\uc73c\ub85c \ubcf5\uadc0\uc2dc\ud0a4\uae30"
},
"condition_type": {
- "is_cleaning": "{entity_name} \uc774(\uac00) \uccad\uc18c \uc911\uc774\uba74",
- "is_docked": "{entity_name} \uc774(\uac00) \ub3c4\ud0b9\ub418\uc5b4\uc788\uc73c\uba74"
+ "is_cleaning": "{entity_name}\uc774(\uac00) \uccad\uc18c \uc911\uc774\uba74",
+ "is_docked": "{entity_name}\uc774(\uac00) \ucda9\uc804 \uc2a4\ud14c\uc774\uc158\uc5d0 \uc788\uc73c\uba74"
},
"trigger_type": {
- "cleaning": "{entity_name} \uc774(\uac00) \uccad\uc18c\ub97c \uc2dc\uc791\ud560 \ub54c",
- "docked": "{entity_name} \uc774(\uac00) \ub3c4\ud0b9\ub420 \ub54c"
+ "cleaning": "{entity_name}\uc774(\uac00) \uccad\uc18c\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c",
+ "docked": "{entity_name}\uc774(\uac00) \ucda9\uc804 \uc2a4\ud14c\uc774\uc158\uc5d0 \ubcf5\uadc0\ud588\uc744 \ub54c"
}
},
"state": {
diff --git a/homeassistant/components/vacuum/translations/nl.json b/homeassistant/components/vacuum/translations/nl.json
index 3fbb0ae50beaef..1de347f9875ef0 100644
--- a/homeassistant/components/vacuum/translations/nl.json
+++ b/homeassistant/components/vacuum/translations/nl.json
@@ -25,5 +25,5 @@
"returning": "Terugkeren naar dock"
}
},
- "title": "Stofzuigen"
+ "title": "Stofzuiger"
}
\ No newline at end of file
diff --git a/homeassistant/components/vacuum/translations/uk.json b/homeassistant/components/vacuum/translations/uk.json
index 9febc8aff1f3c5..64223a85f74491 100644
--- a/homeassistant/components/vacuum/translations/uk.json
+++ b/homeassistant/components/vacuum/translations/uk.json
@@ -1,4 +1,18 @@
{
+ "device_automation": {
+ "action_type": {
+ "clean": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u0438\u0442\u0438 {entity_name} \u0440\u043e\u0431\u0438\u0442\u0438 \u043f\u0440\u0438\u0431\u0438\u0440\u0430\u043d\u043d\u044f",
+ "dock": "{entity_name}: \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u0438 \u043d\u0430 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0456\u044e"
+ },
+ "condition_type": {
+ "is_cleaning": "{entity_name} \u0432\u0438\u043a\u043e\u043d\u0443\u0454 \u043f\u0440\u0438\u0431\u0438\u0440\u0430\u043d\u043d\u044f",
+ "is_docked": "{entity_name} \u043d\u0430 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0456\u0457"
+ },
+ "trigger_type": {
+ "cleaning": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u043f\u0440\u0438\u0431\u0438\u0440\u0430\u043d\u043d\u044f",
+ "docked": "{entity_name} \u0441\u0442\u0438\u043a\u0443\u0454\u0442\u044c\u0441\u044f \u0437 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0456\u0454\u044e"
+ }
+ },
"state": {
"_": {
"cleaning": "\u041f\u0440\u0438\u0431\u0438\u0440\u0430\u043d\u043d\u044f",
@@ -8,7 +22,7 @@
"off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e",
"on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e",
"paused": "\u041f\u0440\u0438\u0437\u0443\u043f\u0438\u043d\u0435\u043d\u043e",
- "returning": "\u041f\u043e\u0432\u0435\u0440\u043d\u0435\u043d\u043d\u044f \u0434\u043e \u0434\u043e\u043a\u0430"
+ "returning": "\u041f\u043e\u0432\u0435\u0440\u043d\u0435\u043d\u043d\u044f \u043d\u0430 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0456\u044e"
}
},
"title": "\u041f\u0438\u043b\u043e\u0441\u043e\u0441"
diff --git a/homeassistant/components/vacuum/translations/zh-Hans.json b/homeassistant/components/vacuum/translations/zh-Hans.json
index 9e252236d0a4c3..1e4be0ebe0b0bb 100644
--- a/homeassistant/components/vacuum/translations/zh-Hans.json
+++ b/homeassistant/components/vacuum/translations/zh-Hans.json
@@ -1,7 +1,8 @@
{
"device_automation": {
"action_type": {
- "clean": "\u4f7f {entity_name} \u5f00\u59cb\u6e05\u626b"
+ "clean": "\u4f7f {entity_name} \u5f00\u59cb\u6e05\u626b",
+ "dock": "\u4f7f {entity_name} \u8fd4\u56de\u5e95\u5ea7"
},
"condition_type": {
"is_cleaning": "{entity_name} \u6b63\u5728\u6e05\u626b",
diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py
index 525bf00f50e1e8..e167791e70237c 100644
--- a/homeassistant/components/vallox/fan.py
+++ b/homeassistant/components/vallox/fan.py
@@ -83,7 +83,7 @@ def is_on(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
return {
ATTR_PROFILE_FAN_SPEED_HOME["description"]: self._fan_speed_home,
diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py
index 6f6755a05e77e4..b4269ac4451aeb 100644
--- a/homeassistant/components/vallox/sensor.py
+++ b/homeassistant/components/vallox/sensor.py
@@ -3,6 +3,7 @@
from datetime import datetime, timedelta
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
@@ -12,7 +13,6 @@
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
from . import DOMAIN, METRIC_KEY_MODE, SIGNAL_VALLOX_STATE_UPDATE
@@ -96,7 +96,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(sensors, update_before_add=False)
-class ValloxSensor(Entity):
+class ValloxSensor(SensorEntity):
"""Representation of a Vallox sensor."""
def __init__(
diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py
index 8b1609be6ba24a..31c5da097ffda2 100644
--- a/homeassistant/components/vasttrafik/sensor.py
+++ b/homeassistant/components/vasttrafik/sensor.py
@@ -5,10 +5,9 @@
import vasttrafik
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.const import ATTR_ATTRIBUTION, CONF_DELAY, CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from homeassistant.util.dt import now
@@ -20,7 +19,6 @@
ATTR_TRACK = "track"
ATTRIBUTION = "Data provided by Västtrafik"
-CONF_DELAY = "delay"
CONF_DEPARTURES = "departures"
CONF_FROM = "from"
CONF_HEADING = "heading"
@@ -55,7 +53,6 @@
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the departure sensor."""
-
planner = vasttrafik.JournyPlanner(config.get(CONF_KEY), config.get(CONF_SECRET))
sensors = []
@@ -73,7 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class VasttrafikDepartureSensor(Entity):
+class VasttrafikDepartureSensor(SensorEntity):
"""Implementation of a Vasttrafik Departure Sensor."""
def __init__(self, planner, name, departure, heading, lines, delay):
@@ -108,7 +105,7 @@ def icon(self):
return ICON
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attributes
diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py
index a859567e21950e..a15b0a641ef1e5 100644
--- a/homeassistant/components/velbus/__init__.py
+++ b/homeassistant/components/velbus/__init__.py
@@ -22,7 +22,7 @@
{DOMAIN: vol.Schema({vol.Required(CONF_PORT): cv.string})}, extra=vol.ALLOW_EXTRA
)
-COMPONENT_TYPES = ["switch", "sensor", "binary_sensor", "cover", "climate", "light"]
+PLATFORMS = ["switch", "sensor", "binary_sensor", "cover", "climate", "light"]
async def async_setup(hass, config):
@@ -51,19 +51,19 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
def callback():
modules = controller.get_modules()
discovery_info = {"cntrl": controller}
- for category in COMPONENT_TYPES:
- discovery_info[category] = []
+ for platform in PLATFORMS:
+ discovery_info[platform] = []
for module in modules:
for channel in range(1, module.number_of_channels() + 1):
- for category in COMPONENT_TYPES:
- if category in module.get_categories(channel):
- discovery_info[category].append(
+ for platform in PLATFORMS:
+ if platform in module.get_categories(channel):
+ discovery_info[platform].append(
(module.get_module_address(), channel)
)
hass.data[DOMAIN][entry.entry_id] = discovery_info
- for category in COMPONENT_TYPES:
- hass.add_job(hass.config_entries.async_forward_entry_setup(entry, category))
+ for platform in PLATFORMS:
+ hass.add_job(hass.config_entries.async_forward_entry_setup(entry, platform))
try:
controller = velbus.Controller(entry.data[CONF_PORT])
@@ -113,8 +113,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""Remove the velbus connection."""
await asyncio.wait(
[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in COMPONENT_TYPES
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
hass.data[DOMAIN][entry.entry_id]["cntrl"].stop()
diff --git a/homeassistant/components/velbus/sensor.py b/homeassistant/components/velbus/sensor.py
index 095108b440115c..9d9b68dd4eb78a 100644
--- a/homeassistant/components/velbus/sensor.py
+++ b/homeassistant/components/velbus/sensor.py
@@ -1,4 +1,5 @@
"""Support for Velbus sensors."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR
from . import VelbusEntity
@@ -18,7 +19,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
async_add_entities(entities)
-class VelbusSensor(VelbusEntity):
+class VelbusSensor(VelbusEntity, SensorEntity):
"""Representation of a sensor."""
def __init__(self, module, channel, counter=False):
diff --git a/homeassistant/components/velbus/translations/de.json b/homeassistant/components/velbus/translations/de.json
index c6c872c85e6c82..c6c452953ca3a5 100644
--- a/homeassistant/components/velbus/translations/de.json
+++ b/homeassistant/components/velbus/translations/de.json
@@ -1,13 +1,17 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"step": {
"user": {
"data": {
"name": "Der Name f\u00fcr diese Velbus-Verbindung",
- "port": "Verbindungs details"
+ "port": "Verbindungsdetails"
},
"title": "Definieren des Velbus-Verbindungstyps"
}
diff --git a/homeassistant/components/velbus/translations/hu.json b/homeassistant/components/velbus/translations/hu.json
new file mode 100644
index 00000000000000..414ee7e60c6a1d
--- /dev/null
+++ b/homeassistant/components/velbus/translations/hu.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/velbus/translations/id.json b/homeassistant/components/velbus/translations/id.json
new file mode 100644
index 00000000000000..69a05411dc43b7
--- /dev/null
+++ b/homeassistant/components/velbus/translations/id.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Nama untuk koneksi velbus ini",
+ "port": "String koneksi"
+ },
+ "title": "Tentukan jenis koneksi velbus"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/velbus/translations/ko.json b/homeassistant/components/velbus/translations/ko.json
index 3d23ff3727dc41..fa9c4f7496d4a3 100644
--- a/homeassistant/components/velbus/translations/ko.json
+++ b/homeassistant/components/velbus/translations/ko.json
@@ -1,5 +1,12 @@
{
"config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/velbus/translations/tr.json b/homeassistant/components/velbus/translations/tr.json
new file mode 100644
index 00000000000000..e7ee4ea7157311
--- /dev/null
+++ b/homeassistant/components/velbus/translations/tr.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/velbus/translations/uk.json b/homeassistant/components/velbus/translations/uk.json
new file mode 100644
index 00000000000000..6e8b97cc4579bf
--- /dev/null
+++ b/homeassistant/components/velbus/translations/uk.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "port": "\u0420\u044f\u0434\u043e\u043a \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f"
+ },
+ "title": "Velbus"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py
index 90ed0a91b14eb8..5c1d8bfd37060a 100644
--- a/homeassistant/components/velux/__init__.py
+++ b/homeassistant/components/velux/__init__.py
@@ -10,7 +10,7 @@
DOMAIN = "velux"
DATA_VELUX = "data_velux"
-SUPPORTED_DOMAINS = ["cover", "scene"]
+PLATFORMS = ["cover", "scene"]
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema(
@@ -34,9 +34,9 @@ async def async_setup(hass, config):
_LOGGER.exception("Can't connect to velux interface: %s", ex)
return False
- for component in SUPPORTED_DOMAINS:
+ for platform in PLATFORMS:
hass.async_create_task(
- discovery.async_load_platform(hass, component, DOMAIN, {}, config)
+ discovery.async_load_platform(hass, platform, DOMAIN, {}, config)
)
return True
diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py
index 261339f70dd43c..b4d8264a3ab807 100644
--- a/homeassistant/components/venstar/climate.py
+++ b/homeassistant/components/venstar/climate.py
@@ -200,7 +200,7 @@ def fan_mode(self):
return FAN_AUTO
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the optional state attributes."""
return {
ATTR_FAN_STATE: self._client.fanstate,
diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py
index 3fd1c189b63647..3654db5072d881 100644
--- a/homeassistant/components/vera/__init__.py
+++ b/homeassistant/components/vera/__init__.py
@@ -1,8 +1,10 @@
"""Support for Vera devices."""
+from __future__ import annotations
+
import asyncio
from collections import defaultdict
import logging
-from typing import Any, Dict, Generic, List, Optional, Type, TypeVar
+from typing import Any, Generic, TypeVar
import pyvera as veraApi
from requests.exceptions import RequestException
@@ -172,7 +174,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
return True
-def map_vera_device(vera_device: veraApi.VeraDevice, remap: List[int]) -> str:
+def map_vera_device(vera_device: veraApi.VeraDevice, remap: list[int]) -> str:
"""Map vera classes to Home Assistant types."""
type_map = {
@@ -187,7 +189,7 @@ def map_vera_device(vera_device: veraApi.VeraDevice, remap: List[int]) -> str:
veraApi.VeraSwitch: "switch",
}
- def map_special_case(instance_class: Type, entity_type: str) -> str:
+ def map_special_case(instance_class: type, entity_type: str) -> str:
if instance_class is veraApi.VeraSwitch and vera_device.device_id in remap:
return "light"
return entity_type
@@ -232,18 +234,20 @@ def _update_callback(self, _device: DeviceType) -> None:
"""Update the state."""
self.schedule_update_ha_state(True)
+ def update(self):
+ """Force a refresh from the device if the device is unavailable."""
+ refresh_needed = self.vera_device.should_poll or not self.available
+ _LOGGER.debug("%s: update called (refresh=%s)", self._name, refresh_needed)
+ if refresh_needed:
+ self.vera_device.refresh()
+
@property
def name(self) -> str:
"""Return the name of the device."""
return self._name
@property
- def should_poll(self) -> bool:
- """Get polling requirement from vera device."""
- return self.vera_device.should_poll
-
- @property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the device."""
attr = {}
@@ -276,6 +280,11 @@ def device_state_attributes(self) -> Optional[Dict[str, Any]]:
return attr
+ @property
+ def available(self):
+ """If device communications have failed return false."""
+ return not self.vera_device.comm_failure
+
@property
def unique_id(self) -> str:
"""Return a unique ID.
diff --git a/homeassistant/components/vera/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py
index a84764209b2003..816234bb6025b7 100644
--- a/homeassistant/components/vera/binary_sensor.py
+++ b/homeassistant/components/vera/binary_sensor.py
@@ -1,5 +1,7 @@
"""Support for Vera binary sensors."""
-from typing import Callable, List, Optional
+from __future__ import annotations
+
+from typing import Callable
import pyvera as veraApi
@@ -19,7 +21,7 @@
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
controller_data = get_controller_data(hass, entry)
@@ -27,7 +29,8 @@ async def async_setup_entry(
[
VeraBinarySensor(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
- ]
+ ],
+ True,
)
@@ -43,10 +46,11 @@ def __init__(
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property
- def is_on(self) -> Optional[bool]:
+ def is_on(self) -> bool | None:
"""Return true if sensor is on."""
return self._state
def update(self) -> None:
"""Get the latest data and update the state."""
+ super().update()
self._state = self.vera_device.is_tripped
diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py
index 70694af012f229..5027becb71fb19 100644
--- a/homeassistant/components/vera/climate.py
+++ b/homeassistant/components/vera/climate.py
@@ -1,5 +1,7 @@
"""Support for Vera thermostats."""
-from typing import Any, Callable, List, Optional
+from __future__ import annotations
+
+from typing import Any, Callable
import pyvera as veraApi
@@ -36,7 +38,7 @@
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
controller_data = get_controller_data(hass, entry)
@@ -44,7 +46,8 @@ async def async_setup_entry(
[
VeraThermostat(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
- ]
+ ],
+ True,
)
@@ -59,7 +62,7 @@ def __init__(
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property
- def supported_features(self) -> Optional[int]:
+ def supported_features(self) -> int | None:
"""Return the list of supported features."""
return SUPPORT_FLAGS
@@ -79,7 +82,7 @@ def hvac_mode(self) -> str:
return HVAC_MODE_OFF
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes.
Need to be a subset of HVAC_MODES.
@@ -87,7 +90,7 @@ def hvac_modes(self) -> List[str]:
return SUPPORT_HVAC
@property
- def fan_mode(self) -> Optional[str]:
+ def fan_mode(self) -> str | None:
"""Return the fan setting."""
mode = self.vera_device.get_fan_mode()
if mode == "ContinuousOn":
@@ -95,7 +98,7 @@ def fan_mode(self) -> Optional[str]:
return FAN_AUTO
@property
- def fan_modes(self) -> Optional[List[str]]:
+ def fan_modes(self) -> list[str] | None:
"""Return a list of available fan modes."""
return FAN_OPERATION_LIST
@@ -109,7 +112,7 @@ def set_fan_mode(self, fan_mode) -> None:
self.schedule_update_ha_state()
@property
- def current_power_w(self) -> Optional[float]:
+ def current_power_w(self) -> float | None:
"""Return the current power usage in W."""
power = self.vera_device.power
if power:
@@ -126,7 +129,7 @@ def temperature_unit(self) -> str:
return TEMP_CELSIUS
@property
- def current_temperature(self) -> Optional[float]:
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self.vera_device.get_current_temperature()
@@ -136,7 +139,7 @@ def operation(self) -> str:
return self.vera_device.get_hvac_mode()
@property
- def target_temperature(self) -> Optional[float]:
+ def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
return self.vera_device.get_current_goal_temperature()
diff --git a/homeassistant/components/vera/common.py b/homeassistant/components/vera/common.py
index fce6475f930c0a..fcc501c20945c2 100644
--- a/homeassistant/components/vera/common.py
+++ b/homeassistant/components/vera/common.py
@@ -1,5 +1,7 @@
"""Common vera code."""
-from typing import DefaultDict, List, NamedTuple, Set
+from __future__ import annotations
+
+from typing import DefaultDict, NamedTuple
import pyvera as pv
@@ -15,12 +17,12 @@ class ControllerData(NamedTuple):
"""Controller data."""
controller: pv.VeraController
- devices: DefaultDict[str, List[pv.VeraDevice]]
- scenes: List[pv.VeraScene]
+ devices: DefaultDict[str, list[pv.VeraDevice]]
+ scenes: list[pv.VeraScene]
config_entry: ConfigEntry
-def get_configured_platforms(controller_data: ControllerData) -> Set[str]:
+def get_configured_platforms(controller_data: ControllerData) -> set[str]:
"""Get configured platforms for a controller."""
platforms = []
for platform in controller_data.devices:
diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py
index 754d2eca542801..a5450cd4a651bf 100644
--- a/homeassistant/components/vera/config_flow.py
+++ b/homeassistant/components/vera/config_flow.py
@@ -1,7 +1,9 @@
"""Config flow for Vera."""
+from __future__ import annotations
+
import logging
import re
-from typing import Any, List
+from typing import Any
import pyvera as pv
from requests.exceptions import RequestException
@@ -13,32 +15,28 @@
from homeassistant.core import callback
from homeassistant.helpers.entity_registry import EntityRegistry
-from .const import ( # pylint: disable=unused-import
- CONF_CONTROLLER,
- CONF_LEGACY_UNIQUE_ID,
- DOMAIN,
-)
+from .const import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN
LIST_REGEX = re.compile("[^0-9]+")
_LOGGER = logging.getLogger(__name__)
-def fix_device_id_list(data: List[Any]) -> List[int]:
+def fix_device_id_list(data: list[Any]) -> list[int]:
"""Fix the id list by converting it to a supported int list."""
return str_to_int_list(list_to_str(data))
-def str_to_int_list(data: str) -> List[int]:
+def str_to_int_list(data: str) -> list[int]:
"""Convert a string to an int list."""
return [int(s) for s in LIST_REGEX.split(data) if len(s) > 0]
-def list_to_str(data: List[Any]) -> str:
+def list_to_str(data: list[Any]) -> str:
"""Convert an int list to a string."""
return " ".join([str(i) for i in data])
-def new_options(lights: List[int], exclude: List[int]) -> dict:
+def new_options(lights: list[int], exclude: list[int]) -> dict:
"""Create a standard options object."""
return {CONF_LIGHTS: lights, CONF_EXCLUDE: exclude}
diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py
index 69e412bdadeac7..cf3dd4a3d135b2 100644
--- a/homeassistant/components/vera/cover.py
+++ b/homeassistant/components/vera/cover.py
@@ -1,5 +1,7 @@
"""Support for Vera cover - curtains, rollershutters etc."""
-from typing import Any, Callable, List
+from __future__ import annotations
+
+from typing import Any, Callable
import pyvera as veraApi
@@ -20,7 +22,7 @@
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
controller_data = get_controller_data(hass, entry)
@@ -28,7 +30,8 @@ async def async_setup_entry(
[
VeraCover(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
- ]
+ ],
+ True,
)
diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py
index 7daaf095a5ce87..7fcb726efcc912 100644
--- a/homeassistant/components/vera/light.py
+++ b/homeassistant/components/vera/light.py
@@ -1,5 +1,7 @@
"""Support for Vera lights."""
-from typing import Any, Callable, List, Optional, Tuple
+from __future__ import annotations
+
+from typing import Any, Callable
import pyvera as veraApi
@@ -24,7 +26,7 @@
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
controller_data = get_controller_data(hass, entry)
@@ -32,7 +34,8 @@ async def async_setup_entry(
[
VeraLight(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
- ]
+ ],
+ True,
)
@@ -50,12 +53,12 @@ def __init__(
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property
- def brightness(self) -> Optional[int]:
+ def brightness(self) -> int | None:
"""Return the brightness of the light."""
return self._brightness
@property
- def hs_color(self) -> Optional[Tuple[float, float]]:
+ def hs_color(self) -> tuple[float, float] | None:
"""Return the color of the light."""
return self._color
@@ -92,6 +95,7 @@ def is_on(self) -> bool:
def update(self) -> None:
"""Call to update state."""
+ super().update()
self._state = self.vera_device.is_switched_on()
if self.vera_device.is_dimmable:
# If it is dimmable, both functions exist. In case color
diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py
index 36a5f4cf2f3e2a..eada4b205502bf 100644
--- a/homeassistant/components/vera/lock.py
+++ b/homeassistant/components/vera/lock.py
@@ -1,5 +1,7 @@
"""Support for Vera locks."""
-from typing import Any, Callable, Dict, List, Optional
+from __future__ import annotations
+
+from typing import Any, Callable
import pyvera as veraApi
@@ -23,7 +25,7 @@
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
controller_data = get_controller_data(hass, entry)
@@ -31,7 +33,8 @@ async def async_setup_entry(
[
VeraLock(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
- ]
+ ],
+ True,
)
@@ -55,19 +58,19 @@ def unlock(self, **kwargs: Any) -> None:
self._state = STATE_UNLOCKED
@property
- def is_locked(self) -> Optional[bool]:
+ def is_locked(self) -> bool | None:
"""Return true if device is on."""
return self._state == STATE_LOCKED
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Who unlocked the lock and did a low battery alert fire.
Reports on the previous poll cycle.
changed_by_name is a string like 'Bob'.
low_battery is 1 if an alert fired, 0 otherwise.
"""
- data = super().device_state_attributes
+ data = super().extra_state_attributes
last_user = self.vera_device.get_last_user_alert()
if last_user is not None:
@@ -77,7 +80,7 @@ def device_state_attributes(self) -> Optional[Dict[str, Any]]:
return data
@property
- def changed_by(self) -> Optional[str]:
+ def changed_by(self) -> str | None:
"""Who unlocked the lock.
Reports on the previous poll cycle.
diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json
index 264f44782f5fb6..76d6bda5c7b300 100644
--- a/homeassistant/components/vera/manifest.json
+++ b/homeassistant/components/vera/manifest.json
@@ -3,6 +3,6 @@
"name": "Vera",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/vera",
- "requirements": ["pyvera==0.3.11"],
- "codeowners": ["@vangorra"]
+ "requirements": ["pyvera==0.3.13"],
+ "codeowners": ["@pavoni"]
}
diff --git a/homeassistant/components/vera/scene.py b/homeassistant/components/vera/scene.py
index a031aadb66cd79..c6eb983a8f78d0 100644
--- a/homeassistant/components/vera/scene.py
+++ b/homeassistant/components/vera/scene.py
@@ -1,5 +1,7 @@
"""Support for Vera scenes."""
-from typing import Any, Callable, Dict, List, Optional
+from __future__ import annotations
+
+from typing import Any, Callable
import pyvera as veraApi
@@ -16,12 +18,12 @@
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
controller_data = get_controller_data(hass, entry)
async_add_entities(
- [VeraScene(device, controller_data) for device in controller_data.scenes]
+ [VeraScene(device, controller_data) for device in controller_data.scenes], True
)
@@ -53,6 +55,6 @@ def name(self) -> str:
return self._name
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the scene."""
return {"vera_scene_id": self.vera_scene.vera_scene_id}
diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py
index e39b752bbf3539..516801b57c6419 100644
--- a/homeassistant/components/vera/sensor.py
+++ b/homeassistant/components/vera/sensor.py
@@ -1,10 +1,16 @@
"""Support for Vera sensors."""
+from __future__ import annotations
+
from datetime import timedelta
-from typing import Callable, List, Optional, cast
+from typing import Callable, cast
import pyvera as veraApi
-from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN, ENTITY_ID_FORMAT
+from homeassistant.components.sensor import (
+ DOMAIN as PLATFORM_DOMAIN,
+ ENTITY_ID_FORMAT,
+ SensorEntity,
+)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import HomeAssistant
@@ -20,7 +26,7 @@
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
controller_data = get_controller_data(hass, entry)
@@ -28,11 +34,12 @@ async def async_setup_entry(
[
VeraSensor(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
- ]
+ ],
+ True,
)
-class VeraSensor(VeraDevice[veraApi.VeraSensor], Entity):
+class VeraSensor(VeraDevice[veraApi.VeraSensor], SensorEntity):
"""Representation of a Vera Sensor."""
def __init__(
@@ -51,7 +58,7 @@ def state(self) -> str:
return self.current_value
@property
- def unit_of_measurement(self) -> Optional[str]:
+ def unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of this entity, if any."""
if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR:
@@ -67,7 +74,7 @@ def unit_of_measurement(self) -> Optional[str]:
def update(self) -> None:
"""Update the state."""
-
+ super().update()
if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR:
self.current_value = self.vera_device.temperature
diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json
index 844d1777f5d752..66958f44a62560 100644
--- a/homeassistant/components/vera/strings.json
+++ b/homeassistant/components/vera/strings.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "cannot_connect": "Could not connect to controller with url {base_url}"
+ "cannot_connect": "Could not connect to controller with URL {base_url}"
},
"step": {
"user": {
"title": "Setup Vera controller",
- "description": "Provide a Vera controller url below. It should look like this: http://192.168.1.161:3480.",
+ "description": "Provide a Vera controller URL below. It should look like this: http://192.168.1.161:3480.",
"data": {
"vera_controller_url": "Controller URL",
"lights": "Vera switch device ids to treat as lights in Home Assistant.",
diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py
index d0cbeba669c765..c779a3c8cfc9e6 100644
--- a/homeassistant/components/vera/switch.py
+++ b/homeassistant/components/vera/switch.py
@@ -1,5 +1,7 @@
"""Support for Vera switches."""
-from typing import Any, Callable, List, Optional
+from __future__ import annotations
+
+from typing import Any, Callable
import pyvera as veraApi
@@ -20,7 +22,7 @@
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
controller_data = get_controller_data(hass, entry)
@@ -28,7 +30,8 @@ async def async_setup_entry(
[
VeraSwitch(device, controller_data)
for device in controller_data.devices.get(PLATFORM_DOMAIN)
- ]
+ ],
+ True,
)
@@ -56,7 +59,7 @@ def turn_off(self, **kwargs: Any) -> None:
self.schedule_update_ha_state()
@property
- def current_power_w(self) -> Optional[float]:
+ def current_power_w(self) -> float | None:
"""Return the current power usage in W."""
power = self.vera_device.power
if power:
@@ -69,4 +72,5 @@ def is_on(self) -> bool:
def update(self) -> None:
"""Update device state."""
+ super().update()
self._state = self.vera_device.is_switched_on()
diff --git a/homeassistant/components/vera/translations/ca.json b/homeassistant/components/vera/translations/ca.json
index 63ec236ef89a6b..13a889cb7db45c 100644
--- a/homeassistant/components/vera/translations/ca.json
+++ b/homeassistant/components/vera/translations/ca.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "cannot_connect": "No s'ha pogut connectar amb el controlador amb l'URL {base_url}"
+ "cannot_connect": "No s'ha pogut connectar amb el controlador amb URL {base_url}"
},
"step": {
"user": {
@@ -10,7 +10,7 @@
"lights": "Identificadors de dispositiu dels commutadors Vera a tractar com a llums a Home Assistant.",
"vera_controller_url": "URL del controlador"
},
- "description": "Proporciona un URL pel controlador Vera. Hauria de quedar aix\u00ed: http://192.168.1.161:3480.",
+ "description": "Proporciona un URL pel controlador Vera. Hauria de ser similar al seg\u00fcent: http://192.168.1.161:3480.",
"title": "Configuraci\u00f3 del controlador Vera"
}
}
diff --git a/homeassistant/components/vera/translations/en.json b/homeassistant/components/vera/translations/en.json
index 5503d6b1034fa0..94f490d71ee1e2 100644
--- a/homeassistant/components/vera/translations/en.json
+++ b/homeassistant/components/vera/translations/en.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "cannot_connect": "Could not connect to controller with url {base_url}"
+ "cannot_connect": "Could not connect to controller with URL {base_url}"
},
"step": {
"user": {
@@ -10,7 +10,7 @@
"lights": "Vera switch device ids to treat as lights in Home Assistant.",
"vera_controller_url": "Controller URL"
},
- "description": "Provide a Vera controller url below. It should look like this: http://192.168.1.161:3480.",
+ "description": "Provide a Vera controller URL below. It should look like this: http://192.168.1.161:3480.",
"title": "Setup Vera controller"
}
}
diff --git a/homeassistant/components/vera/translations/id.json b/homeassistant/components/vera/translations/id.json
new file mode 100644
index 00000000000000..435fc722dba8b4
--- /dev/null
+++ b/homeassistant/components/vera/translations/id.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Tidak dapat terhubung ke pengontrol dengan URL {base_url}"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "exclude": "ID perangkat Vera yang akan dikecualikan dari Home Assistant.",
+ "lights": "ID perangkat sakelar Vera yang diperlakukan sebagai lampu di Home Assistant",
+ "vera_controller_url": "URL Pengontrol"
+ },
+ "description": "Tentukan URL pengontrol Vera di bawah. Ini akan terlihat seperti ini: http://192.168.1.161:3480.",
+ "title": "Siapkan pengontrol Vera"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "exclude": "ID perangkat Vera yang akan dikecualikan dari Home Assistant.",
+ "lights": "ID perangkat sakelar Vera yang diperlakukan sebagai lampu di Home Assistant"
+ },
+ "description": "Lihat dokumentasi Vera untuk informasi tentang parameter opsional: https://www.home-assistant.io/integrations/vera/. Catatan: Setiap perubahan yang dilakukan di sini membutuhkan memulai ulang Home Assistant. Untuk menghapus nilai, ketikkan karakter spasi.",
+ "title": "Opsi pengontrol Vera"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vera/translations/it.json b/homeassistant/components/vera/translations/it.json
index e144bf251cd6c1..3ec026beaac5b7 100644
--- a/homeassistant/components/vera/translations/it.json
+++ b/homeassistant/components/vera/translations/it.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "cannot_connect": "Impossibile connettersi al controllore con l'url {base_url}"
+ "cannot_connect": "Impossibile connettersi al controller con l'URL {base_url}"
},
"step": {
"user": {
@@ -10,7 +10,7 @@
"lights": "Gli ID dei dispositivi switch Vera da trattare come luci in Home Assistant.",
"vera_controller_url": "URL del controller"
},
- "description": "Fornire un url di controllo Vera di seguito. Dovrebbe avere questo aspetto: http://192.168.1.161:3480.",
+ "description": "Fornisci un URL controller Vera di seguito. Dovrebbe assomigliare a questo: http://192.168.1.161:3480.",
"title": "Configurazione controller Vera"
}
}
diff --git a/homeassistant/components/vera/translations/ko.json b/homeassistant/components/vera/translations/ko.json
index 1556b9fe0d2f52..0990435cb4aa56 100644
--- a/homeassistant/components/vera/translations/ko.json
+++ b/homeassistant/components/vera/translations/ko.json
@@ -1,16 +1,16 @@
{
"config": {
"abort": {
- "cannot_connect": "URL {base_url} \uc5d0 \ucee8\ud2b8\ub864\ub7ec\ub97c \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
+ "cannot_connect": "{base_url} URL \uc8fc\uc18c\uc758 \ucee8\ud2b8\ub864\ub7ec\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
"data": {
- "exclude": "Home Assistant \uc5d0\uc11c \uc81c\uc678\ud560 Vera \uae30\uae30 ID.",
- "lights": "Vera \uc2a4\uc704\uce58 \uae30\uae30 ID \ub294 Home Assistant \uc5d0\uc11c \uc870\uba85\uc73c\ub85c \ucde8\uae09\ub429\ub2c8\ub2e4.",
+ "exclude": "Home Assistant\uc5d0\uc11c \uc81c\uc678\ud560 Vera \uae30\uae30 ID.",
+ "lights": "Vera \uc2a4\uc704\uce58 \uae30\uae30 ID \ub294 Home Assistant\uc5d0\uc11c \uc870\uba85\uc73c\ub85c \ucde8\uae09\ub429\ub2c8\ub2e4.",
"vera_controller_url": "\ucee8\ud2b8\ub864\ub7ec URL"
},
- "description": "\uc544\ub798\uc5d0 Vera \ucee8\ud2b8\ub864\ub7ec URL \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. http://192.168.1.161:3480 \uacfc \uac19\uc740 \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4.",
+ "description": "\uc544\ub798\uc5d0 Vera \ucee8\ud2b8\ub864\ub7ec URL \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. http://192.168.1.161:3480 \uacfc \uac19\uc740 \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4.",
"title": "Vera \ucee8\ud2b8\ub864\ub7ec \uc124\uc815\ud558\uae30"
}
}
@@ -19,8 +19,8 @@
"step": {
"init": {
"data": {
- "exclude": "Home Assistant \uc5d0\uc11c \uc81c\uc678\ud560 Vera \uae30\uae30 ID.",
- "lights": "Vera \uc2a4\uc704\uce58 \uae30\uae30 ID \ub294 Home Assistant \uc5d0\uc11c \uc870\uba85\uc73c\ub85c \ucde8\uae09\ub429\ub2c8\ub2e4."
+ "exclude": "Home Assistant\uc5d0\uc11c \uc81c\uc678\ud560 Vera \uae30\uae30 ID.",
+ "lights": "Vera \uc2a4\uc704\uce58 \uae30\uae30 ID \ub294 Home Assistant\uc5d0\uc11c \uc870\uba85\uc73c\ub85c \ucde8\uae09\ub429\ub2c8\ub2e4."
},
"description": "\ub9e4\uac1c \ubcc0\uc218 \uc120\ud0dd\uc0ac\ud56d\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 vera \uc124\uba85\uc11c\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694: https://www.home-assistant.io/integrations/vera/. \ucc38\uace0: \uc5ec\uae30\uc5d0\uc11c \ubcc0\uacbd\ud558\uba74 Home Assistant \uc11c\ubc84\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud574\uc57c \ud569\ub2c8\ub2e4. \uac12\uc744 \uc9c0\uc6b0\ub824\uba74 \uc785\ub825\ub780\uc744 \uacf5\ubc31\uc73c\ub85c \ub450\uc138\uc694.",
"title": "Vera \ucee8\ud2b8\ub864\ub7ec \uc635\uc158"
diff --git a/homeassistant/components/vera/translations/no.json b/homeassistant/components/vera/translations/no.json
index 7ec6850a7c8b8c..f1454be67998fa 100644
--- a/homeassistant/components/vera/translations/no.json
+++ b/homeassistant/components/vera/translations/no.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "cannot_connect": "Kunne ikke koble til kontrolleren med url {base_url}"
+ "cannot_connect": "Kan ikke koble til kontrolleren med URL-adressen {base_url}"
},
"step": {
"user": {
@@ -10,7 +10,7 @@
"lights": "Vera bytter enhets ID-er for \u00e5 behandle som lys i Home Assistant",
"vera_controller_url": "URL-adresse for kontroller"
},
- "description": "Oppgi en Vera-kontroller-url nedenfor. Det skal se slik ut: http://192.168.1.161:3480.",
+ "description": "Gi en Vera-kontroller-URL nedenfor. Det skal se slik ut: http://192.168.1.161:3480.",
"title": "Oppsett Vera-kontroller"
}
}
diff --git a/homeassistant/components/vera/translations/tr.json b/homeassistant/components/vera/translations/tr.json
new file mode 100644
index 00000000000000..35e81599bb1dc5
--- /dev/null
+++ b/homeassistant/components/vera/translations/tr.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "{base_url} url'si ile denetleyiciye ba\u011flan\u0131lamad\u0131"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "Vera denetleyici se\u00e7enekleri"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vera/translations/uk.json b/homeassistant/components/vera/translations/uk.json
new file mode 100644
index 00000000000000..8c591a1cc105eb
--- /dev/null
+++ b/homeassistant/components/vera/translations/uk.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u043e\u043c \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e {base_url}."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "exclude": "ID \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 Vera, \u0434\u043b\u044f \u0432\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0437 Home Assistant",
+ "lights": "ID \u0432\u0438\u043c\u0438\u043a\u0430\u0447\u0456\u0432 Vera, \u0434\u043b\u044f \u0456\u043c\u043f\u043e\u0440\u0442\u0443 \u0432 \u043e\u0441\u0432\u0456\u0442\u043b\u0435\u043d\u043d\u044f",
+ "vera_controller_url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430"
+ },
+ "description": "\u0412\u043a\u0430\u0436\u0456\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441\u0443 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 'http://192.168.1.161:3480').",
+ "title": "Vera"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "exclude": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 Vera \u0434\u043b\u044f \u0432\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0437 Home Assistant.",
+ "lights": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 Vera \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u043f\u0440\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0437 \u0432\u0438\u043c\u0438\u043a\u0430\u0447\u0430 \u0432 \u043e\u0441\u0432\u0456\u0442\u043b\u0435\u043d\u043d\u044f \u0432 Home Assistant."
+ },
+ "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438: https://www.home-assistant.io/integrations/vera/.\n\u0414\u043b\u044f \u0432\u043d\u0435\u0441\u0435\u043d\u043d\u044f \u0431\u0443\u0434\u044c-\u044f\u043a\u0438\u0445 \u0437\u043c\u0456\u043d \u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Home Assistant. \u0429\u043e\u0431 \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u0438 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f, \u043f\u043e\u0441\u0442\u0430\u0432\u0442\u0435 \u043f\u0440\u043e\u0431\u0456\u043b.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430 Vera"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py
index 2348d42a0d3cd5..32893aec88be93 100644
--- a/homeassistant/components/verisure/__init__.py
+++ b/homeassistant/components/verisure/__init__.py
@@ -1,223 +1,173 @@
"""Support for Verisure devices."""
-from datetime import timedelta
+from __future__ import annotations
+
+import asyncio
+from contextlib import suppress
+import os
+from typing import Any
-from jsonpath import jsonpath
-import verisure
import voluptuous as vol
+from homeassistant.components.alarm_control_panel import (
+ DOMAIN as ALARM_CONTROL_PANEL_DOMAIN,
+)
+from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
+from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
+from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
+from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, ConfigEntry
from homeassistant.const import (
+ CONF_EMAIL,
CONF_PASSWORD,
- CONF_SCAN_INTERVAL,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
- HTTP_SERVICE_UNAVAILABLE,
)
-from homeassistant.helpers import discovery
+from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
-from homeassistant.util import Throttle
+from homeassistant.helpers.storage import STORAGE_DIR
from .const import (
- ATTR_DEVICE_SERIAL,
- CONF_ALARM,
CONF_CODE_DIGITS,
CONF_DEFAULT_LOCK_CODE,
- CONF_DOOR_WINDOW,
CONF_GIID,
- CONF_HYDROMETERS,
- CONF_LOCKS,
- CONF_MOUSE,
- CONF_SMARTCAM,
- CONF_SMARTPLUGS,
- CONF_THERMOMETERS,
- DEFAULT_SCAN_INTERVAL,
+ CONF_LOCK_CODE_DIGITS,
+ CONF_LOCK_DEFAULT_CODE,
+ DEFAULT_LOCK_CODE_DIGITS,
DOMAIN,
- LOGGER,
- MIN_SCAN_INTERVAL,
- SERVICE_CAPTURE_SMARTCAM,
- SERVICE_DISABLE_AUTOLOCK,
- SERVICE_ENABLE_AUTOLOCK,
)
+from .coordinator import VerisureDataUpdateCoordinator
-HUB = None
+PLATFORMS = [
+ ALARM_CONTROL_PANEL_DOMAIN,
+ BINARY_SENSOR_DOMAIN,
+ CAMERA_DOMAIN,
+ LOCK_DOMAIN,
+ SENSOR_DOMAIN,
+ SWITCH_DOMAIN,
+]
CONFIG_SCHEMA = vol.Schema(
- {
- DOMAIN: vol.Schema(
- {
- vol.Required(CONF_PASSWORD): cv.string,
- vol.Required(CONF_USERNAME): cv.string,
- vol.Optional(CONF_ALARM, default=True): cv.boolean,
- vol.Optional(CONF_CODE_DIGITS, default=4): cv.positive_int,
- vol.Optional(CONF_DOOR_WINDOW, default=True): cv.boolean,
- vol.Optional(CONF_GIID): cv.string,
- vol.Optional(CONF_HYDROMETERS, default=True): cv.boolean,
- vol.Optional(CONF_LOCKS, default=True): cv.boolean,
- vol.Optional(CONF_DEFAULT_LOCK_CODE): cv.string,
- vol.Optional(CONF_MOUSE, default=True): cv.boolean,
- vol.Optional(CONF_SMARTPLUGS, default=True): cv.boolean,
- vol.Optional(CONF_THERMOMETERS, default=True): cv.boolean,
- vol.Optional(CONF_SMARTCAM, default=True): cv.boolean,
- vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): (
- vol.All(cv.time_period, vol.Clamp(min=MIN_SCAN_INTERVAL))
- ),
- }
- )
- },
+ vol.All(
+ cv.deprecated(DOMAIN),
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Optional(CONF_CODE_DIGITS): cv.positive_int,
+ vol.Optional(CONF_GIID): cv.string,
+ vol.Optional(CONF_DEFAULT_LOCK_CODE): cv.string,
+ },
+ extra=vol.ALLOW_EXTRA,
+ )
+ },
+ ),
extra=vol.ALLOW_EXTRA,
)
-DEVICE_SERIAL_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_SERIAL): cv.string})
+async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
+ """Set up the Verisure integration."""
+ if DOMAIN in config:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data={
+ CONF_EMAIL: config[DOMAIN][CONF_USERNAME],
+ CONF_PASSWORD: config[DOMAIN][CONF_PASSWORD],
+ CONF_GIID: config[DOMAIN].get(CONF_GIID),
+ CONF_LOCK_CODE_DIGITS: config[DOMAIN].get(CONF_CODE_DIGITS),
+ CONF_LOCK_DEFAULT_CODE: config[DOMAIN].get(CONF_LOCK_DEFAULT_CODE),
+ },
+ )
+ )
-def setup(hass, config):
- """Set up the Verisure component."""
- global HUB # pylint: disable=global-statement
- HUB = VerisureHub(config[DOMAIN])
- HUB.update_overview = Throttle(config[DOMAIN][CONF_SCAN_INTERVAL])(
- HUB.update_overview
- )
- if not HUB.login():
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Set up Verisure from a config entry."""
+ # Migrate old YAML settings (hidden in the config entry),
+ # to config entry options. Can be removed after YAML support is gone.
+ if CONF_LOCK_CODE_DIGITS in entry.data or CONF_DEFAULT_LOCK_CODE in entry.data:
+ options = entry.options.copy()
+
+ if (
+ CONF_LOCK_CODE_DIGITS in entry.data
+ and CONF_LOCK_CODE_DIGITS not in entry.options
+ and entry.data[CONF_LOCK_CODE_DIGITS] != DEFAULT_LOCK_CODE_DIGITS
+ ):
+ options.update(
+ {
+ CONF_LOCK_CODE_DIGITS: entry.data[CONF_LOCK_CODE_DIGITS],
+ }
+ )
+
+ if (
+ CONF_DEFAULT_LOCK_CODE in entry.data
+ and CONF_DEFAULT_LOCK_CODE not in entry.options
+ ):
+ options.update(
+ {
+ CONF_DEFAULT_LOCK_CODE: entry.data[CONF_DEFAULT_LOCK_CODE],
+ }
+ )
+
+ data = entry.data.copy()
+ data.pop(CONF_LOCK_CODE_DIGITS, None)
+ data.pop(CONF_DEFAULT_LOCK_CODE, None)
+ hass.config_entries.async_update_entry(entry, data=data, options=options)
+
+ # Continue as normal...
+ coordinator = VerisureDataUpdateCoordinator(hass, entry=entry)
+
+ if not await coordinator.async_login():
+ await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_REAUTH},
+ data={"entry": entry},
+ )
return False
- hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: HUB.logout())
- HUB.update_overview()
-
- for component in (
- "sensor",
- "switch",
- "alarm_control_panel",
- "lock",
- "camera",
- "binary_sensor",
- ):
- discovery.load_platform(hass, component, DOMAIN, {}, config)
-
- async def capture_smartcam(service):
- """Capture a new picture from a smartcam."""
- device_id = service.data[ATTR_DEVICE_SERIAL]
- try:
- await hass.async_add_executor_job(HUB.smartcam_capture, device_id)
- LOGGER.debug("Capturing new image from %s", ATTR_DEVICE_SERIAL)
- except verisure.Error as ex:
- LOGGER.error("Could not capture image, %s", ex)
-
- hass.services.register(
- DOMAIN, SERVICE_CAPTURE_SMARTCAM, capture_smartcam, schema=DEVICE_SERIAL_SCHEMA
- )
- async def disable_autolock(service):
- """Disable autolock on a doorlock."""
- device_id = service.data[ATTR_DEVICE_SERIAL]
- try:
- await hass.async_add_executor_job(HUB.disable_autolock, device_id)
- LOGGER.debug("Disabling autolock on%s", ATTR_DEVICE_SERIAL)
- except verisure.Error as ex:
- LOGGER.error("Could not disable autolock, %s", ex)
-
- hass.services.register(
- DOMAIN, SERVICE_DISABLE_AUTOLOCK, disable_autolock, schema=DEVICE_SERIAL_SCHEMA
- )
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.async_logout)
- async def enable_autolock(service):
- """Enable autolock on a doorlock."""
- device_id = service.data[ATTR_DEVICE_SERIAL]
- try:
- await hass.async_add_executor_job(HUB.enable_autolock, device_id)
- LOGGER.debug("Enabling autolock on %s", ATTR_DEVICE_SERIAL)
- except verisure.Error as ex:
- LOGGER.error("Could not enable autolock, %s", ex)
-
- hass.services.register(
- DOMAIN, SERVICE_ENABLE_AUTOLOCK, enable_autolock, schema=DEVICE_SERIAL_SCHEMA
- )
- return True
+ await coordinator.async_config_entry_first_refresh()
+ hass.data.setdefault(DOMAIN, {})
+ hass.data[DOMAIN][entry.entry_id] = coordinator
-class VerisureHub:
- """A Verisure hub wrapper class."""
+ # Set up all platforms for this device/entry.
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
- def __init__(self, domain_config):
- """Initialize the Verisure hub."""
- self.overview = {}
- self.imageseries = {}
+ return True
- self.config = domain_config
- self.session = verisure.Session(
- domain_config[CONF_USERNAME], domain_config[CONF_PASSWORD]
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Unload Verisure config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *(
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ )
)
+ )
- self.giid = domain_config.get(CONF_GIID)
-
- def login(self):
- """Login to Verisure."""
- try:
- self.session.login()
- except verisure.Error as ex:
- LOGGER.error("Could not log in to verisure, %s", ex)
- return False
- if self.giid:
- return self.set_giid()
- return True
-
- def logout(self):
- """Logout from Verisure."""
- try:
- self.session.logout()
- except verisure.Error as ex:
- LOGGER.error("Could not log out from verisure, %s", ex)
- return False
- return True
-
- def set_giid(self):
- """Set installation GIID."""
- try:
- self.session.set_giid(self.giid)
- except verisure.Error as ex:
- LOGGER.error("Could not set installation GIID, %s", ex)
- return False
- return True
-
- def update_overview(self):
- """Update the overview."""
- try:
- self.overview = self.session.get_overview()
- except verisure.ResponseError as ex:
- LOGGER.error("Could not read overview, %s", ex)
- if ex.status_code == HTTP_SERVICE_UNAVAILABLE: # Service unavailable
- LOGGER.info("Trying to log in again")
- self.login()
- else:
- raise
-
- @Throttle(timedelta(seconds=60))
- def update_smartcam_imageseries(self):
- """Update the image series."""
- self.imageseries = self.session.get_camera_imageseries()
-
- @Throttle(timedelta(seconds=30))
- def smartcam_capture(self, device_id):
- """Capture a new image from a smartcam."""
- self.session.capture_image(device_id)
-
- def disable_autolock(self, device_id):
- """Disable autolock."""
- self.session.set_lock_config(device_id, auto_lock_enabled=False)
-
- def enable_autolock(self, device_id):
- """Enable autolock."""
- self.session.set_lock_config(device_id, auto_lock_enabled=True)
-
- def get(self, jpath, *args):
- """Get values from the overview that matches the jsonpath."""
- res = jsonpath(self.overview, jpath % args)
- return res or []
-
- def get_first(self, jpath, *args):
- """Get first value from the overview that matches the jsonpath."""
- res = self.get(jpath, *args)
- return res[0] if res else None
-
- def get_image_info(self, jpath, *args):
- """Get values from the imageseries that matches the jsonpath."""
- res = jsonpath(self.imageseries, jpath % args)
- return res or []
+ if not unload_ok:
+ return False
+
+ cookie_file = hass.config.path(STORAGE_DIR, f"verisure_{entry.entry_id}")
+ with suppress(FileNotFoundError):
+ await hass.async_add_executor_job(os.unlink, cookie_file)
+
+ del hass.data[DOMAIN][entry.entry_id]
+
+ if not hass.data[DOMAIN]:
+ del hass.data[DOMAIN]
+
+ return True
diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py
index fff58433a9caaa..1cefd6af27293f 100644
--- a/homeassistant/components/verisure/alarm_control_panel.py
+++ b/homeassistant/components/verisure/alarm_control_panel.py
@@ -1,68 +1,66 @@
"""Support for Verisure alarm control panels."""
-from time import sleep
+from __future__ import annotations
-import homeassistant.components.alarm_control_panel as alarm
+import asyncio
+from typing import Any, Callable, Iterable
+
+from homeassistant.components.alarm_control_panel import (
+ FORMAT_NUMBER,
+ AlarmControlPanelEntity,
+)
from homeassistant.components.alarm_control_panel.const import (
SUPPORT_ALARM_ARM_AWAY,
SUPPORT_ALARM_ARM_HOME,
)
-from homeassistant.const import (
- STATE_ALARM_ARMED_AWAY,
- STATE_ALARM_ARMED_HOME,
- STATE_ALARM_DISARMED,
-)
-
-from . import HUB as hub
-from .const import CONF_ALARM, CONF_CODE_DIGITS, CONF_GIID, LOGGER
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+from .const import ALARM_STATE_TO_HA, CONF_GIID, DOMAIN, LOGGER
+from .coordinator import VerisureDataUpdateCoordinator
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Verisure platform."""
- alarms = []
- if int(hub.config.get(CONF_ALARM, 1)):
- hub.update_overview()
- alarms.append(VerisureAlarm())
- add_entities(alarms)
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: Callable[[Iterable[Entity]], None],
+) -> None:
+ """Set up Verisure alarm control panel from a config entry."""
+ async_add_entities([VerisureAlarm(coordinator=hass.data[DOMAIN][entry.entry_id])])
-def set_arm_state(state, code=None):
- """Send set arm state command."""
- transaction_id = hub.session.set_arm_state(code, state)[
- "armStateChangeTransactionId"
- ]
- LOGGER.info("verisure set arm state %s", state)
- transaction = {}
- while "result" not in transaction:
- sleep(0.5)
- transaction = hub.session.get_arm_state_transaction(transaction_id)
- hub.update_overview()
-
-class VerisureAlarm(alarm.AlarmControlPanelEntity):
+class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity):
"""Representation of a Verisure alarm status."""
- def __init__(self):
- """Initialize the Verisure alarm panel."""
- self._state = None
- self._digits = hub.config.get(CONF_CODE_DIGITS)
- self._changed_by = None
+ coordinator: VerisureDataUpdateCoordinator
+
+ _changed_by: str | None = None
+ _state: str | None = None
@property
- def name(self):
- """Return the name of the device."""
- giid = hub.config.get(CONF_GIID)
- if giid is not None:
- aliass = {i["giid"]: i["alias"] for i in hub.session.installations}
- if giid in aliass:
- return "{} alarm".format(aliass[giid])
+ def name(self) -> str:
+ """Return the name of the entity."""
+ return "Verisure Alarm"
- LOGGER.error("Verisure installation giid not found: %s", giid)
+ @property
+ def unique_id(self) -> str:
+ """Return the unique ID for this entity."""
+ return self.coordinator.entry.data[CONF_GIID]
- return "{} alarm".format(hub.session.installations[0]["alias"])
+ @property
+ def device_info(self) -> dict[str, Any]:
+ """Return device information about this entity."""
+ return {
+ "name": "Verisure Alarm",
+ "manufacturer": "Verisure",
+ "model": "VBox",
+ "identifiers": {(DOMAIN, self.coordinator.entry.data[CONF_GIID])},
+ }
@property
- def state(self):
- """Return the state of the device."""
+ def state(self) -> str | None:
+ """Return the state of the entity."""
return self._state
@property
@@ -71,37 +69,53 @@ def supported_features(self) -> int:
return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY
@property
- def code_format(self):
+ def code_format(self) -> str:
"""Return one or more digits/characters."""
- return alarm.FORMAT_NUMBER
+ return FORMAT_NUMBER
@property
- def changed_by(self):
+ def changed_by(self) -> str | None:
"""Return the last change triggered by."""
return self._changed_by
- def update(self):
- """Update alarm status."""
- hub.update_overview()
- status = hub.get_first("$.armState.statusType")
- if status == "DISARMED":
- self._state = STATE_ALARM_DISARMED
- elif status == "ARMED_HOME":
- self._state = STATE_ALARM_ARMED_HOME
- elif status == "ARMED_AWAY":
- self._state = STATE_ALARM_ARMED_AWAY
- elif status != "PENDING":
- LOGGER.error("Unknown alarm state %s", status)
- self._changed_by = hub.get_first("$.armState.name")
-
- def alarm_disarm(self, code=None):
+ async def _async_set_arm_state(self, state: str, code: str | None = None) -> None:
+ """Send set arm state command."""
+ arm_state = await self.hass.async_add_executor_job(
+ self.coordinator.verisure.set_arm_state, code, state
+ )
+ LOGGER.debug("Verisure set arm state %s", state)
+ transaction = {}
+ while "result" not in transaction:
+ await asyncio.sleep(0.5)
+ transaction = await self.hass.async_add_executor_job(
+ self.coordinator.verisure.get_arm_state_transaction,
+ arm_state["armStateChangeTransactionId"],
+ )
+
+ await self.coordinator.async_refresh()
+
+ async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
- set_arm_state("DISARMED", code)
+ await self._async_set_arm_state("DISARMED", code)
- def alarm_arm_home(self, code=None):
+ async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
- set_arm_state("ARMED_HOME", code)
+ await self._async_set_arm_state("ARMED_HOME", code)
- def alarm_arm_away(self, code=None):
+ async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
- set_arm_state("ARMED_AWAY", code)
+ await self._async_set_arm_state("ARMED_AWAY", code)
+
+ @callback
+ def _handle_coordinator_update(self) -> None:
+ """Handle updated data from the coordinator."""
+ self._state = ALARM_STATE_TO_HA.get(
+ self.coordinator.data["alarm"]["statusType"]
+ )
+ self._changed_by = self.coordinator.data["alarm"].get("name")
+ super()._handle_coordinator_update()
+
+ async def async_added_to_hass(self) -> None:
+ """When entity is added to hass."""
+ await super().async_added_to_hass()
+ self._handle_coordinator_update()
diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py
index 5a7f4386eced25..3363178efe28d8 100644
--- a/homeassistant/components/verisure/binary_sensor.py
+++ b/homeassistant/components/verisure/binary_sensor.py
@@ -1,98 +1,132 @@
"""Support for Verisure binary sensors."""
+from __future__ import annotations
+
+from typing import Any, Callable, Iterable
+
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_CONNECTIVITY,
+ DEVICE_CLASS_OPENING,
BinarySensorEntity,
)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from . import CONF_DOOR_WINDOW, HUB as hub
+from .const import CONF_GIID, DOMAIN
+from .coordinator import VerisureDataUpdateCoordinator
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Verisure binary sensors."""
- sensors = []
- hub.update_overview()
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: Callable[[Iterable[Entity]], None],
+) -> None:
+ """Set up Verisure binary sensors based on a config entry."""
+ coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
- if int(hub.config.get(CONF_DOOR_WINDOW, 1)):
- sensors.extend(
- [
- VerisureDoorWindowSensor(device_label)
- for device_label in hub.get(
- "$.doorWindow.doorWindowDevice[*].deviceLabel"
- )
- ]
- )
+ sensors: list[Entity] = [VerisureEthernetStatus(coordinator)]
- sensors.extend([VerisureEthernetStatus()])
- add_entities(sensors)
+ sensors.extend(
+ VerisureDoorWindowSensor(coordinator, serial_number)
+ for serial_number in coordinator.data["door_window"]
+ )
+ async_add_entities(sensors)
-class VerisureDoorWindowSensor(BinarySensorEntity):
+
+class VerisureDoorWindowSensor(CoordinatorEntity, BinarySensorEntity):
"""Representation of a Verisure door window sensor."""
- def __init__(self, device_label):
+ coordinator: VerisureDataUpdateCoordinator
+
+ def __init__(
+ self, coordinator: VerisureDataUpdateCoordinator, serial_number: str
+ ) -> None:
"""Initialize the Verisure door window sensor."""
- self._device_label = device_label
+ super().__init__(coordinator)
+ self.serial_number = serial_number
@property
- def name(self):
- """Return the name of the binary sensor."""
- return hub.get_first(
- "$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].area",
- self._device_label,
- )
+ def name(self) -> str:
+ """Return the name of this entity."""
+ return self.coordinator.data["door_window"][self.serial_number]["area"]
+
+ @property
+ def unique_id(self) -> str:
+ """Return the unique ID for this entity."""
+ return f"{self.serial_number}_door_window"
+
+ @property
+ def device_info(self) -> dict[str, Any]:
+ """Return device information about this entity."""
+ area = self.coordinator.data["door_window"][self.serial_number]["area"]
+ return {
+ "name": area,
+ "suggested_area": area,
+ "manufacturer": "Verisure",
+ "model": "Shock Sensor Detector",
+ "identifiers": {(DOMAIN, self.serial_number)},
+ "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]),
+ }
@property
- def is_on(self):
+ def device_class(self) -> str:
+ """Return the class of this entity."""
+ return DEVICE_CLASS_OPENING
+
+ @property
+ def is_on(self) -> bool:
"""Return the state of the sensor."""
return (
- hub.get_first(
- "$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].state",
- self._device_label,
- )
- == "OPEN"
+ self.coordinator.data["door_window"][self.serial_number]["state"] == "OPEN"
)
@property
- def available(self):
+ def available(self) -> bool:
"""Return True if entity is available."""
return (
- hub.get_first(
- "$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')]",
- self._device_label,
- )
- is not None
+ super().available
+ and self.serial_number in self.coordinator.data["door_window"]
)
- # pylint: disable=no-self-use
- def update(self):
- """Update the state of the sensor."""
- hub.update_overview()
-
-class VerisureEthernetStatus(BinarySensorEntity):
+class VerisureEthernetStatus(CoordinatorEntity, BinarySensorEntity):
"""Representation of a Verisure VBOX internet status."""
+ coordinator: VerisureDataUpdateCoordinator
+
@property
- def name(self):
- """Return the name of the binary sensor."""
+ def name(self) -> str:
+ """Return the name of this entity."""
return "Verisure Ethernet status"
@property
- def is_on(self):
+ def unique_id(self) -> str:
+ """Return the unique ID for this entity."""
+ return f"{self.coordinator.entry.data[CONF_GIID]}_ethernet"
+
+ @property
+ def device_info(self) -> dict[str, Any]:
+ """Return device information about this entity."""
+ return {
+ "name": "Verisure Alarm",
+ "manufacturer": "Verisure",
+ "model": "VBox",
+ "identifiers": {(DOMAIN, self.coordinator.entry.data[CONF_GIID])},
+ }
+
+ @property
+ def is_on(self) -> bool:
"""Return the state of the sensor."""
- return hub.get_first("$.ethernetConnectedNow")
+ return self.coordinator.data["ethernet"]
@property
- def available(self):
+ def available(self) -> bool:
"""Return True if entity is available."""
- return hub.get_first("$.ethernetConnectedNow") is not None
-
- # pylint: disable=no-self-use
- def update(self):
- """Update the state of the sensor."""
- hub.update_overview()
+ return super().available and self.coordinator.data["ethernet"] is not None
@property
- def device_class(self):
- """Return the class of this device, from component DEVICE_CLASSES."""
+ def device_class(self) -> str:
+ """Return the class of this entity."""
return DEVICE_CLASS_CONNECTIVITY
diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py
index a69e1fb95d8542..e667829bb10e0f 100644
--- a/homeassistant/components/verisure/camera.py
+++ b/homeassistant/components/verisure/camera.py
@@ -1,45 +1,90 @@
"""Support for Verisure cameras."""
+from __future__ import annotations
+
import errno
import os
+from typing import Any, Callable, Iterable
+
+from verisure import Error as VerisureError
from homeassistant.components.camera import Camera
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.entity_platform import current_platform
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import CONF_GIID, DOMAIN, LOGGER, SERVICE_CAPTURE_SMARTCAM
+from .coordinator import VerisureDataUpdateCoordinator
-from . import HUB as hub
-from .const import CONF_SMARTCAM, LOGGER
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: Callable[[Iterable[Entity]], None],
+) -> None:
+ """Set up Verisure sensors based on a config entry."""
+ coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Verisure Camera."""
- if not int(hub.config.get(CONF_SMARTCAM, 1)):
- return False
- directory_path = hass.config.config_dir
- if not os.access(directory_path, os.R_OK):
- LOGGER.error("file path %s is not readable", directory_path)
- return False
- hub.update_overview()
- smartcams = [
- VerisureSmartcam(hass, device_label, directory_path)
- for device_label in hub.get("$.customerImageCameras[*].deviceLabel")
- ]
+ platform = current_platform.get()
+ platform.async_register_entity_service(
+ SERVICE_CAPTURE_SMARTCAM,
+ {},
+ VerisureSmartcam.capture_smartcam.__name__,
+ )
- add_entities(smartcams)
+ assert hass.config.config_dir
+ async_add_entities(
+ VerisureSmartcam(coordinator, serial_number, hass.config.config_dir)
+ for serial_number in coordinator.data["cameras"]
+ )
-class VerisureSmartcam(Camera):
+class VerisureSmartcam(CoordinatorEntity, Camera):
"""Representation of a Verisure camera."""
- def __init__(self, hass, device_label, directory_path):
+ coordinator = VerisureDataUpdateCoordinator
+
+ def __init__(
+ self,
+ coordinator: VerisureDataUpdateCoordinator,
+ serial_number: str,
+ directory_path: str,
+ ):
"""Initialize Verisure File Camera component."""
- super().__init__()
+ super().__init__(coordinator)
+ Camera.__init__(self)
- self._device_label = device_label
+ self.serial_number = serial_number
self._directory_path = directory_path
self._image = None
self._image_id = None
- hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.delete_image)
- def camera_image(self):
+ @property
+ def name(self) -> str:
+ """Return the name of this entity."""
+ return self.coordinator.data["cameras"][self.serial_number]["area"]
+
+ @property
+ def unique_id(self) -> str:
+ """Return the unique ID for this entity."""
+ return self.serial_number
+
+ @property
+ def device_info(self) -> dict[str, Any]:
+ """Return device information about this entity."""
+ area = self.coordinator.data["cameras"][self.serial_number]["area"]
+ return {
+ "name": area,
+ "suggested_area": area,
+ "manufacturer": "Verisure",
+ "model": "SmartCam",
+ "identifiers": {(DOMAIN, self.serial_number)},
+ "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]),
+ }
+
+ def camera_image(self) -> bytes | None:
"""Return image response."""
self.check_imagelist()
if not self._image:
@@ -49,30 +94,38 @@ def camera_image(self):
with open(self._image, "rb") as file:
return file.read()
- def check_imagelist(self):
+ def check_imagelist(self) -> None:
"""Check the contents of the image list."""
- hub.update_smartcam_imageseries()
- image_ids = hub.get_image_info(
- "$.imageSeries[?(@.deviceLabel=='%s')].image[0].imageId", self._device_label
- )
- if not image_ids:
+ self.coordinator.update_smartcam_imageseries()
+
+ images = self.coordinator.imageseries.get("imageSeries", [])
+ new_image_id = None
+ for image in images:
+ if image["deviceLabel"] == self.serial_number:
+ new_image_id = image["image"][0]["imageId"]
+ break
+
+ if not new_image_id:
return
- new_image_id = image_ids[0]
+
if new_image_id in ("-1", self._image_id):
LOGGER.debug("The image is the same, or loading image_id")
return
+
LOGGER.debug("Download new image %s", new_image_id)
new_image_path = os.path.join(
self._directory_path, "{}{}".format(new_image_id, ".jpg")
)
- hub.session.download_image(self._device_label, new_image_id, new_image_path)
+ self.coordinator.verisure.download_image(
+ self.serial_number, new_image_id, new_image_path
+ )
LOGGER.debug("Old image_id=%s", self._image_id)
- self.delete_image(self)
+ self.delete_image()
self._image_id = new_image_id
self._image = new_image_path
- def delete_image(self, event):
+ def delete_image(self, _=None) -> None:
"""Delete an old image."""
remove_image = os.path.join(
self._directory_path, "{}{}".format(self._image_id, ".jpg")
@@ -84,9 +137,15 @@ def delete_image(self, event):
if error.errno != errno.ENOENT:
raise
- @property
- def name(self):
- """Return the name of this camera."""
- return hub.get_first(
- "$.customerImageCameras[?(@.deviceLabel=='%s')].area", self._device_label
- )
+ def capture_smartcam(self) -> None:
+ """Capture a new picture from a smartcam."""
+ try:
+ self.coordinator.smartcam_capture(self.serial_number)
+ LOGGER.debug("Capturing new image from %s", self.serial_number)
+ except VerisureError as ex:
+ LOGGER.error("Could not capture image, %s", ex)
+
+ async def async_added_to_hass(self) -> None:
+ """Entity added to Home Assistant."""
+ await super().async_added_to_hass()
+ self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.delete_image)
diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py
new file mode 100644
index 00000000000000..25560b62b1646c
--- /dev/null
+++ b/homeassistant/components/verisure/config_flow.py
@@ -0,0 +1,236 @@
+"""Config flow for Verisure integration."""
+from __future__ import annotations
+
+from typing import Any
+
+from verisure import (
+ Error as VerisureError,
+ LoginError as VerisureLoginError,
+ ResponseError as VerisureResponseError,
+ Session as Verisure,
+)
+import voluptuous as vol
+
+from homeassistant.config_entries import (
+ CONN_CLASS_CLOUD_POLL,
+ ConfigEntry,
+ ConfigFlow,
+ OptionsFlow,
+)
+from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
+from homeassistant.core import callback
+
+from .const import (
+ CONF_GIID,
+ CONF_LOCK_CODE_DIGITS,
+ CONF_LOCK_DEFAULT_CODE,
+ DEFAULT_LOCK_CODE_DIGITS,
+ DOMAIN,
+ LOGGER,
+)
+
+
+class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Verisure."""
+
+ VERSION = 1
+ CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL
+
+ email: str
+ entry: ConfigEntry
+ installations: dict[str, str]
+ password: str
+
+ # These can be removed after YAML import has been removed.
+ giid: str | None = None
+ settings: dict[str, int | str]
+
+ def __init__(self):
+ """Initialize."""
+ self.settings = {}
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry: ConfigEntry) -> VerisureOptionsFlowHandler:
+ """Get the options flow for this handler."""
+ return VerisureOptionsFlowHandler(config_entry)
+
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
+ """Handle the initial step."""
+ errors: dict[str, str] = {}
+
+ if user_input is not None:
+ verisure = Verisure(
+ username=user_input[CONF_EMAIL], password=user_input[CONF_PASSWORD]
+ )
+ try:
+ await self.hass.async_add_executor_job(verisure.login)
+ except VerisureLoginError as ex:
+ LOGGER.debug("Could not log in to Verisure, %s", ex)
+ errors["base"] = "invalid_auth"
+ except (VerisureError, VerisureResponseError) as ex:
+ LOGGER.debug("Unexpected response from Verisure, %s", ex)
+ errors["base"] = "unknown"
+ else:
+ self.email = user_input[CONF_EMAIL]
+ self.password = user_input[CONF_PASSWORD]
+ self.installations = {
+ inst["giid"]: f"{inst['alias']} ({inst['street']})"
+ for inst in verisure.installations
+ }
+
+ return await self.async_step_installation()
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_EMAIL): str,
+ vol.Required(CONF_PASSWORD): str,
+ }
+ ),
+ errors=errors,
+ )
+
+ async def async_step_installation(
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
+ """Select Verisure installation to add."""
+ if len(self.installations) == 1:
+ user_input = {CONF_GIID: list(self.installations)[0]}
+ elif self.giid and self.giid in self.installations:
+ user_input = {CONF_GIID: self.giid}
+
+ if user_input is None:
+ return self.async_show_form(
+ step_id="installation",
+ data_schema=vol.Schema(
+ {vol.Required(CONF_GIID): vol.In(self.installations)}
+ ),
+ )
+
+ await self.async_set_unique_id(user_input[CONF_GIID])
+ self._abort_if_unique_id_configured()
+
+ return self.async_create_entry(
+ title=self.installations[user_input[CONF_GIID]],
+ data={
+ CONF_EMAIL: self.email,
+ CONF_PASSWORD: self.password,
+ CONF_GIID: user_input[CONF_GIID],
+ **self.settings,
+ },
+ )
+
+ async def async_step_reauth(self, data: dict[str, Any]) -> dict[str, Any]:
+ """Handle initiation of re-authentication with Verisure."""
+ self.entry = data["entry"]
+ return await self.async_step_reauth_confirm()
+
+ async def async_step_reauth_confirm(
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
+ """Handle re-authentication with Verisure."""
+ errors: dict[str, str] = {}
+
+ if user_input is not None:
+ verisure = Verisure(
+ username=user_input[CONF_EMAIL], password=user_input[CONF_PASSWORD]
+ )
+ try:
+ await self.hass.async_add_executor_job(verisure.login)
+ except VerisureLoginError as ex:
+ LOGGER.debug("Could not log in to Verisure, %s", ex)
+ errors["base"] = "invalid_auth"
+ except (VerisureError, VerisureResponseError) as ex:
+ LOGGER.debug("Unexpected response from Verisure, %s", ex)
+ errors["base"] = "unknown"
+ else:
+ data = self.entry.data.copy()
+ self.hass.config_entries.async_update_entry(
+ self.entry,
+ data={
+ **data,
+ CONF_EMAIL: user_input[CONF_EMAIL],
+ CONF_PASSWORD: user_input[CONF_PASSWORD],
+ },
+ )
+ 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_EMAIL, default=self.entry.data[CONF_EMAIL]): str,
+ vol.Required(CONF_PASSWORD): str,
+ }
+ ),
+ errors=errors,
+ )
+
+ async def async_step_import(self, user_input: dict[str, Any]) -> dict[str, Any]:
+ """Import Verisure YAML configuration."""
+ if user_input[CONF_GIID]:
+ self.giid = user_input[CONF_GIID]
+ await self.async_set_unique_id(self.giid)
+ self._abort_if_unique_id_configured()
+ else:
+ # The old YAML configuration could handle 1 single Verisure instance.
+ # Therefore, if we don't know the GIID, we can use the discovery
+ # without a unique ID logic, to prevent re-import/discovery.
+ await self._async_handle_discovery_without_unique_id()
+
+ # Settings, later to be converted to config entry options
+ if user_input[CONF_LOCK_CODE_DIGITS]:
+ self.settings[CONF_LOCK_CODE_DIGITS] = user_input[CONF_LOCK_CODE_DIGITS]
+ if user_input[CONF_LOCK_DEFAULT_CODE]:
+ self.settings[CONF_LOCK_DEFAULT_CODE] = user_input[CONF_LOCK_DEFAULT_CODE]
+
+ return await self.async_step_user(user_input)
+
+
+class VerisureOptionsFlowHandler(OptionsFlow):
+ """Handle Verisure options."""
+
+ def __init__(self, entry: ConfigEntry) -> None:
+ """Initialize Verisure options flow."""
+ self.entry = entry
+
+ async def async_step_init(
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
+ """Manage Verisure options."""
+ errors = {}
+
+ if user_input is not None:
+ if len(user_input[CONF_LOCK_DEFAULT_CODE]) not in [
+ 0,
+ user_input[CONF_LOCK_CODE_DIGITS],
+ ]:
+ errors["base"] = "code_format_mismatch"
+ else:
+ return self.async_create_entry(title="", data=user_input)
+
+ return self.async_show_form(
+ step_id="init",
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_LOCK_CODE_DIGITS,
+ default=self.entry.options.get(
+ CONF_LOCK_CODE_DIGITS, DEFAULT_LOCK_CODE_DIGITS
+ ),
+ ): int,
+ vol.Optional(
+ CONF_LOCK_DEFAULT_CODE,
+ default=self.entry.options.get(CONF_LOCK_DEFAULT_CODE),
+ ): str,
+ }
+ ),
+ errors=errors,
+ )
diff --git a/homeassistant/components/verisure/const.py b/homeassistant/components/verisure/const.py
index 89dcfa396aa1b3..030c5a5807559a 100644
--- a/homeassistant/components/verisure/const.py
+++ b/homeassistant/components/verisure/const.py
@@ -2,27 +2,49 @@
from datetime import timedelta
import logging
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_PENDING,
+)
+
DOMAIN = "verisure"
LOGGER = logging.getLogger(__package__)
-ATTR_DEVICE_SERIAL = "device_serial"
-
-CONF_ALARM = "alarm"
-CONF_CODE_DIGITS = "code_digits"
-CONF_DOOR_WINDOW = "door_window"
CONF_GIID = "giid"
-CONF_HYDROMETERS = "hygrometers"
-CONF_LOCKS = "locks"
-CONF_DEFAULT_LOCK_CODE = "default_lock_code"
-CONF_MOUSE = "mouse"
-CONF_SMARTPLUGS = "smartplugs"
-CONF_THERMOMETERS = "thermometers"
-CONF_SMARTCAM = "smartcam"
+CONF_LOCK_CODE_DIGITS = "lock_code_digits"
+CONF_LOCK_DEFAULT_CODE = "lock_default_code"
DEFAULT_SCAN_INTERVAL = timedelta(minutes=1)
-MIN_SCAN_INTERVAL = timedelta(minutes=1)
+DEFAULT_LOCK_CODE_DIGITS = 4
SERVICE_CAPTURE_SMARTCAM = "capture_smartcam"
SERVICE_DISABLE_AUTOLOCK = "disable_autolock"
SERVICE_ENABLE_AUTOLOCK = "enable_autolock"
+
+# Mapping of device types to a human readable name
+DEVICE_TYPE_NAME = {
+ "CAMERAPIR2": "Camera detector",
+ "HOMEPAD1": "VoiceBox",
+ "HUMIDITY1": "Climate sensor",
+ "PIR2": "Camera detector",
+ "SIREN1": "Siren",
+ "SMARTCAMERA1": "SmartCam",
+ "SMOKE2": "Smoke detector",
+ "SMOKE3": "Smoke detector",
+ "VOICEBOX1": "VoiceBox",
+ "WATER1": "Water detector",
+}
+
+ALARM_STATE_TO_HA = {
+ "DISARMED": STATE_ALARM_DISARMED,
+ "ARMED_HOME": STATE_ALARM_ARMED_HOME,
+ "ARMED_AWAY": STATE_ALARM_ARMED_AWAY,
+ "PENDING": STATE_ALARM_PENDING,
+}
+
+# Legacy; to remove after YAML removal
+CONF_CODE_DIGITS = "code_digits"
+CONF_DEFAULT_LOCK_CODE = "default_lock_code"
diff --git a/homeassistant/components/verisure/coordinator.py b/homeassistant/components/verisure/coordinator.py
new file mode 100644
index 00000000000000..b118979f586f53
--- /dev/null
+++ b/homeassistant/components/verisure/coordinator.py
@@ -0,0 +1,114 @@
+"""DataUpdateCoordinator for the Verisure integration."""
+from __future__ import annotations
+
+from datetime import timedelta
+
+from verisure import (
+ Error as VerisureError,
+ ResponseError as VerisureResponseError,
+ Session as Verisure,
+)
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, HTTP_SERVICE_UNAVAILABLE
+from homeassistant.core import Event, HomeAssistant
+from homeassistant.helpers.storage import STORAGE_DIR
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+from homeassistant.util import Throttle
+
+from .const import CONF_GIID, DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER
+
+
+class VerisureDataUpdateCoordinator(DataUpdateCoordinator):
+ """A Verisure Data Update Coordinator."""
+
+ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
+ """Initialize the Verisure hub."""
+ self.imageseries = {}
+ self.entry = entry
+
+ self.verisure = Verisure(
+ username=entry.data[CONF_EMAIL],
+ password=entry.data[CONF_PASSWORD],
+ cookieFileName=hass.config.path(STORAGE_DIR, f"verisure_{entry.entry_id}"),
+ )
+
+ super().__init__(
+ hass, LOGGER, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL
+ )
+
+ async def async_login(self) -> bool:
+ """Login to Verisure."""
+ try:
+ await self.hass.async_add_executor_job(self.verisure.login)
+ except VerisureError as ex:
+ LOGGER.error("Could not log in to verisure, %s", ex)
+ return False
+
+ await self.hass.async_add_executor_job(
+ self.verisure.set_giid, self.entry.data[CONF_GIID]
+ )
+
+ return True
+
+ async def async_logout(self, _event: Event) -> bool:
+ """Logout from Verisure."""
+ try:
+ await self.hass.async_add_executor_job(self.verisure.logout)
+ except VerisureError as ex:
+ LOGGER.error("Could not log out from verisure, %s", ex)
+ return False
+ return True
+
+ async def _async_update_data(self) -> dict:
+ """Fetch data from Verisure."""
+ try:
+ overview = await self.hass.async_add_executor_job(
+ self.verisure.get_overview
+ )
+ except VerisureResponseError as ex:
+ LOGGER.error("Could not read overview, %s", ex)
+ if ex.status_code == HTTP_SERVICE_UNAVAILABLE: # Service unavailable
+ LOGGER.info("Trying to log in again")
+ await self.async_login()
+ return {}
+ raise
+
+ # Store data in a way Home Assistant can easily consume it
+ return {
+ "alarm": overview["armState"],
+ "ethernet": overview.get("ethernetConnectedNow"),
+ "cameras": {
+ device["deviceLabel"]: device
+ for device in overview["customerImageCameras"]
+ },
+ "climate": {
+ device["deviceLabel"]: device for device in overview["climateValues"]
+ },
+ "door_window": {
+ device["deviceLabel"]: device
+ for device in overview["doorWindow"]["doorWindowDevice"]
+ },
+ "locks": {
+ device["deviceLabel"]: device
+ for device in overview["doorLockStatusList"]
+ },
+ "mice": {
+ device["deviceLabel"]: device
+ for device in overview["eventCounts"]
+ if device["deviceType"] == "MOUSE1"
+ },
+ "smart_plugs": {
+ device["deviceLabel"]: device for device in overview["smartPlugs"]
+ },
+ }
+
+ @Throttle(timedelta(seconds=60))
+ def update_smartcam_imageseries(self) -> None:
+ """Update the image series."""
+ self.imageseries = self.verisure.get_camera_imageseries()
+
+ @Throttle(timedelta(seconds=30))
+ def smartcam_capture(self, device_id: str) -> None:
+ """Capture a new image from a smartcam."""
+ self.verisure.capture_image(device_id)
diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py
index 228c8c6c176522..eeec7e53a0a73a 100644
--- a/homeassistant/components/verisure/lock.py
+++ b/homeassistant/components/verisure/lock.py
@@ -1,137 +1,186 @@
"""Support for Verisure locks."""
-from time import monotonic, sleep
+from __future__ import annotations
-from homeassistant.components.lock import LockEntity
-from homeassistant.const import ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED
-
-from . import HUB as hub
-from .const import CONF_CODE_DIGITS, CONF_DEFAULT_LOCK_CODE, CONF_LOCKS, LOGGER
+import asyncio
+from typing import Any, Callable, Iterable
+from verisure import Error as VerisureError
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Verisure lock platform."""
- locks = []
- if int(hub.config.get(CONF_LOCKS, 1)):
- hub.update_overview()
- locks.extend(
- [
- VerisureDoorlock(device_label)
- for device_label in hub.get("$.doorLockStatusList[*].deviceLabel")
- ]
- )
-
- add_entities(locks)
-
-
-class VerisureDoorlock(LockEntity):
+from homeassistant.components.lock import LockEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.entity_platform import current_platform
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import (
+ CONF_GIID,
+ CONF_LOCK_CODE_DIGITS,
+ CONF_LOCK_DEFAULT_CODE,
+ DEFAULT_LOCK_CODE_DIGITS,
+ DOMAIN,
+ LOGGER,
+ SERVICE_DISABLE_AUTOLOCK,
+ SERVICE_ENABLE_AUTOLOCK,
+)
+from .coordinator import VerisureDataUpdateCoordinator
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: Callable[[Iterable[Entity]], None],
+) -> None:
+ """Set up Verisure alarm control panel from a config entry."""
+ coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
+
+ platform = current_platform.get()
+ platform.async_register_entity_service(
+ SERVICE_DISABLE_AUTOLOCK,
+ {},
+ VerisureDoorlock.disable_autolock.__name__,
+ )
+ platform.async_register_entity_service(
+ SERVICE_ENABLE_AUTOLOCK,
+ {},
+ VerisureDoorlock.enable_autolock.__name__,
+ )
+
+ async_add_entities(
+ VerisureDoorlock(coordinator, serial_number)
+ for serial_number in coordinator.data["locks"]
+ )
+
+
+class VerisureDoorlock(CoordinatorEntity, LockEntity):
"""Representation of a Verisure doorlock."""
- def __init__(self, device_label):
+ coordinator: VerisureDataUpdateCoordinator
+
+ def __init__(
+ self, coordinator: VerisureDataUpdateCoordinator, serial_number: str
+ ) -> None:
"""Initialize the Verisure lock."""
- self._device_label = device_label
+ super().__init__(coordinator)
+ self.serial_number = serial_number
self._state = None
- self._digits = hub.config.get(CONF_CODE_DIGITS)
- self._changed_by = None
- self._change_timestamp = 0
- self._default_lock_code = hub.config.get(CONF_DEFAULT_LOCK_CODE)
+ self._digits = coordinator.entry.options.get(
+ CONF_LOCK_CODE_DIGITS, DEFAULT_LOCK_CODE_DIGITS
+ )
@property
- def name(self):
- """Return the name of the lock."""
- return hub.get_first(
- "$.doorLockStatusList[?(@.deviceLabel=='%s')].area", self._device_label
- )
+ def name(self) -> str:
+ """Return the name of this entity."""
+ return self.coordinator.data["locks"][self.serial_number]["area"]
+
+ @property
+ def unique_id(self) -> str:
+ """Return the unique ID for this entity."""
+ return self.serial_number
@property
- def state(self):
- """Return the state of the lock."""
- return self._state
+ def device_info(self) -> dict[str, Any]:
+ """Return device information about this entity."""
+ area = self.coordinator.data["locks"][self.serial_number]["area"]
+ return {
+ "name": area,
+ "suggested_area": area,
+ "manufacturer": "Verisure",
+ "model": "Lockguard Smartlock",
+ "identifiers": {(DOMAIN, self.serial_number)},
+ "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]),
+ }
@property
- def available(self):
+ def available(self) -> bool:
"""Return True if entity is available."""
return (
- hub.get_first(
- "$.doorLockStatusList[?(@.deviceLabel=='%s')]", self._device_label
- )
- is not None
+ super().available and self.serial_number in self.coordinator.data["locks"]
)
@property
- def changed_by(self):
+ def changed_by(self) -> str | None:
"""Last change triggered by."""
- return self._changed_by
+ return self.coordinator.data["locks"][self.serial_number].get("userString")
@property
- def code_format(self):
+ def code_format(self) -> str:
"""Return the required six digit code."""
return "^\\d{%s}$" % self._digits
- def update(self):
- """Update lock status."""
- if monotonic() - self._change_timestamp < 10:
- return
- hub.update_overview()
- status = hub.get_first(
- "$.doorLockStatusList[?(@.deviceLabel=='%s')].lockedState",
- self._device_label,
- )
- if status == "UNLOCKED":
- self._state = STATE_UNLOCKED
- elif status == "LOCKED":
- self._state = STATE_LOCKED
- elif status != "PENDING":
- LOGGER.error("Unknown lock state %s", status)
- self._changed_by = hub.get_first(
- "$.doorLockStatusList[?(@.deviceLabel=='%s')].userString",
- self._device_label,
- )
-
@property
- def is_locked(self):
+ def is_locked(self) -> bool:
"""Return true if lock is locked."""
- return self._state == STATE_LOCKED
+ return (
+ self.coordinator.data["locks"][self.serial_number]["lockedState"]
+ == "LOCKED"
+ )
- def unlock(self, **kwargs):
+ async def async_unlock(self, **kwargs) -> None:
"""Send unlock command."""
- if self._state is None:
- return
-
- code = kwargs.get(ATTR_CODE, self._default_lock_code)
+ code = kwargs.get(
+ ATTR_CODE, self.coordinator.entry.options.get(CONF_LOCK_DEFAULT_CODE)
+ )
if code is None:
LOGGER.error("Code required but none provided")
return
- self.set_lock_state(code, STATE_UNLOCKED)
+ await self.async_set_lock_state(code, STATE_UNLOCKED)
- def lock(self, **kwargs):
+ async def async_lock(self, **kwargs) -> None:
"""Send lock command."""
- if self._state == STATE_LOCKED:
- return
-
- code = kwargs.get(ATTR_CODE, self._default_lock_code)
+ code = kwargs.get(
+ ATTR_CODE, self.coordinator.entry.options.get(CONF_LOCK_DEFAULT_CODE)
+ )
if code is None:
LOGGER.error("Code required but none provided")
return
- self.set_lock_state(code, STATE_LOCKED)
+ await self.async_set_lock_state(code, STATE_LOCKED)
- def set_lock_state(self, code, state):
+ async def async_set_lock_state(self, code: str, state: str) -> None:
"""Send set lock state command."""
- lock_state = "lock" if state == STATE_LOCKED else "unlock"
- transaction_id = hub.session.set_lock_state(
- code, self._device_label, lock_state
- )["doorLockStateChangeTransactionId"]
+ target_state = "lock" if state == STATE_LOCKED else "unlock"
+ lock_state = await self.hass.async_add_executor_job(
+ self.coordinator.verisure.set_lock_state,
+ code,
+ self.serial_number,
+ target_state,
+ )
+
LOGGER.debug("Verisure doorlock %s", state)
transaction = {}
attempts = 0
while "result" not in transaction:
- transaction = hub.session.get_lock_state_transaction(transaction_id)
+ transaction = await self.hass.async_add_executor_job(
+ self.coordinator.verisure.get_lock_state_transaction,
+ lock_state["doorLockStateChangeTransactionId"],
+ )
attempts += 1
if attempts == 30:
break
if attempts > 1:
- sleep(0.5)
+ await asyncio.sleep(0.5)
if transaction["result"] == "OK":
self._state = state
- self._change_timestamp = monotonic()
+
+ def disable_autolock(self) -> None:
+ """Disable autolock on a doorlock."""
+ try:
+ self.coordinator.verisure.set_lock_config(
+ self.serial_number, auto_lock_enabled=False
+ )
+ LOGGER.debug("Disabling autolock on %s", self.serial_number)
+ except VerisureError as ex:
+ LOGGER.error("Could not disable autolock, %s", ex)
+
+ def enable_autolock(self) -> None:
+ """Enable autolock on a doorlock."""
+ try:
+ self.coordinator.verisure.set_lock_config(
+ self.serial_number, auto_lock_enabled=True
+ )
+ LOGGER.debug("Enabling autolock on %s", self.serial_number)
+ except VerisureError as ex:
+ LOGGER.error("Could not enable autolock, %s", ex)
diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json
index 22c5e0c2362c9e..074ef4f955ccfa 100644
--- a/homeassistant/components/verisure/manifest.json
+++ b/homeassistant/components/verisure/manifest.json
@@ -2,6 +2,8 @@
"domain": "verisure",
"name": "Verisure",
"documentation": "https://www.home-assistant.io/integrations/verisure",
- "requirements": ["jsonpath==0.82", "vsure==1.6.1"],
- "codeowners": ["@frenck"]
+ "requirements": ["vsure==1.7.3"],
+ "codeowners": ["@frenck"],
+ "config_flow": true,
+ "dhcp": [{ "macaddress": "0023C1*" }]
}
diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py
index ac7c8f40e8d764..93e1793da8d03f 100644
--- a/homeassistant/components/verisure/sensor.py
+++ b/homeassistant/components/verisure/sensor.py
@@ -1,178 +1,230 @@
"""Support for Verisure sensors."""
+from __future__ import annotations
+
+from typing import Any, Callable, Iterable
+
+from homeassistant.components.sensor import (
+ DEVICE_CLASS_HUMIDITY,
+ DEVICE_CLASS_TEMPERATURE,
+ SensorEntity,
+)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, TEMP_CELSIUS
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from . import HUB as hub
-from .const import CONF_HYDROMETERS, CONF_MOUSE, CONF_THERMOMETERS
+from .const import CONF_GIID, DEVICE_TYPE_NAME, DOMAIN
+from .coordinator import VerisureDataUpdateCoordinator
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Verisure platform."""
- sensors = []
- hub.update_overview()
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: Callable[[Iterable[Entity]], None],
+) -> None:
+ """Set up Verisure sensors based on a config entry."""
+ coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
- if int(hub.config.get(CONF_THERMOMETERS, 1)):
- sensors.extend(
- [
- VerisureThermometer(device_label)
- for device_label in hub.get(
- "$.climateValues[?(@.temperature)].deviceLabel"
- )
- ]
- )
+ sensors: list[Entity] = [
+ VerisureThermometer(coordinator, serial_number)
+ for serial_number, values in coordinator.data["climate"].items()
+ if "temperature" in values
+ ]
- if int(hub.config.get(CONF_HYDROMETERS, 1)):
- sensors.extend(
- [
- VerisureHygrometer(device_label)
- for device_label in hub.get(
- "$.climateValues[?(@.humidity)].deviceLabel"
- )
- ]
- )
+ sensors.extend(
+ VerisureHygrometer(coordinator, serial_number)
+ for serial_number, values in coordinator.data["climate"].items()
+ if "humidity" in values
+ )
- if int(hub.config.get(CONF_MOUSE, 1)):
- sensors.extend(
- [
- VerisureMouseDetection(device_label)
- for device_label in hub.get(
- "$.eventCounts[?(@.deviceType=='MOUSE1')].deviceLabel"
- )
- ]
- )
+ sensors.extend(
+ VerisureMouseDetection(coordinator, serial_number)
+ for serial_number in coordinator.data["mice"]
+ )
- add_entities(sensors)
+ async_add_entities(sensors)
-class VerisureThermometer(Entity):
+class VerisureThermometer(CoordinatorEntity, SensorEntity):
"""Representation of a Verisure thermometer."""
- def __init__(self, device_label):
+ coordinator: VerisureDataUpdateCoordinator
+
+ def __init__(
+ self, coordinator: VerisureDataUpdateCoordinator, serial_number: str
+ ) -> None:
"""Initialize the sensor."""
- self._device_label = device_label
+ super().__init__(coordinator)
+ self.serial_number = serial_number
@property
- def name(self):
- """Return the name of the device."""
- return (
- hub.get_first(
- "$.climateValues[?(@.deviceLabel=='%s')].deviceArea", self._device_label
- )
- + " temperature"
- )
+ def name(self) -> str:
+ """Return the name of the entity."""
+ name = self.coordinator.data["climate"][self.serial_number]["deviceArea"]
+ return f"{name} Temperature"
@property
- def state(self):
- """Return the state of the device."""
- return hub.get_first(
- "$.climateValues[?(@.deviceLabel=='%s')].temperature", self._device_label
+ def unique_id(self) -> str:
+ """Return the unique ID for this entity."""
+ return f"{self.serial_number}_temperature"
+
+ @property
+ def device_class(self) -> str:
+ """Return the class of this entity."""
+ return DEVICE_CLASS_TEMPERATURE
+
+ @property
+ def device_info(self) -> dict[str, Any]:
+ """Return device information about this entity."""
+ device_type = self.coordinator.data["climate"][self.serial_number].get(
+ "deviceType"
)
+ area = self.coordinator.data["climate"][self.serial_number]["deviceArea"]
+ return {
+ "name": area,
+ "suggested_area": area,
+ "manufacturer": "Verisure",
+ "model": DEVICE_TYPE_NAME.get(device_type, device_type),
+ "identifiers": {(DOMAIN, self.serial_number)},
+ "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]),
+ }
+
+ @property
+ def state(self) -> str | None:
+ """Return the state of the entity."""
+ return self.coordinator.data["climate"][self.serial_number]["temperature"]
@property
- def available(self):
+ def available(self) -> bool:
"""Return True if entity is available."""
return (
- hub.get_first(
- "$.climateValues[?(@.deviceLabel=='%s')].temperature",
- self._device_label,
- )
- is not None
+ super().available
+ and self.serial_number in self.coordinator.data["climate"]
+ and "temperature" in self.coordinator.data["climate"][self.serial_number]
)
@property
- def unit_of_measurement(self):
+ def unit_of_measurement(self) -> str:
"""Return the unit of measurement of this entity."""
return TEMP_CELSIUS
- # pylint: disable=no-self-use
- def update(self):
- """Update the sensor."""
- hub.update_overview()
-
-class VerisureHygrometer(Entity):
+class VerisureHygrometer(CoordinatorEntity, SensorEntity):
"""Representation of a Verisure hygrometer."""
- def __init__(self, device_label):
+ coordinator: VerisureDataUpdateCoordinator
+
+ def __init__(
+ self, coordinator: VerisureDataUpdateCoordinator, serial_number: str
+ ) -> None:
"""Initialize the sensor."""
- self._device_label = device_label
+ super().__init__(coordinator)
+ self.serial_number = serial_number
@property
- def name(self):
- """Return the name of the device."""
- return (
- hub.get_first(
- "$.climateValues[?(@.deviceLabel=='%s')].deviceArea", self._device_label
- )
- + " humidity"
- )
+ def name(self) -> str:
+ """Return the name of the entity."""
+ name = self.coordinator.data["climate"][self.serial_number]["deviceArea"]
+ return f"{name} Humidity"
+
+ @property
+ def unique_id(self) -> str:
+ """Return the unique ID for this entity."""
+ return f"{self.serial_number}_humidity"
+
+ @property
+ def device_class(self) -> str:
+ """Return the class of this entity."""
+ return DEVICE_CLASS_HUMIDITY
@property
- def state(self):
- """Return the state of the device."""
- return hub.get_first(
- "$.climateValues[?(@.deviceLabel=='%s')].humidity", self._device_label
+ def device_info(self) -> dict[str, Any]:
+ """Return device information about this entity."""
+ device_type = self.coordinator.data["climate"][self.serial_number].get(
+ "deviceType"
)
+ area = self.coordinator.data["climate"][self.serial_number]["deviceArea"]
+ return {
+ "name": area,
+ "suggested_area": area,
+ "manufacturer": "Verisure",
+ "model": DEVICE_TYPE_NAME.get(device_type, device_type),
+ "identifiers": {(DOMAIN, self.serial_number)},
+ "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]),
+ }
@property
- def available(self):
+ def state(self) -> str | None:
+ """Return the state of the entity."""
+ return self.coordinator.data["climate"][self.serial_number]["humidity"]
+
+ @property
+ def available(self) -> bool:
"""Return True if entity is available."""
return (
- hub.get_first(
- "$.climateValues[?(@.deviceLabel=='%s')].humidity", self._device_label
- )
- is not None
+ super().available
+ and self.serial_number in self.coordinator.data["climate"]
+ and "humidity" in self.coordinator.data["climate"][self.serial_number]
)
@property
- def unit_of_measurement(self):
+ def unit_of_measurement(self) -> str:
"""Return the unit of measurement of this entity."""
return PERCENTAGE
- # pylint: disable=no-self-use
- def update(self):
- """Update the sensor."""
- hub.update_overview()
-
-class VerisureMouseDetection(Entity):
+class VerisureMouseDetection(CoordinatorEntity, SensorEntity):
"""Representation of a Verisure mouse detector."""
- def __init__(self, device_label):
+ coordinator: VerisureDataUpdateCoordinator
+
+ def __init__(
+ self, coordinator: VerisureDataUpdateCoordinator, serial_number: str
+ ) -> None:
"""Initialize the sensor."""
- self._device_label = device_label
+ super().__init__(coordinator)
+ self.serial_number = serial_number
@property
- def name(self):
- """Return the name of the device."""
- return (
- hub.get_first(
- "$.eventCounts[?(@.deviceLabel=='%s')].area", self._device_label
- )
- + " mouse"
- )
+ def name(self) -> str:
+ """Return the name of the entity."""
+ name = self.coordinator.data["mice"][self.serial_number]["area"]
+ return f"{name} Mouse"
@property
- def state(self):
- """Return the state of the device."""
- return hub.get_first(
- "$.eventCounts[?(@.deviceLabel=='%s')].detections", self._device_label
- )
+ def unique_id(self) -> str:
+ """Return the unique ID for this entity."""
+ return f"{self.serial_number}_mice"
+
+ @property
+ def device_info(self) -> dict[str, Any]:
+ """Return device information about this entity."""
+ area = self.coordinator.data["mice"][self.serial_number]["area"]
+ return {
+ "name": area,
+ "suggested_area": area,
+ "manufacturer": "Verisure",
+ "model": "Mouse detector",
+ "identifiers": {(DOMAIN, self.serial_number)},
+ "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]),
+ }
+
+ @property
+ def state(self) -> str | None:
+ """Return the state of the entity."""
+ return self.coordinator.data["mice"][self.serial_number]["detections"]
@property
- def available(self):
+ def available(self) -> bool:
"""Return True if entity is available."""
return (
- hub.get_first("$.eventCounts[?(@.deviceLabel=='%s')]", self._device_label)
- is not None
+ super().available
+ and self.serial_number in self.coordinator.data["mice"]
+ and "detections" in self.coordinator.data["mice"][self.serial_number]
)
@property
- def unit_of_measurement(self):
+ def unit_of_measurement(self) -> str:
"""Return the unit of measurement of this entity."""
return "Mice"
-
- # pylint: disable=no-self-use
- def update(self):
- """Update the sensor."""
- hub.update_overview()
diff --git a/homeassistant/components/verisure/services.yaml b/homeassistant/components/verisure/services.yaml
index 885b8597549de0..2a4e2a008bee65 100644
--- a/homeassistant/components/verisure/services.yaml
+++ b/homeassistant/components/verisure/services.yaml
@@ -1,6 +1,23 @@
capture_smartcam:
- description: Capture a new image from a smartcam.
- fields:
- device_serial:
- description: The serial number of the smartcam you want to capture an image from.
- example: 2DEU AT5Z
+ name: Capture SmartCam image
+ description: Capture a new image from a Verisure SmartCam
+ target:
+ entity:
+ integration: verisure
+ domain: camera
+
+enable_autolock:
+ name: Enable autolock
+ description: Enable autolock of a Verisure Lockguard Smartlock
+ target:
+ entity:
+ integration: verisure
+ domain: lock
+
+disable_autolock:
+ name: Disable autolock
+ description: Disable autolock of a Verisure Lockguard Smartlock
+ target:
+ entity:
+ integration: verisure
+ domain: lock
diff --git a/homeassistant/components/verisure/strings.json b/homeassistant/components/verisure/strings.json
new file mode 100644
index 00000000000000..5170bff5faa4f1
--- /dev/null
+++ b/homeassistant/components/verisure/strings.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "description": "Sign-in with your Verisure My Pages account.",
+ "email": "[%key:common::config_flow::data::email%]",
+ "password": "[%key:common::config_flow::data::password%]"
+ }
+ },
+ "installation": {
+ "description": "Home Assistant found multiple Verisure installations in your My Pages account. Please, select the installation to add to Home Assistant.",
+ "data": {
+ "giid": "Installation"
+ }
+ },
+ "reauth_confirm": {
+ "data": {
+ "description": "Re-authenticate with your Verisure My Pages account.",
+ "email": "[%key:common::config_flow::data::email%]",
+ "password": "[%key:common::config_flow::data::password%]"
+ }
+ }
+ },
+ "error": {
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "lock_code_digits": "Number of digits in PIN code for locks",
+ "lock_default_code": "Default PIN code for locks, used if none is given"
+ }
+ }
+ },
+ "error": {
+ "code_format_mismatch": "The default PIN code does not match the required number of digits"
+ }
+ }
+}
diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py
index 4615e0a2a494ab..f55db8ce4282fb 100644
--- a/homeassistant/components/verisure/switch.py
+++ b/homeassistant/components/verisure/switch.py
@@ -1,78 +1,96 @@
"""Support for Verisure Smartplugs."""
+from __future__ import annotations
+
from time import monotonic
+from typing import Any, Callable, Iterable
from homeassistant.components.switch import SwitchEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from . import CONF_SMARTPLUGS, HUB as hub
-
+from .const import CONF_GIID, DOMAIN
+from .coordinator import VerisureDataUpdateCoordinator
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Verisure switch platform."""
- if not int(hub.config.get(CONF_SMARTPLUGS, 1)):
- return False
- hub.update_overview()
- switches = []
- switches.extend(
- [
- VerisureSmartplug(device_label)
- for device_label in hub.get("$.smartPlugs[*].deviceLabel")
- ]
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: Callable[[Iterable[Entity]], None],
+) -> None:
+ """Set up Verisure alarm control panel from a config entry."""
+ coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
+ async_add_entities(
+ VerisureSmartplug(coordinator, serial_number)
+ for serial_number in coordinator.data["smart_plugs"]
)
- add_entities(switches)
-class VerisureSmartplug(SwitchEntity):
+class VerisureSmartplug(CoordinatorEntity, SwitchEntity):
"""Representation of a Verisure smartplug."""
- def __init__(self, device_id):
+ coordinator: VerisureDataUpdateCoordinator
+
+ def __init__(
+ self, coordinator: VerisureDataUpdateCoordinator, serial_number: str
+ ) -> None:
"""Initialize the Verisure device."""
- self._device_label = device_id
+ super().__init__(coordinator)
+ self.serial_number = serial_number
self._change_timestamp = 0
self._state = False
@property
- def name(self):
- """Return the name or location of the smartplug."""
- return hub.get_first(
- "$.smartPlugs[?(@.deviceLabel == '%s')].area", self._device_label
- )
+ def name(self) -> str:
+ """Return the name of this entity."""
+ return self.coordinator.data["smart_plugs"][self.serial_number]["area"]
@property
- def is_on(self):
+ def unique_id(self) -> str:
+ """Return the unique ID for this entity."""
+ return self.serial_number
+
+ @property
+ def device_info(self) -> dict[str, Any]:
+ """Return device information about this entity."""
+ area = self.coordinator.data["smart_plugs"][self.serial_number]["area"]
+ return {
+ "name": area,
+ "suggested_area": area,
+ "manufacturer": "Verisure",
+ "model": "SmartPlug",
+ "identifiers": {(DOMAIN, self.serial_number)},
+ "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]),
+ }
+
+ @property
+ def is_on(self) -> bool:
"""Return true if on."""
if monotonic() - self._change_timestamp < 10:
return self._state
self._state = (
- hub.get_first(
- "$.smartPlugs[?(@.deviceLabel == '%s')].currentState",
- self._device_label,
- )
+ self.coordinator.data["smart_plugs"][self.serial_number]["currentState"]
== "ON"
)
return self._state
@property
- def available(self):
+ def available(self) -> bool:
"""Return True if entity is available."""
return (
- hub.get_first("$.smartPlugs[?(@.deviceLabel == '%s')]", self._device_label)
- is not None
+ super().available
+ and self.serial_number in self.coordinator.data["smart_plugs"]
)
- def turn_on(self, **kwargs):
+ def turn_on(self, **kwargs) -> None:
"""Set smartplug status on."""
- hub.session.set_smartplug_state(self._device_label, True)
+ self.coordinator.verisure.set_smartplug_state(self.serial_number, True)
self._state = True
self._change_timestamp = monotonic()
- def turn_off(self, **kwargs):
+ def turn_off(self, **kwargs) -> None:
"""Set smartplug status off."""
- hub.session.set_smartplug_state(self._device_label, False)
+ self.coordinator.verisure.set_smartplug_state(self.serial_number, False)
self._state = False
self._change_timestamp = monotonic()
-
- # pylint: disable=no-self-use
- def update(self):
- """Get the latest date of the smartplug."""
- hub.update_overview()
diff --git a/homeassistant/components/verisure/translations/ca.json b/homeassistant/components/verisure/translations/ca.json
new file mode 100644
index 00000000000000..0ddcf9513f4753
--- /dev/null
+++ b/homeassistant/components/verisure/translations/ca.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El compte ja ha estat configurat",
+ "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament"
+ },
+ "error": {
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "installation": {
+ "data": {
+ "giid": "Instal\u00b7laci\u00f3"
+ },
+ "description": "Home Assistant ha trobat diverses instal\u00b7lacions Verisure al compte de My Pages. Selecciona la instal\u00b7laci\u00f3 a afegir a Home Assistant."
+ },
+ "reauth_confirm": {
+ "data": {
+ "description": "Torna a autenticar-te amb el compte de Verisure My Pages.",
+ "email": "Correu electr\u00f2nic",
+ "password": "Contrasenya"
+ }
+ },
+ "user": {
+ "data": {
+ "description": "Inicia sessi\u00f3 amb el compte de Verisure My Pages.",
+ "email": "Correu electr\u00f2nic",
+ "password": "Contrasenya"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "code_format_mismatch": "El codi PIN predeterminat no coincideix amb el nombre de d\u00edgits correcte"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "lock_code_digits": "Nombre de d\u00edgits del codi PIN dels panys",
+ "lock_default_code": "Codi PIN dels panys predeterminat, s'utilitza si no se n'indica cap"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/verisure/translations/cs.json b/homeassistant/components/verisure/translations/cs.json
new file mode 100644
index 00000000000000..34165a2381f833
--- /dev/null
+++ b/homeassistant/components/verisure/translations/cs.json
@@ -0,0 +1,26 @@
+{
+ "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": {
+ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
+ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "email": "E-mail",
+ "password": "Heslo"
+ }
+ },
+ "user": {
+ "data": {
+ "email": "E-mail",
+ "password": "Heslo"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/verisure/translations/de.json b/homeassistant/components/verisure/translations/de.json
new file mode 100644
index 00000000000000..3eaf6ff04f6f8a
--- /dev/null
+++ b/homeassistant/components/verisure/translations/de.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto wurde bereits konfiguriert",
+ "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
+ },
+ "error": {
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "installation": {
+ "data": {
+ "giid": "Installation"
+ },
+ "description": "Home Assistant hat mehrere Verisure-Installationen in deinen My Pages-Konto gefunden. Bitte w\u00e4hle die Installation aus, die du zu Home Assistant hinzuf\u00fcgen m\u00f6chtest."
+ },
+ "reauth_confirm": {
+ "data": {
+ "description": "Authentifiziere dich erneut mit deinem Verisure My Pages-Konto.",
+ "email": "E-Mail",
+ "password": "Passwort"
+ }
+ },
+ "user": {
+ "data": {
+ "description": "Melde dich mit deinen Verisure My Pages-Konto an.",
+ "email": "E-Mail",
+ "password": "Passwort"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "code_format_mismatch": "Der Standard-PIN-Code stimmt nicht mit der erforderlichen Anzahl von Ziffern \u00fcberein"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "lock_code_digits": "Anzahl der Ziffern im PIN-Code f\u00fcr Schl\u00f6sser",
+ "lock_default_code": "Standard-PIN-Code f\u00fcr Schl\u00f6sser, wird verwendet wenn keiner angegeben wird"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/verisure/translations/el.json b/homeassistant/components/verisure/translations/el.json
new file mode 100644
index 00000000000000..87313dba1d4cb7
--- /dev/null
+++ b/homeassistant/components/verisure/translations/el.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "email": "\u0397\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03a4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf",
+ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/verisure/translations/en.json b/homeassistant/components/verisure/translations/en.json
new file mode 100644
index 00000000000000..57f73c3772b9f3
--- /dev/null
+++ b/homeassistant/components/verisure/translations/en.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Account is already configured",
+ "reauth_successful": "Re-authentication was successful"
+ },
+ "error": {
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "installation": {
+ "data": {
+ "giid": "Installation"
+ },
+ "description": "Home Assistant found multiple Verisure installations in your My Pages account. Please, select the installation to add to Home Assistant."
+ },
+ "reauth_confirm": {
+ "data": {
+ "description": "Re-authenticate with your Verisure My Pages account.",
+ "email": "Email",
+ "password": "Password"
+ }
+ },
+ "user": {
+ "data": {
+ "description": "Sign-in with your Verisure My Pages account.",
+ "email": "Email",
+ "password": "Password"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "code_format_mismatch": "The default PIN code does not match the required number of digits"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "lock_code_digits": "Number of digits in PIN code for locks",
+ "lock_default_code": "Default PIN code for locks, used if none is given"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/verisure/translations/es.json b/homeassistant/components/verisure/translations/es.json
new file mode 100644
index 00000000000000..38605e4f86bc13
--- /dev/null
+++ b/homeassistant/components/verisure/translations/es.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "step": {
+ "installation": {
+ "data": {
+ "giid": "Instalaci\u00f3n"
+ },
+ "description": "Home Assistant encontr\u00f3 varias instalaciones de Verisure en su cuenta de Mis p\u00e1ginas. Por favor, seleccione la instalaci\u00f3n para agregar a Home Assistant."
+ },
+ "reauth_confirm": {
+ "data": {
+ "description": "Vuelva a autenticarse con su cuenta Verisure My Pages."
+ }
+ },
+ "user": {
+ "data": {
+ "description": "Inicia sesi\u00f3n con tu cuenta Verisure My Pages."
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "code_format_mismatch": "El c\u00f3digo PIN predeterminado no coincide con el n\u00famero necesario de d\u00edgitos"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "lock_code_digits": "N\u00famero de d\u00edgitos del c\u00f3digo PIN de las cerraduras",
+ "lock_default_code": "C\u00f3digo PIN por defecto para las cerraduras, utilizado si no se indica ninguno"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/verisure/translations/et.json b/homeassistant/components/verisure/translations/et.json
new file mode 100644
index 00000000000000..78a2c987ef2ee3
--- /dev/null
+++ b/homeassistant/components/verisure/translations/et.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kasutaja on juba seadistatud",
+ "reauth_successful": "Taastuvastamine \u00f5nnestus"
+ },
+ "error": {
+ "invalid_auth": "Vigane autentimine",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "installation": {
+ "data": {
+ "giid": "Paigaldus"
+ },
+ "description": "Home Assistant leidis kontolt Minu lehed mitu Verisure paigaldust. Vali Home Assistantile lisatav paigaldus."
+ },
+ "reauth_confirm": {
+ "data": {
+ "description": "Taastuvasta oma Verisure My Pages'i kontoga.",
+ "email": "E-posti aadress",
+ "password": "Salas\u00f5na"
+ }
+ },
+ "user": {
+ "data": {
+ "description": "Logi sisse oma Verisure My Pages kontoga.",
+ "email": "E-posti aadress",
+ "password": "Salas\u00f5na"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "code_format_mismatch": "Vaikimisi PIN-koodi numbrite arv on vale"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "lock_code_digits": "Lukkude PIN-koodi numbrite arv",
+ "lock_default_code": "Lukkude VAIKIMISI PIN-kood, mida kasutatakse juhul kui seda polem\u00e4\u00e4ratud"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/verisure/translations/fr.json b/homeassistant/components/verisure/translations/fr.json
new file mode 100644
index 00000000000000..4909fec88943fa
--- /dev/null
+++ b/homeassistant/components/verisure/translations/fr.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9",
+ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi"
+ },
+ "error": {
+ "invalid_auth": "Authentification invalide",
+ "unknown": "Erreur inattendue"
+ },
+ "step": {
+ "installation": {
+ "data": {
+ "giid": "Installation"
+ },
+ "description": "Home Assistant a trouv\u00e9 plusieurs installations Verisure dans votre compte My Pages. Veuillez s\u00e9lectionner l'installation \u00e0 ajouter \u00e0 Home Assistant."
+ },
+ "reauth_confirm": {
+ "data": {
+ "description": "R\u00e9-authentifiez-vous avec votre compte Verisure My Pages.",
+ "email": "Email",
+ "password": "Mot de passe"
+ }
+ },
+ "user": {
+ "data": {
+ "description": "Connectez-vous avec votre compte Verisure My Pages.",
+ "email": "Email",
+ "password": "Mot de passe"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "code_format_mismatch": "Le code NIP par d\u00e9faut ne correspond pas au nombre de chiffres requis"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "lock_code_digits": "Nombre de chiffres du code NIP pour les serrures",
+ "lock_default_code": "Code PIN par d\u00e9faut pour les serrures, utilis\u00e9 si aucun n'est indiqu\u00e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/verisure/translations/hu.json b/homeassistant/components/verisure/translations/hu.json
new file mode 100644
index 00000000000000..85e53003566483
--- /dev/null
+++ b/homeassistant/components/verisure/translations/hu.json
@@ -0,0 +1,46 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt"
+ },
+ "error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "installation": {
+ "data": {
+ "giid": "Telep\u00edt\u00e9s"
+ }
+ },
+ "reauth_confirm": {
+ "data": {
+ "description": "Hiteles\u00edts \u00fajra a Verisure My Pages fi\u00f3koddal.",
+ "email": "E-mail",
+ "password": "Jelsz\u00f3"
+ }
+ },
+ "user": {
+ "data": {
+ "description": "Jelentkezz be a Verisure My Pages fi\u00f3koddal.",
+ "email": "E-mail",
+ "password": "Jelsz\u00f3"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "code_format_mismatch": "Az alap\u00e9rtelmezett PIN-k\u00f3d nem egyezik meg a sz\u00fcks\u00e9ges sz\u00e1mjegyek sz\u00e1m\u00e1val"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "lock_code_digits": "Sz\u00e1mjegyek sz\u00e1ma a z\u00e1rak PIN-k\u00f3dj\u00e1ban",
+ "lock_default_code": "Alap\u00e9rtelmezett PIN-k\u00f3d z\u00e1rakhoz, akkor haszn\u00e1latos, ha nincs m\u00e1s megadva"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/verisure/translations/it.json b/homeassistant/components/verisure/translations/it.json
new file mode 100644
index 00000000000000..1c90871db71353
--- /dev/null
+++ b/homeassistant/components/verisure/translations/it.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato",
+ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
+ },
+ "error": {
+ "invalid_auth": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "installation": {
+ "data": {
+ "giid": "Installazione"
+ },
+ "description": "Home Assistant ha trovato pi\u00f9 installazioni Verisure nel tuo account My Pages. Per favore, seleziona l'installazione da aggiungere a Home Assistant."
+ },
+ "reauth_confirm": {
+ "data": {
+ "description": "Autenticati nuovamente con il tuo account Verisure My Pages.",
+ "email": "E-mail",
+ "password": "Password"
+ }
+ },
+ "user": {
+ "data": {
+ "description": "Accedi con il tuo account Verisure My Pages.",
+ "email": "E-mail",
+ "password": "Password"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "code_format_mismatch": "Il codice PIN predefinito non corrisponde al numero di cifre richiesto"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "lock_code_digits": "Numero di cifre nel codice PIN per le serrature",
+ "lock_default_code": "Codice PIN predefinito per le serrature, utilizzato se non ne viene fornito nessuno"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/verisure/translations/ko.json b/homeassistant/components/verisure/translations/ko.json
new file mode 100644
index 00000000000000..470aed32b19655
--- /dev/null
+++ b/homeassistant/components/verisure/translations/ko.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "installation": {
+ "data": {
+ "giid": "\uc124\uce58"
+ },
+ "description": "Home Assistant\uac00 My Pages \uacc4\uc815\uc5d0\uc11c \uc5ec\ub7ec \uac00\uc9c0 Verisure \uc124\uce58\ub97c \ubc1c\uacac\ud588\uc2b5\ub2c8\ub2e4. Home Assistant\uc5d0 \ucd94\uac00\ud560 \uc124\uce58\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694."
+ },
+ "reauth_confirm": {
+ "data": {
+ "description": "Verisure My Pages \uacc4\uc815\uc73c\ub85c \ub2e4\uc2dc \uc778\uc99d\ud574\uc8fc\uc138\uc694.",
+ "email": "\uc774\uba54\uc77c",
+ "password": "\ube44\ubc00\ubc88\ud638"
+ }
+ },
+ "user": {
+ "data": {
+ "description": "Verisure My Pages \uacc4\uc815\uc73c\ub85c \ub85c\uadf8\uc778\ud558\uae30.",
+ "email": "\uc774\uba54\uc77c",
+ "password": "\ube44\ubc00\ubc88\ud638"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "code_format_mismatch": "\uae30\ubcf8 PIN \ucf54\ub4dc\uac00 \ud544\uc694\ud55c \uc790\ub9bf\uc218\uc640 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "lock_code_digits": "\uc7a0\uae08\uc7a5\uce58\uc6a9 PIN \ucf54\ub4dc\uc758 \uc790\ub9bf\uc218",
+ "lock_default_code": "\uc7a0\uae08\uc7a5\uce58\uc5d0 \ub300\ud55c \uae30\ubcf8 PIN \ucf54\ub4dc (\uc81c\uacf5\ub41c PIN\uc774 \uc5c6\ub294 \uacbd\uc6b0 \uc0ac\uc6a9)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/verisure/translations/nl.json b/homeassistant/components/verisure/translations/nl.json
new file mode 100644
index 00000000000000..d0519e584fd886
--- /dev/null
+++ b/homeassistant/components/verisure/translations/nl.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Account is al geconfigureerd",
+ "reauth_successful": "Herauthenticatie was succesvol"
+ },
+ "error": {
+ "invalid_auth": "Ongeldige authenticatie",
+ "unknown": "Onverwachte fout"
+ },
+ "step": {
+ "installation": {
+ "data": {
+ "giid": "Installatie"
+ },
+ "description": "Home Assistant heeft meerdere Verisure-installaties gevonden in uw My Pages-account. Selecteer de installatie om toe te voegen aan Home Assistant."
+ },
+ "reauth_confirm": {
+ "data": {
+ "description": "Verifieer opnieuw met uw Verisure My Pages-account.",
+ "email": "E-mail",
+ "password": "Wachtwoord"
+ }
+ },
+ "user": {
+ "data": {
+ "description": "Aanmelden met Verisure My Pages-account.",
+ "email": "E-mail",
+ "password": "Wachtwoord"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "code_format_mismatch": "De standaard pincode komt niet overeen met het vereiste aantal cijfers"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "lock_code_digits": "Aantal cijfers in pincode voor vergrendelingen",
+ "lock_default_code": "Standaard pincode voor vergrendelingen, gebruikt als er geen wordt gegeven"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/verisure/translations/no.json b/homeassistant/components/verisure/translations/no.json
new file mode 100644
index 00000000000000..0bd5529c51de59
--- /dev/null
+++ b/homeassistant/components/verisure/translations/no.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kontoen er allerede konfigurert",
+ "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket"
+ },
+ "error": {
+ "invalid_auth": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "installation": {
+ "data": {
+ "giid": "Installasjon"
+ },
+ "description": "Home Assistant fant flere Verisure-installasjoner i Mine sider-kontoen din. Velg installasjonen du vil legge til i Home Assistant."
+ },
+ "reauth_confirm": {
+ "data": {
+ "description": "Autentiser p\u00e5 nytt med Verisure Mine sider-kontoen din.",
+ "email": "E-post",
+ "password": "Passord"
+ }
+ },
+ "user": {
+ "data": {
+ "description": "Logg p\u00e5 med Verisure Mine sider-kontoen din.",
+ "email": "E-post",
+ "password": "Passord"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "code_format_mismatch": "Standard PIN-kode samsvarer ikke med det n\u00f8dvendige antallet sifre"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "lock_code_digits": "Antall sifre i PIN-kode for l\u00e5ser",
+ "lock_default_code": "Standard PIN-kode for l\u00e5ser, brukes hvis ingen er oppgitt"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/verisure/translations/pl.json b/homeassistant/components/verisure/translations/pl.json
new file mode 100644
index 00000000000000..baaf61f60c3e41
--- /dev/null
+++ b/homeassistant/components/verisure/translations/pl.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto jest ju\u017c skonfigurowane",
+ "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119"
+ },
+ "error": {
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "installation": {
+ "data": {
+ "giid": "Instalacja"
+ },
+ "description": "Home Assistant znalaz\u0142 wiele instalacji Verisure na Twoim koncie. Wybierz instalacj\u0119, kt\u00f3r\u0105 chcesz doda\u0107 do Home Assistanta."
+ },
+ "reauth_confirm": {
+ "data": {
+ "description": "Ponownie uwierzytelnij za pomoc\u0105 konta Verisure.",
+ "email": "Adres e-mail",
+ "password": "Has\u0142o"
+ }
+ },
+ "user": {
+ "data": {
+ "description": "Zaloguj si\u0119 na swoje konto Verisure.",
+ "email": "Adres e-mail",
+ "password": "Has\u0142o"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "code_format_mismatch": "Domy\u015blny kod PIN nie odpowiada wymaganej liczbie cyfr"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "lock_code_digits": "Liczba cyfr w kodzie PIN dla zamk\u00f3w",
+ "lock_default_code": "Domy\u015blny kod PIN dla zamk\u00f3w. U\u017cywany, je\u015bli nie podano \u017cadnego."
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/verisure/translations/pt.json b/homeassistant/components/verisure/translations/pt.json
new file mode 100644
index 00000000000000..a60db2b4ea0b41
--- /dev/null
+++ b/homeassistant/components/verisure/translations/pt.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Conta j\u00e1 configurada",
+ "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida"
+ },
+ "error": {
+ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida",
+ "unknown": "Erro inesperado"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "email": "Email",
+ "password": "Palavra-passe"
+ }
+ },
+ "user": {
+ "data": {
+ "email": "Email",
+ "password": "Palavra-passe"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/verisure/translations/ru.json b/homeassistant/components/verisure/translations/ru.json
new file mode 100644
index 00000000000000..430b8d773d0bc3
--- /dev/null
+++ b/homeassistant/components/verisure/translations/ru.json
@@ -0,0 +1,47 @@
+{
+ "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.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
+ },
+ "error": {
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "installation": {
+ "data": {
+ "giid": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430"
+ },
+ "description": "Home Assistant \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u043b \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043e\u043a Verisure \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u00ab\u041c\u043e\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b\u00bb \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432 Home Assistant."
+ },
+ "reauth_confirm": {
+ "data": {
+ "description": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Verisure.",
+ "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ }
+ },
+ "user": {
+ "data": {
+ "description": "\u0412\u0445\u043e\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Verisure.",
+ "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "code_format_mismatch": "PIN-\u043a\u043e\u0434 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0442\u0440\u0435\u0431\u0443\u0435\u043c\u043e\u043c\u0443 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0443 \u0446\u0438\u0444\u0440."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "lock_code_digits": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0446\u0438\u0444\u0440 \u0432 PIN-\u043a\u043e\u0434\u0435 \u0434\u043b\u044f \u0437\u0430\u043c\u043a\u043e\u0432",
+ "lock_default_code": "PIN-\u043a\u043e\u0434 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0434\u043b\u044f \u0437\u0430\u043c\u043a\u043e\u0432, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439, \u0435\u0441\u043b\u0438 \u043d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043e"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/verisure/translations/zh-Hant.json b/homeassistant/components/verisure/translations/zh-Hant.json
new file mode 100644
index 00000000000000..29410e390fe09c
--- /dev/null
+++ b/homeassistant/components/verisure/translations/zh-Hant.json
@@ -0,0 +1,47 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f"
+ },
+ "error": {
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "installation": {
+ "data": {
+ "giid": "\u5b89\u88dd"
+ },
+ "description": "Home Assistant \u65bc My Pages \u5e33\u865f\u4e2d\u627e\u5230\u591a\u500b Verisure \u5b89\u88dd\u3002\u8acb\u9078\u64c7\u6240\u8981\u65b0\u589e\u81f3 Home Assistant \u7684\u9805\u76ee\u3002"
+ },
+ "reauth_confirm": {
+ "data": {
+ "description": "\u91cd\u65b0\u8a8d\u8b49 Verisure My Pages \u5e33\u865f\u3002",
+ "email": "\u96fb\u5b50\u90f5\u4ef6",
+ "password": "\u5bc6\u78bc"
+ }
+ },
+ "user": {
+ "data": {
+ "description": "\u4ee5 Verisure My Pages \u5e33\u865f\u767b\u5165",
+ "email": "\u96fb\u5b50\u90f5\u4ef6",
+ "password": "\u5bc6\u78bc"
+ }
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "code_format_mismatch": "\u9810\u8a2d PIN \u78bc\u8207\u6240\u9700\u6578\u5b57\u6578\u4e0d\u7b26\u5408"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "lock_code_digits": "\u9580\u9396 PIN \u78bc\u6578\u5b57\u6578",
+ "lock_default_code": "\u9810\u8a2d\u9580\u9396 PIN \u78bc\uff0c\u65bc\u672a\u63d0\u4f9b\u6642\u4f7f\u7528"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/versasense/sensor.py b/homeassistant/components/versasense/sensor.py
index e598093cd37164..d29032af399558 100644
--- a/homeassistant/components/versasense/sensor.py
+++ b/homeassistant/components/versasense/sensor.py
@@ -1,7 +1,7 @@
"""Support for VersaSense sensor peripheral."""
import logging
-from homeassistant.helpers.entity import Entity
+from homeassistant.components.sensor import SensorEntity
from . import DOMAIN
from .const import (
@@ -40,7 +40,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(sensor_list)
-class VSensor(Entity):
+class VSensor(SensorEntity):
"""Representation of a Sensor."""
def __init__(self, peripheral, parent_name, unit, measurement, consumer):
diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json
index 7fc1a097d81a05..7f55273383dc17 100644
--- a/homeassistant/components/version/manifest.json
+++ b/homeassistant/components/version/manifest.json
@@ -2,7 +2,7 @@
"domain": "version",
"name": "Version",
"documentation": "https://www.home-assistant.io/integrations/version",
- "requirements": ["pyhaversion==3.4.2"],
+ "requirements": ["pyhaversion==21.3.0"],
"codeowners": ["@fabaff", "@ludeeus"],
"quality_scale": "internal"
}
diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py
index c030383a4a692b..9d558f4ba7c22f 100644
--- a/homeassistant/components/version/sensor.py
+++ b/homeassistant/components/version/sensor.py
@@ -1,41 +1,42 @@
"""Sensor that can display the current Home Assistant versions."""
from datetime import timedelta
-from pyhaversion import (
- DockerVersion,
- HaIoVersion,
- HassioVersion,
- LocalVersion,
- PyPiVersion,
-)
+from pyhaversion import HaVersion, HaVersionChannel, HaVersionSource
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, CONF_SOURCE
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
ALL_IMAGES = [
"default",
"intel-nuc",
- "qemux86",
- "qemux86-64",
- "qemuarm",
+ "odroid-c2",
+ "odroid-n2",
+ "odroid-xu",
"qemuarm-64",
+ "qemuarm",
+ "qemux86-64",
+ "qemux86",
"raspberrypi",
"raspberrypi2",
- "raspberrypi3",
"raspberrypi3-64",
- "raspberrypi4",
+ "raspberrypi3",
"raspberrypi4-64",
+ "raspberrypi4",
"tinker",
- "odroid-c2",
- "odroid-n2",
- "odroid-xu",
]
-ALL_SOURCES = ["local", "pypi", "hassio", "docker", "haio"]
+ALL_SOURCES = [
+ "container",
+ "haio",
+ "local",
+ "pypi",
+ "supervisor",
+ "hassio", # Kept to not break existing configurations
+ "docker", # Kept to not break existing configurations
+]
CONF_BETA = "beta"
CONF_IMAGE = "image"
@@ -69,21 +70,30 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
session = async_get_clientsession(hass)
- if beta:
- branch = "beta"
- else:
- branch = "stable"
+ channel = HaVersionChannel.BETA if beta else HaVersionChannel.STABLE
if source == "pypi":
- haversion = VersionData(PyPiVersion(hass.loop, session, branch))
- elif source == "hassio":
- haversion = VersionData(HassioVersion(hass.loop, session, branch, image))
- elif source == "docker":
- haversion = VersionData(DockerVersion(hass.loop, session, branch, image))
+ haversion = VersionData(
+ HaVersion(session, source=HaVersionSource.PYPI, channel=channel)
+ )
+ elif source in ["hassio", "supervisor"]:
+ haversion = VersionData(
+ HaVersion(
+ session, source=HaVersionSource.SUPERVISOR, channel=channel, image=image
+ )
+ )
+ elif source in ["docker", "container"]:
+ if image is not None and image != DEFAULT_IMAGE:
+ image = f"{image}-homeassistant"
+ haversion = VersionData(
+ HaVersion(
+ session, source=HaVersionSource.CONTAINER, channel=channel, image=image
+ )
+ )
elif source == "haio":
- haversion = VersionData(HaIoVersion(hass.loop, session))
+ haversion = VersionData(HaVersion(session, source=HaVersionSource.HAIO))
else:
- haversion = VersionData(LocalVersion(hass.loop, session))
+ haversion = VersionData(HaVersion(session, source=HaVersionSource.LOCAL))
if not name:
if source == DEFAULT_SOURCE:
@@ -94,18 +104,31 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([VersionSensor(haversion, name)], True)
-class VersionSensor(Entity):
+class VersionData:
+ """Get the latest data and update the states."""
+
+ def __init__(self, api: HaVersion):
+ """Initialize the data object."""
+ self.api = api
+
+ @Throttle(TIME_BETWEEN_UPDATES)
+ async def async_update(self):
+ """Get the latest version information."""
+ await self.api.get_version()
+
+
+class VersionSensor(SensorEntity):
"""Representation of a Home Assistant version sensor."""
- def __init__(self, haversion, name):
+ def __init__(self, data: VersionData, name: str):
"""Initialize the Version sensor."""
- self.haversion = haversion
+ self.data = data
self._name = name
self._state = None
async def async_update(self):
"""Get the latest version information."""
- await self.haversion.async_update()
+ await self.data.async_update()
@property
def name(self):
@@ -115,27 +138,14 @@ def name(self):
@property
def state(self):
"""Return the state of the sensor."""
- return self.haversion.api.version
+ return self.data.api.version
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return attributes for the sensor."""
- return self.haversion.api.version_data
+ return self.data.api.version_data
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return ICON
-
-
-class VersionData:
- """Get the latest data and update the states."""
-
- def __init__(self, api):
- """Initialize the data object."""
- self.api = api
-
- @Throttle(TIME_BETWEEN_UPDATES)
- async def async_update(self):
- """Get the latest version information."""
- await self.api.get_version()
diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py
index 94a0d5c2f25635..686a71427c33e7 100644
--- a/homeassistant/components/vesync/__init__.py
+++ b/homeassistant/components/vesync/__init__.py
@@ -18,11 +18,12 @@
VS_DISCOVERY,
VS_DISPATCHERS,
VS_FANS,
+ VS_LIGHTS,
VS_MANAGER,
VS_SWITCHES,
)
-PLATFORMS = ["switch", "fan"]
+PLATFORMS = ["switch", "fan", "light"]
_LOGGER = logging.getLogger(__name__)
@@ -85,6 +86,7 @@ async def async_setup_entry(hass, config_entry):
switches = hass.data[DOMAIN][VS_SWITCHES] = []
fans = hass.data[DOMAIN][VS_FANS] = []
+ lights = hass.data[DOMAIN][VS_LIGHTS] = []
hass.data[DOMAIN][VS_DISPATCHERS] = []
@@ -96,15 +98,21 @@ async def async_setup_entry(hass, config_entry):
fans.extend(device_dict[VS_FANS])
hass.async_create_task(forward_setup(config_entry, "fan"))
+ if device_dict[VS_LIGHTS]:
+ lights.extend(device_dict[VS_LIGHTS])
+ hass.async_create_task(forward_setup(config_entry, "light"))
+
async def async_new_device_discovery(service):
"""Discover if new devices should be added."""
manager = hass.data[DOMAIN][VS_MANAGER]
switches = hass.data[DOMAIN][VS_SWITCHES]
fans = hass.data[DOMAIN][VS_FANS]
+ lights = hass.data[DOMAIN][VS_LIGHTS]
dev_dict = await async_process_devices(hass, manager)
switch_devs = dev_dict.get(VS_SWITCHES, [])
fan_devs = dev_dict.get(VS_FANS, [])
+ light_devs = dev_dict.get(VS_LIGHTS, [])
switch_set = set(switch_devs)
new_switches = list(switch_set.difference(switches))
@@ -126,6 +134,16 @@ async def async_new_device_discovery(service):
fans.extend(new_fans)
hass.async_create_task(forward_setup(config_entry, "fan"))
+ light_set = set(light_devs)
+ new_lights = list(light_set.difference(lights))
+ if new_lights and lights:
+ lights.extend(new_lights)
+ async_dispatcher_send(hass, VS_DISCOVERY.format(VS_LIGHTS), new_lights)
+ return
+ if new_lights and not lights:
+ lights.extend(new_lights)
+ hass.async_create_task(forward_setup(config_entry, "light"))
+
hass.services.async_register(
DOMAIN, SERVICE_UPDATE_DEVS, async_new_device_discovery
)
@@ -138,8 +156,8 @@ async def async_unload_entry(hass, entry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py
index 42e3516f085c85..240a5e48287372 100644
--- a/homeassistant/components/vesync/common.py
+++ b/homeassistant/components/vesync/common.py
@@ -3,7 +3,7 @@
from homeassistant.helpers.entity import ToggleEntity
-from .const import VS_FANS, VS_SWITCHES
+from .const import VS_FANS, VS_LIGHTS, VS_SWITCHES
_LOGGER = logging.getLogger(__name__)
@@ -13,6 +13,7 @@ async def async_process_devices(hass, manager):
devices = {}
devices[VS_SWITCHES] = []
devices[VS_FANS] = []
+ devices[VS_LIGHTS] = []
await hass.async_add_executor_job(manager.update)
@@ -28,7 +29,9 @@ async def async_process_devices(hass, manager):
for switch in manager.switches:
if not switch.is_dimmable():
devices[VS_SWITCHES].append(switch)
- _LOGGER.info("%d VeSync standard switches found", len(manager.switches))
+ else:
+ devices[VS_LIGHTS].append(switch)
+ _LOGGER.info("%d VeSync switches found", len(manager.switches))
return devices
diff --git a/homeassistant/components/vesync/const.py b/homeassistant/components/vesync/const.py
index 9923ab94ecffc6..5d9dfc8aa5d1c5 100644
--- a/homeassistant/components/vesync/const.py
+++ b/homeassistant/components/vesync/const.py
@@ -7,4 +7,5 @@
VS_SWITCHES = "switches"
VS_FANS = "fans"
+VS_LIGHTS = "lights"
VS_MANAGER = "manager"
diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py
index 10754007ce6f52..d01d3d4dc5dd3f 100644
--- a/homeassistant/components/vesync/fan.py
+++ b/homeassistant/components/vesync/fan.py
@@ -6,6 +6,7 @@
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import (
+ int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@@ -77,6 +78,11 @@ def percentage(self):
return ranged_value_to_percentage(SPEED_RANGE, current_level)
return None
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return int_states_in_range(SPEED_RANGE)
+
@property
def preset_modes(self):
"""Get the list of available preset modes."""
@@ -85,8 +91,8 @@ def preset_modes(self):
@property
def preset_mode(self):
"""Get the current preset mode."""
- if self.smartfan.mode == FAN_MODE_AUTO:
- return FAN_MODE_AUTO
+ if self.smartfan.mode in (FAN_MODE_AUTO, FAN_MODE_SLEEP):
+ return self.smartfan.mode
return None
@property
@@ -95,7 +101,7 @@ def unique_info(self):
return self.smartfan.uuid
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the fan."""
return {
"mode": self.smartfan.mode,
@@ -130,7 +136,11 @@ def set_preset_mode(self, preset_mode):
if not self.smartfan.is_on:
self.smartfan.turn_on()
- self.smartfan.auto_mode()
+ if preset_mode == FAN_MODE_AUTO:
+ self.smartfan.auto_mode()
+ elif preset_mode == FAN_MODE_SLEEP:
+ self.smartfan.sleep_mode()
+
self.schedule_update_ha_state()
def turn_on(
@@ -144,4 +154,6 @@ def turn_on(
if preset_mode:
self.set_preset_mode(preset_mode)
return
+ if percentage is None:
+ percentage = 50
self.set_percentage(percentage)
diff --git a/homeassistant/components/vesync/light.py b/homeassistant/components/vesync/light.py
new file mode 100644
index 00000000000000..b98c87e5a7f80b
--- /dev/null
+++ b/homeassistant/components/vesync/light.py
@@ -0,0 +1,84 @@
+"""Support for VeSync dimmers."""
+import logging
+
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS,
+ SUPPORT_BRIGHTNESS,
+ LightEntity,
+)
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+from .common import VeSyncDevice
+from .const import DOMAIN, VS_DISCOVERY, VS_DISPATCHERS, VS_LIGHTS
+
+_LOGGER = logging.getLogger(__name__)
+
+DEV_TYPE_TO_HA = {
+ "ESD16": "light",
+ "ESWD16": "light",
+}
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up lights."""
+
+ async def async_discover(devices):
+ """Add new devices to platform."""
+ _async_setup_entities(devices, async_add_entities)
+
+ disp = async_dispatcher_connect(
+ hass, VS_DISCOVERY.format(VS_LIGHTS), async_discover
+ )
+ hass.data[DOMAIN][VS_DISPATCHERS].append(disp)
+
+ _async_setup_entities(hass.data[DOMAIN][VS_LIGHTS], async_add_entities)
+
+
+@callback
+def _async_setup_entities(devices, async_add_entities):
+ """Check if device is online and add entity."""
+ entities = []
+ for dev in devices:
+ if DEV_TYPE_TO_HA.get(dev.device_type) == "light":
+ entities.append(VeSyncDimmerHA(dev))
+ else:
+ _LOGGER.debug(
+ "%s - Unknown device type - %s", dev.device_name, dev.device_type
+ )
+ continue
+
+ async_add_entities(entities, update_before_add=True)
+
+
+class VeSyncDimmerHA(VeSyncDevice, LightEntity):
+ """Representation of a VeSync dimmer."""
+
+ def __init__(self, dimmer):
+ """Initialize the VeSync dimmer device."""
+ super().__init__(dimmer)
+ self.dimmer = dimmer
+
+ def turn_on(self, **kwargs):
+ """Turn the device on."""
+ if ATTR_BRIGHTNESS in kwargs:
+ # get brightness from HA data
+ brightness = int(kwargs[ATTR_BRIGHTNESS])
+ # convert to percent that vesync api expects
+ brightness = round((brightness / 255) * 100)
+ # clamp to 1-100
+ brightness = max(1, min(brightness, 100))
+ self.dimmer.set_brightness(brightness)
+ # Avoid turning device back on if this is just a brightness adjustment
+ if not self.is_on:
+ self.device.turn_on()
+
+ @property
+ def supported_features(self):
+ """Get supported features for this entity."""
+ return SUPPORT_BRIGHTNESS
+
+ @property
+ def brightness(self):
+ """Get dimmer brightness."""
+ return round((int(self.dimmer.brightness) / 100) * 255)
diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json
index 667cb16d12839c..6aa7a5774fd88d 100644
--- a/homeassistant/components/vesync/manifest.json
+++ b/homeassistant/components/vesync/manifest.json
@@ -8,7 +8,7 @@
"@thegardenmonkey"
],
"requirements": [
- "pyvesync==1.2.0"
+ "pyvesync==1.3.1"
],
"config_flow": true
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py
index 0ce4b931def2f1..1d01e340b203eb 100644
--- a/homeassistant/components/vesync/switch.py
+++ b/homeassistant/components/vesync/switch.py
@@ -72,7 +72,7 @@ def __init__(self, plug):
self.smartplug = plug
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
if not hasattr(self.smartplug, "weekly_energy_total"):
return {}
diff --git a/homeassistant/components/vesync/translations/de.json b/homeassistant/components/vesync/translations/de.json
index c52b10c3293db8..ea05a60ff82a37 100644
--- a/homeassistant/components/vesync/translations/de.json
+++ b/homeassistant/components/vesync/translations/de.json
@@ -1,5 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
+ "error": {
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/vesync/translations/he.json b/homeassistant/components/vesync/translations/he.json
new file mode 100644
index 00000000000000..6f4191da70d538
--- /dev/null
+++ b/homeassistant/components/vesync/translations/he.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vesync/translations/hu.json b/homeassistant/components/vesync/translations/hu.json
index 10607c5a136286..91956aff45227d 100644
--- a/homeassistant/components/vesync/translations/hu.json
+++ b/homeassistant/components/vesync/translations/hu.json
@@ -1,5 +1,11 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
+ "error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/vesync/translations/id.json b/homeassistant/components/vesync/translations/id.json
new file mode 100644
index 00000000000000..968f854184991d
--- /dev/null
+++ b/homeassistant/components/vesync/translations/id.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Email"
+ },
+ "title": "Masukkan Nama Pengguna dan Kata Sandi"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vesync/translations/ko.json b/homeassistant/components/vesync/translations/ko.json
index 888bcd66231dd0..a3f1fc1316a91b 100644
--- a/homeassistant/components/vesync/translations/ko.json
+++ b/homeassistant/components/vesync/translations/ko.json
@@ -1,12 +1,18 @@
{
"config": {
+ "abort": {
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
"step": {
"user": {
"data": {
"password": "\ube44\ubc00\ubc88\ud638",
"username": "\uc774\uba54\uc77c"
},
- "title": "\uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud558\uae30"
+ "title": "\uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694"
}
}
}
diff --git a/homeassistant/components/vesync/translations/nl.json b/homeassistant/components/vesync/translations/nl.json
index 0dc21373c14d8e..ab330237afd95b 100644
--- a/homeassistant/components/vesync/translations/nl.json
+++ b/homeassistant/components/vesync/translations/nl.json
@@ -3,11 +3,14 @@
"abort": {
"single_instance_allowed": "Al geconfigureerd. Slecht \u00e9\u00e9n configuratie mogelijk."
},
+ "error": {
+ "invalid_auth": "Ongeldige authenticatie"
+ },
"step": {
"user": {
"data": {
"password": "Wachtwoord",
- "username": "E-mailadres"
+ "username": "E-mail"
},
"title": "Voer gebruikersnaam en wachtwoord in"
}
diff --git a/homeassistant/components/vesync/translations/ru.json b/homeassistant/components/vesync/translations/ru.json
index fd6132565f60a8..b3ac09685be13a 100644
--- a/homeassistant/components/vesync/translations/ru.json
+++ b/homeassistant/components/vesync/translations/ru.json
@@ -4,7 +4,7 @@
"single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e."
},
"error": {
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"step": {
"user": {
diff --git a/homeassistant/components/vesync/translations/tr.json b/homeassistant/components/vesync/translations/tr.json
new file mode 100644
index 00000000000000..8b4f8b6063058a
--- /dev/null
+++ b/homeassistant/components/vesync/translations/tr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "error": {
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "E-posta"
+ },
+ "title": "Kullan\u0131c\u0131 Ad\u0131 ve \u015eifre Girin"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vesync/translations/uk.json b/homeassistant/components/vesync/translations/uk.json
new file mode 100644
index 00000000000000..7f6b3a46b15524
--- /dev/null
+++ b/homeassistant/components/vesync/translations/uk.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438"
+ },
+ "title": "VeSync"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py
index 5de968f5eacc4f..10821859f9a5e7 100644
--- a/homeassistant/components/viaggiatreno/sensor.py
+++ b/homeassistant/components/viaggiatreno/sensor.py
@@ -6,10 +6,9 @@
import async_timeout
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, HTTP_OK, TIME_MINUTES
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -82,7 +81,7 @@ async def async_http_request(hass, uri):
_LOGGER.error("Received non-JSON data from ViaggiaTreno API endpoint")
-class ViaggiaTrenoSensor(Entity):
+class ViaggiaTrenoSensor(SensorEntity):
"""Implementation of a ViaggiaTreno sensor."""
def __init__(self, train_id, station_id, name):
@@ -119,7 +118,7 @@ def unit_of_measurement(self):
return self._unit
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return extra attributes."""
self._attributes[ATTR_ATTRIBUTION] = ATTRIBUTION
return self._attributes
diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py
index 2b1a367215bbfa..88c4ce33a86f6d 100644
--- a/homeassistant/components/vicare/__init__.py
+++ b/homeassistant/components/vicare/__init__.py
@@ -3,6 +3,7 @@
import logging
from PyViCare.PyViCareDevice import Device
+from PyViCare.PyViCareFuelCell import FuelCell
from PyViCare.PyViCareGazBoiler import GazBoiler
from PyViCare.PyViCareHeatPump import HeatPump
import voluptuous as vol
@@ -19,7 +20,7 @@
_LOGGER = logging.getLogger(__name__)
-VICARE_PLATFORMS = ["climate", "sensor", "binary_sensor", "water_heater"]
+PLATFORMS = ["climate", "sensor", "binary_sensor", "water_heater"]
DOMAIN = "vicare"
PYVICARE_ERROR = "error"
@@ -38,6 +39,7 @@ class HeatingType(enum.Enum):
generic = "generic"
gas = "gas"
heatpump = "heatpump"
+ fuelcell = "fuelcell"
CONFIG_SCHEMA = vol.Schema(
@@ -77,6 +79,8 @@ def setup(hass, config):
vicare_api = GazBoiler(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params)
elif heating_type == HeatingType.heatpump:
vicare_api = HeatPump(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params)
+ elif heating_type == HeatingType.fuelcell:
+ vicare_api = FuelCell(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params)
else:
vicare_api = Device(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params)
except AttributeError:
@@ -90,7 +94,7 @@ def setup(hass, config):
hass.data[DOMAIN][VICARE_NAME] = conf[CONF_NAME]
hass.data[DOMAIN][VICARE_HEATING_TYPE] = heating_type
- for platform in VICARE_PLATFORMS:
+ for platform in PLATFORMS:
discovery.load_platform(hass, platform, DOMAIN, {}, config)
return True
diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py
index b7e926b2379128..823c4f1ba1b2fe 100644
--- a/homeassistant/components/vicare/binary_sensor.py
+++ b/homeassistant/components/vicare/binary_sensor.py
@@ -85,6 +85,7 @@
SENSOR_HEATINGROD_LEVEL2,
SENSOR_HEATINGROD_LEVEL3,
],
+ HeatingType.fuelcell: [SENSOR_BURNER_ACTIVE],
}
diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py
index d1accd8ea0a4c8..c819c6593a173b 100644
--- a/homeassistant/components/vicare/climate.py
+++ b/homeassistant/components/vicare/climate.py
@@ -111,7 +111,7 @@ async def async_setup_platform(
{
vol.Required(SERVICE_SET_VICARE_MODE_ATTR_MODE): vol.In(
VICARE_TO_HA_HVAC_HEATING
- ),
+ )
},
"set_vicare_mode",
)
@@ -278,7 +278,7 @@ def set_preset_mode(self, preset_mode):
self._api.activateProgram(vicare_program)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Show Device Attributes."""
return self._attributes
diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py
index a14e00923c2e44..c988b2a4086fb4 100644
--- a/homeassistant/components/vicare/sensor.py
+++ b/homeassistant/components/vicare/sensor.py
@@ -3,18 +3,20 @@
import requests
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_ICON,
CONF_NAME,
CONF_UNIT_OF_MEASUREMENT,
+ DEVICE_CLASS_ENERGY,
DEVICE_CLASS_TEMPERATURE,
ENERGY_KILO_WATT_HOUR,
+ ENERGY_WATT_HOUR,
PERCENTAGE,
TEMP_CELSIUS,
TIME_HOURS,
)
-from homeassistant.helpers.entity import Entity
from . import (
DOMAIN as VICARE_DOMAIN,
@@ -59,6 +61,13 @@
SENSOR_COMPRESSOR_HOURS_LOADCLASS4 = "compressor_hours_loadclass4"
SENSOR_COMPRESSOR_HOURS_LOADCLASS5 = "compressor_hours_loadclass5"
+# fuelcell sensors
+SENSOR_POWER_PRODUCTION_CURRENT = "power_production_current"
+SENSOR_POWER_PRODUCTION_TODAY = "power_production_today"
+SENSOR_POWER_PRODUCTION_THIS_WEEK = "power_production_this_week"
+SENSOR_POWER_PRODUCTION_THIS_MONTH = "power_production_this_month"
+SENSOR_POWER_PRODUCTION_THIS_YEAR = "power_production_this_year"
+
SENSOR_TYPES = {
SENSOR_OUTSIDE_TEMPERATURE: {
CONF_NAME: "Outside Temperature",
@@ -216,6 +225,42 @@
CONF_GETTER: lambda api: api.getReturnTemperature(),
CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
},
+ # fuelcell sensors
+ SENSOR_POWER_PRODUCTION_CURRENT: {
+ CONF_NAME: "Power production current",
+ CONF_ICON: None,
+ CONF_UNIT_OF_MEASUREMENT: ENERGY_WATT_HOUR,
+ CONF_GETTER: lambda api: api.getPowerProductionCurrent(),
+ CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
+ SENSOR_POWER_PRODUCTION_TODAY: {
+ CONF_NAME: "Power production today",
+ CONF_ICON: None,
+ CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ CONF_GETTER: lambda api: api.getPowerProductionToday(),
+ CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
+ SENSOR_POWER_PRODUCTION_THIS_WEEK: {
+ CONF_NAME: "Power production this week",
+ CONF_ICON: None,
+ CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ CONF_GETTER: lambda api: api.getPowerProductionThisWeek(),
+ CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
+ SENSOR_POWER_PRODUCTION_THIS_MONTH: {
+ CONF_NAME: "Power production this month",
+ CONF_ICON: None,
+ CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ CONF_GETTER: lambda api: api.getPowerProductionThisMonth(),
+ CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
+ SENSOR_POWER_PRODUCTION_THIS_YEAR: {
+ CONF_NAME: "Power production this year",
+ CONF_ICON: None,
+ CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+ CONF_GETTER: lambda api: api.getPowerProductionThisYear(),
+ CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
+ },
}
SENSORS_GENERIC = [SENSOR_OUTSIDE_TEMPERATURE, SENSOR_SUPPLY_TEMPERATURE]
@@ -245,6 +290,27 @@
SENSOR_COMPRESSOR_HOURS_LOADCLASS5,
SENSOR_RETURN_TEMPERATURE,
],
+ HeatingType.fuelcell: [
+ # gas
+ SENSOR_BOILER_TEMPERATURE,
+ SENSOR_BURNER_HOURS,
+ SENSOR_BURNER_MODULATION,
+ SENSOR_BURNER_STARTS,
+ SENSOR_DHW_GAS_CONSUMPTION_TODAY,
+ SENSOR_DHW_GAS_CONSUMPTION_THIS_WEEK,
+ SENSOR_DHW_GAS_CONSUMPTION_THIS_MONTH,
+ SENSOR_DHW_GAS_CONSUMPTION_THIS_YEAR,
+ SENSOR_GAS_CONSUMPTION_TODAY,
+ SENSOR_GAS_CONSUMPTION_THIS_WEEK,
+ SENSOR_GAS_CONSUMPTION_THIS_MONTH,
+ SENSOR_GAS_CONSUMPTION_THIS_YEAR,
+ # fuel cell
+ SENSOR_POWER_PRODUCTION_CURRENT,
+ SENSOR_POWER_PRODUCTION_TODAY,
+ SENSOR_POWER_PRODUCTION_THIS_WEEK,
+ SENSOR_POWER_PRODUCTION_THIS_MONTH,
+ SENSOR_POWER_PRODUCTION_THIS_YEAR,
+ ],
}
@@ -269,7 +335,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
)
-class ViCareSensor(Entity):
+class ViCareSensor(SensorEntity):
"""Representation of a ViCare sensor."""
def __init__(self, name, api, sensor_type):
diff --git a/homeassistant/components/vilfo/config_flow.py b/homeassistant/components/vilfo/config_flow.py
index 2b9df3d919565e..d5646c8caf3e53 100644
--- a/homeassistant/components/vilfo/config_flow.py
+++ b/homeassistant/components/vilfo/config_flow.py
@@ -13,8 +13,7 @@
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_ID, CONF_MAC
-from .const import DOMAIN # pylint:disable=unused-import
-from .const import ROUTER_DEFAULT_HOST
+from .const import DOMAIN, ROUTER_DEFAULT_HOST
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/vilfo/const.py b/homeassistant/components/vilfo/const.py
index 74eb813bcc5699..d47e738a858e5f 100644
--- a/homeassistant/components/vilfo/const.py
+++ b/homeassistant/components/vilfo/const.py
@@ -1,13 +1,16 @@
"""Constants for the Vilfo Router integration."""
-from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ ATTR_ICON,
+ DEVICE_CLASS_TIMESTAMP,
+ PERCENTAGE,
+)
DOMAIN = "vilfo"
ATTR_API_DATA_FIELD = "api_data_field"
ATTR_API_DATA_FIELD_LOAD = "load"
ATTR_API_DATA_FIELD_BOOT_TIME = "boot_time"
-ATTR_DEVICE_CLASS = "device_class"
-ATTR_ICON = "icon"
ATTR_LABEL = "label"
ATTR_LOAD = "load"
ATTR_UNIT = "unit"
diff --git a/homeassistant/components/vilfo/sensor.py b/homeassistant/components/vilfo/sensor.py
index e2909647c2d6ee..90527c60458f42 100644
--- a/homeassistant/components/vilfo/sensor.py
+++ b/homeassistant/components/vilfo/sensor.py
@@ -1,10 +1,10 @@
"""Support for Vilfo Router sensors."""
-from homeassistant.helpers.entity import Entity
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.const import ATTR_ICON
from .const import (
ATTR_API_DATA_FIELD,
ATTR_DEVICE_CLASS,
- ATTR_ICON,
ATTR_LABEL,
ATTR_UNIT,
DOMAIN,
@@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors, True)
-class VilfoRouterSensor(Entity):
+class VilfoRouterSensor(SensorEntity):
"""Define a Vilfo Router Sensor."""
def __init__(self, sensor_type, api):
diff --git a/homeassistant/components/vilfo/translations/de.json b/homeassistant/components/vilfo/translations/de.json
index 4880154b58e42c..8f20c074ff4913 100644
--- a/homeassistant/components/vilfo/translations/de.json
+++ b/homeassistant/components/vilfo/translations/de.json
@@ -4,9 +4,9 @@
"already_configured": "Dieser Vilfo Router ist bereits konfiguriert."
},
"error": {
- "cannot_connect": "Verbindung nicht m\u00f6glich. Bitte \u00fcberpr\u00fcfen Sie die von Ihnen angegebenen Informationen und versuchen Sie es erneut.",
- "invalid_auth": "Ung\u00fcltige Authentifizierung. Bitte \u00fcberpr\u00fcfen Sie den Zugriffstoken und versuchen Sie es erneut.",
- "unknown": "Beim Einrichten der Integration ist ein unerwarteter Fehler aufgetreten."
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung. Bitte \u00fcberpr\u00fcfe den Zugriffstoken und versuche es erneut.",
+ "unknown": "Unerwarteter Fehler"
},
"step": {
"user": {
diff --git a/homeassistant/components/vilfo/translations/hu.json b/homeassistant/components/vilfo/translations/hu.json
index a75149507fcc80..34db9cf7cc9934 100644
--- a/homeassistant/components/vilfo/translations/hu.json
+++ b/homeassistant/components/vilfo/translations/hu.json
@@ -1,5 +1,13 @@
{
"config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/vilfo/translations/id.json b/homeassistant/components/vilfo/translations/id.json
new file mode 100644
index 00000000000000..3871670a1cd92c
--- /dev/null
+++ b/homeassistant/components/vilfo/translations/id.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Token Akses",
+ "host": "Host"
+ },
+ "description": "Siapkan integrasi Router Vilfo. Anda memerlukan nama host/IP Router Vilfo dan token akses API. Untuk informasi lebih lanjut tentang integrasi ini dan cara mendapatkan data tersebut, kunjungi: https://www.home-assistant.io/integrations/vilfo",
+ "title": "Hubungkan ke Router Vilfo"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/translations/ko.json b/homeassistant/components/vilfo/translations/ko.json
index 70a315ae703095..980ee36d7a5324 100644
--- a/homeassistant/components/vilfo/translations/ko.json
+++ b/homeassistant/components/vilfo/translations/ko.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 Vilfo \ub77c\uc6b0\ud130\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uc785\ub825\ud558\uc2e0 \ub0b4\uc6a9\uc744 \ud655\uc778\ud558\uc2e0 \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
- "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \ud655\uc778\ud558\uc2e0 \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
- "unknown": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud558\ub294 \uc911 \uc608\uae30\uce58 \uc54a\uc740 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4."
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
@@ -14,7 +14,7 @@
"access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070",
"host": "\ud638\uc2a4\ud2b8"
},
- "description": "Vilfo \ub77c\uc6b0\ud130 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. Vilfo \ub77c\uc6b0\ud130 \ud638\uc2a4\ud2b8 \uc774\ub984 / IP \uc640 API \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \ucd94\uac00 \uc815\ubcf4\uc640 \uc138\ubd80 \uc0ac\ud56d\uc740 https://www.home-assistant.io/integrations/vilfo \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694",
+ "description": "Vilfo \ub77c\uc6b0\ud130 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. Vilfo \ub77c\uc6b0\ud130 \ud638\uc2a4\ud2b8 \uc774\ub984/IP \uc640 API \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \ucd94\uac00 \uc815\ubcf4\uc640 \uc138\ubd80 \uc0ac\ud56d\uc740 https://www.home-assistant.io/integrations/vilfo \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694",
"title": "Vilfo \ub77c\uc6b0\ud130\uc5d0 \uc5f0\uacb0\ud558\uae30"
}
}
diff --git a/homeassistant/components/vilfo/translations/nl.json b/homeassistant/components/vilfo/translations/nl.json
index d4b117e2b70a15..304871cc145fb7 100644
--- a/homeassistant/components/vilfo/translations/nl.json
+++ b/homeassistant/components/vilfo/translations/nl.json
@@ -1,18 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "Deze Vilfo Router is al geconfigureerd."
+ "already_configured": "Apparaat is al geconfigureerd"
},
"error": {
- "cannot_connect": "Kon niet verbinden. Controleer de door u verstrekte informatie en probeer het opnieuw.",
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
- "unknown": "Er is een onverwachte fout opgetreden tijdens het instellen van de integratie."
+ "unknown": "Onverwachte fout"
},
"step": {
"user": {
"data": {
- "access_token": "Toegangstoken voor de Vilfo Router API",
- "host": "Router hostnaam of IP-adres"
+ "access_token": "Toegangstoken",
+ "host": "Host"
},
"description": "Stel de Vilfo Router-integratie in. U heeft de hostnaam/IP van uw Vilfo Router en een API-toegangstoken nodig. Voor meer informatie over deze integratie en hoe u die details kunt verkrijgen, gaat u naar: https://www.home-assistant.io/integrations/vilfo",
"title": "Maak verbinding met de Vilfo Router"
diff --git a/homeassistant/components/vilfo/translations/ru.json b/homeassistant/components/vilfo/translations/ru.json
index 8e61be904004e8..62ec2fe5daeff9 100644
--- a/homeassistant/components/vilfo/translations/ru.json
+++ b/homeassistant/components/vilfo/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
diff --git a/homeassistant/components/vilfo/translations/tr.json b/homeassistant/components/vilfo/translations/tr.json
new file mode 100644
index 00000000000000..dc66041e35a2aa
--- /dev/null
+++ b/homeassistant/components/vilfo/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Eri\u015fim Belirteci",
+ "host": "Ana Bilgisayar"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/translations/uk.json b/homeassistant/components/vilfo/translations/uk.json
new file mode 100644
index 00000000000000..1a93176f290740
--- /dev/null
+++ b/homeassistant/components/vilfo/translations/uk.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443",
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 Vilfo. \u0412\u043a\u0430\u0436\u0456\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u0440\u043e\u0443\u0442\u0435\u0440\u0430 \u0456 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 API. \u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0446\u0456\u0454\u0457 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457, \u0432\u0456\u0434\u0432\u0456\u0434\u0430\u0439\u0442\u0435 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442: https://www.home-assistant.io/integrations/vilfo.",
+ "title": "Vilfo Router"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/translations/zh-Hant.json b/homeassistant/components/vilfo/translations/zh-Hant.json
index b266e25b39cb3f..88180f9bacf822 100644
--- a/homeassistant/components/vilfo/translations/zh-Hant.json
+++ b/homeassistant/components/vilfo/translations/zh-Hant.json
@@ -11,10 +11,10 @@
"step": {
"user": {
"data": {
- "access_token": "\u5b58\u53d6\u5bc6\u9470",
+ "access_token": "\u5b58\u53d6\u6b0a\u6756",
"host": "\u4e3b\u6a5f\u7aef"
},
- "description": "\u8a2d\u5b9a Vilfo \u8def\u7531\u5668\u6574\u5408\u3002\u9700\u8981\u8f38\u5165 Vilfo \u8def\u7531\u5668\u4e3b\u6a5f\u540d\u7a31/IP \u4f4d\u5740\u3001API \u5b58\u53d6\u5bc6\u9470\u3002\u5176\u4ed6\u6574\u5408\u76f8\u95dc\u8cc7\u8a0a\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/vilfo",
+ "description": "\u8a2d\u5b9a Vilfo \u8def\u7531\u5668\u6574\u5408\u3002\u9700\u8981\u8f38\u5165 Vilfo \u8def\u7531\u5668\u4e3b\u6a5f\u540d\u7a31/IP \u4f4d\u5740\u3001API \u5b58\u53d6\u6b0a\u6756\u3002\u5176\u4ed6\u6574\u5408\u76f8\u95dc\u8cc7\u8a0a\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/vilfo",
"title": "\u9023\u7dda\u81f3 Vilfo \u8def\u7531\u5668"
}
}
diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py
index a7a9c404f74d9c..3719ada27aef5b 100644
--- a/homeassistant/components/vizio/__init__.py
+++ b/homeassistant/components/vizio/__init__.py
@@ -1,8 +1,10 @@
"""The vizio component."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
import logging
-from typing import Any, Dict, List
+from typing import Any
from pyvizio.const import APPS
from pyvizio.util import gen_apps_list_from_url
@@ -116,7 +118,7 @@ def __init__(self, hass: HomeAssistantType) -> None:
)
self.data = APPS
- async def _async_update_data(self) -> List[Dict[str, Any]]:
+ async def _async_update_data(self) -> list[dict[str, Any]]:
"""Update data via library."""
data = await gen_apps_list_from_url(session=async_get_clientsession(self.hass))
if not data:
diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py
index 40f71adda12718..2c3c365b15a306 100644
--- a/homeassistant/components/vizio/config_flow.py
+++ b/homeassistant/components/vizio/config_flow.py
@@ -1,8 +1,10 @@
"""Config flow for Vizio."""
+from __future__ import annotations
+
import copy
import logging
import socket
-from typing import Any, Dict, Optional
+from typing import Any
from pyvizio import VizioAsync, async_guess_device_type
from pyvizio.const import APP_HOME
@@ -48,7 +50,7 @@
_LOGGER = logging.getLogger(__name__)
-def _get_config_schema(input_dict: Dict[str, Any] = None) -> vol.Schema:
+def _get_config_schema(input_dict: dict[str, Any] = None) -> vol.Schema:
"""
Return schema defaults for init step based on user input/config dict.
@@ -76,7 +78,7 @@ def _get_config_schema(input_dict: Dict[str, Any] = None) -> vol.Schema:
)
-def _get_pairing_schema(input_dict: Dict[str, Any] = None) -> vol.Schema:
+def _get_pairing_schema(input_dict: dict[str, Any] = None) -> vol.Schema:
"""
Return schema defaults for pairing data based on user input.
@@ -108,8 +110,8 @@ def __init__(self, config_entry: ConfigEntry) -> None:
self.config_entry = config_entry
async def async_step_init(
- self, user_input: Dict[str, Any] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] = None
+ ) -> dict[str, Any]:
"""Manage the vizio options."""
if user_input is not None:
if user_input.get(CONF_APPS_TO_INCLUDE_OR_EXCLUDE):
@@ -191,7 +193,7 @@ def __init__(self) -> None:
self._data = None
self._apps = {}
- async def _create_entry(self, input_dict: Dict[str, Any]) -> Dict[str, Any]:
+ async def _create_entry(self, input_dict: dict[str, Any]) -> dict[str, Any]:
"""Create vizio config entry."""
# Remove extra keys that will not be used by entry setup
input_dict.pop(CONF_APPS_TO_INCLUDE_OR_EXCLUDE, None)
@@ -203,10 +205,9 @@ async def _create_entry(self, input_dict: Dict[str, Any]) -> Dict[str, Any]:
return self.async_create_entry(title=input_dict[CONF_NAME], data=input_dict)
async def async_step_user(
- self, user_input: Dict[str, Any] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] = None
+ ) -> dict[str, Any]:
"""Handle a flow initialized by the user."""
- assert self.hass
errors = {}
if user_input is not None:
@@ -232,7 +233,6 @@ async def async_step_user(
errors[CONF_HOST] = "existing_config_entry_found"
if not errors:
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
if self._must_show_form and self.context["source"] == SOURCE_ZEROCONF:
# Discovery should always display the config form before trying to
# create entry so that user can update default config options
@@ -251,7 +251,6 @@ async def async_step_user(
if not errors:
return await self._create_entry(user_input)
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
elif self._must_show_form and self.context["source"] == SOURCE_IMPORT:
# Import should always display the config form if CONF_ACCESS_TOKEN
# wasn't included but is needed so that the user can choose to update
@@ -271,16 +270,16 @@ async def async_step_user(
schema = self._user_schema or _get_config_schema()
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
if errors and self.context["source"] == SOURCE_IMPORT:
# Log an error message if import config flow fails since otherwise failure is silent
_LOGGER.error(
- "configuration.yaml import failure: %s", ", ".join(errors.values())
+ "Importing from configuration.yaml failed: %s",
+ ", ".join(errors.values()),
)
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
- async def async_step_import(self, import_config: Dict[str, Any]) -> Dict[str, Any]:
+ async def async_step_import(self, import_config: dict[str, Any]) -> dict[str, Any]:
"""Import a config entry from configuration.yaml."""
# Check if new config entry matches any existing config entries
for entry in self.hass.config_entries.async_entries(DOMAIN):
@@ -343,11 +342,9 @@ async def async_step_import(self, import_config: Dict[str, Any]) -> Dict[str, An
return await self.async_step_user(user_input=import_config)
async def async_step_zeroconf(
- self, discovery_info: Optional[DiscoveryInfoType] = None
- ) -> Dict[str, Any]:
+ self, discovery_info: DiscoveryInfoType | None = None
+ ) -> dict[str, Any]:
"""Handle zeroconf discovery."""
- assert self.hass
-
# If host already has port, no need to add it again
if ":" not in discovery_info[CONF_HOST]:
discovery_info[
@@ -382,8 +379,8 @@ async def async_step_zeroconf(
return await self.async_step_user(user_input=discovery_info)
async def async_step_pair_tv(
- self, user_input: Dict[str, Any] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] = None
+ ) -> dict[str, Any]:
"""
Start pairing process for TV.
@@ -432,7 +429,6 @@ async def async_step_pair_tv(
self._data[CONF_ACCESS_TOKEN] = pair_data.auth_token
self._must_show_form = True
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
if self.context["source"] == SOURCE_IMPORT:
# If user is pairing via config import, show different message
return await self.async_step_pairing_complete_import()
@@ -449,7 +445,7 @@ async def async_step_pair_tv(
errors=errors,
)
- async def _pairing_complete(self, step_id: str) -> Dict[str, Any]:
+ async def _pairing_complete(self, step_id: str) -> dict[str, Any]:
"""Handle config flow completion."""
if not self._must_show_form:
return await self._create_entry(self._data)
@@ -462,8 +458,8 @@ async def _pairing_complete(self, step_id: str) -> Dict[str, Any]:
)
async def async_step_pairing_complete(
- self, user_input: Dict[str, Any] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] = None
+ ) -> dict[str, Any]:
"""
Complete non-import sourced config flow.
@@ -472,8 +468,8 @@ async def async_step_pairing_complete(
return await self._pairing_complete("pairing_complete")
async def async_step_pairing_complete_import(
- self, user_input: Dict[str, Any] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] = None
+ ) -> dict[str, Any]:
"""
Complete import sourced config flow.
diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py
index 4c06c89692afab..fc955d48158b9e 100644
--- a/homeassistant/components/vizio/media_player.py
+++ b/homeassistant/components/vizio/media_player.py
@@ -1,7 +1,9 @@
"""Vizio SmartCast Device support."""
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Any, Callable, Dict, List, Optional, Union
+from typing import Any, Callable
from pyvizio import VizioAsync
from pyvizio.api.apps import find_app_name
@@ -25,7 +27,6 @@
STATE_ON,
)
from homeassistant.core import callback
-from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import entity_platform
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import (
@@ -65,7 +66,7 @@
async def async_setup_entry(
hass: HomeAssistantType,
config_entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up a Vizio media player entry."""
host = config_entry.data[CONF_HOST]
@@ -115,10 +116,6 @@ async def async_setup_entry(
timeout=DEFAULT_TIMEOUT,
)
- if not await device.can_connect_with_auth_check():
- _LOGGER.warning("Failed to connect to %s", host)
- raise PlatformNotReady
-
apps_coordinator = hass.data[DOMAIN].get(CONF_APPS)
entity = VizioDevice(config_entry, device, name, device_class, apps_coordinator)
@@ -171,7 +168,7 @@ def __init__(
self._model = None
self._sw_version = None
- def _apps_list(self, apps: List[str]) -> List[str]:
+ def _apps_list(self, apps: list[str]) -> list[str]:
"""Return process apps list based on configured filters."""
if self._conf_apps.get(CONF_INCLUDE):
return [app for app in apps if app in self._conf_apps[CONF_INCLUDE]]
@@ -183,12 +180,6 @@ def _apps_list(self, apps: List[str]) -> List[str]:
async def async_update(self) -> None:
"""Retrieve latest state of the device."""
- if not self._model:
- self._model = await self._device.get_model_name(log_api_exception=False)
-
- if not self._sw_version:
- self._sw_version = await self._device.get_version(log_api_exception=False)
-
is_on = await self._device.get_power_state(log_api_exception=False)
if is_on is None:
@@ -205,6 +196,12 @@ async def async_update(self) -> None:
)
self._available = True
+ if not self._model:
+ self._model = await self._device.get_model_name(log_api_exception=False)
+
+ if not self._sw_version:
+ self._sw_version = await self._device.get_version(log_api_exception=False)
+
if not is_on:
self._state = STATE_OFF
self._volume_level = None
@@ -279,7 +276,7 @@ async def async_update(self) -> None:
if self._current_app == NO_APP_RUNNING:
self._current_app = None
- def _get_additional_app_names(self) -> List[Dict[str, Any]]:
+ def _get_additional_app_names(self) -> list[dict[str, Any]]:
"""Return list of additional apps that were included in configuration.yaml."""
return [
additional_app["name"] for additional_app in self._additional_app_configs
@@ -301,7 +298,7 @@ async def _async_update_options(self, config_entry: ConfigEntry) -> None:
self._conf_apps.update(config_entry.options.get(CONF_APPS, {}))
async def async_update_setting(
- self, setting_type: str, setting_name: str, new_value: Union[int, str]
+ self, setting_type: str, setting_name: str, new_value: int | str
) -> None:
"""Update a setting when update_setting service is called."""
await self._device.set_setting(
@@ -345,7 +342,7 @@ def available(self) -> bool:
return self._available
@property
- def state(self) -> Optional[str]:
+ def state(self) -> str | None:
"""Return the state of the device."""
return self._state
@@ -360,7 +357,7 @@ def icon(self) -> str:
return self._icon
@property
- def volume_level(self) -> Optional[float]:
+ def volume_level(self) -> float | None:
"""Return the volume level of the device."""
return self._volume_level
@@ -370,7 +367,7 @@ def is_volume_muted(self):
return self._is_volume_muted
@property
- def source(self) -> Optional[str]:
+ def source(self) -> str | None:
"""Return current input of the device."""
if self._current_app is not None and self._current_input in INPUT_APPS:
return self._current_app
@@ -378,7 +375,7 @@ def source(self) -> Optional[str]:
return self._current_input
@property
- def source_list(self) -> List[str]:
+ def source_list(self) -> list[str]:
"""Return list of available inputs of the device."""
# If Smartcast app is in input list, and the app list has been retrieved,
# show the combination with , otherwise just return inputs
@@ -400,7 +397,7 @@ def source_list(self) -> List[str]:
return self._available_inputs
@property
- def app_id(self) -> Optional[str]:
+ def app_id(self) -> str | None:
"""Return the ID of the current app if it is unknown by pyvizio."""
if self._current_app_config and self.app_name == UNKNOWN_APP:
return {
@@ -412,7 +409,7 @@ def app_id(self) -> Optional[str]:
return None
@property
- def app_name(self) -> Optional[str]:
+ def app_name(self) -> str | None:
"""Return the friendly name of the current app."""
return self._current_app
@@ -427,7 +424,7 @@ def unique_id(self) -> str:
return self._config_entry.unique_id
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device registry information."""
return {
"identifiers": {(DOMAIN, self._config_entry.unique_id)},
@@ -443,12 +440,12 @@ def device_class(self) -> str:
return self._device_class
@property
- def sound_mode(self) -> Optional[str]:
+ def sound_mode(self) -> str | None:
"""Name of the current sound mode."""
return self._current_sound_mode
@property
- def sound_mode_list(self) -> Optional[List[str]]:
+ def sound_mode_list(self) -> list[str] | None:
"""List of available sound modes."""
return self._available_sound_modes
diff --git a/homeassistant/components/vizio/services.yaml b/homeassistant/components/vizio/services.yaml
index 50bde6cab788dd..7a2ea859b7dab5 100644
--- a/homeassistant/components/vizio/services.yaml
+++ b/homeassistant/components/vizio/services.yaml
@@ -1,15 +1,33 @@
update_setting:
- description: Update the value of a setting on a particular Vizio media player device.
+ name: Update setting
+ description: Update the value of a setting on a Vizio media player device.
+ target:
+ entity:
+ integration: vizio
+ domain: media_player
fields:
- entity_id:
- description: Name of an entity to send command to.
- example: "media_player.vizio_smartcast"
setting_type:
- description: The type of setting to be changed. Available types are listed in the `setting_types` property.
+ name: Setting type
+ description:
+ The type of setting to be changed. Available types are listed in the
+ 'setting_types' property.
+ required: true
example: "audio"
+ selector:
+ text:
setting_name:
- description: The name of the setting to be changed. Available settings for a given setting_type are listed in the `_settings` property.
+ name: Setting name
+ description:
+ The name of the setting to be changed. Available settings for a given
+ setting_type are listed in the '_settings' property.
+ required: true
example: "eq"
+ selector:
+ text:
new_value:
- description: The new value for the setting
+ name: New value
+ description: The new value for the setting.
+ required: true
example: "Music"
+ selector:
+ text:
diff --git a/homeassistant/components/vizio/translations/de.json b/homeassistant/components/vizio/translations/de.json
index ddb68ec09faa98..913317c88d7e37 100644
--- a/homeassistant/components/vizio/translations/de.json
+++ b/homeassistant/components/vizio/translations/de.json
@@ -1,26 +1,28 @@
{
"config": {
"abort": {
- "cannot_connect": "Verbindungsfehler",
+ "already_configured_device": "Ger\u00e4t ist bereits konfiguriert",
+ "cannot_connect": "Verbindung fehlgeschlagen",
"updated_entry": "Dieser Eintrag wurde bereits eingerichtet, aber der Name, die Apps und / oder die in der Konfiguration definierten Optionen stimmen nicht mit der zuvor importierten Konfiguration \u00fcberein, sodass der Konfigurationseintrag entsprechend aktualisiert wurde."
},
"error": {
- "cannot_connect": "Verbindung fehlgeschlagen"
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "complete_pairing_failed": "Das Pairing konnte nicht abgeschlossen werden. Vergewissere dich, dass der eingegebene PIN korrekt ist und dass der Fernseher noch mit Strom versorgt wird und mit dem Netzwerk verbunden ist, bevor du es erneut versuchst."
},
"step": {
"pair_tv": {
"data": {
- "pin": "PIN"
+ "pin": "PIN-Code"
},
"description": "Ihr Fernseher sollte einen Code anzeigen. Geben Sie diesen Code in das Formular ein und fahren Sie mit dem n\u00e4chsten Schritt fort, um die Kopplung abzuschlie\u00dfen.",
"title": "Schlie\u00dfen Sie den Pairing-Prozess ab"
},
"pairing_complete": {
- "description": "Ihr VIZIO SmartCast-Ger\u00e4t ist jetzt mit Home Assistant verbunden.",
+ "description": "Dein Richten Sie das VIZIO SmartCast-Ger\u00e4t ein ist jetzt mit Home Assistant verbunden.",
"title": "Kopplung abgeschlossen"
},
"pairing_complete_import": {
- "description": "Ihr VIZIO SmartCast-Fernseher ist jetzt mit Home Assistant verbunden. \n\n Ihr Zugriffstoken ist '**{access_token}**'.",
+ "description": "Dein Richten Sie das VIZIO SmartCast-Ger\u00e4t ein ist jetzt mit Home Assistant verbunden.\n\nDein Zugangstoken ist '**{access_token}**'.",
"title": "Kopplung abgeschlossen"
},
"user": {
@@ -30,7 +32,7 @@
"host": "Host",
"name": "Name"
},
- "description": "Ein Zugriffstoken wird nur f\u00fcr Fernsehger\u00e4te ben\u00f6tigt. Wenn Sie ein Fernsehger\u00e4t konfigurieren und noch kein Zugriffstoken haben, lassen Sie es leer, um einen Pairing-Vorgang durchzuf\u00fchren.",
+ "description": "Ein Zugangstoken wird nur f\u00fcr Fernsehger\u00e4te ben\u00f6tigt. Wenn du ein Fernsehger\u00e4t konfigurierst und noch kein Zugangstoken hast, lass es leer, um einen Pairing-Vorgang durchzuf\u00fchren.",
"title": "Richten Sie das VIZIO SmartCast-Ger\u00e4t ein"
}
}
@@ -44,7 +46,7 @@
"volume_step": "Lautst\u00e4rken-Schrittgr\u00f6\u00dfe"
},
"description": "Wenn Sie \u00fcber ein Smart-TV-Ger\u00e4t verf\u00fcgen, k\u00f6nnen Sie Ihre Quellliste optional filtern, indem Sie ausw\u00e4hlen, welche Apps in Ihre Quellliste aufgenommen oder ausgeschlossen werden sollen.",
- "title": "Aktualisieren Sie die VIZIO SmartCast-Optionen"
+ "title": "Aktualisiere die Richten Sie das VIZIO SmartCast-Ger\u00e4t ein-Optionen"
}
}
}
diff --git a/homeassistant/components/vizio/translations/fr.json b/homeassistant/components/vizio/translations/fr.json
index 89c46fd5959681..5fc9158c80345a 100644
--- a/homeassistant/components/vizio/translations/fr.json
+++ b/homeassistant/components/vizio/translations/fr.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
+ "cannot_connect": "\u00c9chec de la connexion ",
"updated_entry": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e mais le nom et/ou les options d\u00e9finis dans la configuration ne correspondent pas \u00e0 la configuration pr\u00e9c\u00e9demment import\u00e9e, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence."
},
"error": {
diff --git a/homeassistant/components/vizio/translations/hu.json b/homeassistant/components/vizio/translations/hu.json
index 7401b751d274d7..6f0962509f5fa7 100644
--- a/homeassistant/components/vizio/translations/hu.json
+++ b/homeassistant/components/vizio/translations/hu.json
@@ -2,9 +2,21 @@
"config": {
"abort": {
"already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
"updated_entry": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban defini\u00e1lt n\u00e9v, appok \u00e9s/vagy be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt konfigur\u00e1ci\u00f3val, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt."
},
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
"step": {
+ "pair_tv": {
+ "data": {
+ "pin": "PIN-k\u00f3d"
+ }
+ },
+ "pairing_complete": {
+ "description": "A VIZIO SmartCast Eszk\u00f6z mostant\u00f3l csatlakozik a Home Assistant-hoz."
+ },
"user": {
"data": {
"access_token": "Hozz\u00e1f\u00e9r\u00e9si token",
@@ -12,7 +24,7 @@
"host": "Hoszt",
"name": "N\u00e9v"
},
- "title": "A Vizio SmartCast Client be\u00e1ll\u00edt\u00e1sa"
+ "title": "VIZIO SmartCast Eszk\u00f6z"
}
}
},
@@ -22,7 +34,7 @@
"data": {
"volume_step": "Hanger\u0151 l\u00e9p\u00e9s nagys\u00e1ga"
},
- "title": "Friss\u00edtse a Vizo SmartCast be\u00e1ll\u00edt\u00e1sokat"
+ "title": "VIZIO SmartCast Eszk\u00f6z be\u00e1ll\u00edt\u00e1sok friss\u00edt\u00e9se"
}
}
}
diff --git a/homeassistant/components/vizio/translations/id.json b/homeassistant/components/vizio/translations/id.json
new file mode 100644
index 00000000000000..19f9b41449c97a
--- /dev/null
+++ b/homeassistant/components/vizio/translations/id.json
@@ -0,0 +1,54 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_device": "Perangkat sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung",
+ "updated_entry": "Entri ini telah disiapkan tetapi nama, aplikasi, dan/atau opsi yang ditentukan dalam konfigurasi tidak cocok dengan konfigurasi yang diimpor sebelumnya, oleh karena itu entri konfigurasi telah diperbarui."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "complete_pairing_failed": "Tidak dapat menyelesaikan pemasangan. Pastikan PIN yang diberikan benar dan TV masih menyala dan terhubung ke jaringan sebelum mengirim ulang.",
+ "existing_config_entry_found": "Entri konfigurasi Perangkat VIZIO SmartCast yang ada dengan nomor seri yang sama telah dikonfigurasi. Anda harus menghapus entri yang ada untuk mengonfigurasi entri ini."
+ },
+ "step": {
+ "pair_tv": {
+ "data": {
+ "pin": "Kode PIN"
+ },
+ "description": "TV Anda harus menampilkan kode. Masukkan kode tersebut ke dalam formulir dan kemudian lanjutkan ke langkah berikutnya untuk menyelesaikan pemasangan.",
+ "title": "Selesaikan Proses Pemasangan"
+ },
+ "pairing_complete": {
+ "description": "Perangkat VIZIO SmartCast sekarang terhubung ke Home Assistant.",
+ "title": "Pemasangan Selesai"
+ },
+ "pairing_complete_import": {
+ "description": "Perangkat VIZIO SmartCast sekarang terhubung ke Home Assistant. \n\nToken Akses adalah '**{access_token}**'.",
+ "title": "Pemasangan Selesai"
+ },
+ "user": {
+ "data": {
+ "access_token": "Token Akses",
+ "device_class": "Jenis Perangkat",
+ "host": "Host",
+ "name": "Nama"
+ },
+ "description": "Token Akses hanya diperlukan untuk TV. Jika Anda mengkonfigurasi TV dan belum memiliki Token Akses , biarkan kosong untuk melakukan proses pemasangan.",
+ "title": "Perangkat VIZIO SmartCast"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "apps_to_include_or_exclude": "Aplikasi untuk Disertakan atau Dikecualikan",
+ "include_or_exclude": "Sertakan atau Kecualikan Aplikasi?",
+ "volume_step": "Langkah Volume"
+ },
+ "description": "Jika memiliki Smart TV, Anda dapat memfilter daftar sumber secara opsional dengan memilih aplikasi mana yang akan disertakan atau dikecualikan dalam daftar sumber Anda.",
+ "title": "Perbarui Opsi Perangkat VIZIO SmartCast"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vizio/translations/ko.json b/homeassistant/components/vizio/translations/ko.json
index ef10cb1f4fc857..cc86e020a1c844 100644
--- a/homeassistant/components/vizio/translations/ko.json
+++ b/homeassistant/components/vizio/translations/ko.json
@@ -2,27 +2,28 @@
"config": {
"abort": {
"already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"updated_entry": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc9c0\ub9cc \uad6c\uc131\uc5d0 \uc815\uc758\ub41c \uc774\ub984, \uc571 \ud639\uc740 \uc635\uc158\uc774 \uc774\uc804\uc5d0 \uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uad6c\uc131 \ud56d\ubaa9\uc774 \uadf8\uc5d0 \ub530\ub77c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
},
"error": {
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
- "complete_pairing_failed": "\ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc81c\ucd9c\ud558\uae30 \uc804\uc5d0 \uc785\ub825\ud55c PIN \uc774 \uc62c\ubc14\ub978\uc9c0, TV \uc758 \uc804\uc6d0\uc774 \ucf1c\uc838 \uc788\uace0 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
- "existing_config_entry_found": "\uc77c\ub828 \ubc88\ud638\uac00 \ub3d9\uc77c\ud55c \uae30\uc874 VIZIO SmartCast \uae30\uae30 \uad6c\uc131 \ud56d\ubaa9\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc774 \ud56d\ubaa9\uc744 \uad6c\uc131\ud558\ub824\uba74 \uae30\uc874 \ud56d\ubaa9\uc744 \uc0ad\uc81c\ud574\uc57c\ud569\ub2c8\ub2e4."
+ "complete_pairing_failed": "\ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud558\uae30 \uc804\uc5d0 \uc785\ub825\ud55c PIN\uc774 \uc62c\ubc14\ub978\uc9c0, TV\uc758 \uc804\uc6d0\uc774 \ucf1c\uc838 \uc788\uace0 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
+ "existing_config_entry_found": "\uc2dc\ub9ac\uc5bc \ubc88\ud638\uac00 \ub3d9\uc77c\ud55c VIZIO SmartCast \uae30\uae30 \uad6c\uc131 \ud56d\ubaa9\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4. \uc774 \ud56d\ubaa9\uc744 \uad6c\uc131\ud558\ub824\uba74 \uae30\uc874 \ud56d\ubaa9\uc744 \uc0ad\uc81c\ud574\uc57c \ud569\ub2c8\ub2e4."
},
"step": {
"pair_tv": {
"data": {
- "pin": "PIN"
+ "pin": "PIN \ucf54\ub4dc"
},
- "description": "TV \uc5d0 \ucf54\ub4dc\uac00 \ud45c\uc2dc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ucf54\ub4dc\ub97c \uc785\ub825\ub780\uc5d0 \uc785\ub825\ud55c \ud6c4 \ub2e4\uc74c \ub2e8\uacc4\ub97c \uacc4\uc18d\ud558\uc5ec \ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud574\uc8fc\uc138\uc694.",
+ "description": "TV\uc5d0 \ucf54\ub4dc\uac00 \ud45c\uc2dc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ucf54\ub4dc\ub97c \uc785\ub825\ub780\uc5d0 \uc785\ub825\ud55c \ud6c4 \ub2e4\uc74c \ub2e8\uacc4\ub97c \uacc4\uc18d\ud558\uc5ec \ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud574\uc8fc\uc138\uc694.",
"title": "\ud398\uc5b4\ub9c1 \uacfc\uc815 \ub05d\ub0b4\uae30"
},
"pairing_complete": {
- "description": "VIZIO SmartCast \uae30\uae30\uac00 Home Assistant \uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "description": "VIZIO SmartCast \uae30\uae30\uac00 Home Assistant\uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"title": "\ud398\uc5b4\ub9c1 \uc644\ub8cc"
},
"pairing_complete_import": {
- "description": "VIZIO SmartCast \uae30\uae30\uac00 Home Assistant \uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \n\n\uc561\uc138\uc2a4 \ud1a0\ud070\uc740 '**{access_token}**' \uc785\ub2c8\ub2e4.",
+ "description": "VIZIO SmartCast \uae30\uae30\uac00 Home Assistant\uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \n\n\uc561\uc138\uc2a4 \ud1a0\ud070\uc740 '**{access_token}**' \uc785\ub2c8\ub2e4.",
"title": "\ud398\uc5b4\ub9c1 \uc644\ub8cc"
},
"user": {
@@ -32,7 +33,7 @@
"host": "\ud638\uc2a4\ud2b8",
"name": "\uc774\ub984"
},
- "description": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc740 TV \uc5d0\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4. TV \ub97c \uad6c\uc131\ud558\uace0 \uc788\uace0 \uc544\uc9c1 \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc5c6\ub294 \uacbd\uc6b0 \ud398\uc5b4\ub9c1 \uacfc\uc815\uc744 \uc9c4\ud589\ud558\ub824\uba74 \ube44\uc6cc\ub450\uc138\uc694.",
+ "description": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc740 TV\uc5d0\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4. TV\ub97c \uad6c\uc131\ud558\uace0 \uc788\uace0 \uc544\uc9c1 \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc5c6\ub294 \uacbd\uc6b0 \ud398\uc5b4\ub9c1 \uacfc\uc815\uc744 \uc9c4\ud589\ud558\ub824\uba74 \ube44\uc6cc\ub450\uc138\uc694.",
"title": "VIZIO SmartCast \uae30\uae30"
}
}
@@ -45,7 +46,7 @@
"include_or_exclude": "\uc571\uc744 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"volume_step": "\ubcfc\ub968 \ub2e8\uacc4 \ud06c\uae30"
},
- "description": "\uc2a4\ub9c8\ud2b8 TV \uac00 \uc788\ub294 \uacbd\uc6b0 \uc120\ud0dd\uc0ac\ud56d\uc73c\ub85c \uc18c\uc2a4 \ubaa9\ub85d\uc5d0 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud560 \uc571\uc744 \uc120\ud0dd\ud558\uc5ec \uc18c\uc2a4 \ubaa9\ub85d\uc744 \ud544\ud130\ub9c1\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "description": "\uc2a4\ub9c8\ud2b8 TV\uac00 \uc788\ub294 \uacbd\uc6b0 \uc120\ud0dd\uc0ac\ud56d\uc73c\ub85c \uc18c\uc2a4 \ubaa9\ub85d\uc5d0 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud560 \uc571\uc744 \uc120\ud0dd\ud558\uc5ec \uc18c\uc2a4 \ubaa9\ub85d\uc744 \ud544\ud130\ub9c1\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
"title": "VIZIO SmartCast \uae30\uae30 \uc635\uc158 \uc5c5\ub370\uc774\ud2b8\ud558\uae30"
}
}
diff --git a/homeassistant/components/vizio/translations/nl.json b/homeassistant/components/vizio/translations/nl.json
index 9841eaa7f508fd..48a7a7d353ded7 100644
--- a/homeassistant/components/vizio/translations/nl.json
+++ b/homeassistant/components/vizio/translations/nl.json
@@ -2,16 +2,18 @@
"config": {
"abort": {
"already_configured_device": "Dit apparaat is al geconfigureerd",
+ "cannot_connect": "Kan geen verbinding maken",
"updated_entry": "Dit item is al ingesteld, maar de naam en/of opties die zijn gedefinieerd in de configuratie komen niet overeen met de eerder ge\u00efmporteerde configuratie, dus het configuratie-item is dienovereenkomstig bijgewerkt."
},
"error": {
"cannot_connect": "Verbinding mislukt",
- "complete_pairing_failed": "Kan het koppelen niet voltooien. Zorg ervoor dat de door u opgegeven pincode correct is en dat de tv nog steeds van stroom wordt voorzien en is verbonden met het netwerk voordat u opnieuw verzendt."
+ "complete_pairing_failed": "Kan het koppelen niet voltooien. Zorg ervoor dat de door u opgegeven pincode correct is en dat de tv nog steeds van stroom wordt voorzien en is verbonden met het netwerk voordat u opnieuw verzendt.",
+ "existing_config_entry_found": "Een bestaande VIZIO SmartCast-apparaat config entry met hetzelfde serienummer is reeds geconfigureerd. U moet de bestaande invoer verwijderen om deze te kunnen configureren."
},
"step": {
"pair_tv": {
"data": {
- "pin": "PIN"
+ "pin": "PIN-code"
},
"description": "Uw TV zou een code moeten weergeven. Voer die code in het formulier in en ga dan door naar de volgende stap om de koppeling te voltooien.",
"title": "Voltooi het koppelingsproces"
@@ -28,11 +30,11 @@
"data": {
"access_token": "Toegangstoken",
"device_class": "Apparaattype",
- "host": ":",
+ "host": "Host",
"name": "Naam"
},
"description": "Een toegangstoken is alleen nodig voor tv's. Als u een TV configureert en nog geen toegangstoken heeft, laat dit dan leeg en doorloop het koppelingsproces.",
- "title": "Vizio SmartCast Client instellen"
+ "title": "VIZIO SmartCast-apparaat"
}
}
},
@@ -45,7 +47,7 @@
"volume_step": "Volume Stapgrootte"
},
"description": "Als je een Smart TV hebt, kun je optioneel je bronnenlijst filteren door te kiezen welke apps je in je bronnenlijst wilt opnemen of uitsluiten.",
- "title": "Update Vizo SmartCast Opties"
+ "title": "Update VIZIO SmartCast-apparaat opties"
}
}
}
diff --git a/homeassistant/components/vizio/translations/sv.json b/homeassistant/components/vizio/translations/sv.json
index 8e5ebe47c43378..82483d80fe8d49 100644
--- a/homeassistant/components/vizio/translations/sv.json
+++ b/homeassistant/components/vizio/translations/sv.json
@@ -4,6 +4,19 @@
"updated_entry": "Den h\u00e4r posten har redan konfigurerats, men namnet och/eller alternativen som definierats i konfigurationen matchar inte den tidigare importerade konfigurationen och d\u00e4rf\u00f6r har konfigureringsposten uppdaterats i enlighet med detta."
},
"step": {
+ "pair_tv": {
+ "data": {
+ "pin": "PIN-kod"
+ },
+ "description": "Din TV borde visa en kod. Skriv koden i formul\u00e4ret och forts\u00e4tt sedan till n\u00e4sta steg f\u00f6r att slutf\u00f6ra parningen.",
+ "title": "Slutf\u00f6r parningsprocessen"
+ },
+ "pairing_complete": {
+ "title": "Parkopplingen slutf\u00f6rd"
+ },
+ "pairing_complete_import": {
+ "title": "Parkopplingen slutf\u00f6rd"
+ },
"user": {
"data": {
"access_token": "\u00c5tkomstnyckel",
diff --git a/homeassistant/components/vizio/translations/tr.json b/homeassistant/components/vizio/translations/tr.json
new file mode 100644
index 00000000000000..4b923cfb4b3b8a
--- /dev/null
+++ b/homeassistant/components/vizio/translations/tr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Eri\u015fim Belirteci",
+ "host": "Ana Bilgisayar"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vizio/translations/uk.json b/homeassistant/components/vizio/translations/uk.json
new file mode 100644
index 00000000000000..958307d543f92f
--- /dev/null
+++ b/homeassistant/components/vizio/translations/uk.json
@@ -0,0 +1,54 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_device": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "updated_entry": "\u0426\u044f \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439, \u0430\u043b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438, \u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u0456 \u0432 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457, \u043d\u0435 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u044e\u0442\u044c \u0440\u0430\u043d\u0456\u0448\u0435 \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u0438\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f\u043c, \u0442\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457 \u0431\u0443\u043b\u0430 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u043d\u0438\u043c \u0447\u0438\u043d\u043e\u043c \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0439."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "complete_pairing_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438. \u041f\u0435\u0440\u0448 \u043d\u0456\u0436 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0438 \u0441\u043f\u0440\u043e\u0431\u0443, \u043f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0432\u0432\u0435\u0434\u0435\u043d\u0438\u0439 \u0412\u0430\u043c\u0438 PIN-\u043a\u043e\u0434 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439, \u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0456 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0434\u043e \u043c\u0435\u0440\u0435\u0436\u0456.",
+ "existing_config_entry_found": "\u0406\u0441\u043d\u0443\u044e\u0447\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 VIZIO SmartCast \u0437 \u0442\u0430\u043a\u0438\u043c \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e. \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u0456\u0441\u043d\u0443\u044e\u0447\u0438\u0439 \u0437\u0430\u043f\u0438\u0441, \u0449\u043e\u0431 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u043e\u0442\u043e\u0447\u043d\u0438\u0439."
+ },
+ "step": {
+ "pair_tv": {
+ "data": {
+ "pin": "PIN-\u043a\u043e\u0434"
+ },
+ "description": "\u0412\u0430\u0448 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0437\u0430\u0440\u0430\u0437 \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u0438 \u043a\u043e\u0434. \u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0446\u0435\u0439 \u043a\u043e\u0434 \u0443 \u0444\u043e\u0440\u043c\u0443, \u0430 \u043f\u043e\u0442\u0456\u043c \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0434\u043e \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u043e\u0433\u043e \u043a\u0440\u043e\u043a\u0443, \u0449\u043e\u0431 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438.",
+ "title": "\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044f \u043f\u0440\u043e\u0446\u0435\u0441\u0443 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f"
+ },
+ "pairing_complete": {
+ "description": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 VIZIO SmartCast \u0442\u0435\u043f\u0435\u0440 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e Home Assistant.",
+ "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u043e"
+ },
+ "pairing_complete_import": {
+ "description": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 VIZIO SmartCast \u0442\u0435\u043f\u0435\u0440 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e Home Assistant. \n\n \u0412\u0430\u0448 \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 - '** {access_token} **'.",
+ "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u043e"
+ },
+ "user": {
+ "data": {
+ "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443",
+ "device_class": "\u0422\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e",
+ "host": "\u0425\u043e\u0441\u0442",
+ "name": "\u041d\u0430\u0437\u0432\u0430"
+ },
+ "description": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0438\u0439 \u0442\u0456\u043b\u044c\u043a\u0438 \u0434\u043b\u044f \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0456\u0432. \u042f\u043a\u0449\u043e \u0412\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0443\u0454\u0442\u0435 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0456 \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0443 \u0412\u0430\u0441 \u0449\u0435 \u043d\u0435 \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043e, \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u0446\u0435 \u043f\u043e\u043b\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c, \u0449\u043e\u0431 \u0432\u0438\u043a\u043e\u043d\u0430\u0442\u0438 \u043f\u0440\u043e\u0446\u0435\u0441 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438.",
+ "title": "VIZIO SmartCast"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "apps_to_include_or_exclude": "\u0421\u043f\u0438\u0441\u043e\u043a \u0434\u0436\u0435\u0440\u0435\u043b",
+ "include_or_exclude": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0430\u0431\u043e \u0432\u0438\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0434\u0436\u0435\u0440\u0435\u043b\u0430?",
+ "volume_step": "\u041a\u0440\u043e\u043a \u0433\u0443\u0447\u043d\u043e\u0441\u0442\u0456"
+ },
+ "description": "\u042f\u043a\u0449\u043e \u0443 \u0432\u0430\u0441 \u0454 Smart TV, \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u0438 \u0431\u0430\u0436\u0430\u043d\u043d\u0456 \u0432\u0456\u0434\u0444\u0456\u043b\u044c\u0442\u0440\u0443\u0432\u0430\u0442\u0438 \u0441\u043f\u0438\u0441\u043e\u043a \u0434\u0436\u0435\u0440\u0435\u043b, \u0432\u043a\u043b\u044e\u0447\u0438\u0432\u0448\u0438 \u0430\u0431\u043e \u0432\u0438\u043a\u043b\u044e\u0447\u0438\u0432\u0448\u0438 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0438 \u0437\u0456 \u0441\u043f\u0438\u0441\u043a\u0443.",
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f VIZIO SmartCast"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vizio/translations/zh-Hant.json b/homeassistant/components/vizio/translations/zh-Hant.json
index 257ed829b6ae50..f4ac22716d18a8 100644
--- a/homeassistant/components/vizio/translations/zh-Hant.json
+++ b/homeassistant/components/vizio/translations/zh-Hant.json
@@ -23,17 +23,17 @@
"title": "\u914d\u5c0d\u5b8c\u6210"
},
"pairing_complete_import": {
- "description": "VIZIO SmartCast \u88dd\u7f6e \u5df2\u9023\u7dda\u81f3 Home Assistant\u3002\n\n\u5b58\u53d6\u5bc6\u9470\u70ba '**{access_token}**'\u3002",
+ "description": "VIZIO SmartCast \u88dd\u7f6e \u5df2\u9023\u7dda\u81f3 Home Assistant\u3002\n\n\u5b58\u53d6\u6b0a\u6756\u70ba '**{access_token}**'\u3002",
"title": "\u914d\u5c0d\u5b8c\u6210"
},
"user": {
"data": {
- "access_token": "\u5b58\u53d6\u5bc6\u9470",
+ "access_token": "\u5b58\u53d6\u6b0a\u6756",
"device_class": "\u88dd\u7f6e\u985e\u5225",
"host": "\u4e3b\u6a5f\u7aef",
"name": "\u540d\u7a31"
},
- "description": "\u6b64\u96fb\u8996\u50c5\u9700\u5b58\u53d6\u5bc6\u9470\u5047\u5982\u60a8\u6b63\u5728\u8a2d\u5b9a\u96fb\u8996\u3001\u5c1a\u672a\u53d6\u5f97\u5b58\u53d6\u5bc6\u9470 \uff0c\u4fdd\u6301\u7a7a\u767d\u4ee5\u9032\u884c\u914d\u5c0d\u904e\u7a0b\u3002",
+ "description": "\u6b64\u96fb\u8996\u50c5\u9700\u5b58\u53d6\u6b0a\u6756\u5047\u5982\u60a8\u6b63\u5728\u8a2d\u5b9a\u96fb\u8996\u3001\u5c1a\u672a\u53d6\u5f97\u5b58\u53d6\u6b0a\u6756 \uff0c\u4fdd\u6301\u7a7a\u767d\u4ee5\u9032\u884c\u914d\u5c0d\u904e\u7a0b\u3002",
"title": "VIZIO SmartCast \u88dd\u7f6e"
}
}
@@ -46,7 +46,7 @@
"include_or_exclude": "\u5305\u542b\u6216\u6392\u9664 App\uff1f",
"volume_step": "\u97f3\u91cf\u5927\u5c0f"
},
- "description": "\u5047\u5982\u60a8\u64c1\u6709 Smart TV\u3001\u53ef\u7531\u4f86\u6e90\u5217\u8868\u4e2d\u9078\u64c7\u6240\u8981\u904e\u6ffe\u5305\u542b\u6216\u6392\u9664\u7684 App\u3002\u3002",
+ "description": "\u5047\u5982\u60a8\u64c1\u6709 Smart TV\u3001\u53ef\u7531\u4f86\u6e90\u5217\u8868\u4e2d\u9078\u64c7\u6240\u8981\u7be9\u9078\u5305\u542b\u6216\u6392\u9664\u7684 App\u3002\u3002",
"title": "\u66f4\u65b0 VIZIO SmartCast \u88dd\u7f6e \u9078\u9805"
}
}
diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py
index fe387dd4adc4a7..db5c26f4a0cca1 100644
--- a/homeassistant/components/vlc/media_player.py
+++ b/homeassistant/components/vlc/media_player.py
@@ -159,7 +159,7 @@ def media_stop(self):
def play_media(self, media_type, media_id, **kwargs):
"""Play media from a URL or file."""
- if not media_type == MEDIA_TYPE_MUSIC:
+ if media_type != MEDIA_TYPE_MUSIC:
_LOGGER.error(
"Invalid media type %s. Only %s is supported",
media_type,
diff --git a/homeassistant/components/vlc_telnet/manifest.json b/homeassistant/components/vlc_telnet/manifest.json
index f6e4aa04521467..37941e15458570 100644
--- a/homeassistant/components/vlc_telnet/manifest.json
+++ b/homeassistant/components/vlc_telnet/manifest.json
@@ -2,6 +2,6 @@
"domain": "vlc_telnet",
"name": "VLC media player Telnet",
"documentation": "https://www.home-assistant.io/integrations/vlc-telnet",
- "requirements": ["python-telnet-vlc==1.0.4"],
- "codeowners": ["@rodripf"]
+ "requirements": ["python-telnet-vlc==2.0.1"],
+ "codeowners": ["@rodripf", "@dmcc"]
}
diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py
index 1f0d62b6ee830a..784df2cabcfbb7 100644
--- a/homeassistant/components/vlc_telnet/media_player.py
+++ b/homeassistant/components/vlc_telnet/media_player.py
@@ -1,7 +1,13 @@
"""Provide functionality to interact with the vlc telnet interface."""
import logging
-from python_telnet_vlc import ConnectionError as ConnErr, VLCTelnet
+from python_telnet_vlc import (
+ CommandError,
+ ConnectionError as ConnErr,
+ LuaError,
+ ParseError,
+ VLCTelnet,
+)
import voluptuous as vol
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
@@ -30,6 +36,7 @@
STATE_UNAVAILABLE,
)
import homeassistant.helpers.config_validation as cv
+import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -37,19 +44,20 @@
DEFAULT_NAME = "VLC-TELNET"
DEFAULT_PORT = 4212
+MAX_VOLUME = 500
SUPPORT_VLC = (
- SUPPORT_PAUSE
- | SUPPORT_SEEK
- | SUPPORT_VOLUME_SET
- | SUPPORT_VOLUME_MUTE
- | SUPPORT_PREVIOUS_TRACK
+ SUPPORT_CLEAR_PLAYLIST
| SUPPORT_NEXT_TRACK
- | SUPPORT_PLAY_MEDIA
- | SUPPORT_STOP
- | SUPPORT_CLEAR_PLAYLIST
+ | SUPPORT_PAUSE
| SUPPORT_PLAY
+ | SUPPORT_PLAY_MEDIA
+ | SUPPORT_PREVIOUS_TRACK
+ | SUPPORT_SEEK
| SUPPORT_SHUFFLE_SET
+ | SUPPORT_STOP
+ | SUPPORT_VOLUME_MUTE
+ | SUPPORT_VOLUME_SET
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -81,7 +89,6 @@ class VlcDevice(MediaPlayerEntity):
def __init__(self, name, host, port, passwd):
"""Initialize the vlc device."""
- self._instance = None
self._name = name
self._volume = None
self._muted = None
@@ -93,7 +100,7 @@ def __init__(self, name, host, port, passwd):
self._port = port
self._password = passwd
self._vlc = None
- self._available = False
+ self._available = True
self._volume_bkp = 0
self._media_artist = ""
self._media_title = ""
@@ -103,43 +110,65 @@ def update(self):
if self._vlc is None:
try:
self._vlc = VLCTelnet(self._host, self._password, self._port)
- self._state = STATE_IDLE
- self._available = True
- except (ConnErr, EOFError):
- self._available = False
+ except (ConnErr, EOFError) as err:
+ if self._available:
+ _LOGGER.error("Connection error: %s", err)
+ self._available = False
self._vlc = None
- else:
- try:
- status = self._vlc.status()
- if status:
- if "volume" in status:
- self._volume = int(status["volume"]) / 500.0
- else:
- self._volume = None
- if "state" in status:
- state = status["state"]
- if state == "playing":
- self._state = STATE_PLAYING
- elif state == "paused":
- self._state = STATE_PAUSED
- else:
- self._state = STATE_IDLE
+ return
+
+ self._state = STATE_IDLE
+ self._available = True
+
+ try:
+ status = self._vlc.status()
+ _LOGGER.debug("Status: %s", status)
+
+ if status:
+ if "volume" in status:
+ self._volume = status["volume"] / MAX_VOLUME
+ else:
+ self._volume = None
+ if "state" in status:
+ state = status["state"]
+ if state == "playing":
+ self._state = STATE_PLAYING
+ elif state == "paused":
+ self._state = STATE_PAUSED
else:
self._state = STATE_IDLE
+ else:
+ self._state = STATE_IDLE
+ if self._state != STATE_IDLE:
self._media_duration = self._vlc.get_length()
- self._media_position = self._vlc.get_time()
-
- info = self._vlc.info()
- if info:
- self._media_artist = info[0].get("artist")
- self._media_title = info[0].get("title")
-
- except (ConnErr, EOFError):
+ vlc_position = self._vlc.get_time()
+
+ # Check if current position is stale.
+ if vlc_position != self._media_position:
+ self._media_position_updated_at = dt_util.utcnow()
+ self._media_position = vlc_position
+
+ info = self._vlc.info()
+ _LOGGER.debug("Info: %s", info)
+
+ if info:
+ self._media_artist = info.get(0, {}).get("artist")
+ self._media_title = info.get(0, {}).get("title")
+
+ if not self._media_title:
+ # Fall back to filename.
+ data_info = info.get("data")
+ if data_info:
+ self._media_title = data_info["filename"]
+
+ except (CommandError, LuaError, ParseError) as err:
+ _LOGGER.error("Command error: %s", err)
+ except (ConnErr, EOFError) as err:
+ if self._available:
+ _LOGGER.error("Connection error: %s", err)
self._available = False
- self._vlc = None
-
- return True
+ self._vlc = None
@property
def name(self):
@@ -203,26 +232,27 @@ def media_artist(self):
def media_seek(self, position):
"""Seek the media to a specific location."""
- track_length = self._vlc.get_length() / 1000
- self._vlc.seek(position / track_length)
+ self._vlc.seek(int(position))
def mute_volume(self, mute):
"""Mute the volume."""
if mute:
self._volume_bkp = self._volume
- self._volume = 0
- self._vlc.set_volume("0")
+ self.set_volume_level(0)
else:
- self._vlc.set_volume(str(self._volume_bkp))
- self._volume = self._volume_bkp
+ self.set_volume_level(self._volume_bkp)
self._muted = mute
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
- self._vlc.set_volume(str(volume * 500))
+ self._vlc.set_volume(volume * MAX_VOLUME)
self._volume = volume
+ if self._muted and self._volume > 0:
+ # This can happen if we were muted and then see a volume_up.
+ self._muted = False
+
def media_play(self):
"""Send play command."""
self._vlc.play()
@@ -230,7 +260,11 @@ def media_play(self):
def media_pause(self):
"""Send pause command."""
- self._vlc.pause()
+ current_state = self._vlc.status().get("state")
+ if current_state != "paused":
+ # Make sure we're not already paused since VLCTelnet.pause() toggles
+ # pause.
+ self._vlc.pause()
self._state = STATE_PAUSED
def media_stop(self):
diff --git a/homeassistant/components/volkszaehler/sensor.py b/homeassistant/components/volkszaehler/sensor.py
index a418780c165b80..61fcf1d2969883 100644
--- a/homeassistant/components/volkszaehler/sensor.py
+++ b/homeassistant/components/volkszaehler/sensor.py
@@ -6,7 +6,7 @@
from volkszaehler.exceptions import VolkszaehlerApiConnectionError
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_HOST,
CONF_MONITORED_CONDITIONS,
@@ -18,7 +18,6 @@
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -77,7 +76,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(dev, True)
-class VolkszaehlerSensor(Entity):
+class VolkszaehlerSensor(SensorEntity):
"""Implementation of a Volkszaehler sensor."""
def __init__(self, vz_api, name, sensor_type):
diff --git a/homeassistant/components/volumio/__init__.py b/homeassistant/components/volumio/__init__.py
index 8d171cab9d2dfa..a9c6fb746aaec3 100644
--- a/homeassistant/components/volumio/__init__.py
+++ b/homeassistant/components/volumio/__init__.py
@@ -14,11 +14,6 @@
PLATFORMS = ["media_player"]
-async def async_setup(hass: HomeAssistant, config: dict):
- """Set up the Volumio component."""
- return True
-
-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Volumio from a config entry."""
@@ -35,9 +30,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
DATA_INFO: info,
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -48,8 +43,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/volumio/config_flow.py b/homeassistant/components/volumio/config_flow.py
index 950a161a5c3ece..80ec2f05d91987 100644
--- a/homeassistant/components/volumio/config_flow.py
+++ b/homeassistant/components/volumio/config_flow.py
@@ -1,6 +1,7 @@
"""Config flow for Volumio integration."""
+from __future__ import annotations
+
import logging
-from typing import Optional
from pyvolumio import CannotConnectError, Volumio
import voluptuous as vol
@@ -11,7 +12,7 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import DiscoveryInfoType
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -39,10 +40,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self):
"""Initialize flow."""
- self._host: Optional[str] = None
- self._port: Optional[int] = None
- self._name: Optional[str] = None
- self._uuid: Optional[str] = None
+ self._host: str | None = None
+ self._port: int | None = None
+ self._name: str | None = None
+ self._uuid: str | None = None
@callback
def _async_get_entry(self):
diff --git a/homeassistant/components/volumio/translations/de.json b/homeassistant/components/volumio/translations/de.json
index ef455299de6e66..45727d85ee05d0 100644
--- a/homeassistant/components/volumio/translations/de.json
+++ b/homeassistant/components/volumio/translations/de.json
@@ -1,7 +1,19 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
"error": {
- "cannot_connect": "Verbindungsfehler"
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/volumio/translations/hu.json b/homeassistant/components/volumio/translations/hu.json
index 3b2d79a34a77e2..e58f0666039ab9 100644
--- a/homeassistant/components/volumio/translations/hu.json
+++ b/homeassistant/components/volumio/translations/hu.json
@@ -1,7 +1,24 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Nem lehet csatlakozni a felfedezett Volumi\u00f3hoz"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "discovery_confirm": {
+ "description": "Szeretn\u00e9d hozz\u00e1adni a Volumio (`{name}`)-t a Home Assistant-hoz?",
+ "title": "Felfedezett Volumio"
+ },
+ "user": {
+ "data": {
+ "host": "Hoszt",
+ "port": "Port"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/volumio/translations/id.json b/homeassistant/components/volumio/translations/id.json
new file mode 100644
index 00000000000000..210c6eca87d439
--- /dev/null
+++ b/homeassistant/components/volumio/translations/id.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "cannot_connect": "Tidak dapat terhubung ke Volumio yang ditemukan"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "discovery_confirm": {
+ "description": "Ingin menambahkan Volumio (`{name}`) ke Home Assistant?",
+ "title": "Volumio yang ditemukan"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/volumio/translations/ko.json b/homeassistant/components/volumio/translations/ko.json
new file mode 100644
index 00000000000000..75ff87b41c85df
--- /dev/null
+++ b/homeassistant/components/volumio/translations/ko.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\ubc1c\uacac\ub41c Volumio\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "discovery_confirm": {
+ "description": "Home Assistant\uc5d0 Volumio (`{name}`)\uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "\ubc1c\uacac\ub41c Volumio"
+ },
+ "user": {
+ "data": {
+ "host": "\ud638\uc2a4\ud2b8",
+ "port": "\ud3ec\ud2b8"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/volumio/translations/nl.json b/homeassistant/components/volumio/translations/nl.json
index 9179418def99c0..96538422fe0c85 100644
--- a/homeassistant/components/volumio/translations/nl.json
+++ b/homeassistant/components/volumio/translations/nl.json
@@ -1,12 +1,18 @@
{
"config": {
"abort": {
- "already_configured": "Apparaat is al geconfigureerd"
+ "already_configured": "Apparaat is al geconfigureerd",
+ "cannot_connect": "Kan geen verbinding maken met Volumio"
},
"error": {
+ "cannot_connect": "Kan geen verbinding maken",
"unknown": "Onverwachte fout"
},
"step": {
+ "discovery_confirm": {
+ "description": "Wilt u Volumio (`{name}`) toevoegen aan Home Assistant?",
+ "title": "Volumio ontdekt"
+ },
"user": {
"data": {
"host": "Host",
diff --git a/homeassistant/components/volumio/translations/tr.json b/homeassistant/components/volumio/translations/tr.json
new file mode 100644
index 00000000000000..249bb17d64ecac
--- /dev/null
+++ b/homeassistant/components/volumio/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ke\u015ffedilen Volumio'ya ba\u011flan\u0131lam\u0131yor"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar",
+ "port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/volumio/translations/uk.json b/homeassistant/components/volumio/translations/uk.json
index 58947e14e4f7e0..c517eafa2bdbd4 100644
--- a/homeassistant/components/volumio/translations/uk.json
+++ b/homeassistant/components/volumio/translations/uk.json
@@ -1,14 +1,16 @@
{
"config": {
"abort": {
- "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e"
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u0438\u043c Volumio."
},
"error": {
- "cannot_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
"unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
},
"step": {
"discovery_confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 Volumio `{name}`?",
"title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e Volumio"
},
"user": {
diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py
index 7b7dffbef18ef5..556a5f2511411b 100644
--- a/homeassistant/components/volvooncall/__init__.py
+++ b/homeassistant/components/volvooncall/__init__.py
@@ -8,6 +8,7 @@
from homeassistant.const import (
CONF_NAME,
CONF_PASSWORD,
+ CONF_REGION,
CONF_RESOURCES,
CONF_SCAN_INTERVAL,
CONF_USERNAME,
@@ -32,14 +33,13 @@
MIN_UPDATE_INTERVAL = timedelta(minutes=1)
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1)
-CONF_REGION = "region"
CONF_SERVICE_URL = "service_url"
CONF_SCANDINAVIAN_MILES = "scandinavian_miles"
CONF_MUTABLE = "mutable"
SIGNAL_STATE_UPDATED = f"{DOMAIN}.updated"
-COMPONENTS = {
+PLATFORMS = {
"sensor": "sensor",
"binary_sensor": "binary_sensor",
"lock": "lock",
@@ -146,7 +146,7 @@ def discover_vehicle(vehicle):
for instrument in (
instrument
for instrument in dashboard.instruments
- if instrument.component in COMPONENTS and is_enabled(instrument.slug_attr)
+ if instrument.component in PLATFORMS and is_enabled(instrument.slug_attr)
):
data.instruments.add(instrument)
@@ -154,7 +154,7 @@ def discover_vehicle(vehicle):
hass.async_create_task(
discovery.async_load_platform(
hass,
- COMPONENTS[instrument.component],
+ PLATFORMS[instrument.component],
DOMAIN,
(vehicle.vin, instrument.component, instrument.attr),
config,
@@ -277,7 +277,7 @@ def assumed_state(self):
return True
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
return dict(
self.instrument.attributes,
diff --git a/homeassistant/components/volvooncall/sensor.py b/homeassistant/components/volvooncall/sensor.py
index 0915408860db35..ad6571576b4adf 100644
--- a/homeassistant/components/volvooncall/sensor.py
+++ b/homeassistant/components/volvooncall/sensor.py
@@ -1,4 +1,6 @@
"""Support for Volvo On Call sensors."""
+from homeassistant.components.sensor import SensorEntity
+
from . import DATA_KEY, VolvoEntity
@@ -9,7 +11,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([VolvoSensor(hass.data[DATA_KEY], *discovery_info)])
-class VolvoSensor(VolvoEntity):
+class VolvoSensor(VolvoEntity, SensorEntity):
"""Representation of a Volvo sensor."""
@property
diff --git a/homeassistant/components/vultr/binary_sensor.py b/homeassistant/components/vultr/binary_sensor.py
index c1b60479e7af8c..c62d5136aa6f46 100644
--- a/homeassistant/components/vultr/binary_sensor.py
+++ b/homeassistant/components/vultr/binary_sensor.py
@@ -86,7 +86,7 @@ def device_class(self):
return DEFAULT_DEVICE_CLASS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the Vultr subscription."""
return {
ATTR_ALLOWED_BANDWIDTH: self.data.get("allowed_bandwidth_gb"),
diff --git a/homeassistant/components/vultr/sensor.py b/homeassistant/components/vultr/sensor.py
index 0bcdcf9d4c153a..5e6815944d76ff 100644
--- a/homeassistant/components/vultr/sensor.py
+++ b/homeassistant/components/vultr/sensor.py
@@ -3,10 +3,9 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, DATA_GIGABYTES
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from . import (
ATTR_CURRENT_BANDWIDTH_USED,
@@ -58,7 +57,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class VultrSensor(Entity):
+class VultrSensor(SensorEntity):
"""Representation of a Vultr subscription sensor."""
def __init__(self, vultr, subscription, condition, name):
diff --git a/homeassistant/components/vultr/switch.py b/homeassistant/components/vultr/switch.py
index a9c43717a71fd0..f93c4f444d6981 100644
--- a/homeassistant/components/vultr/switch.py
+++ b/homeassistant/components/vultr/switch.py
@@ -80,7 +80,7 @@ def icon(self):
return "mdi:server" if self.is_on else "mdi:server-off"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the Vultr subscription."""
return {
ATTR_ALLOWED_BANDWIDTH: self.data.get("allowed_bandwidth_gb"),
diff --git a/homeassistant/components/wake_on_lan/manifest.json b/homeassistant/components/wake_on_lan/manifest.json
index c66f87ae26ed07..b98414257720a7 100644
--- a/homeassistant/components/wake_on_lan/manifest.json
+++ b/homeassistant/components/wake_on_lan/manifest.json
@@ -2,6 +2,6 @@
"domain": "wake_on_lan",
"name": "Wake on LAN",
"documentation": "https://www.home-assistant.io/integrations/wake_on_lan",
- "requirements": ["wakeonlan==1.1.6"],
- "codeowners": []
+ "requirements": ["wakeonlan==2.0.0"],
+ "codeowners": ["@ntilley905"]
}
diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py
index 491bae782c52ef..eba6897647b595 100644
--- a/homeassistant/components/wake_on_lan/switch.py
+++ b/homeassistant/components/wake_on_lan/switch.py
@@ -57,7 +57,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
broadcast_port,
)
],
- True,
+ host is not None,
)
@@ -86,6 +86,7 @@ def __init__(
Script(hass, off_action, name, domain) if off_action else None
)
self._state = False
+ self._assumed_state = host is None
@property
def is_on(self):
@@ -97,6 +98,16 @@ def name(self):
"""Return the name of the switch."""
return self._name
+ @property
+ def assumed_state(self):
+ """Return true if no host is provided."""
+ return self._assumed_state
+
+ @property
+ def should_poll(self):
+ """Return false if assumed state is true."""
+ return not self._assumed_state
+
def turn_on(self, **kwargs):
"""Turn the device on."""
service_kwargs = {}
@@ -114,13 +125,21 @@ def turn_on(self, **kwargs):
wakeonlan.send_magic_packet(self._mac_address, **service_kwargs)
+ if self._assumed_state:
+ self._state = True
+ self.async_write_ha_state()
+
def turn_off(self, **kwargs):
"""Turn the device off if an off action is present."""
if self._off_script is not None:
self._off_script.run(context=self._context)
+ if self._assumed_state:
+ self._state = False
+ self.async_write_ha_state()
+
def update(self):
- """Check if device is on and update the state."""
+ """Check if device is on and update the state. Only called if assumed state is false."""
if platform.system().lower() == "windows":
ping_cmd = [
"ping",
diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py
index ec18880b5ba1e7..ef01c057a9e7db 100644
--- a/homeassistant/components/waqi/sensor.py
+++ b/homeassistant/components/waqi/sensor.py
@@ -7,6 +7,7 @@
import voluptuous as vol
from waqiasync import WaqiClient
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_TEMPERATURE,
@@ -17,7 +18,6 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -93,7 +93,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(dev, True)
-class WaqiSensor(Entity):
+class WaqiSensor(SensorEntity):
"""Implementation of a WAQI sensor."""
def __init__(self, client, station):
@@ -151,7 +151,7 @@ def unit_of_measurement(self):
return "AQI"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the last update."""
attrs = {}
diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py
index 0763c552075118..5ae22c77b5ee02 100644
--- a/homeassistant/components/water_heater/__init__.py
+++ b/homeassistant/components/water_heater/__init__.py
@@ -2,6 +2,7 @@
from datetime import timedelta
import functools as ft
import logging
+from typing import final
import voluptuous as vol
@@ -129,7 +130,7 @@ async def async_unload_entry(hass, entry):
class WaterHeaterEntity(Entity):
- """Representation of a water_heater device."""
+ """Base class for water heater entities."""
@property
def state(self):
@@ -162,6 +163,7 @@ def capability_attributes(self):
return data
+ @final
@property
def state_attributes(self):
"""Return the optional state attributes."""
diff --git a/homeassistant/components/water_heater/device_action.py b/homeassistant/components/water_heater/device_action.py
index 991929580d1a91..e1c84be8753d7a 100644
--- a/homeassistant/components/water_heater/device_action.py
+++ b/homeassistant/components/water_heater/device_action.py
@@ -1,5 +1,5 @@
"""Provides device automations for Water Heater."""
-from typing import List, Optional
+from __future__ import annotations
import voluptuous as vol
@@ -28,7 +28,7 @@
)
-async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device actions for Water Heater devices."""
registry = await entity_registry.async_get_registry(hass)
actions = []
@@ -58,11 +58,9 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
async def async_call_action_from_config(
- hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context]
+ hass: HomeAssistant, config: dict, variables: dict, context: Context | None
) -> None:
"""Execute a device action."""
- config = ACTION_SCHEMA(config)
-
service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]}
if config[CONF_TYPE] == "turn_on":
diff --git a/homeassistant/components/water_heater/reproduce_state.py b/homeassistant/components/water_heater/reproduce_state.py
index 77cdba93f96a34..4675cdb8621def 100644
--- a/homeassistant/components/water_heater/reproduce_state.py
+++ b/homeassistant/components/water_heater/reproduce_state.py
@@ -1,7 +1,9 @@
"""Reproduce an Water heater state."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -47,8 +49,8 @@ async def _async_reproduce_state(
hass: HomeAssistantType,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -124,8 +126,8 @@ async def async_reproduce_states(
hass: HomeAssistantType,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce Water heater states."""
await asyncio.gather(
diff --git a/homeassistant/components/water_heater/strings.json b/homeassistant/components/water_heater/strings.json
index 8f5709ac155cae..71fc1dc9328629 100644
--- a/homeassistant/components/water_heater/strings.json
+++ b/homeassistant/components/water_heater/strings.json
@@ -4,5 +4,16 @@
"turn_on": "Turn on {entity_name}",
"turn_off": "Turn off {entity_name}"
}
+ },
+ "state": {
+ "_": {
+ "off": "[%key:common::state::off%]",
+ "eco": "Eco",
+ "electric": "Electric",
+ "gas": "Gas",
+ "high_demand": "High Demand",
+ "heat_pump": "Heat Pump",
+ "performance": "Performance"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/ca.json b/homeassistant/components/water_heater/translations/ca.json
index 8c1de7124f5b39..022b5e887d8c6e 100644
--- a/homeassistant/components/water_heater/translations/ca.json
+++ b/homeassistant/components/water_heater/translations/ca.json
@@ -4,5 +4,16 @@
"turn_off": "Apaga {entity_name}",
"turn_on": "Engega {entity_name}"
}
+ },
+ "state": {
+ "_": {
+ "eco": "Eco",
+ "electric": "El\u00e8ctric",
+ "gas": "Gas",
+ "heat_pump": "Bomba de calor",
+ "high_demand": "Alta demanda",
+ "off": "OFF",
+ "performance": "Rendiment"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/cs.json b/homeassistant/components/water_heater/translations/cs.json
index c7152528be9870..9aa1831388336b 100644
--- a/homeassistant/components/water_heater/translations/cs.json
+++ b/homeassistant/components/water_heater/translations/cs.json
@@ -4,5 +4,10 @@
"turn_off": "Vypnout {entity_name}",
"turn_on": "Zapnout {entity_name}"
}
+ },
+ "state": {
+ "_": {
+ "off": "Vypnuto"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/de.json b/homeassistant/components/water_heater/translations/de.json
index 9169018a5d4b4e..86f6115835136c 100644
--- a/homeassistant/components/water_heater/translations/de.json
+++ b/homeassistant/components/water_heater/translations/de.json
@@ -4,5 +4,16 @@
"turn_off": "{entity_name} ausschalten",
"turn_on": "{entity_name} einschalten"
}
+ },
+ "state": {
+ "_": {
+ "eco": "Sparmodus",
+ "electric": "Elektrisch",
+ "gas": "Gas",
+ "heat_pump": "W\u00e4rmepumpe",
+ "high_demand": "Hoher Bedarf",
+ "off": "Aus",
+ "performance": "Leistung"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/el.json b/homeassistant/components/water_heater/translations/el.json
new file mode 100644
index 00000000000000..827a171a7acf2f
--- /dev/null
+++ b/homeassistant/components/water_heater/translations/el.json
@@ -0,0 +1,13 @@
+{
+ "state": {
+ "_": {
+ "eco": "\u039f\u03b9\u03ba\u03bf\u03bb\u03bf\u03b3\u03b9\u03ba\u03cc",
+ "electric": "\u0397\u03bb\u03b5\u03ba\u03c4\u03c1\u03b9\u03ba\u03cc",
+ "gas": "\u0391\u03ad\u03c1\u03b9\u03bf",
+ "heat_pump": "\u0391\u03bd\u03c4\u03bb\u03af\u03b1 \u0398\u03b5\u03c1\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2",
+ "high_demand": "\u03a5\u03c8\u03b7\u03bb\u03ae \u0396\u03ae\u03c4\u03b7\u03c3\u03b7",
+ "off": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf",
+ "performance": "\u0391\u03c0\u03cc\u03b4\u03bf\u03c3\u03b7"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/en.json b/homeassistant/components/water_heater/translations/en.json
index d6abfbb995d3ca..6885fcb198e8af 100644
--- a/homeassistant/components/water_heater/translations/en.json
+++ b/homeassistant/components/water_heater/translations/en.json
@@ -4,5 +4,16 @@
"turn_off": "Turn off {entity_name}",
"turn_on": "Turn on {entity_name}"
}
+ },
+ "state": {
+ "_": {
+ "eco": "Eco",
+ "electric": "Electric",
+ "gas": "Gas",
+ "heat_pump": "Heat Pump",
+ "high_demand": "High Demand",
+ "off": "Off",
+ "performance": "Performance"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/es.json b/homeassistant/components/water_heater/translations/es.json
index 46be0201ba5e05..f11f9592b81536 100644
--- a/homeassistant/components/water_heater/translations/es.json
+++ b/homeassistant/components/water_heater/translations/es.json
@@ -4,5 +4,15 @@
"turn_off": "Apagar {entity_name}",
"turn_on": "Encender {entity_name}"
}
+ },
+ "state": {
+ "_": {
+ "eco": "Eco",
+ "electric": "El\u00e9ctrico",
+ "gas": "Gas",
+ "heat_pump": "Bomba de calor",
+ "high_demand": "Alta demanda",
+ "performance": "Rendimiento"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/et.json b/homeassistant/components/water_heater/translations/et.json
index fbb3d11424fcdd..cba6b8274d998f 100644
--- a/homeassistant/components/water_heater/translations/et.json
+++ b/homeassistant/components/water_heater/translations/et.json
@@ -4,5 +4,16 @@
"turn_off": "L\u00fclita {entity_name} v\u00e4lja",
"turn_on": "L\u00fclita {entity_name} sisse"
}
+ },
+ "state": {
+ "_": {
+ "eco": "\u00d6ko",
+ "electric": "Elektriline",
+ "gas": "Gaas",
+ "heat_pump": "Soojuspump",
+ "high_demand": "Suur n\u00f5udlus",
+ "off": "V\u00e4lja l\u00fclitatud",
+ "performance": "J\u00f5udlus"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/fr.json b/homeassistant/components/water_heater/translations/fr.json
index ac72ffab883618..84c9b730b2268c 100644
--- a/homeassistant/components/water_heater/translations/fr.json
+++ b/homeassistant/components/water_heater/translations/fr.json
@@ -4,5 +4,16 @@
"turn_off": "\u00c9teindre {entity_name}",
"turn_on": "Allumer {entity_name}"
}
+ },
+ "state": {
+ "_": {
+ "eco": "\u00c9co",
+ "electric": "\u00c9lectrique",
+ "gas": "Gaz",
+ "heat_pump": "Pompe \u00e0 chaleur",
+ "high_demand": "Demande \u00e9lev\u00e9e",
+ "off": "Inactif",
+ "performance": "Performance"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/hu.json b/homeassistant/components/water_heater/translations/hu.json
index c3c47030acb4fe..82f88d1f0dec88 100644
--- a/homeassistant/components/water_heater/translations/hu.json
+++ b/homeassistant/components/water_heater/translations/hu.json
@@ -4,5 +4,16 @@
"turn_off": "{entity_name} kikapcsol\u00e1sa",
"turn_on": "{entity_name} bekapcsol\u00e1sa"
}
+ },
+ "state": {
+ "_": {
+ "eco": "Takar\u00e9kos",
+ "electric": "Elektromos",
+ "gas": "G\u00e1z",
+ "heat_pump": "H\u0151szivatty\u00fa",
+ "high_demand": "Magas ig\u00e9nybev\u00e9tel",
+ "off": "Ki",
+ "performance": "Teljes\u00edtm\u00e9ny"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/id.json b/homeassistant/components/water_heater/translations/id.json
new file mode 100644
index 00000000000000..591f96ffc4f4e1
--- /dev/null
+++ b/homeassistant/components/water_heater/translations/id.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "turn_off": "Matikan {entity_name}",
+ "turn_on": "Nyalakan {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/it.json b/homeassistant/components/water_heater/translations/it.json
index 86458a54272f50..88c53f67e3a920 100644
--- a/homeassistant/components/water_heater/translations/it.json
+++ b/homeassistant/components/water_heater/translations/it.json
@@ -4,5 +4,16 @@
"turn_off": "Disattiva {entity_name}",
"turn_on": "Attiva {entity_name}"
}
+ },
+ "state": {
+ "_": {
+ "eco": "Eco",
+ "electric": "Elettrico",
+ "gas": "Gas",
+ "heat_pump": "Pompa di calore",
+ "high_demand": "Forte richiesta",
+ "off": "Spento",
+ "performance": "Prestazione"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/ko.json b/homeassistant/components/water_heater/translations/ko.json
new file mode 100644
index 00000000000000..8c591c1991823b
--- /dev/null
+++ b/homeassistant/components/water_heater/translations/ko.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "action_type": {
+ "turn_off": "{entity_name}\uc744(\ub97c) \ub044\uae30",
+ "turn_on": "{entity_name}\uc744(\ub97c) \ucf1c\uae30"
+ }
+ },
+ "state": {
+ "_": {
+ "eco": "\uc808\uc57d",
+ "electric": "\uc804\uae30",
+ "gas": "\uac00\uc2a4",
+ "heat_pump": "\ud788\ud2b8 \ud38c\ud504",
+ "high_demand": "\uace0\uc131\ub2a5",
+ "off": "\uaebc\uc9d0",
+ "performance": "\uace0\ud6a8\uc728"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/nl.json b/homeassistant/components/water_heater/translations/nl.json
index 8b832b52c1c154..4a2d135718825f 100644
--- a/homeassistant/components/water_heater/translations/nl.json
+++ b/homeassistant/components/water_heater/translations/nl.json
@@ -4,5 +4,16 @@
"turn_off": "Schakel {entity_name} uit",
"turn_on": "Schakel {entity_name} in"
}
+ },
+ "state": {
+ "_": {
+ "eco": "Eco",
+ "electric": "Elektriciteit",
+ "gas": "Gas",
+ "heat_pump": "Warmtepomp",
+ "high_demand": "Hoge vraag",
+ "off": "Uit",
+ "performance": "Prestaties"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/no.json b/homeassistant/components/water_heater/translations/no.json
index 6455919518f60e..2cbb96b23e7769 100644
--- a/homeassistant/components/water_heater/translations/no.json
+++ b/homeassistant/components/water_heater/translations/no.json
@@ -4,5 +4,16 @@
"turn_off": "Sl\u00e5 av {entity_name}",
"turn_on": "Sl\u00e5 p\u00e5 {entity_name}"
}
+ },
+ "state": {
+ "_": {
+ "eco": "\u00d8ko",
+ "electric": "Elektrisk",
+ "gas": "Gass",
+ "heat_pump": "Varmepumpe",
+ "high_demand": "H\u00f8y ettersp\u00f8rsel",
+ "off": "Av",
+ "performance": "Ytelse"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/pl.json b/homeassistant/components/water_heater/translations/pl.json
index d6deb99da020d4..bb314c855106f8 100644
--- a/homeassistant/components/water_heater/translations/pl.json
+++ b/homeassistant/components/water_heater/translations/pl.json
@@ -4,5 +4,16 @@
"turn_off": "wy\u0142\u0105cz {entity_name}",
"turn_on": "w\u0142\u0105cz {entity_name}"
}
+ },
+ "state": {
+ "_": {
+ "eco": "ekonomiczny",
+ "electric": "elektryczny",
+ "gas": "gaz",
+ "heat_pump": "pompa ciep\u0142a",
+ "high_demand": "du\u017ce zapotrzebowanie",
+ "off": "wy\u0142.",
+ "performance": "wydajno\u015b\u0107"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/pt.json b/homeassistant/components/water_heater/translations/pt.json
index 2278e7701aadcc..97cc30fe1350a8 100644
--- a/homeassistant/components/water_heater/translations/pt.json
+++ b/homeassistant/components/water_heater/translations/pt.json
@@ -4,5 +4,15 @@
"turn_off": "Desligar {entity_name}",
"turn_on": "Ligar {entity_name}"
}
+ },
+ "state": {
+ "_": {
+ "eco": "Eco",
+ "electric": "El\u00e9trico",
+ "gas": "G\u00e1s",
+ "heat_pump": "Bomba de calor",
+ "high_demand": "Necessidade alta",
+ "off": "Desligado"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/ru.json b/homeassistant/components/water_heater/translations/ru.json
index 7c703da39d5f36..b3491f82e5eb9e 100644
--- a/homeassistant/components/water_heater/translations/ru.json
+++ b/homeassistant/components/water_heater/translations/ru.json
@@ -4,5 +4,16 @@
"turn_off": "{entity_name}: \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c",
"turn_on": "{entity_name}: \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c"
}
+ },
+ "state": {
+ "_": {
+ "eco": "\u042d\u043a\u043e",
+ "electric": "\u042d\u043b\u0435\u043a\u0442\u0440\u0438\u0447\u0435\u0441\u043a\u0438\u0439",
+ "gas": "\u0413\u0430\u0437",
+ "heat_pump": "\u0422\u0435\u043f\u043b\u043e\u0432\u043e\u0439 \u043d\u0430\u0441\u043e\u0441",
+ "high_demand": "\u0411\u043e\u043b\u044c\u0448\u0430\u044f \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0430",
+ "off": "\u0412\u044b\u043a\u043b",
+ "performance": "\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/uk.json b/homeassistant/components/water_heater/translations/uk.json
new file mode 100644
index 00000000000000..d6558828a8ec91
--- /dev/null
+++ b/homeassistant/components/water_heater/translations/uk.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438",
+ "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/zh-Hant.json b/homeassistant/components/water_heater/translations/zh-Hant.json
index 11e88cb1faeb20..5bb7aa129b5179 100644
--- a/homeassistant/components/water_heater/translations/zh-Hant.json
+++ b/homeassistant/components/water_heater/translations/zh-Hant.json
@@ -4,5 +4,16 @@
"turn_off": "\u95dc\u9589{entity_name}",
"turn_on": "\u958b\u555f{entity_name}"
}
+ },
+ "state": {
+ "_": {
+ "eco": "\u7bc0\u80fd",
+ "electric": "\u96fb\u529b",
+ "gas": "\u74e6\u65af",
+ "heat_pump": "\u6696\u6c23",
+ "high_demand": "\u9ad8\u7528\u91cf",
+ "off": "\u95dc\u9589",
+ "performance": "\u6548\u80fd"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/waterfurnace/sensor.py b/homeassistant/components/waterfurnace/sensor.py
index dfb960fe81930a..91e455d03d69ad 100644
--- a/homeassistant/components/waterfurnace/sensor.py
+++ b/homeassistant/components/waterfurnace/sensor.py
@@ -1,9 +1,8 @@
"""Support for Waterfurnace."""
-from homeassistant.components.sensor import ENTITY_ID_FORMAT
+from homeassistant.components.sensor import ENTITY_ID_FORMAT, SensorEntity
from homeassistant.const import PERCENTAGE, POWER_WATT, TEMP_FAHRENHEIT
from homeassistant.core import callback
-from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify
from . import DOMAIN as WF_DOMAIN, UPDATE_TOPIC
@@ -61,7 +60,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors)
-class WaterFurnaceSensor(Entity):
+class WaterFurnaceSensor(SensorEntity):
"""Implementing the Waterfurnace sensor."""
def __init__(self, client, config):
diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py
index 9b2af2ea7fe5ac..ad989ec39fcbfb 100644
--- a/homeassistant/components/watson_tts/tts.py
+++ b/homeassistant/components/watson_tts/tts.py
@@ -8,7 +8,6 @@
CONF_URL = "watson_url"
CONF_APIKEY = "watson_apikey"
-ATTR_CREDENTIALS = "credentials"
DEFAULT_URL = "https://stream.watsonplatform.net/text-to-speech/api"
diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py
index 9674bd9850e5d1..20a0c01c642173 100644
--- a/homeassistant/components/waze_travel_time/__init__.py
+++ b/homeassistant/components/waze_travel_time/__init__.py
@@ -1 +1,29 @@
"""The waze_travel_time component."""
+import asyncio
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+
+PLATFORMS = ["sensor"]
+
+
+async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+ """Load the saved entities."""
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+ """Unload a config entry."""
+ return all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
+ ]
+ )
+ )
diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py
new file mode 100644
index 00000000000000..05dd372f9d9c28
--- /dev/null
+++ b/homeassistant/components/waze_travel_time/config_flow.py
@@ -0,0 +1,149 @@
+"""Config flow for Waze Travel Time integration."""
+import logging
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_NAME, CONF_REGION
+from homeassistant.core import callback
+import homeassistant.helpers.config_validation as cv
+from homeassistant.util import slugify
+
+from .const import (
+ CONF_AVOID_FERRIES,
+ CONF_AVOID_SUBSCRIPTION_ROADS,
+ CONF_AVOID_TOLL_ROADS,
+ CONF_DESTINATION,
+ CONF_EXCL_FILTER,
+ CONF_INCL_FILTER,
+ CONF_ORIGIN,
+ CONF_REALTIME,
+ CONF_UNITS,
+ CONF_VEHICLE_TYPE,
+ DEFAULT_NAME,
+ DOMAIN,
+ REGIONS,
+ UNITS,
+ VEHICLE_TYPES,
+)
+from .helpers import is_valid_config_entry
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class WazeOptionsFlow(config_entries.OptionsFlow):
+ """Handle an options flow for Waze Travel Time."""
+
+ def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
+ """Initialize waze options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(self, user_input=None):
+ """Handle the initial step."""
+ 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_INCL_FILTER,
+ default=self.config_entry.options.get(CONF_INCL_FILTER),
+ ): cv.string,
+ vol.Optional(
+ CONF_EXCL_FILTER,
+ default=self.config_entry.options.get(CONF_EXCL_FILTER),
+ ): cv.string,
+ vol.Optional(
+ CONF_REALTIME,
+ default=self.config_entry.options[CONF_REALTIME],
+ ): cv.boolean,
+ vol.Optional(
+ CONF_VEHICLE_TYPE,
+ default=self.config_entry.options[CONF_VEHICLE_TYPE],
+ ): vol.In(VEHICLE_TYPES),
+ vol.Optional(
+ CONF_UNITS,
+ default=self.config_entry.options[CONF_UNITS],
+ ): vol.In(UNITS),
+ vol.Optional(
+ CONF_AVOID_TOLL_ROADS,
+ default=self.config_entry.options[CONF_AVOID_TOLL_ROADS],
+ ): cv.boolean,
+ vol.Optional(
+ CONF_AVOID_SUBSCRIPTION_ROADS,
+ default=self.config_entry.options[
+ CONF_AVOID_SUBSCRIPTION_ROADS
+ ],
+ ): cv.boolean,
+ vol.Optional(
+ CONF_AVOID_FERRIES,
+ default=self.config_entry.options[CONF_AVOID_FERRIES],
+ ): cv.boolean,
+ }
+ ),
+ )
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Waze Travel Time."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(
+ config_entry: config_entries.ConfigEntry,
+ ) -> WazeOptionsFlow:
+ """Get the options flow for this handler."""
+ return WazeOptionsFlow(config_entry)
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ errors = {}
+ if user_input is not None:
+ if await self.hass.async_add_executor_job(
+ is_valid_config_entry,
+ self.hass,
+ _LOGGER,
+ user_input[CONF_ORIGIN],
+ user_input[CONF_DESTINATION],
+ user_input[CONF_REGION],
+ ):
+ await self.async_set_unique_id(
+ slugify(
+ f"{DOMAIN}_{user_input[CONF_ORIGIN]}_{user_input[CONF_DESTINATION]}"
+ )
+ )
+ self._abort_if_unique_id_configured()
+ return self.async_create_entry(
+ title=(
+ user_input.get(
+ CONF_NAME,
+ (
+ f"{DEFAULT_NAME}: {user_input[CONF_ORIGIN]} -> "
+ f"{user_input[CONF_DESTINATION]}"
+ ),
+ )
+ ),
+ data=user_input,
+ )
+
+ # If we get here, it's because we couldn't connect
+ errors["base"] = "cannot_connect"
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_ORIGIN): cv.string,
+ vol.Required(CONF_DESTINATION): cv.string,
+ vol.Required(CONF_REGION): vol.In(REGIONS),
+ }
+ ),
+ errors=errors,
+ )
+
+ async_step_import = async_step_user
diff --git a/homeassistant/components/waze_travel_time/const.py b/homeassistant/components/waze_travel_time/const.py
new file mode 100644
index 00000000000000..1b89fd5e28249f
--- /dev/null
+++ b/homeassistant/components/waze_travel_time/const.py
@@ -0,0 +1,40 @@
+"""Constants for waze_travel_time."""
+from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC
+
+DOMAIN = "waze_travel_time"
+
+ATTR_DESTINATION = "destination"
+ATTR_DURATION = "duration"
+ATTR_DISTANCE = "distance"
+ATTR_ORIGIN = "origin"
+ATTR_ROUTE = "route"
+
+ATTRIBUTION = "Powered by Waze"
+
+CONF_DESTINATION = "destination"
+CONF_ORIGIN = "origin"
+CONF_INCL_FILTER = "incl_filter"
+CONF_EXCL_FILTER = "excl_filter"
+CONF_REALTIME = "realtime"
+CONF_UNITS = "units"
+CONF_VEHICLE_TYPE = "vehicle_type"
+CONF_AVOID_TOLL_ROADS = "avoid_toll_roads"
+CONF_AVOID_SUBSCRIPTION_ROADS = "avoid_subscription_roads"
+CONF_AVOID_FERRIES = "avoid_ferries"
+
+DEFAULT_NAME = "Waze Travel Time"
+DEFAULT_REALTIME = True
+DEFAULT_VEHICLE_TYPE = "car"
+DEFAULT_AVOID_TOLL_ROADS = False
+DEFAULT_AVOID_SUBSCRIPTION_ROADS = False
+DEFAULT_AVOID_FERRIES = False
+
+ICON = "mdi:car"
+
+UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL]
+
+REGIONS = ["US", "NA", "EU", "IL", "AU"]
+VEHICLE_TYPES = ["car", "taxi", "motorcycle"]
+
+# Attempt to find entity_id without finding address with period.
+ENTITY_ID_PATTERN = "(? None:
+ """Set up a Waze travel time sensor entry."""
+ defaults = {
+ CONF_REALTIME: DEFAULT_REALTIME,
+ CONF_VEHICLE_TYPE: DEFAULT_VEHICLE_TYPE,
+ CONF_UNITS: hass.config.units.name,
+ CONF_AVOID_FERRIES: DEFAULT_AVOID_FERRIES,
+ CONF_AVOID_SUBSCRIPTION_ROADS: DEFAULT_AVOID_SUBSCRIPTION_ROADS,
+ CONF_AVOID_TOLL_ROADS: DEFAULT_AVOID_TOLL_ROADS,
+ }
+ name = None
+ if not config_entry.options:
+ new_data = config_entry.data.copy()
+ name = new_data.pop(CONF_NAME, None)
+ options = {}
+ for key in [
+ CONF_INCL_FILTER,
+ CONF_EXCL_FILTER,
+ CONF_REALTIME,
+ CONF_VEHICLE_TYPE,
+ CONF_AVOID_TOLL_ROADS,
+ CONF_AVOID_SUBSCRIPTION_ROADS,
+ CONF_AVOID_FERRIES,
+ CONF_UNITS,
+ ]:
+ if key in new_data:
+ options[key] = new_data.pop(key)
+ elif key in defaults:
+ options[key] = defaults[key]
+
+ hass.config_entries.async_update_entry(
+ config_entry, data=new_data, options=options
+ )
+
+ destination = config_entry.data[CONF_DESTINATION]
+ origin = config_entry.data[CONF_ORIGIN]
+ region = config_entry.data[CONF_REGION]
+ name = name or f"{DEFAULT_NAME}: {origin} -> {destination}"
+
+ if not await hass.async_add_executor_job(
+ is_valid_config_entry, hass, _LOGGER, origin, destination, region
+ ):
+ raise ConfigEntryNotReady
data = WazeTravelTimeData(
None,
None,
region,
- incl_filter,
- excl_filter,
- realtime,
- units,
- vehicle_type,
- avoid_toll_roads,
- avoid_subscription_roads,
- avoid_ferries,
+ config_entry,
)
- sensor = WazeTravelTime(name, origin, destination, data)
+ sensor = WazeTravelTime(config_entry.unique_id, name, origin, destination, data)
- add_entities([sensor])
+ async_add_entities([sensor], False)
- # Wait until start event is sent to load this component.
- hass.bus.listen_once(EVENT_HOMEASSISTANT_START, lambda _: sensor.update())
-
-def _get_location_from_attributes(state):
- """Get the lat/long string from an states attributes."""
- attr = state.attributes
- return "{},{}".format(attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE))
-
-
-class WazeTravelTime(Entity):
+class WazeTravelTime(SensorEntity):
"""Representation of a Waze travel time sensor."""
- def __init__(self, name, origin, destination, waze_data):
+ def __init__(self, unique_id, name, origin, destination, waze_data):
"""Initialize the Waze travel time sensor."""
- self._name = name
+ self._unique_id = unique_id
self._waze_data = waze_data
+ self._name = name
self._state = None
self._origin_entity_id = None
self._destination_entity_id = None
-
- # Attempt to find entity_id without finding address with period.
- pattern = "(? None:
+ """Handle when entity is added."""
+ if self.hass.state != CoreState.running:
+ self.hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_START, self.first_update
+ )
+ else:
+ await self.first_update()
+
@property
def name(self):
"""Return the name of the sensor."""
@@ -176,7 +220,7 @@ def icon(self):
return ICON
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the last update."""
if self._waze_data.duration is None:
return None
@@ -189,150 +233,118 @@ def device_state_attributes(self):
res[ATTR_DESTINATION] = self._waze_data.destination
return res
- def _get_location_from_entity(self, entity_id):
- """Get the location from the entity_id."""
- state = self.hass.states.get(entity_id)
-
- if state is None:
- _LOGGER.error("Unable to find entity %s", entity_id)
- return None
-
- # Check if the entity has location attributes.
- if location.has_location(state):
- _LOGGER.debug("Getting %s location", entity_id)
- return _get_location_from_attributes(state)
-
- # Check if device is inside a zone.
- zone_state = self.hass.states.get(f"zone.{state.state}")
- if location.has_location(zone_state):
- _LOGGER.debug(
- "%s is in %s, getting zone location", entity_id, zone_state.entity_id
- )
- return _get_location_from_attributes(zone_state)
-
- # If zone was not found in state then use the state as the location.
- if entity_id.startswith("sensor."):
- return state.state
-
- # When everything fails just return nothing.
- return None
-
- def _resolve_zone(self, friendly_name):
- """Get a lat/long from a zones friendly_name."""
- states = self.hass.states.all()
- for state in states:
- if state.domain == "zone" and state.name == friendly_name:
- return _get_location_from_attributes(state)
-
- return friendly_name
+ async def first_update(self, _=None):
+ """Run first update and write state."""
+ await self.hass.async_add_executor_job(self.update)
+ self.async_write_ha_state()
def update(self):
"""Fetch new state data for the sensor."""
_LOGGER.debug("Fetching Route for %s", self._name)
# Get origin latitude and longitude from entity_id.
if self._origin_entity_id is not None:
- self._waze_data.origin = self._get_location_from_entity(
- self._origin_entity_id
+ self._waze_data.origin = get_location_from_entity(
+ self.hass, _LOGGER, self._origin_entity_id
)
# Get destination latitude and longitude from entity_id.
if self._destination_entity_id is not None:
- self._waze_data.destination = self._get_location_from_entity(
- self._destination_entity_id
+ self._waze_data.destination = get_location_from_entity(
+ self.hass, _LOGGER, self._destination_entity_id
)
# Get origin from zone name.
- self._waze_data.origin = self._resolve_zone(self._waze_data.origin)
+ self._waze_data.origin = resolve_zone(self.hass, self._waze_data.origin)
# Get destination from zone name.
- self._waze_data.destination = self._resolve_zone(self._waze_data.destination)
+ self._waze_data.destination = resolve_zone(
+ self.hass, self._waze_data.destination
+ )
self._waze_data.update()
+ @property
+ def device_info(self) -> dict[str, Any] | None:
+ """Return device specific attributes."""
+ return {
+ "name": "Waze",
+ "identifiers": {(DOMAIN, DOMAIN)},
+ "entry_type": "service",
+ }
+
+ @property
+ def unique_id(self) -> str:
+ """Return unique ID of entity."""
+ return self._unique_id
+
class WazeTravelTimeData:
"""WazeTravelTime Data object."""
- def __init__(
- self,
- origin,
- destination,
- region,
- include,
- exclude,
- realtime,
- units,
- vehicle_type,
- avoid_toll_roads,
- avoid_subscription_roads,
- avoid_ferries,
- ):
+ def __init__(self, origin, destination, region, config_entry):
"""Set up WazeRouteCalculator."""
-
- self._calc = WazeRouteCalculator
-
self.origin = origin
self.destination = destination
self.region = region
- self.include = include
- self.exclude = exclude
- self.realtime = realtime
- self.units = units
+ self.config_entry = config_entry
self.duration = None
self.distance = None
self.route = None
- self.avoid_toll_roads = avoid_toll_roads
- self.avoid_subscription_roads = avoid_subscription_roads
- self.avoid_ferries = avoid_ferries
-
- # Currently WazeRouteCalc only supports PRIVATE, TAXI, MOTORCYCLE.
- if vehicle_type.upper() == "CAR":
- # Empty means PRIVATE for waze which translates to car.
- self.vehicle_type = ""
- else:
- self.vehicle_type = vehicle_type.upper()
def update(self):
"""Update WazeRouteCalculator Sensor."""
if self.origin is not None and self.destination is not None:
+ # Grab options on every update
+ incl_filter = self.config_entry.options.get(CONF_INCL_FILTER)
+ excl_filter = self.config_entry.options.get(CONF_EXCL_FILTER)
+ realtime = self.config_entry.options[CONF_REALTIME]
+ vehicle_type = self.config_entry.options[CONF_VEHICLE_TYPE]
+ vehicle_type = "" if vehicle_type.upper() == "CAR" else vehicle_type.upper()
+ avoid_toll_roads = self.config_entry.options[CONF_AVOID_TOLL_ROADS]
+ avoid_subscription_roads = self.config_entry.options[
+ CONF_AVOID_SUBSCRIPTION_ROADS
+ ]
+ avoid_ferries = self.config_entry.options[CONF_AVOID_FERRIES]
+ units = self.config_entry.options[CONF_UNITS]
+
try:
- params = self._calc.WazeRouteCalculator(
+ params = WazeRouteCalculator(
self.origin,
self.destination,
self.region,
- self.vehicle_type,
- self.avoid_toll_roads,
- self.avoid_subscription_roads,
- self.avoid_ferries,
+ vehicle_type,
+ avoid_toll_roads,
+ avoid_subscription_roads,
+ avoid_ferries,
)
- routes = params.calc_all_routes_info(real_time=self.realtime)
+ routes = params.calc_all_routes_info(real_time=realtime)
- if self.include is not None:
+ if incl_filter is not None:
routes = {
k: v
for k, v in routes.items()
- if self.include.lower() in k.lower()
+ if incl_filter.lower() in k.lower()
}
- if self.exclude is not None:
+ if excl_filter is not None:
routes = {
k: v
for k, v in routes.items()
- if self.exclude.lower() not in k.lower()
+ if excl_filter.lower() not in k.lower()
}
route = list(routes)[0]
self.duration, distance = routes[route]
- if self.units == CONF_UNIT_SYSTEM_IMPERIAL:
+ if units == CONF_UNIT_SYSTEM_IMPERIAL:
# Convert to miles.
self.distance = distance / 1.609
else:
self.distance = distance
self.route = route
- except self._calc.WRCError as exp:
+ except WRCError as exp:
_LOGGER.warning("Error on retrieving data: %s", exp)
return
except KeyError:
diff --git a/homeassistant/components/waze_travel_time/strings.json b/homeassistant/components/waze_travel_time/strings.json
new file mode 100644
index 00000000000000..082ee31db7391f
--- /dev/null
+++ b/homeassistant/components/waze_travel_time/strings.json
@@ -0,0 +1,38 @@
+{
+ "title": "Waze Travel Time",
+ "config": {
+ "step": {
+ "user": {
+ "description": "For Origin and Destination, enter the address or the GPS coordinates of the location (GPS coordinates has to be separated by a comma). You can also enter an entity id which provides this information in its state, an entity id with latitude and longitude attributes, or zone friendly name.",
+ "data": {
+ "origin": "Origin",
+ "destination": "Destination",
+ "region": "Region"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_location%]"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "description": "The `substring` inputs will allow you to force the integration to use a particular route or avoid a particular route in its time travel calculation.",
+ "data": {
+ "units": "Units",
+ "vehicle_type": "Vehicle Type",
+ "incl_filter": "Substring in Description of Selected Route",
+ "excl_filter": "Substring NOT in Description of Selected Route",
+ "realtime": "Realtime Travel Time?",
+ "avoid_toll_roads": "Avoid Toll Roads?",
+ "avoid_ferries": "Avoid Ferries?",
+ "avoid_subscription_roads": "Avoid Roads Needing a Vignette / Subscription?"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/waze_travel_time/translations/ca.json b/homeassistant/components/waze_travel_time/translations/ca.json
new file mode 100644
index 00000000000000..f8f1db711c0ad4
--- /dev/null
+++ b/homeassistant/components/waze_travel_time/translations/ca.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "destination": "Destinaci\u00f3",
+ "origin": "Origen",
+ "region": "Regi\u00f3"
+ },
+ "description": "A origen i destinaci\u00f3, introdueix l'adre\u00e7a o les coordenades GPS de la ubicaci\u00f3 (les coordenades GPS han d'estar separades per una coma). Tamb\u00e9 pots introduir un ID d'entitat (que contingui aquesta informaci\u00f3 en el seu estat), un ID d'entitat amb atributs de latitud i longitud o un sobrenom de zona."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid_ferries": "Evita ferris?",
+ "avoid_subscription_roads": "Evita carreteres que necessiten tiquet/subscripci\u00f3?",
+ "avoid_toll_roads": "Evita peatges?",
+ "excl_filter": "Subcadena NO present a la descripci\u00f3 de la ruta seleccionada",
+ "incl_filter": "Subcadena a la descripci\u00f3 de la ruta seleccionada",
+ "realtime": "Temps de viatge en temps real?",
+ "units": "Unitats",
+ "vehicle_type": "Tipus de vehicle"
+ },
+ "description": "Les entrades de 'subcadena' et permeten for\u00e7ar que la integraci\u00f3 utilitzi o eviti una ruta espec\u00edfica durant el c\u00e0lcul del temps de viatge."
+ }
+ }
+ },
+ "title": "Temps de viatge de Waze"
+}
\ No newline at end of file
diff --git a/homeassistant/components/waze_travel_time/translations/de.json b/homeassistant/components/waze_travel_time/translations/de.json
new file mode 100644
index 00000000000000..f5586b3d80d953
--- /dev/null
+++ b/homeassistant/components/waze_travel_time/translations/de.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Standort ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "destination": "Zielort",
+ "origin": "Startort",
+ "region": "Region"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/waze_travel_time/translations/en.json b/homeassistant/components/waze_travel_time/translations/en.json
new file mode 100644
index 00000000000000..31fd8d0793feef
--- /dev/null
+++ b/homeassistant/components/waze_travel_time/translations/en.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Location is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "destination": "Destination",
+ "origin": "Origin",
+ "region": "Region"
+ },
+ "description": "For Origin and Destination, enter the address or the GPS coordinates of the location (GPS coordinates has to be separated by a comma). You can also enter an entity id which provides this information in its state, an entity id with latitude and longitude attributes, or zone friendly name."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid_ferries": "Avoid Ferries?",
+ "avoid_subscription_roads": "Avoid Roads Needing a Vignette / Subscription?",
+ "avoid_toll_roads": "Avoid Toll Roads?",
+ "excl_filter": "Substring NOT in Description of Selected Route",
+ "incl_filter": "Substring in Description of Selected Route",
+ "realtime": "Realtime Travel Time?",
+ "units": "Units",
+ "vehicle_type": "Vehicle Type"
+ },
+ "description": "The `substring` inputs will allow you to force the integration to use a particular route or avoid a particular route in its time travel calculation."
+ }
+ }
+ },
+ "title": "Waze Travel Time"
+}
\ No newline at end of file
diff --git a/homeassistant/components/waze_travel_time/translations/et.json b/homeassistant/components/waze_travel_time/translations/et.json
new file mode 100644
index 00000000000000..a2ff51e1bb9420
--- /dev/null
+++ b/homeassistant/components/waze_travel_time/translations/et.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "See asukoht on juba seadistatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "destination": "Sihtkoht",
+ "origin": "L\u00e4htekoht",
+ "region": "Piirkond"
+ },
+ "description": "L\u00e4htekoha ja sihtkoha jaoks sisesta asukoha aadress v\u00f5i GPS-koordinaadid (GPS-koordinaadid tuleb eraldada komaga). Samuti saaf sisestada \u00fcksuse ID, mis annab selle teabe olekus, \u00fcksuse ID laius- ja pikkuskraadi atribuutidega v\u00f5i tsoonis\u00f5braliku nime."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid_ferries": "V\u00e4ltida parvlaevu?",
+ "avoid_subscription_roads": "V\u00e4ltida teid mis vajavad vinjetti / ettemaksu?",
+ "avoid_toll_roads": "V\u00e4ltida tasulisi teid?",
+ "excl_filter": "Substring EI ole valitud teekonna kirjelduses",
+ "incl_filter": "Alamstring valitud teekonna kirjelduses",
+ "realtime": "Travel Time reaalajas?",
+ "units": "\u00dchikud",
+ "vehicle_type": "S\u00f5iduki t\u00fc\u00fcp"
+ },
+ "description": "\"Alamstringi\" sisendid v\u00f5imaldavad sundida sidumist kasutama kindlat teekondai v\u00f5i v\u00e4ltima kindlat teekonda aja arvutamisel."
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/waze_travel_time/translations/fa.json b/homeassistant/components/waze_travel_time/translations/fa.json
new file mode 100644
index 00000000000000..b6abec0d8578b7
--- /dev/null
+++ b/homeassistant/components/waze_travel_time/translations/fa.json
@@ -0,0 +1,3 @@
+{
+ "title": "\u0632\u0645\u0627\u0646 \u0633\u0641\u0631 \u0628\u0627 Waze"
+}
\ No newline at end of file
diff --git a/homeassistant/components/waze_travel_time/translations/hu.json b/homeassistant/components/waze_travel_time/translations/hu.json
new file mode 100644
index 00000000000000..94e5f96814e8dd
--- /dev/null
+++ b/homeassistant/components/waze_travel_time/translations/hu.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A hely m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "destination": "\u00c9rkez\u00e9s helye",
+ "origin": "Indul\u00e1s helye",
+ "region": "R\u00e9gi\u00f3"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid_ferries": "Ker\u00fclje kompokat?",
+ "avoid_subscription_roads": "Ker\u00fclje el az utakat, amelyekre matrica / el\u0151fizet\u00e9s sz\u00fcks\u00e9ges?",
+ "avoid_toll_roads": "Ker\u00fclje a fizet\u0151s utakat?",
+ "realtime": "Val\u00f3s idej\u0171 utaz\u00e1si id\u0151?",
+ "vehicle_type": "J\u00e1rm\u0171 t\u00edpus"
+ }
+ }
+ }
+ },
+ "title": "Waze Travel Time"
+}
\ No newline at end of file
diff --git a/homeassistant/components/waze_travel_time/translations/it.json b/homeassistant/components/waze_travel_time/translations/it.json
new file mode 100644
index 00000000000000..ce109b3751cf05
--- /dev/null
+++ b/homeassistant/components/waze_travel_time/translations/it.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La posizione \u00e8 gi\u00e0 configurata"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "destination": "Destinazione",
+ "origin": "Origine",
+ "region": "Area geografica"
+ },
+ "description": "Per Origine e Destinazione, inserisci l'indirizzo o le coordinate GPS della posizione (le coordinate GPS devono essere separate da una virgola). Puoi anche inserire un ID entit\u00e0 che fornisce queste informazioni nel suo stato, un ID entit\u00e0 con attributi di latitudine e longitudine o il nome assegnato alla zona."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid_ferries": "Evitare i traghetti?",
+ "avoid_subscription_roads": "Evitare le strade che richiedono una vignetta/abbonamento?",
+ "avoid_toll_roads": "Evitare le strade a pedaggio?",
+ "excl_filter": "Sottostringa NON nella descrizione del percorso selezionato",
+ "incl_filter": "Sottostringa nella descrizione del percorso selezionato",
+ "realtime": "Tempo di viaggio in tempo reale?",
+ "units": "Unit\u00e0",
+ "vehicle_type": "Tipo di veicolo"
+ },
+ "description": "Gli input 'sottostringa' consentono di forzare l'integrazione a utilizzare o evitare un percorso particolare nel calcolo del tempo di viaggio."
+ }
+ }
+ },
+ "title": "Tempo di viaggio di Waze"
+}
\ No newline at end of file
diff --git a/homeassistant/components/waze_travel_time/translations/ko.json b/homeassistant/components/waze_travel_time/translations/ko.json
new file mode 100644
index 00000000000000..3596754ca04076
--- /dev/null
+++ b/homeassistant/components/waze_travel_time/translations/ko.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "destination": "\ubaa9\uc801\uc9c0",
+ "origin": "\ucd9c\ubc1c\uc9c0",
+ "region": "\uc9c0\uc5ed"
+ },
+ "description": "\ucd9c\ubc1c\uc9c0 \ubc0f \ub3c4\ucc29\uc9c0\uc758 \uacbd\uc6b0 \uc704\uce58\uc758 \uc8fc\uc18c \ub610\ub294 GPS \uc88c\ud45c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. (GPS \uc88c\ud45c\ub294 \uc27c\ud45c\ub85c \uad6c\ubd84\ud574\uc57c \ud569\ub2c8\ub2e4) \ub610\ub294 \uc774\ub7ec\ud55c \uc815\ubcf4\ub97c \ud574\ub2f9 \uc0c1\ud0dc\ub85c \uc81c\uacf5\ud558\ub294 \uad6c\uc131\uc694\uc18c ID\ub098 \uc704\ub3c4 \ubc0f \uacbd\ub3c4 \uc18d\uc131\uc744 \uac00\uc9c4 \uad6c\uc131\uc694\uc18c ID \ub610\ub294 \uc9c0\uc5ed \uc774\ub984\uc744 \uc785\ub825\ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/waze_travel_time/translations/nl.json b/homeassistant/components/waze_travel_time/translations/nl.json
new file mode 100644
index 00000000000000..4d7bee4ad4db35
--- /dev/null
+++ b/homeassistant/components/waze_travel_time/translations/nl.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Locatie is al geconfigureerd."
+ },
+ "error": {
+ "cannot_connect": "Kan geen verbinding maken"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "destination": "Bestemming",
+ "origin": "Vertrekpunt",
+ "region": "Regio"
+ },
+ "description": "Voor Vertrekpunt en Bestemming voert u het adres of de GPS-co\u00f6rdinaten van de locatie in (GPS-co\u00f6rdinaten moeten worden gescheiden door een komma). U kunt ook een entiteits-id invoeren die deze informatie in zijn status geeft, een entiteits-id met lengte- en breedtegraadattributen, of een zone-vriendelijke naam."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid_ferries": "Veerboten vermijden?",
+ "avoid_subscription_roads": "Vermijd wegen die een vignet / abonnement nodig hebben?",
+ "avoid_toll_roads": "Tolwegen vermijden?",
+ "excl_filter": "Substring NIET in beschrijving van geselecteerde route",
+ "incl_filter": "Substring in beschrijving van geselecteerde route",
+ "realtime": "Realtime reistijd?",
+ "units": "Eenheden",
+ "vehicle_type": "Voertuigtype"
+ },
+ "description": "Met de 'substring'-invoer kunt u de integratie forceren om een bepaalde route te gebruiken of een bepaalde route te vermijden in de tijdreisberekening."
+ }
+ }
+ },
+ "title": "Waze reistijd"
+}
\ No newline at end of file
diff --git a/homeassistant/components/waze_travel_time/translations/no.json b/homeassistant/components/waze_travel_time/translations/no.json
new file mode 100644
index 00000000000000..7ae2bf8d418129
--- /dev/null
+++ b/homeassistant/components/waze_travel_time/translations/no.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Plasseringen er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "destination": "Destinasjon",
+ "origin": "Opprinnelse",
+ "region": "Region"
+ },
+ "description": "For opprinnelse og destinasjon, skriv inn adressen eller GPS-koordinatene til stedet (GPS-koordinatene m\u00e5 v\u00e6re atskilt med komma). Du kan ogs\u00e5 angi en enhets-ID som gir denne informasjonen i sin tilstand, en enhets-id med breddegrad og lengdegrad eller attributt navn."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid_ferries": "Unng\u00e5 ferger?",
+ "avoid_subscription_roads": "Unng\u00e5 veier trenger en vignett / abonnement?",
+ "avoid_toll_roads": "Unng\u00e5 bomveier?",
+ "excl_filter": "Delstreng IKKE i beskrivelse av valgt rute",
+ "incl_filter": "Delstreng i Beskrivelse av valgt rute",
+ "realtime": "Reisetid i sanntid?",
+ "units": "Enheter",
+ "vehicle_type": "Kj\u00f8ret\u00f8y Type"
+ },
+ "description": "`Substring`-inngangene lar deg tvinge integrasjonen til \u00e5 bruke en bestemt rute eller unng\u00e5 en bestemt rute i beregningen av tidsreiser."
+ }
+ }
+ },
+ "title": "Waze reisetid"
+}
\ No newline at end of file
diff --git a/homeassistant/components/waze_travel_time/translations/pl.json b/homeassistant/components/waze_travel_time/translations/pl.json
new file mode 100644
index 00000000000000..e46469f59d7b64
--- /dev/null
+++ b/homeassistant/components/waze_travel_time/translations/pl.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Lokalizacja jest ju\u017c skonfigurowana"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "destination": "Punkt docelowy",
+ "origin": "Punkt pocz\u0105tkowy",
+ "region": "Region"
+ },
+ "description": "W polu Punkt pocz\u0105tkowy i Punkt docelowy, wprowad\u017a adres lub wsp\u00f3\u0142rz\u0119dne GPS (wsp\u00f3\u0142rz\u0119dne GPS musz\u0105 by\u0107 oddzielone przecinkiem). Mo\u017cesz r\u00f3wnie\u017c wprowadzi\u0107 identyfikator encji (entity_id), kt\u00f3ry dostarcza te informacje w swoim stanie, identyfikator jednostki z atrybutami szeroko\u015bci i d\u0142ugo\u015bci geograficznej lub przyjazn\u0105 nazw\u0119 strefy."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid_ferries": "Unikaj prom\u00f3w?",
+ "avoid_subscription_roads": "Unikaj dr\u00f3g wymagaj\u0105cych winiety / abonamentu?",
+ "avoid_toll_roads": "Unikaj dr\u00f3g p\u0142atnych?",
+ "excl_filter": "NIE MA podci\u0105gu w opisie wybranej trasy",
+ "incl_filter": "Podci\u0105g w opisie wybranej trasy",
+ "realtime": "Czas podr\u00f3\u017cy w czasie rzeczywistym?",
+ "units": "Jednostki",
+ "vehicle_type": "Typ pojazdu"
+ },
+ "description": "Dane wej\u015bciowe \u201epodci\u0105gu\u201d pozwol\u0105 Ci wymusi\u0107 na integracji u\u017cycie okre\u015blonej trasy lub omini\u0119cie okre\u015blonej trasy w obliczeniach dotycz\u0105cych czasu podr\u00f3\u017cy."
+ }
+ }
+ },
+ "title": "Czas podr\u00f3\u017cy Waze"
+}
\ No newline at end of file
diff --git a/homeassistant/components/waze_travel_time/translations/ru.json b/homeassistant/components/waze_travel_time/translations/ru.json
new file mode 100644
index 00000000000000..5d0c0990c28b20
--- /dev/null
+++ b/homeassistant/components/waze_travel_time/translations/ru.json
@@ -0,0 +1,38 @@
+{
+ "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": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "destination": "\u041f\u0443\u043d\u043a\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f",
+ "origin": "\u041f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f",
+ "region": "\u0420\u0435\u0433\u0438\u043e\u043d"
+ },
+ "description": "\u041f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0438 \u043f\u0443\u043d\u043a\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0432 \u0432\u0438\u0434\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u0438\u043b\u0438 GPS-\u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442 (\u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u044b \u0437\u0430\u043f\u044f\u0442\u043e\u0439). \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c ID \u043e\u0431\u044a\u0435\u043a\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u044d\u0442\u0443 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0432 \u0441\u0432\u043e\u0435\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 \u0438\u043b\u0438 \u0432 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430\u0445, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0437\u043e\u043d \u0438\u0437 Home Assistant."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid_ferries": "\u0418\u0437\u0431\u0435\u0433\u0430\u0442\u044c \u043f\u0430\u0440\u043e\u043c\u043e\u0432?",
+ "avoid_subscription_roads": "\u0418\u0437\u0431\u0435\u0433\u0430\u0439\u0442\u0435 \u0434\u043e\u0440\u043e\u0433, \u0442\u0440\u0435\u0431\u0443\u044e\u0449\u0438\u0445 \u0432\u0438\u043d\u044c\u0435\u0442\u043a\u0438/\u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438?",
+ "avoid_toll_roads": "\u0418\u0437\u0431\u0435\u0433\u0430\u0442\u044c \u043f\u043b\u0430\u0442\u043d\u044b\u0445 \u0434\u043e\u0440\u043e\u0433?",
+ "excl_filter": "\u041f\u043e\u0434\u0441\u0442\u0440\u043e\u043a\u0430 \u041d\u0415 \u0432 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430",
+ "incl_filter": "\u041f\u043e\u0434\u0441\u0442\u0440\u043e\u043a\u0430 \u0432 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430",
+ "realtime": "\u0412\u0440\u0435\u043c\u044f \u0432 \u043f\u0443\u0442\u0438 \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438?",
+ "units": "\u0415\u0434\u0438\u043d\u0438\u0446\u044b \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f",
+ "vehicle_type": "\u0422\u0438\u043f \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u043e\u0433\u043e \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430"
+ },
+ "description": "\u0412\u0445\u043e\u0434\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 `\u043f\u043e\u0434\u0441\u0442\u0440\u043e\u043a\u0430` \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u044e\u0442 \u043f\u0440\u0438\u043d\u0443\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u0438\u043b\u0438 \u0438\u0437\u0431\u0435\u0433\u0430\u0442\u044c \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430 \u043f\u0440\u0438 \u0440\u0430\u0441\u0447\u0435\u0442\u0435 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0432 \u043f\u0443\u0442\u0438."
+ }
+ }
+ },
+ "title": "Waze Travel Time"
+}
\ No newline at end of file
diff --git a/homeassistant/components/waze_travel_time/translations/zh-Hant.json b/homeassistant/components/waze_travel_time/translations/zh-Hant.json
new file mode 100644
index 00000000000000..a0f71b51ae2dac
--- /dev/null
+++ b/homeassistant/components/waze_travel_time/translations/zh-Hant.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "destination": "\u76ee\u7684\u5730",
+ "origin": "\u51fa\u767c\u5730",
+ "region": "\u5340\u57df"
+ },
+ "description": "\u65bc\u51fa\u767c\u5730\u8207\u76ee\u7684\u5730\u3001\u8f38\u5165\u5730\u5740\u6216 GPS \u5ea7\u6a19\uff08GPS \u5ea7\u6a19\u4ee5\u9017\u865f\u5206\u9694\uff09\u3002\u540c\u6642\u4e5f\u53ef\u4ee5\u8f38\u5165\u5305\u542b\u72c0\u614b\u7684\u5be6\u9ad4 ID\u3001\u5305\u542b\u7d93\u7def\u5ea6\u5c6c\u6027\u7684\u5be6\u9ad4\u3001\u6216\u5340\u57df\u7684\u540d\u7a31\u3002"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "avoid_ferries": "\u907f\u958b\u6e21\u8f2a\uff1f",
+ "avoid_subscription_roads": "\u907f\u958b\u9700\u8981\u5716\u5b9a\u6a19\u793a / \u8a02\u95b1\u8def\u7dda\uff1f",
+ "avoid_toll_roads": "\u907f\u958b\u6536\u8cbb\u9053\u8def\uff1f",
+ "excl_filter": "\u6240\u9078\u64c7\u8def\u7dda\u63cf\u8ff0\u4e0d\u5305\u542b Substring",
+ "incl_filter": "\u6240\u9078\u64c7\u8def\u7dda\u63cf\u8ff0\u5305\u542b Substring",
+ "realtime": "\u5373\u6642\u65c5\u7a0b\u6642\u9593\uff1f",
+ "units": "\u55ae\u4f4d",
+ "vehicle_type": "\u8eca\u8f1b\u985e\u578b"
+ },
+ "description": "`substring` \u8f38\u5165\u53ef\u4f9b\u5f37\u5236\u6574\u5408\u3001\u65bc\u8a08\u7b97\u65c5\u7a0b\u6642\u9593\u6642\uff0c\u4f7f\u7528\u7279\u5b9a\u8def\u7dda\u6216\u907f\u958b\u4f7f\u7528\u7279\u5b9a\u8def\u7dda\u3002"
+ }
+ }
+ },
+ "title": "Waze \u65c5\u7a0b\u6642\u9593"
+}
\ No newline at end of file
diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py
index 5127dae1102274..da66c354d5a631 100644
--- a/homeassistant/components/weather/__init__.py
+++ b/homeassistant/components/weather/__init__.py
@@ -1,6 +1,7 @@
"""Weather component that handles meteorological data for your location."""
from datetime import timedelta
import logging
+from typing import final
from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, TEMP_CELSIUS
from homeassistant.helpers.config_validation import ( # noqa: F401
@@ -138,6 +139,7 @@ def precision(self):
else PRECISION_WHOLE
)
+ @final
@property
def state_attributes(self):
"""Return the state attributes."""
diff --git a/homeassistant/components/weather/group.py b/homeassistant/components/weather/group.py
index 4741f8a3b548cd..2ac081496cdd90 100644
--- a/homeassistant/components/weather/group.py
+++ b/homeassistant/components/weather/group.py
@@ -2,13 +2,12 @@
from homeassistant.components.group import GroupIntegrationRegistry
-from homeassistant.core import callback
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import HomeAssistant, callback
@callback
def async_describe_on_off_states(
- hass: HomeAssistantType, registry: GroupIntegrationRegistry
+ hass: HomeAssistant, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.exclude_domain()
diff --git a/homeassistant/components/weather/translations/et.json b/homeassistant/components/weather/translations/et.json
index f035d37d62ea68..2de9158c085165 100644
--- a/homeassistant/components/weather/translations/et.json
+++ b/homeassistant/components/weather/translations/et.json
@@ -3,7 +3,7 @@
"_": {
"clear-night": "Selge \u00f6\u00f6",
"cloudy": "Pilves",
- "exceptional": "Erakordne",
+ "exceptional": "Ohtlikud olud",
"fog": "Udu",
"hail": "Rahe",
"lightning": "\u00c4ikeseline",
diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py
index 9c6dfe45e746cb..6d61f5d62dc8a0 100644
--- a/homeassistant/components/webhook/__init__.py
+++ b/homeassistant/components/webhook/__init__.py
@@ -89,7 +89,7 @@ async def async_handle_webhook(hass, webhook_id, request):
# Look at content to provide some context for received webhook
# Limit to 64 chars to avoid flooding the log
content = await request.content.read(64)
- _LOGGER.debug("%s...", content)
+ _LOGGER.debug("%s", content)
return Response(status=HTTP_OK)
try:
diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py
index 81c4c2813cd8e0..a82dd0251c9a33 100644
--- a/homeassistant/components/webhook/trigger.py
+++ b/homeassistant/components/webhook/trigger.py
@@ -17,7 +17,7 @@
)
-async def _handle_webhook(job, hass, webhook_id, request):
+async def _handle_webhook(job, trigger_id, hass, webhook_id, request):
"""Handle incoming webhook."""
result = {"platform": "webhook", "webhook_id": webhook_id}
@@ -28,18 +28,20 @@ async def _handle_webhook(job, hass, webhook_id, request):
result["query"] = request.query
result["description"] = "webhook"
+ result["id"] = trigger_id
hass.async_run_hass_job(job, {"trigger": result})
async def async_attach_trigger(hass, config, action, automation_info):
"""Trigger based on incoming webhooks."""
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
webhook_id = config.get(CONF_WEBHOOK_ID)
job = HassJob(action)
hass.components.webhook.async_register(
automation_info["domain"],
automation_info["name"],
webhook_id,
- partial(_handle_webhook, job),
+ partial(_handle_webhook, job, trigger_id),
)
@callback
diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py
index ebac158c452500..681c2acfe01219 100644
--- a/homeassistant/components/webostv/__init__.py
+++ b/homeassistant/components/webostv/__init__.py
@@ -1,25 +1,17 @@
"""Support for LG webOS Smart TV."""
import asyncio
+from contextlib import suppress
+import json
import logging
+import os
from aiopylgtv import PyLGTVCmdException, PyLGTVPairException, WebOsClient
+from sqlitedict import SqliteDict
import voluptuous as vol
from websockets.exceptions import ConnectionClosed
-from homeassistant.components.webostv.const import (
- ATTR_BUTTON,
- ATTR_COMMAND,
- ATTR_PAYLOAD,
- CONF_ON_ACTION,
- CONF_SOURCES,
- DEFAULT_NAME,
- DOMAIN,
- SERVICE_BUTTON,
- SERVICE_COMMAND,
- SERVICE_SELECT_SOUND_OUTPUT,
- WEBOSTV_CONFIG_FILE,
-)
from homeassistant.const import (
+ ATTR_COMMAND,
ATTR_ENTITY_ID,
CONF_CUSTOMIZE,
CONF_HOST,
@@ -30,7 +22,19 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
-from .const import ATTR_SOUND_OUTPUT
+from .const import (
+ ATTR_BUTTON,
+ ATTR_PAYLOAD,
+ ATTR_SOUND_OUTPUT,
+ CONF_ON_ACTION,
+ CONF_SOURCES,
+ DEFAULT_NAME,
+ DOMAIN,
+ SERVICE_BUTTON,
+ SERVICE_COMMAND,
+ SERVICE_SELECT_SOUND_OUTPUT,
+ WEBOSTV_CONFIG_FILE,
+)
CUSTOMIZE_SCHEMA = vol.Schema(
{vol.Optional(CONF_SOURCES, default=[]): vol.All(cv.ensure_list, [cv.string])}
@@ -101,13 +105,41 @@ async def async_service_handler(service):
return True
+def convert_client_keys(config_file):
+ """In case the config file contains JSON, convert it to a Sqlite config file."""
+ # Return early if config file is non-existing
+ if not os.path.isfile(config_file):
+ return
+
+ # Try to parse the file as being JSON
+ with open(config_file) as json_file:
+ try:
+ json_conf = json.load(json_file)
+ except (json.JSONDecodeError, UnicodeDecodeError):
+ json_conf = None
+
+ # If the file contains JSON, convert it to an Sqlite DB
+ if json_conf:
+ _LOGGER.warning("LG webOS TV client-key file is being migrated to Sqlite!")
+
+ # Clean the JSON file
+ os.remove(config_file)
+
+ # Write the data to the Sqlite DB
+ with SqliteDict(config_file) as conf:
+ for host, key in json_conf.items():
+ conf[host] = key
+ conf.commit()
+
+
async def async_setup_tv(hass, config, conf):
"""Set up a LG WebOS TV based on host parameter."""
host = conf[CONF_HOST]
config_file = hass.config.path(WEBOSTV_CONFIG_FILE)
+ await hass.async_add_executor_job(convert_client_keys, config_file)
- client = WebOsClient(host, config_file)
+ client = await WebOsClient.create(host, config_file)
hass.data[DOMAIN][host] = {"client": client}
if client.is_registered():
@@ -119,9 +151,7 @@ async def async_setup_tv(hass, config, conf):
async def async_connect(client):
"""Attempt a connection, but fail gracefully if tv is off for example."""
- try:
- await client.connect()
- except (
+ with suppress(
OSError,
ConnectionClosed,
ConnectionRefusedError,
@@ -130,7 +160,7 @@ async def async_connect(client):
PyLGTVPairException,
PyLGTVCmdException,
):
- pass
+ await client.connect()
async def async_setup_tv_finalize(hass, config, conf, client):
diff --git a/homeassistant/components/webostv/const.py b/homeassistant/components/webostv/const.py
index bea485a7d68202..9091491a29da98 100644
--- a/homeassistant/components/webostv/const.py
+++ b/homeassistant/components/webostv/const.py
@@ -4,7 +4,6 @@
DEFAULT_NAME = "LG webOS Smart TV"
ATTR_BUTTON = "button"
-ATTR_COMMAND = "command"
ATTR_PAYLOAD = "payload"
ATTR_SOUND_OUTPUT = "sound_output"
diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json
index acdee1d9ca9ee7..7773e9c4963329 100644
--- a/homeassistant/components/webostv/manifest.json
+++ b/homeassistant/components/webostv/manifest.json
@@ -2,7 +2,7 @@
"domain": "webostv",
"name": "LG webOS Smart TV",
"documentation": "https://www.home-assistant.io/integrations/webostv",
- "requirements": ["aiopylgtv==0.3.3"],
+ "requirements": ["aiopylgtv==0.4.0"],
"dependencies": ["configurator"],
"codeowners": ["@bendavid"]
}
diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py
index 4807d780a480d6..d94ab8a7c26683 100644
--- a/homeassistant/components/webostv/media_player.py
+++ b/homeassistant/components/webostv/media_player.py
@@ -1,5 +1,6 @@
"""Support for interface with an LG webOS Smart TV."""
import asyncio
+from contextlib import suppress
from datetime import timedelta
from functools import wraps
import logging
@@ -214,9 +215,7 @@ def update_sources(self):
async def async_update(self):
"""Connect."""
if not self._client.is_connected():
- try:
- await self._client.connect()
- except (
+ with suppress(
OSError,
ConnectionClosed,
ConnectionRefusedError,
@@ -225,7 +224,7 @@ async def async_update(self):
PyLGTVPairException,
PyLGTVCmdException,
):
- pass
+ await self._client.connect()
@property
def unique_id(self):
@@ -271,7 +270,7 @@ def source(self):
@property
def source_list(self):
"""List of available input sources."""
- return sorted(list(self._source_list))
+ return sorted(self._source_list)
@property
def media_content_type(self):
@@ -318,7 +317,7 @@ def supported_features(self):
return supported
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
if self._client.sound_output is None and self.state == STATE_OFF:
return {}
@@ -386,7 +385,7 @@ async def async_play_media(self, media_type, media_id, **kwargs):
_LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id)
if media_type == MEDIA_TYPE_CHANNEL:
- _LOGGER.debug("Searching channel...")
+ _LOGGER.debug("Searching channel")
partial_match_channel_id = None
perfect_match_channel_id = None
diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py
index 2d591455eaf1c5..e7b10e18889e94 100644
--- a/homeassistant/components/websocket_api/__init__.py
+++ b/homeassistant/components/websocket_api/__init__.py
@@ -1,14 +1,16 @@
"""WebSocket based API for Home Assistant."""
-from typing import Optional, Union, cast
+from __future__ import annotations
+
+from typing import cast
import voluptuous as vol
from homeassistant.core import HomeAssistant, callback
from homeassistant.loader import bind_hass
-from . import commands, connection, const, decorators, http, messages # noqa
-from .connection import ActiveConnection # noqa
-from .const import ( # noqa
+from . import commands, connection, const, decorators, http, messages # noqa: F401
+from .connection import ActiveConnection # noqa: F401
+from .const import ( # noqa: F401
ERR_HOME_ASSISTANT_ERROR,
ERR_INVALID_FORMAT,
ERR_NOT_FOUND,
@@ -19,13 +21,13 @@
ERR_UNKNOWN_COMMAND,
ERR_UNKNOWN_ERROR,
)
-from .decorators import ( # noqa
+from .decorators import ( # noqa: F401
async_response,
require_admin,
websocket_command,
ws_require_user,
)
-from .messages import ( # noqa
+from .messages import ( # noqa: F401
BASE_COMMAND_MESSAGE_SCHEMA,
error_message,
event_message,
@@ -43,9 +45,9 @@
@callback
def async_register_command(
hass: HomeAssistant,
- command_or_handler: Union[str, const.WebSocketCommandHandler],
- handler: Optional[const.WebSocketCommandHandler] = None,
- schema: Optional[vol.Schema] = None,
+ command_or_handler: str | const.WebSocketCommandHandler,
+ handler: const.WebSocketCommandHandler | None = None,
+ schema: vol.Schema | None = None,
) -> None:
"""Register a websocket command."""
# pylint: disable=protected-access
diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py
index 77521c1ed98e94..33a3370366821a 100644
--- a/homeassistant/components/websocket_api/commands.py
+++ b/homeassistant/components/websocket_api/commands.py
@@ -4,6 +4,7 @@
import voluptuous as vol
from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_READ
+from homeassistant.bootstrap import SIGNAL_BOOTSTRAP_INTEGRATONS
from homeassistant.components.websocket_api.const import ERR_NOT_FOUND
from homeassistant.const import EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL
from homeassistant.core import DOMAIN as HASS_DOMAIN, callback
@@ -13,11 +14,12 @@
TemplateError,
Unauthorized,
)
-from homeassistant.helpers import config_validation as cv, entity
+from homeassistant.helpers import config_validation as cv, entity, template
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import TrackTemplate, async_track_template_result
from homeassistant.helpers.service import async_get_all_descriptions
-from homeassistant.helpers.template import Template
from homeassistant.loader import IntegrationNotFound, async_get_integration
+from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations
from . import const, decorators, messages
@@ -27,19 +29,22 @@
@callback
def async_register_commands(hass, async_reg):
"""Register commands."""
- async_reg(hass, handle_subscribe_events)
- async_reg(hass, handle_unsubscribe_events)
async_reg(hass, handle_call_service)
- async_reg(hass, handle_get_states)
- async_reg(hass, handle_get_services)
+ async_reg(hass, handle_entity_source)
+ async_reg(hass, handle_execute_script)
async_reg(hass, handle_get_config)
+ async_reg(hass, handle_get_services)
+ async_reg(hass, handle_get_states)
+ async_reg(hass, handle_manifest_get)
+ async_reg(hass, handle_integration_setup_info)
+ async_reg(hass, handle_manifest_list)
async_reg(hass, handle_ping)
async_reg(hass, handle_render_template)
- async_reg(hass, handle_manifest_list)
- async_reg(hass, handle_manifest_get)
- async_reg(hass, handle_entity_source)
+ async_reg(hass, handle_subscribe_bootstrap_integrations)
+ async_reg(hass, handle_subscribe_events)
async_reg(hass, handle_subscribe_trigger)
async_reg(hass, handle_test_condition)
+ async_reg(hass, handle_unsubscribe_events)
def pong_message(iden):
@@ -94,6 +99,27 @@ def forward_events(event):
connection.send_message(messages.result_message(msg["id"]))
+@callback
+@decorators.websocket_command(
+ {
+ vol.Required("type"): "subscribe_bootstrap_integrations",
+ }
+)
+def handle_subscribe_bootstrap_integrations(hass, connection, msg):
+ """Handle subscribe bootstrap integrations command."""
+
+ @callback
+ def forward_bootstrap_integrations(message):
+ """Forward bootstrap integrations to websocket."""
+ connection.send_message(messages.event_message(msg["id"], message))
+
+ connection.subscriptions[msg["id"]] = async_dispatcher_connect(
+ hass, SIGNAL_BOOTSTRAP_INTEGRATONS, forward_bootstrap_integrations
+ )
+
+ connection.send_message(messages.result_message(msg["id"]))
+
+
@callback
@decorators.websocket_command(
{
@@ -121,6 +147,7 @@ def handle_unsubscribe_events(hass, connection, msg):
vol.Required("type"): "call_service",
vol.Required("domain"): str,
vol.Required("service"): str,
+ vol.Optional("target"): cv.ENTITY_SERVICE_FIELDS,
vol.Optional("service_data"): dict,
}
)
@@ -131,6 +158,11 @@ async def handle_call_service(hass, connection, msg):
if msg["domain"] == HASS_DOMAIN and msg["service"] in ["restart", "stop"]:
blocking = False
+ # We do not support templates.
+ target = msg.get("target")
+ if template.is_complex(target):
+ raise vol.Invalid("Templates are not supported here")
+
try:
context = connection.context(msg)
await hass.services.async_call(
@@ -139,6 +171,7 @@ async def handle_call_service(hass, connection, msg):
msg.get("service_data"),
blocking,
context,
+ target=target,
)
connection.send_message(
messages.result_message(msg["id"], {"context": context})
@@ -208,13 +241,9 @@ def handle_get_config(hass, connection, msg):
@decorators.async_response
async def handle_manifest_list(hass, connection, msg):
"""Handle integrations command."""
+ loaded_integrations = async_get_loaded_integrations(hass)
integrations = await asyncio.gather(
- *[
- async_get_integration(hass, domain)
- for domain in hass.config.components
- # Filter out platforms.
- if "." not in domain
- ]
+ *[async_get_integration(hass, domain) for domain in loaded_integrations]
)
connection.send_result(
msg["id"], [integration.manifest for integration in integrations]
@@ -234,6 +263,19 @@ async def handle_manifest_get(hass, connection, msg):
connection.send_error(msg["id"], const.ERR_NOT_FOUND, "Integration not found")
+@decorators.websocket_command({vol.Required("type"): "integration/setup_info"})
+@decorators.async_response
+async def handle_integration_setup_info(hass, connection, msg):
+ """Handle integrations command."""
+ connection.send_result(
+ msg["id"],
+ [
+ {"domain": integration, "seconds": timedelta.total_seconds()}
+ for integration, timedelta in hass.data[DATA_SETUP_TIME].items()
+ ],
+ )
+
+
@callback
@decorators.websocket_command({vol.Required("type"): "ping"})
def handle_ping(hass, connection, msg):
@@ -254,14 +296,14 @@ def handle_ping(hass, connection, msg):
async def handle_render_template(hass, connection, msg):
"""Handle render_template command."""
template_str = msg["template"]
- template = Template(template_str, hass)
+ template_obj = template.Template(template_str, hass)
variables = msg.get("variables")
timeout = msg.get("timeout")
info = None
if timeout:
try:
- timed_out = await template.async_render_will_timeout(timeout)
+ timed_out = await template_obj.async_render_will_timeout(timeout)
except TemplateError as ex:
connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex))
return
@@ -292,7 +334,7 @@ def _template_listener(event, updates):
try:
info = async_track_template_result(
hass,
- [TrackTemplate(template, variables)],
+ [TrackTemplate(template_obj, variables)],
_template_listener,
raise_on_template_error=True,
)
@@ -414,3 +456,24 @@ async def handle_test_condition(hass, connection, msg):
connection.send_result(
msg["id"], {"result": check_condition(hass, msg.get("variables"))}
)
+
+
+@decorators.websocket_command(
+ {
+ vol.Required("type"): "execute_script",
+ vol.Required("sequence"): cv.SCRIPT_SCHEMA,
+ vol.Optional("variables"): dict,
+ }
+)
+@decorators.require_admin
+@decorators.async_response
+async def handle_execute_script(hass, connection, msg):
+ """Handle execute script command."""
+ # Circular dep
+ # pylint: disable=import-outside-toplevel
+ from homeassistant.helpers.script import Script
+
+ context = connection.context(msg)
+ script_obj = Script(hass, msg["sequence"], f"{const.DOMAIN} script", const.DOMAIN)
+ await script_obj.async_run(msg.get("variables"), context=context)
+ connection.send_message(messages.result_message(msg["id"], {"context": context}))
diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py
index 108d4de5ada4e3..dd1bb33369385b 100644
--- a/homeassistant/components/websocket_api/connection.py
+++ b/homeassistant/components/websocket_api/connection.py
@@ -1,6 +1,8 @@
"""Connection session."""
+from __future__ import annotations
+
import asyncio
-from typing import Any, Callable, Dict, Hashable, Optional
+from typing import Any, Callable, Hashable
import voluptuous as vol
@@ -26,7 +28,7 @@ def __init__(self, logger, hass, send_message, user, refresh_token):
else:
self.refresh_token_id = None
- self.subscriptions: Dict[Hashable, Callable[[], Any]] = {}
+ self.subscriptions: dict[Hashable, Callable[[], Any]] = {}
self.last_id = 0
def context(self, msg):
@@ -37,7 +39,7 @@ def context(self, msg):
return Context(user_id=user.id)
@callback
- def send_result(self, msg_id: int, result: Optional[Any] = None) -> None:
+ def send_result(self, msg_id: int, result: Any | None = None) -> None:
"""Send a result message."""
self.send_message(messages.result_message(msg_id, result))
diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py
index 5f2cfb2257dc8a..7c3f18f856c5fb 100644
--- a/homeassistant/components/websocket_api/const.py
+++ b/homeassistant/components/websocket_api/const.py
@@ -9,7 +9,7 @@
from homeassistant.helpers.json import JSONEncoder
if TYPE_CHECKING:
- from .connection import ActiveConnection # noqa
+ from .connection import ActiveConnection
WebSocketCommandHandler = Callable[[HomeAssistant, "ActiveConnection", dict], None]
diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py
index daa8529e8bd35b..27af0424f3cfec 100644
--- a/homeassistant/components/websocket_api/http.py
+++ b/homeassistant/components/websocket_api/http.py
@@ -1,8 +1,9 @@
"""View to accept incoming websocket connection."""
+from __future__ import annotations
+
import asyncio
from contextlib import suppress
import logging
-from typing import Optional
from aiohttp import WSMsgType, web
import async_timeout
@@ -57,7 +58,7 @@ def __init__(self, hass, request):
"""Initialize an active connection."""
self.hass = hass
self.request = request
- self.wsock: Optional[web.WebSocketResponse] = None
+ self.wsock: web.WebSocketResponse | None = None
self._to_write: asyncio.Queue = asyncio.Queue(maxsize=MAX_PENDING_MSG)
self._handle_task = None
self._writer_task = None
@@ -73,11 +74,11 @@ async def _writer(self):
if message is None:
break
- self._logger.debug("Sending %s", message)
-
if not isinstance(message, str):
message = message_to_json(message)
+ self._logger.debug("Sending %s", message)
+
await self.wsock.send_str(message)
# Clean up the peaker checker when we shut down the writer
diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py
index f68beff5924feb..736a7ad59f0634 100644
--- a/homeassistant/components/websocket_api/messages.py
+++ b/homeassistant/components/websocket_api/messages.py
@@ -1,8 +1,9 @@
"""Message templates for websocket commands."""
+from __future__ import annotations
from functools import lru_cache
import logging
-from typing import Any, Dict
+from typing import Any
import voluptuous as vol
@@ -32,12 +33,12 @@
IDEN_JSON_TEMPLATE = '"__IDEN__"'
-def result_message(iden: int, result: Any = None) -> Dict:
+def result_message(iden: int, result: Any = None) -> dict:
"""Return a success result message."""
return {"id": iden, "type": const.TYPE_RESULT, "success": True, "result": result}
-def error_message(iden: int, code: str, message: str) -> Dict:
+def error_message(iden: int, code: str, message: str) -> dict:
"""Return an error result message."""
return {
"id": iden,
@@ -47,7 +48,7 @@ def error_message(iden: int, code: str, message: str) -> Dict:
}
-def event_message(iden: JSON_TYPE, event: Any) -> Dict:
+def event_message(iden: JSON_TYPE, event: Any) -> dict:
"""Return an event message."""
return {"id": iden, "type": "event", "event": event}
diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py
index c026978634f864..dfcdc57842e395 100644
--- a/homeassistant/components/websocket_api/sensor.py
+++ b/homeassistant/components/websocket_api/sensor.py
@@ -1,7 +1,7 @@
"""Entity to track connections to websocket API."""
+from homeassistant.components.sensor import SensorEntity
from homeassistant.core import callback
-from homeassistant.helpers.entity import Entity
from .const import (
DATA_CONNECTIONS,
@@ -19,7 +19,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([entity])
-class APICount(Entity):
+class APICount(SensorEntity):
"""Entity to represent how many people are connected to the stream API."""
def __init__(self):
diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py
index 75ca322b9a3b12..a013d1fdd3461b 100644
--- a/homeassistant/components/wemo/__init__.py
+++ b/homeassistant/components/wemo/__init__.py
@@ -3,7 +3,6 @@
import logging
import pywemo
-import requests
import voluptuous as vol
from homeassistant import config_entries
@@ -12,7 +11,7 @@
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import EVENT_HOMEASSISTANT_STOP
+from homeassistant.const import CONF_DISCOVERY, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -57,7 +56,6 @@ def coerce_host_port(value):
CONF_STATIC = "static"
-CONF_DISCOVERY = "discovery"
DEFAULT_DISCOVERY = True
@@ -115,7 +113,7 @@ async def async_stop_wemo(event):
static_conf = config.get(CONF_STATIC, [])
if static_conf:
- _LOGGER.debug("Adding statically configured WeMo devices...")
+ _LOGGER.debug("Adding statically configured WeMo devices")
for device in await asyncio.gather(
*[
hass.async_add_executor_job(validate_static_config, host, port)
@@ -192,7 +190,7 @@ def __init__(self, hass: HomeAssistant, wemo_dispatcher: WemoDispatcher) -> None
async def async_discover_and_schedule(self, *_) -> None:
"""Periodically scan the network looking for WeMo devices."""
- _LOGGER.debug("Scanning network for WeMo devices...")
+ _LOGGER.debug("Scanning network for WeMo devices")
try:
for device in await self._hass.async_add_executor_job(
pywemo.discover_devices
@@ -230,10 +228,10 @@ def validate_static_config(host, port):
return None
try:
- device = pywemo.discovery.device_from_description(url, None)
+ device = pywemo.discovery.device_from_description(url)
except (
- requests.exceptions.ConnectionError,
- requests.exceptions.Timeout,
+ pywemo.exceptions.ActionException,
+ pywemo.exceptions.HTTPException,
) as err:
_LOGGER.error("Unable to access WeMo at %s (%s)", url, err)
return None
diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py
index b6690ed6d28b23..94d5a587c17250 100644
--- a/homeassistant/components/wemo/binary_sensor.py
+++ b/homeassistant/components/wemo/binary_sensor.py
@@ -2,8 +2,6 @@
import asyncio
import logging
-from pywemo.ouimeaux_device.api.service import ActionException
-
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -35,13 +33,5 @@ class WemoBinarySensor(WemoSubscriptionEntity, BinarySensorEntity):
def _update(self, force_update=True):
"""Update the sensor state."""
- try:
+ with self._wemo_exception_handler("update status"):
self._state = self.wemo.get_state(force_update)
-
- if not self._available:
- _LOGGER.info("Reconnected to %s", self.name)
- self._available = True
- except (AttributeError, ActionException) as err:
- _LOGGER.warning("Could not update status for %s (%s)", self.name, err)
- self._available = False
- self.wemo.reconnect_with_device()
diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py
index e7c0712272ccb2..d10707f4590478 100644
--- a/homeassistant/components/wemo/entity.py
+++ b/homeassistant/components/wemo/entity.py
@@ -1,10 +1,14 @@
"""Classes shared among Wemo entities."""
+from __future__ import annotations
+
import asyncio
+import contextlib
import logging
-from typing import Any, Dict, Optional
+from typing import Any, Generator
import async_timeout
from pywemo import WeMoDevice
+from pywemo.exceptions import ActionException
from homeassistant.helpers.entity import Entity
@@ -13,6 +17,18 @@
_LOGGER = logging.getLogger(__name__)
+class ExceptionHandlerStatus:
+ """Exit status from the _wemo_exception_handler context manager."""
+
+ # An exception if one was raised in the _wemo_exception_handler.
+ exception: Exception | None = None
+
+ @property
+ def success(self) -> bool:
+ """Return True if the handler completed with no exception."""
+ return self.exception is None
+
+
class WemoEntity(Entity):
"""Common methods for Wemo entities.
@@ -25,6 +41,7 @@ def __init__(self, device: WeMoDevice) -> None:
self._state = None
self._available = True
self._update_lock = None
+ self._has_polled = False
@property
def name(self) -> str:
@@ -36,7 +53,24 @@ def available(self) -> bool:
"""Return true if switch is available."""
return self._available
- def _update(self, force_update: Optional[bool] = True):
+ @contextlib.contextmanager
+ def _wemo_exception_handler(
+ self, message: str
+ ) -> Generator[ExceptionHandlerStatus, None, None]:
+ """Wrap device calls to set `_available` when wemo exceptions happen."""
+ status = ExceptionHandlerStatus()
+ try:
+ yield status
+ except ActionException as err:
+ status.exception = err
+ _LOGGER.warning("Could not %s for %s (%s)", message, self.name, err)
+ self._available = False
+ else:
+ if not self._available:
+ _LOGGER.info("Reconnected to %s", self.name)
+ self._available = True
+
+ def _update(self, force_update: bool | None = True):
"""Update the device state."""
raise NotImplementedError()
@@ -49,25 +83,38 @@ async def async_update(self) -> None:
"""Update WeMo state.
Wemo has an aggressive retry logic that sometimes can take over a
- minute to return. If we don't get a state after 5 seconds, assume the
- Wemo switch is unreachable. If update goes through, it will be made
- available again.
+ minute to return. If we don't get a state within the scan interval,
+ assume the Wemo switch is unreachable. If update goes through, it will
+ be made available again.
"""
# If an update is in progress, we don't do anything
if self._update_lock.locked():
return
try:
- with async_timeout.timeout(5):
- await asyncio.shield(self._async_locked_update(True))
+ async with async_timeout.timeout(
+ self.platform.scan_interval.seconds - 0.1
+ ) as timeout:
+ await asyncio.shield(self._async_locked_update(True, timeout))
except asyncio.TimeoutError:
_LOGGER.warning("Lost connection to %s", self.name)
self._available = False
- async def _async_locked_update(self, force_update: bool) -> None:
+ async def _async_locked_update(
+ self, force_update: bool, timeout: async_timeout.timeout | None = None
+ ) -> None:
"""Try updating within an async lock."""
async with self._update_lock:
await self.hass.async_add_executor_job(self._update, force_update)
+ self._has_polled = True
+ # When the timeout expires HomeAssistant is no longer waiting for an
+ # update from the device. Instead, the state needs to be updated
+ # asynchronously. This also handles the case where an update came
+ # directly from the device (device push). In that case no polling
+ # update was involved and the state also needs to be updated
+ # asynchronously.
+ if not timeout or timeout.expired:
+ self.async_write_ha_state()
class WemoSubscriptionEntity(WemoEntity):
@@ -79,7 +126,7 @@ def unique_id(self) -> str:
return self.wemo.serialnumber
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return the device info."""
return {
"name": self.name,
@@ -93,6 +140,31 @@ def is_on(self) -> bool:
"""Return true if the state is on. Standby is on."""
return self._state
+ @property
+ def should_poll(self) -> bool:
+ """Return True if the the device requires local polling, False otherwise.
+
+ It is desirable to allow devices to enter periods of polling when the
+ callback subscription (device push) is not working. To work with the
+ entity platform polling logic, this entity needs to report True for
+ should_poll initially. That is required to cause the entity platform
+ logic to start the polling task (see the discussion in #47182).
+
+ Polling can be disabled if three conditions are met:
+ 1. The device has polled to get the initial state (self._has_polled) and
+ to satisfy the entity platform constraint mentioned above.
+ 2. The polling was successful and the device is in a healthy state
+ (self.available).
+ 3. The pywemo subscription registry reports that there is an active
+ subscription and the subscription has been confirmed by receiving an
+ initial event. This confirms that device push notifications are
+ working correctly (registry.is_subscribed - this method is async safe).
+ """
+ registry = self.hass.data[WEMO_DOMAIN]["registry"]
+ return not (
+ self.available and self._has_polled and registry.is_subscribed(self.wemo)
+ )
+
async def async_added_to_hass(self) -> None:
"""Wemo device added to Home Assistant."""
await super().async_added_to_hass()
@@ -121,4 +193,3 @@ async def _async_locked_subscription_callback(self, force_update: bool) -> None:
return
await self._async_locked_update(force_update)
- self.async_write_ha_state()
diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py
index cdbcc89fae6591..a3da5edae766be 100644
--- a/homeassistant/components/wemo/fan.py
+++ b/homeassistant/components/wemo/fan.py
@@ -4,13 +4,13 @@
import logging
import math
-from pywemo.ouimeaux_device.api.service import ActionException
import voluptuous as vol
from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity
from homeassistant.helpers import entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import (
+ int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@@ -115,7 +115,7 @@ def icon(self):
return "mdi:water-percent"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
return {
ATTR_CURRENT_HUMIDITY: self._current_humidity,
@@ -127,10 +127,15 @@ def device_state_attributes(self):
}
@property
- def percentage(self) -> str:
+ def percentage(self) -> int:
"""Return the current speed percentage."""
return ranged_value_to_percentage(SPEED_RANGE, self._fan_mode)
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return int_states_in_range(SPEED_RANGE)
+
@property
def supported_features(self) -> int:
"""Flag supported features."""
@@ -138,7 +143,7 @@ def supported_features(self) -> int:
def _update(self, force_update=True):
"""Update the device state."""
- try:
+ with self._wemo_exception_handler("update status"):
self._state = self.wemo.get_state(force_update)
self._fan_mode = self.wemo.fan_mode
@@ -152,14 +157,6 @@ def _update(self, force_update=True):
if self.wemo.fan_mode != WEMO_FAN_OFF:
self._last_fan_on_mode = self.wemo.fan_mode
- if not self._available:
- _LOGGER.info("Reconnected to %s", self.name)
- self._available = True
- except (AttributeError, ActionException) as err:
- _LOGGER.warning("Could not update status for %s (%s)", self.name, err)
- self._available = False
- self.wemo.reconnect_with_device()
-
def turn_on(
self,
speed: str = None,
@@ -172,11 +169,8 @@ def turn_on(
def turn_off(self, **kwargs) -> None:
"""Turn the switch off."""
- try:
+ with self._wemo_exception_handler("turn off"):
self.wemo.set_state(WEMO_FAN_OFF)
- except ActionException as err:
- _LOGGER.warning("Error while turning off device %s (%s)", self.name, err)
- self._available = False
self.schedule_update_ha_state()
@@ -189,13 +183,8 @@ def set_percentage(self, percentage: int) -> None:
else:
named_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
- try:
+ with self._wemo_exception_handler("set speed"):
self.wemo.set_state(named_speed)
- except ActionException as err:
- _LOGGER.warning(
- "Error while setting speed of device %s (%s)", self.name, err
- )
- self._available = False
self.schedule_update_ha_state()
@@ -212,24 +201,14 @@ def set_humidity(self, target_humidity: float) -> None:
elif target_humidity >= 100:
pywemo_humidity = WEMO_HUMIDITY_100
- try:
+ with self._wemo_exception_handler("set humidity"):
self.wemo.set_humidity(pywemo_humidity)
- except ActionException as err:
- _LOGGER.warning(
- "Error while setting humidity of device: %s (%s)", self.name, err
- )
- self._available = False
self.schedule_update_ha_state()
def reset_filter_life(self) -> None:
"""Reset the filter life to 100%."""
- try:
+ with self._wemo_exception_handler("reset filter life"):
self.wemo.reset_filter_life()
- except ActionException as err:
- _LOGGER.warning(
- "Error while resetting filter life on device: %s (%s)", self.name, err
- )
- self._available = False
self.schedule_update_ha_state()
diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py
index 1362c7d483cc34..bbcdafaf3513a1 100644
--- a/homeassistant/components/wemo/light.py
+++ b/homeassistant/components/wemo/light.py
@@ -3,8 +3,6 @@
from datetime import timedelta
import logging
-from pywemo.ouimeaux_device.api.service import ActionException
-
from homeassistant import util
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -158,7 +156,7 @@ def turn_on(self, **kwargs):
"force_update": False,
}
- try:
+ with self._wemo_exception_handler("turn on"):
if xy_color is not None:
self.wemo.set_color(xy_color, transition=transition_time)
@@ -167,9 +165,6 @@ def turn_on(self, **kwargs):
if self.wemo.turn_on(**turn_on_kwargs):
self._state["onoff"] = WEMO_ON
- except ActionException as err:
- _LOGGER.warning("Error while turning on device %s (%s)", self.name, err)
- self._available = False
self.schedule_update_ha_state()
@@ -177,29 +172,21 @@ def turn_off(self, **kwargs):
"""Turn the light off."""
transition_time = int(kwargs.get(ATTR_TRANSITION, 0))
- try:
+ with self._wemo_exception_handler("turn off"):
if self.wemo.turn_off(transition=transition_time):
self._state["onoff"] = WEMO_OFF
- except ActionException as err:
- _LOGGER.warning("Error while turning off device %s (%s)", self.name, err)
- self._available = False
self.schedule_update_ha_state()
def _update(self, force_update=True):
"""Synchronize state with bridge."""
- try:
+ with self._wemo_exception_handler("update status") as handler:
self._update_lights(no_throttle=force_update)
self._state = self.wemo.state
- except (AttributeError, ActionException) as err:
- _LOGGER.warning("Could not update status for %s (%s)", self.name, err)
- self._available = False
- self.wemo.bridge.reconnect_with_device()
- else:
+ if handler.success:
self._is_on = self._state.get("onoff") != WEMO_OFF
self._brightness = self._state.get("level", 255)
self._color_temp = self._state.get("temperature_mireds")
- self._available = True
xy_color = self._state.get("color_xy")
@@ -229,20 +216,12 @@ def brightness(self):
def _update(self, force_update=True):
"""Update the device state."""
- try:
+ with self._wemo_exception_handler("update status"):
self._state = self.wemo.get_state(force_update)
wemobrightness = int(self.wemo.get_brightness(force_update))
self._brightness = int((wemobrightness * 255) / 100)
- if not self._available:
- _LOGGER.info("Reconnected to %s", self.name)
- self._available = True
- except (AttributeError, ActionException) as err:
- _LOGGER.warning("Could not update status for %s (%s)", self.name, err)
- self._available = False
- self.wemo.reconnect_with_device()
-
def turn_on(self, **kwargs):
"""Turn the dimmer on."""
# Wemo dimmer switches use a range of [0, 100] to control
@@ -253,24 +232,18 @@ def turn_on(self, **kwargs):
else:
brightness = 255
- try:
+ with self._wemo_exception_handler("turn on"):
if self.wemo.on():
self._state = WEMO_ON
self.wemo.set_brightness(brightness)
- except ActionException as err:
- _LOGGER.warning("Error while turning on device %s (%s)", self.name, err)
- self._available = False
self.schedule_update_ha_state()
def turn_off(self, **kwargs):
"""Turn the dimmer off."""
- try:
+ with self._wemo_exception_handler("turn off"):
if self.wemo.off():
self._state = WEMO_OFF
- except ActionException as err:
- _LOGGER.warning("Error while turning on device %s (%s)", self.name, err)
- self._available = False
self.schedule_update_ha_state()
diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json
index fe5559b58d6cc3..9d91ab7ef967db 100644
--- a/homeassistant/components/wemo/manifest.json
+++ b/homeassistant/components/wemo/manifest.json
@@ -3,7 +3,7 @@
"name": "Belkin WeMo",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wemo",
- "requirements": ["pywemo==0.6.1"],
+ "requirements": ["pywemo==0.6.3"],
"ssdp": [
{
"manufacturer": "Belkin International Inc."
@@ -12,5 +12,5 @@
"homekit": {
"models": ["Socket", "Wemo"]
},
- "codeowners": []
+ "codeowners": ["@esev"]
}
diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py
index 50926e07a11368..5e97031786c177 100644
--- a/homeassistant/components/wemo/switch.py
+++ b/homeassistant/components/wemo/switch.py
@@ -3,8 +3,6 @@
from datetime import datetime, timedelta
import logging
-from pywemo.ouimeaux_device.api.service import ActionException
-
from homeassistant.components.switch import SwitchEntity
from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -61,7 +59,7 @@ def __init__(self, device):
self._mode_string = None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
attr = {}
if self.maker_params:
@@ -140,29 +138,23 @@ def icon(self):
def turn_on(self, **kwargs):
"""Turn the switch on."""
- try:
+ with self._wemo_exception_handler("turn on"):
if self.wemo.on():
self._state = WEMO_ON
- except ActionException as err:
- _LOGGER.warning("Error while turning on device %s (%s)", self.name, err)
- self._available = False
self.schedule_update_ha_state()
def turn_off(self, **kwargs):
"""Turn the switch off."""
- try:
+ with self._wemo_exception_handler("turn off"):
if self.wemo.off():
self._state = WEMO_OFF
- except ActionException as err:
- _LOGGER.warning("Error while turning off device %s (%s)", self.name, err)
- self._available = False
self.schedule_update_ha_state()
def _update(self, force_update=True):
"""Update the device state."""
- try:
+ with self._wemo_exception_handler("update status"):
self._state = self.wemo.get_state(force_update)
if self.wemo.model_name == "Insight":
@@ -173,11 +165,3 @@ def _update(self, force_update=True):
elif self.wemo.model_name == "CoffeeMaker":
self.coffeemaker_mode = self.wemo.mode
self._mode_string = self.wemo.mode_string
-
- if not self._available:
- _LOGGER.info("Reconnected to %s", self.name)
- self._available = True
- except (AttributeError, ActionException) as err:
- _LOGGER.warning("Could not update status for %s (%s)", self.name, err)
- self._available = False
- self.wemo.reconnect_with_device()
diff --git a/homeassistant/components/wemo/translations/de.json b/homeassistant/components/wemo/translations/de.json
index f20ad5598ab2b2..81694f65ea295f 100644
--- a/homeassistant/components/wemo/translations/de.json
+++ b/homeassistant/components/wemo/translations/de.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"no_devices_found": "Es wurden keine Wemo-Ger\u00e4te im Netzwerk gefunden.",
- "single_instance_allowed": "Nur eine einzige Konfiguration von Wemo ist zul\u00e4ssig."
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/wemo/translations/hu.json b/homeassistant/components/wemo/translations/hu.json
new file mode 100644
index 00000000000000..ab799e90c74001
--- /dev/null
+++ b/homeassistant/components/wemo/translations/hu.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/translations/id.json b/homeassistant/components/wemo/translations/id.json
new file mode 100644
index 00000000000000..af0b3128cb9155
--- /dev/null
+++ b/homeassistant/components/wemo/translations/id.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "step": {
+ "confirm": {
+ "description": "Ingin menyiapkan Wemo?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/translations/ko.json b/homeassistant/components/wemo/translations/ko.json
index a262f7ebd3e297..704b11262615ac 100644
--- a/homeassistant/components/wemo/translations/ko.json
+++ b/homeassistant/components/wemo/translations/ko.json
@@ -1,12 +1,12 @@
{
"config": {
"abort": {
- "no_devices_found": "Wemo \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
- "single_instance_allowed": "\ud558\ub098\uc758 Wemo \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"step": {
"confirm": {
- "description": "Wemo \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
+ "description": "Wemo\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?"
}
}
}
diff --git a/homeassistant/components/wemo/translations/nl.json b/homeassistant/components/wemo/translations/nl.json
index 6146072623a95d..e4087863726c0d 100644
--- a/homeassistant/components/wemo/translations/nl.json
+++ b/homeassistant/components/wemo/translations/nl.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "no_devices_found": "Geen Wemo-apparaten gevonden op het netwerk.",
- "single_instance_allowed": "Slechts een enkele configuratie van Wemo is mogelijk."
+ "no_devices_found": "Geen apparaten gevonden op het netwerk",
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/wemo/translations/tr.json b/homeassistant/components/wemo/translations/tr.json
index 411a536ceedbad..a87d832eece777 100644
--- a/homeassistant/components/wemo/translations/tr.json
+++ b/homeassistant/components/wemo/translations/tr.json
@@ -1,7 +1,13 @@
{
"config": {
"abort": {
- "no_devices_found": "A\u011fda Wemo cihaz\u0131 bulunamad\u0131."
+ "no_devices_found": "A\u011fda Wemo cihaz\u0131 bulunamad\u0131.",
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "step": {
+ "confirm": {
+ "description": "Wemo'yu kurmak istiyor musunuz?"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/translations/uk.json b/homeassistant/components/wemo/translations/uk.json
new file mode 100644
index 00000000000000..1217d664234241
--- /dev/null
+++ b/homeassistant/components/wemo/translations/uk.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Wemo?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py
index 7ec5c3dac5e436..6d97037a4ee3c6 100644
--- a/homeassistant/components/whois/sensor.py
+++ b/homeassistant/components/whois/sensor.py
@@ -5,15 +5,12 @@
import voluptuous as vol
import whois
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME, TIME_DAYS
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.const import CONF_DOMAIN, CONF_NAME, TIME_DAYS
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
-CONF_DOMAIN = "domain"
-
DEFAULT_NAME = "Whois"
ATTR_EXPIRES = "expires"
@@ -49,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return
-class WhoisSensor(Entity):
+class WhoisSensor(SensorEntity):
"""Implementation of a WHOIS sensor."""
def __init__(self, name, domain):
@@ -83,7 +80,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Get the more info attributes."""
return self._attributes
diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py
index 000b961bda9f87..36f6e641508fb5 100644
--- a/homeassistant/components/wiffi/__init__.py
+++ b/homeassistant/components/wiffi/__init__.py
@@ -33,11 +33,6 @@
PLATFORMS = ["sensor", "binary_sensor"]
-async def async_setup(hass: HomeAssistant, config: dict):
- """Set up the wiffi component. config contains data from configuration.yaml."""
- return True
-
-
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Set up wiffi from a config entry, config_entry contains data from config entry database."""
if not config_entry.update_listeners:
@@ -59,9 +54,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
_LOGGER.error("Port %s already in use", config_entry.data[CONF_PORT])
raise ConfigEntryNotReady from exc
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, component)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return True
@@ -74,14 +69,14 @@ async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry):
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Unload a config entry."""
- api: "WiffiIntegrationApi" = hass.data[DOMAIN][config_entry.entry_id]
+ api: WiffiIntegrationApi = hass.data[DOMAIN][config_entry.entry_id]
await api.server.close_server()
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(config_entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(config_entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/wiffi/config_flow.py b/homeassistant/components/wiffi/config_flow.py
index f30ee8792df055..768f66bf8decec 100644
--- a/homeassistant/components/wiffi/config_flow.py
+++ b/homeassistant/components/wiffi/config_flow.py
@@ -11,11 +11,7 @@
from homeassistant.const import CONF_PORT, CONF_TIMEOUT
from homeassistant.core import callback
-from .const import ( # pylint: disable=unused-import
- DEFAULT_PORT,
- DEFAULT_TIMEOUT,
- DOMAIN,
-)
+from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN
class WiffiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/wiffi/sensor.py b/homeassistant/components/wiffi/sensor.py
index f207e3be3acaa9..800a420f8f0b2b 100644
--- a/homeassistant/components/wiffi/sensor.py
+++ b/homeassistant/components/wiffi/sensor.py
@@ -5,6 +5,7 @@
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
+ SensorEntity,
)
from homeassistant.const import DEGREE, PRESSURE_MBAR, TEMP_CELSIUS
from homeassistant.core import callback
@@ -58,7 +59,7 @@ def _create_entity(device, metric):
async_dispatcher_connect(hass, CREATE_ENTITY_SIGNAL, _create_entity)
-class NumberEntity(WiffiEntity):
+class NumberEntity(WiffiEntity, SensorEntity):
"""Entity for wiffi metrics which have a number value."""
def __init__(self, device, metric, options):
@@ -100,7 +101,7 @@ def _update_value_callback(self, device, metric):
self.async_write_ha_state()
-class StringEntity(WiffiEntity):
+class StringEntity(WiffiEntity, SensorEntity):
"""Entity for wiffi metrics which have a string value."""
def __init__(self, device, metric, options):
diff --git a/homeassistant/components/wiffi/translations/de.json b/homeassistant/components/wiffi/translations/de.json
index 79bf8168a14a6a..4084cda8f9f217 100644
--- a/homeassistant/components/wiffi/translations/de.json
+++ b/homeassistant/components/wiffi/translations/de.json
@@ -8,7 +8,8 @@
"user": {
"data": {
"port": "Server Port"
- }
+ },
+ "title": "TCP-Server f\u00fcr WIFFI-Ger\u00e4te einrichten"
}
}
},
diff --git a/homeassistant/components/wiffi/translations/hu.json b/homeassistant/components/wiffi/translations/hu.json
index 21320afea78209..c623f6ddabae25 100644
--- a/homeassistant/components/wiffi/translations/hu.json
+++ b/homeassistant/components/wiffi/translations/hu.json
@@ -2,6 +2,22 @@
"config": {
"abort": {
"start_server_failed": "A szerver ind\u00edt\u00e1sa nem siker\u00fclt."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Port"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s (perc)"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wiffi/translations/id.json b/homeassistant/components/wiffi/translations/id.json
new file mode 100644
index 00000000000000..0022f83b0a1b38
--- /dev/null
+++ b/homeassistant/components/wiffi/translations/id.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "addr_in_use": "Port server sudah digunakan.",
+ "start_server_failed": "Gagal memulai server."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Port"
+ },
+ "title": "Siapkan server TCP untuk perangkat WIFFI"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "Tenggang waktu (menit)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wiffi/translations/ko.json b/homeassistant/components/wiffi/translations/ko.json
index c332d3e5f26e5f..74c6568feef550 100644
--- a/homeassistant/components/wiffi/translations/ko.json
+++ b/homeassistant/components/wiffi/translations/ko.json
@@ -7,7 +7,7 @@
"step": {
"user": {
"data": {
- "port": "\uc11c\ubc84 \ud3ec\ud2b8"
+ "port": "\ud3ec\ud2b8"
},
"title": "WIFFI \uae30\uae30\uc6a9 TCP \uc11c\ubc84 \uc124\uc815\ud558\uae30"
}
diff --git a/homeassistant/components/wiffi/translations/nl.json b/homeassistant/components/wiffi/translations/nl.json
index af14d1942a78dc..966f9a18e416a9 100644
--- a/homeassistant/components/wiffi/translations/nl.json
+++ b/homeassistant/components/wiffi/translations/nl.json
@@ -7,7 +7,7 @@
"step": {
"user": {
"data": {
- "port": "Server poort"
+ "port": "Poort"
},
"title": "TCP-server instellen voor WIFFI-apparaten"
}
diff --git a/homeassistant/components/wiffi/translations/tr.json b/homeassistant/components/wiffi/translations/tr.json
new file mode 100644
index 00000000000000..26ec2e61e002e8
--- /dev/null
+++ b/homeassistant/components/wiffi/translations/tr.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "addr_in_use": "Sunucu ba\u011flant\u0131 noktas\u0131 zaten kullan\u0131l\u0131yor.",
+ "start_server_failed": "Ba\u015flatma sunucusu ba\u015far\u0131s\u0131z oldu."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "Port"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "Zaman a\u015f\u0131m\u0131 (dakika)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wiffi/translations/uk.json b/homeassistant/components/wiffi/translations/uk.json
new file mode 100644
index 00000000000000..dc8dac9cd56ecf
--- /dev/null
+++ b/homeassistant/components/wiffi/translations/uk.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "addr_in_use": "\u041f\u043e\u0440\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f.",
+ "start_server_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "port": "\u041f\u043e\u0440\u0442"
+ },
+ "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f TCP-\u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0434\u043b\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 WIFFI"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u0445\u0432\u0438\u043b\u0438\u043d\u0430\u0445)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py
index 97b482571031bc..3e14ea20b0cdc2 100644
--- a/homeassistant/components/wilight/__init__.py
+++ b/homeassistant/components/wilight/__init__.py
@@ -6,11 +6,12 @@
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.entity import Entity
-from .const import DOMAIN
from .parent_device import WiLightParent
+DOMAIN = "wilight"
+
# List the platforms that you want to support.
-PLATFORMS = ["fan", "light"]
+PLATFORMS = ["cover", "fan", "light"]
async def async_setup(hass: HomeAssistant, config: dict):
@@ -32,9 +33,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.data[DOMAIN][entry.entry_id] = parent
# Set up all platforms for this device/entry.
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -46,8 +47,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
# Unload entities for this entry/device.
await asyncio.gather(
*(
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
)
)
diff --git a/homeassistant/components/wilight/config_flow.py b/homeassistant/components/wilight/config_flow.py
index 3f1b12395ba898..2706db07871253 100644
--- a/homeassistant/components/wilight/config_flow.py
+++ b/homeassistant/components/wilight/config_flow.py
@@ -7,7 +7,7 @@
from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, ConfigFlow
from homeassistant.const import CONF_HOST
-from .const import DOMAIN # pylint: disable=unused-import
+from . import DOMAIN
CONF_SERIAL_NUMBER = "serial_number"
CONF_MODEL_NAME = "model_name"
@@ -15,7 +15,7 @@
WILIGHT_MANUFACTURER = "All Automacao Ltda"
# List the components supported by this integration.
-ALLOWED_WILIGHT_COMPONENTS = ["light", "fan"]
+ALLOWED_WILIGHT_COMPONENTS = ["cover", "fan", "light"]
class WiLightFlowHandler(ConfigFlow, domain=DOMAIN):
@@ -84,7 +84,6 @@ async def async_step_ssdp(self, discovery_info):
await self.async_set_unique_id(self._serial_number)
self._abort_if_unique_id_configured(updates={CONF_HOST: self._host})
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = {"name": self._title}
return await self.async_step_confirm()
diff --git a/homeassistant/components/wilight/const.py b/homeassistant/components/wilight/const.py
deleted file mode 100644
index a3d77da44efccd..00000000000000
--- a/homeassistant/components/wilight/const.py
+++ /dev/null
@@ -1,14 +0,0 @@
-"""Constants for the WiLight integration."""
-
-DOMAIN = "wilight"
-
-# Item types
-ITEM_LIGHT = "light"
-
-# Light types
-LIGHT_ON_OFF = "light_on_off"
-LIGHT_DIMMER = "light_dimmer"
-LIGHT_COLOR = "light_rgb"
-
-# Light service support
-SUPPORT_NONE = 0
diff --git a/homeassistant/components/wilight/cover.py b/homeassistant/components/wilight/cover.py
new file mode 100644
index 00000000000000..93c9a8c450307e
--- /dev/null
+++ b/homeassistant/components/wilight/cover.py
@@ -0,0 +1,104 @@
+"""Support for WiLight Cover."""
+
+from pywilight.const import (
+ COVER_V1,
+ ITEM_COVER,
+ WL_CLOSE,
+ WL_CLOSING,
+ WL_OPEN,
+ WL_OPENING,
+ WL_STOP,
+ WL_STOPPED,
+)
+
+from homeassistant.components.cover import ATTR_POSITION, CoverEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+
+from . import DOMAIN, WiLightDevice
+
+
+async def async_setup_entry(
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities
+):
+ """Set up WiLight covers from a config entry."""
+ parent = hass.data[DOMAIN][entry.entry_id]
+
+ # Handle a discovered WiLight device.
+ entities = []
+ for item in parent.api.items:
+ if item["type"] != ITEM_COVER:
+ continue
+ index = item["index"]
+ item_name = item["name"]
+ if item["sub_type"] != COVER_V1:
+ continue
+ entity = WiLightCover(parent.api, index, item_name)
+ entities.append(entity)
+
+ async_add_entities(entities)
+
+
+def wilight_to_hass_position(value):
+ """Convert wilight position 1..255 to hass format 0..100."""
+ return min(100, round((value * 100) / 255))
+
+
+def hass_to_wilight_position(value):
+ """Convert hass position 0..100 to wilight 1..255 scale."""
+ return min(255, round((value * 255) / 100))
+
+
+class WiLightCover(WiLightDevice, CoverEntity):
+ """Representation of a WiLights cover."""
+
+ @property
+ def current_cover_position(self):
+ """Return current position of cover.
+
+ None is unknown, 0 is closed, 100 is fully open.
+ """
+ if "position_current" in self._status:
+ return wilight_to_hass_position(self._status["position_current"])
+ return None
+
+ @property
+ def is_opening(self):
+ """Return if the cover is opening or not."""
+ if "motor_state" not in self._status:
+ return None
+ return self._status["motor_state"] == WL_OPENING
+
+ @property
+ def is_closing(self):
+ """Return if the cover is closing or not."""
+ if "motor_state" not in self._status:
+ return None
+ return self._status["motor_state"] == WL_CLOSING
+
+ @property
+ def is_closed(self):
+ """Return if the cover is closed or not."""
+ if "motor_state" not in self._status or "position_current" not in self._status:
+ return None
+ return (
+ self._status["motor_state"] == WL_STOPPED
+ and wilight_to_hass_position(self._status["position_current"]) == 0
+ )
+
+ async def async_open_cover(self, **kwargs):
+ """Open the cover."""
+ await self._client.cover_command(self._index, WL_OPEN)
+
+ async def async_close_cover(self, **kwargs):
+ """Close cover."""
+ await self._client.cover_command(self._index, WL_CLOSE)
+
+ async def async_set_cover_position(self, **kwargs):
+ """Move the cover to a specific position."""
+ position = hass_to_wilight_position(kwargs[ATTR_POSITION])
+ await self._client.set_cover_position(self._index, position)
+
+ async def async_stop_cover(self, **kwargs):
+ """Stop the cover."""
+ await self._client.cover_command(self._index, WL_STOP)
diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py
index d59b9398d9e9b6..e55413926ac3e0 100644
--- a/homeassistant/components/wilight/fan.py
+++ b/homeassistant/components/wilight/fan.py
@@ -1,7 +1,7 @@
"""Support for WiLight Fan."""
+from __future__ import annotations
from pywilight.const import (
- DOMAIN,
FAN_V1,
ITEM_FAN,
WL_DIRECTION_FORWARD,
@@ -14,19 +14,20 @@
from homeassistant.components.fan import (
DIRECTION_FORWARD,
- SPEED_HIGH,
- SPEED_LOW,
- SPEED_MEDIUM,
SUPPORT_DIRECTION,
SUPPORT_SET_SPEED,
FanEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
+from homeassistant.util.percentage import (
+ ordered_list_item_to_percentage,
+ percentage_to_ordered_list_item,
+)
-from . import WiLightDevice
+from . import DOMAIN, WiLightDevice
-SUPPORTED_SPEEDS = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
+ORDERED_NAMED_FAN_SPEEDS = [WL_SPEED_LOW, WL_SPEED_MEDIUM, WL_SPEED_HIGH]
SUPPORTED_FEATURES = SUPPORT_SET_SPEED | SUPPORT_DIRECTION
@@ -77,30 +78,34 @@ def is_on(self):
return self._status.get("direction", WL_DIRECTION_OFF) != WL_DIRECTION_OFF
@property
- def speed(self) -> str:
- """Return the current speed."""
- return self._status.get("speed", SPEED_HIGH)
+ def percentage(self) -> int | None:
+ """Return the current speed percentage."""
+ if (
+ "direction" in self._status
+ and self._status["direction"] == WL_DIRECTION_OFF
+ ):
+ return 0
+
+ wl_speed = self._status.get("speed")
+ if wl_speed is None:
+ return None
+ return ordered_list_item_to_percentage(ORDERED_NAMED_FAN_SPEEDS, wl_speed)
@property
- def speed_list(self) -> list:
- """Get the list of available speeds."""
- return SUPPORTED_SPEEDS
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return len(ORDERED_NAMED_FAN_SPEEDS)
@property
def current_direction(self) -> str:
"""Return the current direction of the fan."""
- if "direction" in self._status:
- if self._status["direction"] != WL_DIRECTION_OFF:
- self._direction = self._status["direction"]
+ if (
+ "direction" in self._status
+ and self._status["direction"] != WL_DIRECTION_OFF
+ ):
+ self._direction = self._status["direction"]
return self._direction
- #
- # The fan entity model has changed to use percentages and preset_modes
- # instead of speeds.
- #
- # Please review
- # https://developers.home-assistant.io/docs/core/entity/fan/
- #
async def async_turn_on(
self,
speed: str = None,
@@ -109,18 +114,22 @@ async def async_turn_on(
**kwargs,
) -> None:
"""Turn on the fan."""
- if speed is None:
+ if percentage is None:
await self._client.set_fan_direction(self._index, self._direction)
else:
- await self.async_set_speed(speed)
+ await self.async_set_percentage(percentage)
- async def async_set_speed(self, speed: str):
+ async def async_set_percentage(self, percentage: int):
"""Set the speed of the fan."""
- wl_speed = WL_SPEED_HIGH
- if speed == SPEED_LOW:
- wl_speed = WL_SPEED_LOW
- if speed == SPEED_MEDIUM:
- wl_speed = WL_SPEED_MEDIUM
+ if percentage == 0:
+ await self._client.set_fan_direction(self._index, WL_DIRECTION_OFF)
+ return
+ if (
+ "direction" in self._status
+ and self._status["direction"] == WL_DIRECTION_OFF
+ ):
+ await self._client.set_fan_direction(self._index, self._direction)
+ wl_speed = percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage)
await self._client.set_fan_speed(self._index, wl_speed)
async def async_set_direction(self, direction: str):
diff --git a/homeassistant/components/wilight/light.py b/homeassistant/components/wilight/light.py
index e4bf504165daf8..0c7206be00cde8 100644
--- a/homeassistant/components/wilight/light.py
+++ b/homeassistant/components/wilight/light.py
@@ -1,5 +1,13 @@
"""Support for WiLight lights."""
+from pywilight.const import (
+ ITEM_LIGHT,
+ LIGHT_COLOR,
+ LIGHT_DIMMER,
+ LIGHT_ON_OFF,
+ SUPPORT_NONE,
+)
+
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_HS_COLOR,
@@ -10,15 +18,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
-from . import WiLightDevice
-from .const import (
- DOMAIN,
- ITEM_LIGHT,
- LIGHT_COLOR,
- LIGHT_DIMMER,
- LIGHT_ON_OFF,
- SUPPORT_NONE,
-)
+from . import DOMAIN, WiLightDevice
def entities_from_discovered_wilight(hass, api_device):
diff --git a/homeassistant/components/wilight/manifest.json b/homeassistant/components/wilight/manifest.json
index c9f4fb049fce45..5b8a93c60392f2 100644
--- a/homeassistant/components/wilight/manifest.json
+++ b/homeassistant/components/wilight/manifest.json
@@ -3,7 +3,7 @@
"name": "WiLight",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wilight",
- "requirements": ["pywilight==0.0.66"],
+ "requirements": ["pywilight==0.0.68"],
"ssdp": [
{
"manufacturer": "All Automacao Ltda"
diff --git a/homeassistant/components/wilight/translations/de.json b/homeassistant/components/wilight/translations/de.json
index 07d00495af7f9d..d56e782279aa39 100644
--- a/homeassistant/components/wilight/translations/de.json
+++ b/homeassistant/components/wilight/translations/de.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
"flow_title": "WiLight: {name}",
"step": {
"confirm": {
diff --git a/homeassistant/components/wilight/translations/hu.json b/homeassistant/components/wilight/translations/hu.json
index 3b2d79a34a77e2..26cdaf6a025c85 100644
--- a/homeassistant/components/wilight/translations/hu.json
+++ b/homeassistant/components/wilight/translations/hu.json
@@ -2,6 +2,11 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "step": {
+ "confirm": {
+ "title": "WiLight"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wilight/translations/id.json b/homeassistant/components/wilight/translations/id.json
new file mode 100644
index 00000000000000..dae7b0bd16ac3c
--- /dev/null
+++ b/homeassistant/components/wilight/translations/id.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "not_supported_device": "Perangkat WiLight ini saat ini tidak didukung.",
+ "not_wilight_device": "Perangkat ini bukan perangkat WiLight"
+ },
+ "flow_title": "WiLight: {name}",
+ "step": {
+ "confirm": {
+ "description": "Apakah Anda ingin menyiapkan WiLight {name}?\n\nIni mendukung: {components}",
+ "title": "WiLight"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wilight/translations/ko.json b/homeassistant/components/wilight/translations/ko.json
index 677b104c065634..1b53a1ba544524 100644
--- a/homeassistant/components/wilight/translations/ko.json
+++ b/homeassistant/components/wilight/translations/ko.json
@@ -1,11 +1,15 @@
{
"config": {
"abort": {
- "not_wilight_device": "\uc774 \uc7a5\uce58\ub294 WiLight\uac00 \uc544\ub2d9\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "not_supported_device": "\uc774 WiLight\ub294 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4",
+ "not_wilight_device": "\uc774 \uae30\uae30\ub294 WiLight\uac00 \uc544\ub2d9\ub2c8\ub2e4"
},
+ "flow_title": "WiLight: {name}",
"step": {
"confirm": {
- "description": "WiLight {name} \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? \n\n \uc9c0\uc6d0 : {components}"
+ "description": "WiLight {name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?\n\n\uc9c0\uc6d0 \uae30\uae30: {components}",
+ "title": "WiLight"
}
}
}
diff --git a/homeassistant/components/wilight/translations/tr.json b/homeassistant/components/wilight/translations/tr.json
new file mode 100644
index 00000000000000..5307276a71d3a3
--- /dev/null
+++ b/homeassistant/components/wilight/translations/tr.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wilight/translations/uk.json b/homeassistant/components/wilight/translations/uk.json
new file mode 100644
index 00000000000000..7517538499e413
--- /dev/null
+++ b/homeassistant/components/wilight/translations/uk.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "not_supported_device": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0430\u0440\u0430\u0437\u0456 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.",
+ "not_wilight_device": "\u0426\u0435 \u043d\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 WiLight."
+ },
+ "flow_title": "WiLight: {name}",
+ "step": {
+ "confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 WiLight {name}? \n\n \u0426\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454: {components}",
+ "title": "WiLight"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py
index 26666bf4b15674..198bddc937b560 100644
--- a/homeassistant/components/wink/__init__.py
+++ b/homeassistant/components/wink/__init__.py
@@ -778,7 +778,7 @@ def should_poll(self):
return self.wink.pubnub_channel is None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attributes = {}
battery = self._battery_level
@@ -855,9 +855,9 @@ def icon(self):
return "mdi:bell-ring"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
- attributes = super().device_state_attributes
+ attributes = super().extra_state_attributes
auto_shutoff = self.wink.auto_shutoff()
if auto_shutoff is not None:
@@ -913,9 +913,9 @@ def name(self):
return f"{self.parent.name()} dial {self.wink.index() + 1}"
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
- attributes = super().device_state_attributes
+ attributes = super().extra_state_attributes
dial_attributes = self.dial_attributes()
return {**attributes, **dial_attributes}
diff --git a/homeassistant/components/wink/alarm_control_panel.py b/homeassistant/components/wink/alarm_control_panel.py
index 5c45cc7b03dc21..2f5ac83c6f528c 100644
--- a/homeassistant/components/wink/alarm_control_panel.py
+++ b/homeassistant/components/wink/alarm_control_panel.py
@@ -70,6 +70,6 @@ def alarm_arm_away(self, code=None):
self.wink.set_mode("away")
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {"private": self.wink.private()}
diff --git a/homeassistant/components/wink/binary_sensor.py b/homeassistant/components/wink/binary_sensor.py
index 77ff464a5bf523..6a5977c1dc2629 100644
--- a/homeassistant/components/wink/binary_sensor.py
+++ b/homeassistant/components/wink/binary_sensor.py
@@ -40,9 +40,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for sensor in pywink.get_sensors():
_id = sensor.object_id() + sensor.name()
- if _id not in hass.data[DOMAIN]["unique_ids"]:
- if sensor.capability() in SENSOR_TYPES:
- add_entities([WinkBinarySensorEntity(sensor, hass)])
+ if (
+ _id not in hass.data[DOMAIN]["unique_ids"]
+ and sensor.capability() in SENSOR_TYPES
+ ):
+ add_entities([WinkBinarySensorEntity(sensor, hass)])
for key in pywink.get_keys():
_id = key.object_id() + key.name()
@@ -119,18 +121,18 @@ def device_class(self):
return SENSOR_TYPES.get(self.capability)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
- return super().device_state_attributes
+ return super().extra_state_attributes
class WinkSmokeDetector(WinkBinarySensorEntity):
"""Representation of a Wink Smoke detector."""
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
- _attributes = super().device_state_attributes
+ _attributes = super().extra_state_attributes
_attributes["test_activated"] = self.wink.test_activated()
return _attributes
@@ -139,9 +141,9 @@ class WinkHub(WinkBinarySensorEntity):
"""Representation of a Wink Hub."""
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
- _attributes = super().device_state_attributes
+ _attributes = super().extra_state_attributes
_attributes["update_needed"] = self.wink.update_needed()
_attributes["firmware_version"] = self.wink.firmware_version()
_attributes["pairing_mode"] = self.wink.pairing_mode()
@@ -159,9 +161,9 @@ class WinkRemote(WinkBinarySensorEntity):
"""Representation of a Wink Lutron Connected bulb remote."""
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
- _attributes = super().device_state_attributes
+ _attributes = super().extra_state_attributes
_attributes["button_on_pressed"] = self.wink.button_on_pressed()
_attributes["button_off_pressed"] = self.wink.button_off_pressed()
_attributes["button_up_pressed"] = self.wink.button_up_pressed()
@@ -178,9 +180,9 @@ class WinkButton(WinkBinarySensorEntity):
"""Representation of a Wink Relay button."""
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device state attributes."""
- _attributes = super().device_state_attributes
+ _attributes = super().extra_state_attributes
_attributes["pressed"] = self.wink.pressed()
_attributes["long_pressed"] = self.wink.long_pressed()
return _attributes
diff --git a/homeassistant/components/wink/climate.py b/homeassistant/components/wink/climate.py
index 7ee05f0a729c81..4c783e6bde1568 100644
--- a/homeassistant/components/wink/climate.py
+++ b/homeassistant/components/wink/climate.py
@@ -99,7 +99,7 @@ def temperature_unit(self):
return TEMP_CELSIUS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the optional device state attributes."""
data = {}
if self.external_temperature is not None:
@@ -396,7 +396,7 @@ def temperature_unit(self):
return TEMP_CELSIUS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the optional device state attributes."""
data = {}
data[ATTR_TOTAL_CONSUMPTION] = self.wink.total_consumption()
diff --git a/homeassistant/components/wink/lock.py b/homeassistant/components/wink/lock.py
index f82b74e7712310..63a67d9f1ac3c1 100644
--- a/homeassistant/components/wink/lock.py
+++ b/homeassistant/components/wink/lock.py
@@ -187,9 +187,9 @@ def set_alarm_mode(self, mode):
self.wink.set_alarm_mode(mode)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
- super_attrs = super().device_state_attributes
+ super_attrs = super().extra_state_attributes
sensitivity = dict_value_to_key(
ALARM_SENSITIVITY_MAP, self.wink.alarm_sensitivity()
)
diff --git a/homeassistant/components/wink/sensor.py b/homeassistant/components/wink/sensor.py
index cd3eb756fb3d12..f640a24def2253 100644
--- a/homeassistant/components/wink/sensor.py
+++ b/homeassistant/components/wink/sensor.py
@@ -1,8 +1,10 @@
"""Support for Wink sensors."""
+from contextlib import suppress
import logging
import pywink
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEGREE, TEMP_CELSIUS
from . import DOMAIN, WinkDevice
@@ -17,31 +19,33 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for sensor in pywink.get_sensors():
_id = sensor.object_id() + sensor.name()
- if _id not in hass.data[DOMAIN]["unique_ids"]:
- if sensor.capability() in SENSOR_TYPES:
- add_entities([WinkSensorDevice(sensor, hass)])
+ if (
+ _id not in hass.data[DOMAIN]["unique_ids"]
+ and sensor.capability() in SENSOR_TYPES
+ ):
+ add_entities([WinkSensorEntity(sensor, hass)])
for eggtray in pywink.get_eggtrays():
_id = eggtray.object_id() + eggtray.name()
if _id not in hass.data[DOMAIN]["unique_ids"]:
- add_entities([WinkSensorDevice(eggtray, hass)])
+ add_entities([WinkSensorEntity(eggtray, hass)])
for tank in pywink.get_propane_tanks():
_id = tank.object_id() + tank.name()
if _id not in hass.data[DOMAIN]["unique_ids"]:
- add_entities([WinkSensorDevice(tank, hass)])
+ add_entities([WinkSensorEntity(tank, hass)])
for piggy_bank in pywink.get_piggy_banks():
_id = piggy_bank.object_id() + piggy_bank.name()
if _id not in hass.data[DOMAIN]["unique_ids"]:
try:
if piggy_bank.capability() in SENSOR_TYPES:
- add_entities([WinkSensorDevice(piggy_bank, hass)])
+ add_entities([WinkSensorEntity(piggy_bank, hass)])
except AttributeError:
_LOGGER.info("Device is not a sensor")
-class WinkSensorDevice(WinkDevice):
+class WinkSensorEntity(WinkDevice, SensorEntity):
"""Representation of a Wink sensor."""
def __init__(self, wink, hass):
@@ -83,12 +87,12 @@ def unit_of_measurement(self):
return self._unit_of_measurement
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
- super_attrs = super().device_state_attributes
- try:
+ super_attrs = super().extra_state_attributes
+
+ # Ignore error, this sensor isn't an eggminder
+ with suppress(AttributeError):
super_attrs["egg_times"] = self.wink.eggs()
- except AttributeError:
- # Ignore error, this sensor isn't an eggminder
- pass
+
return super_attrs
diff --git a/homeassistant/components/wink/switch.py b/homeassistant/components/wink/switch.py
index 2632036095ad37..d377ae0cddf5e1 100644
--- a/homeassistant/components/wink/switch.py
+++ b/homeassistant/components/wink/switch.py
@@ -48,9 +48,9 @@ def turn_off(self, **kwargs):
self.wink.set_state(False)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
- attributes = super().device_state_attributes
+ attributes = super().extra_state_attributes
try:
event = self.wink.last_event()
if event is not None:
diff --git a/homeassistant/components/wink/water_heater.py b/homeassistant/components/wink/water_heater.py
index 0ce31762c7a656..bf5e84347460bc 100644
--- a/homeassistant/components/wink/water_heater.py
+++ b/homeassistant/components/wink/water_heater.py
@@ -66,7 +66,7 @@ def temperature_unit(self):
return TEMP_CELSIUS
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the optional device state attributes."""
data = {}
data[ATTR_VACATION_MODE] = self.wink.vacation_mode_enabled()
diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py
index 83e92c2250b72d..5da19f54dcf34f 100644
--- a/homeassistant/components/wirelesstag/__init__.py
+++ b/homeassistant/components/wirelesstag/__init__.py
@@ -150,7 +150,7 @@ def binary_event_callback_url(self):
def handle_update_tags_event(self, event):
"""Handle push event from wireless tag manager."""
- _LOGGER.info("push notification for update arrived: %s", event)
+ _LOGGER.info("Push notification for update arrived: %s", event)
try:
tag_id = event.data.get("id")
mac = event.data.get("mac")
@@ -272,7 +272,7 @@ def update(self):
self._state = self.updated_state_value()
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_BATTERY_LEVEL: int(self._tag.battery_remaining * 100),
diff --git a/homeassistant/components/wirelesstag/sensor.py b/homeassistant/components/wirelesstag/sensor.py
index 2a845249028011..cc0ce0cb8882bb 100644
--- a/homeassistant/components/wirelesstag/sensor.py
+++ b/homeassistant/components/wirelesstag/sensor.py
@@ -3,7 +3,7 @@
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
@@ -44,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class WirelessTagSensor(WirelessTagBaseSensor):
+class WirelessTagSensor(WirelessTagBaseSensor, SensorEntity):
"""Representation of a Sensor."""
def __init__(self, api, tag, sensor_type, config):
diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py
index c6f420d172acf5..2cf6d297f124c7 100644
--- a/homeassistant/components/withings/__init__.py
+++ b/homeassistant/components/withings/__init__.py
@@ -3,13 +3,14 @@
For more details about this platform, please refer to the documentation at
"""
+from __future__ import annotations
+
import asyncio
-from typing import Optional, cast
from aiohttp.web import Request, Response
import voluptuous as vol
from withings_api import WithingsAuth
-from withings_api.common import NotifyAppli, enum_or_raise
+from withings_api.common import NotifyAppli
from homeassistant.components import webhook
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
@@ -20,7 +21,6 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_WEBHOOK_ID
from homeassistant.core import HomeAssistant, callback
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.typing import ConfigType
@@ -120,9 +120,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
data_manager = await async_get_data_manager(hass, entry)
_LOGGER.debug("Confirming %s is authenticated to withings", data_manager.profile)
- await data_manager.poll_data_update_coordinator.async_refresh()
- if not data_manager.poll_data_update_coordinator.last_update_success:
- raise ConfigEntryNotReady()
+ await data_manager.poll_data_update_coordinator.async_config_entry_first_refresh()
webhook.async_register(
hass,
@@ -175,7 +173,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_webhook_handler(
hass: HomeAssistant, webhook_id: str, request: Request
-) -> Optional[Response]:
+) -> Response | None:
"""Handle webhooks calls."""
# Handle http head calls to the path.
# When creating a notify subscription, Withings will check that the endpoint is running by sending a HEAD request.
@@ -195,9 +193,7 @@ async def async_webhook_handler(
return json_message_response("Parameter appli not provided", message_code=20)
try:
- appli = cast(
- NotifyAppli, enum_or_raise(int(params.getone("appli")), NotifyAppli)
- )
+ appli = NotifyAppli(int(params.getone("appli")))
except ValueError:
return json_message_response("Invalid appli provided", message_code=21)
diff --git a/homeassistant/components/withings/binary_sensor.py b/homeassistant/components/withings/binary_sensor.py
index 136a12a0e1d235..a7d0a80e8e32a6 100644
--- a/homeassistant/components/withings/binary_sensor.py
+++ b/homeassistant/components/withings/binary_sensor.py
@@ -1,5 +1,7 @@
"""Sensors flow for Withings."""
-from typing import Callable, List
+from __future__ import annotations
+
+from typing import Callable
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_OCCUPANCY,
@@ -16,7 +18,7 @@
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
entities = await async_create_entities(
diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py
index c08ddddf4a5f25..c0d9bcb2599f02 100644
--- a/homeassistant/components/withings/common.py
+++ b/homeassistant/components/withings/common.py
@@ -1,4 +1,6 @@
"""Common code for Withings."""
+from __future__ import annotations
+
import asyncio
from dataclasses import dataclass
import datetime
@@ -6,7 +8,7 @@
from enum import Enum, IntEnum
import logging
import re
-from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+from typing import Any, Callable, Dict
from aiohttp.web import Response
import requests
@@ -26,7 +28,7 @@
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
-from homeassistant.config_entries import ConfigEntry
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.const import (
CONF_WEBHOOK_ID,
HTTP_UNAUTHORIZED,
@@ -86,7 +88,7 @@ class WithingsAttribute:
measute_type: Enum
friendly_name: str
unit_of_measurement: str
- icon: Optional[str]
+ icon: str | None
platform: str
enabled_by_default: bool
update_type: UpdateType
@@ -461,12 +463,12 @@ class StateData:
),
]
-WITHINGS_MEASUREMENTS_MAP: Dict[Measurement, WithingsAttribute] = {
+WITHINGS_MEASUREMENTS_MAP: dict[Measurement, WithingsAttribute] = {
attr.measurement: attr for attr in WITHINGS_ATTRIBUTES
}
-WITHINGS_MEASURE_TYPE_MAP: Dict[
- Union[NotifyAppli, GetSleepSummaryField, MeasureType], WithingsAttribute
+WITHINGS_MEASURE_TYPE_MAP: dict[
+ NotifyAppli | GetSleepSummaryField | MeasureType, WithingsAttribute
] = {attr.measute_type: attr for attr in WITHINGS_ATTRIBUTES}
@@ -486,8 +488,8 @@ def __init__(
self.session = OAuth2Session(hass, config_entry, implementation)
def _request(
- self, path: str, params: Dict[str, Any], method: str = "GET"
- ) -> Dict[str, Any]:
+ self, path: str, params: dict[str, Any], method: str = "GET"
+ ) -> dict[str, Any]:
"""Perform an async request."""
asyncio.run_coroutine_threadsafe(
self.session.async_ensure_token_valid(), self._hass.loop
@@ -524,7 +526,7 @@ def __init__(self, hass: HomeAssistant, user_id: int) -> None:
"""Initialize the object."""
self._hass = hass
self._user_id = user_id
- self._listeners: List[CALLBACK_TYPE] = []
+ self._listeners: list[CALLBACK_TYPE] = []
self.data: MeasurementData = {}
def async_add_listener(self, listener: CALLBACK_TYPE) -> Callable[[], None]:
@@ -573,10 +575,8 @@ def __init__(
self._notify_unsubscribe_delay = datetime.timedelta(seconds=1)
self._is_available = True
- self._cancel_interval_update_interval: Optional[CALLBACK_TYPE] = None
- self._cancel_configure_webhook_subscribe_interval: Optional[
- CALLBACK_TYPE
- ] = None
+ self._cancel_interval_update_interval: CALLBACK_TYPE | None = None
+ self._cancel_configure_webhook_subscribe_interval: CALLBACK_TYPE | None = None
self._api_notification_id = f"withings_{self._user_id}"
self.subscription_update_coordinator = DataUpdateCoordinator(
@@ -600,7 +600,7 @@ def __init__(
self.webhook_update_coordinator = WebhookUpdateCoordinator(
self._hass, self._user_id
)
- self._cancel_subscription_update: Optional[Callable[[], None]] = None
+ self._cancel_subscription_update: Callable[[], None] | None = None
self._subscribe_webhook_run_count = 0
@property
@@ -681,7 +681,7 @@ async def _async_subscribe_webhook(self) -> None:
)
# Determine what subscriptions need to be created.
- ignored_applis = frozenset({NotifyAppli.USER})
+ ignored_applis = frozenset({NotifyAppli.USER, NotifyAppli.UNKNOWN})
to_add_applis = frozenset(
[
appli
@@ -728,7 +728,7 @@ async def _async_unsubscribe_webhook(self) -> None:
self._api.notify_revoke, profile.callbackurl, profile.appli
)
- async def async_get_all_data(self) -> Optional[Dict[MeasureType, Any]]:
+ async def async_get_all_data(self) -> dict[MeasureType, Any] | None:
"""Update all withings data."""
try:
return await self._do_retry(self._async_get_all_data)
@@ -740,7 +740,7 @@ async def async_get_all_data(self) -> Optional[Dict[MeasureType, Any]]:
context = {
const.PROFILE: self._profile,
"userid": self._user_id,
- "source": "reauth",
+ "source": SOURCE_REAUTH,
}
# Check if reauth flow already exists.
@@ -764,14 +764,14 @@ async def async_get_all_data(self) -> Optional[Dict[MeasureType, Any]]:
raise exception
- async def _async_get_all_data(self) -> Optional[Dict[MeasureType, Any]]:
+ async def _async_get_all_data(self) -> dict[MeasureType, Any] | None:
_LOGGER.info("Updating all withings data")
return {
**await self.async_get_measures(),
**await self.async_get_sleep_summary(),
}
- async def async_get_measures(self) -> Dict[MeasureType, Any]:
+ async def async_get_measures(self) -> dict[MeasureType, Any]:
"""Get the measures data."""
_LOGGER.debug("Updating withings measures")
@@ -794,7 +794,7 @@ async def async_get_measures(self) -> Dict[MeasureType, Any]:
for measure in group.measures
}
- async def async_get_sleep_summary(self) -> Dict[MeasureType, Any]:
+ async def async_get_sleep_summary(self) -> dict[MeasureType, Any]:
"""Get the sleep summary data."""
_LOGGER.debug("Updating withing sleep summary")
now = dt.utcnow()
@@ -837,7 +837,7 @@ def get_sleep_summary() -> SleepGetSummaryResponse:
response = await self._hass.async_add_executor_job(get_sleep_summary)
# Set the default to empty lists.
- raw_values: Dict[GetSleepSummaryField, List[int]] = {
+ raw_values: dict[GetSleepSummaryField, list[int]] = {
field: [] for field in GetSleepSummaryField
}
@@ -846,11 +846,11 @@ def get_sleep_summary() -> SleepGetSummaryResponse:
data = serie.data
for field in GetSleepSummaryField:
- raw_values[field].append(data._asdict()[field.value])
+ raw_values[field].append(dict(data)[field.value])
- values: Dict[GetSleepSummaryField, float] = {}
+ values: dict[GetSleepSummaryField, float] = {}
- def average(data: List[int]) -> float:
+ def average(data: list[int]) -> float:
return sum(data) / len(data)
def set_value(field: GetSleepSummaryField, func: Callable) -> None:
@@ -907,7 +907,7 @@ def get_attribute_unique_id(attribute: WithingsAttribute, user_id: int) -> str:
async def async_get_entity_id(
hass: HomeAssistant, attribute: WithingsAttribute, user_id: int
-) -> Optional[str]:
+) -> str | None:
"""Get an entity id for a user's attribute."""
entity_registry: EntityRegistry = (
await hass.helpers.entity_registry.async_get_registry()
@@ -936,7 +936,7 @@ def __init__(self, data_manager: DataManager, attribute: WithingsAttribute) -> N
self._user_id = self._data_manager.user_id
self._name = f"Withings {self._attribute.measurement.value} {self._profile}"
self._unique_id = get_attribute_unique_id(self._attribute, self._user_id)
- self._state_data: Optional[Any] = None
+ self._state_data: Any | None = None
@property
def should_poll(self) -> bool:
@@ -967,11 +967,6 @@ def unique_id(self) -> str:
"""Return a unique, Home Assistant friendly identifier for this entity."""
return self._unique_id
- @property
- def unit_of_measurement(self) -> str:
- """Return the unit of measurement of this entity, if any."""
- return self._attribute.unit_of_measurement
-
@property
def icon(self) -> str:
"""Icon to use in the frontend, if any."""
@@ -1053,7 +1048,7 @@ async def async_get_data_manager(
def get_data_manager_by_webhook_id(
hass: HomeAssistant, webhook_id: str
-) -> Optional[DataManager]:
+) -> DataManager | None:
"""Get a data manager by it's webhook id."""
return next(
iter(
@@ -1067,14 +1062,12 @@ def get_data_manager_by_webhook_id(
)
-def get_all_data_managers(hass: HomeAssistant) -> Tuple[DataManager, ...]:
+def get_all_data_managers(hass: HomeAssistant) -> tuple[DataManager, ...]:
"""Get all configured data managers."""
return tuple(
- [
- config_entry_data[const.DATA_MANAGER]
- for config_entry_data in hass.data[const.DOMAIN].values()
- if const.DATA_MANAGER in config_entry_data
- ]
+ config_entry_data[const.DATA_MANAGER]
+ for config_entry_data in hass.data[const.DOMAIN].values()
+ if const.DATA_MANAGER in config_entry_data
)
@@ -1088,7 +1081,7 @@ async def async_create_entities(
entry: ConfigEntry,
create_func: Callable[[DataManager, WithingsAttribute], Entity],
platform: str,
-) -> List[Entity]:
+) -> list[Entity]:
"""Create withings entities from config entry."""
data_manager = await async_get_data_manager(hass, entry)
@@ -1098,14 +1091,10 @@ async def async_create_entities(
]
-def get_platform_attributes(platform: str) -> Tuple[WithingsAttribute, ...]:
+def get_platform_attributes(platform: str) -> tuple[WithingsAttribute, ...]:
"""Get withings attributes used for a specific platform."""
return tuple(
- [
- attribute
- for attribute in WITHINGS_ATTRIBUTES
- if attribute.platform == platform
- ]
+ attribute for attribute in WITHINGS_ATTRIBUTES if attribute.platform == platform
)
diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py
index d04327808de0fc..f841c61fbcb813 100644
--- a/homeassistant/components/withings/config_flow.py
+++ b/homeassistant/components/withings/config_flow.py
@@ -1,12 +1,14 @@
"""Config flow for Withings."""
+from __future__ import annotations
+
import logging
-from typing import Dict, Union
import voluptuous as vol
from withings_api.common import AuthScope
from homeassistant import config_entries
from homeassistant.components.withings import const
+from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.util import slugify
@@ -19,7 +21,7 @@ class WithingsFlowHandler(
DOMAIN = const.DOMAIN
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
# Temporarily holds authorization data during the profile step.
- _current_data: Dict[str, Union[None, str, int]] = {}
+ _current_data: dict[str, None | str | int] = {}
@property
def logger(self) -> logging.Logger:
@@ -48,10 +50,9 @@ async def async_oauth_create_entry(self, data: dict) -> dict:
async def async_step_profile(self, data: dict) -> dict:
"""Prompt the user to select a user profile."""
errors = {}
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
reauth_profile = (
self.context.get(const.PROFILE)
- if self.context.get("source") == "reauth"
+ if self.context.get("source") == SOURCE_REAUTH
else None
)
profile = data.get(const.PROFILE) or reauth_profile
@@ -81,14 +82,12 @@ async def async_step_reauth(self, data: dict = None) -> dict:
if data is not None:
return await self.async_step_user()
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
placeholders = {const.PROFILE: self.context["profile"]}
self.context.update({"title_placeholders": placeholders})
return self.async_show_form(
step_id="reauth",
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
description_placeholders=placeholders,
)
diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json
index ec981ff691c4d4..6b2918722ba012 100644
--- a/homeassistant/components/withings/manifest.json
+++ b/homeassistant/components/withings/manifest.json
@@ -3,7 +3,7 @@
"name": "Withings",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/withings",
- "requirements": ["withings-api==2.1.6"],
+ "requirements": ["withings-api==2.3.2"],
"dependencies": ["http", "webhook"],
"codeowners": ["@vangorra"]
}
diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py
index a55d83d717bfdd..e26804f1f0a95b 100644
--- a/homeassistant/components/withings/sensor.py
+++ b/homeassistant/components/withings/sensor.py
@@ -1,7 +1,9 @@
"""Sensors flow for Withings."""
-from typing import Callable, List, Union
+from __future__ import annotations
-from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from typing import Callable
+
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
@@ -12,7 +14,7 @@
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up the sensor config entry."""
@@ -26,10 +28,15 @@ async def async_setup_entry(
async_add_entities(entities, True)
-class WithingsHealthSensor(BaseWithingsSensor):
+class WithingsHealthSensor(BaseWithingsSensor, SensorEntity):
"""Implementation of a Withings sensor."""
@property
- def state(self) -> Union[None, str, int, float]:
+ def state(self) -> None | str | int | float:
"""Return the state of the entity."""
return self._state_data
+
+ @property
+ def unit_of_measurement(self) -> str:
+ """Return the unit of measurement of this entity, if any."""
+ return self._attribute.unit_of_measurement
diff --git a/homeassistant/components/withings/translations/de.json b/homeassistant/components/withings/translations/de.json
index d217640e44b835..b4bd6a0c449ca2 100644
--- a/homeassistant/components/withings/translations/de.json
+++ b/homeassistant/components/withings/translations/de.json
@@ -1,22 +1,32 @@
{
"config": {
"abort": {
- "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Autorisierungs-URL.",
- "missing_configuration": "Die Withings-Integration ist nicht konfiguriert. Bitte folgen Sie der Dokumentation."
+ "already_configured": "Konfiguration des Profils aktualisiert.",
+ "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.",
+ "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.",
+ "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})."
},
"create_entry": {
"default": "Erfolgreiche Authentifizierung mit Withings."
},
+ "error": {
+ "already_configured": "Konto wurde bereits konfiguriert"
+ },
+ "flow_title": "Withings: {profile}",
"step": {
"pick_implementation": {
- "title": "Authentifizierungsmethode ausw\u00e4hlen"
+ "title": "W\u00e4hle die Authentifizierungsmethode"
},
"profile": {
"data": {
- "profile": "Profil"
+ "profile": "Profilname"
},
"description": "Welches Profil hast du auf der Withings-Website ausgew\u00e4hlt? Es ist wichtig, dass die Profile \u00fcbereinstimmen, da sonst die Daten falsch beschriftet werden.",
"title": "Benutzerprofil"
+ },
+ "reauth": {
+ "description": "Das Profil \"{profile}\" muss neu authentifiziert werden, um weiterhin Withings-Daten zu empfangen.",
+ "title": "Integration erneut authentifizieren"
}
}
}
diff --git a/homeassistant/components/withings/translations/fr.json b/homeassistant/components/withings/translations/fr.json
index 017a9e63078763..b5f524698f5e96 100644
--- a/homeassistant/components/withings/translations/fr.json
+++ b/homeassistant/components/withings/translations/fr.json
@@ -10,7 +10,7 @@
"default": "Authentifi\u00e9 avec succ\u00e8s \u00e0 Withings pour le profil s\u00e9lectionn\u00e9."
},
"error": {
- "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9"
+ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9"
},
"flow_title": "Withings: {profile}",
"step": {
diff --git a/homeassistant/components/withings/translations/hu.json b/homeassistant/components/withings/translations/hu.json
index 1486048adfc919..d39410b6d1732e 100644
--- a/homeassistant/components/withings/translations/hu.json
+++ b/homeassistant/components/withings/translations/hu.json
@@ -2,15 +2,19 @@
"config": {
"abort": {
"already_configured": "A profil konfigur\u00e1ci\u00f3ja friss\u00edtve.",
- "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.",
- "missing_configuration": "A Withings integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t."
+ "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.",
+ "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.",
+ "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz."
},
"create_entry": {
"default": "A Withings sikeresen hiteles\u00edtett."
},
+ "error": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ },
"step": {
"pick_implementation": {
- "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert"
+ "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert"
},
"profile": {
"data": {
@@ -18,6 +22,9 @@
},
"description": "Melyik profilt v\u00e1lasztottad ki a Withings weboldalon? Fontos, hogy a profilok egyeznek, k\u00fcl\u00f6nben az adatok helytelen c\u00edmk\u00e9vel lesznek ell\u00e1tva.",
"title": "Felhaszn\u00e1l\u00f3i profil."
+ },
+ "reauth": {
+ "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se"
}
}
}
diff --git a/homeassistant/components/withings/translations/id.json b/homeassistant/components/withings/translations/id.json
new file mode 100644
index 00000000000000..e254e61d91e26b
--- /dev/null
+++ b/homeassistant/components/withings/translations/id.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konfigurasi diperbarui untuk profil.",
+ "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.",
+ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.",
+ "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})"
+ },
+ "create_entry": {
+ "default": "Berhasil mengautentikasi dengan Withings."
+ },
+ "error": {
+ "already_configured": "Akun sudah dikonfigurasi"
+ },
+ "flow_title": "Withings: {profile}",
+ "step": {
+ "pick_implementation": {
+ "title": "Pilih Metode Autentikasi"
+ },
+ "profile": {
+ "data": {
+ "profile": "Nama Profil"
+ },
+ "description": "Berikan nama profil unik untuk data ini. Umumnya namanya sama dengan nama profil yang Anda pilih di langkah sebelumnya.",
+ "title": "Profil Pengguna."
+ },
+ "reauth": {
+ "description": "Profil \"{profile}\" perlu diautentikasi ulang untuk terus menerima data Withings.",
+ "title": "Autentikasi Ulang Integrasi"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/translations/it.json b/homeassistant/components/withings/translations/it.json
index 85baeb1f0e07cc..8fb4dee991866a 100644
--- a/homeassistant/components/withings/translations/it.json
+++ b/homeassistant/components/withings/translations/it.json
@@ -26,7 +26,7 @@
},
"reauth": {
"description": "Il profilo \"{profile}\" deve essere autenticato nuovamente per continuare a ricevere i dati Withings.",
- "title": "Reautenticare l'integrazione"
+ "title": "Autenticare nuovamente l'integrazione"
}
}
}
diff --git a/homeassistant/components/withings/translations/ko.json b/homeassistant/components/withings/translations/ko.json
index 902f3c77e686df..4823061de41e4b 100644
--- a/homeassistant/components/withings/translations/ko.json
+++ b/homeassistant/components/withings/translations/ko.json
@@ -2,12 +2,15 @@
"config": {
"abort": {
"already_configured": "\ud504\ub85c\ud544\uc5d0 \ub300\ud55c \uad6c\uc131\uc774 \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "missing_configuration": "Withings \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
- "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})"
+ "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
+ "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694."
},
"create_entry": {
- "default": "Withings \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "default": "Withings\ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"flow_title": "Withings: {profile}",
"step": {
@@ -23,7 +26,7 @@
},
"reauth": {
"description": "Withings \ub370\uc774\ud130\ub97c \uacc4\uc18d \uc218\uc2e0\ud558\ub824\uba74 \"{profile}\" \ud504\ub85c\ud544\uc744 \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4.",
- "title": "\ud504\ub85c\ud544 \uc7ac\uc778\uc99d\ud558\uae30"
+ "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30"
}
}
}
diff --git a/homeassistant/components/withings/translations/nl.json b/homeassistant/components/withings/translations/nl.json
index 21b2d6b11e9be0..23e110a1d60bd2 100644
--- a/homeassistant/components/withings/translations/nl.json
+++ b/homeassistant/components/withings/translations/nl.json
@@ -3,7 +3,7 @@
"abort": {
"already_configured": "Configuratie bijgewerkt voor profiel.",
"authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
- "missing_configuration": "De Withings integratie is niet geconfigureerd. Gelieve de documentatie te volgen.",
+ "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.",
"no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})"
},
"create_entry": {
@@ -12,19 +12,21 @@
"error": {
"already_configured": "Account is al geconfigureerd"
},
+ "flow_title": "Withings: {profile}",
"step": {
"pick_implementation": {
- "title": "Kies Authenticatiemethode"
+ "title": "Kies een authenticatie methode"
},
"profile": {
"data": {
"profile": "Profiel"
},
- "description": "Welk profiel hebt u op de website van Withings selecteren? Het is belangrijk dat de profielen overeenkomen, anders worden gegevens verkeerd gelabeld.",
+ "description": "Geef een unieke profielnaam op voor deze gegevens. Meestal is dit de naam van het profiel dat u in de vorige stap hebt geselecteerd.",
"title": "Gebruikersprofiel."
},
"reauth": {
- "title": "Profiel opnieuw verifi\u00ebren"
+ "description": "Het {profile} \" moet opnieuw worden geverifieerd om Withings-gegevens te blijven ontvangen.",
+ "title": "Verifieer de integratie opnieuw"
}
}
}
diff --git a/homeassistant/components/withings/translations/ru.json b/homeassistant/components/withings/translations/ru.json
index f493fa4594fd41..d8cfd6c0b3b3b1 100644
--- a/homeassistant/components/withings/translations/ru.json
+++ b/homeassistant/components/withings/translations/ru.json
@@ -26,7 +26,7 @@
},
"reauth": {
"description": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c \"{profile}\" \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 Withings.",
- "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f"
+ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f"
}
}
}
diff --git a/homeassistant/components/withings/translations/tr.json b/homeassistant/components/withings/translations/tr.json
new file mode 100644
index 00000000000000..4e0228708ea085
--- /dev/null
+++ b/homeassistant/components/withings/translations/tr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Profil i\u00e7in yap\u0131land\u0131rma g\u00fcncellendi."
+ },
+ "error": {
+ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "step": {
+ "profile": {
+ "data": {
+ "profile": "Profil Ad\u0131"
+ },
+ "title": "Kullan\u0131c\u0131 profili."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/translations/uk.json b/homeassistant/components/withings/translations/uk.json
new file mode 100644
index 00000000000000..5efc27042b1756
--- /dev/null
+++ b/homeassistant/components/withings/translations/uk.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u041e\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u043f\u0440\u043e\u0444\u0456\u043b\u044e.",
+ "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.",
+ "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443."
+ },
+ "create_entry": {
+ "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e."
+ },
+ "error": {
+ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "flow_title": "Withings: {profile}",
+ "step": {
+ "pick_implementation": {
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ },
+ "profile": {
+ "data": {
+ "profile": "\u041d\u0430\u0437\u0432\u0430 \u043f\u0440\u043e\u0444\u0456\u043b\u044e"
+ },
+ "description": "\u0412\u043a\u0430\u0436\u0456\u0442\u044c \u0443\u043d\u0456\u043a\u0430\u043b\u044c\u043d\u0435 \u0456\u043c'\u044f \u043f\u0440\u043e\u0444\u0456\u043b\u044e \u0434\u043b\u044f \u0446\u0438\u0445 \u0434\u0430\u043d\u0438\u0445. \u042f\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u043e, \u0446\u0435 \u043d\u0430\u0437\u0432\u0430, \u043e\u0431\u0440\u0430\u043d\u0430 \u043d\u0430 \u043f\u043e\u043f\u0435\u0440\u0435\u0434\u043d\u044c\u043e\u043c\u0443 \u043a\u0440\u043e\u0446\u0456.",
+ "title": "Withings"
+ },
+ "reauth": {
+ "description": "\u041f\u0440\u043e\u0444\u0456\u043b\u044c \"{profile}\" \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u043e\u0432\u0430\u043d\u0438\u0439 \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u0432\u0436\u0435\u043d\u043d\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0434\u0430\u043d\u0438\u0445 Withings.",
+ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py
index d8aacd59881ce3..a54635f26b842c 100644
--- a/homeassistant/components/wled/__init__.py
+++ b/homeassistant/components/wled/__init__.py
@@ -1,8 +1,10 @@
"""Support for WLED."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
import logging
-from typing import Any, Dict
+from typing import Any
from wled import WLED, Device as WLEDDevice, WLEDConnectionError, WLEDError
@@ -12,9 +14,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_NAME, CONF_HOST
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
@@ -30,25 +30,17 @@
)
SCAN_INTERVAL = timedelta(seconds=5)
-WLED_COMPONENTS = (LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN)
+PLATFORMS = (LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN)
_LOGGER = logging.getLogger(__name__)
-async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
- """Set up the WLED components."""
- return True
-
-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up WLED from a config entry."""
# Create WLED instance for this entry
coordinator = WLEDDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
- await coordinator.async_refresh()
-
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady
+ await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
@@ -60,9 +52,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
# Set up all platforms for this device/entry.
- for component in WLED_COMPONENTS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -72,19 +64,22 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload WLED config entry."""
# Unload entities for this entry/device.
- await asyncio.gather(
- *(
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in WLED_COMPONENTS
+ unload_ok = all(
+ await asyncio.gather(
+ *(
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ )
)
)
- # Cleanup
- del hass.data[DOMAIN][entry.entry_id]
+ if unload_ok:
+ del hass.data[DOMAIN][entry.entry_id]
+
if not hass.data[DOMAIN]:
del hass.data[DOMAIN]
- return True
+ return unload_ok
def wled_exception_handler(func):
@@ -182,7 +177,7 @@ class WLEDDeviceEntity(WLEDEntity):
"""Defines a WLED device entity."""
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return device information about this WLED device."""
return {
ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.mac_address)},
diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py
index 4d0f6bf1606fd3..a85a74fa94bc06 100644
--- a/homeassistant/components/wled/config_flow.py
+++ b/homeassistant/components/wled/config_flow.py
@@ -1,5 +1,7 @@
"""Config flow to configure the WLED integration."""
-from typing import Any, Dict, Optional
+from __future__ import annotations
+
+from typing import Any
import voluptuous as vol
from wled import WLED, WLEDConnectionError
@@ -13,7 +15,7 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
-from .const import DOMAIN # pylint: disable=unused-import
+from .const import DOMAIN
class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
@@ -23,14 +25,14 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL
async def async_step_user(
- self, user_input: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, user_input: ConfigType | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initiated by the user."""
return await self._handle_config_flow(user_input)
async def async_step_zeroconf(
- self, user_input: Optional[ConfigType] = None
- ) -> Dict[str, Any]:
+ self, user_input: ConfigType | None = None
+ ) -> dict[str, Any]:
"""Handle zeroconf discovery."""
if user_input is None:
return self.async_abort(reason="cannot_connect")
@@ -39,7 +41,6 @@ async def async_step_zeroconf(
host = user_input["hostname"].rstrip(".")
name, _ = host.rsplit(".")
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update(
{
CONF_HOST: user_input["host"],
@@ -54,15 +55,14 @@ async def async_step_zeroconf(
async def async_step_zeroconf_confirm(
self, user_input: ConfigType = None
- ) -> Dict[str, Any]:
+ ) -> dict[str, Any]:
"""Handle a flow initiated by zeroconf."""
return await self._handle_config_flow(user_input)
async def _handle_config_flow(
- self, user_input: Optional[ConfigType] = None, prepare: bool = False
- ) -> Dict[str, Any]:
+ self, user_input: ConfigType | None = None, prepare: bool = False
+ ) -> dict[str, Any]:
"""Config flow handler for WLED."""
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
source = self.context.get("source")
# Request user input, unless we are preparing discovery flow
@@ -72,7 +72,6 @@ async def _handle_config_flow(
return self._show_setup_form()
if source == SOURCE_ZEROCONF:
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
user_input[CONF_HOST] = self.context.get(CONF_HOST)
user_input[CONF_MAC] = self.context.get(CONF_MAC)
@@ -93,7 +92,6 @@ async def _handle_config_flow(
title = user_input[CONF_HOST]
if source == SOURCE_ZEROCONF:
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
title = self.context.get(CONF_NAME)
if prepare:
@@ -104,7 +102,7 @@ async def _handle_config_flow(
data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: user_input[CONF_MAC]},
)
- def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
+ def _show_setup_form(self, errors: dict | None = None) -> dict[str, Any]:
"""Show the setup form to the user."""
return self.async_show_form(
step_id="user",
@@ -112,9 +110,8 @@ def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
errors=errors or {},
)
- def _show_confirm_dialog(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
+ def _show_confirm_dialog(self, errors: dict | None = None) -> dict[str, Any]:
"""Show the confirm dialog to the user."""
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
name = self.context.get(CONF_NAME)
return self.async_show_form(
step_id="zeroconf_confirm",
diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py
index 527d985a47b2e3..9de7eafc042d12 100644
--- a/homeassistant/components/wled/light.py
+++ b/homeassistant/components/wled/light.py
@@ -1,6 +1,8 @@
"""Support for LED lights."""
+from __future__ import annotations
+
from functools import partial
-from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+from typing import Any, Callable
import voluptuous as vol
@@ -51,7 +53,7 @@
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up WLED light based on a config entry."""
coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
@@ -115,7 +117,7 @@ def supported_features(self) -> int:
return SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
@property
- def brightness(self) -> Optional[int]:
+ def brightness(self) -> int | None:
"""Return the brightness of this light between 1..255."""
return self.coordinator.data.state.brightness
@@ -149,6 +151,27 @@ async def async_turn_on(self, **kwargs: Any) -> None:
await self.coordinator.wled.master(**data)
+ async def async_effect(
+ self,
+ effect: int | str | None = None,
+ intensity: int | None = None,
+ palette: int | str | None = None,
+ reverse: bool | None = None,
+ speed: int | None = None,
+ ) -> None:
+ """Set the effect of a WLED light."""
+ # Master light does not have an effect setting.
+
+ @wled_exception_handler
+ async def async_preset(
+ self,
+ preset: int,
+ ) -> None:
+ """Set a WLED light to a saved preset."""
+ data = {ATTR_PRESET: preset}
+
+ await self.coordinator.wled.preset(**data)
+
class WLEDSegmentLight(LightEntity, WLEDDeviceEntity):
"""Defines a WLED light based on a segment."""
@@ -188,7 +211,7 @@ def available(self) -> bool:
return super().available
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the entity."""
playlist = self.coordinator.data.state.playlist
if playlist == -1:
@@ -209,18 +232,18 @@ def device_state_attributes(self) -> Optional[Dict[str, Any]]:
}
@property
- def hs_color(self) -> Optional[Tuple[float, float]]:
+ def hs_color(self) -> tuple[float, float] | None:
"""Return the hue and saturation color value [float, float]."""
color = self.coordinator.data.state.segments[self._segment].color_primary
return color_util.color_RGB_to_hs(*color[:3])
@property
- def effect(self) -> Optional[str]:
+ def effect(self) -> str | None:
"""Return the current effect of the light."""
return self.coordinator.data.state.segments[self._segment].effect.name
@property
- def brightness(self) -> Optional[int]:
+ def brightness(self) -> int | None:
"""Return the brightness of this light between 1..255."""
state = self.coordinator.data.state
@@ -234,7 +257,7 @@ def brightness(self) -> Optional[int]:
return state.segments[self._segment].brightness
@property
- def white_value(self) -> Optional[int]:
+ def white_value(self) -> int | None:
"""Return the white value of this light between 0..255."""
color = self.coordinator.data.state.segments[self._segment].color_primary
return color[-1] if self._rgbw else None
@@ -256,7 +279,7 @@ def supported_features(self) -> int:
return flags
@property
- def effect_list(self) -> List[str]:
+ def effect_list(self) -> list[str]:
"""Return the list of supported effects."""
return [effect.name for effect in self.coordinator.data.effects]
@@ -357,11 +380,11 @@ async def async_turn_on(self, **kwargs: Any) -> None:
@wled_exception_handler
async def async_effect(
self,
- effect: Optional[Union[int, str]] = None,
- intensity: Optional[int] = None,
- palette: Optional[Union[int, str]] = None,
- reverse: Optional[bool] = None,
- speed: Optional[int] = None,
+ effect: int | str | None = None,
+ intensity: int | None = None,
+ palette: int | str | None = None,
+ reverse: bool | None = None,
+ speed: int | None = None,
) -> None:
"""Set the effect of a WLED light."""
data = {ATTR_SEGMENT_ID: self._segment}
@@ -398,7 +421,7 @@ async def async_preset(
def async_update_segments(
entry: ConfigEntry,
coordinator: WLEDDataUpdateCoordinator,
- current: Dict[int, WLEDSegmentLight],
+ current: dict[int, WLEDSegmentLight],
async_add_entities,
) -> None:
"""Update segments."""
@@ -438,11 +461,11 @@ def async_update_segments(
async def async_remove_entity(
index: int,
coordinator: WLEDDataUpdateCoordinator,
- current: Dict[int, WLEDSegmentLight],
+ current: dict[int, WLEDSegmentLight],
) -> None:
"""Remove WLED segment light from Home Assistant."""
entity = current[index]
- await entity.async_remove()
+ await entity.async_remove(force_remove=True)
registry = await async_get_entity_registry(coordinator.hass)
if entity.entity_id in registry.entities:
registry.async_remove(entity.entity_id)
diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py
index 89d76776a82fb9..7e91f81dea0863 100644
--- a/homeassistant/components/wled/sensor.py
+++ b/homeassistant/components/wled/sensor.py
@@ -1,8 +1,10 @@
"""Support for WLED sensors."""
+from __future__ import annotations
+
from datetime import timedelta
-from typing import Any, Callable, Dict, List, Optional
+from typing import Any, Callable
-from homeassistant.components.sensor import DEVICE_CLASS_CURRENT
+from homeassistant.components.sensor import DEVICE_CLASS_CURRENT, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
DATA_BYTES,
@@ -22,7 +24,7 @@
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up WLED sensor based on a config entry."""
coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
@@ -40,7 +42,7 @@ async def async_setup_entry(
async_add_entities(sensors, True)
-class WLEDSensor(WLEDDeviceEntity):
+class WLEDSensor(WLEDDeviceEntity, SensorEntity):
"""Defines a WLED sensor."""
def __init__(
@@ -52,7 +54,7 @@ def __init__(
icon: str,
key: str,
name: str,
- unit_of_measurement: Optional[str] = None,
+ unit_of_measurement: str | None = None,
) -> None:
"""Initialize WLED sensor."""
self._unit_of_measurement = unit_of_measurement
@@ -92,7 +94,7 @@ def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> Non
)
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the entity."""
return {
ATTR_LED_COUNT: self.coordinator.data.info.leds.count,
@@ -105,7 +107,7 @@ def state(self) -> int:
return self.coordinator.data.info.leds.power
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the class of this sensor."""
return DEVICE_CLASS_CURRENT
@@ -131,7 +133,7 @@ def state(self) -> str:
return uptime.replace(microsecond=0).isoformat()
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the class of this sensor."""
return DEVICE_CLASS_TIMESTAMP
@@ -199,7 +201,7 @@ def state(self) -> int:
return self.coordinator.data.info.wifi.rssi
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the class of this sensor."""
return DEVICE_CLASS_SIGNAL_STRENGTH
diff --git a/homeassistant/components/wled/services.yaml b/homeassistant/components/wled/services.yaml
index 1f1fa1b809d59e..3ade18cb70e242 100644
--- a/homeassistant/components/wled/services.yaml
+++ b/homeassistant/components/wled/services.yaml
@@ -1,30 +1,66 @@
effect:
- description: Controls the effect settings of WLED
+ name: Set effect
+ description: Control the effect settings of WLED.
+ target:
+ entity:
+ integration: wled
+ domain: light
fields:
- entity_id:
- description: Name of the WLED light entity.
- example: "light.wled"
effect:
+ name: Effect
description: Name or ID of the WLED light effect.
example: "Rainbow"
+ selector:
+ text:
intensity:
+ name: Effect intensity
description: Intensity of the effect. Number between 0 and 255.
example: 100
+ selector:
+ number:
+ min: 0
+ max: 255
+ step: 1
+ mode: slider
palette:
+ name: Color palette
description: Name or ID of the WLED light palette.
example: "Tiamat"
+ selector:
+ text:
speed:
+ name: Effect speed
description: Speed of the effect. Number between 0 (slow) and 255 (fast).
example: 150
+ selector:
+ number:
+ min: 0
+ max: 255
+ step: 1
+ mode: slider
reverse:
- description: Reverse the effect. Either true to reverse or false otherwise.
+ name: Reverse effect
+ description:
+ Reverse the effect. Either true to reverse or false otherwise.
+ default: false
example: false
+ selector:
+ boolean:
+
preset:
- description: Calls a preset on the WLED device
+ name: Set preset
+ description: Set a preset for the WLED device.
+ target:
+ entity:
+ integration: wled
+ domain: light
fields:
- entity_id:
- description: Name of the WLED light entity.
- example: "light.wled"
preset:
+ name: Preset ID
description: ID of the WLED preset
example: 6
+ selector:
+ number:
+ min: -1
+ max: 65535
+ mode: box
diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py
index 38ebd0e9b29881..5902cd246a0181 100644
--- a/homeassistant/components/wled/switch.py
+++ b/homeassistant/components/wled/switch.py
@@ -1,5 +1,7 @@
"""Support for WLED switches."""
-from typing import Any, Callable, Dict, List, Optional
+from __future__ import annotations
+
+from typing import Any, Callable
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
@@ -21,7 +23,7 @@
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up WLED switch based on a config entry."""
coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
@@ -72,7 +74,7 @@ def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> Non
)
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the entity."""
return {
ATTR_DURATION: self.coordinator.data.state.nightlight.duration,
@@ -110,7 +112,7 @@ def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> Non
)
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the entity."""
return {ATTR_UDP_PORT: self.coordinator.data.info.udp_port}
@@ -144,7 +146,7 @@ def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator):
)
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
+ def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the entity."""
return {ATTR_UDP_PORT: self.coordinator.data.info.udp_port}
diff --git a/homeassistant/components/wled/translations/de.json b/homeassistant/components/wled/translations/de.json
index ff12e429bd6c14..0dd13f763d6f52 100644
--- a/homeassistant/components/wled/translations/de.json
+++ b/homeassistant/components/wled/translations/de.json
@@ -1,7 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Dieses WLED-Ger\u00e4t ist bereits konfiguriert."
+ "already_configured": "Dieses WLED-Ger\u00e4t ist bereits konfiguriert.",
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
"flow_title": "WLED: {name}",
"step": {
diff --git a/homeassistant/components/wled/translations/hu.json b/homeassistant/components/wled/translations/hu.json
index b89bd72f704691..0d2c85e477d7ef 100644
--- a/homeassistant/components/wled/translations/hu.json
+++ b/homeassistant/components/wled/translations/hu.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Ez a WLED eszk\u00f6z m\u00e1r konfigur\u00e1lva van.",
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
"error": {
diff --git a/homeassistant/components/wled/translations/id.json b/homeassistant/components/wled/translations/id.json
new file mode 100644
index 00000000000000..6437dfaf83e142
--- /dev/null
+++ b/homeassistant/components/wled/translations/id.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "cannot_connect": "Gagal terhubung"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "flow_title": "WLED: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Siapkan WLED Anda untuk diintegrasikan dengan Home Assistant."
+ },
+ "zeroconf_confirm": {
+ "description": "Ingin menambahkan WLED `{name}` ke Home Assistant?",
+ "title": "Peranti WLED yang ditemukan"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wled/translations/ko.json b/homeassistant/components/wled/translations/ko.json
index 2adb7985fd3c5c..69f30eb7516a3e 100644
--- a/homeassistant/components/wled/translations/ko.json
+++ b/homeassistant/components/wled/translations/ko.json
@@ -1,7 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "\uc774 WLED \uae30\uae30\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
"flow_title": "WLED: {name}",
"step": {
@@ -9,10 +13,10 @@
"data": {
"host": "\ud638\uc2a4\ud2b8"
},
- "description": "Home Assistant \uc5d0 WLED \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4."
+ "description": "Home Assistant\uc5d0 WLED \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4."
},
"zeroconf_confirm": {
- "description": "Home Assistant \uc5d0 WLED `{name}` \uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "description": "Home Assistant\uc5d0 WLED `{name}`\uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "\ubc1c\uacac\ub41c WLED \uae30\uae30"
}
}
diff --git a/homeassistant/components/wled/translations/nl.json b/homeassistant/components/wled/translations/nl.json
index 329716e2cd5850..3e7b16a7f4a3c6 100644
--- a/homeassistant/components/wled/translations/nl.json
+++ b/homeassistant/components/wled/translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Dit WLED-apparaat is al geconfigureerd.",
+ "already_configured": "Apparaat is al geconfigureerd",
"cannot_connect": "Kan geen verbinding maken"
},
"error": {
@@ -11,7 +11,7 @@
"step": {
"user": {
"data": {
- "host": "Hostnaam of IP-adres"
+ "host": "Host"
},
"description": "Stel uw WLED-integratie in met Home Assistant."
},
diff --git a/homeassistant/components/wled/translations/sv.json b/homeassistant/components/wled/translations/sv.json
index 3c802a87007f64..aea858c5bfcbbb 100644
--- a/homeassistant/components/wled/translations/sv.json
+++ b/homeassistant/components/wled/translations/sv.json
@@ -1,10 +1,19 @@
{
"config": {
+ "abort": {
+ "already_configured": "Enheten har redan konfigurerats"
+ },
+ "flow_title": "WLED: {name}",
"step": {
"user": {
"data": {
"host": "V\u00e4rd eller IP-adress"
- }
+ },
+ "description": "St\u00e4ll in din WLED f\u00f6r att integrera med Home Assistant."
+ },
+ "zeroconf_confirm": {
+ "description": "Vill du l\u00e4gga till WLED-enheten `{name}` till Home Assistant?",
+ "title": "Uppt\u00e4ckte WLED-enhet"
}
}
}
diff --git a/homeassistant/components/wled/translations/tr.json b/homeassistant/components/wled/translations/tr.json
new file mode 100644
index 00000000000000..f02764c8abab8b
--- /dev/null
+++ b/homeassistant/components/wled/translations/tr.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "flow_title": "WLED: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ },
+ "description": "WLED'inizi Home Assistant ile t\u00fcmle\u015ftirmek i\u00e7in ayarlay\u0131n."
+ },
+ "zeroconf_confirm": {
+ "description": "Home Assistant'a '{name}' adl\u0131 WLED'i eklemek istiyor musunuz?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wled/translations/uk.json b/homeassistant/components/wled/translations/uk.json
new file mode 100644
index 00000000000000..c0280d33993a99
--- /dev/null
+++ b/homeassistant/components/wled/translations/uk.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "flow_title": "WLED: {name}",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 WLED."
+ },
+ "zeroconf_confirm": {
+ "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 WLED `{name}`?",
+ "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 WLED"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/config_flow.py b/homeassistant/components/wolflink/config_flow.py
index f54789cef78881..20dbd8ef9b79dd 100644
--- a/homeassistant/components/wolflink/config_flow.py
+++ b/homeassistant/components/wolflink/config_flow.py
@@ -9,12 +9,7 @@
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from .const import ( # pylint:disable=unused-import
- DEVICE_GATEWAY,
- DEVICE_ID,
- DEVICE_NAME,
- DOMAIN,
-)
+from .const import DEVICE_GATEWAY, DEVICE_ID, DEVICE_NAME, DOMAIN
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/wolflink/sensor.py b/homeassistant/components/wolflink/sensor.py
index 201979d4dc3349..f243160ff59082 100644
--- a/homeassistant/components/wolflink/sensor.py
+++ b/homeassistant/components/wolflink/sensor.py
@@ -9,6 +9,7 @@
Temperature,
)
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
@@ -46,7 +47,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities, True)
-class WolfLinkSensor(CoordinatorEntity):
+class WolfLinkSensor(CoordinatorEntity, SensorEntity):
"""Base class for all Wolf entities."""
def __init__(self, coordinator, wolf_object: Parameter, device_id):
@@ -69,7 +70,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
"parameter_id": self.wolf_object.parameter_id,
diff --git a/homeassistant/components/wolflink/translations/de.json b/homeassistant/components/wolflink/translations/de.json
index cb7e571d1e6998..71f48a6413dcad 100644
--- a/homeassistant/components/wolflink/translations/de.json
+++ b/homeassistant/components/wolflink/translations/de.json
@@ -1,5 +1,13 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung",
+ "unknown": "Unerwarteter Fehler"
+ },
"step": {
"device": {
"data": {
diff --git a/homeassistant/components/wolflink/translations/hu.json b/homeassistant/components/wolflink/translations/hu.json
index 3b2d79a34a77e2..c7bb483155de36 100644
--- a/homeassistant/components/wolflink/translations/hu.json
+++ b/homeassistant/components/wolflink/translations/hu.json
@@ -2,6 +2,24 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "step": {
+ "device": {
+ "data": {
+ "device_name": "Eszk\u00f6z"
+ }
+ },
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/translations/id.json b/homeassistant/components/wolflink/translations/id.json
new file mode 100644
index 00000000000000..64d692efa7d4cb
--- /dev/null
+++ b/homeassistant/components/wolflink/translations/id.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "invalid_auth": "Autentikasi tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "step": {
+ "device": {
+ "data": {
+ "device_name": "Perangkat"
+ },
+ "title": "Pilih perangkat WOLF"
+ },
+ "user": {
+ "data": {
+ "password": "Kata Sandi",
+ "username": "Nama Pengguna"
+ },
+ "title": "Koneksi WOLF SmartSet"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/translations/nl.json b/homeassistant/components/wolflink/translations/nl.json
index 7fb1b867cdd8da..069ed92896244f 100644
--- a/homeassistant/components/wolflink/translations/nl.json
+++ b/homeassistant/components/wolflink/translations/nl.json
@@ -4,15 +4,23 @@
"already_configured": "Apparaat is al geconfigureerd"
},
"error": {
+ "cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"unknown": "Onverwachte fout"
},
"step": {
+ "device": {
+ "data": {
+ "device_name": "Apparaat"
+ },
+ "title": "Selecteer WOLF-apparaat"
+ },
"user": {
"data": {
"password": "Wachtwoord",
"username": "Gebruikersnaam"
- }
+ },
+ "title": "WOLF SmartSet-verbinding"
}
}
}
diff --git a/homeassistant/components/wolflink/translations/ru.json b/homeassistant/components/wolflink/translations/ru.json
index 841f7b26030b9e..8a430c9a63dedf 100644
--- a/homeassistant/components/wolflink/translations/ru.json
+++ b/homeassistant/components/wolflink/translations/ru.json
@@ -5,7 +5,7 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
@@ -18,7 +18,7 @@
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "username": "\u041b\u043e\u0433\u0438\u043d"
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"title": "WOLF SmartSet"
}
diff --git a/homeassistant/components/wolflink/translations/sensor.cs.json b/homeassistant/components/wolflink/translations/sensor.cs.json
index 046fc4e6ed9a0c..fff383f8b8d4d0 100644
--- a/homeassistant/components/wolflink/translations/sensor.cs.json
+++ b/homeassistant/components/wolflink/translations/sensor.cs.json
@@ -3,7 +3,7 @@
"wolflink__state": {
"aktiviert": "Aktivov\u00e1no",
"aus": "Zak\u00e1z\u00e1no",
- "auto": "Automatika",
+ "auto": "Auto",
"automatik_aus": "Automatick\u00e9 vypnut\u00ed",
"automatik_ein": "Automatick\u00e9 zapnut\u00ed",
"cooling": "Chlazen\u00ed",
diff --git a/homeassistant/components/wolflink/translations/sensor.de.json b/homeassistant/components/wolflink/translations/sensor.de.json
index 373e19895784ed..17c365e88c4c58 100644
--- a/homeassistant/components/wolflink/translations/sensor.de.json
+++ b/homeassistant/components/wolflink/translations/sensor.de.json
@@ -1,6 +1,8 @@
{
"state": {
"wolflink__state": {
+ "partymodus": "Party-Modus",
+ "permanent": "Permanent",
"solarbetrieb": "Solarmodus",
"sparbetrieb": "Sparmodus",
"sparen": "Sparen",
diff --git a/homeassistant/components/wolflink/translations/sensor.hu.json b/homeassistant/components/wolflink/translations/sensor.hu.json
new file mode 100644
index 00000000000000..2d8cdda9315872
--- /dev/null
+++ b/homeassistant/components/wolflink/translations/sensor.hu.json
@@ -0,0 +1,7 @@
+{
+ "state": {
+ "wolflink__state": {
+ "permanent": "\u00c1lland\u00f3"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/translations/sensor.id.json b/homeassistant/components/wolflink/translations/sensor.id.json
new file mode 100644
index 00000000000000..12b755a9c24846
--- /dev/null
+++ b/homeassistant/components/wolflink/translations/sensor.id.json
@@ -0,0 +1,47 @@
+{
+ "state": {
+ "wolflink__state": {
+ "1_x_warmwasser": "1 x DHW",
+ "aktiviert": "Diaktifkan",
+ "antilegionellenfunktion": "Fungsi Anti-legionella",
+ "aus": "Dinonaktifkan",
+ "auto": "Otomatis",
+ "auto_off_cool": "AutoOffCool",
+ "auto_on_cool": "AutoOnCool",
+ "automatik_aus": "Otomatis MATI",
+ "automatik_ein": "Otomatis NYALA",
+ "bereit_keine_ladung": "Siap, tidak memuat",
+ "betrieb_ohne_brenner": "Bekerja tanpa pembakar",
+ "cooling": "Mendinginkan",
+ "deaktiviert": "Tidak aktif",
+ "dhw_prior": "DHWPrior",
+ "eco": "Eco",
+ "ein": "Diaktifkan",
+ "externe_deaktivierung": "Penonaktifan eksternal",
+ "fernschalter_ein": "Kontrol jarak jauh diaktifkan",
+ "heizung": "Memanaskan",
+ "initialisierung": "Inisialisasi",
+ "kalibration": "Kalibrasi",
+ "kalibration_heizbetrieb": "Kalibrasi mode pemanasan",
+ "kalibration_kombibetrieb": "Kalibrasi mode kombi",
+ "kalibration_warmwasserbetrieb": "Kalibrasi DHW",
+ "kaskadenbetrieb": "Operasi bertingkat",
+ "kombibetrieb": "Mode kombi",
+ "mindest_kombizeit": "Waktu kombi minimum",
+ "nur_heizgerat": "Hanya boiler",
+ "parallelbetrieb": "Mode paralel",
+ "partymodus": "Mode pesta",
+ "permanent": "Permanen",
+ "permanentbetrieb": "Mode permanen",
+ "reduzierter_betrieb": "Mode terbatas",
+ "schornsteinfeger": "Uji emisi",
+ "smart_grid": "SmartGrid",
+ "smart_home": "SmartHome",
+ "solarbetrieb": "Mode surya",
+ "sparbetrieb": "Mode ekonomi",
+ "sparen": "Ekonomi",
+ "urlaubsmodus": "Mode liburan",
+ "ventilprufung": "Uji katup"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/translations/sensor.ko.json b/homeassistant/components/wolflink/translations/sensor.ko.json
index 5b6c33d7231004..2597560be1bf71 100644
--- a/homeassistant/components/wolflink/translations/sensor.ko.json
+++ b/homeassistant/components/wolflink/translations/sensor.ko.json
@@ -8,13 +8,80 @@
"aktiviert": "\ud65c\uc131\ud654",
"antilegionellenfunktion": "\ud56d \ub808\uc9c0\uc624\ub12c\ub77c\uade0 \uae30\ub2a5",
"at_abschaltung": "OT \ub044\uae30",
- "at_frostschutz": "OT \ube59\uacb0 \ubcf4\ud638",
+ "at_frostschutz": "OT \ub3d9\ud30c \ubc29\uc9c0",
"aus": "\ube44\ud65c\uc131\ud654",
"auto": "\uc790\ub3d9",
"auto_off_cool": "\ub0c9\ubc29 \uc790\ub3d9 \uaebc\uc9d0",
"auto_on_cool": "\ub0c9\ubc29 \uc790\ub3d9 \ucf1c\uc9d0",
"automatik_aus": "\uc790\ub3d9 \uaebc\uc9d0",
- "automatik_ein": "\uc790\ub3d9 \ucf1c\uc9d0"
+ "automatik_ein": "\uc790\ub3d9 \ucf1c\uc9d0",
+ "bereit_keine_ladung": "\uc900\ube44\ub428, \ub85c\ub4dc\ub418\uc9c0\ub294 \uc54a\uc74c",
+ "betrieb_ohne_brenner": "\ubc84\ub108 \uc5c6\uc774 \uc791\ub3d9",
+ "cooling": "\ub0c9\ubc29",
+ "deaktiviert": "\ube44\ud65c\uc131",
+ "dhw_prior": "DHW \uc6b0\uc120",
+ "eco": "\uc808\uc57d",
+ "ein": "\ud65c\uc131\ud654",
+ "estrichtrocknung": "\uc7a5\uae30\uac04 \uc81c\uc2b5",
+ "externe_deaktivierung": "\uc678\ubd80 \ube44\ud65c\uc131\ud654",
+ "fernschalter_ein": "\uc6d0\uaca9 \uc81c\uc5b4 \ud65c\uc131\ud654",
+ "frost_heizkreis": "\ub09c\ubc29 \ud68c\ub85c \ub3d9\ud30c",
+ "frost_warmwasser": "DHW \ub3d9\ud30c",
+ "frostschutz": "\ub3d9\ud30c \ubc29\uc9c0",
+ "gasdruck": "\uac00\uc2a4 \uc555\ub825",
+ "glt_betrieb": "BMS \ubaa8\ub4dc",
+ "gradienten_uberwachung": "\uae30\uc6b8\uae30 \ubaa8\ub2c8\ud130\ub9c1",
+ "heizbetrieb": "\ub09c\ubc29 \ubaa8\ub4dc",
+ "heizgerat_mit_speicher": "\uc2e4\ub9b0\ub354 \ubcf4\uc77c\ub7ec",
+ "heizung": "\ub09c\ubc29",
+ "initialisierung": "\ucd08\uae30\ud654",
+ "kalibration": "\ubcf4\uc815",
+ "kalibration_heizbetrieb": "\ub09c\ubc29 \ubaa8\ub4dc \ubcf4\uc815",
+ "kalibration_kombibetrieb": "\ucf64\ube44 \ubaa8\ub4dc \ubcf4\uc815",
+ "kalibration_warmwasserbetrieb": "DHW \ubcf4\uc815",
+ "kaskadenbetrieb": "\uce90\uc2a4\ucf00\uc774\ub4dc \uc6b4\uc804",
+ "kombibetrieb": "\ucf64\ube44 \ubaa8\ub4dc",
+ "kombigerat": "\ucf64\ube44 \ubcf4\uc77c\ub7ec",
+ "kombigerat_mit_solareinbindung": "\ud0dc\uc591\uc5f4 \ud1b5\ud569\ud615 \ucf64\ube44 \ubcf4\uc77c\ub7ec",
+ "mindest_kombizeit": "\ucd5c\uc18c \ucf64\ube44 \uc2dc\uac04",
+ "nachlauf_heizkreispumpe": "\ub09c\ubc29 \ud68c\ub85c \ud38c\ud504 \uc6b4\uc804",
+ "nachspulen": "\ud50c\ub7ec\uc2dc \ud6c4",
+ "nur_heizgerat": "\ubcf4\uc77c\ub7ec\ub9cc",
+ "parallelbetrieb": "\ubcd1\ub82c \ubaa8\ub4dc",
+ "partymodus": "\ud30c\ud2f0 \ubaa8\ub4dc",
+ "perm_cooling": "\uc601\uad6c \ub0c9\ubc29",
+ "permanent": "\uc601\uad6c",
+ "permanentbetrieb": "\uc601\uad6c \ubaa8\ub4dc",
+ "reduzierter_betrieb": "\uc81c\ud55c \ubaa8\ub4dc",
+ "rt_abschaltung": "RT \ub044\uae30",
+ "rt_frostschutz": "RT \ub3d9\ud30c \ubcf4\ud638",
+ "ruhekontakt": "\uc0c1\uc2dc \ud3d0\uc1c4(NC)",
+ "schornsteinfeger": "\ubc30\uae30 \ud14c\uc2a4\ud2b8",
+ "smart_grid": "\uc2a4\ub9c8\ud2b8\uadf8\ub9ac\ub4dc",
+ "smart_home": "\uc2a4\ub9c8\ud2b8\ud648",
+ "softstart": "\uc18c\ud504\ud2b8 \uc2a4\ud0c0\ud2b8",
+ "solarbetrieb": "\ud0dc\uc591\uc5f4 \ubaa8\ub4dc",
+ "sparbetrieb": "\uc808\uc57d \ubaa8\ub4dc",
+ "sparen": "\uc808\uc57d",
+ "spreizung_hoch": "\ub108\ubb34 \ub113\uc740 dT",
+ "spreizung_kf": "KF \ud655\uc0b0",
+ "stabilisierung": "\uc548\uc815\ud654",
+ "standby": "\uc900\ube44\uc911",
+ "start": "\uc2dc\uc791",
+ "storung": "\uc624\ub958",
+ "taktsperre": "\uc21c\ud658 \ubc29\uc9c0",
+ "telefonfernschalter": "\uc804\ud654 \uc6d0\uaca9 \uc2a4\uc704\uce58",
+ "test": "\ud14c\uc2a4\ud2b8",
+ "tpw": "TPW",
+ "urlaubsmodus": "\ud734\uc77c \ubaa8\ub4dc",
+ "ventilprufung": "\ubc38\ube0c \ud14c\uc2a4\ud2b8",
+ "vorspulen": "\uc785\uc218\uad6c \uccad\uc18c",
+ "warmwasser": "DHW",
+ "warmwasser_schnellstart": "DHW \ube60\ub978 \uc2dc\uc791",
+ "warmwasserbetrieb": "DHW \ubaa8\ub4dc",
+ "warmwassernachlauf": "DHW \uc6b4\uc804",
+ "warmwasservorrang": "DHW \uc6b0\uc120\uc21c\uc704",
+ "zunden": "\uc810\ud654"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/translations/sensor.nl.json b/homeassistant/components/wolflink/translations/sensor.nl.json
new file mode 100644
index 00000000000000..304a4fd6f27993
--- /dev/null
+++ b/homeassistant/components/wolflink/translations/sensor.nl.json
@@ -0,0 +1,87 @@
+{
+ "state": {
+ "wolflink__state": {
+ "1_x_warmwasser": "1 x DHW",
+ "abgasklappe": "Rookgasklep",
+ "absenkbetrieb": "Setback modus",
+ "absenkstop": "Setback stop",
+ "aktiviert": "Geactiveerd",
+ "antilegionellenfunktion": "Anti-legionella functie",
+ "at_abschaltung": "OT afsluiten",
+ "at_frostschutz": "OT vorstbescherming",
+ "aus": "Uitgeschakeld",
+ "auto": "Auto",
+ "auto_off_cool": "AutoOffCool",
+ "auto_on_cool": "AutoOnCool",
+ "automatik_aus": "Automatisch UIT",
+ "automatik_ein": "Automatisch AAN",
+ "bereit_keine_ladung": "Klaar, niet laden",
+ "betrieb_ohne_brenner": "Werkend zonder brander",
+ "cooling": "Koelen",
+ "deaktiviert": "Inactief",
+ "dhw_prior": "DHWPrior",
+ "eco": "Eco",
+ "ein": "Ingeschakeld",
+ "estrichtrocknung": "Dekvloer drogen",
+ "externe_deaktivierung": "Externe uitschakeling",
+ "fernschalter_ein": "Op afstand bedienen ingeschakeld",
+ "frost_heizkreis": "Verwarmingscircuit ontdooien",
+ "frost_warmwasser": "DHW vorst",
+ "frostschutz": "Vorstbescherming",
+ "gasdruck": "Gasdruk",
+ "glt_betrieb": "BMS-modus",
+ "gradienten_uberwachung": "Gradient monitoring",
+ "heizbetrieb": "Verwarmingsmodus",
+ "heizgerat_mit_speicher": "Boiler met cilinder",
+ "heizung": "Verwarmen",
+ "initialisierung": "Initialisatie",
+ "kalibration": "Kalibratie",
+ "kalibration_heizbetrieb": "Kalibratie verwarmingsmodus",
+ "kalibration_kombibetrieb": "Kalibratie van de combimodus",
+ "kalibration_warmwasserbetrieb": "DHW-kalibratie",
+ "kaskadenbetrieb": "Cascade operation",
+ "kombibetrieb": "Combi-modus",
+ "kombigerat": "Combiketel",
+ "kombigerat_mit_solareinbindung": "Combiketel met zonne-integratie",
+ "mindest_kombizeit": "Minimale combitijd",
+ "nachlauf_heizkreispumpe": "De pomp van het verwarmingscircuit gaat aan",
+ "nachspulen": "Post-flush",
+ "nur_heizgerat": "Alleen ketel",
+ "parallelbetrieb": "Parallelle modus",
+ "partymodus": "Feestmodus",
+ "perm_cooling": "PermCooling",
+ "permanent": "Permanent",
+ "permanentbetrieb": "Permanente modus",
+ "reduzierter_betrieb": "Beperkte modus",
+ "rt_abschaltung": "RT afsluiten",
+ "rt_frostschutz": "RT vorstbescherming",
+ "ruhekontakt": "Rest contact",
+ "schornsteinfeger": "Emissietest",
+ "smart_grid": "SmartGrid",
+ "smart_home": "SmartHome",
+ "softstart": "Zachte start",
+ "solarbetrieb": "Zonnemodus",
+ "sparbetrieb": "Spaarstand",
+ "sparen": "Spaarstand",
+ "spreizung_hoch": "dT te breed",
+ "spreizung_kf": "Spreid KF",
+ "stabilisierung": "Stablisatie",
+ "standby": "Stand-by",
+ "start": "Start",
+ "storung": "Fout",
+ "taktsperre": "Anti-cyclus",
+ "telefonfernschalter": "Telefoon schakelaar op afstand",
+ "test": "Test",
+ "tpw": "TPW",
+ "urlaubsmodus": "Vakantiemodus",
+ "ventilprufung": "Kleptest",
+ "vorspulen": "Invoer spoelen",
+ "warmwasser": "DHW",
+ "warmwasser_schnellstart": "DHW Snel starten",
+ "warmwasserbetrieb": "DHW-modus",
+ "warmwassernachlauf": "DHW aanloop",
+ "warmwasservorrang": "DHW prioriteit",
+ "zunden": "Ontsteking"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/translations/sensor.tr.json b/homeassistant/components/wolflink/translations/sensor.tr.json
index 8b2eb0a8c53052..4b1e2778af13a6 100644
--- a/homeassistant/components/wolflink/translations/sensor.tr.json
+++ b/homeassistant/components/wolflink/translations/sensor.tr.json
@@ -1,10 +1,19 @@
{
"state": {
"wolflink__state": {
+ "glt_betrieb": "BMS modu",
+ "heizbetrieb": "Is\u0131tma modu",
+ "kalibration_heizbetrieb": "Is\u0131tma modu kalibrasyonu",
+ "kalibration_kombibetrieb": "Kombi modu kalibrasyonu",
+ "reduzierter_betrieb": "S\u0131n\u0131rl\u0131 mod",
+ "solarbetrieb": "G\u00fcne\u015f modu",
+ "sparbetrieb": "Ekonomi modu",
"standby": "Bekleme",
"start": "Ba\u015flat",
"storung": "Hata",
- "test": "Test"
+ "test": "Test",
+ "urlaubsmodus": "Tatil modu",
+ "warmwasserbetrieb": "DHW modu"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/translations/sensor.uk.json b/homeassistant/components/wolflink/translations/sensor.uk.json
index 665ff99992c16a..c8a69f2c007c78 100644
--- a/homeassistant/components/wolflink/translations/sensor.uk.json
+++ b/homeassistant/components/wolflink/translations/sensor.uk.json
@@ -1,15 +1,87 @@
{
"state": {
"wolflink__state": {
+ "1_x_warmwasser": "1 \u0445 \u0413\u0412\u041f",
+ "abgasklappe": "\u0417\u0430\u0441\u043b\u0456\u043d\u043a\u0430 \u0434\u0438\u043c\u043e\u0432\u0438\u0445 \u0433\u0430\u0437\u0456\u0432",
+ "absenkbetrieb": "\u0420\u0435\u0436\u0438\u043c \u0430\u0432\u0430\u0440\u0456\u0457",
+ "absenkstop": "\u0410\u0432\u0430\u0440\u0456\u0439\u043d\u0430 \u0437\u0443\u043f\u0438\u043d\u043a\u0430",
+ "aktiviert": "\u0410\u043a\u0442\u0438\u0432\u043e\u0432\u0430\u043d\u043e",
+ "antilegionellenfunktion": "\u0424\u0443\u043d\u043a\u0446\u0456\u044f \u0430\u043d\u0442\u0438-\u043b\u0435\u0433\u0438\u043e\u043d\u0435\u043b\u043b\u0438",
+ "at_abschaltung": "\u041e\u0422 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f",
+ "at_frostschutz": "\u041e\u0422 \u0437\u0430\u0445\u0438\u0441\u0442 \u0432\u0456\u0434 \u0437\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u043d\u044f",
+ "aus": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e",
+ "auto": "\u0410\u0432\u0442\u043e",
+ "auto_off_cool": "AutoOffCool",
+ "auto_on_cool": "AutoOnCool",
+ "automatik_aus": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f",
+ "automatik_ein": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u0432\u043c\u0438\u043a\u0430\u043d\u043d\u044f",
+ "bereit_keine_ladung": "\u0413\u043e\u0442\u043e\u0432\u0438\u0439, \u043d\u0435 \u0437\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0443\u0454\u0442\u044c\u0441\u044f",
+ "betrieb_ohne_brenner": "\u0420\u043e\u0431\u043e\u0442\u0430 \u0431\u0435\u0437 \u043f\u0430\u043b\u044c\u043d\u0438\u043a\u0430",
+ "cooling": "\u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f",
+ "deaktiviert": "\u041d\u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u043e",
+ "dhw_prior": "DHWPrior",
+ "eco": "\u0415\u043a\u043e",
+ "ein": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e",
+ "estrichtrocknung": "\u0421\u0443\u0448\u0456\u043d\u043d\u044f",
+ "externe_deaktivierung": "\u0417\u043e\u0432\u043d\u0456\u0448\u043d\u044f \u0434\u0435\u0430\u043a\u0442\u0438\u0432\u0430\u0446\u0456\u044f",
+ "fernschalter_ein": "\u0414\u0438\u0441\u0442\u0430\u043d\u0446\u0456\u0439\u043d\u0435 \u043a\u0435\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e",
+ "frost_heizkreis": "\u0417\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u043d\u044f \u043a\u043e\u043d\u0442\u0443\u0440\u0443 \u043e\u043f\u0430\u043b\u0435\u043d\u043d\u044f",
+ "frost_warmwasser": "\u0417\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u043d\u044f \u0413\u0412\u041f",
+ "frostschutz": "\u0417\u0430\u0445\u0438\u0441\u0442 \u0432\u0456\u0434 \u0437\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u043d\u044f",
+ "gasdruck": "\u0422\u0438\u0441\u043a \u0433\u0430\u0437\u0443",
+ "glt_betrieb": "\u0420\u0435\u0436\u0438\u043c BMS",
+ "gradienten_uberwachung": "\u0413\u0440\u0430\u0434\u0456\u0454\u043d\u0442\u043d\u0438\u0439 \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433",
+ "heizbetrieb": "\u0420\u0435\u0436\u0438\u043c \u043e\u043f\u0430\u043b\u0435\u043d\u043d\u044f",
+ "heizgerat_mit_speicher": "\u041a\u043e\u0442\u0435\u043b \u0437 \u0446\u0438\u043b\u0456\u043d\u0434\u0440\u043e\u043c",
+ "heizung": "\u041e\u0431\u0456\u0433\u0440\u0456\u0432",
+ "initialisierung": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f",
+ "kalibration": "\u041a\u0430\u043b\u0456\u0431\u0440\u0443\u0432\u0430\u043d\u043d\u044f",
+ "kalibration_heizbetrieb": "\u041a\u0430\u043b\u0456\u0431\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0440\u0435\u0436\u0438\u043c\u0443 \u043e\u043f\u0430\u043b\u0435\u043d\u043d\u044f",
+ "kalibration_kombibetrieb": "\u041a\u0430\u043b\u0456\u0431\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0432 \u043a\u043e\u043c\u0431\u0456\u043d\u043e\u0432\u0430\u043d\u043e\u043c\u0443 \u0440\u0435\u0436\u0438\u043c\u0456",
+ "kalibration_warmwasserbetrieb": "\u041a\u0430\u043b\u0456\u0431\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0413\u0412\u041f",
+ "kaskadenbetrieb": "\u041a\u0430\u0441\u043a\u0430\u0434\u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u044f",
+ "kombibetrieb": "\u041a\u043e\u043c\u0431\u0456\u043d\u043e\u0432\u0430\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c",
+ "kombigerat": "\u0414\u0432\u043e\u043a\u043e\u043d\u0442\u0443\u0440\u043d\u0438\u0439 \u043a\u043e\u0442\u0435\u043b",
+ "kombigerat_mit_solareinbindung": "\u0414\u0432\u043e\u043a\u043e\u043d\u0442\u0443\u0440\u043d\u0438\u0439 \u043a\u043e\u0442\u0435\u043b \u0437 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0454\u044e \u0441\u043e\u043d\u044f\u0447\u043d\u043e\u0457 \u0441\u0438\u0441\u0442\u0435\u043c\u0438",
+ "mindest_kombizeit": "\u041c\u0456\u043d\u0456\u043c\u0430\u043b\u044c\u043d\u0438\u0439 \u043a\u043e\u043c\u0431\u0456\u043d\u043e\u0432\u0430\u043d\u0438\u0439 \u0447\u0430\u0441",
+ "nachlauf_heizkreispumpe": "\u0420\u043e\u0431\u043e\u0442\u0430 \u043d\u0430\u0441\u043e\u0441\u0430 \u043a\u043e\u043d\u0442\u0443\u0440\u0443 \u043e\u043f\u0430\u043b\u0435\u043d\u043d\u044f",
+ "nachspulen": "\u041f\u043e\u0441\u0442-\u043f\u0440\u043e\u043c\u0438\u0432\u043a\u0430",
+ "nur_heizgerat": "\u0422\u0456\u043b\u044c\u043a\u0438 \u0431\u043e\u0439\u043b\u0435\u0440",
+ "parallelbetrieb": "\u041f\u0430\u0440\u0430\u043b\u0435\u043b\u044c\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c",
+ "partymodus": "\u0420\u0435\u0436\u0438\u043c \u0432\u0435\u0447\u0456\u0440\u043a\u0438",
+ "perm_cooling": "\u041f\u043e\u0441\u0442\u0456\u0439\u043d\u0435 \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f",
"permanent": "\u041f\u043e\u0441\u0442\u0456\u0439\u043d\u043e",
+ "permanentbetrieb": "\u041f\u043e\u0441\u0442\u0456\u0439\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c",
+ "reduzierter_betrieb": "\u041e\u0431\u043c\u0435\u0436\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c",
+ "rt_abschaltung": "RT \u0432\u0438\u043c\u0438\u043a\u0430\u043d\u043d\u044f",
+ "rt_frostschutz": "RT \u0437\u0430\u0445\u0438\u0441\u0442 \u0432\u0456\u0434 \u0437\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u043d\u044f",
+ "ruhekontakt": "\u0420\u0435\u0448\u0442\u0430 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u0456\u0432",
+ "schornsteinfeger": "\u0422\u0435\u0441\u0442 \u043d\u0430 \u0432\u0438\u043a\u0438\u0434\u0438",
+ "smart_grid": "\u0420\u043e\u0437\u0443\u043c\u043d\u0430 \u043c\u0435\u0440\u0435\u0436\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043f\u043e\u0441\u0442\u0430\u0447\u0430\u043d\u043d\u044f",
"smart_home": "\u0420\u043e\u0437\u0443\u043c\u043d\u0438\u0439 \u0434\u0456\u043c",
+ "softstart": "\u041c'\u044f\u043a\u0438\u0439 \u0441\u0442\u0430\u0440\u0442",
+ "solarbetrieb": "\u0421\u043e\u043d\u044f\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c",
+ "sparbetrieb": "\u0420\u0435\u0436\u0438\u043c \u0435\u043a\u043e\u043d\u043e\u043c\u0456\u0457",
"sparen": "\u0415\u043a\u043e\u043d\u043e\u043c\u0456\u044f",
+ "spreizung_hoch": "dT \u0437\u0430\u043d\u0430\u0434\u0442\u043e \u0448\u0438\u0440\u043e\u043a\u0438\u0439",
+ "spreizung_kf": "\u0421\u043f\u0440\u0435\u0434 KF",
"stabilisierung": "\u0421\u0442\u0430\u0431\u0456\u043b\u0456\u0437\u0430\u0446\u0456\u044f",
"standby": "\u041e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f",
- "start": "\u041f\u043e\u0447\u0430\u0442\u043e\u043a",
+ "start": "\u0417\u0430\u043f\u0443\u0441\u043a",
"storung": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430",
- "taktsperre": "\u0410\u043d\u0442\u0438\u0446\u0438\u043a\u043b",
- "test": "\u0422\u0435\u0441\u0442"
+ "taktsperre": "\u0410\u043d\u0442\u0438-\u0446\u0438\u043a\u043b",
+ "telefonfernschalter": "\u0414\u0438\u0441\u0442\u0430\u043d\u0446\u0456\u0439\u043d\u0435 \u043a\u0435\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0437 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0443",
+ "test": "\u0422\u0435\u0441\u0442",
+ "tpw": "TPW",
+ "urlaubsmodus": "\u0420\u0435\u0436\u0438\u043c \"\u0432\u0438\u0445\u0456\u0434\u043d\u0456\"",
+ "ventilprufung": "\u0422\u0435\u0441\u0442 \u043a\u043b\u0430\u043f\u0430\u043d\u0430",
+ "vorspulen": "\u041f\u0440\u043e\u043c\u0438\u0432\u0430\u043d\u043d\u044f \u0432\u0445\u043e\u0434\u0443",
+ "warmwasser": "\u0413\u0412\u041f",
+ "warmwasser_schnellstart": "\u0428\u0432\u0438\u0434\u043a\u0438\u0439 \u0437\u0430\u043f\u0443\u0441\u043a \u0413\u0412\u041f",
+ "warmwasserbetrieb": "\u0420\u0435\u0436\u0438\u043c \u0413\u0412\u041f",
+ "warmwassernachlauf": "\u0417\u0430\u043f\u0443\u0441\u043a \u0413\u0412\u041f",
+ "warmwasservorrang": "\u041f\u0440\u0456\u043e\u0440\u0438\u0442\u0435\u0442 \u0413\u0412\u041f",
+ "zunden": "\u0417\u0430\u043f\u0430\u043b\u044e\u0432\u0430\u043d\u043d\u044f"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/translations/tr.json b/homeassistant/components/wolflink/translations/tr.json
new file mode 100644
index 00000000000000..6ed28a58c793c2
--- /dev/null
+++ b/homeassistant/components/wolflink/translations/tr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
+ "unknown": "Beklenmeyen hata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/translations/uk.json b/homeassistant/components/wolflink/translations/uk.json
index a7fbdfff913754..3fdf20a6aceb6e 100644
--- a/homeassistant/components/wolflink/translations/uk.json
+++ b/homeassistant/components/wolflink/translations/uk.json
@@ -1,17 +1,26 @@
{
"config": {
"abort": {
- "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e"
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
},
"error": {
"cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
"unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
},
"step": {
"device": {
"data": {
"device_name": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439"
- }
+ },
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 WOLF"
+ },
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
+ },
+ "title": "WOLF SmartSet"
}
}
}
diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py
index 44f30cf95381e1..ed3822b9698601 100644
--- a/homeassistant/components/workday/binary_sensor.py
+++ b/homeassistant/components/workday/binary_sensor.py
@@ -1,5 +1,5 @@
"""Sensor to indicate whether the current day is a workday."""
-from datetime import datetime, timedelta
+from datetime import timedelta
import logging
from typing import Any
@@ -9,6 +9,7 @@
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
from homeassistant.const import CONF_NAME, WEEKDAYS
import homeassistant.helpers.config_validation as cv
+import homeassistant.util.dt as dt
_LOGGER = logging.getLogger(__name__)
@@ -77,7 +78,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
sensor_name = config[CONF_NAME]
workdays = config[CONF_WORKDAYS]
- year = (get_date(datetime.today()) + timedelta(days=days_offset)).year
+ year = (get_date(dt.now()) + timedelta(days=days_offset)).year
obj_holidays = getattr(holidays, country)(years=year)
if province:
@@ -170,7 +171,7 @@ def is_exclude(self, day, now):
return False
@property
- def state_attributes(self):
+ def extra_state_attributes(self):
"""Return the attributes of the entity."""
# return self._attributes
return {
@@ -185,7 +186,7 @@ async def async_update(self):
self._state = False
# Get ISO day of the week (1 = Monday, 7 = Sunday)
- date = get_date(datetime.today()) + timedelta(days=self._days_offset)
+ date = get_date(dt.now()) + timedelta(days=self._days_offset)
day = date.isoweekday() - 1
day_of_week = day_to_string(day)
diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json
index 4fb25c766ccc69..b87704cde679cc 100644
--- a/homeassistant/components/workday/manifest.json
+++ b/homeassistant/components/workday/manifest.json
@@ -2,7 +2,7 @@
"domain": "workday",
"name": "Workday",
"documentation": "https://www.home-assistant.io/integrations/workday",
- "requirements": ["holidays==0.10.4"],
+ "requirements": ["holidays==0.11.1"],
"codeowners": ["@fabaff"],
"quality_scale": "internal"
}
diff --git a/homeassistant/components/worldclock/sensor.py b/homeassistant/components/worldclock/sensor.py
index e02dc3a0d5c7ab..de5b3991e3f085 100644
--- a/homeassistant/components/worldclock/sensor.py
+++ b/homeassistant/components/worldclock/sensor.py
@@ -1,10 +1,9 @@
"""Support for showing the time in a different time zone."""
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, CONF_TIME_ZONE
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
CONF_TIME_FORMAT = "time_format"
@@ -39,7 +38,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
-class WorldClockSensor(Entity):
+class WorldClockSensor(SensorEntity):
"""Representation of a World clock sensor."""
def __init__(self, time_zone, name, time_format):
diff --git a/homeassistant/components/worldtidesinfo/sensor.py b/homeassistant/components/worldtidesinfo/sensor.py
index aaa9f2d1585919..0fa65957e4001c 100644
--- a/homeassistant/components/worldtidesinfo/sensor.py
+++ b/homeassistant/components/worldtidesinfo/sensor.py
@@ -6,7 +6,7 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_API_KEY,
@@ -15,7 +15,6 @@
CONF_NAME,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -55,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([tides])
-class WorldTidesInfoSensor(Entity):
+class WorldTidesInfoSensor(SensorEntity):
"""Representation of a WorldTidesInfo sensor."""
def __init__(self, name, lat, lon, key):
@@ -72,7 +71,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of this device."""
attr = {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py
index e4fe33f62f128a..e7600670c52280 100644
--- a/homeassistant/components/worxlandroid/sensor.py
+++ b/homeassistant/components/worxlandroid/sensor.py
@@ -6,11 +6,10 @@
import async_timeout
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_HOST, CONF_PIN, CONF_TIMEOUT, PERCENTAGE
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -50,7 +49,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([WorxLandroidSensor(typ, config)])
-class WorxLandroidSensor(Entity):
+class WorxLandroidSensor(SensorEntity):
"""Implementation of a Worx Landroid sensor."""
def __init__(self, sensor, config):
@@ -128,9 +127,8 @@ async def async_update(self):
def get_error(obj):
"""Get the mower error."""
for i, err in enumerate(obj["allarmi"]):
- if i != 2: # ignore wire bounce errors
- if err == 1:
- return ERROR_STATE[i]
+ if i != 2 and err == 1: # ignore wire bounce errors
+ return ERROR_STATE[i]
return None
diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py
index 786fd07f6265c3..9e4d957d028850 100644
--- a/homeassistant/components/wsdot/sensor.py
+++ b/homeassistant/components/wsdot/sensor.py
@@ -6,7 +6,7 @@
import requests
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_NAME,
@@ -17,7 +17,6 @@
TIME_MINUTES,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -65,7 +64,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class WashingtonStateTransportSensor(Entity):
+class WashingtonStateTransportSensor(SensorEntity):
"""
Sensor that reads the WSDOT web API.
@@ -120,7 +119,7 @@ def update(self):
self._state = self._data.get(ATTR_CURRENT_TIME)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return other details about the sensor state."""
if self._data is not None:
attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py
index e1bd79b7ea0efe..358e305dc47742 100644
--- a/homeassistant/components/wunderground/sensor.py
+++ b/homeassistant/components/wunderground/sensor.py
@@ -1,16 +1,18 @@
"""Support for WUnderground weather service."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
import logging
import re
-from typing import Any, Callable, Optional, Union
+from typing import Any, Callable
import aiohttp
import async_timeout
import voluptuous as vol
from homeassistant.components import sensor
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_API_KEY,
@@ -34,7 +36,6 @@
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.util import Throttle
@@ -64,13 +65,13 @@ class WUSensorConfig:
def __init__(
self,
- friendly_name: Union[str, Callable],
+ friendly_name: str | Callable,
feature: str,
- value: Callable[["WUndergroundData"], Any],
- unit_of_measurement: Optional[str] = None,
+ value: Callable[[WUndergroundData], Any],
+ unit_of_measurement: str | None = None,
entity_picture=None,
icon: str = "mdi:gauge",
- device_state_attributes=None,
+ extra_state_attributes=None,
device_class=None,
):
"""Initialize sensor configuration.
@@ -82,7 +83,7 @@ def __init__(
:param unit_of_measurement: unit of measurement
:param entity_picture: value or callback returning URL of entity picture
:param icon: icon name or URL
- :param device_state_attributes: dictionary of attributes, or callable that returns it
+ :param extra_state_attributes: dictionary of attributes, or callable that returns it
"""
self.friendly_name = friendly_name
self.unit_of_measurement = unit_of_measurement
@@ -90,7 +91,7 @@ def __init__(
self.value = value
self.entity_picture = entity_picture
self.icon = icon
- self.device_state_attributes = device_state_attributes or {}
+ self.extra_state_attributes = extra_state_attributes or {}
self.device_class = device_class
@@ -99,10 +100,10 @@ class WUCurrentConditionsSensorConfig(WUSensorConfig):
def __init__(
self,
- friendly_name: Union[str, Callable],
+ friendly_name: str | Callable,
field: str,
- icon: Optional[str] = "mdi:gauge",
- unit_of_measurement: Optional[str] = None,
+ icon: str | None = "mdi:gauge",
+ unit_of_measurement: str | None = None,
device_class=None,
):
"""Initialize current conditions sensor configuration.
@@ -121,7 +122,7 @@ def __init__(
entity_picture=lambda wu: wu.data["current_observation"]["icon_url"]
if icon is None
else None,
- device_state_attributes={
+ extra_state_attributes={
"date": lambda wu: wu.data["current_observation"]["observation_time"]
},
device_class=device_class,
@@ -131,9 +132,7 @@ def __init__(
class WUDailyTextForecastSensorConfig(WUSensorConfig):
"""Helper for defining sensor configurations for daily text forecasts."""
- def __init__(
- self, period: int, field: str, unit_of_measurement: Optional[str] = None
- ):
+ def __init__(self, period: int, field: str, unit_of_measurement: str | None = None):
"""Initialize daily text forecast sensor configuration.
:param period: forecast period number
@@ -152,7 +151,7 @@ def __init__(
"forecastday"
][period]["icon_url"],
unit_of_measurement=unit_of_measurement,
- device_state_attributes={
+ extra_state_attributes={
"date": lambda wu: wu.data["forecast"]["txt_forecast"]["date"]
},
)
@@ -166,8 +165,8 @@ def __init__(
friendly_name: str,
period: int,
field: str,
- wu_unit: Optional[str] = None,
- ha_unit: Optional[str] = None,
+ wu_unit: str | None = None,
+ ha_unit: str | None = None,
icon=None,
device_class=None,
):
@@ -201,7 +200,7 @@ def __init__(
if not icon
else None,
icon=icon,
- device_state_attributes={
+ extra_state_attributes={
"date": lambda wu: wu.data["forecast"]["simpleforecast"]["forecastday"][
period
]["date"]["pretty"]
@@ -227,7 +226,7 @@ def __init__(self, period: int, field: int):
feature="hourly",
value=lambda wu: wu.data["hourly_forecast"][period][field],
entity_picture=lambda wu: wu.data["hourly_forecast"][period]["icon_url"],
- device_state_attributes={
+ extra_state_attributes={
"temp_c": lambda wu: wu.data["hourly_forecast"][period]["temp"][
"metric"
],
@@ -273,7 +272,7 @@ class WUAlmanacSensorConfig(WUSensorConfig):
def __init__(
self,
- friendly_name: Union[str, Callable],
+ friendly_name: str | Callable,
field: str,
value_type: str,
wu_unit: str,
@@ -303,7 +302,7 @@ def __init__(
class WUAlertsSensorConfig(WUSensorConfig):
"""Helper for defining field configuration for alerts."""
- def __init__(self, friendly_name: Union[str, Callable]):
+ def __init__(self, friendly_name: str | Callable):
"""Initialiize alerts sensor configuration.
:param friendly_name: Friendly name
@@ -315,7 +314,7 @@ def __init__(self, friendly_name: Union[str, Callable]):
icon=lambda wu: "mdi:alert-circle-outline"
if wu.data["alerts"]
else "mdi:check-circle-outline",
- device_state_attributes=self._get_attributes,
+ extra_state_attributes=self._get_attributes,
)
@staticmethod
@@ -1117,7 +1116,7 @@ async def async_setup_platform(
async_add_entities(sensors, True)
-class WUndergroundSensor(Entity):
+class WUndergroundSensor(SensorEntity):
"""Implementing the WUnderground sensor."""
def __init__(self, hass: HomeAssistantType, rest, condition, unique_id_base: str):
@@ -1157,7 +1156,7 @@ def _cfg_expand(self, what, default=None):
def _update_attrs(self):
"""Parse and update device state attributes."""
- attrs = self._cfg_expand("device_state_attributes", {})
+ attrs = self._cfg_expand("extra_state_attributes", {})
for (attr, callback) in attrs.items():
if callable(callback):
@@ -1185,7 +1184,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attributes
diff --git a/homeassistant/components/xbee/__init__.py b/homeassistant/components/xbee/__init__.py
index e6175a4dccfe93..58ce7587070e21 100644
--- a/homeassistant/components/xbee/__init__.py
+++ b/homeassistant/components/xbee/__init__.py
@@ -9,6 +9,7 @@
from xbee_helper.device import convert_adc
from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
CONF_ADDRESS,
CONF_DEVICE,
@@ -21,9 +22,9 @@
from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send
from homeassistant.helpers.entity import Entity
-_LOGGER = logging.getLogger(__name__)
+from .const import DOMAIN
-DOMAIN = "xbee"
+_LOGGER = logging.getLogger(__name__)
SIGNAL_XBEE_FRAME_RECEIVED = "xbee_frame_received"
@@ -59,7 +60,6 @@
def setup(hass, config):
"""Set up the connection to the XBee Zigbee device."""
-
usb_device = config[DOMAIN].get(CONF_DEVICE, DEFAULT_DEVICE)
baud = int(config[DOMAIN].get(CONF_BAUD, DEFAULT_BAUD))
try:
@@ -366,7 +366,7 @@ def update(self):
self._state = self._config.state2bool[pin_state]
-class XBeeAnalogIn(Entity):
+class XBeeAnalogIn(SensorEntity):
"""Representation of a GPIO pin configured as an analog input."""
def __init__(self, config, device):
diff --git a/homeassistant/components/xbee/binary_sensor.py b/homeassistant/components/xbee/binary_sensor.py
index 47c7515ddc7770..01095822d1f57c 100644
--- a/homeassistant/components/xbee/binary_sensor.py
+++ b/homeassistant/components/xbee/binary_sensor.py
@@ -3,12 +3,8 @@
from homeassistant.components.binary_sensor import BinarySensorEntity
-from . import DOMAIN, PLATFORM_SCHEMA, XBeeDigitalIn, XBeeDigitalInConfig
-
-CONF_ON_STATE = "on_state"
-
-DEFAULT_ON_STATE = "high"
-STATES = ["high", "low"]
+from . import PLATFORM_SCHEMA, XBeeDigitalIn, XBeeDigitalInConfig
+from .const import CONF_ON_STATE, DOMAIN, STATES
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_ON_STATE): vol.In(STATES)})
diff --git a/homeassistant/components/xbee/const.py b/homeassistant/components/xbee/const.py
new file mode 100644
index 00000000000000..a77e71e92f5b62
--- /dev/null
+++ b/homeassistant/components/xbee/const.py
@@ -0,0 +1,5 @@
+"""Constants for the xbee integration."""
+CONF_ON_STATE = "on_state"
+DEFAULT_ON_STATE = "high"
+DOMAIN = "xbee"
+STATES = ["high", "low"]
diff --git a/homeassistant/components/xbee/light.py b/homeassistant/components/xbee/light.py
index 76ed8120166aee..859feee495bf52 100644
--- a/homeassistant/components/xbee/light.py
+++ b/homeassistant/components/xbee/light.py
@@ -3,12 +3,8 @@
from homeassistant.components.light import LightEntity
-from . import DOMAIN, PLATFORM_SCHEMA, XBeeDigitalOut, XBeeDigitalOutConfig
-
-CONF_ON_STATE = "on_state"
-
-DEFAULT_ON_STATE = "high"
-STATES = ["high", "low"]
+from . import PLATFORM_SCHEMA, XBeeDigitalOut, XBeeDigitalOutConfig
+from .const import CONF_ON_STATE, DEFAULT_ON_STATE, DOMAIN, STATES
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Optional(CONF_ON_STATE, default=DEFAULT_ON_STATE): vol.In(STATES)}
diff --git a/homeassistant/components/xbee/sensor.py b/homeassistant/components/xbee/sensor.py
index 4a392691032020..78cfe964277caf 100644
--- a/homeassistant/components/xbee/sensor.py
+++ b/homeassistant/components/xbee/sensor.py
@@ -5,14 +5,13 @@
import voluptuous as vol
from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure
-from homeassistant.const import TEMP_CELSIUS
-from homeassistant.helpers.entity import Entity
+from homeassistant.components.sensor import SensorEntity
+from homeassistant.const import CONF_TYPE, TEMP_CELSIUS
from . import DOMAIN, PLATFORM_SCHEMA, XBeeAnalogIn, XBeeAnalogInConfig, XBeeConfig
_LOGGER = logging.getLogger(__name__)
-CONF_TYPE = "type"
CONF_MAX_VOLTS = "max_volts"
DEFAULT_VOLTS = 1.2
@@ -44,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([sensor_class(config_class(config), zigbee_device)], True)
-class XBeeTemperatureSensor(Entity):
+class XBeeTemperatureSensor(SensorEntity):
"""Representation of XBee Pro temperature sensor."""
def __init__(self, config, device):
diff --git a/homeassistant/components/xbee/switch.py b/homeassistant/components/xbee/switch.py
index cdb0d2677c541c..b97d9f315d572b 100644
--- a/homeassistant/components/xbee/switch.py
+++ b/homeassistant/components/xbee/switch.py
@@ -3,13 +3,8 @@
from homeassistant.components.switch import SwitchEntity
-from . import DOMAIN, PLATFORM_SCHEMA, XBeeDigitalOut, XBeeDigitalOutConfig
-
-CONF_ON_STATE = "on_state"
-
-DEFAULT_ON_STATE = "high"
-
-STATES = ["high", "low"]
+from . import PLATFORM_SCHEMA, XBeeDigitalOut, XBeeDigitalOutConfig
+from .const import CONF_ON_STATE, DOMAIN, STATES
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_ON_STATE): vol.In(STATES)})
diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py
index 3e8d537799ab88..d287e515cefd41 100644
--- a/homeassistant/components/xbox/__init__.py
+++ b/homeassistant/components/xbox/__init__.py
@@ -1,9 +1,11 @@
"""The xbox integration."""
+from __future__ import annotations
+
import asyncio
+from contextlib import suppress
from dataclasses import dataclass
from datetime import timedelta
import logging
-from typing import Dict, Optional
import voluptuous as vol
from xbox.webapi.api.client import XboxLiveClient
@@ -93,7 +95,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
)
coordinator = XboxUpdateCoordinator(hass, client, consoles)
- await coordinator.async_refresh()
+ await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = {
"client": XboxLiveClient(auth),
@@ -101,9 +103,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"coordinator": coordinator,
}
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -114,8 +116,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -133,7 +135,7 @@ class ConsoleData:
"""Xbox console status data."""
status: SmartglassConsoleStatus
- app_details: Optional[Product]
+ app_details: Product | None
@dataclass
@@ -149,7 +151,7 @@ class PresenceData:
in_game: bool
in_multiplayer: bool
gamer_score: str
- gold_tenure: Optional[str]
+ gold_tenure: str | None
account_tier: str
@@ -157,8 +159,8 @@ class PresenceData:
class XboxData:
"""Xbox dataclass for update coordinator."""
- consoles: Dict[str, ConsoleData]
- presence: Dict[str, PresenceData]
+ consoles: dict[str, ConsoleData]
+ presence: dict[str, PresenceData]
class XboxUpdateCoordinator(DataUpdateCoordinator):
@@ -184,9 +186,9 @@ def __init__(
async def _async_update_data(self) -> XboxData:
"""Fetch the latest console status."""
# Update Console Status
- new_console_data: Dict[str, ConsoleData] = {}
+ new_console_data: dict[str, ConsoleData] = {}
for console in self.consoles.result:
- current_state: Optional[ConsoleData] = self.data.consoles.get(console.id)
+ current_state: ConsoleData | None = self.data.consoles.get(console.id)
status: SmartglassConsoleStatus = (
await self.client.smartglass.get_console_status(console.id)
)
@@ -198,7 +200,7 @@ async def _async_update_data(self) -> XboxData:
)
# Setup focus app
- app_details: Optional[Product] = None
+ app_details: Product | None = None
if current_state is not None:
app_details = current_state.app_details
@@ -246,13 +248,11 @@ async def _async_update_data(self) -> XboxData:
def _build_presence_data(person: Person) -> PresenceData:
"""Build presence data from a person."""
- active_app: Optional[PresenceDetail] = None
- try:
+ active_app: PresenceDetail | None = None
+ with suppress(StopIteration):
active_app = next(
presence for presence in person.presence_details if presence.is_primary
)
- except StopIteration:
- pass
return PresenceData(
xuid=person.xuid,
diff --git a/homeassistant/components/xbox/base_sensor.py b/homeassistant/components/xbox/base_sensor.py
index 028f1d4c9ecb36..c149ce74c3238e 100644
--- a/homeassistant/components/xbox/base_sensor.py
+++ b/homeassistant/components/xbox/base_sensor.py
@@ -1,5 +1,7 @@
"""Base Sensor for the Xbox Integration."""
-from typing import Optional
+from __future__ import annotations
+
+from yarl import URL
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -22,7 +24,7 @@ def unique_id(self) -> str:
return f"{self.xuid}_{self.attribute}"
@property
- def data(self) -> Optional[PresenceData]:
+ def data(self) -> PresenceData | None:
"""Return coordinator data for this console."""
return self.coordinator.data.presence.get(self.xuid)
@@ -44,7 +46,17 @@ def entity_picture(self) -> str:
if not self.data:
return None
- return self.data.display_pic.replace("&mode=Padding", "")
+ # Xbox sometimes returns a domain that uses a wrong certificate which creates issues
+ # with loading the image.
+ # The correct domain is images-eds-ssl which can just be replaced
+ # to point to the correct image, with the correct domain and certificate.
+ # We need to also remove the 'mode=Padding' query because with it, it results in an error 400.
+ url = URL(self.data.display_pic)
+ if url.host == "images-eds.xboxlive.com":
+ url = url.with_host("images-eds-ssl.xboxlive.com")
+ query = dict(url.query)
+ query.pop("mode", None)
+ return str(url.with_query(query))
@property
def entity_registry_enabled_default(self) -> bool:
diff --git a/homeassistant/components/xbox/binary_sensor.py b/homeassistant/components/xbox/binary_sensor.py
index 109b2839b682c7..98e06257146314 100644
--- a/homeassistant/components/xbox/binary_sensor.py
+++ b/homeassistant/components/xbox/binary_sensor.py
@@ -1,6 +1,7 @@
"""Xbox friends binary sensors."""
+from __future__ import annotations
+
from functools import partial
-from typing import Dict, List
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.core import callback
@@ -44,7 +45,7 @@ def is_on(self) -> bool:
@callback
def async_update_friends(
coordinator: XboxUpdateCoordinator,
- current: Dict[str, List[XboxBinarySensorEntity]],
+ current: dict[str, list[XboxBinarySensorEntity]],
async_add_entities,
) -> None:
"""Update friends."""
@@ -73,7 +74,7 @@ def async_update_friends(
async def async_remove_entities(
xuid: str,
coordinator: XboxUpdateCoordinator,
- current: Dict[str, XboxBinarySensorEntity],
+ current: dict[str, XboxBinarySensorEntity],
) -> None:
"""Remove friend sensors from Home Assistant."""
registry = await async_get_entity_registry(coordinator.hass)
diff --git a/homeassistant/components/xbox/browse_media.py b/homeassistant/components/xbox/browse_media.py
index a91713931c2243..0c3eec95c6fad1 100644
--- a/homeassistant/components/xbox/browse_media.py
+++ b/homeassistant/components/xbox/browse_media.py
@@ -1,5 +1,5 @@
"""Support for media browsing."""
-from typing import Dict, List, Optional
+from __future__ import annotations
from xbox.webapi.api.client import XboxLiveClient
from xbox.webapi.api.provider.catalog.const import HOME_APP_IDS, SYSTEM_PFN_ID_MAP
@@ -41,7 +41,7 @@ async def build_item_response(
tv_configured: bool,
media_content_type: str,
media_content_id: str,
-) -> Optional[BrowseMedia]:
+) -> BrowseMedia | None:
"""Create response payload for the provided media query."""
apps: InstalledPackagesList = await client.smartglass.get_installed_apps(device_id)
@@ -149,7 +149,7 @@ async def build_item_response(
)
-def item_payload(item: InstalledPackage, images: Dict[str, List[Image]]):
+def item_payload(item: InstalledPackage, images: dict[str, list[Image]]):
"""Create response payload for a single media item."""
thumbnail = None
image = _find_media_image(images.get(item.one_store_product_id, []))
@@ -169,7 +169,7 @@ def item_payload(item: InstalledPackage, images: Dict[str, List[Image]]):
)
-def _find_media_image(images=List[Image]) -> Optional[Image]:
+def _find_media_image(images: list[Image]) -> Image | None:
purpose_order = ["Poster", "Tile", "Logo", "BoxArt"]
for purpose in purpose_order:
for image in images:
diff --git a/homeassistant/components/xbox/media_player.py b/homeassistant/components/xbox/media_player.py
index 19e8a90d48e2d0..e57f3971042ce9 100644
--- a/homeassistant/components/xbox/media_player.py
+++ b/homeassistant/components/xbox/media_player.py
@@ -1,6 +1,8 @@
"""Xbox Media Player Support."""
+from __future__ import annotations
+
import re
-from typing import List, Optional
+from typing import List
from xbox.webapi.api.client import XboxLiveClient
from xbox.webapi.api.provider.catalog.models import Image
@@ -231,7 +233,7 @@ def device_info(self):
}
-def _find_media_image(images=List[Image]) -> Optional[Image]:
+def _find_media_image(images=List[Image]) -> Image | None:
purpose_order = ["FeaturePromotionalSquareArt", "Tile", "Logo", "BoxArt"]
for purpose in purpose_order:
for image in images:
diff --git a/homeassistant/components/xbox/media_source.py b/homeassistant/components/xbox/media_source.py
index 750300e49ee716..64a16e2c21d7ac 100644
--- a/homeassistant/components/xbox/media_source.py
+++ b/homeassistant/components/xbox/media_source.py
@@ -1,9 +1,10 @@
"""Xbox Media Source Implementation."""
+from __future__ import annotations
+
+from contextlib import suppress
from dataclasses import dataclass
-from typing import List, Tuple
-# pylint: disable=no-name-in-module
-from pydantic.error_wrappers import ValidationError
+from pydantic.error_wrappers import ValidationError # pylint: disable=no-name-in-module
from xbox.webapi.api.client import XboxLiveClient
from xbox.webapi.api.provider.catalog.models import FieldsTemplate, Image
from xbox.webapi.api.provider.gameclips.models import GameclipsResponse
@@ -51,7 +52,7 @@ async def async_get_media_source(hass: HomeAssistantType):
@callback
def async_parse_identifier(
item: MediaSourceItem,
-) -> Tuple[str, str, str]:
+) -> tuple[str, str, str]:
"""Parse identifier."""
identifier = item.identifier or ""
start = ["", "", ""]
@@ -88,7 +89,7 @@ async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
return PlayMedia(url, MIME_TYPE_MAP[kind])
async def async_browse_media(
- self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES
+ self, item: MediaSourceItem, media_types: tuple[str] = MEDIA_MIME_TYPES
) -> BrowseMediaSource:
"""Return media."""
title, category, _ = async_parse_identifier(item)
@@ -137,8 +138,8 @@ async def _build_media_items(self, title, category):
title_id, _, thumbnail = title.split("#", 2)
owner, kind = category.split("#", 1)
- items: List[XboxMediaItem] = []
- try:
+ items: list[XboxMediaItem] = []
+ with suppress(ValidationError): # Unexpected API response
if kind == "gameclips":
if owner == "my":
response: GameclipsResponse = (
@@ -189,9 +190,6 @@ async def _build_media_items(self, title, category):
)
for item in response.screenshots
]
- except ValidationError:
- # Unexpected API response
- pass
return BrowseMediaSource(
domain=DOMAIN,
@@ -207,7 +205,7 @@ async def _build_media_items(self, title, category):
)
-def _build_game_item(item: InstalledPackage, images: List[Image]):
+def _build_game_item(item: InstalledPackage, images: list[Image]):
"""Build individual game."""
thumbnail = ""
image = _find_media_image(images.get(item.one_store_product_id, []))
diff --git a/homeassistant/components/xbox/sensor.py b/homeassistant/components/xbox/sensor.py
index e0ab661b2d982b..ac19a4be1934c2 100644
--- a/homeassistant/components/xbox/sensor.py
+++ b/homeassistant/components/xbox/sensor.py
@@ -1,7 +1,9 @@
"""Xbox friends binary sensors."""
+from __future__ import annotations
+
from functools import partial
-from typing import Dict, List
+from homeassistant.components.sensor import SensorEntity
from homeassistant.core import callback
from homeassistant.helpers.entity_registry import (
async_get_registry as async_get_entity_registry,
@@ -28,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry, async_add_ent
update_friends()
-class XboxSensorEntity(XboxBaseSensorEntity):
+class XboxSensorEntity(XboxBaseSensorEntity, SensorEntity):
"""Representation of a Xbox presence state."""
@property
@@ -43,7 +45,7 @@ def state(self):
@callback
def async_update_friends(
coordinator: XboxUpdateCoordinator,
- current: Dict[str, List[XboxSensorEntity]],
+ current: dict[str, list[XboxSensorEntity]],
async_add_entities,
) -> None:
"""Update friends."""
@@ -72,7 +74,7 @@ def async_update_friends(
async def async_remove_entities(
xuid: str,
coordinator: XboxUpdateCoordinator,
- current: Dict[str, XboxSensorEntity],
+ current: dict[str, XboxSensorEntity],
) -> None:
"""Remove friend sensors from Home Assistant."""
registry = await async_get_entity_registry(coordinator.hass)
diff --git a/homeassistant/components/xbox/translations/de.json b/homeassistant/components/xbox/translations/de.json
index c67f3a49ea4378..04f32e05f8b34a 100644
--- a/homeassistant/components/xbox/translations/de.json
+++ b/homeassistant/components/xbox/translations/de.json
@@ -1,5 +1,10 @@
{
"config": {
+ "abort": {
+ "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.",
+ "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.",
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+ },
"create_entry": {
"default": "Erfolgreich authentifiziert"
},
diff --git a/homeassistant/components/xbox/translations/hu.json b/homeassistant/components/xbox/translations/hu.json
index 19f706be1c8180..b35b1b8e2fcde2 100644
--- a/homeassistant/components/xbox/translations/hu.json
+++ b/homeassistant/components/xbox/translations/hu.json
@@ -1,7 +1,17 @@
{
"config": {
+ "abort": {
+ "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.",
+ "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
"create_entry": {
- "default": "Sikeres autentik\u00e1ci\u00f3"
+ "default": "Sikeres hiteles\u00edt\u00e9s"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/xbox/translations/id.json b/homeassistant/components/xbox/translations/id.json
new file mode 100644
index 00000000000000..ed8106b014430a
--- /dev/null
+++ b/homeassistant/components/xbox/translations/id.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.",
+ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "create_entry": {
+ "default": "Berhasil diautentikasi"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Pilih Metode Autentikasi"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xbox/translations/ko.json b/homeassistant/components/xbox/translations/ko.json
new file mode 100644
index 00000000000000..0765928f8c9f64
--- /dev/null
+++ b/homeassistant/components/xbox/translations/ko.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "create_entry": {
+ "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xbox/translations/lb.json b/homeassistant/components/xbox/translations/lb.json
index d305909389ff41..b83b6d0a4995fe 100644
--- a/homeassistant/components/xbox/translations/lb.json
+++ b/homeassistant/components/xbox/translations/lb.json
@@ -1,8 +1,12 @@
{
"config": {
"abort": {
+ "authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL.",
"missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.",
"single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech."
+ },
+ "create_entry": {
+ "default": "Erfollegr\u00e4ich authentifiz\u00e9iert"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/xbox/translations/nl.json b/homeassistant/components/xbox/translations/nl.json
new file mode 100644
index 00000000000000..858fd264eaf992
--- /dev/null
+++ b/homeassistant/components/xbox/translations/nl.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.",
+ "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.",
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
+ },
+ "create_entry": {
+ "default": "Succesvol geauthenticeerd"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "Kies een authenticatie methode"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xbox/translations/tr.json b/homeassistant/components/xbox/translations/tr.json
new file mode 100644
index 00000000000000..a152eb194683cb
--- /dev/null
+++ b/homeassistant/components/xbox/translations/tr.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xbox/translations/uk.json b/homeassistant/components/xbox/translations/uk.json
new file mode 100644
index 00000000000000..a1b3f8340fc889
--- /dev/null
+++ b/homeassistant/components/xbox/translations/uk.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+ "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "create_entry": {
+ "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e."
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xbox_live/sensor.py b/homeassistant/components/xbox_live/sensor.py
index 300fcbfb095e63..2717bc1ad6268d 100644
--- a/homeassistant/components/xbox_live/sensor.py
+++ b/homeassistant/components/xbox_live/sensor.py
@@ -5,11 +5,10 @@
import voluptuous as vol
from xboxapi import Client
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_API_KEY, CONF_SCAN_INTERVAL
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
_LOGGER = logging.getLogger(__name__)
@@ -73,7 +72,7 @@ def get_user_gamercard(api, xuid):
return None
-class XboxSensor(Entity):
+class XboxSensor(SensorEntity):
"""A class for the Xbox account."""
def __init__(self, api, xuid, gamercard, interval):
@@ -104,7 +103,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attributes = {"gamerscore": self._gamerscore, "tier": self._tier}
diff --git a/homeassistant/components/xfinity/__init__.py b/homeassistant/components/xfinity/__init__.py
deleted file mode 100644
index 22e37eccde97b2..00000000000000
--- a/homeassistant/components/xfinity/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The xfinity component."""
diff --git a/homeassistant/components/xfinity/device_tracker.py b/homeassistant/components/xfinity/device_tracker.py
deleted file mode 100644
index 832c8bb1d5dc86..00000000000000
--- a/homeassistant/components/xfinity/device_tracker.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""Support for device tracking via Xfinity Gateways."""
-import logging
-
-from requests.exceptions import RequestException
-import voluptuous as vol
-from xfinity_gateway import XfinityGateway
-
-from homeassistant.components.device_tracker import (
- DOMAIN,
- PLATFORM_SCHEMA,
- DeviceScanner,
-)
-from homeassistant.const import CONF_HOST
-import homeassistant.helpers.config_validation as cv
-
-_LOGGER = logging.getLogger(__name__)
-
-DEFAULT_HOST = "10.0.0.1"
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string}
-)
-
-
-def get_scanner(hass, config):
- """Validate the configuration and return an Xfinity Gateway scanner."""
- _LOGGER.warning(
- "The Xfinity Gateway has been deprecated and will be removed from "
- "Home Assistant in version 0.109. Please remove it from your "
- "configuration. "
- )
-
- gateway = XfinityGateway(config[DOMAIN][CONF_HOST])
- scanner = None
- try:
- gateway.scan_devices()
- scanner = XfinityDeviceScanner(gateway)
- except (RequestException, ValueError):
- _LOGGER.error(
- "Error communicating with Xfinity Gateway. Check host: %s", gateway.host
- )
-
- return scanner
-
-
-class XfinityDeviceScanner(DeviceScanner):
- """This class queries an Xfinity Gateway."""
-
- def __init__(self, gateway):
- """Initialize the scanner."""
- self.gateway = gateway
-
- def scan_devices(self):
- """Scan for new devices and return a list of found MACs."""
- connected_devices = []
- try:
- connected_devices = self.gateway.scan_devices()
- except (RequestException, ValueError):
- _LOGGER.error("Unable to scan devices. Check connection to gateway")
- return connected_devices
-
- def get_device_name(self, device):
- """Return the name of the given device or None if we don't know."""
- return self.gateway.get_device_name(device)
diff --git a/homeassistant/components/xfinity/manifest.json b/homeassistant/components/xfinity/manifest.json
deleted file mode 100644
index 999b77dfb59799..00000000000000
--- a/homeassistant/components/xfinity/manifest.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "domain": "xfinity",
- "name": "Xfinity Gateway",
- "documentation": "https://www.home-assistant.io/integrations/xfinity",
- "requirements": ["xfinity-gateway==0.0.4"],
- "codeowners": ["@cisasteelersfan"]
-}
diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py
index c5b74e68af5839..ba7f717f42196e 100644
--- a/homeassistant/components/xiaomi_aqara/__init__.py
+++ b/homeassistant/components/xiaomi_aqara/__init__.py
@@ -9,10 +9,12 @@
from homeassistant import config_entries, core
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
+ ATTR_DEVICE_ID,
ATTR_VOLTAGE,
CONF_HOST,
CONF_MAC,
CONF_PORT,
+ CONF_PROTOCOL,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import callback
@@ -26,7 +28,6 @@
from .const import (
CONF_INTERFACE,
CONF_KEY,
- CONF_PROTOCOL,
CONF_SID,
DEFAULT_DISCOVERY_RETRY,
DOMAIN,
@@ -42,7 +43,6 @@
ATTR_GW_MAC = "gw_mac"
ATTR_RINGTONE_ID = "ringtone_id"
ATTR_RINGTONE_VOL = "ringtone_vol"
-ATTR_DEVICE_ID = "device_id"
TIME_TILL_UNAVAILABLE = timedelta(minutes=150)
@@ -188,9 +188,9 @@ def stop_xiaomi(event):
else:
platforms = GATEWAY_PLATFORMS_NO_KEY
- for component in platforms:
+ for platform in platforms:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -208,8 +208,8 @@ async def async_unload_entry(
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in platforms
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in platforms
]
)
)
@@ -241,7 +241,7 @@ def __init__(self, device, device_type, xiaomi_hub, config_entry):
self._type = device_type
self._write_to_hub = xiaomi_hub.write_to_hub
self._get_from_hub = xiaomi_hub.get_from_hub
- self._device_state_attributes = {}
+ self._extra_state_attributes = {}
self._remove_unavailability_tracker = None
self._xiaomi_hub = xiaomi_hub
self.parse_data(device["data"], device["raw_data"])
@@ -319,9 +319,9 @@ def should_poll(self):
return False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
- return self._device_state_attributes
+ return self._extra_state_attributes
@callback
def _async_set_unavailable(self, now):
@@ -364,11 +364,11 @@ def parse_voltage(self, data):
max_volt = 3300
min_volt = 2800
voltage = data[voltage_key]
- self._device_state_attributes[ATTR_VOLTAGE] = round(voltage / 1000.0, 2)
+ self._extra_state_attributes[ATTR_VOLTAGE] = round(voltage / 1000.0, 2)
voltage = min(voltage, max_volt)
voltage = max(voltage, min_volt)
percent = ((voltage - min_volt) / (max_volt - min_volt)) * 100
- self._device_state_attributes[ATTR_BATTERY_LEVEL] = round(percent, 1)
+ self._extra_state_attributes[ATTR_BATTERY_LEVEL] = round(percent, 1)
return True
def parse_data(self, data, raw_data):
diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py
index 8fbecee46e9d70..3d9437e377815e 100644
--- a/homeassistant/components/xiaomi_aqara/binary_sensor.py
+++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py
@@ -170,10 +170,10 @@ def __init__(self, device, xiaomi_hub, config_entry):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attrs = {ATTR_DENSITY: self._density}
- attrs.update(super().device_state_attributes)
+ attrs.update(super().extra_state_attributes)
return attrs
def parse_data(self, data, raw_data):
@@ -214,10 +214,10 @@ def __init__(self, device, hass, xiaomi_hub, config_entry):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attrs = {ATTR_NO_MOTION_SINCE: self._no_motion_since}
- attrs.update(super().device_state_attributes)
+ attrs.update(super().extra_state_attributes)
return attrs
@callback
@@ -308,10 +308,10 @@ def __init__(self, device, xiaomi_hub, config_entry):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attrs = {ATTR_OPEN_SINCE: self._open_since}
- attrs.update(super().device_state_attributes)
+ attrs.update(super().extra_state_attributes)
return attrs
def parse_data(self, data, raw_data):
@@ -389,10 +389,10 @@ def __init__(self, device, xiaomi_hub, config_entry):
)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attrs = {ATTR_DENSITY: self._density}
- attrs.update(super().device_state_attributes)
+ attrs.update(super().extra_state_attributes)
return attrs
def parse_data(self, data, raw_data):
@@ -424,10 +424,10 @@ def __init__(self, device, name, data_key, xiaomi_hub, config_entry):
super().__init__(device, name, xiaomi_hub, data_key, None, config_entry)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attrs = {ATTR_LAST_ACTION: self._last_action}
- attrs.update(super().device_state_attributes)
+ attrs.update(super().extra_state_attributes)
return attrs
def parse_data(self, data, raw_data):
@@ -459,10 +459,10 @@ def __init__(self, device, name, data_key, hass, xiaomi_hub, config_entry):
super().__init__(device, name, xiaomi_hub, data_key, None, config_entry)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attrs = {ATTR_LAST_ACTION: self._last_action}
- attrs.update(super().device_state_attributes)
+ attrs.update(super().extra_state_attributes)
return attrs
def parse_data(self, data, raw_data):
@@ -519,10 +519,10 @@ def __init__(self, device, hass, xiaomi_hub, config_entry):
super().__init__(device, "Cube", xiaomi_hub, data_key, None, config_entry)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attrs = {ATTR_LAST_ACTION: self._last_action}
- attrs.update(super().device_state_attributes)
+ attrs.update(super().extra_state_attributes)
return attrs
def parse_data(self, data, raw_data):
diff --git a/homeassistant/components/xiaomi_aqara/config_flow.py b/homeassistant/components/xiaomi_aqara/config_flow.py
index 6bf1aa4f4ee5e3..c080aec508dec6 100644
--- a/homeassistant/components/xiaomi_aqara/config_flow.py
+++ b/homeassistant/components/xiaomi_aqara/config_flow.py
@@ -6,15 +6,13 @@
from xiaomi_gateway import MULTICAST_PORT, XiaomiGateway, XiaomiGatewayDiscovery
from homeassistant import config_entries
-from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT
+from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_PROTOCOL
from homeassistant.core import callback
from homeassistant.helpers.device_registry import format_mac
-# pylint: disable=unused-import
from .const import (
CONF_INTERFACE,
CONF_KEY,
- CONF_PROTOCOL,
CONF_SID,
DEFAULT_DISCOVERY_RETRY,
DOMAIN,
@@ -181,7 +179,6 @@ async def async_step_zeroconf(self, discovery_info):
{CONF_HOST: self.host, CONF_MAC: mac_address}
)
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update({"title_placeholders": {"name": self.host}})
return await self.async_step_user()
diff --git a/homeassistant/components/xiaomi_aqara/const.py b/homeassistant/components/xiaomi_aqara/const.py
index 1cc3b2d4633e95..11706cdb6fb165 100644
--- a/homeassistant/components/xiaomi_aqara/const.py
+++ b/homeassistant/components/xiaomi_aqara/const.py
@@ -9,7 +9,6 @@
ZEROCONF_ACPARTNER = "lumi-acpartner"
CONF_INTERFACE = "interface"
-CONF_PROTOCOL = "protocol"
CONF_KEY = "key"
CONF_SID = "sid"
diff --git a/homeassistant/components/xiaomi_aqara/lock.py b/homeassistant/components/xiaomi_aqara/lock.py
index 7c5334e0f5c565..5afb1701e336b6 100644
--- a/homeassistant/components/xiaomi_aqara/lock.py
+++ b/homeassistant/components/xiaomi_aqara/lock.py
@@ -50,7 +50,7 @@ def changed_by(self) -> int:
return self._changed_by
@property
- def device_state_attributes(self) -> dict:
+ def extra_state_attributes(self) -> dict:
"""Return the state attributes."""
attributes = {ATTR_VERIFIED_WRONG_TIMES: self._verified_wrong_times}
return attributes
diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py
index 5b1d3467d25b63..fa3d265f12fd97 100644
--- a/homeassistant/components/xiaomi_aqara/sensor.py
+++ b/homeassistant/components/xiaomi_aqara/sensor.py
@@ -1,6 +1,7 @@
"""Support for Xiaomi Aqara sensors."""
import logging
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
DEVICE_CLASS_BATTERY,
@@ -107,7 +108,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities)
-class XiaomiSensor(XiaomiDevice):
+class XiaomiSensor(XiaomiDevice, SensorEntity):
"""Representation of a XiaomiSensor."""
def __init__(self, device, name, data_key, xiaomi_hub, config_entry):
@@ -171,7 +172,7 @@ def parse_data(self, data, raw_data):
return True
-class XiaomiBatterySensor(XiaomiDevice):
+class XiaomiBatterySensor(XiaomiDevice, SensorEntity):
"""Representation of a XiaomiSensor."""
@property
@@ -194,7 +195,7 @@ def parse_data(self, data, raw_data):
succeed = super().parse_voltage(data)
if not succeed:
return False
- battery_level = int(self._device_state_attributes.pop(ATTR_BATTERY_LEVEL))
+ battery_level = int(self._extra_state_attributes.pop(ATTR_BATTERY_LEVEL))
if battery_level <= 0 or battery_level > 100:
return False
self._state = battery_level
diff --git a/homeassistant/components/xiaomi_aqara/strings.json b/homeassistant/components/xiaomi_aqara/strings.json
index d5fb25d2c3f3ea..a2c8a226c95eac 100644
--- a/homeassistant/components/xiaomi_aqara/strings.json
+++ b/homeassistant/components/xiaomi_aqara/strings.json
@@ -4,7 +4,7 @@
"step": {
"user": {
"title": "Xiaomi Aqara Gateway",
- "description": "Connect to your Xiaomi Aqara Gateway, if the IP and mac addresses are left empty, auto-discovery is used",
+ "description": "Connect to your Xiaomi Aqara Gateway, if the IP and MAC addresses are left empty, auto-discovery is used",
"data": {
"interface": "The network interface to use",
"host": "[%key:common::config_flow::data::ip%] (optional)",
@@ -21,7 +21,7 @@
},
"select": {
"title": "Select the Xiaomi Aqara Gateway that you wish to connect",
- "description": "Run the setup again if you want to connect aditional gateways",
+ "description": "Run the setup again if you want to connect additional gateways",
"data": {
"select_ip": "[%key:common::config_flow::data::ip%]"
}
diff --git a/homeassistant/components/xiaomi_aqara/switch.py b/homeassistant/components/xiaomi_aqara/switch.py
index 6e75ddb487e01a..8b16b6491c7ace 100644
--- a/homeassistant/components/xiaomi_aqara/switch.py
+++ b/homeassistant/components/xiaomi_aqara/switch.py
@@ -157,7 +157,7 @@ def is_on(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
if self._supports_power_consumption:
attrs = {
@@ -167,7 +167,7 @@ def device_state_attributes(self):
}
else:
attrs = {}
- attrs.update(super().device_state_attributes)
+ attrs.update(super().extra_state_attributes)
return attrs
@property
diff --git a/homeassistant/components/xiaomi_aqara/translations/de.json b/homeassistant/components/xiaomi_aqara/translations/de.json
index f86868987a0696..e72f00d5f5fe63 100644
--- a/homeassistant/components/xiaomi_aqara/translations/de.json
+++ b/homeassistant/components/xiaomi_aqara/translations/de.json
@@ -1,14 +1,41 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
+ "not_xiaomi_aqara": "Kein Xiaomi Aqara Gateway, gefundenes Ger\u00e4t stimmt nicht mit bekannten Gateways \u00fcberein"
+ },
+ "error": {
+ "discovery_error": "Es konnte kein Xiaomi Aqara Gateway gefunden werden, versuche die IP von Home Assistant als Interface zu nutzen",
+ "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse, schau unter https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem",
+ "invalid_interface": "Ung\u00fcltige Netzwerkschnittstelle",
+ "invalid_key": "Ung\u00fcltiger Gateway-Schl\u00fcssel",
+ "invalid_mac": "Ung\u00fcltige MAC-Adresse"
+ },
"flow_title": "Xiaomi Aqara Gateway: {name}",
"step": {
"select": {
"data": {
"select_ip": "IP-Adresse"
- }
+ },
+ "description": "F\u00fchre das Setup erneut aus, wenn du zus\u00e4tzliche Gateways verbinden m\u00f6chtest",
+ "title": "W\u00e4hle das Xiaomi Aqara Gateway, das du verbinden m\u00f6chtest"
+ },
+ "settings": {
+ "data": {
+ "key": "Der Schl\u00fcssel deines Gateways",
+ "name": "Name des Gateways"
+ },
+ "description": "Der Schl\u00fcssel (das Passwort) kann mithilfe dieser Anleitung abgerufen werden: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Wenn der Schl\u00fcssel nicht angegeben wird, sind nur die Sensoren zug\u00e4nglich",
+ "title": "Xiaomi Aqara Gateway, optionale Einstellungen"
},
"user": {
- "description": "Stellen Sie eine Verbindung zu Ihrem Xiaomi Aqara Gateway her. Wenn die IP- und Mac-Adressen leer bleiben, wird die automatische Erkennung verwendet",
+ "data": {
+ "host": "IP-Adresse",
+ "interface": "Die zu verwendende Netzwerkschnittstelle",
+ "mac": "MAC-Adresse"
+ },
+ "description": "Stelle eine Verbindung zu deinem Xiaomi Aqara Gateway her. Wenn die IP- und MAC-Adressen leer bleiben, wird die automatische Erkennung verwendet",
"title": "Xiaomi Aqara Gateway"
}
}
diff --git a/homeassistant/components/xiaomi_aqara/translations/en.json b/homeassistant/components/xiaomi_aqara/translations/en.json
index 075c8d4a194bcc..d51687a079056c 100644
--- a/homeassistant/components/xiaomi_aqara/translations/en.json
+++ b/homeassistant/components/xiaomi_aqara/translations/en.json
@@ -18,7 +18,7 @@
"data": {
"select_ip": "IP Address"
},
- "description": "Run the setup again if you want to connect aditional gateways",
+ "description": "Run the setup again if you want to connect additional gateways",
"title": "Select the Xiaomi Aqara Gateway that you wish to connect"
},
"settings": {
@@ -35,7 +35,7 @@
"interface": "The network interface to use",
"mac": "Mac Address (optional)"
},
- "description": "Connect to your Xiaomi Aqara Gateway, if the IP and mac addresses are left empty, auto-discovery is used",
+ "description": "Connect to your Xiaomi Aqara Gateway, if the IP and MAC addresses are left empty, auto-discovery is used",
"title": "Xiaomi Aqara Gateway"
}
}
diff --git a/homeassistant/components/xiaomi_aqara/translations/hu.json b/homeassistant/components/xiaomi_aqara/translations/hu.json
index 1a69e20c6b1edf..295fcef83feb83 100644
--- a/homeassistant/components/xiaomi_aqara/translations/hu.json
+++ b/homeassistant/components/xiaomi_aqara/translations/hu.json
@@ -2,11 +2,12 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
- "already_in_progress": "A konfigur\u00e1ci\u00f3s folyamat m\u00e1r fut"
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
+ "not_xiaomi_aqara": "Nem egy Xiaomi Aqara Gateway, a felfedezett eszk\u00f6z nem egyezett az ismert \u00e1tj\u00e1r\u00f3kkal"
},
"error": {
"discovery_error": "Nem siker\u00fclt felfedezni a Xiaomi Aqara K\u00f6zponti egys\u00e9get, pr\u00f3b\u00e1lja meg interf\u00e9szk\u00e9nt haszn\u00e1lni a HomeAssistant futtat\u00f3 eszk\u00f6z IP-j\u00e9t",
- "invalid_host": " , l\u00e1sd: https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem",
+ "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm, l\u00e1sd: https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem",
"invalid_interface": "\u00c9rv\u00e9nytelen h\u00e1l\u00f3zati interf\u00e9sz",
"invalid_key": "\u00c9rv\u00e9nytelen kulcs",
"invalid_mac": "\u00c9rv\u00e9nytelen Mac-c\u00edm"
@@ -17,7 +18,7 @@
"data": {
"select_ip": "IP c\u00edm"
},
- "description": "Futtassa \u00fajra a be\u00e1ll\u00edt\u00e1st, ha egy m\u00e1sik K\u00f6zponti egys\u00e9get szeretne csatlakoztatni",
+ "description": "Futtassa \u00fajra a be\u00e1ll\u00edt\u00e1st, ha egy m\u00e1sik k\u00f6zponti egys\u00e9get szeretne csatlakoztatni",
"title": "V\u00e1lassza ki a csatlakoztatni k\u00edv\u00e1nt Xiaomi Aqara K\u00f6zponti egys\u00e9get"
},
"settings": {
@@ -25,7 +26,7 @@
"key": "K\u00f6zponti egys\u00e9g kulcsa",
"name": "K\u00f6zponti egys\u00e9g neve"
},
- "description": "A kulcs (jelsz\u00f3) az al\u00e1bbi oktat\u00f3anyag seg\u00edts\u00e9g\u00e9vel t\u00f6lthet\u0151 le: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Ha a kulcs nincs megadva, csak az \u00e9rz\u00e9kel\u0151k f\u00e9rhetnek hozz\u00e1",
+ "description": "A kulcs (jelsz\u00f3) az al\u00e1bbi oktat\u00f3anyag seg\u00edts\u00e9g\u00e9vel t\u00f6lthet\u0151 le: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Ha a kulcs nincs megadva, csak az \u00e9rz\u00e9kel\u0151k lesznek hozz\u00e1f\u00e9rhet\u0151k",
"title": "Xiaomi Aqara k\u00f6zponti egys\u00e9g, opcion\u00e1lis be\u00e1ll\u00edt\u00e1sok"
},
"user": {
@@ -34,7 +35,7 @@
"interface": "A haszn\u00e1lni k\u00edv\u00e1nt h\u00e1l\u00f3zati interf\u00e9sz",
"mac": "Mac-c\u00edm (opcion\u00e1lis)"
},
- "description": "Csatlakozzon a Xiaomi Aqara k\u00f6zponti egys\u00e9ghez, ha az IP- \u00e9s a mac-c\u00edm \u00fcresen marad, akkor az automatikus felder\u00edt\u00e9st haszn\u00e1lja",
+ "description": "Csatlakozzon a Xiaomi Aqara k\u00f6zponti egys\u00e9ghez, ha az IP- \u00e9s a MAC-c\u00edm \u00fcresen marad, akkor az automatikus felder\u00edt\u00e9st haszn\u00e1lja",
"title": "Xiaomi Aqara k\u00f6zponti egys\u00e9g"
}
}
diff --git a/homeassistant/components/xiaomi_aqara/translations/id.json b/homeassistant/components/xiaomi_aqara/translations/id.json
new file mode 100644
index 00000000000000..5a2acfa330a709
--- /dev/null
+++ b/homeassistant/components/xiaomi_aqara/translations/id.json
@@ -0,0 +1,43 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "not_xiaomi_aqara": "Bukan Gateway Xiaomi Aqara, perangkat yang ditemukan tidak sesuai dengan gateway yang dikenal"
+ },
+ "error": {
+ "discovery_error": "Gagal menemukan Xiaomi Aqara Gateway, coba gunakan IP perangkat yang menjalankan HomeAssistant sebagai antarmuka",
+ "invalid_host": "Nama host atau alamat IP tidak valid, lihat https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem",
+ "invalid_interface": "Antarmuka jaringan tidak valid",
+ "invalid_key": "Kunci gateway tidak valid",
+ "invalid_mac": "Alamat MAC Tidak Valid"
+ },
+ "flow_title": "Xiaomi Aqara Gateway: {name}",
+ "step": {
+ "select": {
+ "data": {
+ "select_ip": "Alamat IP"
+ },
+ "description": "Jalankan penyiapan lagi jika Anda ingin menghubungkan gateway lainnya",
+ "title": "Pilih Gateway Xiaomi Aqara yang ingin dihubungkan"
+ },
+ "settings": {
+ "data": {
+ "key": "Kunci gateway Anda",
+ "name": "Nama Gateway"
+ },
+ "description": "Kunci (kata sandi) dapat diambil menggunakan tutorial ini: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Jika kunci tidak disediakan, hanya sensor yang akan dapat diakses",
+ "title": "Xiaomi Aqara Gateway, pengaturan opsional"
+ },
+ "user": {
+ "data": {
+ "host": "Alamat IP (opsional)",
+ "interface": "Antarmuka jaringan yang akan digunakan",
+ "mac": "Alamat MAC (opsional)"
+ },
+ "description": "Hubungkan ke Xiaomi Aqara Gateway Anda, jika alamat IP dan MAC dibiarkan kosong, penemuan otomatis digunakan",
+ "title": "Xiaomi Aqara Gateway"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_aqara/translations/it.json b/homeassistant/components/xiaomi_aqara/translations/it.json
index 3299fa092f8293..275729e4e81a1d 100644
--- a/homeassistant/components/xiaomi_aqara/translations/it.json
+++ b/homeassistant/components/xiaomi_aqara/translations/it.json
@@ -18,7 +18,7 @@
"data": {
"select_ip": "Indirizzo IP"
},
- "description": "Eseguire nuovamente l'installazione se si desidera connettere gateway adizionali",
+ "description": "Esegui di nuovo la configurazione se desideri connettere gateway aggiuntivi",
"title": "Selezionare il Gateway Xiaomi Aqara che si desidera collegare"
},
"settings": {
@@ -35,7 +35,7 @@
"interface": "L'interfaccia di rete da utilizzare",
"mac": "Indirizzo Mac (opzionale)"
},
- "description": "Connettiti al tuo Xiaomi Aqara Gateway, se gli indirizzi IP e mac sono lasciati vuoti, verr\u00e0 utilizzato il rilevamento automatico",
+ "description": "Connettiti al tuo Xiaomi Aqara Gateway, se gli indirizzi IP e MAC sono lasciati vuoti, verr\u00e0 utilizzato il rilevamento automatico",
"title": "Xiaomi Aqara Gateway"
}
}
diff --git a/homeassistant/components/xiaomi_aqara/translations/ko.json b/homeassistant/components/xiaomi_aqara/translations/ko.json
index 1b4e11c6ea3ff0..dd8a9ae5ede0aa 100644
--- a/homeassistant/components/xiaomi_aqara/translations/ko.json
+++ b/homeassistant/components/xiaomi_aqara/translations/ko.json
@@ -2,21 +2,23 @@
"config": {
"abort": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "already_in_progress": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
"not_xiaomi_aqara": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774\uac00 \uc544\ub2d9\ub2c8\ub2e4. \ubc1c\uacac\ub41c \uae30\uae30\uac00 \uc54c\ub824\uc9c4 \uac8c\uc774\ud2b8\uc6e8\uc774\uc640 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4"
},
"error": {
"discovery_error": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \ubc1c\uacac\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. HomeAssistant \ub97c \uc778\ud130\ud398\uc774\uc2a4\ub85c \uc0ac\uc6a9\ud558\ub294 \uae30\uae30\uc758 IP \ub85c \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694.",
+ "invalid_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694",
"invalid_interface": "\ub124\ud2b8\uc6cc\ud06c \uc778\ud130\ud398\uc774\uc2a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "invalid_key": "\uac8c\uc774\ud2b8\uc6e8\uc774 \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "invalid_key": "\uac8c\uc774\ud2b8\uc6e8\uc774 \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "invalid_mac": "Mac \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"flow_title": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774: {name}",
"step": {
"select": {
"data": {
- "select_ip": "\uac8c\uc774\ud2b8\uc6e8\uc774 IP"
+ "select_ip": "IP \uc8fc\uc18c"
},
- "description": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uc5f0\uacb0\uc744 \ucd94\uac00\ud558\ub824\uba74 \uc124\uc815\uc744 \ub2e4\uc2dc \uc2e4\ud589\ud574\uc8fc\uc138\uc694",
+ "description": "\uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \ucd94\uac00 \uc5f0\uacb0\ud558\ub824\uba74 \uc124\uc815\uc744 \ub2e4\uc2dc \uc2e4\ud589\ud574\uc8fc\uc138\uc694",
"title": "\uc5f0\uacb0\ud560 Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774 \uc120\ud0dd\ud558\uae30"
},
"settings": {
@@ -24,16 +26,16 @@
"key": "\uac8c\uc774\ud2b8\uc6e8\uc774 \ud0a4",
"name": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uc774\ub984"
},
- "description": "\ud0a4(\ube44\ubc00\ubc88\ud638)\ub97c \uc5bb\uc740 \ubc29\ubc95\uc740 \ub2e4\uc74c\uc758 \uc548\ub0b4\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. \ud0a4\uac00 \uc81c\uacf5\ub418\uc9c0 \uc54a\uc73c\uba74 \uc13c\uc11c\uc5d0\ub9cc \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "description": "\ud0a4\uac00 \uc81c\uacf5\ub418\uc9c0 \uc54a\uc73c\uba74 \uc13c\uc11c\uc5d0\ub9cc \uc811\uadfc\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ud0a4(\ube44\ubc00\ubc88\ud638)\ub97c \uc5bb\ub294 \ubc29\ubc95\uc740 \ub2e4\uc74c\uc758 \uc548\ub0b4\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz",
"title": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774 \ucd94\uac00 \uc124\uc815\ud558\uae30"
},
"user": {
"data": {
"host": "IP \uc8fc\uc18c (\uc120\ud0dd \uc0ac\ud56d)",
"interface": "\uc0ac\uc6a9\ud560 \ub124\ud2b8\uc6cc\ud06c \uc778\ud130\ud398\uc774\uc2a4",
- "mac": "Mac \uc8fc\uc18c(\uc120\ud0dd \uc0ac\ud56d)"
+ "mac": "Mac \uc8fc\uc18c (\uc120\ud0dd \uc0ac\ud56d)"
},
- "description": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud569\ub2c8\ub2e4. IP \ubc0f Mac \uc8fc\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc790\ub3d9 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4",
+ "description": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud569\ub2c8\ub2e4. IP \uc8fc\uc18c \ubc0f MAC \uc8fc\uc18c\ub97c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4",
"title": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774"
}
}
diff --git a/homeassistant/components/xiaomi_aqara/translations/nl.json b/homeassistant/components/xiaomi_aqara/translations/nl.json
index e17b3b572d1988..a356ed36e1bbaf 100644
--- a/homeassistant/components/xiaomi_aqara/translations/nl.json
+++ b/homeassistant/components/xiaomi_aqara/translations/nl.json
@@ -1,20 +1,38 @@
{
"config": {
"abort": {
- "already_configured": "Apparaat is al geconfigureerd"
+ "already_configured": "Apparaat is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
+ "not_xiaomi_aqara": "Geen Xiaomi Aqara Gateway, ontdekt apparaat kwam niet overeen met bekende gateways"
+ },
+ "error": {
+ "discovery_error": "Het is niet gelukt om een Xiaomi Aqara Gateway te vinden, probeer het IP van het apparaat waarop HomeAssistant draait als interface te gebruiken",
+ "invalid_host": "Ongeldige hostnaam of IP-adres, zie https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem",
+ "invalid_interface": "Ongeldige netwerkinterface",
+ "invalid_key": "Ongeldige gatewaysleutel",
+ "invalid_mac": "Ongeldig MAC-adres"
},
"flow_title": "Xiaomi Aqara Gateway: {name}",
"step": {
"select": {
+ "data": {
+ "select_ip": "IP-adres"
+ },
"description": "Voer de installatie opnieuw uit als u extra gateways wilt aansluiten",
"title": "Selecteer de Xiaomi Aqara Gateway waarmee u verbinding wilt maken"
},
"settings": {
- "description": "De sleutel (wachtwoord) kan worden opgehaald met behulp van deze tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Als de sleutel niet wordt meegeleverd, zijn alleen sensoren toegankelijk"
+ "data": {
+ "key": "De sleutel van uw gateway",
+ "name": "Naam van de Gateway"
+ },
+ "description": "De sleutel (wachtwoord) kan worden opgehaald met behulp van deze tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Als de sleutel niet wordt meegeleverd, zijn alleen sensoren toegankelijk",
+ "title": "Xiaomi Aqara Gateway, optionele instellingen"
},
"user": {
"data": {
"host": "IP-adres (optioneel)",
+ "interface": "De netwerkinterface die moet worden gebruikt",
"mac": "MAC-adres (optioneel)"
},
"description": "Maak verbinding met uw Xiaomi Aqara Gateway, als de IP- en mac-adressen leeg worden gelaten, wordt automatische detectie gebruikt",
diff --git a/homeassistant/components/xiaomi_aqara/translations/no.json b/homeassistant/components/xiaomi_aqara/translations/no.json
index 523e2da898c4c7..5a46d66fcf0464 100644
--- a/homeassistant/components/xiaomi_aqara/translations/no.json
+++ b/homeassistant/components/xiaomi_aqara/translations/no.json
@@ -18,7 +18,7 @@
"data": {
"select_ip": "IP adresse"
},
- "description": "Kj\u00f8r oppsettet igjen hvis du vil koble til tilleggsportaler",
+ "description": "Kj\u00f8r oppsettet p\u00e5 nytt hvis du vil koble til flere gatewayer",
"title": "Velg Xiaomi Aqara Gateway som du \u00f8nsker \u00e5 koble til"
},
"settings": {
@@ -35,7 +35,7 @@
"interface": "Nettverksgrensesnittet som skal brukes",
"mac": "MAC-adresse (valgfritt)"
},
- "description": "Koble til Xiaomi Aqara Gateway, hvis IP- og MAC-adressene er tomme, brukes automatisk oppdagelse",
+ "description": "Koble til Xiaomi Aqara Gateway, hvis IP- og MAC-adressene blir tomme, brukes automatisk oppdagelse",
"title": ""
}
}
diff --git a/homeassistant/components/xiaomi_aqara/translations/ru.json b/homeassistant/components/xiaomi_aqara/translations/ru.json
index 96da0a24074962..4ede8019a4f003 100644
--- a/homeassistant/components/xiaomi_aqara/translations/ru.json
+++ b/homeassistant/components/xiaomi_aqara/translations/ru.json
@@ -18,7 +18,7 @@
"data": {
"select_ip": "IP-\u0430\u0434\u0440\u0435\u0441"
},
- "description": "\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0435\u0449\u0451 \u0440\u0430\u0437, \u0435\u0441\u043b\u0438 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u0448\u043b\u044e\u0437",
+ "description": "\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0435\u0449\u0451 \u0440\u0430\u0437, \u0435\u0441\u043b\u0438 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u0448\u043b\u044e\u0437.",
"title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 Xiaomi Aqara"
},
"settings": {
diff --git a/homeassistant/components/xiaomi_aqara/translations/tr.json b/homeassistant/components/xiaomi_aqara/translations/tr.json
index 10d1374187e594..24da29417d14dc 100644
--- a/homeassistant/components/xiaomi_aqara/translations/tr.json
+++ b/homeassistant/components/xiaomi_aqara/translations/tr.json
@@ -1,7 +1,38 @@
{
"config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor",
+ "not_xiaomi_aqara": "Xiaomi Aqara A\u011f Ge\u00e7idi de\u011fil, ke\u015ffedilen cihaz bilinen a\u011f ge\u00e7itleriyle e\u015fle\u015fmedi"
+ },
"error": {
+ "discovery_error": "Bir Xiaomi Aqara A\u011f Ge\u00e7idi ke\u015ffedilemedi, HomeAssistant'\u0131 aray\u00fcz olarak \u00e7al\u0131\u015ft\u0131ran cihaz\u0131n IP'sini kullanmay\u0131 deneyin",
+ "invalid_interface": "Ge\u00e7ersiz a\u011f aray\u00fcz\u00fc",
+ "invalid_key": "Ge\u00e7ersiz a\u011f ge\u00e7idi anahtar\u0131",
"invalid_mac": "Ge\u00e7ersiz Mac Adresi"
+ },
+ "flow_title": "Xiaomi Aqara A\u011f Ge\u00e7idi: {name}",
+ "step": {
+ "select": {
+ "data": {
+ "select_ip": "\u0130p Adresi"
+ },
+ "description": "Ek a\u011f ge\u00e7itlerini ba\u011flamak istiyorsan\u0131z kurulumu tekrar \u00e7al\u0131\u015ft\u0131r\u0131n.",
+ "title": "Ba\u011flamak istedi\u011finiz Xiaomi Aqara A\u011f Ge\u00e7idini se\u00e7in"
+ },
+ "settings": {
+ "data": {
+ "key": "A\u011f ge\u00e7idinizin anahtar\u0131",
+ "name": "A\u011f Ge\u00e7idinin Ad\u0131"
+ },
+ "description": "Anahtar (parola) bu \u00f6\u011fretici kullan\u0131larak al\u0131nabilir: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Anahtar sa\u011flanmazsa, yaln\u0131zca sens\u00f6rlere eri\u015filebilir"
+ },
+ "user": {
+ "data": {
+ "host": "\u0130p Adresi (iste\u011fe ba\u011fl\u0131)",
+ "mac": "Mac Adresi (iste\u011fe ba\u011fl\u0131)"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_aqara/translations/uk.json b/homeassistant/components/xiaomi_aqara/translations/uk.json
new file mode 100644
index 00000000000000..1598e96b38eecc
--- /dev/null
+++ b/homeassistant/components/xiaomi_aqara/translations/uk.json
@@ -0,0 +1,43 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.",
+ "not_xiaomi_aqara": "\u0426\u0435 \u043d\u0435 \u0448\u043b\u044e\u0437 Xiaomi Aqara. \u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0454 \u0432\u0456\u0434\u043e\u043c\u0438\u043c \u0448\u043b\u044e\u0437\u0456\u0432."
+ },
+ "error": {
+ "discovery_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0438\u044f\u0432\u0438\u0442\u0438 \u0448\u043b\u044e\u0437 Xiaomi Aqara, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u0442\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 HomeAssistant \u0432 \u044f\u043a\u043e\u0441\u0442\u0456 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443.",
+ "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430. . \u0421\u043f\u043e\u0441\u043e\u0431\u0438 \u0432\u0438\u0440\u0456\u0448\u0435\u043d\u043d\u044f \u0446\u0456\u0454\u0457 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u043e\u043f\u0438\u0441\u0430\u043d\u0456 \u0442\u0443\u0442: https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem.",
+ "invalid_interface": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0439 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.",
+ "invalid_key": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 \u0448\u043b\u044e\u0437\u0443.",
+ "invalid_mac": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430 MAC-\u0430\u0434\u0440\u0435\u0441\u0430."
+ },
+ "flow_title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara: {name}",
+ "step": {
+ "select": {
+ "data": {
+ "select_ip": "IP-\u0430\u0434\u0440\u0435\u0441\u0430"
+ },
+ "description": "\u041f\u043e\u0447\u043d\u0456\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u044e\u0432\u0430\u043d\u043d\u044f \u0437\u043d\u043e\u0432\u0443, \u044f\u043a\u0449\u043e \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0434\u043e\u0434\u0430\u0442\u0438 \u0456\u043d\u0448\u0438\u0439 \u0448\u043b\u044e\u0437",
+ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0448\u043b\u044e\u0437 Xiaomi Aqara"
+ },
+ "settings": {
+ "data": {
+ "key": "\u041a\u043b\u044e\u0447",
+ "name": "\u041d\u0430\u0437\u0432\u0430"
+ },
+ "description": "\u041a\u043b\u044e\u0447 (\u043f\u0430\u0440\u043e\u043b\u044c) \u043c\u043e\u0436\u043d\u0430 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0446\u0456\u0454\u0457 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0457: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. \u042f\u043a\u0449\u043e \u043a\u043b\u044e\u0447 \u043d\u0435 \u0432\u043a\u0430\u0437\u0430\u043d\u043e, \u0431\u0443\u0434\u0443\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0456 \u0442\u0456\u043b\u044c\u043a\u0438 \u0434\u0430\u0442\u0447\u0438\u043a\u0438.",
+ "title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara"
+ },
+ "user": {
+ "data": {
+ "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)",
+ "interface": "\u041c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0439 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441",
+ "mac": "MAC-\u0430\u0434\u0440\u0435\u0441\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)"
+ },
+ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437\u0456 \u0448\u043b\u044e\u0437\u043e\u043c Xiaomi Aqara. \u0414\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u0448\u043b\u044e\u0437\u0443, \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u043b\u044f IP \u0456 MAC-\u0430\u0434\u0440\u0435\u0441\u0438 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c\u0438.",
+ "title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json
index 582aea354c6f01..5d2d097e832d53 100644
--- a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json
+++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json
@@ -18,7 +18,7 @@
"data": {
"select_ip": "IP \u4f4d\u5740"
},
- "description": "\u5982\u679c\u9084\u6709\u5176\u4ed6\u7db2\u95dc\u9700\u8981\u9023\u7dda\uff0c\u8acb\u518d\u57f7\u884c\u4e00\u6b21\u8a2d\u5b9a",
+ "description": "\u5982\u679c\u9084\u9700\u8981\u9023\u7dda\u81f3\u5176\u4ed6\u7db2\u95dc\uff0c\u8acb\u518d\u57f7\u884c\u4e00\u6b21\u8a2d\u5b9a",
"title": "\u9078\u64c7\u6240\u8981\u9023\u7dda\u7684\u5c0f\u7c73 Aqara \u7db2\u95dc"
},
"settings": {
diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py
index 7ff1ed999c46c0..f97d4623d69191 100644
--- a/homeassistant/components/xiaomi_miio/__init__.py
+++ b/homeassistant/components/xiaomi_miio/__init__.py
@@ -1,13 +1,38 @@
"""Support for Xiaomi Miio."""
+from datetime import timedelta
+import logging
+
+from miio.gateway.gateway import GatewayException
+
from homeassistant import config_entries, core
from homeassistant.const import CONF_HOST, CONF_TOKEN
from homeassistant.helpers import device_registry as dr
-
-from .config_flow import CONF_FLOW_TYPE, CONF_GATEWAY
-from .const import DOMAIN
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+
+from .const import (
+ ATTR_AVAILABLE,
+ CONF_DEVICE,
+ CONF_FLOW_TYPE,
+ CONF_GATEWAY,
+ CONF_MODEL,
+ DOMAIN,
+ KEY_COORDINATOR,
+ MODELS_AIR_MONITOR,
+ MODELS_FAN,
+ MODELS_LIGHT,
+ MODELS_SWITCH,
+ MODELS_VACUUM,
+)
from .gateway import ConnectXiaomiGateway
-GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor", "light"]
+_LOGGER = logging.getLogger(__name__)
+
+GATEWAY_PLATFORMS = ["alarm_control_panel", "light", "sensor", "switch"]
+SWITCH_PLATFORMS = ["switch"]
+FAN_PLATFORMS = ["fan"]
+LIGHT_PLATFORMS = ["light"]
+VACUUM_PLATFORMS = ["vacuum"]
+AIR_MONITOR_PLATFORMS = ["air_quality", "sensor"]
async def async_setup(hass: core.HomeAssistant, config: dict):
@@ -19,12 +44,16 @@ async def async_setup_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
):
"""Set up the Xiaomi Miio components from a config entry."""
- hass.data[DOMAIN] = {}
- if entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY:
- if not await async_setup_gateway_entry(hass, entry):
- return False
+ hass.data.setdefault(DOMAIN, {})
+ if entry.data[
+ CONF_FLOW_TYPE
+ ] == CONF_GATEWAY and not await async_setup_gateway_entry(hass, entry):
+ return False
- return True
+ return bool(
+ entry.data[CONF_FLOW_TYPE] != CONF_DEVICE
+ or await async_setup_device_entry(hass, entry)
+ )
async def async_setup_gateway_entry(
@@ -46,8 +75,6 @@ async def async_setup_gateway_entry(
return False
gateway_info = gateway.gateway_info
- hass.data[DOMAIN][entry.entry_id] = gateway.gateway_device
-
gateway_model = f"{gateway_info.model}-{gateway_info.hardware_version}"
device_registry = await dr.async_get_registry(hass)
@@ -61,9 +88,74 @@ async def async_setup_gateway_entry(
sw_version=gateway_info.firmware_version,
)
- for component in GATEWAY_PLATFORMS:
+ def update_data():
+ """Fetch data from the subdevice."""
+ data = {}
+ for sub_device in gateway.gateway_device.devices.values():
+ try:
+ sub_device.update()
+ except GatewayException as ex:
+ _LOGGER.error("Got exception while fetching the state: %s", ex)
+ data[sub_device.sid] = {ATTR_AVAILABLE: False}
+ else:
+ data[sub_device.sid] = {ATTR_AVAILABLE: True}
+ return data
+
+ async def async_update_data():
+ """Fetch data from the subdevice using async_add_executor_job."""
+ return await hass.async_add_executor_job(update_data)
+
+ # Create update coordinator
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ # Name of the data. For logging purposes.
+ name=name,
+ update_method=async_update_data,
+ # Polling interval. Will only be polled if there are subscribers.
+ update_interval=timedelta(seconds=10),
+ )
+
+ hass.data[DOMAIN][entry.entry_id] = {
+ CONF_GATEWAY: gateway.gateway_device,
+ KEY_COORDINATOR: coordinator,
+ }
+
+ for platform in GATEWAY_PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
+ return True
+
+
+async def async_setup_device_entry(
+ hass: core.HomeAssistant, entry: config_entries.ConfigEntry
+):
+ """Set up the Xiaomi Miio device component from a config entry."""
+ model = entry.data[CONF_MODEL]
+
+ # Identify platforms to setup
+ platforms = []
+ if model in MODELS_SWITCH:
+ platforms = SWITCH_PLATFORMS
+ elif model in MODELS_FAN:
+ platforms = FAN_PLATFORMS
+ elif model in MODELS_LIGHT:
+ platforms = LIGHT_PLATFORMS
+ for vacuum_model in MODELS_VACUUM:
+ if model.startswith(vacuum_model):
+ platforms = VACUUM_PLATFORMS
+ for air_monitor_model in MODELS_AIR_MONITOR:
+ if model.startswith(air_monitor_model):
+ platforms = AIR_MONITOR_PLATFORMS
+
+ if not platforms:
+ return False
+
+ for platform in platforms:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py
index 1e1e1b58632f66..4b56c60cd8251f 100644
--- a/homeassistant/components/xiaomi_miio/air_quality.py
+++ b/homeassistant/components/xiaomi_miio/air_quality.py
@@ -1,19 +1,25 @@
"""Support for Xiaomi Mi Air Quality Monitor (PM2.5)."""
import logging
-from miio import AirQualityMonitor, Device, DeviceException
+from miio import AirQualityMonitor, AirQualityMonitorCGDN1, DeviceException
import voluptuous as vol
from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity
+from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
-from homeassistant.exceptions import NoEntitySpecifiedError, PlatformNotReady
import homeassistant.helpers.config_validation as cv
from .const import (
+ CONF_DEVICE,
+ CONF_FLOW_TYPE,
+ CONF_MODEL,
+ DOMAIN,
MODEL_AIRQUALITYMONITOR_B1,
+ MODEL_AIRQUALITYMONITOR_CGDN1,
MODEL_AIRQUALITYMONITOR_S1,
MODEL_AIRQUALITYMONITOR_V1,
)
+from .device import XiaomiMiioEntity
_LOGGER = logging.getLogger(__name__)
@@ -40,53 +46,13 @@
}
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the sensor from config."""
-
- host = config[CONF_HOST]
- token = config[CONF_TOKEN]
- name = config[CONF_NAME]
-
- _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
-
- miio_device = Device(host, token)
-
- try:
- device_info = await hass.async_add_executor_job(miio_device.info)
- except DeviceException as ex:
- raise PlatformNotReady from ex
-
- model = device_info.model
- unique_id = f"{model}-{device_info.mac_address}"
- _LOGGER.debug(
- "%s %s %s detected",
- model,
- device_info.firmware_version,
- device_info.hardware_version,
- )
-
- device = AirQualityMonitor(host, token, model=model)
-
- if model == MODEL_AIRQUALITYMONITOR_S1:
- entity = AirMonitorS1(name, device, unique_id)
- elif model == MODEL_AIRQUALITYMONITOR_B1:
- entity = AirMonitorB1(name, device, unique_id)
- elif model == MODEL_AIRQUALITYMONITOR_V1:
- entity = AirMonitorV1(name, device, unique_id)
- else:
- raise NoEntitySpecifiedError(f"Not support for entity {unique_id}")
-
- async_add_entities([entity], update_before_add=True)
-
-
-class AirMonitorB1(AirQualityEntity):
+class AirMonitorB1(XiaomiMiioEntity, AirQualityEntity):
"""Air Quality class for Xiaomi cgllc.airmonitor.b1 device."""
- def __init__(self, name, device, unique_id):
+ def __init__(self, name, device, entry, unique_id):
"""Initialize the entity."""
- self._name = name
- self._device = device
- self._unique_id = unique_id
+ super().__init__(name, device, entry, unique_id)
+
self._icon = "mdi:cloud"
self._available = None
self._air_quality_index = None
@@ -112,11 +78,6 @@ async def async_update(self):
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
- @property
- def name(self):
- """Return the name of this entity, if any."""
- return self._name
-
@property
def icon(self):
"""Return the icon to use for device if any."""
@@ -127,11 +88,6 @@ def available(self):
"""Return true when state is known."""
return self._available
- @property
- def unique_id(self):
- """Return the unique ID."""
- return self._unique_id
-
@property
def air_quality_index(self):
"""Return the Air Quality Index (AQI)."""
@@ -168,7 +124,7 @@ def humidity(self):
return self._humidity
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
data = {}
@@ -219,3 +175,119 @@ async def async_update(self):
def unit_of_measurement(self):
"""Return the unit of measurement."""
return None
+
+
+class AirMonitorCGDN1(XiaomiMiioEntity, AirQualityEntity):
+ """Air Quality class for cgllc.airm.cgdn1 device."""
+
+ def __init__(self, name, device, entry, unique_id):
+ """Initialize the entity."""
+ super().__init__(name, device, entry, unique_id)
+
+ self._icon = "mdi:cloud"
+ self._available = None
+ self._carbon_dioxide = None
+ self._particulate_matter_2_5 = None
+ self._particulate_matter_10 = None
+
+ async def async_update(self):
+ """Fetch state from the miio device."""
+ try:
+ state = await self.hass.async_add_executor_job(self._device.status)
+ _LOGGER.debug("Got new state: %s", state)
+ self._carbon_dioxide = state.co2
+ self._particulate_matter_2_5 = round(state.pm25, 1)
+ self._particulate_matter_10 = round(state.pm10, 1)
+ self._available = True
+ except DeviceException as ex:
+ self._available = False
+ _LOGGER.error("Got exception while fetching the state: %s", ex)
+
+ @property
+ def icon(self):
+ """Return the icon to use for device if any."""
+ return self._icon
+
+ @property
+ def available(self):
+ """Return true when state is known."""
+ return self._available
+
+ @property
+ def carbon_dioxide(self):
+ """Return the CO2 (carbon dioxide) level."""
+ return self._carbon_dioxide
+
+ @property
+ def particulate_matter_2_5(self):
+ """Return the particulate matter 2.5 level."""
+ return self._particulate_matter_2_5
+
+ @property
+ def particulate_matter_10(self):
+ """Return the particulate matter 10 level."""
+ return self._particulate_matter_10
+
+
+DEVICE_MAP = {
+ MODEL_AIRQUALITYMONITOR_S1: {
+ "device_class": AirQualityMonitor,
+ "entity_class": AirMonitorS1,
+ },
+ MODEL_AIRQUALITYMONITOR_B1: {
+ "device_class": AirQualityMonitor,
+ "entity_class": AirMonitorB1,
+ },
+ MODEL_AIRQUALITYMONITOR_V1: {
+ "device_class": AirQualityMonitor,
+ "entity_class": AirMonitorV1,
+ },
+ MODEL_AIRQUALITYMONITOR_CGDN1: {
+ "device_class": lambda host, token, model: AirQualityMonitorCGDN1(host, token),
+ "entity_class": AirMonitorCGDN1,
+ },
+}
+
+
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Import Miio configuration from YAML."""
+ _LOGGER.warning(
+ "Loading Xiaomi Miio Air Quality via platform setup is deprecated. "
+ "Please remove it from your configuration"
+ )
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data=config,
+ )
+ )
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the Xiaomi Air Quality from a config entry."""
+ entities = []
+
+ if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
+ host = config_entry.data[CONF_HOST]
+ token = config_entry.data[CONF_TOKEN]
+ name = config_entry.title
+ model = config_entry.data[CONF_MODEL]
+ unique_id = config_entry.unique_id
+
+ _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
+
+ if model in DEVICE_MAP:
+ device_entry = DEVICE_MAP[model]
+ entities.append(
+ device_entry["entity_class"](
+ name,
+ device_entry["device_class"](host, token, model=model),
+ config_entry,
+ unique_id,
+ )
+ )
+ else:
+ _LOGGER.warning("AirQualityMonitor model '%s' is not yet supported", model)
+
+ async_add_entities(entities, update_before_add=True)
diff --git a/homeassistant/components/xiaomi_miio/alarm_control_panel.py b/homeassistant/components/xiaomi_miio/alarm_control_panel.py
index 6880202cbd62e0..26421770771f3d 100644
--- a/homeassistant/components/xiaomi_miio/alarm_control_panel.py
+++ b/homeassistant/components/xiaomi_miio/alarm_control_panel.py
@@ -15,7 +15,7 @@
STATE_ALARM_DISARMED,
)
-from .const import DOMAIN
+from .const import CONF_GATEWAY, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -27,7 +27,7 @@
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Xiaomi Gateway Alarm from a config entry."""
entities = []
- gateway = hass.data[DOMAIN][config_entry.entry_id]
+ gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY]
entity = XiaomiGatewayAlarm(
gateway,
f"{config_entry.title} Alarm",
diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py
index 6ebb50cd7ce34d..9320972abcbb14 100644
--- a/homeassistant/components/xiaomi_miio/config_flow.py
+++ b/homeassistant/components/xiaomi_miio/config_flow.py
@@ -1,5 +1,6 @@
"""Config flow to configure Xiaomi Miio."""
import logging
+from re import search
import voluptuous as vol
@@ -7,25 +8,28 @@
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
from homeassistant.helpers.device_registry import format_mac
-# pylint: disable=unused-import
-from .const import DOMAIN
-from .gateway import ConnectXiaomiGateway
+from .const import (
+ CONF_DEVICE,
+ CONF_FLOW_TYPE,
+ CONF_GATEWAY,
+ CONF_MAC,
+ CONF_MODEL,
+ DOMAIN,
+ MODELS_ALL,
+ MODELS_ALL_DEVICES,
+ MODELS_GATEWAY,
+)
+from .device import ConnectXiaomiDevice
_LOGGER = logging.getLogger(__name__)
-CONF_FLOW_TYPE = "config_flow_device"
-CONF_GATEWAY = "gateway"
DEFAULT_GATEWAY_NAME = "Xiaomi Gateway"
-ZEROCONF_GATEWAY = "lumi-gateway"
-ZEROCONF_ACPARTNER = "lumi-acpartner"
-GATEWAY_SETTINGS = {
+DEVICE_SETTINGS = {
vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)),
- vol.Optional(CONF_NAME, default=DEFAULT_GATEWAY_NAME): str,
}
-GATEWAY_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(GATEWAY_SETTINGS)
-
-CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_GATEWAY, default=False): bool})
+DEVICE_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(DEVICE_SETTINGS)
+DEVICE_MODEL_CONFIG = {vol.Optional(CONF_MODEL): vol.In(MODELS_ALL)}
class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@@ -37,42 +41,58 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self):
"""Initialize."""
self.host = None
+ self.mac = None
+
+ async def async_step_import(self, conf: dict):
+ """Import a configuration from config.yaml."""
+ host = conf[CONF_HOST]
+ self.context.update({"title_placeholders": {"name": f"YAML import {host}"}})
+ return await self.async_step_device(user_input=conf)
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
- errors = {}
- if user_input is not None:
- # Check which device needs to be connected.
- if user_input[CONF_GATEWAY]:
- return await self.async_step_gateway()
-
- errors["base"] = "no_device_selected"
-
- return self.async_show_form(
- step_id="user", data_schema=CONFIG_SCHEMA, errors=errors
- )
+ return await self.async_step_device()
async def async_step_zeroconf(self, discovery_info):
"""Handle zeroconf discovery."""
name = discovery_info.get("name")
self.host = discovery_info.get("host")
- mac_address = discovery_info.get("properties", {}).get("mac")
-
- if not name or not self.host or not mac_address:
+ self.mac = discovery_info.get("properties", {}).get("mac")
+ if self.mac is None:
+ poch = discovery_info.get("properties", {}).get("poch", "")
+ result = search(r"mac=\w+", poch)
+ if result is not None:
+ self.mac = result.group(0).split("=")[1]
+
+ if not name or not self.host or not self.mac:
return self.async_abort(reason="not_xiaomi_miio")
+ self.mac = format_mac(self.mac)
+
# Check which device is discovered.
- if name.startswith(ZEROCONF_GATEWAY) or name.startswith(ZEROCONF_ACPARTNER):
- unique_id = format_mac(mac_address)
- await self.async_set_unique_id(unique_id)
- self._abort_if_unique_id_configured({CONF_HOST: self.host})
+ for gateway_model in MODELS_GATEWAY:
+ if name.startswith(gateway_model.replace(".", "-")):
+ unique_id = self.mac
+ await self.async_set_unique_id(unique_id)
+ self._abort_if_unique_id_configured({CONF_HOST: self.host})
+
+ self.context.update(
+ {"title_placeholders": {"name": f"Gateway {self.host}"}}
+ )
+
+ return await self.async_step_device()
+
+ for device_model in MODELS_ALL_DEVICES:
+ if name.startswith(device_model.replace(".", "-")):
+ unique_id = self.mac
+ await self.async_set_unique_id(unique_id)
+ self._abort_if_unique_id_configured({CONF_HOST: self.host})
- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
- self.context.update(
- {"title_placeholders": {"name": f"Gateway {self.host}"}}
- )
+ self.context.update(
+ {"title_placeholders": {"name": f"{device_model} {self.host}"}}
+ )
- return await self.async_step_gateway()
+ return await self.async_step_device()
# Discovered device is not yet supported
_LOGGER.debug(
@@ -82,42 +102,76 @@ async def async_step_zeroconf(self, discovery_info):
)
return self.async_abort(reason="not_xiaomi_miio")
- async def async_step_gateway(self, user_input=None):
- """Handle a flow initialized by the user to configure a gateway."""
+ async def async_step_device(self, user_input=None):
+ """Handle a flow initialized by the user to configure a xiaomi miio device."""
errors = {}
if user_input is not None:
token = user_input[CONF_TOKEN]
+ model = user_input.get(CONF_MODEL)
if user_input.get(CONF_HOST):
self.host = user_input[CONF_HOST]
- # Try to connect to a Xiaomi Gateway.
- connect_gateway_class = ConnectXiaomiGateway(self.hass)
- await connect_gateway_class.async_connect_gateway(self.host, token)
- gateway_info = connect_gateway_class.gateway_info
-
- if gateway_info is not None:
- mac = format_mac(gateway_info.mac_address)
- unique_id = mac
- await self.async_set_unique_id(unique_id)
- self._abort_if_unique_id_configured()
- return self.async_create_entry(
- title=user_input[CONF_NAME],
- data={
- CONF_FLOW_TYPE: CONF_GATEWAY,
- CONF_HOST: self.host,
- CONF_TOKEN: token,
- "model": gateway_info.model,
- "mac": mac,
- },
- )
-
- errors["base"] = "cannot_connect"
+ # Try to connect to a Xiaomi Device.
+ connect_device_class = ConnectXiaomiDevice(self.hass)
+ await connect_device_class.async_connect_device(self.host, token)
+ device_info = connect_device_class.device_info
+
+ if model is None and device_info is not None:
+ model = device_info.model
+
+ if model is not None:
+ if self.mac is None and device_info is not None:
+ self.mac = format_mac(device_info.mac_address)
+
+ # Setup Gateways
+ for gateway_model in MODELS_GATEWAY:
+ if model.startswith(gateway_model):
+ unique_id = self.mac
+ await self.async_set_unique_id(
+ unique_id, raise_on_progress=False
+ )
+ self._abort_if_unique_id_configured()
+ return self.async_create_entry(
+ title=DEFAULT_GATEWAY_NAME,
+ data={
+ CONF_FLOW_TYPE: CONF_GATEWAY,
+ CONF_HOST: self.host,
+ CONF_TOKEN: token,
+ CONF_MODEL: model,
+ CONF_MAC: self.mac,
+ },
+ )
+
+ # Setup all other Miio Devices
+ name = user_input.get(CONF_NAME, model)
+
+ for device_model in MODELS_ALL_DEVICES:
+ if model.startswith(device_model):
+ unique_id = self.mac
+ await self.async_set_unique_id(
+ unique_id, raise_on_progress=False
+ )
+ self._abort_if_unique_id_configured()
+ return self.async_create_entry(
+ title=name,
+ data={
+ CONF_FLOW_TYPE: CONF_DEVICE,
+ CONF_HOST: self.host,
+ CONF_TOKEN: token,
+ CONF_MODEL: model,
+ CONF_MAC: self.mac,
+ },
+ )
+ errors["base"] = "unknown_device"
+ else:
+ errors["base"] = "cannot_connect"
if self.host:
- schema = vol.Schema(GATEWAY_SETTINGS)
+ schema = vol.Schema(DEVICE_SETTINGS)
else:
- schema = GATEWAY_CONFIG
+ schema = DEVICE_CONFIG
- return self.async_show_form(
- step_id="gateway", data_schema=schema, errors=errors
- )
+ if errors:
+ schema = schema.extend(DEVICE_MODEL_CONFIG)
+
+ return self.async_show_form(step_id="device", data_schema=schema, errors=errors)
diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py
index 8de68cda97f602..35c4d4a1662070 100644
--- a/homeassistant/components/xiaomi_miio/const.py
+++ b/homeassistant/components/xiaomi_miio/const.py
@@ -1,6 +1,122 @@
"""Constants for the Xiaomi Miio component."""
DOMAIN = "xiaomi_miio"
+CONF_FLOW_TYPE = "config_flow_device"
+CONF_GATEWAY = "gateway"
+CONF_DEVICE = "device"
+CONF_MODEL = "model"
+CONF_MAC = "mac"
+
+KEY_COORDINATOR = "coordinator"
+
+ATTR_AVAILABLE = "available"
+
+# Fan Models
+MODEL_AIRPURIFIER_V1 = "zhimi.airpurifier.v1"
+MODEL_AIRPURIFIER_V2 = "zhimi.airpurifier.v2"
+MODEL_AIRPURIFIER_V3 = "zhimi.airpurifier.v3"
+MODEL_AIRPURIFIER_V5 = "zhimi.airpurifier.v5"
+MODEL_AIRPURIFIER_PRO = "zhimi.airpurifier.v6"
+MODEL_AIRPURIFIER_PRO_V7 = "zhimi.airpurifier.v7"
+MODEL_AIRPURIFIER_M1 = "zhimi.airpurifier.m1"
+MODEL_AIRPURIFIER_M2 = "zhimi.airpurifier.m2"
+MODEL_AIRPURIFIER_MA1 = "zhimi.airpurifier.ma1"
+MODEL_AIRPURIFIER_MA2 = "zhimi.airpurifier.ma2"
+MODEL_AIRPURIFIER_SA1 = "zhimi.airpurifier.sa1"
+MODEL_AIRPURIFIER_SA2 = "zhimi.airpurifier.sa2"
+MODEL_AIRPURIFIER_2S = "zhimi.airpurifier.mc1"
+MODEL_AIRPURIFIER_2H = "zhimi.airpurifier.mc2"
+MODEL_AIRPURIFIER_3 = "zhimi.airpurifier.ma4"
+MODEL_AIRPURIFIER_3H = "zhimi.airpurifier.mb3"
+MODEL_AIRPURIFIER_PROH = "zhimi.airpurifier.va1"
+
+MODEL_AIRHUMIDIFIER_V1 = "zhimi.humidifier.v1"
+MODEL_AIRHUMIDIFIER_CA1 = "zhimi.humidifier.ca1"
+MODEL_AIRHUMIDIFIER_CA4 = "zhimi.humidifier.ca4"
+MODEL_AIRHUMIDIFIER_CB1 = "zhimi.humidifier.cb1"
+
+MODEL_AIRFRESH_VA2 = "zhimi.airfresh.va2"
+
+MODELS_PURIFIER_MIOT = [
+ MODEL_AIRPURIFIER_3,
+ MODEL_AIRPURIFIER_3H,
+ MODEL_AIRPURIFIER_PROH,
+]
+MODELS_HUMIDIFIER_MIOT = [MODEL_AIRHUMIDIFIER_CA4]
+MODELS_FAN_MIIO = [
+ MODEL_AIRPURIFIER_V1,
+ MODEL_AIRPURIFIER_V2,
+ MODEL_AIRPURIFIER_V3,
+ MODEL_AIRPURIFIER_V5,
+ MODEL_AIRPURIFIER_PRO,
+ MODEL_AIRPURIFIER_PRO_V7,
+ MODEL_AIRPURIFIER_M1,
+ MODEL_AIRPURIFIER_M2,
+ MODEL_AIRPURIFIER_MA1,
+ MODEL_AIRPURIFIER_MA2,
+ MODEL_AIRPURIFIER_SA1,
+ MODEL_AIRPURIFIER_SA2,
+ MODEL_AIRPURIFIER_2S,
+ MODEL_AIRPURIFIER_2H,
+ MODEL_AIRHUMIDIFIER_V1,
+ MODEL_AIRHUMIDIFIER_CA1,
+ MODEL_AIRHUMIDIFIER_CB1,
+ MODEL_AIRFRESH_VA2,
+]
+
+# AirQuality Models
+MODEL_AIRQUALITYMONITOR_V1 = "zhimi.airmonitor.v1"
+MODEL_AIRQUALITYMONITOR_B1 = "cgllc.airmonitor.b1"
+MODEL_AIRQUALITYMONITOR_S1 = "cgllc.airmonitor.s1"
+MODEL_AIRQUALITYMONITOR_CGDN1 = "cgllc.airm.cgdn1"
+
+# Light Models
+MODELS_LIGHT_EYECARE = ["philips.light.sread1"]
+MODELS_LIGHT_CEILING = ["philips.light.ceiling", "philips.light.zyceiling"]
+MODELS_LIGHT_MOON = ["philips.light.moonlight"]
+MODELS_LIGHT_BULB = [
+ "philips.light.bulb",
+ "philips.light.candle",
+ "philips.light.candle2",
+ "philips.light.downlight",
+]
+MODELS_LIGHT_MONO = ["philips.light.mono1"]
+
+# Model lists
+MODELS_GATEWAY = ["lumi.gateway", "lumi.acpartner"]
+MODELS_SWITCH = [
+ "chuangmi.plug.v1",
+ "chuangmi.plug.v3",
+ "chuangmi.plug.hmi208",
+ "qmi.powerstrip.v1",
+ "zimi.powerstrip.v2",
+ "chuangmi.plug.m1",
+ "chuangmi.plug.m3",
+ "chuangmi.plug.v2",
+ "chuangmi.plug.hmi205",
+ "chuangmi.plug.hmi206",
+]
+MODELS_FAN = MODELS_FAN_MIIO + MODELS_HUMIDIFIER_MIOT + MODELS_PURIFIER_MIOT
+MODELS_LIGHT = (
+ MODELS_LIGHT_EYECARE
+ + MODELS_LIGHT_CEILING
+ + MODELS_LIGHT_MOON
+ + MODELS_LIGHT_BULB
+ + MODELS_LIGHT_MONO
+)
+MODELS_VACUUM = ["roborock.vacuum", "rockrobo.vacuum"]
+MODELS_AIR_MONITOR = [
+ MODEL_AIRQUALITYMONITOR_V1,
+ MODEL_AIRQUALITYMONITOR_B1,
+ MODEL_AIRQUALITYMONITOR_S1,
+ MODEL_AIRQUALITYMONITOR_CGDN1,
+]
+
+MODELS_ALL_DEVICES = (
+ MODELS_SWITCH + MODELS_VACUUM + MODELS_AIR_MONITOR + MODELS_FAN + MODELS_LIGHT
+)
+MODELS_ALL = MODELS_ALL_DEVICES + MODELS_GATEWAY
+
# Fan Services
SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on"
SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off"
@@ -52,8 +168,3 @@
SERVICE_CLEAN_SEGMENT = "vacuum_clean_segment"
SERVICE_CLEAN_ZONE = "vacuum_clean_zone"
SERVICE_GOTO = "vacuum_goto"
-
-# AirQuality Model
-MODEL_AIRQUALITYMONITOR_V1 = "zhimi.airmonitor.v1"
-MODEL_AIRQUALITYMONITOR_B1 = "cgllc.airmonitor.b1"
-MODEL_AIRQUALITYMONITOR_S1 = "cgllc.airmonitor.s1"
diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py
new file mode 100644
index 00000000000000..cb91726ecadd3a
--- /dev/null
+++ b/homeassistant/components/xiaomi_miio/device.py
@@ -0,0 +1,91 @@
+"""Code to handle a Xiaomi Device."""
+import logging
+
+from miio import Device, DeviceException
+
+from homeassistant.helpers import device_registry as dr
+from homeassistant.helpers.entity import Entity
+
+from .const import CONF_MAC, CONF_MODEL, DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class ConnectXiaomiDevice:
+ """Class to async connect to a Xiaomi Device."""
+
+ def __init__(self, hass):
+ """Initialize the entity."""
+ self._hass = hass
+ self._device = None
+ self._device_info = None
+
+ @property
+ def device(self):
+ """Return the class containing all connections to the device."""
+ return self._device
+
+ @property
+ def device_info(self):
+ """Return the class containing device info."""
+ return self._device_info
+
+ async def async_connect_device(self, host, token):
+ """Connect to the Xiaomi Device."""
+ _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
+ try:
+ self._device = Device(host, token)
+ # get the device info
+ self._device_info = await self._hass.async_add_executor_job(
+ self._device.info
+ )
+ except DeviceException:
+ _LOGGER.error(
+ "DeviceException during setup of xiaomi device with host %s", host
+ )
+ return False
+ _LOGGER.debug(
+ "%s %s %s detected",
+ self._device_info.model,
+ self._device_info.firmware_version,
+ self._device_info.hardware_version,
+ )
+ return True
+
+
+class XiaomiMiioEntity(Entity):
+ """Representation of a base Xiaomi Miio Entity."""
+
+ def __init__(self, name, device, entry, unique_id):
+ """Initialize the Xiaomi Miio Device."""
+ self._device = device
+ self._model = entry.data[CONF_MODEL]
+ self._mac = entry.data[CONF_MAC]
+ self._device_id = entry.unique_id
+ self._unique_id = unique_id
+ self._name = name
+
+ @property
+ def unique_id(self):
+ """Return an unique ID."""
+ return self._unique_id
+
+ @property
+ def name(self):
+ """Return the name of this entity, if any."""
+ return self._name
+
+ @property
+ def device_info(self):
+ """Return the device info."""
+ device_info = {
+ "identifiers": {(DOMAIN, self._device_id)},
+ "manufacturer": "Xiaomi",
+ "name": self._name,
+ "model": self._model,
+ }
+
+ if self._mac is not None:
+ device_info["connections"] = {(dr.CONNECTION_NETWORK_MAC, self._mac)}
+
+ return device_info
diff --git a/homeassistant/components/xiaomi_miio/device_tracker.py b/homeassistant/components/xiaomi_miio/device_tracker.py
index ef527d0aa407a2..a6c1c7e5a28d61 100644
--- a/homeassistant/components/xiaomi_miio/device_tracker.py
+++ b/homeassistant/components/xiaomi_miio/device_tracker.py
@@ -1,7 +1,7 @@
"""Support for Xiaomi Mi WiFi Repeater 2."""
import logging
-from miio import DeviceException, WifiRepeater # pylint: disable=import-error
+from miio import DeviceException, WifiRepeater
import voluptuous as vol
from homeassistant.components.device_tracker import (
diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py
index 0d07654e61b188..6d18131cdeb0be 100644
--- a/homeassistant/components/xiaomi_miio/fan.py
+++ b/homeassistant/components/xiaomi_miio/fan.py
@@ -4,33 +4,32 @@
from functools import partial
import logging
-from miio import ( # pylint: disable=import-error
+from miio import (
AirFresh,
AirHumidifier,
AirHumidifierMiot,
AirPurifier,
AirPurifierMiot,
- Device,
DeviceException,
)
-from miio.airfresh import ( # pylint: disable=import-error, import-error
+from miio.airfresh import (
LedBrightness as AirfreshLedBrightness,
OperationMode as AirfreshOperationMode,
)
-from miio.airhumidifier import ( # pylint: disable=import-error, import-error
+from miio.airhumidifier import (
LedBrightness as AirhumidifierLedBrightness,
OperationMode as AirhumidifierOperationMode,
)
-from miio.airhumidifier_miot import ( # pylint: disable=import-error, import-error
+from miio.airhumidifier_miot import (
LedBrightness as AirhumidifierMiotLedBrightness,
OperationMode as AirhumidifierMiotOperationMode,
PressedButton as AirhumidifierPressedButton,
)
-from miio.airpurifier import ( # pylint: disable=import-error, import-error
+from miio.airpurifier import (
LedBrightness as AirpurifierLedBrightness,
OperationMode as AirpurifierOperationMode,
)
-from miio.airpurifier_miot import ( # pylint: disable=import-error, import-error
+from miio.airpurifier_miot import (
LedBrightness as AirpurifierMiotLedBrightness,
OperationMode as AirpurifierMiotOperationMode,
)
@@ -44,18 +43,32 @@
SUPPORT_SET_SPEED,
FanEntity,
)
+from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_MODE,
+ ATTR_TEMPERATURE,
CONF_HOST,
CONF_NAME,
CONF_TOKEN,
)
-from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from .const import (
+ CONF_DEVICE,
+ CONF_FLOW_TYPE,
DOMAIN,
+ MODEL_AIRHUMIDIFIER_CA1,
+ MODEL_AIRHUMIDIFIER_CA4,
+ MODEL_AIRHUMIDIFIER_CB1,
+ MODEL_AIRPURIFIER_2H,
+ MODEL_AIRPURIFIER_2S,
+ MODEL_AIRPURIFIER_PRO,
+ MODEL_AIRPURIFIER_PRO_V7,
+ MODEL_AIRPURIFIER_V3,
+ MODELS_FAN,
+ MODELS_HUMIDIFIER_MIOT,
+ MODELS_PURIFIER_MIOT,
SERVICE_RESET_FILTER,
SERVICE_SET_AUTO_DETECT_OFF,
SERVICE_SET_AUTO_DETECT_ON,
@@ -77,6 +90,7 @@
SERVICE_SET_TARGET_HUMIDITY,
SERVICE_SET_VOLUME,
)
+from .device import XiaomiMiioEntity
_LOGGER = logging.getLogger(__name__)
@@ -84,65 +98,20 @@
DATA_KEY = "fan.xiaomi_miio"
CONF_MODEL = "model"
-MODEL_AIRPURIFIER_V1 = "zhimi.airpurifier.v1"
-MODEL_AIRPURIFIER_V2 = "zhimi.airpurifier.v2"
-MODEL_AIRPURIFIER_V3 = "zhimi.airpurifier.v3"
-MODEL_AIRPURIFIER_V5 = "zhimi.airpurifier.v5"
-MODEL_AIRPURIFIER_PRO = "zhimi.airpurifier.v6"
-MODEL_AIRPURIFIER_PRO_V7 = "zhimi.airpurifier.v7"
-MODEL_AIRPURIFIER_M1 = "zhimi.airpurifier.m1"
-MODEL_AIRPURIFIER_M2 = "zhimi.airpurifier.m2"
-MODEL_AIRPURIFIER_MA1 = "zhimi.airpurifier.ma1"
-MODEL_AIRPURIFIER_MA2 = "zhimi.airpurifier.ma2"
-MODEL_AIRPURIFIER_SA1 = "zhimi.airpurifier.sa1"
-MODEL_AIRPURIFIER_SA2 = "zhimi.airpurifier.sa2"
-MODEL_AIRPURIFIER_2S = "zhimi.airpurifier.mc1"
-MODEL_AIRPURIFIER_3 = "zhimi.airpurifier.ma4"
-MODEL_AIRPURIFIER_3H = "zhimi.airpurifier.mb3"
-
-MODEL_AIRHUMIDIFIER_V1 = "zhimi.humidifier.v1"
-MODEL_AIRHUMIDIFIER_CA1 = "zhimi.humidifier.ca1"
-MODEL_AIRHUMIDIFIER_CA4 = "zhimi.humidifier.ca4"
-MODEL_AIRHUMIDIFIER_CB1 = "zhimi.humidifier.cb1"
-
-MODEL_AIRFRESH_VA2 = "zhimi.airfresh.va2"
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_MODEL): vol.In(
- [
- MODEL_AIRPURIFIER_V1,
- MODEL_AIRPURIFIER_V2,
- MODEL_AIRPURIFIER_V3,
- MODEL_AIRPURIFIER_V5,
- MODEL_AIRPURIFIER_PRO,
- MODEL_AIRPURIFIER_PRO_V7,
- MODEL_AIRPURIFIER_M1,
- MODEL_AIRPURIFIER_M2,
- MODEL_AIRPURIFIER_MA1,
- MODEL_AIRPURIFIER_MA2,
- MODEL_AIRPURIFIER_SA1,
- MODEL_AIRPURIFIER_SA2,
- MODEL_AIRPURIFIER_2S,
- MODEL_AIRPURIFIER_3,
- MODEL_AIRPURIFIER_3H,
- MODEL_AIRHUMIDIFIER_V1,
- MODEL_AIRHUMIDIFIER_CA1,
- MODEL_AIRHUMIDIFIER_CA4,
- MODEL_AIRHUMIDIFIER_CB1,
- MODEL_AIRFRESH_VA2,
- ]
- ),
+ vol.Optional(CONF_MODEL): vol.In(MODELS_FAN),
}
)
ATTR_MODEL = "model"
# Air Purifier
-ATTR_TEMPERATURE = "temperature"
ATTR_HUMIDITY = "humidity"
ATTR_AIR_QUALITY_INDEX = "aqi"
ATTR_FILTER_HOURS_USED = "filter_hours_used"
@@ -193,9 +162,6 @@
# Air Fresh
ATTR_CO2 = "co2"
-PURIFIER_MIOT = [MODEL_AIRPURIFIER_3, MODEL_AIRPURIFIER_3H]
-HUMIDIFIER_MIOT = [MODEL_AIRHUMIDIFIER_CA4]
-
# Map attributes to properties of the state object
AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = {
ATTR_TEMPERATURE: "temperature",
@@ -553,104 +519,114 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the miio fan device from config."""
- if DATA_KEY not in hass.data:
- hass.data[DATA_KEY] = {}
-
- host = config[CONF_HOST]
- token = config[CONF_TOKEN]
- name = config[CONF_NAME]
- model = config.get(CONF_MODEL)
-
- _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
- unique_id = None
-
- if model is None:
- try:
- miio_device = Device(host, token)
- device_info = await hass.async_add_executor_job(miio_device.info)
- model = device_info.model
- unique_id = f"{model}-{device_info.mac_address}"
- _LOGGER.info(
- "%s %s %s detected",
- model,
- device_info.firmware_version,
- device_info.hardware_version,
- )
- except DeviceException as ex:
- raise PlatformNotReady from ex
-
- if model in PURIFIER_MIOT:
- air_purifier = AirPurifierMiot(host, token)
- device = XiaomiAirPurifierMiot(name, air_purifier, model, unique_id)
- elif model.startswith("zhimi.airpurifier."):
- air_purifier = AirPurifier(host, token)
- device = XiaomiAirPurifier(name, air_purifier, model, unique_id)
- elif model in HUMIDIFIER_MIOT:
- air_humidifier = AirHumidifierMiot(host, token)
- device = XiaomiAirHumidifierMiot(name, air_humidifier, model, unique_id)
- elif model.startswith("zhimi.humidifier."):
- air_humidifier = AirHumidifier(host, token, model=model)
- device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id)
- elif model.startswith("zhimi.airfresh."):
- air_fresh = AirFresh(host, token)
- device = XiaomiAirFresh(name, air_fresh, model, unique_id)
- else:
- _LOGGER.error(
- "Unsupported device found! Please create an issue at "
- "https://github.com/syssi/xiaomi_airpurifier/issues "
- "and provide the following data: %s",
- model,
+ """Import Miio configuration from YAML."""
+ _LOGGER.warning(
+ "Loading Xiaomi Miio Fan via platform setup is deprecated. "
+ "Please remove it from your configuration"
+ )
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data=config,
)
- return False
-
- hass.data[DATA_KEY][host] = device
- async_add_entities([device], update_before_add=True)
-
- async def async_service_handler(service):
- """Map services to methods on XiaomiAirPurifier."""
- method = SERVICE_TO_METHOD.get(service.service)
- params = {
- key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID
- }
- entity_ids = service.data.get(ATTR_ENTITY_ID)
- if entity_ids:
- devices = [
- device
- for device in hass.data[DATA_KEY].values()
- if device.entity_id in entity_ids
- ]
+ )
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the Fan from a config entry."""
+ entities = []
+
+ if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
+ if DATA_KEY not in hass.data:
+ hass.data[DATA_KEY] = {}
+
+ host = config_entry.data[CONF_HOST]
+ token = config_entry.data[CONF_TOKEN]
+ name = config_entry.title
+ model = config_entry.data[CONF_MODEL]
+ unique_id = config_entry.unique_id
+
+ _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
+
+ if model in MODELS_PURIFIER_MIOT:
+ air_purifier = AirPurifierMiot(host, token)
+ entity = XiaomiAirPurifierMiot(name, air_purifier, config_entry, unique_id)
+ elif model.startswith("zhimi.airpurifier."):
+ air_purifier = AirPurifier(host, token)
+ entity = XiaomiAirPurifier(name, air_purifier, config_entry, unique_id)
+ elif model in MODELS_HUMIDIFIER_MIOT:
+ air_humidifier = AirHumidifierMiot(host, token)
+ entity = XiaomiAirHumidifierMiot(
+ name, air_humidifier, config_entry, unique_id
+ )
+ elif model.startswith("zhimi.humidifier."):
+ air_humidifier = AirHumidifier(host, token, model=model)
+ entity = XiaomiAirHumidifier(name, air_humidifier, config_entry, unique_id)
+ elif model.startswith("zhimi.airfresh."):
+ air_fresh = AirFresh(host, token)
+ entity = XiaomiAirFresh(name, air_fresh, config_entry, unique_id)
else:
- devices = hass.data[DATA_KEY].values()
-
- update_tasks = []
- for device in devices:
- if not hasattr(device, method["method"]):
- continue
- await getattr(device, method["method"])(**params)
- update_tasks.append(device.async_update_ha_state(True))
+ _LOGGER.error(
+ "Unsupported device found! Please create an issue at "
+ "https://github.com/syssi/xiaomi_airpurifier/issues "
+ "and provide the following data: %s",
+ model,
+ )
+ return
- if update_tasks:
- await asyncio.wait(update_tasks)
+ hass.data[DATA_KEY][host] = entity
+ entities.append(entity)
+
+ async def async_service_handler(service):
+ """Map services to methods on XiaomiAirPurifier."""
+ method = SERVICE_TO_METHOD[service.service]
+ params = {
+ key: value
+ for key, value in service.data.items()
+ if key != ATTR_ENTITY_ID
+ }
+ entity_ids = service.data.get(ATTR_ENTITY_ID)
+ if entity_ids:
+ entities = [
+ entity
+ for entity in hass.data[DATA_KEY].values()
+ if entity.entity_id in entity_ids
+ ]
+ else:
+ entities = hass.data[DATA_KEY].values()
+
+ update_tasks = []
+
+ for entity in entities:
+ entity_method = getattr(entity, method["method"], None)
+ if not entity_method:
+ continue
+ await entity_method(**params)
+ update_tasks.append(
+ hass.async_create_task(entity.async_update_ha_state(True))
+ )
+
+ if update_tasks:
+ await asyncio.wait(update_tasks)
+
+ for air_purifier_service in SERVICE_TO_METHOD:
+ schema = SERVICE_TO_METHOD[air_purifier_service].get(
+ "schema", AIRPURIFIER_SERVICE_SCHEMA
+ )
+ hass.services.async_register(
+ DOMAIN, air_purifier_service, async_service_handler, schema=schema
+ )
- for air_purifier_service in SERVICE_TO_METHOD:
- schema = SERVICE_TO_METHOD[air_purifier_service].get(
- "schema", AIRPURIFIER_SERVICE_SCHEMA
- )
- hass.services.async_register(
- DOMAIN, air_purifier_service, async_service_handler, schema=schema
- )
+ async_add_entities(entities, update_before_add=True)
-class XiaomiGenericDevice(FanEntity):
+class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity):
"""Representation of a generic Xiaomi device."""
- def __init__(self, name, device, model, unique_id):
+ def __init__(self, name, device, entry, unique_id):
"""Initialize the generic Xiaomi device."""
- self._name = name
- self._device = device
- self._model = model
- self._unique_id = unique_id
+ super().__init__(name, device, entry, unique_id)
self._available = False
self._state = None
@@ -668,23 +644,13 @@ def should_poll(self):
"""Poll the device."""
return True
- @property
- def unique_id(self):
- """Return an unique ID."""
- return self._unique_id
-
- @property
- def name(self):
- """Return the name of the device if any."""
- return self._name
-
@property
def available(self):
"""Return true when state is known."""
return self._available
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
return self._state_attrs
@@ -803,9 +769,9 @@ async def async_set_child_lock_off(self):
class XiaomiAirPurifier(XiaomiGenericDevice):
"""Representation of a Xiaomi Air Purifier."""
- def __init__(self, name, device, model, unique_id):
+ def __init__(self, name, device, entry, unique_id):
"""Initialize the plug switch."""
- super().__init__(name, device, model, unique_id)
+ super().__init__(name, device, entry, unique_id)
if self._model == MODEL_AIRPURIFIER_PRO:
self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO
@@ -815,11 +781,11 @@ def __init__(self, name, device, model, unique_id):
self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO_V7
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7
self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO_V7
- elif self._model == MODEL_AIRPURIFIER_2S:
+ elif self._model in [MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_2H]:
self._device_features = FEATURE_FLAGS_AIRPURIFIER_2S
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S
self._speed_list = OPERATION_MODES_AIRPURIFIER_2S
- elif self._model == MODEL_AIRPURIFIER_3 or self._model == MODEL_AIRPURIFIER_3H:
+ elif self._model in MODELS_PURIFIER_MIOT:
self._device_features = FEATURE_FLAGS_AIRPURIFIER_3
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_3
self._speed_list = OPERATION_MODES_AIRPURIFIER_3
@@ -1056,9 +1022,9 @@ async def async_set_led_brightness(self, brightness: int = 2):
class XiaomiAirHumidifier(XiaomiGenericDevice):
"""Representation of a Xiaomi Air Humidifier."""
- def __init__(self, name, device, model, unique_id):
+ def __init__(self, name, device, entry, unique_id):
"""Initialize the plug switch."""
- super().__init__(name, device, model, unique_id)
+ super().__init__(name, device, entry, unique_id)
if self._model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]:
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB
@@ -1214,7 +1180,6 @@ def button_pressed(self):
async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
-
await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode,
@@ -1247,9 +1212,9 @@ async def async_set_motor_speed(self, motor_speed: int = 400):
class XiaomiAirFresh(XiaomiGenericDevice):
"""Representation of a Xiaomi Air Fresh."""
- def __init__(self, name, device, model, unique_id):
+ def __init__(self, name, device, entry, unique_id):
"""Initialize the miio device."""
- super().__init__(name, device, model, unique_id)
+ super().__init__(name, device, entry, unique_id)
self._device_features = FEATURE_FLAGS_AIRFRESH
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRFRESH
diff --git a/homeassistant/components/xiaomi_miio/gateway.py b/homeassistant/components/xiaomi_miio/gateway.py
index eb2f4cdf2eb267..be96f77240a09a 100644
--- a/homeassistant/components/xiaomi_miio/gateway.py
+++ b/homeassistant/components/xiaomi_miio/gateway.py
@@ -4,8 +4,9 @@
from miio import DeviceException, gateway
from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from .const import DOMAIN
+from .const import ATTR_AVAILABLE, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -56,16 +57,16 @@ async def async_connect_gateway(self, host, token):
return True
-class XiaomiGatewayDevice(Entity):
+class XiaomiGatewayDevice(CoordinatorEntity, Entity):
"""Representation of a base Xiaomi Gateway Device."""
- def __init__(self, sub_device, entry):
+ def __init__(self, coordinator, sub_device, entry):
"""Initialize the Xiaomi Gateway Device."""
+ super().__init__(coordinator)
self._sub_device = sub_device
self._entry = entry
self._unique_id = sub_device.sid
self._name = f"{sub_device.name} ({sub_device.sid})"
- self._available = False
@property
def unique_id(self):
@@ -91,15 +92,8 @@ def device_info(self):
@property
def available(self):
- """Return true when state is known."""
- return self._available
+ """Return if entity is available."""
+ if self.coordinator.data is None:
+ return False
- async def async_update(self):
- """Fetch state from the sub device."""
- try:
- await self.hass.async_add_executor_job(self._sub_device.update)
- self._available = True
- except gateway.GatewayException as ex:
- if self._available:
- self._available = False
- _LOGGER.error("Got exception while fetching the state: %s", ex)
+ return self.coordinator.data[self._sub_device.sid][ATTR_AVAILABLE]
diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py
index d1746fcd889be9..f6cd468ad00dfb 100644
--- a/homeassistant/components/xiaomi_miio/light.py
+++ b/homeassistant/components/xiaomi_miio/light.py
@@ -6,15 +6,8 @@
import logging
from math import ceil
-from miio import ( # pylint: disable=import-error
- Ceil,
- Device,
- DeviceException,
- PhilipsBulb,
- PhilipsEyecare,
- PhilipsMoonlight,
-)
-from miio.gateway import (
+from miio import Ceil, DeviceException, PhilipsBulb, PhilipsEyecare, PhilipsMoonlight
+from miio.gateway.gateway import (
GATEWAY_MODEL_AC_V1,
GATEWAY_MODEL_AC_V2,
GATEWAY_MODEL_AC_V3,
@@ -32,14 +25,24 @@
SUPPORT_COLOR_TEMP,
LightEntity,
)
+from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN
-from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.util import color, dt
-from .config_flow import CONF_FLOW_TYPE, CONF_GATEWAY
from .const import (
+ CONF_DEVICE,
+ CONF_FLOW_TYPE,
+ CONF_GATEWAY,
+ CONF_MODEL,
DOMAIN,
+ KEY_COORDINATOR,
+ MODELS_LIGHT,
+ MODELS_LIGHT_BULB,
+ MODELS_LIGHT_CEILING,
+ MODELS_LIGHT_EYECARE,
+ MODELS_LIGHT_MONO,
+ MODELS_LIGHT_MOON,
SERVICE_EYECARE_MODE_OFF,
SERVICE_EYECARE_MODE_ON,
SERVICE_NIGHT_LIGHT_MODE_OFF,
@@ -49,32 +52,20 @@
SERVICE_SET_DELAYED_TURN_OFF,
SERVICE_SET_SCENE,
)
+from .device import XiaomiMiioEntity
+from .gateway import XiaomiGatewayDevice
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Xiaomi Philips Light"
DATA_KEY = "light.xiaomi_miio"
-CONF_MODEL = "model"
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_MODEL): vol.In(
- [
- "philips.light.sread1",
- "philips.light.ceiling",
- "philips.light.zyceiling",
- "philips.light.moonlight",
- "philips.light.bulb",
- "philips.light.candle",
- "philips.light.candle2",
- "philips.light.mono1",
- "philips.light.downlight",
- ]
- ),
+ vol.Optional(CONF_MODEL): vol.In(MODELS_LIGHT),
}
)
@@ -86,7 +77,6 @@
DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES = 1
SUCCESS = ["ok"]
-ATTR_MODEL = "model"
ATTR_SCENE = "scene"
ATTR_DELAYED_TURN_OFF = "delayed_turn_off"
ATTR_TIME_PERIOD = "time_period"
@@ -99,8 +89,8 @@
ATTR_SLEEP_ASSISTANT = "sleep_assistant"
ATTR_SLEEP_OFF_TIME = "sleep_off_time"
ATTR_TOTAL_ASSISTANT_SLEEP_TIME = "total_assistant_sleep_time"
-ATTR_BRAND_SLEEP = "brand_sleep"
-ATTR_BRAND = "brand"
+ATTR_BAND_SLEEP = "band_sleep"
+ATTR_BAND = "band"
XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
@@ -130,12 +120,27 @@
}
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Import Miio configuration from YAML."""
+ _LOGGER.warning(
+ "Loading Xiaomi Miio Light via platform setup is deprecated. "
+ "Please remove it from your configuration"
+ )
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data=config,
+ )
+ )
+
+
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Xiaomi light from a config entry."""
entities = []
if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY:
- gateway = hass.data[DOMAIN][config_entry.entry_id]
+ gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY]
# Gateway light
if gateway.model not in [
GATEWAY_MODEL_AC_V1,
@@ -145,148 +150,119 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities.append(
XiaomiGatewayLight(gateway, config_entry.title, config_entry.unique_id)
)
+ # Gateway sub devices
+ sub_devices = gateway.devices
+ coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
+ for sub_device in sub_devices.values():
+ if sub_device.device_type == "LightBulb":
+ entities.append(
+ XiaomiGatewayBulb(coordinator, sub_device, config_entry)
+ )
- async_add_entities(entities, update_before_add=True)
-
+ if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
+ if DATA_KEY not in hass.data:
+ hass.data[DATA_KEY] = {}
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the light from config."""
- if DATA_KEY not in hass.data:
- hass.data[DATA_KEY] = {}
+ host = config_entry.data[CONF_HOST]
+ token = config_entry.data[CONF_TOKEN]
+ name = config_entry.title
+ model = config_entry.data[CONF_MODEL]
+ unique_id = config_entry.unique_id
- host = config[CONF_HOST]
- token = config[CONF_TOKEN]
- name = config[CONF_NAME]
- model = config.get(CONF_MODEL)
+ _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
- _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
+ if model in MODELS_LIGHT_EYECARE:
+ light = PhilipsEyecare(host, token)
+ entity = XiaomiPhilipsEyecareLamp(name, light, config_entry, unique_id)
+ entities.append(entity)
+ hass.data[DATA_KEY][host] = entity
- devices = []
- unique_id = None
-
- if model is None:
- try:
- miio_device = Device(host, token)
- device_info = await hass.async_add_executor_job(miio_device.info)
- model = device_info.model
- unique_id = f"{model}-{device_info.mac_address}"
- _LOGGER.info(
- "%s %s %s detected",
- model,
- device_info.firmware_version,
- device_info.hardware_version,
+ entities.append(
+ XiaomiPhilipsEyecareLampAmbientLight(
+ name, light, config_entry, unique_id
+ )
)
- except DeviceException as ex:
- raise PlatformNotReady from ex
-
- if model == "philips.light.sread1":
- light = PhilipsEyecare(host, token)
- primary_device = XiaomiPhilipsEyecareLamp(name, light, model, unique_id)
- devices.append(primary_device)
- hass.data[DATA_KEY][host] = primary_device
-
- secondary_device = XiaomiPhilipsEyecareLampAmbientLight(
- name, light, model, unique_id
- )
- devices.append(secondary_device)
- # The ambient light doesn't expose additional services.
- # A hass.data[DATA_KEY] entry isn't needed.
- elif model in ["philips.light.ceiling", "philips.light.zyceiling"]:
- light = Ceil(host, token)
- device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id)
- devices.append(device)
- hass.data[DATA_KEY][host] = device
- elif model == "philips.light.moonlight":
- light = PhilipsMoonlight(host, token)
- device = XiaomiPhilipsMoonlightLamp(name, light, model, unique_id)
- devices.append(device)
- hass.data[DATA_KEY][host] = device
- elif model in [
- "philips.light.bulb",
- "philips.light.candle",
- "philips.light.candle2",
- "philips.light.downlight",
- ]:
- light = PhilipsBulb(host, token)
- device = XiaomiPhilipsBulb(name, light, model, unique_id)
- devices.append(device)
- hass.data[DATA_KEY][host] = device
- elif model == "philips.light.mono1":
- light = PhilipsBulb(host, token)
- device = XiaomiPhilipsGenericLight(name, light, model, unique_id)
- devices.append(device)
- hass.data[DATA_KEY][host] = device
- else:
- _LOGGER.error(
- "Unsupported device found! Please create an issue at "
- "https://github.com/syssi/philipslight/issues "
- "and provide the following data: %s",
- model,
- )
- return False
-
- async_add_entities(devices, update_before_add=True)
-
- async def async_service_handler(service):
- """Map services to methods on Xiaomi Philips Lights."""
- method = SERVICE_TO_METHOD.get(service.service)
- params = {
- key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID
- }
- entity_ids = service.data.get(ATTR_ENTITY_ID)
- if entity_ids:
- target_devices = [
- dev
- for dev in hass.data[DATA_KEY].values()
- if dev.entity_id in entity_ids
- ]
+ # The ambient light doesn't expose additional services.
+ # A hass.data[DATA_KEY] entry isn't needed.
+ elif model in MODELS_LIGHT_CEILING:
+ light = Ceil(host, token)
+ entity = XiaomiPhilipsCeilingLamp(name, light, config_entry, unique_id)
+ entities.append(entity)
+ hass.data[DATA_KEY][host] = entity
+ elif model in MODELS_LIGHT_MOON:
+ light = PhilipsMoonlight(host, token)
+ entity = XiaomiPhilipsMoonlightLamp(name, light, config_entry, unique_id)
+ entities.append(entity)
+ hass.data[DATA_KEY][host] = entity
+ elif model in MODELS_LIGHT_BULB:
+ light = PhilipsBulb(host, token)
+ entity = XiaomiPhilipsBulb(name, light, config_entry, unique_id)
+ entities.append(entity)
+ hass.data[DATA_KEY][host] = entity
+ elif model in MODELS_LIGHT_MONO:
+ light = PhilipsBulb(host, token)
+ entity = XiaomiPhilipsGenericLight(name, light, config_entry, unique_id)
+ entities.append(entity)
+ hass.data[DATA_KEY][host] = entity
else:
- target_devices = hass.data[DATA_KEY].values()
-
- update_tasks = []
- for target_device in target_devices:
- if not hasattr(target_device, method["method"]):
- continue
- await getattr(target_device, method["method"])(**params)
- update_tasks.append(target_device.async_update_ha_state(True))
+ _LOGGER.error(
+ "Unsupported device found! Please create an issue at "
+ "https://github.com/syssi/philipslight/issues "
+ "and provide the following data: %s",
+ model,
+ )
+ return
- if update_tasks:
- await asyncio.wait(update_tasks)
+ async def async_service_handler(service):
+ """Map services to methods on Xiaomi Philips Lights."""
+ method = SERVICE_TO_METHOD.get(service.service)
+ params = {
+ key: value
+ for key, value in service.data.items()
+ if key != ATTR_ENTITY_ID
+ }
+ entity_ids = service.data.get(ATTR_ENTITY_ID)
+ if entity_ids:
+ target_devices = [
+ dev
+ for dev in hass.data[DATA_KEY].values()
+ if dev.entity_id in entity_ids
+ ]
+ else:
+ target_devices = hass.data[DATA_KEY].values()
+
+ update_tasks = []
+ for target_device in target_devices:
+ if not hasattr(target_device, method["method"]):
+ continue
+ await getattr(target_device, method["method"])(**params)
+ update_tasks.append(target_device.async_update_ha_state(True))
+
+ if update_tasks:
+ await asyncio.wait(update_tasks)
+
+ for xiaomi_miio_service in SERVICE_TO_METHOD:
+ schema = SERVICE_TO_METHOD[xiaomi_miio_service].get(
+ "schema", XIAOMI_MIIO_SERVICE_SCHEMA
+ )
+ hass.services.async_register(
+ DOMAIN, xiaomi_miio_service, async_service_handler, schema=schema
+ )
- for xiaomi_miio_service in SERVICE_TO_METHOD:
- schema = SERVICE_TO_METHOD[xiaomi_miio_service].get(
- "schema", XIAOMI_MIIO_SERVICE_SCHEMA
- )
- hass.services.async_register(
- DOMAIN, xiaomi_miio_service, async_service_handler, schema=schema
- )
+ async_add_entities(entities, update_before_add=True)
-class XiaomiPhilipsAbstractLight(LightEntity):
+class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity):
"""Representation of a Abstract Xiaomi Philips Light."""
- def __init__(self, name, light, model, unique_id):
+ def __init__(self, name, device, entry, unique_id):
"""Initialize the light device."""
- self._name = name
- self._light = light
- self._model = model
- self._unique_id = unique_id
+ super().__init__(name, device, entry, unique_id)
self._brightness = None
-
self._available = False
self._state = None
- self._state_attrs = {ATTR_MODEL: self._model}
-
- @property
- def unique_id(self):
- """Return an unique ID."""
- return self._unique_id
-
- @property
- def name(self):
- """Return the name of the device if any."""
- return self._name
+ self._state_attrs = {}
@property
def available(self):
@@ -294,7 +270,7 @@ def available(self):
return self._available
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
return self._state_attrs
@@ -340,23 +316,23 @@ async def async_turn_on(self, **kwargs):
result = await self._try_command(
"Setting brightness failed: %s",
- self._light.set_brightness,
+ self._device.set_brightness,
percent_brightness,
)
if result:
self._brightness = brightness
else:
- await self._try_command("Turning the light on failed.", self._light.on)
+ await self._try_command("Turning the light on failed.", self._device.on)
async def async_turn_off(self, **kwargs):
"""Turn the light off."""
- await self._try_command("Turning the light off failed.", self._light.off)
+ await self._try_command("Turning the light off failed.", self._device.off)
async def async_update(self):
"""Fetch state from the device."""
try:
- state = await self.hass.async_add_executor_job(self._light.status)
+ state = await self.hass.async_add_executor_job(self._device.status)
except DeviceException as ex:
if self._available:
self._available = False
@@ -373,16 +349,16 @@ async def async_update(self):
class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight):
"""Representation of a Generic Xiaomi Philips Light."""
- def __init__(self, name, light, model, unique_id):
+ def __init__(self, name, device, entry, unique_id):
"""Initialize the light device."""
- super().__init__(name, light, model, unique_id)
+ super().__init__(name, device, entry, unique_id)
self._state_attrs.update({ATTR_SCENE: None, ATTR_DELAYED_TURN_OFF: None})
async def async_update(self):
"""Fetch state from the device."""
try:
- state = await self.hass.async_add_executor_job(self._light.status)
+ state = await self.hass.async_add_executor_job(self._device.status)
except DeviceException as ex:
if self._available:
self._available = False
@@ -408,14 +384,14 @@ async def async_update(self):
async def async_set_scene(self, scene: int = 1):
"""Set the fixed scene."""
await self._try_command(
- "Setting a fixed scene failed.", self._light.set_scene, scene
+ "Setting a fixed scene failed.", self._device.set_scene, scene
)
async def async_set_delayed_turn_off(self, time_period: timedelta):
"""Set delayed turn off."""
await self._try_command(
"Setting the turn off delay failed.",
- self._light.delay_off,
+ self._device.delay_off,
time_period.total_seconds(),
)
@@ -444,9 +420,9 @@ def delayed_turn_off_timestamp(
class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight):
"""Representation of a Xiaomi Philips Bulb."""
- def __init__(self, name, light, model, unique_id):
+ def __init__(self, name, device, entry, unique_id):
"""Initialize the light device."""
- super().__init__(name, light, model, unique_id)
+ super().__init__(name, device, entry, unique_id)
self._color_temp = None
@@ -494,7 +470,7 @@ async def async_turn_on(self, **kwargs):
result = await self._try_command(
"Setting brightness and color temperature failed: %s bri, %s cct",
- self._light.set_brightness_and_color_temperature,
+ self._device.set_brightness_and_color_temperature,
percent_brightness,
percent_color_temp,
)
@@ -512,7 +488,7 @@ async def async_turn_on(self, **kwargs):
result = await self._try_command(
"Setting color temperature failed: %s cct",
- self._light.set_color_temperature,
+ self._device.set_color_temperature,
percent_color_temp,
)
@@ -527,7 +503,7 @@ async def async_turn_on(self, **kwargs):
result = await self._try_command(
"Setting brightness failed: %s",
- self._light.set_brightness,
+ self._device.set_brightness,
percent_brightness,
)
@@ -535,12 +511,12 @@ async def async_turn_on(self, **kwargs):
self._brightness = brightness
else:
- await self._try_command("Turning the light on failed.", self._light.on)
+ await self._try_command("Turning the light on failed.", self._device.on)
async def async_update(self):
"""Fetch state from the device."""
try:
- state = await self.hass.async_add_executor_job(self._light.status)
+ state = await self.hass.async_add_executor_job(self._device.status)
except DeviceException as ex:
if self._available:
self._available = False
@@ -578,9 +554,9 @@ def translate(value, left_min, left_max, right_min, right_max):
class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb):
"""Representation of a Xiaomi Philips Ceiling Lamp."""
- def __init__(self, name, light, model, unique_id):
+ def __init__(self, name, device, entry, unique_id):
"""Initialize the light device."""
- super().__init__(name, light, model, unique_id)
+ super().__init__(name, device, entry, unique_id)
self._state_attrs.update(
{ATTR_NIGHT_LIGHT_MODE: None, ATTR_AUTOMATIC_COLOR_TEMPERATURE: None}
@@ -599,7 +575,7 @@ def max_mireds(self):
async def async_update(self):
"""Fetch state from the device."""
try:
- state = await self.hass.async_add_executor_job(self._light.status)
+ state = await self.hass.async_add_executor_job(self._device.status)
except DeviceException as ex:
if self._available:
self._available = False
@@ -634,9 +610,9 @@ async def async_update(self):
class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight):
"""Representation of a Xiaomi Philips Eyecare Lamp 2."""
- def __init__(self, name, light, model, unique_id):
+ def __init__(self, name, device, entry, unique_id):
"""Initialize the light device."""
- super().__init__(name, light, model, unique_id)
+ super().__init__(name, device, entry, unique_id)
self._state_attrs.update(
{ATTR_REMINDER: None, ATTR_NIGHT_LIGHT_MODE: None, ATTR_EYECARE_MODE: None}
@@ -645,7 +621,7 @@ def __init__(self, name, light, model, unique_id):
async def async_update(self):
"""Fetch state from the device."""
try:
- state = await self.hass.async_add_executor_job(self._light.status)
+ state = await self.hass.async_add_executor_job(self._device.status)
except DeviceException as ex:
if self._available:
self._available = False
@@ -678,46 +654,46 @@ async def async_set_delayed_turn_off(self, time_period: timedelta):
"""Set delayed turn off."""
await self._try_command(
"Setting the turn off delay failed.",
- self._light.delay_off,
+ self._device.delay_off,
round(time_period.total_seconds() / 60),
)
async def async_reminder_on(self):
"""Enable the eye fatigue notification."""
await self._try_command(
- "Turning on the reminder failed.", self._light.reminder_on
+ "Turning on the reminder failed.", self._device.reminder_on
)
async def async_reminder_off(self):
"""Disable the eye fatigue notification."""
await self._try_command(
- "Turning off the reminder failed.", self._light.reminder_off
+ "Turning off the reminder failed.", self._device.reminder_off
)
async def async_night_light_mode_on(self):
"""Turn the smart night light mode on."""
await self._try_command(
"Turning on the smart night light mode failed.",
- self._light.smart_night_light_on,
+ self._device.smart_night_light_on,
)
async def async_night_light_mode_off(self):
"""Turn the smart night light mode off."""
await self._try_command(
"Turning off the smart night light mode failed.",
- self._light.smart_night_light_off,
+ self._device.smart_night_light_off,
)
async def async_eyecare_mode_on(self):
"""Turn the eyecare mode on."""
await self._try_command(
- "Turning on the eyecare mode failed.", self._light.eyecare_on
+ "Turning on the eyecare mode failed.", self._device.eyecare_on
)
async def async_eyecare_mode_off(self):
"""Turn the eyecare mode off."""
await self._try_command(
- "Turning off the eyecare mode failed.", self._light.eyecare_off
+ "Turning off the eyecare mode failed.", self._device.eyecare_off
)
@staticmethod
@@ -747,12 +723,12 @@ def delayed_turn_off_timestamp(
class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight):
"""Representation of a Xiaomi Philips Eyecare Lamp Ambient Light."""
- def __init__(self, name, light, model, unique_id):
+ def __init__(self, name, device, entry, unique_id):
"""Initialize the light device."""
name = f"{name} Ambient Light"
if unique_id is not None:
unique_id = f"{unique_id}-ambient"
- super().__init__(name, light, model, unique_id)
+ super().__init__(name, device, entry, unique_id)
async def async_turn_on(self, **kwargs):
"""Turn the light on."""
@@ -768,7 +744,7 @@ async def async_turn_on(self, **kwargs):
result = await self._try_command(
"Setting brightness of the ambient failed: %s",
- self._light.set_ambient_brightness,
+ self._device.set_ambient_brightness,
percent_brightness,
)
@@ -776,19 +752,19 @@ async def async_turn_on(self, **kwargs):
self._brightness = brightness
else:
await self._try_command(
- "Turning the ambient light on failed.", self._light.ambient_on
+ "Turning the ambient light on failed.", self._device.ambient_on
)
async def async_turn_off(self, **kwargs):
"""Turn the light off."""
await self._try_command(
- "Turning the ambient light off failed.", self._light.ambient_off
+ "Turning the ambient light off failed.", self._device.ambient_off
)
async def async_update(self):
"""Fetch state from the device."""
try:
- state = await self.hass.async_add_executor_job(self._light.status)
+ state = await self.hass.async_add_executor_job(self._device.status)
except DeviceException as ex:
if self._available:
self._available = False
@@ -805,9 +781,9 @@ async def async_update(self):
class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb):
"""Representation of a Xiaomi Philips Zhirui Bedside Lamp."""
- def __init__(self, name, light, model, unique_id):
+ def __init__(self, name, device, entry, unique_id):
"""Initialize the light device."""
- super().__init__(name, light, model, unique_id)
+ super().__init__(name, device, entry, unique_id)
self._hs_color = None
self._state_attrs.pop(ATTR_DELAYED_TURN_OFF)
@@ -816,8 +792,8 @@ def __init__(self, name, light, model, unique_id):
ATTR_SLEEP_ASSISTANT: None,
ATTR_SLEEP_OFF_TIME: None,
ATTR_TOTAL_ASSISTANT_SLEEP_TIME: None,
- ATTR_BRAND_SLEEP: None,
- ATTR_BRAND: None,
+ ATTR_BAND_SLEEP: None,
+ ATTR_BAND: None,
}
)
@@ -867,7 +843,7 @@ async def async_turn_on(self, **kwargs):
result = await self._try_command(
"Setting brightness and color failed: %s bri, %s color",
- self._light.set_brightness_and_rgb,
+ self._device.set_brightness_and_rgb,
percent_brightness,
rgb,
)
@@ -888,7 +864,7 @@ async def async_turn_on(self, **kwargs):
result = await self._try_command(
"Setting brightness and color temperature failed: %s bri, %s cct",
- self._light.set_brightness_and_color_temperature,
+ self._device.set_brightness_and_color_temperature,
percent_brightness,
percent_color_temp,
)
@@ -901,7 +877,7 @@ async def async_turn_on(self, **kwargs):
_LOGGER.debug("Setting color: %s", rgb)
result = await self._try_command(
- "Setting color failed: %s", self._light.set_rgb, rgb
+ "Setting color failed: %s", self._device.set_rgb, rgb
)
if result:
@@ -916,7 +892,7 @@ async def async_turn_on(self, **kwargs):
result = await self._try_command(
"Setting color temperature failed: %s cct",
- self._light.set_color_temperature,
+ self._device.set_color_temperature,
percent_color_temp,
)
@@ -931,7 +907,7 @@ async def async_turn_on(self, **kwargs):
result = await self._try_command(
"Setting brightness failed: %s",
- self._light.set_brightness,
+ self._device.set_brightness,
percent_brightness,
)
@@ -939,12 +915,12 @@ async def async_turn_on(self, **kwargs):
self._brightness = brightness
else:
- await self._try_command("Turning the light on failed.", self._light.on)
+ await self._try_command("Turning the light on failed.", self._device.on)
async def async_update(self):
"""Fetch state from the device."""
try:
- state = await self.hass.async_add_executor_job(self._light.status)
+ state = await self.hass.async_add_executor_job(self._device.status)
except DeviceException as ex:
if self._available:
self._available = False
@@ -967,8 +943,8 @@ async def async_update(self):
ATTR_SLEEP_ASSISTANT: state.sleep_assistant,
ATTR_SLEEP_OFF_TIME: state.sleep_off_time,
ATTR_TOTAL_ASSISTANT_SLEEP_TIME: state.total_assistant_sleep_time,
- ATTR_BRAND_SLEEP: state.brand_sleep,
- ATTR_BRAND: state.brand,
+ ATTR_BAND_SLEEP: state.brand_sleep,
+ ATTR_BAND: state.brand,
}
)
@@ -1076,3 +1052,57 @@ async def async_update(self):
self._brightness_pct = state_dict["brightness"]
self._rgb = state_dict["rgb"]
self._hs = color.color_RGB_to_hs(*self._rgb)
+
+
+class XiaomiGatewayBulb(XiaomiGatewayDevice, LightEntity):
+ """Representation of Xiaomi Gateway Bulb."""
+
+ @property
+ def brightness(self):
+ """Return the brightness of the light."""
+ return round((self._sub_device.status["brightness"] * 255) / 100)
+
+ @property
+ def color_temp(self):
+ """Return current color temperature."""
+ return self._sub_device.status["color_temp"]
+
+ @property
+ def is_on(self):
+ """Return true if light is on."""
+ return self._sub_device.status["status"] == "on"
+
+ @property
+ def min_mireds(self):
+ """Return min cct."""
+ return self._sub_device.status["cct_min"]
+
+ @property
+ def max_mireds(self):
+ """Return max cct."""
+ return self._sub_device.status["cct_max"]
+
+ @property
+ def supported_features(self):
+ """Return the supported features."""
+ return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP
+
+ async def async_turn_on(self, **kwargs):
+ """Instruct the light to turn on."""
+ await self.hass.async_add_executor_job(self._sub_device.on)
+
+ if ATTR_COLOR_TEMP in kwargs:
+ color_temp = kwargs[ATTR_COLOR_TEMP]
+ await self.hass.async_add_executor_job(
+ self._sub_device.set_color_temp, color_temp
+ )
+
+ if ATTR_BRIGHTNESS in kwargs:
+ brightness = round((kwargs[ATTR_BRIGHTNESS] * 100) / 255)
+ await self.hass.async_add_executor_job(
+ self._sub_device.set_brightness, brightness
+ )
+
+ async def async_turn_off(self, **kwargsf):
+ """Instruct the light to turn off."""
+ await self.hass.async_add_executor_job(self._sub_device.off)
diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json
index 2536b0e0aa7ab5..6f8069be681b8a 100644
--- a/homeassistant/components/xiaomi_miio/manifest.json
+++ b/homeassistant/components/xiaomi_miio/manifest.json
@@ -3,7 +3,7 @@
"name": "Xiaomi Miio",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
- "requirements": ["construct==2.10.56", "python-miio==0.5.4"],
+ "requirements": ["construct==2.10.56", "python-miio==0.5.5"],
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG"],
"zeroconf": ["_miio._udp.local."]
}
diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py
index c946533ab54c4b..7d75e943d4dfc5 100644
--- a/homeassistant/components/xiaomi_miio/remote.py
+++ b/homeassistant/components/xiaomi_miio/remote.py
@@ -4,7 +4,7 @@
import logging
import time
-from miio import ChuangmiIr, DeviceException # pylint: disable=import-error
+from miio import ChuangmiIr, DeviceException
import voluptuous as vol
from homeassistant.components.remote import (
diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py
index d20c2dfac1e2ac..ac9a7ab4543042 100644
--- a/homeassistant/components/xiaomi_miio/sensor.py
+++ b/homeassistant/components/xiaomi_miio/sensor.py
@@ -2,43 +2,43 @@
from dataclasses import dataclass
import logging
-from miio import AirQualityMonitor, DeviceException # pylint: disable=import-error
-from miio.gateway import (
+from miio import AirQualityMonitor, DeviceException
+from miio.gateway.gateway import (
GATEWAY_MODEL_AC_V1,
GATEWAY_MODEL_AC_V2,
GATEWAY_MODEL_AC_V3,
GATEWAY_MODEL_EU,
- DeviceType,
GatewayException,
)
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
+ ATTR_BATTERY_LEVEL,
CONF_HOST,
CONF_NAME,
CONF_TOKEN,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
+ DEVICE_CLASS_POWER,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
- LIGHT_LUX,
PERCENTAGE,
+ POWER_WATT,
PRESSURE_HPA,
TEMP_CELSIUS,
)
-from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
-from .config_flow import CONF_FLOW_TYPE, CONF_GATEWAY
-from .const import DOMAIN
+from .const import CONF_DEVICE, CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN, KEY_COORDINATOR
+from .device import XiaomiMiioEntity
from .gateway import XiaomiGatewayDevice
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Xiaomi Miio Sensor"
-DATA_KEY = "sensor.xiaomi_miio"
+UNIT_LUMEN = "lm"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -50,13 +50,11 @@
ATTR_POWER = "power"
ATTR_CHARGING = "charging"
-ATTR_BATTERY_LEVEL = "battery_level"
ATTR_DISPLAY_CLOCK = "display_clock"
ATTR_NIGHT_MODE = "night_mode"
ATTR_NIGHT_TIME_BEGIN = "night_time_begin"
ATTR_NIGHT_TIME_END = "night_time_end"
ATTR_SENSOR_STATE = "sensor_state"
-ATTR_MODEL = "model"
SUCCESS = ["ok"]
@@ -80,15 +78,33 @@ class SensorType:
"pressure": SensorType(
unit=PRESSURE_HPA, icon=None, device_class=DEVICE_CLASS_PRESSURE
),
+ "load_power": SensorType(
+ unit=POWER_WATT, icon=None, device_class=DEVICE_CLASS_POWER
+ ),
}
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Import Miio configuration from YAML."""
+ _LOGGER.warning(
+ "Loading Xiaomi Miio Sensor via platform setup is deprecated. "
+ "Please remove it from your configuration"
+ )
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data=config,
+ )
+ )
+
+
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Xiaomi sensor from a config entry."""
entities = []
if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY:
- gateway = hass.data[DOMAIN][config_entry.entry_id]
+ gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY]
# Gateway illuminance sensor
if gateway.model not in [
GATEWAY_MODEL_AC_V1,
@@ -103,62 +119,39 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
# Gateway sub devices
sub_devices = gateway.devices
+ coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
for sub_device in sub_devices.values():
- sensor_variables = None
- if sub_device.type == DeviceType.SensorHT:
- sensor_variables = ["temperature", "humidity"]
- if sub_device.type == DeviceType.AqaraHT:
- sensor_variables = ["temperature", "humidity", "pressure"]
- if sensor_variables is not None:
+ sensor_variables = set(sub_device.status) & set(GATEWAY_SENSOR_TYPES)
+ if sensor_variables:
entities.extend(
[
- XiaomiGatewaySensor(sub_device, config_entry, variable)
+ XiaomiGatewaySensor(
+ coordinator, sub_device, config_entry, variable
+ )
for variable in sensor_variables
]
)
- async_add_entities(entities, update_before_add=True)
+ if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
+ host = config_entry.data[CONF_HOST]
+ token = config_entry.data[CONF_TOKEN]
+ name = config_entry.title
+ unique_id = config_entry.unique_id
+ _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the sensor from config."""
- if DATA_KEY not in hass.data:
- hass.data[DATA_KEY] = {}
-
- host = config[CONF_HOST]
- token = config[CONF_TOKEN]
- name = config[CONF_NAME]
-
- _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
-
- try:
- air_quality_monitor = AirQualityMonitor(host, token)
- device_info = await hass.async_add_executor_job(air_quality_monitor.info)
- model = device_info.model
- unique_id = f"{model}-{device_info.mac_address}"
- _LOGGER.info(
- "%s %s %s detected",
- model,
- device_info.firmware_version,
- device_info.hardware_version,
- )
- device = XiaomiAirQualityMonitor(name, air_quality_monitor, model, unique_id)
- except DeviceException as ex:
- raise PlatformNotReady from ex
+ device = AirQualityMonitor(host, token)
+ entities.append(XiaomiAirQualityMonitor(name, device, config_entry, unique_id))
- hass.data[DATA_KEY][host] = device
- async_add_entities([device], update_before_add=True)
+ async_add_entities(entities, update_before_add=True)
-class XiaomiAirQualityMonitor(Entity):
+class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity):
"""Representation of a Xiaomi Air Quality Monitor."""
- def __init__(self, name, device, model, unique_id):
+ def __init__(self, name, device, entry, unique_id):
"""Initialize the entity."""
- self._name = name
- self._device = device
- self._model = model
- self._unique_id = unique_id
+ super().__init__(name, device, entry, unique_id)
self._icon = "mdi:cloud"
self._unit_of_measurement = "AQI"
@@ -173,19 +166,8 @@ def __init__(self, name, device, model, unique_id):
ATTR_NIGHT_TIME_BEGIN: None,
ATTR_NIGHT_TIME_END: None,
ATTR_SENSOR_STATE: None,
- ATTR_MODEL: self._model,
}
- @property
- def unique_id(self):
- """Return an unique ID."""
- return self._unique_id
-
- @property
- def name(self):
- """Return the name of this entity, if any."""
- return self._name
-
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
@@ -207,7 +189,7 @@ def state(self):
return self._state
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
return self._state_attrs
@@ -238,12 +220,12 @@ async def async_update(self):
_LOGGER.error("Got exception while fetching the state: %s", ex)
-class XiaomiGatewaySensor(XiaomiGatewayDevice):
+class XiaomiGatewaySensor(XiaomiGatewayDevice, SensorEntity):
"""Representation of a XiaomiGatewaySensor."""
- def __init__(self, sub_device, entry, data_key):
+ def __init__(self, coordinator, sub_device, entry, data_key):
"""Initialize the XiaomiSensor."""
- super().__init__(sub_device, entry)
+ super().__init__(coordinator, sub_device, entry)
self._data_key = data_key
self._unique_id = f"{sub_device.sid}-{data_key}"
self._name = f"{data_key} ({sub_device.sid})".capitalize()
@@ -269,7 +251,7 @@ def state(self):
return self._sub_device.status[self._data_key]
-class XiaomiGatewayIlluminanceSensor(Entity):
+class XiaomiGatewayIlluminanceSensor(SensorEntity):
"""Representation of the gateway device's illuminance sensor."""
def __init__(self, gateway_device, gateway_name, gateway_device_id):
@@ -289,9 +271,7 @@ def unique_id(self):
@property
def device_info(self):
"""Return the device info of the gateway."""
- return {
- "identifiers": {(DOMAIN, self._gateway_device_id)},
- }
+ return {"identifiers": {(DOMAIN, self._gateway_device_id)}}
@property
def name(self):
@@ -306,7 +286,7 @@ def available(self):
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
- return LIGHT_LUX
+ return UNIT_LUMEN
@property
def device_class(self):
diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json
index 68536de76e51a5..e3d9376bc318c7 100644
--- a/homeassistant/components/xiaomi_miio/strings.json
+++ b/homeassistant/components/xiaomi_miio/strings.json
@@ -1,31 +1,24 @@
{
"config": {
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "unknown_device": "The device model is not known, not able to setup the device using config flow."
+ },
"flow_title": "Xiaomi Miio: {name}",
"step": {
- "user": {
- "title": "Xiaomi Miio",
- "description": "Select to which device you want to connect.",
- "data": {
- "gateway": "Connect to a Xiaomi Gateway"
- }
- },
- "gateway": {
- "title": "Connect to a Xiaomi Gateway",
- "description": "You will need the 32 character [%key:common::config_flow::data::api_token%], see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this [%key:common::config_flow::data::api_token%] is different from the key used by the Xiaomi Aqara integration.",
+ "device": {
"data": {
"host": "[%key:common::config_flow::data::ip%]",
- "token": "[%key:common::config_flow::data::api_token%]",
- "name": "Name of the Gateway"
- }
+ "model": "Device model (Optional)",
+ "token": "[%key:common::config_flow::data::api_token%]"
+ },
+ "description": "You will need the 32 character [%key:common::config_flow::data::api_token%], see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this [%key:common::config_flow::data::api_token%] is different from the key used by the Xiaomi Aqara integration.",
+ "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway"
}
- },
- "error": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "no_device_selected": "No device selected, please select one device."
- },
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
}
}
}
diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py
index b9e90cc5c23b84..09a9786c372278 100644
--- a/homeassistant/components/xiaomi_miio/switch.py
+++ b/homeassistant/components/xiaomi_miio/switch.py
@@ -3,44 +3,56 @@
from functools import partial
import logging
-from miio import ( # pylint: disable=import-error
- AirConditioningCompanionV3,
- ChuangmiPlug,
- Device,
- DeviceException,
- PowerStrip,
-)
-from miio.powerstrip import PowerMode # pylint: disable=import-error
+from miio import AirConditioningCompanionV3, ChuangmiPlug, DeviceException, PowerStrip
+from miio.powerstrip import PowerMode
import voluptuous as vol
-from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
+from homeassistant.components.switch import (
+ DEVICE_CLASS_SWITCH,
+ PLATFORM_SCHEMA,
+ SwitchEntity,
+)
+from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_MODE,
+ ATTR_TEMPERATURE,
CONF_HOST,
CONF_NAME,
CONF_TOKEN,
)
-from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from .const import (
+ CONF_DEVICE,
+ CONF_FLOW_TYPE,
+ CONF_GATEWAY,
+ CONF_MODEL,
DOMAIN,
+ KEY_COORDINATOR,
SERVICE_SET_POWER_MODE,
SERVICE_SET_POWER_PRICE,
SERVICE_SET_WIFI_LED_OFF,
SERVICE_SET_WIFI_LED_ON,
)
+from .device import XiaomiMiioEntity
+from .gateway import XiaomiGatewayDevice
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Xiaomi Miio Switch"
DATA_KEY = "switch.xiaomi_miio"
-CONF_MODEL = "model"
MODEL_POWER_STRIP_V2 = "zimi.powerstrip.v2"
MODEL_PLUG_V3 = "chuangmi.plug.v3"
+KEY_CHANNEL = "channel"
+GATEWAY_SWITCH_VARS = {
+ "status_ch0": {KEY_CHANNEL: 0},
+ "status_ch1": {KEY_CHANNEL: 1},
+ "status_ch2": {KEY_CHANNEL: 2},
+}
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
@@ -65,7 +77,6 @@
)
ATTR_POWER = "power"
-ATTR_TEMPERATURE = "temperature"
ATTR_LOAD_POWER = "load_power"
ATTR_MODEL = "model"
ATTR_POWER_MODE = "power_mode"
@@ -114,119 +125,180 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the switch from config."""
- if DATA_KEY not in hass.data:
- hass.data[DATA_KEY] = {}
+ """Import Miio configuration from YAML."""
+ _LOGGER.warning(
+ "Loading Xiaomi Miio Switch via platform setup is deprecated; Please remove it from your configuration"
+ )
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data=config,
+ )
+ )
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the switch from a config entry."""
+ entities = []
+
+ host = config_entry.data[CONF_HOST]
+ token = config_entry.data[CONF_TOKEN]
+ name = config_entry.title
+ model = config_entry.data[CONF_MODEL]
+ unique_id = config_entry.unique_id
+
+ if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY:
+ gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY]
+ # Gateway sub devices
+ sub_devices = gateway.devices
+ coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
+ for sub_device in sub_devices.values():
+ if sub_device.device_type != "Switch":
+ continue
+ switch_variables = set(sub_device.status) & set(GATEWAY_SWITCH_VARS)
+ if switch_variables:
+ entities.extend(
+ [
+ XiaomiGatewaySwitch(
+ coordinator, sub_device, config_entry, variable
+ )
+ for variable in switch_variables
+ ]
+ )
+
+ if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE or (
+ config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY
+ and model == "lumi.acpartner.v3"
+ ):
+ if DATA_KEY not in hass.data:
+ hass.data[DATA_KEY] = {}
+
+ _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
+
+ if model in ["chuangmi.plug.v1", "chuangmi.plug.v3", "chuangmi.plug.hmi208"]:
+ plug = ChuangmiPlug(host, token, model=model)
+
+ # The device has two switchable channels (mains and a USB port).
+ # A switch device per channel will be created.
+ for channel_usb in [True, False]:
+ if channel_usb:
+ unique_id_ch = f"{unique_id}-USB"
+ else:
+ unique_id_ch = f"{unique_id}-mains"
+ device = ChuangMiPlugSwitch(
+ name, plug, config_entry, unique_id_ch, channel_usb
+ )
+ entities.append(device)
+ hass.data[DATA_KEY][host] = device
+ elif model in ["qmi.powerstrip.v1", "zimi.powerstrip.v2"]:
+ plug = PowerStrip(host, token, model=model)
+ device = XiaomiPowerStripSwitch(name, plug, config_entry, unique_id)
+ entities.append(device)
+ hass.data[DATA_KEY][host] = device
+ elif model in [
+ "chuangmi.plug.m1",
+ "chuangmi.plug.m3",
+ "chuangmi.plug.v2",
+ "chuangmi.plug.hmi205",
+ "chuangmi.plug.hmi206",
+ ]:
+ plug = ChuangmiPlug(host, token, model=model)
+ device = XiaomiPlugGenericSwitch(name, plug, config_entry, unique_id)
+ entities.append(device)
+ hass.data[DATA_KEY][host] = device
+ elif model in ["lumi.acpartner.v3"]:
+ plug = AirConditioningCompanionV3(host, token)
+ device = XiaomiAirConditioningCompanionSwitch(
+ name, plug, config_entry, unique_id
+ )
+ entities.append(device)
+ hass.data[DATA_KEY][host] = device
+ else:
+ _LOGGER.error(
+ "Unsupported device found! Please create an issue at "
+ "https://github.com/rytilahti/python-miio/issues "
+ "and provide the following data: %s",
+ model,
+ )
- host = config[CONF_HOST]
- token = config[CONF_TOKEN]
- name = config[CONF_NAME]
- model = config.get(CONF_MODEL)
+ async def async_service_handler(service):
+ """Map services to methods on XiaomiPlugGenericSwitch."""
+ method = SERVICE_TO_METHOD.get(service.service)
+ params = {
+ key: value
+ for key, value in service.data.items()
+ if key != ATTR_ENTITY_ID
+ }
+ entity_ids = service.data.get(ATTR_ENTITY_ID)
+ if entity_ids:
+ devices = [
+ device
+ for device in hass.data[DATA_KEY].values()
+ if device.entity_id in entity_ids
+ ]
+ else:
+ devices = hass.data[DATA_KEY].values()
+
+ update_tasks = []
+ for device in devices:
+ if not hasattr(device, method["method"]):
+ continue
+ await getattr(device, method["method"])(**params)
+ update_tasks.append(device.async_update_ha_state(True))
+
+ if update_tasks:
+ await asyncio.wait(update_tasks)
+
+ for plug_service in SERVICE_TO_METHOD:
+ schema = SERVICE_TO_METHOD[plug_service].get("schema", SERVICE_SCHEMA)
+ hass.services.async_register(
+ DOMAIN, plug_service, async_service_handler, schema=schema
+ )
- _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
+ async_add_entities(entities, update_before_add=True)
- devices = []
- unique_id = None
- if model is None:
- try:
- miio_device = Device(host, token)
- device_info = await hass.async_add_executor_job(miio_device.info)
- model = device_info.model
- unique_id = f"{model}-{device_info.mac_address}"
- _LOGGER.info(
- "%s %s %s detected",
- model,
- device_info.firmware_version,
- device_info.hardware_version,
- )
- except DeviceException as ex:
- raise PlatformNotReady from ex
+class XiaomiGatewaySwitch(XiaomiGatewayDevice, SwitchEntity):
+ """Representation of a XiaomiGatewaySwitch."""
- if model in ["chuangmi.plug.v1", "chuangmi.plug.v3", "chuangmi.plug.hmi208"]:
- plug = ChuangmiPlug(host, token, model=model)
+ def __init__(self, coordinator, sub_device, entry, variable):
+ """Initialize the XiaomiSensor."""
+ super().__init__(coordinator, sub_device, entry)
+ self._channel = GATEWAY_SWITCH_VARS[variable][KEY_CHANNEL]
+ self._data_key = f"status_ch{self._channel}"
+ self._unique_id = f"{sub_device.sid}-ch{self._channel}"
+ self._name = f"{sub_device.name} ch{self._channel} ({sub_device.sid})"
- # The device has two switchable channels (mains and a USB port).
- # A switch device per channel will be created.
- for channel_usb in [True, False]:
- device = ChuangMiPlugSwitch(name, plug, model, unique_id, channel_usb)
- devices.append(device)
- hass.data[DATA_KEY][host] = device
+ @property
+ def device_class(self):
+ """Return the device class of this entity."""
+ return DEVICE_CLASS_SWITCH
- elif model in ["qmi.powerstrip.v1", "zimi.powerstrip.v2"]:
- plug = PowerStrip(host, token, model=model)
- device = XiaomiPowerStripSwitch(name, plug, model, unique_id)
- devices.append(device)
- hass.data[DATA_KEY][host] = device
- elif model in [
- "chuangmi.plug.m1",
- "chuangmi.plug.m3",
- "chuangmi.plug.v2",
- "chuangmi.plug.hmi205",
- "chuangmi.plug.hmi206",
- ]:
- plug = ChuangmiPlug(host, token, model=model)
- device = XiaomiPlugGenericSwitch(name, plug, model, unique_id)
- devices.append(device)
- hass.data[DATA_KEY][host] = device
- elif model in ["lumi.acpartner.v3"]:
- plug = AirConditioningCompanionV3(host, token)
- device = XiaomiAirConditioningCompanionSwitch(name, plug, model, unique_id)
- devices.append(device)
- hass.data[DATA_KEY][host] = device
- else:
- _LOGGER.error(
- "Unsupported device found! Please create an issue at "
- "https://github.com/rytilahti/python-miio/issues "
- "and provide the following data: %s",
- model,
- )
- return False
-
- async_add_entities(devices, update_before_add=True)
-
- async def async_service_handler(service):
- """Map services to methods on XiaomiPlugGenericSwitch."""
- method = SERVICE_TO_METHOD.get(service.service)
- params = {
- key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID
- }
- entity_ids = service.data.get(ATTR_ENTITY_ID)
- if entity_ids:
- devices = [
- device
- for device in hass.data[DATA_KEY].values()
- if device.entity_id in entity_ids
- ]
- else:
- devices = hass.data[DATA_KEY].values()
+ @property
+ def is_on(self):
+ """Return true if switch is on."""
+ return self._sub_device.status[self._data_key] == "on"
- update_tasks = []
- for device in devices:
- if not hasattr(device, method["method"]):
- continue
- await getattr(device, method["method"])(**params)
- update_tasks.append(device.async_update_ha_state(True))
+ async def async_turn_on(self, **kwargs):
+ """Turn the switch on."""
+ await self.hass.async_add_executor_job(self._sub_device.on, self._channel)
- if update_tasks:
- await asyncio.wait(update_tasks)
+ async def async_turn_off(self, **kwargs):
+ """Turn the switch off."""
+ await self.hass.async_add_executor_job(self._sub_device.off, self._channel)
- for plug_service in SERVICE_TO_METHOD:
- schema = SERVICE_TO_METHOD[plug_service].get("schema", SERVICE_SCHEMA)
- hass.services.async_register(
- DOMAIN, plug_service, async_service_handler, schema=schema
- )
+ async def async_toggle(self, **kwargs):
+ """Toggle the switch."""
+ await self.hass.async_add_executor_job(self._sub_device.toggle, self._channel)
-class XiaomiPlugGenericSwitch(SwitchEntity):
+class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity):
"""Representation of a Xiaomi Plug Generic."""
- def __init__(self, name, plug, model, unique_id):
+ def __init__(self, name, device, entry, unique_id):
"""Initialize the plug switch."""
- self._name = name
- self._plug = plug
- self._model = model
- self._unique_id = unique_id
+ super().__init__(name, device, entry, unique_id)
self._icon = "mdi:power-socket"
self._available = False
@@ -235,16 +307,6 @@ def __init__(self, name, plug, model, unique_id):
self._device_features = FEATURE_FLAGS_GENERIC
self._skip_update = False
- @property
- def unique_id(self):
- """Return an unique ID."""
- return self._unique_id
-
- @property
- def name(self):
- """Return the name of the device if any."""
- return self._name
-
@property
def icon(self):
"""Return the icon to use for device if any."""
@@ -256,7 +318,7 @@ def available(self):
return self._available
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
return self._state_attrs
@@ -288,7 +350,7 @@ async def _try_command(self, mask_error, func, *args, **kwargs):
async def async_turn_on(self, **kwargs):
"""Turn the plug on."""
- result = await self._try_command("Turning the plug on failed.", self._plug.on)
+ result = await self._try_command("Turning the plug on failed", self._device.on)
if result:
self._state = True
@@ -296,7 +358,9 @@ async def async_turn_on(self, **kwargs):
async def async_turn_off(self, **kwargs):
"""Turn the plug off."""
- result = await self._try_command("Turning the plug off failed.", self._plug.off)
+ result = await self._try_command(
+ "Turning the plug off failed", self._device.off
+ )
if result:
self._state = False
@@ -310,7 +374,7 @@ async def async_update(self):
return
try:
- state = await self.hass.async_add_executor_job(self._plug.status)
+ state = await self.hass.async_add_executor_job(self._device.status)
_LOGGER.debug("Got new state: %s", state)
self._available = True
@@ -328,7 +392,7 @@ async def async_set_wifi_led_on(self):
return
await self._try_command(
- "Turning the wifi led on failed.", self._plug.set_wifi_led, True
+ "Turning the wifi led on failed", self._device.set_wifi_led, True
)
async def async_set_wifi_led_off(self):
@@ -337,7 +401,7 @@ async def async_set_wifi_led_off(self):
return
await self._try_command(
- "Turning the wifi led off failed.", self._plug.set_wifi_led, False
+ "Turning the wifi led off failed", self._device.set_wifi_led, False
)
async def async_set_power_price(self, price: int):
@@ -346,8 +410,8 @@ async def async_set_power_price(self, price: int):
return
await self._try_command(
- "Setting the power price of the power strip failed.",
- self._plug.set_power_price,
+ "Setting the power price of the power strip failed",
+ self._device.set_power_price,
price,
)
@@ -383,7 +447,7 @@ async def async_update(self):
return
try:
- state = await self.hass.async_add_executor_job(self._plug.status)
+ state = await self.hass.async_add_executor_job(self._device.status)
_LOGGER.debug("Got new state: %s", state)
self._available = True
@@ -415,8 +479,8 @@ async def async_set_power_mode(self, mode: str):
return
await self._try_command(
- "Setting the power mode of the power strip failed.",
- self._plug.set_power_mode,
+ "Setting the power mode of the power strip failed",
+ self._device.set_power_mode,
PowerMode(mode),
)
@@ -424,14 +488,14 @@ async def async_set_power_mode(self, mode: str):
class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch):
"""Representation of a Chuang Mi Plug V1 and V3."""
- def __init__(self, name, plug, model, unique_id, channel_usb):
+ def __init__(self, name, plug, entry, unique_id, channel_usb):
"""Initialize the plug switch."""
name = f"{name} USB" if channel_usb else name
if unique_id is not None and channel_usb:
unique_id = f"{unique_id}-usb"
- super().__init__(name, plug, model, unique_id)
+ super().__init__(name, plug, entry, unique_id)
self._channel_usb = channel_usb
if self._model == MODEL_PLUG_V3:
@@ -444,11 +508,11 @@ async def async_turn_on(self, **kwargs):
"""Turn a channel on."""
if self._channel_usb:
result = await self._try_command(
- "Turning the plug on failed.", self._plug.usb_on
+ "Turning the plug on failed", self._device.usb_on
)
else:
result = await self._try_command(
- "Turning the plug on failed.", self._plug.on
+ "Turning the plug on failed", self._device.on
)
if result:
@@ -459,11 +523,11 @@ async def async_turn_off(self, **kwargs):
"""Turn a channel off."""
if self._channel_usb:
result = await self._try_command(
- "Turning the plug on failed.", self._plug.usb_off
+ "Turning the plug off failed", self._device.usb_off
)
else:
result = await self._try_command(
- "Turning the plug on failed.", self._plug.off
+ "Turning the plug off failed", self._device.off
)
if result:
@@ -478,7 +542,7 @@ async def async_update(self):
return
try:
- state = await self.hass.async_add_executor_job(self._plug.status)
+ state = await self.hass.async_add_executor_job(self._device.status)
_LOGGER.debug("Got new state: %s", state)
self._available = True
@@ -513,7 +577,7 @@ def __init__(self, name, plug, model, unique_id):
async def async_turn_on(self, **kwargs):
"""Turn the socket on."""
result = await self._try_command(
- "Turning the socket on failed.", self._plug.socket_on
+ "Turning the socket on failed", self._device.socket_on
)
if result:
@@ -523,7 +587,7 @@ async def async_turn_on(self, **kwargs):
async def async_turn_off(self, **kwargs):
"""Turn the socket off."""
result = await self._try_command(
- "Turning the socket off failed.", self._plug.socket_off
+ "Turning the socket off failed", self._device.socket_off
)
if result:
@@ -538,7 +602,7 @@ async def async_update(self):
return
try:
- state = await self.hass.async_add_executor_job(self._plug.status)
+ state = await self.hass.async_add_executor_job(self._device.status)
_LOGGER.debug("Got new state: %s", state)
self._available = True
diff --git a/homeassistant/components/xiaomi_miio/translations/bg.json b/homeassistant/components/xiaomi_miio/translations/bg.json
new file mode 100644
index 00000000000000..bd5387fe8f9bb5
--- /dev/null
+++ b/homeassistant/components/xiaomi_miio/translations/bg.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "device": {
+ "data": {
+ "host": "IP \u0430\u0434\u0440\u0435\u0441"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/ca.json b/homeassistant/components/xiaomi_miio/translations/ca.json
index 5183157371beeb..f8dee0efe6921b 100644
--- a/homeassistant/components/xiaomi_miio/translations/ca.json
+++ b/homeassistant/components/xiaomi_miio/translations/ca.json
@@ -6,10 +6,21 @@
},
"error": {
"cannot_connect": "Ha fallat la connexi\u00f3",
- "no_device_selected": "No hi ha cap dispositiu seleccionat, selecciona'n un."
+ "no_device_selected": "No hi ha cap dispositiu seleccionat, selecciona'n un.",
+ "unknown_device": "No es reconeix el model del dispositiu, no es pot configurar el dispositiu mitjan\u00e7ant el flux de configuraci\u00f3."
},
"flow_title": "Xiaomi Miio: {name}",
"step": {
+ "device": {
+ "data": {
+ "host": "Adre\u00e7a IP",
+ "model": "Model del dispositiu (opcional)",
+ "name": "Nom del dispositiu",
+ "token": "Token d'API"
+ },
+ "description": "Necessitar\u00e0s el Token d'API de 32 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Tingues en compte que aquest Token d'API \u00e9s diferent a la clau utilitzada per la integraci\u00f3 Xiaomi Aqara.",
+ "title": "Connexi\u00f3 amb un dispositiu Xiaomi Miio o una passarel\u00b7la de Xiaomi"
+ },
"gateway": {
"data": {
"host": "Adre\u00e7a IP",
diff --git a/homeassistant/components/xiaomi_miio/translations/cs.json b/homeassistant/components/xiaomi_miio/translations/cs.json
index 91c30a69e54054..ec275b9333036b 100644
--- a/homeassistant/components/xiaomi_miio/translations/cs.json
+++ b/homeassistant/components/xiaomi_miio/translations/cs.json
@@ -10,6 +10,12 @@
},
"flow_title": "Xiaomi Miio: {name}",
"step": {
+ "device": {
+ "data": {
+ "host": "IP adresa",
+ "token": "API token"
+ }
+ },
"gateway": {
"data": {
"host": "IP adresa",
diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json
index 0b5f593ffcde70..2817d18b57819e 100644
--- a/homeassistant/components/xiaomi_miio/translations/de.json
+++ b/homeassistant/components/xiaomi_miio/translations/de.json
@@ -2,25 +2,37 @@
"config": {
"abort": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
- "already_in_progress": "Der Konfigurationsablauf f\u00fcr dieses Xiaomi Miio-Ger\u00e4t wird bereits ausgef\u00fchrt."
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt"
},
"error": {
- "no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hlen Sie ein Ger\u00e4t aus."
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hle ein Ger\u00e4t aus.",
+ "unknown_device": "Das Ger\u00e4temodell ist nicht bekannt und das Ger\u00e4t kann nicht mithilfe des Assistenten eingerichtet werden."
},
"flow_title": "Xiaomi Miio: {name}",
"step": {
+ "device": {
+ "data": {
+ "host": "IP-Adresse",
+ "model": "Ger\u00e4temodell (optional)",
+ "name": "Name des Ger\u00e4ts",
+ "token": "API-Token"
+ },
+ "description": "Sie ben\u00f6tigen den 32 Zeichen langen API-Token, siehe https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token f\u00fcr eine Anleitung. Dieser unterscheidet sich vom API-Token, den die Xiaomi Aqara-Integration nutzt.",
+ "title": "Herstellen einer Verbindung mit einem Xiaomi Miio-Ger\u00e4t oder Xiaomi Gateway"
+ },
"gateway": {
"data": {
- "host": "IP Adresse",
+ "host": "IP-Adresse",
"name": "Name des Gateways",
"token": "API-Token"
},
- "description": "Sie ben\u00f6tigen den 32 Zeichen langen API-Token. Anweisungen finden Sie unter https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.",
- "title": "Stellen Sie eine Verbindung zu einem Xiaomi Gateway her"
+ "description": "Sie ben\u00f6tigen den 32 Zeichen langen API-Token. Anweisungen finden Sie unter https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.",
+ "title": "Stelle eine Verbindung zu einem Xiaomi Gateway her"
},
"user": {
"data": {
- "gateway": "Stellen Sie eine Verbindung zu einem Xiaomi Gateway her"
+ "gateway": "Stelle eine Verbindung zu einem Xiaomi Gateway her"
},
"description": "W\u00e4hlen Sie aus, mit welchem Ger\u00e4t Sie eine Verbindung herstellen m\u00f6chten.",
"title": "Xiaomi Miio"
diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json
index 4d39a6d113781d..3d893ade2f0805 100644
--- a/homeassistant/components/xiaomi_miio/translations/en.json
+++ b/homeassistant/components/xiaomi_miio/translations/en.json
@@ -6,10 +6,21 @@
},
"error": {
"cannot_connect": "Failed to connect",
- "no_device_selected": "No device selected, please select one device."
+ "no_device_selected": "No device selected, please select one device.",
+ "unknown_device": "The device model is not known, not able to setup the device using config flow."
},
"flow_title": "Xiaomi Miio: {name}",
"step": {
+ "device": {
+ "data": {
+ "host": "IP Address",
+ "model": "Device model (Optional)",
+ "name": "Name of the device",
+ "token": "API Token"
+ },
+ "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.",
+ "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway"
+ },
"gateway": {
"data": {
"host": "IP Address",
diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json
index 46fb93012adb98..60a989ade0d6ce 100644
--- a/homeassistant/components/xiaomi_miio/translations/es.json
+++ b/homeassistant/components/xiaomi_miio/translations/es.json
@@ -6,10 +6,19 @@
},
"error": {
"cannot_connect": "No se pudo conectar",
- "no_device_selected": "No se ha seleccionado ning\u00fan dispositivo, por favor, seleccione un dispositivo."
+ "no_device_selected": "No se ha seleccionado ning\u00fan dispositivo, por favor, seleccione un dispositivo.",
+ "unknown_device": "No se conoce el modelo del dispositivo, no se puede configurar el dispositivo mediante el flujo de configuraci\u00f3n."
},
"flow_title": "Xiaomi Miio: {name}",
"step": {
+ "device": {
+ "data": {
+ "model": "Modelo de dispositivo (opcional)",
+ "name": "Nombre del dispositivo"
+ },
+ "description": "Necesitar\u00e1 la clave de 32 caracteres Token API, consulte https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token para obtener instrucciones. Tenga en cuenta que esta Token API es diferente de la clave utilizada por la integraci\u00f3n de Xiaomi Aqara.",
+ "title": "Con\u00e9ctese a un dispositivo Xiaomi Miio o Xiaomi Gateway"
+ },
"gateway": {
"data": {
"host": "Direcci\u00f3n IP",
diff --git a/homeassistant/components/xiaomi_miio/translations/et.json b/homeassistant/components/xiaomi_miio/translations/et.json
index f6bd3218e14b85..a290f80ad311b9 100644
--- a/homeassistant/components/xiaomi_miio/translations/et.json
+++ b/homeassistant/components/xiaomi_miio/translations/et.json
@@ -6,10 +6,21 @@
},
"error": {
"cannot_connect": "\u00dchendus nurjus",
- "no_device_selected": "Seadmeid pole valitud, vali \u00fcks seade."
+ "no_device_selected": "Seadmeid pole valitud, vali \u00fcks seade.",
+ "unknown_device": "Seadme mudel pole teada, seadet ei saa seadistamisvoo abil seadistada."
},
"flow_title": "Xiaomi Miio: {name}",
"step": {
+ "device": {
+ "data": {
+ "host": "IP-aadress",
+ "model": "Seadme mudel (valikuline)",
+ "name": "Seadme nimi",
+ "token": "API v\u00f5ti"
+ },
+ "description": "Vaja on 32 t\u00e4hem\u00e4rgilist v\u00f5tit API v\u00f5ti , juhiste saamiseks vaata https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. Pane t\u00e4hele, et see v\u00f5ti API v\u00f5ti erineb Xiaomi Aqara sidumises kasutatavast v\u00f5tmest.",
+ "title": "\u00dchenda Xiaomi Miio seade v\u00f5i Xiaomi Gateway"
+ },
"gateway": {
"data": {
"host": "IP aadress",
diff --git a/homeassistant/components/xiaomi_miio/translations/fr.json b/homeassistant/components/xiaomi_miio/translations/fr.json
index 84849041c8c64e..30def127e7a66b 100644
--- a/homeassistant/components/xiaomi_miio/translations/fr.json
+++ b/homeassistant/components/xiaomi_miio/translations/fr.json
@@ -6,10 +6,21 @@
},
"error": {
"cannot_connect": "\u00c9chec de connexion",
- "no_device_selected": "Aucun appareil s\u00e9lectionn\u00e9, veuillez s\u00e9lectionner un appareil."
+ "no_device_selected": "Aucun appareil s\u00e9lectionn\u00e9, veuillez s\u00e9lectionner un appareil.",
+ "unknown_device": "Le mod\u00e8le d'appareil n'est pas connu, impossible de configurer l'appareil \u00e0 l'aide du flux de configuration."
},
"flow_title": "Xiaomi Miio: {name}",
"step": {
+ "device": {
+ "data": {
+ "host": "Adresse IP",
+ "model": "Mod\u00e8le d'appareil (facultatif)",
+ "name": "Nom de l'appareil",
+ "token": "Jeton d'API"
+ },
+ "description": "Vous aurez besoin des 32 caract\u00e8res Jeton d'API , voir https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token pour les instructions. Veuillez noter que cette Jeton d'API est diff\u00e9rente de la cl\u00e9 utilis\u00e9e par l'int\u00e9gration Xiaomi Aqara.",
+ "title": "Connectez-vous \u00e0 un appareil Xiaomi Miio ou \u00e0 une passerelle Xiaomi"
+ },
"gateway": {
"data": {
"host": "Adresse IP",
diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json
index beb5c06c098773..e5cf4501608804 100644
--- a/homeassistant/components/xiaomi_miio/translations/hu.json
+++ b/homeassistant/components/xiaomi_miio/translations/hu.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
- "already_in_progress": "A konfigur\u00e1ci\u00f3s folyamat m\u00e1r fut"
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van."
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
@@ -10,20 +10,30 @@
},
"flow_title": "Xiaomi Miio: {name}",
"step": {
+ "device": {
+ "data": {
+ "host": "IP c\u00edm",
+ "model": "Eszk\u00f6z modell (opcion\u00e1lis)",
+ "name": "Eszk\u00f6z neve",
+ "token": "API Token"
+ },
+ "description": "Sz\u00fcks\u00e9ged lesz a 32 karakteres API Tokenre, k\u00f6vesd a https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token oldal instrukci\u00f3it. Vedd figyelembe, hogy ez az API Token k\u00fcl\u00f6nb\u00f6zik a Xiaomi Aqara integr\u00e1ci\u00f3 \u00e1ltal haszn\u00e1lt kulcst\u00f3l.",
+ "title": "Csatlakoz\u00e1s Xiaomi Miio eszk\u00f6zh\u00f6z vagy Xiaomi Gateway-hez"
+ },
"gateway": {
"data": {
"host": "IP c\u00edm",
"name": "K\u00f6zponti egys\u00e9g neve",
"token": "API Token"
},
- "description": "Sz\u00fcks\u00e9ge lesz az API Tokenre, tov\u00e1bbi inforaciok: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token",
+ "description": "Sz\u00fcks\u00e9ge lesz az API Tokenre, tov\u00e1bbi inforaciok: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. K\u00e9rj\u00fck, vegye figyelembe, hogy ez az API Token k\u00fcl\u00f6nb\u00f6zik a Xiaomi Aqara integr\u00e1ci\u00f3 \u00e1ltal haszn\u00e1lt kulcst\u00f3l.",
"title": "Csatlakozzon egy Xiaomi K\u00f6zponti egys\u00e9ghez"
},
"user": {
"data": {
"gateway": "Csatlakozzon egy Xiaomi K\u00f6zponti egys\u00e9ghez"
},
- "description": "V\u00e1lassza ki, melyik k\u00e9sz\u00fcl\u00e9khez szeretne csatlakozni. ",
+ "description": "V\u00e1lassza ki, melyik k\u00e9sz\u00fcl\u00e9khez szeretne csatlakozni.",
"title": "Xiaomi Miio"
}
}
diff --git a/homeassistant/components/xiaomi_miio/translations/id.json b/homeassistant/components/xiaomi_miio/translations/id.json
new file mode 100644
index 00000000000000..d55e19980a7166
--- /dev/null
+++ b/homeassistant/components/xiaomi_miio/translations/id.json
@@ -0,0 +1,42 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung",
+ "no_device_selected": "Tidak ada perangkat yang dipilih, pilih satu perangkat.",
+ "unknown_device": "Model perangkat tidak diketahui, tidak dapat menyiapkan perangkat menggunakan alur konfigurasi."
+ },
+ "flow_title": "Xiaomi Miio: {name}",
+ "step": {
+ "device": {
+ "data": {
+ "host": "Alamat IP",
+ "model": "Model perangkat (Opsional)",
+ "name": "Nama perangkat",
+ "token": "Token API"
+ },
+ "description": "Anda akan membutuhkan Token API 32 karakter, lihat https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token untuk mendapatkan petunjuknya. Perhatikan bahwa Token API ini berbeda dari kunci yang digunakan oleh integrasi Xiaomi Aqara.",
+ "title": "Hubungkan ke Perangkat Xiaomi Miio atau Xiaomi Gateway"
+ },
+ "gateway": {
+ "data": {
+ "host": "Alamat IP",
+ "name": "Nama Gateway",
+ "token": "Token API"
+ },
+ "description": "Anda akan membutuhkan Token API 32 karakter, lihat https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token untuk mendapatkan petunjuknya. Perhatikan bahwa Token API ini berbeda dari kunci yang digunakan oleh integrasi Xiaomi Aqara.",
+ "title": "Hubungkan ke Xiaomi Gateway"
+ },
+ "user": {
+ "data": {
+ "gateway": "Hubungkan ke Xiaomi Gateway"
+ },
+ "description": "Pilih perangkat mana yang ingin disambungkan.",
+ "title": "Xiaomi Miio"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json
index cbfc2d60621a5c..7eec7d7e42439d 100644
--- a/homeassistant/components/xiaomi_miio/translations/it.json
+++ b/homeassistant/components/xiaomi_miio/translations/it.json
@@ -6,10 +6,21 @@
},
"error": {
"cannot_connect": "Impossibile connettersi",
- "no_device_selected": "Nessun dispositivo selezionato, selezionare un dispositivo."
+ "no_device_selected": "Nessun dispositivo selezionato, selezionare un dispositivo.",
+ "unknown_device": "Il modello del dispositivo non \u00e8 noto, non \u00e8 possibile configurare il dispositivo utilizzando il flusso di configurazione."
},
"flow_title": "Xiaomi Miio: {name}",
"step": {
+ "device": {
+ "data": {
+ "host": "Indirizzo IP",
+ "model": "Modello del dispositivo (opzionale)",
+ "name": "Nome del dispositivo",
+ "token": "Token API"
+ },
+ "description": "Avrai bisogno dei 32 caratteri della Token API, vedi https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token per istruzioni. Tieni presente che questa Token API \u00e8 diversa dalla chiave utilizzata dall'integrazione Xiaomi Aqara.",
+ "title": "Connettiti a un dispositivo Xiaomi Miio o Xiaomi Gateway"
+ },
"gateway": {
"data": {
"host": "Indirizzo IP",
diff --git a/homeassistant/components/xiaomi_miio/translations/ko.json b/homeassistant/components/xiaomi_miio/translations/ko.json
index 52f1ed960d2d9a..03043f929571dc 100644
--- a/homeassistant/components/xiaomi_miio/translations/ko.json
+++ b/homeassistant/components/xiaomi_miio/translations/ko.json
@@ -2,20 +2,32 @@
"config": {
"abort": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "already_in_progress": "Xiaomi Miio \uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4."
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4"
},
"error": {
- "no_device_selected": "\uc120\ud0dd\ub41c \uae30\uae30\uac00 \uc5c6\uc2b5\ub2c8\ub2e4. \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694."
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "no_device_selected": "\uc120\ud0dd\ub41c \uae30\uae30\uac00 \uc5c6\uc2b5\ub2c8\ub2e4. \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.",
+ "unknown_device": "\uae30\uae30\uc758 \ubaa8\ub378\uc744 \uc54c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uad6c\uc131 \ud750\ub984\uc5d0\uc11c \uae30\uae30\ub97c \uc124\uc815\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
},
"flow_title": "Xiaomi Miio: {name}",
"step": {
+ "device": {
+ "data": {
+ "host": "IP \uc8fc\uc18c",
+ "model": "\uae30\uae30 \ubaa8\ub378 (\uc120\ud0dd \uc0ac\ud56d)",
+ "name": "\uae30\uae30 \uc774\ub984",
+ "token": "API \ud1a0\ud070"
+ },
+ "description": "32\uac1c\uc758 \ubb38\uc790\uc5f4\ub85c \uad6c\uc131\ub41c API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694. \ucc38\uace0\ub85c \uc774 API \ud1a0\ud070\uc740 Xiaomi Aqara \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0\uc11c \uc0ac\uc6a9\ub418\ub294 \ud0a4\uc640 \ub2e4\ub985\ub2c8\ub2e4.",
+ "title": "Xiaomi Miio \uae30\uae30 \ub610\ub294 Xiaomi \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\uae30"
+ },
"gateway": {
"data": {
"host": "IP \uc8fc\uc18c",
"name": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uc774\ub984",
"token": "API \ud1a0\ud070"
},
- "description": "32 \ubb38\uc790\uc758 API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694. \uc774 \ud1a0\ud070\uc740 Xiaomi Aqara \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0\uc11c \uc0ac\uc6a9\ub418\ub294 \ud0a4\uc640 \ub2e4\ub985\ub2c8\ub2e4.",
+ "description": "32\uac1c\uc758 \ubb38\uc790\uc5f4\ub85c \uad6c\uc131\ub41c API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694. \ucc38\uace0\ub85c \uc774 API \ud1a0\ud070\uc740 Xiaomi Aqara \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0\uc11c \uc0ac\uc6a9\ub418\ub294 \ud0a4\uc640 \ub2e4\ub985\ub2c8\ub2e4.",
"title": "Xiaomi \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\uae30"
},
"user": {
diff --git a/homeassistant/components/xiaomi_miio/translations/nl.json b/homeassistant/components/xiaomi_miio/translations/nl.json
index eea72c1c4c0206..394d43fc26164d 100644
--- a/homeassistant/components/xiaomi_miio/translations/nl.json
+++ b/homeassistant/components/xiaomi_miio/translations/nl.json
@@ -2,12 +2,25 @@
"config": {
"abort": {
"already_configured": "Apparaat is al geconfigureerd",
- "already_in_progress": "De configuratiestroom voor dit Xiaomi Miio-apparaat is al bezig."
+ "already_in_progress": "De configuratiestroom is al aan de gang"
},
"error": {
- "no_device_selected": "Geen apparaat geselecteerd, selecteer 1 apparaat alstublieft"
+ "cannot_connect": "Kan geen verbinding maken",
+ "no_device_selected": "Geen apparaat geselecteerd, selecteer 1 apparaat alstublieft",
+ "unknown_device": "Het apparaatmodel is niet bekend, niet in staat om het apparaat in te stellen met config flow."
},
+ "flow_title": "Xiaomi Miio: {name}",
"step": {
+ "device": {
+ "data": {
+ "host": "IP-adres",
+ "model": "Apparaatmodel (Optioneel)",
+ "name": "Naam van het apparaat",
+ "token": "API-token"
+ },
+ "description": "U hebt de 32 karakter API-token nodig, zie https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token voor instructies. Let op, deze API-token is anders dan de sleutel die wordt gebruikt door de Xiaomi Aqara integratie.",
+ "title": "Verbinding maken met een Xiaomi Miio-apparaat of Xiaomi Gateway"
+ },
"gateway": {
"data": {
"host": "IP-adres",
diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json
index c7128900fc7c30..74a398a9ba6948 100644
--- a/homeassistant/components/xiaomi_miio/translations/no.json
+++ b/homeassistant/components/xiaomi_miio/translations/no.json
@@ -6,10 +6,21 @@
},
"error": {
"cannot_connect": "Tilkobling mislyktes",
- "no_device_selected": "Ingen enhet valgt, vennligst velg en enhet."
+ "no_device_selected": "Ingen enhet valgt, vennligst velg en enhet.",
+ "unknown_device": "Enhetsmodellen er ikke kjent, kan ikke konfigurere enheten ved hjelp av konfigurasjonsflyt."
},
"flow_title": "",
"step": {
+ "device": {
+ "data": {
+ "host": "IP adresse",
+ "model": "Enhetsmodell (valgfritt)",
+ "name": "Navnet p\u00e5 enheten",
+ "token": "API-token"
+ },
+ "description": "Du trenger 32 tegn API-token , se https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instruksjoner. V\u00e6r oppmerksom p\u00e5 at denne API-token er forskjellig fra n\u00f8kkelen som brukes av Xiaomi Aqara-integrasjonen.",
+ "title": "Koble til en Xiaomi Miio-enhet eller Xiaomi Gateway"
+ },
"gateway": {
"data": {
"host": "IP adresse",
diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json
index 82f7f9589057c3..8b7105b673694c 100644
--- a/homeassistant/components/xiaomi_miio/translations/pl.json
+++ b/homeassistant/components/xiaomi_miio/translations/pl.json
@@ -6,10 +6,21 @@
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
- "no_device_selected": "Nie wybrano \u017cadnego urz\u0105dzenia, wybierz jedno urz\u0105dzenie"
+ "no_device_selected": "Nie wybrano \u017cadnego urz\u0105dzenia, wybierz jedno urz\u0105dzenie",
+ "unknown_device": "Model urz\u0105dzenia nie jest znany, nie mo\u017cna skonfigurowa\u0107 urz\u0105dzenia przy u\u017cyciu interfejsu u\u017cytkownika."
},
"flow_title": "Xiaomi Miio: {name}",
"step": {
+ "device": {
+ "data": {
+ "host": "Adres IP",
+ "model": "Model urz\u0105dzenia (opcjonalnie)",
+ "name": "Nazwa urz\u0105dzenia",
+ "token": "Token API"
+ },
+ "description": "B\u0119dziesz potrzebowa\u0107 tokenu API (32 znaki), odwied\u017a https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token, aby uzyska\u0107 instrukcje. Zauwa\u017c i\u017c jest to inny token ni\u017c w integracji Xiaomi Aqara.",
+ "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi b\u0105d\u017a innym urz\u0105dzeniem Xiaomi Miio"
+ },
"gateway": {
"data": {
"host": "Adres IP",
diff --git a/homeassistant/components/xiaomi_miio/translations/pt.json b/homeassistant/components/xiaomi_miio/translations/pt.json
index 65edf2dbe31bd8..922c2441c3dba3 100644
--- a/homeassistant/components/xiaomi_miio/translations/pt.json
+++ b/homeassistant/components/xiaomi_miio/translations/pt.json
@@ -7,6 +7,12 @@
"cannot_connect": "Falha na liga\u00e7\u00e3o"
},
"step": {
+ "device": {
+ "data": {
+ "host": "Endere\u00e7o IP",
+ "token": "API Token"
+ }
+ },
"gateway": {
"data": {
"host": "Endere\u00e7o IP",
diff --git a/homeassistant/components/xiaomi_miio/translations/ru.json b/homeassistant/components/xiaomi_miio/translations/ru.json
index be02fd14f3ed87..b17291746b3e8f 100644
--- a/homeassistant/components/xiaomi_miio/translations/ru.json
+++ b/homeassistant/components/xiaomi_miio/translations/ru.json
@@ -6,10 +6,21 @@
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "no_device_selected": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u043e \u0438\u0437 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432."
+ "no_device_selected": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u043e \u0438\u0437 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.",
+ "unknown_device": "\u041c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430, \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043c\u0430\u0441\u0442\u0435\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438."
},
"flow_title": "Xiaomi Miio: {name}",
"step": {
+ "device": {
+ "data": {
+ "host": "IP-\u0430\u0434\u0440\u0435\u0441",
+ "model": "\u041c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)",
+ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430",
+ "token": "\u0422\u043e\u043a\u0435\u043d API"
+ },
+ "description": "\u0414\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f 32-\u0445 \u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u0422\u043e\u043a\u0435\u043d API. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0437\u0434\u0435\u0441\u044c: \nhttps://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token.\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u044d\u0442\u043e\u0442 \u0442\u043e\u043a\u0435\u043d \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043a\u043b\u044e\u0447\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u043f\u0440\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Xiaomi Aqara.",
+ "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Xiaomi Miio \u0438\u043b\u0438 \u0448\u043b\u044e\u0437\u0443 Xiaomi"
+ },
"gateway": {
"data": {
"host": "IP-\u0430\u0434\u0440\u0435\u0441",
diff --git a/homeassistant/components/xiaomi_miio/translations/tr.json b/homeassistant/components/xiaomi_miio/translations/tr.json
new file mode 100644
index 00000000000000..3dbf08bd6f1963
--- /dev/null
+++ b/homeassistant/components/xiaomi_miio/translations/tr.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "no_device_selected": "Cihaz se\u00e7ilmedi, l\u00fctfen bir cihaz se\u00e7in."
+ },
+ "step": {
+ "device": {
+ "data": {
+ "name": "Cihaz\u0131n ad\u0131"
+ }
+ },
+ "gateway": {
+ "data": {
+ "host": "\u0130p Adresi",
+ "name": "A\u011f Ge\u00e7idinin Ad\u0131",
+ "token": "API Belirteci"
+ },
+ "title": "Bir Xiaomi A\u011f Ge\u00e7idine ba\u011flan\u0131n"
+ },
+ "user": {
+ "data": {
+ "gateway": "Bir Xiaomi A\u011f Ge\u00e7idine ba\u011flan\u0131n"
+ },
+ "description": "Hangi cihaza ba\u011flanmak istedi\u011finizi se\u00e7in.",
+ "title": "Xiaomi Miio"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/uk.json b/homeassistant/components/xiaomi_miio/translations/uk.json
new file mode 100644
index 00000000000000..f32105589f6a2e
--- /dev/null
+++ b/homeassistant/components/xiaomi_miio/translations/uk.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "no_device_selected": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0434\u0438\u043d \u0437 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432."
+ },
+ "flow_title": "Xiaomi Miio: {name}",
+ "step": {
+ "gateway": {
+ "data": {
+ "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430",
+ "name": "\u041d\u0430\u0437\u0432\u0430",
+ "token": "\u0422\u043e\u043a\u0435\u043d API"
+ },
+ "description": "\u0414\u043b\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e 32-\u0445 \u0437\u043d\u0430\u0447\u043d\u0438\u0439 \u0422\u043e\u043a\u0435\u043d API . \u041f\u0440\u043e \u0442\u0435, \u044f\u043a \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0442\u043e\u043a\u0435\u043d, \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u0442\u0443\u0442:\nhttps://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.\n\u0417\u0432\u0435\u0440\u043d\u0456\u0442\u044c \u0443\u0432\u0430\u0433\u0443, \u0449\u043e \u0446\u0435\u0439 \u0442\u043e\u043a\u0435\u043d \u0432\u0456\u0434\u0440\u0456\u0437\u043d\u044f\u0454\u0442\u044c\u0441\u044f \u0432\u0456\u0434 \u043a\u043b\u044e\u0447\u0430, \u044f\u043a\u0438\u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 Xiaomi Aqara.",
+ "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0448\u043b\u044e\u0437\u0443 Xiaomi"
+ },
+ "user": {
+ "data": {
+ "gateway": "\u0428\u043b\u044e\u0437 Xiaomi"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u044f\u043a\u0438\u0439 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438.",
+ "title": "Xiaomi Miio"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json
index 95499fb7b827de..db1d825cea878f 100644
--- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json
+++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json
@@ -6,17 +6,28 @@
},
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557",
- "no_device_selected": "\u672a\u9078\u64c7\u88dd\u7f6e\uff0c\u8acb\u9078\u64c7\u4e00\u9805\u88dd\u7f6e\u3002"
+ "no_device_selected": "\u672a\u9078\u64c7\u88dd\u7f6e\uff0c\u8acb\u9078\u64c7\u4e00\u9805\u88dd\u7f6e\u3002",
+ "unknown_device": "\u88dd\u7f6e\u578b\u865f\u672a\u77e5\uff0c\u7121\u6cd5\u4f7f\u7528\u8a2d\u5b9a\u6d41\u7a0b\u3002"
},
"flow_title": "Xiaomi Miio\uff1a{name}",
"step": {
+ "device": {
+ "data": {
+ "host": "IP \u4f4d\u5740",
+ "model": "\u88dd\u7f6e\u578b\u865f\uff08\u9078\u9805\uff09",
+ "name": "\u88dd\u7f6e\u540d\u7a31",
+ "token": "API \u6b0a\u6756"
+ },
+ "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u6b0a\u6756\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u6b0a\u6756\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64 API \u6b0a\u6756\u8207 Xiaomi Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u6b0a\u6756\u4e0d\u540c\u3002",
+ "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc"
+ },
"gateway": {
"data": {
"host": "IP \u4f4d\u5740",
"name": "\u7db2\u95dc\u540d\u7a31",
- "token": "API \u5bc6\u9470"
+ "token": "API \u6b0a\u6756"
},
- "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u5bc6\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64API \u5bc6\u9470\u8207 Xiaomi Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u5bc6\u9470\u4e0d\u540c\u3002",
+ "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u6b0a\u6756\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u6b0a\u6756\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64API \u6b0a\u6756\u8207 Xiaomi Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u6b0a\u6756\u4e0d\u540c\u3002",
"title": "\u9023\u7dda\u81f3\u5c0f\u7c73\u7db2\u95dc"
},
"user": {
diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py
index ab76d14a69ae44..8551a80ff891bb 100644
--- a/homeassistant/components/xiaomi_miio/vacuum.py
+++ b/homeassistant/components/xiaomi_miio/vacuum.py
@@ -2,7 +2,7 @@
from functools import partial
import logging
-from miio import DeviceException, Vacuum # pylint: disable=import-error
+from miio import DeviceException, Vacuum
import voluptuous as vol
from homeassistant.components.vacuum import (
@@ -26,11 +26,15 @@
SUPPORT_STOP,
StateVacuumEntity,
)
+from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.util.dt import as_utc
from .const import (
+ CONF_DEVICE,
+ CONF_FLOW_TYPE,
+ DOMAIN,
SERVICE_CLEAN_SEGMENT,
SERVICE_CLEAN_ZONE,
SERVICE_GOTO,
@@ -39,11 +43,11 @@
SERVICE_START_REMOTE_CONTROL,
SERVICE_STOP_REMOTE_CONTROL,
)
+from .device import XiaomiMiioEntity
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Xiaomi Vacuum cleaner"
-DATA_KEY = "vacuum.xiaomi_miio"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -116,110 +120,124 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the Xiaomi vacuum cleaner robot platform."""
- if DATA_KEY not in hass.data:
- hass.data[DATA_KEY] = {}
+ """Import Miio configuration from YAML."""
+ _LOGGER.warning(
+ "Loading Xiaomi Miio Vacuum via platform setup is deprecated; Please remove it from your configuration"
+ )
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data=config,
+ )
+ )
- host = config[CONF_HOST]
- token = config[CONF_TOKEN]
- name = config[CONF_NAME]
- # Create handler
- _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
- vacuum = Vacuum(host, token)
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the Xiaomi vacuum cleaner robot from a config entry."""
+ entities = []
- mirobo = MiroboVacuum(name, vacuum)
- hass.data[DATA_KEY][host] = mirobo
+ if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
+ host = config_entry.data[CONF_HOST]
+ token = config_entry.data[CONF_TOKEN]
+ name = config_entry.title
+ unique_id = config_entry.unique_id
- async_add_entities([mirobo], update_before_add=True)
+ # Create handler
+ _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
+ vacuum = Vacuum(host, token)
- platform = entity_platform.current_platform.get()
+ mirobo = MiroboVacuum(name, vacuum, config_entry, unique_id)
+ entities.append(mirobo)
- platform.async_register_entity_service(
- SERVICE_START_REMOTE_CONTROL,
- {},
- MiroboVacuum.async_remote_control_start.__name__,
- )
+ platform = entity_platform.current_platform.get()
- platform.async_register_entity_service(
- SERVICE_STOP_REMOTE_CONTROL,
- {},
- MiroboVacuum.async_remote_control_stop.__name__,
- )
+ platform.async_register_entity_service(
+ SERVICE_START_REMOTE_CONTROL,
+ {},
+ MiroboVacuum.async_remote_control_start.__name__,
+ )
- platform.async_register_entity_service(
- SERVICE_MOVE_REMOTE_CONTROL,
- {
- vol.Optional(ATTR_RC_VELOCITY): vol.All(
- vol.Coerce(float), vol.Clamp(min=-0.29, max=0.29)
- ),
- vol.Optional(ATTR_RC_ROTATION): vol.All(
- vol.Coerce(int), vol.Clamp(min=-179, max=179)
- ),
- vol.Optional(ATTR_RC_DURATION): cv.positive_int,
- },
- MiroboVacuum.async_remote_control_move.__name__,
- )
+ platform.async_register_entity_service(
+ SERVICE_STOP_REMOTE_CONTROL,
+ {},
+ MiroboVacuum.async_remote_control_stop.__name__,
+ )
- platform.async_register_entity_service(
- SERVICE_MOVE_REMOTE_CONTROL_STEP,
- {
- vol.Optional(ATTR_RC_VELOCITY): vol.All(
- vol.Coerce(float), vol.Clamp(min=-0.29, max=0.29)
- ),
- vol.Optional(ATTR_RC_ROTATION): vol.All(
- vol.Coerce(int), vol.Clamp(min=-179, max=179)
- ),
- vol.Optional(ATTR_RC_DURATION): cv.positive_int,
- },
- MiroboVacuum.async_remote_control_move_step.__name__,
- )
+ platform.async_register_entity_service(
+ SERVICE_MOVE_REMOTE_CONTROL,
+ {
+ vol.Optional(ATTR_RC_VELOCITY): vol.All(
+ vol.Coerce(float), vol.Clamp(min=-0.29, max=0.29)
+ ),
+ vol.Optional(ATTR_RC_ROTATION): vol.All(
+ vol.Coerce(int), vol.Clamp(min=-179, max=179)
+ ),
+ vol.Optional(ATTR_RC_DURATION): cv.positive_int,
+ },
+ MiroboVacuum.async_remote_control_move.__name__,
+ )
- platform.async_register_entity_service(
- SERVICE_CLEAN_ZONE,
- {
- vol.Required(ATTR_ZONE_ARRAY): vol.All(
- list,
- [
- vol.ExactSequence(
- [
- vol.Coerce(int),
- vol.Coerce(int),
- vol.Coerce(int),
- vol.Coerce(int),
- ]
- )
- ],
- ),
- vol.Required(ATTR_ZONE_REPEATER): vol.All(
- vol.Coerce(int), vol.Clamp(min=1, max=3)
- ),
- },
- MiroboVacuum.async_clean_zone.__name__,
- )
+ platform.async_register_entity_service(
+ SERVICE_MOVE_REMOTE_CONTROL_STEP,
+ {
+ vol.Optional(ATTR_RC_VELOCITY): vol.All(
+ vol.Coerce(float), vol.Clamp(min=-0.29, max=0.29)
+ ),
+ vol.Optional(ATTR_RC_ROTATION): vol.All(
+ vol.Coerce(int), vol.Clamp(min=-179, max=179)
+ ),
+ vol.Optional(ATTR_RC_DURATION): cv.positive_int,
+ },
+ MiroboVacuum.async_remote_control_move_step.__name__,
+ )
- platform.async_register_entity_service(
- SERVICE_GOTO,
- {
- vol.Required("x_coord"): vol.Coerce(int),
- vol.Required("y_coord"): vol.Coerce(int),
- },
- MiroboVacuum.async_goto.__name__,
- )
- platform.async_register_entity_service(
- SERVICE_CLEAN_SEGMENT,
- {vol.Required("segments"): vol.Any(vol.Coerce(int), [vol.Coerce(int)])},
- MiroboVacuum.async_clean_segment.__name__,
- )
+ platform.async_register_entity_service(
+ SERVICE_CLEAN_ZONE,
+ {
+ vol.Required(ATTR_ZONE_ARRAY): vol.All(
+ list,
+ [
+ vol.ExactSequence(
+ [
+ vol.Coerce(int),
+ vol.Coerce(int),
+ vol.Coerce(int),
+ vol.Coerce(int),
+ ]
+ )
+ ],
+ ),
+ vol.Required(ATTR_ZONE_REPEATER): vol.All(
+ vol.Coerce(int), vol.Clamp(min=1, max=3)
+ ),
+ },
+ MiroboVacuum.async_clean_zone.__name__,
+ )
+
+ platform.async_register_entity_service(
+ SERVICE_GOTO,
+ {
+ vol.Required("x_coord"): vol.Coerce(int),
+ vol.Required("y_coord"): vol.Coerce(int),
+ },
+ MiroboVacuum.async_goto.__name__,
+ )
+ platform.async_register_entity_service(
+ SERVICE_CLEAN_SEGMENT,
+ {vol.Required("segments"): vol.Any(vol.Coerce(int), [vol.Coerce(int)])},
+ MiroboVacuum.async_clean_segment.__name__,
+ )
+ async_add_entities(entities, update_before_add=True)
-class MiroboVacuum(StateVacuumEntity):
+
+class MiroboVacuum(XiaomiMiioEntity, StateVacuumEntity):
"""Representation of a Xiaomi Vacuum cleaner robot."""
- def __init__(self, name, vacuum):
+ def __init__(self, name, device, entry, unique_id):
"""Initialize the Xiaomi vacuum cleaner robot handler."""
- self._name = name
- self._vacuum = vacuum
+ super().__init__(name, device, entry, unique_id)
self.vacuum_state = None
self._available = False
@@ -233,11 +251,6 @@ def __init__(self, name, vacuum):
self._timers = None
- @property
- def name(self):
- """Return the name of the device."""
- return self._name
-
@property
def state(self):
"""Return the status of the vacuum cleaner."""
@@ -292,7 +305,7 @@ def timers(self):
]
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the specific state attributes of this vacuum cleaner."""
attrs = {}
if self.vacuum_state is not None:
@@ -364,16 +377,16 @@ async def _try_command(self, mask_error, func, *args, **kwargs):
async def async_start(self):
"""Start or resume the cleaning task."""
await self._try_command(
- "Unable to start the vacuum: %s", self._vacuum.resume_or_start
+ "Unable to start the vacuum: %s", self._device.resume_or_start
)
async def async_pause(self):
"""Pause the cleaning task."""
- await self._try_command("Unable to set start/pause: %s", self._vacuum.pause)
+ await self._try_command("Unable to set start/pause: %s", self._device.pause)
async def async_stop(self, **kwargs):
"""Stop the vacuum cleaner."""
- await self._try_command("Unable to stop: %s", self._vacuum.stop)
+ await self._try_command("Unable to stop: %s", self._device.stop)
async def async_set_fan_speed(self, fan_speed, **kwargs):
"""Set fan speed."""
@@ -390,28 +403,28 @@ async def async_set_fan_speed(self, fan_speed, **kwargs):
)
return
await self._try_command(
- "Unable to set fan speed: %s", self._vacuum.set_fan_speed, fan_speed
+ "Unable to set fan speed: %s", self._device.set_fan_speed, fan_speed
)
async def async_return_to_base(self, **kwargs):
"""Set the vacuum cleaner to return to the dock."""
- await self._try_command("Unable to return home: %s", self._vacuum.home)
+ await self._try_command("Unable to return home: %s", self._device.home)
async def async_clean_spot(self, **kwargs):
"""Perform a spot clean-up."""
await self._try_command(
- "Unable to start the vacuum for a spot clean-up: %s", self._vacuum.spot
+ "Unable to start the vacuum for a spot clean-up: %s", self._device.spot
)
async def async_locate(self, **kwargs):
"""Locate the vacuum cleaner."""
- await self._try_command("Unable to locate the botvac: %s", self._vacuum.find)
+ await self._try_command("Unable to locate the botvac: %s", self._device.find)
async def async_send_command(self, command, params=None, **kwargs):
"""Send raw command."""
await self._try_command(
"Unable to send command to the vacuum: %s",
- self._vacuum.raw_command,
+ self._device.raw_command,
command,
params,
)
@@ -419,13 +432,13 @@ async def async_send_command(self, command, params=None, **kwargs):
async def async_remote_control_start(self):
"""Start remote control mode."""
await self._try_command(
- "Unable to start remote control the vacuum: %s", self._vacuum.manual_start
+ "Unable to start remote control the vacuum: %s", self._device.manual_start
)
async def async_remote_control_stop(self):
"""Stop remote control mode."""
await self._try_command(
- "Unable to stop remote control the vacuum: %s", self._vacuum.manual_stop
+ "Unable to stop remote control the vacuum: %s", self._device.manual_stop
)
async def async_remote_control_move(
@@ -434,7 +447,7 @@ async def async_remote_control_move(
"""Move vacuum with remote control mode."""
await self._try_command(
"Unable to move with remote control the vacuum: %s",
- self._vacuum.manual_control,
+ self._device.manual_control,
velocity=velocity,
rotation=rotation,
duration=duration,
@@ -446,7 +459,7 @@ async def async_remote_control_move_step(
"""Move vacuum one step with remote control mode."""
await self._try_command(
"Unable to remote control the vacuum: %s",
- self._vacuum.manual_control_once,
+ self._device.manual_control_once,
velocity=velocity,
rotation=rotation,
duration=duration,
@@ -456,7 +469,7 @@ async def async_goto(self, x_coord: int, y_coord: int):
"""Goto the specified coordinates."""
await self._try_command(
"Unable to send the vacuum cleaner to the specified coordinates: %s",
- self._vacuum.goto,
+ self._device.goto,
x_coord=x_coord,
y_coord=y_coord,
)
@@ -468,23 +481,23 @@ async def async_clean_segment(self, segments):
await self._try_command(
"Unable to start cleaning of the specified segments: %s",
- self._vacuum.segment_clean,
+ self._device.segment_clean,
segments=segments,
)
def update(self):
"""Fetch state from the device."""
try:
- state = self._vacuum.status()
+ state = self._device.status()
self.vacuum_state = state
- self._fan_speeds = self._vacuum.fan_speed_presets()
+ self._fan_speeds = self._device.fan_speed_presets()
self._fan_speeds_reverse = {v: k for k, v in self._fan_speeds.items()}
- self.consumable_state = self._vacuum.consumable_status()
- self.clean_history = self._vacuum.clean_history()
- self.last_clean = self._vacuum.last_clean_details()
- self.dnd_state = self._vacuum.dnd_status()
+ self.consumable_state = self._device.consumable_status()
+ self.clean_history = self._device.clean_history()
+ self.last_clean = self._device.last_clean_details()
+ self.dnd_state = self._device.dnd_status()
self._available = True
except (OSError, DeviceException) as exc:
@@ -494,7 +507,7 @@ def update(self):
# Fetch timers separately, see #38285
try:
- self._timers = self._vacuum.timer()
+ self._timers = self._device.timer()
except DeviceException as exc:
_LOGGER.debug(
"Unable to fetch timers, this may happen on some devices: %s", exc
@@ -507,6 +520,6 @@ async def async_clean_zone(self, zone, repeats=1):
_zone.append(repeats)
_LOGGER.debug("Zone with repeats: %s", zone)
try:
- await self.hass.async_add_executor_job(self._vacuum.zoned_clean, zone)
+ await self.hass.async_add_executor_job(self._device.zoned_clean, zone)
except (OSError, DeviceException) as exc:
_LOGGER.error("Unable to send zoned_clean command to the vacuum: %s", exc)
diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json
index b56d43b9c9c1db..ced8bd19e4090e 100644
--- a/homeassistant/components/xmpp/manifest.json
+++ b/homeassistant/components/xmpp/manifest.json
@@ -2,6 +2,6 @@
"domain": "xmpp",
"name": "Jabber (XMPP)",
"documentation": "https://www.home-assistant.io/integrations/xmpp",
- "requirements": ["slixmpp==1.6.0"],
+ "requirements": ["slixmpp==1.7.0"],
"codeowners": ["@fabaff", "@flowolf"]
}
diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py
index 78dc3c4303269a..2abd3ffa245c4d 100644
--- a/homeassistant/components/xmpp/notify.py
+++ b/homeassistant/components/xmpp/notify.py
@@ -55,7 +55,7 @@
{
vol.Required(CONF_SENDER): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
- vol.Required(CONF_RECIPIENT): cv.string,
+ vol.Required(CONF_RECIPIENT): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_RESOURCE, default=DEFAULT_RESOURCE): cv.string,
vol.Optional(CONF_ROOM, default=""): cv.string,
vol.Optional(CONF_TLS, default=True): cv.boolean,
@@ -87,7 +87,7 @@ def __init__(self, sender, resource, password, recipient, tls, verify, room, has
self._sender = sender
self._resource = resource
self._password = password
- self._recipient = recipient
+ self._recipients = recipient
self._tls = tls
self._verify = verify
self._room = room
@@ -102,7 +102,7 @@ async def async_send_message(self, message="", **kwargs):
await async_send_message(
f"{self._sender}/{self._resource}",
self._password,
- self._recipient,
+ self._recipients,
self._tls,
self._verify,
self._room,
@@ -116,7 +116,7 @@ async def async_send_message(self, message="", **kwargs):
async def async_send_message(
sender,
password,
- recipient,
+ recipients,
use_tls,
verify_certificate,
room,
@@ -165,7 +165,7 @@ async def start(self, event):
if message:
self.send_text_message()
- self.disconnect(wait=True)
+ self.disconnect()
async def send_file(self, timeout=None):
"""Send file via XMPP.
@@ -174,7 +174,7 @@ async def send_file(self, timeout=None):
HTTP Upload (XEP_0363)
"""
if room:
- self.plugin["xep_0045"].join_muc(room, sender, wait=True)
+ self.plugin["xep_0045"].join_muc(room, sender)
try:
# Uploading with XEP_0363
@@ -182,19 +182,21 @@ async def send_file(self, timeout=None):
url = await self.upload_file(timeout=timeout)
_LOGGER.info("Upload success")
- if room:
- _LOGGER.info("Sending file to %s", room)
- message = self.Message(sto=room, stype="groupchat")
- else:
- _LOGGER.info("Sending file to %s", recipient)
- message = self.Message(sto=recipient, stype="chat")
-
- message["body"] = url
- message["oob"]["url"] = url
- try:
- message.send()
- except (IqError, IqTimeout, XMPPError) as ex:
- _LOGGER.error("Could not send image message %s", ex)
+ for recipient in recipients:
+ if room:
+ _LOGGER.info("Sending file to %s", room)
+ message = self.Message(sto=room, stype="groupchat")
+ else:
+ _LOGGER.info("Sending file to %s", recipient)
+ message = self.Message(sto=recipient, stype="chat")
+ message["body"] = url
+ message["oob"]["url"] = url
+ try:
+ message.send()
+ except (IqError, IqTimeout, XMPPError) as ex:
+ _LOGGER.error("Could not send image message %s", ex)
+ if room:
+ break
except (IqError, IqTimeout, XMPPError) as ex:
_LOGGER.error("Upload error, could not send message %s", ex)
except NotConnectedError as ex:
@@ -333,11 +335,12 @@ def send_text_message(self):
try:
if room:
_LOGGER.debug("Joining room %s", room)
- self.plugin["xep_0045"].join_muc(room, sender, wait=True)
+ self.plugin["xep_0045"].join_muc(room, sender)
self.send_message(mto=room, mbody=message, mtype="groupchat")
else:
- _LOGGER.debug("Sending message to %s", recipient)
- self.send_message(mto=recipient, mbody=message, mtype="chat")
+ for recipient in recipients:
+ _LOGGER.debug("Sending message to %s", recipient)
+ self.send_message(mto=recipient, mbody=message, mtype="chat")
except (IqError, IqTimeout, XMPPError) as ex:
_LOGGER.error("Could not send text message %s", ex)
except NotConnectedError as ex:
diff --git a/homeassistant/components/xs1/__init__.py b/homeassistant/components/xs1/__init__.py
index 8651c33546c3a1..1d65b2bcfd1e74 100644
--- a/homeassistant/components/xs1/__init__.py
+++ b/homeassistant/components/xs1/__init__.py
@@ -38,7 +38,7 @@
extra=vol.ALLOW_EXTRA,
)
-XS1_COMPONENTS = ["climate", "sensor", "switch"]
+PLATFORMS = ["climate", "sensor", "switch"]
# Lock used to limit the amount of concurrent update requests
# as the XS1 Gateway can only handle a very
@@ -47,7 +47,7 @@
def setup(hass, config):
- """Set up XS1 Component."""
+ """Set up XS1 integration."""
_LOGGER.debug("Initializing XS1")
host = config[DOMAIN][CONF_HOST]
@@ -68,7 +68,7 @@ def setup(hass, config):
)
return False
- _LOGGER.debug("Establishing connection to XS1 gateway and retrieving data...")
+ _LOGGER.debug("Establishing connection to XS1 gateway and retrieving data")
hass.data[DOMAIN] = {}
@@ -78,10 +78,10 @@ def setup(hass, config):
hass.data[DOMAIN][ACTUATORS] = actuators
hass.data[DOMAIN][SENSORS] = sensors
- _LOGGER.debug("Loading components for XS1 platform...")
- # Load components for supported devices
- for component in XS1_COMPONENTS:
- discovery.load_platform(hass, component, DOMAIN, {}, config)
+ _LOGGER.debug("Loading platforms for XS1 integration")
+ # Load platforms for supported devices
+ for platform in PLATFORMS:
+ discovery.load_platform(hass, platform, DOMAIN, {}, config)
return True
diff --git a/homeassistant/components/xs1/sensor.py b/homeassistant/components/xs1/sensor.py
index 9e6afa40fa47cb..f158e7d74b8bcc 100644
--- a/homeassistant/components/xs1/sensor.py
+++ b/homeassistant/components/xs1/sensor.py
@@ -1,7 +1,7 @@
"""Support for XS1 sensors."""
from xs1_api_client.api_constants import ActuatorType
-from homeassistant.helpers.entity import Entity
+from homeassistant.components.sensor import SensorEntity
from . import ACTUATORS, DOMAIN as COMPONENT_DOMAIN, SENSORS, XS1DeviceEntity
@@ -28,7 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensor_entities)
-class XS1Sensor(XS1DeviceEntity, Entity):
+class XS1Sensor(XS1DeviceEntity, SensorEntity):
"""Representation of a Sensor."""
@property
diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py
index 957844e519d9ae..08e856a721e48a 100644
--- a/homeassistant/components/yandex_transport/sensor.py
+++ b/homeassistant/components/yandex_transport/sensor.py
@@ -6,11 +6,10 @@
from aioymaps import YandexMapsRequester
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TIMESTAMP
from homeassistant.helpers.aiohttp_client import async_create_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -47,7 +46,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([DiscoverYandexTransport(data, stop_id, routes, name)], True)
-class DiscoverYandexTransport(Entity):
+class DiscoverYandexTransport(SensorEntity):
"""Implementation of yandex_transport sensor."""
def __init__(self, requester: YandexMapsRequester, stop_id, routes, name):
@@ -124,7 +123,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attrs
diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py
index b61df3f810b1be..c1e0c555e02269 100644
--- a/homeassistant/components/yeelight/__init__.py
+++ b/homeassistant/components/yeelight/__init__.py
@@ -1,8 +1,9 @@
"""Support for Xiaomi Yeelight WiFi color bulb."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
import logging
-from typing import Optional
import voluptuous as vol
from yeelight import Bulb, BulbException, discover_bulbs
@@ -42,7 +43,6 @@
CONF_CUSTOM_EFFECTS = "custom_effects"
CONF_NIGHTLIGHT_SWITCH_TYPE = "nightlight_switch_type"
CONF_NIGHTLIGHT_SWITCH = "nightlight_switch"
-CONF_DEVICE = "device"
DATA_CONFIG_ENTRIES = "config_entries"
DATA_CUSTOM_EFFECTS = "custom_effects"
@@ -182,7 +182,7 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Yeelight from a config entry."""
- async def _initialize(host: str, capabilities: Optional[dict] = None) -> None:
+ async def _initialize(host: str, capabilities: dict | None = None) -> None:
remove_dispatcher = async_dispatcher_connect(
hass,
DEVICE_INITIALIZED.format(host),
@@ -199,9 +199,9 @@ async def _initialize(host: str, capabilities: Optional[dict] = None) -> None:
async def _load_platforms():
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
# Move options from data for imported entries
@@ -247,8 +247,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -423,7 +423,6 @@ def is_nightlight_supported(self) -> bool:
Uses brightness as it appears to be supported in both ceiling and other lights.
"""
-
return self._nightlight_brightness is not None
@property
@@ -595,7 +594,7 @@ async def _async_get_device(
hass: HomeAssistant,
host: str,
entry: ConfigEntry,
- capabilities: Optional[dict],
+ capabilities: dict | None,
) -> YeelightDevice:
# Get model from config and capabilities
model = entry.options.get(CONF_MODEL)
diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py
index 52f27932403056..0473cc1042c077 100644
--- a/homeassistant/components/yeelight/config_flow.py
+++ b/homeassistant/components/yeelight/config_flow.py
@@ -5,22 +5,21 @@
import yeelight
from homeassistant import config_entries, exceptions
-from homeassistant.const import CONF_HOST, CONF_ID, CONF_NAME
+from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_NAME
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from . import (
- CONF_DEVICE,
CONF_MODE_MUSIC,
CONF_MODEL,
CONF_NIGHTLIGHT_SWITCH,
CONF_NIGHTLIGHT_SWITCH_TYPE,
CONF_SAVE_ON_CHANGE,
CONF_TRANSITION,
+ DOMAIN,
NIGHTLIGHT_SWITCH_TYPE_LIGHT,
_async_unique_name,
)
-from . import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py
index c256cfb23e04c6..218bcbbdb27e18 100644
--- a/homeassistant/components/yeelight/light.py
+++ b/homeassistant/components/yeelight/light.py
@@ -1,10 +1,13 @@
"""Light platform support for yeelight."""
+from __future__ import annotations
+
from functools import partial
import logging
import voluptuous as vol
import yeelight
from yeelight import (
+ Bulb,
BulbException,
Flow,
RGBTransition,
@@ -261,7 +264,6 @@ async def async_setup_entry(
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
) -> None:
"""Set up Yeelight from a config entry."""
-
custom_effects = _parse_custom_effects(hass.data[DOMAIN][DATA_CUSTOM_EFFECTS])
device = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE]
@@ -530,9 +532,8 @@ def effect(self):
"""Return the current effect."""
return self._effect
- # F821: https://github.com/PyCQA/pyflakes/issues/373
@property
- def _bulb(self) -> "Bulb": # noqa: F821
+ def _bulb(self) -> Bulb:
return self.device.bulb
@property
@@ -561,9 +562,8 @@ def _predefined_effects(self):
return YEELIGHT_MONO_EFFECT_LIST
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
-
attributes = {
"flowing": self.device.is_color_flow_enabled,
"music_mode": self._bulb.music_mode,
diff --git a/homeassistant/components/yeelight/translations/de.json b/homeassistant/components/yeelight/translations/de.json
index 90c154f882bf04..6eaff2e87a3bb4 100644
--- a/homeassistant/components/yeelight/translations/de.json
+++ b/homeassistant/components/yeelight/translations/de.json
@@ -1,5 +1,12 @@
{
"config": {
+ "abort": {
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
"step": {
"pick_device": {
"data": {
@@ -9,7 +16,8 @@
"user": {
"data": {
"host": "Host"
- }
+ },
+ "description": "Wenn du den Host leer l\u00e4sst, wird die Erkennung verwendet, um Ger\u00e4te zu finden."
}
}
},
diff --git a/homeassistant/components/yeelight/translations/hu.json b/homeassistant/components/yeelight/translations/hu.json
index 10a03cebd21cc0..ac463142359233 100644
--- a/homeassistant/components/yeelight/translations/hu.json
+++ b/homeassistant/components/yeelight/translations/hu.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
- "no_devices_found": "Nincs eszk\u00f6z a h\u00e1l\u00f3zaton"
+ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton"
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
@@ -15,7 +15,7 @@
},
"user": {
"data": {
- "host": "Gazdag\u00e9p"
+ "host": "Hoszt"
},
"description": "Ha a gazdag\u00e9pet \u00fcresen hagyja, felder\u00edt\u00e9sre ker\u00fcl automatikusan."
}
diff --git a/homeassistant/components/yeelight/translations/id.json b/homeassistant/components/yeelight/translations/id.json
new file mode 100644
index 00000000000000..0c81739095d52a
--- /dev/null
+++ b/homeassistant/components/yeelight/translations/id.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan"
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "pick_device": {
+ "data": {
+ "device": "Perangkat"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Host"
+ },
+ "description": "Jika host dibiarkan kosong, proses penemuan akan digunakan untuk menemukan perangkat."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "model": "Model (Opsional)",
+ "nightlight_switch": "Gunakan Sakelar Lampu Malam",
+ "save_on_change": "Simpan Status Saat Berubah",
+ "transition": "Waktu Transisi (milidetik)",
+ "use_music_mode": "Aktifkan Mode Musik"
+ },
+ "description": "Jika model dibiarkan kosong, model akan dideteksi secara otomatis."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yeelight/translations/ko.json b/homeassistant/components/yeelight/translations/ko.json
index 7164e56b595c90..4abb8fcbbfff15 100644
--- a/homeassistant/components/yeelight/translations/ko.json
+++ b/homeassistant/components/yeelight/translations/ko.json
@@ -1,23 +1,23 @@
{
"config": {
"abort": {
- "already_configured": "\uc7a5\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.",
- "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c \uc0c1\uc5d0 \ubc1c\uacac\ub41c \uc7a5\uce58\uac00 \uc5c6\uc2b5\ub2c8\ub2e4."
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
},
"error": {
- "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328"
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"pick_device": {
"data": {
- "device": "\uc7a5\uce58"
+ "device": "\uae30\uae30"
}
},
"user": {
"data": {
"host": "\ud638\uc2a4\ud2b8"
},
- "description": "\ud638\uc2a4\ud2b8\ub97c \ube44\uc6cc\ub450\uba74 \uc7a5\uce58\ub97c \ucc3e\ub294 \ub370 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4."
+ "description": "\ud638\uc2a4\ud2b8\ub97c \ube44\uc6cc \ub450\uba74 \uae30\uae30\ub97c \ucc3e\ub294 \ub370 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4"
}
}
},
@@ -25,11 +25,11 @@
"step": {
"init": {
"data": {
- "model": "\ubaa8\ub378(\uc120\ud0dd \uc0ac\ud56d)",
- "nightlight_switch": "\uc57c\uac04 \uc870\uba85 \uc2a4\uc704\uce58 \uc0ac\uc6a9",
- "save_on_change": "\ubcc0\uacbd\uc2dc \uc0c1\ud0dc \uc800\uc7a5",
+ "model": "\ubaa8\ub378 (\uc120\ud0dd \uc0ac\ud56d)",
+ "nightlight_switch": "\uc57c\uac04 \uc870\uba85 \uc804\ud658 \uc0ac\uc6a9\ud558\uae30",
+ "save_on_change": "\ubcc0\uacbd \uc2dc \uc0c1\ud0dc\ub97c \uc800\uc7a5\ud558\uae30",
"transition": "\uc804\ud658 \uc2dc\uac04(ms)",
- "use_music_mode": "\uc74c\uc545 \ubaa8\ub4dc \ud65c\uc131\ud654"
+ "use_music_mode": "\uc74c\uc545 \ubaa8\ub4dc \ud65c\uc131\ud654\ud558\uae30"
},
"description": "\ubaa8\ub378\uc744 \ube44\uc6cc \ub450\uba74 \uc790\ub3d9\uc73c\ub85c \uac80\uc0c9\ub429\ub2c8\ub2e4."
}
diff --git a/homeassistant/components/yeelight/translations/tr.json b/homeassistant/components/yeelight/translations/tr.json
new file mode 100644
index 00000000000000..322f13f47b04d5
--- /dev/null
+++ b/homeassistant/components/yeelight/translations/tr.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "pick_device": {
+ "data": {
+ "device": "Cihaz"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Ana Bilgisayar"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "model": "Model (Opsiyonel)",
+ "save_on_change": "De\u011fi\u015fiklikte Durumu Kaydet",
+ "transition": "Ge\u00e7i\u015f S\u00fcresi (ms)",
+ "use_music_mode": "M\u00fczik Modunu Etkinle\u015ftir"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yeelight/translations/uk.json b/homeassistant/components/yeelight/translations/uk.json
new file mode 100644
index 00000000000000..0a173ccb6e4a4c
--- /dev/null
+++ b/homeassistant/components/yeelight/translations/uk.json
@@ -0,0 +1,38 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "pick_device": {
+ "data": {
+ "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442"
+ },
+ "description": "\u042f\u043a\u0449\u043e \u043d\u0435 \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u0430\u0434\u0440\u0435\u0441\u0443 \u0445\u043e\u0441\u0442\u0430, \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0431\u0443\u0434\u0443\u0442\u044c \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u0456 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "model": "\u041c\u043e\u0434\u0435\u043b\u044c (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)",
+ "nightlight_switch": "\u041f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u0447 \u0434\u043b\u044f \u043d\u0456\u0447\u043d\u0438\u043a\u0430",
+ "save_on_change": "\u0417\u0431\u0435\u0440\u0456\u0433\u0430\u0442\u0438 \u0441\u0442\u0430\u0442\u0443\u0441 \u043f\u0440\u0438 \u0437\u043c\u0456\u043d\u0456",
+ "transition": "\u0427\u0430\u0441 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0443 (\u0432 \u043c\u0456\u043b\u0456\u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)",
+ "use_music_mode": "\u041c\u0443\u0437\u0438\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c"
+ },
+ "description": "\u042f\u043a\u0449\u043e \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u0432\u0438\u0431\u0440\u0430\u043d\u043e, \u0432\u043e\u043d\u0430 \u0431\u0443\u0434\u0435 \u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zabbix/__init__.py b/homeassistant/components/zabbix/__init__.py
index b9d02731570fd3..82807c4aceefd0 100644
--- a/homeassistant/components/zabbix/__init__.py
+++ b/homeassistant/components/zabbix/__init__.py
@@ -1,4 +1,5 @@
"""Support for Zabbix."""
+from contextlib import suppress
import json
import logging
import math
@@ -202,7 +203,7 @@ def get_metrics(self):
dropped = 0
- try:
+ with suppress(queue.Empty):
while len(metrics) < BATCH_BUFFER_SIZE and not self.shutdown:
timeout = None if count == 0 else BATCH_TIMEOUT
item = self.queue.get(timeout=timeout)
@@ -223,9 +224,6 @@ def get_metrics(self):
else:
dropped += 1
- except queue.Empty:
- pass
-
if dropped:
_LOGGER.warning("Catching up, dropped %d old events", dropped)
diff --git a/homeassistant/components/zabbix/sensor.py b/homeassistant/components/zabbix/sensor.py
index 3fa29a0789688b..a264428769086c 100644
--- a/homeassistant/components/zabbix/sensor.py
+++ b/homeassistant/components/zabbix/sensor.py
@@ -4,10 +4,9 @@
import voluptuous as vol
from homeassistant.components import zabbix
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -37,7 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
zapi = hass.data[zabbix.DOMAIN]
if not zapi:
- _LOGGER.error("zapi is None. Zabbix integration hasn't been loaded?")
+ _LOGGER.error("Zabbix integration hasn't been loaded? zapi is None")
return False
_LOGGER.info("Connected to Zabbix API Version %s", zapi.api_version())
@@ -79,7 +78,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors)
-class ZabbixTriggerCountSensor(Entity):
+class ZabbixTriggerCountSensor(SensorEntity):
"""Get the active trigger count for all Zabbix monitored hosts."""
def __init__(self, zApi, name="Zabbix"):
@@ -116,7 +115,7 @@ def update(self):
self._state = len(triggers)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes of the device."""
return self._attributes
diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py
index 4852e874672ac5..2e2d07cea62ba3 100644
--- a/homeassistant/components/zamg/sensor.py
+++ b/homeassistant/components/zamg/sensor.py
@@ -11,6 +11,7 @@
import requests
import voluptuous as vol
+from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
AREA_SQUARE_METERS,
ATTR_ATTRIBUTION,
@@ -27,7 +28,6 @@
__version__,
)
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -132,7 +132,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
)
-class ZamgSensor(Entity):
+class ZamgSensor(SensorEntity):
"""Implementation of a ZAMG sensor."""
def __init__(self, probe, variable, name):
@@ -157,7 +157,7 @@ def unit_of_measurement(self):
return SENSOR_TYPES[self.variable][1]
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py
index 2ef7db3a1b4db2..d2eaa6ca766227 100644
--- a/homeassistant/components/zeroconf/__init__.py
+++ b/homeassistant/components/zeroconf/__init__.py
@@ -1,19 +1,20 @@
"""Support for exposing Home Assistant via Zeroconf."""
+from __future__ import annotations
+
+from contextlib import suppress
import fnmatch
from functools import partial
import ipaddress
import logging
import socket
+from typing import Any, TypedDict
import voluptuous as vol
from zeroconf import (
- DNSPointer,
- DNSRecord,
Error as ZeroconfError,
InterfaceChoice,
IPVersion,
NonUniqueNameException,
- ServiceBrowser,
ServiceInfo,
ServiceStateChange,
Zeroconf,
@@ -21,29 +22,24 @@
from homeassistant import util
from homeassistant.const import (
- ATTR_NAME,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STARTED,
EVENT_HOMEASSISTANT_STOP,
__version__,
)
+from homeassistant.core import Event, HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.network import NoURLAvailableError, get_url
from homeassistant.helpers.singleton import singleton
from homeassistant.loader import async_get_homekit, async_get_zeroconf
+from .models import HaServiceBrowser, HaZeroconf
from .usage import install_multiple_zeroconf_catcher
_LOGGER = logging.getLogger(__name__)
DOMAIN = "zeroconf"
-ATTR_HOST = "host"
-ATTR_PORT = "port"
-ATTR_HOSTNAME = "hostname"
-ATTR_TYPE = "type"
-ATTR_PROPERTIES = "properties"
-
ZEROCONF_TYPE = "_home-assistant._tcp.local."
HOMEKIT_TYPES = [
"_hap._tcp.local.",
@@ -53,10 +49,9 @@
CONF_DEFAULT_INTERFACE = "default_interface"
CONF_IPV6 = "ipv6"
-DEFAULT_DEFAULT_INTERFACE = False
+DEFAULT_DEFAULT_INTERFACE = True
DEFAULT_IPV6 = True
-HOMEKIT_PROPERTIES = "properties"
HOMEKIT_PAIRED_STATUS_FLAG = "sf"
HOMEKIT_MODEL = "md"
@@ -82,20 +77,31 @@
)
+class HaServiceInfo(TypedDict):
+ """Prepared info from mDNS entries."""
+
+ host: str
+ port: int | None
+ hostname: str
+ type: str
+ name: str
+ properties: dict[str, Any]
+
+
@singleton(DOMAIN)
-async def async_get_instance(hass):
+async def async_get_instance(hass: HomeAssistant) -> HaZeroconf:
"""Zeroconf instance to be shared with other integrations that use it."""
return await _async_get_instance(hass)
-async def _async_get_instance(hass, **zcargs):
+async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaZeroconf:
logging.getLogger("zeroconf").setLevel(logging.NOTSET)
zeroconf = await hass.async_add_executor_job(partial(HaZeroconf, **zcargs))
install_multiple_zeroconf_catcher(zeroconf)
- def _stop_zeroconf(_):
+ def _stop_zeroconf(_event: Event) -> None:
"""Stop Zeroconf."""
zeroconf.ha_close()
@@ -104,40 +110,10 @@ def _stop_zeroconf(_):
return zeroconf
-class HaServiceBrowser(ServiceBrowser):
- """ServiceBrowser that only consumes DNSPointer records."""
-
- def update_record(self, zc: "Zeroconf", now: float, record: DNSRecord) -> None:
- """Pre-Filter update_record to DNSPointers for the configured type."""
-
- #
- # Each ServerBrowser currently runs in its own thread which
- # processes every A or AAAA record update per instance.
- #
- # As the list of zeroconf names we watch for grows, each additional
- # ServiceBrowser would process all the A and AAAA updates on the network.
- #
- # To avoid overwhemling the system we pre-filter here and only process
- # DNSPointers for the configured record name (type)
- #
- if record.name not in self.types or not isinstance(record, DNSPointer):
- return
- super().update_record(zc, now, record)
-
-
-class HaZeroconf(Zeroconf):
- """Zeroconf that cannot be closed."""
-
- def close(self):
- """Fake method to avoid integrations closing it."""
-
- ha_close = Zeroconf.close
-
-
-async def async_setup(hass, config):
+async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up Zeroconf and make Home Assistant discoverable."""
zc_config = config.get(DOMAIN, {})
- zc_args = {}
+ zc_args: dict = {}
if zc_config.get(CONF_DEFAULT_INTERFACE, DEFAULT_DEFAULT_INTERFACE):
zc_args["interfaces"] = InterfaceChoice.Default
if not zc_config.get(CONF_IPV6, DEFAULT_IPV6):
@@ -145,7 +121,7 @@ async def async_setup(hass, config):
zeroconf = hass.data[DOMAIN] = await _async_get_instance(hass, **zc_args)
- async def _async_zeroconf_hass_start(_event):
+ async def _async_zeroconf_hass_start(_event: Event) -> None:
"""Expose Home Assistant on zeroconf when it starts.
Wait till started or otherwise HTTP is not up and running.
@@ -155,7 +131,7 @@ async def _async_zeroconf_hass_start(_event):
_register_hass_zc_service, hass, zeroconf, uuid
)
- async def _async_zeroconf_hass_started(_event):
+ async def _async_zeroconf_hass_started(_event: Event) -> None:
"""Start the service browser."""
await _async_start_zeroconf_browser(hass, zeroconf)
@@ -168,7 +144,9 @@ async def _async_zeroconf_hass_started(_event):
return True
-def _register_hass_zc_service(hass, zeroconf, uuid):
+def _register_hass_zc_service(
+ hass: HomeAssistant, zeroconf: HaZeroconf, uuid: str
+) -> None:
# Get instance UUID
valid_location_name = _truncate_location_name_to_valid(hass.config.location_name)
@@ -185,15 +163,11 @@ def _register_hass_zc_service(hass, zeroconf, uuid):
}
# Get instance URL's
- try:
+ with suppress(NoURLAvailableError):
params["external_url"] = get_url(hass, allow_internal=False)
- except NoURLAvailableError:
- pass
- try:
+ with suppress(NoURLAvailableError):
params["internal_url"] = get_url(hass, allow_external=False)
- except NoURLAvailableError:
- pass
# Set old base URL based on external or internal
params["base_url"] = params["external_url"] or params["internal_url"]
@@ -225,7 +199,9 @@ def _register_hass_zc_service(hass, zeroconf, uuid):
)
-async def _async_start_zeroconf_browser(hass, zeroconf):
+async def _async_start_zeroconf_browser(
+ hass: HomeAssistant, zeroconf: HaZeroconf
+) -> None:
"""Start the zeroconf browser."""
zeroconf_types = await async_get_zeroconf(hass)
@@ -237,12 +213,17 @@ async def _async_start_zeroconf_browser(hass, zeroconf):
if hk_type not in zeroconf_types:
types.append(hk_type)
- def service_update(zeroconf, service_type, name, state_change):
+ def service_update(
+ zeroconf: Zeroconf,
+ service_type: str,
+ name: str,
+ state_change: ServiceStateChange,
+ ) -> None:
"""Service state changed."""
nonlocal zeroconf_types
nonlocal homekit_models
- if state_change != ServiceStateChange.Added:
+ if state_change == ServiceStateChange.Removed:
return
try:
@@ -277,12 +258,11 @@ def service_update(zeroconf, service_type, name, state_change):
# offering a second discovery for the same device
if (
discovery_was_forwarded
- and HOMEKIT_PROPERTIES in info
- and HOMEKIT_PAIRED_STATUS_FLAG in info[HOMEKIT_PROPERTIES]
+ and HOMEKIT_PAIRED_STATUS_FLAG in info["properties"]
):
try:
# 0 means paired and not discoverable by iOS clients)
- if int(info[HOMEKIT_PROPERTIES][HOMEKIT_PAIRED_STATUS_FLAG]):
+ if int(info["properties"][HOMEKIT_PAIRED_STATUS_FLAG]):
return
except ValueError:
# HomeKit pairing status unknown
@@ -290,15 +270,22 @@ def service_update(zeroconf, service_type, name, state_change):
return
if "name" in info:
- lowercase_name = info["name"].lower()
+ lowercase_name: str | None = info["name"].lower()
else:
lowercase_name = None
- if "macaddress" in info.get("properties", {}):
- uppercase_mac = info["properties"]["macaddress"].upper()
+ if "macaddress" in info["properties"]:
+ uppercase_mac: str | None = info["properties"]["macaddress"].upper()
else:
uppercase_mac = None
+ if "manufacturer" in info["properties"]:
+ lowercase_manufacturer: str | None = info["properties"][
+ "manufacturer"
+ ].lower()
+ else:
+ lowercase_manufacturer = None
+
# Not all homekit types are currently used for discovery
# so not all service type exist in zeroconf_types
for entry in zeroconf_types.get(service_type, []):
@@ -315,24 +302,34 @@ def service_update(zeroconf, service_type, name, state_change):
and not fnmatch.fnmatch(lowercase_name, entry["name"])
):
continue
+ if (
+ lowercase_manufacturer is not None
+ and "manufacturer" in entry
+ and not fnmatch.fnmatch(
+ lowercase_manufacturer, entry["manufacturer"]
+ )
+ ):
+ continue
hass.add_job(
hass.config_entries.flow.async_init(
entry["domain"], context={"source": DOMAIN}, data=info
- )
+ ) # type: ignore
)
_LOGGER.debug("Starting Zeroconf browser")
HaServiceBrowser(zeroconf, types, handlers=[service_update])
-def handle_homekit(hass, homekit_models, info) -> bool:
+def handle_homekit(
+ hass: HomeAssistant, homekit_models: dict[str, str], info: HaServiceInfo
+) -> bool:
"""Handle a HomeKit discovery.
Return if discovery was forwarded.
"""
model = None
- props = info.get(HOMEKIT_PROPERTIES, {})
+ props = info["properties"]
for key in props:
if key.lower() == HOMEKIT_MODEL:
@@ -353,16 +350,16 @@ def handle_homekit(hass, homekit_models, info) -> bool:
hass.add_job(
hass.config_entries.flow.async_init(
homekit_models[test_model], context={"source": "homekit"}, data=info
- )
+ ) # type: ignore
)
return True
return False
-def info_from_service(service):
+def info_from_service(service: ServiceInfo) -> HaServiceInfo | None:
"""Return prepared info from mDNS entries."""
- properties = {"_raw": {}}
+ properties: dict[str, Any] = {"_raw": {}}
for key, value in service.properties.items():
# See https://ietf.org/rfc/rfc6763.html#section-6.4 and
@@ -378,30 +375,26 @@ def info_from_service(service):
properties["_raw"][key] = value
- try:
+ with suppress(UnicodeDecodeError):
if isinstance(value, bytes):
properties[key] = value.decode("utf-8")
- except UnicodeDecodeError:
- pass
if not service.addresses:
return None
address = service.addresses[0]
- info = {
- ATTR_HOST: str(ipaddress.ip_address(address)),
- ATTR_PORT: service.port,
- ATTR_HOSTNAME: service.server,
- ATTR_TYPE: service.type,
- ATTR_NAME: service.name,
- ATTR_PROPERTIES: properties,
+ return {
+ "host": str(ipaddress.ip_address(address)),
+ "port": service.port,
+ "hostname": service.server,
+ "type": service.type,
+ "name": service.name,
+ "properties": properties,
}
- return info
-
-def _suppress_invalid_properties(properties):
+def _suppress_invalid_properties(properties: dict) -> None:
"""Suppress any properties that will cause zeroconf to fail to startup."""
for prop, prop_value in properties.items():
@@ -418,7 +411,7 @@ def _suppress_invalid_properties(properties):
properties[prop] = ""
-def _truncate_location_name_to_valid(location_name):
+def _truncate_location_name_to_valid(location_name: str) -> str:
"""Truncate or return the location name usable for zeroconf."""
if len(location_name.encode("utf-8")) < MAX_NAME_LEN:
return location_name
diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json
index 654eec820c3e66..d407acece57464 100644
--- a/homeassistant/components/zeroconf/manifest.json
+++ b/homeassistant/components/zeroconf/manifest.json
@@ -2,7 +2,7 @@
"domain": "zeroconf",
"name": "Zero-configuration networking (zeroconf)",
"documentation": "https://www.home-assistant.io/integrations/zeroconf",
- "requirements": ["zeroconf==0.28.8"],
+ "requirements": ["zeroconf==0.29.0"],
"dependencies": ["api"],
"codeowners": ["@bdraco"],
"quality_scale": "internal"
diff --git a/homeassistant/components/zeroconf/models.py b/homeassistant/components/zeroconf/models.py
new file mode 100644
index 00000000000000..02a6fc7cdaac46
--- /dev/null
+++ b/homeassistant/components/zeroconf/models.py
@@ -0,0 +1,33 @@
+"""Models for Zeroconf."""
+
+from zeroconf import DNSPointer, DNSRecord, ServiceBrowser, Zeroconf
+
+
+class HaZeroconf(Zeroconf):
+ """Zeroconf that cannot be closed."""
+
+ def close(self) -> None:
+ """Fake method to avoid integrations closing it."""
+
+ ha_close = Zeroconf.close
+
+
+class HaServiceBrowser(ServiceBrowser):
+ """ServiceBrowser that only consumes DNSPointer records."""
+
+ def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None:
+ """Pre-Filter update_record to DNSPointers for the configured type."""
+
+ #
+ # Each ServerBrowser currently runs in its own thread which
+ # processes every A or AAAA record update per instance.
+ #
+ # As the list of zeroconf names we watch for grows, each additional
+ # ServiceBrowser would process all the A and AAAA updates on the network.
+ #
+ # To avoid overwhemling the system we pre-filter here and only process
+ # DNSPointers for the configured record name (type)
+ #
+ if record.name not in self.types or not isinstance(record, DNSPointer):
+ return
+ super().update_record(zc, now, record)
diff --git a/homeassistant/components/zeroconf/usage.py b/homeassistant/components/zeroconf/usage.py
index 1303412249c47a..f7689ab63a422b 100644
--- a/homeassistant/components/zeroconf/usage.py
+++ b/homeassistant/components/zeroconf/usage.py
@@ -1,6 +1,8 @@
"""Zeroconf usage utility to warn about multiple instances."""
+from contextlib import suppress
import logging
+from typing import Any
import zeroconf
@@ -10,23 +12,25 @@
report_integration,
)
+from .models import HaZeroconf
+
_LOGGER = logging.getLogger(__name__)
-def install_multiple_zeroconf_catcher(hass_zc) -> None:
+def install_multiple_zeroconf_catcher(hass_zc: HaZeroconf) -> None:
"""Wrap the Zeroconf class to return the shared instance if multiple instances are detected."""
- def new_zeroconf_new(self, *k, **kw):
+ def new_zeroconf_new(self: zeroconf.Zeroconf, *k: Any, **kw: Any) -> HaZeroconf:
_report(
"attempted to create another Zeroconf instance. Please use the shared Zeroconf via await homeassistant.components.zeroconf.async_get_instance(hass)",
)
return hass_zc
- def new_zeroconf_init(self, *k, **kw):
+ def new_zeroconf_init(self: zeroconf.Zeroconf, *k: Any, **kw: Any) -> None:
return
- zeroconf.Zeroconf.__new__ = new_zeroconf_new
- zeroconf.Zeroconf.__init__ = new_zeroconf_init
+ zeroconf.Zeroconf.__new__ = new_zeroconf_new # type: ignore
+ zeroconf.Zeroconf.__init__ = new_zeroconf_init # type: ignore
def _report(what: str) -> None:
@@ -36,14 +40,12 @@ def _report(what: str) -> None:
"""
integration_frame = None
- try:
+ with suppress(MissingIntegrationFrame):
integration_frame = get_integration_frame(exclude_integrations={"zeroconf"})
- except MissingIntegrationFrame:
- pass
if not integration_frame:
_LOGGER.warning(
- "Detected code that %s. Please report this issue.", what, stack_info=True
+ "Detected code that %s; Please report this issue", what, stack_info=True
)
return
diff --git a/homeassistant/components/zerproc/__init__.py b/homeassistant/components/zerproc/__init__.py
index 2c652f61c21ed9..12953afeb2dbf7 100644
--- a/homeassistant/components/zerproc/__init__.py
+++ b/homeassistant/components/zerproc/__init__.py
@@ -4,7 +4,7 @@
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.core import HomeAssistant
-from .const import DOMAIN
+from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN
PLATFORMS = ["light"]
@@ -20,9 +20,14 @@ async def async_setup(hass, config):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Zerproc from a config entry."""
- for component in PLATFORMS:
+ if DOMAIN not in hass.data:
+ hass.data[DOMAIN] = {}
+ if DATA_ADDRESSES not in hass.data[DOMAIN]:
+ hass.data[DOMAIN][DATA_ADDRESSES] = set()
+
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
@@ -30,11 +35,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
+ # Stop discovery
+ unregister_discovery = hass.data[DOMAIN].pop(DATA_DISCOVERY_SUBSCRIPTION, None)
+ if unregister_discovery:
+ unregister_discovery()
+
+ hass.data.pop(DOMAIN, None)
+
return all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/homeassistant/components/zerproc/const.py b/homeassistant/components/zerproc/const.py
index a5481bd4c34032..69d5fcfb74081a 100644
--- a/homeassistant/components/zerproc/const.py
+++ b/homeassistant/components/zerproc/const.py
@@ -1,2 +1,5 @@
"""Constants for the Zerproc integration."""
DOMAIN = "zerproc"
+
+DATA_ADDRESSES = "addresses"
+DATA_DISCOVERY_SUBSCRIPTION = "discovery_subscription"
diff --git a/homeassistant/components/zerproc/light.py b/homeassistant/components/zerproc/light.py
index 89f60faf84ebb4..627358ab971889 100644
--- a/homeassistant/components/zerproc/light.py
+++ b/homeassistant/components/zerproc/light.py
@@ -1,8 +1,9 @@
"""Zerproc light platform."""
-import asyncio
+from __future__ import annotations
+
from datetime import timedelta
import logging
-from typing import Callable, List, Optional
+from typing import Callable
import pyzerproc
@@ -21,7 +22,7 @@
from homeassistant.helpers.typing import HomeAssistantType
import homeassistant.util.color as color_util
-from .const import DOMAIN
+from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -30,34 +31,21 @@
DISCOVERY_INTERVAL = timedelta(seconds=60)
-async def connect_light(light: pyzerproc.Light) -> Optional[pyzerproc.Light]:
- """Return the given light if it connects successfully."""
- try:
- await light.connect()
- except pyzerproc.ZerprocException:
- _LOGGER.debug("Unable to connect to '%s'", light.address, exc_info=True)
- return None
- return light
-
-
-async def discover_entities(hass: HomeAssistant) -> List[Entity]:
+async def discover_entities(hass: HomeAssistant) -> list[Entity]:
"""Attempt to discover new lights."""
lights = await pyzerproc.discover()
# Filter out already discovered lights
new_lights = [
- light for light in lights if light.address not in hass.data[DOMAIN]["addresses"]
+ light
+ for light in lights
+ if light.address not in hass.data[DOMAIN][DATA_ADDRESSES]
]
entities = []
- connected_lights = filter(
- None, await asyncio.gather(*(connect_light(light) for light in new_lights))
- )
- for light in connected_lights:
- # Double-check the light hasn't been added in the meantime
- if light.address not in hass.data[DOMAIN]["addresses"]:
- hass.data[DOMAIN]["addresses"].add(light.address)
- entities.append(ZerprocLight(light))
+ for light in new_lights:
+ hass.data[DOMAIN][DATA_ADDRESSES].add(light.address)
+ entities.append(ZerprocLight(light))
return entities
@@ -65,14 +53,9 @@ async def discover_entities(hass: HomeAssistant) -> List[Entity]:
async def async_setup_entry(
hass: HomeAssistantType,
config_entry: ConfigEntry,
- async_add_entities: Callable[[List[Entity], bool], None],
+ async_add_entities: Callable[[list[Entity], bool], None],
) -> None:
"""Set up Zerproc light devices."""
- if DOMAIN not in hass.data:
- hass.data[DOMAIN] = {}
- if "addresses" not in hass.data[DOMAIN]:
- hass.data[DOMAIN]["addresses"] = set()
-
warned = False
async def discover(*args):
@@ -91,7 +74,9 @@ async def discover(*args):
hass.async_create_task(discover())
# Perform recurring discovery of new devices
- async_track_time_interval(hass, discover, DISCOVERY_INTERVAL)
+ hass.data[DOMAIN][DATA_DISCOVERY_SUBSCRIPTION] = async_track_time_interval(
+ hass, discover, DISCOVERY_INTERVAL
+ )
class ZerprocLight(LightEntity):
@@ -120,7 +105,7 @@ async def async_will_remove_from_hass(self, *args) -> None:
await self._light.disconnect()
except pyzerproc.ZerprocException:
_LOGGER.debug(
- "Exception disconnected from %s", self.entity_id, exc_info=True
+ "Exception disconnecting from %s", self._light.address, exc_info=True
)
@property
@@ -143,7 +128,7 @@ def device_info(self):
}
@property
- def icon(self) -> Optional[str]:
+ def icon(self) -> str | None:
"""Return the icon to use in the frontend."""
return "mdi:string-lights"
@@ -198,11 +183,11 @@ async def async_update(self):
state = await self._light.get_state()
except pyzerproc.ZerprocException:
if self._available:
- _LOGGER.warning("Unable to connect to %s", self.entity_id)
+ _LOGGER.warning("Unable to connect to %s", self._light.address)
self._available = False
return
if self._available is False:
- _LOGGER.info("Reconnected to %s", self.entity_id)
+ _LOGGER.info("Reconnected to %s", self._light.address)
self._available = True
self._is_on = state.is_on
hsv = color_util.color_RGB_to_hsv(*state.color)
diff --git a/homeassistant/components/zerproc/manifest.json b/homeassistant/components/zerproc/manifest.json
index 54b70d78673f83..d2d00987ab7a38 100644
--- a/homeassistant/components/zerproc/manifest.json
+++ b/homeassistant/components/zerproc/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zerproc",
"requirements": [
- "pyzerproc==0.4.7"
+ "pyzerproc==0.4.8"
],
"codeowners": [
"@emlove"
diff --git a/homeassistant/components/zerproc/translations/id.json b/homeassistant/components/zerproc/translations/id.json
new file mode 100644
index 00000000000000..223836a8b40992
--- /dev/null
+++ b/homeassistant/components/zerproc/translations/id.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "step": {
+ "confirm": {
+ "description": "Ingin memulai penyiapan?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zerproc/translations/ko.json b/homeassistant/components/zerproc/translations/ko.json
index 7011a61f7573ab..e5ae04d6e5c810 100644
--- a/homeassistant/components/zerproc/translations/ko.json
+++ b/homeassistant/components/zerproc/translations/ko.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
- "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4."
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"step": {
"confirm": {
diff --git a/homeassistant/components/zerproc/translations/nl.json b/homeassistant/components/zerproc/translations/nl.json
new file mode 100644
index 00000000000000..d11896014fd2c6
--- /dev/null
+++ b/homeassistant/components/zerproc/translations/nl.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Geen apparaten gevonden op het netwerk",
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
+ },
+ "step": {
+ "confirm": {
+ "description": "Wil je beginnen met instellen?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zerproc/translations/tr.json b/homeassistant/components/zerproc/translations/tr.json
index 49fa9545e94d2c..3df15466f030f1 100644
--- a/homeassistant/components/zerproc/translations/tr.json
+++ b/homeassistant/components/zerproc/translations/tr.json
@@ -1,7 +1,13 @@
{
"config": {
"abort": {
- "no_devices_found": "A\u011fda cihaz bulunamad\u0131"
+ "no_devices_found": "A\u011fda cihaz bulunamad\u0131",
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "step": {
+ "confirm": {
+ "description": "Kuruluma ba\u015flamak ister misiniz?"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zerproc/translations/uk.json b/homeassistant/components/zerproc/translations/uk.json
new file mode 100644
index 00000000000000..292861e9129dbd
--- /dev/null
+++ b/homeassistant/components/zerproc/translations/uk.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py
index cdf7e6304adafb..0333bb76a201b7 100644
--- a/homeassistant/components/zestimate/sensor.py
+++ b/homeassistant/components/zestimate/sensor.py
@@ -6,10 +6,9 @@
import voluptuous as vol
import xmltodict
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
_RESOURCE = "http://www.zillow.com/webservice/GetZestimate.htm"
@@ -56,7 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors, True)
-class ZestimateDataSensor(Entity):
+class ZestimateDataSensor(SensorEntity):
"""Implementation of a Zestimate sensor."""
def __init__(self, name, params):
@@ -86,7 +85,7 @@ def state(self):
return None
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
attributes = {}
if self.data is not None:
diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py
index d5f76fa5e232c3..707e0292c45166 100644
--- a/homeassistant/components/zha/__init__.py
+++ b/homeassistant/components/zha/__init__.py
@@ -4,6 +4,7 @@
import logging
import voluptuous as vol
+from zhaquirks import setup as setup_quirks
from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH
from homeassistant import config_entries, const as ha_const
@@ -16,7 +17,6 @@
from .core import ZHAGateway
from .core.const import (
BAUD_RATES,
- COMPONENTS,
CONF_BAUDRATE,
CONF_DATABASE,
CONF_DEVICE_CONFIG,
@@ -30,6 +30,7 @@
DATA_ZHA_GATEWAY,
DATA_ZHA_PLATFORM_LOADED,
DOMAIN,
+ PLATFORMS,
SIGNAL_ADD_ENTITIES,
RadioType,
)
@@ -88,21 +89,19 @@ async def async_setup_entry(hass, config_entry):
zha_data = hass.data.setdefault(DATA_ZHA, {})
config = zha_data.get(DATA_ZHA_CONFIG, {})
- for component in COMPONENTS:
- zha_data.setdefault(component, [])
+ for platform in PLATFORMS:
+ zha_data.setdefault(platform, [])
if config.get(CONF_ENABLE_QUIRKS, True):
- # needs to be done here so that the ZHA module is finished loading
- # before zhaquirks is imported
- import zhaquirks # noqa: F401 pylint: disable=unused-import, import-outside-toplevel, import-error
+ setup_quirks(config)
zha_gateway = ZHAGateway(hass, config, config_entry)
await zha_gateway.async_initialize()
zha_data[DATA_ZHA_DISPATCHERS] = []
zha_data[DATA_ZHA_PLATFORM_LOADED] = []
- for component in COMPONENTS:
- coro = hass.config_entries.async_forward_entry_setup(config_entry, component)
+ for platform in PLATFORMS:
+ coro = hass.config_entries.async_forward_entry_setup(config_entry, platform)
zha_data[DATA_ZHA_PLATFORM_LOADED].append(hass.async_create_task(coro))
device_registry = await hass.helpers.device_registry.async_get_registry()
@@ -138,8 +137,8 @@ async def async_unload_entry(hass, config_entry):
for unsub_dispatcher in dispatchers:
unsub_dispatcher()
- for component in COMPONENTS:
- await hass.config_entries.async_forward_entry_unload(config_entry, component)
+ for platform in PLATFORMS:
+ await hass.config_entries.async_forward_entry_unload(config_entry, platform)
return True
diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py
index 91fe51f7793bdf..7e265d03c0940b 100644
--- a/homeassistant/components/zha/api.py
+++ b/homeassistant/components/zha/api.py
@@ -11,6 +11,7 @@
import zigpy.zdo.types as zdo_types
from homeassistant.components import websocket_api
+from homeassistant.const import ATTR_COMMAND, ATTR_NAME
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -20,14 +21,12 @@
ATTR_ATTRIBUTE,
ATTR_CLUSTER_ID,
ATTR_CLUSTER_TYPE,
- ATTR_COMMAND,
ATTR_COMMAND_TYPE,
ATTR_ENDPOINT_ID,
ATTR_IEEE,
ATTR_LEVEL,
ATTR_MANUFACTURER,
ATTR_MEMBERS,
- ATTR_NAME,
ATTR_VALUE,
ATTR_WARNING_DEVICE_DURATION,
ATTR_WARNING_DEVICE_MODE,
@@ -61,6 +60,7 @@
get_matched_clusters,
qr_to_install_code,
)
+from .core.typing import ZhaDeviceType, ZhaGatewayType
_LOGGER = logging.getLogger(__name__)
@@ -462,13 +462,55 @@ async def websocket_remove_group_members(hass, connection, msg):
)
async def websocket_reconfigure_node(hass, connection, msg):
"""Reconfigure a ZHA nodes entities by its ieee address."""
- zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
+ zha_gateway: ZhaGatewayType = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
ieee = msg[ATTR_IEEE]
- device = zha_gateway.get_device(ieee)
+ device: ZhaDeviceType = zha_gateway.get_device(ieee)
+ ieee_str = str(device.ieee)
+ nwk_str = device.nwk.__repr__()
+
+ class DeviceLogFilterer(logging.Filter):
+ """Log filterer that limits messages to the specified device."""
+
+ def filter(self, record):
+ message = record.getMessage()
+ return nwk_str in message or ieee_str in message
+
+ filterer = DeviceLogFilterer()
+
+ async def forward_messages(data):
+ """Forward events to websocket."""
+ connection.send_message(websocket_api.event_message(msg["id"], data))
+
+ remove_dispatcher_function = async_dispatcher_connect(
+ hass, "zha_gateway_message", forward_messages
+ )
+
+ @callback
+ def async_cleanup() -> None:
+ """Remove signal listener and turn off debug mode."""
+ zha_gateway.async_disable_debug_mode(filterer=filterer)
+ remove_dispatcher_function()
+
+ connection.subscriptions[msg["id"]] = async_cleanup
+ zha_gateway.async_enable_debug_mode(filterer=filterer)
+
_LOGGER.debug("Reconfiguring node with ieee_address: %s", ieee)
hass.async_create_task(device.async_configure())
+@websocket_api.require_admin
+@websocket_api.async_response
+@websocket_api.websocket_command(
+ {
+ vol.Required(TYPE): "zha/topology/update",
+ }
+)
+async def websocket_update_topology(hass, connection, msg):
+ """Update the ZHA network topology."""
+ zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
+ hass.async_create_task(zha_gateway.application_controller.topology.scan())
+
+
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command(
@@ -880,9 +922,12 @@ async def permit(service):
async def remove(service):
"""Remove a node from the network."""
ieee = service.data[ATTR_IEEE]
- zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
- zha_device = zha_gateway.get_device(ieee)
- if zha_device is not None and zha_device.is_coordinator:
+ zha_gateway: ZhaGatewayType = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
+ zha_device: ZhaDeviceType = zha_gateway.get_device(ieee)
+ if zha_device is not None and (
+ zha_device.is_coordinator
+ and zha_device.ieee == zha_gateway.application_controller.ieee
+ ):
_LOGGER.info("Removing the coordinator (%s) is not allowed", ieee)
return
_LOGGER.info("Removing node %s", ieee)
@@ -1143,6 +1188,7 @@ async def warning_device_warn(service):
websocket_api.async_register_command(hass, websocket_get_bindable_devices)
websocket_api.async_register_command(hass, websocket_bind_devices)
websocket_api.async_register_command(hass, websocket_unbind_devices)
+ websocket_api.async_register_command(hass, websocket_update_topology)
@callback
diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py
index 48f35e035f0c18..a0d8abc12330b1 100644
--- a/homeassistant/components/zha/binary_sensor.py
+++ b/homeassistant/components/zha/binary_sensor.py
@@ -1,6 +1,5 @@
"""Binary sensors on Zigbee Home Automation networks."""
import functools
-import logging
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_GAS,
@@ -32,8 +31,6 @@
from .core.registries import ZHA_ENTITIES
from .entity import ZhaEntity
-_LOGGER = logging.getLogger(__name__)
-
# Zigbee Cluster Library Zone Type to Home Assistant device class
CLASS_MAPPING = {
0x000D: DEVICE_CLASS_MOTION,
diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py
index ab0f15f7559515..475b0c5d0b89f9 100644
--- a/homeassistant/components/zha/climate.py
+++ b/homeassistant/components/zha/climate.py
@@ -4,11 +4,12 @@
For more details on this platform, please refer to the documentation
at https://home-assistant.io/components/zha.climate/
"""
+from __future__ import annotations
+
from datetime import datetime, timedelta
import enum
import functools
from random import randint
-from typing import List, Optional, Tuple
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
@@ -30,6 +31,9 @@
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
PRESET_AWAY,
+ PRESET_BOOST,
+ PRESET_COMFORT,
+ PRESET_ECO,
PRESET_NONE,
SUPPORT_FAN_MODE,
SUPPORT_PRESET_MODE,
@@ -48,6 +52,8 @@
CHANNEL_THERMOSTAT,
DATA_ZHA,
DATA_ZHA_DISPATCHERS,
+ PRESET_COMPLEX,
+ PRESET_SCHEDULE,
SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED,
)
@@ -185,7 +191,7 @@ def current_temperature(self):
return self._thrm.local_temp / ZCL_TEMP
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
data = {}
if self.hvac_mode:
@@ -212,7 +218,7 @@ def device_state_attributes(self):
return data
@property
- def fan_mode(self) -> Optional[str]:
+ def fan_mode(self) -> str | None:
"""Return current FAN mode."""
if self._thrm.running_state is None:
return FAN_AUTO
@@ -224,14 +230,14 @@ def fan_mode(self) -> Optional[str]:
return FAN_AUTO
@property
- def fan_modes(self) -> Optional[List[str]]:
+ def fan_modes(self) -> list[str] | None:
"""Return supported FAN modes."""
if not self._fan:
return None
return [FAN_AUTO, FAN_ON]
@property
- def hvac_action(self) -> Optional[str]:
+ def hvac_action(self) -> str | None:
"""Return the current HVAC action."""
if (
self._thrm.pi_heating_demand is None
@@ -241,7 +247,7 @@ def hvac_action(self) -> Optional[str]:
return self._pi_demand_action
@property
- def _rm_rs_action(self) -> Optional[str]:
+ def _rm_rs_action(self) -> str | None:
"""Return the current HVAC action based on running mode and running state."""
running_mode = self._thrm.running_mode
@@ -260,7 +266,7 @@ def _rm_rs_action(self) -> Optional[str]:
return CURRENT_HVAC_OFF
@property
- def _pi_demand_action(self) -> Optional[str]:
+ def _pi_demand_action(self) -> str | None:
"""Return the current HVAC action based on pi_demands."""
heating_demand = self._thrm.pi_heating_demand
@@ -275,12 +281,12 @@ def _pi_demand_action(self) -> Optional[str]:
return CURRENT_HVAC_OFF
@property
- def hvac_mode(self) -> Optional[str]:
+ def hvac_mode(self) -> str | None:
"""Return HVAC operation mode."""
return SYSTEM_MODE_2_HVAC.get(self._thrm.system_mode)
@property
- def hvac_modes(self) -> Tuple[str, ...]:
+ def hvac_modes(self) -> tuple[str, ...]:
"""Return the list of available HVAC operation modes."""
return SEQ_OF_OPERATION.get(self._thrm.ctrl_seqe_of_oper, (HVAC_MODE_OFF,))
@@ -290,12 +296,12 @@ def precision(self):
return PRECISION_TENTHS
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return current preset mode."""
return self._preset
@property
- def preset_modes(self) -> Optional[List[str]]:
+ def preset_modes(self) -> list[str] | None:
"""Return supported preset modes."""
return self._presets
@@ -442,15 +448,22 @@ async def async_set_preset_mode(self, preset_mode: str) -> None:
self.debug("preset mode '%s' is not supported", preset_mode)
return
- if self.preset_mode not in (preset_mode, PRESET_NONE):
- if not await self.async_preset_handler(self.preset_mode, enable=False):
- self.debug("Couldn't turn off '%s' preset", self.preset_mode)
- return
+ if (
+ self.preset_mode
+ not in (
+ preset_mode,
+ PRESET_NONE,
+ )
+ and not await self.async_preset_handler(self.preset_mode, enable=False)
+ ):
+ self.debug("Couldn't turn off '%s' preset", self.preset_mode)
+ return
- if preset_mode != PRESET_NONE:
- if not await self.async_preset_handler(preset_mode, enable=True):
- self.debug("Couldn't turn on '%s' preset", preset_mode)
- return
+ if preset_mode != PRESET_NONE and not await self.async_preset_handler(
+ preset_mode, enable=True
+ ):
+ self.debug("Couldn't turn on '%s' preset", preset_mode)
+ return
self._preset = preset_mode
self.async_write_ha_state()
@@ -566,7 +579,7 @@ class ZenWithinThermostat(Thermostat):
"""Zen Within Thermostat implementation."""
@property
- def _rm_rs_action(self) -> Optional[str]:
+ def _rm_rs_action(self) -> str | None:
"""Return the current HVAC action based on running mode and running state."""
running_state = self._thrm.running_state
@@ -594,3 +607,88 @@ def _rm_rs_action(self) -> Optional[str]:
)
class CentralitePearl(ZenWithinThermostat):
"""Centralite Pearl Thermostat implementation."""
+
+
+@STRICT_MATCH(
+ channel_names=CHANNEL_THERMOSTAT,
+ manufacturers={
+ "_TZE200_ckud7u2l",
+ "_TZE200_ywdxldoj",
+ "_TYST11_ckud7u2l",
+ "_TYST11_ywdxldoj",
+ },
+)
+class MoesThermostat(Thermostat):
+ """Moes Thermostat implementation."""
+
+ def __init__(self, unique_id, zha_device, channels, **kwargs):
+ """Initialize ZHA Thermostat instance."""
+ super().__init__(unique_id, zha_device, channels, **kwargs)
+ self._presets = [
+ PRESET_NONE,
+ PRESET_AWAY,
+ PRESET_SCHEDULE,
+ PRESET_COMFORT,
+ PRESET_ECO,
+ PRESET_BOOST,
+ PRESET_COMPLEX,
+ ]
+ self._supported_flags |= SUPPORT_PRESET_MODE
+
+ @property
+ def hvac_modes(self) -> tuple[str, ...]:
+ """Return only the heat mode, because the device can't be turned off."""
+ return (HVAC_MODE_HEAT,)
+
+ async def async_attribute_updated(self, record):
+ """Handle attribute update from device."""
+ if record.attr_name == "operation_preset":
+ if record.value == 0:
+ self._preset = PRESET_AWAY
+ if record.value == 1:
+ self._preset = PRESET_SCHEDULE
+ if record.value == 2:
+ self._preset = PRESET_NONE
+ if record.value == 3:
+ self._preset = PRESET_COMFORT
+ if record.value == 4:
+ self._preset = PRESET_ECO
+ if record.value == 5:
+ self._preset = PRESET_BOOST
+ if record.value == 6:
+ self._preset = PRESET_COMPLEX
+ await super().async_attribute_updated(record)
+
+ async def async_preset_handler(self, preset: str, enable: bool = False) -> bool:
+ """Set the preset mode."""
+ mfg_code = self._zha_device.manufacturer_code
+ if not enable:
+ return await self._thrm.write_attributes(
+ {"operation_preset": 2}, manufacturer=mfg_code
+ )
+ if preset == PRESET_AWAY:
+ return await self._thrm.write_attributes(
+ {"operation_preset": 0}, manufacturer=mfg_code
+ )
+ if preset == PRESET_SCHEDULE:
+ return await self._thrm.write_attributes(
+ {"operation_preset": 1}, manufacturer=mfg_code
+ )
+ if preset == PRESET_COMFORT:
+ return await self._thrm.write_attributes(
+ {"operation_preset": 3}, manufacturer=mfg_code
+ )
+ if preset == PRESET_ECO:
+ return await self._thrm.write_attributes(
+ {"operation_preset": 4}, manufacturer=mfg_code
+ )
+ if preset == PRESET_BOOST:
+ return await self._thrm.write_attributes(
+ {"operation_preset": 5}, manufacturer=mfg_code
+ )
+ if preset == PRESET_COMPLEX:
+ return await self._thrm.write_attributes(
+ {"operation_preset": 6}, manufacturer=mfg_code
+ )
+
+ return False
diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py
index 473d39c6f7a139..9c440c29cd310c 100644
--- a/homeassistant/components/zha/config_flow.py
+++ b/homeassistant/components/zha/config_flow.py
@@ -1,14 +1,18 @@
"""Config flow for ZHA."""
+from __future__ import annotations
+
import os
-from typing import Any, Dict, Optional
+from typing import Any
import serial.tools.list_ports
import voluptuous as vol
from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH
from homeassistant import config_entries
+from homeassistant.const import CONF_HOST, CONF_NAME
+from homeassistant.helpers.typing import DiscoveryInfoType
-from .core.const import ( # pylint:disable=unused-import
+from .core.const import (
CONF_BAUDRATE,
CONF_FLOWCONTROL,
CONF_RADIO_TYPE,
@@ -45,6 +49,10 @@ async def async_step_user(self, user_input=None):
+ (f" - {p.manufacturer}" if p.manufacturer else "")
for p in ports
]
+
+ if not list_of_ports:
+ return await self.async_step_pick_radio()
+
list_of_ports.append(CONF_MANUAL_PATH)
if user_input is not None:
@@ -85,6 +93,37 @@ async def async_step_pick_radio(self, user_input=None):
data_schema=vol.Schema(schema),
)
+ async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType):
+ """Handle zeroconf discovery."""
+ # Hostname is format: livingroom.local.
+ local_name = discovery_info["hostname"][:-1]
+ node_name = local_name[: -len(".local")]
+ host = discovery_info[CONF_HOST]
+ device_path = f"socket://{host}:6638"
+
+ await self.async_set_unique_id(node_name)
+ self._abort_if_unique_id_configured(
+ updates={
+ CONF_DEVICE: {CONF_DEVICE_PATH: device_path},
+ }
+ )
+
+ # Check if already configured
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
+ # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
+ self.context["title_placeholders"] = {
+ CONF_NAME: node_name,
+ }
+
+ self._device_path = device_path
+ self._radio_type = (
+ RadioType.ezsp.name if "efr32" in local_name else RadioType.znp.name
+ )
+
+ return await self.async_step_port_config()
+
async def async_step_port_config(self, user_input=None):
"""Enter port settings specific for this type of radio."""
errors = {}
@@ -112,9 +151,13 @@ async def async_step_port_config(self, user_input=None):
if isinstance(radio_schema, vol.Schema):
radio_schema = radio_schema.schema
+ # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
+ source = self.context.get("source")
for param, value in radio_schema.items():
if param in SUPPORTED_PORT_SETTINGS:
schema[param] = value
+ if source == config_entries.SOURCE_ZEROCONF and param == CONF_BAUDRATE:
+ schema[param] = 115200
return self.async_show_form(
step_id="port_config",
@@ -123,7 +166,7 @@ async def async_step_port_config(self, user_input=None):
)
-async def detect_radios(dev_path: str) -> Optional[Dict[str, Any]]:
+async def detect_radios(dev_path: str) -> dict[str, Any] | None:
"""Probe all radio types on the device port."""
for radio in RadioType:
dev_config = radio.controller.SCHEMA_DEVICE({CONF_DEVICE_PATH: dev_path})
diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py
index 1bd8a52b6e6b08..289f1c36d4df18 100644
--- a/homeassistant/components/zha/core/channels/__init__.py
+++ b/homeassistant/components/zha/core/channels/__init__.py
@@ -1,6 +1,8 @@
"""Channels module for Zigbee Home Automation."""
+from __future__ import annotations
+
import asyncio
-from typing import Any, Dict, List, Optional, Tuple, Union
+from typing import Any, Dict
import zigpy.zcl.clusters.closures
@@ -8,7 +10,7 @@
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
-from . import ( # noqa: F401 # pylint: disable=unused-import
+from . import ( # noqa: F401
base,
closures,
general,
@@ -38,7 +40,7 @@ class Channels:
def __init__(self, zha_device: zha_typing.ZhaDeviceType) -> None:
"""Initialize instance."""
- self._pools: List[zha_typing.ChannelPoolType] = []
+ self._pools: list[zha_typing.ChannelPoolType] = []
self._power_config = None
self._identify = None
self._semaphore = asyncio.Semaphore(3)
@@ -47,7 +49,7 @@ def __init__(self, zha_device: zha_typing.ZhaDeviceType) -> None:
self._zha_device = zha_device
@property
- def pools(self) -> List["ChannelPool"]:
+ def pools(self) -> list[ChannelPool]:
"""Return channel pools list."""
return self._pools
@@ -94,7 +96,7 @@ def unique_id(self):
return self._unique_id
@property
- def zigbee_signature(self) -> Dict[int, Dict[str, Any]]:
+ def zigbee_signature(self) -> dict[int, dict[str, Any]]:
"""Get the zigbee signatures for the pools in channels."""
return {
signature[0]: signature[1]
@@ -102,7 +104,7 @@ def zigbee_signature(self) -> Dict[int, Dict[str, Any]]:
}
@classmethod
- def new(cls, zha_device: zha_typing.ZhaDeviceType) -> "Channels":
+ def new(cls, zha_device: zha_typing.ZhaDeviceType) -> Channels:
"""Create new instance."""
channels = cls(zha_device)
for ep_id in sorted(zha_device.device.endpoints):
@@ -135,7 +137,7 @@ def async_new_entity(
component: str,
entity_class: zha_typing.CALLABLE_T,
unique_id: str,
- channels: List[zha_typing.ChannelType],
+ channels: list[zha_typing.ChannelType],
):
"""Signal new entity addition."""
if self.zha_device.status == zha_core_device.DeviceStatus.INITIALIZED:
@@ -151,7 +153,7 @@ def async_send_signal(self, signal: str, *args: Any) -> None:
async_dispatcher_send(self.zha_device.hass, signal, *args)
@callback
- def zha_send_event(self, event_data: Dict[str, Union[str, int]]) -> None:
+ def zha_send_event(self, event_data: dict[str, str | int]) -> None:
"""Relay events to hass."""
self.zha_device.hass.bus.async_fire(
"zha_event",
@@ -173,7 +175,7 @@ def __init__(self, channels: Channels, ep_id: int):
self._channels: Channels = channels
self._claimed_channels: ChannelsDict = {}
self._id: int = ep_id
- self._client_channels: Dict[str, zha_typing.ClientChannelType] = {}
+ self._client_channels: dict[str, zha_typing.ClientChannelType] = {}
self._unique_id: str = f"{channels.unique_id}-{ep_id}"
@property
@@ -187,7 +189,7 @@ def claimed_channels(self) -> ChannelsDict:
return self._claimed_channels
@property
- def client_channels(self) -> Dict[str, zha_typing.ClientChannelType]:
+ def client_channels(self) -> dict[str, zha_typing.ClientChannelType]:
"""Return a dict of client channels."""
return self._client_channels
@@ -212,12 +214,12 @@ def is_mains_powered(self) -> bool:
return self._channels.zha_device.is_mains_powered
@property
- def manufacturer(self) -> Optional[str]:
+ def manufacturer(self) -> str | None:
"""Return device manufacturer."""
return self._channels.zha_device.manufacturer
@property
- def manufacturer_code(self) -> Optional[int]:
+ def manufacturer_code(self) -> int | None:
"""Return device manufacturer."""
return self._channels.zha_device.manufacturer_code
@@ -227,7 +229,7 @@ def hass(self):
return self._channels.zha_device.hass
@property
- def model(self) -> Optional[str]:
+ def model(self) -> str | None:
"""Return device model."""
return self._channels.zha_device.model
@@ -242,7 +244,7 @@ def unique_id(self):
return self._unique_id
@property
- def zigbee_signature(self) -> Tuple[int, Dict[str, Any]]:
+ def zigbee_signature(self) -> tuple[int, dict[str, Any]]:
"""Get the zigbee signature for the endpoint this pool represents."""
return (
self.endpoint.endpoint_id,
@@ -263,7 +265,7 @@ def zigbee_signature(self) -> Tuple[int, Dict[str, Any]]:
)
@classmethod
- def new(cls, channels: Channels, ep_id: int) -> "ChannelPool":
+ def new(cls, channels: Channels, ep_id: int) -> ChannelPool:
"""Create new channels for an endpoint."""
pool = cls(channels, ep_id)
pool.add_all_channels()
@@ -340,7 +342,7 @@ def async_new_entity(
component: str,
entity_class: zha_typing.CALLABLE_T,
unique_id: str,
- channels: List[zha_typing.ChannelType],
+ channels: list[zha_typing.ChannelType],
):
"""Signal new entity addition."""
self._channels.async_new_entity(component, entity_class, unique_id, channels)
@@ -351,19 +353,19 @@ def async_send_signal(self, signal: str, *args: Any) -> None:
self._channels.async_send_signal(signal, *args)
@callback
- def claim_channels(self, channels: List[zha_typing.ChannelType]) -> None:
+ def claim_channels(self, channels: list[zha_typing.ChannelType]) -> None:
"""Claim a channel."""
self.claimed_channels.update({ch.id: ch for ch in channels})
@callback
- def unclaimed_channels(self) -> List[zha_typing.ChannelType]:
+ def unclaimed_channels(self) -> list[zha_typing.ChannelType]:
"""Return a list of available (unclaimed) channels."""
claimed = set(self.claimed_channels)
available = set(self.all_channels)
return [self.all_channels[chan_id] for chan_id in (available - claimed)]
@callback
- def zha_send_event(self, event_data: Dict[str, Union[str, int]]) -> None:
+ def zha_send_event(self, event_data: dict[str, str | int]) -> None:
"""Relay events to hass."""
self._channels.zha_send_event(
{
diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py
index 2dbd1629487a31..bc93459dbad419 100644
--- a/homeassistant/components/zha/core/channels/base.py
+++ b/homeassistant/components/zha/core/channels/base.py
@@ -1,13 +1,15 @@
"""Base classes for channels."""
+from __future__ import annotations
import asyncio
from enum import Enum
from functools import wraps
import logging
-from typing import Any, Union
+from typing import Any
import zigpy.exceptions
+from homeassistant.const import ATTR_COMMAND
from homeassistant.core import callback
from .. import typing as zha_typing
@@ -16,7 +18,6 @@
ATTR_ATTRIBUTE_ID,
ATTR_ATTRIBUTE_NAME,
ATTR_CLUSTER_ID,
- ATTR_COMMAND,
ATTR_UNIQUE_ID,
ATTR_VALUE,
CHANNEL_ZDO,
@@ -238,7 +239,7 @@ def zdo_command(self, *args, **kwargs):
"""Handle ZDO commands on this cluster."""
@callback
- def zha_send_event(self, command: str, args: Union[int, dict]) -> None:
+ def zha_send_event(self, command: str, args: int | dict) -> None:
"""Relay events to hass."""
self._ch_pool.zha_send_event(
{
diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py
index 0326f18ac69f38..e427bc962b4cea 100644
--- a/homeassistant/components/zha/core/channels/closures.py
+++ b/homeassistant/components/zha/core/channels/closures.py
@@ -23,6 +23,27 @@ async def async_update(self):
f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", 0, "lock_state", result
)
+ @callback
+ def cluster_command(self, tsn, command_id, args):
+ """Handle a cluster command received on this cluster."""
+
+ if (
+ self._cluster.client_commands is None
+ or self._cluster.client_commands.get(command_id) is None
+ ):
+ return
+
+ command_name = self._cluster.client_commands.get(command_id, [command_id])[0]
+ if command_name == "operation_event_notification":
+ self.zha_send_event(
+ command_name,
+ {
+ "source": args[0].name,
+ "operation": args[1].name,
+ "code_slot": (args[2] + 1), # start code slots at 1
+ },
+ )
+
@callback
def attribute_updated(self, attrid, value):
"""Handle attribute update from lock cluster."""
@@ -35,6 +56,53 @@ def attribute_updated(self, attrid, value):
f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, attr_name, value
)
+ async def async_set_user_code(self, code_slot: int, user_code: str) -> None:
+ """Set the user code for the code slot."""
+
+ await self.set_pin_code(
+ code_slot - 1, # start code slots at 1, Zigbee internals use 0
+ closures.DoorLock.UserStatus.Enabled,
+ closures.DoorLock.UserType.Unrestricted,
+ user_code,
+ )
+
+ async def async_enable_user_code(self, code_slot: int) -> None:
+ """Enable the code slot."""
+
+ await self.set_user_status(code_slot - 1, closures.DoorLock.UserStatus.Enabled)
+
+ async def async_disable_user_code(self, code_slot: int) -> None:
+ """Disable the code slot."""
+
+ await self.set_user_status(code_slot - 1, closures.DoorLock.UserStatus.Disabled)
+
+ async def async_get_user_code(self, code_slot: int) -> int:
+ """Get the user code from the code slot."""
+
+ result = await self.get_pin_code(code_slot - 1)
+ return result
+
+ async def async_clear_user_code(self, code_slot: int) -> None:
+ """Clear the code slot."""
+
+ await self.clear_pin_code(code_slot - 1)
+
+ async def async_clear_all_user_codes(self) -> None:
+ """Clear all code slots."""
+
+ await self.clear_all_pin_codes()
+
+ async def async_set_user_type(self, code_slot: int, user_type: str) -> None:
+ """Set user type."""
+
+ await self.set_user_type(code_slot - 1, user_type)
+
+ async def async_get_user_type(self, code_slot: int) -> str:
+ """Get user type."""
+
+ result = await self.get_user_type(code_slot - 1)
+ return result
+
@registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.Shade.cluster_id)
class Shade(ZigbeeChannel):
diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py
index d105572c182a23..6ef0bd9e66515d 100644
--- a/homeassistant/components/zha/core/channels/general.py
+++ b/homeassistant/components/zha/core/channels/general.py
@@ -1,6 +1,8 @@
"""General channels module for Zigbee Home Automation."""
+from __future__ import annotations
+
import asyncio
-from typing import Any, Coroutine, List, Optional
+from typing import Any, Coroutine
import zigpy.exceptions
import zigpy.zcl.clusters.general as general
@@ -44,42 +46,42 @@ class AnalogOutput(ZigbeeChannel):
REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}]
@property
- def present_value(self) -> Optional[float]:
+ def present_value(self) -> float | None:
"""Return cached value of present_value."""
return self.cluster.get("present_value")
@property
- def min_present_value(self) -> Optional[float]:
+ def min_present_value(self) -> float | None:
"""Return cached value of min_present_value."""
return self.cluster.get("min_present_value")
@property
- def max_present_value(self) -> Optional[float]:
+ def max_present_value(self) -> float | None:
"""Return cached value of max_present_value."""
return self.cluster.get("max_present_value")
@property
- def resolution(self) -> Optional[float]:
+ def resolution(self) -> float | None:
"""Return cached value of resolution."""
return self.cluster.get("resolution")
@property
- def relinquish_default(self) -> Optional[float]:
+ def relinquish_default(self) -> float | None:
"""Return cached value of relinquish_default."""
return self.cluster.get("relinquish_default")
@property
- def description(self) -> Optional[str]:
+ def description(self) -> str | None:
"""Return cached value of description."""
return self.cluster.get("description")
@property
- def engineering_units(self) -> Optional[int]:
+ def engineering_units(self) -> int | None:
"""Return cached value of engineering_units."""
return self.cluster.get("engineering_units")
@property
- def application_type(self) -> Optional[int]:
+ def application_type(self) -> int | None:
"""Return cached value of application_type."""
return self.cluster.get("application_type")
@@ -91,7 +93,7 @@ async def async_set_present_value(self, value: float) -> bool:
self.error("Could not set value: %s", ex)
return False
if isinstance(res, list) and all(
- [record.status == Status.SUCCESS for record in res[0]]
+ record.status == Status.SUCCESS for record in res[0]
):
return True
return False
@@ -215,7 +217,7 @@ class LevelControlChannel(ZigbeeChannel):
REPORT_CONFIG = ({"attr": "current_level", "config": REPORT_CONFIG_ASAP},)
@property
- def current_level(self) -> Optional[int]:
+ def current_level(self) -> int | None:
"""Return cached value of the current_level attribute."""
return self.cluster.get("current_level")
@@ -293,7 +295,7 @@ def __init__(
self._off_listener = None
@property
- def on_off(self) -> Optional[bool]:
+ def on_off(self) -> bool | None:
"""Return cached value of on/off attribute."""
return self.cluster.get("on_off")
@@ -367,7 +369,7 @@ class Ota(ZigbeeChannel):
@callback
def cluster_command(
- self, tsn: int, command_id: int, args: Optional[List[Any]]
+ self, tsn: int, command_id: int, args: list[Any] | None
) -> None:
"""Handle OTA commands."""
cmd_name = self.cluster.server_commands.get(command_id, [command_id])[0]
@@ -389,6 +391,9 @@ class PollControl(ZigbeeChannel):
CHECKIN_INTERVAL = 55 * 60 * 4 # 55min
CHECKIN_FAST_POLL_TIMEOUT = 2 * 4 # 2s
LONG_POLL = 6 * 4 # 6s
+ _IGNORED_MANUFACTURER_ID = {
+ 4476,
+ } # IKEA
async def async_configure_channel_specific(self) -> None:
"""Configure channel: set check-in interval."""
@@ -402,7 +407,7 @@ async def async_configure_channel_specific(self) -> None:
@callback
def cluster_command(
- self, tsn: int, command_id: int, args: Optional[List[Any]]
+ self, tsn: int, command_id: int, args: list[Any] | None
) -> None:
"""Handle commands received to this cluster."""
cmd_name = self.cluster.client_commands.get(command_id, [command_id])[0]
@@ -414,7 +419,13 @@ def cluster_command(
async def check_in_response(self, tsn: int) -> None:
"""Respond to checkin command."""
await self.checkin_response(True, self.CHECKIN_FAST_POLL_TIMEOUT, tsn=tsn)
- await self.set_long_poll_interval(self.LONG_POLL)
+ if self._ch_pool.manufacturer_code not in self._IGNORED_MANUFACTURER_ID:
+ await self.set_long_poll_interval(self.LONG_POLL)
+
+ @callback
+ def skip_manufacturer_id(self, manufacturer_code: int) -> None:
+ """Block a specific manufacturer id from changing default polling."""
+ self._IGNORED_MANUFACTURER_ID.add(manufacturer_code)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerConfiguration.cluster_id)
diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py
index 5b3a4778fcd842..989cc17f97de0b 100644
--- a/homeassistant/components/zha/core/channels/homeautomation.py
+++ b/homeassistant/components/zha/core/channels/homeautomation.py
@@ -1,5 +1,7 @@
"""Home automation channels module for Zigbee Home Automation."""
-from typing import Coroutine, Optional
+from __future__ import annotations
+
+from typing import Coroutine
import zigpy.zcl.clusters.homeautomation as homeautomation
@@ -76,14 +78,14 @@ def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine:
)
@property
- def divisor(self) -> Optional[int]:
+ def divisor(self) -> int | None:
"""Return active power divisor."""
return self.cluster.get(
"ac_power_divisor", self.cluster.get("power_divisor", 1)
)
@property
- def multiplier(self) -> Optional[int]:
+ def multiplier(self) -> int | None:
"""Return active power divisor."""
return self.cluster.get(
"ac_power_multiplier", self.cluster.get("power_multiplier", 1)
diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py
index 1647c5ce52d96b..76f9c0b4e807b1 100644
--- a/homeassistant/components/zha/core/channels/hvac.py
+++ b/homeassistant/components/zha/core/channels/hvac.py
@@ -4,9 +4,11 @@
For more details about this component, please refer to the documentation at
https://home-assistant.io/integrations/zha/
"""
+from __future__ import annotations
+
import asyncio
from collections import namedtuple
-from typing import Any, Dict, List, Optional, Tuple, Union
+from typing import Any
from zigpy.exceptions import ZigbeeException
import zigpy.zcl.clusters.hvac as hvac
@@ -44,7 +46,7 @@ class FanChannel(ZigbeeChannel):
REPORT_CONFIG = ({"attr": "fan_mode", "config": REPORT_CONFIG_OP},)
@property
- def fan_mode(self) -> Optional[int]:
+ def fan_mode(self) -> int | None:
"""Return current fan mode."""
return self.cluster.get("fan_mode")
@@ -198,22 +200,22 @@ def min_heat_setpoint_limit(self) -> int:
return self._min_heat_setpoint_limit
@property
- def local_temp(self) -> Optional[int]:
+ def local_temp(self) -> int | None:
"""Thermostat temperature."""
return self._local_temp
@property
- def occupancy(self) -> Optional[int]:
+ def occupancy(self) -> int | None:
"""Is occupancy detected."""
return self._occupancy
@property
- def occupied_cooling_setpoint(self) -> Optional[int]:
+ def occupied_cooling_setpoint(self) -> int | None:
"""Temperature when room is occupied."""
return self._occupied_cooling_setpoint
@property
- def occupied_heating_setpoint(self) -> Optional[int]:
+ def occupied_heating_setpoint(self) -> int | None:
"""Temperature when room is occupied."""
return self._occupied_heating_setpoint
@@ -228,27 +230,27 @@ def pi_heating_demand(self) -> int:
return self._pi_heating_demand
@property
- def running_mode(self) -> Optional[int]:
+ def running_mode(self) -> int | None:
"""Thermostat running mode."""
return self._running_mode
@property
- def running_state(self) -> Optional[int]:
+ def running_state(self) -> int | None:
"""Thermostat running state, state of heat, cool, fan relays."""
return self._running_state
@property
- def system_mode(self) -> Optional[int]:
+ def system_mode(self) -> int | None:
"""System mode."""
return self._system_mode
@property
- def unoccupied_cooling_setpoint(self) -> Optional[int]:
+ def unoccupied_cooling_setpoint(self) -> int | None:
"""Temperature when room is not occupied."""
return self._unoccupied_cooling_setpoint
@property
- def unoccupied_heating_setpoint(self) -> Optional[int]:
+ def unoccupied_heating_setpoint(self) -> int | None:
"""Temperature when room is not occupied."""
return self._unoccupied_heating_setpoint
@@ -309,7 +311,7 @@ async def configure_reporting(self):
chunk, rest = rest[:4], rest[4:]
def _configure_reporting_status(
- self, attrs: Dict[Union[int, str], Tuple], res: Union[List, Tuple]
+ self, attrs: dict[int | str, tuple], res: list | tuple
) -> None:
"""Parse configure reporting result."""
if not isinstance(res, list):
@@ -405,7 +407,7 @@ async def async_set_cooling_setpoint(
self.debug("set cooling setpoint to %s", temperature)
return True
- async def get_occupancy(self) -> Optional[bool]:
+ async def get_occupancy(self) -> bool | None:
"""Get unreportable occupancy attribute."""
try:
res, fail = await self.cluster.read_attributes(["occupancy"])
@@ -434,7 +436,7 @@ def check_result(res: list) -> bool:
if not isinstance(res, list):
return False
- return all([record.status == Status.SUCCESS for record in res[0]])
+ return all(record.status == Status.SUCCESS for record in res[0])
@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.UserInterface.cluster_id)
diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py
index c8827e20e01a90..eef4c56e3794a8 100644
--- a/homeassistant/components/zha/core/channels/lighting.py
+++ b/homeassistant/components/zha/core/channels/lighting.py
@@ -1,5 +1,8 @@
"""Lighting channels module for Zigbee Home Automation."""
-from typing import Coroutine, Optional
+from __future__ import annotations
+
+from contextlib import suppress
+from typing import Coroutine
import zigpy.zcl.clusters.lighting as lighting
@@ -37,31 +40,29 @@ class ColorChannel(ZigbeeChannel):
@property
def color_capabilities(self) -> int:
"""Return color capabilities of the light."""
- try:
+ with suppress(KeyError):
return self.cluster["color_capabilities"]
- except KeyError:
- pass
if self.cluster.get("color_temperature") is not None:
return self.CAPABILITIES_COLOR_XY | self.CAPABILITIES_COLOR_TEMP
return self.CAPABILITIES_COLOR_XY
@property
- def color_loop_active(self) -> Optional[int]:
+ def color_loop_active(self) -> int | None:
"""Return cached value of the color_loop_active attribute."""
return self.cluster.get("color_loop_active")
@property
- def color_temperature(self) -> Optional[int]:
+ def color_temperature(self) -> int | None:
"""Return cached value of color temperature."""
return self.cluster.get("color_temperature")
@property
- def current_x(self) -> Optional[int]:
+ def current_x(self) -> int | None:
"""Return cached value of the current_x attribute."""
return self.cluster.get("current_x")
@property
- def current_y(self) -> Optional[int]:
+ def current_y(self) -> int | None:
"""Return cached value of the current_y attribute."""
return self.cluster.get("current_y")
diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py
index 64db1aa82ac98a..78ff12a9bf300e 100644
--- a/homeassistant/components/zha/core/channels/measurement.py
+++ b/homeassistant/components/zha/core/channels/measurement.py
@@ -74,3 +74,31 @@ class TemperatureMeasurement(ZigbeeChannel):
"config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50),
}
]
+
+
+@registries.ZIGBEE_CHANNEL_REGISTRY.register(
+ measurement.CarbonMonoxideConcentration.cluster_id
+)
+class CarbonMonoxideConcentration(ZigbeeChannel):
+ """Carbon Monoxide measurement channel."""
+
+ REPORT_CONFIG = [
+ {
+ "attr": "measured_value",
+ "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.000001),
+ }
+ ]
+
+
+@registries.ZIGBEE_CHANNEL_REGISTRY.register(
+ measurement.CarbonDioxideConcentration.cluster_id
+)
+class CarbonDioxideConcentration(ZigbeeChannel):
+ """Carbon Dioxide measurement channel."""
+
+ REPORT_CONFIG = [
+ {
+ "attr": "measured_value",
+ "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.000001),
+ }
+ ]
diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py
index 32e4902799e338..a815c75c8b36ed 100644
--- a/homeassistant/components/zha/core/channels/smartenergy.py
+++ b/homeassistant/components/zha/core/channels/smartenergy.py
@@ -1,5 +1,7 @@
"""Smart energy channels module for Zigbee Home Automation."""
-from typing import Coroutine, Union
+from __future__ import annotations
+
+from typing import Coroutine
import zigpy.zcl.clusters.smartenergy as smartenergy
@@ -139,7 +141,7 @@ async def fetch_config(self, from_cache: bool) -> None:
else:
self._format_spec = "{:0" + str(width) + "." + str(r_digits) + "f}"
- def formatter_function(self, value: int) -> Union[int, float]:
+ def formatter_function(self, value: int) -> int | float:
"""Return formatted value for display."""
value = value * self.multiplier / self.divisor
if self.unit_of_measurement == POWER_WATT:
diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py
index 1d3f767353b04f..2c968a5f02d1aa 100644
--- a/homeassistant/components/zha/core/const.py
+++ b/homeassistant/components/zha/core/const.py
@@ -1,7 +1,8 @@
"""All constants related to the ZHA component."""
+from __future__ import annotations
+
import enum
import logging
-from typing import List
import bellows.zigbee.application
from zigpy.config import CONF_DEVICE_PATH # noqa: F401 # pylint: disable=unused-import
@@ -31,7 +32,6 @@
ATTR_AVAILABLE = "available"
ATTR_CLUSTER_ID = "cluster_id"
ATTR_CLUSTER_TYPE = "cluster_type"
-ATTR_COMMAND = "command"
ATTR_COMMAND_TYPE = "command_type"
ATTR_DEVICE_IEEE = "device_ieee"
ATTR_DEVICE_TYPE = "device_type"
@@ -47,7 +47,6 @@
ATTR_MANUFACTURER_CODE = "manufacturer_code"
ATTR_MEMBERS = "members"
ATTR_MODEL = "model"
-ATTR_NAME = "name"
ATTR_NEIGHBORS = "neighbors"
ATTR_NODE_DESCRIPTOR = "node_descriptor"
ATTR_NWK = "nwk"
@@ -104,7 +103,7 @@
CLUSTER_TYPE_IN = "in"
CLUSTER_TYPE_OUT = "out"
-COMPONENTS = (
+PLATFORMS = (
BINARY_SENSOR,
CLIMATE,
COVER,
@@ -174,6 +173,9 @@
POWER_MAINS_POWERED = "Mains"
POWER_BATTERY_OR_UNKNOWN = "Battery or Unknown"
+PRESET_SCHEDULE = "schedule"
+PRESET_COMPLEX = "complex"
+
class RadioType(enum.Enum):
"""Possible options for radio type."""
@@ -204,7 +206,7 @@ class RadioType(enum.Enum):
)
@classmethod
- def list(cls) -> List[str]:
+ def list(cls) -> list[str]:
"""Return a list of descriptions."""
return [e.description for e in RadioType]
diff --git a/homeassistant/components/zha/core/decorators.py b/homeassistant/components/zha/core/decorators.py
index c416548dbe97e4..c3eec07e980cec 100644
--- a/homeassistant/components/zha/core/decorators.py
+++ b/homeassistant/components/zha/core/decorators.py
@@ -1,5 +1,7 @@
"""Decorators for ZHA core registries."""
-from typing import Callable, TypeVar, Union
+from __future__ import annotations
+
+from typing import Callable, TypeVar
CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name
@@ -8,7 +10,7 @@ class DictRegistry(dict):
"""Dict Registry of items."""
def register(
- self, name: Union[int, str], item: Union[str, CALLABLE_T] = None
+ self, name: int | str, item: str | CALLABLE_T = None
) -> Callable[[CALLABLE_T], CALLABLE_T]:
"""Return decorator to register item with a specific name."""
@@ -26,7 +28,7 @@ def decorator(channel: CALLABLE_T) -> CALLABLE_T:
class SetRegistry(set):
"""Set Registry of items."""
- def register(self, name: Union[int, str]) -> Callable[[CALLABLE_T], CALLABLE_T]:
+ def register(self, name: int | str) -> Callable[[CALLABLE_T], CALLABLE_T]:
"""Return decorator to register item with a specific name."""
def decorator(channel: CALLABLE_T) -> CALLABLE_T:
diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py
index cd3b1bd93cec39..65605b2f7a3ff6 100644
--- a/homeassistant/components/zha/core/device.py
+++ b/homeassistant/components/zha/core/device.py
@@ -1,11 +1,13 @@
"""Device for Zigbee Home Automation."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
from enum import Enum
import logging
import random
import time
-from typing import Any, Dict
+from typing import Any
from zigpy import types
import zigpy.exceptions
@@ -14,6 +16,7 @@
from zigpy.zcl.clusters.general import Groups
import zigpy.zdo.types as zdo_types
+from homeassistant.const import ATTR_COMMAND, ATTR_NAME
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
@@ -28,7 +31,6 @@
ATTR_ATTRIBUTE,
ATTR_AVAILABLE,
ATTR_CLUSTER_ID,
- ATTR_COMMAND,
ATTR_COMMAND_TYPE,
ATTR_DEVICE_TYPE,
ATTR_ENDPOINT_ID,
@@ -40,7 +42,6 @@
ATTR_MANUFACTURER,
ATTR_MANUFACTURER_CODE,
ATTR_MODEL,
- ATTR_NAME,
ATTR_NEIGHBORS,
ATTR_NODE_DESCRIPTOR,
ATTR_NWK,
@@ -276,7 +277,7 @@ def available(self, new_availability: bool) -> None:
self._available = new_availability
@property
- def zigbee_signature(self) -> Dict[str, Any]:
+ def zigbee_signature(self) -> dict[str, Any]:
"""Get zigbee signature for this device."""
return {
ATTR_NODE_DESCRIPTOR: str(self._zigpy_device.node_desc),
diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py
index e071a523321db0..338796acffe0a0 100644
--- a/homeassistant/components/zha/core/discovery.py
+++ b/homeassistant/components/zha/core/discovery.py
@@ -1,8 +1,9 @@
"""Device discovery functions for Zigbee Home Automation."""
+from __future__ import annotations
from collections import Counter
import logging
-from typing import Callable, List, Tuple
+from typing import Callable
from homeassistant import const as ha_const
from homeassistant.core import callback
@@ -34,10 +35,10 @@
@callback
async def async_add_entities(
_async_add_entities: Callable,
- entities: List[
- Tuple[
+ entities: list[
+ tuple[
zha_typing.ZhaEntityType,
- Tuple[str, zha_typing.ZhaDeviceType, List[zha_typing.ChannelType]],
+ tuple[str, zha_typing.ZhaDeviceType, list[zha_typing.ChannelType]],
]
],
update_before_add: bool = True,
@@ -75,7 +76,7 @@ def discover_by_device_type(self, channel_pool: zha_typing.ChannelPoolType) -> N
ep_device_type = channel_pool.endpoint.device_type
component = zha_regs.DEVICE_CLASS[ep_profile_id].get(ep_device_type)
- if component and component in zha_const.COMPONENTS:
+ if component and component in zha_const.PLATFORMS:
channels = channel_pool.unclaimed_channels()
entity_class, claimed = zha_regs.ZHA_ENTITIES.get_entity(
component, channel_pool.manufacturer, channel_pool.model, channels
@@ -122,7 +123,7 @@ def probe_single_cluster(
ep_channels: zha_typing.ChannelPoolType,
) -> None:
"""Probe specified cluster for specific component."""
- if component is None or component not in zha_const.COMPONENTS:
+ if component is None or component not in zha_const.PLATFORMS:
return
channel_list = [channel]
unique_id = f"{ep_channels.unique_id}-{channel.cluster.cluster_id}"
@@ -235,9 +236,9 @@ def discover_group_entities(self, group: zha_typing.ZhaGroupType) -> None:
@staticmethod
def determine_entity_domains(
hass: HomeAssistantType, group: zha_typing.ZhaGroupType
- ) -> List[str]:
+ ) -> list[str]:
"""Determine the entity domains for this group."""
- entity_domains: List[str] = []
+ entity_domains: list[str] = []
zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY]
all_domain_occurrences = []
for member in group.members:
diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py
index c57c726972306a..de65ed6695e15c 100644
--- a/homeassistant/components/zha/core/gateway.py
+++ b/homeassistant/components/zha/core/gateway.py
@@ -1,4 +1,5 @@
"""Virtual gateway for Zigbee Home Automation."""
+from __future__ import annotations
import asyncio
import collections
@@ -9,7 +10,6 @@
import os
import time
import traceback
-from typing import List, Optional
from serial import SerialException
from zigpy.config import CONF_DEVICE
@@ -347,7 +347,8 @@ async def _async_remove_device(self, device, entity_refs):
remove_tasks = []
for entity_ref in entity_refs:
remove_tasks.append(entity_ref.remove_future)
- await asyncio.wait(remove_tasks)
+ if remove_tasks:
+ await asyncio.wait(remove_tasks)
reg_device = self.ha_device_registry.async_get(device.device_id)
if reg_device is not None:
self.ha_device_registry.async_remove_device(reg_device.id)
@@ -377,12 +378,12 @@ def get_device(self, ieee):
"""Return ZHADevice for given ieee."""
return self._devices.get(ieee)
- def get_group(self, group_id: str) -> Optional[ZhaGroupType]:
+ def get_group(self, group_id: str) -> ZhaGroupType | None:
"""Return Group for given group id."""
return self.groups.get(group_id)
@callback
- def async_get_group_by_name(self, group_name: str) -> Optional[ZhaGroupType]:
+ def async_get_group_by_name(self, group_name: str) -> ZhaGroupType | None:
"""Get ZHA group by name."""
for group in self.groups.values():
if group.name == group_name:
@@ -472,24 +473,29 @@ def register_entity_reference(
)
@callback
- def async_enable_debug_mode(self):
+ def async_enable_debug_mode(self, filterer=None):
"""Enable debug mode for ZHA."""
self._log_levels[DEBUG_LEVEL_ORIGINAL] = async_capture_log_levels()
async_set_logger_levels(DEBUG_LEVELS)
self._log_levels[DEBUG_LEVEL_CURRENT] = async_capture_log_levels()
+ if filterer:
+ self._log_relay_handler.addFilter(filterer)
+
for logger_name in DEBUG_RELAY_LOGGERS:
logging.getLogger(logger_name).addHandler(self._log_relay_handler)
self.debug_enabled = True
@callback
- def async_disable_debug_mode(self):
+ def async_disable_debug_mode(self, filterer=None):
"""Disable debug mode for ZHA."""
async_set_logger_levels(self._log_levels[DEBUG_LEVEL_ORIGINAL])
self._log_levels[DEBUG_LEVEL_CURRENT] = async_capture_log_levels()
for logger_name in DEBUG_RELAY_LOGGERS:
logging.getLogger(logger_name).removeHandler(self._log_relay_handler)
+ if filterer:
+ self._log_relay_handler.removeFilter(filterer)
self.debug_enabled = False
@callback
@@ -619,7 +625,7 @@ async def _async_device_rejoined(self, zha_device):
zha_device.update_available(True)
async def async_create_zigpy_group(
- self, name: str, members: List[GroupMember]
+ self, name: str, members: list[GroupMember]
) -> ZhaGroupType:
"""Create a new Zigpy Zigbee group."""
# we start with two to fill any gaps from a user removing existing groups
@@ -727,9 +733,8 @@ def __init__(self, hass, gateway):
def emit(self, record):
"""Relay log message via dispatcher."""
stack = []
- if record.levelno >= logging.WARN:
- if not record.exc_info:
- stack = [f for f, _, _, _ in traceback.extract_stack()]
+ if record.levelno >= logging.WARN and not record.exc_info:
+ stack = [f for f, _, _, _ in traceback.extract_stack()]
entry = LogEntry(record, stack, _figure_out_source(record, stack, self.hass))
async_dispatcher_send(
diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py
index 59277a394b3ba9..beaebbe8767031 100644
--- a/homeassistant/components/zha/core/group.py
+++ b/homeassistant/components/zha/core/group.py
@@ -1,8 +1,10 @@
"""Group for Zigbee Home Automation."""
+from __future__ import annotations
+
import asyncio
import collections
import logging
-from typing import Any, Dict, List
+from typing import Any
import zigpy.exceptions
@@ -58,16 +60,16 @@ def device(self) -> ZhaDeviceType:
return self._zha_device
@property
- def member_info(self) -> Dict[str, Any]:
+ def member_info(self) -> dict[str, Any]:
"""Get ZHA group info."""
- member_info: Dict[str, Any] = {}
+ member_info: dict[str, Any] = {}
member_info["endpoint_id"] = self.endpoint_id
member_info["device"] = self.device.zha_device_info
member_info["entities"] = self.associated_entities
return member_info
@property
- def associated_entities(self) -> List[GroupEntityReference]:
+ def associated_entities(self) -> list[GroupEntityReference]:
"""Return the list of entities that were derived from this endpoint."""
ha_entity_registry = self.device.gateway.ha_entity_registry
zha_device_registry = self.device.gateway.device_registry
@@ -136,7 +138,7 @@ def endpoint(self) -> ZigpyEndpointType:
return self._zigpy_group.endpoint
@property
- def members(self) -> List[ZHAGroupMember]:
+ def members(self) -> list[ZHAGroupMember]:
"""Return the ZHA devices that are members of this group."""
return [
ZHAGroupMember(
@@ -146,7 +148,7 @@ def members(self) -> List[ZHAGroupMember]:
if member_ieee in self._zha_gateway.devices
]
- async def async_add_members(self, members: List[GroupMember]) -> None:
+ async def async_add_members(self, members: list[GroupMember]) -> None:
"""Add members to this group."""
if len(members) > 1:
tasks = []
@@ -162,7 +164,7 @@ async def async_add_members(self, members: List[GroupMember]) -> None:
members[0].ieee
].async_add_endpoint_to_group(members[0].endpoint_id, self.group_id)
- async def async_remove_members(self, members: List[GroupMember]) -> None:
+ async def async_remove_members(self, members: list[GroupMember]) -> None:
"""Remove members from this group."""
if len(members) > 1:
tasks = []
@@ -181,18 +183,18 @@ async def async_remove_members(self, members: List[GroupMember]) -> None:
].async_remove_endpoint_from_group(members[0].endpoint_id, self.group_id)
@property
- def member_entity_ids(self) -> List[str]:
+ def member_entity_ids(self) -> list[str]:
"""Return the ZHA entity ids for all entities for the members of this group."""
- all_entity_ids: List[str] = []
+ all_entity_ids: list[str] = []
for member in self.members:
entity_references = member.associated_entities
for entity_reference in entity_references:
all_entity_ids.append(entity_reference["entity_id"])
return all_entity_ids
- def get_domain_entity_ids(self, domain) -> List[str]:
+ def get_domain_entity_ids(self, domain) -> list[str]:
"""Return entity ids from the entity domain for this group."""
- domain_entity_ids: List[str] = []
+ domain_entity_ids: list[str] = []
for member in self.members:
if member.device.is_coordinator:
continue
@@ -207,9 +209,9 @@ def get_domain_entity_ids(self, domain) -> List[str]:
return domain_entity_ids
@property
- def group_info(self) -> Dict[str, Any]:
+ def group_info(self) -> dict[str, Any]:
"""Get ZHA group info."""
- group_info: Dict[str, Any] = {}
+ group_info: dict[str, Any] = {}
group_info["group_id"] = self.group_id
group_info["name"] = self.name
group_info["members"] = [member.member_info for member in self.members]
diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py
index 47911fc1078498..cf3d040f020538 100644
--- a/homeassistant/components/zha/core/helpers.py
+++ b/homeassistant/components/zha/core/helpers.py
@@ -4,6 +4,7 @@
For more details about this component, please refer to the documentation at
https://home-assistant.io/integrations/zha/
"""
+from __future__ import annotations
import asyncio
import binascii
@@ -13,7 +14,7 @@
import logging
from random import uniform
import re
-from typing import Any, Callable, Iterator, List, Optional, Tuple
+from typing import Any, Callable, Iterator
import voluptuous as vol
import zigpy.exceptions
@@ -67,7 +68,7 @@ async def safe_read(
async def get_matched_clusters(
source_zha_device: ZhaDeviceType, target_zha_device: ZhaDeviceType
-) -> List[BindingPair]:
+) -> list[BindingPair]:
"""Get matched input/output cluster pairs for 2 devices."""
source_clusters = source_zha_device.async_get_std_clusters()
target_clusters = target_zha_device.async_get_std_clusters()
@@ -131,7 +132,7 @@ async def async_get_zha_device(hass, device_id):
return zha_gateway.devices[ieee]
-def find_state_attributes(states: List[State], key: str) -> Iterator[Any]:
+def find_state_attributes(states: list[State], key: str) -> Iterator[Any]:
"""Find attributes with matching key from states."""
for state in states:
value = state.attributes.get(key)
@@ -150,9 +151,9 @@ def mean_tuple(*args):
def reduce_attribute(
- states: List[State],
+ states: list[State],
key: str,
- default: Optional[Any] = None,
+ default: Any | None = None,
reduce: Callable[..., Any] = mean_int,
) -> Any:
"""Find the first attribute matching key from states.
@@ -280,7 +281,7 @@ def convert_install_code(value: str) -> bytes:
)
-def qr_to_install_code(qr_code: str) -> Tuple[zigpy.types.EUI64, bytes]:
+def qr_to_install_code(qr_code: str) -> tuple[zigpy.types.EUI64, bytes]:
"""Try to parse the QR code.
if successful, return a tuple of a EUI64 address and install code.
diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py
index 4dcccc98c05aff..2f9ed57745a26e 100644
--- a/homeassistant/components/zha/core/registries.py
+++ b/homeassistant/components/zha/core/registries.py
@@ -1,6 +1,8 @@
"""Mapping registries for Zigbee Home Automation."""
+from __future__ import annotations
+
import collections
-from typing import Callable, Dict, List, Set, Tuple, Union
+from typing import Callable, Dict
import attr
import zigpy.profiles.zha
@@ -68,6 +70,8 @@
zcl.clusters.general.PowerConfiguration.cluster_id: SENSOR,
zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: SENSOR,
zcl.clusters.hvac.Fan.cluster_id: FAN,
+ zcl.clusters.measurement.CarbonDioxideConcentration.cluster_id: SENSOR,
+ zcl.clusters.measurement.CarbonMonoxideConcentration.cluster_id: SENSOR,
zcl.clusters.measurement.IlluminanceMeasurement.cluster_id: SENSOR,
zcl.clusters.measurement.OccupancySensing.cluster_id: BINARY_SENSOR,
zcl.clusters.measurement.PressureMeasurement.cluster_id: SENSOR,
@@ -132,19 +136,19 @@ def set_or_callable(value):
class MatchRule:
"""Match a ZHA Entity to a channel name or generic id."""
- channel_names: Union[Callable, Set[str], str] = attr.ib(
+ channel_names: Callable | set[str] | str = attr.ib(
factory=frozenset, converter=set_or_callable
)
- generic_ids: Union[Callable, Set[str], str] = attr.ib(
+ generic_ids: Callable | set[str] | str = attr.ib(
factory=frozenset, converter=set_or_callable
)
- manufacturers: Union[Callable, Set[str], str] = attr.ib(
+ manufacturers: Callable | set[str] | str = attr.ib(
factory=frozenset, converter=set_or_callable
)
- models: Union[Callable, Set[str], str] = attr.ib(
+ models: Callable | set[str] | str = attr.ib(
factory=frozenset, converter=set_or_callable
)
- aux_channels: Union[Callable, Set[str], str] = attr.ib(
+ aux_channels: Callable | set[str] | str = attr.ib(
factory=frozenset, converter=set_or_callable
)
@@ -174,7 +178,7 @@ def weight(self) -> int:
weight += 1 * len(self.aux_channels)
return weight
- def claim_channels(self, channel_pool: List[ChannelType]) -> List[ChannelType]:
+ def claim_channels(self, channel_pool: list[ChannelType]) -> list[ChannelType]:
"""Return a list of channels this rule matches + aux channels."""
claimed = []
if isinstance(self.channel_names, frozenset):
@@ -187,15 +191,15 @@ def claim_channels(self, channel_pool: List[ChannelType]) -> List[ChannelType]:
claimed.extend([ch for ch in channel_pool if ch.name in self.aux_channels])
return claimed
- def strict_matched(self, manufacturer: str, model: str, channels: List) -> bool:
+ def strict_matched(self, manufacturer: str, model: str, channels: list) -> bool:
"""Return True if this device matches the criteria."""
return all(self._matched(manufacturer, model, channels))
- def loose_matched(self, manufacturer: str, model: str, channels: List) -> bool:
+ def loose_matched(self, manufacturer: str, model: str, channels: list) -> bool:
"""Return True if this device matches the criteria."""
return any(self._matched(manufacturer, model, channels))
- def _matched(self, manufacturer: str, model: str, channels: List) -> list:
+ def _matched(self, manufacturer: str, model: str, channels: list) -> list:
"""Return a list of field matches."""
if not any(attr.asdict(self).values()):
return [False]
@@ -243,9 +247,9 @@ def get_entity(
component: str,
manufacturer: str,
model: str,
- channels: List[ChannelType],
+ channels: list[ChannelType],
default: CALLABLE_T = None,
- ) -> Tuple[CALLABLE_T, List[ChannelType]]:
+ ) -> tuple[CALLABLE_T, list[ChannelType]]:
"""Match a ZHA Channels to a ZHA Entity class."""
matches = self._strict_registry[component]
for match in sorted(matches, key=lambda x: x.weight, reverse=True):
@@ -262,11 +266,11 @@ def get_group_entity(self, component: str) -> CALLABLE_T:
def strict_match(
self,
component: str,
- channel_names: Union[Callable, Set[str], str] = None,
- generic_ids: Union[Callable, Set[str], str] = None,
- manufacturers: Union[Callable, Set[str], str] = None,
- models: Union[Callable, Set[str], str] = None,
- aux_channels: Union[Callable, Set[str], str] = None,
+ channel_names: Callable | set[str] | str = None,
+ generic_ids: Callable | set[str] | str = None,
+ manufacturers: Callable | set[str] | str = None,
+ models: Callable | set[str] | str = None,
+ aux_channels: Callable | set[str] | str = None,
) -> Callable[[CALLABLE_T], CALLABLE_T]:
"""Decorate a strict match rule."""
@@ -287,11 +291,11 @@ def decorator(zha_ent: CALLABLE_T) -> CALLABLE_T:
def loose_match(
self,
component: str,
- channel_names: Union[Callable, Set[str], str] = None,
- generic_ids: Union[Callable, Set[str], str] = None,
- manufacturers: Union[Callable, Set[str], str] = None,
- models: Union[Callable, Set[str], str] = None,
- aux_channels: Union[Callable, Set[str], str] = None,
+ channel_names: Callable | set[str] | str = None,
+ generic_ids: Callable | set[str] | str = None,
+ manufacturers: Callable | set[str] | str = None,
+ models: Callable | set[str] | str = None,
+ aux_channels: Callable | set[str] | str = None,
) -> Callable[[CALLABLE_T], CALLABLE_T]:
"""Decorate a loose match rule."""
diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py
index 051fcaa29252b1..9381c529187202 100644
--- a/homeassistant/components/zha/core/store.py
+++ b/homeassistant/components/zha/core/store.py
@@ -1,9 +1,10 @@
"""Data storage helper for ZHA."""
-# pylint: disable=unused-import
+from __future__ import annotations
+
from collections import OrderedDict
import datetime
import time
-from typing import MutableMapping, Optional, cast
+from typing import MutableMapping, cast
import attr
@@ -25,9 +26,9 @@
class ZhaDeviceEntry:
"""Zha Device storage Entry."""
- name: Optional[str] = attr.ib(default=None)
- ieee: Optional[str] = attr.ib(default=None)
- last_seen: Optional[float] = attr.ib(default=None)
+ name: str | None = attr.ib(default=None)
+ ieee: str | None = attr.ib(default=None)
+ last_seen: float | None = attr.ib(default=None)
class ZhaStorage:
@@ -92,7 +93,7 @@ async def async_load(self) -> None:
"""Load the registry of zha device entries."""
data = await self._store.async_load()
- devices: "OrderedDict[str, ZhaDeviceEntry]" = OrderedDict()
+ devices: OrderedDict[str, ZhaDeviceEntry] = OrderedDict()
if data is not None:
for device in data["devices"]:
diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py
index 45114c677aff1f..5530cd3e3f5176 100644
--- a/homeassistant/components/zha/cover.py
+++ b/homeassistant/components/zha/cover.py
@@ -1,8 +1,9 @@
"""Support for ZHA covers."""
+from __future__ import annotations
+
import asyncio
import functools
import logging
-from typing import List, Optional
from zigpy.zcl.foundation import Status
@@ -180,7 +181,7 @@ def __init__(
self,
unique_id: str,
zha_device: ZhaDeviceType,
- channels: List[ChannelType],
+ channels: list[ChannelType],
**kwargs,
):
"""Initialize the ZHA light."""
@@ -199,12 +200,12 @@ def current_cover_position(self):
return self._position
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the class of this device, from component DEVICE_CLASSES."""
return DEVICE_CLASS_SHADE
@property
- def is_closed(self) -> Optional[bool]:
+ def is_closed(self) -> bool | None:
"""Return True if shade is closed."""
if self._is_open is None:
return None
@@ -289,7 +290,7 @@ class KeenVent(Shade):
"""Keen vent cover."""
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the class of this device, from component DEVICE_CLASSES."""
return DEVICE_CLASS_DAMPER
@@ -301,7 +302,7 @@ async def async_open_cover(self, **kwargs):
self._on_off_channel.on(),
]
results = await asyncio.gather(*tasks, return_exceptions=True)
- if any([isinstance(result, Exception) for result in results]):
+ if any(isinstance(result, Exception) for result in results):
self.debug("couldn't open cover")
return
diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py
index 4636393919052b..9d419b164357f0 100644
--- a/homeassistant/components/zha/device_action.py
+++ b/homeassistant/components/zha/device_action.py
@@ -1,5 +1,5 @@
"""Provides device actions for ZHA devices."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -54,7 +54,7 @@ async def async_call_action_from_config(
)
-async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device actions."""
try:
zha_device = await async_get_zha_device(hass, device_id)
diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py
index 53191789eba2e9..ffb37e33b0fcc3 100644
--- a/homeassistant/components/zha/device_tracker.py
+++ b/homeassistant/components/zha/device_tracker.py
@@ -83,7 +83,7 @@ def source_type(self):
@callback
def async_battery_percentage_remaining_updated(self, attr_id, attr_name, value):
"""Handle tracking."""
- if not attr_name == "battery_percentage_remaining":
+ if attr_name != "battery_percentage_remaining":
return
self.debug("battery_percentage_remaining updated: %s", value)
self._connected = True
diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py
index 96f005ba288242..445151899ee0bf 100644
--- a/homeassistant/components/zha/entity.py
+++ b/homeassistant/components/zha/entity.py
@@ -1,9 +1,12 @@
"""Entity for Zigbee Home Automation."""
+from __future__ import annotations
import asyncio
+import functools
import logging
-from typing import Any, Awaitable, Dict, List, Optional
+from typing import Any, Awaitable
+from homeassistant.const import ATTR_NAME
from homeassistant.core import CALLBACK_TYPE, Event, callback
from homeassistant.helpers import entity
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
@@ -17,7 +20,6 @@
from .core.const import (
ATTR_MANUFACTURER,
ATTR_MODEL,
- ATTR_NAME,
DATA_ZHA,
DATA_ZHA_BRIDGE_ID,
DOMAIN,
@@ -31,6 +33,7 @@
_LOGGER = logging.getLogger(__name__)
ENTITY_SUFFIX = "entity_suffix"
+UPDATE_GROUP_FROM_CHILD_DELAY = 0.2
class BaseZhaEntity(LogMixin, entity.Entity):
@@ -43,9 +46,9 @@ def __init__(self, unique_id: str, zha_device: ZhaDeviceType, **kwargs):
self._should_poll: bool = False
self._unique_id: str = unique_id
self._state: Any = None
- self._device_state_attributes: Dict[str, Any] = {}
+ self._extra_state_attributes: dict[str, Any] = {}
self._zha_device: ZhaDeviceType = zha_device
- self._unsubs: List[CALLABLE_T] = []
+ self._unsubs: list[CALLABLE_T] = []
self.remove_future: Awaitable[None] = None
@property
@@ -64,9 +67,9 @@ def zha_device(self) -> ZhaDeviceType:
return self._zha_device
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return device specific state attributes."""
- return self._device_state_attributes
+ return self._extra_state_attributes
@property
def force_update(self) -> bool:
@@ -79,7 +82,7 @@ def should_poll(self) -> bool:
return self._should_poll
@property
- def device_info(self) -> Dict[str, Any]:
+ def device_info(self) -> dict[str, Any]:
"""Return a device description for device registry."""
zha_device_info = self._zha_device.device_info
ieee = zha_device_info["ieee"]
@@ -100,7 +103,7 @@ def async_state_changed(self) -> None:
@callback
def async_update_state_attribute(self, key: str, value: Any) -> None:
"""Update a single device state attribute."""
- self._device_state_attributes.update({key: value})
+ self._extra_state_attributes.update({key: value})
self.async_write_ha_state()
@callback
@@ -141,7 +144,7 @@ def __init__(
self,
unique_id: str,
zha_device: ZhaDeviceType,
- channels: List[ChannelType],
+ channels: list[ChannelType],
**kwargs,
):
"""Init ZHA entity."""
@@ -150,7 +153,7 @@ def __init__(
ch_names = [ch.cluster.ep_attribute for ch in channels]
ch_names = ", ".join(sorted(ch_names))
self._name: str = f"{zha_device.name} {ieeetail} {ch_names}"
- self.cluster_channels: Dict[str, ChannelType] = {}
+ self.cluster_channels: dict[str, ChannelType] = {}
for channel in channels:
self.cluster_channels[channel.name] = channel
@@ -165,7 +168,7 @@ async def async_added_to_hass(self) -> None:
self.async_accept_signal(
None,
f"{SIGNAL_REMOVE}_{self.zha_device.ieee}",
- self.async_remove,
+ functools.partial(self.async_remove, force_remove=True),
signal_override=True,
)
@@ -215,7 +218,7 @@ class ZhaGroupEntity(BaseZhaEntity):
"""A base class for ZHA group entities."""
def __init__(
- self, entity_ids: List[str], unique_id: str, group_id: int, zha_device, **kwargs
+ self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs
) -> None:
"""Initialize a light group."""
super().__init__(unique_id, zha_device, **kwargs)
@@ -223,8 +226,8 @@ def __init__(
self._group = zha_device.gateway.groups.get(group_id)
self._name = f"{self._group.name}_zha_group_0x{group_id:04x}"
self._group_id: int = group_id
- self._entity_ids: List[str] = entity_ids
- self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None
+ self._entity_ids: list[str] = entity_ids
+ self._async_unsub_state_changed: CALLBACK_TYPE | None = None
self._handled_group_membership = False
@property
@@ -239,7 +242,7 @@ async def _handle_group_membership_changed(self):
return
self._handled_group_membership = True
- await self.async_remove()
+ await self.async_remove(force_remove=True)
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@@ -266,7 +269,11 @@ def send_removed_signal():
@callback
def async_state_changed_listener(self, event: Event):
"""Handle child updates."""
- self.async_schedule_update_ha_state(True)
+ # Delay to ensure that we get updates from all members before updating the group
+ self.hass.loop.call_later(
+ UPDATE_GROUP_FROM_CHILD_DELAY,
+ lambda: self.async_schedule_update_ha_state(True),
+ )
async def async_will_remove_from_hass(self) -> None:
"""Handle removal from Home Assistant."""
diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py
index bc5714ef08c327..3c50261b56578b 100644
--- a/homeassistant/components/zha/fan.py
+++ b/homeassistant/components/zha/fan.py
@@ -1,22 +1,29 @@
"""Fans on Zigbee Home Automation networks."""
+from __future__ import annotations
+
+from abc import abstractmethod
import functools
-from typing import List, Optional
+import math
from zigpy.exceptions import ZigbeeException
import zigpy.zcl.clusters.hvac as hvac
from homeassistant.components.fan import (
+ ATTR_PERCENTAGE,
+ ATTR_PRESET_MODE,
DOMAIN,
- SPEED_HIGH,
- SPEED_LOW,
- SPEED_MEDIUM,
- SPEED_OFF,
SUPPORT_SET_SPEED,
FanEntity,
+ NotValidPresetModeError,
)
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import State, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.util.percentage import (
+ int_states_in_range,
+ percentage_to_ranged_value,
+ ranged_value_to_percentage,
+)
from .core import discovery
from .core.const import (
@@ -32,24 +39,20 @@
# Additional speeds in zigbee's ZCL
# Spec is unclear as to what this value means. On King Of Fans HBUniversal
# receiver, this means Very High.
-SPEED_ON = "on"
+PRESET_MODE_ON = "on"
# The fan speed is self-regulated
-SPEED_AUTO = "auto"
+PRESET_MODE_AUTO = "auto"
# When the heated/cooled space is occupied, the fan is always on
-SPEED_SMART = "smart"
-
-SPEED_LIST = [
- SPEED_OFF,
- SPEED_LOW,
- SPEED_MEDIUM,
- SPEED_HIGH,
- SPEED_ON,
- SPEED_AUTO,
- SPEED_SMART,
-]
-
-VALUE_TO_SPEED = dict(enumerate(SPEED_LIST))
-SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)}
+PRESET_MODE_SMART = "smart"
+
+SPEED_RANGE = (1, 3) # off is not included
+PRESET_MODES_TO_NAME = {4: PRESET_MODE_ON, 5: PRESET_MODE_AUTO, 6: PRESET_MODE_SMART}
+
+NAME_TO_PRESET_MODE = {v: k for k, v in PRESET_MODES_TO_NAME.items()}
+PRESET_MODES = list(NAME_TO_PRESET_MODE)
+
+DEFAULT_ON_PERCENTAGE = 50
+
STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN)
GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, DOMAIN)
@@ -74,51 +77,49 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class BaseFan(FanEntity):
"""Base representation of a ZHA fan."""
- def __init__(self, *args, **kwargs):
- """Initialize the fan."""
- super().__init__(*args, **kwargs)
- self._state = None
- self._fan_channel = None
-
- @property
- def speed_list(self) -> list:
- """Get the list of available speeds."""
- return SPEED_LIST
-
@property
- def speed(self) -> str:
- """Return the current speed."""
- return self._state
+ def preset_modes(self) -> list[str]:
+ """Return the available preset modes."""
+ return PRESET_MODES
@property
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_SET_SPEED
- #
- # The fan entity model has changed to use percentages and preset_modes
- # instead of speeds.
- #
- # Please review
- # https://developers.home-assistant.io/docs/core/entity/fan/
- #
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return int_states_in_range(SPEED_RANGE)
+
async def async_turn_on(
self, speed=None, percentage=None, preset_mode=None, **kwargs
) -> None:
"""Turn the entity on."""
- if speed is None:
- speed = SPEED_MEDIUM
-
- await self.async_set_speed(speed)
+ if percentage is None:
+ percentage = DEFAULT_ON_PERCENTAGE
+ await self.async_set_percentage(percentage)
async def async_turn_off(self, **kwargs) -> None:
"""Turn the entity off."""
- await self.async_set_speed(SPEED_OFF)
+ await self.async_set_percentage(0)
+
+ async def async_set_percentage(self, percentage: int | None) -> None:
+ """Set the speed percenage of the fan."""
+ fan_mode = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
+ await self._async_set_fan_mode(fan_mode)
+
+ async def async_set_preset_mode(self, preset_mode: str) -> None:
+ """Set the preset mode for the fan."""
+ if preset_mode not in self.preset_modes:
+ raise NotValidPresetModeError(
+ f"The preset_mode {preset_mode} is not a valid preset_mode: {self.preset_modes}"
+ )
+ await self._async_set_fan_mode(NAME_TO_PRESET_MODE[preset_mode])
- async def async_set_speed(self, speed: str) -> None:
- """Set the speed of the fan."""
- await self._fan_channel.async_set_speed(SPEED_TO_VALUE[speed])
- self.async_set_state(0, "fan_mode", speed)
+ @abstractmethod
+ async def _async_set_fan_mode(self, fan_mode: int) -> None:
+ """Set the fan mode for the fan."""
@callback
def async_set_state(self, attr_id, attr_name, value):
@@ -142,52 +143,87 @@ async def async_added_to_hass(self):
)
@property
- def speed(self) -> Optional[str]:
- """Return the current speed."""
- return VALUE_TO_SPEED.get(self._fan_channel.fan_mode)
+ def percentage(self) -> int | None:
+ """Return the current speed percentage."""
+ if (
+ self._fan_channel.fan_mode is None
+ or self._fan_channel.fan_mode > SPEED_RANGE[1]
+ ):
+ return None
+ if self._fan_channel.fan_mode == 0:
+ return 0
+ return ranged_value_to_percentage(SPEED_RANGE, self._fan_channel.fan_mode)
+
+ @property
+ def preset_mode(self) -> str | None:
+ """Return the current preset mode."""
+ return PRESET_MODES_TO_NAME.get(self._fan_channel.fan_mode)
@callback
def async_set_state(self, attr_id, attr_name, value):
"""Handle state update from channel."""
self.async_write_ha_state()
+ async def _async_set_fan_mode(self, fan_mode: int) -> None:
+ """Set the fan mode for the fan."""
+ await self._fan_channel.async_set_speed(fan_mode)
+ self.async_set_state(0, "fan_mode", fan_mode)
+
@GROUP_MATCH()
class FanGroup(BaseFan, ZhaGroupEntity):
"""Representation of a fan group."""
def __init__(
- self, entity_ids: List[str], unique_id: str, group_id: int, zha_device, **kwargs
+ self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs
) -> None:
"""Initialize a fan group."""
super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs)
self._available: bool = False
group = self.zha_device.gateway.get_group(self._group_id)
self._fan_channel = group.endpoint[hvac.Fan.cluster_id]
+ self._percentage = None
+ self._preset_mode = None
- # what should we do with this hack?
- async def async_set_speed(value) -> None:
- """Set the speed of the fan."""
- try:
- await self._fan_channel.write_attributes({"fan_mode": value})
- except ZigbeeException as ex:
- self.error("Could not set speed: %s", ex)
- return
+ @property
+ def percentage(self) -> int | None:
+ """Return the current speed percentage."""
+ return self._percentage
- self._fan_channel.async_set_speed = async_set_speed
+ @property
+ def preset_mode(self) -> str | None:
+ """Return the current preset mode."""
+ return self._preset_mode
+
+ async def _async_set_fan_mode(self, fan_mode: int) -> None:
+ """Set the fan mode for the group."""
+ try:
+ await self._fan_channel.write_attributes({"fan_mode": fan_mode})
+ except ZigbeeException as ex:
+ self.error("Could not set fan mode: %s", ex)
+ self.async_set_state(0, "fan_mode", fan_mode)
async def async_update(self):
"""Attempt to retrieve on off state from the fan."""
all_states = [self.hass.states.get(x) for x in self._entity_ids]
- states: List[State] = list(filter(None, all_states))
- on_states: List[State] = [state for state in states if state.state != SPEED_OFF]
-
+ states: list[State] = list(filter(None, all_states))
+ percentage_states: list[State] = [
+ state for state in states if state.attributes.get(ATTR_PERCENTAGE)
+ ]
+ preset_mode_states: list[State] = [
+ state for state in states if state.attributes.get(ATTR_PRESET_MODE)
+ ]
self._available = any(state.state != STATE_UNAVAILABLE for state in states)
- # for now just use first non off state since its kind of arbitrary
- if not on_states:
- self._state = SPEED_OFF
+
+ if percentage_states:
+ self._percentage = percentage_states[0].attributes[ATTR_PERCENTAGE]
+ self._preset_mode = None
+ elif preset_mode_states:
+ self._preset_mode = preset_mode_states[0].attributes[ATTR_PRESET_MODE]
+ self._percentage = None
else:
- self._state = on_states[0].state
+ self._percentage = None
+ self._preset_mode = None
async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass."""
diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py
index 32b8a064054bb6..72807458d266ac 100644
--- a/homeassistant/components/zha/light.py
+++ b/homeassistant/components/zha/light.py
@@ -1,4 +1,6 @@
"""Lights on Zigbee Home Automation networks."""
+from __future__ import annotations
+
from collections import Counter
from datetime import timedelta
import enum
@@ -6,7 +8,7 @@
import itertools
import logging
import random
-from typing import Any, Dict, List, Optional, Tuple
+from typing import Any
from zigpy.zcl.clusters.general import Identify, LevelControl, OnOff
from zigpy.zcl.clusters.lighting import Color
@@ -65,6 +67,8 @@
CAPABILITIES_COLOR_XY = 0x08
CAPABILITIES_COLOR_TEMP = 0x10
+DEFAULT_TRANSITION = 1
+
UPDATE_COLORLOOP_ACTION = 0x1
UPDATE_COLORLOOP_DIRECTION = 0x2
UPDATE_COLORLOOP_TIME = 0x4
@@ -114,19 +118,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class BaseLight(LogMixin, light.LightEntity):
"""Operations common to all light entities."""
+ _FORCE_ON = False
+
def __init__(self, *args, **kwargs):
"""Initialize the light."""
super().__init__(*args, **kwargs)
self._available: bool = False
- self._brightness: Optional[int] = None
- self._off_brightness: Optional[int] = None
- self._hs_color: Optional[Tuple[float, float]] = None
- self._color_temp: Optional[int] = None
- self._min_mireds: Optional[int] = 153
- self._max_mireds: Optional[int] = 500
- self._white_value: Optional[int] = None
- self._effect_list: Optional[List[str]] = None
- self._effect: Optional[str] = None
+ self._brightness: int | None = None
+ self._off_brightness: int | None = None
+ self._hs_color: tuple[float, float] | None = None
+ self._color_temp: int | None = None
+ self._min_mireds: int | None = 153
+ self._max_mireds: int | None = 500
+ self._white_value: int | None = None
+ self._effect_list: list[str] | None = None
+ self._effect: str | None = None
self._supported_features: int = 0
self._state: bool = False
self._on_off_channel = None
@@ -135,7 +141,7 @@ def __init__(self, *args, **kwargs):
self._identify_channel = None
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return state attributes."""
attributes = {"off_brightness": self._off_brightness}
return attributes
@@ -201,7 +207,7 @@ def supported_features(self):
async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
transition = kwargs.get(light.ATTR_TRANSITION)
- duration = transition * 10 if transition else 1
+ duration = transition * 10 if transition else DEFAULT_TRANSITION
brightness = kwargs.get(light.ATTR_BRIGHTNESS)
effect = kwargs.get(light.ATTR_EFFECT)
flash = kwargs.get(light.ATTR_FLASH)
@@ -228,7 +234,7 @@ async def async_turn_on(self, **kwargs):
if level:
self._brightness = level
- if brightness is None or brightness:
+ if brightness is None or (self._FORCE_ON and brightness):
# since some lights don't always turn on with move_to_level_with_on_off,
# we should call the on command on the on_off cluster if brightness is not 0.
result = await self._on_off_channel.on()
@@ -344,8 +350,8 @@ def __init__(self, unique_id, zha_device: ZhaDeviceType, channels, **kwargs):
self._color_channel = self.cluster_channels.get(CHANNEL_COLOR)
self._identify_channel = self.zha_device.channels.identify_ch
if self._color_channel:
- self._min_mireds: Optional[int] = self._color_channel.min_mireds
- self._max_mireds: Optional[int] = self._color_channel.max_mireds
+ self._min_mireds: int | None = self._color_channel.min_mireds
+ self._max_mireds: int | None = self._color_channel.max_mireds
self._cancel_refresh_handle = None
effect_list = []
@@ -512,12 +518,23 @@ class HueLight(Light):
_REFRESH_INTERVAL = (3, 5)
+@STRICT_MATCH(
+ channel_names=CHANNEL_ON_OFF,
+ aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL},
+ manufacturers="Jasco",
+)
+class ForceOnLight(Light):
+ """Representation of a light which does not respect move_to_level_with_on_off."""
+
+ _FORCE_ON = True
+
+
@GROUP_MATCH()
class LightGroup(BaseLight, ZhaGroupEntity):
"""Representation of a light group."""
def __init__(
- self, entity_ids: List[str], unique_id: str, group_id: int, zha_device, **kwargs
+ self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs
) -> None:
"""Initialize a light group."""
super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs)
@@ -554,7 +571,7 @@ async def async_turn_off(self, **kwargs):
async def async_update(self) -> None:
"""Query all members and determine the light group state."""
all_states = [self.hass.states.get(x) for x in self._entity_ids]
- states: List[State] = list(filter(None, all_states))
+ states: list[State] = list(filter(None, all_states))
on_states = [state for state in states if state.state == STATE_ON]
self._state = len(on_states) > 0
diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py
index 5cc7d7c56f6cb4..5684b22db6a158 100644
--- a/homeassistant/components/zha/lock.py
+++ b/homeassistant/components/zha/lock.py
@@ -1,6 +1,7 @@
"""Locks on Zigbee Home Automation networks."""
import functools
+import voluptuous as vol
from zigpy.zcl.foundation import Status
from homeassistant.components.lock import (
@@ -10,6 +11,7 @@
LockEntity,
)
from homeassistant.core import callback
+from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .core import discovery
@@ -29,6 +31,11 @@
VALUE_TO_STATE = dict(enumerate(STATE_LIST))
+SERVICE_SET_LOCK_USER_CODE = "set_lock_user_code"
+SERVICE_ENABLE_LOCK_USER_CODE = "enable_lock_user_code"
+SERVICE_DISABLE_LOCK_USER_CODE = "disable_lock_user_code"
+SERVICE_CLEAR_LOCK_USER_CODE = "clear_lock_user_code"
+
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Zigbee Home Automation Door Lock from config entry."""
@@ -43,6 +50,42 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub)
+ platform = entity_platform.current_platform.get()
+ assert platform
+
+ platform.async_register_entity_service( # type: ignore
+ SERVICE_SET_LOCK_USER_CODE,
+ {
+ vol.Required("code_slot"): vol.Coerce(int),
+ vol.Required("user_code"): cv.string,
+ },
+ "async_set_lock_user_code",
+ )
+
+ platform.async_register_entity_service( # type: ignore
+ SERVICE_ENABLE_LOCK_USER_CODE,
+ {
+ vol.Required("code_slot"): vol.Coerce(int),
+ },
+ "async_enable_lock_user_code",
+ )
+
+ platform.async_register_entity_service( # type: ignore
+ SERVICE_DISABLE_LOCK_USER_CODE,
+ {
+ vol.Required("code_slot"): vol.Coerce(int),
+ },
+ "async_disable_lock_user_code",
+ )
+
+ platform.async_register_entity_service( # type: ignore
+ SERVICE_CLEAR_LOCK_USER_CODE,
+ {
+ vol.Required("code_slot"): vol.Coerce(int),
+ },
+ "async_clear_lock_user_code",
+ )
+
@STRICT_MATCH(channel_names=CHANNEL_DOORLOCK)
class ZhaDoorLock(ZhaEntity, LockEntity):
@@ -73,7 +116,7 @@ def is_locked(self) -> bool:
return self._state == STATE_LOCKED
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return state attributes."""
return self.state_attributes
@@ -116,3 +159,27 @@ async def async_get_state(self, from_cache=True):
async def refresh(self, time):
"""Call async_get_state at an interval."""
await self.async_get_state(from_cache=False)
+
+ async def async_set_lock_user_code(self, code_slot: int, user_code: str) -> None:
+ """Set the user_code to index X on the lock."""
+ if self._doorlock_channel:
+ await self._doorlock_channel.async_set_user_code(code_slot, user_code)
+ self.debug("User code at slot %s set", code_slot)
+
+ async def async_enable_lock_user_code(self, code_slot: int) -> None:
+ """Enable user_code at index X on the lock."""
+ if self._doorlock_channel:
+ await self._doorlock_channel.async_enable_user_code(code_slot)
+ self.debug("User code at slot %s enabled", code_slot)
+
+ async def async_disable_lock_user_code(self, code_slot: int) -> None:
+ """Disable user_code at index X on the lock."""
+ if self._doorlock_channel:
+ await self._doorlock_channel.async_disable_user_code(code_slot)
+ self.debug("User code at slot %s disabled", code_slot)
+
+ async def async_clear_lock_user_code(self, code_slot: int) -> None:
+ """Clear the user_code at index X on the lock."""
+ if self._doorlock_channel:
+ await self._doorlock_channel.async_clear_user_code(code_slot)
+ self.debug("User code at slot %s cleared", code_slot)
diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json
index a24c20872f2cf5..5825bdcda0f7c7 100644
--- a/homeassistant/components/zha/manifest.json
+++ b/homeassistant/components/zha/manifest.json
@@ -4,16 +4,18 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [
- "bellows==0.21.0",
+ "bellows==0.23.1",
"pyserial==3.5",
"pyserial-asyncio==0.5",
- "zha-quirks==0.0.53",
+ "zha-quirks==0.0.55",
"zigpy-cc==0.5.2",
- "zigpy-deconz==0.11.1",
- "zigpy==0.32.0",
+ "zigpy-deconz==0.12.0",
+ "zigpy==0.33.0",
"zigpy-xbee==0.13.0",
"zigpy-zigate==0.7.3",
- "zigpy-znp==0.3.0"
+ "zigpy-znp==0.4.0"
],
- "codeowners": ["@dmulcahey", "@adminiuga"]
+ "codeowners": ["@dmulcahey", "@adminiuga"],
+ "zeroconf": [{ "type": "_esphomelib._tcp.local.", "name": "tube*" }],
+ "after_dependencies": ["zeroconf"]
}
diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py
index b02b3a549be9dc..41dce816e86f75 100644
--- a/homeassistant/components/zha/sensor.py
+++ b/homeassistant/components/zha/sensor.py
@@ -1,19 +1,25 @@
"""Sensors on Zigbee Home Automation networks."""
+from __future__ import annotations
+
import functools
import numbers
-from typing import Any, Callable, Dict, List, Optional, Union
+from typing import Any, Callable
from homeassistant.components.sensor import (
DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_CO,
+ DEVICE_CLASS_CO2,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_POWER,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DOMAIN,
+ SensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
+ CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
@@ -84,21 +90,21 @@ async def async_setup_entry(
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub)
-class Sensor(ZhaEntity):
+class Sensor(ZhaEntity, SensorEntity):
"""Base ZHA sensor."""
- SENSOR_ATTR: Optional[Union[int, str]] = None
+ SENSOR_ATTR: int | str | None = None
_decimals: int = 1
- _device_class: Optional[str] = None
+ _device_class: str | None = None
_divisor: int = 1
_multiplier: int = 1
- _unit: Optional[str] = None
+ _unit: str | None = None
def __init__(
self,
unique_id: str,
zha_device: ZhaDeviceType,
- channels: List[ChannelType],
+ channels: list[ChannelType],
**kwargs,
):
"""Init this sensor."""
@@ -118,7 +124,7 @@ def device_class(self) -> str:
return self._device_class
@property
- def unit_of_measurement(self) -> Optional[str]:
+ def unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of this entity."""
return self._unit
@@ -136,7 +142,7 @@ def async_set_state(self, attr_id: int, attr_name: str, value: Any) -> None:
"""Handle state update from channel."""
self.async_write_ha_state()
- def formatter(self, value: int) -> Union[int, float]:
+ def formatter(self, value: int) -> int | float:
"""Numeric pass-through formatter."""
if self._decimals > 0:
return round(
@@ -175,7 +181,7 @@ def formatter(value: int) -> int:
return value
@property
- def device_state_attributes(self) -> Dict[str, Any]:
+ def extra_state_attributes(self) -> dict[str, Any]:
"""Return device state attrs for battery sensors."""
state_attrs = {}
battery_size = self._channel.cluster.get("battery_size")
@@ -186,7 +192,9 @@ def device_state_attributes(self) -> Dict[str, Any]:
state_attrs["battery_quantity"] = battery_quantity
battery_voltage = self._channel.cluster.get("battery_voltage")
if battery_voltage is not None:
- state_attrs["battery_voltage"] = round(battery_voltage / 10, 1)
+ v_10mv = round(battery_voltage / 10, 2)
+ v_100mv = round(battery_voltage / 10, 1)
+ state_attrs["battery_voltage"] = v_100mv if v_100mv == v_10mv else v_10mv
return state_attrs
@@ -203,7 +211,7 @@ def should_poll(self) -> bool:
"""Return True if HA needs to poll for state changes."""
return True
- def formatter(self, value: int) -> Union[int, float]:
+ def formatter(self, value: int) -> int | float:
"""Return 'normalized' value."""
value = value * self._channel.multiplier / self._channel.divisor
if value < 100 and self._channel.divisor > 1:
@@ -249,7 +257,7 @@ class SmartEnergyMetering(Sensor):
SENSOR_ATTR = "instantaneous_demand"
_device_class = DEVICE_CLASS_POWER
- def formatter(self, value: int) -> Union[int, float]:
+ def formatter(self, value: int) -> int | float:
"""Pass through channel formatter."""
return self._channel.formatter_function(value)
@@ -277,3 +285,25 @@ class Temperature(Sensor):
_device_class = DEVICE_CLASS_TEMPERATURE
_divisor = 100
_unit = TEMP_CELSIUS
+
+
+@STRICT_MATCH(channel_names="carbon_dioxide_concentration")
+class CarbonDioxideConcentration(Sensor):
+ """Carbon Dioxide Concentration sensor."""
+
+ SENSOR_ATTR = "measured_value"
+ _device_class = DEVICE_CLASS_CO2
+ _decimals = 0
+ _multiplier = 1e6
+ _unit = CONCENTRATION_PARTS_PER_MILLION
+
+
+@STRICT_MATCH(channel_names="carbon_monoxide_concentration")
+class CarbonMonoxideConcentration(Sensor):
+ """Carbon Monoxide Concentration sensor."""
+
+ SENSOR_ATTR = "measured_value"
+ _device_class = DEVICE_CLASS_CO
+ _decimals = 0
+ _multiplier = 1e6
+ _unit = CONCENTRATION_PARTS_PER_MILLION
diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml
index 74793d6000f571..e756edbc48b4fa 100644
--- a/homeassistant/components/zha/services.yaml
+++ b/homeassistant/components/zha/services.yaml
@@ -163,3 +163,74 @@ warning_device_warn:
description: >-
Indicates the intensity of the strobe as shown in Table 8-23 of the ZCL spec. This attribute is designed to vary the output of the strobe (i.e., brightness) and not its frequency, which is detailed in section 8.4.2.3.1.6 of the ZCL spec.
example: 2
+
+clear_lock_user_code:
+ name: Clear lock user
+ description: Clear a user code from a lock
+ target:
+ entity:
+ domain: lock
+ integration: zha
+ fields:
+ code_slot:
+ name: Code slot
+ description: Code slot to clear code from
+ required: true
+ example: 1
+ selector:
+ text:
+
+enable_lock_user_code:
+ name: Enable lock user
+ description: Enable a user code on a lock
+ target:
+ entity:
+ domain: lock
+ integration: zha
+ fields:
+ code_slot:
+ name: Code slot
+ description: Code slot to enable
+ required: true
+ example: 1
+ selector:
+ text:
+
+disable_lock_user_code:
+ name: Disable lock user
+ description: Disable a user code on a lock
+ target:
+ entity:
+ domain: lock
+ integration: zha
+ fields:
+ code_slot:
+ name: Code slot
+ description: Code slot to disable
+ required: true
+ example: 1
+ selector:
+ text:
+
+set_lock_user_code:
+ name: Set lock user code
+ description: Set a user code on a lock
+ target:
+ entity:
+ domain: lock
+ integration: zha
+ fields:
+ code_slot:
+ name: Code slot
+ description: Code slot to set the code in
+ required: true
+ example: 1
+ selector:
+ text:
+ user_code:
+ name: Code
+ description: Code to set
+ required: true
+ example: 1234
+ selector:
+ text:
diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json
index 93b5cd7ccf5d6b..550fad3c2c5886 100644
--- a/homeassistant/components/zha/strings.json
+++ b/homeassistant/components/zha/strings.json
@@ -1,5 +1,6 @@
{
"config": {
+ "flow_title": "ZHA: {name}",
"step": {
"user": {
"title": "ZHA",
@@ -21,7 +22,9 @@
}
}
},
- "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ },
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py
index 60db9ec0a08b8b..75254f631b9d7f 100644
--- a/homeassistant/components/zha/switch.py
+++ b/homeassistant/components/zha/switch.py
@@ -1,6 +1,8 @@
"""Switches on Zigbee Home Automation networks."""
+from __future__ import annotations
+
import functools
-from typing import Any, List
+from typing import Any
from zigpy.zcl.clusters.general import OnOff
from zigpy.zcl.foundation import Status
@@ -113,7 +115,7 @@ class SwitchGroup(BaseSwitch, ZhaGroupEntity):
"""Representation of a switch group."""
def __init__(
- self, entity_ids: List[str], unique_id: str, group_id: int, zha_device, **kwargs
+ self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs
) -> None:
"""Initialize a switch group."""
super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs)
@@ -124,7 +126,7 @@ def __init__(
async def async_update(self) -> None:
"""Query all members and determine the light group state."""
all_states = [self.hass.states.get(x) for x in self._entity_ids]
- states: List[State] = list(filter(None, all_states))
+ states: list[State] = list(filter(None, all_states))
on_states = [state for state in states if state.state == STATE_ON]
self._state = len(on_states) > 0
diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json
index 7fb36f115324c3..20eee443960ffe 100644
--- a/homeassistant/components/zha/translations/ca.json
+++ b/homeassistant/components/zha/translations/ca.json
@@ -6,6 +6,7 @@
"error": {
"cannot_connect": "Ha fallat la connexi\u00f3"
},
+ "flow_title": "ZHA: {name}",
"step": {
"pick_radio": {
"data": {
diff --git a/homeassistant/components/zha/translations/cs.json b/homeassistant/components/zha/translations/cs.json
index 1ac4c7c2d6136b..2422941312b3f3 100644
--- a/homeassistant/components/zha/translations/cs.json
+++ b/homeassistant/components/zha/translations/cs.json
@@ -6,6 +6,7 @@
"error": {
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit"
},
+ "flow_title": "ZHA: {name}",
"step": {
"port_config": {
"data": {
@@ -51,20 +52,20 @@
"device_rotated": "Za\u0159\u00edzen\u00ed oto\u010deno \"{subtype}\"",
"device_shaken": "Za\u0159\u00edzen\u00ed se zat\u0159\u00e1slo",
"device_tilted": "Za\u0159\u00edzen\u00ed naklon\u011bno",
- "remote_button_alt_double_press": "Dvakr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)",
+ "remote_button_alt_double_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto dvakr\u00e1t (alternativn\u00ed re\u017eim)",
"remote_button_alt_long_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\" po dlouh\u00e9m stisku (alternativn\u00ed re\u017eim)",
- "remote_button_alt_quadruple_press": "\u010cty\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)",
- "remote_button_alt_quintuple_press": "P\u011btkr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)",
- "remote_button_alt_short_press": "Stiknuto tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)",
+ "remote_button_alt_quadruple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t (alternativn\u00ed re\u017eim)",
+ "remote_button_alt_quintuple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto p\u011btkr\u00e1t (alternativn\u00ed re\u017eim)",
+ "remote_button_alt_short_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto (alternativn\u00ed re\u017eim)",
"remote_button_alt_short_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)",
- "remote_button_alt_triple_press": "T\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)",
- "remote_button_double_press": "Dvakr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"",
+ "remote_button_alt_triple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto t\u0159ikr\u00e1t (alternativn\u00ed re\u017eim)",
+ "remote_button_double_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto dvakr\u00e1t",
"remote_button_long_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\" po dlouh\u00e9m stisku",
- "remote_button_quadruple_press": "\u010cty\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"",
- "remote_button_quintuple_press": "P\u011btkr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"",
- "remote_button_short_press": "Stiknuto tla\u010d\u00edtko \"{subtype}\"",
+ "remote_button_quadruple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t",
+ "remote_button_quintuple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto p\u011btkr\u00e1t",
+ "remote_button_short_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto",
"remote_button_short_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\"",
- "remote_button_triple_press": "T\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\""
+ "remote_button_triple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto t\u0159ikr\u00e1t"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json
index 592450fcfbc40b..a0cc570a900db4 100644
--- a/homeassistant/components/zha/translations/de.json
+++ b/homeassistant/components/zha/translations/de.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Es ist nur eine einzige Konfiguration von ZHA zul\u00e4ssig."
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"error": {
- "cannot_connect": "Kein Verbindung zu ZHA-Ger\u00e4t m\u00f6glich"
+ "cannot_connect": "Verbindung fehlgeschlagen"
},
+ "flow_title": "ZHA: {name}",
"step": {
"pick_radio": {
"data": {
diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json
index d9c78171d943d1..d3ed2ddfce4a10 100644
--- a/homeassistant/components/zha/translations/en.json
+++ b/homeassistant/components/zha/translations/en.json
@@ -6,6 +6,7 @@
"error": {
"cannot_connect": "Failed to connect"
},
+ "flow_title": "ZHA: {name}",
"step": {
"pick_radio": {
"data": {
diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json
index 4926476a5c68f6..d03300f9971c11 100644
--- a/homeassistant/components/zha/translations/et.json
+++ b/homeassistant/components/zha/translations/et.json
@@ -6,6 +6,7 @@
"error": {
"cannot_connect": "\u00dchendamine nurjus"
},
+ "flow_title": "ZHA: {name}",
"step": {
"pick_radio": {
"data": {
diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json
index 935663ed9e49a4..aaa41429fde758 100644
--- a/homeassistant/components/zha/translations/hu.json
+++ b/homeassistant/components/zha/translations/hu.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Csak egyetlen ZHA konfigur\u00e1ci\u00f3 megengedett."
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
},
"error": {
- "cannot_connect": "Nem lehet csatlakozni a ZHA eszk\u00f6zh\u00f6z."
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
+ "flow_title": "ZHA: {n\u00e9v}",
"step": {
"port_config": {
"data": {
@@ -17,5 +18,10 @@
"title": "ZHA"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "device_offline": "Eszk\u00f6z offline"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json
new file mode 100644
index 00000000000000..5baf04e13149e5
--- /dev/null
+++ b/homeassistant/components/zha/translations/id.json
@@ -0,0 +1,91 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "cannot_connect": "Gagal terhubung"
+ },
+ "step": {
+ "pick_radio": {
+ "data": {
+ "radio_type": "Jenis Radio"
+ },
+ "description": "Pilih jenis radio Zigbee Anda",
+ "title": "Jenis Radio"
+ },
+ "port_config": {
+ "data": {
+ "baudrate": "kecepatan port",
+ "flow_control": "kontrol data flow",
+ "path": "Jalur perangkat serial"
+ },
+ "description": "Masukkan pengaturan khusus port",
+ "title": "Setelan"
+ },
+ "user": {
+ "data": {
+ "path": "Jalur Perangkat Serial"
+ },
+ "description": "Pilih port serial untuk radio Zigbee",
+ "title": "ZHA"
+ }
+ }
+ },
+ "device_automation": {
+ "action_type": {
+ "squawk": "Squawk",
+ "warn": "Peringatkan"
+ },
+ "trigger_subtype": {
+ "both_buttons": "Kedua tombol",
+ "button_1": "Tombol pertama",
+ "button_2": "Tombol kedua",
+ "button_3": "Tombol ketiga",
+ "button_4": "Tombol keempat",
+ "button_5": "Tombol kelima",
+ "button_6": "Tombol keenam",
+ "close": "Tutup",
+ "dim_down": "Redupkan",
+ "dim_up": "Terangkan",
+ "face_1": "dengan wajah 1 diaktifkan",
+ "face_2": "dengan wajah 2 diaktifkan",
+ "face_3": "dengan wajah 3 diaktifkan",
+ "face_4": "dengan wajah 4 diaktifkan",
+ "face_5": "dengan wajah 5 diaktifkan",
+ "face_6": "dengan wajah 6 diaktifkan",
+ "face_any": "Dengan wajah apa pun/yang ditentukan diaktifkan",
+ "left": "Kiri",
+ "open": "Buka",
+ "right": "Kanan",
+ "turn_off": "Matikan",
+ "turn_on": "Nyalakan"
+ },
+ "trigger_type": {
+ "device_dropped": "Perangkat dijatuhkan",
+ "device_flipped": "Perangkat dibalik \"{subtype}\"",
+ "device_knocked": "Perangkat diketuk \"{subtype}\"",
+ "device_offline": "Perangkat offline",
+ "device_rotated": "Perangkat diputar \"{subtype}\"",
+ "device_shaken": "Perangkat diguncangkan",
+ "device_slid": "Perangkat diluncurkan \"{subtype}\"",
+ "device_tilted": "Perangkat dimiringkan",
+ "remote_button_alt_double_press": "Tombol \"{subtype}\" diklik dua kali (Mode alternatif)",
+ "remote_button_alt_long_press": "Tombol \"{subtype}\" terus ditekan (Mode alternatif)",
+ "remote_button_alt_long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama (Mode alternatif)",
+ "remote_button_alt_quadruple_press": "Tombol \"{subtype}\" diklik empat kali (Mode alternatif)",
+ "remote_button_alt_quintuple_press": "Tombol \"{subtype}\" diklik lima kali (Mode alternatif)",
+ "remote_button_alt_short_press": "Tombol \"{subtype}\" ditekan (Mode alternatif)",
+ "remote_button_alt_short_release": "Tombol \"{subtype}\" dilepaskan (Mode alternatif)",
+ "remote_button_alt_triple_press": "Tombol \"{subtype}\" diklik tiga kali (Mode alternatif)",
+ "remote_button_double_press": "Tombol \"{subtype}\" diklik dua kali",
+ "remote_button_long_press": "Tombol \"{subtype}\" terus ditekan",
+ "remote_button_long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama",
+ "remote_button_quadruple_press": "Tombol \"{subtype}\" diklik empat kali",
+ "remote_button_quintuple_press": "Tombol \"{subtype}\" diklik lima kali",
+ "remote_button_short_press": "Tombol \"{subtype}\" ditekan",
+ "remote_button_short_release": "Tombol \"{subtype}\" dilepaskan",
+ "remote_button_triple_press": "Tombol \"{subtype}\" diklik tiga kali"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json
index 066a45feab1d14..e97828d8e2ab4f 100644
--- a/homeassistant/components/zha/translations/it.json
+++ b/homeassistant/components/zha/translations/it.json
@@ -6,6 +6,7 @@
"error": {
"cannot_connect": "Impossibile connettersi"
},
+ "flow_title": "ZHA: {name}",
"step": {
"pick_radio": {
"data": {
diff --git a/homeassistant/components/zha/translations/ko.json b/homeassistant/components/zha/translations/ko.json
index 93582cc9202e9d..89dfaeba9be6f6 100644
--- a/homeassistant/components/zha/translations/ko.json
+++ b/homeassistant/components/zha/translations/ko.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "single_instance_allowed": "\ud558\ub098\uc758 ZHA \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"error": {
- "cannot_connect": "ZHA \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
},
+ "flow_title": "ZHA: {name}",
"step": {
"pick_radio": {
"data": {
@@ -34,8 +35,8 @@
},
"device_automation": {
"action_type": {
- "squawk": "\ube44\uc0c1",
- "warn": "\uacbd\uace0"
+ "squawk": "\uc2a4\ucffc\ud06c \ud558\uae30",
+ "warn": "\uacbd\uace0\ud558\uae30"
},
"trigger_subtype": {
"both_buttons": "\ub450 \uac1c",
@@ -63,28 +64,29 @@
},
"trigger_type": {
"device_dropped": "\uae30\uae30\uac00 \ub5a8\uc5b4\uc84c\uc744 \ub54c",
- "device_flipped": "\"{subtype}\" \uae30\uae30\uac00 \ub4a4\uc9d1\uc5b4\uc9c8 \ub54c",
- "device_knocked": "\"{subtype}\" \uae30\uae30\uac00 \ub450\ub4dc\ub824\uc9c8 \ub54c",
- "device_rotated": "\"{subtype}\" \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c",
- "device_shaken": "\uae30\uae30\uac00 \ud754\ub4e4\ub9b4 \ub54c",
- "device_slid": "\"{subtype}\" \uae30\uae30\uac00 \ubbf8\ub044\ub7ec\uc9c8 \ub54c",
- "device_tilted": "\uae30\uae30\uac00 \uae30\uc6b8\uc5b4\uc9c8 \ub54c",
- "remote_button_alt_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub9b4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)",
- "remote_button_alt_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub824\uc9c8 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)",
- "remote_button_alt_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c (\ub300\uccb4\ubaa8\ub4dc)",
- "remote_button_alt_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub124 \ubc88 \ub20c\ub9b4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)",
- "remote_button_alt_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub2e4\uc12f \ubc88 \ub20c\ub9b4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)",
- "remote_button_alt_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub9b4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)",
- "remote_button_alt_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)",
- "remote_button_alt_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)",
- "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub9b4 \ub54c",
- "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub824\uc9c8 \ub54c",
- "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c",
- "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub124 \ubc88 \ub20c\ub9b4 \ub54c",
- "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub2e4\uc12f \ubc88 \ub20c\ub9b4 \ub54c",
- "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub9b4 \ub54c",
- "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c",
- "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c"
+ "device_flipped": "\"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ub4a4\uc9d1\uc5b4\uc84c\uc744 \ub54c",
+ "device_knocked": "\"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ub450\ub4dc\ub824\uc84c\uc744 \ub54c",
+ "device_offline": "\uae30\uae30\uac00 \uc624\ud504\ub77c\uc778\uc774 \ub418\uc5c8\uc744 \ub54c",
+ "device_rotated": "\"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c",
+ "device_shaken": "\uae30\uae30\uac00 \ud754\ub4e4\ub838\uc744 \ub54c",
+ "device_slid": "\"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ubbf8\ub044\ub7ec\uc84c\uc744 \ub54c",
+ "device_tilted": "\uae30\uae30\uac00 \uae30\uc6b8\uc5b4\uc84c\uc744 \ub54c",
+ "remote_button_alt_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub838\uc744 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)",
+ "remote_button_alt_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub838\uc744 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)",
+ "remote_button_alt_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \ub5bc\uc600\uc744 \ub54c (\ub300\uccb4\ubaa8\ub4dc)",
+ "remote_button_alt_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub124 \ubc88 \ub20c\ub838\uc744 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)",
+ "remote_button_alt_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub2e4\uc12f \ubc88 \ub20c\ub838\uc744 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)",
+ "remote_button_alt_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub838\uc744 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)",
+ "remote_button_alt_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5bc\uc5c8\uc744 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)",
+ "remote_button_alt_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub838\uc744 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)",
+ "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub838\uc744 \ub54c",
+ "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub838\uc744 \ub54c",
+ "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \ub5bc\uc600\uc744 \ub54c",
+ "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub124 \ubc88 \ub20c\ub838\uc744 \ub54c",
+ "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub2e4\uc12f \ubc88 \ub20c\ub838\uc744 \ub54c",
+ "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub838\uc744 \ub54c",
+ "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5bc\uc5c8\uc744 \ub54c",
+ "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub838\uc744 \ub54c"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json
index 9a7a83e19d3284..83e3426dcbb3ae 100644
--- a/homeassistant/components/zha/translations/nl.json
+++ b/homeassistant/components/zha/translations/nl.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van ZHA is toegestaan."
+ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk."
},
"error": {
- "cannot_connect": "Kan geen verbinding maken met ZHA apparaat."
+ "cannot_connect": "Kan geen verbinding maken"
},
+ "flow_title": "ZHA: {name}",
"step": {
"pick_radio": {
"data": {
@@ -65,6 +66,7 @@
"device_dropped": "Apparaat gevallen",
"device_flipped": "Apparaat omgedraaid \"{subtype}\"",
"device_knocked": "Apparaat klopte \"{subtype}\"",
+ "device_offline": "Apparaat offline",
"device_rotated": "Apparaat gedraaid \" {subtype} \"",
"device_shaken": "Apparaat geschud",
"device_slid": "Apparaat geschoven \"{subtype}\"\".",
diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json
index a99e74fc6c79d3..3917ebd103b381 100644
--- a/homeassistant/components/zha/translations/no.json
+++ b/homeassistant/components/zha/translations/no.json
@@ -6,6 +6,7 @@
"error": {
"cannot_connect": "Tilkobling mislyktes"
},
+ "flow_title": "ZHA: {name}",
"step": {
"pick_radio": {
"data": {
diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json
index ab4402558ba9ca..f9b34d1be8297a 100644
--- a/homeassistant/components/zha/translations/pl.json
+++ b/homeassistant/components/zha/translations/pl.json
@@ -6,6 +6,7 @@
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
+ "flow_title": "ZHA: {name}",
"step": {
"pick_radio": {
"data": {
@@ -70,22 +71,22 @@
"device_shaken": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem",
"device_slid": "nast\u0105pi przesuni\u0119cie urz\u0105dzenia \"{subtype}\"",
"device_tilted": "nast\u0105pi przechylenie urz\u0105dzenia",
- "remote_button_alt_double_press": "\"{subtype}\" dwukrotnie naci\u015bni\u0119ty (tryb alternatywny)",
- "remote_button_alt_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y (tryb alternatywny)",
- "remote_button_alt_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu (tryb alternatywny)",
- "remote_button_alt_quadruple_press": "\"{subtype}\" czterokrotnie naci\u015bni\u0119ty (tryb alternatywny)",
- "remote_button_alt_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty (tryb alternatywny)",
- "remote_button_alt_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty (tryb alternatywny)",
- "remote_button_alt_short_release": "\"{subtype}\" zostanie zwolniony (tryb alternatywny)",
- "remote_button_alt_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty (tryb alternatywny)",
- "remote_button_double_press": "\"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty",
- "remote_button_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y",
- "remote_button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu",
- "remote_button_quadruple_press": "\"{subtype}\" czterokrotnie naci\u015bni\u0119ty",
- "remote_button_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty",
- "remote_button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty",
- "remote_button_short_release": "\"{subtype}\" zostanie zwolniony",
- "remote_button_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty"
+ "remote_button_alt_double_press": "przycisk \"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty (tryb alternatywny)",
+ "remote_button_alt_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y (tryb alternatywny)",
+ "remote_button_alt_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu (tryb alternatywny)",
+ "remote_button_alt_quadruple_press": "przycisk \"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty (tryb alternatywny)",
+ "remote_button_alt_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty (tryb alternatywny)",
+ "remote_button_alt_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty (tryb alternatywny)",
+ "remote_button_alt_short_release": "przycisk \"{subtype}\" zostanie zwolniony (tryb alternatywny)",
+ "remote_button_alt_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty (tryb alternatywny)",
+ "remote_button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty",
+ "remote_button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y",
+ "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu",
+ "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty",
+ "remote_button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty",
+ "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty",
+ "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony",
+ "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json
index 13394e5a293443..6ee8d58f50d09d 100644
--- a/homeassistant/components/zha/translations/ru.json
+++ b/homeassistant/components/zha/translations/ru.json
@@ -6,6 +6,7 @@
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
},
+ "flow_title": "ZHA: {name}",
"step": {
"pick_radio": {
"data": {
diff --git a/homeassistant/components/zha/translations/tr.json b/homeassistant/components/zha/translations/tr.json
new file mode 100644
index 00000000000000..a74f56a2f4e6be
--- /dev/null
+++ b/homeassistant/components/zha/translations/tr.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
+ "error": {
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "step": {
+ "pick_radio": {
+ "title": "Radyo Tipi"
+ },
+ "port_config": {
+ "data": {
+ "path": "Seri cihaz yolu"
+ },
+ "title": "Ayarlar"
+ }
+ }
+ },
+ "device_automation": {
+ "trigger_type": {
+ "device_offline": "Cihaz \u00e7evrimd\u0131\u015f\u0131"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zha/translations/uk.json b/homeassistant/components/zha/translations/uk.json
new file mode 100644
index 00000000000000..7bd62cf26e1dca
--- /dev/null
+++ b/homeassistant/components/zha/translations/uk.json
@@ -0,0 +1,91 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f"
+ },
+ "step": {
+ "pick_radio": {
+ "data": {
+ "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0456\u043e\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Zigbee",
+ "title": "\u0422\u0438\u043f \u0440\u0430\u0434\u0456\u043e\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ },
+ "port_config": {
+ "data": {
+ "baudrate": "\u0448\u0432\u0438\u0434\u043a\u0456\u0441\u0442\u044c \u043f\u043e\u0440\u0442\u0443",
+ "flow_control": "\u043a\u0435\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0442\u043e\u043a\u043e\u043c \u0434\u0430\u043d\u0438\u0445",
+ "path": "\u0428\u043b\u044f\u0445 \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ },
+ "description": "\u0412\u043a\u0430\u0436\u0456\u0442\u044c \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0440\u0442\u0443",
+ "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438"
+ },
+ "user": {
+ "data": {
+ "path": "\u0428\u043b\u044f\u0445 \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ },
+ "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u043e\u0441\u043b\u0456\u0434\u043e\u0432\u043d\u0438\u0439 \u043f\u043e\u0440\u0442 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u043e\u0440\u0430 \u043c\u0435\u0440\u0435\u0436\u0456 Zigbee",
+ "title": "Zigbee Home Automation"
+ }
+ }
+ },
+ "device_automation": {
+ "action_type": {
+ "squawk": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u043d\u0434\u0435\u0440",
+ "warn": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f \u043e\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u043d\u044f"
+ },
+ "trigger_subtype": {
+ "both_buttons": "\u041e\u0431\u0438\u0434\u0432\u0456 \u043a\u043d\u043e\u043f\u043a\u0438",
+ "button_1": "\u041f\u0435\u0440\u0448\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_2": "\u0414\u0440\u0443\u0433\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_3": "\u0422\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_5": "\u041f'\u044f\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_6": "\u0428\u043e\u0441\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430",
+ "close": "\u0417\u0430\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f",
+ "dim_down": "\u0417\u043c\u0435\u043d\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c",
+ "dim_up": "\u0417\u0431\u0456\u043b\u044c\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c",
+ "face_1": "\u041d\u0430 \u043f\u0435\u0440\u0448\u0456\u0439 \u0433\u0440\u0430\u043d\u0456",
+ "face_2": "\u041d\u0430 \u0434\u0440\u0443\u0433\u0438\u0439 \u0433\u0440\u0430\u043d\u0456",
+ "face_3": "\u041d\u0430 \u0442\u0440\u0435\u0442\u0456\u0439 \u0433\u0440\u0430\u043d\u0456",
+ "face_4": "\u041d\u0430 \u0447\u0435\u0442\u0432\u0435\u0440\u0442\u0456\u0439 \u0433\u0440\u0430\u043d\u0456",
+ "face_5": "\u041d\u0430 \u043f'\u044f\u0442\u0456\u0439 \u0433\u0440\u0430\u043d\u0456",
+ "face_6": "\u041d\u0430 \u0448\u043e\u0441\u0442\u0438\u0439 \u0433\u0440\u0430\u043d\u0456",
+ "face_any": "\u041d\u0430 \u0431\u0443\u0434\u044c-\u044f\u043a\u0456\u0439 \u0433\u0440\u0430\u043d\u0456",
+ "left": "\u041b\u0456\u0432\u043e\u0440\u0443\u0447",
+ "open": "\u0412\u0456\u0434\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f",
+ "right": "\u041f\u0440\u0430\u0432\u043e\u0440\u0443\u0447",
+ "turn_off": "\u0412\u0438\u043c\u043a\u043d\u0443\u0442\u0438",
+ "turn_on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438"
+ },
+ "trigger_type": {
+ "device_dropped": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0441\u043a\u0438\u043d\u0443\u043b\u0438",
+ "device_flipped": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 {subtype}",
+ "device_knocked": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c \u043f\u043e\u0441\u0442\u0443\u043a\u0430\u043b\u0438 {subtype}",
+ "device_offline": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456",
+ "device_rotated": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 {subtype}",
+ "device_shaken": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0442\u0440\u044f\u0441\u043b\u0438",
+ "device_slid": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0437\u0440\u0443\u0448\u0438\u043b\u0438 {subtype}",
+ "device_tilted": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0430\u0445\u0438\u043b\u0438\u043b\u0438",
+ "remote_button_alt_double_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0438 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)",
+ "remote_button_alt_long_press": "{subtype} \u0434\u043e\u0432\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)",
+ "remote_button_alt_long_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)",
+ "remote_button_alt_quadruple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0447\u043e\u0442\u0438\u0440\u0438 \u0440\u0430\u0437\u0438 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)",
+ "remote_button_alt_quintuple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u043f'\u044f\u0442\u044c \u0440\u0430\u0437\u0456\u0432 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)",
+ "remote_button_alt_short_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)",
+ "remote_button_alt_short_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)",
+ "remote_button_alt_triple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0438 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)",
+ "remote_button_double_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0438",
+ "remote_button_long_press": "{subtype} \u0434\u043e\u0432\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430",
+ "remote_button_long_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f",
+ "remote_button_quadruple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0447\u043e\u0442\u0438\u0440\u0438 \u0440\u0430\u0437\u0438",
+ "remote_button_quintuple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u043f'\u044f\u0442\u044c \u0440\u0430\u0437\u0456\u0432",
+ "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430",
+ "remote_button_short_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f",
+ "remote_button_triple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0438"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json
index 6582507431343b..c1ad9b82262481 100644
--- a/homeassistant/components/zha/translations/zh-Hant.json
+++ b/homeassistant/components/zha/translations/zh-Hant.json
@@ -6,6 +6,7 @@
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557"
},
+ "flow_title": "ZHA\uff1a{name}",
"step": {
"pick_radio": {
"data": {
diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py
index 68247537847f5b..f20c9f7e328ae4 100644
--- a/homeassistant/components/zhong_hong/climate.py
+++ b/homeassistant/components/zhong_hong/climate.py
@@ -95,7 +95,7 @@ def _start_hub():
async def startup():
"""Start hub socket after all climate entity is set up."""
nonlocal hub_is_initialized
- if not all([device.is_initialized for device in devices]):
+ if not all(device.is_initialized for device in devices):
return
if hub_is_initialized:
diff --git a/homeassistant/components/zodiac/sensor.py b/homeassistant/components/zodiac/sensor.py
index 5113c5c6e1821f..4c037a7aa021de 100644
--- a/homeassistant/components/zodiac/sensor.py
+++ b/homeassistant/components/zodiac/sensor.py
@@ -1,5 +1,5 @@
"""Support for tracking the zodiac sign."""
-from homeassistant.helpers.entity import Entity
+from homeassistant.components.sensor import SensorEntity
from homeassistant.util.dt import as_local, utcnow
from .const import (
@@ -162,7 +162,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities([ZodiacSensor()], True)
-class ZodiacSensor(Entity):
+class ZodiacSensor(SensorEntity):
"""Representation of a Zodiac sensor."""
def __init__(self):
@@ -196,7 +196,7 @@ def icon(self):
return ZODIAC_ICONS.get(self._state)
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the state attributes."""
return self._attrs
diff --git a/homeassistant/components/zodiac/translations/sensor.hu.json b/homeassistant/components/zodiac/translations/sensor.hu.json
new file mode 100644
index 00000000000000..8897339b74f87b
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.hu.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "V\u00edz\u00f6nt\u0151",
+ "aries": "Kos",
+ "cancer": "R\u00e1k",
+ "capricorn": "Bak",
+ "gemini": "Ikrek",
+ "leo": "Oroszl\u00e1n",
+ "libra": "M\u00e9rleg",
+ "pisces": "Halak",
+ "sagittarius": "Nyilas",
+ "scorpio": "Skorpi\u00f3",
+ "taurus": "Bika",
+ "virgo": "Sz\u0171z"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.id.json b/homeassistant/components/zodiac/translations/sensor.id.json
new file mode 100644
index 00000000000000..cd671e146ed712
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.id.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "Aquarius",
+ "aries": "Aries",
+ "cancer": "Cancer",
+ "capricorn": "Capricorn",
+ "gemini": "Gemini",
+ "leo": "Leo",
+ "libra": "Libra",
+ "pisces": "Pisces",
+ "sagittarius": "Sagittarius",
+ "scorpio": "Scorpio",
+ "taurus": "Taurus",
+ "virgo": "Virgo"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.ko.json b/homeassistant/components/zodiac/translations/sensor.ko.json
index 0a9fc83cdeacf9..88221b2f34e883 100644
--- a/homeassistant/components/zodiac/translations/sensor.ko.json
+++ b/homeassistant/components/zodiac/translations/sensor.ko.json
@@ -1,18 +1,18 @@
{
"state": {
"zodiac__sign": {
- "aquarius": "\ubb3c\ubcd1 \uc790\ub9ac",
- "aries": "\uc591 \uc790\ub9ac",
- "cancer": "\uac8c \uc790\ub9ac",
- "capricorn": "\uc5fc\uc18c \uc790\ub9ac",
- "gemini": "\uc30d\ub465\uc774 \uc790\ub9ac",
- "leo": "\uc0ac\uc790 \uc790\ub9ac",
- "libra": "\ucc9c\uce6d \uc790\ub9ac",
- "pisces": "\ubb3c\uace0\uae30 \uc790\ub9ac",
- "sagittarius": "\uad81\uc218 \uc790\ub9ac",
- "scorpio": "\uc804\uac08 \uc790\ub9ac",
- "taurus": "\ud669\uc18c \uc790\ub9ac",
- "virgo": "\ucc98\ub140 \uc790\ub9ac"
+ "aquarius": "\ubb3c\ubcd1\uc790\ub9ac",
+ "aries": "\uc591\uc790\ub9ac",
+ "cancer": "\uac8c\uc790\ub9ac",
+ "capricorn": "\uc5fc\uc18c\uc790\ub9ac",
+ "gemini": "\uc30d\ub465\uc774\uc790\ub9ac",
+ "leo": "\uc0ac\uc790\uc790\ub9ac",
+ "libra": "\ucc9c\uce6d\uc790\ub9ac",
+ "pisces": "\ubb3c\uace0\uae30\uc790\ub9ac",
+ "sagittarius": "\uad81\uc218\uc790\ub9ac",
+ "scorpio": "\uc804\uac08\uc790\ub9ac",
+ "taurus": "\ud669\uc18c\uc790\ub9ac",
+ "virgo": "\ucc98\ub140\uc790\ub9ac"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.nl.json b/homeassistant/components/zodiac/translations/sensor.nl.json
index c07b20de21b819..6dba645ed83832 100644
--- a/homeassistant/components/zodiac/translations/sensor.nl.json
+++ b/homeassistant/components/zodiac/translations/sensor.nl.json
@@ -3,6 +3,7 @@
"zodiac__sign": {
"aquarius": "Waterman",
"aries": "Ram",
+ "cancer": "Kreeft",
"capricorn": "Steenbok",
"gemini": "Tweelingen",
"leo": "Leo",
diff --git a/homeassistant/components/zodiac/translations/sensor.tr.json b/homeassistant/components/zodiac/translations/sensor.tr.json
new file mode 100644
index 00000000000000..f9e0357799d9b3
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.tr.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "Kova",
+ "aries": "Ko\u00e7",
+ "cancer": "Yenge\u00e7",
+ "capricorn": "O\u011flak",
+ "gemini": "Ikizler",
+ "leo": "Aslan",
+ "libra": "Terazi",
+ "pisces": "Bal\u0131k",
+ "sagittarius": "Yay",
+ "scorpio": "Akrep",
+ "taurus": "Bo\u011fa",
+ "virgo": "Ba\u015fak"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zodiac/translations/sensor.uk.json b/homeassistant/components/zodiac/translations/sensor.uk.json
new file mode 100644
index 00000000000000..e0c891a8b23d67
--- /dev/null
+++ b/homeassistant/components/zodiac/translations/sensor.uk.json
@@ -0,0 +1,18 @@
+{
+ "state": {
+ "zodiac__sign": {
+ "aquarius": "\u0412\u043e\u0434\u043e\u043b\u0456\u0439",
+ "aries": "\u041e\u0432\u0435\u043d",
+ "cancer": "\u0420\u0430\u043a",
+ "capricorn": "\u041a\u043e\u0437\u0435\u0440\u0456\u0433",
+ "gemini": "\u0411\u043b\u0438\u0437\u043d\u044e\u043a\u0438",
+ "leo": "\u041b\u0435\u0432",
+ "libra": "\u0422\u0435\u0440\u0435\u0437\u0438",
+ "pisces": "\u0420\u0438\u0431\u0438",
+ "sagittarius": "\u0421\u0442\u0440\u0456\u043b\u0435\u0446\u044c",
+ "scorpio": "\u0421\u043a\u043e\u0440\u043f\u0456\u043e\u043d",
+ "taurus": "\u0422\u0435\u043b\u0435\u0446\u044c",
+ "virgo": "\u0414\u0456\u0432\u0430"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py
index 01a8b9aa0f427f..4866c27807459c 100644
--- a/homeassistant/components/zone/__init__.py
+++ b/homeassistant/components/zone/__init__.py
@@ -1,6 +1,8 @@
"""Support for the definition of zones."""
+from __future__ import annotations
+
import logging
-from typing import Any, Dict, Optional, cast
+from typing import Any, Dict, cast
import voluptuous as vol
@@ -25,7 +27,6 @@
config_validation as cv,
entity,
entity_component,
- entity_registry,
service,
storage,
)
@@ -91,7 +92,7 @@ def empty_value(value: Any) -> Any:
@bind_hass
def async_active_zone(
hass: HomeAssistant, latitude: float, longitude: float, radius: int = 0
-) -> Optional[State]:
+) -> State | None:
"""Find the active zone for given latitude, longitude.
This method must be run in the event loop.
@@ -160,22 +161,22 @@ class ZoneStorageCollection(collection.StorageCollection):
CREATE_SCHEMA = vol.Schema(CREATE_FIELDS)
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
- async def _process_create_data(self, data: Dict) -> Dict:
+ async def _process_create_data(self, data: dict) -> dict:
"""Validate the config is valid."""
return cast(Dict, self.CREATE_SCHEMA(data))
@callback
- def _get_suggested_id(self, info: Dict) -> str:
+ def _get_suggested_id(self, info: dict) -> str:
"""Suggest an ID based on the config."""
return cast(str, info[CONF_NAME])
- async def _update_data(self, data: dict, update_data: Dict) -> Dict:
+ async def _update_data(self, data: dict, update_data: dict) -> dict:
"""Return a new updated data object."""
update_data = self.UPDATE_SCHEMA(update_data)
return {**data, **update_data}
-async def async_setup(hass: HomeAssistant, config: Dict) -> bool:
+async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up configured zones as well as Home Assistant zone if necessary."""
component = entity_component.EntityComponent(_LOGGER, DOMAIN, hass)
id_manager = collection.IDManager()
@@ -183,8 +184,8 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool:
yaml_collection = collection.IDLessCollection(
logging.getLogger(f"{__name__}.yaml_collection"), id_manager
)
- collection.attach_entity_component_collection(
- component, yaml_collection, lambda conf: Zone(conf, False)
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, component, yaml_collection, Zone.from_yaml
)
storage_collection = ZoneStorageCollection(
@@ -192,8 +193,8 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool:
logging.getLogger(f"{__name__}.storage_collection"),
id_manager,
)
- collection.attach_entity_component_collection(
- component, storage_collection, lambda conf: Zone(conf, True)
+ collection.sync_entity_lifecycle(
+ hass, DOMAIN, DOMAIN, component, storage_collection, Zone
)
if config[DOMAIN]:
@@ -205,18 +206,6 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool:
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
).async_setup(hass)
- async def _collection_changed(change_type: str, item_id: str, config: Dict) -> None:
- """Handle a collection change: clean up entity registry on removals."""
- if change_type != collection.CHANGE_REMOVED:
- return
-
- ent_reg = await entity_registry.async_get_registry(hass)
- ent_reg.async_remove(
- cast(str, ent_reg.async_get_entity_id(DOMAIN, DOMAIN, item_id))
- )
-
- storage_collection.async_add_listener(_collection_changed)
-
async def reload_service_handler(service_call: ServiceCall) -> None:
"""Remove all zones and load new ones from config."""
conf = await component.async_prepare_reload(skip_reset=True)
@@ -235,10 +224,7 @@ async def reload_service_handler(service_call: ServiceCall) -> None:
if component.get_entity("zone.home"):
return True
- home_zone = Zone(
- _home_conf(hass),
- True,
- )
+ home_zone = Zone(_home_conf(hass))
home_zone.entity_id = ENTITY_ID_HOME
await component.async_add_entities([home_zone])
@@ -254,7 +240,7 @@ async def core_config_updated(_: Event) -> None:
@callback
-def _home_conf(hass: HomeAssistant) -> Dict:
+def _home_conf(hass: HomeAssistant) -> dict:
"""Return the home zone config."""
return {
CONF_NAME: hass.config.location_name,
@@ -293,13 +279,21 @@ async def async_unload_entry(
class Zone(entity.Entity):
"""Representation of a Zone."""
- def __init__(self, config: Dict, editable: bool):
+ def __init__(self, config: dict):
"""Initialize the zone."""
self._config = config
- self._editable = editable
- self._attrs: Optional[Dict] = None
+ self.editable = True
+ self._attrs: dict | None = None
self._generate_attrs()
+ @classmethod
+ def from_yaml(cls, config: dict) -> Zone:
+ """Return entity instance initialized from yaml storage."""
+ zone = cls(config)
+ zone.editable = False
+ zone._generate_attrs() # pylint:disable=protected-access
+ return zone
+
@property
def state(self) -> str:
"""Return the state property really does nothing for a zone."""
@@ -311,17 +305,17 @@ def name(self) -> str:
return cast(str, self._config[CONF_NAME])
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return unique ID."""
return self._config.get(CONF_ID)
@property
- def icon(self) -> Optional[str]:
+ def icon(self) -> str | None:
"""Return the icon if any."""
return self._config.get(CONF_ICON)
@property
- def state_attributes(self) -> Optional[Dict]:
+ def extra_state_attributes(self) -> dict | None:
"""Return the state attributes of the zone."""
return self._attrs
@@ -330,7 +324,7 @@ def should_poll(self) -> bool:
"""Zone does not poll."""
return False
- async def async_update_config(self, config: Dict) -> None:
+ async def async_update_config(self, config: dict) -> None:
"""Handle when the config is updated."""
if self._config == config:
return
@@ -346,5 +340,5 @@ def _generate_attrs(self) -> None:
ATTR_LONGITUDE: self._config[CONF_LONGITUDE],
ATTR_RADIUS: self._config[CONF_RADIUS],
ATTR_PASSIVE: self._config[CONF_PASSIVE],
- ATTR_EDITABLE: self._editable,
+ ATTR_EDITABLE: self.editable,
}
diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py
index bb34a83ad260ce..de163146ab7e8c 100644
--- a/homeassistant/components/zone/config_flow.py
+++ b/homeassistant/components/zone/config_flow.py
@@ -6,7 +6,7 @@
"""
from homeassistant import config_entries
-from .const import DOMAIN # noqa # pylint:disable=unused-import
+from .const import DOMAIN
class ZoneConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
diff --git a/homeassistant/components/zone/translations/id.json b/homeassistant/components/zone/translations/id.json
index b84710dc408bd6..aa05923b561029 100644
--- a/homeassistant/components/zone/translations/id.json
+++ b/homeassistant/components/zone/translations/id.json
@@ -8,7 +8,7 @@
"data": {
"icon": "Ikon",
"latitude": "Lintang",
- "longitude": "Garis bujur",
+ "longitude": "Bujur",
"name": "Nama",
"passive": "Pasif",
"radius": "Radius"
diff --git a/homeassistant/components/zone/translations/tr.json b/homeassistant/components/zone/translations/tr.json
new file mode 100644
index 00000000000000..dad65ac92a7c11
--- /dev/null
+++ b/homeassistant/components/zone/translations/tr.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "init": {
+ "data": {
+ "latitude": "Enlem",
+ "longitude": "Boylam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py
index bc827c2ba0d70a..db5ca2cf01b1ab 100644
--- a/homeassistant/components/zone/trigger.py
+++ b/homeassistant/components/zone/trigger.py
@@ -37,6 +37,7 @@ async def async_attach_trigger(
hass, config, action, automation_info, *, platform_type: str = "zone"
) -> CALLBACK_TYPE:
"""Listen for state changes based on configuration."""
+ trigger_id = automation_info.get("trigger_id") if automation_info else None
entity_id = config.get(CONF_ENTITY_ID)
zone_entity_id = config.get(CONF_ZONE)
event = config.get(CONF_EVENT)
@@ -58,7 +59,7 @@ def zone_automation_listener(zone_event):
zone_state = hass.states.get(zone_entity_id)
from_match = condition.zone(hass, zone_state, from_s) if from_s else False
- to_match = condition.zone(hass, zone_state, to_s)
+ to_match = condition.zone(hass, zone_state, to_s) if to_s else False
if (
event == EVENT_ENTER
@@ -80,6 +81,7 @@ def zone_automation_listener(zone_event):
"zone": zone_state,
"event": event,
"description": description,
+ "id": trigger_id,
}
},
to_s.context,
diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py
index 75531e79e13b81..701f4b490d34bf 100644
--- a/homeassistant/components/zoneminder/sensor.py
+++ b/homeassistant/components/zoneminder/sensor.py
@@ -4,10 +4,9 @@
import voluptuous as vol
from zoneminder.monitor import TimePeriod
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_MONITORED_CONDITIONS
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from . import DOMAIN as ZONEMINDER_DOMAIN
@@ -57,7 +56,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(sensors)
-class ZMSensorMonitors(Entity):
+class ZMSensorMonitors(SensorEntity):
"""Get the status of each ZoneMinder monitor."""
def __init__(self, monitor):
@@ -91,7 +90,7 @@ def update(self):
self._is_available = self._monitor.is_available
-class ZMSensorEvents(Entity):
+class ZMSensorEvents(SensorEntity):
"""Get the number of events for each monitor."""
def __init__(self, monitor, include_archived, sensor_type):
@@ -122,7 +121,7 @@ def update(self):
self._state = self._monitor.get_events(self.time_period, self._include_archived)
-class ZMSensorRunState(Entity):
+class ZMSensorRunState(SensorEntity):
"""Get the ZoneMinder run state."""
def __init__(self, client):
diff --git a/homeassistant/components/zoneminder/translations/de.json b/homeassistant/components/zoneminder/translations/de.json
index 1362dcbd62dba3..5fa5d0a52345b1 100644
--- a/homeassistant/components/zoneminder/translations/de.json
+++ b/homeassistant/components/zoneminder/translations/de.json
@@ -1,10 +1,19 @@
{
"config": {
+ "abort": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
+ },
+ "error": {
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_auth": "Ung\u00fcltige Authentifizierung"
+ },
"flow_title": "ZoneMinder",
"step": {
"user": {
"data": {
"password": "Passwort",
+ "ssl": "Nutzt ein SSL-Zertifikat",
"username": "Benutzername",
"verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen"
}
diff --git a/homeassistant/components/zoneminder/translations/et.json b/homeassistant/components/zoneminder/translations/et.json
index 087158a450d13c..c8ccef3a44b835 100644
--- a/homeassistant/components/zoneminder/translations/et.json
+++ b/homeassistant/components/zoneminder/translations/et.json
@@ -23,7 +23,7 @@
"password": "Salas\u00f5na",
"path": "ZM aadress",
"path_zms": "ZMS-i aadress",
- "ssl": "Kasutage ZoneMinderiga \u00fchenduse loomiseks SSL-i",
+ "ssl": "Kasuta ZoneMinderiga \u00fchenduse loomiseks SSL-i",
"username": "Kasutajanimi",
"verify_ssl": "Kontrolli SSL sertifikaati"
},
diff --git a/homeassistant/components/zoneminder/translations/hu.json b/homeassistant/components/zoneminder/translations/hu.json
index f1f99fa2f7c44e..a40d92992519d6 100644
--- a/homeassistant/components/zoneminder/translations/hu.json
+++ b/homeassistant/components/zoneminder/translations/hu.json
@@ -1,13 +1,28 @@
{
"config": {
"abort": {
- "connection_error": "Nem siker\u00fclt csatlakozni a ZoneMinder szerverhez."
+ "auth_fail": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v vagy jelsz\u00f3",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "connection_error": "Nem siker\u00fclt csatlakozni a ZoneMinder szerverhez.",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
},
"create_entry": {
"default": "ZoneMinder szerver hozz\u00e1adva."
},
"error": {
- "connection_error": "Nem siker\u00fclt csatlakozni a ZoneMinder szerverhez."
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "connection_error": "Nem siker\u00fclt csatlakozni a ZoneMinder szerverhez.",
+ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v",
+ "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se"
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/id.json b/homeassistant/components/zoneminder/translations/id.json
new file mode 100644
index 00000000000000..25f1d98fc26d0d
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/id.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Nama pengguna atau kata sandi salah.",
+ "cannot_connect": "Gagal terhubung",
+ "connection_error": "Gagal terhubung ke server ZoneMinder.",
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "create_entry": {
+ "default": "Server ZoneMinder ditambahkan."
+ },
+ "error": {
+ "auth_fail": "Nama pengguna atau kata sandi salah.",
+ "cannot_connect": "Gagal terhubung",
+ "connection_error": "Gagal terhubung ke server ZoneMinder.",
+ "invalid_auth": "Autentikasi tidak valid"
+ },
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host dan Port (mis. 10.10.0.4:8010)",
+ "password": "Kata Sandi",
+ "path": "Jalur ZM",
+ "path_zms": "Jalur ZMS",
+ "ssl": "Menggunakan sertifikat SSL",
+ "username": "Nama Pengguna",
+ "verify_ssl": "Verifikasi sertifikat SSL"
+ },
+ "title": "Tambahkan Server ZoneMinder."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/ko.json b/homeassistant/components/zoneminder/translations/ko.json
index 3625d6e402ead4..bee0b7b6465af7 100644
--- a/homeassistant/components/zoneminder/translations/ko.json
+++ b/homeassistant/components/zoneminder/translations/ko.json
@@ -1,29 +1,33 @@
{
"config": {
"abort": {
- "auth_fail": "\uc0ac\uc6a9\uc790\uba85\uacfc \uc554\ud638\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.",
- "connection_error": "ZoneMinder \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4."
+ "auth_fail": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "connection_error": "ZoneMinder \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"create_entry": {
"default": "ZoneMinder \uc11c\ubc84\uac00 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
},
"error": {
- "auth_fail": "\uc0ac\uc6a9\uc790\uba85\uacfc \uc554\ud638\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.",
- "connection_error": "ZoneMinder \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4."
+ "auth_fail": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "connection_error": "ZoneMinder \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.",
+ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"flow_title": "ZoneMinder",
"step": {
"user": {
"data": {
- "host": "\ud638\uc2a4\ud2b8 \ubc0f \ud3ec\ud2b8(\uc608: 10.10.0.4:8010)",
- "password": "\uc554\ud638",
- "path": "ZMS \uacbd\ub85c",
+ "host": "\ud638\uc2a4\ud2b8 \ubc0f \ud3ec\ud2b8 (\uc608: 10.10.0.4:8010)",
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "path": "ZM \uacbd\ub85c",
"path_zms": "ZMS \uacbd\ub85c",
- "ssl": "ZoneMinder \uc5f0\uacb0\uc5d0 SSL \uc0ac\uc6a9",
- "username": "\uc0ac\uc6a9\uc790\uba85",
+ "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984",
"verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778"
},
- "title": "ZoneMinder \uc11c\ubc84\ub97c \ucd94\uac00\ud558\uc138\uc694."
+ "title": "ZoneMinder \uc11c\ubc84\ub97c \ucd94\uac00\ud574\uc8fc\uc138\uc694."
}
}
}
diff --git a/homeassistant/components/zoneminder/translations/nl.json b/homeassistant/components/zoneminder/translations/nl.json
index ebfd26329dcdb2..8aed5085391b50 100644
--- a/homeassistant/components/zoneminder/translations/nl.json
+++ b/homeassistant/components/zoneminder/translations/nl.json
@@ -12,7 +12,8 @@
"error": {
"auth_fail": "Gebruikersnaam of wachtwoord is onjuist.",
"cannot_connect": "Kon niet verbinden",
- "connection_error": "Kan geen verbinding maken met een ZoneMinder-server."
+ "connection_error": "Kan geen verbinding maken met een ZoneMinder-server.",
+ "invalid_auth": "Ongeldige authenticatie"
},
"flow_title": "ZoneMinder",
"step": {
@@ -22,7 +23,7 @@
"password": "Wachtwoord",
"path": "ZM-pad",
"path_zms": "ZMS-pad",
- "ssl": "Gebruik SSL voor verbindingen met ZoneMinder",
+ "ssl": "Gebruik een SSL-certificaat",
"username": "Gebruikersnaam",
"verify_ssl": "Verifieer SSL-certificaat"
},
diff --git a/homeassistant/components/zoneminder/translations/ru.json b/homeassistant/components/zoneminder/translations/ru.json
index d599e767f64a49..f520f0e29bd27b 100644
--- a/homeassistant/components/zoneminder/translations/ru.json
+++ b/homeassistant/components/zoneminder/translations/ru.json
@@ -1,19 +1,19 @@
{
"config": {
"abort": {
- "auth_fail": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.",
+ "auth_fail": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.",
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 ZoneMinder.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"create_entry": {
"default": "\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u0441\u0435\u0440\u0432\u0435\u0440 ZoneMinder."
},
"error": {
- "auth_fail": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.",
+ "auth_fail": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.",
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 ZoneMinder.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"flow_title": "ZoneMinder",
"step": {
@@ -24,7 +24,7 @@
"path": "\u041f\u0443\u0442\u044c \u043a ZM",
"path_zms": "\u041f\u0443\u0442\u044c \u043a ZMS",
"ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
- "username": "\u041b\u043e\u0433\u0438\u043d",
+ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f",
"verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL"
},
"title": "ZoneMinder"
diff --git a/homeassistant/components/zoneminder/translations/tr.json b/homeassistant/components/zoneminder/translations/tr.json
new file mode 100644
index 00000000000000..971f8cc9bd758c
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/tr.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "Kullan\u0131c\u0131 ad\u0131 veya \u015fifre yanl\u0131\u015f.",
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "error": {
+ "auth_fail": "Kullan\u0131c\u0131 ad\u0131 veya \u015fifre yanl\u0131\u015f.",
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 Ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/uk.json b/homeassistant/components/zoneminder/translations/uk.json
new file mode 100644
index 00000000000000..e5b04ae124f5e1
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/uk.json
@@ -0,0 +1,34 @@
+{
+ "config": {
+ "abort": {
+ "auth_fail": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043b\u043e\u0433\u0456\u043d \u0430\u0431\u043e \u043f\u0430\u0440\u043e\u043b\u044c.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "connection_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 ZoneMinder.",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "create_entry": {
+ "default": "\u0414\u043e\u0434\u0430\u043d\u043e \u0441\u0435\u0440\u0432\u0435\u0440 ZoneMinder."
+ },
+ "error": {
+ "auth_fail": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043b\u043e\u0433\u0456\u043d \u0430\u0431\u043e \u043f\u0430\u0440\u043e\u043b\u044c.",
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "connection_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 ZoneMinder.",
+ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f."
+ },
+ "flow_title": "ZoneMinder",
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442 \u0456 \u043f\u043e\u0440\u0442 (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 10.10.0.4:8010)",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "path": "\u0428\u043b\u044f\u0445 \u0434\u043e ZM",
+ "path_zms": "\u0428\u043b\u044f\u0445 \u0434\u043e ZMS",
+ "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL",
+ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430",
+ "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL"
+ },
+ "title": "ZoneMinder"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py
index 27f6c0a4801fb9..12ea668dce8670 100644
--- a/homeassistant/components/zwave/__init__.py
+++ b/homeassistant/components/zwave/__init__.py
@@ -11,6 +11,7 @@
from homeassistant import config_entries
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_NAME,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
)
@@ -36,7 +37,6 @@
from homeassistant.util import convert
import homeassistant.util.dt as dt_util
-from . import config_flow # noqa: F401 pylint: disable=unused-import
from . import const, websocket_api as wsapi, workaround
from .const import (
CONF_AUTOHEAL,
@@ -90,7 +90,7 @@
DEFAULT_CONF_REFRESH_VALUE = False
DEFAULT_CONF_REFRESH_DELAY = 5
-SUPPORTED_PLATFORMS = [
+PLATFORMS = [
"binary_sensor",
"climate",
"cover",
@@ -104,7 +104,7 @@
RENAME_NODE_SCHEMA = vol.Schema(
{
vol.Required(const.ATTR_NODE_ID): vol.Coerce(int),
- vol.Required(const.ATTR_NAME): cv.string,
+ vol.Required(ATTR_NAME): cv.string,
vol.Optional(const.ATTR_UPDATE_IDS, default=False): cv.boolean,
}
)
@@ -113,7 +113,7 @@
{
vol.Required(const.ATTR_NODE_ID): vol.Coerce(int),
vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int),
- vol.Required(const.ATTR_NAME): cv.string,
+ vol.Required(ATTR_NAME): cv.string,
vol.Optional(const.ATTR_UPDATE_IDS, default=False): cv.boolean,
}
)
@@ -397,7 +397,6 @@ async def async_setup_entry(hass, config_entry):
Will automatically load components to support devices found on the network.
"""
- # pylint: disable=import-error
from openzwave.group import ZWaveGroup
from openzwave.network import ZWaveNetwork
from openzwave.option import ZWaveOption
@@ -663,7 +662,7 @@ async def rename_node(service):
"""Rename a node."""
node_id = service.data.get(const.ATTR_NODE_ID)
node = network.nodes[node_id] # pylint: disable=unsubscriptable-object
- name = service.data.get(const.ATTR_NAME)
+ name = service.data.get(ATTR_NAME)
node.name = name
_LOGGER.info("Renamed Z-Wave node %d to %s", node_id, name)
update_ids = service.data.get(const.ATTR_UPDATE_IDS)
@@ -684,7 +683,7 @@ async def rename_value(service):
value_id = service.data.get(const.ATTR_VALUE_ID)
node = network.nodes[node_id] # pylint: disable=unsubscriptable-object
value = node.values[value_id]
- name = service.data.get(const.ATTR_NAME)
+ name = service.data.get(ATTR_NAME)
value.label = name
_LOGGER.info(
"Renamed Z-Wave value (Node %d Value %d) to %s", node_id, value_id, name
@@ -890,9 +889,7 @@ def reset_node_meters(service):
continue
network.manager.pressButton(value.value_id)
network.manager.releaseButton(value.value_id)
- _LOGGER.info(
- "Resetting meters on node %s instance %s....", node_id, instance
- )
+ _LOGGER.info("Resetting meters on node %s instance %s", node_id, instance)
return
_LOGGER.info(
"Node %s on instance %s does not have resettable meters", node_id, instance
@@ -916,7 +913,7 @@ def test_node(service):
def start_zwave(_service_or_event):
"""Startup Z-Wave network."""
- _LOGGER.info("Starting Z-Wave network...")
+ _LOGGER.info("Starting Z-Wave network")
network.start()
hass.bus.fire(const.EVENT_NETWORK_START)
@@ -940,7 +937,7 @@ async def _check_awaked():
"Z-Wave not ready after %d seconds, continuing anyway", waited
)
_LOGGER.info(
- "final network state: %d %s", network.state, network.state_str
+ "Final network state: %d %s", network.state, network.state_str
)
break
@@ -1062,7 +1059,7 @@ def _finalize_start():
hass.services.async_register(DOMAIN, const.SERVICE_START_NETWORK, start_zwave)
- for entry_component in SUPPORTED_PLATFORMS:
+ for entry_component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, entry_component)
)
@@ -1230,7 +1227,7 @@ async def discover_device(component, device):
return
self._hass.data[DATA_DEVICES][device.unique_id] = device
- if component in SUPPORTED_PLATFORMS:
+ if component in PLATFORMS:
async_dispatcher_send(self._hass, f"zwave_new_{component}", device)
else:
await discovery.async_load_platform(
@@ -1252,7 +1249,6 @@ class ZWaveDeviceEntity(ZWaveBaseEntity):
def __init__(self, values, domain):
"""Initialize the z-Wave device."""
- # pylint: disable=import-error
super().__init__()
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
@@ -1364,7 +1360,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
attrs = {
const.ATTR_NODE_ID: self.node_id,
diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py
index 9c9c1ed6128296..75780eb314a143 100644
--- a/homeassistant/components/zwave/climate.py
+++ b/homeassistant/components/zwave/climate.py
@@ -1,7 +1,8 @@
"""Support for Z-Wave climate devices."""
# Because we do not compile openzwave on CI
+from __future__ import annotations
+
import logging
-from typing import Optional, Tuple
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
@@ -181,17 +182,19 @@ def __init__(self, values, temp_unit):
int(self.node.manufacturer_id, 16),
int(self.node.product_id, 16),
)
- if specific_sensor_key in DEVICE_MAPPINGS:
- if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120:
- _LOGGER.debug("Remotec ZXT-120 Zwave Thermostat workaround")
- self._zxt_120 = 1
+ if (
+ specific_sensor_key in DEVICE_MAPPINGS
+ and DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120
+ ):
+ _LOGGER.debug("Remotec ZXT-120 Zwave Thermostat workaround")
+ self._zxt_120 = 1
self.update_properties()
def _mode(self) -> None:
"""Return thermostat mode Z-Wave value."""
raise NotImplementedError()
- def _current_mode_setpoints(self) -> Tuple:
+ def _current_mode_setpoints(self) -> tuple:
"""Return a tuple of current setpoint Z-Wave value(s)."""
raise NotImplementedError()
@@ -483,12 +486,12 @@ def target_temperature(self):
return self._target_temperature
@property
- def target_temperature_low(self) -> Optional[float]:
+ def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach."""
return self._target_temperature_range[0]
@property
- def target_temperature_high(self) -> Optional[float]:
+ def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach."""
return self._target_temperature_range[1]
@@ -566,14 +569,13 @@ def set_preset_mode(self, preset_mode):
def set_swing_mode(self, swing_mode):
"""Set new target swing mode."""
_LOGGER.debug("Set swing_mode to %s", swing_mode)
- if self._zxt_120 == 1:
- if self.values.zxt_120_swing_mode:
- self.values.zxt_120_swing_mode.data = swing_mode
+ if self._zxt_120 == 1 and self.values.zxt_120_swing_mode:
+ self.values.zxt_120_swing_mode.data = swing_mode
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the optional state attributes."""
- data = super().device_state_attributes
+ data = super().extra_state_attributes
if self._fan_action:
data[ATTR_FAN_ACTION] = self._fan_action
return data
@@ -590,7 +592,7 @@ def _mode(self) -> None:
"""Return thermostat mode Z-Wave value."""
return self.values.mode
- def _current_mode_setpoints(self) -> Tuple:
+ def _current_mode_setpoints(self) -> tuple:
"""Return a tuple of current setpoint Z-Wave value(s)."""
return (self.values.primary,)
@@ -606,7 +608,7 @@ def _mode(self) -> None:
"""Return thermostat mode Z-Wave value."""
return self.values.primary
- def _current_mode_setpoints(self) -> Tuple:
+ def _current_mode_setpoints(self) -> tuple:
"""Return a tuple of current setpoint Z-Wave value(s)."""
current_mode = str(self.values.primary.data).lower()
setpoints_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ())
diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py
index 83fb43fd3fbe79..d11d308c4907b8 100644
--- a/homeassistant/components/zwave/const.py
+++ b/homeassistant/components/zwave/const.py
@@ -8,7 +8,6 @@
ATTR_GROUP = "group"
ATTR_VALUE_ID = "value_id"
ATTR_MESSAGES = "messages"
-ATTR_NAME = "name"
ATTR_RETURN_ROUTES = "return_routes"
ATTR_SCENE_ID = "scene_id"
ATTR_SCENE_DATA = "scene_data"
diff --git a/homeassistant/components/zwave/fan.py b/homeassistant/components/zwave/fan.py
index ea529ccd90bd10..7fb0fb8e8bee5f 100644
--- a/homeassistant/components/zwave/fan.py
+++ b/homeassistant/components/zwave/fan.py
@@ -5,6 +5,7 @@
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import (
+ int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@@ -68,6 +69,11 @@ def percentage(self):
"""Return the current speed percentage."""
return ranged_value_to_percentage(SPEED_RANGE, self._state)
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return int_states_in_range(SPEED_RANGE)
+
@property
def supported_features(self):
"""Flag supported features."""
diff --git a/homeassistant/components/zwave/light.py b/homeassistant/components/zwave/light.py
index 244b4a557e1ee3..140f601b1d9f55 100644
--- a/homeassistant/components/zwave/light.py
+++ b/homeassistant/components/zwave/light.py
@@ -138,10 +138,12 @@ def __init__(self, values, refresh, delay):
int(self.node.manufacturer_id, 16),
int(self.node.product_id, 16),
)
- if specific_sensor_key in DEVICE_MAPPINGS:
- if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098:
- _LOGGER.debug("AEOTEC ZW098 workaround enabled")
- self._zw098 = 1
+ if (
+ specific_sensor_key in DEVICE_MAPPINGS
+ and DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098
+ ):
+ _LOGGER.debug("AEOTEC ZW098 workaround enabled")
+ self._zw098 = 1
# Used for value change event handling
self._refreshing = False
@@ -175,7 +177,7 @@ def _refresh_value():
self._refreshing = True
self.values.primary.refresh()
- if self._timer is not None and self._timer.isAlive():
+ if self._timer is not None and self._timer.is_alive():
self._timer.cancel()
self._timer = Timer(self._delay, _refresh_value)
diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py
index c9601679f573f9..bc49f9c0bd222e 100644
--- a/homeassistant/components/zwave/lock.py
+++ b/homeassistant/components/zwave/lock.py
@@ -95,7 +95,7 @@
"27": "Auto re-lock",
"33": "User deleted: ",
"112": "Master code changed or User added: ",
- "113": "Duplicate Pin-code: ",
+ "113": "Duplicate PIN code: ",
"130": "RF module, power restored",
"144": "Unlocked by NFC Tag or Card by user ",
"161": "Tamper Alarm: ",
@@ -291,17 +291,17 @@ def update_properties(self):
if self._state_workaround:
self._state = LOCK_STATUS.get(str(notification_data))
_LOGGER.debug("workaround: lock state set to %s", self._state)
- if self._v2btze:
- if (
- self.values.v2btze_advanced
- and self.values.v2btze_advanced.data == CONFIG_ADVANCED
- ):
- self._state = LOCK_STATUS.get(str(notification_data))
- _LOGGER.debug(
- "Lock state set from Access Control value and is %s, get=%s",
- str(notification_data),
- self.state,
- )
+ if (
+ self._v2btze
+ and self.values.v2btze_advanced
+ and self.values.v2btze_advanced.data == CONFIG_ADVANCED
+ ):
+ self._state = LOCK_STATUS.get(str(notification_data))
+ _LOGGER.debug(
+ "Lock state set from Access Control value and is %s, get=%s",
+ str(notification_data),
+ self.state,
+ )
if self._track_message_workaround:
this_message = self.node.stats["lastReceivedMessage"][5]
@@ -374,9 +374,9 @@ def unlock(self, **kwargs):
self.values.primary.data = False
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
- data = super().device_state_attributes
+ data = super().extra_state_attributes
if self._notification:
data[ATTR_NOTIFICATION] = self._notification
if self._lock_status:
diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json
index a3a2b5e0d83e39..6623036d2fea94 100644
--- a/homeassistant/components/zwave/manifest.json
+++ b/homeassistant/components/zwave/manifest.json
@@ -1,6 +1,6 @@
{
"domain": "zwave",
- "name": "Z-Wave",
+ "name": "Z-Wave (deprecated)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave",
"requirements": ["homeassistant-pyozw==0.1.10", "pydispatcher==2.0.5"],
diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py
index 56dea1639a3068..3fa26439ad5642 100644
--- a/homeassistant/components/zwave/node_entity.py
+++ b/homeassistant/components/zwave/node_entity.py
@@ -95,7 +95,7 @@ def try_remove_and_add(self):
"""Remove this entity and add it back."""
async def _async_remove_and_add():
- await self.async_remove()
+ await self.async_remove(force_remove=True)
self.entity_id = None
await self.platform.async_add_entities([self])
@@ -104,7 +104,7 @@ async def _async_remove_and_add():
async def node_removed(self):
"""Call when a node is removed from the Z-Wave network."""
- await self.async_remove()
+ await self.async_remove(force_remove=True)
registry = await async_get_registry(self.hass)
if self.entity_id not in registry.entities:
@@ -118,7 +118,6 @@ class ZWaveNodeEntity(ZWaveBaseEntity):
def __init__(self, node, network):
"""Initialize node."""
- # pylint: disable=import-error
super().__init__()
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
@@ -352,7 +351,7 @@ def name(self):
return self._name
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return the device specific state attributes."""
attrs = {
ATTR_NODE_ID: self.node_id,
diff --git a/homeassistant/components/zwave/sensor.py b/homeassistant/components/zwave/sensor.py
index aae38382f2e720..a3183ba8927e8f 100644
--- a/homeassistant/components/zwave/sensor.py
+++ b/homeassistant/components/zwave/sensor.py
@@ -1,5 +1,5 @@
"""Support for Z-Wave sensors."""
-from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, DOMAIN
+from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, DOMAIN, SensorEntity
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -37,7 +37,7 @@ def get_device(node, values, **kwargs):
return None
-class ZWaveSensor(ZWaveDeviceEntity):
+class ZWaveSensor(ZWaveDeviceEntity, SensorEntity):
"""Representation of a Z-Wave sensor."""
def __init__(self, values):
diff --git a/homeassistant/components/zwave/strings.json b/homeassistant/components/zwave/strings.json
index 852b8ca22fab11..69401b171e2e7d 100644
--- a/homeassistant/components/zwave/strings.json
+++ b/homeassistant/components/zwave/strings.json
@@ -2,8 +2,7 @@
"config": {
"step": {
"user": {
- "title": "Set up Z-Wave",
- "description": "See https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables",
+ "description": "This integration is no longer maintained. For new installations, use Z-Wave JS instead.\n\nSee https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables",
"data": {
"usb_path": "[%key:common::config_flow::data::usb_path%]",
"network_key": "Network Key (leave blank to auto-generate)"
diff --git a/homeassistant/components/zwave/translations/ca.json b/homeassistant/components/zwave/translations/ca.json
index 3c97d8c212f25c..13805a2d1ed9e3 100644
--- a/homeassistant/components/zwave/translations/ca.json
+++ b/homeassistant/components/zwave/translations/ca.json
@@ -13,7 +13,7 @@
"network_key": "Clau de xarxa (deixa-ho en blanc per generar-la autom\u00e0ticament)",
"usb_path": "Ruta del port USB del dispositiu"
},
- "description": "Consulta https://www.home-assistant.io/docs/z-wave/installation/ per obtenir informaci\u00f3 sobre les variables de configuraci\u00f3",
+ "description": "Aquesta integraci\u00f3 ja no s'actualitzar\u00e0. Utilitza Z-Wave JS per a instal\u00b7lacions noves.\n\nConsulta https://www.home-assistant.io/docs/z-wave/installation/ per a m\u00e9s informaci\u00f3 sobre les variables de configuraci\u00f3",
"title": "Configuraci\u00f3 de Z-Wave"
}
}
diff --git a/homeassistant/components/zwave/translations/de.json b/homeassistant/components/zwave/translations/de.json
index 60b5aa88024605..f592c2243aca53 100644
--- a/homeassistant/components/zwave/translations/de.json
+++ b/homeassistant/components/zwave/translations/de.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Z-Wave ist bereits konfiguriert"
+ "already_configured": "Z-Wave ist bereits konfiguriert",
+ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"error": {
"option_error": "Z-Wave-Validierung fehlgeschlagen. Ist der Pfad zum USB-Stick korrekt?"
diff --git a/homeassistant/components/zwave/translations/en.json b/homeassistant/components/zwave/translations/en.json
index d13e5575e610ac..2fe3e15646a866 100644
--- a/homeassistant/components/zwave/translations/en.json
+++ b/homeassistant/components/zwave/translations/en.json
@@ -13,7 +13,7 @@
"network_key": "Network Key (leave blank to auto-generate)",
"usb_path": "USB Device Path"
},
- "description": "See https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables",
+ "description": "This integration is no longer maintained. For new installations, use Z-Wave JS instead.\n\nSee https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables",
"title": "Set up Z-Wave"
}
}
diff --git a/homeassistant/components/zwave/translations/et.json b/homeassistant/components/zwave/translations/et.json
index ef36101b3ad057..b1fa6127076fd5 100644
--- a/homeassistant/components/zwave/translations/et.json
+++ b/homeassistant/components/zwave/translations/et.json
@@ -13,7 +13,7 @@
"network_key": "V\u00f5rguv\u00f5ti (j\u00e4ta automaatse genereerimise jaoks t\u00fchjaks)",
"usb_path": "USB seadme rada"
},
- "description": "Konfiguratsioonimuutujate kohta leiad teavet https://www.home-assistant.io/docs/z-wave/installation/",
+ "description": "Seda sidumist enam ei hallata. Uueks sidumiseks kasuta Z-Wave JS.\n\nKonfiguratsioonimuutujate kohta leiad teavet https://www.home-assistant.io/docs/z-wave/installation/",
"title": "Seadista Z-Wave"
}
}
diff --git a/homeassistant/components/zwave/translations/hu.json b/homeassistant/components/zwave/translations/hu.json
index 240e7fe776c13f..68a19863b53f25 100644
--- a/homeassistant/components/zwave/translations/hu.json
+++ b/homeassistant/components/zwave/translations/hu.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "A Z-Wave m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
},
"error": {
"option_error": "A Z-Wave \u00e9rv\u00e9nyes\u00edt\u00e9s sikertelen. Az USB-meghajt\u00f3 el\u00e9r\u00e9si \u00fatj\u00e1t helyesen adtad meg?"
diff --git a/homeassistant/components/zwave/translations/id.json b/homeassistant/components/zwave/translations/id.json
index 76c9c148b1eb71..99bd62703263e8 100644
--- a/homeassistant/components/zwave/translations/id.json
+++ b/homeassistant/components/zwave/translations/id.json
@@ -1,4 +1,23 @@
{
+ "config": {
+ "abort": {
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+ },
+ "error": {
+ "option_error": "Validasi Z-Wave gagal. Apakah jalur ke stik USB sudah benar?"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "network_key": "Kunci Jaringan (biarkan kosong untuk dibuat secara otomatis)",
+ "usb_path": "Jalur Perangkat USB"
+ },
+ "description": "Integrasi ini tidak lagi dipertahankan. Untuk instalasi baru, gunakan Z-Wave JS sebagai gantinya.\n\nBaca https://www.home-assistant.io/docs/z-wave/installation/ untuk informasi tentang variabel konfigurasi",
+ "title": "Siapkan Z-Wave"
+ }
+ }
+ },
"state": {
"_": {
"dead": "Mati",
@@ -7,8 +26,8 @@
"sleeping": "Tidur"
},
"query_stage": {
- "dead": "Mati ({query_stage})",
- "initializing": "Inisialisasi ( {query_stage} )"
+ "dead": "Mati",
+ "initializing": "Inisialisasi"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zwave/translations/it.json b/homeassistant/components/zwave/translations/it.json
index 0534d54f32f493..d3522cf0889551 100644
--- a/homeassistant/components/zwave/translations/it.json
+++ b/homeassistant/components/zwave/translations/it.json
@@ -13,7 +13,7 @@
"network_key": "Chiave di rete (lascia vuoto per generare automaticamente)",
"usb_path": "Percorso del dispositivo USB"
},
- "description": "Vai su https://www.home-assistant.io/docs/z-wave/installation/ per le informazioni sulle variabili di configurazione",
+ "description": "Questa integrazione non viene pi\u00f9 mantenuta. Per le nuove installazioni, usa invece Z-Wave JS. \n\nVedere https://www.home-assistant.io/docs/z-wave/installation/ per informazioni sulle variabili di configurazione",
"title": "Configura Z-Wave"
}
}
diff --git a/homeassistant/components/zwave/translations/ko.json b/homeassistant/components/zwave/translations/ko.json
index 1357fd492c5bd6..674476ac7598c1 100644
--- a/homeassistant/components/zwave/translations/ko.json
+++ b/homeassistant/components/zwave/translations/ko.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Z-Wave \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"error": {
"option_error": "Z-Wave \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. USB \uc2a4\ud2f1\uc758 \uacbd\ub85c\uac00 \uc815\ud655\ud569\ub2c8\uae4c?"
@@ -12,7 +13,7 @@
"network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4 (\uacf5\ub780\uc73c\ub85c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uc0dd\uc131\ud569\ub2c8\ub2e4)",
"usb_path": "USB \uc7a5\uce58 \uacbd\ub85c"
},
- "description": "\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/docs/z-wave/installation/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694",
+ "description": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \ub354 \uc774\uc0c1 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc0c8\ub85c\uc6b4 \uc124\uce58\uc758 \uacbd\uc6b0 Z-Wave JS \ub97c \uc0ac\uc6a9\ud574\uc8fc\uc138\uc694.\n\n\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/docs/z-wave/installation/ \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694",
"title": "Z-Wave \uc124\uc815"
}
}
diff --git a/homeassistant/components/zwave/translations/nl.json b/homeassistant/components/zwave/translations/nl.json
index 50e81003d27d26..a366d1d50df8c8 100644
--- a/homeassistant/components/zwave/translations/nl.json
+++ b/homeassistant/components/zwave/translations/nl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Z-Wave is al geconfigureerd",
+ "already_configured": "Apparaat is al geconfigureerd",
"single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk."
},
"error": {
@@ -11,9 +11,9 @@
"user": {
"data": {
"network_key": "Netwerksleutel (laat leeg om automatisch te genereren)",
- "usb_path": "USB-pad"
+ "usb_path": "USB-apparaatpad"
},
- "description": "Zie https://www.home-assistant.io/docs/z-wave/installation/ voor informatie over de configuratievariabelen",
+ "description": "Deze integratie wordt niet langer onderhouden. Voor nieuwe installaties, gebruik Z-Wave JS in plaats daarvan.\n\nZie https://www.home-assistant.io/docs/z-wave/installation/ voor informatie over de configuratievariabelen",
"title": "Stel Z-Wave in"
}
}
diff --git a/homeassistant/components/zwave/translations/no.json b/homeassistant/components/zwave/translations/no.json
index ba875354f7f6f7..ab5a405f975392 100644
--- a/homeassistant/components/zwave/translations/no.json
+++ b/homeassistant/components/zwave/translations/no.json
@@ -13,7 +13,7 @@
"network_key": "Nettverksn\u00f8kkel (la v\u00e6re tom for automatisk oppretting)",
"usb_path": "USB enhetsbane"
},
- "description": "Se [www.home-assistant.io/docs/z-wave/installation/](https://www.home-assistant.io/docs/z-wave/installation/) for informasjon om konfigurasjon variablene",
+ "description": "Denne integrasjonen opprettholdes ikke lenger. For nye installasjoner, bruk Z-Wave JS i stedet. \n\n Se https://www.home-assistant.io/docs/z-wave/installation/ for informasjon om konfigurasjonsvariablene",
"title": "Sett opp Z-Wave"
}
}
diff --git a/homeassistant/components/zwave/translations/pl.json b/homeassistant/components/zwave/translations/pl.json
index 90ff1a37894c52..0a4b6a4828c78e 100644
--- a/homeassistant/components/zwave/translations/pl.json
+++ b/homeassistant/components/zwave/translations/pl.json
@@ -13,7 +13,7 @@
"network_key": "Klucz sieciowy (pozostaw pusty, by generowa\u0107 automatycznie)",
"usb_path": "\u015acie\u017cka urz\u0105dzenia USB"
},
- "description": "Przejd\u017a na https://www.home-assistant.io/docs/z-wave/installation/, aby uzyska\u0107 informacje na temat zmiennych konfiguracyjnych",
+ "description": "Ta integracja nie jest ju\u017c wspierana. Dla nowych instalacji, u\u017cyj Z-Wave JS.\n\nPrzejd\u017a na https://www.home-assistant.io/docs/z-wave/installation/, aby uzyska\u0107 informacje na temat zmiennych konfiguracyjnych",
"title": "Konfiguracja Z-Wave"
}
}
diff --git a/homeassistant/components/zwave/translations/ru.json b/homeassistant/components/zwave/translations/ru.json
index 515a47d87a62ce..5188bb8330e0d2 100644
--- a/homeassistant/components/zwave/translations/ru.json
+++ b/homeassistant/components/zwave/translations/ru.json
@@ -13,7 +13,7 @@
"network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)",
"usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443"
},
- "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.",
+ "description": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0432\u043c\u0435\u0441\u0442\u043e \u043d\u0435\u0451 Z-Wave JS.\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.",
"title": "Z-Wave"
}
}
diff --git a/homeassistant/components/zwave/translations/tr.json b/homeassistant/components/zwave/translations/tr.json
index 3938868d2808c1..383ccc6cc4f805 100644
--- a/homeassistant/components/zwave/translations/tr.json
+++ b/homeassistant/components/zwave/translations/tr.json
@@ -1,5 +1,9 @@
{
"config": {
+ "abort": {
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/zwave/translations/uk.json b/homeassistant/components/zwave/translations/uk.json
index d00986cae5807f..5cdd6060cc4b3f 100644
--- a/homeassistant/components/zwave/translations/uk.json
+++ b/homeassistant/components/zwave/translations/uk.json
@@ -1,14 +1,33 @@
{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.",
+ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e."
+ },
+ "error": {
+ "option_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 Z-Wave. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0448\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "network_key": "\u041a\u043b\u044e\u0447 \u043c\u0435\u0440\u0435\u0436\u0456 (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)",
+ "usb_path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e"
+ },
+ "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.",
+ "title": "Z-Wave"
+ }
+ }
+ },
"state": {
"_": {
- "dead": "\u041d\u0435\u0440\u043e\u0431\u043e\u0447\u0430",
+ "dead": "\u041d\u0435\u0441\u043f\u0440\u0430\u0432\u043d\u0438\u0439",
"initializing": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f",
"ready": "\u0413\u043e\u0442\u043e\u0432\u0438\u0439",
- "sleeping": "\u0421\u043f\u043b\u044f\u0447\u043a\u0430"
+ "sleeping": "\u0420\u0435\u0436\u0438\u043c \u0441\u043d\u0443"
},
"query_stage": {
- "dead": "\u041d\u0435\u0440\u043e\u0431\u043e\u0447\u0430 ({query_stage})",
- "initializing": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f ( {query_stage} )"
+ "dead": "\u041d\u0435\u0441\u043f\u0440\u0430\u0432\u043d\u0438\u0439",
+ "initializing": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zwave/translations/zh-Hant.json b/homeassistant/components/zwave/translations/zh-Hant.json
index f5c07a9efc9326..545da7b2ee74f2 100644
--- a/homeassistant/components/zwave/translations/zh-Hant.json
+++ b/homeassistant/components/zwave/translations/zh-Hant.json
@@ -13,7 +13,7 @@
"network_key": "\u7db2\u8def\u5bc6\u9470\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09",
"usb_path": "USB \u88dd\u7f6e\u8def\u5f91"
},
- "description": "\u95dc\u65bc\u8a2d\u5b9a\u8b8a\u6578\u8cc7\u8a0a\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/docs/z-wave/installation/",
+ "description": "\u6b64\u6574\u5408\u5df2\u7d93\u4e0d\u518d\u9032\u884c\u7dad\u8b77\uff0c\u8acb\u4f7f\u7528 Z-Wave JS \u53d6\u4ee3\u70ba\u65b0\u5b89\u88dd\u65b9\u5f0f\u3002\n\n\u8acb\u53c3\u95b1 https://www.home-assistant.io/docs/z-wave/installation/ \u4ee5\n\u7372\u5f97\u8a2d\u5b9a\u8b8a\u6578\u8cc7\u8a0a",
"title": "\u8a2d\u5b9a Z-Wave"
}
}
diff --git a/homeassistant/components/zwave_js/README.md b/homeassistant/components/zwave_js/README.md
new file mode 100644
index 00000000000000..920fc4a6a0b60a
--- /dev/null
+++ b/homeassistant/components/zwave_js/README.md
@@ -0,0 +1,49 @@
+# Z-Wave JS Architecture
+
+This document describes the architecture of Z-Wave JS in Home Assistant and how the integration is connected all the way to the Z-Wave USB stick controller.
+
+## Architecture
+
+### Connection diagram
+
+![alt text][connection_diagram]
+
+#### Z-Wave USB stick
+
+Communicates with devices via the Z-Wave radio and stores device pairing.
+
+#### Z-Wave JS
+
+Represents the USB stick serial protocol as devices.
+
+#### Z-Wave JS Server
+
+Forward the state of Z-Wave JS over a WebSocket connection.
+
+#### Z-Wave JS Server Python
+
+Consumes the WebSocket connection and makes the Z-Wave JS state available in Python.
+
+#### Z-Wave JS integration
+
+Represents Z-Wave devices in Home Assistant and allows control.
+
+#### Home Assistant
+
+Best home automation platform in the world.
+
+### Running Z-Wave JS Server
+
+![alt text][running_zwave_js_server]
+
+Z-Wave JS Server can be run as a standalone Node app.
+
+It can also run as part of Z-Wave JS 2 MQTT, which is also a standalone Node app.
+
+Both apps are available as Home Assistant add-ons. There are also Docker containers etc.
+
+[connection_diagram]: docs/z_wave_js_connection.png "Connection Diagram"
+[//]: # (https://docs.google.com/drawings/d/10yrczSRwV4kjQwzDnCLGoAJkePaB0BMVb1sWZeeDO7U/edit?usp=sharing)
+
+[running_zwave_js_server]: docs/running_z_wave_js_server.png "Running Z-Wave JS Server"
+[//]: # (https://docs.google.com/drawings/d/1YhSVNuss3fa1VFTKQLaACxXg7y6qo742n2oYpdLRs7E/edit?usp=sharing)
diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py
index c995749f924c00..10cc2543921e83 100644
--- a/homeassistant/components/zwave_js/__init__.py
+++ b/homeassistant/components/zwave_js/__init__.py
@@ -1,34 +1,77 @@
"""The Z-Wave JS integration."""
+from __future__ import annotations
+
import asyncio
-import logging
-from typing import Tuple
+from typing import Callable
from async_timeout import timeout
from zwave_js_server.client import Client as ZwaveClient
+from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion
from zwave_js_server.model.node import Node as ZwaveNode
+from zwave_js_server.model.notification import (
+ EntryControlNotification,
+ NotificationNotification,
+)
+from zwave_js_server.model.value import ValueNotification
-from homeassistant.components.hassio.handler import HassioAPIError
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
+from homeassistant.const import (
+ ATTR_DEVICE_ID,
+ ATTR_DOMAIN,
+ CONF_URL,
+ EVENT_HOMEASSISTANT_STOP,
+)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers import device_registry
+from homeassistant.helpers import device_registry, entity_registry
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
+from .addon import AddonError, AddonManager, get_addon_manager
from .api import async_register_api
from .const import (
+ ATTR_COMMAND_CLASS,
+ ATTR_COMMAND_CLASS_NAME,
+ ATTR_DATA_TYPE,
+ ATTR_ENDPOINT,
+ ATTR_EVENT,
+ ATTR_EVENT_DATA,
+ ATTR_EVENT_LABEL,
+ ATTR_EVENT_TYPE,
+ ATTR_HOME_ID,
+ ATTR_LABEL,
+ ATTR_NODE_ID,
+ ATTR_PARAMETERS,
+ ATTR_PROPERTY,
+ ATTR_PROPERTY_KEY,
+ ATTR_PROPERTY_KEY_NAME,
+ ATTR_PROPERTY_NAME,
+ ATTR_TYPE,
+ ATTR_VALUE,
+ ATTR_VALUE_RAW,
CONF_INTEGRATION_CREATED_ADDON,
+ CONF_NETWORK_KEY,
+ CONF_USB_PATH,
+ CONF_USE_ADDON,
DATA_CLIENT,
DATA_UNSUBSCRIBE,
DOMAIN,
EVENT_DEVICE_ADDED_TO_REGISTRY,
+ LOGGER,
PLATFORMS,
+ ZWAVE_JS_NOTIFICATION_EVENT,
+ ZWAVE_JS_VALUE_NOTIFICATION_EVENT,
)
from .discovery import async_discover_values
+from .helpers import get_device_id
+from .migrate import async_migrate_discovered_value
+from .services import ZWaveServices
-LOGGER = logging.getLogger(__name__)
CONNECT_TIMEOUT = 10
+DATA_CLIENT_LISTEN_TASK = "client_listen_task"
+DATA_START_PLATFORM_TASK = "start_platform_task"
+DATA_CONNECT_FAILED_LOGGED = "connect_failed_logged"
+DATA_INVALID_SERVER_VERSION_LOGGED = "invalid_server_version_logged"
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
@@ -37,12 +80,6 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
return True
-@callback
-def get_device_id(client: ZwaveClient, node: ZwaveNode) -> Tuple[str, str]:
- """Get device registry identifier for Z-Wave node."""
- return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}")
-
-
@callback
def register_node_in_dev_reg(
hass: HomeAssistant,
@@ -52,45 +89,30 @@ def register_node_in_dev_reg(
node: ZwaveNode,
) -> None:
"""Register node in dev reg."""
- device = dev_reg.async_get_or_create(
- config_entry_id=entry.entry_id,
- identifiers={get_device_id(client, node)},
- sw_version=node.firmware_version,
- name=node.name or node.device_config.description or f"Node {node.node_id}",
- model=node.device_config.label,
- manufacturer=node.device_config.manufacturer,
- )
+ params = {
+ "config_entry_id": entry.entry_id,
+ "identifiers": {get_device_id(client, node)},
+ "sw_version": node.firmware_version,
+ "name": node.name or node.device_config.description or f"Node {node.node_id}",
+ "model": node.device_config.label,
+ "manufacturer": node.device_config.manufacturer,
+ }
+ if node.location:
+ params["suggested_area"] = node.location
+ device = dev_reg.async_get_or_create(**params)
async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Z-Wave JS from a config entry."""
- client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass))
- connected = asyncio.Event()
- initialized = asyncio.Event()
- dev_reg = await device_registry.async_get_registry(hass)
+ use_addon = entry.data.get(CONF_USE_ADDON)
+ if use_addon:
+ await async_ensure_addon_running(hass, entry)
- async def async_on_connect() -> None:
- """Handle websocket is (re)connected."""
- LOGGER.info("Connected to Zwave JS Server")
- connected.set()
-
- async def async_on_disconnect() -> None:
- """Handle websocket is disconnected."""
- LOGGER.info("Disconnected from Zwave JS Server")
- connected.clear()
- if initialized.is_set():
- initialized.clear()
- # update entity availability
- async_dispatcher_send(hass, f"{DOMAIN}_{entry.entry_id}_connection_state")
-
- async def async_on_initialized() -> None:
- """Handle initial full state received."""
- LOGGER.info("Connection to Zwave JS Server initialized.")
- initialized.set()
- # update entity availability
- async_dispatcher_send(hass, f"{DOMAIN}_{entry.entry_id}_connection_state")
+ client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass))
+ dev_reg = device_registry.async_get(hass)
+ ent_reg = entity_registry.async_get(hass)
@callback
def async_on_node_ready(node: ZwaveNode) -> None:
@@ -103,9 +125,23 @@ def async_on_node_ready(node: ZwaveNode) -> None:
# run discovery on all node values and create/update entities
for disc_info in async_discover_values(node):
LOGGER.debug("Discovered entity: %s", disc_info)
+
+ # This migration logic was added in 2021.3 to handle a breaking change to
+ # the value_id format. Some time in the future, this call (as well as the
+ # helper functions) can be removed.
+ async_migrate_discovered_value(ent_reg, client, disc_info)
async_dispatcher_send(
hass, f"{DOMAIN}_{entry.entry_id}_add_{disc_info.platform}", disc_info
)
+ # add listener for stateless node value notification events
+ node.on(
+ "value notification",
+ lambda event: async_on_value_notification(event["value_notification"]),
+ )
+ # add listener for stateless node notification events
+ node.on(
+ "notification", lambda event: async_on_notification(event["notification"])
+ )
@callback
def async_on_node_added(node: ZwaveNode) -> None:
@@ -116,7 +152,7 @@ def async_on_node_added(node: ZwaveNode) -> None:
async_on_node_ready(node)
return
# if node is not yet ready, register one-time callback for ready state
- LOGGER.debug("Node added: %s - waiting for it to become ready.", node.node_id)
+ LOGGER.debug("Node added: %s - waiting for it to become ready", node.node_id)
node.once(
"ready",
lambda event: async_on_node_ready(event["node"]),
@@ -131,36 +167,101 @@ def async_on_node_removed(node: ZwaveNode) -> None:
# grab device in device registry attached to this node
dev_id = get_device_id(client, node)
device = dev_reg.async_get_device({dev_id})
- # note: removal of entity registry is handled by core
- dev_reg.async_remove_device(device.id)
+ # note: removal of entity registry entry is handled by core
+ dev_reg.async_remove_device(device.id) # type: ignore
- async def handle_ha_shutdown(event: Event) -> None:
- """Handle HA shutdown."""
- await client.disconnect()
+ @callback
+ def async_on_value_notification(notification: ValueNotification) -> None:
+ """Relay stateless value notification events from Z-Wave nodes to hass."""
+ device = dev_reg.async_get_device({get_device_id(client, notification.node)})
+ raw_value = value = notification.value
+ if notification.metadata.states:
+ value = notification.metadata.states.get(str(value), value)
+ hass.bus.async_fire(
+ ZWAVE_JS_VALUE_NOTIFICATION_EVENT,
+ {
+ ATTR_DOMAIN: DOMAIN,
+ ATTR_NODE_ID: notification.node.node_id,
+ ATTR_HOME_ID: client.driver.controller.home_id,
+ ATTR_ENDPOINT: notification.endpoint,
+ ATTR_DEVICE_ID: device.id, # type: ignore
+ ATTR_COMMAND_CLASS: notification.command_class,
+ ATTR_COMMAND_CLASS_NAME: notification.command_class_name,
+ ATTR_LABEL: notification.metadata.label,
+ ATTR_PROPERTY: notification.property_,
+ ATTR_PROPERTY_NAME: notification.property_name,
+ ATTR_PROPERTY_KEY: notification.property_key,
+ ATTR_PROPERTY_KEY_NAME: notification.property_key_name,
+ ATTR_VALUE: value,
+ ATTR_VALUE_RAW: raw_value,
+ },
+ )
+
+ @callback
+ def async_on_notification(
+ notification: EntryControlNotification | NotificationNotification,
+ ) -> None:
+ """Relay stateless notification events from Z-Wave nodes to hass."""
+ device = dev_reg.async_get_device({get_device_id(client, notification.node)})
+ event_data = {
+ ATTR_DOMAIN: DOMAIN,
+ ATTR_NODE_ID: notification.node.node_id,
+ ATTR_HOME_ID: client.driver.controller.home_id,
+ ATTR_DEVICE_ID: device.id, # type: ignore
+ ATTR_COMMAND_CLASS: notification.command_class,
+ }
+
+ if isinstance(notification, EntryControlNotification):
+ event_data.update(
+ {
+ ATTR_COMMAND_CLASS_NAME: "Entry Control",
+ ATTR_EVENT_TYPE: notification.event_type,
+ ATTR_DATA_TYPE: notification.data_type,
+ ATTR_EVENT_DATA: notification.event_data,
+ }
+ )
+ else:
+ event_data.update(
+ {
+ ATTR_COMMAND_CLASS_NAME: "Notification",
+ ATTR_LABEL: notification.label,
+ ATTR_TYPE: notification.type_,
+ ATTR_EVENT: notification.event,
+ ATTR_EVENT_LABEL: notification.event_label,
+ ATTR_PARAMETERS: notification.parameters,
+ }
+ )
- # register main event callbacks.
- unsubs = [
- client.register_on_initialized(async_on_initialized),
- client.register_on_disconnect(async_on_disconnect),
- client.register_on_connect(async_on_connect),
- hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown),
- ]
+ hass.bus.async_fire(ZWAVE_JS_NOTIFICATION_EVENT, event_data)
+ entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {})
# connect and throw error if connection failed
- asyncio.create_task(client.connect())
try:
async with timeout(CONNECT_TIMEOUT):
- await connected.wait()
- except asyncio.TimeoutError as err:
- for unsub in unsubs:
- unsub()
- await client.disconnect()
+ await client.connect()
+ except InvalidServerVersion as err:
+ if not entry_hass_data.get(DATA_INVALID_SERVER_VERSION_LOGGED):
+ LOGGER.error("Invalid server version: %s", err)
+ entry_hass_data[DATA_INVALID_SERVER_VERSION_LOGGED] = True
+ if use_addon:
+ async_ensure_addon_updated(hass)
+ raise ConfigEntryNotReady from err
+ except (asyncio.TimeoutError, BaseZwaveJSServerError) as err:
+ if not entry_hass_data.get(DATA_CONNECT_FAILED_LOGGED):
+ LOGGER.error("Failed to connect: %s", err)
+ entry_hass_data[DATA_CONNECT_FAILED_LOGGED] = True
raise ConfigEntryNotReady from err
+ else:
+ LOGGER.info("Connected to Zwave JS Server")
+ entry_hass_data[DATA_CONNECT_FAILED_LOGGED] = False
+ entry_hass_data[DATA_INVALID_SERVER_VERSION_LOGGED] = False
- hass.data[DOMAIN][entry.entry_id] = {
- DATA_CLIENT: client,
- DATA_UNSUBSCRIBE: unsubs,
- }
+ unsubscribe_callbacks: list[Callable] = []
+ entry_hass_data[DATA_CLIENT] = client
+ entry_hass_data[DATA_UNSUBSCRIBE] = unsubscribe_callbacks
+
+ services = ZWaveServices(hass, ent_reg)
+ services.async_register()
# Set up websocket API
async_register_api(hass)
@@ -170,14 +271,46 @@ async def start_platforms() -> None:
# wait until all required platforms are ready
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_setup(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ for platform in PLATFORMS
]
)
- # Wait till we're initialized
- LOGGER.info("Waiting for Z-Wave to be fully initialized")
- await initialized.wait()
+ driver_ready = asyncio.Event()
+
+ async def handle_ha_shutdown(event: Event) -> None:
+ """Handle HA shutdown."""
+ await disconnect_client(hass, entry, client, listen_task, platform_task)
+
+ listen_task = asyncio.create_task(
+ client_listen(hass, entry, client, driver_ready)
+ )
+ entry_hass_data[DATA_CLIENT_LISTEN_TASK] = listen_task
+ unsubscribe_callbacks.append(
+ hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown)
+ )
+
+ try:
+ await driver_ready.wait()
+ except asyncio.CancelledError:
+ LOGGER.debug("Cancelling start platforms")
+ return
+
+ LOGGER.info("Connection to Zwave JS Server initialized")
+
+ # Check for nodes that no longer exist and remove them
+ stored_devices = device_registry.async_entries_for_config_entry(
+ dev_reg, entry.entry_id
+ )
+ known_devices = [
+ dev_reg.async_get_device({get_device_id(client, node)})
+ for node in client.driver.controller.nodes.values()
+ ]
+
+ # Devices that are in the device registry that are not known by the controller can be removed
+ for device in stored_devices:
+ if device not in known_devices:
+ dev_reg.async_remove_device(device.id)
# run discovery on all ready nodes
for node in client.driver.controller.nodes.values():
@@ -193,18 +326,63 @@ async def start_platforms() -> None:
"node removed", lambda event: async_on_node_removed(event["node"])
)
- hass.async_create_task(start_platforms())
+ platform_task = hass.async_create_task(start_platforms())
+ entry_hass_data[DATA_START_PLATFORM_TASK] = platform_task
return True
+async def client_listen(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ client: ZwaveClient,
+ driver_ready: asyncio.Event,
+) -> None:
+ """Listen with the client."""
+ should_reload = True
+ try:
+ await client.listen(driver_ready)
+ except asyncio.CancelledError:
+ should_reload = False
+ except BaseZwaveJSServerError as err:
+ LOGGER.error("Failed to listen: %s", err)
+ except Exception as err: # pylint: disable=broad-except
+ # We need to guard against unknown exceptions to not crash this task.
+ LOGGER.exception("Unexpected exception: %s", err)
+
+ # The entry needs to be reloaded since a new driver state
+ # will be acquired on reconnect.
+ # All model instances will be replaced when the new state is acquired.
+ if should_reload:
+ LOGGER.info("Disconnected from server. Reloading integration")
+ asyncio.create_task(hass.config_entries.async_reload(entry.entry_id))
+
+
+async def disconnect_client(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ client: ZwaveClient,
+ listen_task: asyncio.Task,
+ platform_task: asyncio.Task,
+) -> None:
+ """Disconnect client."""
+ listen_task.cancel()
+ platform_task.cancel()
+
+ await asyncio.gather(listen_task, platform_task)
+
+ if client.connected:
+ await client.disconnect()
+ LOGGER.info("Disconnected from Zwave JS Server")
+
+
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
@@ -216,7 +394,23 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
for unsub in info[DATA_UNSUBSCRIBE]:
unsub()
- await info[DATA_CLIENT].disconnect()
+ if DATA_CLIENT_LISTEN_TASK in info:
+ await disconnect_client(
+ hass,
+ entry,
+ info[DATA_CLIENT],
+ info[DATA_CLIENT_LISTEN_TASK],
+ platform_task=info[DATA_START_PLATFORM_TASK],
+ )
+
+ if entry.data.get(CONF_USE_ADDON) and entry.disabled_by:
+ addon_manager: AddonManager = get_addon_manager(hass)
+ LOGGER.debug("Stopping Z-Wave JS add-on")
+ try:
+ await addon_manager.async_stop_addon()
+ except AddonError as err:
+ LOGGER.error("Failed to stop the Z-Wave JS add-on: %s", err)
+ return False
return True
@@ -226,12 +420,55 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
if not entry.data.get(CONF_INTEGRATION_CREATED_ADDON):
return
+ addon_manager: AddonManager = get_addon_manager(hass)
+ try:
+ await addon_manager.async_stop_addon()
+ except AddonError as err:
+ LOGGER.error(err)
+ return
try:
- await hass.components.hassio.async_stop_addon("core_zwave_js")
- except HassioAPIError as err:
- LOGGER.error("Failed to stop the Z-Wave JS add-on: %s", err)
+ await addon_manager.async_create_snapshot()
+ except AddonError as err:
+ LOGGER.error(err)
return
try:
- await hass.components.hassio.async_uninstall_addon("core_zwave_js")
- except HassioAPIError as err:
- LOGGER.error("Failed to uninstall the Z-Wave JS add-on: %s", err)
+ await addon_manager.async_uninstall_addon()
+ except AddonError as err:
+ LOGGER.error(err)
+
+
+async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> None:
+ """Ensure that Z-Wave JS add-on is installed and running."""
+ addon_manager: AddonManager = get_addon_manager(hass)
+ if addon_manager.task_in_progress():
+ raise ConfigEntryNotReady
+ try:
+ addon_is_installed = await addon_manager.async_is_addon_installed()
+ addon_is_running = await addon_manager.async_is_addon_running()
+ except AddonError as err:
+ LOGGER.error("Failed to get the Z-Wave JS add-on info")
+ raise ConfigEntryNotReady from err
+
+ usb_path: str = entry.data[CONF_USB_PATH]
+ network_key: str = entry.data[CONF_NETWORK_KEY]
+
+ if not addon_is_installed:
+ addon_manager.async_schedule_install_setup_addon(
+ usb_path, network_key, catch_error=True
+ )
+ raise ConfigEntryNotReady
+
+ if not addon_is_running:
+ addon_manager.async_schedule_setup_addon(
+ usb_path, network_key, catch_error=True
+ )
+ raise ConfigEntryNotReady
+
+
+@callback
+def async_ensure_addon_updated(hass: HomeAssistant) -> None:
+ """Ensure that Z-Wave JS add-on is updated and running."""
+ addon_manager: AddonManager = get_addon_manager(hass)
+ if addon_manager.task_in_progress():
+ raise ConfigEntryNotReady
+ addon_manager.async_schedule_update_addon(catch_error=True)
diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py
new file mode 100644
index 00000000000000..0c2fdb17944303
--- /dev/null
+++ b/homeassistant/components/zwave_js/addon.py
@@ -0,0 +1,280 @@
+"""Provide add-on management."""
+from __future__ import annotations
+
+import asyncio
+from functools import partial
+from typing import Any, Callable, TypeVar, cast
+
+from homeassistant.components.hassio import (
+ async_create_snapshot,
+ async_get_addon_discovery_info,
+ async_get_addon_info,
+ async_install_addon,
+ async_set_addon_options,
+ async_start_addon,
+ async_stop_addon,
+ async_uninstall_addon,
+ async_update_addon,
+)
+from homeassistant.components.hassio.handler import HassioAPIError
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers.singleton import singleton
+
+from .const import ADDON_SLUG, CONF_ADDON_DEVICE, CONF_ADDON_NETWORK_KEY, DOMAIN, LOGGER
+
+F = TypeVar("F", bound=Callable[..., Any]) # pylint: disable=invalid-name
+
+DATA_ADDON_MANAGER = f"{DOMAIN}_addon_manager"
+
+
+@singleton(DATA_ADDON_MANAGER)
+@callback
+def get_addon_manager(hass: HomeAssistant) -> AddonManager:
+ """Get the add-on manager."""
+ return AddonManager(hass)
+
+
+def api_error(error_message: str) -> Callable[[F], F]:
+ """Handle HassioAPIError and raise a specific AddonError."""
+
+ def handle_hassio_api_error(func: F) -> F:
+ """Handle a HassioAPIError."""
+
+ async def wrapper(*args, **kwargs): # type: ignore
+ """Wrap an add-on manager method."""
+ try:
+ return_value = await func(*args, **kwargs)
+ except HassioAPIError as err:
+ raise AddonError(error_message) from err
+
+ return return_value
+
+ return cast(F, wrapper)
+
+ return handle_hassio_api_error
+
+
+class AddonManager:
+ """Manage the add-on.
+
+ Methods may raise AddonError.
+ Only one instance of this class may exist
+ to keep track of running add-on tasks.
+ """
+
+ def __init__(self, hass: HomeAssistant) -> None:
+ """Set up the add-on manager."""
+ self._hass = hass
+ self._install_task: asyncio.Task | None = None
+ self._start_task: asyncio.Task | None = None
+ self._update_task: asyncio.Task | None = None
+
+ def task_in_progress(self) -> bool:
+ """Return True if any of the add-on tasks are in progress."""
+ return any(
+ task and not task.done()
+ for task in (
+ self._install_task,
+ self._start_task,
+ self._update_task,
+ )
+ )
+
+ @api_error("Failed to get Z-Wave JS add-on discovery info")
+ async def async_get_addon_discovery_info(self) -> dict:
+ """Return add-on discovery info."""
+ discovery_info = await async_get_addon_discovery_info(self._hass, ADDON_SLUG)
+
+ if not discovery_info:
+ raise AddonError("Failed to get Z-Wave JS add-on discovery info")
+
+ discovery_info_config: dict = discovery_info["config"]
+ return discovery_info_config
+
+ @api_error("Failed to get the Z-Wave JS add-on info")
+ async def async_get_addon_info(self) -> dict:
+ """Return and cache Z-Wave JS add-on info."""
+ addon_info: dict = await async_get_addon_info(self._hass, ADDON_SLUG)
+ return addon_info
+
+ async def async_is_addon_running(self) -> bool:
+ """Return True if Z-Wave JS add-on is running."""
+ addon_info = await self.async_get_addon_info()
+ return bool(addon_info["state"] == "started")
+
+ async def async_is_addon_installed(self) -> bool:
+ """Return True if Z-Wave JS add-on is installed."""
+ addon_info = await self.async_get_addon_info()
+ return addon_info["version"] is not None
+
+ async def async_get_addon_options(self) -> dict:
+ """Get Z-Wave JS add-on options."""
+ addon_info = await self.async_get_addon_info()
+ return cast(dict, addon_info["options"])
+
+ @api_error("Failed to set the Z-Wave JS add-on options")
+ async def async_set_addon_options(self, config: dict) -> None:
+ """Set Z-Wave JS add-on options."""
+ options = {"options": config}
+ await async_set_addon_options(self._hass, ADDON_SLUG, options)
+
+ @api_error("Failed to install the Z-Wave JS add-on")
+ async def async_install_addon(self) -> None:
+ """Install the Z-Wave JS add-on."""
+ await async_install_addon(self._hass, ADDON_SLUG)
+
+ @callback
+ def async_schedule_install_addon(self, catch_error: bool = False) -> asyncio.Task:
+ """Schedule a task that installs the Z-Wave JS add-on.
+
+ Only schedule a new install task if the there's no running task.
+ """
+ if not self._install_task or self._install_task.done():
+ LOGGER.info("Z-Wave JS add-on is not installed. Installing add-on")
+ self._install_task = self._async_schedule_addon_operation(
+ self.async_install_addon, catch_error=catch_error
+ )
+ return self._install_task
+
+ @callback
+ def async_schedule_install_setup_addon(
+ self, usb_path: str, network_key: str, catch_error: bool = False
+ ) -> asyncio.Task:
+ """Schedule a task that installs and sets up the Z-Wave JS add-on.
+
+ Only schedule a new install task if the there's no running task.
+ """
+ if not self._install_task or self._install_task.done():
+ LOGGER.info("Z-Wave JS add-on is not installed. Installing add-on")
+ self._install_task = self._async_schedule_addon_operation(
+ self.async_install_addon,
+ partial(self.async_configure_addon, usb_path, network_key),
+ self.async_start_addon,
+ catch_error=catch_error,
+ )
+ return self._install_task
+
+ @api_error("Failed to uninstall the Z-Wave JS add-on")
+ async def async_uninstall_addon(self) -> None:
+ """Uninstall the Z-Wave JS add-on."""
+ await async_uninstall_addon(self._hass, ADDON_SLUG)
+
+ @api_error("Failed to update the Z-Wave JS add-on")
+ async def async_update_addon(self) -> None:
+ """Update the Z-Wave JS add-on if needed."""
+ addon_info = await self.async_get_addon_info()
+ addon_version = addon_info["version"]
+ update_available = addon_info["update_available"]
+
+ if addon_version is None:
+ raise AddonError("Z-Wave JS add-on is not installed")
+
+ if not update_available:
+ return
+
+ await self.async_create_snapshot()
+ await async_update_addon(self._hass, ADDON_SLUG)
+
+ @callback
+ def async_schedule_update_addon(self, catch_error: bool = False) -> asyncio.Task:
+ """Schedule a task that updates and sets up the Z-Wave JS add-on.
+
+ Only schedule a new update task if the there's no running task.
+ """
+ if not self._update_task or self._update_task.done():
+ LOGGER.info("Trying to update the Z-Wave JS add-on")
+ self._update_task = self._async_schedule_addon_operation(
+ self.async_update_addon,
+ catch_error=catch_error,
+ )
+ return self._update_task
+
+ @api_error("Failed to start the Z-Wave JS add-on")
+ async def async_start_addon(self) -> None:
+ """Start the Z-Wave JS add-on."""
+ await async_start_addon(self._hass, ADDON_SLUG)
+
+ @callback
+ def async_schedule_start_addon(self, catch_error: bool = False) -> asyncio.Task:
+ """Schedule a task that starts the Z-Wave JS add-on.
+
+ Only schedule a new start task if the there's no running task.
+ """
+ if not self._start_task or self._start_task.done():
+ LOGGER.info("Z-Wave JS add-on is not running. Starting add-on")
+ self._start_task = self._async_schedule_addon_operation(
+ self.async_start_addon, catch_error=catch_error
+ )
+ return self._start_task
+
+ @api_error("Failed to stop the Z-Wave JS add-on")
+ async def async_stop_addon(self) -> None:
+ """Stop the Z-Wave JS add-on."""
+ await async_stop_addon(self._hass, ADDON_SLUG)
+
+ async def async_configure_addon(self, usb_path: str, network_key: str) -> None:
+ """Configure and start Z-Wave JS add-on."""
+ addon_options = await self.async_get_addon_options()
+
+ new_addon_options = {
+ CONF_ADDON_DEVICE: usb_path,
+ CONF_ADDON_NETWORK_KEY: network_key,
+ }
+
+ if new_addon_options != addon_options:
+ await self.async_set_addon_options(new_addon_options)
+
+ @callback
+ def async_schedule_setup_addon(
+ self, usb_path: str, network_key: str, catch_error: bool = False
+ ) -> asyncio.Task:
+ """Schedule a task that configures and starts the Z-Wave JS add-on.
+
+ Only schedule a new setup task if the there's no running task.
+ """
+ if not self._start_task or self._start_task.done():
+ LOGGER.info("Z-Wave JS add-on is not running. Starting add-on")
+ self._start_task = self._async_schedule_addon_operation(
+ partial(self.async_configure_addon, usb_path, network_key),
+ self.async_start_addon,
+ catch_error=catch_error,
+ )
+ return self._start_task
+
+ @api_error("Failed to create a snapshot of the Z-Wave JS add-on.")
+ async def async_create_snapshot(self) -> None:
+ """Create a partial snapshot of the Z-Wave JS add-on."""
+ addon_info = await self.async_get_addon_info()
+ addon_version = addon_info["version"]
+ name = f"addon_{ADDON_SLUG}_{addon_version}"
+
+ LOGGER.debug("Creating snapshot: %s", name)
+ await async_create_snapshot(
+ self._hass,
+ {"name": name, "addons": [ADDON_SLUG]},
+ partial=True,
+ )
+
+ @callback
+ def _async_schedule_addon_operation(
+ self, *funcs: Callable, catch_error: bool = False
+ ) -> asyncio.Task:
+ """Schedule an add-on task."""
+
+ async def addon_operation() -> None:
+ """Do the add-on operation and catch AddonError."""
+ for func in funcs:
+ try:
+ await func()
+ except AddonError as err:
+ if not catch_error:
+ raise
+ LOGGER.error(err)
+ break
+
+ return self._hass.async_create_task(addon_operation())
+
+
+class AddonError(HomeAssistantError):
+ """Represent an error with Z-Wave JS add-on."""
diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py
index 1a8a197571b1b8..2792fc6819b584 100644
--- a/homeassistant/components/zwave_js/api.py
+++ b/homeassistant/components/zwave_js/api.py
@@ -1,28 +1,54 @@
"""Websocket API for Z-Wave JS."""
+from __future__ import annotations
+
+import dataclasses
import json
-import logging
from aiohttp import hdrs, web, web_exceptions
import voluptuous as vol
from zwave_js_server import dump
+from zwave_js_server.const import LogLevel
+from zwave_js_server.exceptions import InvalidNewValue, NotFoundError, SetValueFailed
+from zwave_js_server.model.log_config import LogConfig
+from zwave_js_server.util.node import async_set_config_parameter
from homeassistant.components import websocket_api
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.components.websocket_api.connection import ActiveConnection
+from homeassistant.components.websocket_api.const import (
+ ERR_NOT_FOUND,
+ ERR_NOT_SUPPORTED,
+ ERR_UNKNOWN_ERROR,
+)
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import DATA_CLIENT, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY
-_LOGGER = logging.getLogger(__name__)
-
+# general API constants
ID = "id"
ENTRY_ID = "entry_id"
NODE_ID = "node_id"
TYPE = "type"
+PROPERTY = "property"
+PROPERTY_KEY = "property_key"
+VALUE = "value"
+
+# constants for log config commands
+CONFIG = "config"
+LEVEL = "level"
+LOG_TO_FILE = "log_to_file"
+FILENAME = "filename"
+ENABLED = "enabled"
+FORCE_CONSOLE = "force_console"
+
+# constants for setting config parameters
+VALUE_ID = "value_id"
+STATUS = "status"
@callback
@@ -34,6 +60,11 @@ def async_register_api(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, websocket_stop_inclusion)
websocket_api.async_register_command(hass, websocket_remove_node)
websocket_api.async_register_command(hass, websocket_stop_exclusion)
+ websocket_api.async_register_command(hass, websocket_refresh_node_info)
+ websocket_api.async_register_command(hass, websocket_update_log_config)
+ websocket_api.async_register_command(hass, websocket_get_log_config)
+ websocket_api.async_register_command(hass, websocket_get_config_parameters)
+ websocket_api.async_register_command(hass, websocket_set_config_parameter)
hass.http.register_view(DumpView) # type: ignore
@@ -51,7 +82,7 @@ def websocket_network_status(
data = {
"client": {
"ws_server_url": client.ws_server_url,
- "state": client.state,
+ "state": "connected" if client.connected else "disconnected",
"driver_version": client.version.driver_version,
"server_version": client.version.server_version,
},
@@ -81,7 +112,12 @@ def websocket_node_status(
entry_id = msg[ENTRY_ID]
client = hass.data[DOMAIN][entry_id][DATA_CLIENT]
node_id = msg[NODE_ID]
- node = client.driver.controller.nodes[node_id]
+ node = client.driver.controller.nodes.get(node_id)
+
+ if node is None:
+ connection.send_error(msg[ID], ERR_NOT_FOUND, f"Node {node_id} not found")
+ return
+
data = {
"node_id": node.node_id,
"is_routing": node.is_routing,
@@ -266,6 +302,201 @@ def node_removed(event: dict) -> None:
)
+@websocket_api.require_admin # type: ignore
+@websocket_api.async_response
+@websocket_api.websocket_command(
+ {
+ vol.Required(TYPE): "zwave_js/refresh_node_info",
+ vol.Required(ENTRY_ID): str,
+ vol.Required(NODE_ID): int,
+ },
+)
+async def websocket_refresh_node_info(
+ hass: HomeAssistant, connection: ActiveConnection, msg: dict
+) -> None:
+ """Re-interview a node."""
+ entry_id = msg[ENTRY_ID]
+ node_id = msg[NODE_ID]
+ client = hass.data[DOMAIN][entry_id][DATA_CLIENT]
+ node = client.driver.controller.nodes.get(node_id)
+
+ if node is None:
+ connection.send_error(msg[ID], ERR_NOT_FOUND, f"Node {node_id} not found")
+ return
+
+ await node.async_refresh_info()
+ connection.send_result(msg[ID])
+
+
+@websocket_api.require_admin # type:ignore
+@websocket_api.async_response
+@websocket_api.websocket_command(
+ {
+ vol.Required(TYPE): "zwave_js/set_config_parameter",
+ vol.Required(ENTRY_ID): str,
+ vol.Required(NODE_ID): int,
+ vol.Required(PROPERTY): int,
+ vol.Optional(PROPERTY_KEY): int,
+ vol.Required(VALUE): int,
+ }
+)
+async def websocket_set_config_parameter(
+ hass: HomeAssistant, connection: ActiveConnection, msg: dict
+) -> None:
+ """Set a config parameter value for a Z-Wave node."""
+ entry_id = msg[ENTRY_ID]
+ node_id = msg[NODE_ID]
+ property_ = msg[PROPERTY]
+ property_key = msg.get(PROPERTY_KEY)
+ value = msg[VALUE]
+ client = hass.data[DOMAIN][entry_id][DATA_CLIENT]
+ node = client.driver.controller.nodes[node_id]
+ try:
+ zwave_value, cmd_status = await async_set_config_parameter(
+ node, value, property_, property_key=property_key
+ )
+ except (InvalidNewValue, NotFoundError, NotImplementedError, SetValueFailed) as err:
+ code = ERR_UNKNOWN_ERROR
+ if isinstance(err, NotFoundError):
+ code = ERR_NOT_FOUND
+ elif isinstance(err, (InvalidNewValue, NotImplementedError)):
+ code = ERR_NOT_SUPPORTED
+
+ connection.send_error(
+ msg[ID],
+ code,
+ str(err),
+ )
+ return
+
+ connection.send_result(
+ msg[ID],
+ {
+ VALUE_ID: zwave_value.value_id,
+ STATUS: cmd_status,
+ },
+ )
+
+
+@websocket_api.require_admin
+@websocket_api.websocket_command(
+ {
+ vol.Required(TYPE): "zwave_js/get_config_parameters",
+ vol.Required(ENTRY_ID): str,
+ vol.Required(NODE_ID): int,
+ }
+)
+@callback
+def websocket_get_config_parameters(
+ hass: HomeAssistant, connection: ActiveConnection, msg: dict
+) -> None:
+ """Get a list of configuration parameters for a Z-Wave node."""
+ entry_id = msg[ENTRY_ID]
+ node_id = msg[NODE_ID]
+ client = hass.data[DOMAIN][entry_id][DATA_CLIENT]
+ node = client.driver.controller.nodes.get(node_id)
+
+ if node is None:
+ connection.send_error(msg[ID], ERR_NOT_FOUND, f"Node {node_id} not found")
+ return
+
+ values = node.get_configuration_values()
+ result = {}
+ for value_id, zwave_value in values.items():
+ metadata = zwave_value.metadata
+ result[value_id] = {
+ "property": zwave_value.property_,
+ "property_key": zwave_value.property_key,
+ "configuration_value_type": zwave_value.configuration_value_type.value,
+ "metadata": {
+ "description": metadata.description,
+ "label": metadata.label,
+ "type": metadata.type,
+ "min": metadata.min,
+ "max": metadata.max,
+ "unit": metadata.unit,
+ "writeable": metadata.writeable,
+ "readable": metadata.readable,
+ },
+ "value": zwave_value.value,
+ }
+ if zwave_value.metadata.states:
+ result[value_id]["metadata"]["states"] = zwave_value.metadata.states
+
+ connection.send_result(
+ msg[ID],
+ result,
+ )
+
+
+def filename_is_present_if_logging_to_file(obj: dict) -> dict:
+ """Validate that filename is provided if log_to_file is True."""
+ if obj.get(LOG_TO_FILE, False) and FILENAME not in obj:
+ raise vol.Invalid("`filename` must be provided if logging to file")
+ return obj
+
+
+@websocket_api.require_admin # type: ignore
+@websocket_api.async_response
+@websocket_api.websocket_command(
+ {
+ vol.Required(TYPE): "zwave_js/update_log_config",
+ vol.Required(ENTRY_ID): str,
+ vol.Required(CONFIG): vol.All(
+ vol.Schema(
+ {
+ vol.Optional(ENABLED): cv.boolean,
+ vol.Optional(LEVEL): vol.All(
+ cv.string,
+ vol.Lower,
+ vol.In([log_level.value for log_level in LogLevel]),
+ lambda val: LogLevel(val), # pylint: disable=unnecessary-lambda
+ ),
+ vol.Optional(LOG_TO_FILE): cv.boolean,
+ vol.Optional(FILENAME): cv.string,
+ vol.Optional(FORCE_CONSOLE): cv.boolean,
+ }
+ ),
+ cv.has_at_least_one_key(
+ ENABLED, FILENAME, FORCE_CONSOLE, LEVEL, LOG_TO_FILE
+ ),
+ filename_is_present_if_logging_to_file,
+ ),
+ },
+)
+async def websocket_update_log_config(
+ hass: HomeAssistant, connection: ActiveConnection, msg: dict
+) -> None:
+ """Update the driver log config."""
+ entry_id = msg[ENTRY_ID]
+ client = hass.data[DOMAIN][entry_id][DATA_CLIENT]
+ await client.driver.async_update_log_config(LogConfig(**msg[CONFIG]))
+ connection.send_result(
+ msg[ID],
+ )
+
+
+@websocket_api.require_admin # type: ignore
+@websocket_api.async_response
+@websocket_api.websocket_command(
+ {
+ vol.Required(TYPE): "zwave_js/get_log_config",
+ vol.Required(ENTRY_ID): str,
+ },
+)
+async def websocket_get_log_config(
+ hass: HomeAssistant, connection: ActiveConnection, msg: dict
+) -> None:
+ """Get log configuration for the Z-Wave JS driver."""
+ entry_id = msg[ENTRY_ID]
+ client = hass.data[DOMAIN][entry_id][DATA_CLIENT]
+ result = await client.driver.async_get_log_config()
+ connection.send_result(
+ msg[ID],
+ dataclasses.asdict(result),
+ )
+
+
class DumpView(HomeAssistantView):
"""View to dump the state of the Z-Wave JS server."""
@@ -284,9 +515,9 @@ async def get(self, request: web.Request, config_entry_id: str) -> web.Response:
msgs = await dump.dump_msgs(entry.data[CONF_URL], async_get_clientsession(hass))
return web.Response(
- body="\n".join(json.dumps(msg) for msg in msgs) + "\n",
+ body=json.dumps(msgs, indent=2) + "\n",
headers={
- hdrs.CONTENT_TYPE: "application/jsonl",
- hdrs.CONTENT_DISPOSITION: 'attachment; filename="zwave_js_dump.jsonl"',
+ hdrs.CONTENT_TYPE: "application/json",
+ hdrs.CONTENT_DISPOSITION: 'attachment; filename="zwave_js_dump.json"',
},
)
diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py
index 6dc5cc58df544e..b97975b0507070 100644
--- a/homeassistant/components/zwave_js/binary_sensor.py
+++ b/homeassistant/components/zwave_js/binary_sensor.py
@@ -1,7 +1,8 @@
"""Representation of Z-Wave binary sensors."""
+from __future__ import annotations
import logging
-from typing import Callable, List, Optional, TypedDict
+from typing import Callable, TypedDict
from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.const import CommandClass
@@ -14,7 +15,6 @@
DEVICE_CLASS_LOCK,
DEVICE_CLASS_MOISTURE,
DEVICE_CLASS_MOTION,
- DEVICE_CLASS_POWER,
DEVICE_CLASS_PROBLEM,
DEVICE_CLASS_SAFETY,
DEVICE_CLASS_SMOKE,
@@ -57,201 +57,144 @@ class NotificationSensorMapping(TypedDict, total=False):
"""Represent a notification sensor mapping dict type."""
type: int # required
- states: List[int] # required
+ states: list[str]
device_class: str
enabled: bool
# Mappings for Notification sensors
-NOTIFICATION_SENSOR_MAPPINGS: List[NotificationSensorMapping] = [
+# https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/notifications.json
+NOTIFICATION_SENSOR_MAPPINGS: list[NotificationSensorMapping] = [
{
- # NotificationType 1: Smoke Alarm - State Id's 1 and 2
- # Assuming here that Value 1 and 2 are not present at the same time
+ # NotificationType 1: Smoke Alarm - State Id's 1 and 2 - Smoke detected
"type": NOTIFICATION_SMOKE_ALARM,
- "states": [1, 2],
+ "states": ["1", "2"],
"device_class": DEVICE_CLASS_SMOKE,
},
{
# NotificationType 1: Smoke Alarm - All other State Id's
- # Create as disabled sensors
"type": NOTIFICATION_SMOKE_ALARM,
- "states": [3, 4, 5, 6, 7, 8],
- "device_class": DEVICE_CLASS_SMOKE,
- "enabled": False,
+ "device_class": DEVICE_CLASS_PROBLEM,
},
{
# NotificationType 2: Carbon Monoxide - State Id's 1 and 2
"type": NOTIFICATION_CARBON_MONOOXIDE,
- "states": [1, 2],
+ "states": ["1", "2"],
"device_class": DEVICE_CLASS_GAS,
},
{
# NotificationType 2: Carbon Monoxide - All other State Id's
"type": NOTIFICATION_CARBON_MONOOXIDE,
- "states": [4, 5, 7],
- "device_class": DEVICE_CLASS_GAS,
- "enabled": False,
+ "device_class": DEVICE_CLASS_PROBLEM,
},
{
# NotificationType 3: Carbon Dioxide - State Id's 1 and 2
"type": NOTIFICATION_CARBON_DIOXIDE,
- "states": [1, 2],
+ "states": ["1", "2"],
"device_class": DEVICE_CLASS_GAS,
},
{
# NotificationType 3: Carbon Dioxide - All other State Id's
"type": NOTIFICATION_CARBON_DIOXIDE,
- "states": [4, 5, 7],
- "device_class": DEVICE_CLASS_GAS,
- "enabled": False,
+ "device_class": DEVICE_CLASS_PROBLEM,
},
{
# NotificationType 4: Heat - State Id's 1, 2, 5, 6 (heat/underheat)
"type": NOTIFICATION_HEAT,
- "states": [1, 2, 5, 6],
+ "states": ["1", "2", "5", "6"],
"device_class": DEVICE_CLASS_HEAT,
},
{
# NotificationType 4: Heat - All other State Id's
"type": NOTIFICATION_HEAT,
- "states": [3, 4, 8, 10, 11],
- "device_class": DEVICE_CLASS_HEAT,
- "enabled": False,
+ "device_class": DEVICE_CLASS_PROBLEM,
},
{
# NotificationType 5: Water - State Id's 1, 2, 3, 4
"type": NOTIFICATION_WATER,
- "states": [1, 2, 3, 4],
+ "states": ["1", "2", "3", "4"],
"device_class": DEVICE_CLASS_MOISTURE,
},
{
# NotificationType 5: Water - All other State Id's
"type": NOTIFICATION_WATER,
- "states": [5],
- "device_class": DEVICE_CLASS_MOISTURE,
- "enabled": False,
+ "device_class": DEVICE_CLASS_PROBLEM,
},
{
# NotificationType 6: Access Control - State Id's 1, 2, 3, 4 (Lock)
"type": NOTIFICATION_ACCESS_CONTROL,
- "states": [1, 2, 3, 4],
+ "states": ["1", "2", "3", "4"],
"device_class": DEVICE_CLASS_LOCK,
},
{
- # NotificationType 6: Access Control - State Id 22 (door/window open)
+ # NotificationType 6: Access Control - State Id 16 (door/window open)
"type": NOTIFICATION_ACCESS_CONTROL,
- "states": [22],
+ "states": ["22"],
"device_class": DEVICE_CLASS_DOOR,
},
+ {
+ # NotificationType 6: Access Control - State Id 17 (door/window closed)
+ "type": NOTIFICATION_ACCESS_CONTROL,
+ "states": ["23"],
+ "enabled": False,
+ },
{
# NotificationType 7: Home Security - State Id's 1, 2 (intrusion)
- # Assuming that value 1 and 2 are not present at the same time
"type": NOTIFICATION_HOME_SECURITY,
- "states": [1, 2],
+ "states": ["1", "2"],
"device_class": DEVICE_CLASS_SAFETY,
},
{
# NotificationType 7: Home Security - State Id's 3, 4, 9 (tampering)
"type": NOTIFICATION_HOME_SECURITY,
- "states": [3, 4, 9],
+ "states": ["3", "4", "9"],
"device_class": DEVICE_CLASS_SAFETY,
},
{
# NotificationType 7: Home Security - State Id's 5, 6 (glass breakage)
- # Assuming that value 5 and 6 are not present at the same time
"type": NOTIFICATION_HOME_SECURITY,
- "states": [5, 6],
+ "states": ["5", "6"],
"device_class": DEVICE_CLASS_SAFETY,
},
{
# NotificationType 7: Home Security - State Id's 7, 8 (motion)
"type": NOTIFICATION_HOME_SECURITY,
- "states": [7, 8],
+ "states": ["7", "8"],
"device_class": DEVICE_CLASS_MOTION,
},
- {
- # NotificationType 8: Power management - Values 1...9
- "type": NOTIFICATION_POWER_MANAGEMENT,
- "states": [1, 2, 3, 4, 5, 6, 7, 8, 9],
- "device_class": DEVICE_CLASS_POWER,
- "enabled": False,
- },
- {
- # NotificationType 8: Power management - Values 10...15
- # Battery values (mutually exclusive)
- "type": NOTIFICATION_POWER_MANAGEMENT,
- "states": [10, 11, 12, 13, 14, 15],
- "device_class": DEVICE_CLASS_BATTERY,
- "enabled": False,
- },
{
# NotificationType 9: System - State Id's 1, 2, 6, 7
"type": NOTIFICATION_SYSTEM,
- "states": [1, 2, 6, 7],
+ "states": ["1", "2", "6", "7"],
"device_class": DEVICE_CLASS_PROBLEM,
- "enabled": False,
},
{
# NotificationType 10: Emergency - State Id's 1, 2, 3
"type": NOTIFICATION_EMERGENCY,
- "states": [1, 2, 3],
+ "states": ["1", "2", "3"],
"device_class": DEVICE_CLASS_PROBLEM,
},
- {
- # NotificationType 11: Clock - State Id's 1, 2
- "type": NOTIFICATION_CLOCK,
- "states": [1, 2],
- "enabled": False,
- },
- {
- # NotificationType 12: Appliance - All State Id's
- "type": NOTIFICATION_APPLIANCE,
- "states": list(range(1, 22)),
- },
- {
- # NotificationType 13: Home Health - State Id's 1,2,3,4,5
- "type": NOTIFICATION_APPLIANCE,
- "states": [1, 2, 3, 4, 5],
- },
{
# NotificationType 14: Siren
"type": NOTIFICATION_SIREN,
- "states": [1],
+ "states": ["1"],
"device_class": DEVICE_CLASS_SOUND,
},
- {
- # NotificationType 15: Water valve
- # ignore non-boolean values
- "type": NOTIFICATION_WATER_VALVE,
- "states": [3, 4],
- "device_class": DEVICE_CLASS_PROBLEM,
- },
- {
- # NotificationType 16: Weather
- "type": NOTIFICATION_WEATHER,
- "states": [1, 2],
- "device_class": DEVICE_CLASS_PROBLEM,
- },
- {
- # NotificationType 17: Irrigation
- # ignore non-boolean values
- "type": NOTIFICATION_IRRIGATION,
- "states": [1, 2, 3, 4, 5],
- },
{
# NotificationType 18: Gas
"type": NOTIFICATION_GAS,
- "states": [1, 2, 3, 4],
+ "states": ["1", "2", "3", "4"],
"device_class": DEVICE_CLASS_GAS,
},
{
# NotificationType 18: Gas
"type": NOTIFICATION_GAS,
- "states": [6],
+ "states": ["6"],
"device_class": DEVICE_CLASS_PROBLEM,
},
]
+
PROPERTY_DOOR_STATUS = "doorStatus"
@@ -259,13 +202,13 @@ class PropertySensorMapping(TypedDict, total=False):
"""Represent a property sensor mapping dict type."""
property_name: str # required
- on_states: List[str] # required
+ on_states: list[str] # required
device_class: str
enabled: bool
# Mappings for property sensors
-PROPERTY_SENSOR_MAPPINGS: List[PropertySensorMapping] = [
+PROPERTY_SENSOR_MAPPINGS: list[PropertySensorMapping] = [
{
"property_name": PROPERTY_DOOR_STATUS,
"on_states": ["open"],
@@ -284,10 +227,17 @@ async def async_setup_entry(
@callback
def async_add_binary_sensor(info: ZwaveDiscoveryInfo) -> None:
"""Add Z-Wave Binary Sensor."""
- entities: List[ZWaveBaseEntity] = []
+ entities: list[BinarySensorEntity] = []
if info.platform_hint == "notification":
- entities.append(ZWaveNotificationBinarySensor(config_entry, client, info))
+ # Get all sensors from Notification CC states
+ for state_key in info.primary_value.metadata.states:
+ # ignore idle key (0)
+ if state_key == "0":
+ continue
+ entities.append(
+ ZWaveNotificationBinarySensor(config_entry, client, info, state_key)
+ )
elif info.platform_hint == "property":
entities.append(ZWavePropertyBinarySensor(config_entry, client, info))
else:
@@ -308,13 +258,25 @@ def async_add_binary_sensor(info: ZwaveDiscoveryInfo) -> None:
class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
"""Representation of a Z-Wave binary_sensor."""
+ def __init__(
+ self,
+ config_entry: ConfigEntry,
+ client: ZwaveClient,
+ info: ZwaveDiscoveryInfo,
+ ) -> None:
+ """Initialize a ZWaveBooleanBinarySensor entity."""
+ super().__init__(config_entry, client, info)
+ self._name = self.generate_name(include_value_name=True)
+
@property
- def is_on(self) -> bool:
+ def is_on(self) -> bool | None:
"""Return if the sensor is on or off."""
+ if self.info.primary_value.value is None:
+ return None
return bool(self.info.primary_value.value)
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return device class."""
if self.info.primary_value.command_class == CommandClass.BATTERY:
return DEVICE_CLASS_BATTERY
@@ -323,62 +285,71 @@ def device_class(self) -> Optional[str]:
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
- if self.info.primary_value.command_class == CommandClass.SENSOR_BINARY:
- # Legacy binary sensors are phased out (replaced by notification sensors)
- # Disable by default to not confuse users
- if self.info.node.device_class.generic != "Binary Sensor":
- return False
- return True
+ # Legacy binary sensors are phased out (replaced by notification sensors)
+ # Disable by default to not confuse users
+ return bool(
+ self.info.primary_value.command_class != CommandClass.SENSOR_BINARY
+ or self.info.node.device_class.generic.key == 0x20
+ )
class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
"""Representation of a Z-Wave binary_sensor from Notification CommandClass."""
def __init__(
- self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo
+ self,
+ config_entry: ConfigEntry,
+ client: ZwaveClient,
+ info: ZwaveDiscoveryInfo,
+ state_key: str,
) -> None:
"""Initialize a ZWaveNotificationBinarySensor entity."""
super().__init__(config_entry, client, info)
+ self.state_key = state_key
+ self._name = self.generate_name(
+ include_value_name=True,
+ alternate_value_name=self.info.primary_value.property_name,
+ additional_info=[self.info.primary_value.metadata.states[self.state_key]],
+ )
# check if we have a custom mapping for this value
self._mapping_info = self._get_sensor_mapping()
@property
- def is_on(self) -> bool:
+ def is_on(self) -> bool | None:
"""Return if the sensor is on or off."""
- if self._mapping_info:
- return self.info.primary_value.value in self._mapping_info["states"]
- return bool(self.info.primary_value.value != 0)
+ if self.info.primary_value.value is None:
+ return None
+ return int(self.info.primary_value.value) == int(self.state_key)
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return device class."""
return self._mapping_info.get("device_class")
+ @property
+ def unique_id(self) -> str:
+ """Return unique id for this entity."""
+ return f"{super().unique_id}.{self.state_key}"
+
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
- # We hide some more advanced sensors by default to not overwhelm users
if not self._mapping_info:
- # consider value for which we do not have a mapping as advanced.
- return False
+ return True
return self._mapping_info.get("enabled", True)
@callback
def _get_sensor_mapping(self) -> NotificationSensorMapping:
"""Try to get a device specific mapping for this sensor."""
for mapping in NOTIFICATION_SENSOR_MAPPINGS:
- if mapping["type"] != int(
- self.info.primary_value.metadata.cc_specific["notificationType"]
+ if (
+ mapping["type"]
+ != self.info.primary_value.metadata.cc_specific["notificationType"]
):
continue
- for state_key in self.info.primary_value.metadata.states:
- # make sure the key is int
- state_key = int(state_key)
- if state_key not in mapping["states"]:
- continue
+ if not mapping.get("states") or self.state_key in mapping["states"]:
# match found
- mapping_info = mapping.copy()
- return mapping_info
+ return mapping
return {}
@@ -392,14 +363,17 @@ def __init__(
super().__init__(config_entry, client, info)
# check if we have a custom mapping for this value
self._mapping_info = self._get_sensor_mapping()
+ self._name = self.generate_name(include_value_name=True)
@property
- def is_on(self) -> bool:
+ def is_on(self) -> bool | None:
"""Return if the sensor is on or off."""
+ if self.info.primary_value.value is None:
+ return None
return self.info.primary_value.value in self._mapping_info["on_states"]
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return device class."""
return self._mapping_info.get("device_class")
diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py
index 07d4a3f7d0f419..c64a5ef788fc9b 100644
--- a/homeassistant/components/zwave_js/climate.py
+++ b/homeassistant/components/zwave_js/climate.py
@@ -1,10 +1,12 @@
"""Representation of Z-Wave thermostats."""
-import logging
-from typing import Any, Callable, Dict, List, Optional
+from __future__ import annotations
+
+from typing import Any, Callable, cast
from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.const import (
THERMOSTAT_CURRENT_TEMP_PROPERTY,
+ THERMOSTAT_MODE_PROPERTY,
THERMOSTAT_MODE_SETPOINT_MAP,
THERMOSTAT_MODES,
THERMOSTAT_OPERATING_STATE_PROPERTY,
@@ -33,12 +35,18 @@
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
PRESET_NONE,
+ SUPPORT_FAN_MODE,
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.const import (
+ ATTR_TEMPERATURE,
+ PRECISION_TENTHS,
+ TEMP_CELSIUS,
+ TEMP_FAHRENHEIT,
+)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -46,12 +54,10 @@
from .discovery import ZwaveDiscoveryInfo
from .entity import ZWaveBaseEntity
-_LOGGER = logging.getLogger(__name__)
-
# Map Z-Wave HVAC Mode to Home Assistant value
# Note: We treat "auto" as "heat_cool" as most Z-Wave devices
# report auto_changeover as auto without schedule support.
-ZW_HVAC_MODE_MAP: Dict[int, str] = {
+ZW_HVAC_MODE_MAP: dict[int, str] = {
ThermostatMode.OFF: HVAC_MODE_OFF,
ThermostatMode.HEAT: HVAC_MODE_HEAT,
ThermostatMode.COOL: HVAC_MODE_COOL,
@@ -68,7 +74,7 @@
ThermostatMode.FULL_POWER: HVAC_MODE_HEAT,
}
-HVAC_CURRENT_MAP: Dict[int, str] = {
+HVAC_CURRENT_MAP: dict[int, str] = {
ThermostatOperatingState.IDLE: CURRENT_HVAC_IDLE,
ThermostatOperatingState.PENDING_HEAT: CURRENT_HVAC_IDLE,
ThermostatOperatingState.HEATING: CURRENT_HVAC_HEAT,
@@ -83,6 +89,8 @@
ThermostatOperatingState.THIRD_STAGE_AUX_HEAT: CURRENT_HVAC_HEAT,
}
+ATTR_FAN_STATE = "fan_state"
+
async def async_setup_entry(
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable
@@ -93,7 +101,7 @@ async def async_setup_entry(
@callback
def async_add_climate(info: ZwaveDiscoveryInfo) -> None:
"""Add Z-Wave Climate."""
- entities: List[ZWaveBaseEntity] = []
+ entities: list[ZWaveBaseEntity] = []
entities.append(ZWaveClimate(config_entry, client, info))
async_add_entities(entities)
@@ -115,21 +123,28 @@ def __init__(
) -> None:
"""Initialize lock."""
super().__init__(config_entry, client, info)
- self._hvac_modes: Dict[str, Optional[int]] = {}
- self._hvac_presets: Dict[str, Optional[int]] = {}
- self._unit_value: ZwaveValue = None
+ self._hvac_modes: dict[str, int | None] = {}
+ self._hvac_presets: dict[str, int | None] = {}
+ self._unit_value: ZwaveValue | None = None
- self._current_mode = self.info.primary_value
- self._setpoint_values: Dict[ThermostatSetpointType, ZwaveValue] = {}
+ self._current_mode = self.get_zwave_value(
+ THERMOSTAT_MODE_PROPERTY, command_class=CommandClass.THERMOSTAT_MODE
+ )
+ self._setpoint_values: dict[ThermostatSetpointType, ZwaveValue] = {}
for enum in ThermostatSetpointType:
self._setpoint_values[enum] = self.get_zwave_value(
THERMOSTAT_SETPOINT_PROPERTY,
command_class=CommandClass.THERMOSTAT_SETPOINT,
- value_property_key_name=enum.value,
+ value_property_key=enum.value,
add_to_watched_value_ids=True,
)
- # Use the first found setpoint value to always determine the temperature unit
- if self._setpoint_values[enum] and not self._unit_value:
+ # Use the first found non N/A setpoint value to always determine the
+ # temperature unit
+ if (
+ not self._unit_value
+ and enum != ThermostatSetpointType.NA
+ and self._setpoint_values[enum]
+ ):
self._unit_value = self._setpoint_values[enum]
self._operating_state = self.get_zwave_value(
THERMOSTAT_OPERATING_STATE_PROPERTY,
@@ -140,8 +155,40 @@ def __init__(
THERMOSTAT_CURRENT_TEMP_PROPERTY,
command_class=CommandClass.SENSOR_MULTILEVEL,
add_to_watched_value_ids=True,
+ check_all_endpoints=True,
+ )
+ if not self._unit_value:
+ self._unit_value = self._current_temp
+ self._current_humidity = self.get_zwave_value(
+ "Humidity",
+ command_class=CommandClass.SENSOR_MULTILEVEL,
+ add_to_watched_value_ids=True,
+ check_all_endpoints=True,
+ )
+ self._fan_mode = self.get_zwave_value(
+ THERMOSTAT_MODE_PROPERTY,
+ CommandClass.THERMOSTAT_FAN_MODE,
+ add_to_watched_value_ids=True,
+ check_all_endpoints=True,
+ )
+ self._fan_state = self.get_zwave_value(
+ THERMOSTAT_OPERATING_STATE_PROPERTY,
+ CommandClass.THERMOSTAT_FAN_STATE,
+ add_to_watched_value_ids=True,
+ check_all_endpoints=True,
)
self._set_modes_and_presets()
+ self._supported_features = 0
+ if len(self._hvac_presets) > 1:
+ self._supported_features |= SUPPORT_PRESET_MODE
+ # If any setpoint value exists, we can assume temperature
+ # can be set
+ if any(self._setpoint_values.values()):
+ self._supported_features |= SUPPORT_TARGET_TEMPERATURE
+ if HVAC_MODE_HEAT_COOL in self.hvac_modes:
+ self._supported_features |= SUPPORT_TARGET_TEMPERATURE_RANGE
+ if self._fan_mode:
+ self._supported_features |= SUPPORT_FAN_MODE
def _setpoint_value(self, setpoint_type: ThermostatSetpointType) -> ZwaveValue:
"""Optionally return a ZwaveValue for a setpoint."""
@@ -153,15 +200,17 @@ def _setpoint_value(self, setpoint_type: ThermostatSetpointType) -> ZwaveValue:
def _set_modes_and_presets(self) -> None:
"""Convert Z-Wave Thermostat modes into Home Assistant modes and presets."""
- all_modes: Dict[str, Optional[int]] = {}
- all_presets: Dict[str, Optional[int]] = {PRESET_NONE: None}
+ all_modes: dict[str, int | None] = {}
+ all_presets: dict[str, int | None] = {PRESET_NONE: None}
# Z-Wave uses one list for both modes and presets.
# Iterate over all Z-Wave ThermostatModes and extract the hvac modes and presets.
- current_mode = self._current_mode
- if not current_mode:
+ if self._current_mode is None:
+ self._hvac_modes = {
+ ZW_HVAC_MODE_MAP[ThermostatMode.HEAT]: ThermostatMode.HEAT
+ }
return
- for mode_id, mode_name in current_mode.metadata.states.items():
+ for mode_id, mode_name in self._current_mode.metadata.states.items():
mode_id = int(mode_id)
if mode_id in THERMOSTAT_MODES:
# treat value as hvac mode
@@ -175,88 +224,177 @@ def _set_modes_and_presets(self) -> None:
self._hvac_presets = all_presets
@property
- def _current_mode_setpoint_enums(self) -> List[Optional[ThermostatSetpointType]]:
+ def _current_mode_setpoint_enums(self) -> list[ThermostatSetpointType | None]:
"""Return the list of enums that are relevant to the current thermostat mode."""
+ if self._current_mode is None:
+ # Thermostat(valve) with no support for setting a mode is considered heating-only
+ return [ThermostatSetpointType.HEATING]
return THERMOSTAT_MODE_SETPOINT_MAP.get(int(self._current_mode.value), []) # type: ignore
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement used by the platform."""
- if "f" in self._unit_value.metadata.unit.lower():
+ if (
+ self._unit_value
+ and self._unit_value.metadata.unit
+ and "f" in self._unit_value.metadata.unit.lower()
+ ):
return TEMP_FAHRENHEIT
return TEMP_CELSIUS
+ @property
+ def precision(self) -> float:
+ """Return the precision of 0.1."""
+ return PRECISION_TENTHS
+
@property
def hvac_mode(self) -> str:
"""Return hvac operation ie. heat, cool mode."""
if self._current_mode is None:
# Thermostat(valve) with no support for setting a mode is considered heating-only
return HVAC_MODE_HEAT
+ if self._current_mode.value is None:
+ # guard missing value
+ return HVAC_MODE_HEAT
return ZW_HVAC_MODE_MAP.get(int(self._current_mode.value), HVAC_MODE_HEAT_COOL)
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes."""
return list(self._hvac_modes)
@property
- def hvac_action(self) -> Optional[str]:
+ def hvac_action(self) -> str | None:
"""Return the current running hvac operation if supported."""
if not self._operating_state:
return None
+ if self._operating_state.value is None:
+ # guard missing value
+ return None
return HVAC_CURRENT_MAP.get(int(self._operating_state.value))
@property
- def current_temperature(self) -> Optional[float]:
+ def current_humidity(self) -> int | None:
+ """Return the current humidity level."""
+ return self._current_humidity.value if self._current_humidity else None
+
+ @property
+ def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self._current_temp.value if self._current_temp else None
@property
- def target_temperature(self) -> Optional[float]:
+ def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
- temp = self._setpoint_value(self._current_mode_setpoint_enums[0])
+ if self._current_mode and self._current_mode.value is None:
+ # guard missing value
+ return None
+ try:
+ temp = self._setpoint_value(self._current_mode_setpoint_enums[0])
+ except (IndexError, ValueError):
+ return None
return temp.value if temp else None
@property
- def target_temperature_high(self) -> Optional[float]:
+ def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach."""
- temp = self._setpoint_value(self._current_mode_setpoint_enums[1])
+ if self._current_mode and self._current_mode.value is None:
+ # guard missing value
+ return None
+ try:
+ temp = self._setpoint_value(self._current_mode_setpoint_enums[1])
+ except (IndexError, ValueError):
+ return None
return temp.value if temp else None
@property
- def target_temperature_low(self) -> Optional[float]:
+ def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach."""
- return self.target_temperature
+ if self._current_mode and self._current_mode.value is None:
+ # guard missing value
+ return None
+ if len(self._current_mode_setpoint_enums) > 1:
+ return self.target_temperature
+ return None
@property
- def preset_mode(self) -> Optional[str]:
+ def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp."""
+ if self._current_mode and self._current_mode.value is None:
+ # guard missing value
+ return None
if self._current_mode and int(self._current_mode.value) not in THERMOSTAT_MODES:
return_val: str = self._current_mode.metadata.states.get(
- self._current_mode.value
+ str(self._current_mode.value)
)
return return_val
return PRESET_NONE
@property
- def preset_modes(self) -> Optional[List[str]]:
+ def preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes."""
return list(self._hvac_presets)
+ @property
+ def fan_mode(self) -> str | None:
+ """Return the fan setting."""
+ if (
+ self._fan_mode
+ and self._fan_mode.value is not None
+ and str(self._fan_mode.value) in self._fan_mode.metadata.states
+ ):
+ return cast(str, self._fan_mode.metadata.states[str(self._fan_mode.value)])
+ return None
+
+ @property
+ def fan_modes(self) -> list[str] | None:
+ """Return the list of available fan modes."""
+ if self._fan_mode and self._fan_mode.metadata.states:
+ return list(self._fan_mode.metadata.states.values())
+ return None
+
+ @property
+ def extra_state_attributes(self) -> dict[str, str] | None:
+ """Return the optional state attributes."""
+ if (
+ self._fan_state
+ and self._fan_state.value is not None
+ and str(self._fan_state.value) in self._fan_state.metadata.states
+ ):
+ return {
+ ATTR_FAN_STATE: self._fan_state.metadata.states[
+ str(self._fan_state.value)
+ ]
+ }
+
+ return None
+
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
- support = SUPPORT_PRESET_MODE
- if len(self._current_mode_setpoint_enums) == 1:
- support |= SUPPORT_TARGET_TEMPERATURE
- if len(self._current_mode_setpoint_enums) > 1:
- support |= SUPPORT_TARGET_TEMPERATURE_RANGE
- return support
+ return self._supported_features
+
+ async def async_set_fan_mode(self, fan_mode: str) -> None:
+ """Set new target fan mode."""
+ if not self._fan_mode:
+ return
+
+ try:
+ new_state = int(
+ next(
+ state
+ for state, label in self._fan_mode.metadata.states.items()
+ if label == fan_mode
+ )
+ )
+ except StopIteration:
+ raise ValueError(f"Received an invalid fan mode: {fan_mode}") from None
+
+ await self.info.node.async_set_value(self._fan_mode, new_state)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
- assert self.hass
- hvac_mode: Optional[str] = kwargs.get(ATTR_HVAC_MODE)
+ hvac_mode: str | None = kwargs.get(ATTR_HVAC_MODE)
if hvac_mode is not None:
await self.async_set_hvac_mode(hvac_mode)
@@ -264,7 +402,7 @@ async def async_set_temperature(self, **kwargs: Any) -> None:
setpoint: ZwaveValue = self._setpoint_value(
self._current_mode_setpoint_enums[0]
)
- target_temp: Optional[float] = kwargs.get(ATTR_TEMPERATURE)
+ target_temp: float | None = kwargs.get(ATTR_TEMPERATURE)
if target_temp is not None:
await self.info.node.async_set_value(setpoint, target_temp)
elif len(self._current_mode_setpoint_enums) == 2:
@@ -274,8 +412,8 @@ async def async_set_temperature(self, **kwargs: Any) -> None:
setpoint_high: ZwaveValue = self._setpoint_value(
self._current_mode_setpoint_enums[1]
)
- target_temp_low: Optional[float] = kwargs.get(ATTR_TARGET_TEMP_LOW)
- target_temp_high: Optional[float] = kwargs.get(ATTR_TARGET_TEMP_HIGH)
+ target_temp_low: float | None = kwargs.get(ATTR_TARGET_TEMP_LOW)
+ target_temp_high: float | None = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if target_temp_low is not None:
await self.info.node.async_set_value(setpoint_low, target_temp_low)
if target_temp_high is not None:
diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py
index 5faaa02d03db57..313f4e146a51e4 100644
--- a/homeassistant/components/zwave_js/config_flow.py
+++ b/homeassistant/components/zwave_js/config_flow.py
@@ -1,40 +1,47 @@
"""Config flow for Z-Wave JS integration."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Optional, cast
+from typing import Any, cast
import aiohttp
from async_timeout import timeout
import voluptuous as vol
from zwave_js_server.version import VersionInfo, get_server_version
-from homeassistant import config_entries, core, exceptions
+from homeassistant import config_entries, exceptions
+from homeassistant.components.hassio import is_hassio
from homeassistant.const import CONF_URL
+from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from .const import ( # pylint:disable=unused-import
+from .addon import AddonError, AddonManager, get_addon_manager
+from .const import (
+ CONF_ADDON_DEVICE,
+ CONF_ADDON_NETWORK_KEY,
CONF_INTEGRATION_CREATED_ADDON,
+ CONF_NETWORK_KEY,
+ CONF_USB_PATH,
CONF_USE_ADDON,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
-CONF_ADDON_DEVICE = "device"
-CONF_ADDON_NETWORK_KEY = "network_key"
-CONF_NETWORK_KEY = "network_key"
-CONF_USB_PATH = "usb_path"
DEFAULT_URL = "ws://localhost:3000"
TITLE = "Z-Wave JS"
-ADDON_SETUP_TIME = 10
+ADDON_SETUP_TIMEOUT = 5
+ADDON_SETUP_TIMEOUT_ROUNDS = 4
+SERVER_VERSION_TIMEOUT = 10
ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool})
STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_URL, default=DEFAULT_URL): str})
-async def validate_input(hass: core.HomeAssistant, user_input: dict) -> VersionInfo:
+async def validate_input(hass: HomeAssistant, user_input: dict) -> VersionInfo:
"""Validate if the user input allows us to connect."""
ws_address = user_input[CONF_URL]
@@ -47,18 +54,18 @@ async def validate_input(hass: core.HomeAssistant, user_input: dict) -> VersionI
raise InvalidInput("cannot_connect") from err
-async def async_get_version_info(
- hass: core.HomeAssistant, ws_address: str
-) -> VersionInfo:
+async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> VersionInfo:
"""Return Z-Wave JS version info."""
- async with timeout(10):
- try:
+ try:
+ async with timeout(SERVER_VERSION_TIMEOUT):
version_info: VersionInfo = await get_server_version(
ws_address, async_get_clientsession(hass)
)
- except (asyncio.TimeoutError, aiohttp.ClientError) as err:
- _LOGGER.error("Failed to connect to Z-Wave JS server: %s", err)
- raise CannotConnect from err
+ except (asyncio.TimeoutError, aiohttp.ClientError) as err:
+ # We don't want to spam the log if the add-on isn't started
+ # or takes a long time to start.
+ _LOGGER.debug("Failed to connect to Z-Wave JS server: %s", err)
+ raise CannotConnect from err
return version_info
@@ -71,28 +78,27 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Set up flow instance."""
- self.addon_config: Optional[dict] = None
- self.network_key: Optional[str] = None
- self.usb_path: Optional[str] = None
+ self.network_key: str | None = None
+ self.usb_path: str | None = None
self.use_addon = False
- self.ws_address: Optional[str] = None
+ self.ws_address: str | None = None
# If we install the add-on we should uninstall it on entry remove.
self.integration_created_addon = False
- self.install_task: Optional[asyncio.Task] = None
+ self.install_task: asyncio.Task | None = None
+ self.start_task: asyncio.Task | None = None
async def async_step_user(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle the initial step."""
- assert self.hass # typing
- if self.hass.components.hassio.is_hassio():
+ if is_hassio(self.hass): # type: ignore # no-untyped-call
return await self.async_step_on_supervisor()
return await self.async_step_manual()
async def async_step_manual(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle a manual configuration."""
if user_input is None:
return self.async_show_form(
@@ -101,7 +107,6 @@ async def async_step_manual(
errors = {}
- assert self.hass # typing
try:
version_info = await validate_input(self.hass, user_input)
except InvalidInput as err:
@@ -113,7 +118,15 @@ async def async_step_manual(
await self.async_set_unique_id(
version_info.home_id, raise_on_progress=False
)
- self._abort_if_unique_id_configured(user_input)
+ # Make sure we disable any add-on handling
+ # if the controller is reconfigured in a manual step.
+ self._abort_if_unique_id_configured(
+ updates={
+ **user_input,
+ CONF_USE_ADDON: False,
+ CONF_INTEGRATION_CREATED_ADDON: False,
+ }
+ )
self.ws_address = user_input[CONF_URL]
return self._async_create_entry_from_vars()
@@ -121,14 +134,13 @@ async def async_step_manual(
step_id="manual", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
- async def async_step_hassio( # type: ignore
- self, discovery_info: Dict[str, Any]
- ) -> Dict[str, Any]:
+ async def async_step_hassio( # type: ignore # override
+ self, discovery_info: dict[str, Any]
+ ) -> dict[str, Any]:
"""Receive configuration from add-on discovery info.
This flow is triggered by the Z-Wave JS add-on.
"""
- assert self.hass
self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}"
try:
version_info = await async_get_version_info(self.hass, self.ws_address)
@@ -141,8 +153,8 @@ async def async_step_hassio( # type: ignore
return await self.async_step_hassio_confirm()
async def async_step_hassio_confirm(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Confirm the add-on discovery."""
if user_input is not None:
return await self.async_step_on_supervisor(
@@ -151,7 +163,8 @@ async def async_step_hassio_confirm(
return self.async_show_form(step_id="hassio_confirm")
- def _async_create_entry_from_vars(self) -> Dict[str, Any]:
+ @callback
+ def _async_create_entry_from_vars(self) -> dict[str, Any]:
"""Return a config entry for the flow."""
return self.async_create_entry(
title=TITLE,
@@ -165,8 +178,8 @@ def _async_create_entry_from_vars(self) -> Dict[str, Any]:
)
async def async_step_on_supervisor(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle logic when on Supervisor host."""
if user_input is None:
return self.async_show_form(
@@ -178,68 +191,49 @@ async def async_step_on_supervisor(
self.use_addon = True
if await self._async_is_addon_running():
- discovery_info = await self._async_get_addon_discovery_info()
- self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}"
-
- if not self.unique_id:
- assert self.hass
- try:
- version_info = await async_get_version_info(
- self.hass, self.ws_address
- )
- except CannotConnect:
- return self.async_abort(reason="cannot_connect")
- await self.async_set_unique_id(
- version_info.home_id, raise_on_progress=False
- )
-
- self._abort_if_unique_id_configured()
addon_config = await self._async_get_addon_config()
self.usb_path = addon_config[CONF_ADDON_DEVICE]
self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "")
- return self._async_create_entry_from_vars()
+ return await self.async_step_finish_addon_setup()
if await self._async_is_addon_installed():
- return await self.async_step_start_addon()
+ return await self.async_step_configure_addon()
return await self.async_step_install_addon()
async def async_step_install_addon(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Install Z-Wave JS add-on."""
- assert self.hass
if not self.install_task:
self.install_task = self.hass.async_create_task(self._async_install_addon())
return self.async_show_progress(
step_id="install_addon", progress_action="install_addon"
)
- assert self.hass
try:
await self.install_task
- except self.hass.components.hassio.HassioAPIError as err:
+ except AddonError as err:
_LOGGER.error("Failed to install Z-Wave JS add-on: %s", err)
return self.async_show_progress_done(next_step_id="install_failed")
self.integration_created_addon = True
- return self.async_show_progress_done(next_step_id="start_addon")
+ return self.async_show_progress_done(next_step_id="configure_addon")
async def async_step_install_failed(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Add-on installation failed."""
return self.async_abort(reason="addon_install_failed")
- async def async_step_start_addon(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
- """Ask for config and start Z-Wave JS add-on."""
- if self.addon_config is None:
- self.addon_config = await self._async_get_addon_config()
+ async def async_step_configure_addon(
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
+ """Ask for config for Z-Wave JS add-on."""
+ addon_config = await self._async_get_addon_config()
- errors = {}
+ errors: dict[str, str] = {}
if user_input is not None:
self.network_key = user_input[CONF_NETWORK_KEY]
@@ -250,41 +244,13 @@ async def async_step_start_addon(
CONF_ADDON_NETWORK_KEY: self.network_key,
}
- if new_addon_config != self.addon_config:
+ if new_addon_config != addon_config:
await self._async_set_addon_config(new_addon_config)
- assert self.hass
- try:
- await self.hass.components.hassio.async_start_addon("core_zwave_js")
- except self.hass.components.hassio.HassioAPIError as err:
- _LOGGER.error("Failed to start Z-Wave JS add-on: %s", err)
- errors["base"] = "addon_start_failed"
- else:
- # Sleep some seconds to let the add-on start properly before connecting.
- await asyncio.sleep(ADDON_SETUP_TIME)
- discovery_info = await self._async_get_addon_discovery_info()
- self.ws_address = (
- f"ws://{discovery_info['host']}:{discovery_info['port']}"
- )
-
- if not self.unique_id:
- try:
- version_info = await async_get_version_info(
- self.hass, self.ws_address
- )
- except CannotConnect:
- return self.async_abort(reason="cannot_connect")
- await self.async_set_unique_id(
- version_info.home_id, raise_on_progress=False
- )
-
- self._abort_if_unique_id_configured()
- return self._async_create_entry_from_vars()
+ return await self.async_step_start_addon()
- usb_path = self.addon_config.get(CONF_ADDON_DEVICE, self.usb_path or "")
- network_key = self.addon_config.get(
- CONF_ADDON_NETWORK_KEY, self.network_key or ""
- )
+ usb_path = addon_config.get(CONF_ADDON_DEVICE, self.usb_path or "")
+ network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, self.network_key or "")
data_schema = vol.Schema(
{
@@ -294,17 +260,100 @@ async def async_step_start_addon(
)
return self.async_show_form(
- step_id="start_addon", data_schema=data_schema, errors=errors
+ step_id="configure_addon", data_schema=data_schema, errors=errors
)
+ async def async_step_start_addon(
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
+ """Start Z-Wave JS add-on."""
+ if not self.start_task:
+ self.start_task = self.hass.async_create_task(self._async_start_addon())
+ return self.async_show_progress(
+ step_id="start_addon", progress_action="start_addon"
+ )
+
+ try:
+ await self.start_task
+ except (CannotConnect, AddonError) as err:
+ _LOGGER.error("Failed to start Z-Wave JS add-on: %s", err)
+ return self.async_show_progress_done(next_step_id="start_failed")
+
+ return self.async_show_progress_done(next_step_id="finish_addon_setup")
+
+ async def async_step_start_failed(
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
+ """Add-on start failed."""
+ return self.async_abort(reason="addon_start_failed")
+
+ async def _async_start_addon(self) -> None:
+ """Start the Z-Wave JS add-on."""
+ addon_manager: AddonManager = get_addon_manager(self.hass)
+ try:
+ await addon_manager.async_schedule_start_addon()
+ # Sleep some seconds to let the add-on start properly before connecting.
+ for _ in range(ADDON_SETUP_TIMEOUT_ROUNDS):
+ await asyncio.sleep(ADDON_SETUP_TIMEOUT)
+ try:
+ if not self.ws_address:
+ discovery_info = await self._async_get_addon_discovery_info()
+ self.ws_address = (
+ f"ws://{discovery_info['host']}:{discovery_info['port']}"
+ )
+ await async_get_version_info(self.hass, self.ws_address)
+ except (AbortFlow, CannotConnect) as err:
+ _LOGGER.debug(
+ "Add-on not ready yet, waiting %s seconds: %s",
+ ADDON_SETUP_TIMEOUT,
+ err,
+ )
+ else:
+ break
+ else:
+ raise CannotConnect("Failed to start add-on: timeout")
+ finally:
+ # Continue the flow after show progress when the task is done.
+ self.hass.async_create_task(
+ self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
+ )
+
+ async def async_step_finish_addon_setup(
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
+ """Prepare info needed to complete the config entry.
+
+ Get add-on discovery info and server version info.
+ Set unique id and abort if already configured.
+ """
+ if not self.ws_address:
+ discovery_info = await self._async_get_addon_discovery_info()
+ self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}"
+
+ if not self.unique_id:
+ try:
+ version_info = await async_get_version_info(self.hass, self.ws_address)
+ except CannotConnect as err:
+ raise AbortFlow("cannot_connect") from err
+ await self.async_set_unique_id(
+ version_info.home_id, raise_on_progress=False
+ )
+
+ self._abort_if_unique_id_configured(
+ updates={
+ CONF_URL: self.ws_address,
+ CONF_USB_PATH: self.usb_path,
+ CONF_NETWORK_KEY: self.network_key,
+ }
+ )
+ return self._async_create_entry_from_vars()
+
async def _async_get_addon_info(self) -> dict:
"""Return and cache Z-Wave JS add-on info."""
- assert self.hass
+ addon_manager: AddonManager = get_addon_manager(self.hass)
try:
- addon_info: dict = await self.hass.components.hassio.async_get_addon_info(
- "core_zwave_js"
- )
- except self.hass.components.hassio.HassioAPIError as err:
+ addon_info: dict = await addon_manager.async_get_addon_info()
+ except AddonError as err:
_LOGGER.error("Failed to get Z-Wave JS add-on info: %s", err)
raise AbortFlow("addon_info_failed") from err
@@ -327,21 +376,19 @@ async def _async_get_addon_config(self) -> dict:
async def _async_set_addon_config(self, config: dict) -> None:
"""Set Z-Wave JS add-on config."""
- assert self.hass
options = {"options": config}
+ addon_manager: AddonManager = get_addon_manager(self.hass)
try:
- await self.hass.components.hassio.async_set_addon_options(
- "core_zwave_js", options
- )
- except self.hass.components.hassio.HassioAPIError as err:
+ await addon_manager.async_set_addon_options(options)
+ except AddonError as err:
_LOGGER.error("Failed to set Z-Wave JS add-on config: %s", err)
raise AbortFlow("addon_set_config_failed") from err
async def _async_install_addon(self) -> None:
"""Install the Z-Wave JS add-on."""
- assert self.hass
+ addon_manager: AddonManager = get_addon_manager(self.hass)
try:
- await self.hass.components.hassio.async_install_addon("core_zwave_js")
+ await addon_manager.async_schedule_install_addon()
finally:
# Continue the flow after show progress when the task is done.
self.hass.async_create_task(
@@ -350,22 +397,13 @@ async def _async_install_addon(self) -> None:
async def _async_get_addon_discovery_info(self) -> dict:
"""Return add-on discovery info."""
- assert self.hass
+ addon_manager: AddonManager = get_addon_manager(self.hass)
try:
- discovery_info: dict = (
- await self.hass.components.hassio.async_get_addon_discovery_info(
- "core_zwave_js"
- )
- )
- except self.hass.components.hassio.HassioAPIError as err:
+ discovery_info_config = await addon_manager.async_get_addon_discovery_info()
+ except AddonError as err:
_LOGGER.error("Failed to get Z-Wave JS add-on discovery info: %s", err)
raise AbortFlow("addon_get_discovery_info_failed") from err
- if not discovery_info:
- _LOGGER.error("Failed to get Z-Wave JS add-on discovery info")
- raise AbortFlow("addon_missing_discovery_info")
-
- discovery_info_config: dict = discovery_info["config"]
return discovery_info_config
diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py
index 526a8429bd429f..1c9f78b1751661 100644
--- a/homeassistant/components/zwave_js/const.py
+++ b/homeassistant/components/zwave_js/const.py
@@ -1,5 +1,11 @@
"""Constants for the Z-Wave JS integration."""
+import logging
+
+CONF_ADDON_DEVICE = "device"
+CONF_ADDON_NETWORK_KEY = "network_key"
CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon"
+CONF_NETWORK_KEY = "network_key"
+CONF_USB_PATH = "usb_path"
CONF_USE_ADDON = "use_addon"
DOMAIN = "zwave_js"
PLATFORMS = [
@@ -9,6 +15,7 @@
"fan",
"light",
"lock",
+ "number",
"sensor",
"switch",
]
@@ -17,3 +24,45 @@
DATA_UNSUBSCRIBE = "unsubs"
EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry"
+
+LOGGER = logging.getLogger(__package__)
+
+# constants for events
+ZWAVE_JS_VALUE_NOTIFICATION_EVENT = f"{DOMAIN}_value_notification"
+ZWAVE_JS_NOTIFICATION_EVENT = f"{DOMAIN}_notification"
+ATTR_NODE_ID = "node_id"
+ATTR_HOME_ID = "home_id"
+ATTR_ENDPOINT = "endpoint"
+ATTR_LABEL = "label"
+ATTR_VALUE = "value"
+ATTR_VALUE_RAW = "value_raw"
+ATTR_COMMAND_CLASS = "command_class"
+ATTR_COMMAND_CLASS_NAME = "command_class_name"
+ATTR_TYPE = "type"
+ATTR_PROPERTY_NAME = "property_name"
+ATTR_PROPERTY_KEY_NAME = "property_key_name"
+ATTR_PROPERTY = "property"
+ATTR_PROPERTY_KEY = "property_key"
+ATTR_PARAMETERS = "parameters"
+ATTR_EVENT = "event"
+ATTR_EVENT_LABEL = "event_label"
+ATTR_EVENT_TYPE = "event_type"
+ATTR_EVENT_DATA = "event_data"
+ATTR_DATA_TYPE = "data_type"
+ATTR_WAIT_FOR_RESULT = "wait_for_result"
+
+# service constants
+SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter"
+SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS = "bulk_set_partial_config_parameters"
+
+ATTR_CONFIG_PARAMETER = "parameter"
+ATTR_CONFIG_PARAMETER_BITMASK = "bitmask"
+ATTR_CONFIG_VALUE = "value"
+
+SERVICE_REFRESH_VALUE = "refresh_value"
+
+ATTR_REFRESH_ALL_VALUES = "refresh_all_values"
+
+SERVICE_SET_VALUE = "set_value"
+
+ADDON_SLUG = "core_zwave_js"
diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py
index b7834e8e59cf70..25c69335ed1c23 100644
--- a/homeassistant/components/zwave_js/cover.py
+++ b/homeassistant/components/zwave_js/cover.py
@@ -1,11 +1,15 @@
"""Support for Z-Wave cover devices."""
+from __future__ import annotations
+
import logging
-from typing import Any, Callable, List
+from typing import Any, Callable
from zwave_js_server.client import Client as ZwaveClient
+from zwave_js_server.model.value import Value as ZwaveValue
from homeassistant.components.cover import (
ATTR_POSITION,
+ DEVICE_CLASS_GARAGE,
DOMAIN as COVER_DOMAIN,
SUPPORT_CLOSE,
SUPPORT_OPEN,
@@ -20,7 +24,15 @@
from .entity import ZWaveBaseEntity
LOGGER = logging.getLogger(__name__)
-SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
+
+BARRIER_TARGET_CLOSE = 0
+BARRIER_TARGET_OPEN = 255
+
+BARRIER_STATE_CLOSED = 0
+BARRIER_STATE_CLOSING = 252
+BARRIER_STATE_STOPPED = 253
+BARRIER_STATE_OPENING = 254
+BARRIER_STATE_OPEN = 255
async def async_setup_entry(
@@ -32,8 +44,11 @@ async def async_setup_entry(
@callback
def async_add_cover(info: ZwaveDiscoveryInfo) -> None:
"""Add Z-Wave cover."""
- entities: List[ZWaveBaseEntity] = []
- entities.append(ZWaveCover(config_entry, client, info))
+ entities: list[ZWaveBaseEntity] = []
+ if info.platform_hint == "motorized_barrier":
+ entities.append(ZwaveMotorizedBarrier(config_entry, client, info))
+ else:
+ entities.append(ZWaveCover(config_entry, client, info))
async_add_entities(entities)
hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append(
@@ -59,13 +74,19 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity):
"""Representation of a Z-Wave Cover device."""
@property
- def is_closed(self) -> bool:
+ def is_closed(self) -> bool | None:
"""Return true if cover is closed."""
+ if self.info.primary_value.value is None:
+ # guard missing value
+ return None
return bool(self.info.primary_value.value == 0)
@property
- def current_cover_position(self) -> int:
+ def current_cover_position(self) -> int | None:
"""Return the current position of cover where 0 means closed and 100 is fully open."""
+ if self.info.primary_value.value is None:
+ # guard missing value
+ return None
return round((self.info.primary_value.value / 99) * 100)
async def async_set_cover_position(self, **kwargs: Any) -> None:
@@ -84,3 +105,74 @@ async def async_close_cover(self, **kwargs: Any) -> None:
"""Close cover."""
target_value = self.get_zwave_value("targetValue")
await self.info.node.async_set_value(target_value, 0)
+
+ async def async_stop_cover(self, **kwargs: Any) -> None:
+ """Stop cover."""
+ target_value = self.get_zwave_value("Open") or self.get_zwave_value("Up")
+ if target_value:
+ await self.info.node.async_set_value(target_value, False)
+ target_value = self.get_zwave_value("Close") or self.get_zwave_value("Down")
+ if target_value:
+ await self.info.node.async_set_value(target_value, False)
+
+
+class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity):
+ """Representation of a Z-Wave motorized barrier device."""
+
+ def __init__(
+ self,
+ config_entry: ConfigEntry,
+ client: ZwaveClient,
+ info: ZwaveDiscoveryInfo,
+ ) -> None:
+ """Initialize a ZwaveMotorizedBarrier entity."""
+ super().__init__(config_entry, client, info)
+ self._target_state: ZwaveValue = self.get_zwave_value(
+ "targetState", add_to_watched_value_ids=False
+ )
+
+ @property
+ def supported_features(self) -> int | None:
+ """Flag supported features."""
+ return SUPPORT_OPEN | SUPPORT_CLOSE
+
+ @property
+ def device_class(self) -> str | None:
+ """Return the class of this device, from component DEVICE_CLASSES."""
+ return DEVICE_CLASS_GARAGE
+
+ @property
+ def is_opening(self) -> bool | None:
+ """Return if the cover is opening or not."""
+ if self.info.primary_value.value is None:
+ return None
+ return bool(self.info.primary_value.value == BARRIER_STATE_OPENING)
+
+ @property
+ def is_closing(self) -> bool | None:
+ """Return if the cover is closing or not."""
+ if self.info.primary_value.value is None:
+ return None
+ return bool(self.info.primary_value.value == BARRIER_STATE_CLOSING)
+
+ @property
+ def is_closed(self) -> bool | None:
+ """Return if the cover is closed or not."""
+ if self.info.primary_value.value is None:
+ return None
+ # If a barrier is in the stopped state, the only way to proceed is by
+ # issuing an open cover command. Return None in this case which
+ # produces an unknown state and allows it to be resolved with an open
+ # command.
+ if self.info.primary_value.value == BARRIER_STATE_STOPPED:
+ return None
+
+ return bool(self.info.primary_value.value == BARRIER_STATE_CLOSED)
+
+ async def async_open_cover(self, **kwargs: Any) -> None:
+ """Open the garage door."""
+ await self.info.node.async_set_value(self._target_state, BARRIER_TARGET_OPEN)
+
+ async def async_close_cover(self, **kwargs: Any) -> None:
+ """Close the garage door."""
+ await self.info.node.async_set_value(self._target_state, BARRIER_TARGET_CLOSE)
diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py
index 1fdd8e12fd6128..17ae01aa9b210d 100644
--- a/homeassistant/components/zwave_js/discovery.py
+++ b/homeassistant/components/zwave_js/discovery.py
@@ -1,9 +1,11 @@
"""Map Z-Wave nodes and values to Home Assistant entities."""
+from __future__ import annotations
from dataclasses import dataclass
-from typing import Generator, Optional, Set, Union
+from typing import Any, Generator
from zwave_js_server.const import CommandClass
+from zwave_js_server.model.device_class import DeviceClassItem
from zwave_js_server.model.node import Node as ZwaveNode
from zwave_js_server.model.value import Value as ZwaveValue
@@ -14,49 +16,200 @@
class ZwaveDiscoveryInfo:
"""Info discovered from (primary) ZWave Value to create entity."""
- node: ZwaveNode # node to which the value(s) belongs
- primary_value: ZwaveValue # the value object itself for primary value
- platform: str # the home assistant platform for which an entity should be created
- platform_hint: Optional[
- str
- ] = "" # hint for the platform about this discovered entity
+ # node to which the value(s) belongs
+ node: ZwaveNode
+ # the value object itself for primary value
+ primary_value: ZwaveValue
+ # the home assistant platform for which an entity should be created
+ platform: str
+ # hint for the platform about this discovered entity
+ platform_hint: str | None = ""
+
+
+@dataclass
+class ZWaveValueDiscoverySchema:
+ """Z-Wave Value discovery schema.
+
+ The Z-Wave Value must match these conditions.
+ Use the Z-Wave specifications to find out the values for these parameters:
+ https://github.com/zwave-js/node-zwave-js/tree/master/specs
+ """
- @property
- def value_id(self) -> str:
- """Return the unique value_id belonging to primary value."""
- return f"{self.node.node_id}.{self.primary_value.value_id}"
+ # [optional] the value's command class must match ANY of these values
+ command_class: set[int] | None = None
+ # [optional] the value's endpoint must match ANY of these values
+ endpoint: set[int] | None = None
+ # [optional] the value's property must match ANY of these values
+ property: set[str | int] | None = None
+ # [optional] the value's property name must match ANY of these values
+ property_name: set[str] | None = None
+ # [optional] the value's property key must match ANY of these values
+ property_key: set[str | int] | None = None
+ # [optional] the value's property key name must match ANY of these values
+ property_key_name: set[str] | None = None
+ # [optional] the value's metadata_type must match ANY of these values
+ type: set[str] | None = None
@dataclass
class ZWaveDiscoverySchema:
"""Z-Wave discovery schema.
- The (primary) value for an entity must match these conditions.
+ The Z-Wave node and it's (primary) value for an entity must match these conditions.
Use the Z-Wave specifications to find out the values for these parameters:
https://github.com/zwave-js/node-zwave-js/tree/master/specs
"""
# specify the hass platform for which this scheme applies (e.g. light, sensor)
platform: str
+ # primary value belonging to this discovery scheme
+ primary_value: ZWaveValueDiscoverySchema
# [optional] hint for platform
- hint: Optional[str] = None
+ hint: str | None = None
+ # [optional] the node's manufacturer_id must match ANY of these values
+ manufacturer_id: set[int] | None = None
+ # [optional] the node's product_id must match ANY of these values
+ product_id: set[int] | None = None
+ # [optional] the node's product_type must match ANY of these values
+ product_type: set[int] | None = None
+ # [optional] the node's firmware_version must match ANY of these values
+ firmware_version: set[str] | None = None
# [optional] the node's basic device class must match ANY of these values
- device_class_basic: Optional[Set[str]] = None
+ device_class_basic: set[str | int] | None = None
# [optional] the node's generic device class must match ANY of these values
- device_class_generic: Optional[Set[str]] = None
+ device_class_generic: set[str | int] | None = None
# [optional] the node's specific device class must match ANY of these values
- device_class_specific: Optional[Set[str]] = None
- # [optional] the value's command class must match ANY of these values
- command_class: Optional[Set[int]] = None
- # [optional] the value's endpoint must match ANY of these values
- endpoint: Optional[Set[int]] = None
- # [optional] the value's property must match ANY of these values
- property: Optional[Set[Union[str, int]]] = None
- # [optional] the value's metadata_type must match ANY of these values
- type: Optional[Set[str]] = None
+ device_class_specific: set[str | int] | None = None
+ # [optional] additional values that ALL need to be present on the node for this scheme to pass
+ required_values: list[ZWaveValueDiscoverySchema] | None = None
+ # [optional] additional values that MAY NOT be present on the node for this scheme to pass
+ absent_values: list[ZWaveValueDiscoverySchema] | None = None
+ # [optional] bool to specify if this primary value may be discovered by multiple platforms
+ allow_multi: bool = False
+
+
+def get_config_parameter_discovery_schema(
+ property_: set[str | int] | None = None,
+ property_name: set[str] | None = None,
+ property_key: set[str | int] | None = None,
+ property_key_name: set[str] | None = None,
+ **kwargs: Any,
+) -> ZWaveDiscoverySchema:
+ """
+ Return a discovery schema for a config parameter.
+
+ Supports all keyword arguments to ZWaveValueDiscoverySchema except platform, hint,
+ and primary_value.
+ """
+ return ZWaveDiscoverySchema(
+ platform="sensor",
+ hint="config_parameter",
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={CommandClass.CONFIGURATION},
+ property=property_,
+ property_name=property_name,
+ property_key=property_key,
+ property_key_name=property_key_name,
+ type={"number"},
+ ),
+ **kwargs,
+ )
+
+SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema(
+ command_class={CommandClass.SWITCH_MULTILEVEL},
+ property={"currentValue"},
+ type={"number"},
+)
+# For device class mapping see:
+# https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/deviceClasses.json
DISCOVERY_SCHEMAS = [
+ # ====== START OF DEVICE SPECIFIC MAPPING SCHEMAS =======
+ # Honeywell 39358 In-Wall Fan Control using switch multilevel CC
+ ZWaveDiscoverySchema(
+ platform="fan",
+ manufacturer_id={0x0039},
+ product_id={0x3131},
+ product_type={0x4944},
+ primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
+ ),
+ # GE/Jasco fan controllers using switch multilevel CC
+ ZWaveDiscoverySchema(
+ platform="fan",
+ manufacturer_id={0x0063},
+ product_id={0x3034, 0x3131, 0x3138},
+ product_type={0x4944},
+ primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
+ ),
+ # Leviton ZW4SF fan controllers using switch multilevel CC
+ ZWaveDiscoverySchema(
+ platform="fan",
+ manufacturer_id={0x001D},
+ product_id={0x0002},
+ product_type={0x0038},
+ primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
+ ),
+ # Inovelli LZW36 light / fan controller combo using switch multilevel CC
+ # The fan is endpoint 2, the light is endpoint 1.
+ ZWaveDiscoverySchema(
+ platform="fan",
+ manufacturer_id={0x031E},
+ product_id={0x0001},
+ product_type={0x000E},
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={CommandClass.SWITCH_MULTILEVEL},
+ endpoint={2},
+ property={"currentValue"},
+ type={"number"},
+ ),
+ ),
+ # Fibaro Shutter Fibaro FGS222
+ ZWaveDiscoverySchema(
+ platform="cover",
+ manufacturer_id={0x010F},
+ product_id={0x1000},
+ product_type={0x0302},
+ primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
+ ),
+ # Qubino flush shutter
+ ZWaveDiscoverySchema(
+ platform="cover",
+ manufacturer_id={0x0159},
+ product_id={0x0052},
+ product_type={0x0003},
+ primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
+ ),
+ # Graber/Bali/Spring Fashion Covers
+ ZWaveDiscoverySchema(
+ platform="cover",
+ manufacturer_id={0x026E},
+ product_id={0x5A31},
+ product_type={0x4353},
+ primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
+ ),
+ # iBlinds v2 window blind motor
+ ZWaveDiscoverySchema(
+ platform="cover",
+ manufacturer_id={0x0287},
+ product_id={0x000D},
+ product_type={0x0003},
+ primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
+ ),
+ # ====== START OF CONFIG PARAMETER SPECIFIC MAPPING SCHEMAS =======
+ # Door lock mode config parameter. Functionality equivalent to Notification CC
+ # list sensors.
+ get_config_parameter_discovery_schema(
+ property_name={"Door lock mode"},
+ device_class_generic={"Entry Control"},
+ device_class_specific={
+ "Door Lock",
+ "Advanced Door Lock",
+ "Secure Keypad Door Lock",
+ "Secure Lockbox",
+ },
+ ),
+ # ====== START OF GENERIC MAPPING SCHEMAS =======
# locks
ZWaveDiscoverySchema(
platform="lock",
@@ -67,12 +220,14 @@ class ZWaveDiscoverySchema:
"Secure Keypad Door Lock",
"Secure Lockbox",
},
- command_class={
- CommandClass.LOCK,
- CommandClass.DOOR_LOCK,
- },
- property={"currentMode", "locked"},
- type={"number", "boolean"},
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={
+ CommandClass.LOCK,
+ CommandClass.DOOR_LOCK,
+ },
+ property={"currentMode", "locked"},
+ type={"number", "boolean"},
+ ),
),
# door lock door status
ZWaveDiscoverySchema(
@@ -85,97 +240,150 @@ class ZWaveDiscoverySchema:
"Secure Keypad Door Lock",
"Secure Lockbox",
},
- command_class={
- CommandClass.LOCK,
- CommandClass.DOOR_LOCK,
- },
- property={"doorStatus"},
- type={"any"},
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={
+ CommandClass.LOCK,
+ CommandClass.DOOR_LOCK,
+ },
+ property={"doorStatus"},
+ type={"any"},
+ ),
),
# climate
+ # thermostats supporting mode (and optional setpoint)
ZWaveDiscoverySchema(
platform="climate",
- device_class_generic={"Thermostat"},
- device_class_specific={
- "Setback Thermostat",
- "Thermostat General",
- "Thermostat General V2",
- },
- command_class={CommandClass.THERMOSTAT_MODE},
- property={"mode"},
- type={"number"},
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={CommandClass.THERMOSTAT_MODE},
+ property={"mode"},
+ type={"number"},
+ ),
),
- # lights
- # primary value is the currentValue (brightness)
+ # thermostats supporting setpoint only (and thus not mode)
ZWaveDiscoverySchema(
- platform="light",
- device_class_generic={"Multilevel Switch", "Remote Switch"},
- device_class_specific={
- "Tunable Color Light",
- "Binary Tunable Color Light",
- "Multilevel Remote Switch",
- "Multilevel Power Switch",
- },
- command_class={CommandClass.SWITCH_MULTILEVEL},
- property={"currentValue"},
- type={"number"},
+ platform="climate",
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={CommandClass.THERMOSTAT_SETPOINT},
+ property={"setpoint"},
+ type={"number"},
+ ),
+ absent_values=[ # mode must not be present to prevent dupes
+ ZWaveValueDiscoverySchema(
+ command_class={CommandClass.THERMOSTAT_MODE},
+ property={"mode"},
+ type={"number"},
+ ),
+ ],
),
# binary sensors
ZWaveDiscoverySchema(
platform="binary_sensor",
hint="boolean",
- command_class={
- CommandClass.SENSOR_BINARY,
- CommandClass.BATTERY,
- },
- type={"boolean"},
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={
+ CommandClass.SENSOR_BINARY,
+ CommandClass.BATTERY,
+ CommandClass.SENSOR_ALARM,
+ },
+ type={"boolean"},
+ ),
),
ZWaveDiscoverySchema(
platform="binary_sensor",
hint="notification",
- command_class={
- CommandClass.NOTIFICATION,
- },
- type={"number"},
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={
+ CommandClass.NOTIFICATION,
+ },
+ type={"number"},
+ ),
+ allow_multi=True,
),
# generic text sensors
ZWaveDiscoverySchema(
platform="sensor",
hint="string_sensor",
- command_class={
- CommandClass.ALARM,
- CommandClass.SENSOR_ALARM,
- CommandClass.INDICATOR,
- CommandClass.NOTIFICATION,
- },
- type={"string"},
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={
+ CommandClass.SENSOR_ALARM,
+ CommandClass.INDICATOR,
+ },
+ type={"string"},
+ ),
),
# generic numeric sensors
ZWaveDiscoverySchema(
platform="sensor",
hint="numeric_sensor",
- command_class={
- CommandClass.SENSOR_MULTILEVEL,
- CommandClass.METER,
- CommandClass.ALARM,
- CommandClass.SENSOR_ALARM,
- CommandClass.INDICATOR,
- CommandClass.BATTERY,
- CommandClass.NOTIFICATION,
- CommandClass.BASIC,
- },
- type={"number"},
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={
+ CommandClass.SENSOR_MULTILEVEL,
+ CommandClass.SENSOR_ALARM,
+ CommandClass.INDICATOR,
+ CommandClass.BATTERY,
+ },
+ type={"number"},
+ ),
+ ),
+ # numeric sensors for Meter CC
+ ZWaveDiscoverySchema(
+ platform="sensor",
+ hint="numeric_sensor",
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={
+ CommandClass.METER,
+ },
+ type={"number"},
+ property={"value"},
+ ),
+ ),
+ # special list sensors (Notification CC)
+ ZWaveDiscoverySchema(
+ platform="sensor",
+ hint="list_sensor",
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={
+ CommandClass.NOTIFICATION,
+ },
+ type={"number"},
+ ),
+ allow_multi=True,
+ ),
+ # sensor for basic CC
+ ZWaveDiscoverySchema(
+ platform="sensor",
+ hint="numeric_sensor",
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={
+ CommandClass.BASIC,
+ },
+ type={"number"},
+ property={"currentValue"},
+ ),
),
# binary switches
ZWaveDiscoverySchema(
platform="switch",
- command_class={CommandClass.SWITCH_BINARY},
- property={"currentValue"},
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={CommandClass.SWITCH_BINARY}, property={"currentValue"}
+ ),
+ ),
+ # binary switch
+ # barrier operator signaling states
+ ZWaveDiscoverySchema(
+ platform="switch",
+ hint="barrier_event_signaling_state",
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={CommandClass.BARRIER_OPERATOR},
+ property={"signalingState"},
+ type={"number"},
+ ),
),
# cover
+ # window coverings
ZWaveDiscoverySchema(
platform="cover",
- hint="cover",
+ hint="window_cover",
device_class_generic={"Multilevel Switch"},
device_class_specific={
"Motor Control Class A",
@@ -183,9 +391,25 @@ class ZWaveDiscoverySchema:
"Motor Control Class C",
"Multiposition Motor",
},
- command_class={CommandClass.SWITCH_MULTILEVEL},
- property={"currentValue"},
- type={"number"},
+ primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
+ ),
+ # cover
+ # motorized barriers
+ ZWaveDiscoverySchema(
+ platform="cover",
+ hint="motorized_barrier",
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={CommandClass.BARRIER_OPERATOR},
+ property={"currentState"},
+ type={"number"},
+ ),
+ required_values=[
+ ZWaveValueDiscoverySchema(
+ command_class={CommandClass.BARRIER_OPERATOR},
+ property={"targetState"},
+ type={"number"},
+ ),
+ ],
),
# fan
ZWaveDiscoverySchema(
@@ -193,9 +417,24 @@ class ZWaveDiscoverySchema:
hint="fan",
device_class_generic={"Multilevel Switch"},
device_class_specific={"Fan Switch"},
- command_class={CommandClass.SWITCH_MULTILEVEL},
- property={"currentValue"},
- type={"number"},
+ primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
+ ),
+ # number platform
+ # valve control for thermostats
+ ZWaveDiscoverySchema(
+ platform="number",
+ hint="Valve control",
+ device_class_generic={"Thermostat"},
+ primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
+ ),
+ # lights
+ # primary value is the currentValue (brightness)
+ # catch any device with multilevel CC as light
+ # NOTE: keep this at the bottom of the discovery scheme,
+ # to handle all others that need the multilevel CC first
+ ZWaveDiscoverySchema(
+ platform="light",
+ primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
),
]
@@ -204,54 +443,133 @@ class ZWaveDiscoverySchema:
def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None, None]:
"""Run discovery on ZWave node and return matching (primary) values."""
for value in node.values.values():
- disc_val = async_discover_value(value)
- if disc_val:
- yield disc_val
+ for schema in DISCOVERY_SCHEMAS:
+ # check manufacturer_id
+ if (
+ schema.manufacturer_id is not None
+ and value.node.manufacturer_id not in schema.manufacturer_id
+ ):
+ continue
+
+ # check product_id
+ if (
+ schema.product_id is not None
+ and value.node.product_id not in schema.product_id
+ ):
+ continue
+
+ # check product_type
+ if (
+ schema.product_type is not None
+ and value.node.product_type not in schema.product_type
+ ):
+ continue
+
+ # check firmware_version
+ if (
+ schema.firmware_version is not None
+ and value.node.firmware_version not in schema.firmware_version
+ ):
+ continue
+
+ # check device_class_basic
+ if not check_device_class(
+ value.node.device_class.basic, schema.device_class_basic
+ ):
+ continue
+
+ # check device_class_generic
+ if not check_device_class(
+ value.node.device_class.generic, schema.device_class_generic
+ ):
+ continue
+
+ # check device_class_specific
+ if not check_device_class(
+ value.node.device_class.specific, schema.device_class_specific
+ ):
+ continue
+
+ # check primary value
+ if not check_value(value, schema.primary_value):
+ continue
+
+ # check additional required values
+ if schema.required_values is not None and not all(
+ any(check_value(val, val_scheme) for val in node.values.values())
+ for val_scheme in schema.required_values
+ ):
+ continue
+
+ # check for values that may not be present
+ if schema.absent_values is not None and any(
+ any(check_value(val, val_scheme) for val in node.values.values())
+ for val_scheme in schema.absent_values
+ ):
+ continue
+
+ # all checks passed, this value belongs to an entity
+ yield ZwaveDiscoveryInfo(
+ node=value.node,
+ primary_value=value,
+ platform=schema.platform,
+ platform_hint=schema.hint,
+ )
+
+ if not schema.allow_multi:
+ # break out of loop, this value may not be discovered by other schemas/platforms
+ break
+
+
+@callback
+def check_value(value: ZwaveValue, schema: ZWaveValueDiscoverySchema) -> bool:
+ """Check if value matches scheme."""
+ # check command_class
+ if (
+ schema.command_class is not None
+ and value.command_class not in schema.command_class
+ ):
+ return False
+ # check endpoint
+ if schema.endpoint is not None and value.endpoint not in schema.endpoint:
+ return False
+ # check property
+ if schema.property is not None and value.property_ not in schema.property:
+ return False
+ # check property_name
+ if (
+ schema.property_name is not None
+ and value.property_name not in schema.property_name
+ ):
+ return False
+ # check property_key
+ if (
+ schema.property_key is not None
+ and value.property_key not in schema.property_key
+ ):
+ return False
+ # check property_key_name
+ if (
+ schema.property_key_name is not None
+ and value.property_key_name not in schema.property_key_name
+ ):
+ return False
+ # check metadata_type
+ if schema.type is not None and value.metadata.type not in schema.type:
+ return False
+ return True
@callback
-def async_discover_value(value: ZwaveValue) -> Optional[ZwaveDiscoveryInfo]:
- """Run discovery on Z-Wave value and return ZwaveDiscoveryInfo if match found."""
- for schema in DISCOVERY_SCHEMAS:
- # check device_class_basic
- if (
- schema.device_class_basic is not None
- and value.node.device_class.basic not in schema.device_class_basic
- ):
- continue
- # check device_class_generic
- if (
- schema.device_class_generic is not None
- and value.node.device_class.generic not in schema.device_class_generic
- ):
- continue
- # check device_class_specific
- if (
- schema.device_class_specific is not None
- and value.node.device_class.specific not in schema.device_class_specific
- ):
- continue
- # check command_class
- if (
- schema.command_class is not None
- and value.command_class not in schema.command_class
- ):
- continue
- # check endpoint
- if schema.endpoint is not None and value.endpoint not in schema.endpoint:
- continue
- # check property
- if schema.property is not None and value.property_ not in schema.property:
- continue
- # check metadata_type
- if schema.type is not None and value.metadata.type not in schema.type:
- continue
- # all checks passed, this value belongs to an entity
- return ZwaveDiscoveryInfo(
- node=value.node,
- primary_value=value,
- platform=schema.platform,
- platform_hint=schema.hint,
- )
-
- return None
+def check_device_class(
+ device_class: DeviceClassItem, required_value: set[str | int] | None
+) -> bool:
+ """Check if device class id or label matches."""
+ if required_value is None:
+ return True
+ for val in required_value:
+ if isinstance(val, str) and device_class.label == val:
+ return True
+ if isinstance(val, int) and device_class.key == val:
+ return True
+ return False
diff --git a/homeassistant/components/zwave_js/docs/running_z_wave_js_server.png b/homeassistant/components/zwave_js/docs/running_z_wave_js_server.png
new file mode 100644
index 00000000000000..53b5bdd3f8f3dc
Binary files /dev/null and b/homeassistant/components/zwave_js/docs/running_z_wave_js_server.png differ
diff --git a/homeassistant/components/zwave_js/docs/z_wave_js_connection.png b/homeassistant/components/zwave_js/docs/z_wave_js_connection.png
new file mode 100644
index 00000000000000..dd40a78728f101
Binary files /dev/null and b/homeassistant/components/zwave_js/docs/z_wave_js_connection.png differ
diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py
index 5c64ddbc496b0e..458cc7216506da 100644
--- a/homeassistant/components/zwave_js/entity.py
+++ b/homeassistant/components/zwave_js/entity.py
@@ -1,7 +1,7 @@
"""Generic Z-Wave Entity Class."""
+from __future__ import annotations
import logging
-from typing import Optional, Union
from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.model.value import Value as ZwaveValue, get_value_id
@@ -13,6 +13,7 @@
from .const import DOMAIN
from .discovery import ZwaveDiscoveryInfo
+from .helpers import get_device_id, get_unique_id
LOGGER = logging.getLogger(__name__)
@@ -29,6 +30,10 @@ def __init__(
self.config_entry = config_entry
self.client = client
self.info = info
+ self._name = self.generate_name()
+ self._unique_id = get_unique_id(
+ self.client.driver.controller.home_id, self.info.primary_value.value_id
+ )
# entities requiring additional values, can add extra ids to this list
self.watched_value_ids = {self.info.primary_value.value_id}
@@ -39,19 +44,45 @@ def on_value_update(self) -> None:
To be overridden by platforms needing this event.
"""
+ async def async_poll_value(self, refresh_all_values: bool) -> None:
+ """Poll a value."""
+ if not refresh_all_values:
+ self.hass.async_create_task(
+ self.info.node.async_poll_value(self.info.primary_value)
+ )
+ LOGGER.info(
+ (
+ "Refreshing primary value %s for %s, "
+ "state update may be delayed for devices on battery"
+ ),
+ self.info.primary_value,
+ self.entity_id,
+ )
+ return
+
+ for value_id in self.watched_value_ids:
+ self.hass.async_create_task(self.info.node.async_poll_value(value_id))
+
+ LOGGER.info(
+ (
+ "Refreshing values %s for %s, state update may be delayed for "
+ "devices on battery"
+ ),
+ ", ".join(self.watched_value_ids),
+ self.entity_id,
+ )
+
async def async_added_to_hass(self) -> None:
"""Call when entity is added."""
- assert self.hass # typing
# Add value_changed callbacks.
self.async_on_remove(
self.info.node.on(EVENT_VALUE_UPDATED, self._value_changed)
)
-
self.async_on_remove(
async_dispatcher_connect(
self.hass,
- f"{DOMAIN}_{self.config_entry.entry_id}_connection_state",
- self.async_write_ha_state,
+ f"{DOMAIN}_{self.unique_id}_poll_value",
+ self.async_poll_value,
)
)
@@ -60,29 +91,52 @@ def device_info(self) -> dict:
"""Return device information for the device registry."""
# device is precreated in main handler
return {
- "identifiers": {
- (
- DOMAIN,
- f"{self.client.driver.controller.home_id}-{self.info.node.node_id}",
- )
- },
+ "identifiers": {get_device_id(self.client, self.info.node)},
}
+ def generate_name(
+ self,
+ include_value_name: bool = False,
+ alternate_value_name: str | None = None,
+ additional_info: list[str] | None = None,
+ name_suffix: str | None = None,
+ ) -> str:
+ """Generate entity name."""
+ if additional_info is None:
+ additional_info = []
+ name: str = (
+ self.info.node.name
+ or self.info.node.device_config.description
+ or f"Node {self.info.node.node_id}"
+ )
+ if name_suffix:
+ name = f"{name} {name_suffix}"
+ if include_value_name:
+ value_name = (
+ alternate_value_name
+ or self.info.primary_value.metadata.label
+ or self.info.primary_value.property_key_name
+ or self.info.primary_value.property_name
+ )
+ name = f"{name}: {value_name}"
+ for item in additional_info:
+ if item:
+ name += f" - {item}"
+ # append endpoint if > 1
+ if self.info.primary_value.endpoint > 1:
+ name += f" ({self.info.primary_value.endpoint})"
+
+ return name
+
@property
def name(self) -> str:
"""Return default name from device name and value name combination."""
- node_name = self.info.node.name or self.info.node.device_config.description
- value_name = (
- self.info.primary_value.metadata.label
- or self.info.primary_value.property_key_name
- or self.info.primary_value.property_name
- )
- return f"{node_name}: {value_name}"
+ return self._name
@property
def unique_id(self) -> str:
"""Return the unique_id of the entity."""
- return f"{self.client.driver.controller.home_id}.{self.info.value_id}"
+ return self._unique_id
@property
def available(self) -> bool:
@@ -116,12 +170,13 @@ def _value_changed(self, event_data: dict) -> None:
@callback
def get_zwave_value(
self,
- value_property: Union[str, int],
- command_class: Optional[int] = None,
- endpoint: Optional[int] = None,
- value_property_key_name: Optional[str] = None,
+ value_property: str | int,
+ command_class: int | None = None,
+ endpoint: int | None = None,
+ value_property_key: int | None = None,
add_to_watched_value_ids: bool = True,
- ) -> Optional[ZwaveValue]:
+ check_all_endpoints: bool = False,
+ ) -> ZwaveValue | None:
"""Return specific ZwaveValue on this ZwaveNode."""
# use commandclass and endpoint from primary value if omitted
return_value = None
@@ -129,17 +184,33 @@ def get_zwave_value(
command_class = self.info.primary_value.command_class
if endpoint is None:
endpoint = self.info.primary_value.endpoint
+
# lookup value by value_id
value_id = get_value_id(
self.info.node,
- {
- "commandClass": command_class,
- "endpoint": endpoint,
- "property": value_property,
- "propertyKeyName": value_property_key_name,
- },
+ command_class,
+ value_property,
+ endpoint=endpoint,
+ property_key=value_property_key,
)
return_value = self.info.node.values.get(value_id)
+
+ # If we haven't found a value and check_all_endpoints is True, we should
+ # return the first value we can find on any other endpoint
+ if return_value is None and check_all_endpoints:
+ for endpoint_ in self.info.node.endpoints:
+ if endpoint_.index != self.info.primary_value.endpoint:
+ value_id = get_value_id(
+ self.info.node,
+ command_class,
+ value_property,
+ endpoint=endpoint_.index,
+ property_key=value_property_key,
+ )
+ return_value = self.info.node.values.get(value_id)
+ if return_value:
+ break
+
# add to watched_ids list so we will be triggered when the value updates
if (
return_value
diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py
index 6e62869f74983d..100e400f9f70ae 100644
--- a/homeassistant/components/zwave_js/fan.py
+++ b/homeassistant/components/zwave_js/fan.py
@@ -1,7 +1,8 @@
"""Support for Z-Wave fans."""
-import logging
+from __future__ import annotations
+
import math
-from typing import Any, Callable, List, Optional
+from typing import Any, Callable
from zwave_js_server.client import Client as ZwaveClient
@@ -14,6 +15,7 @@
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import (
+ int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@@ -22,8 +24,6 @@
from .discovery import ZwaveDiscoveryInfo
from .entity import ZWaveBaseEntity
-_LOGGER = logging.getLogger(__name__)
-
SUPPORTED_FEATURES = SUPPORT_SET_SPEED
SPEED_RANGE = (1, 99) # off is not included
@@ -38,7 +38,7 @@ async def async_setup_entry(
@callback
def async_add_fan(info: ZwaveDiscoveryInfo) -> None:
"""Add Z-Wave fan."""
- entities: List[ZWaveBaseEntity] = []
+ entities: list[ZWaveBaseEntity] = []
entities.append(ZwaveFan(config_entry, client, info))
async_add_entities(entities)
@@ -54,7 +54,7 @@ def async_add_fan(info: ZwaveDiscoveryInfo) -> None:
class ZwaveFan(ZWaveBaseEntity, FanEntity):
"""Representation of a Z-Wave fan."""
- async def async_set_percentage(self, percentage: Optional[int]) -> None:
+ async def async_set_percentage(self, percentage: int | None) -> None:
"""Set the speed percentage of the fan."""
target_value = self.get_zwave_value("targetValue")
@@ -70,9 +70,9 @@ async def async_set_percentage(self, percentage: Optional[int]) -> None:
async def async_turn_on(
self,
- speed: Optional[str] = None,
- percentage: Optional[int] = None,
- preset_mode: Optional[str] = None,
+ speed: str | None = None,
+ percentage: int | None = None,
+ preset_mode: str | None = None,
**kwargs: Any,
) -> None:
"""Turn the device on."""
@@ -84,15 +84,26 @@ async def async_turn_off(self, **kwargs: Any) -> None:
await self.info.node.async_set_value(target_value, 0)
@property
- def is_on(self) -> bool:
+ def is_on(self) -> bool | None: # type: ignore
"""Return true if device is on (speed above 0)."""
+ if self.info.primary_value.value is None:
+ # guard missing value
+ return None
return bool(self.info.primary_value.value > 0)
@property
- def percentage(self) -> int:
+ def percentage(self) -> int | None:
"""Return the current speed percentage."""
+ if self.info.primary_value.value is None:
+ # guard missing value
+ return None
return ranged_value_to_percentage(SPEED_RANGE, self.info.primary_value.value)
+ @property
+ def speed_count(self) -> int:
+ """Return the number of speeds the fan supports."""
+ return int_states_in_range(SPEED_RANGE)
+
@property
def supported_features(self) -> int:
"""Flag supported features."""
diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py
new file mode 100644
index 00000000000000..d535a22394cd94
--- /dev/null
+++ b/homeassistant/components/zwave_js/helpers.py
@@ -0,0 +1,108 @@
+"""Helper functions for Z-Wave JS integration."""
+from __future__ import annotations
+
+from typing import cast
+
+from zwave_js_server.client import Client as ZwaveClient
+from zwave_js_server.model.node import Node as ZwaveNode
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.device_registry import async_get as async_get_dev_reg
+from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg
+
+from .const import DATA_CLIENT, DOMAIN
+
+
+@callback
+def get_unique_id(home_id: str, value_id: str) -> str:
+ """Get unique ID from home ID and value ID."""
+ return f"{home_id}.{value_id}"
+
+
+@callback
+def get_device_id(client: ZwaveClient, node: ZwaveNode) -> tuple[str, str]:
+ """Get device registry identifier for Z-Wave node."""
+ return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}")
+
+
+@callback
+def get_home_and_node_id_from_device_id(device_id: tuple[str, str]) -> list[str]:
+ """
+ Get home ID and node ID for Z-Wave device registry entry.
+
+ Returns [home_id, node_id]
+ """
+ return device_id[1].split("-")
+
+
+@callback
+def async_get_node_from_device_id(hass: HomeAssistant, device_id: str) -> ZwaveNode:
+ """
+ Get node from a device ID.
+
+ Raises ValueError if device is invalid or node can't be found.
+ """
+ device_entry = async_get_dev_reg(hass).async_get(device_id)
+
+ if not device_entry:
+ raise ValueError("Device ID is not valid")
+
+ # Use device config entry ID's to validate that this is a valid zwave_js device
+ # and to get the client
+ config_entry_ids = device_entry.config_entries
+ config_entry_id = next(
+ (
+ config_entry_id
+ for config_entry_id in config_entry_ids
+ if cast(
+ ConfigEntry,
+ hass.config_entries.async_get_entry(config_entry_id),
+ ).domain
+ == DOMAIN
+ ),
+ None,
+ )
+ if config_entry_id is None or config_entry_id not in hass.data[DOMAIN]:
+ raise ValueError("Device is not from an existing zwave_js config entry")
+
+ client = hass.data[DOMAIN][config_entry_id][DATA_CLIENT]
+
+ # Get node ID from device identifier, perform some validation, and then get the
+ # node
+ identifier = next(
+ (
+ get_home_and_node_id_from_device_id(identifier)
+ for identifier in device_entry.identifiers
+ if identifier[0] == DOMAIN
+ ),
+ None,
+ )
+
+ node_id = int(identifier[1]) if identifier is not None else None
+
+ if node_id is None or node_id not in client.driver.controller.nodes:
+ raise ValueError("Device node can't be found")
+
+ return client.driver.controller.nodes[node_id]
+
+
+@callback
+def async_get_node_from_entity_id(hass: HomeAssistant, entity_id: str) -> ZwaveNode:
+ """
+ Get node from an entity ID.
+
+ Raises ValueError if entity is invalid.
+ """
+ entity_entry = async_get_ent_reg(hass).async_get(entity_id)
+
+ if not entity_entry:
+ raise ValueError("Entity ID is not valid")
+
+ if entity_entry.platform != DOMAIN:
+ raise ValueError("Entity is not from zwave_js integration")
+
+ # Assert for mypy, safe because we know that zwave_js entities are always
+ # tied to a device
+ assert entity_entry.device_id
+ return async_get_node_from_device_id(hass, entity_entry.device_id)
diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py
index acfa22e5847979..d809874c432d8a 100644
--- a/homeassistant/components/zwave_js/light.py
+++ b/homeassistant/components/zwave_js/light.py
@@ -1,9 +1,11 @@
"""Support for Z-Wave lights."""
+from __future__ import annotations
+
import logging
-from typing import Any, Callable, Optional, Tuple
+from typing import Any, Callable
from zwave_js_server.client import Client as ZwaveClient
-from zwave_js_server.const import CommandClass
+from zwave_js_server.const import ColorComponent, CommandClass
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -30,6 +32,17 @@
LOGGER = logging.getLogger(__name__)
+MULTI_COLOR_MAP = {
+ ColorComponent.WARM_WHITE: "warmWhite",
+ ColorComponent.COLD_WHITE: "coldWhite",
+ ColorComponent.RED: "red",
+ ColorComponent.GREEN: "green",
+ ColorComponent.BLUE: "blue",
+ ColorComponent.AMBER: "amber",
+ ColorComponent.CYAN: "cyan",
+ ColorComponent.PURPLE: "purple",
+}
+
async def async_setup_entry(
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable
@@ -74,9 +87,9 @@ def __init__(
self._supports_color = False
self._supports_white_value = False
self._supports_color_temp = False
- self._hs_color: Optional[Tuple[float, float]] = None
- self._white_value: Optional[int] = None
- self._color_temp: Optional[int] = None
+ self._hs_color: tuple[float, float] | None = None
+ self._white_value: int | None = None
+ self._color_temp: int | None = None
self._min_mireds = 153 # 6500K as a safe default
self._max_mireds = 370 # 2700K as a safe default
self._supported_features = SUPPORT_BRIGHTNESS
@@ -105,14 +118,6 @@ def brightness(self) -> int:
Z-Wave multilevel switches use a range of [0, 99] to control brightness.
"""
- # prefer targetValue only if CC Version >= 4
- # otherwise use currentValue (pre V4 dimmers)
- if (
- self._target_value
- and self._target_value.value is not None
- and self._target_value.cc_version >= 4
- ):
- return round((self._target_value.value / 99) * 255)
if self.info.primary_value.value is not None:
return round((self.info.primary_value.value / 99) * 255)
return 0
@@ -123,17 +128,17 @@ def is_on(self) -> bool:
return self.brightness > 0
@property
- def hs_color(self) -> Optional[Tuple[float, float]]:
+ def hs_color(self) -> tuple[float, float] | None:
"""Return the hs color."""
return self._hs_color
@property
- def white_value(self) -> Optional[int]:
+ def white_value(self) -> int | None:
"""Return the white value of this light between 0..255."""
return self._white_value
@property
- def color_temp(self) -> Optional[int]:
+ def color_temp(self) -> int | None:
"""Return the color temperature."""
return self._color_temp
@@ -148,7 +153,7 @@ def max_mireds(self) -> int:
return self._max_mireds
@property
- def supported_features(self) -> Optional[int]:
+ def supported_features(self) -> int:
"""Flag supported features."""
return self._supported_features
@@ -157,21 +162,21 @@ async def async_turn_on(self, **kwargs: Any) -> None:
# RGB/HS color
hs_color = kwargs.get(ATTR_HS_COLOR)
if hs_color is not None and self._supports_color:
- # set white levels to 0 when setting rgb
- await self._async_set_color("Warm White", 0)
- await self._async_set_color("Cold White", 0)
red, green, blue = color_util.color_hs_to_RGB(*hs_color)
- await self._async_set_color("Red", red)
- await self._async_set_color("Green", green)
- await self._async_set_color("Blue", blue)
+ colors = {
+ ColorComponent.RED: red,
+ ColorComponent.GREEN: green,
+ ColorComponent.BLUE: blue,
+ }
+ if self._supports_color_temp:
+ # turn of white leds when setting rgb
+ colors[ColorComponent.WARM_WHITE] = 0
+ colors[ColorComponent.COLD_WHITE] = 0
+ await self._async_set_colors(colors)
# Color temperature
color_temp = kwargs.get(ATTR_COLOR_TEMP)
if color_temp is not None and self._supports_color_temp:
- # turn off rgb when setting white values
- await self._async_set_color("Red", 0)
- await self._async_set_color("Green", 0)
- await self._async_set_color("Blue", 0)
# Limit color temp to min/max values
cold = max(
0,
@@ -185,17 +190,28 @@ async def async_turn_on(self, **kwargs: Any) -> None:
),
)
warm = 255 - cold
- await self._async_set_color("Warm White", warm)
- await self._async_set_color("Cold White", cold)
+ await self._async_set_colors(
+ {
+ # turn off color leds when setting color temperature
+ ColorComponent.RED: 0,
+ ColorComponent.GREEN: 0,
+ ColorComponent.BLUE: 0,
+ ColorComponent.WARM_WHITE: warm,
+ ColorComponent.COLD_WHITE: cold,
+ }
+ )
# White value
white_value = kwargs.get(ATTR_WHITE_VALUE)
if white_value is not None and self._supports_white_value:
- # turn off rgb when setting white values
- await self._async_set_color("Red", 0)
- await self._async_set_color("Green", 0)
- await self._async_set_color("Blue", 0)
- await self._async_set_color("Warm White", white_value)
+ # white led brightness is controlled by white level
+ # rgb leds (if any) can be on at the same time
+ await self._async_set_colors(
+ {
+ ColorComponent.WARM_WHITE: white_value,
+ ColorComponent.COLD_WHITE: white_value,
+ }
+ )
# set brightness
await self._async_set_brightness(
@@ -206,33 +222,46 @@ async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION))
- async def _async_set_color(self, color_name: str, new_value: int) -> None:
- """Set defined color to given value."""
- cur_zwave_value = self.get_zwave_value(
- "currentColor",
+ async def _async_set_colors(self, colors: dict[ColorComponent, int]) -> None:
+ """Set (multiple) defined colors to given value(s)."""
+ # prefer the (new) combined color property
+ # https://github.com/zwave-js/node-zwave-js/pull/1782
+ combined_color_val = self.get_zwave_value(
+ "targetColor",
CommandClass.SWITCH_COLOR,
- value_property_key_name=color_name,
+ value_property_key=None,
)
- # guard for unsupported command
- if cur_zwave_value is None:
+ if combined_color_val and isinstance(combined_color_val.value, dict):
+ colors_dict = {}
+ for color, value in colors.items():
+ color_name = MULTI_COLOR_MAP[color]
+ colors_dict[color_name] = value
+ # set updated color object
+ await self.info.node.async_set_value(combined_color_val, colors_dict)
return
+
+ # fallback to setting the color(s) one by one if multicolor fails
+ # not sure this is needed at all, but just in case
+ for color, value in colors.items():
+ await self._async_set_color(color, value)
+
+ async def _async_set_color(self, color: ColorComponent, new_value: int) -> None:
+ """Set defined color to given value."""
# actually set the new color value
target_zwave_value = self.get_zwave_value(
"targetColor",
CommandClass.SWITCH_COLOR,
- value_property_key_name=color_name,
+ value_property_key=color.value,
)
if target_zwave_value is None:
+ # guard for unsupported color
return
await self.info.node.async_set_value(target_zwave_value, new_value)
async def _async_set_brightness(
- self, brightness: Optional[int], transition: Optional[int] = None
+ self, brightness: int | None, transition: int | None = None
) -> None:
"""Set new brightness to light."""
- if brightness is None and self.info.primary_value.value:
- # there is no point in setting default brightness when light is already on
- return
if brightness is None:
# Level 255 means to set it to previous value.
zwave_brightness = 255
@@ -245,9 +274,7 @@ async def _async_set_brightness(
# setting a value requires setting targetValue
await self.info.node.async_set_value(self._target_value, zwave_brightness)
- async def _async_set_transition_duration(
- self, duration: Optional[int] = None
- ) -> None:
+ async def _async_set_transition_duration(self, duration: int | None = None) -> None:
"""Set the transition time for the brightness value."""
if self._dimming_duration is None:
return
@@ -281,57 +308,74 @@ async def _async_set_transition_duration(
@callback
def _calculate_color_values(self) -> None:
"""Calculate light colors."""
-
- # RGB support
+ # NOTE: We lookup all values here (instead of relying on the multicolor one)
+ # to find out what colors are supported
+ # as this is a simple lookup by key, this not heavy
red_val = self.get_zwave_value(
- "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Red"
+ "currentColor",
+ CommandClass.SWITCH_COLOR,
+ value_property_key=ColorComponent.RED.value,
)
green_val = self.get_zwave_value(
- "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Green"
+ "currentColor",
+ CommandClass.SWITCH_COLOR,
+ value_property_key=ColorComponent.GREEN.value,
)
blue_val = self.get_zwave_value(
- "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Blue"
+ "currentColor",
+ CommandClass.SWITCH_COLOR,
+ value_property_key=ColorComponent.BLUE.value,
)
- if red_val and green_val and blue_val:
- self._supports_color = True
- # convert to HS
- if (
- red_val.value is not None
- and green_val.value is not None
- and blue_val.value is not None
- ):
- self._hs_color = color_util.color_RGB_to_hs(
- red_val.value, green_val.value, blue_val.value
- )
-
- # White colors
ww_val = self.get_zwave_value(
"currentColor",
CommandClass.SWITCH_COLOR,
- value_property_key_name="Warm White",
+ value_property_key=ColorComponent.WARM_WHITE.value,
)
cw_val = self.get_zwave_value(
"currentColor",
CommandClass.SWITCH_COLOR,
- value_property_key_name="Cold White",
+ value_property_key=ColorComponent.COLD_WHITE.value,
)
+ # prefer the (new) combined color property
+ # https://github.com/zwave-js/node-zwave-js/pull/1782
+ combined_color_val = self.get_zwave_value(
+ "currentColor",
+ CommandClass.SWITCH_COLOR,
+ value_property_key=None,
+ )
+ if combined_color_val and isinstance(combined_color_val.value, dict):
+ multi_color = combined_color_val.value
+ else:
+ multi_color = {}
+
+ # RGB support
+ if red_val and green_val and blue_val:
+ # prefer values from the multicolor property
+ red = multi_color.get("red", red_val.value)
+ green = multi_color.get("green", green_val.value)
+ blue = multi_color.get("blue", blue_val.value)
+ self._supports_color = True
+ # convert to HS
+ self._hs_color = color_util.color_RGB_to_hs(red, green, blue)
+
+ # color temperature support
if ww_val and cw_val:
- # Color temperature (CW + WW) Support
self._supports_color_temp = True
+ warm_white = multi_color.get("warmWhite", ww_val.value)
+ cold_white = multi_color.get("coldWhite", cw_val.value)
# Calculate color temps based on whites
- cold_level = cw_val.value or 0
- if cold_level or ww_val.value is not None:
+ if cold_white or warm_white:
self._color_temp = round(
self._max_mireds
- - ((cold_level / 255) * (self._max_mireds - self._min_mireds))
+ - ((cold_white / 255) * (self._max_mireds - self._min_mireds))
)
else:
self._color_temp = None
+ # only one white channel (warm white) = white_level support
elif ww_val:
- # only one white channel (warm white)
self._supports_white_value = True
- self._white_value = ww_val.value
+ self._white_value = multi_color.get("warmWhite", ww_val.value)
+ # only one white channel (cool white) = white_level support
elif cw_val:
- # only one white channel (cool white)
self._supports_white_value = True
- self._white_value = cw_val.value
+ self._white_value = multi_color.get("coldWhite", cw_val.value)
diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py
index dedaf9a5e45500..0647885345b678 100644
--- a/homeassistant/components/zwave_js/lock.py
+++ b/homeassistant/components/zwave_js/lock.py
@@ -1,6 +1,8 @@
"""Representation of Z-Wave locks."""
+from __future__ import annotations
+
import logging
-from typing import Any, Callable, Dict, List, Optional, Union
+from typing import Any, Callable
import voluptuous as vol
from zwave_js_server.client import Client as ZwaveClient
@@ -28,7 +30,7 @@
LOGGER = logging.getLogger(__name__)
-STATE_TO_ZWAVE_MAP: Dict[int, Dict[str, Union[int, bool]]] = {
+STATE_TO_ZWAVE_MAP: dict[int, dict[str, int | bool]] = {
CommandClass.DOOR_LOCK: {
STATE_UNLOCKED: DoorLockMode.UNSECURED,
STATE_LOCKED: DoorLockMode.SECURED,
@@ -52,7 +54,7 @@ async def async_setup_entry(
@callback
def async_add_lock(info: ZwaveDiscoveryInfo) -> None:
"""Add Z-Wave Lock."""
- entities: List[ZWaveBaseEntity] = []
+ entities: list[ZWaveBaseEntity] = []
entities.append(ZWaveLock(config_entry, client, info))
async_add_entities(entities)
@@ -88,8 +90,11 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity):
"""Representation of a Z-Wave lock."""
@property
- def is_locked(self) -> Optional[bool]:
+ def is_locked(self) -> bool | None:
"""Return true if the lock is locked."""
+ if self.info.primary_value.value is None:
+ # guard missing value
+ return None
return int(
LOCK_CMD_CLASS_TO_LOCKED_STATE_MAP[
CommandClass(self.info.primary_value.command_class)
@@ -97,7 +102,7 @@ def is_locked(self) -> Optional[bool]:
) == int(self.info.primary_value.value)
async def _set_lock_state(
- self, target_state: str, **kwargs: Dict[str, Any]
+ self, target_state: str, **kwargs: dict[str, Any]
) -> None:
"""Set the lock state."""
target_value: ZwaveValue = self.get_zwave_value(
@@ -109,11 +114,11 @@ async def _set_lock_state(
STATE_TO_ZWAVE_MAP[self.info.primary_value.command_class][target_state],
)
- async def async_lock(self, **kwargs: Dict[str, Any]) -> None:
+ async def async_lock(self, **kwargs: dict[str, Any]) -> None:
"""Lock the lock."""
await self._set_lock_state(STATE_LOCKED)
- async def async_unlock(self, **kwargs: Dict[str, Any]) -> None:
+ async def async_unlock(self, **kwargs: dict[str, Any]) -> None:
"""Unlock the lock."""
await self._set_lock_state(STATE_UNLOCKED)
diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json
index 08ca9668a99b1d..e6b4ed7c2a8301 100644
--- a/homeassistant/components/zwave_js/manifest.json
+++ b/homeassistant/components/zwave_js/manifest.json
@@ -3,7 +3,7 @@
"name": "Z-Wave JS",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
- "requirements": ["zwave-js-server-python==0.14.1"],
+ "requirements": ["zwave-js-server-python==0.23.1"],
"codeowners": ["@home-assistant/z-wave"],
"dependencies": ["http", "websocket_api"]
}
diff --git a/homeassistant/components/zwave_js/migrate.py b/homeassistant/components/zwave_js/migrate.py
new file mode 100644
index 00000000000000..997d34c844530f
--- /dev/null
+++ b/homeassistant/components/zwave_js/migrate.py
@@ -0,0 +1,114 @@
+"""Functions used to migrate unique IDs for Z-Wave JS entities."""
+from __future__ import annotations
+
+import logging
+
+from zwave_js_server.client import Client as ZwaveClient
+from zwave_js_server.model.value import Value as ZwaveValue
+
+from homeassistant.core import callback
+from homeassistant.helpers.entity_registry import EntityRegistry
+
+from .const import DOMAIN
+from .discovery import ZwaveDiscoveryInfo
+from .helpers import get_unique_id
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@callback
+def async_migrate_entity(
+ ent_reg: EntityRegistry, platform: str, old_unique_id: str, new_unique_id: str
+) -> None:
+ """Check if entity with old unique ID exists, and if so migrate it to new ID."""
+ if entity_id := ent_reg.async_get_entity_id(platform, DOMAIN, old_unique_id):
+ _LOGGER.debug(
+ "Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
+ entity_id,
+ old_unique_id,
+ new_unique_id,
+ )
+ try:
+ ent_reg.async_update_entity(
+ entity_id,
+ new_unique_id=new_unique_id,
+ )
+ except ValueError:
+ _LOGGER.debug(
+ (
+ "Entity %s can't be migrated because the unique ID is taken; "
+ "Cleaning it up since it is likely no longer valid"
+ ),
+ entity_id,
+ )
+ ent_reg.async_remove(entity_id)
+
+
+@callback
+def async_migrate_discovered_value(
+ ent_reg: EntityRegistry, client: ZwaveClient, disc_info: ZwaveDiscoveryInfo
+) -> None:
+ """Migrate unique ID for entity/entities tied to discovered value."""
+ new_unique_id = get_unique_id(
+ client.driver.controller.home_id,
+ disc_info.primary_value.value_id,
+ )
+
+ # 2021.2.*, 2021.3.0b0, and 2021.3.0 formats
+ for value_id in get_old_value_ids(disc_info.primary_value):
+ old_unique_id = get_unique_id(
+ client.driver.controller.home_id,
+ value_id,
+ )
+ # Most entities have the same ID format, but notification binary sensors
+ # have a state key in their ID so we need to handle them differently
+ if (
+ disc_info.platform == "binary_sensor"
+ and disc_info.platform_hint == "notification"
+ ):
+ for state_key in disc_info.primary_value.metadata.states:
+ # ignore idle key (0)
+ if state_key == "0":
+ continue
+
+ async_migrate_entity(
+ ent_reg,
+ disc_info.platform,
+ f"{old_unique_id}.{state_key}",
+ f"{new_unique_id}.{state_key}",
+ )
+
+ # Once we've iterated through all state keys, we can move on to the
+ # next item
+ continue
+
+ async_migrate_entity(ent_reg, disc_info.platform, old_unique_id, new_unique_id)
+
+
+@callback
+def get_old_value_ids(value: ZwaveValue) -> list[str]:
+ """Get old value IDs so we can migrate entity unique ID."""
+ value_ids = []
+
+ # Pre 2021.3.0 value ID
+ command_class = value.command_class
+ endpoint = value.endpoint or "00"
+ property_ = value.property_
+ property_key_name = value.property_key_name or "00"
+ value_ids.append(
+ f"{value.node.node_id}.{value.node.node_id}-{command_class}-{endpoint}-"
+ f"{property_}-{property_key_name}"
+ )
+
+ endpoint = "00" if value.endpoint is None else value.endpoint
+ property_key = "00" if value.property_key is None else value.property_key
+ property_key_name = value.property_key_name or "00"
+
+ value_id = (
+ f"{value.node.node_id}-{command_class}-{endpoint}-"
+ f"{property_}-{property_key}-{property_key_name}"
+ )
+ # 2021.3.0b0 and 2021.3.0 value IDs
+ value_ids.extend([f"{value.node.node_id}.{value_id}", value_id])
+
+ return value_ids
diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py
new file mode 100644
index 00000000000000..f418ee3d35b4a1
--- /dev/null
+++ b/homeassistant/components/zwave_js/number.py
@@ -0,0 +1,86 @@
+"""Support for Z-Wave controls using the number platform."""
+from __future__ import annotations
+
+from typing import Callable
+
+from zwave_js_server.client import Client as ZwaveClient
+
+from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN
+from .discovery import ZwaveDiscoveryInfo
+from .entity import ZWaveBaseEntity
+
+
+async def async_setup_entry(
+ hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable
+) -> None:
+ """Set up Z-Wave Number entity from Config Entry."""
+ client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
+
+ @callback
+ def async_add_number(info: ZwaveDiscoveryInfo) -> None:
+ """Add Z-Wave number entity."""
+ entities: list[ZWaveBaseEntity] = []
+ entities.append(ZwaveNumberEntity(config_entry, client, info))
+ async_add_entities(entities)
+
+ hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append(
+ async_dispatcher_connect(
+ hass,
+ f"{DOMAIN}_{config_entry.entry_id}_add_{NUMBER_DOMAIN}",
+ async_add_number,
+ )
+ )
+
+
+class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity):
+ """Representation of a Z-Wave number entity."""
+
+ def __init__(
+ self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo
+ ) -> None:
+ """Initialize a ZwaveNumberEntity entity."""
+ super().__init__(config_entry, client, info)
+ self._name = self.generate_name(
+ include_value_name=True, alternate_value_name=info.platform_hint
+ )
+ if self.info.primary_value.metadata.writeable:
+ self._target_value = self.info.primary_value
+ else:
+ self._target_value = self.get_zwave_value("targetValue")
+
+ @property
+ def min_value(self) -> float:
+ """Return the minimum value."""
+ if self.info.primary_value.metadata.min is None:
+ return 0
+ return float(self.info.primary_value.metadata.min)
+
+ @property
+ def max_value(self) -> float:
+ """Return the maximum value."""
+ if self.info.primary_value.metadata.max is None:
+ return 255
+ return float(self.info.primary_value.metadata.max)
+
+ @property
+ def value(self) -> float | None: # type: ignore
+ """Return the entity value."""
+ if self.info.primary_value.value is None:
+ return None
+ return float(self.info.primary_value.value)
+
+ @property
+ def unit_of_measurement(self) -> str | None:
+ """Return the unit of measurement of this entity, if any."""
+ if self.info.primary_value.metadata.unit is None:
+ return None
+ return str(self.info.primary_value.metadata.unit)
+
+ async def async_set_value(self, value: float) -> None:
+ """Set new value."""
+ await self.info.node.async_set_value(self._target_value, value)
diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py
index ff48790b5f3078..d1e18763b5b1cd 100644
--- a/homeassistant/components/zwave_js/sensor.py
+++ b/homeassistant/components/zwave_js/sensor.py
@@ -1,19 +1,28 @@
"""Representation of Z-Wave sensors."""
+from __future__ import annotations
import logging
-from typing import Callable, Dict, List, Optional
+from typing import Callable, cast
from zwave_js_server.client import Client as ZwaveClient
-from zwave_js_server.const import CommandClass
+from zwave_js_server.const import CommandClass, ConfigurationValueType
+from zwave_js_server.model.value import ConfigurationValue
from homeassistant.components.sensor import (
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_ENERGY,
+ DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_POWER,
DOMAIN as SENSOR_DOMAIN,
+ SensorEntity,
)
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.const import (
+ DEVICE_CLASS_HUMIDITY,
+ DEVICE_CLASS_TEMPERATURE,
+ TEMP_CELSIUS,
+ TEMP_FAHRENHEIT,
+)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -33,12 +42,16 @@ async def async_setup_entry(
@callback
def async_add_sensor(info: ZwaveDiscoveryInfo) -> None:
"""Add Z-Wave Sensor."""
- entities: List[ZWaveBaseEntity] = []
+ entities: list[ZWaveBaseEntity] = []
if info.platform_hint == "string_sensor":
entities.append(ZWaveStringSensor(config_entry, client, info))
elif info.platform_hint == "numeric_sensor":
entities.append(ZWaveNumericSensor(config_entry, client, info))
+ elif info.platform_hint == "list_sensor":
+ entities.append(ZWaveListSensor(config_entry, client, info))
+ elif info.platform_hint == "config_parameter":
+ entities.append(ZWaveConfigParameterSensor(config_entry, client, info))
else:
LOGGER.warning(
"Sensor not implemented for %s/%s",
@@ -58,28 +71,57 @@ def async_add_sensor(info: ZwaveDiscoveryInfo) -> None:
)
-class ZwaveSensorBase(ZWaveBaseEntity):
+class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity):
"""Basic Representation of a Z-Wave sensor."""
- @property
- def device_class(self) -> Optional[str]:
- """Return the device class of the sensor."""
+ def __init__(
+ self,
+ config_entry: ConfigEntry,
+ client: ZwaveClient,
+ info: ZwaveDiscoveryInfo,
+ ) -> None:
+ """Initialize a ZWaveSensorBase entity."""
+ super().__init__(config_entry, client, info)
+ self._name = self.generate_name(include_value_name=True)
+ self._device_class = self._get_device_class()
+
+ def _get_device_class(self) -> str | None:
+ """
+ Get the device class of the sensor.
+
+ This should be run once during initialization so we don't have to calculate
+ this value on every state update.
+ """
if self.info.primary_value.command_class == CommandClass.BATTERY:
return DEVICE_CLASS_BATTERY
if self.info.primary_value.command_class == CommandClass.METER:
- if self.info.primary_value.property_key_name == "kWh_Consumed":
+ if self.info.primary_value.metadata.unit == "kWh":
return DEVICE_CLASS_ENERGY
return DEVICE_CLASS_POWER
- if self.info.primary_value.property_ == "Air temperature":
- return DEVICE_CLASS_TEMPERATURE
+ if isinstance(self.info.primary_value.property_, str):
+ property_lower = self.info.primary_value.property_.lower()
+ if "humidity" in property_lower:
+ return DEVICE_CLASS_HUMIDITY
+ if "temperature" in property_lower:
+ return DEVICE_CLASS_TEMPERATURE
+ if self.info.primary_value.metadata.unit == "W":
+ return DEVICE_CLASS_POWER
+ if self.info.primary_value.metadata.unit == "Lux":
+ return DEVICE_CLASS_ILLUMINANCE
return None
+ @property
+ def device_class(self) -> str | None:
+ """Return the device class of the sensor."""
+ return self._device_class
+
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
# We hide some of the more advanced sensors by default to not overwhelm users
if self.info.primary_value.command_class in [
CommandClass.BASIC,
+ CommandClass.CONFIGURATION,
CommandClass.INDICATOR,
CommandClass.NOTIFICATION,
]:
@@ -96,14 +138,14 @@ class ZWaveStringSensor(ZwaveSensorBase):
"""Representation of a Z-Wave String sensor."""
@property
- def state(self) -> Optional[str]:
+ def state(self) -> str | None:
"""Return state of the sensor."""
if self.info.primary_value.value is None:
return None
return str(self.info.primary_value.value)
@property
- def unit_of_measurement(self) -> Optional[str]:
+ def unit_of_measurement(self) -> str | None:
"""Return unit of measurement the value is expressed in."""
if self.info.primary_value.metadata.unit is None:
return None
@@ -113,6 +155,20 @@ def unit_of_measurement(self) -> Optional[str]:
class ZWaveNumericSensor(ZwaveSensorBase):
"""Representation of a Z-Wave Numeric sensor."""
+ def __init__(
+ self,
+ config_entry: ConfigEntry,
+ client: ZwaveClient,
+ info: ZwaveDiscoveryInfo,
+ ) -> None:
+ """Initialize a ZWaveNumericSensor entity."""
+ super().__init__(config_entry, client, info)
+ if self.info.primary_value.command_class == CommandClass.BASIC:
+ self._name = self.generate_name(
+ include_value_name=True,
+ alternate_value_name=self.info.primary_value.command_class_name,
+ )
+
@property
def state(self) -> float:
"""Return state of the sensor."""
@@ -121,7 +177,7 @@ def state(self) -> float:
return round(float(self.info.primary_value.value), 2)
@property
- def unit_of_measurement(self) -> Optional[str]:
+ def unit_of_measurement(self) -> str | None:
"""Return unit of measurement the value is expressed in."""
if self.info.primary_value.metadata.unit is None:
return None
@@ -132,18 +188,85 @@ def unit_of_measurement(self) -> Optional[str]:
return str(self.info.primary_value.metadata.unit)
+
+class ZWaveListSensor(ZwaveSensorBase):
+ """Representation of a Z-Wave Numeric sensor with multiple states."""
+
+ def __init__(
+ self,
+ config_entry: ConfigEntry,
+ client: ZwaveClient,
+ info: ZwaveDiscoveryInfo,
+ ) -> None:
+ """Initialize a ZWaveListSensor entity."""
+ super().__init__(config_entry, client, info)
+ self._name = self.generate_name(
+ include_value_name=True,
+ alternate_value_name=self.info.primary_value.property_name,
+ additional_info=[self.info.primary_value.property_key_name],
+ )
+
@property
- def device_state_attributes(self) -> Optional[Dict[str, str]]:
- """Return the device specific state attributes."""
+ def state(self) -> str | None:
+ """Return state of the sensor."""
+ if self.info.primary_value.value is None:
+ return None
if (
- self.info.primary_value.value is None
- or not self.info.primary_value.metadata.states
+ str(self.info.primary_value.value)
+ not in self.info.primary_value.metadata.states
):
+ return str(self.info.primary_value.value)
+ return str(
+ self.info.primary_value.metadata.states[str(self.info.primary_value.value)]
+ )
+
+ @property
+ def extra_state_attributes(self) -> dict[str, str] | None:
+ """Return the device specific state attributes."""
+ # add the value's int value as property for multi-value (list) items
+ return {"value": self.info.primary_value.value}
+
+
+class ZWaveConfigParameterSensor(ZwaveSensorBase):
+ """Representation of a Z-Wave config parameter sensor."""
+
+ def __init__(
+ self,
+ config_entry: ConfigEntry,
+ client: ZwaveClient,
+ info: ZwaveDiscoveryInfo,
+ ) -> None:
+ """Initialize a ZWaveConfigParameterSensor entity."""
+ super().__init__(config_entry, client, info)
+ self._name = self.generate_name(
+ include_value_name=True,
+ alternate_value_name=self.info.primary_value.property_name,
+ additional_info=[self.info.primary_value.property_key_name],
+ name_suffix="Config Parameter",
+ )
+ self._primary_value = cast(ConfigurationValue, self.info.primary_value)
+
+ @property
+ def state(self) -> str | None:
+ """Return state of the sensor."""
+ if self.info.primary_value.value is None:
return None
- # add the value's label as property for multi-value (list) items
- label = self.info.primary_value.metadata.states.get(
- self.info.primary_value.value
- ) or self.info.primary_value.metadata.states.get(
- str(self.info.primary_value.value)
+ if (
+ self._primary_value.configuration_value_type == ConfigurationValueType.RANGE
+ or (
+ not str(self.info.primary_value.value)
+ in self.info.primary_value.metadata.states
+ )
+ ):
+ return str(self.info.primary_value.value)
+ return str(
+ self.info.primary_value.metadata.states[str(self.info.primary_value.value)]
)
- return {"label": label}
+
+ @property
+ def extra_state_attributes(self) -> dict[str, str] | None:
+ """Return the device specific state attributes."""
+ if self._primary_value.configuration_value_type == ConfigurationValueType.RANGE:
+ return None
+ # add the value's int value as property for multi-value (list) items
+ return {"value": self.info.primary_value.value}
diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py
new file mode 100644
index 00000000000000..513abd97318c1c
--- /dev/null
+++ b/homeassistant/components/zwave_js/services.py
@@ -0,0 +1,270 @@
+"""Methods and classes related to executing Z-Wave commands and publishing these to hass."""
+from __future__ import annotations
+
+import logging
+
+import voluptuous as vol
+from zwave_js_server.const import CommandStatus
+from zwave_js_server.exceptions import SetValueFailed
+from zwave_js_server.model.node import Node as ZwaveNode
+from zwave_js_server.model.value import get_value_id
+from zwave_js_server.util.node import (
+ async_bulk_set_partial_config_parameters,
+ async_set_config_parameter,
+)
+
+from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID
+from homeassistant.core import HomeAssistant, ServiceCall, callback
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.entity_registry import EntityRegistry
+
+from . import const
+from .helpers import async_get_node_from_device_id, async_get_node_from_entity_id
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def parameter_name_does_not_need_bitmask(
+ val: dict[str, int | str]
+) -> dict[str, int | str]:
+ """Validate that if a parameter name is provided, bitmask is not as well."""
+ if isinstance(val[const.ATTR_CONFIG_PARAMETER], str) and (
+ val.get(const.ATTR_CONFIG_PARAMETER_BITMASK)
+ ):
+ raise vol.Invalid(
+ "Don't include a bitmask when a parameter name is specified",
+ path=[const.ATTR_CONFIG_PARAMETER, const.ATTR_CONFIG_PARAMETER_BITMASK],
+ )
+ return val
+
+
+# Validates that a bitmask is provided in hex form and converts it to decimal
+# int equivalent since that's what the library uses
+BITMASK_SCHEMA = vol.All(
+ cv.string,
+ vol.Lower,
+ vol.Match(
+ r"^(0x)?[0-9a-f]+$",
+ msg="Must provide an integer (e.g. 255) or a bitmask in hex form (e.g. 0xff)",
+ ),
+ lambda value: int(value, 16),
+)
+
+
+class ZWaveServices:
+ """Class that holds our services (Zwave Commands) that should be published to hass."""
+
+ def __init__(self, hass: HomeAssistant, ent_reg: EntityRegistry):
+ """Initialize with hass object."""
+ self._hass = hass
+ self._ent_reg = ent_reg
+
+ @callback
+ def async_register(self) -> None:
+ """Register all our services."""
+ self._hass.services.async_register(
+ const.DOMAIN,
+ const.SERVICE_SET_CONFIG_PARAMETER,
+ self.async_set_config_parameter,
+ schema=vol.All(
+ {
+ vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
+ vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any(
+ vol.Coerce(int), cv.string
+ ),
+ vol.Optional(const.ATTR_CONFIG_PARAMETER_BITMASK): vol.Any(
+ vol.Coerce(int), BITMASK_SCHEMA
+ ),
+ vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(
+ vol.Coerce(int), cv.string
+ ),
+ },
+ cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID),
+ parameter_name_does_not_need_bitmask,
+ ),
+ )
+
+ self._hass.services.async_register(
+ const.DOMAIN,
+ const.SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
+ self.async_bulk_set_partial_config_parameters,
+ schema=vol.All(
+ {
+ vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
+ vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any(
+ vol.Coerce(int), cv.string
+ ),
+ vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(
+ vol.Coerce(int),
+ {
+ vol.Any(vol.Coerce(int), BITMASK_SCHEMA): vol.Any(
+ vol.Coerce(int), cv.string
+ )
+ },
+ ),
+ },
+ cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID),
+ ),
+ )
+
+ self._hass.services.async_register(
+ const.DOMAIN,
+ const.SERVICE_REFRESH_VALUE,
+ self.async_poll_value,
+ schema=vol.Schema(
+ {
+ vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
+ vol.Optional(const.ATTR_REFRESH_ALL_VALUES, default=False): bool,
+ }
+ ),
+ )
+
+ self._hass.services.async_register(
+ const.DOMAIN,
+ const.SERVICE_SET_VALUE,
+ self.async_set_value,
+ schema=vol.Schema(
+ {
+ vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
+ vol.Required(const.ATTR_COMMAND_CLASS): vol.Coerce(int),
+ vol.Required(const.ATTR_PROPERTY): vol.Any(vol.Coerce(int), str),
+ vol.Optional(const.ATTR_PROPERTY_KEY): vol.Any(
+ vol.Coerce(int), str
+ ),
+ vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int),
+ vol.Required(const.ATTR_VALUE): vol.Any(
+ bool, vol.Coerce(int), vol.Coerce(float), cv.string
+ ),
+ vol.Optional(const.ATTR_WAIT_FOR_RESULT): vol.Coerce(bool),
+ },
+ cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID),
+ ),
+ )
+
+ async def async_set_config_parameter(self, service: ServiceCall) -> None:
+ """Set a config value on a node."""
+ nodes: set[ZwaveNode] = set()
+ if ATTR_ENTITY_ID in service.data:
+ nodes |= {
+ async_get_node_from_entity_id(self._hass, entity_id)
+ for entity_id in service.data[ATTR_ENTITY_ID]
+ }
+ if ATTR_DEVICE_ID in service.data:
+ nodes |= {
+ async_get_node_from_device_id(self._hass, device_id)
+ for device_id in service.data[ATTR_DEVICE_ID]
+ }
+ property_or_property_name = service.data[const.ATTR_CONFIG_PARAMETER]
+ property_key = service.data.get(const.ATTR_CONFIG_PARAMETER_BITMASK)
+ new_value = service.data[const.ATTR_CONFIG_VALUE]
+
+ for node in nodes:
+ zwave_value, cmd_status = await async_set_config_parameter(
+ node,
+ new_value,
+ property_or_property_name,
+ property_key=property_key,
+ )
+
+ if cmd_status == CommandStatus.ACCEPTED:
+ msg = "Set configuration parameter %s on Node %s with value %s"
+ else:
+ msg = (
+ "Added command to queue to set configuration parameter %s on Node "
+ "%s with value %s. Parameter will be set when the device wakes up"
+ )
+
+ _LOGGER.info(msg, zwave_value, node, new_value)
+
+ async def async_bulk_set_partial_config_parameters(
+ self, service: ServiceCall
+ ) -> None:
+ """Bulk set multiple partial config values on a node."""
+ nodes: set[ZwaveNode] = set()
+ if ATTR_ENTITY_ID in service.data:
+ nodes |= {
+ async_get_node_from_entity_id(self._hass, entity_id)
+ for entity_id in service.data[ATTR_ENTITY_ID]
+ }
+ if ATTR_DEVICE_ID in service.data:
+ nodes |= {
+ async_get_node_from_device_id(self._hass, device_id)
+ for device_id in service.data[ATTR_DEVICE_ID]
+ }
+ property_ = service.data[const.ATTR_CONFIG_PARAMETER]
+ new_value = service.data[const.ATTR_CONFIG_VALUE]
+
+ for node in nodes:
+ cmd_status = await async_bulk_set_partial_config_parameters(
+ node,
+ property_,
+ new_value,
+ )
+
+ if cmd_status == CommandStatus.ACCEPTED:
+ msg = "Bulk set partials for configuration parameter %s on Node %s"
+ else:
+ msg = (
+ "Added command to queue to bulk set partials for configuration "
+ "parameter %s on Node %s"
+ )
+
+ _LOGGER.info(msg, property_, node)
+
+ async def async_poll_value(self, service: ServiceCall) -> None:
+ """Poll value on a node."""
+ for entity_id in service.data[ATTR_ENTITY_ID]:
+ entry = self._ent_reg.async_get(entity_id)
+ if entry is None or entry.platform != const.DOMAIN:
+ raise ValueError(
+ f"Entity {entity_id} is not a valid {const.DOMAIN} entity."
+ )
+ async_dispatcher_send(
+ self._hass,
+ f"{const.DOMAIN}_{entry.unique_id}_poll_value",
+ service.data[const.ATTR_REFRESH_ALL_VALUES],
+ )
+
+ async def async_set_value(self, service: ServiceCall) -> None:
+ """Set a value on a node."""
+ nodes: set[ZwaveNode] = set()
+ if ATTR_ENTITY_ID in service.data:
+ nodes |= {
+ async_get_node_from_entity_id(self._hass, entity_id)
+ for entity_id in service.data[ATTR_ENTITY_ID]
+ }
+ if ATTR_DEVICE_ID in service.data:
+ nodes |= {
+ async_get_node_from_device_id(self._hass, device_id)
+ for device_id in service.data[ATTR_DEVICE_ID]
+ }
+ command_class = service.data[const.ATTR_COMMAND_CLASS]
+ property_ = service.data[const.ATTR_PROPERTY]
+ property_key = service.data.get(const.ATTR_PROPERTY_KEY)
+ endpoint = service.data.get(const.ATTR_ENDPOINT)
+ new_value = service.data[const.ATTR_VALUE]
+ wait_for_result = service.data.get(const.ATTR_WAIT_FOR_RESULT)
+
+ for node in nodes:
+ success = await node.async_set_value(
+ get_value_id(
+ node,
+ command_class,
+ property_,
+ endpoint=endpoint,
+ property_key=property_key,
+ ),
+ new_value,
+ wait_for_result=wait_for_result,
+ )
+
+ if success is False:
+ raise SetValueFailed(
+ "Unable to set value, refer to "
+ "https://zwave-js.github.io/node-zwave-js/#/api/node?id=setvalue "
+ "for possible reasons"
+ )
diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml
index cc81da7ed58ff5..f9d90f94779290 100644
--- a/homeassistant/components/zwave_js/services.yaml
+++ b/homeassistant/components/zwave_js/services.yaml
@@ -1,24 +1,165 @@
# Describes the format for available Z-Wave services
clear_lock_usercode:
- description: Clear a usercode from lock.
+ name: Clear a usercode from a lock
+ description: Clear a usercode from a lock
+ target:
+ entity:
+ domain: lock
+ integration: zwave_js
fields:
- entity_id:
- description: Lock entity_id.
- example: lock.front_door_locked
code_slot:
- description: Code slot to clear code from.
+ name: Code slot
+ description: Code slot to clear code from
+ required: true
example: 1
+ selector:
+ text:
set_lock_usercode:
- description: Set a usercode to lock.
+ name: Set a usercode on a lock
+ description: Set a usercode on a lock
+ target:
+ entity:
+ domain: lock
+ integration: zwave_js
fields:
- entity_id:
- description: Lock entity_id.
- example: lock.front_door_locked
code_slot:
+ name: Code slot
description: Code slot to set the code.
+ required: true
example: 1
+ selector:
+ text:
usercode:
+ name: Code
description: Code to set.
+ required: true
example: 1234
+ selector:
+ text:
+
+set_config_parameter:
+ name: Set a Z-Wave device configuration parameter
+ description: Allow for changing configuration parameters of your Z-Wave devices.
+ target:
+ entity:
+ integration: zwave_js
+ fields:
+ parameter:
+ name: Parameter
+ description: The (name or id of the) configuration parameter you want to configure.
+ example: Minimum brightness level
+ required: true
+ selector:
+ text:
+ value:
+ name: Value
+ description: The new value to set for this configuration parameter.
+ example: 5
+ required: true
+ selector:
+ text:
+ bitmask:
+ name: Bitmask
+ description: Target a specific bitmask (see the documentation for more information).
+ advanced: true
+ selector:
+ text:
+
+bulk_set_partial_config_parameters:
+ name: Bulk set partial configuration parameters for a Z-Wave device (Advanced).
+ description: Allow for bulk setting partial parameters. Useful when multiple partial parameters have to be set at the same time.
+ target:
+ entity:
+ integration: zwave_js
+ fields:
+ parameter:
+ name: Parameter
+ description: The id of the configuration parameter you want to configure.
+ example: 9
+ required: true
+ selector:
+ text:
+ value:
+ name: Value
+ description: The new value(s) to set for this configuration parameter. Can either be a raw integer value to represent the bulk change or a mapping where the key is the bitmask (either in hex or integer form) and the value is the new value you want to set for that partial parameter.
+ example:
+ "0x1": 1
+ "0x10": 1
+ "0x20": 1
+ "0x40": 1
+ required: true
+ selector:
+ object:
+
+refresh_value:
+ name: Refresh value(s) of a Z-Wave entity
+ description: Force update value(s) for a Z-Wave entity
+ fields:
+ entity_id:
+ name: Entity
+ description: Entity whose value(s) should be refreshed
+ required: true
+ example: sensor.family_room_motion
+ selector:
+ entity:
+ integration: zwave_js
+ refresh_all_values:
+ name: Refresh all values?
+ description: Whether to refresh all values (true) or just the primary value (false)
+ required: false
+ example: true
+ default: false
+ selector:
+ boolean:
+
+set_value:
+ name: Set a value on a Z-Wave device (Advanced)
+ description: Allow for changing any value that Z-Wave JS recognizes on a Z-Wave device. This service has minimal validation so only use this service if you know what you are doing.
+ target:
+ entity:
+ integration: zwave_js
+ fields:
+ command_class:
+ name: Command Class
+ description: The ID of the command class for the value.
+ example: 117
+ required: true
+ selector:
+ text:
+ endpoint:
+ name: Endpoint
+ description: The endpoint for the value.
+ example: 1
+ required: false
+ selector:
+ text:
+ property:
+ name: Property
+ description: The ID of the property for the value.
+ example: currentValue
+ required: true
+ selector:
+ text:
+ property_key:
+ name: Property Key
+ description: The ID of the property key for the value
+ example: 1
+ required: false
+ selector:
+ text:
+ value:
+ name: Value
+ description: The new value to set.
+ example: "ffbb99"
+ required: true
+ selector:
+ object:
+ wait_for_result:
+ name: Wait for result?
+ description: Whether or not to wait for a response from the node. If not included in the payload, the integration will decide whether to wait or not. If set to `true`, note that the service call can take a while if setting a value on an asleep battery device.
+ example: false
+ required: false
+ selector:
+ boolean:
diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json
index 212bef708891ba..eb13ad512e3e3c 100644
--- a/homeassistant/components/zwave_js/strings.json
+++ b/homeassistant/components/zwave_js/strings.json
@@ -15,13 +15,14 @@
"install_addon": {
"title": "The Z-Wave JS add-on installation has started"
},
- "start_addon": {
+ "configure_addon": {
"title": "Enter the Z-Wave JS add-on configuration",
"data": {
"usb_path": "[%key:common::config_flow::data::usb_path%]",
"network_key": "Network Key"
}
},
+ "start_addon": { "title": "The Z-Wave JS add-on is starting." },
"hassio_confirm": {
"title": "Set up Z-Wave JS integration with the Z-Wave JS add-on"
}
@@ -38,12 +39,13 @@
"addon_info_failed": "Failed to get Z-Wave JS add-on info.",
"addon_install_failed": "Failed to install the Z-Wave JS add-on.",
"addon_set_config_failed": "Failed to set Z-Wave JS configuration.",
+ "addon_start_failed": "Failed to start the Z-Wave JS add-on.",
"addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.",
- "addon_missing_discovery_info": "Missing Z-Wave JS add-on discovery info.",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"progress": {
- "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes."
+ "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.",
+ "start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds."
}
}
}
diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py
index 2060894684c859..e64ea57703d81b 100644
--- a/homeassistant/components/zwave_js/switch.py
+++ b/homeassistant/components/zwave_js/switch.py
@@ -1,7 +1,8 @@
"""Representation of Z-Wave switches."""
+from __future__ import annotations
import logging
-from typing import Any, Callable, List
+from typing import Any, Callable
from zwave_js_server.client import Client as ZwaveClient
@@ -17,6 +18,10 @@
LOGGER = logging.getLogger(__name__)
+BARRIER_EVENT_SIGNALING_OFF = 0
+BARRIER_EVENT_SIGNALING_ON = 255
+
+
async def async_setup_entry(
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable
) -> None:
@@ -26,8 +31,13 @@ async def async_setup_entry(
@callback
def async_add_switch(info: ZwaveDiscoveryInfo) -> None:
"""Add Z-Wave Switch."""
- entities: List[ZWaveBaseEntity] = []
- entities.append(ZWaveSwitch(config_entry, client, info))
+ entities: list[ZWaveBaseEntity] = []
+ if info.platform_hint == "barrier_event_signaling_state":
+ entities.append(
+ ZWaveBarrierEventSignalingSwitch(config_entry, client, info)
+ )
+ else:
+ entities.append(ZWaveSwitch(config_entry, client, info))
async_add_entities(entities)
@@ -44,8 +54,11 @@ class ZWaveSwitch(ZWaveBaseEntity, SwitchEntity):
"""Representation of a Z-Wave switch."""
@property
- def is_on(self) -> bool:
+ def is_on(self) -> bool | None: # type: ignore
"""Return a boolean for the state of the switch."""
+ if self.info.primary_value.value is None:
+ # guard missing value
+ return None
return bool(self.info.primary_value.value)
async def async_turn_on(self, **kwargs: Any) -> None:
@@ -59,3 +72,59 @@ async def async_turn_off(self, **kwargs: Any) -> None:
target_value = self.get_zwave_value("targetValue")
if target_value is not None:
await self.info.node.async_set_value(target_value, False)
+
+
+class ZWaveBarrierEventSignalingSwitch(ZWaveBaseEntity, SwitchEntity):
+ """This switch is used to turn on or off a barrier device's event signaling subsystem."""
+
+ def __init__(
+ self,
+ config_entry: ConfigEntry,
+ client: ZwaveClient,
+ info: ZwaveDiscoveryInfo,
+ ) -> None:
+ """Initialize a ZWaveBarrierEventSignalingSwitch entity."""
+ super().__init__(config_entry, client, info)
+ self._name = self.generate_name(include_value_name=True)
+ self._state: bool | None = None
+
+ self._update_state()
+
+ @callback
+ def on_value_update(self) -> None:
+ """Call when a watched value is added or updated."""
+ self._update_state()
+
+ @property
+ def name(self) -> str:
+ """Return default name from device name and value name combination."""
+ return self._name
+
+ @property
+ def is_on(self) -> bool | None: # type: ignore
+ """Return a boolean for the state of the switch."""
+ return self._state
+
+ async def async_turn_on(self, **kwargs: Any) -> None:
+ """Turn the switch on."""
+ await self.info.node.async_set_value(
+ self.info.primary_value, BARRIER_EVENT_SIGNALING_ON
+ )
+ # this value is not refreshed, so assume success
+ self._state = True
+ self.async_write_ha_state()
+
+ async def async_turn_off(self, **kwargs: Any) -> None:
+ """Turn the switch off."""
+ await self.info.node.async_set_value(
+ self.info.primary_value, BARRIER_EVENT_SIGNALING_OFF
+ )
+ # this value is not refreshed, so assume success
+ self._state = False
+ self.async_write_ha_state()
+
+ @callback
+ def _update_state(self) -> None:
+ self._state = None
+ if self.info.primary_value.value is not None:
+ self._state = self.info.primary_value.value == BARRIER_EVENT_SIGNALING_ON
diff --git a/homeassistant/components/zwave_js/translations/bg.json b/homeassistant/components/zwave_js/translations/bg.json
new file mode 100644
index 00000000000000..abf89f00513fb6
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/bg.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430"
+ },
+ "step": {
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "user": {
+ "data": {
+ "url": "URL"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json
new file mode 100644
index 00000000000000..731c0bbcea8535
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/ca.json
@@ -0,0 +1,61 @@
+{
+ "config": {
+ "abort": {
+ "addon_get_discovery_info_failed": "No s'ha pogut obtenir la informaci\u00f3 de descobriment del complement Z-Wave JS.",
+ "addon_info_failed": "No s'ha pogut obtenir la informaci\u00f3 del complement Z-Wave JS.",
+ "addon_install_failed": "No s'ha pogut instal\u00b7lar el complement Z-Wave JS.",
+ "addon_missing_discovery_info": "Falta la informaci\u00f3 de descobriment del complement Z-Wave JS.",
+ "addon_set_config_failed": "No s'ha pogut establir la configuraci\u00f3 de Z-Wave JS.",
+ "addon_start_failed": "No s'ha pogut iniciar el complement Z-Wave JS.",
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
+ "cannot_connect": "Ha fallat la connexi\u00f3"
+ },
+ "error": {
+ "addon_start_failed": "No s'ha pogut iniciar el complement Z-Wave JS. Comprova la configuraci\u00f3.",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_ws_url": "URL del websocket inv\u00e0lid",
+ "unknown": "Error inesperat"
+ },
+ "progress": {
+ "install_addon": "Espera mentre finalitza la instal\u00b7laci\u00f3 del complement Z-Wave JS. Pot tardar uns quants minuts.",
+ "start_addon": "Espera mentre es completa la inicialitzaci\u00f3 del complement Z-Wave JS. Pot tardar uns segons."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "network_key": "Clau de xarxa",
+ "usb_path": "Ruta del port USB del dispositiu"
+ },
+ "title": "Introdueix la configuraci\u00f3 del complement Z-Wave JS"
+ },
+ "hassio_confirm": {
+ "title": "Configura la integraci\u00f3 Z-Wave JS mitjan\u00e7ant el complement Z-Wave JS"
+ },
+ "install_addon": {
+ "title": "Ha comen\u00e7at la instal\u00b7laci\u00f3 del complement Z-Wave JS"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Utilitza el complement Z-Wave JS Supervisor"
+ },
+ "description": "Vols utilitzar el complement Supervisor de Z-Wave JS?",
+ "title": "Selecciona el m\u00e8tode de connexi\u00f3"
+ },
+ "start_addon": {
+ "title": "El complement Z-Wave JS s'est\u00e0 iniciant."
+ },
+ "user": {
+ "data": {
+ "url": "URL"
+ }
+ }
+ }
+ },
+ "title": "Z-Wave JS"
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/cs.json b/homeassistant/components/zwave_js/translations/cs.json
new file mode 100644
index 00000000000000..57e7a6b74db5dc
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/cs.json
@@ -0,0 +1,30 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno",
+ "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1",
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit"
+ },
+ "error": {
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
+ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "usb_path": "Cesta k USB za\u0159\u00edzen\u00ed"
+ }
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "user": {
+ "data": {
+ "url": "URL"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json
new file mode 100644
index 00000000000000..ae9293cf9261a8
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/de.json
@@ -0,0 +1,59 @@
+{
+ "config": {
+ "abort": {
+ "addon_info_failed": "Fehler beim Abrufen von Z-Wave JS Add-on Informationen.",
+ "addon_install_failed": "Installation des Z-Wave JS Add-Ons fehlgeschlagen.",
+ "addon_set_config_failed": "Setzen der Z-Wave JS Konfiguration fehlgeschlagen",
+ "addon_start_failed": "Starten des Z-Wave JS Add-ons fehlgeschlagen.",
+ "already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
+ "cannot_connect": "Verbindung fehlgeschlagen"
+ },
+ "error": {
+ "addon_start_failed": "Fehler beim Starten des Z-Wave JS Add-Ons. \u00dcberpr\u00fcfe die Konfiguration.",
+ "cannot_connect": "Verbindung fehlgeschlagen",
+ "invalid_ws_url": "Ung\u00fcltige Websocket-URL",
+ "unknown": "Unerwarteter Fehler"
+ },
+ "progress": {
+ "install_addon": "Bitte warte, w\u00e4hrend die Installation des Z-Wave JS Add-ons abgeschlossen wird. Dies kann einige Minuten dauern.",
+ "start_addon": "Bitte warte, w\u00e4hrend der Start des Z-Wave JS Add-ons abgeschlossen wird. Dies kann einige Sekunden dauern."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "network_key": "Netzwerk-Schl\u00fcssel",
+ "usb_path": "USB-Ger\u00e4te-Pfad"
+ },
+ "title": "Gib die Konfiguration des Z-Wave JS Add-ons ein"
+ },
+ "hassio_confirm": {
+ "title": "Einrichten der Z-Wave JS Integration mit dem Z-Wave JS Add-on"
+ },
+ "install_addon": {
+ "title": "Die Installation des Z-Wave-JS-Add-ons hat begonnen"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Verwende das Z-Wave JS Supervisor Add-on"
+ },
+ "description": "M\u00f6chtest du das Z-Wave JS Supervisor Add-on verwenden?",
+ "title": "Verbindungstyp ausw\u00e4hlen"
+ },
+ "start_addon": {
+ "title": "Z-Wave JS Add-on wird gestartet."
+ },
+ "user": {
+ "data": {
+ "url": "URL"
+ }
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json
index 4aa510df6bea12..5be980d52cb07a 100644
--- a/homeassistant/components/zwave_js/translations/en.json
+++ b/homeassistant/components/zwave_js/translations/en.json
@@ -6,6 +6,7 @@
"addon_install_failed": "Failed to install the Z-Wave JS add-on.",
"addon_missing_discovery_info": "Missing Z-Wave JS add-on discovery info.",
"addon_set_config_failed": "Failed to set Z-Wave JS configuration.",
+ "addon_start_failed": "Failed to start the Z-Wave JS add-on.",
"already_configured": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress",
"cannot_connect": "Failed to connect"
@@ -17,9 +18,17 @@
"unknown": "Unexpected error"
},
"progress": {
- "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes."
+ "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.",
+ "start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds."
},
"step": {
+ "configure_addon": {
+ "data": {
+ "network_key": "Network Key",
+ "usb_path": "USB Device Path"
+ },
+ "title": "Enter the Z-Wave JS add-on configuration"
+ },
"hassio_confirm": {
"title": "Set up Z-Wave JS integration with the Z-Wave JS add-on"
},
@@ -39,11 +48,12 @@
"title": "Select connection method"
},
"start_addon": {
+ "title": "The Z-Wave JS add-on is starting."
+ },
+ "user": {
"data": {
- "network_key": "Network Key",
- "usb_path": "USB Device Path"
- },
- "title": "Enter the Z-Wave JS add-on configuration"
+ "url": "URL"
+ }
}
}
},
diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json
new file mode 100644
index 00000000000000..26fd155a0ad2ae
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/es.json
@@ -0,0 +1,61 @@
+{
+ "config": {
+ "abort": {
+ "addon_get_discovery_info_failed": "Fallo en la obtenci\u00f3n de la informaci\u00f3n de descubrimiento del complemento Z-Wave JS.",
+ "addon_info_failed": "No se pudo obtener la informaci\u00f3n del complemento Z-Wave JS.",
+ "addon_install_failed": "No se ha podido instalar el complemento Z-Wave JS.",
+ "addon_missing_discovery_info": "Falta informaci\u00f3n de descubrimiento del complemento Z-Wave JS.",
+ "addon_set_config_failed": "Fallo en la configuraci\u00f3n de Z-Wave JS.",
+ "addon_start_failed": "No se ha podido iniciar el complemento Z-Wave JS.",
+ "already_configured": "El dispositivo ya est\u00e1 configurado",
+ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso",
+ "cannot_connect": "No se pudo conectar"
+ },
+ "error": {
+ "addon_start_failed": "No se pudo iniciar el complemento Z-Wave JS. Comprueba la configuraci\u00f3n.",
+ "cannot_connect": "No se pudo conectar",
+ "invalid_ws_url": "URL de websocket no v\u00e1lida",
+ "unknown": "Error inesperado"
+ },
+ "progress": {
+ "install_addon": "Espera mientras termina la instalaci\u00f3n del complemento Z-Wave JS. Puede tardar varios minutos.",
+ "start_addon": "Espere mientras se completa el inicio del complemento Z-Wave JS. Esto puede tardar unos segundos."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "network_key": "Clave de red",
+ "usb_path": "Ruta del dispositivo USB"
+ },
+ "title": "Introduzca la configuraci\u00f3n del complemento Z-Wave JS"
+ },
+ "hassio_confirm": {
+ "title": "Configurar la integraci\u00f3n de Z-Wave JS con el complemento Z-Wave JS"
+ },
+ "install_addon": {
+ "title": "La instalaci\u00f3n del complemento Z-Wave JS ha comenzado"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Usar el complemento Z-Wave JS Supervisor"
+ },
+ "description": "\u00bfQuieres utilizar el complemento Z-Wave JS Supervisor?",
+ "title": "Selecciona el m\u00e9todo de conexi\u00f3n"
+ },
+ "start_addon": {
+ "title": "Se est\u00e1 iniciando el complemento Z-Wave JS."
+ },
+ "user": {
+ "data": {
+ "url": "URL"
+ }
+ }
+ }
+ },
+ "title": "Z-Wave JS"
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/et.json b/homeassistant/components/zwave_js/translations/et.json
new file mode 100644
index 00000000000000..4c68e63530ff82
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/et.json
@@ -0,0 +1,61 @@
+{
+ "config": {
+ "abort": {
+ "addon_get_discovery_info_failed": "Z-Wave JS lisandmooduli tuvastusteabe hankimine nurjus.",
+ "addon_info_failed": "Z-Wave JS lisandmooduli teabe hankimine nurjus.",
+ "addon_install_failed": "Z-Wave JS lisandmooduli paigaldamine nurjus.",
+ "addon_missing_discovery_info": "Z-Wave JS lisandmooduli tuvastusteave puudub.",
+ "addon_set_config_failed": "Z-Wave JS konfiguratsiooni m\u00e4\u00e4ramine nurjus.",
+ "addon_start_failed": "Z-Wave JS-i lisandmooduli k\u00e4ivitamine nurjus.",
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "already_in_progress": "Seadistamine on juba k\u00e4imas",
+ "cannot_connect": "\u00dchendamine nurjus"
+ },
+ "error": {
+ "addon_start_failed": "Z-Wave JS lisandmooduli k\u00e4ivitamine nurjus. Kontrolli seadistusi.",
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_ws_url": "Vale sihtkoha aadress",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "progress": {
+ "install_addon": "Palun oota kuni Z-Wave JS lisandmoodul on paigaldatud. See v\u00f5ib v\u00f5tta mitu minutit.",
+ "start_addon": "Palun oota kuni Z-Wave JS lisandmooduli ak\u00e4ivitumine l\u00f5ppeb. See v\u00f5ib v\u00f5tta m\u00f5ned sekundid."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "network_key": "V\u00f5rgu v\u00f5ti",
+ "usb_path": "USB-seadme asukoha rada"
+ },
+ "title": "Sisesta Z-Wave JS lisandmooduli seaded"
+ },
+ "hassio_confirm": {
+ "title": "Seadista Z-Wave JS-i sidumine Z-Wave JS-i lisandmooduliga"
+ },
+ "install_addon": {
+ "title": "Z-Wave JS lisandmooduli paigaldamine on alanud"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Kasuta lisandmoodulit Z-Wave JS Supervisor"
+ },
+ "description": "Kas soovid kasutada Z-Wave JSi halduri lisandmoodulit?",
+ "title": "Vali \u00fchendusviis"
+ },
+ "start_addon": {
+ "title": "Z-Wave JS lisandmoodul k\u00e4ivitub."
+ },
+ "user": {
+ "data": {
+ "url": ""
+ }
+ }
+ }
+ },
+ "title": "Z-Wave JS"
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json
new file mode 100644
index 00000000000000..ce9f2f8b501dd8
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/fr.json
@@ -0,0 +1,61 @@
+{
+ "config": {
+ "abort": {
+ "addon_get_discovery_info_failed": "Impossible d'obtenir les informations de d\u00e9couverte du module compl\u00e9mentaire Z-Wave JS.",
+ "addon_info_failed": "Impossible d'obtenir les informations sur le module compl\u00e9mentaire Z-Wave JS.",
+ "addon_install_failed": "\u00c9chec de l'installation du module compl\u00e9mentaire Z-Wave JS.",
+ "addon_missing_discovery_info": "Informations manquantes sur la d\u00e9couverte du module compl\u00e9mentaire Z-Wave JS.",
+ "addon_set_config_failed": "\u00c9chec de la d\u00e9finition de la configuration Z-Wave JS.",
+ "addon_start_failed": "\u00c9chec du d\u00e9marrage du module compl\u00e9mentaire Z-Wave JS.",
+ "already_configured": "Le p\u00e9riph\u00e9rique est d\u00e9j\u00e0 configur\u00e9",
+ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours",
+ "cannot_connect": "\u00c9chec de la connexion "
+ },
+ "error": {
+ "addon_start_failed": "\u00c9chec du d\u00e9marrage du module compl\u00e9mentaire Z-Wave JS. V\u00e9rifiez la configuration.",
+ "cannot_connect": "Erreur de connection",
+ "invalid_ws_url": "URL websocket invalide",
+ "unknown": "Erreur inattendue"
+ },
+ "progress": {
+ "install_addon": "Veuillez patienter pendant l'installation du module compl\u00e9mentaire Z-Wave JS. Cela peut prendre plusieurs minutes.",
+ "start_addon": "Veuillez patienter pendant le d\u00e9marrage du module compl\u00e9mentaire Z-Wave JS. Cela peut prendre quelques secondes."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "network_key": "Cl\u00e9 r\u00e9seau",
+ "usb_path": "Chemin du p\u00e9riph\u00e9rique USB"
+ },
+ "title": "Entrez la configuration du module compl\u00e9mentaire Z-Wave JS"
+ },
+ "hassio_confirm": {
+ "title": "Configurer l'int\u00e9gration Z-Wave JS avec le module compl\u00e9mentaire Z-Wave JS"
+ },
+ "install_addon": {
+ "title": "L'installation du module compl\u00e9mentaire Z-Wave JS a d\u00e9marr\u00e9"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Utiliser le module compl\u00e9mentaire Z-Wave JS du Supervisor"
+ },
+ "description": "Voulez-vous utiliser le module compl\u00e9mentaire Z-Wave JS du Supervisor?",
+ "title": "S\u00e9lectionner la m\u00e9thode de connexion"
+ },
+ "start_addon": {
+ "title": "Le module compl\u00e9mentaire Z-Wave JS est d\u00e9marr\u00e9."
+ },
+ "user": {
+ "data": {
+ "url": "URL"
+ }
+ }
+ }
+ },
+ "title": "Z-Wave JS"
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json
new file mode 100644
index 00000000000000..6732251f3a0a10
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/hu.json
@@ -0,0 +1,49 @@
+{
+ "config": {
+ "abort": {
+ "addon_start_failed": "Nem siker\u00fclt elind\u00edtani a Z-Wave JS b\u0151v\u00edtm\u00e9nyt.",
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_ws_url": "\u00c9rv\u00e9nytelen websocket URL",
+ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
+ },
+ "progress": {
+ "start_addon": "V\u00e1rj am\u00edg a Z-Wave JS b\u0151v\u00edtm\u00e9ny elindul. Ez eltarthat n\u00e9h\u00e1ny m\u00e1sodpercig."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat"
+ }
+ },
+ "install_addon": {
+ "title": "Elkezd\u0151d\u00f6tt a Z-Wave JS b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Haszn\u00e1ld a Z-Wave JS Supervisor b\u0151v\u00edtm\u00e9nyt"
+ },
+ "description": "Szeretn\u00e9d haszn\u00e1lni az Z-Wave JS Supervisor b\u0151v\u00edtm\u00e9nyt?",
+ "title": "V\u00e1laszd ki a csatlakoz\u00e1si m\u00f3dot"
+ },
+ "start_addon": {
+ "title": "Indul a Z-Wave JS b\u0151v\u00edtm\u00e9ny."
+ },
+ "user": {
+ "data": {
+ "url": "URL"
+ }
+ }
+ }
+ },
+ "title": "Z-Wave JS"
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/id.json b/homeassistant/components/zwave_js/translations/id.json
new file mode 100644
index 00000000000000..e8ea9381544c78
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/id.json
@@ -0,0 +1,61 @@
+{
+ "config": {
+ "abort": {
+ "addon_get_discovery_info_failed": "Gagal mendapatkan info penemuan add-on Z-Wave JS.",
+ "addon_info_failed": "Gagal mendapatkan info add-on Z-Wave JS.",
+ "addon_install_failed": "Gagal menginstal add-on Z-Wave JS.",
+ "addon_missing_discovery_info": "Info penemuan add-on Z-Wave JS tidak ada.",
+ "addon_set_config_failed": "Gagal menyetel konfigurasi Z-Wave JS.",
+ "addon_start_failed": "Gagal memulai add-on Z-Wave JS.",
+ "already_configured": "Perangkat sudah dikonfigurasi",
+ "already_in_progress": "Alur konfigurasi sedang berlangsung",
+ "cannot_connect": "Gagal terhubung"
+ },
+ "error": {
+ "addon_start_failed": "Gagal memulai add-on Z-Wave JS. Periksa konfigurasi.",
+ "cannot_connect": "Gagal terhubung",
+ "invalid_ws_url": "URL websocket tidak valid",
+ "unknown": "Kesalahan yang tidak diharapkan"
+ },
+ "progress": {
+ "install_addon": "Harap tunggu hingga penginstalan add-on Z-Wave JS selesai. Ini bisa memakan waktu beberapa saat.",
+ "start_addon": "Harap tunggu hingga add-on Z-Wave JS selesai. Ini mungkin perlu waktu beberapa saat."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "network_key": "Kunci Jaringan",
+ "usb_path": "Jalur Perangkat USB"
+ },
+ "title": "Masukkan konfigurasi add-on Z-Wave JS"
+ },
+ "hassio_confirm": {
+ "title": "Siapkan integrasi Z-Wave JS dengan add-on Z-Wave JS"
+ },
+ "install_addon": {
+ "title": "Instalasi add-on Z-Wave JS telah dimulai"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Gunakan add-on Supervisor Z-Wave JS"
+ },
+ "description": "Ingin menggunakan add-on Supervisor Z-Wave JS?",
+ "title": "Pilih metode koneksi"
+ },
+ "start_addon": {
+ "title": "Add-on Z-Wave JS sedang dimulai."
+ },
+ "user": {
+ "data": {
+ "url": "URL"
+ }
+ }
+ }
+ },
+ "title": "Z-Wave JS"
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json
new file mode 100644
index 00000000000000..abe0ab066fb111
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/it.json
@@ -0,0 +1,61 @@
+{
+ "config": {
+ "abort": {
+ "addon_get_discovery_info_failed": "Impossibile ottenere le informazioni sul rilevamento del componente aggiuntivo Z-Wave JS.",
+ "addon_info_failed": "Impossibile ottenere le informazioni sul componente aggiuntivo Z-Wave JS.",
+ "addon_install_failed": "Impossibile installare il componente aggiuntivo Z-Wave JS.",
+ "addon_missing_discovery_info": "Informazioni sul rilevamento del componente aggiuntivo Z-Wave JS mancanti.",
+ "addon_set_config_failed": "Impossibile impostare la configurazione di Z-Wave JS.",
+ "addon_start_failed": "Impossibile avviare il componente aggiuntivo Z-Wave JS.",
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
+ "cannot_connect": "Impossibile connettersi"
+ },
+ "error": {
+ "addon_start_failed": "Impossibile avviare il componente aggiuntivo Z-Wave JS. Controlla la configurazione.",
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_ws_url": "URL websocket non valido",
+ "unknown": "Errore imprevisto"
+ },
+ "progress": {
+ "install_addon": "Attendi il termine dell'installazione del componente aggiuntivo Z-Wave JS. Questa operazione pu\u00f2 richiedere diversi minuti.",
+ "start_addon": "Attendi il completamento dell'avvio del componente aggiuntivo Z-Wave JS. L'operazione potrebbe richiedere alcuni secondi."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "network_key": "Chiave di rete",
+ "usb_path": "Percorso del dispositivo USB"
+ },
+ "title": "Accedi alla configurazione del componente aggiuntivo Z-Wave JS"
+ },
+ "hassio_confirm": {
+ "title": "Configura l'integrazione di Z-Wave JS con il componente aggiuntivo Z-Wave JS"
+ },
+ "install_addon": {
+ "title": "L'installazione del componente aggiuntivo Z-Wave JS \u00e8 iniziata"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Usa il componente aggiuntivo Z-Wave JS Supervisor"
+ },
+ "description": "Desideri utilizzare il componente aggiuntivo Z-Wave JS Supervisor?",
+ "title": "Seleziona il metodo di connessione"
+ },
+ "start_addon": {
+ "title": "Il componente aggiuntivo Z-Wave JS si sta avviando."
+ },
+ "user": {
+ "data": {
+ "url": "URL"
+ }
+ }
+ }
+ },
+ "title": "Z-Wave JS"
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/ko.json b/homeassistant/components/zwave_js/translations/ko.json
new file mode 100644
index 00000000000000..22149af7496bcc
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/ko.json
@@ -0,0 +1,61 @@
+{
+ "config": {
+ "abort": {
+ "addon_get_discovery_info_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc758 \uac80\uc0c9 \uc815\ubcf4\ub97c \uac00\uc838\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.",
+ "addon_info_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc758 \uc815\ubcf4\ub97c \uac00\uc838\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.",
+ "addon_install_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc744 \uc124\uce58\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.",
+ "addon_missing_discovery_info": "Z-Wave JS \uc560\ub4dc\uc628\uc758 \uac80\uc0c9 \uc815\ubcf4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.",
+ "addon_set_config_failed": "Z-Wave JS \uad6c\uc131\uc744 \uc124\uc815\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.",
+ "addon_start_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc744 \uc2dc\uc791\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.",
+ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "addon_start_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc744 \uc2dc\uc791\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uad6c\uc131 \ub0b4\uc6a9\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
+ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
+ "invalid_ws_url": "\uc6f9 \uc18c\ucf13 URL \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "progress": {
+ "install_addon": "Z-Wave JS \uc560\ub4dc\uc628\uc758 \uc124\uce58\uac00 \uc644\ub8cc\ub418\ub294 \ub3d9\uc548 \uc7a0\uc2dc \uae30\ub2e4\ub824\uc8fc\uc138\uc694. \uba87 \ubd84 \uc815\ub3c4 \uac78\ub9b4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "start_addon": "Z-Wave JS \uc560\ub4dc\uc628 \uc2dc\uc791\uc774 \uc644\ub8cc\ub418\ub294 \ub3d9\uc548 \uc7a0\uc2dc \uae30\ub2e4\ub824\uc8fc\uc138\uc694. \uba87 \ucd08 \uc815\ub3c4 \uac78\ub9b4 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4",
+ "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c"
+ },
+ "title": "Z-Wave JS \uc560\ub4dc\uc628\uc758 \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694"
+ },
+ "hassio_confirm": {
+ "title": "Z-Wave JS \uc560\ub4dc\uc628\uc73c\ub85c Z-Wave JS \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc124\uc815\ud558\uae30"
+ },
+ "install_addon": {
+ "title": "Z-Wave JS \uc560\ub4dc\uc628 \uc124\uce58\uac00 \uc2dc\uc791\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "manual": {
+ "data": {
+ "url": "URL \uc8fc\uc18c"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Z-Wave JS Supervisor \uc560\ub4dc\uc628\uc744 \uc0ac\uc6a9\ud558\uae30"
+ },
+ "description": "Z-Wave JS Supervisor \uc560\ub4dc\uc628\uc744 \uc0ac\uc6a9\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "\uc5f0\uacb0 \ubc29\ubc95 \uc120\ud0dd\ud558\uae30"
+ },
+ "start_addon": {
+ "title": "Z-Wave JS \uc560\ub4dc\uc628\uc774 \uc2dc\uc791\ud558\ub294 \uc911\uc785\ub2c8\ub2e4."
+ },
+ "user": {
+ "data": {
+ "url": "URL \uc8fc\uc18c"
+ }
+ }
+ }
+ },
+ "title": "Z-Wave JS"
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/lb.json b/homeassistant/components/zwave_js/translations/lb.json
new file mode 100644
index 00000000000000..302addbd7cf958
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/lb.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparat ass scho konfigur\u00e9iert"
+ },
+ "error": {
+ "cannot_connect": "Feeler beim verbannen",
+ "invalid_ws_url": "Ong\u00eblteg Websocket URL",
+ "unknown": "Onerwaarte Feeler"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "url": "URL"
+ }
+ }
+ }
+ },
+ "title": "Z-Wave JS"
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json
new file mode 100644
index 00000000000000..f50c9c8cebae46
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/nl.json
@@ -0,0 +1,61 @@
+{
+ "config": {
+ "abort": {
+ "addon_get_discovery_info_failed": "Ophalen van ontdekkingsinformatie voor Z-Wave JS-add-on is mislukt.",
+ "addon_info_failed": "Ophalen van Z-Wave JS add-on-info is mislukt.",
+ "addon_install_failed": "Kan de Z-Wave JS add-on niet installeren.",
+ "addon_missing_discovery_info": "De Z-Wave JS addon mist ontdekkings informatie",
+ "addon_set_config_failed": "Instellen van de Z-Wave JS configuratie is mislukt.",
+ "addon_start_failed": "Kan de Z-Wave JS add-on niet starten.",
+ "already_configured": "Apparaat is al geconfigureerd",
+ "already_in_progress": "De configuratiestroom is al aan de gang",
+ "cannot_connect": "Kan geen verbinding maken"
+ },
+ "error": {
+ "addon_start_failed": "Het is niet gelukt om de Z-Wave JS add-on te starten. Controleer de configuratie.",
+ "cannot_connect": "Kan geen verbinding maken",
+ "invalid_ws_url": "Ongeldige websocket URL",
+ "unknown": "Onverwachte fout"
+ },
+ "progress": {
+ "install_addon": "Een ogenblik geduld terwijl de installatie van de Z-Wave JS add-on is voltooid. Dit kan enkele minuten duren.",
+ "start_addon": "Wacht alstublieft terwijl de Z-Wave JS add-on start voltooid is. Dit kan enkele seconden duren."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "network_key": "Netwerksleutel",
+ "usb_path": "USB-apparaatpad"
+ },
+ "title": "Voer de Z-Wave JS add-on configuratie in"
+ },
+ "hassio_confirm": {
+ "title": "Z-Wave JS integratie instellen met de Z-Wave JS add-on"
+ },
+ "install_addon": {
+ "title": "De Z-Wave JS add-on installatie is gestart"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Gebruik de Z-Wave JS Supervisor add-on"
+ },
+ "description": "Wilt u de Z-Wave JS Supervisor add-on gebruiken?",
+ "title": "Selecteer verbindingsmethode"
+ },
+ "start_addon": {
+ "title": "De add-on Z-Wave JS wordt gestart."
+ },
+ "user": {
+ "data": {
+ "url": "URL"
+ }
+ }
+ }
+ },
+ "title": "Z-Wave JS"
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json
new file mode 100644
index 00000000000000..f893d2d7684c35
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/no.json
@@ -0,0 +1,61 @@
+{
+ "config": {
+ "abort": {
+ "addon_get_discovery_info_failed": "Kunne ikke hente oppdagelsesinformasjon om Z-Wave JS-tillegg",
+ "addon_info_failed": "Kunne ikke hente informasjon om Z-Wave JS-tillegg",
+ "addon_install_failed": "Kunne ikke installere Z-Wave JS-tillegg",
+ "addon_missing_discovery_info": "Manglende oppdagelsesinformasjon for Z-Wave JS-tillegg",
+ "addon_set_config_failed": "Kunne ikke angi Z-Wave JS-konfigurasjon",
+ "addon_start_failed": "Kunne ikke starte Z-Wave JS-tillegget.",
+ "already_configured": "Enheten er allerede konfigurert",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
+ "cannot_connect": "Tilkobling mislyktes"
+ },
+ "error": {
+ "addon_start_failed": "Kunne ikke starte Z-Wave JS-tillegg. Sjekk konfigurasjonen.",
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_ws_url": "Ugyldig websocket URL",
+ "unknown": "Uventet feil"
+ },
+ "progress": {
+ "install_addon": "Vent mens installasjonen av Z-Wave JS-tillegg er ferdig. Dette kan ta flere minutter.",
+ "start_addon": "Vent mens Z-Wave JS-tillegget er ferdig startet. Dette kan ta noen sekunder."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "network_key": "Nettverksn\u00f8kkel",
+ "usb_path": "USB enhetsbane"
+ },
+ "title": "Angi konfigurasjon for Z-Wave JS-tillegg"
+ },
+ "hassio_confirm": {
+ "title": "Sett opp Z-Wave JS-integrasjon med Z-Wave JS-tillegg"
+ },
+ "install_addon": {
+ "title": "Installasjon av Z-Wave JS-tillegg har startet"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Bruk Z-Wave JS Supervisor-tillegg"
+ },
+ "description": "Vil du bruke Z-Wave JS Supervisor-tillegg?",
+ "title": "Velg tilkoblingsmetode"
+ },
+ "start_addon": {
+ "title": "Z-Wave JS-tillegget starter"
+ },
+ "user": {
+ "data": {
+ "url": "URL"
+ }
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json
new file mode 100644
index 00000000000000..2bfd994132b7ab
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/pl.json
@@ -0,0 +1,61 @@
+{
+ "config": {
+ "abort": {
+ "addon_get_discovery_info_failed": "Nie uda\u0142o si\u0119 uzyska\u0107 informacji wykrywania dodatku Z-Wave JS",
+ "addon_info_failed": "Nie uda\u0142o si\u0119 uzyska\u0107 informacji o dodatku Z-Wave JS",
+ "addon_install_failed": "Nie uda\u0142o si\u0119 zainstalowa\u0107 dodatku Z-Wave JS",
+ "addon_missing_discovery_info": "Brak informacji wykrywania dodatku Z-Wave JS",
+ "addon_set_config_failed": "Nie uda\u0142o si\u0119 skonfigurowa\u0107 Z-Wave JS",
+ "addon_start_failed": "Nie uda\u0142o si\u0119 uruchomi\u0107 dodatku Z-Wave JS.",
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "already_in_progress": "Konfiguracja jest ju\u017c w toku",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ },
+ "error": {
+ "addon_start_failed": "Nie uda\u0142o si\u0119 uruchomi\u0107 dodatku Z-Wave JS. Sprawd\u017a konfiguracj\u0119",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_ws_url": "Nieprawid\u0142owy URL websocket",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "progress": {
+ "install_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 instalacja dodatku Z-Wave JS. Mo\u017ce to zaj\u0105\u0107 kilka minut.",
+ "start_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 uruchamianie dodatku Z-Wave JS. Mo\u017ce to zaj\u0105\u0107 chwil\u0119."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "network_key": "Klucz sieci",
+ "usb_path": "\u015acie\u017cka urz\u0105dzenia USB"
+ },
+ "title": "Wprowad\u017a konfiguracj\u0119 dodatku Z-Wave JS"
+ },
+ "hassio_confirm": {
+ "title": "Skonfiguruj integracj\u0119 Z-Wave JS z dodatkiem Z-Wave JS"
+ },
+ "install_addon": {
+ "title": "Rozpocz\u0119\u0142a si\u0119 instalacja dodatku Z-Wave JS"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "U\u017cyj dodatku Z-Wave JS Supervisor"
+ },
+ "description": "Czy chcesz skorzysta\u0107 z dodatku Z-Wave JS Supervisor?",
+ "title": "Wybierz metod\u0119 po\u0142\u0105czenia"
+ },
+ "start_addon": {
+ "title": "Dodatek Z-Wave JS uruchamia si\u0119."
+ },
+ "user": {
+ "data": {
+ "url": "URL"
+ }
+ }
+ }
+ },
+ "title": "Z-Wave JS"
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/pt-BR.json b/homeassistant/components/zwave_js/translations/pt-BR.json
new file mode 100644
index 00000000000000..e29d809ebff3dd
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/pt-BR.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json
new file mode 100644
index 00000000000000..1a65ce3ea71184
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/ru.json
@@ -0,0 +1,61 @@
+{
+ "config": {
+ "abort": {
+ "addon_get_discovery_info_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e\u0431 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0438 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS.",
+ "addon_info_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 Z-Wave JS.",
+ "addon_install_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS.",
+ "addon_missing_discovery_info": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 Z-Wave JS.",
+ "addon_set_config_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e Z-Wave JS.",
+ "addon_start_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS.",
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
+ },
+ "error": {
+ "addon_start_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_ws_url": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "progress": {
+ "install_addon": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u0438\u043d\u0443\u0442.",
+ "start_addon": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0437\u0430\u043f\u0443\u0441\u043a \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0435\u043a\u0443\u043d\u0434."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438",
+ "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443"
+ },
+ "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS"
+ },
+ "hassio_confirm": {
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Z-Wave JS (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant Z-Wave JS)"
+ },
+ "install_addon": {
+ "title": "\u041d\u0430\u0447\u0430\u043b\u0430\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS"
+ },
+ "manual": {
+ "data": {
+ "url": "URL-\u0430\u0434\u0440\u0435\u0441"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor Z-Wave JS"
+ },
+ "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor Z-Wave JS?",
+ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f"
+ },
+ "start_addon": {
+ "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f"
+ },
+ "user": {
+ "data": {
+ "url": "URL-\u0430\u0434\u0440\u0435\u0441"
+ }
+ }
+ }
+ },
+ "title": "Z-Wave JS"
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/tr.json b/homeassistant/components/zwave_js/translations/tr.json
new file mode 100644
index 00000000000000..04ddcc5252ca44
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/tr.json
@@ -0,0 +1,56 @@
+{
+ "config": {
+ "abort": {
+ "addon_get_discovery_info_failed": "Z-Wave JS eklenti ke\u015fif bilgileri al\u0131namad\u0131.",
+ "addon_info_failed": "Z-Wave JS eklenti bilgileri al\u0131namad\u0131.",
+ "addon_install_failed": "Z-Wave JS eklentisi y\u00fcklenemedi.",
+ "addon_missing_discovery_info": "Eksik Z-Wave JS eklenti bulma bilgileri.",
+ "addon_set_config_failed": "Z-Wave JS yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.",
+ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
+ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor",
+ "cannot_connect": "Ba\u011flanma hatas\u0131"
+ },
+ "error": {
+ "addon_start_failed": "Z-Wave JS eklentisi ba\u015flat\u0131lamad\u0131. Yap\u0131land\u0131rmay\u0131 kontrol edin.",
+ "cannot_connect": "Ba\u011flanma hatas\u0131",
+ "invalid_ws_url": "Ge\u00e7ersiz websocket URL'si",
+ "unknown": "Beklenmeyen hata"
+ },
+ "progress": {
+ "install_addon": "L\u00fctfen Z-Wave JS eklenti kurulumu bitene kadar bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "network_key": "A\u011f Anahtar\u0131",
+ "usb_path": "USB Ayg\u0131t Yolu"
+ },
+ "title": "Z-Wave JS eklenti yap\u0131land\u0131rmas\u0131na girin"
+ },
+ "hassio_confirm": {
+ "title": "Z-Wave JS eklentisiyle Z-Wave JS entegrasyonunu ayarlay\u0131n"
+ },
+ "install_addon": {
+ "title": "Z-Wave JS eklenti kurulumu ba\u015flad\u0131"
+ },
+ "manual": {
+ "data": {
+ "url": "URL"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Z-Wave JS Supervisor eklentisini kullan\u0131n"
+ },
+ "description": "Z-Wave JS Supervisor eklentisini kullanmak istiyor musunuz?",
+ "title": "Ba\u011flant\u0131 y\u00f6ntemini se\u00e7in"
+ },
+ "user": {
+ "data": {
+ "url": "URL"
+ }
+ }
+ }
+ },
+ "title": "Z-Wave JS"
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/uk.json b/homeassistant/components/zwave_js/translations/uk.json
new file mode 100644
index 00000000000000..f5ff5224347ae9
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/uk.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
+ "invalid_ws_url": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432\u0435\u0431-\u0441\u043e\u043a\u0435\u0442\u0430",
+ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430"
+ }
+ }
+ }
+ },
+ "title": "Z-Wave JS"
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json
new file mode 100644
index 00000000000000..10b003f71e8cb3
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/zh-Hant.json
@@ -0,0 +1,61 @@
+{
+ "config": {
+ "abort": {
+ "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u63a2\u7d22\u8cc7\u8a0a\u5931\u6557\u3002",
+ "addon_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8cc7\u8a0a\u5931\u6557\u3002",
+ "addon_install_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5931\u6557\u3002",
+ "addon_missing_discovery_info": "\u7f3a\u5c11 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u63a2\u7d22\u8cc7\u8a0a\u3002",
+ "addon_set_config_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u5931\u6557\u3002",
+ "addon_start_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u5931\u6557\u3002",
+ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
+ },
+ "error": {
+ "addon_start_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u52d5\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u3002",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_ws_url": "Websocket URL \u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "progress": {
+ "install_addon": "\u8acb\u7a0d\u7b49 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002",
+ "start_addon": "\u8acb\u7a0d\u7b49 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002"
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "network_key": "\u7db2\u8def\u5bc6\u9470",
+ "usb_path": "USB \u88dd\u7f6e\u8def\u5f91"
+ },
+ "title": "\u8f38\u5165 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a"
+ },
+ "hassio_confirm": {
+ "title": "\u4ee5 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a Z-Wave JS \u6574\u5408"
+ },
+ "install_addon": {
+ "title": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5df2\u555f\u52d5"
+ },
+ "manual": {
+ "data": {
+ "url": "\u7db2\u5740"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "\u4f7f\u7528 Z-Wave JS Supervisor \u9644\u52a0\u5143\u4ef6"
+ },
+ "description": "\u662f\u5426\u8981\u4f7f\u7528 Z-Wave JS Supervisor \u9644\u52a0\u5143\u4ef6\uff1f",
+ "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b"
+ },
+ "start_addon": {
+ "title": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u4e2d\u3002"
+ },
+ "user": {
+ "data": {
+ "url": "\u7db2\u5740"
+ }
+ }
+ }
+ },
+ "title": "Z-Wave JS"
+}
\ No newline at end of file
diff --git a/homeassistant/config.py b/homeassistant/config.py
index 2da9b0331c97f0..362c93d04fa863 100644
--- a/homeassistant/config.py
+++ b/homeassistant/config.py
@@ -1,13 +1,16 @@
"""Module to help with parsing and generating configuration files."""
+from __future__ import annotations
+
from collections import OrderedDict
-from distutils.version import LooseVersion # pylint: disable=import-error
import logging
import os
+from pathlib import Path
import re
import shutil
from types import ModuleType
-from typing import Any, Callable, Dict, Optional, Sequence, Set, Tuple, Union
+from typing import Any, Callable, Sequence
+from awesomeversion import AwesomeVersion
import voluptuous as vol
from voluptuous.humanize import humanize_error
@@ -51,6 +54,7 @@
from homeassistant.helpers import config_per_platform, extract_domain_configs
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_values import EntityValues
+from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import Integration, IntegrationNotFound
from homeassistant.requirements import (
RequirementsNotFound,
@@ -58,7 +62,7 @@
)
from homeassistant.util.package import is_docker_env
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
-from homeassistant.util.yaml import SECRET_YAML, load_yaml
+from homeassistant.util.yaml import SECRET_YAML, Secrets, load_yaml
_LOGGER = logging.getLogger(__name__)
@@ -75,6 +79,13 @@
SCRIPT_CONFIG_PATH = "scripts.yaml"
SCENE_CONFIG_PATH = "scenes.yaml"
+LOAD_EXCEPTIONS = (ImportError, FileNotFoundError)
+INTEGRATION_LOAD_EXCEPTIONS = (
+ IntegrationNotFound,
+ RequirementsNotFound,
+ *LOAD_EXCEPTIONS,
+)
+
DEFAULT_CONFIG = f"""
# Configure a default setup of Home Assistant (frontend, api, etc)
default_config:
@@ -105,14 +116,14 @@
def _no_duplicate_auth_provider(
- configs: Sequence[Dict[str, Any]]
-) -> Sequence[Dict[str, Any]]:
+ configs: Sequence[dict[str, Any]]
+) -> Sequence[dict[str, Any]]:
"""No duplicate auth provider config allowed in a list.
Each type of auth provider can only have one config without optional id.
Unique id is required if same type of auth provider used multiple times.
"""
- config_keys: Set[Tuple[str, Optional[str]]] = set()
+ config_keys: set[tuple[str, str | None]] = set()
for config in configs:
key = (config[CONF_TYPE], config.get(CONF_ID))
if key in config_keys:
@@ -126,8 +137,8 @@ def _no_duplicate_auth_provider(
def _no_duplicate_auth_mfa_module(
- configs: Sequence[Dict[str, Any]]
-) -> Sequence[Dict[str, Any]]:
+ configs: Sequence[dict[str, Any]]
+) -> Sequence[dict[str, Any]]:
"""No duplicate auth mfa module item allowed in a list.
Each type of mfa module can only have one config without optional id.
@@ -135,7 +146,7 @@ def _no_duplicate_auth_mfa_module(
times.
Note: this is different than auth provider
"""
- config_keys: Set[str] = set()
+ config_keys: set[str] = set()
for config in configs:
key = config.get(CONF_ID, config[CONF_TYPE])
if key in config_keys:
@@ -304,29 +315,39 @@ def _write_default_config(config_dir: str) -> bool:
return False
-async def async_hass_config_yaml(hass: HomeAssistant) -> Dict:
+async def async_hass_config_yaml(hass: HomeAssistant) -> dict:
"""Load YAML from a Home Assistant configuration file.
This function allow a component inside the asyncio loop to reload its
configuration by itself. Include package merge.
"""
+ if hass.config.config_dir is None:
+ secrets = None
+ else:
+ secrets = Secrets(Path(hass.config.config_dir))
+
# Not using async_add_executor_job because this is an internal method.
config = await hass.loop.run_in_executor(
- None, load_yaml_config_file, hass.config.path(YAML_CONFIG_FILE)
+ None,
+ load_yaml_config_file,
+ hass.config.path(YAML_CONFIG_FILE),
+ secrets,
)
core_config = config.get(CONF_CORE, {})
await merge_packages_config(hass, config, core_config.get(CONF_PACKAGES, {}))
return config
-def load_yaml_config_file(config_path: str) -> Dict[Any, Any]:
+def load_yaml_config_file(
+ config_path: str, secrets: Secrets | None = None
+) -> dict[Any, Any]:
"""Parse a YAML configuration file.
Raises FileNotFoundError or HomeAssistantError.
This method needs to run in an executor.
"""
- conf_dict = load_yaml(config_path)
+ conf_dict = load_yaml(config_path, secrets)
if not isinstance(conf_dict, dict):
msg = (
@@ -363,15 +384,15 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None:
"Upgrading configuration directory from %s to %s", conf_version, __version__
)
- version_obj = LooseVersion(conf_version)
+ version_obj = AwesomeVersion(conf_version)
- if version_obj < LooseVersion("0.50"):
+ if version_obj < AwesomeVersion("0.50"):
# 0.50 introduced persistent deps dir.
lib_path = hass.config.path("deps")
if os.path.isdir(lib_path):
shutil.rmtree(lib_path)
- if version_obj < LooseVersion("0.92"):
+ if version_obj < AwesomeVersion("0.92"):
# 0.92 moved google/tts.py to google_translate/tts.py
config_path = hass.config.path(YAML_CONFIG_FILE)
@@ -387,7 +408,7 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None:
except OSError:
_LOGGER.exception("Migrating to google_translate tts failed")
- if version_obj < LooseVersion("0.94") and is_docker_env():
+ if version_obj < AwesomeVersion("0.94") and is_docker_env():
# In 0.94 we no longer install packages inside the deps folder when
# running inside a Docker container.
lib_path = hass.config.path("deps")
@@ -402,9 +423,9 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None:
def async_log_exception(
ex: Exception,
domain: str,
- config: Dict,
+ config: dict,
hass: HomeAssistant,
- link: Optional[str] = None,
+ link: str | None = None,
) -> None:
"""Log an error for configuration validation.
@@ -418,8 +439,8 @@ def async_log_exception(
@callback
def _format_config_error(
- ex: Exception, domain: str, config: Dict, link: Optional[str] = None
-) -> Tuple[str, bool]:
+ ex: Exception, domain: str, config: dict, link: str | None = None
+) -> tuple[str, bool]:
"""Generate log exception for configuration validation.
This method must be run in the event loop.
@@ -455,7 +476,7 @@ def _format_config_error(
return message, is_friendly
-async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> None:
+async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> None:
"""Process the [homeassistant] section from the configuration.
This method is a coroutine.
@@ -584,7 +605,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non
)
-def _log_pkg_error(package: str, component: str, config: Dict, message: str) -> None:
+def _log_pkg_error(package: str, component: str, config: dict, message: str) -> None:
"""Log an error while merging packages."""
message = f"Package {package} setup failed. Integration {component} {message}"
@@ -597,7 +618,7 @@ def _log_pkg_error(package: str, component: str, config: Dict, message: str) ->
_LOGGER.error(message)
-def _identify_config_schema(module: ModuleType) -> Optional[str]:
+def _identify_config_schema(module: ModuleType) -> str | None:
"""Extract the schema and identify list or dict based."""
if not isinstance(module.CONFIG_SCHEMA, vol.Schema): # type: ignore
return None
@@ -645,9 +666,9 @@ def _identify_config_schema(module: ModuleType) -> Optional[str]:
return None
-def _recursive_merge(conf: Dict[str, Any], package: Dict[str, Any]) -> Union[bool, str]:
+def _recursive_merge(conf: dict[str, Any], package: dict[str, Any]) -> bool | str:
"""Merge package into conf, recursively."""
- error: Union[bool, str] = False
+ error: bool | str = False
for key, pack_conf in package.items():
if isinstance(pack_conf, dict):
if not pack_conf:
@@ -669,10 +690,10 @@ def _recursive_merge(conf: Dict[str, Any], package: Dict[str, Any]) -> Union[boo
async def merge_packages_config(
hass: HomeAssistant,
- config: Dict,
- packages: Dict[str, Any],
+ config: dict,
+ packages: dict[str, Any],
_log_pkg_error: Callable = _log_pkg_error,
-) -> Dict:
+) -> dict:
"""Merge packages into the top-level configuration. Mutate config."""
PACKAGES_CONFIG_SCHEMA(packages)
for pack_name, pack_conf in packages.items():
@@ -688,7 +709,7 @@ async def merge_packages_config(
hass, domain
)
component = integration.get_component()
- except (IntegrationNotFound, RequirementsNotFound, ImportError) as ex:
+ except INTEGRATION_LOAD_EXCEPTIONS as ex:
_log_pkg_error(pack_name, comp_name, config, str(ex))
continue
@@ -734,8 +755,8 @@ async def merge_packages_config(
async def async_process_component_config(
- hass: HomeAssistant, config: Dict, integration: Integration
-) -> Optional[Dict]:
+ hass: HomeAssistant, config: ConfigType, integration: Integration
+) -> ConfigType | None:
"""Check component configuration and return processed configuration.
Returns None on error.
@@ -745,7 +766,7 @@ async def async_process_component_config(
domain = integration.domain
try:
component = integration.get_component()
- except ImportError as ex:
+ except LOAD_EXCEPTIONS as ex:
_LOGGER.error("Unable to import %s: %s", domain, ex)
return None
@@ -824,7 +845,7 @@ async def async_process_component_config(
try:
platform = p_integration.get_platform(domain)
- except ImportError:
+ except LOAD_EXCEPTIONS:
_LOGGER.exception("Platform error: %s", domain)
continue
@@ -860,13 +881,13 @@ async def async_process_component_config(
@callback
-def config_without_domain(config: Dict, domain: str) -> Dict:
+def config_without_domain(config: dict, domain: str) -> dict:
"""Return a config with all configuration for a domain removed."""
filter_keys = extract_domain_configs(config, domain)
return {key: value for key, value in config.items() if key not in filter_keys}
-async def async_check_ha_config_file(hass: HomeAssistant) -> Optional[str]:
+async def async_check_ha_config_file(hass: HomeAssistant) -> str | None:
"""Check if Home Assistant configuration file is valid.
This method is a coroutine.
@@ -883,7 +904,7 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> Optional[str]:
@callback
def async_notify_setup_error(
- hass: HomeAssistant, component: str, display_link: Optional[str] = None
+ hass: HomeAssistant, component: str, display_link: str | None = None
) -> None:
"""Print a persistent notification.
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index abc6b2f46afc57..23758cf88f2add 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -1,17 +1,20 @@
"""Manage config entries in Home Assistant."""
+from __future__ import annotations
+
import asyncio
import functools
import logging
from types import MappingProxyType, MethodType
-from typing import Any, Callable, Dict, List, Optional, Set, Union, cast
+from typing import Any, Callable, Optional, cast
import weakref
import attr
from homeassistant import data_entry_flow, loader
-from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
+from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
+from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import device_registry, entity_registry
from homeassistant.helpers.event import Event
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from homeassistant.setup import async_process_deps_reqs, async_setup_component
@@ -90,6 +93,8 @@
CONN_CLASS_ASSUMED = "assumed"
CONN_CLASS_UNKNOWN = "unknown"
+DISABLED_USER = "user"
+
RELOAD_AFTER_UPDATE_DELAY = 30
@@ -124,6 +129,7 @@ class ConfigEntry:
"source",
"connection_class",
"state",
+ "disabled_by",
"_setup_lock",
"update_listeners",
"_async_cancel_retry_setup",
@@ -138,10 +144,11 @@ def __init__(
source: str,
connection_class: str,
system_options: dict,
- options: Optional[dict] = None,
- unique_id: Optional[str] = None,
- entry_id: Optional[str] = None,
+ options: dict | None = None,
+ unique_id: str | None = None,
+ entry_id: str | None = None,
state: str = ENTRY_STATE_NOT_LOADED,
+ disabled_by: str | None = None,
) -> None:
"""Initialize a config entry."""
# Unique id of the config entry
@@ -177,26 +184,29 @@ def __init__(
# Unique ID of this entry.
self.unique_id = unique_id
+ # Config entry is disabled
+ self.disabled_by = disabled_by
+
# Supports unload
self.supports_unload = False
# Listeners to call on update
- self.update_listeners: List[
- Union[weakref.ReferenceType[UpdateListenerType], weakref.WeakMethod]
+ self.update_listeners: list[
+ weakref.ReferenceType[UpdateListenerType] | weakref.WeakMethod
] = []
# Function to cancel a scheduled retry
- self._async_cancel_retry_setup: Optional[Callable[[], Any]] = None
+ self._async_cancel_retry_setup: Callable[[], Any] | None = None
async def async_setup(
self,
hass: HomeAssistant,
*,
- integration: Optional[loader.Integration] = None,
+ integration: loader.Integration | None = None,
tries: int = 0,
) -> None:
"""Set up an entry."""
- if self.source == SOURCE_IGNORE:
+ if self.source == SOURCE_IGNORE or self.disabled_by:
return
if integration is None:
@@ -243,25 +253,43 @@ async def async_setup(
"%s.async_setup_entry did not return boolean", integration.domain
)
result = False
- except ConfigEntryNotReady:
+ except ConfigEntryNotReady as ex:
self.state = ENTRY_STATE_SETUP_RETRY
wait_time = 2 ** min(tries, 4) * 5
tries += 1
- _LOGGER.warning(
- "Config entry '%s' for %s integration not ready yet. Retrying in %d seconds",
- self.title,
- self.domain,
- wait_time,
- )
+ message = str(ex)
+ if not message and ex.__cause__:
+ message = str(ex.__cause__)
+ ready_message = f"ready yet: {message}" if message else "ready yet"
+ if tries == 1:
+ _LOGGER.warning(
+ "Config entry '%s' for %s integration not %s; Retrying in background",
+ self.title,
+ self.domain,
+ ready_message,
+ )
+ else:
+ _LOGGER.debug(
+ "Config entry '%s' for %s integration not %s; Retrying in %d seconds",
+ self.title,
+ self.domain,
+ ready_message,
+ wait_time,
+ )
- async def setup_again(now: Any) -> None:
+ async def setup_again(*_: Any) -> None:
"""Run setup again."""
self._async_cancel_retry_setup = None
await self.async_setup(hass, integration=integration, tries=tries)
- self._async_cancel_retry_setup = hass.helpers.event.async_call_later(
- wait_time, setup_again
- )
+ if hass.state == CoreState.running:
+ self._async_cancel_retry_setup = hass.helpers.event.async_call_later(
+ wait_time, setup_again
+ )
+ else:
+ self._async_cancel_retry_setup = hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_STARTED, setup_again
+ )
return
except Exception: # pylint: disable=broad-except
_LOGGER.exception(
@@ -279,7 +307,7 @@ async def setup_again(now: Any) -> None:
self.state = ENTRY_STATE_SETUP_ERROR
async def async_unload(
- self, hass: HomeAssistant, *, integration: Optional[loader.Integration] = None
+ self, hass: HomeAssistant, *, integration: loader.Integration | None = None
) -> bool:
"""Unload an entry.
@@ -426,7 +454,7 @@ def add_update_listener(self, listener: UpdateListenerType) -> CALLBACK_TYPE:
return lambda: self.update_listeners.remove(weak_listener)
- def as_dict(self) -> Dict[str, Any]:
+ def as_dict(self) -> dict[str, Any]:
"""Return dictionary version of this entry."""
return {
"entry_id": self.entry_id,
@@ -439,6 +467,7 @@ def as_dict(self) -> Dict[str, Any]:
"source": self.source,
"connection_class": self.connection_class,
"unique_id": self.unique_id,
+ "disabled_by": self.disabled_by,
}
@@ -446,7 +475,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager):
"""Manage all the config entry flows that are in progress."""
def __init__(
- self, hass: HomeAssistant, config_entries: "ConfigEntries", hass_config: dict
+ self, hass: HomeAssistant, config_entries: ConfigEntries, hass_config: dict
):
"""Initialize the config entry flow manager."""
super().__init__(hass)
@@ -454,8 +483,8 @@ def __init__(
self._hass_config = hass_config
async def async_finish_flow(
- self, flow: data_entry_flow.FlowHandler, result: Dict[str, Any]
- ) -> Dict[str, Any]:
+ self, flow: data_entry_flow.FlowHandler, result: dict[str, Any]
+ ) -> dict[str, Any]:
"""Finish a config flow and add an entry."""
flow = cast(ConfigFlow, flow)
@@ -525,8 +554,8 @@ async def async_finish_flow(
return result
async def async_create_flow(
- self, handler_key: Any, *, context: Optional[Dict] = None, data: Any = None
- ) -> "ConfigFlow":
+ self, handler_key: Any, *, context: dict | None = None, data: Any = None
+ ) -> ConfigFlow:
"""Create a flow for specified handler.
Handler key is the domain of the component that we want to set up.
@@ -602,45 +631,47 @@ def __init__(self, hass: HomeAssistant, hass_config: dict) -> None:
self.flow = ConfigEntriesFlowManager(hass, self, hass_config)
self.options = OptionsFlowManager(hass)
self._hass_config = hass_config
- self._entries: List[ConfigEntry] = []
+ self._entries: dict[str, ConfigEntry] = {}
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
EntityRegistryDisabledHandler(hass).async_setup()
@callback
- def async_domains(self) -> List[str]:
+ def async_domains(
+ self, include_ignore: bool = False, include_disabled: bool = False
+ ) -> list[str]:
"""Return domains for which we have entries."""
- seen: Set[str] = set()
- result = []
-
- for entry in self._entries:
- if entry.domain not in seen:
- seen.add(entry.domain)
- result.append(entry.domain)
-
- return result
+ return list(
+ {
+ entry.domain: None
+ for entry in self._entries.values()
+ if (include_ignore or entry.source != SOURCE_IGNORE)
+ and (include_disabled or not entry.disabled_by)
+ }
+ )
@callback
- def async_get_entry(self, entry_id: str) -> Optional[ConfigEntry]:
+ def async_get_entry(self, entry_id: str) -> ConfigEntry | None:
"""Return entry with matching entry_id."""
- for entry in self._entries:
- if entry_id == entry.entry_id:
- return entry
- return None
+ return self._entries.get(entry_id)
@callback
- def async_entries(self, domain: Optional[str] = None) -> List[ConfigEntry]:
+ def async_entries(self, domain: str | None = None) -> list[ConfigEntry]:
"""Return all entries or entries for a specific domain."""
if domain is None:
- return list(self._entries)
- return [entry for entry in self._entries if entry.domain == domain]
+ return list(self._entries.values())
+ return [entry for entry in self._entries.values() if entry.domain == domain]
async def async_add(self, entry: ConfigEntry) -> None:
"""Add and setup an entry."""
- self._entries.append(entry)
+ if entry.entry_id in self._entries:
+ raise HomeAssistantError(
+ f"An entry with the id {entry.entry_id} already exists."
+ )
+ self._entries[entry.entry_id] = entry
await self.async_setup(entry.entry_id)
self._async_schedule_save()
- async def async_remove(self, entry_id: str) -> Dict[str, Any]:
+ async def async_remove(self, entry_id: str) -> dict[str, Any]:
"""Remove an entry."""
entry = self.async_get_entry(entry_id)
@@ -654,7 +685,7 @@ async def async_remove(self, entry_id: str) -> Dict[str, Any]:
await entry.async_remove(self.hass)
- self._entries.remove(entry)
+ del self._entries[entry.entry_id]
self._async_schedule_save()
dev_reg, ent_reg = await asyncio.gather(
@@ -690,11 +721,11 @@ async def async_initialize(self) -> None:
)
if config is None:
- self._entries = []
+ self._entries = {}
return
- self._entries = [
- ConfigEntry(
+ self._entries = {
+ entry["entry_id"]: ConfigEntry(
version=entry["version"],
domain=entry["domain"],
entry_id=entry["entry_id"],
@@ -709,9 +740,11 @@ async def async_initialize(self) -> None:
system_options=entry.get("system_options", {}),
# New in 0.104
unique_id=entry.get("unique_id"),
+ # New in 2021.3
+ disabled_by=entry.get("disabled_by"),
)
for entry in config["entries"]
- ]
+ }
async def async_setup(self, entry_id: str) -> bool:
"""Set up a config entry.
@@ -757,23 +790,64 @@ async def async_reload(self, entry_id: str) -> bool:
If an entry was not loaded, will just load.
"""
+ entry = self.async_get_entry(entry_id)
+
+ if entry is None:
+ raise UnknownEntry
+
unload_result = await self.async_unload(entry_id)
- if not unload_result:
+ if not unload_result or entry.disabled_by:
return unload_result
return await self.async_setup(entry_id)
+ async def async_set_disabled_by(
+ self, entry_id: str, disabled_by: str | None
+ ) -> bool:
+ """Disable an entry.
+
+ If disabled_by is changed, the config entry will be reloaded.
+ """
+ entry = self.async_get_entry(entry_id)
+
+ if entry is None:
+ raise UnknownEntry
+
+ if entry.disabled_by == disabled_by:
+ return True
+
+ entry.disabled_by = disabled_by
+ self._async_schedule_save()
+
+ dev_reg = device_registry.async_get(self.hass)
+ ent_reg = entity_registry.async_get(self.hass)
+
+ if not entry.disabled_by:
+ # The config entry will no longer be disabled, enable devices and entities
+ device_registry.async_config_entry_disabled_by_changed(dev_reg, entry)
+ entity_registry.async_config_entry_disabled_by_changed(ent_reg, entry)
+
+ # Load or unload the config entry
+ reload_result = await self.async_reload(entry_id)
+
+ if entry.disabled_by:
+ # The config entry has been disabled, disable devices and entities
+ device_registry.async_config_entry_disabled_by_changed(dev_reg, entry)
+ entity_registry.async_config_entry_disabled_by_changed(ent_reg, entry)
+
+ return reload_result
+
@callback
def async_update_entry(
self,
entry: ConfigEntry,
*,
- unique_id: Union[str, dict, None, UndefinedType] = UNDEFINED,
- title: Union[str, dict, UndefinedType] = UNDEFINED,
- data: Union[dict, UndefinedType] = UNDEFINED,
- options: Union[dict, UndefinedType] = UNDEFINED,
- system_options: Union[dict, UndefinedType] = UNDEFINED,
+ unique_id: str | dict | None | UndefinedType = UNDEFINED,
+ title: str | dict | UndefinedType = UNDEFINED,
+ data: dict | UndefinedType = UNDEFINED,
+ options: dict | UndefinedType = UNDEFINED,
+ system_options: dict | UndefinedType = UNDEFINED,
) -> bool:
"""Update a config entry.
@@ -858,12 +932,12 @@ def _async_schedule_save(self) -> None:
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
@callback
- def _data_to_save(self) -> Dict[str, List[Dict[str, Any]]]:
+ def _data_to_save(self) -> dict[str, list[dict[str, Any]]]:
"""Return data to save."""
- return {"entries": [entry.as_dict() for entry in self._entries]}
+ return {"entries": [entry.as_dict() for entry in self._entries.values()]}
-async def _old_conf_migrator(old_config: Dict[str, Any]) -> Dict[str, Any]:
+async def _old_conf_migrator(old_config: dict[str, Any]) -> dict[str, Any]:
"""Migrate the pre-0.73 config format to the latest version."""
return {"entries": old_config}
@@ -871,7 +945,7 @@ async def _old_conf_migrator(old_config: Dict[str, Any]) -> Dict[str, Any]:
class ConfigFlow(data_entry_flow.FlowHandler):
"""Base class for config flows with some helpers."""
- def __init_subclass__(cls, domain: Optional[str] = None, **kwargs: Any) -> None:
+ def __init_subclass__(cls, domain: str | None = None, **kwargs: Any) -> None:
"""Initialize a subclass, register if possible."""
super().__init_subclass__(**kwargs) # type: ignore
if domain is not None:
@@ -880,9 +954,8 @@ def __init_subclass__(cls, domain: Optional[str] = None, **kwargs: Any) -> None:
CONNECTION_CLASS = CONN_CLASS_UNKNOWN
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return unique ID if available."""
- # pylint: disable=no-member
if not self.context:
return None
@@ -890,22 +963,21 @@ def unique_id(self) -> Optional[str]:
@staticmethod
@callback
- def async_get_options_flow(config_entry: ConfigEntry) -> "OptionsFlow":
+ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
raise data_entry_flow.UnknownHandler
@callback
def _abort_if_unique_id_configured(
self,
- updates: Optional[Dict[Any, Any]] = None,
+ updates: dict[Any, Any] | None = None,
reload_on_update: bool = True,
) -> None:
"""Abort if the unique ID is already configured."""
- assert self.hass
if self.unique_id is None:
return
- for entry in self._async_current_entries():
+ for entry in self._async_current_entries(include_ignore=True):
if entry.unique_id == self.unique_id:
if updates is not None:
changed = self.hass.config_entries.async_update_entry(
@@ -925,14 +997,14 @@ def _abort_if_unique_id_configured(
raise data_entry_flow.AbortFlow("already_configured")
async def async_set_unique_id(
- self, unique_id: Optional[str] = None, *, raise_on_progress: bool = True
- ) -> Optional[ConfigEntry]:
+ self, unique_id: str | None = None, *, raise_on_progress: bool = True
+ ) -> ConfigEntry | None:
"""Set a unique ID for the config flow.
Returns optionally existing config entry with same ID.
"""
if unique_id is None:
- self.context["unique_id"] = None # pylint: disable=no-member
+ self.context["unique_id"] = None
return None
if raise_on_progress:
@@ -940,31 +1012,49 @@ async def async_set_unique_id(
if progress["context"].get("unique_id") == unique_id:
raise data_entry_flow.AbortFlow("already_in_progress")
- self.context["unique_id"] = unique_id # pylint: disable=no-member
+ self.context["unique_id"] = unique_id
# Abort discoveries done using the default discovery unique id
- assert self.hass is not None
if unique_id != DEFAULT_DISCOVERY_UNIQUE_ID:
for progress in self._async_in_progress():
if progress["context"].get("unique_id") == DEFAULT_DISCOVERY_UNIQUE_ID:
self.hass.config_entries.flow.async_abort(progress["flow_id"])
- for entry in self._async_current_entries():
+ for entry in self._async_current_entries(include_ignore=True):
if entry.unique_id == unique_id:
return entry
return None
@callback
- def _async_current_entries(self) -> List[ConfigEntry]:
- """Return current entries."""
- assert self.hass is not None
- return self.hass.config_entries.async_entries(self.handler)
+ def _set_confirm_only(
+ self,
+ ) -> None:
+ """Mark the config flow as only needing user confirmation to finish flow."""
+ self.context["confirm_only"] = True
@callback
- def _async_current_ids(self, include_ignore: bool = True) -> Set[Optional[str]]:
+ def _async_current_entries(
+ self, include_ignore: bool | None = None
+ ) -> list[ConfigEntry]:
+ """Return current entries.
+
+ If the flow is user initiated, filter out ignored entries unless include_ignore is True.
+ """
+ config_entries = self.hass.config_entries.async_entries(self.handler)
+
+ if (
+ include_ignore is True
+ or include_ignore is None
+ and self.source != SOURCE_USER
+ ):
+ return config_entries
+
+ return [entry for entry in config_entries if entry.source != SOURCE_IGNORE]
+
+ @callback
+ def _async_current_ids(self, include_ignore: bool = True) -> set[str | None]:
"""Return current unique IDs."""
- assert self.hass is not None
return {
entry.unique_id
for entry in self.hass.config_entries.async_entries(self.handler)
@@ -972,27 +1062,28 @@ def _async_current_ids(self, include_ignore: bool = True) -> Set[Optional[str]]:
}
@callback
- def _async_in_progress(self) -> List[Dict]:
+ def _async_in_progress(self, include_uninitialized: bool = False) -> list[dict]:
"""Return other in progress flows for current domain."""
- assert self.hass is not None
return [
flw
- for flw in self.hass.config_entries.flow.async_progress()
+ for flw in self.hass.config_entries.flow.async_progress(
+ include_uninitialized=include_uninitialized
+ )
if flw["handler"] == self.handler and flw["flow_id"] != self.flow_id
]
- async def async_step_ignore(self, user_input: Dict[str, Any]) -> Dict[str, Any]:
+ async def async_step_ignore(self, user_input: dict[str, Any]) -> dict[str, Any]:
"""Ignore this config flow."""
await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False)
return self.async_create_entry(title=user_input["title"], data={})
- async def async_step_unignore(self, user_input: Dict[str, Any]) -> Dict[str, Any]:
+ async def async_step_unignore(self, user_input: dict[str, Any]) -> dict[str, Any]:
"""Rediscover a config entry by it's unique_id."""
return self.async_abort(reason="not_implemented")
async def async_step_user(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initiated by the user."""
return self.async_abort(reason="not_implemented")
@@ -1017,24 +1108,21 @@ async def _async_handle_discovery_without_unique_id(self) -> None:
self._abort_if_unique_id_configured()
# Abort if any other flow for this handler is already in progress
- assert self.hass is not None
- if self._async_in_progress():
+ if self._async_in_progress(include_uninitialized=True):
raise data_entry_flow.AbortFlow("already_in_progress")
async def async_step_discovery(
- self, discovery_info: Dict[str, Any]
- ) -> Dict[str, Any]:
+ self, discovery_info: dict[str, Any]
+ ) -> dict[str, Any]:
"""Handle a flow initialized by discovery."""
await self._async_handle_discovery_without_unique_id()
return await self.async_step_user()
@callback
def async_abort(
- self, *, reason: str, description_placeholders: Optional[Dict] = None
- ) -> Dict[str, Any]:
+ self, *, reason: str, description_placeholders: dict | None = None
+ ) -> dict[str, Any]:
"""Abort the config flow."""
- assert self.hass
-
# Remove reauth notification if no reauth flows are in progress
if self.source == SOURCE_REAUTH and not any(
ent["context"]["source"] == SOURCE_REAUTH
@@ -1064,9 +1152,9 @@ async def async_create_flow(
self,
handler_key: Any,
*,
- context: Optional[Dict[str, Any]] = None,
- data: Optional[Dict[str, Any]] = None,
- ) -> "OptionsFlow":
+ context: dict[str, Any] | None = None,
+ data: dict[str, Any] | None = None,
+ ) -> OptionsFlow:
"""Create an options flow for a config entry.
Entry_id and flow.handler is the same thing to map entry with flow.
@@ -1081,8 +1169,8 @@ async def async_create_flow(
return cast(OptionsFlow, HANDLERS[entry.domain].async_get_options_flow(entry))
async def async_finish_flow(
- self, flow: data_entry_flow.FlowHandler, result: Dict[str, Any]
- ) -> Dict[str, Any]:
+ self, flow: data_entry_flow.FlowHandler, result: dict[str, Any]
+ ) -> dict[str, Any]:
"""Finish an options flow and update options for configuration entry.
Flow.handler and entry_id is the same thing to map flow with entry.
@@ -1118,7 +1206,7 @@ def update(self, *, disable_new_entities: bool) -> None:
"""Update properties."""
self.disable_new_entities = disable_new_entities
- def as_dict(self) -> Dict[str, Any]:
+ def as_dict(self) -> dict[str, Any]:
"""Return dictionary version of this config entries system options."""
return {"disable_new_entities": self.disable_new_entities}
@@ -1129,25 +1217,21 @@ class EntityRegistryDisabledHandler:
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the handler."""
self.hass = hass
- self.registry: Optional[entity_registry.EntityRegistry] = None
- self.changed: Set[str] = set()
- self._remove_call_later: Optional[Callable[[], None]] = None
+ self.registry: entity_registry.EntityRegistry | None = None
+ self.changed: set[str] = set()
+ self._remove_call_later: Callable[[], None] | None = None
@callback
def async_setup(self) -> None:
"""Set up the disable handler."""
self.hass.bus.async_listen(
- entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, self._handle_entry_updated
+ entity_registry.EVENT_ENTITY_REGISTRY_UPDATED,
+ self._handle_entry_updated,
+ event_filter=_handle_entry_updated_filter,
)
async def _handle_entry_updated(self, event: Event) -> None:
"""Handle entity registry entry update."""
- if (
- event.data["action"] != "update"
- or "disabled_by" not in event.data["changes"]
- ):
- return
-
if self.registry is None:
self.registry = await entity_registry.async_get_registry(self.hass)
@@ -1201,6 +1285,22 @@ async def _handle_reload(self, _now: Any) -> None:
)
+@callback
+def _handle_entry_updated_filter(event: Event) -> bool:
+ """Handle entity registry entry update filter.
+
+ Only handle changes to "disabled_by".
+ If "disabled_by" was DISABLED_CONFIG_ENTRY, reload is not needed.
+ """
+ if (
+ event.data["action"] != "update"
+ or "disabled_by" not in event.data["changes"]
+ or event.data["changes"]["disabled_by"] == entity_registry.DISABLED_CONFIG_ENTRY
+ ):
+ return False
+ return True
+
+
async def support_entry_unload(hass: HomeAssistant, domain: str) -> bool:
"""Test if a domain supports entry unloading."""
integration = await loader.async_get_integration(hass, domain)
diff --git a/homeassistant/const.py b/homeassistant/const.py
index 4406c8bdfc3e2c..7d05a7c03f4768 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -1,6 +1,6 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 2021
-MINOR_VERSION = 3
+MINOR_VERSION = 5
PATCH_VERSION = "0.dev0"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
@@ -22,6 +22,10 @@
# If no name is specified
DEVICE_DEFAULT_NAME = "Unnamed Device"
+# Max characters for an event_type (changing this requires a recorder
+# database migration)
+MAX_LENGTH_EVENT_TYPE = 64
+
# Sun events
SUN_EVENT_SUNSET = "sunset"
SUN_EVENT_SUNRISE = "sunrise"
@@ -73,6 +77,7 @@
CONF_DEFAULT = "default"
CONF_DELAY = "delay"
CONF_DELAY_TIME = "delay_time"
+CONF_DESCRIPTION = "description"
CONF_DEVICE = "device"
CONF_DEVICES = "devices"
CONF_DEVICE_CLASS = "device_class"
@@ -209,7 +214,6 @@
EVENT_HOMEASSISTANT_STOP = "homeassistant_stop"
EVENT_HOMEASSISTANT_FINAL_WRITE = "homeassistant_final_write"
EVENT_LOGBOOK_ENTRY = "logbook_entry"
-EVENT_PLATFORM_DISCOVERED = "platform_discovered"
EVENT_SERVICE_REGISTERED = "service_registered"
EVENT_SERVICE_REMOVED = "service_removed"
EVENT_STATE_CHANGED = "state_changed"
@@ -220,6 +224,8 @@
# #### DEVICE CLASSES ####
DEVICE_CLASS_BATTERY = "battery"
+DEVICE_CLASS_CO = "carbon_monoxide"
+DEVICE_CLASS_CO2 = "carbon_dioxide"
DEVICE_CLASS_HUMIDITY = "humidity"
DEVICE_CLASS_ILLUMINANCE = "illuminance"
DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength"
@@ -312,9 +318,6 @@
# Electrical attributes
ATTR_VOLTAGE = "voltage"
-# Contains the information that is discovered
-ATTR_DISCOVERED = "discovered"
-
# Location of the device/sensor
ATTR_LOCATION = "location"
diff --git a/homeassistant/core.py b/homeassistant/core.py
index 58d9d1e67540ad..6ad722e0d18b43 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -4,11 +4,12 @@
Home Assistant is a Home Automation framework for observing the state
of entities and react to changes.
"""
+from __future__ import annotations
+
import asyncio
import datetime
import enum
import functools
-from ipaddress import ip_address
import logging
import os
import pathlib
@@ -23,14 +24,10 @@
Callable,
Collection,
Coroutine,
- Dict,
Iterable,
- List,
Mapping,
Optional,
- Set,
TypeVar,
- Union,
cast,
)
@@ -61,17 +58,23 @@
EVENT_TIMER_OUT_OF_SYNC,
LENGTH_METERS,
MATCH_ALL,
+ MAX_LENGTH_EVENT_TYPE,
__version__,
)
from homeassistant.exceptions import (
HomeAssistantError,
InvalidEntityFormatError,
InvalidStateError,
+ MaxLengthExceeded,
ServiceNotFound,
Unauthorized,
)
-from homeassistant.util import location, network
-from homeassistant.util.async_ import fire_coroutine_threadsafe, run_callback_threadsafe
+from homeassistant.util import location
+from homeassistant.util.async_ import (
+ fire_coroutine_threadsafe,
+ run_callback_threadsafe,
+ shutdown_run_callback_threadsafe,
+)
import homeassistant.util.dt as dt_util
from homeassistant.util.timeout import TimeoutManager
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem
@@ -115,7 +118,7 @@
_LOGGER = logging.getLogger(__name__)
-def split_entity_id(entity_id: str) -> List[str]:
+def split_entity_id(entity_id: str) -> list[str]:
"""Split a state entity ID into domain and object ID."""
return entity_id.split(".", 1)
@@ -205,15 +208,15 @@ class CoreState(enum.Enum):
def __str__(self) -> str: # pylint: disable=invalid-str-returned
"""Return the event."""
- return self.value # type: ignore
+ return self.value
class HomeAssistant:
"""Root object of the Home Assistant home automation."""
- auth: "AuthManager"
- http: "HomeAssistantHTTP" = None # type: ignore
- config_entries: "ConfigEntries" = None # type: ignore
+ auth: AuthManager
+ http: HomeAssistantHTTP = None # type: ignore
+ config_entries: ConfigEntries = None # type: ignore
def __init__(self) -> None:
"""Initialize new Home Assistant object."""
@@ -231,7 +234,7 @@ def __init__(self) -> None:
self.state: CoreState = CoreState.not_running
self.exit_code: int = 0
# If not None, use to signal end-of-loop
- self._stopped: Optional[asyncio.Event] = None
+ self._stopped: asyncio.Event | None = None
# Timeout handler for Core/Helper namespace
self.timeout: TimeoutManager = TimeoutManager()
@@ -336,7 +339,7 @@ def add_job(self, target: Callable[..., Any], *args: Any) -> None:
@callback
def async_add_job(
self, target: Callable[..., Any], *args: Any
- ) -> Optional[asyncio.Future]:
+ ) -> asyncio.Future | None:
"""Add a job from within the event loop.
This method must be run in the event loop.
@@ -353,9 +356,7 @@ def async_add_job(
return self.async_add_hass_job(HassJob(target), *args)
@callback
- def async_add_hass_job(
- self, hassjob: HassJob, *args: Any
- ) -> Optional[asyncio.Future]:
+ def async_add_hass_job(self, hassjob: HassJob, *args: Any) -> asyncio.Future | None:
"""Add a HassJob from within the event loop.
This method must be run in the event loop.
@@ -417,9 +418,7 @@ def async_stop_track_tasks(self) -> None:
self._track_task = False
@callback
- def async_run_hass_job(
- self, hassjob: HassJob, *args: Any
- ) -> Optional[asyncio.Future]:
+ def async_run_hass_job(self, hassjob: HassJob, *args: Any) -> asyncio.Future | None:
"""Run a HassJob from within the event loop.
This method must be run in the event loop.
@@ -435,8 +434,8 @@ def async_run_hass_job(
@callback
def async_run_job(
- self, target: Callable[..., Union[None, Awaitable]], *args: Any
- ) -> Optional[asyncio.Future]:
+ self, target: Callable[..., None | Awaitable], *args: Any
+ ) -> asyncio.Future | None:
"""Run a job from within the event loop.
This method must be run in the event loop.
@@ -459,7 +458,7 @@ async def async_block_till_done(self) -> None:
"""Block until all pending work is done."""
# To flush out any call_soon_threadsafe
await asyncio.sleep(0)
- start_time: Optional[float] = None
+ start_time: float | None = None
while self._pending_tasks:
pending = [task for task in self._pending_tasks if not task.done()]
@@ -516,11 +515,13 @@ async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None:
if self.state == CoreState.not_running: # just ignore
return
if self.state in [CoreState.stopping, CoreState.final_write]:
- _LOGGER.info("async_stop called twice: ignored")
+ _LOGGER.info("Additional call to async_stop was ignored")
return
if self.state == CoreState.starting:
# This may not work
- _LOGGER.warning("async_stop called before startup is complete")
+ _LOGGER.warning(
+ "Stopping Home Assistant before startup has completed may fail"
+ )
# stage 1
self.state = CoreState.stopping
@@ -548,6 +549,14 @@ async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None:
# stage 3
self.state = CoreState.not_running
self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE)
+
+ # Prevent run_callback_threadsafe from scheduling any additional
+ # callbacks in the event loop as callbacks created on the futures
+ # it returns will never run after the final `self.async_block_till_done`
+ # which will cause the futures to block forever when waiting for
+ # the `result()` which will cause a deadlock when shutting down the executor.
+ shutdown_run_callback_threadsafe(self.loop)
+
try:
async with self.timeout.async_timeout(30):
await self.async_block_till_done()
@@ -568,10 +577,10 @@ class Context:
"""The context that triggered something."""
user_id: str = attr.ib(default=None)
- parent_id: Optional[str] = attr.ib(default=None)
+ parent_id: str | None = attr.ib(default=None)
id: str = attr.ib(factory=uuid_util.random_uuid_hex)
- def as_dict(self) -> Dict[str, Optional[str]]:
+ def as_dict(self) -> dict[str, str | None]:
"""Return a dictionary representation of the context."""
return {"id": self.id, "parent_id": self.parent_id, "user_id": self.user_id}
@@ -584,7 +593,7 @@ class EventOrigin(enum.Enum):
def __str__(self) -> str: # pylint: disable=invalid-str-returned
"""Return the event."""
- return self.value # type: ignore
+ return self.value
class Event:
@@ -595,10 +604,10 @@ class Event:
def __init__(
self,
event_type: str,
- data: Optional[Dict[str, Any]] = None,
+ data: dict[str, Any] | None = None,
origin: EventOrigin = EventOrigin.local,
- time_fired: Optional[datetime.datetime] = None,
- context: Optional[Context] = None,
+ time_fired: datetime.datetime | None = None,
+ context: Context | None = None,
) -> None:
"""Initialize a new event."""
self.event_type = event_type
@@ -612,7 +621,7 @@ def __hash__(self) -> int:
# The only event type that shares context are the TIME_CHANGED
return hash((self.event_type, self.context.id, self.time_fired))
- def as_dict(self) -> Dict[str, Any]:
+ def as_dict(self) -> dict[str, Any]:
"""Create a dict representation of this Event.
Async friendly.
@@ -627,7 +636,6 @@ def as_dict(self) -> Dict[str, Any]:
def __repr__(self) -> str:
"""Return the representation."""
- # pylint: disable=maybe-no-member
if self.data:
return f""
@@ -650,11 +658,11 @@ class EventBus:
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize a new event bus."""
- self._listeners: Dict[str, List[HassJob]] = {}
+ self._listeners: dict[str, list[tuple[HassJob, Callable | None]]] = {}
self._hass = hass
@callback
- def async_listeners(self) -> Dict[str, int]:
+ def async_listeners(self) -> dict[str, int]:
"""Return dictionary with events and the number of listeners.
This method must be run in the event loop.
@@ -662,16 +670,16 @@ def async_listeners(self) -> Dict[str, int]:
return {key: len(self._listeners[key]) for key in self._listeners}
@property
- def listeners(self) -> Dict[str, int]:
+ def listeners(self) -> dict[str, int]:
"""Return dictionary with events and the number of listeners."""
return run_callback_threadsafe(self._hass.loop, self.async_listeners).result()
def fire(
self,
event_type: str,
- event_data: Optional[Dict] = None,
+ event_data: dict | None = None,
origin: EventOrigin = EventOrigin.local,
- context: Optional[Context] = None,
+ context: Context | None = None,
) -> None:
"""Fire an event."""
self._hass.loop.call_soon_threadsafe(
@@ -682,15 +690,18 @@ def fire(
def async_fire(
self,
event_type: str,
- event_data: Optional[Dict[str, Any]] = None,
+ event_data: dict[str, Any] | None = None,
origin: EventOrigin = EventOrigin.local,
- context: Optional[Context] = None,
- time_fired: Optional[datetime.datetime] = None,
+ context: Context | None = None,
+ time_fired: datetime.datetime | None = None,
) -> None:
"""Fire an event.
This method must be run in the event loop.
"""
+ if len(event_type) > MAX_LENGTH_EVENT_TYPE:
+ raise MaxLengthExceeded(event_type, "event_type", MAX_LENGTH_EVENT_TYPE)
+
listeners = self._listeners.get(event_type, [])
# EVENT_HOMEASSISTANT_CLOSE should go only to his listeners
@@ -706,7 +717,14 @@ def async_fire(
if not listeners:
return
- for job in listeners:
+ for job, event_filter in listeners:
+ if event_filter is not None:
+ try:
+ if not event_filter(event):
+ continue
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Error in event filter")
+ continue
self._hass.async_add_hass_job(job, event)
def listen(self, event_type: str, listener: Callable) -> CALLBACK_TYPE:
@@ -726,23 +744,38 @@ def remove_listener() -> None:
return remove_listener
@callback
- def async_listen(self, event_type: str, listener: Callable) -> CALLBACK_TYPE:
+ def async_listen(
+ self,
+ event_type: str,
+ listener: Callable,
+ event_filter: Callable | None = None,
+ ) -> CALLBACK_TYPE:
"""Listen for all events or events of a specific type.
To listen to all events specify the constant ``MATCH_ALL``
as event_type.
+ An optional event_filter, which must be a callable decorated with
+ @callback that returns a boolean value, determines if the
+ listener callable should run.
+
This method must be run in the event loop.
"""
- return self._async_listen_job(event_type, HassJob(listener))
+ if event_filter is not None and not is_callback(event_filter):
+ raise HomeAssistantError(f"Event filter {event_filter} is not a callback")
+ return self._async_listen_filterable_job(
+ event_type, (HassJob(listener), event_filter)
+ )
@callback
- def _async_listen_job(self, event_type: str, hassjob: HassJob) -> CALLBACK_TYPE:
- self._listeners.setdefault(event_type, []).append(hassjob)
+ def _async_listen_filterable_job(
+ self, event_type: str, filterable_job: tuple[HassJob, Callable | None]
+ ) -> CALLBACK_TYPE:
+ self._listeners.setdefault(event_type, []).append(filterable_job)
def remove_listener() -> None:
"""Remove the listener."""
- self._async_remove_listener(event_type, hassjob)
+ self._async_remove_listener(event_type, filterable_job)
return remove_listener
@@ -775,12 +808,12 @@ def async_listen_once(self, event_type: str, listener: Callable) -> CALLBACK_TYP
This method must be run in the event loop.
"""
- job: Optional[HassJob] = None
+ filterable_job: tuple[HassJob, Callable | None] | None = None
@callback
def _onetime_listener(event: Event) -> None:
"""Remove listener from event bus and then fire listener."""
- nonlocal job
+ nonlocal filterable_job
if hasattr(_onetime_listener, "run"):
return
# Set variable so that we will never run twice.
@@ -789,22 +822,24 @@ def _onetime_listener(event: Event) -> None:
# multiple times as well.
# This will make sure the second time it does nothing.
setattr(_onetime_listener, "run", True)
- assert job is not None
- self._async_remove_listener(event_type, job)
+ assert filterable_job is not None
+ self._async_remove_listener(event_type, filterable_job)
self._hass.async_run_job(listener, event)
- job = HassJob(_onetime_listener)
+ filterable_job = (HassJob(_onetime_listener), None)
- return self._async_listen_job(event_type, job)
+ return self._async_listen_filterable_job(event_type, filterable_job)
@callback
- def _async_remove_listener(self, event_type: str, hassjob: HassJob) -> None:
+ def _async_remove_listener(
+ self, event_type: str, filterable_job: tuple[HassJob, Callable | None]
+ ) -> None:
"""Remove a listener of a specific event_type.
This method must be run in the event loop.
"""
try:
- self._listeners[event_type].remove(hassjob)
+ self._listeners[event_type].remove(filterable_job)
# delete event_type list if empty
if not self._listeners[event_type]:
@@ -812,7 +847,9 @@ def _async_remove_listener(self, event_type: str, hassjob: HassJob) -> None:
except (KeyError, ValueError):
# KeyError is key event_type listener did not exist
# ValueError if listener did not exist within event_type
- _LOGGER.exception("Unable to remove unknown job listener %s", hassjob)
+ _LOGGER.exception(
+ "Unable to remove unknown job listener %s", filterable_job
+ )
class State:
@@ -844,11 +881,11 @@ def __init__(
self,
entity_id: str,
state: str,
- attributes: Optional[Mapping[str, Any]] = None,
- last_changed: Optional[datetime.datetime] = None,
- last_updated: Optional[datetime.datetime] = None,
- context: Optional[Context] = None,
- validate_entity_id: Optional[bool] = True,
+ attributes: Mapping[str, Any] | None = None,
+ last_changed: datetime.datetime | None = None,
+ last_updated: datetime.datetime | None = None,
+ context: Context | None = None,
+ validate_entity_id: bool | None = True,
) -> None:
"""Initialize a new state."""
state = str(state)
@@ -872,7 +909,7 @@ def __init__(
self.last_changed = last_changed or self.last_updated
self.context = context or Context()
self.domain, self.object_id = split_entity_id(self.entity_id)
- self._as_dict: Optional[Dict[str, Collection[Any]]] = None
+ self._as_dict: dict[str, Collection[Any]] | None = None
@property
def name(self) -> str:
@@ -881,7 +918,7 @@ def name(self) -> str:
"_", " "
)
- def as_dict(self) -> Dict:
+ def as_dict(self) -> dict:
"""Return a dict representation of the State.
Async friendly.
@@ -906,7 +943,7 @@ def as_dict(self) -> Dict:
return self._as_dict
@classmethod
- def from_dict(cls, json_dict: Dict) -> Any:
+ def from_dict(cls, json_dict: dict) -> Any:
"""Initialize a state from a dict.
Async friendly.
@@ -964,12 +1001,12 @@ class StateMachine:
def __init__(self, bus: EventBus, loop: asyncio.events.AbstractEventLoop) -> None:
"""Initialize state machine."""
- self._states: Dict[str, State] = {}
- self._reservations: Set[str] = set()
+ self._states: dict[str, State] = {}
+ self._reservations: set[str] = set()
self._bus = bus
self._loop = loop
- def entity_ids(self, domain_filter: Optional[str] = None) -> List[str]:
+ def entity_ids(self, domain_filter: str | None = None) -> list[str]:
"""List of entity ids that are being tracked."""
future = run_callback_threadsafe(
self._loop, self.async_entity_ids, domain_filter
@@ -978,8 +1015,8 @@ def entity_ids(self, domain_filter: Optional[str] = None) -> List[str]:
@callback
def async_entity_ids(
- self, domain_filter: Optional[Union[str, Iterable]] = None
- ) -> List[str]:
+ self, domain_filter: str | Iterable | None = None
+ ) -> list[str]:
"""List of entity ids that are being tracked.
This method must be run in the event loop.
@@ -998,7 +1035,7 @@ def async_entity_ids(
@callback
def async_entity_ids_count(
- self, domain_filter: Optional[Union[str, Iterable]] = None
+ self, domain_filter: str | Iterable | None = None
) -> int:
"""Count the entity ids that are being tracked.
@@ -1014,16 +1051,14 @@ def async_entity_ids_count(
[None for state in self._states.values() if state.domain in domain_filter]
)
- def all(self, domain_filter: Optional[Union[str, Iterable]] = None) -> List[State]:
+ def all(self, domain_filter: str | Iterable | None = None) -> list[State]:
"""Create a list of all states."""
return run_callback_threadsafe(
self._loop, self.async_all, domain_filter
).result()
@callback
- def async_all(
- self, domain_filter: Optional[Union[str, Iterable]] = None
- ) -> List[State]:
+ def async_all(self, domain_filter: str | Iterable | None = None) -> list[State]:
"""Create a list of all states matching the filter.
This method must be run in the event loop.
@@ -1038,7 +1073,7 @@ def async_all(
state for state in self._states.values() if state.domain in domain_filter
]
- def get(self, entity_id: str) -> Optional[State]:
+ def get(self, entity_id: str) -> State | None:
"""Retrieve state of entity_id or None if not found.
Async friendly.
@@ -1063,7 +1098,7 @@ def remove(self, entity_id: str) -> bool:
).result()
@callback
- def async_remove(self, entity_id: str, context: Optional[Context] = None) -> bool:
+ def async_remove(self, entity_id: str, context: Context | None = None) -> bool:
"""Remove the state of an entity.
Returns boolean to indicate if an entity was removed.
@@ -1091,9 +1126,9 @@ def set(
self,
entity_id: str,
new_state: str,
- attributes: Optional[Mapping[str, Any]] = None,
+ attributes: Mapping[str, Any] | None = None,
force_update: bool = False,
- context: Optional[Context] = None,
+ context: Context | None = None,
) -> None:
"""Set the state of an entity, add entity if it does not exist.
@@ -1140,9 +1175,9 @@ def async_set(
self,
entity_id: str,
new_state: str,
- attributes: Optional[Mapping[str, Any]] = None,
+ attributes: Mapping[str, Any] | None = None,
force_update: bool = False,
- context: Optional[Context] = None,
+ context: Context | None = None,
) -> None:
"""Set the state of an entity, add entity if it does not exist.
@@ -1201,8 +1236,8 @@ class Service:
def __init__(
self,
func: Callable,
- schema: Optional[vol.Schema],
- context: Optional[Context] = None,
+ schema: vol.Schema | None,
+ context: Context | None = None,
) -> None:
"""Initialize a service."""
self.job = HassJob(func)
@@ -1218,8 +1253,8 @@ def __init__(
self,
domain: str,
service: str,
- data: Optional[Dict] = None,
- context: Optional[Context] = None,
+ data: dict | None = None,
+ context: Context | None = None,
) -> None:
"""Initialize a service call."""
self.domain = domain.lower()
@@ -1243,16 +1278,16 @@ class ServiceRegistry:
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize a service registry."""
- self._services: Dict[str, Dict[str, Service]] = {}
+ self._services: dict[str, dict[str, Service]] = {}
self._hass = hass
@property
- def services(self) -> Dict[str, Dict[str, Service]]:
+ def services(self) -> dict[str, dict[str, Service]]:
"""Return dictionary with per domain a list of available services."""
return run_callback_threadsafe(self._hass.loop, self.async_services).result()
@callback
- def async_services(self) -> Dict[str, Dict[str, Service]]:
+ def async_services(self) -> dict[str, dict[str, Service]]:
"""Return dictionary with per domain a list of available services.
This method must be run in the event loop.
@@ -1271,7 +1306,7 @@ def register(
domain: str,
service: str,
service_func: Callable,
- schema: Optional[vol.Schema] = None,
+ schema: vol.Schema | None = None,
) -> None:
"""
Register a service.
@@ -1288,7 +1323,7 @@ def async_register(
domain: str,
service: str,
service_func: Callable,
- schema: Optional[vol.Schema] = None,
+ schema: vol.Schema | None = None,
) -> None:
"""
Register a service.
@@ -1342,18 +1377,21 @@ def call(
self,
domain: str,
service: str,
- service_data: Optional[Dict] = None,
+ service_data: dict | None = None,
blocking: bool = False,
- context: Optional[Context] = None,
- limit: Optional[float] = SERVICE_CALL_LIMIT,
- ) -> Optional[bool]:
+ context: Context | None = None,
+ limit: float | None = SERVICE_CALL_LIMIT,
+ target: dict | None = None,
+ ) -> bool | None:
"""
Call a service.
See description of async_call for details.
"""
return asyncio.run_coroutine_threadsafe(
- self.async_call(domain, service, service_data, blocking, context, limit),
+ self.async_call(
+ domain, service, service_data, blocking, context, limit, target
+ ),
self._hass.loop,
).result()
@@ -1361,11 +1399,12 @@ async def async_call(
self,
domain: str,
service: str,
- service_data: Optional[Dict] = None,
+ service_data: dict | None = None,
blocking: bool = False,
- context: Optional[Context] = None,
- limit: Optional[float] = SERVICE_CALL_LIMIT,
- ) -> Optional[bool]:
+ context: Context | None = None,
+ limit: float | None = SERVICE_CALL_LIMIT,
+ target: dict | None = None,
+ ) -> bool | None:
"""
Call a service.
@@ -1392,6 +1431,9 @@ async def async_call(
except KeyError:
raise ServiceNotFound(domain, service) from None
+ if target:
+ service_data.update(target)
+
if handler.schema:
try:
processed_data = handler.schema(service_data)
@@ -1450,7 +1492,7 @@ async def async_call(
return False
def _run_service_in_background(
- self, coro_or_task: Union[Coroutine, asyncio.Task], service_call: ServiceCall
+ self, coro_or_task: Coroutine | asyncio.Task, service_call: ServiceCall
) -> None:
"""Run service call in background, catching and logging any exceptions."""
@@ -1495,8 +1537,8 @@ def __init__(self, hass: HomeAssistant) -> None:
self.location_name: str = "Home"
self.time_zone: datetime.tzinfo = dt_util.UTC
self.units: UnitSystem = METRIC_SYSTEM
- self.internal_url: Optional[str] = None
- self.external_url: Optional[str] = None
+ self.internal_url: str | None = None
+ self.external_url: str | None = None
self.config_source: str = "default"
@@ -1504,22 +1546,22 @@ def __init__(self, hass: HomeAssistant) -> None:
self.skip_pip: bool = False
# List of loaded components
- self.components: Set[str] = set()
+ self.components: set[str] = set()
# API (HTTP) server configuration, see components.http.ApiConfig
- self.api: Optional[Any] = None
+ self.api: Any | None = None
# Directory that holds the configuration
- self.config_dir: Optional[str] = None
+ self.config_dir: str | None = None
# List of allowed external dirs to access
- self.allowlist_external_dirs: Set[str] = set()
+ self.allowlist_external_dirs: set[str] = set()
# List of allowed external URLs that integrations may use
- self.allowlist_external_urls: Set[str] = set()
+ self.allowlist_external_urls: set[str] = set()
# Dictionary of Media folders that integrations may use
- self.media_dirs: Dict[str, str] = {}
+ self.media_dirs: dict[str, str] = {}
# If Home Assistant is running in safe mode
self.safe_mode: bool = False
@@ -1527,7 +1569,7 @@ def __init__(self, hass: HomeAssistant) -> None:
# Use legacy template behavior
self.legacy_templates: bool = False
- def distance(self, lat: float, lon: float) -> Optional[float]:
+ def distance(self, lat: float, lon: float) -> float | None:
"""Calculate distance from Home Assistant.
Async friendly.
@@ -1578,7 +1620,7 @@ def is_allowed_path(self, path: str) -> bool:
return False
- def as_dict(self) -> Dict:
+ def as_dict(self) -> dict:
"""Create a dictionary representation of the configuration.
Async friendly.
@@ -1623,15 +1665,15 @@ def _update(
self,
*,
source: str,
- latitude: Optional[float] = None,
- longitude: Optional[float] = None,
- elevation: Optional[int] = None,
- unit_system: Optional[str] = None,
- location_name: Optional[str] = None,
- time_zone: Optional[str] = None,
+ latitude: float | None = None,
+ longitude: float | None = None,
+ elevation: int | None = None,
+ unit_system: str | None = None,
+ location_name: str | None = None,
+ time_zone: str | None = None,
# pylint: disable=dangerous-default-value # _UNDEFs not modified
- external_url: Optional[Union[str, dict]] = _UNDEF,
- internal_url: Optional[Union[str, dict]] = _UNDEF,
+ external_url: str | dict | None = _UNDEF,
+ internal_url: str | dict | None = _UNDEF,
) -> None:
"""Update the configuration from a dictionary."""
self.config_source = source
@@ -1668,39 +1710,7 @@ async def async_load(self) -> None:
)
data = await store.async_load()
- async def migrate_base_url(_: Event) -> None:
- """Migrate base_url to internal_url/external_url."""
- if self.hass.config.api is None:
- return
-
- base_url = yarl.URL(self.hass.config.api.deprecated_base_url)
-
- # Check if this is an internal URL
- if str(base_url.host).endswith(".local") or (
- network.is_ip_address(str(base_url.host))
- and network.is_private(ip_address(base_url.host))
- ):
- await self.async_update(
- internal_url=network.normalize_url(str(base_url))
- )
- return
-
- # External, ensure this is not a loopback address
- if not (
- network.is_ip_address(str(base_url.host))
- and network.is_loopback(ip_address(base_url.host))
- ):
- await self.async_update(
- external_url=network.normalize_url(str(base_url))
- )
-
if data:
- # Try to migrate base_url to internal_url/external_url
- if "external_url" not in data:
- self.hass.bus.async_listen_once(
- EVENT_HOMEASSISTANT_START, migrate_base_url
- )
-
self._update(
source=SOURCE_STORAGE,
latitude=data.get("latitude"),
diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py
index c5b67ff16e8c94..40c9ace0f8dee6 100644
--- a/homeassistant/data_entry_flow.py
+++ b/homeassistant/data_entry_flow.py
@@ -1,7 +1,10 @@
"""Classes to help gather user submissions."""
+from __future__ import annotations
+
import abc
import asyncio
-from typing import Any, Dict, List, Optional, cast
+from types import MappingProxyType
+from typing import Any
import uuid
import voluptuous as vol
@@ -40,7 +43,7 @@ class UnknownStep(FlowError):
class AbortFlow(FlowError):
"""Exception to indicate a flow needs to be aborted."""
- def __init__(self, reason: str, description_placeholders: Optional[Dict] = None):
+ def __init__(self, reason: str, description_placeholders: dict | None = None):
"""Initialize an abort flow exception."""
super().__init__(f"Flow aborted: {reason}")
self.reason = reason
@@ -56,8 +59,8 @@ def __init__(
) -> None:
"""Initialize the flow manager."""
self.hass = hass
- self._initializing: Dict[str, List[asyncio.Future]] = {}
- self._progress: Dict[str, Any] = {}
+ self._initializing: dict[str, list[asyncio.Future]] = {}
+ self._progress: dict[str, Any] = {}
async def async_wait_init_flow_finish(self, handler: str) -> None:
"""Wait till all flows in progress are initialized."""
@@ -73,9 +76,9 @@ async def async_create_flow(
self,
handler_key: Any,
*,
- context: Optional[Dict[str, Any]] = None,
- data: Optional[Dict[str, Any]] = None,
- ) -> "FlowHandler":
+ context: dict[str, Any] | None = None,
+ data: dict[str, Any] | None = None,
+ ) -> FlowHandler:
"""Create a flow for specified handler.
Handler key is the domain of the component that we want to set up.
@@ -83,31 +86,29 @@ async def async_create_flow(
@abc.abstractmethod
async def async_finish_flow(
- self, flow: "FlowHandler", result: Dict[str, Any]
- ) -> Dict[str, Any]:
+ self, flow: FlowHandler, result: dict[str, Any]
+ ) -> dict[str, Any]:
"""Finish a config flow and add an entry."""
- async def async_post_init(
- self, flow: "FlowHandler", result: Dict[str, Any]
- ) -> None:
+ async def async_post_init(self, flow: FlowHandler, result: dict[str, Any]) -> None:
"""Entry has finished executing its first step asynchronously."""
@callback
- def async_progress(self) -> List[Dict]:
+ def async_progress(self, include_uninitialized: bool = False) -> list[dict]:
"""Return the flows in progress."""
return [
{
"flow_id": flow.flow_id,
"handler": flow.handler,
"context": flow.context,
- "step_id": flow.cur_step["step_id"],
+ "step_id": flow.cur_step["step_id"] if flow.cur_step else None,
}
for flow in self._progress.values()
- if flow.cur_step is not None
+ if include_uninitialized or flow.cur_step is not None
]
async def async_init(
- self, handler: str, *, context: Optional[Dict] = None, data: Any = None
+ self, handler: str, *, context: dict | None = None, data: Any = None
) -> Any:
"""Start a configuration flow."""
if context is None:
@@ -139,7 +140,7 @@ async def async_init(
return result
async def async_configure(
- self, flow_id: str, user_input: Optional[Dict] = None
+ self, flow_id: str, user_input: dict | None = None
) -> Any:
"""Continue a configuration flow."""
flow = self._progress.get(flow_id)
@@ -195,9 +196,9 @@ async def _async_handle_step(
self,
flow: Any,
step_id: str,
- user_input: Optional[Dict],
- step_done: Optional[asyncio.Future] = None,
- ) -> Dict:
+ user_input: dict | None,
+ step_done: asyncio.Future | None = None,
+ ) -> dict:
"""Handle a step of a flow."""
method = f"async_step_{step_id}"
@@ -210,7 +211,7 @@ async def _async_handle_step(
)
try:
- result: Dict = await getattr(flow, method)(user_input)
+ result: dict = await getattr(flow, method)(user_input)
except AbortFlow as err:
result = _create_abort_data(
flow.flow_id, flow.handler, err.reason, err.description_placeholders
@@ -262,11 +263,13 @@ class FlowHandler:
"""Handle the configuration flow of a component."""
# Set by flow manager
+ cur_step: dict[str, str] | None = None
+ # Ignore types: https://github.com/PyCQA/pylint/issues/3167
flow_id: str = None # type: ignore
- hass: Optional[HomeAssistant] = None
- handler: Optional[str] = None
- cur_step: Optional[Dict[str, str]] = None
- context: Dict
+ hass: HomeAssistant = None # type: ignore
+ handler: str = None # type: ignore
+ # Ensure the attribute has a subscriptable, but immutable, default value.
+ context: dict = MappingProxyType({}) # type: ignore
# Set by _async_create_flow callback
init_step = "init"
@@ -275,7 +278,7 @@ class FlowHandler:
VERSION = 1
@property
- def source(self) -> Optional[str]:
+ def source(self) -> str | None:
"""Source that initialized the flow."""
if not hasattr(self, "context"):
return None
@@ -296,9 +299,9 @@ def async_show_form(
*,
step_id: str,
data_schema: vol.Schema = None,
- errors: Optional[Dict] = None,
- description_placeholders: Optional[Dict] = None,
- ) -> Dict[str, Any]:
+ errors: dict | None = None,
+ description_placeholders: dict | None = None,
+ ) -> dict[str, Any]:
"""Return the definition of a form to gather user input."""
return {
"type": RESULT_TYPE_FORM,
@@ -315,10 +318,10 @@ def async_create_entry(
self,
*,
title: str,
- data: Dict,
- description: Optional[str] = None,
- description_placeholders: Optional[Dict] = None,
- ) -> Dict[str, Any]:
+ data: dict,
+ description: str | None = None,
+ description_placeholders: dict | None = None,
+ ) -> dict[str, Any]:
"""Finish config flow and create a config entry."""
return {
"version": self.VERSION,
@@ -333,17 +336,17 @@ def async_create_entry(
@callback
def async_abort(
- self, *, reason: str, description_placeholders: Optional[Dict] = None
- ) -> Dict[str, Any]:
+ self, *, reason: str, description_placeholders: dict | None = None
+ ) -> dict[str, Any]:
"""Abort the config flow."""
return _create_abort_data(
- self.flow_id, cast(str, self.handler), reason, description_placeholders
+ self.flow_id, self.handler, reason, description_placeholders
)
@callback
def async_external_step(
- self, *, step_id: str, url: str, description_placeholders: Optional[Dict] = None
- ) -> Dict[str, Any]:
+ self, *, step_id: str, url: str, description_placeholders: dict | None = None
+ ) -> dict[str, Any]:
"""Return the definition of an external step for the user to take."""
return {
"type": RESULT_TYPE_EXTERNAL_STEP,
@@ -355,7 +358,7 @@ def async_external_step(
}
@callback
- def async_external_step_done(self, *, next_step_id: str) -> Dict[str, Any]:
+ def async_external_step_done(self, *, next_step_id: str) -> dict[str, Any]:
"""Return the definition of an external step for the user to take."""
return {
"type": RESULT_TYPE_EXTERNAL_STEP_DONE,
@@ -370,8 +373,8 @@ def async_show_progress(
*,
step_id: str,
progress_action: str,
- description_placeholders: Optional[Dict] = None,
- ) -> Dict[str, Any]:
+ description_placeholders: dict | None = None,
+ ) -> dict[str, Any]:
"""Show a progress message to the user, without user input allowed."""
return {
"type": RESULT_TYPE_SHOW_PROGRESS,
@@ -383,7 +386,7 @@ def async_show_progress(
}
@callback
- def async_show_progress_done(self, *, next_step_id: str) -> Dict[str, Any]:
+ def async_show_progress_done(self, *, next_step_id: str) -> dict[str, Any]:
"""Mark the progress done."""
return {
"type": RESULT_TYPE_SHOW_PROGRESS_DONE,
@@ -398,8 +401,8 @@ def _create_abort_data(
flow_id: str,
handler: str,
reason: str,
- description_placeholders: Optional[Dict] = None,
-) -> Dict[str, Any]:
+ description_placeholders: dict | None = None,
+) -> dict[str, Any]:
"""Return the definition of an external step for the user to take."""
return {
"type": RESULT_TYPE_ABORT,
diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py
index e37f68a07bf2f0..b40aa99520d1d9 100644
--- a/homeassistant/exceptions.py
+++ b/homeassistant/exceptions.py
@@ -1,8 +1,12 @@
"""The exceptions used by Home Assistant."""
-from typing import TYPE_CHECKING, Optional
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Generator, Sequence
+
+import attr
if TYPE_CHECKING:
- from .core import Context # noqa: F401 pylint: disable=unused-import
+ from .core import Context
class HomeAssistantError(Exception):
@@ -25,6 +29,75 @@ def __init__(self, exception: Exception) -> None:
super().__init__(f"{exception.__class__.__name__}: {exception}")
+@attr.s
+class ConditionError(HomeAssistantError):
+ """Error during condition evaluation."""
+
+ # The type of the failed condition, such as 'and' or 'numeric_state'
+ type: str = attr.ib()
+
+ @staticmethod
+ def _indent(indent: int, message: str) -> str:
+ """Return indentation."""
+ return " " * indent + message
+
+ def output(self, indent: int) -> Generator:
+ """Yield an indented representation."""
+ raise NotImplementedError()
+
+ def __str__(self) -> str:
+ """Return string representation."""
+ return "\n".join(list(self.output(indent=0)))
+
+
+@attr.s
+class ConditionErrorMessage(ConditionError):
+ """Condition error message."""
+
+ # A message describing this error
+ message: str = attr.ib()
+
+ def output(self, indent: int) -> Generator:
+ """Yield an indented representation."""
+ yield self._indent(indent, f"In '{self.type}' condition: {self.message}")
+
+
+@attr.s
+class ConditionErrorIndex(ConditionError):
+ """Condition error with index."""
+
+ # The zero-based index of the failed condition, for conditions with multiple parts
+ index: int = attr.ib()
+ # The total number of parts in this condition, including non-failed parts
+ total: int = attr.ib()
+ # The error that this error wraps
+ error: ConditionError = attr.ib()
+
+ def output(self, indent: int) -> Generator:
+ """Yield an indented representation."""
+ if self.total > 1:
+ yield self._indent(
+ indent, f"In '{self.type}' (item {self.index+1} of {self.total}):"
+ )
+ else:
+ yield self._indent(indent, f"In '{self.type}':")
+
+ yield from self.error.output(indent + 1)
+
+
+@attr.s
+class ConditionErrorContainer(ConditionError):
+ """Condition error with subconditions."""
+
+ # List of ConditionErrors that this error wraps
+ errors: Sequence[ConditionError] = attr.ib()
+
+ def output(self, indent: int) -> Generator:
+ """Yield an indented representation."""
+ for item in self.errors:
+ yield from item.output(indent)
+
+
class PlatformNotReady(HomeAssistantError):
"""Error to indicate that platform is not ready."""
@@ -42,12 +115,12 @@ class Unauthorized(HomeAssistantError):
def __init__(
self,
- context: Optional["Context"] = None,
- user_id: Optional[str] = None,
- entity_id: Optional[str] = None,
- config_entry_id: Optional[str] = None,
- perm_category: Optional[str] = None,
- permission: Optional[str] = None,
+ context: Context | None = None,
+ user_id: str | None = None,
+ entity_id: str | None = None,
+ config_entry_id: str | None = None,
+ perm_category: str | None = None,
+ permission: str | None = None,
) -> None:
"""Unauthorized error."""
super().__init__(self.__class__.__name__)
@@ -81,3 +154,20 @@ def __init__(self, domain: str, service: str) -> None:
def __str__(self) -> str:
"""Return string representation."""
return f"Unable to find service {self.domain}.{self.service}"
+
+
+class MaxLengthExceeded(HomeAssistantError):
+ """Raised when a property value has exceeded the max character length."""
+
+ def __init__(self, value: str, property_name: str, max_length: int) -> None:
+ """Initialize error."""
+ super().__init__(
+ self,
+ (
+ f"Value {value} for property {property_name} has a max length of "
+ f"{max_length} characters"
+ ),
+ )
+ self.value = value
+ self.property_name = property_name
+ self.max_length = max_length
diff --git a/homeassistant/generated/__init__.py b/homeassistant/generated/__init__.py
new file mode 100644
index 00000000000000..b86c779f9b84ec
--- /dev/null
+++ b/homeassistant/generated/__init__.py
@@ -0,0 +1,4 @@
+"""All files in this module are automatically generated by hassfest.
+
+To update, run python3 -m script.hassfest
+"""
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 4c12ff30e49690..808f18c319d506 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -11,6 +11,7 @@
"acmeda",
"adguard",
"advantage_air",
+ "aemet",
"agent_dvr",
"airly",
"airnow",
@@ -21,6 +22,7 @@
"ambient_station",
"apple_tv",
"arcam_fmj",
+ "asuswrt",
"atag",
"august",
"aurora",
@@ -38,6 +40,7 @@
"canary",
"cast",
"cert_expiry",
+ "climacell",
"cloudflare",
"control4",
"coolmaster",
@@ -57,10 +60,14 @@
"econet",
"elgato",
"elkm1",
+ "emonitor",
"emulated_roku",
"enocean",
+ "enphase_envoy",
"epson",
"esphome",
+ "ezviz",
+ "faa_delays",
"fireservicerota",
"flick_electric",
"flo",
@@ -80,16 +87,19 @@
"glances",
"goalzero",
"gogogate2",
+ "google_travel_time",
"gpslogger",
"gree",
- "griddy",
"guardian",
+ "habitica",
"hangouts",
"harmony",
"heos",
"hisense_aehw4a1",
+ "hive",
"hlk_sw16",
"home_connect",
+ "home_plus_control",
"homekit",
"homekit_controller",
"homematicip_cloud",
@@ -111,19 +121,27 @@
"isy994",
"izone",
"juicenet",
+ "keenetic_ndms2",
+ "kmtronic",
"kodi",
"konnected",
+ "kostal_plenticore",
"kulersky",
"life360",
"lifx",
+ "litejet",
+ "litterrobot",
"local_ip",
"locative",
"logi_circle",
"luftdaten",
"lutron_caseta",
+ "lyric",
"mailgun",
+ "mazda",
"melcloud",
"met",
+ "met_eireann",
"meteo_france",
"metoffice",
"mikrotik",
@@ -133,7 +151,9 @@
"monoprice",
"motion_blinds",
"mqtt",
+ "mullvad",
"myq",
+ "mysensors",
"neato",
"nest",
"netatmo",
@@ -156,6 +176,7 @@
"owntracks",
"ozw",
"panasonic_viera",
+ "philips_js",
"pi_hole",
"plaato",
"plex",
@@ -174,12 +195,14 @@
"rfxtrx",
"ring",
"risco",
+ "rituals_perfume_genie",
"roku",
"roomba",
"roon",
"rpi_power",
"ruckus_unleashed",
"samsungtv",
+ "screenlogic",
"sense",
"sentry",
"sharkiq",
@@ -190,6 +213,7 @@
"smart_meter_texas",
"smarthab",
"smartthings",
+ "smarttub",
"smhi",
"sms",
"solaredge",
@@ -206,6 +230,7 @@
"squeezebox",
"srp_energy",
"starline",
+ "subaru",
"syncthru",
"synology_dsm",
"tado",
@@ -230,10 +255,12 @@
"upnp",
"velbus",
"vera",
+ "verisure",
"vesync",
"vilfo",
"vizio",
"volumio",
+ "waze_travel_time",
"wemo",
"wiffi",
"wilight",
diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py
index 0b6f5166f8815c..4d4e3688c1ba4f 100644
--- a/homeassistant/generated/dhcp.py
+++ b/homeassistant/generated/dhcp.py
@@ -16,6 +16,11 @@
"hostname": "connect",
"macaddress": "B8B7F1*"
},
+ {
+ "domain": "august",
+ "hostname": "august*",
+ "macaddress": "E076D0*"
+ },
{
"domain": "axis",
"hostname": "axis-00408c*",
@@ -31,6 +36,32 @@
"hostname": "axis-b8a44f*",
"macaddress": "B8A44F*"
},
+ {
+ "domain": "blink",
+ "hostname": "blink*",
+ "macaddress": "B85F98*"
+ },
+ {
+ "domain": "broadlink",
+ "macaddress": "34EA34*"
+ },
+ {
+ "domain": "broadlink",
+ "macaddress": "24DFA7*"
+ },
+ {
+ "domain": "broadlink",
+ "macaddress": "A043B0*"
+ },
+ {
+ "domain": "broadlink",
+ "macaddress": "B4430D*"
+ },
+ {
+ "domain": "emonitor",
+ "hostname": "emonitor*",
+ "macaddress": "0090C2*"
+ },
{
"domain": "flume",
"hostname": "flume-gw-*",
@@ -41,6 +72,21 @@
"hostname": "flume-gw-*",
"macaddress": "B4E62D*"
},
+ {
+ "domain": "lyric",
+ "hostname": "lyric-*",
+ "macaddress": "48A2E6"
+ },
+ {
+ "domain": "lyric",
+ "hostname": "lyric-*",
+ "macaddress": "B82CA0"
+ },
+ {
+ "domain": "lyric",
+ "hostname": "lyric-*",
+ "macaddress": "00D02D"
+ },
{
"domain": "nest",
"macaddress": "18B430*"
@@ -55,6 +101,10 @@
"hostname": "nuheat",
"macaddress": "002338*"
},
+ {
+ "domain": "nuki",
+ "hostname": "nuki_bridge_*"
+ },
{
"domain": "powerwall",
"hostname": "1118431-*",
@@ -90,6 +140,16 @@
"hostname": "irobot-*",
"macaddress": "501479*"
},
+ {
+ "domain": "roomba",
+ "hostname": "roomba-*",
+ "macaddress": "80A589*"
+ },
+ {
+ "domain": "screenlogic",
+ "hostname": "pentair: *",
+ "macaddress": "00C033*"
+ },
{
"domain": "sense",
"hostname": "sense-*",
@@ -134,5 +194,9 @@
"domain": "toon",
"hostname": "eneco-*",
"macaddress": "74C63B*"
+ },
+ {
+ "domain": "verisure",
+ "macaddress": "0023C1*"
}
]
diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py
index 5521ab9da8f21a..b3fa7064aee34d 100644
--- a/homeassistant/generated/zeroconf.py
+++ b/homeassistant/generated/zeroconf.py
@@ -54,9 +54,23 @@
"domain": "elgato"
}
],
+ "_enphase-envoy._tcp.local.": [
+ {
+ "domain": "enphase_envoy"
+ }
+ ],
"_esphomelib._tcp.local.": [
{
"domain": "esphome"
+ },
+ {
+ "domain": "zha",
+ "name": "tube*"
+ }
+ ],
+ "_fbx-api._tcp.local.": [
+ {
+ "domain": "freebox"
}
],
"_googlecast._tcp.local.": [
diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py
index 6f22be8d323d3e..a1964c432fc0df 100644
--- a/homeassistant/helpers/__init__.py
+++ b/homeassistant/helpers/__init__.py
@@ -1,6 +1,8 @@
"""Helper methods for components within Home Assistant."""
+from __future__ import annotations
+
import re
-from typing import TYPE_CHECKING, Any, Iterable, Sequence, Tuple
+from typing import TYPE_CHECKING, Any, Iterable, Sequence
from homeassistant.const import CONF_PLATFORM
@@ -8,7 +10,7 @@
from .typing import ConfigType
-def config_per_platform(config: "ConfigType", domain: str) -> Iterable[Tuple[Any, Any]]:
+def config_per_platform(config: ConfigType, domain: str) -> Iterable[tuple[Any, Any]]:
"""Break a component config into different platforms.
For example, will find 'switch', 'switch 2', 'switch 3', .. etc
@@ -32,7 +34,7 @@ def config_per_platform(config: "ConfigType", domain: str) -> Iterable[Tuple[Any
yield platform, item
-def extract_domain_configs(config: "ConfigType", domain: str) -> Sequence[str]:
+def extract_domain_configs(config: ConfigType, domain: str) -> Sequence[str]:
"""Extract keys from config for given domain name.
Async friendly.
diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py
index 3e1e45e5981159..f3ded75062e2b4 100644
--- a/homeassistant/helpers/aiohttp_client.py
+++ b/homeassistant/helpers/aiohttp_client.py
@@ -1,8 +1,11 @@
"""Helper for aiohttp webclient stuff."""
+from __future__ import annotations
+
import asyncio
+from contextlib import suppress
from ssl import SSLContext
import sys
-from typing import Any, Awaitable, Optional, Union, cast
+from typing import Any, Awaitable, cast
import aiohttp
from aiohttp import web
@@ -11,9 +14,8 @@
import async_timeout
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__
-from homeassistant.core import Event, callback
+from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers.frame import warn_use
-from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.loader import bind_hass
from homeassistant.util import ssl as ssl_util
@@ -29,16 +31,15 @@
@callback
@bind_hass
def async_get_clientsession(
- hass: HomeAssistantType, verify_ssl: bool = True
+ hass: HomeAssistant, verify_ssl: bool = True
) -> aiohttp.ClientSession:
"""Return default aiohttp ClientSession.
This method must be run in the event loop.
"""
+ key = DATA_CLIENTSESSION_NOTVERIFY
if verify_ssl:
key = DATA_CLIENTSESSION
- else:
- key = DATA_CLIENTSESSION_NOTVERIFY
if key not in hass.data:
hass.data[key] = async_create_clientsession(hass, verify_ssl)
@@ -49,7 +50,7 @@ def async_get_clientsession(
@callback
@bind_hass
def async_create_clientsession(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
verify_ssl: bool = True,
auto_cleanup: bool = True,
**kwargs: Any,
@@ -82,12 +83,12 @@ def async_create_clientsession(
@bind_hass
async def async_aiohttp_proxy_web(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
request: web.BaseRequest,
web_coro: Awaitable[aiohttp.ClientResponse],
buffer_size: int = 102400,
timeout: int = 10,
-) -> Optional[web.StreamResponse]:
+) -> web.StreamResponse | None:
"""Stream websession request to aiohttp web response."""
try:
with async_timeout.timeout(timeout):
@@ -115,10 +116,10 @@ async def async_aiohttp_proxy_web(
@bind_hass
async def async_aiohttp_proxy_stream(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
request: web.BaseRequest,
stream: aiohttp.StreamReader,
- content_type: Optional[str],
+ content_type: str | None,
buffer_size: int = 102400,
timeout: int = 10,
) -> web.StreamResponse:
@@ -128,7 +129,8 @@ async def async_aiohttp_proxy_stream(
response.content_type = content_type
await response.prepare(request)
- try:
+ # Suppressing something went wrong fetching data, closed connection
+ with suppress(asyncio.TimeoutError, aiohttp.ClientError):
while hass.is_running:
with async_timeout.timeout(timeout):
data = await stream.read(buffer_size)
@@ -137,16 +139,12 @@ async def async_aiohttp_proxy_stream(
break
await response.write(data)
- except (asyncio.TimeoutError, aiohttp.ClientError):
- # Something went wrong fetching data, closed connection
- pass
-
return response
@callback
def _async_register_clientsession_shutdown(
- hass: HomeAssistantType, clientsession: aiohttp.ClientSession
+ hass: HomeAssistant, clientsession: aiohttp.ClientSession
) -> None:
"""Register ClientSession close on Home Assistant shutdown.
@@ -163,7 +161,7 @@ def _async_close_websession(event: Event) -> None:
@callback
def _async_get_connector(
- hass: HomeAssistantType, verify_ssl: bool = True
+ hass: HomeAssistant, verify_ssl: bool = True
) -> aiohttp.BaseConnector:
"""Return the connector pool for aiohttp.
@@ -175,7 +173,7 @@ def _async_get_connector(
return cast(aiohttp.BaseConnector, hass.data[key])
if verify_ssl:
- ssl_context: Union[bool, SSLContext] = ssl_util.client_context()
+ ssl_context: bool | SSLContext = ssl_util.client_context()
else:
ssl_context = False
diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py
index 1a919996f86f0f..af568b404188fb 100644
--- a/homeassistant/helpers/area_registry.py
+++ b/homeassistant/helpers/area_registry.py
@@ -1,15 +1,17 @@
"""Provide a way to connect devices to one physical location."""
-from asyncio import Event, gather
+from __future__ import annotations
+
from collections import OrderedDict
-from typing import Container, Dict, Iterable, List, MutableMapping, Optional, cast
+from typing import Container, Iterable, MutableMapping, cast
import attr
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.loader import bind_hass
from homeassistant.util import slugify
-from .typing import HomeAssistantType
+# mypy: disallow-any-generics
DATA_REGISTRY = "area_registry"
EVENT_AREA_REGISTRY_UPDATED = "area_registry_updated"
@@ -23,9 +25,10 @@ class AreaEntry:
"""Area Registry Entry."""
name: str = attr.ib()
- id: Optional[str] = attr.ib(default=None)
+ normalized_name: str = attr.ib()
+ id: str | None = attr.ib(default=None)
- def generate_id(self, existing_ids: Container) -> None:
+ def generate_id(self, existing_ids: Container[str]) -> None:
"""Initialize ID."""
suggestion = suggestion_base = slugify(self.name)
tries = 1
@@ -38,48 +41,69 @@ def generate_id(self, existing_ids: Container) -> None:
class AreaRegistry:
"""Class to hold a registry of areas."""
- def __init__(self, hass: HomeAssistantType) -> None:
+ def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the area registry."""
self.hass = hass
self.areas: MutableMapping[str, AreaEntry] = {}
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
+ self._normalized_name_area_idx: dict[str, str] = {}
@callback
- def async_get_area(self, area_id: str) -> Optional[AreaEntry]:
- """Get all areas."""
+ def async_get_area(self, area_id: str) -> AreaEntry | None:
+ """Get area by id."""
return self.areas.get(area_id)
+ @callback
+ def async_get_area_by_name(self, name: str) -> AreaEntry | None:
+ """Get area by name."""
+ normalized_name = normalize_area_name(name)
+ if normalized_name not in self._normalized_name_area_idx:
+ return None
+ return self.areas[self._normalized_name_area_idx[normalized_name]]
+
@callback
def async_list_areas(self) -> Iterable[AreaEntry]:
"""Get all areas."""
return self.areas.values()
+ @callback
+ def async_get_or_create(self, name: str) -> AreaEntry:
+ """Get or create an area."""
+ area = self.async_get_area_by_name(name)
+ if area:
+ return area
+ return self.async_create(name)
+
@callback
def async_create(self, name: str) -> AreaEntry:
"""Create a new area."""
- if self._async_is_registered(name):
- raise ValueError("Name is already in use")
+ normalized_name = normalize_area_name(name)
+
+ if self.async_get_area_by_name(name):
+ raise ValueError(f"The name {name} ({normalized_name}) is already in use")
- area = AreaEntry(name=name)
+ area = AreaEntry(name=name, normalized_name=normalized_name)
area.generate_id(self.areas)
assert area.id is not None
self.areas[area.id] = area
+ self._normalized_name_area_idx[normalized_name] = area.id
self.async_schedule_save()
self.hass.bus.async_fire(
EVENT_AREA_REGISTRY_UPDATED, {"action": "create", "area_id": area.id}
)
return area
- async def async_delete(self, area_id: str) -> None:
+ @callback
+ def async_delete(self, area_id: str) -> None:
"""Delete area."""
- device_registry, entity_registry = await gather(
- self.hass.helpers.device_registry.async_get_registry(),
- self.hass.helpers.entity_registry.async_get_registry(),
- )
+ area = self.areas[area_id]
+ device_registry = dr.async_get(self.hass)
+ entity_registry = er.async_get(self.hass)
device_registry.async_clear_area_id(area_id)
entity_registry.async_clear_area_id(area_id)
del self.areas[area_id]
+ del self._normalized_name_area_idx[area.normalized_name]
self.hass.bus.async_fire(
EVENT_AREA_REGISTRY_UPDATED, {"action": "remove", "area_id": area_id}
@@ -106,23 +130,22 @@ def _async_update(self, area_id: str, name: str) -> AreaEntry:
if name == old.name:
return old
- if self._async_is_registered(name):
- raise ValueError("Name is already in use")
+ normalized_name = normalize_area_name(name)
+
+ if normalized_name != old.normalized_name and self.async_get_area_by_name(name):
+ raise ValueError(f"The name {name} ({normalized_name}) is already in use")
changes["name"] = name
+ changes["normalized_name"] = normalized_name
new = self.areas[area_id] = attr.evolve(old, **changes)
+ self._normalized_name_area_idx[
+ normalized_name
+ ] = self._normalized_name_area_idx.pop(old.normalized_name)
+
self.async_schedule_save()
return new
- @callback
- def _async_is_registered(self, name: str) -> Optional[AreaEntry]:
- """Check if a name is currently registered."""
- for area in self.areas.values():
- if name == area.name:
- return area
- return None
-
async def async_load(self) -> None:
"""Load the area registry."""
data = await self._store.async_load()
@@ -131,7 +154,11 @@ async def async_load(self) -> None:
if data is not None:
for area in data["areas"]:
- areas[area["id"]] = AreaEntry(name=area["name"], id=area["id"])
+ normalized_name = normalize_area_name(area["name"])
+ areas[area["id"]] = AreaEntry(
+ name=area["name"], id=area["id"], normalized_name=normalized_name
+ )
+ self._normalized_name_area_idx[normalized_name] = area["id"]
self.areas = areas
@@ -141,35 +168,43 @@ def async_schedule_save(self) -> None:
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
@callback
- def _data_to_save(self) -> Dict[str, List[Dict[str, Optional[str]]]]:
+ def _data_to_save(self) -> dict[str, list[dict[str, str | None]]]:
"""Return data of area registry to store in a file."""
data = {}
data["areas"] = [
- {"name": entry.name, "id": entry.id} for entry in self.areas.values()
+ {
+ "name": entry.name,
+ "id": entry.id,
+ }
+ for entry in self.areas.values()
]
return data
-@bind_hass
-async def async_get_registry(hass: HomeAssistantType) -> AreaRegistry:
- """Return area registry instance."""
- reg_or_evt = hass.data.get(DATA_REGISTRY)
+@callback
+def async_get(hass: HomeAssistant) -> AreaRegistry:
+ """Get area registry."""
+ return cast(AreaRegistry, hass.data[DATA_REGISTRY])
+
- if not reg_or_evt:
- evt = hass.data[DATA_REGISTRY] = Event()
+async def async_load(hass: HomeAssistant) -> None:
+ """Load area registry."""
+ assert DATA_REGISTRY not in hass.data
+ hass.data[DATA_REGISTRY] = AreaRegistry(hass)
+ await hass.data[DATA_REGISTRY].async_load()
- reg = AreaRegistry(hass)
- await reg.async_load()
- hass.data[DATA_REGISTRY] = reg
- evt.set()
- return reg
+@bind_hass
+async def async_get_registry(hass: HomeAssistant) -> AreaRegistry:
+ """Get area registry.
+
+ This is deprecated and will be removed in the future. Use async_get instead.
+ """
+ return async_get(hass)
- if isinstance(reg_or_evt, Event):
- evt = reg_or_evt
- await evt.wait()
- return cast(AreaRegistry, hass.data.get(DATA_REGISTRY))
- return cast(AreaRegistry, reg_or_evt)
+def normalize_area_name(area_name: str) -> str:
+ """Normalize an area name by removing whitespace and case folding."""
+ return area_name.casefold().replace(" ", "")
diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py
index 97445b8cee2c0b..a486c8bcc14792 100644
--- a/homeassistant/helpers/check_config.py
+++ b/homeassistant/helpers/check_config.py
@@ -1,8 +1,11 @@
"""Helper to check the configuration file."""
+from __future__ import annotations
+
from collections import OrderedDict
import logging
import os
-from typing import List, NamedTuple, Optional
+from pathlib import Path
+from typing import NamedTuple
import voluptuous as vol
@@ -32,8 +35,8 @@ class CheckConfigError(NamedTuple):
"""Configuration check error."""
message: str
- domain: Optional[str]
- config: Optional[ConfigType]
+ domain: str | None
+ config: ConfigType | None
class HomeAssistantConfig(OrderedDict):
@@ -42,14 +45,14 @@ class HomeAssistantConfig(OrderedDict):
def __init__(self) -> None:
"""Initialize HA config."""
super().__init__()
- self.errors: List[CheckConfigError] = []
+ self.errors: list[CheckConfigError] = []
def add_error(
self,
message: str,
- domain: Optional[str] = None,
- config: Optional[ConfigType] = None,
- ) -> "HomeAssistantConfig":
+ domain: str | None = None,
+ config: ConfigType | None = None,
+ ) -> HomeAssistantConfig:
"""Add a single error."""
self.errors.append(CheckConfigError(str(message), domain, config))
return self
@@ -85,13 +88,18 @@ def _comp_error(ex: Exception, domain: str, config: ConfigType) -> None:
try:
if not await hass.async_add_executor_job(os.path.isfile, config_path):
return result.add_error("File configuration.yaml not found.")
- config = await hass.async_add_executor_job(load_yaml_config_file, config_path)
+
+ assert hass.config.config_dir is not None
+
+ config = await hass.async_add_executor_job(
+ load_yaml_config_file,
+ config_path,
+ yaml_loader.Secrets(Path(hass.config.config_dir)),
+ )
except FileNotFoundError:
return result.add_error(f"File not found: {config_path}")
except HomeAssistantError as err:
return result.add_error(f"Error loading {config_path}: {err}")
- finally:
- yaml_loader.clear_secret_cache()
# Extract and validate core [homeassistant] config
try:
diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py
index 6733b1d3dbd66f..248059f7f93f63 100644
--- a/homeassistant/helpers/collection.py
+++ b/homeassistant/helpers/collection.py
@@ -1,9 +1,12 @@
"""Helper to deal with YAML + storage."""
+from __future__ import annotations
+
from abc import ABC, abstractmethod
import asyncio
from dataclasses import dataclass
+from itertools import groupby
import logging
-from typing import Any, Awaitable, Callable, Dict, Iterable, List, Optional, cast
+from typing import Any, Awaitable, Callable, Coroutine, Iterable, Optional, cast
import voluptuous as vol
from voluptuous.humanize import humanize_error
@@ -16,7 +19,6 @@
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.storage import Store
-from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import slugify
STORAGE_VERSION = 1
@@ -53,6 +55,8 @@ class CollectionChangeSet:
Awaitable[None],
]
+ChangeSetListener = Callable[[Iterable[CollectionChangeSet]], Awaitable[None]]
+
class CollectionError(HomeAssistantError):
"""Base class for collection related errors."""
@@ -72,9 +76,9 @@ class IDManager:
def __init__(self) -> None:
"""Initiate the ID manager."""
- self.collections: List[Dict[str, Any]] = []
+ self.collections: list[dict[str, Any]] = []
- def add_collection(self, collection: Dict[str, Any]) -> None:
+ def add_collection(self, collection: dict[str, Any]) -> None:
"""Add a collection to check for ID usage."""
self.collections.append(collection)
@@ -98,17 +102,18 @@ def generate_id(self, suggestion: str) -> str:
class ObservableCollection(ABC):
"""Base collection type that can be observed."""
- def __init__(self, logger: logging.Logger, id_manager: Optional[IDManager] = None):
+ def __init__(self, logger: logging.Logger, id_manager: IDManager | None = None):
"""Initialize the base collection."""
self.logger = logger
self.id_manager = id_manager or IDManager()
- self.data: Dict[str, dict] = {}
- self.listeners: List[ChangeListener] = []
+ self.data: dict[str, dict] = {}
+ self.listeners: list[ChangeListener] = []
+ self.change_set_listeners: list[ChangeSetListener] = []
self.id_manager.add_collection(self.data)
@callback
- def async_items(self) -> List[dict]:
+ def async_items(self) -> list[dict]:
"""Return list of items in collection."""
return list(self.data.values())
@@ -120,6 +125,14 @@ def async_add_listener(self, listener: ChangeListener) -> None:
"""
self.listeners.append(listener)
+ @callback
+ def async_add_change_set_listener(self, listener: ChangeSetListener) -> None:
+ """Add a listener for a full change set.
+
+ Will be called with [(change_type, item_id, updated_config), ...]
+ """
+ self.change_set_listeners.append(listener)
+
async def notify_changes(self, change_sets: Iterable[CollectionChangeSet]) -> None:
"""Notify listeners of a change."""
await asyncio.gather(
@@ -127,16 +140,19 @@ async def notify_changes(self, change_sets: Iterable[CollectionChangeSet]) -> No
listener(change_set.change_type, change_set.item_id, change_set.item)
for listener in self.listeners
for change_set in change_sets
- ]
+ ],
+ *[
+ change_set_listener(change_sets)
+ for change_set_listener in self.change_set_listeners
+ ],
)
class YamlCollection(ObservableCollection):
"""Offer a collection based on static data."""
- async def async_load(self, data: List[dict]) -> None:
+ async def async_load(self, data: list[dict]) -> None:
"""Load the YAML collection. Overrides existing data."""
-
old_ids = set(self.data)
change_sets = []
@@ -172,7 +188,7 @@ def __init__(
self,
store: Store,
logger: logging.Logger,
- id_manager: Optional[IDManager] = None,
+ id_manager: IDManager | None = None,
):
"""Initialize the storage collection."""
super().__init__(logger, id_manager)
@@ -183,7 +199,7 @@ def hass(self) -> HomeAssistant:
"""Home Assistant object."""
return self.store.hass
- async def _async_load_data(self) -> Optional[dict]:
+ async def _async_load_data(self) -> dict | None:
"""Load the data."""
return cast(Optional[dict], await self.store.async_load())
@@ -275,7 +291,7 @@ class IDLessCollection(ObservableCollection):
counter = 0
- async def async_load(self, data: List[dict]) -> None:
+ async def async_load(self, data: list[dict]) -> None:
"""Load the collection. Overrides existing data."""
await self.notify_changes(
[
@@ -301,53 +317,65 @@ async def async_load(self, data: List[dict]) -> None:
@callback
-def attach_entity_component_collection(
+def sync_entity_lifecycle(
+ hass: HomeAssistant,
+ domain: str,
+ platform: str,
entity_component: EntityComponent,
collection: ObservableCollection,
create_entity: Callable[[dict], Entity],
) -> None:
"""Map a collection to an entity component."""
entities = {}
+ ent_reg = entity_registry.async_get(hass)
- async def _collection_changed(change_type: str, item_id: str, config: dict) -> None:
- """Handle a collection change."""
- if change_type == CHANGE_ADDED:
- entity = create_entity(config)
- await entity_component.async_add_entities([entity])
- entities[item_id] = entity
- return
-
- if change_type == CHANGE_REMOVED:
- entity = entities.pop(item_id)
- await entity.async_remove()
- return
+ async def _add_entity(change_set: CollectionChangeSet) -> Entity:
+ entities[change_set.item_id] = create_entity(change_set.item)
+ return entities[change_set.item_id]
- # CHANGE_UPDATED
- await entities[item_id].async_update_config(config) # type: ignore
-
- collection.async_add_listener(_collection_changed)
-
-
-@callback
-def attach_entity_registry_cleaner(
- hass: HomeAssistantType,
- domain: str,
- platform: str,
- collection: ObservableCollection,
-) -> None:
- """Attach a listener to clean up entity registry on collection changes."""
-
- async def _collection_changed(change_type: str, item_id: str, config: Dict) -> None:
- """Handle a collection change: clean up entity registry on removals."""
- if change_type != CHANGE_REMOVED:
- return
-
- ent_reg = await entity_registry.async_get_registry(hass)
- ent_to_remove = ent_reg.async_get_entity_id(domain, platform, item_id)
+ async def _remove_entity(change_set: CollectionChangeSet) -> None:
+ ent_to_remove = ent_reg.async_get_entity_id(
+ domain, platform, change_set.item_id
+ )
if ent_to_remove is not None:
ent_reg.async_remove(ent_to_remove)
+ else:
+ await entities[change_set.item_id].async_remove(force_remove=True)
+ entities.pop(change_set.item_id)
+
+ async def _update_entity(change_set: CollectionChangeSet) -> None:
+ await entities[change_set.item_id].async_update_config(change_set.item) # type: ignore
+
+ _func_map: dict[
+ str, Callable[[CollectionChangeSet], Coroutine[Any, Any, Entity | None]]
+ ] = {
+ CHANGE_ADDED: _add_entity,
+ CHANGE_REMOVED: _remove_entity,
+ CHANGE_UPDATED: _update_entity,
+ }
+
+ async def _collection_changed(change_sets: Iterable[CollectionChangeSet]) -> None:
+ """Handle a collection change."""
+ # Create a new bucket every time we have a different change type
+ # to ensure operations happen in order. We only group
+ # the same change type.
+ for _, grouped in groupby(
+ change_sets, lambda change_set: change_set.change_type
+ ):
+ new_entities = [
+ entity
+ for entity in await asyncio.gather(
+ *[
+ _func_map[change_set.change_type](change_set)
+ for change_set in grouped
+ ]
+ )
+ if entity is not None
+ ]
+ if new_entities:
+ await entity_component.async_add_entities(new_entities)
- collection.async_add_listener(_collection_changed)
+ collection.async_add_change_set_listener(_collection_changed)
class StorageCollectionWebsocket:
diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py
index 5ace4c91bcf752..6adcb4d1fd9752 100644
--- a/homeassistant/helpers/condition.py
+++ b/homeassistant/helpers/condition.py
@@ -1,12 +1,15 @@
"""Offer reusable conditions."""
+from __future__ import annotations
+
import asyncio
from collections import deque
+from contextlib import contextmanager
from datetime import datetime, timedelta
import functools as ft
import logging
import re
import sys
-from typing import Any, Callable, Container, List, Optional, Set, Union, cast
+from typing import Any, Callable, Container, Generator, cast
from homeassistant.components import zone as zone_cmp
from homeassistant.components.device_automation import (
@@ -36,7 +39,14 @@
WEEKDAYS,
)
from homeassistant.core import HomeAssistant, State, callback
-from homeassistant.exceptions import HomeAssistantError, TemplateError
+from homeassistant.exceptions import (
+ ConditionError,
+ ConditionErrorContainer,
+ ConditionErrorIndex,
+ ConditionErrorMessage,
+ HomeAssistantError,
+ TemplateError,
+)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.sun import get_astral_event_date
from homeassistant.helpers.template import Template
@@ -44,6 +54,17 @@
from homeassistant.util.async_ import run_callback_threadsafe
import homeassistant.util.dt as dt_util
+from .trace import (
+ TraceElement,
+ trace_append_element,
+ trace_path,
+ trace_path_get,
+ trace_stack_cv,
+ trace_stack_pop,
+ trace_stack_push,
+ trace_stack_top,
+)
+
FROM_CONFIG_FORMAT = "{}_from_config"
ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config"
@@ -56,9 +77,56 @@
ConditionCheckerType = Callable[[HomeAssistant, TemplateVarsType], bool]
+def condition_trace_append(variables: TemplateVarsType, path: str) -> TraceElement:
+ """Append a TraceElement to trace[path]."""
+ trace_element = TraceElement(variables, path)
+ trace_append_element(trace_element)
+ return trace_element
+
+
+def condition_trace_set_result(result: bool, **kwargs: Any) -> None:
+ """Set the result of TraceElement at the top of the stack."""
+ node = trace_stack_top(trace_stack_cv)
+
+ # The condition function may be called directly, in which case tracing
+ # is not setup
+ if not node:
+ return
+
+ node.set_result(result=result, **kwargs)
+
+
+@contextmanager
+def trace_condition(variables: TemplateVarsType) -> Generator:
+ """Trace condition evaluation."""
+ trace_element = condition_trace_append(variables, trace_path_get())
+ trace_stack_push(trace_stack_cv, trace_element)
+ try:
+ yield trace_element
+ except Exception as ex:
+ trace_element.set_error(ex)
+ raise ex
+ finally:
+ trace_stack_pop(trace_stack_cv)
+
+
+def trace_condition_function(condition: ConditionCheckerType) -> ConditionCheckerType:
+ """Wrap a condition function to enable basic tracing."""
+
+ @ft.wraps(condition)
+ def wrapper(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
+ """Trace condition."""
+ with trace_condition(variables):
+ result = condition(hass, variables)
+ condition_trace_set_result(result)
+ return result
+
+ return wrapper
+
+
async def async_from_config(
hass: HomeAssistant,
- config: Union[ConfigType, Template],
+ config: ConfigType | Template,
config_validation: bool = True,
) -> ConditionCheckerType:
"""Turn a condition configuration into a method.
@@ -104,17 +172,25 @@ async def async_and_from_config(
await async_from_config(hass, entry, False) for entry in config["conditions"]
]
+ @trace_condition_function
def if_and_condition(
hass: HomeAssistant, variables: TemplateVarsType = None
) -> bool:
"""Test and condition."""
- try:
- for check in checks:
- if not check(hass, variables):
- return False
- except Exception as ex: # pylint: disable=broad-except
- _LOGGER.warning("Error during and-condition: %s", ex)
- return False
+ errors = []
+ for index, check in enumerate(checks):
+ try:
+ with trace_path(["conditions", str(index)]):
+ if not check(hass, variables):
+ return False
+ except ConditionError as ex:
+ errors.append(
+ ConditionErrorIndex("and", index=index, total=len(checks), error=ex)
+ )
+
+ # Raise the errors if no check was false
+ if errors:
+ raise ConditionErrorContainer("and", errors=errors)
return True
@@ -131,16 +207,25 @@ async def async_or_from_config(
await async_from_config(hass, entry, False) for entry in config["conditions"]
]
+ @trace_condition_function
def if_or_condition(
hass: HomeAssistant, variables: TemplateVarsType = None
) -> bool:
- """Test and condition."""
- try:
- for check in checks:
- if check(hass, variables):
- return True
- except Exception as ex: # pylint: disable=broad-except
- _LOGGER.warning("Error during or-condition: %s", ex)
+ """Test or condition."""
+ errors = []
+ for index, check in enumerate(checks):
+ try:
+ with trace_path(["conditions", str(index)]):
+ if check(hass, variables):
+ return True
+ except ConditionError as ex:
+ errors.append(
+ ConditionErrorIndex("or", index=index, total=len(checks), error=ex)
+ )
+
+ # Raise the errors if no check was true
+ if errors:
+ raise ConditionErrorContainer("or", errors=errors)
return False
@@ -157,16 +242,25 @@ async def async_not_from_config(
await async_from_config(hass, entry, False) for entry in config["conditions"]
]
+ @trace_condition_function
def if_not_condition(
hass: HomeAssistant, variables: TemplateVarsType = None
) -> bool:
"""Test not condition."""
- try:
- for check in checks:
- if check(hass, variables):
- return False
- except Exception as ex: # pylint: disable=broad-except
- _LOGGER.warning("Error during not-condition: %s", ex)
+ errors = []
+ for index, check in enumerate(checks):
+ try:
+ with trace_path(["conditions", str(index)]):
+ if check(hass, variables):
+ return False
+ except ConditionError as ex:
+ errors.append(
+ ConditionErrorIndex("not", index=index, total=len(checks), error=ex)
+ )
+
+ # Raise the errors if no check was true
+ if errors:
+ raise ConditionErrorContainer("not", errors=errors)
return True
@@ -175,10 +269,10 @@ def if_not_condition(
def numeric_state(
hass: HomeAssistant,
- entity: Union[None, str, State],
- below: Optional[Union[float, str]] = None,
- above: Optional[Union[float, str]] = None,
- value_template: Optional[Template] = None,
+ entity: None | str | State,
+ below: float | str | None = None,
+ above: float | str | None = None,
+ value_template: Template | None = None,
variables: TemplateVarsType = None,
) -> bool:
"""Test a numeric state condition."""
@@ -196,19 +290,31 @@ def numeric_state(
def async_numeric_state(
hass: HomeAssistant,
- entity: Union[None, str, State],
- below: Optional[Union[float, str]] = None,
- above: Optional[Union[float, str]] = None,
- value_template: Optional[Template] = None,
+ entity: None | str | State,
+ below: float | str | None = None,
+ above: float | str | None = None,
+ value_template: Template | None = None,
variables: TemplateVarsType = None,
- attribute: Optional[str] = None,
+ attribute: str | None = None,
) -> bool:
"""Test a numeric state condition."""
+ if entity is None:
+ raise ConditionErrorMessage("numeric_state", "no entity specified")
+
if isinstance(entity, str):
+ entity_id = entity
entity = hass.states.get(entity)
- if entity is None or (attribute is not None and attribute not in entity.attributes):
- return False
+ if entity is None:
+ raise ConditionErrorMessage("numeric_state", f"unknown entity {entity_id}")
+ else:
+ entity_id = entity.entity_id
+
+ if attribute is not None and attribute not in entity.attributes:
+ raise ConditionErrorMessage(
+ "numeric_state",
+ f"attribute '{attribute}' (of entity {entity_id}) does not exist",
+ )
value: Any = None
if value_template is None:
@@ -222,46 +328,81 @@ def async_numeric_state(
try:
value = value_template.async_render(variables)
except TemplateError as ex:
- _LOGGER.error("Template error: %s", ex)
- return False
+ raise ConditionErrorMessage(
+ "numeric_state", f"template error: {ex}"
+ ) from ex
+ # Known states that never match the numeric condition
if value in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return False
try:
fvalue = float(value)
- except ValueError:
- _LOGGER.warning(
- "Value cannot be processed as a number: %s (Offending entity: %s)",
- entity,
- value,
- )
- return False
+ except (ValueError, TypeError) as ex:
+ raise ConditionErrorMessage(
+ "numeric_state",
+ f"entity {entity_id} state '{value}' cannot be processed as a number",
+ ) from ex
if below is not None:
if isinstance(below, str):
below_entity = hass.states.get(below)
- if (
- not below_entity
- or below_entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
- or fvalue >= float(below_entity.state)
+ if not below_entity:
+ raise ConditionErrorMessage(
+ "numeric_state", f"unknown 'below' entity {below}"
+ )
+ if below_entity.state in (
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
):
return False
+ try:
+ if fvalue >= float(below_entity.state):
+ condition_trace_set_result(
+ False,
+ state=fvalue,
+ wanted_state_below=float(below_entity.state),
+ )
+ return False
+ except (ValueError, TypeError) as ex:
+ raise ConditionErrorMessage(
+ "numeric_state",
+ f"the 'below' entity {below} state '{below_entity.state}' cannot be processed as a number",
+ ) from ex
elif fvalue >= below:
+ condition_trace_set_result(False, state=fvalue, wanted_state_below=below)
return False
if above is not None:
if isinstance(above, str):
above_entity = hass.states.get(above)
- if (
- not above_entity
- or above_entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
- or fvalue <= float(above_entity.state)
+ if not above_entity:
+ raise ConditionErrorMessage(
+ "numeric_state", f"unknown 'above' entity {above}"
+ )
+ if above_entity.state in (
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
):
return False
+ try:
+ if fvalue <= float(above_entity.state):
+ condition_trace_set_result(
+ False,
+ state=fvalue,
+ wanted_state_above=float(above_entity.state),
+ )
+ return False
+ except (ValueError, TypeError) as ex:
+ raise ConditionErrorMessage(
+ "numeric_state",
+ f"the 'above' entity {above} state '{above_entity.state}' cannot be processed as a number",
+ ) from ex
elif fvalue <= above:
+ condition_trace_set_result(False, state=fvalue, wanted_state_above=above)
return False
+ condition_trace_set_result(True, state=fvalue)
return True
@@ -277,6 +418,7 @@ def async_numeric_state_from_config(
above = config.get(CONF_ABOVE)
value_template = config.get(CONF_VALUE_TEMPLATE)
+ @trace_condition_function
def if_numeric_state(
hass: HomeAssistant, variables: TemplateVarsType = None
) -> bool:
@@ -284,32 +426,63 @@ def if_numeric_state(
if value_template is not None:
value_template.hass = hass
- return all(
- async_numeric_state(
- hass, entity_id, below, above, value_template, variables, attribute
- )
- for entity_id in entity_ids
- )
+ errors = []
+ for index, entity_id in enumerate(entity_ids):
+ try:
+ with trace_path(["entity_id", str(index)]), trace_condition(variables):
+ if not async_numeric_state(
+ hass,
+ entity_id,
+ below,
+ above,
+ value_template,
+ variables,
+ attribute,
+ ):
+ return False
+ except ConditionError as ex:
+ errors.append(
+ ConditionErrorIndex(
+ "numeric_state", index=index, total=len(entity_ids), error=ex
+ )
+ )
+
+ # Raise the errors if no check was false
+ if errors:
+ raise ConditionErrorContainer("numeric_state", errors=errors)
+
+ return True
return if_numeric_state
def state(
hass: HomeAssistant,
- entity: Union[None, str, State],
+ entity: None | str | State,
req_state: Any,
- for_period: Optional[timedelta] = None,
- attribute: Optional[str] = None,
+ for_period: timedelta | None = None,
+ attribute: str | None = None,
) -> bool:
"""Test if state matches requirements.
Async friendly.
"""
+ if entity is None:
+ raise ConditionErrorMessage("state", "no entity specified")
+
if isinstance(entity, str):
+ entity_id = entity
entity = hass.states.get(entity)
- if entity is None or (attribute is not None and attribute not in entity.attributes):
- return False
+ if entity is None:
+ raise ConditionErrorMessage("state", f"unknown entity {entity_id}")
+ else:
+ entity_id = entity.entity_id
+
+ if attribute is not None and attribute not in entity.attributes:
+ raise ConditionErrorMessage(
+ "state", f"attribute '{attribute}' (of entity {entity_id}) does not exist"
+ )
assert isinstance(entity, State)
@@ -330,16 +503,22 @@ def state(
):
state_entity = hass.states.get(req_state_value)
if not state_entity:
- continue
+ raise ConditionErrorMessage(
+ "state", f"the 'state' entity {req_state_value} is unavailable"
+ )
state_value = state_entity.state
is_state = value == state_value
if is_state:
break
if for_period is None or not is_state:
+ condition_trace_set_result(is_state, state=value, wanted_state=state_value)
return is_state
- return dt_util.utcnow() - for_period > entity.last_changed
+ duration = dt_util.utcnow() - for_period
+ duration_ok = duration > entity.last_changed
+ condition_trace_set_result(duration_ok, state=value, duration=duration)
+ return duration_ok
def state_from_config(
@@ -349,29 +528,44 @@ def state_from_config(
if config_validation:
config = cv.STATE_CONDITION_SCHEMA(config)
entity_ids = config.get(CONF_ENTITY_ID, [])
- req_states: Union[str, List[str]] = config.get(CONF_STATE, [])
+ req_states: str | list[str] = config.get(CONF_STATE, [])
for_period = config.get("for")
attribute = config.get(CONF_ATTRIBUTE)
if not isinstance(req_states, list):
req_states = [req_states]
+ @trace_condition_function
def if_state(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
"""Test if condition."""
- return all(
- state(hass, entity_id, req_states, for_period, attribute)
- for entity_id in entity_ids
- )
+ errors = []
+ for index, entity_id in enumerate(entity_ids):
+ try:
+ with trace_path(["entity_id", str(index)]), trace_condition(variables):
+ if not state(hass, entity_id, req_states, for_period, attribute):
+ return False
+ except ConditionError as ex:
+ errors.append(
+ ConditionErrorIndex(
+ "state", index=index, total=len(entity_ids), error=ex
+ )
+ )
+
+ # Raise the errors if no check was false
+ if errors:
+ raise ConditionErrorContainer("state", errors=errors)
+
+ return True
return if_state
def sun(
hass: HomeAssistant,
- before: Optional[str] = None,
- after: Optional[str] = None,
- before_offset: Optional[timedelta] = None,
- after_offset: Optional[timedelta] = None,
+ before: str | None = None,
+ after: str | None = None,
+ before_offset: timedelta | None = None,
+ after_offset: timedelta | None = None,
) -> bool:
"""Test if current time matches sun requirements."""
utcnow = dt_util.utcnow()
@@ -432,11 +626,12 @@ def sun_from_config(
before_offset = config.get("before_offset")
after_offset = config.get("after_offset")
- def time_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
+ @trace_condition_function
+ def sun_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
"""Validate time based if-condition."""
return sun(hass, before, after, before_offset, after_offset)
- return time_if
+ return sun_if
def template(
@@ -455,8 +650,7 @@ def async_template(
try:
value: str = value_template.async_render(variables, parse_result=False)
except TemplateError as ex:
- _LOGGER.error("Error during template condition: %s", ex)
- return False
+ raise ConditionErrorMessage("template", str(ex)) from ex
return value.lower() == "true"
@@ -469,6 +663,7 @@ def async_template_from_config(
config = cv.TEMPLATE_CONDITION_SCHEMA(config)
value_template = cast(Template, config.get(CONF_VALUE_TEMPLATE))
+ @trace_condition_function
def template_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
"""Validate template based if-condition."""
value_template.hass = hass
@@ -480,9 +675,9 @@ def template_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool
def time(
hass: HomeAssistant,
- before: Optional[Union[dt_util.dt.time, str]] = None,
- after: Optional[Union[dt_util.dt.time, str]] = None,
- weekday: Union[None, str, Container[str]] = None,
+ before: dt_util.dt.time | str | None = None,
+ after: dt_util.dt.time | str | None = None,
+ weekday: None | str | Container[str] = None,
) -> bool:
"""Test if local time condition matches.
@@ -499,7 +694,7 @@ def time(
elif isinstance(after, str):
after_entity = hass.states.get(after)
if not after_entity:
- return False
+ raise ConditionErrorMessage("time", f"unknown 'after' entity {after}")
after = dt_util.dt.time(
after_entity.attributes.get("hour", 23),
after_entity.attributes.get("minute", 59),
@@ -511,7 +706,7 @@ def time(
elif isinstance(before, str):
before_entity = hass.states.get(before)
if not before_entity:
- return False
+ raise ConditionErrorMessage("time", f"unknown 'before' entity {before}")
before = dt_util.dt.time(
before_entity.attributes.get("hour", 23),
before_entity.attributes.get("minute", 59),
@@ -549,6 +744,7 @@ def time_from_config(
after = config.get(CONF_AFTER)
weekday = config.get(CONF_WEEKDAY)
+ @trace_condition_function
def time_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
"""Validate time based if-condition."""
return time(hass, before, after, weekday)
@@ -558,30 +754,47 @@ def time_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
def zone(
hass: HomeAssistant,
- zone_ent: Union[None, str, State],
- entity: Union[None, str, State],
+ zone_ent: None | str | State,
+ entity: None | str | State,
) -> bool:
"""Test if zone-condition matches.
Async friendly.
"""
+ if zone_ent is None:
+ raise ConditionErrorMessage("zone", "no zone specified")
+
if isinstance(zone_ent, str):
+ zone_ent_id = zone_ent
zone_ent = hass.states.get(zone_ent)
- if zone_ent is None:
- return False
+ if zone_ent is None:
+ raise ConditionErrorMessage("zone", f"unknown zone {zone_ent_id}")
+
+ if entity is None:
+ raise ConditionErrorMessage("zone", "no entity specified")
if isinstance(entity, str):
+ entity_id = entity
entity = hass.states.get(entity)
- if entity is None:
- return False
+ if entity is None:
+ raise ConditionErrorMessage("zone", f"unknown entity {entity_id}")
+ else:
+ entity_id = entity.entity_id
latitude = entity.attributes.get(ATTR_LATITUDE)
longitude = entity.attributes.get(ATTR_LONGITUDE)
- if latitude is None or longitude is None:
- return False
+ if latitude is None:
+ raise ConditionErrorMessage(
+ "zone", f"entity {entity_id} has no 'latitude' attribute"
+ )
+
+ if longitude is None:
+ raise ConditionErrorMessage(
+ "zone", f"entity {entity_id} has no 'longitude' attribute"
+ )
return zone_cmp.in_zone(
zone_ent, latitude, longitude, entity.attributes.get(ATTR_GPS_ACCURACY, 0)
@@ -597,15 +810,34 @@ def zone_from_config(
entity_ids = config.get(CONF_ENTITY_ID, [])
zone_entity_ids = config.get(CONF_ZONE, [])
+ @trace_condition_function
def if_in_zone(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
"""Test if condition."""
- return all(
- any(
- zone(hass, zone_entity_id, entity_id)
- for zone_entity_id in zone_entity_ids
- )
- for entity_id in entity_ids
- )
+ errors = []
+
+ all_ok = True
+ for entity_id in entity_ids:
+ entity_ok = False
+ for zone_entity_id in zone_entity_ids:
+ try:
+ if zone(hass, zone_entity_id, entity_id):
+ entity_ok = True
+ except ConditionErrorMessage as ex:
+ errors.append(
+ ConditionErrorMessage(
+ "zone",
+ f"error matching {entity_id} with {zone_entity_id}: {ex.message}",
+ )
+ )
+
+ if not entity_ok:
+ all_ok = False
+
+ # Raise the errors only if no definitive result was found
+ if errors and not all_ok:
+ raise ConditionErrorContainer("zone", errors=errors)
+
+ return all_ok
return if_in_zone
@@ -619,15 +851,17 @@ async def async_device_from_config(
platform = await async_get_device_automation_platform(
hass, config[CONF_DOMAIN], "condition"
)
- return cast(
- ConditionCheckerType,
- platform.async_condition_from_config(config, config_validation), # type: ignore
+ return trace_condition_function(
+ cast(
+ ConditionCheckerType,
+ platform.async_condition_from_config(config, config_validation), # type: ignore
+ )
)
async def async_validate_condition_config(
- hass: HomeAssistant, config: Union[ConfigType, Template]
-) -> Union[ConfigType, Template]:
+ hass: HomeAssistant, config: ConfigType | Template
+) -> ConfigType | Template:
"""Validate config."""
if isinstance(config, Template):
return config
@@ -652,9 +886,9 @@ async def async_validate_condition_config(
@callback
-def async_extract_entities(config: Union[ConfigType, Template]) -> Set[str]:
+def async_extract_entities(config: ConfigType | Template) -> set[str]:
"""Extract entities from a condition."""
- referenced: Set[str] = set()
+ referenced: set[str] = set()
to_process = deque([config])
while to_process:
@@ -680,7 +914,7 @@ def async_extract_entities(config: Union[ConfigType, Template]) -> Set[str]:
@callback
-def async_extract_devices(config: Union[ConfigType, Template]) -> Set[str]:
+def async_extract_devices(config: ConfigType | Template) -> set[str]:
"""Extract devices from a condition."""
referenced = set()
to_process = deque([config])
diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py
index 889981537c62fb..6abcf0ece56bff 100644
--- a/homeassistant/helpers/config_entry_flow.py
+++ b/homeassistant/helpers/config_entry_flow.py
@@ -1,9 +1,10 @@
"""Helpers for data entry flows for config entries."""
-from typing import Any, Awaitable, Callable, Dict, Optional, Union
+from __future__ import annotations
-from homeassistant import config_entries
+from typing import Any, Awaitable, Callable, Union
-from .typing import HomeAssistantType
+from homeassistant import config_entries
+from homeassistant.core import HomeAssistant
DiscoveryFunctionType = Callable[[], Union[Awaitable[bool], bool]]
@@ -27,8 +28,8 @@ def __init__(
self.CONNECTION_CLASS = connection_class # pylint: disable=invalid-name
async def async_step_user(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle a flow initialized by the user."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
@@ -38,10 +39,11 @@ async def async_step_user(
return await self.async_step_confirm()
async def async_step_confirm(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Confirm setup."""
if user_input is None:
+ self._set_confirm_only()
return self.async_show_form(step_id="confirm")
if self.source == config_entries.SOURCE_USER:
@@ -58,7 +60,6 @@ async def async_step_confirm(
return self.async_abort(reason="no_devices_found")
# Cancel the discovered one.
- assert self.hass is not None
for flow in in_progress:
self.hass.config_entries.flow.async_abort(flow["flow_id"])
@@ -68,8 +69,8 @@ async def async_step_confirm(
return self.async_create_entry(title=self._title, data={})
async def async_step_discovery(
- self, discovery_info: Dict[str, Any]
- ) -> Dict[str, Any]:
+ self, discovery_info: dict[str, Any]
+ ) -> dict[str, Any]:
"""Handle a flow initialized by discovery."""
if self._async_in_progress() or self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
@@ -84,13 +85,12 @@ async def async_step_discovery(
async_step_homekit = async_step_discovery
async_step_dhcp = async_step_discovery
- async def async_step_import(self, _: Optional[Dict[str, Any]]) -> Dict[str, Any]:
+ async def async_step_import(self, _: dict[str, Any] | None) -> dict[str, Any]:
"""Handle a flow initialized by import."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
# Cancel other flows.
- assert self.hass is not None
in_progress = self._async_in_progress()
for flow in in_progress:
self.hass.config_entries.flow.async_abort(flow["flow_id"])
@@ -134,8 +134,8 @@ def __init__(
self._allow_multiple = allow_multiple
async def async_step_user(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle a user initiated set up flow to create a webhook."""
if not self._allow_multiple and self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
@@ -143,7 +143,6 @@ async def async_step_user(
if user_input is None:
return self.async_show_form(step_id="user")
- assert self.hass is not None
webhook_id = self.hass.components.webhook.async_generate_id()
if (
@@ -182,7 +181,7 @@ def __init__(self) -> None:
async def webhook_async_remove_entry(
- hass: HomeAssistantType, entry: config_entries.ConfigEntry
+ hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> None:
"""Remove a webhook config entry."""
if not entry.data.get("cloudhook") or "cloud" not in hass.config.components:
diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py
index 653d07a333e116..795c08dd1c98f2 100644
--- a/homeassistant/helpers/config_entry_oauth2_flow.py
+++ b/homeassistant/helpers/config_entry_oauth2_flow.py
@@ -5,12 +5,14 @@
- OAuth2 implementation that works with local provided client ID/secret
"""
+from __future__ import annotations
+
from abc import ABC, ABCMeta, abstractmethod
import asyncio
import logging
import secrets
import time
-from typing import Any, Awaitable, Callable, Dict, Optional, cast
+from typing import Any, Awaitable, Callable, Dict, cast
from aiohttp import client, web
import async_timeout
@@ -231,10 +233,9 @@ def extra_authorize_data(self) -> dict:
return {}
async def async_step_pick_implementation(
- self, user_input: Optional[dict] = None
+ self, user_input: dict | None = None
) -> dict:
"""Handle a flow start."""
- assert self.hass
implementations = await async_get_implementations(self.hass, self.DOMAIN)
if user_input is not None:
@@ -244,8 +245,10 @@ async def async_step_pick_implementation(
if not implementations:
return self.async_abort(reason="missing_configuration")
- if len(implementations) == 1:
- # Pick first implementation as we have only one.
+ req = http.current_request.get()
+ if len(implementations) == 1 and req is not None:
+ # Pick first implementation if we have only one, but only
+ # if this is triggered by a user interaction (request).
self.flow_impl = list(implementations.values())[0]
return await self.async_step_auth()
@@ -261,8 +264,8 @@ async def async_step_pick_implementation(
)
async def async_step_auth(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Create an entry for auth."""
# Flow has been triggered by external data
if user_input:
@@ -287,8 +290,8 @@ async def async_step_auth(
return self.async_external_step(step_id="auth", url=url)
async def async_step_creation(
- self, user_input: Optional[Dict[str, Any]] = None
- ) -> Dict[str, Any]:
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Create config entry from external data."""
token = await self.flow_impl.async_resolve_external_data(self.external_data)
# Force int for non-compliant oauth2 providers
@@ -312,24 +315,7 @@ async def async_oauth_create_entry(self, data: dict) -> dict:
"""
return self.async_create_entry(title=self.flow_impl.name, data=data)
- async def async_step_discovery(
- self, discovery_info: Dict[str, Any]
- ) -> Dict[str, Any]:
- """Handle a flow initialized by discovery."""
- await self.async_set_unique_id(self.DOMAIN)
-
- assert self.hass is not None
- if self.hass.config_entries.async_entries(self.DOMAIN):
- return self.async_abort(reason="already_configured")
-
- return await self.async_step_pick_implementation()
-
async_step_user = async_step_pick_implementation
- async_step_mqtt = async_step_discovery
- async_step_ssdp = async_step_discovery
- async_step_zeroconf = async_step_discovery
- async_step_homekit = async_step_discovery
- async_step_dhcp = async_step_discovery
@classmethod
def async_register_implementation(
@@ -356,7 +342,7 @@ def async_register_implementation(
async def async_get_implementations(
hass: HomeAssistant, domain: str
-) -> Dict[str, AbstractOAuth2Implementation]:
+) -> dict[str, AbstractOAuth2Implementation]:
"""Return OAuth2 implementations for specified domain."""
registered = cast(
Dict[str, AbstractOAuth2Implementation],
@@ -394,7 +380,7 @@ def async_add_implementation_provider(
hass: HomeAssistant,
provider_domain: str,
async_provide_implementation: Callable[
- [HomeAssistant, str], Awaitable[Optional[AbstractOAuth2Implementation]]
+ [HomeAssistant, str], Awaitable[AbstractOAuth2Implementation | None]
],
) -> None:
"""Add an implementation provider.
@@ -518,7 +504,7 @@ def _encode_jwt(hass: HomeAssistant, data: dict) -> str:
@callback
-def _decode_jwt(hass: HomeAssistant, encoded: str) -> Optional[dict]:
+def _decode_jwt(hass: HomeAssistant, encoded: str) -> dict | None:
"""JWT encode data."""
secret = cast(str, hass.data.get(DATA_JWT_SECRET))
diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py
index acf6139708a51f..21d04f11551794 100644
--- a/homeassistant/helpers/config_validation.py
+++ b/homeassistant/helpers/config_validation.py
@@ -1,4 +1,6 @@
"""Helpers for config validation using voluptuous."""
+from __future__ import annotations
+
from datetime import (
date as date_sys,
datetime as datetime_sys,
@@ -12,19 +14,7 @@
import os
import re
from socket import _GLOBAL_DEFAULT_TIMEOUT # type: ignore # private, not in typeshed
-from typing import (
- Any,
- Callable,
- Dict,
- Hashable,
- List,
- Optional,
- Pattern,
- Type,
- TypeVar,
- Union,
- cast,
-)
+from typing import Any, Callable, Dict, Hashable, Pattern, TypeVar, cast
from urllib.parse import urlparse
from uuid import UUID
@@ -131,7 +121,7 @@ def path(value: Any) -> str:
def has_at_least_one_key(*keys: str) -> Callable:
"""Validate that at least one key exists."""
- def validate(obj: Dict) -> Dict:
+ def validate(obj: dict) -> dict:
"""Test keys exist in dict."""
if not isinstance(obj, dict):
raise vol.Invalid("expected dictionary")
@@ -144,10 +134,10 @@ def validate(obj: Dict) -> Dict:
return validate
-def has_at_most_one_key(*keys: str) -> Callable[[Dict], Dict]:
+def has_at_most_one_key(*keys: str) -> Callable[[dict], dict]:
"""Validate that zero keys exist or one key exists."""
- def validate(obj: Dict) -> Dict:
+ def validate(obj: dict) -> dict:
"""Test zero keys exist or one key exists in dict."""
if not isinstance(obj, dict):
raise vol.Invalid("expected dictionary")
@@ -253,7 +243,7 @@ def isdir(value: Any) -> str:
return dir_in
-def ensure_list(value: Union[T, List[T], None]) -> List[T]:
+def ensure_list(value: T | list[T] | None) -> list[T]:
"""Wrap value in list if it is not one."""
if value is None:
return []
@@ -266,10 +256,10 @@ def entity_id(value: Any) -> str:
if valid_entity_id(str_value):
return str_value
- raise vol.Invalid(f"Entity ID {value} is an invalid entity id")
+ raise vol.Invalid(f"Entity ID {value} is an invalid entity ID")
-def entity_ids(value: Union[str, List]) -> List[str]:
+def entity_ids(value: str | list) -> list[str]:
"""Validate Entity IDs."""
if value is None:
raise vol.Invalid("Entity IDs can not be None")
@@ -284,7 +274,7 @@ def entity_ids(value: Union[str, List]) -> List[str]:
)
-def entity_domain(domain: Union[str, List[str]]) -> Callable[[Any], str]:
+def entity_domain(domain: str | list[str]) -> Callable[[Any], str]:
"""Validate that entity belong to domain."""
ent_domain = entities_domain(domain)
@@ -298,9 +288,7 @@ def validate(value: str) -> str:
return validate
-def entities_domain(
- domain: Union[str, List[str]]
-) -> Callable[[Union[str, List]], List[str]]:
+def entities_domain(domain: str | list[str]) -> Callable[[str | list], list[str]]:
"""Validate that entities belong to domain."""
if isinstance(domain, str):
@@ -312,7 +300,7 @@ def check_invalid(val: str) -> bool:
def check_invalid(val: str) -> bool:
return val not in domain
- def validate(values: Union[str, List]) -> List[str]:
+ def validate(values: str | list) -> list[str]:
"""Test if entity domain is domain."""
values = entity_ids(values)
for ent_id in values:
@@ -325,7 +313,7 @@ def validate(values: Union[str, List]) -> List[str]:
return validate
-def enum(enumClass: Type[Enum]) -> vol.All:
+def enum(enumClass: type[Enum]) -> vol.All:
"""Create validator for specified enum."""
return vol.All(vol.In(enumClass.__members__), enumClass.__getitem__)
@@ -423,7 +411,7 @@ def time_period_str(value: str) -> timedelta:
return offset
-def time_period_seconds(value: Union[float, str]) -> timedelta:
+def time_period_seconds(value: float | str) -> timedelta:
"""Validate and transform seconds to a time offset."""
try:
return timedelta(seconds=float(value))
@@ -450,7 +438,7 @@ def positive_timedelta(value: timedelta) -> timedelta:
positive_time_period = vol.All(time_period, positive_timedelta)
-def remove_falsy(value: List[T]) -> List[T]:
+def remove_falsy(value: list[T]) -> list[T]:
"""Remove falsy values from a list."""
return [v for v in value if v]
@@ -477,7 +465,7 @@ def slug(value: Any) -> str:
def schema_with_slug_keys(
- value_schema: Union[T, Callable], *, slug_validator: Callable[[Any], str] = slug
+ value_schema: T | Callable, *, slug_validator: Callable[[Any], str] = slug
) -> Callable:
"""Ensure dicts have slugs as keys.
@@ -486,7 +474,7 @@ def schema_with_slug_keys(
"""
schema = vol.Schema({str: value_schema})
- def verify(value: Dict) -> Dict:
+ def verify(value: dict) -> dict:
"""Validate all keys are slugs and then the value_schema."""
if not isinstance(value, dict):
raise vol.Invalid("expected dictionary")
@@ -547,9 +535,8 @@ def temperature_unit(value: Any) -> str:
)
-def template(value: Optional[Any]) -> template_helper.Template:
+def template(value: Any | None) -> template_helper.Template:
"""Validate a jinja2 template."""
-
if value is None:
raise vol.Invalid("template value is None")
if isinstance(value, (list, dict, template_helper.Template)):
@@ -564,15 +551,14 @@ def template(value: Optional[Any]) -> template_helper.Template:
raise vol.Invalid(f"invalid template ({ex})") from ex
-def dynamic_template(value: Optional[Any]) -> template_helper.Template:
+def dynamic_template(value: Any | None) -> template_helper.Template:
"""Validate a dynamic (non static) jinja2 template."""
-
if value is None:
raise vol.Invalid("template value is None")
if isinstance(value, (list, dict, template_helper.Template)):
raise vol.Invalid("template value should be a string")
if not template_helper.is_template_string(str(value)):
- raise vol.Invalid("template value does not contain a dynmamic template")
+ raise vol.Invalid("template value does not contain a dynamic template")
template_value = template_helper.Template(str(value)) # type: ignore
try:
@@ -634,7 +620,7 @@ def time_zone(value: str) -> str:
weekdays = vol.All(ensure_list, [vol.In(WEEKDAYS)])
-def socket_timeout(value: Optional[Any]) -> object:
+def socket_timeout(value: Any | None) -> object:
"""Validate timeout float > 0.0.
None coerced to socket._GLOBAL_DEFAULT_TIMEOUT bare object.
@@ -683,7 +669,7 @@ def uuid4_hex(value: Any) -> str:
return result.hex
-def ensure_list_csv(value: Any) -> List:
+def ensure_list_csv(value: Any) -> list:
"""Ensure that input is a list or make one from comma-separated string."""
if isinstance(value, str):
return [member.strip() for member in value.split(",")]
@@ -711,9 +697,9 @@ def __call__(self, selected: list) -> list:
def deprecated(
key: str,
- replacement_key: Optional[str] = None,
- default: Optional[Any] = None,
-) -> Callable[[Dict], Dict]:
+ replacement_key: str | None = None,
+ default: Any | None = None,
+) -> Callable[[dict], dict]:
"""
Log key as deprecated and provide a replacement (if exists).
@@ -725,7 +711,7 @@ def deprecated(
- No warning if neither key nor replacement_key are provided
- Adds replacement_key with default value in this case
"""
- module = inspect.getmodule(inspect.stack()[1][0])
+ module = inspect.getmodule(inspect.stack(context=0)[1].frame)
if module is not None:
module_name = module.__name__
else:
@@ -745,15 +731,24 @@ def deprecated(
" please remove it from your configuration"
)
- def validator(config: Dict) -> Dict:
+ def validator(config: dict) -> dict:
"""Check if key is in config and log warning."""
if key in config:
- KeywordStyleAdapter(logging.getLogger(module_name)).warning(
- warning,
- key=key,
- replacement_key=replacement_key,
- )
-
+ try:
+ KeywordStyleAdapter(logging.getLogger(module_name)).warning(
+ warning.replace(
+ "'{key}' option",
+ f"'{key}' option near {config.__config_file__}:{config.__line__}", # type: ignore
+ ),
+ key=key,
+ replacement_key=replacement_key,
+ )
+ except AttributeError:
+ KeywordStyleAdapter(logging.getLogger(module_name)).warning(
+ warning,
+ key=key,
+ replacement_key=replacement_key,
+ )
value = config[key]
if replacement_key:
config.pop(key)
@@ -774,14 +769,14 @@ def validator(config: Dict) -> Dict:
def key_value_schemas(
- key: str, value_schemas: Dict[str, vol.Schema]
-) -> Callable[[Any], Dict[str, Any]]:
+ key: str, value_schemas: dict[str, vol.Schema]
+) -> Callable[[Any], dict[str, Any]]:
"""Create a validator that validates based on a value for specific key.
This gives better error messages.
"""
- def key_value_validator(value: Any) -> Dict[str, Any]:
+ def key_value_validator(value: Any) -> dict[str, Any]:
if not isinstance(value, dict):
raise vol.Invalid("Expected a dictionary")
@@ -802,10 +797,10 @@ def key_value_validator(value: Any) -> Dict[str, Any]:
def key_dependency(
key: Hashable, dependency: Hashable
-) -> Callable[[Dict[Hashable, Any]], Dict[Hashable, Any]]:
+) -> Callable[[dict[Hashable, Any]], dict[Hashable, Any]]:
"""Validate that all dependencies exist for key."""
- def validator(value: Dict[Hashable, Any]) -> Dict[Hashable, Any]:
+ def validator(value: dict[Hashable, Any]) -> dict[Hashable, Any]:
"""Test dependencies."""
if not isinstance(value, dict):
raise vol.Invalid("key dependencies require a dict")
@@ -849,11 +844,18 @@ def custom_serializer(schema: Any) -> Any:
PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
ENTITY_SERVICE_FIELDS = {
- vol.Optional(ATTR_ENTITY_ID): comp_entity_ids,
+ # Either accept static entity IDs, a single dynamic template or a mixed list
+ # of static and dynamic templates. While this could be solved with a single
+ # complex template, handling it like this, keeps config validation useful.
+ vol.Optional(ATTR_ENTITY_ID): vol.Any(
+ comp_entity_ids, dynamic_template, vol.All(list, template_complex)
+ ),
vol.Optional(ATTR_DEVICE_ID): vol.Any(
- ENTITY_MATCH_NONE, vol.All(ensure_list, [str])
+ ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
+ ),
+ vol.Optional(ATTR_AREA_ID): vol.Any(
+ ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
),
- vol.Optional(ATTR_AREA_ID): vol.Any(ENTITY_MATCH_NONE, vol.All(ensure_list, [str])),
}
@@ -890,9 +892,11 @@ def script_action(value: Any) -> dict:
SCRIPT_SCHEMA = vol.All(ensure_list, [script_action])
+SCRIPT_ACTION_BASE_SCHEMA = {vol.Optional(CONF_ALIAS): string}
+
EVENT_SCHEMA = vol.Schema(
{
- vol.Optional(CONF_ALIAS): string,
+ **SCRIPT_ACTION_BASE_SCHEMA,
vol.Required(CONF_EVENT): string,
vol.Optional(CONF_EVENT_DATA): vol.All(dict, template_complex),
vol.Optional(CONF_EVENT_DATA_TEMPLATE): vol.All(dict, template_complex),
@@ -902,7 +906,7 @@ def script_action(value: Any) -> dict:
SERVICE_SCHEMA = vol.All(
vol.Schema(
{
- vol.Optional(CONF_ALIAS): string,
+ **SCRIPT_ACTION_BASE_SCHEMA,
vol.Exclusive(CONF_SERVICE, "service name"): vol.Any(
service, dynamic_template
),
@@ -912,7 +916,7 @@ def script_action(value: Any) -> dict:
vol.Optional("data"): vol.All(dict, template_complex),
vol.Optional("data_template"): vol.All(dict, template_complex),
vol.Optional(CONF_ENTITY_ID): comp_entity_ids,
- vol.Optional(CONF_TARGET): ENTITY_SERVICE_FIELDS,
+ vol.Optional(CONF_TARGET): vol.Any(ENTITY_SERVICE_FIELDS, dynamic_template),
}
),
has_at_least_one_key(CONF_SERVICE, CONF_SERVICE_TEMPLATE),
@@ -922,9 +926,12 @@ def script_action(value: Any) -> dict:
vol.Coerce(float), vol.All(str, entity_domain("input_number"))
)
+CONDITION_BASE_SCHEMA = {vol.Optional(CONF_ALIAS): string}
+
NUMERIC_STATE_CONDITION_SCHEMA = vol.All(
vol.Schema(
{
+ **CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): "numeric_state",
vol.Required(CONF_ENTITY_ID): entity_ids,
vol.Optional(CONF_ATTRIBUTE): str,
@@ -937,6 +944,7 @@ def script_action(value: Any) -> dict:
)
STATE_CONDITION_BASE_SCHEMA = {
+ **CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): "state",
vol.Required(CONF_ENTITY_ID): entity_ids,
vol.Optional(CONF_ATTRIBUTE): str,
@@ -977,6 +985,7 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name
SUN_CONDITION_SCHEMA = vol.All(
vol.Schema(
{
+ **CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): "sun",
vol.Optional("before"): sun_event,
vol.Optional("before_offset"): time_period,
@@ -991,6 +1000,7 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name
TEMPLATE_CONDITION_SCHEMA = vol.Schema(
{
+ **CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): "template",
vol.Required(CONF_VALUE_TEMPLATE): template,
}
@@ -999,6 +1009,7 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name
TIME_CONDITION_SCHEMA = vol.All(
vol.Schema(
{
+ **CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): "time",
"before": vol.Any(time, vol.All(str, entity_domain("input_datetime"))),
"after": vol.Any(time, vol.All(str, entity_domain("input_datetime"))),
@@ -1010,6 +1021,7 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name
ZONE_CONDITION_SCHEMA = vol.Schema(
{
+ **CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): "zone",
vol.Required(CONF_ENTITY_ID): entity_ids,
"zone": entity_ids,
@@ -1021,6 +1033,7 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name
AND_CONDITION_SCHEMA = vol.Schema(
{
+ **CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): "and",
vol.Required(CONF_CONDITIONS): vol.All(
ensure_list,
@@ -1032,6 +1045,7 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name
OR_CONDITION_SCHEMA = vol.Schema(
{
+ **CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): "or",
vol.Required(CONF_CONDITIONS): vol.All(
ensure_list,
@@ -1043,6 +1057,7 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name
NOT_CONDITION_SCHEMA = vol.Schema(
{
+ **CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): "not",
vol.Required(CONF_CONDITIONS): vol.All(
ensure_list,
@@ -1054,6 +1069,7 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name
DEVICE_CONDITION_BASE_SCHEMA = vol.Schema(
{
+ **CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): "device",
vol.Required(CONF_DEVICE_ID): str,
vol.Required(CONF_DOMAIN): str,
@@ -1089,14 +1105,14 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name
_SCRIPT_DELAY_SCHEMA = vol.Schema(
{
- vol.Optional(CONF_ALIAS): string,
+ **SCRIPT_ACTION_BASE_SCHEMA,
vol.Required(CONF_DELAY): positive_time_period_template,
}
)
_SCRIPT_WAIT_TEMPLATE_SCHEMA = vol.Schema(
{
- vol.Optional(CONF_ALIAS): string,
+ **SCRIPT_ACTION_BASE_SCHEMA,
vol.Required(CONF_WAIT_TEMPLATE): template,
vol.Optional(CONF_TIMEOUT): positive_time_period_template,
vol.Optional(CONF_CONTINUE_ON_TIMEOUT): boolean,
@@ -1104,16 +1120,22 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name
)
DEVICE_ACTION_BASE_SCHEMA = vol.Schema(
- {vol.Required(CONF_DEVICE_ID): string, vol.Required(CONF_DOMAIN): str}
+ {
+ **SCRIPT_ACTION_BASE_SCHEMA,
+ vol.Required(CONF_DEVICE_ID): string,
+ vol.Required(CONF_DOMAIN): str,
+ }
)
DEVICE_ACTION_SCHEMA = DEVICE_ACTION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
-_SCRIPT_SCENE_SCHEMA = vol.Schema({vol.Required(CONF_SCENE): entity_domain("scene")})
+_SCRIPT_SCENE_SCHEMA = vol.Schema(
+ {**SCRIPT_ACTION_BASE_SCHEMA, vol.Required(CONF_SCENE): entity_domain("scene")}
+)
_SCRIPT_REPEAT_SCHEMA = vol.Schema(
{
- vol.Optional(CONF_ALIAS): string,
+ **SCRIPT_ACTION_BASE_SCHEMA,
vol.Required(CONF_REPEAT): vol.All(
{
vol.Exclusive(CONF_COUNT, "repeat"): vol.Any(vol.Coerce(int), template),
@@ -1132,11 +1154,12 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name
_SCRIPT_CHOOSE_SCHEMA = vol.Schema(
{
- vol.Optional(CONF_ALIAS): string,
+ **SCRIPT_ACTION_BASE_SCHEMA,
vol.Required(CONF_CHOOSE): vol.All(
ensure_list,
[
{
+ vol.Optional(CONF_ALIAS): string,
vol.Required(CONF_CONDITIONS): vol.All(
ensure_list, [CONDITION_SCHEMA]
),
@@ -1150,7 +1173,7 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name
_SCRIPT_WAIT_FOR_TRIGGER_SCHEMA = vol.Schema(
{
- vol.Optional(CONF_ALIAS): string,
+ **SCRIPT_ACTION_BASE_SCHEMA,
vol.Required(CONF_WAIT_FOR_TRIGGER): TRIGGER_SCHEMA,
vol.Optional(CONF_TIMEOUT): positive_time_period_template,
vol.Optional(CONF_CONTINUE_ON_TIMEOUT): boolean,
@@ -1159,7 +1182,7 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name
_SCRIPT_SET_SCHEMA = vol.Schema(
{
- vol.Optional(CONF_ALIAS): string,
+ **SCRIPT_ACTION_BASE_SCHEMA,
vol.Required(CONF_VARIABLES): SCRIPT_VARIABLES_SCHEMA,
}
)
@@ -1212,7 +1235,7 @@ def determine_script_action(action: dict) -> str:
return SCRIPT_ACTION_CALL_SERVICE
-ACTION_TYPE_SCHEMAS: Dict[str, Callable[[Any], dict]] = {
+ACTION_TYPE_SCHEMAS: dict[str, Callable[[Any], dict]] = {
SCRIPT_ACTION_CALL_SERVICE: SERVICE_SCHEMA,
SCRIPT_ACTION_DELAY: _SCRIPT_DELAY_SCHEMA,
SCRIPT_ACTION_WAIT_TEMPLATE: _SCRIPT_WAIT_TEMPLATE_SCHEMA,
diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py
index e686dd2ae4b910..00d12d3ab907dc 100644
--- a/homeassistant/helpers/data_entry_flow.py
+++ b/homeassistant/helpers/data_entry_flow.py
@@ -1,6 +1,7 @@
"""Helpers for the data entry flow."""
+from __future__ import annotations
-from typing import Any, Dict
+from typing import Any
from aiohttp import web
import voluptuous as vol
@@ -20,7 +21,7 @@ def __init__(self, flow_mgr: data_entry_flow.FlowManager) -> None:
self._flow_mgr = flow_mgr
# pylint: disable=no-self-use
- def _prepare_result_json(self, result: Dict[str, Any]) -> Dict[str, Any]:
+ def _prepare_result_json(self, result: dict[str, Any]) -> dict[str, Any]:
"""Convert result to JSON."""
if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
data = result.copy()
@@ -58,7 +59,7 @@ class FlowManagerIndexView(_BaseFlowManagerView):
extra=vol.ALLOW_EXTRA,
)
)
- async def post(self, request: web.Request, data: Dict[str, Any]) -> web.Response:
+ async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
"""Handle a POST request."""
if isinstance(data["handler"], list):
handler = tuple(data["handler"])
@@ -99,7 +100,7 @@ async def get(self, request: web.Request, flow_id: str) -> web.Response:
@RequestDataValidator(vol.Schema(dict), allow_empty=True)
async def post(
- self, request: web.Request, flow_id: str, data: Dict[str, Any]
+ self, request: web.Request, flow_id: str, data: dict[str, Any]
) -> web.Response:
"""Handle a POST request."""
try:
diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py
index 23727c2a00fe77..705f48bbd708a9 100644
--- a/homeassistant/helpers/debounce.py
+++ b/homeassistant/helpers/debounce.py
@@ -1,7 +1,9 @@
"""Debounce helper."""
+from __future__ import annotations
+
import asyncio
from logging import Logger
-from typing import Any, Awaitable, Callable, Optional
+from typing import Any, Awaitable, Callable
from homeassistant.core import HassJob, HomeAssistant, callback
@@ -16,7 +18,7 @@ def __init__(
*,
cooldown: float,
immediate: bool,
- function: Optional[Callable[..., Awaitable[Any]]] = None,
+ function: Callable[..., Awaitable[Any]] | None = None,
):
"""Initialize debounce.
@@ -29,13 +31,13 @@ def __init__(
self._function = function
self.cooldown = cooldown
self.immediate = immediate
- self._timer_task: Optional[asyncio.TimerHandle] = None
+ self._timer_task: asyncio.TimerHandle | None = None
self._execute_at_end_of_timer: bool = False
self._execute_lock = asyncio.Lock()
- self._job: Optional[HassJob] = None if function is None else HassJob(function)
+ self._job: HassJob | None = None if function is None else HassJob(function)
@property
- def function(self) -> Optional[Callable[..., Awaitable[Any]]]:
+ def function(self) -> Callable[..., Awaitable[Any]] | None:
"""Return the function being wrapped by the Debouncer."""
return self._function
diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py
index 0022f8888296e7..06f09327dc91f1 100644
--- a/homeassistant/helpers/deprecation.py
+++ b/homeassistant/helpers/deprecation.py
@@ -1,8 +1,12 @@
"""Deprecation helpers for Home Assistant."""
+from __future__ import annotations
+
import functools
import inspect
import logging
-from typing import Any, Callable, Dict, Optional
+from typing import Any, Callable
+
+from ..helpers.frame import MissingIntegrationFrame, get_integration_frame
def deprecated_substitute(substitute_name: str) -> Callable[..., Callable]:
@@ -47,15 +51,15 @@ def func_wrapper(self: Callable) -> Any:
def get_deprecated(
- config: Dict[str, Any], new_name: str, old_name: str, default: Optional[Any] = None
-) -> Optional[Any]:
+ config: dict[str, Any], new_name: str, old_name: str, default: Any | None = None
+) -> Any | None:
"""Allow an old config name to be deprecated with a replacement.
If the new config isn't found, but the old one is, the old value is used
and a warning is issued to the user.
"""
if old_name in config:
- module = inspect.getmodule(inspect.stack()[1][0])
+ module = inspect.getmodule(inspect.stack(context=0)[1].frame)
if module is not None:
module_name = module.__name__
else:
@@ -83,14 +87,32 @@ def deprecated_decorator(func: Callable) -> Callable:
"""Decorate function as deprecated."""
@functools.wraps(func)
- def deprecated_func(*args: tuple, **kwargs: Dict[str, Any]) -> Any:
+ def deprecated_func(*args: tuple, **kwargs: dict[str, Any]) -> Any:
"""Wrap for the original function."""
logger = logging.getLogger(func.__module__)
- logger.warning(
- "%s is a deprecated function. Use %s instead",
- func.__name__,
- replacement,
- )
+ try:
+ _, integration, path = get_integration_frame()
+ if path == "custom_components/":
+ logger.warning(
+ "%s was called from %s, this is a deprecated function. Use %s instead, please report this to the maintainer of %s",
+ func.__name__,
+ integration,
+ replacement,
+ integration,
+ )
+ else:
+ logger.warning(
+ "%s was called from %s, this is a deprecated function. Use %s instead",
+ func.__name__,
+ integration,
+ replacement,
+ )
+ except MissingIntegrationFrame:
+ logger.warning(
+ "%s is a deprecated function. Use %s instead",
+ func.__name__,
+ replacement,
+ )
return func(*args, **kwargs)
return deprecated_func
diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py
index c449d2ed4d02e9..e0e5130a94f27b 100644
--- a/homeassistant/helpers/device_registry.py
+++ b/homeassistant/helpers/device_registry.py
@@ -1,22 +1,26 @@
"""Provide a way to connect entities belonging to one device."""
+from __future__ import annotations
+
from collections import OrderedDict
import logging
import time
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union
+from typing import TYPE_CHECKING, Any, cast
import attr
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
-from homeassistant.core import Event, callback
+from homeassistant.core import Event, HomeAssistant, callback
+from homeassistant.loader import bind_hass
import homeassistant.util.uuid as uuid_util
from .debounce import Debouncer
-from .singleton import singleton
-from .typing import UNDEFINED, HomeAssistantType, UndefinedType
+from .typing import UNDEFINED, UndefinedType
# mypy: disallow_any_generics
if TYPE_CHECKING:
+ from homeassistant.config_entries import ConfigEntry
+
from . import entity_registry
_LOGGER = logging.getLogger(__name__)
@@ -37,6 +41,7 @@
REGISTERED_DEVICE = "registered"
DELETED_DEVICE = "deleted"
+DISABLED_CONFIG_ENTRY = "config_entry"
DISABLED_INTEGRATION = "integration"
DISABLED_USER = "user"
@@ -47,30 +52,32 @@
class DeviceEntry:
"""Device Registry Entry."""
- config_entries: Set[str] = attr.ib(converter=set, factory=set)
- connections: Set[Tuple[str, str]] = attr.ib(converter=set, factory=set)
- identifiers: Set[Tuple[str, str]] = attr.ib(converter=set, factory=set)
- manufacturer: Optional[str] = attr.ib(default=None)
- model: Optional[str] = attr.ib(default=None)
- name: Optional[str] = attr.ib(default=None)
- sw_version: Optional[str] = attr.ib(default=None)
- via_device_id: Optional[str] = attr.ib(default=None)
- area_id: Optional[str] = attr.ib(default=None)
- name_by_user: Optional[str] = attr.ib(default=None)
- entry_type: Optional[str] = attr.ib(default=None)
+ config_entries: set[str] = attr.ib(converter=set, factory=set)
+ connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set)
+ identifiers: set[tuple[str, str]] = attr.ib(converter=set, factory=set)
+ manufacturer: str | None = attr.ib(default=None)
+ model: str | None = attr.ib(default=None)
+ name: str | None = attr.ib(default=None)
+ sw_version: str | None = attr.ib(default=None)
+ via_device_id: str | None = attr.ib(default=None)
+ area_id: str | None = attr.ib(default=None)
+ name_by_user: str | None = attr.ib(default=None)
+ entry_type: str | None = attr.ib(default=None)
id: str = attr.ib(factory=uuid_util.random_uuid_hex)
# This value is not stored, just used to keep track of events to fire.
is_new: bool = attr.ib(default=False)
- disabled_by: Optional[str] = attr.ib(
+ disabled_by: str | None = attr.ib(
default=None,
validator=attr.validators.in_(
(
+ DISABLED_CONFIG_ENTRY,
DISABLED_INTEGRATION,
DISABLED_USER,
None,
)
),
)
+ suggested_area: str | None = attr.ib(default=None)
@property
def disabled(self) -> bool:
@@ -82,17 +89,17 @@ def disabled(self) -> bool:
class DeletedDeviceEntry:
"""Deleted Device Registry Entry."""
- config_entries: Set[str] = attr.ib()
- connections: Set[Tuple[str, str]] = attr.ib()
- identifiers: Set[Tuple[str, str]] = attr.ib()
+ config_entries: set[str] = attr.ib()
+ connections: set[tuple[str, str]] = attr.ib()
+ identifiers: set[tuple[str, str]] = attr.ib()
id: str = attr.ib()
- orphaned_timestamp: Optional[float] = attr.ib()
+ orphaned_timestamp: float | None = attr.ib()
def to_device_entry(
self,
config_entry_id: str,
- connections: Set[Tuple[str, str]],
- identifiers: Set[Tuple[str, str]],
+ connections: set[tuple[str, str]],
+ identifiers: set[tuple[str, str]],
) -> DeviceEntry:
"""Create DeviceEntry from DeletedDeviceEntry."""
return DeviceEntry(
@@ -128,27 +135,27 @@ def format_mac(mac: str) -> str:
class DeviceRegistry:
"""Class to hold a registry of devices."""
- devices: Dict[str, DeviceEntry]
- deleted_devices: Dict[str, DeletedDeviceEntry]
- _devices_index: Dict[str, Dict[str, Dict[Tuple[str, str], str]]]
+ devices: dict[str, DeviceEntry]
+ deleted_devices: dict[str, DeletedDeviceEntry]
+ _devices_index: dict[str, dict[str, dict[tuple[str, str], str]]]
- def __init__(self, hass: HomeAssistantType) -> None:
+ def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the device registry."""
self.hass = hass
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
self._clear_index()
@callback
- def async_get(self, device_id: str) -> Optional[DeviceEntry]:
+ def async_get(self, device_id: str) -> DeviceEntry | None:
"""Get device."""
return self.devices.get(device_id)
@callback
def async_get_device(
self,
- identifiers: Set[Tuple[str, str]],
- connections: Optional[Set[Tuple[str, str]]] = None,
- ) -> Optional[DeviceEntry]:
+ identifiers: set[tuple[str, str]],
+ connections: set[tuple[str, str]] | None = None,
+ ) -> DeviceEntry | None:
"""Check if device is registered."""
device_id = self._async_get_device_id_from_index(
REGISTERED_DEVICE, identifiers, connections
@@ -159,9 +166,9 @@ def async_get_device(
def _async_get_deleted_device(
self,
- identifiers: Set[Tuple[str, str]],
- connections: Optional[Set[Tuple[str, str]]],
- ) -> Optional[DeletedDeviceEntry]:
+ identifiers: set[tuple[str, str]],
+ connections: set[tuple[str, str]] | None,
+ ) -> DeletedDeviceEntry | None:
"""Check if device is deleted."""
device_id = self._async_get_device_id_from_index(
DELETED_DEVICE, identifiers, connections
@@ -173,9 +180,9 @@ def _async_get_deleted_device(
def _async_get_device_id_from_index(
self,
index: str,
- identifiers: Set[Tuple[str, str]],
- connections: Optional[Set[Tuple[str, str]]],
- ) -> Optional[str]:
+ identifiers: set[tuple[str, str]],
+ connections: set[tuple[str, str]] | None,
+ ) -> str | None:
"""Check if device has previously been registered."""
devices_index = self._devices_index[index]
for identifier in identifiers:
@@ -188,7 +195,7 @@ def _async_get_device_id_from_index(
return devices_index[IDX_CONNECTIONS][connection]
return None
- def _add_device(self, device: Union[DeviceEntry, DeletedDeviceEntry]) -> None:
+ def _add_device(self, device: DeviceEntry | DeletedDeviceEntry) -> None:
"""Add a device and index it."""
if isinstance(device, DeletedDeviceEntry):
devices_index = self._devices_index[DELETED_DEVICE]
@@ -199,7 +206,7 @@ def _add_device(self, device: Union[DeviceEntry, DeletedDeviceEntry]) -> None:
_add_device_to_index(devices_index, device)
- def _remove_device(self, device: Union[DeviceEntry, DeletedDeviceEntry]) -> None:
+ def _remove_device(self, device: DeviceEntry | DeletedDeviceEntry) -> None:
"""Remove a device and remove it from the index."""
if isinstance(device, DeletedDeviceEntry):
devices_index = self._devices_index[DELETED_DEVICE]
@@ -238,20 +245,21 @@ def async_get_or_create(
self,
*,
config_entry_id: str,
- connections: Optional[Set[Tuple[str, str]]] = None,
- identifiers: Optional[Set[Tuple[str, str]]] = None,
- manufacturer: Union[str, None, UndefinedType] = UNDEFINED,
- model: Union[str, None, UndefinedType] = UNDEFINED,
- name: Union[str, None, UndefinedType] = UNDEFINED,
- default_manufacturer: Union[str, None, UndefinedType] = UNDEFINED,
- default_model: Union[str, None, UndefinedType] = UNDEFINED,
- default_name: Union[str, None, UndefinedType] = UNDEFINED,
- sw_version: Union[str, None, UndefinedType] = UNDEFINED,
- entry_type: Union[str, None, UndefinedType] = UNDEFINED,
- via_device: Optional[Tuple[str, str]] = None,
+ connections: set[tuple[str, str]] | None = None,
+ identifiers: set[tuple[str, str]] | None = None,
+ manufacturer: str | None | UndefinedType = UNDEFINED,
+ model: str | None | UndefinedType = UNDEFINED,
+ name: str | None | UndefinedType = UNDEFINED,
+ default_manufacturer: str | None | UndefinedType = UNDEFINED,
+ default_model: str | None | UndefinedType = UNDEFINED,
+ default_name: str | None | UndefinedType = UNDEFINED,
+ sw_version: str | None | UndefinedType = UNDEFINED,
+ entry_type: str | None | UndefinedType = UNDEFINED,
+ via_device: tuple[str, str] | None = None,
# To disable a device if it gets created
- disabled_by: Union[str, None, UndefinedType] = UNDEFINED,
- ) -> Optional[DeviceEntry]:
+ disabled_by: str | None | UndefinedType = UNDEFINED,
+ suggested_area: str | None | UndefinedType = UNDEFINED,
+ ) -> DeviceEntry | None:
"""Get device. Create if it doesn't exist."""
if not identifiers and not connections:
return None
@@ -288,7 +296,7 @@ def async_get_or_create(
if via_device is not None:
via = self.async_get_device({via_device})
- via_device_id: Union[str, UndefinedType] = via.id if via else UNDEFINED
+ via_device_id: str | UndefinedType = via.id if via else UNDEFINED
else:
via_device_id = UNDEFINED
@@ -304,6 +312,7 @@ def async_get_or_create(
sw_version=sw_version,
entry_type=entry_type,
disabled_by=disabled_by,
+ suggested_area=suggested_area,
)
@callback
@@ -311,17 +320,18 @@ def async_update_device(
self,
device_id: str,
*,
- area_id: Union[str, None, UndefinedType] = UNDEFINED,
- manufacturer: Union[str, None, UndefinedType] = UNDEFINED,
- model: Union[str, None, UndefinedType] = UNDEFINED,
- name: Union[str, None, UndefinedType] = UNDEFINED,
- name_by_user: Union[str, None, UndefinedType] = UNDEFINED,
- new_identifiers: Union[Set[Tuple[str, str]], UndefinedType] = UNDEFINED,
- sw_version: Union[str, None, UndefinedType] = UNDEFINED,
- via_device_id: Union[str, None, UndefinedType] = UNDEFINED,
- remove_config_entry_id: Union[str, UndefinedType] = UNDEFINED,
- disabled_by: Union[str, None, UndefinedType] = UNDEFINED,
- ) -> Optional[DeviceEntry]:
+ area_id: str | None | UndefinedType = UNDEFINED,
+ manufacturer: str | None | UndefinedType = UNDEFINED,
+ model: str | None | UndefinedType = UNDEFINED,
+ name: str | None | UndefinedType = UNDEFINED,
+ name_by_user: str | None | UndefinedType = UNDEFINED,
+ new_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED,
+ sw_version: str | None | UndefinedType = UNDEFINED,
+ via_device_id: str | None | UndefinedType = UNDEFINED,
+ remove_config_entry_id: str | UndefinedType = UNDEFINED,
+ disabled_by: str | None | UndefinedType = UNDEFINED,
+ suggested_area: str | None | UndefinedType = UNDEFINED,
+ ) -> DeviceEntry | None:
"""Update properties of a device."""
return self._async_update_device(
device_id,
@@ -335,6 +345,7 @@ def async_update_device(
via_device_id=via_device_id,
remove_config_entry_id=remove_config_entry_id,
disabled_by=disabled_by,
+ suggested_area=suggested_area,
)
@callback
@@ -342,28 +353,39 @@ def _async_update_device(
self,
device_id: str,
*,
- add_config_entry_id: Union[str, UndefinedType] = UNDEFINED,
- remove_config_entry_id: Union[str, UndefinedType] = UNDEFINED,
- merge_connections: Union[Set[Tuple[str, str]], UndefinedType] = UNDEFINED,
- merge_identifiers: Union[Set[Tuple[str, str]], UndefinedType] = UNDEFINED,
- new_identifiers: Union[Set[Tuple[str, str]], UndefinedType] = UNDEFINED,
- manufacturer: Union[str, None, UndefinedType] = UNDEFINED,
- model: Union[str, None, UndefinedType] = UNDEFINED,
- name: Union[str, None, UndefinedType] = UNDEFINED,
- sw_version: Union[str, None, UndefinedType] = UNDEFINED,
- entry_type: Union[str, None, UndefinedType] = UNDEFINED,
- via_device_id: Union[str, None, UndefinedType] = UNDEFINED,
- area_id: Union[str, None, UndefinedType] = UNDEFINED,
- name_by_user: Union[str, None, UndefinedType] = UNDEFINED,
- disabled_by: Union[str, None, UndefinedType] = UNDEFINED,
- ) -> Optional[DeviceEntry]:
+ add_config_entry_id: str | UndefinedType = UNDEFINED,
+ remove_config_entry_id: str | UndefinedType = UNDEFINED,
+ merge_connections: set[tuple[str, str]] | UndefinedType = UNDEFINED,
+ merge_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED,
+ new_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED,
+ manufacturer: str | None | UndefinedType = UNDEFINED,
+ model: str | None | UndefinedType = UNDEFINED,
+ name: str | None | UndefinedType = UNDEFINED,
+ sw_version: str | None | UndefinedType = UNDEFINED,
+ entry_type: str | None | UndefinedType = UNDEFINED,
+ via_device_id: str | None | UndefinedType = UNDEFINED,
+ area_id: str | None | UndefinedType = UNDEFINED,
+ name_by_user: str | None | UndefinedType = UNDEFINED,
+ disabled_by: str | None | UndefinedType = UNDEFINED,
+ suggested_area: str | None | UndefinedType = UNDEFINED,
+ ) -> DeviceEntry | None:
"""Update device attributes."""
old = self.devices[device_id]
- changes: Dict[str, Any] = {}
+ changes: dict[str, Any] = {}
config_entries = old.config_entries
+ if (
+ suggested_area not in (UNDEFINED, None, "")
+ and area_id is UNDEFINED
+ and old.area_id is None
+ ):
+ area = self.hass.helpers.area_registry.async_get(
+ self.hass
+ ).async_get_or_create(suggested_area)
+ area_id = area.id
+
if (
add_config_entry_id is not UNDEFINED
and add_config_entry_id not in old.config_entries
@@ -403,6 +425,7 @@ def _async_update_device(
("entry_type", entry_type),
("via_device_id", via_device_id),
("disabled_by", disabled_by),
+ ("suggested_area", suggested_area),
):
if value is not UNDEFINED and value != getattr(old, attr_name):
changes[attr_name] = value
@@ -508,7 +531,7 @@ def async_schedule_save(self) -> None:
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
@callback
- def _data_to_save(self) -> Dict[str, List[Dict[str, Any]]]:
+ def _data_to_save(self) -> dict[str, list[dict[str, Any]]]:
"""Return data of device registry to store in a file."""
data = {}
@@ -593,16 +616,30 @@ def async_clear_area_id(self, area_id: str) -> None:
self._async_update_device(dev_id, area_id=None)
-@singleton(DATA_REGISTRY)
-async def async_get_registry(hass: HomeAssistantType) -> DeviceRegistry:
- """Create entity registry."""
- reg = DeviceRegistry(hass)
- await reg.async_load()
- return reg
+@callback
+def async_get(hass: HomeAssistant) -> DeviceRegistry:
+ """Get device registry."""
+ return cast(DeviceRegistry, hass.data[DATA_REGISTRY])
+
+
+async def async_load(hass: HomeAssistant) -> None:
+ """Load device registry."""
+ assert DATA_REGISTRY not in hass.data
+ hass.data[DATA_REGISTRY] = DeviceRegistry(hass)
+ await hass.data[DATA_REGISTRY].async_load()
+
+
+@bind_hass
+async def async_get_registry(hass: HomeAssistant) -> DeviceRegistry:
+ """Get device registry.
+
+ This is deprecated and will be removed in the future. Use async_get instead.
+ """
+ return async_get(hass)
@callback
-def async_entries_for_area(registry: DeviceRegistry, area_id: str) -> List[DeviceEntry]:
+def async_entries_for_area(registry: DeviceRegistry, area_id: str) -> list[DeviceEntry]:
"""Return entries that match an area."""
return [device for device in registry.devices.values() if device.area_id == area_id]
@@ -610,7 +647,7 @@ def async_entries_for_area(registry: DeviceRegistry, area_id: str) -> List[Devic
@callback
def async_entries_for_config_entry(
registry: DeviceRegistry, config_entry_id: str
-) -> List[DeviceEntry]:
+) -> list[DeviceEntry]:
"""Return entries that match a config entry."""
return [
device
@@ -619,11 +656,39 @@ def async_entries_for_config_entry(
]
+@callback
+def async_config_entry_disabled_by_changed(
+ registry: DeviceRegistry, config_entry: ConfigEntry
+) -> None:
+ """Handle a config entry being disabled or enabled.
+
+ Disable devices in the registry that are associated with a config entry when
+ the config entry is disabled, enable devices in the registry that are associated
+ with a config entry when the config entry is enabled and the devices are marked
+ DISABLED_CONFIG_ENTRY.
+ """
+
+ devices = async_entries_for_config_entry(registry, config_entry.entry_id)
+
+ if not config_entry.disabled_by:
+ for device in devices:
+ if device.disabled_by != DISABLED_CONFIG_ENTRY:
+ continue
+ registry.async_update_device(device.id, disabled_by=None)
+ return
+
+ for device in devices:
+ if device.disabled:
+ # Device already disabled, do not overwrite
+ continue
+ registry.async_update_device(device.id, disabled_by=DISABLED_CONFIG_ENTRY)
+
+
@callback
def async_cleanup(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
dev_reg: DeviceRegistry,
- ent_reg: "entity_registry.EntityRegistry",
+ ent_reg: entity_registry.EntityRegistry,
) -> None:
"""Clean up device registry."""
# Find all devices that are referenced by a config_entry.
@@ -658,7 +723,7 @@ def async_cleanup(
@callback
-def async_setup_cleanup(hass: HomeAssistantType, dev_reg: DeviceRegistry) -> None:
+def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None:
"""Clean up device registry when entities removed."""
from . import entity_registry # pylint: disable=import-outside-toplevel
@@ -672,32 +737,41 @@ async def cleanup() -> None:
)
async def entity_registry_changed(event: Event) -> None:
- """Handle entity updated or removed."""
+ """Handle entity updated or removed dispatch."""
+ await debounced_cleanup.async_call()
+
+ @callback
+ def entity_registry_changed_filter(event: Event) -> bool:
+ """Handle entity updated or removed filter."""
if (
event.data["action"] == "update"
and "device_id" not in event.data["changes"]
) or event.data["action"] == "create":
- return
+ return False
- await debounced_cleanup.async_call()
+ return True
if hass.is_running:
hass.bus.async_listen(
- entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, entity_registry_changed
+ entity_registry.EVENT_ENTITY_REGISTRY_UPDATED,
+ entity_registry_changed,
+ event_filter=entity_registry_changed_filter,
)
return
async def startup_clean(event: Event) -> None:
"""Clean up on startup."""
hass.bus.async_listen(
- entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, entity_registry_changed
+ entity_registry.EVENT_ENTITY_REGISTRY_UPDATED,
+ entity_registry_changed,
+ event_filter=entity_registry_changed_filter,
)
await debounced_cleanup.async_call()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, startup_clean)
-def _normalize_connections(connections: Set[Tuple[str, str]]) -> Set[Tuple[str, str]]:
+def _normalize_connections(connections: set[tuple[str, str]]) -> set[tuple[str, str]]:
"""Normalize connections to ensure we can match mac addresses."""
return {
(key, format_mac(value)) if key == CONNECTION_NETWORK_MAC else (key, value)
@@ -706,8 +780,8 @@ def _normalize_connections(connections: Set[Tuple[str, str]]) -> Set[Tuple[str,
def _add_device_to_index(
- devices_index: Dict[str, Dict[Tuple[str, str], str]],
- device: Union[DeviceEntry, DeletedDeviceEntry],
+ devices_index: dict[str, dict[tuple[str, str], str]],
+ device: DeviceEntry | DeletedDeviceEntry,
) -> None:
"""Add a device to the index."""
for identifier in device.identifiers:
@@ -717,8 +791,8 @@ def _add_device_to_index(
def _remove_device_from_index(
- devices_index: Dict[str, Dict[Tuple[str, str], str]],
- device: Union[DeviceEntry, DeletedDeviceEntry],
+ devices_index: dict[str, dict[tuple[str, str], str]],
+ device: DeviceEntry | DeletedDeviceEntry,
) -> None:
"""Remove a device from the index."""
for identifier in device.identifiers:
diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py
index acde8d73a50be9..53dbca867d7a54 100644
--- a/homeassistant/helpers/discovery.py
+++ b/homeassistant/helpers/discovery.py
@@ -5,55 +5,57 @@
- listen_platform/discover_platform is for platforms. These are used by
components to allow discovery of their platforms.
"""
-from typing import Any, Callable, Collection, Dict, Optional, Union
+from __future__ import annotations
+
+from typing import Any, Callable, TypedDict
from homeassistant import core, setup
-from homeassistant.const import ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED
-from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
+from homeassistant.core import CALLBACK_TYPE
from homeassistant.loader import bind_hass
-from homeassistant.util.async_ import run_callback_threadsafe
+from .dispatcher import async_dispatcher_connect, async_dispatcher_send
+from .typing import ConfigType, DiscoveryInfoType
+
+SIGNAL_PLATFORM_DISCOVERED = "discovery.platform_discovered_{}"
EVENT_LOAD_PLATFORM = "load_platform.{}"
ATTR_PLATFORM = "platform"
+ATTR_DISCOVERED = "discovered"
+# mypy: disallow-any-generics
-@bind_hass
-def listen(
- hass: core.HomeAssistant, service: Union[str, Collection[str]], callback: Callable
-) -> None:
- """Set up listener for discovery of specific service.
- Service can be a string or a list/tuple.
- """
- run_callback_threadsafe(hass.loop, async_listen, hass, service, callback).result()
+class DiscoveryDict(TypedDict):
+ """Discovery data."""
+
+ service: str
+ platform: str | None
+ discovered: DiscoveryInfoType | None
@core.callback
@bind_hass
def async_listen(
- hass: core.HomeAssistant, service: Union[str, Collection[str]], callback: Callable
+ hass: core.HomeAssistant,
+ service: str,
+ callback: CALLBACK_TYPE,
) -> None:
"""Set up listener for discovery of specific service.
Service can be a string or a list/tuple.
"""
- if isinstance(service, str):
- service = (service,)
- else:
- service = tuple(service)
-
job = core.HassJob(callback)
- async def discovery_event_listener(event: core.Event) -> None:
+ async def discovery_event_listener(discovered: DiscoveryDict) -> None:
"""Listen for discovery events."""
- if ATTR_SERVICE in event.data and event.data[ATTR_SERVICE] in service:
- task = hass.async_run_hass_job(
- job, event.data[ATTR_SERVICE], event.data.get(ATTR_DISCOVERED)
- )
- if task:
- await task
+ task = hass.async_run_hass_job(
+ job, discovered["service"], discovered["discovered"]
+ )
+ if task:
+ await task
- hass.bus.async_listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener)
+ async_dispatcher_connect(
+ hass, SIGNAL_PLATFORM_DISCOVERED.format(service), discovery_event_listener
+ )
@bind_hass
@@ -76,37 +78,28 @@ def discover(
async def async_discover(
hass: core.HomeAssistant,
service: str,
- discovered: Optional[DiscoveryInfoType],
- component: Optional[str],
+ discovered: DiscoveryInfoType | None,
+ component: str | None,
hass_config: ConfigType,
) -> None:
"""Fire discovery event. Can ensure a component is loaded."""
if component is not None and component not in hass.config.components:
await setup.async_setup_component(hass, component, hass_config)
- data: Dict[str, Any] = {ATTR_SERVICE: service}
-
- if discovered is not None:
- data[ATTR_DISCOVERED] = discovered
-
- hass.bus.async_fire(EVENT_PLATFORM_DISCOVERED, data)
-
+ data: DiscoveryDict = {
+ "service": service,
+ "platform": None,
+ "discovered": discovered,
+ }
-@bind_hass
-def listen_platform(
- hass: core.HomeAssistant, component: str, callback: Callable
-) -> None:
- """Register a platform loader listener."""
- run_callback_threadsafe(
- hass.loop, async_listen_platform, hass, component, callback
- ).result()
+ async_dispatcher_send(hass, SIGNAL_PLATFORM_DISCOVERED.format(service), data)
@bind_hass
def async_listen_platform(
hass: core.HomeAssistant,
component: str,
- callback: Callable[[str, Optional[Dict[str, Any]]], Any],
+ callback: Callable[[str, dict[str, Any] | None], Any],
) -> None:
"""Register a platform loader listener.
@@ -115,21 +108,20 @@ def async_listen_platform(
service = EVENT_LOAD_PLATFORM.format(component)
job = core.HassJob(callback)
- async def discovery_platform_listener(event: core.Event) -> None:
+ async def discovery_platform_listener(discovered: DiscoveryDict) -> None:
"""Listen for platform discovery events."""
- if event.data.get(ATTR_SERVICE) != service:
- return
-
- platform = event.data.get(ATTR_PLATFORM)
+ platform = discovered["platform"]
if not platform:
return
- task = hass.async_run_hass_job(job, platform, event.data.get(ATTR_DISCOVERED))
+ task = hass.async_run_hass_job(job, platform, discovered.get("discovered"))
if task:
await task
- hass.bus.async_listen(EVENT_PLATFORM_DISCOVERED, discovery_platform_listener)
+ async_dispatcher_connect(
+ hass, SIGNAL_PLATFORM_DISCOVERED.format(service), discovery_platform_listener
+ )
@bind_hass
@@ -140,16 +132,7 @@ def load_platform(
discovered: DiscoveryInfoType,
hass_config: ConfigType,
) -> None:
- """Load a component and platform dynamically.
-
- Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be
- fired to load the platform. The event will contain:
- { ATTR_SERVICE = EVENT_LOAD_PLATFORM + '.' + <>
- ATTR_PLATFORM = <>
- ATTR_DISCOVERED = <> }
-
- Use `listen_platform` to register a callback for these events.
- """
+ """Load a component and platform dynamically."""
hass.add_job(
async_load_platform( # type: ignore
hass, component, platform, discovered, hass_config
@@ -167,18 +150,10 @@ async def async_load_platform(
) -> None:
"""Load a component and platform dynamically.
- Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be
- fired to load the platform. The event will contain:
- { ATTR_SERVICE = EVENT_LOAD_PLATFORM + '.' + <>
- ATTR_PLATFORM = <>
- ATTR_DISCOVERED = <> }
-
- Use `listen_platform` to register a callback for these events.
+ Use `async_listen_platform` to register a callback for these events.
Warning: Do not await this inside a setup method to avoid a dead lock.
Use `hass.async_create_task(async_load_platform(..))` instead.
-
- This method is a coroutine.
"""
assert hass_config, "You need to pass in the real hass config"
@@ -187,16 +162,16 @@ async def async_load_platform(
if component not in hass.config.components:
setup_success = await setup.async_setup_component(hass, component, hass_config)
- # No need to fire event if we could not set up component
+ # No need to send signal if we could not set up component
if not setup_success:
return
- data: Dict[str, Any] = {
- ATTR_SERVICE: EVENT_LOAD_PLATFORM.format(component),
- ATTR_PLATFORM: platform,
- }
+ service = EVENT_LOAD_PLATFORM.format(component)
- if discovered is not None:
- data[ATTR_DISCOVERED] = discovered
+ data: DiscoveryDict = {
+ "service": service,
+ "platform": platform,
+ "discovered": discovered,
+ }
- hass.bus.async_fire(EVENT_PLATFORM_DISCOVERED, data)
+ async_dispatcher_send(hass, SIGNAL_PLATFORM_DISCOVERED.format(service), data)
diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py
index cdf24ec23e98f0..2b365412e27ef6 100644
--- a/homeassistant/helpers/dispatcher.py
+++ b/homeassistant/helpers/dispatcher.py
@@ -2,20 +2,18 @@
import logging
from typing import Any, Callable
-from homeassistant.core import HassJob, callback
+from homeassistant.core import HassJob, HomeAssistant, callback
from homeassistant.loader import bind_hass
from homeassistant.util.async_ import run_callback_threadsafe
from homeassistant.util.logging import catch_log_exception
-from .typing import HomeAssistantType
-
_LOGGER = logging.getLogger(__name__)
DATA_DISPATCHER = "dispatcher"
@bind_hass
def dispatcher_connect(
- hass: HomeAssistantType, signal: str, target: Callable[..., None]
+ hass: HomeAssistant, signal: str, target: Callable[..., None]
) -> Callable[[], None]:
"""Connect a callable function to a signal."""
async_unsub = run_callback_threadsafe(
@@ -32,7 +30,7 @@ def remove_dispatcher() -> None:
@callback
@bind_hass
def async_dispatcher_connect(
- hass: HomeAssistantType, signal: str, target: Callable[..., Any]
+ hass: HomeAssistant, signal: str, target: Callable[..., Any]
) -> Callable[[], None]:
"""Connect a callable function to a signal.
@@ -69,14 +67,14 @@ def async_remove_dispatcher() -> None:
@bind_hass
-def dispatcher_send(hass: HomeAssistantType, signal: str, *args: Any) -> None:
+def dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None:
"""Send signal and data."""
hass.loop.call_soon_threadsafe(async_dispatcher_send, hass, signal, *args)
@callback
@bind_hass
-def async_dispatcher_send(hass: HomeAssistantType, signal: str, *args: Any) -> None:
+def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None:
"""Send signal and data.
This method must be run in the event loop.
diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py
index 03342a9f235392..0074c0ba5e89d0 100644
--- a/homeassistant/helpers/entity.py
+++ b/homeassistant/helpers/entity.py
@@ -1,11 +1,14 @@
"""An abstract class for entities."""
+from __future__ import annotations
+
from abc import ABC
import asyncio
+from collections.abc import Mapping
from datetime import datetime, timedelta
import functools as ft
import logging
from timeit import default_timer as timer
-from typing import Any, Awaitable, Dict, Iterable, List, Optional
+from typing import Any, Awaitable, Iterable
from homeassistant.config import DATA_CUSTOMIZE
from homeassistant.const import (
@@ -42,16 +45,16 @@
@callback
@bind_hass
-def entity_sources(hass: HomeAssistant) -> Dict[str, Dict[str, str]]:
+def entity_sources(hass: HomeAssistant) -> dict[str, dict[str, str]]:
"""Get the entity sources."""
return hass.data.get(DATA_ENTITY_SOURCE, {})
def generate_entity_id(
entity_id_format: str,
- name: Optional[str],
- current_ids: Optional[List[str]] = None,
- hass: Optional[HomeAssistant] = None,
+ name: str | None,
+ current_ids: list[str] | None = None,
+ hass: HomeAssistant | None = None,
) -> str:
"""Generate a unique entity ID based on given entity IDs or used IDs."""
return async_generate_entity_id(entity_id_format, name, current_ids, hass)
@@ -60,12 +63,11 @@ def generate_entity_id(
@callback
def async_generate_entity_id(
entity_id_format: str,
- name: Optional[str],
- current_ids: Optional[Iterable[str]] = None,
- hass: Optional[HomeAssistant] = None,
+ name: str | None,
+ current_ids: Iterable[str] | None = None,
+ hass: HomeAssistant | None = None,
) -> str:
"""Generate a unique entity ID based on given entity IDs or used IDs."""
-
name = (name or DEVICE_DEFAULT_NAME).lower()
preferred_string = entity_id_format.format(slugify(name))
@@ -90,13 +92,16 @@ class Entity(ABC):
# SAFE TO OVERWRITE
# The properties and methods here are safe to overwrite when inheriting
# this class. These may be used to customize the behavior of the entity.
- entity_id = None # type: str
+ entity_id: str = None # type: ignore
# Owning hass instance. Will be set by EntityPlatform
- hass: Optional[HomeAssistant] = None
+ # While not purely typed, it makes typehinting more useful for us
+ # and removes the need for constant None checks or asserts.
+ # Ignore types: https://github.com/PyCQA/pylint/issues/3167
+ hass: HomeAssistant = None # type: ignore
# Owning platform instance. Will be set by EntityPlatform
- platform: Optional[EntityPlatform] = None
+ platform: EntityPlatform | None = None
# If we reported if this entity was slow
_slow_reported = False
@@ -108,17 +113,17 @@ class Entity(ABC):
_update_staged = False
# Process updates in parallel
- parallel_updates: Optional[asyncio.Semaphore] = None
+ parallel_updates: asyncio.Semaphore | None = None
# Entry in the entity registry
- registry_entry: Optional[RegistryEntry] = None
+ registry_entry: RegistryEntry | None = None
# Hold list for functions to call on remove.
- _on_remove: Optional[List[CALLBACK_TYPE]] = None
+ _on_remove: list[CALLBACK_TYPE] | None = None
# Context
- _context: Optional[Context] = None
- _context_set: Optional[datetime] = None
+ _context: Context | None = None
+ _context_set: datetime | None = None
# If entity is added to an entity platform
_added = False
@@ -132,12 +137,12 @@ def should_poll(self) -> bool:
return True
@property
- def unique_id(self) -> Optional[str]:
+ def unique_id(self) -> str | None:
"""Return a unique ID."""
return None
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
"""Return the name of the entity."""
return None
@@ -147,7 +152,7 @@ def state(self) -> StateType:
return STATE_UNKNOWN
@property
- def capability_attributes(self) -> Optional[Dict[str, Any]]:
+ def capability_attributes(self) -> Mapping[str, Any] | None:
"""Return the capability attributes.
Attributes that explain the capabilities of an entity.
@@ -158,17 +163,26 @@ def capability_attributes(self) -> Optional[Dict[str, Any]]:
return None
@property
- def state_attributes(self) -> Optional[Dict[str, Any]]:
+ def state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes.
- Implemented by component base class. Convention for attribute names
- is lowercase snake_case.
+ Implemented by component base class, should not be extended by integrations.
+ Convention for attribute names is lowercase snake_case.
"""
return None
@property
- def device_state_attributes(self) -> Optional[Dict[str, Any]]:
- """Return device specific state attributes.
+ def device_state_attributes(self) -> Mapping[str, Any] | None:
+ """Return entity specific state attributes.
+
+ This method is deprecated, platform classes should implement
+ extra_state_attributes instead.
+ """
+ return None
+
+ @property
+ def extra_state_attributes(self) -> Mapping[str, Any] | None:
+ """Return entity specific state attributes.
Implemented by platform classes. Convention for attribute names
is lowercase snake_case.
@@ -176,7 +190,7 @@ def device_state_attributes(self) -> Optional[Dict[str, Any]]:
return None
@property
- def device_info(self) -> Optional[Dict[str, Any]]:
+ def device_info(self) -> Mapping[str, Any] | None:
"""Return device specific attributes.
Implemented by platform classes.
@@ -184,22 +198,22 @@ def device_info(self) -> Optional[Dict[str, Any]]:
return None
@property
- def device_class(self) -> Optional[str]:
+ def device_class(self) -> str | None:
"""Return the class of this device, from component DEVICE_CLASSES."""
return None
@property
- def unit_of_measurement(self) -> Optional[str]:
+ def unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of this entity, if any."""
return None
@property
- def icon(self) -> Optional[str]:
+ def icon(self) -> str | None:
"""Return the icon to use in the frontend, if any."""
return None
@property
- def entity_picture(self) -> Optional[str]:
+ def entity_picture(self) -> str | None:
"""Return the entity picture to use in the frontend, if any."""
return None
@@ -223,7 +237,7 @@ def force_update(self) -> bool:
return False
@property
- def supported_features(self) -> Optional[int]:
+ def supported_features(self) -> int | None:
"""Flag supported features."""
return None
@@ -320,7 +334,12 @@ def _async_write_ha_state(self) -> None:
sstate = self.state
state = STATE_UNKNOWN if sstate is None else str(sstate)
attr.update(self.state_attributes or {})
- attr.update(self.device_state_attributes or {})
+ extra_state_attributes = self.extra_state_attributes
+ # Backwards compatibility for "device_state_attributes" deprecated in 2021.4
+ # Add warning in 2021.6, remove in 2021.10
+ if extra_state_attributes is None:
+ extra_state_attributes = self.device_state_attributes
+ attr.update(extra_state_attributes or {})
unit_of_measurement = self.unit_of_measurement
if unit_of_measurement is not None:
@@ -378,7 +397,6 @@ def _async_write_ha_state(self) -> None:
)
# Overwrite properties that have been set in the config file.
- assert self.hass is not None
if DATA_CUSTOMIZE in self.hass.data:
attr.update(self.hass.data[DATA_CUSTOMIZE].get(self.entity_id))
@@ -419,7 +437,6 @@ def schedule_update_ha_state(self, force_refresh: bool = False) -> None:
If state is changed more than once before the ha state change task has
been executed, the intermediate state transitions will be missed.
"""
- assert self.hass is not None
self.hass.add_job(self.async_update_ha_state(force_refresh)) # type: ignore
@callback
@@ -435,7 +452,6 @@ def async_schedule_update_ha_state(self, force_refresh: bool = False) -> None:
been executed, the intermediate state transitions will be missed.
"""
if force_refresh:
- assert self.hass is not None
self.hass.async_create_task(self.async_update_ha_state(force_refresh))
else:
self.async_write_ha_state()
@@ -503,7 +519,7 @@ def add_to_platform_start(
self,
hass: HomeAssistant,
platform: EntityPlatform,
- parallel_updates: Optional[asyncio.Semaphore],
+ parallel_updates: asyncio.Semaphore | None,
) -> None:
"""Start adding an entity to a platform."""
if self._added:
@@ -519,7 +535,7 @@ def add_to_platform_start(
@callback
def add_to_platform_abort(self) -> None:
"""Abort adding an entity to a platform."""
- self.hass = None
+ self.hass = None # type: ignore
self.platform = None
self.parallel_updates = None
self._added = False
@@ -530,10 +546,16 @@ async def add_to_platform_finish(self) -> None:
await self.async_added_to_hass()
self.async_write_ha_state()
- async def async_remove(self) -> None:
- """Remove entity from Home Assistant."""
- assert self.hass is not None
+ async def async_remove(self, *, force_remove: bool = False) -> None:
+ """Remove entity from Home Assistant.
+
+ If the entity has a non disabled entry in the entity registry,
+ the entity's state will be set to unavailable, in the same way
+ as when the entity registry is loaded.
+ If the entity doesn't have a non disabled entry in the entity registry,
+ or if force_remove=True, its state will be removed.
+ """
if self.platform and not self._added:
raise HomeAssistantError(
f"Entity {self.entity_id} async_remove called twice"
@@ -548,7 +570,16 @@ async def async_remove(self) -> None:
await self.async_internal_will_remove_from_hass()
await self.async_will_remove_from_hass()
- self.hass.states.async_remove(self.entity_id, context=self._context)
+ # Check if entry still exists in entity registry (e.g. unloading config entry)
+ if (
+ not force_remove
+ and self.registry_entry
+ and not self.registry_entry.disabled
+ ):
+ # Set the entity's state will to unavailable + ATTR_RESTORED: True
+ self.registry_entry.write_unavailable_state(self.hass)
+ else:
+ self.hass.states.async_remove(self.entity_id, context=self._context)
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass.
@@ -567,8 +598,6 @@ async def async_internal_added_to_hass(self) -> None:
Not to be extended by integrations.
"""
- assert self.hass is not None
-
if self.platform:
info = {"domain": self.platform.platform_name}
@@ -598,7 +627,6 @@ async def async_internal_will_remove_from_hass(self) -> None:
Not to be extended by integrations.
"""
if self.platform:
- assert self.hass is not None
self.hass.data[DATA_ENTITY_SOURCE].pop(self.entity_id)
async def _async_registry_updated(self, event: Event) -> None:
@@ -606,18 +634,18 @@ async def _async_registry_updated(self, event: Event) -> None:
data = event.data
if data["action"] == "remove":
await self.async_removed_from_registry()
+ self.registry_entry = None
await self.async_remove()
if data["action"] != "update":
return
- assert self.hass is not None
ent_reg = await self.hass.helpers.entity_registry.async_get_registry()
old = self.registry_entry
self.registry_entry = ent_reg.async_get(data["entity_id"])
assert self.registry_entry is not None
- if self.registry_entry.disabled_by is not None:
+ if self.registry_entry.disabled:
await self.async_remove()
return
@@ -626,7 +654,7 @@ async def _async_registry_updated(self, event: Event) -> None:
self.async_write_ha_state()
return
- await self.async_remove()
+ await self.async_remove(force_remove=True)
assert self.platform is not None
self.entity_id = self.registry_entry.entity_id
@@ -686,7 +714,6 @@ def turn_on(self, **kwargs: Any) -> None:
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
- assert self.hass is not None
await self.hass.async_add_executor_job(ft.partial(self.turn_on, **kwargs))
def turn_off(self, **kwargs: Any) -> None:
@@ -695,7 +722,6 @@ def turn_off(self, **kwargs: Any) -> None:
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
- assert self.hass is not None
await self.hass.async_add_executor_job(ft.partial(self.turn_off, **kwargs))
def toggle(self, **kwargs: Any) -> None:
diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py
index 0f1f04e3aec10e..17131665240466 100644
--- a/homeassistant/helpers/entity_component.py
+++ b/homeassistant/helpers/entity_component.py
@@ -1,10 +1,12 @@
"""Helpers for components that manage entities."""
+from __future__ import annotations
+
import asyncio
from datetime import timedelta
from itertools import chain
import logging
from types import ModuleType
-from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
+from typing import Any, Callable, Iterable
import voluptuous as vol
@@ -76,10 +78,10 @@ def __init__(
self.domain = domain
self.scan_interval = scan_interval
- self.config: Optional[ConfigType] = None
+ self.config: ConfigType | None = None
- self._platforms: Dict[
- Union[str, Tuple[str, Optional[timedelta], Optional[str]]], EntityPlatform
+ self._platforms: dict[
+ str | tuple[str, timedelta | None, str | None], EntityPlatform
] = {domain: self._async_init_entity_platform(domain, None)}
self.async_add_entities = self._platforms[domain].async_add_entities
self.add_entities = self._platforms[domain].add_entities
@@ -93,7 +95,7 @@ def entities(self) -> Iterable[entity.Entity]:
platform.entities.values() for platform in self._platforms.values()
)
- def get_entity(self, entity_id: str) -> Optional[entity.Entity]:
+ def get_entity(self, entity_id: str) -> entity.Entity | None:
"""Get an entity."""
for platform in self._platforms.values():
entity_obj = platform.entities.get(entity_id)
@@ -125,7 +127,7 @@ async def async_setup(self, config: ConfigType) -> None:
# Generic discovery listener for loading platform dynamically
# Refer to: homeassistant.helpers.discovery.async_load_platform()
async def component_platform_discovered(
- platform: str, info: Optional[Dict[str, Any]]
+ platform: str, info: dict[str, Any] | None
) -> None:
"""Handle the loading of a platform."""
await self.async_setup_platform(platform, {}, info)
@@ -176,7 +178,7 @@ async def async_unload_entry(self, config_entry: ConfigEntry) -> bool:
async def async_extract_from_service(
self, service_call: ServiceCall, expand_group: bool = True
- ) -> List[entity.Entity]:
+ ) -> list[entity.Entity]:
"""Extract all known and available entities from a service call.
Will return an empty list if entities specified but unknown.
@@ -191,9 +193,9 @@ async def async_extract_from_service(
def async_register_entity_service(
self,
name: str,
- schema: Union[Dict[str, Any], vol.Schema],
- func: Union[str, Callable[..., Any]],
- required_features: Optional[List[int]] = None,
+ schema: dict[str, Any] | vol.Schema,
+ func: str | Callable[..., Any],
+ required_features: list[int] | None = None,
) -> None:
"""Register an entity service."""
if isinstance(schema, dict):
@@ -211,7 +213,7 @@ async def async_setup_platform(
self,
platform_type: str,
platform_config: ConfigType,
- discovery_info: Optional[DiscoveryInfoType] = None,
+ discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up a platform for this component."""
if self.config is None:
@@ -272,7 +274,9 @@ async def async_remove_entity(self, entity_id: str) -> None:
if found:
await found.async_remove_entity(entity_id)
- async def async_prepare_reload(self, *, skip_reset: bool = False) -> Optional[dict]:
+ async def async_prepare_reload(
+ self, *, skip_reset: bool = False
+ ) -> ConfigType | None:
"""Prepare reloading this entity component.
This method must be run in the event loop.
@@ -301,9 +305,9 @@ async def async_prepare_reload(self, *, skip_reset: bool = False) -> Optional[di
def _async_init_entity_platform(
self,
platform_type: str,
- platform: Optional[ModuleType],
- scan_interval: Optional[timedelta] = None,
- entity_namespace: Optional[str] = None,
+ platform: ModuleType | None,
+ scan_interval: timedelta | None = None,
+ entity_namespace: str | None = None,
) -> EntityPlatform:
"""Initialize an entity platform."""
if scan_interval is None:
diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py
index bd687ab7ce8e2a..dc7386c18a8cb4 100644
--- a/homeassistant/helpers/entity_platform.py
+++ b/homeassistant/helpers/entity_platform.py
@@ -1,23 +1,37 @@
"""Class to manage the entities for a single platform."""
+from __future__ import annotations
+
import asyncio
from contextvars import ContextVar
from datetime import datetime, timedelta
+import logging
from logging import Logger
from types import ModuleType
-from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Iterable, List, Optional
+from typing import TYPE_CHECKING, Callable, Coroutine, Iterable
from homeassistant import config_entries
-from homeassistant.const import ATTR_RESTORED, DEVICE_DEFAULT_NAME
+from homeassistant.const import (
+ ATTR_RESTORED,
+ DEVICE_DEFAULT_NAME,
+ EVENT_HOMEASSISTANT_STARTED,
+)
from homeassistant.core import (
CALLBACK_TYPE,
+ CoreState,
+ HomeAssistant,
ServiceCall,
callback,
split_entity_id,
valid_entity_id,
)
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
-from homeassistant.helpers import config_validation as cv, service
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers import (
+ config_validation as cv,
+ device_registry as dev_reg,
+ entity_registry as ent_reg,
+ service,
+)
+from homeassistant.setup import async_start_setup
from homeassistant.util.async_ import run_callback_threadsafe
from .entity_registry import DISABLED_INTEGRATION
@@ -36,6 +50,8 @@
DATA_ENTITY_PLATFORM = "entity_platform"
PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds
+_LOGGER = logging.getLogger(__name__)
+
class EntityPlatform:
"""Manage the entities for a single platform."""
@@ -43,13 +59,13 @@ class EntityPlatform:
def __init__(
self,
*,
- hass: HomeAssistantType,
+ hass: HomeAssistant,
logger: Logger,
domain: str,
platform_name: str,
- platform: Optional[ModuleType],
+ platform: ModuleType | None,
scan_interval: timedelta,
- entity_namespace: Optional[str],
+ entity_namespace: str | None,
):
"""Initialize the entity platform."""
self.hass = hass
@@ -59,18 +75,18 @@ def __init__(
self.platform = platform
self.scan_interval = scan_interval
self.entity_namespace = entity_namespace
- self.config_entry: Optional[config_entries.ConfigEntry] = None
- self.entities: Dict[str, Entity] = {} # pylint: disable=used-before-assignment
- self._tasks: List[asyncio.Future] = []
+ self.config_entry: config_entries.ConfigEntry | None = None
+ self.entities: dict[str, Entity] = {}
+ self._tasks: list[asyncio.Future] = []
# Stop tracking tasks after setup is completed
self._setup_complete = False
# Method to cancel the state change listener
- self._async_unsub_polling: Optional[CALLBACK_TYPE] = None
+ self._async_unsub_polling: CALLBACK_TYPE | None = None
# Method to cancel the retry of setup
- self._async_cancel_retry_setup: Optional[CALLBACK_TYPE] = None
- self._process_updates: Optional[asyncio.Lock] = None
+ self._async_cancel_retry_setup: CALLBACK_TYPE | None = None
+ self._process_updates: asyncio.Lock | None = None
- self.parallel_updates: Optional[asyncio.Semaphore] = None
+ self.parallel_updates: asyncio.Semaphore | None = None
# Platform is None for the EntityComponent "catch-all" EntityPlatform
# which powers entity_component.add_entities
@@ -87,7 +103,7 @@ def __repr__(self) -> str:
@callback
def _get_parallel_updates_semaphore(
self, entity_has_async_update: bool
- ) -> Optional[asyncio.Semaphore]:
+ ) -> asyncio.Semaphore | None:
"""Get or create a semaphore for parallel updates.
Semaphore will be created on demand because we base it off if update method is async or not.
@@ -190,62 +206,80 @@ async def _async_setup_platform(
self.platform_name,
SLOW_SETUP_WARNING,
)
-
- try:
- task = async_create_setup_task()
-
- async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, self.domain):
- await asyncio.shield(task)
-
- # Block till all entities are done
- while self._tasks:
- pending = [task for task in self._tasks if not task.done()]
- self._tasks.clear()
-
- if pending:
- await asyncio.gather(*pending)
-
- hass.config.components.add(full_name)
- self._setup_complete = True
- return True
- except PlatformNotReady:
- tries += 1
- wait_time = min(tries, 6) * PLATFORM_NOT_READY_BASE_WAIT_TIME
- logger.warning(
- "Platform %s not ready yet. Retrying in %d seconds.",
- self.platform_name,
- wait_time,
- )
-
- async def setup_again(now): # type: ignore[no-untyped-def]
- """Run setup again."""
- self._async_cancel_retry_setup = None
- await self._async_setup_platform(async_create_setup_task, tries)
-
- self._async_cancel_retry_setup = async_call_later(
- hass, wait_time, setup_again
- )
- return False
- except asyncio.TimeoutError:
- logger.error(
- "Setup of platform %s is taking longer than %s seconds."
- " Startup will proceed without waiting any longer.",
- self.platform_name,
- SLOW_SETUP_MAX_WAIT,
- )
- return False
- except Exception: # pylint: disable=broad-except
- logger.exception(
- "Error while setting up %s platform for %s",
- self.platform_name,
- self.domain,
- )
- return False
- finally:
- warn_task.cancel()
+ with async_start_setup(hass, [full_name]):
+ try:
+ task = async_create_setup_task()
+
+ async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, self.domain):
+ await asyncio.shield(task)
+
+ # Block till all entities are done
+ while self._tasks:
+ pending = [task for task in self._tasks if not task.done()]
+ self._tasks.clear()
+
+ if pending:
+ await asyncio.gather(*pending)
+
+ hass.config.components.add(full_name)
+ self._setup_complete = True
+ return True
+ except PlatformNotReady as ex:
+ tries += 1
+ wait_time = min(tries, 6) * PLATFORM_NOT_READY_BASE_WAIT_TIME
+ message = str(ex)
+ if not message and ex.__cause__:
+ message = str(ex.__cause__)
+ ready_message = f"ready yet: {message}" if message else "ready yet"
+ if tries == 1:
+ logger.warning(
+ "Platform %s not %s; Retrying in background in %d seconds",
+ self.platform_name,
+ ready_message,
+ wait_time,
+ )
+ else:
+ logger.debug(
+ "Platform %s not %s; Retrying in %d seconds",
+ self.platform_name,
+ ready_message,
+ wait_time,
+ )
+
+ async def setup_again(*_): # type: ignore[no-untyped-def]
+ """Run setup again."""
+ self._async_cancel_retry_setup = None
+ await self._async_setup_platform(async_create_setup_task, tries)
+
+ if hass.state == CoreState.running:
+ self._async_cancel_retry_setup = async_call_later(
+ hass, wait_time, setup_again
+ )
+ else:
+ self._async_cancel_retry_setup = hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_STARTED, setup_again
+ )
+ return False
+ except asyncio.TimeoutError:
+ logger.error(
+ "Setup of platform %s is taking longer than %s seconds."
+ " Startup will proceed without waiting any longer.",
+ self.platform_name,
+ SLOW_SETUP_MAX_WAIT,
+ )
+ return False
+ except Exception: # pylint: disable=broad-except
+ logger.exception(
+ "Error while setting up %s platform for %s",
+ self.platform_name,
+ self.domain,
+ )
+ return False
+ finally:
+ warn_task.cancel()
def _schedule_add_entities(
- self, new_entities: Iterable["Entity"], update_before_add: bool = False
+ self, new_entities: Iterable[Entity], update_before_add: bool = False
) -> None:
"""Schedule adding entities for a single platform, synchronously."""
run_callback_threadsafe(
@@ -257,7 +291,7 @@ def _schedule_add_entities(
@callback
def _async_schedule_add_entities(
- self, new_entities: Iterable["Entity"], update_before_add: bool = False
+ self, new_entities: Iterable[Entity], update_before_add: bool = False
) -> None:
"""Schedule adding entities for a single platform async."""
task = self.hass.async_create_task(
@@ -268,7 +302,7 @@ def _async_schedule_add_entities(
self._tasks.append(task)
def add_entities(
- self, new_entities: Iterable["Entity"], update_before_add: bool = False
+ self, new_entities: Iterable[Entity], update_before_add: bool = False
) -> None:
"""Add entities for a single platform."""
# That avoid deadlocks
@@ -284,7 +318,7 @@ def add_entities(
).result()
async def async_add_entities(
- self, new_entities: Iterable["Entity"], update_before_add: bool = False
+ self, new_entities: Iterable[Entity], update_before_add: bool = False
) -> None:
"""Add entities for a single platform async.
@@ -296,8 +330,8 @@ async def async_add_entities(
hass = self.hass
- device_registry = await hass.helpers.device_registry.async_get_registry()
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ device_registry = dev_reg.async_get(hass)
+ entity_registry = ent_reg.async_get(hass)
tasks = [
self._async_add_entity( # type: ignore
entity, update_before_add, entity_registry, device_registry
@@ -362,7 +396,7 @@ async def _async_add_entity( # type: ignore[no-untyped-def]
return
requested_entity_id = None
- suggested_object_id: Optional[str] = None
+ suggested_object_id: str | None = None
# Get entity_id from unique ID registration
if entity.unique_id is not None:
@@ -376,7 +410,7 @@ async def _async_add_entity( # type: ignore[no-untyped-def]
suggested_object_id = f"{self.entity_namespace} {suggested_object_id}"
if self.config_entry is not None:
- config_entry_id: Optional[str] = self.config_entry.entry_id
+ config_entry_id: str | None = self.config_entry.entry_id
else:
config_entry_id = None
@@ -397,6 +431,7 @@ async def _async_add_entity( # type: ignore[no-untyped-def]
"sw_version",
"entry_type",
"via_device",
+ "suggested_area",
):
if key in device_info:
processed_dev_info[key] = device_info[key]
@@ -405,7 +440,7 @@ async def _async_add_entity( # type: ignore[no-untyped-def]
if device:
device_id = device.id
- disabled_by: Optional[str] = None
+ disabled_by: str | None = None
if not entity.entity_registry_enabled_default:
disabled_by = DISABLED_INTEGRATION
@@ -517,7 +552,7 @@ async def async_reset(self) -> None:
if not self.entities:
return
- tasks = [self.async_remove_entity(entity_id) for entity_id in self.entities]
+ tasks = [entity.async_remove() for entity in self.entities.values()]
await asyncio.gather(*tasks)
@@ -547,7 +582,7 @@ async def async_remove_entity(self, entity_id: str) -> None:
async def async_extract_from_service(
self, service_call: ServiceCall, expand_group: bool = True
- ) -> List["Entity"]:
+ ) -> list[Entity]:
"""Extract all known and available entities from a service call.
Will return an empty list if entities specified but unknown.
@@ -618,15 +653,15 @@ async def _update_entity_states(self, now: datetime) -> None:
await asyncio.gather(*tasks)
-current_platform: ContextVar[Optional[EntityPlatform]] = ContextVar(
+current_platform: ContextVar[EntityPlatform | None] = ContextVar(
"current_platform", default=None
)
@callback
def async_get_platforms(
- hass: HomeAssistantType, integration_name: str
-) -> List[EntityPlatform]:
+ hass: HomeAssistant, integration_name: str
+) -> list[EntityPlatform]:
"""Find existing platforms."""
if (
DATA_ENTITY_PLATFORM not in hass.data
@@ -634,6 +669,6 @@ def async_get_platforms(
):
return []
- platforms: List[EntityPlatform] = hass.data[DATA_ENTITY_PLATFORM][integration_name]
+ platforms: list[EntityPlatform] = hass.data[DATA_ENTITY_PLATFORM][integration_name]
return platforms
diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py
index 0628c1e0eb5b3e..db16b3cc0b1840 100644
--- a/homeassistant/helpers/entity_registry.py
+++ b/homeassistant/helpers/entity_registry.py
@@ -7,19 +7,11 @@
registered. Registering a new entity while a timer is in progress resets the
timer.
"""
+from __future__ import annotations
+
from collections import OrderedDict
import logging
-from typing import (
- TYPE_CHECKING,
- Any,
- Callable,
- Dict,
- Iterable,
- List,
- Optional,
- Tuple,
- Union,
-)
+from typing import TYPE_CHECKING, Any, Callable, Iterable, cast
import attr
@@ -33,16 +25,23 @@
EVENT_HOMEASSISTANT_START,
STATE_UNAVAILABLE,
)
-from homeassistant.core import Event, callback, split_entity_id, valid_entity_id
+from homeassistant.core import (
+ Event,
+ HomeAssistant,
+ callback,
+ split_entity_id,
+ valid_entity_id,
+)
+from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
+from homeassistant.loader import bind_hass
from homeassistant.util import slugify
from homeassistant.util.yaml import load_yaml
-from .singleton import singleton
-from .typing import UNDEFINED, HomeAssistantType, UndefinedType
+from .typing import UNDEFINED, UndefinedType
if TYPE_CHECKING:
- from homeassistant.config_entries import ConfigEntry # noqa: F401
+ from homeassistant.config_entries import ConfigEntry
PATH_REGISTRY = "entity_registry.yaml"
DATA_REGISTRY = "entity_registry"
@@ -78,12 +77,12 @@ class RegistryEntry:
entity_id: str = attr.ib()
unique_id: str = attr.ib()
platform: str = attr.ib()
- name: Optional[str] = attr.ib(default=None)
- icon: Optional[str] = attr.ib(default=None)
- device_id: Optional[str] = attr.ib(default=None)
- area_id: Optional[str] = attr.ib(default=None)
- config_entry_id: Optional[str] = attr.ib(default=None)
- disabled_by: Optional[str] = attr.ib(
+ name: str | None = attr.ib(default=None)
+ icon: str | None = attr.ib(default=None)
+ device_id: str | None = attr.ib(default=None)
+ area_id: str | None = attr.ib(default=None)
+ config_entry_id: str | None = attr.ib(default=None)
+ disabled_by: str | None = attr.ib(
default=None,
validator=attr.validators.in_(
(
@@ -96,13 +95,13 @@ class RegistryEntry:
)
),
)
- capabilities: Optional[Dict[str, Any]] = attr.ib(default=None)
+ capabilities: dict[str, Any] | None = attr.ib(default=None)
supported_features: int = attr.ib(default=0)
- device_class: Optional[str] = attr.ib(default=None)
- unit_of_measurement: Optional[str] = attr.ib(default=None)
+ device_class: str | None = attr.ib(default=None)
+ unit_of_measurement: str | None = attr.ib(default=None)
# As set by integration
- original_name: Optional[str] = attr.ib(default=None)
- original_icon: Optional[str] = attr.ib(default=None)
+ original_name: str | None = attr.ib(default=None)
+ original_icon: str | None = attr.ib(default=None)
domain: str = attr.ib(init=False, repr=False)
@domain.default
@@ -115,15 +114,42 @@ def disabled(self) -> bool:
"""Return if entry is disabled."""
return self.disabled_by is not None
+ @callback
+ def write_unavailable_state(self, hass: HomeAssistant) -> None:
+ """Write the unavailable state to the state machine."""
+ attrs: dict[str, Any] = {ATTR_RESTORED: True}
+
+ if self.capabilities is not None:
+ attrs.update(self.capabilities)
+
+ if self.supported_features is not None:
+ attrs[ATTR_SUPPORTED_FEATURES] = self.supported_features
+
+ if self.device_class is not None:
+ attrs[ATTR_DEVICE_CLASS] = self.device_class
+
+ if self.unit_of_measurement is not None:
+ attrs[ATTR_UNIT_OF_MEASUREMENT] = self.unit_of_measurement
+
+ name = self.name or self.original_name
+ if name is not None:
+ attrs[ATTR_FRIENDLY_NAME] = name
+
+ icon = self.icon or self.original_icon
+ if icon is not None:
+ attrs[ATTR_ICON] = icon
+
+ hass.states.async_set(self.entity_id, STATE_UNAVAILABLE, attrs)
+
class EntityRegistry:
"""Class to hold a registry of entities."""
- def __init__(self, hass: HomeAssistantType):
+ def __init__(self, hass: HomeAssistant):
"""Initialize the registry."""
self.hass = hass
- self.entities: Dict[str, RegistryEntry]
- self._index: Dict[Tuple[str, str, str], str] = {}
+ self.entities: dict[str, RegistryEntry]
+ self._index: dict[tuple[str, str, str], str] = {}
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
self.hass.bus.async_listen(
EVENT_DEVICE_REGISTRY_UPDATED, self.async_device_modified
@@ -132,7 +158,7 @@ def __init__(self, hass: HomeAssistantType):
@callback
def async_get_device_class_lookup(self, domain_device_classes: set) -> dict:
"""Return a lookup for the device class by domain."""
- lookup: Dict[str, Dict[Tuple[Any, Any], str]] = {}
+ lookup: dict[str, dict[tuple[Any, Any], str]] = {}
for entity in self.entities.values():
if not entity.device_id:
continue
@@ -151,14 +177,14 @@ def async_is_registered(self, entity_id: str) -> bool:
return entity_id in self.entities
@callback
- def async_get(self, entity_id: str) -> Optional[RegistryEntry]:
+ def async_get(self, entity_id: str) -> RegistryEntry | None:
"""Get EntityEntry for an entity_id."""
return self.entities.get(entity_id)
@callback
def async_get_entity_id(
self, domain: str, platform: str, unique_id: str
- ) -> Optional[str]:
+ ) -> str | None:
"""Check if an entity_id is currently registered."""
return self._index.get((domain, platform, unique_id))
@@ -167,7 +193,7 @@ def async_generate_entity_id(
self,
domain: str,
suggested_object_id: str,
- known_object_ids: Optional[Iterable[str]] = None,
+ known_object_ids: Iterable[str] | None = None,
) -> str:
"""Generate an entity ID that does not conflict.
@@ -197,20 +223,20 @@ def async_get_or_create(
unique_id: str,
*,
# To influence entity ID generation
- suggested_object_id: Optional[str] = None,
- known_object_ids: Optional[Iterable[str]] = None,
+ suggested_object_id: str | None = None,
+ known_object_ids: Iterable[str] | None = None,
# To disable an entity if it gets created
- disabled_by: Optional[str] = None,
+ disabled_by: str | None = None,
# Data that we want entry to have
- config_entry: Optional["ConfigEntry"] = None,
- device_id: Optional[str] = None,
- area_id: Optional[str] = None,
- capabilities: Optional[Dict[str, Any]] = None,
- supported_features: Optional[int] = None,
- device_class: Optional[str] = None,
- unit_of_measurement: Optional[str] = None,
- original_name: Optional[str] = None,
- original_icon: Optional[str] = None,
+ config_entry: ConfigEntry | None = None,
+ device_id: str | None = None,
+ area_id: str | None = None,
+ capabilities: dict[str, Any] | None = None,
+ supported_features: int | None = None,
+ device_class: str | None = None,
+ unit_of_measurement: str | None = None,
+ original_name: str | None = None,
+ original_icon: str | None = None,
) -> RegistryEntry:
"""Get entity. Create if it doesn't exist."""
config_entry_id = None
@@ -285,7 +311,8 @@ def async_remove(self, entity_id: str) -> None:
)
self.async_schedule_save()
- async def async_device_modified(self, event: Event) -> None:
+ @callback
+ def async_device_modified(self, event: Event) -> None:
"""Handle the removal or update of a device.
Remove entities from the registry that are associated to a device when
@@ -305,9 +332,11 @@ async def async_device_modified(self, event: Event) -> None:
if event.data["action"] != "update":
return
- device_registry = await self.hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(self.hass)
device = device_registry.async_get(event.data["device_id"])
- if not device.disabled:
+
+ # The device may be deleted already if the event handling is late
+ if not device or not device.disabled:
entities = async_entries_for_device(
self, event.data["device_id"], include_disabled_entities=True
)
@@ -317,6 +346,11 @@ async def async_device_modified(self, event: Event) -> None:
self.async_update_entity(entity.entity_id, disabled_by=None)
return
+ if device.disabled_by == dr.DISABLED_CONFIG_ENTRY:
+ # Handled by async_config_entry_disabled
+ return
+
+ # Fetch entities which are not already disabled
entities = async_entries_for_device(self, event.data["device_id"])
for entity in entities:
self.async_update_entity(entity.entity_id, disabled_by=DISABLED_DEVICE)
@@ -326,12 +360,12 @@ def async_update_entity(
self,
entity_id: str,
*,
- name: Union[str, None, UndefinedType] = UNDEFINED,
- icon: Union[str, None, UndefinedType] = UNDEFINED,
- area_id: Union[str, None, UndefinedType] = UNDEFINED,
- new_entity_id: Union[str, UndefinedType] = UNDEFINED,
- new_unique_id: Union[str, UndefinedType] = UNDEFINED,
- disabled_by: Union[str, None, UndefinedType] = UNDEFINED,
+ name: str | None | UndefinedType = UNDEFINED,
+ icon: str | None | UndefinedType = UNDEFINED,
+ area_id: str | None | UndefinedType = UNDEFINED,
+ new_entity_id: str | UndefinedType = UNDEFINED,
+ new_unique_id: str | UndefinedType = UNDEFINED,
+ disabled_by: str | None | UndefinedType = UNDEFINED,
) -> RegistryEntry:
"""Update properties of an entity."""
return self._async_update_entity(
@@ -349,25 +383,26 @@ def _async_update_entity(
self,
entity_id: str,
*,
- name: Union[str, None, UndefinedType] = UNDEFINED,
- icon: Union[str, None, UndefinedType] = UNDEFINED,
- config_entry_id: Union[str, None, UndefinedType] = UNDEFINED,
- new_entity_id: Union[str, UndefinedType] = UNDEFINED,
- device_id: Union[str, None, UndefinedType] = UNDEFINED,
- area_id: Union[str, None, UndefinedType] = UNDEFINED,
- new_unique_id: Union[str, UndefinedType] = UNDEFINED,
- disabled_by: Union[str, None, UndefinedType] = UNDEFINED,
- capabilities: Union[Dict[str, Any], None, UndefinedType] = UNDEFINED,
- supported_features: Union[int, UndefinedType] = UNDEFINED,
- device_class: Union[str, None, UndefinedType] = UNDEFINED,
- unit_of_measurement: Union[str, None, UndefinedType] = UNDEFINED,
- original_name: Union[str, None, UndefinedType] = UNDEFINED,
- original_icon: Union[str, None, UndefinedType] = UNDEFINED,
+ name: str | None | UndefinedType = UNDEFINED,
+ icon: str | None | UndefinedType = UNDEFINED,
+ config_entry_id: str | None | UndefinedType = UNDEFINED,
+ new_entity_id: str | UndefinedType = UNDEFINED,
+ device_id: str | None | UndefinedType = UNDEFINED,
+ area_id: str | None | UndefinedType = UNDEFINED,
+ new_unique_id: str | UndefinedType = UNDEFINED,
+ disabled_by: str | None | UndefinedType = UNDEFINED,
+ capabilities: dict[str, Any] | None | UndefinedType = UNDEFINED,
+ supported_features: int | UndefinedType = UNDEFINED,
+ device_class: str | None | UndefinedType = UNDEFINED,
+ unit_of_measurement: str | None | UndefinedType = UNDEFINED,
+ original_name: str | None | UndefinedType = UNDEFINED,
+ original_icon: str | None | UndefinedType = UNDEFINED,
) -> RegistryEntry:
"""Private facing update properties method."""
old = self.entities[entity_id]
- changes = {}
+ new_values = {} # Dict with new key/value pairs
+ old_values = {} # Dict with old key/value pairs
for attr_name, value in (
("name", name),
@@ -384,7 +419,8 @@ def _async_update_entity(
("original_icon", original_icon),
):
if value is not UNDEFINED and value != getattr(old, attr_name):
- changes[attr_name] = value
+ new_values[attr_name] = value
+ old_values[attr_name] = getattr(old, attr_name)
if new_entity_id is not UNDEFINED and new_entity_id != old.entity_id:
if self.async_is_registered(new_entity_id):
@@ -397,7 +433,8 @@ def _async_update_entity(
raise ValueError("New entity ID should be same domain")
self.entities.pop(entity_id)
- entity_id = changes["entity_id"] = new_entity_id
+ entity_id = new_values["entity_id"] = new_entity_id
+ old_values["entity_id"] = old.entity_id
if new_unique_id is not UNDEFINED:
conflict_entity_id = self.async_get_entity_id(
@@ -408,18 +445,19 @@ def _async_update_entity(
f"Unique id '{new_unique_id}' is already in use by "
f"'{conflict_entity_id}'"
)
- changes["unique_id"] = new_unique_id
+ new_values["unique_id"] = new_unique_id
+ old_values["unique_id"] = old.unique_id
- if not changes:
+ if not new_values:
return old
self._remove_index(old)
- new = attr.evolve(old, **changes)
+ new = attr.evolve(old, **new_values)
self._register_entry(new)
self.async_schedule_save()
- data = {"action": "update", "entity_id": entity_id, "changes": list(changes)}
+ data = {"action": "update", "entity_id": entity_id, "changes": old_values}
if old.entity_id != entity_id:
data["old_entity_id"] = old.entity_id
@@ -438,7 +476,7 @@ async def async_load(self) -> None:
old_conf_load_func=load_yaml,
old_conf_migrate_func=_async_migrate,
)
- entities: Dict[str, RegistryEntry] = OrderedDict()
+ entities: dict[str, RegistryEntry] = OrderedDict()
if data is not None:
for entity in data["entities"]:
@@ -475,7 +513,7 @@ def async_schedule_save(self) -> None:
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
@callback
- def _data_to_save(self) -> Dict[str, Any]:
+ def _data_to_save(self) -> dict[str, Any]:
"""Return data of entity registry to store in a file."""
data = {}
@@ -539,18 +577,32 @@ def _rebuild_index(self) -> None:
self._add_index(entry)
-@singleton(DATA_REGISTRY)
-async def async_get_registry(hass: HomeAssistantType) -> EntityRegistry:
- """Create entity registry."""
- reg = EntityRegistry(hass)
- await reg.async_load()
- return reg
+@callback
+def async_get(hass: HomeAssistant) -> EntityRegistry:
+ """Get entity registry."""
+ return cast(EntityRegistry, hass.data[DATA_REGISTRY])
+
+
+async def async_load(hass: HomeAssistant) -> None:
+ """Load entity registry."""
+ assert DATA_REGISTRY not in hass.data
+ hass.data[DATA_REGISTRY] = EntityRegistry(hass)
+ await hass.data[DATA_REGISTRY].async_load()
+
+
+@bind_hass
+async def async_get_registry(hass: HomeAssistant) -> EntityRegistry:
+ """Get entity registry.
+
+ This is deprecated and will be removed in the future. Use async_get instead.
+ """
+ return async_get(hass)
@callback
def async_entries_for_device(
registry: EntityRegistry, device_id: str, include_disabled_entities: bool = False
-) -> List[RegistryEntry]:
+) -> list[RegistryEntry]:
"""Return entries that match a device."""
return [
entry
@@ -563,7 +615,7 @@ def async_entries_for_device(
@callback
def async_entries_for_area(
registry: EntityRegistry, area_id: str
-) -> List[RegistryEntry]:
+) -> list[RegistryEntry]:
"""Return entries that match an area."""
return [entry for entry in registry.entities.values() if entry.area_id == area_id]
@@ -571,7 +623,7 @@ def async_entries_for_area(
@callback
def async_entries_for_config_entry(
registry: EntityRegistry, config_entry_id: str
-) -> List[RegistryEntry]:
+) -> list[RegistryEntry]:
"""Return entries that match a config entry."""
return [
entry
@@ -580,7 +632,37 @@ def async_entries_for_config_entry(
]
-async def _async_migrate(entities: Dict[str, Any]) -> Dict[str, List[Dict[str, Any]]]:
+@callback
+def async_config_entry_disabled_by_changed(
+ registry: EntityRegistry, config_entry: ConfigEntry
+) -> None:
+ """Handle a config entry being disabled or enabled.
+
+ Disable entities in the registry that are associated with a config entry when
+ the config entry is disabled, enable entities in the registry that are associated
+ with a config entry when the config entry is enabled and the entities are marked
+ DISABLED_CONFIG_ENTRY.
+ """
+
+ entities = async_entries_for_config_entry(registry, config_entry.entry_id)
+
+ if not config_entry.disabled_by:
+ for entity in entities:
+ if entity.disabled_by != DISABLED_CONFIG_ENTRY:
+ continue
+ registry.async_update_entity(entity.entity_id, disabled_by=None)
+ return
+
+ for entity in entities:
+ if entity.disabled:
+ # Entity already disabled, do not overwrite
+ continue
+ registry.async_update_entity(
+ entity.entity_id, disabled_by=DISABLED_CONFIG_ENTRY
+ )
+
+
+async def _async_migrate(entities: dict[str, Any]) -> dict[str, list[dict[str, Any]]]:
"""Migrate the YAML config file to storage helper format."""
return {
"entities": [
@@ -590,17 +672,17 @@ async def _async_migrate(entities: Dict[str, Any]) -> Dict[str, List[Dict[str, A
@callback
-def async_setup_entity_restore(
- hass: HomeAssistantType, registry: EntityRegistry
-) -> None:
+def async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) -> None:
"""Set up the entity restore mechanism."""
+ @callback
+ def cleanup_restored_states_filter(event: Event) -> bool:
+ """Clean up restored states filter."""
+ return bool(event.data["action"] == "remove")
+
@callback
def cleanup_restored_states(event: Event) -> None:
"""Clean up restored states."""
- if event.data["action"] != "remove":
- return
-
state = hass.states.get(event.data["entity_id"])
if state is None or not state.attributes.get(ATTR_RESTORED):
@@ -608,7 +690,11 @@ def cleanup_restored_states(event: Event) -> None:
hass.states.async_remove(event.data["entity_id"], context=event.context)
- hass.bus.async_listen(EVENT_ENTITY_REGISTRY_UPDATED, cleanup_restored_states)
+ hass.bus.async_listen(
+ EVENT_ENTITY_REGISTRY_UPDATED,
+ cleanup_restored_states,
+ event_filter=cleanup_restored_states_filter,
+ )
if hass.is_running:
return
@@ -616,44 +702,21 @@ def cleanup_restored_states(event: Event) -> None:
@callback
def _write_unavailable_states(_: Event) -> None:
"""Make sure state machine contains entry for each registered entity."""
- states = hass.states
- existing = set(states.async_entity_ids())
+ existing = set(hass.states.async_entity_ids())
for entry in registry.entities.values():
if entry.entity_id in existing or entry.disabled:
continue
- attrs: Dict[str, Any] = {ATTR_RESTORED: True}
-
- if entry.capabilities is not None:
- attrs.update(entry.capabilities)
-
- if entry.supported_features is not None:
- attrs[ATTR_SUPPORTED_FEATURES] = entry.supported_features
-
- if entry.device_class is not None:
- attrs[ATTR_DEVICE_CLASS] = entry.device_class
-
- if entry.unit_of_measurement is not None:
- attrs[ATTR_UNIT_OF_MEASUREMENT] = entry.unit_of_measurement
-
- name = entry.name or entry.original_name
- if name is not None:
- attrs[ATTR_FRIENDLY_NAME] = name
-
- icon = entry.icon or entry.original_icon
- if icon is not None:
- attrs[ATTR_ICON] = icon
-
- states.async_set(entry.entity_id, STATE_UNAVAILABLE, attrs)
+ entry.write_unavailable_state(hass)
hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states)
async def async_migrate_entries(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
config_entry_id: str,
- entry_callback: Callable[[RegistryEntry], Optional[dict]],
+ entry_callback: Callable[[RegistryEntry], dict | None],
) -> None:
"""Migrator of unique IDs."""
ent_reg = await async_get_registry(hass)
diff --git a/homeassistant/helpers/entity_values.py b/homeassistant/helpers/entity_values.py
index 7f44e8b27684d1..57dbb34c560a05 100644
--- a/homeassistant/helpers/entity_values.py
+++ b/homeassistant/helpers/entity_values.py
@@ -1,8 +1,10 @@
"""A class to hold entity values."""
+from __future__ import annotations
+
from collections import OrderedDict
import fnmatch
import re
-from typing import Any, Dict, Optional, Pattern
+from typing import Any, Pattern
from homeassistant.core import split_entity_id
@@ -14,17 +16,17 @@ class EntityValues:
def __init__(
self,
- exact: Optional[Dict[str, Dict[str, str]]] = None,
- domain: Optional[Dict[str, Dict[str, str]]] = None,
- glob: Optional[Dict[str, Dict[str, str]]] = None,
+ exact: dict[str, dict[str, str]] | None = None,
+ domain: dict[str, dict[str, str]] | None = None,
+ glob: dict[str, dict[str, str]] | None = None,
) -> None:
"""Initialize an EntityConfigDict."""
- self._cache: Dict[str, Dict[str, str]] = {}
+ self._cache: dict[str, dict[str, str]] = {}
self._exact = exact
self._domain = domain
if glob is None:
- compiled: Optional[Dict[Pattern[str], Any]] = None
+ compiled: dict[Pattern[str], Any] | None = None
else:
compiled = OrderedDict()
for key, value in glob.items():
@@ -32,7 +34,7 @@ def __init__(
self._glob = compiled
- def get(self, entity_id: str) -> Dict[str, str]:
+ def get(self, entity_id: str) -> dict[str, str]:
"""Get config for an entity id."""
if entity_id in self._cache:
return self._cache[entity_id]
diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py
index 608fae0242ef98..ebde309de144e9 100644
--- a/homeassistant/helpers/entityfilter.py
+++ b/homeassistant/helpers/entityfilter.py
@@ -1,7 +1,9 @@
"""Helper class to implement include/exclude of entities and domains."""
+from __future__ import annotations
+
import fnmatch
import re
-from typing import Callable, Dict, List, Pattern
+from typing import Callable, Pattern
import voluptuous as vol
@@ -19,7 +21,7 @@
CONF_ENTITY_GLOBS = "entity_globs"
-def convert_filter(config: Dict[str, List[str]]) -> Callable[[str], bool]:
+def convert_filter(config: dict[str, list[str]]) -> Callable[[str], bool]:
"""Convert the filter schema into a filter."""
filt = generate_filter(
config[CONF_INCLUDE_DOMAINS],
@@ -57,7 +59,7 @@ def convert_filter(config: Dict[str, List[str]]) -> Callable[[str], bool]:
def convert_include_exclude_filter(
- config: Dict[str, Dict[str, List[str]]]
+ config: dict[str, dict[str, list[str]]]
) -> Callable[[str], bool]:
"""Convert the include exclude filter schema into a filter."""
include = config[CONF_INCLUDE]
@@ -107,7 +109,7 @@ def _glob_to_re(glob: str) -> Pattern[str]:
return re.compile(fnmatch.translate(glob))
-def _test_against_patterns(patterns: List[Pattern[str]], entity_id: str) -> bool:
+def _test_against_patterns(patterns: list[Pattern[str]], entity_id: str) -> bool:
"""Test entity against list of patterns, true if any match."""
for pattern in patterns:
if pattern.match(entity_id):
@@ -119,12 +121,12 @@ def _test_against_patterns(patterns: List[Pattern[str]], entity_id: str) -> bool
# It's safe since we don't modify it. And None causes typing warnings
# pylint: disable=dangerous-default-value
def generate_filter(
- include_domains: List[str],
- include_entities: List[str],
- exclude_domains: List[str],
- exclude_entities: List[str],
- include_entity_globs: List[str] = [],
- exclude_entity_globs: List[str] = [],
+ include_domains: list[str],
+ include_entities: list[str],
+ exclude_domains: list[str],
+ exclude_entities: list[str],
+ include_entity_globs: list[str] = [],
+ exclude_entity_globs: list[str] = [],
) -> Callable[[str], bool]:
"""Return a function that will filter entities based on the args."""
include_d = set(include_domains)
diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py
index f06ac8aca3f068..2a3ee75ce7561b 100644
--- a/homeassistant/helpers/event.py
+++ b/homeassistant/helpers/event.py
@@ -1,4 +1,6 @@
"""Helpers for listening to events."""
+from __future__ import annotations
+
import asyncio
import copy
from dataclasses import dataclass
@@ -6,18 +8,7 @@
import functools as ft
import logging
import time
-from typing import (
- Any,
- Awaitable,
- Callable,
- Dict,
- Iterable,
- List,
- Optional,
- Set,
- Tuple,
- Union,
-)
+from typing import Any, Awaitable, Callable, Iterable, List
import attr
@@ -79,8 +70,8 @@ class TrackStates:
"""
all_states: bool
- entities: Set
- domains: Set
+ entities: set
+ domains: set
@dataclass
@@ -94,7 +85,7 @@ class TrackTemplate:
template: Template
variables: TemplateVarsType
- rate_limit: Optional[timedelta] = None
+ rate_limit: timedelta | None = None
@dataclass
@@ -116,7 +107,9 @@ class TrackTemplateResult:
result: Any
-def threaded_listener_factory(async_factory: Callable[..., Any]) -> CALLBACK_TYPE:
+def threaded_listener_factory(
+ async_factory: Callable[..., Any]
+) -> Callable[..., CALLBACK_TYPE]:
"""Convert an async event helper to a threaded one."""
@ft.wraps(async_factory)
@@ -144,10 +137,10 @@ def remove() -> None:
@bind_hass
def async_track_state_change(
hass: HomeAssistant,
- entity_ids: Union[str, Iterable[str]],
+ entity_ids: str | Iterable[str],
action: Callable[[str, State, State], None],
- from_state: Union[None, str, Iterable[str]] = None,
- to_state: Union[None, str, Iterable[str]] = None,
+ from_state: None | str | Iterable[str] = None,
+ to_state: None | str | Iterable[str] = None,
) -> CALLBACK_TYPE:
"""Track specific state changes.
@@ -178,7 +171,7 @@ def async_track_state_change(
job = HassJob(action)
@callback
- def state_change_listener(event: Event) -> None:
+ def state_change_filter(event: Event) -> bool:
"""Handle specific state changes."""
if from_state is not None:
old_state = event.data.get("old_state")
@@ -186,15 +179,21 @@ def state_change_listener(event: Event) -> None:
old_state = old_state.state
if not match_from_state(old_state):
- return
+ return False
+
if to_state is not None:
new_state = event.data.get("new_state")
if new_state is not None:
new_state = new_state.state
if not match_to_state(new_state):
- return
+ return False
+ return True
+
+ @callback
+ def state_change_dispatcher(event: Event) -> None:
+ """Handle specific state changes."""
hass.async_run_hass_job(
job,
event.data.get("entity_id"),
@@ -202,6 +201,14 @@ def state_change_listener(event: Event) -> None:
event.data.get("new_state"),
)
+ @callback
+ def state_change_listener(event: Event) -> None:
+ """Handle specific state changes."""
+ if not state_change_filter(event):
+ return
+
+ state_change_dispatcher(event)
+
if entity_ids != MATCH_ALL:
# If we have a list of entity ids we use
# async_track_state_change_event to route
@@ -213,7 +220,9 @@ def state_change_listener(event: Event) -> None:
# entity_id.
return async_track_state_change_event(hass, entity_ids, state_change_listener)
- return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener)
+ return hass.bus.async_listen(
+ EVENT_STATE_CHANGED, state_change_dispatcher, event_filter=state_change_filter
+ )
track_state_change = threaded_listener_factory(async_track_state_change)
@@ -222,7 +231,7 @@ def state_change_listener(event: Event) -> None:
@bind_hass
def async_track_state_change_event(
hass: HomeAssistant,
- entity_ids: Union[str, Iterable[str]],
+ entity_ids: str | Iterable[str],
action: Callable[[Event], Any],
) -> Callable[[], None]:
"""Track specific state change events indexed by entity_id.
@@ -244,6 +253,11 @@ def async_track_state_change_event(
if TRACK_STATE_CHANGE_LISTENER not in hass.data:
+ @callback
+ def _async_state_change_filter(event: Event) -> bool:
+ """Filter state changes by entity_id."""
+ return event.data.get("entity_id") in entity_callbacks
+
@callback
def _async_state_change_dispatcher(event: Event) -> None:
"""Dispatch state changes by entity_id."""
@@ -257,11 +271,13 @@ def _async_state_change_dispatcher(event: Event) -> None:
hass.async_run_hass_job(job, event)
except Exception: # pylint: disable=broad-except
_LOGGER.exception(
- "Error while processing state changed for %s", entity_id
+ "Error while processing state change for %s", entity_id
)
hass.data[TRACK_STATE_CHANGE_LISTENER] = hass.bus.async_listen(
- EVENT_STATE_CHANGED, _async_state_change_dispatcher
+ EVENT_STATE_CHANGED,
+ _async_state_change_dispatcher,
+ event_filter=_async_state_change_filter,
)
job = HassJob(action)
@@ -297,7 +313,6 @@ def _async_remove_indexed_listeners(
job: HassJob,
) -> None:
"""Remove a listener."""
-
callbacks = hass.data[data_key]
for storage_key in storage_keys:
@@ -313,7 +328,7 @@ def _async_remove_indexed_listeners(
@bind_hass
def async_track_entity_registry_updated_event(
hass: HomeAssistant,
- entity_ids: Union[str, Iterable[str]],
+ entity_ids: str | Iterable[str],
action: Callable[[Event], Any],
) -> Callable[[], None]:
"""Track specific entity registry updated events indexed by entity_id.
@@ -328,6 +343,12 @@ def async_track_entity_registry_updated_event(
if TRACK_ENTITY_REGISTRY_UPDATED_LISTENER not in hass.data:
+ @callback
+ def _async_entity_registry_updated_filter(event: Event) -> bool:
+ """Filter entity registry updates by entity_id."""
+ entity_id = event.data.get("old_entity_id", event.data["entity_id"])
+ return entity_id in entity_callbacks
+
@callback
def _async_entity_registry_updated_dispatcher(event: Event) -> None:
"""Dispatch entity registry updates by entity_id."""
@@ -346,7 +367,9 @@ def _async_entity_registry_updated_dispatcher(event: Event) -> None:
)
hass.data[TRACK_ENTITY_REGISTRY_UPDATED_LISTENER] = hass.bus.async_listen(
- EVENT_ENTITY_REGISTRY_UPDATED, _async_entity_registry_updated_dispatcher
+ EVENT_ENTITY_REGISTRY_UPDATED,
+ _async_entity_registry_updated_dispatcher,
+ event_filter=_async_entity_registry_updated_filter,
)
job = HassJob(action)
@@ -370,7 +393,7 @@ def remove_listener() -> None:
@callback
def _async_dispatch_domain_event(
- hass: HomeAssistant, event: Event, callbacks: Dict[str, List]
+ hass: HomeAssistant, event: Event, callbacks: dict[str, list]
) -> None:
domain = split_entity_id(event.data["entity_id"])[0]
@@ -391,7 +414,7 @@ def _async_dispatch_domain_event(
@bind_hass
def async_track_state_added_domain(
hass: HomeAssistant,
- domains: Union[str, Iterable[str]],
+ domains: str | Iterable[str],
action: Callable[[Event], Any],
) -> Callable[[], None]:
"""Track state change events when an entity is added to domains."""
@@ -403,6 +426,11 @@ def async_track_state_added_domain(
if TRACK_STATE_ADDED_DOMAIN_LISTENER not in hass.data:
+ @callback
+ def _async_state_change_filter(event: Event) -> bool:
+ """Filter state changes by entity_id."""
+ return event.data.get("old_state") is None
+
@callback
def _async_state_change_dispatcher(event: Event) -> None:
"""Dispatch state changes by entity_id."""
@@ -412,7 +440,9 @@ def _async_state_change_dispatcher(event: Event) -> None:
_async_dispatch_domain_event(hass, event, domain_callbacks)
hass.data[TRACK_STATE_ADDED_DOMAIN_LISTENER] = hass.bus.async_listen(
- EVENT_STATE_CHANGED, _async_state_change_dispatcher
+ EVENT_STATE_CHANGED,
+ _async_state_change_dispatcher,
+ event_filter=_async_state_change_filter,
)
job = HassJob(action)
@@ -437,7 +467,7 @@ def remove_listener() -> None:
@bind_hass
def async_track_state_removed_domain(
hass: HomeAssistant,
- domains: Union[str, Iterable[str]],
+ domains: str | Iterable[str],
action: Callable[[Event], Any],
) -> Callable[[], None]:
"""Track state change events when an entity is removed from domains."""
@@ -449,6 +479,11 @@ def async_track_state_removed_domain(
if TRACK_STATE_REMOVED_DOMAIN_LISTENER not in hass.data:
+ @callback
+ def _async_state_change_filter(event: Event) -> bool:
+ """Filter state changes by entity_id."""
+ return event.data.get("new_state") is None
+
@callback
def _async_state_change_dispatcher(event: Event) -> None:
"""Dispatch state changes by entity_id."""
@@ -458,7 +493,9 @@ def _async_state_change_dispatcher(event: Event) -> None:
_async_dispatch_domain_event(hass, event, domain_callbacks)
hass.data[TRACK_STATE_REMOVED_DOMAIN_LISTENER] = hass.bus.async_listen(
- EVENT_STATE_CHANGED, _async_state_change_dispatcher
+ EVENT_STATE_CHANGED,
+ _async_state_change_dispatcher,
+ event_filter=_async_state_change_filter,
)
job = HassJob(action)
@@ -481,7 +518,7 @@ def remove_listener() -> None:
@callback
-def _async_string_to_lower_list(instr: Union[str, Iterable[str]]) -> List[str]:
+def _async_string_to_lower_list(instr: str | Iterable[str]) -> list[str]:
if isinstance(instr, str):
return [instr.lower()]
@@ -500,7 +537,7 @@ def __init__(
"""Handle removal / refresh of tracker init."""
self.hass = hass
self._action = action
- self._listeners: Dict[str, Callable] = {}
+ self._listeners: dict[str, Callable] = {}
self._last_track_states: TrackStates = track_states
@callback
@@ -523,7 +560,7 @@ def async_setup(self) -> None:
self._setup_entities_listener(track_states.domains, track_states.entities)
@property
- def listeners(self) -> Dict:
+ def listeners(self) -> dict:
"""State changes that will cause a re-render."""
track_states = self._last_track_states
return {
@@ -582,7 +619,7 @@ def _cancel_listener(self, listener_name: str) -> None:
self._listeners.pop(listener_name)()
@callback
- def _setup_entities_listener(self, domains: Set, entities: Set) -> None:
+ def _setup_entities_listener(self, domains: set, entities: set) -> None:
if domains:
entities = entities.copy()
entities.update(self.hass.states.async_entity_ids(domains))
@@ -596,7 +633,7 @@ def _setup_entities_listener(self, domains: Set, entities: Set) -> None:
)
@callback
- def _setup_domains_listener(self, domains: Set) -> None:
+ def _setup_domains_listener(self, domains: set) -> None:
if not domains:
return
@@ -645,8 +682,8 @@ def async_track_state_change_filtered(
def async_track_template(
hass: HomeAssistant,
template: Template,
- action: Callable[[str, Optional[State], Optional[State]], None],
- variables: Optional[TemplateVarsType] = None,
+ action: Callable[[str, State | None, State | None], None],
+ variables: TemplateVarsType | None = None,
) -> Callable[[], None]:
"""Add a listener that fires when a a template evaluates to 'true'.
@@ -684,12 +721,11 @@ def async_track_template(
Callable to unregister the listener.
"""
-
job = HassJob(action)
@callback
def _template_changed_listener(
- event: Event, updates: List[TrackTemplateResult]
+ event: Event, updates: list[TrackTemplateResult]
) -> None:
"""Check if condition is correct and run action."""
track_result = updates.pop()
@@ -747,12 +783,12 @@ def __init__(
track_template_.template.hass = hass
self._track_templates = track_templates
- self._last_result: Dict[Template, Union[str, TemplateError]] = {}
+ self._last_result: dict[Template, str | TemplateError] = {}
self._rate_limit = KeyedRateLimit(hass)
- self._info: Dict[Template, RenderInfo] = {}
- self._track_state_changes: Optional[_TrackStateChangeFiltered] = None
- self._time_listeners: Dict[Template, Callable] = {}
+ self._info: dict[Template, RenderInfo] = {}
+ self._track_state_changes: _TrackStateChangeFiltered | None = None
+ self._time_listeners: dict[Template, Callable] = {}
def async_setup(self, raise_on_template_error: bool) -> None:
"""Activation of template tracking."""
@@ -781,7 +817,7 @@ def async_setup(self, raise_on_template_error: bool) -> None:
)
@property
- def listeners(self) -> Dict:
+ def listeners(self) -> dict:
"""State changes that will cause a re-render."""
assert self._track_state_changes
return {
@@ -837,8 +873,8 @@ def _render_template_if_ready(
self,
track_template_: TrackTemplate,
now: datetime,
- event: Optional[Event],
- ) -> Union[bool, TrackTemplateResult]:
+ event: Event | None,
+ ) -> bool | TrackTemplateResult:
"""Re-render the template if conditions match.
Returns False if the template was not be re-rendered
@@ -882,7 +918,7 @@ def _render_template_if_ready(
)
try:
- result: Union[str, TemplateError] = info.result()
+ result: str | TemplateError = info.result()
except TemplateError as ex:
result = ex
@@ -900,9 +936,9 @@ def _render_template_if_ready(
@callback
def _refresh(
self,
- event: Optional[Event],
- track_templates: Optional[Iterable[TrackTemplate]] = None,
- replayed: Optional[bool] = False,
+ event: Event | None,
+ track_templates: Iterable[TrackTemplate] | None = None,
+ replayed: bool | None = False,
) -> None:
"""Refresh the template.
@@ -1031,16 +1067,16 @@ def async_track_same_state(
hass: HomeAssistant,
period: timedelta,
action: Callable[..., None],
- async_check_same_func: Callable[[str, Optional[State], Optional[State]], bool],
- entity_ids: Union[str, Iterable[str]] = MATCH_ALL,
+ async_check_same_func: Callable[[str, State | None, State | None], bool],
+ entity_ids: str | Iterable[str] = MATCH_ALL,
) -> CALLBACK_TYPE:
"""Track the state of entities for a period and run an action.
If async_check_func is None it use the state of orig_value.
Without entity_ids we track all state changes.
"""
- async_remove_state_for_cancel: Optional[CALLBACK_TYPE] = None
- async_remove_state_for_listener: Optional[CALLBACK_TYPE] = None
+ async_remove_state_for_cancel: CALLBACK_TYPE | None = None
+ async_remove_state_for_listener: CALLBACK_TYPE | None = None
job = HassJob(action)
@@ -1068,8 +1104,8 @@ def state_for_listener(now: Any) -> None:
def state_for_cancel_listener(event: Event) -> None:
"""Fire on changes and cancel for listener if changed."""
entity: str = event.data["entity_id"]
- from_state: Optional[State] = event.data.get("old_state")
- to_state: Optional[State] = event.data.get("new_state")
+ from_state: State | None = event.data.get("old_state")
+ to_state: State | None = event.data.get("new_state")
if not async_check_same_func(entity, from_state, to_state):
clear_listener()
@@ -1099,11 +1135,10 @@ def state_for_cancel_listener(event: Event) -> None:
@bind_hass
def async_track_point_in_time(
hass: HomeAssistant,
- action: Union[HassJob, Callable[..., None]],
+ action: HassJob | Callable[..., None],
point_in_time: datetime,
) -> CALLBACK_TYPE:
"""Add a listener that fires once after a specific point in time."""
-
job = action if isinstance(action, HassJob) else HassJob(action)
@callback
@@ -1121,7 +1156,7 @@ def utc_converter(utc_now: datetime) -> None:
@bind_hass
def async_track_point_in_utc_time(
hass: HomeAssistant,
- action: Union[HassJob, Callable[..., None]],
+ action: HassJob | Callable[..., None],
point_in_time: datetime,
) -> CALLBACK_TYPE:
"""Add a listener that fires once after a specific point in UTC time."""
@@ -1132,7 +1167,7 @@ def async_track_point_in_utc_time(
# having to figure out how to call the action every time its called.
job = action if isinstance(action, HassJob) else HassJob(action)
- cancel_callback: Optional[asyncio.TimerHandle] = None
+ cancel_callback: asyncio.TimerHandle | None = None
@callback
def run_action() -> None:
@@ -1173,7 +1208,7 @@ def unsub_point_in_time_listener() -> None:
@callback
@bind_hass
def async_call_later(
- hass: HomeAssistant, delay: float, action: Union[HassJob, Callable[..., None]]
+ hass: HomeAssistant, delay: float, action: HassJob | Callable[..., None]
) -> CALLBACK_TYPE:
"""Add a listener that is called in ."""
return async_track_point_in_utc_time(
@@ -1188,7 +1223,7 @@ def async_call_later(
@bind_hass
def async_track_time_interval(
hass: HomeAssistant,
- action: Callable[..., Union[None, Awaitable]],
+ action: Callable[..., None | Awaitable],
interval: timedelta,
) -> CALLBACK_TYPE:
"""Add a listener that fires repetitively at every timedelta interval."""
@@ -1232,9 +1267,9 @@ class SunListener:
hass: HomeAssistant = attr.ib()
job: HassJob = attr.ib()
event: str = attr.ib()
- offset: Optional[timedelta] = attr.ib()
- _unsub_sun: Optional[CALLBACK_TYPE] = attr.ib(default=None)
- _unsub_config: Optional[CALLBACK_TYPE] = attr.ib(default=None)
+ offset: timedelta | None = attr.ib()
+ _unsub_sun: CALLBACK_TYPE | None = attr.ib(default=None)
+ _unsub_config: CALLBACK_TYPE | None = attr.ib(default=None)
@callback
def async_attach(self) -> None:
@@ -1288,7 +1323,7 @@ def _handle_config_event(self, _event: Any) -> None:
@callback
@bind_hass
def async_track_sunrise(
- hass: HomeAssistant, action: Callable[..., None], offset: Optional[timedelta] = None
+ hass: HomeAssistant, action: Callable[..., None], offset: timedelta | None = None
) -> CALLBACK_TYPE:
"""Add a listener that will fire a specified offset from sunrise daily."""
listener = SunListener(hass, HassJob(action), SUN_EVENT_SUNRISE, offset)
@@ -1302,7 +1337,7 @@ def async_track_sunrise(
@callback
@bind_hass
def async_track_sunset(
- hass: HomeAssistant, action: Callable[..., None], offset: Optional[timedelta] = None
+ hass: HomeAssistant, action: Callable[..., None], offset: timedelta | None = None
) -> CALLBACK_TYPE:
"""Add a listener that will fire a specified offset from sunset daily."""
listener = SunListener(hass, HassJob(action), SUN_EVENT_SUNSET, offset)
@@ -1321,13 +1356,12 @@ def async_track_sunset(
def async_track_utc_time_change(
hass: HomeAssistant,
action: Callable[..., None],
- hour: Optional[Any] = None,
- minute: Optional[Any] = None,
- second: Optional[Any] = None,
+ hour: Any | None = None,
+ minute: Any | None = None,
+ second: Any | None = None,
local: bool = False,
) -> CALLBACK_TYPE:
"""Add a listener that will fire if time matches a pattern."""
-
job = HassJob(action)
# We do not have to wrap the function with time pattern matching logic
# if no pattern given
@@ -1351,7 +1385,7 @@ def calculate_next(now: datetime) -> datetime:
localized_now, matching_seconds, matching_minutes, matching_hours
)
- time_listener: Optional[CALLBACK_TYPE] = None
+ time_listener: CALLBACK_TYPE | None = None
@callback
def pattern_time_change_listener(_: datetime) -> None:
@@ -1388,9 +1422,9 @@ def unsub_pattern_time_change_listener() -> None:
def async_track_time_change(
hass: HomeAssistant,
action: Callable[..., None],
- hour: Optional[Any] = None,
- minute: Optional[Any] = None,
- second: Optional[Any] = None,
+ hour: Any | None = None,
+ minute: Any | None = None,
+ second: Any | None = None,
) -> CALLBACK_TYPE:
"""Add a listener that will fire if UTC time matches a pattern."""
return async_track_utc_time_change(hass, action, hour, minute, second, local=True)
@@ -1399,9 +1433,7 @@ def async_track_time_change(
track_time_change = threaded_listener_factory(async_track_time_change)
-def process_state_match(
- parameter: Union[None, str, Iterable[str]]
-) -> Callable[[str], bool]:
+def process_state_match(parameter: None | str | Iterable[str]) -> Callable[[str], bool]:
"""Convert parameter to function that matches input against parameter."""
if parameter is None or parameter == MATCH_ALL:
return lambda _: True
@@ -1416,7 +1448,7 @@ def process_state_match(
@callback
def _entities_domains_from_render_infos(
render_infos: Iterable[RenderInfo],
-) -> Tuple[Set, Set]:
+) -> tuple[set, set]:
"""Combine from multiple RenderInfo."""
entities = set()
domains = set()
@@ -1477,7 +1509,7 @@ def _event_triggers_rerender(event: Event, info: RenderInfo) -> bool:
@callback
def _rate_limit_for_event(
event: Event, info: RenderInfo, track_template_: TrackTemplate
-) -> Optional[timedelta]:
+) -> timedelta | None:
"""Determine the rate limit for an event."""
entity_id = event.data.get(ATTR_ENTITY_ID)
@@ -1489,7 +1521,7 @@ def _rate_limit_for_event(
if track_template_.rate_limit is not None:
return track_template_.rate_limit
- rate_limit: Optional[timedelta] = info.rate_limit
+ rate_limit: timedelta | None = info.rate_limit
return rate_limit
diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py
index def2508ff925e1..f10e8f4c25c4a5 100644
--- a/homeassistant/helpers/frame.py
+++ b/homeassistant/helpers/frame.py
@@ -1,9 +1,11 @@
"""Provide frame helper for finding the current frame context."""
+from __future__ import annotations
+
import asyncio
import functools
import logging
from traceback import FrameSummary, extract_stack
-from typing import Any, Callable, Optional, Tuple, TypeVar, cast
+from typing import Any, Callable, TypeVar, cast
from homeassistant.exceptions import HomeAssistantError
@@ -13,8 +15,8 @@
def get_integration_frame(
- exclude_integrations: Optional[set] = None,
-) -> Tuple[FrameSummary, str, str]:
+ exclude_integrations: set | None = None,
+) -> tuple[FrameSummary, str, str]:
"""Return the frame, integration and integration path of the current stack frame."""
found_frame = None
if not exclude_integrations:
@@ -64,13 +66,12 @@ def report(what: str) -> None:
def report_integration(
- what: str, integration_frame: Tuple[FrameSummary, str, str]
+ what: str, integration_frame: tuple[FrameSummary, str, str]
) -> None:
"""Report incorrect usage in an integration.
Async friendly.
"""
-
found_frame, integration, path = integration_frame
index = found_frame.filename.index(path)
diff --git a/homeassistant/helpers/httpx_client.py b/homeassistant/helpers/httpx_client.py
index 0f1719b388de5d..cc4f5be47d8246 100644
--- a/homeassistant/helpers/httpx_client.py
+++ b/homeassistant/helpers/httpx_client.py
@@ -1,13 +1,14 @@
"""Helper for httpx."""
+from __future__ import annotations
+
import sys
-from typing import Any, Callable, Optional
+from typing import Any, Callable
import httpx
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__
-from homeassistant.core import Event, callback
+from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers.frame import warn_use
-from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.loader import bind_hass
DATA_ASYNC_CLIENT = "httpx_async_client"
@@ -20,16 +21,14 @@
@callback
@bind_hass
-def get_async_client(
- hass: HomeAssistantType, verify_ssl: bool = True
-) -> httpx.AsyncClient:
+def get_async_client(hass: HomeAssistant, verify_ssl: bool = True) -> httpx.AsyncClient:
"""Return default httpx AsyncClient.
This method must be run in the event loop.
"""
key = DATA_ASYNC_CLIENT if verify_ssl else DATA_ASYNC_CLIENT_NOVERIFY
- client: Optional[httpx.AsyncClient] = hass.data.get(key)
+ client: httpx.AsyncClient | None = hass.data.get(key)
if client is None:
client = hass.data[key] = create_async_httpx_client(hass, verify_ssl)
@@ -37,9 +36,20 @@ def get_async_client(
return client
+class HassHttpXAsyncClient(httpx.AsyncClient):
+ """httpx AsyncClient that suppresses context management."""
+
+ async def __aenter__(self: HassHttpXAsyncClient) -> HassHttpXAsyncClient:
+ """Prevent an integration from reopen of the client via context manager."""
+ return self
+
+ async def __aexit__(self, *args: Any) -> None:
+ """Prevent an integration from close of the client via context manager."""
+
+
@callback
def create_async_httpx_client(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
verify_ssl: bool = True,
auto_cleanup: bool = True,
**kwargs: Any,
@@ -51,8 +61,7 @@ def create_async_httpx_client(
This method must be run in the event loop.
"""
-
- client = httpx.AsyncClient(
+ client = HassHttpXAsyncClient(
verify=verify_ssl,
headers={USER_AGENT: SERVER_SOFTWARE},
**kwargs,
@@ -72,7 +81,7 @@ def create_async_httpx_client(
@callback
def _async_register_async_client_shutdown(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
client: httpx.AsyncClient,
original_aclose: Callable[..., Any],
) -> None:
diff --git a/homeassistant/helpers/icon.py b/homeassistant/helpers/icon.py
index dd64e9c92f11d6..628dc9d341e396 100644
--- a/homeassistant/helpers/icon.py
+++ b/homeassistant/helpers/icon.py
@@ -1,9 +1,9 @@
"""Icon helper methods."""
-from typing import Optional
+from __future__ import annotations
def icon_for_battery_level(
- battery_level: Optional[int] = None, charging: bool = False
+ battery_level: int | None = None, charging: bool = False
) -> str:
"""Return a battery icon valid identifier."""
icon = "mdi:battery"
@@ -20,7 +20,7 @@ def icon_for_battery_level(
return icon
-def icon_for_signal_level(signal_level: Optional[int] = None) -> str:
+def icon_for_signal_level(signal_level: int | None = None) -> str:
"""Return a signal icon valid identifier."""
if signal_level is None or signal_level == 0:
return "mdi:signal-cellular-outline"
diff --git a/homeassistant/helpers/instance_id.py b/homeassistant/helpers/instance_id.py
index 5feca605099770..5b6f645c55a353 100644
--- a/homeassistant/helpers/instance_id.py
+++ b/homeassistant/helpers/instance_id.py
@@ -1,5 +1,6 @@
"""Helper to create a unique instance ID."""
-from typing import Dict, Optional
+from __future__ import annotations
+
import uuid
from homeassistant.core import HomeAssistant
@@ -17,7 +18,7 @@ async def async_get(hass: HomeAssistant) -> str:
"""Get unique ID for the hass instance."""
store = storage.Store(hass, DATA_VERSION, DATA_KEY, True)
- data: Optional[Dict[str, str]] = await storage.async_migrator( # type: ignore
+ data: dict[str, str] | None = await storage.async_migrator( # type: ignore
hass,
hass.config.path(LEGACY_UUID_FILE),
store,
diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py
index f8c8b2c6d8c60b..6ed8a6b596813c 100644
--- a/homeassistant/helpers/intent.py
+++ b/homeassistant/helpers/intent.py
@@ -1,15 +1,16 @@
"""Module to coordinate user intentions."""
+from __future__ import annotations
+
import logging
import re
-from typing import Any, Callable, Dict, Iterable, Optional
+from typing import Any, Callable, Dict, Iterable
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES
-from homeassistant.core import Context, State, T, callback
+from homeassistant.core import Context, HomeAssistant, State, T, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.loader import bind_hass
_LOGGER = logging.getLogger(__name__)
@@ -29,7 +30,7 @@
@callback
@bind_hass
-def async_register(hass: HomeAssistantType, handler: "IntentHandler") -> None:
+def async_register(hass: HomeAssistant, handler: IntentHandler) -> None:
"""Register an intent with Home Assistant."""
intents = hass.data.get(DATA_KEY)
if intents is None:
@@ -47,13 +48,13 @@ def async_register(hass: HomeAssistantType, handler: "IntentHandler") -> None:
@bind_hass
async def async_handle(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
platform: str,
intent_type: str,
- slots: Optional[_SlotsType] = None,
- text_input: Optional[str] = None,
- context: Optional[Context] = None,
-) -> "IntentResponse":
+ slots: _SlotsType | None = None,
+ text_input: str | None = None,
+ context: Context | None = None,
+) -> IntentResponse:
"""Handle an intent."""
handler: IntentHandler = hass.data.get(DATA_KEY, {}).get(intent_type)
@@ -101,7 +102,7 @@ class IntentUnexpectedError(IntentError):
@callback
@bind_hass
def async_match_state(
- hass: HomeAssistantType, name: str, states: Optional[Iterable[State]] = None
+ hass: HomeAssistant, name: str, states: Iterable[State] | None = None
) -> State:
"""Find a state that matches the name."""
if states is None:
@@ -125,13 +126,13 @@ def async_test_feature(state: State, feature: int, feature_name: str) -> None:
class IntentHandler:
"""Intent handler registration."""
- intent_type: Optional[str] = None
- slot_schema: Optional[vol.Schema] = None
- _slot_schema: Optional[vol.Schema] = None
- platforms: Optional[Iterable[str]] = []
+ intent_type: str | None = None
+ slot_schema: vol.Schema | None = None
+ _slot_schema: vol.Schema | None = None
+ platforms: Iterable[str] | None = []
@callback
- def async_can_handle(self, intent_obj: "Intent") -> bool:
+ def async_can_handle(self, intent_obj: Intent) -> bool:
"""Test if an intent can be handled."""
return self.platforms is None or intent_obj.platform in self.platforms
@@ -152,7 +153,7 @@ def async_validate_slots(self, slots: _SlotsType) -> _SlotsType:
return self._slot_schema(slots) # type: ignore
- async def async_handle(self, intent_obj: "Intent") -> "IntentResponse":
+ async def async_handle(self, intent_obj: Intent) -> IntentResponse:
"""Handle the intent."""
raise NotImplementedError()
@@ -161,7 +162,7 @@ def __repr__(self) -> str:
return f"<{self.__class__.__name__} - {self.intent_type}>"
-def _fuzzymatch(name: str, items: Iterable[T], key: Callable[[T], str]) -> Optional[T]:
+def _fuzzymatch(name: str, items: Iterable[T], key: Callable[[T], str]) -> T | None:
"""Fuzzy matching function."""
matches = []
pattern = ".*?".join(name)
@@ -195,7 +196,7 @@ def __init__(
self.service = service
self.speech = speech
- async def async_handle(self, intent_obj: "Intent") -> "IntentResponse":
+ async def async_handle(self, intent_obj: Intent) -> IntentResponse:
"""Handle the hass intent."""
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
@@ -220,11 +221,11 @@ class Intent:
def __init__(
self,
- hass: HomeAssistantType,
+ hass: HomeAssistant,
platform: str,
intent_type: str,
slots: _SlotsType,
- text_input: Optional[str],
+ text_input: str | None,
context: Context,
) -> None:
"""Initialize an intent."""
@@ -236,7 +237,7 @@ def __init__(
self.context = context
@callback
- def create_response(self) -> "IntentResponse":
+ def create_response(self) -> IntentResponse:
"""Create a response."""
return IntentResponse(self)
@@ -244,15 +245,15 @@ def create_response(self) -> "IntentResponse":
class IntentResponse:
"""Response to an intent."""
- def __init__(self, intent: Optional[Intent] = None) -> None:
+ def __init__(self, intent: Intent | None = None) -> None:
"""Initialize an IntentResponse."""
self.intent = intent
- self.speech: Dict[str, Dict[str, Any]] = {}
- self.card: Dict[str, Dict[str, str]] = {}
+ self.speech: dict[str, dict[str, Any]] = {}
+ self.card: dict[str, dict[str, str]] = {}
@callback
def async_set_speech(
- self, speech: str, speech_type: str = "plain", extra_data: Optional[Any] = None
+ self, speech: str, speech_type: str = "plain", extra_data: Any | None = None
) -> None:
"""Set speech response."""
self.speech[speech_type] = {"speech": speech, "extra_data": extra_data}
@@ -265,6 +266,6 @@ def async_set_card(
self.card[card_type] = {"title": title, "content": content}
@callback
- def as_dict(self) -> Dict[str, Dict[str, Dict[str, Any]]]:
+ def as_dict(self) -> dict[str, dict[str, dict[str, Any]]]:
"""Return a dictionary representation of an intent response."""
return {"speech": self.speech, "card": self.card}
diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py
index bca2996dfa2da1..a613220ef0f677 100644
--- a/homeassistant/helpers/location.py
+++ b/homeassistant/helpers/location.py
@@ -1,13 +1,13 @@
"""Location helpers for Home Assistant."""
+from __future__ import annotations
import logging
-from typing import Optional, Sequence
+from typing import Sequence
import voluptuous as vol
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
-from homeassistant.core import State
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import HomeAssistant, State
from homeassistant.util import location as loc_util
_LOGGER = logging.getLogger(__name__)
@@ -18,17 +18,14 @@ def has_location(state: State) -> bool:
Async friendly.
"""
- # type ignore: https://github.com/python/mypy/issues/7207
return (
- isinstance(state, State) # type: ignore
+ isinstance(state, State)
and isinstance(state.attributes.get(ATTR_LATITUDE), float)
and isinstance(state.attributes.get(ATTR_LONGITUDE), float)
)
-def closest(
- latitude: float, longitude: float, states: Sequence[State]
-) -> Optional[State]:
+def closest(latitude: float, longitude: float, states: Sequence[State]) -> State | None:
"""Return closest state to point.
Async friendly.
@@ -51,8 +48,8 @@ def closest(
def find_coordinates(
- hass: HomeAssistantType, entity_id: str, recursion_history: Optional[list] = None
-) -> Optional[str]:
+ hass: HomeAssistant, entity_id: str, recursion_history: list | None = None
+) -> str | None:
"""Find the gps coordinates of the entity in the form of '90.000,180.000'."""
entity_state = hass.states.get(entity_id)
diff --git a/homeassistant/helpers/logging.py b/homeassistant/helpers/logging.py
index 49b9bfcffec6f6..a32d13ce513359 100644
--- a/homeassistant/helpers/logging.py
+++ b/homeassistant/helpers/logging.py
@@ -1,7 +1,9 @@
"""Helpers for logging allowing more advanced logging styles to be used."""
+from __future__ import annotations
+
import inspect
import logging
-from typing import Any, Mapping, MutableMapping, Optional, Tuple
+from typing import Any, Mapping, MutableMapping
class KeywordMessage:
@@ -26,7 +28,7 @@ class KeywordStyleAdapter(logging.LoggerAdapter):
"""Represents an adapter wrapping the logger allowing KeywordMessages."""
def __init__(
- self, logger: logging.Logger, extra: Optional[Mapping[str, Any]] = None
+ self, logger: logging.Logger, extra: Mapping[str, Any] | None = None
) -> None:
"""Initialize a new StyleAdapter for the provided logger."""
super().__init__(logger, extra or {})
@@ -41,7 +43,7 @@ def log(self, level: int, msg: Any, *args: Any, **kwargs: Any) -> None:
def process(
self, msg: Any, kwargs: MutableMapping[str, Any]
- ) -> Tuple[Any, MutableMapping[str, Any]]:
+ ) -> tuple[Any, MutableMapping[str, Any]]:
"""Process the keyword args in preparation for logging."""
return (
msg,
diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py
index 4e066eaa13c5e6..6ed8084413ff59 100644
--- a/homeassistant/helpers/network.py
+++ b/homeassistant/helpers/network.py
@@ -1,6 +1,9 @@
"""Network helpers."""
+from __future__ import annotations
+
+from contextlib import suppress
from ipaddress import ip_address
-from typing import Optional, cast
+from typing import cast
import yarl
@@ -8,13 +11,7 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import bind_hass
-from homeassistant.util.network import (
- is_ip_address,
- is_local,
- is_loopback,
- is_private,
- normalize_url,
-)
+from homeassistant.util.network import is_ip_address, is_loopback, normalize_url
TYPE_URL_INTERNAL = "internal_url"
TYPE_URL_EXTERNAL = "external_url"
@@ -60,7 +57,7 @@ def get_url(
for url_type in order:
if allow_internal and url_type == TYPE_URL_INTERNAL:
- try:
+ with suppress(NoURLAvailableError):
return _get_internal_url(
hass,
allow_ip=allow_ip,
@@ -68,11 +65,9 @@ def get_url(
require_ssl=require_ssl,
require_standard_port=require_standard_port,
)
- except NoURLAvailableError:
- pass
if allow_external and url_type == TYPE_URL_EXTERNAL:
- try:
+ with suppress(NoURLAvailableError):
return _get_external_url(
hass,
allow_cloud=allow_cloud,
@@ -82,8 +77,6 @@ def get_url(
require_ssl=require_ssl,
require_standard_port=require_standard_port,
)
- except NoURLAvailableError:
- pass
# For current request, we accept loopback interfaces (e.g., 127.0.0.1),
# the Supervisor hostname and localhost transparently
@@ -123,7 +116,7 @@ def get_url(
raise NoURLAvailableError
-def _get_request_host() -> Optional[str]:
+def _get_request_host() -> str | None:
"""Get the host address of the current request."""
request = http.current_request.get()
if request is None:
@@ -151,19 +144,6 @@ def _get_internal_url(
):
return normalize_url(str(internal_url))
- # Fallback to old base_url
- try:
- return _get_deprecated_base_url(
- hass,
- internal=True,
- allow_ip=allow_ip,
- require_current_request=require_current_request,
- require_ssl=require_ssl,
- require_standard_port=require_standard_port,
- )
- except NoURLAvailableError:
- pass
-
# Fallback to detected local IP
if allow_ip and not (
require_ssl or hass.config.api is None or hass.config.api.use_ssl
@@ -194,10 +174,8 @@ def _get_external_url(
) -> str:
"""Get external URL of this instance."""
if prefer_cloud and allow_cloud:
- try:
+ with suppress(NoURLAvailableError):
return _get_cloud_url(hass)
- except NoURLAvailableError:
- pass
if hass.config.external_url:
external_url = yarl.URL(hass.config.external_url)
@@ -217,22 +195,9 @@ def _get_external_url(
):
return normalize_url(str(external_url))
- try:
- return _get_deprecated_base_url(
- hass,
- allow_ip=allow_ip,
- require_current_request=require_current_request,
- require_ssl=require_ssl,
- require_standard_port=require_standard_port,
- )
- except NoURLAvailableError:
- pass
-
if allow_cloud:
- try:
+ with suppress(NoURLAvailableError):
return _get_cloud_url(hass, require_current_request=require_current_request)
- except NoURLAvailableError:
- pass
raise NoURLAvailableError
@@ -250,50 +215,3 @@ def _get_cloud_url(hass: HomeAssistant, require_current_request: bool = False) -
return normalize_url(str(cloud_url))
raise NoURLAvailableError
-
-
-@bind_hass
-def _get_deprecated_base_url(
- hass: HomeAssistant,
- *,
- internal: bool = False,
- allow_ip: bool = True,
- require_current_request: bool = False,
- require_ssl: bool = False,
- require_standard_port: bool = False,
-) -> str:
- """Work with the deprecated `base_url`, used as fallback."""
- if hass.config.api is None or not hass.config.api.deprecated_base_url:
- raise NoURLAvailableError
-
- base_url = yarl.URL(hass.config.api.deprecated_base_url)
- # Rules that apply to both internal and external
- if (
- (allow_ip or not is_ip_address(str(base_url.host)))
- and (not require_current_request or base_url.host == _get_request_host())
- and (not require_ssl or base_url.scheme == "https")
- and (not require_standard_port or base_url.is_default_port())
- ):
- # Check to ensure an internal URL
- if internal and (
- str(base_url.host).endswith(".local")
- or (
- is_ip_address(str(base_url.host))
- and not is_loopback(ip_address(base_url.host))
- and is_private(ip_address(base_url.host))
- )
- ):
- return normalize_url(str(base_url))
-
- # Check to ensure an external URL (a little)
- if (
- not internal
- and not str(base_url.host).endswith(".local")
- and not (
- is_ip_address(str(base_url.host))
- and is_local(ip_address(str(base_url.host)))
- )
- ):
- return normalize_url(str(base_url))
-
- raise NoURLAvailableError
diff --git a/homeassistant/helpers/ratelimit.py b/homeassistant/helpers/ratelimit.py
index 40f10e69d2512d..fa671c6627f915 100644
--- a/homeassistant/helpers/ratelimit.py
+++ b/homeassistant/helpers/ratelimit.py
@@ -1,8 +1,10 @@
"""Ratelimit helper."""
+from __future__ import annotations
+
import asyncio
from datetime import datetime, timedelta
import logging
-from typing import Any, Callable, Dict, Hashable, Optional
+from typing import Any, Callable, Hashable
from homeassistant.core import HomeAssistant, callback
import homeassistant.util.dt as dt_util
@@ -19,8 +21,8 @@ def __init__(
):
"""Initialize ratelimit tracker."""
self.hass = hass
- self._last_triggered: Dict[Hashable, datetime] = {}
- self._rate_limit_timers: Dict[Hashable, asyncio.TimerHandle] = {}
+ self._last_triggered: dict[Hashable, datetime] = {}
+ self._rate_limit_timers: dict[Hashable, asyncio.TimerHandle] = {}
@callback
def async_has_timer(self, key: Hashable) -> bool:
@@ -30,7 +32,7 @@ def async_has_timer(self, key: Hashable) -> bool:
return key in self._rate_limit_timers
@callback
- def async_triggered(self, key: Hashable, now: Optional[datetime] = None) -> None:
+ def async_triggered(self, key: Hashable, now: datetime | None = None) -> None:
"""Call when the action we are tracking was triggered."""
self.async_cancel_timer(key)
self._last_triggered[key] = now or dt_util.utcnow()
@@ -54,11 +56,11 @@ def async_remove(self) -> None:
def async_schedule_action(
self,
key: Hashable,
- rate_limit: Optional[timedelta],
+ rate_limit: timedelta | None,
now: datetime,
action: Callable,
*args: Any,
- ) -> Optional[datetime]:
+ ) -> datetime | None:
"""Check rate limits and schedule an action if we hit the limit.
If the rate limit is hit:
diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py
index e596027b7e1658..ef1d033cfa7ade 100644
--- a/homeassistant/helpers/reload.py
+++ b/homeassistant/helpers/reload.py
@@ -1,16 +1,17 @@
"""Class to reload platforms."""
+from __future__ import annotations
import asyncio
import logging
-from typing import Any, Dict, Iterable, List, Optional
+from typing import Iterable
from homeassistant import config as conf_util
from homeassistant.const import SERVICE_RELOAD
-from homeassistant.core import Event, callback
+from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform
from homeassistant.helpers.entity_platform import EntityPlatform, async_get_platforms
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_integration
from homeassistant.setup import async_setup_component
@@ -18,7 +19,7 @@
async def async_reload_integration_platforms(
- hass: HomeAssistantType, integration_name: str, integration_platforms: Iterable
+ hass: HomeAssistant, integration_name: str, integration_platforms: Iterable
) -> None:
"""Reload an integration's platforms.
@@ -46,10 +47,10 @@ async def async_reload_integration_platforms(
async def _resetup_platform(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
integration_name: str,
integration_platform: str,
- unprocessed_conf: Dict,
+ unprocessed_conf: ConfigType,
) -> None:
"""Resetup a platform."""
integration = await async_get_integration(hass, integration_platform)
@@ -61,7 +62,7 @@ async def _resetup_platform(
if not conf:
return
- root_config: Dict = {integration_platform: []}
+ root_config: dict = {integration_platform: []}
# Extract only the config for template, ignore the rest.
for p_type, p_config in config_per_platform(conf, integration_platform):
if p_type != integration_name:
@@ -98,10 +99,10 @@ async def _resetup_platform(
async def _async_setup_platform(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
integration_name: str,
integration_platform: str,
- platform_configs: List[Dict],
+ platform_configs: list[dict],
) -> None:
"""Platform for the first time when new configuration is added."""
if integration_platform not in hass.data:
@@ -119,7 +120,7 @@ async def _async_setup_platform(
async def _async_reconfig_platform(
- platform: EntityPlatform, platform_configs: List[Dict]
+ platform: EntityPlatform, platform_configs: list[dict]
) -> None:
"""Reconfigure an already loaded platform."""
await platform.async_reset()
@@ -128,8 +129,8 @@ async def _async_reconfig_platform(
async def async_integration_yaml_config(
- hass: HomeAssistantType, integration_name: str
-) -> Optional[Dict[Any, Any]]:
+ hass: HomeAssistant, integration_name: str
+) -> ConfigType | None:
"""Fetch the latest yaml configuration for an integration."""
integration = await async_get_integration(hass, integration_name)
@@ -140,8 +141,8 @@ async def async_integration_yaml_config(
@callback
def async_get_platform_without_config_entry(
- hass: HomeAssistantType, integration_name: str, integration_platform_name: str
-) -> Optional[EntityPlatform]:
+ hass: HomeAssistant, integration_name: str, integration_platform_name: str
+) -> EntityPlatform | None:
"""Find an existing platform that is not a config entry."""
for integration_platform in async_get_platforms(hass, integration_name):
if integration_platform.config_entry is not None:
@@ -154,16 +155,14 @@ def async_get_platform_without_config_entry(
async def async_setup_reload_service(
- hass: HomeAssistantType, domain: str, platforms: Iterable
+ hass: HomeAssistant, domain: str, platforms: Iterable
) -> None:
"""Create the reload service for the domain."""
-
if hass.services.has_service(domain, SERVICE_RELOAD):
return
async def _reload_config(call: Event) -> None:
"""Reload the platforms."""
-
await async_reload_integration_platforms(hass, domain, platforms)
hass.bus.async_fire(f"event_{domain}_reloaded", context=call.context)
@@ -172,11 +171,8 @@ async def _reload_config(call: Event) -> None:
)
-def setup_reload_service(
- hass: HomeAssistantType, domain: str, platforms: Iterable
-) -> None:
+def setup_reload_service(hass: HomeAssistant, domain: str, platforms: Iterable) -> None:
"""Sync version of async_setup_reload_service."""
-
asyncio.run_coroutine_threadsafe(
async_setup_reload_service(hass, domain, platforms),
hass.loop,
diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py
index 97069913c80d87..3350ed7a073cc4 100644
--- a/homeassistant/helpers/restore_state.py
+++ b/homeassistant/helpers/restore_state.py
@@ -1,8 +1,10 @@
"""Support for restoring entity states on startup."""
+from __future__ import annotations
+
import asyncio
from datetime import datetime, timedelta
import logging
-from typing import Any, Dict, List, Optional, Set, cast
+from typing import Any, cast
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import (
@@ -43,12 +45,12 @@ def __init__(self, state: State, last_seen: datetime) -> None:
self.state = state
self.last_seen = last_seen
- def as_dict(self) -> Dict[str, Any]:
+ def as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the stored state."""
return {"state": self.state.as_dict(), "last_seen": self.last_seen}
@classmethod
- def from_dict(cls, json_dict: Dict) -> "StoredState":
+ def from_dict(cls, json_dict: dict) -> StoredState:
"""Initialize a stored state from a dict."""
last_seen = json_dict["last_seen"]
@@ -62,11 +64,11 @@ class RestoreStateData:
"""Helper class for managing the helper saved data."""
@classmethod
- async def async_get_instance(cls, hass: HomeAssistant) -> "RestoreStateData":
+ async def async_get_instance(cls, hass: HomeAssistant) -> RestoreStateData:
"""Get the singleton instance of this data helper."""
@singleton(DATA_RESTORE_STATE_TASK)
- async def load_instance(hass: HomeAssistant) -> "RestoreStateData":
+ async def load_instance(hass: HomeAssistant) -> RestoreStateData:
"""Get the singleton instance of this data helper."""
data = cls(hass)
@@ -104,11 +106,11 @@ def __init__(self, hass: HomeAssistant) -> None:
self.store: Store = Store(
hass, STORAGE_VERSION, STORAGE_KEY, encoder=JSONEncoder
)
- self.last_states: Dict[str, StoredState] = {}
- self.entity_ids: Set[str] = set()
+ self.last_states: dict[str, StoredState] = {}
+ self.entity_ids: set[str] = set()
@callback
- def async_get_stored_states(self) -> List[StoredState]:
+ def async_get_stored_states(self) -> list[StoredState]:
"""Get the set of states which should be stored.
This includes the states of all registered entities, as well as the
@@ -233,7 +235,6 @@ class RestoreEntity(Entity):
async def async_internal_added_to_hass(self) -> None:
"""Register this entity as a restorable entity."""
- assert self.hass is not None
_, data = await asyncio.gather(
super().async_internal_added_to_hass(),
RestoreStateData.async_get_instance(self.hass),
@@ -242,18 +243,17 @@ async def async_internal_added_to_hass(self) -> None:
async def async_internal_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
- assert self.hass is not None
_, data = await asyncio.gather(
super().async_internal_will_remove_from_hass(),
RestoreStateData.async_get_instance(self.hass),
)
data.async_restore_entity_removed(self.entity_id)
- async def async_get_last_state(self) -> Optional[State]:
+ async def async_get_last_state(self) -> State | None:
"""Get the entity state from the previous run."""
if self.hass is None or self.entity_id is None:
# Return None if this entity isn't added to hass yet
- _LOGGER.warning("Cannot get last state. Entity not added to hass")
+ _LOGGER.warning("Cannot get last state. Entity not added to hass") # type: ignore[unreachable]
return None
data = await RestoreStateData.async_get_instance(self.hass)
if self.entity_id not in data.last_states:
diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py
index f197664f7e6dd6..bf52fc81b6a398 100644
--- a/homeassistant/helpers/script.py
+++ b/homeassistant/helpers/script.py
@@ -1,30 +1,23 @@
"""Helpers to execute scripts."""
+from __future__ import annotations
+
import asyncio
+from contextlib import asynccontextmanager, suppress
from datetime import datetime, timedelta
from functools import partial
import itertools
import logging
from types import MappingProxyType
-from typing import (
- Any,
- Callable,
- Dict,
- List,
- Optional,
- Sequence,
- Set,
- Tuple,
- Union,
- cast,
-)
+from typing import Any, Callable, Dict, Sequence, Union, cast
-from async_timeout import timeout
+import async_timeout
import voluptuous as vol
from homeassistant import exceptions
from homeassistant.components import device_automation, scene
from homeassistant.components.logger import LOGSEVERITY
from homeassistant.const import (
+ ATTR_AREA_ID,
ATTR_DEVICE_ID,
ATTR_ENTITY_ID,
CONF_ALIAS,
@@ -44,6 +37,7 @@
CONF_REPEAT,
CONF_SCENE,
CONF_SEQUENCE,
+ CONF_SERVICE,
CONF_TARGET,
CONF_TIMEOUT,
CONF_UNTIL,
@@ -62,8 +56,14 @@
callback,
)
from homeassistant.helpers import condition, config_validation as cv, service, template
+from homeassistant.helpers.condition import trace_condition_function
+from homeassistant.helpers.dispatcher import (
+ async_dispatcher_connect,
+ async_dispatcher_send,
+)
from homeassistant.helpers.event import async_call_later, async_track_template
from homeassistant.helpers.script_variables import ScriptVariables
+from homeassistant.helpers.trace import script_execution_set
from homeassistant.helpers.trigger import (
async_initialize_triggers,
async_validate_trigger_config,
@@ -72,6 +72,19 @@
from homeassistant.util import slugify
from homeassistant.util.dt import utcnow
+from .trace import (
+ TraceElement,
+ async_trace_path,
+ trace_append_element,
+ trace_id_get,
+ trace_path,
+ trace_path_get,
+ trace_set_result,
+ trace_stack_cv,
+ trace_stack_pop,
+ trace_stack_push,
+)
+
# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
SCRIPT_MODE_PARALLEL = "parallel"
@@ -95,9 +108,11 @@
ATTR_CUR = "current"
ATTR_MAX = "max"
-ATTR_MODE = "mode"
DATA_SCRIPTS = "helpers.script"
+DATA_SCRIPT_BREAKPOINTS = "helpers.script_breakpoints"
+RUN_ID_ANY = "*"
+NODE_ANY = "*"
_LOGGER = logging.getLogger(__name__)
@@ -107,6 +122,80 @@
_SHUTDOWN_MAX_WAIT = 60
+ACTION_TRACE_NODE_MAX_LEN = 20 # Max length of a trace node for repeated actions
+
+SCRIPT_BREAKPOINT_HIT = "script_breakpoint_hit"
+SCRIPT_DEBUG_CONTINUE_STOP = "script_debug_continue_stop_{}_{}"
+SCRIPT_DEBUG_CONTINUE_ALL = "script_debug_continue_all"
+
+
+def action_trace_append(variables, path):
+ """Append a TraceElement to trace[path]."""
+ trace_element = TraceElement(variables, path)
+ trace_append_element(trace_element, ACTION_TRACE_NODE_MAX_LEN)
+ return trace_element
+
+
+@asynccontextmanager
+async def trace_action(hass, script_run, stop, variables):
+ """Trace action execution."""
+ path = trace_path_get()
+ trace_element = action_trace_append(variables, path)
+ trace_stack_push(trace_stack_cv, trace_element)
+
+ trace_id = trace_id_get()
+ if trace_id:
+ key = trace_id[0]
+ run_id = trace_id[1]
+ breakpoints = hass.data[DATA_SCRIPT_BREAKPOINTS]
+ if key in breakpoints and (
+ (
+ run_id in breakpoints[key]
+ and (
+ path in breakpoints[key][run_id]
+ or NODE_ANY in breakpoints[key][run_id]
+ )
+ )
+ or (
+ RUN_ID_ANY in breakpoints[key]
+ and (
+ path in breakpoints[key][RUN_ID_ANY]
+ or NODE_ANY in breakpoints[key][RUN_ID_ANY]
+ )
+ )
+ ):
+ async_dispatcher_send(hass, SCRIPT_BREAKPOINT_HIT, key, run_id, path)
+
+ done = asyncio.Event()
+
+ @callback
+ def async_continue_stop(command=None):
+ if command == "stop":
+ stop.set()
+ done.set()
+
+ signal = SCRIPT_DEBUG_CONTINUE_STOP.format(key, run_id)
+ remove_signal1 = async_dispatcher_connect(hass, signal, async_continue_stop)
+ remove_signal2 = async_dispatcher_connect(
+ hass, SCRIPT_DEBUG_CONTINUE_ALL, async_continue_stop
+ )
+
+ tasks = [hass.async_create_task(flag.wait()) for flag in (stop, done)]
+ await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
+ for task in tasks:
+ task.cancel()
+ remove_signal1()
+ remove_signal2()
+
+ try:
+ yield trace_element
+ except Exception as ex:
+ trace_element.set_error(ex)
+ raise ex
+ finally:
+ trace_stack_pop(trace_stack_cv)
+
+
def make_script_schema(schema, default_script_mode, extra=vol.PREVENT_EXTRA):
"""Make a schema for a component that uses the script helper."""
return vol.Schema(
@@ -137,8 +226,8 @@ def make_script_schema(schema, default_script_mode, extra=vol.PREVENT_EXTRA):
async def async_validate_actions_config(
- hass: HomeAssistant, actions: List[ConfigType]
-) -> List[ConfigType]:
+ hass: HomeAssistant, actions: list[ConfigType]
+) -> list[ConfigType]:
"""Validate a list of actions."""
return await asyncio.gather(
*[async_validate_action_config(hass, action) for action in actions]
@@ -204,9 +293,9 @@ class _ScriptRun:
def __init__(
self,
hass: HomeAssistant,
- script: "Script",
- variables: Dict[str, Any],
- context: Optional[Context],
+ script: Script,
+ variables: dict[str, Any],
+ context: Context | None,
log_exceptions: bool,
) -> None:
self._hass = hass
@@ -215,7 +304,7 @@ def __init__(
self._context = context
self._log_exceptions = log_exceptions
self._step = -1
- self._action: Optional[Dict[str, Any]] = None
+ self._action: dict[str, Any] | None = None
self._stop = asyncio.Event()
self._stopped = asyncio.Event()
@@ -234,32 +323,46 @@ def _log(
msg, *args, level=level, **kwargs
)
+ def _step_log(self, default_message, timeout=None):
+ self._script.last_action = self._action.get(CONF_ALIAS, default_message)
+ _timeout = (
+ "" if timeout is None else f" (timeout: {timedelta(seconds=timeout)})"
+ )
+ self._log("Executing step %s%s", self._script.last_action, _timeout)
+
async def async_run(self) -> None:
"""Run script."""
try:
- if self._stop.is_set():
- return
self._log("Running %s", self._script.running_description)
for self._step, self._action in enumerate(self._script.sequence):
if self._stop.is_set():
+ script_execution_set("cancelled")
break
await self._async_step(log_exceptions=False)
+ else:
+ script_execution_set("finished")
except _StopScript:
- pass
+ script_execution_set("aborted")
+ except Exception:
+ script_execution_set("error")
+ raise
finally:
self._finish()
async def _async_step(self, log_exceptions):
- try:
- await getattr(
- self, f"_async_{cv.determine_script_action(self._action)}_step"
- )()
- except Exception as ex:
- if not isinstance(ex, (_StopScript, asyncio.CancelledError)) and (
- self._log_exceptions or log_exceptions
- ):
- self._log_exception(ex)
- raise
+ with trace_path(str(self._step)):
+ async with trace_action(self._hass, self, self._stop, self._variables):
+ if self._stop.is_set():
+ return
+ try:
+ handler = f"_async_{cv.determine_script_action(self._action)}_step"
+ await getattr(self, handler)()
+ except Exception as ex:
+ if not isinstance(ex, _StopScript) and (
+ self._log_exceptions or log_exceptions
+ ):
+ self._log_exception(ex)
+ raise
def _finish(self) -> None:
self._script._runs.remove(self) # pylint: disable=protected-access
@@ -326,32 +429,28 @@ async def _async_delay_step(self):
"""Handle delay."""
delay = self._get_pos_time_period_template(CONF_DELAY)
- self._script.last_action = self._action.get(CONF_ALIAS, f"delay {delay}")
- self._log("Executing step %s", self._script.last_action)
+ self._step_log(f"delay {delay}")
delay = delay.total_seconds()
self._changed()
+ trace_set_result(delay=delay, done=False)
try:
- async with timeout(delay):
+ async with async_timeout.timeout(delay):
await self._stop.wait()
except asyncio.TimeoutError:
- pass
+ trace_set_result(delay=delay, done=True)
async def _async_wait_template_step(self):
"""Handle a wait template."""
if CONF_TIMEOUT in self._action:
- delay = self._get_pos_time_period_template(CONF_TIMEOUT).total_seconds()
+ timeout = self._get_pos_time_period_template(CONF_TIMEOUT).total_seconds()
else:
- delay = None
+ timeout = None
- self._script.last_action = self._action.get(CONF_ALIAS, "wait template")
- self._log(
- "Executing step %s%s",
- self._script.last_action,
- "" if delay is None else f" (timeout: {timedelta(seconds=delay)})",
- )
+ self._step_log("wait template", timeout)
- self._variables["wait"] = {"remaining": delay, "completed": False}
+ self._variables["wait"] = {"remaining": timeout, "completed": False}
+ trace_set_result(wait=self._variables["wait"])
wait_template = self._action[CONF_WAIT_TEMPLATE]
wait_template.hass = self._hass
@@ -364,10 +463,9 @@ async def _async_wait_template_step(self):
@callback
def async_script_wait(entity_id, from_s, to_s):
"""Handle script after template condition is true."""
- self._variables["wait"] = {
- "remaining": to_context.remaining if to_context else delay,
- "completed": True,
- }
+ wait_var = self._variables["wait"]
+ wait_var["remaining"] = to_context.remaining if to_context else timeout
+ wait_var["completed"] = True
done.set()
to_context = None
@@ -381,13 +479,14 @@ def async_script_wait(entity_id, from_s, to_s):
self._hass.async_create_task(flag.wait()) for flag in (self._stop, done)
]
try:
- async with timeout(delay) as to_context:
+ async with async_timeout.timeout(timeout) as to_context:
await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
except asyncio.TimeoutError as ex:
+ self._variables["wait"]["remaining"] = 0.0
if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True):
self._log(_TIMEOUT_MSG)
+ trace_set_result(wait=self._variables["wait"], timeout=True)
raise _StopScript from ex
- self._variables["wait"]["remaining"] = 0.0
finally:
for task in tasks:
task.cancel()
@@ -399,10 +498,8 @@ async def _async_run_long_action(self, long_task):
async def async_cancel_long_task() -> None:
# Stop long task and wait for it to finish.
long_task.cancel()
- try:
+ with suppress(Exception):
await long_task
- except Exception: # pylint: disable=broad-except
- pass
# Wait for long task while monitoring for a stop request.
stop_task = self._hass.async_create_task(self._stop.wait())
@@ -430,17 +527,16 @@ async def async_cancel_long_task() -> None:
async def _async_call_service_step(self):
"""Call the service specified in the action."""
- self._script.last_action = self._action.get(CONF_ALIAS, "call service")
- self._log("Executing step %s", self._script.last_action)
+ self._step_log("call service")
- domain, service_name, service_data = service.async_prepare_call_from_config(
+ params = service.async_prepare_call_from_config(
self._hass, self._action, self._variables
)
running_script = (
- domain == "automation"
- and service_name == "trigger"
- or domain in ("python_script", "script")
+ params[CONF_DOMAIN] == "automation"
+ and params[CONF_SERVICE] == "trigger"
+ or params[CONF_DOMAIN] in ("python_script", "script")
)
# If this might start a script then disable the call timeout.
# Otherwise use the normal service call limit.
@@ -449,11 +545,10 @@ async def _async_call_service_step(self):
else:
limit = SERVICE_CALL_LIMIT
+ trace_set_result(params=params, running_script=running_script, limit=limit)
service_task = self._hass.async_create_task(
self._hass.services.async_call(
- domain,
- service_name,
- service_data,
+ **params,
blocking=True,
context=self._context,
limit=limit,
@@ -468,8 +563,7 @@ async def _async_call_service_step(self):
async def _async_device_step(self):
"""Perform the device automation specified in the action."""
- self._script.last_action = self._action.get(CONF_ALIAS, "device automation")
- self._log("Executing step %s", self._script.last_action)
+ self._step_log("device automation")
platform = await device_automation.async_get_device_automation_platform(
self._hass, self._action[CONF_DOMAIN], "action"
)
@@ -479,8 +573,8 @@ async def _async_device_step(self):
async def _async_scene_step(self):
"""Activate the scene specified in the action."""
- self._script.last_action = self._action.get(CONF_ALIAS, "activate scene")
- self._log("Executing step %s", self._script.last_action)
+ self._step_log("activate scene")
+ trace_set_result(scene=self._action[CONF_SCENE])
await self._hass.services.async_call(
scene.DOMAIN,
SERVICE_TURN_ON,
@@ -491,10 +585,7 @@ async def _async_scene_step(self):
async def _async_event_step(self):
"""Fire an event."""
- self._script.last_action = self._action.get(
- CONF_ALIAS, self._action[CONF_EVENT]
- )
- self._log("Executing step %s", self._script.last_action)
+ self._step_log(self._action.get(CONF_ALIAS, self._action[CONF_EVENT]))
event_data = {}
for conf in [CONF_EVENT_DATA, CONF_EVENT_DATA_TEMPLATE]:
if conf not in self._action:
@@ -509,6 +600,7 @@ async def _async_event_step(self):
"Error rendering event data template: %s", ex, level=logging.ERROR
)
+ trace_set_result(event=self._action[CONF_EVENT], event_data=event_data)
self._hass.bus.async_fire(
self._action[CONF_EVENT], event_data, context=self._context
)
@@ -519,11 +611,40 @@ async def _async_condition_step(self):
CONF_ALIAS, self._action[CONF_CONDITION]
)
cond = await self._async_get_condition(self._action)
- check = cond(self._hass, self._variables)
+ try:
+ with trace_path("condition"):
+ check = cond(self._hass, self._variables)
+ except exceptions.ConditionError as ex:
+ _LOGGER.warning("Error in 'condition' evaluation:\n%s", ex)
+ check = False
+
self._log("Test condition %s: %s", self._script.last_action, check)
+ trace_set_result(result=check)
if not check:
raise _StopScript
+ def _test_conditions(self, conditions, name, condition_path=None):
+ if condition_path is None:
+ condition_path = name
+
+ @trace_condition_function
+ def traced_test_conditions(hass, variables):
+ try:
+ with trace_path(condition_path):
+ for idx, cond in enumerate(conditions):
+ with trace_path(str(idx)):
+ if not cond(hass, variables):
+ return False
+ except exceptions.ConditionError as ex:
+ _LOGGER.warning("Error in '%s[%s]' evaluation: %s", name, idx, ex)
+ return None
+
+ return True
+
+ result = traced_test_conditions(self._hass, self._variables)
+ return result
+
+ @async_trace_path("repeat")
async def _async_repeat_step(self):
"""Repeat a sequence."""
description = self._action.get(CONF_ALIAS, "sequence")
@@ -542,7 +663,8 @@ def set_repeat_var(iteration, count=None):
async def async_run_sequence(iteration, extra_msg=""):
self._log("Repeating %s: Iteration %i%s", description, iteration, extra_msg)
- await self._async_run_script(script)
+ with trace_path("sequence"):
+ await self._async_run_script(script)
if CONF_COUNT in repeat:
count = repeat[CONF_COUNT]
@@ -570,10 +692,15 @@ async def async_run_sequence(iteration, extra_msg=""):
]
for iteration in itertools.count(1):
set_repeat_var(iteration)
- if self._stop.is_set() or not all(
- cond(self._hass, self._variables) for cond in conditions
- ):
+ try:
+ if self._stop.is_set():
+ break
+ if not self._test_conditions(conditions, "while"):
+ break
+ except exceptions.ConditionError as ex:
+ _LOGGER.warning("Error in 'while' evaluation:\n%s", ex)
break
+
await async_run_sequence(iteration)
elif CONF_UNTIL in repeat:
@@ -583,9 +710,13 @@ async def async_run_sequence(iteration, extra_msg=""):
for iteration in itertools.count(1):
set_repeat_var(iteration)
await async_run_sequence(iteration)
- if self._stop.is_set() or all(
- cond(self._hass, self._variables) for cond in conditions
- ):
+ try:
+ if self._stop.is_set():
+ break
+ if self._test_conditions(conditions, "until") in [True, None]:
+ break
+ except exceptions.ConditionError as ex:
+ _LOGGER.warning("Error in 'until' evaluation:\n%s", ex)
break
if saved_repeat_vars:
@@ -598,36 +729,42 @@ async def _async_choose_step(self) -> None:
# pylint: disable=protected-access
choose_data = await self._script._async_get_choose_data(self._step)
- for conditions, script in choose_data["choices"]:
- if all(condition(self._hass, self._variables) for condition in conditions):
- await self._async_run_script(script)
- return
+ with trace_path("choose"):
+ for idx, (conditions, script) in enumerate(choose_data["choices"]):
+ with trace_path(str(idx)):
+ try:
+ if self._test_conditions(conditions, "choose", "conditions"):
+ trace_set_result(choice=idx)
+ with trace_path("sequence"):
+ await self._async_run_script(script)
+ return
+ except exceptions.ConditionError as ex:
+ _LOGGER.warning("Error in 'choose' evaluation:\n%s", ex)
if choose_data["default"]:
- await self._async_run_script(choose_data["default"])
+ trace_set_result(choice="default")
+ with trace_path(["default"]):
+ await self._async_run_script(choose_data["default"])
async def _async_wait_for_trigger_step(self):
"""Wait for a trigger event."""
if CONF_TIMEOUT in self._action:
- delay = self._get_pos_time_period_template(CONF_TIMEOUT).total_seconds()
+ timeout = self._get_pos_time_period_template(CONF_TIMEOUT).total_seconds()
else:
- delay = None
+ timeout = None
- self._script.last_action = self._action.get(CONF_ALIAS, "wait for trigger")
- self._log(
- "Executing step %s%s",
- self._script.last_action,
- "" if delay is None else f" (timeout: {timedelta(seconds=delay)})",
- )
+ self._step_log("wait for trigger", timeout)
variables = {**self._variables}
- self._variables["wait"] = {"remaining": delay, "trigger": None}
+ self._variables["wait"] = {"remaining": timeout, "trigger": None}
+ trace_set_result(wait=self._variables["wait"])
+
+ done = asyncio.Event()
async def async_done(variables, context=None):
- self._variables["wait"] = {
- "remaining": to_context.remaining if to_context else delay,
- "trigger": variables["trigger"],
- }
+ wait_var = self._variables["wait"]
+ wait_var["remaining"] = to_context.remaining if to_context else timeout
+ wait_var["trigger"] = variables["trigger"]
done.set()
def log_cb(level, msg, **kwargs):
@@ -647,18 +784,18 @@ def log_cb(level, msg, **kwargs):
return
self._changed()
- done = asyncio.Event()
tasks = [
self._hass.async_create_task(flag.wait()) for flag in (self._stop, done)
]
try:
- async with timeout(delay) as to_context:
+ async with async_timeout.timeout(timeout) as to_context:
await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
except asyncio.TimeoutError as ex:
+ self._variables["wait"]["remaining"] = 0.0
if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True):
self._log(_TIMEOUT_MSG)
+ trace_set_result(wait=self._variables["wait"], timeout=True)
raise _StopScript from ex
- self._variables["wait"]["remaining"] = 0.0
finally:
for task in tasks:
task.cancel()
@@ -666,8 +803,7 @@ def log_cb(level, msg, **kwargs):
async def _async_variables_step(self):
"""Set a variable value."""
- self._script.last_action = self._action.get(CONF_ALIAS, "setting variables")
- self._log("Executing step %s", self._script.last_action)
+ self._step_log("setting variables")
self._variables = self._action[CONF_VARIABLES].async_render(
self._hass, self._variables, render_as_defaults=False
)
@@ -759,7 +895,7 @@ async def _async_stop_scripts_at_shutdown(hass, event):
_VarsType = Union[Dict[str, Any], MappingProxyType]
-def _referenced_extract_ids(data: Dict, key: str, found: Set[str]) -> None:
+def _referenced_extract_ids(data: dict[str, Any], key: str, found: set[str]) -> None:
"""Extract referenced IDs."""
if not data:
return
@@ -770,10 +906,10 @@ def _referenced_extract_ids(data: Dict, key: str, found: Set[str]) -> None:
return
if isinstance(item_ids, str):
- item_ids = [item_ids]
-
- for item_id in item_ids:
- found.add(item_id)
+ found.add(item_ids)
+ else:
+ for item_id in item_ids:
+ found.add(item_id)
class Script:
@@ -782,20 +918,20 @@ class Script:
def __init__(
self,
hass: HomeAssistant,
- sequence: Sequence[Dict[str, Any]],
+ sequence: Sequence[dict[str, Any]],
name: str,
domain: str,
*,
# Used in "Running " log message
- running_description: Optional[str] = None,
- change_listener: Optional[Callable[..., Any]] = None,
+ running_description: str | None = None,
+ change_listener: Callable[..., Any] | None = None,
script_mode: str = DEFAULT_SCRIPT_MODE,
max_runs: int = DEFAULT_MAX,
max_exceeded: str = DEFAULT_MAX_EXCEEDED,
- logger: Optional[logging.Logger] = None,
+ logger: logging.Logger | None = None,
log_exceptions: bool = True,
top_level: bool = True,
- variables: Optional[ScriptVariables] = None,
+ variables: ScriptVariables | None = None,
) -> None:
"""Initialize the script."""
all_scripts = hass.data.get(DATA_SCRIPTS)
@@ -809,6 +945,8 @@ def __init__(
all_scripts.append(
{"instance": self, "started_before_shutdown": not hass.is_stopping}
)
+ if DATA_SCRIPT_BREAKPOINTS not in hass.data:
+ hass.data[DATA_SCRIPT_BREAKPOINTS] = {}
self._hass = hass
self.sequence = sequence
@@ -826,25 +964,26 @@ def __init__(
self._log_exceptions = log_exceptions
self.last_action = None
- self.last_triggered: Optional[datetime] = None
+ self.last_triggered: datetime | None = None
- self._runs: List[_ScriptRun] = []
+ self._runs: list[_ScriptRun] = []
self.max_runs = max_runs
self._max_exceeded = max_exceeded
if script_mode == SCRIPT_MODE_QUEUED:
self._queue_lck = asyncio.Lock()
- self._config_cache: Dict[Set[Tuple], Callable[..., bool]] = {}
- self._repeat_script: Dict[int, Script] = {}
- self._choose_data: Dict[int, Dict[str, Any]] = {}
- self._referenced_entities: Optional[Set[str]] = None
- self._referenced_devices: Optional[Set[str]] = None
+ self._config_cache: dict[set[tuple], Callable[..., bool]] = {}
+ self._repeat_script: dict[int, Script] = {}
+ self._choose_data: dict[int, dict[str, Any]] = {}
+ self._referenced_entities: set[str] | None = None
+ self._referenced_devices: set[str] | None = None
+ self._referenced_areas: set[str] | None = None
self.variables = variables
self._variables_dynamic = template.is_complex(variables)
if self._variables_dynamic:
template.attach(hass, variables)
@property
- def change_listener(self) -> Optional[Callable[..., Any]]:
+ def change_listener(self) -> Callable[..., Any] | None:
"""Return the change_listener."""
return self._change_listener
@@ -858,13 +997,13 @@ def change_listener(self, change_listener: Callable[..., Any]) -> None:
):
self._change_listener_job = HassJob(change_listener)
- def _set_logger(self, logger: Optional[logging.Logger] = None) -> None:
+ def _set_logger(self, logger: logging.Logger | None = None) -> None:
if logger:
self._logger = logger
else:
self._logger = logging.getLogger(f"{__name__}.{slugify(self.name)}")
- def update_logger(self, logger: Optional[logging.Logger] = None) -> None:
+ def update_logger(self, logger: logging.Logger | None = None) -> None:
"""Update logger."""
self._set_logger(logger)
for script in self._repeat_script.values():
@@ -899,20 +1038,41 @@ def supports_max(self) -> bool:
"""Return true if the current mode support max."""
return self.script_mode in (SCRIPT_MODE_PARALLEL, SCRIPT_MODE_QUEUED)
+ @property
+ def referenced_areas(self):
+ """Return a set of referenced areas."""
+ if self._referenced_areas is not None:
+ return self._referenced_areas
+
+ referenced: set[str] = set()
+
+ for step in self.sequence:
+ action = cv.determine_script_action(step)
+
+ if action == cv.SCRIPT_ACTION_CALL_SERVICE:
+ for data in (
+ step.get(CONF_TARGET),
+ step.get(service.CONF_SERVICE_DATA),
+ step.get(service.CONF_SERVICE_DATA_TEMPLATE),
+ ):
+ _referenced_extract_ids(data, ATTR_AREA_ID, referenced)
+
+ self._referenced_areas = referenced
+ return referenced
+
@property
def referenced_devices(self):
"""Return a set of referenced devices."""
if self._referenced_devices is not None:
return self._referenced_devices
- referenced: Set[str] = set()
+ referenced: set[str] = set()
for step in self.sequence:
action = cv.determine_script_action(step)
if action == cv.SCRIPT_ACTION_CALL_SERVICE:
for data in (
- step,
step.get(CONF_TARGET),
step.get(service.CONF_SERVICE_DATA),
step.get(service.CONF_SERVICE_DATA_TEMPLATE),
@@ -934,7 +1094,7 @@ def referenced_entities(self):
if self._referenced_entities is not None:
return self._referenced_entities
- referenced: Set[str] = set()
+ referenced: set[str] = set()
for step in self.sequence:
action = cv.determine_script_action(step)
@@ -958,7 +1118,7 @@ def referenced_entities(self):
return referenced
def run(
- self, variables: Optional[_VarsType] = None, context: Optional[Context] = None
+ self, variables: _VarsType | None = None, context: Context | None = None
) -> None:
"""Run script."""
asyncio.run_coroutine_threadsafe(
@@ -967,9 +1127,9 @@ def run(
async def async_run(
self,
- run_variables: Optional[_VarsType] = None,
- context: Optional[Context] = None,
- started_action: Optional[Callable[..., Any]] = None,
+ run_variables: _VarsType | None = None,
+ context: Context | None = None,
+ started_action: Callable[..., Any] | None = None,
) -> None:
"""Run script."""
if context is None:
@@ -982,6 +1142,7 @@ async def async_run(
if self.script_mode == SCRIPT_MODE_SINGLE:
if self._max_exceeded != "SILENT":
self._log("Already running", level=LOGSEVERITY[self._max_exceeded])
+ script_execution_set("failed_single")
return
if self.script_mode == SCRIPT_MODE_RESTART:
self._log("Restarting")
@@ -992,6 +1153,7 @@ async def async_run(
"Maximum number of runs exceeded",
level=LOGSEVERITY[self._max_exceeded],
)
+ script_execution_set("failed_max_runs")
return
# If this is a top level Script then make a copy of the variables in case they
@@ -1037,7 +1199,7 @@ async def async_run(
raise
async def _async_stop(self, update_state):
- aws = [run.async_stop() for run in self._runs]
+ aws = [asyncio.create_task(run.async_stop()) for run in self._runs]
if not aws:
return
await asyncio.wait(aws)
@@ -1092,10 +1254,11 @@ async def _async_prep_choose_data(self, step):
await self._async_get_condition(config)
for config in choice.get(CONF_CONDITIONS, [])
]
+ choice_name = choice.get(CONF_ALIAS, f"choice {idx}")
sub_script = Script(
self._hass,
choice[CONF_SEQUENCE],
- f"{self.name}: {step_name}: choice {idx}",
+ f"{self.name}: {step_name}: {choice_name}",
self.domain,
running_description=self.running_description,
script_mode=SCRIPT_MODE_PARALLEL,
@@ -1145,3 +1308,71 @@ def _log(
self._logger.exception(msg, *args, **kwargs)
else:
self._logger.log(level, msg, *args, **kwargs)
+
+
+@callback
+def breakpoint_clear(hass, key, run_id, node):
+ """Clear a breakpoint."""
+ run_id = run_id or RUN_ID_ANY
+ breakpoints = hass.data[DATA_SCRIPT_BREAKPOINTS]
+ if key not in breakpoints or run_id not in breakpoints[key]:
+ return
+ breakpoints[key][run_id].discard(node)
+
+
+@callback
+def breakpoint_clear_all(hass):
+ """Clear all breakpoints."""
+ hass.data[DATA_SCRIPT_BREAKPOINTS] = {}
+
+
+@callback
+def breakpoint_set(hass, key, run_id, node):
+ """Set a breakpoint."""
+ run_id = run_id or RUN_ID_ANY
+ breakpoints = hass.data[DATA_SCRIPT_BREAKPOINTS]
+ if key not in breakpoints:
+ breakpoints[key] = {}
+ if run_id not in breakpoints[key]:
+ breakpoints[key][run_id] = set()
+ breakpoints[key][run_id].add(node)
+
+
+@callback
+def breakpoint_list(hass):
+ """List breakpoints."""
+ breakpoints = hass.data[DATA_SCRIPT_BREAKPOINTS]
+
+ return [
+ {"key": key, "run_id": run_id, "node": node}
+ for key in breakpoints
+ for run_id in breakpoints[key]
+ for node in breakpoints[key][run_id]
+ ]
+
+
+@callback
+def debug_continue(hass, key, run_id):
+ """Continue execution of a halted script."""
+ # Clear any wildcard breakpoint
+ breakpoint_clear(hass, key, run_id, NODE_ANY)
+
+ signal = SCRIPT_DEBUG_CONTINUE_STOP.format(key, run_id)
+ async_dispatcher_send(hass, signal, "continue")
+
+
+@callback
+def debug_step(hass, key, run_id):
+ """Single step a halted script."""
+ # Set a wildcard breakpoint
+ breakpoint_set(hass, key, run_id, NODE_ANY)
+
+ signal = SCRIPT_DEBUG_CONTINUE_STOP.format(key, run_id)
+ async_dispatcher_send(hass, signal, "continue")
+
+
+@callback
+def debug_stop(hass, key, run_id):
+ """Stop execution of a running or halted script."""
+ signal = SCRIPT_DEBUG_CONTINUE_STOP.format(key, run_id)
+ async_dispatcher_send(hass, signal, "stop")
diff --git a/homeassistant/helpers/script_variables.py b/homeassistant/helpers/script_variables.py
index 3140fc4dcedcb4..86a700bc62bf1c 100644
--- a/homeassistant/helpers/script_variables.py
+++ b/homeassistant/helpers/script_variables.py
@@ -1,5 +1,7 @@
"""Script variables."""
-from typing import Any, Dict, Mapping, Optional
+from __future__ import annotations
+
+from typing import Any, Mapping
from homeassistant.core import HomeAssistant, callback
@@ -9,19 +11,20 @@
class ScriptVariables:
"""Class to hold and render script variables."""
- def __init__(self, variables: Dict[str, Any]):
+ def __init__(self, variables: dict[str, Any]):
"""Initialize script variables."""
self.variables = variables
- self._has_template: Optional[bool] = None
+ self._has_template: bool | None = None
@callback
def async_render(
self,
hass: HomeAssistant,
- run_variables: Optional[Mapping[str, Any]],
+ run_variables: Mapping[str, Any] | None,
*,
render_as_defaults: bool = True,
- ) -> Dict[str, Any]:
+ limited: bool = False,
+ ) -> dict[str, Any]:
"""Render script variables.
The run variables are used to compute the static variables.
@@ -55,7 +58,9 @@ def async_render(
if render_as_defaults and key in rendered_variables:
continue
- rendered_variables[key] = template.render_complex(value, rendered_variables)
+ rendered_variables[key] = template.render_complex(
+ value, rendered_variables, limited
+ )
return rendered_variables
diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py
index b48ffb6e96435a..99d871fc25b676 100644
--- a/homeassistant/helpers/selector.py
+++ b/homeassistant/helpers/selector.py
@@ -1,4 +1,6 @@
"""Selectors for Home Assistant."""
+from __future__ import annotations
+
from typing import Any, Callable, Dict, cast
import voluptuous as vol
@@ -9,7 +11,7 @@
SELECTORS = decorator.Registry()
-def validate_selector(config: Any) -> Dict:
+def validate_selector(config: Any) -> dict:
"""Validate a selector."""
if not isinstance(config, dict):
raise vol.Invalid("Expected a dictionary")
@@ -68,9 +70,7 @@ class DeviceSelector(Selector):
# Model of device
vol.Optional("model"): str,
# Device has to contain entities matching this selector
- vol.Optional(
- "entity"
- ): EntitySelector.CONFIG_SCHEMA, # pylint: disable=no-member
+ vol.Optional("entity"): EntitySelector.CONFIG_SCHEMA,
}
)
@@ -116,6 +116,13 @@ class NumberSelector(Selector):
)
+@SELECTORS.register("addon")
+class AddonSelector(Selector):
+ """Selector of a add-on."""
+
+ CONFIG_SCHEMA = vol.Schema({})
+
+
@SELECTORS.register("boolean")
class BooleanSelector(Selector):
"""Selector of a boolean value."""
@@ -176,3 +183,12 @@ class StringSelector(Selector):
"""Selector for a multi-line text string."""
CONFIG_SCHEMA = vol.Schema({vol.Optional("multiline", default=False): bool})
+
+
+@SELECTORS.register("select")
+class SelectSelector(Selector):
+ """Selector for an single-choice input select."""
+
+ CONFIG_SCHEMA = vol.Schema(
+ {vol.Required("options"): vol.All([str], vol.Length(min=1))}
+ )
diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py
index c95f942c6dc3d3..4e484c6aaabc04 100644
--- a/homeassistant/helpers/service.py
+++ b/homeassistant/helpers/service.py
@@ -1,22 +1,11 @@
"""Service calling related helpers."""
+from __future__ import annotations
+
import asyncio
import dataclasses
from functools import partial, wraps
import logging
-from typing import (
- TYPE_CHECKING,
- Any,
- Awaitable,
- Callable,
- Dict,
- Iterable,
- List,
- Optional,
- Set,
- Tuple,
- Union,
- cast,
-)
+from typing import TYPE_CHECKING, Any, Awaitable, Callable, Iterable, TypedDict
import voluptuous as vol
@@ -25,13 +14,15 @@
ATTR_AREA_ID,
ATTR_DEVICE_ID,
ATTR_ENTITY_ID,
+ CONF_ENTITY_ID,
CONF_SERVICE,
+ CONF_SERVICE_DATA,
CONF_SERVICE_TEMPLATE,
CONF_TARGET,
ENTITY_MATCH_ALL,
ENTITY_MATCH_NONE,
)
-import homeassistant.core as ha
+from homeassistant.core import Context, HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import (
HomeAssistantError,
TemplateError,
@@ -45,7 +36,7 @@
entity_registry,
template,
)
-from homeassistant.helpers.typing import ConfigType, HomeAssistantType, TemplateVarsType
+from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from homeassistant.loader import (
MAX_LOAD_CONCURRENTLY,
Integration,
@@ -57,12 +48,11 @@
from homeassistant.util.yaml.loader import JSON_TYPE
if TYPE_CHECKING:
- from homeassistant.helpers.entity import Entity # noqa
+ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import EntityPlatform
CONF_SERVICE_ENTITY_ID = "entity_id"
-CONF_SERVICE_DATA = "data"
CONF_SERVICE_DATA_TEMPLATE = "data_template"
_LOGGER = logging.getLogger(__name__)
@@ -70,22 +60,57 @@
SERVICE_DESCRIPTION_CACHE = "service_description_cache"
+class ServiceParams(TypedDict):
+ """Type for service call parameters."""
+
+ domain: str
+ service: str
+ service_data: dict[str, Any]
+ target: dict | None
+
+
+class ServiceTargetSelector:
+ """Class to hold a target selector for a service."""
+
+ def __init__(self, service_call: ServiceCall):
+ """Extract ids from service call data."""
+ entity_ids: str | list | None = service_call.data.get(ATTR_ENTITY_ID)
+ device_ids: str | list | None = service_call.data.get(ATTR_DEVICE_ID)
+ area_ids: str | list | None = service_call.data.get(ATTR_AREA_ID)
+
+ self.entity_ids = (
+ set(cv.ensure_list(entity_ids)) if _has_match(entity_ids) else set()
+ )
+ self.device_ids = (
+ set(cv.ensure_list(device_ids)) if _has_match(device_ids) else set()
+ )
+ self.area_ids = set(cv.ensure_list(area_ids)) if _has_match(area_ids) else set()
+
+ @property
+ def has_any_selector(self) -> bool:
+ """Determine if any selectors are present."""
+ return bool(self.entity_ids or self.device_ids or self.area_ids)
+
+
@dataclasses.dataclass
class SelectedEntities:
"""Class to hold the selected entities."""
# Entities that were explicitly mentioned.
- referenced: Set[str] = dataclasses.field(default_factory=set)
+ referenced: set[str] = dataclasses.field(default_factory=set)
# Entities that were referenced via device/area ID.
# Should not trigger a warning when they don't exist.
- indirectly_referenced: Set[str] = dataclasses.field(default_factory=set)
+ indirectly_referenced: set[str] = dataclasses.field(default_factory=set)
# Referenced items that could not be found.
- missing_devices: Set[str] = dataclasses.field(default_factory=set)
- missing_areas: Set[str] = dataclasses.field(default_factory=set)
+ missing_devices: set[str] = dataclasses.field(default_factory=set)
+ missing_areas: set[str] = dataclasses.field(default_factory=set)
+
+ # Referenced devices
+ referenced_devices: set[str] = dataclasses.field(default_factory=set)
- def log_missing(self, missing_entities: Set[str]) -> None:
+ def log_missing(self, missing_entities: set[str]) -> None:
"""Log about missing items."""
parts = []
for label, items in (
@@ -104,7 +129,7 @@ def log_missing(self, missing_entities: Set[str]) -> None:
@bind_hass
def call_from_config(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
config: ConfigType,
blocking: bool = False,
variables: TemplateVarsType = None,
@@ -119,12 +144,12 @@ def call_from_config(
@bind_hass
async def async_call_from_config(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
config: ConfigType,
blocking: bool = False,
variables: TemplateVarsType = None,
validate_config: bool = True,
- context: Optional[ha.Context] = None,
+ context: Context | None = None,
) -> None:
"""Call a service based on a config hash."""
try:
@@ -136,17 +161,17 @@ async def async_call_from_config(
raise
_LOGGER.error(ex)
else:
- await hass.services.async_call(*params, blocking, context)
+ await hass.services.async_call(**params, blocking=blocking, context=context)
-@ha.callback
+@callback
@bind_hass
def async_prepare_call_from_config(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
config: ConfigType,
variables: TemplateVarsType = None,
validate_config: bool = False,
-) -> Tuple[str, str, Dict[str, Any]]:
+) -> ServiceParams:
"""Prepare to call a service based on a config hash."""
if validate_config:
try:
@@ -177,10 +202,29 @@ def async_prepare_call_from_config(
domain, service = domain_service.split(".", 1)
- service_data = {}
-
+ target = {}
if CONF_TARGET in config:
- service_data.update(config[CONF_TARGET])
+ conf = config[CONF_TARGET]
+ try:
+ if isinstance(conf, template.Template):
+ conf.hass = hass
+ target.update(conf.async_render(variables))
+ else:
+ template.attach(hass, conf)
+ target.update(template.render_complex(conf, variables))
+
+ if CONF_ENTITY_ID in target:
+ target[CONF_ENTITY_ID] = cv.comp_entity_ids(target[CONF_ENTITY_ID])
+ except TemplateError as ex:
+ raise HomeAssistantError(
+ f"Error rendering service target template: {ex}"
+ ) from ex
+ except vol.Invalid as ex:
+ raise HomeAssistantError(
+ f"Template rendered invalid entity IDs: {target[CONF_ENTITY_ID]}"
+ ) from ex
+
+ service_data = {}
for conf in [CONF_SERVICE_DATA, CONF_SERVICE_DATA_TEMPLATE]:
if conf not in config:
@@ -192,15 +236,23 @@ def async_prepare_call_from_config(
raise HomeAssistantError(f"Error rendering data template: {ex}") from ex
if CONF_SERVICE_ENTITY_ID in config:
- service_data[ATTR_ENTITY_ID] = config[CONF_SERVICE_ENTITY_ID]
+ if target:
+ target[ATTR_ENTITY_ID] = config[CONF_SERVICE_ENTITY_ID]
+ else:
+ target = {ATTR_ENTITY_ID: config[CONF_SERVICE_ENTITY_ID]}
- return domain, service, service_data
+ return {
+ "domain": domain,
+ "service": service,
+ "service_data": service_data,
+ "target": target,
+ }
@bind_hass
def extract_entity_ids(
- hass: HomeAssistantType, service_call: ha.ServiceCall, expand_group: bool = True
-) -> Set[str]:
+ hass: HomeAssistant, service_call: ServiceCall, expand_group: bool = True
+) -> set[str]:
"""Extract a list of entity ids from a service call.
Will convert group entity ids to the entity ids it represents.
@@ -212,11 +264,11 @@ def extract_entity_ids(
@bind_hass
async def async_extract_entities(
- hass: HomeAssistantType,
- entities: Iterable["Entity"],
- service_call: ha.ServiceCall,
+ hass: HomeAssistant,
+ entities: Iterable[Entity],
+ service_call: ServiceCall,
expand_group: bool = True,
-) -> List["Entity"]:
+) -> list[Entity]:
"""Extract a list of entity objects from a service call.
Will convert group entity ids to the entity ids it represents.
@@ -251,8 +303,8 @@ async def async_extract_entities(
@bind_hass
async def async_extract_entity_ids(
- hass: HomeAssistantType, service_call: ha.ServiceCall, expand_group: bool = True
-) -> Set[str]:
+ hass: HomeAssistant, service_call: ServiceCall, expand_group: bool = True
+) -> set[str]:
"""Extract a set of entity ids from a service call.
Will convert group entity ids to the entity ids it represents.
@@ -263,99 +315,89 @@ async def async_extract_entity_ids(
return referenced.referenced | referenced.indirectly_referenced
+def _has_match(ids: str | list | None) -> bool:
+ """Check if ids can match anything."""
+ return ids not in (None, ENTITY_MATCH_NONE)
+
+
@bind_hass
async def async_extract_referenced_entity_ids(
- hass: HomeAssistantType, service_call: ha.ServiceCall, expand_group: bool = True
+ hass: HomeAssistant, service_call: ServiceCall, expand_group: bool = True
) -> SelectedEntities:
"""Extract referenced entity IDs from a service call."""
- entity_ids = service_call.data.get(ATTR_ENTITY_ID)
- device_ids = service_call.data.get(ATTR_DEVICE_ID)
- area_ids = service_call.data.get(ATTR_AREA_ID)
-
- selects_entity_ids = entity_ids not in (None, ENTITY_MATCH_NONE)
- selects_device_ids = device_ids not in (None, ENTITY_MATCH_NONE)
- selects_area_ids = area_ids not in (None, ENTITY_MATCH_NONE)
-
+ selector = ServiceTargetSelector(service_call)
selected = SelectedEntities()
- if not selects_entity_ids and not selects_device_ids and not selects_area_ids:
+ if not selector.has_any_selector:
return selected
- if selects_entity_ids:
- assert entity_ids is not None
+ entity_ids = selector.entity_ids
+ if expand_group:
+ entity_ids = hass.components.group.expand_entity_ids(entity_ids)
- # Entity ID attr can be a list or a string
- if isinstance(entity_ids, str):
- entity_ids = [entity_ids]
+ selected.referenced.update(entity_ids)
- if expand_group:
- entity_ids = hass.components.group.expand_entity_ids(entity_ids)
-
- selected.referenced.update(entity_ids)
-
- if not selects_device_ids and not selects_area_ids:
+ if not selector.device_ids and not selector.area_ids:
return selected
- area_reg, dev_reg, ent_reg = cast(
- Tuple[
- area_registry.AreaRegistry,
- device_registry.DeviceRegistry,
- entity_registry.EntityRegistry,
- ],
- await asyncio.gather(
- area_registry.async_get_registry(hass),
- device_registry.async_get_registry(hass),
- entity_registry.async_get_registry(hass),
- ),
- )
+ ent_reg = entity_registry.async_get(hass)
+ dev_reg = device_registry.async_get(hass)
+ area_reg = area_registry.async_get(hass)
- picked_devices = set()
+ for device_id in selector.device_ids:
+ if device_id not in dev_reg.devices:
+ selected.missing_devices.add(device_id)
- if selects_device_ids:
- if isinstance(device_ids, str):
- picked_devices = {device_ids}
- else:
- assert isinstance(device_ids, list)
- picked_devices = set(device_ids)
+ for area_id in selector.area_ids:
+ if area_id not in area_reg.areas:
+ selected.missing_areas.add(area_id)
- for device_id in picked_devices:
- if device_id not in dev_reg.devices:
- selected.missing_devices.add(device_id)
+ # Find devices for this area
+ selected.referenced_devices.update(selector.device_ids)
+ for device_entry in dev_reg.devices.values():
+ if device_entry.area_id in selector.area_ids:
+ selected.referenced_devices.add(device_entry.id)
- if selects_area_ids:
- assert area_ids is not None
+ if not selector.area_ids and not selected.referenced_devices:
+ return selected
- if isinstance(area_ids, str):
- area_lookup = {area_ids}
- else:
- area_lookup = set(area_ids)
+ for ent_entry in ent_reg.entities.values():
+ if ent_entry.area_id in selector.area_ids or (
+ not ent_entry.area_id and ent_entry.device_id in selected.referenced_devices
+ ):
+ selected.indirectly_referenced.add(ent_entry.entity_id)
- for area_id in area_lookup:
- if area_id not in area_reg.areas:
- selected.missing_areas.add(area_id)
- continue
+ return selected
- # Find entities tied to an area
- for entity_entry in ent_reg.entities.values():
- if entity_entry.area_id in area_lookup:
- selected.indirectly_referenced.add(entity_entry.entity_id)
- # Find devices for this area
- for device_entry in dev_reg.devices.values():
- if device_entry.area_id in area_lookup:
- picked_devices.add(device_entry.id)
+@bind_hass
+async def async_extract_config_entry_ids(
+ hass: HomeAssistant, service_call: ServiceCall, expand_group: bool = True
+) -> set:
+ """Extract referenced config entry ids from a service call."""
+ referenced = await async_extract_referenced_entity_ids(
+ hass, service_call, expand_group
+ )
+ ent_reg = entity_registry.async_get(hass)
+ dev_reg = device_registry.async_get(hass)
+ config_entry_ids: set[str] = set()
- if not picked_devices:
- return selected
+ # Some devices may have no entities
+ for device_id in referenced.referenced_devices:
+ if device_id in dev_reg.devices:
+ device = dev_reg.async_get(device_id)
+ if device is not None:
+ config_entry_ids.update(device.config_entries)
- for entity_entry in ent_reg.entities.values():
- if not entity_entry.area_id and entity_entry.device_id in picked_devices:
- selected.indirectly_referenced.add(entity_entry.entity_id)
+ for entity_id in referenced.referenced | referenced.indirectly_referenced:
+ entry = ent_reg.async_get(entity_id)
+ if entry is not None and entry.config_entry_id is not None:
+ config_entry_ids.add(entry.config_entry_id)
- return selected
+ return config_entry_ids
-def _load_services_file(hass: HomeAssistantType, integration: Integration) -> JSON_TYPE:
+def _load_services_file(hass: HomeAssistant, integration: Integration) -> JSON_TYPE:
"""Load services file for an integration."""
try:
return load_yaml(str(integration.file_path / "services.yaml"))
@@ -372,16 +414,16 @@ def _load_services_file(hass: HomeAssistantType, integration: Integration) -> JS
def _load_services_files(
- hass: HomeAssistantType, integrations: Iterable[Integration]
-) -> List[JSON_TYPE]:
+ hass: HomeAssistant, integrations: Iterable[Integration]
+) -> list[JSON_TYPE]:
"""Load service files for multiple intergrations."""
return [_load_services_file(hass, integration) for integration in integrations]
@bind_hass
async def async_get_all_descriptions(
- hass: HomeAssistantType,
-) -> Dict[str, Dict[str, Any]]:
+ hass: HomeAssistant,
+) -> dict[str, dict[str, Any]]:
"""Return descriptions (i.e. user documentation) for all service calls."""
descriptions_cache = hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {})
format_cache_key = "{}.{}".format
@@ -413,7 +455,7 @@ async def async_get_all_descriptions(
loaded[domain] = content
# Build response
- descriptions: Dict[str, Dict[str, Any]] = {}
+ descriptions: dict[str, dict[str, Any]] = {}
for domain in services:
descriptions[domain] = {}
@@ -429,39 +471,49 @@ async def async_get_all_descriptions(
# Don't warn for missing services, because it triggers false
# positives for things like scripts, that register as a service
- description = descriptions_cache[cache_key] = {
+ description = {
+ "name": yaml_description.get("name", ""),
"description": yaml_description.get("description", ""),
"fields": yaml_description.get("fields", {}),
}
+ if "target" in yaml_description:
+ description["target"] = yaml_description["target"]
+
+ descriptions_cache[cache_key] = description
+
descriptions[domain][service] = description
return descriptions
-@ha.callback
+@callback
@bind_hass
def async_set_service_schema(
- hass: HomeAssistantType, domain: str, service: str, schema: Dict[str, Any]
+ hass: HomeAssistant, domain: str, service: str, schema: dict[str, Any]
) -> None:
"""Register a description for a service."""
hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {})
description = {
- "description": schema.get("description") or "",
- "fields": schema.get("fields") or {},
+ "name": schema.get("name", ""),
+ "description": schema.get("description", ""),
+ "fields": schema.get("fields", {}),
}
+ if "target" in schema:
+ description["target"] = schema["target"]
+
hass.data[SERVICE_DESCRIPTION_CACHE][f"{domain}.{service}"] = description
@bind_hass
async def entity_service_call(
- hass: HomeAssistantType,
- platforms: Iterable["EntityPlatform"],
- func: Union[str, Callable[..., Any]],
- call: ha.ServiceCall,
- required_features: Optional[Iterable[int]] = None,
+ hass: HomeAssistant,
+ platforms: Iterable[EntityPlatform],
+ func: str | Callable[..., Any],
+ call: ServiceCall,
+ required_features: Iterable[int] | None = None,
) -> None:
"""Handle an entity service call.
@@ -471,17 +523,17 @@ async def entity_service_call(
user = await hass.auth.async_get_user(call.context.user_id)
if user is None:
raise UnknownUser(context=call.context)
- entity_perms: Optional[
+ entity_perms: None | (
Callable[[str, str], bool]
- ] = user.permissions.check_entity
+ ) = user.permissions.check_entity
else:
entity_perms = None
target_all_entities = call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL
if target_all_entities:
- referenced: Optional[SelectedEntities] = None
- all_referenced: Optional[Set[str]] = None
+ referenced: SelectedEntities | None = None
+ all_referenced: set[str] | None = None
else:
# A set of entities we're trying to target.
referenced = await async_extract_referenced_entity_ids(hass, call, True)
@@ -489,7 +541,7 @@ async def entity_service_call(
# If the service function is a string, we'll pass it the service call data
if isinstance(func, str):
- data: Union[Dict, ha.ServiceCall] = {
+ data: dict | ServiceCall = {
key: val
for key, val in call.data.items()
if key not in cv.ENTITY_SERVICE_FIELDS
@@ -501,7 +553,7 @@ async def entity_service_call(
# Check the permissions
# A list with entities to call the service on.
- entity_candidates: List["Entity"] = []
+ entity_candidates: list[Entity] = []
if entity_perms is None:
for platform in platforms:
@@ -584,8 +636,10 @@ async def entity_service_call(
done, pending = await asyncio.wait(
[
- entity.async_request_call(
- _handle_entity_call(hass, entity, func, data, call.context)
+ asyncio.create_task(
+ entity.async_request_call(
+ _handle_entity_call(hass, entity, func, data, call.context)
+ )
)
for entity in entities
]
@@ -603,7 +657,7 @@ async def entity_service_call(
# Context expires if the turn on commands took a long time.
# Set context again so it's there when we update
entity.async_set_context(call.context)
- tasks.append(entity.async_update_ha_state(True))
+ tasks.append(asyncio.create_task(entity.async_update_ha_state(True)))
if tasks:
done, pending = await asyncio.wait(tasks)
@@ -613,11 +667,11 @@ async def entity_service_call(
async def _handle_entity_call(
- hass: HomeAssistantType,
- entity: "Entity",
- func: Union[str, Callable[..., Any]],
- data: Union[Dict, ha.ServiceCall],
- context: ha.Context,
+ hass: HomeAssistant,
+ entity: Entity,
+ func: str | Callable[..., Any],
+ data: dict | ServiceCall,
+ context: Context,
) -> None:
"""Handle calling service method."""
entity.async_set_context(context)
@@ -641,18 +695,18 @@ async def _handle_entity_call(
@bind_hass
-@ha.callback
+@callback
def async_register_admin_service(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
domain: str,
service: str,
- service_func: Callable[[ha.ServiceCall], Optional[Awaitable]],
+ service_func: Callable[[ServiceCall], Awaitable | None],
schema: vol.Schema = vol.Schema({}, extra=vol.PREVENT_EXTRA),
) -> None:
"""Register a service that requires admin access."""
@wraps(service_func)
- async def admin_handler(call: ha.ServiceCall) -> None:
+ async def admin_handler(call: ServiceCall) -> None:
if call.context.user_id:
user = await hass.auth.async_get_user(call.context.user_id)
if user is None:
@@ -668,16 +722,20 @@ async def admin_handler(call: ha.ServiceCall) -> None:
@bind_hass
-@ha.callback
-def verify_domain_control(hass: HomeAssistantType, domain: str) -> Callable:
+@callback
+def verify_domain_control(
+ hass: HomeAssistant, domain: str
+) -> Callable[[Callable[[ServiceCall], Any]], Callable[[ServiceCall], Any]]:
"""Ensure permission to access any entity under domain in service call."""
- def decorator(service_handler: Callable[[ha.ServiceCall], Any]) -> Callable:
+ def decorator(
+ service_handler: Callable[[ServiceCall], Any]
+ ) -> Callable[[ServiceCall], Any]:
"""Decorate."""
if not asyncio.iscoroutinefunction(service_handler):
raise HomeAssistantError("Can only decorate async functions.")
- async def check_permissions(call: ha.ServiceCall) -> Any:
+ async def check_permissions(call: ServiceCall) -> Any:
"""Check user permission and raise before call if unauthorized."""
if not call.context.user_id:
return await service_handler(call)
diff --git a/homeassistant/helpers/significant_change.py b/homeassistant/helpers/significant_change.py
index 0a5b6aae10d24c..b34df0075a32e4 100644
--- a/homeassistant/helpers/significant_change.py
+++ b/homeassistant/helpers/significant_change.py
@@ -26,8 +26,10 @@ async def async_check_significant_change(
- if either state is unknown/unavailable
- state adding/removing
"""
+from __future__ import annotations
+
from types import MappingProxyType
-from typing import Any, Callable, Dict, Optional, Union
+from typing import Any, Callable, Optional, Union
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State, callback
@@ -47,13 +49,28 @@ async def async_check_significant_change(
Optional[bool],
]
+ExtraCheckTypeFunc = Callable[
+ [
+ HomeAssistant,
+ str,
+ Union[dict, MappingProxyType],
+ Any,
+ str,
+ Union[dict, MappingProxyType],
+ Any,
+ ],
+ Optional[bool],
+]
+
async def create_checker(
- hass: HomeAssistant, _domain: str
-) -> "SignificantlyChangedChecker":
+ hass: HomeAssistant,
+ _domain: str,
+ extra_significant_check: ExtraCheckTypeFunc | None = None,
+) -> SignificantlyChangedChecker:
"""Create a significantly changed checker for a domain."""
await _initialize(hass)
- return SignificantlyChangedChecker(hass)
+ return SignificantlyChangedChecker(hass, extra_significant_check)
# Marked as singleton so multiple calls all wait for same output.
@@ -73,15 +90,15 @@ async def process_platform(
await async_process_integration_platforms(hass, PLATFORM, process_platform)
-def either_one_none(val1: Optional[Any], val2: Optional[Any]) -> bool:
+def either_one_none(val1: Any | None, val2: Any | None) -> bool:
"""Test if exactly one value is None."""
return (val1 is None and val2 is not None) or (val1 is not None and val2 is None)
def check_numeric_changed(
- val1: Optional[Union[int, float]],
- val2: Optional[Union[int, float]],
- change: Union[int, float],
+ val1: int | float | None,
+ val2: int | float | None,
+ change: int | float,
) -> bool:
"""Check if two numeric values have changed."""
if val1 is None and val2 is None:
@@ -105,62 +122,85 @@ class SignificantlyChangedChecker:
Will always compare the entity to the last entity that was considered significant.
"""
- def __init__(self, hass: HomeAssistant) -> None:
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ extra_significant_check: ExtraCheckTypeFunc | None = None,
+ ) -> None:
"""Test if an entity has significantly changed."""
self.hass = hass
- self.last_approved_entities: Dict[str, State] = {}
+ self.last_approved_entities: dict[str, tuple[State, Any]] = {}
+ self.extra_significant_check = extra_significant_check
@callback
- def async_is_significant_change(self, new_state: State) -> bool:
- """Return if this was a significant change."""
- old_state: Optional[State] = self.last_approved_entities.get(
+ def async_is_significant_change(
+ self, new_state: State, *, extra_arg: Any | None = None
+ ) -> bool:
+ """Return if this was a significant change.
+
+ Extra kwargs are passed to the extra significant checker.
+ """
+ old_data: tuple[State, Any] | None = self.last_approved_entities.get(
new_state.entity_id
)
# First state change is always ok to report
- if old_state is None:
- self.last_approved_entities[new_state.entity_id] = new_state
+ if old_data is None:
+ self.last_approved_entities[new_state.entity_id] = (new_state, extra_arg)
return True
+ old_state, old_extra_arg = old_data
+
# Handle state unknown or unavailable
if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
if new_state.state == old_state.state:
return False
- self.last_approved_entities[new_state.entity_id] = new_state
+ self.last_approved_entities[new_state.entity_id] = (new_state, extra_arg)
return True
# If last state was unknown/unavailable, also significant.
if old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
- self.last_approved_entities[new_state.entity_id] = new_state
+ self.last_approved_entities[new_state.entity_id] = (new_state, extra_arg)
return True
- functions: Optional[Dict[str, CheckTypeFunc]] = self.hass.data.get(
- DATA_FUNCTIONS
- )
+ functions: dict[str, CheckTypeFunc] | None = self.hass.data.get(DATA_FUNCTIONS)
if functions is None:
raise RuntimeError("Significant Change not initialized")
check_significantly_changed = functions.get(new_state.domain)
- # No platform available means always true.
- if check_significantly_changed is None:
- self.last_approved_entities[new_state.entity_id] = new_state
- return True
+ if check_significantly_changed is not None:
+ result = check_significantly_changed(
+ self.hass,
+ old_state.state,
+ old_state.attributes,
+ new_state.state,
+ new_state.attributes,
+ )
- result = check_significantly_changed(
- self.hass,
- old_state.state,
- old_state.attributes,
- new_state.state,
- new_state.attributes,
- )
+ if result is False:
+ return False
- if result is False:
- return False
+ if self.extra_significant_check is not None:
+ result = self.extra_significant_check(
+ self.hass,
+ old_state.state,
+ old_state.attributes,
+ old_extra_arg,
+ new_state.state,
+ new_state.attributes,
+ extra_arg,
+ )
+
+ if result is False:
+ return False
# Result is either True or None.
# None means the function doesn't know. For now assume it's True
- self.last_approved_entities[new_state.entity_id] = new_state
+ self.last_approved_entities[new_state.entity_id] = (
+ new_state,
+ extra_arg,
+ )
return True
diff --git a/homeassistant/helpers/singleton.py b/homeassistant/helpers/singleton.py
index ab4d12dc1cc3b5..a48ea5d64f0783 100644
--- a/homeassistant/helpers/singleton.py
+++ b/homeassistant/helpers/singleton.py
@@ -1,7 +1,9 @@
"""Helper to help coordinating calls."""
+from __future__ import annotations
+
import asyncio
import functools
-from typing import Callable, Optional, TypeVar, cast
+from typing import Callable, TypeVar, cast
from homeassistant.core import HomeAssistant
from homeassistant.loader import bind_hass
@@ -24,7 +26,7 @@ def wrapper(func: FUNC) -> FUNC:
@bind_hass
@functools.wraps(func)
def wrapped(hass: HomeAssistant) -> T:
- obj: Optional[T] = hass.data.get(data_key)
+ obj: T | None = hass.data.get(data_key)
if obj is None:
obj = hass.data[data_key] = func(hass)
return obj
diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py
index 87112cd913383d..c9f267a89f5545 100644
--- a/homeassistant/helpers/state.py
+++ b/homeassistant/helpers/state.py
@@ -1,10 +1,12 @@
"""Helpers that help with state related things."""
+from __future__ import annotations
+
import asyncio
from collections import defaultdict
import datetime as dt
import logging
from types import ModuleType, TracebackType
-from typing import Any, Dict, Iterable, List, Optional, Type, Union
+from typing import Any, Iterable
from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON
from homeassistant.const import (
@@ -18,11 +20,11 @@
STATE_UNKNOWN,
STATE_UNLOCKED,
)
-from homeassistant.core import Context, State
+from homeassistant.core import Context, HomeAssistant, State
from homeassistant.loader import IntegrationNotFound, async_get_integration, bind_hass
import homeassistant.util.dt as dt_util
-from .typing import HomeAssistantType
+from .frame import report
_LOGGER = logging.getLogger(__name__)
@@ -35,24 +37,27 @@ class AsyncTrackStates:
when with-block is exited.
Must be run within the event loop.
+
+ Deprecated. Remove after June 2021.
+ Warning added via `get_changed_since`.
"""
- def __init__(self, hass: HomeAssistantType) -> None:
+ def __init__(self, hass: HomeAssistant) -> None:
"""Initialize a TrackStates block."""
self.hass = hass
- self.states: List[State] = []
+ self.states: list[State] = []
# pylint: disable=attribute-defined-outside-init
- def __enter__(self) -> List[State]:
+ def __enter__(self) -> list[State]:
"""Record time from which to track changes."""
self.now = dt_util.utcnow()
return self.states
def __exit__(
self,
- exc_type: Optional[Type[BaseException]],
- exc_value: Optional[BaseException],
- traceback: Optional[TracebackType],
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
) -> None:
"""Add changes states to changes list."""
self.states.extend(get_changed_since(self.hass.states.async_all(), self.now))
@@ -60,29 +65,33 @@ def __exit__(
def get_changed_since(
states: Iterable[State], utc_point_in_time: dt.datetime
-) -> List[State]:
- """Return list of states that have been changed since utc_point_in_time."""
+) -> list[State]:
+ """Return list of states that have been changed since utc_point_in_time.
+
+ Deprecated. Remove after June 2021.
+ """
+ report("uses deprecated `get_changed_since`")
return [state for state in states if state.last_updated >= utc_point_in_time]
@bind_hass
async def async_reproduce_state(
- hass: HomeAssistantType,
- states: Union[State, Iterable[State]],
+ hass: HomeAssistant,
+ states: State | Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a list of states on multiple domains."""
if isinstance(states, State):
states = [states]
- to_call: Dict[str, List[State]] = defaultdict(list)
+ to_call: dict[str, list[State]] = defaultdict(list)
for state in states:
to_call[state.domain].append(state)
- async def worker(domain: str, states_by_domain: List[State]) -> None:
+ async def worker(domain: str, states_by_domain: list[State]) -> None:
try:
integration = await async_get_integration(hass, domain)
except IntegrationNotFound:
@@ -92,7 +101,7 @@ async def worker(domain: str, states_by_domain: List[State]) -> None:
return
try:
- platform: Optional[ModuleType] = integration.get_platform("reproduce_state")
+ platform: ModuleType | None = integration.get_platform("reproduce_state")
except ImportError:
_LOGGER.warning("Integration %s does not support reproduce state", domain)
return
diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py
index a969b2cad9a7ee..5a08a97a210045 100644
--- a/homeassistant/helpers/storage.py
+++ b/homeassistant/helpers/storage.py
@@ -1,9 +1,12 @@
"""Helper to help store data."""
+from __future__ import annotations
+
import asyncio
+from contextlib import suppress
from json import JSONEncoder
import logging
import os
-from typing import Any, Callable, Dict, List, Optional, Type, Union
+from typing import Any, Callable
from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE
from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback
@@ -71,18 +74,18 @@ def __init__(
key: str,
private: bool = False,
*,
- encoder: Optional[Type[JSONEncoder]] = None,
+ encoder: type[JSONEncoder] | None = None,
):
"""Initialize storage class."""
self.version = version
self.key = key
self.hass = hass
self._private = private
- self._data: Optional[Dict[str, Any]] = None
- self._unsub_delay_listener: Optional[CALLBACK_TYPE] = None
- self._unsub_final_write_listener: Optional[CALLBACK_TYPE] = None
+ self._data: dict[str, Any] | None = None
+ self._unsub_delay_listener: CALLBACK_TYPE | None = None
+ self._unsub_final_write_listener: CALLBACK_TYPE | None = None
self._write_lock = asyncio.Lock()
- self._load_task: Optional[asyncio.Future] = None
+ self._load_task: asyncio.Future | None = None
self._encoder = encoder
@property
@@ -90,7 +93,7 @@ def path(self):
"""Return the config path."""
return self.hass.config.path(STORAGE_DIR, self.key)
- async def async_load(self) -> Union[Dict, List, None]:
+ async def async_load(self) -> dict | list | None:
"""Load data.
If the expected version does not match the given version, the migrate
@@ -140,7 +143,7 @@ async def _async_load_data(self):
return stored
- async def async_save(self, data: Union[Dict, List]) -> None:
+ async def async_save(self, data: dict | list) -> None:
"""Save data."""
self._data = {"version": self.version, "key": self.key, "data": data}
@@ -151,7 +154,7 @@ async def async_save(self, data: Union[Dict, List]) -> None:
await self._async_handle_write_data()
@callback
- def async_delay_save(self, data_func: Callable[[], Dict], delay: float = 0) -> None:
+ def async_delay_save(self, data_func: Callable[[], dict], delay: float = 0) -> None:
"""Save data with an optional delay."""
self._data = {"version": self.version, "key": self.key, "data_func": data_func}
@@ -202,7 +205,6 @@ async def _async_callback_final_write(self, _event):
async def _async_handle_write_data(self, *_args):
"""Handle writing the config."""
-
async with self._write_lock:
self._async_cleanup_delay_listener()
self._async_cleanup_final_write_listener()
@@ -225,7 +227,7 @@ async def _async_handle_write_data(self, *_args):
except (json_util.SerializationError, json_util.WriteError) as err:
_LOGGER.error("Error writing config for %s: %s", self.key, err)
- def _write_data(self, path: str, data: Dict) -> None:
+ def _write_data(self, path: str, data: dict) -> None:
"""Write the data."""
if not os.path.isdir(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
@@ -242,7 +244,5 @@ async def async_remove(self):
self._async_cleanup_delay_listener()
self._async_cleanup_final_write_listener()
- try:
+ with suppress(FileNotFoundError):
await self.hass.async_add_executor_job(os.unlink, self.path)
- except FileNotFoundError:
- pass
diff --git a/homeassistant/helpers/sun.py b/homeassistant/helpers/sun.py
index 818010c3410599..3c18dcc32784a5 100644
--- a/homeassistant/helpers/sun.py
+++ b/homeassistant/helpers/sun.py
@@ -1,65 +1,71 @@
"""Helpers for sun events."""
+from __future__ import annotations
+
import datetime
-from typing import TYPE_CHECKING, Optional, Union
+from typing import TYPE_CHECKING
from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
from homeassistant.loader import bind_hass
from homeassistant.util import dt as dt_util
-from .typing import HomeAssistantType
-
if TYPE_CHECKING:
- import astral # pylint: disable=unused-import
+ import astral
DATA_LOCATION_CACHE = "astral_location_cache"
+ELEVATION_AGNOSTIC_EVENTS = ("noon", "midnight")
+
@callback
@bind_hass
-def get_astral_location(hass: HomeAssistantType) -> "astral.Location":
+def get_astral_location(
+ hass: HomeAssistant,
+) -> tuple[astral.location.Location, astral.Elevation]:
"""Get an astral location for the current Home Assistant configuration."""
-
- from astral import Location # pylint: disable=import-outside-toplevel
+ from astral import LocationInfo # pylint: disable=import-outside-toplevel
+ from astral.location import Location # pylint: disable=import-outside-toplevel
latitude = hass.config.latitude
longitude = hass.config.longitude
timezone = str(hass.config.time_zone)
elevation = hass.config.elevation
- info = ("", "", latitude, longitude, timezone, elevation)
+ info = ("", "", timezone, latitude, longitude)
# Cache astral locations so they aren't recreated with the same args
if DATA_LOCATION_CACHE not in hass.data:
hass.data[DATA_LOCATION_CACHE] = {}
if info not in hass.data[DATA_LOCATION_CACHE]:
- hass.data[DATA_LOCATION_CACHE][info] = Location(info)
+ hass.data[DATA_LOCATION_CACHE][info] = Location(LocationInfo(*info))
- return hass.data[DATA_LOCATION_CACHE][info]
+ return hass.data[DATA_LOCATION_CACHE][info], elevation
@callback
@bind_hass
def get_astral_event_next(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
event: str,
- utc_point_in_time: Optional[datetime.datetime] = None,
- offset: Optional[datetime.timedelta] = None,
+ utc_point_in_time: datetime.datetime | None = None,
+ offset: datetime.timedelta | None = None,
) -> datetime.datetime:
"""Calculate the next specified solar event."""
- location = get_astral_location(hass)
- return get_location_astral_event_next(location, event, utc_point_in_time, offset)
+ location, elevation = get_astral_location(hass)
+ return get_location_astral_event_next(
+ location, elevation, event, utc_point_in_time, offset
+ )
@callback
def get_location_astral_event_next(
- location: "astral.Location",
+ location: astral.location.Location,
+ elevation: astral.Elevation,
event: str,
- utc_point_in_time: Optional[datetime.datetime] = None,
- offset: Optional[datetime.timedelta] = None,
+ utc_point_in_time: datetime.datetime | None = None,
+ offset: datetime.timedelta | None = None,
) -> datetime.datetime:
"""Calculate the next specified solar event."""
- from astral import AstralError # pylint: disable=import-outside-toplevel
if offset is None:
offset = datetime.timedelta()
@@ -67,6 +73,10 @@ def get_location_astral_event_next(
if utc_point_in_time is None:
utc_point_in_time = dt_util.utcnow()
+ kwargs = {"local": False}
+ if event not in ELEVATION_AGNOSTIC_EVENTS:
+ kwargs["observer_elevation"] = elevation
+
mod = -1
while True:
try:
@@ -74,13 +84,13 @@ def get_location_astral_event_next(
getattr(location, event)(
dt_util.as_local(utc_point_in_time).date()
+ datetime.timedelta(days=mod),
- local=False,
+ **kwargs,
)
+ offset
)
if next_dt > utc_point_in_time:
return next_dt
- except AstralError:
+ except ValueError:
pass
mod += 1
@@ -88,14 +98,12 @@ def get_location_astral_event_next(
@callback
@bind_hass
def get_astral_event_date(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
event: str,
- date: Union[datetime.date, datetime.datetime, None] = None,
-) -> Optional[datetime.datetime]:
+ date: datetime.date | datetime.datetime | None = None,
+) -> datetime.datetime | None:
"""Calculate the astral event time for the specified date."""
- from astral import AstralError # pylint: disable=import-outside-toplevel
-
- location = get_astral_location(hass)
+ location, elevation = get_astral_location(hass)
if date is None:
date = dt_util.now().date()
@@ -103,9 +111,13 @@ def get_astral_event_date(
if isinstance(date, datetime.datetime):
date = dt_util.as_local(date).date()
+ kwargs = {"local": False}
+ if event not in ELEVATION_AGNOSTIC_EVENTS:
+ kwargs["observer_elevation"] = elevation
+
try:
- return getattr(location, event)(date, local=False) # type: ignore
- except AstralError:
+ return getattr(location, event)(date, **kwargs) # type: ignore
+ except ValueError:
# Event never occurs for specified date.
return None
@@ -113,7 +125,7 @@ def get_astral_event_date(
@callback
@bind_hass
def is_up(
- hass: HomeAssistantType, utc_point_in_time: Optional[datetime.datetime] = None
+ hass: HomeAssistant, utc_point_in_time: datetime.datetime | None = None
) -> bool:
"""Calculate if the sun is currently up."""
if utc_point_in_time is None:
diff --git a/homeassistant/helpers/system_info.py b/homeassistant/helpers/system_info.py
index 9c2c4b181adeec..6d6c912f8c9d99 100644
--- a/homeassistant/helpers/system_info.py
+++ b/homeassistant/helpers/system_info.py
@@ -1,17 +1,18 @@
"""Helper to gather system info."""
+from __future__ import annotations
+
import os
import platform
-from typing import Any, Dict
+from typing import Any
from homeassistant.const import __version__ as current_version
+from homeassistant.core import HomeAssistant
from homeassistant.loader import bind_hass
from homeassistant.util.package import is_virtual_env
-from .typing import HomeAssistantType
-
@bind_hass
-async def async_get_system_info(hass: HomeAssistantType) -> Dict[str, Any]:
+async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]:
"""Return info about the system."""
info_object = {
"installation_type": "Unknown",
diff --git a/homeassistant/helpers/temperature.py b/homeassistant/helpers/temperature.py
index 18ca8355159199..e0f089e93b9d8d 100644
--- a/homeassistant/helpers/temperature.py
+++ b/homeassistant/helpers/temperature.py
@@ -1,6 +1,7 @@
"""Temperature helpers for Home Assistant."""
+from __future__ import annotations
+
from numbers import Number
-from typing import Optional
from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS
from homeassistant.core import HomeAssistant
@@ -8,8 +9,8 @@
def display_temp(
- hass: HomeAssistant, temperature: Optional[float], unit: str, precision: float
-) -> Optional[float]:
+ hass: HomeAssistant, temperature: float | None, unit: str, precision: float
+) -> float | None:
"""Convert temperature into preferred units/precision for display."""
temperature_unit = unit
ha_unit = hass.config.units.temperature_unit
diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py
index 5f506c02eef003..9580da82d65ac2 100644
--- a/homeassistant/helpers/template.py
+++ b/homeassistant/helpers/template.py
@@ -1,8 +1,12 @@
"""Template helper methods for rendering strings with Home Assistant data."""
+from __future__ import annotations
+
from ast import literal_eval
import asyncio
import base64
import collections.abc
+from contextlib import suppress
+from contextvars import ContextVar
from datetime import datetime, timedelta
from functools import partial, wraps
import json
@@ -11,7 +15,7 @@
from operator import attrgetter
import random
import re
-from typing import Any, Dict, Generator, Iterable, Optional, Type, Union, cast
+from typing import Any, Generator, Iterable, cast
from urllib.parse import urlencode as urllib_urlencode
import weakref
@@ -29,10 +33,16 @@
LENGTH_METERS,
STATE_UNKNOWN,
)
-from homeassistant.core import State, callback, split_entity_id, valid_entity_id
+from homeassistant.core import (
+ HomeAssistant,
+ State,
+ callback,
+ split_entity_id,
+ valid_entity_id,
+)
from homeassistant.exceptions import TemplateError
-from homeassistant.helpers import location as loc_helper
-from homeassistant.helpers.typing import HomeAssistantType, TemplateVarsType
+from homeassistant.helpers import entity_registry, location as loc_helper
+from homeassistant.helpers.typing import TemplateVarsType
from homeassistant.loader import bind_hass
from homeassistant.util import convert, dt as dt_util, location as loc_util
from homeassistant.util.async_ import run_callback_threadsafe
@@ -46,6 +56,7 @@
_RENDER_INFO = "template.render_info"
_ENVIRONMENT = "template.environment"
+_ENVIRONMENT_LIMITED = "template.environment_limited"
_RE_JINJA_DELIMITERS = re.compile(r"\{%|\{\{|\{#")
# Match "simple" ints and floats. -1.0, 1, +5, 5.0
@@ -69,9 +80,11 @@
ALL_STATES_RATE_LIMIT = timedelta(minutes=1)
DOMAIN_STATES_RATE_LIMIT = timedelta(seconds=1)
+template_cv: ContextVar[str | None] = ContextVar("template_cv", default=None)
+
@bind_hass
-def attach(hass: HomeAssistantType, obj: Any) -> None:
+def attach(hass: HomeAssistant, obj: Any) -> None:
"""Recursively attach hass to all template instances in list and dict."""
if isinstance(obj, list):
for child in obj:
@@ -84,7 +97,9 @@ def attach(hass: HomeAssistantType, obj: Any) -> None:
obj.hass = hass
-def render_complex(value: Any, variables: TemplateVarsType = None) -> Any:
+def render_complex(
+ value: Any, variables: TemplateVarsType = None, limited: bool = False
+) -> Any:
"""Recursive template creator helper function."""
if isinstance(value, list):
return [render_complex(item, variables) for item in value]
@@ -94,7 +109,7 @@ def render_complex(value: Any, variables: TemplateVarsType = None) -> Any:
for key, item in value.items()
}
if isinstance(value, Template):
- return value.async_render(variables)
+ return value.async_render(variables, limited=limited)
return value
@@ -120,7 +135,7 @@ def is_template_string(maybe_template: str) -> bool:
class ResultWrapper:
"""Result wrapper class to store render result."""
- render_result: Optional[str]
+ render_result: str | None
def gen_result_wrapper(kls):
@@ -129,7 +144,7 @@ def gen_result_wrapper(kls):
class Wrapper(kls, ResultWrapper):
"""Wrapper of a kls that can store render_result."""
- def __init__(self, *args: tuple, render_result: Optional[str] = None) -> None:
+ def __init__(self, *args: tuple, render_result: str | None = None) -> None:
super().__init__(*args)
self.render_result = render_result
@@ -151,15 +166,13 @@ class TupleWrapper(tuple, ResultWrapper):
# This is all magic to be allowed to subclass a tuple.
- def __new__(
- cls, value: tuple, *, render_result: Optional[str] = None
- ) -> "TupleWrapper":
+ def __new__(cls, value: tuple, *, render_result: str | None = None) -> TupleWrapper:
"""Create a new tuple class."""
return super().__new__(cls, tuple(value))
# pylint: disable=super-init-not-called
- def __init__(self, value: tuple, *, render_result: Optional[str] = None):
+ def __init__(self, value: tuple, *, render_result: str | None = None):
"""Initialize a new tuple class."""
self.render_result = render_result
@@ -171,7 +184,7 @@ def __str__(self) -> str:
return self.render_result
-RESULT_WRAPPERS: Dict[Type, Type] = {
+RESULT_WRAPPERS: dict[type, type] = {
kls: gen_result_wrapper(kls) # type: ignore[no-untyped-call]
for kls in (list, dict, set)
}
@@ -195,15 +208,15 @@ def __init__(self, template):
# Will be set sensibly once frozen.
self.filter_lifecycle = _true
self.filter = _true
- self._result: Optional[str] = None
+ self._result: str | None = None
self.is_static = False
- self.exception: Optional[TemplateError] = None
+ self.exception: TemplateError | None = None
self.all_states = False
self.all_states_lifecycle = False
self.domains = set()
self.domains_lifecycle = set()
self.entities = set()
- self.rate_limit: Optional[timedelta] = None
+ self.rate_limit: timedelta | None = None
self.has_time = False
def __repr__(self) -> str:
@@ -279,6 +292,7 @@ class Template:
"is_static",
"_compiled_code",
"_compiled",
+ "_limited",
)
def __init__(self, template, hass=None):
@@ -288,17 +302,19 @@ def __init__(self, template, hass=None):
self.template: str = template.strip()
self._compiled_code = None
- self._compiled: Optional[Template] = None
+ self._compiled: jinja2.Template | None = None
self.hass = hass
self.is_static = not is_template_string(template)
+ self._limited = None
@property
- def _env(self) -> "TemplateEnvironment":
+ def _env(self) -> TemplateEnvironment:
if self.hass is None:
return _NO_HASS_ENV
- ret: Optional[TemplateEnvironment] = self.hass.data.get(_ENVIRONMENT)
+ wanted_env = _ENVIRONMENT_LIMITED if self._limited else _ENVIRONMENT
+ ret: TemplateEnvironment | None = self.hass.data.get(wanted_env)
if ret is None:
- ret = self.hass.data[_ENVIRONMENT] = TemplateEnvironment(self.hass) # type: ignore[no-untyped-call]
+ ret = self.hass.data[wanted_env] = TemplateEnvironment(self.hass, self._limited) # type: ignore[no-untyped-call]
return ret
def ensure_valid(self) -> None:
@@ -315,17 +331,21 @@ def render(
self,
variables: TemplateVarsType = None,
parse_result: bool = True,
+ limited: bool = False,
**kwargs: Any,
) -> Any:
- """Render given template."""
+ """Render given template.
+
+ If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine.
+ """
if self.is_static:
- if self.hass.config.legacy_templates or not parse_result:
+ if not parse_result or self.hass.config.legacy_templates:
return self.template
return self._parse_result(self.template)
return run_callback_threadsafe(
self.hass.loop,
- partial(self.async_render, variables, parse_result, **kwargs),
+ partial(self.async_render, variables, parse_result, limited, **kwargs),
).result()
@callback
@@ -333,25 +353,28 @@ def async_render(
self,
variables: TemplateVarsType = None,
parse_result: bool = True,
+ limited: bool = False,
**kwargs: Any,
) -> Any:
"""Render given template.
This method must be run in the event loop.
+
+ If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine.
"""
if self.is_static:
- if self.hass.config.legacy_templates or not parse_result:
+ if not parse_result or self.hass.config.legacy_templates:
return self.template
return self._parse_result(self.template)
- compiled = self._compiled or self._ensure_compiled()
+ compiled = self._compiled or self._ensure_compiled(limited)
if variables is not None:
kwargs.update(variables)
try:
- render_result = compiled.render(kwargs)
- except Exception as err: # pylint: disable=broad-except
+ render_result = _render_with_context(self.template, compiled, **kwargs)
+ except Exception as err:
raise TemplateError(err) from err
render_result = render_result.strip()
@@ -410,8 +433,6 @@ async def async_render_will_timeout(
This method must be run in the event loop.
"""
- assert self.hass
-
if self.is_static:
return False
@@ -424,7 +445,7 @@ async def async_render_will_timeout(
def _render_template() -> None:
try:
- compiled.render(kwargs)
+ _render_with_context(self.template, compiled, **kwargs)
except TimeoutError:
pass
finally:
@@ -502,13 +523,13 @@ def async_render_with_possible_json_value(
variables = dict(variables or {})
variables["value"] = value
- try:
+ with suppress(ValueError, TypeError):
variables["value_json"] = json.loads(value)
- except (ValueError, TypeError):
- pass
try:
- return self._compiled.render(variables).strip()
+ return _render_with_context(
+ self.template, self._compiled, **variables
+ ).strip()
except jinja2.TemplateError as ex:
if error_value is _SENTINEL:
_LOGGER.error(
@@ -519,16 +540,20 @@ def async_render_with_possible_json_value(
)
return value if error_value is _SENTINEL else error_value
- def _ensure_compiled(self) -> "Template":
+ def _ensure_compiled(self, limited: bool = False) -> jinja2.Template:
"""Bind a template to a specific hass instance."""
self.ensure_valid()
assert self.hass is not None, "hass variable not set on template"
+ assert (
+ self._limited is None or self._limited == limited
+ ), "can't change between limited and non limited template"
+ self._limited = limited
env = self._env
self._compiled = cast(
- Template,
+ jinja2.Template,
jinja2.Template.from_code(env, self._compiled_code, env.globals, None),
)
@@ -554,7 +579,7 @@ def __repr__(self) -> str:
class AllStates:
"""Class to expose all HA states as attributes."""
- def __init__(self, hass: HomeAssistantType) -> None:
+ def __init__(self, hass: HomeAssistant) -> None:
"""Initialize all states."""
self._hass = hass
@@ -608,7 +633,7 @@ def __repr__(self) -> str:
class DomainStates:
"""Class to expose a specific HA domain as attributes."""
- def __init__(self, hass: HomeAssistantType, domain: str) -> None:
+ def __init__(self, hass: HomeAssistant, domain: str) -> None:
"""Initialize the domain states."""
self._hass = hass
self._domain = domain
@@ -653,9 +678,7 @@ class TemplateState(State):
# Inheritance is done so functions that check against State keep working
# pylint: disable=super-init-not-called
- def __init__(
- self, hass: HomeAssistantType, state: State, collect: bool = True
- ) -> None:
+ def __init__(self, hass: HomeAssistant, state: State, collect: bool = True) -> None:
"""Initialize template state."""
self._hass = hass
self._state = state
@@ -753,34 +776,32 @@ def __repr__(self) -> str:
return f""
-def _collect_state(hass: HomeAssistantType, entity_id: str) -> None:
+def _collect_state(hass: HomeAssistant, entity_id: str) -> None:
entity_collect = hass.data.get(_RENDER_INFO)
if entity_collect is not None:
entity_collect.entities.add(entity_id)
-def _state_generator(hass: HomeAssistantType, domain: Optional[str]) -> Generator:
+def _state_generator(hass: HomeAssistant, domain: str | None) -> Generator:
"""State generator for a domain or all states."""
for state in sorted(hass.states.async_all(domain), key=attrgetter("entity_id")):
yield TemplateState(hass, state, collect=False)
-def _get_state_if_valid(
- hass: HomeAssistantType, entity_id: str
-) -> Optional[TemplateState]:
+def _get_state_if_valid(hass: HomeAssistant, entity_id: str) -> TemplateState | None:
state = hass.states.get(entity_id)
if state is None and not valid_entity_id(entity_id):
raise TemplateError(f"Invalid entity ID '{entity_id}'") # type: ignore
return _get_template_state_from_state(hass, entity_id, state)
-def _get_state(hass: HomeAssistantType, entity_id: str) -> Optional[TemplateState]:
+def _get_state(hass: HomeAssistant, entity_id: str) -> TemplateState | None:
return _get_template_state_from_state(hass, entity_id, hass.states.get(entity_id))
def _get_template_state_from_state(
- hass: HomeAssistantType, entity_id: str, state: Optional[State]
-) -> Optional[TemplateState]:
+ hass: HomeAssistant, entity_id: str, state: State | None
+) -> TemplateState | None:
if state is None:
# Only need to collect if none, if not none collect first actual
# access to the state properties in the state wrapper.
@@ -790,8 +811,8 @@ def _get_template_state_from_state(
def _resolve_state(
- hass: HomeAssistantType, entity_id_or_state: Any
-) -> Union[State, TemplateState, None]:
+ hass: HomeAssistant, entity_id_or_state: Any
+) -> State | TemplateState | None:
"""Return state or entity_id if given."""
if isinstance(entity_id_or_state, State):
return entity_id_or_state
@@ -800,7 +821,7 @@ def _resolve_state(
return None
-def result_as_boolean(template_result: Optional[str]) -> bool:
+def result_as_boolean(template_result: str | None) -> bool:
"""Convert the template result to a boolean.
True/not 0/'1'/'true'/'yes'/'on'/'enable' are considered truthy
@@ -818,7 +839,7 @@ def result_as_boolean(template_result: Optional[str]) -> bool:
return False
-def expand(hass: HomeAssistantType, *args: Any) -> Iterable[State]:
+def expand(hass: HomeAssistant, *args: Any) -> Iterable[State]:
"""Expand out any groups into entity states."""
search = list(args)
found = {}
@@ -850,6 +871,13 @@ def expand(hass: HomeAssistantType, *args: Any) -> Iterable[State]:
return sorted(found.values(), key=lambda a: a.entity_id)
+def device_entities(hass: HomeAssistant, device_id: str) -> Iterable[str]:
+ """Get entity ids for entities tied to a device."""
+ entity_reg = entity_registry.async_get(hass)
+ entries = entity_registry.async_entries_for_device(entity_reg, device_id)
+ return [entry.entity_id for entry in entries]
+
+
def closest(hass, *args):
"""Find closest entity.
@@ -960,7 +988,7 @@ def distance(hass, *args):
else:
if not loc_helper.has_location(point_state):
_LOGGER.warning(
- "distance:State does not contain valid location: %s", point_state
+ "Distance:State does not contain valid location: %s", point_state
)
return None
@@ -977,7 +1005,7 @@ def distance(hass, *args):
)
-def is_state(hass: HomeAssistantType, entity_id: str, state: State) -> bool:
+def is_state(hass: HomeAssistant, entity_id: str, state: State) -> bool:
"""Test if a state is a specific value."""
state_obj = _get_state(hass, entity_id)
return state_obj is not None and state_obj.state == state
@@ -1277,7 +1305,6 @@ def relative_time(value):
If the input are not a datetime object the input will be returned unmodified.
"""
-
if not isinstance(value, datetime):
return value
if not value.tzinfo:
@@ -1292,12 +1319,59 @@ def urlencode(value):
return urllib_urlencode(value).encode("utf-8")
+def _render_with_context(
+ template_str: str, template: jinja2.Template, **kwargs: Any
+) -> str:
+ """Store template being rendered in a ContextVar to aid error handling."""
+ template_cv.set(template_str)
+ return template.render(**kwargs)
+
+
+class LoggingUndefined(jinja2.Undefined):
+ """Log on undefined variables."""
+
+ def _log_message(self):
+ template = template_cv.get() or ""
+ _LOGGER.warning(
+ "Template variable warning: %s when rendering '%s'",
+ self._undefined_message,
+ template,
+ )
+
+ def _fail_with_undefined_error(self, *args, **kwargs):
+ try:
+ return super()._fail_with_undefined_error(*args, **kwargs)
+ except self._undefined_exception as ex:
+ template = template_cv.get() or ""
+ _LOGGER.error(
+ "Template variable error: %s when rendering '%s'",
+ self._undefined_message,
+ template,
+ )
+ raise ex
+
+ def __str__(self):
+ """Log undefined __str___."""
+ self._log_message()
+ return super().__str__()
+
+ def __iter__(self):
+ """Log undefined __iter___."""
+ self._log_message()
+ return super().__iter__()
+
+ def __bool__(self):
+ """Log undefined __bool___."""
+ self._log_message()
+ return super().__bool__()
+
+
class TemplateEnvironment(ImmutableSandboxedEnvironment):
"""The Home Assistant template environment."""
- def __init__(self, hass):
+ def __init__(self, hass, limited=False):
"""Initialise template environment."""
- super().__init__()
+ super().__init__(undefined=LoggingUndefined)
self.hass = hass
self.template_cache = weakref.WeakValueDictionary()
self.filters["round"] = forgiving_round
@@ -1367,6 +1441,38 @@ def wrapper(*args, **kwargs):
return contextfunction(wrapper)
+ self.globals["device_entities"] = hassfunction(device_entities)
+ self.filters["device_entities"] = contextfilter(self.globals["device_entities"])
+
+ if limited:
+ # Only device_entities is available to limited templates, mark other
+ # functions and filters as unsupported.
+ def unsupported(name):
+ def warn_unsupported(*args, **kwargs):
+ raise TemplateError(
+ f"Use of '{name}' is not supported in limited templates"
+ )
+
+ return warn_unsupported
+
+ hass_globals = [
+ "closest",
+ "distance",
+ "expand",
+ "is_state",
+ "is_state_attr",
+ "state_attr",
+ "states",
+ "utcnow",
+ "now",
+ ]
+ hass_filters = ["closest", "expand"]
+ for glob in hass_globals:
+ self.globals[glob] = unsupported(glob)
+ for filt in hass_filters:
+ self.filters[filt] = unsupported(filt)
+ return
+
self.globals["expand"] = hassfunction(expand)
self.filters["expand"] = contextfilter(self.globals["expand"])
self.globals["closest"] = hassfunction(closest)
@@ -1386,7 +1492,7 @@ def is_safe_callable(self, obj):
def is_safe_attribute(self, obj, attr, value):
"""Test if attribute is safe."""
if isinstance(obj, (AllStates, DomainStates, TemplateState)):
- return not attr[0] == "_"
+ return attr[0] != "_"
if isinstance(obj, Namespace):
return True
diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py
new file mode 100644
index 00000000000000..c92766036c639c
--- /dev/null
+++ b/homeassistant/helpers/trace.py
@@ -0,0 +1,247 @@
+"""Helpers for script and condition tracing."""
+from __future__ import annotations
+
+from collections import deque
+from contextlib import contextmanager
+from contextvars import ContextVar
+from functools import wraps
+from typing import Any, Callable, Deque, Generator, cast
+
+from homeassistant.helpers.typing import TemplateVarsType
+import homeassistant.util.dt as dt_util
+
+
+class TraceElement:
+ """Container for trace data."""
+
+ def __init__(self, variables: TemplateVarsType, path: str):
+ """Container for trace data."""
+ self._child_key: tuple[str, str] | None = None
+ self._child_run_id: str | None = None
+ self._error: Exception | None = None
+ self.path: str = path
+ self._result: dict | None = None
+ self._timestamp = dt_util.utcnow()
+
+ if variables is None:
+ variables = {}
+ last_variables = variables_cv.get() or {}
+ variables_cv.set(dict(variables))
+ changed_variables = {
+ key: value
+ for key, value in variables.items()
+ if key not in last_variables or last_variables[key] != value
+ }
+ self._variables = changed_variables
+
+ def __repr__(self) -> str:
+ """Container for trace data."""
+ return str(self.as_dict())
+
+ def set_child_id(self, child_key: tuple[str, str], child_run_id: str) -> None:
+ """Set trace id of a nested script run."""
+ self._child_key = child_key
+ self._child_run_id = child_run_id
+
+ def set_error(self, ex: Exception) -> None:
+ """Set error."""
+ self._error = ex
+
+ def set_result(self, **kwargs: Any) -> None:
+ """Set result."""
+ self._result = {**kwargs}
+
+ def as_dict(self) -> dict[str, Any]:
+ """Return dictionary version of this TraceElement."""
+ result: dict[str, Any] = {"path": self.path, "timestamp": self._timestamp}
+ if self._child_key is not None:
+ result["child_id"] = {
+ "domain": self._child_key[0],
+ "item_id": self._child_key[1],
+ "run_id": str(self._child_run_id),
+ }
+ if self._variables:
+ result["changed_variables"] = self._variables
+ if self._error is not None:
+ result["error"] = str(self._error)
+ if self._result is not None:
+ result["result"] = self._result
+ return result
+
+
+# Context variables for tracing
+# Current trace
+trace_cv: ContextVar[dict[str, Deque[TraceElement]] | None] = ContextVar(
+ "trace_cv", default=None
+)
+# Stack of TraceElements
+trace_stack_cv: ContextVar[list[TraceElement] | None] = ContextVar(
+ "trace_stack_cv", default=None
+)
+# Current location in config tree
+trace_path_stack_cv: ContextVar[list[str] | None] = ContextVar(
+ "trace_path_stack_cv", default=None
+)
+# Copy of last variables
+variables_cv: ContextVar[Any | None] = ContextVar("variables_cv", default=None)
+# (domain, item_id) + Run ID
+trace_id_cv: ContextVar[tuple[str, str] | None] = ContextVar(
+ "trace_id_cv", default=None
+)
+# Reason for stopped script execution
+script_execution_cv: ContextVar[StopReason | None] = ContextVar(
+ "script_execution_cv", default=None
+)
+
+
+def trace_id_set(trace_id: tuple[str, str]) -> None:
+ """Set id of the current trace."""
+ trace_id_cv.set(trace_id)
+
+
+def trace_id_get() -> tuple[str, str] | None:
+ """Get id if the current trace."""
+ return trace_id_cv.get()
+
+
+def trace_stack_push(trace_stack_var: ContextVar, node: Any) -> None:
+ """Push an element to the top of a trace stack."""
+ trace_stack = trace_stack_var.get()
+ if trace_stack is None:
+ trace_stack = []
+ trace_stack_var.set(trace_stack)
+ trace_stack.append(node)
+
+
+def trace_stack_pop(trace_stack_var: ContextVar) -> None:
+ """Remove the top element from a trace stack."""
+ trace_stack = trace_stack_var.get()
+ trace_stack.pop()
+
+
+def trace_stack_top(trace_stack_var: ContextVar) -> Any | None:
+ """Return the element at the top of a trace stack."""
+ trace_stack = trace_stack_var.get()
+ return trace_stack[-1] if trace_stack else None
+
+
+def trace_path_push(suffix: str | list[str]) -> int:
+ """Go deeper in the config tree."""
+ if isinstance(suffix, str):
+ suffix = [suffix]
+ for node in suffix:
+ trace_stack_push(trace_path_stack_cv, node)
+ return len(suffix)
+
+
+def trace_path_pop(count: int) -> None:
+ """Go n levels up in the config tree."""
+ for _ in range(count):
+ trace_stack_pop(trace_path_stack_cv)
+
+
+def trace_path_get() -> str:
+ """Return a string representing the current location in the config tree."""
+ path = trace_path_stack_cv.get()
+ if not path:
+ return ""
+ return "/".join(path)
+
+
+def trace_append_element(
+ trace_element: TraceElement,
+ maxlen: int | None = None,
+) -> None:
+ """Append a TraceElement to trace[path]."""
+ path = trace_element.path
+ trace = trace_cv.get()
+ if trace is None:
+ trace = {}
+ trace_cv.set(trace)
+ if path not in trace:
+ trace[path] = deque(maxlen=maxlen)
+ trace[path].append(trace_element)
+
+
+def trace_get(clear: bool = True) -> dict[str, Deque[TraceElement]] | None:
+ """Return the current trace."""
+ if clear:
+ trace_clear()
+ return trace_cv.get()
+
+
+def trace_clear() -> None:
+ """Clear the trace."""
+ trace_cv.set({})
+ trace_stack_cv.set(None)
+ trace_path_stack_cv.set(None)
+ variables_cv.set(None)
+ script_execution_cv.set(StopReason())
+
+
+def trace_set_child_id(child_key: tuple[str, str], child_run_id: str) -> None:
+ """Set child trace_id of TraceElement at the top of the stack."""
+ node = cast(TraceElement, trace_stack_top(trace_stack_cv))
+ if node:
+ node.set_child_id(child_key, child_run_id)
+
+
+def trace_set_result(**kwargs: Any) -> None:
+ """Set the result of TraceElement at the top of the stack."""
+ node = cast(TraceElement, trace_stack_top(trace_stack_cv))
+ node.set_result(**kwargs)
+
+
+class StopReason:
+ """Mutable container class for script_execution."""
+
+ script_execution: str | None = None
+
+
+def script_execution_set(reason: str) -> None:
+ """Set stop reason."""
+ data = script_execution_cv.get()
+ if data is None:
+ return
+ data.script_execution = reason
+
+
+def script_execution_get() -> str | None:
+ """Return the current trace."""
+ data = script_execution_cv.get()
+ if data is None:
+ return None
+ return data.script_execution
+
+
+@contextmanager
+def trace_path(suffix: str | list[str]) -> Generator:
+ """Go deeper in the config tree.
+
+ Can not be used as a decorator on couroutine functions.
+ """
+ count = trace_path_push(suffix)
+ try:
+ yield
+ finally:
+ trace_path_pop(count)
+
+
+def async_trace_path(suffix: str | list[str]) -> Callable:
+ """Go deeper in the config tree.
+
+ To be used as a decorator on coroutine functions.
+ """
+
+ def _trace_path_decorator(func: Callable) -> Callable:
+ """Decorate a coroutine function."""
+
+ @wraps(func)
+ async def async_wrapper(*args: Any) -> None:
+ """Catch and log exception."""
+ with trace_path(suffix):
+ await func(*args)
+
+ return async_wrapper
+
+ return _trace_path_decorator
diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py
index bd229d79111911..ed9049a8a13ef1 100644
--- a/homeassistant/helpers/translation.py
+++ b/homeassistant/helpers/translation.py
@@ -1,10 +1,12 @@
"""Translation string lookup helpers."""
+from __future__ import annotations
+
import asyncio
from collections import ChainMap
import logging
-from typing import Any, Dict, List, Optional, Set
+from typing import Any
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
from homeassistant.loader import (
MAX_LOAD_CONCURRENTLY,
Integration,
@@ -15,8 +17,6 @@
from homeassistant.util.async_ import gather_with_concurrency
from homeassistant.util.json import load_json
-from .typing import HomeAssistantType
-
_LOGGER = logging.getLogger(__name__)
TRANSLATION_LOAD_LOCK = "translation_load_lock"
@@ -24,7 +24,7 @@
LOCALE_EN = "en"
-def recursive_flatten(prefix: Any, data: Dict) -> Dict[str, Any]:
+def recursive_flatten(prefix: Any, data: dict) -> dict[str, Any]:
"""Return a flattened representation of dict data."""
output = {}
for key, value in data.items():
@@ -38,7 +38,7 @@ def recursive_flatten(prefix: Any, data: Dict) -> Dict[str, Any]:
@callback
def component_translation_path(
component: str, language: str, integration: Integration
-) -> Optional[str]:
+) -> str | None:
"""Return the translation json file location for a component.
For component:
@@ -69,8 +69,8 @@ def component_translation_path(
def load_translations_files(
- translation_files: Dict[str, str]
-) -> Dict[str, Dict[str, Any]]:
+ translation_files: dict[str, str]
+) -> dict[str, dict[str, Any]]:
"""Load and parse translation.json files."""
loaded = {}
for component, translation_file in translation_files.items():
@@ -90,13 +90,13 @@ def load_translations_files(
def _merge_resources(
- translation_strings: Dict[str, Dict[str, Any]],
- components: Set[str],
+ translation_strings: dict[str, dict[str, Any]],
+ components: set[str],
category: str,
-) -> Dict[str, Dict[str, Any]]:
+) -> dict[str, dict[str, Any]]:
"""Build and merge the resources response for the given components and platforms."""
# Build response
- resources: Dict[str, Dict[str, Any]] = {}
+ resources: dict[str, dict[str, Any]] = {}
for component in components:
if "." not in component:
domain = component
@@ -131,10 +131,10 @@ def _merge_resources(
def _build_resources(
- translation_strings: Dict[str, Dict[str, Any]],
- components: Set[str],
+ translation_strings: dict[str, dict[str, Any]],
+ components: set[str],
category: str,
-) -> Dict[str, Dict[str, Any]]:
+) -> dict[str, dict[str, Any]]:
"""Build the resources response for the given components."""
# Build response
return {
@@ -146,8 +146,8 @@ def _build_resources(
async def async_get_component_strings(
- hass: HomeAssistantType, language: str, components: Set[str]
-) -> Dict[str, Any]:
+ hass: HomeAssistant, language: str, components: set[str]
+) -> dict[str, Any]:
"""Load translations."""
domains = list({loaded.split(".")[-1] for loaded in components})
integrations = dict(
@@ -160,7 +160,7 @@ async def async_get_component_strings(
)
)
- translations: Dict[str, Any] = {}
+ translations: dict[str, Any] = {}
# Determine paths of missing components/platforms
files_to_load = {}
@@ -202,18 +202,18 @@ async def async_get_component_strings(
class _TranslationCache:
"""Cache for flattened translations."""
- def __init__(self, hass: HomeAssistantType) -> None:
+ def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the cache."""
self.hass = hass
- self.loaded: Dict[str, Set[str]] = {}
- self.cache: Dict[str, Dict[str, Dict[str, Any]]] = {}
+ self.loaded: dict[str, set[str]] = {}
+ self.cache: dict[str, dict[str, dict[str, Any]]] = {}
async def async_fetch(
self,
language: str,
category: str,
- components: Set,
- ) -> List[Dict[str, Dict[str, Any]]]:
+ components: set,
+ ) -> list[dict[str, dict[str, Any]]]:
"""Load resources into the cache."""
components_to_load = components - self.loaded.setdefault(language, set())
@@ -224,7 +224,7 @@ async def async_fetch(
return [cached.get(component, {}).get(category, {}) for component in components]
- async def _async_load(self, language: str, components: Set) -> None:
+ async def _async_load(self, language: str, components: set) -> None:
"""Populate the cache for a given set of components."""
_LOGGER.debug(
"Cache miss for %s: %s",
@@ -247,12 +247,12 @@ async def _async_load(self, language: str, components: Set) -> None:
def _build_category_cache(
self,
language: str,
- components: Set,
- translation_strings: Dict[str, Dict[str, Any]],
+ components: set,
+ translation_strings: dict[str, dict[str, Any]],
) -> None:
"""Extract resources into the cache."""
cached = self.cache.setdefault(language, {})
- categories: Set[str] = set()
+ categories: set[str] = set()
for resource in translation_strings.values():
categories.update(resource)
@@ -263,7 +263,7 @@ def _build_category_cache(
new_resources = resource_func(translation_strings, components, category)
for component, resource in new_resources.items():
- category_cache: Dict[str, Any] = cached.setdefault(
+ category_cache: dict[str, Any] = cached.setdefault(
component, {}
).setdefault(category, {})
@@ -280,12 +280,12 @@ def _build_category_cache(
@bind_hass
async def async_get_translations(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
language: str,
category: str,
- integration: Optional[str] = None,
- config_flow: Optional[bool] = None,
-) -> Dict[str, Any]:
+ integration: str | None = None,
+ config_flow: bool | None = None,
+) -> dict[str, Any]:
"""Return all backend translations.
If integration specified, load it for that one.
diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py
index 2c7275a9cc3e2e..045c56d964c876 100644
--- a/homeassistant/helpers/trigger.py
+++ b/homeassistant/helpers/trigger.py
@@ -1,14 +1,17 @@
"""Triggers."""
+from __future__ import annotations
+
import asyncio
import logging
from types import MappingProxyType
-from typing import Any, Callable, Dict, List, Optional, Union
+from typing import Any, Callable
import voluptuous as vol
from homeassistant.const import CONF_PLATFORM
-from homeassistant.core import CALLBACK_TYPE, callback
-from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import IntegrationNotFound, async_get_integration
_PLATFORM_ALIASES = {
@@ -17,9 +20,7 @@
}
-async def _async_get_trigger_platform(
- hass: HomeAssistantType, config: ConfigType
-) -> Any:
+async def _async_get_trigger_platform(hass: HomeAssistant, config: ConfigType) -> Any:
platform = config[CONF_PLATFORM]
for alias, triggers in _PLATFORM_ALIASES.items():
if platform in triggers:
@@ -38,8 +39,8 @@ async def _async_get_trigger_platform(
async def async_validate_trigger_config(
- hass: HomeAssistantType, trigger_config: List[ConfigType]
-) -> List[ConfigType]:
+ hass: HomeAssistant, trigger_config: list[ConfigType]
+) -> list[ConfigType]:
"""Validate triggers."""
config = []
for conf in trigger_config:
@@ -53,15 +54,15 @@ async def async_validate_trigger_config(
async def async_initialize_triggers(
- hass: HomeAssistantType,
- trigger_config: List[ConfigType],
+ hass: HomeAssistant,
+ trigger_config: list[ConfigType],
action: Callable,
domain: str,
name: str,
log_cb: Callable,
home_assistant_start: bool = False,
- variables: Optional[Union[Dict[str, Any], MappingProxyType]] = None,
-) -> Optional[CALLBACK_TYPE]:
+ variables: dict[str, Any] | MappingProxyType | None = None,
+) -> CALLBACK_TYPE | None:
"""Initialize triggers."""
info = {
"domain": domain,
@@ -71,15 +72,18 @@ async def async_initialize_triggers(
}
triggers = []
- for conf in trigger_config:
+ for idx, conf in enumerate(trigger_config):
platform = await _async_get_trigger_platform(hass, conf)
+ info = {**info, "trigger_id": f"{idx}"}
triggers.append(platform.async_attach_trigger(hass, conf, action, info))
attach_results = await asyncio.gather(*triggers, return_exceptions=True)
removes = []
for result in attach_results:
- if isinstance(result, Exception):
+ if isinstance(result, HomeAssistantError):
+ log_cb(logging.ERROR, f"Got error '{result}' when setting up triggers for")
+ elif isinstance(result, Exception):
log_cb(logging.ERROR, "Error setting up trigger", exc_info=result)
elif result is None:
log_cb(
diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py
index 8df2c57b1e7f74..53e92c433a944e 100644
--- a/homeassistant/helpers/update_coordinator.py
+++ b/homeassistant/helpers/update_coordinator.py
@@ -1,9 +1,11 @@
"""Helpers to help coordinate updates."""
+from __future__ import annotations
+
import asyncio
from datetime import datetime, timedelta
import logging
from time import monotonic
-from typing import Awaitable, Callable, Generic, List, Optional, TypeVar
+from typing import Any, Awaitable, Callable, Generic, TypeVar
import urllib.error
import aiohttp
@@ -11,6 +13,7 @@
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback
+from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity, event
from homeassistant.util.dt import utcnow
@@ -21,6 +24,8 @@
T = TypeVar("T")
+# mypy: disallow-any-generics
+
class UpdateFailed(Exception):
"""Raised when an update has failed."""
@@ -35,9 +40,9 @@ def __init__(
logger: logging.Logger,
*,
name: str,
- update_interval: Optional[timedelta] = None,
- update_method: Optional[Callable[[], Awaitable[T]]] = None,
- request_refresh_debouncer: Optional[Debouncer] = None,
+ update_interval: timedelta | None = None,
+ update_method: Callable[[], Awaitable[T]] | None = None,
+ request_refresh_debouncer: Debouncer | None = None,
):
"""Initialize global data updater."""
self.hass = hass
@@ -46,13 +51,14 @@ def __init__(
self.update_method = update_method
self.update_interval = update_interval
- self.data: Optional[T] = None
+ self.data: T | None = None
- self._listeners: List[CALLBACK_TYPE] = []
+ self._listeners: list[CALLBACK_TYPE] = []
self._job = HassJob(self._handle_refresh_interval)
- self._unsub_refresh: Optional[CALLBACK_TYPE] = None
- self._request_refresh_task: Optional[asyncio.TimerHandle] = None
+ self._unsub_refresh: CALLBACK_TYPE | None = None
+ self._request_refresh_task: asyncio.TimerHandle | None = None
self.last_update_success = True
+ self.last_exception: Exception | None = None
if request_refresh_debouncer is None:
request_refresh_debouncer = Debouncer(
@@ -130,13 +136,31 @@ async def async_request_refresh(self) -> None:
"""
await self._debounced_refresh.async_call()
- async def _async_update_data(self) -> Optional[T]:
+ async def _async_update_data(self) -> T | None:
"""Fetch the latest data from the source."""
if self.update_method is None:
raise NotImplementedError("Update method not implemented")
return await self.update_method()
+ async def async_config_entry_first_refresh(self) -> None:
+ """Refresh data for the first time when a config entry is setup.
+
+ Will automatically raise ConfigEntryNotReady if the refresh
+ fails. Additionally logging is handled by config entry setup
+ to ensure that multiple retries do not cause log spam.
+ """
+ await self._async_refresh(log_failures=False)
+ if self.last_update_success:
+ return
+ ex = ConfigEntryNotReady()
+ ex.__cause__ = self.last_exception
+ raise ex
+
async def async_refresh(self) -> None:
+ """Refresh data and log errors."""
+ await self._async_refresh(log_failures=True)
+
+ async def _async_refresh(self, log_failures: bool = True) -> None:
"""Refresh data."""
if self._unsub_refresh:
self._unsub_refresh()
@@ -148,37 +172,50 @@ async def async_refresh(self) -> None:
try:
self.data = await self._async_update_data()
- except (asyncio.TimeoutError, requests.exceptions.Timeout):
+ except (asyncio.TimeoutError, requests.exceptions.Timeout) as err:
+ self.last_exception = err
if self.last_update_success:
- self.logger.error("Timeout fetching %s data", self.name)
+ if log_failures:
+ self.logger.error("Timeout fetching %s data", self.name)
self.last_update_success = False
except (aiohttp.ClientError, requests.exceptions.RequestException) as err:
+ self.last_exception = err
if self.last_update_success:
- self.logger.error("Error requesting %s data: %s", self.name, err)
+ if log_failures:
+ self.logger.error("Error requesting %s data: %s", self.name, err)
self.last_update_success = False
except urllib.error.URLError as err:
+ self.last_exception = err
if self.last_update_success:
- if err.reason == "timed out":
- self.logger.error("Timeout fetching %s data", self.name)
- else:
- self.logger.error("Error requesting %s data: %s", self.name, err)
+ if log_failures:
+ if err.reason == "timed out":
+ self.logger.error("Timeout fetching %s data", self.name)
+ else:
+ self.logger.error(
+ "Error requesting %s data: %s", self.name, err
+ )
self.last_update_success = False
except UpdateFailed as err:
+ self.last_exception = err
if self.last_update_success:
- self.logger.error("Error fetching %s data: %s", self.name, err)
+ if log_failures:
+ self.logger.error("Error fetching %s data: %s", self.name, err)
self.last_update_success = False
except NotImplementedError as err:
+ self.last_exception = err
raise err
except Exception as err: # pylint: disable=broad-except
+ self.last_exception = err
self.last_update_success = False
- self.logger.exception(
- "Unexpected error fetching %s data: %s", self.name, err
- )
+ if log_failures:
+ self.logger.exception(
+ "Unexpected error fetching %s data: %s", self.name, err
+ )
else:
if not self.last_update_success:
@@ -231,7 +268,7 @@ def _async_stop_refresh(self, _: Event) -> None:
class CoordinatorEntity(entity.Entity):
"""A class for entities using DataUpdateCoordinator."""
- def __init__(self, coordinator: DataUpdateCoordinator) -> None:
+ def __init__(self, coordinator: DataUpdateCoordinator[Any]) -> None:
"""Create the entity with a DataUpdateCoordinator."""
self.coordinator = coordinator
@@ -262,7 +299,6 @@ async def async_update(self) -> None:
Only used by the generic entity update service.
"""
-
# Ignore manual update requests if the entity is disabled
if not self.enabled:
return
diff --git a/homeassistant/loader.py b/homeassistant/loader.py
index 215f552a90837a..44823720ea5f11 100644
--- a/homeassistant/loader.py
+++ b/homeassistant/loader.py
@@ -4,7 +4,10 @@
This module has quite some complex parts. I have tried to add as much
documentation as possible to keep it understandable.
"""
+from __future__ import annotations
+
import asyncio
+from contextlib import suppress
import functools as ft
import importlib
import json
@@ -12,19 +15,9 @@
import pathlib
import sys
from types import ModuleType
-from typing import (
- TYPE_CHECKING,
- Any,
- Callable,
- Dict,
- List,
- Optional,
- Set,
- TypedDict,
- TypeVar,
- Union,
- cast,
-)
+from typing import TYPE_CHECKING, Any, Callable, Dict, TypedDict, TypeVar, cast
+
+from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
from homeassistant.generated.dhcp import DHCP
from homeassistant.generated.mqtt import MQTT
@@ -49,10 +42,21 @@
PACKAGE_CUSTOM_COMPONENTS = "custom_components"
PACKAGE_BUILTIN = "homeassistant.components"
CUSTOM_WARNING = (
- "You are using a custom integration for %s which has not "
+ "You are using a custom integration %s which has not "
"been tested by Home Assistant. This component might "
"cause stability problems, be sure to disable it if you "
- "experience issues with Home Assistant."
+ "experience issues with Home Assistant"
+)
+CUSTOM_WARNING_VERSION_MISSING = (
+ "No 'version' key in the manifest file for "
+ "custom integration '%s'. As of Home Assistant "
+ "2021.6, this integration will no longer be "
+ "loaded. Please report this to the maintainer of '%s'"
+)
+CUSTOM_WARNING_VERSION_TYPE = (
+ "'%s' is not a valid version for "
+ "custom integration '%s'. "
+ "Please report this to the maintainer of '%s'"
)
_UNDEF = object() # Internal; not helpers.typing.UNDEFINED due to circular dependency
@@ -70,20 +74,21 @@ class Manifest(TypedDict, total=False):
name: str
disabled: str
domain: str
- dependencies: List[str]
- after_dependencies: List[str]
- requirements: List[str]
+ dependencies: list[str]
+ after_dependencies: list[str]
+ requirements: list[str]
config_flow: bool
documentation: str
issue_tracker: str
quality_scale: str
- mqtt: List[str]
- ssdp: List[Dict[str, str]]
- zeroconf: List[Union[str, Dict[str, str]]]
- dhcp: List[Dict[str, str]]
- homekit: Dict[str, List[str]]
+ mqtt: list[str]
+ ssdp: list[dict[str, str]]
+ zeroconf: list[str | dict[str, str]]
+ dhcp: list[dict[str, str]]
+ homekit: dict[str, list[str]]
is_built_in: bool
- codeowners: List[str]
+ version: str
+ codeowners: list[str]
def manifest_from_legacy_module(domain: str, module: ModuleType) -> Manifest:
@@ -98,8 +103,8 @@ def manifest_from_legacy_module(domain: str, module: ModuleType) -> Manifest:
async def _async_get_custom_components(
- hass: "HomeAssistant",
-) -> Dict[str, "Integration"]:
+ hass: HomeAssistant,
+) -> dict[str, Integration]:
"""Return list of custom integrations."""
if hass.config.safe_mode:
return {}
@@ -109,7 +114,7 @@ async def _async_get_custom_components(
except ImportError:
return {}
- def get_sub_directories(paths: List[str]) -> List[pathlib.Path]:
+ def get_sub_directories(paths: list[str]) -> list[pathlib.Path]:
"""Return all sub directories in a set of paths."""
return [
entry
@@ -139,8 +144,8 @@ def get_sub_directories(paths: List[str]) -> List[pathlib.Path]:
async def async_get_custom_components(
- hass: "HomeAssistant",
-) -> Dict[str, "Integration"]:
+ hass: HomeAssistant,
+) -> dict[str, Integration]:
"""Return cached list of custom integrations."""
reg_or_evt = hass.data.get(DATA_CUSTOM_COMPONENTS)
@@ -160,12 +165,12 @@ async def async_get_custom_components(
return cast(Dict[str, "Integration"], reg_or_evt)
-async def async_get_config_flows(hass: "HomeAssistant") -> Set[str]:
+async def async_get_config_flows(hass: HomeAssistant) -> set[str]:
"""Return cached list of config flows."""
# pylint: disable=import-outside-toplevel
from homeassistant.generated.config_flows import FLOWS
- flows: Set[str] = set()
+ flows: set[str] = set()
flows.update(FLOWS)
integrations = await async_get_custom_components(hass)
@@ -180,9 +185,9 @@ async def async_get_config_flows(hass: "HomeAssistant") -> Set[str]:
return flows
-async def async_get_zeroconf(hass: "HomeAssistant") -> Dict[str, List[Dict[str, str]]]:
+async def async_get_zeroconf(hass: HomeAssistant) -> dict[str, list[dict[str, str]]]:
"""Return cached list of zeroconf types."""
- zeroconf: Dict[str, List[Dict[str, str]]] = ZEROCONF.copy()
+ zeroconf: dict[str, list[dict[str, str]]] = ZEROCONF.copy()
integrations = await async_get_custom_components(hass)
for integration in integrations.values():
@@ -203,9 +208,9 @@ async def async_get_zeroconf(hass: "HomeAssistant") -> Dict[str, List[Dict[str,
return zeroconf
-async def async_get_dhcp(hass: "HomeAssistant") -> List[Dict[str, str]]:
+async def async_get_dhcp(hass: HomeAssistant) -> list[dict[str, str]]:
"""Return cached list of dhcp types."""
- dhcp: List[Dict[str, str]] = DHCP.copy()
+ dhcp: list[dict[str, str]] = DHCP.copy()
integrations = await async_get_custom_components(hass)
for integration in integrations.values():
@@ -217,10 +222,10 @@ async def async_get_dhcp(hass: "HomeAssistant") -> List[Dict[str, str]]:
return dhcp
-async def async_get_homekit(hass: "HomeAssistant") -> Dict[str, str]:
+async def async_get_homekit(hass: HomeAssistant) -> dict[str, str]:
"""Return cached list of homekit models."""
- homekit: Dict[str, str] = HOMEKIT.copy()
+ homekit: dict[str, str] = HOMEKIT.copy()
integrations = await async_get_custom_components(hass)
for integration in integrations.values():
@@ -236,10 +241,10 @@ async def async_get_homekit(hass: "HomeAssistant") -> Dict[str, str]:
return homekit
-async def async_get_ssdp(hass: "HomeAssistant") -> Dict[str, List[Dict[str, str]]]:
+async def async_get_ssdp(hass: HomeAssistant) -> dict[str, list[dict[str, str]]]:
"""Return cached list of ssdp mappings."""
- ssdp: Dict[str, List[Dict[str, str]]] = SSDP.copy()
+ ssdp: dict[str, list[dict[str, str]]] = SSDP.copy()
integrations = await async_get_custom_components(hass)
for integration in integrations.values():
@@ -251,10 +256,10 @@ async def async_get_ssdp(hass: "HomeAssistant") -> Dict[str, List[Dict[str, str]
return ssdp
-async def async_get_mqtt(hass: "HomeAssistant") -> Dict[str, List[str]]:
+async def async_get_mqtt(hass: HomeAssistant) -> dict[str, list[str]]:
"""Return cached list of MQTT mappings."""
- mqtt: Dict[str, List[str]] = MQTT.copy()
+ mqtt: dict[str, list[str]] = MQTT.copy()
integrations = await async_get_custom_components(hass)
for integration in integrations.values():
@@ -271,8 +276,8 @@ class Integration:
@classmethod
def resolve_from_root(
- cls, hass: "HomeAssistant", root_module: ModuleType, domain: str
- ) -> "Optional[Integration]":
+ cls, hass: HomeAssistant, root_module: ModuleType, domain: str
+ ) -> Integration | None:
"""Resolve an integration from a root module."""
for base in root_module.__path__: # type: ignore
manifest_path = pathlib.Path(base) / domain / "manifest.json"
@@ -295,9 +300,7 @@ def resolve_from_root(
return None
@classmethod
- def resolve_legacy(
- cls, hass: "HomeAssistant", domain: str
- ) -> "Optional[Integration]":
+ def resolve_legacy(cls, hass: HomeAssistant, domain: str) -> Integration | None:
"""Resolve legacy component.
Will create a stub manifest.
@@ -316,7 +319,7 @@ def resolve_legacy(
def __init__(
self,
- hass: "HomeAssistant",
+ hass: HomeAssistant,
pkg_path: str,
file_path: pathlib.Path,
manifest: Manifest,
@@ -329,8 +332,8 @@ def __init__(
manifest["is_built_in"] = self.is_built_in
if self.dependencies:
- self._all_dependencies_resolved: Optional[bool] = None
- self._all_dependencies: Optional[Set[str]] = None
+ self._all_dependencies_resolved: bool | None = None
+ self._all_dependencies: set[str] | None = None
else:
self._all_dependencies_resolved = True
self._all_dependencies = set()
@@ -343,7 +346,7 @@ def name(self) -> str:
return self.manifest["name"]
@property
- def disabled(self) -> Optional[str]:
+ def disabled(self) -> str | None:
"""Return reason integration is disabled."""
return self.manifest.get("disabled")
@@ -353,17 +356,17 @@ def domain(self) -> str:
return self.manifest["domain"]
@property
- def dependencies(self) -> List[str]:
+ def dependencies(self) -> list[str]:
"""Return dependencies."""
return self.manifest.get("dependencies", [])
@property
- def after_dependencies(self) -> List[str]:
+ def after_dependencies(self) -> list[str]:
"""Return after_dependencies."""
return self.manifest.get("after_dependencies", [])
@property
- def requirements(self) -> List[str]:
+ def requirements(self) -> list[str]:
"""Return requirements."""
return self.manifest.get("requirements", [])
@@ -373,42 +376,42 @@ def config_flow(self) -> bool:
return self.manifest.get("config_flow") or False
@property
- def documentation(self) -> Optional[str]:
+ def documentation(self) -> str | None:
"""Return documentation."""
return self.manifest.get("documentation")
@property
- def issue_tracker(self) -> Optional[str]:
+ def issue_tracker(self) -> str | None:
"""Return issue tracker link."""
return self.manifest.get("issue_tracker")
@property
- def quality_scale(self) -> Optional[str]:
+ def quality_scale(self) -> str | None:
"""Return Integration Quality Scale."""
return self.manifest.get("quality_scale")
@property
- def mqtt(self) -> Optional[List[str]]:
+ def mqtt(self) -> list[str] | None:
"""Return Integration MQTT entries."""
return self.manifest.get("mqtt")
@property
- def ssdp(self) -> Optional[List[Dict[str, str]]]:
+ def ssdp(self) -> list[dict[str, str]] | None:
"""Return Integration SSDP entries."""
return self.manifest.get("ssdp")
@property
- def zeroconf(self) -> Optional[List[Union[str, Dict[str, str]]]]:
+ def zeroconf(self) -> list[str | dict[str, str]] | None:
"""Return Integration zeroconf entries."""
return self.manifest.get("zeroconf")
@property
- def dhcp(self) -> Optional[List[Dict[str, str]]]:
+ def dhcp(self) -> list[dict[str, str]] | None:
"""Return Integration dhcp entries."""
return self.manifest.get("dhcp")
@property
- def homekit(self) -> Optional[Dict[str, List[str]]]:
+ def homekit(self) -> dict[str, list[str]] | None:
"""Return Integration homekit entries."""
return self.manifest.get("homekit")
@@ -418,7 +421,14 @@ def is_built_in(self) -> bool:
return self.pkg_path.startswith(PACKAGE_BUILTIN)
@property
- def all_dependencies(self) -> Set[str]:
+ def version(self) -> AwesomeVersion | None:
+ """Return the version of the integration."""
+ if "version" not in self.manifest:
+ return None
+ return AwesomeVersion(self.manifest["version"])
+
+ @property
+ def all_dependencies(self) -> set[str]:
"""Return all dependencies including sub-dependencies."""
if self._all_dependencies is None:
raise RuntimeError("Dependencies not resolved!")
@@ -484,7 +494,7 @@ def __repr__(self) -> str:
return f""
-async def async_get_integration(hass: "HomeAssistant", domain: str) -> Integration:
+async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration:
"""Get an integration."""
cache = hass.data.get(DATA_INTEGRATIONS)
if cache is None:
@@ -492,7 +502,7 @@ async def async_get_integration(hass: "HomeAssistant", domain: str) -> Integrati
raise IntegrationNotFound(domain)
cache = hass.data[DATA_INTEGRATIONS] = {}
- int_or_evt: Union[Integration, asyncio.Event, None] = cache.get(domain, _UNDEF)
+ int_or_evt: Integration | asyncio.Event | None = cache.get(domain, _UNDEF)
if isinstance(int_or_evt, asyncio.Event):
await int_or_evt.wait()
@@ -513,7 +523,7 @@ async def async_get_integration(hass: "HomeAssistant", domain: str) -> Integrati
# components to find the integration.
integration = (await async_get_custom_components(hass)).get(domain)
if integration is not None:
- _LOGGER.warning(CUSTOM_WARNING, domain)
+ custom_integration_warning(integration)
cache[domain] = integration
event.set()
return integration
@@ -531,6 +541,7 @@ async def async_get_integration(hass: "HomeAssistant", domain: str) -> Integrati
integration = Integration.resolve_legacy(hass, domain)
if integration is not None:
+ custom_integration_warning(integration)
cache[domain] = integration
else:
# Remove event from cache.
@@ -568,18 +579,16 @@ def __init__(self, from_domain: str, to_domain: str) -> None:
def _load_file(
- hass: "HomeAssistant", comp_or_platform: str, base_paths: List[str]
-) -> Optional[ModuleType]:
+ hass: HomeAssistant, comp_or_platform: str, base_paths: list[str]
+) -> ModuleType | None:
"""Try to load specified file.
Looks in config dir first, then built-in components.
Only returns it if also found to be valid.
Async friendly.
"""
- try:
+ with suppress(KeyError):
return hass.data[DATA_COMPONENTS][comp_or_platform] # type: ignore
- except KeyError:
- pass
cache = hass.data.get(DATA_COMPONENTS)
if cache is None:
@@ -605,9 +614,6 @@ def _load_file(
cache[comp_or_platform] = module
- if module.__name__.startswith(PACKAGE_CUSTOM_COMPONENTS):
- _LOGGER.warning(CUSTOM_WARNING, comp_or_platform)
-
return module
except ImportError as err:
@@ -632,7 +638,7 @@ def _load_file(
class ModuleWrapper:
"""Class to wrap a Python module and auto fill in hass argument."""
- def __init__(self, hass: "HomeAssistant", module: ModuleType) -> None:
+ def __init__(self, hass: HomeAssistant, module: ModuleType) -> None:
"""Initialize the module wrapper."""
self._hass = hass
self._module = module
@@ -651,7 +657,7 @@ def __getattr__(self, attr: str) -> Any:
class Components:
"""Helper to load components."""
- def __init__(self, hass: "HomeAssistant") -> None:
+ def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the Components class."""
self._hass = hass
@@ -661,7 +667,7 @@ def __getattr__(self, comp_name: str) -> ModuleWrapper:
integration = self._hass.data.get(DATA_INTEGRATIONS, {}).get(comp_name)
if isinstance(integration, Integration):
- component: Optional[ModuleType] = integration.get_component()
+ component: ModuleType | None = integration.get_component()
else:
# Fallback to importing old-school
component = _load_file(self._hass, comp_name, _lookup_path(self._hass))
@@ -677,7 +683,7 @@ def __getattr__(self, comp_name: str) -> ModuleWrapper:
class Helpers:
"""Helper to load helpers."""
- def __init__(self, hass: "HomeAssistant") -> None:
+ def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the Helpers class."""
self._hass = hass
@@ -696,12 +702,12 @@ def bind_hass(func: CALLABLE_T) -> CALLABLE_T:
async def _async_component_dependencies(
- hass: "HomeAssistant",
+ hass: HomeAssistant,
start_domain: str,
integration: Integration,
- loaded: Set[str],
- loading: Set[str],
-) -> Set[str]:
+ loaded: set[str],
+ loading: set[str],
+) -> set[str]:
"""Recursive function to get component dependencies.
Async friendly.
@@ -738,7 +744,7 @@ async def _async_component_dependencies(
return loaded
-def _async_mount_config_dir(hass: "HomeAssistant") -> bool:
+def _async_mount_config_dir(hass: HomeAssistant) -> bool:
"""Mount config dir in order to load custom_component.
Async friendly but not a coroutine.
@@ -751,8 +757,40 @@ def _async_mount_config_dir(hass: "HomeAssistant") -> bool:
return True
-def _lookup_path(hass: "HomeAssistant") -> List[str]:
+def _lookup_path(hass: HomeAssistant) -> list[str]:
"""Return the lookup paths for legacy lookups."""
if hass.config.safe_mode:
return [PACKAGE_BUILTIN]
return [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN]
+
+
+def validate_custom_integration_version(version: str) -> bool:
+ """Validate the version of custom integrations."""
+ return AwesomeVersion(version).strategy in (
+ AwesomeVersionStrategy.CALVER,
+ AwesomeVersionStrategy.SEMVER,
+ AwesomeVersionStrategy.SIMPLEVER,
+ AwesomeVersionStrategy.BUILDVER,
+ AwesomeVersionStrategy.PEP440,
+ )
+
+
+def custom_integration_warning(integration: Integration) -> None:
+ """Create logs for custom integrations."""
+ if not integration.pkg_path.startswith(PACKAGE_CUSTOM_COMPONENTS):
+ return None
+
+ _LOGGER.warning(CUSTOM_WARNING, integration.domain)
+
+ if integration.manifest.get("version") is None:
+ _LOGGER.warning(
+ CUSTOM_WARNING_VERSION_MISSING, integration.domain, integration.domain
+ )
+ else:
+ if not validate_custom_integration_version(integration.manifest["version"]):
+ _LOGGER.warning(
+ CUSTOM_WARNING_VERSION_TYPE,
+ integration.manifest["version"],
+ integration.domain,
+ integration.domain,
+ )
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 0845cebc663460..2a9df6eebe4f58 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -1,36 +1,39 @@
PyJWT==1.7.1
PyNaCl==1.3.0
-aiohttp==3.7.3
+aiodiscover==1.3.3
+aiohttp==3.7.4.post0
aiohttp_cors==0.7.0
-astral==1.10.1
+astral==2.2
+async-upnp-client==0.16.0
async_timeout==3.0.1
-attrs==19.3.0
+attrs==20.3.0
+awesomeversion==21.2.3
bcrypt==3.1.7
certifi>=2020.12.5
ciso8601==2.1.3
-cryptography==3.3.1
+cryptography==3.3.2
defusedxml==0.6.0
distro==1.5.0
-emoji==0.5.4
-hass-nabucasa==0.41.0
-home-assistant-frontend==20210127.5
-httpx==0.16.1
-jinja2>=2.11.2
+emoji==1.2.0
+hass-nabucasa==0.42.0
+home-assistant-frontend==20210407.2
+httpx==0.17.1
+jinja2>=2.11.3
netdisco==2.8.2
paho-mqtt==1.5.1
-pillow==8.1.0
+pillow==8.1.2
pip>=8.0.3,<20.3
python-slugify==4.0.1
-pytz>=2020.5
+pytz>=2021.1
pyyaml==5.4.1
requests==2.25.1
ruamel.yaml==0.15.100
scapy==2.4.4
-sqlalchemy==1.3.22
+sqlalchemy==1.3.23
voluptuous-serialize==2.4.0
voluptuous==0.12.1
yarl==1.6.3
-zeroconf==0.28.8
+zeroconf==0.29.0
pycryptodome>=3.6.6
@@ -44,8 +47,9 @@ h11>=0.12.0
# https://github.com/encode/httpcore/issues/239
httpcore>=0.12.3
-# Constrain httplib2 to protect against CVE-2020-11078
-httplib2>=0.18.0
+# Constrain httplib2 to protect against GHSA-93xj-8mrv-444m
+# https://github.com/advisories/GHSA-93xj-8mrv-444m
+httplib2>=0.19.0
# gRPC 1.32+ currently causes issues on ARMv7, see:
# https://github.com/home-assistant/core/issues/40148
diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py
index 6ea2074ddf4259..aaad5c1f25143d 100644
--- a/homeassistant/requirements.py
+++ b/homeassistant/requirements.py
@@ -1,7 +1,9 @@
"""Module to handle installing requirements."""
+from __future__ import annotations
+
import asyncio
import os
-from typing import Any, Dict, Iterable, List, Optional, Set, Union, cast
+from typing import Any, Iterable, cast
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -15,7 +17,7 @@
DATA_PKG_CACHE = "pkg_cache"
DATA_INTEGRATIONS_WITH_REQS = "integrations_with_reqs"
CONSTRAINT_FILE = "package_constraints.txt"
-DISCOVERY_INTEGRATIONS: Dict[str, Iterable[str]] = {
+DISCOVERY_INTEGRATIONS: dict[str, Iterable[str]] = {
"dhcp": ("dhcp",),
"mqtt": ("mqtt",),
"ssdp": ("ssdp",),
@@ -26,7 +28,7 @@
class RequirementsNotFound(HomeAssistantError):
"""Raised when a component is not found."""
- def __init__(self, domain: str, requirements: List[str]) -> None:
+ def __init__(self, domain: str, requirements: list[str]) -> None:
"""Initialize a component not found error."""
super().__init__(f"Requirements for {domain} not found: {requirements}.")
self.domain = domain
@@ -34,7 +36,7 @@ def __init__(self, domain: str, requirements: List[str]) -> None:
async def async_get_integration_with_requirements(
- hass: HomeAssistant, domain: str, done: Optional[Set[str]] = None
+ hass: HomeAssistant, domain: str, done: set[str] | None = None
) -> Integration:
"""Get an integration with all requirements installed, including the dependencies.
@@ -56,7 +58,7 @@ async def async_get_integration_with_requirements(
if cache is None:
cache = hass.data[DATA_INTEGRATIONS_WITH_REQS] = {}
- int_or_evt: Union[Integration, asyncio.Event, None, UndefinedType] = cache.get(
+ int_or_evt: Integration | asyncio.Event | None | UndefinedType = cache.get(
domain, UNDEFINED
)
@@ -95,12 +97,21 @@ async def async_get_integration_with_requirements(
deps_to_check.append(check_domain)
if deps_to_check:
- await asyncio.gather(
+ results = await asyncio.gather(
*[
async_get_integration_with_requirements(hass, dep, done)
for dep in deps_to_check
- ]
+ ],
+ return_exceptions=True,
)
+ for result in results:
+ if not isinstance(result, BaseException):
+ continue
+ if not isinstance(result, IntegrationNotFound) or not (
+ not integration.is_built_in
+ and result.domain in integration.after_dependencies
+ ):
+ raise result
cache[domain] = integration
event.set()
@@ -108,7 +119,7 @@ async def async_get_integration_with_requirements(
async def async_process_requirements(
- hass: HomeAssistant, name: str, requirements: List[str]
+ hass: HomeAssistant, name: str, requirements: list[str]
) -> None:
"""Install the requirements for a component or platform.
@@ -126,7 +137,7 @@ async def async_process_requirements(
if pkg_util.is_installed(req):
continue
- def _install(req: str, kwargs: Dict[str, Any]) -> bool:
+ def _install(req: str, kwargs: dict[str, Any]) -> bool:
"""Install requirement."""
return pkg_util.install_package(req, **kwargs)
@@ -136,7 +147,7 @@ def _install(req: str, kwargs: Dict[str, Any]) -> bool:
raise RequirementsNotFound(name, [req])
-def pip_kwargs(config_dir: Optional[str]) -> Dict[str, Any]:
+def pip_kwargs(config_dir: str | None) -> dict[str, Any]:
"""Return keyword arguments for PIP install."""
is_docker = pkg_util.is_docker_env()
kwargs = {
diff --git a/homeassistant/runner.py b/homeassistant/runner.py
index 6f13a47be8164a..5adddb5f6efdd3 100644
--- a/homeassistant/runner.py
+++ b/homeassistant/runner.py
@@ -1,9 +1,11 @@
"""Run Home Assistant."""
+from __future__ import annotations
+
import asyncio
from concurrent.futures import ThreadPoolExecutor
import dataclasses
import logging
-from typing import Any, Dict, Optional
+from typing import Any
from homeassistant import bootstrap
from homeassistant.core import callback
@@ -34,8 +36,8 @@ class RuntimeConfig:
verbose: bool = False
- log_rotate_days: Optional[int] = None
- log_file: Optional[str] = None
+ log_rotate_days: int | None = None
+ log_file: str | None = None
log_no_color: bool = False
debug: bool = False
@@ -83,7 +85,7 @@ def close() -> None:
@callback
-def _async_loop_exception_handler(_: Any, context: Dict[str, Any]) -> None:
+def _async_loop_exception_handler(_: Any, context: dict[str, Any]) -> None:
"""Handle all exception inside the core loop."""
kwargs = {}
exception = context.get("exception")
diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py
index 8042e546884019..9aa07b94dc8952 100644
--- a/homeassistant/scripts/__init__.py
+++ b/homeassistant/scripts/__init__.py
@@ -1,11 +1,13 @@
"""Home Assistant command line scripts."""
+from __future__ import annotations
+
import argparse
import asyncio
import importlib
import logging
import os
import sys
-from typing import List, Optional, Sequence, Text
+from typing import Sequence
from homeassistant import runner
from homeassistant.bootstrap import async_mount_local_lib_path
@@ -16,7 +18,7 @@
# mypy: allow-untyped-defs, no-warn-return-any
-def run(args: List) -> int:
+def run(args: list) -> int:
"""Run a script."""
scripts = []
path = os.path.dirname(__file__)
@@ -65,7 +67,7 @@ def run(args: List) -> int:
return script.run(args[1:]) # type: ignore
-def extract_config_dir(args: Optional[Sequence[Text]] = None) -> str:
+def extract_config_dir(args: Sequence[str] | None = None) -> str:
"""Extract the config dir from the arguments or get the default."""
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("-c", "--config", default=None)
diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py
index 48e6d7d530210a..2acefbce12891a 100644
--- a/homeassistant/scripts/benchmark/__init__.py
+++ b/homeassistant/scripts/benchmark/__init__.py
@@ -1,4 +1,6 @@
"""Script to run benchmarks."""
+from __future__ import annotations
+
import argparse
import asyncio
import collections
@@ -7,7 +9,7 @@
import json
import logging
from timeit import default_timer as timer
-from typing import Callable, Dict, TypeVar
+from typing import Callable, TypeVar
from homeassistant import core
from homeassistant.components.websocket_api.const import JSON_DUMP
@@ -21,7 +23,7 @@
CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name
-BENCHMARKS: Dict[str, Callable] = {}
+BENCHMARKS: dict[str, Callable] = {}
def run(args):
@@ -62,7 +64,7 @@ async def fire_events(hass):
"""Fire a million events."""
count = 0
event_name = "benchmark_event"
- event = asyncio.Event()
+ events_to_fire = 10 ** 6
@core.callback
def listener(_):
@@ -70,17 +72,48 @@ def listener(_):
nonlocal count
count += 1
- if count == 10 ** 6:
- event.set()
-
hass.bus.async_listen(event_name, listener)
- for _ in range(10 ** 6):
+ for _ in range(events_to_fire):
hass.bus.async_fire(event_name)
start = timer()
- await event.wait()
+ await hass.async_block_till_done()
+
+ assert count == events_to_fire
+
+ return timer() - start
+
+
+@benchmark
+async def fire_events_with_filter(hass):
+ """Fire a million events with a filter that rejects them."""
+ count = 0
+ event_name = "benchmark_event"
+ events_to_fire = 10 ** 6
+
+ @core.callback
+ def event_filter(event):
+ """Filter event."""
+ return False
+
+ @core.callback
+ def listener(_):
+ """Handle event."""
+ nonlocal count
+ count += 1
+
+ hass.bus.async_listen(event_name, listener, event_filter=event_filter)
+
+ for _ in range(events_to_fire):
+ hass.bus.async_fire(event_name)
+
+ start = timer()
+
+ await hass.async_block_till_done()
+
+ assert count == 0
return timer() - start
@@ -154,7 +187,7 @@ async def state_changed_event_helper(hass):
"""Run a million events through state changed event helper with 1000 entities."""
count = 0
entity_id = "light.kitchen"
- event = asyncio.Event()
+ events_to_fire = 10 ** 6
@core.callback
def listener(*args):
@@ -162,9 +195,6 @@ def listener(*args):
nonlocal count
count += 1
- if count == 10 ** 6:
- event.set()
-
hass.helpers.event.async_track_state_change_event(
[f"{entity_id}{idx}" for idx in range(1000)], listener
)
@@ -175,12 +205,49 @@ def listener(*args):
"new_state": core.State(entity_id, "on"),
}
- for _ in range(10 ** 6):
+ for _ in range(events_to_fire):
hass.bus.async_fire(EVENT_STATE_CHANGED, event_data)
start = timer()
- await event.wait()
+ await hass.async_block_till_done()
+
+ assert count == events_to_fire
+
+ return timer() - start
+
+
+@benchmark
+async def state_changed_event_filter_helper(hass):
+ """Run a million events through state changed event helper with 1000 entities that all get filtered."""
+ count = 0
+ entity_id = "light.kitchen"
+ events_to_fire = 10 ** 6
+
+ @core.callback
+ def listener(*args):
+ """Handle event."""
+ nonlocal count
+ count += 1
+
+ hass.helpers.event.async_track_state_change_event(
+ [f"{entity_id}{idx}" for idx in range(1000)], listener
+ )
+
+ event_data = {
+ "entity_id": "switch.no_listeners",
+ "old_state": core.State(entity_id, "off"),
+ "new_state": core.State(entity_id, "on"),
+ }
+
+ for _ in range(events_to_fire):
+ hass.bus.async_fire(EVENT_STATE_CHANGED, event_data)
+
+ start = timer()
+
+ await hass.async_block_till_done()
+
+ assert count == 0
return timer() - start
diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py
index 07a6a54e402eb3..893351c7715266 100644
--- a/homeassistant/scripts/check_config.py
+++ b/homeassistant/scripts/check_config.py
@@ -1,4 +1,6 @@
"""Script to check the configuration file."""
+from __future__ import annotations
+
import argparse
import asyncio
from collections import OrderedDict
@@ -6,29 +8,29 @@
from glob import glob
import logging
import os
-from typing import Any, Callable, Dict, List, Tuple
+from typing import Any, Callable
from unittest.mock import patch
-from homeassistant import bootstrap, core
+from homeassistant import core
from homeassistant.config import get_default_config_dir
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.check_config import async_check_ha_config_file
+from homeassistant.util.yaml import Secrets
import homeassistant.util.yaml.loader as yaml_loader
# mypy: allow-untyped-calls, allow-untyped-defs
-REQUIREMENTS = ("colorlog==4.6.2",)
+REQUIREMENTS = ("colorlog==4.8.0",)
_LOGGER = logging.getLogger(__name__)
# pylint: disable=protected-access
-MOCKS: Dict[str, Tuple[str, Callable]] = {
+MOCKS: dict[str, tuple[str, Callable]] = {
"load": ("homeassistant.util.yaml.loader.load_yaml", yaml_loader.load_yaml),
"load*": ("homeassistant.config.load_yaml", yaml_loader.load_yaml),
"secrets": ("homeassistant.util.yaml.loader.secret_yaml", yaml_loader.secret_yaml),
}
-SILENCE = ("homeassistant.scripts.check_config.yaml_loader.clear_secret_cache",)
-PATCHES: Dict[str, Any] = {}
+PATCHES: dict[str, Any] = {}
C_HEAD = "bold"
ERROR_STR = "General Errors"
@@ -48,7 +50,7 @@ def color(the_color, *args, reset=None):
raise ValueError(f"Invalid color {k!s} in {the_color}") from k
-def run(script_args: List) -> int:
+def run(script_args: list) -> int:
"""Handle check config commandline script."""
parser = argparse.ArgumentParser(description="Check Home Assistant configuration.")
parser.add_argument("--script", choices=["check_config"])
@@ -83,7 +85,7 @@ def run(script_args: List) -> int:
res = check(config_dir, args.secrets)
- domain_info: List[str] = []
+ domain_info: list[str] = []
if args.info:
domain_info = args.info.split(",")
@@ -123,7 +125,7 @@ def run(script_args: List) -> int:
dump_dict(res["components"].get(domain))
if args.secrets:
- flatsecret: Dict[str, str] = {}
+ flatsecret: dict[str, str] = {}
for sfn, sdict in res["secret_cache"].items():
sss = []
@@ -141,12 +143,7 @@ def run(script_args: List) -> int:
if sval is None:
print(" -", skey + ":", color("red", "not found"))
continue
- print(
- " -",
- skey + ":",
- sval,
- color("cyan", "[from:", flatsecret.get(skey, "keyring") + "]"),
- )
+ print(" -", skey + ":", sval)
return len(res["except"])
@@ -154,19 +151,19 @@ def run(script_args: List) -> int:
def check(config_dir, secrets=False):
"""Perform a check by mocking hass load functions."""
logging.getLogger("homeassistant.loader").setLevel(logging.CRITICAL)
- res: Dict[str, Any] = {
+ res: dict[str, Any] = {
"yaml_files": OrderedDict(), # yaml_files loaded
"secrets": OrderedDict(), # secret cache and secrets loaded
"except": OrderedDict(), # exceptions raised (with config)
#'components' is a HomeAssistantConfig # noqa: E265
- "secret_cache": None,
+ "secret_cache": {},
}
# pylint: disable=possibly-unused-variable
- def mock_load(filename):
+ def mock_load(filename, secrets=None):
"""Mock hass.util.load_yaml to save config file names."""
res["yaml_files"][filename] = True
- return MOCKS["load"][1](filename)
+ return MOCKS["load"][1](filename, secrets)
# pylint: disable=possibly-unused-variable
def mock_secrets(ldr, node):
@@ -178,10 +175,6 @@ def mock_secrets(ldr, node):
res["secrets"][node.value] = val
return val
- # Patches to skip functions
- for sil in SILENCE:
- PATCHES[sil] = patch(sil)
-
# Patches with local mock functions
for key, val in MOCKS.items():
if not secrets and key == "secrets":
@@ -197,11 +190,19 @@ def mock_secrets(ldr, node):
if secrets:
# Ensure !secrets point to the patched function
- yaml_loader.yaml.SafeLoader.add_constructor("!secret", yaml_loader.secret_yaml)
+ yaml_loader.SafeLineLoader.add_constructor("!secret", yaml_loader.secret_yaml)
+
+ def secrets_proxy(*args):
+ secrets = Secrets(*args)
+ res["secret_cache"] = secrets._cache
+ return secrets
try:
- res["components"] = asyncio.run(async_check_config(config_dir))
- res["secret_cache"] = OrderedDict(yaml_loader.__SECRET_CACHE)
+ with patch.object(yaml_loader, "Secrets", secrets_proxy):
+ res["components"] = asyncio.run(async_check_config(config_dir))
+ res["secret_cache"] = {
+ str(key): val for key, val in res["secret_cache"].items()
+ }
for err in res["components"].errors:
domain = err.domain or ERROR_STR
res["except"].setdefault(domain, []).append(err.message)
@@ -217,10 +218,9 @@ def mock_secrets(ldr, node):
pat.stop()
if secrets:
# Ensure !secrets point to the original function
- yaml_loader.yaml.SafeLoader.add_constructor(
+ yaml_loader.SafeLineLoader.add_constructor(
"!secret", yaml_loader.secret_yaml
)
- bootstrap.clear_secret_cache()
return res
diff --git a/homeassistant/scripts/credstash.py b/homeassistant/scripts/credstash.py
deleted file mode 100644
index 99227d81b663ce..00000000000000
--- a/homeassistant/scripts/credstash.py
+++ /dev/null
@@ -1,74 +0,0 @@
-"""Script to get, put and delete secrets stored in credstash."""
-import argparse
-import getpass
-
-from homeassistant.util.yaml import _SECRET_NAMESPACE
-
-# mypy: allow-untyped-defs
-
-REQUIREMENTS = ["credstash==1.15.0"]
-
-
-def run(args):
- """Handle credstash script."""
- parser = argparse.ArgumentParser(
- description=(
- "Modify Home Assistant secrets in credstash."
- "Use the secrets in configuration files with: "
- "!secret "
- )
- )
- parser.add_argument("--script", choices=["credstash"])
- parser.add_argument(
- "action",
- choices=["get", "put", "del", "list"],
- help="Get, put or delete a secret, or list all available secrets",
- )
- parser.add_argument("name", help="Name of the secret", nargs="?", default=None)
- parser.add_argument(
- "value", help="The value to save when putting a secret", nargs="?", default=None
- )
-
- # pylint: disable=import-error, no-member, import-outside-toplevel
- import credstash
-
- args = parser.parse_args(args)
- table = _SECRET_NAMESPACE
-
- try:
- credstash.listSecrets(table=table)
- except Exception: # pylint: disable=broad-except
- credstash.createDdbTable(table=table)
-
- if args.action == "list":
- secrets = [i["name"] for i in credstash.listSecrets(table=table)]
- deduped_secrets = sorted(set(secrets))
-
- print("Saved secrets:")
- for secret in deduped_secrets:
- print(secret)
- return 0
-
- if args.name is None:
- parser.print_help()
- return 1
-
- if args.action == "put":
- if args.value:
- the_secret = args.value
- else:
- the_secret = getpass.getpass(f"Please enter the secret for {args.name}: ")
- current_version = credstash.getHighestVersion(args.name, table=table)
- credstash.putSecret(
- args.name, the_secret, version=int(current_version) + 1, table=table
- )
- print(f"Secret {args.name} put successfully")
- elif args.action == "get":
- the_secret = credstash.getSecret(args.name, table=table)
- if the_secret is None:
- print(f"Secret {args.name} not found")
- else:
- print(f"Secret {args.name}={the_secret}")
- elif args.action == "del":
- credstash.deleteSecrets(args.name, table=table)
- print(f"Deleted secret {args.name}")
diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py
deleted file mode 100644
index 0166d41ce0c0c6..00000000000000
--- a/homeassistant/scripts/keyring.py
+++ /dev/null
@@ -1,62 +0,0 @@
-"""Script to get, set and delete secrets stored in the keyring."""
-import argparse
-import getpass
-import os
-
-from homeassistant.util.yaml import _SECRET_NAMESPACE
-
-# mypy: allow-untyped-defs
-REQUIREMENTS = ["keyring==21.2.0", "keyrings.alt==3.4.0"]
-
-
-def run(args):
- """Handle keyring script."""
- parser = argparse.ArgumentParser(
- description=(
- "Modify Home Assistant secrets in the default keyring. "
- "Use the secrets in configuration files with: "
- "!secret "
- )
- )
- parser.add_argument("--script", choices=["keyring"])
- parser.add_argument(
- "action",
- choices=["get", "set", "del", "info"],
- help="Get, set or delete a secret",
- )
- parser.add_argument("name", help="Name of the secret", nargs="?", default=None)
-
- import keyring # pylint: disable=import-outside-toplevel
-
- # pylint: disable=import-outside-toplevel
- from keyring.util import platform_ as platform
-
- args = parser.parse_args(args)
-
- if args.action == "info":
- keyr = keyring.get_keyring()
- print("Keyring version {}\n".format(REQUIREMENTS[0].split("==")[1]))
- print(f"Active keyring : {keyr.__module__}")
- config_name = os.path.join(platform.config_root(), "keyringrc.cfg")
- print(f"Config location : {config_name}")
- print(f"Data location : {platform.data_root()}\n")
- elif args.name is None:
- parser.print_help()
- return 1
-
- if args.action == "set":
- entered_secret = getpass.getpass(f"Please enter the secret for {args.name}: ")
- keyring.set_password(_SECRET_NAMESPACE, args.name, entered_secret)
- print(f"Secret {args.name} set successfully")
- elif args.action == "get":
- the_secret = keyring.get_password(_SECRET_NAMESPACE, args.name)
- if the_secret is None:
- print(f"Secret {args.name} not found")
- else:
- print(f"Secret {args.name}={the_secret}")
- elif args.action == "del":
- try:
- keyring.delete_password(_SECRET_NAMESPACE, args.name)
- print(f"Deleted secret {args.name}")
- except keyring.errors.PasswordDeleteError:
- print(f"Secret {args.name} not found")
diff --git a/homeassistant/setup.py b/homeassistant/setup.py
index 4060b0410d6601..c65e428e03a451 100644
--- a/homeassistant/setup.py
+++ b/homeassistant/setup.py
@@ -1,23 +1,50 @@
"""All methods needed to bootstrap a Home Assistant instance."""
+from __future__ import annotations
+
import asyncio
+import contextlib
import logging.handlers
from timeit import default_timer as timer
from types import ModuleType
-from typing import Awaitable, Callable, Optional, Set
+from typing import Awaitable, Callable, Generator, Iterable
from homeassistant import config as conf_util, core, loader, requirements
from homeassistant.config import async_notify_setup_error
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType
-from homeassistant.util import dt as dt_util
+from homeassistant.util import dt as dt_util, ensure_unique_string
_LOGGER = logging.getLogger(__name__)
ATTR_COMPONENT = "component"
+BASE_PLATFORMS = {
+ "air_quality",
+ "alarm_control_panel",
+ "binary_sensor",
+ "climate",
+ "cover",
+ "device_tracker",
+ "fan",
+ "humidifier",
+ "image_processing",
+ "light",
+ "lock",
+ "media_player",
+ "notify",
+ "remote",
+ "scene",
+ "sensor",
+ "switch",
+ "vacuum",
+ "water_heater",
+}
+
DATA_SETUP_DONE = "setup_done"
DATA_SETUP_STARTED = "setup_started"
+DATA_SETUP_TIME = "setup_time"
+
DATA_SETUP = "setup_tasks"
DATA_DEPS_REQS = "deps_reqs_processed"
@@ -26,7 +53,7 @@
@core.callback
-def async_set_domains_to_be_loaded(hass: core.HomeAssistant, domains: Set[str]) -> None:
+def async_set_domains_to_be_loaded(hass: core.HomeAssistant, domains: set[str]) -> None:
"""Set domains that are going to be loaded from the config.
This will allow us to properly handle after_dependencies.
@@ -133,7 +160,7 @@ async def _async_setup_component(
This method is a coroutine.
"""
- def log_error(msg: str, link: Optional[str] = None) -> None:
+ def log_error(msg: str, link: str | None = None) -> None:
"""Log helper."""
_LOGGER.error("Setup failed for %s: %s", domain, msg)
async_notify_setup_error(hass, domain, link)
@@ -145,7 +172,7 @@ def log_error(msg: str, link: Optional[str] = None) -> None:
return False
if integration.disabled:
- log_error(f"dependency is disabled - {integration.disabled}")
+ log_error(f"Dependency is disabled - {integration.disabled}")
return False
# Validate all dependencies exist and there are no circular dependencies
@@ -181,81 +208,77 @@ def log_error(msg: str, link: Optional[str] = None) -> None:
start = timer()
_LOGGER.info("Setting up %s", domain)
- hass.data.setdefault(DATA_SETUP_STARTED, {})[domain] = dt_util.utcnow()
-
- if hasattr(component, "PLATFORM_SCHEMA"):
- # Entity components have their own warning
- warn_task = None
- else:
- warn_task = hass.loop.call_later(
- SLOW_SETUP_WARNING,
- _LOGGER.warning,
- "Setup of %s is taking over %s seconds.",
- domain,
- SLOW_SETUP_WARNING,
- )
+ with async_start_setup(hass, [domain]):
+ if hasattr(component, "PLATFORM_SCHEMA"):
+ # Entity components have their own warning
+ warn_task = None
+ else:
+ warn_task = hass.loop.call_later(
+ SLOW_SETUP_WARNING,
+ _LOGGER.warning,
+ "Setup of %s is taking over %s seconds.",
+ domain,
+ SLOW_SETUP_WARNING,
+ )
- try:
- if hasattr(component, "async_setup"):
- task = component.async_setup(hass, processed_config) # type: ignore
- elif hasattr(component, "setup"):
- # This should not be replaced with hass.async_add_executor_job because
- # we don't want to track this task in case it blocks startup.
- task = hass.loop.run_in_executor(
- None, component.setup, hass, processed_config # type: ignore
+ task = None
+ result = True
+ try:
+ if hasattr(component, "async_setup"):
+ task = component.async_setup(hass, processed_config) # type: ignore
+ elif hasattr(component, "setup"):
+ # This should not be replaced with hass.async_add_executor_job because
+ # we don't want to track this task in case it blocks startup.
+ task = hass.loop.run_in_executor(
+ None, component.setup, hass, processed_config # type: ignore
+ )
+ elif not hasattr(component, "async_setup_entry"):
+ log_error("No setup or config entry setup function defined.")
+ return False
+
+ if task:
+ async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, domain):
+ result = await task
+ except asyncio.TimeoutError:
+ _LOGGER.error(
+ "Setup of %s is taking longer than %s seconds."
+ " Startup will proceed without waiting any longer",
+ domain,
+ SLOW_SETUP_MAX_WAIT,
+ )
+ return False
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Error during setup of component %s", domain)
+ async_notify_setup_error(hass, domain, integration.documentation)
+ return False
+ finally:
+ end = timer()
+ if warn_task:
+ warn_task.cancel()
+ _LOGGER.info("Setup of domain %s took %.1f seconds", domain, end - start)
+
+ if result is False:
+ log_error("Integration failed to initialize.")
+ return False
+ if result is not True:
+ log_error(
+ f"Integration {domain!r} did not return boolean if setup was "
+ "successful. Disabling component."
)
- else:
- log_error("No setup function defined.")
- hass.data[DATA_SETUP_STARTED].pop(domain)
return False
- async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, domain):
- result = await task
- except asyncio.TimeoutError:
- _LOGGER.error(
- "Setup of %s is taking longer than %s seconds."
- " Startup will proceed without waiting any longer",
- domain,
- SLOW_SETUP_MAX_WAIT,
- )
- hass.data[DATA_SETUP_STARTED].pop(domain)
- return False
- except Exception: # pylint: disable=broad-except
- _LOGGER.exception("Error during setup of component %s", domain)
- async_notify_setup_error(hass, domain, integration.documentation)
- hass.data[DATA_SETUP_STARTED].pop(domain)
- return False
- finally:
- end = timer()
- if warn_task:
- warn_task.cancel()
- _LOGGER.info("Setup of domain %s took %.1f seconds", domain, end - start)
-
- if result is False:
- log_error("Integration failed to initialize.")
- hass.data[DATA_SETUP_STARTED].pop(domain)
- return False
- if result is not True:
- log_error(
- f"Integration {domain!r} did not return boolean if setup was "
- "successful. Disabling component."
- )
- hass.data[DATA_SETUP_STARTED].pop(domain)
- return False
-
- # Flush out async_setup calling create_task. Fragile but covered by test.
- await asyncio.sleep(0)
- await hass.config_entries.flow.async_wait_init_flow_finish(domain)
+ # Flush out async_setup calling create_task. Fragile but covered by test.
+ await asyncio.sleep(0)
+ await hass.config_entries.flow.async_wait_init_flow_finish(domain)
- await asyncio.gather(
- *[
- entry.async_setup(hass, integration=integration)
- for entry in hass.config_entries.async_entries(domain)
- ]
- )
+ await asyncio.gather(
+ *[
+ entry.async_setup(hass, integration=integration)
+ for entry in hass.config_entries.async_entries(domain)
+ ]
+ )
- hass.config.components.add(domain)
- hass.data[DATA_SETUP_STARTED].pop(domain)
+ hass.config.components.add(domain)
# Cleanup
if domain in hass.data[DATA_SETUP]:
@@ -268,7 +291,7 @@ def log_error(msg: str, link: Optional[str] = None) -> None:
async def async_prepare_setup_platform(
hass: core.HomeAssistant, hass_config: ConfigType, domain: str, platform_name: str
-) -> Optional[ModuleType]:
+) -> ModuleType | None:
"""Load a platform and makes sure dependencies are setup.
This method is a coroutine.
@@ -313,10 +336,11 @@ def log_error(msg: str) -> None:
log_error(f"Unable to import the component ({exc}).")
return None
- if hasattr(component, "setup") or hasattr(component, "async_setup"):
- if not await async_setup_component(hass, integration.domain, hass_config):
- log_error("Unable to set up component.")
- return None
+ if (
+ hasattr(component, "setup") or hasattr(component, "async_setup")
+ ) and not await async_setup_component(hass, integration.domain, hass_config):
+ log_error("Unable to set up component.")
+ return None
return platform
@@ -378,3 +402,44 @@ async def loaded_event(event: core.Event) -> None:
await when_setup()
unsub = hass.bus.async_listen(EVENT_COMPONENT_LOADED, loaded_event)
+
+
+@core.callback
+def async_get_loaded_integrations(hass: core.HomeAssistant) -> set:
+ """Return the complete list of loaded integrations."""
+ integrations = set()
+ for component in hass.config.components:
+ if "." not in component:
+ integrations.add(component)
+ continue
+ domain, platform = component.split(".", 1)
+ if domain in BASE_PLATFORMS:
+ integrations.add(platform)
+ return integrations
+
+
+@contextlib.contextmanager
+def async_start_setup(hass: core.HomeAssistant, components: Iterable) -> Generator:
+ """Keep track of when setup starts and finishes."""
+ setup_started = hass.data.setdefault(DATA_SETUP_STARTED, {})
+ started = dt_util.utcnow()
+ unique_components = {}
+ for domain in components:
+ unique = ensure_unique_string(domain, setup_started)
+ unique_components[unique] = domain
+ setup_started[unique] = started
+
+ yield
+
+ setup_time = hass.data.setdefault(DATA_SETUP_TIME, {})
+ time_taken = dt_util.utcnow() - started
+ for unique, domain in unique_components.items():
+ del setup_started[unique]
+ if "." in domain:
+ _, integration = domain.split(".", 1)
+ else:
+ integration = domain
+ if integration in setup_time:
+ setup_time[integration] += time_taken
+ else:
+ setup_time[integration] = time_taken
diff --git a/homeassistant/strings.json b/homeassistant/strings.json
index e2a85637fbbc64..31693c5bba183a 100644
--- a/homeassistant/strings.json
+++ b/homeassistant/strings.json
@@ -67,11 +67,12 @@
"already_in_progress": "Configuration flow is already in progress",
"no_devices_found": "No devices found on the network",
"webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages.",
+ "oauth2_error": "Received invalid token data.",
"oauth2_missing_configuration": "The component is not configured. Please follow the documentation.",
"oauth2_authorize_url_timeout": "Timeout generating authorize URL.",
"oauth2_no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})",
"reauth_successful": "Re-authentication was successful",
- "unknown_authorize_url_generation": "Unknown error generating an authorize url."
+ "unknown_authorize_url_generation": "Unknown error generating an authorize URL."
}
}
}
diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py
index 281a7d5308c302..79ceeada0c28aa 100644
--- a/homeassistant/util/__init__.py
+++ b/homeassistant/util/__init__.py
@@ -1,4 +1,6 @@
"""Helper methods for various modules."""
+from __future__ import annotations
+
import asyncio
from datetime import datetime, timedelta
import enum
@@ -9,16 +11,7 @@
import string
import threading
from types import MappingProxyType
-from typing import (
- Any,
- Callable,
- Coroutine,
- Iterable,
- KeysView,
- Optional,
- TypeVar,
- Union,
-)
+from typing import Any, Callable, Coroutine, Iterable, KeysView, TypeVar
import slugify as unicode_slug
@@ -106,8 +99,8 @@ def repr_helper(inp: Any) -> str:
def convert(
- value: Optional[T], to_type: Callable[[T], U], default: Optional[U] = None
-) -> Optional[U]:
+ value: T | None, to_type: Callable[[T], U], default: U | None = None
+) -> U | None:
"""Convert value to to_type, returns default if fails."""
try:
return default if value is None else to_type(value)
@@ -117,7 +110,7 @@ def convert(
def ensure_unique_string(
- preferred_string: str, current_strings: Union[Iterable[str], KeysView[str]]
+ preferred_string: str, current_strings: Iterable[str] | KeysView[str]
) -> str:
"""Return a string that is not present in current_strings.
@@ -213,7 +206,7 @@ class Throttle:
"""
def __init__(
- self, min_time: timedelta, limit_no_throttle: Optional[timedelta] = None
+ self, min_time: timedelta, limit_no_throttle: timedelta | None = None
) -> None:
"""Initialize the throttle."""
self.min_time = min_time
@@ -253,7 +246,7 @@ def throttled_value() -> None: # type: ignore
)
@wraps(method)
- def wrapper(*args: Any, **kwargs: Any) -> Union[Callable, Coroutine]:
+ def wrapper(*args: Any, **kwargs: Any) -> Callable | Coroutine:
"""Wrap that allows wrapped to be called only once per min_time.
If we cannot acquire the lock, it is running so return None.
diff --git a/homeassistant/util/aiohttp.py b/homeassistant/util/aiohttp.py
index 36cdc0f25e2fbf..7d14ec252d9bf3 100644
--- a/homeassistant/util/aiohttp.py
+++ b/homeassistant/util/aiohttp.py
@@ -1,7 +1,9 @@
"""Utilities to help with aiohttp."""
+from __future__ import annotations
+
import io
import json
-from typing import Any, Dict, Optional
+from typing import Any
from urllib.parse import parse_qsl
from multidict import CIMultiDict, MultiDict
@@ -26,7 +28,7 @@ async def read(self, byte_count: int = -1) -> bytes:
class MockRequest:
"""Mock an aiohttp request."""
- mock_source: Optional[str] = None
+ mock_source: str | None = None
def __init__(
self,
@@ -34,8 +36,8 @@ def __init__(
mock_source: str,
method: str = "GET",
status: int = HTTP_OK,
- headers: Optional[Dict[str, str]] = None,
- query_string: Optional[str] = None,
+ headers: dict[str, str] | None = None,
+ query_string: str | None = None,
url: str = "",
) -> None:
"""Initialize a request."""
@@ -48,7 +50,7 @@ def __init__(
self.mock_source = mock_source
@property
- def query(self) -> "MultiDict[str]":
+ def query(self) -> MultiDict[str]:
"""Return a dictionary with the query variables."""
return MultiDict(parse_qsl(self.query_string, keep_blank_values=True))
@@ -66,7 +68,7 @@ async def json(self) -> Any:
"""Return the body as JSON."""
return json.loads(self._text)
- async def post(self) -> "MultiDict[str]":
+ async def post(self) -> MultiDict[str]:
"""Return POST parameters."""
return MultiDict(parse_qsl(self._text, keep_blank_values=True))
diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py
index ded44473038d6f..15353d1f7eb8d8 100644
--- a/homeassistant/util/async_.py
+++ b/homeassistant/util/async_.py
@@ -1,4 +1,6 @@
"""Asyncio utilities."""
+from __future__ import annotations
+
from asyncio import Semaphore, coroutines, ensure_future, gather, get_running_loop
from asyncio.events import AbstractEventLoop
import concurrent.futures
@@ -10,6 +12,8 @@
_LOGGER = logging.getLogger(__name__)
+_SHUTDOWN_RUN_CALLBACK_THREADSAFE = "_shutdown_run_callback_threadsafe"
+
T = TypeVar("T")
@@ -36,7 +40,7 @@ def callback() -> None:
def run_callback_threadsafe(
loop: AbstractEventLoop, callback: Callable[..., T], *args: Any
-) -> "concurrent.futures.Future[T]":
+) -> concurrent.futures.Future[T]: # pylint: disable=unsubscriptable-object
"""Submit a callback object to a given event loop.
Return a concurrent.futures.Future to access the result.
@@ -58,6 +62,28 @@ def run_callback() -> None:
_LOGGER.warning("Exception on lost future: ", exc_info=True)
loop.call_soon_threadsafe(run_callback)
+
+ if hasattr(loop, _SHUTDOWN_RUN_CALLBACK_THREADSAFE):
+ #
+ # If the final `HomeAssistant.async_block_till_done` in
+ # `HomeAssistant.async_stop` has already been called, the callback
+ # will never run and, `future.result()` will block forever which
+ # will prevent the thread running this code from shutting down which
+ # will result in a deadlock when the main thread attempts to shutdown
+ # the executor and `.join()` the thread running this code.
+ #
+ # To prevent this deadlock we do the following on shutdown:
+ #
+ # 1. Set the _SHUTDOWN_RUN_CALLBACK_THREADSAFE attr on this function
+ # by calling `shutdown_run_callback_threadsafe`
+ # 2. Call `hass.async_block_till_done` at least once after shutdown
+ # to ensure all callbacks have run
+ # 3. Raise an exception here to ensure `future.result()` can never be
+ # called and hit the deadlock since once `shutdown_run_callback_threadsafe`
+ # we cannot promise the callback will be executed.
+ #
+ raise RuntimeError("The event loop is in the process of shutting down.")
+
return future
@@ -110,6 +136,10 @@ def check_loop() -> None:
found_frame.lineno,
found_frame.line.strip(),
)
+ raise RuntimeError(
+ f"I/O must be done in the executor; Use `await hass.async_add_executor_job()` "
+ f"at {found_frame.filename[index:]}, line {found_frame.lineno}: {found_frame.line.strip()}"
+ )
def protect_loop(func: Callable) -> Callable:
@@ -139,3 +169,20 @@ async def sem_task(task: Awaitable[Any]) -> Any:
return await gather(
*(sem_task(task) for task in tasks), return_exceptions=return_exceptions
)
+
+
+def shutdown_run_callback_threadsafe(loop: AbstractEventLoop) -> None:
+ """Call when run_callback_threadsafe should prevent creating new futures.
+
+ We must finish all callbacks before the executor is shutdown
+ or we can end up in a deadlock state where:
+
+ `executor.result()` is waiting for its `._condition`
+ and the executor shutdown is trying to `.join()` the
+ executor thread.
+
+ This function is considered irreversible and should only ever
+ be called when Home Assistant is going to shutdown and
+ python is going to exit.
+ """
+ setattr(loop, _SHUTDOWN_RUN_CALLBACK_THREADSAFE, True)
diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py
index 1e782f0c0f9ee2..2a34fe82c5933d 100644
--- a/homeassistant/util/color.py
+++ b/homeassistant/util/color.py
@@ -1,7 +1,8 @@
"""Color util methods."""
+from __future__ import annotations
+
import colorsys
import math
-from typing import List, Optional, Tuple
import attr
@@ -160,6 +161,8 @@
"whitesmoke": (245, 245, 245),
"yellow": (255, 255, 0),
"yellowgreen": (154, 205, 50),
+ # And...
+ "homeassistant": (3, 169, 244),
}
@@ -181,7 +184,7 @@ class GamutType:
blue: XYPoint = attr.ib()
-def color_name_to_rgb(color_name: str) -> Tuple[int, int, int]:
+def color_name_to_rgb(color_name: str) -> tuple[int, int, int]:
"""Convert color name to RGB hex value."""
# COLORS map has no spaces in it, so make the color_name have no
# spaces in it as well for matching purposes
@@ -193,9 +196,11 @@ def color_name_to_rgb(color_name: str) -> Tuple[int, int, int]:
# pylint: disable=invalid-name
+
+
def color_RGB_to_xy(
- iR: int, iG: int, iB: int, Gamut: Optional[GamutType] = None
-) -> Tuple[float, float]:
+ iR: int, iG: int, iB: int, Gamut: GamutType | None = None
+) -> tuple[float, float]:
"""Convert from RGB color to XY color."""
return color_RGB_to_xy_brightness(iR, iG, iB, Gamut)[:2]
@@ -203,10 +208,9 @@ def color_RGB_to_xy(
# Taken from:
# http://www.developers.meethue.com/documentation/color-conversions-rgb-xy
# License: Code is given as is. Use at your own risk and discretion.
-# pylint: disable=invalid-name
def color_RGB_to_xy_brightness(
- iR: int, iG: int, iB: int, Gamut: Optional[GamutType] = None
-) -> Tuple[float, float, int]:
+ iR: int, iG: int, iB: int, Gamut: GamutType | None = None
+) -> tuple[float, float, int]:
"""Convert from RGB color to XY color."""
if iR + iG + iB == 0:
return 0.0, 0.0, 0
@@ -245,8 +249,8 @@ def color_RGB_to_xy_brightness(
def color_xy_to_RGB(
- vX: float, vY: float, Gamut: Optional[GamutType] = None
-) -> Tuple[int, int, int]:
+ vX: float, vY: float, Gamut: GamutType | None = None
+) -> tuple[int, int, int]:
"""Convert from XY to a normalized RGB."""
return color_xy_brightness_to_RGB(vX, vY, 255, Gamut)
@@ -254,14 +258,13 @@ def color_xy_to_RGB(
# Converted to Python from Obj-C, original source from:
# http://www.developers.meethue.com/documentation/color-conversions-rgb-xy
def color_xy_brightness_to_RGB(
- vX: float, vY: float, ibrightness: int, Gamut: Optional[GamutType] = None
-) -> Tuple[int, int, int]:
+ vX: float, vY: float, ibrightness: int, Gamut: GamutType | None = None
+) -> tuple[int, int, int]:
"""Convert from XYZ to RGB."""
- if Gamut:
- if not check_point_in_lamps_reach((vX, vY), Gamut):
- xy_closest = get_closest_point_to_point((vX, vY), Gamut)
- vX = xy_closest[0]
- vY = xy_closest[1]
+ if Gamut and not check_point_in_lamps_reach((vX, vY), Gamut):
+ xy_closest = get_closest_point_to_point((vX, vY), Gamut)
+ vX = xy_closest[0]
+ vY = xy_closest[1]
brightness = ibrightness / 255.0
if brightness == 0.0:
@@ -301,7 +304,7 @@ def color_xy_brightness_to_RGB(
return (ir, ig, ib)
-def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> Tuple[int, int, int]:
+def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> tuple[int, int, int]:
"""Convert a hsb into its rgb representation."""
if fS == 0.0:
fV = int(fB * 255)
@@ -342,7 +345,7 @@ def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> Tuple[int, int, int]:
return (r, g, b)
-def color_RGB_to_hsv(iR: float, iG: float, iB: float) -> Tuple[float, float, float]:
+def color_RGB_to_hsv(iR: float, iG: float, iB: float) -> tuple[float, float, float]:
"""Convert an rgb color to its hsv representation.
Hue is scaled 0-360
@@ -353,12 +356,12 @@ def color_RGB_to_hsv(iR: float, iG: float, iB: float) -> Tuple[float, float, flo
return round(fHSV[0] * 360, 3), round(fHSV[1] * 100, 3), round(fHSV[2] * 100, 3)
-def color_RGB_to_hs(iR: float, iG: float, iB: float) -> Tuple[float, float]:
+def color_RGB_to_hs(iR: float, iG: float, iB: float) -> tuple[float, float]:
"""Convert an rgb color to its hs representation."""
return color_RGB_to_hsv(iR, iG, iB)[:2]
-def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> Tuple[int, int, int]:
+def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> tuple[int, int, int]:
"""Convert an hsv color into its rgb representation.
Hue is scaled 0-360
@@ -369,27 +372,27 @@ def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> Tuple[int, int, int]:
return (int(fRGB[0] * 255), int(fRGB[1] * 255), int(fRGB[2] * 255))
-def color_hs_to_RGB(iH: float, iS: float) -> Tuple[int, int, int]:
+def color_hs_to_RGB(iH: float, iS: float) -> tuple[int, int, int]:
"""Convert an hsv color into its rgb representation."""
return color_hsv_to_RGB(iH, iS, 100)
def color_xy_to_hs(
- vX: float, vY: float, Gamut: Optional[GamutType] = None
-) -> Tuple[float, float]:
+ vX: float, vY: float, Gamut: GamutType | None = None
+) -> tuple[float, float]:
"""Convert an xy color to its hs representation."""
h, s, _ = color_RGB_to_hsv(*color_xy_to_RGB(vX, vY, Gamut))
return h, s
def color_hs_to_xy(
- iH: float, iS: float, Gamut: Optional[GamutType] = None
-) -> Tuple[float, float]:
+ iH: float, iS: float, Gamut: GamutType | None = None
+) -> tuple[float, float]:
"""Convert an hs color to its xy representation."""
return color_RGB_to_xy(*color_hs_to_RGB(iH, iS), Gamut)
-def _match_max_scale(input_colors: Tuple, output_colors: Tuple) -> Tuple:
+def _match_max_scale(input_colors: tuple, output_colors: tuple) -> tuple:
"""Match the maximum value of the output to the input."""
max_in = max(input_colors)
max_out = max(output_colors)
@@ -400,7 +403,7 @@ def _match_max_scale(input_colors: Tuple, output_colors: Tuple) -> Tuple:
return tuple(int(round(i * factor)) for i in output_colors)
-def color_rgb_to_rgbw(r: int, g: int, b: int) -> Tuple[int, int, int, int]:
+def color_rgb_to_rgbw(r: int, g: int, b: int) -> tuple[int, int, int, int]:
"""Convert an rgb color to an rgbw representation."""
# Calculate the white channel as the minimum of input rgb channels.
# Subtract the white portion from the remaining rgb channels.
@@ -412,7 +415,7 @@ def color_rgb_to_rgbw(r: int, g: int, b: int) -> Tuple[int, int, int, int]:
return _match_max_scale((r, g, b), rgbw) # type: ignore
-def color_rgbw_to_rgb(r: int, g: int, b: int, w: int) -> Tuple[int, int, int]:
+def color_rgbw_to_rgb(r: int, g: int, b: int, w: int) -> tuple[int, int, int]:
"""Convert an rgbw color to an rgb representation."""
# Add the white channel back into the rgb channels.
rgb = (r + w, g + w, b + w)
@@ -427,7 +430,7 @@ def color_rgb_to_hex(r: int, g: int, b: int) -> str:
return "{:02x}{:02x}{:02x}".format(round(r), round(g), round(b))
-def rgb_hex_to_rgb_list(hex_string: str) -> List[int]:
+def rgb_hex_to_rgb_list(hex_string: str) -> list[int]:
"""Return an RGB color value list from a hex color string."""
return [
int(hex_string[i : i + len(hex_string) // 3], 16)
@@ -435,14 +438,14 @@ def rgb_hex_to_rgb_list(hex_string: str) -> List[int]:
]
-def color_temperature_to_hs(color_temperature_kelvin: float) -> Tuple[float, float]:
+def color_temperature_to_hs(color_temperature_kelvin: float) -> tuple[float, float]:
"""Return an hs color from a color temperature in Kelvin."""
return color_RGB_to_hs(*color_temperature_to_rgb(color_temperature_kelvin))
def color_temperature_to_rgb(
color_temperature_kelvin: float,
-) -> Tuple[float, float, float]:
+) -> tuple[float, float, float]:
"""
Return an RGB color from a color temperature in Kelvin.
@@ -505,12 +508,12 @@ def _get_blue(temperature: float) -> float:
return _bound(blue)
-def color_temperature_mired_to_kelvin(mired_temperature: float) -> float:
+def color_temperature_mired_to_kelvin(mired_temperature: float) -> int:
"""Convert absolute mired shift to degrees kelvin."""
return math.floor(1000000 / mired_temperature)
-def color_temperature_kelvin_to_mired(kelvin_temperature: float) -> float:
+def color_temperature_kelvin_to_mired(kelvin_temperature: float) -> int:
"""Convert degrees kelvin to mired shift."""
return math.floor(1000000 / kelvin_temperature)
@@ -552,8 +555,8 @@ def get_closest_point_to_line(A: XYPoint, B: XYPoint, P: XYPoint) -> XYPoint:
def get_closest_point_to_point(
- xy_tuple: Tuple[float, float], Gamut: GamutType
-) -> Tuple[float, float]:
+ xy_tuple: tuple[float, float], Gamut: GamutType
+) -> tuple[float, float]:
"""
Get the closest matching color within the gamut of the light.
@@ -589,7 +592,7 @@ def get_closest_point_to_point(
return (cx, cy)
-def check_point_in_lamps_reach(p: Tuple[float, float], Gamut: GamutType) -> bool:
+def check_point_in_lamps_reach(p: tuple[float, float], Gamut: GamutType) -> bool:
"""Check if the provided XYPoint can be recreated by a Hue lamp."""
v1 = XYPoint(Gamut.green.x - Gamut.red.x, Gamut.green.y - Gamut.red.y)
v2 = XYPoint(Gamut.blue.x - Gamut.red.x, Gamut.blue.y - Gamut.red.y)
diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py
index 0e0a060c49cbca..592c7c3145ea3b 100644
--- a/homeassistant/util/distance.py
+++ b/homeassistant/util/distance.py
@@ -1,6 +1,8 @@
"""Distance util functions."""
+from __future__ import annotations
+
from numbers import Number
-from typing import Callable, Dict
+from typing import Callable
from homeassistant.const import (
LENGTH,
@@ -26,7 +28,7 @@
LENGTH_YARD,
]
-TO_METERS: Dict[str, Callable[[float], float]] = {
+TO_METERS: dict[str, Callable[[float], float]] = {
LENGTH_METERS: lambda meters: meters,
LENGTH_MILES: lambda miles: miles * 1609.344,
LENGTH_YARD: lambda yards: yards * 0.9144,
@@ -37,7 +39,7 @@
LENGTH_MILLIMETERS: lambda millimeters: millimeters * 0.001,
}
-METERS_TO: Dict[str, Callable[[float], float]] = {
+METERS_TO: dict[str, Callable[[float], float]] = {
LENGTH_METERS: lambda meters: meters,
LENGTH_MILES: lambda meters: meters * 0.000621371,
LENGTH_YARD: lambda meters: meters * 1.09361,
diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py
index a4d5fd81c4f229..b0c6cb21fec06c 100644
--- a/homeassistant/util/dt.py
+++ b/homeassistant/util/dt.py
@@ -1,7 +1,10 @@
"""Helper methods to handle the time in Home Assistant."""
+from __future__ import annotations
+
+from contextlib import suppress
import datetime as dt
import re
-from typing import Any, Dict, List, Optional, Union, cast
+from typing import Any, cast
import ciso8601
import pytz
@@ -40,7 +43,7 @@ def set_default_time_zone(time_zone: dt.tzinfo) -> None:
DEFAULT_TIME_ZONE = time_zone
-def get_time_zone(time_zone_str: str) -> Optional[dt.tzinfo]:
+def get_time_zone(time_zone_str: str) -> dt.tzinfo | None:
"""Get time zone from string. Return None if unable to determine.
Async friendly.
@@ -56,7 +59,7 @@ def utcnow() -> dt.datetime:
return dt.datetime.now(NATIVE_UTC)
-def now(time_zone: Optional[dt.tzinfo] = None) -> dt.datetime:
+def now(time_zone: dt.tzinfo | None = None) -> dt.datetime:
"""Get now in specified time zone."""
return dt.datetime.now(time_zone or DEFAULT_TIME_ZONE)
@@ -77,7 +80,7 @@ def as_utc(dattim: dt.datetime) -> dt.datetime:
def as_timestamp(dt_value: dt.datetime) -> float:
"""Convert a date/time into a unix time (seconds since 1970)."""
if hasattr(dt_value, "timestamp"):
- parsed_dt: Optional[dt.datetime] = dt_value
+ parsed_dt: dt.datetime | None = dt_value
else:
parsed_dt = parse_datetime(str(dt_value))
if parsed_dt is None:
@@ -100,9 +103,7 @@ def utc_from_timestamp(timestamp: float) -> dt.datetime:
return UTC.localize(dt.datetime.utcfromtimestamp(timestamp))
-def start_of_local_day(
- dt_or_d: Union[dt.date, dt.datetime, None] = None
-) -> dt.datetime:
+def start_of_local_day(dt_or_d: dt.date | dt.datetime | None = None) -> dt.datetime:
"""Return local datetime object of start of day from date or datetime."""
if dt_or_d is None:
date: dt.date = now().date()
@@ -119,7 +120,7 @@ def start_of_local_day(
# Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved.
# https://github.com/django/django/blob/master/LICENSE
-def parse_datetime(dt_str: str) -> Optional[dt.datetime]:
+def parse_datetime(dt_str: str) -> dt.datetime | None:
"""Parse a string and return a datetime.datetime.
This function supports time zone offsets. When the input contains one,
@@ -127,19 +128,18 @@ def parse_datetime(dt_str: str) -> Optional[dt.datetime]:
Raises ValueError if the input is well formatted but not a valid datetime.
Returns None if the input isn't well formatted.
"""
- try:
+ with suppress(ValueError, IndexError):
return ciso8601.parse_datetime(dt_str)
- except (ValueError, IndexError):
- pass
+
match = DATETIME_RE.match(dt_str)
if not match:
return None
- kws: Dict[str, Any] = match.groupdict()
+ kws: dict[str, Any] = match.groupdict()
if kws["microsecond"]:
kws["microsecond"] = kws["microsecond"].ljust(6, "0")
tzinfo_str = kws.pop("tzinfo")
- tzinfo: Optional[dt.tzinfo] = None
+ tzinfo: dt.tzinfo | None = None
if tzinfo_str == "Z":
tzinfo = UTC
elif tzinfo_str is not None:
@@ -154,7 +154,7 @@ def parse_datetime(dt_str: str) -> Optional[dt.datetime]:
return dt.datetime(**kws)
-def parse_date(dt_str: str) -> Optional[dt.date]:
+def parse_date(dt_str: str) -> dt.date | None:
"""Convert a date string to a date object."""
try:
return dt.datetime.strptime(dt_str, DATE_STR_FORMAT).date()
@@ -162,7 +162,7 @@ def parse_date(dt_str: str) -> Optional[dt.date]:
return None
-def parse_time(time_str: str) -> Optional[dt.time]:
+def parse_time(time_str: str) -> dt.time | None:
"""Parse a time string (00:20:00) into Time object.
Return None if invalid.
@@ -213,7 +213,7 @@ def formatn(number: int, unit: str) -> str:
return formatn(rounded_delta, selected_unit)
-def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> List[int]:
+def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> list[int]:
"""Parse the time expression part and return a list of times to match."""
if parameter is None or parameter == MATCH_ALL:
res = list(range(min_value, max_value + 1))
@@ -227,7 +227,7 @@ def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> Lis
elif not hasattr(parameter, "__iter__"):
res = [int(parameter)]
else:
- res = list(sorted(int(x) for x in parameter))
+ res = sorted(int(x) for x in parameter)
for val in res:
if val < min_value or val > max_value:
@@ -241,9 +241,9 @@ def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> Lis
def find_next_time_expression_time(
now: dt.datetime, # pylint: disable=redefined-outer-name
- seconds: List[int],
- minutes: List[int],
- hours: List[int],
+ seconds: list[int],
+ minutes: list[int],
+ hours: list[int],
) -> dt.datetime:
"""Find the next datetime from now for which the time expression matches.
@@ -257,7 +257,7 @@ def find_next_time_expression_time(
if not seconds or not minutes or not hours:
raise ValueError("Cannot find a next time: Time expression never matches!")
- def _lower_bound(arr: List[int], cmp: int) -> Optional[int]:
+ def _lower_bound(arr: list[int], cmp: int) -> int | None:
"""Return the first value in arr greater or equal to cmp.
Return None if no such value exists.
diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py
index e906462a250f2e..fac008d9f0fd2c 100644
--- a/homeassistant/util/json.py
+++ b/homeassistant/util/json.py
@@ -1,10 +1,12 @@
"""JSON utility functions."""
+from __future__ import annotations
+
from collections import deque
import json
import logging
import os
import tempfile
-from typing import Any, Callable, Dict, List, Optional, Type, Union
+from typing import Any, Callable
from homeassistant.core import Event, State
from homeassistant.exceptions import HomeAssistantError
@@ -20,9 +22,7 @@ class WriteError(HomeAssistantError):
"""Error writing the data."""
-def load_json(
- filename: str, default: Union[List, Dict, None] = None
-) -> Union[List, Dict]:
+def load_json(filename: str, default: list | dict | None = None) -> list | dict:
"""Load JSON data from a file and return as dict or list.
Defaults to returning empty dict if file is not found.
@@ -44,10 +44,10 @@ def load_json(
def save_json(
filename: str,
- data: Union[List, Dict],
+ data: list | dict,
private: bool = False,
*,
- encoder: Optional[Type[json.JSONEncoder]] = None,
+ encoder: type[json.JSONEncoder] | None = None,
) -> None:
"""Save JSON data to a file.
@@ -85,7 +85,7 @@ def save_json(
_LOGGER.error("JSON replacement cleanup failed: %s", err)
-def format_unserializable_data(data: Dict[str, Any]) -> str:
+def format_unserializable_data(data: dict[str, Any]) -> str:
"""Format output of find_paths in a friendly way.
Format is comma separated: =()
@@ -95,7 +95,7 @@ def format_unserializable_data(data: Dict[str, Any]) -> str:
def find_paths_unserializable_data(
bad_data: Any, *, dump: Callable[[Any], str] = json.dumps
-) -> Dict[str, Any]:
+) -> dict[str, Any]:
"""Find the paths to unserializable data.
This method is slow! Only use for error handling.
@@ -112,12 +112,15 @@ def find_paths_unserializable_data(
except (ValueError, TypeError):
pass
- # We convert states and events to dict so we can find bad data inside it
- if isinstance(obj, State):
- obj_path += f"(state: {obj.entity_id})"
- obj = obj.as_dict()
- elif isinstance(obj, Event):
- obj_path += f"(event: {obj.event_type})"
+ # We convert objects with as_dict to their dict values so we can find bad data inside it
+ if hasattr(obj, "as_dict"):
+ desc = obj.__class__.__name__
+ if isinstance(obj, State):
+ desc += f": {obj.entity_id}"
+ elif isinstance(obj, Event):
+ desc += f": {obj.event_type}"
+
+ obj_path += f"({desc})"
obj = obj.as_dict()
if isinstance(obj, dict):
diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py
index 85db07e2d4260b..c22f5213130b59 100644
--- a/homeassistant/util/location.py
+++ b/homeassistant/util/location.py
@@ -3,10 +3,12 @@
detect_location_info and elevation are mocked by default during tests.
"""
+from __future__ import annotations
+
import asyncio
import collections
import math
-from typing import Any, Dict, Optional, Tuple
+from typing import Any
import aiohttp
@@ -47,7 +49,7 @@
async def async_detect_location_info(
session: aiohttp.ClientSession,
-) -> Optional[LocationInfo]:
+) -> LocationInfo | None:
"""Detect location information."""
data = await _get_ipapi(session)
@@ -63,8 +65,8 @@ async def async_detect_location_info(
def distance(
- lat1: Optional[float], lon1: Optional[float], lat2: float, lon2: float
-) -> Optional[float]:
+ lat1: float | None, lon1: float | None, lat2: float, lon2: float
+) -> float | None:
"""Calculate the distance in meters between two points.
Async friendly.
@@ -81,8 +83,8 @@ def distance(
# Source: https://github.com/maurycyp/vincenty
# License: https://github.com/maurycyp/vincenty/blob/master/LICENSE
def vincenty(
- point1: Tuple[float, float], point2: Tuple[float, float], miles: bool = False
-) -> Optional[float]:
+ point1: tuple[float, float], point2: tuple[float, float], miles: bool = False
+) -> float | None:
"""
Vincenty formula (inverse method) to calculate the distance.
@@ -162,7 +164,7 @@ def vincenty(
return round(s, 6)
-async def _get_ipapi(session: aiohttp.ClientSession) -> Optional[Dict[str, Any]]:
+async def _get_ipapi(session: aiohttp.ClientSession) -> dict[str, Any] | None:
"""Query ipapi.co for location data."""
try:
resp = await session.get(IPAPI, timeout=5)
@@ -192,7 +194,7 @@ async def _get_ipapi(session: aiohttp.ClientSession) -> Optional[Dict[str, Any]]
}
-async def _get_ip_api(session: aiohttp.ClientSession) -> Optional[Dict[str, Any]]:
+async def _get_ip_api(session: aiohttp.ClientSession) -> dict[str, Any] | None:
"""Query ip-api.com for location data."""
try:
resp = await session.get(IP_API, timeout=5)
diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py
index feef339a200e53..816af95718d6bf 100644
--- a/homeassistant/util/logging.py
+++ b/homeassistant/util/logging.py
@@ -1,4 +1,6 @@
"""Logging utilities."""
+from __future__ import annotations
+
import asyncio
from functools import partial, wraps
import inspect
@@ -6,10 +8,10 @@
import logging.handlers
import queue
import traceback
-from typing import Any, Callable, Coroutine
+from typing import Any, Awaitable, Callable, Coroutine, cast, overload
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import HomeAssistant, callback, is_callback
class HideSensitiveDataFilter(logging.Filter):
@@ -30,13 +32,6 @@ def filter(self, record: logging.LogRecord) -> bool:
class HomeAssistantQueueHandler(logging.handlers.QueueHandler):
"""Process the log in another thread."""
- def emit(self, record: logging.LogRecord) -> None:
- """Emit a log record."""
- try:
- self.enqueue(record)
- except Exception: # pylint: disable=broad-except
- self.handleError(record)
-
def handle(self, record: logging.LogRecord) -> Any:
"""
Conditionally emit the specified logging record.
@@ -90,7 +85,7 @@ def _async_stop_queue_handler(_: Any) -> None:
def log_exception(format_err: Callable[..., Any], *args: Any) -> None:
"""Log an exception with additional context."""
- module = inspect.getmodule(inspect.stack()[1][0])
+ module = inspect.getmodule(inspect.stack(context=0)[1].frame)
if module is not None:
module_name = module.__name__
else:
@@ -106,9 +101,23 @@ def log_exception(format_err: Callable[..., Any], *args: Any) -> None:
logging.getLogger(module_name).error("%s\n%s", friendly_msg, exc_msg)
+@overload
+def catch_log_exception( # type: ignore
+ func: Callable[..., Awaitable[Any]], format_err: Callable[..., Any], *args: Any
+) -> Callable[..., Awaitable[None]]:
+ """Overload for Callables that return an Awaitable."""
+
+
+@overload
def catch_log_exception(
func: Callable[..., Any], format_err: Callable[..., Any], *args: Any
-) -> Callable[[], None]:
+) -> Callable[..., None]:
+ """Overload for Callables that return Any."""
+
+
+def catch_log_exception(
+ func: Callable[..., Any], format_err: Callable[..., Any], *args: Any
+) -> Callable[..., None] | Callable[..., Awaitable[None]]:
"""Decorate a callback to catch and log exceptions."""
# Check for partials to properly determine if coroutine function
@@ -116,18 +125,20 @@ def catch_log_exception(
while isinstance(check_func, partial):
check_func = check_func.func
- wrapper_func = None
+ wrapper_func: Callable[..., None] | Callable[..., Awaitable[None]]
if asyncio.iscoroutinefunction(check_func):
+ async_func = cast(Callable[..., Awaitable[None]], func)
- @wraps(func)
+ @wraps(async_func)
async def async_wrapper(*args: Any) -> None:
"""Catch and log exception."""
try:
- await func(*args)
+ await async_func(*args)
except Exception: # pylint: disable=broad-except
log_exception(format_err, *args)
wrapper_func = async_wrapper
+
else:
@wraps(func)
@@ -138,6 +149,9 @@ def wrapper(*args: Any) -> None:
except Exception: # pylint: disable=broad-except
log_exception(format_err, *args)
+ if is_callback(check_func):
+ wrapper = callback(wrapper)
+
wrapper_func = wrapper
return wrapper_func
diff --git a/homeassistant/util/network.py b/homeassistant/util/network.py
index 94b43ad78033b5..e714b6b6b31527 100644
--- a/homeassistant/util/network.py
+++ b/homeassistant/util/network.py
@@ -1,6 +1,7 @@
"""Network utilities."""
+from __future__ import annotations
+
from ipaddress import IPv4Address, IPv6Address, ip_address, ip_network
-from typing import Union
import yarl
@@ -23,26 +24,31 @@
LINK_LOCAL_NETWORK = ip_network("169.254.0.0/16")
-def is_loopback(address: Union[IPv4Address, IPv6Address]) -> bool:
+def is_loopback(address: IPv4Address | IPv6Address) -> bool:
"""Check if an address is a loopback address."""
return any(address in network for network in LOOPBACK_NETWORKS)
-def is_private(address: Union[IPv4Address, IPv6Address]) -> bool:
+def is_private(address: IPv4Address | IPv6Address) -> bool:
"""Check if an address is a private address."""
return any(address in network for network in PRIVATE_NETWORKS)
-def is_link_local(address: Union[IPv4Address, IPv6Address]) -> bool:
+def is_link_local(address: IPv4Address | IPv6Address) -> bool:
"""Check if an address is link local."""
return address in LINK_LOCAL_NETWORK
-def is_local(address: Union[IPv4Address, IPv6Address]) -> bool:
+def is_local(address: IPv4Address | IPv6Address) -> bool:
"""Check if an address is loopback or private."""
return is_loopback(address) or is_private(address)
+def is_invalid(address: IPv4Address | IPv6Address) -> bool:
+ """Check if an address is invalid."""
+ return bool(address == ip_address("0.0.0.0"))
+
+
def is_ip_address(address: str) -> bool:
"""Check if a given string is an IP address."""
try:
diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py
index 5391d92ed89cbf..99afcd0fcf8713 100644
--- a/homeassistant/util/package.py
+++ b/homeassistant/util/package.py
@@ -1,4 +1,6 @@
"""Helpers to install PyPi packages."""
+from __future__ import annotations
+
import asyncio
from importlib.metadata import PackageNotFoundError, version
import logging
@@ -6,7 +8,6 @@
from pathlib import Path
from subprocess import PIPE, Popen
import sys
-from typing import Optional
from urllib.parse import urlparse
import pkg_resources
@@ -34,6 +35,9 @@ def is_installed(package: str) -> bool:
Returns False when the package is not installed or doesn't meet req.
"""
try:
+ pkg_resources.get_distribution(package)
+ return True
+ except (pkg_resources.ResolutionError, pkg_resources.ExtractionError):
req = pkg_resources.Requirement.parse(package)
except ValueError:
# This is a zip file. We no longer use this in Home Assistant,
@@ -41,7 +45,14 @@ def is_installed(package: str) -> bool:
req = pkg_resources.Requirement.parse(urlparse(package).fragment)
try:
- return version(req.project_name) in req
+ installed_version = version(req.project_name)
+ # This will happen when an install failed or
+ # was aborted while in progress see
+ # https://github.com/home-assistant/core/issues/47699
+ if installed_version is None:
+ _LOGGER.error("Installed version for %s resolved to None", req.project_name) # type: ignore
+ return False
+ return installed_version in req
except PackageNotFoundError:
return False
@@ -49,10 +60,10 @@ def is_installed(package: str) -> bool:
def install_package(
package: str,
upgrade: bool = True,
- target: Optional[str] = None,
- constraints: Optional[str] = None,
- find_links: Optional[str] = None,
- no_cache_dir: Optional[bool] = False,
+ target: str | None = None,
+ constraints: str | None = None,
+ find_links: str | None = None,
+ no_cache_dir: bool | None = False,
) -> bool:
"""Install a package on PyPi. Accepts pip compatible package strings.
diff --git a/homeassistant/util/percentage.py b/homeassistant/util/percentage.py
index fa4c9dcc252c32..ec05a2dc2ec608 100644
--- a/homeassistant/util/percentage.py
+++ b/homeassistant/util/percentage.py
@@ -1,9 +1,8 @@
"""Percentage util functions."""
+from __future__ import annotations
-from typing import List, Tuple
-
-def ordered_list_item_to_percentage(ordered_list: List[str], item: str) -> int:
+def ordered_list_item_to_percentage(ordered_list: list[str], item: str) -> int:
"""Determine the percentage of an item in an ordered list.
When using this utility for fan speeds, do not include "off"
@@ -19,14 +18,14 @@ def ordered_list_item_to_percentage(ordered_list: List[str], item: str) -> int:
"""
if item not in ordered_list:
- raise ValueError
+ raise ValueError(f'The item "{item}"" is not in "{ordered_list}"')
list_len = len(ordered_list)
list_position = ordered_list.index(item) + 1
return (list_position * 100) // list_len
-def percentage_to_ordered_list_item(ordered_list: List[str], percentage: int) -> str:
+def percentage_to_ordered_list_item(ordered_list: list[str], percentage: int) -> str:
"""Find the item that most closely matches the percentage in an ordered list.
When using this utility for fan speeds, do not include "off"
@@ -42,7 +41,7 @@ def percentage_to_ordered_list_item(ordered_list: List[str], percentage: int) ->
"""
list_len = len(ordered_list)
if not list_len:
- raise ValueError
+ raise ValueError("The ordered list is empty")
for offset, speed in enumerate(ordered_list):
list_position = offset + 1
@@ -54,7 +53,7 @@ def percentage_to_ordered_list_item(ordered_list: List[str], percentage: int) ->
def ranged_value_to_percentage(
- low_high_range: Tuple[float, float], value: float
+ low_high_range: tuple[float, float], value: float
) -> int:
"""Given a range of low and high values convert a single value to a percentage.
@@ -67,11 +66,12 @@ def ranged_value_to_percentage(
(1,255), 127: 50
(1,255), 10: 4
"""
- return int((value * 100) // (low_high_range[1] - low_high_range[0] + 1))
+ offset = low_high_range[0] - 1
+ return int(((value - offset) * 100) // states_in_range(low_high_range))
def percentage_to_ranged_value(
- low_high_range: Tuple[float, float], percentage: int
+ low_high_range: tuple[float, float], percentage: int
) -> float:
"""Given a range of low and high values convert a percentage to a single value.
@@ -84,4 +84,15 @@ def percentage_to_ranged_value(
(1,255), 50: 127.5
(1,255), 4: 10.2
"""
- return (low_high_range[1] - low_high_range[0] + 1) * percentage / 100
+ offset = low_high_range[0] - 1
+ return states_in_range(low_high_range) * percentage / 100 + offset
+
+
+def states_in_range(low_high_range: tuple[float, float]) -> float:
+ """Given a range of low and high values return how many states exist."""
+ return low_high_range[1] - low_high_range[0] + 1
+
+
+def int_states_in_range(low_high_range: tuple[float, float]) -> int:
+ """Given a range of low and high values return how many integer states exist."""
+ return int(states_in_range(low_high_range))
diff --git a/homeassistant/util/pil.py b/homeassistant/util/pil.py
index 80c11c9c4104c0..7caeac15458aa5 100644
--- a/homeassistant/util/pil.py
+++ b/homeassistant/util/pil.py
@@ -2,18 +2,18 @@
Can only be used by integrations that have pillow in their requirements.
"""
-from typing import Tuple
+from __future__ import annotations
from PIL import ImageDraw
def draw_box(
draw: ImageDraw,
- box: Tuple[float, float, float, float],
+ box: tuple[float, float, float, float],
img_width: int,
img_height: int,
text: str = "",
- color: Tuple[int, int, int] = (255, 255, 0),
+ color: tuple[int, int, int] = (255, 255, 0),
) -> None:
"""
Draw a bounding box on and image.
diff --git a/homeassistant/util/ruamel_yaml.py b/homeassistant/util/ruamel_yaml.py
index 496ca377936cd4..7bb49b0545beba 100644
--- a/homeassistant/util/ruamel_yaml.py
+++ b/homeassistant/util/ruamel_yaml.py
@@ -1,9 +1,12 @@
"""ruamel.yaml utility functions."""
+from __future__ import annotations
+
from collections import OrderedDict
+from contextlib import suppress
import logging
import os
from os import O_CREAT, O_TRUNC, O_WRONLY, stat_result
-from typing import Dict, List, Optional, Union
+from typing import Dict, List, Union
import ruamel.yaml
from ruamel.yaml import YAML # type: ignore
@@ -22,7 +25,7 @@
class ExtSafeConstructor(SafeConstructor):
"""Extended SafeConstructor."""
- name: Optional[str] = None
+ name: str | None = None
class UnsupportedYamlError(HomeAssistantError):
@@ -77,7 +80,7 @@ def yaml_to_object(data: str) -> JSON_TYPE:
"""Create object from yaml string."""
yaml = YAML(typ="rt")
try:
- result: Union[List, Dict, str] = yaml.load(data)
+ result: list | dict | str = yaml.load(data)
return result
except YAMLError as exc:
_LOGGER.error("YAML error: %s", exc)
@@ -126,10 +129,8 @@ def save_yaml(fname: str, data: JSON_TYPE) -> None:
yaml.dump(data, temp_file)
os.replace(tmp_fname, fname)
if hasattr(os, "chown") and file_stat.st_ctime > -1:
- try:
+ with suppress(OSError):
os.chown(fname, file_stat.st_uid, file_stat.st_gid)
- except OSError:
- pass
except YAMLError as exc:
_LOGGER.error(str(exc))
raise HomeAssistantError(exc) from exc
diff --git a/homeassistant/util/ssl.py b/homeassistant/util/ssl.py
index 7b987d8eeb218f..4f10809ff21172 100644
--- a/homeassistant/util/ssl.py
+++ b/homeassistant/util/ssl.py
@@ -23,7 +23,7 @@ def server_context_modern() -> ssl.SSLContext:
https://wiki.mozilla.org/Security/Server_Side_TLS
Modern guidelines are followed.
"""
- context = ssl.SSLContext(ssl.PROTOCOL_TLS) # pylint: disable=no-member
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS)
context.options |= (
ssl.OP_NO_SSLv2
@@ -53,7 +53,7 @@ def server_context_intermediate() -> ssl.SSLContext:
https://wiki.mozilla.org/Security/Server_Side_TLS
Intermediate guidelines are followed.
"""
- context = ssl.SSLContext(ssl.PROTOCOL_TLS) # pylint: disable=no-member
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS)
context.options |= (
ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_CIPHER_SERVER_PREFERENCE
diff --git a/homeassistant/util/timeout.py b/homeassistant/util/timeout.py
index d8fc3e48fe6c26..5821f89659e7a2 100644
--- a/homeassistant/util/timeout.py
+++ b/homeassistant/util/timeout.py
@@ -8,7 +8,7 @@
import asyncio
import enum
from types import TracebackType
-from typing import Any, Dict, List, Optional, Type, Union
+from typing import Any
from .async_ import run_callback_threadsafe
@@ -38,10 +38,10 @@ async def __aenter__(self) -> _GlobalFreezeContext:
async def __aexit__(
self,
- exc_type: Type[BaseException],
+ exc_type: type[BaseException],
exc_val: BaseException,
exc_tb: TracebackType,
- ) -> Optional[bool]:
+ ) -> bool | None:
self._exit()
return None
@@ -51,10 +51,10 @@ def __enter__(self) -> _GlobalFreezeContext:
def __exit__( # pylint: disable=useless-return
self,
- exc_type: Type[BaseException],
+ exc_type: type[BaseException],
exc_val: BaseException,
exc_tb: TracebackType,
- ) -> Optional[bool]:
+ ) -> bool | None:
self._loop.call_soon_threadsafe(self._exit)
return None
@@ -106,10 +106,10 @@ async def __aenter__(self) -> _ZoneFreezeContext:
async def __aexit__(
self,
- exc_type: Type[BaseException],
+ exc_type: type[BaseException],
exc_val: BaseException,
exc_tb: TracebackType,
- ) -> Optional[bool]:
+ ) -> bool | None:
self._exit()
return None
@@ -119,10 +119,10 @@ def __enter__(self) -> _ZoneFreezeContext:
def __exit__( # pylint: disable=useless-return
self,
- exc_type: Type[BaseException],
+ exc_type: type[BaseException],
exc_val: BaseException,
exc_tb: TracebackType,
- ) -> Optional[bool]:
+ ) -> bool | None:
self._loop.call_soon_threadsafe(self._exit)
return None
@@ -155,8 +155,8 @@ def __init__(
self._manager: TimeoutManager = manager
self._task: asyncio.Task[Any] = task
self._time_left: float = timeout
- self._expiration_time: Optional[float] = None
- self._timeout_handler: Optional[asyncio.Handle] = None
+ self._expiration_time: float | None = None
+ self._timeout_handler: asyncio.Handle | None = None
self._wait_zone: asyncio.Event = asyncio.Event()
self._state: _State = _State.INIT
self._cool_down: float = cool_down
@@ -169,10 +169,10 @@ async def __aenter__(self) -> _GlobalTaskContext:
async def __aexit__(
self,
- exc_type: Type[BaseException],
+ exc_type: type[BaseException],
exc_val: BaseException,
exc_tb: TracebackType,
- ) -> Optional[bool]:
+ ) -> bool | None:
self._stop_timer()
self._manager.global_tasks.remove(self)
@@ -243,7 +243,7 @@ async def _on_wait(self) -> None:
"""Wait until zones are done."""
await self._wait_zone.wait()
await asyncio.sleep(self._cool_down) # Allow context switch
- if not self.state == _State.TIMEOUT:
+ if self.state != _State.TIMEOUT:
return
self._cancel_task()
@@ -263,8 +263,8 @@ def __init__(
self._task: asyncio.Task[Any] = task
self._state: _State = _State.INIT
self._time_left: float = timeout
- self._expiration_time: Optional[float] = None
- self._timeout_handler: Optional[asyncio.Handle] = None
+ self._expiration_time: float | None = None
+ self._timeout_handler: asyncio.Handle | None = None
@property
def state(self) -> _State:
@@ -283,10 +283,10 @@ async def __aenter__(self) -> _ZoneTaskContext:
async def __aexit__(
self,
- exc_type: Type[BaseException],
+ exc_type: type[BaseException],
exc_val: BaseException,
exc_tb: TracebackType,
- ) -> Optional[bool]:
+ ) -> bool | None:
self._zone.exit_task(self)
self._stop_timer()
@@ -344,8 +344,8 @@ def __init__(self, manager: TimeoutManager, zone: str) -> None:
"""Initialize internal timeout context manager."""
self._manager: TimeoutManager = manager
self._zone: str = zone
- self._tasks: List[_ZoneTaskContext] = []
- self._freezes: List[_ZoneFreezeContext] = []
+ self._tasks: list[_ZoneTaskContext] = []
+ self._freezes: list[_ZoneFreezeContext] = []
def __repr__(self) -> str:
"""Representation of a zone."""
@@ -418,9 +418,9 @@ class TimeoutManager:
def __init__(self) -> None:
"""Initialize TimeoutManager."""
self._loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
- self._zones: Dict[str, _ZoneTimeoutManager] = {}
- self._globals: List[_GlobalTaskContext] = []
- self._freezes: List[_GlobalFreezeContext] = []
+ self._zones: dict[str, _ZoneTimeoutManager] = {}
+ self._globals: list[_GlobalTaskContext] = []
+ self._freezes: list[_GlobalFreezeContext] = []
@property
def zones_done(self) -> bool:
@@ -433,17 +433,17 @@ def freezes_done(self) -> bool:
return not self._freezes
@property
- def zones(self) -> Dict[str, _ZoneTimeoutManager]:
+ def zones(self) -> dict[str, _ZoneTimeoutManager]:
"""Return all Zones."""
return self._zones
@property
- def global_tasks(self) -> List[_GlobalTaskContext]:
+ def global_tasks(self) -> list[_GlobalTaskContext]:
"""Return all global Tasks."""
return self._globals
@property
- def global_freezes(self) -> List[_GlobalFreezeContext]:
+ def global_freezes(self) -> list[_GlobalFreezeContext]:
"""Return all global Freezes."""
return self._freezes
@@ -459,12 +459,12 @@ def drop_zone(self, zone_name: str) -> None:
def async_timeout(
self, timeout: float, zone_name: str = ZONE_GLOBAL, cool_down: float = 0
- ) -> Union[_ZoneTaskContext, _GlobalTaskContext]:
+ ) -> _ZoneTaskContext | _GlobalTaskContext:
"""Timeout based on a zone.
For using as Async Context Manager.
"""
- current_task: Optional[asyncio.Task[Any]] = asyncio.current_task()
+ current_task: asyncio.Task[Any] | None = asyncio.current_task()
assert current_task
# Global Zone
@@ -483,7 +483,7 @@ def async_timeout(
def async_freeze(
self, zone_name: str = ZONE_GLOBAL
- ) -> Union[_ZoneFreezeContext, _GlobalFreezeContext]:
+ ) -> _ZoneFreezeContext | _GlobalFreezeContext:
"""Freeze all timer until job is done.
For using as Async Context Manager.
@@ -502,7 +502,7 @@ def async_freeze(
def freeze(
self, zone_name: str = ZONE_GLOBAL
- ) -> Union[_ZoneFreezeContext, _GlobalFreezeContext]:
+ ) -> _ZoneFreezeContext | _GlobalFreezeContext:
"""Freeze all timer until job is done.
For using as Context Manager.
diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py
index 5cba1bfeb19cf9..b5c8c38425a003 100644
--- a/homeassistant/util/unit_system.py
+++ b/homeassistant/util/unit_system.py
@@ -1,6 +1,7 @@
"""Unit system helper class and methods."""
+from __future__ import annotations
+
from numbers import Number
-from typing import Dict, Optional
from homeassistant.const import (
CONF_UNIT_SYSTEM_IMPERIAL,
@@ -109,7 +110,7 @@ def temperature(self, temperature: float, from_unit: str) -> float:
return temperature_util.convert(temperature, from_unit, self.temperature_unit)
- def length(self, length: Optional[float], from_unit: str) -> float:
+ def length(self, length: float | None, from_unit: str) -> float:
"""Convert the given length to this unit system."""
if not isinstance(length, Number):
raise TypeError(f"{length!s} is not a numeric value.")
@@ -119,7 +120,7 @@ def length(self, length: Optional[float], from_unit: str) -> float:
length, from_unit, self.length_unit
)
- def pressure(self, pressure: Optional[float], from_unit: str) -> float:
+ def pressure(self, pressure: float | None, from_unit: str) -> float:
"""Convert the given pressure to this unit system."""
if not isinstance(pressure, Number):
raise TypeError(f"{pressure!s} is not a numeric value.")
@@ -129,7 +130,7 @@ def pressure(self, pressure: Optional[float], from_unit: str) -> float:
pressure, from_unit, self.pressure_unit
)
- def volume(self, volume: Optional[float], from_unit: str) -> float:
+ def volume(self, volume: float | None, from_unit: str) -> float:
"""Convert the given volume to this unit system."""
if not isinstance(volume, Number):
raise TypeError(f"{volume!s} is not a numeric value.")
@@ -137,7 +138,7 @@ def volume(self, volume: Optional[float], from_unit: str) -> float:
# type ignore: https://github.com/python/mypy/issues/7207
return volume_util.convert(volume, from_unit, self.volume_unit) # type: ignore
- def as_dict(self) -> Dict[str, str]:
+ def as_dict(self) -> dict[str, str]:
"""Convert the unit system to a dictionary."""
return {
LENGTH: self.length_unit,
diff --git a/homeassistant/util/yaml/__init__.py b/homeassistant/util/yaml/__init__.py
index ac4ac2f9a16951..b3f1b7ecd43768 100644
--- a/homeassistant/util/yaml/__init__.py
+++ b/homeassistant/util/yaml/__init__.py
@@ -1,17 +1,16 @@
"""YAML utility functions."""
-from .const import _SECRET_NAMESPACE, SECRET_YAML
+from .const import SECRET_YAML
from .dumper import dump, save_yaml
from .input import UndefinedSubstitution, extract_inputs, substitute
-from .loader import clear_secret_cache, load_yaml, parse_yaml, secret_yaml
+from .loader import Secrets, load_yaml, parse_yaml, secret_yaml
from .objects import Input
__all__ = [
"SECRET_YAML",
- "_SECRET_NAMESPACE",
"Input",
"dump",
"save_yaml",
- "clear_secret_cache",
+ "Secrets",
"load_yaml",
"secret_yaml",
"parse_yaml",
diff --git a/homeassistant/util/yaml/const.py b/homeassistant/util/yaml/const.py
index bf1615edb9343b..9d930b50fd6f63 100644
--- a/homeassistant/util/yaml/const.py
+++ b/homeassistant/util/yaml/const.py
@@ -1,4 +1,2 @@
"""Constants."""
SECRET_YAML = "secrets.yaml"
-
-_SECRET_NAMESPACE = "homeassistant"
diff --git a/homeassistant/util/yaml/input.py b/homeassistant/util/yaml/input.py
index 6282509fae2e1f..ab5948db605f52 100644
--- a/homeassistant/util/yaml/input.py
+++ b/homeassistant/util/yaml/input.py
@@ -1,6 +1,7 @@
"""Deal with YAML input."""
+from __future__ import annotations
-from typing import Any, Dict, Set
+from typing import Any
from .objects import Input
@@ -14,14 +15,14 @@ def __init__(self, input_name: str) -> None:
self.input = input
-def extract_inputs(obj: Any) -> Set[str]:
+def extract_inputs(obj: Any) -> set[str]:
"""Extract input from a structure."""
- found: Set[str] = set()
+ found: set[str] = set()
_extract_inputs(obj, found)
return found
-def _extract_inputs(obj: Any, found: Set[str]) -> None:
+def _extract_inputs(obj: Any, found: set[str]) -> None:
"""Extract input from a structure."""
if isinstance(obj, Input):
found.add(obj.name)
@@ -38,7 +39,7 @@ def _extract_inputs(obj: Any, found: Set[str]) -> None:
return
-def substitute(obj: Any, substitutions: Dict[str, Any]) -> Any:
+def substitute(obj: Any, substitutions: dict[str, Any]) -> Any:
"""Substitute values."""
if isinstance(obj, Input):
if obj.name not in substitutions:
diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py
index 746806f527def0..b03e93f17dfaa4 100644
--- a/homeassistant/util/yaml/loader.py
+++ b/homeassistant/util/yaml/loader.py
@@ -1,52 +1,102 @@
"""Custom loader."""
+from __future__ import annotations
+
from collections import OrderedDict
import fnmatch
import logging
import os
-import sys
-from typing import Dict, Iterator, List, TextIO, TypeVar, Union, overload
+from pathlib import Path
+from typing import Any, Dict, Iterator, List, TextIO, TypeVar, Union, overload
import yaml
from homeassistant.exceptions import HomeAssistantError
-from .const import _SECRET_NAMESPACE, SECRET_YAML
+from .const import SECRET_YAML
from .objects import Input, NodeListClass, NodeStrClass
-try:
- import keyring
-except ImportError:
- keyring = None
-
-try:
- import credstash
-except ImportError:
- credstash = None
-
-
# mypy: allow-untyped-calls, no-warn-return-any
JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name
DICT_T = TypeVar("DICT_T", bound=Dict) # pylint: disable=invalid-name
_LOGGER = logging.getLogger(__name__)
-__SECRET_CACHE: Dict[str, JSON_TYPE] = {}
-CREDSTASH_WARN = False
-KEYRING_WARN = False
+class Secrets:
+ """Store secrets while loading YAML."""
-def clear_secret_cache() -> None:
- """Clear the secret cache.
+ def __init__(self, config_dir: Path):
+ """Initialize secrets."""
+ self.config_dir = config_dir
+ self._cache: dict[Path, dict[str, str]] = {}
- Async friendly.
- """
- __SECRET_CACHE.clear()
+ def get(self, requester_path: str, secret: str) -> str:
+ """Return the value of a secret."""
+ current_path = Path(requester_path)
+
+ secret_dir = current_path
+ while True:
+ secret_dir = secret_dir.parent
+
+ try:
+ secret_dir.relative_to(self.config_dir)
+ except ValueError:
+ # We went above the config dir
+ break
+
+ secrets = self._load_secret_yaml(secret_dir)
+
+ if secret in secrets:
+ _LOGGER.debug(
+ "Secret %s retrieved from secrets.yaml in folder %s",
+ secret,
+ secret_dir,
+ )
+ return secrets[secret]
+
+ raise HomeAssistantError(f"Secret {secret} not defined")
+
+ def _load_secret_yaml(self, secret_dir: Path) -> dict[str, str]:
+ """Load the secrets yaml from path."""
+ secret_path = secret_dir / SECRET_YAML
+
+ if secret_path in self._cache:
+ return self._cache[secret_path]
+
+ _LOGGER.debug("Loading %s", secret_path)
+ try:
+ secrets = load_yaml(str(secret_path))
+
+ if not isinstance(secrets, dict):
+ raise HomeAssistantError("Secrets is not a dictionary")
+
+ if "logger" in secrets:
+ logger = str(secrets["logger"]).lower()
+ if logger == "debug":
+ _LOGGER.setLevel(logging.DEBUG)
+ else:
+ _LOGGER.error(
+ "Error in secrets.yaml: 'logger: debug' expected, but 'logger: %s' found",
+ logger,
+ )
+ del secrets["logger"]
+ except FileNotFoundError:
+ secrets = {}
+
+ self._cache[secret_path] = secrets
+
+ return secrets
class SafeLineLoader(yaml.SafeLoader):
"""Loader class that keeps track of line numbers."""
+ def __init__(self, stream: Any, secrets: Secrets | None = None) -> None:
+ """Initialize a safe line loader."""
+ super().__init__(stream)
+ self.secrets = secrets
+
def compose_node(self, parent: yaml.nodes.Node, index: int) -> yaml.nodes.Node:
"""Annotate a node with the first line it was seen."""
last_line: int = self.line
@@ -55,22 +105,25 @@ def compose_node(self, parent: yaml.nodes.Node, index: int) -> yaml.nodes.Node:
return node
-def load_yaml(fname: str) -> JSON_TYPE:
+def load_yaml(fname: str, secrets: Secrets | None = None) -> JSON_TYPE:
"""Load a YAML file."""
try:
with open(fname, encoding="utf-8") as conf_file:
- return parse_yaml(conf_file)
+ return parse_yaml(conf_file, secrets)
except UnicodeDecodeError as exc:
_LOGGER.error("Unable to read file %s: %s", fname, exc)
raise HomeAssistantError(exc) from exc
-def parse_yaml(content: Union[str, TextIO]) -> JSON_TYPE:
+def parse_yaml(content: str | TextIO, secrets: Secrets | None = None) -> JSON_TYPE:
"""Load a YAML file."""
try:
# If configuration file is empty YAML returns None
# We convert that to an empty dict
- return yaml.load(content, Loader=SafeLineLoader) or OrderedDict()
+ return (
+ yaml.load(content, Loader=lambda stream: SafeLineLoader(stream, secrets))
+ or OrderedDict()
+ )
except yaml.YAMLError as exc:
_LOGGER.error(str(exc))
raise HomeAssistantError(exc) from exc
@@ -78,21 +131,21 @@ def parse_yaml(content: Union[str, TextIO]) -> JSON_TYPE:
@overload
def _add_reference(
- obj: Union[list, NodeListClass], loader: yaml.SafeLoader, node: yaml.nodes.Node
+ obj: list | NodeListClass, loader: SafeLineLoader, node: yaml.nodes.Node
) -> NodeListClass:
...
@overload
def _add_reference(
- obj: Union[str, NodeStrClass], loader: yaml.SafeLoader, node: yaml.nodes.Node
+ obj: str | NodeStrClass, loader: SafeLineLoader, node: yaml.nodes.Node
) -> NodeStrClass:
...
@overload
def _add_reference(
- obj: DICT_T, loader: yaml.SafeLoader, node: yaml.nodes.Node
+ obj: DICT_T, loader: SafeLineLoader, node: yaml.nodes.Node
) -> DICT_T:
...
@@ -117,7 +170,7 @@ def _include_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE:
"""
fname = os.path.join(os.path.dirname(loader.name), node.value)
try:
- return _add_reference(load_yaml(fname), loader, node)
+ return _add_reference(load_yaml(fname, loader.secrets), loader, node)
except FileNotFoundError as exc:
raise HomeAssistantError(
f"{node.start_mark}: Unable to read file {fname}."
@@ -149,7 +202,7 @@ def _include_dir_named_yaml(
filename = os.path.splitext(os.path.basename(fname))[0]
if os.path.basename(fname) == SECRET_YAML:
continue
- mapping[filename] = load_yaml(fname)
+ mapping[filename] = load_yaml(fname, loader.secrets)
return _add_reference(mapping, loader, node)
@@ -162,7 +215,7 @@ def _include_dir_merge_named_yaml(
for fname in _find_files(loc, "*.yaml"):
if os.path.basename(fname) == SECRET_YAML:
continue
- loaded_yaml = load_yaml(fname)
+ loaded_yaml = load_yaml(fname, loader.secrets)
if isinstance(loaded_yaml, dict):
mapping.update(loaded_yaml)
return _add_reference(mapping, loader, node)
@@ -170,11 +223,11 @@ def _include_dir_merge_named_yaml(
def _include_dir_list_yaml(
loader: SafeLineLoader, node: yaml.nodes.Node
-) -> List[JSON_TYPE]:
+) -> list[JSON_TYPE]:
"""Load multiple files from directory as a list."""
loc = os.path.join(os.path.dirname(loader.name), node.value)
return [
- load_yaml(f)
+ load_yaml(f, loader.secrets)
for f in _find_files(loc, "*.yaml")
if os.path.basename(f) != SECRET_YAML
]
@@ -185,11 +238,11 @@ def _include_dir_merge_list_yaml(
) -> JSON_TYPE:
"""Load multiple files from directory as a merged list."""
loc: str = os.path.join(os.path.dirname(loader.name), node.value)
- merged_list: List[JSON_TYPE] = []
+ merged_list: list[JSON_TYPE] = []
for fname in _find_files(loc, "*.yaml"):
if os.path.basename(fname) == SECRET_YAML:
continue
- loaded_yaml = load_yaml(fname)
+ loaded_yaml = load_yaml(fname, loader.secrets)
if isinstance(loaded_yaml, list):
merged_list.extend(loaded_yaml)
return _add_reference(merged_list, loader, node)
@@ -200,7 +253,7 @@ def _ordered_dict(loader: SafeLineLoader, node: yaml.nodes.MappingNode) -> Order
loader.flatten_mapping(node)
nodes = loader.construct_pairs(node)
- seen: Dict = {}
+ seen: dict = {}
for (key, _), (child_node, _) in zip(nodes, node.value):
line = child_node.start_mark.line
@@ -246,107 +299,27 @@ def _env_var_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> str:
raise HomeAssistantError(node.value)
-def _load_secret_yaml(secret_path: str) -> JSON_TYPE:
- """Load the secrets yaml from path."""
- secret_path = os.path.join(secret_path, SECRET_YAML)
- if secret_path in __SECRET_CACHE:
- return __SECRET_CACHE[secret_path]
-
- _LOGGER.debug("Loading %s", secret_path)
- try:
- secrets = load_yaml(secret_path)
- if not isinstance(secrets, dict):
- raise HomeAssistantError("Secrets is not a dictionary")
- if "logger" in secrets:
- logger = str(secrets["logger"]).lower()
- if logger == "debug":
- _LOGGER.setLevel(logging.DEBUG)
- else:
- _LOGGER.error(
- "secrets.yaml: 'logger: debug' expected, but 'logger: %s' found",
- logger,
- )
- del secrets["logger"]
- except FileNotFoundError:
- secrets = {}
- __SECRET_CACHE[secret_path] = secrets
- return secrets
-
-
def secret_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE:
"""Load secrets and embed it into the configuration YAML."""
- secret_path = os.path.dirname(loader.name)
- while True:
- secrets = _load_secret_yaml(secret_path)
-
- if node.value in secrets:
- _LOGGER.debug(
- "Secret %s retrieved from secrets.yaml in folder %s",
- node.value,
- secret_path,
- )
- return secrets[node.value]
-
- if secret_path == os.path.dirname(sys.path[0]):
- break # sys.path[0] set to config/deps folder by bootstrap
-
- secret_path = os.path.dirname(secret_path)
- if not os.path.exists(secret_path) or len(secret_path) < 5:
- break # Somehow we got past the .homeassistant config folder
-
- if keyring:
- # do some keyring stuff
- pwd = keyring.get_password(_SECRET_NAMESPACE, node.value)
- if pwd:
- global KEYRING_WARN # pylint: disable=global-statement
-
- if not KEYRING_WARN:
- KEYRING_WARN = True
- _LOGGER.warning(
- "Keyring is deprecated and will be removed in March 2021."
- )
-
- _LOGGER.debug("Secret %s retrieved from keyring", node.value)
- return pwd
-
- global credstash # pylint: disable=invalid-name, global-statement
-
- if credstash:
- # pylint: disable=no-member
- try:
- pwd = credstash.getSecret(node.value, table=_SECRET_NAMESPACE)
- if pwd:
- global CREDSTASH_WARN # pylint: disable=global-statement
-
- if not CREDSTASH_WARN:
- CREDSTASH_WARN = True
- _LOGGER.warning(
- "Credstash is deprecated and will be removed in March 2021."
- )
- _LOGGER.debug("Secret %s retrieved from credstash", node.value)
- return pwd
- except credstash.ItemNotFound:
- pass
- except Exception: # pylint: disable=broad-except
- # Catch if package installed and no config
- credstash = None
+ if loader.secrets is None:
+ raise HomeAssistantError("Secrets not supported in this YAML file")
- raise HomeAssistantError(f"Secret {node.value} not defined")
+ return loader.secrets.get(loader.name, node.value)
-yaml.SafeLoader.add_constructor("!include", _include_yaml)
-yaml.SafeLoader.add_constructor(
+SafeLineLoader.add_constructor("!include", _include_yaml)
+SafeLineLoader.add_constructor(
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _ordered_dict
)
-yaml.SafeLoader.add_constructor(
+SafeLineLoader.add_constructor(
yaml.resolver.BaseResolver.DEFAULT_SEQUENCE_TAG, _construct_seq
)
-yaml.SafeLoader.add_constructor("!env_var", _env_var_yaml)
-yaml.SafeLoader.add_constructor("!secret", secret_yaml)
-yaml.SafeLoader.add_constructor("!include_dir_list", _include_dir_list_yaml)
-yaml.SafeLoader.add_constructor("!include_dir_merge_list", _include_dir_merge_list_yaml)
-yaml.SafeLoader.add_constructor("!include_dir_named", _include_dir_named_yaml)
-yaml.SafeLoader.add_constructor(
+SafeLineLoader.add_constructor("!env_var", _env_var_yaml)
+SafeLineLoader.add_constructor("!secret", secret_yaml)
+SafeLineLoader.add_constructor("!include_dir_list", _include_dir_list_yaml)
+SafeLineLoader.add_constructor("!include_dir_merge_list", _include_dir_merge_list_yaml)
+SafeLineLoader.add_constructor("!include_dir_named", _include_dir_named_yaml)
+SafeLineLoader.add_constructor(
"!include_dir_merge_named", _include_dir_merge_named_yaml
)
-yaml.SafeLoader.add_constructor("!input", Input.from_node)
+SafeLineLoader.add_constructor("!input", Input.from_node)
diff --git a/homeassistant/util/yaml/objects.py b/homeassistant/util/yaml/objects.py
index 0e46820e0db120..2d318a9def0e24 100644
--- a/homeassistant/util/yaml/objects.py
+++ b/homeassistant/util/yaml/objects.py
@@ -1,4 +1,6 @@
"""Custom yaml object types."""
+from __future__ import annotations
+
from dataclasses import dataclass
import yaml
@@ -19,6 +21,6 @@ class Input:
name: str
@classmethod
- def from_node(cls, loader: yaml.Loader, node: yaml.nodes.Node) -> "Input":
+ def from_node(cls, loader: yaml.Loader, node: yaml.nodes.Node) -> Input:
"""Create a new placeholder from a node."""
return cls(node.value)
diff --git a/machine/generic-x86-64 b/machine/generic-x86-64
new file mode 100644
index 00000000000000..4c83228387ddb9
--- /dev/null
+++ b/machine/generic-x86-64
@@ -0,0 +1,34 @@
+ARG BUILD_VERSION
+FROM homeassistant/amd64-homeassistant:$BUILD_VERSION
+
+RUN apk --no-cache add \
+ libva-intel-driver \
+ usbutils
+
+##
+# Build libcec for HDMI-CEC
+ARG LIBCEC_VERSION=6.0.2
+RUN apk add --no-cache \
+ eudev-libs \
+ p8-platform \
+ && apk add --no-cache --virtual .build-dependencies \
+ build-base \
+ cmake \
+ eudev-dev \
+ swig \
+ p8-platform-dev \
+ linux-headers \
+ && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
+ && cd /usr/src/libcec \
+ && mkdir -p /usr/src/libcec/build \
+ && cd /usr/src/libcec/build \
+ && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
+ -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
+ -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
+ -DHAVE_LINUX_API=1 \
+ .. \
+ && make -j$(nproc) \
+ && make install \
+ && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
+ && apk del .build-dependencies \
+ && rm -rf /usr/src/libcec*
diff --git a/machine/intel-nuc b/machine/intel-nuc
index 4c83228387ddb9..b5538b8ccad233 100644
--- a/machine/intel-nuc
+++ b/machine/intel-nuc
@@ -1,6 +1,9 @@
ARG BUILD_VERSION
FROM homeassistant/amd64-homeassistant:$BUILD_VERSION
+# NOTE: intel-nuc will be replaced by generic-x86-64. Make sure to apply
+# changes in generic-x86-64 as well.
+
RUN apk --no-cache add \
libva-intel-driver \
usbutils
diff --git a/pylint/plugins/hass_logger.py b/pylint/plugins/hass_logger.py
new file mode 100644
index 00000000000000..b771b07aa5ee73
--- /dev/null
+++ b/pylint/plugins/hass_logger.py
@@ -0,0 +1,85 @@
+import astroid
+from pylint.checkers import BaseChecker
+from pylint.interfaces import IAstroidChecker
+
+LOGGER_NAMES = ("LOGGER", "_LOGGER")
+LOG_LEVEL_ALLOWED_LOWER_START = ("debug",)
+
+# This is our checker class.
+# Checkers should always inherit from `BaseChecker`.
+class HassLoggerFormatChecker(BaseChecker):
+ """Add class member attributes to the class locals dictionary."""
+
+ __implements__ = IAstroidChecker
+
+ # The name defines a custom section of the config for this checker.
+ name = "hass_logger"
+ priority = -1
+ msgs = {
+ "W0001": (
+ "User visible logger messages must not end with a period",
+ "hass-logger-period",
+ "Periods are not permitted at the end of logger messages",
+ ),
+ "W0002": (
+ "User visible logger messages must start with a capital letter or downgrade to debug",
+ "hass-logger-capital",
+ "All logger messages must start with a capital letter",
+ ),
+ }
+ options = (
+ (
+ "hass-logger",
+ {
+ "default": "properties",
+ "help": (
+ "Validate _LOGGER or LOGGER messages conform to Home Assistant standards."
+ ),
+ },
+ ),
+ )
+
+ def visit_call(self, node):
+ """Called when a :class:`.astroid.node_classes.Call` node is visited.
+ See :mod:`astroid` for the description of available nodes.
+ :param node: The node to check.
+ :type node: astroid.node_classes.Call
+ """
+ if not isinstance(node.func, astroid.Attribute) or not isinstance(
+ node.func.expr, astroid.Name
+ ):
+ return
+
+ if not node.func.expr.name in LOGGER_NAMES:
+ return
+
+ if not node.args:
+ return
+
+ first_arg = node.args[0]
+
+ if not isinstance(first_arg, astroid.Const) or not first_arg.value:
+ return
+
+ log_message = first_arg.value
+
+ if len(log_message) < 1:
+ return
+
+ if log_message[-1] == ".":
+ self.add_message("hass-logger-period", args=node.args, node=node)
+
+ if (
+ isinstance(node.func.attrname, str)
+ and node.func.attrname not in LOG_LEVEL_ALLOWED_LOWER_START
+ and log_message[0].upper() != log_message[0]
+ ):
+ self.add_message("hass-logger-capital", args=node.args, node=node)
+
+
+def register(linter):
+ """This required method auto registers the checker.
+ :param linter: The linter to register the checker to.
+ :type linter: pylint.lint.PyLinter
+ """
+ linter.register_checker(HassLoggerFormatChecker(linter))
diff --git a/pyproject.toml b/pyproject.toml
index 445f13e8724943..217cbebe3b0a6a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,10 +22,11 @@ ignore = [
]
# Use a conservative default here; 2 should speed up most setups and not hurt
# any too bad. Override on command line as appropriate.
-# Disabled for now: https://github.com/PyCQA/pylint/issues/3584
-#jobs = 2
+jobs = 2
+init-hook='from pylint.config.find_default_config_files import find_default_config_files; from pathlib import Path; import sys; sys.path.append(str(Path(Path(list(find_default_config_files())[0]).parent, "pylint/plugins")))'
load-plugins = [
"pylint_strict_informational",
+ "hass_logger"
]
persistent = false
extension-pkg-whitelist = [
@@ -34,6 +35,7 @@ extension-pkg-whitelist = [
]
[tool.pylint.BASIC]
+class-const-naming-style = "any"
good-names = [
"_",
"ev",
diff --git a/requirements.txt b/requirements.txt
index ef7d73e1cb501d..a3facbe5ab23f6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,20 +1,21 @@
-c homeassistant/package_constraints.txt
# Home Assistant Core
-aiohttp==3.7.3
-astral==1.10.1
+aiohttp==3.7.4.post0
+astral==2.2
async_timeout==3.0.1
-attrs==19.3.0
+attrs==20.3.0
+awesomeversion==21.2.3
bcrypt==3.1.7
certifi>=2020.12.5
ciso8601==2.1.3
-httpx==0.16.1
-jinja2>=2.11.2
+httpx==0.17.1
+jinja2>=2.11.3
PyJWT==1.7.1
-cryptography==3.3.1
+cryptography==3.3.2
pip>=8.0.3,<20.3
python-slugify==4.0.1
-pytz>=2020.5
+pytz>=2021.1
pyyaml==5.4.1
requests==2.25.1
ruamel.yaml==0.15.100
diff --git a/requirements_all.txt b/requirements_all.txt
index 2db6ddc37e62d7..db66c39447f443 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1,8 +1,8 @@
# Home Assistant Core, full dependency set
-r requirements.txt
-# homeassistant.components.nuimo_controller
-# --only-binary=all nuimo==0.1.0
+# homeassistant.components.aemet
+AEMET-OpenData==0.1.8
# homeassistant.components.dht
# Adafruit-DHT==1.4.0
@@ -17,7 +17,7 @@ Adafruit-SHT31==1.0.2
# Adafruit_BBIO==1.1.1
# homeassistant.components.homekit
-HAP-python==3.1.0
+HAP-python==3.4.1
# homeassistant.components.mastodon
Mastodon.py==1.5.1
@@ -49,7 +49,7 @@ PyNaCl==1.3.0
PyQRCode==1.2.1
# homeassistant.components.rmvtransport
-PyRMVtransport==0.2.10
+PyRMVtransport==0.3.1
# homeassistant.components.telegram_bot
PySocks==1.7.1
@@ -72,7 +72,7 @@ PyXiaomiGateway==0.13.4
# homeassistant.components.bmp280
# homeassistant.components.mcp23017
# homeassistant.components.rpi_gpio
-# RPi.GPIO==0.7.0
+# RPi.GPIO==0.7.1a4
# homeassistant.components.remember_the_milk
RtmAPI==0.7.2
@@ -81,7 +81,7 @@ RtmAPI==0.7.2
TravisPy==0.3.5
# homeassistant.components.twitter
-TwitterAPI==2.6.3
+TwitterAPI==2.6.8
# homeassistant.components.tof
# VL53L1X2==0.1.5
@@ -96,7 +96,7 @@ WazeRouteCalculator==0.12
abodepy==1.2.0
# homeassistant.components.accuweather
-accuweather==0.0.11
+accuweather==0.1.1
# homeassistant.components.bmp280
adafruit-circuitpython-bmp280==3.1.1
@@ -108,10 +108,10 @@ adafruit-circuitpython-mcp230xx==2.2.2
adb-shell[async]==0.2.1
# homeassistant.components.alarmdecoder
-adext==0.3
+adext==0.4.1
# homeassistant.components.adguard
-adguardhome==0.4.2
+adguardhome==0.5.0
# homeassistant.components.advantage_air
advantage_air==0.2.1
@@ -146,6 +146,9 @@ aioazuredevops==1.3.5
# homeassistant.components.aws
aiobotocore==0.11.1
+# homeassistant.components.dhcp
+aiodiscover==1.3.3
+
# homeassistant.components.dnsip
# homeassistant.components.minecraft_server
aiodns==2.0.0
@@ -153,15 +156,15 @@ aiodns==2.0.0
# homeassistant.components.eafm
aioeafm==0.1.2
+# homeassistant.components.emonitor
+aioemonitor==1.0.5
+
# homeassistant.components.esphome
-aioesphomeapi==2.6.4
+aioesphomeapi==2.6.6
# homeassistant.components.flo
aioflo==0.4.1
-# homeassistant.components.freebox
-aiofreepybox==0.0.8
-
# homeassistant.components.yi
aioftp==0.12.0
@@ -169,7 +172,7 @@ aioftp==0.12.0
aioguardian==1.0.4
# homeassistant.components.harmony
-aioharmony==0.2.6
+aioharmony==0.2.7
# homeassistant.components.homekit_controller
aiohomekit==0.2.60
@@ -197,7 +200,10 @@ aiolifx==0.6.9
aiolifx_effects==0.2.2
# homeassistant.components.lutron_caseta
-aiolip==1.0.1
+aiolip==1.1.4
+
+# homeassistant.components.lyric
+aiolyric==1.0.6
# homeassistant.components.keyboard_remote
aionotify==0.2.0
@@ -215,13 +221,13 @@ aiopvapi==1.6.14
aiopvpc==2.0.2
# homeassistant.components.webostv
-aiopylgtv==0.3.3
+aiopylgtv==0.4.0
# homeassistant.components.recollect_waste
-aiorecollect==1.0.1
+aiorecollect==1.0.4
# homeassistant.components.shelly
-aioshelly==0.5.3
+aioshelly==0.6.2
# homeassistant.components.switcher_kis
aioswitcher==1.2.1
@@ -233,7 +239,7 @@ aiounifi==26
aioymaps==1.1.0
# homeassistant.components.airly
-airly==1.0.0
+airly==1.1.0
# homeassistant.components.aladdin_connect
aladdin_connect==0.3
@@ -245,7 +251,7 @@ alpha_vantage==2.3.1
ambiclimate==0.2.1
# homeassistant.components.amcrest
-amcrest==1.7.0
+amcrest==1.7.1
# homeassistant.components.androidtv
androidtv[async]==0.0.57
@@ -269,7 +275,7 @@ apprise==0.8.9
aprslib==0.6.46
# homeassistant.components.aqualogic
-aqualogic==1.0
+aqualogic==2.6
# homeassistant.components.arcam_fmj
arcam-fmj==0.5.3
@@ -284,8 +290,9 @@ asmog==0.0.6
asterisk_mbox==0.5.0
# homeassistant.components.dlna_dmr
+# homeassistant.components.ssdp
# homeassistant.components.upnp
-async-upnp-client==0.14.13
+async-upnp-client==0.16.0
# homeassistant.components.supla
asyncpysupla==0.0.5
@@ -300,7 +307,7 @@ auroranoaa==0.0.2
aurorapy==0.2.6
# homeassistant.components.stream
-av==8.0.2
+av==8.0.3
# homeassistant.components.avea
# avea==1.5.1
@@ -309,7 +316,7 @@ av==8.0.2
# avion==0.10
# homeassistant.components.axis
-axis==43
+axis==44
# homeassistant.components.azure_event_hub
azure-eventhub==5.1.0
@@ -339,10 +346,10 @@ beautifulsoup4==4.9.3
# beewi_smartclim==0.0.10
# homeassistant.components.zha
-bellows==0.21.0
+bellows==0.23.1
# homeassistant.components.bmw_connected_drive
-bimmer_connected==0.7.14
+bimmer_connected==0.7.15
# homeassistant.components.bizkaibus
bizkaibus==0.1.1
@@ -351,7 +358,7 @@ bizkaibus==0.1.1
blebox_uniapi==1.3.2
# homeassistant.components.blink
-blinkpy==0.16.4
+blinkpy==0.17.0
# homeassistant.components.blinksticklight
blinkstick==1.1.8
@@ -370,7 +377,7 @@ blockchain==1.4.4
# bme680==1.0.5
# homeassistant.components.bond
-bond-api==0.1.8
+bond-api==0.1.12
# homeassistant.components.amazon_polly
# homeassistant.components.route53
@@ -380,10 +387,10 @@ boto3==1.9.252
bravia-tv==1.0.8
# homeassistant.components.broadlink
-broadlink==0.16.0
+broadlink==0.17.0
# homeassistant.components.brother
-brother==0.1.20
+brother==0.2.2
# homeassistant.components.brottsplatskartan
brottsplatskartan==0.0.1
@@ -427,11 +434,8 @@ co2signal==0.4.2
# homeassistant.components.coinbase
coinbase==2.1.0
-# homeassistant.components.coinmarketcap
-coinmarketcap==5.0.3
-
# homeassistant.scripts.check_config
-colorlog==4.6.2
+colorlog==4.8.0
# homeassistant.components.color_extractor
colorthief==0.2.1
@@ -450,12 +454,6 @@ construct==2.10.56
# homeassistant.components.coronavirus
coronavirus==1.1.1
-# homeassistant.scripts.credstash
-# credstash==1.15.0
-
-# homeassistant.components.crimereports
-crimereports==1.0.1
-
# homeassistant.components.datadog
datadog==0.15.0
@@ -481,10 +479,10 @@ defusedxml==0.6.0
deluge-client==1.7.1
# homeassistant.components.denonavr
-denonavr==0.9.10
+denonavr==0.10.5
# homeassistant.components.devolo_home_control
-devolo-home-control-api==0.16.0
+devolo-home-control-api==0.17.1
# homeassistant.components.directv
directv==0.4.0
@@ -508,7 +506,7 @@ doorbirdpy==2.1.0
dovado==0.4.1
# homeassistant.components.dsmr
-dsmr_parser==0.25
+dsmr_parser==0.28
# homeassistant.components.dwd_weather_warnings
dwdwfsapi==1.0.3
@@ -532,7 +530,7 @@ ecoaliface==0.4.0
eebrightbox==0.0.4
# homeassistant.components.elgato
-elgato==1.0.0
+elgato==2.0.1
# homeassistant.components.eliqonline
eliqonline==1.2.2
@@ -541,7 +539,7 @@ eliqonline==1.2.2
elkm1-lib==0.8.10
# homeassistant.components.mobile_app
-emoji==0.5.4
+emoji==1.2.0
# homeassistant.components.emulated_roku
emulated_roku==0.2.1
@@ -577,7 +575,10 @@ eternalegypt==0.0.12
# evdev==1.1.2
# homeassistant.components.evohome
-evohome-async==0.3.5.post1
+evohome-async==0.3.8
+
+# homeassistant.components.faa_delays
+faadelays==0.0.6
# homeassistant.components.dlib_face_detect
# homeassistant.components.dlib_face_identify
@@ -613,6 +614,9 @@ foobot_async==1.0.0
# homeassistant.components.fortios
fortiosapi==0.10.8
+# homeassistant.components.freebox
+freebox-api==0.0.10
+
# homeassistant.components.free_mobile
freesms==0.1.2
@@ -622,13 +626,10 @@ freesms==0.1.2
fritzconnection==1.4.0
# homeassistant.components.google_translate
-gTTS==2.2.1
+gTTS==2.2.2
# homeassistant.components.garmin_connect
-garminconnect==0.1.16
-
-# homeassistant.components.geizhals
-geizhals==0.0.9
+garminconnect==0.1.19
# homeassistant.components.geniushub
geniushub-client==0.6.30
@@ -649,7 +650,6 @@ georss_ign_sismologia_client==0.2
# homeassistant.components.qld_bushfire
georss_qld_bushfire_alert_client==0.3
-# homeassistant.components.denonavr
# homeassistant.components.huawei_lte
# homeassistant.components.kef
# homeassistant.components.minecraft_server
@@ -657,7 +657,7 @@ georss_qld_bushfire_alert_client==0.3
getmac==0.8.2
# homeassistant.components.gios
-gios==0.1.5
+gios==0.2.1
# homeassistant.components.gitter
gitterpy==0.1.7
@@ -684,7 +684,7 @@ google-cloud-pubsub==2.1.0
google-cloud-texttospeech==0.4.0
# homeassistant.components.nest
-google-nest-sdm==0.2.9
+google-nest-sdm==0.2.12
# homeassistant.components.google_travel_time
googlemaps==2.5.1
@@ -707,9 +707,6 @@ greeneye_monitor==2.1
# homeassistant.components.greenwave
greenwavereality==0.5.1
-# homeassistant.components.griddy
-griddypower==0.1.0
-
# homeassistant.components.growatt_server
growattServer==0.1.1
@@ -723,7 +720,7 @@ guppy3==3.1.0
ha-ffmpeg==3.0.2
# homeassistant.components.philips_js
-ha-philipsjs==0.0.8
+ha-philipsjs==2.3.2
# homeassistant.components.habitica
habitipy==0.2.0
@@ -732,13 +729,13 @@ habitipy==0.2.0
hangups==0.4.11
# homeassistant.components.cloud
-hass-nabucasa==0.41.0
+hass-nabucasa==0.42.0
# homeassistant.components.splunk
hass_splunk==0.1.1
# homeassistant.components.tasmota
-hatasmota==0.2.7
+hatasmota==0.2.9
# homeassistant.components.jewish_calendar
hdate==0.9.12
@@ -762,10 +759,10 @@ hlk-sw16==0.0.9
hole==0.5.1
# homeassistant.components.workday
-holidays==0.10.4
+holidays==0.11.1
# homeassistant.components.frontend
-home-assistant-frontend==20210127.5
+home-assistant-frontend==20210407.2
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -776,12 +773,15 @@ homeconnect==0.6.3
# homeassistant.components.homematicip_cloud
homematicip==0.13.1
+# homeassistant.components.home_plus_control
+homepluscontrol==0.0.5
+
# homeassistant.components.horizon
horimote==0.4.1
# homeassistant.components.google
# homeassistant.components.remember_the_milk
-httplib2==0.18.1
+httplib2==0.19.0
# homeassistant.components.huawei_lte
huawei-lte-api==1.4.17
@@ -813,7 +813,7 @@ ibm-watson==4.0.1
ibmiotf==0.3.4
# homeassistant.components.ping
-icmplib==2.0
+icmplib==2.1.1
# homeassistant.components.iglo
iglo==1.2.7
@@ -822,10 +822,10 @@ iglo==1.2.7
ihcsdk==2.7.0
# homeassistant.components.incomfort
-incomfort-client==0.4.0
+incomfort-client==0.4.4
# homeassistant.components.influxdb
-influxdb-client==1.8.0
+influxdb-client==1.14.0
# homeassistant.components.influxdb
influxdb==5.2.3
@@ -834,7 +834,6 @@ influxdb==5.2.3
iperf3==0.1.11
# homeassistant.components.rest
-# homeassistant.components.verisure
jsonpath==0.82
# homeassistant.components.kaiterra
@@ -843,18 +842,15 @@ kaiterra-async-client==0.0.2
# homeassistant.components.keba
keba-kecontact==1.1.0
-# homeassistant.scripts.keyring
-keyring==21.2.0
-
-# homeassistant.scripts.keyring
-keyrings.alt==3.4.0
-
# homeassistant.components.kiwi
kiwiki-client==0.1.1
# homeassistant.components.konnected
konnected==1.2.0
+# homeassistant.components.kostal_plenticore
+kostal_plenticore==0.2.0
+
# homeassistant.components.eufy
lakeside==0.12
@@ -880,7 +876,7 @@ life360==4.1.1
liffylights==0.9.4
# homeassistant.components.osramlightify
-lightify==1.0.7.2
+lightify==1.0.7.3
# homeassistant.components.lightwave
lightwave==0.19
@@ -922,13 +918,13 @@ magicseaweed==1.0.3
matrix-client==0.3.2
# homeassistant.components.maxcube
-maxcube-api==0.3.0
+maxcube-api==0.4.1
# homeassistant.components.mythicbeastsdns
mbddns==0.1.2
# homeassistant.components.minecraft_server
-mcstatus==2.3.0
+mcstatus==5.1.1
# homeassistant.components.message_bird
messagebird==1.2.0
@@ -937,7 +933,7 @@ messagebird==1.2.0
meteoalertapi==0.1.6
# homeassistant.components.meteo_france
-meteofrance-api==1.0.1
+meteofrance-api==1.0.2
# homeassistant.components.mfi
mficlient==0.3.0
@@ -955,7 +951,10 @@ minio==4.0.9
mitemp_bt==0.0.3
# homeassistant.components.motion_blinds
-motionblinds==0.4.8
+motionblinds==0.4.10
+
+# homeassistant.components.mullvad
+mullvad-api==1.0.0
# homeassistant.components.tts
mutagen==1.45.1
@@ -973,7 +972,7 @@ n26==0.2.7
nad_receiver==0.0.12
# homeassistant.components.keenetic_ndms2
-ndms2_client==0.0.11
+ndms2_client==0.1.1
# homeassistant.components.ness_alarm
nessclient==0.9.15
@@ -1016,13 +1015,14 @@ nsw-fuel-api-client==1.0.10
nuheat==0.3.0
# homeassistant.components.numato
-numato-gpio==0.8.0
+numato-gpio==0.10.0
+# homeassistant.components.compensation
# homeassistant.components.iqvia
# homeassistant.components.opencv
# homeassistant.components.tensorflow
# homeassistant.components.trend
-numpy==1.19.2
+numpy==1.20.2
# homeassistant.components.oasa_telematics
oasatelematics==0.3
@@ -1037,7 +1037,7 @@ objgraph==3.4.1
oemthermostat==1.1.1
# homeassistant.components.omnilogic
-omnilogic==0.4.2
+omnilogic==0.4.3
# homeassistant.components.ondilo_ico
ondilo==0.2.0
@@ -1070,7 +1070,10 @@ opensensemap-api==0.1.5
openwebifpy==3.2.7
# homeassistant.components.luci
-openwrt-luci-rpc==1.1.6
+openwrt-luci-rpc==1.1.8
+
+# homeassistant.components.ubus
+openwrt-ubus-rpc==0.0.2
# homeassistant.components.oru
oru==0.1.11
@@ -1131,19 +1134,19 @@ pilight==0.1.1
# homeassistant.components.seven_segments
# homeassistant.components.sighthound
# homeassistant.components.tensorflow
-pillow==8.1.0
+pillow==8.1.2
# homeassistant.components.dominos
pizzapi==0.0.3
# homeassistant.components.plex
-plexapi==4.3.0
+plexapi==4.5.1
# homeassistant.components.plex
plexauth==0.0.6
# homeassistant.components.plex
-plexwebsocket==0.0.12
+plexwebsocket==0.0.13
# homeassistant.components.plugwise
plugwise==0.8.5
@@ -1159,7 +1162,7 @@ pmsensor==0.4
poolsense==0.0.8
# homeassistant.components.reddit
-praw==7.1.0
+praw==7.1.4
# homeassistant.components.islamic_prayer_times
prayer_times_calculator==0.0.3
@@ -1197,9 +1200,6 @@ pushover_complete==1.1.1
# homeassistant.components.rpi_gpio_pwm
pwmled==1.6.7
-# homeassistant.components.august
-py-august==0.25.2
-
# homeassistant.components.canary
py-canary==0.5.1
@@ -1215,17 +1215,14 @@ py-nightscout==1.2.2
# homeassistant.components.schluter
py-schluter==0.1.7
-# homeassistant.components.synology
-py-synology==0.2.0
-
# homeassistant.components.zabbix
py-zabbix==1.1.7
# homeassistant.components.seventeentrack
-py17track==2.2.2
+py17track==3.2.1
# homeassistant.components.hdmi_cec
-pyCEC==0.4.14
+pyCEC==0.5.1
# homeassistant.components.control4
pyControl4==0.0.6
@@ -1233,6 +1230,9 @@ pyControl4==0.0.6
# homeassistant.components.tplink
pyHS100==0.3.5.2
+# homeassistant.components.met_eireann
+pyMetEireann==0.2
+
# homeassistant.components.met
# homeassistant.components.norway_air
pyMetno==0.8.1
@@ -1244,7 +1244,7 @@ pyRFXtrx==0.26.1
# pySwitchmate==0.4.6
# homeassistant.components.tibber
-pyTibber==0.16.1
+pyTibber==0.16.2
# homeassistant.components.dlink
pyW215==0.7.0
@@ -1277,7 +1277,7 @@ pyalmond==0.0.2
pyarlo==0.2.4
# homeassistant.components.atag
-pyatag==0.3.4.4
+pyatag==0.3.5.3
# homeassistant.components.netatmo
pyatmo==4.2.2
@@ -1286,7 +1286,7 @@ pyatmo==4.2.2
pyatome==0.1.1
# homeassistant.components.apple_tv
-pyatv==0.7.5
+pyatv==0.7.7
# homeassistant.components.bbox
pybbox==0.0.5-alpha
@@ -1310,11 +1310,14 @@ pycfdns==1.2.1
pychannels==1.0.0
# homeassistant.components.cast
-pychromecast==7.7.2
+pychromecast==9.1.2
# homeassistant.components.pocketcasts
pycketcasts==1.0.0
+# homeassistant.components.climacell
+pyclimacell==0.18.0
+
# homeassistant.components.cmus
pycmus==0.1.1
@@ -1340,7 +1343,7 @@ pydaikin==2.4.1
pydanfossair==0.1.0
# homeassistant.components.deconz
-pydeconz==77
+pydeconz==78
# homeassistant.components.delijn
pydelijn==0.6.1
@@ -1361,7 +1364,7 @@ pydroid-ipcam==0.8
pyebox==1.1.4
# homeassistant.components.econet
-pyeconet==0.1.12
+pyeconet==0.1.13
# homeassistant.components.edimax
pyedimax==0.2.1
@@ -1381,6 +1384,9 @@ pyephember==0.3.1
# homeassistant.components.everlights
pyeverlights==0.1.0
+# homeassistant.components.ezviz
+pyezviz==0.1.8.7
+
# homeassistant.components.fido
pyfido==2.1.1
@@ -1425,7 +1431,7 @@ pygtfs==0.1.5
pygti==0.9.2
# homeassistant.components.version
-pyhaversion==3.4.2
+pyhaversion==21.3.0
# homeassistant.components.heos
pyheos==0.7.2
@@ -1434,22 +1440,22 @@ pyheos==0.7.2
pyhik==0.2.8
# homeassistant.components.hive
-pyhiveapi==0.2.20.2
+pyhiveapi==0.4.1
# homeassistant.components.homematic
-pyhomematic==0.1.71
+pyhomematic==0.1.72
# homeassistant.components.homeworks
pyhomeworks==0.0.6
# homeassistant.components.icloud
-pyicloud==0.9.7
+pyicloud==0.10.2
# homeassistant.components.insteon
pyinsteon==1.0.9
# homeassistant.components.intesishome
-pyintesishome==1.7.5
+pyintesishome==1.7.6
# homeassistant.components.ipma
pyipma==2.0.5
@@ -1467,7 +1473,7 @@ pyirishrail==0.0.2
pyiss==1.0.1
# homeassistant.components.isy994
-pyisy==2.1.0
+pyisy==2.1.1
# homeassistant.components.itach
pyitachip2ir==0.0.7
@@ -1475,11 +1481,14 @@ pyitachip2ir==0.0.7
# homeassistant.components.kira
pykira==0.1.1
+# homeassistant.components.kmtronic
+pykmtronic==0.0.3
+
# homeassistant.components.kodi
-pykodi==0.2.1
+pykodi==0.2.4
# homeassistant.components.kulersky
-pykulersky==0.4.0
+pykulersky==0.5.2
# homeassistant.components.kwb
pykwb==0.0.8
@@ -1488,7 +1497,7 @@ pykwb==0.0.8
pylacrosse==0.4
# homeassistant.components.lastfm
-pylast==4.1.0
+pylast==4.2.0
# homeassistant.components.launch_library
pylaunches==1.0.0
@@ -1500,7 +1509,10 @@ pylgnetcast-homeassistant==0.2.0.dev0
pylibrespot-java==0.1.0
# homeassistant.components.litejet
-pylitejet==0.1
+pylitejet==0.3.0
+
+# homeassistant.components.litterrobot
+pylitterbot==2021.2.8
# homeassistant.components.loopenergy
pyloopenergy==0.2.1
@@ -1509,7 +1521,7 @@ pyloopenergy==0.2.1
pylutron-caseta==0.9.0
# homeassistant.components.lutron
-pylutron==0.2.5
+pylutron==0.2.7
# homeassistant.components.mailgun
pymailgunner==1.4
@@ -1517,6 +1529,9 @@ pymailgunner==1.4
# homeassistant.components.firmata
pymata-express==1.19
+# homeassistant.components.mazda
+pymazda==0.0.9
+
# homeassistant.components.mediaroom
pymediaroom==0.6.4.1
@@ -1545,13 +1560,13 @@ pymsteams==0.1.12
pymusiccast==0.1.6
# homeassistant.components.myq
-pymyq==2.0.14
+pymyq==3.0.4
# homeassistant.components.mysensors
-pymysensors==0.18.0
+pymysensors==0.21.0
# homeassistant.components.nanoleaf
-pynanoleaf==0.0.5
+pynanoleaf==0.1.0
# homeassistant.components.nello
pynello==2.0.3
@@ -1563,7 +1578,7 @@ pynetgear==0.6.1
pynetio==0.1.9.1
# homeassistant.components.nuki
-pynuki==1.3.8
+pynuki==1.4.1
# homeassistant.components.nut
pynut2==2.1.2
@@ -1596,7 +1611,7 @@ pyoppleio==1.0.5
pyota==2.0.5
# homeassistant.components.opentherm_gw
-pyotgw==1.0b1
+pyotgw==1.1b1
# homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp
@@ -1604,7 +1619,7 @@ pyotgw==1.0b1
pyotp==2.3.0
# homeassistant.components.openweathermap
-pyowm==3.1.1
+pyowm==3.2.0
# homeassistant.components.onewire
pyownet==0.10.0.post1
@@ -1618,8 +1633,11 @@ pypck==0.7.9
# homeassistant.components.pjlink
pypjlink2==1.2.1
+# homeassistant.components.plaato
+pyplaato==0.0.15
+
# homeassistant.components.point
-pypoint==2.0.0
+pypoint==2.1.0
# homeassistant.components.profiler
pyprof2calltree==1.4.5
@@ -1648,6 +1666,9 @@ pyrepetier==3.0.5
# homeassistant.components.risco
pyrisco==0.3.1
+# homeassistant.components.rituals_perfume_genie
+pyrituals==0.0.2
+
# homeassistant.components.ruckus_unleashed
pyruckus==0.12
@@ -1687,7 +1708,7 @@ pyskyqhub==0.1.3
pysma==0.3.5
# homeassistant.components.smappee
-pysmappee==0.2.16
+pysmappee==0.2.17
# homeassistant.components.smartthings
pysmartapp==0.3.3
@@ -1708,7 +1729,7 @@ pysnmp==4.4.12
pysoma==0.0.10
# homeassistant.components.sonos
-pysonos==0.0.37
+pysonos==0.0.42
# homeassistant.components.spc
pyspcwebgw==0.4.0
@@ -1747,7 +1768,7 @@ python-clementine-remote==1.0.1
python-digitalocean==1.13.2
# homeassistant.components.ecobee
-python-ecobee-api==0.2.8
+python-ecobee-api==0.2.10
# homeassistant.components.eq3btsmart
# python-eq3bt==0.1.11
@@ -1774,7 +1795,7 @@ python-gitlab==1.6.0
python-hpilo==4.3
# homeassistant.components.izone
-python-izone==1.1.3
+python-izone==1.1.4
# homeassistant.components.joaoapps_join
python-join-api==0.0.6
@@ -1786,10 +1807,10 @@ python-juicenet==1.0.1
# python-lirc==1.2.3
# homeassistant.components.xiaomi_miio
-python-miio==0.5.4
+python-miio==0.5.5
# homeassistant.components.mpd
-python-mpd2==3.0.3
+python-mpd2==3.0.4
# homeassistant.components.mystrom
python-mystrom==1.1.2
@@ -1809,6 +1830,9 @@ python-qbittorrent==0.4.2
# homeassistant.components.ripple
python-ripple-api==0.0.3
+# homeassistant.components.smarttub
+python-smarttub==0.0.19
+
# homeassistant.components.sochain
python-sochain-api==0.0.2
@@ -1822,7 +1846,7 @@ python-tado==0.10.0
python-telegram-bot==13.1
# homeassistant.components.vlc_telnet
-python-telnet-vlc==1.0.4
+python-telnet-vlc==2.0.1
# homeassistant.components.twitch
python-twitch-client==0.6.0
@@ -1874,13 +1898,13 @@ pyuptimerobot==0.0.5
# pyuserinput==0.1.11
# homeassistant.components.vera
-pyvera==0.3.11
+pyvera==0.3.13
# homeassistant.components.versasense
pyversasense==0.0.6
# homeassistant.components.vesync
-pyvesync==1.2.0
+pyvesync==1.3.1
# homeassistant.components.vizio
pyvizio==0.1.57
@@ -1895,10 +1919,10 @@ pyvolumio==0.1.3
pywebpush==1.9.2
# homeassistant.components.wemo
-pywemo==0.6.1
+pywemo==0.6.3
# homeassistant.components.wilight
-pywilight==0.0.66
+pywilight==0.0.68
# homeassistant.components.xeoma
pyxeoma==1.4.1
@@ -1907,10 +1931,10 @@ pyxeoma==1.4.1
pyzbar==0.1.7
# homeassistant.components.zerproc
-pyzerproc==0.4.7
+pyzerproc==0.4.8
# homeassistant.components.qnap
-qnapstats==0.3.0
+qnapstats==0.3.1
# homeassistant.components.quantum_gateway
quantum-gateway==0.0.5
@@ -1940,7 +1964,7 @@ restrictedpython==5.1
rfk101py==0.0.1
# homeassistant.components.rflink
-rflink==0.0.55
+rflink==0.0.58
# homeassistant.components.ring
ring_doorbell==0.6.2
@@ -1955,16 +1979,16 @@ rjpl==0.3.6
rocketchat-API==0.6.1
# homeassistant.components.roku
-rokuecp==0.6.0
+rokuecp==0.8.1
# homeassistant.components.roomba
roombapy==1.6.2
# homeassistant.components.roon
-roonapi==0.0.31
+roonapi==0.0.32
# homeassistant.components.rova
-rova==0.1.0
+rova==0.2.1
# homeassistant.components.rpi_power
rpi-bad-power==0.1.0
@@ -1985,7 +2009,7 @@ rxv==0.6.0
samsungctl[websocket]==0.7.1
# homeassistant.components.samsungtv
-samsungtvws==1.4.0
+samsungtvws==1.6.0
# homeassistant.components.satel_integra
satel_integra==0.3.4
@@ -1996,21 +2020,24 @@ scapy==2.4.4
# homeassistant.components.deutsche_bahn
schiene==0.23
+# homeassistant.components.screenlogic
+screenlogicpy==0.2.1
+
# homeassistant.components.scsgate
scsgate==0.1.0
# homeassistant.components.sendgrid
-sendgrid==6.5.0
+sendgrid==6.6.0
# homeassistant.components.sensehat
sense-hat==2.2.0
# homeassistant.components.emulated_kasa
# homeassistant.components.sense
-sense_energy==0.8.1
+sense_energy==0.9.0
# homeassistant.components.sentry
-sentry-sdk==0.19.5
+sentry-sdk==1.0.0
# homeassistant.components.sharkiq
sharkiqpy==0.1.8
@@ -2028,7 +2055,7 @@ simplehound==0.3
simplepush==1.1.4
# homeassistant.components.simplisafe
-simplisafe-python==9.6.2
+simplisafe-python==9.6.9
# homeassistant.components.sisyphus
sisyphus-control==3.0
@@ -2043,7 +2070,7 @@ slackclient==2.5.0
sleepyq==0.8.1
# homeassistant.components.xmpp
-slixmpp==1.6.0
+slixmpp==1.7.0
# homeassistant.components.smart_meter_texas
smart-meter-texas==0.4.0
@@ -2063,10 +2090,7 @@ smarthab==0.21
smhi-pkg==1.0.13
# homeassistant.components.snapcast
-snapcast==2.1.1
-
-# homeassistant.components.socialblade
-socialbladeclient==0.5
+snapcast==2.1.2
# homeassistant.components.solaredge_local
solaredge-local==0.2.0
@@ -2075,7 +2099,7 @@ solaredge-local==0.2.0
solaredge==0.0.2
# homeassistant.components.solax
-solax==0.2.5
+solax==0.2.6
# homeassistant.components.honeywell
somecomfort==0.5.2
@@ -2090,7 +2114,7 @@ sonarr==0.3.0
speak2mary==1.4.0
# homeassistant.components.speedtestdotnet
-speedtest-cli==2.1.2
+speedtest-cli==2.1.3
# homeassistant.components.spider
spiderpy==1.4.2
@@ -2099,11 +2123,11 @@ spiderpy==1.4.2
spotcrime==1.0.4
# homeassistant.components.spotify
-spotipy==2.16.1
+spotipy==2.17.1
# homeassistant.components.recorder
# homeassistant.components.sql
-sqlalchemy==1.3.22
+sqlalchemy==1.3.23
# homeassistant.components.srp_energy
srpenergy==1.3.2
@@ -2132,6 +2156,9 @@ streamlabswater==1.0.1
# homeassistant.components.traccar
stringcase==1.2.0
+# homeassistant.components.subaru
+subarulink==0.3.12
+
# homeassistant.components.ecovacs
sucks==0.9.4
@@ -2148,7 +2175,7 @@ swisshydrodata==0.0.3
synology-srm==0.2.0
# homeassistant.components.synology_dsm
-synologydsm-api==1.0.1
+synologydsm-api==1.0.2
# homeassistant.components.tahoma
tahoma-api==0.0.16
@@ -2178,10 +2205,10 @@ temperusb==1.5.3
# tensorflow==2.3.0
# homeassistant.components.powerwall
-tesla-powerwall==0.3.3
+tesla-powerwall==0.3.5
# homeassistant.components.tesla
-teslajsonpy==0.10.4
+teslajsonpy==0.11.5
# homeassistant.components.tensorflow
# tf-models-official==2.3.0
@@ -2205,7 +2232,7 @@ todoist-python==8.0.0
toonapi==0.2.0
# homeassistant.components.totalconnect
-total_connect_client==0.55
+total_connect_client==0.57
# homeassistant.components.tplink_lte
tp-connected==0.0.4
@@ -2214,7 +2241,7 @@ tp-connected==0.0.4
transmissionrpc==0.11
# homeassistant.components.tuya
-tuyaha==0.0.9
+tuyaha==0.0.10
# homeassistant.components.twentemilieu
twentemilieu==0.3.0
@@ -2235,7 +2262,7 @@ unifiled==0.11
upb_lib==0.4.12
# homeassistant.components.upcloud
-upcloud-api==0.4.5
+upcloud-api==1.0.1
# homeassistant.components.huawei_lte
# homeassistant.components.syncthru
@@ -2263,7 +2290,7 @@ volkszaehler==0.2.1
volvooncall==0.8.12
# homeassistant.components.verisure
-vsure==1.6.1
+vsure==1.7.3
# homeassistant.components.vasttrafik
vtjp==0.1.14
@@ -2272,13 +2299,13 @@ vtjp==0.1.14
vultr==0.1.2
# homeassistant.components.wake_on_lan
-wakeonlan==1.1.6
+wakeonlan==2.0.0
# homeassistant.components.waqi
waqiasync==1.0.0
# homeassistant.components.folder_watcher
-watchdog==0.8.3
+watchdog==1.0.2
# homeassistant.components.waterfurnace
waterfurnace==1.1.0
@@ -2296,7 +2323,7 @@ wiffi==1.0.1
wirelesstagpy==0.4.1
# homeassistant.components.withings
-withings-api==2.1.6
+withings-api==2.3.2
# homeassistant.components.wled
wled==0.4.4
@@ -2313,11 +2340,8 @@ xbox-webapi==2.0.8
# homeassistant.components.xbox_live
xboxapi==2.0.1
-# homeassistant.components.xfinity
-xfinity-gateway==0.0.4
-
# homeassistant.components.knx
-xknx==0.16.2
+xknx==0.18.0
# homeassistant.components.bluesound
# homeassistant.components.rest
@@ -2332,6 +2356,9 @@ xs1-api-client==3.0.0
# homeassistant.components.yale_smart_alarm
yalesmartalarmclient==0.1.6
+# homeassistant.components.august
+yalexs==1.1.10
+
# homeassistant.components.yeelight
yeelight==0.5.4
@@ -2339,7 +2366,7 @@ yeelight==0.5.4
yeelightsunflower==0.0.10
# homeassistant.components.media_extractor
-youtube_dl==2021.01.16
+youtube_dl==2021.03.14
# homeassistant.components.onvif
zeep[async]==4.0.0
@@ -2348,10 +2375,10 @@ zeep[async]==4.0.0
zengge==0.2
# homeassistant.components.zeroconf
-zeroconf==0.28.8
+zeroconf==0.29.0
# homeassistant.components.zha
-zha-quirks==0.0.53
+zha-quirks==0.0.55
# homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9
@@ -2363,7 +2390,7 @@ ziggo-mediabox-xl==1.1.0
zigpy-cc==0.5.2
# homeassistant.components.zha
-zigpy-deconz==0.11.1
+zigpy-deconz==0.12.0
# homeassistant.components.zha
zigpy-xbee==0.13.0
@@ -2372,13 +2399,13 @@ zigpy-xbee==0.13.0
zigpy-zigate==0.7.3
# homeassistant.components.zha
-zigpy-znp==0.3.0
+zigpy-znp==0.4.0
# homeassistant.components.zha
-zigpy==0.32.0
+zigpy==0.33.0
# homeassistant.components.zoneminder
zm-py==0.5.2
# homeassistant.components.zwave_js
-zwave-js-server-python==0.14.1
+zwave-js-server-python==0.23.1
diff --git a/requirements_test.txt b/requirements_test.txt
index 5815ddd8ed0915..1d4ada0afcb001 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -5,15 +5,14 @@
-c homeassistant/package_constraints.txt
-r requirements_test_pre_commit.txt
codecov==2.1.10
-coverage==5.4
+coverage==5.5
jsonpickle==1.4.1
mock-open==1.4.0
-mypy==0.790
-pre-commit==2.9.3
-pylint==2.6.0
-astroid==2.4.2
+mypy==0.812
+pre-commit==2.12.0
+pylint==2.7.4
+astroid==2.5.2
pipdeptree==1.0.0
-awesomeversion==21.1.3
pylint-strict-informational==0.1
pytest-aiohttp==0.3.0
pytest-cov==2.10.1
@@ -21,7 +20,7 @@ pytest-test-groups==1.0.3
pytest-sugar==0.9.4
pytest-timeout==1.4.2
pytest-xdist==2.1.0
-pytest==6.2.2
+pytest==6.2.3
requests_mock==1.8.0
responses==0.12.0
respx==0.16.2
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 345edcfbb35106..e57032be86e7fb 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -3,8 +3,11 @@
-r requirements_test.txt
+# homeassistant.components.aemet
+AEMET-OpenData==0.1.8
+
# homeassistant.components.homekit
-HAP-python==3.1.0
+HAP-python==3.4.1
# homeassistant.components.flick_electric
PyFlick==0.0.2
@@ -18,7 +21,7 @@ PyNaCl==1.3.0
PyQRCode==1.2.1
# homeassistant.components.rmvtransport
-PyRMVtransport==0.2.10
+PyRMVtransport==0.3.1
# homeassistant.components.transport_nsw
PyTransportNSW==0.1.1
@@ -35,20 +38,23 @@ RtmAPI==0.7.2
# homeassistant.components.onvif
WSDiscovery==2.0.0
+# homeassistant.components.waze_travel_time
+WazeRouteCalculator==0.12
+
# homeassistant.components.abode
abodepy==1.2.0
# homeassistant.components.accuweather
-accuweather==0.0.11
+accuweather==0.1.1
# homeassistant.components.androidtv
adb-shell[async]==0.2.1
# homeassistant.components.alarmdecoder
-adext==0.3
+adext==0.4.1
# homeassistant.components.adguard
-adguardhome==0.4.2
+adguardhome==0.5.0
# homeassistant.components.advantage_air
advantage_air==0.2.1
@@ -80,6 +86,9 @@ aioazuredevops==1.3.5
# homeassistant.components.aws
aiobotocore==0.11.1
+# homeassistant.components.dhcp
+aiodiscover==1.3.3
+
# homeassistant.components.dnsip
# homeassistant.components.minecraft_server
aiodns==2.0.0
@@ -87,20 +96,20 @@ aiodns==2.0.0
# homeassistant.components.eafm
aioeafm==0.1.2
+# homeassistant.components.emonitor
+aioemonitor==1.0.5
+
# homeassistant.components.esphome
-aioesphomeapi==2.6.4
+aioesphomeapi==2.6.6
# homeassistant.components.flo
aioflo==0.4.1
-# homeassistant.components.freebox
-aiofreepybox==0.0.8
-
# homeassistant.components.guardian
aioguardian==1.0.4
# homeassistant.components.harmony
-aioharmony==0.2.6
+aioharmony==0.2.7
# homeassistant.components.homekit_controller
aiohomekit==0.2.60
@@ -116,7 +125,10 @@ aiohue==2.1.0
aiokafka==0.6.0
# homeassistant.components.lutron_caseta
-aiolip==1.0.1
+aiolip==1.1.4
+
+# homeassistant.components.lyric
+aiolyric==1.0.6
# homeassistant.components.notion
aionotion==1.1.0
@@ -131,13 +143,13 @@ aiopvapi==1.6.14
aiopvpc==2.0.2
# homeassistant.components.webostv
-aiopylgtv==0.3.3
+aiopylgtv==0.4.0
# homeassistant.components.recollect_waste
-aiorecollect==1.0.1
+aiorecollect==1.0.4
# homeassistant.components.shelly
-aioshelly==0.5.3
+aioshelly==0.6.2
# homeassistant.components.switcher_kis
aioswitcher==1.2.1
@@ -149,7 +161,7 @@ aiounifi==26
aioymaps==1.1.0
# homeassistant.components.airly
-airly==1.0.0
+airly==1.1.0
# homeassistant.components.ambiclimate
ambiclimate==0.2.1
@@ -170,17 +182,18 @@ aprslib==0.6.46
arcam-fmj==0.5.3
# homeassistant.components.dlna_dmr
+# homeassistant.components.ssdp
# homeassistant.components.upnp
-async-upnp-client==0.14.13
+async-upnp-client==0.16.0
# homeassistant.components.aurora
auroranoaa==0.0.2
# homeassistant.components.stream
-av==8.0.2
+av==8.0.3
# homeassistant.components.axis
-axis==43
+axis==44
# homeassistant.components.azure_event_hub
azure-eventhub==5.1.0
@@ -189,28 +202,28 @@ azure-eventhub==5.1.0
base36==0.1.1
# homeassistant.components.zha
-bellows==0.21.0
+bellows==0.23.1
# homeassistant.components.bmw_connected_drive
-bimmer_connected==0.7.14
+bimmer_connected==0.7.15
# homeassistant.components.blebox
blebox_uniapi==1.3.2
# homeassistant.components.blink
-blinkpy==0.16.4
+blinkpy==0.17.0
# homeassistant.components.bond
-bond-api==0.1.8
+bond-api==0.1.12
# homeassistant.components.braviatv
bravia-tv==1.0.8
# homeassistant.components.broadlink
-broadlink==0.16.0
+broadlink==0.17.0
# homeassistant.components.brother
-brother==0.1.20
+brother==0.2.2
# homeassistant.components.bsblan
bsblan==0.4.0
@@ -221,11 +234,8 @@ buienradar==1.0.4
# homeassistant.components.caldav
caldav==0.7.1
-# homeassistant.components.coinmarketcap
-coinmarketcap==5.0.3
-
# homeassistant.scripts.check_config
-colorlog==4.6.2
+colorlog==4.8.0
# homeassistant.components.color_extractor
colorthief==0.2.1
@@ -238,9 +248,6 @@ construct==2.10.56
# homeassistant.components.coronavirus
coronavirus==1.1.1
-# homeassistant.scripts.credstash
-# credstash==1.15.0
-
# homeassistant.components.datadog
datadog==0.15.0
@@ -257,10 +264,10 @@ debugpy==1.2.1
defusedxml==0.6.0
# homeassistant.components.denonavr
-denonavr==0.9.10
+denonavr==0.10.5
# homeassistant.components.devolo_home_control
-devolo-home-control-api==0.16.0
+devolo-home-control-api==0.17.1
# homeassistant.components.directv
directv==0.4.0
@@ -272,7 +279,7 @@ distro==1.5.0
doorbirdpy==2.1.0
# homeassistant.components.dsmr
-dsmr_parser==0.25
+dsmr_parser==0.28
# homeassistant.components.dynalite
dynalite_devices==0.1.46
@@ -281,13 +288,13 @@ dynalite_devices==0.1.46
eebrightbox==0.0.4
# homeassistant.components.elgato
-elgato==1.0.0
+elgato==2.0.1
# homeassistant.components.elkm1
elkm1-lib==0.8.10
# homeassistant.components.mobile_app
-emoji==0.5.4
+emoji==1.2.0
# homeassistant.components.emulated_roku
emulated_roku==0.2.1
@@ -295,12 +302,18 @@ emulated_roku==0.2.1
# homeassistant.components.enocean
enocean==0.50
+# homeassistant.components.enphase_envoy
+envoy_reader==0.18.3
+
# homeassistant.components.season
ephem==3.7.7.0
# homeassistant.components.epson
epson-projector==0.2.3
+# homeassistant.components.faa_delays
+faadelays==0.0.6
+
# homeassistant.components.feedreader
feedparser==6.0.2
@@ -310,16 +323,19 @@ fnvhash==0.1.0
# homeassistant.components.foobot
foobot_async==1.0.0
+# homeassistant.components.freebox
+freebox-api==0.0.10
+
# homeassistant.components.fritz
# homeassistant.components.fritzbox_callmonitor
# homeassistant.components.fritzbox_netmonitor
fritzconnection==1.4.0
# homeassistant.components.google_translate
-gTTS==2.2.1
+gTTS==2.2.2
# homeassistant.components.garmin_connect
-garminconnect==0.1.16
+garminconnect==0.1.19
# homeassistant.components.geo_json_events
# homeassistant.components.usgs_earthquakes_feed
@@ -337,7 +353,6 @@ georss_ign_sismologia_client==0.2
# homeassistant.components.qld_bushfire
georss_qld_bushfire_alert_client==0.3
-# homeassistant.components.denonavr
# homeassistant.components.huawei_lte
# homeassistant.components.kef
# homeassistant.components.minecraft_server
@@ -345,7 +360,7 @@ georss_qld_bushfire_alert_client==0.3
getmac==0.8.2
# homeassistant.components.gios
-gios==0.1.5
+gios==0.2.1
# homeassistant.components.glances
glances_api==0.2.0
@@ -363,28 +378,34 @@ google-api-python-client==1.6.4
google-cloud-pubsub==2.1.0
# homeassistant.components.nest
-google-nest-sdm==0.2.9
+google-nest-sdm==0.2.12
+
+# homeassistant.components.google_travel_time
+googlemaps==2.5.1
# homeassistant.components.gree
greeclimate==0.10.3
-# homeassistant.components.griddy
-griddypower==0.1.0
-
# homeassistant.components.profiler
guppy3==3.1.0
# homeassistant.components.ffmpeg
ha-ffmpeg==3.0.2
+# homeassistant.components.philips_js
+ha-philipsjs==2.3.2
+
+# homeassistant.components.habitica
+habitipy==0.2.0
+
# homeassistant.components.hangouts
hangups==0.4.11
# homeassistant.components.cloud
-hass-nabucasa==0.41.0
+hass-nabucasa==0.42.0
# homeassistant.components.tasmota
-hatasmota==0.2.7
+hatasmota==0.2.9
# homeassistant.components.jewish_calendar
hdate==0.9.12
@@ -399,10 +420,10 @@ hlk-sw16==0.0.9
hole==0.5.1
# homeassistant.components.workday
-holidays==0.10.4
+holidays==0.11.1
# homeassistant.components.frontend
-home-assistant-frontend==20210127.5
+home-assistant-frontend==20210407.2
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -413,9 +434,12 @@ homeconnect==0.6.3
# homeassistant.components.homematicip_cloud
homematicip==0.13.1
+# homeassistant.components.home_plus_control
+homepluscontrol==0.0.5
+
# homeassistant.components.google
# homeassistant.components.remember_the_milk
-httplib2==0.18.1
+httplib2==0.19.0
# homeassistant.components.huawei_lte
huawei-lte-api==1.4.17
@@ -430,27 +454,23 @@ hyperion-py==0.7.0
iaqualink==0.3.4
# homeassistant.components.ping
-icmplib==2.0
+icmplib==2.1.1
# homeassistant.components.influxdb
-influxdb-client==1.8.0
+influxdb-client==1.14.0
# homeassistant.components.influxdb
influxdb==5.2.3
# homeassistant.components.rest
-# homeassistant.components.verisure
jsonpath==0.82
-# homeassistant.scripts.keyring
-keyring==21.2.0
-
-# homeassistant.scripts.keyring
-keyrings.alt==3.4.0
-
# homeassistant.components.konnected
konnected==1.2.0
+# homeassistant.components.kostal_plenticore
+kostal_plenticore==0.2.0
+
# homeassistant.components.dyson
libpurecool==0.6.4
@@ -469,14 +489,17 @@ logi_circle==0.2.2
# homeassistant.components.luftdaten
luftdaten==0.6.4
+# homeassistant.components.maxcube
+maxcube-api==0.4.1
+
# homeassistant.components.mythicbeastsdns
mbddns==0.1.2
# homeassistant.components.minecraft_server
-mcstatus==2.3.0
+mcstatus==5.1.1
# homeassistant.components.meteo_france
-meteofrance-api==1.0.1
+meteofrance-api==1.0.2
# homeassistant.components.mfi
mficlient==0.3.0
@@ -488,11 +511,17 @@ millheater==0.4.0
minio==4.0.9
# homeassistant.components.motion_blinds
-motionblinds==0.4.8
+motionblinds==0.4.10
+
+# homeassistant.components.mullvad
+mullvad-api==1.0.0
# homeassistant.components.tts
mutagen==1.45.1
+# homeassistant.components.keenetic_ndms2
+ndms2_client==0.1.1
+
# homeassistant.components.ness_alarm
nessclient==0.9.15
@@ -510,13 +539,14 @@ nsw-fuel-api-client==1.0.10
nuheat==0.3.0
# homeassistant.components.numato
-numato-gpio==0.8.0
+numato-gpio==0.10.0
+# homeassistant.components.compensation
# homeassistant.components.iqvia
# homeassistant.components.opencv
# homeassistant.components.tensorflow
# homeassistant.components.trend
-numpy==1.19.2
+numpy==1.20.2
# homeassistant.components.google
oauth2client==4.0.0
@@ -525,7 +555,7 @@ oauth2client==4.0.0
objgraph==3.4.1
# homeassistant.components.omnilogic
-omnilogic==0.4.2
+omnilogic==0.4.3
# homeassistant.components.ondilo_ico
ondilo==0.2.0
@@ -568,16 +598,16 @@ pilight==0.1.1
# homeassistant.components.seven_segments
# homeassistant.components.sighthound
# homeassistant.components.tensorflow
-pillow==8.1.0
+pillow==8.1.2
# homeassistant.components.plex
-plexapi==4.3.0
+plexapi==4.5.1
# homeassistant.components.plex
plexauth==0.0.6
# homeassistant.components.plex
-plexwebsocket==0.0.12
+plexwebsocket==0.0.13
# homeassistant.components.plugwise
plugwise==0.8.5
@@ -593,7 +623,7 @@ pmsensor==0.4
poolsense==0.0.8
# homeassistant.components.reddit
-praw==7.1.0
+praw==7.1.4
# homeassistant.components.islamic_prayer_times
prayer_times_calculator==0.0.3
@@ -610,9 +640,6 @@ pure-python-adb[async]==0.3.0.dev0
# homeassistant.components.pushbullet
pushbullet.py==0.11.0
-# homeassistant.components.august
-py-august==0.25.2
-
# homeassistant.components.canary
py-canary==0.5.1
@@ -623,7 +650,7 @@ py-melissa-climate==2.1.4
py-nightscout==1.2.2
# homeassistant.components.seventeentrack
-py17track==2.2.2
+py17track==3.2.1
# homeassistant.components.control4
pyControl4==0.0.6
@@ -631,6 +658,9 @@ pyControl4==0.0.6
# homeassistant.components.tplink
pyHS100==0.3.5.2
+# homeassistant.components.met_eireann
+pyMetEireann==0.2
+
# homeassistant.components.met
# homeassistant.components.norway_air
pyMetno==0.8.1
@@ -639,7 +669,7 @@ pyMetno==0.8.1
pyRFXtrx==0.26.1
# homeassistant.components.tibber
-pyTibber==0.16.1
+pyTibber==0.16.2
# homeassistant.components.nextbus
py_nextbusnext==0.1.4
@@ -660,13 +690,13 @@ pyalmond==0.0.2
pyarlo==0.2.4
# homeassistant.components.atag
-pyatag==0.3.4.4
+pyatag==0.3.5.3
# homeassistant.components.netatmo
pyatmo==4.2.2
# homeassistant.components.apple_tv
-pyatv==0.7.5
+pyatv==0.7.7
# homeassistant.components.blackbird
pyblackbird==0.5
@@ -678,7 +708,10 @@ pybotvac==0.0.20
pycfdns==1.2.1
# homeassistant.components.cast
-pychromecast==7.7.2
+pychromecast==9.1.2
+
+# homeassistant.components.climacell
+pyclimacell==0.18.0
# homeassistant.components.comfoconnect
pycomfoconnect==0.4
@@ -690,7 +723,7 @@ pycoolmasternet-async==0.1.2
pydaikin==2.4.1
# homeassistant.components.deconz
-pydeconz==77
+pydeconz==78
# homeassistant.components.dexcom
pydexcom==0.2.0
@@ -699,11 +732,14 @@ pydexcom==0.2.0
pydispatcher==2.0.5
# homeassistant.components.econet
-pyeconet==0.1.12
+pyeconet==0.1.13
# homeassistant.components.everlights
pyeverlights==0.1.0
+# homeassistant.components.ezviz
+pyezviz==0.1.8.7
+
# homeassistant.components.fido
pyfido==2.1.1
@@ -733,16 +769,19 @@ pygatt[GATTTOOL]==4.0.5
pygti==0.9.2
# homeassistant.components.version
-pyhaversion==3.4.2
+pyhaversion==21.3.0
# homeassistant.components.heos
pyheos==0.7.2
+# homeassistant.components.hive
+pyhiveapi==0.4.1
+
# homeassistant.components.homematic
-pyhomematic==0.1.71
+pyhomematic==0.1.72
# homeassistant.components.icloud
-pyicloud==0.9.7
+pyicloud==0.10.2
# homeassistant.components.insteon
pyinsteon==1.0.9
@@ -757,25 +796,31 @@ pyipp==0.11.0
pyiqvia==0.3.1
# homeassistant.components.isy994
-pyisy==2.1.0
+pyisy==2.1.1
# homeassistant.components.kira
pykira==0.1.1
+# homeassistant.components.kmtronic
+pykmtronic==0.0.3
+
# homeassistant.components.kodi
-pykodi==0.2.1
+pykodi==0.2.4
# homeassistant.components.kulersky
-pykulersky==0.4.0
+pykulersky==0.5.2
# homeassistant.components.lastfm
-pylast==4.1.0
+pylast==4.2.0
# homeassistant.components.forked_daapd
pylibrespot-java==0.1.0
# homeassistant.components.litejet
-pylitejet==0.1
+pylitejet==0.3.0
+
+# homeassistant.components.litterrobot
+pylitterbot==2021.2.8
# homeassistant.components.lutron_caseta
pylutron-caseta==0.9.0
@@ -786,6 +831,9 @@ pymailgunner==1.4
# homeassistant.components.firmata
pymata-express==1.19
+# homeassistant.components.mazda
+pymazda==0.0.9
+
# homeassistant.components.melcloud
pymelcloud==2.5.2
@@ -802,10 +850,13 @@ pymodbus==2.3.0
pymonoprice==0.3
# homeassistant.components.myq
-pymyq==2.0.14
+pymyq==3.0.4
+
+# homeassistant.components.mysensors
+pymysensors==0.21.0
# homeassistant.components.nuki
-pynuki==1.3.8
+pynuki==1.4.1
# homeassistant.components.nut
pynut2==2.1.2
@@ -826,7 +877,7 @@ pyopenuv==1.0.9
pyopnsense==0.2.0
# homeassistant.components.opentherm_gw
-pyotgw==1.0b1
+pyotgw==1.1b1
# homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp
@@ -834,13 +885,19 @@ pyotgw==1.0b1
pyotp==2.3.0
# homeassistant.components.openweathermap
-pyowm==3.1.1
+pyowm==3.2.0
# homeassistant.components.onewire
pyownet==0.10.0.post1
+# homeassistant.components.lcn
+pypck==0.7.9
+
+# homeassistant.components.plaato
+pyplaato==0.0.15
+
# homeassistant.components.point
-pypoint==2.0.0
+pypoint==2.1.0
# homeassistant.components.profiler
pyprof2calltree==1.4.5
@@ -854,6 +911,9 @@ pyqwikswitch==0.93
# homeassistant.components.risco
pyrisco==0.3.1
+# homeassistant.components.rituals_perfume_genie
+pyrituals==0.0.2
+
# homeassistant.components.ruckus_unleashed
pyruckus==0.12
@@ -872,7 +932,7 @@ pysignalclirestapi==0.3.4
pysma==0.3.5
# homeassistant.components.smappee
-pysmappee==0.2.16
+pysmappee==0.2.17
# homeassistant.components.smartthings
pysmartapp==0.3.3
@@ -884,7 +944,7 @@ pysmartthings==0.7.6
pysoma==0.0.10
# homeassistant.components.sonos
-pysonos==0.0.37
+pysonos==0.0.42
# homeassistant.components.spc
pyspcwebgw==0.4.0
@@ -896,19 +956,19 @@ pysqueezebox==0.5.5
pysyncthru==0.7.0
# homeassistant.components.ecobee
-python-ecobee-api==0.2.8
+python-ecobee-api==0.2.10
# homeassistant.components.darksky
python-forecastio==1.4.0
# homeassistant.components.izone
-python-izone==1.1.3
+python-izone==1.1.4
# homeassistant.components.juicenet
python-juicenet==1.0.1
# homeassistant.components.xiaomi_miio
-python-miio==0.5.4
+python-miio==0.5.5
# homeassistant.components.nest
python-nest==4.1.0
@@ -916,6 +976,9 @@ python-nest==4.1.0
# homeassistant.components.ozw
python-openzwave-mqtt[mqtt-client]==1.4.0
+# homeassistant.components.smarttub
+python-smarttub==0.0.19
+
# homeassistant.components.songpal
python-songpal==0.12
@@ -941,10 +1004,10 @@ pytraccar==0.9.0
pytradfri[async]==7.0.6
# homeassistant.components.vera
-pyvera==0.3.11
+pyvera==0.3.13
# homeassistant.components.vesync
-pyvesync==1.2.0
+pyvesync==1.3.1
# homeassistant.components.vizio
pyvizio==0.1.57
@@ -956,13 +1019,13 @@ pyvolumio==0.1.3
pywebpush==1.9.2
# homeassistant.components.wemo
-pywemo==0.6.1
+pywemo==0.6.3
# homeassistant.components.wilight
-pywilight==0.0.66
+pywilight==0.0.68
# homeassistant.components.zerproc
-pyzerproc==0.4.7
+pyzerproc==0.4.8
# homeassistant.components.rachio
rachiopy==1.0.3
@@ -974,19 +1037,19 @@ regenmaschine==3.0.0
restrictedpython==5.1
# homeassistant.components.rflink
-rflink==0.0.55
+rflink==0.0.58
# homeassistant.components.ring
ring_doorbell==0.6.2
# homeassistant.components.roku
-rokuecp==0.6.0
+rokuecp==0.8.1
# homeassistant.components.roomba
roombapy==1.6.2
# homeassistant.components.roon
-roonapi==0.0.31
+roonapi==0.0.32
# homeassistant.components.rpi_power
rpi-bad-power==0.1.0
@@ -998,17 +1061,20 @@ rxv==0.6.0
samsungctl[websocket]==0.7.1
# homeassistant.components.samsungtv
-samsungtvws==1.4.0
+samsungtvws==1.6.0
# homeassistant.components.dhcp
scapy==2.4.4
+# homeassistant.components.screenlogic
+screenlogicpy==0.2.1
+
# homeassistant.components.emulated_kasa
# homeassistant.components.sense
-sense_energy==0.8.1
+sense_energy==0.9.0
# homeassistant.components.sentry
-sentry-sdk==0.19.5
+sentry-sdk==1.0.0
# homeassistant.components.sharkiq
sharkiqpy==0.1.8
@@ -1017,7 +1083,7 @@ sharkiqpy==0.1.8
simplehound==0.3
# homeassistant.components.simplisafe
-simplisafe-python==9.6.2
+simplisafe-python==9.6.9
# homeassistant.components.slack
slackclient==2.5.0
@@ -1050,17 +1116,17 @@ sonarr==0.3.0
speak2mary==1.4.0
# homeassistant.components.speedtestdotnet
-speedtest-cli==2.1.2
+speedtest-cli==2.1.3
# homeassistant.components.spider
spiderpy==1.4.2
# homeassistant.components.spotify
-spotipy==2.16.1
+spotipy==2.17.1
# homeassistant.components.recorder
# homeassistant.components.sql
-sqlalchemy==1.3.22
+sqlalchemy==1.3.23
# homeassistant.components.srp_energy
srpenergy==1.3.2
@@ -1077,6 +1143,9 @@ statsd==3.2.1
# homeassistant.components.traccar
stringcase==1.2.0
+# homeassistant.components.subaru
+subarulink==0.3.12
+
# homeassistant.components.solarlog
sunwatcher==0.2.1
@@ -1084,28 +1153,28 @@ sunwatcher==0.2.1
surepy==0.4.0
# homeassistant.components.synology_dsm
-synologydsm-api==1.0.1
+synologydsm-api==1.0.2
# homeassistant.components.tellduslive
tellduslive==0.10.11
# homeassistant.components.powerwall
-tesla-powerwall==0.3.3
+tesla-powerwall==0.3.5
# homeassistant.components.tesla
-teslajsonpy==0.10.4
+teslajsonpy==0.11.5
# homeassistant.components.toon
toonapi==0.2.0
# homeassistant.components.totalconnect
-total_connect_client==0.55
+total_connect_client==0.57
# homeassistant.components.transmission
transmissionrpc==0.11
# homeassistant.components.tuya
-tuyaha==0.0.9
+tuyaha==0.0.10
# homeassistant.components.twentemilieu
twentemilieu==0.3.0
@@ -1120,7 +1189,7 @@ twinkly-client==0.0.2
upb_lib==0.4.12
# homeassistant.components.upcloud
-upcloud-api==0.4.5
+upcloud-api==1.0.1
# homeassistant.components.huawei_lte
# homeassistant.components.syncthru
@@ -1133,22 +1202,22 @@ uvcclient==0.11.0
vilfo-api-client==0.3.2
# homeassistant.components.verisure
-vsure==1.6.1
+vsure==1.7.3
# homeassistant.components.vultr
vultr==0.1.2
# homeassistant.components.wake_on_lan
-wakeonlan==1.1.6
+wakeonlan==2.0.0
# homeassistant.components.folder_watcher
-watchdog==0.8.3
+watchdog==1.0.2
# homeassistant.components.wiffi
wiffi==1.0.1
# homeassistant.components.withings
-withings-api==2.1.6
+withings-api==2.3.2
# homeassistant.components.wled
wled==0.4.4
@@ -1159,6 +1228,9 @@ wolf_smartset==0.1.8
# homeassistant.components.xbox
xbox-webapi==2.0.8
+# homeassistant.components.knx
+xknx==0.18.0
+
# homeassistant.components.bluesound
# homeassistant.components.rest
# homeassistant.components.startca
@@ -1166,6 +1238,9 @@ xbox-webapi==2.0.8
# homeassistant.components.zestimate
xmltodict==0.12.0
+# homeassistant.components.august
+yalexs==1.1.10
+
# homeassistant.components.yeelight
yeelight==0.5.4
@@ -1173,16 +1248,16 @@ yeelight==0.5.4
zeep[async]==4.0.0
# homeassistant.components.zeroconf
-zeroconf==0.28.8
+zeroconf==0.29.0
# homeassistant.components.zha
-zha-quirks==0.0.53
+zha-quirks==0.0.55
# homeassistant.components.zha
zigpy-cc==0.5.2
# homeassistant.components.zha
-zigpy-deconz==0.11.1
+zigpy-deconz==0.12.0
# homeassistant.components.zha
zigpy-xbee==0.13.0
@@ -1191,10 +1266,10 @@ zigpy-xbee==0.13.0
zigpy-zigate==0.7.3
# homeassistant.components.zha
-zigpy-znp==0.3.0
+zigpy-znp==0.4.0
# homeassistant.components.zha
-zigpy==0.32.0
+zigpy==0.33.0
# homeassistant.components.zwave_js
-zwave-js-server-python==0.14.1
+zwave-js-server-python==0.23.1
diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt
index 953c7d75394bd6..06e87a5c51c516 100644
--- a/requirements_test_pre_commit.txt
+++ b/requirements_test_pre_commit.txt
@@ -3,9 +3,13 @@
bandit==1.7.0
black==20.8b1
codespell==2.0.0
-flake8-docstrings==1.5.0
-flake8==3.8.4
-isort==5.5.3
-pydocstyle==5.1.1
-pyupgrade==2.7.2
+flake8-comprehensions==3.4.0
+flake8-docstrings==1.6.0
+flake8-noqa==1.1.0
+flake8==3.9.0
+isort==5.7.0
+pycodestyle==2.7.0
+pydocstyle==6.0.0
+pyflakes==2.3.1
+pyupgrade==2.11.0
yamllint==1.24.2
diff --git a/script/bootstrap b/script/bootstrap
index 32e9d11bc4d39d..b641ec7e8c0384 100755
--- a/script/bootstrap
+++ b/script/bootstrap
@@ -6,14 +6,6 @@ set -e
cd "$(dirname "$0")/.."
-# Add default vscode settings if not existing
-SETTINGS_FILE=./.vscode/settings.json
-SETTINGS_TEMPLATE_FILE=./.vscode/settings.default.json
-if [ ! -f "$SETTINGS_FILE" ]; then
- echo "Copy $SETTINGS_TEMPLATE_FILE to $SETTINGS_FILE."
- cp "$SETTINGS_TEMPLATE_FILE" "$SETTINGS_FILE"
-fi
-
echo "Installing development dependencies..."
python3 -m pip install wheel --constraint homeassistant/package_constraints.txt
-python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) $(grep awesomeversion requirements_test.txt) --constraint homeassistant/package_constraints.txt
+python3 -m pip install tox tox-pip-version colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) $(grep awesomeversion requirements.txt) --constraint homeassistant/package_constraints.txt
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index dc1ef9a471bb61..ac1e4bc2ed9e43 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -21,14 +21,12 @@
"blinkt",
"bluepy",
"bme680",
- "credstash",
"decora",
"decora_wifi",
"envirophat",
"evdev",
"face_recognition",
"i2csense",
- "nuimo",
"opencv-python-headless",
"py_noaa",
"pybluez",
@@ -48,7 +46,7 @@
"VL53L1X2",
)
-IGNORE_PIN = ("colorlog>2.1,<3", "keyring>=9.3,<10.0", "urllib3")
+IGNORE_PIN = ("colorlog>2.1,<3", "urllib3")
URL_PIN = (
"https://developers.home-assistant.io/docs/"
@@ -72,8 +70,9 @@
# https://github.com/encode/httpcore/issues/239
httpcore>=0.12.3
-# Constrain httplib2 to protect against CVE-2020-11078
-httplib2>=0.18.0
+# Constrain httplib2 to protect against GHSA-93xj-8mrv-444m
+# https://github.com/advisories/GHSA-93xj-8mrv-444m
+httplib2>=0.19.0
# gRPC 1.32+ currently causes issues on ARMv7, see:
# https://github.com/home-assistant/core/issues/40148
@@ -97,6 +96,7 @@
"check-json",
"no-commit-to-branch",
"prettier",
+ "python-typing-update",
)
diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py
index eba657f64cf19d..81c3c883965480 100644
--- a/script/hassfest/codeowners.py
+++ b/script/hassfest/codeowners.py
@@ -1,5 +1,5 @@
"""Generate CODEOWNERS."""
-from typing import Dict
+from __future__ import annotations
from .model import Config, Integration
@@ -33,7 +33,7 @@
"""
-def generate_and_validate(integrations: Dict[str, Integration]):
+def generate_and_validate(integrations: dict[str, Integration]):
"""Generate CODEOWNERS."""
parts = [BASE]
@@ -61,7 +61,7 @@ def generate_and_validate(integrations: Dict[str, Integration]):
return "\n".join(parts)
-def validate(integrations: Dict[str, Integration], config: Config):
+def validate(integrations: dict[str, Integration], config: Config):
"""Validate CODEOWNERS."""
codeowners_path = config.root / "CODEOWNERS"
config.cache["codeowners"] = content = generate_and_validate(integrations)
@@ -79,7 +79,7 @@ def validate(integrations: Dict[str, Integration], config: Config):
return
-def generate(integrations: Dict[str, Integration], config: Config):
+def generate(integrations: dict[str, Integration], config: Config):
"""Generate CODEOWNERS."""
codeowners_path = config.root / "CODEOWNERS"
with open(str(codeowners_path), "w") as fp:
diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py
index 9ae0daee1b02d5..e4d1be7bc4694e 100644
--- a/script/hassfest/config_flow.py
+++ b/script/hassfest/config_flow.py
@@ -1,6 +1,7 @@
"""Generate config flow file."""
+from __future__ import annotations
+
import json
-from typing import Dict
from .model import Config, Integration
@@ -90,7 +91,7 @@ def validate_integration(config: Config, integration: Integration):
)
-def generate_and_validate(integrations: Dict[str, Integration], config: Config):
+def generate_and_validate(integrations: dict[str, Integration], config: Config):
"""Validate and generate config flow data."""
domains = []
@@ -117,7 +118,7 @@ def generate_and_validate(integrations: Dict[str, Integration], config: Config):
return BASE.format(json.dumps(domains, indent=4))
-def validate(integrations: Dict[str, Integration], config: Config):
+def validate(integrations: dict[str, Integration], config: Config):
"""Validate config flow file."""
config_flow_path = config.root / "homeassistant/generated/config_flows.py"
config.cache["config_flow"] = content = generate_and_validate(integrations, config)
@@ -136,7 +137,7 @@ def validate(integrations: Dict[str, Integration], config: Config):
return
-def generate(integrations: Dict[str, Integration], config: Config):
+def generate(integrations: dict[str, Integration], config: Config):
"""Generate config flow file."""
config_flow_path = config.root / "homeassistant/generated/config_flows.py"
with open(str(config_flow_path), "w") as fp:
diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py
index c561c5d9f167e5..06e38902060664 100644
--- a/script/hassfest/coverage.py
+++ b/script/hassfest/coverage.py
@@ -1,6 +1,7 @@
"""Validate coverage files."""
+from __future__ import annotations
+
from pathlib import Path
-from typing import Dict
from .model import Config, Integration
@@ -69,7 +70,7 @@
}
-def validate(integrations: Dict[str, Integration], config: Config):
+def validate(integrations: dict[str, Integration], config: Config):
"""Validate coverage."""
coverage_path = config.root / ".coveragerc"
@@ -105,8 +106,8 @@ def validate(integrations: Dict[str, Integration], config: Config):
if (
not line.startswith("homeassistant/components/")
- or not len(path.parts) == 4
- or not path.parts[-1] == "*"
+ or len(path.parts) != 4
+ or path.parts[-1] != "*"
):
continue
diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py
index 6283b2d8665c09..cb7458af154d26 100644
--- a/script/hassfest/dependencies.py
+++ b/script/hassfest/dependencies.py
@@ -1,9 +1,11 @@
"""Validate dependencies."""
+from __future__ import annotations
+
import ast
from pathlib import Path
-from typing import Dict, Set
from homeassistant.requirements import DISCOVERY_INTEGRATIONS
+from homeassistant.setup import BASE_PLATFORMS
from .model import Integration
@@ -14,7 +16,7 @@ class ImportCollector(ast.NodeVisitor):
def __init__(self, integration: Integration):
"""Initialize the import collector."""
self.integration = integration
- self.referenced: Dict[Path, Set[str]] = {}
+ self.referenced: dict[Path, set[str]] = {}
# Current file or dir we're inspecting
self._cur_fil_dir = None
@@ -116,22 +118,7 @@ def visit_Attribute(self, node):
"websocket_api",
"zone",
# Entity integrations with platforms
- "alarm_control_panel",
- "binary_sensor",
- "climate",
- "cover",
- "device_tracker",
- "fan",
- "humidifier",
- "image_processing",
- "light",
- "lock",
- "media_player",
- "scene",
- "sensor",
- "switch",
- "vacuum",
- "water_heater",
+ *BASE_PLATFORMS,
# Other
"mjpeg", # base class, has no reqs or component to load.
"stream", # Stream cannot install on all systems, can be imported without reqs.
@@ -147,6 +134,8 @@ def visit_Attribute(self, node):
# Demo
("demo", "manual"),
("demo", "openalpr_local"),
+ # Migration wizard from zwave to ozw.
+ "ozw",
# This should become a helper method that integrations can submit data to
("websocket_api", "lovelace"),
("websocket_api", "shopping_list"),
@@ -154,7 +143,7 @@ def visit_Attribute(self, node):
}
-def calc_allowed_references(integration: Integration) -> Set[str]:
+def calc_allowed_references(integration: Integration) -> set[str]:
"""Return a set of allowed references."""
allowed_references = (
ALLOWED_USED_COMPONENTS
@@ -171,9 +160,9 @@ def calc_allowed_references(integration: Integration) -> Set[str]:
def find_non_referenced_integrations(
- integrations: Dict[str, Integration],
+ integrations: dict[str, Integration],
integration: Integration,
- references: Dict[Path, Set[str]],
+ references: dict[Path, set[str]],
):
"""Find intergrations that are not allowed to be referenced."""
allowed_references = calc_allowed_references(integration)
@@ -219,7 +208,7 @@ def find_non_referenced_integrations(
def validate_dependencies(
- integrations: Dict[str, Integration], integration: Integration
+ integrations: dict[str, Integration], integration: Integration
):
"""Validate all dependencies."""
# Some integrations are allowed to have violations.
@@ -242,7 +231,7 @@ def validate_dependencies(
)
-def validate(integrations: Dict[str, Integration], config):
+def validate(integrations: dict[str, Integration], config):
"""Handle dependencies for integrations."""
# check for non-existing dependencies
for integration in integrations.values():
diff --git a/script/hassfest/dhcp.py b/script/hassfest/dhcp.py
index fbf695a9f7378c..a3abe80063e797 100644
--- a/script/hassfest/dhcp.py
+++ b/script/hassfest/dhcp.py
@@ -1,6 +1,7 @@
"""Generate dhcp file."""
+from __future__ import annotations
+
import json
-from typing import Dict, List
from .model import Config, Integration
@@ -16,7 +17,7 @@
""".strip()
-def generate_and_validate(integrations: List[Dict[str, str]]):
+def generate_and_validate(integrations: list[dict[str, str]]):
"""Validate and generate dhcp data."""
match_list = []
@@ -37,7 +38,7 @@ def generate_and_validate(integrations: List[Dict[str, str]]):
return BASE.format(json.dumps(match_list, indent=4))
-def validate(integrations: Dict[str, Integration], config: Config):
+def validate(integrations: dict[str, Integration], config: Config):
"""Validate dhcp file."""
dhcp_path = config.root / "homeassistant/generated/dhcp.py"
config.cache["dhcp"] = content = generate_and_validate(integrations)
@@ -56,7 +57,7 @@ def validate(integrations: Dict[str, Integration], config: Config):
return
-def generate(integrations: Dict[str, Integration], config: Config):
+def generate(integrations: dict[str, Integration], config: Config):
"""Generate dhcp file."""
dhcp_path = config.root / "homeassistant/generated/dhcp.py"
with open(str(dhcp_path), "w") as fp:
diff --git a/script/hassfest/json.py b/script/hassfest/json.py
index 39c1f6f13a926e..49ebb05bbeafc2 100644
--- a/script/hassfest/json.py
+++ b/script/hassfest/json.py
@@ -1,6 +1,7 @@
"""Validate integration JSON files."""
+from __future__ import annotations
+
import json
-from typing import Dict
from .model import Integration
@@ -20,7 +21,7 @@ def validate_json_files(integration: Integration):
return
-def validate(integrations: Dict[str, Integration], config):
+def validate(integrations: dict[str, Integration], config):
"""Handle JSON files inside integrations."""
if not config.specific_integrations:
return
diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py
index e02df86f4e1899..55bfa717a3fb64 100644
--- a/script/hassfest/manifest.py
+++ b/script/hassfest/manifest.py
@@ -1,12 +1,13 @@
"""Manifest validation."""
-from typing import Dict
+from __future__ import annotations
+
from urllib.parse import urlparse
-from awesomeversion import AwesomeVersion
-from awesomeversion.strategy import AwesomeVersionStrategy
import voluptuous as vol
from voluptuous.humanize import humanize_error
+from homeassistant.loader import validate_custom_integration_version
+
from .model import Integration
DOCUMENTATION_URL_SCHEMA = "https"
@@ -53,15 +54,9 @@ def verify_uppercase(value: str):
def verify_version(value: str):
"""Verify the version."""
- version = AwesomeVersion(value)
- if version.strategy not in [
- AwesomeVersionStrategy.CALVER,
- AwesomeVersionStrategy.SEMVER,
- AwesomeVersionStrategy.SIMPLEVER,
- AwesomeVersionStrategy.BUILDVER,
- ]:
+ if not validate_custom_integration_version(value):
raise vol.Invalid(
- f"'{version}' is not a valid version. This will cause a future version of Home Assistant to block this integration.",
+ f"'{value}' is not a valid version. This will cause a future version of Home Assistant to block this integration.",
)
return value
@@ -125,7 +120,7 @@ def validate_version(integration: Integration):
Will be removed when the version key is no longer optional for custom integrations.
"""
if not integration.manifest.get("version"):
- integration.add_warning(
+ integration.add_error(
"manifest",
"No 'version' key in the manifest file. This will cause a future version of Home Assistant to block this integration.",
)
@@ -151,7 +146,7 @@ def validate_manifest(integration: Integration):
validate_version(integration)
-def validate(integrations: Dict[str, Integration], config):
+def validate(integrations: dict[str, Integration], config):
"""Handle all integrations manifests."""
for integration in integrations.values():
if integration.manifest:
diff --git a/script/hassfest/model.py b/script/hassfest/model.py
index 0750ef10b6c475..c5b8dbff618753 100644
--- a/script/hassfest/model.py
+++ b/script/hassfest/model.py
@@ -1,8 +1,10 @@
"""Models for manifest validator."""
+from __future__ import annotations
+
import importlib
import json
import pathlib
-from typing import Any, Dict, List, Optional
+from typing import Any
import attr
@@ -24,12 +26,12 @@ def __str__(self) -> str:
class Config:
"""Config for the run."""
- specific_integrations: Optional[pathlib.Path] = attr.ib()
+ specific_integrations: pathlib.Path | None = attr.ib()
root: pathlib.Path = attr.ib()
action: str = attr.ib()
requirements: bool = attr.ib()
- errors: List[Error] = attr.ib(factory=list)
- cache: Dict[str, Any] = attr.ib(factory=dict)
+ errors: list[Error] = attr.ib(factory=list)
+ cache: dict[str, Any] = attr.ib(factory=dict)
def add_error(self, *args, **kwargs):
"""Add an error."""
@@ -65,9 +67,9 @@ def load_dir(cls, path: pathlib.Path):
return integrations
path: pathlib.Path = attr.ib()
- manifest: Optional[dict] = attr.ib(default=None)
- errors: List[Error] = attr.ib(factory=list)
- warnings: List[Error] = attr.ib(factory=list)
+ manifest: dict | None = attr.ib(default=None)
+ errors: list[Error] = attr.ib(factory=list)
+ warnings: list[Error] = attr.ib(factory=list)
@property
def domain(self) -> str:
@@ -80,17 +82,17 @@ def core(self) -> bool:
return self.path.as_posix().startswith("homeassistant/components")
@property
- def disabled(self) -> Optional[str]:
+ def disabled(self) -> str | None:
"""List of disabled."""
return self.manifest.get("disabled")
@property
- def requirements(self) -> List[str]:
+ def requirements(self) -> list[str]:
"""List of requirements."""
return self.manifest.get("requirements", [])
@property
- def dependencies(self) -> List[str]:
+ def dependencies(self) -> list[str]:
"""List of dependencies."""
return self.manifest.get("dependencies", [])
diff --git a/script/hassfest/mqtt.py b/script/hassfest/mqtt.py
index fdc16895d8cd50..718df4ac827e9e 100644
--- a/script/hassfest/mqtt.py
+++ b/script/hassfest/mqtt.py
@@ -1,7 +1,8 @@
"""Generate MQTT file."""
+from __future__ import annotations
+
from collections import defaultdict
import json
-from typing import Dict
from .model import Config, Integration
@@ -17,7 +18,7 @@
""".strip()
-def generate_and_validate(integrations: Dict[str, Integration]):
+def generate_and_validate(integrations: dict[str, Integration]):
"""Validate and generate MQTT data."""
data = defaultdict(list)
@@ -39,7 +40,7 @@ def generate_and_validate(integrations: Dict[str, Integration]):
return BASE.format(json.dumps(data, indent=4))
-def validate(integrations: Dict[str, Integration], config: Config):
+def validate(integrations: dict[str, Integration], config: Config):
"""Validate MQTT file."""
mqtt_path = config.root / "homeassistant/generated/mqtt.py"
config.cache["mqtt"] = content = generate_and_validate(integrations)
@@ -57,7 +58,7 @@ def validate(integrations: Dict[str, Integration], config: Config):
return
-def generate(integrations: Dict[str, Integration], config: Config):
+def generate(integrations: dict[str, Integration], config: Config):
"""Generate MQTT file."""
mqtt_path = config.root / "homeassistant/generated/mqtt.py"
with open(str(mqtt_path), "w") as fp:
diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py
index b51cbff7185408..fa5a36c655914e 100644
--- a/script/hassfest/requirements.py
+++ b/script/hassfest/requirements.py
@@ -1,4 +1,6 @@
"""Validate requirements."""
+from __future__ import annotations
+
from collections import deque
import json
import operator
@@ -6,7 +8,6 @@
import re
import subprocess
import sys
-from typing import Dict, Set
from stdlib_list import stdlib_list
from tqdm import tqdm
@@ -58,7 +59,7 @@ def normalize_package_name(requirement: str) -> str:
return package
-def validate(integrations: Dict[str, Integration], config: Config):
+def validate(integrations: dict[str, Integration], config: Config):
"""Handle requirements for integrations."""
ensure_cache()
@@ -153,7 +154,7 @@ def ensure_cache():
PIPDEPTREE_CACHE = cache
-def get_requirements(integration: Integration, packages: Set[str]) -> Set[str]:
+def get_requirements(integration: Integration, packages: set[str]) -> set[str]:
"""Return all (recursively) requirements for an integration."""
ensure_cache()
@@ -184,7 +185,7 @@ def get_requirements(integration: Integration, packages: Set[str]) -> Set[str]:
return all_requirements
-def install_requirements(integration: Integration, requirements: Set[str]) -> bool:
+def install_requirements(integration: Integration, requirements: set[str]) -> bool:
"""Install integration requirements.
Return True if successful.
diff --git a/script/hassfest/services.py b/script/hassfest/services.py
index c07d3bbc6efd9a..9577d134ccc148 100644
--- a/script/hassfest/services.py
+++ b/script/hassfest/services.py
@@ -1,7 +1,8 @@
"""Validate dependencies."""
+from __future__ import annotations
+
import pathlib
import re
-from typing import Dict
import voluptuous as vol
from voluptuous.humanize import humanize_error
@@ -24,10 +25,12 @@ def exists(value):
FIELD_SCHEMA = vol.Schema(
{
vol.Required("description"): str,
+ vol.Optional("name"): str,
vol.Optional("example"): exists,
vol.Optional("default"): exists,
vol.Optional("values"): exists,
vol.Optional("required"): bool,
+ vol.Optional("advanced"): bool,
vol.Optional(CONF_SELECTOR): selector.validate_selector,
}
)
@@ -35,6 +38,10 @@ def exists(value):
SERVICE_SCHEMA = vol.Schema(
{
vol.Required("description"): str,
+ vol.Optional("name"): str,
+ vol.Optional("target"): vol.Any(
+ selector.TargetSelector.CONFIG_SCHEMA, None # pylint: disable=no-member
+ ),
vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}),
}
)
@@ -60,7 +67,9 @@ def validate_services(integration: Integration):
"""Validate services."""
# Find if integration uses services
has_services = grep_dir(
- integration.path, "**/*.py", r"hass\.services\.(register|async_register)"
+ integration.path,
+ "**/*.py",
+ r"(hass\.services\.(register|async_register))|async_register_entity_service",
)
if not has_services:
@@ -85,7 +94,7 @@ def validate_services(integration: Integration):
)
-def validate(integrations: Dict[str, Integration], config):
+def validate(integrations: dict[str, Integration], config):
"""Handle dependencies for integrations."""
# check services.yaml is cool
for integration in integrations.values():
diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py
index c9b3b8931187ca..c71d5432adf70e 100644
--- a/script/hassfest/ssdp.py
+++ b/script/hassfest/ssdp.py
@@ -1,7 +1,8 @@
"""Generate ssdp file."""
+from __future__ import annotations
+
from collections import OrderedDict, defaultdict
import json
-from typing import Dict
from .model import Config, Integration
@@ -22,7 +23,7 @@ def sort_dict(value):
return OrderedDict((key, value[key]) for key in sorted(value))
-def generate_and_validate(integrations: Dict[str, Integration]):
+def generate_and_validate(integrations: dict[str, Integration]):
"""Validate and generate ssdp data."""
data = defaultdict(list)
@@ -44,7 +45,7 @@ def generate_and_validate(integrations: Dict[str, Integration]):
return BASE.format(json.dumps(data, indent=4))
-def validate(integrations: Dict[str, Integration], config: Config):
+def validate(integrations: dict[str, Integration], config: Config):
"""Validate ssdp file."""
ssdp_path = config.root / "homeassistant/generated/ssdp.py"
config.cache["ssdp"] = content = generate_and_validate(integrations)
@@ -62,7 +63,7 @@ def validate(integrations: Dict[str, Integration], config: Config):
return
-def generate(integrations: Dict[str, Integration], config: Config):
+def generate(integrations: dict[str, Integration], config: Config):
"""Generate ssdp file."""
ssdp_path = config.root / "homeassistant/generated/ssdp.py"
with open(str(ssdp_path), "w") as fp:
diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py
index 75886eedc6f836..b7a6341d25c087 100644
--- a/script/hassfest/translations.py
+++ b/script/hassfest/translations.py
@@ -1,9 +1,10 @@
"""Validate integration translation files."""
+from __future__ import annotations
+
from functools import partial
from itertools import chain
import json
import re
-from typing import Dict
import voluptuous as vol
from voluptuous.humanize import humanize_error
@@ -295,7 +296,7 @@ def validate_translation_file(config: Config, integration: Integration, all_stri
)
-def validate(integrations: Dict[str, Integration], config: Config):
+def validate(integrations: dict[str, Integration], config: Config):
"""Handle JSON files inside integrations."""
if config.specific_integrations:
all_strings = None
diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py
index 61162b02761026..907c6aacefff73 100644
--- a/script/hassfest/zeroconf.py
+++ b/script/hassfest/zeroconf.py
@@ -1,7 +1,8 @@
"""Generate zeroconf file."""
+from __future__ import annotations
+
from collections import OrderedDict, defaultdict
import json
-from typing import Dict
from .model import Config, Integration
@@ -19,7 +20,7 @@
""".strip()
-def generate_and_validate(integrations: Dict[str, Integration]):
+def generate_and_validate(integrations: dict[str, Integration]):
"""Validate and generate zeroconf data."""
service_type_dict = defaultdict(list)
homekit_dict = {}
@@ -89,7 +90,7 @@ def generate_and_validate(integrations: Dict[str, Integration]):
return BASE.format(json.dumps(zeroconf, indent=4), json.dumps(homekit, indent=4))
-def validate(integrations: Dict[str, Integration], config: Config):
+def validate(integrations: dict[str, Integration], config: Config):
"""Validate zeroconf file."""
zeroconf_path = config.root / "homeassistant/generated/zeroconf.py"
config.cache["zeroconf"] = content = generate_and_validate(integrations)
@@ -108,7 +109,7 @@ def validate(integrations: Dict[str, Integration], config: Config):
return
-def generate(integrations: Dict[str, Integration], config: Config):
+def generate(integrations: dict[str, Integration], config: Config):
"""Generate zeroconf file."""
zeroconf_path = config.root / "homeassistant/generated/zeroconf.py"
with open(str(zeroconf_path), "w") as fp:
diff --git a/script/lazytox.py b/script/lazytox.py
index 5a8837b8154dc4..1f2f4cf02b0217 100755
--- a/script/lazytox.py
+++ b/script/lazytox.py
@@ -32,13 +32,14 @@ def printc(the_color, *args):
return
try:
print(escape_codes[the_color] + msg + escape_codes["reset"])
- except KeyError:
+ except KeyError as err:
print(msg)
- raise ValueError(f"Invalid color {the_color}")
+ raise ValueError(f"Invalid color {the_color}") from err
def validate_requirements_ok():
"""Validate requirements, returns True of ok."""
+ # pylint: disable=import-error,import-outside-toplevel
from gen_requirements_all import main as req_main
return req_main(True) == 0
@@ -67,7 +68,6 @@ async def async_exec(*args, display=False):
printc("cyan", *argsp)
try:
kwargs = {
- "loop": LOOP,
"stdout": asyncio.subprocess.PIPE,
"stderr": asyncio.subprocess.STDOUT,
}
@@ -232,15 +232,7 @@ async def main():
if __name__ == "__main__":
- LOOP = (
- asyncio.ProactorEventLoop()
- if sys.platform == "win32"
- else asyncio.get_event_loop()
- )
-
try:
- LOOP.run_until_complete(main())
+ asyncio.run(main())
except (FileNotFoundError, KeyboardInterrupt):
pass
- finally:
- LOOP.close()
diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py
index 4cf1624eb4b821..10de17e45ee13e 100644
--- a/script/scaffold/generate.py
+++ b/script/scaffold/generate.py
@@ -163,6 +163,9 @@ def _custom_tasks(template, info) -> None:
}
},
"abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
+ "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
diff --git a/script/scaffold/model.py b/script/scaffold/model.py
index bfbcfa52544958..f9c71072a1b401 100644
--- a/script/scaffold/model.py
+++ b/script/scaffold/model.py
@@ -1,7 +1,8 @@
"""Models for scaffolding."""
+from __future__ import annotations
+
import json
from pathlib import Path
-from typing import Set
import attr
@@ -21,9 +22,9 @@ class Info:
discoverable: str = attr.ib(default=None)
oauth2: str = attr.ib(default=None)
- files_added: Set[Path] = attr.ib(factory=set)
- tests_added: Set[Path] = attr.ib(factory=set)
- examples_added: Set[Path] = attr.ib(factory=set)
+ files_added: set[Path] = attr.ib(factory=set)
+ tests_added: set[Path] = attr.ib(factory=set)
+ examples_added: set[Path] = attr.ib(factory=set)
@property
def integration_dir(self) -> Path:
diff --git a/script/scaffold/templates/config_flow/integration/__init__.py b/script/scaffold/templates/config_flow/integration/__init__.py
index c6df6e99979587..6c187d1dafece3 100644
--- a/script/scaffold/templates/config_flow/integration/__init__.py
+++ b/script/scaffold/templates/config_flow/integration/__init__.py
@@ -1,4 +1,6 @@
"""The NEW_NAME integration."""
+from __future__ import annotations
+
import asyncio
from homeassistant.config_entries import ConfigEntry
@@ -11,31 +13,26 @@
PLATFORMS = ["light"]
-async def async_setup(hass: HomeAssistant, config: dict):
- """Set up the NEW_NAME component."""
- return True
-
-
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up NEW_NAME from a config entry."""
# TODO Store an API object for your platforms to access
# hass.data[DOMAIN][entry.entry_id] = MyApi(...)
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
-async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/script/scaffold/templates/config_flow/integration/config_flow.py b/script/scaffold/templates/config_flow/integration/config_flow.py
index c9700463c86566..eea7d73b54c175 100644
--- a/script/scaffold/templates/config_flow/integration/config_flow.py
+++ b/script/scaffold/templates/config_flow/integration/config_flow.py
@@ -1,11 +1,16 @@
"""Config flow for NEW_NAME integration."""
+from __future__ import annotations
+
import logging
+from typing import Any
import voluptuous as vol
-from homeassistant import config_entries, core, exceptions
+from homeassistant import config_entries
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import HomeAssistantError
-from .const import DOMAIN # pylint:disable=unused-import
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -19,16 +24,16 @@ class PlaceholderHub:
TODO Remove this placeholder class and replace with things from your PyPI package.
"""
- def __init__(self, host):
+ def __init__(self, host: str) -> None:
"""Initialize."""
self.host = host
- async def authenticate(self, username, password) -> bool:
+ async def authenticate(self, username: str, password: str) -> bool:
"""Test if we can authenticate with the host."""
return True
-async def validate_input(hass: core.HomeAssistant, data):
+async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
@@ -62,7 +67,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
# TODO pick one of the available connection classes in homeassistant/config_entries.py
CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN
- async def async_step_user(self, user_input=None):
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
@@ -88,9 +95,9 @@ async def async_step_user(self, user_input=None):
)
-class CannotConnect(exceptions.HomeAssistantError):
+class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
-class InvalidAuth(exceptions.HomeAssistantError):
+class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
diff --git a/script/scaffold/templates/config_flow/tests/test_config_flow.py b/script/scaffold/templates/config_flow/tests/test_config_flow.py
index 04eab6e683c6c4..674cb921cdd8d5 100644
--- a/script/scaffold/templates/config_flow/tests/test_config_flow.py
+++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py
@@ -4,9 +4,10 @@
from homeassistant import config_entries, setup
from homeassistant.components.NEW_DOMAIN.config_flow import CannotConnect, InvalidAuth
from homeassistant.components.NEW_DOMAIN.const import DOMAIN
+from homeassistant.core import HomeAssistant
-async def test_form(hass):
+async def test_form(hass: HomeAssistant) -> None:
"""Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
@@ -19,8 +20,6 @@ async def test_form(hass):
"homeassistant.components.NEW_DOMAIN.config_flow.PlaceholderHub.authenticate",
return_value=True,
), patch(
- "homeassistant.components.NEW_DOMAIN.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.NEW_DOMAIN.async_setup_entry",
return_value=True,
) as mock_setup_entry:
@@ -41,11 +40,10 @@ async def test_form(hass):
"username": "test-username",
"password": "test-password",
}
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
-async def test_form_invalid_auth(hass):
+async def test_form_invalid_auth(hass: HomeAssistant) -> None:
"""Test we handle invalid auth."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -68,7 +66,7 @@ async def test_form_invalid_auth(hass):
assert result2["errors"] == {"base": "invalid_auth"}
-async def test_form_cannot_connect(hass):
+async def test_form_cannot_connect(hass: HomeAssistant) -> None:
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
diff --git a/script/scaffold/templates/config_flow_discovery/integration/__init__.py b/script/scaffold/templates/config_flow_discovery/integration/__init__.py
index c6df6e99979587..6c187d1dafece3 100644
--- a/script/scaffold/templates/config_flow_discovery/integration/__init__.py
+++ b/script/scaffold/templates/config_flow_discovery/integration/__init__.py
@@ -1,4 +1,6 @@
"""The NEW_NAME integration."""
+from __future__ import annotations
+
import asyncio
from homeassistant.config_entries import ConfigEntry
@@ -11,31 +13,26 @@
PLATFORMS = ["light"]
-async def async_setup(hass: HomeAssistant, config: dict):
- """Set up the NEW_NAME component."""
- return True
-
-
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up NEW_NAME from a config entry."""
# TODO Store an API object for your platforms to access
# hass.data[DOMAIN][entry.entry_id] = MyApi(...)
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
-async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/script/scaffold/templates/config_flow_discovery/integration/config_flow.py b/script/scaffold/templates/config_flow_discovery/integration/config_flow.py
index db5f719ce3db4a..4d2ee2c9f6bf5a 100644
--- a/script/scaffold/templates/config_flow_discovery/integration/config_flow.py
+++ b/script/scaffold/templates/config_flow_discovery/integration/config_flow.py
@@ -2,12 +2,13 @@
import my_pypi_dependency
from homeassistant import config_entries
+from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_flow
from .const import DOMAIN
-async def _async_has_devices(hass) -> bool:
+async def _async_has_devices(hass: HomeAssistant) -> bool:
"""Return if there are devices that can be discovered."""
# TODO Check if there are any devices that can be discovered in the network.
devices = await hass.async_add_executor_job(my_pypi_dependency.discover)
diff --git a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py
index 20b5a03206e848..304df8f9c799cd 100644
--- a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py
+++ b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py
@@ -1,5 +1,8 @@
"""The NEW_NAME integration."""
+from __future__ import annotations
+
import asyncio
+from typing import Any
import voluptuous as vol
@@ -32,7 +35,7 @@
PLATFORMS = ["light"]
-async def async_setup(hass: HomeAssistant, config: dict):
+async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up the NEW_NAME component."""
hass.data[DOMAIN] = {}
@@ -54,7 +57,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up NEW_NAME from a config entry."""
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
@@ -65,28 +68,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
# If using a requests-based API lib
- hass.data[DOMAIN][entry.entry_id] = api.ConfigEntryAuth(hass, entry, session)
+ hass.data[DOMAIN][entry.entry_id] = api.ConfigEntryAuth(hass, session)
# If using an aiohttp-based API lib
hass.data[DOMAIN][entry.entry_id] = api.AsyncConfigEntryAuth(
aiohttp_client.async_get_clientsession(hass), session
)
- for component in PLATFORMS:
+ for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, component)
+ hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
-async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
- hass.config_entries.async_forward_entry_unload(entry, component)
- for component in PLATFORMS
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
]
)
)
diff --git a/script/scaffold/templates/config_flow_oauth2/integration/api.py b/script/scaffold/templates/config_flow_oauth2/integration/api.py
index 710c76600fb4cc..4f15099c8e119b 100644
--- a/script/scaffold/templates/config_flow_oauth2/integration/api.py
+++ b/script/scaffold/templates/config_flow_oauth2/integration/api.py
@@ -4,7 +4,7 @@
from aiohttp import ClientSession
import my_pypi_package
-from homeassistant import config_entries, core
+from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow
# TODO the following two API examples are based on our suggested best practices
@@ -17,16 +17,12 @@ class ConfigEntryAuth(my_pypi_package.AbstractAuth):
def __init__(
self,
- hass: core.HomeAssistant,
- config_entry: config_entries.ConfigEntry,
- implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation,
- ):
+ hass: HomeAssistant,
+ oauth_session: config_entry_oauth2_flow.OAuth2Session,
+ ) -> None:
"""Initialize NEW_NAME Auth."""
self.hass = hass
- self.config_entry = config_entry
- self.session = config_entry_oauth2_flow.OAuth2Session(
- hass, config_entry, implementation
- )
+ self.session = oauth_session
super().__init__(self.session.token)
def refresh_tokens(self) -> str:
@@ -45,7 +41,7 @@ def __init__(
self,
websession: ClientSession,
oauth_session: config_entry_oauth2_flow.OAuth2Session,
- ):
+ ) -> None:
"""Initialize NEW_NAME auth."""
super().__init__(websession)
self._oauth_session = oauth_session
diff --git a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py
index dd0fc3446b3cc9..ff9c5bfb848571 100644
--- a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py
+++ b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py
@@ -7,6 +7,7 @@
OAUTH2_AUTHORIZE,
OAUTH2_TOKEN,
)
+from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow
CLIENT_ID = "1234"
@@ -14,8 +15,11 @@
async def test_full_flow(
- hass, aiohttp_client, aioclient_mock, current_request_with_host
-):
+ hass: HomeAssistant,
+ aiohttp_client,
+ aioclient_mock,
+ current_request_with_host,
+) -> None:
"""Check full flow."""
assert await setup.async_setup_component(
hass,
diff --git a/script/scaffold/templates/device_action/integration/device_action.py b/script/scaffold/templates/device_action/integration/device_action.py
index 27a27cb95ee0c2..3bd1c0b91b320b 100644
--- a/script/scaffold/templates/device_action/integration/device_action.py
+++ b/script/scaffold/templates/device_action/integration/device_action.py
@@ -1,5 +1,5 @@
"""Provides device actions for NEW_NAME."""
-from typing import List, Optional
+from __future__ import annotations
import voluptuous as vol
@@ -29,7 +29,7 @@
)
-async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device actions for NEW_NAME devices."""
registry = await entity_registry.async_get_registry(hass)
actions = []
@@ -69,7 +69,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
async def async_call_action_from_config(
- hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context]
+ hass: HomeAssistant, config: dict, variables: dict, context: Context | None
) -> None:
"""Execute a device action."""
service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]}
diff --git a/script/scaffold/templates/device_action/tests/test_device_action.py b/script/scaffold/templates/device_action/tests/test_device_action.py
index 91a4693ebeb6b3..424fa0a9afd08c 100644
--- a/script/scaffold/templates/device_action/tests/test_device_action.py
+++ b/script/scaffold/templates/device_action/tests/test_device_action.py
@@ -3,7 +3,8 @@
from homeassistant.components import automation
from homeassistant.components.NEW_DOMAIN import DOMAIN
-from homeassistant.helpers import device_registry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import device_registry, entity_registry
from homeassistant.setup import async_setup_component
from tests.common import (
@@ -17,18 +18,22 @@
@pytest.fixture
-def device_reg(hass):
+def device_reg(hass: HomeAssistant) -> device_registry.DeviceRegistry:
"""Return an empty, loaded, registry."""
return mock_device_registry(hass)
@pytest.fixture
-def entity_reg(hass):
+def entity_reg(hass: HomeAssistant) -> entity_registry.EntityRegistry:
"""Return an empty, loaded, registry."""
return mock_registry(hass)
-async def test_get_actions(hass, device_reg, entity_reg):
+async def test_get_actions(
+ hass: HomeAssistant,
+ device_reg: device_registry.DeviceRegistry,
+ entity_reg: entity_registry.EntityRegistry,
+) -> None:
"""Test we get the expected actions from a NEW_DOMAIN."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
@@ -55,7 +60,7 @@ async def test_get_actions(hass, device_reg, entity_reg):
assert_lists_same(actions, expected_actions)
-async def test_action(hass):
+async def test_action(hass: HomeAssistant) -> None:
"""Test for turn_on and turn_off actions."""
assert await async_setup_component(
hass,
diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py
index 6ad89332f8ec8b..413812828e5c00 100644
--- a/script/scaffold/templates/device_condition/integration/device_condition.py
+++ b/script/scaffold/templates/device_condition/integration/device_condition.py
@@ -1,5 +1,5 @@
"""Provide the device conditions for NEW_NAME."""
-from typing import Dict, List
+from __future__ import annotations
import voluptuous as vol
@@ -33,7 +33,7 @@
async def async_get_conditions(
hass: HomeAssistant, device_id: str
-) -> List[Dict[str, str]]:
+) -> list[dict[str, str]]:
"""List device conditions for NEW_NAME devices."""
registry = await entity_registry.async_get_registry(hass)
conditions = []
diff --git a/script/scaffold/templates/device_condition/tests/test_device_condition.py b/script/scaffold/templates/device_condition/tests/test_device_condition.py
index 07e0afd05ebd2d..9a283fa1f5bb83 100644
--- a/script/scaffold/templates/device_condition/tests/test_device_condition.py
+++ b/script/scaffold/templates/device_condition/tests/test_device_condition.py
@@ -1,10 +1,13 @@
"""The tests for NEW_NAME device conditions."""
+from __future__ import annotations
+
import pytest
from homeassistant.components import automation
from homeassistant.components.NEW_DOMAIN import DOMAIN
from homeassistant.const import STATE_OFF, STATE_ON
-from homeassistant.helpers import device_registry
+from homeassistant.core import HomeAssistant, ServiceCall
+from homeassistant.helpers import device_registry, entity_registry
from homeassistant.setup import async_setup_component
from tests.common import (
@@ -18,24 +21,28 @@
@pytest.fixture
-def device_reg(hass):
+def device_reg(hass: HomeAssistant) -> device_registry.DeviceRegistry:
"""Return an empty, loaded, registry."""
return mock_device_registry(hass)
@pytest.fixture
-def entity_reg(hass):
+def entity_reg(hass: HomeAssistant) -> entity_registry.EntityRegistry:
"""Return an empty, loaded, registry."""
return mock_registry(hass)
@pytest.fixture
-def calls(hass):
+def calls(hass: HomeAssistant) -> list[ServiceCall]:
"""Track calls to a mock service."""
return async_mock_service(hass, "test", "automation")
-async def test_get_conditions(hass, device_reg, entity_reg):
+async def test_get_conditions(
+ hass: HomeAssistant,
+ device_reg: device_registry.DeviceRegistry,
+ entity_reg: entity_registry.EntityRegistry,
+) -> None:
"""Test we get the expected conditions from a NEW_DOMAIN."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
@@ -64,7 +71,7 @@ async def test_get_conditions(hass, device_reg, entity_reg):
assert_lists_same(conditions, expected_conditions)
-async def test_if_state(hass, calls):
+async def test_if_state(hass: HomeAssistant, calls: list[ServiceCall]) -> None:
"""Test for turn_on and turn_off conditions."""
hass.states.async_set("NEW_DOMAIN.entity", STATE_ON)
diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py
index 7709813957ed4d..ca430368544ea6 100644
--- a/script/scaffold/templates/device_trigger/integration/device_trigger.py
+++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py
@@ -1,5 +1,5 @@
"""Provides device triggers for NEW_NAME."""
-from typing import List
+from __future__ import annotations
import voluptuous as vol
@@ -32,7 +32,7 @@
)
-async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device triggers for NEW_NAME devices."""
registry = await entity_registry.async_get_registry(hass)
triggers = []
@@ -84,16 +84,13 @@ async def async_attach_trigger(
# Use the existing state or event triggers from the automation integration.
if config[CONF_TYPE] == "turned_on":
- from_state = STATE_OFF
to_state = STATE_ON
else:
- from_state = STATE_ON
to_state = STATE_OFF
state_config = {
state.CONF_PLATFORM: "state",
CONF_ENTITY_ID: config[CONF_ENTITY_ID],
- state.CONF_FROM: from_state,
state.CONF_TO: to_state,
}
state_config = state.TRIGGER_SCHEMA(state_config)
diff --git a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py
index 23daaf8daddeb0..55343abadb16c3 100644
--- a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py
+++ b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py
@@ -87,7 +87,8 @@ async def test_if_fires_on_state_change(hass, calls):
"some": (
"turn_on - {{ trigger.platform}} - "
"{{ trigger.entity_id}} - {{ trigger.from_state.state}} - "
- "{{ trigger.to_state.state}} - {{ trigger.for }}"
+ "{{ trigger.to_state.state}} - {{ trigger.for }} - "
+ "{{ trigger.id}}"
)
},
},
@@ -106,7 +107,8 @@ async def test_if_fires_on_state_change(hass, calls):
"some": (
"turn_off - {{ trigger.platform}} - "
"{{ trigger.entity_id}} - {{ trigger.from_state.state}} - "
- "{{ trigger.to_state.state}} - {{ trigger.for }}"
+ "{{ trigger.to_state.state}} - {{ trigger.for }} - "
+ "{{ trigger.id}}"
)
},
},
@@ -119,14 +121,14 @@ async def test_if_fires_on_state_change(hass, calls):
hass.states.async_set("NEW_DOMAIN.entity", STATE_ON)
await hass.async_block_till_done()
assert len(calls) == 1
- assert calls[0].data["some"] == "turn_on - device - {} - off - on - None".format(
- "NEW_DOMAIN.entity"
- )
+ assert calls[0].data[
+ "some"
+ ] == "turn_on - device - {} - off - on - None - 0".format("NEW_DOMAIN.entity")
# Fake that the entity is turning off.
hass.states.async_set("NEW_DOMAIN.entity", STATE_OFF)
await hass.async_block_till_done()
assert len(calls) == 2
- assert calls[1].data["some"] == "turn_off - device - {} - on - off - None".format(
- "NEW_DOMAIN.entity"
- )
+ assert calls[1].data[
+ "some"
+ ] == "turn_off - device - {} - on - off - None - 0".format("NEW_DOMAIN.entity")
diff --git a/script/scaffold/templates/integration/integration/__init__.py b/script/scaffold/templates/integration/integration/__init__.py
index 0ab65cb7da8b59..c1f34d5f5b1712 100644
--- a/script/scaffold/templates/integration/integration/__init__.py
+++ b/script/scaffold/templates/integration/integration/__init__.py
@@ -1,4 +1,8 @@
"""The NEW_NAME integration."""
+from __future__ import annotations
+
+from typing import Any
+
import voluptuous as vol
from homeassistant.core import HomeAssistant
@@ -8,6 +12,6 @@
CONFIG_SCHEMA = vol.Schema({vol.Optional(DOMAIN): {}}, extra=vol.ALLOW_EXTRA)
-async def async_setup(hass: HomeAssistant, config: dict):
+async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up the NEW_NAME integration."""
return True
diff --git a/script/scaffold/templates/reproduce_state/integration/reproduce_state.py b/script/scaffold/templates/reproduce_state/integration/reproduce_state.py
index bb2ee7aee963f2..19e046f4c92edd 100644
--- a/script/scaffold/templates/reproduce_state/integration/reproduce_state.py
+++ b/script/scaffold/templates/reproduce_state/integration/reproduce_state.py
@@ -1,7 +1,9 @@
"""Reproduce an NEW_NAME state."""
+from __future__ import annotations
+
import asyncio
import logging
-from typing import Any, Dict, Iterable, Optional
+from typing import Any, Iterable
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -10,8 +12,7 @@
STATE_OFF,
STATE_ON,
)
-from homeassistant.core import Context, State
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.core import Context, HomeAssistant, State
from . import DOMAIN
@@ -22,11 +23,11 @@
async def _async_reproduce_state(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
state: State,
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
@@ -67,11 +68,11 @@ async def _async_reproduce_state(
async def async_reproduce_states(
- hass: HomeAssistantType,
+ hass: HomeAssistant,
states: Iterable[State],
*,
- context: Optional[Context] = None,
- reproduce_options: Optional[Dict[str, Any]] = None,
+ context: Context | None = None,
+ reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce NEW_NAME states."""
# TODO pick one and remove other one
diff --git a/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py b/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py
index ff15625ad7c4fb..83d95570b45247 100644
--- a/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py
+++ b/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py
@@ -1,10 +1,14 @@
"""Test reproduce state for NEW_NAME."""
-from homeassistant.core import State
+import pytest
+
+from homeassistant.core import HomeAssistant, State
from tests.common import async_mock_service
-async def test_reproducing_states(hass, caplog):
+async def test_reproducing_states(
+ hass: HomeAssistant, caplog: pytest.LogCaptureFixture
+) -> None:
"""Test reproducing NEW_NAME states."""
hass.states.async_set("NEW_DOMAIN.entity_off", "off", {})
hass.states.async_set("NEW_DOMAIN.entity_on", "on", {"color": "red"})
diff --git a/script/scaffold/templates/significant_change/integration/significant_change.py b/script/scaffold/templates/significant_change/integration/significant_change.py
index 23a00c603ac296..8e6022f171bc73 100644
--- a/script/scaffold/templates/significant_change/integration/significant_change.py
+++ b/script/scaffold/templates/significant_change/integration/significant_change.py
@@ -1,5 +1,7 @@
"""Helper to test significant NEW_NAME state changes."""
-from typing import Any, Optional
+from __future__ import annotations
+
+from typing import Any
from homeassistant.const import ATTR_DEVICE_CLASS
from homeassistant.core import HomeAssistant, callback
@@ -13,7 +15,7 @@ def async_check_significant_change(
new_state: str,
new_attrs: dict,
**kwargs: Any,
-) -> Optional[bool]:
+) -> bool | None:
"""Test if state significantly changed."""
device_class = new_attrs.get(ATTR_DEVICE_CLASS)
diff --git a/script/setup b/script/setup
index 83c2d24f038f39..f827c3a373f5a3 100755
--- a/script/setup
+++ b/script/setup
@@ -6,10 +6,20 @@ set -e
cd "$(dirname "$0")/.."
+# Add default vscode settings if not existing
+SETTINGS_FILE=./.vscode/settings.json
+SETTINGS_TEMPLATE_FILE=./.vscode/settings.default.json
+if [ ! -f "$SETTINGS_FILE" ]; then
+ echo "Copy $SETTINGS_TEMPLATE_FILE to $SETTINGS_FILE."
+ cp "$SETTINGS_TEMPLATE_FILE" "$SETTINGS_FILE"
+fi
+
mkdir -p config
-python3 -m venv venv
-source venv/bin/activate
+if [ ! -n "$DEVCONTAINER" ];then
+ python3 -m venv venv
+ source venv/bin/activate
+fi
script/bootstrap
@@ -18,9 +28,11 @@ python3 -m pip install -e . --constraint homeassistant/package_constraints.txt
hass --script ensure_config -c config
+if ! grep -R "logger" config/configuration.yaml >> /dev/null;then
echo "
logger:
default: info
logs:
homeassistant.components.cloud: debug
" >> config/configuration.yaml
+fi
\ No newline at end of file
diff --git a/script/translations/download.py b/script/translations/download.py
index 7fc4c3365ccac9..eab9c40370ee59 100755
--- a/script/translations/download.py
+++ b/script/translations/download.py
@@ -1,11 +1,12 @@
#!/usr/bin/env python3
"""Merge all translation sources into a single JSON file."""
+from __future__ import annotations
+
import json
import os
import pathlib
import re
import subprocess
-from typing import Dict, List, Union
from .const import CLI_2_DOCKER_IMAGE, CORE_PROJECT_ID, INTEGRATIONS_DIR
from .error import ExitApp
@@ -51,7 +52,7 @@ def run_download_docker():
raise ExitApp("Failed to download translations")
-def save_json(filename: str, data: Union[List, Dict]):
+def save_json(filename: str, data: list | dict):
"""Save JSON data to a file.
Returns True on success.
diff --git a/script/translations/lokalise.py b/script/translations/lokalise.py
index 69860b49e45c23..a23291169f41d8 100644
--- a/script/translations/lokalise.py
+++ b/script/translations/lokalise.py
@@ -1,4 +1,6 @@
"""API for Lokalise."""
+from __future__ import annotations
+
from pprint import pprint
import requests
@@ -6,7 +8,7 @@
from .util import get_lokalise_token
-def get_api(project_id, debug=False) -> "Lokalise":
+def get_api(project_id, debug=False) -> Lokalise:
"""Get Lokalise API."""
return Lokalise(project_id, get_lokalise_token(), debug)
diff --git a/setup.cfg b/setup.cfg
index 7761ff2d67edae..d8569ad2188577 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[metadata]
-license = Apache License 2.0
+license = Apache-2.0
license_file = LICENSE.md
platforms = any
description = Open-source home automation platform running on Python 3.
@@ -30,6 +30,7 @@ ignore =
E203,
D202,
W504
+noqa-require-code = True
[mypy]
python_version = 3.8
@@ -42,7 +43,7 @@ warn_redundant_casts = true
warn_unused_configs = true
-[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.hyperion.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.number.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.slack.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.components.zwave_js.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,tests.components.hyperion.*]
+[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.bond.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.hyperion.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.knx.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.number.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.recorder.purge,homeassistant.components.recorder.repack,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.slack.*,homeassistant.components.sonos.media_player,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zeroconf.*,homeassistant.components.zone.*,homeassistant.components.zwave_js.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,tests.components.hyperion.*]
strict = true
ignore_errors = false
warn_unreachable = true
diff --git a/setup.py b/setup.py
index 5998a40a24e591..f74a913cb81ca7 100755
--- a/setup.py
+++ b/setup.py
@@ -32,21 +32,22 @@
PACKAGES = find_packages(exclude=["tests", "tests.*"])
REQUIRES = [
- "aiohttp==3.7.3",
- "astral==1.10.1",
+ "aiohttp==3.7.4.post0",
+ "astral==2.2",
"async_timeout==3.0.1",
- "attrs==19.3.0",
+ "attrs==20.3.0",
+ "awesomeversion==21.2.3",
"bcrypt==3.1.7",
"certifi>=2020.12.5",
"ciso8601==2.1.3",
- "httpx==0.16.1",
- "jinja2>=2.11.2",
+ "httpx==0.17.1",
+ "jinja2>=2.11.3",
"PyJWT==1.7.1",
# PyJWT has loose dependency. We want the latest one.
- "cryptography==3.3.1",
+ "cryptography==3.3.2",
"pip>=8.0.3,<20.3",
"python-slugify==4.0.1",
- "pytz>=2020.5",
+ "pytz>=2021.1",
"pyyaml==5.4.1",
"requests==2.25.1",
"ruamel.yaml==0.15.100",
diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py
index 4f34ce1d595991..255fcac7694e07 100644
--- a/tests/auth/test_init.py
+++ b/tests/auth/test_init.py
@@ -501,9 +501,8 @@ async def test_refresh_token_provider_validation(mock_hass):
with patch(
"homeassistant.auth.providers.insecure_example.ExampleAuthProvider.async_validate_refresh_token",
side_effect=InvalidAuthError("Invalid access"),
- ) as call:
- with pytest.raises(InvalidAuthError):
- manager.async_create_access_token(refresh_token, ip)
+ ) as call, pytest.raises(InvalidAuthError):
+ manager.async_create_access_token(refresh_token, ip)
call.assert_called_with(refresh_token, ip)
diff --git a/tests/common.py b/tests/common.py
index 2621f2f4b158ee..cc971ca4f13fff 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -1,4 +1,6 @@
"""Test the helper method for writing tests."""
+from __future__ import annotations
+
import asyncio
import collections
from collections import OrderedDict
@@ -12,10 +14,12 @@
import pathlib
import threading
import time
+from time import monotonic
+import types
+from typing import Any, Awaitable, Collection
from unittest.mock import AsyncMock, Mock, patch
-import uuid
-from aiohttp.test_utils import unused_port as get_test_instance_port # noqa
+from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F401
from homeassistant import auth, config_entries, core as ha, loader
from homeassistant.auth import (
@@ -33,17 +37,14 @@
from homeassistant.components.mqtt.models import Message
from homeassistant.config import async_process_component_config
from homeassistant.const import (
- ATTR_DISCOVERED,
- ATTR_SERVICE,
DEVICE_DEFAULT_NAME,
EVENT_HOMEASSISTANT_CLOSE,
- EVENT_PLATFORM_DISCOVERED,
EVENT_STATE_CHANGED,
EVENT_TIME_CHANGED,
STATE_OFF,
STATE_ON,
)
-from homeassistant.core import State
+from homeassistant.core import BLOCK_LOG_TIMEOUT, State
from homeassistant.helpers import (
area_registry,
device_registry,
@@ -59,6 +60,7 @@
from homeassistant.util.async_ import run_callback_threadsafe
import homeassistant.util.dt as date_util
from homeassistant.util.unit_system import METRIC_SYSTEM
+import homeassistant.util.uuid as uuid_util
import homeassistant.util.yaml.loader as yaml_loader
_LOGGER = logging.getLogger(__name__)
@@ -143,7 +145,7 @@ def stop_hass():
# pylint: disable=protected-access
-async def async_test_home_assistant(loop):
+async def async_test_home_assistant(loop, load_registries=True):
"""Return a Home Assistant object pointing at test config dir."""
hass = ha.HomeAssistant()
store = auth_store.AuthStore(hass)
@@ -190,9 +192,76 @@ def async_create_task(coroutine):
return orig_async_create_task(coroutine)
+ async def async_wait_for_task_count(self, max_remaining_tasks: int = 0) -> None:
+ """Block until at most max_remaining_tasks remain.
+
+ Based on HomeAssistant.async_block_till_done
+ """
+ # To flush out any call_soon_threadsafe
+ await asyncio.sleep(0)
+ start_time: float | None = None
+
+ while len(self._pending_tasks) > max_remaining_tasks:
+ pending: Collection[Awaitable[Any]] = [
+ task for task in self._pending_tasks if not task.done()
+ ]
+ self._pending_tasks.clear()
+ if len(pending) > max_remaining_tasks:
+ remaining_pending = await self._await_count_and_log_pending(
+ pending, max_remaining_tasks=max_remaining_tasks
+ )
+ self._pending_tasks.extend(remaining_pending)
+
+ if start_time is None:
+ # Avoid calling monotonic() until we know
+ # we may need to start logging blocked tasks.
+ start_time = 0
+ elif start_time == 0:
+ # If we have waited twice then we set the start
+ # time
+ start_time = monotonic()
+ elif monotonic() - start_time > BLOCK_LOG_TIMEOUT:
+ # We have waited at least three loops and new tasks
+ # continue to block. At this point we start
+ # logging all waiting tasks.
+ for task in pending:
+ _LOGGER.debug("Waiting for task: %s", task)
+ else:
+ self._pending_tasks.extend(pending)
+ await asyncio.sleep(0)
+
+ async def _await_count_and_log_pending(
+ self, pending: Collection[Awaitable[Any]], max_remaining_tasks: int = 0
+ ) -> Collection[Awaitable[Any]]:
+ """Block at most max_remaining_tasks remain and log tasks that take a long time.
+
+ Based on HomeAssistant._await_and_log_pending
+ """
+ wait_time = 0
+
+ return_when = asyncio.ALL_COMPLETED
+ if max_remaining_tasks:
+ return_when = asyncio.FIRST_COMPLETED
+
+ while len(pending) > max_remaining_tasks:
+ _, pending = await asyncio.wait(
+ pending, timeout=BLOCK_LOG_TIMEOUT, return_when=return_when
+ )
+ if not pending or max_remaining_tasks:
+ return pending
+ wait_time += BLOCK_LOG_TIMEOUT
+ for task in pending:
+ _LOGGER.debug("Waited %s seconds for task: %s", wait_time, task)
+
+ return []
+
hass.async_add_job = async_add_job
hass.async_add_executor_job = async_add_executor_job
hass.async_create_task = async_create_task
+ hass.async_wait_for_task_count = types.MethodType(async_wait_for_task_count, hass)
+ hass._await_count_and_log_pending = types.MethodType(
+ _await_count_and_log_pending, hass
+ )
hass.data[loader.DATA_CUSTOM_COMPONENTS] = {}
@@ -207,9 +276,18 @@ def async_create_task(coroutine):
hass.config.skip_pip = True
hass.config_entries = config_entries.ConfigEntries(hass, {})
- hass.config_entries._entries = []
+ hass.config_entries._entries = {}
hass.config_entries._store._async_ensure_stop_listener = lambda: None
+ # Load the registries
+ if load_registries:
+ await asyncio.gather(
+ device_registry.async_load(hass),
+ entity_registry.async_load(hass),
+ area_registry.async_load(hass),
+ )
+ await hass.async_block_till_done()
+
hass.state = ha.CoreState.running
# Mock async_start
@@ -308,21 +386,6 @@ def async_fire_time_changed(hass, datetime_, fire_all=False):
fire_time_changed = threadsafe_callback_factory(async_fire_time_changed)
-def fire_service_discovered(hass, service, info):
- """Fire the MQTT message."""
- hass.bus.fire(
- EVENT_PLATFORM_DISCOVERED, {ATTR_SERVICE: service, ATTR_DISCOVERED: info}
- )
-
-
-@ha.callback
-def async_fire_service_discovered(hass, service, info):
- """Fire the MQTT message."""
- hass.bus.async_fire(
- EVENT_PLATFORM_DISCOVERED, {ATTR_SERVICE: service, ATTR_DISCOVERED: info}
- )
-
-
def load_fixture(filename):
"""Load a fixture."""
path = os.path.join(os.path.dirname(__file__), "fixtures", filename)
@@ -503,7 +566,7 @@ def __init__(
if platform_schema_base is not None:
self.PLATFORM_SCHEMA_BASE = platform_schema_base
- if setup is not None:
+ if setup:
# We run this in executor, wrap it in function
self.setup = lambda *args: setup(*args)
@@ -670,10 +733,11 @@ def __init__(
system_options={},
connection_class=config_entries.CONN_CLASS_UNKNOWN,
unique_id=None,
+ disabled_by=None,
):
"""Initialize a mock config entry."""
kwargs = {
- "entry_id": entry_id or uuid.uuid4().hex,
+ "entry_id": entry_id or uuid_util.random_uuid_hex(),
"domain": domain,
"data": data or {},
"system_options": system_options,
@@ -682,6 +746,7 @@ def __init__(
"title": title,
"connection_class": connection_class,
"unique_id": unique_id,
+ "disabled_by": disabled_by,
}
if source is not None:
kwargs["source"] = source
@@ -691,17 +756,17 @@ def __init__(
def add_to_hass(self, hass):
"""Test helper to add entry to hass."""
- hass.config_entries._entries.append(self)
+ hass.config_entries._entries[self.entry_id] = self
def add_to_manager(self, manager):
"""Test helper to add entry to entry manager."""
- manager._entries.append(self)
+ manager._entries[self.entry_id] = self
def patch_yaml_files(files_dict, endswith=True):
"""Patch load_yaml with a dictionary of yaml files."""
# match using endswith, start search with longest string
- matchlist = sorted(list(files_dict.keys()), key=len) if endswith else []
+ matchlist = sorted(files_dict.keys(), key=len) if endswith else []
def mock_open_f(fname, **_):
"""Mock open() in the yaml module, used by load_yaml."""
@@ -981,10 +1046,15 @@ async def get_system_health_info(hass, domain):
return await hass.data["system_health"][domain].info_callback(hass)
-def mock_integration(hass, module):
+def mock_integration(hass, module, built_in=True):
"""Mock an integration."""
integration = loader.Integration(
- hass, f"homeassistant.components.{module.DOMAIN}", None, module.mock_manifest()
+ hass,
+ f"{loader.PACKAGE_BUILTIN}.{module.DOMAIN}"
+ if built_in
+ else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}",
+ None,
+ module.mock_manifest(),
)
def mock_import_platform(platform_name):
diff --git a/tests/components/abode/common.py b/tests/components/abode/common.py
index aabc732daa2e6f..c134552ccd41a6 100644
--- a/tests/components/abode/common.py
+++ b/tests/components/abode/common.py
@@ -16,7 +16,7 @@ async def setup_platform(hass, platform):
)
mock_entry.add_to_hass(hass)
- with patch("homeassistant.components.abode.ABODE_PLATFORMS", [platform]), patch(
+ with patch("homeassistant.components.abode.PLATFORMS", [platform]), patch(
"abodepy.event_controller.sio"
), patch("abodepy.utils.save_cache"):
assert await async_setup_component(hass, ABODE_DOMAIN, {})
diff --git a/tests/components/abode/conftest.py b/tests/components/abode/conftest.py
index cc18597c56ec4b..21d64a644a9c2f 100644
--- a/tests/components/abode/conftest.py
+++ b/tests/components/abode/conftest.py
@@ -3,7 +3,7 @@
import pytest
from tests.common import load_fixture
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
@pytest.fixture(autouse=True)
diff --git a/tests/components/abode/test_alarm_control_panel.py b/tests/components/abode/test_alarm_control_panel.py
index 63ae20441f5bad..55ff22b9ee3922 100644
--- a/tests/components/abode/test_alarm_control_panel.py
+++ b/tests/components/abode/test_alarm_control_panel.py
@@ -16,6 +16,7 @@
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
)
+from homeassistant.helpers import entity_registry as er
from .common import setup_platform
@@ -25,7 +26,7 @@
async def test_entity_registry(hass):
"""Tests that the devices are registered in the entity registry."""
await setup_platform(hass, ALARM_DOMAIN)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry = entity_registry.async_get(DEVICE_ID)
# Abode alarm device unique_id is the MAC address
diff --git a/tests/components/abode/test_binary_sensor.py b/tests/components/abode/test_binary_sensor.py
index a826191ccf3f6d..e4aa08c7f5fd77 100644
--- a/tests/components/abode/test_binary_sensor.py
+++ b/tests/components/abode/test_binary_sensor.py
@@ -11,6 +11,7 @@
ATTR_FRIENDLY_NAME,
STATE_OFF,
)
+from homeassistant.helpers import entity_registry as er
from .common import setup_platform
@@ -18,7 +19,7 @@
async def test_entity_registry(hass):
"""Tests that the devices are registered in the entity registry."""
await setup_platform(hass, BINARY_SENSOR_DOMAIN)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry = entity_registry.async_get("binary_sensor.front_door")
assert entry.unique_id == "2834013428b6035fba7d4054aa7b25a3"
diff --git a/tests/components/abode/test_camera.py b/tests/components/abode/test_camera.py
index 06540955464a4f..7dc943a0a741a0 100644
--- a/tests/components/abode/test_camera.py
+++ b/tests/components/abode/test_camera.py
@@ -4,6 +4,7 @@
from homeassistant.components.abode.const import DOMAIN as ABODE_DOMAIN
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, STATE_IDLE
+from homeassistant.helpers import entity_registry as er
from .common import setup_platform
@@ -11,7 +12,7 @@
async def test_entity_registry(hass):
"""Tests that the devices are registered in the entity registry."""
await setup_platform(hass, CAMERA_DOMAIN)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry = entity_registry.async_get("camera.test_cam")
assert entry.unique_id == "d0a3a1c316891ceb00c20118aae2a133"
diff --git a/tests/components/abode/test_cover.py b/tests/components/abode/test_cover.py
index bb1b8fceffb4d6..edd40a867079ab 100644
--- a/tests/components/abode/test_cover.py
+++ b/tests/components/abode/test_cover.py
@@ -10,6 +10,7 @@
SERVICE_OPEN_COVER,
STATE_CLOSED,
)
+from homeassistant.helpers import entity_registry as er
from .common import setup_platform
@@ -19,7 +20,7 @@
async def test_entity_registry(hass):
"""Tests that the devices are registered in the entity registry."""
await setup_platform(hass, COVER_DOMAIN)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry = entity_registry.async_get(DEVICE_ID)
assert entry.unique_id == "61cbz3b542d2o33ed2fz02721bda3324"
diff --git a/tests/components/abode/test_light.py b/tests/components/abode/test_light.py
index f0eee4b209bd62..b5160aece2a26b 100644
--- a/tests/components/abode/test_light.py
+++ b/tests/components/abode/test_light.py
@@ -16,6 +16,7 @@
SERVICE_TURN_ON,
STATE_ON,
)
+from homeassistant.helpers import entity_registry as er
from .common import setup_platform
@@ -25,7 +26,7 @@
async def test_entity_registry(hass):
"""Tests that the devices are registered in the entity registry."""
await setup_platform(hass, LIGHT_DOMAIN)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry = entity_registry.async_get(DEVICE_ID)
assert entry.unique_id == "741385f4388b2637df4c6b398fe50581"
diff --git a/tests/components/abode/test_lock.py b/tests/components/abode/test_lock.py
index 45e17861d33216..c688b6f02bcda4 100644
--- a/tests/components/abode/test_lock.py
+++ b/tests/components/abode/test_lock.py
@@ -10,6 +10,7 @@
SERVICE_UNLOCK,
STATE_LOCKED,
)
+from homeassistant.helpers import entity_registry as er
from .common import setup_platform
@@ -19,7 +20,7 @@
async def test_entity_registry(hass):
"""Tests that the devices are registered in the entity registry."""
await setup_platform(hass, LOCK_DOMAIN)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry = entity_registry.async_get(DEVICE_ID)
assert entry.unique_id == "51cab3b545d2o34ed7fz02731bda5324"
diff --git a/tests/components/abode/test_sensor.py b/tests/components/abode/test_sensor.py
index d99fac50ddee69..5e3195430ab181 100644
--- a/tests/components/abode/test_sensor.py
+++ b/tests/components/abode/test_sensor.py
@@ -9,6 +9,7 @@
PERCENTAGE,
TEMP_CELSIUS,
)
+from homeassistant.helpers import entity_registry as er
from .common import setup_platform
@@ -16,7 +17,7 @@
async def test_entity_registry(hass):
"""Tests that the devices are registered in the entity registry."""
await setup_platform(hass, SENSOR_DOMAIN)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry = entity_registry.async_get("sensor.environment_sensor_humidity")
assert entry.unique_id == "13545b21f4bdcd33d9abd461f8443e65-humidity"
diff --git a/tests/components/abode/test_switch.py b/tests/components/abode/test_switch.py
index 3ec9648d87de0c..829c5e8ae374e1 100644
--- a/tests/components/abode/test_switch.py
+++ b/tests/components/abode/test_switch.py
@@ -13,6 +13,7 @@
STATE_OFF,
STATE_ON,
)
+from homeassistant.helpers import entity_registry as er
from .common import setup_platform
@@ -25,7 +26,7 @@
async def test_entity_registry(hass):
"""Tests that the devices are registered in the entity registry."""
await setup_platform(hass, SWITCH_DOMAIN)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry = entity_registry.async_get(AUTOMATION_ID)
assert entry.unique_id == AUTOMATION_UID
diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py
index 361422883d4e71..bafad72ec0bd84 100644
--- a/tests/components/accuweather/test_sensor.py
+++ b/tests/components/accuweather/test_sensor.py
@@ -22,6 +22,7 @@
TIME_HOURS,
UV_INDEX,
)
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
@@ -32,7 +33,7 @@
async def test_sensor_without_forecast(hass):
"""Test states of the sensor without forecast."""
await init_integration(hass)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
state = hass.states.get("sensor.home_cloud_ceiling")
assert state
@@ -94,7 +95,7 @@ async def test_sensor_without_forecast(hass):
async def test_sensor_with_forecast(hass):
"""Test states of the sensor with forecast."""
await init_integration(hass, forecast=True)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
state = hass.states.get("sensor.home_hours_of_sun_0d")
assert state
@@ -166,7 +167,7 @@ async def test_sensor_with_forecast(hass):
async def test_sensor_disabled(hass):
"""Test sensor disabled by default."""
await init_integration(hass)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("sensor.home_apparent_temperature")
assert entry
@@ -185,7 +186,7 @@ async def test_sensor_disabled(hass):
async def test_sensor_enabled_without_forecast(hass):
"""Test enabling an advanced sensor."""
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
registry.async_get_or_create(
SENSOR_DOMAIN,
diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py
index 0c1559ef0d6be3..8190d96e634772 100644
--- a/tests/components/accuweather/test_weather.py
+++ b/tests/components/accuweather/test_weather.py
@@ -23,6 +23,7 @@
ATTR_WEATHER_WIND_SPEED,
)
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, STATE_UNAVAILABLE
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
@@ -33,7 +34,7 @@
async def test_weather_without_forecast(hass):
"""Test states of the weather without forecast."""
await init_integration(hass)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
state = hass.states.get("weather.home")
assert state
@@ -56,7 +57,7 @@ async def test_weather_without_forecast(hass):
async def test_weather_with_forecast(hass):
"""Test states of the weather with forecast."""
await init_integration(hass, forecast=True)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
state = hass.states.get("weather.home")
assert state
diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py
index 06fe235741f746..94760cade9fa4d 100644
--- a/tests/components/adguard/test_config_flow.py
+++ b/tests/components/adguard/test_config_flow.py
@@ -16,8 +16,10 @@
CONF_VERIFY_SSL,
CONTENT_TYPE_JSON,
)
+from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
+from tests.test_util.aiohttp import AiohttpClientMocker
FIXTURE_USER_INPUT = {
CONF_HOST: "127.0.0.1",
@@ -29,7 +31,7 @@
}
-async def test_show_authenticate_form(hass):
+async def test_show_authenticate_form(hass: HomeAssistant) -> None:
"""Test that the setup form is served."""
flow = config_flow.AdGuardHomeFlowHandler()
flow.hass = hass
@@ -39,7 +41,9 @@ async def test_show_authenticate_form(hass):
assert result["step_id"] == "user"
-async def test_connection_error(hass, aioclient_mock):
+async def test_connection_error(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
"""Test we show user form on AdGuard Home connection error."""
aioclient_mock.get(
f"{'https' if FIXTURE_USER_INPUT[CONF_SSL] else 'http'}"
@@ -57,7 +61,9 @@ async def test_connection_error(hass, aioclient_mock):
assert result["errors"] == {"base": "cannot_connect"}
-async def test_full_flow_implementation(hass, aioclient_mock):
+async def test_full_flow_implementation(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
"""Test registering an integration and finishing flow works."""
aioclient_mock.get(
f"{'https' if FIXTURE_USER_INPUT[CONF_SSL] else 'http'}"
@@ -84,7 +90,7 @@ async def test_full_flow_implementation(hass, aioclient_mock):
assert result["data"][CONF_VERIFY_SSL] == FIXTURE_USER_INPUT[CONF_VERIFY_SSL]
-async def test_integration_already_exists(hass):
+async def test_integration_already_exists(hass: HomeAssistant) -> None:
"""Test we only allow a single config flow."""
MockConfigEntry(domain=DOMAIN).add_to_hass(hass)
@@ -95,7 +101,7 @@ async def test_integration_already_exists(hass):
assert result["reason"] == "single_instance_allowed"
-async def test_hassio_single_instance(hass):
+async def test_hassio_single_instance(hass: HomeAssistant) -> None:
"""Test we only allow a single config flow."""
MockConfigEntry(
domain="adguard", data={"host": "mock-adguard", "port": "3000"}
@@ -110,7 +116,7 @@ async def test_hassio_single_instance(hass):
assert result["reason"] == "single_instance_allowed"
-async def test_hassio_update_instance_not_running(hass):
+async def test_hassio_update_instance_not_running(hass: HomeAssistant) -> None:
"""Test we only allow a single config flow."""
entry = MockConfigEntry(
domain="adguard", data={"host": "mock-adguard", "port": "3000"}
@@ -131,7 +137,9 @@ async def test_hassio_update_instance_not_running(hass):
assert result["reason"] == "existing_instance_updated"
-async def test_hassio_update_instance_running(hass, aioclient_mock):
+async def test_hassio_update_instance_running(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
"""Test we only allow a single config flow."""
aioclient_mock.get(
"http://mock-adguard-updated:3000/control/status",
@@ -192,7 +200,9 @@ async def test_hassio_update_instance_running(hass, aioclient_mock):
assert entry.data["host"] == "mock-adguard-updated"
-async def test_hassio_confirm(hass, aioclient_mock):
+async def test_hassio_confirm(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
"""Test we can finish a config flow."""
aioclient_mock.get(
"http://mock-adguard:3000/control/status",
@@ -220,7 +230,9 @@ async def test_hassio_confirm(hass, aioclient_mock):
assert result["data"][CONF_VERIFY_SSL]
-async def test_hassio_connection_error(hass, aioclient_mock):
+async def test_hassio_connection_error(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
"""Test we show Hass.io confirm form on AdGuard Home connection error."""
aioclient_mock.get(
"http://mock-adguard:3000/control/status", exc=aiohttp.ClientError
diff --git a/tests/components/advantage_air/test_binary_sensor.py b/tests/components/advantage_air/test_binary_sensor.py
index d0b1a90aaada40..dee4b9fd99a317 100644
--- a/tests/components/advantage_air/test_binary_sensor.py
+++ b/tests/components/advantage_air/test_binary_sensor.py
@@ -1,6 +1,7 @@
"""Test the Advantage Air Binary Sensor Platform."""
from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.helpers import entity_registry as er
from tests.components.advantage_air import (
TEST_SET_RESPONSE,
@@ -24,7 +25,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock):
)
await add_mock_config(hass)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert len(aioclient_mock.mock_calls) == 1
diff --git a/tests/components/advantage_air/test_climate.py b/tests/components/advantage_air/test_climate.py
index 7eb1729a5db092..ea0cf02546211d 100644
--- a/tests/components/advantage_air/test_climate.py
+++ b/tests/components/advantage_air/test_climate.py
@@ -22,6 +22,7 @@
SERVICE_SET_TEMPERATURE,
)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
+from homeassistant.helpers import entity_registry as er
from tests.components.advantage_air import (
TEST_SET_RESPONSE,
@@ -45,7 +46,7 @@ async def test_climate_async_setup_entry(hass, aioclient_mock):
)
await add_mock_config(hass)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert len(aioclient_mock.mock_calls) == 1
diff --git a/tests/components/advantage_air/test_cover.py b/tests/components/advantage_air/test_cover.py
index 18fd4f05b5aaf8..29f0d288fdb117 100644
--- a/tests/components/advantage_air/test_cover.py
+++ b/tests/components/advantage_air/test_cover.py
@@ -15,6 +15,7 @@
SERVICE_SET_COVER_POSITION,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_OPEN
+from homeassistant.helpers import entity_registry as er
from tests.components.advantage_air import (
TEST_SET_RESPONSE,
@@ -39,7 +40,7 @@ async def test_cover_async_setup_entry(hass, aioclient_mock):
await add_mock_config(hass)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert len(aioclient_mock.mock_calls) == 1
diff --git a/tests/components/advantage_air/test_sensor.py b/tests/components/advantage_air/test_sensor.py
index e420ab978fd955..684b965d94f712 100644
--- a/tests/components/advantage_air/test_sensor.py
+++ b/tests/components/advantage_air/test_sensor.py
@@ -8,6 +8,7 @@
ADVANTAGE_AIR_SET_COUNTDOWN_VALUE,
)
from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.helpers import entity_registry as er
from tests.components.advantage_air import (
TEST_SET_RESPONSE,
@@ -31,7 +32,7 @@ async def test_sensor_platform(hass, aioclient_mock):
)
await add_mock_config(hass)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert len(aioclient_mock.mock_calls) == 1
diff --git a/tests/components/advantage_air/test_switch.py b/tests/components/advantage_air/test_switch.py
index f45477adc70fc3..1a78025df70b76 100644
--- a/tests/components/advantage_air/test_switch.py
+++ b/tests/components/advantage_air/test_switch.py
@@ -1,5 +1,4 @@
"""Test the Advantage Air Switch Platform."""
-
from json import loads
from homeassistant.components.advantage_air.const import (
@@ -12,6 +11,7 @@
SERVICE_TURN_ON,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF
+from homeassistant.helpers import entity_registry as er
from tests.components.advantage_air import (
TEST_SET_RESPONSE,
@@ -36,7 +36,7 @@ async def test_cover_async_setup_entry(hass, aioclient_mock):
await add_mock_config(hass)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert len(aioclient_mock.mock_calls) == 1
diff --git a/tests/components/aemet/__init__.py b/tests/components/aemet/__init__.py
new file mode 100644
index 00000000000000..a92ff2764b1b5c
--- /dev/null
+++ b/tests/components/aemet/__init__.py
@@ -0,0 +1 @@
+"""Tests for the AEMET OpenData integration."""
diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py
new file mode 100644
index 00000000000000..be01ac9de0728e
--- /dev/null
+++ b/tests/components/aemet/test_config_flow.py
@@ -0,0 +1,97 @@
+"""Define tests for the AEMET OpenData config flow."""
+
+from unittest.mock import MagicMock, patch
+
+import requests_mock
+
+from homeassistant import data_entry_flow
+from homeassistant.components.aemet.const import DOMAIN
+from homeassistant.config_entries import ENTRY_STATE_LOADED, SOURCE_USER
+from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+import homeassistant.util.dt as dt_util
+
+from .util import aemet_requests_mock
+
+from tests.common import MockConfigEntry
+
+CONFIG = {
+ CONF_NAME: "aemet",
+ CONF_API_KEY: "foo",
+ CONF_LATITUDE: 40.30403754,
+ CONF_LONGITUDE: -3.72935236,
+}
+
+
+async def test_form(hass):
+ """Test that the form is served with valid input."""
+
+ with patch(
+ "homeassistant.components.aemet.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry, requests_mock.mock() as _m:
+ aemet_requests_mock(_m)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == SOURCE_USER
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], CONFIG
+ )
+
+ await hass.async_block_till_done()
+
+ conf_entries = hass.config_entries.async_entries(DOMAIN)
+ entry = conf_entries[0]
+ assert entry.state == ENTRY_STATE_LOADED
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == CONFIG[CONF_NAME]
+ assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE]
+ assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE]
+ assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY]
+
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_duplicated_id(hass):
+ """Test that the options form."""
+
+ now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
+ with patch("homeassistant.util.dt.now", return_value=now), patch(
+ "homeassistant.util.dt.utcnow", return_value=now
+ ), requests_mock.mock() as _m:
+ aemet_requests_mock(_m)
+
+ entry = MockConfigEntry(
+ domain=DOMAIN, unique_id="40.30403754--3.72935236", data=CONFIG
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+
+
+async def test_form_api_offline(hass):
+ """Test setting up with api call error."""
+ mocked_aemet = MagicMock()
+
+ mocked_aemet.get_conventional_observation_stations.return_value = None
+
+ with patch(
+ "homeassistant.components.aemet.config_flow.AEMET",
+ return_value=mocked_aemet,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
+ )
+
+ assert result["errors"] == {"base": "invalid_api_key"}
diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py
new file mode 100644
index 00000000000000..f1c6c48f3f3323
--- /dev/null
+++ b/tests/components/aemet/test_init.py
@@ -0,0 +1,44 @@
+"""Define tests for the AEMET OpenData init."""
+
+from unittest.mock import patch
+
+import requests_mock
+
+from homeassistant.components.aemet.const import DOMAIN
+from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED
+from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+import homeassistant.util.dt as dt_util
+
+from .util import aemet_requests_mock
+
+from tests.common import MockConfigEntry
+
+CONFIG = {
+ CONF_NAME: "aemet",
+ CONF_API_KEY: "foo",
+ CONF_LATITUDE: 40.30403754,
+ CONF_LONGITUDE: -3.72935236,
+}
+
+
+async def test_unload_entry(hass):
+ """Test that the options form."""
+
+ now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
+ with patch("homeassistant.util.dt.now", return_value=now), patch(
+ "homeassistant.util.dt.utcnow", return_value=now
+ ), requests_mock.mock() as _m:
+ aemet_requests_mock(_m)
+
+ config_entry = MockConfigEntry(
+ domain=DOMAIN, unique_id="aemet_unique_id", data=CONFIG
+ )
+ config_entry.add_to_hass(hass)
+
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert config_entry.state == ENTRY_STATE_LOADED
+
+ await hass.config_entries.async_unload(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert config_entry.state == ENTRY_STATE_NOT_LOADED
diff --git a/tests/components/aemet/test_sensor.py b/tests/components/aemet/test_sensor.py
new file mode 100644
index 00000000000000..7887139a38609e
--- /dev/null
+++ b/tests/components/aemet/test_sensor.py
@@ -0,0 +1,139 @@
+"""The sensor tests for the AEMET OpenData platform."""
+
+from unittest.mock import patch
+
+from homeassistant.components.weather import (
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_SNOWY,
+)
+from homeassistant.const import STATE_UNKNOWN
+import homeassistant.util.dt as dt_util
+
+from .util import async_init_integration
+
+
+async def test_aemet_forecast_create_sensors(hass):
+ """Test creation of forecast sensors."""
+
+ now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
+ with patch("homeassistant.util.dt.now", return_value=now), patch(
+ "homeassistant.util.dt.utcnow", return_value=now
+ ):
+ await async_init_integration(hass)
+
+ state = hass.states.get("sensor.aemet_daily_forecast_condition")
+ assert state.state == ATTR_CONDITION_PARTLYCLOUDY
+
+ state = hass.states.get("sensor.aemet_daily_forecast_precipitation")
+ assert state.state == STATE_UNKNOWN
+
+ state = hass.states.get("sensor.aemet_daily_forecast_precipitation_probability")
+ assert state.state == "30"
+
+ state = hass.states.get("sensor.aemet_daily_forecast_temperature")
+ assert state.state == "4"
+
+ state = hass.states.get("sensor.aemet_daily_forecast_temperature_low")
+ assert state.state == "-4"
+
+ state = hass.states.get("sensor.aemet_daily_forecast_time")
+ assert (
+ state.state == dt_util.parse_datetime("2021-01-10 00:00:00+00:00").isoformat()
+ )
+
+ state = hass.states.get("sensor.aemet_daily_forecast_wind_bearing")
+ assert state.state == "45.0"
+
+ state = hass.states.get("sensor.aemet_daily_forecast_wind_speed")
+ assert state.state == "20"
+
+ state = hass.states.get("sensor.aemet_hourly_forecast_condition")
+ assert state is None
+
+ state = hass.states.get("sensor.aemet_hourly_forecast_precipitation")
+ assert state is None
+
+ state = hass.states.get("sensor.aemet_hourly_forecast_precipitation_probability")
+ assert state is None
+
+ state = hass.states.get("sensor.aemet_hourly_forecast_temperature")
+ assert state is None
+
+ state = hass.states.get("sensor.aemet_hourly_forecast_temperature_low")
+ assert state is None
+
+ state = hass.states.get("sensor.aemet_hourly_forecast_time")
+ assert state is None
+
+ state = hass.states.get("sensor.aemet_hourly_forecast_wind_bearing")
+ assert state is None
+
+ state = hass.states.get("sensor.aemet_hourly_forecast_wind_speed")
+ assert state is None
+
+
+async def test_aemet_weather_create_sensors(hass):
+ """Test creation of weather sensors."""
+
+ now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
+ with patch("homeassistant.util.dt.now", return_value=now), patch(
+ "homeassistant.util.dt.utcnow", return_value=now
+ ):
+ await async_init_integration(hass)
+
+ state = hass.states.get("sensor.aemet_condition")
+ assert state.state == ATTR_CONDITION_SNOWY
+
+ state = hass.states.get("sensor.aemet_humidity")
+ assert state.state == "99.0"
+
+ state = hass.states.get("sensor.aemet_pressure")
+ assert state.state == "1004.4"
+
+ state = hass.states.get("sensor.aemet_rain")
+ assert state.state == "1.8"
+
+ state = hass.states.get("sensor.aemet_rain_probability")
+ assert state.state == "100"
+
+ state = hass.states.get("sensor.aemet_snow")
+ assert state.state == "1.8"
+
+ state = hass.states.get("sensor.aemet_snow_probability")
+ assert state.state == "100"
+
+ state = hass.states.get("sensor.aemet_station_id")
+ assert state.state == "3195"
+
+ state = hass.states.get("sensor.aemet_station_name")
+ assert state.state == "MADRID RETIRO"
+
+ state = hass.states.get("sensor.aemet_station_timestamp")
+ assert state.state == "2021-01-09T12:00:00+00:00"
+
+ state = hass.states.get("sensor.aemet_storm_probability")
+ assert state.state == "0"
+
+ state = hass.states.get("sensor.aemet_temperature")
+ assert state.state == "-0.7"
+
+ state = hass.states.get("sensor.aemet_temperature_feeling")
+ assert state.state == "-4"
+
+ state = hass.states.get("sensor.aemet_town_id")
+ assert state.state == "id28065"
+
+ state = hass.states.get("sensor.aemet_town_name")
+ assert state.state == "Getafe"
+
+ state = hass.states.get("sensor.aemet_town_timestamp")
+ assert state.state == "2021-01-09T11:47:45+00:00"
+
+ state = hass.states.get("sensor.aemet_wind_bearing")
+ assert state.state == "90.0"
+
+ state = hass.states.get("sensor.aemet_wind_max_speed")
+ assert state.state == "24"
+
+ state = hass.states.get("sensor.aemet_wind_speed")
+ assert state.state == "15"
diff --git a/tests/components/aemet/test_weather.py b/tests/components/aemet/test_weather.py
new file mode 100644
index 00000000000000..43acf4c1c8731c
--- /dev/null
+++ b/tests/components/aemet/test_weather.py
@@ -0,0 +1,62 @@
+"""The sensor tests for the AEMET OpenData platform."""
+
+from unittest.mock import patch
+
+from homeassistant.components.aemet.const import ATTRIBUTION
+from homeassistant.components.weather import (
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_FORECAST,
+ ATTR_FORECAST_CONDITION,
+ ATTR_FORECAST_PRECIPITATION,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY,
+ ATTR_FORECAST_TEMP,
+ ATTR_FORECAST_TEMP_LOW,
+ ATTR_FORECAST_TIME,
+ ATTR_FORECAST_WIND_BEARING,
+ ATTR_FORECAST_WIND_SPEED,
+ ATTR_WEATHER_HUMIDITY,
+ ATTR_WEATHER_PRESSURE,
+ ATTR_WEATHER_TEMPERATURE,
+ ATTR_WEATHER_WIND_BEARING,
+ ATTR_WEATHER_WIND_SPEED,
+)
+from homeassistant.const import ATTR_ATTRIBUTION
+import homeassistant.util.dt as dt_util
+
+from .util import async_init_integration
+
+
+async def test_aemet_weather(hass):
+ """Test states of the weather."""
+
+ now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
+ with patch("homeassistant.util.dt.now", return_value=now), patch(
+ "homeassistant.util.dt.utcnow", return_value=now
+ ):
+ await async_init_integration(hass)
+
+ state = hass.states.get("weather.aemet_daily")
+ assert state
+ assert state.state == ATTR_CONDITION_SNOWY
+ assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
+ assert state.attributes.get(ATTR_WEATHER_HUMIDITY) == 99.0
+ assert state.attributes.get(ATTR_WEATHER_PRESSURE) == 1004.4
+ assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == -0.7
+ assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 90.0
+ assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 15
+ forecast = state.attributes.get(ATTR_FORECAST)[0]
+ assert forecast.get(ATTR_FORECAST_CONDITION) == ATTR_CONDITION_PARTLYCLOUDY
+ assert forecast.get(ATTR_FORECAST_PRECIPITATION) is None
+ assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 30
+ assert forecast.get(ATTR_FORECAST_TEMP) == 4
+ assert forecast.get(ATTR_FORECAST_TEMP_LOW) == -4
+ assert (
+ forecast.get(ATTR_FORECAST_TIME)
+ == dt_util.parse_datetime("2021-01-10 00:00:00+00:00").isoformat()
+ )
+ assert forecast.get(ATTR_FORECAST_WIND_BEARING) == 45.0
+ assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 20
+
+ state = hass.states.get("weather.aemet_hourly")
+ assert state is None
diff --git a/tests/components/aemet/util.py b/tests/components/aemet/util.py
new file mode 100644
index 00000000000000..991e7459bf6d95
--- /dev/null
+++ b/tests/components/aemet/util.py
@@ -0,0 +1,93 @@
+"""Tests for the AEMET OpenData integration."""
+
+import requests_mock
+
+from homeassistant.components.aemet import DOMAIN
+from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+from homeassistant.core import HomeAssistant
+
+from tests.common import MockConfigEntry, load_fixture
+
+
+def aemet_requests_mock(mock):
+ """Mock requests performed to AEMET OpenData API."""
+
+ station_3195_fixture = "aemet/station-3195.json"
+ station_3195_data_fixture = "aemet/station-3195-data.json"
+ station_list_fixture = "aemet/station-list.json"
+ station_list_data_fixture = "aemet/station-list-data.json"
+
+ town_28065_forecast_daily_fixture = "aemet/town-28065-forecast-daily.json"
+ town_28065_forecast_daily_data_fixture = "aemet/town-28065-forecast-daily-data.json"
+ town_28065_forecast_hourly_fixture = "aemet/town-28065-forecast-hourly.json"
+ town_28065_forecast_hourly_data_fixture = (
+ "aemet/town-28065-forecast-hourly-data.json"
+ )
+ town_id28065_fixture = "aemet/town-id28065.json"
+ town_list_fixture = "aemet/town-list.json"
+
+ mock.get(
+ "https://opendata.aemet.es/opendata/api/observacion/convencional/datos/estacion/3195",
+ text=load_fixture(station_3195_fixture),
+ )
+ mock.get(
+ "https://opendata.aemet.es/opendata/sh/208c3ca3",
+ text=load_fixture(station_3195_data_fixture),
+ )
+ mock.get(
+ "https://opendata.aemet.es/opendata/api/observacion/convencional/todas",
+ text=load_fixture(station_list_fixture),
+ )
+ mock.get(
+ "https://opendata.aemet.es/opendata/sh/2c55192f",
+ text=load_fixture(station_list_data_fixture),
+ )
+ mock.get(
+ "https://opendata.aemet.es/opendata/api/prediccion/especifica/municipio/diaria/28065",
+ text=load_fixture(town_28065_forecast_daily_fixture),
+ )
+ mock.get(
+ "https://opendata.aemet.es/opendata/sh/64e29abb",
+ text=load_fixture(town_28065_forecast_daily_data_fixture),
+ )
+ mock.get(
+ "https://opendata.aemet.es/opendata/api/prediccion/especifica/municipio/horaria/28065",
+ text=load_fixture(town_28065_forecast_hourly_fixture),
+ )
+ mock.get(
+ "https://opendata.aemet.es/opendata/sh/18ca1886",
+ text=load_fixture(town_28065_forecast_hourly_data_fixture),
+ )
+ mock.get(
+ "https://opendata.aemet.es/opendata/api/maestro/municipio/id28065",
+ text=load_fixture(town_id28065_fixture),
+ )
+ mock.get(
+ "https://opendata.aemet.es/opendata/api/maestro/municipios",
+ text=load_fixture(town_list_fixture),
+ )
+
+
+async def async_init_integration(
+ hass: HomeAssistant,
+ skip_setup: bool = False,
+):
+ """Set up the AEMET OpenData integration in Home Assistant."""
+
+ with requests_mock.mock() as _m:
+ aemet_requests_mock(_m)
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ CONF_API_KEY: "mock",
+ CONF_LATITUDE: "40.30403754",
+ CONF_LONGITUDE: "-3.72935236",
+ CONF_NAME: "AEMET",
+ },
+ )
+ entry.add_to_hass(hass)
+
+ if not skip_setup:
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
diff --git a/tests/components/airly/test_air_quality.py b/tests/components/airly/test_air_quality.py
index 24a98cbf155415..de059e84aa4a69 100644
--- a/tests/components/airly/test_air_quality.py
+++ b/tests/components/airly/test_air_quality.py
@@ -23,6 +23,7 @@
HTTP_INTERNAL_SERVER_ERROR,
STATE_UNAVAILABLE,
)
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
@@ -35,7 +36,7 @@
async def test_air_quality(hass, aioclient_mock):
"""Test states of the air_quality."""
await init_integration(hass, aioclient_mock)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
state = hass.states.get("air_quality.home")
assert state
diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py
index abc53294bbc765..925f3acb6d2495 100644
--- a/tests/components/airly/test_sensor.py
+++ b/tests/components/airly/test_sensor.py
@@ -17,6 +17,7 @@
STATE_UNAVAILABLE,
TEMP_CELSIUS,
)
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
@@ -29,7 +30,7 @@
async def test_sensor(hass, aioclient_mock):
"""Test states of the sensor."""
await init_integration(hass, aioclient_mock)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
state = hass.states.get("sensor.home_humidity")
assert state
diff --git a/tests/components/airly/test_system_health.py b/tests/components/airly/test_system_health.py
index 02ee67ae4524a9..42fc50ed0510ed 100644
--- a/tests/components/airly/test_system_health.py
+++ b/tests/components/airly/test_system_health.py
@@ -18,7 +18,11 @@ async def test_airly_system_health(hass, aioclient_mock):
hass.data[DOMAIN] = {}
hass.data[DOMAIN]["0123xyz"] = Mock(
- airly=Mock(AIRLY_API_URL="https://airapi.airly.eu/v2/")
+ airly=Mock(
+ AIRLY_API_URL="https://airapi.airly.eu/v2/",
+ requests_remaining=42,
+ requests_per_day=100,
+ )
)
info = await get_system_health_info(hass, DOMAIN)
@@ -27,7 +31,9 @@ async def test_airly_system_health(hass, aioclient_mock):
if asyncio.iscoroutine(val):
info[key] = await val
- assert info == {"can_reach_server": "ok"}
+ assert info["can_reach_server"] == "ok"
+ assert info["requests_remaining"] == 42
+ assert info["requests_per_day"] == 100
async def test_airly_system_health_fail(hass, aioclient_mock):
@@ -38,7 +44,11 @@ async def test_airly_system_health_fail(hass, aioclient_mock):
hass.data[DOMAIN] = {}
hass.data[DOMAIN]["0123xyz"] = Mock(
- airly=Mock(AIRLY_API_URL="https://airapi.airly.eu/v2/")
+ airly=Mock(
+ AIRLY_API_URL="https://airapi.airly.eu/v2/",
+ requests_remaining=0,
+ requests_per_day=1000,
+ )
)
info = await get_system_health_info(hass, DOMAIN)
@@ -47,4 +57,6 @@ async def test_airly_system_health_fail(hass, aioclient_mock):
if asyncio.iscoroutine(val):
info[key] = await val
- assert info == {"can_reach_server": {"type": "failed", "error": "unreachable"}}
+ assert info["can_reach_server"] == {"type": "failed", "error": "unreachable"}
+ assert info["requests_remaining"] == 0
+ assert info["requests_per_day"] == 1000
diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py
index f7533b7f5ac799..9937e899643d88 100644
--- a/tests/components/airnow/test_config_flow.py
+++ b/tests/components/airnow/test_config_flow.py
@@ -75,9 +75,7 @@ async def test_form(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
- with patch("pyairnow.WebServiceAPI._get", return_value=MOCK_RESPONSE,), patch(
- "homeassistant.components.airnow.async_setup", return_value=True
- ) as mock_setup, patch(
+ with patch("pyairnow.WebServiceAPI._get", return_value=MOCK_RESPONSE), patch(
"homeassistant.components.airnow.async_setup_entry",
return_value=True,
) as mock_setup_entry:
@@ -90,7 +88,6 @@ async def test_form(hass):
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result2["data"] == CONFIG
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py
index 4e550d94b09f0c..248abaf6b5f69b 100644
--- a/tests/components/airvisual/test_config_flow.py
+++ b/tests/components/airvisual/test_config_flow.py
@@ -1,14 +1,22 @@
"""Define tests for the AirVisual config flow."""
from unittest.mock import patch
-from pyairvisual.errors import InvalidKeyError, NodeProError
+from pyairvisual.errors import (
+ AirVisualError,
+ InvalidKeyError,
+ NodeProError,
+ NotFoundError,
+)
from homeassistant import data_entry_flow
-from homeassistant.components.airvisual import (
+from homeassistant.components.airvisual.const import (
+ CONF_CITY,
+ CONF_COUNTRY,
CONF_GEOGRAPHIES,
CONF_INTEGRATION_TYPE,
DOMAIN,
- INTEGRATION_TYPE_GEOGRAPHY,
+ INTEGRATION_TYPE_GEOGRAPHY_COORDS,
+ INTEGRATION_TYPE_GEOGRAPHY_NAME,
INTEGRATION_TYPE_NODE_PRO,
)
from homeassistant.config_entries import SOURCE_USER
@@ -19,6 +27,7 @@
CONF_LONGITUDE,
CONF_PASSWORD,
CONF_SHOW_ON_MAP,
+ CONF_STATE,
)
from homeassistant.setup import async_setup_component
@@ -38,7 +47,9 @@ async def test_duplicate_error(hass):
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}, data={"type": "Geographical Location"}
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS},
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=geography_conf
@@ -64,14 +75,8 @@ async def test_duplicate_error(hass):
assert result["reason"] == "already_configured"
-async def test_invalid_identifier(hass):
- """Test that an invalid API key or Node/Pro ID throws an error."""
- geography_conf = {
- CONF_API_KEY: "abcde12345",
- CONF_LATITUDE: 51.528308,
- CONF_LONGITUDE: -0.3817765,
- }
-
+async def test_invalid_identifier_geography_api_key(hass):
+ """Test that an invalid API key throws an error."""
with patch(
"pyairvisual.air_quality.AirQuality.nearest_city",
side_effect=InvalidKeyError,
@@ -79,23 +84,96 @@ async def test_invalid_identifier(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
- data={"type": "Geographical Location"},
+ data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS},
)
result = await hass.config_entries.flow.async_configure(
- result["flow_id"], user_input=geography_conf
+ result["flow_id"],
+ user_input={
+ CONF_API_KEY: "abcde12345",
+ CONF_LATITUDE: 51.528308,
+ CONF_LONGITUDE: -0.3817765,
+ },
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {CONF_API_KEY: "invalid_api_key"}
+async def test_invalid_identifier_geography_name(hass):
+ """Test that an invalid location name throws an error."""
+ with patch(
+ "pyairvisual.air_quality.AirQuality.city",
+ side_effect=NotFoundError,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data={"type": INTEGRATION_TYPE_GEOGRAPHY_NAME},
+ )
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_API_KEY: "abcde12345",
+ CONF_CITY: "Beijing",
+ CONF_STATE: "Beijing",
+ CONF_COUNTRY: "China",
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {CONF_CITY: "location_not_found"}
+
+
+async def test_invalid_identifier_geography_unknown(hass):
+ """Test that an unknown identifier issue throws an error."""
+ with patch(
+ "pyairvisual.air_quality.AirQuality.city",
+ side_effect=AirVisualError,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data={"type": INTEGRATION_TYPE_GEOGRAPHY_NAME},
+ )
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_API_KEY: "abcde12345",
+ CONF_CITY: "Beijing",
+ CONF_STATE: "Beijing",
+ CONF_COUNTRY: "China",
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "unknown"}
+
+
+async def test_invalid_identifier_node_pro(hass):
+ """Test that an invalid Node/Pro identifier shows an error."""
+ node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "my_password"}
+
+ with patch(
+ "pyairvisual.node.NodeSamba.async_connect",
+ side_effect=NodeProError,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data={"type": "AirVisual Node/Pro"}
+ )
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input=node_pro_conf
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {CONF_IP_ADDRESS: "cannot_connect"}
+
+
async def test_migration(hass):
"""Test migrating from version 1 to the current version."""
conf = {
CONF_API_KEY: "abcde12345",
CONF_GEOGRAPHIES: [
{CONF_LATITUDE: 51.528308, CONF_LONGITUDE: -0.3817765},
- {CONF_LATITUDE: 35.48847, CONF_LONGITUDE: 137.5263065},
+ {CONF_CITY: "Beijing", CONF_STATE: "Beijing", CONF_COUNTRY: "China"},
],
}
@@ -106,9 +184,9 @@ async def test_migration(hass):
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
- with patch("pyairvisual.air_quality.AirQuality.nearest_city"), patch.object(
- hass.config_entries, "async_forward_entry_setup"
- ):
+ with patch("pyairvisual.air_quality.AirQuality.city"), patch(
+ "pyairvisual.air_quality.AirQuality.nearest_city"
+ ), patch.object(hass.config_entries, "async_forward_entry_setup"):
assert await async_setup_component(hass, DOMAIN, {DOMAIN: conf})
await hass.async_block_till_done()
@@ -122,37 +200,20 @@ async def test_migration(hass):
CONF_API_KEY: "abcde12345",
CONF_LATITUDE: 51.528308,
CONF_LONGITUDE: -0.3817765,
- CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY,
+ CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS,
}
- assert config_entries[1].unique_id == "35.48847, 137.5263065"
- assert config_entries[1].title == "Cloud API (35.48847, 137.5263065)"
+ assert config_entries[1].unique_id == "Beijing, Beijing, China"
+ assert config_entries[1].title == "Cloud API (Beijing, Beijing, China)"
assert config_entries[1].data == {
CONF_API_KEY: "abcde12345",
- CONF_LATITUDE: 35.48847,
- CONF_LONGITUDE: 137.5263065,
- CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY,
+ CONF_CITY: "Beijing",
+ CONF_STATE: "Beijing",
+ CONF_COUNTRY: "China",
+ CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_NAME,
}
-async def test_node_pro_error(hass):
- """Test that an invalid Node/Pro ID shows an error."""
- node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "my_password"}
-
- with patch(
- "pyairvisual.node.NodeSamba.async_connect",
- side_effect=NodeProError,
- ):
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}, data={"type": "AirVisual Node/Pro"}
- )
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"], user_input=node_pro_conf
- )
- assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {CONF_IP_ADDRESS: "cannot_connect"}
-
-
async def test_options_flow(hass):
"""Test config flow options."""
geography_conf = {
@@ -186,8 +247,8 @@ async def test_options_flow(hass):
assert config_entry.options == {CONF_SHOW_ON_MAP: False}
-async def test_step_geography(hass):
- """Test the geograph (cloud API) step."""
+async def test_step_geography_by_coords(hass):
+ """Test setting up a geopgraphy entry by latitude/longitude."""
conf = {
CONF_API_KEY: "abcde12345",
CONF_LATITUDE: 51.528308,
@@ -200,7 +261,7 @@ async def test_step_geography(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
- data={"type": "Geographical Location"},
+ data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS},
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=conf
@@ -212,7 +273,39 @@ async def test_step_geography(hass):
CONF_API_KEY: "abcde12345",
CONF_LATITUDE: 51.528308,
CONF_LONGITUDE: -0.3817765,
- CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY,
+ CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS,
+ }
+
+
+async def test_step_geography_by_name(hass):
+ """Test setting up a geopgraphy entry by city/state/country."""
+ conf = {
+ CONF_API_KEY: "abcde12345",
+ CONF_CITY: "Beijing",
+ CONF_STATE: "Beijing",
+ CONF_COUNTRY: "China",
+ }
+
+ with patch(
+ "homeassistant.components.airvisual.async_setup_entry", return_value=True
+ ), patch("pyairvisual.air_quality.AirQuality.city"):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data={"type": INTEGRATION_TYPE_GEOGRAPHY_NAME},
+ )
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input=conf
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "Cloud API (Beijing, Beijing, China)"
+ assert result["data"] == {
+ CONF_API_KEY: "abcde12345",
+ CONF_CITY: "Beijing",
+ CONF_STATE: "Beijing",
+ CONF_COUNTRY: "China",
+ CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_NAME,
}
@@ -244,18 +337,19 @@ async def test_step_node_pro(hass):
async def test_step_reauth(hass):
"""Test that the reauth step works."""
- geography_conf = {
+ entry_data = {
CONF_API_KEY: "abcde12345",
CONF_LATITUDE: 51.528308,
CONF_LONGITUDE: -0.3817765,
+ CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS,
}
MockConfigEntry(
- domain=DOMAIN, unique_id="51.528308, -0.3817765", data=geography_conf
+ domain=DOMAIN, unique_id="51.528308, -0.3817765", data=entry_data
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": "reauth"}, data=geography_conf
+ DOMAIN, context={"source": "reauth"}, data=entry_data
)
assert result["step_id"] == "reauth_confirm"
@@ -287,11 +381,20 @@ async def test_step_user(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
- data={"type": INTEGRATION_TYPE_GEOGRAPHY},
+ data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "geography_by_coords"
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data={"type": INTEGRATION_TYPE_GEOGRAPHY_NAME},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["step_id"] == "geography"
+ assert result["step_id"] == "geography_by_name"
result = await hass.config_entries.flow.async_init(
DOMAIN,
diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py
index 514e8fa81f241a..2b50f83fd8d7e5 100644
--- a/tests/components/alarm_control_panel/test_device_action.py
+++ b/tests/components/alarm_control_panel/test_device_action.py
@@ -23,7 +23,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/alarm_control_panel/test_device_condition.py b/tests/components/alarm_control_panel/test_device_condition.py
index 33f717e189304a..450393135af2c0 100644
--- a/tests/components/alarm_control_panel/test_device_condition.py
+++ b/tests/components/alarm_control_panel/test_device_condition.py
@@ -22,7 +22,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py
index 82432bc37aba1f..3380cdd9654a82 100644
--- a/tests/components/alarm_control_panel/test_device_trigger.py
+++ b/tests/components/alarm_control_panel/test_device_trigger.py
@@ -1,4 +1,6 @@
"""The tests for Alarm control panel device triggers."""
+from datetime import timedelta
+
import pytest
from homeassistant.components.alarm_control_panel import DOMAIN
@@ -13,16 +15,19 @@
)
from homeassistant.helpers import device_registry
from homeassistant.setup import async_setup_component
+import homeassistant.util.dt as dt_util
from tests.common import (
MockConfigEntry,
assert_lists_same,
+ async_fire_time_changed,
+ async_get_device_automation_capabilities,
async_get_device_automations,
async_mock_service,
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
@@ -44,7 +49,7 @@ def calls(hass):
async def test_get_triggers(hass, device_reg, entity_reg):
- """Test we get the expected triggers from a alarm_control_panel."""
+ """Test we get the expected triggers from an alarm_control_panel."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
@@ -103,6 +108,32 @@ async def test_get_triggers(hass, device_reg, entity_reg):
assert_lists_same(triggers, expected_triggers)
+async def test_get_trigger_capabilities(hass, device_reg, entity_reg):
+ """Test we get the expected capabilities from an alarm_control_panel."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+ hass.states.async_set(
+ "alarm_control_panel.test_5678", "attributes", {"supported_features": 15}
+ )
+
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert len(triggers) == 6
+ for trigger in triggers:
+ capabilities = await async_get_device_automation_capabilities(
+ hass, "trigger", trigger
+ )
+ assert capabilities == {
+ "extra_fields": [
+ {"name": "for", "optional": True, "type": "positive_time_period_dict"}
+ ]
+ }
+
+
async def test_if_fires_on_state_change(hass, calls):
"""Test for turn_on and turn_off triggers firing."""
hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_PENDING)
@@ -230,31 +261,83 @@ async def test_if_fires_on_state_change(hass, calls):
)
# Fake that the entity is armed home.
- hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_PENDING)
hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_HOME)
await hass.async_block_till_done()
assert len(calls) == 3
assert (
calls[2].data["some"]
- == "armed_home - device - alarm_control_panel.entity - pending - armed_home - None"
+ == "armed_home - device - alarm_control_panel.entity - disarmed - armed_home - None"
)
# Fake that the entity is armed away.
- hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_PENDING)
hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_AWAY)
await hass.async_block_till_done()
assert len(calls) == 4
assert (
calls[3].data["some"]
- == "armed_away - device - alarm_control_panel.entity - pending - armed_away - None"
+ == "armed_away - device - alarm_control_panel.entity - armed_home - armed_away - None"
)
# Fake that the entity is armed night.
- hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_PENDING)
hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_NIGHT)
await hass.async_block_till_done()
assert len(calls) == 5
assert (
calls[4].data["some"]
- == "armed_night - device - alarm_control_panel.entity - pending - armed_night - None"
+ == "armed_night - device - alarm_control_panel.entity - armed_away - armed_night - None"
+ )
+
+
+async def test_if_fires_on_state_change_with_for(hass, calls):
+ """Test for triggers firing with delay."""
+ entity_id = f"{DOMAIN}.entity"
+ hass.states.async_set(entity_id, STATE_ALARM_DISARMED)
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": entity_id,
+ "type": "triggered",
+ "for": {"seconds": 5},
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "turn_off {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(
+ (
+ "platform",
+ "entity_id",
+ "from_state.state",
+ "to_state.state",
+ "for",
+ )
+ )
+ },
+ },
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
+ assert len(calls) == 0
+
+ hass.states.async_set(entity_id, STATE_ALARM_TRIGGERED)
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ await hass.async_block_till_done()
+ assert (
+ calls[0].data["some"]
+ == f"turn_off device - {entity_id} - disarmed - triggered - 0:00:05"
)
diff --git a/tests/components/alarmdecoder/test_config_flow.py b/tests/components/alarmdecoder/test_config_flow.py
index 52d1686691ca55..2ab965023bd6d9 100644
--- a/tests/components/alarmdecoder/test_config_flow.py
+++ b/tests/components/alarmdecoder/test_config_flow.py
@@ -76,8 +76,6 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title):
with patch("homeassistant.components.alarmdecoder.config_flow.AdExt.open"), patch(
"homeassistant.components.alarmdecoder.config_flow.AdExt.close"
), patch(
- "homeassistant.components.alarmdecoder.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.alarmdecoder.async_setup_entry",
return_value=True,
) as mock_setup_entry:
@@ -92,7 +90,6 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title):
}
await hass.async_block_till_done()
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
diff --git a/tests/components/alert/test_init.py b/tests/components/alert/test_init.py
index 3796322a5b5c39..199be9845ca390 100644
--- a/tests/components/alert/test_init.py
+++ b/tests/components/alert/test_init.py
@@ -120,7 +120,7 @@ async def test_is_on(hass):
async def test_setup(hass):
"""Test setup method."""
assert await async_setup_component(hass, alert.DOMAIN, TEST_CONFIG)
- assert STATE_IDLE == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_IDLE
async def test_fire(hass, mock_notifier):
@@ -128,7 +128,7 @@ async def test_fire(hass, mock_notifier):
assert await async_setup_component(hass, alert.DOMAIN, TEST_CONFIG)
hass.states.async_set("sensor.test", STATE_ON)
await hass.async_block_till_done()
- assert STATE_ON == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_ON
async def test_silence(hass, mock_notifier):
@@ -138,15 +138,15 @@ async def test_silence(hass, mock_notifier):
await hass.async_block_till_done()
async_turn_off(hass, ENTITY_ID)
await hass.async_block_till_done()
- assert STATE_OFF == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_OFF
# alert should not be silenced on next fire
hass.states.async_set("sensor.test", STATE_OFF)
await hass.async_block_till_done()
- assert STATE_IDLE == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_IDLE
hass.states.async_set("sensor.test", STATE_ON)
await hass.async_block_till_done()
- assert STATE_ON == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_ON
async def test_reset(hass, mock_notifier):
@@ -156,10 +156,10 @@ async def test_reset(hass, mock_notifier):
await hass.async_block_till_done()
async_turn_off(hass, ENTITY_ID)
await hass.async_block_till_done()
- assert STATE_OFF == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_OFF
async_turn_on(hass, ENTITY_ID)
await hass.async_block_till_done()
- assert STATE_ON == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_ON
async def test_toggle(hass, mock_notifier):
@@ -167,13 +167,13 @@ async def test_toggle(hass, mock_notifier):
assert await async_setup_component(hass, alert.DOMAIN, TEST_CONFIG)
hass.states.async_set("sensor.test", STATE_ON)
await hass.async_block_till_done()
- assert STATE_ON == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_ON
async_toggle(hass, ENTITY_ID)
await hass.async_block_till_done()
- assert STATE_OFF == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_OFF
async_toggle(hass, ENTITY_ID)
await hass.async_block_till_done()
- assert STATE_ON == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_ON
async def test_notification_no_done_message(hass):
diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py
index 0bdbac70d7db6d..cd013ca70d9b0e 100644
--- a/tests/components/alexa/test_capabilities.py
+++ b/tests/components/alexa/test_capabilities.py
@@ -323,6 +323,7 @@ async def test_report_fan_speed_state(hass):
"friendly_name": "Off fan",
"speed": "off",
"supported_features": 1,
+ "percentage": 0,
"speed_list": ["off", "low", "medium", "high"],
},
)
@@ -333,6 +334,7 @@ async def test_report_fan_speed_state(hass):
"friendly_name": "Low speed fan",
"speed": "low",
"supported_features": 1,
+ "percentage": 33,
"speed_list": ["off", "low", "medium", "high"],
},
)
@@ -343,6 +345,7 @@ async def test_report_fan_speed_state(hass):
"friendly_name": "Medium speed fan",
"speed": "medium",
"supported_features": 1,
+ "percentage": 66,
"speed_list": ["off", "low", "medium", "high"],
},
)
@@ -353,6 +356,7 @@ async def test_report_fan_speed_state(hass):
"friendly_name": "High speed fan",
"speed": "high",
"supported_features": 1,
+ "percentage": 100,
"speed_list": ["off", "low", "medium", "high"],
},
)
diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py
index 05a60c86ae06c5..c018e07c2648b0 100644
--- a/tests/components/alexa/test_smart_home.py
+++ b/tests/components/alexa/test_smart_home.py
@@ -26,7 +26,7 @@
import homeassistant.components.vacuum as vacuum
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
-from homeassistant.core import Context, callback
+from homeassistant.core import Context
from homeassistant.helpers import entityfilter
from homeassistant.setup import async_setup_component
@@ -42,17 +42,13 @@
reported_properties,
)
-from tests.common import async_mock_service
+from tests.common import async_capture_events, async_mock_service
@pytest.fixture
def events(hass):
"""Fixture that catches alexa events."""
- events = []
- hass.bus.async_listen(
- smart_home.EVENT_ALEXA_SMART_HOME, callback(lambda e: events.append(e))
- )
- yield events
+ return async_capture_events(hass, smart_home.EVENT_ALEXA_SMART_HOME)
@pytest.fixture
@@ -383,6 +379,7 @@ async def test_variable_fan(hass):
"supported_features": 1,
"speed_list": ["low", "medium", "high"],
"speed": "high",
+ "percentage": 100,
},
)
appliance = await discovery_test(device, hass)
@@ -423,82 +420,82 @@ async def test_variable_fan(hass):
"Alexa.PercentageController",
"SetPercentage",
"fan#test_2",
- "fan.set_speed",
+ "fan.set_percentage",
hass,
payload={"percentage": "50"},
)
- assert call.data["speed"] == "medium"
+ assert call.data["percentage"] == 50
call, _ = await assert_request_calls_service(
"Alexa.PercentageController",
"SetPercentage",
"fan#test_2",
- "fan.set_speed",
+ "fan.set_percentage",
hass,
payload={"percentage": "33"},
)
- assert call.data["speed"] == "low"
+ assert call.data["percentage"] == 33
call, _ = await assert_request_calls_service(
"Alexa.PercentageController",
"SetPercentage",
"fan#test_2",
- "fan.set_speed",
+ "fan.set_percentage",
hass,
payload={"percentage": "100"},
)
- assert call.data["speed"] == "high"
+ assert call.data["percentage"] == 100
await assert_percentage_changes(
hass,
- [("high", "-5"), ("off", "5"), ("low", "-80"), ("medium", "-34")],
+ [(95, "-5"), (100, "5"), (20, "-80"), (66, "-34")],
"Alexa.PercentageController",
"AdjustPercentage",
"fan#test_2",
"percentageDelta",
- "fan.set_speed",
- "speed",
+ "fan.set_percentage",
+ "percentage",
)
call, _ = await assert_request_calls_service(
"Alexa.PowerLevelController",
"SetPowerLevel",
"fan#test_2",
- "fan.set_speed",
+ "fan.set_percentage",
hass,
payload={"powerLevel": "20"},
)
- assert call.data["speed"] == "low"
+ assert call.data["percentage"] == 20
call, _ = await assert_request_calls_service(
"Alexa.PowerLevelController",
"SetPowerLevel",
"fan#test_2",
- "fan.set_speed",
+ "fan.set_percentage",
hass,
payload={"powerLevel": "50"},
)
- assert call.data["speed"] == "medium"
+ assert call.data["percentage"] == 50
call, _ = await assert_request_calls_service(
"Alexa.PowerLevelController",
"SetPowerLevel",
"fan#test_2",
- "fan.set_speed",
+ "fan.set_percentage",
hass,
payload={"powerLevel": "99"},
)
- assert call.data["speed"] == "high"
+ assert call.data["percentage"] == 99
await assert_percentage_changes(
hass,
- [("high", "-5"), ("medium", "-50"), ("low", "-80")],
+ [(95, "-5"), (50, "-50"), (20, "-80")],
"Alexa.PowerLevelController",
"AdjustPowerLevel",
"fan#test_2",
"powerLevelDelta",
- "fan.set_speed",
- "speed",
+ "fan.set_percentage",
+ "percentage",
)
diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py
index 809bca5638b263..2cbf8636d79c1b 100644
--- a/tests/components/alexa/test_state_report.py
+++ b/tests/components/alexa/test_state_report.py
@@ -175,9 +175,26 @@ async def test_doorbell_event(hass, aioclient_mock):
assert call_json["event"]["payload"]["cause"]["type"] == "PHYSICAL_INTERACTION"
assert call_json["event"]["endpoint"]["endpointId"] == "binary_sensor#test_doorbell"
+ hass.states.async_set(
+ "binary_sensor.test_doorbell",
+ "off",
+ {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"},
+ )
+
+ hass.states.async_set(
+ "binary_sensor.test_doorbell",
+ "on",
+ {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"},
+ )
+
+ await hass.async_block_till_done()
+
+ assert len(aioclient_mock.mock_calls) == 2
+
async def test_proactive_mode_filter_states(hass, aioclient_mock):
"""Test all the cases that filter states."""
+ aioclient_mock.post(TEST_URL, text="", status=202)
await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG)
# First state should report
@@ -186,7 +203,8 @@ async def test_proactive_mode_filter_states(hass, aioclient_mock):
"on",
{"friendly_name": "Test Contact Sensor", "device_class": "door"},
)
- assert len(aioclient_mock.mock_calls) == 0
+ await hass.async_block_till_done()
+ assert len(aioclient_mock.mock_calls) == 1
aioclient_mock.clear_requests()
@@ -238,3 +256,24 @@ async def test_proactive_mode_filter_states(hass, aioclient_mock):
await hass.async_block_till_done()
await hass.async_block_till_done()
assert len(aioclient_mock.mock_calls) == 0
+
+ # If serializes to same properties, it should not report
+ aioclient_mock.post(TEST_URL, text="", status=202)
+ with patch(
+ "homeassistant.components.alexa.entities.AlexaEntity.serialize_properties",
+ return_value=[{"same": "info"}],
+ ):
+ hass.states.async_set(
+ "binary_sensor.same_serialize",
+ "off",
+ {"friendly_name": "Test Contact Sensor", "device_class": "door"},
+ )
+ await hass.async_block_till_done()
+ hass.states.async_set(
+ "binary_sensor.same_serialize",
+ "off",
+ {"friendly_name": "Test Contact Sensor", "device_class": "door"},
+ )
+
+ await hass.async_block_till_done()
+ assert len(aioclient_mock.mock_calls) == 1
diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py
index b87c217181575c..3a325490064841 100644
--- a/tests/components/ambiclimate/test_config_flow.py
+++ b/tests/components/ambiclimate/test_config_flow.py
@@ -5,6 +5,7 @@
from homeassistant import data_entry_flow
from homeassistant.components.ambiclimate import config_flow
+from homeassistant.config import async_process_ha_core_config
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.setup import async_setup_component
from homeassistant.util import aiohttp
@@ -12,9 +13,11 @@
async def init_config_flow(hass):
"""Init a configuration flow."""
- await async_setup_component(
- hass, "http", {"http": {"base_url": "https://hass.com"}}
+ await async_process_ha_core_config(
+ hass,
+ {"external_url": "https://example.com"},
)
+ await async_setup_component(hass, "http", {})
config_flow.register_flow_implementation(hass, "id", "secret")
flow = config_flow.AmbiclimateFlowHandler()
@@ -58,20 +61,20 @@ async def test_full_flow_implementation(hass):
assert result["step_id"] == "auth"
assert (
result["description_placeholders"]["cb_url"]
- == "https://hass.com/api/ambiclimate"
+ == "https://example.com/api/ambiclimate"
)
url = result["description_placeholders"]["authorization_url"]
assert "https://api.ambiclimate.com/oauth2/authorize" in url
assert "client_id=id" in url
assert "response_type=code" in url
- assert "redirect_uri=https%3A%2F%2Fhass.com%2Fapi%2Fambiclimate" in url
+ assert "redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Fambiclimate" in url
with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value="test"):
result = await flow.async_step_code("123ABC")
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Ambiclimate"
- assert result["data"]["callback_url"] == "https://hass.com/api/ambiclimate"
+ assert result["data"]["callback_url"] == "https://example.com/api/ambiclimate"
assert result["data"][CONF_CLIENT_SECRET] == "secret"
assert result["data"][CONF_CLIENT_ID] == "id"
diff --git a/tests/components/analytics/__init__.py b/tests/components/analytics/__init__.py
new file mode 100644
index 00000000000000..7cf0ac9f7baf2f
--- /dev/null
+++ b/tests/components/analytics/__init__.py
@@ -0,0 +1 @@
+"""Tests for the analytics integration."""
diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py
new file mode 100644
index 00000000000000..e1716df9cdb9d4
--- /dev/null
+++ b/tests/components/analytics/test_analytics.py
@@ -0,0 +1,335 @@
+"""The tests for the analytics ."""
+from unittest.mock import AsyncMock, Mock, PropertyMock, patch
+
+import aiohttp
+import pytest
+
+from homeassistant.components.analytics.analytics import Analytics
+from homeassistant.components.analytics.const import (
+ ANALYTICS_ENDPOINT_URL,
+ ATTR_BASE,
+ ATTR_DIAGNOSTICS,
+ ATTR_PREFERENCES,
+ ATTR_STATISTICS,
+ ATTR_USAGE,
+)
+from homeassistant.components.api import ATTR_UUID
+from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
+from homeassistant.loader import IntegrationNotFound
+from homeassistant.setup import async_setup_component
+
+MOCK_UUID = "abcdefg"
+
+
+async def test_no_send(hass, caplog, aioclient_mock):
+ """Test send when no prefrences are defined."""
+ aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
+ analytics = Analytics(hass)
+ with patch(
+ "homeassistant.components.hassio.is_hassio",
+ side_effect=Mock(return_value=False),
+ ):
+ assert not analytics.preferences[ATTR_BASE]
+
+ await analytics.send_analytics()
+
+ assert "Nothing to submit" in caplog.text
+ assert len(aioclient_mock.mock_calls) == 0
+
+
+async def test_load_with_supervisor_diagnostics(hass):
+ """Test loading with a supervisor that has diagnostics enabled."""
+ analytics = Analytics(hass)
+ assert not analytics.preferences[ATTR_DIAGNOSTICS]
+ with patch(
+ "homeassistant.components.hassio.get_supervisor_info",
+ side_effect=Mock(return_value={"diagnostics": True}),
+ ), patch(
+ "homeassistant.components.hassio.is_hassio",
+ side_effect=Mock(return_value=True),
+ ):
+ await analytics.load()
+ assert analytics.preferences[ATTR_DIAGNOSTICS]
+
+
+async def test_load_with_supervisor_without_diagnostics(hass):
+ """Test loading with a supervisor that has not diagnostics enabled."""
+ analytics = Analytics(hass)
+ analytics._data[ATTR_PREFERENCES][ATTR_DIAGNOSTICS] = True
+
+ assert analytics.preferences[ATTR_DIAGNOSTICS]
+
+ with patch(
+ "homeassistant.components.hassio.get_supervisor_info",
+ side_effect=Mock(return_value={"diagnostics": False}),
+ ), patch(
+ "homeassistant.components.hassio.is_hassio",
+ side_effect=Mock(return_value=True),
+ ):
+ await analytics.load()
+
+ assert not analytics.preferences[ATTR_DIAGNOSTICS]
+
+
+async def test_failed_to_send(hass, caplog, aioclient_mock):
+ """Test failed to send payload."""
+ aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=400)
+ analytics = Analytics(hass)
+ await analytics.save_preferences({ATTR_BASE: True})
+ assert analytics.preferences[ATTR_BASE]
+ await analytics.send_analytics()
+ assert "Sending analytics failed with statuscode 400" in caplog.text
+
+
+async def test_failed_to_send_raises(hass, caplog, aioclient_mock):
+ """Test raises when failed to send payload."""
+ aioclient_mock.post(ANALYTICS_ENDPOINT_URL, exc=aiohttp.ClientError())
+ analytics = Analytics(hass)
+ await analytics.save_preferences({ATTR_BASE: True})
+ assert analytics.preferences[ATTR_BASE]
+ await analytics.send_analytics()
+ assert "Error sending analytics" in caplog.text
+
+
+async def test_send_base(hass, caplog, aioclient_mock):
+ """Test send base prefrences are defined."""
+ aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
+ analytics = Analytics(hass)
+
+ await analytics.save_preferences({ATTR_BASE: True})
+ assert analytics.preferences[ATTR_BASE]
+
+ with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex:
+ hex.return_value = MOCK_UUID
+ await analytics.send_analytics()
+
+ assert f"'uuid': '{MOCK_UUID}'" in caplog.text
+ assert f"'version': '{HA_VERSION}'" in caplog.text
+ assert "'installation_type':" in caplog.text
+ assert "'integration_count':" not in caplog.text
+ assert "'integrations':" not in caplog.text
+
+
+async def test_send_base_with_supervisor(hass, caplog, aioclient_mock):
+ """Test send base prefrences are defined."""
+ aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
+
+ analytics = Analytics(hass)
+ await analytics.save_preferences({ATTR_BASE: True})
+ assert analytics.preferences[ATTR_BASE]
+
+ with patch(
+ "homeassistant.components.hassio.get_supervisor_info",
+ side_effect=Mock(return_value={"supported": True, "healthy": True}),
+ ), patch(
+ "homeassistant.components.hassio.get_info",
+ side_effect=Mock(return_value={}),
+ ), patch(
+ "homeassistant.components.hassio.get_host_info",
+ side_effect=Mock(return_value={}),
+ ), patch(
+ "homeassistant.components.hassio.is_hassio",
+ side_effect=Mock(return_value=True),
+ ), patch(
+ "uuid.UUID.hex", new_callable=PropertyMock
+ ) as hex:
+ hex.return_value = MOCK_UUID
+ await analytics.load()
+
+ await analytics.send_analytics()
+
+ assert f"'uuid': '{MOCK_UUID}'" in caplog.text
+ assert f"'version': '{HA_VERSION}'" in caplog.text
+ assert "'supervisor': {'healthy': True, 'supported': True}}" in caplog.text
+ assert "'installation_type':" in caplog.text
+ assert "'integration_count':" not in caplog.text
+ assert "'integrations':" not in caplog.text
+
+
+async def test_send_usage(hass, caplog, aioclient_mock):
+ """Test send usage prefrences are defined."""
+ aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
+ analytics = Analytics(hass)
+ await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
+
+ assert analytics.preferences[ATTR_BASE]
+ assert analytics.preferences[ATTR_USAGE]
+ hass.config.components = ["default_config"]
+
+ await analytics.send_analytics()
+
+ assert "'integrations': ['default_config']" in caplog.text
+ assert "'integration_count':" not in caplog.text
+
+
+async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock):
+ """Test send usage with supervisor prefrences are defined."""
+ aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
+
+ analytics = Analytics(hass)
+ await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
+ assert analytics.preferences[ATTR_BASE]
+ assert analytics.preferences[ATTR_USAGE]
+ hass.config.components = ["default_config"]
+
+ with patch(
+ "homeassistant.components.hassio.get_supervisor_info",
+ side_effect=Mock(
+ return_value={
+ "healthy": True,
+ "supported": True,
+ "addons": [{"slug": "test_addon"}],
+ }
+ ),
+ ), patch(
+ "homeassistant.components.hassio.get_info",
+ side_effect=Mock(return_value={}),
+ ), patch(
+ "homeassistant.components.hassio.get_host_info",
+ side_effect=Mock(return_value={}),
+ ), patch(
+ "homeassistant.components.hassio.async_get_addon_info",
+ side_effect=AsyncMock(
+ return_value={
+ "slug": "test_addon",
+ "protected": True,
+ "version": "1",
+ "auto_update": False,
+ }
+ ),
+ ), patch(
+ "homeassistant.components.hassio.is_hassio",
+ side_effect=Mock(return_value=True),
+ ):
+ await analytics.send_analytics()
+ assert (
+ "'addons': [{'slug': 'test_addon', 'protected': True, 'version': '1', 'auto_update': False}]"
+ in caplog.text
+ )
+ assert "'addon_count':" not in caplog.text
+
+
+async def test_send_statistics(hass, caplog, aioclient_mock):
+ """Test send statistics prefrences are defined."""
+ aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
+ analytics = Analytics(hass)
+ await analytics.save_preferences({ATTR_BASE: True, ATTR_STATISTICS: True})
+ assert analytics.preferences[ATTR_BASE]
+ assert analytics.preferences[ATTR_STATISTICS]
+ hass.config.components = ["default_config"]
+
+ await analytics.send_analytics()
+ assert (
+ "'state_count': 0, 'automation_count': 0, 'integration_count': 1, 'user_count': 0"
+ in caplog.text
+ )
+ assert "'integrations':" not in caplog.text
+
+
+async def test_send_statistics_one_integration_fails(hass, caplog, aioclient_mock):
+ """Test send statistics prefrences are defined."""
+ aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
+ analytics = Analytics(hass)
+ await analytics.save_preferences({ATTR_BASE: True, ATTR_STATISTICS: True})
+ assert analytics.preferences[ATTR_BASE]
+ assert analytics.preferences[ATTR_STATISTICS]
+ hass.config.components = ["default_config"]
+
+ with patch(
+ "homeassistant.components.analytics.analytics.async_get_integration",
+ side_effect=IntegrationNotFound("any"),
+ ):
+ await analytics.send_analytics()
+
+ post_call = aioclient_mock.mock_calls[0]
+ assert "uuid" in post_call[2]
+ assert post_call[2]["integration_count"] == 0
+
+
+async def test_send_statistics_async_get_integration_unknown_exception(
+ hass, caplog, aioclient_mock
+):
+ """Test send statistics prefrences are defined."""
+ aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
+ analytics = Analytics(hass)
+ await analytics.save_preferences({ATTR_BASE: True, ATTR_STATISTICS: True})
+ assert analytics.preferences[ATTR_BASE]
+ assert analytics.preferences[ATTR_STATISTICS]
+ hass.config.components = ["default_config"]
+
+ with pytest.raises(ValueError), patch(
+ "homeassistant.components.analytics.analytics.async_get_integration",
+ side_effect=ValueError,
+ ):
+ await analytics.send_analytics()
+
+
+async def test_send_statistics_with_supervisor(hass, caplog, aioclient_mock):
+ """Test send statistics prefrences are defined."""
+ aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
+ analytics = Analytics(hass)
+ await analytics.save_preferences({ATTR_BASE: True, ATTR_STATISTICS: True})
+ assert analytics.preferences[ATTR_BASE]
+ assert analytics.preferences[ATTR_STATISTICS]
+
+ with patch(
+ "homeassistant.components.hassio.get_supervisor_info",
+ side_effect=Mock(
+ return_value={
+ "healthy": True,
+ "supported": True,
+ "addons": [{"slug": "test_addon"}],
+ }
+ ),
+ ), patch(
+ "homeassistant.components.hassio.get_info",
+ side_effect=Mock(return_value={}),
+ ), patch(
+ "homeassistant.components.hassio.get_host_info",
+ side_effect=Mock(return_value={}),
+ ), patch(
+ "homeassistant.components.hassio.async_get_addon_info",
+ side_effect=AsyncMock(
+ return_value={
+ "slug": "test_addon",
+ "protected": True,
+ "version": "1",
+ "auto_update": False,
+ }
+ ),
+ ), patch(
+ "homeassistant.components.hassio.is_hassio",
+ side_effect=Mock(return_value=True),
+ ):
+ await analytics.send_analytics()
+ assert "'addon_count': 1" in caplog.text
+ assert "'integrations':" not in caplog.text
+
+
+async def test_reusing_uuid(hass, aioclient_mock):
+ """Test reusing the stored UUID."""
+ aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
+ analytics = Analytics(hass)
+ analytics._data[ATTR_UUID] = "NOT_MOCK_UUID"
+
+ await analytics.save_preferences({ATTR_BASE: True})
+
+ with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex:
+ # This is not actually called but that in itself prove the test
+ hex.return_value = MOCK_UUID
+ await analytics.send_analytics()
+
+ assert analytics.uuid == "NOT_MOCK_UUID"
+
+
+async def test_custom_integrations(hass, aioclient_mock):
+ """Test sending custom integrations."""
+ aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
+ analytics = Analytics(hass)
+ assert await async_setup_component(hass, "test_package", {"test_package": {}})
+ await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True})
+
+ await analytics.send_analytics()
+
+ payload = aioclient_mock.mock_calls[0][2]
+ assert payload["custom_integrations"][0][ATTR_DOMAIN] == "test_package"
diff --git a/tests/components/analytics/test_init.py b/tests/components/analytics/test_init.py
new file mode 100644
index 00000000000000..af10592692644d
--- /dev/null
+++ b/tests/components/analytics/test_init.py
@@ -0,0 +1,36 @@
+"""The tests for the analytics ."""
+from homeassistant.components.analytics.const import ANALYTICS_ENDPOINT_URL, DOMAIN
+from homeassistant.setup import async_setup_component
+
+
+async def test_setup(hass):
+ """Test setup of the integration."""
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
+ await hass.async_block_till_done()
+
+ assert DOMAIN in hass.data
+
+
+async def test_websocket(hass, hass_ws_client, aioclient_mock):
+ """Test websocekt commands."""
+ aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
+ await hass.async_block_till_done()
+
+ ws_client = await hass_ws_client(hass)
+ await ws_client.send_json({"id": 1, "type": "analytics"})
+
+ response = await ws_client.receive_json()
+
+ assert response["success"]
+
+ await ws_client.send_json(
+ {"id": 2, "type": "analytics/preferences", "preferences": {"base": True}}
+ )
+ response = await ws_client.receive_json()
+ assert len(aioclient_mock.mock_calls) == 1
+ assert response["result"]["preferences"]["base"]
+
+ await ws_client.send_json({"id": 3, "type": "analytics"})
+ response = await ws_client.receive_json()
+ assert response["result"]["preferences"]["base"]
diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py
index a9a803741b12c7..d72cf36438b32e 100644
--- a/tests/components/androidtv/test_media_player.py
+++ b/tests/components/androidtv/test_media_player.py
@@ -933,12 +933,11 @@ async def test_update_lock_not_acquired(hass):
with patch(
"androidtv.androidtv.androidtv_async.AndroidTVAsync.update",
side_effect=LockNotAcquiredException,
- ):
- with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
- await hass.helpers.entity_component.async_update_entity(entity_id)
- state = hass.states.get(entity_id)
- assert state is not None
- assert state.state == STATE_OFF
+ ), patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
+ await hass.helpers.entity_component.async_update_entity(entity_id)
+ state = hass.states.get(entity_id)
+ assert state is not None
+ assert state.state == STATE_OFF
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
await hass.helpers.entity_component.async_update_entity(entity_id)
@@ -1206,19 +1205,18 @@ async def test_connection_closed_on_ha_stop(hass):
"""Test that the ADB socket connection is closed when HA stops."""
patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER)
- with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]:
- with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
- assert await async_setup_component(
- hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER
- )
- await hass.async_block_till_done()
+ with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
+ patch_key
+ ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
+ assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER)
+ await hass.async_block_till_done()
- with patch(
- "androidtv.androidtv.androidtv_async.AndroidTVAsync.adb_close"
- ) as adb_close:
- hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
- await hass.async_block_till_done()
- assert adb_close.called
+ with patch(
+ "androidtv.androidtv.androidtv_async.AndroidTVAsync.adb_close"
+ ) as adb_close:
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
+ await hass.async_block_till_done()
+ assert adb_close.called
async def test_exception(hass):
diff --git a/tests/components/apache_kafka/test_init.py b/tests/components/apache_kafka/test_init.py
index 7e793bce96af35..c4285a0cc652d7 100644
--- a/tests/components/apache_kafka/test_init.py
+++ b/tests/components/apache_kafka/test_init.py
@@ -1,7 +1,9 @@
"""The tests for the Apache Kafka component."""
+from __future__ import annotations
+
from asyncio import AbstractEventLoop
from dataclasses import dataclass
-from typing import Callable, Type
+from typing import Callable
from unittest.mock import patch
import pytest
@@ -31,7 +33,7 @@ class FilterTest:
class MockKafkaClient:
"""Mock of the Apache Kafka client for testing."""
- init: Callable[[Type[AbstractEventLoop], str, str], None]
+ init: Callable[[type[AbstractEventLoop], str, str], None]
start: Callable[[], None]
send_and_wait: Callable[[str, str], None]
diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py
index 678a8096af5129..ffda908a29b193 100644
--- a/tests/components/api/test_init.py
+++ b/tests/components/api/test_init.py
@@ -270,7 +270,6 @@ def listener(service_call):
async def test_api_call_service_with_data(hass, mock_api_client):
"""Test if the API allows us to call a service."""
- test_value = []
@ha.callback
def listener(service_call):
@@ -278,17 +277,24 @@ def listener(service_call):
Also test if our data came through.
"""
- if "test" in service_call.data:
- test_value.append(1)
+ hass.states.async_set(
+ "test.data",
+ "on",
+ {"data": service_call.data["test"]},
+ context=service_call.context,
+ )
hass.services.async_register("test_domain", "test_service", listener)
- await mock_api_client.post(
+ resp = await mock_api_client.post(
"/api/services/test_domain/test_service", json={"test": 1}
)
-
- await hass.async_block_till_done()
- assert len(test_value) == 1
+ data = await resp.json()
+ assert len(data) == 1
+ state = data[0]
+ assert state["entity_id"] == "test.data"
+ assert state["state"] == "on"
+ assert state["attributes"] == {"data": 1}
async def test_api_template(hass, mock_api_client):
diff --git a/tests/components/apns/test_notify.py b/tests/components/apns/test_notify.py
index 22d0a30ab16331..301ef1362faeb6 100644
--- a/tests/components/apns/test_notify.py
+++ b/tests/components/apns/test_notify.py
@@ -199,7 +199,7 @@ def fake_write(_out, device):
assert test_device_1 is not None
assert test_device_2 is not None
- assert "updated device 1" == test_device_1.name
+ assert test_device_1.name == "updated device 1"
@patch("homeassistant.components.apns.notify._write_device")
@@ -239,8 +239,8 @@ def fake_write(_out, device):
assert test_device_1 is not None
assert test_device_2 is not None
- assert "tracking123" == test_device_1.tracking_device_id
- assert "tracking456" == test_device_2.tracking_device_id
+ assert test_device_1.tracking_device_id == "tracking123"
+ assert test_device_2.tracking_device_id == "tracking456"
@patch("homeassistant.components.apns.notify.APNsClient")
@@ -267,16 +267,16 @@ async def test_send(mock_client, hass):
)
assert send.called
- assert 1 == len(send.mock_calls)
+ assert len(send.mock_calls) == 1
target = send.mock_calls[0][1][0]
payload = send.mock_calls[0][1][1]
- assert "1234" == target
- assert "Hello" == payload.alert
- assert 1 == payload.badge
- assert "test.mp3" == payload.sound
- assert "testing" == payload.category
+ assert target == "1234"
+ assert payload.alert == "Hello"
+ assert payload.badge == 1
+ assert payload.sound == "test.mp3"
+ assert payload.category == "testing"
@patch("homeassistant.components.apns.notify.APNsClient")
@@ -337,13 +337,13 @@ async def test_send_with_state(mock_client, hass):
notify_service.send_message(message="Hello", target="home")
assert send.called
- assert 1 == len(send.mock_calls)
+ assert len(send.mock_calls) == 1
target = send.mock_calls[0][1][0]
payload = send.mock_calls[0][1][1]
- assert "5678" == target
- assert "Hello" == payload.alert
+ assert target == "5678"
+ assert payload.alert == "Hello"
@patch("homeassistant.components.apns.notify.APNsClient")
diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py
index 8135f4e8e2c2e5..3e539e73516471 100644
--- a/tests/components/apprise/test_notify.py
+++ b/tests/components/apprise/test_notify.py
@@ -28,13 +28,14 @@ async def test_apprise_config_load_fail02(hass):
BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": "/path/"}
}
- with patch("apprise.Apprise.add", return_value=False):
- with patch("apprise.AppriseConfig.add", return_value=True):
- assert await async_setup_component(hass, BASE_COMPONENT, config)
- await hass.async_block_till_done()
+ with patch("apprise.Apprise.add", return_value=False), patch(
+ "apprise.AppriseConfig.add", return_value=True
+ ):
+ assert await async_setup_component(hass, BASE_COMPONENT, config)
+ await hass.async_block_till_done()
- # Test that our service failed to load
- assert not hass.services.has_service(BASE_COMPONENT, "test")
+ # Test that our service failed to load
+ assert not hass.services.has_service(BASE_COMPONENT, "test")
async def test_apprise_config_load_okay(hass, tmp_path):
diff --git a/tests/components/arcam_fmj/test_device_trigger.py b/tests/components/arcam_fmj/test_device_trigger.py
index 0f2cfaf28932ce..d5ab820ce46219 100644
--- a/tests/components/arcam_fmj/test_device_trigger.py
+++ b/tests/components/arcam_fmj/test_device_trigger.py
@@ -7,13 +7,12 @@
from tests.common import (
MockConfigEntry,
- assert_lists_same,
async_get_device_automations,
async_mock_service,
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
@@ -55,7 +54,13 @@ async def test_get_triggers(hass, device_reg, entity_reg):
},
]
triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
- assert_lists_same(triggers, expected_triggers)
+
+ # Test triggers are either arcam_fmj specific or media_player entity triggers
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ for expected_trigger in expected_triggers:
+ assert expected_trigger in triggers
+ for trigger in triggers:
+ assert trigger in expected_triggers or trigger["domain"] == "media_player"
async def test_if_fires_on_turn_on_request(hass, calls, player_setup, state):
@@ -77,7 +82,10 @@ async def test_if_fires_on_turn_on_request(hass, calls, player_setup, state):
},
"action": {
"service": "test.automation",
- "data_template": {"some": "{{ trigger.entity_id }}"},
+ "data_template": {
+ "some": "{{ trigger.entity_id }}",
+ "id": "{{ trigger.id }}",
+ },
},
}
]
@@ -94,3 +102,4 @@ async def test_if_fires_on_turn_on_request(hass, calls, player_setup, state):
await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0].data["some"] == player_setup
+ assert calls[0].data["id"] == 0
diff --git a/tests/components/arlo/test_sensor.py b/tests/components/arlo/test_sensor.py
index 5d729a5a6589e9..b8389d1903fb80 100644
--- a/tests/components/arlo/test_sensor.py
+++ b/tests/components/arlo/test_sensor.py
@@ -194,7 +194,7 @@ def test_update_captured_today(captured_sensor):
def _test_attributes(sensor_type):
data = _get_named_tuple({"model_id": "TEST123"})
sensor = _get_sensor("test", sensor_type, data)
- attrs = sensor.device_state_attributes
+ attrs = sensor.extra_state_attributes
assert attrs.get(ATTR_ATTRIBUTION) == "Data provided by arlo.netgear.com"
assert attrs.get("brand") == "Netgear Arlo"
assert attrs.get("model") == "TEST123"
@@ -211,7 +211,7 @@ def test_state_attributes():
def test_attributes_total_cameras(cameras_sensor):
"""Test attributes for total cameras sensor type."""
- attrs = cameras_sensor.device_state_attributes
+ attrs = cameras_sensor.extra_state_attributes
assert attrs.get(ATTR_ATTRIBUTION) == "Data provided by arlo.netgear.com"
assert attrs.get("brand") == "Netgear Arlo"
assert attrs.get("model") is None
diff --git a/tests/components/asuswrt/test_config_flow.py b/tests/components/asuswrt/test_config_flow.py
new file mode 100644
index 00000000000000..a6e24b09462154
--- /dev/null
+++ b/tests/components/asuswrt/test_config_flow.py
@@ -0,0 +1,296 @@
+"""Tests for the AsusWrt config flow."""
+from socket import gaierror
+from unittest.mock import AsyncMock, Mock, patch
+
+import pytest
+
+from homeassistant import data_entry_flow
+from homeassistant.components.asuswrt.const import (
+ CONF_DNSMASQ,
+ CONF_INTERFACE,
+ CONF_REQUIRE_IP,
+ CONF_SSH_KEY,
+ CONF_TRACK_UNKNOWN,
+ DOMAIN,
+)
+from homeassistant.components.device_tracker.const import CONF_CONSIDER_HOME
+from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_MODE,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_PROTOCOL,
+ CONF_USERNAME,
+)
+
+from tests.common import MockConfigEntry
+
+HOST = "myrouter.asuswrt.com"
+IP_ADDRESS = "192.168.1.1"
+SSH_KEY = "1234"
+
+CONFIG_DATA = {
+ CONF_HOST: HOST,
+ CONF_PORT: 22,
+ CONF_PROTOCOL: "telnet",
+ CONF_USERNAME: "user",
+ CONF_PASSWORD: "pwd",
+ CONF_MODE: "ap",
+}
+
+
+@pytest.fixture(name="connect")
+def mock_controller_connect():
+ """Mock a successful connection."""
+ with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock:
+ service_mock.return_value.connection.async_connect = AsyncMock()
+ service_mock.return_value.is_connected = True
+ service_mock.return_value.connection.disconnect = Mock()
+ yield service_mock
+
+
+async def test_user(hass, connect):
+ """Test user config."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+ # test with all provided
+ with patch(
+ "homeassistant.components.asuswrt.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry, patch(
+ "homeassistant.components.asuswrt.config_flow.socket.gethostbyname",
+ return_value=IP_ADDRESS,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data=CONFIG_DATA,
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == HOST
+ assert result["data"] == CONFIG_DATA
+
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_import(hass, connect):
+ """Test import step."""
+ with patch(
+ "homeassistant.components.asuswrt.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry, patch(
+ "homeassistant.components.asuswrt.config_flow.socket.gethostbyname",
+ return_value=IP_ADDRESS,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data=CONFIG_DATA,
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == HOST
+ assert result["data"] == CONFIG_DATA
+
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_import_ssh(hass, connect):
+ """Test import step with ssh file."""
+ config_data = CONFIG_DATA.copy()
+ config_data.pop(CONF_PASSWORD)
+ config_data[CONF_SSH_KEY] = SSH_KEY
+
+ with patch(
+ "homeassistant.components.asuswrt.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry, patch(
+ "homeassistant.components.asuswrt.config_flow.socket.gethostbyname",
+ return_value=IP_ADDRESS,
+ ), patch(
+ "homeassistant.components.asuswrt.config_flow.os.path.isfile",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.asuswrt.config_flow.os.access",
+ return_value=True,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data=config_data,
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == HOST
+ assert result["data"] == config_data
+
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_error_no_password_ssh(hass):
+ """Test we abort if component is already setup."""
+ config_data = CONFIG_DATA.copy()
+ config_data.pop(CONF_PASSWORD)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data=config_data,
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "pwd_or_ssh"}
+
+
+async def test_error_both_password_ssh(hass):
+ """Test we abort if component is already setup."""
+ config_data = CONFIG_DATA.copy()
+ config_data[CONF_SSH_KEY] = SSH_KEY
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data=config_data,
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "pwd_and_ssh"}
+
+
+async def test_error_invalid_ssh(hass):
+ """Test we abort if component is already setup."""
+ config_data = CONFIG_DATA.copy()
+ config_data.pop(CONF_PASSWORD)
+ config_data[CONF_SSH_KEY] = SSH_KEY
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data=config_data,
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "ssh_not_file"}
+
+
+async def test_error_invalid_host(hass):
+ """Test we abort if host name is invalid."""
+ with patch(
+ "homeassistant.components.asuswrt.config_flow.socket.gethostbyname",
+ side_effect=gaierror,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data=CONFIG_DATA,
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "invalid_host"}
+
+
+async def test_abort_if_already_setup(hass):
+ """Test we abort if component is already setup."""
+ MockConfigEntry(
+ domain=DOMAIN,
+ data=CONFIG_DATA,
+ ).add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.asuswrt.config_flow.socket.gethostbyname",
+ return_value=IP_ADDRESS,
+ ):
+ # Should fail, same HOST (flow)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data=CONFIG_DATA,
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "single_instance_allowed"
+
+ # Should fail, same HOST (import)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data=CONFIG_DATA,
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "single_instance_allowed"
+
+
+async def test_on_connect_failed(hass):
+ """Test when we have errors connecting the router."""
+ flow_result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ )
+
+ with patch("homeassistant.components.asuswrt.router.AsusWrt") as asus_wrt:
+ asus_wrt.return_value.connection.async_connect = AsyncMock()
+ asus_wrt.return_value.is_connected = False
+ result = await hass.config_entries.flow.async_configure(
+ flow_result["flow_id"], user_input=CONFIG_DATA
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "cannot_connect"}
+
+ with patch("homeassistant.components.asuswrt.router.AsusWrt") as asus_wrt:
+ asus_wrt.return_value.connection.async_connect = AsyncMock(side_effect=OSError)
+ result = await hass.config_entries.flow.async_configure(
+ flow_result["flow_id"], user_input=CONFIG_DATA
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "cannot_connect"}
+
+ with patch("homeassistant.components.asuswrt.router.AsusWrt") as asus_wrt:
+ asus_wrt.return_value.connection.async_connect = AsyncMock(
+ side_effect=TypeError
+ )
+ result = await hass.config_entries.flow.async_configure(
+ flow_result["flow_id"], user_input=CONFIG_DATA
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "unknown"}
+
+
+async def test_options_flow(hass):
+ """Test config flow options."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=CONFIG_DATA,
+ options={CONF_REQUIRE_IP: True},
+ )
+ config_entry.add_to_hass(hass)
+
+ with patch("homeassistant.components.asuswrt.async_setup_entry", return_value=True):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_CONSIDER_HOME: 20,
+ CONF_TRACK_UNKNOWN: True,
+ CONF_INTERFACE: "aaa",
+ CONF_DNSMASQ: "bbb",
+ CONF_REQUIRE_IP: False,
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert config_entry.options[CONF_CONSIDER_HOME] == 20
+ assert config_entry.options[CONF_TRACK_UNKNOWN] is True
+ assert config_entry.options[CONF_INTERFACE] == "aaa"
+ assert config_entry.options[CONF_DNSMASQ] == "bbb"
+ assert config_entry.options[CONF_REQUIRE_IP] is False
diff --git a/tests/components/asuswrt/test_device_tracker.py b/tests/components/asuswrt/test_device_tracker.py
deleted file mode 100644
index 941b0c340d6177..00000000000000
--- a/tests/components/asuswrt/test_device_tracker.py
+++ /dev/null
@@ -1,119 +0,0 @@
-"""The tests for the ASUSWRT device tracker platform."""
-
-from unittest.mock import AsyncMock, patch
-
-from homeassistant.components.asuswrt import (
- CONF_DNSMASQ,
- CONF_INTERFACE,
- DATA_ASUSWRT,
- DOMAIN,
-)
-from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
-from homeassistant.setup import async_setup_component
-
-
-async def test_password_or_pub_key_required(hass):
- """Test creating an AsusWRT scanner without a pass or pubkey."""
- with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt:
- AsusWrt().connection.async_connect = AsyncMock()
- AsusWrt().is_connected = False
- result = await async_setup_component(
- hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}}
- )
- assert not result
-
-
-async def test_network_unreachable(hass):
- """Test creating an AsusWRT scanner without a pass or pubkey."""
- with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt:
- AsusWrt().connection.async_connect = AsyncMock(side_effect=OSError)
- AsusWrt().is_connected = False
- result = await async_setup_component(
- hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}}
- )
- assert result
- assert hass.data.get(DATA_ASUSWRT) is None
-
-
-async def test_get_scanner_with_password_no_pubkey(hass):
- """Test creating an AsusWRT scanner with a password and no pubkey."""
- with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt:
- AsusWrt().connection.async_connect = AsyncMock()
- AsusWrt().connection.async_get_connected_devices = AsyncMock(return_value={})
- result = await async_setup_component(
- hass,
- DOMAIN,
- {
- DOMAIN: {
- CONF_HOST: "fake_host",
- CONF_USERNAME: "fake_user",
- CONF_PASSWORD: "4321",
- CONF_DNSMASQ: "/",
- }
- },
- )
- assert result
- assert hass.data[DATA_ASUSWRT] is not None
-
-
-async def test_specify_non_directory_path_for_dnsmasq(hass):
- """Test creating an AsusWRT scanner with a dnsmasq location which is not a valid directory."""
- with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt:
- AsusWrt().connection.async_connect = AsyncMock()
- AsusWrt().is_connected = False
- result = await async_setup_component(
- hass,
- DOMAIN,
- {
- DOMAIN: {
- CONF_HOST: "fake_host",
- CONF_USERNAME: "fake_user",
- CONF_PASSWORD: "4321",
- CONF_DNSMASQ: 1234,
- }
- },
- )
- assert not result
-
-
-async def test_interface(hass):
- """Test creating an AsusWRT scanner using interface eth1."""
- with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt:
- AsusWrt().connection.async_connect = AsyncMock()
- AsusWrt().connection.async_get_connected_devices = AsyncMock(return_value={})
- result = await async_setup_component(
- hass,
- DOMAIN,
- {
- DOMAIN: {
- CONF_HOST: "fake_host",
- CONF_USERNAME: "fake_user",
- CONF_PASSWORD: "4321",
- CONF_DNSMASQ: "/",
- CONF_INTERFACE: "eth1",
- }
- },
- )
- assert result
- assert hass.data[DATA_ASUSWRT] is not None
-
-
-async def test_no_interface(hass):
- """Test creating an AsusWRT scanner using no interface."""
- with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt:
- AsusWrt().connection.async_connect = AsyncMock()
- AsusWrt().is_connected = False
- result = await async_setup_component(
- hass,
- DOMAIN,
- {
- DOMAIN: {
- CONF_HOST: "fake_host",
- CONF_USERNAME: "fake_user",
- CONF_PASSWORD: "4321",
- CONF_DNSMASQ: "/",
- CONF_INTERFACE: None,
- }
- },
- )
- assert not result
diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py
index 69c70c409d5d41..e69af0cd3222f6 100644
--- a/tests/components/asuswrt/test_sensor.py
+++ b/tests/components/asuswrt/test_sensor.py
@@ -1,71 +1,158 @@
-"""The tests for the AsusWrt sensor platform."""
-
-from unittest.mock import AsyncMock, patch
+"""Tests for the AsusWrt sensor."""
+from datetime import timedelta
+from unittest.mock import AsyncMock, Mock, patch
from aioasuswrt.asuswrt import Device
+import pytest
-from homeassistant.components import sensor
-from homeassistant.components.asuswrt import (
- CONF_DNSMASQ,
- CONF_INTERFACE,
+from homeassistant.components import device_tracker, sensor
+from homeassistant.components.asuswrt.const import DOMAIN
+from homeassistant.components.asuswrt.sensor import DEFAULT_PREFIX
+from homeassistant.components.device_tracker.const import CONF_CONSIDER_HOME
+from homeassistant.const import (
+ CONF_HOST,
CONF_MODE,
+ CONF_PASSWORD,
CONF_PORT,
CONF_PROTOCOL,
- CONF_SENSORS,
- DOMAIN,
+ CONF_USERNAME,
+ STATE_HOME,
+ STATE_NOT_HOME,
)
-from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
-from homeassistant.core import HomeAssistant
-from homeassistant.setup import async_setup_component
-
-VALID_CONFIG_ROUTER_SSH = {
- DOMAIN: {
- CONF_DNSMASQ: "/",
- CONF_HOST: "fake_host",
- CONF_INTERFACE: "eth0",
- CONF_MODE: "router",
- CONF_PORT: "22",
- CONF_PROTOCOL: "ssh",
- CONF_USERNAME: "fake_user",
- CONF_PASSWORD: "fake_pass",
- CONF_SENSORS: [
- "devices",
- "download_speed",
- "download",
- "upload_speed",
- "upload",
- ],
- }
+from homeassistant.helpers import entity_registry as er
+from homeassistant.util.dt import utcnow
+
+from tests.common import MockConfigEntry, async_fire_time_changed
+
+HOST = "myrouter.asuswrt.com"
+IP_ADDRESS = "192.168.1.1"
+
+CONFIG_DATA = {
+ CONF_HOST: HOST,
+ CONF_PORT: 22,
+ CONF_PROTOCOL: "ssh",
+ CONF_USERNAME: "user",
+ CONF_PASSWORD: "pwd",
+ CONF_MODE: "router",
}
MOCK_DEVICES = {
"a1:b1:c1:d1:e1:f1": Device("a1:b1:c1:d1:e1:f1", "192.168.1.2", "Test"),
"a2:b2:c2:d2:e2:f2": Device("a2:b2:c2:d2:e2:f2", "192.168.1.3", "TestTwo"),
- "a3:b3:c3:d3:e3:f3": Device("a3:b3:c3:d3:e3:f3", "192.168.1.4", "TestThree"),
}
MOCK_BYTES_TOTAL = [60000000000, 50000000000]
MOCK_CURRENT_TRANSFER_RATES = [20000000, 10000000]
-async def test_sensors(hass: HomeAssistant, mock_device_tracker_conf):
- """Test creating an AsusWRT sensor."""
- with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt:
- AsusWrt().connection.async_connect = AsyncMock()
- AsusWrt().async_get_connected_devices = AsyncMock(return_value=MOCK_DEVICES)
- AsusWrt().async_get_bytes_total = AsyncMock(return_value=MOCK_BYTES_TOTAL)
- AsusWrt().async_get_current_transfer_rates = AsyncMock(
+@pytest.fixture(name="connect")
+def mock_controller_connect():
+ """Mock a successful connection."""
+ with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock:
+ service_mock.return_value.connection.async_connect = AsyncMock()
+ service_mock.return_value.is_connected = True
+ service_mock.return_value.connection.disconnect = Mock()
+ service_mock.return_value.async_get_connected_devices = AsyncMock(
+ return_value=MOCK_DEVICES
+ )
+ service_mock.return_value.async_get_bytes_total = AsyncMock(
+ return_value=MOCK_BYTES_TOTAL
+ )
+ service_mock.return_value.async_get_current_transfer_rates = AsyncMock(
return_value=MOCK_CURRENT_TRANSFER_RATES
)
+ yield service_mock
- assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_ROUTER_SSH)
- await hass.async_block_till_done()
- assert (
- hass.states.get(f"{sensor.DOMAIN}.asuswrt_devices_connected").state == "3"
- )
- assert (
- hass.states.get(f"{sensor.DOMAIN}.asuswrt_download_speed").state == "160.0"
- )
- assert hass.states.get(f"{sensor.DOMAIN}.asuswrt_download").state == "60.0"
- assert hass.states.get(f"{sensor.DOMAIN}.asuswrt_upload_speed").state == "80.0"
- assert hass.states.get(f"{sensor.DOMAIN}.asuswrt_upload").state == "50.0"
+async def test_sensors(hass, connect):
+ """Test creating an AsusWRT sensor."""
+ entity_reg = er.async_get(hass)
+
+ # init config entry
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=CONFIG_DATA,
+ options={CONF_CONSIDER_HOME: 60},
+ )
+
+ # init variable
+ unique_id = DOMAIN
+ name_prefix = DEFAULT_PREFIX
+ obj_prefix = name_prefix.lower()
+ sensor_prefix = f"{sensor.DOMAIN}.{obj_prefix}"
+
+ # Pre-enable the status sensor
+ entity_reg.async_get_or_create(
+ sensor.DOMAIN,
+ DOMAIN,
+ f"{unique_id} {name_prefix} Devices Connected",
+ suggested_object_id=f"{obj_prefix}_devices_connected",
+ disabled_by=None,
+ )
+ entity_reg.async_get_or_create(
+ sensor.DOMAIN,
+ DOMAIN,
+ f"{unique_id} {name_prefix} Download Speed",
+ suggested_object_id=f"{obj_prefix}_download_speed",
+ disabled_by=None,
+ )
+ entity_reg.async_get_or_create(
+ sensor.DOMAIN,
+ DOMAIN,
+ f"{unique_id} {name_prefix} Download",
+ suggested_object_id=f"{obj_prefix}_download",
+ disabled_by=None,
+ )
+ entity_reg.async_get_or_create(
+ sensor.DOMAIN,
+ DOMAIN,
+ f"{unique_id} {name_prefix} Upload Speed",
+ suggested_object_id=f"{obj_prefix}_upload_speed",
+ disabled_by=None,
+ )
+ entity_reg.async_get_or_create(
+ sensor.DOMAIN,
+ DOMAIN,
+ f"{unique_id} {name_prefix} Upload",
+ suggested_object_id=f"{obj_prefix}_upload",
+ disabled_by=None,
+ )
+
+ config_entry.add_to_hass(hass)
+
+ # initial devices setup
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
+ await hass.async_block_till_done()
+
+ assert hass.states.get(f"{device_tracker.DOMAIN}.test").state == STATE_HOME
+ assert hass.states.get(f"{device_tracker.DOMAIN}.testtwo").state == STATE_HOME
+ assert hass.states.get(f"{sensor_prefix}_download_speed").state == "160.0"
+ assert hass.states.get(f"{sensor_prefix}_download").state == "60.0"
+ assert hass.states.get(f"{sensor_prefix}_upload_speed").state == "80.0"
+ assert hass.states.get(f"{sensor_prefix}_upload").state == "50.0"
+ assert hass.states.get(f"{sensor_prefix}_devices_connected").state == "2"
+
+ # add one device and remove another
+ MOCK_DEVICES.pop("a1:b1:c1:d1:e1:f1")
+ MOCK_DEVICES["a3:b3:c3:d3:e3:f3"] = Device(
+ "a3:b3:c3:d3:e3:f3", "192.168.1.4", "TestThree"
+ )
+ async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
+ await hass.async_block_till_done()
+
+ # consider home option set, all devices still home
+ assert hass.states.get(f"{device_tracker.DOMAIN}.test").state == STATE_HOME
+ assert hass.states.get(f"{device_tracker.DOMAIN}.testtwo").state == STATE_HOME
+ assert hass.states.get(f"{device_tracker.DOMAIN}.testthree").state == STATE_HOME
+ assert hass.states.get(f"{sensor_prefix}_devices_connected").state == "2"
+
+ hass.config_entries.async_update_entry(
+ config_entry, options={CONF_CONSIDER_HOME: 0}
+ )
+ await hass.async_block_till_done()
+ async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
+ await hass.async_block_till_done()
+
+ # consider home option not set, device "test" not home
+ assert hass.states.get(f"{device_tracker.DOMAIN}.test").state == STATE_NOT_HOME
diff --git a/tests/components/atag/__init__.py b/tests/components/atag/__init__.py
index 52d53ee99480cb..c41632b9715586 100644
--- a/tests/components/atag/__init__.py
+++ b/tests/components/atag/__init__.py
@@ -1,7 +1,7 @@
"""Tests for the Atag integration."""
-from homeassistant.components.atag import DOMAIN
-from homeassistant.const import CONF_EMAIL, CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON
+from homeassistant.components.atag import DOMAIN, AtagException
+from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@@ -9,12 +9,15 @@
USER_INPUT = {
CONF_HOST: "127.0.0.1",
- CONF_EMAIL: "atag@domain.com",
CONF_PORT: 10000,
}
UID = "xxxx-xxxx-xxxx_xx-xx-xxx-xxx"
-PAIR_REPLY = {"pair_reply": {"status": {"device_id": UID}, "acc_status": 2}}
-UPDATE_REPLY = {"update_reply": {"status": {"device_id": UID}, "acc_status": 2}}
+AUTHORIZED = 2
+UNAUTHORIZED = 3
+PAIR_REPLY = {"pair_reply": {"status": {"device_id": UID}, "acc_status": AUTHORIZED}}
+UPDATE_REPLY = {
+ "update_reply": {"status": {"device_id": UID}, "acc_status": AUTHORIZED}
+}
RECEIVE_REPLY = {
"retrieve_reply": {
"status": {"device_id": UID},
@@ -46,35 +49,52 @@
"dhw_max_set": 65,
"dhw_min_set": 40,
},
- "acc_status": 2,
+ "acc_status": AUTHORIZED,
}
}
-async def init_integration(
- hass: HomeAssistant,
- aioclient_mock: AiohttpClientMocker,
- rgbw: bool = False,
- skip_setup: bool = False,
-) -> MockConfigEntry:
- """Set up the Atag integration in Home Assistant."""
-
+def mock_connection(
+ aioclient_mock: AiohttpClientMocker, authorized=True, conn_error=False
+) -> None:
+ """Mock the requests to Atag endpoint."""
+ if conn_error:
+ aioclient_mock.post(
+ "http://127.0.0.1:10000/pair",
+ exc=AtagException,
+ )
+ aioclient_mock.post(
+ "http://127.0.0.1:10000/retrieve",
+ exc=AtagException,
+ )
+ return
+ PAIR_REPLY["pair_reply"].update(
+ {"acc_status": AUTHORIZED if authorized else UNAUTHORIZED}
+ )
+ RECEIVE_REPLY["retrieve_reply"].update(
+ {"acc_status": AUTHORIZED if authorized else UNAUTHORIZED}
+ )
aioclient_mock.post(
"http://127.0.0.1:10000/retrieve",
json=RECEIVE_REPLY,
- headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.post(
"http://127.0.0.1:10000/update",
json=UPDATE_REPLY,
- headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.post(
"http://127.0.0.1:10000/pair",
json=PAIR_REPLY,
- headers={"Content-Type": CONTENT_TYPE_JSON},
)
+
+async def init_integration(
+ hass: HomeAssistant,
+ aioclient_mock: AiohttpClientMocker,
+ skip_setup: bool = False,
+) -> MockConfigEntry:
+ """Set up the Atag integration in Home Assistant."""
+ mock_connection(aioclient_mock)
entry = MockConfigEntry(domain=DOMAIN, data=USER_INPUT)
entry.add_to_hass(hass)
diff --git a/tests/components/atag/test_climate.py b/tests/components/atag/test_climate.py
index 3d511821bafb0f..3c9a9c3f820a6f 100644
--- a/tests/components/atag/test_climate.py
+++ b/tests/components/atag/test_climate.py
@@ -1,8 +1,7 @@
"""Tests for the Atag climate platform."""
-
from unittest.mock import PropertyMock, patch
-from homeassistant.components.atag import CLIMATE, DOMAIN
+from homeassistant.components.atag.climate import CLIMATE, DOMAIN, PRESET_MAP
from homeassistant.components.climate import (
ATTR_HVAC_ACTION,
ATTR_HVAC_MODE,
@@ -12,13 +11,11 @@
SERVICE_SET_PRESET_MODE,
SERVICE_SET_TEMPERATURE,
)
-from homeassistant.components.climate.const import CURRENT_HVAC_HEAT, PRESET_AWAY
-from homeassistant.components.homeassistant import (
- DOMAIN as HA_DOMAIN,
- SERVICE_UPDATE_ENTITY,
-)
+from homeassistant.components.climate.const import CURRENT_HVAC_IDLE, PRESET_AWAY
+from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from tests.components.atag import UID, init_integration
@@ -31,17 +28,13 @@ async def test_climate(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test the creation and values of Atag climate device."""
- with patch("pyatag.entities.Climate.status"):
- entry = await init_integration(hass, aioclient_mock)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ await init_integration(hass, aioclient_mock)
+ entity_registry = er.async_get(hass)
- assert registry.async_is_registered(CLIMATE_ID)
- entry = registry.async_get(CLIMATE_ID)
- assert entry.unique_id == f"{UID}-{CLIMATE}"
- assert (
- hass.states.get(CLIMATE_ID).attributes[ATTR_HVAC_ACTION]
- == CURRENT_HVAC_HEAT
- )
+ assert entity_registry.async_is_registered(CLIMATE_ID)
+ entity = entity_registry.async_get(CLIMATE_ID)
+ assert entity.unique_id == f"{UID}-{CLIMATE}"
+ assert hass.states.get(CLIMATE_ID).attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
async def test_setting_climate(
@@ -67,7 +60,7 @@ async def test_setting_climate(
blocking=True,
)
await hass.async_block_till_done()
- mock_set_preset.assert_called_once_with(PRESET_AWAY)
+ mock_set_preset.assert_called_once_with(PRESET_MAP[PRESET_AWAY])
with patch("pyatag.entities.Climate.set_hvac_mode") as mock_set_hvac:
await hass.services.async_call(
@@ -93,18 +86,18 @@ async def test_incorrect_modes(
assert hass.states.get(CLIMATE_ID).state == STATE_UNKNOWN
-async def test_update_service(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+async def test_update_failed(
+ hass: HomeAssistant,
+ aioclient_mock: AiohttpClientMocker,
) -> None:
- """Test the updater service is called."""
- await init_integration(hass, aioclient_mock)
+ """Test data is not destroyed on update failure."""
+ entry = await init_integration(hass, aioclient_mock)
await async_setup_component(hass, HA_DOMAIN, {})
- with patch("pyatag.AtagOne.update") as updater:
- await hass.services.async_call(
- HA_DOMAIN,
- SERVICE_UPDATE_ENTITY,
- {ATTR_ENTITY_ID: CLIMATE_ID},
- blocking=True,
- )
+ assert hass.states.get(CLIMATE_ID).state == HVAC_MODE_HEAT
+ coordinator = hass.data[DOMAIN][entry.entry_id]
+ with patch("pyatag.AtagOne.update", side_effect=TimeoutError) as updater:
+ await coordinator.async_refresh()
await hass.async_block_till_done()
updater.assert_called_once()
+ assert not coordinator.last_update_success
+ assert coordinator.data.id == UID
diff --git a/tests/components/atag/test_config_flow.py b/tests/components/atag/test_config_flow.py
index 81375792c711f4..a92e73ae18e4bc 100644
--- a/tests/components/atag/test_config_flow.py
+++ b/tests/components/atag/test_config_flow.py
@@ -1,24 +1,18 @@
"""Tests for the Atag config flow."""
from unittest.mock import PropertyMock, patch
-from pyatag import errors
-
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.atag import DOMAIN
from homeassistant.core import HomeAssistant
-from tests.components.atag import (
- PAIR_REPLY,
- RECEIVE_REPLY,
- UID,
- USER_INPUT,
- init_integration,
-)
+from . import UID, USER_INPUT, init_integration, mock_connection
+
from tests.test_util.aiohttp import AiohttpClientMocker
-async def test_show_form(hass):
+async def test_show_form(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker):
"""Test that the form is served with no input."""
+ mock_connection(aioclient_mock)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
@@ -48,28 +42,30 @@ async def test_adding_second_device(
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
-async def test_connection_error(hass):
+async def test_connection_error(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+):
"""Test we show user form on Atag connection error."""
- with patch("pyatag.AtagOne.authorize", side_effect=errors.AtagException()):
- result = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": config_entries.SOURCE_USER},
- data=USER_INPUT,
- )
+ mock_connection(aioclient_mock, conn_error=True)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_USER},
+ data=USER_INPUT,
+ )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "cannot_connect"}
-async def test_unauthorized(hass):
+async def test_unauthorized(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker):
"""Test we show correct form when Unauthorized error is raised."""
- with patch("pyatag.AtagOne.authorize", side_effect=errors.Unauthorized()):
- result = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": config_entries.SOURCE_USER},
- data=USER_INPUT,
- )
+ mock_connection(aioclient_mock, authorized=False)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_USER},
+ data=USER_INPUT,
+ )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "unauthorized"}
@@ -79,14 +75,7 @@ async def test_full_flow_implementation(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test registering an integration and finishing flow works."""
- aioclient_mock.post(
- "http://127.0.0.1:10000/pair",
- json=PAIR_REPLY,
- )
- aioclient_mock.post(
- "http://127.0.0.1:10000/retrieve",
- json=RECEIVE_REPLY,
- )
+ mock_connection(aioclient_mock)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
diff --git a/tests/components/atag/test_init.py b/tests/components/atag/test_init.py
index b86de8a8be5ee5..7b7f3c1e33a416 100644
--- a/tests/components/atag/test_init.py
+++ b/tests/components/atag/test_init.py
@@ -1,13 +1,11 @@
"""Tests for the ATAG integration."""
-from unittest.mock import patch
-
-import aiohttp
from homeassistant.components.atag import DOMAIN
from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY
from homeassistant.core import HomeAssistant
-from tests.components.atag import init_integration
+from . import init_integration, mock_connection
+
from tests.test_util.aiohttp import AiohttpClientMocker
@@ -15,20 +13,11 @@ async def test_config_entry_not_ready(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test configuration entry not ready on library error."""
- aioclient_mock.post("http://127.0.0.1:10000/retrieve", exc=aiohttp.ClientError)
+ mock_connection(aioclient_mock, conn_error=True)
entry = await init_integration(hass, aioclient_mock)
assert entry.state == ENTRY_STATE_SETUP_RETRY
-async def test_config_entry_empty_reply(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
-) -> None:
- """Test configuration entry not ready when library returns False."""
- with patch("pyatag.AtagOne.update", return_value=False):
- entry = await init_integration(hass, aioclient_mock)
- assert entry.state == ENTRY_STATE_SETUP_RETRY
-
-
async def test_unload_config_entry(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
diff --git a/tests/components/atag/test_sensors.py b/tests/components/atag/test_sensors.py
index e7bf4df44e9040..aeefcb2789b97d 100644
--- a/tests/components/atag/test_sensors.py
+++ b/tests/components/atag/test_sensors.py
@@ -1,7 +1,7 @@
"""Tests for the Atag sensor platform."""
-
from homeassistant.components.atag.sensor import SENSORS
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
from tests.components.atag import UID, init_integration
from tests.test_util.aiohttp import AiohttpClientMocker
@@ -12,7 +12,7 @@ async def test_sensors(
) -> None:
"""Test the creation of ATAG sensors."""
entry = await init_integration(hass, aioclient_mock)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
for item in SENSORS:
sensor_id = "_".join(f"sensor.{item}".lower().split())
diff --git a/tests/components/atag/test_water_heater.py b/tests/components/atag/test_water_heater.py
index 5eb219fa3bcd6d..4c78302224d359 100644
--- a/tests/components/atag/test_water_heater.py
+++ b/tests/components/atag/test_water_heater.py
@@ -1,11 +1,11 @@
"""Tests for the Atag water heater platform."""
-
from unittest.mock import patch
from homeassistant.components.atag import DOMAIN, WATER_HEATER
from homeassistant.components.water_heater import SERVICE_SET_TEMPERATURE
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
from tests.components.atag import UID, init_integration
from tests.test_util.aiohttp import AiohttpClientMocker
@@ -19,7 +19,7 @@ async def test_water_heater(
"""Test the creation of Atag water heater."""
with patch("pyatag.entities.DHW.status"):
entry = await init_integration(hass, aioclient_mock)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert registry.async_is_registered(WATER_HEATER_ID)
entry = registry.async_get(WATER_HEATER_ID)
diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py
index e02e1ec59dd502..9a54a708a4f834 100644
--- a/tests/components/august/mocks.py
+++ b/tests/components/august/mocks.py
@@ -6,31 +6,31 @@
# from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
-from august.activity import (
+from yalexs.activity import (
+ ACTIVITY_ACTIONS_BRIDGE_OPERATION,
ACTIVITY_ACTIONS_DOOR_OPERATION,
ACTIVITY_ACTIONS_DOORBELL_DING,
ACTIVITY_ACTIONS_DOORBELL_MOTION,
ACTIVITY_ACTIONS_DOORBELL_VIEW,
ACTIVITY_ACTIONS_LOCK_OPERATION,
+ SOURCE_LOCK_OPERATE,
+ SOURCE_LOG,
+ BridgeOperationActivity,
DoorbellDingActivity,
DoorbellMotionActivity,
DoorbellViewActivity,
DoorOperationActivity,
LockOperationActivity,
)
-from august.authenticator import AuthenticationState
-from august.doorbell import Doorbell, DoorbellDetail
-from august.lock import Lock, LockDetail
-
-from homeassistant.components.august import (
- CONF_LOGIN_METHOD,
- CONF_PASSWORD,
- CONF_USERNAME,
- DOMAIN,
-)
-from homeassistant.setup import async_setup_component
+from yalexs.authenticator import AuthenticationState
+from yalexs.doorbell import Doorbell, DoorbellDetail
+from yalexs.lock import Lock, LockDetail
+from yalexs.pubnub_async import AugustPubNub
+
+from homeassistant.components.august.const import CONF_LOGIN_METHOD, DOMAIN
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from tests.common import load_fixture
+from tests.common import MockConfigEntry, load_fixture
def _mock_get_config():
@@ -53,7 +53,9 @@ def _mock_authenticator(auth_state):
@patch("homeassistant.components.august.gateway.ApiAsync")
@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate")
-async def _mock_setup_august(hass, api_instance, authenticate_mock, api_mock):
+async def _mock_setup_august(
+ hass, api_instance, pubnub_mock, authenticate_mock, api_mock
+):
"""Set up august integration."""
authenticate_mock.side_effect = MagicMock(
return_value=_mock_august_authentication(
@@ -61,16 +63,27 @@ async def _mock_setup_august(hass, api_instance, authenticate_mock, api_mock):
)
)
api_mock.return_value = api_instance
- assert await async_setup_component(hass, DOMAIN, _mock_get_config())
- await hass.async_block_till_done()
- return True
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=_mock_get_config()[DOMAIN],
+ options={},
+ )
+ entry.add_to_hass(hass)
+ with patch("homeassistant.components.august.async_create_pubnub"), patch(
+ "homeassistant.components.august.AugustPubNub", return_value=pubnub_mock
+ ):
+ assert await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+ return entry
async def _create_august_with_devices(
- hass, devices, api_call_side_effects=None, activities=None
+ hass, devices, api_call_side_effects=None, activities=None, pubnub=None
):
if api_call_side_effects is None:
api_call_side_effects = {}
+ if pubnub is None:
+ pubnub = AugustPubNub()
device_data = {"doorbells": [], "locks": []}
for device in devices:
@@ -151,10 +164,12 @@ def unlock_return_activities_side_effect(access_token, device_id):
"unlock_return_activities"
] = unlock_return_activities_side_effect
- return await _mock_setup_august_with_api_side_effects(hass, api_call_side_effects)
+ return await _mock_setup_august_with_api_side_effects(
+ hass, api_call_side_effects, pubnub
+ )
-async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects):
+async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects, pubnub):
api_instance = MagicMock(name="Api")
if api_call_side_effects["get_lock_detail"]:
@@ -192,11 +207,13 @@ async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects):
side_effect=api_call_side_effects["unlock_return_activities"]
)
- return await _mock_setup_august(hass, api_instance)
+ api_instance.async_get_user = AsyncMock(return_value={"UserID": "abc"})
+
+ return await _mock_setup_august(hass, api_instance, pubnub)
def _mock_august_authentication(token_text, token_timestamp, state):
- authentication = MagicMock(name="august.authentication")
+ authentication = MagicMock(name="yalexs.authentication")
type(authentication).state = PropertyMock(return_value=state)
type(authentication).access_token = PropertyMock(return_value=token_text)
type(authentication).access_token_expires = PropertyMock(
@@ -300,23 +317,25 @@ async def _mock_doorsense_missing_august_lock_detail(hass):
def _mock_lock_operation_activity(lock, action, offset):
return LockOperationActivity(
+ SOURCE_LOCK_OPERATE,
{
"dateTime": (time.time() + offset) * 1000,
"deviceID": lock.device_id,
"deviceType": "lock",
"action": action,
- }
+ },
)
def _mock_door_operation_activity(lock, action, offset):
return DoorOperationActivity(
+ SOURCE_LOCK_OPERATE,
{
"dateTime": (time.time() + offset) * 1000,
"deviceID": lock.device_id,
"deviceType": "lock",
"action": action,
- }
+ },
)
@@ -326,13 +345,15 @@ def _activity_from_dict(activity_dict):
activity_dict["dateTime"] = time.time() * 1000
if action in ACTIVITY_ACTIONS_DOORBELL_DING:
- return DoorbellDingActivity(activity_dict)
+ return DoorbellDingActivity(SOURCE_LOG, activity_dict)
if action in ACTIVITY_ACTIONS_DOORBELL_MOTION:
- return DoorbellMotionActivity(activity_dict)
+ return DoorbellMotionActivity(SOURCE_LOG, activity_dict)
if action in ACTIVITY_ACTIONS_DOORBELL_VIEW:
- return DoorbellViewActivity(activity_dict)
+ return DoorbellViewActivity(SOURCE_LOG, activity_dict)
if action in ACTIVITY_ACTIONS_LOCK_OPERATION:
- return LockOperationActivity(activity_dict)
+ return LockOperationActivity(SOURCE_LOG, activity_dict)
if action in ACTIVITY_ACTIONS_DOOR_OPERATION:
- return DoorOperationActivity(activity_dict)
+ return DoorOperationActivity(SOURCE_LOG, activity_dict)
+ if action in ACTIVITY_ACTIONS_BRIDGE_OPERATION:
+ return BridgeOperationActivity(SOURCE_LOG, activity_dict)
return None
diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py
index 763f9f9528f8ed..0912b05bec11f2 100644
--- a/tests/components/august/test_binary_sensor.py
+++ b/tests/components/august/test_binary_sensor.py
@@ -1,4 +1,8 @@
"""The binary_sensor tests for the august platform."""
+import datetime
+from unittest.mock import Mock, patch
+
+from yalexs.pubnub_async import AugustPubNub
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.const import (
@@ -9,7 +13,10 @@
STATE_ON,
STATE_UNAVAILABLE,
)
+from homeassistant.helpers import device_registry as dr
+import homeassistant.util.dt as dt_util
+from tests.common import async_fire_time_changed
from tests.components.august.mocks import (
_create_august_with_devices,
_mock_activities_from_fixture,
@@ -52,6 +59,22 @@ async def test_doorsense(hass):
assert binary_sensor_online_with_doorsense_name.state == STATE_OFF
+async def test_lock_bridge_offline(hass):
+ """Test creation of a lock with doorsense and bridge that goes offline."""
+ lock_one = await _mock_lock_from_fixture(
+ hass, "get_lock.online_with_doorsense.json"
+ )
+ activities = await _mock_activities_from_fixture(
+ hass, "get_activity.bridge_offline.json"
+ )
+ await _create_august_with_devices(hass, [lock_one], activities=activities)
+
+ binary_sensor_online_with_doorsense_name = hass.states.get(
+ "binary_sensor.online_with_doorsense_name_open"
+ )
+ assert binary_sensor_online_with_doorsense_name.state == STATE_UNAVAILABLE
+
+
async def test_create_doorbell(hass):
"""Test creation of a doorbell."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
@@ -112,6 +135,108 @@ async def test_create_doorbell_with_motion(hass):
"binary_sensor.k98gidt45gul_name_ding"
)
assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF
+ new_time = dt_util.utcnow() + datetime.timedelta(seconds=40)
+ native_time = datetime.datetime.now() + datetime.timedelta(seconds=40)
+ with patch(
+ "homeassistant.components.august.binary_sensor._native_datetime",
+ return_value=native_time,
+ ):
+ async_fire_time_changed(hass, new_time)
+ await hass.async_block_till_done()
+ binary_sensor_k98gidt45gul_name_motion = hass.states.get(
+ "binary_sensor.k98gidt45gul_name_motion"
+ )
+ assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF
+
+
+async def test_doorbell_update_via_pubnub(hass):
+ """Test creation of a doorbell that can be updated via pubnub."""
+ doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
+ pubnub = AugustPubNub()
+
+ await _create_august_with_devices(hass, [doorbell_one], pubnub=pubnub)
+ assert doorbell_one.pubsub_channel == "7c7a6672-59c8-3333-ffff-dcd98705cccc"
+
+ binary_sensor_k98gidt45gul_name_motion = hass.states.get(
+ "binary_sensor.k98gidt45gul_name_motion"
+ )
+ assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF
+ binary_sensor_k98gidt45gul_name_ding = hass.states.get(
+ "binary_sensor.k98gidt45gul_name_ding"
+ )
+ assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF
+
+ pubnub.message(
+ pubnub,
+ Mock(
+ channel=doorbell_one.pubsub_channel,
+ timetoken=dt_util.utcnow().timestamp() * 10000000,
+ message={
+ "status": "imagecapture",
+ "data": {
+ "result": {
+ "created_at": "2021-03-16T01:07:08.817Z",
+ "secure_url": "https://dyu7azbnaoi74.cloudfront.net/zip/images/zip.jpeg",
+ },
+ },
+ },
+ ),
+ )
+
+ await hass.async_block_till_done()
+
+ binary_sensor_k98gidt45gul_name_motion = hass.states.get(
+ "binary_sensor.k98gidt45gul_name_motion"
+ )
+ assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON
+ binary_sensor_k98gidt45gul_name_ding = hass.states.get(
+ "binary_sensor.k98gidt45gul_name_ding"
+ )
+ assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF
+
+ new_time = dt_util.utcnow() + datetime.timedelta(seconds=40)
+ native_time = datetime.datetime.now() + datetime.timedelta(seconds=40)
+ with patch(
+ "homeassistant.components.august.binary_sensor._native_datetime",
+ return_value=native_time,
+ ):
+ async_fire_time_changed(hass, new_time)
+ await hass.async_block_till_done()
+
+ binary_sensor_k98gidt45gul_name_motion = hass.states.get(
+ "binary_sensor.k98gidt45gul_name_motion"
+ )
+ assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF
+
+ pubnub.message(
+ pubnub,
+ Mock(
+ channel=doorbell_one.pubsub_channel,
+ timetoken=dt_util.utcnow().timestamp() * 10000000,
+ message={
+ "status": "buttonpush",
+ },
+ ),
+ )
+ await hass.async_block_till_done()
+
+ binary_sensor_k98gidt45gul_name_ding = hass.states.get(
+ "binary_sensor.k98gidt45gul_name_ding"
+ )
+ assert binary_sensor_k98gidt45gul_name_ding.state == STATE_ON
+ new_time = dt_util.utcnow() + datetime.timedelta(seconds=40)
+ native_time = datetime.datetime.now() + datetime.timedelta(seconds=40)
+ with patch(
+ "homeassistant.components.august.binary_sensor._native_datetime",
+ return_value=native_time,
+ ):
+ async_fire_time_changed(hass, new_time)
+ await hass.async_block_till_done()
+
+ binary_sensor_k98gidt45gul_name_ding = hass.states.get(
+ "binary_sensor.k98gidt45gul_name_ding"
+ )
+ assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF
async def test_doorbell_device_registry(hass):
@@ -119,7 +244,7 @@ async def test_doorbell_device_registry(hass):
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json")
await _create_august_with_devices(hass, [doorbell_one])
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
reg_device = device_registry.async_get_device(identifiers={("august", "tmt100")})
assert reg_device.model == "hydra1"
diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py
index c1e7c9bb3c5544..c87291e0f79d2a 100644
--- a/tests/components/august/test_config_flow.py
+++ b/tests/components/august/test_config_flow.py
@@ -1,7 +1,7 @@
"""Test the August config flow."""
from unittest.mock import patch
-from august.authenticator import ValidationResult
+from yalexs.authenticator import ValidationResult
from homeassistant import config_entries, setup
from homeassistant.components.august.const import (
@@ -54,9 +54,7 @@ async def test_form(hass):
assert result2["data"] == {
CONF_LOGIN_METHOD: "email",
CONF_USERNAME: "my@email.tld",
- CONF_PASSWORD: "test-password",
CONF_INSTALL_ID: None,
- CONF_TIMEOUT: 10,
CONF_ACCESS_TOKEN_CACHE_FILE: ".my@email.tld.august.conf",
}
assert len(mock_setup.mock_calls) == 1
@@ -215,9 +213,7 @@ async def test_form_needs_validate(hass):
assert result4["data"] == {
CONF_LOGIN_METHOD: "email",
CONF_USERNAME: "my@email.tld",
- CONF_PASSWORD: "test-password",
CONF_INSTALL_ID: None,
- CONF_TIMEOUT: 10,
CONF_ACCESS_TOKEN_CACHE_FILE: ".my@email.tld.august.conf",
}
assert len(mock_setup.mock_calls) == 1
@@ -268,3 +264,75 @@ async def test_form_reauth(hass):
assert result2["reason"] == "reauth_successful"
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_reauth_with_2fa(hass):
+ """Test reauthenticate with 2fa."""
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ CONF_LOGIN_METHOD: "email",
+ CONF_USERNAME: "my@email.tld",
+ CONF_PASSWORD: "test-password",
+ CONF_INSTALL_ID: None,
+ CONF_TIMEOUT: 10,
+ CONF_ACCESS_TOKEN_CACHE_FILE: ".my@email.tld.august.conf",
+ },
+ unique_id="my@email.tld",
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=entry.data
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.august.config_flow.AugustGateway.async_authenticate",
+ side_effect=RequireValidation,
+ ), patch(
+ "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code",
+ return_value=True,
+ ) as mock_send_verification_code:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_PASSWORD: "new-test-password",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert len(mock_send_verification_code.mock_calls) == 1
+ assert result2["type"] == "form"
+ assert result2["errors"] is None
+ assert result2["step_id"] == "validation"
+
+ # Try with the CORRECT verification code and we setup
+ with patch(
+ "homeassistant.components.august.config_flow.AugustGateway.async_authenticate",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.august.gateway.AuthenticatorAsync.async_validate_verification_code",
+ return_value=ValidationResult.VALIDATED,
+ ) as mock_validate_verification_code, patch(
+ "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code",
+ return_value=True,
+ ) as mock_send_verification_code, patch(
+ "homeassistant.components.august.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.august.async_setup_entry", return_value=True
+ ) as mock_setup_entry:
+ result3 = await hass.config_entries.flow.async_configure(
+ result2["flow_id"],
+ {VERIFICATION_CODE_KEY: "correct"},
+ )
+ await hass.async_block_till_done()
+
+ assert len(mock_validate_verification_code.mock_calls) == 1
+ assert len(mock_send_verification_code.mock_calls) == 0
+ assert result3["type"] == "abort"
+ assert result3["reason"] == "reauth_successful"
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py
index ced073600082da..54a5e9321f2860 100644
--- a/tests/components/august/test_gateway.py
+++ b/tests/components/august/test_gateway.py
@@ -1,7 +1,7 @@
"""The gateway tests for the august platform."""
from unittest.mock import MagicMock, patch
-from august.authenticator_common import AuthenticationState
+from yalexs.authenticator_common import AuthenticationState
from homeassistant.components.august.const import DOMAIN
from homeassistant.components.august.gateway import AugustGateway
diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py
index e881ac09c975d9..8b0885f7341779 100644
--- a/tests/components/august/test_init.py
+++ b/tests/components/august/test_init.py
@@ -3,34 +3,25 @@
from unittest.mock import patch
from aiohttp import ClientResponseError
-from august.authenticator_common import AuthenticationState
-from august.exceptions import AugustApiAIOHTTPError
+from yalexs.authenticator_common import AuthenticationState
+from yalexs.exceptions import AugustApiAIOHTTPError
from homeassistant import setup
-from homeassistant.components.august.const import (
- CONF_ACCESS_TOKEN_CACHE_FILE,
- CONF_INSTALL_ID,
- CONF_LOGIN_METHOD,
- DEFAULT_AUGUST_CONFIG_FILE,
- DOMAIN,
-)
+from homeassistant.components.august.const import DOMAIN
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.config_entries import (
+ ENTRY_STATE_LOADED,
ENTRY_STATE_SETUP_ERROR,
ENTRY_STATE_SETUP_RETRY,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
- CONF_PASSWORD,
- CONF_TIMEOUT,
- CONF_USERNAME,
SERVICE_LOCK,
SERVICE_UNLOCK,
STATE_LOCKED,
STATE_ON,
)
from homeassistant.exceptions import HomeAssistantError
-from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
from tests.components.august.mocks import (
@@ -56,7 +47,7 @@ async def test_august_is_offline(hass):
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
- "august.authenticator_async.AuthenticatorAsync.async_authenticate",
+ "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate",
side_effect=asyncio.TimeoutError,
):
await hass.config_entries.async_setup(config_entry.entry_id)
@@ -149,35 +140,6 @@ async def test_lock_has_doorsense(hass):
assert binary_sensor_missing_doorsense_id_name_open is None
-async def test_set_up_from_yaml(hass):
- """Test to make sure config is imported from yaml."""
-
- await setup.async_setup_component(hass, "persistent_notification", {})
- with patch(
- "homeassistant.components.august.async_setup_august",
- return_value=True,
- ) as mock_setup_august, patch(
- "homeassistant.components.august.config_flow.AugustGateway.async_authenticate",
- return_value=True,
- ):
- assert await async_setup_component(hass, DOMAIN, _mock_get_config())
- await hass.async_block_till_done()
- assert len(mock_setup_august.mock_calls) == 1
- call = mock_setup_august.call_args
- args, _ = call
- imported_config_entry = args[1]
- # The import must use DEFAULT_AUGUST_CONFIG_FILE so they
- # do not loose their token when config is migrated
- assert imported_config_entry.data == {
- CONF_ACCESS_TOKEN_CACHE_FILE: DEFAULT_AUGUST_CONFIG_FILE,
- CONF_INSTALL_ID: None,
- CONF_LOGIN_METHOD: "email",
- CONF_PASSWORD: "mocked_password",
- CONF_TIMEOUT: None,
- CONF_USERNAME: "mocked_username",
- }
-
-
async def test_auth_fails(hass):
"""Config entry state is ENTRY_STATE_SETUP_ERROR when auth fails."""
@@ -191,7 +153,7 @@ async def test_auth_fails(hass):
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
- "august.authenticator_async.AuthenticatorAsync.async_authenticate",
+ "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate",
side_effect=ClientResponseError(None, None, status=401),
):
await hass.config_entries.async_setup(config_entry.entry_id)
@@ -201,7 +163,7 @@ async def test_auth_fails(hass):
flows = hass.config_entries.flow.async_progress()
- assert flows[0]["step_id"] == "user"
+ assert flows[0]["step_id"] == "reauth_validate"
async def test_bad_password(hass):
@@ -217,7 +179,7 @@ async def test_bad_password(hass):
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
- "august.authenticator_async.AuthenticatorAsync.async_authenticate",
+ "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate",
return_value=_mock_august_authentication(
"original_token", 1234, AuthenticationState.BAD_PASSWORD
),
@@ -229,7 +191,7 @@ async def test_bad_password(hass):
flows = hass.config_entries.flow.async_progress()
- assert flows[0]["step_id"] == "user"
+ assert flows[0]["step_id"] == "reauth_validate"
async def test_http_failure(hass):
@@ -245,7 +207,7 @@ async def test_http_failure(hass):
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
- "august.authenticator_async.AuthenticatorAsync.async_authenticate",
+ "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate",
side_effect=ClientResponseError(None, None, status=500),
):
await hass.config_entries.async_setup(config_entry.entry_id)
@@ -269,7 +231,7 @@ async def test_unknown_auth_state(hass):
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
- "august.authenticator_async.AuthenticatorAsync.async_authenticate",
+ "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate",
return_value=_mock_august_authentication("original_token", 1234, None),
):
await hass.config_entries.async_setup(config_entry.entry_id)
@@ -279,7 +241,7 @@ async def test_unknown_auth_state(hass):
flows = hass.config_entries.flow.async_progress()
- assert flows[0]["step_id"] == "user"
+ assert flows[0]["step_id"] == "reauth_validate"
async def test_requires_validation_state(hass):
@@ -295,7 +257,7 @@ async def test_requires_validation_state(hass):
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
- "august.authenticator_async.AuthenticatorAsync.async_authenticate",
+ "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate",
return_value=_mock_august_authentication(
"original_token", 1234, AuthenticationState.REQUIRES_VALIDATION
),
@@ -305,4 +267,20 @@ async def test_requires_validation_state(hass):
assert config_entry.state == ENTRY_STATE_SETUP_ERROR
- assert hass.config_entries.flow.async_progress() == []
+ assert len(hass.config_entries.flow.async_progress()) == 1
+ assert hass.config_entries.flow.async_progress()[0]["context"]["source"] == "reauth"
+
+
+async def test_load_unload(hass):
+ """Config entry can be unloaded."""
+
+ august_operative_lock = await _mock_operative_august_lock_detail(hass)
+ august_inoperative_lock = await _mock_inoperative_august_lock_detail(hass)
+ config_entry = await _create_august_with_devices(
+ hass, [august_operative_lock, august_inoperative_lock]
+ )
+
+ assert config_entry.state == ENTRY_STATE_LOADED
+
+ await hass.config_entries.async_unload(config_entry.entry_id)
+ await hass.async_block_till_done()
diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py
index d013da30ff6435..5b3c163780f39c 100644
--- a/tests/components/august/test_lock.py
+++ b/tests/components/august/test_lock.py
@@ -1,4 +1,8 @@
"""The lock tests for the august platform."""
+import datetime
+from unittest.mock import Mock
+
+from yalexs.pubnub_async import AugustPubNub
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.const import (
@@ -6,10 +10,14 @@
SERVICE_LOCK,
SERVICE_UNLOCK,
STATE_LOCKED,
+ STATE_UNAVAILABLE,
STATE_UNKNOWN,
STATE_UNLOCKED,
)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
+import homeassistant.util.dt as dt_util
+from tests.common import async_fire_time_changed
from tests.components.august.mocks import (
_create_august_with_devices,
_mock_activities_from_fixture,
@@ -23,7 +31,7 @@ async def test_lock_device_registry(hass):
lock_one = await _mock_doorsense_enabled_august_lock_detail(hass)
await _create_august_with_devices(hass, [lock_one])
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
reg_device = device_registry.async_get_device(
identifiers={("august", "online_with_doorsense")}
@@ -90,7 +98,7 @@ async def test_one_lock_operation(hass):
assert lock_online_with_doorsense_name.state == STATE_LOCKED
# No activity means it will be unavailable until the activity feed has data
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
lock_operator_sensor = entity_registry.async_get(
"sensor.online_with_doorsense_name_operator"
)
@@ -112,3 +120,116 @@ async def test_one_lock_unknown_state(hass):
lock_brokenid_name = hass.states.get("lock.brokenid_name")
assert lock_brokenid_name.state == STATE_UNKNOWN
+
+
+async def test_lock_bridge_offline(hass):
+ """Test creation of a lock with doorsense and bridge that goes offline."""
+ lock_one = await _mock_doorsense_enabled_august_lock_detail(hass)
+
+ activities = await _mock_activities_from_fixture(
+ hass, "get_activity.bridge_offline.json"
+ )
+ await _create_august_with_devices(hass, [lock_one], activities=activities)
+
+ lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
+
+ assert lock_online_with_doorsense_name.state == STATE_UNAVAILABLE
+
+
+async def test_lock_bridge_online(hass):
+ """Test creation of a lock with doorsense and bridge that goes offline."""
+ lock_one = await _mock_doorsense_enabled_august_lock_detail(hass)
+
+ activities = await _mock_activities_from_fixture(
+ hass, "get_activity.bridge_online.json"
+ )
+ await _create_august_with_devices(hass, [lock_one], activities=activities)
+
+ lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
+
+ assert lock_online_with_doorsense_name.state == STATE_LOCKED
+
+
+async def test_lock_update_via_pubnub(hass):
+ """Test creation of a lock with doorsense and bridge."""
+ lock_one = await _mock_doorsense_enabled_august_lock_detail(hass)
+ assert lock_one.pubsub_channel == "pubsub"
+ pubnub = AugustPubNub()
+
+ activities = await _mock_activities_from_fixture(hass, "get_activity.lock.json")
+ config_entry = await _create_august_with_devices(
+ hass, [lock_one], activities=activities, pubnub=pubnub
+ )
+
+ lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
+
+ assert lock_online_with_doorsense_name.state == STATE_LOCKED
+
+ pubnub.message(
+ pubnub,
+ Mock(
+ channel=lock_one.pubsub_channel,
+ timetoken=dt_util.utcnow().timestamp() * 10000000,
+ message={
+ "status": "kAugLockState_Unlocking",
+ },
+ ),
+ )
+
+ await hass.async_block_till_done()
+ lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
+ assert lock_online_with_doorsense_name.state == STATE_UNLOCKED
+
+ pubnub.message(
+ pubnub,
+ Mock(
+ channel=lock_one.pubsub_channel,
+ timetoken=dt_util.utcnow().timestamp() * 10000000,
+ message={
+ "status": "kAugLockState_Locking",
+ },
+ ),
+ )
+
+ await hass.async_block_till_done()
+ lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
+ assert lock_online_with_doorsense_name.state == STATE_LOCKED
+
+ async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30))
+ await hass.async_block_till_done()
+ lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
+ assert lock_online_with_doorsense_name.state == STATE_LOCKED
+
+ pubnub.connected = True
+ async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30))
+ await hass.async_block_till_done()
+ lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
+ assert lock_online_with_doorsense_name.state == STATE_LOCKED
+
+ # Ensure pubnub status is always preserved
+ async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2))
+ await hass.async_block_till_done()
+ lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
+ assert lock_online_with_doorsense_name.state == STATE_LOCKED
+
+ pubnub.message(
+ pubnub,
+ Mock(
+ channel=lock_one.pubsub_channel,
+ timetoken=dt_util.utcnow().timestamp() * 10000000,
+ message={
+ "status": "kAugLockState_Unlocking",
+ },
+ ),
+ )
+ await hass.async_block_till_done()
+ lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
+ assert lock_online_with_doorsense_name.state == STATE_UNLOCKED
+
+ async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4))
+ await hass.async_block_till_done()
+ lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
+ assert lock_online_with_doorsense_name.state == STATE_UNLOCKED
+
+ await hass.config_entries.async_unload(config_entry.entry_id)
+ await hass.async_block_till_done()
diff --git a/tests/components/august/test_sensor.py b/tests/components/august/test_sensor.py
index fb7ddfde979a32..254f88ef4e913a 100644
--- a/tests/components/august/test_sensor.py
+++ b/tests/components/august/test_sensor.py
@@ -1,6 +1,6 @@
"""The sensor tests for the august platform."""
-
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNKNOWN
+from homeassistant.helpers import entity_registry as er
from tests.components.august.mocks import (
_create_august_with_devices,
@@ -29,7 +29,7 @@ async def test_create_doorbell_offline(hass):
"""Test creation of a doorbell that is offline."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json")
await _create_august_with_devices(hass, [doorbell_one])
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
sensor_tmt100_name_battery = hass.states.get("sensor.tmt100_name_battery")
assert sensor_tmt100_name_battery.state == "81"
@@ -55,7 +55,7 @@ async def test_create_lock_with_linked_keypad(hass):
"""Test creation of a lock with a linked keypad that both have a battery."""
lock_one = await _mock_lock_from_fixture(hass, "get_lock.doorsense_init.json")
await _create_august_with_devices(hass, [lock_one])
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
sensor_a6697750d607098bae8d6baa11ef8063_name_battery = hass.states.get(
"sensor.a6697750d607098bae8d6baa11ef8063_name_battery"
@@ -85,7 +85,7 @@ async def test_create_lock_with_low_battery_linked_keypad(hass):
"""Test creation of a lock with a linked keypad that both have a battery."""
lock_one = await _mock_lock_from_fixture(hass, "get_lock.low_keypad_battery.json")
await _create_august_with_devices(hass, [lock_one])
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
sensor_a6697750d607098bae8d6baa11ef8063_name_battery = hass.states.get(
"sensor.a6697750d607098bae8d6baa11ef8063_name_battery"
@@ -133,7 +133,7 @@ async def test_lock_operator_bluetooth(hass):
)
await _create_august_with_devices(hass, [lock_one], activities=activities)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
lock_operator_sensor = entity_registry.async_get(
"sensor.online_with_doorsense_name_operator"
)
@@ -177,7 +177,7 @@ async def test_lock_operator_keypad(hass):
)
await _create_august_with_devices(hass, [lock_one], activities=activities)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
lock_operator_sensor = entity_registry.async_get(
"sensor.online_with_doorsense_name_operator"
)
@@ -219,7 +219,7 @@ async def test_lock_operator_remote(hass):
activities = await _mock_activities_from_fixture(hass, "get_activity.lock.json")
await _create_august_with_devices(hass, [lock_one], activities=activities)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
lock_operator_sensor = entity_registry.async_get(
"sensor.online_with_doorsense_name_operator"
)
@@ -263,7 +263,7 @@ async def test_lock_operator_autorelock(hass):
)
await _create_august_with_devices(hass, [lock_one], activities=activities)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
lock_operator_sensor = entity_registry.async_get(
"sensor.online_with_doorsense_name_operator"
)
diff --git a/tests/components/aurora/test_config_flow.py b/tests/components/aurora/test_config_flow.py
index b9e0496f66803e..d839b024468575 100644
--- a/tests/components/aurora/test_config_flow.py
+++ b/tests/components/aurora/test_config_flow.py
@@ -2,6 +2,8 @@
from unittest.mock import patch
+from aiohttp import ClientError
+
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.aurora.const import DOMAIN
@@ -27,8 +29,6 @@ async def test_form(hass):
"homeassistant.components.aurora.config_flow.AuroraForecast.get_forecast_data",
return_value=True,
), patch(
- "homeassistant.components.aurora.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.aurora.async_setup_entry",
return_value=True,
) as mock_setup_entry:
@@ -41,7 +41,6 @@ async def test_form(hass):
assert result2["type"] == "create_entry"
assert result2["title"] == "Aurora - Home"
assert result2["data"] == DATA
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
@@ -55,7 +54,7 @@ async def test_form_cannot_connect(hass):
with patch(
"homeassistant.components.aurora.AuroraForecast.get_forecast_data",
- side_effect=ConnectionError,
+ side_effect=ClientError,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
diff --git a/tests/components/automation/conftest.py b/tests/components/automation/conftest.py
index a967e0af19254d..438948e84754fb 100644
--- a/tests/components/automation/conftest.py
+++ b/tests/components/automation/conftest.py
@@ -1,3 +1,3 @@
"""Conftest for automation tests."""
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py
index 1a6ccec7a8e119..747e162fe46d21 100644
--- a/tests/components/automation/test_blueprint.py
+++ b/tests/components/automation/test_blueprint.py
@@ -84,6 +84,7 @@ def set_person_state(state, extra={}):
_hass, config, variables, _context = mock_call_action.mock_calls[0][1]
message_tpl = config.pop("message")
assert config == {
+ "alias": "Notify that a person has left the zone",
"domain": "mobile_app",
"type": "notify",
"device_id": "abcdefgh",
diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py
index c31af555e32075..5997be22644cee 100644
--- a/tests/components/automation/test_init.py
+++ b/tests/components/automation/test_init.py
@@ -1,5 +1,6 @@
"""The tests for the automation component."""
import asyncio
+import logging
from unittest.mock import Mock, patch
import pytest
@@ -29,7 +30,12 @@
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
-from tests.common import assert_setup_component, async_mock_service, mock_restore_cache
+from tests.common import (
+ assert_setup_component,
+ async_capture_events,
+ async_mock_service,
+ mock_restore_cache,
+)
from tests.components.logbook.test_init import MockLazyEventPartialState
@@ -152,7 +158,7 @@ async def test_two_triggers(hass, calls):
assert len(calls) == 2
-async def test_trigger_service_ignoring_condition(hass, calls):
+async def test_trigger_service_ignoring_condition(hass, caplog, calls):
"""Test triggers."""
assert await async_setup_component(
hass,
@@ -162,19 +168,25 @@ async def test_trigger_service_ignoring_condition(hass, calls):
"alias": "test",
"trigger": [{"platform": "event", "event_type": "test_event"}],
"condition": {
- "condition": "state",
+ "condition": "numeric_state",
"entity_id": "non.existing",
- "state": "beer",
+ "above": "1",
},
"action": {"service": "test.automation"},
}
},
)
+ caplog.clear()
+ caplog.set_level(logging.WARNING)
+
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 0
+ assert len(caplog.record_tuples) == 1
+ assert caplog.record_tuples[0][1] == logging.WARNING
+
await hass.services.async_call(
"automation", "trigger", {"entity_id": "automation.test"}, blocking=True
)
@@ -489,10 +501,7 @@ async def test_reload_config_service(hass, calls, hass_admin_user, hass_read_onl
assert len(calls) == 1
assert calls[0].data.get("event") == "test_event"
- test_reload_event = []
- hass.bus.async_listen(
- EVENT_AUTOMATION_RELOADED, lambda event: test_reload_event.append(event)
- )
+ test_reload_event = async_capture_events(hass, EVENT_AUTOMATION_RELOADED)
with patch(
"homeassistant.config.load_yaml_config_file",
@@ -1235,6 +1244,94 @@ async def test_automation_variables(hass, caplog):
assert len(calls) == 3
+async def test_automation_trigger_variables(hass, caplog):
+ """Test automation trigger variables."""
+ calls = async_mock_service(hass, "test", "automation")
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "variables": {
+ "event_type": "{{ trigger.event.event_type }}",
+ },
+ "trigger_variables": {
+ "test_var": "defined_in_config",
+ },
+ "trigger": {"platform": "event", "event_type": "test_event"},
+ "action": {
+ "service": "test.automation",
+ "data": {
+ "value": "{{ test_var }}",
+ "event_type": "{{ event_type }}",
+ },
+ },
+ },
+ {
+ "variables": {
+ "event_type": "{{ trigger.event.event_type }}",
+ "test_var": "overridden_in_config",
+ },
+ "trigger_variables": {
+ "test_var": "defined_in_config",
+ },
+ "trigger": {"platform": "event", "event_type": "test_event_2"},
+ "action": {
+ "service": "test.automation",
+ "data": {
+ "value": "{{ test_var }}",
+ "event_type": "{{ event_type }}",
+ },
+ },
+ },
+ ]
+ },
+ )
+ hass.bus.async_fire("test_event")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["value"] == "defined_in_config"
+ assert calls[0].data["event_type"] == "test_event"
+
+ hass.bus.async_fire("test_event_2")
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[1].data["value"] == "overridden_in_config"
+ assert calls[1].data["event_type"] == "test_event_2"
+
+ assert "Error rendering variables" not in caplog.text
+
+
+async def test_automation_bad_trigger_variables(hass, caplog):
+ """Test automation trigger variables accessing hass is rejected."""
+ calls = async_mock_service(hass, "test", "automation")
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger_variables": {
+ "test_var": "{{ states('foo.bar') }}",
+ },
+ "trigger": {"platform": "event", "event_type": "test_event"},
+ "action": {
+ "service": "test.automation",
+ },
+ },
+ ]
+ },
+ )
+ hass.bus.async_fire("test_event")
+ assert "Use of 'states' is not supported in limited templates" in caplog.text
+
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+
async def test_blueprint_automation(hass, calls):
"""Test blueprint automation."""
assert await async_setup_component(
@@ -1258,3 +1355,53 @@ async def test_blueprint_automation(hass, calls):
assert automation.entities_in_automation(hass, "automation.automation_0") == [
"light.kitchen"
]
+
+
+async def test_blueprint_automation_bad_config(hass, caplog):
+ """Test blueprint automation with bad inputs."""
+ assert await async_setup_component(
+ hass,
+ "automation",
+ {
+ "automation": {
+ "use_blueprint": {
+ "path": "test_event_service.yaml",
+ "input": {
+ "trigger_event": "blueprint_event",
+ "service_to_call": {"dict": "not allowed"},
+ },
+ }
+ }
+ },
+ )
+ assert "generated invalid automation" in caplog.text
+
+
+async def test_trigger_service(hass, calls):
+ """Test the automation trigger service."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "alias": "hello",
+ "trigger": {"platform": "event", "event_type": "test_event"},
+ "action": {
+ "service": "test.automation",
+ "data_template": {"trigger": "{{ trigger }}"},
+ },
+ }
+ },
+ )
+ context = Context()
+ await hass.services.async_call(
+ "automation",
+ "trigger",
+ {"entity_id": "automation.hello"},
+ blocking=True,
+ context=context,
+ )
+
+ assert len(calls) == 1
+ assert calls[0].data.get("trigger") == {"platform": None}
+ assert calls[0].context.parent_id is context.id
diff --git a/tests/components/automation/test_logbook.py b/tests/components/automation/test_logbook.py
new file mode 100644
index 00000000000000..e13ebdc17a111f
--- /dev/null
+++ b/tests/components/automation/test_logbook.py
@@ -0,0 +1,54 @@
+"""Test automation logbook."""
+from homeassistant.components import automation, logbook
+from homeassistant.core import Context
+from homeassistant.setup import async_setup_component
+
+from tests.components.logbook.test_init import MockLazyEventPartialState
+
+
+async def test_humanify_automation_trigger_event(hass):
+ """Test humanifying Shelly click event."""
+ hass.config.components.add("recorder")
+ assert await async_setup_component(hass, "automation", {})
+ assert await async_setup_component(hass, "logbook", {})
+ entity_attr_cache = logbook.EntityAttributeCache(hass)
+ context = Context()
+
+ event1, event2 = list(
+ logbook.humanify(
+ hass,
+ [
+ MockLazyEventPartialState(
+ automation.EVENT_AUTOMATION_TRIGGERED,
+ {
+ "name": "Bla",
+ "entity_id": "automation.bla",
+ "source": "state change of input_boolean.yo",
+ },
+ context=context,
+ ),
+ MockLazyEventPartialState(
+ automation.EVENT_AUTOMATION_TRIGGERED,
+ {
+ "name": "Bla",
+ "entity_id": "automation.bla",
+ },
+ context=context,
+ ),
+ ],
+ entity_attr_cache,
+ {},
+ )
+ )
+
+ assert event1["name"] == "Bla"
+ assert event1["message"] == "has been triggered by state change of input_boolean.yo"
+ assert event1["source"] == "state change of input_boolean.yo"
+ assert event1["context_id"] == context.id
+ assert event1["entity_id"] == "automation.bla"
+
+ assert event2["name"] == "Bla"
+ assert event2["message"] == "has been triggered"
+ assert event2["source"] is None
+ assert event2["context_id"] == context.id
+ assert event2["entity_id"] == "automation.bla"
diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py
index 0fcbab99a3a207..b37e8dbf5d2c33 100644
--- a/tests/components/awair/test_sensor.py
+++ b/tests/components/awair/test_sensor.py
@@ -1,5 +1,4 @@
"""Tests for the Awair sensor platform."""
-
from unittest.mock import patch
from homeassistant.components.awair.const import (
@@ -27,6 +26,7 @@
STATE_UNAVAILABLE,
TEMP_CELSIUS,
)
+from homeassistant.helpers import entity_registry as er
from .const import (
AWAIR_UUID,
@@ -74,7 +74,7 @@ async def test_awair_gen1_sensors(hass):
fixtures = [USER_FIXTURE, DEVICES_FIXTURE, GEN1_DATA_FIXTURE]
await setup_awair(hass, fixtures)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert_expected_properties(
hass,
@@ -170,7 +170,7 @@ async def test_awair_gen2_sensors(hass):
fixtures = [USER_FIXTURE, DEVICES_FIXTURE, GEN2_DATA_FIXTURE]
await setup_awair(hass, fixtures)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert_expected_properties(
hass,
@@ -204,7 +204,7 @@ async def test_awair_mint_sensors(hass):
fixtures = [USER_FIXTURE, DEVICES_FIXTURE, MINT_DATA_FIXTURE]
await setup_awair(hass, fixtures)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert_expected_properties(
hass,
@@ -246,7 +246,7 @@ async def test_awair_glow_sensors(hass):
fixtures = [USER_FIXTURE, DEVICES_FIXTURE, GLOW_DATA_FIXTURE]
await setup_awair(hass, fixtures)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert_expected_properties(
hass,
@@ -266,7 +266,7 @@ async def test_awair_omni_sensors(hass):
fixtures = [USER_FIXTURE, DEVICES_FIXTURE, OMNI_DATA_FIXTURE]
await setup_awair(hass, fixtures)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert_expected_properties(
hass,
@@ -319,7 +319,7 @@ async def test_awair_unavailable(hass):
fixtures = [USER_FIXTURE, DEVICES_FIXTURE, GEN1_DATA_FIXTURE]
await setup_awair(hass, fixtures)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert_expected_properties(
hass,
diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py
index 33ea820f9cfefd..be4483593665b2 100644
--- a/tests/components/axis/conftest.py
+++ b/tests/components/axis/conftest.py
@@ -1,2 +1,112 @@
-"""axis conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+"""Axis conftest."""
+
+from typing import Optional
+from unittest.mock import patch
+
+from axis.rtsp import (
+ SIGNAL_DATA,
+ SIGNAL_FAILED,
+ SIGNAL_PLAYING,
+ STATE_PLAYING,
+ STATE_STOPPED,
+)
+import pytest
+
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
+
+
+@pytest.fixture(autouse=True)
+def mock_axis_rtspclient():
+ """No real RTSP communication allowed."""
+ with patch("axis.streammanager.RTSPClient") as rtsp_client_mock:
+
+ rtsp_client_mock.return_value.session.state = STATE_STOPPED
+
+ async def start_stream():
+ """Set state to playing when calling RTSPClient.start."""
+ rtsp_client_mock.return_value.session.state = STATE_PLAYING
+
+ rtsp_client_mock.return_value.start = start_stream
+
+ def stop_stream():
+ """Set state to stopped when calling RTSPClient.stop."""
+ rtsp_client_mock.return_value.session.state = STATE_STOPPED
+
+ rtsp_client_mock.return_value.stop = stop_stream
+
+ def make_rtsp_call(data: Optional[dict] = None, state: str = ""):
+ """Generate a RTSP call."""
+ axis_streammanager_session_callback = rtsp_client_mock.call_args[0][4]
+
+ if data:
+ rtsp_client_mock.return_value.rtp.data = data
+ axis_streammanager_session_callback(signal=SIGNAL_DATA)
+ elif state:
+ axis_streammanager_session_callback(signal=state)
+ else:
+ raise NotImplementedError
+
+ yield make_rtsp_call
+
+
+@pytest.fixture(autouse=True)
+def mock_rtsp_event(mock_axis_rtspclient):
+ """Fixture to allow mocking received RTSP events."""
+
+ def send_event(
+ topic: str,
+ data_type: str,
+ data_value: str,
+ operation: str = "Initialized",
+ source_name: str = "",
+ source_idx: str = "",
+ ) -> None:
+ source = ""
+ if source_name != "" and source_idx != "":
+ source = f''
+
+ event = f"""
+
+
+
+
+ {topic}
+
+
+
+ uri://bf32a3b9-e5e7-4d57-a48d-1b5be9ae7b16/ProducerReference
+
+
+
+
+ {source}
+
+
+
+
+
+
+
+
+
+"""
+
+ mock_axis_rtspclient(data=event.encode("utf-8"))
+
+ yield send_event
+
+
+@pytest.fixture(autouse=True)
+def mock_rtsp_signal_state(mock_axis_rtspclient):
+ """Fixture to allow mocking RTSP state signalling."""
+
+ def send_signal(connected: bool) -> None:
+ """Signal state change of RTSP connection."""
+ signal = SIGNAL_PLAYING if connected else SIGNAL_FAILED
+ mock_axis_rtspclient(state=signal)
+
+ yield send_signal
diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py
index 98ef55282c3aac..2429ec618554ac 100644
--- a/tests/components/axis/test_binary_sensor.py
+++ b/tests/components/axis/test_binary_sensor.py
@@ -10,31 +10,6 @@
from .test_device import NAME, setup_axis_integration
-EVENTS = [
- {
- "operation": "Initialized",
- "topic": "tns1:Device/tnsaxis:Sensor/PIR",
- "source": "sensor",
- "source_idx": "0",
- "type": "state",
- "value": "0",
- },
- {
- "operation": "Initialized",
- "topic": "tns1:PTZController/tnsaxis:PTZPresets/Channel_1",
- "source": "PresetToken",
- "source_idx": "0",
- "type": "on_preset",
- "value": "1",
- },
- {
- "operation": "Initialized",
- "topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1",
- "type": "active",
- "value": "1",
- },
-]
-
async def test_platform_manually_configured(hass):
"""Test that nothing happens when platform is manually configured."""
@@ -57,12 +32,30 @@ async def test_no_binary_sensors(hass):
assert not hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)
-async def test_binary_sensors(hass):
+async def test_binary_sensors(hass, mock_rtsp_event):
"""Test that sensors are loaded properly."""
- config_entry = await setup_axis_integration(hass)
- device = hass.data[AXIS_DOMAIN][config_entry.unique_id]
+ await setup_axis_integration(hass)
- device.api.event.update(EVENTS)
+ mock_rtsp_event(
+ topic="tns1:Device/tnsaxis:Sensor/PIR",
+ data_type="state",
+ data_value="0",
+ source_name="sensor",
+ source_idx="0",
+ )
+ mock_rtsp_event(
+ topic="tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1",
+ data_type="active",
+ data_value="1",
+ )
+ # Unsupported event
+ mock_rtsp_event(
+ topic="tns1:PTZController/tnsaxis:PTZPresets/Channel_1",
+ data_type="on_preset",
+ data_value="1",
+ source_name="PresetToken",
+ source_idx="0",
+ )
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 2
diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py
index a5371395638543..cb6e5b1a12be0d 100644
--- a/tests/components/axis/test_device.py
+++ b/tests/components/axis/test_device.py
@@ -23,7 +23,9 @@
CONF_PASSWORD,
CONF_PORT,
CONF_USERNAME,
+ STATE_OFF,
STATE_ON,
+ STATE_UNAVAILABLE,
)
from tests.common import MockConfigEntry, async_fire_mqtt_message
@@ -288,7 +290,7 @@ async def setup_axis_integration(hass, config=ENTRY_CONFIG, options=ENTRY_OPTION
)
config_entry.add_to_hass(hass)
- with patch("axis.rtsp.RTSPClient.start", return_value=True), respx.mock:
+ with respx.mock:
mock_default_vapix_requests(respx)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
@@ -389,12 +391,38 @@ async def test_update_address(hass):
assert len(mock_setup_entry.mock_calls) == 1
-async def test_device_unavailable(hass):
+async def test_device_unavailable(hass, mock_rtsp_event, mock_rtsp_signal_state):
"""Successful setup."""
- config_entry = await setup_axis_integration(hass)
- device = hass.data[AXIS_DOMAIN][config_entry.unique_id]
- device.async_connection_status_callback(status=False)
- assert not device.available
+ await setup_axis_integration(hass)
+
+ # Provide an entity that can be used to verify connection state on
+ mock_rtsp_event(
+ topic="tns1:AudioSource/tnsaxis:TriggerLevel",
+ data_type="triggered",
+ data_value="10",
+ source_name="channel",
+ source_idx="1",
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1").state == STATE_OFF
+
+ # Connection to device has failed
+
+ mock_rtsp_signal_state(connected=False)
+ await hass.async_block_till_done()
+
+ assert (
+ hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1").state
+ == STATE_UNAVAILABLE
+ )
+
+ # Connection to device has been restored
+
+ mock_rtsp_signal_state(connected=True)
+ await hass.async_block_till_done()
+
+ assert hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1").state == STATE_OFF
async def test_device_reset(hass):
diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py
index b7faceaf10d7bf..5ca9fb1eb8dbaf 100644
--- a/tests/components/axis/test_init.py
+++ b/tests/components/axis/test_init.py
@@ -13,7 +13,7 @@
CONF_PORT,
CONF_USERNAME,
)
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import format_mac
from homeassistant.setup import async_setup_component
@@ -83,7 +83,7 @@ async def test_migrate_entry(hass):
assert not entry.unique_id
# Create entity entry to migrate to new unique ID
- registry = await entity_registry.async_get_registry(hass)
+ registry = er.async_get(hass)
registry.async_get_or_create(
BINARY_SENSOR_DOMAIN,
AXIS_DOMAIN,
@@ -109,7 +109,7 @@ async def test_migrate_entry(hass):
CONF_MODEL: "model",
CONF_NAME: "name",
}
- assert entry.version == 3
+ assert entry.version == 2 # Keep version to support rollbacking
assert entry.unique_id == "00:40:8c:12:34:56"
vmd4_entity = registry.async_get("binary_sensor.vmd4")
diff --git a/tests/components/axis/test_light.py b/tests/components/axis/test_light.py
index db4ba86ceae436..db7ca6921fb88d 100644
--- a/tests/components/axis/test_light.py
+++ b/tests/components/axis/test_light.py
@@ -27,24 +27,6 @@
"name": "Light Control",
}
-EVENT_ON = {
- "operation": "Initialized",
- "topic": "tns1:Device/tnsaxis:Light/Status",
- "source": "id",
- "source_idx": "0",
- "type": "state",
- "value": "ON",
-}
-
-EVENT_OFF = {
- "operation": "Initialized",
- "topic": "tns1:Device/tnsaxis:Light/Status",
- "source": "id",
- "source_idx": "0",
- "type": "state",
- "value": "OFF",
-}
-
async def test_platform_manually_configured(hass):
"""Test that nothing happens when platform is manually configured."""
@@ -62,7 +44,9 @@ async def test_no_lights(hass):
assert not hass.states.async_entity_ids(LIGHT_DOMAIN)
-async def test_no_light_entity_without_light_control_representation(hass):
+async def test_no_light_entity_without_light_control_representation(
+ hass, mock_rtsp_event
+):
"""Verify no lights entities get created without light control representation."""
api_discovery = deepcopy(API_DISCOVERY_RESPONSE)
api_discovery["data"]["apiList"].append(API_DISCOVERY_LIGHT_CONTROL)
@@ -73,23 +57,27 @@ async def test_no_light_entity_without_light_control_representation(hass):
with patch.dict(API_DISCOVERY_RESPONSE, api_discovery), patch.dict(
LIGHT_CONTROL_RESPONSE, light_control
):
- config_entry = await setup_axis_integration(hass)
- device = hass.data[AXIS_DOMAIN][config_entry.unique_id]
-
- device.api.event.update([EVENT_ON])
+ await setup_axis_integration(hass)
+
+ mock_rtsp_event(
+ topic="tns1:Device/tnsaxis:Light/Status",
+ data_type="state",
+ data_value="ON",
+ source_name="id",
+ source_idx="0",
+ )
await hass.async_block_till_done()
assert not hass.states.async_entity_ids(LIGHT_DOMAIN)
-async def test_lights(hass):
+async def test_lights(hass, mock_rtsp_event):
"""Test that lights are loaded properly."""
api_discovery = deepcopy(API_DISCOVERY_RESPONSE)
api_discovery["data"]["apiList"].append(API_DISCOVERY_LIGHT_CONTROL)
with patch.dict(API_DISCOVERY_RESPONSE, api_discovery):
- config_entry = await setup_axis_integration(hass)
- device = hass.data[AXIS_DOMAIN][config_entry.unique_id]
+ await setup_axis_integration(hass)
# Add light
with patch(
@@ -99,7 +87,13 @@ async def test_lights(hass):
"axis.light_control.LightControl.get_valid_intensity",
return_value={"data": {"ranges": [{"high": 150}]}},
):
- device.api.event.update([EVENT_ON])
+ mock_rtsp_event(
+ topic="tns1:Device/tnsaxis:Light/Status",
+ data_type="state",
+ data_value="ON",
+ source_name="id",
+ source_idx="0",
+ )
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 1
@@ -144,7 +138,13 @@ async def test_lights(hass):
mock_deactivate.assert_called_once()
# Event turn off light
- device.api.event.update([EVENT_OFF])
+ mock_rtsp_event(
+ topic="tns1:Device/tnsaxis:Light/Status",
+ data_type="state",
+ data_value="OFF",
+ source_name="id",
+ source_idx="0",
+ )
await hass.async_block_till_done()
light_0 = hass.states.get(entity_id)
diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py
index dcbe285cb54ce5..541c377d3ffbb1 100644
--- a/tests/components/axis/test_switch.py
+++ b/tests/components/axis/test_switch.py
@@ -21,25 +21,6 @@
setup_axis_integration,
)
-EVENTS = [
- {
- "operation": "Initialized",
- "topic": "tns1:Device/Trigger/Relay",
- "source": "RelayToken",
- "source_idx": "0",
- "type": "LogicalState",
- "value": "inactive",
- },
- {
- "operation": "Initialized",
- "topic": "tns1:Device/Trigger/Relay",
- "source": "RelayToken",
- "source_idx": "1",
- "type": "LogicalState",
- "value": "active",
- },
-]
-
async def test_platform_manually_configured(hass):
"""Test that nothing happens when platform is manually configured."""
@@ -57,7 +38,7 @@ async def test_no_switches(hass):
assert not hass.states.async_entity_ids(SWITCH_DOMAIN)
-async def test_switches_with_port_cgi(hass):
+async def test_switches_with_port_cgi(hass, mock_rtsp_event):
"""Test that switches are loaded properly using port.cgi."""
config_entry = await setup_axis_integration(hass)
device = hass.data[AXIS_DOMAIN][config_entry.unique_id]
@@ -68,7 +49,20 @@ async def test_switches_with_port_cgi(hass):
device.api.vapix.ports["0"].close = AsyncMock()
device.api.vapix.ports["1"].name = ""
- device.api.event.update(EVENTS)
+ mock_rtsp_event(
+ topic="tns1:Device/Trigger/Relay",
+ data_type="LogicalState",
+ data_value="inactive",
+ source_name="RelayToken",
+ source_idx="0",
+ )
+ mock_rtsp_event(
+ topic="tns1:Device/Trigger/Relay",
+ data_type="LogicalState",
+ data_value="active",
+ source_name="RelayToken",
+ source_idx="1",
+ )
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
@@ -100,7 +94,9 @@ async def test_switches_with_port_cgi(hass):
device.api.vapix.ports["0"].open.assert_called_once()
-async def test_switches_with_port_management(hass):
+async def test_switches_with_port_management(
+ hass, mock_axis_rtspclient, mock_rtsp_event
+):
"""Test that switches are loaded properly using port management."""
api_discovery = deepcopy(API_DISCOVERY_RESPONSE)
api_discovery["data"]["apiList"].append(API_DISCOVERY_PORT_MANAGEMENT)
@@ -115,7 +111,20 @@ async def test_switches_with_port_management(hass):
device.api.vapix.ports["0"].close = AsyncMock()
device.api.vapix.ports["1"].name = ""
- device.api.event.update(EVENTS)
+ mock_rtsp_event(
+ topic="tns1:Device/Trigger/Relay",
+ data_type="LogicalState",
+ data_value="inactive",
+ source_name="RelayToken",
+ source_idx="0",
+ )
+ mock_rtsp_event(
+ topic="tns1:Device/Trigger/Relay",
+ data_type="LogicalState",
+ data_value="active",
+ source_name="RelayToken",
+ source_idx="1",
+ )
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
diff --git a/tests/components/azure_devops/test_config_flow.py b/tests/components/azure_devops/test_config_flow.py
index 4cda9076b9847a..744d04042e0288 100644
--- a/tests/components/azure_devops/test_config_flow.py
+++ b/tests/components/azure_devops/test_config_flow.py
@@ -228,8 +228,6 @@ async def test_reauth_flow(hass: HomeAssistant) -> None:
async def test_full_flow_implementation(hass: HomeAssistant) -> None:
"""Test registering an integration and finishing flow works."""
with patch(
- "homeassistant.components.azure_devops.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.azure_devops.async_setup_entry",
return_value=True,
) as mock_setup_entry, patch(
@@ -255,7 +253,6 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None:
FIXTURE_USER_INPUT,
)
await hass.async_block_till_done()
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py
index 01f2664ea67d57..9c181e90deb165 100644
--- a/tests/components/bayesian/test_binary_sensor.py
+++ b/tests/components/bayesian/test_binary_sensor.py
@@ -119,7 +119,7 @@ async def test_sensor_numeric_state(hass):
state = hass.states.get("binary_sensor.test_binary")
assert [] == state.attributes.get("observations")
- assert 0.2 == state.attributes.get("probability")
+ assert state.attributes.get("probability") == 0.2
assert state.state == "off"
@@ -146,7 +146,7 @@ async def test_sensor_numeric_state(hass):
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test_binary")
- assert 0.2 == state.attributes.get("probability")
+ assert state.attributes.get("probability") == 0.2
assert state.state == "off"
@@ -186,7 +186,7 @@ async def test_sensor_state(hass):
state = hass.states.get("binary_sensor.test_binary")
assert [] == state.attributes.get("observations")
- assert 0.2 == state.attributes.get("probability")
+ assert state.attributes.get("probability") == 0.2
assert state.state == "off"
@@ -242,7 +242,7 @@ async def test_sensor_value_template(hass):
state = hass.states.get("binary_sensor.test_binary")
assert [] == state.attributes.get("observations")
- assert 0.2 == state.attributes.get("probability")
+ assert state.attributes.get("probability") == 0.2
assert state.state == "off"
@@ -339,7 +339,7 @@ async def test_multiple_observations(hass):
for key, attrs in state.attributes.items():
json.dumps(attrs)
assert [] == state.attributes.get("observations")
- assert 0.2 == state.attributes.get("probability")
+ assert state.attributes.get("probability") == 0.2
assert state.state == "off"
diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py
index f25b529d426fdb..d8c9e1ca894103 100644
--- a/tests/components/binary_sensor/test_device_condition.py
+++ b/tests/components/binary_sensor/test_device_condition.py
@@ -20,7 +20,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py
index fef109eb9d5437..9b50d52b785914 100644
--- a/tests/components/binary_sensor/test_device_trigger.py
+++ b/tests/components/binary_sensor/test_device_trigger.py
@@ -20,7 +20,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/binary_sensor/test_init.py b/tests/components/binary_sensor/test_init.py
index 99af954cd4d257..0c574df1569b45 100644
--- a/tests/components/binary_sensor/test_init.py
+++ b/tests/components/binary_sensor/test_init.py
@@ -8,17 +8,17 @@
def test_state():
"""Test binary sensor state."""
sensor = binary_sensor.BinarySensorEntity()
- assert STATE_OFF == sensor.state
+ assert sensor.state == STATE_OFF
with mock.patch(
"homeassistant.components.binary_sensor.BinarySensorEntity.is_on",
new=False,
):
- assert STATE_OFF == binary_sensor.BinarySensorEntity().state
+ assert binary_sensor.BinarySensorEntity().state == STATE_OFF
with mock.patch(
"homeassistant.components.binary_sensor.BinarySensorEntity.is_on",
new=True,
):
- assert STATE_ON == binary_sensor.BinarySensorEntity().state
+ assert binary_sensor.BinarySensorEntity().state == STATE_ON
def test_deprecated_base_class(caplog):
diff --git a/tests/components/binary_sensor/test_significant_change.py b/tests/components/binary_sensor/test_significant_change.py
new file mode 100644
index 00000000000000..673374a15e4b57
--- /dev/null
+++ b/tests/components/binary_sensor/test_significant_change.py
@@ -0,0 +1,20 @@
+"""Test the Binary Sensor significant change platform."""
+from homeassistant.components.binary_sensor.significant_change import (
+ async_check_significant_change,
+)
+
+
+async def test_significant_change():
+ """Detect Binary Sensor significant changes."""
+ old_attrs = {"attr_1": "value_1"}
+ new_attrs = {"attr_1": "value_2"}
+
+ assert (
+ async_check_significant_change(None, "on", old_attrs, "on", old_attrs) is False
+ )
+ assert (
+ async_check_significant_change(None, "on", old_attrs, "on", new_attrs) is False
+ )
+ assert (
+ async_check_significant_change(None, "on", old_attrs, "off", old_attrs) is True
+ )
diff --git a/tests/components/blackbird/test_media_player.py b/tests/components/blackbird/test_media_player.py
index 316ed681fa092a..73b40fdec9765d 100644
--- a/tests/components/blackbird/test_media_player.py
+++ b/tests/components/blackbird/test_media_player.py
@@ -219,9 +219,9 @@ def test_setup_platform(self, *args):
def test_setallzones_service_call_with_entity_id(self):
"""Test set all zone source service call with entity id."""
self.media_player.update()
- assert "Zone name" == self.media_player.name
- assert STATE_ON == self.media_player.state
- assert "one" == self.media_player.source
+ assert self.media_player.name == "Zone name"
+ assert self.media_player.state == STATE_ON
+ assert self.media_player.source == "one"
# Call set all zones service
self.hass.services.call(
@@ -232,16 +232,16 @@ def test_setallzones_service_call_with_entity_id(self):
)
# Check that source was changed
- assert 3 == self.blackbird.zones[3].av
+ assert self.blackbird.zones[3].av == 3
self.media_player.update()
- assert "three" == self.media_player.source
+ assert self.media_player.source == "three"
def test_setallzones_service_call_without_entity_id(self):
"""Test set all zone source service call without entity id."""
self.media_player.update()
- assert "Zone name" == self.media_player.name
- assert STATE_ON == self.media_player.state
- assert "one" == self.media_player.source
+ assert self.media_player.name == "Zone name"
+ assert self.media_player.state == STATE_ON
+ assert self.media_player.source == "one"
# Call set all zones service
self.hass.services.call(
@@ -249,9 +249,9 @@ def test_setallzones_service_call_without_entity_id(self):
)
# Check that source was changed
- assert 3 == self.blackbird.zones[3].av
+ assert self.blackbird.zones[3].av == 3
self.media_player.update()
- assert "three" == self.media_player.source
+ assert self.media_player.source == "three"
def test_update(self):
"""Test updating values from blackbird."""
@@ -260,23 +260,23 @@ def test_update(self):
self.media_player.update()
- assert STATE_ON == self.media_player.state
- assert "one" == self.media_player.source
+ assert self.media_player.state == STATE_ON
+ assert self.media_player.source == "one"
def test_name(self):
"""Test name property."""
- assert "Zone name" == self.media_player.name
+ assert self.media_player.name == "Zone name"
def test_state(self):
"""Test state property."""
assert self.media_player.state is None
self.media_player.update()
- assert STATE_ON == self.media_player.state
+ assert self.media_player.state == STATE_ON
self.blackbird.zones[3].power = False
self.media_player.update()
- assert STATE_OFF == self.media_player.state
+ assert self.media_player.state == STATE_OFF
def test_supported_features(self):
"""Test supported features property."""
@@ -289,54 +289,54 @@ def test_source(self):
"""Test source property."""
assert self.media_player.source is None
self.media_player.update()
- assert "one" == self.media_player.source
+ assert self.media_player.source == "one"
def test_media_title(self):
"""Test media title property."""
assert self.media_player.media_title is None
self.media_player.update()
- assert "one" == self.media_player.media_title
+ assert self.media_player.media_title == "one"
def test_source_list(self):
"""Test source list property."""
# Note, the list is sorted!
- assert ["one", "two", "three"] == self.media_player.source_list
+ assert self.media_player.source_list == ["one", "two", "three"]
def test_select_source(self):
"""Test source selection methods."""
self.media_player.update()
- assert "one" == self.media_player.source
+ assert self.media_player.source == "one"
self.media_player.select_source("two")
- assert 2 == self.blackbird.zones[3].av
+ assert self.blackbird.zones[3].av == 2
self.media_player.update()
- assert "two" == self.media_player.source
+ assert self.media_player.source == "two"
# Trying to set unknown source.
self.media_player.select_source("no name")
- assert 2 == self.blackbird.zones[3].av
+ assert self.blackbird.zones[3].av == 2
self.media_player.update()
- assert "two" == self.media_player.source
+ assert self.media_player.source == "two"
def test_turn_on(self):
"""Testing turning on the zone."""
self.blackbird.zones[3].power = False
self.media_player.update()
- assert STATE_OFF == self.media_player.state
+ assert self.media_player.state == STATE_OFF
self.media_player.turn_on()
assert self.blackbird.zones[3].power
self.media_player.update()
- assert STATE_ON == self.media_player.state
+ assert self.media_player.state == STATE_ON
def test_turn_off(self):
"""Testing turning off the zone."""
self.blackbird.zones[3].power = True
self.media_player.update()
- assert STATE_ON == self.media_player.state
+ assert self.media_player.state == STATE_ON
self.media_player.turn_off()
assert not self.blackbird.zones[3].power
self.media_player.update()
- assert STATE_OFF == self.media_player.state
+ assert self.media_player.state == STATE_OFF
diff --git a/tests/components/blebox/conftest.py b/tests/components/blebox/conftest.py
index 3b12c682f3feb0..a63a0090c3a3e4 100644
--- a/tests/components/blebox/conftest.py
+++ b/tests/components/blebox/conftest.py
@@ -7,10 +7,11 @@
from homeassistant.components.blebox.const import DOMAIN
from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
def patch_product_identify(path=None, **kwargs):
@@ -84,7 +85,7 @@ async def async_setup_entities(hass, config, entity_ids):
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
return [entity_registry.async_get(entity_id) for entity_id in entity_ids]
diff --git a/tests/components/blebox/test_air_quality.py b/tests/components/blebox/test_air_quality.py
index 4f1f6dff6717b4..8b5bc67d4bc0fe 100644
--- a/tests/components/blebox/test_air_quality.py
+++ b/tests/components/blebox/test_air_quality.py
@@ -1,5 +1,4 @@
"""Blebox air_quality tests."""
-
import logging
from unittest.mock import AsyncMock, PropertyMock
@@ -8,6 +7,7 @@
from homeassistant.components.air_quality import ATTR_PM_0_1, ATTR_PM_2_5, ATTR_PM_10
from homeassistant.const import ATTR_ICON, STATE_UNKNOWN
+from homeassistant.helpers import device_registry as dr
from .conftest import async_setup_entity, mock_feature
@@ -49,7 +49,7 @@ async def test_init(airsensor, hass, config):
assert state.state == STATE_UNKNOWN
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.name == "My air sensor"
diff --git a/tests/components/blebox/test_climate.py b/tests/components/blebox/test_climate.py
index baaa5a5009ee35..0b27846e654cae 100644
--- a/tests/components/blebox/test_climate.py
+++ b/tests/components/blebox/test_climate.py
@@ -1,5 +1,4 @@
"""BleBox climate entities tests."""
-
import logging
from unittest.mock import AsyncMock, PropertyMock
@@ -28,6 +27,7 @@
ATTR_TEMPERATURE,
STATE_UNKNOWN,
)
+from homeassistant.helpers import device_registry as dr
from .conftest import async_setup_entity, mock_feature
@@ -79,7 +79,7 @@ async def test_init(saunabox, hass, config):
assert state.state == STATE_UNKNOWN
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.name == "My sauna"
diff --git a/tests/components/blebox/test_cover.py b/tests/components/blebox/test_cover.py
index a5d3a8f705bad5..8355411a0bb0e7 100644
--- a/tests/components/blebox/test_cover.py
+++ b/tests/components/blebox/test_cover.py
@@ -1,5 +1,4 @@
"""BleBox cover entities tests."""
-
import logging
from unittest.mock import AsyncMock, PropertyMock
@@ -30,6 +29,7 @@
SERVICE_STOP_COVER,
STATE_UNKNOWN,
)
+from homeassistant.helpers import device_registry as dr
from .conftest import async_setup_entity, mock_feature
@@ -117,7 +117,7 @@ async def test_init_gatecontroller(gatecontroller, hass, config):
assert ATTR_CURRENT_POSITION not in state.attributes
assert state.state == STATE_UNKNOWN
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.name == "My gate controller"
@@ -147,7 +147,7 @@ async def test_init_shutterbox(shutterbox, hass, config):
assert ATTR_CURRENT_POSITION not in state.attributes
assert state.state == STATE_UNKNOWN
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.name == "My shutter"
@@ -179,7 +179,7 @@ async def test_init_gatebox(gatebox, hass, config):
assert ATTR_CURRENT_POSITION not in state.attributes
assert state.state == STATE_UNKNOWN
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.name == "My gatebox"
diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py
index 5d9e5709e4d0b2..6c8c26fe9383e6 100644
--- a/tests/components/blebox/test_light.py
+++ b/tests/components/blebox/test_light.py
@@ -1,5 +1,4 @@
"""BleBox light entities tests."""
-
import logging
from unittest.mock import AsyncMock, PropertyMock
@@ -21,6 +20,7 @@
STATE_OFF,
STATE_ON,
)
+from homeassistant.helpers import device_registry as dr
from homeassistant.util import color
from .conftest import async_setup_entity, mock_feature
@@ -65,7 +65,7 @@ async def test_dimmer_init(dimmer, hass, config):
assert state.attributes[ATTR_BRIGHTNESS] == 65
assert state.state == STATE_ON
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.name == "My dimmer"
@@ -236,7 +236,7 @@ async def test_wlightbox_s_init(wlightbox_s, hass, config):
assert ATTR_BRIGHTNESS not in state.attributes
assert state.state == STATE_OFF
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.name == "My wLightBoxS"
@@ -339,7 +339,7 @@ async def test_wlightbox_init(wlightbox, hass, config):
assert ATTR_BRIGHTNESS not in state.attributes
assert state.state == STATE_OFF
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.name == "My wLightBox"
diff --git a/tests/components/blebox/test_sensor.py b/tests/components/blebox/test_sensor.py
index aeb726cc726d05..2281c4ea68cde2 100644
--- a/tests/components/blebox/test_sensor.py
+++ b/tests/components/blebox/test_sensor.py
@@ -1,5 +1,4 @@
"""Blebox sensors tests."""
-
import logging
from unittest.mock import AsyncMock, PropertyMock
@@ -13,6 +12,7 @@
STATE_UNKNOWN,
TEMP_CELSIUS,
)
+from homeassistant.helpers import device_registry as dr
from .conftest import async_setup_entity, mock_feature
@@ -49,7 +49,7 @@ async def test_init(tempsensor, hass, config):
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
assert state.state == STATE_UNKNOWN
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.name == "My temperature sensor"
diff --git a/tests/components/blebox/test_switch.py b/tests/components/blebox/test_switch.py
index e2bc1240510a17..e67c0479cb3f7d 100644
--- a/tests/components/blebox/test_switch.py
+++ b/tests/components/blebox/test_switch.py
@@ -1,5 +1,4 @@
"""Blebox switch tests."""
-
import logging
from unittest.mock import AsyncMock, PropertyMock
@@ -14,6 +13,7 @@
STATE_OFF,
STATE_ON,
)
+from homeassistant.helpers import device_registry as dr
from .conftest import (
async_setup_entities,
@@ -58,7 +58,7 @@ async def test_switchbox_init(switchbox, hass, config):
assert state.state == STATE_OFF
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.name == "My switch box"
@@ -204,7 +204,7 @@ async def test_switchbox_d_init(switchbox_d, hass, config):
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_SWITCH
assert state.state == STATE_OFF # NOTE: should instead be STATE_UNKNOWN?
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.name == "My relays"
@@ -221,7 +221,7 @@ async def test_switchbox_d_init(switchbox_d, hass, config):
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_SWITCH
assert state.state == STATE_OFF # NOTE: should instead be STATE_UNKNOWN?
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.name == "My relays"
diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py
index 91264997769827..2d1746b87ba3d8 100644
--- a/tests/components/blink/test_config_flow.py
+++ b/tests/components/blink/test_config_flow.py
@@ -23,8 +23,6 @@ async def test_form(hass):
"homeassistant.components.blink.config_flow.Auth.check_key_required",
return_value=False,
), patch(
- "homeassistant.components.blink.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.blink.async_setup_entry",
return_value=True,
) as mock_setup_entry:
@@ -47,7 +45,6 @@ async def test_form(hass):
"client_id": None,
"region_id": None,
}
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
@@ -61,9 +58,7 @@ async def test_form_2fa(hass):
with patch("homeassistant.components.blink.config_flow.Auth.startup"), patch(
"homeassistant.components.blink.config_flow.Auth.check_key_required",
return_value=True,
- ), patch(
- "homeassistant.components.blink.async_setup", return_value=True
- ) as mock_setup:
+ ):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"username": "blink@example.com", "password": "example"},
@@ -82,8 +77,6 @@ async def test_form_2fa(hass):
"homeassistant.components.blink.config_flow.Blink.setup_urls",
return_value=True,
), patch(
- "homeassistant.components.blink.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.blink.async_setup_entry", return_value=True
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
@@ -94,7 +87,6 @@ async def test_form_2fa(hass):
assert result3["type"] == "create_entry"
assert result3["title"] == "blink"
assert result3["result"].unique_id == "blink@example.com"
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
@@ -108,7 +100,7 @@ async def test_form_2fa_connect_error(hass):
with patch("homeassistant.components.blink.config_flow.Auth.startup"), patch(
"homeassistant.components.blink.config_flow.Auth.check_key_required",
return_value=True,
- ), patch("homeassistant.components.blink.async_setup", return_value=True):
+ ):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"username": "blink@example.com", "password": "example"},
@@ -126,8 +118,6 @@ async def test_form_2fa_connect_error(hass):
), patch(
"homeassistant.components.blink.config_flow.Blink.setup_urls",
side_effect=BlinkSetupError,
- ), patch(
- "homeassistant.components.blink.async_setup", return_value=True
), patch(
"homeassistant.components.blink.async_setup_entry", return_value=True
):
@@ -149,7 +139,7 @@ async def test_form_2fa_invalid_key(hass):
with patch("homeassistant.components.blink.config_flow.Auth.startup"), patch(
"homeassistant.components.blink.config_flow.Auth.check_key_required",
return_value=True,
- ), patch("homeassistant.components.blink.async_setup", return_value=True):
+ ):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"username": "blink@example.com", "password": "example"},
@@ -167,8 +157,6 @@ async def test_form_2fa_invalid_key(hass):
), patch(
"homeassistant.components.blink.config_flow.Blink.setup_urls",
return_value=True,
- ), patch(
- "homeassistant.components.blink.async_setup", return_value=True
), patch(
"homeassistant.components.blink.async_setup_entry", return_value=True
):
@@ -190,7 +178,7 @@ async def test_form_2fa_unknown_error(hass):
with patch("homeassistant.components.blink.config_flow.Auth.startup"), patch(
"homeassistant.components.blink.config_flow.Auth.check_key_required",
return_value=True,
- ), patch("homeassistant.components.blink.async_setup", return_value=True):
+ ):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"username": "blink@example.com", "password": "example"},
@@ -208,8 +196,6 @@ async def test_form_2fa_unknown_error(hass):
), patch(
"homeassistant.components.blink.config_flow.Blink.setup_urls",
side_effect=KeyError,
- ), patch(
- "homeassistant.components.blink.async_setup", return_value=True
), patch(
"homeassistant.components.blink.async_setup_entry", return_value=True
):
@@ -273,7 +259,7 @@ async def test_options_flow(hass):
data={"username": "blink@example.com", "password": "example"},
options={},
entry_id=1,
- version=2,
+ version=3,
)
config_entry.add_to_hass(hass)
diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py
index 52433f2f58f943..d56978deb2707f 100644
--- a/tests/components/bmw_connected_drive/test_config_flow.py
+++ b/tests/components/bmw_connected_drive/test_config_flow.py
@@ -5,10 +5,9 @@
from homeassistant.components.bmw_connected_drive.config_flow import DOMAIN
from homeassistant.components.bmw_connected_drive.const import (
CONF_READ_ONLY,
- CONF_REGION,
CONF_USE_LOCATION,
)
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
from tests.common import MockConfigEntry
diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py
index 9aaaf9a249d0d4..0791d002fed77a 100644
--- a/tests/components/bond/common.py
+++ b/tests/components/bond/common.py
@@ -1,9 +1,11 @@
"""Common methods used across tests for Bond."""
+from __future__ import annotations
+
from asyncio import TimeoutError as AsyncIOTimeoutError
from contextlib import nullcontext
from datetime import timedelta
-from typing import Any, Dict, Optional
-from unittest.mock import patch
+from typing import Any
+from unittest.mock import MagicMock, patch
from homeassistant import core
from homeassistant.components.bond.const import DOMAIN as BOND_DOMAIN
@@ -29,13 +31,19 @@ async def setup_bond_entity(
patch_version=False,
patch_device_ids=False,
patch_platforms=False,
+ patch_bridge=False,
+ patch_token=False,
):
"""Set up Bond entity."""
config_entry.add_to_hass(hass)
- with patch_bond_version(enabled=patch_version), patch_bond_device_ids(
+ with patch_start_bpup(), patch_bond_bridge(enabled=patch_bridge), patch_bond_token(
+ enabled=patch_token
+ ), patch_bond_version(enabled=patch_version), patch_bond_device_ids(
enabled=patch_device_ids
- ), patch_setup_entry("cover", enabled=patch_platforms), patch_setup_entry(
+ ), patch_setup_entry(
+ "cover", enabled=patch_platforms
+ ), patch_setup_entry(
"fan", enabled=patch_platforms
), patch_setup_entry(
"light", enabled=patch_platforms
@@ -48,12 +56,14 @@ async def setup_bond_entity(
async def setup_platform(
hass: core.HomeAssistant,
platform: str,
- discovered_device: Dict[str, Any],
+ discovered_device: dict[str, Any],
*,
bond_device_id: str = "bond-device-id",
- bond_version: Dict[str, Any] = None,
- props: Dict[str, Any] = None,
- state: Dict[str, Any] = None,
+ bond_version: dict[str, Any] = None,
+ props: dict[str, Any] = None,
+ state: dict[str, Any] = None,
+ bridge: dict[str, Any] = None,
+ token: dict[str, Any] = None,
):
"""Set up the specified Bond platform."""
mock_entry = MockConfigEntry(
@@ -62,24 +72,29 @@ async def setup_platform(
)
mock_entry.add_to_hass(hass)
- with patch("homeassistant.components.bond.PLATFORMS", [platform]):
- with patch_bond_version(return_value=bond_version), patch_bond_device_ids(
- return_value=[bond_device_id]
- ), patch_bond_device(
- return_value=discovered_device
- ), patch_bond_device_properties(
- return_value=props
- ), patch_bond_device_state(
- return_value=state
- ):
- assert await async_setup_component(hass, BOND_DOMAIN, {})
- await hass.async_block_till_done()
+ with patch(
+ "homeassistant.components.bond.PLATFORMS", [platform]
+ ), patch_bond_version(return_value=bond_version), patch_bond_bridge(
+ return_value=bridge
+ ), patch_bond_token(
+ return_value=token
+ ), patch_bond_device_ids(
+ return_value=[bond_device_id]
+ ), patch_start_bpup(), patch_bond_device(
+ return_value=discovered_device
+ ), patch_bond_device_properties(
+ return_value=props
+ ), patch_bond_device_state(
+ return_value=state
+ ):
+ assert await async_setup_component(hass, BOND_DOMAIN, {})
+ await hass.async_block_till_done()
return mock_entry
def patch_bond_version(
- enabled: bool = True, return_value: Optional[dict] = None, side_effect=None
+ enabled: bool = True, return_value: dict | None = None, side_effect=None
):
"""Patch Bond API version endpoint."""
if not enabled:
@@ -95,6 +110,44 @@ def patch_bond_version(
)
+def patch_bond_bridge(
+ enabled: bool = True, return_value: dict | None = None, side_effect=None
+):
+ """Patch Bond API bridge endpoint."""
+ if not enabled:
+ return nullcontext()
+
+ if return_value is None:
+ return_value = {
+ "name": "bond-name",
+ "location": "bond-location",
+ "bluelight": 127,
+ }
+
+ return patch(
+ "homeassistant.components.bond.Bond.bridge",
+ return_value=return_value,
+ side_effect=side_effect,
+ )
+
+
+def patch_bond_token(
+ enabled: bool = True, return_value: dict | None = None, side_effect=None
+):
+ """Patch Bond API token endpoint."""
+ if not enabled:
+ return nullcontext()
+
+ if return_value is None:
+ return_value = {"locked": 1}
+
+ return patch(
+ "homeassistant.components.bond.Bond.token",
+ return_value=return_value,
+ side_effect=side_effect,
+ )
+
+
def patch_bond_device_ids(enabled: bool = True, return_value=None, side_effect=None):
"""Patch Bond API devices endpoint."""
if not enabled:
@@ -118,6 +171,14 @@ def patch_bond_device(return_value=None):
)
+def patch_start_bpup():
+ """Patch start_bpup."""
+ return patch(
+ "homeassistant.components.bond.start_bpup",
+ return_value=MagicMock(),
+ )
+
+
def patch_bond_action():
"""Patch Bond API action endpoint."""
return patch("homeassistant.components.bond.Bond.action")
@@ -147,7 +208,7 @@ def patch_bond_device_state(return_value=None, side_effect=None):
async def help_test_entity_available(
- hass: core.HomeAssistant, domain: str, device: Dict[str, Any], entity_id: str
+ hass: core.HomeAssistant, domain: str, device: dict[str, Any], entity_id: str
):
"""Run common test to verify available property."""
await setup_platform(hass, domain, device)
diff --git a/tests/components/bond/conftest.py b/tests/components/bond/conftest.py
index 7ab1805cb73416..378c3340e2940d 100644
--- a/tests/components/bond/conftest.py
+++ b/tests/components/bond/conftest.py
@@ -1,2 +1,2 @@
"""bond conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py
index dba6c5906412ed..89c183ec1ba086 100644
--- a/tests/components/bond/test_config_flow.py
+++ b/tests/components/bond/test_config_flow.py
@@ -1,5 +1,7 @@
"""Test the Bond config flow."""
-from typing import Any, Dict
+from __future__ import annotations
+
+from typing import Any
from unittest.mock import Mock, patch
from aiohttp import ClientConnectionError, ClientResponseError
@@ -8,7 +10,14 @@
from homeassistant.components.bond.const import DOMAIN
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
-from .common import patch_bond_device_ids, patch_bond_version
+from .common import (
+ patch_bond_bridge,
+ patch_bond_device,
+ patch_bond_device_ids,
+ patch_bond_device_properties,
+ patch_bond_token,
+ patch_bond_version,
+)
from tests.common import MockConfigEntry
@@ -24,7 +33,44 @@ async def test_user_form(hass: core.HomeAssistant):
with patch_bond_version(
return_value={"bondid": "test-bond-id"}
- ), patch_bond_device_ids(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry:
+ ), patch_bond_device_ids(
+ return_value=["f6776c11", "f6776c12"]
+ ), patch_bond_bridge(), patch_bond_device_properties(), patch_bond_device(), _patch_async_setup_entry() as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"},
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "bond-name"
+ assert result2["data"] == {
+ CONF_HOST: "some host",
+ CONF_ACCESS_TOKEN: "test-token",
+ }
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_user_form_with_non_bridge(hass: core.HomeAssistant):
+ """Test setup a smart by bond fan."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch_bond_version(
+ return_value={"bondid": "test-bond-id"}
+ ), patch_bond_device_ids(
+ return_value=["f6776c11"]
+ ), patch_bond_device_properties(), patch_bond_device(
+ return_value={
+ "name": "New Fan",
+ }
+ ), patch_bond_bridge(
+ return_value={}
+ ), _patch_async_setup_entry() as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"},
@@ -32,12 +78,11 @@ async def test_user_form(hass: core.HomeAssistant):
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
- assert result2["title"] == "test-bond-id"
+ assert result2["title"] == "New Fan"
assert result2["data"] == {
CONF_HOST: "some host",
CONF_ACCESS_TOKEN: "test-token",
}
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
@@ -49,7 +94,7 @@ async def test_user_form_invalid_auth(hass: core.HomeAssistant):
with patch_bond_version(
return_value={"bond_id": "test-bond-id"}
- ), patch_bond_device_ids(
+ ), patch_bond_bridge(), patch_bond_device_ids(
side_effect=ClientResponseError(Mock(), Mock(), status=401),
):
result2 = await hass.config_entries.flow.async_configure(
@@ -69,7 +114,7 @@ async def test_user_form_cannot_connect(hass: core.HomeAssistant):
with patch_bond_version(
side_effect=ClientConnectionError()
- ), patch_bond_device_ids():
+ ), patch_bond_bridge(), patch_bond_device_ids():
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"},
@@ -87,7 +132,7 @@ async def test_user_form_old_firmware(hass: core.HomeAssistant):
with patch_bond_version(
return_value={"no_bond_id": "present"}
- ), patch_bond_device_ids():
+ ), patch_bond_bridge(), patch_bond_device_ids():
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"},
@@ -133,7 +178,7 @@ async def test_user_form_one_entry_per_device_allowed(hass: core.HomeAssistant):
with patch_bond_version(
return_value={"bondid": "already-registered-bond-id"}
- ), patch_bond_device_ids(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry:
+ ), patch_bond_bridge(), patch_bond_device_ids(), _patch_async_setup_entry() as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"},
@@ -143,7 +188,6 @@ async def test_user_form_one_entry_per_device_allowed(hass: core.HomeAssistant):
assert result2["reason"] == "already_configured"
await hass.async_block_till_done()
- assert len(mock_setup.mock_calls) == 0
assert len(mock_setup_entry.mock_calls) == 0
@@ -160,7 +204,36 @@ async def test_zeroconf_form(hass: core.HomeAssistant):
with patch_bond_version(
return_value={"bondid": "test-bond-id"}
- ), patch_bond_device_ids(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry:
+ ), patch_bond_bridge(), patch_bond_device_ids(), _patch_async_setup_entry() as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_ACCESS_TOKEN: "test-token"},
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "bond-name"
+ assert result2["data"] == {
+ CONF_HOST: "test-host",
+ CONF_ACCESS_TOKEN: "test-token",
+ }
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_zeroconf_form_token_unavailable(hass: core.HomeAssistant):
+ """Test we get the discovery form and we handle the token being unavailable."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch_bond_version(), patch_bond_token():
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_ZEROCONF},
+ data={"name": "test-bond-id.some-other-tail-info", "host": "test-host"},
+ )
+ await hass.async_block_till_done()
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch_bond_version(), patch_bond_bridge(), patch_bond_device_ids(), _patch_async_setup_entry() as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_ACCESS_TOKEN: "test-token"},
@@ -168,12 +241,44 @@ async def test_zeroconf_form(hass: core.HomeAssistant):
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
- assert result2["title"] == "test-bond-id"
+ assert result2["title"] == "bond-name"
assert result2["data"] == {
CONF_HOST: "test-host",
CONF_ACCESS_TOKEN: "test-token",
}
- assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant):
+ """Test we get the discovery form when we can get the token."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch_bond_version(return_value={"bondid": "test-bond-id"}), patch_bond_token(
+ return_value={"token": "discovered-token"}
+ ), patch_bond_bridge(
+ return_value={"name": "discovered-name"}
+ ), patch_bond_device_ids():
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_ZEROCONF},
+ data={"name": "test-bond-id.some-other-tail-info", "host": "test-host"},
+ )
+ await hass.async_block_till_done()
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with _patch_async_setup_entry() as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "discovered-name"
+ assert result2["data"] == {
+ CONF_HOST: "test-host",
+ CONF_ACCESS_TOKEN: "discovered-token",
+ }
assert len(mock_setup_entry.mock_calls) == 1
@@ -188,7 +293,7 @@ async def test_zeroconf_already_configured(hass: core.HomeAssistant):
)
entry.add_to_hass(hass)
- with _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry:
+ with _patch_async_setup_entry() as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
@@ -203,7 +308,6 @@ async def test_zeroconf_already_configured(hass: core.HomeAssistant):
assert entry.data["host"] == "updated-host"
await hass.async_block_till_done()
- assert len(mock_setup.mock_calls) == 0
assert len(mock_setup_entry.mock_calls) == 0
@@ -225,8 +329,8 @@ async def _help_test_form_unexpected_error(
hass: core.HomeAssistant,
*,
source: str,
- initial_input: Dict[str, Any] = None,
- user_input: Dict[str, Any],
+ initial_input: dict[str, Any] = None,
+ user_input: dict[str, Any],
error: Exception,
):
"""Test we handle unexpected error gracefully."""
@@ -245,10 +349,6 @@ async def _help_test_form_unexpected_error(
assert result2["errors"] == {"base": "unknown"}
-def _patch_async_setup():
- return patch("homeassistant.components.bond.async_setup", return_value=True)
-
-
def _patch_async_setup_entry():
return patch(
"homeassistant.components.bond.async_setup_entry",
diff --git a/tests/components/bond/test_cover.py b/tests/components/bond/test_cover.py
index dbb8ee0f3b73f6..f516d84d50a634 100644
--- a/tests/components/bond/test_cover.py
+++ b/tests/components/bond/test_cover.py
@@ -11,6 +11,7 @@
SERVICE_OPEN_COVER,
SERVICE_STOP_COVER,
)
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_registry import EntityRegistry
from homeassistant.util import utcnow
@@ -39,7 +40,7 @@ async def test_entity_registry(hass: core.HomeAssistant):
bond_device_id="test-device-id",
)
- registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry()
+ registry: EntityRegistry = er.async_get(hass)
entity = registry.entities["cover.name_1"]
assert entity.unique_id == "test-hub-id_test-device-id"
diff --git a/tests/components/bond/test_entity.py b/tests/components/bond/test_entity.py
new file mode 100644
index 00000000000000..e0a3f156ff51ae
--- /dev/null
+++ b/tests/components/bond/test_entity.py
@@ -0,0 +1,169 @@
+"""Tests for the Bond entities."""
+import asyncio
+from datetime import timedelta
+from unittest.mock import patch
+
+from bond_api import BPUPSubscriptions, DeviceType
+
+from homeassistant import core
+from homeassistant.components import fan
+from homeassistant.components.fan import DOMAIN as FAN_DOMAIN
+from homeassistant.const import STATE_ON, STATE_UNAVAILABLE
+from homeassistant.util import utcnow
+
+from .common import patch_bond_device_state, setup_platform
+
+from tests.common import async_fire_time_changed
+
+
+def ceiling_fan(name: str):
+ """Create a ceiling fan with given name."""
+ return {
+ "name": name,
+ "type": DeviceType.CEILING_FAN,
+ "actions": ["SetSpeed", "SetDirection"],
+ }
+
+
+async def test_bpup_goes_offline_and_recovers_same_entity(hass: core.HomeAssistant):
+ """Test that push updates fail and we fallback to polling and then bpup recovers.
+
+ The BPUP recovery is triggered by an update for the entity and
+ we do not fallback to polling because state is in sync.
+ """
+ bpup_subs = BPUPSubscriptions()
+ with patch(
+ "homeassistant.components.bond.BPUPSubscriptions",
+ return_value=bpup_subs,
+ ):
+ await setup_platform(
+ hass, FAN_DOMAIN, ceiling_fan("name-1"), bond_device_id="test-device-id"
+ )
+
+ bpup_subs.notify(
+ {
+ "s": 200,
+ "t": "bond/test-device-id/update",
+ "b": {"power": 1, "speed": 3, "direction": 0},
+ }
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 100
+
+ bpup_subs.notify(
+ {
+ "s": 200,
+ "t": "bond/test-device-id/update",
+ "b": {"power": 1, "speed": 1, "direction": 0},
+ }
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 33
+
+ bpup_subs.last_message_time = 0
+ with patch_bond_device_state(side_effect=asyncio.TimeoutError):
+ async_fire_time_changed(hass, utcnow() + timedelta(seconds=230))
+ await hass.async_block_till_done()
+
+ assert hass.states.get("fan.name_1").state == STATE_UNAVAILABLE
+
+ # Ensure we do not poll to get the state
+ # since bpup has recovered and we know we
+ # are back in sync
+ with patch_bond_device_state(side_effect=Exception):
+ bpup_subs.notify(
+ {
+ "s": 200,
+ "t": "bond/test-device-id/update",
+ "b": {"power": 1, "speed": 2, "direction": 0},
+ }
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("fan.name_1")
+ assert state.state == STATE_ON
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 66
+
+
+async def test_bpup_goes_offline_and_recovers_different_entity(
+ hass: core.HomeAssistant,
+):
+ """Test that push updates fail and we fallback to polling and then bpup recovers.
+
+ The BPUP recovery is triggered by an update for a different entity which
+ forces a poll since we need to re-get the state.
+ """
+ bpup_subs = BPUPSubscriptions()
+ with patch(
+ "homeassistant.components.bond.BPUPSubscriptions",
+ return_value=bpup_subs,
+ ):
+ await setup_platform(
+ hass, FAN_DOMAIN, ceiling_fan("name-1"), bond_device_id="test-device-id"
+ )
+
+ bpup_subs.notify(
+ {
+ "s": 200,
+ "t": "bond/test-device-id/update",
+ "b": {"power": 1, "speed": 3, "direction": 0},
+ }
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 100
+
+ bpup_subs.notify(
+ {
+ "s": 200,
+ "t": "bond/test-device-id/update",
+ "b": {"power": 1, "speed": 1, "direction": 0},
+ }
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 33
+
+ bpup_subs.last_message_time = 0
+ with patch_bond_device_state(side_effect=asyncio.TimeoutError):
+ async_fire_time_changed(hass, utcnow() + timedelta(seconds=230))
+ await hass.async_block_till_done()
+
+ assert hass.states.get("fan.name_1").state == STATE_UNAVAILABLE
+
+ bpup_subs.notify(
+ {
+ "s": 200,
+ "t": "bond/not-this-device-id/update",
+ "b": {"power": 1, "speed": 2, "direction": 0},
+ }
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("fan.name_1").state == STATE_UNAVAILABLE
+
+ with patch_bond_device_state(return_value={"power": 1, "speed": 1}):
+ async_fire_time_changed(hass, utcnow() + timedelta(seconds=430))
+ await hass.async_block_till_done()
+
+ state = hass.states.get("fan.name_1")
+ assert state.state == STATE_ON
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 33
+
+
+async def test_polling_fails_and_recovers(hass: core.HomeAssistant):
+ """Test that polling fails and we recover."""
+ await setup_platform(
+ hass, FAN_DOMAIN, ceiling_fan("name-1"), bond_device_id="test-device-id"
+ )
+
+ with patch_bond_device_state(side_effect=asyncio.TimeoutError):
+ async_fire_time_changed(hass, utcnow() + timedelta(seconds=230))
+ await hass.async_block_till_done()
+
+ assert hass.states.get("fan.name_1").state == STATE_UNAVAILABLE
+
+ with patch_bond_device_state(return_value={"power": 1, "speed": 1}):
+ async_fire_time_changed(hass, utcnow() + timedelta(seconds=230))
+ await hass.async_block_till_done()
+
+ state = hass.states.get("fan.name_1")
+ assert state.state == STATE_ON
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 33
diff --git a/tests/components/bond/test_fan.py b/tests/components/bond/test_fan.py
index 49a6e4a5b686e5..bd5994f51824e4 100644
--- a/tests/components/bond/test_fan.py
+++ b/tests/components/bond/test_fan.py
@@ -1,6 +1,7 @@
"""Tests for the Bond fan device."""
+from __future__ import annotations
+
from datetime import timedelta
-from typing import Optional
from bond_api import Action, DeviceType, Direction
@@ -18,6 +19,7 @@
SPEED_OFF,
)
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_registry import EntityRegistry
from homeassistant.util import utcnow
@@ -43,8 +45,8 @@ def ceiling_fan(name: str):
async def turn_fan_on(
hass: core.HomeAssistant,
fan_id: str,
- speed: Optional[str] = None,
- percentage: Optional[int] = None,
+ speed: str | None = None,
+ percentage: int | None = None,
) -> None:
"""Turn the fan on at the specified speed."""
service_data = {ATTR_ENTITY_ID: fan_id}
@@ -71,7 +73,7 @@ async def test_entity_registry(hass: core.HomeAssistant):
bond_device_id="test-device-id",
)
- registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry()
+ registry: EntityRegistry = er.async_get(hass)
entity = registry.entities["fan.name_1"]
assert entity.unique_id == "test-hub-id_test-device-id"
diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py
index 98d86058c49d5c..0bba04b4d97ceb 100644
--- a/tests/components/bond/test_init.py
+++ b/tests/components/bond/test_init.py
@@ -1,5 +1,8 @@
"""Tests for the Bond module."""
-from aiohttp import ClientConnectionError
+from unittest.mock import Mock
+
+from aiohttp import ClientConnectionError, ClientResponseError
+from bond_api import DeviceType
from homeassistant.components.bond.const import DOMAIN
from homeassistant.config_entries import (
@@ -12,7 +15,17 @@
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
-from .common import patch_bond_version, patch_setup_entry, setup_bond_entity
+from .common import (
+ patch_bond_bridge,
+ patch_bond_device,
+ patch_bond_device_ids,
+ patch_bond_device_properties,
+ patch_bond_device_state,
+ patch_bond_version,
+ patch_setup_entry,
+ patch_start_bpup,
+ setup_bond_entity,
+)
from tests.common import MockConfigEntry
@@ -44,34 +57,31 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss
data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"},
)
- with patch_bond_version(
+ with patch_bond_bridge(), patch_bond_version(
return_value={
"bondid": "test-bond-id",
"target": "test-model",
"fw_ver": "test-version",
}
- ):
- with patch_setup_entry(
- "cover"
- ) as mock_cover_async_setup_entry, patch_setup_entry(
- "fan"
- ) as mock_fan_async_setup_entry, patch_setup_entry(
- "light"
- ) as mock_light_async_setup_entry, patch_setup_entry(
- "switch"
- ) as mock_switch_async_setup_entry:
- result = await setup_bond_entity(hass, config_entry, patch_device_ids=True)
- assert result is True
- await hass.async_block_till_done()
+ ), patch_setup_entry("cover") as mock_cover_async_setup_entry, patch_setup_entry(
+ "fan"
+ ) as mock_fan_async_setup_entry, patch_setup_entry(
+ "light"
+ ) as mock_light_async_setup_entry, patch_setup_entry(
+ "switch"
+ ) as mock_switch_async_setup_entry:
+ result = await setup_bond_entity(hass, config_entry, patch_device_ids=True)
+ assert result is True
+ await hass.async_block_till_done()
assert config_entry.entry_id in hass.data[DOMAIN]
assert config_entry.state == ENTRY_STATE_LOADED
assert config_entry.unique_id == "test-bond-id"
# verify hub device is registered correctly
- device_registry = await dr.async_get_registry(hass)
+ device_registry = dr.async_get(hass)
hub = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")})
- assert hub.name == "test-bond-id"
+ assert hub.name == "bond-name"
assert hub.manufacturer == "Olibra"
assert hub.model == "test-model"
assert hub.sw_version == "test-version"
@@ -96,6 +106,7 @@ async def test_unload_config_entry(hass: HomeAssistant):
patch_version=True,
patch_device_ids=True,
patch_platforms=True,
+ patch_bridge=True,
)
assert result is True
await hass.async_block_till_done()
@@ -105,3 +116,141 @@ async def test_unload_config_entry(hass: HomeAssistant):
assert config_entry.entry_id not in hass.data[DOMAIN]
assert config_entry.state == ENTRY_STATE_NOT_LOADED
+
+
+async def test_old_identifiers_are_removed(hass: HomeAssistant):
+ """Test we remove the old non-unique identifiers."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"},
+ )
+
+ old_identifers = (DOMAIN, "device_id")
+ new_identifiers = (DOMAIN, "test-bond-id", "device_id")
+ device_registry = dr.async_get(hass)
+ device_registry.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ identifiers={old_identifers},
+ manufacturer="any",
+ name="old",
+ )
+
+ config_entry.add_to_hass(hass)
+
+ with patch_bond_bridge(), patch_bond_version(
+ return_value={
+ "bondid": "test-bond-id",
+ "target": "test-model",
+ "fw_ver": "test-version",
+ }
+ ), patch_start_bpup(), patch_bond_device_ids(
+ return_value=["bond-device-id", "device_id"]
+ ), patch_bond_device(
+ return_value={
+ "name": "test1",
+ "type": DeviceType.GENERIC_DEVICE,
+ }
+ ), patch_bond_device_properties(
+ return_value={}
+ ), patch_bond_device_state(
+ return_value={}
+ ):
+ assert await hass.config_entries.async_setup(config_entry.entry_id) is True
+ await hass.async_block_till_done()
+
+ assert config_entry.entry_id in hass.data[DOMAIN]
+ assert config_entry.state == ENTRY_STATE_LOADED
+ assert config_entry.unique_id == "test-bond-id"
+
+ # verify the device info is cleaned up
+ assert device_registry.async_get_device(identifiers={old_identifers}) is None
+ assert device_registry.async_get_device(identifiers={new_identifiers}) is not None
+
+
+async def test_smart_by_bond_device_suggested_area(hass: HomeAssistant):
+ """Test we can setup a smart by bond device and get the suggested area."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"},
+ )
+
+ config_entry.add_to_hass(hass)
+
+ with patch_bond_bridge(
+ side_effect=ClientResponseError(Mock(), Mock(), status=404)
+ ), patch_bond_version(
+ return_value={
+ "bondid": "test-bond-id",
+ "target": "test-model",
+ "fw_ver": "test-version",
+ }
+ ), patch_start_bpup(), patch_bond_device_ids(
+ return_value=["bond-device-id", "device_id"]
+ ), patch_bond_device(
+ return_value={
+ "name": "test1",
+ "type": DeviceType.GENERIC_DEVICE,
+ "location": "Den",
+ }
+ ), patch_bond_device_properties(
+ return_value={}
+ ), patch_bond_device_state(
+ return_value={}
+ ):
+ assert await hass.config_entries.async_setup(config_entry.entry_id) is True
+ await hass.async_block_till_done()
+
+ assert config_entry.entry_id in hass.data[DOMAIN]
+ assert config_entry.state == ENTRY_STATE_LOADED
+ assert config_entry.unique_id == "test-bond-id"
+
+ device_registry = dr.async_get(hass)
+ device = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")})
+ assert device is not None
+ assert device.suggested_area == "Den"
+
+
+async def test_bridge_device_suggested_area(hass: HomeAssistant):
+ """Test we can setup a bridge bond device and get the suggested area."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"},
+ )
+
+ config_entry.add_to_hass(hass)
+
+ with patch_bond_bridge(
+ return_value={
+ "name": "Office Bridge",
+ "location": "Office",
+ }
+ ), patch_bond_version(
+ return_value={
+ "bondid": "test-bond-id",
+ "target": "test-model",
+ "fw_ver": "test-version",
+ }
+ ), patch_start_bpup(), patch_bond_device_ids(
+ return_value=["bond-device-id", "device_id"]
+ ), patch_bond_device(
+ return_value={
+ "name": "test1",
+ "type": DeviceType.GENERIC_DEVICE,
+ "location": "Bathroom",
+ }
+ ), patch_bond_device_properties(
+ return_value={}
+ ), patch_bond_device_state(
+ return_value={}
+ ):
+ assert await hass.config_entries.async_setup(config_entry.entry_id) is True
+ await hass.async_block_till_done()
+
+ assert config_entry.entry_id in hass.data[DOMAIN]
+ assert config_entry.state == ENTRY_STATE_LOADED
+ assert config_entry.unique_id == "test-bond-id"
+
+ device_registry = dr.async_get(hass)
+ device = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")})
+ assert device is not None
+ assert device.suggested_area == "Office"
diff --git a/tests/components/bond/test_light.py b/tests/components/bond/test_light.py
index 6d871187e26e11..e59efcd7bcff2a 100644
--- a/tests/components/bond/test_light.py
+++ b/tests/components/bond/test_light.py
@@ -16,6 +16,7 @@
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_registry import EntityRegistry
from homeassistant.util import utcnow
@@ -29,6 +30,15 @@
from tests.common import async_fire_time_changed
+def light(name: str):
+ """Create a light with a given name."""
+ return {
+ "name": name,
+ "type": DeviceType.LIGHT,
+ "actions": [Action.TURN_LIGHT_ON, Action.TURN_LIGHT_OFF, Action.SET_BRIGHTNESS],
+ }
+
+
def ceiling_fan(name: str):
"""Create a ceiling fan (that has built-in light) with given name."""
return {
@@ -47,6 +57,24 @@ def dimmable_ceiling_fan(name: str):
}
+def down_light_ceiling_fan(name: str):
+ """Create a ceiling fan (that has built-in down light) with given name."""
+ return {
+ "name": name,
+ "type": DeviceType.CEILING_FAN,
+ "actions": [Action.TURN_DOWN_LIGHT_ON, Action.TURN_DOWN_LIGHT_OFF],
+ }
+
+
+def up_light_ceiling_fan(name: str):
+ """Create a ceiling fan (that has built-in down light) with given name."""
+ return {
+ "name": name,
+ "type": DeviceType.CEILING_FAN,
+ "actions": [Action.TURN_UP_LIGHT_ON, Action.TURN_UP_LIGHT_OFF],
+ }
+
+
def fireplace(name: str):
"""Create a fireplace with given name."""
return {
@@ -80,11 +108,41 @@ async def test_fan_entity_registry(hass: core.HomeAssistant):
bond_device_id="test-device-id",
)
- registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry()
+ registry: EntityRegistry = er.async_get(hass)
entity = registry.entities["light.fan_name"]
assert entity.unique_id == "test-hub-id_test-device-id"
+async def test_fan_up_light_entity_registry(hass: core.HomeAssistant):
+ """Tests that fan with up light devices are registered in the entity registry."""
+ await setup_platform(
+ hass,
+ LIGHT_DOMAIN,
+ up_light_ceiling_fan("fan-name"),
+ bond_version={"bondid": "test-hub-id"},
+ bond_device_id="test-device-id",
+ )
+
+ registry: EntityRegistry = er.async_get(hass)
+ entity = registry.entities["light.fan_name_up_light"]
+ assert entity.unique_id == "test-hub-id_test-device-id_up_light"
+
+
+async def test_fan_down_light_entity_registry(hass: core.HomeAssistant):
+ """Tests that fan with down light devices are registered in the entity registry."""
+ await setup_platform(
+ hass,
+ LIGHT_DOMAIN,
+ down_light_ceiling_fan("fan-name"),
+ bond_version={"bondid": "test-hub-id"},
+ bond_device_id="test-device-id",
+ )
+
+ registry: EntityRegistry = er.async_get(hass)
+ entity = registry.entities["light.fan_name_down_light"]
+ assert entity.unique_id == "test-hub-id_test-device-id_down_light"
+
+
async def test_fireplace_entity_registry(hass: core.HomeAssistant):
"""Tests that flame fireplace devices are registered in the entity registry."""
await setup_platform(
@@ -95,7 +153,7 @@ async def test_fireplace_entity_registry(hass: core.HomeAssistant):
bond_device_id="test-device-id",
)
- registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry()
+ registry: EntityRegistry = er.async_get(hass)
entity = registry.entities["light.fireplace_name"]
assert entity.unique_id == "test-hub-id_test-device-id"
@@ -110,13 +168,28 @@ async def test_fireplace_with_light_entity_registry(hass: core.HomeAssistant):
bond_device_id="test-device-id",
)
- registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry()
+ registry: EntityRegistry = er.async_get(hass)
entity_flame = registry.entities["light.fireplace_name"]
assert entity_flame.unique_id == "test-hub-id_test-device-id"
- entity_light = registry.entities["light.fireplace_name_2"]
+ entity_light = registry.entities["light.fireplace_name_light"]
assert entity_light.unique_id == "test-hub-id_test-device-id_light"
+async def test_light_entity_registry(hass: core.HomeAssistant):
+ """Tests lights are registered in the entity registry."""
+ await setup_platform(
+ hass,
+ LIGHT_DOMAIN,
+ light("light-name"),
+ bond_version={"bondid": "test-hub-id"},
+ bond_device_id="test-device-id",
+ )
+
+ registry: EntityRegistry = er.async_get(hass)
+ entity = registry.entities["light.light_name"]
+ assert entity.unique_id == "test-hub-id_test-device-id"
+
+
async def test_sbb_trust_state(hass: core.HomeAssistant):
"""Assumed state should be False if device is a Smart by Bond."""
version = {
@@ -124,7 +197,7 @@ async def test_sbb_trust_state(hass: core.HomeAssistant):
"bondid": "test-bond-id",
}
await setup_platform(
- hass, LIGHT_DOMAIN, ceiling_fan("name-1"), bond_version=version
+ hass, LIGHT_DOMAIN, ceiling_fan("name-1"), bond_version=version, bridge={}
)
device = hass.states.get("light.name_1")
@@ -245,6 +318,98 @@ async def test_turn_on_light_with_brightness(hass: core.HomeAssistant):
)
+async def test_turn_on_up_light(hass: core.HomeAssistant):
+ """Tests that turn on command, on an up light, delegates to API."""
+ await setup_platform(
+ hass,
+ LIGHT_DOMAIN,
+ up_light_ceiling_fan("name-1"),
+ bond_device_id="test-device-id",
+ )
+
+ with patch_bond_action() as mock_turn_on, patch_bond_device_state():
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "light.name_1_up_light"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ mock_turn_on.assert_called_once_with(
+ "test-device-id", Action(Action.TURN_UP_LIGHT_ON)
+ )
+
+
+async def test_turn_off_up_light(hass: core.HomeAssistant):
+ """Tests that turn off command, on an up light, delegates to API."""
+ await setup_platform(
+ hass,
+ LIGHT_DOMAIN,
+ up_light_ceiling_fan("name-1"),
+ bond_device_id="test-device-id",
+ )
+
+ with patch_bond_action() as mock_turn_off, patch_bond_device_state():
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "light.name_1_up_light"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ mock_turn_off.assert_called_once_with(
+ "test-device-id", Action(Action.TURN_UP_LIGHT_OFF)
+ )
+
+
+async def test_turn_on_down_light(hass: core.HomeAssistant):
+ """Tests that turn on command, on a down light, delegates to API."""
+ await setup_platform(
+ hass,
+ LIGHT_DOMAIN,
+ down_light_ceiling_fan("name-1"),
+ bond_device_id="test-device-id",
+ )
+
+ with patch_bond_action() as mock_turn_on, patch_bond_device_state():
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "light.name_1_down_light"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ mock_turn_on.assert_called_once_with(
+ "test-device-id", Action(Action.TURN_DOWN_LIGHT_ON)
+ )
+
+
+async def test_turn_off_down_light(hass: core.HomeAssistant):
+ """Tests that turn off command, on a down light, delegates to API."""
+ await setup_platform(
+ hass,
+ LIGHT_DOMAIN,
+ down_light_ceiling_fan("name-1"),
+ bond_device_id="test-device-id",
+ )
+
+ with patch_bond_action() as mock_turn_off, patch_bond_device_state():
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "light.name_1_down_light"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ mock_turn_off.assert_called_once_with(
+ "test-device-id", Action(Action.TURN_DOWN_LIGHT_OFF)
+ )
+
+
async def test_update_reports_light_is_on(hass: core.HomeAssistant):
"""Tests that update command sets correct state when Bond API reports the light is on."""
await setup_platform(hass, LIGHT_DOMAIN, ceiling_fan("name-1"))
@@ -267,6 +432,50 @@ async def test_update_reports_light_is_off(hass: core.HomeAssistant):
assert hass.states.get("light.name_1").state == "off"
+async def test_update_reports_up_light_is_on(hass: core.HomeAssistant):
+ """Tests that update command sets correct state when Bond API reports the up light is on."""
+ await setup_platform(hass, LIGHT_DOMAIN, up_light_ceiling_fan("name-1"))
+
+ with patch_bond_device_state(return_value={"up_light": 1, "light": 1}):
+ async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
+ await hass.async_block_till_done()
+
+ assert hass.states.get("light.name_1_up_light").state == "on"
+
+
+async def test_update_reports_up_light_is_off(hass: core.HomeAssistant):
+ """Tests that update command sets correct state when Bond API reports the up light is off."""
+ await setup_platform(hass, LIGHT_DOMAIN, up_light_ceiling_fan("name-1"))
+
+ with patch_bond_device_state(return_value={"up_light": 0, "light": 0}):
+ async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
+ await hass.async_block_till_done()
+
+ assert hass.states.get("light.name_1_up_light").state == "off"
+
+
+async def test_update_reports_down_light_is_on(hass: core.HomeAssistant):
+ """Tests that update command sets correct state when Bond API reports the down light is on."""
+ await setup_platform(hass, LIGHT_DOMAIN, down_light_ceiling_fan("name-1"))
+
+ with patch_bond_device_state(return_value={"down_light": 1, "light": 1}):
+ async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
+ await hass.async_block_till_done()
+
+ assert hass.states.get("light.name_1_down_light").state == "on"
+
+
+async def test_update_reports_down_light_is_off(hass: core.HomeAssistant):
+ """Tests that update command sets correct state when Bond API reports the down light is off."""
+ await setup_platform(hass, LIGHT_DOMAIN, down_light_ceiling_fan("name-1"))
+
+ with patch_bond_device_state(return_value={"down_light": 0, "light": 0}):
+ async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
+ await hass.async_block_till_done()
+
+ assert hass.states.get("light.name_1_down_light").state == "off"
+
+
async def test_turn_on_fireplace_with_brightness(hass: core.HomeAssistant):
"""Tests that turn on command delegates to set flame API."""
await setup_platform(
diff --git a/tests/components/bond/test_switch.py b/tests/components/bond/test_switch.py
index f2568f5fb9940d..94a9179d3a7b96 100644
--- a/tests/components/bond/test_switch.py
+++ b/tests/components/bond/test_switch.py
@@ -6,6 +6,7 @@
from homeassistant import core
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_registry import EntityRegistry
from homeassistant.util import utcnow
@@ -34,7 +35,7 @@ async def test_entity_registry(hass: core.HomeAssistant):
bond_device_id="test-device-id",
)
- registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry()
+ registry: EntityRegistry = er.async_get(hass)
entity = registry.entities["switch.name_1"]
assert entity.unique_id == "test-hub-id_test-device-id"
diff --git a/tests/components/broadlink/__init__.py b/tests/components/broadlink/__init__.py
index 7185c605f5ce8c..780887551f235b 100644
--- a/tests/components/broadlink/__init__.py
+++ b/tests/components/broadlink/__init__.py
@@ -12,7 +12,7 @@
"34ea34befc25",
"RM mini 3",
"Broadlink",
- "RM2",
+ "RMMINI",
0x2737,
57,
8,
@@ -22,7 +22,7 @@
"34ea34b43b5a",
"RM mini 3",
"Broadlink",
- "RM4",
+ "RMMINIB",
0x5F36,
44017,
10,
@@ -32,7 +32,7 @@
"34ea34b43d22",
"RM pro",
"Broadlink",
- "RM2",
+ "RMPRO",
0x2787,
20025,
7,
@@ -42,7 +42,7 @@
"34ea34c43f31",
"RM4 pro",
"Broadlink",
- "RM4",
+ "RM4PRO",
0x6026,
52,
4,
@@ -62,7 +62,7 @@
"34ea34b61d2c",
"LB1",
"Broadlink",
- "SmartBulb",
+ "LB1",
0x504E,
57,
5,
@@ -96,9 +96,6 @@ async def setup_entry(self, hass, mock_api=None, mock_entry=None):
with patch(
"homeassistant.components.broadlink.device.blk.gendevice",
return_value=mock_api,
- ), patch(
- "homeassistant.components.broadlink.updater.blk.discover",
- return_value=[mock_api],
):
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py
index 30f19c178b7add..503db632cf5912 100644
--- a/tests/components/broadlink/test_config_flow.py
+++ b/tests/components/broadlink/test_config_flow.py
@@ -6,8 +6,10 @@
import broadlink.exceptions as blke
import pytest
-from homeassistant import config_entries
+from homeassistant import config_entries, setup
from homeassistant.components.broadlink.const import DOMAIN
+from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS
+from homeassistant.helpers import device_registry
from . import get_device
@@ -819,3 +821,170 @@ async def test_flow_reauth_valid_host(hass):
assert mock_entry.data["host"] == device.host
assert mock_discover.call_count == 1
assert mock_api.auth.call_count == 1
+
+
+async def test_dhcp_can_finish(hass):
+ """Test DHCP discovery flow can finish right away."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ device = get_device("Living Room")
+ device.host = "1.2.3.4"
+ mock_api = device.get_mock_api()
+
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "dhcp"},
+ data={
+ HOSTNAME: "broadlink",
+ IP_ADDRESS: "1.2.3.4",
+ MAC_ADDRESS: device_registry.format_mac(device.mac),
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "finish"
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Living Room"
+ assert result2["data"] == {
+ "host": "1.2.3.4",
+ "mac": "34ea34b43b5a",
+ "timeout": 10,
+ "type": 24374,
+ }
+
+
+async def test_dhcp_fails_to_connect(hass):
+ """Test DHCP discovery flow that fails to connect."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch(DEVICE_DISCOVERY, side_effect=IndexError()):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "dhcp"},
+ data={
+ HOSTNAME: "broadlink",
+ IP_ADDRESS: "1.2.3.4",
+ MAC_ADDRESS: "34:ea:34:b4:3b:5a",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "cannot_connect"
+
+
+async def test_dhcp_unreachable(hass):
+ """Test DHCP discovery flow that fails to connect."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.ENETUNREACH, None)):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "dhcp"},
+ data={
+ HOSTNAME: "broadlink",
+ IP_ADDRESS: "1.2.3.4",
+ MAC_ADDRESS: "34:ea:34:b4:3b:5a",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "cannot_connect"
+
+
+async def test_dhcp_connect_einval(hass):
+ """Test DHCP discovery flow that fails to connect with EINVAL."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.EINVAL, None)):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "dhcp"},
+ data={
+ HOSTNAME: "broadlink",
+ IP_ADDRESS: "1.2.3.4",
+ MAC_ADDRESS: "34:ea:34:b4:3b:5a",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "invalid_host"
+
+
+async def test_dhcp_connect_unknown_error(hass):
+ """Test DHCP discovery flow that fails to connect with an unknown error."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch(DEVICE_DISCOVERY, side_effect=ValueError("Unknown failure")):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "dhcp"},
+ data={
+ HOSTNAME: "broadlink",
+ IP_ADDRESS: "1.2.3.4",
+ MAC_ADDRESS: "34:ea:34:b4:3b:5a",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "unknown"
+
+
+async def test_dhcp_already_exists(hass):
+ """Test DHCP discovery flow that fails to connect."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ device = get_device("Living Room")
+ mock_entry = device.get_mock_entry()
+ mock_entry.add_to_hass(hass)
+ device.host = "1.2.3.4"
+ mock_api = device.get_mock_api()
+
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "dhcp"},
+ data={
+ HOSTNAME: "broadlink",
+ IP_ADDRESS: "1.2.3.4",
+ MAC_ADDRESS: "34:ea:34:b4:3b:5a",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+
+
+async def test_dhcp_updates_host(hass):
+ """Test DHCP updates host."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ device = get_device("Living Room")
+ device.host = "1.2.3.4"
+ mock_entry = device.get_mock_entry()
+ mock_entry.add_to_hass(hass)
+ mock_api = device.get_mock_api()
+
+ with patch(DEVICE_DISCOVERY, return_value=[mock_api]):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "dhcp"},
+ data={
+ HOSTNAME: "broadlink",
+ IP_ADDRESS: "4.5.6.7",
+ MAC_ADDRESS: "34:ea:34:b4:3b:5a",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+ assert mock_entry.data["host"] == "4.5.6.7"
diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py
index b386b0753b79ce..ab48721dec545f 100644
--- a/tests/components/brother/test_sensor.py
+++ b/tests/components/brother/test_sensor.py
@@ -14,6 +14,7 @@
PERCENTAGE,
STATE_UNAVAILABLE,
)
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import UTC, utcnow
@@ -28,7 +29,7 @@ async def test_sensors(hass):
"""Test states of the sensors."""
entry = await init_integration(hass, skip_setup=True)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
# Pre-create registry entries for disabled by default sensors
registry.async_get_or_create(
@@ -241,7 +242,7 @@ async def test_disabled_by_default_sensors(hass):
"""Test the disabled by default Brother sensors."""
await init_integration(hass)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
state = hass.states.get("sensor.hl_l2340dw_uptime")
assert state is None
diff --git a/tests/components/buienradar/test_camera.py b/tests/components/buienradar/test_camera.py
index 9935cc19cb860f..c9c6d7b4793970 100644
--- a/tests/components/buienradar/test_camera.py
+++ b/tests/components/buienradar/test_camera.py
@@ -1,5 +1,6 @@
"""The tests for generic camera component."""
import asyncio
+from contextlib import suppress
from aiohttp.client_exceptions import ClientResponseError
@@ -214,10 +215,8 @@ async def test_retries_after_error(aioclient_mock, hass, hass_client):
aioclient_mock.get(radar_map_url(), text=None, status=HTTP_INTERNAL_SERVER_ERROR)
# A 404 should not return data and throw:
- try:
+ with suppress(ClientResponseError):
await client.get("/api/camera_proxy/camera.config_test")
- except ClientResponseError:
- pass
assert aioclient_mock.call_count == 1
diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py
index 2c2d744deb9b39..340a4b5d7567e3 100644
--- a/tests/components/camera/test_init.py
+++ b/tests/components/camera/test_init.py
@@ -155,25 +155,20 @@ async def test_websocket_camera_thumbnail(hass, hass_ws_client, mock_camera):
async def test_websocket_stream_no_source(
hass, hass_ws_client, mock_camera, mock_stream
):
- """Test camera/stream websocket command."""
+ """Test camera/stream websocket command with camera with no source."""
await async_setup_component(hass, "camera", {})
- with patch(
- "homeassistant.components.camera.request_stream",
- return_value="http://home.assistant/playlist.m3u8",
- ) as mock_request_stream:
- # Request playlist through WebSocket
- client = await hass_ws_client(hass)
- await client.send_json(
- {"id": 6, "type": "camera/stream", "entity_id": "camera.demo_camera"}
- )
- msg = await client.receive_json()
+ # Request playlist through WebSocket
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {"id": 6, "type": "camera/stream", "entity_id": "camera.demo_camera"}
+ )
+ msg = await client.receive_json()
- # Assert WebSocket response
- assert not mock_request_stream.called
- assert msg["id"] == 6
- assert msg["type"] == TYPE_RESULT
- assert not msg["success"]
+ # Assert WebSocket response
+ assert msg["id"] == 6
+ assert msg["type"] == TYPE_RESULT
+ assert not msg["success"]
async def test_websocket_camera_stream(hass, hass_ws_client, mock_camera, mock_stream):
@@ -181,9 +176,9 @@ async def test_websocket_camera_stream(hass, hass_ws_client, mock_camera, mock_s
await async_setup_component(hass, "camera", {})
with patch(
- "homeassistant.components.camera.request_stream",
+ "homeassistant.components.camera.Stream.endpoint_url",
return_value="http://home.assistant/playlist.m3u8",
- ) as mock_request_stream, patch(
+ ) as mock_stream_view_url, patch(
"homeassistant.components.demo.camera.DemoCamera.stream_source",
return_value="http://example.com",
):
@@ -195,7 +190,7 @@ async def test_websocket_camera_stream(hass, hass_ws_client, mock_camera, mock_s
msg = await client.receive_json()
# Assert WebSocket response
- assert mock_request_stream.called
+ assert mock_stream_view_url.called
assert msg["id"] == 6
assert msg["type"] == TYPE_RESULT
assert msg["success"]
@@ -248,9 +243,7 @@ async def test_play_stream_service_no_source(hass, mock_camera, mock_stream):
ATTR_ENTITY_ID: "camera.demo_camera",
camera.ATTR_MEDIA_PLAYER: "media_player.test",
}
- with patch("homeassistant.components.camera.request_stream"), pytest.raises(
- HomeAssistantError
- ):
+ with pytest.raises(HomeAssistantError):
# Call service
await hass.services.async_call(
camera.DOMAIN, camera.SERVICE_PLAY_STREAM, data, blocking=True
@@ -265,7 +258,7 @@ async def test_handle_play_stream_service(hass, mock_camera, mock_stream):
)
await async_setup_component(hass, "media_player", {})
with patch(
- "homeassistant.components.camera.request_stream"
+ "homeassistant.components.camera.Stream.endpoint_url",
) as mock_request_stream, patch(
"homeassistant.components.demo.camera.DemoCamera.stream_source",
return_value="http://example.com",
@@ -289,7 +282,7 @@ async def test_no_preload_stream(hass, mock_stream):
"""Test camera preload preference."""
demo_prefs = CameraEntityPreferences({PREF_PRELOAD_STREAM: False})
with patch(
- "homeassistant.components.camera.request_stream"
+ "homeassistant.components.camera.Stream.endpoint_url",
) as mock_request_stream, patch(
"homeassistant.components.camera.prefs.CameraPreferences.get",
return_value=demo_prefs,
@@ -308,8 +301,8 @@ async def test_preload_stream(hass, mock_stream):
"""Test camera preload preference."""
demo_prefs = CameraEntityPreferences({PREF_PRELOAD_STREAM: True})
with patch(
- "homeassistant.components.camera.request_stream"
- ) as mock_request_stream, patch(
+ "homeassistant.components.camera.create_stream"
+ ) as mock_create_stream, patch(
"homeassistant.components.camera.prefs.CameraPreferences.get",
return_value=demo_prefs,
), patch(
@@ -322,7 +315,7 @@ async def test_preload_stream(hass, mock_stream):
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
- assert mock_request_stream.called
+ assert mock_create_stream.called
async def test_record_service_invalid_path(hass, mock_camera):
@@ -348,10 +341,9 @@ async def test_record_service(hass, mock_camera, mock_stream):
"homeassistant.components.demo.camera.DemoCamera.stream_source",
return_value="http://example.com",
), patch(
- "homeassistant.components.stream.async_handle_record_service",
- ) as mock_record_service, patch.object(
- hass.config, "is_allowed_path", return_value=True
- ):
+ "homeassistant.components.stream.Stream.async_record",
+ autospec=True,
+ ) as mock_record:
# Call service
await hass.services.async_call(
camera.DOMAIN,
@@ -361,4 +353,4 @@ async def test_record_service(hass, mock_camera, mock_stream):
)
# So long as we call stream.record, the rest should be covered
# by those tests.
- assert mock_record_service.called
+ assert mock_record.called
diff --git a/tests/components/cast/conftest.py b/tests/components/cast/conftest.py
new file mode 100644
index 00000000000000..a8118a94967407
--- /dev/null
+++ b/tests/components/cast/conftest.py
@@ -0,0 +1,77 @@
+"""Test fixtures for the cast integration."""
+# pylint: disable=protected-access
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pychromecast
+import pytest
+
+
+@pytest.fixture()
+def dial_mock():
+ """Mock pychromecast dial."""
+ dial_mock = MagicMock()
+ dial_mock.get_device_status.return_value.uuid = "fake_uuid"
+ dial_mock.get_device_status.return_value.manufacturer = "fake_manufacturer"
+ dial_mock.get_device_status.return_value.model_name = "fake_model_name"
+ dial_mock.get_device_status.return_value.friendly_name = "fake_friendly_name"
+ dial_mock.get_multizone_status.return_value.dynamic_groups = []
+ return dial_mock
+
+
+@pytest.fixture()
+def castbrowser_mock():
+ """Mock pychromecast CastBrowser."""
+ return MagicMock()
+
+
+@pytest.fixture()
+def castbrowser_constructor_mock():
+ """Mock pychromecast CastBrowser constructor."""
+ return MagicMock()
+
+
+@pytest.fixture()
+def mz_mock():
+ """Mock pychromecast MultizoneManager."""
+ return MagicMock()
+
+
+@pytest.fixture()
+def pycast_mock(castbrowser_mock, castbrowser_constructor_mock):
+ """Mock pychromecast."""
+ pycast_mock = MagicMock()
+ pycast_mock.IGNORE_CEC = []
+ pycast_mock.discovery.CastBrowser = castbrowser_constructor_mock
+ pycast_mock.discovery.CastBrowser.return_value = castbrowser_mock
+ pycast_mock.discovery.AbstractCastListener = (
+ pychromecast.discovery.AbstractCastListener
+ )
+ return pycast_mock
+
+
+@pytest.fixture()
+def quick_play_mock():
+ """Mock pychromecast quick_play."""
+ return MagicMock()
+
+
+@pytest.fixture(autouse=True)
+def cast_mock(dial_mock, mz_mock, pycast_mock, quick_play_mock):
+ """Mock pychromecast."""
+ with patch(
+ "homeassistant.components.cast.media_player.pychromecast", pycast_mock
+ ), patch(
+ "homeassistant.components.cast.discovery.pychromecast", pycast_mock
+ ), patch(
+ "homeassistant.components.cast.helpers.dial", dial_mock
+ ), patch(
+ "homeassistant.components.cast.media_player.MultizoneManager",
+ return_value=mz_mock,
+ ), patch(
+ "homeassistant.components.cast.media_player.zeroconf.async_get_instance",
+ AsyncMock(),
+ ), patch(
+ "homeassistant.components.cast.media_player.quick_play",
+ quick_play_mock,
+ ):
+ yield
diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py
new file mode 100644
index 00000000000000..064406df717aac
--- /dev/null
+++ b/tests/components/cast/test_config_flow.py
@@ -0,0 +1,238 @@
+"""Tests for the Cast config flow."""
+from unittest.mock import ANY, patch
+
+import pytest
+
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.components import cast
+
+from tests.common import MockConfigEntry
+
+
+async def test_creating_entry_sets_up_media_player(hass):
+ """Test setting up Cast loads the media player."""
+ with patch(
+ "homeassistant.components.cast.media_player.async_setup_entry",
+ return_value=True,
+ ) as mock_setup, patch(
+ "pychromecast.discovery.discover_chromecasts", return_value=(True, None)
+ ), patch(
+ "pychromecast.discovery.stop_discovery"
+ ):
+ result = await hass.config_entries.flow.async_init(
+ cast.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ # Confirmation form
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ await hass.async_block_till_done()
+
+ assert len(mock_setup.mock_calls) == 1
+
+
+@pytest.mark.parametrize("source", ["import", "user", "zeroconf"])
+async def test_single_instance(hass, source):
+ """Test we only allow a single config flow."""
+ MockConfigEntry(domain="cast").add_to_hass(hass)
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.flow.async_init(
+ "cast", context={"source": source}
+ )
+ assert result["type"] == "abort"
+ assert result["reason"] == "single_instance_allowed"
+
+
+async def test_user_setup(hass):
+ """Test we can finish a config flow."""
+ result = await hass.config_entries.flow.async_init(
+ "cast", context={"source": "user"}
+ )
+ assert result["type"] == "form"
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+
+ users = await hass.auth.async_get_users()
+ assert len(users) == 1
+ assert result["type"] == "create_entry"
+ assert result["result"].data == {
+ "ignore_cec": [],
+ "known_hosts": [],
+ "uuid": [],
+ "user_id": users[0].id, # Home Assistant cast user
+ }
+
+
+async def test_user_setup_options(hass):
+ """Test we can finish a config flow."""
+ result = await hass.config_entries.flow.async_init(
+ "cast", context={"source": "user"}
+ )
+ assert result["type"] == "form"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"known_hosts": "192.168.0.1, , 192.168.0.2 "}
+ )
+
+ users = await hass.auth.async_get_users()
+ assert len(users) == 1
+ assert result["type"] == "create_entry"
+ assert result["result"].data == {
+ "ignore_cec": [],
+ "known_hosts": ["192.168.0.1", "192.168.0.2"],
+ "uuid": [],
+ "user_id": users[0].id, # Home Assistant cast user
+ }
+
+
+async def test_zeroconf_setup(hass):
+ """Test we can finish a config flow through zeroconf."""
+ result = await hass.config_entries.flow.async_init(
+ "cast", context={"source": "zeroconf"}
+ )
+ assert result["type"] == "form"
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+
+ users = await hass.auth.async_get_users()
+ assert len(users) == 1
+ assert result["type"] == "create_entry"
+ assert result["result"].data == {
+ "ignore_cec": [],
+ "known_hosts": [],
+ "uuid": [],
+ "user_id": users[0].id, # Home Assistant cast user
+ }
+
+
+def get_suggested(schema, key):
+ """Get suggested value for key in voluptuous schema."""
+ for k in schema.keys():
+ if k == key:
+ if k.description is None or "suggested_value" not in k.description:
+ return None
+ return k.description["suggested_value"]
+
+
+@pytest.mark.parametrize(
+ "parameter_data",
+ [
+ (
+ "known_hosts",
+ ["192.168.0.10", "192.168.0.11"],
+ "192.168.0.10,192.168.0.11",
+ "192.168.0.1, , 192.168.0.2 ",
+ ["192.168.0.1", "192.168.0.2"],
+ ),
+ (
+ "uuid",
+ ["bla", "blu"],
+ "bla,blu",
+ "foo, , bar ",
+ ["foo", "bar"],
+ ),
+ (
+ "ignore_cec",
+ ["cast1", "cast2"],
+ "cast1,cast2",
+ "other_cast, , some_cast ",
+ ["other_cast", "some_cast"],
+ ),
+ ],
+)
+async def test_option_flow(hass, parameter_data):
+ """Test config flow options."""
+ all_parameters = ["ignore_cec", "known_hosts", "uuid"]
+ parameter, initial, suggested, user_input, updated = parameter_data
+
+ data = {
+ "ignore_cec": [],
+ "known_hosts": [],
+ "uuid": [],
+ }
+ data[parameter] = initial
+ config_entry = MockConfigEntry(domain="cast", data=data)
+ config_entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ # Test ignore_cec and uuid options are hidden if advanced options are disabled
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "options"
+ data_schema = result["data_schema"].schema
+ assert set(data_schema) == {"known_hosts"}
+
+ # Reconfigure ignore_cec, known_hosts, uuid
+ context = {"source": "user", "show_advanced_options": True}
+ result = await hass.config_entries.options.async_init(
+ config_entry.entry_id, context=context
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "options"
+ data_schema = result["data_schema"].schema
+ for other_param in all_parameters:
+ if other_param == parameter:
+ continue
+ assert get_suggested(data_schema, other_param) == ""
+ assert get_suggested(data_schema, parameter) == suggested
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={parameter: user_input},
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["data"] is None
+ for other_param in all_parameters:
+ if other_param == parameter:
+ continue
+ assert config_entry.data[other_param] == []
+ assert config_entry.data[parameter] == updated
+
+ # Clear known_hosts
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={"known_hosts": ""},
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["data"] is None
+ assert config_entry.data == {"ignore_cec": [], "known_hosts": [], "uuid": []}
+
+
+async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock):
+ """Test known hosts is passed to pychromecasts."""
+ result = await hass.config_entries.flow.async_init(
+ "cast", context={"source": "user"}
+ )
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"}
+ )
+ assert result["type"] == "create_entry"
+ await hass.async_block_till_done()
+ config_entry = hass.config_entries.async_entries("cast")[0]
+
+ assert castbrowser_mock.start_discovery.call_count == 1
+ castbrowser_constructor_mock.assert_called_once_with(
+ ANY, ANY, ["192.168.0.1", "192.168.0.2"]
+ )
+ castbrowser_mock.reset_mock()
+ castbrowser_constructor_mock.reset_mock()
+
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={"known_hosts": "192.168.0.11, 192.168.0.12"},
+ )
+
+ await hass.async_block_till_done()
+
+ castbrowser_mock.start_discovery.assert_not_called()
+ castbrowser_constructor_mock.assert_not_called()
+ castbrowser_mock.host_browser.update_hosts.assert_called_once_with(
+ ["192.168.0.11", "192.168.0.12"]
+ )
diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py
index 3fd0e921ca6b81..6ac4f9c9d0c197 100644
--- a/tests/components/cast/test_home_assistant_cast.py
+++ b/tests/components/cast/test_home_assistant_cast.py
@@ -102,12 +102,8 @@ async def test_remove_entry(hass, mock_zeroconf):
entry.add_to_hass(hass)
with patch(
- "homeassistant.components.cast.media_player._async_setup_platform"
- ), patch(
"pychromecast.discovery.discover_chromecasts", return_value=(True, None)
- ), patch(
- "pychromecast.discovery.stop_discovery"
- ):
+ ), patch("pychromecast.discovery.stop_discovery"):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert "cast" in hass.config.components
diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py
index d364256b703625..178f721959f875 100644
--- a/tests/components/cast/test_init.py
+++ b/tests/components/cast/test_init.py
@@ -1,52 +1,43 @@
-"""Tests for the Cast config flow."""
-
+"""Tests for the Cast integration."""
from unittest.mock import patch
-from homeassistant import config_entries, data_entry_flow
from homeassistant.components import cast
from homeassistant.setup import async_setup_component
-async def test_creating_entry_sets_up_media_player(hass):
- """Test setting up Cast loads the media player."""
- with patch(
- "homeassistant.components.cast.media_player.async_setup_entry",
- return_value=True,
- ) as mock_setup, patch(
- "pychromecast.discovery.discover_chromecasts", return_value=(True, None)
- ), patch(
- "pychromecast.discovery.stop_discovery"
- ):
- result = await hass.config_entries.flow.async_init(
- cast.DOMAIN, context={"source": config_entries.SOURCE_USER}
- )
-
- # Confirmation form
- assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
-
- result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
- assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
-
- await hass.async_block_till_done()
-
- assert len(mock_setup.mock_calls) == 1
-
-
-async def test_configuring_cast_creates_entry(hass):
+async def test_import(hass, caplog):
"""Test that specifying config will create an entry."""
with patch(
"homeassistant.components.cast.async_setup_entry", return_value=True
) as mock_setup:
await async_setup_component(
- hass, cast.DOMAIN, {"cast": {"some_config": "to_trigger_import"}}
+ hass,
+ cast.DOMAIN,
+ {
+ "cast": {
+ "media_player": [
+ {"uuid": "abcd"},
+ {"uuid": "abcd", "ignore_cec": "milk"},
+ {"uuid": "efgh", "ignore_cec": "beer"},
+ {"incorrect": "config"},
+ ]
+ }
+ },
)
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
+ assert len(hass.config_entries.async_entries("cast")) == 1
+ entry = hass.config_entries.async_entries("cast")[0]
+ assert set(entry.data["ignore_cec"]) == {"milk", "beer"}
+ assert set(entry.data["uuid"]) == {"abcd", "efgh"}
+
+ assert "Invalid config '{'incorrect': 'config'}'" in caplog.text
+
async def test_not_configuring_cast_not_creates_entry(hass):
- """Test that no config will not create an entry."""
+ """Test that an empty config does not create an entry."""
with patch(
"homeassistant.components.cast.async_setup_entry", return_value=True
) as mock_setup:
diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py
index 050d6a6932db8e..4b1978e8da5baa 100644
--- a/tests/components/cast/test_media_player.py
+++ b/tests/components/cast/test_media_player.py
@@ -1,19 +1,34 @@
"""The tests for the Cast Media player platform."""
# pylint: disable=protected-access
+from __future__ import annotations
+
import json
-from typing import Optional
-from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch
+from unittest.mock import ANY, MagicMock, patch
from uuid import UUID
import attr
+import pychromecast
import pytest
from homeassistant.components import tts
from homeassistant.components.cast import media_player as cast
from homeassistant.components.cast.media_player import ChromecastInfo
+from homeassistant.components.media_player.const import (
+ SUPPORT_NEXT_TRACK,
+ SUPPORT_PAUSE,
+ SUPPORT_PLAY,
+ SUPPORT_PLAY_MEDIA,
+ SUPPORT_PREVIOUS_TRACK,
+ SUPPORT_SEEK,
+ SUPPORT_STOP,
+ SUPPORT_TURN_OFF,
+ SUPPORT_TURN_ON,
+ SUPPORT_VOLUME_MUTE,
+ SUPPORT_VOLUME_SET,
+)
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
-from homeassistant.exceptions import PlatformNotReady
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.setup import async_setup_component
@@ -21,61 +36,6 @@
from tests.common import MockConfigEntry, assert_setup_component
from tests.components.media_player import common
-
-@pytest.fixture()
-def dial_mock():
- """Mock pychromecast dial."""
- dial_mock = MagicMock()
- dial_mock.get_device_status.return_value.uuid = "fake_uuid"
- dial_mock.get_device_status.return_value.manufacturer = "fake_manufacturer"
- dial_mock.get_device_status.return_value.model_name = "fake_model_name"
- dial_mock.get_device_status.return_value.friendly_name = "fake_friendly_name"
- dial_mock.get_multizone_status.return_value.dynamic_groups = []
- return dial_mock
-
-
-@pytest.fixture()
-def mz_mock():
- """Mock pychromecast MultizoneManager."""
- return MagicMock()
-
-
-@pytest.fixture()
-def pycast_mock():
- """Mock pychromecast."""
- pycast_mock = MagicMock()
- pycast_mock.start_discovery.return_value = (None, Mock())
- return pycast_mock
-
-
-@pytest.fixture()
-def quick_play_mock():
- """Mock pychromecast quick_play."""
- return MagicMock()
-
-
-@pytest.fixture(autouse=True)
-def cast_mock(dial_mock, mz_mock, pycast_mock, quick_play_mock):
- """Mock pychromecast."""
- with patch(
- "homeassistant.components.cast.media_player.pychromecast", pycast_mock
- ), patch(
- "homeassistant.components.cast.discovery.pychromecast", pycast_mock
- ), patch(
- "homeassistant.components.cast.helpers.dial", dial_mock
- ), patch(
- "homeassistant.components.cast.media_player.MultizoneManager",
- return_value=mz_mock,
- ), patch(
- "homeassistant.components.cast.media_player.zeroconf.async_get_instance",
- AsyncMock(),
- ), patch(
- "homeassistant.components.cast.media_player.quick_play",
- quick_play_mock,
- ):
- yield
-
-
# pylint: disable=invalid-name
FakeUUID = UUID("57355bce-9364-4aa6-ac1e-eb849dccf9e2")
FakeUUID2 = UUID("57355bce-9364-4aa6-ac1e-eb849dccf9e4")
@@ -84,21 +44,44 @@ def cast_mock(dial_mock, mz_mock, pycast_mock, quick_play_mock):
def get_fake_chromecast(info: ChromecastInfo):
"""Generate a Fake Chromecast object with the specified arguments."""
- mock = MagicMock(host=info.host, port=info.port, uuid=info.uuid)
+ mock = MagicMock(uuid=info.uuid)
mock.media_controller.status = None
return mock
def get_fake_chromecast_info(
- host="192.168.178.42", port=8009, uuid: Optional[UUID] = FakeUUID
+ host="192.168.178.42", port=8009, uuid: UUID | None = FakeUUID
):
"""Generate a Fake ChromecastInfo with the specified arguments."""
- return ChromecastInfo(
+
+ @attr.s(slots=True, frozen=True, eq=False)
+ class ExtendedChromecastInfo(ChromecastInfo):
+ host: str | None = attr.ib(default=None)
+ port: int | None = attr.ib(default=0)
+
+ def __eq__(self, other):
+ if isinstance(other, ChromecastInfo):
+ return (
+ ChromecastInfo(
+ services=self.services,
+ uuid=self.uuid,
+ manufacturer=self.manufacturer,
+ model_name=self.model_name,
+ friendly_name=self.friendly_name,
+ is_audio_group=self.is_audio_group,
+ is_dynamic_group=self.is_dynamic_group,
+ )
+ == other
+ )
+ return super().__eq__(other)
+
+ return ExtendedChromecastInfo(
host=host,
port=port,
uuid=uuid,
friendly_name="Speaker",
services={"the-service"},
+ is_audio_group=port != 8009,
)
@@ -116,11 +99,13 @@ async def async_setup_cast(hass, config=None):
"""Set up the cast platform."""
if config is None:
config = {}
+ data = {**{"ignore_cec": [], "known_hosts": [], "uuid": []}, **config}
with patch(
"homeassistant.helpers.entity_platform.EntityPlatform._async_schedule_add_entities"
) as add_entities:
- MockConfigEntry(domain="cast").add_to_hass(hass)
- await async_setup_component(hass, "cast", {"cast": {"media_player": config}})
+ entry = MockConfigEntry(data=data, domain="cast")
+ entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return add_entities
@@ -128,32 +113,30 @@ async def async_setup_cast(hass, config=None):
async def async_setup_cast_internal_discovery(hass, config=None):
"""Set up the cast platform and the discovery."""
- listener = MagicMock(services={})
- browser = MagicMock(zc={})
+ browser = MagicMock(devices={}, zc={})
with patch(
- "homeassistant.components.cast.discovery.pychromecast.CastListener",
- return_value=listener,
- ) as cast_listener, patch(
- "homeassistant.components.cast.discovery.pychromecast.start_discovery",
+ "homeassistant.components.cast.discovery.pychromecast.discovery.CastBrowser",
return_value=browser,
- ) as start_discovery:
+ ) as cast_browser:
add_entities = await async_setup_cast(hass, config)
await hass.async_block_till_done()
await hass.async_block_till_done()
- assert start_discovery.call_count == 1
+ assert browser.start_discovery.call_count == 1
- discovery_callback = cast_listener.call_args[0][0]
- remove_callback = cast_listener.call_args[0][1]
+ discovery_callback = cast_browser.call_args[0][0].add_cast
+ remove_callback = cast_browser.call_args[0][0].remove_cast
def discover_chromecast(service_name: str, info: ChromecastInfo) -> None:
"""Discover a chromecast device."""
- listener.services[info.uuid] = (
+ browser.devices[info.uuid] = pychromecast.discovery.CastInfo(
{service_name},
info.uuid,
info.model_name,
info.friendly_name,
+ info.host,
+ info.port,
)
discovery_callback(info.uuid, service_name)
@@ -162,7 +145,14 @@ def remove_chromecast(service_name: str, info: ChromecastInfo) -> None:
remove_callback(
info.uuid,
service_name,
- (set(), info.uuid, info.model_name, info.friendly_name),
+ pychromecast.discovery.CastInfo(
+ set(),
+ info.uuid,
+ info.model_name,
+ info.friendly_name,
+ info.host,
+ info.port,
+ ),
)
return discover_chromecast, remove_chromecast, add_entities
@@ -170,21 +160,17 @@ def remove_chromecast(service_name: str, info: ChromecastInfo) -> None:
async def async_setup_media_player_cast(hass: HomeAssistantType, info: ChromecastInfo):
"""Set up the cast platform with async_setup_component."""
- listener = MagicMock(services={})
- browser = MagicMock(zc={})
+ browser = MagicMock(devices={}, zc={})
chromecast = get_fake_chromecast(info)
zconf = get_fake_zconf(host=info.host, port=info.port)
with patch(
- "homeassistant.components.cast.discovery.pychromecast.get_chromecast_from_service",
+ "homeassistant.components.cast.discovery.pychromecast.get_chromecast_from_cast_info",
return_value=chromecast,
) as get_chromecast, patch(
- "homeassistant.components.cast.discovery.pychromecast.CastListener",
- return_value=listener,
- ) as cast_listener, patch(
- "homeassistant.components.cast.discovery.pychromecast.start_discovery",
+ "homeassistant.components.cast.discovery.pychromecast.discovery.CastBrowser",
return_value=browser,
- ), patch(
+ ) as cast_browser, patch(
"homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf",
return_value=zconf,
):
@@ -192,15 +178,18 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas
hass, "cast", {"cast": {"media_player": {"uuid": info.uuid}}}
)
await hass.async_block_till_done()
+ await hass.async_block_till_done()
- discovery_callback = cast_listener.call_args[0][0]
+ discovery_callback = cast_browser.call_args[0][0].add_cast
service_name = "the-service"
- listener.services[info.uuid] = (
+ browser.devices[info.uuid] = pychromecast.discovery.CastInfo(
{service_name},
info.uuid,
info.model_name,
info.friendly_name,
+ info.host,
+ info.port,
)
discovery_callback(info.uuid, service_name)
@@ -210,11 +199,13 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas
def discover_chromecast(service_name: str, info: ChromecastInfo) -> None:
"""Discover a chromecast device."""
- listener.services[info.uuid] = (
+ browser.devices[info.uuid] = pychromecast.discovery.CastInfo(
{service_name},
info.uuid,
info.model_name,
info.friendly_name,
+ info.host,
+ info.port,
)
discovery_callback(info.uuid, service_name)
@@ -240,18 +231,13 @@ def get_status_callbacks(chromecast_mock, mz_mock=None):
return cast_status_cb, conn_status_cb, media_status_cb, group_media_status_cb
-async def test_start_discovery_called_once(hass):
+async def test_start_discovery_called_once(hass, castbrowser_mock):
"""Test pychromecast.start_discovery called exactly once."""
- with patch(
- "homeassistant.components.cast.discovery.pychromecast.start_discovery",
- return_value=Mock(),
- ) as start_discovery:
- await async_setup_cast(hass)
+ await async_setup_cast(hass)
+ assert castbrowser_mock.start_discovery.call_count == 1
- assert start_discovery.call_count == 1
-
- await async_setup_cast(hass)
- assert start_discovery.call_count == 1
+ await async_setup_cast(hass)
+ assert castbrowser_mock.start_discovery.call_count == 1
async def test_internal_discovery_callback_fill_out(hass):
@@ -337,7 +323,6 @@ async def test_internal_discovery_callback_fill_out_fail(hass):
# when called with incomplete info, it should use HTTP to get missing
discover = signal.mock_calls[0][1][0]
assert discover == full_info
- # assert 1 == 2
async def test_internal_discovery_callback_fill_out_group(hass):
@@ -371,27 +356,16 @@ async def test_internal_discovery_callback_fill_out_group(hass):
assert discover == full_info
-async def test_stop_discovery_called_on_stop(hass):
+async def test_stop_discovery_called_on_stop(hass, castbrowser_mock):
"""Test pychromecast.stop_discovery called on shutdown."""
- browser = MagicMock(zc={})
-
- with patch(
- "homeassistant.components.cast.discovery.pychromecast.start_discovery",
- return_value=browser,
- ) as start_discovery:
- # start_discovery should be called with empty config
- await async_setup_cast(hass, {})
-
- assert start_discovery.call_count == 1
+ # start_discovery should be called with empty config
+ await async_setup_cast(hass, {})
+ assert castbrowser_mock.start_discovery.call_count == 1
- with patch(
- "homeassistant.components.cast.discovery.pychromecast.discovery.stop_discovery"
- ) as stop_discovery:
- # stop discovery should be called on shutdown
- hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
- await hass.async_block_till_done()
-
- stop_discovery.assert_called_once_with(browser)
+ # stop discovery should be called on shutdown
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
+ await hass.async_block_till_done()
+ assert castbrowser_mock.stop_discovery.call_count == 1
async def test_create_cast_device_without_uuid(hass):
@@ -415,43 +389,6 @@ async def test_create_cast_device_with_uuid(hass):
assert cast_device is None
-async def test_replay_past_chromecasts(hass):
- """Test cast platform re-playing past chromecasts when adding new one."""
- cast_group1 = get_fake_chromecast_info(host="host1", port=8009, uuid=FakeUUID)
- cast_group2 = get_fake_chromecast_info(
- host="host2", port=8009, uuid=UUID("9462202c-e747-4af5-a66b-7dce0e1ebc09")
- )
- zconf_1 = get_fake_zconf(host="host1", port=8009)
- zconf_2 = get_fake_zconf(host="host2", port=8009)
-
- discover_cast, _, add_dev1 = await async_setup_cast_internal_discovery(
- hass, config={"uuid": FakeUUID}
- )
-
- with patch(
- "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf",
- return_value=zconf_2,
- ):
- discover_cast("service2", cast_group2)
- await hass.async_block_till_done()
- await hass.async_block_till_done() # having tasks that add jobs
- assert add_dev1.call_count == 0
-
- with patch(
- "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf",
- return_value=zconf_1,
- ):
- discover_cast("service1", cast_group1)
- await hass.async_block_till_done()
- await hass.async_block_till_done() # having tasks that add jobs
- assert add_dev1.call_count == 1
-
- add_dev2 = Mock()
- await cast._async_setup_platform(hass, {"host": "host2"}, add_dev2)
- await hass.async_block_till_done()
- assert add_dev2.call_count == 1
-
-
async def test_manual_cast_chromecasts_uuid(hass):
"""Test only wanted casts are added for manual configuration."""
cast_1 = get_fake_chromecast_info(host="host_1", uuid=FakeUUID)
@@ -461,7 +398,7 @@ async def test_manual_cast_chromecasts_uuid(hass):
# Manual configuration of media player with host "configured_host"
discover_cast, _, add_dev1 = await async_setup_cast_internal_discovery(
- hass, config={"uuid": FakeUUID}
+ hass, config={"uuid": str(FakeUUID)}
)
with patch(
"homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf",
@@ -517,7 +454,7 @@ async def test_discover_dynamic_group(hass, dial_mock, pycast_mock, caplog):
zconf_1 = get_fake_zconf(host="host_1", port=23456)
zconf_2 = get_fake_zconf(host="host_2", port=34567)
- reg = await hass.helpers.entity_registry.async_get_registry()
+ reg = er.async_get(hass)
# Fake dynamic group info
tmp1 = MagicMock()
@@ -526,7 +463,7 @@ async def test_discover_dynamic_group(hass, dial_mock, pycast_mock, caplog):
tmp2.uuid = FakeUUID2
dial_mock.get_multizone_status.return_value.dynamic_groups = [tmp1, tmp2]
- pycast_mock.get_chromecast_from_service.assert_not_called()
+ pycast_mock.get_chromecast_from_cast_info.assert_not_called()
discover_cast, remove_cast, add_dev1 = await async_setup_cast_internal_discovery(
hass
)
@@ -539,8 +476,8 @@ async def test_discover_dynamic_group(hass, dial_mock, pycast_mock, caplog):
discover_cast("service", cast_1)
await hass.async_block_till_done()
await hass.async_block_till_done() # having tasks that add jobs
- pycast_mock.get_chromecast_from_service.assert_called()
- pycast_mock.get_chromecast_from_service.reset_mock()
+ pycast_mock.get_chromecast_from_cast_info.assert_called()
+ pycast_mock.get_chromecast_from_cast_info.reset_mock()
assert add_dev1.call_count == 0
assert reg.async_get_entity_id("media_player", "cast", cast_1.uuid) is None
@@ -552,8 +489,8 @@ async def test_discover_dynamic_group(hass, dial_mock, pycast_mock, caplog):
discover_cast("service", cast_2)
await hass.async_block_till_done()
await hass.async_block_till_done() # having tasks that add jobs
- pycast_mock.get_chromecast_from_service.assert_called()
- pycast_mock.get_chromecast_from_service.reset_mock()
+ pycast_mock.get_chromecast_from_cast_info.assert_called()
+ pycast_mock.get_chromecast_from_cast_info.reset_mock()
assert add_dev1.call_count == 0
assert reg.async_get_entity_id("media_player", "cast", cast_1.uuid) is None
@@ -565,7 +502,7 @@ async def test_discover_dynamic_group(hass, dial_mock, pycast_mock, caplog):
discover_cast("service", cast_1)
await hass.async_block_till_done()
await hass.async_block_till_done() # having tasks that add jobs
- pycast_mock.get_chromecast_from_service.assert_not_called()
+ pycast_mock.get_chromecast_from_cast_info.assert_not_called()
assert add_dev1.call_count == 0
assert reg.async_get_entity_id("media_player", "cast", cast_1.uuid) is None
@@ -641,7 +578,7 @@ async def test_entity_availability(hass: HomeAssistantType):
async def test_entity_cast_status(hass: HomeAssistantType):
"""Test handling of cast status."""
entity_id = "media_player.speaker"
- reg = await hass.helpers.entity_registry.async_get_registry()
+ reg = er.async_get(hass)
info = get_fake_chromecast_info()
full_info = attr.evolve(
@@ -662,6 +599,17 @@ async def test_entity_cast_status(hass: HomeAssistantType):
assert state.state == "unknown"
assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid)
+ assert state.attributes.get("supported_features") == (
+ SUPPORT_PAUSE
+ | SUPPORT_PLAY
+ | SUPPORT_PLAY_MEDIA
+ | SUPPORT_STOP
+ | SUPPORT_TURN_OFF
+ | SUPPORT_TURN_ON
+ | SUPPORT_VOLUME_MUTE
+ | SUPPORT_VOLUME_SET
+ )
+
cast_status = MagicMock()
cast_status.volume_level = 0.5
cast_status.volume_muted = False
@@ -680,11 +628,26 @@ async def test_entity_cast_status(hass: HomeAssistantType):
assert state.attributes.get("volume_level") == 0.2
assert state.attributes.get("is_volume_muted")
+ # Disable support for volume control
+ cast_status = MagicMock()
+ cast_status.volume_control_type = "fixed"
+ cast_status_cb(cast_status)
+ await hass.async_block_till_done()
+ state = hass.states.get(entity_id)
+ assert state.attributes.get("supported_features") == (
+ SUPPORT_PAUSE
+ | SUPPORT_PLAY
+ | SUPPORT_PLAY_MEDIA
+ | SUPPORT_STOP
+ | SUPPORT_TURN_OFF
+ | SUPPORT_TURN_ON
+ )
+
async def test_entity_play_media(hass: HomeAssistantType):
"""Test playing media."""
entity_id = "media_player.speaker"
- reg = await hass.helpers.entity_registry.async_get_registry()
+ reg = er.async_get(hass)
info = get_fake_chromecast_info()
full_info = attr.evolve(
@@ -713,7 +676,7 @@ async def test_entity_play_media(hass: HomeAssistantType):
async def test_entity_play_media_cast(hass: HomeAssistantType, quick_play_mock):
"""Test playing media with cast special features."""
entity_id = "media_player.speaker"
- reg = await hass.helpers.entity_registry.async_get_registry()
+ reg = er.async_get(hass)
info = get_fake_chromecast_info()
full_info = attr.evolve(
@@ -746,7 +709,7 @@ async def test_entity_play_media_cast(hass: HomeAssistantType, quick_play_mock):
async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock):
"""Test playing media."""
entity_id = "media_player.speaker"
- reg = await hass.helpers.entity_registry.async_get_registry()
+ reg = er.async_get(hass)
info = get_fake_chromecast_info()
full_info = attr.evolve(
@@ -819,7 +782,7 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistantType):
async def test_entity_media_content_type(hass: HomeAssistantType):
"""Test various content types."""
entity_id = "media_player.speaker"
- reg = await hass.helpers.entity_registry.async_get_registry()
+ reg = er.async_get(hass)
info = get_fake_chromecast_info()
full_info = attr.evolve(
@@ -873,7 +836,7 @@ async def test_entity_media_content_type(hass: HomeAssistantType):
async def test_entity_control(hass: HomeAssistantType):
"""Test various device and media controls."""
entity_id = "media_player.speaker"
- reg = await hass.helpers.entity_registry.async_get_registry()
+ reg = er.async_get(hass)
info = get_fake_chromecast_info()
full_info = attr.evolve(
@@ -894,6 +857,17 @@ async def test_entity_control(hass: HomeAssistantType):
assert state.state == "unknown"
assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid)
+ assert state.attributes.get("supported_features") == (
+ SUPPORT_PAUSE
+ | SUPPORT_PLAY
+ | SUPPORT_PLAY_MEDIA
+ | SUPPORT_STOP
+ | SUPPORT_TURN_OFF
+ | SUPPORT_TURN_ON
+ | SUPPORT_VOLUME_MUTE
+ | SUPPORT_VOLUME_SET
+ )
+
# Turn on
await common.async_turn_on(hass, entity_id)
chromecast.play_media.assert_called_once_with(
@@ -940,6 +914,21 @@ async def test_entity_control(hass: HomeAssistantType):
media_status_cb(media_status)
await hass.async_block_till_done()
+ state = hass.states.get(entity_id)
+ assert state.attributes.get("supported_features") == (
+ SUPPORT_PAUSE
+ | SUPPORT_PLAY
+ | SUPPORT_PLAY_MEDIA
+ | SUPPORT_STOP
+ | SUPPORT_TURN_OFF
+ | SUPPORT_TURN_ON
+ | SUPPORT_PREVIOUS_TRACK
+ | SUPPORT_NEXT_TRACK
+ | SUPPORT_SEEK
+ | SUPPORT_VOLUME_MUTE
+ | SUPPORT_VOLUME_SET
+ )
+
# Media previous
await common.async_media_previous_track(hass, entity_id)
chromecast.media_controller.queue_prev.assert_called_once_with()
@@ -956,7 +945,7 @@ async def test_entity_control(hass: HomeAssistantType):
async def test_entity_media_states(hass: HomeAssistantType):
"""Test various entity media states."""
entity_id = "media_player.speaker"
- reg = await hass.helpers.entity_registry.async_get_registry()
+ reg = er.async_get(hass)
info = get_fake_chromecast_info()
full_info = attr.evolve(
@@ -1015,7 +1004,7 @@ async def test_entity_media_states(hass: HomeAssistantType):
async def test_group_media_states(hass, mz_mock):
"""Test media states are read from group if entity has no state."""
entity_id = "media_player.speaker"
- reg = await hass.helpers.entity_registry.async_get_registry()
+ reg = er.async_get(hass)
info = get_fake_chromecast_info()
full_info = attr.evolve(
@@ -1068,7 +1057,7 @@ async def test_group_media_states(hass, mz_mock):
async def test_group_media_control(hass, mz_mock):
"""Test media controls are handled by group if entity has no state."""
entity_id = "media_player.speaker"
- reg = await hass.helpers.entity_registry.async_get_registry()
+ reg = er.async_get(hass)
info = get_fake_chromecast_info()
full_info = attr.evolve(
@@ -1265,65 +1254,54 @@ async def test_disconnect_on_stop(hass: HomeAssistantType):
async def test_entry_setup_no_config(hass: HomeAssistantType):
- """Test setting up entry with no config.."""
+ """Test deprecated empty yaml config.."""
await async_setup_component(hass, "cast", {})
await hass.async_block_till_done()
- with patch(
- "homeassistant.components.cast.media_player._async_setup_platform",
- ) as mock_setup:
- await cast.async_setup_entry(hass, MockConfigEntry(), None)
-
- assert len(mock_setup.mock_calls) == 1
- assert mock_setup.mock_calls[0][1][1] == {}
+ assert not hass.config_entries.async_entries("cast")
-async def test_entry_setup_single_config(hass: HomeAssistantType):
- """Test setting up entry and having a single config option."""
- await async_setup_component(
- hass, "cast", {"cast": {"media_player": {"uuid": "bla"}}}
- )
+async def test_entry_setup_empty_config(hass: HomeAssistantType):
+ """Test deprecated empty yaml config.."""
+ await async_setup_component(hass, "cast", {"cast": {}})
await hass.async_block_till_done()
- with patch(
- "homeassistant.components.cast.media_player._async_setup_platform",
- ) as mock_setup:
- await cast.async_setup_entry(hass, MockConfigEntry(), None)
-
- assert len(mock_setup.mock_calls) == 1
- assert mock_setup.mock_calls[0][1][1] == {"uuid": "bla"}
+ config_entry = hass.config_entries.async_entries("cast")[0]
+ assert config_entry.data["uuid"] == []
+ assert config_entry.data["ignore_cec"] == []
-async def test_entry_setup_list_config(hass: HomeAssistantType):
- """Test setting up entry and having multiple config options."""
+async def test_entry_setup_single_config(hass: HomeAssistantType, pycast_mock):
+ """Test deprecated yaml config with a single config media_player."""
await async_setup_component(
- hass, "cast", {"cast": {"media_player": [{"uuid": "bla"}, {"uuid": "blu"}]}}
+ hass, "cast", {"cast": {"media_player": {"uuid": "bla", "ignore_cec": "cast1"}}}
)
await hass.async_block_till_done()
- with patch(
- "homeassistant.components.cast.media_player._async_setup_platform",
- ) as mock_setup:
- await cast.async_setup_entry(hass, MockConfigEntry(), None)
+ config_entry = hass.config_entries.async_entries("cast")[0]
+ assert config_entry.data["uuid"] == ["bla"]
+ assert config_entry.data["ignore_cec"] == ["cast1"]
- assert len(mock_setup.mock_calls) == 2
- assert mock_setup.mock_calls[0][1][1] == {"uuid": "bla"}
- assert mock_setup.mock_calls[1][1][1] == {"uuid": "blu"}
+ assert pycast_mock.IGNORE_CEC == ["cast1"]
-async def test_entry_setup_platform_not_ready(hass: HomeAssistantType):
- """Test failed setting up entry will raise PlatformNotReady."""
+async def test_entry_setup_list_config(hass: HomeAssistantType, pycast_mock):
+ """Test deprecated yaml config with multiple media_players."""
await async_setup_component(
- hass, "cast", {"cast": {"media_player": {"uuid": "bla"}}}
+ hass,
+ "cast",
+ {
+ "cast": {
+ "media_player": [
+ {"uuid": "bla", "ignore_cec": "cast1"},
+ {"uuid": "blu", "ignore_cec": ["cast2", "cast3"]},
+ ]
+ }
+ },
)
await hass.async_block_till_done()
- with patch(
- "homeassistant.components.cast.media_player._async_setup_platform",
- side_effect=Exception,
- ) as mock_setup:
- with pytest.raises(PlatformNotReady):
- await cast.async_setup_entry(hass, MockConfigEntry(), None)
-
- assert len(mock_setup.mock_calls) == 1
- assert mock_setup.mock_calls[0][1][1] == {"uuid": "bla"}
+ config_entry = hass.config_entries.async_entries("cast")[0]
+ assert set(config_entry.data["uuid"]) == {"bla", "blu"}
+ assert set(config_entry.data["ignore_cec"]) == {"cast1", "cast2", "cast3"}
+ assert set(pycast_mock.IGNORE_CEC) == {"cast1", "cast2", "cast3"}
diff --git a/tests/components/cert_expiry/test_init.py b/tests/components/cert_expiry/test_init.py
index ea31ba50ea0000..1c62782107b7e9 100644
--- a/tests/components/cert_expiry/test_init.py
+++ b/tests/components/cert_expiry/test_init.py
@@ -5,7 +5,12 @@
from homeassistant.components.cert_expiry.const import DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED
-from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_START
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PORT,
+ EVENT_HOMEASSISTANT_START,
+ STATE_UNAVAILABLE,
+)
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -94,4 +99,9 @@ async def test_unload_config_entry(mock_now, hass):
assert entry.state == ENTRY_STATE_NOT_LOADED
state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
+ assert state.state == STATE_UNAVAILABLE
+
+ await hass.config_entries.async_remove(entry.entry_id)
+ await hass.async_block_till_done()
+ state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
assert state is None
diff --git a/tests/components/climacell/__init__.py b/tests/components/climacell/__init__.py
new file mode 100644
index 00000000000000..04ebc3c14c39c2
--- /dev/null
+++ b/tests/components/climacell/__init__.py
@@ -0,0 +1 @@
+"""Tests for the ClimaCell Weather API integration."""
diff --git a/tests/components/climacell/conftest.py b/tests/components/climacell/conftest.py
new file mode 100644
index 00000000000000..d4c77c58879f06
--- /dev/null
+++ b/tests/components/climacell/conftest.py
@@ -0,0 +1,51 @@
+"""Configure py.test."""
+import json
+from unittest.mock import patch
+
+import pytest
+
+from tests.common import load_fixture
+
+
+@pytest.fixture(name="skip_notifications", autouse=True)
+def skip_notifications_fixture():
+ """Skip notification calls."""
+ with patch("homeassistant.components.persistent_notification.async_create"), patch(
+ "homeassistant.components.persistent_notification.async_dismiss"
+ ):
+ yield
+
+
+@pytest.fixture(name="climacell_config_flow_connect", autouse=True)
+def climacell_config_flow_connect():
+ """Mock valid climacell config flow setup."""
+ with patch(
+ "homeassistant.components.climacell.config_flow.ClimaCellV3.realtime",
+ return_value={},
+ ), patch(
+ "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime",
+ return_value={},
+ ):
+ yield
+
+
+@pytest.fixture(name="climacell_config_entry_update")
+def climacell_config_entry_update_fixture():
+ """Mock valid climacell config entry setup."""
+ with patch(
+ "homeassistant.components.climacell.ClimaCellV3.realtime",
+ return_value=json.loads(load_fixture("climacell/v3_realtime.json")),
+ ), patch(
+ "homeassistant.components.climacell.ClimaCellV3.forecast_hourly",
+ return_value=json.loads(load_fixture("climacell/v3_forecast_hourly.json")),
+ ), patch(
+ "homeassistant.components.climacell.ClimaCellV3.forecast_daily",
+ return_value=json.loads(load_fixture("climacell/v3_forecast_daily.json")),
+ ), patch(
+ "homeassistant.components.climacell.ClimaCellV3.forecast_nowcast",
+ return_value=json.loads(load_fixture("climacell/v3_forecast_nowcast.json")),
+ ), patch(
+ "homeassistant.components.climacell.ClimaCellV4.realtime_and_all_forecasts",
+ return_value=json.loads(load_fixture("climacell/v4.json")),
+ ):
+ yield
diff --git a/tests/components/climacell/const.py b/tests/components/climacell/const.py
new file mode 100644
index 00000000000000..be933ecde290f5
--- /dev/null
+++ b/tests/components/climacell/const.py
@@ -0,0 +1,38 @@
+"""Constants for climacell tests."""
+
+from homeassistant.const import (
+ CONF_API_KEY,
+ CONF_API_VERSION,
+ CONF_LATITUDE,
+ CONF_LONGITUDE,
+ CONF_NAME,
+)
+
+API_KEY = "aa"
+
+MIN_CONFIG = {
+ CONF_API_KEY: API_KEY,
+}
+
+V1_ENTRY_DATA = {
+ CONF_NAME: "ClimaCell",
+ CONF_API_KEY: API_KEY,
+ CONF_LATITUDE: 80,
+ CONF_LONGITUDE: 80,
+}
+
+API_V3_ENTRY_DATA = {
+ CONF_NAME: "ClimaCell",
+ CONF_API_KEY: API_KEY,
+ CONF_LATITUDE: 80,
+ CONF_LONGITUDE: 80,
+ CONF_API_VERSION: 3,
+}
+
+API_V4_ENTRY_DATA = {
+ CONF_NAME: "ClimaCell",
+ CONF_API_KEY: API_KEY,
+ CONF_LATITUDE: 80,
+ CONF_LONGITUDE: 80,
+ CONF_API_VERSION: 4,
+}
diff --git a/tests/components/climacell/test_config_flow.py b/tests/components/climacell/test_config_flow.py
new file mode 100644
index 00000000000000..6cd5fb85794373
--- /dev/null
+++ b/tests/components/climacell/test_config_flow.py
@@ -0,0 +1,201 @@
+"""Test the ClimaCell config flow."""
+import logging
+from unittest.mock import patch
+
+from pyclimacell.exceptions import (
+ CantConnectException,
+ InvalidAPIKeyException,
+ RateLimitedException,
+ UnknownException,
+)
+
+from homeassistant import data_entry_flow
+from homeassistant.components.climacell.config_flow import (
+ _get_config_schema,
+ _get_unique_id,
+)
+from homeassistant.components.climacell.const import (
+ CONF_TIMESTEP,
+ DEFAULT_NAME,
+ DEFAULT_TIMESTEP,
+ DOMAIN,
+)
+from homeassistant.config_entries import SOURCE_USER
+from homeassistant.const import (
+ CONF_API_KEY,
+ CONF_API_VERSION,
+ CONF_LATITUDE,
+ CONF_LONGITUDE,
+ CONF_NAME,
+)
+from homeassistant.helpers.typing import HomeAssistantType
+
+from .const import API_KEY, MIN_CONFIG
+
+from tests.common import MockConfigEntry
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def test_user_flow_minimum_fields(hass: HomeAssistantType) -> None:
+ """Test user config flow with minimum fields."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG),
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == DEFAULT_NAME
+ assert result["data"][CONF_NAME] == DEFAULT_NAME
+ assert result["data"][CONF_API_KEY] == API_KEY
+ assert result["data"][CONF_API_VERSION] == 4
+ assert result["data"][CONF_LATITUDE] == hass.config.latitude
+ assert result["data"][CONF_LONGITUDE] == hass.config.longitude
+
+
+async def test_user_flow_v3(hass: HomeAssistantType) -> None:
+ """Test user config flow with v3 API."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+ data = _get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG)
+ data[CONF_API_VERSION] = 3
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input=data,
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == DEFAULT_NAME
+ assert result["data"][CONF_NAME] == DEFAULT_NAME
+ assert result["data"][CONF_API_KEY] == API_KEY
+ assert result["data"][CONF_API_VERSION] == 3
+ assert result["data"][CONF_LATITUDE] == hass.config.latitude
+ assert result["data"][CONF_LONGITUDE] == hass.config.longitude
+
+
+async def test_user_flow_same_unique_ids(hass: HomeAssistantType) -> None:
+ """Test user config flow with the same unique ID as an existing entry."""
+ user_input = _get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG)
+ MockConfigEntry(
+ domain=DOMAIN,
+ data=user_input,
+ source=SOURCE_USER,
+ unique_id=_get_unique_id(hass, user_input),
+ version=2,
+ ).add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data=user_input,
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+
+async def test_user_flow_cannot_connect(hass: HomeAssistantType) -> None:
+ """Test user config flow when ClimaCell can't connect."""
+ with patch(
+ "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime",
+ side_effect=CantConnectException,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG),
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "cannot_connect"}
+
+
+async def test_user_flow_invalid_api(hass: HomeAssistantType) -> None:
+ """Test user config flow when API key is invalid."""
+ with patch(
+ "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime",
+ side_effect=InvalidAPIKeyException,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG),
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {CONF_API_KEY: "invalid_api_key"}
+
+
+async def test_user_flow_rate_limited(hass: HomeAssistantType) -> None:
+ """Test user config flow when API key is rate limited."""
+ with patch(
+ "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime",
+ side_effect=RateLimitedException,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG),
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {CONF_API_KEY: "rate_limited"}
+
+
+async def test_user_flow_unknown_exception(hass: HomeAssistantType) -> None:
+ """Test user config flow when unknown error occurs."""
+ with patch(
+ "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime",
+ side_effect=UnknownException,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG),
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "unknown"}
+
+
+async def test_options_flow(hass: HomeAssistantType) -> None:
+ """Test options config flow for climacell."""
+ user_config = _get_config_schema(hass)(MIN_CONFIG)
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=user_config,
+ source=SOURCE_USER,
+ unique_id=_get_unique_id(hass, user_config),
+ version=1,
+ )
+ entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(entry.entry_id)
+
+ assert entry.options[CONF_TIMESTEP] == DEFAULT_TIMESTEP
+ assert CONF_TIMESTEP not in entry.data
+
+ result = await hass.config_entries.options.async_init(entry.entry_id, data=None)
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={CONF_TIMESTEP: 1}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == ""
+ assert result["data"][CONF_TIMESTEP] == 1
+ assert entry.options[CONF_TIMESTEP] == 1
diff --git a/tests/components/climacell/test_init.py b/tests/components/climacell/test_init.py
new file mode 100644
index 00000000000000..33a18d553f34a3
--- /dev/null
+++ b/tests/components/climacell/test_init.py
@@ -0,0 +1,91 @@
+"""Tests for Climacell init."""
+import logging
+
+import pytest
+
+from homeassistant.components.climacell.config_flow import (
+ _get_config_schema,
+ _get_unique_id,
+)
+from homeassistant.components.climacell.const import CONF_TIMESTEP, DOMAIN
+from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
+from homeassistant.const import CONF_API_VERSION
+from homeassistant.helpers.typing import HomeAssistantType
+
+from .const import API_V3_ENTRY_DATA, MIN_CONFIG, V1_ENTRY_DATA
+
+from tests.common import MockConfigEntry
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def test_load_and_unload(
+ hass: HomeAssistantType,
+ climacell_config_entry_update: pytest.fixture,
+) -> None:
+ """Test loading and unloading entry."""
+ data = _get_config_schema(hass)(MIN_CONFIG)
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=data,
+ unique_id=_get_unique_id(hass, data),
+ version=1,
+ )
+ config_entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 1
+
+ assert await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0
+
+
+async def test_v3_load_and_unload(
+ hass: HomeAssistantType,
+ climacell_config_entry_update: pytest.fixture,
+) -> None:
+ """Test loading and unloading v3 entry."""
+ data = _get_config_schema(hass)(API_V3_ENTRY_DATA)
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=data,
+ unique_id=_get_unique_id(hass, data),
+ version=1,
+ )
+ config_entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 1
+
+ assert await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0
+
+
+@pytest.mark.parametrize(
+ "old_timestep, new_timestep", [(2, 1), (7, 5), (20, 15), (21, 30)]
+)
+async def test_migrate_timestep(
+ hass: HomeAssistantType,
+ climacell_config_entry_update: pytest.fixture,
+ old_timestep: int,
+ new_timestep: int,
+) -> None:
+ """Test migration to standardized timestep."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=V1_ENTRY_DATA,
+ options={CONF_TIMESTEP: old_timestep},
+ unique_id=_get_unique_id(hass, V1_ENTRY_DATA),
+ version=1,
+ )
+ config_entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert config_entry.version == 1
+ assert (
+ CONF_API_VERSION in config_entry.data
+ and config_entry.data[CONF_API_VERSION] == 3
+ )
+ assert config_entry.options[CONF_TIMESTEP] == new_timestep
diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py
new file mode 100644
index 00000000000000..c49ad8b3c484c2
--- /dev/null
+++ b/tests/components/climacell/test_weather.py
@@ -0,0 +1,382 @@
+"""Tests for Climacell weather entity."""
+from datetime import datetime
+import logging
+from typing import Any, Dict
+from unittest.mock import patch
+
+import pytest
+import pytz
+
+from homeassistant.components.climacell.config_flow import (
+ _get_config_schema,
+ _get_unique_id,
+)
+from homeassistant.components.climacell.const import ATTRIBUTION, DOMAIN
+from homeassistant.components.weather import (
+ ATTR_CONDITION_CLOUDY,
+ ATTR_CONDITION_RAINY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_CONDITION_SUNNY,
+ ATTR_FORECAST,
+ ATTR_FORECAST_CONDITION,
+ ATTR_FORECAST_PRECIPITATION,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY,
+ ATTR_FORECAST_TEMP,
+ ATTR_FORECAST_TEMP_LOW,
+ ATTR_FORECAST_TIME,
+ ATTR_FORECAST_WIND_BEARING,
+ ATTR_FORECAST_WIND_SPEED,
+ ATTR_WEATHER_HUMIDITY,
+ ATTR_WEATHER_OZONE,
+ ATTR_WEATHER_PRESSURE,
+ ATTR_WEATHER_TEMPERATURE,
+ ATTR_WEATHER_VISIBILITY,
+ ATTR_WEATHER_WIND_BEARING,
+ ATTR_WEATHER_WIND_SPEED,
+ DOMAIN as WEATHER_DOMAIN,
+)
+from homeassistant.const import ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME
+from homeassistant.core import State
+from homeassistant.helpers.entity_registry import async_get
+from homeassistant.helpers.typing import HomeAssistantType
+
+from .const import API_V3_ENTRY_DATA, API_V4_ENTRY_DATA
+
+from tests.common import MockConfigEntry
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def _enable_entity(hass: HomeAssistantType, entity_name: str) -> None:
+ """Enable disabled entity."""
+ ent_reg = async_get(hass)
+ entry = ent_reg.async_get(entity_name)
+ updated_entry = ent_reg.async_update_entity(
+ entry.entity_id, **{"disabled_by": None}
+ )
+ assert updated_entry != entry
+ assert updated_entry.disabled is False
+
+
+async def _setup(hass: HomeAssistantType, config: Dict[str, Any]) -> State:
+ """Set up entry and return entity state."""
+ with patch(
+ "homeassistant.util.dt.utcnow",
+ return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=pytz.UTC),
+ ):
+ data = _get_config_schema(hass)(config)
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=data,
+ unique_id=_get_unique_id(hass, data),
+ version=1,
+ )
+ config_entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ await _enable_entity(hass, "weather.climacell_hourly")
+ await _enable_entity(hass, "weather.climacell_nowcast")
+ await hass.async_block_till_done()
+ assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 3
+
+ return hass.states.get("weather.climacell_daily")
+
+
+async def test_v3_weather(
+ hass: HomeAssistantType,
+ climacell_config_entry_update: pytest.fixture,
+) -> None:
+ """Test v3 weather data."""
+ weather_state = await _setup(hass, API_V3_ENTRY_DATA)
+ assert weather_state.state == ATTR_CONDITION_SUNNY
+ assert weather_state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION
+ assert weather_state.attributes[ATTR_FORECAST] == [
+ {
+ ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY,
+ ATTR_FORECAST_TIME: "2021-03-07T00:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
+ ATTR_FORECAST_TEMP: 7,
+ ATTR_FORECAST_TEMP_LOW: -5,
+ },
+ {
+ ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
+ ATTR_FORECAST_TIME: "2021-03-08T00:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
+ ATTR_FORECAST_TEMP: 10,
+ ATTR_FORECAST_TEMP_LOW: -4,
+ },
+ {
+ ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
+ ATTR_FORECAST_TIME: "2021-03-09T00:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
+ ATTR_FORECAST_TEMP: 19,
+ ATTR_FORECAST_TEMP_LOW: 0,
+ },
+ {
+ ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
+ ATTR_FORECAST_TIME: "2021-03-10T00:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
+ ATTR_FORECAST_TEMP: 18,
+ ATTR_FORECAST_TEMP_LOW: 3,
+ },
+ {
+ ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
+ ATTR_FORECAST_TIME: "2021-03-11T00:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5,
+ ATTR_FORECAST_TEMP: 20,
+ ATTR_FORECAST_TEMP_LOW: 9,
+ },
+ {
+ ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
+ ATTR_FORECAST_TIME: "2021-03-12T00:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0.04572,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25,
+ ATTR_FORECAST_TEMP: 20,
+ ATTR_FORECAST_TEMP_LOW: 12,
+ },
+ {
+ ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
+ ATTR_FORECAST_TIME: "2021-03-13T00:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25,
+ ATTR_FORECAST_TEMP: 16,
+ ATTR_FORECAST_TEMP_LOW: 7,
+ },
+ {
+ ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY,
+ ATTR_FORECAST_TIME: "2021-03-14T00:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 1.07442,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 75,
+ ATTR_FORECAST_TEMP: 6,
+ ATTR_FORECAST_TEMP_LOW: 3,
+ },
+ {
+ ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY,
+ ATTR_FORECAST_TIME: "2021-03-15T00:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 7.305040000000001,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95,
+ ATTR_FORECAST_TEMP: 1,
+ ATTR_FORECAST_TEMP_LOW: 0,
+ },
+ {
+ ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
+ ATTR_FORECAST_TIME: "2021-03-16T00:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0.00508,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5,
+ ATTR_FORECAST_TEMP: 6,
+ ATTR_FORECAST_TEMP_LOW: -2,
+ },
+ {
+ ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
+ ATTR_FORECAST_TIME: "2021-03-17T00:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
+ ATTR_FORECAST_TEMP: 11,
+ ATTR_FORECAST_TEMP_LOW: 1,
+ },
+ {
+ ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
+ ATTR_FORECAST_TIME: "2021-03-18T00:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5,
+ ATTR_FORECAST_TEMP: 12,
+ ATTR_FORECAST_TEMP_LOW: 6,
+ },
+ {
+ ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
+ ATTR_FORECAST_TIME: "2021-03-19T00:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0.1778,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 45,
+ ATTR_FORECAST_TEMP: 9,
+ ATTR_FORECAST_TEMP_LOW: 5,
+ },
+ {
+ ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY,
+ ATTR_FORECAST_TIME: "2021-03-20T00:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 1.2319,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55,
+ ATTR_FORECAST_TEMP: 5,
+ ATTR_FORECAST_TEMP_LOW: 3,
+ },
+ {
+ ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
+ ATTR_FORECAST_TIME: "2021-03-21T00:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0.043179999999999996,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 20,
+ ATTR_FORECAST_TEMP: 7,
+ ATTR_FORECAST_TEMP_LOW: 1,
+ },
+ ]
+ assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "ClimaCell - Daily"
+ assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 24
+ assert weather_state.attributes[ATTR_WEATHER_OZONE] == 52.625
+ assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1028.124632345
+ assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 7
+ assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.994026240000002
+ assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 320.31
+ assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.62893696
+
+
+async def test_v4_weather(
+ hass: HomeAssistantType,
+ climacell_config_entry_update: pytest.fixture,
+) -> None:
+ """Test v4 weather data."""
+ weather_state = await _setup(hass, API_V4_ENTRY_DATA)
+ assert weather_state.state == ATTR_CONDITION_SUNNY
+ assert weather_state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION
+ assert weather_state.attributes[ATTR_FORECAST] == [
+ {
+ ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY,
+ ATTR_FORECAST_TIME: "2021-03-07T11:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
+ ATTR_FORECAST_TEMP: 8,
+ ATTR_FORECAST_TEMP_LOW: -3,
+ ATTR_FORECAST_WIND_BEARING: 239.6,
+ ATTR_FORECAST_WIND_SPEED: 15.272674560000002,
+ },
+ {
+ ATTR_FORECAST_CONDITION: "cloudy",
+ ATTR_FORECAST_TIME: "2021-03-08T11:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
+ ATTR_FORECAST_TEMP: 10,
+ ATTR_FORECAST_TEMP_LOW: -3,
+ ATTR_FORECAST_WIND_BEARING: 262.82,
+ ATTR_FORECAST_WIND_SPEED: 11.65165056,
+ },
+ {
+ ATTR_FORECAST_CONDITION: "cloudy",
+ ATTR_FORECAST_TIME: "2021-03-09T11:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
+ ATTR_FORECAST_TEMP: 19,
+ ATTR_FORECAST_TEMP_LOW: 0,
+ ATTR_FORECAST_WIND_BEARING: 229.3,
+ ATTR_FORECAST_WIND_SPEED: 11.3458752,
+ },
+ {
+ ATTR_FORECAST_CONDITION: "cloudy",
+ ATTR_FORECAST_TIME: "2021-03-10T11:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
+ ATTR_FORECAST_TEMP: 18,
+ ATTR_FORECAST_TEMP_LOW: 3,
+ ATTR_FORECAST_WIND_BEARING: 149.91,
+ ATTR_FORECAST_WIND_SPEED: 17.123420160000002,
+ },
+ {
+ ATTR_FORECAST_CONDITION: "cloudy",
+ ATTR_FORECAST_TIME: "2021-03-11T11:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
+ ATTR_FORECAST_TEMP: 19,
+ ATTR_FORECAST_TEMP_LOW: 9,
+ ATTR_FORECAST_WIND_BEARING: 210.45,
+ ATTR_FORECAST_WIND_SPEED: 25.250607360000004,
+ },
+ {
+ ATTR_FORECAST_CONDITION: "rainy",
+ ATTR_FORECAST_TIME: "2021-03-12T11:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0.12192000000000001,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25,
+ ATTR_FORECAST_TEMP: 20,
+ ATTR_FORECAST_TEMP_LOW: 12,
+ ATTR_FORECAST_WIND_BEARING: 217.98,
+ ATTR_FORECAST_WIND_SPEED: 19.794931200000004,
+ },
+ {
+ ATTR_FORECAST_CONDITION: "cloudy",
+ ATTR_FORECAST_TIME: "2021-03-13T11:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25,
+ ATTR_FORECAST_TEMP: 12,
+ ATTR_FORECAST_TEMP_LOW: 6,
+ ATTR_FORECAST_WIND_BEARING: 58.79,
+ ATTR_FORECAST_WIND_SPEED: 15.642823680000001,
+ },
+ {
+ ATTR_FORECAST_CONDITION: "snowy",
+ ATTR_FORECAST_TIME: "2021-03-14T10:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 23.95728,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95,
+ ATTR_FORECAST_TEMP: 6,
+ ATTR_FORECAST_TEMP_LOW: 1,
+ ATTR_FORECAST_WIND_BEARING: 70.25,
+ ATTR_FORECAST_WIND_SPEED: 26.15184,
+ },
+ {
+ ATTR_FORECAST_CONDITION: "snowy",
+ ATTR_FORECAST_TIME: "2021-03-15T10:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 1.46304,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55,
+ ATTR_FORECAST_TEMP: 6,
+ ATTR_FORECAST_TEMP_LOW: -1,
+ ATTR_FORECAST_WIND_BEARING: 84.47,
+ ATTR_FORECAST_WIND_SPEED: 25.57247616,
+ },
+ {
+ ATTR_FORECAST_CONDITION: "cloudy",
+ ATTR_FORECAST_TIME: "2021-03-16T10:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
+ ATTR_FORECAST_TEMP: 6,
+ ATTR_FORECAST_TEMP_LOW: -2,
+ ATTR_FORECAST_WIND_BEARING: 103.85,
+ ATTR_FORECAST_WIND_SPEED: 10.79869824,
+ },
+ {
+ ATTR_FORECAST_CONDITION: "cloudy",
+ ATTR_FORECAST_TIME: "2021-03-17T10:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
+ ATTR_FORECAST_TEMP: 11,
+ ATTR_FORECAST_TEMP_LOW: 1,
+ ATTR_FORECAST_WIND_BEARING: 145.41,
+ ATTR_FORECAST_WIND_SPEED: 11.69993088,
+ },
+ {
+ ATTR_FORECAST_CONDITION: "cloudy",
+ ATTR_FORECAST_TIME: "2021-03-18T10:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 0,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 10,
+ ATTR_FORECAST_TEMP: 12,
+ ATTR_FORECAST_TEMP_LOW: 5,
+ ATTR_FORECAST_WIND_BEARING: 62.99,
+ ATTR_FORECAST_WIND_SPEED: 10.58948352,
+ },
+ {
+ ATTR_FORECAST_CONDITION: "rainy",
+ ATTR_FORECAST_TIME: "2021-03-19T10:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 2.92608,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55,
+ ATTR_FORECAST_TEMP: 9,
+ ATTR_FORECAST_TEMP_LOW: 4,
+ ATTR_FORECAST_WIND_BEARING: 68.54,
+ ATTR_FORECAST_WIND_SPEED: 22.38597504,
+ },
+ {
+ ATTR_FORECAST_CONDITION: "snowy",
+ ATTR_FORECAST_TIME: "2021-03-20T10:00:00+00:00",
+ ATTR_FORECAST_PRECIPITATION: 1.2192,
+ ATTR_FORECAST_PRECIPITATION_PROBABILITY: 33.3,
+ ATTR_FORECAST_TEMP: 5,
+ ATTR_FORECAST_TEMP_LOW: 2,
+ ATTR_FORECAST_WIND_BEARING: 56.98,
+ ATTR_FORECAST_WIND_SPEED: 27.922118400000002,
+ },
+ ]
+ assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "ClimaCell - Daily"
+ assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23
+ assert weather_state.attributes[ATTR_WEATHER_OZONE] == 46.53
+ assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1027.7690615000001
+ assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 7
+ assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 13.116153600000002
+ assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14
+ assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.01517952
diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py
index 4084d37358eca1..dc956f0738ce8c 100644
--- a/tests/components/climate/test_device_action.py
+++ b/tests/components/climate/test_device_action.py
@@ -15,7 +15,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py
index 8e6d5829c415b5..27341b6c2c915c 100644
--- a/tests/components/climate/test_device_condition.py
+++ b/tests/components/climate/test_device_condition.py
@@ -15,7 +15,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
@@ -260,7 +260,7 @@ async def test_capabilities(hass):
capabilities["extra_fields"], custom_serializer=cv.custom_serializer
) == [
{
- "name": "preset_modes",
+ "name": "preset_mode",
"options": [("home", "home"), ("away", "away")],
"required": True,
"type": "select",
diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py
index 69bb4626e495c7..017385362d17bf 100644
--- a/tests/components/climate/test_device_trigger.py
+++ b/tests/components/climate/test_device_trigger.py
@@ -16,7 +16,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py
index 8113c1e343a3d0..9473e61a165a4f 100644
--- a/tests/components/climate/test_init.py
+++ b/tests/components/climate/test_init.py
@@ -1,5 +1,6 @@
"""The tests for the climate component."""
-from typing import List
+from __future__ import annotations
+
from unittest.mock import MagicMock
import pytest
@@ -58,7 +59,7 @@ def hvac_mode(self) -> str:
return HVAC_MODE_HEAT
@property
- def hvac_modes(self) -> List[str]:
+ def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes.
Need to be a subset of HVAC_MODES.
diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py
index 4755d470418fad..75276a9f2e21ca 100644
--- a/tests/components/cloud/conftest.py
+++ b/tests/components/cloud/conftest.py
@@ -43,7 +43,20 @@ def mock_cloud_login(hass, mock_cloud_setup):
hass.data[const.DOMAIN].id_token = jwt.encode(
{
"email": "hello@home-assistant.io",
- "custom:sub-exp": "2018-01-03",
+ "custom:sub-exp": "2300-01-03",
+ "cognito:username": "abcdefghjkl",
+ },
+ "test",
+ )
+
+
+@pytest.fixture
+def mock_expired_cloud_login(hass, mock_cloud_setup):
+ """Mock cloud is logged in."""
+ hass.data[const.DOMAIN].id_token = jwt.encode(
+ {
+ "email": "hello@home-assistant.io",
+ "custom:sub-exp": "2018-01-01",
"cognito:username": "abcdefghjkl",
},
"test",
diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py
index 62225597939f74..c1022dc6aae30f 100644
--- a/tests/components/cloud/test_account_link.py
+++ b/tests/components/cloud/test_account_link.py
@@ -108,7 +108,7 @@ async def test_get_services_error(hass):
assert account_link.DATA_SERVICES not in hass.data
-async def test_implementation(hass, flow_handler):
+async def test_implementation(hass, flow_handler, current_request_with_host):
"""Test Cloud OAuth2 implementation."""
hass.data["cloud"] = None
diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py
index 966ef4b0af312a..8e104f641b2a7f 100644
--- a/tests/components/cloud/test_alexa_config.py
+++ b/tests/components/cloud/test_alexa_config.py
@@ -215,3 +215,16 @@ async def test_alexa_update_report_state(hass, cloud_prefs):
await hass.async_block_till_done()
assert len(mock_sync.mock_calls) == 1
+
+
+def test_enabled_requires_valid_sub(hass, mock_expired_cloud_login, cloud_prefs):
+ """Test that alexa config enabled requires a valid Cloud sub."""
+ assert cloud_prefs.alexa_enabled
+ assert hass.data["cloud"].is_logged_in
+ assert hass.data["cloud"].subscription_expired
+
+ config = alexa_config.AlexaConfig(
+ hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"]
+ )
+
+ assert not config.enabled
diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py
index f58ea1a415b07c..5f29a41c6e0c55 100644
--- a/tests/components/cloud/test_google_config.py
+++ b/tests/components/cloud/test_google_config.py
@@ -1,5 +1,5 @@
"""Test the Cloud Google Config."""
-from unittest.mock import AsyncMock, Mock, patch
+from unittest.mock import Mock, patch
import pytest
@@ -41,21 +41,19 @@ async def test_google_update_report_state(mock_conf, hass, cloud_prefs):
assert len(mock_report_state.mock_calls) == 1
-async def test_sync_entities(aioclient_mock, hass, cloud_prefs):
+async def test_sync_entities(mock_conf, hass, cloud_prefs):
"""Test sync devices."""
- config = CloudGoogleConfig(
- hass,
- GACTIONS_SCHEMA({}),
- "mock-user-id",
- cloud_prefs,
- Mock(auth=Mock(async_check_token=AsyncMock())),
- )
+ await mock_conf.async_initialize()
+ await mock_conf.async_connect_agent_user("mock-user-id")
+
+ assert len(mock_conf._store.agent_user_ids) == 1
with patch(
"hass_nabucasa.cloud_api.async_google_actions_request_sync",
return_value=Mock(status=HTTP_NOT_FOUND),
) as mock_request_sync:
- assert await config.async_sync_entities("user") == HTTP_NOT_FOUND
+ assert await mock_conf.async_sync_entities("mock-user-id") == HTTP_NOT_FOUND
+ assert len(mock_conf._store.agent_user_ids) == 0
assert len(mock_request_sync.mock_calls) == 1
@@ -165,7 +163,29 @@ async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs):
assert len(mock_sync.mock_calls) == 3
+
+async def test_sync_google_when_started(hass, mock_cloud_login, cloud_prefs):
+ """Test Google config syncs on init."""
+ config = CloudGoogleConfig(
+ hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"]
+ )
+ with patch.object(config, "async_sync_entities_all") as mock_sync:
+ await config.async_initialize()
+ await config.async_connect_agent_user("mock-user-id")
+ assert len(mock_sync.mock_calls) == 1
+
+
+async def test_sync_google_on_home_assistant_start(hass, mock_cloud_login, cloud_prefs):
+ """Test Google config syncs when home assistant started."""
+ config = CloudGoogleConfig(
+ hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"]
+ )
+ hass.state = CoreState.starting
with patch.object(config, "async_sync_entities_all") as mock_sync:
+ await config.async_initialize()
+ await config.async_connect_agent_user("mock-user-id")
+ assert len(mock_sync.mock_calls) == 0
+
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert len(mock_sync.mock_calls) == 1
@@ -192,3 +212,16 @@ async def test_google_config_expose_entity_prefs(mock_conf, cloud_prefs):
google_default_expose=["sensor"],
)
assert not mock_conf.should_expose(state)
+
+
+def test_enabled_requires_valid_sub(hass, mock_expired_cloud_login, cloud_prefs):
+ """Test that google config enabled requires a valid Cloud sub."""
+ assert cloud_prefs.google_enabled
+ assert hass.data["cloud"].is_logged_in
+ assert hass.data["cloud"].subscription_expired
+
+ config = CloudGoogleConfig(
+ hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"]
+ )
+
+ assert not config.enabled
diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py
index 80641c304be749..35d261d5603020 100644
--- a/tests/components/cloud/test_http_api.py
+++ b/tests/components/cloud/test_http_api.py
@@ -379,6 +379,7 @@ async def test_websocket_status(
"exclude_entity_globs": [],
"exclude_entities": [],
},
+ "google_registered": False,
"remote_domain": None,
"remote_connected": False,
"remote_certificate": None,
diff --git a/tests/components/cloud/test_prefs.py b/tests/components/cloud/test_prefs.py
index d1b6f9ed867dab..4c0d9acb8f0b18 100644
--- a/tests/components/cloud/test_prefs.py
+++ b/tests/components/cloud/test_prefs.py
@@ -39,6 +39,18 @@ async def test_set_username_migration(hass):
assert not prefs.google_enabled
+async def test_set_new_username(hass, hass_storage):
+ """Test if setting new username returns true."""
+ hass_storage[STORAGE_KEY] = {"version": 1, "data": {"username": "old-user"}}
+
+ prefs = CloudPreferences(hass)
+ await prefs.async_initialize()
+
+ assert not await prefs.async_set_username("old-user")
+
+ assert await prefs.async_set_username("new-user")
+
+
async def test_load_invalid_cloud_user(hass, hass_storage):
"""Test loading cloud user with invalid storage."""
hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": "non-existing"}}
diff --git a/tests/components/cloudflare/__init__.py b/tests/components/cloudflare/__init__.py
index c72a9cd84b0126..60ce0f055d50f7 100644
--- a/tests/components/cloudflare/__init__.py
+++ b/tests/components/cloudflare/__init__.py
@@ -1,5 +1,6 @@
"""Tests for the Cloudflare integration."""
-from typing import List
+from __future__ import annotations
+
from unittest.mock import AsyncMock, patch
from pycfdns import CFRecord
@@ -71,7 +72,7 @@ async def init_integration(
def _get_mock_cfupdate(
zone: str = MOCK_ZONE,
zone_id: str = MOCK_ZONE_ID,
- records: List = MOCK_ZONE_RECORDS,
+ records: list = MOCK_ZONE_RECORDS,
):
client = AsyncMock()
diff --git a/tests/components/coinmarketcap/__init__.py b/tests/components/coinmarketcap/__init__.py
deleted file mode 100644
index 9e9b871bbe2cac..00000000000000
--- a/tests/components/coinmarketcap/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Tests for the coinmarketcap component."""
diff --git a/tests/components/coinmarketcap/test_sensor.py b/tests/components/coinmarketcap/test_sensor.py
deleted file mode 100644
index 369a006f568368..00000000000000
--- a/tests/components/coinmarketcap/test_sensor.py
+++ /dev/null
@@ -1,42 +0,0 @@
-"""Tests for the CoinMarketCap sensor platform."""
-import json
-from unittest.mock import patch
-
-import pytest
-
-from homeassistant.components.sensor import DOMAIN
-from homeassistant.setup import async_setup_component
-
-from tests.common import assert_setup_component, load_fixture
-
-VALID_CONFIG = {
- DOMAIN: {
- "platform": "coinmarketcap",
- "currency_id": 1027,
- "display_currency": "EUR",
- "display_currency_decimals": 3,
- }
-}
-
-
-@pytest.fixture
-async def setup_sensor(hass):
- """Set up demo sensor component."""
- with assert_setup_component(1, DOMAIN):
- with patch(
- "coinmarketcap.Market.ticker",
- return_value=json.loads(load_fixture("coinmarketcap.json")),
- ):
- await async_setup_component(hass, DOMAIN, VALID_CONFIG)
- await hass.async_block_till_done()
-
-
-async def test_setup(hass, setup_sensor):
- """Test the setup with custom settings."""
- state = hass.states.get("sensor.ethereum")
- assert state is not None
-
- assert state.name == "Ethereum"
- assert state.state == "493.455"
- assert state.attributes.get("symbol") == "ETH"
- assert state.attributes.get("unit_of_measurement") == "EUR"
diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py
index 90871faaf78807..21209a8b60dc08 100644
--- a/tests/components/command_line/test_binary_sensor.py
+++ b/tests/components/command_line/test_binary_sensor.py
@@ -1,68 +1,65 @@
"""The tests for the Command line Binary sensor platform."""
-import unittest
-
-from homeassistant.components.command_line import binary_sensor as command_line
+from homeassistant import setup
+from homeassistant.components.binary_sensor import DOMAIN
from homeassistant.const import STATE_OFF, STATE_ON
-from homeassistant.helpers import template
-
-from tests.common import get_test_home_assistant
+from homeassistant.helpers.typing import Any, Dict, HomeAssistantType
-class TestCommandSensorBinarySensor(unittest.TestCase):
- """Test the Command line Binary sensor."""
+async def setup_test_entity(
+ hass: HomeAssistantType, config_dict: Dict[str, Any]
+) -> None:
+ """Set up a test command line binary_sensor entity."""
+ assert await setup.async_setup_component(
+ hass,
+ DOMAIN,
+ {DOMAIN: {"platform": "command_line", "name": "Test", **config_dict}},
+ )
+ await hass.async_block_till_done()
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.hass.stop)
- def test_setup(self):
- """Test sensor setup."""
- config = {
- "name": "Test",
+async def test_setup(hass: HomeAssistantType) -> None:
+ """Test sensor setup."""
+ await setup_test_entity(
+ hass,
+ {
"command": "echo 1",
"payload_on": "1",
"payload_off": "0",
- "command_timeout": 15,
- }
-
- devices = []
+ },
+ )
- def add_dev_callback(devs, update):
- """Add callback to add devices."""
- for dev in devs:
- devices.append(dev)
+ entity_state = hass.states.get("binary_sensor.test")
+ assert entity_state
+ assert entity_state.state == STATE_ON
+ assert entity_state.name == "Test"
- command_line.setup_platform(self.hass, config, add_dev_callback)
- assert 1 == len(devices)
- entity = devices[0]
- entity.update()
- assert "Test" == entity.name
- assert STATE_ON == entity.state
+async def test_template(hass: HomeAssistantType) -> None:
+ """Test setting the state with a template."""
- def test_template(self):
- """Test setting the state with a template."""
- data = command_line.CommandSensorData(self.hass, "echo 10", 15)
+ await setup_test_entity(
+ hass,
+ {
+ "command": "echo 10",
+ "payload_on": "1.0",
+ "payload_off": "0",
+ "value_template": "{{ value | multiply(0.1) }}",
+ },
+ )
- entity = command_line.CommandBinarySensor(
- self.hass,
- data,
- "test",
- None,
- "1.0",
- "0",
- template.Template("{{ value | multiply(0.1) }}", self.hass),
- )
- entity.update()
- assert STATE_ON == entity.state
+ entity_state = hass.states.get("binary_sensor.test")
+ assert entity_state.state == STATE_ON
- def test_sensor_off(self):
- """Test setting the state with a template."""
- data = command_line.CommandSensorData(self.hass, "echo 0", 15)
- entity = command_line.CommandBinarySensor(
- self.hass, data, "test", None, "1", "0", None
- )
- entity.update()
- assert STATE_OFF == entity.state
+async def test_sensor_off(hass: HomeAssistantType) -> None:
+ """Test setting the state with a template."""
+ await setup_test_entity(
+ hass,
+ {
+ "command": "echo 0",
+ "payload_on": "1",
+ "payload_off": "0",
+ },
+ )
+ entity_state = hass.states.get("binary_sensor.test")
+ assert entity_state.state == STATE_OFF
diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py
index ee692413bcd258..093c1e86212d1b 100644
--- a/tests/components/command_line/test_cover.py
+++ b/tests/components/command_line/test_cover.py
@@ -1,15 +1,10 @@
"""The tests the cover command line platform."""
import os
-from os import path
import tempfile
-from unittest import mock
from unittest.mock import patch
-import pytest
-
-from homeassistant import config as hass_config
-import homeassistant.components.command_line.cover as cmd_rs
-from homeassistant.components.cover import DOMAIN
+from homeassistant import config as hass_config, setup
+from homeassistant.components.cover import DOMAIN, SCAN_INTERVAL
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_CLOSE_COVER,
@@ -17,101 +12,128 @@
SERVICE_RELOAD,
SERVICE_STOP_COVER,
)
-from homeassistant.setup import async_setup_component
+from homeassistant.helpers.typing import Any, Dict, HomeAssistantType
+import homeassistant.util.dt as dt_util
+
+from tests.common import async_fire_time_changed
-@pytest.fixture
-def rs(hass):
- """Return CommandCover instance."""
- return cmd_rs.CommandCover(
+async def setup_test_entity(
+ hass: HomeAssistantType, config_dict: Dict[str, Any]
+) -> None:
+ """Set up a test command line notify service."""
+ assert await setup.async_setup_component(
hass,
- "foo",
- "command_open",
- "command_close",
- "command_stop",
- "command_state",
- None,
- 15,
+ DOMAIN,
+ {
+ DOMAIN: [
+ {"platform": "command_line", "covers": config_dict},
+ ]
+ },
)
+ await hass.async_block_till_done()
-def test_should_poll_new(rs):
- """Test the setting of polling."""
- assert rs.should_poll is True
- rs._command_state = None
- assert rs.should_poll is False
+async def test_no_covers(caplog: Any, hass: HomeAssistantType) -> None:
+ """Test that the cover does not polls when there's no state command."""
+ with patch(
+ "homeassistant.components.command_line.subprocess.check_output",
+ return_value=b"50\n",
+ ):
+ await setup_test_entity(hass, {})
+ assert "No covers added" in caplog.text
-def test_query_state_value(rs):
- """Test with state value."""
- with mock.patch("subprocess.check_output") as mock_run:
- mock_run.return_value = b" foo bar "
- result = rs._query_state_value("runme")
- assert "foo bar" == result
- assert mock_run.call_count == 1
- assert mock_run.call_args == mock.call(
- "runme", shell=True, timeout=15 # nosec # shell by design
+
+async def test_no_poll_when_cover_has_no_command_state(hass: HomeAssistantType) -> None:
+ """Test that the cover does not polls when there's no state command."""
+
+ with patch(
+ "homeassistant.components.command_line.subprocess.check_output",
+ return_value=b"50\n",
+ ) as check_output:
+ await setup_test_entity(hass, {"test": {}})
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
+ await hass.async_block_till_done()
+ assert not check_output.called
+
+
+async def test_poll_when_cover_has_command_state(hass: HomeAssistantType) -> None:
+ """Test that the cover polls when there's a state command."""
+
+ with patch(
+ "homeassistant.components.command_line.subprocess.check_output",
+ return_value=b"50\n",
+ ) as check_output:
+ await setup_test_entity(hass, {"test": {"command_state": "echo state"}})
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
+ await hass.async_block_till_done()
+ check_output.assert_called_once_with(
+ "echo state", shell=True, timeout=15 # nosec # shell by design
)
-async def test_state_value(hass):
+async def test_state_value(hass: HomeAssistantType) -> None:
"""Test with state value."""
with tempfile.TemporaryDirectory() as tempdirname:
path = os.path.join(tempdirname, "cover_status")
- test_cover = {
- "command_state": f"cat {path}",
- "command_open": f"echo 1 > {path}",
- "command_close": f"echo 1 > {path}",
- "command_stop": f"echo 0 > {path}",
- "value_template": "{{ value }}",
- }
- assert (
- await async_setup_component(
- hass,
- DOMAIN,
- {"cover": {"platform": "command_line", "covers": {"test": test_cover}}},
- )
- is True
+ await setup_test_entity(
+ hass,
+ {
+ "test": {
+ "command_state": f"cat {path}",
+ "command_open": f"echo 1 > {path}",
+ "command_close": f"echo 1 > {path}",
+ "command_stop": f"echo 0 > {path}",
+ "value_template": "{{ value }}",
+ }
+ },
)
- await hass.async_block_till_done()
- assert "unknown" == hass.states.get("cover.test").state
+ entity_state = hass.states.get("cover.test")
+ assert entity_state
+ assert entity_state.state == "unknown"
await hass.services.async_call(
DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: "cover.test"}, blocking=True
)
- assert "open" == hass.states.get("cover.test").state
+ entity_state = hass.states.get("cover.test")
+ assert entity_state
+ assert entity_state.state == "open"
await hass.services.async_call(
DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: "cover.test"}, blocking=True
)
- assert "open" == hass.states.get("cover.test").state
+ entity_state = hass.states.get("cover.test")
+ assert entity_state
+ assert entity_state.state == "open"
await hass.services.async_call(
DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: "cover.test"}, blocking=True
)
- assert "closed" == hass.states.get("cover.test").state
+ entity_state = hass.states.get("cover.test")
+ assert entity_state
+ assert entity_state.state == "closed"
-async def test_reload(hass):
+async def test_reload(hass: HomeAssistantType) -> None:
"""Verify we can reload command_line covers."""
- test_cover = {
- "command_state": "echo open",
- "value_template": "{{ value }}",
- }
- await async_setup_component(
+ await setup_test_entity(
hass,
- DOMAIN,
- {"cover": {"platform": "command_line", "covers": {"test": test_cover}}},
+ {
+ "test": {
+ "command_state": "echo open",
+ "value_template": "{{ value }}",
+ }
+ },
)
- await hass.async_block_till_done()
-
- assert len(hass.states.async_all()) == 1
- assert hass.states.get("cover.test").state
+ entity_state = hass.states.get("cover.test")
+ assert entity_state
+ assert entity_state.state == "unknown"
- yaml_path = path.join(
- _get_fixtures_base_path(),
+ yaml_path = os.path.join(
+ os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
"fixtures",
"command_line/configuration.yaml",
)
@@ -126,9 +148,18 @@ async def test_reload(hass):
assert len(hass.states.async_all()) == 1
- assert hass.states.get("cover.test") is None
+ assert not hass.states.get("cover.test")
assert hass.states.get("cover.from_yaml")
-def _get_fixtures_base_path():
- return path.dirname(path.dirname(path.dirname(__file__)))
+async def test_move_cover_failure(caplog: Any, hass: HomeAssistantType) -> None:
+ """Test with state value."""
+
+ await setup_test_entity(
+ hass,
+ {"test": {"command_open": "exit 1"}},
+ )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: "cover.test"}, blocking=True
+ )
+ assert "Command failed" in caplog.text
diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py
index 3dcb521cfd283d..4166b9e8bbf251 100644
--- a/tests/components/command_line/test_notify.py
+++ b/tests/components/command_line/test_notify.py
@@ -1,117 +1,115 @@
"""The tests for the command line notification platform."""
import os
+import subprocess
import tempfile
-import unittest
from unittest.mock import patch
-import homeassistant.components.notify as notify
-from homeassistant.setup import async_setup_component, setup_component
-
-from tests.common import assert_setup_component, get_test_home_assistant
-
-
-class TestCommandLine(unittest.TestCase):
- """Test the command line notifications."""
-
- def setUp(self): # pylint: disable=invalid-name
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.tear_down_cleanup)
-
- def tear_down_cleanup(self):
- """Stop down everything that was started."""
- self.hass.stop()
-
- def test_setup(self):
- """Test setup."""
- with assert_setup_component(1) as handle_config:
- assert setup_component(
- self.hass,
- "notify",
- {
- "notify": {
- "name": "test",
- "platform": "command_line",
- "command": "echo $(cat); exit 1",
- }
- },
- )
- assert handle_config[notify.DOMAIN]
-
- def test_bad_config(self):
- """Test set up the platform with bad/missing configuration."""
- config = {notify.DOMAIN: {"name": "test", "platform": "command_line"}}
- with assert_setup_component(0) as handle_config:
- assert setup_component(self.hass, notify.DOMAIN, config)
- assert not handle_config[notify.DOMAIN]
-
- def test_command_line_output(self):
- """Test the command line output."""
- with tempfile.TemporaryDirectory() as tempdirname:
- filename = os.path.join(tempdirname, "message.txt")
- message = "one, two, testing, testing"
- with assert_setup_component(1) as handle_config:
- assert setup_component(
- self.hass,
- notify.DOMAIN,
- {
- "notify": {
- "name": "test",
- "platform": "command_line",
- "command": f"echo $(cat) > {filename}",
- }
- },
- )
- assert handle_config[notify.DOMAIN]
-
- assert self.hass.services.call(
- "notify", "test", {"message": message}, blocking=True
- )
-
- with open(filename) as fil:
- # the echo command adds a line break
- assert fil.read() == f"{message}\n"
-
- @patch("homeassistant.components.command_line.notify._LOGGER.error")
- def test_error_for_none_zero_exit_code(self, mock_error):
- """Test if an error is logged for non zero exit codes."""
- with assert_setup_component(1) as handle_config:
- assert setup_component(
- self.hass,
- notify.DOMAIN,
- {
- "notify": {
- "name": "test",
- "platform": "command_line",
- "command": "echo $(cat); exit 1",
- }
- },
- )
- assert handle_config[notify.DOMAIN]
-
- assert self.hass.services.call(
- "notify", "test", {"message": "error"}, blocking=True
- )
- assert mock_error.call_count == 1
+from homeassistant import setup
+from homeassistant.components.notify import DOMAIN
+from homeassistant.helpers.typing import Any, Dict, HomeAssistantType
-async def test_timeout(hass, caplog):
- """Test we do not block forever."""
- assert await async_setup_component(
+async def setup_test_service(
+ hass: HomeAssistantType, config_dict: Dict[str, Any]
+) -> None:
+ """Set up a test command line notify service."""
+ assert await setup.async_setup_component(
hass,
- notify.DOMAIN,
+ DOMAIN,
{
- "notify": {
- "name": "test",
- "platform": "command_line",
- "command": "sleep 10000",
- "command_timeout": 0.0000001,
- }
+ DOMAIN: [
+ {"platform": "command_line", "name": "Test", **config_dict},
+ ]
},
)
await hass.async_block_till_done()
+
+
+async def test_setup(hass: HomeAssistantType) -> None:
+ """Test sensor setup."""
+ await setup_test_service(hass, {"command": "exit 0"})
+ assert hass.services.has_service(DOMAIN, "test")
+
+
+async def test_bad_config(hass: HomeAssistantType) -> None:
+ """Test set up the platform with bad/missing configuration."""
+ await setup_test_service(hass, {})
+ assert not hass.services.has_service(DOMAIN, "test")
+
+
+async def test_command_line_output(hass: HomeAssistantType) -> None:
+ """Test the command line output."""
+ with tempfile.TemporaryDirectory() as tempdirname:
+ filename = os.path.join(tempdirname, "message.txt")
+ message = "one, two, testing, testing"
+ await setup_test_service(
+ hass,
+ {
+ "command": f"cat > {filename}",
+ },
+ )
+
+ assert hass.services.has_service(DOMAIN, "test")
+
+ assert await hass.services.async_call(
+ DOMAIN, "test", {"message": message}, blocking=True
+ )
+ with open(filename) as handle:
+ # the echo command adds a line break
+ assert message == handle.read()
+
+
+async def test_error_for_none_zero_exit_code(
+ caplog: Any, hass: HomeAssistantType
+) -> None:
+ """Test if an error is logged for non zero exit codes."""
+ await setup_test_service(
+ hass,
+ {
+ "command": "exit 1",
+ },
+ )
+
assert await hass.services.async_call(
- "notify", "test", {"message": "error"}, blocking=True
+ DOMAIN, "test", {"message": "error"}, blocking=True
+ )
+ assert "Command failed" in caplog.text
+
+
+async def test_timeout(caplog: Any, hass: HomeAssistantType) -> None:
+ """Test blocking is not forever."""
+ await setup_test_service(
+ hass,
+ {
+ "command": "sleep 10000",
+ "command_timeout": 0.0000001,
+ },
+ )
+ assert await hass.services.async_call(
+ DOMAIN, "test", {"message": "error"}, blocking=True
)
- await hass.async_block_till_done()
assert "Timeout" in caplog.text
+
+
+async def test_subprocess_exceptions(caplog: Any, hass: HomeAssistantType) -> None:
+ """Test that notify subprocess exceptions are handled correctly."""
+
+ with patch(
+ "homeassistant.components.command_line.notify.subprocess.Popen",
+ side_effect=[
+ subprocess.TimeoutExpired("cmd", 10),
+ subprocess.SubprocessError(),
+ ],
+ ) as check_output:
+ await setup_test_service(hass, {"command": "exit 0"})
+ assert await hass.services.async_call(
+ DOMAIN, "test", {"message": "error"}, blocking=True
+ )
+ assert check_output.call_count == 1
+ assert "Timeout for command" in caplog.text
+
+ assert await hass.services.async_call(
+ DOMAIN, "test", {"message": "error"}, blocking=True
+ )
+ assert check_output.call_count == 2
+ assert "Error trying to exec command" in caplog.text
diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py
index 042c9acf432646..66472c5feba9e8 100644
--- a/tests/components/command_line/test_sensor.py
+++ b/tests/components/command_line/test_sensor.py
@@ -1,201 +1,224 @@
"""The tests for the Command line sensor platform."""
-import unittest
from unittest.mock import patch
-from homeassistant.components.command_line import sensor as command_line
-from homeassistant.helpers.template import Template
-
-from tests.common import get_test_home_assistant
-
-
-class TestCommandSensorSensor(unittest.TestCase):
- """Test the Command line sensor."""
-
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.hass.stop)
-
- def update_side_effect(self, data):
- """Side effect function for mocking CommandSensorData.update()."""
- self.commandline.data = data
-
- def test_setup(self):
- """Test sensor setup."""
- config = {
- "name": "Test",
- "unit_of_measurement": "in",
+from homeassistant import setup
+from homeassistant.components.sensor import DOMAIN
+from homeassistant.helpers.typing import Any, Dict, HomeAssistantType
+
+
+async def setup_test_entities(
+ hass: HomeAssistantType, config_dict: Dict[str, Any]
+) -> None:
+ """Set up a test command line sensor entity."""
+ assert await setup.async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ DOMAIN: [
+ {
+ "platform": "template",
+ "sensors": {
+ "template_sensor": {
+ "value_template": "template_value",
+ }
+ },
+ },
+ {"platform": "command_line", "name": "Test", **config_dict},
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+
+
+async def test_setup(hass: HomeAssistantType) -> None:
+ """Test sensor setup."""
+ await setup_test_entities(
+ hass,
+ {
"command": "echo 5",
- "command_timeout": 15,
- }
- devices = []
-
- def add_dev_callback(devs, update):
- """Add callback to add devices."""
- for dev in devs:
- devices.append(dev)
-
- command_line.setup_platform(self.hass, config, add_dev_callback)
-
- assert len(devices) == 1
- entity = devices[0]
- entity.update()
- assert entity.name == "Test"
- assert entity.unit_of_measurement == "in"
- assert entity.state == "5"
-
- def test_template(self):
- """Test command sensor with template."""
- data = command_line.CommandSensorData(self.hass, "echo 50", 15)
-
- entity = command_line.CommandSensor(
- self.hass,
- data,
- "test",
- "in",
- Template("{{ value | multiply(0.1) }}", self.hass),
- [],
+ "unit_of_measurement": "in",
+ },
+ )
+ entity_state = hass.states.get("sensor.test")
+ assert entity_state
+ assert entity_state.state == "5"
+ assert entity_state.name == "Test"
+ assert entity_state.attributes["unit_of_measurement"] == "in"
+
+
+async def test_template(hass: HomeAssistantType) -> None:
+ """Test command sensor with template."""
+ await setup_test_entities(
+ hass,
+ {
+ "command": "echo 50",
+ "unit_of_measurement": "in",
+ "value_template": "{{ value | multiply(0.1) }}",
+ },
+ )
+ entity_state = hass.states.get("sensor.test")
+ assert entity_state
+ assert float(entity_state.state) == 5
+
+
+async def test_template_render(hass: HomeAssistantType) -> None:
+ """Ensure command with templates get rendered properly."""
+
+ await setup_test_entities(
+ hass,
+ {
+ "command": "echo {{ states.sensor.template_sensor.state }}",
+ },
+ )
+ entity_state = hass.states.get("sensor.test")
+ assert entity_state
+ assert entity_state.state == "template_value"
+
+
+async def test_template_render_with_quote(hass: HomeAssistantType) -> None:
+ """Ensure command with templates and quotes get rendered properly."""
+
+ with patch(
+ "homeassistant.components.command_line.subprocess.check_output",
+ return_value=b"Works\n",
+ ) as check_output:
+ await setup_test_entities(
+ hass,
+ {
+ "command": 'echo "{{ states.sensor.template_sensor.state }}" "3 4"',
+ },
)
- entity.update()
- assert float(entity.state) == 5
-
- def test_template_render(self):
- """Ensure command with templates get rendered properly."""
- self.hass.states.set("sensor.test_state", "Works")
- data = command_line.CommandSensorData(
- self.hass, "echo {{ states.sensor.test_state.state }}", 15
- )
- data.update()
-
- assert data.value == "Works"
-
- def test_template_render_with_quote(self):
- """Ensure command with templates and quotes get rendered properly."""
- self.hass.states.set("sensor.test_state", "Works 2")
- with patch(
- "homeassistant.components.command_line.subprocess.check_output",
- return_value=b"Works\n",
- ) as check_output:
- data = command_line.CommandSensorData(
- self.hass,
- 'echo "{{ states.sensor.test_state.state }}" "3 4"',
- 15,
- )
- data.update()
-
- assert data.value == "Works"
check_output.assert_called_once_with(
- 'echo "Works 2" "3 4"', shell=True, timeout=15 # nosec # shell by design
- )
-
- def test_bad_command(self):
- """Test bad command."""
- data = command_line.CommandSensorData(self.hass, "asdfasdf", 15)
- data.update()
-
- assert data.value is None
-
- def test_update_with_json_attrs(self):
- """Test attributes get extracted from a JSON result."""
- data = command_line.CommandSensorData(
- self.hass,
- (
- 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\":\
- \\"another_json_value\\", \\"key_three\\": \\"value_three\\" }'
- ),
- 15,
- )
-
- self.sensor = command_line.CommandSensor(
- self.hass, data, "test", None, None, ["key", "another_key", "key_three"]
- )
- self.sensor.update()
- assert self.sensor.device_state_attributes["key"] == "some_json_value"
- assert (
- self.sensor.device_state_attributes["another_key"] == "another_json_value"
- )
- assert self.sensor.device_state_attributes["key_three"] == "value_three"
-
- @patch("homeassistant.components.command_line.sensor._LOGGER")
- def test_update_with_json_attrs_no_data(self, mock_logger):
- """Test attributes when no JSON result fetched."""
- data = command_line.CommandSensorData(self.hass, "echo ", 15)
- self.sensor = command_line.CommandSensor(
- self.hass, data, "test", None, None, ["key"]
- )
- self.sensor.update()
- assert {} == self.sensor.device_state_attributes
- assert mock_logger.warning.called
-
- @patch("homeassistant.components.command_line.sensor._LOGGER")
- def test_update_with_json_attrs_not_dict(self, mock_logger):
- """Test attributes get extracted from a JSON result."""
- data = command_line.CommandSensorData(self.hass, "echo [1, 2, 3]", 15)
- self.sensor = command_line.CommandSensor(
- self.hass, data, "test", None, None, ["key"]
- )
- self.sensor.update()
- assert {} == self.sensor.device_state_attributes
- assert mock_logger.warning.called
-
- @patch("homeassistant.components.command_line.sensor._LOGGER")
- def test_update_with_json_attrs_bad_JSON(self, mock_logger):
- """Test attributes get extracted from a JSON result."""
- data = command_line.CommandSensorData(
- self.hass, "echo This is text rather than JSON data.", 15
- )
- self.sensor = command_line.CommandSensor(
- self.hass, data, "test", None, None, ["key"]
- )
- self.sensor.update()
- assert {} == self.sensor.device_state_attributes
- assert mock_logger.warning.called
-
- def test_update_with_missing_json_attrs(self):
- """Test attributes get extracted from a JSON result."""
- data = command_line.CommandSensorData(
- self.hass,
- (
- 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\":\
- \\"another_json_value\\", \\"key_three\\": \\"value_three\\" }'
- ),
- 15,
- )
-
- self.sensor = command_line.CommandSensor(
- self.hass,
- data,
- "test",
- None,
- None,
- ["key", "another_key", "key_three", "special_key"],
- )
- self.sensor.update()
- assert self.sensor.device_state_attributes["key"] == "some_json_value"
- assert (
- self.sensor.device_state_attributes["another_key"] == "another_json_value"
- )
- assert self.sensor.device_state_attributes["key_three"] == "value_three"
- assert "special_key" not in self.sensor.device_state_attributes
-
- def test_update_with_unnecessary_json_attrs(self):
- """Test attributes get extracted from a JSON result."""
- data = command_line.CommandSensorData(
- self.hass,
- (
- 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\":\
- \\"another_json_value\\", \\"key_three\\": \\"value_three\\" }'
- ),
- 15,
- )
-
- self.sensor = command_line.CommandSensor(
- self.hass, data, "test", None, None, ["key", "another_key"]
- )
- self.sensor.update()
- assert self.sensor.device_state_attributes["key"] == "some_json_value"
- assert (
- self.sensor.device_state_attributes["another_key"] == "another_json_value"
- )
- assert "key_three" not in self.sensor.device_state_attributes
+ 'echo "template_value" "3 4"',
+ shell=True, # nosec # shell by design
+ timeout=15,
+ )
+
+
+async def test_bad_template_render(caplog: Any, hass: HomeAssistantType) -> None:
+ """Test rendering a broken template."""
+
+ await setup_test_entities(
+ hass,
+ {
+ "command": "echo {{ this template doesn't parse",
+ },
+ )
+
+ assert "Error rendering command template" in caplog.text
+
+
+async def test_bad_command(hass: HomeAssistantType) -> None:
+ """Test bad command."""
+ await setup_test_entities(
+ hass,
+ {
+ "command": "asdfasdf",
+ },
+ )
+ entity_state = hass.states.get("sensor.test")
+ assert entity_state
+ assert entity_state.state == "unknown"
+
+
+async def test_update_with_json_attrs(hass: HomeAssistantType) -> None:
+ """Test attributes get extracted from a JSON result."""
+ await setup_test_entities(
+ hass,
+ {
+ "command": 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\":\
+ \\"another_json_value\\", \\"key_three\\": \\"value_three\\" }',
+ "json_attributes": ["key", "another_key", "key_three"],
+ },
+ )
+ entity_state = hass.states.get("sensor.test")
+ assert entity_state
+ assert entity_state.attributes["key"] == "some_json_value"
+ assert entity_state.attributes["another_key"] == "another_json_value"
+ assert entity_state.attributes["key_three"] == "value_three"
+
+
+async def test_update_with_json_attrs_no_data(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def]
+ """Test attributes when no JSON result fetched."""
+
+ await setup_test_entities(
+ hass,
+ {
+ "command": "echo",
+ "json_attributes": ["key"],
+ },
+ )
+ entity_state = hass.states.get("sensor.test")
+ assert entity_state
+ assert "key" not in entity_state.attributes
+ assert "Empty reply found when expecting JSON data" in caplog.text
+
+
+async def test_update_with_json_attrs_not_dict(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def]
+ """Test attributes when the return value not a dict."""
+
+ await setup_test_entities(
+ hass,
+ {
+ "command": "echo [1, 2, 3]",
+ "json_attributes": ["key"],
+ },
+ )
+ entity_state = hass.states.get("sensor.test")
+ assert entity_state
+ assert "key" not in entity_state.attributes
+ assert "JSON result was not a dictionary" in caplog.text
+
+
+async def test_update_with_json_attrs_bad_json(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def]
+ """Test attributes when the return value is invalid JSON."""
+
+ await setup_test_entities(
+ hass,
+ {
+ "command": "echo This is text rather than JSON data.",
+ "json_attributes": ["key"],
+ },
+ )
+ entity_state = hass.states.get("sensor.test")
+ assert entity_state
+ assert "key" not in entity_state.attributes
+ assert "Unable to parse output as JSON" in caplog.text
+
+
+async def test_update_with_missing_json_attrs(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def]
+ """Test attributes when an expected key is missing."""
+
+ await setup_test_entities(
+ hass,
+ {
+ "command": 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\":\
+ \\"another_json_value\\", \\"key_three\\": \\"value_three\\" }',
+ "json_attributes": ["key", "another_key", "key_three", "missing_key"],
+ },
+ )
+ entity_state = hass.states.get("sensor.test")
+ assert entity_state
+ assert entity_state.attributes["key"] == "some_json_value"
+ assert entity_state.attributes["another_key"] == "another_json_value"
+ assert entity_state.attributes["key_three"] == "value_three"
+ assert "missing_key" not in entity_state.attributes
+
+
+async def test_update_with_unnecessary_json_attrs(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def]
+ """Test attributes when an expected key is missing."""
+
+ await setup_test_entities(
+ hass,
+ {
+ "command": 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\":\
+ \\"another_json_value\\", \\"key_three\\": \\"value_three\\" }',
+ "json_attributes": ["key", "another_key"],
+ },
+ )
+ entity_state = hass.states.get("sensor.test")
+ assert entity_state
+ assert entity_state.attributes["key"] == "some_json_value"
+ assert entity_state.attributes["another_key"] == "another_json_value"
+ assert "key_three" not in entity_state.attributes
diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py
index c6d315b05b59e1..0e31999f928890 100644
--- a/tests/components/command_line/test_switch.py
+++ b/tests/components/command_line/test_switch.py
@@ -1,10 +1,12 @@
"""The tests for the Command line switch platform."""
import json
import os
+import subprocess
import tempfile
+from unittest.mock import patch
-import homeassistant.components.command_line.switch as command_line
-import homeassistant.components.switch as switch
+from homeassistant import setup
+from homeassistant.components.switch import DOMAIN, SCAN_INTERVAL
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
@@ -12,230 +14,358 @@
STATE_OFF,
STATE_ON,
)
-from homeassistant.setup import async_setup_component
+from homeassistant.helpers.typing import Any, Dict, HomeAssistantType
+import homeassistant.util.dt as dt_util
+from tests.common import async_fire_time_changed
-async def test_state_none(hass):
+
+async def setup_test_entity(
+ hass: HomeAssistantType, config_dict: Dict[str, Any]
+) -> None:
+ """Set up a test command line switch entity."""
+ assert await setup.async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ DOMAIN: [
+ {"platform": "command_line", "switches": config_dict},
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+
+
+async def test_state_none(hass: HomeAssistantType) -> None:
"""Test with none state."""
with tempfile.TemporaryDirectory() as tempdirname:
path = os.path.join(tempdirname, "switch_status")
- test_switch = {
- "command_on": f"echo 1 > {path}",
- "command_off": f"echo 0 > {path}",
- }
- assert await async_setup_component(
+ await setup_test_entity(
hass,
- switch.DOMAIN,
{
- "switch": {
- "platform": "command_line",
- "switches": {"test": test_switch},
+ "test": {
+ "command_on": f"echo 1 > {path}",
+ "command_off": f"echo 0 > {path}",
}
},
)
- await hass.async_block_till_done()
- state = hass.states.get("switch.test")
- assert STATE_OFF == state.state
+ entity_state = hass.states.get("switch.test")
+ assert entity_state
+ assert entity_state.state == STATE_OFF
await hass.services.async_call(
- switch.DOMAIN,
+ DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "switch.test"},
blocking=True,
)
- state = hass.states.get("switch.test")
- assert STATE_ON == state.state
+ entity_state = hass.states.get("switch.test")
+ assert entity_state
+ assert entity_state.state == STATE_ON
await hass.services.async_call(
- switch.DOMAIN,
+ DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.test"},
blocking=True,
)
- state = hass.states.get("switch.test")
- assert STATE_OFF == state.state
+ entity_state = hass.states.get("switch.test")
+ assert entity_state
+ assert entity_state.state == STATE_OFF
-async def test_state_value(hass):
+async def test_state_value(hass: HomeAssistantType) -> None:
"""Test with state value."""
with tempfile.TemporaryDirectory() as tempdirname:
path = os.path.join(tempdirname, "switch_status")
- test_switch = {
- "command_state": f"cat {path}",
- "command_on": f"echo 1 > {path}",
- "command_off": f"echo 0 > {path}",
- "value_template": '{{ value=="1" }}',
- }
- assert await async_setup_component(
+ await setup_test_entity(
hass,
- switch.DOMAIN,
{
- "switch": {
- "platform": "command_line",
- "switches": {"test": test_switch},
+ "test": {
+ "command_state": f"cat {path}",
+ "command_on": f"echo 1 > {path}",
+ "command_off": f"echo 0 > {path}",
+ "value_template": '{{ value=="1" }}',
}
},
)
- await hass.async_block_till_done()
- state = hass.states.get("switch.test")
- assert STATE_OFF == state.state
+ entity_state = hass.states.get("switch.test")
+ assert entity_state
+ assert entity_state.state == STATE_OFF
await hass.services.async_call(
- switch.DOMAIN,
+ DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "switch.test"},
blocking=True,
)
- state = hass.states.get("switch.test")
- assert STATE_ON == state.state
+ entity_state = hass.states.get("switch.test")
+ assert entity_state
+ assert entity_state.state == STATE_ON
await hass.services.async_call(
- switch.DOMAIN,
+ DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.test"},
blocking=True,
)
- state = hass.states.get("switch.test")
- assert STATE_OFF == state.state
+ entity_state = hass.states.get("switch.test")
+ assert entity_state
+ assert entity_state.state == STATE_OFF
-async def test_state_json_value(hass):
+async def test_state_json_value(hass: HomeAssistantType) -> None:
"""Test with state JSON value."""
with tempfile.TemporaryDirectory() as tempdirname:
path = os.path.join(tempdirname, "switch_status")
oncmd = json.dumps({"status": "ok"})
offcmd = json.dumps({"status": "nope"})
- test_switch = {
- "command_state": f"cat {path}",
- "command_on": f"echo '{oncmd}' > {path}",
- "command_off": f"echo '{offcmd}' > {path}",
- "value_template": '{{ value_json.status=="ok" }}',
- }
- assert await async_setup_component(
+
+ await setup_test_entity(
hass,
- switch.DOMAIN,
{
- "switch": {
- "platform": "command_line",
- "switches": {"test": test_switch},
+ "test": {
+ "command_state": f"cat {path}",
+ "command_on": f"echo '{oncmd}' > {path}",
+ "command_off": f"echo '{offcmd}' > {path}",
+ "value_template": '{{ value_json.status=="ok" }}',
}
},
)
- await hass.async_block_till_done()
- state = hass.states.get("switch.test")
- assert STATE_OFF == state.state
+ entity_state = hass.states.get("switch.test")
+ assert entity_state
+ assert entity_state.state == STATE_OFF
await hass.services.async_call(
- switch.DOMAIN,
+ DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "switch.test"},
blocking=True,
)
- state = hass.states.get("switch.test")
- assert STATE_ON == state.state
+ entity_state = hass.states.get("switch.test")
+ assert entity_state
+ assert entity_state.state == STATE_ON
await hass.services.async_call(
- switch.DOMAIN,
+ DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.test"},
blocking=True,
)
- state = hass.states.get("switch.test")
- assert STATE_OFF == state.state
+ entity_state = hass.states.get("switch.test")
+ assert entity_state
+ assert entity_state.state == STATE_OFF
-async def test_state_code(hass):
+async def test_state_code(hass: HomeAssistantType) -> None:
"""Test with state code."""
with tempfile.TemporaryDirectory() as tempdirname:
path = os.path.join(tempdirname, "switch_status")
- test_switch = {
- "command_state": f"cat {path}",
- "command_on": f"echo 1 > {path}",
- "command_off": f"echo 0 > {path}",
- }
- assert await async_setup_component(
+ await setup_test_entity(
hass,
- switch.DOMAIN,
{
- "switch": {
- "platform": "command_line",
- "switches": {"test": test_switch},
+ "test": {
+ "command_state": f"cat {path}",
+ "command_on": f"echo 1 > {path}",
+ "command_off": f"echo 0 > {path}",
}
},
)
- await hass.async_block_till_done()
- state = hass.states.get("switch.test")
- assert STATE_OFF == state.state
+ entity_state = hass.states.get("switch.test")
+ assert entity_state
+ assert entity_state.state == STATE_OFF
await hass.services.async_call(
- switch.DOMAIN,
+ DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "switch.test"},
blocking=True,
)
- state = hass.states.get("switch.test")
- assert STATE_ON == state.state
+ entity_state = hass.states.get("switch.test")
+ assert entity_state
+ assert entity_state.state == STATE_ON
await hass.services.async_call(
- switch.DOMAIN,
+ DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.test"},
blocking=True,
)
- state = hass.states.get("switch.test")
- assert STATE_ON == state.state
+ entity_state = hass.states.get("switch.test")
+ assert entity_state
+ assert entity_state.state == STATE_ON
+
+async def test_assumed_state_should_be_true_if_command_state_is_none(
+ hass: HomeAssistantType,
+) -> None:
+ """Test with state value."""
-def test_assumed_state_should_be_true_if_command_state_is_none(hass):
+ await setup_test_entity(
+ hass,
+ {
+ "test": {
+ "command_on": "echo 'on command'",
+ "command_off": "echo 'off command'",
+ }
+ },
+ )
+ entity_state = hass.states.get("switch.test")
+ assert entity_state
+ assert entity_state.attributes["assumed_state"]
+
+
+async def test_assumed_state_should_absent_if_command_state_present(
+ hass: HomeAssistantType,
+) -> None:
"""Test with state value."""
- # args: hass, device_name, friendly_name, command_on, command_off,
- # command_state, value_template
- init_args = [
+
+ await setup_test_entity(
+ hass,
+ {
+ "test": {
+ "command_on": "echo 'on command'",
+ "command_off": "echo 'off command'",
+ "command_state": "cat {}",
+ }
+ },
+ )
+ entity_state = hass.states.get("switch.test")
+ assert entity_state
+ assert "assumed_state" not in entity_state.attributes
+
+
+async def test_name_is_set_correctly(hass: HomeAssistantType) -> None:
+ """Test that name is set correctly."""
+ await setup_test_entity(
+ hass,
+ {
+ "test": {
+ "command_on": "echo 'on command'",
+ "command_off": "echo 'off command'",
+ "friendly_name": "Test friendly name!",
+ }
+ },
+ )
+
+ entity_state = hass.states.get("switch.test")
+ assert entity_state.name == "Test friendly name!"
+
+
+async def test_switch_command_state_fail(caplog: Any, hass: HomeAssistantType) -> None:
+ """Test that switch failures are handled correctly."""
+ await setup_test_entity(
hass,
- "test_device_name",
- "Test friendly name!",
- "echo 'on command'",
- "echo 'off command'",
- None,
- None,
- 15,
- ]
+ {
+ "test": {
+ "command_on": "exit 0",
+ "command_off": "exit 0'",
+ "command_state": "echo 1",
+ }
+ },
+ )
+
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ entity_state = hass.states.get("switch.test")
+ assert entity_state.state == "on"
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.test"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ entity_state = hass.states.get("switch.test")
+ assert entity_state.state == "on"
+
+ assert "Command failed" in caplog.text
+
+
+async def test_switch_command_state_code_exceptions(
+ caplog: Any, hass: HomeAssistantType
+) -> None:
+ """Test that switch state code exceptions are handled correctly."""
+
+ with patch(
+ "homeassistant.components.command_line.subprocess.check_output",
+ side_effect=[
+ subprocess.TimeoutExpired("cmd", 10),
+ subprocess.SubprocessError(),
+ ],
+ ) as check_output:
+ await setup_test_entity(
+ hass,
+ {
+ "test": {
+ "command_on": "exit 0",
+ "command_off": "exit 0'",
+ "command_state": "echo 1",
+ }
+ },
+ )
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
+ await hass.async_block_till_done()
+ assert check_output.called
+ assert "Timeout for command" in caplog.text
- no_state_device = command_line.CommandSwitch(*init_args)
- assert no_state_device.assumed_state
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 2)
+ await hass.async_block_till_done()
+ assert check_output.called
+ assert "Error trying to exec command" in caplog.text
+
+
+async def test_switch_command_state_value_exceptions(
+ caplog: Any, hass: HomeAssistantType
+) -> None:
+ """Test that switch state value exceptions are handled correctly."""
+
+ with patch(
+ "homeassistant.components.command_line.subprocess.check_output",
+ side_effect=[
+ subprocess.TimeoutExpired("cmd", 10),
+ subprocess.SubprocessError(),
+ ],
+ ) as check_output:
+ await setup_test_entity(
+ hass,
+ {
+ "test": {
+ "command_on": "exit 0",
+ "command_off": "exit 0'",
+ "command_state": "echo 1",
+ "value_template": '{{ value=="1" }}',
+ }
+ },
+ )
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
+ await hass.async_block_till_done()
+ assert check_output.call_count == 1
+ assert "Timeout for command" in caplog.text
- # Set state command
- init_args[-3] = "cat {}"
+ async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 2)
+ await hass.async_block_till_done()
+ assert check_output.call_count == 2
+ assert "Error trying to exec command" in caplog.text
- state_device = command_line.CommandSwitch(*init_args)
- assert not state_device.assumed_state
+async def test_no_switches(caplog: Any, hass: HomeAssistantType) -> None:
+ """Test with no switches."""
-def test_entity_id_set_correctly(hass):
- """Test that entity_id is set correctly from object_id."""
- init_args = [
- hass,
- "test_device_name",
- "Test friendly name!",
- "echo 'on command'",
- "echo 'off command'",
- False,
- None,
- 15,
- ]
-
- test_switch = command_line.CommandSwitch(*init_args)
- assert test_switch.entity_id == "switch.test_device_name"
- assert test_switch.name == "Test friendly name!"
+ await setup_test_entity(hass, {})
+ assert "No switches" in caplog.text
diff --git a/tests/components/compensation/__init__.py b/tests/components/compensation/__init__.py
new file mode 100644
index 00000000000000..55d365adc0e582
--- /dev/null
+++ b/tests/components/compensation/__init__.py
@@ -0,0 +1 @@
+"""Tests for the compensation component."""
diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py
new file mode 100644
index 00000000000000..3bd86280750ff7
--- /dev/null
+++ b/tests/components/compensation/test_sensor.py
@@ -0,0 +1,228 @@
+"""The tests for the integration sensor platform."""
+
+from homeassistant.components.compensation.const import CONF_PRECISION, DOMAIN
+from homeassistant.components.compensation.sensor import ATTR_COEFFICIENTS
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.const import (
+ ATTR_UNIT_OF_MEASUREMENT,
+ EVENT_HOMEASSISTANT_START,
+ EVENT_STATE_CHANGED,
+ STATE_UNKNOWN,
+)
+from homeassistant.setup import async_setup_component
+
+
+async def test_linear_state(hass):
+ """Test compensation sensor state."""
+ config = {
+ "compensation": {
+ "test": {
+ "source": "sensor.uncompensated",
+ "data_points": [
+ [1.0, 2.0],
+ [2.0, 3.0],
+ ],
+ "precision": 2,
+ "unit_of_measurement": "a",
+ }
+ }
+ }
+ expected_entity_id = "sensor.compensation_sensor_uncompensated"
+
+ assert await async_setup_component(hass, DOMAIN, config)
+ assert await async_setup_component(hass, SENSOR_DOMAIN, config)
+ await hass.async_block_till_done()
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ entity_id = config[DOMAIN]["test"]["source"]
+ hass.states.async_set(entity_id, 4, {})
+ await hass.async_block_till_done()
+
+ state = hass.states.get(expected_entity_id)
+ assert state is not None
+
+ assert round(float(state.state), config[DOMAIN]["test"][CONF_PRECISION]) == 5.0
+
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "a"
+
+ coefs = [round(v, 1) for v in state.attributes.get(ATTR_COEFFICIENTS)]
+ assert coefs == [1.0, 1.0]
+
+ hass.states.async_set(entity_id, "foo", {})
+ await hass.async_block_till_done()
+
+ state = hass.states.get(expected_entity_id)
+ assert state is not None
+
+ assert state.state == STATE_UNKNOWN
+
+
+async def test_linear_state_from_attribute(hass):
+ """Test compensation sensor state that pulls from attribute."""
+ config = {
+ "compensation": {
+ "test": {
+ "source": "sensor.uncompensated",
+ "attribute": "value",
+ "data_points": [
+ [1.0, 2.0],
+ [2.0, 3.0],
+ ],
+ "precision": 2,
+ }
+ }
+ }
+ expected_entity_id = "sensor.compensation_sensor_uncompensated_value"
+
+ assert await async_setup_component(hass, DOMAIN, config)
+ assert await async_setup_component(hass, SENSOR_DOMAIN, config)
+ await hass.async_block_till_done()
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+
+ entity_id = config[DOMAIN]["test"]["source"]
+ hass.states.async_set(entity_id, 3, {"value": 4})
+ await hass.async_block_till_done()
+
+ state = hass.states.get(expected_entity_id)
+ assert state is not None
+
+ assert round(float(state.state), config[DOMAIN]["test"][CONF_PRECISION]) == 5.0
+
+ coefs = [round(v, 1) for v in state.attributes.get(ATTR_COEFFICIENTS)]
+ assert coefs == [1.0, 1.0]
+
+ hass.states.async_set(entity_id, 3, {"value": "bar"})
+ await hass.async_block_till_done()
+
+ state = hass.states.get(expected_entity_id)
+ assert state is not None
+
+ assert state.state == STATE_UNKNOWN
+
+
+async def test_quadratic_state(hass):
+ """Test 3 degree polynominial compensation sensor."""
+ config = {
+ "compensation": {
+ "test": {
+ "source": "sensor.temperature",
+ "data_points": [
+ [50, 3.3],
+ [50, 2.8],
+ [50, 2.9],
+ [70, 2.3],
+ [70, 2.6],
+ [70, 2.1],
+ [80, 2.5],
+ [80, 2.9],
+ [80, 2.4],
+ [90, 3.0],
+ [90, 3.1],
+ [90, 2.8],
+ [100, 3.3],
+ [100, 3.5],
+ [100, 3.0],
+ ],
+ "degree": 2,
+ "precision": 3,
+ }
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ entity_id = config[DOMAIN]["test"]["source"]
+ hass.states.async_set(entity_id, 43.2, {})
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.compensation_sensor_temperature")
+
+ assert state is not None
+
+ assert round(float(state.state), config[DOMAIN]["test"][CONF_PRECISION]) == 3.327
+
+
+async def test_numpy_errors(hass, caplog):
+ """Tests bad polyfits."""
+ config = {
+ "compensation": {
+ "test": {
+ "source": "sensor.uncompensated",
+ "data_points": [
+ [1.0, 1.0],
+ [1.0, 1.0],
+ ],
+ },
+ "test2": {
+ "source": "sensor.uncompensated2",
+ "data_points": [
+ [0.0, 1.0],
+ [0.0, 1.0],
+ ],
+ },
+ }
+ }
+ await async_setup_component(hass, DOMAIN, config)
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert "polyfit may be poorly conditioned" in caplog.text
+
+ assert "invalid value encountered in true_divide" in caplog.text
+
+
+async def test_datapoints_greater_than_degree(hass, caplog):
+ """Tests 3 bad data points."""
+ config = {
+ "compensation": {
+ "test": {
+ "source": "sensor.uncompensated",
+ "data_points": [
+ [1.0, 2.0],
+ [2.0, 3.0],
+ ],
+ "degree": 2,
+ },
+ }
+ }
+ await async_setup_component(hass, DOMAIN, config)
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert "data_points must have at least 3 data_points" in caplog.text
+
+
+async def test_new_state_is_none(hass):
+ """Tests catch for empty new states."""
+ config = {
+ "compensation": {
+ "test": {
+ "source": "sensor.uncompensated",
+ "data_points": [
+ [1.0, 2.0],
+ [2.0, 3.0],
+ ],
+ "precision": 2,
+ "unit_of_measurement": "a",
+ }
+ }
+ }
+ expected_entity_id = "sensor.compensation_sensor_uncompensated"
+
+ await async_setup_component(hass, DOMAIN, config)
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ last_changed = hass.states.get(expected_entity_id).last_changed
+
+ hass.bus.async_fire(
+ EVENT_STATE_CHANGED, event_data={"entity_id": "sensor.uncompensated"}
+ )
+
+ assert last_changed == hass.states.get(expected_entity_id).last_changed
diff --git a/tests/components/config/test_area_registry.py b/tests/components/config/test_area_registry.py
index f66e16e606fc2e..35176cc79f90fd 100644
--- a/tests/components/config/test_area_registry.py
+++ b/tests/components/config/test_area_registry.py
@@ -55,7 +55,7 @@ async def test_create_area_with_name_already_in_use(hass, client, registry):
assert not msg["success"]
assert msg["error"]["code"] == "invalid_info"
- assert msg["error"]["message"] == "Name is already in use"
+ assert msg["error"]["message"] == "The name mock (mock) is already in use"
assert len(registry.areas) == 1
@@ -147,5 +147,5 @@ async def test_update_area_with_name_already_in_use(hass, client, registry):
assert not msg["success"]
assert msg["error"]["code"] == "invalid_info"
- assert msg["error"]["message"] == "Name is already in use"
+ assert msg["error"]["message"] == "The name mock 2 (mock2) is already in use"
assert len(registry.areas) == 2
diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py
index 00c89edeef0642..6aeb71a7fd0551 100644
--- a/tests/components/config/test_automation.py
+++ b/tests/components/config/test_automation.py
@@ -4,8 +4,9 @@
from homeassistant.bootstrap import async_setup_component
from homeassistant.components import config
+from homeassistant.helpers import entity_registry as er
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
async def test_get_device_config(hass, hass_client):
@@ -110,7 +111,7 @@ def mock_write(path, data):
async def test_delete_automation(hass, hass_client):
"""Test deleting an automation."""
- ent_reg = await hass.helpers.entity_registry.async_get_registry()
+ ent_reg = er.async_get(hass)
assert await async_setup_component(
hass,
diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py
index 87b1559a21b126..128d0798b660f4 100644
--- a/tests/components/config/test_config_entries.py
+++ b/tests/components/config/test_config_entries.py
@@ -68,6 +68,12 @@ def async_get_options_flow(config, options):
state=core_ce.ENTRY_STATE_LOADED,
connection_class=core_ce.CONN_CLASS_ASSUMED,
).add_to_hass(hass)
+ MockConfigEntry(
+ domain="comp3",
+ title="Test 3",
+ source="bla3",
+ disabled_by="user",
+ ).add_to_hass(hass)
resp = await client.get("/api/config/config_entries/entry")
assert resp.status == 200
@@ -83,6 +89,7 @@ def async_get_options_flow(config, options):
"connection_class": "local_poll",
"supports_options": True,
"supports_unload": True,
+ "disabled_by": None,
},
{
"domain": "comp2",
@@ -92,6 +99,17 @@ def async_get_options_flow(config, options):
"connection_class": "assumed",
"supports_options": False,
"supports_unload": False,
+ "disabled_by": None,
+ },
+ {
+ "domain": "comp3",
+ "title": "Test 3",
+ "source": "bla3",
+ "state": "not_loaded",
+ "connection_class": "unknown",
+ "supports_options": False,
+ "supports_unload": False,
+ "disabled_by": "user",
},
]
@@ -302,7 +320,17 @@ async def async_step_user(self, user_input=None):
"title": "Test Entry",
"type": "create_entry",
"version": 1,
- "result": entries[0].entry_id,
+ "result": {
+ "connection_class": "unknown",
+ "disabled_by": None,
+ "domain": "test",
+ "entry_id": entries[0].entry_id,
+ "source": "user",
+ "state": "loaded",
+ "supports_options": False,
+ "supports_unload": False,
+ "title": "Test Entry",
+ },
"description": None,
"description_placeholders": None,
}
@@ -361,7 +389,17 @@ async def async_step_account(self, user_input=None):
"type": "create_entry",
"title": "user-title",
"version": 1,
- "result": entries[0].entry_id,
+ "result": {
+ "connection_class": "unknown",
+ "disabled_by": None,
+ "domain": "test",
+ "entry_id": entries[0].entry_id,
+ "source": "user",
+ "state": "loaded",
+ "supports_options": False,
+ "supports_unload": False,
+ "title": "user-title",
+ },
"description": None,
"description_placeholders": None,
}
@@ -551,7 +589,7 @@ async def async_step_init(self, user_input=None):
source="bla",
connection_class=core_ce.CONN_CLASS_LOCAL_POLL,
).add_to_hass(hass)
- entry = hass.config_entries._entries[0]
+ entry = hass.config_entries.async_entries()[0]
with patch.dict(HANDLERS, {"test": TestFlow}):
url = "/api/config/config_entries/options/flow"
@@ -600,7 +638,7 @@ async def async_step_finish(self, user_input=None):
source="bla",
connection_class=core_ce.CONN_CLASS_LOCAL_POLL,
).add_to_hass(hass)
- entry = hass.config_entries._entries[0]
+ entry = hass.config_entries.async_entries()[0]
with patch.dict(HANDLERS, {"test": TestFlow}):
url = "/api/config/config_entries/options/flow"
@@ -680,6 +718,25 @@ async def test_update_system_options(hass, hass_ws_client):
assert entry.system_options.disable_new_entities
+async def test_update_system_options_nonexisting(hass, hass_ws_client):
+ """Test that we can update entry."""
+ assert await async_setup_component(hass, "config", {})
+ ws_client = await hass_ws_client(hass)
+
+ await ws_client.send_json(
+ {
+ "id": 5,
+ "type": "config_entries/system_options/update",
+ "entry_id": "non_existing",
+ "disable_new_entities": True,
+ }
+ )
+ response = await ws_client.receive_json()
+
+ assert not response["success"]
+ assert response["error"]["code"] == "not_found"
+
+
async def test_update_entry(hass, hass_ws_client):
"""Test that we can update entry."""
assert await async_setup_component(hass, "config", {})
@@ -722,6 +779,83 @@ async def test_update_entry_nonexisting(hass, hass_ws_client):
assert response["error"]["code"] == "not_found"
+async def test_disable_entry(hass, hass_ws_client):
+ """Test that we can disable entry."""
+ assert await async_setup_component(hass, "config", {})
+ ws_client = await hass_ws_client(hass)
+
+ entry = MockConfigEntry(domain="demo", state="loaded")
+ entry.add_to_hass(hass)
+ assert entry.disabled_by is None
+
+ # Disable
+ await ws_client.send_json(
+ {
+ "id": 5,
+ "type": "config_entries/disable",
+ "entry_id": entry.entry_id,
+ "disabled_by": "user",
+ }
+ )
+ response = await ws_client.receive_json()
+
+ assert response["success"]
+ assert response["result"] == {"require_restart": True}
+ assert entry.disabled_by == "user"
+ assert entry.state == "failed_unload"
+
+ # Enable
+ await ws_client.send_json(
+ {
+ "id": 6,
+ "type": "config_entries/disable",
+ "entry_id": entry.entry_id,
+ "disabled_by": None,
+ }
+ )
+ response = await ws_client.receive_json()
+
+ assert response["success"]
+ assert response["result"] == {"require_restart": True}
+ assert entry.disabled_by is None
+ assert entry.state == "failed_unload"
+
+ # Enable again -> no op
+ await ws_client.send_json(
+ {
+ "id": 7,
+ "type": "config_entries/disable",
+ "entry_id": entry.entry_id,
+ "disabled_by": None,
+ }
+ )
+ response = await ws_client.receive_json()
+
+ assert response["success"]
+ assert response["result"] == {"require_restart": False}
+ assert entry.disabled_by is None
+ assert entry.state == "failed_unload"
+
+
+async def test_disable_entry_nonexisting(hass, hass_ws_client):
+ """Test that we can disable entry."""
+ assert await async_setup_component(hass, "config", {})
+ ws_client = await hass_ws_client(hass)
+
+ await ws_client.send_json(
+ {
+ "id": 5,
+ "type": "config_entries/disable",
+ "entry_id": "non_existing",
+ "disabled_by": "user",
+ }
+ )
+ response = await ws_client.receive_json()
+
+ assert not response["success"]
+ assert response["error"]["code"] == "not_found"
+
+
async def test_ignore_flow(hass, hass_ws_client):
"""Test we can ignore a flow."""
assert await async_setup_component(hass, "config", {})
@@ -763,3 +897,22 @@ async def async_step_user(self, user_input=None):
assert entry.source == "ignore"
assert entry.unique_id == "mock-unique-id"
assert entry.title == "Test Integration"
+
+
+async def test_ignore_flow_nonexisting(hass, hass_ws_client):
+ """Test we can ignore a flow."""
+ assert await async_setup_component(hass, "config", {})
+ ws_client = await hass_ws_client(hass)
+
+ await ws_client.send_json(
+ {
+ "id": 5,
+ "type": "config_entries/ignore_flow",
+ "flow_id": "non_existing",
+ "title": "Test Integration",
+ }
+ )
+ response = await ws_client.receive_json()
+
+ assert not response["success"]
+ assert response["error"]["code"] == "not_found"
diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py
index b2273d640de907..a123a2edb35cf9 100644
--- a/tests/components/config/test_device_registry.py
+++ b/tests/components/config/test_device_registry.py
@@ -4,7 +4,7 @@
from homeassistant.components.config import device_registry
from tests.common import mock_device_registry
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py
index bdb2a2e3f10928..8e4276cc9fd7d5 100644
--- a/tests/components/config/test_scene.py
+++ b/tests/components/config/test_scene.py
@@ -4,6 +4,7 @@
from homeassistant.bootstrap import async_setup_component
from homeassistant.components import config
+from homeassistant.helpers import entity_registry as er
from homeassistant.util.yaml import dump
@@ -114,7 +115,7 @@ def mock_write(path, data):
async def test_delete_scene(hass, hass_client):
"""Test deleting a scene."""
- ent_reg = await hass.helpers.entity_registry.async_get_registry()
+ ent_reg = er.async_get(hass)
assert await async_setup_component(
hass,
diff --git a/tests/components/configurator/test_init.py b/tests/components/configurator/test_init.py
index 8d116c301054b4..65701cbd139819 100644
--- a/tests/components/configurator/test_init.py
+++ b/tests/components/configurator/test_init.py
@@ -8,18 +8,18 @@ async def test_request_least_info(hass):
"""Test request config with least amount of data."""
request_id = configurator.async_request_config(hass, "Test Request", lambda _: None)
- assert 1 == len(
- hass.services.async_services().get(configurator.DOMAIN, [])
+ assert (
+ len(hass.services.async_services().get(configurator.DOMAIN, [])) == 1
), "No new service registered"
states = hass.states.async_all()
- assert 1 == len(states), "Expected a new state registered"
+ assert len(states) == 1, "Expected a new state registered"
state = states[0]
- assert configurator.STATE_CONFIGURE == state.state
- assert request_id == state.attributes.get(configurator.ATTR_CONFIGURE_ID)
+ assert state.state == configurator.STATE_CONFIGURE
+ assert state.attributes.get(configurator.ATTR_CONFIGURE_ID) == request_id
async def test_request_all_info(hass):
@@ -49,11 +49,11 @@ async def test_request_all_info(hass):
}
states = hass.states.async_all()
- assert 1 == len(states)
+ assert len(states) == 1
state = states[0]
- assert configurator.STATE_CONFIGURE == state.state
- assert exp_attr == state.attributes
+ assert state.state == configurator.STATE_CONFIGURE
+ assert state.attributes == exp_attr
async def test_callback_called_on_configure(hass):
@@ -70,7 +70,7 @@ async def test_callback_called_on_configure(hass):
)
await hass.async_block_till_done()
- assert 1 == len(calls), "Callback not called"
+ assert len(calls) == 1, "Callback not called"
async def test_state_change_on_notify_errors(hass):
@@ -80,9 +80,9 @@ async def test_state_change_on_notify_errors(hass):
configurator.async_notify_errors(hass, request_id, error)
states = hass.states.async_all()
- assert 1 == len(states)
+ assert len(states) == 1
state = states[0]
- assert error == state.attributes.get(configurator.ATTR_ERRORS)
+ assert state.attributes.get(configurator.ATTR_ERRORS) == error
async def test_notify_errors_fail_silently_on_bad_request_id(hass):
@@ -94,11 +94,11 @@ async def test_request_done_works(hass):
"""Test if calling request done works."""
request_id = configurator.async_request_config(hass, "Test Request", lambda _: None)
configurator.async_request_done(hass, request_id)
- assert 1 == len(hass.states.async_all())
+ assert len(hass.states.async_all()) == 1
hass.bus.async_fire(EVENT_TIME_CHANGED)
await hass.async_block_till_done()
- assert 0 == len(hass.states.async_all())
+ assert len(hass.states.async_all()) == 0
async def test_request_done_fail_silently_on_bad_request_id(hass):
diff --git a/tests/components/control4/test_config_flow.py b/tests/components/control4/test_config_flow.py
index 48ff82011665e6..5ffdc4f1ea8f6d 100644
--- a/tests/components/control4/test_config_flow.py
+++ b/tests/components/control4/test_config_flow.py
@@ -62,8 +62,6 @@ async def test_form(hass):
"homeassistant.components.control4.config_flow.C4Director",
return_value=c4_director,
), patch(
- "homeassistant.components.control4.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.control4.async_setup_entry",
return_value=True,
) as mock_setup_entry:
@@ -85,7 +83,6 @@ async def test_form(hass):
CONF_PASSWORD: "test-password",
"controller_unique_id": "control4_model_00AA00AA00AA",
}
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py
index 3dd0f27ecdf4b2..1c2abaf83899ef 100644
--- a/tests/components/coolmaster/test_config_flow.py
+++ b/tests/components/coolmaster/test_config_flow.py
@@ -24,8 +24,6 @@ async def test_form(hass):
"homeassistant.components.coolmaster.config_flow.CoolMasterNet.status",
return_value={"test_id": "test_unit"},
), patch(
- "homeassistant.components.coolmaster.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.coolmaster.async_setup_entry",
return_value=True,
) as mock_setup_entry:
@@ -41,7 +39,6 @@ async def test_form(hass):
"port": 10102,
"supported_modes": AVAILABLE_MODES,
}
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
diff --git a/tests/components/coronavirus/test_init.py b/tests/components/coronavirus/test_init.py
index 483fabab9f9c21..cc49bf7d4b6fed 100644
--- a/tests/components/coronavirus/test_init.py
+++ b/tests/components/coronavirus/test_init.py
@@ -1,6 +1,6 @@
"""Test init of Coronavirus integration."""
from homeassistant.components.coronavirus.const import DOMAIN, OPTION_WORLDWIDE
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, mock_registry
@@ -17,13 +17,13 @@ async def test_migration(hass):
mock_registry(
hass,
{
- "sensor.netherlands_confirmed": entity_registry.RegistryEntry(
+ "sensor.netherlands_confirmed": er.RegistryEntry(
entity_id="sensor.netherlands_confirmed",
unique_id="34-confirmed",
platform="coronavirus",
config_entry_id=nl_entry.entry_id,
),
- "sensor.worldwide_confirmed": entity_registry.RegistryEntry(
+ "sensor.worldwide_confirmed": er.RegistryEntry(
entity_id="sensor.worldwide_confirmed",
unique_id="__worldwide-confirmed",
platform="coronavirus",
@@ -34,7 +34,7 @@ async def test_migration(hass):
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
sensor_nl = ent_reg.async_get("sensor.netherlands_confirmed")
assert sensor_nl.unique_id == "Netherlands-confirmed"
diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py
index d6a41af6deb2c1..107dd97924d554 100644
--- a/tests/components/counter/test_init.py
+++ b/tests/components/counter/test_init.py
@@ -21,7 +21,7 @@
)
from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_NAME
from homeassistant.core import Context, CoreState, State
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from tests.common import mock_restore_cache
@@ -114,16 +114,16 @@ async def test_config_options(hass):
assert state_2 is not None
assert state_3 is not None
- assert 0 == int(state_1.state)
+ assert int(state_1.state) == 0
assert ATTR_ICON not in state_1.attributes
assert ATTR_FRIENDLY_NAME not in state_1.attributes
- assert 10 == int(state_2.state)
- assert "Hello World" == state_2.attributes.get(ATTR_FRIENDLY_NAME)
- assert "mdi:work" == state_2.attributes.get(ATTR_ICON)
+ assert int(state_2.state) == 10
+ assert state_2.attributes.get(ATTR_FRIENDLY_NAME) == "Hello World"
+ assert state_2.attributes.get(ATTR_ICON) == "mdi:work"
- assert DEFAULT_INITIAL == state_3.attributes.get(ATTR_INITIAL)
- assert DEFAULT_STEP == state_3.attributes.get(ATTR_STEP)
+ assert state_3.attributes.get(ATTR_INITIAL) == DEFAULT_INITIAL
+ assert state_3.attributes.get(ATTR_STEP) == DEFAULT_STEP
async def test_methods(hass):
@@ -135,31 +135,31 @@ async def test_methods(hass):
entity_id = "counter.test_1"
state = hass.states.get(entity_id)
- assert 0 == int(state.state)
+ assert int(state.state) == 0
async_increment(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert 1 == int(state.state)
+ assert int(state.state) == 1
async_increment(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert 2 == int(state.state)
+ assert int(state.state) == 2
async_decrement(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert 1 == int(state.state)
+ assert int(state.state) == 1
async_reset(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert 0 == int(state.state)
+ assert int(state.state) == 0
async def test_methods_with_config(hass):
@@ -173,25 +173,25 @@ async def test_methods_with_config(hass):
entity_id = "counter.test"
state = hass.states.get(entity_id)
- assert 10 == int(state.state)
+ assert int(state.state) == 10
async_increment(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert 15 == int(state.state)
+ assert int(state.state) == 15
async_increment(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert 20 == int(state.state)
+ assert int(state.state) == 20
async_decrement(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert 15 == int(state.state)
+ assert int(state.state) == 15
async def test_initial_state_overrules_restore_state(hass):
@@ -370,7 +370,7 @@ async def test_configure(hass, hass_admin_user):
state = hass.states.get("counter.test")
assert state is not None
assert state.state == "10"
- assert 10 == state.attributes.get("maximum")
+ assert state.attributes.get("maximum") == 10
# update max
await hass.services.async_call(
@@ -384,7 +384,7 @@ async def test_configure(hass, hass_admin_user):
state = hass.states.get("counter.test")
assert state is not None
assert state.state == "0"
- assert 0 == state.attributes.get("maximum")
+ assert state.attributes.get("maximum") == 0
# disable max
await hass.services.async_call(
@@ -413,7 +413,7 @@ async def test_configure(hass, hass_admin_user):
state = hass.states.get("counter.test")
assert state is not None
assert state.state == "5"
- assert 5 == state.attributes.get("minimum")
+ assert state.attributes.get("minimum") == 5
# disable min
await hass.services.async_call(
@@ -430,7 +430,7 @@ async def test_configure(hass, hass_admin_user):
assert state.attributes.get("minimum") is None
# update step
- assert 1 == state.attributes.get("step")
+ assert state.attributes.get("step") == 1
await hass.services.async_call(
"counter",
"configure",
@@ -442,7 +442,7 @@ async def test_configure(hass, hass_admin_user):
state = hass.states.get("counter.test")
assert state is not None
assert state.state == "5"
- assert 3 == state.attributes.get("step")
+ assert state.attributes.get("step") == 3
# update value
await hass.services.async_call(
@@ -469,7 +469,7 @@ async def test_configure(hass, hass_admin_user):
state = hass.states.get("counter.test")
assert state is not None
assert state.state == "6"
- assert 5 == state.attributes.get("initial")
+ assert state.attributes.get("initial") == 5
# update all
await hass.services.async_call(
@@ -490,10 +490,10 @@ async def test_configure(hass, hass_admin_user):
state = hass.states.get("counter.test")
assert state is not None
assert state.state == "5"
- assert 5 == state.attributes.get("step")
- assert 0 == state.attributes.get("minimum")
- assert 9 == state.attributes.get("maximum")
- assert 6 == state.attributes.get("initial")
+ assert state.attributes.get("step") == 5
+ assert state.attributes.get("minimum") == 0
+ assert state.attributes.get("maximum") == 9
+ assert state.attributes.get("initial") == 6
async def test_load_from_storage(hass, storage_setup):
@@ -569,7 +569,7 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup):
input_id = "from_storage"
input_entity_id = f"{DOMAIN}.{input_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state is not None
@@ -606,7 +606,7 @@ async def test_update_min_max(hass, hass_ws_client, storage_setup):
input_id = "from_storage"
input_entity_id = f"{DOMAIN}.{input_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state is not None
@@ -683,7 +683,7 @@ async def test_create(hass, hass_ws_client, storage_setup):
counter_id = "new_counter"
input_entity_id = f"{DOMAIN}.{counter_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state is None
diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py
index cad6074ff34806..5cec3d901e1180 100644
--- a/tests/components/cover/test_device_action.py
+++ b/tests/components/cover/test_device_action.py
@@ -16,7 +16,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py
index b3098ceeca97a9..04415661515965 100644
--- a/tests/components/cover/test_device_condition.py
+++ b/tests/components/cover/test_device_condition.py
@@ -22,7 +22,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py
index ab054ad8223bff..7ff5a434e5b81d 100644
--- a/tests/components/cover/test_device_trigger.py
+++ b/tests/components/cover/test_device_trigger.py
@@ -1,4 +1,6 @@
"""The tests for Cover device triggers."""
+from datetime import timedelta
+
import pytest
import homeassistant.components.automation as automation
@@ -12,17 +14,19 @@
)
from homeassistant.helpers import device_registry
from homeassistant.setup import async_setup_component
+import homeassistant.util.dt as dt_util
from tests.common import (
MockConfigEntry,
assert_lists_same,
+ async_fire_time_changed,
async_get_device_automation_capabilities,
async_get_device_automations,
async_mock_service,
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
@@ -234,7 +238,11 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg):
capabilities = await async_get_device_automation_capabilities(
hass, "trigger", trigger
)
- assert capabilities == {"extra_fields": []}
+ assert capabilities == {
+ "extra_fields": [
+ {"name": "for", "optional": True, "type": "positive_time_period_dict"}
+ ]
+ }
async def test_get_trigger_capabilities_set_pos(hass, device_reg, entity_reg):
@@ -284,7 +292,15 @@ async def test_get_trigger_capabilities_set_pos(hass, device_reg, entity_reg):
if trigger["type"] == "position":
assert capabilities == expected_capabilities
else:
- assert capabilities == {"extra_fields": []}
+ assert capabilities == {
+ "extra_fields": [
+ {
+ "name": "for",
+ "optional": True,
+ "type": "positive_time_period_dict",
+ }
+ ]
+ }
async def test_get_trigger_capabilities_set_tilt_pos(hass, device_reg, entity_reg):
@@ -334,7 +350,15 @@ async def test_get_trigger_capabilities_set_tilt_pos(hass, device_reg, entity_re
if trigger["type"] == "tilt_position":
assert capabilities == expected_capabilities
else:
- assert capabilities == {"extra_fields": []}
+ assert capabilities == {
+ "extra_fields": [
+ {
+ "name": "for",
+ "optional": True,
+ "type": "positive_time_period_dict",
+ }
+ ]
+ }
async def test_if_fires_on_state_change(hass, calls):
@@ -459,6 +483,61 @@ async def test_if_fires_on_state_change(hass, calls):
] == "closing - device - {} - opening - closing - None".format("cover.entity")
+async def test_if_fires_on_state_change_with_for(hass, calls):
+ """Test for triggers firing with delay."""
+ entity_id = "cover.entity"
+ hass.states.async_set(entity_id, STATE_CLOSED)
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": entity_id,
+ "type": "opened",
+ "for": {"seconds": 5},
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "turn_off {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(
+ (
+ "platform",
+ "entity_id",
+ "from_state.state",
+ "to_state.state",
+ "for",
+ )
+ )
+ },
+ },
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(entity_id).state == STATE_CLOSED
+ assert len(calls) == 0
+
+ hass.states.async_set(entity_id, STATE_OPEN)
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ await hass.async_block_till_done()
+ assert (
+ calls[0].data["some"]
+ == f"turn_off device - {entity_id} - closed - open - 0:00:05"
+ )
+
+
async def test_if_fires_on_position(hass, calls):
"""Test for position triggers."""
platform = getattr(hass.components, f"test.{DOMAIN}")
@@ -542,8 +621,12 @@ async def test_if_fires_on_position(hass, calls):
]
},
)
+ hass.states.async_set(ent.entity_id, STATE_OPEN, attributes={"current_position": 1})
+ hass.states.async_set(
+ ent.entity_id, STATE_CLOSED, attributes={"current_position": 95}
+ )
hass.states.async_set(
- ent.entity_id, STATE_CLOSED, attributes={"current_position": 50}
+ ent.entity_id, STATE_OPEN, attributes={"current_position": 50}
)
await hass.async_block_till_done()
assert len(calls) == 3
@@ -551,8 +634,8 @@ async def test_if_fires_on_position(hass, calls):
[calls[0].data["some"], calls[1].data["some"], calls[2].data["some"]]
) == sorted(
[
- "is_pos_gt_45_lt_90 - device - cover.set_position_cover - open - closed - None",
- "is_pos_lt_90 - device - cover.set_position_cover - open - closed - None",
+ "is_pos_gt_45_lt_90 - device - cover.set_position_cover - closed - open - None",
+ "is_pos_lt_90 - device - cover.set_position_cover - closed - open - None",
"is_pos_gt_45 - device - cover.set_position_cover - open - closed - None",
]
)
@@ -666,7 +749,13 @@ async def test_if_fires_on_tilt_position(hass, calls):
},
)
hass.states.async_set(
- ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 50}
+ ent.entity_id, STATE_OPEN, attributes={"current_tilt_position": 1}
+ )
+ hass.states.async_set(
+ ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 95}
+ )
+ hass.states.async_set(
+ ent.entity_id, STATE_OPEN, attributes={"current_tilt_position": 50}
)
await hass.async_block_till_done()
assert len(calls) == 3
@@ -674,8 +763,8 @@ async def test_if_fires_on_tilt_position(hass, calls):
[calls[0].data["some"], calls[1].data["some"], calls[2].data["some"]]
) == sorted(
[
- "is_pos_gt_45_lt_90 - device - cover.set_position_cover - open - closed - None",
- "is_pos_lt_90 - device - cover.set_position_cover - open - closed - None",
+ "is_pos_gt_45_lt_90 - device - cover.set_position_cover - closed - open - None",
+ "is_pos_lt_90 - device - cover.set_position_cover - closed - open - None",
"is_pos_gt_45 - device - cover.set_position_cover - open - closed - None",
]
)
diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py
index a7165b2cb9b3ae..076f9f54878e84 100644
--- a/tests/components/daikin/test_config_flow.py
+++ b/tests/components/daikin/test_config_flow.py
@@ -7,13 +7,8 @@
from aiohttp.web_exceptions import HTTPForbidden
import pytest
-from homeassistant.components.daikin.const import KEY_IP, KEY_MAC
-from homeassistant.config_entries import (
- SOURCE_DISCOVERY,
- SOURCE_IMPORT,
- SOURCE_USER,
- SOURCE_ZEROCONF,
-)
+from homeassistant.components.daikin.const import KEY_MAC
+from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, SOURCE_ZEROCONF
from homeassistant.const import CONF_HOST
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
@@ -132,7 +127,6 @@ async def test_device_abort(hass, mock_daikin, s_effect, reason):
@pytest.mark.parametrize(
"source, data, unique_id",
[
- (SOURCE_DISCOVERY, {KEY_IP: HOST, KEY_MAC: MAC}, MAC),
(SOURCE_ZEROCONF, {CONF_HOST: HOST}, MAC),
],
)
diff --git a/tests/components/datadog/test_init.py b/tests/components/datadog/test_init.py
index 087e0f4b8846cc..13d99289bb8056 100644
--- a/tests/components/datadog/test_init.py
+++ b/tests/components/datadog/test_init.py
@@ -37,8 +37,8 @@ async def test_datadog_setup_full(hass):
assert mock_init.call_args == mock.call(statsd_host="host", statsd_port=123)
assert hass.bus.listen.called
- assert EVENT_LOGBOOK_ENTRY == hass.bus.listen.call_args_list[0][0][0]
- assert EVENT_STATE_CHANGED == hass.bus.listen.call_args_list[1][0][0]
+ assert hass.bus.listen.call_args_list[0][0][0] == EVENT_LOGBOOK_ENTRY
+ assert hass.bus.listen.call_args_list[1][0][0] == EVENT_STATE_CHANGED
async def test_datadog_setup_defaults(hass):
diff --git a/tests/components/deconz/conftest.py b/tests/components/deconz/conftest.py
index 4c45d3c912b9ce..7b2c691bcae7ea 100644
--- a/tests/components/deconz/conftest.py
+++ b/tests/components/deconz/conftest.py
@@ -1,2 +1,29 @@
"""deconz conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from __future__ import annotations
+
+from unittest.mock import patch
+
+import pytest
+
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
+
+
+@pytest.fixture(autouse=True)
+def mock_deconz_websocket():
+ """No real websocket allowed."""
+ with patch("pydeconz.gateway.WSClient") as mock:
+
+ async def make_websocket_call(data: dict | None = None, state: str = ""):
+ """Generate a websocket call."""
+ pydeconz_gateway_session_handler = mock.call_args[0][3]
+
+ if data:
+ mock.return_value.data = data
+ await pydeconz_gateway_session_handler(signal="data")
+ elif state:
+ mock.return_value.state = state
+ await pydeconz_gateway_session_handler(signal="state")
+ else:
+ raise NotImplementedError
+
+ yield make_websocket_call
diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py
index 3611e30f66500b..9d4c86ead6c9f8 100644
--- a/tests/components/deconz/test_binary_sensor.py
+++ b/tests/components/deconz/test_binary_sensor.py
@@ -1,12 +1,10 @@
"""deCONZ binary sensor platform tests."""
-from copy import deepcopy
from unittest.mock import patch
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_MOTION,
DEVICE_CLASS_VIBRATION,
- DOMAIN as BINARY_SENSOR_DOMAIN,
)
from homeassistant.components.deconz.const import (
CONF_ALLOW_CLIP_SENSOR,
@@ -14,78 +12,65 @@
CONF_MASTER_GATEWAY,
DOMAIN as DECONZ_DOMAIN,
)
-from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH
-from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_registry import async_entries_for_config_entry
-from homeassistant.setup import async_setup_component
-from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
-
-SENSORS = {
- "1": {
- "id": "Presence sensor id",
- "name": "Presence sensor",
- "type": "ZHAPresence",
- "state": {"dark": False, "presence": False},
- "config": {"on": True, "reachable": True, "temperature": 10},
- "uniqueid": "00:00:00:00:00:00:00:00-00",
- },
- "2": {
- "id": "Temperature sensor id",
- "name": "Temperature sensor",
- "type": "ZHATemperature",
- "state": {"temperature": False},
- "config": {},
- "uniqueid": "00:00:00:00:00:00:00:01-00",
- },
- "3": {
- "id": "CLIP presence sensor id",
- "name": "CLIP presence sensor",
- "type": "CLIPPresence",
- "state": {"presence": False},
- "config": {},
- "uniqueid": "00:00:00:00:00:00:00:02-00",
- },
- "4": {
- "id": "Vibration sensor id",
- "name": "Vibration sensor",
- "type": "ZHAVibration",
- "state": {
- "orientation": [1, 2, 3],
- "tiltangle": 36,
- "vibration": True,
- "vibrationstrength": 10,
- },
- "config": {"on": True, "reachable": True, "temperature": 10},
- "uniqueid": "00:00:00:00:00:00:00:03-00",
- },
-}
-
-
-async def test_platform_manually_configured(hass):
- """Test that we do not discover anything or try to set up a gateway."""
- assert (
- await async_setup_component(
- hass, BINARY_SENSOR_DOMAIN, {"binary_sensor": {"platform": DECONZ_DOMAIN}}
- )
- is True
- )
- assert DECONZ_DOMAIN not in hass.data
+from .test_gateway import (
+ DECONZ_WEB_REQUEST,
+ mock_deconz_request,
+ setup_deconz_integration,
+)
-async def test_no_binary_sensors(hass):
+async def test_no_binary_sensors(hass, aioclient_mock):
"""Test that no sensors in deconz results in no sensor entities."""
- await setup_deconz_integration(hass)
+ await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 0
-async def test_binary_sensors(hass):
+async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket):
"""Test successful creation of binary sensor entities."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = deepcopy(SENSORS)
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ data = {
+ "sensors": {
+ "1": {
+ "name": "Presence sensor",
+ "type": "ZHAPresence",
+ "state": {"dark": False, "presence": False},
+ "config": {"on": True, "reachable": True, "temperature": 10},
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ },
+ "2": {
+ "name": "Temperature sensor",
+ "type": "ZHATemperature",
+ "state": {"temperature": False},
+ "config": {},
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ },
+ "3": {
+ "name": "CLIP presence sensor",
+ "type": "CLIPPresence",
+ "state": {"presence": False},
+ "config": {},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
+ },
+ "4": {
+ "name": "Vibration sensor",
+ "type": "ZHAVibration",
+ "state": {
+ "orientation": [1, 2, 3],
+ "tiltangle": 36,
+ "vibration": True,
+ "vibrationstrength": 10,
+ },
+ "config": {"on": True, "reachable": True, "temperature": 10},
+ "uniqueid": "00:00:00:00:00:00:00:03-00",
+ },
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 3
presence_sensor = hass.states.get("binary_sensor.presence_sensor")
@@ -97,38 +82,57 @@ async def test_binary_sensors(hass):
assert vibration_sensor.state == STATE_ON
assert vibration_sensor.attributes["device_class"] == DEVICE_CLASS_VIBRATION
- state_changed_event = {
+ event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "1",
"state": {"presence": True},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get("binary_sensor.presence_sensor").state == STATE_ON
await hass.config_entries.async_unload(config_entry.entry_id)
+ assert hass.states.get("binary_sensor.presence_sensor").state == STATE_UNAVAILABLE
+
+ await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
+
assert len(hass.states.async_all()) == 0
-async def test_allow_clip_sensor(hass):
+async def test_allow_clip_sensor(hass, aioclient_mock):
"""Test that CLIP sensors can be allowed."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = deepcopy(SENSORS)
- config_entry = await setup_deconz_integration(
- hass,
- options={CONF_ALLOW_CLIP_SENSOR: True},
- get_state_response=data,
- )
+ data = {
+ "sensors": {
+ "1": {
+ "name": "Presence sensor",
+ "type": "ZHAPresence",
+ "state": {"presence": False},
+ "config": {"on": True, "reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ },
+ "2": {
+ "name": "CLIP presence sensor",
+ "type": "CLIPPresence",
+ "state": {"presence": False},
+ "config": {},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
+ },
+ }
+ }
- assert len(hass.states.async_all()) == 4
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(
+ hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True}
+ )
+
+ assert len(hass.states.async_all()) == 2
assert hass.states.get("binary_sensor.presence_sensor").state == STATE_OFF
- assert hass.states.get("binary_sensor.temperature_sensor") is None
assert hass.states.get("binary_sensor.clip_presence_sensor").state == STATE_OFF
- assert hass.states.get("binary_sensor.vibration_sensor").state == STATE_ON
# Disallow clip sensors
@@ -137,8 +141,8 @@ async def test_allow_clip_sensor(hass):
)
await hass.async_block_till_done()
- assert len(hass.states.async_all()) == 3
- assert hass.states.get("binary_sensor.clip_presence_sensor") is None
+ assert len(hass.states.async_all()) == 1
+ assert not hass.states.get("binary_sensor.clip_presence_sensor")
# Allow clip sensors
@@ -147,67 +151,81 @@ async def test_allow_clip_sensor(hass):
)
await hass.async_block_till_done()
- assert len(hass.states.async_all()) == 4
+ assert len(hass.states.async_all()) == 2
assert hass.states.get("binary_sensor.clip_presence_sensor").state == STATE_OFF
-async def test_add_new_binary_sensor(hass):
+async def test_add_new_binary_sensor(hass, aioclient_mock, mock_deconz_websocket):
"""Test that adding a new binary sensor works."""
- config_entry = await setup_deconz_integration(hass)
- gateway = get_gateway_from_config_entry(hass, config_entry)
- assert len(hass.states.async_all()) == 0
-
- state_added_event = {
+ event_added_sensor = {
"t": "event",
"e": "added",
"r": "sensors",
"id": "1",
- "sensor": deepcopy(SENSORS["1"]),
+ "sensor": {
+ "id": "Presence sensor id",
+ "name": "Presence sensor",
+ "type": "ZHAPresence",
+ "state": {"presence": False},
+ "config": {"on": True, "reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ },
}
- gateway.api.event_handler(state_added_event)
+
+ await setup_deconz_integration(hass, aioclient_mock)
+ assert len(hass.states.async_all()) == 0
+
+ await mock_deconz_websocket(data=event_added_sensor)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
assert hass.states.get("binary_sensor.presence_sensor").state == STATE_OFF
-async def test_add_new_binary_sensor_ignored(hass):
+async def test_add_new_binary_sensor_ignored(
+ hass, aioclient_mock, mock_deconz_websocket
+):
"""Test that adding a new binary sensor is not allowed."""
+ sensor = {
+ "name": "Presence sensor",
+ "type": "ZHAPresence",
+ "state": {"presence": False},
+ "config": {"on": True, "reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ }
+ event_added_sensor = {
+ "t": "event",
+ "e": "added",
+ "r": "sensors",
+ "id": "1",
+ "sensor": sensor,
+ }
+
config_entry = await setup_deconz_integration(
hass,
+ aioclient_mock,
options={CONF_MASTER_GATEWAY: True, CONF_ALLOW_NEW_DEVICES: False},
)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+
assert len(hass.states.async_all()) == 0
- state_added_event = {
- "t": "event",
- "e": "added",
- "r": "sensors",
- "id": "1",
- "sensor": deepcopy(SENSORS["1"]),
- }
- gateway.api.event_handler(state_added_event)
+ await mock_deconz_websocket(data=event_added_sensor)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
assert not hass.states.get("binary_sensor.presence_sensor")
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
assert (
len(async_entries_for_config_entry(entity_registry, config_entry.entry_id)) == 0
)
- with patch(
- "pydeconz.DeconzSession.request",
- return_value={
- "groups": {},
- "lights": {},
- "sensors": {"1": deepcopy(SENSORS["1"])},
- },
- ):
- await hass.services.async_call(DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH)
- await hass.async_block_till_done()
+ aioclient_mock.clear_requests()
+ data = {"groups": {}, "lights": {}, "sensors": {"1": sensor}}
+ mock_deconz_request(aioclient_mock, config_entry.data, data)
+
+ await hass.services.async_call(DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH)
+ await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
assert hass.states.get("binary_sensor.presence_sensor")
diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py
index 4d68ba2a6a71a9..92ca38fdf4d5ef 100644
--- a/tests/components/deconz/test_climate.py
+++ b/tests/components/deconz/test_climate.py
@@ -1,6 +1,5 @@
"""deCONZ climate platform tests."""
-from copy import deepcopy
from unittest.mock import patch
import pytest
@@ -28,105 +27,79 @@
HVAC_MODE_COOL,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
+ PRESET_BOOST,
PRESET_COMFORT,
+ PRESET_ECO,
)
from homeassistant.components.deconz.climate import (
DECONZ_FAN_SMART,
+ DECONZ_PRESET_AUTO,
+ DECONZ_PRESET_COMPLEX,
+ DECONZ_PRESET_HOLIDAY,
DECONZ_PRESET_MANUAL,
)
-from homeassistant.components.deconz.const import (
- CONF_ALLOW_CLIP_SENSOR,
- DOMAIN as DECONZ_DOMAIN,
+from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_TEMPERATURE,
+ STATE_OFF,
+ STATE_UNAVAILABLE,
+)
+
+from .test_gateway import (
+ DECONZ_WEB_REQUEST,
+ mock_deconz_put_request,
+ setup_deconz_integration,
)
-from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
-from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_OFF
-from homeassistant.setup import async_setup_component
-
-from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
-
-SENSORS = {
- "1": {
- "id": "Thermostat id",
- "name": "Thermostat",
- "type": "ZHAThermostat",
- "state": {"on": True, "temperature": 2260, "valve": 30},
- "config": {
- "battery": 100,
- "heatsetpoint": 2200,
- "mode": "auto",
- "offset": 10,
- "reachable": True,
- },
- "uniqueid": "00:00:00:00:00:00:00:00-00",
- },
- "2": {
- "id": "CLIP thermostat id",
- "name": "CLIP thermostat",
- "type": "CLIPThermostat",
- "state": {"on": True, "temperature": 2260, "valve": 30},
- "config": {"reachable": True},
- "uniqueid": "00:00:00:00:00:00:00:02-00",
- },
-}
-
-
-async def test_platform_manually_configured(hass):
- """Test that we do not discover anything or try to set up a gateway."""
- assert (
- await async_setup_component(
- hass, CLIMATE_DOMAIN, {"climate": {"platform": DECONZ_DOMAIN}}
- )
- is True
- )
- assert DECONZ_DOMAIN not in hass.data
-async def test_no_sensors(hass):
+async def test_no_sensors(hass, aioclient_mock):
"""Test that no sensors in deconz results in no climate entities."""
- await setup_deconz_integration(hass)
+ await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 0
-async def test_simple_climate_device(hass):
+async def test_simple_climate_device(hass, aioclient_mock, mock_deconz_websocket):
"""Test successful creation of climate entities.
This is a simple water heater that only supports setting temperature and on and off.
"""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = {
- "0": {
- "config": {
- "battery": 59,
- "displayflipped": None,
- "heatsetpoint": 2100,
- "locked": None,
- "mountingmode": None,
- "offset": 0,
- "on": True,
- "reachable": True,
- },
- "ep": 1,
- "etag": "6130553ac247174809bae47144ee23f8",
- "lastseen": "2020-11-29T19:31Z",
- "manufacturername": "Danfoss",
- "modelid": "eTRV0100",
- "name": "thermostat",
- "state": {
- "errorcode": None,
- "lastupdated": "2020-11-29T19:28:40.665",
- "mountingmodeactive": False,
- "on": True,
- "temperature": 2102,
- "valve": 24,
- "windowopen": "Closed",
- },
- "swversion": "01.02.0008 01.02",
- "type": "ZHAThermostat",
- "uniqueid": "14:b4:57:ff:fe:d5:4e:77-01-0201",
+ data = {
+ "sensors": {
+ "0": {
+ "config": {
+ "battery": 59,
+ "displayflipped": None,
+ "heatsetpoint": 2100,
+ "locked": True,
+ "mountingmode": None,
+ "offset": 0,
+ "on": True,
+ "reachable": True,
+ },
+ "ep": 1,
+ "etag": "6130553ac247174809bae47144ee23f8",
+ "lastseen": "2020-11-29T19:31Z",
+ "manufacturername": "Danfoss",
+ "modelid": "eTRV0100",
+ "name": "thermostat",
+ "state": {
+ "errorcode": None,
+ "lastupdated": "2020-11-29T19:28:40.665",
+ "mountingmodeactive": False,
+ "on": True,
+ "temperature": 2102,
+ "valve": 24,
+ "windowopen": "Closed",
+ },
+ "swversion": "01.02.0008 01.02",
+ "type": "ZHAThermostat",
+ "uniqueid": "14:b4:57:ff:fe:d5:4e:77-01-0201",
+ }
}
}
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 2
climate_thermostat = hass.states.get("climate.thermostat")
@@ -137,69 +110,64 @@ async def test_simple_climate_device(hass):
]
assert climate_thermostat.attributes["current_temperature"] == 21.0
assert climate_thermostat.attributes["temperature"] == 21.0
+ assert climate_thermostat.attributes["locked"] is True
assert hass.states.get("sensor.thermostat_battery_level").state == "59"
# Event signals thermostat configured off
- state_changed_event = {
+ event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "0",
"state": {"on": False},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get("climate.thermostat").state == STATE_OFF
# Event signals thermostat state on
- state_changed_event = {
+ event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "0",
"state": {"on": True},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get("climate.thermostat").state == HVAC_MODE_HEAT
# Verify service calls
- thermostat_device = gateway.api.sensors["0"]
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config")
# Service turn on thermostat
- with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- CLIMATE_DOMAIN,
- SERVICE_SET_HVAC_MODE,
- {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_HEAT},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/sensors/0/config", json={"on": True})
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_HEAT},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"on": True}
# Service turn on thermostat
- with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- CLIMATE_DOMAIN,
- SERVICE_SET_HVAC_MODE,
- {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_OFF},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/sensors/0/config", json={"on": False})
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_OFF},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[2][2] == {"on": False}
# Service set HVAC mode to unsupported value
- with patch.object(
- thermostat_device, "_request", return_value=True
- ) as set_callback, pytest.raises(ValueError):
+ with pytest.raises(ValueError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
@@ -208,12 +176,29 @@ async def test_simple_climate_device(hass):
)
-async def test_climate_device_without_cooling_support(hass):
+async def test_climate_device_without_cooling_support(
+ hass, aioclient_mock, mock_deconz_websocket
+):
"""Test successful creation of sensor entities."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = deepcopy(SENSORS)
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ data = {
+ "sensors": {
+ "1": {
+ "name": "Thermostat",
+ "type": "ZHAThermostat",
+ "state": {"on": True, "temperature": 2260, "valve": 30},
+ "config": {
+ "battery": 100,
+ "heatsetpoint": 2200,
+ "mode": "auto",
+ "offset": 10,
+ "reachable": True,
+ },
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ }
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 2
climate_thermostat = hass.states.get("climate.thermostat")
@@ -232,21 +217,21 @@ async def test_climate_device_without_cooling_support(hass):
# Event signals thermostat configured off
- state_changed_event = {
+ event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "1",
"config": {"mode": "off"},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get("climate.thermostat").state == STATE_OFF
# Event signals thermostat state on
- state_changed_event = {
+ event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
@@ -254,75 +239,62 @@ async def test_climate_device_without_cooling_support(hass):
"config": {"mode": "other"},
"state": {"on": True},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get("climate.thermostat").state == HVAC_MODE_HEAT
# Event signals thermostat state off
- state_changed_event = {
+ event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "1",
"state": {"on": False},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get("climate.thermostat").state == STATE_OFF
# Verify service calls
- thermostat_device = gateway.api.sensors["1"]
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/1/config")
# Service set HVAC mode to auto
- with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- CLIMATE_DOMAIN,
- SERVICE_SET_HVAC_MODE,
- {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_AUTO},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with(
- "put", "/sensors/1/config", json={"mode": "auto"}
- )
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_AUTO},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"mode": "auto"}
# Service set HVAC mode to heat
- with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- CLIMATE_DOMAIN,
- SERVICE_SET_HVAC_MODE,
- {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_HEAT},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with(
- "put", "/sensors/1/config", json={"mode": "heat"}
- )
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_HEAT},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[2][2] == {"mode": "heat"}
# Service set HVAC mode to off
- with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- CLIMATE_DOMAIN,
- SERVICE_SET_HVAC_MODE,
- {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_OFF},
- blocking=True,
- )
- set_callback.assert_called_with(
- "put", "/sensors/1/config", json={"mode": "off"}
- )
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_OFF},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[3][2] == {"mode": "off"}
# Service set HVAC mode to unsupported value
- with patch.object(
- thermostat_device, "_request", return_value=True
- ) as set_callback, pytest.raises(ValueError):
+ with pytest.raises(ValueError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
@@ -332,22 +304,17 @@ async def test_climate_device_without_cooling_support(hass):
# Service set temperature to 20
- with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- CLIMATE_DOMAIN,
- SERVICE_SET_TEMPERATURE,
- {ATTR_ENTITY_ID: "climate.thermostat", ATTR_TEMPERATURE: 20},
- blocking=True,
- )
- set_callback.assert_called_with(
- "put", "/sensors/1/config", json={"heatsetpoint": 2000.0}
- )
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {ATTR_ENTITY_ID: "climate.thermostat", ATTR_TEMPERATURE: 20},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[4][2] == {"heatsetpoint": 2000.0}
# Service set temperature without providing temperature attribute
- with patch.object(
- thermostat_device, "_request", return_value=True
- ) as set_callback, pytest.raises(ValueError):
+ with pytest.raises(ValueError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
@@ -361,41 +328,51 @@ async def test_climate_device_without_cooling_support(hass):
await hass.config_entries.async_unload(config_entry.entry_id)
+ states = hass.states.async_all()
+ assert len(states) == 2
+ for state in states:
+ assert state.state == STATE_UNAVAILABLE
+
+ await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
-async def test_climate_device_with_cooling_support(hass):
+async def test_climate_device_with_cooling_support(
+ hass, aioclient_mock, mock_deconz_websocket
+):
"""Test successful creation of sensor entities."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = {
- "0": {
- "config": {
- "battery": 25,
- "coolsetpoint": None,
- "fanmode": None,
- "heatsetpoint": 2222,
- "mode": "heat",
- "offset": 0,
- "on": True,
- "reachable": True,
- },
- "ep": 1,
- "etag": "074549903686a77a12ef0f06c499b1ef",
- "lastseen": "2020-11-27T13:45Z",
- "manufacturername": "Zen Within",
- "modelid": "Zen-01",
- "name": "Zen-01",
- "state": {
- "lastupdated": "2020-11-27T13:42:40.863",
- "on": False,
- "temperature": 2320,
- },
- "type": "ZHAThermostat",
- "uniqueid": "00:24:46:00:00:11:6f:56-01-0201",
+ data = {
+ "sensors": {
+ "0": {
+ "config": {
+ "battery": 25,
+ "coolsetpoint": None,
+ "fanmode": None,
+ "heatsetpoint": 2222,
+ "mode": "heat",
+ "offset": 0,
+ "on": True,
+ "reachable": True,
+ },
+ "ep": 1,
+ "etag": "074549903686a77a12ef0f06c499b1ef",
+ "lastseen": "2020-11-27T13:45Z",
+ "manufacturername": "Zen Within",
+ "modelid": "Zen-01",
+ "name": "Zen-01",
+ "state": {
+ "lastupdated": "2020-11-27T13:42:40.863",
+ "on": False,
+ "temperature": 2320,
+ },
+ "type": "ZHAThermostat",
+ "uniqueid": "00:24:46:00:00:11:6f:56-01-0201",
+ }
}
}
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 2
climate_thermostat = hass.states.get("climate.zen_01")
@@ -412,68 +389,68 @@ async def test_climate_device_with_cooling_support(hass):
# Event signals thermostat state cool
- state_changed_event = {
+ event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "0",
"config": {"mode": "cool"},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get("climate.zen_01").state == HVAC_MODE_COOL
# Verify service calls
- thermostat_device = gateway.api.sensors["0"]
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config")
# Service set temperature to 20
- with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- CLIMATE_DOMAIN,
- SERVICE_SET_TEMPERATURE,
- {ATTR_ENTITY_ID: "climate.zen_01", ATTR_TEMPERATURE: 20},
- blocking=True,
- )
- set_callback.assert_called_with(
- "put", "/sensors/0/config", json={"coolsetpoint": 2000.0}
- )
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {ATTR_ENTITY_ID: "climate.zen_01", ATTR_TEMPERATURE: 20},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"coolsetpoint": 2000.0}
-async def test_climate_device_with_fan_support(hass):
+async def test_climate_device_with_fan_support(
+ hass, aioclient_mock, mock_deconz_websocket
+):
"""Test successful creation of sensor entities."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = {
- "0": {
- "config": {
- "battery": 25,
- "coolsetpoint": None,
- "fanmode": "auto",
- "heatsetpoint": 2222,
- "mode": "heat",
- "offset": 0,
- "on": True,
- "reachable": True,
- },
- "ep": 1,
- "etag": "074549903686a77a12ef0f06c499b1ef",
- "lastseen": "2020-11-27T13:45Z",
- "manufacturername": "Zen Within",
- "modelid": "Zen-01",
- "name": "Zen-01",
- "state": {
- "lastupdated": "2020-11-27T13:42:40.863",
- "on": False,
- "temperature": 2320,
- },
- "type": "ZHAThermostat",
- "uniqueid": "00:24:46:00:00:11:6f:56-01-0201",
+ data = {
+ "sensors": {
+ "0": {
+ "config": {
+ "battery": 25,
+ "coolsetpoint": None,
+ "fanmode": "auto",
+ "heatsetpoint": 2222,
+ "mode": "heat",
+ "offset": 0,
+ "on": True,
+ "reachable": True,
+ },
+ "ep": 1,
+ "etag": "074549903686a77a12ef0f06c499b1ef",
+ "lastseen": "2020-11-27T13:45Z",
+ "manufacturername": "Zen Within",
+ "modelid": "Zen-01",
+ "name": "Zen-01",
+ "state": {
+ "lastupdated": "2020-11-27T13:42:40.863",
+ "on": False,
+ "temperature": 2320,
+ },
+ "type": "ZHAThermostat",
+ "uniqueid": "00:24:46:00:00:11:6f:56-01-0201",
+ }
}
}
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 2
climate_thermostat = hass.states.get("climate.zen_01")
@@ -491,21 +468,21 @@ async def test_climate_device_with_fan_support(hass):
# Event signals fan mode defaults to off
- state_changed_event = {
+ event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "0",
"config": {"fanmode": "unsupported"},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_OFF
# Event signals unsupported fan mode
- state_changed_event = {
+ event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
@@ -513,60 +490,52 @@ async def test_climate_device_with_fan_support(hass):
"config": {"fanmode": "unsupported"},
"state": {"on": True},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_ON
# Event signals unsupported fan mode
- state_changed_event = {
+ event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "0",
"config": {"fanmode": "unsupported"},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_ON
# Verify service calls
- thermostat_device = gateway.api.sensors["0"]
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config")
# Service set fan mode to off
- with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- CLIMATE_DOMAIN,
- SERVICE_SET_FAN_MODE,
- {ATTR_ENTITY_ID: "climate.zen_01", ATTR_FAN_MODE: FAN_OFF},
- blocking=True,
- )
- set_callback.assert_called_with(
- "put", "/sensors/0/config", json={"fanmode": "off"}
- )
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_FAN_MODE,
+ {ATTR_ENTITY_ID: "climate.zen_01", ATTR_FAN_MODE: FAN_OFF},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"fanmode": "off"}
# Service set fan mode to custom deCONZ mode smart
- with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- CLIMATE_DOMAIN,
- SERVICE_SET_FAN_MODE,
- {ATTR_ENTITY_ID: "climate.zen_01", ATTR_FAN_MODE: DECONZ_FAN_SMART},
- blocking=True,
- )
- set_callback.assert_called_with(
- "put", "/sensors/0/config", json={"fanmode": "smart"}
- )
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_FAN_MODE,
+ {ATTR_ENTITY_ID: "climate.zen_01", ATTR_FAN_MODE: DECONZ_FAN_SMART},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[2][2] == {"fanmode": "smart"}
# Service set fan mode to unsupported value
- with patch.object(
- thermostat_device, "_request", return_value=True
- ) as set_callback, pytest.raises(ValueError):
+ with pytest.raises(ValueError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
@@ -575,39 +544,40 @@ async def test_climate_device_with_fan_support(hass):
)
-async def test_climate_device_with_preset(hass):
+async def test_climate_device_with_preset(hass, aioclient_mock, mock_deconz_websocket):
"""Test successful creation of sensor entities."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = {
- "0": {
- "config": {
- "battery": 25,
- "coolsetpoint": None,
- "fanmode": None,
- "heatsetpoint": 2222,
- "mode": "heat",
- "preset": "auto",
- "offset": 0,
- "on": True,
- "reachable": True,
- },
- "ep": 1,
- "etag": "074549903686a77a12ef0f06c499b1ef",
- "lastseen": "2020-11-27T13:45Z",
- "manufacturername": "Zen Within",
- "modelid": "Zen-01",
- "name": "Zen-01",
- "state": {
- "lastupdated": "2020-11-27T13:42:40.863",
- "on": False,
- "temperature": 2320,
- },
- "type": "ZHAThermostat",
- "uniqueid": "00:24:46:00:00:11:6f:56-01-0201",
+ data = {
+ "sensors": {
+ "0": {
+ "config": {
+ "battery": 25,
+ "coolsetpoint": None,
+ "fanmode": None,
+ "heatsetpoint": 2222,
+ "mode": "heat",
+ "preset": "auto",
+ "offset": 0,
+ "on": True,
+ "reachable": True,
+ },
+ "ep": 1,
+ "etag": "074549903686a77a12ef0f06c499b1ef",
+ "lastseen": "2020-11-27T13:45Z",
+ "manufacturername": "Zen Within",
+ "modelid": "Zen-01",
+ "name": "Zen-01",
+ "state": {
+ "lastupdated": "2020-11-27T13:42:40.863",
+ "on": False,
+ "temperature": 2320,
+ },
+ "type": "ZHAThermostat",
+ "uniqueid": "00:24:46:00:00:11:6f:56-01-0201",
+ }
}
}
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 2
@@ -615,27 +585,27 @@ async def test_climate_device_with_preset(hass):
assert climate_zen_01.state == HVAC_MODE_HEAT
assert climate_zen_01.attributes["current_temperature"] == 23.2
assert climate_zen_01.attributes["temperature"] == 22.2
- assert climate_zen_01.attributes["preset_mode"] == "auto"
+ assert climate_zen_01.attributes["preset_mode"] == DECONZ_PRESET_AUTO
assert climate_zen_01.attributes["preset_modes"] == [
- "auto",
- "boost",
- "comfort",
- "complex",
- "eco",
- "holiday",
- "manual",
+ DECONZ_PRESET_AUTO,
+ PRESET_BOOST,
+ PRESET_COMFORT,
+ DECONZ_PRESET_COMPLEX,
+ PRESET_ECO,
+ DECONZ_PRESET_HOLIDAY,
+ DECONZ_PRESET_MANUAL,
]
# Event signals deCONZ preset
- state_changed_event = {
+ event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "0",
"config": {"preset": "manual"},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert (
@@ -645,55 +615,45 @@ async def test_climate_device_with_preset(hass):
# Event signals unknown preset
- state_changed_event = {
+ event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "0",
"config": {"preset": "unsupported"},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get("climate.zen_01").attributes["preset_mode"] is None
# Verify service calls
- thermostat_device = gateway.api.sensors["0"]
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config")
# Service set preset to HASS preset
- with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- CLIMATE_DOMAIN,
- SERVICE_SET_PRESET_MODE,
- {ATTR_ENTITY_ID: "climate.zen_01", ATTR_PRESET_MODE: PRESET_COMFORT},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with(
- "put", "/sensors/0/config", json={"preset": "comfort"}
- )
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: "climate.zen_01", ATTR_PRESET_MODE: PRESET_COMFORT},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"preset": "comfort"}
# Service set preset to custom deCONZ preset
- with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- CLIMATE_DOMAIN,
- SERVICE_SET_PRESET_MODE,
- {ATTR_ENTITY_ID: "climate.zen_01", ATTR_PRESET_MODE: DECONZ_PRESET_MANUAL},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with(
- "put", "/sensors/0/config", json={"preset": "manual"}
- )
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: "climate.zen_01", ATTR_PRESET_MODE: DECONZ_PRESET_MANUAL},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[2][2] == {"preset": "manual"}
# Service set preset to unsupported value
- with patch.object(
- thermostat_device, "_request", return_value=True
- ) as set_callback, pytest.raises(ValueError):
+ with pytest.raises(ValueError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE,
@@ -702,20 +662,38 @@ async def test_climate_device_with_preset(hass):
)
-async def test_clip_climate_device(hass):
+async def test_clip_climate_device(hass, aioclient_mock):
"""Test successful creation of sensor entities."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = deepcopy(SENSORS)
- config_entry = await setup_deconz_integration(
- hass,
- options={CONF_ALLOW_CLIP_SENSOR: True},
- get_state_response=data,
- )
+ data = {
+ "sensors": {
+ "1": {
+ "name": "Thermostat",
+ "type": "ZHAThermostat",
+ "state": {"on": True, "temperature": 2260, "valve": 30},
+ "config": {
+ "battery": 100,
+ "heatsetpoint": 2200,
+ "mode": "auto",
+ "offset": 10,
+ "reachable": True,
+ },
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ },
+ "2": {
+ "name": "CLIP thermostat",
+ "type": "CLIPThermostat",
+ "state": {"on": True, "temperature": 2260, "valve": 30},
+ "config": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
+ },
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(
+ hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True}
+ )
assert len(hass.states.async_all()) == 3
- assert hass.states.get("climate.thermostat").state == HVAC_MODE_AUTO
- assert hass.states.get("sensor.thermostat") is None
- assert hass.states.get("sensor.thermostat_battery_level").state == "100"
assert hass.states.get("climate.clip_thermostat").state == HVAC_MODE_HEAT
# Disallow clip sensors
@@ -726,7 +704,7 @@ async def test_clip_climate_device(hass):
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 2
- assert hass.states.get("climate.clip_thermostat") is None
+ assert not hass.states.get("climate.clip_thermostat")
# Allow clip sensors
@@ -739,43 +717,70 @@ async def test_clip_climate_device(hass):
assert hass.states.get("climate.clip_thermostat").state == HVAC_MODE_HEAT
-async def test_verify_state_update(hass):
+async def test_verify_state_update(hass, aioclient_mock, mock_deconz_websocket):
"""Test that state update properly."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = deepcopy(SENSORS)
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ data = {
+ "sensors": {
+ "1": {
+ "name": "Thermostat",
+ "type": "ZHAThermostat",
+ "state": {"on": True, "temperature": 2260, "valve": 30},
+ "config": {
+ "battery": 100,
+ "heatsetpoint": 2200,
+ "mode": "auto",
+ "offset": 10,
+ "reachable": True,
+ },
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ }
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
assert hass.states.get("climate.thermostat").state == HVAC_MODE_AUTO
- state_changed_event = {
+ event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "1",
"state": {"on": False},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get("climate.thermostat").state == HVAC_MODE_AUTO
- assert gateway.api.sensors["1"].changed_keys == {"state", "r", "t", "on", "e", "id"}
-async def test_add_new_climate_device(hass):
+async def test_add_new_climate_device(hass, aioclient_mock, mock_deconz_websocket):
"""Test that adding a new climate device works."""
- config_entry = await setup_deconz_integration(hass)
- gateway = get_gateway_from_config_entry(hass, config_entry)
- assert len(hass.states.async_all()) == 0
-
- state_added_event = {
+ event_added_sensor = {
"t": "event",
"e": "added",
"r": "sensors",
"id": "1",
- "sensor": deepcopy(SENSORS["1"]),
+ "sensor": {
+ "id": "Thermostat id",
+ "name": "Thermostat",
+ "type": "ZHAThermostat",
+ "state": {"on": True, "temperature": 2260, "valve": 30},
+ "config": {
+ "battery": 100,
+ "heatsetpoint": 2200,
+ "mode": "auto",
+ "offset": 10,
+ "reachable": True,
+ },
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ },
}
- gateway.api.event_handler(state_added_event)
+
+ await setup_deconz_integration(hass, aioclient_mock)
+ assert len(hass.states.async_all()) == 0
+
+ await mock_deconz_websocket(data=event_added_sensor)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 2
diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py
index e18418ff9aeced..4380b8c6021634 100644
--- a/tests/components/deconz/test_config_flow.py
+++ b/tests/components/deconz/test_config_flow.py
@@ -212,7 +212,7 @@ async def test_manual_configuration_after_discovery_ResponseError(hass, aioclien
async def test_manual_configuration_update_configuration(hass, aioclient_mock):
"""Test that manual configuration can update existing config entry."""
- config_entry = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
aioclient_mock.get(
pydeconz.utils.URL_DISCOVER,
@@ -258,7 +258,7 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock):
async def test_manual_configuration_dont_update_configuration(hass, aioclient_mock):
"""Test that _create_entry work and that bridgeid can be requested."""
- await setup_deconz_integration(hass)
+ await setup_deconz_integration(hass, aioclient_mock)
aioclient_mock.get(
pydeconz.utils.URL_DISCOVER,
@@ -374,7 +374,7 @@ async def test_link_get_api_key_ResponseError(hass, aioclient_mock):
async def test_reauth_flow_update_configuration(hass, aioclient_mock):
"""Verify reauth flow can update gateway API key."""
- config_entry = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
result = await hass.config_entries.flow.async_init(
DECONZ_DOMAIN,
@@ -442,9 +442,21 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock):
}
-async def test_ssdp_discovery_update_configuration(hass):
+async def test_flow_ssdp_bad_discovery(hass, aioclient_mock):
+ """Test that SSDP discovery aborts if manufacturer URL is wrong."""
+ result = await hass.config_entries.flow.async_init(
+ DECONZ_DOMAIN,
+ data={ATTR_UPNP_MANUFACTURER_URL: "other"},
+ context={"source": SOURCE_SSDP},
+ )
+
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "not_deconz_bridge"
+
+
+async def test_ssdp_discovery_update_configuration(hass, aioclient_mock):
"""Test if a discovered bridge is configured but updates with new attributes."""
- config_entry = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
with patch(
"homeassistant.components.deconz.async_setup_entry",
@@ -467,9 +479,9 @@ async def test_ssdp_discovery_update_configuration(hass):
assert len(mock_setup_entry.mock_calls) == 1
-async def test_ssdp_discovery_dont_update_configuration(hass):
+async def test_ssdp_discovery_dont_update_configuration(hass, aioclient_mock):
"""Test if a discovered bridge has already been configured."""
- config_entry = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
result = await hass.config_entries.flow.async_init(
DECONZ_DOMAIN,
@@ -486,9 +498,13 @@ async def test_ssdp_discovery_dont_update_configuration(hass):
assert config_entry.data[CONF_HOST] == "1.2.3.4"
-async def test_ssdp_discovery_dont_update_existing_hassio_configuration(hass):
+async def test_ssdp_discovery_dont_update_existing_hassio_configuration(
+ hass, aioclient_mock
+):
"""Test to ensure the SSDP discovery does not update an Hass.io entry."""
- config_entry = await setup_deconz_integration(hass, source=SOURCE_HASSIO)
+ config_entry = await setup_deconz_integration(
+ hass, aioclient_mock, source=SOURCE_HASSIO
+ )
result = await hass.config_entries.flow.async_init(
DECONZ_DOMAIN,
@@ -523,8 +539,6 @@ async def test_flow_hassio_discovery(hass):
assert result["description_placeholders"] == {"addon": "Mock Addon"}
with patch(
- "homeassistant.components.deconz.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.deconz.async_setup_entry",
return_value=True,
) as mock_setup_entry:
@@ -539,13 +553,12 @@ async def test_flow_hassio_discovery(hass):
CONF_PORT: 80,
CONF_API_KEY: API_KEY,
}
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
-async def test_hassio_discovery_update_configuration(hass):
+async def test_hassio_discovery_update_configuration(hass, aioclient_mock):
"""Test we can update an existing config entry."""
- config_entry = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
with patch(
"homeassistant.components.deconz.async_setup_entry",
@@ -571,9 +584,9 @@ async def test_hassio_discovery_update_configuration(hass):
assert len(mock_setup_entry.mock_calls) == 1
-async def test_hassio_discovery_dont_update_configuration(hass):
+async def test_hassio_discovery_dont_update_configuration(hass, aioclient_mock):
"""Test we can update an existing config entry."""
- await setup_deconz_integration(hass)
+ await setup_deconz_integration(hass, aioclient_mock)
result = await hass.config_entries.flow.async_init(
DECONZ_DOMAIN,
@@ -590,9 +603,9 @@ async def test_hassio_discovery_dont_update_configuration(hass):
assert result["reason"] == "already_configured"
-async def test_option_flow(hass):
+async def test_option_flow(hass, aioclient_mock):
"""Test config flow options."""
- config_entry = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
result = await hass.config_entries.options.async_init(config_entry.entry_id)
diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py
index 5314a41b315be8..c252b00a228c19 100644
--- a/tests/components/deconz/test_cover.py
+++ b/tests/components/deconz/test_cover.py
@@ -1,9 +1,9 @@
"""deCONZ cover platform tests."""
-from copy import deepcopy
from unittest.mock import patch
from homeassistant.components.cover import (
+ ATTR_CURRENT_POSITION,
ATTR_CURRENT_TILT_POSITION,
ATTR_POSITION,
ATTR_TILT_POSITION,
@@ -17,315 +17,280 @@
SERVICE_STOP_COVER,
SERVICE_STOP_COVER_TILT,
)
-from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN
-from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
-from homeassistant.const import ATTR_ENTITY_ID, STATE_CLOSED, STATE_OPEN
-from homeassistant.setup import async_setup_component
-
-from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
-
-COVERS = {
- "1": {
- "id": "Level controllable cover id",
- "name": "Level controllable cover",
- "type": "Level controllable output",
- "state": {"bri": 254, "on": False, "reachable": True},
- "modelid": "Not zigbee spec",
- "uniqueid": "00:00:00:00:00:00:00:00-00",
- },
- "2": {
- "id": "Window covering device id",
- "name": "Window covering device",
- "type": "Window covering device",
- "state": {"lift": 100, "open": False, "reachable": True},
- "modelid": "lumi.curtain",
- "uniqueid": "00:00:00:00:00:00:00:01-00",
- },
- "3": {
- "id": "Unsupported cover id",
- "name": "Unsupported cover",
- "type": "Not a cover",
- "state": {"reachable": True},
- "uniqueid": "00:00:00:00:00:00:00:02-00",
- },
- "4": {
- "id": "deconz old brightness cover id",
- "name": "deconz old brightness cover",
- "type": "Level controllable output",
- "state": {"bri": 255, "on": False, "reachable": True},
- "modelid": "Not zigbee spec",
- "uniqueid": "00:00:00:00:00:00:00:03-00",
- },
- "5": {
- "id": "Window covering controller id",
- "name": "Window covering controller",
- "type": "Window covering controller",
- "state": {"bri": 253, "on": True, "reachable": True},
- "modelid": "Motor controller",
- "uniqueid": "00:00:00:00:00:00:00:04-00",
- },
-}
-
-
-async def test_platform_manually_configured(hass):
- """Test that we do not discover anything or try to set up a gateway."""
- assert (
- await async_setup_component(
- hass, COVER_DOMAIN, {"cover": {"platform": DECONZ_DOMAIN}}
- )
- is True
- )
- assert DECONZ_DOMAIN not in hass.data
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ STATE_CLOSED,
+ STATE_OPEN,
+ STATE_UNAVAILABLE,
+)
+from .test_gateway import (
+ DECONZ_WEB_REQUEST,
+ mock_deconz_put_request,
+ setup_deconz_integration,
+)
-async def test_no_covers(hass):
+
+async def test_no_covers(hass, aioclient_mock):
"""Test that no cover entities are created."""
- await setup_deconz_integration(hass)
+ await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 0
-async def test_cover(hass):
+async def test_cover(hass, aioclient_mock, mock_deconz_websocket):
"""Test that all supported cover entities are created."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["lights"] = deepcopy(COVERS)
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ data = {
+ "lights": {
+ "1": {
+ "name": "Level controllable cover",
+ "type": "Level controllable output",
+ "state": {"bri": 254, "on": False, "reachable": True},
+ "modelid": "Not zigbee spec",
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ },
+ "2": {
+ "name": "Window covering device",
+ "type": "Window covering device",
+ "state": {"lift": 100, "open": False, "reachable": True},
+ "modelid": "lumi.curtain",
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ },
+ "3": {
+ "name": "Unsupported cover",
+ "type": "Not a cover",
+ "state": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
+ },
+ "4": {
+ "name": "deconz old brightness cover",
+ "type": "Level controllable output",
+ "state": {"bri": 255, "on": False, "reachable": True},
+ "modelid": "Not zigbee spec",
+ "uniqueid": "00:00:00:00:00:00:00:03-00",
+ },
+ "5": {
+ "name": "Window covering controller",
+ "type": "Window covering controller",
+ "state": {"bri": 253, "on": True, "reachable": True},
+ "modelid": "Motor controller",
+ "uniqueid": "00:00:00:00:00:00:00:04-00",
+ },
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 5
assert hass.states.get("cover.level_controllable_cover").state == STATE_OPEN
assert hass.states.get("cover.window_covering_device").state == STATE_CLOSED
- assert hass.states.get("cover.unsupported_cover") is None
+ assert not hass.states.get("cover.unsupported_cover")
assert hass.states.get("cover.deconz_old_brightness_cover").state == STATE_OPEN
assert hass.states.get("cover.window_covering_controller").state == STATE_CLOSED
# Event signals cover is closed
- state_changed_event = {
+ event_changed_light = {
"t": "event",
"e": "changed",
"r": "lights",
"id": "1",
"state": {"on": True},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_light)
await hass.async_block_till_done()
assert hass.states.get("cover.level_controllable_cover").state == STATE_CLOSED
# Verify service calls for cover
- windows_covering_device = gateway.api.lights["2"]
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/2/state")
# Service open cover
- with patch.object(
- windows_covering_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- COVER_DOMAIN,
- SERVICE_OPEN_COVER,
- {ATTR_ENTITY_ID: "cover.window_covering_device"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/2/state", json={"open": True})
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_OPEN_COVER,
+ {ATTR_ENTITY_ID: "cover.window_covering_device"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"open": True}
# Service close cover
- with patch.object(
- windows_covering_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- COVER_DOMAIN,
- SERVICE_CLOSE_COVER,
- {ATTR_ENTITY_ID: "cover.window_covering_device"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/2/state", json={"open": False})
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_CLOSE_COVER,
+ {ATTR_ENTITY_ID: "cover.window_covering_device"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[2][2] == {"open": False}
# Service set cover position
- with patch.object(
- windows_covering_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- COVER_DOMAIN,
- SERVICE_SET_COVER_POSITION,
- {ATTR_ENTITY_ID: "cover.window_covering_device", ATTR_POSITION: 40},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/2/state", json={"lift": 60})
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_SET_COVER_POSITION,
+ {ATTR_ENTITY_ID: "cover.window_covering_device", ATTR_POSITION: 40},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[3][2] == {"lift": 60}
# Service stop cover movement
- with patch.object(
- windows_covering_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- COVER_DOMAIN,
- SERVICE_STOP_COVER,
- {ATTR_ENTITY_ID: "cover.window_covering_device"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/2/state", json={"stop": True})
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_STOP_COVER,
+ {ATTR_ENTITY_ID: "cover.window_covering_device"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[4][2] == {"stop": True}
# Verify service calls for legacy cover
- level_controllable_cover_device = gateway.api.lights["1"]
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state")
# Service open cover
- with patch.object(
- level_controllable_cover_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- COVER_DOMAIN,
- SERVICE_OPEN_COVER,
- {ATTR_ENTITY_ID: "cover.level_controllable_cover"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/1/state", json={"on": False})
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_OPEN_COVER,
+ {ATTR_ENTITY_ID: "cover.level_controllable_cover"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[5][2] == {"on": False}
# Service close cover
- with patch.object(
- level_controllable_cover_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- COVER_DOMAIN,
- SERVICE_CLOSE_COVER,
- {ATTR_ENTITY_ID: "cover.level_controllable_cover"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/1/state", json={"on": True})
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_CLOSE_COVER,
+ {ATTR_ENTITY_ID: "cover.level_controllable_cover"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[6][2] == {"on": True}
# Service set cover position
- with patch.object(
- level_controllable_cover_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- COVER_DOMAIN,
- SERVICE_SET_COVER_POSITION,
- {ATTR_ENTITY_ID: "cover.level_controllable_cover", ATTR_POSITION: 40},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/1/state", json={"bri": 152})
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_SET_COVER_POSITION,
+ {ATTR_ENTITY_ID: "cover.level_controllable_cover", ATTR_POSITION: 40},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[7][2] == {"bri": 152}
# Service stop cover movement
- with patch.object(
- level_controllable_cover_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- COVER_DOMAIN,
- SERVICE_STOP_COVER,
- {ATTR_ENTITY_ID: "cover.level_controllable_cover"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/1/state", json={"bri_inc": 0})
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_STOP_COVER,
+ {ATTR_ENTITY_ID: "cover.level_controllable_cover"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[8][2] == {"bri_inc": 0}
# Test that a reported cover position of 255 (deconz-rest-api < 2.05.73) is interpreted correctly.
assert hass.states.get("cover.deconz_old_brightness_cover").state == STATE_OPEN
- state_changed_event = {
+ event_changed_light = {
"t": "event",
"e": "changed",
"r": "lights",
"id": "4",
"state": {"on": True},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_light)
await hass.async_block_till_done()
deconz_old_brightness_cover = hass.states.get("cover.deconz_old_brightness_cover")
assert deconz_old_brightness_cover.state == STATE_CLOSED
- assert deconz_old_brightness_cover.attributes["current_position"] == 0
+ assert deconz_old_brightness_cover.attributes[ATTR_CURRENT_POSITION] == 0
await hass.config_entries.async_unload(config_entry.entry_id)
+ states = hass.states.async_all()
+ assert len(states) == 5
+ for state in states:
+ assert state.state == STATE_UNAVAILABLE
+
+ await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
-async def test_tilt_cover(hass):
+async def test_tilt_cover(hass, aioclient_mock):
"""Test that tilting a cover works."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["lights"] = {
- "0": {
- "etag": "87269755b9b3a046485fdae8d96b252c",
- "lastannounced": None,
- "lastseen": "2020-08-01T16:22:05Z",
- "manufacturername": "AXIS",
- "modelid": "Gear",
- "name": "Covering device",
- "state": {
- "bri": 0,
- "lift": 0,
- "on": False,
- "open": True,
- "reachable": True,
- "tilt": 0,
- },
- "swversion": "100-5.3.5.1122",
- "type": "Window covering device",
- "uniqueid": "00:24:46:00:00:12:34:56-01",
+ data = {
+ "lights": {
+ "0": {
+ "etag": "87269755b9b3a046485fdae8d96b252c",
+ "lastannounced": None,
+ "lastseen": "2020-08-01T16:22:05Z",
+ "manufacturername": "AXIS",
+ "modelid": "Gear",
+ "name": "Covering device",
+ "state": {
+ "bri": 0,
+ "lift": 0,
+ "on": False,
+ "open": True,
+ "reachable": True,
+ "tilt": 0,
+ },
+ "swversion": "100-5.3.5.1122",
+ "type": "Window covering device",
+ "uniqueid": "00:24:46:00:00:12:34:56-01",
+ }
}
}
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 1
- entity = hass.states.get("cover.covering_device")
- assert entity.state == STATE_OPEN
- assert entity.attributes[ATTR_CURRENT_TILT_POSITION] == 100
-
- covering_device = gateway.api.lights["0"]
-
- with patch.object(covering_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- COVER_DOMAIN,
- SERVICE_SET_COVER_TILT_POSITION,
- {ATTR_ENTITY_ID: "cover.covering_device", ATTR_TILT_POSITION: 40},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/0/state", json={"tilt": 60})
-
- with patch.object(covering_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- COVER_DOMAIN,
- SERVICE_OPEN_COVER_TILT,
- {ATTR_ENTITY_ID: "cover.covering_device"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/0/state", json={"tilt": 0})
-
- with patch.object(covering_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- COVER_DOMAIN,
- SERVICE_CLOSE_COVER_TILT,
- {ATTR_ENTITY_ID: "cover.covering_device"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/0/state", json={"tilt": 100})
+ covering_device = hass.states.get("cover.covering_device")
+ assert covering_device.state == STATE_OPEN
+ assert covering_device.attributes[ATTR_CURRENT_TILT_POSITION] == 100
+
+ # Verify service calls for tilting cover
+
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state")
+
+ # Service set tilt cover
+
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_SET_COVER_TILT_POSITION,
+ {ATTR_ENTITY_ID: "cover.covering_device", ATTR_TILT_POSITION: 40},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"tilt": 60}
+
+ # Service open tilt cover
+
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_OPEN_COVER_TILT,
+ {ATTR_ENTITY_ID: "cover.covering_device"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[2][2] == {"tilt": 0}
+
+ # Service close tilt cover
+
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_CLOSE_COVER_TILT,
+ {ATTR_ENTITY_ID: "cover.covering_device"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[3][2] == {"tilt": 100}
# Service stop cover movement
- with patch.object(covering_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- COVER_DOMAIN,
- SERVICE_STOP_COVER_TILT,
- {ATTR_ENTITY_ID: "cover.covering_device"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/0/state", json={"stop": True})
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_STOP_COVER_TILT,
+ {ATTR_ENTITY_ID: "cover.covering_device"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[4][2] == {"stop": True}
diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py
index 14faf1a938c5c8..fc7544f39185eb 100644
--- a/tests/components/deconz/test_deconz_event.py
+++ b/tests/components/deconz/test_deconz_event.py
@@ -1,125 +1,203 @@
"""Test deCONZ remote events."""
-from copy import deepcopy
+from unittest.mock import patch
+from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN
from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT
-from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
+from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.helpers.device_registry import async_entries_for_config_entry
from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
from tests.common import async_capture_events
-SENSORS = {
- "1": {
- "id": "Switch 1 id",
- "name": "Switch 1",
- "type": "ZHASwitch",
- "state": {"buttonevent": 1000},
- "config": {},
- "uniqueid": "00:00:00:00:00:00:00:01-00",
- },
- "2": {
- "id": "Switch 2 id",
- "name": "Switch 2",
- "type": "ZHASwitch",
- "state": {"buttonevent": 1000},
- "config": {"battery": 100},
- "uniqueid": "00:00:00:00:00:00:00:02-00",
- },
- "3": {
- "id": "Switch 3 id",
- "name": "Switch 3",
- "type": "ZHASwitch",
- "state": {"buttonevent": 1000, "gesture": 1},
- "config": {"battery": 100},
- "uniqueid": "00:00:00:00:00:00:00:03-00",
- },
- "4": {
- "id": "Switch 4 id",
- "name": "Switch 4",
- "type": "ZHASwitch",
- "state": {"buttonevent": 1000, "gesture": 1},
- "config": {"battery": 100},
- "uniqueid": "00:00:00:00:00:00:00:04-00",
- },
- "5": {
- "id": "ZHA remote 1 id",
- "name": "ZHA remote 1",
- "type": "ZHASwitch",
- "state": {"angle": 0, "buttonevent": 1000, "xy": [0.0, 0.0]},
- "config": {"group": "4,5,6", "reachable": True, "on": True},
- "uniqueid": "00:00:00:00:00:00:00:05-00",
- },
-}
-
-
-async def test_deconz_events(hass):
+
+async def test_deconz_events(hass, aioclient_mock, mock_deconz_websocket):
"""Test successful creation of deconz events."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = deepcopy(SENSORS)
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ data = {
+ "sensors": {
+ "1": {
+ "name": "Switch 1",
+ "type": "ZHASwitch",
+ "state": {"buttonevent": 1000},
+ "config": {},
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ },
+ "2": {
+ "name": "Switch 2",
+ "type": "ZHASwitch",
+ "state": {"buttonevent": 1000},
+ "config": {"battery": 100},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
+ },
+ "3": {
+ "name": "Switch 3",
+ "type": "ZHASwitch",
+ "state": {"buttonevent": 1000, "gesture": 1},
+ "config": {"battery": 100},
+ "uniqueid": "00:00:00:00:00:00:00:03-00",
+ },
+ "4": {
+ "name": "Switch 4",
+ "type": "ZHASwitch",
+ "state": {"buttonevent": 1000, "gesture": 1},
+ "config": {"battery": 100},
+ "uniqueid": "00:00:00:00:00:00:00:04-00",
+ },
+ "5": {
+ "name": "ZHA remote 1",
+ "type": "ZHASwitch",
+ "state": {"angle": 0, "buttonevent": 1000, "xy": [0.0, 0.0]},
+ "config": {"group": "4,5,6", "reachable": True, "on": True},
+ "uniqueid": "00:00:00:00:00:00:00:05-00",
+ },
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
assert len(hass.states.async_all()) == 3
- assert len(gateway.events) == 5
- assert hass.states.get("sensor.switch_1") is None
- assert hass.states.get("sensor.switch_1_battery_level") is None
- assert hass.states.get("sensor.switch_2") is None
+ # 5 switches + 2 additional devices for deconz service and host
+ assert (
+ len(async_entries_for_config_entry(device_registry, config_entry.entry_id)) == 7
+ )
assert hass.states.get("sensor.switch_2_battery_level").state == "100"
+ assert hass.states.get("sensor.switch_3_battery_level").state == "100"
+ assert hass.states.get("sensor.switch_4_battery_level").state == "100"
- events = async_capture_events(hass, CONF_DECONZ_EVENT)
+ captured_events = async_capture_events(hass, CONF_DECONZ_EVENT)
- gateway.api.sensors["1"].update({"state": {"buttonevent": 2000}})
+ event_changed_sensor = {
+ "t": "event",
+ "e": "changed",
+ "r": "sensors",
+ "id": "1",
+ "state": {"buttonevent": 2000},
+ }
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
- assert len(events) == 1
- assert events[0].data == {
+ device = device_registry.async_get_device(
+ identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:01")}
+ )
+
+ assert len(captured_events) == 1
+ assert captured_events[0].data == {
"id": "switch_1",
"unique_id": "00:00:00:00:00:00:00:01",
"event": 2000,
- "device_id": gateway.events[0].device_id,
+ "device_id": device.id,
}
- gateway.api.sensors["3"].update({"state": {"buttonevent": 2000}})
+ event_changed_sensor = {
+ "t": "event",
+ "e": "changed",
+ "r": "sensors",
+ "id": "3",
+ "state": {"buttonevent": 2000},
+ }
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
- assert len(events) == 2
- assert events[1].data == {
+ device = device_registry.async_get_device(
+ identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:03")}
+ )
+
+ assert len(captured_events) == 2
+ assert captured_events[1].data == {
"id": "switch_3",
"unique_id": "00:00:00:00:00:00:00:03",
"event": 2000,
"gesture": 1,
- "device_id": gateway.events[2].device_id,
+ "device_id": device.id,
}
- gateway.api.sensors["4"].update({"state": {"gesture": 0}})
+ event_changed_sensor = {
+ "t": "event",
+ "e": "changed",
+ "r": "sensors",
+ "id": "4",
+ "state": {"gesture": 0},
+ }
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
- assert len(events) == 3
- assert events[2].data == {
+ device = device_registry.async_get_device(
+ identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:04")}
+ )
+
+ assert len(captured_events) == 3
+ assert captured_events[2].data == {
"id": "switch_4",
"unique_id": "00:00:00:00:00:00:00:04",
"event": 1000,
"gesture": 0,
- "device_id": gateway.events[3].device_id,
+ "device_id": device.id,
}
- gateway.api.sensors["5"].update(
- {"state": {"buttonevent": 6002, "angle": 110, "xy": [0.5982, 0.3897]}}
- )
+ event_changed_sensor = {
+ "t": "event",
+ "e": "changed",
+ "r": "sensors",
+ "id": "5",
+ "state": {"buttonevent": 6002, "angle": 110, "xy": [0.5982, 0.3897]},
+ }
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
- assert len(events) == 4
- assert events[3].data == {
+ device = device_registry.async_get_device(
+ identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:05")}
+ )
+
+ assert len(captured_events) == 4
+ assert captured_events[3].data == {
"id": "zha_remote_1",
"unique_id": "00:00:00:00:00:00:00:05",
"event": 6002,
"angle": 110,
"xy": [0.5982, 0.3897],
- "device_id": gateway.events[4].device_id,
+ "device_id": device.id,
}
await hass.config_entries.async_unload(config_entry.entry_id)
+ states = hass.states.async_all()
+ assert len(hass.states.async_all()) == 3
+ for state in states:
+ assert state.state == STATE_UNAVAILABLE
+
+ await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
- assert len(gateway.events) == 0
+
+
+async def test_deconz_events_bad_unique_id(hass, aioclient_mock, mock_deconz_websocket):
+ """Verify no devices are created if unique id is bad or missing."""
+ data = {
+ "sensors": {
+ "1": {
+ "name": "Switch 1 no unique id",
+ "type": "ZHASwitch",
+ "state": {"buttonevent": 1000},
+ "config": {},
+ },
+ "2": {
+ "name": "Switch 2 bad unique id",
+ "type": "ZHASwitch",
+ "state": {"buttonevent": 1000},
+ "config": {"battery": 100},
+ "uniqueid": "00:00-00",
+ },
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+
+ assert len(hass.states.async_all()) == 1
+ assert (
+ len(async_entries_for_config_entry(device_registry, config_entry.entry_id)) == 2
+ )
diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py
index a5399fe479638e..640a4b61ce31e5 100644
--- a/tests/components/deconz/test_device_trigger.py
+++ b/tests/components/deconz/test_device_trigger.py
@@ -1,11 +1,13 @@
"""deCONZ device automation tests."""
-from copy import deepcopy
+from unittest.mock import Mock, patch
+import pytest
+
+from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN
from homeassistant.components.deconz import device_trigger
from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN
from homeassistant.components.deconz.device_trigger import CONF_SUBTYPE
-from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
@@ -15,89 +17,105 @@
CONF_PLATFORM,
CONF_TYPE,
)
+from homeassistant.helpers.trigger import async_initialize_triggers
+from homeassistant.setup import async_setup_component
from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
-from tests.common import assert_lists_same, async_get_device_automations
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
-
-SENSORS = {
- "1": {
- "config": {
- "alert": "none",
- "battery": 60,
- "group": "10",
- "on": True,
- "reachable": True,
- },
- "ep": 1,
- "etag": "1b355c0b6d2af28febd7ca9165881952",
- "manufacturername": "IKEA of Sweden",
- "mode": 1,
- "modelid": "TRADFRI on/off switch",
- "name": "TRÅDFRI on/off switch ",
- "state": {"buttonevent": 2002, "lastupdated": "2019-09-07T07:39:39"},
- "swversion": "1.4.018",
- CONF_TYPE: "ZHASwitch",
- "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000",
- }
-}
+from tests.common import (
+ assert_lists_same,
+ async_get_device_automations,
+ async_mock_service,
+)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
-async def test_get_triggers(hass):
+@pytest.fixture
+def automation_calls(hass):
+ """Track automation calls to a mock service."""
+ return async_mock_service(hass, "test", "automation")
+
+
+async def test_get_triggers(hass, aioclient_mock):
"""Test triggers work."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = deepcopy(SENSORS)
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
- device_id = gateway.events[0].device_id
- triggers = await async_get_device_automations(hass, "trigger", device_id)
+ data = {
+ "sensors": {
+ "1": {
+ "config": {
+ "alert": "none",
+ "battery": 60,
+ "group": "10",
+ "on": True,
+ "reachable": True,
+ },
+ "ep": 1,
+ "etag": "1b355c0b6d2af28febd7ca9165881952",
+ "manufacturername": "IKEA of Sweden",
+ "mode": 1,
+ "modelid": "TRADFRI on/off switch",
+ "name": "TRÅDFRI on/off switch ",
+ "state": {"buttonevent": 2002, "lastupdated": "2019-09-07T07:39:39"},
+ "swversion": "1.4.018",
+ "type": "ZHASwitch",
+ "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000",
+ }
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get_device(
+ identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")}
+ )
+
+ triggers = await async_get_device_automations(hass, "trigger", device.id)
expected_triggers = [
{
- CONF_DEVICE_ID: device_id,
+ CONF_DEVICE_ID: device.id,
CONF_DOMAIN: DECONZ_DOMAIN,
CONF_PLATFORM: "device",
CONF_TYPE: device_trigger.CONF_SHORT_PRESS,
CONF_SUBTYPE: device_trigger.CONF_TURN_ON,
},
{
- CONF_DEVICE_ID: device_id,
+ CONF_DEVICE_ID: device.id,
CONF_DOMAIN: DECONZ_DOMAIN,
CONF_PLATFORM: "device",
CONF_TYPE: device_trigger.CONF_LONG_PRESS,
CONF_SUBTYPE: device_trigger.CONF_TURN_ON,
},
{
- CONF_DEVICE_ID: device_id,
+ CONF_DEVICE_ID: device.id,
CONF_DOMAIN: DECONZ_DOMAIN,
CONF_PLATFORM: "device",
CONF_TYPE: device_trigger.CONF_LONG_RELEASE,
CONF_SUBTYPE: device_trigger.CONF_TURN_ON,
},
{
- CONF_DEVICE_ID: device_id,
+ CONF_DEVICE_ID: device.id,
CONF_DOMAIN: DECONZ_DOMAIN,
CONF_PLATFORM: "device",
CONF_TYPE: device_trigger.CONF_SHORT_PRESS,
CONF_SUBTYPE: device_trigger.CONF_TURN_OFF,
},
{
- CONF_DEVICE_ID: device_id,
+ CONF_DEVICE_ID: device.id,
CONF_DOMAIN: DECONZ_DOMAIN,
CONF_PLATFORM: "device",
CONF_TYPE: device_trigger.CONF_LONG_PRESS,
CONF_SUBTYPE: device_trigger.CONF_TURN_OFF,
},
{
- CONF_DEVICE_ID: device_id,
+ CONF_DEVICE_ID: device.id,
CONF_DOMAIN: DECONZ_DOMAIN,
CONF_PLATFORM: "device",
CONF_TYPE: device_trigger.CONF_LONG_RELEASE,
CONF_SUBTYPE: device_trigger.CONF_TURN_OFF,
},
{
- CONF_DEVICE_ID: device_id,
+ CONF_DEVICE_ID: device.id,
CONF_DOMAIN: SENSOR_DOMAIN,
ATTR_ENTITY_ID: "sensor.tradfri_on_off_switch_battery_level",
CONF_PLATFORM: "device",
@@ -108,25 +126,279 @@ async def test_get_triggers(hass):
assert_lists_same(triggers, expected_triggers)
-async def test_helper_successful(hass):
- """Verify trigger helper."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = deepcopy(SENSORS)
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
- device_id = gateway.events[0].device_id
- deconz_event = device_trigger._get_deconz_event_from_device_id(hass, device_id)
- assert deconz_event == gateway.events[0]
+async def test_get_triggers_manage_unsupported_remotes(hass, aioclient_mock):
+ """Verify no triggers for an unsupported remote."""
+ data = {
+ "sensors": {
+ "1": {
+ "config": {
+ "alert": "none",
+ "group": "10",
+ "on": True,
+ "reachable": True,
+ },
+ "ep": 1,
+ "etag": "1b355c0b6d2af28febd7ca9165881952",
+ "manufacturername": "IKEA of Sweden",
+ "mode": 1,
+ "modelid": "Unsupported model",
+ "name": "TRÅDFRI on/off switch ",
+ "state": {"buttonevent": 2002, "lastupdated": "2019-09-07T07:39:39"},
+ "swversion": "1.4.018",
+ "type": "ZHASwitch",
+ "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000",
+ }
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get_device(
+ identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")}
+ )
+
+ triggers = await async_get_device_automations(hass, "trigger", device.id)
+
+ expected_triggers = []
+
+ assert_lists_same(triggers, expected_triggers)
+
+
+async def test_functional_device_trigger(
+ hass, aioclient_mock, mock_deconz_websocket, automation_calls
+):
+ """Test proper matching and attachment of device trigger automation."""
+ await async_setup_component(hass, "persistent_notification", {})
+
+ data = {
+ "sensors": {
+ "1": {
+ "config": {
+ "alert": "none",
+ "battery": 60,
+ "group": "10",
+ "on": True,
+ "reachable": True,
+ },
+ "ep": 1,
+ "etag": "1b355c0b6d2af28febd7ca9165881952",
+ "manufacturername": "IKEA of Sweden",
+ "mode": 1,
+ "modelid": "TRADFRI on/off switch",
+ "name": "TRÅDFRI on/off switch ",
+ "state": {"buttonevent": 2002, "lastupdated": "2019-09-07T07:39:39"},
+ "swversion": "1.4.018",
+ "type": "ZHASwitch",
+ "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000",
+ }
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get_device(
+ identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")}
+ )
+
+ assert await async_setup_component(
+ hass,
+ AUTOMATION_DOMAIN,
+ {
+ AUTOMATION_DOMAIN: [
+ {
+ "trigger": {
+ CONF_PLATFORM: "device",
+ CONF_DOMAIN: DECONZ_DOMAIN,
+ CONF_DEVICE_ID: device.id,
+ CONF_TYPE: device_trigger.CONF_SHORT_PRESS,
+ CONF_SUBTYPE: device_trigger.CONF_TURN_ON,
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {"some": "test_trigger_button_press"},
+ },
+ },
+ ]
+ },
+ )
+
+ assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 1
+
+ event_changed_sensor = {
+ "t": "event",
+ "e": "changed",
+ "r": "sensors",
+ "id": "1",
+ "state": {"buttonevent": 1002},
+ }
+ await mock_deconz_websocket(data=event_changed_sensor)
+ await hass.async_block_till_done()
+
+ assert len(automation_calls) == 1
+ assert automation_calls[0].data["some"] == "test_trigger_button_press"
+
+async def test_validate_trigger_unknown_device(
+ hass, aioclient_mock, mock_deconz_websocket
+):
+ """Test unknown device does not return a trigger config."""
+ await setup_deconz_integration(hass, aioclient_mock)
-async def test_helper_no_match(hass):
- """Verify trigger helper returns None when no event could be matched."""
- await setup_deconz_integration(hass)
- deconz_event = device_trigger._get_deconz_event_from_device_id(hass, "mock-id")
- assert deconz_event is None
+ assert await async_setup_component(
+ hass,
+ AUTOMATION_DOMAIN,
+ {
+ AUTOMATION_DOMAIN: [
+ {
+ "trigger": {
+ CONF_PLATFORM: "device",
+ CONF_DOMAIN: DECONZ_DOMAIN,
+ CONF_DEVICE_ID: "unknown device",
+ CONF_TYPE: device_trigger.CONF_SHORT_PRESS,
+ CONF_SUBTYPE: device_trigger.CONF_TURN_ON,
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {"some": "test_trigger_button_press"},
+ },
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 0
+
+
+async def test_validate_trigger_unsupported_device(
+ hass, aioclient_mock, mock_deconz_websocket
+):
+ """Test unsupported device doesn't return a trigger config."""
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")},
+ model="unsupported",
+ )
+
+ assert await async_setup_component(
+ hass,
+ AUTOMATION_DOMAIN,
+ {
+ AUTOMATION_DOMAIN: [
+ {
+ "trigger": {
+ CONF_PLATFORM: "device",
+ CONF_DOMAIN: DECONZ_DOMAIN,
+ CONF_DEVICE_ID: device.id,
+ CONF_TYPE: device_trigger.CONF_SHORT_PRESS,
+ CONF_SUBTYPE: device_trigger.CONF_TURN_ON,
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {"some": "test_trigger_button_press"},
+ },
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 0
+
+
+async def test_validate_trigger_unsupported_trigger(
+ hass, aioclient_mock, mock_deconz_websocket
+):
+ """Test unsupported trigger does not return a trigger config."""
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")},
+ model="TRADFRI on/off switch",
+ )
+
+ trigger_config = {
+ CONF_PLATFORM: "device",
+ CONF_DOMAIN: DECONZ_DOMAIN,
+ CONF_DEVICE_ID: device.id,
+ CONF_TYPE: "unsupported",
+ CONF_SUBTYPE: device_trigger.CONF_TURN_ON,
+ }
+
+ assert await async_setup_component(
+ hass,
+ AUTOMATION_DOMAIN,
+ {
+ AUTOMATION_DOMAIN: [
+ {
+ "trigger": trigger_config,
+ "action": {
+ "service": "test.automation",
+ "data_template": {"some": "test_trigger_button_press"},
+ },
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 0
+
+
+async def test_attach_trigger_no_matching_event(
+ hass, aioclient_mock, mock_deconz_websocket
+):
+ """Test no matching event for device doesn't return a trigger config."""
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")},
+ name="Tradfri switch",
+ model="TRADFRI on/off switch",
+ )
+
+ trigger_config = {
+ CONF_PLATFORM: "device",
+ CONF_DOMAIN: DECONZ_DOMAIN,
+ CONF_DEVICE_ID: device.id,
+ CONF_TYPE: device_trigger.CONF_SHORT_PRESS,
+ CONF_SUBTYPE: device_trigger.CONF_TURN_ON,
+ }
+
+ assert await async_setup_component(
+ hass,
+ AUTOMATION_DOMAIN,
+ {
+ AUTOMATION_DOMAIN: [
+ {
+ "trigger": trigger_config,
+ "action": {
+ "service": "test.automation",
+ "data_template": {"some": "test_trigger_button_press"},
+ },
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 1
-async def test_helper_no_gateway_exist(hass):
- """Verify trigger helper returns None when no gateway exist."""
- deconz_event = device_trigger._get_deconz_event_from_device_id(hass, "mock-id")
- assert deconz_event is None
+ # Assert that deCONZ async_attach_trigger raises InvalidDeviceAutomationConfig
+ assert not await async_initialize_triggers(
+ hass,
+ [trigger_config],
+ action=Mock(),
+ domain=AUTOMATION_DOMAIN,
+ name="mock-name",
+ log_cb=Mock(),
+ )
diff --git a/tests/components/deconz/test_fan.py b/tests/components/deconz/test_fan.py
index b9c154a2791c07..ddd4a4e46f48e7 100644
--- a/tests/components/deconz/test_fan.py
+++ b/tests/components/deconz/test_fan.py
@@ -1,15 +1,14 @@
"""deCONZ fan platform tests."""
-from copy import deepcopy
from unittest.mock import patch
import pytest
-from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN
-from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
from homeassistant.components.fan import (
+ ATTR_PERCENTAGE,
ATTR_SPEED,
DOMAIN as FAN_DOMAIN,
+ SERVICE_SET_PERCENTAGE,
SERVICE_SET_SPEED,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
@@ -18,193 +17,474 @@
SPEED_MEDIUM,
SPEED_OFF,
)
-from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
-from homeassistant.setup import async_setup_component
-
-from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
-
-FANS = {
- "1": {
- "etag": "432f3de28965052961a99e3c5494daf4",
- "hascolor": False,
- "manufacturername": "King Of Fans, Inc.",
- "modelid": "HDC52EastwindFan",
- "name": "Ceiling fan",
- "state": {
- "alert": "none",
- "bri": 254,
- "on": False,
- "reachable": True,
- "speed": 4,
- },
- "swversion": "0000000F",
- "type": "Fan",
- "uniqueid": "00:22:a3:00:00:27:8b:81-01",
- }
-}
-
+from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE
-async def test_platform_manually_configured(hass):
- """Test that we do not discover anything or try to set up a gateway."""
- assert (
- await async_setup_component(
- hass, FAN_DOMAIN, {"fan": {"platform": DECONZ_DOMAIN}}
- )
- is True
- )
- assert DECONZ_DOMAIN not in hass.data
+from .test_gateway import (
+ DECONZ_WEB_REQUEST,
+ mock_deconz_put_request,
+ setup_deconz_integration,
+)
-async def test_no_fans(hass):
+async def test_no_fans(hass, aioclient_mock):
"""Test that no fan entities are created."""
- await setup_deconz_integration(hass)
+ await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 0
-async def test_fans(hass):
+async def test_fans(hass, aioclient_mock, mock_deconz_websocket):
"""Test that all supported fan entities are created."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["lights"] = deepcopy(FANS)
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ data = {
+ "lights": {
+ "1": {
+ "etag": "432f3de28965052961a99e3c5494daf4",
+ "hascolor": False,
+ "manufacturername": "King Of Fans, Inc.",
+ "modelid": "HDC52EastwindFan",
+ "name": "Ceiling fan",
+ "state": {
+ "alert": "none",
+ "bri": 254,
+ "on": False,
+ "reachable": True,
+ "speed": 4,
+ },
+ "swversion": "0000000F",
+ "type": "Fan",
+ "uniqueid": "00:22:a3:00:00:27:8b:81-01",
+ }
+ }
+ }
+
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 2 # Light and fan
- assert hass.states.get("fan.ceiling_fan")
+ assert hass.states.get("fan.ceiling_fan").state == STATE_ON
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 100
# Test states
+ event_changed_light = {
+ "t": "event",
+ "e": "changed",
+ "r": "lights",
+ "id": "1",
+ "state": {"speed": 1},
+ }
+ await mock_deconz_websocket(data=event_changed_light)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("fan.ceiling_fan").state == STATE_ON
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 25
+
+ event_changed_light = {
+ "t": "event",
+ "e": "changed",
+ "r": "lights",
+ "id": "1",
+ "state": {"speed": 2},
+ }
+ await mock_deconz_websocket(data=event_changed_light)
+ await hass.async_block_till_done()
+
assert hass.states.get("fan.ceiling_fan").state == STATE_ON
- assert hass.states.get("fan.ceiling_fan").attributes["speed"] == SPEED_HIGH
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 50
- state_changed_event = {
+ event_changed_light = {
+ "t": "event",
+ "e": "changed",
+ "r": "lights",
+ "id": "1",
+ "state": {"speed": 3},
+ }
+ await mock_deconz_websocket(data=event_changed_light)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("fan.ceiling_fan").state == STATE_ON
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 75
+
+ event_changed_light = {
+ "t": "event",
+ "e": "changed",
+ "r": "lights",
+ "id": "1",
+ "state": {"speed": 4},
+ }
+ await mock_deconz_websocket(data=event_changed_light)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("fan.ceiling_fan").state == STATE_ON
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 100
+
+ event_changed_light = {
"t": "event",
"e": "changed",
"r": "lights",
"id": "1",
"state": {"speed": 0},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_light)
await hass.async_block_till_done()
assert hass.states.get("fan.ceiling_fan").state == STATE_OFF
- assert hass.states.get("fan.ceiling_fan").attributes["speed"] == SPEED_OFF
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 0
# Test service calls
- ceiling_fan_device = gateway.api.lights["1"]
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state")
- # Service turn on fan
+ # Service turn on fan using saved default_on_speed
- with patch.object(
- ceiling_fan_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- FAN_DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: "fan.ceiling_fan"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/1/state", json={"speed": 4})
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"speed": 4}
# Service turn off fan
- with patch.object(
- ceiling_fan_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- FAN_DOMAIN,
- SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: "fan.ceiling_fan"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/1/state", json={"speed": 0})
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[2][2] == {"speed": 0}
+
+ # Service turn on fan to 20%
+
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 20},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[3][2] == {"speed": 1}
+
+ # Service set fan percentage to 20%
+
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_SET_PERCENTAGE,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 20},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[4][2] == {"speed": 1}
+
+ # Service set fan percentage to 40%
+
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_SET_PERCENTAGE,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 40},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[5][2] == {"speed": 2}
+
+ # Service set fan percentage to 60%
+
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_SET_PERCENTAGE,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 60},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[6][2] == {"speed": 3}
+
+ # Service set fan percentage to 80%
+
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_SET_PERCENTAGE,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 80},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[7][2] == {"speed": 4}
+
+ # Service set fan percentage to 0% does not equal off
+
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_SET_PERCENTAGE,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 0},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[8][2] == {"speed": 1}
+
+ # Events with an unsupported speed does not get converted
+
+ event_changed_light = {
+ "t": "event",
+ "e": "changed",
+ "r": "lights",
+ "id": "1",
+ "state": {"speed": 5},
+ }
+ await mock_deconz_websocket(data=event_changed_light)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("fan.ceiling_fan").state == STATE_ON
+ assert not hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE]
+
+ await hass.config_entries.async_unload(config_entry.entry_id)
+
+ states = hass.states.async_all()
+ assert len(states) == 2
+ for state in states:
+ assert state.state == STATE_UNAVAILABLE
+
+ await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 0
+
+
+async def test_fans_legacy_speed_modes(hass, aioclient_mock, mock_deconz_websocket):
+ """Test that all supported fan entities are created.
+
+ Legacy fan support.
+ """
+ data = {
+ "lights": {
+ "1": {
+ "etag": "432f3de28965052961a99e3c5494daf4",
+ "hascolor": False,
+ "manufacturername": "King Of Fans, Inc.",
+ "modelid": "HDC52EastwindFan",
+ "name": "Ceiling fan",
+ "state": {
+ "alert": "none",
+ "bri": 254,
+ "on": False,
+ "reachable": True,
+ "speed": 4,
+ },
+ "swversion": "0000000F",
+ "type": "Fan",
+ "uniqueid": "00:22:a3:00:00:27:8b:81-01",
+ }
+ }
+ }
+
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
+
+ assert len(hass.states.async_all()) == 2 # Light and fan
+ assert hass.states.get("fan.ceiling_fan").state == STATE_ON
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_HIGH
+
+ # Test states
+
+ event_changed_light = {
+ "t": "event",
+ "e": "changed",
+ "r": "lights",
+ "id": "1",
+ "state": {"speed": 1},
+ }
+ await mock_deconz_websocket(data=event_changed_light)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("fan.ceiling_fan").state == STATE_ON
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 25
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_LOW
+
+ event_changed_light = {
+ "t": "event",
+ "e": "changed",
+ "r": "lights",
+ "id": "1",
+ "state": {"speed": 2},
+ }
+ await mock_deconz_websocket(data=event_changed_light)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("fan.ceiling_fan").state == STATE_ON
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 50
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_MEDIUM
+
+ event_changed_light = {
+ "t": "event",
+ "e": "changed",
+ "r": "lights",
+ "id": "1",
+ "state": {"speed": 3},
+ }
+ await mock_deconz_websocket(data=event_changed_light)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("fan.ceiling_fan").state == STATE_ON
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 75
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_MEDIUM
+
+ event_changed_light = {
+ "t": "event",
+ "e": "changed",
+ "r": "lights",
+ "id": "1",
+ "state": {"speed": 4},
+ }
+ await mock_deconz_websocket(data=event_changed_light)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("fan.ceiling_fan").state == STATE_ON
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 100
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_HIGH
+
+ event_changed_light = {
+ "t": "event",
+ "e": "changed",
+ "r": "lights",
+ "id": "1",
+ "state": {"speed": 0},
+ }
+ await mock_deconz_websocket(data=event_changed_light)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("fan.ceiling_fan").state == STATE_OFF
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 0
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_OFF
+
+ # Test service calls
+
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state")
+
+ # Service turn on fan using saved default_on_speed
+
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"speed": 4}
+
+ # Service turn on fan with speed_off
+ # async_turn_on_compat use speed_to_percentage which will return 0
+
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_OFF},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[2][2] == {"speed": 1}
+
+ # Service turn on fan with bad speed
+ # async_turn_on_compat use speed_to_percentage which will convert to SPEED_MEDIUM -> 2
+
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: "bad"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[3][2] == {"speed": 2}
+
+ # Service turn on fan to low speed
+
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_LOW},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[4][2] == {"speed": 1}
+
+ # Service turn on fan to medium speed
+
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_MEDIUM},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[5][2] == {"speed": 2}
+
+ # Service turn on fan to high speed
+
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_HIGH},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[6][2] == {"speed": 4}
# Service set fan speed to low
- with patch.object(
- ceiling_fan_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- FAN_DOMAIN,
- SERVICE_SET_SPEED,
- {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_LOW},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/1/state", json={"speed": 1})
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_SET_SPEED,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_LOW},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[7][2] == {"speed": 1}
# Service set fan speed to medium
- with patch.object(
- ceiling_fan_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- FAN_DOMAIN,
- SERVICE_SET_SPEED,
- {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_MEDIUM},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/1/state", json={"speed": 2})
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_SET_SPEED,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_MEDIUM},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[8][2] == {"speed": 2}
# Service set fan speed to high
- with patch.object(
- ceiling_fan_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- FAN_DOMAIN,
- SERVICE_SET_SPEED,
- {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_HIGH},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/1/state", json={"speed": 4})
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_SET_SPEED,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_HIGH},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[9][2] == {"speed": 4}
# Service set fan speed to off
- with patch.object(
- ceiling_fan_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- FAN_DOMAIN,
- SERVICE_SET_SPEED,
- {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_OFF},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/1/state", json={"speed": 0})
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_SET_SPEED,
+ {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_OFF},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[10][2] == {"speed": 0}
# Service set fan speed to unsupported value
- with patch.object(
- ceiling_fan_device, "_request", return_value=True
- ) as set_callback, pytest.raises(ValueError):
+ with pytest.raises(ValueError):
await hass.services.async_call(
FAN_DOMAIN,
SERVICE_SET_SPEED,
{ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: "bad value"},
blocking=True,
)
- await hass.async_block_till_done()
# Events with an unsupported speed gets converted to default speed "medium"
- state_changed_event = {
+ event_changed_light = {
"t": "event",
"e": "changed",
"r": "lights",
"id": "1",
"state": {"speed": 3},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_light)
await hass.async_block_till_done()
assert hass.states.get("fan.ceiling_fan").state == STATE_ON
- assert hass.states.get("fan.ceiling_fan").attributes["speed"] == SPEED_MEDIUM
+ assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_MEDIUM
await hass.config_entries.async_unload(config_entry.entry_id)
+ states = hass.states.async_all()
+ assert len(states) == 2
+ for state in states:
+ assert state.state == STATE_UNAVAILABLE
+
+ await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py
index 1790b6ed6e1f44..603644f47e355f 100644
--- a/tests/components/deconz/test_gateway.py
+++ b/tests/components/deconz/test_gateway.py
@@ -4,6 +4,7 @@
from unittest.mock import Mock, patch
import pydeconz
+from pydeconz.websocket import STATE_RETRYING, STATE_RUNNING
import pytest
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
@@ -29,21 +30,31 @@
)
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, SOURCE_SSDP
-from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
-from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.const import (
+ CONF_API_KEY,
+ CONF_HOST,
+ CONF_PORT,
+ CONTENT_TYPE_JSON,
+ STATE_OFF,
+ STATE_UNAVAILABLE,
+)
from tests.common import MockConfigEntry
API_KEY = "1234567890ABCDEF"
BRIDGEID = "01234E56789A"
+HOST = "1.2.3.4"
+PORT = 80
+
+DEFAULT_URL = f"http://{HOST}:{PORT}/api/{API_KEY}"
-ENTRY_CONFIG = {CONF_API_KEY: API_KEY, CONF_HOST: "1.2.3.4", CONF_PORT: 80}
+ENTRY_CONFIG = {CONF_API_KEY: API_KEY, CONF_HOST: HOST, CONF_PORT: PORT}
ENTRY_OPTIONS = {}
DECONZ_CONFIG = {
"bridgeid": BRIDGEID,
- "ipaddress": "1.2.3.4",
+ "ipaddress": HOST,
"mac": "00:11:22:33:44:55",
"modelid": "deCONZ",
"name": "deCONZ mock gateway",
@@ -60,12 +71,41 @@
}
+def mock_deconz_request(aioclient_mock, config, data):
+ """Mock a deCONZ get request."""
+ host = config[CONF_HOST]
+ port = config[CONF_PORT]
+ api_key = config[CONF_API_KEY]
+
+ aioclient_mock.get(
+ f"http://{host}:{port}/api/{api_key}",
+ json=deepcopy(data),
+ headers={"content-type": CONTENT_TYPE_JSON},
+ )
+
+
+def mock_deconz_put_request(aioclient_mock, config, path):
+ """Mock a deCONZ put request."""
+ host = config[CONF_HOST]
+ port = config[CONF_PORT]
+ api_key = config[CONF_API_KEY]
+
+ aioclient_mock.put(
+ f"http://{host}:{port}/api/{api_key}{path}",
+ json={},
+ headers={"content-type": CONTENT_TYPE_JSON},
+ )
+
+
async def setup_deconz_integration(
hass,
+ aioclient_mock=None,
+ *,
config=ENTRY_CONFIG,
options=ENTRY_OPTIONS,
get_state_response=DECONZ_WEB_REQUEST,
entry_id="1",
+ unique_id=BRIDGEID,
source="user",
):
"""Create the deCONZ gateway."""
@@ -76,25 +116,26 @@ async def setup_deconz_integration(
connection_class=CONN_CLASS_LOCAL_PUSH,
options=deepcopy(options),
entry_id=entry_id,
+ unique_id=unique_id,
)
config_entry.add_to_hass(hass)
- with patch(
- "pydeconz.DeconzSession.request", return_value=deepcopy(get_state_response)
- ), patch("pydeconz.DeconzSession.start", return_value=True):
- await hass.config_entries.async_setup(config_entry.entry_id)
+ if aioclient_mock:
+ mock_deconz_request(aioclient_mock, config, get_state_response)
+
+ await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return config_entry
-async def test_gateway_setup(hass):
+async def test_gateway_setup(hass, aioclient_mock):
"""Successful setup."""
with patch(
"homeassistant.config_entries.ConfigEntries.async_forward_entry_setup",
return_value=True,
) as forward_entry_setup:
- config_entry = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
gateway = get_gateway_from_config_entry(hass, config_entry)
assert gateway.bridgeid == BRIDGEID
assert gateway.master is True
@@ -138,26 +179,40 @@ async def test_gateway_setup_fails(hass):
assert not hass.data[DECONZ_DOMAIN]
-async def test_connection_status_signalling(hass):
+async def test_connection_status_signalling(
+ hass, aioclient_mock, mock_deconz_websocket
+):
"""Make sure that connection status triggers a dispatcher send."""
- config_entry = await setup_deconz_integration(hass)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ data = {
+ "sensors": {
+ "1": {
+ "name": "presence",
+ "type": "ZHAPresence",
+ "state": {"presence": False},
+ "config": {"on": True, "reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ }
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
+
+ assert hass.states.get("binary_sensor.presence").state == STATE_OFF
+
+ await mock_deconz_websocket(state=STATE_RETRYING)
+ await hass.async_block_till_done()
- event_call = Mock()
- unsub = async_dispatcher_connect(hass, gateway.signal_reachable, event_call)
+ assert hass.states.get("binary_sensor.presence").state == STATE_UNAVAILABLE
- gateway.async_connection_status_callback(False)
+ await mock_deconz_websocket(state=STATE_RUNNING)
await hass.async_block_till_done()
- assert gateway.available is False
- assert len(event_call.mock_calls) == 1
-
- unsub()
+ assert hass.states.get("binary_sensor.presence").state == STATE_OFF
-async def test_update_address(hass):
+async def test_update_address(hass, aioclient_mock):
"""Make sure that connection status triggers a dispatcher send."""
- config_entry = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
gateway = get_gateway_from_config_entry(hass, config_entry)
assert gateway.api.host == "1.2.3.4"
@@ -193,9 +248,9 @@ async def test_gateway_trigger_reauth_flow(hass):
assert hass.data[DECONZ_DOMAIN] == {}
-async def test_reset_after_successful_setup(hass):
+async def test_reset_after_successful_setup(hass, aioclient_mock):
"""Make sure that connection status triggers a dispatcher send."""
- config_entry = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
gateway = get_gateway_from_config_entry(hass, config_entry)
result = await gateway.async_reset()
diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py
index d408d764d0e9fc..6583372d7bd0df 100644
--- a/tests/components/deconz/test_init.py
+++ b/tests/components/deconz/test_init.py
@@ -1,19 +1,26 @@
"""Test deCONZ component setup process."""
import asyncio
-from copy import deepcopy
from unittest.mock import patch
from homeassistant.components.deconz import (
DeconzGateway,
async_setup_entry,
async_unload_entry,
+ async_update_group_unique_id,
)
-from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN
-from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
+from homeassistant.components.deconz.const import (
+ CONF_GROUP_ID_BASE,
+ DOMAIN as DECONZ_DOMAIN,
+)
+from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
+from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
+from homeassistant.helpers import entity_registry as er
from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
+from tests.common import MockConfigEntry
+
ENTRY1_HOST = "1.2.3.4"
ENTRY1_PORT = 80
ENTRY1_API_KEY = "1234567890ABCDEF"
@@ -49,56 +56,125 @@ async def test_setup_entry_no_available_bridge(hass):
assert not hass.data[DECONZ_DOMAIN]
-async def test_setup_entry_successful(hass):
+async def test_setup_entry_successful(hass, aioclient_mock):
"""Test setup entry is successful."""
- config_entry = await setup_deconz_integration(hass)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert hass.data[DECONZ_DOMAIN]
- assert gateway.bridgeid in hass.data[DECONZ_DOMAIN]
- assert hass.data[DECONZ_DOMAIN][gateway.bridgeid].master
+ assert config_entry.unique_id in hass.data[DECONZ_DOMAIN]
+ assert hass.data[DECONZ_DOMAIN][config_entry.unique_id].master
-async def test_setup_entry_multiple_gateways(hass):
+async def test_setup_entry_multiple_gateways(hass, aioclient_mock):
"""Test setup entry is successful with multiple gateways."""
- config_entry = await setup_deconz_integration(hass)
- gateway = get_gateway_from_config_entry(hass, config_entry)
-
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["config"]["bridgeid"] = "01234E56789B"
- config_entry2 = await setup_deconz_integration(
- hass, get_state_response=data, entry_id="2"
- )
- gateway2 = get_gateway_from_config_entry(hass, config_entry2)
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
+ aioclient_mock.clear_requests()
+
+ data = {"config": {"bridgeid": "01234E56789B"}}
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry2 = await setup_deconz_integration(
+ hass,
+ aioclient_mock,
+ entry_id="2",
+ unique_id="01234E56789B",
+ )
assert len(hass.data[DECONZ_DOMAIN]) == 2
- assert hass.data[DECONZ_DOMAIN][gateway.bridgeid].master
- assert not hass.data[DECONZ_DOMAIN][gateway2.bridgeid].master
+ assert hass.data[DECONZ_DOMAIN][config_entry.unique_id].master
+ assert not hass.data[DECONZ_DOMAIN][config_entry2.unique_id].master
-async def test_unload_entry(hass):
+async def test_unload_entry(hass, aioclient_mock):
"""Test being able to unload an entry."""
- config_entry = await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert hass.data[DECONZ_DOMAIN]
assert await async_unload_entry(hass, config_entry)
assert not hass.data[DECONZ_DOMAIN]
-async def test_unload_entry_multiple_gateways(hass):
+async def test_unload_entry_multiple_gateways(hass, aioclient_mock):
"""Test being able to unload an entry and master gateway gets moved."""
- config_entry = await setup_deconz_integration(hass)
-
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["config"]["bridgeid"] = "01234E56789B"
- config_entry2 = await setup_deconz_integration(
- hass, get_state_response=data, entry_id="2"
- )
- gateway2 = get_gateway_from_config_entry(hass, config_entry2)
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
+ aioclient_mock.clear_requests()
+
+ data = {"config": {"bridgeid": "01234E56789B"}}
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry2 = await setup_deconz_integration(
+ hass,
+ aioclient_mock,
+ entry_id="2",
+ unique_id="01234E56789B",
+ )
assert len(hass.data[DECONZ_DOMAIN]) == 2
assert await async_unload_entry(hass, config_entry)
assert len(hass.data[DECONZ_DOMAIN]) == 1
- assert hass.data[DECONZ_DOMAIN][gateway2.bridgeid].master
+ assert hass.data[DECONZ_DOMAIN][config_entry2.unique_id].master
+
+
+async def test_update_group_unique_id(hass):
+ """Test successful migration of entry data."""
+ old_unique_id = "123"
+ new_unique_id = "1234"
+ entry = MockConfigEntry(
+ domain=DECONZ_DOMAIN,
+ unique_id=new_unique_id,
+ data={
+ CONF_API_KEY: "1",
+ CONF_HOST: "2",
+ CONF_GROUP_ID_BASE: old_unique_id,
+ CONF_PORT: "3",
+ },
+ )
+
+ registry = er.async_get(hass)
+ # Create entity entry to migrate to new unique ID
+ registry.async_get_or_create(
+ LIGHT_DOMAIN,
+ DECONZ_DOMAIN,
+ f"{old_unique_id}-OLD",
+ suggested_object_id="old",
+ config_entry=entry,
+ )
+ # Create entity entry with new unique ID
+ registry.async_get_or_create(
+ LIGHT_DOMAIN,
+ DECONZ_DOMAIN,
+ f"{new_unique_id}-NEW",
+ suggested_object_id="new",
+ config_entry=entry,
+ )
+
+ await async_update_group_unique_id(hass, entry)
+
+ assert entry.data == {CONF_API_KEY: "1", CONF_HOST: "2", CONF_PORT: "3"}
+ assert registry.async_get(f"{LIGHT_DOMAIN}.old").unique_id == f"{new_unique_id}-OLD"
+ assert registry.async_get(f"{LIGHT_DOMAIN}.new").unique_id == f"{new_unique_id}-NEW"
+
+
+async def test_update_group_unique_id_no_legacy_group_id(hass):
+ """Test migration doesn't trigger without old legacy group id in entry data."""
+ old_unique_id = "123"
+ new_unique_id = "1234"
+ entry = MockConfigEntry(
+ domain=DECONZ_DOMAIN,
+ unique_id=new_unique_id,
+ data={},
+ )
+
+ registry = er.async_get(hass)
+ # Create entity entry to migrate to new unique ID
+ registry.async_get_or_create(
+ LIGHT_DOMAIN,
+ DECONZ_DOMAIN,
+ f"{old_unique_id}-OLD",
+ suggested_object_id="old",
+ config_entry=entry,
+ )
+
+ await async_update_group_unique_id(hass, entry)
+
+ assert registry.async_get(f"{LIGHT_DOMAIN}.old").unique_id == f"{old_unique_id}-OLD"
diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py
index 20fb50247eeb9d..84c7a9b1078696 100644
--- a/tests/components/deconz/test_light.py
+++ b/tests/components/deconz/test_light.py
@@ -1,15 +1,10 @@
"""deCONZ light platform tests."""
-from copy import deepcopy
from unittest.mock import patch
import pytest
-from homeassistant.components.deconz.const import (
- CONF_ALLOW_DECONZ_GROUPS,
- DOMAIN as DECONZ_DOMAIN,
-)
-from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
+from homeassistant.components.deconz.const import CONF_ALLOW_DECONZ_GROUPS
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
@@ -31,105 +26,91 @@
ATTR_SUPPORTED_FEATURES,
STATE_OFF,
STATE_ON,
+ STATE_UNAVAILABLE,
)
-from homeassistant.setup import async_setup_component
-
-from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
-
-GROUPS = {
- "1": {
- "id": "Light group id",
- "name": "Light group",
- "type": "LightGroup",
- "state": {"all_on": False, "any_on": True},
- "action": {},
- "scenes": [],
- "lights": ["1", "2"],
- },
- "2": {
- "id": "Empty group id",
- "name": "Empty group",
- "type": "LightGroup",
- "state": {},
- "action": {},
- "scenes": [],
- "lights": [],
- },
-}
-
-LIGHTS = {
- "1": {
- "id": "RGB light id",
- "name": "RGB light",
- "state": {
- "on": True,
- "bri": 255,
- "colormode": "xy",
- "effect": "colorloop",
- "xy": (500, 500),
- "reachable": True,
- },
- "type": "Extended color light",
- "uniqueid": "00:00:00:00:00:00:00:00-00",
- },
- "2": {
- "ctmax": 454,
- "ctmin": 155,
- "id": "Tunable white light id",
- "name": "Tunable white light",
- "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True},
- "type": "Tunable white light",
- "uniqueid": "00:00:00:00:00:00:00:01-00",
- },
- "3": {
- "id": "On off switch id",
- "name": "On off switch",
- "type": "On/Off plug-in unit",
- "state": {"reachable": True},
- "uniqueid": "00:00:00:00:00:00:00:02-00",
- },
- "4": {
- "name": "On off light",
- "state": {"on": True, "reachable": True},
- "type": "On and Off light",
- "uniqueid": "00:00:00:00:00:00:00:03-00",
- },
- "5": {
- "ctmax": 1000,
- "ctmin": 0,
- "id": "Tunable white light with bad maxmin values id",
- "name": "Tunable white light with bad maxmin values",
- "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True},
- "type": "Tunable white light",
- "uniqueid": "00:00:00:00:00:00:00:04-00",
- },
-}
-
-
-async def test_platform_manually_configured(hass):
- """Test that we do not discover anything or try to set up a gateway."""
- assert (
- await async_setup_component(
- hass, LIGHT_DOMAIN, {"light": {"platform": DECONZ_DOMAIN}}
- )
- is True
- )
- assert DECONZ_DOMAIN not in hass.data
+from .test_gateway import (
+ DECONZ_WEB_REQUEST,
+ mock_deconz_put_request,
+ setup_deconz_integration,
+)
-async def test_no_lights_or_groups(hass):
+
+async def test_no_lights_or_groups(hass, aioclient_mock):
"""Test that no lights or groups entities are created."""
- await setup_deconz_integration(hass)
+ await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 0
-async def test_lights_and_groups(hass):
+async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket):
"""Test that lights or groups entities are created."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["groups"] = deepcopy(GROUPS)
- data["lights"] = deepcopy(LIGHTS)
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ data = {
+ "groups": {
+ "1": {
+ "id": "Light group id",
+ "name": "Light group",
+ "type": "LightGroup",
+ "state": {"all_on": False, "any_on": True},
+ "action": {},
+ "scenes": [],
+ "lights": ["1", "2"],
+ },
+ "2": {
+ "id": "Empty group id",
+ "name": "Empty group",
+ "type": "LightGroup",
+ "state": {},
+ "action": {},
+ "scenes": [],
+ "lights": [],
+ },
+ },
+ "lights": {
+ "1": {
+ "name": "RGB light",
+ "state": {
+ "on": True,
+ "bri": 255,
+ "colormode": "xy",
+ "effect": "colorloop",
+ "xy": (500, 500),
+ "reachable": True,
+ },
+ "type": "Extended color light",
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ },
+ "2": {
+ "ctmax": 454,
+ "ctmin": 155,
+ "name": "Tunable white light",
+ "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True},
+ "type": "Tunable white light",
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ },
+ "3": {
+ "name": "On off switch",
+ "type": "On/Off plug-in unit",
+ "state": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
+ },
+ "4": {
+ "name": "On off light",
+ "state": {"on": True, "reachable": True},
+ "type": "On and Off light",
+ "uniqueid": "00:00:00:00:00:00:00:03-00",
+ },
+ "5": {
+ "ctmax": 1000,
+ "ctmin": 0,
+ "name": "Tunable white light with bad maxmin values",
+ "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True},
+ "type": "Tunable white light",
+ "uniqueid": "00:00:00:00:00:00:00:04-00",
+ },
+ },
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 6
@@ -160,168 +141,186 @@ async def test_lights_and_groups(hass):
assert on_off_light.state == STATE_ON
assert on_off_light.attributes[ATTR_SUPPORTED_FEATURES] == 0
- light_group = hass.states.get("light.light_group")
- assert light_group.state == STATE_ON
- assert light_group.attributes["all_on"] is False
+ assert hass.states.get("light.light_group").state == STATE_ON
+ assert hass.states.get("light.light_group").attributes["all_on"] is False
empty_group = hass.states.get("light.empty_group")
assert empty_group is None
- state_changed_event = {
+ event_changed_light = {
"t": "event",
"e": "changed",
"r": "lights",
"id": "1",
"state": {"on": False},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_light)
await hass.async_block_till_done()
- rgb_light = hass.states.get("light.rgb_light")
- assert rgb_light.state == STATE_OFF
+ assert hass.states.get("light.rgb_light").state == STATE_OFF
# Verify service calls
- rgb_light_device = gateway.api.lights["1"]
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state")
# Service turn on light with short color loop
- with patch.object(rgb_light_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_ON,
- {
- ATTR_ENTITY_ID: "light.rgb_light",
- ATTR_COLOR_TEMP: 2500,
- ATTR_BRIGHTNESS: 200,
- ATTR_TRANSITION: 5,
- ATTR_FLASH: FLASH_SHORT,
- ATTR_EFFECT: EFFECT_COLORLOOP,
- },
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with(
- "put",
- "/lights/1/state",
- json={
- "ct": 2500,
- "bri": 200,
- "transitiontime": 50,
- "alert": "select",
- "effect": "colorloop",
- },
- )
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: "light.rgb_light",
+ ATTR_COLOR_TEMP: 2500,
+ ATTR_BRIGHTNESS: 200,
+ ATTR_TRANSITION: 5,
+ ATTR_FLASH: FLASH_SHORT,
+ ATTR_EFFECT: EFFECT_COLORLOOP,
+ },
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {
+ "ct": 2500,
+ "bri": 200,
+ "transitiontime": 50,
+ "alert": "select",
+ "effect": "colorloop",
+ }
# Service turn on light disabling color loop with long flashing
- with patch.object(rgb_light_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_ON,
- {
- ATTR_ENTITY_ID: "light.rgb_light",
- ATTR_HS_COLOR: (20, 30),
- ATTR_FLASH: FLASH_LONG,
- ATTR_EFFECT: "None",
- },
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with(
- "put",
- "/lights/1/state",
- json={"xy": (0.411, 0.351), "alert": "lselect", "effect": "none"},
- )
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: "light.rgb_light",
+ ATTR_HS_COLOR: (20, 30),
+ ATTR_FLASH: FLASH_LONG,
+ ATTR_EFFECT: "None",
+ },
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[2][2] == {
+ "xy": (0.411, 0.351),
+ "alert": "lselect",
+ "effect": "none",
+ }
- # Service turn on light with short flashing
+ # Service turn on light with short flashing not supported
- with patch.object(rgb_light_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_OFF,
- {
- ATTR_ENTITY_ID: "light.rgb_light",
- ATTR_TRANSITION: 5,
- ATTR_FLASH: FLASH_SHORT,
- },
- blocking=True,
- )
- await hass.async_block_till_done()
- assert not set_callback.called
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {
+ ATTR_ENTITY_ID: "light.rgb_light",
+ ATTR_TRANSITION: 5,
+ ATTR_FLASH: FLASH_SHORT,
+ },
+ blocking=True,
+ )
+ assert len(aioclient_mock.mock_calls) == 3 # Not called
- state_changed_event = {
+ event_changed_light = {
"t": "event",
"e": "changed",
"r": "lights",
"id": "1",
"state": {"on": True},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_light)
await hass.async_block_till_done()
# Service turn off light with short flashing
- with patch.object(rgb_light_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_OFF,
- {
- ATTR_ENTITY_ID: "light.rgb_light",
- ATTR_TRANSITION: 5,
- ATTR_FLASH: FLASH_SHORT,
- },
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with(
- "put",
- "/lights/1/state",
- json={"bri": 0, "transitiontime": 50, "alert": "select"},
- )
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {
+ ATTR_ENTITY_ID: "light.rgb_light",
+ ATTR_TRANSITION: 5,
+ ATTR_FLASH: FLASH_SHORT,
+ },
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[3][2] == {
+ "bri": 0,
+ "transitiontime": 50,
+ "alert": "select",
+ }
# Service turn off light with long flashing
- with patch.object(rgb_light_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: "light.rgb_light", ATTR_FLASH: FLASH_LONG},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with(
- "put", "/lights/1/state", json={"alert": "lselect"}
- )
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "light.rgb_light", ATTR_FLASH: FLASH_LONG},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[4][2] == {"alert": "lselect"}
await hass.config_entries.async_unload(config_entry.entry_id)
+ states = hass.states.async_all()
+ assert len(states) == 6
+ for state in states:
+ assert state.state == STATE_UNAVAILABLE
+
+ await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
-async def test_disable_light_groups(hass):
+async def test_disable_light_groups(hass, aioclient_mock):
"""Test disallowing light groups work."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["groups"] = deepcopy(GROUPS)
- data["lights"] = deepcopy(LIGHTS)
- config_entry = await setup_deconz_integration(
- hass,
- options={CONF_ALLOW_DECONZ_GROUPS: False},
- get_state_response=data,
- )
+ data = {
+ "groups": {
+ "1": {
+ "id": "Light group id",
+ "name": "Light group",
+ "type": "LightGroup",
+ "state": {"all_on": False, "any_on": True},
+ "action": {},
+ "scenes": [],
+ "lights": ["1"],
+ },
+ "2": {
+ "id": "Empty group id",
+ "name": "Empty group",
+ "type": "LightGroup",
+ "state": {},
+ "action": {},
+ "scenes": [],
+ "lights": [],
+ },
+ },
+ "lights": {
+ "1": {
+ "ctmax": 454,
+ "ctmin": 155,
+ "name": "Tunable white light",
+ "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True},
+ "type": "Tunable white light",
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ },
+ },
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(
+ hass,
+ aioclient_mock,
+ options={CONF_ALLOW_DECONZ_GROUPS: False},
+ )
- assert len(hass.states.async_all()) == 5
- assert hass.states.get("light.rgb_light")
+ assert len(hass.states.async_all()) == 1
assert hass.states.get("light.tunable_white_light")
- assert hass.states.get("light.light_group") is None
- assert hass.states.get("light.empty_group") is None
+ assert not hass.states.get("light.light_group")
+ assert not hass.states.get("light.empty_group")
hass.config_entries.async_update_entry(
config_entry, options={CONF_ALLOW_DECONZ_GROUPS: True}
)
await hass.async_block_till_done()
- assert len(hass.states.async_all()) == 6
+ assert len(hass.states.async_all()) == 2
assert hass.states.get("light.light_group")
hass.config_entries.async_update_entry(
@@ -329,177 +328,205 @@ async def test_disable_light_groups(hass):
)
await hass.async_block_till_done()
- assert len(hass.states.async_all()) == 5
- assert hass.states.get("light.light_group") is None
-
-
-async def test_configuration_tool(hass):
- """Test that lights or groups entities are created."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["lights"] = {
- "0": {
- "etag": "26839cb118f5bf7ba1f2108256644010",
- "hascolor": False,
- "lastannounced": None,
- "lastseen": "2020-11-22T11:27Z",
- "manufacturername": "dresden elektronik",
- "modelid": "ConBee II",
- "name": "Configuration tool 1",
- "state": {"reachable": True},
- "swversion": "0x264a0700",
- "type": "Configuration tool",
- "uniqueid": "00:21:2e:ff:ff:05:a7:a3-01",
+ assert len(hass.states.async_all()) == 1
+ assert not hass.states.get("light.light_group")
+
+
+async def test_configuration_tool(hass, aioclient_mock):
+ """Test that configuration tool is not created."""
+ data = {
+ "lights": {
+ "0": {
+ "etag": "26839cb118f5bf7ba1f2108256644010",
+ "hascolor": False,
+ "lastannounced": None,
+ "lastseen": "2020-11-22T11:27Z",
+ "manufacturername": "dresden elektronik",
+ "modelid": "ConBee II",
+ "name": "Configuration tool 1",
+ "state": {"reachable": True},
+ "swversion": "0x264a0700",
+ "type": "Configuration tool",
+ "uniqueid": "00:21:2e:ff:ff:05:a7:a3-01",
+ }
}
}
- await setup_deconz_integration(hass, get_state_response=data)
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 0
-async def test_lidl_christmas_light(hass):
- """Test that lights or groups entities are created."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["lights"] = {
- "0": {
- "etag": "87a89542bf9b9d0aa8134919056844f8",
- "hascolor": True,
- "lastannounced": None,
- "lastseen": "2020-12-05T22:57Z",
- "manufacturername": "_TZE200_s8gkrkxk",
- "modelid": "TS0601",
- "name": "xmas light",
- "state": {
- "bri": 25,
- "colormode": "hs",
- "effect": "none",
- "hue": 53691,
- "on": True,
- "reachable": True,
- "sat": 141,
+async def test_ikea_default_transition_time(hass, aioclient_mock):
+ """Verify that service calls to IKEA lights always extend with transition tinme 0 if absent."""
+ data = {
+ "lights": {
+ "1": {
+ "manufacturername": "IKEA",
+ "name": "Dimmable light",
+ "state": {"on": True, "bri": 255, "reachable": True},
+ "type": "Dimmable light",
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
},
- "swversion": None,
- "type": "Color dimmable light",
- "uniqueid": "58:8e:81:ff:fe:db:7b:be-01",
+ },
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
+
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state")
+
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "light.dimmable_light", ATTR_BRIGHTNESS: 100},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {
+ "bri": 100,
+ "on": True,
+ "transitiontime": 0,
+ }
+
+
+async def test_lidl_christmas_light(hass, aioclient_mock):
+ """Test that lights or groups entities are created."""
+ data = {
+ "lights": {
+ "0": {
+ "etag": "87a89542bf9b9d0aa8134919056844f8",
+ "hascolor": True,
+ "lastannounced": None,
+ "lastseen": "2020-12-05T22:57Z",
+ "manufacturername": "_TZE200_s8gkrkxk",
+ "modelid": "TS0601",
+ "name": "xmas light",
+ "state": {
+ "bri": 25,
+ "colormode": "hs",
+ "effect": "none",
+ "hue": 53691,
+ "on": True,
+ "reachable": True,
+ "sat": 141,
+ },
+ "swversion": None,
+ "type": "Color dimmable light",
+ "uniqueid": "58:8e:81:ff:fe:db:7b:be-01",
+ }
}
}
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
- xmas_light_device = gateway.api.lights["0"]
- assert len(hass.states.async_all()) == 1
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
- with patch.object(xmas_light_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_ON,
- {
- ATTR_ENTITY_ID: "light.xmas_light",
- ATTR_HS_COLOR: (20, 30),
- },
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with(
- "put",
- "/lights/0/state",
- json={"on": True, "hue": 3640, "sat": 76},
- )
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state")
+
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: "light.xmas_light",
+ ATTR_HS_COLOR: (20, 30),
+ },
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"on": True, "hue": 3640, "sat": 76}
assert hass.states.get("light.xmas_light")
-async def test_non_color_light_reports_color(hass):
+async def test_non_color_light_reports_color(
+ hass, aioclient_mock, mock_deconz_websocket
+):
"""Verify hs_color does not crash when a group gets updated with a bad color value.
After calling a scene color temp light of certain manufacturers
report color temp in color space.
"""
- data = deepcopy(DECONZ_WEB_REQUEST)
-
- data["groups"] = {
- "0": {
- "action": {
- "alert": "none",
- "bri": 127,
- "colormode": "hs",
- "ct": 0,
- "effect": "none",
- "hue": 0,
- "on": True,
- "sat": 127,
- "scene": None,
- "xy": [0, 0],
- },
- "devicemembership": [],
- "etag": "81e42cf1b47affb72fa72bc2e25ba8bf",
- "id": "0",
- "lights": ["0", "1"],
- "name": "All",
- "scenes": [],
- "state": {"all_on": False, "any_on": True},
- "type": "LightGroup",
- }
- }
-
- data["lights"] = {
- "0": {
- "ctmax": 500,
- "ctmin": 153,
- "etag": "026bcfe544ad76c7534e5ca8ed39047c",
- "hascolor": True,
- "manufacturername": "dresden elektronik",
- "modelid": "FLS-PP3",
- "name": "Light 1",
- "pointsymbol": {},
- "state": {
- "alert": None,
- "bri": 111,
- "colormode": "ct",
- "ct": 307,
- "effect": None,
+ data = {
+ "groups": {
+ "0": {
+ "action": {
+ "alert": "none",
+ "bri": 127,
+ "colormode": "hs",
+ "ct": 0,
+ "effect": "none",
+ "hue": 0,
+ "on": True,
+ "sat": 127,
+ "scene": None,
+ "xy": [0, 0],
+ },
+ "devicemembership": [],
+ "etag": "81e42cf1b47affb72fa72bc2e25ba8bf",
+ "lights": ["0", "1"],
+ "name": "All",
+ "scenes": [],
+ "state": {"all_on": False, "any_on": True},
+ "type": "LightGroup",
+ }
+ },
+ "lights": {
+ "0": {
+ "ctmax": 500,
+ "ctmin": 153,
+ "etag": "026bcfe544ad76c7534e5ca8ed39047c",
"hascolor": True,
- "hue": 7998,
- "on": False,
- "reachable": True,
- "sat": 172,
- "xy": [0.421253, 0.39921],
+ "manufacturername": "dresden elektronik",
+ "modelid": "FLS-PP3",
+ "name": "Light 1",
+ "pointsymbol": {},
+ "state": {
+ "alert": None,
+ "bri": 111,
+ "colormode": "ct",
+ "ct": 307,
+ "effect": None,
+ "hascolor": True,
+ "hue": 7998,
+ "on": False,
+ "reachable": True,
+ "sat": 172,
+ "xy": [0.421253, 0.39921],
+ },
+ "swversion": "020C.201000A0",
+ "type": "Extended color light",
+ "uniqueid": "00:21:2E:FF:FF:EE:DD:CC-0A",
},
- "swversion": "020C.201000A0",
- "type": "Extended color light",
- "uniqueid": "00:21:2E:FF:FF:EE:DD:CC-0A",
- },
- "1": {
- "colorcapabilities": 0,
- "ctmax": 65535,
- "ctmin": 0,
- "etag": "9dd510cd474791481f189d2a68a3c7f1",
- "hascolor": True,
- "lastannounced": "2020-12-17T17:44:38Z",
- "lastseen": "2021-01-11T18:36Z",
- "manufacturername": "IKEA of Sweden",
- "modelid": "TRADFRI bulb E27 WS opal 1000lm",
- "name": "Küchenlicht",
- "state": {
- "alert": "none",
- "bri": 156,
- "colormode": "ct",
- "ct": 250,
- "on": True,
- "reachable": True,
+ "1": {
+ "colorcapabilities": 0,
+ "ctmax": 65535,
+ "ctmin": 0,
+ "etag": "9dd510cd474791481f189d2a68a3c7f1",
+ "hascolor": True,
+ "lastannounced": "2020-12-17T17:44:38Z",
+ "lastseen": "2021-01-11T18:36Z",
+ "manufacturername": "IKEA of Sweden",
+ "modelid": "TRADFRI bulb E27 WS opal 1000lm",
+ "name": "Küchenlicht",
+ "state": {
+ "alert": "none",
+ "bri": 156,
+ "colormode": "ct",
+ "ct": 250,
+ "on": True,
+ "reachable": True,
+ },
+ "swversion": "2.0.022",
+ "type": "Color temperature light",
+ "uniqueid": "ec:1b:bd:ff:fe:ee:ed:dd-01",
},
- "swversion": "2.0.022",
- "type": "Color temperature light",
- "uniqueid": "ec:1b:bd:ff:fe:ee:ed:dd-01",
},
}
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 3
assert hass.states.get("light.all").attributes[ATTR_COLOR_TEMP] == 307
# Updating a scene will return a faulty color value for a non-color light causing an exception in hs_color
- state_changed_event = {
+ event_changed_light = {
"e": "changed",
"id": "1",
"r": "lights",
@@ -514,7 +541,7 @@ async def test_non_color_light_reports_color(hass):
"t": "event",
"uniqueid": "ec:1b:bd:ff:fe:ee:ed:dd-01",
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_light)
await hass.async_block_till_done()
# Bug is fixed if we reach this point, but device won't have neither color temp nor color
@@ -523,11 +550,10 @@ async def test_non_color_light_reports_color(hass):
assert hass.states.get("light.all").attributes[ATTR_HS_COLOR]
-async def test_verify_group_supported_features(hass):
+async def test_verify_group_supported_features(hass, aioclient_mock):
"""Test that group supported features reflect what included lights support."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["groups"] = deepcopy(
- {
+ data = {
+ "groups": {
"1": {
"id": "Group1",
"name": "group",
@@ -537,19 +563,15 @@ async def test_verify_group_supported_features(hass):
"scenes": [],
"lights": ["1", "2", "3"],
},
- }
- )
- data["lights"] = deepcopy(
- {
+ },
+ "lights": {
"1": {
- "id": "light1",
"name": "Dimmable light",
"state": {"on": True, "bri": 255, "reachable": True},
"type": "Light",
"uniqueid": "00:00:00:00:00:00:00:01-00",
},
"2": {
- "id": "light2",
"name": "Color light",
"state": {
"on": True,
@@ -565,18 +587,17 @@ async def test_verify_group_supported_features(hass):
"3": {
"ctmax": 454,
"ctmin": 155,
- "id": "light3",
"name": "Tunable light",
"state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True},
"type": "Tunable white light",
"uniqueid": "00:00:00:00:00:00:00:03-00",
},
- }
- )
- await setup_deconz_integration(hass, get_state_response=data)
+ },
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 4
- group = hass.states.get("light.group")
- assert group.state == STATE_ON
- assert group.attributes[ATTR_SUPPORTED_FEATURES] == 63
+ assert hass.states.get("light.group").state == STATE_ON
+ assert hass.states.get("light.group").attributes[ATTR_SUPPORTED_FEATURES] == 63
diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py
index 7e9b8233778e95..5aff698149d0c2 100644
--- a/tests/components/deconz/test_lock.py
+++ b/tests/components/deconz/test_lock.py
@@ -1,107 +1,181 @@
"""deCONZ lock platform tests."""
-from copy import deepcopy
from unittest.mock import patch
-from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN
-from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
from homeassistant.components.lock import (
DOMAIN as LOCK_DOMAIN,
SERVICE_LOCK,
SERVICE_UNLOCK,
)
-from homeassistant.const import ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED
-from homeassistant.setup import async_setup_component
-
-from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
-
-LOCKS = {
- "1": {
- "etag": "5c2ec06cde4bd654aef3a555fcd8ad12",
- "hascolor": False,
- "lastannounced": None,
- "lastseen": "2020-08-22T15:29:03Z",
- "manufacturername": "Danalock",
- "modelid": "V3-BTZB",
- "name": "Door lock",
- "state": {"alert": "none", "on": False, "reachable": True},
- "swversion": "19042019",
- "type": "Door Lock",
- "uniqueid": "00:00:00:00:00:00:00:00-00",
- }
-}
-
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ STATE_LOCKED,
+ STATE_UNAVAILABLE,
+ STATE_UNLOCKED,
+)
-async def test_platform_manually_configured(hass):
- """Test that we do not discover anything or try to set up a gateway."""
- assert (
- await async_setup_component(
- hass, LOCK_DOMAIN, {"lock": {"platform": DECONZ_DOMAIN}}
- )
- is True
- )
- assert DECONZ_DOMAIN not in hass.data
+from .test_gateway import (
+ DECONZ_WEB_REQUEST,
+ mock_deconz_put_request,
+ setup_deconz_integration,
+)
-async def test_no_locks(hass):
+async def test_no_locks(hass, aioclient_mock):
"""Test that no lock entities are created."""
- await setup_deconz_integration(hass)
+ await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 0
-async def test_locks(hass):
- """Test that all supported lock entities are created."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["lights"] = deepcopy(LOCKS)
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+async def test_lock_from_light(hass, aioclient_mock, mock_deconz_websocket):
+ """Test that all supported lock entities based on lights are created."""
+ data = {
+ "lights": {
+ "1": {
+ "etag": "5c2ec06cde4bd654aef3a555fcd8ad12",
+ "hascolor": False,
+ "lastannounced": None,
+ "lastseen": "2020-08-22T15:29:03Z",
+ "manufacturername": "Danalock",
+ "modelid": "V3-BTZB",
+ "name": "Door lock",
+ "state": {"alert": "none", "on": False, "reachable": True},
+ "swversion": "19042019",
+ "type": "Door Lock",
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ }
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 1
assert hass.states.get("lock.door_lock").state == STATE_UNLOCKED
- door_lock = hass.states.get("lock.door_lock")
- assert door_lock.state == STATE_UNLOCKED
-
- state_changed_event = {
+ event_changed_light = {
"t": "event",
"e": "changed",
"r": "lights",
"id": "1",
"state": {"on": True},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_light)
await hass.async_block_till_done()
assert hass.states.get("lock.door_lock").state == STATE_LOCKED
# Verify service calls
- door_lock_device = gateway.api.lights["1"]
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state")
# Service lock door
- with patch.object(door_lock_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- LOCK_DOMAIN,
- SERVICE_LOCK,
- {ATTR_ENTITY_ID: "lock.door_lock"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/1/state", json={"on": True})
+ await hass.services.async_call(
+ LOCK_DOMAIN,
+ SERVICE_LOCK,
+ {ATTR_ENTITY_ID: "lock.door_lock"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"on": True}
# Service unlock door
- with patch.object(door_lock_device, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- LOCK_DOMAIN,
- SERVICE_UNLOCK,
- {ATTR_ENTITY_ID: "lock.door_lock"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/1/state", json={"on": False})
+ await hass.services.async_call(
+ LOCK_DOMAIN,
+ SERVICE_UNLOCK,
+ {ATTR_ENTITY_ID: "lock.door_lock"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[2][2] == {"on": False}
await hass.config_entries.async_unload(config_entry.entry_id)
+ states = hass.states.async_all()
+ assert len(states) == 1
+ for state in states:
+ assert state.state == STATE_UNAVAILABLE
+
+ await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 0
+
+
+async def test_lock_from_sensor(hass, aioclient_mock, mock_deconz_websocket):
+ """Test that all supported lock entities based on sensors are created."""
+ data = {
+ "sensors": {
+ "1": {
+ "config": {
+ "battery": 100,
+ "lock": False,
+ "on": True,
+ "reachable": True,
+ },
+ "ep": 11,
+ "etag": "a43862f76b7fa48b0fbb9107df123b0e",
+ "lastseen": "2021-03-06T22:25Z",
+ "manufacturername": "Onesti Products AS",
+ "modelid": "easyCodeTouch_v1",
+ "name": "Door lock",
+ "state": {
+ "lastupdated": "2021-03-06T21:25:45.624",
+ "lockstate": "unlocked",
+ },
+ "swversion": "20201211",
+ "type": "ZHADoorLock",
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ }
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
+
+ assert len(hass.states.async_all()) == 2
+ assert hass.states.get("lock.door_lock").state == STATE_UNLOCKED
+
+ event_changed_light = {
+ "t": "event",
+ "e": "changed",
+ "r": "sensors",
+ "id": "1",
+ "state": {"lockstate": "locked"},
+ }
+ await mock_deconz_websocket(data=event_changed_light)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("lock.door_lock").state == STATE_LOCKED
+
+ # Verify service calls
+
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/1/config")
+
+ # Service lock door
+
+ await hass.services.async_call(
+ LOCK_DOMAIN,
+ SERVICE_LOCK,
+ {ATTR_ENTITY_ID: "lock.door_lock"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"lock": True}
+
+ # Service unlock door
+
+ await hass.services.async_call(
+ LOCK_DOMAIN,
+ SERVICE_UNLOCK,
+ {ATTR_ENTITY_ID: "lock.door_lock"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[2][2] == {"lock": False}
+
+ await hass.config_entries.async_unload(config_entry.entry_id)
+
+ states = hass.states.async_all()
+ assert len(states) == 2
+ for state in states:
+ assert state.state == STATE_UNAVAILABLE
+
+ await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
diff --git a/tests/components/deconz/test_logbook.py b/tests/components/deconz/test_logbook.py
index 7315a766d5c7fd..e8e5244a2224a1 100644
--- a/tests/components/deconz/test_logbook.py
+++ b/tests/components/deconz/test_logbook.py
@@ -1,42 +1,83 @@
"""The tests for deCONZ logbook."""
-from copy import deepcopy
+from unittest.mock import patch
from homeassistant.components import logbook
+from homeassistant.components.deconz.const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN
from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT
-from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_UNIQUE_ID
from homeassistant.setup import async_setup_component
+from homeassistant.util import slugify
from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
from tests.components.logbook.test_init import MockLazyEventPartialState
-async def test_humanifying_deconz_event(hass):
+async def test_humanifying_deconz_event(hass, aioclient_mock):
"""Test humanifying deCONZ event."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = {
- "0": {
- "id": "Switch 1 id",
- "name": "Switch 1",
- "type": "ZHASwitch",
- "state": {"buttonevent": 1000},
- "config": {},
- "uniqueid": "00:00:00:00:00:00:00:01-00",
- },
- "1": {
- "id": "Hue remote id",
- "name": "Hue remote",
- "type": "ZHASwitch",
- "modelid": "RWL021",
- "state": {"buttonevent": 1000},
- "config": {},
- "uniqueid": "00:00:00:00:00:00:00:02-00",
- },
+ data = {
+ "sensors": {
+ "1": {
+ "name": "Switch 1",
+ "type": "ZHASwitch",
+ "state": {"buttonevent": 1000},
+ "config": {},
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ },
+ "2": {
+ "name": "Hue remote",
+ "type": "ZHASwitch",
+ "modelid": "RWL021",
+ "state": {"buttonevent": 1000},
+ "config": {},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
+ },
+ "3": {
+ "name": "Xiaomi cube",
+ "type": "ZHASwitch",
+ "modelid": "lumi.sensor_cube",
+ "state": {"buttonevent": 1000, "gesture": 1},
+ "config": {},
+ "uniqueid": "00:00:00:00:00:00:00:03-00",
+ },
+ "4": {
+ "name": "Faulty event",
+ "type": "ZHASwitch",
+ "state": {},
+ "config": {},
+ "uniqueid": "00:00:00:00:00:00:00:04-00",
+ },
+ }
}
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+
+ switch_event_id = slugify(data["sensors"]["1"]["name"])
+ switch_serial = data["sensors"]["1"]["uniqueid"].split("-", 1)[0]
+ switch_entry = device_registry.async_get_device(
+ identifiers={(DECONZ_DOMAIN, switch_serial)}
+ )
+
+ hue_remote_event_id = slugify(data["sensors"]["2"]["name"])
+ hue_remote_serial = data["sensors"]["2"]["uniqueid"].split("-", 1)[0]
+ hue_remote_entry = device_registry.async_get_device(
+ identifiers={(DECONZ_DOMAIN, hue_remote_serial)}
+ )
+
+ xiaomi_cube_event_id = slugify(data["sensors"]["3"]["name"])
+ xiaomi_cube_serial = data["sensors"]["3"]["uniqueid"].split("-", 1)[0]
+ xiaomi_cube_entry = device_registry.async_get_device(
+ identifiers={(DECONZ_DOMAIN, xiaomi_cube_serial)}
+ )
+
+ faulty_event_id = slugify(data["sensors"]["4"]["name"])
+ faulty_serial = data["sensors"]["4"]["uniqueid"].split("-", 1)[0]
+ faulty_entry = device_registry.async_get_device(
+ identifiers={(DECONZ_DOMAIN, faulty_serial)}
+ )
hass.config.components.add("recorder")
assert await async_setup_component(hass, "logbook", {})
@@ -46,22 +87,54 @@ async def test_humanifying_deconz_event(hass):
logbook.humanify(
hass,
[
+ # Event without matching device trigger
MockLazyEventPartialState(
CONF_DECONZ_EVENT,
{
- CONF_DEVICE_ID: gateway.events[0].device_id,
+ CONF_DEVICE_ID: switch_entry.id,
CONF_EVENT: 2000,
- CONF_ID: gateway.events[0].event_id,
- CONF_UNIQUE_ID: gateway.events[0].serial,
+ CONF_ID: switch_event_id,
+ CONF_UNIQUE_ID: switch_serial,
},
),
+ # Event with matching device trigger
MockLazyEventPartialState(
CONF_DECONZ_EVENT,
{
- CONF_DEVICE_ID: gateway.events[1].device_id,
+ CONF_DEVICE_ID: hue_remote_entry.id,
CONF_EVENT: 2001,
- CONF_ID: gateway.events[1].event_id,
- CONF_UNIQUE_ID: gateway.events[1].serial,
+ CONF_ID: hue_remote_event_id,
+ CONF_UNIQUE_ID: hue_remote_serial,
+ },
+ ),
+ # Gesture with matching device trigger
+ MockLazyEventPartialState(
+ CONF_DECONZ_EVENT,
+ {
+ CONF_DEVICE_ID: xiaomi_cube_entry.id,
+ CONF_GESTURE: 1,
+ CONF_ID: xiaomi_cube_event_id,
+ CONF_UNIQUE_ID: xiaomi_cube_serial,
+ },
+ ),
+ # Unsupported device trigger
+ MockLazyEventPartialState(
+ CONF_DECONZ_EVENT,
+ {
+ CONF_DEVICE_ID: xiaomi_cube_entry.id,
+ CONF_GESTURE: "unsupported_gesture",
+ CONF_ID: xiaomi_cube_event_id,
+ CONF_UNIQUE_ID: xiaomi_cube_serial,
+ },
+ ),
+ # Unknown event
+ MockLazyEventPartialState(
+ CONF_DECONZ_EVENT,
+ {
+ CONF_DEVICE_ID: faulty_entry.id,
+ "unknown_event": None,
+ CONF_ID: faulty_event_id,
+ CONF_UNIQUE_ID: faulty_serial,
},
),
],
@@ -77,3 +150,15 @@ async def test_humanifying_deconz_event(hass):
assert events[1]["name"] == "Hue remote"
assert events[1]["domain"] == "deconz"
assert events[1]["message"] == "'Long press' event for 'Dim up' was fired."
+
+ assert events[2]["name"] == "Xiaomi cube"
+ assert events[2]["domain"] == "deconz"
+ assert events[2]["message"] == "fired event 'Shake'."
+
+ assert events[3]["name"] == "Xiaomi cube"
+ assert events[3]["domain"] == "deconz"
+ assert events[3]["message"] == "fired event 'unsupported_gesture'."
+
+ assert events[4]["name"] == "Faulty event"
+ assert events[4]["domain"] == "deconz"
+ assert events[4]["message"] == "fired an unknown event."
diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py
index ca8df2c04259c1..189eb1e6eb7290 100644
--- a/tests/components/deconz/test_scene.py
+++ b/tests/components/deconz/test_scene.py
@@ -1,71 +1,59 @@
"""deCONZ scene platform tests."""
-from copy import deepcopy
from unittest.mock import patch
-from homeassistant.components.deconz import DOMAIN as DECONZ_DOMAIN
-from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, SERVICE_TURN_ON
from homeassistant.const import ATTR_ENTITY_ID
-from homeassistant.setup import async_setup_component
-from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
+from .test_gateway import (
+ DECONZ_WEB_REQUEST,
+ mock_deconz_put_request,
+ setup_deconz_integration,
+)
-GROUPS = {
- "1": {
- "id": "Light group id",
- "name": "Light group",
- "type": "LightGroup",
- "state": {"all_on": False, "any_on": True},
- "action": {},
- "scenes": [{"id": "1", "name": "Scene"}],
- "lights": [],
- }
-}
-
-
-async def test_platform_manually_configured(hass):
- """Test that we do not discover anything or try to set up a gateway."""
- assert (
- await async_setup_component(
- hass, SCENE_DOMAIN, {"scene": {"platform": DECONZ_DOMAIN}}
- )
- is True
- )
- assert DECONZ_DOMAIN not in hass.data
-
-async def test_no_scenes(hass):
+async def test_no_scenes(hass, aioclient_mock):
"""Test that scenes can be loaded without scenes being available."""
- await setup_deconz_integration(hass)
+ await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 0
-async def test_scenes(hass):
+async def test_scenes(hass, aioclient_mock):
"""Test that scenes works."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["groups"] = deepcopy(GROUPS)
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ data = {
+ "groups": {
+ "1": {
+ "id": "Light group id",
+ "name": "Light group",
+ "type": "LightGroup",
+ "state": {"all_on": False, "any_on": True},
+ "action": {},
+ "scenes": [{"id": "1", "name": "Scene"}],
+ "lights": [],
+ }
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 1
assert hass.states.get("scene.light_group_scene")
# Verify service calls
- group_scene = gateway.api.groups["1"].scenes["1"]
+ mock_deconz_put_request(
+ aioclient_mock, config_entry.data, "/groups/1/scenes/1/recall"
+ )
# Service turn on scene
- with patch.object(group_scene, "_request", return_value=True) as set_callback:
- await hass.services.async_call(
- SCENE_DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: "scene.light_group_scene"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/groups/1/scenes/1/recall", json={})
+ await hass.services.async_call(
+ SCENE_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "scene.light_group_scene"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {}
await hass.config_entries.async_unload(config_entry.entry_id)
diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py
index def2a1412e50c8..a4d4e0063664e6 100644
--- a/tests/components/deconz/test_sensor.py
+++ b/tests/components/deconz/test_sensor.py
@@ -1,184 +1,193 @@
"""deCONZ sensor platform tests."""
-from copy import deepcopy
+from datetime import timedelta
+from unittest.mock import patch
-from homeassistant.components.deconz.const import (
- CONF_ALLOW_CLIP_SENSOR,
- DOMAIN as DECONZ_DOMAIN,
-)
-from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
+from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR
+from homeassistant.components.deconz.sensor import ATTR_DAYLIGHT
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_POWER,
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
)
-from homeassistant.setup import async_setup_component
+from homeassistant.helpers import entity_registry as er
+from homeassistant.util import dt
from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
-SENSORS = {
- "1": {
- "id": "Light sensor id",
- "name": "Light level sensor",
- "type": "ZHALightLevel",
- "state": {"lightlevel": 30000, "dark": False},
- "config": {"on": True, "reachable": True, "temperature": 10},
- "uniqueid": "00:00:00:00:00:00:00:00-00",
- },
- "2": {
- "id": "Presence sensor id",
- "name": "Presence sensor",
- "type": "ZHAPresence",
- "state": {"presence": False},
- "config": {},
- "uniqueid": "00:00:00:00:00:00:00:01-00",
- },
- "3": {
- "id": "Switch 1 id",
- "name": "Switch 1",
- "type": "ZHASwitch",
- "state": {"buttonevent": 1000},
- "config": {},
- "uniqueid": "00:00:00:00:00:00:00:02-00",
- },
- "4": {
- "id": "Switch 2 id",
- "name": "Switch 2",
- "type": "ZHASwitch",
- "state": {"buttonevent": 1000},
- "config": {"battery": 100},
- "uniqueid": "00:00:00:00:00:00:00:03-00",
- },
- "5": {
- "id": "Daylight sensor id",
- "name": "Daylight sensor",
- "type": "Daylight",
- "state": {"daylight": True, "status": 130},
- "config": {},
- "uniqueid": "00:00:00:00:00:00:00:04-00",
- },
- "6": {
- "id": "Power sensor id",
- "name": "Power sensor",
- "type": "ZHAPower",
- "state": {"current": 2, "power": 6, "voltage": 3},
- "config": {"reachable": True},
- "uniqueid": "00:00:00:00:00:00:00:05-00",
- },
- "7": {
- "id": "Consumption id",
- "name": "Consumption sensor",
- "type": "ZHAConsumption",
- "state": {"consumption": 2, "power": 6},
- "config": {"reachable": True},
- "uniqueid": "00:00:00:00:00:00:00:06-00",
- },
- "8": {
- "id": "CLIP light sensor id",
- "name": "CLIP light level sensor",
- "type": "CLIPLightLevel",
- "state": {"lightlevel": 30000},
- "config": {"reachable": True},
- "uniqueid": "00:00:00:00:00:00:00:07-00",
- },
-}
-
-
-async def test_platform_manually_configured(hass):
- """Test that we do not discover anything or try to set up a gateway."""
- assert (
- await async_setup_component(
- hass, SENSOR_DOMAIN, {"sensor": {"platform": DECONZ_DOMAIN}}
- )
- is True
- )
- assert DECONZ_DOMAIN not in hass.data
+from tests.common import async_fire_time_changed
-async def test_no_sensors(hass):
+async def test_no_sensors(hass, aioclient_mock):
"""Test that no sensors in deconz results in no sensor entities."""
- await setup_deconz_integration(hass)
+ await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 0
-async def test_sensors(hass):
+async def test_sensors(hass, aioclient_mock, mock_deconz_websocket):
"""Test successful creation of sensor entities."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = deepcopy(SENSORS)
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ data = {
+ "sensors": {
+ "1": {
+ "name": "Light level sensor",
+ "type": "ZHALightLevel",
+ "state": {"daylight": 6955, "lightlevel": 30000, "dark": False},
+ "config": {"on": True, "reachable": True, "temperature": 10},
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ },
+ "2": {
+ "name": "Presence sensor",
+ "type": "ZHAPresence",
+ "state": {"presence": False},
+ "config": {},
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ },
+ "3": {
+ "name": "Switch 1",
+ "type": "ZHASwitch",
+ "state": {"buttonevent": 1000},
+ "config": {},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
+ },
+ "4": {
+ "name": "Switch 2",
+ "type": "ZHASwitch",
+ "state": {"buttonevent": 1000},
+ "config": {"battery": 100},
+ "uniqueid": "00:00:00:00:00:00:00:03-00",
+ },
+ "5": {
+ "name": "Power sensor",
+ "type": "ZHAPower",
+ "state": {"current": 2, "power": 6, "voltage": 3},
+ "config": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:05-00",
+ },
+ "6": {
+ "name": "Consumption sensor",
+ "type": "ZHAConsumption",
+ "state": {"consumption": 2, "power": 6},
+ "config": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:06-00",
+ },
+ "7": {
+ "id": "CLIP light sensor id",
+ "name": "CLIP light level sensor",
+ "type": "CLIPLightLevel",
+ "state": {"lightlevel": 30000},
+ "config": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:07-00",
+ },
+ }
+ }
+
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 5
light_level_sensor = hass.states.get("sensor.light_level_sensor")
assert light_level_sensor.state == "999.8"
- assert light_level_sensor.attributes["device_class"] == DEVICE_CLASS_ILLUMINANCE
+ assert light_level_sensor.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ILLUMINANCE
+ assert light_level_sensor.attributes[ATTR_DAYLIGHT] == 6955
- assert hass.states.get("sensor.presence_sensor") is None
- assert hass.states.get("sensor.switch_1") is None
- assert hass.states.get("sensor.switch_1_battery_level") is None
- assert hass.states.get("sensor.switch_2") is None
+ assert not hass.states.get("sensor.presence_sensor")
+ assert not hass.states.get("sensor.switch_1")
+ assert not hass.states.get("sensor.switch_1_battery_level")
+ assert not hass.states.get("sensor.switch_2")
switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level")
assert switch_2_battery_level.state == "100"
- assert switch_2_battery_level.attributes["device_class"] == DEVICE_CLASS_BATTERY
+ assert switch_2_battery_level.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_BATTERY
- assert hass.states.get("sensor.daylight_sensor") is None
+ assert not hass.states.get("sensor.daylight_sensor")
power_sensor = hass.states.get("sensor.power_sensor")
assert power_sensor.state == "6"
- assert power_sensor.attributes["device_class"] == DEVICE_CLASS_POWER
+ assert power_sensor.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER
consumption_sensor = hass.states.get("sensor.consumption_sensor")
assert consumption_sensor.state == "0.002"
- assert "device_class" not in consumption_sensor.attributes
+ assert ATTR_DEVICE_CLASS not in consumption_sensor.attributes
- assert hass.states.get("sensor.clip_light_level_sensor") is None
+ assert not hass.states.get("sensor.clip_light_level_sensor")
# Event signals new light level
- state_changed_event = {
+ event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "1",
"state": {"lightlevel": 2000},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_sensor)
assert hass.states.get("sensor.light_level_sensor").state == "1.6"
# Event signals new battery level
- state_changed_event = {
+ event_changed_sensor = {
"t": "event",
"e": "changed",
"r": "sensors",
"id": "4",
"config": {"battery": 75},
}
- gateway.api.event_handler(state_changed_event)
- await hass.async_block_till_done()
+ await mock_deconz_websocket(data=event_changed_sensor)
assert hass.states.get("sensor.switch_2_battery_level").state == "75"
+ # Unload entry
+
await hass.config_entries.async_unload(config_entry.entry_id)
+ states = hass.states.async_all()
+ assert len(states) == 5
+ for state in states:
+ assert state.state == STATE_UNAVAILABLE
+
+ # Remove entry
+
+ await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
-async def test_allow_clip_sensors(hass):
+async def test_allow_clip_sensors(hass, aioclient_mock):
"""Test that CLIP sensors can be allowed."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = deepcopy(SENSORS)
- config_entry = await setup_deconz_integration(
- hass,
- options={CONF_ALLOW_CLIP_SENSOR: True},
- get_state_response=data,
- )
+ data = {
+ "sensors": {
+ "1": {
+ "name": "Light level sensor",
+ "type": "ZHALightLevel",
+ "state": {"lightlevel": 30000, "dark": False},
+ "config": {"on": True, "reachable": True, "temperature": 10},
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ },
+ "2": {
+ "id": "CLIP light sensor id",
+ "name": "CLIP light level sensor",
+ "type": "CLIPLightLevel",
+ "state": {"lightlevel": 30000},
+ "config": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ },
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(
+ hass,
+ aioclient_mock,
+ options={CONF_ALLOW_CLIP_SENSOR: True},
+ )
- assert len(hass.states.async_all()) == 6
+ assert len(hass.states.async_all()) == 2
assert hass.states.get("sensor.clip_light_level_sensor").state == "999.8"
# Disallow clip sensors
@@ -188,8 +197,8 @@ async def test_allow_clip_sensors(hass):
)
await hass.async_block_till_done()
- assert len(hass.states.async_all()) == 5
- assert hass.states.get("sensor.clip_light_level_sensor") is None
+ assert len(hass.states.async_all()) == 1
+ assert not hass.states.get("sensor.clip_light_level_sensor")
# Allow clip sensors
@@ -198,125 +207,332 @@ async def test_allow_clip_sensors(hass):
)
await hass.async_block_till_done()
- assert len(hass.states.async_all()) == 6
- assert hass.states.get("sensor.clip_light_level_sensor")
+ assert len(hass.states.async_all()) == 2
+ assert hass.states.get("sensor.clip_light_level_sensor").state == "999.8"
-async def test_add_new_sensor(hass):
+async def test_add_new_sensor(hass, aioclient_mock, mock_deconz_websocket):
"""Test that adding a new sensor works."""
- config_entry = await setup_deconz_integration(hass)
- gateway = get_gateway_from_config_entry(hass, config_entry)
- assert len(hass.states.async_all()) == 0
-
- state_added_event = {
+ event_added_sensor = {
"t": "event",
"e": "added",
"r": "sensors",
"id": "1",
- "sensor": deepcopy(SENSORS["1"]),
+ "sensor": {
+ "id": "Light sensor id",
+ "name": "Light level sensor",
+ "type": "ZHALightLevel",
+ "state": {"lightlevel": 30000, "dark": False},
+ "config": {"on": True, "reachable": True, "temperature": 10},
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ },
}
- gateway.api.event_handler(state_added_event)
+
+ await setup_deconz_integration(hass, aioclient_mock)
+
+ assert len(hass.states.async_all()) == 0
+
+ await mock_deconz_websocket(data=event_added_sensor)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
assert hass.states.get("sensor.light_level_sensor").state == "999.8"
-async def test_add_battery_later(hass):
+async def test_add_battery_later(hass, aioclient_mock, mock_deconz_websocket):
"""Test that a sensor without an initial battery state creates a battery sensor once state exist."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = {"1": deepcopy(SENSORS["3"])}
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
- remote = gateway.api.sensors["1"]
+ data = {
+ "sensors": {
+ "1": {
+ "name": "Switch 1",
+ "type": "ZHASwitch",
+ "state": {"buttonevent": 1000},
+ "config": {},
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ }
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 0
- assert len(gateway.events) == 1
- assert len(remote._callbacks) == 2 # Event and battery tracker
+ assert not hass.states.get("sensor.switch_1_battery_level")
- remote.update({"config": {"battery": 50}})
+ event_changed_sensor = {
+ "t": "event",
+ "e": "changed",
+ "r": "sensors",
+ "id": "1",
+ "config": {"battery": 50},
+ }
+ await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
- assert len(gateway.events) == 1
- assert len(remote._callbacks) == 2 # Event and battery entity
- assert hass.states.get("sensor.switch_1_battery_level")
+ assert hass.states.get("sensor.switch_1_battery_level").state == "50"
+
+
+async def test_special_danfoss_battery_creation(hass, aioclient_mock):
+ """Test the special Danfoss battery creation works.
+
+ Normally there should only be one battery sensor per device from deCONZ.
+ With specific Danfoss devices each endpoint can report its own battery state.
+ """
+ data = {
+ "sensors": {
+ "1": {
+ "config": {
+ "battery": 70,
+ "heatsetpoint": 2300,
+ "offset": 0,
+ "on": True,
+ "reachable": True,
+ "schedule": {},
+ "schedule_on": False,
+ },
+ "ep": 1,
+ "etag": "982d9acc38bee5b251e24a9be26558e4",
+ "lastseen": "2021-02-15T12:23Z",
+ "manufacturername": "Danfoss",
+ "modelid": "0x8030",
+ "name": "0x8030",
+ "state": {
+ "lastupdated": "2021-02-15T12:23:07.994",
+ "on": False,
+ "temperature": 2307,
+ },
+ "swversion": "YYYYMMDD",
+ "type": "ZHAThermostat",
+ "uniqueid": "58:8e:81:ff:fe:00:11:22-01-0201",
+ },
+ "2": {
+ "config": {
+ "battery": 86,
+ "heatsetpoint": 2300,
+ "offset": 0,
+ "on": True,
+ "reachable": True,
+ "schedule": {},
+ "schedule_on": False,
+ },
+ "ep": 2,
+ "etag": "62f12749f9f51c950086aff37dd02b61",
+ "lastseen": "2021-02-15T12:23Z",
+ "manufacturername": "Danfoss",
+ "modelid": "0x8030",
+ "name": "0x8030",
+ "state": {
+ "lastupdated": "2021-02-15T12:23:22.399",
+ "on": False,
+ "temperature": 2316,
+ },
+ "swversion": "YYYYMMDD",
+ "type": "ZHAThermostat",
+ "uniqueid": "58:8e:81:ff:fe:00:11:22-02-0201",
+ },
+ "3": {
+ "config": {
+ "battery": 86,
+ "heatsetpoint": 2350,
+ "offset": 0,
+ "on": True,
+ "reachable": True,
+ "schedule": {},
+ "schedule_on": False,
+ },
+ "ep": 3,
+ "etag": "f50061174bb7f18a3d95789bab8b646d",
+ "lastseen": "2021-02-15T12:23Z",
+ "manufacturername": "Danfoss",
+ "modelid": "0x8030",
+ "name": "0x8030",
+ "state": {
+ "lastupdated": "2021-02-15T12:23:25.466",
+ "on": False,
+ "temperature": 2337,
+ },
+ "swversion": "YYYYMMDD",
+ "type": "ZHAThermostat",
+ "uniqueid": "58:8e:81:ff:fe:00:11:22-03-0201",
+ },
+ "4": {
+ "config": {
+ "battery": 85,
+ "heatsetpoint": 2300,
+ "offset": 0,
+ "on": True,
+ "reachable": True,
+ "schedule": {},
+ "schedule_on": False,
+ },
+ "ep": 4,
+ "etag": "eea97adf8ce1b971b8b6a3a31793f96b",
+ "lastseen": "2021-02-15T12:23Z",
+ "manufacturername": "Danfoss",
+ "modelid": "0x8030",
+ "name": "0x8030",
+ "state": {
+ "lastupdated": "2021-02-15T12:23:41.939",
+ "on": False,
+ "temperature": 2333,
+ },
+ "swversion": "YYYYMMDD",
+ "type": "ZHAThermostat",
+ "uniqueid": "58:8e:81:ff:fe:00:11:22-04-0201",
+ },
+ "5": {
+ "config": {
+ "battery": 83,
+ "heatsetpoint": 2300,
+ "offset": 0,
+ "on": True,
+ "reachable": True,
+ "schedule": {},
+ "schedule_on": False,
+ },
+ "ep": 5,
+ "etag": "1f7cd1a5d66dc27ac5eb44b8c47362fb",
+ "lastseen": "2021-02-15T12:23Z",
+ "manufacturername": "Danfoss",
+ "modelid": "0x8030",
+ "name": "0x8030",
+ "state": {"lastupdated": "none", "on": False, "temperature": 2325},
+ "swversion": "YYYYMMDD",
+ "type": "ZHAThermostat",
+ "uniqueid": "58:8e:81:ff:fe:00:11:22-05-0201",
+ },
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
+
+ assert len(hass.states.async_all()) == 10
+ assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 5
-async def test_air_quality_sensor(hass):
+async def test_air_quality_sensor(hass, aioclient_mock):
"""Test successful creation of air quality sensor entities."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = {
- "0": {
- "config": {"on": True, "reachable": True},
- "ep": 2,
- "etag": "c2d2e42396f7c78e11e46c66e2ec0200",
- "lastseen": "2020-11-20T22:48Z",
- "manufacturername": "BOSCH",
- "modelid": "AIR",
- "name": "Air quality",
- "state": {
- "airquality": "poor",
- "airqualityppb": 809,
- "lastupdated": "2020-11-20T22:48:00.209",
- },
- "swversion": "20200402",
- "type": "ZHAAirQuality",
- "uniqueid": "00:12:4b:00:14:4d:00:07-02-fdef",
+ data = {
+ "sensors": {
+ "0": {
+ "config": {"on": True, "reachable": True},
+ "ep": 2,
+ "etag": "c2d2e42396f7c78e11e46c66e2ec0200",
+ "lastseen": "2020-11-20T22:48Z",
+ "manufacturername": "BOSCH",
+ "modelid": "AIR",
+ "name": "Air quality",
+ "state": {
+ "airquality": "poor",
+ "airqualityppb": 809,
+ "lastupdated": "2020-11-20T22:48:00.209",
+ },
+ "swversion": "20200402",
+ "type": "ZHAAirQuality",
+ "uniqueid": "00:12:4b:00:14:4d:00:07-02-fdef",
+ }
}
}
- await setup_deconz_integration(hass, get_state_response=data)
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 1
+ assert hass.states.get("sensor.air_quality").state == "poor"
+
+
+async def test_daylight_sensor(hass, aioclient_mock):
+ """Test daylight sensor is disabled by default and when created has expected attributes."""
+ data = {
+ "sensors": {
+ "0": {
+ "config": {
+ "configured": True,
+ "on": True,
+ "sunriseoffset": 30,
+ "sunsetoffset": -30,
+ },
+ "etag": "55047cf652a7e594d0ee7e6fae01dd38",
+ "manufacturername": "Philips",
+ "modelid": "PHDL00",
+ "name": "Daylight sensor",
+ "state": {
+ "daylight": True,
+ "lastupdated": "2018-03-24T17:26:12",
+ "status": 170,
+ },
+ "swversion": "1.0",
+ "type": "Daylight",
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ }
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
+
+ assert len(hass.states.async_all()) == 0
+ assert not hass.states.get("sensor.daylight_sensor")
- air_quality = hass.states.get("sensor.air_quality")
- assert air_quality.state == "poor"
+ # Enable in entity registry
+ entity_registry = er.async_get(hass)
+ entity_registry.async_update_entity(
+ entity_id="sensor.daylight_sensor", disabled_by=None
+ )
+ await hass.async_block_till_done()
+
+ async_fire_time_changed(
+ hass,
+ dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
+ )
+ await hass.async_block_till_done()
-async def test_time_sensor(hass):
+ assert len(hass.states.async_all()) == 1
+ assert hass.states.get("sensor.daylight_sensor")
+ assert hass.states.get("sensor.daylight_sensor").attributes[ATTR_DAYLIGHT]
+
+
+async def test_time_sensor(hass, aioclient_mock):
"""Test successful creation of time sensor entities."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = {
- "0": {
- "config": {"battery": 40, "on": True, "reachable": True},
- "ep": 1,
- "etag": "28e796678d9a24712feef59294343bb6",
- "lastseen": "2020-11-22T11:26Z",
- "manufacturername": "Danfoss",
- "modelid": "eTRV0100",
- "name": "Time",
- "state": {
- "lastset": "2020-11-19T08:07:08Z",
- "lastupdated": "2020-11-22T10:51:03.444",
- "localtime": "2020-11-22T10:51:01",
- "utc": "2020-11-22T10:51:01Z",
- },
- "swversion": "20200429",
- "type": "ZHATime",
- "uniqueid": "cc:cc:cc:ff:fe:38:4d:b3-01-000a",
+ data = {
+ "sensors": {
+ "0": {
+ "config": {"battery": 40, "on": True, "reachable": True},
+ "ep": 1,
+ "etag": "28e796678d9a24712feef59294343bb6",
+ "lastseen": "2020-11-22T11:26Z",
+ "manufacturername": "Danfoss",
+ "modelid": "eTRV0100",
+ "name": "Time",
+ "state": {
+ "lastset": "2020-11-19T08:07:08Z",
+ "lastupdated": "2020-11-22T10:51:03.444",
+ "localtime": "2020-11-22T10:51:01",
+ "utc": "2020-11-22T10:51:01Z",
+ },
+ "swversion": "20200429",
+ "type": "ZHATime",
+ "uniqueid": "cc:cc:cc:ff:fe:38:4d:b3-01-000a",
+ }
}
}
- await setup_deconz_integration(hass, get_state_response=data)
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 2
+ assert hass.states.get("sensor.time").state == "2020-11-19T08:07:08Z"
+ assert hass.states.get("sensor.time_battery_level").state == "40"
- time = hass.states.get("sensor.time")
- assert time.state == "2020-11-19T08:07:08Z"
- time_battery = hass.states.get("sensor.time_battery_level")
- assert time_battery.state == "40"
-
-
-async def test_unsupported_sensor(hass):
+async def test_unsupported_sensor(hass, aioclient_mock):
"""Test that unsupported sensors doesn't break anything."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["sensors"] = {
- "0": {"type": "not supported", "name": "name", "state": {}, "config": {}}
+ data = {
+ "sensors": {
+ "0": {"type": "not supported", "name": "name", "state": {}, "config": {}}
+ }
}
- await setup_deconz_integration(hass, get_state_response=data)
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 1
-
- unsupported_sensor = hass.states.get("sensor.name")
- assert unsupported_sensor.state == "unknown"
+ assert hass.states.get("sensor.name").state == STATE_UNKNOWN
diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py
index faa1d3485bbd4d..249f4dbbb574da 100644
--- a/tests/components/deconz/test_services.py
+++ b/tests/components/deconz/test_services.py
@@ -1,6 +1,4 @@
"""deCONZ service tests."""
-
-from copy import deepcopy
from unittest.mock import Mock, patch
import pytest
@@ -10,7 +8,7 @@
CONF_BRIDGE_ID,
DOMAIN as DECONZ_DOMAIN,
)
-from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
+from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT
from homeassistant.components.deconz.services import (
DECONZ_SERVICES,
SERVICE_CONFIGURE_DEVICE,
@@ -23,53 +21,18 @@
async_unload_services,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.entity_registry import async_entries_for_config_entry
-from .test_gateway import BRIDGEID, DECONZ_WEB_REQUEST, setup_deconz_integration
-
-GROUP = {
- "1": {
- "id": "Group 1 id",
- "name": "Group 1 name",
- "type": "LightGroup",
- "state": {},
- "action": {},
- "scenes": [{"id": "1", "name": "Scene 1"}],
- "lights": ["1"],
- }
-}
-
-LIGHT = {
- "1": {
- "id": "Light 1 id",
- "name": "Light 1 name",
- "state": {"reachable": True},
- "type": "Light",
- "uniqueid": "00:00:00:00:00:00:00:01-00",
- }
-}
-
-SENSOR = {
- "1": {
- "id": "Sensor 1 id",
- "name": "Sensor 1 name",
- "type": "ZHALightLevel",
- "state": {"lightlevel": 30000, "dark": False},
- "config": {"reachable": True},
- "uniqueid": "00:00:00:00:00:00:00:02-00",
- }
-}
+from .test_gateway import (
+ BRIDGEID,
+ DECONZ_WEB_REQUEST,
+ mock_deconz_put_request,
+ mock_deconz_request,
+ setup_deconz_integration,
+)
-SWITCH = {
- "1": {
- "id": "Switch 1 id",
- "name": "Switch 1",
- "type": "ZHASwitch",
- "state": {"buttonevent": 1000, "gesture": 1},
- "config": {"battery": 100},
- "uniqueid": "00:00:00:00:00:00:00:03-00",
- },
-}
+from tests.common import async_capture_events
async def test_service_setup(hass):
@@ -114,72 +77,84 @@ async def test_service_unload_not_registered(hass):
async_remove.assert_not_called()
-async def test_configure_service_with_field(hass):
+async def test_configure_service_with_field(hass, aioclient_mock):
"""Test that service invokes pydeconz with the correct path and data."""
- await setup_deconz_integration(hass)
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
data = {
- SERVICE_FIELD: "/light/2",
+ SERVICE_FIELD: "/lights/2",
CONF_BRIDGE_ID: BRIDGEID,
SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20},
}
- with patch("pydeconz.DeconzSession.request", return_value=Mock(True)) as put_state:
- await hass.services.async_call(
- DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data
- )
- await hass.async_block_till_done()
- put_state.assert_called_with(
- "put", "/light/2", json={"on": True, "attr1": 10, "attr2": 20}
- )
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/2")
+ await hass.services.async_call(
+ DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data, blocking=True
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"on": True, "attr1": 10, "attr2": 20}
-async def test_configure_service_with_entity(hass):
+
+async def test_configure_service_with_entity(hass, aioclient_mock):
"""Test that service invokes pydeconz with the correct path and data."""
- config_entry = await setup_deconz_integration(hass)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ data = {
+ "lights": {
+ "1": {
+ "name": "Test",
+ "state": {"reachable": True},
+ "type": "Light",
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ }
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
- gateway.deconz_ids["light.test"] = "/light/1"
data = {
SERVICE_ENTITY: "light.test",
SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20},
}
- with patch("pydeconz.DeconzSession.request", return_value=Mock(True)) as put_state:
- await hass.services.async_call(
- DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data
- )
- await hass.async_block_till_done()
- put_state.assert_called_with(
- "put", "/light/1", json={"on": True, "attr1": 10, "attr2": 20}
- )
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1")
+
+ await hass.services.async_call(
+ DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data, blocking=True
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"on": True, "attr1": 10, "attr2": 20}
-async def test_configure_service_with_entity_and_field(hass):
+async def test_configure_service_with_entity_and_field(hass, aioclient_mock):
"""Test that service invokes pydeconz with the correct path and data."""
- config_entry = await setup_deconz_integration(hass)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ data = {
+ "lights": {
+ "1": {
+ "name": "Test",
+ "state": {"reachable": True},
+ "type": "Light",
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ }
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
- gateway.deconz_ids["light.test"] = "/light/1"
data = {
SERVICE_ENTITY: "light.test",
SERVICE_FIELD: "/state",
SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20},
}
- with patch("pydeconz.DeconzSession.request", return_value=Mock(True)) as put_state:
- await hass.services.async_call(
- DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data
- )
- await hass.async_block_till_done()
- put_state.assert_called_with(
- "put", "/light/1/state", json={"on": True, "attr1": 10, "attr2": 20}
- )
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state")
+ await hass.services.async_call(
+ DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data, blocking=True
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"on": True, "attr1": 10, "attr2": 20}
-async def test_configure_service_with_faulty_field(hass):
+
+async def test_configure_service_with_faulty_field(hass, aioclient_mock):
"""Test that service invokes pydeconz with the correct path and data."""
- await setup_deconz_integration(hass)
+ await setup_deconz_integration(hass, aioclient_mock)
data = {SERVICE_FIELD: "light/2", SERVICE_DATA: {}}
@@ -190,57 +165,162 @@ async def test_configure_service_with_faulty_field(hass):
await hass.async_block_till_done()
-async def test_configure_service_with_faulty_entity(hass):
+async def test_configure_service_with_faulty_entity(hass, aioclient_mock):
"""Test that service invokes pydeconz with the correct path and data."""
- await setup_deconz_integration(hass)
+ await setup_deconz_integration(hass, aioclient_mock)
+ aioclient_mock.clear_requests()
data = {
SERVICE_ENTITY: "light.nonexisting",
SERVICE_DATA: {},
}
- with patch("pydeconz.DeconzSession.request", return_value=Mock(True)) as put_state:
- await hass.services.async_call(
- DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data
- )
- await hass.async_block_till_done()
- put_state.assert_not_called()
+ await hass.services.async_call(
+ DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data
+ )
+ await hass.async_block_till_done()
+
+ assert len(aioclient_mock.mock_calls) == 0
-async def test_service_refresh_devices(hass):
+async def test_service_refresh_devices(hass, aioclient_mock):
"""Test that service can refresh devices."""
- config_entry = await setup_deconz_integration(hass)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
- data = {CONF_BRIDGE_ID: BRIDGEID}
+ assert len(hass.states.async_all()) == 0
- with patch(
- "pydeconz.DeconzSession.request",
- return_value={"groups": GROUP, "lights": LIGHT, "sensors": SENSOR},
- ):
- await hass.services.async_call(
- DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH, service_data=data
- )
- await hass.async_block_till_done()
+ aioclient_mock.clear_requests()
- assert gateway.deconz_ids == {
- "light.group_1_name": "/groups/1",
- "light.light_1_name": "/lights/1",
- "scene.group_1_name_scene_1": "/groups/1/scenes/1",
- "sensor.sensor_1_name": "/sensors/1",
+ data = {
+ "groups": {
+ "1": {
+ "id": "Group 1 id",
+ "name": "Group 1 name",
+ "type": "LightGroup",
+ "state": {},
+ "action": {},
+ "scenes": [{"id": "1", "name": "Scene 1"}],
+ "lights": ["1"],
+ }
+ },
+ "lights": {
+ "1": {
+ "name": "Light 1 name",
+ "state": {"reachable": True},
+ "type": "Light",
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ }
+ },
+ "sensors": {
+ "1": {
+ "name": "Sensor 1 name",
+ "type": "ZHALightLevel",
+ "state": {"lightlevel": 30000, "dark": False},
+ "config": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
+ }
+ },
}
+ mock_deconz_request(aioclient_mock, config_entry.data, data)
-async def test_remove_orphaned_entries_service(hass):
- """Test service works and also don't remove more than expected."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["lights"] = deepcopy(LIGHT)
- data["sensors"] = deepcopy(SWITCH)
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ await hass.services.async_call(
+ DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH, service_data={CONF_BRIDGE_ID: BRIDGEID}
+ )
+ await hass.async_block_till_done()
- data = {CONF_BRIDGE_ID: BRIDGEID}
+ assert len(hass.states.async_all()) == 4
+
+
+async def test_service_refresh_devices_trigger_no_state_update(hass, aioclient_mock):
+ """Verify that gateway.ignore_state_updates are honored."""
+ data = {
+ "sensors": {
+ "1": {
+ "name": "Switch 1",
+ "type": "ZHASwitch",
+ "state": {"buttonevent": 1000},
+ "config": {"battery": 100},
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ }
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
+
+ assert len(hass.states.async_all()) == 1
+
+ captured_events = async_capture_events(hass, CONF_DECONZ_EVENT)
+
+ aioclient_mock.clear_requests()
+
+ data = {
+ "groups": {
+ "1": {
+ "id": "Group 1 id",
+ "name": "Group 1 name",
+ "type": "LightGroup",
+ "state": {},
+ "action": {},
+ "scenes": [{"id": "1", "name": "Scene 1"}],
+ "lights": ["1"],
+ }
+ },
+ "lights": {
+ "1": {
+ "name": "Light 1 name",
+ "state": {"reachable": True},
+ "type": "Light",
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ }
+ },
+ "sensors": {
+ "1": {
+ "name": "Switch 1",
+ "type": "ZHASwitch",
+ "state": {"buttonevent": 1000},
+ "config": {"battery": 100},
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ }
+ },
+ }
+
+ mock_deconz_request(aioclient_mock, config_entry.data, data)
+
+ await hass.services.async_call(
+ DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH, service_data={CONF_BRIDGE_ID: BRIDGEID}
+ )
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 4
+ assert len(captured_events) == 0
+
+
+async def test_remove_orphaned_entries_service(hass, aioclient_mock):
+ """Test service works and also don't remove more than expected."""
+ data = {
+ "lights": {
+ "1": {
+ "name": "Light 1 name",
+ "state": {"reachable": True},
+ "type": "Light",
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ }
+ },
+ "sensors": {
+ "1": {
+ "name": "Switch 1",
+ "type": "ZHASwitch",
+ "state": {"buttonevent": 1000, "gesture": 1},
+ "config": {"battery": 100},
+ "uniqueid": "00:00:00:00:00:00:00:03-00",
+ },
+ },
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id, identifiers={("mac", "123")}
)
@@ -256,7 +336,7 @@ async def test_remove_orphaned_entries_service(hass):
== 5 # Host, gateway, light, switch and orphan
)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entity_registry.async_get_or_create(
SENSOR_DOMAIN,
DECONZ_DOMAIN,
@@ -274,7 +354,7 @@ async def test_remove_orphaned_entries_service(hass):
await hass.services.async_call(
DECONZ_DOMAIN,
SERVICE_REMOVE_ORPHANED_ENTRIES,
- service_data=data,
+ service_data={CONF_BRIDGE_ID: BRIDGEID},
)
await hass.async_block_till_done()
diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py
index e42e89d903eb57..cffdf07ae2bc25 100644
--- a/tests/components/deconz/test_switch.py
+++ b/tests/components/deconz/test_switch.py
@@ -1,92 +1,59 @@
"""deCONZ switch platform tests."""
-from copy import deepcopy
from unittest.mock import patch
-from homeassistant.components.deconz import DOMAIN as DECONZ_DOMAIN
-from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
-from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
-from homeassistant.setup import async_setup_component
-
-from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
-
-POWER_PLUGS = {
- "1": {
- "id": "On off switch id",
- "name": "On off switch",
- "type": "On/Off plug-in unit",
- "state": {"on": True, "reachable": True},
- "uniqueid": "00:00:00:00:00:00:00:00-00",
- },
- "2": {
- "id": "Smart plug id",
- "name": "Smart plug",
- "type": "Smart plug",
- "state": {"on": False, "reachable": True},
- "uniqueid": "00:00:00:00:00:00:00:01-00",
- },
- "3": {
- "id": "Unsupported switch id",
- "name": "Unsupported switch",
- "type": "Not a switch",
- "state": {"reachable": True},
- "uniqueid": "00:00:00:00:00:00:00:03-00",
- },
- "4": {
- "id": "On off relay id",
- "name": "On off relay",
- "state": {"on": True, "reachable": True},
- "type": "On/Off light",
- "uniqueid": "00:00:00:00:00:00:00:04-00",
- },
-}
-
-SIRENS = {
- "1": {
- "id": "Warning device id",
- "name": "Warning device",
- "type": "Warning device",
- "state": {"alert": "lselect", "reachable": True},
- "uniqueid": "00:00:00:00:00:00:00:00-00",
- },
- "2": {
- "id": "Unsupported switch id",
- "name": "Unsupported switch",
- "type": "Not a switch",
- "state": {"reachable": True},
- "uniqueid": "00:00:00:00:00:00:00:01-00",
- },
-}
-
-
-async def test_platform_manually_configured(hass):
- """Test that we do not discover anything or try to set up a gateway."""
- assert (
- await async_setup_component(
- hass, SWITCH_DOMAIN, {"switch": {"platform": DECONZ_DOMAIN}}
- )
- is True
- )
- assert DECONZ_DOMAIN not in hass.data
+from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE
+
+from .test_gateway import (
+ DECONZ_WEB_REQUEST,
+ mock_deconz_put_request,
+ setup_deconz_integration,
+)
-async def test_no_switches(hass):
+async def test_no_switches(hass, aioclient_mock):
"""Test that no switch entities are created."""
- await setup_deconz_integration(hass)
+ await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 0
-async def test_power_plugs(hass):
+async def test_power_plugs(hass, aioclient_mock, mock_deconz_websocket):
"""Test that all supported switch entities are created."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["lights"] = deepcopy(POWER_PLUGS)
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ data = {
+ "lights": {
+ "1": {
+ "name": "On off switch",
+ "type": "On/Off plug-in unit",
+ "state": {"on": True, "reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ },
+ "2": {
+ "name": "Smart plug",
+ "type": "Smart plug",
+ "state": {"on": False, "reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ },
+ "3": {
+ "name": "Unsupported switch",
+ "type": "Not a switch",
+ "state": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:03-00",
+ },
+ "4": {
+ "name": "On off relay",
+ "state": {"on": True, "reachable": True},
+ "type": "On/Off light",
+ "uniqueid": "00:00:00:00:00:00:00:04-00",
+ },
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 4
assert hass.states.get("switch.on_off_switch").state == STATE_ON
@@ -94,112 +61,122 @@ async def test_power_plugs(hass):
assert hass.states.get("switch.on_off_relay").state == STATE_ON
assert hass.states.get("switch.unsupported_switch") is None
- state_changed_event = {
+ event_changed_light = {
"t": "event",
"e": "changed",
"r": "lights",
"id": "1",
"state": {"on": False},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_light)
+ await hass.async_block_till_done()
assert hass.states.get("switch.on_off_switch").state == STATE_OFF
# Verify service calls
- on_off_switch_device = gateway.api.lights["1"]
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state")
# Service turn on power plug
- with patch.object(
- on_off_switch_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: "switch.on_off_switch"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/1/state", json={"on": True})
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.on_off_switch"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"on": True}
# Service turn off power plug
- with patch.object(
- on_off_switch_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: "switch.on_off_switch"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with("put", "/lights/1/state", json={"on": False})
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.on_off_switch"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[2][2] == {"on": False}
await hass.config_entries.async_unload(config_entry.entry_id)
+ states = hass.states.async_all()
+ assert len(states) == 4
+ for state in states:
+ assert state.state == STATE_UNAVAILABLE
+
+ await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
-async def test_sirens(hass):
+async def test_sirens(hass, aioclient_mock, mock_deconz_websocket):
"""Test that siren entities are created."""
- data = deepcopy(DECONZ_WEB_REQUEST)
- data["lights"] = deepcopy(SIRENS)
- config_entry = await setup_deconz_integration(hass, get_state_response=data)
- gateway = get_gateway_from_config_entry(hass, config_entry)
+ data = {
+ "lights": {
+ "1": {
+ "name": "Warning device",
+ "type": "Warning device",
+ "state": {"alert": "lselect", "reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ },
+ "2": {
+ "name": "Unsupported switch",
+ "type": "Not a switch",
+ "state": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ },
+ }
+ }
+ with patch.dict(DECONZ_WEB_REQUEST, data):
+ config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 2
assert hass.states.get("switch.warning_device").state == STATE_ON
- assert hass.states.get("switch.unsupported_switch") is None
+ assert not hass.states.get("switch.unsupported_switch")
- state_changed_event = {
+ event_changed_light = {
"t": "event",
"e": "changed",
"r": "lights",
"id": "1",
"state": {"alert": None},
}
- gateway.api.event_handler(state_changed_event)
+ await mock_deconz_websocket(data=event_changed_light)
+ await hass.async_block_till_done()
assert hass.states.get("switch.warning_device").state == STATE_OFF
# Verify service calls
- warning_device_device = gateway.api.lights["1"]
+ mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state")
# Service turn on siren
- with patch.object(
- warning_device_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: "switch.warning_device"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with(
- "put", "/lights/1/state", json={"alert": "lselect"}
- )
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.warning_device"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[1][2] == {"alert": "lselect"}
# Service turn off siren
- with patch.object(
- warning_device_device, "_request", return_value=True
- ) as set_callback:
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: "switch.warning_device"},
- blocking=True,
- )
- await hass.async_block_till_done()
- set_callback.assert_called_with(
- "put", "/lights/1/state", json={"alert": "none"}
- )
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.warning_device"},
+ blocking=True,
+ )
+ assert aioclient_mock.mock_calls[2][2] == {"alert": "none"}
await hass.config_entries.async_unload(config_entry.entry_id)
+ states = hass.states.async_all()
+ assert len(states) == 2
+ for state in states:
+ assert state.state == STATE_UNAVAILABLE
+
+ await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py
index 9830d94447128b..ec2d207a68be0b 100644
--- a/tests/components/default_config/test_init.py
+++ b/tests/components/default_config/test_init.py
@@ -5,7 +5,7 @@
from homeassistant.setup import async_setup_component
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture(autouse=True)
diff --git a/tests/components/demo/conftest.py b/tests/components/demo/conftest.py
index 666224fb737fff..13574c5018213a 100644
--- a/tests/components/demo/conftest.py
+++ b/tests/components/demo/conftest.py
@@ -1,2 +1,2 @@
"""demo conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
diff --git a/tests/components/demo/test_climate.py b/tests/components/demo/test_climate.py
index aa6ff39cb0edf5..2f4317c49c5cbb 100644
--- a/tests/components/demo/test_climate.py
+++ b/tests/components/demo/test_climate.py
@@ -69,7 +69,7 @@ def test_setup_params(hass):
assert state.attributes.get(ATTR_HUMIDITY) == 67
assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 54
assert state.attributes.get(ATTR_SWING_MODE) == "Off"
- assert STATE_OFF == state.attributes.get(ATTR_AUX_HEAT)
+ assert state.attributes.get(ATTR_AUX_HEAT) == STATE_OFF
assert state.attributes.get(ATTR_HVAC_MODES) == [
"off",
"heat",
diff --git a/tests/components/demo/test_fan.py b/tests/components/demo/test_fan.py
index 5297e64bda9041..a788e69b0d347a 100644
--- a/tests/components/demo/test_fan.py
+++ b/tests/components/demo/test_fan.py
@@ -2,7 +2,12 @@
import pytest
from homeassistant.components import fan
-from homeassistant.components.demo.fan import PRESET_MODE_AUTO, PRESET_MODE_SMART
+from homeassistant.components.demo.fan import (
+ PRESET_MODE_AUTO,
+ PRESET_MODE_ON,
+ PRESET_MODE_SLEEP,
+ PRESET_MODE_SMART,
+)
from homeassistant.const import (
ATTR_ENTITY_ID,
ENTITY_MATCH_ALL,
@@ -22,6 +27,7 @@
FANS_WITH_PRESET_MODES = FULL_FAN_ENTITY_IDS + [
"fan.percentage_limited_fan",
]
+PERCENTAGE_MODEL_FANS = ["fan.percentage_full_fan", "fan.percentage_limited_fan"]
@pytest.fixture(autouse=True)
@@ -60,6 +66,28 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id):
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH
assert state.attributes[fan.ATTR_PERCENTAGE] == 100
+ await hass.services.async_call(
+ fan.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_MEDIUM},
+ blocking=True,
+ )
+ state = hass.states.get(fan_entity_id)
+ assert state.state == STATE_ON
+ assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 66
+
+ await hass.services.async_call(
+ fan.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_LOW},
+ blocking=True,
+ )
+ state = hass.states.get(fan_entity_id)
+ assert state.state == STATE_ON
+ assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 33
+
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
@@ -71,6 +99,39 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id):
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH
assert state.attributes[fan.ATTR_PERCENTAGE] == 100
+ await hass.services.async_call(
+ fan.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 66},
+ blocking=True,
+ )
+ state = hass.states.get(fan_entity_id)
+ assert state.state == STATE_ON
+ assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 66
+
+ await hass.services.async_call(
+ fan.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 33},
+ blocking=True,
+ )
+ state = hass.states.get(fan_entity_id)
+ assert state.state == STATE_ON
+ assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 33
+
+ await hass.services.async_call(
+ fan.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 0},
+ blocking=True,
+ )
+ state = hass.states.get(fan_entity_id)
+ assert state.state == STATE_OFF
+ assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 0
+
@pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODE_ONLY)
async def test_turn_on_with_preset_mode_only(hass, fan_entity_id):
@@ -89,6 +150,8 @@ async def test_turn_on_with_preset_mode_only(hass, fan_entity_id):
assert state.attributes[fan.ATTR_PRESET_MODES] == [
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
+ PRESET_MODE_SLEEP,
+ PRESET_MODE_ON,
]
await hass.services.async_call(
@@ -145,10 +208,14 @@ async def test_turn_on_with_preset_mode_and_speed(hass, fan_entity_id):
fan.SPEED_HIGH,
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
+ PRESET_MODE_SLEEP,
+ PRESET_MODE_ON,
]
assert state.attributes[fan.ATTR_PRESET_MODES] == [
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
+ PRESET_MODE_SLEEP,
+ PRESET_MODE_ON,
]
await hass.services.async_call(
@@ -331,6 +398,128 @@ async def test_set_percentage(hass, fan_entity_id):
assert state.attributes[fan.ATTR_PERCENTAGE] == 33
+@pytest.mark.parametrize("fan_entity_id", LIMITED_AND_FULL_FAN_ENTITY_IDS)
+async def test_increase_decrease_speed(hass, fan_entity_id):
+ """Test increasing and decreasing the percentage speed of the device."""
+ state = hass.states.get(fan_entity_id)
+ assert state.state == STATE_OFF
+ assert state.attributes[fan.ATTR_PERCENTAGE_STEP] == 100 / 3
+
+ await hass.services.async_call(
+ fan.DOMAIN,
+ fan.SERVICE_INCREASE_SPEED,
+ {ATTR_ENTITY_ID: fan_entity_id},
+ blocking=True,
+ )
+ state = hass.states.get(fan_entity_id)
+ assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 33
+
+ await hass.services.async_call(
+ fan.DOMAIN,
+ fan.SERVICE_INCREASE_SPEED,
+ {ATTR_ENTITY_ID: fan_entity_id},
+ blocking=True,
+ )
+ state = hass.states.get(fan_entity_id)
+ assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 66
+
+ await hass.services.async_call(
+ fan.DOMAIN,
+ fan.SERVICE_INCREASE_SPEED,
+ {ATTR_ENTITY_ID: fan_entity_id},
+ blocking=True,
+ )
+ state = hass.states.get(fan_entity_id)
+ assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 100
+
+ await hass.services.async_call(
+ fan.DOMAIN,
+ fan.SERVICE_INCREASE_SPEED,
+ {ATTR_ENTITY_ID: fan_entity_id},
+ blocking=True,
+ )
+ state = hass.states.get(fan_entity_id)
+ assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 100
+
+ await hass.services.async_call(
+ fan.DOMAIN,
+ fan.SERVICE_DECREASE_SPEED,
+ {ATTR_ENTITY_ID: fan_entity_id},
+ blocking=True,
+ )
+ state = hass.states.get(fan_entity_id)
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 66
+ assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM
+
+ await hass.services.async_call(
+ fan.DOMAIN,
+ fan.SERVICE_DECREASE_SPEED,
+ {ATTR_ENTITY_ID: fan_entity_id},
+ blocking=True,
+ )
+ state = hass.states.get(fan_entity_id)
+ assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 33
+
+ await hass.services.async_call(
+ fan.DOMAIN,
+ fan.SERVICE_DECREASE_SPEED,
+ {ATTR_ENTITY_ID: fan_entity_id},
+ blocking=True,
+ )
+ state = hass.states.get(fan_entity_id)
+ assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 0
+
+ await hass.services.async_call(
+ fan.DOMAIN,
+ fan.SERVICE_DECREASE_SPEED,
+ {ATTR_ENTITY_ID: fan_entity_id},
+ blocking=True,
+ )
+ state = hass.states.get(fan_entity_id)
+ assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 0
+
+
+@pytest.mark.parametrize("fan_entity_id", PERCENTAGE_MODEL_FANS)
+async def test_increase_decrease_speed_with_percentage_step(hass, fan_entity_id):
+ """Test increasing speed with a percentage step."""
+ await hass.services.async_call(
+ fan.DOMAIN,
+ fan.SERVICE_INCREASE_SPEED,
+ {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE_STEP: 25},
+ blocking=True,
+ )
+ state = hass.states.get(fan_entity_id)
+ assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 25
+
+ await hass.services.async_call(
+ fan.DOMAIN,
+ fan.SERVICE_INCREASE_SPEED,
+ {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE_STEP: 25},
+ blocking=True,
+ )
+ state = hass.states.get(fan_entity_id)
+ assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 50
+
+ await hass.services.async_call(
+ fan.DOMAIN,
+ fan.SERVICE_INCREASE_SPEED,
+ {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE_STEP: 25},
+ blocking=True,
+ )
+ state = hass.states.get(fan_entity_id)
+ assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH
+ assert state.attributes[fan.ATTR_PERCENTAGE] == 75
+
+
@pytest.mark.parametrize("fan_entity_id", FULL_FAN_ENTITY_IDS)
async def test_oscillate(hass, fan_entity_id):
"""Test oscillating the fan."""
diff --git a/tests/components/demo/test_humidifier.py b/tests/components/demo/test_humidifier.py
index ba2bd60f8f22fe..cd400f983471d9 100644
--- a/tests/components/demo/test_humidifier.py
+++ b/tests/components/demo/test_humidifier.py
@@ -7,7 +7,6 @@
ATTR_HUMIDITY,
ATTR_MAX_HUMIDITY,
ATTR_MIN_HUMIDITY,
- ATTR_MODE,
DOMAIN,
MODE_AWAY,
MODE_ECO,
@@ -16,6 +15,7 @@
)
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_MODE,
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py
index 420707e54d0484..68d4dfbf3796d7 100644
--- a/tests/components/demo/test_init.py
+++ b/tests/components/demo/test_init.py
@@ -1,4 +1,5 @@
"""The tests for the Demo component."""
+from contextlib import suppress
import json
import os
@@ -20,10 +21,8 @@ def mock_history(hass):
def demo_cleanup(hass):
"""Clean up device tracker demo file."""
yield
- try:
+ with suppress(FileNotFoundError):
os.remove(hass.config.path(YAML_DEVICES))
- except FileNotFoundError:
- pass
async def test_setting_up_demo(hass):
diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py
index a32a99bbc63312..4ed41ce9c83f6e 100644
--- a/tests/components/demo/test_media_player.py
+++ b/tests/components/demo/test_media_player.py
@@ -442,3 +442,39 @@ def detach(self):
req = await client.get(state.attributes.get(ATTR_ENTITY_PICTURE))
assert req.status == 200
assert await req.text() == fake_picture_data
+
+
+async def test_grouping(hass):
+ """Test the join/unjoin services."""
+ walkman = "media_player.walkman"
+ kitchen = "media_player.kitchen"
+
+ assert await async_setup_component(
+ hass, mp.DOMAIN, {"media_player": {"platform": "demo"}}
+ )
+ await hass.async_block_till_done()
+ state = hass.states.get(walkman)
+ assert state.attributes.get(mp.ATTR_GROUP_MEMBERS) == []
+
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_JOIN,
+ {
+ ATTR_ENTITY_ID: walkman,
+ mp.ATTR_GROUP_MEMBERS: [
+ kitchen,
+ ],
+ },
+ blocking=True,
+ )
+ state = hass.states.get(walkman)
+ assert state.attributes.get(mp.ATTR_GROUP_MEMBERS) == [walkman, kitchen]
+
+ await hass.services.async_call(
+ mp.DOMAIN,
+ mp.SERVICE_UNJOIN,
+ {ATTR_ENTITY_ID: walkman},
+ blocking=True,
+ )
+ state = hass.states.get(walkman)
+ assert state.attributes.get(mp.ATTR_GROUP_MEMBERS) == []
diff --git a/tests/components/demo/test_notify.py b/tests/components/demo/test_notify.py
index 7c7f83312dda3c..153f065235cc20 100644
--- a/tests/components/demo/test_notify.py
+++ b/tests/components/demo/test_notify.py
@@ -12,7 +12,7 @@
from homeassistant.helpers import discovery
from homeassistant.setup import async_setup_component
-from tests.common import assert_setup_component
+from tests.common import assert_setup_component, async_capture_events
CONFIG = {notify.DOMAIN: {"platform": "demo"}}
@@ -20,9 +20,7 @@
@pytest.fixture
def events(hass):
"""Fixture that catches notify events."""
- events = []
- hass.bus.async_listen(demo.EVENT_NOTIFY, callback(lambda e: events.append(e)))
- yield events
+ return async_capture_events(hass, demo.EVENT_NOTIFY)
@pytest.fixture
diff --git a/tests/components/denonavr/test_config_flow.py b/tests/components/denonavr/test_config_flow.py
index 67d1a4e10db1ea..74ce77f1db7609 100644
--- a/tests/components/denonavr/test_config_flow.py
+++ b/tests/components/denonavr/test_config_flow.py
@@ -14,13 +14,13 @@
CONF_ZONE2,
CONF_ZONE3,
DOMAIN,
+ AvrTimoutError,
)
-from homeassistant.const import CONF_HOST, CONF_MAC
+from homeassistant.const import CONF_HOST
from tests.common import MockConfigEntry
TEST_HOST = "1.2.3.4"
-TEST_MAC = "ab:cd:ef:gh"
TEST_HOST2 = "5.6.7.8"
TEST_NAME = "Test_Receiver"
TEST_MODEL = "model5"
@@ -38,41 +38,29 @@
def denonavr_connect_fixture():
"""Mock denonavr connection and entry setup."""
with patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR._update_input_func_list",
- return_value=True,
- ), patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR._get_receiver_name",
- return_value=TEST_NAME,
- ), patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR._get_support_sound_mode",
- return_value=True,
- ), patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR._update_avr_2016",
- return_value=True,
+ "homeassistant.components.denonavr.receiver.DenonAVR.async_setup",
+ return_value=None,
), patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR._update_avr",
- return_value=True,
+ "homeassistant.components.denonavr.receiver.DenonAVR.async_update",
+ return_value=None,
), patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.get_device_info",
+ "homeassistant.components.denonavr.receiver.DenonAVR.support_sound_mode",
return_value=True,
), patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.name",
+ "homeassistant.components.denonavr.receiver.DenonAVR.name",
TEST_NAME,
), patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.model_name",
+ "homeassistant.components.denonavr.receiver.DenonAVR.model_name",
TEST_MODEL,
), patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.serial_number",
+ "homeassistant.components.denonavr.receiver.DenonAVR.serial_number",
TEST_SERIALNUMBER,
), patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.manufacturer",
+ "homeassistant.components.denonavr.receiver.DenonAVR.manufacturer",
TEST_MANUFACTURER,
), patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.receiver_type",
+ "homeassistant.components.denonavr.receiver.DenonAVR.receiver_type",
TEST_RECEIVER_TYPE,
- ), patch(
- "homeassistant.components.denonavr.config_flow.get_mac_address",
- return_value=TEST_MAC,
), patch(
"homeassistant.components.denonavr.async_setup_entry", return_value=True
):
@@ -102,7 +90,6 @@ async def test_config_flow_manual_host_success(hass):
assert result["title"] == TEST_NAME
assert result["data"] == {
CONF_HOST: TEST_HOST,
- CONF_MAC: TEST_MAC,
CONF_MODEL: TEST_MODEL,
CONF_TYPE: TEST_RECEIVER_TYPE,
CONF_MANUFACTURER: TEST_MANUFACTURER,
@@ -125,7 +112,7 @@ async def test_config_flow_manual_discover_1_success(hass):
assert result["errors"] == {}
with patch(
- "homeassistant.components.denonavr.config_flow.denonavr.ssdp.identify_denonavr_receivers",
+ "homeassistant.components.denonavr.config_flow.denonavr.async_discover",
return_value=TEST_DISCOVER_1_RECEIVER,
):
result = await hass.config_entries.flow.async_configure(
@@ -137,7 +124,6 @@ async def test_config_flow_manual_discover_1_success(hass):
assert result["title"] == TEST_NAME
assert result["data"] == {
CONF_HOST: TEST_HOST,
- CONF_MAC: TEST_MAC,
CONF_MODEL: TEST_MODEL,
CONF_TYPE: TEST_RECEIVER_TYPE,
CONF_MANUFACTURER: TEST_MANUFACTURER,
@@ -160,7 +146,7 @@ async def test_config_flow_manual_discover_2_success(hass):
assert result["errors"] == {}
with patch(
- "homeassistant.components.denonavr.config_flow.denonavr.ssdp.identify_denonavr_receivers",
+ "homeassistant.components.denonavr.config_flow.denonavr.async_discover",
return_value=TEST_DISCOVER_2_RECEIVER,
):
result = await hass.config_entries.flow.async_configure(
@@ -181,7 +167,6 @@ async def test_config_flow_manual_discover_2_success(hass):
assert result["title"] == TEST_NAME
assert result["data"] == {
CONF_HOST: TEST_HOST2,
- CONF_MAC: TEST_MAC,
CONF_MODEL: TEST_MODEL,
CONF_TYPE: TEST_RECEIVER_TYPE,
CONF_MANUFACTURER: TEST_MANUFACTURER,
@@ -204,7 +189,7 @@ async def test_config_flow_manual_discover_error(hass):
assert result["errors"] == {}
with patch(
- "homeassistant.components.denonavr.config_flow.denonavr.ssdp.identify_denonavr_receivers",
+ "homeassistant.components.denonavr.config_flow.denonavr.async_discover",
return_value=[],
):
result = await hass.config_entries.flow.async_configure(
@@ -232,119 +217,8 @@ async def test_config_flow_manual_host_no_serial(hass):
assert result["errors"] == {}
with patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.serial_number",
- None,
- ):
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {CONF_HOST: TEST_HOST},
- )
-
- assert result["type"] == "create_entry"
- assert result["title"] == TEST_NAME
- assert result["data"] == {
- CONF_HOST: TEST_HOST,
- CONF_MAC: TEST_MAC,
- CONF_MODEL: TEST_MODEL,
- CONF_TYPE: TEST_RECEIVER_TYPE,
- CONF_MANUFACTURER: TEST_MANUFACTURER,
- CONF_SERIAL_NUMBER: None,
- }
-
-
-async def test_config_flow_manual_host_no_mac(hass):
- """
- Successful flow manually initialized by the user.
-
- Host specified and an error getting the mac address.
- """
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": config_entries.SOURCE_USER}
- )
-
- assert result["type"] == "form"
- assert result["step_id"] == "user"
- assert result["errors"] == {}
-
- with patch(
- "homeassistant.components.denonavr.config_flow.get_mac_address",
- return_value=None,
- ):
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {CONF_HOST: TEST_HOST},
- )
-
- assert result["type"] == "create_entry"
- assert result["title"] == TEST_NAME
- assert result["data"] == {
- CONF_HOST: TEST_HOST,
- CONF_MAC: None,
- CONF_MODEL: TEST_MODEL,
- CONF_TYPE: TEST_RECEIVER_TYPE,
- CONF_MANUFACTURER: TEST_MANUFACTURER,
- CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
- }
-
-
-async def test_config_flow_manual_host_no_serial_no_mac(hass):
- """
- Successful flow manually initialized by the user.
-
- Host specified and an error getting the serial number and mac address.
- """
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": config_entries.SOURCE_USER}
- )
-
- assert result["type"] == "form"
- assert result["step_id"] == "user"
- assert result["errors"] == {}
-
- with patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.serial_number",
+ "homeassistant.components.denonavr.receiver.DenonAVR.serial_number",
None,
- ), patch(
- "homeassistant.components.denonavr.config_flow.get_mac_address",
- return_value=None,
- ):
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {CONF_HOST: TEST_HOST},
- )
-
- assert result["type"] == "create_entry"
- assert result["title"] == TEST_NAME
- assert result["data"] == {
- CONF_HOST: TEST_HOST,
- CONF_MAC: None,
- CONF_MODEL: TEST_MODEL,
- CONF_TYPE: TEST_RECEIVER_TYPE,
- CONF_MANUFACTURER: TEST_MANUFACTURER,
- CONF_SERIAL_NUMBER: None,
- }
-
-
-async def test_config_flow_manual_host_no_serial_no_mac_exception(hass):
- """
- Successful flow manually initialized by the user.
-
- Host specified and an error getting the serial number and exception getting mac address.
- """
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": config_entries.SOURCE_USER}
- )
-
- assert result["type"] == "form"
- assert result["step_id"] == "user"
- assert result["errors"] == {}
-
- with patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.serial_number",
- None,
- ), patch(
- "homeassistant.components.denonavr.config_flow.get_mac_address",
- side_effect=OSError,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -355,7 +229,6 @@ async def test_config_flow_manual_host_no_serial_no_mac_exception(hass):
assert result["title"] == TEST_NAME
assert result["data"] == {
CONF_HOST: TEST_HOST,
- CONF_MAC: None,
CONF_MODEL: TEST_MODEL,
CONF_TYPE: TEST_RECEIVER_TYPE,
CONF_MANUFACTURER: TEST_MANUFACTURER,
@@ -378,10 +251,10 @@ async def test_config_flow_manual_host_connection_error(hass):
assert result["errors"] == {}
with patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.get_device_info",
- side_effect=ConnectionError,
+ "homeassistant.components.denonavr.receiver.DenonAVR.async_setup",
+ side_effect=AvrTimoutError("Timeout", "async_setup"),
), patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.receiver_type",
+ "homeassistant.components.denonavr.receiver.DenonAVR.receiver_type",
None,
):
result = await hass.config_entries.flow.async_configure(
@@ -408,7 +281,7 @@ async def test_config_flow_manual_host_no_device_info(hass):
assert result["errors"] == {}
with patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.receiver_type",
+ "homeassistant.components.denonavr.receiver.DenonAVR.receiver_type",
None,
):
result = await hass.config_entries.flow.async_configure(
@@ -445,7 +318,6 @@ async def test_config_flow_ssdp(hass):
assert result["title"] == TEST_NAME
assert result["data"] == {
CONF_HOST: TEST_HOST,
- CONF_MAC: TEST_MAC,
CONF_MODEL: TEST_MODEL,
CONF_TYPE: TEST_RECEIVER_TYPE,
CONF_MANUFACTURER: TEST_MANUFACTURER,
@@ -521,7 +393,6 @@ async def test_options_flow(hass):
unique_id=TEST_UNIQUE_ID,
data={
CONF_HOST: TEST_HOST,
- CONF_MAC: TEST_MAC,
CONF_MODEL: TEST_MODEL,
CONF_TYPE: TEST_RECEIVER_TYPE,
CONF_MANUFACTURER: TEST_MANUFACTURER,
@@ -567,7 +438,7 @@ async def test_config_flow_manual_host_no_serial_double_config(hass):
assert result["errors"] == {}
with patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.serial_number",
+ "homeassistant.components.denonavr.receiver.DenonAVR.serial_number",
None,
):
result = await hass.config_entries.flow.async_configure(
@@ -579,7 +450,6 @@ async def test_config_flow_manual_host_no_serial_double_config(hass):
assert result["title"] == TEST_NAME
assert result["data"] == {
CONF_HOST: TEST_HOST,
- CONF_MAC: TEST_MAC,
CONF_MODEL: TEST_MODEL,
CONF_TYPE: TEST_RECEIVER_TYPE,
CONF_MANUFACTURER: TEST_MANUFACTURER,
@@ -595,7 +465,7 @@ async def test_config_flow_manual_host_no_serial_double_config(hass):
assert result["errors"] == {}
with patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.serial_number",
+ "homeassistant.components.denonavr.receiver.DenonAVR.serial_number",
None,
):
result = await hass.config_entries.flow.async_configure(
diff --git a/tests/components/denonavr/test_media_player.py b/tests/components/denonavr/test_media_player.py
index bb9f83b58d747d..71c873a2b9d725 100644
--- a/tests/components/denonavr/test_media_player.py
+++ b/tests/components/denonavr/test_media_player.py
@@ -4,7 +4,6 @@
import pytest
from homeassistant.components import media_player
-from homeassistant.components.denonavr import ATTR_COMMAND, SERVICE_GET_COMMAND
from homeassistant.components.denonavr.config_flow import (
CONF_MANUFACTURER,
CONF_MODEL,
@@ -12,12 +11,15 @@
CONF_TYPE,
DOMAIN,
)
-from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_MAC
+from homeassistant.components.denonavr.media_player import (
+ ATTR_COMMAND,
+ SERVICE_GET_COMMAND,
+)
+from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST
from tests.common import MockConfigEntry
TEST_HOST = "1.2.3.4"
-TEST_MAC = "ab:cd:ef:gh"
TEST_NAME = "Test_Receiver"
TEST_MODEL = "model5"
TEST_SERIALNUMBER = "123456789"
@@ -36,10 +38,10 @@
def client_fixture():
"""Patch of client library for tests."""
with patch(
- "homeassistant.components.denonavr.receiver.denonavr.DenonAVR",
+ "homeassistant.components.denonavr.receiver.DenonAVR",
autospec=True,
) as mock_client_class, patch(
- "homeassistant.components.denonavr.receiver.denonavr.discover"
+ "homeassistant.components.denonavr.config_flow.denonavr.async_discover"
):
mock_client_class.return_value.name = TEST_NAME
mock_client_class.return_value.model_name = TEST_MODEL
@@ -57,7 +59,6 @@ async def setup_denonavr(hass):
"""Initialize media_player for tests."""
entry_data = {
CONF_HOST: TEST_HOST,
- CONF_MAC: TEST_MAC,
CONF_MODEL: TEST_MODEL,
CONF_TYPE: TEST_RECEIVER_TYPE,
CONF_MANUFACTURER: TEST_MANUFACTURER,
@@ -92,4 +93,4 @@ async def test_get_command(hass, client):
await hass.services.async_call(DOMAIN, SERVICE_GET_COMMAND, data)
await hass.async_block_till_done()
- client.send_get_command.assert_called_with("test_command")
+ client.async_get_command.assert_awaited_with("test_command")
diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py
index 83ec146b53edc8..a2f042bcd73761 100644
--- a/tests/components/device_automation/test_init.py
+++ b/tests/components/device_automation/test_init.py
@@ -13,7 +13,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/device_tracker/test_device_condition.py b/tests/components/device_tracker/test_device_condition.py
index a187f21e954d51..2cd4aceeb0780c 100644
--- a/tests/components/device_tracker/test_device_condition.py
+++ b/tests/components/device_tracker/test_device_condition.py
@@ -15,7 +15,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/device_tracker/test_device_trigger.py b/tests/components/device_tracker/test_device_trigger.py
index 2f0ec14ec4baf6..0ec1ee67d3638d 100644
--- a/tests/components/device_tracker/test_device_trigger.py
+++ b/tests/components/device_tracker/test_device_trigger.py
@@ -16,7 +16,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
AWAY_LATITUDE = 32.881011
AWAY_LONGITUDE = -117.234758
diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py
index c7aba405ccd774..af0c7658ac77e9 100644
--- a/tests/components/device_tracker/test_init.py
+++ b/tests/components/device_tracker/test_init.py
@@ -237,21 +237,20 @@ async def test_update_stale(hass, mock_device_tracker_conf):
with patch(
"homeassistant.components.device_tracker.legacy.dt_util.utcnow",
return_value=register_time,
- ):
- with assert_setup_component(1, device_tracker.DOMAIN):
- assert await async_setup_component(
- hass,
- device_tracker.DOMAIN,
- {
- device_tracker.DOMAIN: {
- CONF_PLATFORM: "test",
- device_tracker.CONF_CONSIDER_HOME: 59,
- }
- },
- )
- await hass.async_block_till_done()
-
- assert STATE_HOME == hass.states.get("device_tracker.dev1").state
+ ), assert_setup_component(1, device_tracker.DOMAIN):
+ assert await async_setup_component(
+ hass,
+ device_tracker.DOMAIN,
+ {
+ device_tracker.DOMAIN: {
+ CONF_PLATFORM: "test",
+ device_tracker.CONF_CONSIDER_HOME: 59,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("device_tracker.dev1").state == STATE_HOME
scanner.leave_home("DEV1")
@@ -262,7 +261,7 @@ async def test_update_stale(hass, mock_device_tracker_conf):
async_fire_time_changed(hass, scan_time)
await hass.async_block_till_done()
- assert STATE_NOT_HOME == hass.states.get("device_tracker.dev1").state
+ assert hass.states.get("device_tracker.dev1").state == STATE_NOT_HOME
async def test_entity_attributes(hass, mock_device_tracker_conf):
@@ -458,23 +457,22 @@ async def test_see_passive_zone_state(hass, mock_device_tracker_conf):
with patch(
"homeassistant.components.device_tracker.legacy.dt_util.utcnow",
return_value=register_time,
- ):
- with assert_setup_component(1, device_tracker.DOMAIN):
- assert await async_setup_component(
- hass,
- device_tracker.DOMAIN,
- {
- device_tracker.DOMAIN: {
- CONF_PLATFORM: "test",
- device_tracker.CONF_CONSIDER_HOME: 59,
- }
- },
- )
- await hass.async_block_till_done()
+ ), assert_setup_component(1, device_tracker.DOMAIN):
+ assert await async_setup_component(
+ hass,
+ device_tracker.DOMAIN,
+ {
+ device_tracker.DOMAIN: {
+ CONF_PLATFORM: "test",
+ device_tracker.CONF_CONSIDER_HOME: 59,
+ }
+ },
+ )
+ await hass.async_block_till_done()
state = hass.states.get("device_tracker.dev1")
attrs = state.attributes
- assert STATE_HOME == state.state
+ assert state.state == STATE_HOME
assert state.object_id == "dev1"
assert state.name == "dev1"
assert attrs.get("friendly_name") == "dev1"
@@ -494,7 +492,7 @@ async def test_see_passive_zone_state(hass, mock_device_tracker_conf):
state = hass.states.get("device_tracker.dev1")
attrs = state.attributes
- assert STATE_NOT_HOME == state.state
+ assert state.state == STATE_NOT_HOME
assert state.object_id == "dev1"
assert state.name == "dev1"
assert attrs.get("friendly_name") == "dev1"
diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py
index 7d2c9ce40f61ed..370d86c7c94091 100644
--- a/tests/components/devolo_home_control/test_config_flow.py
+++ b/tests/components/devolo_home_control/test_config_flow.py
@@ -20,9 +20,6 @@ async def test_form(hass):
assert result["errors"] == {}
with patch(
- "homeassistant.components.devolo_home_control.async_setup",
- return_value=True,
- ) as mock_setup, patch(
"homeassistant.components.devolo_home_control.async_setup_entry",
return_value=True,
) as mock_setup_entry, patch(
@@ -43,7 +40,6 @@ async def test_form(hass):
"mydevolo_url": "https://www.mydevolo.com",
}
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
@@ -90,9 +86,6 @@ async def test_form_advanced_options(hass):
assert result["errors"] == {}
with patch(
- "homeassistant.components.devolo_home_control.async_setup",
- return_value=True,
- ) as mock_setup, patch(
"homeassistant.components.devolo_home_control.async_setup_entry",
return_value=True,
) as mock_setup_entry, patch(
@@ -117,5 +110,4 @@ async def test_form_advanced_options(hass):
"mydevolo_url": "https://test_mydevolo_url.test",
}
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
diff --git a/tests/components/devolo_home_control/test_init.py b/tests/components/devolo_home_control/test_init.py
index f45400716f8508..3f124ee2098f96 100644
--- a/tests/components/devolo_home_control/test_init.py
+++ b/tests/components/devolo_home_control/test_init.py
@@ -39,17 +39,6 @@ async def test_setup_entry_maintenance(hass: HomeAssistant):
assert entry.state == ENTRY_STATE_SETUP_RETRY
-async def test_setup_connection_error(hass: HomeAssistant):
- """Test setup entry fails on connection error."""
- entry = configure_integration(hass)
- with patch(
- "homeassistant.components.devolo_home_control.HomeControl",
- side_effect=ConnectionError,
- ):
- await hass.config_entries.async_setup(entry.entry_id)
- assert entry.state == ENTRY_STATE_SETUP_RETRY
-
-
async def test_setup_gateway_offline(hass: HomeAssistant):
"""Test setup entry fails on gateway offline."""
entry = configure_integration(hass)
diff --git a/tests/components/dexcom/test_config_flow.py b/tests/components/dexcom/test_config_flow.py
index 2e1dfbcdee5209..65db7ca16ac9b5 100644
--- a/tests/components/dexcom/test_config_flow.py
+++ b/tests/components/dexcom/test_config_flow.py
@@ -24,8 +24,6 @@ async def test_form(hass):
"homeassistant.components.dexcom.config_flow.Dexcom.create_session",
return_value="test_session_id",
), patch(
- "homeassistant.components.dexcom.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.dexcom.async_setup_entry",
return_value=True,
) as mock_setup_entry:
@@ -38,7 +36,6 @@ async def test_form(hass):
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == CONFIG[CONF_USERNAME]
assert result2["data"] == CONFIG
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py
index 049128248a7b8e..25fbbea459a428 100644
--- a/tests/components/dhcp/test_init.py
+++ b/tests/components/dhcp/test_init.py
@@ -1,4 +1,5 @@
"""Test the DHCP discovery integration."""
+import datetime
import threading
from unittest.mock import patch
@@ -21,8 +22,9 @@
STATE_NOT_HOME,
)
from homeassistant.setup import async_setup_component
+import homeassistant.util.dt as dt_util
-from tests.common import mock_coro
+from tests.common import async_fire_time_changed
# connect b8:b7:f1:6d:b5:33 192.168.210.56
RAW_DHCP_REQUEST = (
@@ -48,6 +50,36 @@
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
)
+# iRobot-AE9EC12DD3B04885BCBFA36AFB01E1CC 50:14:79:03:85:2c 192.168.1.120
+RAW_DHCP_RENEWAL = (
+ b"\x00\x15\x5d\x8e\xed\x02\x50\x14\x79\x03\x85\x2c\x08\x00\x45\x00"
+ b"\x01\x8e\x51\xd2\x40\x00\x40\x11\x63\xa1\xc0\xa8\x01\x78\xc0\xa8"
+ b"\x01\x23\x00\x44\x00\x43\x01\x7a\x12\x09\x01\x01\x06\x00\xd4\xea"
+ b"\xb2\xfd\xff\xff\x00\x00\xc0\xa8\x01\x78\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x50\x14\x79\x03\x85\x2c\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x63\x82\x53\x63\x35\x01\x03\x39\x02\x05"
+ b"\xdc\x3c\x45\x64\x68\x63\x70\x63\x64\x2d\x35\x2e\x32\x2e\x31\x30"
+ b"\x3a\x4c\x69\x6e\x75\x78\x2d\x33\x2e\x31\x38\x2e\x37\x31\x3a\x61"
+ b"\x72\x6d\x76\x37\x6c\x3a\x51\x75\x61\x6c\x63\x6f\x6d\x6d\x20\x54"
+ b"\x65\x63\x68\x6e\x6f\x6c\x6f\x67\x69\x65\x73\x2c\x20\x49\x6e\x63"
+ b"\x20\x41\x50\x51\x38\x30\x30\x39\x0c\x27\x69\x52\x6f\x62\x6f\x74"
+ b"\x2d\x41\x45\x39\x45\x43\x31\x32\x44\x44\x33\x42\x30\x34\x38\x38"
+ b"\x35\x42\x43\x42\x46\x41\x33\x36\x41\x46\x42\x30\x31\x45\x31\x43"
+ b"\x43\x37\x08\x01\x21\x03\x06\x1c\x33\x3a\x3b\xff"
+)
+
async def test_dhcp_match_hostname_and_macaddress(hass):
"""Test matching based on hostname and macaddress."""
@@ -59,9 +91,7 @@ async def test_dhcp_match_hostname_and_macaddress(hass):
packet = Ether(RAW_DHCP_REQUEST)
- with patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init:
dhcp_watcher.handle_dhcp_packet(packet)
# Ensure no change is ignored
dhcp_watcher.handle_dhcp_packet(packet)
@@ -76,6 +106,31 @@ async def test_dhcp_match_hostname_and_macaddress(hass):
}
+async def test_dhcp_renewal_match_hostname_and_macaddress(hass):
+ """Test renewal matching based on hostname and macaddress."""
+ dhcp_watcher = dhcp.DHCPWatcher(
+ hass,
+ {},
+ [{"domain": "mock-domain", "hostname": "irobot-*", "macaddress": "501479*"}],
+ )
+
+ packet = Ether(RAW_DHCP_RENEWAL)
+
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init:
+ dhcp_watcher.handle_dhcp_packet(packet)
+ # Ensure no change is ignored
+ dhcp_watcher.handle_dhcp_packet(packet)
+
+ assert len(mock_init.mock_calls) == 1
+ assert mock_init.mock_calls[0][1][0] == "mock-domain"
+ assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"}
+ assert mock_init.mock_calls[0][2]["data"] == {
+ dhcp.IP_ADDRESS: "192.168.1.120",
+ dhcp.HOSTNAME: "irobot-ae9ec12dd3b04885bcbfa36afb01e1cc",
+ dhcp.MAC_ADDRESS: "50147903852c",
+ }
+
+
async def test_dhcp_match_hostname(hass):
"""Test matching based on hostname only."""
dhcp_watcher = dhcp.DHCPWatcher(
@@ -84,9 +139,7 @@ async def test_dhcp_match_hostname(hass):
packet = Ether(RAW_DHCP_REQUEST)
- with patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init:
dhcp_watcher.handle_dhcp_packet(packet)
assert len(mock_init.mock_calls) == 1
@@ -107,9 +160,7 @@ async def test_dhcp_match_macaddress(hass):
packet = Ether(RAW_DHCP_REQUEST)
- with patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init:
dhcp_watcher.handle_dhcp_packet(packet)
assert len(mock_init.mock_calls) == 1
@@ -130,9 +181,7 @@ async def test_dhcp_nomatch(hass):
packet = Ether(RAW_DHCP_REQUEST)
- with patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init:
dhcp_watcher.handle_dhcp_packet(packet)
assert len(mock_init.mock_calls) == 0
@@ -146,9 +195,7 @@ async def test_dhcp_nomatch_hostname(hass):
packet = Ether(RAW_DHCP_REQUEST)
- with patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init:
dhcp_watcher.handle_dhcp_packet(packet)
assert len(mock_init.mock_calls) == 0
@@ -162,9 +209,7 @@ async def test_dhcp_nomatch_non_dhcp_packet(hass):
packet = Ether(b"")
- with patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init:
dhcp_watcher.handle_dhcp_packet(packet)
assert len(mock_init.mock_calls) == 0
@@ -187,9 +232,7 @@ async def test_dhcp_nomatch_non_dhcp_request_packet(hass):
("hostname", b"connect"),
]
- with patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init:
dhcp_watcher.handle_dhcp_packet(packet)
assert len(mock_init.mock_calls) == 0
@@ -212,9 +255,7 @@ async def test_dhcp_invalid_hostname(hass):
("hostname", "connect"),
]
- with patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init:
dhcp_watcher.handle_dhcp_packet(packet)
assert len(mock_init.mock_calls) == 0
@@ -237,9 +278,7 @@ async def test_dhcp_missing_hostname(hass):
("hostname", None),
]
- with patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init:
dhcp_watcher.handle_dhcp_packet(packet)
assert len(mock_init.mock_calls) == 0
@@ -262,9 +301,7 @@ async def test_dhcp_invalid_option(hass):
("hostname"),
]
- with patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init:
dhcp_watcher.handle_dhcp_packet(packet)
assert len(mock_init.mock_calls) == 0
@@ -280,7 +317,11 @@ async def test_setup_and_stop(hass):
)
await hass.async_block_till_done()
- with patch("homeassistant.components.dhcp.AsyncSniffer.start") as start_call:
+ with patch("homeassistant.components.dhcp.AsyncSniffer.start") as start_call, patch(
+ "homeassistant.components.dhcp._verify_l2socket_setup",
+ ), patch("homeassistant.components.dhcp.compile_filter",), patch(
+ "homeassistant.components.dhcp.DiscoverHosts.async_discover"
+ ):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
@@ -303,9 +344,9 @@ async def test_setup_fails_as_root(hass, caplog):
wait_event = threading.Event()
with patch("os.geteuid", return_value=0), patch(
- "homeassistant.components.dhcp._verify_l2socket_creation_permission",
+ "homeassistant.components.dhcp._verify_l2socket_setup",
side_effect=Scapy_Exception,
- ):
+ ), patch("homeassistant.components.dhcp.DiscoverHosts.async_discover"):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
@@ -325,37 +366,63 @@ async def test_setup_fails_non_root(hass, caplog):
)
await hass.async_block_till_done()
- wait_event = threading.Event()
-
with patch("os.geteuid", return_value=10), patch(
- "homeassistant.components.dhcp._verify_l2socket_creation_permission",
+ "homeassistant.components.dhcp._verify_l2socket_setup",
side_effect=Scapy_Exception,
- ):
+ ), patch("homeassistant.components.dhcp.DiscoverHosts.async_discover"):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
+ await hass.async_block_till_done()
- hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
- await hass.async_block_till_done()
- wait_event.set()
assert "Cannot watch for dhcp packets without root or CAP_NET_RAW" in caplog.text
+async def test_setup_fails_with_broken_libpcap(hass, caplog):
+ """Test we abort if libpcap is missing or broken."""
+
+ assert await async_setup_component(
+ hass,
+ dhcp.DOMAIN,
+ {},
+ )
+ await hass.async_block_till_done()
+
+ with patch("homeassistant.components.dhcp._verify_l2socket_setup",), patch(
+ "homeassistant.components.dhcp.compile_filter",
+ side_effect=ImportError,
+ ) as compile_filter, patch(
+ "homeassistant.components.dhcp.AsyncSniffer",
+ ) as async_sniffer, patch(
+ "homeassistant.components.dhcp.DiscoverHosts.async_discover"
+ ):
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ await hass.async_block_till_done()
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
+ await hass.async_block_till_done()
+
+ assert compile_filter.called
+ assert not async_sniffer.called
+ assert (
+ "Cannot watch for dhcp packets without a functional packet filter"
+ in caplog.text
+ )
+
+
async def test_device_tracker_hostname_and_macaddress_exists_before_start(hass):
"""Test matching based on hostname and macaddress before start."""
hass.states.async_set(
"device_tracker.august_connect",
STATE_HOME,
{
- ATTR_HOST_NAME: "connect",
+ ATTR_HOST_NAME: "Connect",
ATTR_IP: "192.168.210.56",
ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER,
ATTR_MAC: "B8:B7:F1:6D:B5:33",
},
)
- with patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init:
device_tracker_watcher = dhcp.DeviceTrackerWatcher(
hass,
{},
@@ -379,9 +446,7 @@ async def test_device_tracker_hostname_and_macaddress_exists_before_start(hass):
async def test_device_tracker_hostname_and_macaddress_after_start(hass):
"""Test matching based on hostname and macaddress after start."""
- with patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init:
device_tracker_watcher = dhcp.DeviceTrackerWatcher(
hass,
{},
@@ -393,7 +458,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start(hass):
"device_tracker.august_connect",
STATE_HOME,
{
- ATTR_HOST_NAME: "connect",
+ ATTR_HOST_NAME: "Connect",
ATTR_IP: "192.168.210.56",
ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER,
ATTR_MAC: "B8:B7:F1:6D:B5:33",
@@ -416,9 +481,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start(hass):
async def test_device_tracker_hostname_and_macaddress_after_start_not_home(hass):
"""Test matching based on hostname and macaddress after start but not home."""
- with patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init:
device_tracker_watcher = dhcp.DeviceTrackerWatcher(
hass,
{},
@@ -446,9 +509,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start_not_home(hass)
async def test_device_tracker_hostname_and_macaddress_after_start_not_router(hass):
"""Test matching based on hostname and macaddress after start but not router."""
- with patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init:
device_tracker_watcher = dhcp.DeviceTrackerWatcher(
hass,
{},
@@ -478,9 +539,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start_hostname_missi
):
"""Test matching based on hostname and macaddress after start but missing hostname."""
- with patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init:
device_tracker_watcher = dhcp.DeviceTrackerWatcher(
hass,
{},
@@ -517,9 +576,7 @@ async def test_device_tracker_ignore_self_assigned_ips_before_start(hass):
},
)
- with patch.object(
- hass.config_entries.flow, "async_init", return_value=mock_coro()
- ) as mock_init:
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init:
device_tracker_watcher = dhcp.DeviceTrackerWatcher(
hass,
{},
@@ -531,3 +588,136 @@ async def test_device_tracker_ignore_self_assigned_ips_before_start(hass):
await hass.async_block_till_done()
assert len(mock_init.mock_calls) == 0
+
+
+async def test_aiodiscover_finds_new_hosts(hass):
+ """Test aiodiscover finds new host."""
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init, patch(
+ "homeassistant.components.dhcp.DiscoverHosts.async_discover",
+ return_value=[
+ {
+ dhcp.DISCOVERY_IP_ADDRESS: "192.168.210.56",
+ dhcp.DISCOVERY_HOSTNAME: "connect",
+ dhcp.DISCOVERY_MAC_ADDRESS: "b8b7f16db533",
+ }
+ ],
+ ):
+ device_tracker_watcher = dhcp.NetworkWatcher(
+ hass,
+ {},
+ [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}],
+ )
+ await device_tracker_watcher.async_start()
+ await hass.async_block_till_done()
+ await device_tracker_watcher.async_stop()
+ await hass.async_block_till_done()
+
+ assert len(mock_init.mock_calls) == 1
+ assert mock_init.mock_calls[0][1][0] == "mock-domain"
+ assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"}
+ assert mock_init.mock_calls[0][2]["data"] == {
+ dhcp.IP_ADDRESS: "192.168.210.56",
+ dhcp.HOSTNAME: "connect",
+ dhcp.MAC_ADDRESS: "b8b7f16db533",
+ }
+
+
+async def test_aiodiscover_does_not_call_again_on_shorter_hostname(hass):
+ """Verify longer hostnames generate a new flow but shorter ones do not.
+
+ Some routers will truncate hostnames so we want to accept
+ additional discovery where the hostname is longer and then
+ reject shorter ones.
+ """
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init, patch(
+ "homeassistant.components.dhcp.DiscoverHosts.async_discover",
+ return_value=[
+ {
+ dhcp.DISCOVERY_IP_ADDRESS: "192.168.210.56",
+ dhcp.DISCOVERY_HOSTNAME: "irobot-abc",
+ dhcp.DISCOVERY_MAC_ADDRESS: "b8b7f16db533",
+ },
+ {
+ dhcp.DISCOVERY_IP_ADDRESS: "192.168.210.56",
+ dhcp.DISCOVERY_HOSTNAME: "irobot-abcdef",
+ dhcp.DISCOVERY_MAC_ADDRESS: "b8b7f16db533",
+ },
+ {
+ dhcp.DISCOVERY_IP_ADDRESS: "192.168.210.56",
+ dhcp.DISCOVERY_HOSTNAME: "irobot-abc",
+ dhcp.DISCOVERY_MAC_ADDRESS: "b8b7f16db533",
+ },
+ ],
+ ):
+ device_tracker_watcher = dhcp.NetworkWatcher(
+ hass,
+ {},
+ [
+ {
+ "domain": "mock-domain",
+ "hostname": "irobot-*",
+ "macaddress": "B8B7F1*",
+ }
+ ],
+ )
+ await device_tracker_watcher.async_start()
+ await hass.async_block_till_done()
+ await device_tracker_watcher.async_stop()
+ await hass.async_block_till_done()
+
+ assert len(mock_init.mock_calls) == 2
+ assert mock_init.mock_calls[0][1][0] == "mock-domain"
+ assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"}
+ assert mock_init.mock_calls[0][2]["data"] == {
+ dhcp.IP_ADDRESS: "192.168.210.56",
+ dhcp.HOSTNAME: "irobot-abc",
+ dhcp.MAC_ADDRESS: "b8b7f16db533",
+ }
+ assert mock_init.mock_calls[1][1][0] == "mock-domain"
+ assert mock_init.mock_calls[1][2]["context"] == {"source": "dhcp"}
+ assert mock_init.mock_calls[1][2]["data"] == {
+ dhcp.IP_ADDRESS: "192.168.210.56",
+ dhcp.HOSTNAME: "irobot-abcdef",
+ dhcp.MAC_ADDRESS: "b8b7f16db533",
+ }
+
+
+async def test_aiodiscover_finds_new_hosts_after_interval(hass):
+ """Test aiodiscover finds new host after interval."""
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init, patch(
+ "homeassistant.components.dhcp.DiscoverHosts.async_discover",
+ return_value=[],
+ ):
+ device_tracker_watcher = dhcp.NetworkWatcher(
+ hass,
+ {},
+ [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}],
+ )
+ await device_tracker_watcher.async_start()
+ await hass.async_block_till_done()
+
+ assert len(mock_init.mock_calls) == 0
+
+ with patch.object(hass.config_entries.flow, "async_init") as mock_init, patch(
+ "homeassistant.components.dhcp.DiscoverHosts.async_discover",
+ return_value=[
+ {
+ dhcp.DISCOVERY_IP_ADDRESS: "192.168.210.56",
+ dhcp.DISCOVERY_HOSTNAME: "connect",
+ dhcp.DISCOVERY_MAC_ADDRESS: "b8b7f16db533",
+ }
+ ],
+ ):
+ async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(minutes=65))
+ await hass.async_block_till_done()
+ await device_tracker_watcher.async_stop()
+ await hass.async_block_till_done()
+
+ assert len(mock_init.mock_calls) == 1
+ assert mock_init.mock_calls[0][1][0] == "mock-domain"
+ assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"}
+ assert mock_init.mock_calls[0][2]["data"] == {
+ dhcp.IP_ADDRESS: "192.168.210.56",
+ dhcp.HOSTNAME: "connect",
+ dhcp.MAC_ADDRESS: "b8b7f16db533",
+ }
diff --git a/tests/components/directv/test_config_flow.py b/tests/components/directv/test_config_flow.py
index 3372d48aadb51f..8c2d190f01429c 100644
--- a/tests/components/directv/test_config_flow.py
+++ b/tests/components/directv/test_config_flow.py
@@ -229,9 +229,7 @@ async def test_full_user_flow_implementation(
assert result["step_id"] == "user"
user_input = MOCK_USER_INPUT.copy()
- with patch(
- "homeassistant.components.directv.async_setup_entry", return_value=True
- ), patch("homeassistant.components.directv.async_setup", return_value=True):
+ with patch("homeassistant.components.directv.async_setup_entry", return_value=True):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=user_input,
diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py
index 506cf62a44d0f3..8e7fad62c8943f 100644
--- a/tests/components/directv/test_media_player.py
+++ b/tests/components/directv/test_media_player.py
@@ -1,6 +1,7 @@
"""The tests for the DirecTV Media player platform."""
+from __future__ import annotations
+
from datetime import datetime, timedelta
-from typing import Optional
from unittest.mock import patch
from pytest import fixture
@@ -53,6 +54,7 @@
STATE_PLAYING,
STATE_UNAVAILABLE,
)
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import dt as dt_util
@@ -76,24 +78,20 @@ def mock_now() -> datetime:
return dt_util.utcnow()
-async def async_turn_on(
- hass: HomeAssistantType, entity_id: Optional[str] = None
-) -> None:
+async def async_turn_on(hass: HomeAssistantType, entity_id: str | None = None) -> None:
"""Turn on specified media player or all."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
await hass.services.async_call(MP_DOMAIN, SERVICE_TURN_ON, data)
-async def async_turn_off(
- hass: HomeAssistantType, entity_id: Optional[str] = None
-) -> None:
+async def async_turn_off(hass: HomeAssistantType, entity_id: str | None = None) -> None:
"""Turn off specified media player or all."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
await hass.services.async_call(MP_DOMAIN, SERVICE_TURN_OFF, data)
async def async_media_pause(
- hass: HomeAssistantType, entity_id: Optional[str] = None
+ hass: HomeAssistantType, entity_id: str | None = None
) -> None:
"""Send the media player the command for pause."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
@@ -101,7 +99,7 @@ async def async_media_pause(
async def async_media_play(
- hass: HomeAssistantType, entity_id: Optional[str] = None
+ hass: HomeAssistantType, entity_id: str | None = None
) -> None:
"""Send the media player the command for play/pause."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
@@ -109,7 +107,7 @@ async def async_media_play(
async def async_media_stop(
- hass: HomeAssistantType, entity_id: Optional[str] = None
+ hass: HomeAssistantType, entity_id: str | None = None
) -> None:
"""Send the media player the command for stop."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
@@ -117,7 +115,7 @@ async def async_media_stop(
async def async_media_next_track(
- hass: HomeAssistantType, entity_id: Optional[str] = None
+ hass: HomeAssistantType, entity_id: str | None = None
) -> None:
"""Send the media player the command for next track."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
@@ -125,7 +123,7 @@ async def async_media_next_track(
async def async_media_previous_track(
- hass: HomeAssistantType, entity_id: Optional[str] = None
+ hass: HomeAssistantType, entity_id: str | None = None
) -> None:
"""Send the media player the command for prev track."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
@@ -136,8 +134,8 @@ async def async_play_media(
hass: HomeAssistantType,
media_type: str,
media_id: str,
- entity_id: Optional[str] = None,
- enqueue: Optional[str] = None,
+ entity_id: str | None = None,
+ enqueue: str | None = None,
) -> None:
"""Send the media player the command for playing media."""
data = {ATTR_MEDIA_CONTENT_TYPE: media_type, ATTR_MEDIA_CONTENT_ID: media_id}
@@ -167,7 +165,7 @@ async def test_unique_id(
"""Test unique id."""
await setup_integration(hass, aioclient_mock)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
main = entity_registry.async_get(MAIN_ENTITY_ID)
assert main.device_class == DEVICE_CLASS_RECEIVER
diff --git a/tests/components/directv/test_remote.py b/tests/components/directv/test_remote.py
index 33521958747f72..92bcd6af014d25 100644
--- a/tests/components/directv/test_remote.py
+++ b/tests/components/directv/test_remote.py
@@ -7,6 +7,7 @@
SERVICE_SEND_COMMAND,
)
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.typing import HomeAssistantType
from tests.components.directv import setup_integration
@@ -36,7 +37,7 @@ async def test_unique_id(
"""Test unique id."""
await setup_integration(hass, aioclient_mock)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
main = entity_registry.async_get(MAIN_ENTITY_ID)
assert main.unique_id == "028877455858"
diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py
index fd66e59ef21210..4dd77c981873a3 100644
--- a/tests/components/discovery/test_init.py
+++ b/tests/components/discovery/test_init.py
@@ -16,8 +16,8 @@
SERVICE = "yamaha"
SERVICE_COMPONENT = "media_player"
-SERVICE_NO_PLATFORM = "hass_ios"
-SERVICE_NO_PLATFORM_COMPONENT = "ios"
+SERVICE_NO_PLATFORM = "netgear_router"
+SERVICE_NO_PLATFORM_COMPONENT = "device_tracker"
SERVICE_INFO = {"key": "value"} # Can be anything
UNKNOWN_SERVICE = "this_service_will_never_be_supported"
@@ -38,19 +38,17 @@ async def mock_discovery(hass, discoveries, config=BASE_CONFIG):
"""Mock discoveries."""
with patch("homeassistant.components.zeroconf.async_get_instance"), patch(
"homeassistant.components.zeroconf.async_setup", return_value=True
- ):
+ ), patch.object(discovery, "_discover", discoveries), patch(
+ "homeassistant.components.discovery.async_discover"
+ ) as mock_discover, patch(
+ "homeassistant.components.discovery.async_load_platform",
+ return_value=mock_coro(),
+ ) as mock_platform:
assert await async_setup_component(hass, "discovery", config)
await hass.async_block_till_done()
await hass.async_start()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
-
- with patch.object(discovery, "_discover", discoveries), patch(
- "homeassistant.components.discovery.async_discover", return_value=mock_coro()
- ) as mock_discover, patch(
- "homeassistant.components.discovery.async_load_platform",
- return_value=mock_coro(),
- ) as mock_platform:
async_fire_time_changed(hass, utcnow())
# Work around an issue where our loop.call_soon not get caught
await hass.async_block_till_done()
diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py
index e94f73239f13af..9fa9752dc659b7 100644
--- a/tests/components/doorbird/test_config_flow.py
+++ b/tests/components/doorbird/test_config_flow.py
@@ -1,13 +1,15 @@
"""Test the DoorBird config flow."""
-from unittest.mock import MagicMock, patch
-import urllib
+from unittest.mock import MagicMock, Mock, patch
+
+import pytest
+import requests
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.doorbird import CONF_CUSTOM_URL, CONF_TOKEN
from homeassistant.components.doorbird.const import CONF_EVENTS, DOMAIN
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
-from tests.common import MockConfigEntry, init_recorder_component
+from tests.common import MockConfigEntry
VALID_CONFIG = {
CONF_HOST: "1.2.3.4",
@@ -21,7 +23,9 @@ def _get_mock_doorbirdapi_return_values(ready=None, info=None):
doorbirdapi_mock = MagicMock()
type(doorbirdapi_mock).ready = MagicMock(return_value=ready)
type(doorbirdapi_mock).info = MagicMock(return_value=info)
-
+ type(doorbirdapi_mock).doorbell_state = MagicMock(
+ side_effect=requests.exceptions.HTTPError(response=Mock(status_code=401))
+ )
return doorbirdapi_mock
@@ -35,10 +39,6 @@ def _get_mock_doorbirdapi_side_effects(ready=None, info=None):
async def test_user_form(hass):
"""Test we get the user form."""
- await hass.async_add_executor_job(
- init_recorder_component, hass
- ) # force in memory db
-
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -78,10 +78,6 @@ async def test_user_form(hass):
async def test_form_import(hass):
"""Test we get the form with import source."""
- await hass.async_add_executor_job(
- init_recorder_component, hass
- ) # force in memory db
-
await setup.async_setup_component(hass, "persistent_notification", {})
import_config = VALID_CONFIG.copy()
@@ -131,23 +127,27 @@ async def test_form_import(hass):
async def test_form_import_with_zeroconf_already_discovered(hass):
"""Test we get the form with import source."""
- await hass.async_add_executor_job(
- init_recorder_component, hass
- ) # force in memory db
-
await setup.async_setup_component(hass, "persistent_notification", {})
+ doorbirdapi = _get_mock_doorbirdapi_return_values(
+ ready=[True], info={"WIFI_MAC_ADDR": "1CCAE3DOORBIRD"}
+ )
# Running the zeroconf init will make the unique id
# in progress
- zero_conf = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": config_entries.SOURCE_ZEROCONF},
- data={
- "properties": {"macaddress": "1CCAE3DOORBIRD"},
- "name": "Doorstation - abc123._axis-video._tcp.local.",
- "host": "192.168.1.5",
- },
- )
+ with patch(
+ "homeassistant.components.doorbird.config_flow.DoorBird",
+ return_value=doorbirdapi,
+ ):
+ zero_conf = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_ZEROCONF},
+ data={
+ "properties": {"macaddress": "1CCAE3DOORBIRD"},
+ "name": "Doorstation - abc123._axis-video._tcp.local.",
+ "host": "192.168.1.5",
+ },
+ )
+ await hass.async_block_till_done()
assert zero_conf["type"] == data_entry_flow.RESULT_TYPE_FORM
assert zero_conf["step_id"] == "user"
assert zero_conf["errors"] == {}
@@ -159,9 +159,6 @@ async def test_form_import_with_zeroconf_already_discovered(hass):
CONF_CUSTOM_URL
] = "http://legacy.custom.url/should/only/come/in/from/yaml"
- doorbirdapi = _get_mock_doorbirdapi_return_values(
- ready=[True], info={"WIFI_MAC_ADDR": "1CCAE3DOORBIRD"}
- )
with patch(
"homeassistant.components.doorbird.config_flow.DoorBird",
return_value=doorbirdapi,
@@ -199,10 +196,6 @@ async def test_form_import_with_zeroconf_already_discovered(hass):
async def test_form_zeroconf_wrong_oui(hass):
"""Test we abort when we get the wrong OUI via zeroconf."""
- await hass.async_add_executor_job(
- init_recorder_component, hass
- ) # force in memory db
-
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
@@ -220,10 +213,6 @@ async def test_form_zeroconf_wrong_oui(hass):
async def test_form_zeroconf_link_local_ignored(hass):
"""Test we abort when we get a link local address via zeroconf."""
- await hass.async_add_executor_job(
- init_recorder_component, hass
- ) # force in memory db
-
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
@@ -241,27 +230,29 @@ async def test_form_zeroconf_link_local_ignored(hass):
async def test_form_zeroconf_correct_oui(hass):
"""Test we can setup from zeroconf with the correct OUI source."""
- await hass.async_add_executor_job(
- init_recorder_component, hass
- ) # force in memory db
-
+ doorbirdapi = _get_mock_doorbirdapi_return_values(
+ ready=[True], info={"WIFI_MAC_ADDR": "macaddr"}
+ )
await setup.async_setup_component(hass, "persistent_notification", {})
- result = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": config_entries.SOURCE_ZEROCONF},
- data={
- "properties": {"macaddress": "1CCAE3DOORBIRD"},
- "name": "Doorstation - abc123._axis-video._tcp.local.",
- "host": "192.168.1.5",
- },
- )
+ with patch(
+ "homeassistant.components.doorbird.config_flow.DoorBird",
+ return_value=doorbirdapi,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_ZEROCONF},
+ data={
+ "properties": {"macaddress": "1CCAE3DOORBIRD"},
+ "name": "Doorstation - abc123._axis-video._tcp.local.",
+ "host": "192.168.1.5",
+ },
+ )
+ await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
- doorbirdapi = _get_mock_doorbirdapi_return_values(
- ready=[True], info={"WIFI_MAC_ADDR": "macaddr"}
- )
+
with patch(
"homeassistant.components.doorbird.config_flow.DoorBird",
return_value=doorbirdapi,
@@ -288,12 +279,42 @@ async def test_form_zeroconf_correct_oui(hass):
assert len(mock_setup_entry.mock_calls) == 1
+@pytest.mark.parametrize(
+ "doorbell_state_side_effect",
+ [
+ requests.exceptions.HTTPError(response=Mock(status_code=404)),
+ OSError,
+ None,
+ ],
+)
+async def test_form_zeroconf_correct_oui_wrong_device(hass, doorbell_state_side_effect):
+ """Test we can setup from zeroconf with the correct OUI source but not a doorstation."""
+ doorbirdapi = _get_mock_doorbirdapi_return_values(
+ ready=[True], info={"WIFI_MAC_ADDR": "macaddr"}
+ )
+ type(doorbirdapi).doorbell_state = MagicMock(side_effect=doorbell_state_side_effect)
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ with patch(
+ "homeassistant.components.doorbird.config_flow.DoorBird",
+ return_value=doorbirdapi,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_ZEROCONF},
+ data={
+ "properties": {"macaddress": "1CCAE3DOORBIRD"},
+ "name": "Doorstation - abc123._axis-video._tcp.local.",
+ "host": "192.168.1.5",
+ },
+ )
+ await hass.async_block_till_done()
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "not_doorbird_device"
+
+
async def test_form_user_cannot_connect(hass):
"""Test we handle cannot connect error."""
- await hass.async_add_executor_job(
- init_recorder_component, hass
- ) # force in memory db
-
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
@@ -314,18 +335,12 @@ async def test_form_user_cannot_connect(hass):
async def test_form_user_invalid_auth(hass):
"""Test we handle cannot invalid auth error."""
- await hass.async_add_executor_job(
- init_recorder_component, hass
- ) # force in memory db
-
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- mock_urllib_error = urllib.error.HTTPError(
- "http://xyz.tld", 401, "login failed", {}, None
- )
- doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=mock_urllib_error)
+ mock_error = requests.exceptions.HTTPError(response=Mock(status_code=401))
+ doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=mock_error)
with patch(
"homeassistant.components.doorbird.config_flow.DoorBird",
return_value=doorbirdapi,
diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py
index dde66c6bfb7f2a..e037585f8ff4ff 100644
--- a/tests/components/dsmr/test_sensor.py
+++ b/tests/components/dsmr/test_sensor.py
@@ -19,6 +19,7 @@
VOLUME_CUBIC_METERS,
VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR,
)
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, patch
@@ -38,7 +39,7 @@ async def test_setup_platform(hass, dsmr_connection_fixture):
serial_data = {"serial_id": "1234", "serial_id_gas": "5678"}
- with patch("homeassistant.components.dsmr.async_setup", return_value=True), patch(
+ with patch(
"homeassistant.components.dsmr.async_setup_entry", return_value=True
), patch(
"homeassistant.components.dsmr.config_flow._validate_dsmr_connection",
@@ -107,7 +108,7 @@ async def test_default_setup(hass, dsmr_connection_fixture):
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("sensor.power_consumption")
assert entry
@@ -167,7 +168,7 @@ async def test_setup_only_energy(hass, dsmr_connection_fixture):
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("sensor.power_consumption")
assert entry
@@ -183,7 +184,7 @@ async def test_derivative():
config = {"platform": "dsmr"}
- entity = DerivativeDSMREntity("test", "test_device", "5678", "1.0.0", config)
+ entity = DerivativeDSMREntity("test", "test_device", "5678", "1.0.0", config, False)
await entity.async_update()
assert entity.state is None, "initial state not unknown"
diff --git a/tests/components/duckdns/test_init.py b/tests/components/duckdns/test_init.py
index 03fce0df20e5ec..a78d9d280d0c47 100644
--- a/tests/components/duckdns/test_init.py
+++ b/tests/components/duckdns/test_init.py
@@ -87,7 +87,7 @@ async def test_setup_backoff(hass, aioclient_mock):
tme = utcnow()
await hass.async_block_till_done()
- _LOGGER.debug("Backoff...")
+ _LOGGER.debug("Backoff")
for idx in range(1, len(intervals)):
tme += intervals[idx]
async_fire_time_changed(hass, tme)
@@ -156,7 +156,7 @@ async def _return(now):
assert call_count == 1
- _LOGGER.debug("Backoff...")
+ _LOGGER.debug("Backoff")
for idx in range(1, len(intervals)):
tme += intervals[idx]
async_fire_time_changed(hass, tme + timedelta(seconds=0.1))
diff --git a/tests/components/dynalite/common.py b/tests/components/dynalite/common.py
index 48ec378689ed13..446cdc74c0bd01 100644
--- a/tests/components/dynalite/common.py
+++ b/tests/components/dynalite/common.py
@@ -2,11 +2,11 @@
from unittest.mock import AsyncMock, Mock, call, patch
from homeassistant.components import dynalite
-from homeassistant.helpers import entity_registry
+from homeassistant.const import ATTR_SERVICE
+from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry
-ATTR_SERVICE = "service"
ATTR_METHOD = "method"
ATTR_ARGS = "args"
@@ -23,7 +23,7 @@ def create_mock_device(platform, spec):
async def get_entry_id_from_hass(hass):
"""Get the config entry id from hass."""
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
assert ent_reg
conf_entries = hass.config_entries.async_entries(dynalite.DOMAIN)
assert len(conf_entries) == 1
diff --git a/tests/components/dynalite/conftest.py b/tests/components/dynalite/conftest.py
index 187e4f9cbaac6b..59f109e7e47525 100644
--- a/tests/components/dynalite/conftest.py
+++ b/tests/components/dynalite/conftest.py
@@ -1,2 +1,2 @@
"""dynalite conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py
index d231f82d2f851a..34b66399a3e604 100644
--- a/tests/components/dynalite/test_init.py
+++ b/tests/components/dynalite/test_init.py
@@ -7,7 +7,7 @@
from voluptuous import MultipleInvalid
import homeassistant.components.dynalite.const as dynalite
-from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_ROOM
+from homeassistant.const import CONF_DEFAULT, CONF_HOST, CONF_NAME, CONF_PORT, CONF_ROOM
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
@@ -54,7 +54,7 @@ async def test_async_setup(hass):
dynalite.CONF_TEMPLATE: dynalite.CONF_TIME_COVER,
},
},
- dynalite.CONF_DEFAULT: {dynalite.CONF_FADE: 2.3},
+ CONF_DEFAULT: {dynalite.CONF_FADE: 2.3},
dynalite.CONF_ACTIVE: dynalite.ACTIVE_INIT,
dynalite.CONF_PRESET: {
"5": {CONF_NAME: "pres5", dynalite.CONF_FADE: 4.5}
@@ -277,9 +277,7 @@ async def test_unload_entry(hass):
) as mock_unload:
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
- assert mock_unload.call_count == len(dynalite.ENTITY_PLATFORMS)
- expected_calls = [
- call(entry, platform) for platform in dynalite.ENTITY_PLATFORMS
- ]
+ assert mock_unload.call_count == len(dynalite.PLATFORMS)
+ expected_calls = [call(entry, platform) for platform in dynalite.PLATFORMS]
for cur_call in mock_unload.mock_calls:
assert cur_call in expected_calls
diff --git a/tests/components/dynalite/test_light.py b/tests/components/dynalite/test_light.py
index 7df10fb08e8e45..230e7584d70abc 100644
--- a/tests/components/dynalite/test_light.py
+++ b/tests/components/dynalite/test_light.py
@@ -4,7 +4,11 @@
import pytest
from homeassistant.components.light import SUPPORT_BRIGHTNESS
-from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES
+from homeassistant.const import (
+ ATTR_FRIENDLY_NAME,
+ ATTR_SUPPORTED_FEATURES,
+ STATE_UNAVAILABLE,
+)
from .common import (
ATTR_METHOD,
@@ -40,11 +44,21 @@ async def test_light_setup(hass, mock_device):
)
-async def test_remove_entity(hass, mock_device):
- """Test when an entity is removed from HA."""
+async def test_unload_config_entry(hass, mock_device):
+ """Test when a config entry is unloaded from HA."""
await create_entity_from_device(hass, mock_device)
assert hass.states.get("light.name")
entry_id = await get_entry_id_from_hass(hass)
assert await hass.config_entries.async_unload(entry_id)
await hass.async_block_till_done()
+ assert hass.states.get("light.name").state == STATE_UNAVAILABLE
+
+
+async def test_remove_config_entry(hass, mock_device):
+ """Test when a config entry is removed from HA."""
+ await create_entity_from_device(hass, mock_device)
+ assert hass.states.get("light.name")
+ entry_id = await get_entry_id_from_hass(hass)
+ assert await hass.config_entries.async_remove(entry_id)
+ await hass.async_block_till_done()
assert not hass.states.get("light.name")
diff --git a/tests/components/dyson/common.py b/tests/components/dyson/common.py
index b26c48d55f8378..4fde47183d2b6a 100644
--- a/tests/components/dyson/common.py
+++ b/tests/components/dyson/common.py
@@ -1,6 +1,6 @@
"""Common utils for Dyson tests."""
+from __future__ import annotations
-from typing import Optional, Type
from unittest import mock
from unittest.mock import MagicMock
@@ -37,7 +37,7 @@
@callback
-def async_get_basic_device(spec: Type[DysonDevice]) -> DysonDevice:
+def async_get_basic_device(spec: type[DysonDevice]) -> DysonDevice:
"""Return a basic device with common fields filled out."""
device = MagicMock(spec=spec)
device.serial = SERIAL
@@ -88,7 +88,7 @@ def async_get_purecool_device() -> DysonPureCool:
async def async_update_device(
- hass: HomeAssistant, device: DysonDevice, state_type: Optional[Type] = None
+ hass: HomeAssistant, device: DysonDevice, state_type: type | None = None
) -> None:
"""Update the device using callback function."""
callbacks = [args[0][0] for args in device.add_message_listener.call_args_list]
diff --git a/tests/components/dyson/conftest.py b/tests/components/dyson/conftest.py
index 747f7a43986567..300c80f3a730e8 100644
--- a/tests/components/dyson/conftest.py
+++ b/tests/components/dyson/conftest.py
@@ -26,8 +26,8 @@ async def device(hass: HomeAssistant, request) -> DysonDevice:
device = get_device()
with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True), patch(
f"{BASE_PATH}.DysonAccount.devices", return_value=[device]
- ), patch(f"{BASE_PATH}.DYSON_PLATFORMS", [platform]):
- # DYSON_PLATFORMS is patched so that only the platform being tested is set up
+ ), patch(f"{BASE_PATH}.PLATFORMS", [platform]):
+ # PLATFORMS is patched so that only the platform being tested is set up
await async_setup_component(
hass,
DOMAIN,
diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py
index 0e389000c2965b..2591b90f596e8e 100644
--- a/tests/components/dyson/test_climate.py
+++ b/tests/components/dyson/test_climate.py
@@ -1,6 +1,5 @@
"""Test the Dyson fan component."""
-
-from typing import Type
+from __future__ import annotations
from libpurecool.const import (
AutoMode,
@@ -60,7 +59,7 @@
ATTR_TEMPERATURE,
)
from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from .common import (
ENTITY_NAME,
@@ -74,7 +73,7 @@
@callback
-def async_get_device(spec: Type[DysonDevice]) -> DysonDevice:
+def async_get_device(spec: type[DysonDevice]) -> DysonDevice:
"""Return a Dyson climate device."""
device = async_get_basic_device(spec)
device.state.heat_target = 2900
@@ -99,8 +98,8 @@ def async_get_device(spec: Type[DysonDevice]) -> DysonDevice:
)
async def test_state_common(hass: HomeAssistant, device: DysonDevice) -> None:
"""Test common state and attributes of two types of climate entities."""
- er = await entity_registry.async_get_registry(hass)
- assert er.async_get(ENTITY_ID).unique_id == SERIAL
+ entity_registry = er.async_get(hass)
+ assert entity_registry.async_get(ENTITY_ID).unique_id == SERIAL
state = hass.states.get(ENTITY_ID)
assert state.name == NAME
diff --git a/tests/components/dyson/test_fan.py b/tests/components/dyson/test_fan.py
index 310d919713323a..67149ff7f2e382 100644
--- a/tests/components/dyson/test_fan.py
+++ b/tests/components/dyson/test_fan.py
@@ -1,5 +1,5 @@
"""Test the Dyson fan component."""
-from typing import Type
+from __future__ import annotations
from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation
from libpurecool.dyson_pure_cool import DysonPureCool, DysonPureCoolLink
@@ -19,16 +19,18 @@
ATTR_HEPA_FILTER,
ATTR_NIGHT_MODE,
ATTR_TIMER,
+ PRESET_MODE_AUTO,
SERVICE_SET_ANGLE,
SERVICE_SET_AUTO_MODE,
SERVICE_SET_DYSON_SPEED,
SERVICE_SET_FLOW_DIRECTION_FRONT,
SERVICE_SET_NIGHT_MODE,
SERVICE_SET_TIMER,
- SPEED_LOW,
)
from homeassistant.components.fan import (
ATTR_OSCILLATING,
+ ATTR_PERCENTAGE,
+ ATTR_PRESET_MODE,
ATTR_SPEED,
ATTR_SPEED_LIST,
DOMAIN as PLATFORM_DOMAIN,
@@ -37,7 +39,9 @@
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
SPEED_HIGH,
+ SPEED_LOW,
SPEED_MEDIUM,
+ SPEED_OFF,
SUPPORT_OSCILLATE,
SUPPORT_SET_SPEED,
)
@@ -48,7 +52,7 @@
STATE_ON,
)
from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from .common import (
ENTITY_NAME,
@@ -63,7 +67,7 @@
@callback
-def async_get_device(spec: Type[DysonPureCoolLink]) -> DysonPureCoolLink:
+def async_get_device(spec: type[DysonPureCoolLink]) -> DysonPureCoolLink:
"""Return a Dyson fan device."""
if spec == DysonPureCoolLink:
return async_get_purecoollink_device()
@@ -75,8 +79,8 @@ async def test_state_purecoollink(
hass: HomeAssistant, device: DysonPureCoolLink
) -> None:
"""Test the state of a PureCoolLink fan."""
- er = await entity_registry.async_get_registry(hass)
- assert er.async_get(ENTITY_ID).unique_id == SERIAL
+ entity_registry = er.async_get(hass)
+ assert entity_registry.async_get(ENTITY_ID).unique_id == SERIAL
state = hass.states.get(ENTITY_ID)
assert state.state == STATE_ON
@@ -84,8 +88,16 @@ async def test_state_purecoollink(
attributes = state.attributes
assert attributes[ATTR_NIGHT_MODE] is True
assert attributes[ATTR_OSCILLATING] is True
+ assert attributes[ATTR_PERCENTAGE] == 10
+ assert attributes[ATTR_PRESET_MODE] is None
assert attributes[ATTR_SPEED] == SPEED_LOW
- assert attributes[ATTR_SPEED_LIST] == [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
+ assert attributes[ATTR_SPEED_LIST] == [
+ SPEED_OFF,
+ SPEED_LOW,
+ SPEED_MEDIUM,
+ SPEED_HIGH,
+ PRESET_MODE_AUTO,
+ ]
assert attributes[ATTR_DYSON_SPEED] == 1
assert attributes[ATTR_DYSON_SPEED_LIST] == list(range(1, 11))
assert attributes[ATTR_AUTO_MODE] is False
@@ -106,7 +118,9 @@ async def test_state_purecoollink(
attributes = state.attributes
assert attributes[ATTR_NIGHT_MODE] is False
assert attributes[ATTR_OSCILLATING] is False
- assert attributes[ATTR_SPEED] == SPEED_MEDIUM
+ assert attributes[ATTR_PERCENTAGE] is None
+ assert attributes[ATTR_PRESET_MODE] == "auto"
+ assert attributes[ATTR_SPEED] == PRESET_MODE_AUTO
assert attributes[ATTR_DYSON_SPEED] == "AUTO"
assert attributes[ATTR_AUTO_MODE] is True
@@ -114,8 +128,8 @@ async def test_state_purecoollink(
@pytest.mark.parametrize("device", [DysonPureCool], indirect=True)
async def test_state_purecool(hass: HomeAssistant, device: DysonPureCool) -> None:
"""Test the state of a PureCool fan."""
- er = await entity_registry.async_get_registry(hass)
- assert er.async_get(ENTITY_ID).unique_id == SERIAL
+ entity_registry = er.async_get(hass)
+ assert entity_registry.async_get(ENTITY_ID).unique_id == SERIAL
state = hass.states.get(ENTITY_ID)
assert state.state == STATE_ON
@@ -125,8 +139,16 @@ async def test_state_purecool(hass: HomeAssistant, device: DysonPureCool) -> Non
assert attributes[ATTR_OSCILLATING] is True
assert attributes[ATTR_ANGLE_LOW] == 24
assert attributes[ATTR_ANGLE_HIGH] == 254
+ assert attributes[ATTR_PERCENTAGE] == 10
+ assert attributes[ATTR_PRESET_MODE] is None
assert attributes[ATTR_SPEED] == SPEED_LOW
- assert attributes[ATTR_SPEED_LIST] == [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
+ assert attributes[ATTR_SPEED_LIST] == [
+ SPEED_OFF,
+ SPEED_LOW,
+ SPEED_MEDIUM,
+ SPEED_HIGH,
+ PRESET_MODE_AUTO,
+ ]
assert attributes[ATTR_DYSON_SPEED] == 1
assert attributes[ATTR_DYSON_SPEED_LIST] == list(range(1, 11))
assert attributes[ATTR_AUTO_MODE] is False
@@ -148,7 +170,9 @@ async def test_state_purecool(hass: HomeAssistant, device: DysonPureCool) -> Non
attributes = state.attributes
assert attributes[ATTR_NIGHT_MODE] is False
assert attributes[ATTR_OSCILLATING] is False
- assert attributes[ATTR_SPEED] == SPEED_MEDIUM
+ assert attributes[ATTR_PERCENTAGE] is None
+ assert attributes[ATTR_PRESET_MODE] == "auto"
+ assert attributes[ATTR_SPEED] == PRESET_MODE_AUTO
assert attributes[ATTR_DYSON_SPEED] == "AUTO"
assert attributes[ATTR_AUTO_MODE] is True
assert attributes[ATTR_FLOW_DIRECTION_FRONT] is False
@@ -170,6 +194,11 @@ async def test_state_purecool(hass: HomeAssistant, device: DysonPureCool) -> Non
{ATTR_SPEED: SPEED_LOW},
{"fan_mode": FanMode.FAN, "fan_speed": FanSpeed.FAN_SPEED_4},
),
+ (
+ SERVICE_TURN_ON,
+ {ATTR_PERCENTAGE: 40},
+ {"fan_mode": FanMode.FAN, "fan_speed": FanSpeed.FAN_SPEED_4},
+ ),
(SERVICE_TURN_OFF, {}, {"fan_mode": FanMode.OFF}),
(
SERVICE_OSCILLATE,
@@ -229,6 +258,18 @@ async def test_commands_purecoollink(
"set_fan_speed",
[FanSpeed.FAN_SPEED_4],
),
+ (
+ SERVICE_TURN_ON,
+ {ATTR_PERCENTAGE: 40},
+ "set_fan_speed",
+ [FanSpeed.FAN_SPEED_4],
+ ),
+ (
+ SERVICE_TURN_ON,
+ {ATTR_PRESET_MODE: "auto"},
+ "enable_auto_mode",
+ [],
+ ),
(SERVICE_TURN_OFF, {}, "turn_off", []),
(SERVICE_OSCILLATE, {ATTR_OSCILLATING: True}, "enable_oscillation", []),
(SERVICE_OSCILLATE, {ATTR_OSCILLATING: False}, "disable_oscillation", []),
diff --git a/tests/components/dyson/test_init.py b/tests/components/dyson/test_init.py
index 2535da4d166dd2..714ac919c19270 100644
--- a/tests/components/dyson/test_init.py
+++ b/tests/components/dyson/test_init.py
@@ -54,7 +54,7 @@ async def test_setup_manual(hass: HomeAssistant):
with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True) as login, patch(
f"{BASE_PATH}.DysonAccount.devices", return_value=devices
) as devices_method, patch(
- f"{BASE_PATH}.DYSON_PLATFORMS", ["fan", "vacuum"]
+ f"{BASE_PATH}.PLATFORMS", ["fan", "vacuum"]
): # Patch platforms to get rid of sensors
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
@@ -85,7 +85,7 @@ async def test_setup_autoconnect(hass: HomeAssistant):
with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True), patch(
f"{BASE_PATH}.DysonAccount.devices", return_value=devices
), patch(
- f"{BASE_PATH}.DYSON_PLATFORMS", ["fan"]
+ f"{BASE_PATH}.PLATFORMS", ["fan"]
): # Patch platforms to get rid of sensors
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
diff --git a/tests/components/dyson/test_sensor.py b/tests/components/dyson/test_sensor.py
index a1f8e4bb37c881..5bd6fd85c3a3df 100644
--- a/tests/components/dyson/test_sensor.py
+++ b/tests/components/dyson/test_sensor.py
@@ -1,5 +1,6 @@
"""Test the Dyson sensor(s) component."""
-from typing import List, Type
+from __future__ import annotations
+
from unittest.mock import patch
from libpurecool.dyson_pure_cool import DysonPureCool
@@ -16,7 +17,7 @@
TEMP_FAHRENHEIT,
)
from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem
from .common import (
@@ -79,7 +80,7 @@ def _async_assign_values(
@callback
-def async_get_device(spec: Type[DysonPureCoolLink], combi=False) -> DysonPureCoolLink:
+def async_get_device(spec: type[DysonPureCoolLink], combi=False) -> DysonPureCoolLink:
"""Return a device of the given type."""
device = async_get_basic_device(spec)
_async_assign_values(device, combi=combi)
@@ -113,19 +114,19 @@ def _async_get_entity_id(sensor_type: str) -> str:
indirect=["device"],
)
async def test_sensors(
- hass: HomeAssistant, device: DysonPureCoolLink, sensors: List[str]
+ hass: HomeAssistant, device: DysonPureCoolLink, sensors: list[str]
) -> None:
"""Test the sensors."""
# Temperature is given by the device in kelvin
# Make sure no other sensors are set up
assert len(hass.states.async_all()) == len(sensors)
- er = await entity_registry.async_get_registry(hass)
+ entity_registry = er.async_get(hass)
for sensor in sensors:
entity_id = _async_get_entity_id(sensor)
# Test unique id
- assert er.async_get(entity_id).unique_id == f"{SERIAL}-{sensor}"
+ assert entity_registry.async_get(entity_id).unique_id == f"{SERIAL}-{sensor}"
# Test state
state = hass.states.get(entity_id)
@@ -168,8 +169,8 @@ async def test_temperature(
device = async_get_device(DysonPureCoolLink)
with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True), patch(
f"{BASE_PATH}.DysonAccount.devices", return_value=[device]
- ), patch(f"{BASE_PATH}.DYSON_PLATFORMS", [PLATFORM_DOMAIN]):
- # DYSON_PLATFORMS is patched so that only the platform being tested is set up
+ ), patch(f"{BASE_PATH}.PLATFORMS", [PLATFORM_DOMAIN]):
+ # PLATFORMS is patched so that only the platform being tested is set up
await async_setup_component(
hass,
DOMAIN,
diff --git a/tests/components/dyson/test_vacuum.py b/tests/components/dyson/test_vacuum.py
index 03a3b076b029de..b77dee3270f0cd 100644
--- a/tests/components/dyson/test_vacuum.py
+++ b/tests/components/dyson/test_vacuum.py
@@ -24,7 +24,7 @@
STATE_ON,
)
from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from .common import (
ENTITY_NAME,
@@ -45,8 +45,8 @@ def async_get_device(state=Dyson360EyeMode.FULL_CLEAN_RUNNING) -> Dyson360Eye:
async def test_state(hass: HomeAssistant, device: Dyson360Eye) -> None:
"""Test the state of the vacuum."""
- er = await entity_registry.async_get_registry(hass)
- assert er.async_get(ENTITY_ID).unique_id == SERIAL
+ entity_registry = er.async_get(hass)
+ assert entity_registry.async_get(ENTITY_ID).unique_id == SERIAL
state = hass.states.get(ENTITY_ID)
assert state.name == NAME
diff --git a/tests/components/eafm/test_sensor.py b/tests/components/eafm/test_sensor.py
index a7ee0403c7cd9d..3f2eb72a8e39bb 100644
--- a/tests/components/eafm/test_sensor.py
+++ b/tests/components/eafm/test_sensor.py
@@ -5,7 +5,7 @@
import pytest
from homeassistant import config_entries
-from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT
+from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNAVAILABLE
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -428,5 +428,8 @@ async def test_unload_entry(hass, mock_get_station):
assert await entry.async_unload(hass)
- # And the entity should be gone
- assert not hass.states.get("sensor.my_station_water_level_stage")
+ # And the entity should be unavailable
+ assert (
+ hass.states.get("sensor.my_station_water_level_stage").state
+ == STATE_UNAVAILABLE
+ )
diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py
index a9b9165d7134f9..95b4b290b70a48 100644
--- a/tests/components/ecobee/test_climate.py
+++ b/tests/components/ecobee/test_climate.py
@@ -117,9 +117,9 @@ async def test_fan(ecobee_fixture, thermostat):
"""Test fan property."""
assert const.STATE_ON == thermostat.fan
ecobee_fixture["equipmentStatus"] = ""
- assert STATE_OFF == thermostat.fan
+ assert thermostat.fan == STATE_OFF
ecobee_fixture["equipmentStatus"] = "heatPump, heatPump2"
- assert STATE_OFF == thermostat.fan
+ assert thermostat.fan == STATE_OFF
async def test_hvac_mode(ecobee_fixture, thermostat):
@@ -147,7 +147,7 @@ async def test_hvac_mode2(ecobee_fixture, thermostat):
assert thermostat.hvac_mode == "heat"
-async def test_device_state_attributes(ecobee_fixture, thermostat):
+async def test_extra_state_attributes(ecobee_fixture, thermostat):
"""Test device state attributes property."""
ecobee_fixture["equipmentStatus"] = "heatPump2"
assert {
@@ -155,7 +155,7 @@ async def test_device_state_attributes(ecobee_fixture, thermostat):
"climate_mode": "Climate1",
"fan_min_on_time": 10,
"equipment_running": "heatPump2",
- } == thermostat.device_state_attributes
+ } == thermostat.extra_state_attributes
ecobee_fixture["equipmentStatus"] = "auxHeat2"
assert {
@@ -163,21 +163,21 @@ async def test_device_state_attributes(ecobee_fixture, thermostat):
"climate_mode": "Climate1",
"fan_min_on_time": 10,
"equipment_running": "auxHeat2",
- } == thermostat.device_state_attributes
+ } == thermostat.extra_state_attributes
ecobee_fixture["equipmentStatus"] = "compCool1"
assert {
"fan": "off",
"climate_mode": "Climate1",
"fan_min_on_time": 10,
"equipment_running": "compCool1",
- } == thermostat.device_state_attributes
+ } == thermostat.extra_state_attributes
ecobee_fixture["equipmentStatus"] = ""
assert {
"fan": "off",
"climate_mode": "Climate1",
"fan_min_on_time": 10,
"equipment_running": "",
- } == thermostat.device_state_attributes
+ } == thermostat.extra_state_attributes
ecobee_fixture["equipmentStatus"] = "Unknown"
assert {
@@ -185,7 +185,7 @@ async def test_device_state_attributes(ecobee_fixture, thermostat):
"climate_mode": "Climate1",
"fan_min_on_time": 10,
"equipment_running": "Unknown",
- } == thermostat.device_state_attributes
+ } == thermostat.extra_state_attributes
ecobee_fixture["program"]["currentClimateRef"] = "c2"
assert {
@@ -193,7 +193,7 @@ async def test_device_state_attributes(ecobee_fixture, thermostat):
"climate_mode": "Climate2",
"fan_min_on_time": 10,
"equipment_running": "Unknown",
- } == thermostat.device_state_attributes
+ } == thermostat.extra_state_attributes
async def test_is_aux_heat_on(ecobee_fixture, thermostat):
@@ -209,14 +209,14 @@ async def test_set_temperature(ecobee_fixture, thermostat, data):
data.reset_mock()
thermostat.set_temperature(target_temp_low=20, target_temp_high=30)
data.ecobee.set_hold_temp.assert_has_calls(
- [mock.call(1, 30, 20, "nextTransition", 0)]
+ [mock.call(1, 30, 20, "nextTransition", None)]
)
# Auto -> Hold
data.reset_mock()
thermostat.set_temperature(temperature=20)
data.ecobee.set_hold_temp.assert_has_calls(
- [mock.call(1, 25, 15, "nextTransition", 0)]
+ [mock.call(1, 25, 15, "nextTransition", None)]
)
# Cool -> Hold
@@ -224,7 +224,7 @@ async def test_set_temperature(ecobee_fixture, thermostat, data):
ecobee_fixture["settings"]["hvacMode"] = "cool"
thermostat.set_temperature(temperature=20.5)
data.ecobee.set_hold_temp.assert_has_calls(
- [mock.call(1, 20.5, 20.5, "nextTransition", 0)]
+ [mock.call(1, 20.5, 20.5, "nextTransition", None)]
)
# Heat -> Hold
@@ -232,7 +232,7 @@ async def test_set_temperature(ecobee_fixture, thermostat, data):
ecobee_fixture["settings"]["hvacMode"] = "heat"
thermostat.set_temperature(temperature=20)
data.ecobee.set_hold_temp.assert_has_calls(
- [mock.call(1, 20, 20, "nextTransition", 0)]
+ [mock.call(1, 20, 20, "nextTransition", None)]
)
# Heat -> Auto
@@ -311,7 +311,7 @@ def test_hold_hours(ecobee_fixture, thermostat):
"askMe",
]:
ecobee_fixture["settings"]["holdAction"] = action
- assert thermostat.hold_hours() == 0
+ assert thermostat.hold_hours() is None
async def test_set_fan_mode_on(thermostat, data):
diff --git a/tests/components/efergy/test_sensor.py b/tests/components/efergy/test_sensor.py
index 3dfbfc354f8756..a56e28cb3ef01f 100644
--- a/tests/components/efergy/test_sensor.py
+++ b/tests/components/efergy/test_sensor.py
@@ -63,11 +63,11 @@ async def test_single_sensor_readings(hass, requests_mock):
assert await async_setup_component(hass, "sensor", {"sensor": ONE_SENSOR_CONFIG})
await hass.async_block_till_done()
- assert "38.21" == hass.states.get("sensor.energy_consumed").state
- assert "1580" == hass.states.get("sensor.energy_usage").state
- assert "ok" == hass.states.get("sensor.energy_budget").state
- assert "5.27" == hass.states.get("sensor.energy_cost").state
- assert "1628" == hass.states.get("sensor.efergy_728386").state
+ assert hass.states.get("sensor.energy_consumed").state == "38.21"
+ assert hass.states.get("sensor.energy_usage").state == "1580"
+ assert hass.states.get("sensor.energy_budget").state == "ok"
+ assert hass.states.get("sensor.energy_cost").state == "5.27"
+ assert hass.states.get("sensor.efergy_728386").state == "1628"
async def test_multi_sensor_readings(hass, requests_mock):
@@ -76,6 +76,6 @@ async def test_multi_sensor_readings(hass, requests_mock):
assert await async_setup_component(hass, "sensor", {"sensor": MULTI_SENSOR_CONFIG})
await hass.async_block_till_done()
- assert "218" == hass.states.get("sensor.efergy_728386").state
- assert "1808" == hass.states.get("sensor.efergy_0").state
- assert "312" == hass.states.get("sensor.efergy_728387").state
+ assert hass.states.get("sensor.efergy_728386").state == "218"
+ assert hass.states.get("sensor.efergy_0").state == "1808"
+ assert hass.states.get("sensor.efergy_728387").state == "312"
diff --git a/tests/components/elgato/__init__.py b/tests/components/elgato/__init__.py
index 3b1942aee14f6f..ea63bc0c4d0c65 100644
--- a/tests/components/elgato/__init__.py
+++ b/tests/components/elgato/__init__.py
@@ -14,27 +14,26 @@ async def init_integration(
skip_setup: bool = False,
) -> MockConfigEntry:
"""Set up the Elgato Key Light integration in Home Assistant."""
-
aioclient_mock.get(
- "http://1.2.3.4:9123/elgato/accessory-info",
+ "http://127.0.0.1:9123/elgato/accessory-info",
text=load_fixture("elgato/info.json"),
headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.put(
- "http://1.2.3.4:9123/elgato/lights",
+ "http://127.0.0.1:9123/elgato/lights",
text=load_fixture("elgato/state.json"),
headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
- "http://1.2.3.4:9123/elgato/lights",
+ "http://127.0.0.1:9123/elgato/lights",
text=load_fixture("elgato/state.json"),
headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
- "http://5.6.7.8:9123/elgato/accessory-info",
+ "http://127.0.0.2:9123/elgato/accessory-info",
text=load_fixture("elgato/info.json"),
headers={"Content-Type": CONTENT_TYPE_JSON},
)
@@ -43,7 +42,7 @@ async def init_integration(
domain=DOMAIN,
unique_id="CN11A1A00001",
data={
- CONF_HOST: "1.2.3.4",
+ CONF_HOST: "127.0.0.1",
CONF_PORT: 9123,
CONF_SERIAL_NUMBER: "CN11A1A00001",
},
diff --git a/tests/components/elgato/conftest.py b/tests/components/elgato/conftest.py
index f2ac45155fb51f..fe86f26b535f3d 100644
--- a/tests/components/elgato/conftest.py
+++ b/tests/components/elgato/conftest.py
@@ -1,2 +1,2 @@
"""elgato conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py
index c1dfa697041a6b..49c85c5f2a25a3 100644
--- a/tests/components/elgato/test_config_flow.py
+++ b/tests/components/elgato/test_config_flow.py
@@ -2,10 +2,9 @@
import aiohttp
from homeassistant import data_entry_flow
-from homeassistant.components.elgato import config_flow
-from homeassistant.components.elgato.const import CONF_SERIAL_NUMBER
+from homeassistant.components.elgato.const import CONF_SERIAL_NUMBER, DOMAIN
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
-from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON
+from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SOURCE, CONTENT_TYPE_JSON
from homeassistant.core import HomeAssistant
from . import init_integration
@@ -14,62 +13,102 @@
from tests.test_util.aiohttp import AiohttpClientMocker
-async def test_show_user_form(hass: HomeAssistant) -> None:
- """Test that the user set up form is served."""
+async def test_full_user_flow_implementation(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test the full manual user flow from start to finish."""
+ aioclient_mock.get(
+ "http://127.0.0.1:9123/elgato/accessory-info",
+ text=load_fixture("elgato/info.json"),
+ headers={"Content-Type": CONTENT_TYPE_JSON},
+ )
+
+ # Start a discovered configuration flow, to guarantee a user flow doesn't abort
+ await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={CONF_SOURCE: SOURCE_ZEROCONF},
+ data={
+ "host": "127.0.0.1",
+ "hostname": "example.local.",
+ "port": 9123,
+ "properties": {},
+ },
+ )
+
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
- context={"source": SOURCE_USER},
+ DOMAIN,
+ context={CONF_SOURCE: SOURCE_USER},
)
assert result["step_id"] == "user"
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={CONF_HOST: "127.0.0.1", CONF_PORT: 9123}
+ )
-async def test_show_zeroconf_confirm_form(hass: HomeAssistant) -> None:
- """Test that the zeroconf confirmation form is served."""
- flow = config_flow.ElgatoFlowHandler()
- flow.hass = hass
- flow.context = {"source": SOURCE_ZEROCONF, CONF_SERIAL_NUMBER: "12345"}
- result = await flow.async_step_zeroconf_confirm()
+ assert result["data"][CONF_HOST] == "127.0.0.1"
+ assert result["data"][CONF_PORT] == 9123
+ assert result["data"][CONF_SERIAL_NUMBER] == "CN11A1A00001"
+ assert result["title"] == "CN11A1A00001"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result["description_placeholders"] == {CONF_SERIAL_NUMBER: "12345"}
- assert result["step_id"] == "zeroconf_confirm"
- assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ entries = hass.config_entries.async_entries(DOMAIN)
+ assert entries[0].unique_id == "CN11A1A00001"
-async def test_show_zerconf_form(
+async def test_full_zeroconf_flow_implementation(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
- """Test that the zeroconf confirmation form is served."""
+ """Test the zeroconf flow from start to finish."""
aioclient_mock.get(
- "http://1.2.3.4:9123/elgato/accessory-info",
+ "http://127.0.0.1:9123/elgato/accessory-info",
text=load_fixture("elgato/info.json"),
headers={"Content-Type": CONTENT_TYPE_JSON},
)
- flow = config_flow.ElgatoFlowHandler()
- flow.hass = hass
- flow.context = {"source": SOURCE_ZEROCONF}
- result = await flow.async_step_zeroconf({"host": "1.2.3.4", "port": 9123})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={CONF_SOURCE: SOURCE_ZEROCONF},
+ data={
+ "host": "127.0.0.1",
+ "hostname": "example.local.",
+ "port": 9123,
+ "properties": {},
+ },
+ )
- assert flow.context[CONF_HOST] == "1.2.3.4"
- assert flow.context[CONF_PORT] == 9123
- assert flow.context[CONF_SERIAL_NUMBER] == "CN11A1A00001"
assert result["description_placeholders"] == {CONF_SERIAL_NUMBER: "CN11A1A00001"}
assert result["step_id"] == "zeroconf_confirm"
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ progress = hass.config_entries.flow.async_progress()
+ assert len(progress) == 1
+ assert progress[0]["flow_id"] == result["flow_id"]
+ assert progress[0]["context"]["confirm_only"] is True
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={}
+ )
+ assert result["data"][CONF_HOST] == "127.0.0.1"
+ assert result["data"][CONF_PORT] == 9123
+ assert result["data"][CONF_SERIAL_NUMBER] == "CN11A1A00001"
+ assert result["title"] == "CN11A1A00001"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
async def test_connection_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we show user form on Elgato Key Light connection error."""
- aioclient_mock.get("http://1.2.3.4/elgato/accessory-info", exc=aiohttp.ClientError)
+ aioclient_mock.get(
+ "http://127.0.0.1/elgato/accessory-info", exc=aiohttp.ClientError
+ )
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
- context={"source": SOURCE_USER},
- data={CONF_HOST: "1.2.3.4", CONF_PORT: 9123},
+ DOMAIN,
+ context={CONF_SOURCE: SOURCE_USER},
+ data={CONF_HOST: "127.0.0.1", CONF_PORT: 9123},
)
assert result["errors"] == {"base": "cannot_connect"}
@@ -81,51 +120,20 @@ async def test_zeroconf_connection_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort zeroconf flow on Elgato Key Light connection error."""
- aioclient_mock.get("http://1.2.3.4/elgato/accessory-info", exc=aiohttp.ClientError)
+ aioclient_mock.get(
+ "http://127.0.0.1/elgato/accessory-info", exc=aiohttp.ClientError
+ )
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
+ DOMAIN,
context={"source": SOURCE_ZEROCONF},
- data={"host": "1.2.3.4", "port": 9123},
- )
-
- assert result["reason"] == "cannot_connect"
- assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
-
-
-async def test_zeroconf_confirm_connection_error(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
-) -> None:
- """Test we abort zeroconf flow on Elgato Key Light connection error."""
- aioclient_mock.get("http://1.2.3.4/elgato/accessory-info", exc=aiohttp.ClientError)
-
- flow = config_flow.ElgatoFlowHandler()
- flow.hass = hass
- flow.context = {
- "source": SOURCE_ZEROCONF,
- CONF_HOST: "1.2.3.4",
- CONF_PORT: 9123,
- }
- result = await flow.async_step_zeroconf_confirm(
- user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 9123}
+ data={"host": "127.0.0.1", "port": 9123},
)
assert result["reason"] == "cannot_connect"
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
-async def test_zeroconf_no_data(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
-) -> None:
- """Test we abort if zeroconf provides no data."""
- flow = config_flow.ElgatoFlowHandler()
- flow.hass = hass
- result = await flow.async_step_zeroconf()
-
- assert result["reason"] == "cannot_connect"
- assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
-
-
async def test_user_device_exists_abort(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@@ -133,9 +141,9 @@ async def test_user_device_exists_abort(
await init_integration(hass, aioclient_mock)
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
- context={"source": SOURCE_USER},
- data={CONF_HOST: "1.2.3.4", CONF_PORT: 9123},
+ DOMAIN,
+ context={CONF_SOURCE: SOURCE_USER},
+ data={CONF_HOST: "127.0.0.1", CONF_PORT: 9123},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
@@ -148,84 +156,22 @@ async def test_zeroconf_device_exists_abort(
await init_integration(hass, aioclient_mock)
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
- context={"source": SOURCE_ZEROCONF},
- data={"host": "1.2.3.4", "port": 9123},
+ DOMAIN,
+ context={CONF_SOURCE: SOURCE_ZEROCONF},
+ data={"host": "127.0.0.1", "port": 9123},
)
assert result["reason"] == "already_configured"
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
- context={"source": SOURCE_ZEROCONF, CONF_HOST: "1.2.3.4", "port": 9123},
- data={"host": "5.6.7.8", "port": 9123},
+ DOMAIN,
+ context={CONF_SOURCE: SOURCE_ZEROCONF},
+ data={"host": "127.0.0.2", "port": 9123},
)
assert result["reason"] == "already_configured"
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- entries = hass.config_entries.async_entries(config_flow.DOMAIN)
- assert entries[0].data[CONF_HOST] == "5.6.7.8"
-
-
-async def test_full_user_flow_implementation(
- hass: HomeAssistant, aioclient_mock
-) -> None:
- """Test the full manual user flow from start to finish."""
- aioclient_mock.get(
- "http://1.2.3.4:9123/elgato/accessory-info",
- text=load_fixture("elgato/info.json"),
- headers={"Content-Type": CONTENT_TYPE_JSON},
- )
-
- result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
- context={"source": SOURCE_USER},
- )
-
- assert result["step_id"] == "user"
- assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
-
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"], user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 9123}
- )
-
- assert result["data"][CONF_HOST] == "1.2.3.4"
- assert result["data"][CONF_PORT] == 9123
- assert result["data"][CONF_SERIAL_NUMBER] == "CN11A1A00001"
- assert result["title"] == "CN11A1A00001"
- assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
-
- entries = hass.config_entries.async_entries(config_flow.DOMAIN)
- assert entries[0].unique_id == "CN11A1A00001"
-
-
-async def test_full_zeroconf_flow_implementation(
- hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
-) -> None:
- """Test the full manual user flow from start to finish."""
- aioclient_mock.get(
- "http://1.2.3.4:9123/elgato/accessory-info",
- text=load_fixture("elgato/info.json"),
- headers={"Content-Type": CONTENT_TYPE_JSON},
- )
-
- flow = config_flow.ElgatoFlowHandler()
- flow.hass = hass
- flow.context = {"source": SOURCE_ZEROCONF}
- result = await flow.async_step_zeroconf({"host": "1.2.3.4", "port": 9123})
-
- assert flow.context[CONF_HOST] == "1.2.3.4"
- assert flow.context[CONF_PORT] == 9123
- assert flow.context[CONF_SERIAL_NUMBER] == "CN11A1A00001"
- assert result["description_placeholders"] == {CONF_SERIAL_NUMBER: "CN11A1A00001"}
- assert result["step_id"] == "zeroconf_confirm"
- assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
-
- result = await flow.async_step_zeroconf_confirm(user_input={CONF_HOST: "1.2.3.4"})
- assert result["data"][CONF_HOST] == "1.2.3.4"
- assert result["data"][CONF_PORT] == 9123
- assert result["data"][CONF_SERIAL_NUMBER] == "CN11A1A00001"
- assert result["title"] == "CN11A1A00001"
- assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ entries = hass.config_entries.async_entries(DOMAIN)
+ assert entries[0].data[CONF_HOST] == "127.0.0.2"
diff --git a/tests/components/elgato/test_init.py b/tests/components/elgato/test_init.py
index 2f0e39e05a8fef..069e533c423dc6 100644
--- a/tests/components/elgato/test_init.py
+++ b/tests/components/elgato/test_init.py
@@ -14,7 +14,7 @@ async def test_config_entry_not_ready(
) -> None:
"""Test the Elgato Key Light configuration entry not ready."""
aioclient_mock.get(
- "http://1.2.3.4:9123/elgato/accessory-info", exc=aiohttp.ClientError
+ "http://127.0.0.1:9123/elgato/accessory-info", exc=aiohttp.ClientError
)
entry = await init_integration(hass, aioclient_mock)
diff --git a/tests/components/elgato/test_light.py b/tests/components/elgato/test_light.py
index 838608c0aac3ff..6c4de76719fcdf 100644
--- a/tests/components/elgato/test_light.py
+++ b/tests/components/elgato/test_light.py
@@ -1,7 +1,8 @@
"""Tests for the Elgato Key Light light platform."""
from unittest.mock import patch
-from homeassistant.components.elgato.light import ElgatoError
+from elgato import ElgatoError
+
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
@@ -15,6 +16,7 @@
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
from tests.common import mock_coro
from tests.components.elgato import init_integration
@@ -27,7 +29,7 @@ async def test_light_state(
"""Test the creation and values of the Elgato Key Lights."""
await init_integration(hass, aioclient_mock)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
# First segment of the strip
state = hass.states.get("light.frenck")
@@ -91,17 +93,16 @@ async def test_light_unavailable(
with patch(
"homeassistant.components.elgato.light.Elgato.light",
side_effect=ElgatoError,
+ ), patch(
+ "homeassistant.components.elgato.light.Elgato.state",
+ side_effect=ElgatoError,
):
- with patch(
- "homeassistant.components.elgato.light.Elgato.state",
- side_effect=ElgatoError,
- ):
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: "light.frenck"},
- blocking=True,
- )
- await hass.async_block_till_done()
- state = hass.states.get("light.frenck")
- assert state.state == STATE_UNAVAILABLE
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "light.frenck"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ state = hass.states.get("light.frenck")
+ assert state.state == STATE_UNAVAILABLE
diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py
index d73bbe53e9a9a6..a84ff0351d593b 100644
--- a/tests/components/elkm1/test_config_flow.py
+++ b/tests/components/elkm1/test_config_flow.py
@@ -15,9 +15,8 @@ def handler_callbacks(type_, callback):
if type_ == "login":
if invalid_auth is not None:
callback(not invalid_auth)
- elif type_ == "sync_complete":
- if sync_complete:
- callback()
+ elif type_ == "sync_complete" and sync_complete:
+ callback()
mocked_elk = MagicMock()
mocked_elk.add_handler.side_effect = handler_callbacks
diff --git a/tests/components/emonitor/__init__.py b/tests/components/emonitor/__init__.py
new file mode 100644
index 00000000000000..6415078299f413
--- /dev/null
+++ b/tests/components/emonitor/__init__.py
@@ -0,0 +1 @@
+"""Tests for the SiteSage Emonitor integration."""
diff --git a/tests/components/emonitor/test_config_flow.py b/tests/components/emonitor/test_config_flow.py
new file mode 100644
index 00000000000000..1d71275409a644
--- /dev/null
+++ b/tests/components/emonitor/test_config_flow.py
@@ -0,0 +1,223 @@
+"""Test the SiteSage Emonitor config flow."""
+from unittest.mock import MagicMock, patch
+
+from aioemonitor.monitor import EmonitorNetwork, EmonitorStatus
+import aiohttp
+
+from homeassistant import config_entries, setup
+from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS
+from homeassistant.components.emonitor.const import DOMAIN
+from homeassistant.const import CONF_HOST
+
+from tests.common import MockConfigEntry
+
+
+def _mock_emonitor():
+ return EmonitorStatus(
+ MagicMock(), EmonitorNetwork("AABBCCDDEEFF", "1.2.3.4"), MagicMock()
+ )
+
+
+async def test_form(hass):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status",
+ return_value=_mock_emonitor(),
+ ), patch(
+ "homeassistant.components.emonitor.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.2.3.4",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Emonitor DDEEFF"
+ assert result2["data"] == {
+ "host": "1.2.3.4",
+ }
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_unknown_error(hass):
+ """Test we handle unknown error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status",
+ side_effect=Exception,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.2.3.4",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "unknown"}
+
+
+async def test_form_cannot_connect(hass):
+ """Test we handle cannot connect error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status",
+ side_effect=aiohttp.ClientError,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.2.3.4",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {CONF_HOST: "cannot_connect"}
+
+
+async def test_dhcp_can_confirm(hass):
+ """Test DHCP discovery flow can confirm right away."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ with patch(
+ "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status",
+ return_value=_mock_emonitor(),
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "dhcp"},
+ data={
+ HOSTNAME: "emonitor",
+ IP_ADDRESS: "1.2.3.4",
+ MAC_ADDRESS: "aa:bb:cc:dd:ee:ff",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "confirm"
+ assert result["description_placeholders"] == {
+ "host": "1.2.3.4",
+ "name": "Emonitor DDEEFF",
+ }
+
+ with patch(
+ "homeassistant.components.emonitor.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Emonitor DDEEFF"
+ assert result2["data"] == {
+ "host": "1.2.3.4",
+ }
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_dhcp_fails_to_connect(hass):
+ """Test DHCP discovery flow that fails to connect."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ with patch(
+ "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status",
+ side_effect=aiohttp.ClientError,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "dhcp"},
+ data={
+ HOSTNAME: "emonitor",
+ IP_ADDRESS: "1.2.3.4",
+ MAC_ADDRESS: "aa:bb:cc:dd:ee:ff",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+
+async def test_dhcp_already_exists(hass):
+ """Test DHCP discovery flow that fails to connect."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={CONF_HOST: "1.2.3.4"},
+ unique_id="aa:bb:cc:dd:ee:ff",
+ )
+ entry.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status",
+ return_value=_mock_emonitor(),
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "dhcp"},
+ data={
+ HOSTNAME: "emonitor",
+ IP_ADDRESS: "1.2.3.4",
+ MAC_ADDRESS: "aa:bb:cc:dd:ee:ff",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+
+
+async def test_user_unique_id_already_exists(hass):
+ """Test creating an entry where the unique_id already exists."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={CONF_HOST: "1.2.3.4"},
+ unique_id="aa:bb:cc:dd:ee:ff",
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status",
+ return_value=_mock_emonitor(),
+ ), patch(
+ "homeassistant.components.emonitor.async_setup_entry",
+ return_value=True,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.2.3.4",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "abort"
+ assert result2["reason"] == "already_configured"
diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py
index 04832f4adc7223..f10786d36de7f1 100644
--- a/tests/components/emulated_hue/test_hue_api.py
+++ b/tests/components/emulated_hue/test_hue_api.py
@@ -28,6 +28,8 @@
HUE_API_STATE_HUE,
HUE_API_STATE_ON,
HUE_API_STATE_SAT,
+ HUE_API_STATE_TRANSITION,
+ HUE_API_STATE_XY,
HUE_API_USERNAME,
HueAllGroupsStateView,
HueAllLightsStateView,
@@ -39,6 +41,7 @@
)
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
CONTENT_TYPE_JSON,
HTTP_NOT_FOUND,
HTTP_OK,
@@ -51,7 +54,11 @@
from homeassistant.core import callback
import homeassistant.util.dt as dt_util
-from tests.common import async_fire_time_changed, get_test_instance_port
+from tests.common import (
+ async_fire_time_changed,
+ async_mock_service,
+ get_test_instance_port,
+)
HTTP_SERVER_PORT = get_test_instance_port()
BRIDGE_SERVER_PORT = get_test_instance_port()
@@ -83,6 +90,7 @@
"21": "humidifier.hygrostat",
"22": "scene.light_on",
"23": "scene.light_off",
+ "24": "media_player.kitchen",
}
ENTITY_NUMBERS_BY_ID = {v: k for k, v in ENTITY_IDS_BY_NUMBER.items()}
@@ -355,7 +363,7 @@ def mock_service_call(call):
call = turn_off_calls[-1]
assert light.DOMAIN == call.domain
- assert SERVICE_TURN_OFF == call.service
+ assert call.service == SERVICE_TURN_OFF
assert "light.no_brightness" in call.data[ATTR_ENTITY_ID]
@@ -393,11 +401,11 @@ def mock_service_call(call):
# Verify that SERVICE_TURN_ON has been called
await hass_hue.async_block_till_done()
- assert 1 == len(turn_on_calls)
+ assert len(turn_on_calls) == 1
call = turn_on_calls[-1]
assert light.DOMAIN == call.domain
- assert SERVICE_TURN_ON == call.service
+ assert call.service == SERVICE_TURN_ON
assert "light.no_brightness" in call.data[ATTR_ENTITY_ID]
@@ -663,6 +671,25 @@ async def test_put_light_state(hass, hass_hue, hue_client):
assert ceiling_json["state"][HUE_API_STATE_HUE] == 4369
assert ceiling_json["state"][HUE_API_STATE_SAT] == 127
+ # update light state through api
+ await perform_put_light_state(
+ hass_hue,
+ hue_client,
+ "light.ceiling_lights",
+ True,
+ brightness=100,
+ xy=((0.488, 0.48)),
+ )
+
+ # go through api to get the state back
+ ceiling_json = await perform_get_light_state(
+ hue_client, "light.ceiling_lights", HTTP_OK
+ )
+ assert ceiling_json["state"][HUE_API_STATE_BRI] == 100
+ assert hass.states.get("light.ceiling_lights").attributes[light.ATTR_XY_COLOR] == (
+ (0.488, 0.48)
+ )
+
# Go through the API to turn it off
ceiling_result = await perform_put_light_state(
hass_hue, hue_client, "light.ceiling_lights", False
@@ -714,6 +741,30 @@ async def test_put_light_state(hass, hass_hue, hue_client):
== 50
)
+ # mock light.turn_on call
+ hass.states.async_set(
+ "light.ceiling_lights", STATE_ON, {ATTR_SUPPORTED_FEATURES: 55}
+ )
+ call_turn_on = async_mock_service(hass, "light", "turn_on")
+
+ # update light state through api
+ await perform_put_light_state(
+ hass_hue,
+ hue_client,
+ "light.ceiling_lights",
+ True,
+ brightness=99,
+ xy=((0.488, 0.48)),
+ transitiontime=60,
+ )
+
+ await hass.async_block_till_done()
+ assert call_turn_on[0]
+ assert call_turn_on[0].data[ATTR_ENTITY_ID] == ["light.ceiling_lights"]
+ assert call_turn_on[0].data[light.ATTR_BRIGHTNESS] == 99
+ assert call_turn_on[0].data[light.ATTR_XY_COLOR] == ((0.488, 0.48))
+ assert call_turn_on[0].data[light.ATTR_TRANSITION] == 6
+
async def test_put_light_state_script(hass, hass_hue, hue_client):
"""Test the setting of script variables."""
@@ -1173,6 +1224,8 @@ async def perform_put_light_state(
saturation=None,
color_temp=None,
with_state=True,
+ xy=None,
+ transitiontime=None,
):
"""Test the setting of a light state."""
req_headers = {"Content-Type": content_type}
@@ -1188,8 +1241,12 @@ async def perform_put_light_state(
data[HUE_API_STATE_HUE] = hue
if saturation is not None:
data[HUE_API_STATE_SAT] = saturation
+ if xy is not None:
+ data[HUE_API_STATE_XY] = xy
if color_temp is not None:
data[HUE_API_STATE_CT] = color_temp
+ if transitiontime is not None:
+ data[HUE_API_STATE_TRANSITION] = transitiontime
entity_number = ENTITY_NUMBERS_BY_ID[entity_id]
result = await client.put(
diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py
index 6fa6d9695390ca..8e8c0f412499f6 100644
--- a/tests/components/emulated_hue/test_init.py
+++ b/tests/components/emulated_hue/test_init.py
@@ -13,29 +13,30 @@ def test_config_google_home_entity_id_to_number():
with patch(
"homeassistant.components.emulated_hue.load_json",
return_value={"1": "light.test2"},
- ) as json_loader:
- with patch("homeassistant.components.emulated_hue.save_json") as json_saver:
- number = conf.entity_id_to_number("light.test")
- assert number == "2"
+ ) as json_loader, patch(
+ "homeassistant.components.emulated_hue.save_json"
+ ) as json_saver:
+ number = conf.entity_id_to_number("light.test")
+ assert number == "2"
- assert json_saver.mock_calls[0][1][1] == {
- "1": "light.test2",
- "2": "light.test",
- }
+ assert json_saver.mock_calls[0][1][1] == {
+ "1": "light.test2",
+ "2": "light.test",
+ }
- assert json_saver.call_count == 1
- assert json_loader.call_count == 1
+ assert json_saver.call_count == 1
+ assert json_loader.call_count == 1
- number = conf.entity_id_to_number("light.test")
- assert number == "2"
- assert json_saver.call_count == 1
+ number = conf.entity_id_to_number("light.test")
+ assert number == "2"
+ assert json_saver.call_count == 1
- number = conf.entity_id_to_number("light.test2")
- assert number == "1"
- assert json_saver.call_count == 1
+ number = conf.entity_id_to_number("light.test2")
+ assert number == "1"
+ assert json_saver.call_count == 1
- entity_id = conf.number_to_entity_id("1")
- assert entity_id == "light.test2"
+ entity_id = conf.number_to_entity_id("1")
+ assert entity_id == "light.test2"
def test_config_google_home_entity_id_to_number_altered():
@@ -47,28 +48,29 @@ def test_config_google_home_entity_id_to_number_altered():
with patch(
"homeassistant.components.emulated_hue.load_json",
return_value={"21": "light.test2"},
- ) as json_loader:
- with patch("homeassistant.components.emulated_hue.save_json") as json_saver:
- number = conf.entity_id_to_number("light.test")
- assert number == "22"
- assert json_saver.call_count == 1
- assert json_loader.call_count == 1
+ ) as json_loader, patch(
+ "homeassistant.components.emulated_hue.save_json"
+ ) as json_saver:
+ number = conf.entity_id_to_number("light.test")
+ assert number == "22"
+ assert json_saver.call_count == 1
+ assert json_loader.call_count == 1
- assert json_saver.mock_calls[0][1][1] == {
- "21": "light.test2",
- "22": "light.test",
- }
+ assert json_saver.mock_calls[0][1][1] == {
+ "21": "light.test2",
+ "22": "light.test",
+ }
- number = conf.entity_id_to_number("light.test")
- assert number == "22"
- assert json_saver.call_count == 1
+ number = conf.entity_id_to_number("light.test")
+ assert number == "22"
+ assert json_saver.call_count == 1
- number = conf.entity_id_to_number("light.test2")
- assert number == "21"
- assert json_saver.call_count == 1
+ number = conf.entity_id_to_number("light.test2")
+ assert number == "21"
+ assert json_saver.call_count == 1
- entity_id = conf.number_to_entity_id("21")
- assert entity_id == "light.test2"
+ entity_id = conf.number_to_entity_id("21")
+ assert entity_id == "light.test2"
def test_config_google_home_entity_id_to_number_empty():
@@ -79,25 +81,26 @@ def test_config_google_home_entity_id_to_number_empty():
with patch(
"homeassistant.components.emulated_hue.load_json", return_value={}
- ) as json_loader:
- with patch("homeassistant.components.emulated_hue.save_json") as json_saver:
- number = conf.entity_id_to_number("light.test")
- assert number == "1"
- assert json_saver.call_count == 1
- assert json_loader.call_count == 1
-
- assert json_saver.mock_calls[0][1][1] == {"1": "light.test"}
-
- number = conf.entity_id_to_number("light.test")
- assert number == "1"
- assert json_saver.call_count == 1
-
- number = conf.entity_id_to_number("light.test2")
- assert number == "2"
- assert json_saver.call_count == 2
-
- entity_id = conf.number_to_entity_id("2")
- assert entity_id == "light.test2"
+ ) as json_loader, patch(
+ "homeassistant.components.emulated_hue.save_json"
+ ) as json_saver:
+ number = conf.entity_id_to_number("light.test")
+ assert number == "1"
+ assert json_saver.call_count == 1
+ assert json_loader.call_count == 1
+
+ assert json_saver.mock_calls[0][1][1] == {"1": "light.test"}
+
+ number = conf.entity_id_to_number("light.test")
+ assert number == "1"
+ assert json_saver.call_count == 1
+
+ number = conf.entity_id_to_number("light.test2")
+ assert number == "2"
+ assert json_saver.call_count == 2
+
+ entity_id = conf.number_to_entity_id("2")
+ assert entity_id == "light.test2"
def test_config_alexa_entity_id_to_number():
diff --git a/tests/components/enocean/test_config_flow.py b/tests/components/enocean/test_config_flow.py
index 4cea2a4fb9b4fa..60a20af5eae933 100644
--- a/tests/components/enocean/test_config_flow.py
+++ b/tests/components/enocean/test_config_flow.py
@@ -73,13 +73,14 @@ async def test_detection_flow_with_custom_path(hass):
USER_PROVIDED_PATH = EnOceanFlowHandler.MANUAL_PATH_VALUE
FAKE_DONGLE_PATH = "/fake/dongle"
- with patch(DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=True)):
- with patch(DONGLE_DETECT_METHOD, Mock(return_value=[FAKE_DONGLE_PATH])):
- result = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": "detect"},
- data={CONF_DEVICE: USER_PROVIDED_PATH},
- )
+ with patch(DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=True)), patch(
+ DONGLE_DETECT_METHOD, Mock(return_value=[FAKE_DONGLE_PATH])
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "detect"},
+ data={CONF_DEVICE: USER_PROVIDED_PATH},
+ )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "manual"
@@ -90,13 +91,14 @@ async def test_detection_flow_with_invalid_path(hass):
USER_PROVIDED_PATH = "/invalid/path"
FAKE_DONGLE_PATH = "/fake/dongle"
- with patch(DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=False)):
- with patch(DONGLE_DETECT_METHOD, Mock(return_value=[FAKE_DONGLE_PATH])):
- result = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": "detect"},
- data={CONF_DEVICE: USER_PROVIDED_PATH},
- )
+ with patch(DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=False)), patch(
+ DONGLE_DETECT_METHOD, Mock(return_value=[FAKE_DONGLE_PATH])
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "detect"},
+ data={CONF_DEVICE: USER_PROVIDED_PATH},
+ )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "detect"
diff --git a/tests/components/enphase_envoy/__init__.py b/tests/components/enphase_envoy/__init__.py
new file mode 100644
index 00000000000000..6c6293ab76b4c2
--- /dev/null
+++ b/tests/components/enphase_envoy/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Enphase Envoy integration."""
diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py
new file mode 100644
index 00000000000000..99efca883c8cea
--- /dev/null
+++ b/tests/components/enphase_envoy/test_config_flow.py
@@ -0,0 +1,304 @@
+"""Test the Enphase Envoy config flow."""
+from unittest.mock import MagicMock, patch
+
+import httpx
+
+from homeassistant import config_entries, setup
+from homeassistant.components.enphase_envoy.const import DOMAIN
+from homeassistant.core import HomeAssistant
+
+from tests.common import MockConfigEntry
+
+
+async def test_form(hass: HomeAssistant) -> None:
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.enphase_envoy.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Envoy"
+ assert result2["data"] == {
+ "host": "1.1.1.1",
+ "name": "Envoy",
+ "username": "test-username",
+ "password": "test-password",
+ }
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_invalid_auth(hass: HomeAssistant) -> None:
+ """Test we handle invalid auth."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData",
+ side_effect=httpx.HTTPStatusError(
+ "any", request=MagicMock(), response=MagicMock()
+ ),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "invalid_auth"}
+
+
+async def test_form_cannot_connect(hass: HomeAssistant) -> None:
+ """Test we handle cannot connect error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData",
+ side_effect=httpx.HTTPError("any", request=MagicMock()),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "cannot_connect"}
+
+
+async def test_form_unknown_error(hass: HomeAssistant) -> None:
+ """Test we handle unknown error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData",
+ side_effect=ValueError,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "unknown"}
+
+
+async def test_import(hass: HomeAssistant) -> None:
+ """Test we can import from yaml."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch(
+ "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.enphase_envoy.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "import"},
+ data={
+ "ip_address": "1.1.1.1",
+ "name": "Pool Envoy",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Pool Envoy"
+ assert result2["data"] == {
+ "host": "1.1.1.1",
+ "name": "Pool Envoy",
+ "username": "test-username",
+ "password": "test-password",
+ }
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_zeroconf(hass: HomeAssistant) -> None:
+ """Test we can setup from zeroconf."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "zeroconf"},
+ data={
+ "properties": {"serialnum": "1234"},
+ "host": "1.1.1.1",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ with patch(
+ "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.enphase_envoy.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Envoy 1234"
+ assert result2["result"].unique_id == "1234"
+ assert result2["data"] == {
+ "host": "1.1.1.1",
+ "name": "Envoy 1234",
+ "username": "test-username",
+ "password": "test-password",
+ }
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_host_already_exists(hass: HomeAssistant) -> None:
+ """Test host already exists."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "host": "1.1.1.1",
+ "name": "Envoy",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ title="Envoy",
+ )
+ config_entry.add_to_hass(hass)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData",
+ return_value=True,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "abort"
+ assert result2["reason"] == "already_configured"
+
+
+async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None:
+ """Test serial number already exists from zeroconf."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "host": "1.1.1.1",
+ "name": "Envoy",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ unique_id="1234",
+ title="Envoy",
+ )
+ config_entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "zeroconf"},
+ data={
+ "properties": {"serialnum": "1234"},
+ "host": "1.1.1.1",
+ },
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+
+
+async def test_zeroconf_host_already_exists(hass: HomeAssistant) -> None:
+ """Test hosts already exists from zeroconf."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "host": "1.1.1.1",
+ "name": "Envoy",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ title="Envoy",
+ )
+ config_entry.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.enphase_envoy.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "zeroconf"},
+ data={
+ "properties": {"serialnum": "1234"},
+ "host": "1.1.1.1",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+
+ assert config_entry.unique_id == "1234"
+ assert config_entry.title == "Envoy 1234"
+ assert len(mock_setup_entry.mock_calls) == 1
diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py
index f3afce0d43b109..233255c1a890df 100644
--- a/tests/components/esphome/test_config_flow.py
+++ b/tests/components/esphome/test_config_flow.py
@@ -4,7 +4,7 @@
import pytest
-from homeassistant.components.esphome import DATA_KEY
+from homeassistant.components.esphome import DOMAIN
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
@@ -207,7 +207,7 @@ async def test_discovery_initiation(hass, mock_client):
async def test_discovery_already_configured_hostname(hass, mock_client):
"""Test discovery aborts if already configured via hostname."""
entry = MockConfigEntry(
- domain="esphome",
+ domain=DOMAIN,
data={CONF_HOST: "test8266.local", CONF_PORT: 6053, CONF_PASSWORD: ""},
)
@@ -232,7 +232,7 @@ async def test_discovery_already_configured_hostname(hass, mock_client):
async def test_discovery_already_configured_ip(hass, mock_client):
"""Test discovery aborts if already configured via static IP."""
entry = MockConfigEntry(
- domain="esphome",
+ domain=DOMAIN,
data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""},
)
@@ -257,14 +257,14 @@ async def test_discovery_already_configured_ip(hass, mock_client):
async def test_discovery_already_configured_name(hass, mock_client):
"""Test discovery aborts if already configured via name."""
entry = MockConfigEntry(
- domain="esphome",
+ domain=DOMAIN,
data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""},
)
entry.add_to_hass(hass)
mock_entry_data = MagicMock()
mock_entry_data.device_info.name = "test8266"
- hass.data[DATA_KEY] = {entry.entry_id: mock_entry_data}
+ hass.data[DOMAIN] = {entry.entry_id: mock_entry_data}
service_info = {
"host": "192.168.43.184",
@@ -310,7 +310,7 @@ async def test_discovery_duplicate_data(hass, mock_client):
async def test_discovery_updates_unique_id(hass, mock_client):
"""Test a duplicate discovery host aborts and updates existing entry."""
entry = MockConfigEntry(
- domain="esphome",
+ domain=DOMAIN,
data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""},
)
diff --git a/tests/components/everlights/conftest.py b/tests/components/everlights/conftest.py
index 31915934b314bd..5009251b10201a 100644
--- a/tests/components/everlights/conftest.py
+++ b/tests/components/everlights/conftest.py
@@ -1,2 +1,2 @@
"""everlights conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
diff --git a/tests/components/ezviz/__init__.py b/tests/components/ezviz/__init__.py
new file mode 100644
index 00000000000000..9a133a6f50bd3f
--- /dev/null
+++ b/tests/components/ezviz/__init__.py
@@ -0,0 +1,118 @@
+"""Tests for the Ezviz integration."""
+from unittest.mock import patch
+
+from homeassistant.components.ezviz.const import (
+ ATTR_SERIAL,
+ ATTR_TYPE_CAMERA,
+ ATTR_TYPE_CLOUD,
+ CONF_CAMERAS,
+ CONF_FFMPEG_ARGUMENTS,
+ DEFAULT_FFMPEG_ARGUMENTS,
+ DEFAULT_TIMEOUT,
+ DOMAIN,
+)
+from homeassistant.const import (
+ CONF_IP_ADDRESS,
+ CONF_PASSWORD,
+ CONF_TIMEOUT,
+ CONF_TYPE,
+ CONF_URL,
+ CONF_USERNAME,
+)
+from homeassistant.helpers.typing import HomeAssistantType
+
+from tests.common import MockConfigEntry
+
+ENTRY_CONFIG = {
+ CONF_USERNAME: "test-username",
+ CONF_PASSWORD: "test-password",
+ CONF_URL: "apiieu.ezvizlife.com",
+ CONF_TYPE: ATTR_TYPE_CLOUD,
+}
+
+ENTRY_OPTIONS = {
+ CONF_FFMPEG_ARGUMENTS: DEFAULT_FFMPEG_ARGUMENTS,
+ CONF_TIMEOUT: DEFAULT_TIMEOUT,
+}
+
+USER_INPUT_VALIDATE = {
+ CONF_USERNAME: "test-username",
+ CONF_PASSWORD: "test-password",
+ CONF_URL: "apiieu.ezvizlife.com",
+}
+
+USER_INPUT = {
+ CONF_USERNAME: "test-username",
+ CONF_PASSWORD: "test-password",
+ CONF_URL: "apiieu.ezvizlife.com",
+ CONF_TYPE: ATTR_TYPE_CLOUD,
+}
+
+USER_INPUT_CAMERA_VALIDATE = {
+ ATTR_SERIAL: "C666666",
+ CONF_PASSWORD: "test-password",
+ CONF_USERNAME: "test-username",
+}
+
+USER_INPUT_CAMERA = {
+ CONF_PASSWORD: "test-password",
+ CONF_USERNAME: "test-username",
+ CONF_TYPE: ATTR_TYPE_CAMERA,
+}
+
+YAML_CONFIG = {
+ CONF_USERNAME: "test-username",
+ CONF_PASSWORD: "test-password",
+ CONF_URL: "apiieu.ezvizlife.com",
+ CONF_CAMERAS: {
+ "C666666": {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}
+ },
+}
+
+YAML_INVALID = {
+ "C666666": {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}
+}
+
+YAML_CONFIG_CAMERA = {
+ ATTR_SERIAL: "C666666",
+ CONF_USERNAME: "test-username",
+ CONF_PASSWORD: "test-password",
+}
+
+DISCOVERY_INFO = {
+ ATTR_SERIAL: "C666666",
+ CONF_USERNAME: None,
+ CONF_PASSWORD: None,
+ CONF_IP_ADDRESS: "127.0.0.1",
+}
+
+TEST = {
+ CONF_USERNAME: None,
+ CONF_PASSWORD: None,
+ CONF_IP_ADDRESS: "127.0.0.1",
+}
+
+
+def _patch_async_setup_entry(return_value=True):
+ return patch(
+ "homeassistant.components.ezviz.async_setup_entry",
+ return_value=return_value,
+ )
+
+
+async def init_integration(
+ hass: HomeAssistantType,
+ *,
+ data: dict = ENTRY_CONFIG,
+ options: dict = ENTRY_OPTIONS,
+ skip_entry_setup: bool = False,
+) -> MockConfigEntry:
+ """Set up the Ezviz integration in Home Assistant."""
+ entry = MockConfigEntry(domain=DOMAIN, data=data, options=options)
+ entry.add_to_hass(hass)
+
+ if not skip_entry_setup:
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ return entry
diff --git a/tests/components/ezviz/conftest.py b/tests/components/ezviz/conftest.py
new file mode 100644
index 00000000000000..64d9981a9806d0
--- /dev/null
+++ b/tests/components/ezviz/conftest.py
@@ -0,0 +1,48 @@
+"""Define fixtures available for all tests."""
+from unittest.mock import MagicMock, patch
+
+from pyezviz import EzvizClient
+from pyezviz.test_cam_rtsp import TestRTSPAuth
+from pytest import fixture
+
+
+@fixture(autouse=True)
+def mock_ffmpeg(hass):
+ """Mock ffmpeg is loaded."""
+ hass.config.components.add("ffmpeg")
+
+
+@fixture
+def ezviz_test_rtsp_config_flow(hass):
+ """Mock the EzvizApi for easier testing."""
+ with patch.object(TestRTSPAuth, "main", return_value=True), patch(
+ "homeassistant.components.ezviz.config_flow.TestRTSPAuth"
+ ) as mock_ezviz_test_rtsp:
+ instance = mock_ezviz_test_rtsp.return_value = TestRTSPAuth(
+ "test-ip",
+ "test-username",
+ "test-password",
+ )
+
+ instance.main = MagicMock(return_value=True)
+
+ yield mock_ezviz_test_rtsp
+
+
+@fixture
+def ezviz_config_flow(hass):
+ """Mock the EzvizAPI for easier config flow testing."""
+ with patch.object(EzvizClient, "login", return_value=True), patch(
+ "homeassistant.components.ezviz.config_flow.EzvizClient"
+ ) as mock_ezviz:
+ instance = mock_ezviz.return_value = EzvizClient(
+ "test-username",
+ "test-password",
+ "local.host",
+ "1",
+ )
+
+ instance.login = MagicMock(return_value=True)
+ instance.get_detection_sensibility = MagicMock(return_value=True)
+
+ yield mock_ezviz
diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py
new file mode 100644
index 00000000000000..b762f10447ff57
--- /dev/null
+++ b/tests/components/ezviz/test_config_flow.py
@@ -0,0 +1,547 @@
+"""Test the Ezviz config flow."""
+
+from unittest.mock import patch
+
+from pyezviz.client import HTTPError, InvalidURL, PyEzvizError
+from pyezviz.test_cam_rtsp import AuthTestResultFailed, InvalidHost
+
+from homeassistant.components.ezviz.const import (
+ ATTR_SERIAL,
+ ATTR_TYPE_CAMERA,
+ ATTR_TYPE_CLOUD,
+ CONF_FFMPEG_ARGUMENTS,
+ DEFAULT_FFMPEG_ARGUMENTS,
+ DEFAULT_TIMEOUT,
+ DOMAIN,
+)
+from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, SOURCE_USER
+from homeassistant.const import (
+ CONF_CUSTOMIZE,
+ CONF_IP_ADDRESS,
+ CONF_PASSWORD,
+ CONF_TIMEOUT,
+ CONF_TYPE,
+ CONF_URL,
+ CONF_USERNAME,
+)
+from homeassistant.data_entry_flow import (
+ RESULT_TYPE_ABORT,
+ RESULT_TYPE_CREATE_ENTRY,
+ RESULT_TYPE_FORM,
+)
+from homeassistant.setup import async_setup_component
+
+from . import (
+ DISCOVERY_INFO,
+ USER_INPUT,
+ USER_INPUT_CAMERA,
+ USER_INPUT_CAMERA_VALIDATE,
+ USER_INPUT_VALIDATE,
+ YAML_CONFIG,
+ YAML_CONFIG_CAMERA,
+ YAML_INVALID,
+ _patch_async_setup_entry,
+ init_integration,
+)
+
+
+async def test_user_form(hass, ezviz_config_flow):
+ """Test the user initiated form."""
+ await async_setup_component(hass, "persistent_notification", {})
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ with _patch_async_setup_entry() as mock_setup_entry:
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ USER_INPUT_VALIDATE,
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "test-username"
+ assert result["data"] == {**USER_INPUT}
+
+ assert len(mock_setup_entry.mock_calls) == 1
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured_account"
+
+
+async def test_user_custom_url(hass, ezviz_config_flow):
+ """Test custom url step."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_USERNAME: "test-user", CONF_PASSWORD: "test-pass", CONF_URL: "customize"},
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "user_custom_url"
+ assert result["errors"] == {}
+
+ with _patch_async_setup_entry() as mock_setup_entry:
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_URL: "test-user"},
+ )
+
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["data"] == {
+ CONF_PASSWORD: "test-pass",
+ CONF_TYPE: ATTR_TYPE_CLOUD,
+ CONF_URL: "test-user",
+ CONF_USERNAME: "test-user",
+ }
+
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_async_step_import(hass, ezviz_config_flow):
+ """Test the config import flow."""
+ await async_setup_component(hass, "persistent_notification", {})
+
+ with _patch_async_setup_entry() as mock_setup_entry:
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG
+ )
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["data"] == USER_INPUT
+
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_async_step_import_camera(hass, ezviz_config_flow):
+ """Test the config import camera flow."""
+ await async_setup_component(hass, "persistent_notification", {})
+
+ with _patch_async_setup_entry() as mock_setup_entry:
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG_CAMERA
+ )
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["data"] == USER_INPUT_CAMERA
+
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_async_step_import_2nd_form_returns_camera(hass, ezviz_config_flow):
+ """Test we get the user initiated form."""
+ await async_setup_component(hass, "persistent_notification", {})
+
+ with _patch_async_setup_entry() as mock_setup_entry:
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG
+ )
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["data"] == USER_INPUT
+
+ assert len(mock_setup_entry.mock_calls) == 1
+
+ with _patch_async_setup_entry() as mock_setup_entry:
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=USER_INPUT_CAMERA_VALIDATE
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["data"] == USER_INPUT_CAMERA
+
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_async_step_import_abort(hass, ezviz_config_flow):
+ """Test the config import flow with invalid data."""
+ await async_setup_component(hass, "persistent_notification", {})
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_INVALID
+ )
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "unknown"
+
+
+async def test_step_discovery_abort_if_cloud_account_missing(hass):
+ """Test discovery and confirm step, abort if cloud account was removed."""
+ await async_setup_component(hass, "persistent_notification", {})
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_DISCOVERY}, data=DISCOVERY_INFO
+ )
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "confirm"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: "test-user",
+ CONF_PASSWORD: "test-pass",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "ezviz_cloud_account_missing"
+
+
+async def test_async_step_discovery(
+ hass, ezviz_config_flow, ezviz_test_rtsp_config_flow
+):
+ """Test discovery and confirm step."""
+ with patch("homeassistant.components.ezviz.PLATFORMS", []):
+ await init_integration(hass)
+ await async_setup_component(hass, "persistent_notification", {})
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_DISCOVERY}, data=DISCOVERY_INFO
+ )
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "confirm"
+ assert result["errors"] == {}
+
+ with _patch_async_setup_entry() as mock_setup_entry:
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: "test-user",
+ CONF_PASSWORD: "test-pass",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["data"] == {
+ CONF_PASSWORD: "test-pass",
+ CONF_TYPE: ATTR_TYPE_CAMERA,
+ CONF_USERNAME: "test-user",
+ }
+
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_options_flow(hass):
+ """Test updating options."""
+ with patch("homeassistant.components.ezviz.PLATFORMS", []):
+ entry = await init_integration(hass)
+
+ assert entry.options[CONF_FFMPEG_ARGUMENTS] == DEFAULT_FFMPEG_ARGUMENTS
+ assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+ assert result["errors"] is None
+
+ with _patch_async_setup_entry() as mock_setup_entry:
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={CONF_FFMPEG_ARGUMENTS: "/H.264", CONF_TIMEOUT: 25},
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["data"][CONF_FFMPEG_ARGUMENTS] == "/H.264"
+ assert result["data"][CONF_TIMEOUT] == 25
+
+ assert len(mock_setup_entry.mock_calls) == 0
+
+
+async def test_user_form_exception(hass, ezviz_config_flow):
+ """Test we handle exception on user form."""
+ ezviz_config_flow.side_effect = PyEzvizError
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ USER_INPUT_VALIDATE,
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {"base": "invalid_auth"}
+
+ ezviz_config_flow.side_effect = InvalidURL
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ USER_INPUT_VALIDATE,
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {"base": "invalid_host"}
+
+ ezviz_config_flow.side_effect = HTTPError
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ USER_INPUT_VALIDATE,
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {"base": "cannot_connect"}
+
+ ezviz_config_flow.side_effect = Exception
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ USER_INPUT_VALIDATE,
+ )
+
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "unknown"
+
+
+async def test_import_exception(hass, ezviz_config_flow):
+ """Test we handle unexpected exception on import."""
+ ezviz_config_flow.side_effect = PyEzvizError
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG
+ )
+
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "invalid_auth"
+
+ ezviz_config_flow.side_effect = InvalidURL
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG
+ )
+
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "invalid_host"
+
+ ezviz_config_flow.side_effect = HTTPError
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG
+ )
+
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "cannot_connect"
+
+ ezviz_config_flow.side_effect = Exception
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG
+ )
+
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "unknown"
+
+
+async def test_discover_exception_step1(
+ hass,
+ ezviz_config_flow,
+):
+ """Test we handle unexpected exception on discovery."""
+ with patch("homeassistant.components.ezviz.PLATFORMS", []):
+ await init_integration(hass)
+
+ await async_setup_component(hass, "persistent_notification", {})
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_DISCOVERY},
+ data={ATTR_SERIAL: "C66666", CONF_IP_ADDRESS: "test-ip"},
+ )
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "confirm"
+ assert result["errors"] == {}
+
+ # Test Step 1
+ ezviz_config_flow.side_effect = PyEzvizError
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: "test-user",
+ CONF_PASSWORD: "test-pass",
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "confirm"
+ assert result["errors"] == {"base": "invalid_auth"}
+
+ ezviz_config_flow.side_effect = InvalidURL
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: "test-user",
+ CONF_PASSWORD: "test-pass",
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "confirm"
+ assert result["errors"] == {"base": "invalid_host"}
+
+ ezviz_config_flow.side_effect = HTTPError
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: "test-user",
+ CONF_PASSWORD: "test-pass",
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "confirm"
+ assert result["errors"] == {"base": "invalid_host"}
+
+ ezviz_config_flow.side_effect = Exception
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: "test-user",
+ CONF_PASSWORD: "test-pass",
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "unknown"
+
+
+async def test_discover_exception_step3(
+ hass,
+ ezviz_config_flow,
+ ezviz_test_rtsp_config_flow,
+):
+ """Test we handle unexpected exception on discovery."""
+ with patch("homeassistant.components.ezviz.PLATFORMS", []):
+ await init_integration(hass)
+ await async_setup_component(hass, "persistent_notification", {})
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_DISCOVERY},
+ data={ATTR_SERIAL: "C66666", CONF_IP_ADDRESS: "test-ip"},
+ )
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "confirm"
+ assert result["errors"] == {}
+
+ # Test Step 3
+ ezviz_test_rtsp_config_flow.side_effect = AuthTestResultFailed
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: "test-user",
+ CONF_PASSWORD: "test-pass",
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "confirm"
+ assert result["errors"] == {"base": "invalid_auth"}
+
+ ezviz_test_rtsp_config_flow.side_effect = InvalidHost
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: "test-user",
+ CONF_PASSWORD: "test-pass",
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "confirm"
+ assert result["errors"] == {"base": "invalid_host"}
+
+ ezviz_test_rtsp_config_flow.side_effect = Exception
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: "test-user",
+ CONF_PASSWORD: "test-pass",
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "unknown"
+
+
+async def test_user_custom_url_exception(hass, ezviz_config_flow):
+ """Test we handle unexpected exception."""
+ ezviz_config_flow.side_effect = PyEzvizError()
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: "test-user",
+ CONF_PASSWORD: "test-pass",
+ CONF_URL: CONF_CUSTOMIZE,
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "user_custom_url"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_URL: "test-user"},
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "user_custom_url"
+ assert result["errors"] == {"base": "invalid_auth"}
+
+ ezviz_config_flow.side_effect = InvalidURL
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_URL: "test-user"},
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "user_custom_url"
+ assert result["errors"] == {"base": "invalid_host"}
+
+ ezviz_config_flow.side_effect = HTTPError
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_URL: "test-user"},
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "user_custom_url"
+ assert result["errors"] == {"base": "cannot_connect"}
+
+ ezviz_config_flow.side_effect = Exception
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_URL: "test-user"},
+ )
+
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "unknown"
diff --git a/tests/components/faa_delays/__init__.py b/tests/components/faa_delays/__init__.py
new file mode 100644
index 00000000000000..2bb5194605dd15
--- /dev/null
+++ b/tests/components/faa_delays/__init__.py
@@ -0,0 +1 @@
+"""Tests for the FAA Delays integration."""
diff --git a/tests/components/faa_delays/test_config_flow.py b/tests/components/faa_delays/test_config_flow.py
new file mode 100644
index 00000000000000..c289f1544153fb
--- /dev/null
+++ b/tests/components/faa_delays/test_config_flow.py
@@ -0,0 +1,120 @@
+"""Test the FAA Delays config flow."""
+from unittest.mock import patch
+
+from aiohttp import ClientConnectionError
+import faadelays
+
+from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.components.faa_delays.const import DOMAIN
+from homeassistant.const import CONF_ID
+from homeassistant.exceptions import HomeAssistantError
+
+from tests.common import MockConfigEntry
+
+
+async def mock_valid_airport(self, *args, **kwargs):
+ """Return a valid airport."""
+ self.name = "Test airport"
+
+
+async def test_form(hass):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch.object(faadelays.Airport, "update", new=mock_valid_airport), patch(
+ "homeassistant.components.faa_delays.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.faa_delays.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "id": "test",
+ },
+ )
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Test airport"
+ assert result2["data"] == {
+ "id": "test",
+ }
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_duplicate_error(hass):
+ """Test that we handle a duplicate configuration."""
+ conf = {CONF_ID: "test"}
+
+ MockConfigEntry(domain=DOMAIN, unique_id="test", data=conf).add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+
+async def test_form_invalid_airport(hass):
+ """Test we handle invalid airport."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "faadelays.Airport.update",
+ side_effect=faadelays.InvalidAirport,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "id": "test",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {CONF_ID: "invalid_airport"}
+
+
+async def test_form_cannot_connect(hass):
+ """Test we handle a connection error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch("faadelays.Airport.update", side_effect=ClientConnectionError):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "id": "test",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "cannot_connect"}
+
+
+async def test_form_unexpected_exception(hass):
+ """Test we handle an unexpected exception."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch("faadelays.Airport.update", side_effect=HomeAssistantError):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "id": "test",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "unknown"}
diff --git a/tests/components/fail2ban/test_sensor.py b/tests/components/fail2ban/test_sensor.py
index e43064c54aba04..f9c78e14888ee2 100644
--- a/tests/components/fail2ban/test_sensor.py
+++ b/tests/components/fail2ban/test_sensor.py
@@ -89,8 +89,8 @@ async def test_single_ban(hass):
sensor.update()
assert sensor.state == "111.111.111.111"
- assert sensor.state_attributes[STATE_CURRENT_BANS] == ["111.111.111.111"]
- assert sensor.state_attributes[STATE_ALL_BANS] == ["111.111.111.111"]
+ assert sensor.extra_state_attributes[STATE_CURRENT_BANS] == ["111.111.111.111"]
+ assert sensor.extra_state_attributes[STATE_ALL_BANS] == ["111.111.111.111"]
async def test_ipv6_ban(hass):
@@ -103,8 +103,8 @@ async def test_ipv6_ban(hass):
sensor.update()
assert sensor.state == "2607:f0d0:1002:51::4"
- assert sensor.state_attributes[STATE_CURRENT_BANS] == ["2607:f0d0:1002:51::4"]
- assert sensor.state_attributes[STATE_ALL_BANS] == ["2607:f0d0:1002:51::4"]
+ assert sensor.extra_state_attributes[STATE_CURRENT_BANS] == ["2607:f0d0:1002:51::4"]
+ assert sensor.extra_state_attributes[STATE_ALL_BANS] == ["2607:f0d0:1002:51::4"]
async def test_multiple_ban(hass):
@@ -117,11 +117,11 @@ async def test_multiple_ban(hass):
sensor.update()
assert sensor.state == "222.222.222.222"
- assert sensor.state_attributes[STATE_CURRENT_BANS] == [
+ assert sensor.extra_state_attributes[STATE_CURRENT_BANS] == [
"111.111.111.111",
"222.222.222.222",
]
- assert sensor.state_attributes[STATE_ALL_BANS] == [
+ assert sensor.extra_state_attributes[STATE_ALL_BANS] == [
"111.111.111.111",
"222.222.222.222",
]
@@ -137,8 +137,8 @@ async def test_unban_all(hass):
sensor.update()
assert sensor.state == "None"
- assert sensor.state_attributes[STATE_CURRENT_BANS] == []
- assert sensor.state_attributes[STATE_ALL_BANS] == [
+ assert sensor.extra_state_attributes[STATE_CURRENT_BANS] == []
+ assert sensor.extra_state_attributes[STATE_ALL_BANS] == [
"111.111.111.111",
"222.222.222.222",
]
@@ -154,8 +154,8 @@ async def test_unban_one(hass):
sensor.update()
assert sensor.state == "222.222.222.222"
- assert sensor.state_attributes[STATE_CURRENT_BANS] == ["222.222.222.222"]
- assert sensor.state_attributes[STATE_ALL_BANS] == [
+ assert sensor.extra_state_attributes[STATE_CURRENT_BANS] == ["222.222.222.222"]
+ assert sensor.extra_state_attributes[STATE_ALL_BANS] == [
"111.111.111.111",
"222.222.222.222",
]
@@ -174,11 +174,11 @@ async def test_multi_jail(hass):
sensor2.update()
assert sensor1.state == "111.111.111.111"
- assert sensor1.state_attributes[STATE_CURRENT_BANS] == ["111.111.111.111"]
- assert sensor1.state_attributes[STATE_ALL_BANS] == ["111.111.111.111"]
+ assert sensor1.extra_state_attributes[STATE_CURRENT_BANS] == ["111.111.111.111"]
+ assert sensor1.extra_state_attributes[STATE_ALL_BANS] == ["111.111.111.111"]
assert sensor2.state == "222.222.222.222"
- assert sensor2.state_attributes[STATE_CURRENT_BANS] == ["222.222.222.222"]
- assert sensor2.state_attributes[STATE_ALL_BANS] == ["222.222.222.222"]
+ assert sensor2.extra_state_attributes[STATE_CURRENT_BANS] == ["222.222.222.222"]
+ assert sensor2.extra_state_attributes[STATE_ALL_BANS] == ["222.222.222.222"]
async def test_ban_active_after_update(hass):
@@ -192,5 +192,5 @@ async def test_ban_active_after_update(hass):
assert sensor.state == "111.111.111.111"
sensor.update()
assert sensor.state == "111.111.111.111"
- assert sensor.state_attributes[STATE_CURRENT_BANS] == ["111.111.111.111"]
- assert sensor.state_attributes[STATE_ALL_BANS] == ["111.111.111.111"]
+ assert sensor.extra_state_attributes[STATE_CURRENT_BANS] == ["111.111.111.111"]
+ assert sensor.extra_state_attributes[STATE_ALL_BANS] == ["111.111.111.111"]
diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py
index 215849e6aabe5c..c32686b9311dfd 100644
--- a/tests/components/fan/common.py
+++ b/tests/components/fan/common.py
@@ -7,9 +7,12 @@
ATTR_DIRECTION,
ATTR_OSCILLATING,
ATTR_PERCENTAGE,
+ ATTR_PERCENTAGE_STEP,
ATTR_PRESET_MODE,
ATTR_SPEED,
DOMAIN,
+ SERVICE_DECREASE_SPEED,
+ SERVICE_INCREASE_SPEED,
SERVICE_OSCILLATE,
SERVICE_SET_DIRECTION,
SERVICE_SET_PERCENTAGE,
@@ -106,6 +109,38 @@ async def async_set_percentage(
await hass.services.async_call(DOMAIN, SERVICE_SET_PERCENTAGE, data, blocking=True)
+async def async_increase_speed(
+ hass, entity_id=ENTITY_MATCH_ALL, percentage_step: int = None
+) -> None:
+ """Increase speed for all or specified fan."""
+ data = {
+ key: value
+ for key, value in [
+ (ATTR_ENTITY_ID, entity_id),
+ (ATTR_PERCENTAGE_STEP, percentage_step),
+ ]
+ if value is not None
+ }
+
+ await hass.services.async_call(DOMAIN, SERVICE_INCREASE_SPEED, data, blocking=True)
+
+
+async def async_decrease_speed(
+ hass, entity_id=ENTITY_MATCH_ALL, percentage_step: int = None
+) -> None:
+ """Decrease speed for all or specified fan."""
+ data = {
+ key: value
+ for key, value in [
+ (ATTR_ENTITY_ID, entity_id),
+ (ATTR_PERCENTAGE_STEP, percentage_step),
+ ]
+ if value is not None
+ }
+
+ await hass.services.async_call(DOMAIN, SERVICE_DECREASE_SPEED, data, blocking=True)
+
+
async def async_set_direction(
hass, entity_id=ENTITY_MATCH_ALL, direction: str = None
) -> None:
diff --git a/tests/components/fan/test_device_action.py b/tests/components/fan/test_device_action.py
index d3a9aedcf9c443..491e6afab6a16f 100644
--- a/tests/components/fan/test_device_action.py
+++ b/tests/components/fan/test_device_action.py
@@ -14,7 +14,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/fan/test_device_condition.py b/tests/components/fan/test_device_condition.py
index 6725587aeda3d2..8f11d963ed3d80 100644
--- a/tests/components/fan/test_device_condition.py
+++ b/tests/components/fan/test_device_condition.py
@@ -15,7 +15,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py
index d96f0a828f361a..cecb4151c3f05b 100644
--- a/tests/components/fan/test_device_trigger.py
+++ b/tests/components/fan/test_device_trigger.py
@@ -1,4 +1,6 @@
"""The tests for Fan device triggers."""
+from datetime import timedelta
+
import pytest
import homeassistant.components.automation as automation
@@ -6,16 +8,19 @@
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.helpers import device_registry
from homeassistant.setup import async_setup_component
+import homeassistant.util.dt as dt_util
from tests.common import (
MockConfigEntry,
assert_lists_same,
+ async_fire_time_changed,
+ async_get_device_automation_capabilities,
async_get_device_automations,
async_mock_service,
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
@@ -65,6 +70,28 @@ async def test_get_triggers(hass, device_reg, entity_reg):
assert_lists_same(triggers, expected_triggers)
+async def test_get_trigger_capabilities(hass, device_reg, entity_reg):
+ """Test we get the expected capabilities from a switch trigger."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+ expected_capabilities = {
+ "extra_fields": [
+ {"name": "for", "optional": True, "type": "positive_time_period_dict"}
+ ]
+ }
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ for trigger in triggers:
+ capabilities = await async_get_device_automation_capabilities(
+ hass, "trigger", trigger
+ )
+ assert capabilities == expected_capabilities
+
+
async def test_if_fires_on_state_change(hass, calls):
"""Test for turn_on and turn_off triggers firing."""
hass.states.async_set("fan.entity", STATE_OFF)
@@ -127,3 +154,57 @@ async def test_if_fires_on_state_change(hass, calls):
await hass.async_block_till_done()
assert len(calls) == 2
assert calls[1].data["some"] == "turn_off - device - fan.entity - on - off - None"
+
+
+async def test_if_fires_on_state_change_with_for(hass, calls):
+ """Test for triggers firing with delay."""
+ entity_id = "fan.entity"
+ hass.states.async_set(entity_id, STATE_ON)
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": entity_id,
+ "type": "turned_off",
+ "for": {"seconds": 5},
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "turn_off {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(
+ (
+ "platform",
+ "entity_id",
+ "from_state.state",
+ "to_state.state",
+ "for",
+ )
+ )
+ },
+ },
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(entity_id).state == STATE_ON
+ assert len(calls) == 0
+
+ hass.states.async_set(entity_id, STATE_OFF)
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ await hass.async_block_till_done()
+ assert (
+ calls[0].data["some"] == f"turn_off device - {entity_id} - on - off - 0:00:05"
+ )
diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py
index f5c303bd416a8b..05ced3b8be760d 100644
--- a/tests/components/fan/test_init.py
+++ b/tests/components/fan/test_init.py
@@ -19,6 +19,8 @@ def test_fanentity():
assert len(fan.speed_list) == 0
assert len(fan.preset_modes) == 0
assert fan.supported_features == 0
+ assert fan.percentage_step == 1
+ assert fan.speed_count == 100
assert fan.capability_attributes == {}
# Test set_speed not required
with pytest.raises(NotImplementedError):
@@ -43,6 +45,8 @@ async def test_async_fanentity(hass):
assert len(fan.speed_list) == 0
assert len(fan.preset_modes) == 0
assert fan.supported_features == 0
+ assert fan.percentage_step == 1
+ assert fan.speed_count == 100
assert fan.capability_attributes == {}
# Test set_speed not required
with pytest.raises(NotImplementedError):
@@ -57,3 +61,7 @@ async def test_async_fanentity(hass):
await fan.async_turn_on()
with pytest.raises(NotImplementedError):
await fan.async_turn_off()
+ with pytest.raises(NotImplementedError):
+ await fan.async_increase_speed()
+ with pytest.raises(NotImplementedError):
+ await fan.async_decrease_speed()
diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py
index 307ba577de3c71..a433bbe9935c97 100644
--- a/tests/components/feedreader/test_init.py
+++ b/tests/components/feedreader/test_init.py
@@ -1,6 +1,5 @@
"""The tests for the feedreader component."""
from datetime import timedelta
-from logging import getLogger
from os import remove
from os.path import exists
import time
@@ -24,8 +23,6 @@
from tests.common import get_test_home_assistant, load_fixture
-_LOGGER = getLogger(__name__)
-
URL = "http://some.rss.local/rss_feed.xml"
VALID_CONFIG_1 = {feedreader.DOMAIN: {CONF_URLS: [URL]}}
VALID_CONFIG_2 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_SCAN_INTERVAL: 60}}
diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py
index 417c84e8ea4f9b..b8cdaf3c88a567 100644
--- a/tests/components/filter/test_sensor.py
+++ b/tests/components/filter/test_sensor.py
@@ -75,7 +75,7 @@ async def test_chain(hass, values):
await hass.async_block_till_done()
state = hass.states.get("sensor.test")
- assert "18.05" == state.state
+ assert state.state == "18.05"
async def test_chain_history(hass, values, missing=False):
@@ -116,24 +116,23 @@ async def test_chain_history(hass, values, missing=False):
with patch(
"homeassistant.components.history.state_changes_during_period",
return_value=fake_states,
+ ), patch(
+ "homeassistant.components.history.get_last_state_changes",
+ return_value=fake_states,
):
- with patch(
- "homeassistant.components.history.get_last_state_changes",
- return_value=fake_states,
- ):
- with assert_setup_component(1, "sensor"):
- assert await async_setup_component(hass, "sensor", config)
- await hass.async_block_till_done()
+ with assert_setup_component(1, "sensor"):
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
- for value in values:
- hass.states.async_set(config["sensor"]["entity_id"], value.state)
- await hass.async_block_till_done()
+ for value in values:
+ hass.states.async_set(config["sensor"]["entity_id"], value.state)
+ await hass.async_block_till_done()
- state = hass.states.get("sensor.test")
- if missing:
- assert "18.05" == state.state
- else:
- assert "17.05" == state.state
+ state = hass.states.get("sensor.test")
+ if missing:
+ assert state.state == "18.05"
+ else:
+ assert state.state == "17.05"
async def test_source_state_none(hass, values):
@@ -234,18 +233,17 @@ async def test_history_time(hass):
with patch(
"homeassistant.components.history.state_changes_during_period",
return_value=fake_states,
+ ), patch(
+ "homeassistant.components.history.get_last_state_changes",
+ return_value=fake_states,
):
- with patch(
- "homeassistant.components.history.get_last_state_changes",
- return_value=fake_states,
- ):
- with assert_setup_component(1, "sensor"):
- assert await async_setup_component(hass, "sensor", config)
- await hass.async_block_till_done()
-
+ with assert_setup_component(1, "sensor"):
+ assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
- state = hass.states.get("sensor.test")
- assert "18.0" == state.state
+
+ await hass.async_block_till_done()
+ state = hass.states.get("sensor.test")
+ assert state.state == "18.0"
async def test_setup(hass):
@@ -316,7 +314,7 @@ async def test_outlier(values):
filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0)
for state in values:
filtered = filt.filter_state(state)
- assert 21 == filtered.state
+ assert filtered.state == 21
def test_outlier_step(values):
@@ -331,7 +329,7 @@ def test_outlier_step(values):
values[-1].state = 22
for state in values:
filtered = filt.filter_state(state)
- assert 22 == filtered.state
+ assert filtered.state == 22
def test_initial_outlier(values):
@@ -340,7 +338,7 @@ def test_initial_outlier(values):
out = ha.State("sensor.test_monitored", 4000)
for state in [out] + values:
filtered = filt.filter_state(state)
- assert 21 == filtered.state
+ assert filtered.state == 21
def test_unknown_state_outlier(values):
@@ -352,7 +350,7 @@ def test_unknown_state_outlier(values):
filtered = filt.filter_state(state)
except ValueError:
assert state.state == "unknown"
- assert 21 == filtered.state
+ assert filtered.state == 21
def test_precision_zero(values):
@@ -372,7 +370,7 @@ def test_lowpass(values):
filtered = filt.filter_state(state)
except ValueError:
assert state.state == "unknown"
- assert 18.05 == filtered.state
+ assert filtered.state == 18.05
def test_range(values):
@@ -438,7 +436,7 @@ def test_time_sma(values):
)
for state in values:
filtered = filt.filter_state(state)
- assert 21.5 == filtered.state
+ assert filtered.state == 21.5
async def test_reload(hass):
diff --git a/tests/components/fireservicerota/test_config_flow.py b/tests/components/fireservicerota/test_config_flow.py
index 69f37bc3ad42a8..752adb2edc53bc 100644
--- a/tests/components/fireservicerota/test_config_flow.py
+++ b/tests/components/fireservicerota/test_config_flow.py
@@ -78,8 +78,6 @@ async def test_step_user(hass):
with patch(
"homeassistant.components.fireservicerota.config_flow.FireServiceRota"
) as mock_fsr, patch(
- "homeassistant.components.fireservicerota.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.fireservicerota.async_setup_entry",
return_value=True,
) as mock_setup_entry:
@@ -108,5 +106,4 @@ async def test_step_user(hass):
},
}
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
diff --git a/tests/components/flo/conftest.py b/tests/components/flo/conftest.py
index 907feb855698a3..d4ba80e6406ddc 100644
--- a/tests/components/flo/conftest.py
+++ b/tests/components/flo/conftest.py
@@ -43,13 +43,19 @@ def aioclient_mock_fixture(aioclient_mock):
headers={"Content-Type": CONTENT_TYPE_JSON},
status=200,
)
- # Mocks the device for flo.
+ # Mocks the devices for flo.
aioclient_mock.get(
"https://api-gw.meetflo.com/api/v2/devices/98765",
text=load_fixture("flo/device_info_response.json"),
status=200,
headers={"Content-Type": CONTENT_TYPE_JSON},
)
+ aioclient_mock.get(
+ "https://api-gw.meetflo.com/api/v2/devices/32839",
+ text=load_fixture("flo/device_info_response_detector.json"),
+ status=200,
+ headers={"Content-Type": CONTENT_TYPE_JSON},
+ )
# Mocks the water consumption for flo.
aioclient_mock.get(
"https://api-gw.meetflo.com/api/v2/water/consumption",
diff --git a/tests/components/flo/test_binary_sensor.py b/tests/components/flo/test_binary_sensor.py
index 64b8f787a85903..b6a8abf727cf98 100644
--- a/tests/components/flo/test_binary_sensor.py
+++ b/tests/components/flo/test_binary_sensor.py
@@ -4,6 +4,7 @@
ATTR_FRIENDLY_NAME,
CONF_PASSWORD,
CONF_USERNAME,
+ STATE_OFF,
STATE_ON,
)
from homeassistant.setup import async_setup_component
@@ -19,12 +20,14 @@ async def test_binary_sensors(hass, config_entry, aioclient_mock_fixture):
)
await hass.async_block_till_done()
- assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1
+ assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2
- # we should have 6 entities for the device
- state = hass.states.get("binary_sensor.pending_system_alerts")
- assert state.state == STATE_ON
- assert state.attributes.get("info") == 0
- assert state.attributes.get("warning") == 2
- assert state.attributes.get("critical") == 0
- assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pending System Alerts"
+ valve_state = hass.states.get("binary_sensor.pending_system_alerts")
+ assert valve_state.state == STATE_ON
+ assert valve_state.attributes.get("info") == 0
+ assert valve_state.attributes.get("warning") == 2
+ assert valve_state.attributes.get("critical") == 0
+ assert valve_state.attributes.get(ATTR_FRIENDLY_NAME) == "Pending System Alerts"
+
+ detector_state = hass.states.get("binary_sensor.water_detected")
+ assert detector_state.state == STATE_OFF
diff --git a/tests/components/flo/test_device.py b/tests/components/flo/test_device.py
index 63e81a16fb438d..5ac31a2bff91d1 100644
--- a/tests/components/flo/test_device.py
+++ b/tests/components/flo/test_device.py
@@ -13,46 +13,64 @@
async def test_device(hass, config_entry, aioclient_mock_fixture, aioclient_mock):
- """Test Flo by Moen device."""
+ """Test Flo by Moen devices."""
config_entry.add_to_hass(hass)
assert await async_setup_component(
hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD}
)
await hass.async_block_till_done()
- assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1
+ assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2
- device: FloDeviceDataUpdateCoordinator = hass.data[FLO_DOMAIN][
+ valve: FloDeviceDataUpdateCoordinator = hass.data[FLO_DOMAIN][
config_entry.entry_id
]["devices"][0]
- assert device.api_client is not None
- assert device.available
- assert device.consumption_today == 3.674
- assert device.current_flow_rate == 0
- assert device.current_psi == 54.20000076293945
- assert device.current_system_mode == "home"
- assert device.target_system_mode == "home"
- assert device.firmware_version == "6.1.1"
- assert device.device_type == "flo_device_v2"
- assert device.id == "98765"
- assert device.last_heard_from_time == "2020-07-24T12:45:00Z"
- assert device.location_id == "mmnnoopp"
- assert device.hass is not None
- assert device.temperature == 70
- assert device.mac_address == "111111111111"
- assert device.model == "flo_device_075_v2"
- assert device.manufacturer == "Flo by Moen"
- assert device.device_name == "Flo by Moen flo_device_075_v2"
- assert device.rssi == -47
- assert device.pending_info_alerts_count == 0
- assert device.pending_critical_alerts_count == 0
- assert device.pending_warning_alerts_count == 2
- assert device.has_alerts is True
- assert device.last_known_valve_state == "open"
- assert device.target_valve_state == "open"
+ assert valve.api_client is not None
+ assert valve.available
+ assert valve.consumption_today == 3.674
+ assert valve.current_flow_rate == 0
+ assert valve.current_psi == 54.20000076293945
+ assert valve.current_system_mode == "home"
+ assert valve.target_system_mode == "home"
+ assert valve.firmware_version == "6.1.1"
+ assert valve.device_type == "flo_device_v2"
+ assert valve.id == "98765"
+ assert valve.last_heard_from_time == "2020-07-24T12:45:00Z"
+ assert valve.location_id == "mmnnoopp"
+ assert valve.hass is not None
+ assert valve.temperature == 70
+ assert valve.mac_address == "111111111111"
+ assert valve.model == "flo_device_075_v2"
+ assert valve.manufacturer == "Flo by Moen"
+ assert valve.device_name == "Smart Water Shutoff"
+ assert valve.rssi == -47
+ assert valve.pending_info_alerts_count == 0
+ assert valve.pending_critical_alerts_count == 0
+ assert valve.pending_warning_alerts_count == 2
+ assert valve.has_alerts is True
+ assert valve.last_known_valve_state == "open"
+ assert valve.target_valve_state == "open"
+
+ detector: FloDeviceDataUpdateCoordinator = hass.data[FLO_DOMAIN][
+ config_entry.entry_id
+ ]["devices"][1]
+ assert detector.api_client is not None
+ assert detector.available
+ assert detector.device_type == "puck_oem"
+ assert detector.id == "32839"
+ assert detector.last_heard_from_time == "2021-03-07T14:05:00Z"
+ assert detector.location_id == "mmnnoopp"
+ assert detector.hass is not None
+ assert detector.temperature == 61
+ assert detector.humidity == 43
+ assert detector.water_detected is False
+ assert detector.mac_address == "1a2b3c4d5e6f"
+ assert detector.model == "puck_v1"
+ assert detector.manufacturer == "Flo by Moen"
+ assert detector.device_name == "Kitchen Sink"
call_count = aioclient_mock.call_count
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=90))
await hass.async_block_till_done()
- assert aioclient_mock.call_count == call_count + 2
+ assert aioclient_mock.call_count == call_count + 4
diff --git a/tests/components/flo/test_init.py b/tests/components/flo/test_init.py
index 9061477da47765..13f16f06cbfbe5 100644
--- a/tests/components/flo/test_init.py
+++ b/tests/components/flo/test_init.py
@@ -13,6 +13,6 @@ async def test_setup_entry(hass, config_entry, aioclient_mock_fixture):
hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD}
)
await hass.async_block_till_done()
- assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1
+ assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2
assert await hass.config_entries.async_unload(config_entry.entry_id)
diff --git a/tests/components/flo/test_sensor.py b/tests/components/flo/test_sensor.py
index 309dfc11266d77..f1572dae02c43c 100644
--- a/tests/components/flo/test_sensor.py
+++ b/tests/components/flo/test_sensor.py
@@ -14,14 +14,19 @@ async def test_sensors(hass, config_entry, aioclient_mock_fixture):
)
await hass.async_block_till_done()
- assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1
+ assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2
- # we should have 5 entities for the device
+ # we should have 5 entities for the valve
assert hass.states.get("sensor.current_system_mode").state == "home"
assert hass.states.get("sensor.today_s_water_usage").state == "3.7"
assert hass.states.get("sensor.water_flow_rate").state == "0"
assert hass.states.get("sensor.water_pressure").state == "54.2"
- assert hass.states.get("sensor.water_temperature").state == "21.1"
+ assert hass.states.get("sensor.water_temperature").state == "21"
+
+ # and 3 entities for the detector
+ assert hass.states.get("sensor.temperature").state == "16"
+ assert hass.states.get("sensor.humidity").state == "43"
+ assert hass.states.get("sensor.battery").state == "100"
async def test_manual_update_entity(
@@ -34,7 +39,7 @@ async def test_manual_update_entity(
)
await hass.async_block_till_done()
- assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1
+ assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2
await async_setup_component(hass, "homeassistant", {})
diff --git a/tests/components/flo/test_services.py b/tests/components/flo/test_services.py
index 270279e4a9d947..4941a118e48f3d 100644
--- a/tests/components/flo/test_services.py
+++ b/tests/components/flo/test_services.py
@@ -25,8 +25,8 @@ async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mo
)
await hass.async_block_till_done()
- assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1
- assert aioclient_mock.call_count == 4
+ assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2
+ assert aioclient_mock.call_count == 6
await hass.services.async_call(
FLO_DOMAIN,
@@ -35,7 +35,7 @@ async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mo
blocking=True,
)
await hass.async_block_till_done()
- assert aioclient_mock.call_count == 5
+ assert aioclient_mock.call_count == 7
await hass.services.async_call(
FLO_DOMAIN,
@@ -44,7 +44,7 @@ async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mo
blocking=True,
)
await hass.async_block_till_done()
- assert aioclient_mock.call_count == 6
+ assert aioclient_mock.call_count == 8
await hass.services.async_call(
FLO_DOMAIN,
@@ -53,7 +53,7 @@ async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mo
blocking=True,
)
await hass.async_block_till_done()
- assert aioclient_mock.call_count == 7
+ assert aioclient_mock.call_count == 9
await hass.services.async_call(
FLO_DOMAIN,
@@ -66,4 +66,4 @@ async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mo
blocking=True,
)
await hass.async_block_till_done()
- assert aioclient_mock.call_count == 8
+ assert aioclient_mock.call_count == 10
diff --git a/tests/components/flo/test_switch.py b/tests/components/flo/test_switch.py
index 25a64433a29365..cc6ab7e3a7ede3 100644
--- a/tests/components/flo/test_switch.py
+++ b/tests/components/flo/test_switch.py
@@ -15,7 +15,7 @@ async def test_valve_switches(hass, config_entry, aioclient_mock_fixture):
)
await hass.async_block_till_done()
- assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1
+ assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2
entity_id = "switch.shutoff_valve"
assert hass.states.get(entity_id).state == STATE_ON
diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py
index 0d4e4f0595ee20..7438b690ab5e45 100644
--- a/tests/components/flux/test_switch.py
+++ b/tests/components/flux/test_switch.py
@@ -140,7 +140,7 @@ async def test_flux_when_switch_is_off(hass, legacy_patchable_time):
# Verify initial state of light
state = hass.states.get(ent1.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -191,7 +191,7 @@ async def test_flux_before_sunrise(hass, legacy_patchable_time):
# Verify initial state of light
state = hass.states.get(ent1.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -250,7 +250,7 @@ async def test_flux_before_sunrise_known_location(hass, legacy_patchable_time):
# Verify initial state of light
state = hass.states.get(ent1.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -309,7 +309,7 @@ async def test_flux_after_sunrise_before_sunset(hass, legacy_patchable_time):
# Verify initial state of light
state = hass.states.get(ent1.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -368,7 +368,7 @@ async def test_flux_after_sunset_before_stop(hass, legacy_patchable_time):
# Verify initial state of light
state = hass.states.get(ent1.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -428,7 +428,7 @@ async def test_flux_after_stop_before_sunrise(hass, legacy_patchable_time):
# Verify initial state of light
state = hass.states.get(ent1.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -487,7 +487,7 @@ async def test_flux_with_custom_start_stop_times(hass, legacy_patchable_time):
# Verify initial state of light
state = hass.states.get(ent1.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -550,7 +550,7 @@ async def test_flux_before_sunrise_stop_next_day(hass, legacy_patchable_time):
# Verify initial state of light
state = hass.states.get(ent1.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -616,7 +616,7 @@ async def test_flux_after_sunrise_before_sunset_stop_next_day(
# Verify initial state of light
state = hass.states.get(ent1.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -682,7 +682,7 @@ async def test_flux_after_sunset_before_midnight_stop_next_day(
# Verify initial state of light
state = hass.states.get(ent1.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -747,7 +747,7 @@ async def test_flux_after_sunset_after_midnight_stop_next_day(
# Verify initial state of light
state = hass.states.get(ent1.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -812,7 +812,7 @@ async def test_flux_after_stop_before_sunrise_stop_next_day(
# Verify initial state of light
state = hass.states.get(ent1.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -872,7 +872,7 @@ async def test_flux_with_custom_colortemps(hass, legacy_patchable_time):
# Verify initial state of light
state = hass.states.get(ent1.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -934,7 +934,7 @@ async def test_flux_with_custom_brightness(hass, legacy_patchable_time):
# Verify initial state of light
state = hass.states.get(ent1.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -1001,17 +1001,17 @@ async def test_flux_with_multiple_lights(hass, legacy_patchable_time):
await hass.async_block_till_done()
state = hass.states.get(ent1.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
state = hass.states.get(ent2.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
state = hass.states.get(ent3.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -1077,7 +1077,7 @@ async def test_flux_with_mired(hass, legacy_patchable_time):
# Verify initial state of light
state = hass.states.get(ent1.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("color_temp") is None
test_time = dt_util.utcnow().replace(hour=8, minute=30, second=0)
@@ -1134,7 +1134,7 @@ async def test_flux_with_rgb(hass, legacy_patchable_time):
# Verify initial state of light
state = hass.states.get(ent1.entity_id)
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert state.attributes.get("color_temp") is None
test_time = dt_util.utcnow().replace(hour=8, minute=30, second=0)
diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py
index 149cbdae4e2560..ffbf7a569f9bea 100644
--- a/tests/components/forked_daapd/test_media_player.py
+++ b/tests/components/forked_daapd/test_media_player.py
@@ -345,12 +345,12 @@ async def pause_side_effect():
async def test_unload_config_entry(hass, config_entry, mock_api_object):
- """Test the player is removed when the config entry is unloaded."""
+ """Test the player is set unavailable when the config entry is unloaded."""
assert hass.states.get(TEST_MASTER_ENTITY_NAME)
assert hass.states.get(TEST_ZONE_ENTITY_NAMES[0])
await config_entry.async_unload(hass)
- assert not hass.states.get(TEST_MASTER_ENTITY_NAME)
- assert not hass.states.get(TEST_ZONE_ENTITY_NAMES[0])
+ assert hass.states.get(TEST_MASTER_ENTITY_NAME).state == STATE_UNAVAILABLE
+ assert hass.states.get(TEST_ZONE_ENTITY_NAMES[0]).state == STATE_UNAVAILABLE
def test_master_state(hass, mock_api_object):
diff --git a/tests/components/foscam/test_config_flow.py b/tests/components/foscam/test_config_flow.py
index 8087ac1894f1a2..3b8910c4dbc94f 100644
--- a/tests/components/foscam/test_config_flow.py
+++ b/tests/components/foscam/test_config_flow.py
@@ -1,7 +1,12 @@
"""Test the Foscam config flow."""
from unittest.mock import patch
-from libpyfoscam.foscam import ERROR_FOSCAM_AUTH, ERROR_FOSCAM_UNAVAILABLE
+from libpyfoscam.foscam import (
+ ERROR_FOSCAM_AUTH,
+ ERROR_FOSCAM_CMD,
+ ERROR_FOSCAM_UNAVAILABLE,
+ ERROR_FOSCAM_UNKNOWN,
+)
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.foscam import config_flow
@@ -14,6 +19,13 @@
config_flow.CONF_USERNAME: "admin",
config_flow.CONF_PASSWORD: "1234",
config_flow.CONF_STREAM: "Main",
+ config_flow.CONF_RTSP_PORT: 554,
+}
+OPERATOR_CONFIG = {
+ config_flow.CONF_USERNAME: "operator",
+}
+INVALID_RESPONSE_CONFIG = {
+ config_flow.CONF_USERNAME: "interr",
}
CAMERA_NAME = "Mocked Foscam Camera"
CAMERA_MAC = "C0:C1:D0:F4:B4:D4"
@@ -23,26 +35,39 @@ def setup_mock_foscam_camera(mock_foscam_camera):
"""Mock FoscamCamera simulating behaviour using a base valid config."""
def configure_mock_on_init(host, port, user, passwd, verbose=False):
- return_code = 0
- data = {}
+ product_all_info_rc = 0
+ dev_info_rc = 0
+ dev_info_data = {}
if (
host != VALID_CONFIG[config_flow.CONF_HOST]
or port != VALID_CONFIG[config_flow.CONF_PORT]
):
- return_code = ERROR_FOSCAM_UNAVAILABLE
+ product_all_info_rc = dev_info_rc = ERROR_FOSCAM_UNAVAILABLE
elif (
- user != VALID_CONFIG[config_flow.CONF_USERNAME]
+ user
+ not in [
+ VALID_CONFIG[config_flow.CONF_USERNAME],
+ OPERATOR_CONFIG[config_flow.CONF_USERNAME],
+ INVALID_RESPONSE_CONFIG[config_flow.CONF_USERNAME],
+ ]
or passwd != VALID_CONFIG[config_flow.CONF_PASSWORD]
):
- return_code = ERROR_FOSCAM_AUTH
+ product_all_info_rc = dev_info_rc = ERROR_FOSCAM_AUTH
+
+ elif user == INVALID_RESPONSE_CONFIG[config_flow.CONF_USERNAME]:
+ product_all_info_rc = dev_info_rc = ERROR_FOSCAM_UNKNOWN
+
+ elif user == OPERATOR_CONFIG[config_flow.CONF_USERNAME]:
+ dev_info_rc = ERROR_FOSCAM_CMD
else:
- data["devName"] = CAMERA_NAME
- data["mac"] = CAMERA_MAC
+ dev_info_data["devName"] = CAMERA_NAME
+ dev_info_data["mac"] = CAMERA_MAC
- mock_foscam_camera.get_dev_info.return_value = (return_code, data)
+ mock_foscam_camera.get_product_all_info.return_value = (product_all_info_rc, {})
+ mock_foscam_camera.get_dev_info.return_value = (dev_info_rc, dev_info_data)
return mock_foscam_camera
@@ -142,12 +167,44 @@ async def test_user_cannot_connect(hass):
assert result["errors"] == {"base": "cannot_connect"}
+async def test_user_invalid_response(hass):
+ """Test we handle invalid response error from user input."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.foscam.config_flow.FoscamCamera",
+ ) as mock_foscam_camera:
+ setup_mock_foscam_camera(mock_foscam_camera)
+
+ invalid_response = VALID_CONFIG.copy()
+ invalid_response[config_flow.CONF_USERNAME] = INVALID_RESPONSE_CONFIG[
+ config_flow.CONF_USERNAME
+ ]
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ invalid_response,
+ )
+
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "invalid_response"}
+
+
async def test_user_already_configured(hass):
"""Test we handle already configured from user input."""
await setup.async_setup_component(hass, "persistent_notification", {})
entry = MockConfigEntry(
- domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC
+ domain=config_flow.DOMAIN,
+ data=VALID_CONFIG,
)
entry.add_to_hass(hass)
@@ -201,6 +258,8 @@ async def test_user_unknown_exception(hass):
async def test_import_user_valid(hass):
"""Test valid config from import."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
with patch(
"homeassistant.components.foscam.config_flow.FoscamCamera",
) as mock_foscam_camera, patch(
@@ -229,6 +288,8 @@ async def test_import_user_valid(hass):
async def test_import_user_valid_with_name(hass):
"""Test valid config with extra name from import."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
with patch(
"homeassistant.components.foscam.config_flow.FoscamCamera",
) as mock_foscam_camera, patch(
@@ -261,10 +322,7 @@ async def test_import_user_valid_with_name(hass):
async def test_import_invalid_auth(hass):
"""Test we handle invalid auth from import."""
- entry = MockConfigEntry(
- domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC
- )
- entry.add_to_hass(hass)
+ await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.foscam.config_flow.FoscamCamera",
@@ -287,11 +345,8 @@ async def test_import_invalid_auth(hass):
async def test_import_cannot_connect(hass):
- """Test we handle invalid auth from import."""
- entry = MockConfigEntry(
- domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC
- )
- entry.add_to_hass(hass)
+ """Test we handle cannot connect error from import."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.foscam.config_flow.FoscamCamera",
@@ -313,10 +368,39 @@ async def test_import_cannot_connect(hass):
assert result["reason"] == "cannot_connect"
+async def test_import_invalid_response(hass):
+ """Test we handle invalid response error from import."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ with patch(
+ "homeassistant.components.foscam.config_flow.FoscamCamera",
+ ) as mock_foscam_camera:
+ setup_mock_foscam_camera(mock_foscam_camera)
+
+ invalid_response = VALID_CONFIG.copy()
+ invalid_response[config_flow.CONF_USERNAME] = INVALID_RESPONSE_CONFIG[
+ config_flow.CONF_USERNAME
+ ]
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data=invalid_response,
+ )
+
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "invalid_response"
+
+
async def test_import_already_configured(hass):
"""Test we handle already configured from import."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
entry = MockConfigEntry(
- domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC
+ domain=config_flow.DOMAIN,
+ data=VALID_CONFIG,
)
entry.add_to_hass(hass)
diff --git a/tests/components/freebox/conftest.py b/tests/components/freebox/conftest.py
index e813469cbbfb4d..3220552b6cf9d4 100644
--- a/tests/components/freebox/conftest.py
+++ b/tests/components/freebox/conftest.py
@@ -1,11 +1,41 @@
"""Test helpers for Freebox."""
-from unittest.mock import patch
+from unittest.mock import AsyncMock, patch
import pytest
+from .const import (
+ DATA_CALL_GET_CALLS_LOG,
+ DATA_CONNECTION_GET_STATUS,
+ DATA_LAN_GET_HOSTS_LIST,
+ DATA_STORAGE_GET_DISKS,
+ DATA_SYSTEM_GET_CONFIG,
+ WIFI_GET_GLOBAL_CONFIG,
+)
+
@pytest.fixture(autouse=True)
def mock_path():
"""Mock path lib."""
with patch("homeassistant.components.freebox.router.Path"):
yield
+
+
+@pytest.fixture(name="router")
+def mock_router():
+ """Mock a successful connection."""
+ with patch("homeassistant.components.freebox.router.Freepybox") as service_mock:
+ instance = service_mock.return_value
+ instance.open = AsyncMock()
+ instance.system.get_config = AsyncMock(return_value=DATA_SYSTEM_GET_CONFIG)
+ # sensor
+ instance.call.get_calls_log = AsyncMock(return_value=DATA_CALL_GET_CALLS_LOG)
+ instance.storage.get_disks = AsyncMock(return_value=DATA_STORAGE_GET_DISKS)
+ instance.connection.get_status = AsyncMock(
+ return_value=DATA_CONNECTION_GET_STATUS
+ )
+ # switch
+ instance.wifi.get_global_config = AsyncMock(return_value=WIFI_GET_GLOBAL_CONFIG)
+ # device_tracker
+ instance.lan.get_hosts_list = AsyncMock(return_value=DATA_LAN_GET_HOSTS_LIST)
+ instance.close = AsyncMock()
+ yield service_mock
diff --git a/tests/components/freebox/const.py b/tests/components/freebox/const.py
new file mode 100644
index 00000000000000..cc3d720d7ef385
--- /dev/null
+++ b/tests/components/freebox/const.py
@@ -0,0 +1,376 @@
+"""Test constants."""
+
+MOCK_HOST = "myrouter.freeboxos.fr"
+MOCK_PORT = 1234
+
+# router
+DATA_SYSTEM_GET_CONFIG = {
+ "mac": "68:A3:78:00:00:00",
+ "model_info": {
+ "has_ext_telephony": True,
+ "has_speakers_jack": True,
+ "wifi_type": "2d4_5g",
+ "pretty_name": "Freebox Server (r2)",
+ "customer_hdd_slots": 0,
+ "name": "fbxgw-r2/full",
+ "has_speakers": True,
+ "internal_hdd_size": 250,
+ "has_femtocell_exp": True,
+ "has_internal_hdd": True,
+ "has_dect": True,
+ },
+ "fans": [{"id": "fan0_speed", "name": "Ventilateur 1", "value": 2130}],
+ "sensors": [
+ {"id": "temp_hdd", "name": "Disque dur", "value": 40},
+ {"id": "temp_sw", "name": "Température Switch", "value": 50},
+ {"id": "temp_cpum", "name": "Température CPU M", "value": 60},
+ {"id": "temp_cpub", "name": "Température CPU B", "value": 56},
+ ],
+ "board_name": "fbxgw2r",
+ "disk_status": "active",
+ "uptime": "156 jours 19 heures 56 minutes 16 secondes",
+ "uptime_val": 13550176,
+ "user_main_storage": "Disque dur",
+ "box_authenticated": True,
+ "serial": "762601T190510709",
+ "firmware_version": "4.2.5",
+}
+
+# sensors
+DATA_CONNECTION_GET_STATUS = {
+ "type": "ethernet",
+ "rate_down": 198900,
+ "bytes_up": 12035728872949,
+ "ipv4_port_range": [0, 65535],
+ "rate_up": 1440000,
+ "bandwidth_up": 700000000,
+ "ipv6": "2a01:e35:ffff:ffff::1",
+ "bandwidth_down": 1000000000,
+ "media": "ftth",
+ "state": "up",
+ "bytes_down": 2355966141297,
+ "ipv4": "82.67.00.00",
+}
+
+DATA_CALL_GET_CALLS_LOG = [
+ {
+ "number": "0988290475",
+ "type": "missed",
+ "id": 94,
+ "duration": 15,
+ "datetime": 1613752718,
+ "contact_id": 0,
+ "line_id": 0,
+ "name": "0988290475",
+ "new": True,
+ },
+ {
+ "number": "0367250217",
+ "type": "missed",
+ "id": 93,
+ "duration": 25,
+ "datetime": 1613662328,
+ "contact_id": 0,
+ "line_id": 0,
+ "name": "0367250217",
+ "new": True,
+ },
+ {
+ "number": "0184726018",
+ "type": "missed",
+ "id": 92,
+ "duration": 25,
+ "datetime": 1613225098,
+ "contact_id": 0,
+ "line_id": 0,
+ "name": "0184726018",
+ "new": True,
+ },
+]
+
+DATA_STORAGE_GET_DISKS = [
+ {
+ "idle_duration": 0,
+ "read_error_requests": 0,
+ "read_requests": 110,
+ "spinning": True,
+ # "table_type": "ms-dos", API returns without dash, but codespell isn't agree
+ "firmware": "SC1D",
+ "type": "internal",
+ "idle": False,
+ "connector": 0,
+ "id": 0,
+ "write_error_requests": 0,
+ "state": "enabled",
+ "write_requests": 2708929,
+ "total_bytes": 250050000000,
+ "model": "ST9250311CS",
+ "active_duration": 0,
+ "temp": 40,
+ "serial": "6VCQY907",
+ "partitions": [
+ {
+ "fstype": "ext4",
+ "total_bytes": 244950000000,
+ "label": "Disque dur",
+ "id": 2,
+ "internal": True,
+ "fsck_result": "no_run_yet",
+ "state": "mounted",
+ "disk_id": 0,
+ "free_bytes": 227390000000,
+ "used_bytes": 5090000000,
+ "path": "L0Rpc3F1ZSBkdXI=",
+ }
+ ],
+ }
+]
+
+# switch
+WIFI_GET_GLOBAL_CONFIG = {"enabled": True, "mac_filter_state": "disabled"}
+
+# device_tracker
+DATA_LAN_GET_HOSTS_LIST = [
+ {
+ "l2ident": {"id": "8C:97:EA:00:00:00", "type": "mac_address"},
+ "active": True,
+ "persistent": False,
+ "names": [
+ {"name": "d633d0c8-958c-43cc-e807-d881b076924b", "source": "mdns"},
+ {"name": "Freebox Player POP", "source": "mdns_srv"},
+ ],
+ "vendor_name": "Freebox SAS",
+ "host_type": "smartphone",
+ "interface": "pub",
+ "id": "ether-8c:97:ea:00:00:00",
+ "last_time_reachable": 1614107652,
+ "primary_name_manual": False,
+ "l3connectivities": [
+ {
+ "addr": "192.168.1.180",
+ "active": True,
+ "reachable": True,
+ "last_activity": 1614107614,
+ "af": "ipv4",
+ "last_time_reachable": 1614104242,
+ },
+ {
+ "addr": "fe80::dcef:dbba:6604:31d1",
+ "active": True,
+ "reachable": True,
+ "last_activity": 1614107645,
+ "af": "ipv6",
+ "last_time_reachable": 1614107645,
+ },
+ {
+ "addr": "2a01:e34:eda1:eb40:8102:4704:7ce0:2ace",
+ "active": False,
+ "reachable": False,
+ "last_activity": 1611574428,
+ "af": "ipv6",
+ "last_time_reachable": 1611574428,
+ },
+ {
+ "addr": "2a01:e34:eda1:eb40:c8e5:c524:c96d:5f5e",
+ "active": False,
+ "reachable": False,
+ "last_activity": 1612475101,
+ "af": "ipv6",
+ "last_time_reachable": 1612475101,
+ },
+ {
+ "addr": "2a01:e34:eda1:eb40:583a:49df:1df0:c2df",
+ "active": True,
+ "reachable": True,
+ "last_activity": 1614107652,
+ "af": "ipv6",
+ "last_time_reachable": 1614107652,
+ },
+ {
+ "addr": "2a01:e34:eda1:eb40:147e:3569:86ab:6aaa",
+ "active": False,
+ "reachable": False,
+ "last_activity": 1612486752,
+ "af": "ipv6",
+ "last_time_reachable": 1612486752,
+ },
+ ],
+ "default_name": "Freebox Player POP",
+ "model": "fbx8am",
+ "reachable": True,
+ "last_activity": 1614107652,
+ "primary_name": "Freebox Player POP",
+ },
+ {
+ "l2ident": {"id": "DE:00:B0:00:00:00", "type": "mac_address"},
+ "active": False,
+ "persistent": False,
+ "vendor_name": "",
+ "host_type": "workstation",
+ "interface": "pub",
+ "id": "ether-de:00:b0:00:00:00",
+ "last_time_reachable": 1607125599,
+ "primary_name_manual": False,
+ "default_name": "",
+ "l3connectivities": [
+ {
+ "addr": "192.168.1.181",
+ "active": False,
+ "reachable": False,
+ "last_activity": 1607125599,
+ "af": "ipv4",
+ "last_time_reachable": 1607125599,
+ },
+ {
+ "addr": "192.168.1.182",
+ "active": False,
+ "reachable": False,
+ "last_activity": 1605958758,
+ "af": "ipv4",
+ "last_time_reachable": 1605958758,
+ },
+ {
+ "addr": "2a01:e34:eda1:eb40:dc00:b0ff:fedf:e30",
+ "active": False,
+ "reachable": False,
+ "last_activity": 1607125594,
+ "af": "ipv6",
+ "last_time_reachable": 1607125594,
+ },
+ ],
+ "reachable": False,
+ "last_activity": 1607125599,
+ "primary_name": "",
+ },
+ {
+ "l2ident": {"id": "DC:00:B0:00:00:00", "type": "mac_address"},
+ "active": True,
+ "persistent": False,
+ "names": [
+ {"name": "Repeteur-Wifi-Freebox", "source": "mdns"},
+ {"name": "Repeteur Wifi Freebox", "source": "mdns_srv"},
+ ],
+ "vendor_name": "",
+ "host_type": "freebox_wifi",
+ "interface": "pub",
+ "id": "ether-dc:00:b0:00:00:00",
+ "last_time_reachable": 1614107678,
+ "primary_name_manual": False,
+ "l3connectivities": [
+ {
+ "addr": "192.168.1.145",
+ "active": True,
+ "reachable": True,
+ "last_activity": 1614107678,
+ "af": "ipv4",
+ "last_time_reachable": 1614107678,
+ },
+ {
+ "addr": "fe80::de00:b0ff:fe52:6ef6",
+ "active": True,
+ "reachable": True,
+ "last_activity": 1614107608,
+ "af": "ipv6",
+ "last_time_reachable": 1614107603,
+ },
+ {
+ "addr": "2a01:e34:eda1:eb40:de00:b0ff:fe52:6ef6",
+ "active": True,
+ "reachable": True,
+ "last_activity": 1614107618,
+ "af": "ipv6",
+ "last_time_reachable": 1614107618,
+ },
+ ],
+ "default_name": "Repeteur Wifi Freebox",
+ "model": "fbxwmr",
+ "reachable": True,
+ "last_activity": 1614107678,
+ "primary_name": "Repeteur Wifi Freebox",
+ },
+ {
+ "l2ident": {"id": "5E:65:55:00:00:00", "type": "mac_address"},
+ "active": False,
+ "persistent": False,
+ "names": [
+ {"name": "iPhoneofQuentin", "source": "dhcp"},
+ {"name": "iPhone-of-Quentin", "source": "mdns"},
+ ],
+ "vendor_name": "",
+ "host_type": "smartphone",
+ "interface": "pub",
+ "id": "ether-5e:65:55:00:00:00",
+ "last_time_reachable": 1612611982,
+ "primary_name_manual": False,
+ "default_name": "iPhonedeQuentin",
+ "l3connectivities": [
+ {
+ "addr": "192.168.1.148",
+ "active": False,
+ "reachable": False,
+ "last_activity": 1612611973,
+ "af": "ipv4",
+ "last_time_reachable": 1612611973,
+ },
+ {
+ "addr": "fe80::14ca:6c30:938b:e281",
+ "active": False,
+ "reachable": False,
+ "last_activity": 1609693223,
+ "af": "ipv6",
+ "last_time_reachable": 1609693223,
+ },
+ {
+ "addr": "fe80::1c90:2b94:1ba2:bd8b",
+ "active": False,
+ "reachable": False,
+ "last_activity": 1610797303,
+ "af": "ipv6",
+ "last_time_reachable": 1610797303,
+ },
+ {
+ "addr": "fe80::8c8:e58b:838e:6785",
+ "active": False,
+ "reachable": False,
+ "last_activity": 1612611951,
+ "af": "ipv6",
+ "last_time_reachable": 1612611946,
+ },
+ {
+ "addr": "2a01:e34:eda1:eb40:f0e7:e198:3a69:58",
+ "active": False,
+ "reachable": False,
+ "last_activity": 1609693245,
+ "af": "ipv6",
+ "last_time_reachable": 1609693245,
+ },
+ {
+ "addr": "2a01:e34:eda1:eb40:1dc4:c6f8:aa20:c83b",
+ "active": False,
+ "reachable": False,
+ "last_activity": 1610797176,
+ "af": "ipv6",
+ "last_time_reachable": 1610797176,
+ },
+ {
+ "addr": "2a01:e34:eda1:eb40:6cf6:5811:1770:c662",
+ "active": False,
+ "reachable": False,
+ "last_activity": 1612611982,
+ "af": "ipv6",
+ "last_time_reachable": 1612611982,
+ },
+ {
+ "addr": "2a01:e34:eda1:eb40:438:9b2c:4f8f:f48a",
+ "active": False,
+ "reachable": False,
+ "last_activity": 1612611946,
+ "af": "ipv6",
+ "last_time_reachable": 1612611946,
+ },
+ ],
+ "reachable": False,
+ "last_activity": 1612611982,
+ "primary_name": "iPhoneofQuentin",
+ },
+]
diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py
index f7150df7efc217..565387d3fb4895 100644
--- a/tests/components/freebox/test_config_flow.py
+++ b/tests/components/freebox/test_config_flow.py
@@ -1,43 +1,43 @@
"""Tests for the Freebox config flow."""
-from unittest.mock import AsyncMock, patch
+from unittest.mock import Mock, patch
-from aiofreepybox.exceptions import (
+from freebox_api.exceptions import (
AuthorizationError,
HttpRequestError,
InvalidTokenError,
)
-import pytest
from homeassistant import data_entry_flow
from homeassistant.components.freebox.const import DOMAIN
-from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, SOURCE_USER
+from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, SOURCE_ZEROCONF
from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.helpers.typing import HomeAssistantType
-from tests.common import MockConfigEntry
-
-HOST = "myrouter.freeboxos.fr"
-PORT = 1234
-
-
-@pytest.fixture(name="connect")
-def mock_controller_connect():
- """Mock a successful connection."""
- with patch("homeassistant.components.freebox.router.Freepybox") as service_mock:
- service_mock.return_value.open = AsyncMock()
- service_mock.return_value.system.get_config = AsyncMock(
- return_value={
- "mac": "abcd",
- "model_info": {"pretty_name": "Pretty Model"},
- "firmware_version": "123",
- }
- )
- service_mock.return_value.lan.get_hosts_list = AsyncMock()
- service_mock.return_value.connection.get_status = AsyncMock()
- service_mock.return_value.close = AsyncMock()
- yield service_mock
+from .const import MOCK_HOST, MOCK_PORT
+from tests.common import MockConfigEntry
-async def test_user(hass):
+MOCK_ZEROCONF_DATA = {
+ "host": "192.168.0.254",
+ "port": 80,
+ "hostname": "Freebox-Server.local.",
+ "type": "_fbx-api._tcp.local.",
+ "name": "Freebox Server._fbx-api._tcp.local.",
+ "properties": {
+ "api_version": "8.0",
+ "device_type": "FreeboxServer1,2",
+ "api_base_url": "/api/",
+ "uid": "b15ab20debb399f95001a9ca207d2777",
+ "https_available": "1",
+ "https_port": f"{MOCK_PORT}",
+ "box_model": "fbxgw-r2/full",
+ "box_model_name": "Freebox Server (r2)",
+ "api_domain": MOCK_HOST,
+ },
+}
+
+
+async def test_user(hass: HomeAssistantType):
"""Test user config."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
@@ -49,81 +49,92 @@ async def test_user(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
- data={CONF_HOST: HOST, CONF_PORT: PORT},
+ data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "link"
-async def test_import(hass):
+async def test_import(hass: HomeAssistantType):
"""Test import step."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
- data={CONF_HOST: HOST, CONF_PORT: PORT},
+ data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "link"
-async def test_discovery(hass):
- """Test discovery step."""
+async def test_zeroconf(hass: HomeAssistantType):
+ """Test zeroconf step."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
- context={"source": SOURCE_DISCOVERY},
- data={CONF_HOST: HOST, CONF_PORT: PORT},
+ context={"source": SOURCE_ZEROCONF},
+ data=MOCK_ZEROCONF_DATA,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "link"
-async def test_link(hass, connect):
+async def test_link(hass: HomeAssistantType, router: Mock):
"""Test linking."""
- result = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": SOURCE_USER},
- data={CONF_HOST: HOST, CONF_PORT: PORT},
- )
+ with patch(
+ "homeassistant.components.freebox.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.freebox.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
+ )
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["result"].unique_id == MOCK_HOST
+ assert result["title"] == MOCK_HOST
+ assert result["data"][CONF_HOST] == MOCK_HOST
+ assert result["data"][CONF_PORT] == MOCK_PORT
- result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
- assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result["result"].unique_id == HOST
- assert result["title"] == HOST
- assert result["data"][CONF_HOST] == HOST
- assert result["data"][CONF_PORT] == PORT
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
-async def test_abort_if_already_setup(hass):
+async def test_abort_if_already_setup(hass: HomeAssistantType):
"""Test we abort if component is already setup."""
MockConfigEntry(
- domain=DOMAIN, data={CONF_HOST: HOST, CONF_PORT: PORT}, unique_id=HOST
+ domain=DOMAIN,
+ data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
+ unique_id=MOCK_HOST,
).add_to_hass(hass)
- # Should fail, same HOST (import)
+ # Should fail, same MOCK_HOST (import)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
- data={CONF_HOST: HOST, CONF_PORT: PORT},
+ data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
- # Should fail, same HOST (flow)
+ # Should fail, same MOCK_HOST (flow)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
- data={CONF_HOST: HOST, CONF_PORT: PORT},
+ data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
-async def test_on_link_failed(hass):
+async def test_on_link_failed(hass: HomeAssistantType):
"""Test when we have errors during linking the router."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
- data={CONF_HOST: HOST, CONF_PORT: PORT},
+ data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
)
with patch(
diff --git a/tests/components/freebox/test_init.py b/tests/components/freebox/test_init.py
new file mode 100644
index 00000000000000..aae5f911e10797
--- /dev/null
+++ b/tests/components/freebox/test_init.py
@@ -0,0 +1,119 @@
+"""Tests for the Freebox config flow."""
+from unittest.mock import Mock, patch
+
+from homeassistant.components.device_tracker import DOMAIN as DT_DOMAIN
+from homeassistant.components.freebox.const import DOMAIN as DOMAIN, SERVICE_REBOOT
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
+from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED
+from homeassistant.const import CONF_HOST, CONF_PORT, STATE_UNAVAILABLE
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.setup import async_setup_component
+
+from .const import MOCK_HOST, MOCK_PORT
+
+from tests.common import MockConfigEntry
+
+
+async def test_setup(hass: HomeAssistantType, router: Mock):
+ """Test setup of integration."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
+ unique_id=MOCK_HOST,
+ )
+ entry.add_to_hass(hass)
+ assert await async_setup_component(hass, DOMAIN, {})
+ await hass.async_block_till_done()
+ assert hass.config_entries.async_entries() == [entry]
+
+ assert router.call_count == 1
+ assert router().open.call_count == 1
+
+ assert hass.services.has_service(DOMAIN, SERVICE_REBOOT)
+
+ with patch(
+ "homeassistant.components.freebox.router.FreeboxRouter.reboot"
+ ) as mock_service:
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_REBOOT,
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_service.assert_called_once()
+
+
+async def test_setup_import(hass: HomeAssistantType, router: Mock):
+ """Test setup of integration from import."""
+ await async_setup_component(hass, "persistent_notification", {})
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
+ unique_id=MOCK_HOST,
+ )
+ entry.add_to_hass(hass)
+ assert await async_setup_component(
+ hass, DOMAIN, {DOMAIN: {CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}}
+ )
+ await hass.async_block_till_done()
+ assert hass.config_entries.async_entries() == [entry]
+
+ assert router.call_count == 1
+ assert router().open.call_count == 1
+
+ assert hass.services.has_service(DOMAIN, SERVICE_REBOOT)
+
+
+async def test_unload_remove(hass: HomeAssistantType, router: Mock):
+ """Test unload and remove of integration."""
+ entity_id_dt = f"{DT_DOMAIN}.freebox_server_r2"
+ entity_id_sensor = f"{SENSOR_DOMAIN}.freebox_download_speed"
+ entity_id_switch = f"{SWITCH_DOMAIN}.freebox_wifi"
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
+ )
+ entry.add_to_hass(hass)
+
+ config_entries = hass.config_entries.async_entries(DOMAIN)
+ assert len(config_entries) == 1
+ assert entry is config_entries[0]
+
+ assert await async_setup_component(hass, DOMAIN, {}) is True
+ await hass.async_block_till_done()
+
+ assert entry.state == ENTRY_STATE_LOADED
+ state_dt = hass.states.get(entity_id_dt)
+ assert state_dt
+ state_sensor = hass.states.get(entity_id_sensor)
+ assert state_sensor
+ state_switch = hass.states.get(entity_id_switch)
+ assert state_switch
+
+ await hass.config_entries.async_unload(entry.entry_id)
+
+ assert entry.state == ENTRY_STATE_NOT_LOADED
+ state_dt = hass.states.get(entity_id_dt)
+ assert state_dt.state == STATE_UNAVAILABLE
+ state_sensor = hass.states.get(entity_id_sensor)
+ assert state_sensor.state == STATE_UNAVAILABLE
+ state_switch = hass.states.get(entity_id_switch)
+ assert state_switch.state == STATE_UNAVAILABLE
+
+ assert router().close.call_count == 1
+ assert not hass.services.has_service(DOMAIN, SERVICE_REBOOT)
+
+ await hass.config_entries.async_remove(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert router().close.call_count == 1
+ assert entry.state == ENTRY_STATE_NOT_LOADED
+ state_dt = hass.states.get(entity_id_dt)
+ assert state_dt is None
+ state_sensor = hass.states.get(entity_id_sensor)
+ assert state_sensor is None
+ state_switch = hass.states.get(entity_id_switch)
+ assert state_switch is None
diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py
index 11067c1aa51bf5..08655033f4dea8 100644
--- a/tests/components/fritzbox/test_init.py
+++ b/tests/components/fritzbox/test_init.py
@@ -4,7 +4,13 @@
from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED
-from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME
+from homeassistant.const import (
+ CONF_DEVICES,
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_USERNAME,
+ STATE_UNAVAILABLE,
+)
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.setup import async_setup_component
@@ -45,8 +51,8 @@ async def test_setup_duplicate_config(hass: HomeAssistantType, fritz: Mock, capl
assert "duplicate host entries found" in caplog.text
-async def test_unload(hass: HomeAssistantType, fritz: Mock):
- """Test unload of integration."""
+async def test_unload_remove(hass: HomeAssistantType, fritz: Mock):
+ """Test unload and remove of integration."""
fritz().get_devices.return_value = [FritzDeviceSwitchMock()]
entity_id = f"{SWITCH_DOMAIN}.fake_name"
@@ -70,6 +76,14 @@ async def test_unload(hass: HomeAssistantType, fritz: Mock):
await hass.config_entries.async_unload(entry.entry_id)
+ assert fritz().logout.call_count == 1
+ assert entry.state == ENTRY_STATE_NOT_LOADED
+ state = hass.states.get(entity_id)
+ assert state.state == STATE_UNAVAILABLE
+
+ await hass.config_entries.async_remove(entry.entry_id)
+ await hass.async_block_till_done()
+
assert fritz().logout.call_count == 1
assert entry.state == ENTRY_STATE_NOT_LOADED
state = hass.states.get(entity_id)
diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py
index 5ae8d707cb1ee5..0e8e31bb20de98 100644
--- a/tests/components/frontend/test_init.py
+++ b/tests/components/frontend/test_init.py
@@ -33,44 +33,67 @@
@pytest.fixture
-def mock_http_client(hass, aiohttp_client):
+async def ignore_frontend_deps(hass):
+ """Frontend dependencies."""
+ frontend = await async_get_integration(hass, "frontend")
+ for dep in frontend.dependencies:
+ if dep not in ("http", "websocket_api"):
+ hass.config.components.add(dep)
+
+
+@pytest.fixture
+async def frontend(hass, ignore_frontend_deps):
+ """Frontend setup with themes."""
+ assert await async_setup_component(
+ hass,
+ "frontend",
+ {},
+ )
+
+
+@pytest.fixture
+async def frontend_themes(hass):
+ """Frontend setup with themes."""
+ assert await async_setup_component(
+ hass,
+ "frontend",
+ CONFIG_THEMES,
+ )
+
+
+@pytest.fixture
+async def mock_http_client(hass, aiohttp_client, frontend):
"""Start the Home Assistant HTTP component."""
- hass.loop.run_until_complete(async_setup_component(hass, "frontend", {}))
- return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
+ return await aiohttp_client(hass.http.app)
@pytest.fixture
-def mock_http_client_with_themes(hass, aiohttp_client):
+async def themes_ws_client(hass, hass_ws_client, frontend_themes):
"""Start the Home Assistant HTTP component."""
- hass.loop.run_until_complete(
- async_setup_component(
- hass,
- "frontend",
- {DOMAIN: {CONF_THEMES: {"happy": {"primary-color": "red"}}}},
- )
- )
- return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
+ return await hass_ws_client(hass)
@pytest.fixture
-def mock_http_client_with_urls(hass, aiohttp_client):
+async def ws_client(hass, hass_ws_client, frontend):
"""Start the Home Assistant HTTP component."""
- hass.loop.run_until_complete(
- async_setup_component(
- hass,
- "frontend",
- {
- DOMAIN: {
- CONF_JS_VERSION: "auto",
- CONF_EXTRA_HTML_URL: ["https://domain.com/my_extra_url.html"],
- CONF_EXTRA_HTML_URL_ES5: [
- "https://domain.com/my_extra_url_es5.html"
- ],
- }
- },
- )
+ return await hass_ws_client(hass)
+
+
+@pytest.fixture
+async def mock_http_client_with_urls(hass, aiohttp_client, ignore_frontend_deps):
+ """Start the Home Assistant HTTP component."""
+ assert await async_setup_component(
+ hass,
+ "frontend",
+ {
+ DOMAIN: {
+ CONF_JS_VERSION: "auto",
+ CONF_EXTRA_HTML_URL: ["https://domain.com/my_extra_url.html"],
+ CONF_EXTRA_HTML_URL_ES5: ["https://domain.com/my_extra_url_es5.html"],
+ }
+ },
)
- return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
+ return await aiohttp_client(hass.http.app)
@pytest.fixture
@@ -118,13 +141,10 @@ async def test_we_cannot_POST_to_root(mock_http_client):
assert resp.status == 405
-async def test_themes_api(hass, hass_ws_client):
+async def test_themes_api(hass, themes_ws_client):
"""Test that /api/themes returns correct data."""
- assert await async_setup_component(hass, "frontend", CONFIG_THEMES)
- client = await hass_ws_client(hass)
-
- await client.send_json({"id": 5, "type": "frontend/get_themes"})
- msg = await client.receive_json()
+ await themes_ws_client.send_json({"id": 5, "type": "frontend/get_themes"})
+ msg = await themes_ws_client.receive_json()
assert msg["result"]["default_theme"] == "default"
assert msg["result"]["default_dark_theme"] is None
@@ -135,8 +155,8 @@ async def test_themes_api(hass, hass_ws_client):
# safe mode
hass.config.safe_mode = True
- await client.send_json({"id": 6, "type": "frontend/get_themes"})
- msg = await client.receive_json()
+ await themes_ws_client.send_json({"id": 6, "type": "frontend/get_themes"})
+ msg = await themes_ws_client.receive_json()
assert msg["result"]["default_theme"] == "safe_mode"
assert msg["result"]["themes"] == {
@@ -144,9 +164,8 @@ async def test_themes_api(hass, hass_ws_client):
}
-async def test_themes_persist(hass, hass_ws_client, hass_storage):
+async def test_themes_persist(hass, hass_storage, hass_ws_client, ignore_frontend_deps):
"""Test that theme settings are restores after restart."""
-
hass_storage[THEMES_STORAGE_KEY] = {
"key": THEMES_STORAGE_KEY,
"version": 1,
@@ -157,26 +176,18 @@ async def test_themes_persist(hass, hass_ws_client, hass_storage):
}
assert await async_setup_component(hass, "frontend", CONFIG_THEMES)
- client = await hass_ws_client(hass)
+ themes_ws_client = await hass_ws_client(hass)
- await client.send_json({"id": 5, "type": "frontend/get_themes"})
- msg = await client.receive_json()
+ await themes_ws_client.send_json({"id": 5, "type": "frontend/get_themes"})
+ msg = await themes_ws_client.receive_json()
assert msg["result"]["default_theme"] == "happy"
assert msg["result"]["default_dark_theme"] == "dark"
-async def test_themes_save_storage(hass, hass_storage):
+async def test_themes_save_storage(hass, hass_storage, frontend_themes):
"""Test that theme settings are restores after restart."""
- hass_storage[THEMES_STORAGE_KEY] = {
- "key": THEMES_STORAGE_KEY,
- "version": 1,
- "data": {},
- }
-
- assert await async_setup_component(hass, "frontend", CONFIG_THEMES)
-
await hass.services.async_call(
DOMAIN, "set_theme", {"name": "happy"}, blocking=True
)
@@ -196,17 +207,14 @@ async def test_themes_save_storage(hass, hass_storage):
}
-async def test_themes_set_theme(hass, hass_ws_client):
+async def test_themes_set_theme(hass, themes_ws_client):
"""Test frontend.set_theme service."""
- assert await async_setup_component(hass, "frontend", CONFIG_THEMES)
- client = await hass_ws_client(hass)
-
await hass.services.async_call(
DOMAIN, "set_theme", {"name": "happy"}, blocking=True
)
- await client.send_json({"id": 5, "type": "frontend/get_themes"})
- msg = await client.receive_json()
+ await themes_ws_client.send_json({"id": 5, "type": "frontend/get_themes"})
+ msg = await themes_ws_client.receive_json()
assert msg["result"]["default_theme"] == "happy"
@@ -214,8 +222,8 @@ async def test_themes_set_theme(hass, hass_ws_client):
DOMAIN, "set_theme", {"name": "default"}, blocking=True
)
- await client.send_json({"id": 6, "type": "frontend/get_themes"})
- msg = await client.receive_json()
+ await themes_ws_client.send_json({"id": 6, "type": "frontend/get_themes"})
+ msg = await themes_ws_client.receive_json()
assert msg["result"]["default_theme"] == "default"
@@ -225,39 +233,35 @@ async def test_themes_set_theme(hass, hass_ws_client):
await hass.services.async_call(DOMAIN, "set_theme", {"name": "none"}, blocking=True)
- await client.send_json({"id": 7, "type": "frontend/get_themes"})
- msg = await client.receive_json()
+ await themes_ws_client.send_json({"id": 7, "type": "frontend/get_themes"})
+ msg = await themes_ws_client.receive_json()
assert msg["result"]["default_theme"] == "default"
-async def test_themes_set_theme_wrong_name(hass, hass_ws_client):
+async def test_themes_set_theme_wrong_name(hass, themes_ws_client):
"""Test frontend.set_theme service called with wrong name."""
- assert await async_setup_component(hass, "frontend", CONFIG_THEMES)
- client = await hass_ws_client(hass)
await hass.services.async_call(
DOMAIN, "set_theme", {"name": "wrong"}, blocking=True
)
- await client.send_json({"id": 5, "type": "frontend/get_themes"})
+ await themes_ws_client.send_json({"id": 5, "type": "frontend/get_themes"})
- msg = await client.receive_json()
+ msg = await themes_ws_client.receive_json()
assert msg["result"]["default_theme"] == "default"
-async def test_themes_set_dark_theme(hass, hass_ws_client):
+async def test_themes_set_dark_theme(hass, themes_ws_client):
"""Test frontend.set_theme service called with dark mode."""
- assert await async_setup_component(hass, "frontend", CONFIG_THEMES)
- client = await hass_ws_client(hass)
await hass.services.async_call(
DOMAIN, "set_theme", {"name": "dark", "mode": "dark"}, blocking=True
)
- await client.send_json({"id": 5, "type": "frontend/get_themes"})
- msg = await client.receive_json()
+ await themes_ws_client.send_json({"id": 5, "type": "frontend/get_themes"})
+ msg = await themes_ws_client.receive_json()
assert msg["result"]["default_dark_theme"] == "dark"
@@ -265,8 +269,8 @@ async def test_themes_set_dark_theme(hass, hass_ws_client):
DOMAIN, "set_theme", {"name": "default", "mode": "dark"}, blocking=True
)
- await client.send_json({"id": 6, "type": "frontend/get_themes"})
- msg = await client.receive_json()
+ await themes_ws_client.send_json({"id": 6, "type": "frontend/get_themes"})
+ msg = await themes_ws_client.receive_json()
assert msg["result"]["default_dark_theme"] == "default"
@@ -274,32 +278,27 @@ async def test_themes_set_dark_theme(hass, hass_ws_client):
DOMAIN, "set_theme", {"name": "none", "mode": "dark"}, blocking=True
)
- await client.send_json({"id": 7, "type": "frontend/get_themes"})
- msg = await client.receive_json()
+ await themes_ws_client.send_json({"id": 7, "type": "frontend/get_themes"})
+ msg = await themes_ws_client.receive_json()
assert msg["result"]["default_dark_theme"] is None
-async def test_themes_set_dark_theme_wrong_name(hass, hass_ws_client):
+async def test_themes_set_dark_theme_wrong_name(hass, frontend, themes_ws_client):
"""Test frontend.set_theme service called with mode dark and wrong name."""
- assert await async_setup_component(hass, "frontend", CONFIG_THEMES)
- client = await hass_ws_client(hass)
-
await hass.services.async_call(
DOMAIN, "set_theme", {"name": "wrong", "mode": "dark"}, blocking=True
)
- await client.send_json({"id": 5, "type": "frontend/get_themes"})
+ await themes_ws_client.send_json({"id": 5, "type": "frontend/get_themes"})
- msg = await client.receive_json()
+ msg = await themes_ws_client.receive_json()
assert msg["result"]["default_dark_theme"] is None
-async def test_themes_reload_themes(hass, hass_ws_client):
+async def test_themes_reload_themes(hass, frontend, themes_ws_client):
"""Test frontend.reload_themes service."""
- assert await async_setup_component(hass, "frontend", CONFIG_THEMES)
- client = await hass_ws_client(hass)
with patch(
"homeassistant.components.frontend.async_hass_config_yaml",
@@ -310,22 +309,19 @@ async def test_themes_reload_themes(hass, hass_ws_client):
)
await hass.services.async_call(DOMAIN, "reload_themes", blocking=True)
- await client.send_json({"id": 5, "type": "frontend/get_themes"})
+ await themes_ws_client.send_json({"id": 5, "type": "frontend/get_themes"})
- msg = await client.receive_json()
+ msg = await themes_ws_client.receive_json()
assert msg["result"]["themes"] == {"sad": {"primary-color": "blue"}}
assert msg["result"]["default_theme"] == "default"
-async def test_missing_themes(hass, hass_ws_client):
+async def test_missing_themes(hass, ws_client):
"""Test that themes API works when themes are not defined."""
- await async_setup_component(hass, "frontend", {})
+ await ws_client.send_json({"id": 5, "type": "frontend/get_themes"})
- client = await hass_ws_client(hass)
- await client.send_json({"id": 5, "type": "frontend/get_themes"})
-
- msg = await client.receive_json()
+ msg = await ws_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == TYPE_RESULT
@@ -372,10 +368,10 @@ async def test_get_panels(hass, hass_ws_client, mock_http_client):
assert len(events) == 2
-async def test_get_panels_non_admin(hass, hass_ws_client, hass_admin_user):
+async def test_get_panels_non_admin(hass, ws_client, hass_admin_user):
"""Test get_panels command."""
hass_admin_user.groups = []
- await async_setup_component(hass, "frontend", {})
+
hass.components.frontend.async_register_built_in_panel(
"map", "Map", "mdi:tooltip-account", require_admin=True
)
@@ -383,10 +379,9 @@ async def test_get_panels_non_admin(hass, hass_ws_client, hass_admin_user):
"history", "History", "mdi:history"
)
- client = await hass_ws_client(hass)
- await client.send_json({"id": 5, "type": "get_panels"})
+ await ws_client.send_json({"id": 5, "type": "get_panels"})
- msg = await client.receive_json()
+ msg = await ws_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == TYPE_RESULT
@@ -395,18 +390,15 @@ async def test_get_panels_non_admin(hass, hass_ws_client, hass_admin_user):
assert "map" not in msg["result"]
-async def test_get_translations(hass, hass_ws_client):
+async def test_get_translations(hass, ws_client):
"""Test get_translations command."""
- await async_setup_component(hass, "frontend", {})
- client = await hass_ws_client(hass)
-
with patch(
"homeassistant.components.frontend.async_get_translations",
side_effect=lambda hass, lang, category, integration, config_flow: {
"lang": lang
},
):
- await client.send_json(
+ await ws_client.send_json(
{
"id": 5,
"type": "frontend/get_translations",
@@ -414,7 +406,7 @@ async def test_get_translations(hass, hass_ws_client):
"category": "lang",
}
)
- msg = await client.receive_json()
+ msg = await ws_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == TYPE_RESULT
@@ -422,16 +414,16 @@ async def test_get_translations(hass, hass_ws_client):
assert msg["result"] == {"resources": {"lang": "nl"}}
-async def test_auth_load(mock_http_client, mock_onboarded):
+async def test_auth_load(hass):
"""Test auth component loaded by default."""
- resp = await mock_http_client.get("/auth/providers")
- assert resp.status == 200
+ frontend = await async_get_integration(hass, "frontend")
+ assert "auth" in frontend.dependencies
-async def test_onboarding_load(mock_http_client):
+async def test_onboarding_load(hass):
"""Test onboarding component loaded by default."""
- resp = await mock_http_client.get("/api/onboarding")
- assert resp.status == 200
+ frontend = await async_get_integration(hass, "frontend")
+ assert "onboarding" in frontend.dependencies
async def test_auth_authorize(mock_http_client):
@@ -457,7 +449,7 @@ async def test_auth_authorize(mock_http_client):
assert "public" in resp.headers.get("cache-control")
-async def test_get_version(hass, hass_ws_client):
+async def test_get_version(hass, ws_client):
"""Test get_version command."""
frontend = await async_get_integration(hass, "frontend")
cur_version = next(
@@ -466,11 +458,8 @@ async def test_get_version(hass, hass_ws_client):
if req.startswith("home-assistant-frontend==")
)
- await async_setup_component(hass, "frontend", {})
- client = await hass_ws_client(hass)
-
- await client.send_json({"id": 5, "type": "frontend/get_version"})
- msg = await client.receive_json()
+ await ws_client.send_json({"id": 5, "type": "frontend/get_version"})
+ msg = await ws_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == TYPE_RESULT
diff --git a/tests/components/gdacs/test_geo_location.py b/tests/components/gdacs/test_geo_location.py
index 7dc23eaa5d8c2e..d93db73aa79e85 100644
--- a/tests/components/gdacs/test_geo_location.py
+++ b/tests/components/gdacs/test_geo_location.py
@@ -29,7 +29,7 @@
EVENT_HOMEASSISTANT_START,
LENGTH_KILOMETERS,
)
-from homeassistant.helpers.entity_registry import async_get_registry
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from homeassistant.util.unit_system import IMPERIAL_SYSTEM
@@ -99,7 +99,7 @@ async def test_setup(hass, legacy_patchable_time):
all_states = hass.states.async_all()
# 3 geolocation and 1 sensor entities
assert len(all_states) == 4
- entity_registry = await async_get_registry(hass)
+ entity_registry = er.async_get(hass)
assert len(entity_registry.entities) == 4
state = hass.states.get("geo_location.drought_name_1")
diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py
index 9a147995541074..deb8049da33c6d 100644
--- a/tests/components/generic/test_camera.py
+++ b/tests/components/generic/test_camera.py
@@ -3,6 +3,9 @@
from os import path
from unittest.mock import patch
+import httpx
+import respx
+
from homeassistant import config as hass_config
from homeassistant.components.generic import DOMAIN
from homeassistant.components.websocket_api.const import TYPE_RESULT
@@ -14,9 +17,10 @@
from homeassistant.setup import async_setup_component
-async def test_fetching_url(aioclient_mock, hass, hass_client):
+@respx.mock
+async def test_fetching_url(hass, hass_client):
"""Test that it fetches the given url."""
- aioclient_mock.get("http://example.com", text="hello world")
+ respx.get("http://example.com").respond(text="hello world")
await async_setup_component(
hass,
@@ -38,12 +42,12 @@ async def test_fetching_url(aioclient_mock, hass, hass_client):
resp = await client.get("/api/camera_proxy/camera.config_test")
assert resp.status == 200
- assert aioclient_mock.call_count == 1
+ assert respx.calls.call_count == 1
body = await resp.text()
assert body == "hello world"
resp = await client.get("/api/camera_proxy/camera.config_test")
- assert aioclient_mock.call_count == 2
+ assert respx.calls.call_count == 2
async def test_fetching_without_verify_ssl(aioclient_mock, hass, hass_client):
@@ -100,12 +104,13 @@ async def test_fetching_url_with_verify_ssl(aioclient_mock, hass, hass_client):
assert resp.status == 200
-async def test_limit_refetch(aioclient_mock, hass, hass_client):
+@respx.mock
+async def test_limit_refetch(hass, hass_client):
"""Test that it fetches the given url."""
- aioclient_mock.get("http://example.com/5a", text="hello world")
- aioclient_mock.get("http://example.com/10a", text="hello world")
- aioclient_mock.get("http://example.com/15a", text="hello planet")
- aioclient_mock.get("http://example.com/20a", status=HTTP_NOT_FOUND)
+ respx.get("http://example.com/5a").respond(text="hello world")
+ respx.get("http://example.com/10a").respond(text="hello world")
+ respx.get("http://example.com/15a").respond(text="hello planet")
+ respx.get("http://example.com/20a").respond(status_code=HTTP_NOT_FOUND)
await async_setup_component(
hass,
@@ -129,19 +134,19 @@ async def test_limit_refetch(aioclient_mock, hass, hass_client):
with patch("async_timeout.timeout", side_effect=asyncio.TimeoutError()):
resp = await client.get("/api/camera_proxy/camera.config_test")
- assert aioclient_mock.call_count == 0
+ assert respx.calls.call_count == 0
assert resp.status == HTTP_INTERNAL_SERVER_ERROR
hass.states.async_set("sensor.temp", "10")
resp = await client.get("/api/camera_proxy/camera.config_test")
- assert aioclient_mock.call_count == 1
+ assert respx.calls.call_count == 1
assert resp.status == 200
body = await resp.text()
assert body == "hello world"
resp = await client.get("/api/camera_proxy/camera.config_test")
- assert aioclient_mock.call_count == 1
+ assert respx.calls.call_count == 1
assert resp.status == 200
body = await resp.text()
assert body == "hello world"
@@ -150,7 +155,7 @@ async def test_limit_refetch(aioclient_mock, hass, hass_client):
# Url change = fetch new image
resp = await client.get("/api/camera_proxy/camera.config_test")
- assert aioclient_mock.call_count == 2
+ assert respx.calls.call_count == 2
assert resp.status == 200
body = await resp.text()
assert body == "hello planet"
@@ -158,7 +163,7 @@ async def test_limit_refetch(aioclient_mock, hass, hass_client):
# Cause a template render error
hass.states.async_remove("sensor.temp")
resp = await client.get("/api/camera_proxy/camera.config_test")
- assert aioclient_mock.call_count == 2
+ assert respx.calls.call_count == 2
assert resp.status == 200
body = await resp.text()
assert body == "hello planet"
@@ -176,17 +181,18 @@ async def test_stream_source(aioclient_mock, hass, hass_client, hass_ws_client):
"still_image_url": "https://example.com",
"stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}',
"limit_refetch_to_url_change": True,
- }
+ },
},
)
+ assert await async_setup_component(hass, "stream", {})
await hass.async_block_till_done()
hass.states.async_set("sensor.temp", "5")
with patch(
- "homeassistant.components.camera.request_stream",
+ "homeassistant.components.camera.Stream.endpoint_url",
return_value="http://home.assistant/playlist.m3u8",
- ) as mock_request_stream:
+ ) as mock_stream_url:
# Request playlist through WebSocket
client = await hass_ws_client(hass)
@@ -196,25 +202,47 @@ async def test_stream_source(aioclient_mock, hass, hass_client, hass_ws_client):
msg = await client.receive_json()
# Assert WebSocket response
- assert mock_request_stream.call_count == 1
- assert mock_request_stream.call_args[0][1] == "http://example.com/5a"
+ assert mock_stream_url.call_count == 1
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]
assert msg["result"]["url"][-13:] == "playlist.m3u8"
- # Cause a template render error
- hass.states.async_remove("sensor.temp")
+
+async def test_stream_source_error(aioclient_mock, hass, hass_client, hass_ws_client):
+ """Test that the stream source has an error."""
+ assert await async_setup_component(
+ hass,
+ "camera",
+ {
+ "camera": {
+ "name": "config_test",
+ "platform": "generic",
+ "still_image_url": "https://example.com",
+ # Does not exist
+ "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}',
+ "limit_refetch_to_url_change": True,
+ },
+ },
+ )
+ assert await async_setup_component(hass, "stream", {})
+ await hass.async_block_till_done()
+
+ with patch(
+ "homeassistant.components.camera.Stream.endpoint_url",
+ return_value="http://home.assistant/playlist.m3u8",
+ ) as mock_stream_url:
+ # Request playlist through WebSocket
+ client = await hass_ws_client(hass)
await client.send_json(
- {"id": 2, "type": "camera/stream", "entity_id": "camera.config_test"}
+ {"id": 1, "type": "camera/stream", "entity_id": "camera.config_test"}
)
msg = await client.receive_json()
- # Assert that no new call to the stream request should have been made
- assert mock_request_stream.call_count == 1
- # Assert the websocket error message
- assert msg["id"] == 2
+ # Assert WebSocket response
+ assert mock_stream_url.call_count == 0
+ assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"] is False
assert msg["error"] == {
@@ -223,6 +251,28 @@ async def test_stream_source(aioclient_mock, hass, hass_client, hass_ws_client):
}
+async def test_setup_alternative_options(hass, hass_ws_client):
+ """Test that the stream source is setup with different config options."""
+ assert await async_setup_component(
+ hass,
+ "camera",
+ {
+ "camera": {
+ "name": "config_test",
+ "platform": "generic",
+ "still_image_url": "https://example.com",
+ "authentication": "digest",
+ "username": "user",
+ "password": "pass",
+ "stream_source": "rtsp://example.com:554/rtsp/",
+ "rtsp_transport": "udp",
+ },
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.data["camera"].get_entity("camera.config_test")
+
+
async def test_no_stream_source(aioclient_mock, hass, hass_client, hass_ws_client):
"""Test a stream request without stream source option set."""
assert await async_setup_component(
@@ -240,7 +290,7 @@ async def test_no_stream_source(aioclient_mock, hass, hass_client, hass_ws_clien
await hass.async_block_till_done()
with patch(
- "homeassistant.components.camera.request_stream",
+ "homeassistant.components.camera.Stream.endpoint_url",
return_value="http://home.assistant/playlist.m3u8",
) as mock_request_stream:
# Request playlist through WebSocket
@@ -262,11 +312,12 @@ async def test_no_stream_source(aioclient_mock, hass, hass_client, hass_ws_clien
}
-async def test_camera_content_type(aioclient_mock, hass, hass_client):
+@respx.mock
+async def test_camera_content_type(hass, hass_client):
"""Test generic camera with custom content_type."""
svg_image = ""
urlsvg = "https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg"
- aioclient_mock.get(urlsvg, text=svg_image)
+ respx.get(urlsvg).respond(text=svg_image)
cam_config_svg = {
"name": "config_test_svg",
@@ -286,23 +337,24 @@ async def test_camera_content_type(aioclient_mock, hass, hass_client):
client = await hass_client()
resp_1 = await client.get("/api/camera_proxy/camera.config_test_svg")
- assert aioclient_mock.call_count == 1
+ assert respx.calls.call_count == 1
assert resp_1.status == 200
assert resp_1.content_type == "image/svg+xml"
body = await resp_1.text()
assert body == svg_image
resp_2 = await client.get("/api/camera_proxy/camera.config_test_jpg")
- assert aioclient_mock.call_count == 2
+ assert respx.calls.call_count == 2
assert resp_2.status == 200
assert resp_2.content_type == "image/jpeg"
body = await resp_2.text()
assert body == svg_image
-async def test_reloading(aioclient_mock, hass, hass_client):
+@respx.mock
+async def test_reloading(hass, hass_client):
"""Test we can cleanly reload."""
- aioclient_mock.get("http://example.com", text="hello world")
+ respx.get("http://example.com").respond(text="hello world")
await async_setup_component(
hass,
@@ -324,7 +376,7 @@ async def test_reloading(aioclient_mock, hass, hass_client):
resp = await client.get("/api/camera_proxy/camera.config_test")
assert resp.status == 200
- assert aioclient_mock.call_count == 1
+ assert respx.calls.call_count == 1
body = await resp.text()
assert body == "hello world"
@@ -351,10 +403,61 @@ async def test_reloading(aioclient_mock, hass, hass_client):
resp = await client.get("/api/camera_proxy/camera.reload")
assert resp.status == 200
- assert aioclient_mock.call_count == 2
+ assert respx.calls.call_count == 2
body = await resp.text()
assert body == "hello world"
+@respx.mock
+async def test_timeout_cancelled(hass, hass_client):
+ """Test that timeouts and cancellations return last image."""
+
+ respx.get("http://example.com").respond(text="hello world")
+
+ await async_setup_component(
+ hass,
+ "camera",
+ {
+ "camera": {
+ "name": "config_test",
+ "platform": "generic",
+ "still_image_url": "http://example.com",
+ "username": "user",
+ "password": "pass",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ client = await hass_client()
+
+ resp = await client.get("/api/camera_proxy/camera.config_test")
+
+ assert resp.status == 200
+ assert respx.calls.call_count == 1
+ assert await resp.text() == "hello world"
+
+ respx.get("http://example.com").respond(text="not hello world")
+
+ with patch(
+ "homeassistant.components.generic.camera.GenericCamera._async_camera_image",
+ side_effect=asyncio.CancelledError(),
+ ):
+ resp = await client.get("/api/camera_proxy/camera.config_test")
+ assert respx.calls.call_count == 1
+ assert resp.status == 500
+
+ respx.get("http://example.com").side_effect = [
+ httpx.RequestError,
+ httpx.TimeoutException,
+ ]
+
+ for total_calls in range(2, 4):
+ resp = await client.get("/api/camera_proxy/camera.config_test")
+ assert respx.calls.call_count == total_calls
+ assert resp.status == 200
+ assert await resp.text() == "hello world"
+
+
def _get_fixtures_base_path():
return path.dirname(path.dirname(path.dirname(__file__)))
diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py
index 201ed0130ffe84..f5a27ac8b97212 100644
--- a/tests/components/generic_thermostat/test_climate.py
+++ b/tests/components/generic_thermostat/test_climate.py
@@ -35,6 +35,7 @@
)
import homeassistant.core as ha
from homeassistant.core import DOMAIN as HASS_DOMAIN, CoreState, State, callback
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util.unit_system import METRIC_SYSTEM
@@ -115,13 +116,13 @@ async def test_heater_input_boolean(hass, setup_comp_1):
)
await hass.async_block_till_done()
- assert STATE_OFF == hass.states.get(heater_switch).state
+ assert hass.states.get(heater_switch).state == STATE_OFF
_setup_sensor(hass, 18)
await hass.async_block_till_done()
await common.async_set_temperature(hass, 23)
- assert STATE_ON == hass.states.get(heater_switch).state
+ assert hass.states.get(heater_switch).state == STATE_ON
async def test_heater_switch(hass, setup_comp_1):
@@ -150,13 +151,40 @@ async def test_heater_switch(hass, setup_comp_1):
)
await hass.async_block_till_done()
- assert STATE_OFF == hass.states.get(heater_switch).state
+ assert hass.states.get(heater_switch).state == STATE_OFF
_setup_sensor(hass, 18)
await common.async_set_temperature(hass, 23)
await hass.async_block_till_done()
- assert STATE_ON == hass.states.get(heater_switch).state
+ assert hass.states.get(heater_switch).state == STATE_ON
+
+
+async def test_unique_id(hass, setup_comp_1):
+ """Test heater switching input_boolean."""
+ unique_id = "some_unique_id"
+ _setup_sensor(hass, 18)
+ _setup_switch(hass, True)
+ assert await async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ "climate": {
+ "platform": "generic_thermostat",
+ "name": "test",
+ "heater": ENT_SWITCH,
+ "target_sensor": ENT_SENSOR,
+ "unique_id": unique_id,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ entity_registry = er.async_get(hass)
+
+ entry = entity_registry.async_get(ENTITY)
+ assert entry
+ assert entry.unique_id == unique_id
def _setup_sensor(hass, temp):
@@ -206,7 +234,7 @@ async def test_setup_defaults_to_unknown(hass):
},
)
await hass.async_block_till_done()
- assert HVAC_MODE_OFF == hass.states.get(ENTITY).state
+ assert hass.states.get(ENTITY).state == HVAC_MODE_OFF
async def test_setup_gets_current_temp_from_sensor(hass):
@@ -236,27 +264,27 @@ async def test_setup_gets_current_temp_from_sensor(hass):
async def test_default_setup_params(hass, setup_comp_2):
"""Test the setup with default parameters."""
state = hass.states.get(ENTITY)
- assert 7 == state.attributes.get("min_temp")
- assert 35 == state.attributes.get("max_temp")
- assert 7 == state.attributes.get("temperature")
+ assert state.attributes.get("min_temp") == 7
+ assert state.attributes.get("max_temp") == 35
+ assert state.attributes.get("temperature") == 7
async def test_get_hvac_modes(hass, setup_comp_2):
"""Test that the operation list returns the correct modes."""
state = hass.states.get(ENTITY)
modes = state.attributes.get("hvac_modes")
- assert [HVAC_MODE_HEAT, HVAC_MODE_OFF] == modes
+ assert modes == [HVAC_MODE_HEAT, HVAC_MODE_OFF]
async def test_set_target_temp(hass, setup_comp_2):
"""Test the setting of the target temperature."""
await common.async_set_temperature(hass, 30)
state = hass.states.get(ENTITY)
- assert 30.0 == state.attributes.get("temperature")
+ assert state.attributes.get("temperature") == 30.0
with pytest.raises(vol.Invalid):
await common.async_set_temperature(hass, None)
state = hass.states.get(ENTITY)
- assert 30.0 == state.attributes.get("temperature")
+ assert state.attributes.get("temperature") == 30.0
async def test_set_away_mode(hass, setup_comp_2):
@@ -264,7 +292,7 @@ async def test_set_away_mode(hass, setup_comp_2):
await common.async_set_temperature(hass, 23)
await common.async_set_preset_mode(hass, PRESET_AWAY)
state = hass.states.get(ENTITY)
- assert 16 == state.attributes.get("temperature")
+ assert state.attributes.get("temperature") == 16
async def test_set_away_mode_and_restore_prev_temp(hass, setup_comp_2):
@@ -275,10 +303,10 @@ async def test_set_away_mode_and_restore_prev_temp(hass, setup_comp_2):
await common.async_set_temperature(hass, 23)
await common.async_set_preset_mode(hass, PRESET_AWAY)
state = hass.states.get(ENTITY)
- assert 16 == state.attributes.get("temperature")
+ assert state.attributes.get("temperature") == 16
await common.async_set_preset_mode(hass, PRESET_NONE)
state = hass.states.get(ENTITY)
- assert 23 == state.attributes.get("temperature")
+ assert state.attributes.get("temperature") == 23
async def test_set_away_mode_twice_and_restore_prev_temp(hass, setup_comp_2):
@@ -290,10 +318,10 @@ async def test_set_away_mode_twice_and_restore_prev_temp(hass, setup_comp_2):
await common.async_set_preset_mode(hass, PRESET_AWAY)
await common.async_set_preset_mode(hass, PRESET_AWAY)
state = hass.states.get(ENTITY)
- assert 16 == state.attributes.get("temperature")
+ assert state.attributes.get("temperature") == 16
await common.async_set_preset_mode(hass, PRESET_NONE)
state = hass.states.get(ENTITY)
- assert 23 == state.attributes.get("temperature")
+ assert state.attributes.get("temperature") == 23
async def test_sensor_bad_value(hass, setup_comp_2):
@@ -303,9 +331,18 @@ async def test_sensor_bad_value(hass, setup_comp_2):
_setup_sensor(hass, None)
await hass.async_block_till_done()
+ state = hass.states.get(ENTITY)
+ assert state.attributes.get("current_temperature") == temp
+
+ _setup_sensor(hass, "inf")
+ await hass.async_block_till_done()
+ state = hass.states.get(ENTITY)
+ assert state.attributes.get("current_temperature") == temp
+ _setup_sensor(hass, "nan")
+ await hass.async_block_till_done()
state = hass.states.get(ENTITY)
- assert temp == state.attributes.get("current_temperature")
+ assert state.attributes.get("current_temperature") == temp
async def test_sensor_unknown(hass):
@@ -354,11 +391,11 @@ async def test_set_target_temp_heater_on(hass, setup_comp_2):
_setup_sensor(hass, 25)
await hass.async_block_till_done()
await common.async_set_temperature(hass, 30)
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_ON == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_ON
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_set_target_temp_heater_off(hass, setup_comp_2):
@@ -367,11 +404,11 @@ async def test_set_target_temp_heater_off(hass, setup_comp_2):
_setup_sensor(hass, 30)
await hass.async_block_till_done()
await common.async_set_temperature(hass, 25)
- assert 2 == len(calls)
+ assert len(calls) == 2
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_OFF == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_OFF
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_temp_change_heater_on_within_tolerance(hass, setup_comp_2):
@@ -380,7 +417,7 @@ async def test_temp_change_heater_on_within_tolerance(hass, setup_comp_2):
await common.async_set_temperature(hass, 30)
_setup_sensor(hass, 29)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async def test_temp_change_heater_on_outside_tolerance(hass, setup_comp_2):
@@ -389,11 +426,11 @@ async def test_temp_change_heater_on_outside_tolerance(hass, setup_comp_2):
await common.async_set_temperature(hass, 30)
_setup_sensor(hass, 27)
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_ON == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_ON
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_temp_change_heater_off_within_tolerance(hass, setup_comp_2):
@@ -402,7 +439,7 @@ async def test_temp_change_heater_off_within_tolerance(hass, setup_comp_2):
await common.async_set_temperature(hass, 30)
_setup_sensor(hass, 33)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async def test_temp_change_heater_off_outside_tolerance(hass, setup_comp_2):
@@ -411,11 +448,11 @@ async def test_temp_change_heater_off_outside_tolerance(hass, setup_comp_2):
await common.async_set_temperature(hass, 30)
_setup_sensor(hass, 35)
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_OFF == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_OFF
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_running_when_hvac_mode_is_off(hass, setup_comp_2):
@@ -423,11 +460,11 @@ async def test_running_when_hvac_mode_is_off(hass, setup_comp_2):
calls = _setup_switch(hass, True)
await common.async_set_temperature(hass, 30)
await common.async_set_hvac_mode(hass, HVAC_MODE_OFF)
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_OFF == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_OFF
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_no_state_change_when_hvac_mode_off(hass, setup_comp_2):
@@ -437,7 +474,7 @@ async def test_no_state_change_when_hvac_mode_off(hass, setup_comp_2):
await common.async_set_hvac_mode(hass, HVAC_MODE_OFF)
_setup_sensor(hass, 25)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async def test_hvac_mode_heat(hass, setup_comp_2):
@@ -451,11 +488,11 @@ async def test_hvac_mode_heat(hass, setup_comp_2):
await hass.async_block_till_done()
calls = _setup_switch(hass, False)
await common.async_set_hvac_mode(hass, HVAC_MODE_HEAT)
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_ON == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_ON
+ assert call.data["entity_id"] == ENT_SWITCH
def _setup_switch(hass, is_on):
@@ -504,11 +541,11 @@ async def test_set_target_temp_ac_off(hass, setup_comp_3):
_setup_sensor(hass, 25)
await hass.async_block_till_done()
await common.async_set_temperature(hass, 30)
- assert 2 == len(calls)
+ assert len(calls) == 2
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_OFF == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_OFF
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_turn_away_mode_on_cooling(hass, setup_comp_3):
@@ -519,7 +556,7 @@ async def test_turn_away_mode_on_cooling(hass, setup_comp_3):
await common.async_set_temperature(hass, 19)
await common.async_set_preset_mode(hass, PRESET_AWAY)
state = hass.states.get(ENTITY)
- assert 30 == state.attributes.get("temperature")
+ assert state.attributes.get("temperature") == 30
async def test_hvac_mode_cool(hass, setup_comp_3):
@@ -533,11 +570,11 @@ async def test_hvac_mode_cool(hass, setup_comp_3):
await hass.async_block_till_done()
calls = _setup_switch(hass, False)
await common.async_set_hvac_mode(hass, HVAC_MODE_COOL)
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_ON == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_ON
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_set_target_temp_ac_on(hass, setup_comp_3):
@@ -546,11 +583,11 @@ async def test_set_target_temp_ac_on(hass, setup_comp_3):
_setup_sensor(hass, 30)
await hass.async_block_till_done()
await common.async_set_temperature(hass, 25)
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_ON == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_ON
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_temp_change_ac_off_within_tolerance(hass, setup_comp_3):
@@ -559,7 +596,7 @@ async def test_temp_change_ac_off_within_tolerance(hass, setup_comp_3):
await common.async_set_temperature(hass, 30)
_setup_sensor(hass, 29.8)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async def test_set_temp_change_ac_off_outside_tolerance(hass, setup_comp_3):
@@ -568,11 +605,11 @@ async def test_set_temp_change_ac_off_outside_tolerance(hass, setup_comp_3):
await common.async_set_temperature(hass, 30)
_setup_sensor(hass, 27)
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_OFF == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_OFF
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_temp_change_ac_on_within_tolerance(hass, setup_comp_3):
@@ -581,7 +618,7 @@ async def test_temp_change_ac_on_within_tolerance(hass, setup_comp_3):
await common.async_set_temperature(hass, 25)
_setup_sensor(hass, 25.2)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async def test_temp_change_ac_on_outside_tolerance(hass, setup_comp_3):
@@ -590,11 +627,11 @@ async def test_temp_change_ac_on_outside_tolerance(hass, setup_comp_3):
await common.async_set_temperature(hass, 25)
_setup_sensor(hass, 30)
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_ON == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_ON
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_running_when_operating_mode_is_off_2(hass, setup_comp_3):
@@ -602,11 +639,11 @@ async def test_running_when_operating_mode_is_off_2(hass, setup_comp_3):
calls = _setup_switch(hass, True)
await common.async_set_temperature(hass, 30)
await common.async_set_hvac_mode(hass, HVAC_MODE_OFF)
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_OFF == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_OFF
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_no_state_change_when_operation_mode_off_2(hass, setup_comp_3):
@@ -616,7 +653,7 @@ async def test_no_state_change_when_operation_mode_off_2(hass, setup_comp_3):
await common.async_set_hvac_mode(hass, HVAC_MODE_OFF)
_setup_sensor(hass, 35)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
@pytest.fixture
@@ -649,7 +686,7 @@ async def test_temp_change_ac_trigger_on_not_long_enough(hass, setup_comp_4):
await common.async_set_temperature(hass, 25)
_setup_sensor(hass, 30)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async def test_temp_change_ac_trigger_on_long_enough(hass, setup_comp_4):
@@ -664,11 +701,11 @@ async def test_temp_change_ac_trigger_on_long_enough(hass, setup_comp_4):
await common.async_set_temperature(hass, 25)
_setup_sensor(hass, 30)
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_ON == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_ON
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_temp_change_ac_trigger_off_not_long_enough(hass, setup_comp_4):
@@ -677,7 +714,7 @@ async def test_temp_change_ac_trigger_off_not_long_enough(hass, setup_comp_4):
await common.async_set_temperature(hass, 30)
_setup_sensor(hass, 25)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async def test_temp_change_ac_trigger_off_long_enough(hass, setup_comp_4):
@@ -692,11 +729,11 @@ async def test_temp_change_ac_trigger_off_long_enough(hass, setup_comp_4):
await common.async_set_temperature(hass, 30)
_setup_sensor(hass, 25)
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_OFF == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_OFF
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_mode_change_ac_trigger_off_not_long_enough(hass, setup_comp_4):
@@ -705,13 +742,13 @@ async def test_mode_change_ac_trigger_off_not_long_enough(hass, setup_comp_4):
await common.async_set_temperature(hass, 30)
_setup_sensor(hass, 25)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
await common.async_set_hvac_mode(hass, HVAC_MODE_OFF)
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert "homeassistant" == call.domain
- assert SERVICE_TURN_OFF == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == "homeassistant"
+ assert call.service == SERVICE_TURN_OFF
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_mode_change_ac_trigger_on_not_long_enough(hass, setup_comp_4):
@@ -720,13 +757,13 @@ async def test_mode_change_ac_trigger_on_not_long_enough(hass, setup_comp_4):
await common.async_set_temperature(hass, 25)
_setup_sensor(hass, 30)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
await common.async_set_hvac_mode(hass, HVAC_MODE_HEAT)
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert "homeassistant" == call.domain
- assert SERVICE_TURN_ON == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == "homeassistant"
+ assert call.service == SERVICE_TURN_ON
+ assert call.data["entity_id"] == ENT_SWITCH
@pytest.fixture
@@ -759,7 +796,7 @@ async def test_temp_change_ac_trigger_on_not_long_enough_2(hass, setup_comp_5):
await common.async_set_temperature(hass, 25)
_setup_sensor(hass, 30)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async def test_temp_change_ac_trigger_on_long_enough_2(hass, setup_comp_5):
@@ -774,11 +811,11 @@ async def test_temp_change_ac_trigger_on_long_enough_2(hass, setup_comp_5):
await common.async_set_temperature(hass, 25)
_setup_sensor(hass, 30)
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_ON == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_ON
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_temp_change_ac_trigger_off_not_long_enough_2(hass, setup_comp_5):
@@ -787,7 +824,7 @@ async def test_temp_change_ac_trigger_off_not_long_enough_2(hass, setup_comp_5):
await common.async_set_temperature(hass, 30)
_setup_sensor(hass, 25)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async def test_temp_change_ac_trigger_off_long_enough_2(hass, setup_comp_5):
@@ -802,11 +839,11 @@ async def test_temp_change_ac_trigger_off_long_enough_2(hass, setup_comp_5):
await common.async_set_temperature(hass, 30)
_setup_sensor(hass, 25)
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_OFF == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_OFF
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_mode_change_ac_trigger_off_not_long_enough_2(hass, setup_comp_5):
@@ -815,13 +852,13 @@ async def test_mode_change_ac_trigger_off_not_long_enough_2(hass, setup_comp_5):
await common.async_set_temperature(hass, 30)
_setup_sensor(hass, 25)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
await common.async_set_hvac_mode(hass, HVAC_MODE_OFF)
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert "homeassistant" == call.domain
- assert SERVICE_TURN_OFF == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == "homeassistant"
+ assert call.service == SERVICE_TURN_OFF
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_mode_change_ac_trigger_on_not_long_enough_2(hass, setup_comp_5):
@@ -830,13 +867,13 @@ async def test_mode_change_ac_trigger_on_not_long_enough_2(hass, setup_comp_5):
await common.async_set_temperature(hass, 25)
_setup_sensor(hass, 30)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
await common.async_set_hvac_mode(hass, HVAC_MODE_HEAT)
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert "homeassistant" == call.domain
- assert SERVICE_TURN_ON == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == "homeassistant"
+ assert call.service == SERVICE_TURN_ON
+ assert call.data["entity_id"] == ENT_SWITCH
@pytest.fixture
@@ -868,7 +905,7 @@ async def test_temp_change_heater_trigger_off_not_long_enough(hass, setup_comp_6
await common.async_set_temperature(hass, 25)
_setup_sensor(hass, 30)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async def test_temp_change_heater_trigger_on_not_long_enough(hass, setup_comp_6):
@@ -877,7 +914,7 @@ async def test_temp_change_heater_trigger_on_not_long_enough(hass, setup_comp_6)
await common.async_set_temperature(hass, 30)
_setup_sensor(hass, 25)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async def test_temp_change_heater_trigger_on_long_enough(hass, setup_comp_6):
@@ -892,11 +929,11 @@ async def test_temp_change_heater_trigger_on_long_enough(hass, setup_comp_6):
await common.async_set_temperature(hass, 30)
_setup_sensor(hass, 25)
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_ON == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_ON
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_temp_change_heater_trigger_off_long_enough(hass, setup_comp_6):
@@ -911,11 +948,11 @@ async def test_temp_change_heater_trigger_off_long_enough(hass, setup_comp_6):
await common.async_set_temperature(hass, 25)
_setup_sensor(hass, 30)
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_OFF == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_OFF
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_mode_change_heater_trigger_off_not_long_enough(hass, setup_comp_6):
@@ -924,13 +961,13 @@ async def test_mode_change_heater_trigger_off_not_long_enough(hass, setup_comp_6
await common.async_set_temperature(hass, 25)
_setup_sensor(hass, 30)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
await common.async_set_hvac_mode(hass, HVAC_MODE_OFF)
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert "homeassistant" == call.domain
- assert SERVICE_TURN_OFF == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == "homeassistant"
+ assert call.service == SERVICE_TURN_OFF
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_mode_change_heater_trigger_on_not_long_enough(hass, setup_comp_6):
@@ -939,13 +976,13 @@ async def test_mode_change_heater_trigger_on_not_long_enough(hass, setup_comp_6)
await common.async_set_temperature(hass, 30)
_setup_sensor(hass, 25)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
await common.async_set_hvac_mode(hass, HVAC_MODE_HEAT)
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert "homeassistant" == call.domain
- assert SERVICE_TURN_ON == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == "homeassistant"
+ assert call.service == SERVICE_TURN_ON
+ assert call.data["entity_id"] == ENT_SWITCH
@pytest.fixture
@@ -985,17 +1022,17 @@ async def test_temp_change_ac_trigger_on_long_enough_3(hass, setup_comp_7):
test_time = datetime.datetime.now(pytz.UTC)
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=5))
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=10))
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_ON == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_ON
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_temp_change_ac_trigger_off_long_enough_3(hass, setup_comp_7):
@@ -1008,17 +1045,17 @@ async def test_temp_change_ac_trigger_off_long_enough_3(hass, setup_comp_7):
test_time = datetime.datetime.now(pytz.UTC)
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=5))
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=10))
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_OFF == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_OFF
+ assert call.data["entity_id"] == ENT_SWITCH
@pytest.fixture
@@ -1056,17 +1093,17 @@ async def test_temp_change_heater_trigger_on_long_enough_2(hass, setup_comp_8):
test_time = datetime.datetime.now(pytz.UTC)
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=5))
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=10))
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_ON == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_ON
+ assert call.data["entity_id"] == ENT_SWITCH
async def test_temp_change_heater_trigger_off_long_enough_2(hass, setup_comp_8):
@@ -1079,17 +1116,17 @@ async def test_temp_change_heater_trigger_off_long_enough_2(hass, setup_comp_8):
test_time = datetime.datetime.now(pytz.UTC)
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=5))
await hass.async_block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=10))
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
call = calls[0]
- assert HASS_DOMAIN == call.domain
- assert SERVICE_TURN_OFF == call.service
- assert ENT_SWITCH == call.data["entity_id"]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_OFF
+ assert call.data["entity_id"] == ENT_SWITCH
@pytest.fixture
@@ -1121,7 +1158,7 @@ async def test_precision(hass, setup_comp_9):
"""Test that setting precision to tenths works as intended."""
await common.async_set_temperature(hass, 23.27)
state = hass.states.get(ENTITY)
- assert 23.3 == state.attributes.get("temperature")
+ assert state.attributes.get("temperature") == 23.3
async def test_custom_setup_params(hass):
@@ -1221,6 +1258,92 @@ async def test_no_restore_state(hass):
assert state.state == HVAC_MODE_OFF
+async def test_initial_hvac_off_force_heater_off(hass):
+ """Ensure that restored state is coherent with real situation.
+
+ 'initial_hvac_mode: off' will force HVAC status, but we must be sure
+ that heater don't keep on.
+ """
+ # switch is on
+ calls = _setup_switch(hass, True)
+ assert hass.states.get(ENT_SWITCH).state == STATE_ON
+
+ _setup_sensor(hass, 16)
+
+ await async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ "climate": {
+ "platform": "generic_thermostat",
+ "name": "test_thermostat",
+ "heater": ENT_SWITCH,
+ "target_sensor": ENT_SENSOR,
+ "target_temp": 20,
+ "initial_hvac_mode": HVAC_MODE_OFF,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ state = hass.states.get("climate.test_thermostat")
+ # 'initial_hvac_mode' will force state but must prevent heather keep working
+ assert state.state == HVAC_MODE_OFF
+ # heater must be switched off
+ assert len(calls) == 1
+ call = calls[0]
+ assert call.domain == HASS_DOMAIN
+ assert call.service == SERVICE_TURN_OFF
+ assert call.data["entity_id"] == ENT_SWITCH
+
+
+async def test_restore_will_turn_off_(hass):
+ """Ensure that restored state is coherent with real situation.
+
+ Thermostat status must trigger heater event if temp raises the target .
+ """
+ heater_switch = "input_boolean.test"
+ mock_restore_cache(
+ hass,
+ (
+ State(
+ "climate.test_thermostat",
+ HVAC_MODE_HEAT,
+ {ATTR_TEMPERATURE: "18", ATTR_PRESET_MODE: PRESET_NONE},
+ ),
+ State(heater_switch, STATE_ON, {}),
+ ),
+ )
+
+ hass.state = CoreState.starting
+
+ assert await async_setup_component(
+ hass, input_boolean.DOMAIN, {"input_boolean": {"test": None}}
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(heater_switch).state == STATE_ON
+
+ _setup_sensor(hass, 22)
+
+ await async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ "climate": {
+ "platform": "generic_thermostat",
+ "name": "test_thermostat",
+ "heater": heater_switch,
+ "target_sensor": ENT_SENSOR,
+ "target_temp": 20,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ state = hass.states.get("climate.test_thermostat")
+ assert state.attributes[ATTR_TEMPERATURE] == 20
+ assert state.state == HVAC_MODE_HEAT
+ assert hass.states.get(heater_switch).state == STATE_ON
+
+
async def test_restore_state_uncoherence_case(hass):
"""
Test restore from a strange state.
@@ -1236,14 +1359,14 @@ async def test_restore_state_uncoherence_case(hass):
await hass.async_block_till_done()
state = hass.states.get(ENTITY)
- assert 20 == state.attributes[ATTR_TEMPERATURE]
- assert HVAC_MODE_OFF == state.state
- assert 0 == len(calls)
+ assert state.attributes[ATTR_TEMPERATURE] == 20
+ assert state.state == HVAC_MODE_OFF
+ assert len(calls) == 0
calls = _setup_switch(hass, False)
await hass.async_block_till_done()
state = hass.states.get(ENTITY)
- assert HVAC_MODE_OFF == state.state
+ assert state.state == HVAC_MODE_OFF
async def _setup_climate(hass):
diff --git a/tests/components/geo_json_events/test_geo_location.py b/tests/components/geo_json_events/test_geo_location.py
index 75f41bb93c0646..9d68e3d497338c 100644
--- a/tests/components/geo_json_events/test_geo_location.py
+++ b/tests/components/geo_json_events/test_geo_location.py
@@ -198,60 +198,59 @@ async def test_setup_race_condition(hass, legacy_patchable_time):
utcnow = dt_util.utcnow()
with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch(
"geojson_client.generic_feed.GenericFeed"
- ) as mock_feed:
- with assert_setup_component(1, geo_location.DOMAIN):
- assert await async_setup_component(hass, geo_location.DOMAIN, CONFIG)
- await hass.async_block_till_done()
+ ) as mock_feed, assert_setup_component(1, geo_location.DOMAIN):
+ assert await async_setup_component(hass, geo_location.DOMAIN, CONFIG)
+ await hass.async_block_till_done()
- mock_feed.return_value.update.return_value = "OK", [mock_entry_1]
+ mock_feed.return_value.update.return_value = "OK", [mock_entry_1]
- # Artificially trigger update.
- hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
- # Collect events.
- await hass.async_block_till_done()
+ # Artificially trigger update.
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ # Collect events.
+ await hass.async_block_till_done()
- all_states = hass.states.async_all()
- assert len(all_states) == 1
- assert len(hass.data[DATA_DISPATCHER][delete_signal]) == 1
- assert len(hass.data[DATA_DISPATCHER][update_signal]) == 1
+ all_states = hass.states.async_all()
+ assert len(all_states) == 1
+ assert len(hass.data[DATA_DISPATCHER][delete_signal]) == 1
+ assert len(hass.data[DATA_DISPATCHER][update_signal]) == 1
- # Simulate an update - empty data, removes all entities
- mock_feed.return_value.update.return_value = "ERROR", None
- async_fire_time_changed(hass, utcnow + SCAN_INTERVAL)
- await hass.async_block_till_done()
+ # Simulate an update - empty data, removes all entities
+ mock_feed.return_value.update.return_value = "ERROR", None
+ async_fire_time_changed(hass, utcnow + SCAN_INTERVAL)
+ await hass.async_block_till_done()
- all_states = hass.states.async_all()
- assert len(all_states) == 0
- assert len(hass.data[DATA_DISPATCHER][delete_signal]) == 0
- assert len(hass.data[DATA_DISPATCHER][update_signal]) == 0
+ all_states = hass.states.async_all()
+ assert len(all_states) == 0
+ assert len(hass.data[DATA_DISPATCHER][delete_signal]) == 0
+ assert len(hass.data[DATA_DISPATCHER][update_signal]) == 0
- # Simulate an update - 1 entry
- mock_feed.return_value.update.return_value = "OK", [mock_entry_1]
- async_fire_time_changed(hass, utcnow + 2 * SCAN_INTERVAL)
- await hass.async_block_till_done()
-
- all_states = hass.states.async_all()
- assert len(all_states) == 1
- assert len(hass.data[DATA_DISPATCHER][delete_signal]) == 1
- assert len(hass.data[DATA_DISPATCHER][update_signal]) == 1
-
- # Simulate an update - 1 entry
- mock_feed.return_value.update.return_value = "OK", [mock_entry_1]
- async_fire_time_changed(hass, utcnow + 3 * SCAN_INTERVAL)
- await hass.async_block_till_done()
-
- all_states = hass.states.async_all()
- assert len(all_states) == 1
- assert len(hass.data[DATA_DISPATCHER][delete_signal]) == 1
- assert len(hass.data[DATA_DISPATCHER][update_signal]) == 1
+ # Simulate an update - 1 entry
+ mock_feed.return_value.update.return_value = "OK", [mock_entry_1]
+ async_fire_time_changed(hass, utcnow + 2 * SCAN_INTERVAL)
+ await hass.async_block_till_done()
- # Simulate an update - empty data, removes all entities
- mock_feed.return_value.update.return_value = "ERROR", None
- async_fire_time_changed(hass, utcnow + 4 * SCAN_INTERVAL)
- await hass.async_block_till_done()
+ all_states = hass.states.async_all()
+ assert len(all_states) == 1
+ assert len(hass.data[DATA_DISPATCHER][delete_signal]) == 1
+ assert len(hass.data[DATA_DISPATCHER][update_signal]) == 1
- all_states = hass.states.async_all()
- assert len(all_states) == 0
- # Ensure that delete and update signal targets are now empty.
- assert len(hass.data[DATA_DISPATCHER][delete_signal]) == 0
- assert len(hass.data[DATA_DISPATCHER][update_signal]) == 0
+ # Simulate an update - 1 entry
+ mock_feed.return_value.update.return_value = "OK", [mock_entry_1]
+ async_fire_time_changed(hass, utcnow + 3 * SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ all_states = hass.states.async_all()
+ assert len(all_states) == 1
+ assert len(hass.data[DATA_DISPATCHER][delete_signal]) == 1
+ assert len(hass.data[DATA_DISPATCHER][update_signal]) == 1
+
+ # Simulate an update - empty data, removes all entities
+ mock_feed.return_value.update.return_value = "ERROR", None
+ async_fire_time_changed(hass, utcnow + 4 * SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ all_states = hass.states.async_all()
+ assert len(all_states) == 0
+ # Ensure that delete and update signal targets are now empty.
+ assert len(hass.data[DATA_DISPATCHER][delete_signal]) == 0
+ assert len(hass.data[DATA_DISPATCHER][update_signal]) == 0
diff --git a/tests/components/geo_location/test_trigger.py b/tests/components/geo_location/test_trigger.py
index ab984a2c309142..e40b134e657376 100644
--- a/tests/components/geo_location/test_trigger.py
+++ b/tests/components/geo_location/test_trigger.py
@@ -7,7 +7,7 @@
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service, mock_component
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
@@ -68,6 +68,7 @@ async def test_if_fires_on_zone_enter(hass, calls):
"from_state.state",
"to_state.state",
"zone.name",
+ "id",
)
)
},
@@ -88,7 +89,7 @@ async def test_if_fires_on_zone_enter(hass, calls):
assert calls[0].context.parent_id == context.id
assert (
calls[0].data["some"]
- == "geo_location - geo_location.entity - hello - hello - test"
+ == "geo_location - geo_location.entity - hello - hello - test - 0"
)
# Set out of zone again so we can trigger call
diff --git a/tests/components/geo_rss_events/test_sensor.py b/tests/components/geo_rss_events/test_sensor.py
index ead81e5c5a8482..95fb89301a9167 100644
--- a/tests/components/geo_rss_events/test_sensor.py
+++ b/tests/components/geo_rss_events/test_sensor.py
@@ -68,54 +68,55 @@ async def test_setup(hass, mock_feed):
utcnow = dt_util.utcnow()
# Patching 'utcnow' to gain more control over the timed update.
- with patch("homeassistant.util.dt.utcnow", return_value=utcnow):
- with assert_setup_component(1, sensor.DOMAIN):
- assert await async_setup_component(hass, sensor.DOMAIN, VALID_CONFIG)
- # Artificially trigger update.
- hass.bus.fire(EVENT_HOMEASSISTANT_START)
- # Collect events.
- await hass.async_block_till_done()
-
- all_states = hass.states.async_all()
- assert len(all_states) == 1
-
- state = hass.states.get("sensor.event_service_any")
- assert state is not None
- assert state.name == "Event Service Any"
- assert int(state.state) == 2
- assert state.attributes == {
- ATTR_FRIENDLY_NAME: "Event Service Any",
- ATTR_UNIT_OF_MEASUREMENT: "Events",
- ATTR_ICON: "mdi:alert",
- "Title 1": "16km",
- "Title 2": "20km",
- }
-
- # Simulate an update - empty data, but successful update,
- # so no changes to entities.
- mock_feed.return_value.update.return_value = "OK_NO_DATA", None
- async_fire_time_changed(hass, utcnow + geo_rss_events.SCAN_INTERVAL)
- await hass.async_block_till_done()
-
- all_states = hass.states.async_all()
- assert len(all_states) == 1
- state = hass.states.get("sensor.event_service_any")
- assert int(state.state) == 2
-
- # Simulate an update - empty data, removes all entities
- mock_feed.return_value.update.return_value = "ERROR", None
- async_fire_time_changed(hass, utcnow + 2 * geo_rss_events.SCAN_INTERVAL)
- await hass.async_block_till_done()
-
- all_states = hass.states.async_all()
- assert len(all_states) == 1
- state = hass.states.get("sensor.event_service_any")
- assert int(state.state) == 0
- assert state.attributes == {
- ATTR_FRIENDLY_NAME: "Event Service Any",
- ATTR_UNIT_OF_MEASUREMENT: "Events",
- ATTR_ICON: "mdi:alert",
- }
+ with patch(
+ "homeassistant.util.dt.utcnow", return_value=utcnow
+ ), assert_setup_component(1, sensor.DOMAIN):
+ assert await async_setup_component(hass, sensor.DOMAIN, VALID_CONFIG)
+ # Artificially trigger update.
+ hass.bus.fire(EVENT_HOMEASSISTANT_START)
+ # Collect events.
+ await hass.async_block_till_done()
+
+ all_states = hass.states.async_all()
+ assert len(all_states) == 1
+
+ state = hass.states.get("sensor.event_service_any")
+ assert state is not None
+ assert state.name == "Event Service Any"
+ assert int(state.state) == 2
+ assert state.attributes == {
+ ATTR_FRIENDLY_NAME: "Event Service Any",
+ ATTR_UNIT_OF_MEASUREMENT: "Events",
+ ATTR_ICON: "mdi:alert",
+ "Title 1": "16km",
+ "Title 2": "20km",
+ }
+
+ # Simulate an update - empty data, but successful update,
+ # so no changes to entities.
+ mock_feed.return_value.update.return_value = "OK_NO_DATA", None
+ async_fire_time_changed(hass, utcnow + geo_rss_events.SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ all_states = hass.states.async_all()
+ assert len(all_states) == 1
+ state = hass.states.get("sensor.event_service_any")
+ assert int(state.state) == 2
+
+ # Simulate an update - empty data, removes all entities
+ mock_feed.return_value.update.return_value = "ERROR", None
+ async_fire_time_changed(hass, utcnow + 2 * geo_rss_events.SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ all_states = hass.states.async_all()
+ assert len(all_states) == 1
+ state = hass.states.get("sensor.event_service_any")
+ assert int(state.state) == 0
+ assert state.attributes == {
+ ATTR_FRIENDLY_NAME: "Event Service Any",
+ ATTR_UNIT_OF_MEASUREMENT: "Events",
+ ATTR_ICON: "mdi:alert",
+ }
async def test_setup_with_categories(hass, mock_feed):
diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py
index b87b201a144cc5..b84b6b681ae221 100644
--- a/tests/components/geofency/test_init.py
+++ b/tests/components/geofency/test_init.py
@@ -16,6 +16,7 @@
STATE_HOME,
STATE_NOT_HOME,
)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util import slugify
@@ -195,7 +196,7 @@ async def test_gps_enter_and_exit_home(hass, geofency_client, webhook_id):
assert req.status == HTTP_OK
device_name = slugify(GPS_ENTER_HOME["device"])
state_name = hass.states.get(f"device_tracker.{device_name}").state
- assert STATE_HOME == state_name
+ assert state_name == STATE_HOME
# Exit the Home zone
req = await geofency_client.post(url, data=GPS_EXIT_HOME)
@@ -203,7 +204,7 @@ async def test_gps_enter_and_exit_home(hass, geofency_client, webhook_id):
assert req.status == HTTP_OK
device_name = slugify(GPS_EXIT_HOME["device"])
state_name = hass.states.get(f"device_tracker.{device_name}").state
- assert STATE_NOT_HOME == state_name
+ assert state_name == STATE_NOT_HOME
# Exit the Home zone with "Send Current Position" enabled
data = GPS_EXIT_HOME.copy()
@@ -217,16 +218,16 @@ async def test_gps_enter_and_exit_home(hass, geofency_client, webhook_id):
current_latitude = hass.states.get(f"device_tracker.{device_name}").attributes[
"latitude"
]
- assert NOT_HOME_LATITUDE == current_latitude
+ assert current_latitude == NOT_HOME_LATITUDE
current_longitude = hass.states.get(f"device_tracker.{device_name}").attributes[
"longitude"
]
- assert NOT_HOME_LONGITUDE == current_longitude
+ assert current_longitude == NOT_HOME_LONGITUDE
- dev_reg = await hass.helpers.device_registry.async_get_registry()
+ dev_reg = dr.async_get(hass)
assert len(dev_reg.devices) == 1
- ent_reg = await hass.helpers.entity_registry.async_get_registry()
+ ent_reg = er.async_get(hass)
assert len(ent_reg.entities) == 1
@@ -240,7 +241,7 @@ async def test_beacon_enter_and_exit_home(hass, geofency_client, webhook_id):
assert req.status == HTTP_OK
device_name = slugify(f"beacon_{BEACON_ENTER_HOME['name']}")
state_name = hass.states.get(f"device_tracker.{device_name}").state
- assert STATE_HOME == state_name
+ assert state_name == STATE_HOME
# Exit the Home zone
req = await geofency_client.post(url, data=BEACON_EXIT_HOME)
@@ -248,7 +249,7 @@ async def test_beacon_enter_and_exit_home(hass, geofency_client, webhook_id):
assert req.status == HTTP_OK
device_name = slugify(f"beacon_{BEACON_ENTER_HOME['name']}")
state_name = hass.states.get(f"device_tracker.{device_name}").state
- assert STATE_NOT_HOME == state_name
+ assert state_name == STATE_NOT_HOME
async def test_beacon_enter_and_exit_car(hass, geofency_client, webhook_id):
@@ -261,7 +262,7 @@ async def test_beacon_enter_and_exit_car(hass, geofency_client, webhook_id):
assert req.status == HTTP_OK
device_name = slugify(f"beacon_{BEACON_ENTER_CAR['name']}")
state_name = hass.states.get(f"device_tracker.{device_name}").state
- assert STATE_NOT_HOME == state_name
+ assert state_name == STATE_NOT_HOME
# Exit the Car away from Home zone
req = await geofency_client.post(url, data=BEACON_EXIT_CAR)
@@ -269,7 +270,7 @@ async def test_beacon_enter_and_exit_car(hass, geofency_client, webhook_id):
assert req.status == HTTP_OK
device_name = slugify(f"beacon_{BEACON_ENTER_CAR['name']}")
state_name = hass.states.get(f"device_tracker.{device_name}").state
- assert STATE_NOT_HOME == state_name
+ assert state_name == STATE_NOT_HOME
# Enter the Car in the Home zone
data = BEACON_ENTER_CAR.copy()
@@ -280,7 +281,7 @@ async def test_beacon_enter_and_exit_car(hass, geofency_client, webhook_id):
assert req.status == HTTP_OK
device_name = slugify(f"beacon_{data['name']}")
state_name = hass.states.get(f"device_tracker.{device_name}").state
- assert STATE_HOME == state_name
+ assert state_name == STATE_HOME
# Exit the Car in the Home zone
req = await geofency_client.post(url, data=data)
@@ -288,7 +289,7 @@ async def test_beacon_enter_and_exit_car(hass, geofency_client, webhook_id):
assert req.status == HTTP_OK
device_name = slugify(f"beacon_{data['name']}")
state_name = hass.states.get(f"device_tracker.{device_name}").state
- assert STATE_HOME == state_name
+ assert state_name == STATE_HOME
async def test_load_unload_entry(hass, geofency_client, webhook_id):
@@ -301,7 +302,7 @@ async def test_load_unload_entry(hass, geofency_client, webhook_id):
assert req.status == HTTP_OK
device_name = slugify(GPS_ENTER_HOME["device"])
state_1 = hass.states.get(f"device_tracker.{device_name}")
- assert STATE_HOME == state_1.state
+ assert state_1.state == STATE_HOME
assert len(hass.data[DOMAIN]["devices"]) == 1
entry = hass.config_entries.async_entries(DOMAIN)[0]
@@ -317,6 +318,6 @@ async def test_load_unload_entry(hass, geofency_client, webhook_id):
assert state_2 is not None
assert state_1 is not state_2
- assert STATE_HOME == state_2.state
+ assert state_2.state == STATE_HOME
assert state_2.attributes[ATTR_LATITUDE] == HOME_LATITUDE
assert state_2.attributes[ATTR_LONGITUDE] == HOME_LONGITUDE
diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py
index b0e54e899298b1..9c700c2b38e4be 100644
--- a/tests/components/geonetnz_quakes/test_geo_location.py
+++ b/tests/components/geonetnz_quakes/test_geo_location.py
@@ -25,7 +25,7 @@
EVENT_HOMEASSISTANT_START,
LENGTH_KILOMETERS,
)
-from homeassistant.helpers.entity_registry import async_get_registry
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from homeassistant.util.unit_system import IMPERIAL_SYSTEM
@@ -75,7 +75,7 @@ async def test_setup(hass):
all_states = hass.states.async_all()
# 3 geolocation and 1 sensor entities
assert len(all_states) == 4
- entity_registry = await async_get_registry(hass)
+ entity_registry = er.async_get(hass)
assert len(entity_registry.entities) == 4
state = hass.states.get("geo_location.title_1")
diff --git a/tests/components/gios/test_air_quality.py b/tests/components/gios/test_air_quality.py
index 21a1abf637aa3b..873e1e089a3a37 100644
--- a/tests/components/gios/test_air_quality.py
+++ b/tests/components/gios/test_air_quality.py
@@ -23,6 +23,7 @@
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
STATE_UNAVAILABLE,
)
+from homeassistant.helpers import entity_registry as er
from homeassistant.util.dt import utcnow
from tests.common import async_fire_time_changed, load_fixture
@@ -32,7 +33,7 @@
async def test_air_quality(hass):
"""Test states of the air_quality."""
await init_integration(hass)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
state = hass.states.get("air_quality.home")
assert state
@@ -60,7 +61,7 @@ async def test_air_quality(hass):
async def test_air_quality_with_incomplete_data(hass):
"""Test states of the air_quality with incomplete data from measuring station."""
await init_integration(hass, incomplete_data=True)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
state = hass.states.get("air_quality.home")
assert state
diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py
index 2d19ee70609242..621ea4d52328e1 100644
--- a/tests/components/gogogate2/test_config_flow.py
+++ b/tests/components/gogogate2/test_config_flow.py
@@ -26,11 +26,10 @@
MOCK_MAC_ADDR = "AA:BB:CC:DD:EE:FF"
-@patch("homeassistant.components.gogogate2.async_setup", return_value=True)
@patch("homeassistant.components.gogogate2.async_setup_entry", return_value=True)
@patch("homeassistant.components.gogogate2.common.GogoGate2Api")
async def test_auth_fail(
- gogogate2api_mock, async_setup_entry_mock, async_setup_mock, hass: HomeAssistant
+ gogogate2api_mock, async_setup_entry_mock, hass: HomeAssistant
) -> None:
"""Test authorization failures."""
api: GogoGate2Api = MagicMock(spec=GogoGate2Api)
diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py
index 31810cb67d0786..41b4368c64078d 100644
--- a/tests/components/gogogate2/test_cover.py
+++ b/tests/components/gogogate2/test_cover.py
@@ -21,6 +21,8 @@
DEVICE_CLASS_GARAGE,
DEVICE_CLASS_GATE,
DOMAIN as COVER_DOMAIN,
+ SUPPORT_CLOSE,
+ SUPPORT_OPEN,
)
from homeassistant.components.gogogate2.const import (
DEVICE_TYPE_GOGOGATE2,
@@ -319,6 +321,13 @@ def info_response(door_status: DoorStatus) -> GogoGate2InfoResponse:
wifi=Wifi(SSID="", linkquality="", signal=""),
)
+ expected_attributes = {
+ "device_class": "garage",
+ "door_id": 1,
+ "friendly_name": "Door1",
+ "supported_features": SUPPORT_CLOSE | SUPPORT_OPEN,
+ }
+
api = MagicMock(GogoGate2Api)
api.async_activate.return_value = GogoGate2ActivateResponse(result=True)
api.async_info.return_value = info_response(DoorStatus.OPENED)
@@ -339,6 +348,7 @@ def info_response(door_status: DoorStatus) -> GogoGate2InfoResponse:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get("cover.door1").state == STATE_OPEN
+ assert dict(hass.states.get("cover.door1").attributes) == expected_attributes
api.async_info.return_value = info_response(DoorStatus.CLOSED)
await hass.services.async_call(
@@ -376,6 +386,13 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None:
"""Test availability."""
closed_door_response = _mocked_ismartgate_closed_door_response()
+ expected_attributes = {
+ "device_class": "garage",
+ "door_id": 1,
+ "friendly_name": "Door1",
+ "supported_features": SUPPORT_CLOSE | SUPPORT_OPEN,
+ }
+
api = MagicMock(ISmartGateApi)
api.async_info.return_value = closed_door_response
ismartgateapi_mock.return_value = api
@@ -416,6 +433,7 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None:
async_fire_time_changed(hass, utcnow() + timedelta(hours=2))
await hass.async_block_till_done()
assert hass.states.get("cover.door1").state == STATE_CLOSED
+ assert dict(hass.states.get("cover.door1").attributes) == expected_attributes
@patch("homeassistant.components.gogogate2.common.ISmartGateApi")
diff --git a/tests/components/gogogate2/test_init.py b/tests/components/gogogate2/test_init.py
index f51f08b231926c..7bcd2f8d2f23ff 100644
--- a/tests/components/gogogate2/test_init.py
+++ b/tests/components/gogogate2/test_init.py
@@ -1,11 +1,11 @@
"""Tests for the GogoGate2 component."""
+import asyncio
from unittest.mock import MagicMock, patch
from gogogate2_api import GogoGate2Api
import pytest
from homeassistant.components.gogogate2 import DEVICE_TYPE_GOGOGATE2, async_setup_entry
-from homeassistant.components.gogogate2.common import DeviceDataUpdateCoordinator
from homeassistant.components.gogogate2.const import DEVICE_TYPE_ISMARTGATE, DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import (
@@ -78,19 +78,22 @@ async def test_config_no_update(ismartgateapi_mock, hass: HomeAssistant) -> None
}
-async def test_auth_fail(hass: HomeAssistant) -> None:
- """Test authorization failures."""
-
- coordinator_mock: DeviceDataUpdateCoordinator = MagicMock(
- spec=DeviceDataUpdateCoordinator
+async def test_api_failure_on_startup(hass: HomeAssistant) -> None:
+ """Test api failure on startup raises ConfigEntryNotReady."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ source=SOURCE_USER,
+ data={
+ CONF_DEVICE: DEVICE_TYPE_ISMARTGATE,
+ CONF_IP_ADDRESS: "127.0.0.1",
+ CONF_USERNAME: "admin",
+ CONF_PASSWORD: "password",
+ },
)
- coordinator_mock.last_update_success = False
-
- config_entry = MockConfigEntry()
config_entry.add_to_hass(hass)
with patch(
- "homeassistant.components.gogogate2.get_data_update_coordinator",
- return_value=coordinator_mock,
+ "homeassistant.components.gogogate2.common.ISmartGateApi.async_info",
+ side_effect=asyncio.TimeoutError,
), pytest.raises(ConfigEntryNotReady):
await async_setup_entry(hass, config_entry)
diff --git a/tests/components/gogogate2/test_sensor.py b/tests/components/gogogate2/test_sensor.py
new file mode 100644
index 00000000000000..020989c003adb7
--- /dev/null
+++ b/tests/components/gogogate2/test_sensor.py
@@ -0,0 +1,319 @@
+"""Tests for the GogoGate2 component."""
+from datetime import timedelta
+from unittest.mock import MagicMock, patch
+
+from gogogate2_api import GogoGate2Api, ISmartGateApi
+from gogogate2_api.common import (
+ DoorMode,
+ DoorStatus,
+ GogoGate2ActivateResponse,
+ GogoGate2Door,
+ GogoGate2InfoResponse,
+ ISmartGateDoor,
+ ISmartGateInfoResponse,
+ Network,
+ Outputs,
+ Wifi,
+)
+
+from homeassistant.components.gogogate2.const import DEVICE_TYPE_ISMARTGATE, DOMAIN
+from homeassistant.config_entries import SOURCE_USER
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ ATTR_UNIT_OF_MEASUREMENT,
+ CONF_DEVICE,
+ CONF_IP_ADDRESS,
+ CONF_PASSWORD,
+ CONF_USERNAME,
+ DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_TEMPERATURE,
+ STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.util.dt import utcnow
+
+from tests.common import MockConfigEntry, async_fire_time_changed
+
+
+def _mocked_gogogate_sensor_response(battery_level: int, temperature: float):
+ return GogoGate2InfoResponse(
+ user="user1",
+ gogogatename="gogogatename0",
+ model="",
+ apiversion="",
+ remoteaccessenabled=False,
+ remoteaccess="abc123.blah.blah",
+ firmwareversion="",
+ apicode="",
+ door1=GogoGate2Door(
+ door_id=1,
+ permission=True,
+ name="Door1",
+ gate=False,
+ mode=DoorMode.GARAGE,
+ status=DoorStatus.OPENED,
+ sensor=True,
+ sensorid="ABCD",
+ camera=False,
+ events=2,
+ temperature=temperature,
+ voltage=battery_level,
+ ),
+ door2=GogoGate2Door(
+ door_id=2,
+ permission=True,
+ name="Door2",
+ gate=True,
+ mode=DoorMode.GARAGE,
+ status=DoorStatus.UNDEFINED,
+ sensor=True,
+ sensorid="WIRE",
+ camera=False,
+ events=0,
+ temperature=temperature,
+ voltage=battery_level,
+ ),
+ door3=GogoGate2Door(
+ door_id=3,
+ permission=True,
+ name="Door3",
+ gate=False,
+ mode=DoorMode.GARAGE,
+ status=DoorStatus.UNDEFINED,
+ sensor=True,
+ sensorid=None,
+ camera=False,
+ events=0,
+ temperature=temperature,
+ voltage=battery_level,
+ ),
+ outputs=Outputs(output1=True, output2=False, output3=True),
+ network=Network(ip=""),
+ wifi=Wifi(SSID="", linkquality="", signal=""),
+ )
+
+
+def _mocked_ismartgate_sensor_response(battery_level: int, temperature: float):
+ return ISmartGateInfoResponse(
+ user="user1",
+ ismartgatename="ismartgatename0",
+ model="ismartgatePRO",
+ apiversion="",
+ remoteaccessenabled=False,
+ remoteaccess="abc321.blah.blah",
+ firmwareversion="555",
+ pin=123,
+ lang="en",
+ newfirmware=False,
+ door1=ISmartGateDoor(
+ door_id=1,
+ permission=True,
+ name="Door1",
+ gate=False,
+ mode=DoorMode.GARAGE,
+ status=DoorStatus.CLOSED,
+ sensor=True,
+ sensorid="ABCD",
+ camera=False,
+ events=2,
+ temperature=temperature,
+ enabled=True,
+ apicode="apicode0",
+ customimage=False,
+ voltage=battery_level,
+ ),
+ door2=ISmartGateDoor(
+ door_id=2,
+ permission=True,
+ name="Door2",
+ gate=True,
+ mode=DoorMode.GARAGE,
+ status=DoorStatus.CLOSED,
+ sensor=True,
+ sensorid="WIRE",
+ camera=False,
+ events=2,
+ temperature=temperature,
+ enabled=True,
+ apicode="apicode0",
+ customimage=False,
+ voltage=battery_level,
+ ),
+ door3=ISmartGateDoor(
+ door_id=3,
+ permission=True,
+ name="Door3",
+ gate=False,
+ mode=DoorMode.GARAGE,
+ status=DoorStatus.UNDEFINED,
+ sensor=True,
+ sensorid=None,
+ camera=False,
+ events=0,
+ temperature=temperature,
+ enabled=True,
+ apicode="apicode0",
+ customimage=False,
+ voltage=battery_level,
+ ),
+ network=Network(ip=""),
+ wifi=Wifi(SSID="", linkquality="", signal=""),
+ )
+
+
+@patch("homeassistant.components.gogogate2.common.GogoGate2Api")
+async def test_sensor_update(gogogate2api_mock, hass: HomeAssistant) -> None:
+ """Test data update."""
+
+ bat_attributes = {
+ "device_class": "battery",
+ "door_id": 1,
+ "friendly_name": "Door1 battery",
+ "sensor_id": "ABCD",
+ }
+ temp_attributes = {
+ "device_class": "temperature",
+ "door_id": 1,
+ "friendly_name": "Door1 temperature",
+ "sensor_id": "ABCD",
+ "unit_of_measurement": "°C",
+ }
+
+ api = MagicMock(GogoGate2Api)
+ api.async_activate.return_value = GogoGate2ActivateResponse(result=True)
+ api.async_info.return_value = _mocked_gogogate_sensor_response(25, 7.0)
+ gogogate2api_mock.return_value = api
+
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ source=SOURCE_USER,
+ data={
+ CONF_IP_ADDRESS: "127.0.0.1",
+ CONF_USERNAME: "admin",
+ CONF_PASSWORD: "password",
+ },
+ )
+ config_entry.add_to_hass(hass)
+
+ assert hass.states.get("cover.door1") is None
+ assert hass.states.get("cover.door2") is None
+ assert hass.states.get("cover.door3") is None
+ assert hass.states.get("sensor.door1_battery") is None
+ assert hass.states.get("sensor.door2_battery") is None
+ assert hass.states.get("sensor.door3_battery") is None
+ assert hass.states.get("sensor.door1_temperature") is None
+ assert hass.states.get("sensor.door2_temperature") is None
+ assert hass.states.get("sensor.door3_temperature") is None
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert hass.states.get("cover.door1")
+ assert hass.states.get("cover.door2")
+ assert hass.states.get("cover.door3")
+ assert hass.states.get("sensor.door1_battery").state == "25"
+ assert dict(hass.states.get("sensor.door1_battery").attributes) == bat_attributes
+ assert hass.states.get("sensor.door2_battery") is None
+ assert hass.states.get("sensor.door2_battery") is None
+ assert hass.states.get("sensor.door1_temperature").state == "7.0"
+ assert (
+ dict(hass.states.get("sensor.door1_temperature").attributes) == temp_attributes
+ )
+ assert hass.states.get("sensor.door2_temperature") is None
+ assert hass.states.get("sensor.door3_temperature") is None
+
+ api.async_info.return_value = _mocked_gogogate_sensor_response(40, 10.0)
+ async_fire_time_changed(hass, utcnow() + timedelta(hours=2))
+ await hass.async_block_till_done()
+ assert hass.states.get("sensor.door1_battery").state == "40"
+ assert hass.states.get("sensor.door1_temperature").state == "10.0"
+
+ api.async_info.return_value = _mocked_gogogate_sensor_response(None, None)
+ async_fire_time_changed(hass, utcnow() + timedelta(hours=2))
+ await hass.async_block_till_done()
+ assert hass.states.get("sensor.door1_battery").state == STATE_UNKNOWN
+ assert hass.states.get("sensor.door1_temperature").state == STATE_UNKNOWN
+
+ assert await hass.config_entries.async_unload(config_entry.entry_id)
+ assert not hass.states.async_entity_ids(DOMAIN)
+
+
+@patch("homeassistant.components.gogogate2.common.ISmartGateApi")
+async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None:
+ """Test availability."""
+ bat_attributes = {
+ "device_class": "battery",
+ "door_id": 1,
+ "friendly_name": "Door1 battery",
+ "sensor_id": "ABCD",
+ }
+ temp_attributes = {
+ "device_class": "temperature",
+ "door_id": 1,
+ "friendly_name": "Door1 temperature",
+ "sensor_id": "ABCD",
+ "unit_of_measurement": "°C",
+ }
+
+ sensor_response = _mocked_ismartgate_sensor_response(35, -4.0)
+ api = MagicMock(ISmartGateApi)
+ api.async_info.return_value = sensor_response
+ ismartgateapi_mock.return_value = api
+
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ source=SOURCE_USER,
+ data={
+ CONF_DEVICE: DEVICE_TYPE_ISMARTGATE,
+ CONF_IP_ADDRESS: "127.0.0.1",
+ CONF_USERNAME: "admin",
+ CONF_PASSWORD: "password",
+ },
+ )
+ config_entry.add_to_hass(hass)
+
+ assert hass.states.get("cover.door1") is None
+ assert hass.states.get("cover.door2") is None
+ assert hass.states.get("cover.door3") is None
+ assert hass.states.get("sensor.door1_battery") is None
+ assert hass.states.get("sensor.door2_battery") is None
+ assert hass.states.get("sensor.door3_battery") is None
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert hass.states.get("cover.door1")
+ assert hass.states.get("cover.door2")
+ assert hass.states.get("cover.door3")
+ assert hass.states.get("sensor.door1_battery").state == "35"
+ assert hass.states.get("sensor.door2_battery") is None
+ assert hass.states.get("sensor.door3_battery") is None
+ assert hass.states.get("sensor.door1_temperature").state == "-4.0"
+ assert hass.states.get("sensor.door2_temperature") is None
+ assert hass.states.get("sensor.door3_temperature") is None
+ assert (
+ hass.states.get("sensor.door1_battery").attributes[ATTR_DEVICE_CLASS]
+ == DEVICE_CLASS_BATTERY
+ )
+ assert (
+ hass.states.get("sensor.door1_temperature").attributes[ATTR_DEVICE_CLASS]
+ == DEVICE_CLASS_TEMPERATURE
+ )
+ assert (
+ hass.states.get("sensor.door1_temperature").attributes[ATTR_UNIT_OF_MEASUREMENT]
+ == "°C"
+ )
+
+ api.async_info.side_effect = Exception("Error")
+
+ async_fire_time_changed(hass, utcnow() + timedelta(hours=2))
+ await hass.async_block_till_done()
+ assert hass.states.get("sensor.door1_battery").state == STATE_UNAVAILABLE
+ assert hass.states.get("sensor.door1_temperature").state == STATE_UNAVAILABLE
+
+ api.async_info.side_effect = None
+ api.async_info.return_value = sensor_response
+ async_fire_time_changed(hass, utcnow() + timedelta(hours=2))
+ await hass.async_block_till_done()
+ assert hass.states.get("sensor.door1_battery").state == "35"
+ assert dict(hass.states.get("sensor.door1_battery").attributes) == bat_attributes
+ assert (
+ dict(hass.states.get("sensor.door1_temperature").attributes) == temp_attributes
+ )
diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py
index 4bef45cf0ee733..0fe89d0fa7b07d 100644
--- a/tests/components/google_assistant/__init__.py
+++ b/tests/components/google_assistant/__init__.py
@@ -192,6 +192,19 @@ def should_2fa(self, state):
"type": "action.devices.types.SETTOP",
"willReportState": False,
},
+ {
+ "id": "media_player.kitchen",
+ "name": {"name": "Kitchen"},
+ "traits": [
+ "action.devices.traits.OnOff",
+ "action.devices.traits.Volume",
+ "action.devices.traits.Modes",
+ "action.devices.traits.TransportControl",
+ "action.devices.traits.MediaState",
+ ],
+ "type": "action.devices.types.SETTOP",
+ "willReportState": False,
+ },
{
"id": "media_player.living_room",
"name": {"name": "Living Room"},
@@ -276,7 +289,15 @@ def should_2fa(self, state):
"type": "action.devices.types.THERMOSTAT",
"willReportState": False,
"attributes": {
- "availableThermostatModes": "off,heat,cool,heatcool,auto,dry,fan-only",
+ "availableThermostatModes": [
+ "off",
+ "heat",
+ "cool",
+ "heatcool",
+ "auto",
+ "dry",
+ "fan-only",
+ ],
"thermostatTemperatureUnit": "C",
},
},
diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py
index cd268a5c2d9640..bc9195264d9ab1 100644
--- a/tests/components/google_assistant/test_google_assistant.py
+++ b/tests/components/google_assistant/test_google_assistant.py
@@ -473,4 +473,4 @@ async def test_execute_request(hass_fixture, assistant_client, auth_header):
assert dehumidifier.attributes.get(humidifier.ATTR_HUMIDITY) == 45
hygrostat = hass_fixture.states.get("humidifier.hygrostat")
- assert hygrostat.attributes.get(humidifier.ATTR_MODE) == "eco"
+ assert hygrostat.attributes.get(const.ATTR_MODE) == "eco"
diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py
index abf2773d67e4b8..d6094a771bd14e 100644
--- a/tests/components/google_assistant/test_helpers.py
+++ b/tests/components/google_assistant/test_helpers.py
@@ -5,7 +5,7 @@
import pytest
from homeassistant.components.google_assistant import helpers
-from homeassistant.components.google_assistant.const import ( # noqa: F401
+from homeassistant.components.google_assistant.const import (
EVENT_COMMAND_RECEIVED,
NOT_EXPOSE_LOCAL,
)
diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py
index 72130dbfdb9848..f464be60bb93cd 100644
--- a/tests/components/google_assistant/test_report_state.py
+++ b/tests/components/google_assistant/test_report_state.py
@@ -46,6 +46,24 @@ async def test_report_state(hass, caplog, legacy_patchable_time):
"devices": {"states": {"light.kitchen": {"on": True, "online": True}}}
}
+ # Test that if serialize returns same value, we don't send
+ with patch(
+ "homeassistant.components.google_assistant.report_state.GoogleEntity.query_serialize",
+ return_value={"same": "info"},
+ ), patch.object(BASIC_CONFIG, "async_report_state_all", AsyncMock()) as mock_report:
+ # New state, so reported
+ hass.states.async_set("light.double_report", "on")
+ await hass.async_block_till_done()
+
+ # Changed, but serialize is same, so filtered out by extra check
+ hass.states.async_set("light.double_report", "off")
+ await hass.async_block_till_done()
+
+ assert len(mock_report.mock_calls) == 1
+ assert mock_report.mock_calls[0][1][0] == {
+ "devices": {"states": {"light.double_report": {"same": "info"}}}
+ }
+
# Test that only significant state changes are reported
with patch.object(
BASIC_CONFIG, "async_report_state_all", AsyncMock()
diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py
index 9c8f9a483388d7..9531602ef0c512 100644
--- a/tests/components/google_assistant/test_smart_home.py
+++ b/tests/components/google_assistant/test_smart_home.py
@@ -30,7 +30,12 @@
from . import BASIC_CONFIG, MockConfig
-from tests.common import mock_area_registry, mock_device_registry, mock_registry
+from tests.common import (
+ async_capture_events,
+ mock_area_registry,
+ mock_device_registry,
+ mock_registry,
+)
REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf"
@@ -77,8 +82,7 @@ async def test_sync_message(hass):
},
)
- events = []
- hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append)
+ events = async_capture_events(hass, EVENT_SYNC_RECEIVED)
result = await sh.async_handle_message(
hass,
@@ -192,8 +196,7 @@ async def test_sync_in_area(area_on_device, hass, registries):
config = MockConfig(should_expose=lambda _: True, entity_config={})
- events = []
- hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append)
+ events = async_capture_events(hass, EVENT_SYNC_RECEIVED)
result = await sh.async_handle_message(
hass,
@@ -295,8 +298,7 @@ async def test_query_message(hass):
light3.entity_id = "light.color_temp_light"
await light3.async_update_ha_state()
- events = []
- hass.bus.async_listen(EVENT_QUERY_RECEIVED, events.append)
+ events = async_capture_events(hass, EVENT_QUERY_RECEIVED)
result = await sh.async_handle_message(
hass,
@@ -387,11 +389,8 @@ async def test_execute(hass):
"light", "turn_off", {"entity_id": "light.ceiling_lights"}, blocking=True
)
- events = []
- hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append)
-
- service_events = []
- hass.bus.async_listen(EVENT_CALL_SERVICE, service_events.append)
+ events = async_capture_events(hass, EVENT_COMMAND_RECEIVED)
+ service_events = async_capture_events(hass, EVENT_CALL_SERVICE)
result = await sh.async_handle_message(
hass,
@@ -570,8 +569,7 @@ async def test_raising_error_trait(hass):
{ATTR_MIN_TEMP: 15, ATTR_MAX_TEMP: 30, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
)
- events = []
- hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append)
+ events = async_capture_events(hass, EVENT_COMMAND_RECEIVED)
await hass.async_block_till_done()
result = await sh.async_handle_message(
@@ -660,8 +658,7 @@ async def test_unavailable_state_does_sync(hass):
light._available = False # pylint: disable=protected-access
await light.async_update_ha_state()
- events = []
- hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append)
+ events = async_capture_events(hass, EVENT_SYNC_RECEIVED)
result = await sh.async_handle_message(
hass,
diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py
index 9b573f1cf719dd..fd62d225aace40 100644
--- a/tests/components/google_assistant/test_trait.py
+++ b/tests/components/google_assistant/test_trait.py
@@ -31,6 +31,7 @@
ATTR_ASSUMED_STATE,
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
+ ATTR_MODE,
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
SERVICE_TURN_OFF,
@@ -54,7 +55,7 @@
from . import BASIC_CONFIG, MockConfig
-from tests.common import async_mock_service
+from tests.common import async_capture_events, async_mock_service
REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf"
@@ -84,8 +85,7 @@ async def test_brightness_light(hass):
assert trt.query_attributes() == {"brightness": 95}
- events = []
- hass.bus.async_listen(EVENT_CALL_SERVICE, events.append)
+ events = async_capture_events(hass, EVENT_CALL_SERVICE)
calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
await trt.execute(
@@ -713,7 +713,7 @@ async def test_temperature_setting_climate_onoff(hass):
BASIC_CONFIG,
)
assert trt.sync_attributes() == {
- "availableThermostatModes": "off,cool,heat,heatcool,on",
+ "availableThermostatModes": ["off", "cool", "heat", "heatcool", "on"],
"thermostatTemperatureUnit": "F",
}
assert trt.can_execute(trait.COMMAND_THERMOSTAT_SET_MODE, {})
@@ -752,7 +752,7 @@ async def test_temperature_setting_climate_no_modes(hass):
BASIC_CONFIG,
)
assert trt.sync_attributes() == {
- "availableThermostatModes": "heat",
+ "availableThermostatModes": ["heat"],
"thermostatTemperatureUnit": "C",
}
@@ -788,7 +788,7 @@ async def test_temperature_setting_climate_range(hass):
BASIC_CONFIG,
)
assert trt.sync_attributes() == {
- "availableThermostatModes": "off,cool,heat,auto,on",
+ "availableThermostatModes": ["off", "cool", "heat", "auto", "on"],
"thermostatTemperatureUnit": "F",
}
assert trt.query_attributes() == {
@@ -862,7 +862,7 @@ async def test_temperature_setting_climate_setpoint(hass):
BASIC_CONFIG,
)
assert trt.sync_attributes() == {
- "availableThermostatModes": "off,cool,on",
+ "availableThermostatModes": ["off", "cool", "on"],
"thermostatTemperatureUnit": "C",
}
assert trt.query_attributes() == {
@@ -920,7 +920,7 @@ async def test_temperature_setting_climate_setpoint_auto(hass):
BASIC_CONFIG,
)
assert trt.sync_attributes() == {
- "availableThermostatModes": "off,heatcool,on",
+ "availableThermostatModes": ["off", "heatcool", "on"],
"thermostatTemperatureUnit": "C",
}
assert trt.query_attributes() == {
@@ -1391,6 +1391,7 @@ async def test_fan_speed(hass):
fan.SPEED_HIGH,
],
"speed": "low",
+ "percentage": 33,
},
),
BASIC_CONFIG,
@@ -1438,11 +1439,13 @@ async def test_fan_speed(hass):
],
},
"reversible": False,
+ "supportsFanSpeedPercent": True,
}
assert trt.query_attributes() == {
"currentFanSpeedSetting": "low",
"on": True,
+ "currentFanSpeedPercent": 33,
}
assert trt.can_execute(trait.COMMAND_FANSPEED, params={"fanSpeed": "medium"})
@@ -1453,6 +1456,14 @@ async def test_fan_speed(hass):
assert len(calls) == 1
assert calls[0].data == {"entity_id": "fan.living_room_fan", "speed": "medium"}
+ assert trt.can_execute(trait.COMMAND_FANSPEED, params={"fanSpeedPercent": 10})
+
+ calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_PERCENTAGE)
+ await trt.execute(trait.COMMAND_FANSPEED, BASIC_DATA, {"fanSpeedPercent": 10}, {})
+
+ assert len(calls) == 1
+ assert calls[0].data == {"entity_id": "fan.living_room_fan", "percentage": 10}
+
async def test_climate_fan_speed(hass):
"""Test FanSpeed trait speed control support for climate domain."""
@@ -1495,6 +1506,7 @@ async def test_climate_fan_speed(hass):
],
},
"reversible": False,
+ "supportsFanSpeedPercent": True,
}
assert trt.query_attributes() == {
@@ -1767,7 +1779,7 @@ async def test_modes_humidifier(hass):
humidifier.ATTR_MIN_HUMIDITY: 30,
humidifier.ATTR_MAX_HUMIDITY: 99,
humidifier.ATTR_HUMIDITY: 50,
- humidifier.ATTR_MODE: humidifier.MODE_AUTO,
+ ATTR_MODE: humidifier.MODE_AUTO,
},
),
BASIC_CONFIG,
@@ -1921,14 +1933,18 @@ async def test_openclose_cover(hass):
assert trt.sync_attributes() == {}
assert trt.query_attributes() == {"openPercent": 75}
- calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION)
+ calls_set = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION)
+ calls_open = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_OPEN_COVER)
+
await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 50}, {})
await trt.execute(
trait.COMMAND_OPENCLOSE_RELATIVE, BASIC_DATA, {"openRelativePercent": 50}, {}
)
- assert len(calls) == 2
- assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 50}
- assert calls[1].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 100}
+ assert len(calls_set) == 1
+ assert calls_set[0].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 50}
+
+ assert len(calls_open) == 1
+ assert calls_open[0].data == {ATTR_ENTITY_ID: "cover.bla"}
async def test_openclose_cover_unknown_state(hass):
@@ -2099,6 +2115,7 @@ async def test_openclose_cover_secure(hass, device_class):
assert trt.query_attributes() == {"openPercent": 75}
calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION)
+ calls_close = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_CLOSE_COVER)
# No challenge data
with pytest.raises(error.ChallengeNeeded) as err:
@@ -2124,8 +2141,8 @@ async def test_openclose_cover_secure(hass, device_class):
# no challenge on close
await trt.execute(trait.COMMAND_OPENCLOSE, PIN_DATA, {"openPercent": 0}, {})
- assert len(calls) == 2
- assert calls[1].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 0}
+ assert len(calls_close) == 1
+ assert calls_close[0].data == {ATTR_ENTITY_ID: "cover.bla"}
@pytest.mark.parametrize(
diff --git a/tests/components/google_pubsub/test_init.py b/tests/components/google_pubsub/test_init.py
index c174e45470100f..fc1fecb04ed4d3 100644
--- a/tests/components/google_pubsub/test_init.py
+++ b/tests/components/google_pubsub/test_init.py
@@ -82,7 +82,7 @@ async def test_minimal_config(hass, mock_client):
assert await async_setup_component(hass, google_pubsub.DOMAIN, config)
await hass.async_block_till_done()
assert hass.bus.listen.called
- assert EVENT_STATE_CHANGED == hass.bus.listen.call_args_list[0][0][0]
+ assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED
assert mock_client.PublisherClient.from_service_account_json.call_count == 1
assert (
mock_client.PublisherClient.from_service_account_json.call_args[0][0] == "path"
@@ -109,7 +109,7 @@ async def test_full_config(hass, mock_client):
assert await async_setup_component(hass, google_pubsub.DOMAIN, config)
await hass.async_block_till_done()
assert hass.bus.listen.called
- assert EVENT_STATE_CHANGED == hass.bus.listen.call_args_list[0][0][0]
+ assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED
assert mock_client.PublisherClient.from_service_account_json.call_count == 1
assert (
mock_client.PublisherClient.from_service_account_json.call_args[0][0] == "path"
diff --git a/tests/components/google_travel_time/__init__.py b/tests/components/google_travel_time/__init__.py
new file mode 100644
index 00000000000000..7a24541000145a
--- /dev/null
+++ b/tests/components/google_travel_time/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Google Maps Travel Time integration."""
diff --git a/tests/components/google_travel_time/conftest.py b/tests/components/google_travel_time/conftest.py
new file mode 100644
index 00000000000000..18e16a79e27c10
--- /dev/null
+++ b/tests/components/google_travel_time/conftest.py
@@ -0,0 +1,57 @@
+"""Fixtures for Google Time Travel tests."""
+from unittest.mock import Mock, patch
+
+from googlemaps.exceptions import ApiError
+import pytest
+
+
+@pytest.fixture(name="skip_notifications", autouse=True)
+def skip_notifications_fixture():
+ """Skip notification calls."""
+ with patch("homeassistant.components.persistent_notification.async_create"), patch(
+ "homeassistant.components.persistent_notification.async_dismiss"
+ ):
+ yield
+
+
+@pytest.fixture(name="validate_config_entry")
+def validate_config_entry_fixture():
+ """Return valid config entry."""
+ with patch(
+ "homeassistant.components.google_travel_time.helpers.Client",
+ return_value=Mock(),
+ ), patch(
+ "homeassistant.components.google_travel_time.helpers.distance_matrix",
+ return_value=None,
+ ):
+ yield
+
+
+@pytest.fixture(name="bypass_setup")
+def bypass_setup_fixture():
+ """Bypass entry setup."""
+ with patch(
+ "homeassistant.components.google_travel_time.async_setup_entry",
+ return_value=True,
+ ):
+ yield
+
+
+@pytest.fixture(name="bypass_update")
+def bypass_update_fixture():
+ """Bypass sensor update."""
+ with patch("homeassistant.components.google_travel_time.sensor.distance_matrix"):
+ yield
+
+
+@pytest.fixture(name="invalidate_config_entry")
+def invalidate_config_entry_fixture():
+ """Return invalid config entry."""
+ with patch(
+ "homeassistant.components.google_travel_time.helpers.Client",
+ return_value=Mock(),
+ ), patch(
+ "homeassistant.components.google_travel_time.helpers.distance_matrix",
+ side_effect=ApiError("test"),
+ ):
+ yield
diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py
new file mode 100644
index 00000000000000..64dc77903ff514
--- /dev/null
+++ b/tests/components/google_travel_time/test_config_flow.py
@@ -0,0 +1,297 @@
+"""Test the Google Maps Travel Time config flow."""
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.components.google_travel_time.const import (
+ ARRIVAL_TIME,
+ CONF_ARRIVAL_TIME,
+ CONF_AVOID,
+ CONF_DEPARTURE_TIME,
+ CONF_DESTINATION,
+ CONF_LANGUAGE,
+ CONF_OPTIONS,
+ CONF_ORIGIN,
+ CONF_TIME,
+ CONF_TIME_TYPE,
+ CONF_TRAFFIC_MODEL,
+ CONF_TRANSIT_MODE,
+ CONF_TRANSIT_ROUTING_PREFERENCE,
+ CONF_UNITS,
+ DEFAULT_NAME,
+ DEPARTURE_TIME,
+ DOMAIN,
+)
+from homeassistant.const import (
+ CONF_API_KEY,
+ CONF_MODE,
+ CONF_NAME,
+ CONF_UNIT_SYSTEM_IMPERIAL,
+)
+
+from tests.common import MockConfigEntry
+
+
+async def test_minimum_fields(hass, validate_config_entry, bypass_setup):
+ """Test we get the form."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_API_KEY: "api_key",
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ },
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result2["title"] == f"{DEFAULT_NAME}: location1 -> location2"
+ assert result2["data"] == {
+ CONF_API_KEY: "api_key",
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ }
+
+
+async def test_invalid_config_entry(hass, invalidate_config_entry):
+ """Test we get the form."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_API_KEY: "api_key",
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ },
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "cannot_connect"}
+
+
+async def test_options_flow(hass, validate_config_entry, bypass_update):
+ """Test options flow."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ CONF_API_KEY: "api_key",
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ },
+ options={
+ CONF_MODE: "driving",
+ CONF_ARRIVAL_TIME: "test",
+ CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+ },
+ )
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.options.async_init(entry.entry_id, data=None)
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_MODE: "driving",
+ CONF_LANGUAGE: "en",
+ CONF_AVOID: "tolls",
+ CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_TIME_TYPE: ARRIVAL_TIME,
+ CONF_TIME: "test",
+ CONF_TRAFFIC_MODEL: "best_guess",
+ CONF_TRANSIT_MODE: "train",
+ CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking",
+ },
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == ""
+ assert result["data"] == {
+ CONF_MODE: "driving",
+ CONF_LANGUAGE: "en",
+ CONF_AVOID: "tolls",
+ CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_ARRIVAL_TIME: "test",
+ CONF_TRAFFIC_MODEL: "best_guess",
+ CONF_TRANSIT_MODE: "train",
+ CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking",
+ }
+
+ assert entry.options == {
+ CONF_MODE: "driving",
+ CONF_LANGUAGE: "en",
+ CONF_AVOID: "tolls",
+ CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_ARRIVAL_TIME: "test",
+ CONF_TRAFFIC_MODEL: "best_guess",
+ CONF_TRANSIT_MODE: "train",
+ CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking",
+ }
+
+
+async def test_options_flow_departure_time(hass, validate_config_entry, bypass_update):
+ """Test options flow wiith departure time."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ CONF_API_KEY: "api_key",
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ },
+ )
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.options.async_init(entry.entry_id, data=None)
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_MODE: "driving",
+ CONF_LANGUAGE: "en",
+ CONF_AVOID: "tolls",
+ CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_TIME_TYPE: DEPARTURE_TIME,
+ CONF_TIME: "test",
+ CONF_TRAFFIC_MODEL: "best_guess",
+ CONF_TRANSIT_MODE: "train",
+ CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking",
+ },
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == ""
+ assert result["data"] == {
+ CONF_MODE: "driving",
+ CONF_LANGUAGE: "en",
+ CONF_AVOID: "tolls",
+ CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_DEPARTURE_TIME: "test",
+ CONF_TRAFFIC_MODEL: "best_guess",
+ CONF_TRANSIT_MODE: "train",
+ CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking",
+ }
+
+ assert entry.options == {
+ CONF_MODE: "driving",
+ CONF_LANGUAGE: "en",
+ CONF_AVOID: "tolls",
+ CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_DEPARTURE_TIME: "test",
+ CONF_TRAFFIC_MODEL: "best_guess",
+ CONF_TRANSIT_MODE: "train",
+ CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking",
+ }
+
+
+async def test_dupe_id(hass, validate_config_entry, bypass_setup):
+ """Test setting up the same entry twice fails."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_API_KEY: "test",
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ },
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_API_KEY: "test",
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result2["reason"] == "already_configured"
+
+
+async def test_import_flow(hass, validate_config_entry, bypass_update):
+ """Test import_flow."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={
+ CONF_API_KEY: "api_key",
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ CONF_NAME: "test_name",
+ CONF_OPTIONS: {
+ CONF_MODE: "driving",
+ CONF_LANGUAGE: "en",
+ CONF_AVOID: "tolls",
+ CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_ARRIVAL_TIME: "test",
+ CONF_TRAFFIC_MODEL: "best_guess",
+ CONF_TRANSIT_MODE: "train",
+ CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking",
+ },
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "test_name"
+ assert result["data"] == {
+ CONF_API_KEY: "api_key",
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ CONF_NAME: "test_name",
+ CONF_OPTIONS: {
+ CONF_MODE: "driving",
+ CONF_LANGUAGE: "en",
+ CONF_AVOID: "tolls",
+ CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_ARRIVAL_TIME: "test",
+ CONF_TRAFFIC_MODEL: "best_guess",
+ CONF_TRANSIT_MODE: "train",
+ CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking",
+ },
+ }
+
+ entry = hass.config_entries.async_entries(DOMAIN)[0]
+ assert entry.data == {
+ CONF_API_KEY: "api_key",
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ }
+ assert entry.options == {
+ CONF_MODE: "driving",
+ CONF_LANGUAGE: "en",
+ CONF_AVOID: "tolls",
+ CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_ARRIVAL_TIME: "test",
+ CONF_TRAFFIC_MODEL: "best_guess",
+ CONF_TRANSIT_MODE: "train",
+ CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking",
+ }
diff --git a/tests/components/google_wifi/test_sensor.py b/tests/components/google_wifi/test_sensor.py
index 06ad5e0c3ea087..6f4b4652e76c8e 100644
--- a/tests/components/google_wifi/test_sensor.py
+++ b/tests/components/google_wifi/test_sensor.py
@@ -129,13 +129,13 @@ def test_state(hass, requests_mock):
fake_delay(hass, 2)
sensor.update()
if name == google_wifi.ATTR_LAST_RESTART:
- assert "1969-12-31 00:00:00" == sensor.state
+ assert sensor.state == "1969-12-31 00:00:00"
elif name == google_wifi.ATTR_UPTIME:
- assert 1 == sensor.state
+ assert sensor.state == 1
elif name == google_wifi.ATTR_STATUS:
- assert "Online" == sensor.state
+ assert sensor.state == "Online"
else:
- assert "initial" == sensor.state
+ assert sensor.state == "initial"
def test_update_when_value_is_none(hass, requests_mock):
@@ -158,17 +158,17 @@ def test_update_when_value_changed(hass, requests_mock):
fake_delay(hass, 2)
sensor.update()
if name == google_wifi.ATTR_LAST_RESTART:
- assert "1969-12-30 00:00:00" == sensor.state
+ assert sensor.state == "1969-12-30 00:00:00"
elif name == google_wifi.ATTR_UPTIME:
- assert 2 == sensor.state
+ assert sensor.state == 2
elif name == google_wifi.ATTR_STATUS:
- assert "Offline" == sensor.state
+ assert sensor.state == "Offline"
elif name == google_wifi.ATTR_NEW_VERSION:
- assert "Latest" == sensor.state
+ assert sensor.state == "Latest"
elif name == google_wifi.ATTR_LOCAL_IP:
- assert STATE_UNKNOWN == sensor.state
+ assert sensor.state == STATE_UNKNOWN
else:
- assert "next" == sensor.state
+ assert sensor.state == "next"
def test_when_api_data_missing(hass, requests_mock):
@@ -180,7 +180,7 @@ def test_when_api_data_missing(hass, requests_mock):
sensor = sensor_dict[name]["sensor"]
fake_delay(hass, 2)
sensor.update()
- assert STATE_UNKNOWN == sensor.state
+ assert sensor.state == STATE_UNKNOWN
def test_update_when_unavailable(requests_mock):
diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py
index d30f57f0f33459..1dad262a285672 100644
--- a/tests/components/gpslogger/test_init.py
+++ b/tests/components/gpslogger/test_init.py
@@ -14,6 +14,7 @@
STATE_HOME,
STATE_NOT_HOME,
)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import DATA_DISPATCHER
from homeassistant.setup import async_setup_component
@@ -116,14 +117,14 @@ async def test_enter_and_exit(hass, gpslogger_client, webhook_id):
await hass.async_block_till_done()
assert req.status == HTTP_OK
state_name = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}").state
- assert STATE_HOME == state_name
+ assert state_name == STATE_HOME
# Enter Home again
req = await gpslogger_client.post(url, data=data)
await hass.async_block_till_done()
assert req.status == HTTP_OK
state_name = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}").state
- assert STATE_HOME == state_name
+ assert state_name == STATE_HOME
data["longitude"] = 0
data["latitude"] = 0
@@ -133,12 +134,12 @@ async def test_enter_and_exit(hass, gpslogger_client, webhook_id):
await hass.async_block_till_done()
assert req.status == HTTP_OK
state_name = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}").state
- assert STATE_NOT_HOME == state_name
+ assert state_name == STATE_NOT_HOME
- dev_reg = await hass.helpers.device_registry.async_get_registry()
+ dev_reg = dr.async_get(hass)
assert len(dev_reg.devices) == 1
- ent_reg = await hass.helpers.entity_registry.async_get_registry()
+ ent_reg = er.async_get(hass)
assert len(ent_reg.entities) == 1
@@ -212,7 +213,7 @@ async def test_load_unload_entry(hass, gpslogger_client, webhook_id):
await hass.async_block_till_done()
assert req.status == HTTP_OK
state_name = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}").state
- assert STATE_HOME == state_name
+ assert state_name == STATE_HOME
assert len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) == 1
entry = hass.config_entries.async_entries(DOMAIN)[0]
diff --git a/tests/components/graphite/test_init.py b/tests/components/graphite/test_init.py
index 88be3723936fe9..23a25b1623e551 100644
--- a/tests/components/graphite/test_init.py
+++ b/tests/components/graphite/test_init.py
@@ -24,7 +24,7 @@ class TestGraphite(unittest.TestCase):
def setup_method(self, method):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
- self.gf = graphite.GraphiteFeeder(self.hass, "foo", 123, "ha")
+ self.gf = graphite.GraphiteFeeder(self.hass, "foo", 123, "tcp", "ha")
def teardown_method(self, method):
"""Stop everything that was started."""
@@ -45,10 +45,23 @@ def test_full_config(self, mock_gf, mock_socket):
assert setup_component(self.hass, graphite.DOMAIN, config)
assert mock_gf.call_count == 1
- assert mock_gf.call_args == mock.call(self.hass, "foo", 123, "me")
+ assert mock_gf.call_args == mock.call(self.hass, "foo", 123, "tcp", "me")
assert mock_socket.call_count == 1
assert mock_socket.call_args == mock.call(socket.AF_INET, socket.SOCK_STREAM)
+ @patch("socket.socket")
+ @patch("homeassistant.components.graphite.GraphiteFeeder")
+ def test_full_udp_config(self, mock_gf, mock_socket):
+ """Test setup with full configuration and UDP protocol."""
+ config = {
+ "graphite": {"host": "foo", "port": 123, "protocol": "udp", "prefix": "me"}
+ }
+
+ assert setup_component(self.hass, graphite.DOMAIN, config)
+ assert mock_gf.call_count == 1
+ assert mock_gf.call_args == mock.call(self.hass, "foo", 123, "udp", "me")
+ assert mock_socket.call_count == 0
+
@patch("socket.socket")
@patch("homeassistant.components.graphite.GraphiteFeeder")
def test_config_port(self, mock_gf, mock_socket):
@@ -63,7 +76,7 @@ def test_config_port(self, mock_gf, mock_socket):
def test_subscribe(self):
"""Test the subscription."""
fake_hass = mock.MagicMock()
- gf = graphite.GraphiteFeeder(fake_hass, "foo", 123, "ha")
+ gf = graphite.GraphiteFeeder(fake_hass, "foo", 123, "tcp", "ha")
fake_hass.bus.listen_once.has_calls(
[
mock.call(EVENT_HOMEASSISTANT_START, gf.start_listen),
@@ -207,11 +220,12 @@ def fake_get():
runs.append(1)
return event
- with mock.patch.object(self.gf, "_queue") as mock_queue:
- with mock.patch.object(self.gf, "_report_attributes") as mock_r:
- mock_queue.get.side_effect = fake_get
- self.gf.run()
- # Twice for two events, once for the stop
- assert mock_queue.task_done.call_count == 3
- assert mock_r.call_count == 1
- assert mock_r.call_args == mock.call("entity", event.data["new_state"])
+ with mock.patch.object(self.gf, "_queue") as mock_queue, mock.patch.object(
+ self.gf, "_report_attributes"
+ ) as mock_r:
+ mock_queue.get.side_effect = fake_get
+ self.gf.run()
+ # Twice for two events, once for the stop
+ assert mock_queue.task_done.call_count == 3
+ assert mock_r.call_count == 1
+ assert mock_r.call_args == mock.call("entity", event.data["new_state"])
diff --git a/tests/components/griddy/__init__.py b/tests/components/griddy/__init__.py
deleted file mode 100644
index 415ddc3ba5cd9c..00000000000000
--- a/tests/components/griddy/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Tests for the Griddy Power integration."""
diff --git a/tests/components/griddy/test_config_flow.py b/tests/components/griddy/test_config_flow.py
deleted file mode 100644
index cfc2b23a8ed95f..00000000000000
--- a/tests/components/griddy/test_config_flow.py
+++ /dev/null
@@ -1,56 +0,0 @@
-"""Test the Griddy Power config flow."""
-import asyncio
-from unittest.mock import MagicMock, patch
-
-from homeassistant import config_entries, setup
-from homeassistant.components.griddy.const import DOMAIN
-
-
-async def test_form(hass):
- """Test we get the form."""
- await setup.async_setup_component(hass, "persistent_notification", {})
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": config_entries.SOURCE_USER}
- )
- assert result["type"] == "form"
- assert result["errors"] == {}
-
- with patch(
- "homeassistant.components.griddy.config_flow.AsyncGriddy.async_getnow",
- return_value=MagicMock(),
- ), patch(
- "homeassistant.components.griddy.async_setup", return_value=True
- ) as mock_setup, patch(
- "homeassistant.components.griddy.async_setup_entry",
- return_value=True,
- ) as mock_setup_entry:
- result2 = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {"loadzone": "LZ_HOUSTON"},
- )
- await hass.async_block_till_done()
-
- assert result2["type"] == "create_entry"
- assert result2["title"] == "Load Zone LZ_HOUSTON"
- assert result2["data"] == {"loadzone": "LZ_HOUSTON"}
- assert len(mock_setup.mock_calls) == 1
- assert len(mock_setup_entry.mock_calls) == 1
-
-
-async def test_form_cannot_connect(hass):
- """Test we handle cannot connect error."""
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": config_entries.SOURCE_USER}
- )
-
- with patch(
- "homeassistant.components.griddy.config_flow.AsyncGriddy.async_getnow",
- side_effect=asyncio.TimeoutError,
- ):
- result2 = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {"loadzone": "LZ_NORTH"},
- )
-
- assert result2["type"] == "form"
- assert result2["errors"] == {"base": "cannot_connect"}
diff --git a/tests/components/griddy/test_sensor.py b/tests/components/griddy/test_sensor.py
deleted file mode 100644
index 46f8d238c49d65..00000000000000
--- a/tests/components/griddy/test_sensor.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""The sensor tests for the griddy platform."""
-import json
-import os
-from unittest.mock import patch
-
-from griddypower.async_api import GriddyPriceData
-
-from homeassistant.components.griddy import CONF_LOADZONE, DOMAIN
-from homeassistant.setup import async_setup_component
-
-from tests.common import load_fixture
-
-
-async def _load_json_fixture(hass, path):
- fixture = await hass.async_add_executor_job(
- load_fixture, os.path.join("griddy", path)
- )
- return json.loads(fixture)
-
-
-def _mock_get_config():
- """Return a default griddy config."""
- return {DOMAIN: {CONF_LOADZONE: "LZ_HOUSTON"}}
-
-
-async def test_houston_loadzone(hass):
- """Test creation of the houston load zone."""
-
- getnow_json = await _load_json_fixture(hass, "getnow.json")
- griddy_price_data = GriddyPriceData(getnow_json)
- with patch(
- "homeassistant.components.griddy.AsyncGriddy.async_getnow",
- return_value=griddy_price_data,
- ):
- assert await async_setup_component(hass, DOMAIN, _mock_get_config())
- await hass.async_block_till_done()
-
- sensor_lz_houston_price_now = hass.states.get("sensor.lz_houston_price_now")
- assert sensor_lz_houston_price_now.state == "1.269"
diff --git a/tests/components/group/conftest.py b/tests/components/group/conftest.py
index 6fe34aca91cae1..e26e98598e6e73 100644
--- a/tests/components/group/conftest.py
+++ b/tests/components/group/conftest.py
@@ -1,2 +1,2 @@
"""group conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py
index 627e3c5bbe00d0..fff1526b7119f3 100644
--- a/tests/components/group/test_init.py
+++ b/tests/components/group/test_init.py
@@ -38,7 +38,7 @@ async def test_setup_group_with_mixed_groupable_states(hass):
await hass.async_block_till_done()
- assert STATE_ON == hass.states.get(f"{group.DOMAIN}.person_and_light").state
+ assert hass.states.get(f"{group.DOMAIN}.person_and_light").state == STATE_ON
async def test_setup_group_with_a_non_existing_state(hass):
@@ -51,7 +51,7 @@ async def test_setup_group_with_a_non_existing_state(hass):
hass, "light_and_nothing", ["light.Bowl", "non.existing"]
)
- assert STATE_ON == grp.state
+ assert grp.state == STATE_ON
async def test_setup_group_with_non_groupable_states(hass):
@@ -90,7 +90,7 @@ async def test_monitor_group(hass):
assert test_group.entity_id in hass.states.async_entity_ids()
group_state = hass.states.get(test_group.entity_id)
- assert STATE_ON == group_state.state
+ assert group_state.state == STATE_ON
assert group_state.attributes.get(group.ATTR_AUTO)
@@ -108,7 +108,7 @@ async def test_group_turns_off_if_all_off(hass):
await hass.async_block_till_done()
group_state = hass.states.get(test_group.entity_id)
- assert STATE_OFF == group_state.state
+ assert group_state.state == STATE_OFF
async def test_group_turns_on_if_all_are_off_and_one_turns_on(hass):
@@ -127,7 +127,7 @@ async def test_group_turns_on_if_all_are_off_and_one_turns_on(hass):
await hass.async_block_till_done()
group_state = hass.states.get(test_group.entity_id)
- assert STATE_ON == group_state.state
+ assert group_state.state == STATE_ON
async def test_allgroup_stays_off_if_all_are_off_and_one_turns_on(hass):
@@ -146,7 +146,7 @@ async def test_allgroup_stays_off_if_all_are_off_and_one_turns_on(hass):
await hass.async_block_till_done()
group_state = hass.states.get(test_group.entity_id)
- assert STATE_OFF == group_state.state
+ assert group_state.state == STATE_OFF
async def test_allgroup_turn_on_if_last_turns_on(hass):
@@ -165,7 +165,7 @@ async def test_allgroup_turn_on_if_last_turns_on(hass):
await hass.async_block_till_done()
group_state = hass.states.get(test_group.entity_id)
- assert STATE_ON == group_state.state
+ assert group_state.state == STATE_ON
async def test_expand_entity_ids(hass):
@@ -287,7 +287,7 @@ async def test_group_being_init_before_first_tracked_state_is_set_to_on(hass):
await hass.async_block_till_done()
group_state = hass.states.get(test_group.entity_id)
- assert STATE_ON == group_state.state
+ assert group_state.state == STATE_ON
async def test_group_being_init_before_first_tracked_state_is_set_to_off(hass):
@@ -306,7 +306,7 @@ async def test_group_being_init_before_first_tracked_state_is_set_to_off(hass):
await hass.async_block_till_done()
group_state = hass.states.get(test_group.entity_id)
- assert STATE_OFF == group_state.state
+ assert group_state.state == STATE_OFF
async def test_groups_get_unique_names(hass):
@@ -385,7 +385,7 @@ async def test_group_updated_after_device_tracker_zone_change(hass):
hass.states.async_set("device_tracker.Adam", "cool_state_not_home")
await hass.async_block_till_done()
- assert STATE_NOT_HOME == hass.states.get(f"{group.DOMAIN}.peeps").state
+ assert hass.states.get(f"{group.DOMAIN}.peeps").state == STATE_NOT_HOME
async def test_is_on(hass):
@@ -517,20 +517,20 @@ async def test_setup(hass):
await hass.async_block_till_done()
group_state = hass.states.get(f"{group.DOMAIN}.created_group")
- assert STATE_ON == group_state.state
+ assert group_state.state == STATE_ON
assert {test_group.entity_id, "light.bowl"} == set(
group_state.attributes["entity_id"]
)
assert group_state.attributes.get(group.ATTR_AUTO) is None
- assert "mdi:work" == group_state.attributes.get(ATTR_ICON)
- assert 3 == group_state.attributes.get(group.ATTR_ORDER)
+ assert group_state.attributes.get(ATTR_ICON) == "mdi:work"
+ assert group_state.attributes.get(group.ATTR_ORDER) == 3
group_state = hass.states.get(f"{group.DOMAIN}.test_group")
- assert STATE_UNKNOWN == group_state.state
- assert {"sensor.happy", "hello.world"} == set(group_state.attributes["entity_id"])
+ assert group_state.state == STATE_UNKNOWN
+ assert set(group_state.attributes["entity_id"]) == {"sensor.happy", "hello.world"}
assert group_state.attributes.get(group.ATTR_AUTO) is None
assert group_state.attributes.get(ATTR_ICON) is None
- assert 0 == group_state.attributes.get(group.ATTR_ORDER)
+ assert group_state.attributes.get(group.ATTR_ORDER) == 0
async def test_service_group_services(hass):
@@ -579,7 +579,7 @@ async def test_service_group_set_group_remove_group(hass):
assert group_state.attributes[group.ATTR_AUTO]
assert group_state.attributes["friendly_name"] == "Test2"
assert group_state.attributes["icon"] == "mdi:camera"
- assert sorted(list(group_state.attributes["entity_id"])) == sorted(
+ assert sorted(group_state.attributes["entity_id"]) == sorted(
["test.entity_bla1", "test.entity_id2"]
)
diff --git a/tests/components/habitica/__init__.py b/tests/components/habitica/__init__.py
new file mode 100644
index 00000000000000..a7f62afff8f2bf
--- /dev/null
+++ b/tests/components/habitica/__init__.py
@@ -0,0 +1 @@
+"""Tests for the habitica integration."""
diff --git a/tests/components/habitica/test_config_flow.py b/tests/components/habitica/test_config_flow.py
new file mode 100644
index 00000000000000..d02a9031d631a7
--- /dev/null
+++ b/tests/components/habitica/test_config_flow.py
@@ -0,0 +1,134 @@
+"""Test the habitica config flow."""
+from unittest.mock import AsyncMock, MagicMock, patch
+
+from aiohttp import ClientResponseError
+
+from homeassistant import config_entries, setup
+from homeassistant.components.habitica.const import DEFAULT_URL, DOMAIN
+
+from tests.common import MockConfigEntry
+
+
+async def test_form(hass):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ mock_obj = MagicMock()
+ mock_obj.user.get = AsyncMock()
+
+ with patch(
+ "homeassistant.components.habitica.config_flow.HabitipyAsync",
+ return_value=mock_obj,
+ ), patch(
+ "homeassistant.components.habitica.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.habitica.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"api_user": "test-api-user", "api_key": "test-api-key"},
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Default username"
+ assert result2["data"] == {
+ "url": DEFAULT_URL,
+ "api_user": "test-api-user",
+ "api_key": "test-api-key",
+ }
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_invalid_credentials(hass):
+ """Test we handle invalid credentials error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ mock_obj = MagicMock()
+ mock_obj.user.get = AsyncMock(side_effect=ClientResponseError(MagicMock(), ()))
+
+ with patch(
+ "homeassistant.components.habitica.config_flow.HabitipyAsync",
+ return_value=mock_obj,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "url": DEFAULT_URL,
+ "api_user": "test-api-user",
+ "api_key": "test-api-key",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "invalid_credentials"}
+
+
+async def test_form_unexpected_exception(hass):
+ """Test we handle unexpected exception error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ mock_obj = MagicMock()
+ mock_obj.user.get = AsyncMock(side_effect=Exception)
+
+ with patch(
+ "homeassistant.components.habitica.config_flow.HabitipyAsync",
+ return_value=mock_obj,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "url": DEFAULT_URL,
+ "api_user": "test-api-user",
+ "api_key": "test-api-key",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "unknown"}
+
+
+async def test_manual_flow_config_exist(hass):
+ """Test config flow discovers only already configured config."""
+ MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="test-api-user",
+ data={"api_user": "test-api-user", "api_key": "test-api-key"},
+ ).add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ mock_obj = MagicMock()
+ mock_obj.user.get = AsyncMock(return_value={"api_user": "test-api-user"})
+
+ with patch(
+ "homeassistant.components.habitica.config_flow.HabitipyAsync",
+ return_value=mock_obj,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "url": DEFAULT_URL,
+ "api_user": "test-api-user",
+ "api_key": "test-api-key",
+ },
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
diff --git a/tests/components/habitica/test_init.py b/tests/components/habitica/test_init.py
new file mode 100644
index 00000000000000..5f7e4b7fbf509d
--- /dev/null
+++ b/tests/components/habitica/test_init.py
@@ -0,0 +1,36 @@
+"""Test the habitica init module."""
+from homeassistant.components.habitica.const import (
+ DEFAULT_URL,
+ DOMAIN,
+ SERVICE_API_CALL,
+)
+
+from tests.common import MockConfigEntry
+
+
+async def test_entry_setup_unload(hass, aioclient_mock):
+ """Test integration setup and unload."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="test-api-user",
+ data={
+ "api_user": "test-api-user",
+ "api_key": "test-api-key",
+ "url": DEFAULT_URL,
+ },
+ )
+ entry.add_to_hass(hass)
+
+ aioclient_mock.get(
+ "https://habitica.com/api/v3/user",
+ json={"data": {"api_user": "test-api-user", "profile": {"name": "test_user"}}},
+ )
+
+ assert await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert hass.services.has_service(DOMAIN, SERVICE_API_CALL)
+
+ assert await hass.config_entries.async_unload(entry.entry_id)
+
+ assert not hass.services.has_service(DOMAIN, SERVICE_API_CALL)
diff --git a/tests/components/harmony/conftest.py b/tests/components/harmony/conftest.py
index e758a2795a94e3..5072eb0deb9bbb 100644
--- a/tests/components/harmony/conftest.py
+++ b/tests/components/harmony/conftest.py
@@ -1,5 +1,4 @@
"""Fixtures for harmony tests."""
-import logging
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
from aioharmony.const import ClientCallbackType
@@ -7,21 +6,20 @@
from homeassistant.components.harmony.const import ACTIVITY_POWER_OFF
-_LOGGER = logging.getLogger(__name__)
-
-WATCH_TV_ACTIVITY_ID = 123
-PLAY_MUSIC_ACTIVITY_ID = 456
+from .const import NILE_TV_ACTIVITY_ID, PLAY_MUSIC_ACTIVITY_ID, WATCH_TV_ACTIVITY_ID
ACTIVITIES_TO_IDS = {
ACTIVITY_POWER_OFF: -1,
"Watch TV": WATCH_TV_ACTIVITY_ID,
"Play Music": PLAY_MUSIC_ACTIVITY_ID,
+ "Nile-TV": NILE_TV_ACTIVITY_ID,
}
IDS_TO_ACTIVITIES = {
-1: ACTIVITY_POWER_OFF,
WATCH_TV_ACTIVITY_ID: "Watch TV",
PLAY_MUSIC_ACTIVITY_ID: "Play Music",
+ NILE_TV_ACTIVITY_ID: "Nile-TV",
}
TV_DEVICE_ID = 1234
@@ -39,16 +37,19 @@
class FakeHarmonyClient:
"""FakeHarmonyClient to mock away network calls."""
- def __init__(
+ def initialize(
self, ip_address: str = "", callbacks: ClientCallbackType = MagicMock()
):
- """Initialize FakeHarmonyClient class."""
+ """Initialize FakeHarmonyClient class to capture callbacks."""
self._activity_name = "Watch TV"
self.close = AsyncMock()
self.send_commands = AsyncMock()
self.change_channel = AsyncMock()
self.sync = AsyncMock()
self._callbacks = callbacks
+ self.fw_version = "123.456"
+
+ return self
async def connect(self):
"""Connect and call the appropriate callbacks."""
@@ -110,6 +111,7 @@ def hub_config(self):
return_value=[
{"name": "Watch TV", "id": WATCH_TV_ACTIVITY_ID},
{"name": "Play Music", "id": PLAY_MUSIC_ACTIVITY_ID},
+ {"name": "Nile-TV", "id": NILE_TV_ACTIVITY_ID},
]
)
type(config).devices = PropertyMock(
@@ -120,20 +122,38 @@ def hub_config(self):
type(config).config = PropertyMock(
return_value={
"activity": [
+ {"id": 10000, "label": None},
+ {"id": -1, "label": "PowerOff"},
{"id": WATCH_TV_ACTIVITY_ID, "label": "Watch TV"},
{"id": PLAY_MUSIC_ACTIVITY_ID, "label": "Play Music"},
+ {"id": NILE_TV_ACTIVITY_ID, "label": "Nile-TV"},
]
}
)
return config
+ def mock_reconnection(self):
+ """Simulate reconnection to the hub."""
+ self._callbacks.connect(None)
+
+ def mock_disconnection(self):
+ """Simulate disconnection to the hub."""
+ self._callbacks.disconnect(None)
+
@pytest.fixture()
-def mock_hc():
- """Create a mock HarmonyClient."""
+def harmony_client():
+ """Create the FakeHarmonyClient instance."""
+ return FakeHarmonyClient()
+
+
+@pytest.fixture()
+def mock_hc(harmony_client):
+ """Patch the real HarmonyClient with initialization side effect."""
+
with patch(
"homeassistant.components.harmony.data.HarmonyClient",
- side_effect=FakeHarmonyClient,
+ side_effect=harmony_client.initialize,
) as fake:
yield fake
diff --git a/tests/components/harmony/const.py b/tests/components/harmony/const.py
index 1911ea949aff07..488fe30dec36b1 100644
--- a/tests/components/harmony/const.py
+++ b/tests/components/harmony/const.py
@@ -4,3 +4,8 @@
ENTITY_REMOTE = "remote.guest_room"
ENTITY_WATCH_TV = "switch.guest_room_watch_tv"
ENTITY_PLAY_MUSIC = "switch.guest_room_play_music"
+ENTITY_NILE_TV = "switch.guest_room_nile_tv"
+
+WATCH_TV_ACTIVITY_ID = 123
+PLAY_MUSIC_ACTIVITY_ID = 456
+NILE_TV_ACTIVITY_ID = 789
diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py
index 52ef71fc8bcd19..7f651890868142 100644
--- a/tests/components/harmony/test_config_flow.py
+++ b/tests/components/harmony/test_config_flow.py
@@ -74,6 +74,10 @@ async def test_form_ssdp(hass):
"host": "Harmony Hub",
"name": "192.168.1.12",
}
+ progress = hass.config_entries.flow.async_progress()
+ assert len(progress) == 1
+ assert progress[0]["flow_id"] == result["flow_id"]
+ assert progress[0]["context"]["confirm_only"] is True
with patch(
"homeassistant.components.harmony.util.HarmonyAPI",
@@ -153,7 +157,7 @@ async def test_form_cannot_connect(hass):
assert result2["errors"] == {"base": "cannot_connect"}
-async def test_options_flow(hass, mock_hc):
+async def test_options_flow(hass, mock_hc, mock_write_config):
"""Test config flow options."""
config_entry = MockConfigEntry(
domain=DOMAIN,
diff --git a/tests/components/harmony/test_connection_changes.py b/tests/components/harmony/test_connection_changes.py
deleted file mode 100644
index 15d462988555a0..00000000000000
--- a/tests/components/harmony/test_connection_changes.py
+++ /dev/null
@@ -1,67 +0,0 @@
-"""Test the Logitech Harmony Hub entities with connection state changes."""
-
-from datetime import timedelta
-
-from homeassistant.components.harmony.const import DOMAIN
-from homeassistant.const import (
- CONF_HOST,
- CONF_NAME,
- STATE_OFF,
- STATE_ON,
- STATE_UNAVAILABLE,
-)
-from homeassistant.util import utcnow
-
-from .const import ENTITY_PLAY_MUSIC, ENTITY_REMOTE, ENTITY_WATCH_TV, HUB_NAME
-
-from tests.common import MockConfigEntry, async_fire_time_changed
-
-
-async def test_connection_state_changes(mock_hc, hass, mock_write_config):
- """Ensure connection changes are reflected in the switch states."""
- entry = MockConfigEntry(
- domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
- )
-
- entry.add_to_hass(hass)
- await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
- data = hass.data[DOMAIN][entry.entry_id]
-
- # mocks start with current activity == Watch TV
- assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
- assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
- assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
-
- data._disconnected()
- await hass.async_block_till_done()
-
- # Entities do not immediately show as unavailable
- assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
- assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
- assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
-
- future_time = utcnow() + timedelta(seconds=10)
- async_fire_time_changed(hass, future_time)
- await hass.async_block_till_done()
- assert hass.states.is_state(ENTITY_REMOTE, STATE_UNAVAILABLE)
- assert hass.states.is_state(ENTITY_WATCH_TV, STATE_UNAVAILABLE)
- assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_UNAVAILABLE)
-
- data._connected()
- await hass.async_block_till_done()
-
- assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
- assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
- assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
-
- data._disconnected()
- data._connected()
- future_time = utcnow() + timedelta(seconds=10)
- async_fire_time_changed(hass, future_time)
-
- await hass.async_block_till_done()
- assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
- assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
- assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
diff --git a/tests/components/harmony/test_init.py b/tests/components/harmony/test_init.py
new file mode 100644
index 00000000000000..29a1ff26b82191
--- /dev/null
+++ b/tests/components/harmony/test_init.py
@@ -0,0 +1,72 @@
+"""Test init of Logitch Harmony Hub integration."""
+from homeassistant.components.harmony.const import DOMAIN
+from homeassistant.const import CONF_HOST, CONF_NAME
+from homeassistant.helpers import entity_registry as er
+from homeassistant.setup import async_setup_component
+
+from .const import (
+ ENTITY_NILE_TV,
+ ENTITY_PLAY_MUSIC,
+ ENTITY_WATCH_TV,
+ HUB_NAME,
+ NILE_TV_ACTIVITY_ID,
+ PLAY_MUSIC_ACTIVITY_ID,
+ WATCH_TV_ACTIVITY_ID,
+)
+
+from tests.common import MockConfigEntry, mock_registry
+
+
+async def test_unique_id_migration(mock_hc, hass, mock_write_config):
+ """Test migration of switch unique ids to stable ones."""
+ entry = MockConfigEntry(
+ domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
+ )
+
+ entry.add_to_hass(hass)
+ mock_registry(
+ hass,
+ {
+ # old format
+ ENTITY_WATCH_TV: er.RegistryEntry(
+ entity_id=ENTITY_WATCH_TV,
+ unique_id="123443-Watch TV",
+ platform="harmony",
+ config_entry_id=entry.entry_id,
+ ),
+ # old format, activity name with -
+ ENTITY_NILE_TV: er.RegistryEntry(
+ entity_id=ENTITY_NILE_TV,
+ unique_id="123443-Nile-TV",
+ platform="harmony",
+ config_entry_id=entry.entry_id,
+ ),
+ # new format
+ ENTITY_PLAY_MUSIC: er.RegistryEntry(
+ entity_id=ENTITY_PLAY_MUSIC,
+ unique_id=f"activity_{PLAY_MUSIC_ACTIVITY_ID}",
+ platform="harmony",
+ config_entry_id=entry.entry_id,
+ ),
+ # old entity which no longer has a matching activity on the hub. skipped.
+ "switch.some_other_activity": er.RegistryEntry(
+ entity_id="switch.some_other_activity",
+ unique_id="123443-Some Other Activity",
+ platform="harmony",
+ config_entry_id=entry.entry_id,
+ ),
+ },
+ )
+ assert await async_setup_component(hass, DOMAIN, {})
+ await hass.async_block_till_done()
+
+ ent_reg = er.async_get(hass)
+
+ switch_tv = ent_reg.async_get(ENTITY_WATCH_TV)
+ assert switch_tv.unique_id == f"activity_{WATCH_TV_ACTIVITY_ID}"
+
+ switch_nile = ent_reg.async_get(ENTITY_NILE_TV)
+ assert switch_nile.unique_id == f"activity_{NILE_TV_ACTIVITY_ID}"
+
+ switch_music = ent_reg.async_get(ENTITY_PLAY_MUSIC)
+ assert switch_music.unique_id == f"activity_{PLAY_MUSIC_ACTIVITY_ID}"
diff --git a/tests/components/harmony/test_commands.py b/tests/components/harmony/test_remote.py
similarity index 62%
rename from tests/components/harmony/test_commands.py
rename to tests/components/harmony/test_remote.py
index 62056a08e1dff2..8c4d67e1117da0 100644
--- a/tests/components/harmony/test_commands.py
+++ b/tests/components/harmony/test_remote.py
@@ -1,4 +1,6 @@
-"""Test sending commands to the Harmony Hub remote."""
+"""Test the Logitech Harmony Hub remote."""
+
+from datetime import timedelta
from aioharmony.const import SendCommandDevice
@@ -9,6 +11,7 @@
)
from homeassistant.components.harmony.remote import ATTR_CHANNEL, ATTR_DELAY_SECS
from homeassistant.components.remote import (
+ ATTR_ACTIVITY,
ATTR_COMMAND,
ATTR_DEVICE,
ATTR_NUM_REPEATS,
@@ -16,18 +19,136 @@
DEFAULT_HOLD_SECS,
DOMAIN as REMOTE_DOMAIN,
SERVICE_SEND_COMMAND,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+)
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ CONF_HOST,
+ CONF_NAME,
+ STATE_OFF,
+ STATE_ON,
+ STATE_UNAVAILABLE,
)
-from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME
+from homeassistant.util import utcnow
-from .conftest import TV_DEVICE_ID, TV_DEVICE_NAME
-from .const import ENTITY_REMOTE, HUB_NAME
+from .conftest import ACTIVITIES_TO_IDS, TV_DEVICE_ID, TV_DEVICE_NAME
+from .const import ENTITY_PLAY_MUSIC, ENTITY_REMOTE, ENTITY_WATCH_TV, HUB_NAME
-from tests.common import MockConfigEntry
+from tests.common import MockConfigEntry, async_fire_time_changed
PLAY_COMMAND = "Play"
STOP_COMMAND = "Stop"
+async def test_connection_state_changes(
+ harmony_client, mock_hc, hass, mock_write_config
+):
+ """Ensure connection changes are reflected in the remote state."""
+ entry = MockConfigEntry(
+ domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
+ )
+
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ # mocks start with current activity == Watch TV
+ assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+
+ harmony_client.mock_disconnection()
+ await hass.async_block_till_done()
+
+ # Entities do not immediately show as unavailable
+ assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+
+ future_time = utcnow() + timedelta(seconds=10)
+ async_fire_time_changed(hass, future_time)
+ await hass.async_block_till_done()
+ assert hass.states.is_state(ENTITY_REMOTE, STATE_UNAVAILABLE)
+
+ harmony_client.mock_reconnection()
+ await hass.async_block_till_done()
+
+ assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+
+ harmony_client.mock_disconnection()
+ harmony_client.mock_reconnection()
+ future_time = utcnow() + timedelta(seconds=10)
+ async_fire_time_changed(hass, future_time)
+
+ await hass.async_block_till_done()
+ assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+
+
+async def test_remote_toggles(mock_hc, hass, mock_write_config):
+ """Ensure calls to the remote also updates the switches."""
+ entry = MockConfigEntry(
+ domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
+ )
+
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ # mocks start with current activity == Watch TV
+ assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+ assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
+ assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
+
+ # turn off remote
+ await hass.services.async_call(
+ REMOTE_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_REMOTE},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.is_state(ENTITY_REMOTE, STATE_OFF)
+ assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF)
+ assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
+
+ # turn on remote, restoring the last activity
+ await hass.services.async_call(
+ REMOTE_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ENTITY_REMOTE},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+ assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
+ assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
+
+ # send new activity command, with activity name
+ await hass.services.async_call(
+ REMOTE_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_ACTIVITY: "Play Music"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+ assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF)
+ assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_ON)
+
+ # send new activity command, with activity id
+ await hass.services.async_call(
+ REMOTE_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_ACTIVITY: ACTIVITIES_TO_IDS["Watch TV"]},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+ assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
+ assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
+
+
async def test_async_send_command(mock_hc, hass, mock_write_config):
"""Ensure calls to send remote commands properly propagate to devices."""
entry = MockConfigEntry(
diff --git a/tests/components/harmony/test_activity_changes.py b/tests/components/harmony/test_switch.py
similarity index 64%
rename from tests/components/harmony/test_activity_changes.py
rename to tests/components/harmony/test_switch.py
index ff76c3ce998bac..1940c54e1123a5 100644
--- a/tests/components/harmony/test_activity_changes.py
+++ b/tests/components/harmony/test_switch.py
@@ -1,9 +1,8 @@
"""Test the Logitech Harmony Hub activity switches."""
-import logging
+from datetime import timedelta
from homeassistant.components.harmony.const import DOMAIN
-from homeassistant.components.remote import ATTR_ACTIVITY, DOMAIN as REMOTE_DOMAIN
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
@@ -15,18 +14,19 @@
CONF_NAME,
STATE_OFF,
STATE_ON,
+ STATE_UNAVAILABLE,
)
+from homeassistant.util import utcnow
-from .conftest import ACTIVITIES_TO_IDS
from .const import ENTITY_PLAY_MUSIC, ENTITY_REMOTE, ENTITY_WATCH_TV, HUB_NAME
-from tests.common import MockConfigEntry
+from tests.common import MockConfigEntry, async_fire_time_changed
-_LOGGER = logging.getLogger(__name__)
-
-async def test_switch_toggles(mock_hc, hass, mock_write_config):
- """Ensure calls to the switch modify the harmony state."""
+async def test_connection_state_changes(
+ harmony_client, mock_hc, hass, mock_write_config
+):
+ """Ensure connection changes are reflected in the switch states."""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
)
@@ -36,31 +36,40 @@ async def test_switch_toggles(mock_hc, hass, mock_write_config):
await hass.async_block_till_done()
# mocks start with current activity == Watch TV
- assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
- # turn off watch tv switch
- await _toggle_switch_and_wait(hass, SERVICE_TURN_OFF, ENTITY_WATCH_TV)
- assert hass.states.is_state(ENTITY_REMOTE, STATE_OFF)
- assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF)
+ harmony_client.mock_disconnection()
+ await hass.async_block_till_done()
+
+ # Entities do not immediately show as unavailable
+ assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
- # turn on play music switch
- await _toggle_switch_and_wait(hass, SERVICE_TURN_ON, ENTITY_PLAY_MUSIC)
- assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
- assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF)
- assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_ON)
+ future_time = utcnow() + timedelta(seconds=10)
+ async_fire_time_changed(hass, future_time)
+ await hass.async_block_till_done()
+ assert hass.states.is_state(ENTITY_WATCH_TV, STATE_UNAVAILABLE)
+ assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_UNAVAILABLE)
+
+ harmony_client.mock_reconnection()
+ await hass.async_block_till_done()
- # turn on watch tv switch
- await _toggle_switch_and_wait(hass, SERVICE_TURN_ON, ENTITY_WATCH_TV)
- assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
+ harmony_client.mock_disconnection()
+ harmony_client.mock_reconnection()
+ future_time = utcnow() + timedelta(seconds=10)
+ async_fire_time_changed(hass, future_time)
-async def test_remote_toggles(mock_hc, hass, mock_write_config):
- """Ensure calls to the remote also updates the switches."""
+ await hass.async_block_till_done()
+ assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
+ assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
+
+
+async def test_switch_toggles(mock_hc, hass, mock_write_config):
+ """Ensure calls to the switch modify the harmony state."""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
)
@@ -74,54 +83,20 @@ async def test_remote_toggles(mock_hc, hass, mock_write_config):
assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
- # turn off remote
- await hass.services.async_call(
- REMOTE_DOMAIN,
- SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: ENTITY_REMOTE},
- blocking=True,
- )
- await hass.async_block_till_done()
-
+ # turn off watch tv switch
+ await _toggle_switch_and_wait(hass, SERVICE_TURN_OFF, ENTITY_WATCH_TV)
assert hass.states.is_state(ENTITY_REMOTE, STATE_OFF)
assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF)
assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
- # turn on remote, restoring the last activity
- await hass.services.async_call(
- REMOTE_DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: ENTITY_REMOTE},
- blocking=True,
- )
- await hass.async_block_till_done()
-
- assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
- assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
- assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
-
- # send new activity command, with activity name
- await hass.services.async_call(
- REMOTE_DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_ACTIVITY: "Play Music"},
- blocking=True,
- )
- await hass.async_block_till_done()
-
+ # turn on play music switch
+ await _toggle_switch_and_wait(hass, SERVICE_TURN_ON, ENTITY_PLAY_MUSIC)
assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF)
assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_ON)
- # send new activity command, with activity id
- await hass.services.async_call(
- REMOTE_DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_ACTIVITY: ACTIVITIES_TO_IDS["Watch TV"]},
- blocking=True,
- )
- await hass.async_block_till_done()
-
+ # turn on watch tv switch
+ await _toggle_switch_and_wait(hass, SERVICE_TURN_ON, ENTITY_WATCH_TV)
assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
diff --git a/tests/components/hassio/__init__.py b/tests/components/hassio/__init__.py
index ad9829f17ff3bb..f3f35b62562a75 100644
--- a/tests/components/hassio/__init__.py
+++ b/tests/components/hassio/__init__.py
@@ -1,3 +1,48 @@
"""Tests for Hass.io component."""
+import pytest
HASSIO_TOKEN = "123456"
+
+
+@pytest.fixture(autouse=True)
+def mock_all(aioclient_mock):
+ """Mock all setup requests."""
+ aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
+ aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"})
+ aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"})
+ aioclient_mock.get(
+ "http://127.0.0.1/info",
+ json={
+ "result": "ok",
+ "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None},
+ },
+ )
+ aioclient_mock.get(
+ "http://127.0.0.1/host/info",
+ json={
+ "result": "ok",
+ "data": {
+ "result": "ok",
+ "data": {
+ "chassis": "vm",
+ "operating_system": "Debian GNU/Linux 10 (buster)",
+ "kernel": "4.19.0-6-amd64",
+ },
+ },
+ },
+ )
+ aioclient_mock.get(
+ "http://127.0.0.1/core/info",
+ json={"result": "ok", "data": {"version_latest": "1.0.0"}},
+ )
+ aioclient_mock.get(
+ "http://127.0.0.1/os/info",
+ json={"result": "ok", "data": {"version_latest": "1.0.0"}},
+ )
+ aioclient_mock.get(
+ "http://127.0.0.1/supervisor/info",
+ json={"result": "ok", "data": {"version_latest": "1.0.0"}},
+ )
+ aioclient_mock.get(
+ "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
+ )
diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py
index 1442d133f1e0e7..efa983c344032d 100644
--- a/tests/components/hassio/conftest.py
+++ b/tests/components/hassio/conftest.py
@@ -17,7 +17,7 @@ def hassio_env():
with patch.dict(os.environ, {"HASSIO": "127.0.0.1"}), patch(
"homeassistant.components.hassio.HassIO.is_connected",
return_value={"result": "ok", "data": {}},
- ), patch.dict(os.environ, {"HASSIO_TOKEN": "123456"}), patch(
+ ), patch.dict(os.environ, {"HASSIO_TOKEN": HASSIO_TOKEN}), patch(
"homeassistant.components.hassio.HassIO.get_info",
Mock(side_effect=HassioAPIError()),
):
diff --git a/tests/components/hassio/test_config_flow.py b/tests/components/hassio/test_config_flow.py
new file mode 100644
index 00000000000000..2b4b8a88914fcc
--- /dev/null
+++ b/tests/components/hassio/test_config_flow.py
@@ -0,0 +1,36 @@
+"""Test the Home Assistant Supervisor config flow."""
+from unittest.mock import patch
+
+from homeassistant import setup
+from homeassistant.components.hassio import DOMAIN
+
+
+async def test_config_flow(hass):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch(
+ "homeassistant.components.hassio.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.hassio.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "system"}
+ )
+ assert result["type"] == "create_entry"
+ assert result["title"] == "Supervisor"
+ assert result["data"] == {}
+ await hass.async_block_till_done()
+
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_multiple_entries(hass):
+ """Test creating multiple hassio entries."""
+ await test_config_flow(hass)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "system"}
+ )
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py
index 2ec964d8e8b0b8..ff1c348a37bb32 100644
--- a/tests/components/hassio/test_http.py
+++ b/tests/components/hassio/test_http.py
@@ -128,8 +128,8 @@ async def test_forwarding_user_info(hassio_client, hass_admin_user, aioclient_mo
assert len(aioclient_mock.mock_calls) == 1
req_headers = aioclient_mock.mock_calls[0][-1]
- req_headers["X-Hass-User-ID"] == hass_admin_user.id
- req_headers["X-Hass-Is-Admin"] == "1"
+ assert req_headers["X-Hass-User-ID"] == hass_admin_user.id
+ assert req_headers["X-Hass-Is-Admin"] == "1"
async def test_snapshot_upload_headers(hassio_client, aioclient_mock):
@@ -147,7 +147,7 @@ async def test_snapshot_upload_headers(hassio_client, aioclient_mock):
assert len(aioclient_mock.mock_calls) == 1
req_headers = aioclient_mock.mock_calls[0][-1]
- req_headers["Content-Type"] == content_type
+ assert req_headers["Content-Type"] == content_type
async def test_snapshot_download_headers(hassio_client, aioclient_mock):
@@ -168,7 +168,7 @@ async def test_snapshot_download_headers(hassio_client, aioclient_mock):
assert len(aioclient_mock.mock_calls) == 1
- resp.headers["Content-Disposition"] == content_disposition
+ assert resp.headers["Content-Disposition"] == content_disposition
def test_need_auth(hass):
diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py
index 7ed24dca457036..5bf8a45ab52190 100644
--- a/tests/components/hassio/test_init.py
+++ b/tests/components/hassio/test_init.py
@@ -1,4 +1,5 @@
"""The tests for the hassio component."""
+from datetime import timedelta
import os
from unittest.mock import patch
@@ -6,14 +7,20 @@
from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.components import frontend
-from homeassistant.components.hassio import STORAGE_KEY
+from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
+from homeassistant.components.hassio import ADDONS_COORDINATOR, DOMAIN, STORAGE_KEY
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.helpers.device_registry import async_get
from homeassistant.setup import async_setup_component
+from homeassistant.util import dt as dt_util
+
+from tests.common import MockConfigEntry, async_fire_time_changed
MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"}
@pytest.fixture(autouse=True)
-def mock_all(aioclient_mock):
+def mock_all(aioclient_mock, request):
"""Mock all setup requests."""
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"})
@@ -49,7 +56,30 @@ def mock_all(aioclient_mock):
)
aioclient_mock.get(
"http://127.0.0.1/supervisor/info",
- json={"result": "ok", "data": {"version_latest": "1.0.0"}},
+ json={
+ "result": "ok",
+ "data": {"version_latest": "1.0.0"},
+ "addons": [
+ {
+ "name": "test",
+ "slug": "test",
+ "installed": True,
+ "update_available": False,
+ "version": "1.0.0",
+ "version_latest": "1.0.0",
+ "url": "https://github.com/home-assistant/addons/test",
+ },
+ {
+ "name": "test2",
+ "slug": "test2",
+ "installed": True,
+ "update_available": False,
+ "version": "1.0.0",
+ "version_latest": "1.0.0",
+ "url": "https://github.com",
+ },
+ ],
+ },
)
aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
@@ -237,6 +267,7 @@ async def test_service_register(hassio_env, hass):
assert hass.services.has_service("hassio", "addon_start")
assert hass.services.has_service("hassio", "addon_stop")
assert hass.services.has_service("hassio", "addon_restart")
+ assert hass.services.has_service("hassio", "addon_update")
assert hass.services.has_service("hassio", "addon_stdin")
assert hass.services.has_service("hassio", "host_shutdown")
assert hass.services.has_service("hassio", "host_reboot")
@@ -254,6 +285,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock):
aioclient_mock.post("http://127.0.0.1/addons/test/start", json={"result": "ok"})
aioclient_mock.post("http://127.0.0.1/addons/test/stop", json={"result": "ok"})
aioclient_mock.post("http://127.0.0.1/addons/test/restart", json={"result": "ok"})
+ aioclient_mock.post("http://127.0.0.1/addons/test/update", json={"result": "ok"})
aioclient_mock.post("http://127.0.0.1/addons/test/stdin", json={"result": "ok"})
aioclient_mock.post("http://127.0.0.1/host/shutdown", json={"result": "ok"})
aioclient_mock.post("http://127.0.0.1/host/reboot", json={"result": "ok"})
@@ -269,19 +301,20 @@ async def test_service_calls(hassio_env, hass, aioclient_mock):
await hass.services.async_call("hassio", "addon_start", {"addon": "test"})
await hass.services.async_call("hassio", "addon_stop", {"addon": "test"})
await hass.services.async_call("hassio", "addon_restart", {"addon": "test"})
+ await hass.services.async_call("hassio", "addon_update", {"addon": "test"})
await hass.services.async_call(
"hassio", "addon_stdin", {"addon": "test", "input": "test"}
)
await hass.async_block_till_done()
- assert aioclient_mock.call_count == 7
+ assert aioclient_mock.call_count == 8
assert aioclient_mock.mock_calls[-1][2] == "test"
await hass.services.async_call("hassio", "host_shutdown", {})
await hass.services.async_call("hassio", "host_reboot", {})
await hass.async_block_till_done()
- assert aioclient_mock.call_count == 9
+ assert aioclient_mock.call_count == 10
await hass.services.async_call("hassio", "snapshot_full", {})
await hass.services.async_call(
@@ -291,7 +324,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock):
)
await hass.async_block_till_done()
- assert aioclient_mock.call_count == 11
+ assert aioclient_mock.call_count == 12
assert aioclient_mock.mock_calls[-1][2] == {
"addons": ["test"],
"folders": ["ssl"],
@@ -312,7 +345,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock):
)
await hass.async_block_till_done()
- assert aioclient_mock.call_count == 13
+ assert aioclient_mock.call_count == 14
assert aioclient_mock.mock_calls[-1][2] == {
"addons": ["test"],
"folders": ["ssl"],
@@ -346,3 +379,143 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock):
assert mock_check_config.called
assert aioclient_mock.call_count == 5
+
+
+async def test_entry_load_and_unload(hass):
+ """Test loading and unloading config entry."""
+ with patch.dict(os.environ, MOCK_ENVIRON):
+ config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
+ config_entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert SENSOR_DOMAIN in hass.config.components
+ assert BINARY_SENSOR_DOMAIN in hass.config.components
+ assert ADDONS_COORDINATOR in hass.data
+
+ assert await hass.config_entries.async_unload(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert ADDONS_COORDINATOR not in hass.data
+
+
+async def test_migration_off_hassio(hass):
+ """Test that when a user moves instance off Hass.io, config entry gets cleaned up."""
+ config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
+ config_entry.add_to_hass(hass)
+ assert not await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert hass.config_entries.async_entries(DOMAIN) == []
+
+
+async def test_device_registry_calls(hass):
+ """Test device registry entries for hassio."""
+ dev_reg = async_get(hass)
+ supervisor_mock_data = {
+ "addons": [
+ {
+ "name": "test",
+ "slug": "test",
+ "installed": True,
+ "update_available": False,
+ "version": "1.0.0",
+ "version_latest": "1.0.0",
+ "repository": "test",
+ "url": "https://github.com/home-assistant/addons/test",
+ },
+ {
+ "name": "test2",
+ "slug": "test2",
+ "installed": True,
+ "update_available": False,
+ "version": "1.0.0",
+ "version_latest": "1.0.0",
+ "url": "https://github.com",
+ },
+ ]
+ }
+ os_mock_data = {
+ "board": "odroid-n2",
+ "boot": "A",
+ "update_available": False,
+ "version": "5.12",
+ "version_latest": "5.12",
+ }
+
+ with patch.dict(os.environ, MOCK_ENVIRON), patch(
+ "homeassistant.components.hassio.HassIO.get_supervisor_info",
+ return_value=supervisor_mock_data,
+ ), patch(
+ "homeassistant.components.hassio.HassIO.get_os_info",
+ return_value=os_mock_data,
+ ):
+ config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
+ config_entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert len(dev_reg.devices) == 3
+
+ supervisor_mock_data = {
+ "addons": [
+ {
+ "name": "test2",
+ "slug": "test2",
+ "installed": True,
+ "update_available": False,
+ "version": "1.0.0",
+ "version_latest": "1.0.0",
+ "url": "https://github.com",
+ },
+ ]
+ }
+
+ # Test that when addon is removed, next update will remove the add-on and subsequent updates won't
+ with patch(
+ "homeassistant.components.hassio.HassIO.get_supervisor_info",
+ return_value=supervisor_mock_data,
+ ), patch(
+ "homeassistant.components.hassio.HassIO.get_os_info",
+ return_value=os_mock_data,
+ ):
+ async_fire_time_changed(hass, dt_util.now() + timedelta(hours=1))
+ await hass.async_block_till_done()
+ assert len(dev_reg.devices) == 2
+
+ async_fire_time_changed(hass, dt_util.now() + timedelta(hours=2))
+ await hass.async_block_till_done()
+ assert len(dev_reg.devices) == 2
+
+ supervisor_mock_data = {
+ "addons": [
+ {
+ "name": "test2",
+ "slug": "test2",
+ "installed": True,
+ "update_available": False,
+ "version": "1.0.0",
+ "version_latest": "1.0.0",
+ "url": "https://github.com",
+ },
+ {
+ "name": "test3",
+ "slug": "test3",
+ "installed": True,
+ "update_available": False,
+ "version": "1.0.0",
+ "version_latest": "1.0.0",
+ "url": "https://github.com",
+ },
+ ]
+ }
+
+ # Test that when addon is added, next update will reload the entry so we register
+ # a new device
+ with patch(
+ "homeassistant.components.hassio.HassIO.get_supervisor_info",
+ return_value=supervisor_mock_data,
+ ), patch(
+ "homeassistant.components.hassio.HassIO.get_os_info",
+ return_value=os_mock_data,
+ ):
+ async_fire_time_changed(hass, dt_util.now() + timedelta(hours=3))
+ await hass.async_block_till_done()
+ assert len(dev_reg.devices) == 3
diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py
new file mode 100644
index 00000000000000..dcf6b64d9e2715
--- /dev/null
+++ b/tests/components/hassio/test_websocket_api.py
@@ -0,0 +1,90 @@
+"""Test websocket API."""
+from homeassistant.components.hassio.const import (
+ ATTR_DATA,
+ ATTR_ENDPOINT,
+ ATTR_METHOD,
+ ATTR_WS_EVENT,
+ EVENT_SUPERVISOR_EVENT,
+ WS_ID,
+ WS_TYPE,
+ WS_TYPE_API,
+ WS_TYPE_SUBSCRIBE,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.setup import async_setup_component
+
+from . import mock_all # noqa: F401
+
+from tests.common import async_mock_signal
+
+
+async def test_ws_subscription(hassio_env, hass: HomeAssistant, hass_ws_client):
+ """Test websocket subscription."""
+ assert await async_setup_component(hass, "hassio", {})
+ client = await hass_ws_client(hass)
+ await client.send_json({WS_ID: 5, WS_TYPE: WS_TYPE_SUBSCRIBE})
+ response = await client.receive_json()
+ assert response["success"]
+
+ calls = async_mock_signal(hass, EVENT_SUPERVISOR_EVENT)
+ async_dispatcher_send(hass, EVENT_SUPERVISOR_EVENT, {"lorem": "ipsum"})
+
+ response = await client.receive_json()
+ assert response["event"]["lorem"] == "ipsum"
+ assert len(calls) == 1
+
+ await client.send_json(
+ {
+ WS_ID: 6,
+ WS_TYPE: "supervisor/event",
+ ATTR_DATA: {ATTR_WS_EVENT: "test", "lorem": "ipsum"},
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ assert len(calls) == 2
+
+ response = await client.receive_json()
+ assert response["event"]["lorem"] == "ipsum"
+
+ # Unsubscribe
+ await client.send_json({WS_ID: 7, WS_TYPE: "unsubscribe_events", "subscription": 5})
+ response = await client.receive_json()
+ assert response["success"]
+
+
+async def test_websocket_supervisor_api(
+ hassio_env, hass: HomeAssistant, hass_ws_client, aioclient_mock
+):
+ """Test Supervisor websocket api."""
+ assert await async_setup_component(hass, "hassio", {})
+ websocket_client = await hass_ws_client(hass)
+ aioclient_mock.post(
+ "http://127.0.0.1/snapshots/new/partial",
+ json={"result": "ok", "data": {"slug": "sn_slug"}},
+ )
+
+ await websocket_client.send_json(
+ {
+ WS_ID: 1,
+ WS_TYPE: WS_TYPE_API,
+ ATTR_ENDPOINT: "/snapshots/new/partial",
+ ATTR_METHOD: "post",
+ }
+ )
+
+ msg = await websocket_client.receive_json()
+ assert msg["result"]["slug"] == "sn_slug"
+
+ await websocket_client.send_json(
+ {
+ WS_ID: 2,
+ WS_TYPE: WS_TYPE_API,
+ ATTR_ENDPOINT: "/supervisor/info",
+ ATTR_METHOD: "get",
+ }
+ )
+
+ msg = await websocket_client.receive_json()
+ assert msg["result"]["version_latest"] == "1.0.0"
diff --git a/tests/components/heos/conftest.py b/tests/components/heos/conftest.py
index fa7615e2de8ba9..2c48b7fe8e1b13 100644
--- a/tests/components/heos/conftest.py
+++ b/tests/components/heos/conftest.py
@@ -1,5 +1,7 @@
"""Configuration for HEOS tests."""
-from typing import Dict, Sequence
+from __future__ import annotations
+
+from typing import Sequence
from unittest.mock import Mock, patch as patch
from pyheos import Dispatcher, Heos, HeosPlayer, HeosSource, InputSource, const
@@ -86,7 +88,7 @@ def player_fixture(quick_selects):
@pytest.fixture(name="favorites")
-def favorites_fixture() -> Dict[int, HeosSource]:
+def favorites_fixture() -> dict[int, HeosSource]:
"""Create favorites fixture."""
station = Mock(HeosSource)
station.type = const.TYPE_STATION
@@ -131,7 +133,7 @@ def discovery_data_fixture() -> dict:
@pytest.fixture(name="quick_selects")
-def quick_selects_fixture() -> Dict[int, str]:
+def quick_selects_fixture() -> dict[int, str]:
"""Create a dict of quick selects for testing."""
return {
1: "Quick Select 1",
@@ -153,12 +155,12 @@ def playlists_fixture() -> Sequence[HeosSource]:
@pytest.fixture(name="change_data")
-def change_data_fixture() -> Dict:
+def change_data_fixture() -> dict:
"""Create player change data for testing."""
return {const.DATA_MAPPED_IDS: {}, const.DATA_NEW: []}
@pytest.fixture(name="change_data_mapped_ids")
-def change_data_mapped_ids_fixture() -> Dict:
+def change_data_mapped_ids_fixture() -> dict:
"""Create player change data for testing."""
return {const.DATA_MAPPED_IDS: {101: 1}, const.DATA_NEW: []}
diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py
index ef7285ab1859da..8b87acfd9fd95d 100644
--- a/tests/components/heos/test_media_player.py
+++ b/tests/components/heos/test_media_player.py
@@ -53,6 +53,7 @@
STATE_PLAYING,
STATE_UNAVAILABLE,
)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
@@ -237,8 +238,8 @@ async def test_updates_from_players_changed_new_ids(
):
"""Test player updates from changes to available players."""
await setup_platform(hass, config_entry, config)
- device_registry = await hass.helpers.device_registry.async_get_registry()
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
+ entity_registry = er.async_get(hass)
player = controller.players[1]
event = asyncio.Event()
@@ -587,10 +588,10 @@ async def test_select_input_command_error(
async def test_unload_config_entry(hass, config_entry, config, controller):
- """Test the player is removed when the config entry is unloaded."""
+ """Test the player is set unavailable when the config entry is unloaded."""
await setup_platform(hass, config_entry, config)
await config_entry.async_unload(hass)
- assert not hass.states.get("media_player.test_player")
+ assert hass.states.get("media_player.test_player").state == STATE_UNAVAILABLE
async def test_play_media_url(hass, config_entry, config, controller, caplog):
diff --git a/tests/components/hisense_aehw4a1/test_init.py b/tests/components/hisense_aehw4a1/test_init.py
index 4add153ee94efd..ef99c3d1c964a6 100644
--- a/tests/components/hisense_aehw4a1/test_init.py
+++ b/tests/components/hisense_aehw4a1/test_init.py
@@ -13,24 +13,21 @@ async def test_creating_entry_sets_up_climate_discovery(hass):
with patch(
"homeassistant.components.hisense_aehw4a1.config_flow.AehW4a1.discovery",
return_value=["1.2.3.4"],
- ):
- with patch(
- "homeassistant.components.hisense_aehw4a1.climate.async_setup_entry",
- return_value=True,
- ) as mock_setup:
- result = await hass.config_entries.flow.async_init(
- hisense_aehw4a1.DOMAIN, context={"source": config_entries.SOURCE_USER}
- )
+ ), patch(
+ "homeassistant.components.hisense_aehw4a1.climate.async_setup_entry",
+ return_value=True,
+ ) as mock_setup:
+ result = await hass.config_entries.flow.async_init(
+ hisense_aehw4a1.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
- # Confirmation form
- assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ # Confirmation form
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"], {}
- )
- assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- await hass.async_block_till_done()
+ await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
@@ -40,17 +37,16 @@ async def test_configuring_hisense_w4a1_create_entry(hass):
with patch(
"homeassistant.components.hisense_aehw4a1.config_flow.AehW4a1.check",
return_value=True,
- ):
- with patch(
- "homeassistant.components.hisense_aehw4a1.async_setup_entry",
- return_value=True,
- ) as mock_setup:
- await async_setup_component(
- hass,
- hisense_aehw4a1.DOMAIN,
- {"hisense_aehw4a1": {"ip_address": ["1.2.3.4"]}},
- )
- await hass.async_block_till_done()
+ ), patch(
+ "homeassistant.components.hisense_aehw4a1.async_setup_entry",
+ return_value=True,
+ ) as mock_setup:
+ await async_setup_component(
+ hass,
+ hisense_aehw4a1.DOMAIN,
+ {"hisense_aehw4a1": {"ip_address": ["1.2.3.4"]}},
+ )
+ await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
@@ -60,17 +56,16 @@ async def test_configuring_hisense_w4a1_not_creates_entry_for_device_not_found(h
with patch(
"homeassistant.components.hisense_aehw4a1.config_flow.AehW4a1.check",
side_effect=exceptions.ConnectionError,
- ):
- with patch(
- "homeassistant.components.hisense_aehw4a1.async_setup_entry",
- return_value=True,
- ) as mock_setup:
- await async_setup_component(
- hass,
- hisense_aehw4a1.DOMAIN,
- {"hisense_aehw4a1": {"ip_address": ["1.2.3.4"]}},
- )
- await hass.async_block_till_done()
+ ), patch(
+ "homeassistant.components.hisense_aehw4a1.async_setup_entry",
+ return_value=True,
+ ) as mock_setup:
+ await async_setup_component(
+ hass,
+ hisense_aehw4a1.DOMAIN,
+ {"hisense_aehw4a1": {"ip_address": ["1.2.3.4"]}},
+ )
+ await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 0
diff --git a/tests/components/history/conftest.py b/tests/components/history/conftest.py
new file mode 100644
index 00000000000000..35829ece721dc2
--- /dev/null
+++ b/tests/components/history/conftest.py
@@ -0,0 +1,49 @@
+"""Fixtures for history tests."""
+import pytest
+
+from homeassistant.components import history
+from homeassistant.components.recorder.const import DATA_INSTANCE
+from homeassistant.setup import setup_component
+
+from tests.common import get_test_home_assistant, init_recorder_component
+
+
+@pytest.fixture
+def hass_recorder():
+ """Home Assistant fixture with in-memory recorder."""
+ hass = get_test_home_assistant()
+
+ def setup_recorder(config=None):
+ """Set up with params."""
+ init_recorder_component(hass, config)
+ hass.start()
+ hass.block_till_done()
+ hass.data[DATA_INSTANCE].block_till_done()
+ return hass
+
+ yield setup_recorder
+ hass.stop()
+
+
+@pytest.fixture
+def hass_history(hass_recorder):
+ """Home Assistant fixture with history."""
+ hass = hass_recorder()
+
+ config = history.CONFIG_SCHEMA(
+ {
+ history.DOMAIN: {
+ history.CONF_INCLUDE: {
+ history.CONF_DOMAINS: ["media_player"],
+ history.CONF_ENTITIES: ["thermostat.test"],
+ },
+ history.CONF_EXCLUDE: {
+ history.CONF_DOMAINS: ["thermostat"],
+ history.CONF_ENTITIES: ["media_player.test"],
+ },
+ }
+ }
+ )
+ assert setup_component(hass, history.DOMAIN, config)
+
+ yield hass
diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py
index 87e18305b8ab2a..f4d1c817858731 100644
--- a/tests/components/history/test_init.py
+++ b/tests/components/history/test_init.py
@@ -3,760 +3,723 @@
from copy import copy
from datetime import timedelta
import json
-import unittest
from unittest.mock import patch, sentinel
+import pytest
+
from homeassistant.components import history, recorder
from homeassistant.components.recorder.models import process_timestamp
import homeassistant.core as ha
from homeassistant.helpers.json import JSONEncoder
-from homeassistant.setup import async_setup_component, setup_component
+from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
-from tests.common import (
- get_test_home_assistant,
- init_recorder_component,
- mock_state_change_event,
-)
+from tests.common import init_recorder_component, mock_state_change_event
from tests.components.recorder.common import trigger_db_commit, wait_recording_done
-class TestComponentHistory(unittest.TestCase):
- """Test History component."""
-
- def setUp(self): # pylint: disable=invalid-name
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.tear_down_cleanup)
-
- def tear_down_cleanup(self):
- """Stop everything that was started."""
- self.hass.stop()
-
- def init_recorder(self):
- """Initialize the recorder."""
- init_recorder_component(self.hass)
- self.hass.start()
- wait_recording_done(self.hass)
-
- def test_setup(self):
- """Test setup method of history."""
- config = history.CONFIG_SCHEMA(
- {
- # ha.DOMAIN: {},
- history.DOMAIN: {
- history.CONF_INCLUDE: {
- history.CONF_DOMAINS: ["media_player"],
- history.CONF_ENTITIES: ["thermostat.test"],
- },
- history.CONF_EXCLUDE: {
- history.CONF_DOMAINS: ["thermostat"],
- history.CONF_ENTITIES: ["media_player.test"],
- },
- }
- }
- )
- self.init_recorder()
- assert setup_component(self.hass, history.DOMAIN, config)
-
- def test_get_states(self):
- """Test getting states at a specific point in time."""
- self.test_setup()
- states = []
-
- now = dt_util.utcnow()
- with patch(
- "homeassistant.components.recorder.dt_util.utcnow", return_value=now
- ):
- for i in range(5):
- state = ha.State(
- "test.point_in_time_{}".format(i % 5),
- f"State {i}",
- {"attribute_test": i},
- )
-
- mock_state_change_event(self.hass, state)
-
- states.append(state)
-
- wait_recording_done(self.hass)
-
- future = now + timedelta(seconds=1)
- with patch(
- "homeassistant.components.recorder.dt_util.utcnow", return_value=future
- ):
- for i in range(5):
- state = ha.State(
- "test.point_in_time_{}".format(i % 5),
- f"State {i}",
- {"attribute_test": i},
- )
-
- mock_state_change_event(self.hass, state)
-
- wait_recording_done(self.hass)
-
- # Get states returns everything before POINT
- for state1, state2 in zip(
- states,
- sorted(
- history.get_states(self.hass, future), key=lambda state: state.entity_id
- ),
- ):
- assert state1 == state2
-
- # Test get_state here because we have a DB setup
- assert states[0] == history.get_state(self.hass, future, states[0].entity_id)
-
- time_before_recorder_ran = now - timedelta(days=1000)
- assert history.get_states(self.hass, time_before_recorder_ran) == []
-
- assert history.get_state(self.hass, time_before_recorder_ran, "demo.id") is None
-
- def test_state_changes_during_period(self):
- """Test state change during period."""
- self.test_setup()
- entity_id = "media_player.test"
-
- def set_state(state):
- """Set the state."""
- self.hass.states.set(entity_id, state)
- wait_recording_done(self.hass)
- return self.hass.states.get(entity_id)
-
- start = dt_util.utcnow()
- point = start + timedelta(seconds=1)
- end = point + timedelta(seconds=1)
-
- with patch(
- "homeassistant.components.recorder.dt_util.utcnow", return_value=start
- ):
- set_state("idle")
- set_state("YouTube")
-
- with patch(
- "homeassistant.components.recorder.dt_util.utcnow", return_value=point
- ):
- states = [
- set_state("idle"),
- set_state("Netflix"),
- set_state("Plex"),
- set_state("YouTube"),
- ]
-
- with patch(
- "homeassistant.components.recorder.dt_util.utcnow", return_value=end
- ):
- set_state("Netflix")
- set_state("Plex")
-
- hist = history.state_changes_during_period(self.hass, start, end, entity_id)
-
- assert states == hist[entity_id]
-
- def test_get_last_state_changes(self):
- """Test number of state changes."""
- self.test_setup()
- entity_id = "sensor.test"
-
- def set_state(state):
- """Set the state."""
- self.hass.states.set(entity_id, state)
- wait_recording_done(self.hass)
- return self.hass.states.get(entity_id)
-
- start = dt_util.utcnow() - timedelta(minutes=2)
- point = start + timedelta(minutes=1)
- point2 = point + timedelta(minutes=1)
-
- with patch(
- "homeassistant.components.recorder.dt_util.utcnow", return_value=start
- ):
- set_state("1")
-
- states = []
- with patch(
- "homeassistant.components.recorder.dt_util.utcnow", return_value=point
- ):
- states.append(set_state("2"))
-
- with patch(
- "homeassistant.components.recorder.dt_util.utcnow", return_value=point2
- ):
- states.append(set_state("3"))
-
- hist = history.get_last_state_changes(self.hass, 2, entity_id)
-
- assert states == hist[entity_id]
-
- def test_ensure_state_can_be_copied(self):
- """Ensure a state can pass though copy().
-
- The filter integration uses copy() on states
- from history.
- """
- self.test_setup()
- entity_id = "sensor.test"
-
- def set_state(state):
- """Set the state."""
- self.hass.states.set(entity_id, state)
- wait_recording_done(self.hass)
- return self.hass.states.get(entity_id)
-
- start = dt_util.utcnow() - timedelta(minutes=2)
- point = start + timedelta(minutes=1)
-
- with patch(
- "homeassistant.components.recorder.dt_util.utcnow", return_value=start
- ):
- set_state("1")
-
- with patch(
- "homeassistant.components.recorder.dt_util.utcnow", return_value=point
- ):
- set_state("2")
-
- hist = history.get_last_state_changes(self.hass, 2, entity_id)
-
- assert copy(hist[entity_id][0]) == hist[entity_id][0]
- assert copy(hist[entity_id][1]) == hist[entity_id][1]
-
- def test_get_significant_states(self):
- """Test that only significant states are returned.
-
- We should get back every thermostat change that
- includes an attribute change, but only the state updates for
- media player (attribute changes are not significant and not returned).
- """
- zero, four, states = self.record_states()
- hist = history.get_significant_states(
- self.hass, zero, four, filters=history.Filters()
- )
- assert states == hist
+@pytest.mark.usefixtures("hass_history")
+def test_setup():
+ """Test setup method of history."""
+ # Verification occurs in the fixture
+ pass
- def test_get_significant_states_minimal_response(self):
- """Test that only significant states are returned.
- When minimal responses is set only the first and
- last states return a complete state.
+def test_get_states(hass_history):
+ """Test getting states at a specific point in time."""
+ hass = hass_history
+ states = []
- We should get back every thermostat change that
- includes an attribute change, but only the state updates for
- media player (attribute changes are not significant and not returned).
- """
- zero, four, states = self.record_states()
- hist = history.get_significant_states(
- self.hass, zero, four, filters=history.Filters(), minimal_response=True
- )
+ now = dt_util.utcnow()
+ with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=now):
+ for i in range(5):
+ state = ha.State(
+ "test.point_in_time_{}".format(i % 5),
+ f"State {i}",
+ {"attribute_test": i},
+ )
- # The second media_player.test state is reduced
- # down to last_changed and state when minimal_response
- # is set. We use JSONEncoder to make sure that are
- # pre-encoded last_changed is always the same as what
- # will happen with encoding a native state
- input_state = states["media_player.test"][1]
- orig_last_changed = json.dumps(
- process_timestamp(input_state.last_changed),
- cls=JSONEncoder,
- ).replace('"', "")
- orig_state = input_state.state
- states["media_player.test"][1] = {
- "last_changed": orig_last_changed,
- "state": orig_state,
- }
+ mock_state_change_event(hass, state)
- assert states == hist
-
- def test_get_significant_states_with_initial(self):
- """Test that only significant states are returned.
-
- We should get back every thermostat change that
- includes an attribute change, but only the state updates for
- media player (attribute changes are not significant and not returned).
- """
- zero, four, states = self.record_states()
- one = zero + timedelta(seconds=1)
- one_and_half = zero + timedelta(seconds=1.5)
- for entity_id in states:
- if entity_id == "media_player.test":
- states[entity_id] = states[entity_id][1:]
- for state in states[entity_id]:
- if state.last_changed == one:
- state.last_changed = one_and_half
-
- hist = history.get_significant_states(
- self.hass,
- one_and_half,
- four,
- filters=history.Filters(),
- include_start_time_state=True,
- )
- assert states == hist
-
- def test_get_significant_states_without_initial(self):
- """Test that only significant states are returned.
-
- We should get back every thermostat change that
- includes an attribute change, but only the state updates for
- media player (attribute changes are not significant and not returned).
- """
- zero, four, states = self.record_states()
- one = zero + timedelta(seconds=1)
- one_and_half = zero + timedelta(seconds=1.5)
- for entity_id in states:
- states[entity_id] = list(
- filter(lambda s: s.last_changed != one, states[entity_id])
+ states.append(state)
+
+ wait_recording_done(hass)
+
+ future = now + timedelta(seconds=1)
+ with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=future):
+ for i in range(5):
+ state = ha.State(
+ "test.point_in_time_{}".format(i % 5),
+ f"State {i}",
+ {"attribute_test": i},
)
- del states["media_player.test2"]
-
- hist = history.get_significant_states(
- self.hass,
- one_and_half,
- four,
- filters=history.Filters(),
- include_start_time_state=False,
- )
- assert states == hist
-
- def test_get_significant_states_entity_id(self):
- """Test that only significant states are returned for one entity."""
- zero, four, states = self.record_states()
- del states["media_player.test2"]
- del states["media_player.test3"]
- del states["thermostat.test"]
- del states["thermostat.test2"]
- del states["script.can_cancel_this_one"]
-
- hist = history.get_significant_states(
- self.hass, zero, four, ["media_player.test"], filters=history.Filters()
- )
- assert states == hist
-
- def test_get_significant_states_multiple_entity_ids(self):
- """Test that only significant states are returned for one entity."""
- zero, four, states = self.record_states()
- del states["media_player.test2"]
- del states["media_player.test3"]
- del states["thermostat.test2"]
- del states["script.can_cancel_this_one"]
-
- hist = history.get_significant_states(
- self.hass,
- zero,
- four,
- ["media_player.test", "thermostat.test"],
- filters=history.Filters(),
- )
- assert states == hist
-
- def test_get_significant_states_exclude_domain(self):
- """Test if significant states are returned when excluding domains.
-
- We should get back every thermostat change that includes an attribute
- change, but no media player changes.
- """
- zero, four, states = self.record_states()
- del states["media_player.test"]
- del states["media_player.test2"]
- del states["media_player.test3"]
-
- config = history.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- history.DOMAIN: {
- history.CONF_EXCLUDE: {history.CONF_DOMAINS: ["media_player"]}
- },
- }
+
+ mock_state_change_event(hass, state)
+
+ wait_recording_done(hass)
+
+ # Get states returns everything before POINT
+ for state1, state2 in zip(
+ states,
+ sorted(history.get_states(hass, future), key=lambda state: state.entity_id),
+ ):
+ assert state1 == state2
+
+ # Test get_state here because we have a DB setup
+ assert states[0] == history.get_state(hass, future, states[0].entity_id)
+
+ time_before_recorder_ran = now - timedelta(days=1000)
+ assert history.get_states(hass, time_before_recorder_ran) == []
+
+ assert history.get_state(hass, time_before_recorder_ran, "demo.id") is None
+
+
+def test_state_changes_during_period(hass_history):
+ """Test state change during period."""
+ hass = hass_history
+ entity_id = "media_player.test"
+
+ def set_state(state):
+ """Set the state."""
+ hass.states.set(entity_id, state)
+ wait_recording_done(hass)
+ return hass.states.get(entity_id)
+
+ start = dt_util.utcnow()
+ point = start + timedelta(seconds=1)
+ end = point + timedelta(seconds=1)
+
+ with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start):
+ set_state("idle")
+ set_state("YouTube")
+
+ with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point):
+ states = [
+ set_state("idle"),
+ set_state("Netflix"),
+ set_state("Plex"),
+ set_state("YouTube"),
+ ]
+
+ with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=end):
+ set_state("Netflix")
+ set_state("Plex")
+
+ hist = history.state_changes_during_period(hass, start, end, entity_id)
+
+ assert states == hist[entity_id]
+
+
+def test_get_last_state_changes(hass_history):
+ """Test number of state changes."""
+ hass = hass_history
+ entity_id = "sensor.test"
+
+ def set_state(state):
+ """Set the state."""
+ hass.states.set(entity_id, state)
+ wait_recording_done(hass)
+ return hass.states.get(entity_id)
+
+ start = dt_util.utcnow() - timedelta(minutes=2)
+ point = start + timedelta(minutes=1)
+ point2 = point + timedelta(minutes=1)
+
+ with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start):
+ set_state("1")
+
+ states = []
+ with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point):
+ states.append(set_state("2"))
+
+ with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point2):
+ states.append(set_state("3"))
+
+ hist = history.get_last_state_changes(hass, 2, entity_id)
+
+ assert states == hist[entity_id]
+
+
+def test_ensure_state_can_be_copied(hass_history):
+ """Ensure a state can pass though copy().
+
+ The filter integration uses copy() on states
+ from history.
+ """
+ hass = hass_history
+ entity_id = "sensor.test"
+
+ def set_state(state):
+ """Set the state."""
+ hass.states.set(entity_id, state)
+ wait_recording_done(hass)
+ return hass.states.get(entity_id)
+
+ start = dt_util.utcnow() - timedelta(minutes=2)
+ point = start + timedelta(minutes=1)
+
+ with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start):
+ set_state("1")
+
+ with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point):
+ set_state("2")
+
+ hist = history.get_last_state_changes(hass, 2, entity_id)
+
+ assert copy(hist[entity_id][0]) == hist[entity_id][0]
+ assert copy(hist[entity_id][1]) == hist[entity_id][1]
+
+
+def test_get_significant_states(hass_history):
+ """Test that only significant states are returned.
+
+ We should get back every thermostat change that
+ includes an attribute change, but only the state updates for
+ media player (attribute changes are not significant and not returned).
+ """
+ hass = hass_history
+ zero, four, states = record_states(hass)
+ hist = history.get_significant_states(hass, zero, four, filters=history.Filters())
+ assert states == hist
+
+
+def test_get_significant_states_minimal_response(hass_history):
+ """Test that only significant states are returned.
+
+ When minimal responses is set only the first and
+ last states return a complete state.
+
+ We should get back every thermostat change that
+ includes an attribute change, but only the state updates for
+ media player (attribute changes are not significant and not returned).
+ """
+ hass = hass_history
+ zero, four, states = record_states(hass)
+ hist = history.get_significant_states(
+ hass, zero, four, filters=history.Filters(), minimal_response=True
+ )
+
+ # The second media_player.test state is reduced
+ # down to last_changed and state when minimal_response
+ # is set. We use JSONEncoder to make sure that are
+ # pre-encoded last_changed is always the same as what
+ # will happen with encoding a native state
+ input_state = states["media_player.test"][1]
+ orig_last_changed = json.dumps(
+ process_timestamp(input_state.last_changed),
+ cls=JSONEncoder,
+ ).replace('"', "")
+ orig_state = input_state.state
+ states["media_player.test"][1] = {
+ "last_changed": orig_last_changed,
+ "state": orig_state,
+ }
+
+ assert states == hist
+
+
+def test_get_significant_states_with_initial(hass_history):
+ """Test that only significant states are returned.
+
+ We should get back every thermostat change that
+ includes an attribute change, but only the state updates for
+ media player (attribute changes are not significant and not returned).
+ """
+ hass = hass_history
+ zero, four, states = record_states(hass)
+ one = zero + timedelta(seconds=1)
+ one_and_half = zero + timedelta(seconds=1.5)
+ for entity_id in states:
+ if entity_id == "media_player.test":
+ states[entity_id] = states[entity_id][1:]
+ for state in states[entity_id]:
+ if state.last_changed == one:
+ state.last_changed = one_and_half
+
+ hist = history.get_significant_states(
+ hass,
+ one_and_half,
+ four,
+ filters=history.Filters(),
+ include_start_time_state=True,
+ )
+ assert states == hist
+
+
+def test_get_significant_states_without_initial(hass_history):
+ """Test that only significant states are returned.
+
+ We should get back every thermostat change that
+ includes an attribute change, but only the state updates for
+ media player (attribute changes are not significant and not returned).
+ """
+ hass = hass_history
+ zero, four, states = record_states(hass)
+ one = zero + timedelta(seconds=1)
+ one_and_half = zero + timedelta(seconds=1.5)
+ for entity_id in states:
+ states[entity_id] = list(
+ filter(lambda s: s.last_changed != one, states[entity_id])
)
- self.check_significant_states(zero, four, states, config)
-
- def test_get_significant_states_exclude_entity(self):
- """Test if significant states are returned when excluding entities.
-
- We should get back every thermostat and script changes, but no media
- player changes.
- """
- zero, four, states = self.record_states()
- del states["media_player.test"]
-
- config = history.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- history.DOMAIN: {
- history.CONF_EXCLUDE: {history.CONF_ENTITIES: ["media_player.test"]}
+ del states["media_player.test2"]
+
+ hist = history.get_significant_states(
+ hass,
+ one_and_half,
+ four,
+ filters=history.Filters(),
+ include_start_time_state=False,
+ )
+ assert states == hist
+
+
+def test_get_significant_states_entity_id(hass_history):
+ """Test that only significant states are returned for one entity."""
+ hass = hass_history
+ zero, four, states = record_states(hass)
+ del states["media_player.test2"]
+ del states["media_player.test3"]
+ del states["thermostat.test"]
+ del states["thermostat.test2"]
+ del states["script.can_cancel_this_one"]
+
+ hist = history.get_significant_states(
+ hass, zero, four, ["media_player.test"], filters=history.Filters()
+ )
+ assert states == hist
+
+
+def test_get_significant_states_multiple_entity_ids(hass_history):
+ """Test that only significant states are returned for one entity."""
+ hass = hass_history
+ zero, four, states = record_states(hass)
+ del states["media_player.test2"]
+ del states["media_player.test3"]
+ del states["thermostat.test2"]
+ del states["script.can_cancel_this_one"]
+
+ hist = history.get_significant_states(
+ hass,
+ zero,
+ four,
+ ["media_player.test", "thermostat.test"],
+ filters=history.Filters(),
+ )
+ assert states == hist
+
+
+def test_get_significant_states_exclude_domain(hass_history):
+ """Test if significant states are returned when excluding domains.
+
+ We should get back every thermostat change that includes an attribute
+ change, but no media player changes.
+ """
+ hass = hass_history
+ zero, four, states = record_states(hass)
+ del states["media_player.test"]
+ del states["media_player.test2"]
+ del states["media_player.test3"]
+
+ config = history.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ history.DOMAIN: {
+ history.CONF_EXCLUDE: {history.CONF_DOMAINS: ["media_player"]}
+ },
+ }
+ )
+ check_significant_states(hass, zero, four, states, config)
+
+
+def test_get_significant_states_exclude_entity(hass_history):
+ """Test if significant states are returned when excluding entities.
+
+ We should get back every thermostat and script changes, but no media
+ player changes.
+ """
+ hass = hass_history
+ zero, four, states = record_states(hass)
+ del states["media_player.test"]
+
+ config = history.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ history.DOMAIN: {
+ history.CONF_EXCLUDE: {history.CONF_ENTITIES: ["media_player.test"]}
+ },
+ }
+ )
+ check_significant_states(hass, zero, four, states, config)
+
+
+def test_get_significant_states_exclude(hass_history):
+ """Test significant states when excluding entities and domains.
+
+ We should not get back every thermostat and media player test changes.
+ """
+ hass = hass_history
+ zero, four, states = record_states(hass)
+ del states["media_player.test"]
+ del states["thermostat.test"]
+ del states["thermostat.test2"]
+
+ config = history.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ history.DOMAIN: {
+ history.CONF_EXCLUDE: {
+ history.CONF_DOMAINS: ["thermostat"],
+ history.CONF_ENTITIES: ["media_player.test"],
+ }
+ },
+ }
+ )
+ check_significant_states(hass, zero, four, states, config)
+
+
+def test_get_significant_states_exclude_include_entity(hass_history):
+ """Test significant states when excluding domains and include entities.
+
+ We should not get back every thermostat and media player test changes.
+ """
+ hass = hass_history
+ zero, four, states = record_states(hass)
+ del states["media_player.test2"]
+ del states["media_player.test3"]
+ del states["thermostat.test"]
+ del states["thermostat.test2"]
+ del states["script.can_cancel_this_one"]
+
+ config = history.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ history.DOMAIN: {
+ history.CONF_INCLUDE: {
+ history.CONF_ENTITIES: ["media_player.test", "thermostat.test"]
},
- }
- )
- self.check_significant_states(zero, four, states, config)
-
- def test_get_significant_states_exclude(self):
- """Test significant states when excluding entities and domains.
-
- We should not get back every thermostat and media player test changes.
- """
- zero, four, states = self.record_states()
- del states["media_player.test"]
- del states["thermostat.test"]
- del states["thermostat.test2"]
-
- config = history.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- history.DOMAIN: {
- history.CONF_EXCLUDE: {
- history.CONF_DOMAINS: ["thermostat"],
- history.CONF_ENTITIES: ["media_player.test"],
- }
+ history.CONF_EXCLUDE: {history.CONF_DOMAINS: ["thermostat"]},
+ },
+ }
+ )
+ check_significant_states(hass, zero, four, states, config)
+
+
+def test_get_significant_states_include_domain(hass_history):
+ """Test if significant states are returned when including domains.
+
+ We should get back every thermostat and script changes, but no media
+ player changes.
+ """
+ hass = hass_history
+ zero, four, states = record_states(hass)
+ del states["media_player.test"]
+ del states["media_player.test2"]
+ del states["media_player.test3"]
+
+ config = history.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ history.DOMAIN: {
+ history.CONF_INCLUDE: {history.CONF_DOMAINS: ["thermostat", "script"]}
+ },
+ }
+ )
+ check_significant_states(hass, zero, four, states, config)
+
+
+def test_get_significant_states_include_entity(hass_history):
+ """Test if significant states are returned when including entities.
+
+ We should only get back changes of the media_player.test entity.
+ """
+ hass = hass_history
+ zero, four, states = record_states(hass)
+ del states["media_player.test2"]
+ del states["media_player.test3"]
+ del states["thermostat.test"]
+ del states["thermostat.test2"]
+ del states["script.can_cancel_this_one"]
+
+ config = history.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ history.DOMAIN: {
+ history.CONF_INCLUDE: {history.CONF_ENTITIES: ["media_player.test"]}
+ },
+ }
+ )
+ check_significant_states(hass, zero, four, states, config)
+
+
+def test_get_significant_states_include(hass_history):
+ """Test significant states when including domains and entities.
+
+ We should only get back changes of the media_player.test entity and the
+ thermostat domain.
+ """
+ hass = hass_history
+ zero, four, states = record_states(hass)
+ del states["media_player.test2"]
+ del states["media_player.test3"]
+ del states["script.can_cancel_this_one"]
+
+ config = history.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ history.DOMAIN: {
+ history.CONF_INCLUDE: {
+ history.CONF_DOMAINS: ["thermostat"],
+ history.CONF_ENTITIES: ["media_player.test"],
+ }
+ },
+ }
+ )
+ check_significant_states(hass, zero, four, states, config)
+
+
+def test_get_significant_states_include_exclude_domain(hass_history):
+ """Test if significant states when excluding and including domains.
+
+ We should not get back any changes since we include only the
+ media_player domain but also exclude it.
+ """
+ hass = hass_history
+ zero, four, states = record_states(hass)
+ del states["media_player.test"]
+ del states["media_player.test2"]
+ del states["media_player.test3"]
+ del states["thermostat.test"]
+ del states["thermostat.test2"]
+ del states["script.can_cancel_this_one"]
+
+ config = history.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ history.DOMAIN: {
+ history.CONF_INCLUDE: {history.CONF_DOMAINS: ["media_player"]},
+ history.CONF_EXCLUDE: {history.CONF_DOMAINS: ["media_player"]},
+ },
+ }
+ )
+ check_significant_states(hass, zero, four, states, config)
+
+
+def test_get_significant_states_include_exclude_entity(hass_history):
+ """Test if significant states when excluding and including domains.
+
+ We should not get back any changes since we include only
+ media_player.test but also exclude it.
+ """
+ hass = hass_history
+ zero, four, states = record_states(hass)
+ del states["media_player.test"]
+ del states["media_player.test2"]
+ del states["media_player.test3"]
+ del states["thermostat.test"]
+ del states["thermostat.test2"]
+ del states["script.can_cancel_this_one"]
+
+ config = history.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ history.DOMAIN: {
+ history.CONF_INCLUDE: {history.CONF_ENTITIES: ["media_player.test"]},
+ history.CONF_EXCLUDE: {history.CONF_ENTITIES: ["media_player.test"]},
+ },
+ }
+ )
+ check_significant_states(hass, zero, four, states, config)
+
+
+def test_get_significant_states_include_exclude(hass_history):
+ """Test if significant states when in/excluding domains and entities.
+
+ We should only get back changes of the media_player.test2 entity.
+ """
+ hass = hass_history
+ zero, four, states = record_states(hass)
+ del states["media_player.test"]
+ del states["thermostat.test"]
+ del states["thermostat.test2"]
+ del states["script.can_cancel_this_one"]
+
+ config = history.CONFIG_SCHEMA(
+ {
+ ha.DOMAIN: {},
+ history.DOMAIN: {
+ history.CONF_INCLUDE: {
+ history.CONF_DOMAINS: ["media_player"],
+ history.CONF_ENTITIES: ["thermostat.test"],
},
- }
- )
- self.check_significant_states(zero, four, states, config)
-
- def test_get_significant_states_exclude_include_entity(self):
- """Test significant states when excluding domains and include entities.
-
- We should not get back every thermostat and media player test changes.
- """
- zero, four, states = self.record_states()
- del states["media_player.test2"]
- del states["media_player.test3"]
- del states["thermostat.test"]
- del states["thermostat.test2"]
- del states["script.can_cancel_this_one"]
-
- config = history.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- history.DOMAIN: {
- history.CONF_INCLUDE: {
- history.CONF_ENTITIES: ["media_player.test", "thermostat.test"]
- },
- history.CONF_EXCLUDE: {history.CONF_DOMAINS: ["thermostat"]},
+ history.CONF_EXCLUDE: {
+ history.CONF_DOMAINS: ["thermostat"],
+ history.CONF_ENTITIES: ["media_player.test"],
},
- }
+ },
+ }
+ )
+ check_significant_states(hass, zero, four, states, config)
+
+
+def test_get_significant_states_are_ordered(hass_history):
+ """Test order of results from get_significant_states.
+
+ When entity ids are given, the results should be returned with the data
+ in the same order.
+ """
+ hass = hass_history
+ zero, four, _states = record_states(hass)
+ entity_ids = ["media_player.test", "media_player.test2"]
+ hist = history.get_significant_states(
+ hass, zero, four, entity_ids, filters=history.Filters()
+ )
+ assert list(hist.keys()) == entity_ids
+ entity_ids = ["media_player.test2", "media_player.test"]
+ hist = history.get_significant_states(
+ hass, zero, four, entity_ids, filters=history.Filters()
+ )
+ assert list(hist.keys()) == entity_ids
+
+
+def test_get_significant_states_only(hass_history):
+ """Test significant states when significant_states_only is set."""
+ hass = hass_history
+ entity_id = "sensor.test"
+
+ def set_state(state, **kwargs):
+ """Set the state."""
+ hass.states.set(entity_id, state, **kwargs)
+ wait_recording_done(hass)
+ return hass.states.get(entity_id)
+
+ start = dt_util.utcnow() - timedelta(minutes=4)
+ points = []
+ for i in range(1, 4):
+ points.append(start + timedelta(minutes=i))
+
+ states = []
+ with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start):
+ set_state("123", attributes={"attribute": 10.64})
+
+ with patch(
+ "homeassistant.components.recorder.dt_util.utcnow", return_value=points[0]
+ ):
+ # Attributes are different, state not
+ states.append(set_state("123", attributes={"attribute": 21.42}))
+
+ with patch(
+ "homeassistant.components.recorder.dt_util.utcnow", return_value=points[1]
+ ):
+ # state is different, attributes not
+ states.append(set_state("32", attributes={"attribute": 21.42}))
+
+ with patch(
+ "homeassistant.components.recorder.dt_util.utcnow", return_value=points[2]
+ ):
+ # everything is different
+ states.append(set_state("412", attributes={"attribute": 54.23}))
+
+ hist = history.get_significant_states(hass, start, significant_changes_only=True)
+
+ assert len(hist[entity_id]) == 2
+ assert states[0] not in hist[entity_id]
+ assert states[1] in hist[entity_id]
+ assert states[2] in hist[entity_id]
+
+ hist = history.get_significant_states(hass, start, significant_changes_only=False)
+
+ assert len(hist[entity_id]) == 3
+ assert states == hist[entity_id]
+
+
+def check_significant_states(hass, zero, four, states, config):
+ """Check if significant states are retrieved."""
+ filters = history.Filters()
+ exclude = config[history.DOMAIN].get(history.CONF_EXCLUDE)
+ if exclude:
+ filters.excluded_entities = exclude.get(history.CONF_ENTITIES, [])
+ filters.excluded_domains = exclude.get(history.CONF_DOMAINS, [])
+ include = config[history.DOMAIN].get(history.CONF_INCLUDE)
+ if include:
+ filters.included_entities = include.get(history.CONF_ENTITIES, [])
+ filters.included_domains = include.get(history.CONF_DOMAINS, [])
+
+ hist = history.get_significant_states(hass, zero, four, filters=filters)
+ assert states == hist
+
+
+def record_states(hass):
+ """Record some test states.
+
+ We inject a bunch of state updates from media player, zone and
+ thermostat.
+ """
+ mp = "media_player.test"
+ mp2 = "media_player.test2"
+ mp3 = "media_player.test3"
+ therm = "thermostat.test"
+ therm2 = "thermostat.test2"
+ zone = "zone.home"
+ script_c = "script.can_cancel_this_one"
+
+ def set_state(entity_id, state, **kwargs):
+ """Set the state."""
+ hass.states.set(entity_id, state, **kwargs)
+ wait_recording_done(hass)
+ return hass.states.get(entity_id)
+
+ zero = dt_util.utcnow()
+ one = zero + timedelta(seconds=1)
+ two = one + timedelta(seconds=1)
+ three = two + timedelta(seconds=1)
+ four = three + timedelta(seconds=1)
+
+ states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []}
+ with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one):
+ states[mp].append(
+ set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)})
)
- self.check_significant_states(zero, four, states, config)
-
- def test_get_significant_states_include_domain(self):
- """Test if significant states are returned when including domains.
-
- We should get back every thermostat and script changes, but no media
- player changes.
- """
- zero, four, states = self.record_states()
- del states["media_player.test"]
- del states["media_player.test2"]
- del states["media_player.test3"]
-
- config = history.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- history.DOMAIN: {
- history.CONF_INCLUDE: {
- history.CONF_DOMAINS: ["thermostat", "script"]
- }
- },
- }
+ states[mp].append(
+ set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)})
)
- self.check_significant_states(zero, four, states, config)
-
- def test_get_significant_states_include_entity(self):
- """Test if significant states are returned when including entities.
-
- We should only get back changes of the media_player.test entity.
- """
- zero, four, states = self.record_states()
- del states["media_player.test2"]
- del states["media_player.test3"]
- del states["thermostat.test"]
- del states["thermostat.test2"]
- del states["script.can_cancel_this_one"]
-
- config = history.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- history.DOMAIN: {
- history.CONF_INCLUDE: {history.CONF_ENTITIES: ["media_player.test"]}
- },
- }
+ states[mp2].append(
+ set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)})
)
- self.check_significant_states(zero, four, states, config)
-
- def test_get_significant_states_include(self):
- """Test significant states when including domains and entities.
-
- We should only get back changes of the media_player.test entity and the
- thermostat domain.
- """
- zero, four, states = self.record_states()
- del states["media_player.test2"]
- del states["media_player.test3"]
- del states["script.can_cancel_this_one"]
-
- config = history.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- history.DOMAIN: {
- history.CONF_INCLUDE: {
- history.CONF_DOMAINS: ["thermostat"],
- history.CONF_ENTITIES: ["media_player.test"],
- }
- },
- }
+ states[mp3].append(
+ set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)})
)
- self.check_significant_states(zero, four, states, config)
-
- def test_get_significant_states_include_exclude_domain(self):
- """Test if significant states when excluding and including domains.
-
- We should not get back any changes since we include only the
- media_player domain but also exclude it.
- """
- zero, four, states = self.record_states()
- del states["media_player.test"]
- del states["media_player.test2"]
- del states["media_player.test3"]
- del states["thermostat.test"]
- del states["thermostat.test2"]
- del states["script.can_cancel_this_one"]
-
- config = history.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- history.DOMAIN: {
- history.CONF_INCLUDE: {history.CONF_DOMAINS: ["media_player"]},
- history.CONF_EXCLUDE: {history.CONF_DOMAINS: ["media_player"]},
- },
- }
+ states[therm].append(
+ set_state(therm, 20, attributes={"current_temperature": 19.5})
)
- self.check_significant_states(zero, four, states, config)
-
- def test_get_significant_states_include_exclude_entity(self):
- """Test if significant states when excluding and including domains.
-
- We should not get back any changes since we include only
- media_player.test but also exclude it.
- """
- zero, four, states = self.record_states()
- del states["media_player.test"]
- del states["media_player.test2"]
- del states["media_player.test3"]
- del states["thermostat.test"]
- del states["thermostat.test2"]
- del states["script.can_cancel_this_one"]
-
- config = history.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- history.DOMAIN: {
- history.CONF_INCLUDE: {
- history.CONF_ENTITIES: ["media_player.test"]
- },
- history.CONF_EXCLUDE: {
- history.CONF_ENTITIES: ["media_player.test"]
- },
- },
- }
+
+ with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two):
+ # This state will be skipped only different in time
+ set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)})
+ # This state will be skipped because domain is excluded
+ set_state(zone, "zoning")
+ states[script_c].append(
+ set_state(script_c, "off", attributes={"can_cancel": True})
)
- self.check_significant_states(zero, four, states, config)
-
- def test_get_significant_states_include_exclude(self):
- """Test if significant states when in/excluding domains and entities.
-
- We should only get back changes of the media_player.test2 entity.
- """
- zero, four, states = self.record_states()
- del states["media_player.test"]
- del states["thermostat.test"]
- del states["thermostat.test2"]
- del states["script.can_cancel_this_one"]
-
- config = history.CONFIG_SCHEMA(
- {
- ha.DOMAIN: {},
- history.DOMAIN: {
- history.CONF_INCLUDE: {
- history.CONF_DOMAINS: ["media_player"],
- history.CONF_ENTITIES: ["thermostat.test"],
- },
- history.CONF_EXCLUDE: {
- history.CONF_DOMAINS: ["thermostat"],
- history.CONF_ENTITIES: ["media_player.test"],
- },
- },
- }
+ states[therm].append(
+ set_state(therm, 21, attributes={"current_temperature": 19.8})
)
- self.check_significant_states(zero, four, states, config)
-
- def test_get_significant_states_are_ordered(self):
- """Test order of results from get_significant_states.
-
- When entity ids are given, the results should be returned with the data
- in the same order.
- """
- zero, four, states = self.record_states()
- entity_ids = ["media_player.test", "media_player.test2"]
- hist = history.get_significant_states(
- self.hass, zero, four, entity_ids, filters=history.Filters()
+ states[therm2].append(
+ set_state(therm2, 20, attributes={"current_temperature": 19})
)
- assert list(hist.keys()) == entity_ids
- entity_ids = ["media_player.test2", "media_player.test"]
- hist = history.get_significant_states(
- self.hass, zero, four, entity_ids, filters=history.Filters()
+
+ with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three):
+ states[mp].append(
+ set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)})
)
- assert list(hist.keys()) == entity_ids
-
- def test_get_significant_states_only(self):
- """Test significant states when significant_states_only is set."""
- self.test_setup()
- entity_id = "sensor.test"
-
- def set_state(state, **kwargs):
- """Set the state."""
- self.hass.states.set(entity_id, state, **kwargs)
- wait_recording_done(self.hass)
- return self.hass.states.get(entity_id)
-
- start = dt_util.utcnow() - timedelta(minutes=4)
- points = []
- for i in range(1, 4):
- points.append(start + timedelta(minutes=i))
-
- states = []
- with patch(
- "homeassistant.components.recorder.dt_util.utcnow", return_value=start
- ):
- set_state("123", attributes={"attribute": 10.64})
-
- with patch(
- "homeassistant.components.recorder.dt_util.utcnow", return_value=points[0]
- ):
- # Attributes are different, state not
- states.append(set_state("123", attributes={"attribute": 21.42}))
-
- with patch(
- "homeassistant.components.recorder.dt_util.utcnow", return_value=points[1]
- ):
- # state is different, attributes not
- states.append(set_state("32", attributes={"attribute": 21.42}))
-
- with patch(
- "homeassistant.components.recorder.dt_util.utcnow", return_value=points[2]
- ):
- # everything is different
- states.append(set_state("412", attributes={"attribute": 54.23}))
-
- hist = history.get_significant_states(
- self.hass, start, significant_changes_only=True
+ states[mp3].append(
+ set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)})
)
-
- assert len(hist[entity_id]) == 2
- assert states[0] not in hist[entity_id]
- assert states[1] in hist[entity_id]
- assert states[2] in hist[entity_id]
-
- hist = history.get_significant_states(
- self.hass, start, significant_changes_only=False
+ # Attributes changed even though state is the same
+ states[therm].append(
+ set_state(therm, 21, attributes={"current_temperature": 20})
)
- assert len(hist[entity_id]) == 3
- assert states == hist[entity_id]
-
- def check_significant_states(self, zero, four, states, config):
- """Check if significant states are retrieved."""
- filters = history.Filters()
- exclude = config[history.DOMAIN].get(history.CONF_EXCLUDE)
- if exclude:
- filters.excluded_entities = exclude.get(history.CONF_ENTITIES, [])
- filters.excluded_domains = exclude.get(history.CONF_DOMAINS, [])
- include = config[history.DOMAIN].get(history.CONF_INCLUDE)
- if include:
- filters.included_entities = include.get(history.CONF_ENTITIES, [])
- filters.included_domains = include.get(history.CONF_DOMAINS, [])
-
- hist = history.get_significant_states(self.hass, zero, four, filters=filters)
- assert states == hist
-
- def record_states(self):
- """Record some test states.
-
- We inject a bunch of state updates from media player, zone and
- thermostat.
- """
- self.test_setup()
- mp = "media_player.test"
- mp2 = "media_player.test2"
- mp3 = "media_player.test3"
- therm = "thermostat.test"
- therm2 = "thermostat.test2"
- zone = "zone.home"
- script_c = "script.can_cancel_this_one"
-
- def set_state(entity_id, state, **kwargs):
- """Set the state."""
- self.hass.states.set(entity_id, state, **kwargs)
- wait_recording_done(self.hass)
- return self.hass.states.get(entity_id)
-
- zero = dt_util.utcnow()
- one = zero + timedelta(seconds=1)
- two = one + timedelta(seconds=1)
- three = two + timedelta(seconds=1)
- four = three + timedelta(seconds=1)
-
- states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []}
- with patch(
- "homeassistant.components.recorder.dt_util.utcnow", return_value=one
- ):
- states[mp].append(
- set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)})
- )
- states[mp].append(
- set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)})
- )
- states[mp2].append(
- set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)})
- )
- states[mp3].append(
- set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)})
- )
- states[therm].append(
- set_state(therm, 20, attributes={"current_temperature": 19.5})
- )
-
- with patch(
- "homeassistant.components.recorder.dt_util.utcnow", return_value=two
- ):
- # This state will be skipped only different in time
- set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)})
- # This state will be skipped because domain is excluded
- set_state(zone, "zoning")
- states[script_c].append(
- set_state(script_c, "off", attributes={"can_cancel": True})
- )
- states[therm].append(
- set_state(therm, 21, attributes={"current_temperature": 19.8})
- )
- states[therm2].append(
- set_state(therm2, 20, attributes={"current_temperature": 19})
- )
-
- with patch(
- "homeassistant.components.recorder.dt_util.utcnow", return_value=three
- ):
- states[mp].append(
- set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)})
- )
- states[mp3].append(
- set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)})
- )
- # Attributes changed even though state is the same
- states[therm].append(
- set_state(therm, 21, attributes={"current_temperature": 20})
- )
-
- return zero, four, states
+ return zero, four, states
async def test_fetch_period_api(hass, hass_client):
diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py
index 62e3959f4ad40f..f074003ab86dfb 100644
--- a/tests/components/history_stats/test_sensor.py
+++ b/tests/components/history_stats/test_sensor.py
@@ -162,12 +162,11 @@ def test_measure(self):
with patch(
"homeassistant.components.history.state_changes_during_period",
return_value=fake_states,
- ):
- with patch("homeassistant.components.history.get_state", return_value=None):
- sensor1.update()
- sensor2.update()
- sensor3.update()
- sensor4.update()
+ ), patch("homeassistant.components.history.get_state", return_value=None):
+ sensor1.update()
+ sensor2.update()
+ sensor3.update()
+ sensor4.update()
assert sensor1.state == 0.5
assert sensor2.state is None
@@ -246,12 +245,11 @@ def test_measure_multiple(self):
with patch(
"homeassistant.components.history.state_changes_during_period",
return_value=fake_states,
- ):
- with patch("homeassistant.components.history.get_state", return_value=None):
- sensor1.update()
- sensor2.update()
- sensor3.update()
- sensor4.update()
+ ), patch("homeassistant.components.history.get_state", return_value=None):
+ sensor1.update()
+ sensor2.update()
+ sensor3.update()
+ sensor4.update()
assert sensor1.state == 0.5
assert sensor2.state is None
diff --git a/tests/components/hive/test_config_flow.py b/tests/components/hive/test_config_flow.py
new file mode 100644
index 00000000000000..dae69eebd96070
--- /dev/null
+++ b/tests/components/hive/test_config_flow.py
@@ -0,0 +1,576 @@
+"""Test the Hive config flow."""
+from unittest.mock import patch
+
+from apyhiveapi.helper import hive_exceptions
+
+from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.components.hive.const import CONF_CODE, DOMAIN
+from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
+
+from tests.common import MockConfigEntry
+
+USERNAME = "username@home-assistant.com"
+UPDATED_USERNAME = "updated_username@home-assistant.com"
+PASSWORD = "test-password"
+UPDATED_PASSWORD = "updated-password"
+INCORRECT_PASSWORD = "incoreect-password"
+SCAN_INTERVAL = 120
+UPDATED_SCAN_INTERVAL = 60
+MFA_CODE = "1234"
+MFA_RESEND_CODE = "0000"
+MFA_INVALID_CODE = "HIVE"
+
+
+async def test_import_flow(hass):
+ """Check import flow."""
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.login",
+ return_value={
+ "ChallengeName": "SUCCESS",
+ "AuthenticationResult": {
+ "RefreshToken": "mock-refresh-token",
+ "AccessToken": "mock-access-token",
+ },
+ },
+ ), patch(
+ "homeassistant.components.hive.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.hive.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == USERNAME
+ assert result["data"] == {
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ "tokens": {
+ "AuthenticationResult": {
+ "AccessToken": "mock-access-token",
+ "RefreshToken": "mock-refresh-token",
+ },
+ "ChallengeName": "SUCCESS",
+ },
+ }
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_user_flow(hass):
+ """Test the user flow."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.login",
+ return_value={
+ "ChallengeName": "SUCCESS",
+ "AuthenticationResult": {
+ "RefreshToken": "mock-refresh-token",
+ "AccessToken": "mock-access-token",
+ },
+ },
+ ), patch(
+ "homeassistant.components.hive.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.hive.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result2["title"] == USERNAME
+ assert result2["data"] == {
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ "tokens": {
+ "AuthenticationResult": {
+ "AccessToken": "mock-access-token",
+ "RefreshToken": "mock-refresh-token",
+ },
+ "ChallengeName": "SUCCESS",
+ },
+ }
+
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+
+
+async def test_user_flow_2fa(hass):
+ """Test user flow with 2FA."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.login",
+ return_value={
+ "ChallengeName": "SMS_MFA",
+ },
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ },
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == CONF_CODE
+ assert result2["errors"] == {}
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.sms_2fa",
+ return_value={
+ "ChallengeName": "SUCCESS",
+ "AuthenticationResult": {
+ "RefreshToken": "mock-refresh-token",
+ "AccessToken": "mock-access-token",
+ },
+ },
+ ), patch(
+ "homeassistant.components.hive.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.hive.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result3 = await hass.config_entries.flow.async_configure(
+ result2["flow_id"], {CONF_CODE: MFA_CODE}
+ )
+ await hass.async_block_till_done()
+
+ assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result3["title"] == USERNAME
+ assert result3["data"] == {
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ "tokens": {
+ "AuthenticationResult": {
+ "AccessToken": "mock-access-token",
+ "RefreshToken": "mock-refresh-token",
+ },
+ "ChallengeName": "SUCCESS",
+ },
+ }
+
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+
+
+async def test_reauth_flow(hass):
+ """Test the reauth flow."""
+
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ mock_config = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id=USERNAME,
+ data={
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: INCORRECT_PASSWORD,
+ "tokens": {
+ "AccessToken": "mock-access-token",
+ "RefreshToken": "mock-refresh-token",
+ },
+ },
+ )
+ mock_config.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.login",
+ side_effect=hive_exceptions.HiveInvalidPassword(),
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={
+ "source": config_entries.SOURCE_REAUTH,
+ "unique_id": mock_config.unique_id,
+ },
+ data=mock_config.data,
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "invalid_password"}
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.login",
+ return_value={
+ "ChallengeName": "SUCCESS",
+ "AuthenticationResult": {
+ "RefreshToken": "mock-refresh-token",
+ "AccessToken": "mock-access-token",
+ },
+ },
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: UPDATED_PASSWORD,
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert mock_config.data.get("username") == USERNAME
+ assert mock_config.data.get("password") == UPDATED_PASSWORD
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result2["reason"] == "reauth_successful"
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+
+
+async def test_option_flow(hass):
+ """Test config flow options."""
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title=USERNAME,
+ data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ )
+ entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.options.async_init(
+ entry.entry_id,
+ data=None,
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={CONF_SCAN_INTERVAL: UPDATED_SCAN_INTERVAL}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["data"][CONF_SCAN_INTERVAL] == UPDATED_SCAN_INTERVAL
+
+
+async def test_user_flow_2fa_send_new_code(hass):
+ """Resend a 2FA code if it didn't arrive."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.login",
+ return_value={
+ "ChallengeName": "SMS_MFA",
+ },
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ },
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == CONF_CODE
+ assert result2["errors"] == {}
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.login",
+ return_value={
+ "ChallengeName": "SMS_MFA",
+ },
+ ):
+ result3 = await hass.config_entries.flow.async_configure(
+ result2["flow_id"], {CONF_CODE: MFA_RESEND_CODE}
+ )
+ await hass.async_block_till_done()
+
+ assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result3["step_id"] == CONF_CODE
+ assert result3["errors"] == {}
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.sms_2fa",
+ return_value={
+ "ChallengeName": "SUCCESS",
+ "AuthenticationResult": {
+ "RefreshToken": "mock-refresh-token",
+ "AccessToken": "mock-access-token",
+ },
+ },
+ ), patch(
+ "homeassistant.components.hive.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.hive.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result4 = await hass.config_entries.flow.async_configure(
+ result3["flow_id"], {CONF_CODE: MFA_CODE}
+ )
+ await hass.async_block_till_done()
+
+ assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result4["title"] == USERNAME
+ assert result4["data"] == {
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ "tokens": {
+ "AuthenticationResult": {
+ "AccessToken": "mock-access-token",
+ "RefreshToken": "mock-refresh-token",
+ },
+ "ChallengeName": "SUCCESS",
+ },
+ }
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+
+
+async def test_abort_if_existing_entry(hass):
+ """Check flow abort when an entry already exist."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id=USERNAME,
+ data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ options={CONF_SCAN_INTERVAL: SCAN_INTERVAL},
+ )
+ config_entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_USER},
+ data={
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+
+async def test_user_flow_invalid_username(hass):
+ """Test user flow with invalid username."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.login",
+ side_effect=hive_exceptions.HiveInvalidUsername(),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == "user"
+ assert result2["errors"] == {"base": "invalid_username"}
+
+
+async def test_user_flow_invalid_password(hass):
+ """Test user flow with invalid password."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.login",
+ side_effect=hive_exceptions.HiveInvalidPassword(),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == "user"
+ assert result2["errors"] == {"base": "invalid_password"}
+
+
+async def test_user_flow_no_internet_connection(hass):
+ """Test user flow with no internet connection."""
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.login",
+ side_effect=hive_exceptions.HiveApiError(),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == "user"
+ assert result2["errors"] == {"base": "no_internet_available"}
+
+
+async def test_user_flow_2fa_no_internet_connection(hass):
+ """Test user flow with no internet connection."""
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.login",
+ return_value={
+ "ChallengeName": "SMS_MFA",
+ },
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == CONF_CODE
+ assert result2["errors"] == {}
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.sms_2fa",
+ side_effect=hive_exceptions.HiveApiError(),
+ ):
+ result3 = await hass.config_entries.flow.async_configure(
+ result2["flow_id"],
+ {CONF_CODE: MFA_CODE},
+ )
+
+ assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result3["step_id"] == CONF_CODE
+ assert result3["errors"] == {"base": "no_internet_available"}
+
+
+async def test_user_flow_2fa_invalid_code(hass):
+ """Test user flow with 2FA."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.login",
+ return_value={
+ "ChallengeName": "SMS_MFA",
+ },
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == CONF_CODE
+ assert result2["errors"] == {}
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.sms_2fa",
+ side_effect=hive_exceptions.HiveInvalid2FACode(),
+ ):
+ result3 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_CODE: MFA_INVALID_CODE},
+ )
+ assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result3["step_id"] == CONF_CODE
+ assert result3["errors"] == {"base": "invalid_code"}
+
+
+async def test_user_flow_unknown_error(hass):
+ """Test user flow when unknown error occurs."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.login",
+ return_value={"ChallengeName": "FAILED", "InvalidAuthenticationResult": {}},
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "unknown"}
+
+
+async def test_user_flow_2fa_unknown_error(hass):
+ """Test 2fa flow when unknown error occurs."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.login",
+ return_value={
+ "ChallengeName": "SMS_MFA",
+ },
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == CONF_CODE
+
+ with patch(
+ "homeassistant.components.hive.config_flow.Auth.sms_2fa",
+ return_value={"ChallengeName": "FAILED", "InvalidAuthenticationResult": {}},
+ ):
+ result3 = await hass.config_entries.flow.async_configure(
+ result2["flow_id"],
+ {CONF_CODE: MFA_CODE},
+ )
+ await hass.async_block_till_done()
+
+ assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result3["errors"] == {"base": "unknown"}
diff --git a/tests/components/home_plus_control/__init__.py b/tests/components/home_plus_control/__init__.py
new file mode 100644
index 00000000000000..a9caba13e32afe
--- /dev/null
+++ b/tests/components/home_plus_control/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Legrand Home+ Control integration."""
diff --git a/tests/components/home_plus_control/conftest.py b/tests/components/home_plus_control/conftest.py
new file mode 100644
index 00000000000000..cb9c869002f4c3
--- /dev/null
+++ b/tests/components/home_plus_control/conftest.py
@@ -0,0 +1,106 @@
+"""Test setup and fixtures for component Home+ Control by Legrand."""
+from homepluscontrol.homeplusinteractivemodule import HomePlusInteractiveModule
+from homepluscontrol.homeplusplant import HomePlusPlant
+import pytest
+
+from homeassistant import config_entries
+from homeassistant.components.home_plus_control.const import DOMAIN
+
+from tests.common import MockConfigEntry
+
+CLIENT_ID = "1234"
+CLIENT_SECRET = "5678"
+SUBSCRIPTION_KEY = "12345678901234567890123456789012"
+
+
+@pytest.fixture()
+def mock_config_entry():
+ """Return a fake config entry.
+
+ This is a minimal entry to setup the integration and to ensure that the
+ OAuth access token will not expire.
+ """
+ return MockConfigEntry(
+ domain=DOMAIN,
+ title="Home+ Control",
+ data={
+ "auth_implementation": "home_plus_control",
+ "token": {
+ "refresh_token": "mock-refresh-token",
+ "access_token": "mock-access-token",
+ "type": "Bearer",
+ "expires_in": 9999999999,
+ "expires_at": 9999999999.99999999,
+ "expires_on": 9999999999,
+ },
+ },
+ source="test",
+ connection_class=config_entries.CONN_CLASS_LOCAL_POLL,
+ options={},
+ system_options={"disable_new_entities": False},
+ unique_id=DOMAIN,
+ entry_id="home_plus_control_entry_id",
+ )
+
+
+@pytest.fixture()
+def mock_modules():
+ """Return the full set of mock modules."""
+ plant = HomePlusPlant(
+ id="123456789009876543210", name="My Home", country="ES", oauth_client=None
+ )
+ modules = {
+ "0000000987654321fedcba": HomePlusInteractiveModule(
+ plant,
+ id="0000000987654321fedcba",
+ name="Kitchen Wall Outlet",
+ hw_type="NLP",
+ device="plug",
+ fw="42",
+ reachable=True,
+ ),
+ "0000000887654321fedcba": HomePlusInteractiveModule(
+ plant,
+ id="0000000887654321fedcba",
+ name="Bedroom Wall Outlet",
+ hw_type="NLP",
+ device="light",
+ fw="42",
+ reachable=True,
+ ),
+ "0000000787654321fedcba": HomePlusInteractiveModule(
+ plant,
+ id="0000000787654321fedcba",
+ name="Living Room Ceiling Light",
+ hw_type="NLF",
+ device="light",
+ fw="46",
+ reachable=True,
+ ),
+ "0000000687654321fedcba": HomePlusInteractiveModule(
+ plant,
+ id="0000000687654321fedcba",
+ name="Dining Room Ceiling Light",
+ hw_type="NLF",
+ device="light",
+ fw="46",
+ reachable=True,
+ ),
+ "0000000587654321fedcba": HomePlusInteractiveModule(
+ plant,
+ id="0000000587654321fedcba",
+ name="Dining Room Wall Outlet",
+ hw_type="NLP",
+ device="plug",
+ fw="42",
+ reachable=True,
+ ),
+ }
+
+ # Set lights off and plugs on
+ for mod_stat in modules.values():
+ mod_stat.status = "on"
+ if mod_stat.device == "light":
+ mod_stat.status = "off"
+
+ return modules
diff --git a/tests/components/home_plus_control/test_config_flow.py b/tests/components/home_plus_control/test_config_flow.py
new file mode 100644
index 00000000000000..4a7dbd3d3ee4e3
--- /dev/null
+++ b/tests/components/home_plus_control/test_config_flow.py
@@ -0,0 +1,192 @@
+"""Test the Legrand Home+ Control config flow."""
+from unittest.mock import patch
+
+from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.components.home_plus_control.const import (
+ CONF_SUBSCRIPTION_KEY,
+ DOMAIN,
+ OAUTH2_AUTHORIZE,
+ OAUTH2_TOKEN,
+)
+from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
+from homeassistant.helpers import config_entry_oauth2_flow
+
+from tests.common import MockConfigEntry
+from tests.components.home_plus_control.conftest import (
+ CLIENT_ID,
+ CLIENT_SECRET,
+ SUBSCRIPTION_KEY,
+)
+
+
+async def test_full_flow(
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
+):
+ """Check full flow."""
+ assert await setup.async_setup_component(
+ hass,
+ "home_plus_control",
+ {
+ "home_plus_control": {
+ CONF_CLIENT_ID: CLIENT_ID,
+ CONF_CLIENT_SECRET: CLIENT_SECRET,
+ CONF_SUBSCRIPTION_KEY: SUBSCRIPTION_KEY,
+ },
+ },
+ )
+ result = await hass.config_entries.flow.async_init(
+ "home_plus_control", context={"source": config_entries.SOURCE_USER}
+ )
+
+ state = config_entry_oauth2_flow._encode_jwt( # pylint: disable=protected-access
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
+ assert result["step_id"] == "auth"
+ assert result["url"] == (
+ f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
+ "&redirect_uri=https://example.com/auth/external/callback"
+ f"&state={state}"
+ )
+
+ client = await aiohttp_client(hass.http.app)
+ resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
+ assert resp.status == 200
+ assert resp.headers["content-type"] == "text/html; charset=utf-8"
+
+ aioclient_mock.post(
+ OAUTH2_TOKEN,
+ json={
+ "refresh_token": "mock-refresh-token",
+ "access_token": "mock-access-token",
+ "type": "Bearer",
+ "expires_in": 60,
+ },
+ )
+
+ with patch(
+ "homeassistant.components.home_plus_control.async_setup_entry",
+ return_value=True,
+ ) as mock_setup:
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "Home+ Control"
+ config_data = result["data"]
+ assert config_data["token"]["refresh_token"] == "mock-refresh-token"
+ assert config_data["token"]["access_token"] == "mock-access-token"
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+ assert len(mock_setup.mock_calls) == 1
+
+
+async def test_abort_if_entry_in_progress(hass, current_request_with_host):
+ """Check flow abort when an entry is already in progress."""
+ assert await setup.async_setup_component(
+ hass,
+ "home_plus_control",
+ {
+ "home_plus_control": {
+ CONF_CLIENT_ID: CLIENT_ID,
+ CONF_CLIENT_SECRET: CLIENT_SECRET,
+ CONF_SUBSCRIPTION_KEY: SUBSCRIPTION_KEY,
+ },
+ },
+ )
+
+ # Start one flow
+ result = await hass.config_entries.flow.async_init(
+ "home_plus_control", context={"source": config_entries.SOURCE_USER}
+ )
+
+ # Attempt to start another flow
+ result = await hass.config_entries.flow.async_init(
+ "home_plus_control", context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_in_progress"
+
+
+async def test_abort_if_entry_exists(hass, current_request_with_host):
+ """Check flow abort when an entry already exists."""
+ existing_entry = MockConfigEntry(domain=DOMAIN)
+ existing_entry.add_to_hass(hass)
+
+ assert await setup.async_setup_component(
+ hass,
+ "home_plus_control",
+ {
+ "home_plus_control": {
+ CONF_CLIENT_ID: CLIENT_ID,
+ CONF_CLIENT_SECRET: CLIENT_SECRET,
+ CONF_SUBSCRIPTION_KEY: SUBSCRIPTION_KEY,
+ },
+ "http": {},
+ },
+ )
+
+ result = await hass.config_entries.flow.async_init(
+ "home_plus_control", context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "single_instance_allowed"
+
+
+async def test_abort_if_invalid_token(
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
+):
+ """Check flow abort when the token has an invalid value."""
+ assert await setup.async_setup_component(
+ hass,
+ "home_plus_control",
+ {
+ "home_plus_control": {
+ CONF_CLIENT_ID: CLIENT_ID,
+ CONF_CLIENT_SECRET: CLIENT_SECRET,
+ CONF_SUBSCRIPTION_KEY: SUBSCRIPTION_KEY,
+ },
+ },
+ )
+ result = await hass.config_entries.flow.async_init(
+ "home_plus_control", context={"source": config_entries.SOURCE_USER}
+ )
+
+ state = config_entry_oauth2_flow._encode_jwt( # pylint: disable=protected-access
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
+ assert result["step_id"] == "auth"
+ assert result["url"] == (
+ f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
+ "&redirect_uri=https://example.com/auth/external/callback"
+ f"&state={state}"
+ )
+
+ client = await aiohttp_client(hass.http.app)
+ resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
+ assert resp.status == 200
+ assert resp.headers["content-type"] == "text/html; charset=utf-8"
+
+ aioclient_mock.post(
+ OAUTH2_TOKEN,
+ json={
+ "refresh_token": "mock-refresh-token",
+ "access_token": "mock-access-token",
+ "type": "Bearer",
+ "expires_in": "non-integer",
+ },
+ )
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "oauth_error"
diff --git a/tests/components/home_plus_control/test_init.py b/tests/components/home_plus_control/test_init.py
new file mode 100644
index 00000000000000..e48a9dc1f85e54
--- /dev/null
+++ b/tests/components/home_plus_control/test_init.py
@@ -0,0 +1,75 @@
+"""Test the Legrand Home+ Control integration."""
+from unittest.mock import patch
+
+from homeassistant import config_entries, setup
+from homeassistant.components.home_plus_control.const import (
+ CONF_SUBSCRIPTION_KEY,
+ DOMAIN,
+)
+from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
+
+from tests.components.home_plus_control.conftest import (
+ CLIENT_ID,
+ CLIENT_SECRET,
+ SUBSCRIPTION_KEY,
+)
+
+
+async def test_loading(hass, mock_config_entry):
+ """Test component loading."""
+ mock_config_entry.add_to_hass(hass)
+ with patch(
+ "homeassistant.components.home_plus_control.api.HomePlusControlAsyncApi.async_get_modules",
+ return_value={},
+ ) as mock_check:
+ await setup.async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ "home_plus_control": {
+ CONF_CLIENT_ID: CLIENT_ID,
+ CONF_CLIENT_SECRET: CLIENT_SECRET,
+ CONF_SUBSCRIPTION_KEY: SUBSCRIPTION_KEY,
+ },
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert len(mock_check.mock_calls) == 1
+ assert mock_config_entry.state == config_entries.ENTRY_STATE_LOADED
+
+
+async def test_loading_with_no_config(hass, mock_config_entry):
+ """Test component loading failure when it has not configuration."""
+ mock_config_entry.add_to_hass(hass)
+ await setup.async_setup_component(hass, DOMAIN, {})
+ # Component setup fails because the oauth2 implementation could not be registered
+ assert mock_config_entry.state == config_entries.ENTRY_STATE_SETUP_ERROR
+
+
+async def test_unloading(hass, mock_config_entry):
+ """Test component unloading."""
+ mock_config_entry.add_to_hass(hass)
+ with patch(
+ "homeassistant.components.home_plus_control.api.HomePlusControlAsyncApi.async_get_modules",
+ return_value={},
+ ) as mock_check:
+ await setup.async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ "home_plus_control": {
+ CONF_CLIENT_ID: CLIENT_ID,
+ CONF_CLIENT_SECRET: CLIENT_SECRET,
+ CONF_SUBSCRIPTION_KEY: SUBSCRIPTION_KEY,
+ },
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert len(mock_check.mock_calls) == 1
+ assert mock_config_entry.state == config_entries.ENTRY_STATE_LOADED
+
+ # We now unload the entry
+ assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
+ assert mock_config_entry.state == config_entries.ENTRY_STATE_NOT_LOADED
diff --git a/tests/components/home_plus_control/test_switch.py b/tests/components/home_plus_control/test_switch.py
new file mode 100644
index 00000000000000..f699fe08d05772
--- /dev/null
+++ b/tests/components/home_plus_control/test_switch.py
@@ -0,0 +1,464 @@
+"""Test the Legrand Home+ Control switch platform."""
+import datetime as dt
+from unittest.mock import patch
+
+from homepluscontrol.homeplusapi import HomePlusControlApiError
+
+from homeassistant import config_entries, setup
+from homeassistant.components.home_plus_control.const import (
+ CONF_SUBSCRIPTION_KEY,
+ DOMAIN,
+)
+from homeassistant.const import (
+ CONF_CLIENT_ID,
+ CONF_CLIENT_SECRET,
+ STATE_OFF,
+ STATE_ON,
+ STATE_UNAVAILABLE,
+)
+
+from tests.common import async_fire_time_changed
+from tests.components.home_plus_control.conftest import (
+ CLIENT_ID,
+ CLIENT_SECRET,
+ SUBSCRIPTION_KEY,
+)
+
+
+def entity_assertions(
+ hass,
+ num_exp_entities,
+ num_exp_devices=None,
+ expected_entities=None,
+ expected_devices=None,
+):
+ """Assert number of entities and devices."""
+ entity_reg = hass.helpers.entity_registry.async_get(hass)
+ device_reg = hass.helpers.device_registry.async_get(hass)
+
+ if num_exp_devices is None:
+ num_exp_devices = num_exp_entities
+
+ assert len(entity_reg.entities) == num_exp_entities
+ assert len(device_reg.devices) == num_exp_devices
+
+ if expected_entities is not None:
+ for exp_entity_id, present in expected_entities.items():
+ assert bool(entity_reg.async_get(exp_entity_id)) == present
+
+ if expected_devices is not None:
+ for exp_device_id, present in expected_devices.items():
+ assert bool(device_reg.async_get(exp_device_id)) == present
+
+
+def one_entity_state(hass, device_uid):
+ """Assert the presence of an entity and return its state."""
+ entity_reg = hass.helpers.entity_registry.async_get(hass)
+ device_reg = hass.helpers.device_registry.async_get(hass)
+
+ device_id = device_reg.async_get_device({(DOMAIN, device_uid)}).id
+ entity_entries = hass.helpers.entity_registry.async_entries_for_device(
+ entity_reg, device_id
+ )
+
+ assert len(entity_entries) == 1
+ entity_entry = entity_entries[0]
+ return hass.states.get(entity_entry.entity_id).state
+
+
+async def test_plant_update(
+ hass,
+ mock_config_entry,
+ mock_modules,
+):
+ """Test entity and device loading."""
+ # Load the entry
+ mock_config_entry.add_to_hass(hass)
+ with patch(
+ "homeassistant.components.home_plus_control.api.HomePlusControlAsyncApi.async_get_modules",
+ return_value=mock_modules,
+ ) as mock_check:
+ await setup.async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ "home_plus_control": {
+ CONF_CLIENT_ID: CLIENT_ID,
+ CONF_CLIENT_SECRET: CLIENT_SECRET,
+ CONF_SUBSCRIPTION_KEY: SUBSCRIPTION_KEY,
+ },
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(mock_check.mock_calls) == 1
+
+ # Check the entities and devices
+ entity_assertions(
+ hass,
+ num_exp_entities=5,
+ expected_entities={
+ "switch.dining_room_wall_outlet": True,
+ "switch.kitchen_wall_outlet": True,
+ },
+ )
+
+
+async def test_plant_topology_reduction_change(
+ hass,
+ mock_config_entry,
+ mock_modules,
+):
+ """Test an entity leaving the plant topology."""
+ # Load the entry
+ mock_config_entry.add_to_hass(hass)
+ with patch(
+ "homeassistant.components.home_plus_control.api.HomePlusControlAsyncApi.async_get_modules",
+ return_value=mock_modules,
+ ) as mock_check:
+ await setup.async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ "home_plus_control": {
+ CONF_CLIENT_ID: CLIENT_ID,
+ CONF_CLIENT_SECRET: CLIENT_SECRET,
+ CONF_SUBSCRIPTION_KEY: SUBSCRIPTION_KEY,
+ },
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(mock_check.mock_calls) == 1
+
+ # Check the entities and devices - 5 mock entities
+ entity_assertions(
+ hass,
+ num_exp_entities=5,
+ expected_entities={
+ "switch.dining_room_wall_outlet": True,
+ "switch.kitchen_wall_outlet": True,
+ },
+ )
+
+ # Now we refresh the topology with one entity less
+ mock_modules.pop("0000000987654321fedcba")
+ with patch(
+ "homeassistant.components.home_plus_control.api.HomePlusControlAsyncApi.async_get_modules",
+ return_value=mock_modules,
+ ) as mock_check:
+ async_fire_time_changed(
+ hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100)
+ )
+ await hass.async_block_till_done()
+ assert len(mock_check.mock_calls) == 1
+
+ # Check for plant, topology and module status - this time only 4 left
+ entity_assertions(
+ hass,
+ num_exp_entities=4,
+ expected_entities={
+ "switch.dining_room_wall_outlet": True,
+ "switch.kitchen_wall_outlet": False,
+ },
+ )
+
+
+async def test_plant_topology_increase_change(
+ hass,
+ mock_config_entry,
+ mock_modules,
+):
+ """Test an entity entering the plant topology."""
+ # Remove one module initially
+ new_module = mock_modules.pop("0000000987654321fedcba")
+
+ # Load the entry
+ mock_config_entry.add_to_hass(hass)
+ with patch(
+ "homeassistant.components.home_plus_control.api.HomePlusControlAsyncApi.async_get_modules",
+ return_value=mock_modules,
+ ) as mock_check:
+ await setup.async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ "home_plus_control": {
+ CONF_CLIENT_ID: CLIENT_ID,
+ CONF_CLIENT_SECRET: CLIENT_SECRET,
+ CONF_SUBSCRIPTION_KEY: SUBSCRIPTION_KEY,
+ },
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(mock_check.mock_calls) == 1
+
+ # Check the entities and devices - we have 4 entities to start with
+ entity_assertions(
+ hass,
+ num_exp_entities=4,
+ expected_entities={
+ "switch.dining_room_wall_outlet": True,
+ "switch.kitchen_wall_outlet": False,
+ },
+ )
+
+ # Now we refresh the topology with one entity more
+ mock_modules["0000000987654321fedcba"] = new_module
+ with patch(
+ "homeassistant.components.home_plus_control.api.HomePlusControlAsyncApi.async_get_modules",
+ return_value=mock_modules,
+ ) as mock_check:
+ async_fire_time_changed(
+ hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100)
+ )
+ await hass.async_block_till_done()
+ assert len(mock_check.mock_calls) == 1
+
+ entity_assertions(
+ hass,
+ num_exp_entities=5,
+ expected_entities={
+ "switch.dining_room_wall_outlet": True,
+ "switch.kitchen_wall_outlet": True,
+ },
+ )
+
+
+async def test_module_status_unavailable(hass, mock_config_entry, mock_modules):
+ """Test a module becoming unreachable in the plant."""
+ # Load the entry
+ mock_config_entry.add_to_hass(hass)
+ with patch(
+ "homeassistant.components.home_plus_control.api.HomePlusControlAsyncApi.async_get_modules",
+ return_value=mock_modules,
+ ) as mock_check:
+ await setup.async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ "home_plus_control": {
+ CONF_CLIENT_ID: CLIENT_ID,
+ CONF_CLIENT_SECRET: CLIENT_SECRET,
+ CONF_SUBSCRIPTION_KEY: SUBSCRIPTION_KEY,
+ },
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(mock_check.mock_calls) == 1
+
+ # Check the entities and devices - 5 mock entities
+ entity_assertions(
+ hass,
+ num_exp_entities=5,
+ expected_entities={
+ "switch.dining_room_wall_outlet": True,
+ "switch.kitchen_wall_outlet": True,
+ },
+ )
+
+ # Confirm the availability of this particular entity
+ test_entity_uid = "0000000987654321fedcba"
+ test_entity_state = one_entity_state(hass, test_entity_uid)
+ assert test_entity_state == STATE_ON
+
+ # Now we refresh the topology with the module being unreachable
+ mock_modules["0000000987654321fedcba"].reachable = False
+
+ with patch(
+ "homeassistant.components.home_plus_control.api.HomePlusControlAsyncApi.async_get_modules",
+ return_value=mock_modules,
+ ) as mock_check:
+ async_fire_time_changed(
+ hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100)
+ )
+ await hass.async_block_till_done()
+ assert len(mock_check.mock_calls) == 1
+
+ # Assert the devices and entities
+ entity_assertions(
+ hass,
+ num_exp_entities=5,
+ expected_entities={
+ "switch.dining_room_wall_outlet": True,
+ "switch.kitchen_wall_outlet": True,
+ },
+ )
+ await hass.async_block_till_done()
+ # The entity is present, but not available
+ test_entity_state = one_entity_state(hass, test_entity_uid)
+ assert test_entity_state == STATE_UNAVAILABLE
+
+
+async def test_module_status_available(
+ hass,
+ mock_config_entry,
+ mock_modules,
+):
+ """Test a module becoming reachable in the plant."""
+ # Set the module initially unreachable
+ mock_modules["0000000987654321fedcba"].reachable = False
+
+ # Load the entry
+ mock_config_entry.add_to_hass(hass)
+ with patch(
+ "homeassistant.components.home_plus_control.api.HomePlusControlAsyncApi.async_get_modules",
+ return_value=mock_modules,
+ ) as mock_check:
+ await setup.async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ "home_plus_control": {
+ CONF_CLIENT_ID: CLIENT_ID,
+ CONF_CLIENT_SECRET: CLIENT_SECRET,
+ CONF_SUBSCRIPTION_KEY: SUBSCRIPTION_KEY,
+ },
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(mock_check.mock_calls) == 1
+
+ # Assert the devices and entities
+ entity_assertions(
+ hass,
+ num_exp_entities=5,
+ expected_entities={
+ "switch.dining_room_wall_outlet": True,
+ "switch.kitchen_wall_outlet": True,
+ },
+ )
+
+ # This particular entity is not available
+ test_entity_uid = "0000000987654321fedcba"
+ test_entity_state = one_entity_state(hass, test_entity_uid)
+ assert test_entity_state == STATE_UNAVAILABLE
+
+ # Now we refresh the topology with the module being reachable
+ mock_modules["0000000987654321fedcba"].reachable = True
+ with patch(
+ "homeassistant.components.home_plus_control.api.HomePlusControlAsyncApi.async_get_modules",
+ return_value=mock_modules,
+ ) as mock_check:
+ async_fire_time_changed(
+ hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100)
+ )
+ await hass.async_block_till_done()
+ assert len(mock_check.mock_calls) == 1
+
+ # Assert the devices and entities remain the same
+ entity_assertions(
+ hass,
+ num_exp_entities=5,
+ expected_entities={
+ "switch.dining_room_wall_outlet": True,
+ "switch.kitchen_wall_outlet": True,
+ },
+ )
+
+ # Now the entity is available
+ test_entity_uid = "0000000987654321fedcba"
+ test_entity_state = one_entity_state(hass, test_entity_uid)
+ assert test_entity_state == STATE_ON
+
+
+async def test_initial_api_error(
+ hass,
+ mock_config_entry,
+ mock_modules,
+):
+ """Test an API error on initial call."""
+ # Load the entry
+ mock_config_entry.add_to_hass(hass)
+ with patch(
+ "homeassistant.components.home_plus_control.api.HomePlusControlAsyncApi.async_get_modules",
+ return_value=mock_modules,
+ side_effect=HomePlusControlApiError,
+ ) as mock_check:
+ await setup.async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ "home_plus_control": {
+ CONF_CLIENT_ID: CLIENT_ID,
+ CONF_CLIENT_SECRET: CLIENT_SECRET,
+ CONF_SUBSCRIPTION_KEY: SUBSCRIPTION_KEY,
+ },
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(mock_check.mock_calls) == 1
+
+ # The component has been loaded
+ assert mock_config_entry.state == config_entries.ENTRY_STATE_LOADED
+
+ # Check the entities and devices - None have been configured
+ entity_assertions(hass, num_exp_entities=0)
+
+
+async def test_update_with_api_error(
+ hass,
+ mock_config_entry,
+ mock_modules,
+):
+ """Test an API timeout when updating the module data."""
+ # Load the entry
+ mock_config_entry.add_to_hass(hass)
+ with patch(
+ "homeassistant.components.home_plus_control.api.HomePlusControlAsyncApi.async_get_modules",
+ return_value=mock_modules,
+ ) as mock_check:
+ await setup.async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ "home_plus_control": {
+ CONF_CLIENT_ID: CLIENT_ID,
+ CONF_CLIENT_SECRET: CLIENT_SECRET,
+ CONF_SUBSCRIPTION_KEY: SUBSCRIPTION_KEY,
+ },
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(mock_check.mock_calls) == 1
+
+ # The component has been loaded
+ assert mock_config_entry.state == config_entries.ENTRY_STATE_LOADED
+
+ # Check the entities and devices - all entities should be there
+ entity_assertions(
+ hass,
+ num_exp_entities=5,
+ expected_entities={
+ "switch.dining_room_wall_outlet": True,
+ "switch.kitchen_wall_outlet": True,
+ },
+ )
+ for test_entity_uid in mock_modules:
+ test_entity_state = one_entity_state(hass, test_entity_uid)
+ assert test_entity_state in (STATE_ON, STATE_OFF)
+
+ # Attempt to update the data, but API update fails
+ with patch(
+ "homeassistant.components.home_plus_control.api.HomePlusControlAsyncApi.async_get_modules",
+ return_value=mock_modules,
+ side_effect=HomePlusControlApiError,
+ ) as mock_check:
+ async_fire_time_changed(
+ hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100)
+ )
+ await hass.async_block_till_done()
+ assert len(mock_check.mock_calls) == 1
+
+ # Assert the devices and entities - all should still be present
+ entity_assertions(
+ hass,
+ num_exp_entities=5,
+ expected_entities={
+ "switch.dining_room_wall_outlet": True,
+ "switch.kitchen_wall_outlet": True,
+ },
+ )
+
+ # This entity has not returned a status, so appears as unavailable
+ for test_entity_uid in mock_modules:
+ test_entity_state = one_entity_state(hass, test_entity_uid)
+ assert test_entity_state == STATE_UNAVAILABLE
diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py
index ef830c7ee77837..2e2eaf991afd2d 100644
--- a/tests/components/homeassistant/test_init.py
+++ b/tests/components/homeassistant/test_init.py
@@ -11,6 +11,7 @@
from homeassistant import config
import homeassistant.components as comps
from homeassistant.components.homeassistant import (
+ ATTR_ENTRY_ID,
SERVICE_CHECK_CONFIG,
SERVICE_RELOAD_CORE_CONFIG,
SERVICE_SET_LOCATION,
@@ -34,9 +35,11 @@
from homeassistant.setup import async_setup_component
from tests.common import (
+ MockConfigEntry,
async_capture_events,
async_mock_service,
get_test_home_assistant,
+ mock_registry,
mock_service,
patch_yaml_files,
)
@@ -134,28 +137,28 @@ def test_turn_on_without_entities(self):
calls = mock_service(self.hass, "light", SERVICE_TURN_ON)
turn_on(self.hass)
self.hass.block_till_done()
- assert 0 == len(calls)
+ assert len(calls) == 0
def test_turn_on(self):
"""Test turn_on method."""
calls = mock_service(self.hass, "light", SERVICE_TURN_ON)
turn_on(self.hass, "light.Ceiling")
self.hass.block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
def test_turn_off(self):
"""Test turn_off method."""
calls = mock_service(self.hass, "light", SERVICE_TURN_OFF)
turn_off(self.hass, "light.Bowl")
self.hass.block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
def test_toggle(self):
"""Test toggle method."""
calls = mock_service(self.hass, "light", SERVICE_TOGGLE)
toggle(self.hass, "light.Bowl")
self.hass.block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
@patch("homeassistant.config.os.path.isfile", Mock(return_value=True))
def test_reload_core_conf(self):
@@ -385,3 +388,62 @@ async def test_not_allowing_recursion(hass, caplog):
f"Called service homeassistant.{service} with invalid entities homeassistant.light"
in caplog.text
), service
+
+
+async def test_reload_config_entry_by_entity_id(hass):
+ """Test being able to reload a config entry by entity_id."""
+ await async_setup_component(hass, "homeassistant", {})
+ entity_reg = mock_registry(hass)
+ entry1 = MockConfigEntry(domain="mockdomain")
+ entry1.add_to_hass(hass)
+ entry2 = MockConfigEntry(domain="mockdomain")
+ entry2.add_to_hass(hass)
+ reg_entity1 = entity_reg.async_get_or_create(
+ "binary_sensor", "powerwall", "battery_charging", config_entry=entry1
+ )
+ reg_entity2 = entity_reg.async_get_or_create(
+ "binary_sensor", "powerwall", "battery_status", config_entry=entry2
+ )
+ with patch(
+ "homeassistant.config_entries.ConfigEntries.async_reload",
+ return_value=None,
+ ) as mock_reload:
+ await hass.services.async_call(
+ "homeassistant",
+ "reload_config_entry",
+ {"entity_id": f"{reg_entity1.entity_id},{reg_entity2.entity_id}"},
+ blocking=True,
+ )
+
+ assert len(mock_reload.mock_calls) == 2
+ assert {mock_reload.mock_calls[0][1][0], mock_reload.mock_calls[1][1][0]} == {
+ entry1.entry_id,
+ entry2.entry_id,
+ }
+
+ with pytest.raises(ValueError):
+ await hass.services.async_call(
+ "homeassistant",
+ "reload_config_entry",
+ {"entity_id": "unknown.entity_id"},
+ blocking=True,
+ )
+
+
+async def test_reload_config_entry_by_entry_id(hass):
+ """Test being able to reload a config entry by config entry id."""
+ await async_setup_component(hass, "homeassistant", {})
+
+ with patch(
+ "homeassistant.config_entries.ConfigEntries.async_reload",
+ return_value=None,
+ ) as mock_reload:
+ await hass.services.async_call(
+ "homeassistant",
+ "reload_config_entry",
+ {ATTR_ENTRY_ID: "8955375327824e14ba89e4b29cc3ec9a"},
+ blocking=True,
+ )
+
+ assert len(mock_reload.mock_calls) == 1
+ assert mock_reload.mock_calls[0][1][0] == "8955375327824e14ba89e4b29cc3ec9a"
diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py
index 3098543271833a..610bc371b25102 100644
--- a/tests/components/homeassistant/test_scene.py
+++ b/tests/components/homeassistant/test_scene.py
@@ -8,17 +8,14 @@
from homeassistant.components.homeassistant.scene import EVENT_SCENE_RELOADED
from homeassistant.setup import async_setup_component
-from tests.common import async_mock_service
+from tests.common import async_capture_events, async_mock_service
async def test_reload_config_service(hass):
"""Test the reload config service."""
assert await async_setup_component(hass, "scene", {})
- test_reloaded_event = []
- hass.bus.async_listen(
- EVENT_SCENE_RELOADED, lambda event: test_reloaded_event.append(event)
- )
+ test_reloaded_event = async_capture_events(hass, EVENT_SCENE_RELOADED)
with patch(
"homeassistant.config.load_yaml_config_file",
diff --git a/tests/components/homeassistant/triggers/conftest.py b/tests/components/homeassistant/triggers/conftest.py
index 5c983ba698e5c2..77520a1bf689d9 100644
--- a/tests/components/homeassistant/triggers/conftest.py
+++ b/tests/components/homeassistant/triggers/conftest.py
@@ -1,3 +1,3 @@
"""Conftest for HA triggers."""
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py
index 8fedaac381594d..8ca7caaa0a55af 100644
--- a/tests/components/homeassistant/triggers/test_event.py
+++ b/tests/components/homeassistant/triggers/test_event.py
@@ -17,7 +17,7 @@ def calls(hass):
@pytest.fixture
def context_with_user():
- """Track calls to a mock service."""
+ """Create a context with default user_id."""
return Context(user_id="test_user_id")
@@ -37,6 +37,43 @@ async def test_if_fires_on_event(hass, calls):
{
automation.DOMAIN: {
"trigger": {"platform": "event", "event_type": "test_event"},
+ "action": {
+ "service": "test.automation",
+ "data_template": {"id": "{{ trigger.id}}"},
+ },
+ }
+ },
+ )
+
+ hass.bus.async_fire("test_event", context=context)
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].context.parent_id == context.id
+
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
+ blocking=True,
+ )
+
+ hass.bus.async_fire("test_event")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["id"] == 0
+
+
+async def test_if_fires_on_templated_event(hass, calls):
+ """Test the firing of events."""
+ context = Context()
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger_variables": {"event_type": "test_event"},
+ "trigger": {"platform": "event", "event_type": "{{event_type}}"},
"action": {"service": "test.automation"},
}
},
@@ -161,6 +198,58 @@ async def test_if_fires_on_event_with_data_and_context(hass, calls, context_with
assert len(calls) == 1
+async def test_if_fires_on_event_with_templated_data_and_context(
+ hass, calls, context_with_user
+):
+ """Test the firing of events with templated data and context."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger_variables": {
+ "attr_1_val": "milk",
+ "attr_2_val": "beer",
+ "user_id": context_with_user.user_id,
+ },
+ "trigger": {
+ "platform": "event",
+ "event_type": "test_event",
+ "event_data": {
+ "attr_1": "{{attr_1_val}}",
+ "attr_2": "{{attr_2_val}}",
+ },
+ "context": {"user_id": "{{user_id}}"},
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+
+ hass.bus.async_fire(
+ "test_event",
+ {"attr_1": "milk", "another": "value", "attr_2": "beer"},
+ context=context_with_user,
+ )
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+ hass.bus.async_fire(
+ "test_event",
+ {"attr_1": "milk", "another": "value"},
+ context=context_with_user,
+ )
+ await hass.async_block_till_done()
+ assert len(calls) == 1 # No new call
+
+ hass.bus.async_fire(
+ "test_event",
+ {"attr_1": "milk", "another": "value", "attr_2": "beer"},
+ )
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+
async def test_if_fires_on_event_with_empty_data_and_context_config(
hass, calls, context_with_user
):
diff --git a/tests/components/homeassistant/triggers/test_homeassistant.py b/tests/components/homeassistant/triggers/test_homeassistant.py
index 7ff7e566db0a72..231b81e38622be 100644
--- a/tests/components/homeassistant/triggers/test_homeassistant.py
+++ b/tests/components/homeassistant/triggers/test_homeassistant.py
@@ -16,7 +16,10 @@ async def test_if_fires_on_hass_start(hass):
automation.DOMAIN: {
"alias": "hello",
"trigger": {"platform": "homeassistant", "event": "start"},
- "action": {"service": "test.automation"},
+ "action": {
+ "service": "test.automation",
+ "data_template": {"id": "{{ trigger.id}}"},
+ },
}
}
@@ -39,6 +42,7 @@ async def test_if_fires_on_hass_start(hass):
assert automation.is_on(hass, "automation.hello")
assert len(calls) == 1
+ assert calls[0].data["id"] == 0
async def test_if_fires_on_hass_shutdown(hass):
@@ -53,7 +57,10 @@ async def test_if_fires_on_hass_shutdown(hass):
automation.DOMAIN: {
"alias": "hello",
"trigger": {"platform": "homeassistant", "event": "shutdown"},
- "action": {"service": "test.automation"},
+ "action": {
+ "service": "test.automation",
+ "data_template": {"id": "{{ trigger.id}}"},
+ },
}
},
)
@@ -68,3 +75,4 @@ async def test_if_fires_on_hass_shutdown(hass):
with patch.object(hass.loop, "stop"):
await hass.async_stop()
assert len(calls) == 1
+ assert calls[0].data["id"] == 0
diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py
index b9696fffe06742..38372d098258c1 100644
--- a/tests/components/homeassistant/triggers/test_numeric_state.py
+++ b/tests/components/homeassistant/triggers/test_numeric_state.py
@@ -1,5 +1,6 @@
"""The tests for numeric state automation."""
from datetime import timedelta
+import logging
from unittest.mock import patch
import pytest
@@ -91,7 +92,10 @@ async def test_if_fires_on_entity_change_below(hass, calls, below):
"entity_id": "test.entity",
"below": below,
},
- "action": {"service": "test.automation"},
+ "action": {
+ "service": "test.automation",
+ "data_template": {"id": "{{ trigger.id}}"},
+ },
}
},
)
@@ -113,6 +117,7 @@ async def test_if_fires_on_entity_change_below(hass, calls, below):
hass.states.async_set("test.entity", 9)
await hass.async_block_till_done()
assert len(calls) == 1
+ assert calls[0].data["id"] == 0
@pytest.mark.parametrize("below", (10, "input_number.value_10"))
@@ -240,7 +245,7 @@ async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls, below):
@pytest.mark.parametrize("below", (10, "input_number.value_10"))
-async def test_if_fires_on_initial_entity_below(hass, calls, below):
+async def test_if_not_fires_on_initial_entity_below(hass, calls, below):
"""Test the firing when starting with a match."""
hass.states.async_set("test.entity", 9)
await hass.async_block_till_done()
@@ -260,14 +265,14 @@ async def test_if_fires_on_initial_entity_below(hass, calls, below):
},
)
- # Fire on first update even if initial state was already below
+ # Do not fire on first update when initial state was already below
hass.states.async_set("test.entity", 8)
await hass.async_block_till_done()
- assert len(calls) == 1
+ assert len(calls) == 0
@pytest.mark.parametrize("above", (10, "input_number.value_10"))
-async def test_if_fires_on_initial_entity_above(hass, calls, above):
+async def test_if_not_fires_on_initial_entity_above(hass, calls, above):
"""Test the firing when starting with a match."""
hass.states.async_set("test.entity", 11)
await hass.async_block_till_done()
@@ -287,10 +292,10 @@ async def test_if_fires_on_initial_entity_above(hass, calls, above):
},
)
- # Fire on first update even if initial state was already above
+ # Do not fire on first update when initial state was already above
hass.states.async_set("test.entity", 12)
await hass.async_block_till_done()
- assert len(calls) == 1
+ assert len(calls) == 0
@pytest.mark.parametrize("above", (10, "input_number.value_10"))
@@ -319,6 +324,28 @@ async def test_if_fires_on_entity_change_above(hass, calls, above):
assert len(calls) == 1
+async def test_if_fires_on_entity_unavailable_at_startup(hass, calls):
+ """Test the firing with changed entity at startup."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger": {
+ "platform": "numeric_state",
+ "entity_id": "test.entity",
+ "above": 10,
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+ # 11 is above 10
+ hass.states.async_set("test.entity", 11)
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+
@pytest.mark.parametrize("above", (10, "input_number.value_10"))
async def test_if_fires_on_entity_change_below_to_above(hass, calls, above):
"""Test the firing with changed entity."""
@@ -572,6 +599,34 @@ async def test_if_not_fires_if_entity_not_match(hass, calls, below):
assert len(calls) == 0
+async def test_if_not_fires_and_warns_if_below_entity_unknown(hass, caplog, calls):
+ """Test if warns with unknown below entity."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger": {
+ "platform": "numeric_state",
+ "entity_id": "test.entity",
+ "below": "input_number.unknown",
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+
+ caplog.clear()
+ caplog.set_level(logging.WARNING)
+
+ hass.states.async_set("test.entity", 1)
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ assert len(caplog.record_tuples) == 1
+ assert caplog.record_tuples[0][1] == logging.WARNING
+
+
@pytest.mark.parametrize("below", (10, "input_number.value_10"))
async def test_if_fires_on_entity_change_below_with_attribute(hass, calls, below):
"""Test attributes change."""
@@ -1177,7 +1232,7 @@ async def test_wait_template_with_trigger(hass, calls, above):
hass.states.async_set("test.entity", "8")
await hass.async_block_till_done()
assert len(calls) == 1
- assert "numeric_state - test.entity - 12" == calls[0].data["some"]
+ assert calls[0].data["some"] == "numeric_state - test.entity - 12"
@pytest.mark.parametrize(
@@ -1420,6 +1475,42 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls, above, below)
assert len(calls) == 1
+async def test_if_not_fires_on_error_with_for_template(hass, calls):
+ """Test for not firing on error with for template."""
+ hass.states.async_set("test.entity", 0)
+ await hass.async_block_till_done()
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger": {
+ "platform": "numeric_state",
+ "entity_id": "test.entity",
+ "above": 100,
+ "for": "00:00:05",
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+
+ hass.states.async_set("test.entity", 101)
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3))
+ hass.states.async_set("test.entity", "unavailable")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3))
+ hass.states.async_set("test.entity", 101)
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+
@pytest.mark.parametrize(
"above, below",
(
@@ -1621,3 +1712,93 @@ async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop(
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
assert len(calls) == 1
+
+
+@pytest.mark.parametrize(
+ "above, below",
+ ((8, 12),),
+)
+async def test_variables_priority(hass, calls, above, below):
+ """Test an externally defined trigger variable is overridden."""
+ hass.states.async_set("test.entity_1", 0)
+ hass.states.async_set("test.entity_2", 0)
+ await hass.async_block_till_done()
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger_variables": {"trigger": "illegal"},
+ "trigger": {
+ "platform": "numeric_state",
+ "entity_id": ["test.entity_1", "test.entity_2"],
+ "above": above,
+ "below": below,
+ "for": '{{ 5 if trigger.entity_id == "test.entity_1"'
+ " else 10 }}",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "{{ trigger.entity_id }} - {{ trigger.for }}"
+ },
+ },
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ utcnow = dt_util.utcnow()
+ with patch("homeassistant.util.dt.utcnow") as mock_utcnow:
+ mock_utcnow.return_value = utcnow
+ hass.states.async_set("test.entity_1", 9)
+ await hass.async_block_till_done()
+ mock_utcnow.return_value += timedelta(seconds=1)
+ async_fire_time_changed(hass, mock_utcnow.return_value)
+ hass.states.async_set("test.entity_2", 9)
+ await hass.async_block_till_done()
+ mock_utcnow.return_value += timedelta(seconds=1)
+ async_fire_time_changed(hass, mock_utcnow.return_value)
+ hass.states.async_set("test.entity_2", 15)
+ await hass.async_block_till_done()
+ mock_utcnow.return_value += timedelta(seconds=1)
+ async_fire_time_changed(hass, mock_utcnow.return_value)
+ hass.states.async_set("test.entity_2", 9)
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+ mock_utcnow.return_value += timedelta(seconds=3)
+ async_fire_time_changed(hass, mock_utcnow.return_value)
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "test.entity_1 - 0:00:05"
+
+
+@pytest.mark.parametrize("multiplier", (1, 5))
+async def test_template_variable(hass, calls, multiplier):
+ """Test template variable."""
+ hass.states.async_set("test.entity", "entity", {"test_attribute": [11, 15, 11]})
+ await hass.async_block_till_done()
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger_variables": {"multiplier": multiplier},
+ "trigger": {
+ "platform": "numeric_state",
+ "entity_id": "test.entity",
+ "value_template": "{{ state.attributes.test_attribute[2] * multiplier}}",
+ "below": 10,
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+ # 3 is below 10
+ hass.states.async_set("test.entity", "entity", {"test_attribute": [11, 15, 3]})
+ await hass.async_block_till_done()
+ if multiplier * 3 < 10:
+ assert len(calls) == 1
+ else:
+ assert len(calls) == 0
diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py
index dd98dbc429cbc3..8671e40d2938e7 100644
--- a/tests/components/homeassistant/triggers/test_state.py
+++ b/tests/components/homeassistant/triggers/test_state.py
@@ -55,6 +55,7 @@ async def test_if_fires_on_entity_change(hass, calls):
"from_state.state",
"to_state.state",
"for",
+ "id",
)
)
},
@@ -68,7 +69,7 @@ async def test_if_fires_on_entity_change(hass, calls):
await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0].context.parent_id == context.id
- assert calls[0].data["some"] == "state - test.entity - hello - world - None"
+ assert calls[0].data["some"] == "state - test.entity - hello - world - None - 0"
await hass.services.async_call(
automation.DOMAIN,
@@ -506,7 +507,7 @@ async def test_if_fires_on_entity_change_with_for(hass, calls):
await hass.async_block_till_done()
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
async def test_if_fires_on_entity_change_with_for_without_to(hass, calls):
@@ -984,6 +985,33 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls):
assert len(calls) == 1
+async def test_if_fires_on_change_with_for_template_4(hass, calls):
+ """Test for firing on change with for template."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger_variables": {"seconds": 5},
+ "trigger": {
+ "platform": "state",
+ "entity_id": "test.entity",
+ "to": "world",
+ "for": {"seconds": "{{ seconds }}"},
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+
+ hass.states.async_set("test.entity", "world")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+
async def test_if_fires_on_change_from_with_for(hass, calls):
"""Test for firing on change with from/for."""
assert await async_setup_component(
@@ -1269,3 +1297,64 @@ async def test_attribute_if_fires_on_entity_change_with_both_filters_boolean(
hass.states.async_set("test.entity", "bla", {"happening": True})
await hass.async_block_till_done()
assert len(calls) == 1
+
+
+async def test_variables_priority(hass, calls):
+ """Test an externally defined trigger variable is overridden."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger_variables": {"trigger": "illegal"},
+ "trigger": {
+ "platform": "state",
+ "entity_id": ["test.entity_1", "test.entity_2"],
+ "to": "world",
+ "for": '{{ 5 if trigger.entity_id == "test.entity_1"'
+ " else 10 }}",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "{{ trigger.entity_id }} - {{ trigger.for }}"
+ },
+ },
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ utcnow = dt_util.utcnow()
+ with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow:
+ mock_utcnow.return_value = utcnow
+ hass.states.async_set("test.entity_1", "world")
+ await hass.async_block_till_done()
+ mock_utcnow.return_value += timedelta(seconds=1)
+ async_fire_time_changed(hass, mock_utcnow.return_value)
+ hass.states.async_set("test.entity_2", "world")
+ await hass.async_block_till_done()
+ mock_utcnow.return_value += timedelta(seconds=1)
+ async_fire_time_changed(hass, mock_utcnow.return_value)
+ hass.states.async_set("test.entity_2", "hello")
+ await hass.async_block_till_done()
+ mock_utcnow.return_value += timedelta(seconds=1)
+ async_fire_time_changed(hass, mock_utcnow.return_value)
+ hass.states.async_set("test.entity_2", "world")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+ mock_utcnow.return_value += timedelta(seconds=3)
+ async_fire_time_changed(hass, mock_utcnow.return_value)
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "test.entity_1 - 0:00:05"
+
+ mock_utcnow.return_value += timedelta(seconds=3)
+ async_fire_time_changed(hass, mock_utcnow.return_value)
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ mock_utcnow.return_value += timedelta(seconds=5)
+ async_fire_time_changed(hass, mock_utcnow.return_value)
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[1].data["some"] == "test.entity_2 - 0:00:10"
diff --git a/tests/components/homeassistant/triggers/test_time.py b/tests/components/homeassistant/triggers/test_time.py
index 5805aa07fe3cf6..499fcf8611ef03 100644
--- a/tests/components/homeassistant/triggers/test_time.py
+++ b/tests/components/homeassistant/triggers/test_time.py
@@ -51,7 +51,8 @@ async def test_if_fires_using_at(hass, calls):
"action": {
"service": "test.automation",
"data_template": {
- "some": "{{ trigger.platform }} - {{ trigger.now.hour }}"
+ "some": "{{ trigger.platform }} - {{ trigger.now.hour }}",
+ "id": "{{ trigger.id}}",
},
},
}
@@ -64,6 +65,7 @@ async def test_if_fires_using_at(hass, calls):
assert len(calls) == 1
assert calls[0].data["some"] == "time - 5"
+ assert calls[0].data["id"] == 0
@pytest.mark.parametrize(
diff --git a/tests/components/homeassistant/triggers/test_time_pattern.py b/tests/components/homeassistant/triggers/test_time_pattern.py
index 147bb388fed7a1..1018bf7ab1419f 100644
--- a/tests/components/homeassistant/triggers/test_time_pattern.py
+++ b/tests/components/homeassistant/triggers/test_time_pattern.py
@@ -46,7 +46,10 @@ async def test_if_fires_when_hour_matches(hass, calls):
"minutes": "*",
"seconds": "*",
},
- "action": {"service": "test.automation"},
+ "action": {
+ "service": "test.automation",
+ "data_template": {"id": "{{ trigger.id}}"},
+ },
}
},
)
@@ -65,6 +68,7 @@ async def test_if_fires_when_hour_matches(hass, calls):
async_fire_time_changed(hass, now.replace(year=now.year + 1, hour=0))
await hass.async_block_till_done()
assert len(calls) == 1
+ assert calls[0].data["id"] == 0
async def test_if_fires_when_minute_matches(hass, calls):
diff --git a/tests/components/homekit/common.py b/tests/components/homekit/common.py
index 20aa0e04c2b3b7..6b1d87e3f54fde 100644
--- a/tests/components/homekit/common.py
+++ b/tests/components/homekit/common.py
@@ -1,17 +1,9 @@
"""Collection of fixtures and functions for the HomeKit tests."""
-from unittest.mock import Mock, patch
+from unittest.mock import Mock
EMPTY_8_6_JPEG = b"empty_8_6"
-def patch_debounce():
- """Return patch for debounce method."""
- return patch(
- "homeassistant.components.homekit.accessories.debounce",
- lambda f: lambda *args, **kwargs: f(*args, **kwargs),
- )
-
-
def mock_turbo_jpeg(
first_width=None, second_width=None, first_height=None, second_height=None
):
diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py
index ac51c4e636875a..469a0a7deb7191 100644
--- a/tests/components/homekit/conftest.py
+++ b/tests/components/homekit/conftest.py
@@ -5,7 +5,8 @@
import pytest
from homeassistant.components.homekit.const import EVENT_HOMEKIT_CHANGED
-from homeassistant.core import callback as ha_callback
+
+from tests.common import async_capture_events
@pytest.fixture
@@ -13,7 +14,9 @@ def hk_driver(loop):
"""Return a custom AccessoryDriver instance for HomeKit accessory init."""
with patch("pyhap.accessory_driver.Zeroconf"), patch(
"pyhap.accessory_driver.AccessoryEncoder"
- ), patch("pyhap.accessory_driver.HAPServer"), patch(
+ ), patch("pyhap.accessory_driver.HAPServer.async_stop"), patch(
+ "pyhap.accessory_driver.HAPServer.async_start"
+ ), patch(
"pyhap.accessory_driver.AccessoryDriver.publish"
), patch(
"pyhap.accessory_driver.AccessoryDriver.persist"
@@ -24,8 +27,4 @@ def hk_driver(loop):
@pytest.fixture
def events(hass):
"""Yield caught homekit_changed events."""
- events = []
- hass.bus.async_listen(
- EVENT_HOMEKIT_CHANGED, ha_callback(lambda e: events.append(e))
- )
- yield events
+ return async_capture_events(hass, EVENT_HOMEKIT_CHANGED)
diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py
index 886123062c43ed..afaa9ea0892fe3 100644
--- a/tests/components/homekit/test_accessories.py
+++ b/tests/components/homekit/test_accessories.py
@@ -2,7 +2,6 @@
This includes tests for all mock object types.
"""
-from datetime import timedelta
from unittest.mock import Mock, patch
import pytest
@@ -11,7 +10,6 @@
HomeAccessory,
HomeBridge,
HomeDriver,
- debounce,
)
from homeassistant.components.homekit.const import (
ATTR_DISPLAY_NAME,
@@ -45,41 +43,8 @@
__version__,
)
from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS
-import homeassistant.util.dt as dt_util
-from tests.common import async_fire_time_changed, async_mock_service
-
-
-async def test_debounce(hass):
- """Test add_timeout decorator function."""
-
- def demo_func(*args):
- nonlocal arguments, counter
- counter += 1
- arguments = args
-
- arguments = None
- counter = 0
- mock = Mock(hass=hass, debounce={})
-
- debounce_demo = debounce(demo_func)
- assert debounce_demo.__name__ == "demo_func"
- now = dt_util.utcnow()
-
- with patch("homeassistant.util.dt.utcnow", return_value=now):
- await hass.async_add_executor_job(debounce_demo, mock, "value")
- async_fire_time_changed(hass, now + timedelta(seconds=3))
- await hass.async_block_till_done()
- assert counter == 1
- assert len(arguments) == 2
-
- with patch("homeassistant.util.dt.utcnow", return_value=now):
- await hass.async_add_executor_job(debounce_demo, mock, "value")
- await hass.async_add_executor_job(debounce_demo, mock, "value")
-
- async_fire_time_changed(hass, now + timedelta(seconds=3))
- await hass.async_block_till_done()
- assert counter == 2
+from tests.common import async_mock_service
async def test_accessory_cancels_track_state_change_on_stop(hass, hk_driver):
@@ -92,7 +57,7 @@ async def test_accessory_cancels_track_state_change_on_stop(hass, hk_driver):
with patch(
"homeassistant.components.homekit.accessories.HomeAccessory.async_update_state"
):
- await acc.run_handler()
+ await acc.run()
assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS][entity_id]) == 1
acc.async_stop()
assert entity_id not in hass.data[TRACK_STATE_CHANGE_CALLBACKS]
@@ -156,7 +121,7 @@ async def test_home_accessory(hass, hk_driver):
with patch(
"homeassistant.components.homekit.accessories.HomeAccessory.async_update_state"
) as mock_async_update_state:
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
state = hass.states.get(entity_id)
mock_async_update_state.assert_called_with(state)
@@ -191,7 +156,7 @@ async def test_battery_service(hass, hk_driver, caplog):
with patch(
"homeassistant.components.homekit.accessories.HomeAccessory.async_update_state"
) as mock_async_update_state:
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
state = hass.states.get(entity_id)
mock_async_update_state.assert_called_with(state)
@@ -247,7 +212,7 @@ async def test_battery_service(hass, hk_driver, caplog):
with patch(
"homeassistant.components.homekit.accessories.HomeAccessory.async_update_state"
) as mock_async_update_state:
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
state = hass.states.get(entity_id)
mock_async_update_state.assert_called_with(state)
@@ -288,7 +253,7 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog):
with patch(
"homeassistant.components.homekit.accessories.HomeAccessory.async_update_state"
) as mock_async_update_state:
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
state = hass.states.get(entity_id)
mock_async_update_state.assert_called_with(state)
@@ -333,7 +298,7 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog):
with patch(
"homeassistant.components.homekit.accessories.HomeAccessory.async_update_state"
) as mock_async_update_state:
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
state = hass.states.get(entity_id)
mock_async_update_state.assert_called_with(state)
@@ -375,7 +340,7 @@ async def test_linked_battery_charging_sensor(hass, hk_driver, caplog):
with patch(
"homeassistant.components.homekit.accessories.HomeAccessory.async_update_state"
) as mock_async_update_state:
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
state = hass.states.get(entity_id)
mock_async_update_state.assert_called_with(state)
@@ -387,7 +352,7 @@ async def test_linked_battery_charging_sensor(hass, hk_driver, caplog):
"homeassistant.components.homekit.accessories.HomeAccessory.async_update_state"
) as mock_async_update_state:
hass.states.async_set(linked_battery_charging_sensor, STATE_OFF, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
state = hass.states.get(entity_id)
mock_async_update_state.assert_called_with(state)
@@ -397,7 +362,7 @@ async def test_linked_battery_charging_sensor(hass, hk_driver, caplog):
"homeassistant.components.homekit.accessories.HomeAccessory.async_update_state"
) as mock_async_update_state:
hass.states.async_set(linked_battery_charging_sensor, STATE_ON, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
state = hass.states.get(entity_id)
mock_async_update_state.assert_called_with(state)
@@ -407,7 +372,7 @@ async def test_linked_battery_charging_sensor(hass, hk_driver, caplog):
"homeassistant.components.homekit.accessories.HomeAccessory.async_update_state"
) as mock_async_update_state:
hass.states.async_remove(linked_battery_charging_sensor)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc._char_charging.value == 1
@@ -440,7 +405,7 @@ async def test_linked_battery_sensor_and_linked_battery_charging_sensor(
with patch(
"homeassistant.components.homekit.accessories.HomeAccessory.async_update_state"
) as mock_async_update_state:
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
state = hass.states.get(entity_id)
mock_async_update_state.assert_called_with(state)
@@ -484,7 +449,7 @@ async def test_missing_linked_battery_charging_sensor(hass, hk_driver, caplog):
with patch(
"homeassistant.components.homekit.accessories.HomeAccessory.async_update_state"
):
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
# Make sure we don't throw if the entity_id
@@ -493,7 +458,7 @@ async def test_missing_linked_battery_charging_sensor(hass, hk_driver, caplog):
with patch(
"homeassistant.components.homekit.accessories.HomeAccessory.async_update_state"
):
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
@@ -517,7 +482,7 @@ async def test_missing_linked_battery_sensor(hass, hk_driver, caplog):
with patch(
"homeassistant.components.homekit.accessories.HomeAccessory.async_update_state"
) as mock_async_update_state:
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
state = hass.states.get(entity_id)
mock_async_update_state.assert_called_with(state)
@@ -531,7 +496,7 @@ async def test_missing_linked_battery_sensor(hass, hk_driver, caplog):
"homeassistant.components.homekit.accessories.HomeAccessory.async_update_state"
) as mock_async_update_state:
hass.states.async_remove(entity_id)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert not acc.linked_battery_sensor
@@ -552,7 +517,7 @@ async def test_battery_appears_after_startup(hass, hk_driver, caplog):
with patch(
"homeassistant.components.homekit.accessories.HomeAccessory.async_update_state"
) as mock_async_update_state:
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
state = hass.states.get(entity_id)
mock_async_update_state.assert_called_with(state)
@@ -586,7 +551,7 @@ async def test_call_service(hass, hk_driver, events):
test_service = "open_cover"
test_value = "value"
- await acc.async_call_service(
+ acc.async_call_service(
test_domain, test_service, {ATTR_ENTITY_ID: entity_id}, test_value
)
await hass.async_block_till_done()
@@ -638,13 +603,19 @@ def test_home_driver():
with patch("pyhap.accessory_driver.AccessoryDriver.__init__") as mock_driver:
driver = HomeDriver(
- "hass", "entry_id", "name", address=ip_address, port=port, persist_file=path
+ "hass",
+ "entry_id",
+ "name",
+ "title",
+ address=ip_address,
+ port=port,
+ persist_file=path,
)
mock_driver.assert_called_with(address=ip_address, port=port, persist_file=path)
- driver.state = Mock(pincode=pin)
+ driver.state = Mock(pincode=pin, paired=False)
xhm_uri_mock = Mock(return_value="X-HM://0")
- driver.accessory = Mock(xhm_uri=xhm_uri_mock)
+ driver.accessory = Mock(display_name="any", xhm_uri=xhm_uri_mock)
# pair
with patch("pyhap.accessory_driver.AccessoryDriver.pair") as mock_pair, patch(
@@ -662,4 +633,4 @@ def test_home_driver():
driver.unpair("client_uuid")
mock_unpair.assert_called_with("client_uuid")
- mock_show_msg.assert_called_with("hass", "entry_id", "name", pin, "X-HM://0")
+ mock_show_msg.assert_called_with("hass", "entry_id", "title (any)", pin, "X-HM://0")
diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py
index 1dd628af18d105..268046752650e0 100644
--- a/tests/components/homekit/test_config_flow.py
+++ b/tests/components/homekit/test_config_flow.py
@@ -4,8 +4,8 @@
import pytest
from homeassistant import config_entries, data_entry_flow, setup
-from homeassistant.components.homekit.const import DOMAIN
-from homeassistant.config_entries import SOURCE_IMPORT
+from homeassistant.components.homekit.const import DOMAIN, SHORT_BRIDGE_NAME
+from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_IMPORT
from homeassistant.const import CONF_NAME, CONF_PORT
from tests.common import MockConfigEntry
@@ -39,48 +39,41 @@ async def test_setup_in_bridge_mode(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
- assert result["errors"] == {}
+ assert result["errors"] is None
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {"mode": "bridge"},
+ {"include_domains": ["light"]},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result2["step_id"] == "bridge_mode"
+ assert result2["step_id"] == "pairing"
with patch(
- "homeassistant.components.homekit.config_flow.find_next_available_port",
+ "homeassistant.components.homekit.config_flow.async_find_next_available_port",
return_value=12345,
- ):
- result3 = await hass.config_entries.flow.async_configure(
- result2["flow_id"],
- {"include_domains": ["light"]},
- )
- assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result3["step_id"] == "pairing"
-
- with patch(
+ ), patch(
"homeassistant.components.homekit.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.homekit.async_setup_entry",
return_value=True,
) as mock_setup_entry:
- result4 = await hass.config_entries.flow.async_configure(
- result3["flow_id"],
+ result3 = await hass.config_entries.flow.async_configure(
+ result2["flow_id"],
{},
)
await hass.async_block_till_done()
- assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result4["title"][:11] == "HASS Bridge"
- bridge_name = (result4["title"].split(":"))[0]
- assert result4["data"] == {
+ assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ bridge_name = (result3["title"].split(":"))[0]
+ assert bridge_name == SHORT_BRIDGE_NAME
+ assert result3["data"] == {
"filter": {
"exclude_domains": [],
"exclude_entities": [],
"include_domains": ["light"],
"include_entities": [],
},
+ "exclude_accessory_mode": True,
"mode": "bridge",
"name": bridge_name,
"port": 12345,
@@ -89,70 +82,155 @@ async def test_setup_in_bridge_mode(hass):
assert len(mock_setup_entry.mock_calls) == 1
-async def test_setup_in_accessory_mode(hass):
- """Test we can setup a new instance in accessory."""
+async def test_setup_in_bridge_mode_name_taken(hass):
+ """Test we can setup a new instance in bridge mode when the name is taken."""
await setup.async_setup_component(hass, "persistent_notification", {})
- hass.states.async_set("camera.mine", "off")
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={CONF_NAME: SHORT_BRIDGE_NAME, CONF_PORT: 8000},
+ )
+ entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
- assert result["errors"] == {}
+ assert result["errors"] is None
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {"mode": "accessory"},
+ {"include_domains": ["light"]},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result2["step_id"] == "accessory_mode"
+ assert result2["step_id"] == "pairing"
with patch(
- "homeassistant.components.homekit.config_flow.find_next_available_port",
+ "homeassistant.components.homekit.config_flow.async_find_next_available_port",
return_value=12345,
- ):
+ ), patch(
+ "homeassistant.components.homekit.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.homekit.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
- {"entity_id": "camera.mine"},
+ {},
)
- assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result3["step_id"] == "pairing"
+ await hass.async_block_till_done()
+
+ assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result3["title"] != SHORT_BRIDGE_NAME
+ assert result3["title"].startswith(SHORT_BRIDGE_NAME)
+ bridge_name = (result3["title"].split(":"))[0]
+ assert result3["data"] == {
+ "filter": {
+ "exclude_domains": [],
+ "exclude_entities": [],
+ "include_domains": ["light"],
+ "include_entities": [],
+ },
+ "exclude_accessory_mode": True,
+ "mode": "bridge",
+ "name": bridge_name,
+ "port": 12345,
+ }
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 2
+
+
+async def test_setup_creates_entries_for_accessory_mode_devices(hass):
+ """Test we can setup a new instance and we create entries for accessory mode devices."""
+ hass.states.async_set("camera.one", "on")
+ hass.states.async_set("camera.existing", "on")
+ hass.states.async_set("media_player.two", "on", {"device_class": "tv"})
+
+ bridge_mode_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={CONF_NAME: "bridge", CONF_PORT: 8001},
+ options={
+ "mode": "bridge",
+ "filter": {
+ "include_entities": ["camera.existing"],
+ },
+ },
+ )
+ bridge_mode_entry.add_to_hass(hass)
+ accessory_mode_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={CONF_NAME: "accessory", CONF_PORT: 8000},
+ options={
+ "mode": "accessory",
+ "filter": {
+ "include_entities": ["camera.existing"],
+ },
+ },
+ )
+ accessory_mode_entry.add_to_hass(hass)
+
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] is None
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"include_domains": ["camera", "media_player", "light"]},
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == "pairing"
with patch(
+ "homeassistant.components.homekit.config_flow.async_find_next_available_port",
+ return_value=12345,
+ ), patch(
"homeassistant.components.homekit.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.homekit.async_setup_entry",
return_value=True,
) as mock_setup_entry:
- result4 = await hass.config_entries.flow.async_configure(
- result3["flow_id"],
+ result3 = await hass.config_entries.flow.async_configure(
+ result2["flow_id"],
{},
)
await hass.async_block_till_done()
- assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result4["title"][:14] == "HASS Accessory"
- bridge_name = (result4["title"].split(":"))[0]
- assert result4["data"] == {
+ assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result3["title"][:11] == "HASS Bridge"
+ bridge_name = (result3["title"].split(":"))[0]
+ assert result3["data"] == {
"filter": {
"exclude_domains": [],
"exclude_entities": [],
- "include_domains": [],
- "include_entities": ["camera.mine"],
+ "include_domains": ["media_player", "light"],
+ "include_entities": [],
},
- "mode": "accessory",
+ "exclude_accessory_mode": True,
+ "mode": "bridge",
"name": bridge_name,
- "entity_config": {"camera.mine": {"video_codec": "copy"}},
"port": 12345,
}
assert len(mock_setup.mock_calls) == 1
- assert len(mock_setup_entry.mock_calls) == 1
+ #
+ # Existing accessory mode entries should get setup but not duplicated
+ #
+ # 1 - existing accessory for camera.existing
+ # 2 - existing bridge for camera.one
+ # 3 - new bridge
+ # 4 - camera.one in accessory mode
+ # 5 - media_player.two in accessory mode
+ assert len(mock_setup_entry.mock_calls) == 5
async def test_import(hass):
"""Test we can import instance."""
await setup.async_setup_component(hass, "persistent_notification", {})
+ ignored_entry = MockConfigEntry(domain=DOMAIN, data={}, source=SOURCE_IGNORE)
+ ignored_entry.add_to_hass(hass)
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}
)
@@ -492,6 +570,18 @@ async def test_options_flow_include_mode_with_cameras(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
+ assert result["data_schema"]({}) == {
+ "domains": ["fan", "vacuum", "climate", "camera"],
+ "mode": "bridge",
+ }
+ schema = result["data_schema"].schema
+ assert _get_schema_default(schema, "domains") == [
+ "fan",
+ "vacuum",
+ "climate",
+ "camera",
+ ]
+ assert _get_schema_default(schema, "mode") == "bridge"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
@@ -500,6 +590,16 @@ async def test_options_flow_include_mode_with_cameras(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "include_exclude"
+ assert result["data_schema"]({}) == {
+ "entities": ["camera.native_h264", "camera.transcode_h264"],
+ "include_exclude_mode": "include",
+ }
+ schema = result["data_schema"].schema
+ assert _get_schema_default(schema, "entities") == [
+ "camera.native_h264",
+ "camera.transcode_h264",
+ ]
+ assert _get_schema_default(schema, "include_exclude_mode") == "include"
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
@@ -510,6 +610,9 @@ async def test_options_flow_include_mode_with_cameras(hass):
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == "cameras"
+ assert result2["data_schema"]({}) == {"camera_copy": ["camera.native_h264"]}
+ schema = result2["data_schema"].schema
+ assert _get_schema_default(schema, "camera_copy") == ["camera.native_h264"]
result3 = await hass.config_entries.options.async_configure(
result2["flow_id"],
@@ -519,14 +622,14 @@ async def test_options_flow_include_mode_with_cameras(hass):
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert config_entry.options == {
"auto_start": True,
- "mode": "bridge",
+ "entity_config": {"camera.native_h264": {}},
"filter": {
"exclude_domains": [],
"exclude_entities": ["climate.old", "camera.excluded"],
"include_domains": ["fan", "vacuum", "climate", "camera"],
"include_entities": [],
},
- "entity_config": {},
+ "mode": "bridge",
}
@@ -586,6 +689,17 @@ async def test_options_flow_include_mode_basic_accessory(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
+ assert result["data_schema"]({}) == {
+ "domains": [
+ "fan",
+ "humidifier",
+ "vacuum",
+ "media_player",
+ "climate",
+ "alarm_control_panel",
+ ],
+ "mode": "bridge",
+ }
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
@@ -594,6 +708,7 @@ async def test_options_flow_include_mode_basic_accessory(hass):
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == "include_exclude"
+ assert _get_schema_default(result2["data_schema"].schema, "entities") == []
result3 = await hass.config_entries.options.async_configure(
result2["flow_id"],
@@ -612,63 +727,55 @@ async def test_options_flow_include_mode_basic_accessory(hass):
}
-async def test_converting_bridge_to_accessory_mode(hass):
+async def test_converting_bridge_to_accessory_mode(hass, hk_driver):
"""Test we can convert a bridge to accessory mode."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
- assert result["errors"] == {}
+ assert result["errors"] is None
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {"mode": "bridge"},
+ {"include_domains": ["light"]},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result2["step_id"] == "bridge_mode"
+ assert result2["step_id"] == "pairing"
+ # We need to actually setup the config entry or the data
+ # will not get migrated to options
with patch(
- "homeassistant.components.homekit.config_flow.find_next_available_port",
+ "homeassistant.components.homekit.config_flow.async_find_next_available_port",
return_value=12345,
- ):
+ ), patch(
+ "homeassistant.components.homekit.HomeKit.async_start",
+ return_value=True,
+ ) as mock_async_start:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
- {"include_domains": ["light"]},
- )
- assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result3["step_id"] == "pairing"
-
- with patch(
- "homeassistant.components.homekit.async_setup", return_value=True
- ) as mock_setup, patch(
- "homeassistant.components.homekit.async_setup_entry",
- return_value=True,
- ) as mock_setup_entry:
- result4 = await hass.config_entries.flow.async_configure(
- result3["flow_id"],
{},
)
await hass.async_block_till_done()
- assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result4["title"][:11] == "HASS Bridge"
- bridge_name = (result4["title"].split(":"))[0]
- assert result4["data"] == {
+ assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result3["title"][:11] == "HASS Bridge"
+ bridge_name = (result3["title"].split(":"))[0]
+ assert result3["data"] == {
"filter": {
"exclude_domains": [],
"exclude_entities": [],
"include_domains": ["light"],
"include_entities": [],
},
+ "exclude_accessory_mode": True,
"mode": "bridge",
"name": bridge_name,
"port": 12345,
}
- assert len(mock_setup.mock_calls) == 1
- assert len(mock_setup_entry.mock_calls) == 1
+ assert len(mock_async_start.mock_calls) == 1
- config_entry = result4["result"]
+ config_entry = result3["result"]
hass.states.async_set("camera.tv", "off")
hass.states.async_set("camera.sonos", "off")
@@ -681,6 +788,9 @@ async def test_converting_bridge_to_accessory_mode(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
+ schema = result["data_schema"].schema
+ assert _get_schema_default(schema, "mode") == "bridge"
+ assert _get_schema_default(schema, "domains") == ["light"]
result = await hass.config_entries.options.async_configure(
result["flow_id"],
@@ -714,3 +824,11 @@ async def test_converting_bridge_to_accessory_mode(hass):
"include_entities": ["camera.tv"],
},
}
+
+
+def _get_schema_default(schema, key_name):
+ """Iterate schema to find a key."""
+ for schema_key in schema:
+ if schema_key == key_name:
+ return schema_key.default()
+ raise KeyError(f"{key_name} not found in schema")
diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py
index 70d594080110bb..1c68ae7d001bb4 100644
--- a/tests/components/homekit/test_get_accessories.py
+++ b/tests/components/homekit/test_get_accessories.py
@@ -26,6 +26,8 @@
ATTR_UNIT_OF_MEASUREMENT,
CONF_NAME,
CONF_TYPE,
+ DEVICE_CLASS_CO,
+ DEVICE_CLASS_CO2,
LIGHT_LUX,
PERCENTAGE,
TEMP_CELSIUS,
@@ -186,9 +188,19 @@ def test_type_media_player(type_name, entity_id, state, attrs, config):
("BinarySensor", "person.someone", "home", {}),
("AirQualitySensor", "sensor.air_quality_pm25", "40", {}),
("AirQualitySensor", "sensor.air_quality", "40", {ATTR_DEVICE_CLASS: "pm25"}),
- ("CarbonMonoxideSensor", "sensor.airmeter", "2", {ATTR_DEVICE_CLASS: "co"}),
+ (
+ "CarbonMonoxideSensor",
+ "sensor.co",
+ "2",
+ {ATTR_DEVICE_CLASS: DEVICE_CLASS_CO},
+ ),
("CarbonDioxideSensor", "sensor.airmeter_co2", "500", {}),
- ("CarbonDioxideSensor", "sensor.airmeter", "500", {ATTR_DEVICE_CLASS: "co2"}),
+ (
+ "CarbonDioxideSensor",
+ "sensor.co2",
+ "500",
+ {ATTR_DEVICE_CLASS: DEVICE_CLASS_CO2},
+ ),
(
"HumiditySensor",
"sensor.humidity",
diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py
index c6f897c32a20cd..fd7d74aeaba5c1 100644
--- a/tests/components/homekit/test_homekit.py
+++ b/tests/components/homekit/test_homekit.py
@@ -1,6 +1,8 @@
"""Tests for the HomeKit component."""
+from __future__ import annotations
+
+import asyncio
import os
-from typing import Dict
from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch
from pyhap.accessory import Accessory
@@ -8,7 +10,7 @@
import pytest
from homeassistant import config as hass_config
-from homeassistant.components import zeroconf
+from homeassistant.components import homekit as homekit_base, zeroconf
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_BATTERY_CHARGING,
DEVICE_CLASS_MOTION,
@@ -23,24 +25,18 @@
)
from homeassistant.components.homekit.accessories import HomeBridge
from homeassistant.components.homekit.const import (
- AID_STORAGE,
BRIDGE_NAME,
BRIDGE_SERIAL_NUMBER,
CONF_AUTO_START,
- CONF_ENTRY_INDEX,
DEFAULT_PORT,
DOMAIN,
HOMEKIT,
- HOMEKIT_FILE,
HOMEKIT_MODE_ACCESSORY,
HOMEKIT_MODE_BRIDGE,
SERVICE_HOMEKIT_RESET_ACCESSORY,
SERVICE_HOMEKIT_START,
)
-from homeassistant.components.homekit.util import (
- get_aid_storage_fullpath_for_entry_id,
- get_persist_fullpath_for_entry_id,
-)
+from homeassistant.components.homekit.util import get_persist_fullpath_for_entry_id
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_DEVICE_CLASS,
@@ -51,8 +47,7 @@
CONF_PORT,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY,
- EVENT_HOMEASSISTANT_START,
- EVENT_HOMEASSISTANT_STOP,
+ EVENT_HOMEASSISTANT_STARTED,
PERCENTAGE,
SERVICE_RELOAD,
STATE_ON,
@@ -60,14 +55,12 @@
from homeassistant.core import State
from homeassistant.helpers import device_registry
from homeassistant.helpers.entityfilter import generate_filter
-from homeassistant.helpers.storage import STORAGE_DIR
from homeassistant.setup import async_setup_component
from homeassistant.util import json as json_util
from .util import PATH_HOMEKIT, async_init_entry, async_init_integration
from tests.common import MockConfigEntry, mock_device_registry, mock_registry
-from tests.components.homekit.common import patch_debounce
IP_ADDRESS = "127.0.0.1"
@@ -89,16 +82,44 @@ def entity_reg_fixture(hass):
return mock_registry(hass)
-@pytest.fixture(name="debounce_patcher", scope="module")
-def debounce_patcher_fixture():
- """Patch debounce method."""
- patcher = patch_debounce()
- yield patcher.start()
- patcher.stop()
+def _mock_homekit(hass, entry, homekit_mode, entity_filter=None):
+ return HomeKit(
+ hass=hass,
+ name=BRIDGE_NAME,
+ port=DEFAULT_PORT,
+ ip_address=None,
+ entity_filter=entity_filter or generate_filter([], [], [], []),
+ exclude_accessory_mode=False,
+ entity_config={},
+ homekit_mode=homekit_mode,
+ advertise_ip=None,
+ entry_id=entry.entry_id,
+ entry_title=entry.title,
+ )
+
+
+def _mock_homekit_bridge(hass, entry):
+ homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
+ homekit.driver = MagicMock()
+ return homekit
+
+
+def _mock_accessories(accessory_count):
+ accessories = {}
+ for idx in range(accessory_count + 1):
+ accessories[idx + 1000] = MagicMock(async_stop=AsyncMock())
+ return accessories
+
+
+def _mock_pyhap_bridge():
+ return MagicMock(
+ aid=1, accessories=_mock_accessories(10), display_name="HomeKit Bridge"
+ )
async def test_setup_min(hass, mock_zeroconf):
"""Test async_setup with min config options."""
+ await async_setup_component(hass, "persistent_notification", {})
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT},
@@ -118,23 +139,23 @@ async def test_setup_min(hass, mock_zeroconf):
DEFAULT_PORT,
None,
ANY,
+ ANY,
{},
HOMEKIT_MODE_BRIDGE,
None,
entry.entry_id,
+ entry.title,
)
- assert mock_homekit().setup.called is True
# Test auto start enabled
- mock_homekit.reset_mock()
- hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
-
- mock_homekit().async_start.assert_called()
+ assert mock_homekit().async_start.called is True
async def test_setup_auto_start_disabled(hass, mock_zeroconf):
"""Test async_setup with auto start disabled and test service calls."""
+ await async_setup_component(hass, "persistent_notification", {})
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_NAME: "Test Name", CONF_PORT: 11111, CONF_IP_ADDRESS: "172.0.0.0"},
@@ -154,17 +175,18 @@ async def test_setup_auto_start_disabled(hass, mock_zeroconf):
11111,
"172.0.0.0",
ANY,
+ ANY,
{},
HOMEKIT_MODE_BRIDGE,
None,
entry.entry_id,
+ entry.title,
)
- assert mock_homekit().setup.called is True
# Test auto_start disabled
homekit.reset_mock()
homekit.async_start.reset_mock()
- hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert homekit.async_start.called is False
@@ -199,18 +221,20 @@ async def test_homekit_setup(hass, hk_driver, mock_zeroconf):
BRIDGE_NAME,
DEFAULT_PORT,
None,
+ True,
{},
{},
HOMEKIT_MODE_BRIDGE,
advertise_ip=None,
entry_id=entry.entry_id,
+ entry_title=entry.title,
)
hass.states.async_set("light.demo", "on")
hass.states.async_set("light.demo2", "on")
zeroconf_mock = MagicMock()
with patch(
- f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver
+ f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver
) as mock_driver, patch("homeassistant.util.get_local_ip") as mock_ip:
mock_ip.return_value = IP_ADDRESS
await hass.async_add_executor_job(homekit.setup, zeroconf_mock)
@@ -220,6 +244,7 @@ async def test_homekit_setup(hass, hk_driver, mock_zeroconf):
hass,
entry.entry_id,
BRIDGE_NAME,
+ entry.title,
loop=hass.loop,
address=IP_ADDRESS,
port=DEFAULT_PORT,
@@ -229,9 +254,6 @@ async def test_homekit_setup(hass, hk_driver, mock_zeroconf):
)
assert homekit.driver.safe_mode is False
- # Test if stop listener is setup
- assert hass.bus.async_listeners().get(EVENT_HOMEASSISTANT_STOP) == 1
-
async def test_homekit_setup_ip_address(hass, hk_driver, mock_zeroconf):
"""Test setup with given IP address."""
@@ -245,23 +267,24 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_zeroconf):
BRIDGE_NAME,
DEFAULT_PORT,
"172.0.0.0",
+ True,
{},
{},
HOMEKIT_MODE_BRIDGE,
None,
entry_id=entry.entry_id,
+ entry_title=entry.title,
)
mock_zeroconf = MagicMock()
path = get_persist_fullpath_for_entry_id(hass, entry.entry_id)
- with patch(
- f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver
- ) as mock_driver:
+ with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver:
await hass.async_add_executor_job(homekit.setup, mock_zeroconf)
mock_driver.assert_called_with(
hass,
entry.entry_id,
BRIDGE_NAME,
+ entry.title,
loop=hass.loop,
address="172.0.0.0",
port=DEFAULT_PORT,
@@ -283,23 +306,24 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf):
BRIDGE_NAME,
DEFAULT_PORT,
"0.0.0.0",
+ True,
{},
{},
HOMEKIT_MODE_BRIDGE,
"192.168.1.100",
entry_id=entry.entry_id,
+ entry_title=entry.title,
)
zeroconf_instance = MagicMock()
path = get_persist_fullpath_for_entry_id(hass, entry.entry_id)
- with patch(
- f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver
- ) as mock_driver:
+ with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver:
await hass.async_add_executor_job(homekit.setup, zeroconf_instance)
mock_driver.assert_called_with(
hass,
entry.entry_id,
BRIDGE_NAME,
+ entry.title,
loop=hass.loop,
address="0.0.0.0",
port=DEFAULT_PORT,
@@ -311,40 +335,37 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf):
async def test_homekit_add_accessory(hass, mock_zeroconf):
"""Add accessory if config exists and get_acc returns an accessory."""
- entry = await async_init_integration(hass)
-
- homekit = HomeKit(
- hass,
- None,
- None,
- None,
- lambda entity_id: True,
- {},
- HOMEKIT_MODE_BRIDGE,
- advertise_ip=None,
- entry_id=entry.entry_id,
+ await async_setup_component(hass, "persistent_notification", {})
+ entry = MockConfigEntry(
+ domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}
)
- homekit.driver = "driver"
- homekit.bridge = mock_bridge = Mock()
- homekit.bridge.accessories = range(10)
+ entry.add_to_hass(hass)
+ homekit = _mock_homekit_bridge(hass, entry)
mock_acc = Mock(category="any")
- await async_init_integration(hass)
+ with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit):
+ assert await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ homekit.bridge = _mock_pyhap_bridge()
with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc:
mock_get_acc.side_effect = [None, mock_acc, None]
- homekit.add_bridge_accessory(State("light.demo", "on"))
- mock_get_acc.assert_called_with(hass, "driver", ANY, 1403373688, {})
- assert not mock_bridge.add_accessory.called
+ state = State("light.demo", "on")
+ homekit.add_bridge_accessory(state)
+ mock_get_acc.assert_called_with(hass, ANY, ANY, 1403373688, {})
+ assert not homekit.bridge.add_accessory.called
- homekit.add_bridge_accessory(State("demo.test", "on"))
- mock_get_acc.assert_called_with(hass, "driver", ANY, 600325356, {})
- assert mock_bridge.add_accessory.called
+ state = State("demo.test", "on")
+ homekit.add_bridge_accessory(state)
+ mock_get_acc.assert_called_with(hass, ANY, ANY, 600325356, {})
+ assert homekit.bridge.add_accessory.called
- homekit.add_bridge_accessory(State("demo.test_2", "on"))
- mock_get_acc.assert_called_with(hass, "driver", ANY, 1467253281, {})
- mock_bridge.add_accessory.assert_called_with(mock_acc)
+ state = State("demo.test_2", "on")
+ homekit.add_bridge_accessory(state)
+ mock_get_acc.assert_called_with(hass, ANY, ANY, 1467253281, {})
+ assert homekit.bridge.add_accessory.called
@pytest.mark.parametrize("acc_category", [CATEGORY_TELEVISION, CATEGORY_CAMERA])
@@ -352,36 +373,27 @@ async def test_homekit_warn_add_accessory_bridge(
hass, acc_category, mock_zeroconf, caplog
):
"""Test we warn when adding cameras or tvs to a bridge."""
- entry = await async_init_integration(hass)
-
- homekit = HomeKit(
- hass,
- None,
- None,
- None,
- lambda entity_id: True,
- {},
- HOMEKIT_MODE_BRIDGE,
- advertise_ip=None,
- entry_id=entry.entry_id,
+ await async_setup_component(hass, "persistent_notification", {})
+ entry = MockConfigEntry(
+ domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}
)
- homekit.driver = "driver"
- homekit.bridge = mock_bridge = Mock()
- homekit.bridge.accessories = range(10)
+ entry.add_to_hass(hass)
- mock_camera_acc = Mock(category=acc_category)
+ homekit = _mock_homekit_bridge(hass, entry)
+
+ with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit):
+ assert await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
- await async_init_integration(hass)
+ mock_camera_acc = Mock(category=acc_category)
+ homekit.bridge = _mock_pyhap_bridge()
with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc:
mock_get_acc.side_effect = [None, mock_camera_acc, None]
- homekit.add_bridge_accessory(State("light.demo", "on"))
- mock_get_acc.assert_called_with(hass, "driver", ANY, 1403373688, {})
- assert not mock_bridge.add_accessory.called
-
- homekit.add_bridge_accessory(State("camera.test", "on"))
- mock_get_acc.assert_called_with(hass, "driver", ANY, 1508819236, {})
- assert mock_bridge.add_accessory.called
+ state = State("camera.test", "on")
+ homekit.add_bridge_accessory(state)
+ mock_get_acc.assert_called_with(hass, ANY, ANY, 1508819236, {})
+ assert not homekit.bridge.add_accessory.called
assert "accessory mode" in caplog.text
@@ -390,24 +402,15 @@ async def test_homekit_remove_accessory(hass, mock_zeroconf):
"""Remove accessory from bridge."""
entry = await async_init_integration(hass)
- homekit = HomeKit(
- hass,
- None,
- None,
- None,
- lambda entity_id: True,
- {},
- HOMEKIT_MODE_BRIDGE,
- advertise_ip=None,
- entry_id=entry.entry_id,
- )
+ homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
+
homekit.driver = "driver"
- homekit.bridge = mock_bridge = Mock()
- mock_bridge.accessories = {"light.demo": "acc"}
+ homekit.bridge = _mock_pyhap_bridge()
+ homekit.bridge.accessories = {"light.demo": "acc"}
acc = homekit.remove_bridge_accessory("light.demo")
assert acc == "acc"
- assert len(mock_bridge.accessories) == 0
+ assert len(homekit.bridge.accessories) == 0
async def test_homekit_entity_filter(hass, mock_zeroconf):
@@ -415,33 +418,18 @@ async def test_homekit_entity_filter(hass, mock_zeroconf):
entry = await async_init_integration(hass)
entity_filter = generate_filter(["cover"], ["demo.test"], [], [])
- homekit = HomeKit(
- hass,
- None,
- None,
- None,
- entity_filter,
- {},
- HOMEKIT_MODE_BRIDGE,
- advertise_ip=None,
- entry_id=entry.entry_id,
- )
+ homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE, entity_filter)
+
homekit.bridge = Mock()
homekit.bridge.accessories = {}
+ hass.states.async_set("cover.test", "open")
+ hass.states.async_set("demo.test", "on")
+ hass.states.async_set("light.demo", "on")
- with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc:
- mock_get_acc.return_value = None
-
- homekit.add_bridge_accessory(State("cover.test", "open"))
- assert mock_get_acc.called is True
- mock_get_acc.reset_mock()
-
- homekit.add_bridge_accessory(State("demo.test", "on"))
- assert mock_get_acc.called is True
- mock_get_acc.reset_mock()
-
- homekit.add_bridge_accessory(State("light.demo", "light"))
- assert mock_get_acc.called is False
+ filtered_states = await homekit.async_configure_accessories()
+ assert hass.states.get("cover.test") in filtered_states
+ assert hass.states.get("demo.test") in filtered_states
+ assert hass.states.get("light.demo") not in filtered_states
async def test_homekit_entity_glob_filter(hass, mock_zeroconf):
@@ -451,62 +439,34 @@ async def test_homekit_entity_glob_filter(hass, mock_zeroconf):
entity_filter = generate_filter(
["cover"], ["demo.test"], [], [], ["*.included_*"], ["*.excluded_*"]
)
- homekit = HomeKit(
- hass,
- None,
- None,
- None,
- entity_filter,
- {},
- HOMEKIT_MODE_BRIDGE,
- advertise_ip=None,
- entry_id=entry.entry_id,
- )
+ homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE, entity_filter)
+
homekit.bridge = Mock()
homekit.bridge.accessories = {}
- with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc:
- mock_get_acc.return_value = None
-
- homekit.add_bridge_accessory(State("cover.test", "open"))
- assert mock_get_acc.called is True
- mock_get_acc.reset_mock()
-
- homekit.add_bridge_accessory(State("demo.test", "on"))
- assert mock_get_acc.called is True
- mock_get_acc.reset_mock()
+ hass.states.async_set("cover.test", "open")
+ hass.states.async_set("demo.test", "on")
+ hass.states.async_set("cover.excluded_test", "open")
+ hass.states.async_set("light.included_test", "on")
- homekit.add_bridge_accessory(State("cover.excluded_test", "open"))
- assert mock_get_acc.called is False
- mock_get_acc.reset_mock()
+ filtered_states = await homekit.async_configure_accessories()
+ assert hass.states.get("cover.test") in filtered_states
+ assert hass.states.get("demo.test") in filtered_states
+ assert hass.states.get("cover.excluded_test") not in filtered_states
+ assert hass.states.get("light.included_test") in filtered_states
- homekit.add_bridge_accessory(State("light.included_test", "light"))
- assert mock_get_acc.called is True
- mock_get_acc.reset_mock()
-
-async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher):
+async def test_homekit_start(hass, hk_driver, mock_zeroconf, device_reg):
"""Test HomeKit start method."""
entry = await async_init_integration(hass)
- pin = b"123-45-678"
- homekit = HomeKit(
- hass,
- None,
- None,
- None,
- {},
- {},
- HOMEKIT_MODE_BRIDGE,
- advertise_ip=None,
- entry_id=entry.entry_id,
- )
+ homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
+
homekit.bridge = Mock()
homekit.bridge.accessories = []
homekit.driver = hk_driver
- # pylint: disable=protected-access
- homekit._filter = Mock(return_value=True)
- homekit.driver.accessory = Accessory(hk_driver, "any")
+ acc = Accessory(hk_driver, "any")
+ homekit.driver.accessory = acc
connection = (device_registry.CONNECTION_NETWORK_MAC, "AA:BB:CC:DD:EE:FF")
bridge_with_wrong_mac = device_reg.async_get_or_create(
@@ -524,16 +484,15 @@ async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher):
with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch(
f"{PATH_HOMEKIT}.show_setup_message"
) as mock_setup_msg, patch(
- "pyhap.accessory_driver.AccessoryDriver.add_accessory"
- ) as hk_driver_add_acc, patch(
- "pyhap.accessory_driver.AccessoryDriver.start_service"
+ "pyhap.accessory_driver.AccessoryDriver.async_start"
) as hk_driver_start:
await homekit.async_start()
await hass.async_block_till_done()
mock_add_acc.assert_any_call(state)
- mock_setup_msg.assert_called_with(hass, entry.entry_id, None, pin, ANY)
- hk_driver_add_acc.assert_called_with(homekit.bridge)
+ mock_setup_msg.assert_called_with(
+ hass, entry.entry_id, "Mock Title (Home Assistant Bridge)", ANY, ANY
+ )
assert hk_driver_start.called
assert homekit.status == STATUS_RUNNING
@@ -557,9 +516,7 @@ async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher):
with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch(
f"{PATH_HOMEKIT}.show_setup_message"
) as mock_setup_msg, patch(
- "pyhap.accessory_driver.AccessoryDriver.add_accessory"
- ) as hk_driver_add_acc, patch(
- "pyhap.accessory_driver.AccessoryDriver.start_service"
+ "pyhap.accessory_driver.AccessoryDriver.async_start"
) as hk_driver_start:
await homekit.async_start()
@@ -571,30 +528,18 @@ async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher):
assert (device_registry.CONNECTION_NETWORK_MAC, formatted_mac) in device.connections
assert len(device_reg.devices) == 1
+ assert homekit.driver.state.config_version == 2
-async def test_homekit_start_with_a_broken_accessory(
- hass, hk_driver, debounce_patcher, mock_zeroconf
-):
+async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, mock_zeroconf):
"""Test HomeKit start method."""
- pin = b"123-45-678"
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}
)
entity_filter = generate_filter(["cover", "light"], ["demo.test"], [], [])
await async_init_entry(hass, entry)
- homekit = HomeKit(
- hass,
- None,
- None,
- None,
- entity_filter,
- {},
- HOMEKIT_MODE_BRIDGE,
- advertise_ip=None,
- entry_id=entry.entry_id,
- )
+ homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE, entity_filter)
homekit.bridge = Mock()
homekit.bridge.accessories = []
@@ -607,15 +552,14 @@ async def test_homekit_start_with_a_broken_accessory(
with patch(f"{PATH_HOMEKIT}.get_accessory", side_effect=Exception), patch(
f"{PATH_HOMEKIT}.show_setup_message"
) as mock_setup_msg, patch(
- "pyhap.accessory_driver.AccessoryDriver.add_accessory",
- ) as hk_driver_add_acc, patch(
- "pyhap.accessory_driver.AccessoryDriver.start_service"
+ "pyhap.accessory_driver.AccessoryDriver.async_start"
) as hk_driver_start:
await homekit.async_start()
await hass.async_block_till_done()
- mock_setup_msg.assert_called_with(hass, entry.entry_id, None, pin, ANY)
- hk_driver_add_acc.assert_called_with(homekit.bridge)
+ mock_setup_msg.assert_called_with(
+ hass, entry.entry_id, "Mock Title (Home Assistant Bridge)", ANY, ANY
+ )
assert hk_driver_start.called
assert homekit.status == STATUS_RUNNING
@@ -629,18 +573,8 @@ async def test_homekit_start_with_a_broken_accessory(
async def test_homekit_stop(hass):
"""Test HomeKit stop method."""
entry = await async_init_integration(hass)
+ homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
- homekit = HomeKit(
- hass,
- None,
- None,
- None,
- {},
- {},
- HOMEKIT_MODE_BRIDGE,
- advertise_ip=None,
- entry_id=entry.entry_id,
- )
homekit.driver = Mock()
homekit.driver.async_stop = AsyncMock()
homekit.bridge = Mock()
@@ -666,36 +600,23 @@ async def test_homekit_stop(hass):
async def test_homekit_reset_accessories(hass, mock_zeroconf):
"""Test adding too many accessories to HomeKit."""
+ await async_setup_component(hass, "persistent_notification", {})
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}
)
entity_id = "light.demo"
- homekit = HomeKit(
- hass,
- None,
- None,
- None,
- {},
- {entity_id: {}},
- HOMEKIT_MODE_BRIDGE,
- advertise_ip=None,
- entry_id=entry.entry_id,
- )
- homekit.bridge = Mock()
- homekit.bridge.accessories = {}
+ homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch(
- f"{PATH_HOMEKIT}.HomeKit.setup"
- ), patch("pyhap.accessory.Bridge.add_accessory") as mock_add_accessory, patch(
+ "pyhap.accessory.Bridge.add_accessory"
+ ) as mock_add_accessory, patch(
"pyhap.accessory_driver.AccessoryDriver.config_changed"
) as hk_driver_config_changed, patch(
- "pyhap.accessory_driver.AccessoryDriver.start_service"
+ "pyhap.accessory_driver.AccessoryDriver.async_start"
):
await async_init_entry(hass, entry)
- aid = hass.data[DOMAIN][entry.entry_id][
- AID_STORAGE
- ].get_or_allocate_aid_for_entity_id(entity_id)
+ aid = homekit.aid_storage.get_or_allocate_aid_for_entity_id(entity_id)
homekit.bridge.accessories = {aid: "acc"}
homekit.status = STATUS_RUNNING
@@ -718,17 +639,7 @@ async def test_homekit_too_many_accessories(hass, hk_driver, caplog, mock_zeroco
entity_filter = generate_filter(["cover", "light"], ["demo.test"], [], [])
- homekit = HomeKit(
- hass,
- None,
- None,
- None,
- entity_filter,
- {},
- HOMEKIT_MODE_BRIDGE,
- advertise_ip=None,
- entry_id=entry.entry_id,
- )
+ homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE, entity_filter)
def _mock_bridge(*_):
mock_bridge = HomeBridge(hass, hk_driver, "mock_bridge")
@@ -743,37 +654,24 @@ def _mock_bridge(*_):
hass.states.async_set("light.demo2", "on")
hass.states.async_set("light.demo3", "on")
- with patch("pyhap.accessory_driver.AccessoryDriver.start_service"), patch(
- "pyhap.accessory_driver.AccessoryDriver.add_accessory"
- ), patch(f"{PATH_HOMEKIT}.show_setup_message"), patch(
- f"{PATH_HOMEKIT}.accessories.HomeBridge", _mock_bridge
- ):
+ with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch(
+ f"{PATH_HOMEKIT}.show_setup_message"
+ ), patch(f"{PATH_HOMEKIT}.HomeBridge", _mock_bridge):
await homekit.async_start()
await hass.async_block_till_done()
assert "would exceed" in caplog.text
async def test_homekit_finds_linked_batteries(
- hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf
+ hass, hk_driver, device_reg, entity_reg, mock_zeroconf
):
"""Test HomeKit start method."""
entry = await async_init_integration(hass)
- homekit = HomeKit(
- hass,
- None,
- None,
- None,
- {},
- {"light.demo": {}},
- HOMEKIT_MODE_BRIDGE,
- advertise_ip=None,
- entry_id=entry.entry_id,
- )
+ homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
+
homekit.driver = hk_driver
- # pylint: disable=protected-access
- homekit._filter = Mock(return_value=True)
- homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge")
+ homekit.bridge = MagicMock()
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
@@ -813,20 +711,15 @@ async def test_homekit_finds_linked_batteries(
)
hass.states.async_set(light.entity_id, STATE_ON)
- def _mock_get_accessory(*args, **kwargs):
- return [None, "acc", None]
-
- with patch.object(homekit.bridge, "add_accessory"), patch(
- f"{PATH_HOMEKIT}.show_setup_message"
- ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch(
- "pyhap.accessory_driver.AccessoryDriver.start_service"
- ):
+ with patch(f"{PATH_HOMEKIT}.show_setup_message"), patch(
+ f"{PATH_HOMEKIT}.get_accessory"
+ ) as mock_get_acc, patch("pyhap.accessory_driver.AccessoryDriver.async_start"):
await homekit.async_start()
await hass.async_block_till_done()
mock_get_acc.assert_called_with(
hass,
- hk_driver,
+ ANY,
ANY,
ANY,
{
@@ -840,25 +733,13 @@ def _mock_get_accessory(*args, **kwargs):
async def test_homekit_async_get_integration_fails(
- hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf
+ hass, hk_driver, device_reg, entity_reg, mock_zeroconf
):
"""Test that we continue if async_get_integration fails."""
entry = await async_init_integration(hass)
+ homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
- homekit = HomeKit(
- hass,
- None,
- None,
- None,
- {},
- {"light.demo": {}},
- HOMEKIT_MODE_BRIDGE,
- advertise_ip=None,
- entry_id=entry.entry_id,
- )
homekit.driver = hk_driver
- # pylint: disable=protected-access
- homekit._filter = Mock(return_value=True)
homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge")
config_entry = MockConfigEntry(domain="test", data={})
@@ -898,20 +779,17 @@ async def test_homekit_async_get_integration_fails(
)
hass.states.async_set(light.entity_id, STATE_ON)
- def _mock_get_accessory(*args, **kwargs):
- return [None, "acc", None]
-
with patch.object(homekit.bridge, "add_accessory"), patch(
f"{PATH_HOMEKIT}.show_setup_message"
), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch(
- "pyhap.accessory_driver.AccessoryDriver.start_service"
+ "pyhap.accessory_driver.AccessoryDriver.async_start"
):
await homekit.async_start()
await hass.async_block_till_done()
mock_get_acc.assert_called_with(
hass,
- hk_driver,
+ ANY,
ANY,
ANY,
{
@@ -924,71 +802,9 @@ def _mock_get_accessory(*args, **kwargs):
)
-async def test_setup_imported(hass, mock_zeroconf):
- """Test async_setup with imported config options."""
- legacy_persist_file_path = hass.config.path(HOMEKIT_FILE)
- legacy_aid_storage_path = hass.config.path(STORAGE_DIR, "homekit.aids")
- legacy_homekit_state_contents = {"homekit.state": 1}
- legacy_homekit_aids_contents = {"homekit.aids": 1}
- await hass.async_add_executor_job(
- _write_data, legacy_persist_file_path, legacy_homekit_state_contents
- )
- await hass.async_add_executor_job(
- _write_data, legacy_aid_storage_path, legacy_homekit_aids_contents
- )
-
- entry = MockConfigEntry(
- domain=DOMAIN,
- source=SOURCE_IMPORT,
- data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT, CONF_ENTRY_INDEX: 0},
- options={},
- )
- entry.add_to_hass(hass)
-
- with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit:
- mock_homekit.return_value = homekit = Mock()
- type(homekit).async_start = AsyncMock()
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
- mock_homekit.assert_any_call(
- hass,
- BRIDGE_NAME,
- DEFAULT_PORT,
- None,
- ANY,
- {},
- HOMEKIT_MODE_BRIDGE,
- None,
- entry.entry_id,
- )
- assert mock_homekit().setup.called is True
-
- # Test auto start enabled
- mock_homekit.reset_mock()
- hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
- await hass.async_block_till_done()
-
- mock_homekit().async_start.assert_called()
-
- migrated_persist_file_path = get_persist_fullpath_for_entry_id(hass, entry.entry_id)
- assert (
- await hass.async_add_executor_job(
- json_util.load_json, migrated_persist_file_path
- )
- == legacy_homekit_state_contents
- )
- os.unlink(migrated_persist_file_path)
- migrated_aid_file_path = get_aid_storage_fullpath_for_entry_id(hass, entry.entry_id)
- assert (
- await hass.async_add_executor_job(json_util.load_json, migrated_aid_file_path)
- == legacy_homekit_aids_contents
- )
- os.unlink(migrated_aid_file_path)
-
-
async def test_yaml_updates_update_config_entry_for_name(hass, mock_zeroconf):
"""Test async_setup with imported config."""
+ await async_setup_component(hass, "persistent_notification", {})
entry = MockConfigEntry(
domain=DOMAIN,
source=SOURCE_IMPORT,
@@ -1011,38 +827,22 @@ async def test_yaml_updates_update_config_entry_for_name(hass, mock_zeroconf):
12345,
None,
ANY,
+ ANY,
{},
HOMEKIT_MODE_BRIDGE,
None,
entry.entry_id,
+ entry.title,
)
- assert mock_homekit().setup.called is True
# Test auto start enabled
mock_homekit.reset_mock()
- hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
mock_homekit().async_start.assert_called()
-async def test_raise_config_entry_not_ready(hass, mock_zeroconf):
- """Test async_setup when the port is not available."""
- entry = MockConfigEntry(
- domain=DOMAIN,
- data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT},
- options={},
- )
- entry.add_to_hass(hass)
-
- with patch(
- "homeassistant.components.homekit.port_is_available",
- return_value=False,
- ):
- assert not await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
-
async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf):
"""Test HomeKit uses system zeroconf."""
entry = MockConfigEntry(
@@ -1053,7 +853,7 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf):
assert await async_setup_component(hass, "zeroconf", {"zeroconf": {}})
system_zc = await zeroconf.async_get_instance(hass)
- with patch("pyhap.accessory_driver.AccessoryDriver.start_service"), patch(
+ with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch(
f"{PATH_HOMEKIT}.HomeKit.async_stop"
):
entry.add_to_hass(hass)
@@ -1064,7 +864,7 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf):
await hass.async_block_till_done()
-def _write_data(path: str, data: Dict) -> None:
+def _write_data(path: str, data: dict) -> None:
"""Write the data."""
if not os.path.isdir(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
@@ -1072,26 +872,15 @@ def _write_data(path: str, data: Dict) -> None:
async def test_homekit_ignored_missing_devices(
- hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf
+ hass, hk_driver, device_reg, entity_reg, mock_zeroconf
):
"""Test HomeKit handles a device in the entity registry but missing from the device registry."""
+ await async_setup_component(hass, "persistent_notification", {})
entry = await async_init_integration(hass)
+ homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
- homekit = HomeKit(
- hass,
- None,
- None,
- None,
- {},
- {"light.demo": {}},
- HOMEKIT_MODE_BRIDGE,
- advertise_ip=None,
- entry_id=entry.entry_id,
- )
homekit.driver = hk_driver
- # pylint: disable=protected-access
- homekit._filter = Mock(return_value=True)
- homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge")
+ homekit.bridge = _mock_pyhap_bridge()
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
@@ -1120,28 +909,28 @@ async def test_homekit_ignored_missing_devices(
light = entity_reg.async_get_or_create(
"light", "powerwall", "demo", device_id=device_entry.id
)
-
+ before_removal = entity_reg.entities.copy()
# Delete the device to make sure we fallback
# to using the platform
device_reg.async_remove_device(device_entry.id)
+ # Wait for the entities to be removed
+ await asyncio.sleep(0)
+ await asyncio.sleep(0)
+ # Restore the registry
+ entity_reg.entities = before_removal
hass.states.async_set(light.entity_id, STATE_ON)
hass.states.async_set("light.two", STATE_ON)
- def _mock_get_accessory(*args, **kwargs):
- return [None, "acc", None]
-
- with patch.object(homekit.bridge, "add_accessory"), patch(
- f"{PATH_HOMEKIT}.show_setup_message"
- ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch(
- "pyhap.accessory_driver.AccessoryDriver.start_service"
- ):
+ with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch(
+ f"{PATH_HOMEKIT}.HomeBridge", return_value=homekit.bridge
+ ), patch("pyhap.accessory_driver.AccessoryDriver.async_start"):
await homekit.async_start()
- await hass.async_block_till_done()
+ await hass.async_block_till_done()
mock_get_acc.assert_any_call(
hass,
- hk_driver,
+ ANY,
ANY,
ANY,
{
@@ -1153,25 +942,14 @@ def _mock_get_accessory(*args, **kwargs):
async def test_homekit_finds_linked_motion_sensors(
- hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf
+ hass, hk_driver, device_reg, entity_reg, mock_zeroconf
):
"""Test HomeKit start method."""
entry = await async_init_integration(hass)
- homekit = HomeKit(
- hass,
- None,
- None,
- None,
- {},
- {"camera.camera_demo": {}},
- HOMEKIT_MODE_BRIDGE,
- advertise_ip=None,
- entry_id=entry.entry_id,
- )
+ homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
+
homekit.driver = hk_driver
- # pylint: disable=protected-access
- homekit._filter = Mock(return_value=True)
homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge")
config_entry = MockConfigEntry(domain="test", data={})
@@ -1202,20 +980,17 @@ async def test_homekit_finds_linked_motion_sensors(
)
hass.states.async_set(camera.entity_id, STATE_ON)
- def _mock_get_accessory(*args, **kwargs):
- return [None, "acc", None]
-
with patch.object(homekit.bridge, "add_accessory"), patch(
f"{PATH_HOMEKIT}.show_setup_message"
), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch(
- "pyhap.accessory_driver.AccessoryDriver.start_service"
+ "pyhap.accessory_driver.AccessoryDriver.async_start"
):
await homekit.async_start()
await hass.async_block_till_done()
mock_get_acc.assert_called_with(
hass,
- hk_driver,
+ ANY,
ANY,
ANY,
{
@@ -1228,24 +1003,14 @@ def _mock_get_accessory(*args, **kwargs):
async def test_homekit_finds_linked_humidity_sensors(
- hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf
+ hass, hk_driver, device_reg, entity_reg, mock_zeroconf
):
"""Test HomeKit start method."""
entry = await async_init_integration(hass)
- homekit = HomeKit(
- hass,
- None,
- None,
- None,
- {},
- {"humidifier.humidifier": {}},
- HOMEKIT_MODE_BRIDGE,
- advertise_ip=None,
- entry_id=entry.entry_id,
- )
+ homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
+
homekit.driver = hk_driver
- homekit._filter = Mock(return_value=True)
homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge")
config_entry = MockConfigEntry(domain="test", data={})
@@ -1279,20 +1044,17 @@ async def test_homekit_finds_linked_humidity_sensors(
)
hass.states.async_set(humidifier.entity_id, STATE_ON)
- def _mock_get_accessory(*args, **kwargs):
- return [None, "acc", None]
-
with patch.object(homekit.bridge, "add_accessory"), patch(
f"{PATH_HOMEKIT}.show_setup_message"
), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch(
- "pyhap.accessory_driver.AccessoryDriver.start_service"
+ "pyhap.accessory_driver.AccessoryDriver.async_start"
):
await homekit.async_start()
await hass.async_block_till_done()
mock_get_acc.assert_called_with(
hass,
- hk_driver,
+ ANY,
ANY,
ANY,
{
@@ -1306,6 +1068,7 @@ def _mock_get_accessory(*args, **kwargs):
async def test_reload(hass, mock_zeroconf):
"""Test we can reload from yaml."""
+ await async_setup_component(hass, "persistent_notification", {})
entry = MockConfigEntry(
domain=DOMAIN,
source=SOURCE_IMPORT,
@@ -1316,7 +1079,6 @@ async def test_reload(hass, mock_zeroconf):
with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit:
mock_homekit.return_value = homekit = Mock()
- type(homekit).async_start = AsyncMock()
assert await async_setup_component(
hass, "homekit", {"homekit": {CONF_NAME: "reloadable", CONF_PORT: 12345}}
)
@@ -1328,12 +1090,13 @@ async def test_reload(hass, mock_zeroconf):
12345,
None,
ANY,
+ False,
{},
HOMEKIT_MODE_BRIDGE,
None,
entry.entry_id,
+ entry.title,
)
- assert mock_homekit().setup.called is True
yaml_path = os.path.join(
_get_fixtures_base_path(),
"fixtures",
@@ -1346,10 +1109,9 @@ async def test_reload(hass, mock_zeroconf):
), patch(
f"{PATH_HOMEKIT}.get_accessory"
), patch(
- "pyhap.accessory_driver.AccessoryDriver.start_service"
+ "pyhap.accessory_driver.AccessoryDriver.async_start"
):
mock_homekit2.return_value = homekit = Mock()
- type(homekit).async_start = AsyncMock()
await hass.services.async_call(
"homekit",
SERVICE_RELOAD,
@@ -1364,12 +1126,13 @@ async def test_reload(hass, mock_zeroconf):
45678,
None,
ANY,
+ False,
{},
HOMEKIT_MODE_BRIDGE,
None,
entry.entry_id,
+ entry.title,
)
- assert mock_homekit2().setup.called is True
def _get_fixtures_base_path():
@@ -1377,41 +1140,64 @@ def _get_fixtures_base_path():
async def test_homekit_start_in_accessory_mode(
- hass, hk_driver, device_reg, debounce_patcher
+ hass, hk_driver, mock_zeroconf, device_reg
):
"""Test HomeKit start method in accessory mode."""
entry = await async_init_integration(hass)
- pin = b"123-45-678"
- homekit = HomeKit(
- hass,
- None,
- None,
- None,
- {},
- {},
- HOMEKIT_MODE_ACCESSORY,
- advertise_ip=None,
- entry_id=entry.entry_id,
- )
+ homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY)
+
homekit.bridge = Mock()
homekit.bridge.accessories = []
homekit.driver = hk_driver
- # pylint: disable=protected-access
- homekit._filter = Mock(return_value=True)
homekit.driver.accessory = Accessory(hk_driver, "any")
hass.states.async_set("light.demo", "on")
with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch(
- "pyhap.accessory_driver.AccessoryDriver.add_accessory"
- ), patch(f"{PATH_HOMEKIT}.show_setup_message") as mock_setup_msg, patch(
- "pyhap.accessory_driver.AccessoryDriver.start_service"
+ f"{PATH_HOMEKIT}.show_setup_message"
+ ) as mock_setup_msg, patch(
+ "pyhap.accessory_driver.AccessoryDriver.async_start"
) as hk_driver_start:
await homekit.async_start()
await hass.async_block_till_done()
mock_add_acc.assert_not_called()
- mock_setup_msg.assert_called_with(hass, entry.entry_id, None, pin, ANY)
+ mock_setup_msg.assert_called_with(
+ hass, entry.entry_id, "Mock Title (demo)", ANY, ANY
+ )
assert hk_driver_start.called
assert homekit.status == STATUS_RUNNING
+
+
+async def test_wait_for_port_to_free(hass, hk_driver, mock_zeroconf, caplog):
+ """Test we wait for the port to free before declaring unload success."""
+ await async_setup_component(hass, "persistent_notification", {})
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT},
+ options={},
+ )
+ entry.add_to_hass(hass)
+
+ with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch(
+ f"{PATH_HOMEKIT}.HomeKit.async_stop"
+ ), patch(f"{PATH_HOMEKIT}.port_is_available", return_value=True) as port_mock:
+ assert await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+ assert await hass.config_entries.async_unload(entry.entry_id)
+ await hass.async_block_till_done()
+ assert "Waiting for the HomeKit server to shutdown" not in caplog.text
+ assert port_mock.called
+
+ with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch(
+ f"{PATH_HOMEKIT}.HomeKit.async_stop"
+ ), patch.object(homekit_base, "PORT_CLEANUP_CHECK_INTERVAL_SECS", 0), patch(
+ f"{PATH_HOMEKIT}.port_is_available", return_value=False
+ ) as port_mock:
+ assert await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+ assert await hass.config_entries.async_unload(entry.entry_id)
+ await hass.async_block_till_done()
+ assert "Waiting for the HomeKit server to shutdown" in caplog.text
+ assert port_mock.called
diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py
index 804e03a4e6c9a2..ba08ea3caafc5a 100644
--- a/tests/components/homekit/test_type_cameras.py
+++ b/tests/components/homekit/test_type_cameras.py
@@ -45,35 +45,35 @@
async def _async_start_streaming(hass, acc):
"""Start streaming a camera."""
acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
async def _async_setup_endpoints(hass, acc):
"""Set camera endpoints."""
acc.set_endpoints(MOCK_END_POINTS_TLV)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
async def _async_reconfigure_stream(hass, acc, session_info, stream_config):
"""Reconfigure the stream."""
await acc.reconfigure_stream(session_info, stream_config)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
async def _async_stop_all_streams(hass, acc):
"""Stop all camera streams."""
await acc.stop()
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
async def _async_stop_stream(hass, acc, session_info):
"""Stop a camera stream."""
await acc.stop_stream(session_info)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
@@ -99,6 +99,7 @@ def _get_exits_after_startup_mock_ffmpeg():
ffmpeg.open = AsyncMock(return_value=True)
ffmpeg.close = AsyncMock(return_value=True)
ffmpeg.kill = AsyncMock(return_value=True)
+ ffmpeg.get_reader = AsyncMock()
return ffmpeg
@@ -108,6 +109,7 @@ def _get_working_mock_ffmpeg():
ffmpeg.open = AsyncMock(return_value=True)
ffmpeg.close = AsyncMock(return_value=True)
ffmpeg.kill = AsyncMock(return_value=True)
+ ffmpeg.get_reader = AsyncMock()
return ffmpeg
@@ -118,6 +120,7 @@ def _get_failing_mock_ffmpeg():
ffmpeg.open = AsyncMock(return_value=False)
ffmpeg.close = AsyncMock(side_effect=OSError)
ffmpeg.kill = AsyncMock(side_effect=OSError)
+ ffmpeg.get_reader = AsyncMock()
return ffmpeg
@@ -153,7 +156,7 @@ async def test_camera_stream_source_configured(hass, run_driver, events):
bridge.add_accessory(acc)
bridge.add_accessory(not_camera_acc)
- await acc.run_handler()
+ await acc.run()
assert acc.aid == 2
assert acc.category == 17 # Camera
@@ -189,6 +192,8 @@ async def test_camera_stream_source_configured(hass, run_driver, events):
input_source="-i /dev/null",
output=expected_output.format(**session_info),
stdout_pipe=False,
+ extra_cmd="-hide_banner -nostats",
+ stderr_pipe=True,
)
await _async_setup_endpoints(hass, acc)
@@ -212,22 +217,23 @@ async def test_camera_stream_source_configured(hass, run_driver, events):
)
with patch("turbojpeg.TurboJPEG", return_value=turbo_jpeg):
TurboJPEGSingleton()
- assert await hass.async_add_executor_job(
- acc.get_snapshot, {"aid": 2, "image-width": 300, "image-height": 200}
+ assert await acc.async_get_snapshot(
+ {"aid": 2, "image-width": 300, "image-height": 200}
)
- # Verify the bridge only forwards get_snapshot for
+ # Verify the bridge only forwards async_get_snapshot for
# cameras and valid accessory ids
- assert await hass.async_add_executor_job(
- bridge.get_snapshot, {"aid": 2, "image-width": 300, "image-height": 200}
+ assert await bridge.async_get_snapshot(
+ {"aid": 2, "image-width": 300, "image-height": 200}
)
with pytest.raises(ValueError):
- assert await hass.async_add_executor_job(
- bridge.get_snapshot, {"aid": 3, "image-width": 300, "image-height": 200}
+ assert await bridge.async_get_snapshot(
+ {"aid": 3, "image-width": 300, "image-height": 200}
)
+
with pytest.raises(ValueError):
- assert await hass.async_add_executor_job(
- bridge.get_snapshot, {"aid": 4, "image-width": 300, "image-height": 200}
+ assert await bridge.async_get_snapshot(
+ {"aid": 4, "image-width": 300, "image-height": 200}
)
@@ -265,7 +271,7 @@ async def test_camera_stream_source_configured_with_failing_ffmpeg(
bridge.add_accessory(acc)
bridge.add_accessory(not_camera_acc)
- await acc.run_handler()
+ await acc.run()
assert acc.aid == 2
assert acc.category == 17 # Camera
@@ -305,7 +311,7 @@ async def test_camera_stream_source_found(hass, run_driver, events):
2,
{},
)
- await acc.run_handler()
+ await acc.run()
assert acc.aid == 2
assert acc.category == 17 # Camera
@@ -355,7 +361,7 @@ async def test_camera_stream_source_fails(hass, run_driver, events):
2,
{},
)
- await acc.run_handler()
+ await acc.run()
assert acc.aid == 2
assert acc.category == 17 # Camera
@@ -390,7 +396,7 @@ async def test_camera_with_no_stream(hass, run_driver, events):
2,
{},
)
- await acc.run_handler()
+ await acc.run()
assert acc.aid == 2
assert acc.category == 17 # Camera
@@ -400,8 +406,8 @@ async def test_camera_with_no_stream(hass, run_driver, events):
await _async_stop_all_streams(hass, acc)
with pytest.raises(HomeAssistantError):
- await hass.async_add_executor_job(
- acc.get_snapshot, {"aid": 2, "image-width": 300, "image-height": 200}
+ assert await acc.async_get_snapshot(
+ {"aid": 2, "image-width": 300, "image-height": 200}
)
@@ -433,7 +439,7 @@ async def test_camera_stream_source_configured_and_copy_codec(hass, run_driver,
bridge = HomeBridge("hass", run_driver, "Test Bridge")
bridge.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
assert acc.aid == 2
assert acc.category == 17 # Camera
@@ -471,6 +477,8 @@ async def test_camera_stream_source_configured_and_copy_codec(hass, run_driver,
input_source="-i /dev/null",
output=expected_output.format(**session_info),
stdout_pipe=False,
+ extra_cmd="-hide_banner -nostats",
+ stderr_pipe=True,
)
@@ -502,7 +510,7 @@ async def test_camera_streaming_fails_after_starting_ffmpeg(hass, run_driver, ev
bridge = HomeBridge("hass", run_driver, "Test Bridge")
bridge.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
assert acc.aid == 2
assert acc.category == 17 # Camera
@@ -541,6 +549,8 @@ async def test_camera_streaming_fails_after_starting_ffmpeg(hass, run_driver, ev
input_source="-i /dev/null",
output=expected_output.format(**session_info),
stdout_pipe=False,
+ extra_cmd="-hide_banner -nostats",
+ stderr_pipe=True,
)
@@ -578,7 +588,7 @@ async def test_camera_with_linked_motion_sensor(hass, run_driver, events):
bridge = HomeBridge("hass", run_driver, "Test Bridge")
bridge.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
assert acc.aid == 2
assert acc.category == 17 # Camera
@@ -607,7 +617,7 @@ async def test_camera_with_linked_motion_sensor(hass, run_driver, events):
# motion sensor is removed
hass.states.async_remove(motion_entity_id)
await hass.async_block_till_done()
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert char.value is True
@@ -634,7 +644,7 @@ async def test_camera_with_a_missing_linked_motion_sensor(hass, run_driver, even
bridge = HomeBridge("hass", run_driver, "Test Bridge")
bridge.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
assert acc.aid == 2
assert acc.category == 17 # Camera
@@ -676,7 +686,7 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events):
bridge = HomeBridge("hass", run_driver, "Test Bridge")
bridge.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
assert acc.aid == 2
assert acc.category == 17 # Camera
@@ -715,7 +725,7 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events):
# doorbell sensor is removed
hass.states.async_remove(doorbell_entity_id)
await hass.async_block_till_done()
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert char.value == 0
assert char2.value == 0
@@ -743,7 +753,7 @@ async def test_camera_with_a_missing_linked_doorbell_sensor(hass, run_driver, ev
bridge = HomeBridge("hass", run_driver, "Test Bridge")
bridge.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
assert acc.aid == 2
assert acc.category == 17 # Camera
diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py
index 48b20e8e0b897c..8514801687c39e 100644
--- a/tests/components/homekit/test_type_covers.py
+++ b/tests/components/homekit/test_type_covers.py
@@ -1,7 +1,4 @@
"""Test different accessory types: Covers."""
-from collections import namedtuple
-
-import pytest
from homeassistant.components.cover import (
ATTR_CURRENT_POSITION,
@@ -22,6 +19,12 @@
HK_DOOR_OPEN,
HK_DOOR_OPENING,
)
+from homeassistant.components.homekit.type_covers import (
+ GarageDoorOpener,
+ Window,
+ WindowCovering,
+ WindowCoveringBasic,
+)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
@@ -37,41 +40,19 @@
STATE_UNKNOWN,
)
from homeassistant.core import CoreState
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from tests.common import async_mock_service
-from tests.components.homekit.common import patch_debounce
-
-
-@pytest.fixture(scope="module")
-def cls():
- """Patch debounce decorator during import of type_covers."""
- patcher = patch_debounce()
- patcher.start()
- _import = __import__(
- "homeassistant.components.homekit.type_covers",
- fromlist=["GarageDoorOpener", "WindowCovering", "WindowCoveringBasic"],
- )
- patcher_tuple = namedtuple(
- "Cls", ["window", "windowcovering", "windowcovering_basic", "garage"]
- )
- yield patcher_tuple(
- window=_import.Window,
- windowcovering=_import.WindowCovering,
- windowcovering_basic=_import.WindowCoveringBasic,
- garage=_import.GarageDoorOpener,
- )
- patcher.stop()
-async def test_garage_door_open_close(hass, hk_driver, cls, events):
+async def test_garage_door_open_close(hass, hk_driver, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = "cover.garage_door"
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
- acc = cls.garage(hass, hk_driver, "Garage Door", entity_id, 2, None)
- await acc.run_handler()
+ acc = GarageDoorOpener(hass, hk_driver, "Garage Door", entity_id, 2, None)
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -148,14 +129,14 @@ async def test_garage_door_open_close(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] is None
-async def test_windowcovering_set_cover_position(hass, hk_driver, cls, events):
+async def test_windowcovering_set_cover_position(hass, hk_driver, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = "cover.window"
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
- acc = cls.windowcovering(hass, hk_driver, "Cover", entity_id, 2, None)
- await acc.run_handler()
+ acc = WindowCovering(hass, hk_driver, "Cover", entity_id, 2, None)
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -218,14 +199,14 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] == 75
-async def test_window_instantiate(hass, hk_driver, cls, events):
+async def test_window_instantiate(hass, hk_driver, events):
"""Test if Window accessory is instantiated correctly."""
entity_id = "cover.window"
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
- acc = cls.window(hass, hk_driver, "Window", entity_id, 2, None)
- await acc.run_handler()
+ acc = Window(hass, hk_driver, "Window", entity_id, 2, None)
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -235,7 +216,7 @@ async def test_window_instantiate(hass, hk_driver, cls, events):
assert acc.char_target_position.value == 0
-async def test_windowcovering_cover_set_tilt(hass, hk_driver, cls, events):
+async def test_windowcovering_cover_set_tilt(hass, hk_driver, events):
"""Test if accessory and HA update slat tilt accordingly."""
entity_id = "cover.window"
@@ -243,8 +224,8 @@ async def test_windowcovering_cover_set_tilt(hass, hk_driver, cls, events):
entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_TILT_POSITION}
)
await hass.async_block_till_done()
- acc = cls.windowcovering(hass, hk_driver, "Cover", entity_id, 2, None)
- await acc.run_handler()
+ acc = WindowCovering(hass, hk_driver, "Cover", entity_id, 2, None)
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -302,13 +283,13 @@ async def test_windowcovering_cover_set_tilt(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] == 75
-async def test_windowcovering_open_close(hass, hk_driver, cls, events):
+async def test_windowcovering_open_close(hass, hk_driver, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = "cover.window"
hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: 0})
- acc = cls.windowcovering_basic(hass, hk_driver, "Cover", entity_id, 2, None)
- await acc.run_handler()
+ acc = WindowCoveringBasic(hass, hk_driver, "Cover", entity_id, 2, None)
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -383,15 +364,15 @@ async def test_windowcovering_open_close(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] is None
-async def test_windowcovering_open_close_stop(hass, hk_driver, cls, events):
+async def test_windowcovering_open_close_stop(hass, hk_driver, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = "cover.window"
hass.states.async_set(
entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}
)
- acc = cls.windowcovering_basic(hass, hk_driver, "Cover", entity_id, 2, None)
- await acc.run_handler()
+ acc = WindowCoveringBasic(hass, hk_driver, "Cover", entity_id, 2, None)
+ await acc.run()
await hass.async_block_till_done()
# Set from HomeKit
@@ -431,7 +412,7 @@ async def test_windowcovering_open_close_stop(hass, hk_driver, cls, events):
async def test_windowcovering_open_close_with_position_and_stop(
- hass, hk_driver, cls, events
+ hass, hk_driver, events
):
"""Test if accessory and HA are updated accordingly."""
entity_id = "cover.stop_window"
@@ -441,8 +422,8 @@ async def test_windowcovering_open_close_with_position_and_stop(
STATE_UNKNOWN,
{ATTR_SUPPORTED_FEATURES: SUPPORT_STOP | SUPPORT_SET_POSITION},
)
- acc = cls.windowcovering(hass, hk_driver, "Cover", entity_id, 2, None)
- await acc.run_handler()
+ acc = WindowCovering(hass, hk_driver, "Cover", entity_id, 2, None)
+ await acc.run()
await hass.async_block_till_done()
# Set from HomeKit
@@ -461,11 +442,11 @@ async def test_windowcovering_open_close_with_position_and_stop(
assert events[-1].data[ATTR_VALUE] is None
-async def test_windowcovering_basic_restore(hass, hk_driver, cls, events):
+async def test_windowcovering_basic_restore(hass, hk_driver, events):
"""Test setting up an entity from state in the event registry."""
hass.state = CoreState.not_running
- registry = await entity_registry.async_get_registry(hass)
+ registry = er.async_get(hass)
registry.async_get_or_create(
"cover",
@@ -486,26 +467,24 @@ async def test_windowcovering_basic_restore(hass, hk_driver, cls, events):
hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {})
await hass.async_block_till_done()
- acc = cls.windowcovering_basic(hass, hk_driver, "Cover", "cover.simple", 2, None)
+ acc = WindowCoveringBasic(hass, hk_driver, "Cover", "cover.simple", 2, None)
assert acc.category == 14
assert acc.char_current_position is not None
assert acc.char_target_position is not None
assert acc.char_position_state is not None
- acc = cls.windowcovering_basic(
- hass, hk_driver, "Cover", "cover.all_info_set", 2, None
- )
+ acc = WindowCoveringBasic(hass, hk_driver, "Cover", "cover.all_info_set", 2, None)
assert acc.category == 14
assert acc.char_current_position is not None
assert acc.char_target_position is not None
assert acc.char_position_state is not None
-async def test_windowcovering_restore(hass, hk_driver, cls, events):
+async def test_windowcovering_restore(hass, hk_driver, events):
"""Test setting up an entity from state in the event registry."""
hass.state = CoreState.not_running
- registry = await entity_registry.async_get_registry(hass)
+ registry = er.async_get(hass)
registry.async_get_or_create(
"cover",
@@ -526,20 +505,20 @@ async def test_windowcovering_restore(hass, hk_driver, cls, events):
hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {})
await hass.async_block_till_done()
- acc = cls.windowcovering(hass, hk_driver, "Cover", "cover.simple", 2, None)
+ acc = WindowCovering(hass, hk_driver, "Cover", "cover.simple", 2, None)
assert acc.category == 14
assert acc.char_current_position is not None
assert acc.char_target_position is not None
assert acc.char_position_state is not None
- acc = cls.windowcovering(hass, hk_driver, "Cover", "cover.all_info_set", 2, None)
+ acc = WindowCovering(hass, hk_driver, "Cover", "cover.all_info_set", 2, None)
assert acc.category == 14
assert acc.char_current_position is not None
assert acc.char_target_position is not None
assert acc.char_position_state is not None
-async def test_garage_door_with_linked_obstruction_sensor(hass, hk_driver, cls, events):
+async def test_garage_door_with_linked_obstruction_sensor(hass, hk_driver, events):
"""Test if accessory and HA are updated accordingly with a linked obstruction sensor."""
linked_obstruction_sensor_entity_id = "binary_sensor.obstruction"
entity_id = "cover.garage_door"
@@ -547,7 +526,7 @@ async def test_garage_door_with_linked_obstruction_sensor(hass, hk_driver, cls,
hass.states.async_set(linked_obstruction_sensor_entity_id, STATE_OFF)
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
- acc = cls.garage(
+ acc = GarageDoorOpener(
hass,
hk_driver,
"Garage Door",
@@ -555,7 +534,7 @@ async def test_garage_door_with_linked_obstruction_sensor(hass, hk_driver, cls,
2,
{CONF_LINKED_OBSTRUCTION_SENSOR: linked_obstruction_sensor_entity_id},
)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py
index fc5ac4344ad90e..15e2366a883f25 100644
--- a/tests/components/homekit/test_type_fans.py
+++ b/tests/components/homekit/test_type_fans.py
@@ -1,21 +1,24 @@
"""Test different accessory types: Fans."""
-from collections import namedtuple
from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE
-import pytest
from homeassistant.components.fan import (
ATTR_DIRECTION,
ATTR_OSCILLATING,
ATTR_PERCENTAGE,
+ ATTR_PERCENTAGE_STEP,
+ ATTR_PRESET_MODE,
+ ATTR_PRESET_MODES,
DIRECTION_FORWARD,
DIRECTION_REVERSE,
DOMAIN,
SUPPORT_DIRECTION,
SUPPORT_OSCILLATE,
+ SUPPORT_PRESET_MODE,
SUPPORT_SET_SPEED,
)
-from homeassistant.components.homekit.const import ATTR_VALUE
+from homeassistant.components.homekit.const import ATTR_VALUE, PROP_MIN_STEP
+from homeassistant.components.homekit.type_fans import Fan
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
@@ -25,30 +28,18 @@
STATE_UNKNOWN,
)
from homeassistant.core import CoreState
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from tests.common import async_mock_service
-from tests.components.homekit.common import patch_debounce
-@pytest.fixture(scope="module")
-def cls():
- """Patch debounce decorator during import of type_fans."""
- patcher = patch_debounce()
- patcher.start()
- _import = __import__("homeassistant.components.homekit.type_fans", fromlist=["Fan"])
- patcher_tuple = namedtuple("Cls", ["fan"])
- yield patcher_tuple(fan=_import.Fan)
- patcher.stop()
-
-
-async def test_fan_basic(hass, hk_driver, cls, events):
+async def test_fan_basic(hass, hk_driver, events):
"""Test fan with char state."""
entity_id = "fan.demo"
hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0})
await hass.async_block_till_done()
- acc = cls.fan(hass, hk_driver, "Fan", entity_id, 1, None)
+ acc = Fan(hass, hk_driver, "Fan", entity_id, 1, None)
hk_driver.add_accessory(acc)
assert acc.aid == 1
@@ -58,7 +49,7 @@ async def test_fan_basic(hass, hk_driver, cls, events):
# If there are no speed_list values, then HomeKit speed is unsupported
assert acc.char_speed is None
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_active.value == 1
@@ -120,7 +111,7 @@ async def test_fan_basic(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] is None
-async def test_fan_direction(hass, hk_driver, cls, events):
+async def test_fan_direction(hass, hk_driver, events):
"""Test fan with direction."""
entity_id = "fan.demo"
@@ -130,12 +121,12 @@ async def test_fan_direction(hass, hk_driver, cls, events):
{ATTR_SUPPORTED_FEATURES: SUPPORT_DIRECTION, ATTR_DIRECTION: DIRECTION_FORWARD},
)
await hass.async_block_till_done()
- acc = cls.fan(hass, hk_driver, "Fan", entity_id, 1, None)
+ acc = Fan(hass, hk_driver, "Fan", entity_id, 1, None)
hk_driver.add_accessory(acc)
assert acc.char_direction.value == 0
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_direction.value == 0
@@ -188,7 +179,7 @@ async def test_fan_direction(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] == DIRECTION_REVERSE
-async def test_fan_oscillate(hass, hk_driver, cls, events):
+async def test_fan_oscillate(hass, hk_driver, events):
"""Test fan with oscillate."""
entity_id = "fan.demo"
@@ -198,12 +189,12 @@ async def test_fan_oscillate(hass, hk_driver, cls, events):
{ATTR_SUPPORTED_FEATURES: SUPPORT_OSCILLATE, ATTR_OSCILLATING: False},
)
await hass.async_block_till_done()
- acc = cls.fan(hass, hk_driver, "Fan", entity_id, 1, None)
+ acc = Fan(hass, hk_driver, "Fan", entity_id, 1, None)
hk_driver.add_accessory(acc)
assert acc.char_swing.value == 0
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_swing.value == 0
@@ -257,7 +248,7 @@ async def test_fan_oscillate(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] is True
-async def test_fan_speed(hass, hk_driver, cls, events):
+async def test_fan_speed(hass, hk_driver, events):
"""Test fan with speed."""
entity_id = "fan.demo"
@@ -267,17 +258,19 @@ async def test_fan_speed(hass, hk_driver, cls, events):
{
ATTR_SUPPORTED_FEATURES: SUPPORT_SET_SPEED,
ATTR_PERCENTAGE: 0,
+ ATTR_PERCENTAGE_STEP: 25,
},
)
await hass.async_block_till_done()
- acc = cls.fan(hass, hk_driver, "Fan", entity_id, 1, None)
+ acc = Fan(hass, hk_driver, "Fan", entity_id, 1, None)
hk_driver.add_accessory(acc)
# Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the
# speed to 100 when turning on a fan on a freshly booted up server.
assert acc.char_speed.value != 0
+ assert acc.char_speed.properties[PROP_MIN_STEP] == 25
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
hass.states.async_set(entity_id, STATE_ON, {ATTR_PERCENTAGE: 100})
@@ -336,7 +329,7 @@ async def test_fan_speed(hass, hk_driver, cls, events):
assert acc.char_active.value == 1
-async def test_fan_set_all_one_shot(hass, hk_driver, cls, events):
+async def test_fan_set_all_one_shot(hass, hk_driver, events):
"""Test fan with speed."""
entity_id = "fan.demo"
@@ -353,13 +346,13 @@ async def test_fan_set_all_one_shot(hass, hk_driver, cls, events):
},
)
await hass.async_block_till_done()
- acc = cls.fan(hass, hk_driver, "Fan", entity_id, 1, None)
+ acc = Fan(hass, hk_driver, "Fan", entity_id, 1, None)
hk_driver.add_accessory(acc)
# Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the
# speed to 100 when turning on a fan on a freshly booted up server.
assert acc.char_speed.value != 0
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
hass.states.async_set(
@@ -529,11 +522,11 @@ async def test_fan_set_all_one_shot(hass, hk_driver, cls, events):
assert len(call_set_direction) == 2
-async def test_fan_restore(hass, hk_driver, cls, events):
+async def test_fan_restore(hass, hk_driver, events):
"""Test setting up an entity from state in the event registry."""
hass.state = CoreState.not_running
- registry = await entity_registry.async_get_registry(hass)
+ registry = er.async_get(hass)
registry.async_get_or_create(
"fan",
@@ -554,16 +547,96 @@ async def test_fan_restore(hass, hk_driver, cls, events):
hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {})
await hass.async_block_till_done()
- acc = cls.fan(hass, hk_driver, "Fan", "fan.simple", 2, None)
+ acc = Fan(hass, hk_driver, "Fan", "fan.simple", 2, None)
assert acc.category == 3
assert acc.char_active is not None
assert acc.char_direction is None
assert acc.char_speed is None
assert acc.char_swing is None
- acc = cls.fan(hass, hk_driver, "Fan", "fan.all_info_set", 2, None)
+ acc = Fan(hass, hk_driver, "Fan", "fan.all_info_set", 2, None)
assert acc.category == 3
assert acc.char_active is not None
assert acc.char_direction is not None
assert acc.char_speed is not None
assert acc.char_swing is not None
+
+
+async def test_fan_preset_modes(hass, hk_driver, events):
+ """Test fan with direction."""
+ entity_id = "fan.demo"
+
+ hass.states.async_set(
+ entity_id,
+ STATE_ON,
+ {
+ ATTR_SUPPORTED_FEATURES: SUPPORT_PRESET_MODE,
+ ATTR_PRESET_MODE: "auto",
+ ATTR_PRESET_MODES: ["auto", "smart"],
+ },
+ )
+ await hass.async_block_till_done()
+ acc = Fan(hass, hk_driver, "Fan", entity_id, 1, None)
+ hk_driver.add_accessory(acc)
+
+ assert acc.preset_mode_chars["auto"].value == 1
+ assert acc.preset_mode_chars["smart"].value == 0
+
+ await acc.run()
+ await hass.async_block_till_done()
+
+ hass.states.async_set(
+ entity_id,
+ STATE_ON,
+ {
+ ATTR_SUPPORTED_FEATURES: SUPPORT_PRESET_MODE,
+ ATTR_PRESET_MODE: "smart",
+ ATTR_PRESET_MODES: ["auto", "smart"],
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert acc.preset_mode_chars["auto"].value == 0
+ assert acc.preset_mode_chars["smart"].value == 1
+ # Set from HomeKit
+ call_set_preset_mode = async_mock_service(hass, DOMAIN, "set_preset_mode")
+ call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
+
+ char_auto_iid = acc.preset_mode_chars["auto"].to_HAP()[HAP_REPR_IID]
+
+ hk_driver.set_characteristics(
+ {
+ HAP_REPR_CHARS: [
+ {
+ HAP_REPR_AID: acc.aid,
+ HAP_REPR_IID: char_auto_iid,
+ HAP_REPR_VALUE: 1,
+ },
+ ]
+ },
+ "mock_addr",
+ )
+ await hass.async_block_till_done()
+ assert call_set_preset_mode[0]
+ assert call_set_preset_mode[0].data[ATTR_ENTITY_ID] == entity_id
+ assert call_set_preset_mode[0].data[ATTR_PRESET_MODE] == "auto"
+ assert len(events) == 1
+ assert events[-1].data["service"] == "set_preset_mode"
+
+ hk_driver.set_characteristics(
+ {
+ HAP_REPR_CHARS: [
+ {
+ HAP_REPR_AID: acc.aid,
+ HAP_REPR_IID: char_auto_iid,
+ HAP_REPR_VALUE: 0,
+ },
+ ]
+ },
+ "mock_addr",
+ )
+ await hass.async_block_till_done()
+ assert call_turn_on[0]
+ assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
+ assert events[-1].data["service"] == "turn_on"
+ assert len(events) == 2
diff --git a/tests/components/homekit/test_type_humidifiers.py b/tests/components/homekit/test_type_humidifiers.py
index 51f9621d15afd2..1a301e340b3e8a 100644
--- a/tests/components/homekit/test_type_humidifiers.py
+++ b/tests/components/homekit/test_type_humidifiers.py
@@ -54,7 +54,7 @@ async def test_humidifier(hass, hk_driver, events):
)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 1
@@ -135,7 +135,7 @@ async def test_dehumidifier(hass, hk_driver, events):
)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 1
@@ -220,7 +220,7 @@ async def test_hygrostat_power_state(hass, hk_driver, events):
)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_current_humidifier_dehumidifier.value == 2
@@ -298,7 +298,7 @@ async def test_hygrostat_get_humidity_range(hass, hk_driver):
)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_target_humidity.properties[PROP_MAX_VALUE] == 45
@@ -332,7 +332,7 @@ async def test_humidifier_with_linked_humidity_sensor(hass, hk_driver):
)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_current_humidity.value == 42.0
@@ -384,7 +384,7 @@ async def test_humidifier_with_a_missing_linked_humidity_sensor(hass, hk_driver)
)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_current_humidity.value == 0
@@ -401,7 +401,7 @@ async def test_humidifier_as_dehumidifier(hass, hk_driver, events, caplog):
)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_target_humidifier_dehumidifier.value == 1
diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py
index e82bc5bb15ddb6..0c81de2efe7436 100644
--- a/tests/components/homekit/test_type_lights.py
+++ b/tests/components/homekit/test_type_lights.py
@@ -1,10 +1,9 @@
"""Test different accessory types: Lights."""
-from collections import namedtuple
from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE
-import pytest
from homeassistant.components.homekit.const import ATTR_VALUE
+from homeassistant.components.homekit.type_lights import Light
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
@@ -25,39 +24,25 @@
STATE_UNKNOWN,
)
from homeassistant.core import CoreState
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from tests.common import async_mock_service
-from tests.components.homekit.common import patch_debounce
-@pytest.fixture(scope="module")
-def cls():
- """Patch debounce decorator during import of type_lights."""
- patcher = patch_debounce()
- patcher.start()
- _import = __import__(
- "homeassistant.components.homekit.type_lights", fromlist=["Light"]
- )
- patcher_tuple = namedtuple("Cls", ["light"])
- yield patcher_tuple(light=_import.Light)
- patcher.stop()
-
-
-async def test_light_basic(hass, hk_driver, cls, events):
+async def test_light_basic(hass, hk_driver, events):
"""Test light with char state."""
entity_id = "light.demo"
hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0})
await hass.async_block_till_done()
- acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
+ acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
hk_driver.add_accessory(acc)
assert acc.aid == 1
assert acc.category == 5 # Lightbulb
assert acc.char_on.value
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_on.value == 1
@@ -113,7 +98,7 @@ async def test_light_basic(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] == "Set state to 0"
-async def test_light_brightness(hass, hk_driver, cls, events):
+async def test_light_brightness(hass, hk_driver, events):
"""Test light with brightness."""
entity_id = "light.demo"
@@ -123,7 +108,7 @@ async def test_light_brightness(hass, hk_driver, cls, events):
{ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255},
)
await hass.async_block_till_done()
- acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
+ acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
hk_driver.add_accessory(acc)
# Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the
@@ -132,7 +117,7 @@ async def test_light_brightness(hass, hk_driver, cls, events):
char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID]
char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID]
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_brightness.value == 100
@@ -231,7 +216,7 @@ async def test_light_brightness(hass, hk_driver, cls, events):
assert acc.char_brightness.value == 1
-async def test_light_color_temperature(hass, hk_driver, cls, events):
+async def test_light_color_temperature(hass, hk_driver, events):
"""Test light with color temperature."""
entity_id = "light.demo"
@@ -241,12 +226,12 @@ async def test_light_color_temperature(hass, hk_driver, cls, events):
{ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP: 190},
)
await hass.async_block_till_done()
- acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
+ acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
hk_driver.add_accessory(acc)
assert acc.char_color_temperature.value == 190
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_color_temperature.value == 190
@@ -278,7 +263,7 @@ async def test_light_color_temperature(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] == "color temperature at 250"
-async def test_light_color_temperature_and_rgb_color(hass, hk_driver, cls, events):
+async def test_light_color_temperature_and_rgb_color(hass, hk_driver, events):
"""Test light with color temperature and rgb color not exposing temperature."""
entity_id = "light.demo"
@@ -292,7 +277,7 @@ async def test_light_color_temperature_and_rgb_color(hass, hk_driver, cls, event
},
)
await hass.async_block_till_done()
- acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None)
+ acc = Light(hass, hk_driver, "Light", entity_id, 2, None)
assert acc.char_hue.value == 260
assert acc.char_saturation.value == 90
@@ -300,20 +285,20 @@ async def test_light_color_temperature_and_rgb_color(hass, hk_driver, cls, event
hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 224})
await hass.async_block_till_done()
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_hue.value == 27
assert acc.char_saturation.value == 27
hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 352})
await hass.async_block_till_done()
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_hue.value == 28
assert acc.char_saturation.value == 61
-async def test_light_rgb_color(hass, hk_driver, cls, events):
+async def test_light_rgb_color(hass, hk_driver, events):
"""Test light with rgb_color."""
entity_id = "light.demo"
@@ -323,13 +308,13 @@ async def test_light_rgb_color(hass, hk_driver, cls, events):
{ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, ATTR_HS_COLOR: (260, 90)},
)
await hass.async_block_till_done()
- acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
+ acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
hk_driver.add_accessory(acc)
assert acc.char_hue.value == 260
assert acc.char_saturation.value == 90
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_hue.value == 260
assert acc.char_saturation.value == 90
@@ -365,11 +350,11 @@ async def test_light_rgb_color(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)"
-async def test_light_restore(hass, hk_driver, cls, events):
+async def test_light_restore(hass, hk_driver, events):
"""Test setting up an entity from state in the event registry."""
hass.state = CoreState.not_running
- registry = await entity_registry.async_get_registry(hass)
+ registry = er.async_get(hass)
registry.async_get_or_create("light", "hue", "1234", suggested_object_id="simple")
registry.async_get_or_create(
@@ -385,20 +370,20 @@ async def test_light_restore(hass, hk_driver, cls, events):
hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {})
await hass.async_block_till_done()
- acc = cls.light(hass, hk_driver, "Light", "light.simple", 1, None)
+ acc = Light(hass, hk_driver, "Light", "light.simple", 1, None)
hk_driver.add_accessory(acc)
assert acc.category == 5 # Lightbulb
assert acc.chars == []
assert acc.char_on.value == 0
- acc = cls.light(hass, hk_driver, "Light", "light.all_info_set", 2, None)
+ acc = Light(hass, hk_driver, "Light", "light.all_info_set", 2, None)
assert acc.category == 5 # Lightbulb
assert acc.chars == ["Brightness"]
assert acc.char_on.value == 0
-async def test_light_set_brightness_and_color(hass, hk_driver, cls, events):
+async def test_light_set_brightness_and_color(hass, hk_driver, events):
"""Test light with all chars in one go."""
entity_id = "light.demo"
@@ -411,7 +396,7 @@ async def test_light_set_brightness_and_color(hass, hk_driver, cls, events):
},
)
await hass.async_block_till_done()
- acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
+ acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
hk_driver.add_accessory(acc)
# Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the
@@ -422,7 +407,7 @@ async def test_light_set_brightness_and_color(hass, hk_driver, cls, events):
char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID]
char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID]
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_brightness.value == 100
@@ -474,7 +459,7 @@ async def test_light_set_brightness_and_color(hass, hk_driver, cls, events):
)
-async def test_light_set_brightness_and_color_temp(hass, hk_driver, cls, events):
+async def test_light_set_brightness_and_color_temp(hass, hk_driver, events):
"""Test light with all chars in one go."""
entity_id = "light.demo"
@@ -487,7 +472,7 @@ async def test_light_set_brightness_and_color_temp(hass, hk_driver, cls, events)
},
)
await hass.async_block_till_done()
- acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
+ acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
hk_driver.add_accessory(acc)
# Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the
@@ -497,7 +482,7 @@ async def test_light_set_brightness_and_color_temp(hass, hk_driver, cls, events)
char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID]
char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID]
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_brightness.value == 100
diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py
index 7899af36995be4..b2bb9b4736ea94 100644
--- a/tests/components/homekit/test_type_locks.py
+++ b/tests/components/homekit/test_type_locks.py
@@ -24,7 +24,7 @@ async def test_lock_unlock(hass, hk_driver, events):
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
acc = Lock(hass, hk_driver, "Lock", entity_id, 2, config)
- await acc.run_handler()
+ await acc.run()
assert acc.aid == 2
assert acc.category == 6 # DoorLock
diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py
index 9516963a982327..b95903d3e3f68a 100644
--- a/tests/components/homekit/test_type_media_players.py
+++ b/tests/components/homekit/test_type_media_players.py
@@ -37,7 +37,7 @@
STATE_STANDBY,
)
from homeassistant.core import CoreState
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from tests.common import async_mock_service
@@ -61,7 +61,7 @@ async def test_media_player_set_state(hass, hk_driver, events):
)
await hass.async_block_till_done()
acc = MediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, config)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -203,7 +203,7 @@ async def test_media_player_television(hass, hk_driver, events, caplog):
)
await hass.async_block_till_done()
acc = TelevisionMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -375,7 +375,7 @@ async def test_media_player_television_basic(hass, hk_driver, events, caplog):
)
await hass.async_block_till_done()
acc = TelevisionMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.chars_tv == [CHAR_REMOTE_KEY]
@@ -411,7 +411,7 @@ async def test_media_player_television_supports_source_select_no_sources(
)
await hass.async_block_till_done()
acc = TelevisionMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.support_select_source is False
@@ -421,7 +421,7 @@ async def test_tv_restore(hass, hk_driver, events):
"""Test setting up an entity from state in the event registry."""
hass.state = CoreState.not_running
- registry = await entity_registry.async_get_registry(hass)
+ registry = er.async_get(hass)
registry.async_get_or_create(
"media_player",
diff --git a/tests/components/homekit/test_type_remote.py b/tests/components/homekit/test_type_remote.py
new file mode 100644
index 00000000000000..e69ebfb29fbcbe
--- /dev/null
+++ b/tests/components/homekit/test_type_remote.py
@@ -0,0 +1,148 @@
+"""Test different accessory types: Remotes."""
+
+from homeassistant.components.homekit.const import (
+ ATTR_KEY_NAME,
+ ATTR_VALUE,
+ EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED,
+ KEY_ARROW_RIGHT,
+)
+from homeassistant.components.homekit.type_remotes import ActivityRemote
+from homeassistant.components.remote import (
+ ATTR_ACTIVITY,
+ ATTR_ACTIVITY_LIST,
+ ATTR_CURRENT_ACTIVITY,
+ DOMAIN,
+ SUPPORT_ACTIVITY,
+)
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
+ STATE_OFF,
+ STATE_ON,
+ STATE_STANDBY,
+)
+
+from tests.common import async_mock_service
+
+
+async def test_activity_remote(hass, hk_driver, events, caplog):
+ """Test if remote accessory and HA are updated accordingly."""
+ entity_id = "remote.harmony"
+ hass.states.async_set(
+ entity_id,
+ None,
+ {
+ ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY,
+ ATTR_CURRENT_ACTIVITY: "Apple TV",
+ ATTR_ACTIVITY_LIST: ["TV", "Apple TV"],
+ },
+ )
+ await hass.async_block_till_done()
+ acc = ActivityRemote(hass, hk_driver, "ActivityRemote", entity_id, 2, None)
+ await acc.run()
+ await hass.async_block_till_done()
+
+ assert acc.aid == 2
+ assert acc.category == 31 # Television
+
+ assert acc.char_active.value == 0
+ assert acc.char_remote_key.value == 0
+ assert acc.char_input_source.value == 1
+
+ hass.states.async_set(
+ entity_id,
+ STATE_ON,
+ {
+ ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY,
+ ATTR_CURRENT_ACTIVITY: "Apple TV",
+ ATTR_ACTIVITY_LIST: ["TV", "Apple TV"],
+ },
+ )
+ await hass.async_block_till_done()
+ assert acc.char_active.value == 1
+
+ hass.states.async_set(entity_id, STATE_OFF)
+ await hass.async_block_till_done()
+ assert acc.char_active.value == 0
+
+ hass.states.async_set(entity_id, STATE_ON)
+ await hass.async_block_till_done()
+ assert acc.char_active.value == 1
+
+ hass.states.async_set(entity_id, STATE_STANDBY)
+ await hass.async_block_till_done()
+ assert acc.char_active.value == 0
+
+ hass.states.async_set(
+ entity_id,
+ STATE_ON,
+ {
+ ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY,
+ ATTR_CURRENT_ACTIVITY: "TV",
+ ATTR_ACTIVITY_LIST: ["TV", "Apple TV"],
+ },
+ )
+ await hass.async_block_till_done()
+ assert acc.char_input_source.value == 0
+
+ hass.states.async_set(
+ entity_id,
+ STATE_ON,
+ {
+ ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY,
+ ATTR_CURRENT_ACTIVITY: "Apple TV",
+ ATTR_ACTIVITY_LIST: ["TV", "Apple TV"],
+ },
+ )
+ await hass.async_block_till_done()
+ assert acc.char_input_source.value == 1
+
+ # Set from HomeKit
+ call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
+ call_turn_off = async_mock_service(hass, DOMAIN, "turn_off")
+
+ acc.char_active.client_update_value(1)
+ await hass.async_block_till_done()
+ assert call_turn_on
+ assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] is None
+
+ acc.char_active.client_update_value(0)
+ await hass.async_block_till_done()
+ assert call_turn_off
+ assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] is None
+
+ acc.char_input_source.client_update_value(1)
+ await hass.async_block_till_done()
+ assert call_turn_on
+ assert call_turn_on[1].data[ATTR_ENTITY_ID] == entity_id
+ assert call_turn_on[1].data[ATTR_ACTIVITY] == "Apple TV"
+ assert len(events) == 3
+ assert events[-1].data[ATTR_VALUE] is None
+
+ acc.char_input_source.client_update_value(0)
+ await hass.async_block_till_done()
+ assert call_turn_on
+ assert call_turn_on[2].data[ATTR_ENTITY_ID] == entity_id
+ assert call_turn_on[2].data[ATTR_ACTIVITY] == "TV"
+ assert len(events) == 4
+ assert events[-1].data[ATTR_VALUE] is None
+
+ events = []
+
+ def listener(event):
+ events.append(event)
+
+ hass.bus.async_listen(EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, listener)
+
+ acc.char_remote_key.client_update_value(20)
+ await hass.async_block_till_done()
+
+ acc.char_remote_key.client_update_value(7)
+ await hass.async_block_till_done()
+
+ assert len(events) == 1
+ assert events[0].data[ATTR_KEY_NAME] == KEY_ARROW_RIGHT
diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py
index d6bf74bb7cfa17..19b8b5720e2110 100644
--- a/tests/components/homekit/test_type_security_systems.py
+++ b/tests/components/homekit/test_type_security_systems.py
@@ -34,7 +34,7 @@ async def test_switch_set_state(hass, hk_driver, events):
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
acc = SecuritySystem(hass, hk_driver, "SecuritySystem", entity_id, 2, config)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -238,7 +238,7 @@ async def test_supported_states(hass, hk_driver, events):
await hass.async_block_till_done()
acc = SecuritySystem(hass, hk_driver, "SecuritySystem", entity_id, 2, config)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
valid_current_values = acc.char_current_state.properties.get("ValidValues")
diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py
index 7ee79352d7b5c9..c7c06dcc90a189 100644
--- a/tests/components/homekit/test_type_sensors.py
+++ b/tests/components/homekit/test_type_sensors.py
@@ -30,7 +30,7 @@
TEMP_FAHRENHEIT,
)
from homeassistant.core import CoreState
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
async def test_temperature(hass, hk_driver):
@@ -40,7 +40,7 @@ async def test_temperature(hass, hk_driver):
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
acc = TemperatureSensor(hass, hk_driver, "Temperature", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -74,7 +74,7 @@ async def test_humidity(hass, hk_driver):
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
acc = HumiditySensor(hass, hk_driver, "Humidity", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -98,7 +98,7 @@ async def test_air_quality(hass, hk_driver):
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
acc = AirQualitySensor(hass, hk_driver, "Air Quality", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -130,7 +130,7 @@ async def test_co(hass, hk_driver):
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
acc = CarbonMonoxideSensor(hass, hk_driver, "CO", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -170,7 +170,7 @@ async def test_co2(hass, hk_driver):
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
acc = CarbonDioxideSensor(hass, hk_driver, "CO2", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -210,7 +210,7 @@ async def test_light(hass, hk_driver):
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
acc = LightSensor(hass, hk_driver, "Light", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -235,7 +235,7 @@ async def test_binary(hass, hk_driver):
await hass.async_block_till_done()
acc = BinarySensor(hass, hk_driver, "Window Opening", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -274,7 +274,7 @@ async def test_motion_uses_bool(hass, hk_driver):
await hass.async_block_till_done()
acc = BinarySensor(hass, hk_driver, "Motion Sensor", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -326,7 +326,7 @@ async def test_sensor_restore(hass, hk_driver, events):
"""Test setting up an entity from state in the event registry."""
hass.state = CoreState.not_running
- registry = await entity_registry.async_get_registry(hass)
+ registry = er.async_get(hass)
registry.async_get_or_create(
"sensor",
diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py
index 5d218a6ef8a5b2..2ce0acfc8bcd32 100644
--- a/tests/components/homekit/test_type_switches.py
+++ b/tests/components/homekit/test_type_switches.py
@@ -42,7 +42,7 @@ async def test_outlet_set_state(hass, hk_driver, events):
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
acc = Outlet(hass, hk_driver, "Outlet", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -95,7 +95,7 @@ async def test_switch_set_state(hass, hk_driver, entity_id, attrs, events):
hass.states.async_set(entity_id, None, attrs)
await hass.async_block_till_done()
acc = Switch(hass, hk_driver, "Switch", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -139,25 +139,25 @@ async def test_valve_set_state(hass, hk_driver, events):
await hass.async_block_till_done()
acc = Valve(hass, hk_driver, "Valve", entity_id, 2, {CONF_TYPE: TYPE_FAUCET})
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.category == 29 # Faucet
assert acc.char_valve_type.value == 3 # Water faucet
acc = Valve(hass, hk_driver, "Valve", entity_id, 2, {CONF_TYPE: TYPE_SHOWER})
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.category == 30 # Shower
assert acc.char_valve_type.value == 2 # Shower head
acc = Valve(hass, hk_driver, "Valve", entity_id, 2, {CONF_TYPE: TYPE_SPRINKLER})
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.category == 28 # Sprinkler
assert acc.char_valve_type.value == 1 # Irrigation
acc = Valve(hass, hk_driver, "Valve", entity_id, 2, {CONF_TYPE: TYPE_VALVE})
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -210,7 +210,7 @@ async def test_vacuum_set_state_with_returnhome_and_start_support(
await hass.async_block_till_done()
acc = Vacuum(hass, hk_driver, "Vacuum", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
assert acc.category == 8 # Switch
@@ -266,7 +266,7 @@ async def test_vacuum_set_state_without_returnhome_and_start_support(
await hass.async_block_till_done()
acc = Vacuum(hass, hk_driver, "Vacuum", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
assert acc.category == 8 # Switch
@@ -310,7 +310,7 @@ async def test_reset_switch(hass, hk_driver, events):
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
acc = Switch(hass, hk_driver, "Switch", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.activate_only is True
@@ -347,7 +347,7 @@ async def test_reset_switch_reload(hass, hk_driver, events):
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
acc = Switch(hass, hk_driver, "Switch", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.activate_only is False
diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py
index ce17cf7ea07e05..d9c2a6bf0ed908 100644
--- a/tests/components/homekit/test_type_thermostats.py
+++ b/tests/components/homekit/test_type_thermostats.py
@@ -61,7 +61,7 @@
TEMP_FAHRENHEIT,
)
from homeassistant.core import CoreState
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from tests.common import async_mock_service
@@ -89,7 +89,7 @@ async def test_thermostat(hass, hk_driver, events):
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 1
@@ -431,7 +431,7 @@ async def test_thermostat_auto(hass, hk_driver, events):
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_cooling_thresh_temp.value == 23.0
@@ -570,7 +570,7 @@ async def test_thermostat_humidity(hass, hk_driver, events):
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_target_humidity.value == 50
@@ -645,7 +645,7 @@ async def test_thermostat_power_state(hass, hk_driver, events):
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_current_heat_cool.value == 1
@@ -756,7 +756,7 @@ async def test_thermostat_fahrenheit(hass, hk_driver, events):
with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT):
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
hass.states.async_set(
@@ -879,7 +879,7 @@ async def test_thermostat_temperature_step_whole(hass, hk_driver):
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_target_temp.properties[PROP_MIN_STEP] == 0.1
@@ -889,7 +889,7 @@ async def test_thermostat_restore(hass, hk_driver, events):
"""Test setting up an entity from state in the event registry."""
hass.state = CoreState.not_running
- registry = await entity_registry.async_get_registry(hass)
+ registry = er.async_get(hass)
registry.async_get_or_create(
"climate", "generic", "1234", suggested_object_id="simple"
@@ -942,7 +942,7 @@ async def test_thermostat_hvac_modes(hass, hk_driver):
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP()
assert hap["valid-values"] == [0, 1]
@@ -985,7 +985,7 @@ async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver):
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP()
assert hap["valid-values"] == [0, 1, 3]
@@ -1041,7 +1041,7 @@ async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver):
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP()
assert hap["valid-values"] == [0, 1, 3]
@@ -1095,7 +1095,7 @@ async def test_thermostat_hvac_modes_with_auto_only(hass, hk_driver):
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP()
assert hap["valid-values"] == [0, 3]
@@ -1149,7 +1149,7 @@ async def test_thermostat_hvac_modes_with_heat_only(hass, hk_driver):
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP()
assert hap["valid-values"] == [HC_HEAT_COOL_OFF, HC_HEAT_COOL_HEAT]
@@ -1209,7 +1209,7 @@ async def test_thermostat_hvac_modes_with_cool_only(hass, hk_driver):
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP()
assert hap["valid-values"] == [HC_HEAT_COOL_OFF, HC_HEAT_COOL_COOL]
@@ -1273,7 +1273,7 @@ async def test_thermostat_hvac_modes_with_heat_cool_only(hass, hk_driver):
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP()
assert hap["valid-values"] == [
@@ -1362,7 +1362,7 @@ async def test_thermostat_hvac_modes_without_off(hass, hk_driver):
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP()
assert hap["valid-values"] == [1, 3]
@@ -1401,7 +1401,7 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, events
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_cooling_thresh_temp.value == 23.0
@@ -1576,7 +1576,7 @@ async def test_water_heater(hass, hk_driver, events):
hass.states.async_set(entity_id, HVAC_MODE_HEAT)
await hass.async_block_till_done()
acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
@@ -1599,11 +1599,15 @@ async def test_water_heater(hass, hk_driver, events):
hass.states.async_set(
entity_id,
HVAC_MODE_HEAT,
- {ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 56.0},
+ {
+ ATTR_HVAC_MODE: HVAC_MODE_HEAT,
+ ATTR_TEMPERATURE: 56.0,
+ ATTR_CURRENT_TEMPERATURE: 35.0,
+ },
)
await hass.async_block_till_done()
assert acc.char_target_temp.value == 56.0
- assert acc.char_current_temp.value == 50.0
+ assert acc.char_current_temp.value == 35.0
assert acc.char_target_heat_cool.value == 1
assert acc.char_current_heat_cool.value == 1
assert acc.char_display_units.value == 0
@@ -1651,7 +1655,7 @@ async def test_water_heater_fahrenheit(hass, hk_driver, events):
await hass.async_block_till_done()
with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT):
acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
hass.states.async_set(entity_id, HVAC_MODE_HEAT, {ATTR_TEMPERATURE: 131})
@@ -1701,7 +1705,7 @@ async def test_water_heater_restore(hass, hk_driver, events):
"""Test setting up an entity from state in the event registry."""
hass.state = CoreState.not_running
- registry = await entity_registry.async_get_registry(hass)
+ registry = er.async_get(hass)
registry.async_get_or_create(
"water_heater", "generic", "1234", suggested_object_id="simple"
@@ -1758,7 +1762,7 @@ async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, event
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_cooling_thresh_temp.value == 23.0
@@ -1811,7 +1815,7 @@ async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, events):
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_cooling_thresh_temp.value == 23.0
@@ -1865,7 +1869,7 @@ async def test_thermostat_with_temp_clamps(hass, hk_driver, events):
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
- await acc.run_handler()
+ await acc.run()
await hass.async_block_till_done()
assert acc.char_cooling_thresh_temp.value == 100
diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py
index e0f10a94d69604..33c5c8623d1ec5 100644
--- a/tests/components/homekit/test_util.py
+++ b/tests/components/homekit/test_util.py
@@ -1,8 +1,11 @@
"""Test HomeKit util module."""
+from unittest.mock import Mock
+
import pytest
import voluptuous as vol
from homeassistant.components.homekit.const import (
+ BRIDGE_NAME,
CONF_FEATURE,
CONF_FEATURE_LIST,
CONF_LINKED_BATTERY_SENSOR,
@@ -21,14 +24,16 @@
TYPE_VALVE,
)
from homeassistant.components.homekit.util import (
+ accessory_friendly_name,
+ async_find_next_available_port,
cleanup_name_for_homekit,
convert_to_float,
density_to_air_quality,
dismiss_setup_message,
- find_next_available_port,
format_sw_version,
port_is_available,
show_setup_message,
+ state_needs_accessory_mode,
temperature_to_homekit,
temperature_to_states,
validate_entity_config as vec,
@@ -43,6 +48,7 @@
ATTR_CODE,
ATTR_SUPPORTED_FEATURES,
CONF_NAME,
+ CONF_PORT,
CONF_TYPE,
STATE_UNKNOWN,
TEMP_CELSIUS,
@@ -52,7 +58,7 @@
from .util import async_init_integration
-from tests.common import async_mock_service
+from tests.common import MockConfigEntry, async_mock_service
def test_validate_entity_config():
@@ -251,10 +257,26 @@ async def test_dismiss_setup_msg(hass):
async def test_port_is_available(hass):
"""Test we can get an available port and it is actually available."""
- next_port = await hass.async_add_executor_job(
- find_next_available_port, DEFAULT_CONFIG_FLOW_PORT
+ next_port = await async_find_next_available_port(hass, DEFAULT_CONFIG_FLOW_PORT)
+
+ assert next_port
+
+ assert await hass.async_add_executor_job(port_is_available, next_port)
+
+
+async def test_port_is_available_skips_existing_entries(hass):
+ """Test we can get an available port and it is actually available."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_CONFIG_FLOW_PORT},
+ options={},
)
+ entry.add_to_hass(hass)
+
+ next_port = await async_find_next_available_port(hass, DEFAULT_CONFIG_FLOW_PORT)
+
assert next_port
+ assert next_port != DEFAULT_CONFIG_FLOW_PORT
assert await hass.async_add_executor_job(port_is_available, next_port)
@@ -266,3 +288,20 @@ async def test_format_sw_version():
assert format_sw_version("56.0-76060") == "56.0.76060"
assert format_sw_version(3.6) == "3.6"
assert format_sw_version("unknown") is None
+
+
+async def test_accessory_friendly_name():
+ """Test we provide a helpful friendly name."""
+
+ accessory = Mock()
+ accessory.display_name = "same"
+ assert accessory_friendly_name("Same", accessory) == "Same"
+ assert accessory_friendly_name("hass title", accessory) == "hass title (same)"
+ accessory.display_name = "Hass title 123"
+ assert accessory_friendly_name("hass title", accessory) == "Hass title 123"
+
+
+async def test_lock_state_needs_accessory_mode(hass):
+ """Test that locks are setup as accessories."""
+ hass.states.async_set("lock.mine", "locked")
+ assert state_needs_accessory_mode(hass.states.get("lock.mine")) is True
diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py
index 26adb25df21b76..3cde3912709090 100644
--- a/tests/components/homekit_controller/conftest.py
+++ b/tests/components/homekit_controller/conftest.py
@@ -8,7 +8,14 @@
import homeassistant.util.dt as dt_util
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
+
+
+@pytest.fixture(autouse=True)
+def mock_zeroconf():
+ """Mock zeroconf."""
+ with mock.patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc:
+ yield mock_zc.return_value
@pytest.fixture
diff --git a/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py b/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py
index 45e0466ceeaaed..15ac6d2d3ab485 100644
--- a/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py
+++ b/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py
@@ -1,4 +1,5 @@
"""Test against characteristics captured from a eufycam."""
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.components.homekit_controller.common import (
Helper,
@@ -12,7 +13,7 @@ async def test_eufycam_setup(hass):
accessories = await setup_accessories_from_file(hass, "anker_eufycam.json")
config_entry, pairing = await setup_test_accessories(hass, accessories)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
# Check that the camera is correctly found and set up
camera_id = "camera.eufycam2_0000"
@@ -32,7 +33,7 @@ async def test_eufycam_setup(hass):
assert camera_state.state == "idle"
assert camera_state.attributes["supported_features"] == 0
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(camera.device_id)
assert device.manufacturer == "Anker"
diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py
index 8bee6e0591d9ec..b4437a7a9b5c06 100644
--- a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py
+++ b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py
@@ -5,6 +5,7 @@
"""
from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_COLOR
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.components.homekit_controller.common import (
Helper,
@@ -18,7 +19,7 @@ async def test_aqara_gateway_setup(hass):
accessories = await setup_accessories_from_file(hass, "aqara_gateway.json")
config_entry, pairing = await setup_test_accessories(hass, accessories)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
# Check that the light is correctly found and set up
alarm_id = "alarm_control_panel.aqara_hub_1563"
@@ -48,7 +49,7 @@ async def test_aqara_gateway_setup(hass):
SUPPORT_BRIGHTNESS | SUPPORT_COLOR
)
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
# All the entities are services of the same accessory
# So it looks at the protocol like a single physical device
diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py
index a9744fb7bfc68e..6ed0d861193ee8 100644
--- a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py
+++ b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py
@@ -7,6 +7,8 @@
https://github.com/home-assistant/core/pull/39090
"""
+from homeassistant.helpers import entity_registry as er
+
from tests.common import assert_lists_same, async_get_device_automations
from tests.components.homekit_controller.common import (
setup_accessories_from_file,
@@ -19,7 +21,7 @@ async def test_aqara_switch_setup(hass):
accessories = await setup_accessories_from_file(hass, "aqara_switch.json")
config_entry, pairing = await setup_test_accessories(hass, accessories)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
battery_id = "sensor.programmable_switch_battery"
battery = entity_registry.async_get(battery_id)
diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py
index d05e36ed0ebac8..ae050f673242c2 100644
--- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py
+++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py
@@ -15,6 +15,7 @@
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.components.homekit_controller.common import (
Helper,
@@ -30,7 +31,7 @@ async def test_ecobee3_setup(hass):
accessories = await setup_accessories_from_file(hass, "ecobee3.json")
config_entry, pairing = await setup_test_accessories(hass, accessories)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
climate = entity_registry.async_get("climate.homew")
assert climate.unique_id == "homekit-123456789012-16"
@@ -73,7 +74,7 @@ async def test_ecobee3_setup(hass):
occ3 = entity_registry.async_get("binary_sensor.basement")
assert occ3.unique_id == "homekit-AB3C-56"
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
climate_device = device_registry.async_get(climate.device_id)
assert climate_device.manufacturer == "ecobee Inc."
@@ -112,7 +113,7 @@ async def test_ecobee3_setup_from_cache(hass, hass_storage):
await setup_test_accessories(hass, accessories)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
climate = entity_registry.async_get("climate.homew")
assert climate.unique_id == "homekit-123456789012-16"
@@ -131,7 +132,7 @@ async def test_ecobee3_setup_connection_failure(hass):
"""Test that Ecbobee can be correctly setup from its cached entity map."""
accessories = await setup_accessories_from_file(hass, "ecobee3.json")
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
# Test that the connection fails during initial setup.
# No entities should be created.
@@ -170,7 +171,7 @@ async def test_ecobee3_setup_connection_failure(hass):
async def test_ecobee3_add_sensors_at_runtime(hass):
"""Test that new sensors are automatically added."""
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
# Set up a base Ecobee 3 with no additional sensors.
# There shouldn't be any entities but climate visible.
diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py b/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py
index 6823cdc16ead81..63f5d22e04b2f5 100644
--- a/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py
+++ b/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py
@@ -4,6 +4,8 @@
https://github.com/home-assistant/core/issues/31827
"""
+from homeassistant.helpers import device_registry as dr, entity_registry as er
+
from tests.components.homekit_controller.common import (
Helper,
setup_accessories_from_file,
@@ -16,7 +18,7 @@ async def test_ecobee_occupancy_setup(hass):
accessories = await setup_accessories_from_file(hass, "ecobee_occupancy.json")
config_entry, pairing = await setup_test_accessories(hass, accessories)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
sensor = entity_registry.async_get("binary_sensor.master_fan")
assert sensor.unique_id == "homekit-111111111111-56"
@@ -27,7 +29,7 @@ async def test_ecobee_occupancy_setup(hass):
sensor_state = await sensor_helper.poll_and_get_state()
assert sensor_state.attributes["friendly_name"] == "Master Fan"
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(sensor.device_id)
assert device.manufacturer == "ecobee Inc."
diff --git a/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py b/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py
index fbdf00698f7855..1cbfa23b64cfa0 100644
--- a/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py
+++ b/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py
@@ -5,6 +5,7 @@
SUPPORT_OSCILLATE,
SUPPORT_SET_SPEED,
)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.components.homekit_controller.common import (
Helper,
@@ -20,7 +21,7 @@ async def test_homeassistant_bridge_fan_setup(hass):
)
config_entry, pairing = await setup_test_accessories(hass, accessories)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
# Check that the fan is correctly found and set up
fan_id = "fan.living_room_fan"
@@ -42,7 +43,7 @@ async def test_homeassistant_bridge_fan_setup(hass):
SUPPORT_DIRECTION | SUPPORT_SET_SPEED | SUPPORT_OSCILLATE
)
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(fan.device_id)
assert device.manufacturer == "Home Assistant"
diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py
index 168ae85b228d77..0452407bfb8ed2 100644
--- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py
+++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py
@@ -1,5 +1,7 @@
"""Tests for handling accessories on a Hue bridge via HomeKit."""
+from homeassistant.helpers import device_registry as dr, entity_registry as er
+
from tests.common import assert_lists_same, async_get_device_automations
from tests.components.homekit_controller.common import (
Helper,
@@ -13,7 +15,7 @@ async def test_hue_bridge_setup(hass):
accessories = await setup_accessories_from_file(hass, "hue_bridge.json")
config_entry, pairing = await setup_test_accessories(hass, accessories)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
# Check that the battery is correctly found and set up
battery_id = "sensor.hue_dimmer_switch_battery"
@@ -28,7 +30,7 @@ async def test_hue_bridge_setup(hass):
assert battery_state.attributes["icon"] == "mdi:battery"
assert battery_state.state == "100"
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(battery.device_id)
assert device.manufacturer == "Philips"
diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py
index 4682d4b2bcce84..505ff2aacc7f8a 100644
--- a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py
+++ b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py
@@ -8,6 +8,7 @@
import pytest
from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_COLOR
+from homeassistant.helpers import device_registry as dr, entity_registry as er
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed
@@ -25,7 +26,7 @@ async def test_koogeek_ls1_setup(hass):
accessories = await setup_accessories_from_file(hass, "koogeek_ls1.json")
config_entry, pairing = await setup_test_accessories(hass, accessories)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
# Assert that the entity is correctly added to the entity registry
entry = entity_registry.async_get("light.koogeek_ls1_20833f")
@@ -44,7 +45,7 @@ async def test_koogeek_ls1_setup(hass):
SUPPORT_BRIGHTNESS | SUPPORT_COLOR
)
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.manufacturer == "Koogeek"
diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py
index f97821ef111831..db72aad754174b 100644
--- a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py
+++ b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py
@@ -1,5 +1,7 @@
"""Make sure that existing Koogeek P1EU support isn't broken."""
+from homeassistant.helpers import device_registry as dr, entity_registry as er
+
from tests.components.homekit_controller.common import (
Helper,
setup_accessories_from_file,
@@ -12,8 +14,8 @@ async def test_koogeek_p1eu_setup(hass):
accessories = await setup_accessories_from_file(hass, "koogeek_p1eu.json")
config_entry, pairing = await setup_test_accessories(hass, accessories)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
+ device_registry = dr.async_get(hass)
# Check that the switch entity is handled correctly
diff --git a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py
index a49effdb75dcfe..cdab08039e1c75 100644
--- a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py
+++ b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py
@@ -8,6 +8,7 @@
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.components.homekit_controller.common import (
Helper,
@@ -21,7 +22,7 @@ async def test_lennox_e30_setup(hass):
accessories = await setup_accessories_from_file(hass, "lennox_e30.json")
config_entry, pairing = await setup_test_accessories(hass, accessories)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
climate = entity_registry.async_get("climate.lennox")
assert climate.unique_id == "homekit-XXXXXXXX-100"
@@ -35,7 +36,7 @@ async def test_lennox_e30_setup(hass):
SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE
)
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(climate.device_id)
assert device.manufacturer == "Lennox"
diff --git a/tests/components/homekit_controller/specific_devices/test_lg_tv.py b/tests/components/homekit_controller/specific_devices/test_lg_tv.py
index cd3f57137bfd3b..7f7ada4ac1fb2c 100644
--- a/tests/components/homekit_controller/specific_devices/test_lg_tv.py
+++ b/tests/components/homekit_controller/specific_devices/test_lg_tv.py
@@ -5,6 +5,7 @@
SUPPORT_PLAY,
SUPPORT_SELECT_SOURCE,
)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.common import async_get_device_automations
from tests.components.homekit_controller.common import (
@@ -19,7 +20,7 @@ async def test_lg_tv(hass):
accessories = await setup_accessories_from_file(hass, "lg_tv.json")
config_entry, pairing = await setup_test_accessories(hass, accessories)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
# Assert that the entity is correctly added to the entity registry
entry = entity_registry.async_get("media_player.lg_webos_tv_af80")
@@ -54,7 +55,7 @@ async def test_lg_tv(hass):
# CURRENT_MEDIA_STATE. Therefore "ok" is the best we can say.
assert state.state == "ok"
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.manufacturer == "LG Electronics"
@@ -63,6 +64,7 @@ async def test_lg_tv(hass):
assert device.sw_version == "04.71.04"
assert device.via_device_id is None
- # A TV doesn't have any triggers
+ # A TV has media player device triggers
triggers = await async_get_device_automations(hass, "trigger", device.id)
- assert triggers == []
+ for trigger in triggers:
+ assert trigger["domain"] == "media_player"
diff --git a/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py b/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py
index fd95ef98c097fd..81e31918c912d7 100644
--- a/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py
+++ b/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py
@@ -4,6 +4,8 @@
https://github.com/home-assistant/core/issues/31745
"""
+from homeassistant.helpers import device_registry as dr, entity_registry as er
+
from tests.components.homekit_controller.common import (
Helper,
setup_accessories_from_file,
@@ -16,7 +18,7 @@ async def test_rainmachine_pro_8_setup(hass):
accessories = await setup_accessories_from_file(hass, "rainmachine-pro-8.json")
config_entry, pairing = await setup_test_accessories(hass, accessories)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
# Assert that the entity is correctly added to the entity registry
entry = entity_registry.async_get("switch.rainmachine_00ce4a")
@@ -30,7 +32,7 @@ async def test_rainmachine_pro_8_setup(hass):
# Assert that the friendly name is detected correctly
assert state.attributes["friendly_name"] == "RainMachine-00ce4a"
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.manufacturer == "Green Electronics LLC"
diff --git a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py
index b30dd1e9c89d65..a21953202938b7 100644
--- a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py
+++ b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py
@@ -5,6 +5,7 @@
"""
from homeassistant.components.fan import SUPPORT_DIRECTION, SUPPORT_SET_SPEED
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.components.homekit_controller.common import (
Helper,
@@ -18,7 +19,7 @@ async def test_simpleconnect_fan_setup(hass):
accessories = await setup_accessories_from_file(hass, "simpleconnect_fan.json")
config_entry, pairing = await setup_test_accessories(hass, accessories)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
# Check that the fan is correctly found and set up
fan_id = "fan.simpleconnect_fan_06f674"
@@ -40,7 +41,7 @@ async def test_simpleconnect_fan_setup(hass):
SUPPORT_DIRECTION | SUPPORT_SET_SPEED
)
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(fan.device_id)
assert device.manufacturer == "Hunter Fan"
diff --git a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py
index 033b4aa7b4db95..b93afdfbfa4ecd 100644
--- a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py
+++ b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py
@@ -9,6 +9,7 @@
SUPPORT_OPEN,
SUPPORT_SET_POSITION,
)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.components.homekit_controller.common import (
Helper,
@@ -22,7 +23,7 @@ async def test_simpleconnect_cover_setup(hass):
accessories = await setup_accessories_from_file(hass, "velux_gateway.json")
config_entry, pairing = await setup_test_accessories(hass, accessories)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
# Check that the cover is correctly found and set up
cover_id = "cover.velux_window"
@@ -64,7 +65,7 @@ async def test_simpleconnect_cover_setup(hass):
# The cover and sensor are different devices (accessories) attached to the same bridge
assert cover.device_id != sensor.device_id
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(cover.device_id)
assert device.manufacturer == "VELUX"
diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py
index 9cc785f85fb230..42903a530629c5 100644
--- a/tests/components/homekit_controller/test_config_flow.py
+++ b/tests/components/homekit_controller/test_config_flow.py
@@ -269,25 +269,18 @@ async def test_discovery_ignored_model(hass, controller):
async def test_discovery_ignored_hk_bridge(hass, controller):
- """Already paired."""
+ """Ensure we ignore homekit bridges and accessories created by the homekit integration."""
device = setup_mock_accessory(controller)
discovery_info = get_device_discovery_info(device)
config_entry = MockConfigEntry(domain=config_flow.HOMEKIT_BRIDGE_DOMAIN, data={})
+ config_entry.add_to_hass(hass)
formatted_mac = device_registry.format_mac("AA:BB:CC:DD:EE:FF")
dev_reg = mock_device_registry(hass)
dev_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
- identifiers={
- (
- config_flow.HOMEKIT_BRIDGE_DOMAIN,
- config_entry.entry_id,
- config_flow.HOMEKIT_BRIDGE_SERIAL_NUMBER,
- )
- },
connections={(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)},
- model=config_flow.HOMEKIT_BRIDGE_MODEL,
)
discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF"
@@ -300,6 +293,30 @@ async def test_discovery_ignored_hk_bridge(hass, controller):
assert result["reason"] == "ignored_model"
+async def test_discovery_does_not_ignore_non_homekit(hass, controller):
+ """Do not ignore devices that are not from the homekit integration."""
+ device = setup_mock_accessory(controller)
+ discovery_info = get_device_discovery_info(device)
+
+ config_entry = MockConfigEntry(domain="not_homekit", data={})
+ config_entry.add_to_hass(hass)
+ formatted_mac = device_registry.format_mac("AA:BB:CC:DD:EE:FF")
+
+ dev_reg = mock_device_registry(hass)
+ dev_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)},
+ )
+
+ discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF"
+
+ # Device is discovered
+ result = await hass.config_entries.flow.async_init(
+ "homekit_controller", context={"source": "zeroconf"}, data=discovery_info
+ )
+ assert result["type"] == "form"
+
+
async def test_discovery_invalid_config_entry(hass, controller):
"""There is already a config entry for the pairing id but it's invalid."""
MockConfigEntry(
diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py
index 9de9a30f99f71a..7c02c1a6456eb1 100644
--- a/tests/components/homekit_controller/test_device_trigger.py
+++ b/tests/components/homekit_controller/test_device_trigger.py
@@ -5,6 +5,7 @@
import homeassistant.components.automation as automation
from homeassistant.components.homekit_controller.const import DOMAIN
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from tests.common import (
@@ -12,7 +13,7 @@
async_get_device_automations,
async_mock_service,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
from tests.components.homekit_controller.common import setup_test_component
@@ -82,10 +83,10 @@ async def test_enumerate_remote(hass, utcnow):
"""Test that remote is correctly enumerated."""
await setup_test_component(hass, create_remote)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry = entity_registry.async_get("sensor.testdevice_battery")
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
expected = [
@@ -118,10 +119,10 @@ async def test_enumerate_button(hass, utcnow):
"""Test that a button is correctly enumerated."""
await setup_test_component(hass, create_button)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry = entity_registry.async_get("sensor.testdevice_battery")
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
expected = [
@@ -153,10 +154,10 @@ async def test_enumerate_doorbell(hass, utcnow):
"""Test that a button is correctly enumerated."""
await setup_test_component(hass, create_doorbell)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry = entity_registry.async_get("sensor.testdevice_battery")
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
expected = [
@@ -188,10 +189,10 @@ async def test_handle_events(hass, utcnow, calls):
"""Test that events are handled."""
helper = await setup_test_component(hass, create_remote)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry = entity_registry.async_get("sensor.testdevice_battery")
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert await async_setup_component(
@@ -213,7 +214,8 @@ async def test_handle_events(hass, utcnow, calls):
"data_template": {
"some": (
"{{ trigger.platform}} - "
- "{{ trigger.type }} - {{ trigger.subtype }}"
+ "{{ trigger.type }} - {{ trigger.subtype }} - "
+ "{{ trigger.id }}"
)
},
},
@@ -232,7 +234,8 @@ async def test_handle_events(hass, utcnow, calls):
"data_template": {
"some": (
"{{ trigger.platform}} - "
- "{{ trigger.type }} - {{ trigger.subtype }}"
+ "{{ trigger.type }} - {{ trigger.subtype }} - "
+ "{{ trigger.id }}"
)
},
},
@@ -248,7 +251,7 @@ async def test_handle_events(hass, utcnow, calls):
await hass.async_block_till_done()
assert len(calls) == 1
- assert calls[0].data["some"] == "device - button1 - single_press"
+ assert calls[0].data["some"] == "device - button1 - single_press - 0"
# Make sure automation doesn't trigger for long press
helper.pairing.testing.update_named_service(
@@ -273,7 +276,7 @@ async def test_handle_events(hass, utcnow, calls):
await hass.async_block_till_done()
assert len(calls) == 2
- assert calls[1].data["some"] == "device - button2 - long_press"
+ assert calls[1].data["some"] == "device - button2 - long_press - 0"
# Turn the automations off
await hass.services.async_call(
diff --git a/tests/components/homekit_controller/test_fan.py b/tests/components/homekit_controller/test_fan.py
index b8d42b21643f89..d66ce81d5349f7 100644
--- a/tests/components/homekit_controller/test_fan.py
+++ b/tests/components/homekit_controller/test_fan.py
@@ -50,6 +50,38 @@ def create_fanv2_service(accessory):
swing_mode.value = 0
+def create_fanv2_service_with_min_step(accessory):
+ """Define fan v2 characteristics as per HAP spec."""
+ service = accessory.add_service(ServicesTypes.FAN_V2)
+
+ cur_state = service.add_char(CharacteristicsTypes.ACTIVE)
+ cur_state.value = 0
+
+ direction = service.add_char(CharacteristicsTypes.ROTATION_DIRECTION)
+ direction.value = 0
+
+ speed = service.add_char(CharacteristicsTypes.ROTATION_SPEED)
+ speed.value = 0
+ speed.minStep = 25
+
+ swing_mode = service.add_char(CharacteristicsTypes.SWING_MODE)
+ swing_mode.value = 0
+
+
+def create_fanv2_service_without_rotation_speed(accessory):
+ """Define fan v2 characteristics as per HAP spec."""
+ service = accessory.add_service(ServicesTypes.FAN_V2)
+
+ cur_state = service.add_char(CharacteristicsTypes.ACTIVE)
+ cur_state.value = 0
+
+ direction = service.add_char(CharacteristicsTypes.ROTATION_DIRECTION)
+ direction.value = 0
+
+ swing_mode = service.add_char(CharacteristicsTypes.SWING_MODE)
+ swing_mode.value = 0
+
+
async def test_fan_read_state(hass, utcnow):
"""Test that we can read the state of a HomeKit fan accessory."""
helper = await setup_test_component(hass, create_fan_service)
@@ -95,6 +127,29 @@ async def test_turn_on(hass, utcnow):
assert helper.characteristics[V1_ROTATION_SPEED].value == 33.0
+async def test_turn_on_off_without_rotation_speed(hass, utcnow):
+ """Test that we can turn a fan on."""
+ helper = await setup_test_component(
+ hass, create_fanv2_service_without_rotation_speed
+ )
+
+ await hass.services.async_call(
+ "fan",
+ "turn_on",
+ {"entity_id": "fan.testdevice"},
+ blocking=True,
+ )
+ assert helper.characteristics[V2_ACTIVE].value == 1
+
+ await hass.services.async_call(
+ "fan",
+ "turn_off",
+ {"entity_id": "fan.testdevice"},
+ blocking=True,
+ )
+ assert helper.characteristics[V2_ACTIVE].value == 0
+
+
async def test_turn_off(hass, utcnow):
"""Test that we can turn a fan off."""
helper = await setup_test_component(hass, create_fan_service)
@@ -181,6 +236,7 @@ async def test_speed_read(hass, utcnow):
state = await helper.poll_and_get_state()
assert state.attributes["speed"] == "high"
assert state.attributes["percentage"] == 100
+ assert state.attributes["percentage_step"] == 1.0
helper.characteristics[V1_ROTATION_SPEED].value = 50
state = await helper.poll_and_get_state()
@@ -277,6 +333,24 @@ async def test_v2_turn_on(hass, utcnow):
assert helper.characteristics[V2_ACTIVE].value == 1
assert helper.characteristics[V2_ROTATION_SPEED].value == 33.0
+ await hass.services.async_call(
+ "fan",
+ "turn_off",
+ {"entity_id": "fan.testdevice"},
+ blocking=True,
+ )
+ assert helper.characteristics[V2_ACTIVE].value == 0
+ assert helper.characteristics[V2_ROTATION_SPEED].value == 33.0
+
+ await hass.services.async_call(
+ "fan",
+ "turn_on",
+ {"entity_id": "fan.testdevice"},
+ blocking=True,
+ )
+ assert helper.characteristics[V2_ACTIVE].value == 1
+ assert helper.characteristics[V2_ROTATION_SPEED].value == 33.0
+
async def test_v2_turn_off(hass, utcnow):
"""Test that we can turn a fan off."""
@@ -355,6 +429,29 @@ async def test_v2_set_percentage(hass, utcnow):
assert helper.characteristics[V2_ACTIVE].value == 0
+async def test_v2_set_percentage_with_min_step(hass, utcnow):
+ """Test that we set fan speed by percentage."""
+ helper = await setup_test_component(hass, create_fanv2_service_with_min_step)
+
+ helper.characteristics[V2_ACTIVE].value = 1
+
+ await hass.services.async_call(
+ "fan",
+ "set_percentage",
+ {"entity_id": "fan.testdevice", "percentage": 66},
+ blocking=True,
+ )
+ assert helper.characteristics[V2_ROTATION_SPEED].value == 75
+
+ await hass.services.async_call(
+ "fan",
+ "set_percentage",
+ {"entity_id": "fan.testdevice", "percentage": 0},
+ blocking=True,
+ )
+ assert helper.characteristics[V2_ACTIVE].value == 0
+
+
async def test_v2_speed_read(hass, utcnow):
"""Test that we can read a fans oscillation."""
helper = await setup_test_component(hass, create_fanv2_service)
diff --git a/tests/components/homekit_controller/test_light.py b/tests/components/homekit_controller/test_light.py
index e443e36b910719..f4950512063332 100644
--- a/tests/components/homekit_controller/test_light.py
+++ b/tests/components/homekit_controller/test_light.py
@@ -3,6 +3,7 @@
from aiohomekit.model.services import ServicesTypes
from homeassistant.components.homekit_controller.const import KNOWN_DEVICES
+from homeassistant.const import STATE_UNAVAILABLE
from tests.components.homekit_controller.common import setup_test_component
@@ -209,8 +210,8 @@ async def test_light_becomes_unavailable_but_recovers(hass, utcnow):
assert state.attributes["color_temp"] == 400
-async def test_light_unloaded(hass, utcnow):
- """Test entity and HKDevice are correctly unloaded."""
+async def test_light_unloaded_removed(hass, utcnow):
+ """Test entity and HKDevice are correctly unloaded and removed."""
helper = await setup_test_component(hass, create_lightbulb_service_with_color_temp)
# Initial state is that the light is off
@@ -220,9 +221,15 @@ async def test_light_unloaded(hass, utcnow):
unload_result = await helper.config_entry.async_unload(hass)
assert unload_result is True
- # Make sure entity is unloaded
- assert hass.states.get(helper.entity_id) is None
+ # Make sure entity is set to unavailable state
+ assert hass.states.get(helper.entity_id).state == STATE_UNAVAILABLE
# Make sure HKDevice is no longer set to poll this accessory
conn = hass.data[KNOWN_DEVICES]["00:00:00:00:00:00"]
assert not conn.pollable_characteristics
+
+ await helper.config_entry.async_remove(hass)
+ await hass.async_block_till_done()
+
+ # Make sure entity is removed
+ assert hass.states.get(helper.entity_id).state == STATE_UNAVAILABLE
diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py
index b05683d2361c97..aac7f60558ca0c 100644
--- a/tests/components/homematicip_cloud/conftest.py
+++ b/tests/components/homematicip_cloud/conftest.py
@@ -25,7 +25,7 @@
from .helper import AUTH_TOKEN, HAPID, HAPPIN, HomeFactory
from tests.common import MockConfigEntry
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
@pytest.fixture(name="mock_connection")
diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py
index 264d93c4145411..6f1d3071714d95 100644
--- a/tests/components/homematicip_cloud/test_device.py
+++ b/tests/components/homematicip_cloud/test_device.py
@@ -41,8 +41,8 @@ async def test_hmip_remove_device(hass, default_mock_hap_factory):
assert ha_state.state == STATE_ON
assert hmip_device
- device_registry = await dr.async_get_registry(hass)
- entity_registry = await er.async_get_registry(hass)
+ device_registry = dr.async_get(hass)
+ entity_registry = er.async_get(hass)
pre_device_count = len(device_registry.devices)
pre_entity_count = len(entity_registry.entities)
@@ -73,8 +73,8 @@ async def test_hmip_add_device(hass, default_mock_hap_factory, hmip_config_entry
assert ha_state.state == STATE_ON
assert hmip_device
- device_registry = await dr.async_get_registry(hass)
- entity_registry = await er.async_get_registry(hass)
+ device_registry = dr.async_get(hass)
+ entity_registry = er.async_get(hass)
pre_device_count = len(device_registry.devices)
pre_entity_count = len(entity_registry.entities)
@@ -119,8 +119,8 @@ async def test_hmip_remove_group(hass, default_mock_hap_factory):
assert ha_state.state == STATE_ON
assert hmip_device
- device_registry = await dr.async_get_registry(hass)
- entity_registry = await er.async_get_registry(hass)
+ device_registry = dr.async_get(hass)
+ entity_registry = er.async_get(hass)
pre_device_count = len(device_registry.devices)
pre_entity_count = len(entity_registry.entities)
@@ -257,12 +257,12 @@ async def test_hmip_multi_area_device(hass, default_mock_hap_factory):
assert ha_state
# get the entity
- entity_registry = await er.async_get_registry(hass)
+ entity_registry = er.async_get(hass)
entity = entity_registry.async_get(ha_state.entity_id)
assert entity
# get the device
- device_registry = await dr.async_get_registry(hass)
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entity.device_id)
assert device.name == "Wired Eingangsmodul – 32-fach"
diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py
index 058988203e5d36..d97bbc86ed601a 100644
--- a/tests/components/honeywell/test_climate.py
+++ b/tests/components/honeywell/test_climate.py
@@ -181,7 +181,7 @@ def test_eu_setup_full_config(self, mock_round, mock_evo):
mock.call(mock_evo.return_value, "bar", False, 20.0),
]
)
- assert 2 == add_entities.call_count
+ assert add_entities.call_count == 2
@mock.patch("evohomeclient.EvohomeClient")
@mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat")
@@ -269,15 +269,15 @@ def fake_temperatures(force_refresh=None):
def test_attributes(self):
"""Test the attributes."""
- assert "House" == self.round1.name
- assert TEMP_CELSIUS == self.round1.temperature_unit
- assert 20 == self.round1.current_temperature
- assert 21 == self.round1.target_temperature
+ assert self.round1.name == "House"
+ assert self.round1.temperature_unit == TEMP_CELSIUS
+ assert self.round1.current_temperature == 20
+ assert self.round1.target_temperature == 21
assert not self.round1.is_away_mode_on
- assert "Hot Water" == self.round2.name
- assert TEMP_CELSIUS == self.round2.temperature_unit
- assert 21 == self.round2.current_temperature
+ assert self.round2.name == "Hot Water"
+ assert self.round2.temperature_unit == TEMP_CELSIUS
+ assert self.round2.current_temperature == 21
assert self.round2.target_temperature is None
assert not self.round2.is_away_mode_on
@@ -304,12 +304,12 @@ def test_set_temperature(self):
def test_set_hvac_mode(self) -> None:
"""Test setting the system operation."""
self.round1.set_hvac_mode("cool")
- assert "cool" == self.round1.current_operation
- assert "cool" == self.device.system_mode
+ assert self.round1.current_operation == "cool"
+ assert self.device.system_mode == "cool"
self.round1.set_hvac_mode("heat")
- assert "heat" == self.round1.current_operation
- assert "heat" == self.device.system_mode
+ assert self.round1.current_operation == "heat"
+ assert self.device.system_mode == "heat"
class TestHoneywellUS(unittest.TestCase):
@@ -342,40 +342,40 @@ def setup_method(self, method):
def test_properties(self):
"""Test the properties."""
assert self.honeywell.is_fan_on
- assert "test" == self.honeywell.name
- assert 72 == self.honeywell.current_temperature
+ assert self.honeywell.name == "test"
+ assert self.honeywell.current_temperature == 72
def test_unit_of_measurement(self):
"""Test the unit of measurement."""
- assert TEMP_FAHRENHEIT == self.honeywell.temperature_unit
+ assert self.honeywell.temperature_unit == TEMP_FAHRENHEIT
self.device.temperature_unit = "C"
- assert TEMP_CELSIUS == self.honeywell.temperature_unit
+ assert self.honeywell.temperature_unit == TEMP_CELSIUS
def test_target_temp(self):
"""Test the target temperature."""
- assert 65 == self.honeywell.target_temperature
+ assert self.honeywell.target_temperature == 65
self.device.system_mode = "cool"
- assert 78 == self.honeywell.target_temperature
+ assert self.honeywell.target_temperature == 78
def test_set_temp(self):
"""Test setting the temperature."""
self.honeywell.set_temperature(temperature=70)
- assert 70 == self.device.setpoint_heat
- assert 70 == self.honeywell.target_temperature
+ assert self.device.setpoint_heat == 70
+ assert self.honeywell.target_temperature == 70
self.device.system_mode = "cool"
- assert 78 == self.honeywell.target_temperature
+ assert self.honeywell.target_temperature == 78
self.honeywell.set_temperature(temperature=74)
- assert 74 == self.device.setpoint_cool
- assert 74 == self.honeywell.target_temperature
+ assert self.device.setpoint_cool == 74
+ assert self.honeywell.target_temperature == 74
def test_set_hvac_mode(self) -> None:
"""Test setting the operation mode."""
self.honeywell.set_hvac_mode("cool")
- assert "cool" == self.device.system_mode
+ assert self.device.system_mode == "cool"
self.honeywell.set_hvac_mode("heat")
- assert "heat" == self.device.system_mode
+ assert self.device.system_mode == "heat"
def test_set_temp_fail(self):
"""Test if setting the temperature fails."""
@@ -392,10 +392,10 @@ def test_attributes(self):
ATTR_FAN_MODES: somecomfort.FAN_MODES,
ATTR_HVAC_MODES: somecomfort.SYSTEM_MODES,
}
- assert expected == self.honeywell.device_state_attributes
+ assert expected == self.honeywell.extra_state_attributes
expected["fan"] = "idle"
self.device.fan_running = False
- assert expected == self.honeywell.device_state_attributes
+ assert self.honeywell.extra_state_attributes == expected
def test_with_no_fan(self):
"""Test if there is on fan."""
@@ -407,7 +407,7 @@ def test_with_no_fan(self):
ATTR_FAN_MODES: somecomfort.FAN_MODES,
ATTR_HVAC_MODES: somecomfort.SYSTEM_MODES,
}
- assert expected == self.honeywell.device_state_attributes
+ assert self.honeywell.extra_state_attributes == expected
def test_heat_away_mode(self):
"""Test setting the heat away mode."""
diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py
index 3dd587cd7a4d80..993f0dba1fd646 100644
--- a/tests/components/http/test_init.py
+++ b/tests/components/http/test_init.py
@@ -60,73 +60,6 @@ async def test_registering_view_while_running(
hass.http.register_view(TestView)
-def test_api_base_url_with_domain(mock_stack):
- """Test setting API URL with domain."""
- api_config = http.ApiConfig("127.0.0.1", "example.com")
- assert api_config.base_url == "http://example.com:8123"
-
-
-def test_api_base_url_with_ip(mock_stack):
- """Test setting API URL with IP."""
- api_config = http.ApiConfig("127.0.0.1", "1.1.1.1")
- assert api_config.base_url == "http://1.1.1.1:8123"
-
-
-def test_api_base_url_with_ip_and_port(mock_stack):
- """Test setting API URL with IP and port."""
- api_config = http.ApiConfig("127.0.0.1", "1.1.1.1", 8124)
- assert api_config.base_url == "http://1.1.1.1:8124"
-
-
-def test_api_base_url_with_protocol(mock_stack):
- """Test setting API URL with protocol."""
- api_config = http.ApiConfig("127.0.0.1", "https://example.com")
- assert api_config.base_url == "https://example.com:8123"
-
-
-def test_api_base_url_with_protocol_and_port(mock_stack):
- """Test setting API URL with protocol and port."""
- api_config = http.ApiConfig("127.0.0.1", "https://example.com", 433)
- assert api_config.base_url == "https://example.com:433"
-
-
-def test_api_base_url_with_ssl_enable(mock_stack):
- """Test setting API URL with use_ssl enabled."""
- api_config = http.ApiConfig("127.0.0.1", "example.com", use_ssl=True)
- assert api_config.base_url == "https://example.com:8123"
-
-
-def test_api_base_url_with_ssl_enable_and_port(mock_stack):
- """Test setting API URL with use_ssl enabled and port."""
- api_config = http.ApiConfig("127.0.0.1", "1.1.1.1", use_ssl=True, port=8888)
- assert api_config.base_url == "https://1.1.1.1:8888"
-
-
-def test_api_base_url_with_protocol_and_ssl_enable(mock_stack):
- """Test setting API URL with specific protocol and use_ssl enabled."""
- api_config = http.ApiConfig("127.0.0.1", "http://example.com", use_ssl=True)
- assert api_config.base_url == "http://example.com:8123"
-
-
-def test_api_base_url_removes_trailing_slash(mock_stack):
- """Test a trialing slash is removed when setting the API URL."""
- api_config = http.ApiConfig("127.0.0.1", "http://example.com/")
- assert api_config.base_url == "http://example.com:8123"
-
-
-def test_api_local_ip(mock_stack):
- """Test a trialing slash is removed when setting the API URL."""
- api_config = http.ApiConfig("127.0.0.1", "http://example.com/")
- assert api_config.local_ip == "127.0.0.1"
-
-
-async def test_api_no_base_url(hass, mock_stack):
- """Test setting api url."""
- result = await async_setup_component(hass, "http", {"http": {}})
- assert result
- assert hass.config.api.base_url == "http://127.0.0.1:8123"
-
-
async def test_not_log_password(hass, aiohttp_client, caplog, legacy_auth):
"""Test access with password doesn't get logged."""
assert await async_setup_component(hass, "api", {"http": {}})
@@ -260,127 +193,3 @@ async def test_storing_config(hass, aiohttp_client, aiohttp_unused_port):
restored["trusted_proxies"][0] = ip_network(restored["trusted_proxies"][0])
assert restored == http.HTTP_SCHEMA(config)
-
-
-async def test_use_of_base_url(hass):
- """Test detection base_url usage when called without integration context."""
- await async_setup_component(hass, "http", {"http": {}})
- with patch(
- "homeassistant.components.http.extract_stack",
- return_value=[
- Mock(
- filename="/home/frenck/homeassistant/core.py",
- lineno="21",
- line="do_something()",
- ),
- Mock(
- filename="/home/frenck/homeassistant/core.py",
- lineno="42",
- line="url = hass.config.api.base_url",
- ),
- Mock(
- filename="/home/frenck/example/client.py",
- lineno="21",
- line="something()",
- ),
- ],
- ), pytest.raises(RuntimeError):
- hass.config.api.base_url
-
-
-async def test_use_of_base_url_integration(hass, caplog):
- """Test detection base_url usage when called with integration context."""
- await async_setup_component(hass, "http", {"http": {}})
- with patch(
- "homeassistant.components.http.extract_stack",
- return_value=[
- Mock(
- filename="/home/frenck/homeassistant/core.py",
- lineno="21",
- line="do_something()",
- ),
- Mock(
- filename="/home/frenck/homeassistant/components/example/__init__.py",
- lineno="42",
- line="url = hass.config.api.base_url",
- ),
- Mock(
- filename="/home/frenck/example/client.py",
- lineno="21",
- line="something()",
- ),
- ],
- ):
- assert hass.config.api.base_url == "http://127.0.0.1:8123"
-
- assert (
- "Detected use of deprecated `base_url` property, use `homeassistant.helpers.network.get_url` method instead. Please report issue for example using this method at homeassistant/components/example/__init__.py, line 42: url = hass.config.api.base_url"
- in caplog.text
- )
-
-
-async def test_use_of_base_url_integration_webhook(hass, caplog):
- """Test detection base_url usage when called with integration context."""
- await async_setup_component(hass, "http", {"http": {}})
- with patch(
- "homeassistant.components.http.extract_stack",
- return_value=[
- Mock(
- filename="/home/frenck/homeassistant/core.py",
- lineno="21",
- line="do_something()",
- ),
- Mock(
- filename="/home/frenck/homeassistant/components/example/__init__.py",
- lineno="42",
- line="url = hass.config.api.base_url",
- ),
- Mock(
- filename="/home/frenck/homeassistant/components/webhook/__init__.py",
- lineno="42",
- line="return get_url(hass)",
- ),
- Mock(
- filename="/home/frenck/example/client.py",
- lineno="21",
- line="something()",
- ),
- ],
- ):
- assert hass.config.api.base_url == "http://127.0.0.1:8123"
-
- assert (
- "Detected use of deprecated `base_url` property, use `homeassistant.helpers.network.get_url` method instead. Please report issue for example using this method at homeassistant/components/example/__init__.py, line 42: url = hass.config.api.base_url"
- in caplog.text
- )
-
-
-async def test_use_of_base_url_custom_component(hass, caplog):
- """Test detection base_url usage when called with custom component context."""
- await async_setup_component(hass, "http", {"http": {}})
- with patch(
- "homeassistant.components.http.extract_stack",
- return_value=[
- Mock(
- filename="/home/frenck/homeassistant/core.py",
- lineno="21",
- line="do_something()",
- ),
- Mock(
- filename="/home/frenck/.homeassistant/custom_components/example/__init__.py",
- lineno="42",
- line="url = hass.config.api.base_url",
- ),
- Mock(
- filename="/home/frenck/example/client.py",
- lineno="21",
- line="something()",
- ),
- ],
- ):
- assert hass.config.api.base_url == "http://127.0.0.1:8123"
-
- assert (
- "Detected use of deprecated `base_url` property, use `homeassistant.helpers.network.get_url` method instead. Please report issue to the custom component author for example using this method at custom_components/example/__init__.py, line 42: url = hass.config.api.base_url"
- in caplog.text
- )
diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py
index fc42babbb3561b..3fc55692cc489c 100644
--- a/tests/components/hue/conftest.py
+++ b/tests/components/hue/conftest.py
@@ -12,7 +12,8 @@
from homeassistant.components import hue
from homeassistant.components.hue import sensor_base as hue_sensor_base
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.common import MockConfigEntry
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
@pytest.fixture(autouse=True)
@@ -111,13 +112,11 @@ async def setup_bridge_for_sensors(hass, mock_bridge, hostname=None):
if hostname is None:
hostname = "mock-host"
hass.config.components.add(hue.DOMAIN)
- config_entry = config_entries.ConfigEntry(
- 1,
- hue.DOMAIN,
- "Mock Title",
- {"host": hostname},
- "test",
- config_entries.CONN_CLASS_LOCAL_POLL,
+ config_entry = MockConfigEntry(
+ domain=hue.DOMAIN,
+ title="Mock Title",
+ data={"host": hostname},
+ connection_class=config_entries.CONN_CLASS_LOCAL_POLL,
system_options={},
)
mock_bridge.config_entry = config_entry
@@ -125,7 +124,7 @@ async def setup_bridge_for_sensors(hass, mock_bridge, hostname=None):
await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor")
await hass.config_entries.async_forward_entry_setup(config_entry, "sensor")
# simulate a full setup by manually adding the bridge config entry
- hass.config_entries._entries.append(config_entry)
+ config_entry.add_to_hass(hass)
# and make sure it completes before going further
await hass.async_block_till_done()
diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py
index 3e6465d6bc8a42..093f6356b09004 100644
--- a/tests/components/hue/test_bridge.py
+++ b/tests/components/hue/test_bridge.py
@@ -189,6 +189,42 @@ async def test_hue_activate_scene(hass, mock_api):
assert len(mock_api.mock_requests) == 3
assert mock_api.mock_requests[2]["json"]["scene"] == "scene_1"
+ assert "transitiontime" not in mock_api.mock_requests[2]["json"]
+ assert mock_api.mock_requests[2]["path"] == "groups/group_1/action"
+
+
+async def test_hue_activate_scene_transition(hass, mock_api):
+ """Test successful hue_activate_scene with transition."""
+ config_entry = config_entries.ConfigEntry(
+ 1,
+ hue.DOMAIN,
+ "Mock Title",
+ {"host": "mock-host", "username": "mock-username"},
+ "test",
+ config_entries.CONN_CLASS_LOCAL_POLL,
+ system_options={},
+ options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False},
+ )
+ hue_bridge = bridge.HueBridge(hass, config_entry)
+
+ mock_api.mock_group_responses.append(GROUP_RESPONSE)
+ mock_api.mock_scene_responses.append(SCENE_RESPONSE)
+
+ with patch("aiohue.Bridge", return_value=mock_api), patch.object(
+ hass.config_entries, "async_forward_entry_setup"
+ ):
+ assert await hue_bridge.async_setup() is True
+
+ assert hue_bridge.api is mock_api
+
+ call = Mock()
+ call.data = {"group_name": "Group 1", "scene_name": "Cozy dinner", "transition": 30}
+ with patch("aiohue.Bridge", return_value=mock_api):
+ assert await hue_bridge.hue_activate_scene(call) is None
+
+ assert len(mock_api.mock_requests) == 3
+ assert mock_api.mock_requests[2]["json"]["scene"] == "scene_1"
+ assert mock_api.mock_requests[2]["json"]["transitiontime"] == 30
assert mock_api.mock_requests[2]["path"] == "groups/group_1/action"
diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py
index c7dc83183aeb86..12a360cdf94282 100644
--- a/tests/components/hue/test_config_flow.py
+++ b/tests/components/hue/test_config_flow.py
@@ -375,14 +375,15 @@ async def test_flow_link_unknown_host(hass):
assert result["reason"] == "cannot_connect"
-async def test_bridge_ssdp(hass):
+@pytest.mark.parametrize("mf_url", config_flow.HUE_MANUFACTURERURL)
+async def test_bridge_ssdp(hass, mf_url):
"""Test a bridge being discovered."""
result = await hass.config_entries.flow.async_init(
const.DOMAIN,
context={"source": "ssdp"},
data={
ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/",
- ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL,
+ ssdp.ATTR_UPNP_MANUFACTURER_URL: mf_url,
ssdp.ATTR_UPNP_SERIAL: "1234",
},
)
@@ -411,7 +412,7 @@ async def test_bridge_ssdp_emulated_hue(hass):
data={
ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/",
ssdp.ATTR_UPNP_FRIENDLY_NAME: "Home Assistant Bridge",
- ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL,
+ ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0],
ssdp.ATTR_UPNP_SERIAL: "1234",
},
)
@@ -426,7 +427,7 @@ async def test_bridge_ssdp_missing_location(hass):
const.DOMAIN,
context={"source": "ssdp"},
data={
- ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL,
+ ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0],
ssdp.ATTR_UPNP_SERIAL: "1234",
},
)
@@ -442,7 +443,7 @@ async def test_bridge_ssdp_missing_serial(hass):
context={"source": "ssdp"},
data={
ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/",
- ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL,
+ ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0],
},
)
@@ -458,7 +459,7 @@ async def test_bridge_ssdp_espalexa(hass):
data={
ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/",
ssdp.ATTR_UPNP_FRIENDLY_NAME: "Espalexa (0.0.0.0)",
- ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL,
+ ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0],
ssdp.ATTR_UPNP_SERIAL: "1234",
},
)
@@ -478,7 +479,7 @@ async def test_bridge_ssdp_already_configured(hass):
context={"source": "ssdp"},
data={
ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/",
- ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL,
+ ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0],
ssdp.ATTR_UPNP_SERIAL: "1234",
},
)
@@ -570,7 +571,14 @@ async def test_bridge_homekit(hass, aioclient_mock):
)
assert result["type"] == "form"
- assert result["step_id"] == "init"
+ assert result["step_id"] == "link"
+
+ flow = next(
+ flow
+ for flow in hass.config_entries.flow.async_progress()
+ if flow["flow_id"] == result["flow_id"]
+ )
+ assert flow["context"]["unique_id"] == config_entries.DEFAULT_DISCOVERY_UNIQUE_ID
async def test_bridge_import_already_configured(hass):
@@ -617,7 +625,7 @@ async def test_ssdp_discovery_update_configuration(hass):
context={"source": "ssdp"},
data={
ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1/",
- ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL,
+ ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0],
ssdp.ATTR_UPNP_SERIAL: "aabbccddeeff",
},
)
@@ -640,6 +648,15 @@ async def test_options_flow(hass):
assert result["type"] == "form"
assert result["step_id"] == "init"
+ schema = result["data_schema"].schema
+ assert (
+ _get_schema_default(schema, const.CONF_ALLOW_HUE_GROUPS)
+ == const.DEFAULT_ALLOW_HUE_GROUPS
+ )
+ assert (
+ _get_schema_default(schema, const.CONF_ALLOW_UNREACHABLE)
+ == const.DEFAULT_ALLOW_UNREACHABLE
+ )
result = await hass.config_entries.options.async_configure(
result["flow_id"],
@@ -654,3 +671,11 @@ async def test_options_flow(hass):
const.CONF_ALLOW_HUE_GROUPS: True,
const.CONF_ALLOW_UNREACHABLE: True,
}
+
+
+def _get_schema_default(schema, key_name):
+ """Iterate schema to find a key."""
+ for schema_key in schema:
+ if schema_key == key_name:
+ return schema_key.default()
+ raise KeyError(f"{key_name} not found in schema")
diff --git a/tests/components/hue/test_device_trigger.py b/tests/components/hue/test_device_trigger.py
index 178cd7b09f1b8b..5711c36da98a5c 100644
--- a/tests/components/hue/test_device_trigger.py
+++ b/tests/components/hue/test_device_trigger.py
@@ -15,7 +15,7 @@
async_mock_service,
mock_device_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
REMOTES_RESPONSE = {"7": HUE_TAP_REMOTE_1, "8": HUE_DIMMER_REMOTE_1}
diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py
index 629a9a4c98bad8..df873536ce239e 100644
--- a/tests/components/hue/test_light.py
+++ b/tests/components/hue/test_light.py
@@ -7,6 +7,7 @@
from homeassistant import config_entries
from homeassistant.components import hue
from homeassistant.components.hue import light as hue_light
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util import color
HUE_LIGHT_NS = "homeassistant.components.light.hue."
@@ -211,8 +212,10 @@ async def test_no_lights_or_groups(hass, mock_bridge):
async def test_lights(hass, mock_bridge):
"""Test the update_lights function with some lights."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
+ mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
+
await setup_bridge(hass, mock_bridge)
- assert len(mock_bridge.mock_requests) == 1
+ assert len(mock_bridge.mock_requests) == 2
# 2 lights
assert len(hass.states.async_all()) == 2
@@ -230,6 +233,8 @@ async def test_lights(hass, mock_bridge):
async def test_lights_color_mode(hass, mock_bridge):
"""Test that lights only report appropriate color mode."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
+ mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
+
await setup_bridge(hass, mock_bridge)
lamp_1 = hass.states.get("light.hue_lamp_1")
@@ -249,8 +254,8 @@ async def test_lights_color_mode(hass, mock_bridge):
await hass.services.async_call(
"light", "turn_on", {"entity_id": "light.hue_lamp_2"}, blocking=True
)
- # 2x light update, 1 turn on request
- assert len(mock_bridge.mock_requests) == 3
+ # 2x light update, 1 group update, 1 turn on request
+ assert len(mock_bridge.mock_requests) == 4
lamp_1 = hass.states.get("light.hue_lamp_1")
assert lamp_1 is not None
@@ -332,9 +337,10 @@ async def test_new_group_discovered(hass, mock_bridge):
async def test_new_light_discovered(hass, mock_bridge):
"""Test if 2nd update has a new light."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
+ mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
await setup_bridge(hass, mock_bridge)
- assert len(mock_bridge.mock_requests) == 1
+ assert len(mock_bridge.mock_requests) == 2
assert len(hass.states.async_all()) == 2
new_light_response = dict(LIGHT_RESPONSE)
@@ -366,8 +372,8 @@ async def test_new_light_discovered(hass, mock_bridge):
await hass.services.async_call(
"light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True
)
- # 2x light update, 1 turn on request
- assert len(mock_bridge.mock_requests) == 3
+ # 2x light update, 1 group update, 1 turn on request
+ assert len(mock_bridge.mock_requests) == 4
assert len(hass.states.async_all()) == 3
light = hass.states.get("light.hue_lamp_3")
@@ -407,9 +413,10 @@ async def test_group_removed(hass, mock_bridge):
async def test_light_removed(hass, mock_bridge):
"""Test if 2nd update has removed light."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
+ mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
await setup_bridge(hass, mock_bridge)
- assert len(mock_bridge.mock_requests) == 1
+ assert len(mock_bridge.mock_requests) == 2
assert len(hass.states.async_all()) == 2
mock_bridge.mock_light_responses.clear()
@@ -420,8 +427,8 @@ async def test_light_removed(hass, mock_bridge):
"light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True
)
- # 2x light update, 1 turn on request
- assert len(mock_bridge.mock_requests) == 3
+ # 2x light update, 1 group update, 1 turn on request
+ assert len(mock_bridge.mock_requests) == 4
assert len(hass.states.async_all()) == 1
light = hass.states.get("light.hue_lamp_1")
@@ -487,9 +494,10 @@ async def test_other_group_update(hass, mock_bridge):
async def test_other_light_update(hass, mock_bridge):
"""Test changing one light that will impact state of other light."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
+ mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
await setup_bridge(hass, mock_bridge)
- assert len(mock_bridge.mock_requests) == 1
+ assert len(mock_bridge.mock_requests) == 2
assert len(hass.states.async_all()) == 2
lamp_2 = hass.states.get("light.hue_lamp_2")
@@ -526,8 +534,8 @@ async def test_other_light_update(hass, mock_bridge):
await hass.services.async_call(
"light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True
)
- # 2x light update, 1 turn on request
- assert len(mock_bridge.mock_requests) == 3
+ # 2x light update, 1 group update, 1 turn on request
+ assert len(mock_bridge.mock_requests) == 4
assert len(hass.states.async_all()) == 2
lamp_2 = hass.states.get("light.hue_lamp_2")
@@ -549,7 +557,6 @@ async def test_update_timeout(hass, mock_bridge):
async def test_update_unauthorized(hass, mock_bridge):
"""Test bridge marked as not authorized if unauthorized during update."""
mock_bridge.api.lights.update = Mock(side_effect=aiohue.Unauthorized)
- mock_bridge.api.groups.update = Mock(side_effect=aiohue.Unauthorized)
await setup_bridge(hass, mock_bridge)
assert len(mock_bridge.mock_requests) == 0
assert len(hass.states.async_all()) == 0
@@ -559,6 +566,8 @@ async def test_update_unauthorized(hass, mock_bridge):
async def test_light_turn_on_service(hass, mock_bridge):
"""Test calling the turn on service on a light."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
+ mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
+
await setup_bridge(hass, mock_bridge)
light = hass.states.get("light.hue_lamp_2")
assert light is not None
@@ -575,10 +584,10 @@ async def test_light_turn_on_service(hass, mock_bridge):
{"entity_id": "light.hue_lamp_2", "brightness": 100, "color_temp": 300},
blocking=True,
)
- # 2x light update, 1 turn on request
- assert len(mock_bridge.mock_requests) == 3
+ # 2x light update, 1 group update, 1 turn on request
+ assert len(mock_bridge.mock_requests) == 4
- assert mock_bridge.mock_requests[1]["json"] == {
+ assert mock_bridge.mock_requests[2]["json"] == {
"bri": 100,
"on": True,
"ct": 300,
@@ -599,9 +608,9 @@ async def test_light_turn_on_service(hass, mock_bridge):
blocking=True,
)
- assert len(mock_bridge.mock_requests) == 5
+ assert len(mock_bridge.mock_requests) == 6
- assert mock_bridge.mock_requests[3]["json"] == {
+ assert mock_bridge.mock_requests[4]["json"] == {
"on": True,
"xy": (0.138, 0.08),
"alert": "none",
@@ -611,6 +620,8 @@ async def test_light_turn_on_service(hass, mock_bridge):
async def test_light_turn_off_service(hass, mock_bridge):
"""Test calling the turn on service on a light."""
mock_bridge.mock_light_responses.append(LIGHT_RESPONSE)
+ mock_bridge.mock_group_responses.append(GROUP_RESPONSE)
+
await setup_bridge(hass, mock_bridge)
light = hass.states.get("light.hue_lamp_1")
assert light is not None
@@ -624,10 +635,11 @@ async def test_light_turn_off_service(hass, mock_bridge):
await hass.services.async_call(
"light", "turn_off", {"entity_id": "light.hue_lamp_1"}, blocking=True
)
- # 2x light update, 1 turn on request
- assert len(mock_bridge.mock_requests) == 3
- assert mock_bridge.mock_requests[1]["json"] == {"on": False, "alert": "none"}
+ # 2x light update, 1 for group update, 1 turn on request
+ assert len(mock_bridge.mock_requests) == 4
+
+ assert mock_bridge.mock_requests[2]["json"] == {"on": False, "alert": "none"}
assert len(hass.states.async_all()) == 2
@@ -649,6 +661,7 @@ def test_available():
bridge=Mock(allow_unreachable=False),
is_group=False,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
+ rooms={},
)
assert light.available is False
@@ -664,6 +677,7 @@ def test_available():
bridge=Mock(allow_unreachable=True),
is_group=False,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
+ rooms={},
)
assert light.available is True
@@ -679,6 +693,7 @@ def test_available():
bridge=Mock(allow_unreachable=False),
is_group=True,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
+ rooms={},
)
assert light.available is True
@@ -697,6 +712,7 @@ def test_hs_color():
bridge=Mock(),
is_group=False,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
+ rooms={},
)
assert light.hs_color is None
@@ -712,6 +728,7 @@ def test_hs_color():
bridge=Mock(),
is_group=False,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
+ rooms={},
)
assert light.hs_color is None
@@ -727,6 +744,7 @@ def test_hs_color():
bridge=Mock(),
is_group=False,
supported_features=hue_light.SUPPORT_HUE_EXTENDED,
+ rooms={},
)
assert light.hs_color == color.color_xy_to_hs(0.4, 0.5, LIGHT_GAMUT)
@@ -742,7 +760,7 @@ async def test_group_features(hass, mock_bridge):
"1": {
"name": "Group 1",
"lights": ["1", "2"],
- "type": "Room",
+ "type": "LightGroup",
"action": {
"on": True,
"bri": 254,
@@ -757,8 +775,8 @@ async def test_group_features(hass, mock_bridge):
"state": {"any_on": True, "all_on": False},
},
"2": {
- "name": "Group 2",
- "lights": ["3", "4"],
+ "name": "Living Room",
+ "lights": ["2", "3"],
"type": "Room",
"action": {
"on": True,
@@ -774,8 +792,8 @@ async def test_group_features(hass, mock_bridge):
"state": {"any_on": True, "all_on": False},
},
"3": {
- "name": "Group 3",
- "lights": ["1", "3"],
+ "name": "Dining Room",
+ "lights": ["4"],
"type": "Room",
"action": {
"on": True,
@@ -900,6 +918,7 @@ async def test_group_features(hass, mock_bridge):
mock_bridge.mock_light_responses.append(light_response)
mock_bridge.mock_group_responses.append(group_response)
await setup_bridge(hass, mock_bridge)
+ assert len(mock_bridge.mock_requests) == 2
color_temp_feature = hue_light.SUPPORT_HUE["Color temperature light"]
extended_color_feature = hue_light.SUPPORT_HUE["Extended color light"]
@@ -907,8 +926,27 @@ async def test_group_features(hass, mock_bridge):
group_1 = hass.states.get("light.group_1")
assert group_1.attributes["supported_features"] == color_temp_feature
- group_2 = hass.states.get("light.group_2")
+ group_2 = hass.states.get("light.living_room")
assert group_2.attributes["supported_features"] == extended_color_feature
- group_3 = hass.states.get("light.group_3")
+ group_3 = hass.states.get("light.dining_room")
assert group_3.attributes["supported_features"] == extended_color_feature
+
+ entity_registry = er.async_get(hass)
+ device_registry = dr.async_get(hass)
+
+ entry = entity_registry.async_get("light.hue_lamp_1")
+ device_entry = device_registry.async_get(entry.device_id)
+ assert device_entry.suggested_area is None
+
+ entry = entity_registry.async_get("light.hue_lamp_2")
+ device_entry = device_registry.async_get(entry.device_id)
+ assert device_entry.suggested_area == "Living Room"
+
+ entry = entity_registry.async_get("light.hue_lamp_3")
+ device_entry = device_registry.async_get(entry.device_id)
+ assert device_entry.suggested_area == "Living Room"
+
+ entry = entity_registry.async_get("light.hue_lamp_4")
+ device_entry = device_registry.async_get(entry.device_id)
+ assert device_entry.suggested_area == "Dining Room"
diff --git a/tests/components/huisbaasje/test_config_flow.py b/tests/components/huisbaasje/test_config_flow.py
index 245ac2f8ddb57a..35e28b645ebee4 100644
--- a/tests/components/huisbaasje/test_config_flow.py
+++ b/tests/components/huisbaasje/test_config_flow.py
@@ -94,7 +94,7 @@ async def test_form_cannot_connect(hass):
)
assert form_result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert form_result["errors"] == {"base": "connection_exception"}
+ assert form_result["errors"] == {"base": "cannot_connect"}
async def test_form_unknown_error(hass):
diff --git a/tests/components/huisbaasje/test_init.py b/tests/components/huisbaasje/test_init.py
index 96be450f7e42a4..2d68cdf8a11af3 100644
--- a/tests/components/huisbaasje/test_init.py
+++ b/tests/components/huisbaasje/test_init.py
@@ -9,12 +9,12 @@
ENTRY_STATE_LOADED,
ENTRY_STATE_NOT_LOADED,
ENTRY_STATE_SETUP_ERROR,
- ConfigEntry,
)
-from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME
+from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
+from tests.common import MockConfigEntry
from tests.components.huisbaasje.test_data import MOCK_CURRENT_MEASUREMENTS
@@ -36,20 +36,20 @@ async def test_setup_entry(hass: HomeAssistant):
return_value=MOCK_CURRENT_MEASUREMENTS,
) as mock_current_measurements:
hass.config.components.add(huisbaasje.DOMAIN)
- config_entry = ConfigEntry(
- 1,
- huisbaasje.DOMAIN,
- "userId",
- {
+ config_entry = MockConfigEntry(
+ version=1,
+ domain=huisbaasje.DOMAIN,
+ title="userId",
+ data={
CONF_ID: "userId",
CONF_USERNAME: "username",
CONF_PASSWORD: "password",
},
- "test",
- CONN_CLASS_CLOUD_POLL,
+ source="test",
+ connection_class=CONN_CLASS_CLOUD_POLL,
system_options={},
)
- hass.config_entries._entries.append(config_entry)
+ config_entry.add_to_hass(hass)
assert config_entry.state == ENTRY_STATE_NOT_LOADED
assert await hass.config_entries.async_setup(config_entry.entry_id)
@@ -77,20 +77,20 @@ async def test_setup_entry_error(hass: HomeAssistant):
"huisbaasje.Huisbaasje.authenticate", side_effect=HuisbaasjeException
) as mock_authenticate:
hass.config.components.add(huisbaasje.DOMAIN)
- config_entry = ConfigEntry(
- 1,
- huisbaasje.DOMAIN,
- "userId",
- {
+ config_entry = MockConfigEntry(
+ version=1,
+ domain=huisbaasje.DOMAIN,
+ title="userId",
+ data={
CONF_ID: "userId",
CONF_USERNAME: "username",
CONF_PASSWORD: "password",
},
- "test",
- CONN_CLASS_CLOUD_POLL,
+ source="test",
+ connection_class=CONN_CLASS_CLOUD_POLL,
system_options={},
)
- hass.config_entries._entries.append(config_entry)
+ config_entry.add_to_hass(hass)
assert config_entry.state == ENTRY_STATE_NOT_LOADED
await hass.config_entries.async_setup(config_entry.entry_id)
@@ -119,20 +119,20 @@ async def test_unload_entry(hass: HomeAssistant):
return_value=MOCK_CURRENT_MEASUREMENTS,
) as mock_current_measurements:
hass.config.components.add(huisbaasje.DOMAIN)
- config_entry = ConfigEntry(
- 1,
- huisbaasje.DOMAIN,
- "userId",
- {
+ config_entry = MockConfigEntry(
+ version=1,
+ domain=huisbaasje.DOMAIN,
+ title="userId",
+ data={
CONF_ID: "userId",
CONF_USERNAME: "username",
CONF_PASSWORD: "password",
},
- "test",
- CONN_CLASS_CLOUD_POLL,
+ source="test",
+ connection_class=CONN_CLASS_CLOUD_POLL,
system_options={},
)
- hass.config_entries._entries.append(config_entry)
+ config_entry.add_to_hass(hass)
# Load config entry
assert await hass.config_entries.async_setup(config_entry.entry_id)
@@ -145,6 +145,14 @@ async def test_unload_entry(hass: HomeAssistant):
await hass.config_entries.async_unload(config_entry.entry_id)
assert config_entry.state == ENTRY_STATE_NOT_LOADED
entities = hass.states.async_entity_ids("sensor")
+ assert len(entities) == 14
+ for entity in entities:
+ assert hass.states.get(entity).state == STATE_UNAVAILABLE
+
+ # Remove config entry
+ await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
+ entities = hass.states.async_entity_ids("sensor")
assert len(entities) == 0
# Assert mocks are called
diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py
index d1ffe565c84290..cfb17cd5f2d5b3 100644
--- a/tests/components/huisbaasje/test_sensor.py
+++ b/tests/components/huisbaasje/test_sensor.py
@@ -2,10 +2,11 @@
from unittest.mock import patch
from homeassistant.components import huisbaasje
-from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL, ConfigEntry
+from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
+from tests.common import MockConfigEntry
from tests.components.huisbaasje.test_data import (
MOCK_CURRENT_MEASUREMENTS,
MOCK_LIMITED_CURRENT_MEASUREMENTS,
@@ -24,20 +25,20 @@ async def test_setup_entry(hass: HomeAssistant):
) as mock_current_measurements:
hass.config.components.add(huisbaasje.DOMAIN)
- config_entry = ConfigEntry(
- 1,
- huisbaasje.DOMAIN,
- "userId",
- {
+ config_entry = MockConfigEntry(
+ version=1,
+ domain=huisbaasje.DOMAIN,
+ title="userId",
+ data={
CONF_ID: "userId",
CONF_USERNAME: "username",
CONF_PASSWORD: "password",
},
- "test",
- CONN_CLASS_CLOUD_POLL,
+ source="test",
+ connection_class=CONN_CLASS_CLOUD_POLL,
system_options={},
)
- hass.config_entries._entries.append(config_entry)
+ config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
@@ -81,20 +82,20 @@ async def test_setup_entry_absent_measurement(hass: HomeAssistant):
) as mock_current_measurements:
hass.config.components.add(huisbaasje.DOMAIN)
- config_entry = ConfigEntry(
- 1,
- huisbaasje.DOMAIN,
- "userId",
- {
+ config_entry = MockConfigEntry(
+ version=1,
+ domain=huisbaasje.DOMAIN,
+ title="userId",
+ data={
CONF_ID: "userId",
CONF_USERNAME: "username",
CONF_PASSWORD: "password",
},
- "test",
- CONN_CLASS_CLOUD_POLL,
+ source="test",
+ connection_class=CONN_CLASS_CLOUD_POLL,
system_options={},
)
- hass.config_entries._entries.append(config_entry)
+ config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
diff --git a/tests/components/humidifier/test_device_action.py b/tests/components/humidifier/test_device_action.py
index 93b97408c39af3..1bf1c110ec6847 100644
--- a/tests/components/humidifier/test_device_action.py
+++ b/tests/components/humidifier/test_device_action.py
@@ -16,7 +16,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/humidifier/test_device_condition.py b/tests/components/humidifier/test_device_condition.py
index ad001d52ae0af6..59887f65a333af 100644
--- a/tests/components/humidifier/test_device_condition.py
+++ b/tests/components/humidifier/test_device_condition.py
@@ -4,7 +4,7 @@
import homeassistant.components.automation as automation
from homeassistant.components.humidifier import DOMAIN, const, device_condition
-from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.const import ATTR_MODE, STATE_OFF, STATE_ON
from homeassistant.helpers import config_validation as cv, device_registry
from homeassistant.setup import async_setup_component
@@ -17,7 +17,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
@@ -51,7 +51,7 @@ async def test_get_conditions(hass, device_reg, entity_reg):
f"{DOMAIN}.test_5678",
STATE_ON,
{
- const.ATTR_MODE: const.MODE_AWAY,
+ ATTR_MODE: const.MODE_AWAY,
const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY],
},
)
@@ -98,7 +98,7 @@ async def test_get_conditions_toggle_only(hass, device_reg, entity_reg):
f"{DOMAIN}.test_5678",
STATE_ON,
{
- const.ATTR_MODE: const.MODE_AWAY,
+ ATTR_MODE: const.MODE_AWAY,
const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY],
},
)
@@ -127,9 +127,7 @@ async def test_get_conditions_toggle_only(hass, device_reg, entity_reg):
async def test_if_state(hass, calls):
"""Test for turn_on and turn_off conditions."""
- hass.states.async_set(
- "humidifier.entity", STATE_ON, {const.ATTR_MODE: const.MODE_AWAY}
- )
+ hass.states.async_set("humidifier.entity", STATE_ON, {ATTR_MODE: const.MODE_AWAY})
assert await async_setup_component(
hass,
@@ -213,9 +211,7 @@ async def test_if_state(hass, calls):
assert len(calls) == 2
assert calls[1].data["some"] == "is_off event - test_event2"
- hass.states.async_set(
- "humidifier.entity", STATE_ON, {const.ATTR_MODE: const.MODE_AWAY}
- )
+ hass.states.async_set("humidifier.entity", STATE_ON, {ATTR_MODE: const.MODE_AWAY})
hass.bus.async_fire("test_event3")
await hass.async_block_till_done()
@@ -223,9 +219,7 @@ async def test_if_state(hass, calls):
assert len(calls) == 3
assert calls[2].data["some"] == "is_mode - event - test_event3"
- hass.states.async_set(
- "humidifier.entity", STATE_ON, {const.ATTR_MODE: const.MODE_HOME}
- )
+ hass.states.async_set("humidifier.entity", STATE_ON, {ATTR_MODE: const.MODE_HOME})
# Should not fire
hass.bus.async_fire("test_event3")
@@ -239,7 +233,7 @@ async def test_capabilities(hass):
"humidifier.entity",
STATE_ON,
{
- const.ATTR_MODE: const.MODE_AWAY,
+ ATTR_MODE: const.MODE_AWAY,
const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY],
},
)
@@ -262,7 +256,7 @@ async def test_capabilities(hass):
capabilities["extra_fields"], custom_serializer=cv.custom_serializer
) == [
{
- "name": "available_modes",
+ "name": "mode",
"options": [("home", "home"), ("away", "away")],
"required": True,
"type": "select",
@@ -288,9 +282,7 @@ async def test_capabilities_no_state(hass):
assert voluptuous_serialize.convert(
capabilities["extra_fields"], custom_serializer=cv.custom_serializer
- ) == [
- {"name": "available_modes", "options": [], "required": True, "type": "select"}
- ]
+ ) == [{"name": "mode", "options": [], "required": True, "type": "select"}]
async def test_get_condition_capabilities(hass, device_reg, entity_reg):
diff --git a/tests/components/humidifier/test_device_trigger.py b/tests/components/humidifier/test_device_trigger.py
index 7cd736b79f4179..12918684df76c0 100644
--- a/tests/components/humidifier/test_device_trigger.py
+++ b/tests/components/humidifier/test_device_trigger.py
@@ -6,7 +6,7 @@
import homeassistant.components.automation as automation
from homeassistant.components.humidifier import DOMAIN, const, device_trigger
-from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON
+from homeassistant.const import ATTR_MODE, ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON
from homeassistant.helpers import config_validation as cv, device_registry
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -20,7 +20,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
@@ -56,7 +56,7 @@ async def test_get_triggers(hass, device_reg, entity_reg):
STATE_ON,
{
const.ATTR_HUMIDITY: 23,
- const.ATTR_MODE: "home",
+ ATTR_MODE: "home",
const.ATTR_AVAILABLE_MODES: ["home", "away"],
ATTR_SUPPORTED_FEATURES: 1,
},
@@ -95,7 +95,7 @@ async def test_if_fires_on_state_change(hass, calls):
STATE_ON,
{
const.ATTR_HUMIDITY: 23,
- const.ATTR_MODE: "home",
+ ATTR_MODE: "home",
const.ATTR_AVAILABLE_MODES: ["home", "away"],
ATTR_SUPPORTED_FEATURES: 1,
},
@@ -243,7 +243,7 @@ async def test_invalid_config(hass, calls):
STATE_ON,
{
const.ATTR_HUMIDITY: 23,
- const.ATTR_MODE: "home",
+ ATTR_MODE: "home",
const.ATTR_AVAILABLE_MODES: ["home", "away"],
ATTR_SUPPORTED_FEATURES: 1,
},
diff --git a/tests/components/humidifier/test_intent.py b/tests/components/humidifier/test_intent.py
index 18c5b632aa6b79..66ff62872f30e7 100644
--- a/tests/components/humidifier/test_intent.py
+++ b/tests/components/humidifier/test_intent.py
@@ -2,7 +2,6 @@
from homeassistant.components.humidifier import (
ATTR_AVAILABLE_MODES,
ATTR_HUMIDITY,
- ATTR_MODE,
DOMAIN,
SERVICE_SET_HUMIDITY,
SERVICE_SET_MODE,
@@ -10,6 +9,7 @@
)
from homeassistant.const import (
ATTR_ENTITY_ID,
+ ATTR_MODE,
ATTR_SUPPORTED_FEATURES,
SERVICE_TURN_ON,
STATE_OFF,
diff --git a/tests/components/humidifier/test_reproduce_state.py b/tests/components/humidifier/test_reproduce_state.py
index 8c1f69353a089c..15b797c66a864c 100644
--- a/tests/components/humidifier/test_reproduce_state.py
+++ b/tests/components/humidifier/test_reproduce_state.py
@@ -4,7 +4,6 @@
from homeassistant.components.humidifier.const import (
ATTR_HUMIDITY,
- ATTR_MODE,
DOMAIN,
MODE_AWAY,
MODE_ECO,
@@ -13,7 +12,13 @@
SERVICE_SET_MODE,
)
from homeassistant.components.humidifier.reproduce_state import async_reproduce_states
-from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON
+from homeassistant.const import (
+ ATTR_MODE,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ STATE_OFF,
+ STATE_ON,
+)
from homeassistant.core import Context, State
from tests.common import async_mock_service
diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py
index f3b2ad383bd732..d0653f88b83597 100644
--- a/tests/components/hyperion/__init__.py
+++ b/tests/components/hyperion/__init__.py
@@ -1,9 +1,8 @@
"""Tests for the Hyperion component."""
from __future__ import annotations
-import logging
from types import TracebackType
-from typing import Any, Dict, Optional, Type
+from typing import Any
from unittest.mock import AsyncMock, Mock, patch
from hyperion import const
@@ -31,25 +30,25 @@
TEST_TOKEN = "sekr1t"
TEST_CONFIG_ENTRY_ID = "74565ad414754616000674c87bdc876c"
-TEST_CONFIG_ENTRY_OPTIONS: Dict[str, Any] = {CONF_PRIORITY: TEST_PRIORITY}
+TEST_CONFIG_ENTRY_OPTIONS: dict[str, Any] = {CONF_PRIORITY: TEST_PRIORITY}
-TEST_INSTANCE_1: Dict[str, Any] = {
+TEST_INSTANCE_1: dict[str, Any] = {
"friendly_name": "Test instance 1",
"instance": 1,
"running": True,
}
-TEST_INSTANCE_2: Dict[str, Any] = {
+TEST_INSTANCE_2: dict[str, Any] = {
"friendly_name": "Test instance 2",
"instance": 2,
"running": True,
}
-TEST_INSTANCE_3: Dict[str, Any] = {
+TEST_INSTANCE_3: dict[str, Any] = {
"friendly_name": "Test instance 3",
"instance": 3,
"running": True,
}
-TEST_AUTH_REQUIRED_RESP: Dict[str, Any] = {
+TEST_AUTH_REQUIRED_RESP: dict[str, Any] = {
"command": "authorize-tokenRequired",
"info": {
"required": True,
@@ -63,22 +62,20 @@
"info": {"required": False},
}
-_LOGGER = logging.getLogger(__name__)
-
class AsyncContextManagerMock(Mock):
"""An async context manager mock for Hyperion."""
- async def __aenter__(self) -> Optional[AsyncContextManagerMock]:
+ async def __aenter__(self) -> AsyncContextManagerMock | None:
"""Enter context manager and connect the client."""
result = await self.async_client_connect()
return self if result else None
async def __aexit__(
self,
- exc_type: Optional[Type[BaseException]],
- exc: Optional[BaseException],
- traceback: Optional[TracebackType],
+ exc_type: type[BaseException] | None,
+ exc: BaseException | None,
+ traceback: TracebackType | None,
) -> None:
"""Leave context manager and disconnect the client."""
await self.async_client_disconnect()
@@ -98,7 +95,7 @@ def create_mock_client() -> Mock:
)
mock_client.async_sysinfo_id = AsyncMock(return_value=TEST_SYSINFO_ID)
- mock_client.async_sysinfo_version = AsyncMock(return_value=TEST_SYSINFO_ID)
+ mock_client.async_sysinfo_version = AsyncMock(return_value=TEST_SYSINFO_VERSION)
mock_client.async_client_switch_instance = AsyncMock(return_value=True)
mock_client.async_client_login = AsyncMock(return_value=True)
mock_client.async_get_serverinfo = AsyncMock(
@@ -121,7 +118,9 @@ def create_mock_client() -> Mock:
def add_test_config_entry(
- hass: HomeAssistantType, data: Optional[Dict[str, Any]] = None
+ hass: HomeAssistantType,
+ data: dict[str, Any] | None = None,
+ options: dict[str, Any] | None = None,
) -> ConfigEntry:
"""Add a test config entry."""
config_entry: MockConfigEntry = MockConfigEntry( # type: ignore[no-untyped-call]
@@ -134,7 +133,7 @@ def add_test_config_entry(
},
title=f"Hyperion {TEST_SYSINFO_ID}",
unique_id=TEST_SYSINFO_ID,
- options=TEST_CONFIG_ENTRY_OPTIONS,
+ options=options or TEST_CONFIG_ENTRY_OPTIONS,
)
config_entry.add_to_hass(hass) # type: ignore[no-untyped-call]
return config_entry
@@ -142,11 +141,12 @@ def add_test_config_entry(
async def setup_test_config_entry(
hass: HomeAssistantType,
- config_entry: Optional[ConfigEntry] = None,
- hyperion_client: Optional[Mock] = None,
+ config_entry: ConfigEntry | None = None,
+ hyperion_client: Mock | None = None,
+ options: dict[str, Any] | None = None,
) -> ConfigEntry:
"""Add a test Hyperion entity to hass."""
- config_entry = config_entry or add_test_config_entry(hass)
+ config_entry = config_entry or add_test_config_entry(hass, options=options)
hyperion_client = hyperion_client or create_mock_client()
# pylint: disable=attribute-defined-outside-init
diff --git a/tests/components/hyperion/conftest.py b/tests/components/hyperion/conftest.py
index 4eb59770fae22b..f971fa3c76728a 100644
--- a/tests/components/hyperion/conftest.py
+++ b/tests/components/hyperion/conftest.py
@@ -1,2 +1,2 @@
"""hyperion conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py
index ef7046660d644e..7cf0556eddf2fd 100644
--- a/tests/components/hyperion/test_config_flow.py
+++ b/tests/components/hyperion/test_config_flow.py
@@ -1,8 +1,9 @@
"""Tests for the Hyperion config flow."""
+from __future__ import annotations
-import logging
-from typing import Any, Dict, Optional
-from unittest.mock import AsyncMock, patch
+import asyncio
+from typing import Any
+from unittest.mock import AsyncMock, Mock, patch
from hyperion import const
@@ -10,6 +11,8 @@
from homeassistant.components.hyperion.const import (
CONF_AUTH_ID,
CONF_CREATE_TOKEN,
+ CONF_EFFECT_HIDE_LIST,
+ CONF_EFFECT_SHOW_LIST,
CONF_PRIORITY,
DOMAIN,
)
@@ -41,10 +44,8 @@
from tests.common import MockConfigEntry
-_LOGGER = logging.getLogger(__name__)
-
TEST_IP_ADDRESS = "192.168.0.1"
-TEST_HOST_PORT: Dict[str, Any] = {
+TEST_HOST_PORT: dict[str, Any] = {
CONF_HOST: TEST_HOST,
CONF_PORT: TEST_PORT,
}
@@ -126,7 +127,7 @@ async def _create_mock_entry(hass: HomeAssistantType) -> MockConfigEntry:
async def _init_flow(
hass: HomeAssistantType,
source: str = SOURCE_USER,
- data: Optional[Dict[str, Any]] = None,
+ data: dict[str, Any] | None = None,
) -> Any:
"""Initialize a flow."""
data = data or {}
@@ -137,7 +138,7 @@ async def _init_flow(
async def _configure_flow(
- hass: HomeAssistantType, result: Dict, user_input: Optional[Dict[str, Any]] = None
+ hass: HomeAssistantType, result: dict, user_input: dict[str, Any] | None = None
) -> Any:
"""Provide input to a flow."""
user_input = user_input or {}
@@ -311,23 +312,42 @@ async def test_auth_static_token_success(hass: HomeAssistantType) -> None:
}
-async def test_auth_static_token_login_fail(hass: HomeAssistantType) -> None:
- """Test correct behavior with a bad static token."""
+async def test_auth_static_token_login_connect_fail(hass: HomeAssistantType) -> None:
+ """Test correct behavior with a static token that cannot connect."""
result = await _init_flow(hass)
assert result["step_id"] == "user"
client = create_mock_client()
client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP)
- # Fail the login call.
- client.async_login = AsyncMock(
- return_value={"command": "authorize-login", "success": False, "tan": 0}
- )
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
+ client.async_client_connect = AsyncMock(return_value=False)
+ result = await _configure_flow(
+ hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "cannot_connect"
+
+
+async def test_auth_static_token_login_fail(hass: HomeAssistantType) -> None:
+ """Test correct behavior with a static token that cannot login."""
+ result = await _init_flow(hass)
+ assert result["step_id"] == "user"
+
+ client = create_mock_client()
+ client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP)
with patch(
"homeassistant.components.hyperion.client.HyperionClient", return_value=client
):
result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
+ client.async_login = AsyncMock(
+ return_value={"command": "authorize-login", "success": False, "tan": 0}
+ )
result = await _configure_flow(
hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN}
)
@@ -379,6 +399,66 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistantType) -> N
assert result["reason"] == "auth_new_token_not_granted_error"
+async def test_auth_create_token_approval_declined_task_canceled(
+ hass: HomeAssistantType,
+) -> None:
+ """Verify correct behaviour when a token request is declined."""
+ result = await _init_flow(hass)
+
+ client = create_mock_client()
+ client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP)
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
+ assert result["step_id"] == "auth"
+
+ client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_FAIL)
+
+ class CanceledAwaitableMock(AsyncMock):
+ """A canceled awaitable mock."""
+
+ def __await__(self):
+ raise asyncio.CancelledError
+
+ mock_task = CanceledAwaitableMock()
+ task_coro = None
+
+ def create_task(arg):
+ nonlocal task_coro
+ task_coro = arg
+ return mock_task
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ), patch(
+ "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id",
+ return_value=TEST_AUTH_ID,
+ ), patch.object(
+ hass, "async_create_task", side_effect=create_task
+ ):
+ result = await _configure_flow(
+ hass, result, user_input={CONF_CREATE_TOKEN: True}
+ )
+ assert result["step_id"] == "create_token"
+
+ result = await _configure_flow(hass, result)
+ assert result["step_id"] == "create_token_external"
+
+ # Leave the task running, to ensure it is canceled.
+ mock_task.done = Mock(return_value=False)
+ mock_task.cancel = Mock()
+
+ result = await _configure_flow(hass, result)
+
+ # This await will advance to the next step.
+ await task_coro
+
+ # Assert that cancel is called on the task.
+ assert mock_task.cancel.called
+
+
async def test_auth_create_token_when_issued_token_fails(
hass: HomeAssistantType,
) -> None:
@@ -470,6 +550,47 @@ async def test_auth_create_token_success(hass: HomeAssistantType) -> None:
}
+async def test_auth_create_token_success_but_login_fail(
+ hass: HomeAssistantType,
+) -> None:
+ """Verify correct behaviour when a token is successfully created but the login fails."""
+ result = await _init_flow(hass)
+
+ client = create_mock_client()
+ client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP)
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
+ assert result["step_id"] == "auth"
+
+ client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS)
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ), patch(
+ "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id",
+ return_value=TEST_AUTH_ID,
+ ):
+ result = await _configure_flow(
+ hass, result, user_input={CONF_CREATE_TOKEN: True}
+ )
+ assert result["step_id"] == "create_token"
+
+ result = await _configure_flow(hass, result)
+ assert result["step_id"] == "create_token_external"
+
+ client.async_login = AsyncMock(
+ return_value={"command": "authorize-login", "success": False, "tan": 0}
+ )
+
+ # The flow will be automatically advanced by the auth token response.
+ result = await _configure_flow(hass, result)
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "auth_new_token_not_work_error"
+
+
async def test_ssdp_success(hass: HomeAssistantType) -> None:
"""Check an SSDP flow."""
@@ -532,7 +653,7 @@ async def test_ssdp_failure_bad_port_json(hass: HomeAssistantType) -> None:
"""Check an SSDP flow with bad json port."""
client = create_mock_client()
- bad_data: Dict[str, Any] = {**TEST_SSDP_SERVICE_INFO}
+ bad_data: dict[str, Any] = {**TEST_SSDP_SERVICE_INFO}
bad_data["ports"]["jsonServer"] = "not_a_port"
with patch(
@@ -601,8 +722,8 @@ async def test_ssdp_abort_duplicates(hass: HomeAssistantType) -> None:
assert result_2["reason"] == "already_in_progress"
-async def test_options(hass: HomeAssistantType) -> None:
- """Check an options flow."""
+async def test_options_priority(hass: HomeAssistantType) -> None:
+ """Check an options flow priority option."""
config_entry = add_test_config_entry(hass)
@@ -620,11 +741,12 @@ async def test_options(hass: HomeAssistantType) -> None:
new_priority = 1
result = await hass.config_entries.options.async_configure(
- result["flow_id"], user_input={CONF_PRIORITY: new_priority}
+ result["flow_id"],
+ user_input={CONF_PRIORITY: new_priority},
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result["data"] == {CONF_PRIORITY: new_priority}
+ assert result["data"][CONF_PRIORITY] == new_priority
# Turn the light on and ensure the new priority is used.
client.async_send_set_color = AsyncMock(return_value=True)
@@ -638,6 +760,59 @@ async def test_options(hass: HomeAssistantType) -> None:
assert client.async_send_set_color.call_args[1][CONF_PRIORITY] == new_priority
+async def test_options_effect_show_list(hass: HomeAssistantType) -> None:
+ """Check an options flow effect show list."""
+
+ config_entry = add_test_config_entry(hass)
+
+ client = create_mock_client()
+ client.effects = [
+ {const.KEY_NAME: "effect1"},
+ {const.KEY_NAME: "effect2"},
+ {const.KEY_NAME: "effect3"},
+ ]
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={CONF_EFFECT_SHOW_LIST: ["effect1", "effect3"]},
+ )
+ await hass.async_block_till_done()
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ # effect1 and effect3 only, so effect2 & external sources are hidden.
+ assert result["data"][CONF_EFFECT_HIDE_LIST] == sorted(
+ ["effect2"] + const.KEY_COMPONENTID_EXTERNAL_SOURCES
+ )
+
+
+async def test_options_effect_hide_list_cannot_connect(hass: HomeAssistantType) -> None:
+ """Check an options flow effect hide list with a failed connection."""
+
+ config_entry = add_test_config_entry(hass)
+ client = create_mock_client()
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ client.async_client_connect = AsyncMock(return_value=False)
+
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "cannot_connect"
+
+
async def test_reauth_success(hass: HomeAssistantType) -> None:
"""Check a reauth flow that succeeds."""
diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py
index c3226cdd389ab5..bb8fe8d0814416 100644
--- a/tests/components/hyperion/test_light.py
+++ b/tests/components/hyperion/test_light.py
@@ -1,12 +1,16 @@
"""Tests for the Hyperion integration."""
-import logging
-from typing import Optional
+from __future__ import annotations
+
from unittest.mock import AsyncMock, Mock, call, patch
from hyperion import const
from homeassistant.components.hyperion import light as hyperion_light
-from homeassistant.components.hyperion.const import DEFAULT_ORIGIN, DOMAIN
+from homeassistant.components.hyperion.const import (
+ CONF_EFFECT_HIDE_LIST,
+ DEFAULT_ORIGIN,
+ DOMAIN,
+)
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_EFFECT,
@@ -27,7 +31,7 @@
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
-from homeassistant.helpers.entity_registry import async_get_registry
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.typing import HomeAssistantType
import homeassistant.util.color as color_util
@@ -52,14 +56,12 @@
setup_test_config_entry,
)
-_LOGGER = logging.getLogger(__name__)
-
COLOR_BLACK = color_util.COLORS["black"]
def _get_config_entry_from_unique_id(
hass: HomeAssistantType, unique_id: str
-) -> Optional[ConfigEntry]:
+) -> ConfigEntry | None:
for entry in hass.config_entries.async_entries(domain=DOMAIN):
if TEST_SYSINFO_ID == entry.unique_id:
return entry
@@ -112,7 +114,7 @@ async def test_setup_config_entry_not_ready_load_state_fail(
async def test_setup_config_entry_dynamic_instances(hass: HomeAssistantType) -> None:
"""Test dynamic changes in the instance configuration."""
- registry = await async_get_registry(hass)
+ registry = er.async_get(hass)
config_entry = add_test_config_entry(hass)
@@ -1131,3 +1133,21 @@ async def test_priority_light_has_no_external_sources(hass: HomeAssistantType) -
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
assert entity_state
assert entity_state.attributes["effect_list"] == [hyperion_light.KEY_EFFECT_SOLID]
+
+
+async def test_light_option_effect_hide_list(hass: HomeAssistantType) -> None:
+ """Test the effect_hide_list option."""
+ client = create_mock_client()
+ client.effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}]
+
+ await setup_test_config_entry(
+ hass, hyperion_client=client, options={CONF_EFFECT_HIDE_LIST: ["Two", "V4L"]}
+ )
+
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state.attributes["effect_list"] == [
+ "Solid",
+ "BOBLIGHTSERVER",
+ "GRABBER",
+ "One",
+ ]
diff --git a/tests/components/hyperion/test_switch.py b/tests/components/hyperion/test_switch.py
index dcfba9662bfbe4..34030787e20b35 100644
--- a/tests/components/hyperion/test_switch.py
+++ b/tests/components/hyperion/test_switch.py
@@ -1,5 +1,4 @@
"""Tests for the Hyperion integration."""
-import logging
from unittest.mock import AsyncMock, call, patch
from hyperion.const import (
@@ -35,7 +34,6 @@
{"enabled": True, "name": "LEDDEVICE"},
]
-_LOGGER = logging.getLogger(__name__)
TEST_SWITCH_COMPONENT_BASE_ENTITY_ID = "switch.test_instance_1_component"
TEST_SWITCH_COMPONENT_ALL_ENTITY_ID = f"{TEST_SWITCH_COMPONENT_BASE_ENTITY_ID}_all"
diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py
index a774e61f3ece98..998a69c575a297 100644
--- a/tests/components/icloud/test_config_flow.py
+++ b/tests/components/icloud/test_config_flow.py
@@ -51,6 +51,7 @@ def mock_controller_service():
with patch(
"homeassistant.components.icloud.config_flow.PyiCloudService"
) as service_mock:
+ service_mock.return_value.requires_2fa = False
service_mock.return_value.requires_2sa = True
service_mock.return_value.trusted_devices = TRUSTED_DEVICES
service_mock.return_value.send_verification_code = Mock(return_value=True)
@@ -58,15 +59,31 @@ def mock_controller_service():
yield service_mock
+@pytest.fixture(name="service_2fa")
+def mock_controller_2fa_service():
+ """Mock a successful 2fa service."""
+ with patch(
+ "homeassistant.components.icloud.config_flow.PyiCloudService"
+ ) as service_mock:
+ service_mock.return_value.requires_2fa = True
+ service_mock.return_value.requires_2sa = True
+ service_mock.return_value.validate_2fa_code = Mock(return_value=True)
+ service_mock.return_value.is_trusted_session = False
+ yield service_mock
+
+
@pytest.fixture(name="service_authenticated")
def mock_controller_service_authenticated():
"""Mock a successful service while already authenticate."""
with patch(
"homeassistant.components.icloud.config_flow.PyiCloudService"
) as service_mock:
+ service_mock.return_value.requires_2fa = False
service_mock.return_value.requires_2sa = False
+ service_mock.return_value.is_trusted_session = True
service_mock.return_value.trusted_devices = TRUSTED_DEVICES
service_mock.return_value.send_verification_code = Mock(return_value=True)
+ service_mock.return_value.validate_2fa_code = Mock(return_value=True)
service_mock.return_value.validate_verification_code = Mock(return_value=True)
yield service_mock
@@ -77,6 +94,7 @@ def mock_controller_service_authenticated_no_device():
with patch(
"homeassistant.components.icloud.config_flow.PyiCloudService"
) as service_mock:
+ service_mock.return_value.requires_2fa = False
service_mock.return_value.requires_2sa = False
service_mock.return_value.trusted_devices = TRUSTED_DEVICES
service_mock.return_value.send_verification_code = Mock(return_value=True)
@@ -85,24 +103,53 @@ def mock_controller_service_authenticated_no_device():
yield service_mock
+@pytest.fixture(name="service_authenticated_not_trusted")
+def mock_controller_service_authenticated_not_trusted():
+ """Mock a successful service while already authenticated, but the session is not trusted."""
+ with patch(
+ "homeassistant.components.icloud.config_flow.PyiCloudService"
+ ) as service_mock:
+ service_mock.return_value.requires_2fa = False
+ service_mock.return_value.requires_2sa = False
+ service_mock.return_value.is_trusted_session = False
+ service_mock.return_value.trusted_devices = TRUSTED_DEVICES
+ service_mock.return_value.send_verification_code = Mock(return_value=True)
+ service_mock.return_value.validate_2fa_code = Mock(return_value=True)
+ service_mock.return_value.validate_verification_code = Mock(return_value=True)
+ yield service_mock
+
+
@pytest.fixture(name="service_send_verification_code_failed")
def mock_controller_service_send_verification_code_failed():
"""Mock a failed service during sending verification code step."""
with patch(
"homeassistant.components.icloud.config_flow.PyiCloudService"
) as service_mock:
+ service_mock.return_value.requires_2fa = False
service_mock.return_value.requires_2sa = True
service_mock.return_value.trusted_devices = TRUSTED_DEVICES
service_mock.return_value.send_verification_code = Mock(return_value=False)
yield service_mock
+@pytest.fixture(name="service_validate_2fa_code_failed")
+def mock_controller_service_validate_2fa_code_failed():
+ """Mock a failed service during validation of 2FA verification code step."""
+ with patch(
+ "homeassistant.components.icloud.config_flow.PyiCloudService"
+ ) as service_mock:
+ service_mock.return_value.requires_2fa = True
+ service_mock.return_value.validate_2fa_code = Mock(return_value=False)
+ yield service_mock
+
+
@pytest.fixture(name="service_validate_verification_code_failed")
def mock_controller_service_validate_verification_code_failed():
"""Mock a failed service during validation of verification code step."""
with patch(
"homeassistant.components.icloud.config_flow.PyiCloudService"
) as service_mock:
+ service_mock.return_value.requires_2fa = False
service_mock.return_value.requires_2sa = True
service_mock.return_value.trusted_devices = TRUSTED_DEVICES
service_mock.return_value.send_verification_code = Mock(return_value=True)
@@ -409,6 +456,49 @@ async def test_validate_verification_code_failed(
assert result["errors"] == {"base": "validate_verification_code"}
+async def test_2fa_code_success(hass: HomeAssistantType, service_2fa: MagicMock):
+ """Test 2fa step success."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ )
+ service_2fa.return_value.requires_2fa = False
+ service_2fa.return_value.requires_2sa = False
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {CONF_VERIFICATION_CODE: "0"}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["result"].unique_id == USERNAME
+ assert result["title"] == USERNAME
+ assert result["data"][CONF_USERNAME] == USERNAME
+ assert result["data"][CONF_PASSWORD] == PASSWORD
+ assert result["data"][CONF_WITH_FAMILY] == DEFAULT_WITH_FAMILY
+ assert result["data"][CONF_MAX_INTERVAL] == DEFAULT_MAX_INTERVAL
+ assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD
+
+
+async def test_validate_2fa_code_failed(
+ hass: HomeAssistantType, service_validate_2fa_code_failed: MagicMock
+):
+ """Test when we have errors during validate_verification_code."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ )
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {CONF_VERIFICATION_CODE: "0"}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == CONF_VERIFICATION_CODE
+ assert result["errors"] == {"base": "validate_verification_code"}
+
+
async def test_password_update(
hass: HomeAssistantType, service_authenticated: MagicMock
):
diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py
index d10df2492d42dc..41885f0cd26b81 100644
--- a/tests/components/ifttt/test_init.py
+++ b/tests/components/ifttt/test_init.py
@@ -1,17 +1,20 @@
"""Test the init file of IFTTT."""
-from unittest.mock import patch
-
from homeassistant import data_entry_flow
from homeassistant.components import ifttt
+from homeassistant.config import async_process_ha_core_config
from homeassistant.core import callback
async def test_config_flow_registers_webhook(hass, aiohttp_client):
"""Test setting up IFTTT and sending webhook."""
- with patch("homeassistant.util.get_local_ip", return_value="example.com"):
- result = await hass.config_entries.flow.async_init(
- "ifttt", context={"source": "user"}
- )
+ await async_process_ha_core_config(
+ hass,
+ {"internal_url": "http://example.local:8123"},
+ )
+
+ result = await hass.config_entries.flow.async_init(
+ "ifttt", context={"source": "user"}
+ )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
diff --git a/tests/components/imap_email_content/test_sensor.py b/tests/components/imap_email_content/test_sensor.py
index 241dc4dc5064fe..aa25d4f85ff179 100644
--- a/tests/components/imap_email_content/test_sensor.py
+++ b/tests/components/imap_email_content/test_sensor.py
@@ -47,13 +47,13 @@ async def test_allowed_sender(hass):
sensor.entity_id = "sensor.emailtest"
sensor.async_schedule_update_ha_state(True)
await hass.async_block_till_done()
- assert "Test" == sensor.state
- assert "Test Message" == sensor.device_state_attributes["body"]
- assert "sender@test.com" == sensor.device_state_attributes["from"]
- assert "Test" == sensor.device_state_attributes["subject"]
+ assert sensor.state == "Test"
+ assert sensor.extra_state_attributes["body"] == "Test Message"
+ assert sensor.extra_state_attributes["from"] == "sender@test.com"
+ assert sensor.extra_state_attributes["subject"] == "Test"
assert (
datetime.datetime(2016, 1, 1, 12, 44, 57)
- == sensor.device_state_attributes["date"]
+ == sensor.extra_state_attributes["date"]
)
@@ -83,8 +83,8 @@ async def test_multi_part_with_text(hass):
sensor.entity_id = "sensor.emailtest"
sensor.async_schedule_update_ha_state(True)
await hass.async_block_till_done()
- assert "Link" == sensor.state
- assert "Test Message" == sensor.device_state_attributes["body"]
+ assert sensor.state == "Link"
+ assert sensor.extra_state_attributes["body"] == "Test Message"
async def test_multi_part_only_html(hass):
@@ -110,10 +110,10 @@ async def test_multi_part_only_html(hass):
sensor.entity_id = "sensor.emailtest"
sensor.async_schedule_update_ha_state(True)
await hass.async_block_till_done()
- assert "Link" == sensor.state
+ assert sensor.state == "Link"
assert (
- "Test Message"
- == sensor.device_state_attributes["body"]
+ sensor.extra_state_attributes["body"]
+ == "Test Message"
)
@@ -140,8 +140,8 @@ async def test_multi_part_only_other_text(hass):
sensor.entity_id = "sensor.emailtest"
sensor.async_schedule_update_ha_state(True)
await hass.async_block_till_done()
- assert "Link" == sensor.state
- assert "Test Message" == sensor.device_state_attributes["body"]
+ assert sensor.state == "Link"
+ assert sensor.extra_state_attributes["body"] == "Test Message"
async def test_multiple_emails(hass):
@@ -180,10 +180,10 @@ def state_changed_listener(entity_id, from_s, to_s):
sensor.async_schedule_update_ha_state(True)
await hass.async_block_till_done()
- assert "Test" == states[0].state
- assert "Test 2" == states[1].state
+ assert states[0].state == "Test"
+ assert states[1].state == "Test 2"
- assert "Test Message 2" == sensor.device_state_attributes["body"]
+ assert sensor.extra_state_attributes["body"] == "Test Message 2"
async def test_sender_not_allowed(hass):
@@ -227,4 +227,4 @@ async def test_template(hass):
sensor.entity_id = "sensor.emailtest"
sensor.async_schedule_update_ha_state(True)
await hass.async_block_till_done()
- assert "Test from sender@test.com with message Test Message" == sensor.state
+ assert sensor.state == "Test from sender@test.com with message Test Message"
diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py
index db22b5c5236369..e589a42e99af9f 100644
--- a/tests/components/influxdb/test_init.py
+++ b/tests/components/influxdb/test_init.py
@@ -127,10 +127,144 @@ async def test_setup_config_full(hass, mock_client, config_ext, get_write_api):
assert await async_setup_component(hass, influxdb.DOMAIN, config)
await hass.async_block_till_done()
assert hass.bus.listen.called
- assert EVENT_STATE_CHANGED == hass.bus.listen.call_args_list[0][0][0]
+ assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED
assert get_write_api(mock_client).call_count == 1
+@pytest.mark.parametrize(
+ "mock_client, config_base, config_ext, expected_client_args",
+ [
+ (
+ influxdb.DEFAULT_API_VERSION,
+ BASE_V1_CONFIG,
+ {
+ "ssl": True,
+ "verify_ssl": False,
+ },
+ {
+ "ssl": True,
+ "verify_ssl": False,
+ },
+ ),
+ (
+ influxdb.DEFAULT_API_VERSION,
+ BASE_V1_CONFIG,
+ {
+ "ssl": True,
+ "verify_ssl": True,
+ },
+ {
+ "ssl": True,
+ "verify_ssl": True,
+ },
+ ),
+ (
+ influxdb.DEFAULT_API_VERSION,
+ BASE_V1_CONFIG,
+ {
+ "ssl": True,
+ "verify_ssl": True,
+ "ssl_ca_cert": "fake/path/ca.pem",
+ },
+ {
+ "ssl": True,
+ "verify_ssl": "fake/path/ca.pem",
+ },
+ ),
+ (
+ influxdb.DEFAULT_API_VERSION,
+ BASE_V1_CONFIG,
+ {
+ "ssl": True,
+ "ssl_ca_cert": "fake/path/ca.pem",
+ },
+ {
+ "ssl": True,
+ "verify_ssl": "fake/path/ca.pem",
+ },
+ ),
+ (
+ influxdb.DEFAULT_API_VERSION,
+ BASE_V1_CONFIG,
+ {
+ "ssl": True,
+ "verify_ssl": False,
+ "ssl_ca_cert": "fake/path/ca.pem",
+ },
+ {
+ "ssl": True,
+ "verify_ssl": False,
+ },
+ ),
+ (
+ influxdb.API_VERSION_2,
+ BASE_V2_CONFIG,
+ {
+ "api_version": influxdb.API_VERSION_2,
+ "verify_ssl": False,
+ },
+ {
+ "verify_ssl": False,
+ },
+ ),
+ (
+ influxdb.API_VERSION_2,
+ BASE_V2_CONFIG,
+ {
+ "api_version": influxdb.API_VERSION_2,
+ "verify_ssl": True,
+ },
+ {
+ "verify_ssl": True,
+ },
+ ),
+ (
+ influxdb.API_VERSION_2,
+ BASE_V2_CONFIG,
+ {
+ "api_version": influxdb.API_VERSION_2,
+ "verify_ssl": True,
+ "ssl_ca_cert": "fake/path/ca.pem",
+ },
+ {
+ "verify_ssl": True,
+ "ssl_ca_cert": "fake/path/ca.pem",
+ },
+ ),
+ (
+ influxdb.API_VERSION_2,
+ BASE_V2_CONFIG,
+ {
+ "api_version": influxdb.API_VERSION_2,
+ "verify_ssl": False,
+ "ssl_ca_cert": "fake/path/ca.pem",
+ },
+ {
+ "verify_ssl": False,
+ "ssl_ca_cert": "fake/path/ca.pem",
+ },
+ ),
+ ],
+ indirect=["mock_client"],
+)
+async def test_setup_config_ssl(
+ hass, mock_client, config_base, config_ext, expected_client_args
+):
+ """Test the setup with various verify_ssl values."""
+ config = {"influxdb": config_base.copy()}
+ config["influxdb"].update(config_ext)
+
+ with patch("os.access", return_value=True), patch(
+ "os.path.isfile", return_value=True
+ ):
+ assert await async_setup_component(hass, influxdb.DOMAIN, config)
+ await hass.async_block_till_done()
+
+ assert hass.bus.listen.called
+ assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED
+ assert expected_client_args.items() <= mock_client.call_args.kwargs.items()
+
+
@pytest.mark.parametrize(
"mock_client, config_ext, get_write_api",
[
@@ -147,7 +281,7 @@ async def test_setup_minimal_config(hass, mock_client, config_ext, get_write_api
assert await async_setup_component(hass, influxdb.DOMAIN, config)
await hass.async_block_till_done()
assert hass.bus.listen.called
- assert EVENT_STATE_CHANGED == hass.bus.listen.call_args_list[0][0][0]
+ assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED
assert get_write_api(mock_client).call_count == 1
diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py
index 57983d14abab86..9a353f59e424ae 100644
--- a/tests/components/influxdb/test_sensor.py
+++ b/tests/components/influxdb/test_sensor.py
@@ -1,7 +1,8 @@
"""The tests for the InfluxDB sensor."""
+from __future__ import annotations
+
from dataclasses import dataclass
from datetime import timedelta
-from typing import Dict, List, Type
from unittest.mock import MagicMock, patch
from influxdb.exceptions import InfluxDBClientError, InfluxDBServerError
@@ -55,14 +56,14 @@
class Record:
"""Record in a Table."""
- values: Dict
+ values: dict
@dataclass
class Table:
"""Table in an Influx 2 resultset."""
- records: List[Type[Record]]
+ records: list[type[Record]]
@pytest.fixture(name="mock_client")
@@ -441,7 +442,7 @@ async def test_error_querying_influx(
@pytest.mark.parametrize(
- "mock_client, config_ext, queries, set_query_mock, make_resultset",
+ "mock_client, config_ext, queries, set_query_mock, make_resultset, key",
[
(
DEFAULT_API_VERSION,
@@ -458,6 +459,7 @@ async def test_error_querying_influx(
},
_set_query_mock_v1,
_make_v1_resultset,
+ "where",
),
(
API_VERSION_2,
@@ -465,12 +467,13 @@ async def test_error_querying_influx(
{"queries_flux": [{"name": "test", "query": "{{ illegal.template }}"}]},
_set_query_mock_v2,
_make_v2_resultset,
+ "query",
),
],
indirect=["mock_client"],
)
async def test_error_rendering_template(
- hass, caplog, mock_client, config_ext, queries, set_query_mock, make_resultset
+ hass, caplog, mock_client, config_ext, queries, set_query_mock, make_resultset, key
):
"""Test behavior of sensor with error rendering template."""
set_query_mock(mock_client, return_value=make_resultset(42))
@@ -478,7 +481,15 @@ async def test_error_rendering_template(
sensors = await _setup(hass, config_ext, queries, ["sensor.test"])
assert sensors[0].state == STATE_UNKNOWN
assert (
- len([record for record in caplog.records if record.levelname == "ERROR"]) == 1
+ len(
+ [
+ record
+ for record in caplog.records
+ if record.levelname == "ERROR"
+ and f"Could not render {key} template" in record.msg
+ ]
+ )
+ == 1
)
diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py
index 88562678436653..2b7a1f88ef1261 100644
--- a/tests/components/input_boolean/test_init.py
+++ b/tests/components/input_boolean/test_init.py
@@ -20,7 +20,7 @@
STATE_ON,
)
from homeassistant.core import Context, CoreState, State
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from tests.common import mock_component, mock_restore_cache
@@ -109,13 +109,13 @@ async def test_config_options(hass):
assert state_1 is not None
assert state_2 is not None
- assert STATE_OFF == state_1.state
+ assert state_1.state == STATE_OFF
assert ATTR_ICON not in state_1.attributes
assert ATTR_FRIENDLY_NAME not in state_1.attributes
- assert STATE_ON == state_2.state
- assert "Hello World" == state_2.attributes.get(ATTR_FRIENDLY_NAME)
- assert "mdi:work" == state_2.attributes.get(ATTR_ICON)
+ assert state_2.state == STATE_ON
+ assert state_2.attributes.get(ATTR_FRIENDLY_NAME) == "Hello World"
+ assert state_2.attributes.get(ATTR_ICON) == "mdi:work"
async def test_restore_state(hass):
@@ -192,7 +192,7 @@ async def test_input_boolean_context(hass, hass_admin_user):
async def test_reload(hass, hass_admin_user):
"""Test reload service."""
count_start = len(hass.states.async_entity_ids())
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
_LOGGER.debug("ENTITIES @ start: %s", hass.states.async_entity_ids())
@@ -218,7 +218,7 @@ async def test_reload(hass, hass_admin_user):
assert state_1 is not None
assert state_2 is not None
assert state_3 is None
- assert STATE_ON == state_2.state
+ assert state_2.state == STATE_ON
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None
@@ -259,12 +259,12 @@ async def test_reload(hass, hass_admin_user):
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is not None
- assert STATE_ON == state_2.state # reload is not supposed to change entity state
- assert "Hello World reloaded" == state_2.attributes.get(ATTR_FRIENDLY_NAME)
- assert "mdi:work_reloaded" == state_2.attributes.get(ATTR_ICON)
+ assert state_2.state == STATE_ON # reload is not supposed to change entity state
+ assert state_2.attributes.get(ATTR_FRIENDLY_NAME) == "Hello World reloaded"
+ assert state_2.attributes.get(ATTR_ICON) == "mdi:work_reloaded"
-async def test_load_person_storage(hass, storage_setup):
+async def test_load_from_storage(hass, storage_setup):
"""Test set up from storage."""
assert await storage_setup()
state = hass.states.get(f"{DOMAIN}.from_storage")
@@ -313,7 +313,7 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup):
input_id = "from_storage"
input_entity_id = f"{DOMAIN}.{input_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state is not None
diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py
index 8d9ddf9546d211..39497e1164c5fe 100644
--- a/tests/components/input_datetime/test_init.py
+++ b/tests/components/input_datetime/test_init.py
@@ -28,7 +28,7 @@
from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_NAME
from homeassistant.core import Context, CoreState, State
from homeassistant.exceptions import Unauthorized
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
@@ -415,7 +415,7 @@ async def test_input_datetime_context(hass, hass_admin_user):
async def test_reload(hass, hass_admin_user, hass_read_only_user):
"""Test reload service."""
count_start = len(hass.states.async_entity_ids())
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
assert await async_setup_component(
hass,
@@ -544,7 +544,7 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup):
input_id = "from_storage"
input_entity_id = f"{DOMAIN}.datetime_from_storage"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state is not None
@@ -570,7 +570,7 @@ async def test_update(hass, hass_ws_client, storage_setup):
input_id = "from_storage"
input_entity_id = f"{DOMAIN}.datetime_from_storage"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state.attributes[ATTR_FRIENDLY_NAME] == "datetime from storage"
@@ -602,7 +602,7 @@ async def test_ws_create(hass, hass_ws_client, storage_setup):
input_id = "new_datetime"
input_entity_id = f"{DOMAIN}.{input_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state is None
diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py
index 28b9d27d23fd05..d6d80a9ad87049 100644
--- a/tests/components/input_number/test_init.py
+++ b/tests/components/input_number/test_init.py
@@ -21,7 +21,7 @@
)
from homeassistant.core import Context, CoreState, State
from homeassistant.exceptions import Unauthorized
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from tests.common import mock_restore_cache
@@ -116,17 +116,17 @@ async def test_set_value(hass, caplog):
entity_id = "input_number.test_1"
state = hass.states.get(entity_id)
- assert 50 == float(state.state)
+ assert float(state.state) == 50
await set_value(hass, entity_id, "30.4")
state = hass.states.get(entity_id)
- assert 30.4 == float(state.state)
+ assert float(state.state) == 30.4
await set_value(hass, entity_id, "70")
state = hass.states.get(entity_id)
- assert 70 == float(state.state)
+ assert float(state.state) == 70
with pytest.raises(vol.Invalid) as excinfo:
await set_value(hass, entity_id, "110")
@@ -136,7 +136,7 @@ async def test_set_value(hass, caplog):
)
state = hass.states.get(entity_id)
- assert 70 == float(state.state)
+ assert float(state.state) == 70
async def test_increment(hass):
@@ -147,19 +147,19 @@ async def test_increment(hass):
entity_id = "input_number.test_2"
state = hass.states.get(entity_id)
- assert 50 == float(state.state)
+ assert float(state.state) == 50
await increment(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert 51 == float(state.state)
+ assert float(state.state) == 51
await increment(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert 51 == float(state.state)
+ assert float(state.state) == 51
async def test_decrement(hass):
@@ -170,19 +170,19 @@ async def test_decrement(hass):
entity_id = "input_number.test_3"
state = hass.states.get(entity_id)
- assert 50 == float(state.state)
+ assert float(state.state) == 50
await decrement(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert 49 == float(state.state)
+ assert float(state.state) == 49
await decrement(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert 49 == float(state.state)
+ assert float(state.state) == 49
async def test_mode(hass):
@@ -201,15 +201,15 @@ async def test_mode(hass):
state = hass.states.get("input_number.test_default_slider")
assert state
- assert "slider" == state.attributes["mode"]
+ assert state.attributes["mode"] == "slider"
state = hass.states.get("input_number.test_explicit_box")
assert state
- assert "box" == state.attributes["mode"]
+ assert state.attributes["mode"] == "box"
state = hass.states.get("input_number.test_explicit_slider")
assert state
- assert "slider" == state.attributes["mode"]
+ assert state.attributes["mode"] == "slider"
async def test_restore_state(hass):
@@ -300,7 +300,7 @@ async def test_input_number_context(hass, hass_admin_user):
async def test_reload(hass, hass_admin_user, hass_read_only_user):
"""Test reload service."""
count_start = len(hass.states.async_entity_ids())
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
assert await async_setup_component(
hass,
@@ -322,8 +322,8 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user):
assert state_1 is not None
assert state_2 is None
assert state_3 is not None
- assert 50 == float(state_1.state)
- assert 10 == float(state_3.state)
+ assert float(state_1.state) == 50
+ assert float(state_3.state) == 10
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is None
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is not None
@@ -362,8 +362,8 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user):
assert state_1 is not None
assert state_2 is not None
assert state_3 is None
- assert 50 == float(state_1.state)
- assert 20 == float(state_2.state)
+ assert float(state_1.state) == 50
+ assert float(state_2.state) == 20
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is None
@@ -442,7 +442,7 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup):
input_id = "from_storage"
input_entity_id = f"{DOMAIN}.{input_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state is not None
@@ -478,7 +478,7 @@ async def test_update_min_max(hass, hass_ws_client, storage_setup):
input_id = "from_storage"
input_entity_id = f"{DOMAIN}.{input_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state is not None
@@ -518,7 +518,7 @@ async def test_ws_create(hass, hass_ws_client, storage_setup):
input_id = "new_input"
input_entity_id = f"{DOMAIN}.{input_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state is None
diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py
index 38a3c3ba7a23f6..f5f7956e9c5ff6 100644
--- a/tests/components/input_select/test_init.py
+++ b/tests/components/input_select/test_init.py
@@ -26,7 +26,7 @@
)
from homeassistant.core import Context, State
from homeassistant.exceptions import Unauthorized
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from homeassistant.loader import bind_hass
from homeassistant.setup import async_setup_component
@@ -156,19 +156,19 @@ async def test_select_option(hass):
entity_id = "input_select.test_1"
state = hass.states.get(entity_id)
- assert "some option" == state.state
+ assert state.state == "some option"
select_option(hass, entity_id, "another option")
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert "another option" == state.state
+ assert state.state == "another option"
select_option(hass, entity_id, "non existing option")
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert "another option" == state.state
+ assert state.state == "another option"
async def test_select_next(hass):
@@ -188,19 +188,19 @@ async def test_select_next(hass):
entity_id = "input_select.test_1"
state = hass.states.get(entity_id)
- assert "middle option" == state.state
+ assert state.state == "middle option"
select_next(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert "last option" == state.state
+ assert state.state == "last option"
select_next(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert "first option" == state.state
+ assert state.state == "first option"
async def test_select_previous(hass):
@@ -220,19 +220,19 @@ async def test_select_previous(hass):
entity_id = "input_select.test_1"
state = hass.states.get(entity_id)
- assert "middle option" == state.state
+ assert state.state == "middle option"
select_previous(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert "first option" == state.state
+ assert state.state == "first option"
select_previous(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert "last option" == state.state
+ assert state.state == "last option"
async def test_select_first_last(hass):
@@ -252,19 +252,19 @@ async def test_select_first_last(hass):
entity_id = "input_select.test_1"
state = hass.states.get(entity_id)
- assert "middle option" == state.state
+ assert state.state == "middle option"
select_first(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert "first option" == state.state
+ assert state.state == "first option"
select_last(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert "last option" == state.state
+ assert state.state == "last option"
async def test_config_options(hass):
@@ -297,14 +297,14 @@ async def test_config_options(hass):
assert state_1 is not None
assert state_2 is not None
- assert "1" == state_1.state
- assert ["1", "2"] == state_1.attributes.get(ATTR_OPTIONS)
+ assert state_1.state == "1"
+ assert state_1.attributes.get(ATTR_OPTIONS) == ["1", "2"]
assert ATTR_ICON not in state_1.attributes
- assert "Better Option" == state_2.state
- assert test_2_options == state_2.attributes.get(ATTR_OPTIONS)
- assert "Hello World" == state_2.attributes.get(ATTR_FRIENDLY_NAME)
- assert "mdi:work" == state_2.attributes.get(ATTR_ICON)
+ assert state_2.state == "Better Option"
+ assert state_2.attributes.get(ATTR_OPTIONS) == test_2_options
+ assert state_2.attributes.get(ATTR_FRIENDLY_NAME) == "Hello World"
+ assert state_2.attributes.get(ATTR_ICON) == "mdi:work"
async def test_set_options_service(hass):
@@ -324,24 +324,24 @@ async def test_set_options_service(hass):
entity_id = "input_select.test_1"
state = hass.states.get(entity_id)
- assert "middle option" == state.state
+ assert state.state == "middle option"
data = {ATTR_OPTIONS: ["test1", "test2"], "entity_id": entity_id}
await hass.services.async_call(DOMAIN, SERVICE_SET_OPTIONS, data)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert "test1" == state.state
+ assert state.state == "test1"
select_option(hass, entity_id, "first option")
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert "test1" == state.state
+ assert state.state == "test1"
select_option(hass, entity_id, "test2")
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert "test2" == state.state
+ assert state.state == "test2"
async def test_restore_state(hass):
@@ -425,7 +425,7 @@ async def test_input_select_context(hass, hass_admin_user):
async def test_reload(hass, hass_admin_user, hass_read_only_user):
"""Test reload service."""
count_start = len(hass.states.async_entity_ids())
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
assert await async_setup_component(
hass,
@@ -453,8 +453,8 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user):
assert state_1 is not None
assert state_2 is not None
assert state_3 is None
- assert "middle option" == state_1.state
- assert "an option" == state_2.state
+ assert state_1.state == "middle option"
+ assert state_2.state == "an option"
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is None
@@ -499,8 +499,8 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user):
assert state_1 is None
assert state_2 is not None
assert state_3 is not None
- assert "an option" == state_2.state
- assert "newer option" == state_3.state
+ assert state_2.state == "an option"
+ assert state_3.state == "newer option"
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is None
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is not None
@@ -559,7 +559,7 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup):
input_id = "from_storage"
input_entity_id = f"{DOMAIN}.{input_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state is not None
@@ -592,7 +592,7 @@ async def test_update(hass, hass_ws_client, storage_setup):
input_id = "from_storage"
input_entity_id = f"{DOMAIN}.{input_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state.attributes[ATTR_OPTIONS] == ["yaml update 1", "yaml update 2"]
@@ -633,7 +633,7 @@ async def test_ws_create(hass, hass_ws_client, storage_setup):
input_id = "new_input"
input_entity_id = f"{DOMAIN}.{input_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state is None
diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py
index cc226dc1d8741c..531606d67edb59 100644
--- a/tests/components/input_text/test_init.py
+++ b/tests/components/input_text/test_init.py
@@ -25,7 +25,7 @@
)
from homeassistant.core import Context, CoreState, State
from homeassistant.exceptions import Unauthorized
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from homeassistant.loader import bind_hass
from homeassistant.setup import async_setup_component
@@ -393,7 +393,7 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup):
input_id = "from_storage"
input_entity_id = f"{DOMAIN}.{input_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state is not None
@@ -419,7 +419,7 @@ async def test_update(hass, hass_ws_client, storage_setup):
input_id = "from_storage"
input_entity_id = f"{DOMAIN}.{input_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state.attributes[ATTR_FRIENDLY_NAME] == "from storage"
@@ -457,7 +457,7 @@ async def test_ws_create(hass, hass_ws_client, storage_setup):
input_id = "new_input"
input_entity_id = f"{DOMAIN}.{input_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state is None
diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py
index f1940b1eb3927f..1b08317ca303ca 100644
--- a/tests/components/insteon/test_config_flow.py
+++ b/tests/components/insteon/test_config_flow.py
@@ -369,13 +369,16 @@ async def test_options_add_device_override(hass: HomeAssistantType):
CONF_CAT: "05",
CONF_SUBCAT: "bb",
}
- await _options_form(hass, result2["flow_id"], user_input)
+ result3, _ = await _options_form(hass, result2["flow_id"], user_input)
assert len(config_entry.options[CONF_OVERRIDE]) == 2
assert config_entry.options[CONF_OVERRIDE][1][CONF_ADDRESS] == "4D.5E.6F"
assert config_entry.options[CONF_OVERRIDE][1][CONF_CAT] == 5
assert config_entry.options[CONF_OVERRIDE][1][CONF_SUBCAT] == 187
+ # If result1 eq result2 the changes will not save
+ assert result["data"] != result3["data"]
+
async def test_options_remove_device_override(hass: HomeAssistantType):
"""Test removing a device override."""
@@ -477,6 +480,9 @@ async def test_options_add_x10_device(hass: HomeAssistantType):
assert config_entry.options[CONF_X10][1][CONF_PLATFORM] == "binary_sensor"
assert config_entry.options[CONF_X10][1][CONF_DIM_STEPS] == 15
+ # If result2 eq result3 the changes will not save
+ assert result2["data"] != result3["data"]
+
async def test_options_remove_x10_device(hass: HomeAssistantType):
"""Test removing an X10 device."""
diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py
index 493bcfe28cf4b0..8c96b9a01d8745 100644
--- a/tests/components/ipma/test_config_flow.py
+++ b/tests/components/ipma/test_config_flow.py
@@ -4,7 +4,7 @@
from homeassistant.components.ipma import DOMAIN, config_flow
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from .test_weather import MockLocation
@@ -143,13 +143,13 @@ async def test_config_entry_migration(hass):
mock_registry(
hass,
{
- "weather.hometown": entity_registry.RegistryEntry(
+ "weather.hometown": er.RegistryEntry(
entity_id="weather.hometown",
unique_id="0, 0",
platform="ipma",
config_entry_id=ipma_entry.entry_id,
),
- "weather.hometown_2": entity_registry.RegistryEntry(
+ "weather.hometown_2": er.RegistryEntry(
entity_id="weather.hometown_2",
unique_id="0, 0, hourly",
platform="ipma",
@@ -165,7 +165,7 @@ async def test_config_entry_migration(hass):
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
weather_home = ent_reg.async_get("weather.hometown")
assert weather_home.unique_id == "0, 0, daily"
diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py
index 69143faec64f16..9366b290feffd3 100644
--- a/tests/components/ipp/test_sensor.py
+++ b/tests/components/ipp/test_sensor.py
@@ -6,6 +6,7 @@
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from tests.components.ipp import init_integration, mock_connection
@@ -19,7 +20,7 @@ async def test_sensors(
mock_connection(aioclient_mock)
entry = await init_integration(hass, aioclient_mock, skip_setup=True)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
# Pre-create registry entries for disabled by default sensors
registry.async_get_or_create(
@@ -86,7 +87,7 @@ async def test_disabled_by_default_sensors(
) -> None:
"""Test the disabled by default IPP sensors."""
await init_integration(hass, aioclient_mock)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
state = hass.states.get("sensor.epson_xp_6000_series_uptime")
assert state is None
@@ -102,7 +103,7 @@ async def test_missing_entry_unique_id(
) -> None:
"""Test the unique_id of IPP sensor when printer is missing identifiers."""
entry = await init_integration(hass, aioclient_mock, uuid=None, unique_id=None)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entity = registry.async_get("sensor.epson_xp_6000_series")
assert entity
diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py
index 8986c2e6f53ecd..c21211962263b5 100644
--- a/tests/components/jewish_calendar/test_binary_sensor.py
+++ b/tests/components/jewish_calendar/test_binary_sensor.py
@@ -5,6 +5,7 @@
from homeassistant.components import jewish_calendar
from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -18,19 +19,118 @@
from tests.common import async_fire_time_changed
MELACHA_PARAMS = [
- make_nyc_test_params(dt(2018, 9, 1, 16, 0), STATE_ON),
- make_nyc_test_params(dt(2018, 9, 1, 20, 21), STATE_OFF),
- make_nyc_test_params(dt(2018, 9, 7, 13, 1), STATE_OFF),
- make_nyc_test_params(dt(2018, 9, 8, 21, 25), STATE_OFF),
- make_nyc_test_params(dt(2018, 9, 9, 21, 25), STATE_ON),
- make_nyc_test_params(dt(2018, 9, 10, 21, 25), STATE_ON),
- make_nyc_test_params(dt(2018, 9, 28, 21, 25), STATE_ON),
- make_nyc_test_params(dt(2018, 9, 29, 21, 25), STATE_OFF),
- make_nyc_test_params(dt(2018, 9, 30, 21, 25), STATE_ON),
- make_nyc_test_params(dt(2018, 10, 1, 21, 25), STATE_ON),
- make_jerusalem_test_params(dt(2018, 9, 29, 21, 25), STATE_OFF),
- make_jerusalem_test_params(dt(2018, 9, 30, 21, 25), STATE_ON),
- make_jerusalem_test_params(dt(2018, 10, 1, 21, 25), STATE_OFF),
+ make_nyc_test_params(
+ dt(2018, 9, 1, 16, 0),
+ {
+ "state": STATE_ON,
+ "update": dt(2018, 9, 1, 20, 14),
+ "new_state": STATE_OFF,
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 1, 20, 21),
+ {
+ "state": STATE_OFF,
+ "update": dt(2018, 9, 2, 6, 21),
+ "new_state": STATE_OFF,
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 7, 13, 1),
+ {
+ "state": STATE_OFF,
+ "update": dt(2018, 9, 7, 19, 4),
+ "new_state": STATE_ON,
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 8, 21, 25),
+ {
+ "state": STATE_OFF,
+ "update": dt(2018, 9, 9, 6, 27),
+ "new_state": STATE_OFF,
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 9, 21, 25),
+ {
+ "state": STATE_ON,
+ "update": dt(2018, 9, 10, 6, 28),
+ "new_state": STATE_ON,
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 10, 21, 25),
+ {
+ "state": STATE_ON,
+ "update": dt(2018, 9, 11, 6, 29),
+ "new_state": STATE_ON,
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 11, 11, 25),
+ {
+ "state": STATE_ON,
+ "update": dt(2018, 9, 11, 19, 57),
+ "new_state": STATE_OFF,
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 29, 16, 25),
+ {
+ "state": STATE_ON,
+ "update": dt(2018, 9, 29, 19, 25),
+ "new_state": STATE_OFF,
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 29, 21, 25),
+ {
+ "state": STATE_OFF,
+ "update": dt(2018, 9, 30, 6, 48),
+ "new_state": STATE_OFF,
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 30, 21, 25),
+ {
+ "state": STATE_ON,
+ "update": dt(2018, 10, 1, 6, 49),
+ "new_state": STATE_ON,
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 10, 1, 21, 25),
+ {
+ "state": STATE_ON,
+ "update": dt(2018, 10, 2, 6, 50),
+ "new_state": STATE_ON,
+ },
+ ),
+ make_jerusalem_test_params(
+ dt(2018, 9, 29, 21, 25),
+ {
+ "state": STATE_OFF,
+ "update": dt(2018, 9, 30, 6, 29),
+ "new_state": STATE_OFF,
+ },
+ ),
+ make_jerusalem_test_params(
+ dt(2018, 10, 1, 11, 25),
+ {
+ "state": STATE_ON,
+ "update": dt(2018, 10, 1, 19, 2),
+ "new_state": STATE_OFF,
+ },
+ ),
+ make_jerusalem_test_params(
+ dt(2018, 10, 1, 21, 25),
+ {
+ "state": STATE_OFF,
+ "update": dt(2018, 10, 2, 6, 31),
+ "new_state": STATE_OFF,
+ },
+ ),
]
MELACHA_TEST_IDS = [
@@ -39,7 +139,8 @@
"friday_upcoming_shabbat",
"upcoming_rosh_hashana",
"currently_rosh_hashana",
- "second_day_rosh_hashana",
+ "second_day_rosh_hashana_night",
+ "second_day_rosh_hashana_day",
"currently_shabbat_chol_hamoed",
"upcoming_two_day_yomtov_in_diaspora",
"currently_first_day_of_two_day_yomtov_in_diaspora",
@@ -84,7 +185,7 @@ async def test_issur_melacha_sensor(
hass.config.latitude = latitude
hass.config.longitude = longitude
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
with alter_time(test_time):
assert await async_setup_component(
@@ -102,13 +203,9 @@ async def test_issur_melacha_sensor(
)
await hass.async_block_till_done()
- future = dt_util.utcnow() + timedelta(seconds=30)
- async_fire_time_changed(hass, future)
- await hass.async_block_till_done()
-
assert (
hass.states.get("binary_sensor.test_issur_melacha_in_effect").state
- == result
+ == result["state"]
)
entity = registry.async_get("binary_sensor.test_issur_melacha_in_effect")
target_uid = "_".join(
@@ -128,3 +225,82 @@ async def test_issur_melacha_sensor(
)
)
assert entity.unique_id == target_uid
+
+ with alter_time(result["update"]):
+ async_fire_time_changed(hass, result["update"])
+ await hass.async_block_till_done()
+ assert (
+ hass.states.get("binary_sensor.test_issur_melacha_in_effect").state
+ == result["new_state"]
+ )
+
+
+@pytest.mark.parametrize(
+ [
+ "now",
+ "candle_lighting",
+ "havdalah",
+ "diaspora",
+ "tzname",
+ "latitude",
+ "longitude",
+ "result",
+ ],
+ [
+ make_nyc_test_params(
+ dt(2020, 10, 23, 17, 46, 59, 999999), [STATE_OFF, STATE_ON]
+ ),
+ make_nyc_test_params(
+ dt(2020, 10, 24, 18, 44, 59, 999999), [STATE_ON, STATE_OFF]
+ ),
+ ],
+ ids=["before_candle_lighting", "before_havdalah"],
+)
+async def test_issur_melacha_sensor_update(
+ hass,
+ legacy_patchable_time,
+ now,
+ candle_lighting,
+ havdalah,
+ diaspora,
+ tzname,
+ latitude,
+ longitude,
+ result,
+):
+ """Test Issur Melacha sensor output."""
+ time_zone = dt_util.get_time_zone(tzname)
+ test_time = time_zone.localize(now)
+
+ hass.config.time_zone = time_zone
+ hass.config.latitude = latitude
+ hass.config.longitude = longitude
+
+ with alter_time(test_time):
+ assert await async_setup_component(
+ hass,
+ jewish_calendar.DOMAIN,
+ {
+ "jewish_calendar": {
+ "name": "test",
+ "language": "english",
+ "diaspora": diaspora,
+ "candle_lighting_minutes_before_sunset": candle_lighting,
+ "havdalah_minutes_after_sunset": havdalah,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert (
+ hass.states.get("binary_sensor.test_issur_melacha_in_effect").state
+ == result[0]
+ )
+
+ test_time += timedelta(microseconds=1)
+ with alter_time(test_time):
+ async_fire_time_changed(hass, test_time)
+ await hass.async_block_till_done()
+ assert (
+ hass.states.get("binary_sensor.test_issur_melacha_in_effect").state
+ == result[1]
+ )
diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py
index 1c771c71a3a16b..a5c99c850b8e16 100644
--- a/tests/components/jewish_calendar/test_sensor.py
+++ b/tests/components/jewish_calendar/test_sensor.py
@@ -4,6 +4,7 @@
import pytest
from homeassistant.components import jewish_calendar
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -511,7 +512,7 @@ async def test_shabbat_times_sensor(
hass.config.latitude = latitude
hass.config.longitude = longitude
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
with alter_time(test_time):
assert await async_setup_component(
diff --git a/tests/components/keenetic_ndms2/__init__.py b/tests/components/keenetic_ndms2/__init__.py
new file mode 100644
index 00000000000000..1fce0dbe2a6fc5
--- /dev/null
+++ b/tests/components/keenetic_ndms2/__init__.py
@@ -0,0 +1,27 @@
+"""Tests for the Keenetic NDMS2 component."""
+from homeassistant.components.keenetic_ndms2 import const
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_SCAN_INTERVAL,
+ CONF_USERNAME,
+)
+
+MOCK_NAME = "Keenetic Ultra 2030"
+
+MOCK_DATA = {
+ CONF_HOST: "0.0.0.0",
+ CONF_USERNAME: "user",
+ CONF_PASSWORD: "pass",
+ CONF_PORT: 23,
+}
+
+MOCK_OPTIONS = {
+ CONF_SCAN_INTERVAL: 15,
+ const.CONF_CONSIDER_HOME: 150,
+ const.CONF_TRY_HOTSPOT: False,
+ const.CONF_INCLUDE_ARP: True,
+ const.CONF_INCLUDE_ASSOCIATED: True,
+ const.CONF_INTERFACES: ["Home", "VPS0"],
+}
diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py
new file mode 100644
index 00000000000000..aa5369fdc0a366
--- /dev/null
+++ b/tests/components/keenetic_ndms2/test_config_flow.py
@@ -0,0 +1,169 @@
+"""Test Keenetic NDMS2 setup process."""
+
+from unittest.mock import Mock, patch
+
+from ndms2_client import ConnectionException
+from ndms2_client.client import InterfaceInfo, RouterInfo
+import pytest
+
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.components import keenetic_ndms2 as keenetic
+from homeassistant.components.keenetic_ndms2 import const
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import MOCK_DATA, MOCK_NAME, MOCK_OPTIONS
+
+from tests.common import MockConfigEntry
+
+
+@pytest.fixture(name="connect")
+def mock_keenetic_connect():
+ """Mock connection routine."""
+ with patch("ndms2_client.client.Client.get_router_info") as mock_get_router_info:
+ mock_get_router_info.return_value = RouterInfo(
+ name=MOCK_NAME,
+ fw_version="3.0.4",
+ fw_channel="stable",
+ model="mock",
+ hw_version="0000",
+ manufacturer="pytest",
+ vendor="foxel",
+ region="RU",
+ )
+ yield
+
+
+@pytest.fixture(name="connect_error")
+def mock_keenetic_connect_failed():
+ """Mock connection routine."""
+ with patch(
+ "ndms2_client.client.Client.get_router_info",
+ side_effect=ConnectionException("Mocked failure"),
+ ):
+ yield
+
+
+async def test_flow_works(hass: HomeAssistantType, connect):
+ """Test config flow."""
+
+ result = await hass.config_entries.flow.async_init(
+ keenetic.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+ with patch(
+ "homeassistant.components.keenetic_ndms2.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.keenetic_ndms2.async_setup_entry", return_value=True
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input=MOCK_DATA,
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result2["title"] == MOCK_NAME
+ assert result2["data"] == MOCK_DATA
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_import_works(hass: HomeAssistantType, connect):
+ """Test config flow."""
+
+ with patch(
+ "homeassistant.components.keenetic_ndms2.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.keenetic_ndms2.async_setup_entry", return_value=True
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_init(
+ keenetic.DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data=MOCK_DATA,
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == MOCK_NAME
+ assert result["data"] == MOCK_DATA
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_options(hass):
+ """Test updating options."""
+ entry = MockConfigEntry(domain=keenetic.DOMAIN, data=MOCK_DATA)
+ entry.add_to_hass(hass)
+ with patch(
+ "homeassistant.components.keenetic_ndms2.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.keenetic_ndms2.async_setup_entry", return_value=True
+ ) as mock_setup_entry:
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+ # fake router
+ hass.data.setdefault(keenetic.DOMAIN, {})
+ hass.data[keenetic.DOMAIN][entry.entry_id] = {
+ keenetic.ROUTER: Mock(
+ client=Mock(
+ get_interfaces=Mock(
+ return_value=[
+ InterfaceInfo.from_dict({"id": name, "type": "bridge"})
+ for name in MOCK_OPTIONS[const.CONF_INTERFACES]
+ ]
+ )
+ )
+ )
+ }
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+ result2 = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input=MOCK_OPTIONS,
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result2["data"] == MOCK_OPTIONS
+
+
+async def test_host_already_configured(hass, connect):
+ """Test host already configured."""
+
+ entry = MockConfigEntry(
+ domain=keenetic.DOMAIN, data=MOCK_DATA, options=MOCK_OPTIONS
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ keenetic.DOMAIN, context={"source": "user"}
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input=MOCK_DATA
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result2["reason"] == "already_configured"
+
+
+async def test_connection_error(hass, connect_error):
+ """Test error when connection is unsuccessful."""
+
+ result = await hass.config_entries.flow.async_init(
+ keenetic.DOMAIN, context={"source": "user"}
+ )
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input=MOCK_DATA
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "cannot_connect"}
diff --git a/tests/components/kira/test_sensor.py b/tests/components/kira/test_sensor.py
index cd4bee60ae6dfc..b835a25ae9095b 100644
--- a/tests/components/kira/test_sensor.py
+++ b/tests/components/kira/test_sensor.py
@@ -47,4 +47,4 @@ def test_kira_sensor_callback(self):
sensor._update_callback(codeTuple)
assert sensor.state == codeName
- assert sensor.device_state_attributes == {kira.CONF_DEVICE: deviceName}
+ assert sensor.extra_state_attributes == {kira.CONF_DEVICE: deviceName}
diff --git a/tests/components/kmtronic/__init__.py b/tests/components/kmtronic/__init__.py
new file mode 100644
index 00000000000000..2f089d6495f049
--- /dev/null
+++ b/tests/components/kmtronic/__init__.py
@@ -0,0 +1 @@
+"""Tests for the kmtronic integration."""
diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py
new file mode 100644
index 00000000000000..b5ebdc79c8ba44
--- /dev/null
+++ b/tests/components/kmtronic/test_config_flow.py
@@ -0,0 +1,155 @@
+"""Test the kmtronic config flow."""
+from unittest.mock import Mock, patch
+
+from aiohttp import ClientConnectorError, ClientResponseError
+
+from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.components.kmtronic.const import CONF_REVERSE, DOMAIN
+from homeassistant.config_entries import ENTRY_STATE_LOADED
+
+from tests.common import MockConfigEntry
+
+
+async def test_form(hass):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.kmtronic.config_flow.KMTronicHubAPI.async_get_status",
+ return_value=[Mock()],
+ ), patch(
+ "homeassistant.components.kmtronic.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.kmtronic.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ )
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "1.1.1.1"
+ assert result2["data"] == {
+ "host": "1.1.1.1",
+ "username": "test-username",
+ "password": "test-password",
+ }
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_options(hass, aioclient_mock):
+ """Test that the options form."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "host": "1.1.1.1",
+ "username": "admin",
+ "password": "admin",
+ },
+ )
+ config_entry.add_to_hass(hass)
+
+ aioclient_mock.get(
+ "http://1.1.1.1/status.xml",
+ text="00",
+ )
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert config_entry.state == ENTRY_STATE_LOADED
+
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={CONF_REVERSE: True}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert config_entry.options == {CONF_REVERSE: True}
+
+ await hass.async_block_till_done()
+
+ assert config_entry.state == "loaded"
+
+
+async def test_form_invalid_auth(hass):
+ """Test we handle invalid auth."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.kmtronic.config_flow.KMTronicHubAPI.async_get_status",
+ side_effect=ClientResponseError(None, None, status=401),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "invalid_auth"}
+
+
+async def test_form_cannot_connect(hass):
+ """Test we handle cannot connect error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.kmtronic.config_flow.KMTronicHubAPI.async_get_status",
+ side_effect=ClientConnectorError(None, Mock()),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "cannot_connect"}
+
+
+async def test_form_unknown_error(hass):
+ """Test we handle unknown errors."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.kmtronic.config_flow.KMTronicHubAPI.async_get_status",
+ side_effect=Exception(),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "unknown"}
diff --git a/tests/components/kmtronic/test_init.py b/tests/components/kmtronic/test_init.py
new file mode 100644
index 00000000000000..1b9cf7cb40713a
--- /dev/null
+++ b/tests/components/kmtronic/test_init.py
@@ -0,0 +1,62 @@
+"""The tests for the KMtronic component."""
+import asyncio
+
+from homeassistant.components.kmtronic.const import DOMAIN
+from homeassistant.config_entries import (
+ ENTRY_STATE_LOADED,
+ ENTRY_STATE_NOT_LOADED,
+ ENTRY_STATE_SETUP_RETRY,
+)
+
+from tests.common import MockConfigEntry
+
+
+async def test_unload_config_entry(hass, aioclient_mock):
+ """Test entry unloading."""
+
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "host": "1.1.1.1",
+ "username": "admin",
+ "password": "admin",
+ },
+ )
+ config_entry.add_to_hass(hass)
+
+ aioclient_mock.get(
+ "http://1.1.1.1/status.xml",
+ text="00",
+ )
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert config_entry.state == ENTRY_STATE_LOADED
+
+ await hass.config_entries.async_unload(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert config_entry.state == ENTRY_STATE_NOT_LOADED
+
+
+async def test_config_entry_not_ready(hass, aioclient_mock):
+ """Tests configuration entry not ready."""
+
+ aioclient_mock.get(
+ "http://1.1.1.1/status.xml",
+ exc=asyncio.TimeoutError(),
+ )
+
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "host": "1.1.1.1",
+ "username": "foo",
+ "password": "bar",
+ },
+ )
+ config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert config_entry.state == ENTRY_STATE_SETUP_RETRY
diff --git a/tests/components/kmtronic/test_switch.py b/tests/components/kmtronic/test_switch.py
new file mode 100644
index 00000000000000..df8ecda2c2ec95
--- /dev/null
+++ b/tests/components/kmtronic/test_switch.py
@@ -0,0 +1,179 @@
+"""The tests for the KMtronic switch platform."""
+import asyncio
+from datetime import timedelta
+
+from homeassistant.components.kmtronic.const import DOMAIN
+from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.setup import async_setup_component
+from homeassistant.util import dt as dt_util
+
+from tests.common import MockConfigEntry, async_fire_time_changed
+
+
+async def test_relay_on_off(hass, aioclient_mock):
+ """Tests the relay turns on correctly."""
+
+ aioclient_mock.get(
+ "http://1.1.1.1/status.xml",
+ text="00",
+ )
+
+ MockConfigEntry(
+ domain=DOMAIN, data={"host": "1.1.1.1", "username": "foo", "password": "bar"}
+ ).add_to_hass(hass)
+ assert await async_setup_component(hass, DOMAIN, {})
+ await hass.async_block_till_done()
+
+ # Mocks the response for turning a relay1 on
+ aioclient_mock.get(
+ "http://1.1.1.1/FF0101",
+ text="",
+ )
+
+ state = hass.states.get("switch.relay1")
+ assert state.state == "off"
+
+ await hass.services.async_call(
+ "switch", "turn_on", {"entity_id": "switch.relay1"}, blocking=True
+ )
+
+ await hass.async_block_till_done()
+ state = hass.states.get("switch.relay1")
+ assert state.state == "on"
+
+ # Mocks the response for turning a relay1 off
+ aioclient_mock.get(
+ "http://1.1.1.1/FF0100",
+ text="",
+ )
+
+ await hass.services.async_call(
+ "switch", "turn_off", {"entity_id": "switch.relay1"}, blocking=True
+ )
+
+ await hass.async_block_till_done()
+ state = hass.states.get("switch.relay1")
+ assert state.state == "off"
+
+
+async def test_update(hass, aioclient_mock):
+ """Tests switch refreshes status periodically."""
+ now = dt_util.utcnow()
+ future = now + timedelta(minutes=10)
+
+ aioclient_mock.get(
+ "http://1.1.1.1/status.xml",
+ text="00",
+ )
+
+ MockConfigEntry(
+ domain=DOMAIN, data={"host": "1.1.1.1", "username": "foo", "password": "bar"}
+ ).add_to_hass(hass)
+ assert await async_setup_component(hass, DOMAIN, {})
+
+ await hass.async_block_till_done()
+ state = hass.states.get("switch.relay1")
+ assert state.state == "off"
+
+ aioclient_mock.clear_requests()
+ aioclient_mock.get(
+ "http://1.1.1.1/status.xml",
+ text="11",
+ )
+ async_fire_time_changed(hass, future)
+
+ await hass.async_block_till_done()
+ state = hass.states.get("switch.relay1")
+ assert state.state == "on"
+
+
+async def test_failed_update(hass, aioclient_mock):
+ """Tests coordinator update fails."""
+ now = dt_util.utcnow()
+ future = now + timedelta(minutes=10)
+
+ aioclient_mock.get(
+ "http://1.1.1.1/status.xml",
+ text="00",
+ )
+
+ MockConfigEntry(
+ domain=DOMAIN, data={"host": "1.1.1.1", "username": "foo", "password": "bar"}
+ ).add_to_hass(hass)
+ assert await async_setup_component(hass, DOMAIN, {})
+
+ await hass.async_block_till_done()
+ state = hass.states.get("switch.relay1")
+ assert state.state == "off"
+
+ aioclient_mock.clear_requests()
+ aioclient_mock.get(
+ "http://1.1.1.1/status.xml",
+ text="401 Unauthorized: Password required",
+ status=401,
+ )
+ async_fire_time_changed(hass, future)
+
+ await hass.async_block_till_done()
+ state = hass.states.get("switch.relay1")
+ assert state.state == STATE_UNAVAILABLE
+
+ future += timedelta(minutes=10)
+ aioclient_mock.clear_requests()
+ aioclient_mock.get(
+ "http://1.1.1.1/status.xml",
+ exc=asyncio.TimeoutError(),
+ )
+ async_fire_time_changed(hass, future)
+
+ await hass.async_block_till_done()
+ state = hass.states.get("switch.relay1")
+ assert state.state == STATE_UNAVAILABLE
+
+
+async def test_relay_on_off_reversed(hass, aioclient_mock):
+ """Tests the relay turns on correctly when configured as reverse."""
+
+ aioclient_mock.get(
+ "http://1.1.1.1/status.xml",
+ text="00",
+ )
+
+ MockConfigEntry(
+ domain=DOMAIN,
+ data={"host": "1.1.1.1", "username": "foo", "password": "bar"},
+ options={"reverse": True},
+ ).add_to_hass(hass)
+ assert await async_setup_component(hass, DOMAIN, {})
+ await hass.async_block_till_done()
+
+ # Mocks the response for turning a relay1 off
+ aioclient_mock.get(
+ "http://1.1.1.1/FF0101",
+ text="",
+ )
+
+ state = hass.states.get("switch.relay1")
+ assert state.state == "on"
+
+ await hass.services.async_call(
+ "switch", "turn_off", {"entity_id": "switch.relay1"}, blocking=True
+ )
+
+ await hass.async_block_till_done()
+ state = hass.states.get("switch.relay1")
+ assert state.state == "off"
+
+ # Mocks the response for turning a relay1 off
+ aioclient_mock.get(
+ "http://1.1.1.1/FF0100",
+ text="",
+ )
+
+ await hass.services.async_call(
+ "switch", "turn_on", {"entity_id": "switch.relay1"}, blocking=True
+ )
+
+ await hass.async_block_till_done()
+ state = hass.states.get("switch.relay1")
+ assert state.state == "on"
diff --git a/tests/components/knx/__init__.py b/tests/components/knx/__init__.py
new file mode 100644
index 00000000000000..eaa84714dc5a3a
--- /dev/null
+++ b/tests/components/knx/__init__.py
@@ -0,0 +1 @@
+"""Tests for the KNX integration."""
diff --git a/tests/components/knx/test_expose.py b/tests/components/knx/test_expose.py
new file mode 100644
index 00000000000000..1590a7bb2465e6
--- /dev/null
+++ b/tests/components/knx/test_expose.py
@@ -0,0 +1,115 @@
+"""Test knx expose."""
+
+from unittest.mock import AsyncMock, Mock, patch
+
+import pytest
+
+from homeassistant.components.knx import (
+ CONF_KNX_EXPOSE,
+ CONFIG_SCHEMA as KNX_CONFIG_SCHEMA,
+ KNX_ADDRESS,
+)
+from homeassistant.components.knx.const import DOMAIN as KNX_DOMAIN
+from homeassistant.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_TYPE
+from homeassistant.setup import async_setup_component
+
+
+async def setup_knx_integration(hass, knx_mock, config=None):
+ """Create the KNX gateway."""
+ if config is None:
+ config = {}
+ with patch("homeassistant.components.knx.XKNX", return_value=knx_mock):
+ await async_setup_component(
+ hass, KNX_DOMAIN, KNX_CONFIG_SCHEMA({KNX_DOMAIN: config})
+ )
+ await hass.async_block_till_done()
+
+
+@pytest.fixture(autouse=True)
+def xknx_mock():
+ """Create a simple XKNX mock."""
+ xknx_mock = Mock()
+ xknx_mock.telegrams = AsyncMock()
+ xknx_mock.start = AsyncMock()
+ xknx_mock.stop = AsyncMock()
+ return xknx_mock
+
+
+async def test_binary_expose(hass, xknx_mock):
+ """Test that a binary expose sends only telegrams on state change."""
+ entity_id = "fake.entity"
+ await setup_knx_integration(
+ hass,
+ xknx_mock,
+ {
+ CONF_KNX_EXPOSE: {
+ CONF_TYPE: "binary",
+ KNX_ADDRESS: "1/1/8",
+ CONF_ENTITY_ID: entity_id,
+ }
+ },
+ )
+ assert not hass.states.async_all()
+
+ # Change state to on
+ xknx_mock.reset_mock()
+ hass.states.async_set(entity_id, "on", {})
+ await hass.async_block_till_done()
+ assert xknx_mock.telegrams.put.call_count == 1, "Expected telegram for state change"
+
+ # Change attribute; keep state
+ xknx_mock.reset_mock()
+ hass.states.async_set(entity_id, "on", {"brightness": 180})
+ await hass.async_block_till_done()
+ assert (
+ xknx_mock.telegrams.put.call_count == 0
+ ), "Expected no telegram; state not changed"
+
+ # Change attribute and state
+ xknx_mock.reset_mock()
+ hass.states.async_set(entity_id, "off", {"brightness": 0})
+ await hass.async_block_till_done()
+ assert xknx_mock.telegrams.put.call_count == 1, "Expected telegram for state change"
+
+
+async def test_expose_attribute(hass, xknx_mock):
+ """Test that an expose sends only telegrams on attribute change."""
+ entity_id = "fake.entity"
+ attribute = "fake_attribute"
+ await setup_knx_integration(
+ hass,
+ xknx_mock,
+ {
+ CONF_KNX_EXPOSE: {
+ CONF_TYPE: "percentU8",
+ KNX_ADDRESS: "1/1/8",
+ CONF_ENTITY_ID: entity_id,
+ CONF_ATTRIBUTE: attribute,
+ }
+ },
+ )
+ assert not hass.states.async_all()
+
+ # Change state to on; no attribute
+ xknx_mock.reset_mock()
+ hass.states.async_set(entity_id, "on", {})
+ await hass.async_block_till_done()
+ assert xknx_mock.telegrams.put.call_count == 0
+
+ # Change attribute; keep state
+ xknx_mock.reset_mock()
+ hass.states.async_set(entity_id, "on", {attribute: 1})
+ await hass.async_block_till_done()
+ assert xknx_mock.telegrams.put.call_count == 1
+
+ # Change state keep attribute
+ xknx_mock.reset_mock()
+ hass.states.async_set(entity_id, "off", {attribute: 1})
+ await hass.async_block_till_done()
+ assert xknx_mock.telegrams.put.call_count == 0
+
+ # Change state and attribute
+ xknx_mock.reset_mock()
+ hass.states.async_set(entity_id, "on", {attribute: 0})
+ await hass.async_block_till_done()
+ assert xknx_mock.telegrams.put.call_count == 1
diff --git a/tests/components/kodi/test_device_trigger.py b/tests/components/kodi/test_device_trigger.py
index 8cf6c635393d53..462d2381cec2f6 100644
--- a/tests/components/kodi/test_device_trigger.py
+++ b/tests/components/kodi/test_device_trigger.py
@@ -10,13 +10,12 @@
from tests.common import (
MockConfigEntry,
- assert_lists_same,
async_get_device_automations,
async_mock_service,
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
@@ -69,8 +68,13 @@ async def test_get_triggers(hass, device_reg, entity_reg):
"entity_id": f"{MP_DOMAIN}.kodi_5678",
},
]
+
+ # Test triggers are either kodi specific triggers or media_player entity triggers
triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
- assert_lists_same(triggers, expected_triggers)
+ for expected_trigger in expected_triggers:
+ assert expected_trigger in triggers
+ for trigger in triggers:
+ assert trigger in expected_triggers or trigger["domain"] == "media_player"
async def test_if_fires_on_state_change(hass, calls, kodi_media_player):
@@ -91,7 +95,9 @@ async def test_if_fires_on_state_change(hass, calls, kodi_media_player):
"action": {
"service": "test.automation",
"data_template": {
- "some": ("turn_on - {{ trigger.entity_id }}")
+ "some": (
+ "turn_on - {{ trigger.entity_id }} - {{ trigger.id}}"
+ )
},
},
},
@@ -106,7 +112,9 @@ async def test_if_fires_on_state_change(hass, calls, kodi_media_player):
"action": {
"service": "test.automation",
"data_template": {
- "some": ("turn_off - {{ trigger.entity_id }}")
+ "some": (
+ "turn_off - {{ trigger.entity_id }} - {{ trigger.id}}"
+ )
},
},
},
@@ -124,7 +132,7 @@ async def test_if_fires_on_state_change(hass, calls, kodi_media_player):
await hass.async_block_till_done()
assert len(calls) == 1
- assert calls[0].data["some"] == f"turn_on - {kodi_media_player}"
+ assert calls[0].data["some"] == f"turn_on - {kodi_media_player} - 0"
await hass.services.async_call(
MP_DOMAIN,
@@ -135,4 +143,4 @@ async def test_if_fires_on_state_change(hass, calls, kodi_media_player):
await hass.async_block_till_done()
assert len(calls) == 2
- assert calls[1].data["some"] == f"turn_off - {kodi_media_player}"
+ assert calls[1].data["some"] == f"turn_off - {kodi_media_player} - 0"
diff --git a/tests/components/kostal_plenticore/__init__.py b/tests/components/kostal_plenticore/__init__.py
new file mode 100644
index 00000000000000..bba546eea11791
--- /dev/null
+++ b/tests/components/kostal_plenticore/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Kostal Plenticore Solar Inverter integration."""
diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py
new file mode 100644
index 00000000000000..04a69892b43810
--- /dev/null
+++ b/tests/components/kostal_plenticore/test_config_flow.py
@@ -0,0 +1,206 @@
+"""Test the Kostal Plenticore Solar Inverter config flow."""
+import asyncio
+from unittest.mock import ANY, AsyncMock, MagicMock, patch
+
+from kostal.plenticore import PlenticoreAuthenticationException
+
+from homeassistant import config_entries, setup
+from homeassistant.components.kostal_plenticore import config_flow
+from homeassistant.components.kostal_plenticore.const import DOMAIN
+
+from tests.common import MockConfigEntry
+
+
+async def test_formx(hass):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.kostal_plenticore.config_flow.PlenticoreApiClient"
+ ) as mock_api_class, patch(
+ "homeassistant.components.kostal_plenticore.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.kostal_plenticore.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ # mock of the context manager instance
+ mock_api_ctx = MagicMock()
+ mock_api_ctx.login = AsyncMock()
+ mock_api_ctx.get_setting_values = AsyncMock(
+ return_value={"scb:network": {"Hostname": "scb"}}
+ )
+
+ # mock of the return instance of PlenticoreApiClient
+ mock_api = MagicMock()
+ mock_api.__aenter__.return_value = mock_api_ctx
+ mock_api.__aexit__ = AsyncMock()
+
+ mock_api_class.return_value = mock_api
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "password": "test-password",
+ },
+ )
+
+ mock_api_class.assert_called_once_with(ANY, "1.1.1.1")
+ mock_api.__aenter__.assert_called_once()
+ mock_api.__aexit__.assert_called_once()
+ mock_api_ctx.login.assert_called_once_with("test-password")
+ mock_api_ctx.get_setting_values.assert_called_once()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "scb"
+ assert result2["data"] == {
+ "host": "1.1.1.1",
+ "password": "test-password",
+ }
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_invalid_auth(hass):
+ """Test we handle invalid auth."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.kostal_plenticore.config_flow.PlenticoreApiClient"
+ ) as mock_api_class:
+ # mock of the context manager instance
+ mock_api_ctx = MagicMock()
+ mock_api_ctx.login = AsyncMock(
+ side_effect=PlenticoreAuthenticationException(404, "invalid user"),
+ )
+
+ # mock of the return instance of PlenticoreApiClient
+ mock_api = MagicMock()
+ mock_api.__aenter__.return_value = mock_api_ctx
+ mock_api.__aexit__.return_value = None
+
+ mock_api_class.return_value = mock_api
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "password": "test-password",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"password": "invalid_auth"}
+
+
+async def test_form_cannot_connect(hass):
+ """Test we handle cannot connect error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.kostal_plenticore.config_flow.PlenticoreApiClient"
+ ) as mock_api_class:
+ # mock of the context manager instance
+ mock_api_ctx = MagicMock()
+ mock_api_ctx.login = AsyncMock(
+ side_effect=asyncio.TimeoutError(),
+ )
+
+ # mock of the return instance of PlenticoreApiClient
+ mock_api = MagicMock()
+ mock_api.__aenter__.return_value = mock_api_ctx
+ mock_api.__aexit__.return_value = None
+
+ mock_api_class.return_value = mock_api
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "password": "test-password",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"host": "cannot_connect"}
+
+
+async def test_form_unexpected_error(hass):
+ """Test we handle unexpected error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.kostal_plenticore.config_flow.PlenticoreApiClient"
+ ) as mock_api_class:
+ # mock of the context manager instance
+ mock_api_ctx = MagicMock()
+ mock_api_ctx.login = AsyncMock(
+ side_effect=Exception(),
+ )
+
+ # mock of the return instance of PlenticoreApiClient
+ mock_api = MagicMock()
+ mock_api.__aenter__.return_value = mock_api_ctx
+ mock_api.__aexit__.return_value = None
+
+ mock_api_class.return_value = mock_api
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "password": "test-password",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "unknown"}
+
+
+async def test_already_configured(hass):
+ """Test we handle already configured error."""
+ MockConfigEntry(
+ domain="kostal_plenticore",
+ data={"host": "1.1.1.1", "password": "foobar"},
+ unique_id="112233445566",
+ ).add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "password": "test-password",
+ },
+ )
+
+ assert result2["type"] == "abort"
+ assert result2["reason"] == "already_configured"
+
+
+def test_configured_instances(hass):
+ """Test configured_instances returns all configured hosts."""
+ MockConfigEntry(
+ domain="kostal_plenticore",
+ data={"host": "2.2.2.2", "password": "foobar"},
+ unique_id="112233445566",
+ ).add_to_hass(hass)
+
+ result = config_flow.configured_instances(hass)
+
+ assert result == {"2.2.2.2"}
diff --git a/tests/components/kulersky/test_config_flow.py b/tests/components/kulersky/test_config_flow.py
index 24f3f9a010e4b7..c6933a01d3a914 100644
--- a/tests/components/kulersky/test_config_flow.py
+++ b/tests/components/kulersky/test_config_flow.py
@@ -1,5 +1,5 @@
"""Test the Kuler Sky config flow."""
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
import pykulersky
@@ -16,14 +16,12 @@ async def test_flow_success(hass):
assert result["type"] == "form"
assert result["errors"] is None
+ light = MagicMock(spec=pykulersky.Light)
+ light.address = "AA:BB:CC:11:22:33"
+ light.name = "Bedroom"
with patch(
- "homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices",
- return_value=[
- {
- "address": "AA:BB:CC:11:22:33",
- "name": "Bedroom",
- }
- ],
+ "homeassistant.components.kulersky.config_flow.pykulersky.discover",
+ return_value=[light],
), patch(
"homeassistant.components.kulersky.async_setup", return_value=True
) as mock_setup, patch(
@@ -54,7 +52,7 @@ async def test_flow_no_devices_found(hass):
assert result["errors"] is None
with patch(
- "homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices",
+ "homeassistant.components.kulersky.config_flow.pykulersky.discover",
return_value=[],
), patch(
"homeassistant.components.kulersky.async_setup", return_value=True
@@ -84,7 +82,7 @@ async def test_flow_exceptions_caught(hass):
assert result["errors"] is None
with patch(
- "homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices",
+ "homeassistant.components.kulersky.config_flow.pykulersky.discover",
side_effect=pykulersky.PykulerskyException("TEST"),
), patch(
"homeassistant.components.kulersky.async_setup", return_value=True
diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py
index fd5db92908bc5c..ea5eeb5a690567 100644
--- a/tests/components/kulersky/test_light.py
+++ b/tests/components/kulersky/test_light.py
@@ -1,18 +1,26 @@
"""Test the Kuler Sky lights."""
-import asyncio
from unittest.mock import MagicMock, patch
import pykulersky
import pytest
from homeassistant import setup
-from homeassistant.components.kulersky.light import DOMAIN
+from homeassistant.components.kulersky.const import (
+ DATA_ADDRESSES,
+ DATA_DISCOVERY_SUBSCRIPTION,
+ DOMAIN,
+)
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
+ ATTR_COLOR_MODE,
ATTR_HS_COLOR,
ATTR_RGB_COLOR,
+ ATTR_RGBW_COLOR,
+ ATTR_SUPPORTED_COLOR_MODES,
ATTR_WHITE_VALUE,
ATTR_XY_COLOR,
+ COLOR_MODE_HS,
+ COLOR_MODE_RGBW,
SCAN_INTERVAL,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
@@ -45,28 +53,17 @@ async def mock_light(hass, mock_entry):
light = MagicMock(spec=pykulersky.Light)
light.address = "AA:BB:CC:11:22:33"
light.name = "Bedroom"
- light.connected = False
+ light.connect.return_value = True
+ light.get_color.return_value = (0, 0, 0, 0)
with patch(
- "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices",
- return_value=[
- {
- "address": "AA:BB:CC:11:22:33",
- "name": "Bedroom",
- }
- ],
+ "homeassistant.components.kulersky.light.pykulersky.discover",
+ return_value=[light],
):
- with patch(
- "homeassistant.components.kulersky.light.pykulersky.Light",
- return_value=light,
- ), patch.object(light, "connect") as mock_connect, patch.object(
- light, "get_color", return_value=(0, 0, 0, 0)
- ):
- mock_entry.add_to_hass(hass)
- await hass.config_entries.async_setup(mock_entry.entry_id)
- await hass.async_block_till_done()
-
- assert mock_connect.called
- light.connected = True
+ mock_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert light.connect.called
yield light
@@ -77,118 +74,44 @@ async def test_init(hass, mock_light):
assert state.state == STATE_OFF
assert state.attributes == {
ATTR_FRIENDLY_NAME: "Bedroom",
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW],
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS
| SUPPORT_COLOR
| SUPPORT_WHITE_VALUE,
}
- with patch.object(hass.loop, "stop"), patch.object(
- mock_light, "disconnect"
- ) as mock_disconnect:
+ with patch.object(hass.loop, "stop"):
await hass.async_stop()
await hass.async_block_till_done()
- assert mock_disconnect.called
-
-
-async def test_discovery_lock(hass, mock_entry):
- """Test discovery lock."""
- await setup.async_setup_component(hass, "persistent_notification", {})
-
- discovery_finished = None
- first_discovery_started = asyncio.Event()
-
- async def mock_discovery(*args):
- """Block to simulate multiple discovery calls while one still running."""
- nonlocal discovery_finished
- if discovery_finished:
- first_discovery_started.set()
- await discovery_finished.wait()
- return []
-
- with patch(
- "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices",
- return_value=[],
- ), patch(
- "homeassistant.components.kulersky.light.async_track_time_interval",
- ) as mock_track_time_interval:
- mock_entry.add_to_hass(hass)
- await hass.config_entries.async_setup(mock_entry.entry_id)
- await hass.async_block_till_done()
-
- with patch.object(
- hass, "async_add_executor_job", side_effect=mock_discovery
- ) as mock_run_discovery:
- discovery_coroutine = mock_track_time_interval.call_args[0][1]
-
- discovery_finished = asyncio.Event()
+ assert mock_light.disconnect.called
- # Schedule multiple discoveries
- hass.async_create_task(discovery_coroutine())
- hass.async_create_task(discovery_coroutine())
- hass.async_create_task(discovery_coroutine())
- # Wait until the first discovery call is blocked
- await first_discovery_started.wait()
+async def test_remove_entry(hass, mock_light, mock_entry):
+ """Test platform setup."""
+ assert hass.data[DOMAIN][DATA_ADDRESSES] == {"AA:BB:CC:11:22:33"}
+ assert DATA_DISCOVERY_SUBSCRIPTION in hass.data[DOMAIN]
- # Unblock the first discovery
- discovery_finished.set()
+ await hass.config_entries.async_remove(mock_entry.entry_id)
- # Flush the remaining jobs
- await hass.async_block_till_done()
+ assert mock_light.disconnect.called
+ assert DOMAIN not in hass.data
- # The discovery method should only have been called once
- mock_run_discovery.assert_called_once()
+async def test_remove_entry_exceptions_caught(hass, mock_light, mock_entry):
+ """Assert that disconnect exceptions are caught."""
+ mock_light.disconnect.side_effect = pykulersky.PykulerskyException("Mock error")
+ await hass.config_entries.async_remove(mock_entry.entry_id)
-async def test_discovery_connection_error(hass, mock_entry):
- """Test that invalid devices are skipped."""
- await setup.async_setup_component(hass, "persistent_notification", {})
-
- light = MagicMock(spec=pykulersky.Light)
- light.address = "AA:BB:CC:11:22:33"
- light.name = "Bedroom"
- light.connected = False
- with patch(
- "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices",
- return_value=[
- {
- "address": "AA:BB:CC:11:22:33",
- "name": "Bedroom",
- }
- ],
- ):
- with patch(
- "homeassistant.components.kulersky.light.pykulersky.Light"
- ) as mockdevice, patch.object(
- light, "connect", side_effect=pykulersky.PykulerskyException
- ):
- mockdevice.return_value = light
- mock_entry.add_to_hass(hass)
- await hass.config_entries.async_setup(mock_entry.entry_id)
- await hass.async_block_till_done()
-
- # Assert entity was not added
- state = hass.states.get("light.bedroom")
- assert state is None
-
-
-async def test_remove_entry(hass, mock_light, mock_entry):
- """Test platform setup."""
- with patch.object(mock_light, "disconnect") as mock_disconnect:
- await hass.config_entries.async_remove(mock_entry.entry_id)
-
- assert mock_disconnect.called
+ assert mock_light.disconnect.called
async def test_update_exception(hass, mock_light):
"""Test platform setup."""
await setup.async_setup_component(hass, "persistent_notification", {})
- with patch.object(
- mock_light, "get_color", side_effect=pykulersky.PykulerskyException
- ):
- await hass.helpers.entity_component.async_update_entity("light.bedroom")
+ mock_light.get_color.side_effect = pykulersky.PykulerskyException
+ await hass.helpers.entity_component.async_update_entity("light.bedroom")
state = hass.states.get("light.bedroom")
assert state is not None
assert state.state == STATE_UNAVAILABLE
@@ -196,69 +119,59 @@ async def test_update_exception(hass, mock_light):
async def test_light_turn_on(hass, mock_light):
"""Test KulerSkyLight turn_on."""
- with patch.object(mock_light, "set_color") as mock_set_color, patch.object(
- mock_light, "get_color", return_value=(255, 255, 255, 255)
- ):
- await hass.services.async_call(
- "light",
- "turn_on",
- {ATTR_ENTITY_ID: "light.bedroom"},
- blocking=True,
- )
- await hass.async_block_till_done()
- mock_set_color.assert_called_with(255, 255, 255, 255)
-
- with patch.object(mock_light, "set_color") as mock_set_color, patch.object(
- mock_light, "get_color", return_value=(50, 50, 50, 255)
- ):
- await hass.services.async_call(
- "light",
- "turn_on",
- {ATTR_ENTITY_ID: "light.bedroom", ATTR_BRIGHTNESS: 50},
- blocking=True,
- )
- await hass.async_block_till_done()
- mock_set_color.assert_called_with(50, 50, 50, 255)
-
- with patch.object(mock_light, "set_color") as mock_set_color, patch.object(
- mock_light, "get_color", return_value=(50, 45, 25, 255)
- ):
- await hass.services.async_call(
- "light",
- "turn_on",
- {ATTR_ENTITY_ID: "light.bedroom", ATTR_HS_COLOR: (50, 50)},
- blocking=True,
- )
- await hass.async_block_till_done()
-
- mock_set_color.assert_called_with(50, 45, 25, 255)
-
- with patch.object(mock_light, "set_color") as mock_set_color, patch.object(
- mock_light, "get_color", return_value=(220, 201, 110, 180)
- ):
- await hass.services.async_call(
- "light",
- "turn_on",
- {ATTR_ENTITY_ID: "light.bedroom", ATTR_WHITE_VALUE: 180},
- blocking=True,
- )
- await hass.async_block_till_done()
- mock_set_color.assert_called_with(50, 45, 25, 180)
+ mock_light.get_color.return_value = (255, 255, 255, 255)
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {ATTR_ENTITY_ID: "light.bedroom"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_light.set_color.assert_called_with(255, 255, 255, 255)
+
+ mock_light.get_color.return_value = (50, 50, 50, 255)
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {ATTR_ENTITY_ID: "light.bedroom", ATTR_BRIGHTNESS: 50},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_light.set_color.assert_called_with(50, 50, 50, 255)
+
+ mock_light.get_color.return_value = (50, 45, 25, 255)
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {ATTR_ENTITY_ID: "light.bedroom", ATTR_HS_COLOR: (50, 50)},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ mock_light.set_color.assert_called_with(50, 45, 25, 255)
+
+ mock_light.get_color.return_value = (220, 201, 110, 180)
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {ATTR_ENTITY_ID: "light.bedroom", ATTR_WHITE_VALUE: 180},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_light.set_color.assert_called_with(50, 45, 25, 180)
async def test_light_turn_off(hass, mock_light):
"""Test KulerSkyLight turn_on."""
- with patch.object(mock_light, "set_color") as mock_set_color, patch.object(
- mock_light, "get_color", return_value=(0, 0, 0, 0)
- ):
- await hass.services.async_call(
- "light",
- "turn_off",
- {ATTR_ENTITY_ID: "light.bedroom"},
- blocking=True,
- )
- await hass.async_block_till_done()
- mock_set_color.assert_called_with(0, 0, 0, 0)
+ mock_light.get_color.return_value = (0, 0, 0, 0)
+ await hass.services.async_call(
+ "light",
+ "turn_off",
+ {ATTR_ENTITY_ID: "light.bedroom"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_light.set_color.assert_called_with(0, 0, 0, 0)
async def test_light_update(hass, mock_light):
@@ -269,47 +182,47 @@ async def test_light_update(hass, mock_light):
assert state.state == STATE_OFF
assert state.attributes == {
ATTR_FRIENDLY_NAME: "Bedroom",
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW],
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS
| SUPPORT_COLOR
| SUPPORT_WHITE_VALUE,
}
# Test an exception during discovery
- with patch.object(
- mock_light, "get_color", side_effect=pykulersky.PykulerskyException("TEST")
- ):
- utcnow = utcnow + SCAN_INTERVAL
- async_fire_time_changed(hass, utcnow)
- await hass.async_block_till_done()
+ mock_light.get_color.side_effect = pykulersky.PykulerskyException("TEST")
+ utcnow = utcnow + SCAN_INTERVAL
+ async_fire_time_changed(hass, utcnow)
+ await hass.async_block_till_done()
state = hass.states.get("light.bedroom")
assert state.state == STATE_UNAVAILABLE
assert state.attributes == {
ATTR_FRIENDLY_NAME: "Bedroom",
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW],
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS
| SUPPORT_COLOR
| SUPPORT_WHITE_VALUE,
}
- with patch.object(
- mock_light,
- "get_color",
- return_value=(80, 160, 200, 240),
- ):
- utcnow = utcnow + SCAN_INTERVAL
- async_fire_time_changed(hass, utcnow)
- await hass.async_block_till_done()
+ mock_light.get_color.side_effect = None
+ mock_light.get_color.return_value = (80, 160, 200, 240)
+ utcnow = utcnow + SCAN_INTERVAL
+ async_fire_time_changed(hass, utcnow)
+ await hass.async_block_till_done()
state = hass.states.get("light.bedroom")
assert state.state == STATE_ON
assert state.attributes == {
ATTR_FRIENDLY_NAME: "Bedroom",
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW],
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS
| SUPPORT_COLOR
| SUPPORT_WHITE_VALUE,
+ ATTR_COLOR_MODE: COLOR_MODE_RGBW,
ATTR_BRIGHTNESS: 200,
ATTR_HS_COLOR: (200, 60),
ATTR_RGB_COLOR: (102, 203, 255),
+ ATTR_RGBW_COLOR: (102, 203, 255, 240),
ATTR_WHITE_VALUE: 240,
ATTR_XY_COLOR: (0.184, 0.261),
}
diff --git a/tests/components/lcn/__init__.py b/tests/components/lcn/__init__.py
new file mode 100644
index 00000000000000..6ca398de93fc4c
--- /dev/null
+++ b/tests/components/lcn/__init__.py
@@ -0,0 +1 @@
+"""Tests for LCN."""
diff --git a/tests/components/lcn/test_config_flow.py b/tests/components/lcn/test_config_flow.py
new file mode 100644
index 00000000000000..325552f62d3b9b
--- /dev/null
+++ b/tests/components/lcn/test_config_flow.py
@@ -0,0 +1,92 @@
+"""Tests for the LCN config flow."""
+from unittest.mock import patch
+
+from pypck.connection import PchkAuthenticationError, PchkLicenseError
+import pytest
+
+from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.components.lcn.const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, DOMAIN
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_IP_ADDRESS,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_USERNAME,
+)
+
+from tests.common import MockConfigEntry
+
+IMPORT_DATA = {
+ CONF_HOST: "pchk",
+ CONF_IP_ADDRESS: "127.0.0.1",
+ CONF_PORT: 4114,
+ CONF_USERNAME: "lcn",
+ CONF_PASSWORD: "lcn",
+ CONF_SK_NUM_TRIES: 0,
+ CONF_DIM_MODE: "STEPS200",
+}
+
+
+async def test_step_import(hass):
+ """Test for import step."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch("pypck.connection.PchkConnectionManager.async_connect"), patch(
+ "homeassistant.components.lcn.async_setup", return_value=True
+ ), patch("homeassistant.components.lcn.async_setup_entry", return_value=True):
+ data = IMPORT_DATA.copy()
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=data
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "pchk"
+ assert result["data"] == IMPORT_DATA
+
+
+async def test_step_import_existing_host(hass):
+ """Test for update of config_entry if imported host already exists."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ # Create config entry and add it to hass
+ mock_data = IMPORT_DATA.copy()
+ mock_data.update({CONF_SK_NUM_TRIES: 3, CONF_DIM_MODE: 50})
+ mock_entry = MockConfigEntry(domain=DOMAIN, data=mock_data)
+ mock_entry.add_to_hass(hass)
+ # Inititalize a config flow with different data but same host address
+ with patch("pypck.connection.PchkConnectionManager.async_connect"):
+ imported_data = IMPORT_DATA.copy()
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=imported_data
+ )
+ await hass.async_block_till_done()
+
+ # Check if config entry was updated
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "existing_configuration_updated"
+ assert mock_entry.source == config_entries.SOURCE_IMPORT
+ assert mock_entry.data == IMPORT_DATA
+
+
+@pytest.mark.parametrize(
+ "error,reason",
+ [
+ (PchkAuthenticationError, "authentication_error"),
+ (PchkLicenseError, "license_error"),
+ (TimeoutError, "connection_timeout"),
+ ],
+)
+async def test_step_import_error(hass, error, reason):
+ """Test for authentication error is handled correctly."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch(
+ "pypck.connection.PchkConnectionManager.async_connect", side_effect=error
+ ):
+ data = IMPORT_DATA.copy()
+ data.update({CONF_HOST: "pchk"})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=data
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == reason
diff --git a/tests/components/light/common.py b/tests/components/light/common.py
index a9991bf35948c4..229823ceb174f9 100644
--- a/tests/components/light/common.py
+++ b/tests/components/light/common.py
@@ -14,6 +14,8 @@
ATTR_KELVIN,
ATTR_PROFILE,
ATTR_RGB_COLOR,
+ ATTR_RGBW_COLOR,
+ ATTR_RGBWW_COLOR,
ATTR_TRANSITION,
ATTR_WHITE_VALUE,
ATTR_XY_COLOR,
@@ -37,6 +39,8 @@ def turn_on(
brightness=None,
brightness_pct=None,
rgb_color=None,
+ rgbw_color=None,
+ rgbww_color=None,
xy_color=None,
hs_color=None,
color_temp=None,
@@ -56,6 +60,8 @@ def turn_on(
brightness,
brightness_pct,
rgb_color,
+ rgbw_color,
+ rgbww_color,
xy_color,
hs_color,
color_temp,
@@ -75,6 +81,8 @@ async def async_turn_on(
brightness=None,
brightness_pct=None,
rgb_color=None,
+ rgbw_color=None,
+ rgbww_color=None,
xy_color=None,
hs_color=None,
color_temp=None,
@@ -95,6 +103,8 @@ async def async_turn_on(
(ATTR_BRIGHTNESS, brightness),
(ATTR_BRIGHTNESS_PCT, brightness_pct),
(ATTR_RGB_COLOR, rgb_color),
+ (ATTR_RGBW_COLOR, rgbw_color),
+ (ATTR_RGBWW_COLOR, rgbww_color),
(ATTR_XY_COLOR, xy_color),
(ATTR_HS_COLOR, hs_color),
(ATTR_COLOR_TEMP, color_temp),
@@ -111,16 +121,20 @@ async def async_turn_on(
@bind_hass
-def turn_off(hass, entity_id=ENTITY_MATCH_ALL, transition=None):
+def turn_off(hass, entity_id=ENTITY_MATCH_ALL, transition=None, flash=None):
"""Turn all or specified light off."""
- hass.add_job(async_turn_off, hass, entity_id, transition)
+ hass.add_job(async_turn_off, hass, entity_id, transition, flash)
-async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL, transition=None):
+async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL, transition=None, flash=None):
"""Turn all or specified light off."""
data = {
key: value
- for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_TRANSITION, transition)]
+ for key, value in [
+ (ATTR_ENTITY_ID, entity_id),
+ (ATTR_TRANSITION, transition),
+ (ATTR_FLASH, flash),
+ ]
if value is not None
}
diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py
index 63670d9bfab082..4760dfd1c5377a 100644
--- a/tests/components/light/test_device_action.py
+++ b/tests/components/light/test_device_action.py
@@ -21,7 +21,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py
index 2a877478b1ec86..d529c82bfa5bd3 100644
--- a/tests/components/light/test_device_condition.py
+++ b/tests/components/light/test_device_condition.py
@@ -19,7 +19,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py
index fad39898467578..1c9f6cf145407b 100644
--- a/tests/components/light/test_device_trigger.py
+++ b/tests/components/light/test_device_trigger.py
@@ -19,7 +19,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py
index c36fa623e37756..3adb146a225152 100644
--- a/tests/components/light/test_init.py
+++ b/tests/components/light/test_init.py
@@ -706,36 +706,51 @@ async def test_light_turn_on_auth(hass, hass_admin_user):
async def test_light_brightness_step(hass):
"""Test that light context works."""
platform = getattr(hass.components, "test.light")
- platform.init()
- entity = platform.ENTITIES[0]
- entity.supported_features = light.SUPPORT_BRIGHTNESS
- entity.brightness = 100
+ platform.init(empty=True)
+ platform.ENTITIES.append(platform.MockLight("Test_0", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_1", STATE_ON))
+ entity0 = platform.ENTITIES[0]
+ entity0.supported_features = light.SUPPORT_BRIGHTNESS
+ entity0.brightness = 100
+ entity1 = platform.ENTITIES[1]
+ entity1.supported_features = light.SUPPORT_BRIGHTNESS
+ entity1.brightness = 50
assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
await hass.async_block_till_done()
- state = hass.states.get(entity.entity_id)
+ state = hass.states.get(entity0.entity_id)
assert state is not None
assert state.attributes["brightness"] == 100
+ state = hass.states.get(entity1.entity_id)
+ assert state is not None
+ assert state.attributes["brightness"] == 50
await hass.services.async_call(
"light",
"turn_on",
- {"entity_id": entity.entity_id, "brightness_step": -10},
+ {"entity_id": [entity0.entity_id, entity1.entity_id], "brightness_step": -10},
blocking=True,
)
- _, data = entity.last_call("turn_on")
- assert data["brightness"] == 90, data
+ _, data = entity0.last_call("turn_on")
+ assert data["brightness"] == 90 # 100 - 10
+ _, data = entity1.last_call("turn_on")
+ assert data["brightness"] == 40 # 50 - 10
await hass.services.async_call(
"light",
"turn_on",
- {"entity_id": entity.entity_id, "brightness_step_pct": 10},
+ {
+ "entity_id": [entity0.entity_id, entity1.entity_id],
+ "brightness_step_pct": 10,
+ },
blocking=True,
)
- _, data = entity.last_call("turn_on")
- assert data["brightness"] == 126, data
+ _, data = entity0.last_call("turn_on")
+ assert data["brightness"] == 126 # 100 + (255 * 0.10)
+ _, data = entity1.last_call("turn_on")
+ assert data["brightness"] == 76 # 50 + (255 * 0.10)
async def test_light_brightness_pct_conversion(hass):
@@ -760,7 +775,7 @@ async def test_light_brightness_pct_conversion(hass):
)
_, data = entity.last_call("turn_on")
- assert data["brightness"] == 3, data
+ assert data["brightness"] == 3
await hass.services.async_call(
"light",
@@ -770,7 +785,7 @@ async def test_light_brightness_pct_conversion(hass):
)
_, data = entity.last_call("turn_on")
- assert data["brightness"] == 5, data
+ assert data["brightness"] == 5
await hass.services.async_call(
"light",
@@ -780,7 +795,7 @@ async def test_light_brightness_pct_conversion(hass):
)
_, data = entity.last_call("turn_on")
- assert data["brightness"] == 128, data
+ assert data["brightness"] == 128
await hass.services.async_call(
"light",
@@ -790,7 +805,7 @@ async def test_light_brightness_pct_conversion(hass):
)
_, data = entity.last_call("turn_on")
- assert data["brightness"] == 252, data
+ assert data["brightness"] == 252
await hass.services.async_call(
"light",
@@ -800,7 +815,7 @@ async def test_light_brightness_pct_conversion(hass):
)
_, data = entity.last_call("turn_on")
- assert data["brightness"] == 255, data
+ assert data["brightness"] == 255
def test_deprecated_base_class(caplog):
@@ -900,3 +915,482 @@ async def test_profile_load_optional_hs_color(hass):
"invalid_no_brightness_no_color_no_transition",
):
assert invalid_profile_name not in profiles.data
+
+
+@pytest.mark.parametrize("light_state", (STATE_ON, STATE_OFF))
+async def test_light_backwards_compatibility_supported_color_modes(hass, light_state):
+ """Test supported_color_modes if not implemented by the entity."""
+ platform = getattr(hass.components, "test.light")
+ platform.init(empty=True)
+
+ platform.ENTITIES.append(platform.MockLight("Test_0", light_state))
+ platform.ENTITIES.append(platform.MockLight("Test_1", light_state))
+ platform.ENTITIES.append(platform.MockLight("Test_2", light_state))
+ platform.ENTITIES.append(platform.MockLight("Test_3", light_state))
+ platform.ENTITIES.append(platform.MockLight("Test_4", light_state))
+ platform.ENTITIES.append(platform.MockLight("Test_5", light_state))
+ platform.ENTITIES.append(platform.MockLight("Test_6", light_state))
+
+ entity0 = platform.ENTITIES[0]
+
+ entity1 = platform.ENTITIES[1]
+ entity1.supported_features = light.SUPPORT_BRIGHTNESS
+
+ entity2 = platform.ENTITIES[2]
+ entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP
+
+ entity3 = platform.ENTITIES[3]
+ entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR
+
+ entity4 = platform.ENTITIES[4]
+ entity4.supported_features = (
+ light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_WHITE_VALUE
+ )
+
+ entity5 = platform.ENTITIES[5]
+ entity5.supported_features = (
+ light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP
+ )
+
+ entity6 = platform.ENTITIES[6]
+ entity6.supported_features = (
+ light.SUPPORT_BRIGHTNESS
+ | light.SUPPORT_COLOR
+ | light.SUPPORT_COLOR_TEMP
+ | light.SUPPORT_WHITE_VALUE
+ )
+
+ assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity0.entity_id)
+ assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_ONOFF]
+ if light_state == STATE_OFF:
+ assert "color_mode" not in state.attributes
+ else:
+ assert state.attributes["color_mode"] == light.COLOR_MODE_ONOFF
+
+ state = hass.states.get(entity1.entity_id)
+ assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_BRIGHTNESS]
+ if light_state == STATE_OFF:
+ assert "color_mode" not in state.attributes
+ else:
+ assert state.attributes["color_mode"] == light.COLOR_MODE_UNKNOWN
+
+ state = hass.states.get(entity2.entity_id)
+ assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_COLOR_TEMP]
+ if light_state == STATE_OFF:
+ assert "color_mode" not in state.attributes
+ else:
+ assert state.attributes["color_mode"] == light.COLOR_MODE_UNKNOWN
+
+ state = hass.states.get(entity3.entity_id)
+ assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_HS]
+ if light_state == STATE_OFF:
+ assert "color_mode" not in state.attributes
+ else:
+ assert state.attributes["color_mode"] == light.COLOR_MODE_UNKNOWN
+
+ state = hass.states.get(entity4.entity_id)
+ assert state.attributes["supported_color_modes"] == [
+ light.COLOR_MODE_HS,
+ light.COLOR_MODE_RGBW,
+ ]
+ if light_state == STATE_OFF:
+ assert "color_mode" not in state.attributes
+ else:
+ assert state.attributes["color_mode"] == light.COLOR_MODE_UNKNOWN
+
+ state = hass.states.get(entity5.entity_id)
+ assert state.attributes["supported_color_modes"] == [
+ light.COLOR_MODE_COLOR_TEMP,
+ light.COLOR_MODE_HS,
+ ]
+ if light_state == STATE_OFF:
+ assert "color_mode" not in state.attributes
+ else:
+ assert state.attributes["color_mode"] == light.COLOR_MODE_UNKNOWN
+
+ state = hass.states.get(entity6.entity_id)
+ assert state.attributes["supported_color_modes"] == [
+ light.COLOR_MODE_COLOR_TEMP,
+ light.COLOR_MODE_HS,
+ light.COLOR_MODE_RGBW,
+ ]
+ if light_state == STATE_OFF:
+ assert "color_mode" not in state.attributes
+ else:
+ assert state.attributes["color_mode"] == light.COLOR_MODE_UNKNOWN
+
+
+async def test_light_backwards_compatibility_color_mode(hass):
+ """Test color_mode if not implemented by the entity."""
+ platform = getattr(hass.components, "test.light")
+ platform.init(empty=True)
+
+ platform.ENTITIES.append(platform.MockLight("Test_0", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_1", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_2", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_3", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_4", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_5", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_6", STATE_ON))
+
+ entity0 = platform.ENTITIES[0]
+
+ entity1 = platform.ENTITIES[1]
+ entity1.supported_features = light.SUPPORT_BRIGHTNESS
+ entity1.brightness = 100
+
+ entity2 = platform.ENTITIES[2]
+ entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP
+ entity2.color_temp = 100
+
+ entity3 = platform.ENTITIES[3]
+ entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR
+ entity3.hs_color = (240, 100)
+
+ entity4 = platform.ENTITIES[4]
+ entity4.supported_features = (
+ light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_WHITE_VALUE
+ )
+ entity4.hs_color = (240, 100)
+ entity4.white_value = 100
+
+ entity5 = platform.ENTITIES[5]
+ entity5.supported_features = (
+ light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP
+ )
+ entity5.hs_color = (240, 100)
+ entity5.color_temp = 100
+
+ assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity0.entity_id)
+ assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_ONOFF]
+ assert state.attributes["color_mode"] == light.COLOR_MODE_ONOFF
+
+ state = hass.states.get(entity1.entity_id)
+ assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_BRIGHTNESS]
+ assert state.attributes["color_mode"] == light.COLOR_MODE_BRIGHTNESS
+
+ state = hass.states.get(entity2.entity_id)
+ assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_COLOR_TEMP]
+ assert state.attributes["color_mode"] == light.COLOR_MODE_COLOR_TEMP
+
+ state = hass.states.get(entity3.entity_id)
+ assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_HS]
+ assert state.attributes["color_mode"] == light.COLOR_MODE_HS
+
+ state = hass.states.get(entity4.entity_id)
+ assert state.attributes["supported_color_modes"] == [
+ light.COLOR_MODE_HS,
+ light.COLOR_MODE_RGBW,
+ ]
+ assert state.attributes["color_mode"] == light.COLOR_MODE_RGBW
+
+ state = hass.states.get(entity5.entity_id)
+ assert state.attributes["supported_color_modes"] == [
+ light.COLOR_MODE_COLOR_TEMP,
+ light.COLOR_MODE_HS,
+ ]
+ # hs color prioritized over color_temp, light should report mode COLOR_MODE_HS
+ assert state.attributes["color_mode"] == light.COLOR_MODE_HS
+
+
+async def test_light_service_call_rgbw(hass):
+ """Test backwards compatibility for rgbw functionality in service calls."""
+ platform = getattr(hass.components, "test.light")
+ platform.init(empty=True)
+
+ platform.ENTITIES.append(platform.MockLight("Test_legacy_white_value", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_ON))
+
+ entity0 = platform.ENTITIES[0]
+ entity0.supported_features = (
+ light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_WHITE_VALUE
+ )
+
+ entity1 = platform.ENTITIES[1]
+ entity1.supported_color_modes = {light.COLOR_MODE_RGBW}
+
+ assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity0.entity_id)
+ assert state.attributes["supported_color_modes"] == [
+ light.COLOR_MODE_HS,
+ light.COLOR_MODE_RGBW,
+ ]
+
+ state = hass.states.get(entity1.entity_id)
+ assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_RGBW]
+
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {
+ "entity_id": [entity0.entity_id, entity1.entity_id],
+ "brightness_pct": 100,
+ "rgbw_color": (10, 20, 30, 40),
+ },
+ blocking=True,
+ )
+
+ _, data = entity0.last_call("turn_on")
+ assert data == {"brightness": 255, "hs_color": (210.0, 66.667), "white_value": 40}
+ _, data = entity1.last_call("turn_on")
+ assert data == {"brightness": 255, "rgbw_color": (10, 20, 30, 40)}
+
+
+async def test_light_state_rgbw(hass):
+ """Test rgbw color conversion in state updates."""
+ platform = getattr(hass.components, "test.light")
+ platform.init(empty=True)
+
+ platform.ENTITIES.append(platform.MockLight("Test_legacy_white_value", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_ON))
+
+ entity0 = platform.ENTITIES[0]
+ legacy_supported_features = (
+ light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_WHITE_VALUE
+ )
+ entity0.supported_features = legacy_supported_features
+ entity0.hs_color = (210.0, 66.667)
+ entity0.rgb_color = "Invalid" # Should be ignored
+ entity0.rgbww_color = "Invalid" # Should be ignored
+ entity0.white_value = 40
+ entity0.xy_color = "Invalid" # Should be ignored
+
+ entity1 = platform.ENTITIES[1]
+ entity1.supported_color_modes = {light.COLOR_MODE_RGBW}
+ entity1.color_mode = light.COLOR_MODE_RGBW
+ entity1.hs_color = "Invalid" # Should be ignored
+ entity1.rgb_color = "Invalid" # Should be ignored
+ entity1.rgbw_color = (1, 2, 3, 4)
+ entity1.rgbww_color = "Invalid" # Should be ignored
+ entity1.white_value = "Invalid" # Should be ignored
+ entity1.xy_color = "Invalid" # Should be ignored
+
+ assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity0.entity_id)
+ assert state.attributes == {
+ "color_mode": light.COLOR_MODE_RGBW,
+ "friendly_name": "Test_legacy_white_value",
+ "supported_color_modes": [light.COLOR_MODE_HS, light.COLOR_MODE_RGBW],
+ "supported_features": legacy_supported_features,
+ "hs_color": (210.0, 66.667),
+ "rgb_color": (84, 169, 255),
+ "rgbw_color": (84, 169, 255, 40),
+ "white_value": 40,
+ "xy_color": (0.173, 0.207),
+ }
+
+ state = hass.states.get(entity1.entity_id)
+ assert state.attributes == {
+ "color_mode": light.COLOR_MODE_RGBW,
+ "friendly_name": "Test_rgbw",
+ "supported_color_modes": [light.COLOR_MODE_RGBW],
+ "supported_features": 0,
+ "rgbw_color": (1, 2, 3, 4),
+ }
+
+
+async def test_light_service_call_color_conversion(hass):
+ """Test color conversion in service calls."""
+ platform = getattr(hass.components, "test.light")
+ platform.init(empty=True)
+
+ platform.ENTITIES.append(platform.MockLight("Test_hs", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_rgb", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_xy", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_all", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_legacy", STATE_ON))
+
+ entity0 = platform.ENTITIES[0]
+ entity0.supported_color_modes = {light.COLOR_MODE_HS}
+
+ entity1 = platform.ENTITIES[1]
+ entity1.supported_color_modes = {light.COLOR_MODE_RGB}
+
+ entity2 = platform.ENTITIES[2]
+ entity2.supported_color_modes = {light.COLOR_MODE_XY}
+
+ entity3 = platform.ENTITIES[3]
+ entity3.supported_color_modes = {
+ light.COLOR_MODE_HS,
+ light.COLOR_MODE_RGB,
+ light.COLOR_MODE_XY,
+ }
+
+ entity4 = platform.ENTITIES[4]
+ entity4.supported_features = light.SUPPORT_COLOR
+
+ assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity0.entity_id)
+ assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_HS]
+
+ state = hass.states.get(entity1.entity_id)
+ assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_RGB]
+
+ state = hass.states.get(entity2.entity_id)
+ assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_XY]
+
+ state = hass.states.get(entity3.entity_id)
+ assert state.attributes["supported_color_modes"] == [
+ light.COLOR_MODE_HS,
+ light.COLOR_MODE_RGB,
+ light.COLOR_MODE_XY,
+ ]
+
+ state = hass.states.get(entity4.entity_id)
+ assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_HS]
+
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {
+ "entity_id": [
+ entity0.entity_id,
+ entity1.entity_id,
+ entity2.entity_id,
+ entity3.entity_id,
+ entity4.entity_id,
+ ],
+ "brightness_pct": 100,
+ "hs_color": (240, 100),
+ },
+ blocking=True,
+ )
+ _, data = entity0.last_call("turn_on")
+ assert data == {"brightness": 255, "hs_color": (240.0, 100.0)}
+ _, data = entity1.last_call("turn_on")
+ assert data == {"brightness": 255, "rgb_color": (0, 0, 255)}
+ _, data = entity2.last_call("turn_on")
+ assert data == {"brightness": 255, "xy_color": (0.136, 0.04)}
+ _, data = entity3.last_call("turn_on")
+ assert data == {"brightness": 255, "hs_color": (240.0, 100.0)}
+ _, data = entity4.last_call("turn_on")
+ assert data == {"brightness": 255, "hs_color": (240.0, 100.0)}
+
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {
+ "entity_id": [
+ entity0.entity_id,
+ entity1.entity_id,
+ entity2.entity_id,
+ entity3.entity_id,
+ entity4.entity_id,
+ ],
+ "brightness_pct": 50,
+ "rgb_color": (128, 0, 0),
+ },
+ blocking=True,
+ )
+ _, data = entity0.last_call("turn_on")
+ assert data == {"brightness": 128, "hs_color": (0.0, 100.0)}
+ _, data = entity1.last_call("turn_on")
+ assert data == {"brightness": 128, "rgb_color": (128, 0, 0)}
+ _, data = entity2.last_call("turn_on")
+ assert data == {"brightness": 128, "xy_color": (0.701, 0.299)}
+ _, data = entity3.last_call("turn_on")
+ assert data == {"brightness": 128, "rgb_color": (128, 0, 0)}
+ _, data = entity4.last_call("turn_on")
+ assert data == {"brightness": 128, "hs_color": (0.0, 100.0)}
+
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {
+ "entity_id": [
+ entity0.entity_id,
+ entity1.entity_id,
+ entity2.entity_id,
+ entity3.entity_id,
+ entity4.entity_id,
+ ],
+ "brightness_pct": 50,
+ "xy_color": (0.1, 0.8),
+ },
+ blocking=True,
+ )
+ _, data = entity0.last_call("turn_on")
+ assert data == {"brightness": 128, "hs_color": (125.176, 100.0)}
+ _, data = entity1.last_call("turn_on")
+ assert data == {"brightness": 128, "rgb_color": (0, 255, 22)}
+ _, data = entity2.last_call("turn_on")
+ assert data == {"brightness": 128, "xy_color": (0.1, 0.8)}
+ _, data = entity3.last_call("turn_on")
+ assert data == {"brightness": 128, "xy_color": (0.1, 0.8)}
+ _, data = entity4.last_call("turn_on")
+ assert data == {"brightness": 128, "hs_color": (125.176, 100.0)}
+
+
+async def test_light_state_color_conversion(hass):
+ """Test color conversion in state updates."""
+ platform = getattr(hass.components, "test.light")
+ platform.init(empty=True)
+
+ platform.ENTITIES.append(platform.MockLight("Test_hs", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_rgb", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_xy", STATE_ON))
+ platform.ENTITIES.append(platform.MockLight("Test_legacy", STATE_ON))
+
+ entity0 = platform.ENTITIES[0]
+ entity0.supported_color_modes = {light.COLOR_MODE_HS}
+ entity0.color_mode = light.COLOR_MODE_HS
+ entity0.hs_color = (240, 100)
+ entity0.rgb_color = "Invalid" # Should be ignored
+ entity0.xy_color = "Invalid" # Should be ignored
+
+ entity1 = platform.ENTITIES[1]
+ entity1.supported_color_modes = {light.COLOR_MODE_RGB}
+ entity1.color_mode = light.COLOR_MODE_RGB
+ entity1.hs_color = "Invalid" # Should be ignored
+ entity1.rgb_color = (128, 0, 0)
+ entity1.xy_color = "Invalid" # Should be ignored
+
+ entity2 = platform.ENTITIES[2]
+ entity2.supported_color_modes = {light.COLOR_MODE_XY}
+ entity2.color_mode = light.COLOR_MODE_XY
+ entity2.hs_color = "Invalid" # Should be ignored
+ entity2.rgb_color = "Invalid" # Should be ignored
+ entity2.xy_color = (0.1, 0.8)
+
+ entity3 = platform.ENTITIES[3]
+ entity3.hs_color = (240, 100)
+ entity3.supported_features = light.SUPPORT_COLOR
+
+ assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity0.entity_id)
+ assert state.attributes["color_mode"] == light.COLOR_MODE_HS
+ assert state.attributes["hs_color"] == (240, 100)
+ assert state.attributes["rgb_color"] == (0, 0, 255)
+ assert state.attributes["xy_color"] == (0.136, 0.04)
+
+ state = hass.states.get(entity1.entity_id)
+ assert state.attributes["color_mode"] == light.COLOR_MODE_RGB
+ assert state.attributes["hs_color"] == (0.0, 100.0)
+ assert state.attributes["rgb_color"] == (128, 0, 0)
+ assert state.attributes["xy_color"] == (0.701, 0.299)
+
+ state = hass.states.get(entity2.entity_id)
+ assert state.attributes["color_mode"] == light.COLOR_MODE_XY
+ assert state.attributes["hs_color"] == (125.176, 100.0)
+ assert state.attributes["rgb_color"] == (0, 255, 22)
+ assert state.attributes["xy_color"] == (0.1, 0.8)
+
+ state = hass.states.get(entity3.entity_id)
+ assert state.attributes["color_mode"] == light.COLOR_MODE_HS
+ assert state.attributes["hs_color"] == (240, 100)
+ assert state.attributes["rgb_color"] == (0, 0, 255)
+ assert state.attributes["xy_color"] == (0.136, 0.04)
diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py
index e96f4ff4528276..815b8831d37402 100644
--- a/tests/components/light/test_reproduce_state.py
+++ b/tests/components/light/test_reproduce_state.py
@@ -1,4 +1,7 @@
"""Test reproduce state for Light."""
+import pytest
+
+from homeassistant.components import light
from homeassistant.components.light.reproduce_state import DEPRECATION_WARNING
from homeassistant.core import State
@@ -15,6 +18,8 @@
VALID_KELVIN = {"kelvin": 4000}
VALID_PROFILE = {"profile": "relax"}
VALID_RGB_COLOR = {"rgb_color": (255, 63, 111)}
+VALID_RGBW_COLOR = {"rgbw_color": (255, 63, 111, 10)}
+VALID_RGBWW_COLOR = {"rgbww_color": (255, 63, 111, 10, 20)}
VALID_XY_COLOR = {"xy_color": (0.59, 0.274)}
@@ -91,51 +96,51 @@ async def test_reproducing_states(hass, caplog):
expected_calls = []
- expected_off = VALID_BRIGHTNESS
+ expected_off = dict(VALID_BRIGHTNESS)
expected_off["entity_id"] = "light.entity_off"
expected_calls.append(expected_off)
- expected_bright = VALID_WHITE_VALUE
+ expected_bright = dict(VALID_WHITE_VALUE)
expected_bright["entity_id"] = "light.entity_bright"
expected_calls.append(expected_bright)
- expected_white = VALID_FLASH
+ expected_white = dict(VALID_FLASH)
expected_white["entity_id"] = "light.entity_white"
expected_calls.append(expected_white)
- expected_flash = VALID_EFFECT
+ expected_flash = dict(VALID_EFFECT)
expected_flash["entity_id"] = "light.entity_flash"
expected_calls.append(expected_flash)
- expected_effect = VALID_TRANSITION
+ expected_effect = dict(VALID_TRANSITION)
expected_effect["entity_id"] = "light.entity_effect"
expected_calls.append(expected_effect)
- expected_trans = VALID_COLOR_NAME
+ expected_trans = dict(VALID_COLOR_NAME)
expected_trans["entity_id"] = "light.entity_trans"
expected_calls.append(expected_trans)
- expected_name = VALID_COLOR_TEMP
+ expected_name = dict(VALID_COLOR_TEMP)
expected_name["entity_id"] = "light.entity_name"
expected_calls.append(expected_name)
- expected_temp = VALID_HS_COLOR
+ expected_temp = dict(VALID_HS_COLOR)
expected_temp["entity_id"] = "light.entity_temp"
expected_calls.append(expected_temp)
- expected_hs = VALID_KELVIN
+ expected_hs = dict(VALID_KELVIN)
expected_hs["entity_id"] = "light.entity_hs"
expected_calls.append(expected_hs)
- expected_kelvin = VALID_PROFILE
+ expected_kelvin = dict(VALID_PROFILE)
expected_kelvin["entity_id"] = "light.entity_kelvin"
expected_calls.append(expected_kelvin)
- expected_profile = VALID_RGB_COLOR
+ expected_profile = dict(VALID_RGB_COLOR)
expected_profile["entity_id"] = "light.entity_profile"
expected_calls.append(expected_profile)
- expected_rgb = VALID_XY_COLOR
+ expected_rgb = dict(VALID_XY_COLOR)
expected_rgb["entity_id"] = "light.entity_rgb"
expected_calls.append(expected_rgb)
@@ -156,6 +161,59 @@ async def test_reproducing_states(hass, caplog):
assert turn_off_calls[0].data == {"entity_id": "light.entity_xy"}
+@pytest.mark.parametrize(
+ "color_mode",
+ (
+ light.COLOR_MODE_COLOR_TEMP,
+ light.COLOR_MODE_BRIGHTNESS,
+ light.COLOR_MODE_HS,
+ light.COLOR_MODE_ONOFF,
+ light.COLOR_MODE_RGB,
+ light.COLOR_MODE_RGBW,
+ light.COLOR_MODE_RGBWW,
+ light.COLOR_MODE_UNKNOWN,
+ light.COLOR_MODE_XY,
+ ),
+)
+async def test_filter_color_modes(hass, caplog, color_mode):
+ """Test filtering of parameters according to color mode."""
+ hass.states.async_set("light.entity", "off", {})
+ all_colors = {
+ **VALID_WHITE_VALUE,
+ **VALID_COLOR_NAME,
+ **VALID_COLOR_TEMP,
+ **VALID_HS_COLOR,
+ **VALID_KELVIN,
+ **VALID_RGB_COLOR,
+ **VALID_RGBW_COLOR,
+ **VALID_RGBWW_COLOR,
+ **VALID_XY_COLOR,
+ }
+
+ turn_on_calls = async_mock_service(hass, "light", "turn_on")
+
+ await hass.helpers.state.async_reproduce_state(
+ [State("light.entity", "on", {**all_colors, "color_mode": color_mode})]
+ )
+
+ expected_map = {
+ light.COLOR_MODE_COLOR_TEMP: VALID_COLOR_TEMP,
+ light.COLOR_MODE_BRIGHTNESS: {},
+ light.COLOR_MODE_HS: VALID_HS_COLOR,
+ light.COLOR_MODE_ONOFF: {},
+ light.COLOR_MODE_RGB: VALID_RGB_COLOR,
+ light.COLOR_MODE_RGBW: VALID_RGBW_COLOR,
+ light.COLOR_MODE_RGBWW: VALID_RGBWW_COLOR,
+ light.COLOR_MODE_UNKNOWN: {**VALID_HS_COLOR, **VALID_WHITE_VALUE},
+ light.COLOR_MODE_XY: VALID_XY_COLOR,
+ }
+ expected = expected_map[color_mode]
+
+ assert len(turn_on_calls) == 1
+ assert turn_on_calls[0].domain == "light"
+ assert dict(turn_on_calls[0].data) == {"entity_id": "light.entity", **expected}
+
+
async def test_deprecation_warning(hass, caplog):
"""Test deprecation warning."""
hass.states.async_set("light.entity_off", "off", {})
diff --git a/tests/components/litejet/__init__.py b/tests/components/litejet/__init__.py
index 9a01fbe5114fd1..e0304c21617022 100644
--- a/tests/components/litejet/__init__.py
+++ b/tests/components/litejet/__init__.py
@@ -1 +1,52 @@
"""Tests for the litejet component."""
+from homeassistant.components import scene, switch
+from homeassistant.components.litejet import DOMAIN
+from homeassistant.const import CONF_PORT
+from homeassistant.helpers import entity_registry as er
+
+from tests.common import MockConfigEntry
+
+
+async def async_init_integration(
+ hass, use_switch=False, use_scene=False
+) -> MockConfigEntry:
+ """Set up the LiteJet integration in Home Assistant."""
+
+ registry = er.async_get(hass)
+
+ entry_data = {CONF_PORT: "/dev/mock"}
+
+ entry = MockConfigEntry(
+ domain=DOMAIN, unique_id=entry_data[CONF_PORT], data=entry_data
+ )
+
+ if use_switch:
+ registry.async_get_or_create(
+ switch.DOMAIN,
+ DOMAIN,
+ f"{entry.entry_id}_1",
+ suggested_object_id="mock_switch_1",
+ disabled_by=None,
+ )
+ registry.async_get_or_create(
+ switch.DOMAIN,
+ DOMAIN,
+ f"{entry.entry_id}_2",
+ suggested_object_id="mock_switch_2",
+ disabled_by=None,
+ )
+
+ if use_scene:
+ registry.async_get_or_create(
+ scene.DOMAIN,
+ DOMAIN,
+ f"{entry.entry_id}_1",
+ suggested_object_id="mock_scene_1",
+ disabled_by=None,
+ )
+
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ return entry
diff --git a/tests/components/litejet/conftest.py b/tests/components/litejet/conftest.py
index 68797f96ccff14..00b1eb921901e5 100644
--- a/tests/components/litejet/conftest.py
+++ b/tests/components/litejet/conftest.py
@@ -1,2 +1,62 @@
-"""litejet conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+"""Fixtures for LiteJet testing."""
+from datetime import timedelta
+from unittest.mock import patch
+
+import pytest
+
+import homeassistant.util.dt as dt_util
+
+
+@pytest.fixture
+def mock_litejet():
+ """Mock LiteJet system."""
+ with patch("pylitejet.LiteJet") as mock_pylitejet:
+
+ def get_load_name(number):
+ return f"Mock Load #{number}"
+
+ def get_scene_name(number):
+ return f"Mock Scene #{number}"
+
+ def get_switch_name(number):
+ return f"Mock Switch #{number}"
+
+ mock_lj = mock_pylitejet.return_value
+
+ mock_lj.switch_pressed_callbacks = {}
+ mock_lj.switch_released_callbacks = {}
+ mock_lj.load_activated_callbacks = {}
+ mock_lj.load_deactivated_callbacks = {}
+
+ def on_switch_pressed(number, callback):
+ mock_lj.switch_pressed_callbacks[number] = callback
+
+ def on_switch_released(number, callback):
+ mock_lj.switch_released_callbacks[number] = callback
+
+ def on_load_activated(number, callback):
+ mock_lj.load_activated_callbacks[number] = callback
+
+ def on_load_deactivated(number, callback):
+ mock_lj.load_deactivated_callbacks[number] = callback
+
+ mock_lj.on_switch_pressed.side_effect = on_switch_pressed
+ mock_lj.on_switch_released.side_effect = on_switch_released
+ mock_lj.on_load_activated.side_effect = on_load_activated
+ mock_lj.on_load_deactivated.side_effect = on_load_deactivated
+
+ mock_lj.loads.return_value = range(1, 3)
+ mock_lj.get_load_name.side_effect = get_load_name
+ mock_lj.get_load_level.return_value = 0
+
+ mock_lj.button_switches.return_value = range(1, 3)
+ mock_lj.all_switches.return_value = range(1, 6)
+ mock_lj.get_switch_name.side_effect = get_switch_name
+
+ mock_lj.scenes.return_value = range(1, 3)
+ mock_lj.get_scene_name.side_effect = get_scene_name
+
+ mock_lj.start_time = dt_util.utcnow()
+ mock_lj.last_delta = timedelta(0)
+
+ yield mock_lj
diff --git a/tests/components/litejet/test_config_flow.py b/tests/components/litejet/test_config_flow.py
new file mode 100644
index 00000000000000..015ba1c64946a8
--- /dev/null
+++ b/tests/components/litejet/test_config_flow.py
@@ -0,0 +1,77 @@
+"""The tests for the litejet component."""
+from unittest.mock import patch
+
+from serial import SerialException
+
+from homeassistant.components.litejet.const import DOMAIN
+from homeassistant.const import CONF_PORT
+
+from tests.common import MockConfigEntry
+
+
+async def test_show_config_form(hass):
+ """Test show configuration form."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "user"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+
+async def test_create_entry(hass, mock_litejet):
+ """Test create entry from user input."""
+ test_data = {CONF_PORT: "/dev/test"}
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "user"}, data=test_data
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == "/dev/test"
+ assert result["data"] == test_data
+
+
+async def test_flow_entry_already_exists(hass):
+ """Test user input when a config entry already exists."""
+ first_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={CONF_PORT: "/dev/first"},
+ )
+ first_entry.add_to_hass(hass)
+
+ test_data = {CONF_PORT: "/dev/test"}
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "user"}, data=test_data
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "single_instance_allowed"
+
+
+async def test_flow_open_failed(hass):
+ """Test user input when serial port open fails."""
+ test_data = {CONF_PORT: "/dev/test"}
+
+ with patch("pylitejet.LiteJet") as mock_pylitejet:
+ mock_pylitejet.side_effect = SerialException
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "user"}, data=test_data
+ )
+
+ assert result["type"] == "form"
+ assert result["errors"][CONF_PORT] == "open_failed"
+
+
+async def test_import_step(hass):
+ """Test initializing via import step."""
+ test_data = {CONF_PORT: "/dev/imported"}
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "import"}, data=test_data
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == test_data[CONF_PORT]
+ assert result["data"] == test_data
diff --git a/tests/components/litejet/test_init.py b/tests/components/litejet/test_init.py
index 3861e7a058ebb5..636864526217d5 100644
--- a/tests/components/litejet/test_init.py
+++ b/tests/components/litejet/test_init.py
@@ -1,44 +1,30 @@
"""The tests for the litejet component."""
-import logging
-import unittest
-
from homeassistant.components import litejet
+from homeassistant.components.litejet.const import DOMAIN
+from homeassistant.const import CONF_PORT
+from homeassistant.setup import async_setup_component
-from tests.common import get_test_home_assistant
-
-_LOGGER = logging.getLogger(__name__)
+from . import async_init_integration
-class TestLiteJet(unittest.TestCase):
- """Test the litejet component."""
+async def test_setup_with_no_config(hass):
+ """Test that nothing happens."""
+ assert await async_setup_component(hass, DOMAIN, {}) is True
+ assert DOMAIN not in hass.data
- def setup_method(self, method):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.hass.start()
- self.hass.block_till_done()
- def teardown_method(self, method):
- """Stop everything that was started."""
- self.hass.stop()
+async def test_setup_with_config_to_import(hass, mock_litejet):
+ """Test that import happens."""
+ assert (
+ await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PORT: "/dev/hello"}})
+ is True
+ )
+ assert DOMAIN in hass.data
- def test_is_ignored_unspecified(self):
- """Ensure it is ignored when unspecified."""
- self.hass.data["litejet_config"] = {}
- assert not litejet.is_ignored(self.hass, "Test")
- def test_is_ignored_empty(self):
- """Ensure it is ignored when empty."""
- self.hass.data["litejet_config"] = {litejet.CONF_EXCLUDE_NAMES: []}
- assert not litejet.is_ignored(self.hass, "Test")
+async def test_unload_entry(hass, mock_litejet):
+ """Test being able to unload an entry."""
+ entry = await async_init_integration(hass, use_switch=True, use_scene=True)
- def test_is_ignored_normal(self):
- """Test if usually ignored."""
- self.hass.data["litejet_config"] = {
- litejet.CONF_EXCLUDE_NAMES: ["Test", "Other One"]
- }
- assert litejet.is_ignored(self.hass, "Test")
- assert not litejet.is_ignored(self.hass, "Other one")
- assert not litejet.is_ignored(self.hass, "Other 0ne")
- assert litejet.is_ignored(self.hass, "Other One There")
- assert litejet.is_ignored(self.hass, "Other One")
+ assert await litejet.async_unload_entry(hass, entry)
+ assert DOMAIN not in hass.data
diff --git a/tests/components/litejet/test_light.py b/tests/components/litejet/test_light.py
index e08bd5c27ac679..c455d3a960e652 100644
--- a/tests/components/litejet/test_light.py
+++ b/tests/components/litejet/test_light.py
@@ -1,14 +1,11 @@
"""The tests for the litejet component."""
import logging
-import unittest
-from unittest import mock
-from homeassistant import setup
-from homeassistant.components import litejet
-import homeassistant.components.light as light
+from homeassistant.components import light
+from homeassistant.components.light import ATTR_BRIGHTNESS
+from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
-from tests.common import get_test_home_assistant
-from tests.components.light import common
+from . import async_init_integration
_LOGGER = logging.getLogger(__name__)
@@ -18,144 +15,113 @@
ENTITY_OTHER_LIGHT_NUMBER = 2
-class TestLiteJetLight(unittest.TestCase):
- """Test the litejet component."""
+async def test_on_brightness(hass, mock_litejet):
+ """Test turning the light on with brightness."""
+ await async_init_integration(hass)
- @mock.patch("homeassistant.components.litejet.LiteJet")
- def setup_method(self, method, mock_pylitejet):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.hass.start()
+ assert hass.states.get(ENTITY_LIGHT).state == "off"
+ assert hass.states.get(ENTITY_OTHER_LIGHT).state == "off"
- self.load_activated_callbacks = {}
- self.load_deactivated_callbacks = {}
+ assert not light.is_on(hass, ENTITY_LIGHT)
- def get_load_name(number):
- return f"Mock Load #{number}"
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: 102},
+ blocking=True,
+ )
+ mock_litejet.activate_load_at.assert_called_with(ENTITY_LIGHT_NUMBER, 39, 0)
- def on_load_activated(number, callback):
- self.load_activated_callbacks[number] = callback
- def on_load_deactivated(number, callback):
- self.load_deactivated_callbacks[number] = callback
+async def test_on_off(hass, mock_litejet):
+ """Test turning the light on and off."""
+ await async_init_integration(hass)
- self.mock_lj = mock_pylitejet.return_value
- self.mock_lj.loads.return_value = range(1, 3)
- self.mock_lj.button_switches.return_value = range(0)
- self.mock_lj.all_switches.return_value = range(0)
- self.mock_lj.scenes.return_value = range(0)
- self.mock_lj.get_load_level.return_value = 0
- self.mock_lj.get_load_name.side_effect = get_load_name
- self.mock_lj.on_load_activated.side_effect = on_load_activated
- self.mock_lj.on_load_deactivated.side_effect = on_load_deactivated
+ assert hass.states.get(ENTITY_LIGHT).state == "off"
+ assert hass.states.get(ENTITY_OTHER_LIGHT).state == "off"
- assert setup.setup_component(
- self.hass,
- litejet.DOMAIN,
- {"litejet": {"port": "/dev/serial/by-id/mock-litejet"}},
- )
- self.hass.block_till_done()
+ assert not light.is_on(hass, ENTITY_LIGHT)
- self.mock_lj.get_load_level.reset_mock()
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ENTITY_LIGHT},
+ blocking=True,
+ )
+ mock_litejet.activate_load.assert_called_with(ENTITY_LIGHT_NUMBER)
- def light(self):
- """Test for main light entity."""
- return self.hass.states.get(ENTITY_LIGHT)
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_LIGHT},
+ blocking=True,
+ )
+ mock_litejet.deactivate_load.assert_called_with(ENTITY_LIGHT_NUMBER)
- def other_light(self):
- """Test the other light."""
- return self.hass.states.get(ENTITY_OTHER_LIGHT)
- def teardown_method(self, method):
- """Stop everything that was started."""
- self.hass.stop()
+async def test_activated_event(hass, mock_litejet):
+ """Test handling an event from LiteJet."""
- def test_on_brightness(self):
- """Test turning the light on with brightness."""
- assert self.light().state == "off"
- assert self.other_light().state == "off"
+ await async_init_integration(hass)
- assert not light.is_on(self.hass, ENTITY_LIGHT)
+ # Light 1
+ mock_litejet.get_load_level.return_value = 99
+ mock_litejet.get_load_level.reset_mock()
+ mock_litejet.load_activated_callbacks[ENTITY_LIGHT_NUMBER]()
+ await hass.async_block_till_done()
- common.turn_on(self.hass, ENTITY_LIGHT, brightness=102)
- self.hass.block_till_done()
- self.mock_lj.activate_load_at.assert_called_with(ENTITY_LIGHT_NUMBER, 39, 0)
+ mock_litejet.get_load_level.assert_called_once_with(ENTITY_LIGHT_NUMBER)
- def test_on_off(self):
- """Test turning the light on and off."""
- assert self.light().state == "off"
- assert self.other_light().state == "off"
+ assert light.is_on(hass, ENTITY_LIGHT)
+ assert not light.is_on(hass, ENTITY_OTHER_LIGHT)
+ assert hass.states.get(ENTITY_LIGHT).state == "on"
+ assert hass.states.get(ENTITY_OTHER_LIGHT).state == "off"
+ assert hass.states.get(ENTITY_LIGHT).attributes.get(ATTR_BRIGHTNESS) == 255
- assert not light.is_on(self.hass, ENTITY_LIGHT)
+ # Light 2
- common.turn_on(self.hass, ENTITY_LIGHT)
- self.hass.block_till_done()
- self.mock_lj.activate_load.assert_called_with(ENTITY_LIGHT_NUMBER)
+ mock_litejet.get_load_level.return_value = 40
+ mock_litejet.get_load_level.reset_mock()
+ mock_litejet.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]()
+ await hass.async_block_till_done()
- common.turn_off(self.hass, ENTITY_LIGHT)
- self.hass.block_till_done()
- self.mock_lj.deactivate_load.assert_called_with(ENTITY_LIGHT_NUMBER)
+ mock_litejet.get_load_level.assert_called_once_with(ENTITY_OTHER_LIGHT_NUMBER)
- def test_activated_event(self):
- """Test handling an event from LiteJet."""
- self.mock_lj.get_load_level.return_value = 99
+ assert light.is_on(hass, ENTITY_LIGHT)
+ assert light.is_on(hass, ENTITY_OTHER_LIGHT)
+ assert hass.states.get(ENTITY_LIGHT).state == "on"
+ assert hass.states.get(ENTITY_OTHER_LIGHT).state == "on"
+ assert (
+ int(hass.states.get(ENTITY_OTHER_LIGHT).attributes.get(ATTR_BRIGHTNESS)) == 103
+ )
- # Light 1
- _LOGGER.info(self.load_activated_callbacks[ENTITY_LIGHT_NUMBER])
- self.load_activated_callbacks[ENTITY_LIGHT_NUMBER]()
- self.hass.block_till_done()
+async def test_deactivated_event(hass, mock_litejet):
+ """Test handling an event from LiteJet."""
+ await async_init_integration(hass)
- self.mock_lj.get_load_level.assert_called_once_with(ENTITY_LIGHT_NUMBER)
+ # Initial state is on.
+ mock_litejet.get_load_level.return_value = 99
- assert light.is_on(self.hass, ENTITY_LIGHT)
- assert not light.is_on(self.hass, ENTITY_OTHER_LIGHT)
- assert self.light().state == "on"
- assert self.other_light().state == "off"
- assert self.light().attributes.get(light.ATTR_BRIGHTNESS) == 255
+ mock_litejet.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]()
+ await hass.async_block_till_done()
- # Light 2
+ assert light.is_on(hass, ENTITY_OTHER_LIGHT)
- self.mock_lj.get_load_level.return_value = 40
+ # Event indicates it is off now.
- self.mock_lj.get_load_level.reset_mock()
+ mock_litejet.get_load_level.reset_mock()
+ mock_litejet.get_load_level.return_value = 0
- self.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]()
- self.hass.block_till_done()
+ mock_litejet.load_deactivated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]()
+ await hass.async_block_till_done()
- self.mock_lj.get_load_level.assert_called_once_with(ENTITY_OTHER_LIGHT_NUMBER)
+ # (Requesting the level is not strictly needed with a deactivated
+ # event but the implementation happens to do it. This could be
+ # changed to an assert_not_called in the future.)
+ mock_litejet.get_load_level.assert_called_with(ENTITY_OTHER_LIGHT_NUMBER)
- assert light.is_on(self.hass, ENTITY_OTHER_LIGHT)
- assert light.is_on(self.hass, ENTITY_LIGHT)
- assert self.light().state == "on"
- assert self.other_light().state == "on"
- assert int(self.other_light().attributes[light.ATTR_BRIGHTNESS]) == 103
-
- def test_deactivated_event(self):
- """Test handling an event from LiteJet."""
- # Initial state is on.
-
- self.mock_lj.get_load_level.return_value = 99
-
- self.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]()
- self.hass.block_till_done()
-
- assert light.is_on(self.hass, ENTITY_OTHER_LIGHT)
-
- # Event indicates it is off now.
-
- self.mock_lj.get_load_level.reset_mock()
- self.mock_lj.get_load_level.return_value = 0
-
- self.load_deactivated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]()
- self.hass.block_till_done()
-
- # (Requesting the level is not strictly needed with a deactivated
- # event but the implementation happens to do it. This could be
- # changed to an assert_not_called in the future.)
- self.mock_lj.get_load_level.assert_called_with(ENTITY_OTHER_LIGHT_NUMBER)
-
- assert not light.is_on(self.hass, ENTITY_OTHER_LIGHT)
- assert not light.is_on(self.hass, ENTITY_LIGHT)
- assert self.light().state == "off"
- assert self.other_light().state == "off"
+ assert not light.is_on(hass, ENTITY_OTHER_LIGHT)
+ assert not light.is_on(hass, ENTITY_LIGHT)
+ assert hass.states.get(ENTITY_LIGHT).state == "off"
+ assert hass.states.get(ENTITY_OTHER_LIGHT).state == "off"
diff --git a/tests/components/litejet/test_scene.py b/tests/components/litejet/test_scene.py
index c2297af6d3f509..c76c8738f86d89 100644
--- a/tests/components/litejet/test_scene.py
+++ b/tests/components/litejet/test_scene.py
@@ -1,15 +1,9 @@
"""The tests for the litejet component."""
-import logging
-import unittest
-from unittest import mock
+from homeassistant.components import scene
+from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON
+from homeassistant.helpers import entity_registry as er
-from homeassistant import setup
-from homeassistant.components import litejet
-
-from tests.common import get_test_home_assistant
-from tests.components.scene import common
-
-_LOGGER = logging.getLogger(__name__)
+from . import async_init_integration
ENTITY_SCENE = "scene.mock_scene_1"
ENTITY_SCENE_NUMBER = 1
@@ -17,46 +11,31 @@
ENTITY_OTHER_SCENE_NUMBER = 2
-class TestLiteJetScene(unittest.TestCase):
- """Test the litejet component."""
-
- @mock.patch("homeassistant.components.litejet.LiteJet")
- def setup_method(self, method, mock_pylitejet):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.hass.start()
-
- def get_scene_name(number):
- return f"Mock Scene #{number}"
-
- self.mock_lj = mock_pylitejet.return_value
- self.mock_lj.loads.return_value = range(0)
- self.mock_lj.button_switches.return_value = range(0)
- self.mock_lj.all_switches.return_value = range(0)
- self.mock_lj.scenes.return_value = range(1, 3)
- self.mock_lj.get_scene_name.side_effect = get_scene_name
-
- assert setup.setup_component(
- self.hass,
- litejet.DOMAIN,
- {"litejet": {"port": "/dev/serial/by-id/mock-litejet"}},
- )
- self.hass.block_till_done()
-
- def teardown_method(self, method):
- """Stop everything that was started."""
- self.hass.stop()
-
- def scene(self):
- """Get the current scene."""
- return self.hass.states.get(ENTITY_SCENE)
-
- def other_scene(self):
- """Get the other scene."""
- return self.hass.states.get(ENTITY_OTHER_SCENE)
-
- def test_activate(self):
- """Test activating the scene."""
- common.activate(self.hass, ENTITY_SCENE)
- self.hass.block_till_done()
- self.mock_lj.activate_scene.assert_called_once_with(ENTITY_SCENE_NUMBER)
+async def test_disabled_by_default(hass, mock_litejet):
+ """Test the scene is disabled by default."""
+ await async_init_integration(hass)
+
+ registry = er.async_get(hass)
+
+ state = hass.states.get(ENTITY_SCENE)
+ assert state is None
+
+ entry = registry.async_get(ENTITY_SCENE)
+ assert entry
+ assert entry.disabled
+ assert entry.disabled_by == "integration"
+
+
+async def test_activate(hass, mock_litejet):
+ """Test activating the scene."""
+
+ await async_init_integration(hass, use_scene=True)
+
+ state = hass.states.get(ENTITY_SCENE)
+ assert state is not None
+
+ await hass.services.async_call(
+ scene.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SCENE}, blocking=True
+ )
+
+ mock_litejet.activate_scene.assert_called_once_with(ENTITY_SCENE_NUMBER)
diff --git a/tests/components/litejet/test_switch.py b/tests/components/litejet/test_switch.py
index 2f897045c924a9..dfcb980109328a 100644
--- a/tests/components/litejet/test_switch.py
+++ b/tests/components/litejet/test_switch.py
@@ -1,14 +1,10 @@
"""The tests for the litejet component."""
import logging
-import unittest
-from unittest import mock
-from homeassistant import setup
-from homeassistant.components import litejet
-import homeassistant.components.switch as switch
+from homeassistant.components import switch
+from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
-from tests.common import get_test_home_assistant
-from tests.components.switch import common
+from . import async_init_integration
_LOGGER = logging.getLogger(__name__)
@@ -18,117 +14,67 @@
ENTITY_OTHER_SWITCH_NUMBER = 2
-class TestLiteJetSwitch(unittest.TestCase):
- """Test the litejet component."""
-
- @mock.patch("homeassistant.components.litejet.LiteJet")
- def setup_method(self, method, mock_pylitejet):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.hass.start()
+async def test_on_off(hass, mock_litejet):
+ """Test turning the switch on and off."""
- self.switch_pressed_callbacks = {}
- self.switch_released_callbacks = {}
+ await async_init_integration(hass, use_switch=True)
- def get_switch_name(number):
- return f"Mock Switch #{number}"
-
- def on_switch_pressed(number, callback):
- self.switch_pressed_callbacks[number] = callback
-
- def on_switch_released(number, callback):
- self.switch_released_callbacks[number] = callback
-
- self.mock_lj = mock_pylitejet.return_value
- self.mock_lj.loads.return_value = range(0)
- self.mock_lj.button_switches.return_value = range(1, 3)
- self.mock_lj.all_switches.return_value = range(1, 6)
- self.mock_lj.scenes.return_value = range(0)
- self.mock_lj.get_switch_name.side_effect = get_switch_name
- self.mock_lj.on_switch_pressed.side_effect = on_switch_pressed
- self.mock_lj.on_switch_released.side_effect = on_switch_released
-
- config = {"litejet": {"port": "/dev/serial/by-id/mock-litejet"}}
- if method == self.test_include_switches_False:
- config["litejet"]["include_switches"] = False
- elif method != self.test_include_switches_unspecified:
- config["litejet"]["include_switches"] = True
-
- assert setup.setup_component(self.hass, litejet.DOMAIN, config)
- self.hass.block_till_done()
-
- def teardown_method(self, method):
- """Stop everything that was started."""
- self.hass.stop()
-
- def switch(self):
- """Return the switch state."""
- return self.hass.states.get(ENTITY_SWITCH)
-
- def other_switch(self):
- """Return the other switch state."""
- return self.hass.states.get(ENTITY_OTHER_SWITCH)
-
- def test_include_switches_unspecified(self):
- """Test that switches are ignored by default."""
- self.mock_lj.button_switches.assert_not_called()
- self.mock_lj.all_switches.assert_not_called()
-
- def test_include_switches_False(self):
- """Test that switches can be explicitly ignored."""
- self.mock_lj.button_switches.assert_not_called()
- self.mock_lj.all_switches.assert_not_called()
-
- def test_on_off(self):
- """Test turning the switch on and off."""
- assert self.switch().state == "off"
- assert self.other_switch().state == "off"
-
- assert not switch.is_on(self.hass, ENTITY_SWITCH)
-
- common.turn_on(self.hass, ENTITY_SWITCH)
- self.hass.block_till_done()
- self.mock_lj.press_switch.assert_called_with(ENTITY_SWITCH_NUMBER)
-
- common.turn_off(self.hass, ENTITY_SWITCH)
- self.hass.block_till_done()
- self.mock_lj.release_switch.assert_called_with(ENTITY_SWITCH_NUMBER)
-
- def test_pressed_event(self):
- """Test handling an event from LiteJet."""
- # Switch 1
- _LOGGER.info(self.switch_pressed_callbacks[ENTITY_SWITCH_NUMBER])
- self.switch_pressed_callbacks[ENTITY_SWITCH_NUMBER]()
- self.hass.block_till_done()
-
- assert switch.is_on(self.hass, ENTITY_SWITCH)
- assert not switch.is_on(self.hass, ENTITY_OTHER_SWITCH)
- assert self.switch().state == "on"
- assert self.other_switch().state == "off"
-
- # Switch 2
- self.switch_pressed_callbacks[ENTITY_OTHER_SWITCH_NUMBER]()
- self.hass.block_till_done()
-
- assert switch.is_on(self.hass, ENTITY_OTHER_SWITCH)
- assert switch.is_on(self.hass, ENTITY_SWITCH)
- assert self.other_switch().state == "on"
- assert self.switch().state == "on"
-
- def test_released_event(self):
- """Test handling an event from LiteJet."""
- # Initial state is on.
- self.switch_pressed_callbacks[ENTITY_OTHER_SWITCH_NUMBER]()
- self.hass.block_till_done()
-
- assert switch.is_on(self.hass, ENTITY_OTHER_SWITCH)
-
- # Event indicates it is off now.
-
- self.switch_released_callbacks[ENTITY_OTHER_SWITCH_NUMBER]()
- self.hass.block_till_done()
-
- assert not switch.is_on(self.hass, ENTITY_OTHER_SWITCH)
- assert not switch.is_on(self.hass, ENTITY_SWITCH)
- assert self.other_switch().state == "off"
- assert self.switch().state == "off"
+ assert hass.states.get(ENTITY_SWITCH).state == "off"
+ assert hass.states.get(ENTITY_OTHER_SWITCH).state == "off"
+
+ assert not switch.is_on(hass, ENTITY_SWITCH)
+
+ await hass.services.async_call(
+ switch.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True
+ )
+ mock_litejet.press_switch.assert_called_with(ENTITY_SWITCH_NUMBER)
+
+ await hass.services.async_call(
+ switch.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True
+ )
+ mock_litejet.release_switch.assert_called_with(ENTITY_SWITCH_NUMBER)
+
+
+async def test_pressed_event(hass, mock_litejet):
+ """Test handling an event from LiteJet."""
+
+ await async_init_integration(hass, use_switch=True)
+
+ # Switch 1
+ mock_litejet.switch_pressed_callbacks[ENTITY_SWITCH_NUMBER]()
+ await hass.async_block_till_done()
+
+ assert switch.is_on(hass, ENTITY_SWITCH)
+ assert not switch.is_on(hass, ENTITY_OTHER_SWITCH)
+ assert hass.states.get(ENTITY_SWITCH).state == "on"
+ assert hass.states.get(ENTITY_OTHER_SWITCH).state == "off"
+
+ # Switch 2
+ mock_litejet.switch_pressed_callbacks[ENTITY_OTHER_SWITCH_NUMBER]()
+ await hass.async_block_till_done()
+
+ assert switch.is_on(hass, ENTITY_OTHER_SWITCH)
+ assert switch.is_on(hass, ENTITY_SWITCH)
+ assert hass.states.get(ENTITY_SWITCH).state == "on"
+ assert hass.states.get(ENTITY_OTHER_SWITCH).state == "on"
+
+
+async def test_released_event(hass, mock_litejet):
+ """Test handling an event from LiteJet."""
+
+ await async_init_integration(hass, use_switch=True)
+
+ # Initial state is on.
+ mock_litejet.switch_pressed_callbacks[ENTITY_OTHER_SWITCH_NUMBER]()
+ await hass.async_block_till_done()
+
+ assert switch.is_on(hass, ENTITY_OTHER_SWITCH)
+
+ # Event indicates it is off now.
+ mock_litejet.switch_released_callbacks[ENTITY_OTHER_SWITCH_NUMBER]()
+ await hass.async_block_till_done()
+
+ assert not switch.is_on(hass, ENTITY_OTHER_SWITCH)
+ assert not switch.is_on(hass, ENTITY_SWITCH)
+ assert hass.states.get(ENTITY_SWITCH).state == "off"
+ assert hass.states.get(ENTITY_OTHER_SWITCH).state == "off"
diff --git a/tests/components/litejet/test_trigger.py b/tests/components/litejet/test_trigger.py
index 3cbbd474b88a2b..a06d490cea6114 100644
--- a/tests/components/litejet/test_trigger.py
+++ b/tests/components/litejet/test_trigger.py
@@ -2,16 +2,18 @@
from datetime import timedelta
import logging
from unittest import mock
+from unittest.mock import patch
import pytest
from homeassistant import setup
-from homeassistant.components import litejet
import homeassistant.components.automation as automation
import homeassistant.util.dt as dt_util
+from . import async_init_integration
+
from tests.common import async_fire_time_changed, async_mock_service
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
_LOGGER = logging.getLogger(__name__)
@@ -27,88 +29,51 @@ def calls(hass):
return async_mock_service(hass, "test", "automation")
-def get_switch_name(number):
- """Get a mock switch name."""
- return f"Mock Switch #{number}"
-
-
-@pytest.fixture
-def mock_lj(hass):
- """Initialize components."""
- with mock.patch("homeassistant.components.litejet.LiteJet") as mock_pylitejet:
- mock_lj = mock_pylitejet.return_value
-
- mock_lj.switch_pressed_callbacks = {}
- mock_lj.switch_released_callbacks = {}
-
- def on_switch_pressed(number, callback):
- mock_lj.switch_pressed_callbacks[number] = callback
-
- def on_switch_released(number, callback):
- mock_lj.switch_released_callbacks[number] = callback
-
- mock_lj.loads.return_value = range(0)
- mock_lj.button_switches.return_value = range(1, 3)
- mock_lj.all_switches.return_value = range(1, 6)
- mock_lj.scenes.return_value = range(0)
- mock_lj.get_switch_name.side_effect = get_switch_name
- mock_lj.on_switch_pressed.side_effect = on_switch_pressed
- mock_lj.on_switch_released.side_effect = on_switch_released
-
- config = {"litejet": {"port": "/dev/serial/by-id/mock-litejet"}}
- assert hass.loop.run_until_complete(
- setup.async_setup_component(hass, litejet.DOMAIN, config)
- )
-
- mock_lj.start_time = dt_util.utcnow()
- mock_lj.last_delta = timedelta(0)
- return mock_lj
-
-
-async def simulate_press(hass, mock_lj, number):
+async def simulate_press(hass, mock_litejet, number):
"""Test to simulate a press."""
_LOGGER.info("*** simulate press of %d", number)
- callback = mock_lj.switch_pressed_callbacks.get(number)
+ callback = mock_litejet.switch_pressed_callbacks.get(number)
with mock.patch(
"homeassistant.helpers.condition.dt_util.utcnow",
- return_value=mock_lj.start_time + mock_lj.last_delta,
+ return_value=mock_litejet.start_time + mock_litejet.last_delta,
):
if callback is not None:
await hass.async_add_executor_job(callback)
await hass.async_block_till_done()
-async def simulate_release(hass, mock_lj, number):
+async def simulate_release(hass, mock_litejet, number):
"""Test to simulate releasing."""
_LOGGER.info("*** simulate release of %d", number)
- callback = mock_lj.switch_released_callbacks.get(number)
+ callback = mock_litejet.switch_released_callbacks.get(number)
with mock.patch(
"homeassistant.helpers.condition.dt_util.utcnow",
- return_value=mock_lj.start_time + mock_lj.last_delta,
+ return_value=mock_litejet.start_time + mock_litejet.last_delta,
):
if callback is not None:
await hass.async_add_executor_job(callback)
await hass.async_block_till_done()
-async def simulate_time(hass, mock_lj, delta):
+async def simulate_time(hass, mock_litejet, delta):
"""Test to simulate time."""
_LOGGER.info(
- "*** simulate time change by %s: %s", delta, mock_lj.start_time + delta
+ "*** simulate time change by %s: %s", delta, mock_litejet.start_time + delta
)
- mock_lj.last_delta = delta
+ mock_litejet.last_delta = delta
with mock.patch(
"homeassistant.helpers.condition.dt_util.utcnow",
- return_value=mock_lj.start_time + delta,
+ return_value=mock_litejet.start_time + delta,
):
_LOGGER.info("now=%s", dt_util.utcnow())
- async_fire_time_changed(hass, mock_lj.start_time + delta)
+ async_fire_time_changed(hass, mock_litejet.start_time + delta)
await hass.async_block_till_done()
_LOGGER.info("done with now=%s", dt_util.utcnow())
async def setup_automation(hass, trigger):
"""Test setting up the automation."""
+ await async_init_integration(hass, use_switch=True)
assert await setup.async_setup_component(
hass,
automation.DOMAIN,
@@ -117,7 +82,10 @@ async def setup_automation(hass, trigger):
{
"alias": "My Test",
"trigger": trigger,
- "action": {"service": "test.automation"},
+ "action": {
+ "service": "test.automation",
+ "data_template": {"id": "{{ trigger.id}}"},
+ },
}
]
},
@@ -125,19 +93,20 @@ async def setup_automation(hass, trigger):
await hass.async_block_till_done()
-async def test_simple(hass, calls, mock_lj):
+async def test_simple(hass, calls, mock_litejet):
"""Test the simplest form of a LiteJet trigger."""
await setup_automation(
hass, {"platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER}
)
- await simulate_press(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER)
- await simulate_release(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER)
+ await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER)
+ await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER)
assert len(calls) == 1
+ assert calls[0].data["id"] == 0
-async def test_held_more_than_short(hass, calls, mock_lj):
+async def test_held_more_than_short(hass, calls, mock_litejet):
"""Test a too short hold."""
await setup_automation(
hass,
@@ -148,13 +117,13 @@ async def test_held_more_than_short(hass, calls, mock_lj):
},
)
- await simulate_press(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER)
- await simulate_time(hass, mock_lj, timedelta(seconds=0.1))
- await simulate_release(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER)
+ await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER)
+ await simulate_time(hass, mock_litejet, timedelta(seconds=0.1))
+ await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER)
assert len(calls) == 0
-async def test_held_more_than_long(hass, calls, mock_lj):
+async def test_held_more_than_long(hass, calls, mock_litejet):
"""Test a hold that is long enough."""
await setup_automation(
hass,
@@ -165,15 +134,16 @@ async def test_held_more_than_long(hass, calls, mock_lj):
},
)
- await simulate_press(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER)
+ await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER)
assert len(calls) == 0
- await simulate_time(hass, mock_lj, timedelta(seconds=0.3))
+ await simulate_time(hass, mock_litejet, timedelta(seconds=0.3))
assert len(calls) == 1
- await simulate_release(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER)
+ assert calls[0].data["id"] == 0
+ await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER)
assert len(calls) == 1
-async def test_held_less_than_short(hass, calls, mock_lj):
+async def test_held_less_than_short(hass, calls, mock_litejet):
"""Test a hold that is short enough."""
await setup_automation(
hass,
@@ -184,14 +154,15 @@ async def test_held_less_than_short(hass, calls, mock_lj):
},
)
- await simulate_press(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER)
- await simulate_time(hass, mock_lj, timedelta(seconds=0.1))
+ await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER)
+ await simulate_time(hass, mock_litejet, timedelta(seconds=0.1))
assert len(calls) == 0
- await simulate_release(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER)
+ await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER)
assert len(calls) == 1
+ assert calls[0].data["id"] == 0
-async def test_held_less_than_long(hass, calls, mock_lj):
+async def test_held_less_than_long(hass, calls, mock_litejet):
"""Test a hold that is too long."""
await setup_automation(
hass,
@@ -202,15 +173,15 @@ async def test_held_less_than_long(hass, calls, mock_lj):
},
)
- await simulate_press(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER)
+ await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER)
assert len(calls) == 0
- await simulate_time(hass, mock_lj, timedelta(seconds=0.3))
+ await simulate_time(hass, mock_litejet, timedelta(seconds=0.3))
assert len(calls) == 0
- await simulate_release(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER)
+ await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER)
assert len(calls) == 0
-async def test_held_in_range_short(hass, calls, mock_lj):
+async def test_held_in_range_short(hass, calls, mock_litejet):
"""Test an in-range trigger with a too short hold."""
await setup_automation(
hass,
@@ -222,13 +193,13 @@ async def test_held_in_range_short(hass, calls, mock_lj):
},
)
- await simulate_press(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER)
- await simulate_time(hass, mock_lj, timedelta(seconds=0.05))
- await simulate_release(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER)
+ await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER)
+ await simulate_time(hass, mock_litejet, timedelta(seconds=0.05))
+ await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER)
assert len(calls) == 0
-async def test_held_in_range_just_right(hass, calls, mock_lj):
+async def test_held_in_range_just_right(hass, calls, mock_litejet):
"""Test an in-range trigger with a just right hold."""
await setup_automation(
hass,
@@ -240,15 +211,16 @@ async def test_held_in_range_just_right(hass, calls, mock_lj):
},
)
- await simulate_press(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER)
+ await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER)
assert len(calls) == 0
- await simulate_time(hass, mock_lj, timedelta(seconds=0.2))
+ await simulate_time(hass, mock_litejet, timedelta(seconds=0.2))
assert len(calls) == 0
- await simulate_release(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER)
+ await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER)
assert len(calls) == 1
+ assert calls[0].data["id"] == 0
-async def test_held_in_range_long(hass, calls, mock_lj):
+async def test_held_in_range_long(hass, calls, mock_litejet):
"""Test an in-range trigger with a too long hold."""
await setup_automation(
hass,
@@ -260,9 +232,50 @@ async def test_held_in_range_long(hass, calls, mock_lj):
},
)
- await simulate_press(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER)
+ await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER)
+ assert len(calls) == 0
+ await simulate_time(hass, mock_litejet, timedelta(seconds=0.4))
assert len(calls) == 0
- await simulate_time(hass, mock_lj, timedelta(seconds=0.4))
+ await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER)
+ assert len(calls) == 0
+
+
+async def test_reload(hass, calls, mock_litejet):
+ """Test reloading automation."""
+ await setup_automation(
+ hass,
+ {
+ "platform": "litejet",
+ "number": ENTITY_OTHER_SWITCH_NUMBER,
+ "held_more_than": {"milliseconds": "100"},
+ "held_less_than": {"milliseconds": "300"},
+ },
+ )
+
+ with patch(
+ "homeassistant.config.load_yaml_config_file",
+ autospec=True,
+ return_value={
+ "automation": {
+ "trigger": {
+ "platform": "litejet",
+ "number": ENTITY_OTHER_SWITCH_NUMBER,
+ "held_more_than": {"milliseconds": "1000"},
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ ):
+ await hass.services.async_call(
+ "automation",
+ "reload",
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER)
assert len(calls) == 0
- await simulate_release(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER)
+ await simulate_time(hass, mock_litejet, timedelta(seconds=0.5))
assert len(calls) == 0
+ await simulate_time(hass, mock_litejet, timedelta(seconds=1.25))
+ assert len(calls) == 1
diff --git a/tests/components/litterrobot/__init__.py b/tests/components/litterrobot/__init__.py
new file mode 100644
index 00000000000000..a726736510035e
--- /dev/null
+++ b/tests/components/litterrobot/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Litter-Robot Component."""
diff --git a/tests/components/litterrobot/common.py b/tests/components/litterrobot/common.py
new file mode 100644
index 00000000000000..ed893a3a756865
--- /dev/null
+++ b/tests/components/litterrobot/common.py
@@ -0,0 +1,24 @@
+"""Common utils for Litter-Robot tests."""
+from homeassistant.components.litterrobot import DOMAIN
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+
+BASE_PATH = "homeassistant.components.litterrobot"
+CONFIG = {DOMAIN: {CONF_USERNAME: "user@example.com", CONF_PASSWORD: "password"}}
+
+ROBOT_NAME = "Test"
+ROBOT_SERIAL = "LR3C012345"
+ROBOT_DATA = {
+ "powerStatus": "AC",
+ "lastSeen": "2021-02-01T15:30:00.000000",
+ "cleanCycleWaitTimeMinutes": "7",
+ "unitStatus": "RDY",
+ "litterRobotNickname": ROBOT_NAME,
+ "cycleCount": "15",
+ "panelLockActive": "0",
+ "cyclesAfterDrawerFull": "0",
+ "litterRobotSerial": ROBOT_SERIAL,
+ "cycleCapacity": "30",
+ "litterRobotId": "a0123b4567cd8e",
+ "nightLightActive": "1",
+ "sleepModeActive": "112:50:19",
+}
diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py
new file mode 100644
index 00000000000000..11ed66fcb5297c
--- /dev/null
+++ b/tests/components/litterrobot/conftest.py
@@ -0,0 +1,72 @@
+"""Configure pytest for Litter-Robot tests."""
+from __future__ import annotations
+
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pylitterbot
+from pylitterbot import Robot
+import pytest
+
+from homeassistant.components import litterrobot
+
+from .common import CONFIG, ROBOT_DATA
+
+from tests.common import MockConfigEntry
+
+
+def create_mock_robot(unit_status_code: str | None = None):
+ """Create a mock Litter-Robot device."""
+ if not (
+ unit_status_code
+ and Robot.UnitStatus(unit_status_code) != Robot.UnitStatus.UNKNOWN
+ ):
+ unit_status_code = ROBOT_DATA["unitStatus"]
+
+ with patch.dict(ROBOT_DATA, {"unitStatus": unit_status_code}):
+ robot = Robot(data=ROBOT_DATA)
+ robot.start_cleaning = AsyncMock()
+ robot.set_power_status = AsyncMock()
+ robot.reset_waste_drawer = AsyncMock()
+ robot.set_sleep_mode = AsyncMock()
+ robot.set_night_light = AsyncMock()
+ robot.set_panel_lockout = AsyncMock()
+ return robot
+
+
+def create_mock_account(unit_status_code: str | None = None):
+ """Create a mock Litter-Robot account."""
+ account = MagicMock(spec=pylitterbot.Account)
+ account.connect = AsyncMock()
+ account.refresh_robots = AsyncMock()
+ account.robots = [create_mock_robot(unit_status_code)]
+ return account
+
+
+@pytest.fixture
+def mock_account():
+ """Mock a Litter-Robot account."""
+ return create_mock_account()
+
+
+@pytest.fixture
+def mock_account_with_error():
+ """Mock a Litter-Robot account with error."""
+ return create_mock_account("BR")
+
+
+async def setup_integration(hass, mock_account, platform_domain=None):
+ """Load a Litter-Robot platform with the provided hub."""
+ entry = MockConfigEntry(
+ domain=litterrobot.DOMAIN,
+ data=CONFIG[litterrobot.DOMAIN],
+ )
+ entry.add_to_hass(hass)
+
+ with patch("pylitterbot.Account", return_value=mock_account), patch(
+ "homeassistant.components.litterrobot.PLATFORMS",
+ [platform_domain] if platform_domain else [],
+ ):
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ return entry
diff --git a/tests/components/litterrobot/test_config_flow.py b/tests/components/litterrobot/test_config_flow.py
new file mode 100644
index 00000000000000..5068ecf721bdd3
--- /dev/null
+++ b/tests/components/litterrobot/test_config_flow.py
@@ -0,0 +1,109 @@
+"""Test the Litter-Robot config flow."""
+from unittest.mock import patch
+
+from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException
+
+from homeassistant import config_entries, setup
+from homeassistant.components import litterrobot
+
+from .common import CONF_USERNAME, CONFIG, DOMAIN
+
+from tests.common import MockConfigEntry
+
+
+async def test_form(hass, mock_account):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch("pylitterbot.Account", return_value=mock_account), patch(
+ "homeassistant.components.litterrobot.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.litterrobot.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], CONFIG[DOMAIN]
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == CONFIG[DOMAIN][CONF_USERNAME]
+ assert result2["data"] == CONFIG[DOMAIN]
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_already_configured(hass):
+ """Test we handle already configured."""
+ MockConfigEntry(
+ domain=litterrobot.DOMAIN,
+ data=CONFIG[litterrobot.DOMAIN],
+ ).add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_USER},
+ data=CONFIG[litterrobot.DOMAIN],
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+
+
+async def test_form_invalid_auth(hass):
+ """Test we handle invalid auth."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "pylitterbot.Account.connect",
+ side_effect=LitterRobotLoginException,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], CONFIG[DOMAIN]
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "invalid_auth"}
+
+
+async def test_form_cannot_connect(hass):
+ """Test we handle cannot connect error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "pylitterbot.Account.connect",
+ side_effect=LitterRobotException,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], CONFIG[DOMAIN]
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "cannot_connect"}
+
+
+async def test_form_unknown_error(hass):
+ """Test we handle unknown error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "pylitterbot.Account.connect",
+ side_effect=Exception,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], CONFIG[DOMAIN]
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "unknown"}
diff --git a/tests/components/litterrobot/test_init.py b/tests/components/litterrobot/test_init.py
new file mode 100644
index 00000000000000..7cd36f33883c18
--- /dev/null
+++ b/tests/components/litterrobot/test_init.py
@@ -0,0 +1,48 @@
+"""Test Litter-Robot setup process."""
+from unittest.mock import patch
+
+from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException
+import pytest
+
+from homeassistant.components import litterrobot
+from homeassistant.config_entries import (
+ ENTRY_STATE_SETUP_ERROR,
+ ENTRY_STATE_SETUP_RETRY,
+)
+
+from .common import CONFIG
+from .conftest import setup_integration
+
+from tests.common import MockConfigEntry
+
+
+async def test_unload_entry(hass, mock_account):
+ """Test being able to unload an entry."""
+ entry = await setup_integration(hass, mock_account)
+
+ assert await hass.config_entries.async_unload(entry.entry_id)
+ await hass.async_block_till_done()
+ assert hass.data[litterrobot.DOMAIN] == {}
+
+
+@pytest.mark.parametrize(
+ "side_effect,expected_state",
+ (
+ (LitterRobotLoginException, ENTRY_STATE_SETUP_ERROR),
+ (LitterRobotException, ENTRY_STATE_SETUP_RETRY),
+ ),
+)
+async def test_entry_not_setup(hass, side_effect, expected_state):
+ """Test being able to handle config entry not setup."""
+ entry = MockConfigEntry(
+ domain=litterrobot.DOMAIN,
+ data=CONFIG[litterrobot.DOMAIN],
+ )
+ entry.add_to_hass(hass)
+
+ with patch(
+ "pylitterbot.Account.connect",
+ side_effect=side_effect,
+ ):
+ await hass.config_entries.async_setup(entry.entry_id)
+ assert entry.state == expected_state
diff --git a/tests/components/litterrobot/test_sensor.py b/tests/components/litterrobot/test_sensor.py
new file mode 100644
index 00000000000000..7f1570c553eeb2
--- /dev/null
+++ b/tests/components/litterrobot/test_sensor.py
@@ -0,0 +1,57 @@
+"""Test the Litter-Robot sensor entity."""
+from unittest.mock import Mock
+
+from homeassistant.components.litterrobot.sensor import LitterRobotSleepTimeSensor
+from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN
+from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE
+
+from .conftest import create_mock_robot, setup_integration
+
+WASTE_DRAWER_ENTITY_ID = "sensor.test_waste_drawer"
+
+
+async def test_waste_drawer_sensor(hass, mock_account):
+ """Tests the waste drawer sensor entity was set up."""
+ await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
+
+ sensor = hass.states.get(WASTE_DRAWER_ENTITY_ID)
+ assert sensor
+ assert sensor.state == "50"
+ assert sensor.attributes["unit_of_measurement"] == PERCENTAGE
+
+
+async def test_sleep_time_sensor_with_none_state(hass):
+ """Tests the sleep mode start time sensor where sleep mode is inactive."""
+ robot = create_mock_robot()
+ robot.sleep_mode_active = False
+ sensor = LitterRobotSleepTimeSensor(
+ robot, "Sleep Mode Start Time", Mock(), "sleep_mode_start_time"
+ )
+
+ assert sensor
+ assert sensor.state is None
+ assert sensor.device_class == DEVICE_CLASS_TIMESTAMP
+
+
+async def test_gauge_icon():
+ """Test icon generator for gauge sensor."""
+ from homeassistant.components.litterrobot.sensor import icon_for_gauge_level
+
+ GAUGE_EMPTY = "mdi:gauge-empty"
+ GAUGE_LOW = "mdi:gauge-low"
+ GAUGE = "mdi:gauge"
+ GAUGE_FULL = "mdi:gauge-full"
+
+ assert icon_for_gauge_level(None) == GAUGE_EMPTY
+ assert icon_for_gauge_level(0) == GAUGE_EMPTY
+ assert icon_for_gauge_level(5) == GAUGE_LOW
+ assert icon_for_gauge_level(40) == GAUGE
+ assert icon_for_gauge_level(80) == GAUGE_FULL
+ assert icon_for_gauge_level(100) == GAUGE_FULL
+
+ assert icon_for_gauge_level(None, 10) == GAUGE_EMPTY
+ assert icon_for_gauge_level(0, 10) == GAUGE_EMPTY
+ assert icon_for_gauge_level(5, 10) == GAUGE_EMPTY
+ assert icon_for_gauge_level(40, 10) == GAUGE_LOW
+ assert icon_for_gauge_level(80, 10) == GAUGE
+ assert icon_for_gauge_level(100, 10) == GAUGE_FULL
diff --git a/tests/components/litterrobot/test_switch.py b/tests/components/litterrobot/test_switch.py
new file mode 100644
index 00000000000000..69154bef8f5c8e
--- /dev/null
+++ b/tests/components/litterrobot/test_switch.py
@@ -0,0 +1,61 @@
+"""Test the Litter-Robot switch entity."""
+from datetime import timedelta
+
+import pytest
+
+from homeassistant.components.litterrobot.hub import REFRESH_WAIT_TIME
+from homeassistant.components.switch import (
+ DOMAIN as PLATFORM_DOMAIN,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+)
+from homeassistant.const import ATTR_ENTITY_ID, STATE_ON
+from homeassistant.util.dt import utcnow
+
+from .conftest import setup_integration
+
+from tests.common import async_fire_time_changed
+
+NIGHT_LIGHT_MODE_ENTITY_ID = "switch.test_night_light_mode"
+PANEL_LOCKOUT_ENTITY_ID = "switch.test_panel_lockout"
+
+
+async def test_switch(hass, mock_account):
+ """Tests the switch entity was set up."""
+ await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
+
+ switch = hass.states.get(NIGHT_LIGHT_MODE_ENTITY_ID)
+ assert switch
+ assert switch.state == STATE_ON
+
+
+@pytest.mark.parametrize(
+ "entity_id,robot_command",
+ [
+ (NIGHT_LIGHT_MODE_ENTITY_ID, "set_night_light"),
+ (PANEL_LOCKOUT_ENTITY_ID, "set_panel_lockout"),
+ ],
+)
+async def test_on_off_commands(hass, mock_account, entity_id, robot_command):
+ """Test sending commands to the switch."""
+ await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
+
+ switch = hass.states.get(entity_id)
+ assert switch
+
+ data = {ATTR_ENTITY_ID: entity_id}
+
+ count = 0
+ for service in [SERVICE_TURN_ON, SERVICE_TURN_OFF]:
+ count += 1
+
+ await hass.services.async_call(
+ PLATFORM_DOMAIN,
+ service,
+ data,
+ blocking=True,
+ )
+
+ future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME)
+ async_fire_time_changed(hass, future)
+ assert getattr(mock_account.robots[0], robot_command).call_count == count
diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py
new file mode 100644
index 00000000000000..2db2ef21546be5
--- /dev/null
+++ b/tests/components/litterrobot/test_vacuum.py
@@ -0,0 +1,95 @@
+"""Test the Litter-Robot vacuum entity."""
+from datetime import timedelta
+
+import pytest
+
+from homeassistant.components.litterrobot.hub import REFRESH_WAIT_TIME
+from homeassistant.components.vacuum import (
+ ATTR_PARAMS,
+ DOMAIN as PLATFORM_DOMAIN,
+ SERVICE_SEND_COMMAND,
+ SERVICE_START,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ STATE_DOCKED,
+ STATE_ERROR,
+)
+from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID
+from homeassistant.util.dt import utcnow
+
+from .conftest import setup_integration
+
+from tests.common import async_fire_time_changed
+
+ENTITY_ID = "vacuum.test_litter_box"
+
+
+async def test_vacuum(hass, mock_account):
+ """Tests the vacuum entity was set up."""
+ await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
+
+ vacuum = hass.states.get(ENTITY_ID)
+ assert vacuum
+ assert vacuum.state == STATE_DOCKED
+ assert vacuum.attributes["is_sleeping"] is False
+
+
+async def test_vacuum_with_error(hass, mock_account_with_error):
+ """Tests a vacuum entity with an error."""
+ await setup_integration(hass, mock_account_with_error, PLATFORM_DOMAIN)
+
+ vacuum = hass.states.get(ENTITY_ID)
+ assert vacuum
+ assert vacuum.state == STATE_ERROR
+
+
+@pytest.mark.parametrize(
+ "service,command,extra",
+ [
+ (SERVICE_START, "start_cleaning", None),
+ (SERVICE_TURN_OFF, "set_power_status", None),
+ (SERVICE_TURN_ON, "set_power_status", None),
+ (
+ SERVICE_SEND_COMMAND,
+ "reset_waste_drawer",
+ {ATTR_COMMAND: "reset_waste_drawer"},
+ ),
+ (
+ SERVICE_SEND_COMMAND,
+ "set_sleep_mode",
+ {
+ ATTR_COMMAND: "set_sleep_mode",
+ ATTR_PARAMS: {"enabled": True, "sleep_time": "22:30"},
+ },
+ ),
+ (
+ SERVICE_SEND_COMMAND,
+ "set_sleep_mode",
+ {
+ ATTR_COMMAND: "set_sleep_mode",
+ ATTR_PARAMS: {"enabled": True, "sleep_time": None},
+ },
+ ),
+ ],
+)
+async def test_commands(hass, mock_account, service, command, extra):
+ """Test sending commands to the vacuum."""
+ await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
+
+ vacuum = hass.states.get(ENTITY_ID)
+ assert vacuum
+ assert vacuum.state == STATE_DOCKED
+
+ data = {ATTR_ENTITY_ID: ENTITY_ID}
+ if extra:
+ data.update(extra)
+
+ await hass.services.async_call(
+ PLATFORM_DOMAIN,
+ service,
+ data,
+ blocking=True,
+ )
+ future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME)
+ async_fire_time_changed(hass, future)
+ getattr(mock_account.robots[0], command).assert_called_once()
diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py
index 91cab9cdaf4f58..7d484ae96aa378 100644
--- a/tests/components/lock/test_device_action.py
+++ b/tests/components/lock/test_device_action.py
@@ -15,7 +15,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/lock/test_device_condition.py b/tests/components/lock/test_device_condition.py
index 949100daa552ae..b021ef2339188e 100644
--- a/tests/components/lock/test_device_condition.py
+++ b/tests/components/lock/test_device_condition.py
@@ -15,7 +15,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py
index 20674c483fde3f..d4d96927b56f0a 100644
--- a/tests/components/lock/test_device_trigger.py
+++ b/tests/components/lock/test_device_trigger.py
@@ -1,4 +1,6 @@
"""The tests for Lock device triggers."""
+from datetime import timedelta
+
import pytest
import homeassistant.components.automation as automation
@@ -6,16 +8,19 @@
from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED
from homeassistant.helpers import device_registry
from homeassistant.setup import async_setup_component
+import homeassistant.util.dt as dt_util
from tests.common import (
MockConfigEntry,
assert_lists_same,
+ async_fire_time_changed,
+ async_get_device_automation_capabilities,
async_get_device_automations,
async_mock_service,
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
@@ -65,6 +70,29 @@ async def test_get_triggers(hass, device_reg, entity_reg):
assert_lists_same(triggers, expected_triggers)
+async def test_get_trigger_capabilities(hass, device_reg, entity_reg):
+ """Test we get the expected capabilities from a lock."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert len(triggers) == 2
+ for trigger in triggers:
+ capabilities = await async_get_device_automation_capabilities(
+ hass, "trigger", trigger
+ )
+ assert capabilities == {
+ "extra_fields": [
+ {"name": "for", "optional": True, "type": "positive_time_period_dict"}
+ ]
+ }
+
+
async def test_if_fires_on_state_change(hass, calls):
"""Test for turn_on and turn_off triggers firing."""
hass.states.async_set("lock.entity", STATE_UNLOCKED)
@@ -131,3 +159,58 @@ async def test_if_fires_on_state_change(hass, calls):
assert calls[1].data[
"some"
] == "unlocked - device - {} - locked - unlocked - None".format("lock.entity")
+
+
+async def test_if_fires_on_state_change_with_for(hass, calls):
+ """Test for triggers firing with delay."""
+ entity_id = f"{DOMAIN}.entity"
+ hass.states.async_set(entity_id, STATE_UNLOCKED)
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": entity_id,
+ "type": "locked",
+ "for": {"seconds": 5},
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "turn_off {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(
+ (
+ "platform",
+ "entity_id",
+ "from_state.state",
+ "to_state.state",
+ "for",
+ )
+ )
+ },
+ },
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(entity_id).state == STATE_UNLOCKED
+ assert len(calls) == 0
+
+ hass.states.async_set(entity_id, STATE_LOCKED)
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ await hass.async_block_till_done()
+ assert (
+ calls[0].data["some"]
+ == f"turn_off device - {entity_id} - unlocked - locked - 0:00:05"
+ )
diff --git a/tests/components/lock/test_significant_change.py b/tests/components/lock/test_significant_change.py
new file mode 100644
index 00000000000000..a9ffbc0d1c4c33
--- /dev/null
+++ b/tests/components/lock/test_significant_change.py
@@ -0,0 +1,23 @@
+"""Test the Lock significant change platform."""
+from homeassistant.components.lock.significant_change import (
+ async_check_significant_change,
+)
+
+
+async def test_significant_change():
+ """Detect Lock significant changes."""
+ old_attrs = {"attr_1": "a"}
+ new_attrs = {"attr_1": "b"}
+
+ assert (
+ async_check_significant_change(None, "locked", old_attrs, "locked", old_attrs)
+ is False
+ )
+ assert (
+ async_check_significant_change(None, "locked", old_attrs, "locked", new_attrs)
+ is False
+ )
+ assert (
+ async_check_significant_change(None, "locked", old_attrs, "unlocked", old_attrs)
+ is True
+ )
diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py
index cd3fb519dedc09..3dab7e6c2fbd18 100644
--- a/tests/components/logbook/test_init.py
+++ b/tests/components/logbook/test_init.py
@@ -1801,17 +1801,52 @@ async def test_empty_config(hass, hass_client):
_assert_entry(entries[1], name="blu", entity_id=entity_id)
-async def _async_fetch_logbook(client):
+async def test_context_filter(hass, hass_client):
+ """Test we can filter by context."""
+ await hass.async_add_executor_job(init_recorder_component, hass)
+ assert await async_setup_component(hass, "logbook", {})
+ await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
+
+ entity_id = "switch.blu"
+ context = ha.Context()
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ hass.states.async_set(entity_id, None)
+ hass.states.async_set(entity_id, "on", context=context)
+ hass.states.async_set(entity_id, "off")
+ hass.states.async_set(entity_id, "unknown", context=context)
+
+ await _async_commit_and_wait(hass)
+ client = await hass_client()
+
+ # Test results
+ entries = await _async_fetch_logbook(client, {"context_id": context.id})
+
+ assert len(entries) == 2
+ _assert_entry(entries[0], entity_id=entity_id, state="on")
+ _assert_entry(entries[1], entity_id=entity_id, state="unknown")
+
+ # Test we can't combine context filter with entity_id filter
+ response = await client.get(
+ "/api/logbook", params={"context_id": context.id, "entity": entity_id}
+ )
+ assert response.status == 400
+
+
+async def _async_fetch_logbook(client, params=None):
+ if params is None:
+ params = {}
# Today time 00:00:00
start = dt_util.utcnow().date()
start_date = datetime(start.year, start.month, start.day) - timedelta(hours=24)
+ if "end_time" not in params:
+ params["end_time"] = str(start + timedelta(hours=48))
+
# Test today entries without filters
- end_time = start + timedelta(hours=48)
- response = await client.get(
- f"/api/logbook/{start_date.isoformat()}?end_time={end_time}"
- )
+ response = await client.get(f"/api/logbook/{start_date.isoformat()}", params=params)
assert response.status == 200
return await response.json()
@@ -1825,7 +1860,7 @@ async def _async_commit_and_wait(hass):
def _assert_entry(
- entry, when=None, name=None, message=None, domain=None, entity_id=None
+ entry, when=None, name=None, message=None, domain=None, entity_id=None, state=None
):
"""Assert an entry is what is expected."""
if when:
@@ -1843,6 +1878,9 @@ def _assert_entry(
if entity_id:
assert entity_id == entry["entity_id"]
+ if state:
+ assert state == entry["state"]
+
class MockLazyEventPartialState(ha.Event):
"""Minimal mock of a Lazy event."""
diff --git a/tests/components/logentries/test_init.py b/tests/components/logentries/test_init.py
index 3ae25d521cb781..96632865af098e 100644
--- a/tests/components/logentries/test_init.py
+++ b/tests/components/logentries/test_init.py
@@ -15,7 +15,7 @@ async def test_setup_config_full(hass):
hass.bus.listen = MagicMock()
assert await async_setup_component(hass, logentries.DOMAIN, config)
assert hass.bus.listen.called
- assert EVENT_STATE_CHANGED == hass.bus.listen.call_args_list[0][0][0]
+ assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED
async def test_setup_config_defaults(hass):
@@ -24,7 +24,7 @@ async def test_setup_config_defaults(hass):
hass.bus.listen = MagicMock()
assert await async_setup_component(hass, logentries.DOMAIN, config)
assert hass.bus.listen.called
- assert EVENT_STATE_CHANGED == hass.bus.listen.call_args_list[0][0][0]
+ assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED
@pytest.fixture
diff --git a/tests/components/logger/test_init.py b/tests/components/logger/test_init.py
index abb8c9b0de8fb2..d2b0e8931b6908 100644
--- a/tests/components/logger/test_init.py
+++ b/tests/components/logger/test_init.py
@@ -25,6 +25,72 @@ def restore_logging_class():
logging.setLoggerClass(klass)
+async def test_log_filtering(hass, caplog):
+ """Test logging filters."""
+
+ assert await async_setup_component(
+ hass,
+ "logger",
+ {
+ "logger": {
+ "default": "warning",
+ "logs": {
+ "test.filter": "info",
+ },
+ "filters": {
+ "test.filter": [
+ "doesntmatchanything",
+ ".*shouldfilterall.*",
+ "^filterthis:.*",
+ ],
+ "test.other_filter": [".*otherfilterer"],
+ },
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ filter_logger = logging.getLogger("test.filter")
+
+ def msg_test(logger, result, message, *args):
+ logger.error(message, *args)
+ formatted_message = message % args
+ assert (formatted_message in caplog.text) == result
+ caplog.clear()
+
+ msg_test(
+ filter_logger, False, "this line containing shouldfilterall should be filtered"
+ )
+ msg_test(filter_logger, True, "this line should not be filtered filterthis:")
+ msg_test(filter_logger, False, "filterthis: should be filtered")
+ msg_test(filter_logger, False, "format string shouldfilter%s", "all")
+ msg_test(filter_logger, True, "format string shouldfilter%s", "not")
+
+ # Filtering should work even if log level is modified
+ await hass.services.async_call(
+ "logger",
+ "set_level",
+ {"test.filter": "warning"},
+ blocking=True,
+ )
+ assert filter_logger.getEffectiveLevel() == logging.WARNING
+ msg_test(
+ filter_logger,
+ False,
+ "this line containing shouldfilterall should still be filtered",
+ )
+
+ # Filtering should be scoped to a service
+ msg_test(
+ filter_logger, True, "this line containing otherfilterer should not be filtered"
+ )
+ msg_test(
+ logging.getLogger("test.other_filter"),
+ False,
+ "this line containing otherfilterer SHOULD be filtered",
+ )
+
+
async def test_setting_level(hass):
"""Test we set log levels."""
mocks = defaultdict(Mock)
@@ -165,9 +231,9 @@ async def test_can_set_level(hass):
logger.DOMAIN, "set_level", {f"{UNCONFIG_NS}.any": "debug"}, blocking=True
)
- logging.getLogger(UNCONFIG_NS).level == logging.NOTSET
- logging.getLogger(f"{UNCONFIG_NS}.any").level == logging.DEBUG
- logging.getLogger(UNCONFIG_NS).level == logging.NOTSET
+ assert logging.getLogger(UNCONFIG_NS).level == logging.NOTSET
+ assert logging.getLogger(f"{UNCONFIG_NS}.any").level == logging.DEBUG
+ assert logging.getLogger(UNCONFIG_NS).level == logging.NOTSET
await hass.services.async_call(
logger.DOMAIN, "set_default_level", {"level": "debug"}, blocking=True
diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py
index 8a33fab670bfdf..e65eb9d5f476c2 100644
--- a/tests/components/lutron_caseta/test_config_flow.py
+++ b/tests/components/lutron_caseta/test_config_flow.py
@@ -1,5 +1,6 @@
"""Test the Lutron Caseta config flow."""
import asyncio
+import ssl
from unittest.mock import AsyncMock, patch
from pylutron_caseta.pairing import PAIR_CA, PAIR_CERT, PAIR_KEY
@@ -16,11 +17,20 @@
ERROR_CANNOT_CONNECT,
STEP_IMPORT_FAILED,
)
-from homeassistant.components.zeroconf import ATTR_HOSTNAME
from homeassistant.const import CONF_HOST
from tests.common import MockConfigEntry
+ATTR_HOSTNAME = "hostname"
+
+EMPTY_MOCK_CONFIG_ENTRY = {
+ CONF_HOST: "",
+ CONF_KEYFILE: "",
+ CONF_CERTFILE: "",
+ CONF_CA_CERTS: "",
+}
+
+
MOCK_ASYNC_PAIR_SUCCESS = {
PAIR_KEY: "mock_key",
PAIR_CERT: "mock_cert",
@@ -115,21 +125,34 @@ async def test_bridge_cannot_connect(hass):
async def test_bridge_cannot_connect_unknown_error(hass):
"""Test checking for connection and encountering an unknown error."""
- entry_mock_data = {
- CONF_HOST: "",
- CONF_KEYFILE: "",
- CONF_CERTFILE: "",
- CONF_CA_CERTS: "",
- }
-
with patch.object(Smartbridge, "create_tls") as create_tls:
mock_bridge = MockBridge()
- mock_bridge.connect = AsyncMock(side_effect=Exception())
+ mock_bridge.connect = AsyncMock(side_effect=asyncio.TimeoutError)
create_tls.return_value = mock_bridge
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
- data=entry_mock_data,
+ data=EMPTY_MOCK_CONFIG_ENTRY,
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == STEP_IMPORT_FAILED
+ assert result["errors"] == {"base": ERROR_CANNOT_CONNECT}
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT
+
+
+async def test_bridge_invalid_ssl_error(hass):
+ """Test checking for connection and encountering invalid ssl certs."""
+
+ with patch.object(Smartbridge, "create_tls", side_effect=ssl.SSLError):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data=EMPTY_MOCK_CONFIG_ENTRY,
)
assert result["type"] == "form"
@@ -351,23 +374,25 @@ async def test_form_user_reuses_existing_assets_when_pairing_again(hass, tmpdir)
assert result["errors"] is None
assert result["step_id"] == "user"
- result2 = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {
- CONF_HOST: "1.1.1.1",
- },
- )
- await hass.async_block_till_done()
+ with patch.object(Smartbridge, "create_tls") as create_tls:
+ create_tls.return_value = MockBridge(can_connect=True)
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_HOST: "1.1.1.1",
+ },
+ )
+ await hass.async_block_till_done()
+
assert result2["type"] == "form"
assert result2["step_id"] == "link"
- with patch.object(Smartbridge, "create_tls") as create_tls, patch(
+ with patch(
"homeassistant.components.lutron_caseta.async_setup", return_value=True
), patch(
"homeassistant.components.lutron_caseta.async_setup_entry",
return_value=True,
):
- create_tls.return_value = MockBridge(can_connect=True)
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{},
diff --git a/tests/components/lyric/__init__.py b/tests/components/lyric/__init__.py
new file mode 100644
index 00000000000000..794c6bf1ba095b
--- /dev/null
+++ b/tests/components/lyric/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Honeywell Lyric integration."""
diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py
new file mode 100644
index 00000000000000..bfdd45f0f8e83e
--- /dev/null
+++ b/tests/components/lyric/test_config_flow.py
@@ -0,0 +1,133 @@
+"""Test the Honeywell Lyric config flow."""
+import asyncio
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.components.http import CONF_BASE_URL, DOMAIN as DOMAIN_HTTP
+from homeassistant.components.lyric import config_flow
+from homeassistant.components.lyric.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN
+from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
+from homeassistant.helpers import config_entry_oauth2_flow
+
+CLIENT_ID = "1234"
+CLIENT_SECRET = "5678"
+
+
+@pytest.fixture()
+async def mock_impl(hass):
+ """Mock implementation."""
+ await setup.async_setup_component(hass, "http", {})
+
+ impl = config_entry_oauth2_flow.LocalOAuth2Implementation(
+ hass,
+ DOMAIN,
+ CLIENT_ID,
+ CLIENT_SECRET,
+ OAUTH2_AUTHORIZE,
+ OAUTH2_TOKEN,
+ )
+ config_flow.OAuth2FlowHandler.async_register_implementation(hass, impl)
+ return impl
+
+
+async def test_abort_if_no_configuration(hass):
+ """Check flow abort when no configuration."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "missing_configuration"
+
+
+async def test_full_flow(
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
+):
+ """Check full flow."""
+ assert await setup.async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ DOMAIN: {
+ CONF_CLIENT_ID: CLIENT_ID,
+ CONF_CLIENT_SECRET: CLIENT_SECRET,
+ },
+ DOMAIN_HTTP: {CONF_BASE_URL: "https://example.com"},
+ },
+ )
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
+ assert result["url"] == (
+ f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
+ "&redirect_uri=https://example.com/auth/external/callback"
+ f"&state={state}"
+ )
+
+ client = await aiohttp_client(hass.http.app)
+ resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
+ assert resp.status == 200
+ assert resp.headers["content-type"] == "text/html; charset=utf-8"
+
+ aioclient_mock.post(
+ OAUTH2_TOKEN,
+ json={
+ "refresh_token": "mock-refresh-token",
+ "access_token": "mock-access-token",
+ "type": "Bearer",
+ "expires_in": 60,
+ },
+ )
+
+ with patch("homeassistant.components.lyric.api.ConfigEntryLyricClient"), patch(
+ "homeassistant.components.lyric.async_setup_entry", return_value=True
+ ) as mock_setup:
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+
+ assert result["data"]["auth_implementation"] == DOMAIN
+
+ result["data"]["token"].pop("expires_at")
+ assert result["data"]["token"] == {
+ "refresh_token": "mock-refresh-token",
+ "access_token": "mock-access-token",
+ "type": "Bearer",
+ "expires_in": 60,
+ }
+
+ assert DOMAIN in hass.config.components
+ entry = hass.config_entries.async_entries(DOMAIN)[0]
+ assert entry.state == config_entries.ENTRY_STATE_LOADED
+
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+ assert len(mock_setup.mock_calls) == 1
+
+
+async def test_abort_if_authorization_timeout(
+ hass, mock_impl, current_request_with_host
+):
+ """Check Somfy authorization timeout."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ flow = config_flow.OAuth2FlowHandler()
+ flow.hass = hass
+
+ with patch.object(
+ mock_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError
+ ):
+ result = await flow.async_step_user()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "authorize_url_timeout"
diff --git a/tests/components/mailbox/test_init.py b/tests/components/mailbox/test_init.py
index a2b2583bdb5329..75ecc8d9db3cf1 100644
--- a/tests/components/mailbox/test_init.py
+++ b/tests/components/mailbox/test_init.py
@@ -23,7 +23,8 @@ async def test_get_platforms_from_mailbox(mock_http_client):
req = await mock_http_client.get(url)
assert req.status == 200
result = await req.json()
- assert len(result) == 1 and "DemoMailbox" == result[0].get("name", None)
+ assert len(result) == 1
+ assert result[0].get("name") == "DemoMailbox"
async def test_get_messages_from_mailbox(mock_http_client):
diff --git a/tests/components/manual/test_alarm_control_panel.py b/tests/components/manual/test_alarm_control_panel.py
index ee3d97e52d75da..f3cf3ccce3953c 100644
--- a/tests/components/manual/test_alarm_control_panel.py
+++ b/tests/components/manual/test_alarm_control_panel.py
@@ -51,11 +51,11 @@ async def test_arm_home_no_pending(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_home(hass, CODE)
- assert STATE_ALARM_ARMED_HOME == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME
async def test_arm_home_no_pending_when_code_not_req(hass):
@@ -78,11 +78,11 @@ async def test_arm_home_no_pending_when_code_not_req(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_home(hass, 0)
- assert STATE_ALARM_ARMED_HOME == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME
async def test_arm_home_with_pending(hass):
@@ -104,11 +104,11 @@ async def test_arm_home_with_pending(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_home(hass, CODE, entity_id)
- assert STATE_ALARM_ARMING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMING
state = hass.states.get(entity_id)
assert state.attributes["next_state"] == STATE_ALARM_ARMED_HOME
@@ -144,11 +144,11 @@ async def test_arm_home_with_invalid_code(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_home(hass, CODE + "2")
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_arm_away_no_pending(hass):
@@ -170,11 +170,11 @@ async def test_arm_away_no_pending(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE, entity_id)
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
async def test_arm_away_no_pending_when_code_not_req(hass):
@@ -197,11 +197,11 @@ async def test_arm_away_no_pending_when_code_not_req(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, 0, entity_id)
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
async def test_arm_home_with_template_code(hass):
@@ -223,12 +223,12 @@ async def test_arm_home_with_template_code(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_home(hass, "abc")
state = hass.states.get(entity_id)
- assert STATE_ALARM_ARMED_HOME == state.state
+ assert state.state == STATE_ALARM_ARMED_HOME
async def test_arm_away_with_pending(hass):
@@ -250,11 +250,11 @@ async def test_arm_away_with_pending(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE)
- assert STATE_ALARM_ARMING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMING
state = hass.states.get(entity_id)
assert state.attributes["next_state"] == STATE_ALARM_ARMED_AWAY
@@ -290,11 +290,11 @@ async def test_arm_away_with_invalid_code(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE + "2")
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_arm_night_no_pending(hass):
@@ -316,11 +316,11 @@ async def test_arm_night_no_pending(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_night(hass, CODE)
- assert STATE_ALARM_ARMED_NIGHT == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT
async def test_arm_night_no_pending_when_code_not_req(hass):
@@ -343,11 +343,11 @@ async def test_arm_night_no_pending_when_code_not_req(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_night(hass, 0)
- assert STATE_ALARM_ARMED_NIGHT == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT
async def test_arm_night_with_pending(hass):
@@ -369,11 +369,11 @@ async def test_arm_night_with_pending(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_night(hass, CODE, entity_id)
- assert STATE_ALARM_ARMING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMING
state = hass.states.get(entity_id)
assert state.attributes["next_state"] == STATE_ALARM_ARMED_NIGHT
@@ -392,7 +392,7 @@ async def test_arm_night_with_pending(hass):
# Do not go to the pending state when updating to the same state
await common.async_alarm_arm_night(hass, CODE, entity_id)
- assert STATE_ALARM_ARMED_NIGHT == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT
async def test_arm_night_with_invalid_code(hass):
@@ -414,11 +414,11 @@ async def test_arm_night_with_invalid_code(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_night(hass, CODE + "2")
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_trigger_no_pending(hass):
@@ -439,11 +439,11 @@ async def test_trigger_no_pending(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass, entity_id=entity_id)
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
future = dt_util.utcnow() + timedelta(seconds=60)
with patch(
@@ -453,7 +453,7 @@ async def test_trigger_no_pending(hass):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
async def test_trigger_with_delay(hass):
@@ -476,17 +476,17 @@ async def test_trigger_with_delay(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE)
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
state = hass.states.get(entity_id)
- assert STATE_ALARM_PENDING == state.state
- assert STATE_ALARM_TRIGGERED == state.attributes["next_state"]
+ assert state.state == STATE_ALARM_PENDING
+ assert state.attributes["next_state"] == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(
@@ -497,7 +497,7 @@ async def test_trigger_with_delay(hass):
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert STATE_ALARM_TRIGGERED == state.state
+ assert state.state == STATE_ALARM_TRIGGERED
async def test_trigger_zero_trigger_time(hass):
@@ -519,11 +519,11 @@ async def test_trigger_zero_trigger_time(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass)
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_trigger_zero_trigger_time_with_pending(hass):
@@ -545,11 +545,11 @@ async def test_trigger_zero_trigger_time_with_pending(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass)
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_trigger_with_pending(hass):
@@ -571,11 +571,11 @@ async def test_trigger_with_pending(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass)
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
state = hass.states.get(entity_id)
assert state.attributes["next_state"] == STATE_ALARM_TRIGGERED
@@ -624,17 +624,17 @@ async def test_trigger_with_unused_specific_delay(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE)
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
state = hass.states.get(entity_id)
- assert STATE_ALARM_PENDING == state.state
- assert STATE_ALARM_TRIGGERED == state.attributes["next_state"]
+ assert state.state == STATE_ALARM_PENDING
+ assert state.attributes["next_state"] == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -669,17 +669,17 @@ async def test_trigger_with_specific_delay(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE)
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
state = hass.states.get(entity_id)
- assert STATE_ALARM_PENDING == state.state
- assert STATE_ALARM_TRIGGERED == state.attributes["next_state"]
+ assert state.state == STATE_ALARM_PENDING
+ assert state.attributes["next_state"] == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(
@@ -713,11 +713,11 @@ async def test_trigger_with_pending_and_delay(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE)
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
@@ -770,11 +770,11 @@ async def test_trigger_with_pending_and_specific_delay(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE)
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
@@ -826,7 +826,7 @@ async def test_armed_home_with_specific_pending(hass):
await common.async_alarm_arm_home(hass)
- assert STATE_ALARM_ARMING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMING
future = dt_util.utcnow() + timedelta(seconds=2)
with patch(
@@ -836,7 +836,7 @@ async def test_armed_home_with_specific_pending(hass):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_HOME == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME
async def test_armed_away_with_specific_pending(hass):
@@ -859,7 +859,7 @@ async def test_armed_away_with_specific_pending(hass):
await common.async_alarm_arm_away(hass)
- assert STATE_ALARM_ARMING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMING
future = dt_util.utcnow() + timedelta(seconds=2)
with patch(
@@ -869,7 +869,7 @@ async def test_armed_away_with_specific_pending(hass):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
async def test_armed_night_with_specific_pending(hass):
@@ -892,7 +892,7 @@ async def test_armed_night_with_specific_pending(hass):
await common.async_alarm_arm_night(hass)
- assert STATE_ALARM_ARMING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMING
future = dt_util.utcnow() + timedelta(seconds=2)
with patch(
@@ -902,7 +902,7 @@ async def test_armed_night_with_specific_pending(hass):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_NIGHT == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT
async def test_trigger_with_specific_pending(hass):
@@ -927,7 +927,7 @@ async def test_trigger_with_specific_pending(hass):
await common.async_alarm_trigger(hass)
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
future = dt_util.utcnow() + timedelta(seconds=2)
with patch(
@@ -937,7 +937,7 @@ async def test_trigger_with_specific_pending(hass):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -947,7 +947,7 @@ async def test_trigger_with_specific_pending(hass):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_trigger_with_disarm_after_trigger(hass):
@@ -969,11 +969,11 @@ async def test_trigger_with_disarm_after_trigger(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass, entity_id=entity_id)
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -983,7 +983,7 @@ async def test_trigger_with_disarm_after_trigger(hass):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_trigger_with_zero_specific_trigger_time(hass):
@@ -1006,11 +1006,11 @@ async def test_trigger_with_zero_specific_trigger_time(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass, entity_id=entity_id)
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_trigger_with_unused_zero_specific_trigger_time(hass):
@@ -1033,11 +1033,11 @@ async def test_trigger_with_unused_zero_specific_trigger_time(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass, entity_id=entity_id)
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -1047,7 +1047,7 @@ async def test_trigger_with_unused_zero_specific_trigger_time(hass):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_trigger_with_specific_trigger_time(hass):
@@ -1069,11 +1069,11 @@ async def test_trigger_with_specific_trigger_time(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass, entity_id=entity_id)
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -1083,7 +1083,7 @@ async def test_trigger_with_specific_trigger_time(hass):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_trigger_with_no_disarm_after_trigger(hass):
@@ -1106,15 +1106,15 @@ async def test_trigger_with_no_disarm_after_trigger(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE, entity_id)
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -1124,7 +1124,7 @@ async def test_trigger_with_no_disarm_after_trigger(hass):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass):
@@ -1147,15 +1147,15 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE, entity_id)
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -1165,11 +1165,11 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -1179,7 +1179,7 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
async def test_disarm_while_pending_trigger(hass):
@@ -1200,15 +1200,15 @@ async def test_disarm_while_pending_trigger(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass)
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
await common.async_alarm_disarm(hass, entity_id=entity_id)
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -1218,7 +1218,7 @@ async def test_disarm_while_pending_trigger(hass):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_disarm_during_trigger_with_invalid_code(hass):
@@ -1240,15 +1240,15 @@ async def test_disarm_during_trigger_with_invalid_code(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass)
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
await common.async_alarm_disarm(hass, entity_id=entity_id)
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -1258,7 +1258,7 @@ async def test_disarm_during_trigger_with_invalid_code(hass):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
async def test_disarm_with_template_code(hass):
@@ -1280,22 +1280,22 @@ async def test_disarm_with_template_code(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_home(hass, "def")
state = hass.states.get(entity_id)
- assert STATE_ALARM_ARMED_HOME == state.state
+ assert state.state == STATE_ALARM_ARMED_HOME
await common.async_alarm_disarm(hass, "def")
state = hass.states.get(entity_id)
- assert STATE_ALARM_ARMED_HOME == state.state
+ assert state.state == STATE_ALARM_ARMED_HOME
await common.async_alarm_disarm(hass, "abc")
state = hass.states.get(entity_id)
- assert STATE_ALARM_DISARMED == state.state
+ assert state.state == STATE_ALARM_DISARMED
async def test_arm_custom_bypass_no_pending(hass):
@@ -1317,11 +1317,11 @@ async def test_arm_custom_bypass_no_pending(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_custom_bypass(hass, CODE)
- assert STATE_ALARM_ARMED_CUSTOM_BYPASS == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_CUSTOM_BYPASS
async def test_arm_custom_bypass_no_pending_when_code_not_req(hass):
@@ -1344,11 +1344,11 @@ async def test_arm_custom_bypass_no_pending_when_code_not_req(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_custom_bypass(hass, 0)
- assert STATE_ALARM_ARMED_CUSTOM_BYPASS == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_CUSTOM_BYPASS
async def test_arm_custom_bypass_with_pending(hass):
@@ -1370,11 +1370,11 @@ async def test_arm_custom_bypass_with_pending(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_custom_bypass(hass, CODE, entity_id)
- assert STATE_ALARM_ARMING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMING
state = hass.states.get(entity_id)
assert state.attributes["next_state"] == STATE_ALARM_ARMED_CUSTOM_BYPASS
@@ -1410,11 +1410,11 @@ async def test_arm_custom_bypass_with_invalid_code(hass):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_custom_bypass(hass, CODE + "2")
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_armed_custom_bypass_with_specific_pending(hass):
@@ -1437,7 +1437,7 @@ async def test_armed_custom_bypass_with_specific_pending(hass):
await common.async_alarm_arm_custom_bypass(hass)
- assert STATE_ALARM_ARMING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMING
future = dt_util.utcnow() + timedelta(seconds=2)
with patch(
@@ -1447,7 +1447,7 @@ async def test_armed_custom_bypass_with_specific_pending(hass):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_CUSTOM_BYPASS == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_CUSTOM_BYPASS
async def test_arm_away_after_disabled_disarmed(hass, legacy_patchable_time):
@@ -1472,21 +1472,21 @@ async def test_arm_away_after_disabled_disarmed(hass, legacy_patchable_time):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE)
state = hass.states.get(entity_id)
- assert STATE_ALARM_ARMING == state.state
- assert STATE_ALARM_DISARMED == state.attributes["previous_state"]
- assert STATE_ALARM_ARMED_AWAY == state.attributes["next_state"]
+ assert state.state == STATE_ALARM_ARMING
+ assert state.attributes["previous_state"] == STATE_ALARM_DISARMED
+ assert state.attributes["next_state"] == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
state = hass.states.get(entity_id)
- assert STATE_ALARM_ARMING == state.state
- assert STATE_ALARM_DISARMED == state.attributes["previous_state"]
- assert STATE_ALARM_ARMED_AWAY == state.attributes["next_state"]
+ assert state.state == STATE_ALARM_ARMING
+ assert state.attributes["previous_state"] == STATE_ALARM_DISARMED
+ assert state.attributes["next_state"] == STATE_ALARM_ARMED_AWAY
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(
@@ -1497,14 +1497,14 @@ async def test_arm_away_after_disabled_disarmed(hass, legacy_patchable_time):
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert STATE_ALARM_ARMED_AWAY == state.state
+ assert state.state == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
state = hass.states.get(entity_id)
- assert STATE_ALARM_PENDING == state.state
- assert STATE_ALARM_ARMED_AWAY == state.attributes["previous_state"]
- assert STATE_ALARM_TRIGGERED == state.attributes["next_state"]
+ assert state.state == STATE_ALARM_PENDING
+ assert state.attributes["previous_state"] == STATE_ALARM_ARMED_AWAY
+ assert state.attributes["next_state"] == STATE_ALARM_TRIGGERED
future += timedelta(seconds=1)
with patch(
@@ -1515,7 +1515,7 @@ async def test_arm_away_after_disabled_disarmed(hass, legacy_patchable_time):
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert STATE_ALARM_TRIGGERED == state.state
+ assert state.state == STATE_ALARM_TRIGGERED
async def test_restore_armed_state(hass):
diff --git a/tests/components/manual_mqtt/test_alarm_control_panel.py b/tests/components/manual_mqtt/test_alarm_control_panel.py
index 9a98af127ea828..05033bb3347422 100644
--- a/tests/components/manual_mqtt/test_alarm_control_panel.py
+++ b/tests/components/manual_mqtt/test_alarm_control_panel.py
@@ -76,12 +76,12 @@ async def test_arm_home_no_pending(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_home(hass, CODE)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_HOME == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME
async def test_arm_home_no_pending_when_code_not_req(hass, mqtt_mock):
@@ -106,12 +106,12 @@ async def test_arm_home_no_pending_when_code_not_req(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_home(hass, 0)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_HOME == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME
async def test_arm_home_with_pending(hass, mqtt_mock):
@@ -135,12 +135,12 @@ async def test_arm_home_with_pending(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_home(hass, CODE, entity_id)
await hass.async_block_till_done()
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
state = hass.states.get(entity_id)
assert state.attributes["post_pending_state"] == STATE_ALARM_ARMED_HOME
@@ -153,7 +153,7 @@ async def test_arm_home_with_pending(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_HOME == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME
async def test_arm_home_with_invalid_code(hass, mqtt_mock):
@@ -177,12 +177,12 @@ async def test_arm_home_with_invalid_code(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_home(hass, f"{CODE}2")
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_arm_away_no_pending(hass, mqtt_mock):
@@ -206,12 +206,12 @@ async def test_arm_away_no_pending(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE, entity_id)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
async def test_arm_away_no_pending_when_code_not_req(hass, mqtt_mock):
@@ -236,12 +236,12 @@ async def test_arm_away_no_pending_when_code_not_req(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, 0, entity_id)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
async def test_arm_home_with_template_code(hass, mqtt_mock):
@@ -265,13 +265,13 @@ async def test_arm_home_with_template_code(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_home(hass, "abc")
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert STATE_ALARM_ARMED_HOME == state.state
+ assert state.state == STATE_ALARM_ARMED_HOME
async def test_arm_away_with_pending(hass, mqtt_mock):
@@ -295,12 +295,12 @@ async def test_arm_away_with_pending(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE)
await hass.async_block_till_done()
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
state = hass.states.get(entity_id)
assert state.attributes["post_pending_state"] == STATE_ALARM_ARMED_AWAY
@@ -313,7 +313,7 @@ async def test_arm_away_with_pending(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
async def test_arm_away_with_invalid_code(hass, mqtt_mock):
@@ -337,12 +337,12 @@ async def test_arm_away_with_invalid_code(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, f"{CODE}2")
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_arm_night_no_pending(hass, mqtt_mock):
@@ -366,12 +366,12 @@ async def test_arm_night_no_pending(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_night(hass, CODE, entity_id)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_NIGHT == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT
async def test_arm_night_no_pending_when_code_not_req(hass, mqtt_mock):
@@ -396,12 +396,12 @@ async def test_arm_night_no_pending_when_code_not_req(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_night(hass, 0, entity_id)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_NIGHT == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT
async def test_arm_night_with_pending(hass, mqtt_mock):
@@ -425,12 +425,12 @@ async def test_arm_night_with_pending(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_night(hass, CODE)
await hass.async_block_till_done()
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
state = hass.states.get(entity_id)
assert state.attributes["post_pending_state"] == STATE_ALARM_ARMED_NIGHT
@@ -443,13 +443,13 @@ async def test_arm_night_with_pending(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_NIGHT == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT
# Do not go to the pending state when updating to the same state
await common.async_alarm_arm_night(hass, CODE, entity_id)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_NIGHT == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT
async def test_arm_night_with_invalid_code(hass, mqtt_mock):
@@ -473,12 +473,12 @@ async def test_arm_night_with_invalid_code(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_night(hass, f"{CODE}2")
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_trigger_no_pending(hass, mqtt_mock):
@@ -501,12 +501,12 @@ async def test_trigger_no_pending(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass, entity_id=entity_id)
await hass.async_block_till_done()
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
future = dt_util.utcnow() + timedelta(seconds=60)
with patch(
@@ -516,7 +516,7 @@ async def test_trigger_no_pending(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
async def test_trigger_with_delay(hass, mqtt_mock):
@@ -541,19 +541,19 @@ async def test_trigger_with_delay(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert STATE_ALARM_PENDING == state.state
- assert STATE_ALARM_TRIGGERED == state.attributes["post_pending_state"]
+ assert state.state == STATE_ALARM_PENDING
+ assert state.attributes["post_pending_state"] == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(
@@ -564,7 +564,7 @@ async def test_trigger_with_delay(hass, mqtt_mock):
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert STATE_ALARM_TRIGGERED == state.state
+ assert state.state == STATE_ALARM_TRIGGERED
async def test_trigger_zero_trigger_time(hass, mqtt_mock):
@@ -588,12 +588,12 @@ async def test_trigger_zero_trigger_time(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass)
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_trigger_zero_trigger_time_with_pending(hass, mqtt_mock):
@@ -617,12 +617,12 @@ async def test_trigger_zero_trigger_time_with_pending(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass)
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_trigger_with_pending(hass, mqtt_mock):
@@ -646,12 +646,12 @@ async def test_trigger_with_pending(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass)
await hass.async_block_till_done()
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
state = hass.states.get(entity_id)
assert state.attributes["post_pending_state"] == STATE_ALARM_TRIGGERED
@@ -664,7 +664,7 @@ async def test_trigger_with_pending(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -674,7 +674,7 @@ async def test_trigger_with_pending(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_trigger_with_disarm_after_trigger(hass, mqtt_mock):
@@ -698,12 +698,12 @@ async def test_trigger_with_disarm_after_trigger(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass, entity_id=entity_id)
await hass.async_block_till_done()
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -713,7 +713,7 @@ async def test_trigger_with_disarm_after_trigger(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_trigger_with_zero_specific_trigger_time(hass, mqtt_mock):
@@ -738,12 +738,12 @@ async def test_trigger_with_zero_specific_trigger_time(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass, entity_id=entity_id)
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_trigger_with_unused_zero_specific_trigger_time(hass, mqtt_mock):
@@ -768,12 +768,12 @@ async def test_trigger_with_unused_zero_specific_trigger_time(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass, entity_id=entity_id)
await hass.async_block_till_done()
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -783,7 +783,7 @@ async def test_trigger_with_unused_zero_specific_trigger_time(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_trigger_with_specific_trigger_time(hass, mqtt_mock):
@@ -807,12 +807,12 @@ async def test_trigger_with_specific_trigger_time(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass, entity_id=entity_id)
await hass.async_block_till_done()
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -822,7 +822,7 @@ async def test_trigger_with_specific_trigger_time(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass, mqtt_mock):
@@ -846,17 +846,17 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass, mqtt_mock
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE, entity_id)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
await hass.async_block_till_done()
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -866,12 +866,12 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass, mqtt_mock
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
await hass.async_block_till_done()
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -881,7 +881,7 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass, mqtt_mock
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
async def test_disarm_while_pending_trigger(hass, mqtt_mock):
@@ -904,17 +904,17 @@ async def test_disarm_while_pending_trigger(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass)
await hass.async_block_till_done()
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
await common.async_alarm_disarm(hass, entity_id=entity_id)
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -924,7 +924,7 @@ async def test_disarm_while_pending_trigger(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_disarm_during_trigger_with_invalid_code(hass, mqtt_mock):
@@ -948,17 +948,17 @@ async def test_disarm_during_trigger_with_invalid_code(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass)
await hass.async_block_till_done()
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
await common.async_alarm_disarm(hass, entity_id=entity_id)
await hass.async_block_till_done()
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -968,7 +968,7 @@ async def test_disarm_during_trigger_with_invalid_code(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
async def test_trigger_with_unused_specific_delay(hass, mqtt_mock):
@@ -994,19 +994,19 @@ async def test_trigger_with_unused_specific_delay(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert STATE_ALARM_PENDING == state.state
- assert STATE_ALARM_TRIGGERED == state.attributes["post_pending_state"]
+ assert state.state == STATE_ALARM_PENDING
+ assert state.attributes["post_pending_state"] == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -1043,19 +1043,19 @@ async def test_trigger_with_specific_delay(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert STATE_ALARM_PENDING == state.state
- assert STATE_ALARM_TRIGGERED == state.attributes["post_pending_state"]
+ assert state.state == STATE_ALARM_PENDING
+ assert state.attributes["post_pending_state"] == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(
@@ -1092,12 +1092,12 @@ async def test_trigger_with_pending_and_delay(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
await hass.async_block_till_done()
@@ -1154,12 +1154,12 @@ async def test_trigger_with_pending_and_specific_delay(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
await hass.async_block_till_done()
@@ -1215,7 +1215,7 @@ async def test_armed_home_with_specific_pending(hass, mqtt_mock):
await common.async_alarm_arm_home(hass)
await hass.async_block_till_done()
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
future = dt_util.utcnow() + timedelta(seconds=2)
with patch(
@@ -1225,7 +1225,7 @@ async def test_armed_home_with_specific_pending(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_HOME == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME
async def test_armed_away_with_specific_pending(hass, mqtt_mock):
@@ -1251,7 +1251,7 @@ async def test_armed_away_with_specific_pending(hass, mqtt_mock):
await common.async_alarm_arm_away(hass)
await hass.async_block_till_done()
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
future = dt_util.utcnow() + timedelta(seconds=2)
with patch(
@@ -1261,7 +1261,7 @@ async def test_armed_away_with_specific_pending(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
async def test_armed_night_with_specific_pending(hass, mqtt_mock):
@@ -1287,7 +1287,7 @@ async def test_armed_night_with_specific_pending(hass, mqtt_mock):
await common.async_alarm_arm_night(hass)
await hass.async_block_till_done()
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
future = dt_util.utcnow() + timedelta(seconds=2)
with patch(
@@ -1297,7 +1297,7 @@ async def test_armed_night_with_specific_pending(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_NIGHT == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT
async def test_trigger_with_specific_pending(hass, mqtt_mock):
@@ -1325,7 +1325,7 @@ async def test_trigger_with_specific_pending(hass, mqtt_mock):
await common.async_alarm_trigger(hass)
await hass.async_block_till_done()
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
future = dt_util.utcnow() + timedelta(seconds=2)
with patch(
@@ -1335,7 +1335,7 @@ async def test_trigger_with_specific_pending(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_TRIGGERED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(
@@ -1345,7 +1345,7 @@ async def test_trigger_with_specific_pending(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_arm_away_after_disabled_disarmed(hass, legacy_patchable_time, mqtt_mock):
@@ -1372,23 +1372,23 @@ async def test_arm_away_after_disabled_disarmed(hass, legacy_patchable_time, mqt
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_away(hass, CODE)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert STATE_ALARM_PENDING == state.state
- assert STATE_ALARM_DISARMED == state.attributes["pre_pending_state"]
- assert STATE_ALARM_ARMED_AWAY == state.attributes["post_pending_state"]
+ assert state.state == STATE_ALARM_PENDING
+ assert state.attributes["pre_pending_state"] == STATE_ALARM_DISARMED
+ assert state.attributes["post_pending_state"] == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert STATE_ALARM_PENDING == state.state
- assert STATE_ALARM_DISARMED == state.attributes["pre_pending_state"]
- assert STATE_ALARM_ARMED_AWAY == state.attributes["post_pending_state"]
+ assert state.state == STATE_ALARM_PENDING
+ assert state.attributes["pre_pending_state"] == STATE_ALARM_DISARMED
+ assert state.attributes["post_pending_state"] == STATE_ALARM_ARMED_AWAY
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(
@@ -1399,15 +1399,15 @@ async def test_arm_away_after_disabled_disarmed(hass, legacy_patchable_time, mqt
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert STATE_ALARM_ARMED_AWAY == state.state
+ assert state.state == STATE_ALARM_ARMED_AWAY
await common.async_alarm_trigger(hass, entity_id=entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert STATE_ALARM_PENDING == state.state
- assert STATE_ALARM_ARMED_AWAY == state.attributes["pre_pending_state"]
- assert STATE_ALARM_TRIGGERED == state.attributes["post_pending_state"]
+ assert state.state == STATE_ALARM_PENDING
+ assert state.attributes["pre_pending_state"] == STATE_ALARM_ARMED_AWAY
+ assert state.attributes["post_pending_state"] == STATE_ALARM_TRIGGERED
future += timedelta(seconds=1)
with patch(
@@ -1418,7 +1418,7 @@ async def test_arm_away_after_disabled_disarmed(hass, legacy_patchable_time, mqt
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert STATE_ALARM_TRIGGERED == state.state
+ assert state.state == STATE_ALARM_TRIGGERED
async def test_disarm_with_template_code(hass, mqtt_mock):
@@ -1442,25 +1442,25 @@ async def test_disarm_with_template_code(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_arm_home(hass, "def")
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert STATE_ALARM_ARMED_HOME == state.state
+ assert state.state == STATE_ALARM_ARMED_HOME
await common.async_alarm_disarm(hass, "def")
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert STATE_ALARM_ARMED_HOME == state.state
+ assert state.state == STATE_ALARM_ARMED_HOME
await common.async_alarm_disarm(hass, "abc")
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert STATE_ALARM_DISARMED == state.state
+ assert state.state == STATE_ALARM_DISARMED
async def test_arm_home_via_command_topic(hass, mqtt_mock):
@@ -1483,12 +1483,12 @@ async def test_arm_home_via_command_topic(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
# Fire the arm command via MQTT; ensure state changes to pending
async_fire_mqtt_message(hass, "alarm/command", "ARM_HOME")
await hass.async_block_till_done()
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
# Fast-forward a little bit
future = dt_util.utcnow() + timedelta(seconds=1)
@@ -1499,7 +1499,7 @@ async def test_arm_home_via_command_topic(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_HOME == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME
async def test_arm_away_via_command_topic(hass, mqtt_mock):
@@ -1522,12 +1522,12 @@ async def test_arm_away_via_command_topic(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
# Fire the arm command via MQTT; ensure state changes to pending
async_fire_mqtt_message(hass, "alarm/command", "ARM_AWAY")
await hass.async_block_till_done()
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
# Fast-forward a little bit
future = dt_util.utcnow() + timedelta(seconds=1)
@@ -1538,7 +1538,7 @@ async def test_arm_away_via_command_topic(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY
async def test_arm_night_via_command_topic(hass, mqtt_mock):
@@ -1561,12 +1561,12 @@ async def test_arm_night_via_command_topic(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
# Fire the arm command via MQTT; ensure state changes to pending
async_fire_mqtt_message(hass, "alarm/command", "ARM_NIGHT")
await hass.async_block_till_done()
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
# Fast-forward a little bit
future = dt_util.utcnow() + timedelta(seconds=1)
@@ -1577,7 +1577,7 @@ async def test_arm_night_via_command_topic(hass, mqtt_mock):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_NIGHT == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT
async def test_disarm_pending_via_command_topic(hass, mqtt_mock):
@@ -1600,18 +1600,18 @@ async def test_disarm_pending_via_command_topic(hass, mqtt_mock):
entity_id = "alarm_control_panel.test"
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
await common.async_alarm_trigger(hass)
await hass.async_block_till_done()
- assert STATE_ALARM_PENDING == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_PENDING
# Now that we're pending, receive a command to disarm
async_fire_mqtt_message(hass, "alarm/command", "DISARM")
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(entity_id).state
+ assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
async def test_state_changes_are_published_to_mqtt(hass, mqtt_mock):
diff --git a/tests/components/maxcube/conftest.py b/tests/components/maxcube/conftest.py
new file mode 100644
index 00000000000000..6b283cf87c025f
--- /dev/null
+++ b/tests/components/maxcube/conftest.py
@@ -0,0 +1,109 @@
+"""Tests for EQ3 Max! component."""
+from unittest.mock import create_autospec, patch
+
+from maxcube.device import MAX_DEVICE_MODE_AUTOMATIC, MAX_DEVICE_MODE_MANUAL
+from maxcube.room import MaxRoom
+from maxcube.thermostat import MaxThermostat
+from maxcube.wallthermostat import MaxWallThermostat
+from maxcube.windowshutter import MaxWindowShutter
+import pytest
+
+from homeassistant.components.maxcube import DOMAIN
+from homeassistant.setup import async_setup_component
+
+
+@pytest.fixture
+def room():
+ """Create a test MAX! room."""
+ r = MaxRoom()
+ r.id = 1
+ r.name = "TestRoom"
+ return r
+
+
+@pytest.fixture
+def thermostat():
+ """Create test MAX! thermostat."""
+ t = create_autospec(MaxThermostat)
+ t.name = "TestThermostat"
+ t.serial = "AABBCCDD01"
+ t.rf_address = "abc1"
+ t.room_id = 1
+ t.is_thermostat.return_value = True
+ t.is_wallthermostat.return_value = False
+ t.is_windowshutter.return_value = False
+ t.mode = MAX_DEVICE_MODE_AUTOMATIC
+ t.comfort_temperature = 19.0
+ t.eco_temperature = 14.0
+ t.target_temperature = 20.5
+ t.actual_temperature = 19.0
+ t.max_temperature = None
+ t.min_temperature = None
+ t.valve_position = 25 # 25%
+ return t
+
+
+@pytest.fixture
+def wallthermostat():
+ """Create test MAX! wall thermostat."""
+ t = create_autospec(MaxWallThermostat)
+ t.name = "TestWallThermostat"
+ t.serial = "AABBCCDD02"
+ t.rf_address = "abc2"
+ t.room_id = 1
+ t.is_thermostat.return_value = False
+ t.is_wallthermostat.return_value = True
+ t.is_windowshutter.return_value = False
+ t.mode = MAX_DEVICE_MODE_MANUAL
+ t.comfort_temperature = 19.0
+ t.eco_temperature = 14.0
+ t.target_temperature = 4.5
+ t.actual_temperature = 19.0
+ t.max_temperature = 29.0
+ t.min_temperature = 4.5
+ return t
+
+
+@pytest.fixture
+def windowshutter():
+ """Create test MAX! window shutter."""
+ shutter = create_autospec(MaxWindowShutter)
+ shutter.name = "TestShutter"
+ shutter.serial = "AABBCCDD03"
+ shutter.rf_address = "abc3"
+ shutter.room_id = 1
+ shutter.is_open = True
+ shutter.is_thermostat.return_value = False
+ shutter.is_wallthermostat.return_value = False
+ shutter.is_windowshutter.return_value = True
+ return shutter
+
+
+@pytest.fixture
+def hass_config():
+ """Return test HASS configuration."""
+ return {
+ DOMAIN: {
+ "gateways": [
+ {
+ "host": "1.2.3.4",
+ }
+ ]
+ }
+ }
+
+
+@pytest.fixture
+async def cube(hass, hass_config, room, thermostat, wallthermostat, windowshutter):
+ """Build and setup a cube mock with a single room and some devices."""
+ with patch("homeassistant.components.maxcube.MaxCube") as mock:
+ cube = mock.return_value
+ cube.rooms = [room]
+ cube.devices = [thermostat, wallthermostat, windowshutter]
+ cube.room_by_id.return_value = room
+ cube.devices_by_room.return_value = [thermostat, wallthermostat, windowshutter]
+ assert await async_setup_component(hass, DOMAIN, hass_config)
+ await hass.async_block_till_done()
+ gateway = hass_config[DOMAIN]["gateways"][0]
+ mock.assert_called_with(gateway["host"], gateway.get("port", 62910))
+ return cube
diff --git a/tests/components/maxcube/test_maxcube_binary_sensor.py b/tests/components/maxcube/test_maxcube_binary_sensor.py
new file mode 100644
index 00000000000000..db5228c5c9a4fc
--- /dev/null
+++ b/tests/components/maxcube/test_maxcube_binary_sensor.py
@@ -0,0 +1,34 @@
+"""Test EQ3 Max! Window Shutters."""
+from datetime import timedelta
+
+from maxcube.cube import MaxCube
+from maxcube.windowshutter import MaxWindowShutter
+
+from homeassistant.components.binary_sensor import DEVICE_CLASS_WINDOW
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ ATTR_FRIENDLY_NAME,
+ STATE_OFF,
+ STATE_ON,
+)
+from homeassistant.util import utcnow
+
+from tests.common import async_fire_time_changed
+
+ENTITY_ID = "binary_sensor.testroom_testshutter"
+
+
+async def test_window_shuttler(hass, cube: MaxCube, windowshutter: MaxWindowShutter):
+ """Test a successful setup with a shuttler device."""
+ state = hass.states.get(ENTITY_ID)
+ assert state is not None
+ assert state.state == STATE_ON
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "TestRoom TestShutter"
+ assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_WINDOW
+
+ windowshutter.is_open = False
+ async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
+ await hass.async_block_till_done()
+
+ state = hass.states.get(ENTITY_ID)
+ assert state.state == STATE_OFF
diff --git a/tests/components/maxcube/test_maxcube_climate.py b/tests/components/maxcube/test_maxcube_climate.py
new file mode 100644
index 00000000000000..b59e1372fde380
--- /dev/null
+++ b/tests/components/maxcube/test_maxcube_climate.py
@@ -0,0 +1,414 @@
+"""Test EQ3 Max! Thermostats."""
+from datetime import timedelta
+
+from maxcube.cube import MaxCube
+from maxcube.device import (
+ MAX_DEVICE_MODE_AUTOMATIC,
+ MAX_DEVICE_MODE_BOOST,
+ MAX_DEVICE_MODE_MANUAL,
+ MAX_DEVICE_MODE_VACATION,
+)
+from maxcube.thermostat import MaxThermostat
+from maxcube.wallthermostat import MaxWallThermostat
+import pytest
+
+from homeassistant.components.climate.const import (
+ ATTR_CURRENT_TEMPERATURE,
+ ATTR_HVAC_ACTION,
+ ATTR_HVAC_MODE,
+ ATTR_HVAC_MODES,
+ ATTR_MAX_TEMP,
+ ATTR_MIN_TEMP,
+ ATTR_PRESET_MODE,
+ ATTR_PRESET_MODES,
+ ATTR_TARGET_TEMP_HIGH,
+ ATTR_TARGET_TEMP_LOW,
+ CURRENT_HVAC_HEAT,
+ CURRENT_HVAC_IDLE,
+ CURRENT_HVAC_OFF,
+ DOMAIN as CLIMATE_DOMAIN,
+ HVAC_MODE_AUTO,
+ HVAC_MODE_DRY,
+ HVAC_MODE_HEAT,
+ HVAC_MODE_OFF,
+ PRESET_AWAY,
+ PRESET_BOOST,
+ PRESET_COMFORT,
+ PRESET_ECO,
+ PRESET_NONE,
+ SERVICE_SET_HVAC_MODE,
+ SERVICE_SET_PRESET_MODE,
+ SERVICE_SET_TEMPERATURE,
+)
+from homeassistant.components.maxcube.climate import (
+ MAX_TEMPERATURE,
+ MIN_TEMPERATURE,
+ OFF_TEMPERATURE,
+ ON_TEMPERATURE,
+ PRESET_ON,
+ SUPPORT_FLAGS,
+)
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_FRIENDLY_NAME,
+ ATTR_SUPPORTED_FEATURES,
+ ATTR_TEMPERATURE,
+)
+from homeassistant.util import utcnow
+
+from tests.common import async_fire_time_changed
+
+ENTITY_ID = "climate.testroom_testthermostat"
+WALL_ENTITY_ID = "climate.testroom_testwallthermostat"
+VALVE_POSITION = "valve_position"
+
+
+async def test_setup_thermostat(hass, cube: MaxCube):
+ """Test a successful setup of a thermostat device."""
+
+ state = hass.states.get(ENTITY_ID)
+ assert state.state == HVAC_MODE_AUTO
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "TestRoom TestThermostat"
+ assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_HEAT
+ assert state.attributes.get(ATTR_HVAC_MODES) == [
+ HVAC_MODE_OFF,
+ HVAC_MODE_AUTO,
+ HVAC_MODE_HEAT,
+ ]
+ assert state.attributes.get(ATTR_PRESET_MODES) == [
+ PRESET_NONE,
+ PRESET_BOOST,
+ PRESET_COMFORT,
+ PRESET_ECO,
+ PRESET_AWAY,
+ PRESET_ON,
+ ]
+ assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_NONE
+ assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == SUPPORT_FLAGS
+ assert state.attributes.get(ATTR_MAX_TEMP) == MAX_TEMPERATURE
+ assert state.attributes.get(ATTR_MIN_TEMP) == 5.0
+ assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 19.0
+ assert state.attributes.get(ATTR_TEMPERATURE) == 20.5
+ assert state.attributes.get(VALVE_POSITION) == 25
+
+
+async def test_setup_wallthermostat(hass, cube: MaxCube):
+ """Test a successful setup of a wall thermostat device."""
+ state = hass.states.get(WALL_ENTITY_ID)
+ assert state.state == HVAC_MODE_OFF
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "TestRoom TestWallThermostat"
+ assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_HEAT
+ assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_NONE
+ assert state.attributes.get(ATTR_MAX_TEMP) == 29.0
+ assert state.attributes.get(ATTR_MIN_TEMP) == 5.0
+ assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 19.0
+ assert state.attributes.get(ATTR_TEMPERATURE) is None
+
+
+async def test_thermostat_set_hvac_mode_off(
+ hass, cube: MaxCube, thermostat: MaxThermostat
+):
+ """Turn off thermostat."""
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_OFF},
+ blocking=True,
+ )
+ cube.set_temperature_mode.assert_called_once_with(
+ thermostat, OFF_TEMPERATURE, MAX_DEVICE_MODE_MANUAL
+ )
+
+ thermostat.mode = MAX_DEVICE_MODE_MANUAL
+ thermostat.target_temperature = OFF_TEMPERATURE
+ thermostat.valve_position = 0
+
+ async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
+ await hass.async_block_till_done()
+
+ state = hass.states.get(ENTITY_ID)
+ assert state.state == HVAC_MODE_OFF
+ assert state.attributes.get(ATTR_TEMPERATURE) is None
+ assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_OFF
+ assert state.attributes.get(VALVE_POSITION) == 0
+
+ wall_state = hass.states.get(WALL_ENTITY_ID)
+ assert wall_state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_OFF
+
+
+async def test_thermostat_set_hvac_mode_heat(
+ hass, cube: MaxCube, thermostat: MaxThermostat
+):
+ """Set hvac mode to heat."""
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_HEAT},
+ blocking=True,
+ )
+ cube.set_temperature_mode.assert_called_once_with(
+ thermostat, 20.5, MAX_DEVICE_MODE_MANUAL
+ )
+ thermostat.mode = MAX_DEVICE_MODE_MANUAL
+
+ async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+ state = hass.states.get(ENTITY_ID)
+ assert state.state == HVAC_MODE_HEAT
+
+
+async def test_thermostat_set_invalid_hvac_mode(
+ hass, cube: MaxCube, thermostat: MaxThermostat
+):
+ """Set hvac mode to heat."""
+ with pytest.raises(ValueError):
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_DRY},
+ blocking=True,
+ )
+ cube.set_temperature_mode.assert_not_called()
+
+
+async def test_thermostat_set_temperature(
+ hass, cube: MaxCube, thermostat: MaxThermostat
+):
+ """Set hvac mode to heat."""
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 10.0},
+ blocking=True,
+ )
+ cube.set_temperature_mode.assert_called_once_with(thermostat, 10.0, None)
+ thermostat.target_temperature = 10.0
+ thermostat.valve_position = 0
+
+ async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
+ await hass.async_block_till_done()
+
+ state = hass.states.get(ENTITY_ID)
+ assert state.state == HVAC_MODE_AUTO
+ assert state.attributes.get(ATTR_TEMPERATURE) == 10.0
+ assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_IDLE
+
+
+async def test_thermostat_set_no_temperature(
+ hass, cube: MaxCube, thermostat: MaxThermostat
+):
+ """Set hvac mode to heat."""
+ with pytest.raises(ValueError):
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {
+ ATTR_ENTITY_ID: ENTITY_ID,
+ ATTR_TARGET_TEMP_HIGH: 29.0,
+ ATTR_TARGET_TEMP_LOW: 10.0,
+ },
+ blocking=True,
+ )
+ cube.set_temperature_mode.assert_not_called()
+
+
+async def test_thermostat_set_preset_on(hass, cube: MaxCube, thermostat: MaxThermostat):
+ """Set preset mode to on."""
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_ON},
+ blocking=True,
+ )
+
+ cube.set_temperature_mode.assert_called_once_with(
+ thermostat, ON_TEMPERATURE, MAX_DEVICE_MODE_MANUAL
+ )
+ thermostat.mode = MAX_DEVICE_MODE_MANUAL
+ thermostat.target_temperature = ON_TEMPERATURE
+
+ async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
+ await hass.async_block_till_done()
+
+ state = hass.states.get(ENTITY_ID)
+ assert state.state == HVAC_MODE_HEAT
+ assert state.attributes.get(ATTR_TEMPERATURE) is None
+ assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_ON
+
+
+async def test_thermostat_set_preset_comfort(
+ hass, cube: MaxCube, thermostat: MaxThermostat
+):
+ """Set preset mode to comfort."""
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_COMFORT},
+ blocking=True,
+ )
+ cube.set_temperature_mode.assert_called_once_with(
+ thermostat, thermostat.comfort_temperature, MAX_DEVICE_MODE_MANUAL
+ )
+ thermostat.mode = MAX_DEVICE_MODE_MANUAL
+ thermostat.target_temperature = thermostat.comfort_temperature
+
+ async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
+ await hass.async_block_till_done()
+
+ state = hass.states.get(ENTITY_ID)
+ assert state.state == HVAC_MODE_HEAT
+ assert state.attributes.get(ATTR_TEMPERATURE) == thermostat.comfort_temperature
+ assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_COMFORT
+
+
+async def test_thermostat_set_preset_eco(
+ hass, cube: MaxCube, thermostat: MaxThermostat
+):
+ """Set preset mode to eco."""
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_ECO},
+ blocking=True,
+ )
+ cube.set_temperature_mode.assert_called_once_with(
+ thermostat, thermostat.eco_temperature, MAX_DEVICE_MODE_MANUAL
+ )
+ thermostat.mode = MAX_DEVICE_MODE_MANUAL
+ thermostat.target_temperature = thermostat.eco_temperature
+
+ async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
+ await hass.async_block_till_done()
+
+ state = hass.states.get(ENTITY_ID)
+ assert state.state == HVAC_MODE_HEAT
+ assert state.attributes.get(ATTR_TEMPERATURE) == thermostat.eco_temperature
+ assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_ECO
+
+
+async def test_thermostat_set_preset_away(
+ hass, cube: MaxCube, thermostat: MaxThermostat
+):
+ """Set preset mode to away."""
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_AWAY},
+ blocking=True,
+ )
+ cube.set_temperature_mode.assert_called_once_with(
+ thermostat, None, MAX_DEVICE_MODE_VACATION
+ )
+ thermostat.mode = MAX_DEVICE_MODE_VACATION
+ thermostat.target_temperature = thermostat.eco_temperature
+
+ async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
+ await hass.async_block_till_done()
+
+ state = hass.states.get(ENTITY_ID)
+ assert state.state == HVAC_MODE_HEAT
+ assert state.attributes.get(ATTR_TEMPERATURE) == thermostat.eco_temperature
+ assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_AWAY
+
+
+async def test_thermostat_set_preset_boost(
+ hass, cube: MaxCube, thermostat: MaxThermostat
+):
+ """Set preset mode to boost."""
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_BOOST},
+ blocking=True,
+ )
+ cube.set_temperature_mode.assert_called_once_with(
+ thermostat, None, MAX_DEVICE_MODE_BOOST
+ )
+ thermostat.mode = MAX_DEVICE_MODE_BOOST
+ thermostat.target_temperature = thermostat.eco_temperature
+
+ async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
+ await hass.async_block_till_done()
+
+ state = hass.states.get(ENTITY_ID)
+ assert state.state == HVAC_MODE_AUTO
+ assert state.attributes.get(ATTR_TEMPERATURE) == thermostat.eco_temperature
+ assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_BOOST
+
+
+async def test_thermostat_set_preset_none(
+ hass, cube: MaxCube, thermostat: MaxThermostat
+):
+ """Set preset mode to boost."""
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_NONE},
+ blocking=True,
+ )
+ cube.set_temperature_mode.assert_called_once_with(
+ thermostat, None, MAX_DEVICE_MODE_AUTOMATIC
+ )
+
+
+async def test_thermostat_set_invalid_preset(
+ hass, cube: MaxCube, thermostat: MaxThermostat
+):
+ """Set hvac mode to heat."""
+ with pytest.raises(ValueError):
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: "invalid"},
+ blocking=True,
+ )
+ cube.set_temperature_mode.assert_not_called()
+
+
+async def test_wallthermostat_set_hvac_mode_heat(
+ hass, cube: MaxCube, wallthermostat: MaxWallThermostat
+):
+ """Set wall thermostat hvac mode to heat."""
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: WALL_ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_HEAT},
+ blocking=True,
+ )
+ cube.set_temperature_mode.assert_called_once_with(
+ wallthermostat, MIN_TEMPERATURE, MAX_DEVICE_MODE_MANUAL
+ )
+ wallthermostat.target_temperature = MIN_TEMPERATURE
+
+ async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
+ await hass.async_block_till_done()
+
+ state = hass.states.get(WALL_ENTITY_ID)
+ assert state.state == HVAC_MODE_HEAT
+ assert state.attributes.get(ATTR_TEMPERATURE) == MIN_TEMPERATURE
+
+
+async def test_wallthermostat_set_hvac_mode_auto(
+ hass, cube: MaxCube, wallthermostat: MaxWallThermostat
+):
+ """Set wall thermostat hvac mode to auto."""
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: WALL_ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_AUTO},
+ blocking=True,
+ )
+ cube.set_temperature_mode.assert_called_once_with(
+ wallthermostat, None, MAX_DEVICE_MODE_AUTOMATIC
+ )
+ wallthermostat.mode = MAX_DEVICE_MODE_AUTOMATIC
+ wallthermostat.target_temperature = 23.0
+
+ async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
+ await hass.async_block_till_done()
+
+ state = hass.states.get(WALL_ENTITY_ID)
+ assert state.state == HVAC_MODE_AUTO
+ assert state.attributes.get(ATTR_TEMPERATURE) == 23.0
diff --git a/tests/components/mazda/__init__.py b/tests/components/mazda/__init__.py
new file mode 100644
index 00000000000000..f7a267a511033c
--- /dev/null
+++ b/tests/components/mazda/__init__.py
@@ -0,0 +1,53 @@
+"""Tests for the Mazda Connected Services integration."""
+
+import json
+from unittest.mock import AsyncMock, MagicMock, patch
+
+from pymazda import Client as MazdaAPI
+
+from homeassistant.components.mazda.const import DOMAIN
+from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import aiohttp_client
+
+from tests.common import MockConfigEntry, load_fixture
+
+FIXTURE_USER_INPUT = {
+ CONF_EMAIL: "example@example.com",
+ CONF_PASSWORD: "password",
+ CONF_REGION: "MNAO",
+}
+
+
+async def init_integration(hass: HomeAssistant, use_nickname=True) -> MockConfigEntry:
+ """Set up the Mazda Connected Services integration in Home Assistant."""
+ get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json"))
+ if not use_nickname:
+ get_vehicles_fixture[0].pop("nickname")
+
+ get_vehicle_status_fixture = json.loads(
+ load_fixture("mazda/get_vehicle_status.json")
+ )
+
+ config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT)
+ config_entry.add_to_hass(hass)
+
+ client_mock = MagicMock(
+ MazdaAPI(
+ FIXTURE_USER_INPUT[CONF_EMAIL],
+ FIXTURE_USER_INPUT[CONF_PASSWORD],
+ FIXTURE_USER_INPUT[CONF_REGION],
+ aiohttp_client.async_get_clientsession(hass),
+ )
+ )
+ client_mock.get_vehicles = AsyncMock(return_value=get_vehicles_fixture)
+ client_mock.get_vehicle_status = AsyncMock(return_value=get_vehicle_status_fixture)
+
+ with patch(
+ "homeassistant.components.mazda.config_flow.MazdaAPI",
+ return_value=client_mock,
+ ), patch("homeassistant.components.mazda.MazdaAPI", return_value=client_mock):
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ return config_entry
diff --git a/tests/components/mazda/test_config_flow.py b/tests/components/mazda/test_config_flow.py
new file mode 100644
index 00000000000000..fbdd74bfdfae24
--- /dev/null
+++ b/tests/components/mazda/test_config_flow.py
@@ -0,0 +1,310 @@
+"""Test the Mazda Connected Services config flow."""
+from unittest.mock import patch
+
+import aiohttp
+
+from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.components.mazda.config_flow import (
+ MazdaAccountLockedException,
+ MazdaAuthenticationException,
+)
+from homeassistant.components.mazda.const import DOMAIN
+from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
+from homeassistant.core import HomeAssistant
+
+from tests.common import MockConfigEntry
+
+FIXTURE_USER_INPUT = {
+ CONF_EMAIL: "example@example.com",
+ CONF_PASSWORD: "password",
+ CONF_REGION: "MNAO",
+}
+FIXTURE_USER_INPUT_REAUTH = {
+ CONF_EMAIL: "example@example.com",
+ CONF_PASSWORD: "password_fixed",
+ CONF_REGION: "MNAO",
+}
+
+
+async def test_form(hass):
+ """Test the entire flow."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.mazda.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.mazda.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ FIXTURE_USER_INPUT,
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == FIXTURE_USER_INPUT[CONF_EMAIL]
+ assert result2["data"] == FIXTURE_USER_INPUT
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_invalid_auth(hass: HomeAssistant) -> None:
+ """Test we handle invalid auth."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
+ side_effect=MazdaAuthenticationException("Failed to authenticate"),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ FIXTURE_USER_INPUT,
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == "user"
+ assert result2["errors"] == {"base": "invalid_auth"}
+
+
+async def test_form_account_locked(hass: HomeAssistant) -> None:
+ """Test we handle account locked error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
+ side_effect=MazdaAccountLockedException("Account locked"),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ FIXTURE_USER_INPUT,
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == "user"
+ assert result2["errors"] == {"base": "account_locked"}
+
+
+async def test_form_cannot_connect(hass):
+ """Test we handle cannot connect error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
+ side_effect=aiohttp.ClientError,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ FIXTURE_USER_INPUT,
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "cannot_connect"}
+
+
+async def test_form_unknown_error(hass):
+ """Test we handle unknown error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
+ side_effect=Exception,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ FIXTURE_USER_INPUT,
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "unknown"}
+
+
+async def test_reauth_flow(hass: HomeAssistant) -> None:
+ """Test reauth works."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ with patch(
+ "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
+ side_effect=MazdaAuthenticationException("Failed to authenticate"),
+ ):
+ mock_config = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id=FIXTURE_USER_INPUT[CONF_EMAIL],
+ data=FIXTURE_USER_INPUT,
+ )
+ mock_config.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_config.entry_id)
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth"
+ assert result["errors"] == {"base": "invalid_auth"}
+
+ with patch(
+ "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
+ return_value=True,
+ ):
+ result2 = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "reauth", "unique_id": FIXTURE_USER_INPUT[CONF_EMAIL]},
+ data=FIXTURE_USER_INPUT_REAUTH,
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result2["reason"] == "reauth_successful"
+
+
+async def test_reauth_authorization_error(hass: HomeAssistant) -> None:
+ """Test we show user form on authorization error."""
+ with patch(
+ "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
+ side_effect=MazdaAuthenticationException("Failed to authenticate"),
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth"
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ FIXTURE_USER_INPUT_REAUTH,
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == "reauth"
+ assert result2["errors"] == {"base": "invalid_auth"}
+
+
+async def test_reauth_account_locked(hass: HomeAssistant) -> None:
+ """Test we show user form on account_locked error."""
+ with patch(
+ "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
+ side_effect=MazdaAccountLockedException("Account locked"),
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth"
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ FIXTURE_USER_INPUT_REAUTH,
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == "reauth"
+ assert result2["errors"] == {"base": "account_locked"}
+
+
+async def test_reauth_connection_error(hass: HomeAssistant) -> None:
+ """Test we show user form on connection error."""
+ with patch(
+ "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
+ side_effect=aiohttp.ClientError,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth"
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ FIXTURE_USER_INPUT_REAUTH,
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == "reauth"
+ assert result2["errors"] == {"base": "cannot_connect"}
+
+
+async def test_reauth_unknown_error(hass: HomeAssistant) -> None:
+ """Test we show user form on unknown error."""
+ with patch(
+ "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
+ side_effect=Exception,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth"
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ FIXTURE_USER_INPUT_REAUTH,
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == "reauth"
+ assert result2["errors"] == {"base": "unknown"}
+
+
+async def test_reauth_unique_id_not_found(hass: HomeAssistant) -> None:
+ """Test we show user form when unique id not found during reauth."""
+ with patch(
+ "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials",
+ return_value=True,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth"
+
+ # Change the unique_id of the flow in order to cause a mismatch
+ flows = hass.config_entries.flow.async_progress()
+ flows[0]["context"]["unique_id"] = "example2@example.com"
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ FIXTURE_USER_INPUT_REAUTH,
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == "reauth"
+ assert result2["errors"] == {"base": "unknown"}
diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py
new file mode 100644
index 00000000000000..ebd118260bc991
--- /dev/null
+++ b/tests/components/mazda/test_init.py
@@ -0,0 +1,111 @@
+"""Tests for the Mazda Connected Services integration."""
+from datetime import timedelta
+import json
+from unittest.mock import patch
+
+from pymazda import MazdaAuthenticationException, MazdaException
+
+from homeassistant.components.mazda.const import DOMAIN
+from homeassistant.config_entries import (
+ ENTRY_STATE_LOADED,
+ ENTRY_STATE_NOT_LOADED,
+ ENTRY_STATE_SETUP_ERROR,
+ ENTRY_STATE_SETUP_RETRY,
+)
+from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
+from homeassistant.core import HomeAssistant
+from homeassistant.util import dt as dt_util
+
+from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture
+from tests.components.mazda import init_integration
+
+FIXTURE_USER_INPUT = {
+ CONF_EMAIL: "example@example.com",
+ CONF_PASSWORD: "password",
+ CONF_REGION: "MNAO",
+}
+
+
+async def test_config_entry_not_ready(hass: HomeAssistant) -> None:
+ """Test the Mazda configuration entry not ready."""
+ config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT)
+ config_entry.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.mazda.MazdaAPI.validate_credentials",
+ side_effect=MazdaException("Unknown error"),
+ ):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert config_entry.state == ENTRY_STATE_SETUP_RETRY
+
+
+async def test_init_auth_failure(hass: HomeAssistant):
+ """Test auth failure during setup."""
+ with patch(
+ "homeassistant.components.mazda.MazdaAPI.validate_credentials",
+ side_effect=MazdaAuthenticationException("Login failed"),
+ ):
+ config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT)
+ config_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ entries = hass.config_entries.async_entries(DOMAIN)
+ assert len(entries) == 1
+ assert entries[0].state == ENTRY_STATE_SETUP_ERROR
+
+ flows = hass.config_entries.flow.async_progress()
+ assert len(flows) == 1
+ assert flows[0]["step_id"] == "reauth"
+
+
+async def test_update_auth_failure(hass: HomeAssistant):
+ """Test auth failure during data update."""
+ get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json"))
+ get_vehicle_status_fixture = json.loads(
+ load_fixture("mazda/get_vehicle_status.json")
+ )
+
+ with patch(
+ "homeassistant.components.mazda.MazdaAPI.validate_credentials",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.mazda.MazdaAPI.get_vehicles",
+ return_value=get_vehicles_fixture,
+ ), patch(
+ "homeassistant.components.mazda.MazdaAPI.get_vehicle_status",
+ return_value=get_vehicle_status_fixture,
+ ):
+ config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT)
+ config_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ entries = hass.config_entries.async_entries(DOMAIN)
+ assert len(entries) == 1
+ assert entries[0].state == ENTRY_STATE_LOADED
+
+ with patch(
+ "homeassistant.components.mazda.MazdaAPI.get_vehicles",
+ side_effect=MazdaAuthenticationException("Login failed"),
+ ):
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=61))
+ await hass.async_block_till_done()
+
+ flows = hass.config_entries.flow.async_progress()
+ assert len(flows) == 1
+ assert flows[0]["step_id"] == "reauth"
+
+
+async def test_unload_config_entry(hass: HomeAssistant) -> None:
+ """Test the Mazda configuration entry unloading."""
+ entry = await init_integration(hass)
+ assert hass.data[DOMAIN]
+
+ await hass.config_entries.async_unload(entry.entry_id)
+ await hass.async_block_till_done()
+ assert entry.state == ENTRY_STATE_NOT_LOADED
diff --git a/tests/components/mazda/test_sensor.py b/tests/components/mazda/test_sensor.py
new file mode 100644
index 00000000000000..d5f25bce2f31b8
--- /dev/null
+++ b/tests/components/mazda/test_sensor.py
@@ -0,0 +1,161 @@
+"""The sensor tests for the Mazda Connected Services integration."""
+
+from homeassistant.components.mazda.const import DOMAIN
+from homeassistant.const import (
+ ATTR_FRIENDLY_NAME,
+ ATTR_ICON,
+ ATTR_UNIT_OF_MEASUREMENT,
+ LENGTH_KILOMETERS,
+ LENGTH_MILES,
+ PERCENTAGE,
+ PRESSURE_PSI,
+)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+
+from tests.components.mazda import init_integration
+
+
+async def test_device_nickname(hass):
+ """Test creation of the device when vehicle has a nickname."""
+ await init_integration(hass, use_nickname=True)
+
+ device_registry = dr.async_get(hass)
+ reg_device = device_registry.async_get_device(
+ identifiers={(DOMAIN, "JM000000000000000")},
+ )
+
+ assert reg_device.model == "2021 MAZDA3 2.5 S SE AWD"
+ assert reg_device.manufacturer == "Mazda"
+ assert reg_device.name == "My Mazda3"
+
+
+async def test_device_no_nickname(hass):
+ """Test creation of the device when vehicle has no nickname."""
+ await init_integration(hass, use_nickname=False)
+
+ device_registry = dr.async_get(hass)
+ reg_device = device_registry.async_get_device(
+ identifiers={(DOMAIN, "JM000000000000000")},
+ )
+
+ assert reg_device.model == "2021 MAZDA3 2.5 S SE AWD"
+ assert reg_device.manufacturer == "Mazda"
+ assert reg_device.name == "2021 MAZDA3 2.5 S SE AWD"
+
+
+async def test_sensors(hass):
+ """Test creation of the sensors."""
+ await init_integration(hass)
+
+ entity_registry = er.async_get(hass)
+
+ # Fuel Remaining Percentage
+ state = hass.states.get("sensor.my_mazda3_fuel_remaining_percentage")
+ assert state
+ assert (
+ state.attributes.get(ATTR_FRIENDLY_NAME)
+ == "My Mazda3 Fuel Remaining Percentage"
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:gas-station"
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
+ assert state.state == "87.0"
+ entry = entity_registry.async_get("sensor.my_mazda3_fuel_remaining_percentage")
+ assert entry
+ assert entry.unique_id == "JM000000000000000_fuel_remaining_percentage"
+
+ # Fuel Distance Remaining
+ state = hass.states.get("sensor.my_mazda3_fuel_distance_remaining")
+ assert state
+ assert (
+ state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Fuel Distance Remaining"
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:gas-station"
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS
+ assert state.state == "381"
+ entry = entity_registry.async_get("sensor.my_mazda3_fuel_distance_remaining")
+ assert entry
+ assert entry.unique_id == "JM000000000000000_fuel_distance_remaining"
+
+ # Odometer
+ state = hass.states.get("sensor.my_mazda3_odometer")
+ assert state
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Odometer"
+ assert state.attributes.get(ATTR_ICON) == "mdi:speedometer"
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS
+ assert state.state == "2796"
+ entry = entity_registry.async_get("sensor.my_mazda3_odometer")
+ assert entry
+ assert entry.unique_id == "JM000000000000000_odometer"
+
+ # Front Left Tire Pressure
+ state = hass.states.get("sensor.my_mazda3_front_left_tire_pressure")
+ assert state
+ assert (
+ state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Front Left Tire Pressure"
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert"
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI
+ assert state.state == "35"
+ entry = entity_registry.async_get("sensor.my_mazda3_front_left_tire_pressure")
+ assert entry
+ assert entry.unique_id == "JM000000000000000_front_left_tire_pressure"
+
+ # Front Right Tire Pressure
+ state = hass.states.get("sensor.my_mazda3_front_right_tire_pressure")
+ assert state
+ assert (
+ state.attributes.get(ATTR_FRIENDLY_NAME)
+ == "My Mazda3 Front Right Tire Pressure"
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert"
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI
+ assert state.state == "35"
+ entry = entity_registry.async_get("sensor.my_mazda3_front_right_tire_pressure")
+ assert entry
+ assert entry.unique_id == "JM000000000000000_front_right_tire_pressure"
+
+ # Rear Left Tire Pressure
+ state = hass.states.get("sensor.my_mazda3_rear_left_tire_pressure")
+ assert state
+ assert (
+ state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Left Tire Pressure"
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert"
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI
+ assert state.state == "33"
+ entry = entity_registry.async_get("sensor.my_mazda3_rear_left_tire_pressure")
+ assert entry
+ assert entry.unique_id == "JM000000000000000_rear_left_tire_pressure"
+
+ # Rear Right Tire Pressure
+ state = hass.states.get("sensor.my_mazda3_rear_right_tire_pressure")
+ assert state
+ assert (
+ state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Right Tire Pressure"
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert"
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI
+ assert state.state == "33"
+ entry = entity_registry.async_get("sensor.my_mazda3_rear_right_tire_pressure")
+ assert entry
+ assert entry.unique_id == "JM000000000000000_rear_right_tire_pressure"
+
+
+async def test_sensors_imperial_units(hass):
+ """Test that the sensors work properly with imperial units."""
+ hass.config.units = IMPERIAL_SYSTEM
+
+ await init_integration(hass)
+
+ # Fuel Distance Remaining
+ state = hass.states.get("sensor.my_mazda3_fuel_distance_remaining")
+ assert state
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILES
+ assert state.state == "237"
+
+ # Odometer
+ state = hass.states.get("sensor.my_mazda3_odometer")
+ assert state
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILES
+ assert state.state == "1737"
diff --git a/tests/components/media_player/test_device_condition.py b/tests/components/media_player/test_device_condition.py
index c7668d748af6c6..63cdb1c55a7d2a 100644
--- a/tests/components/media_player/test_device_condition.py
+++ b/tests/components/media_player/test_device_condition.py
@@ -21,7 +21,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/media_player/test_device_trigger.py b/tests/components/media_player/test_device_trigger.py
new file mode 100644
index 00000000000000..2e3641242f9ab6
--- /dev/null
+++ b/tests/components/media_player/test_device_trigger.py
@@ -0,0 +1,229 @@
+"""The tests for Media player device triggers."""
+from datetime import timedelta
+
+import pytest
+
+import homeassistant.components.automation as automation
+from homeassistant.components.media_player import DOMAIN
+from homeassistant.const import (
+ STATE_IDLE,
+ STATE_OFF,
+ STATE_ON,
+ STATE_PAUSED,
+ STATE_PLAYING,
+)
+from homeassistant.helpers import device_registry
+from homeassistant.setup import async_setup_component
+import homeassistant.util.dt as dt_util
+
+from tests.common import (
+ MockConfigEntry,
+ assert_lists_same,
+ async_fire_time_changed,
+ async_get_device_automation_capabilities,
+ async_get_device_automations,
+ async_mock_service,
+ mock_device_registry,
+ mock_registry,
+)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
+
+
+@pytest.fixture
+def device_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_device_registry(hass)
+
+
+@pytest.fixture
+def entity_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_registry(hass)
+
+
+@pytest.fixture
+def calls(hass):
+ """Track calls to a mock service."""
+ return async_mock_service(hass, "test", "automation")
+
+
+async def test_get_triggers(hass, device_reg, entity_reg):
+ """Test we get the expected triggers from a media player."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+
+ trigger_types = {"turned_on", "turned_off", "idle", "paused", "playing"}
+ expected_triggers = [
+ {
+ "platform": "device",
+ "domain": DOMAIN,
+ "type": trigger,
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ }
+ for trigger in trigger_types
+ ]
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert_lists_same(triggers, expected_triggers)
+
+
+async def test_get_trigger_capabilities(hass, device_reg, entity_reg):
+ """Test we get the expected capabilities from a media player."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert len(triggers) == 5
+ for trigger in triggers:
+ capabilities = await async_get_device_automation_capabilities(
+ hass, "trigger", trigger
+ )
+ assert capabilities == {
+ "extra_fields": [
+ {"name": "for", "optional": True, "type": "positive_time_period_dict"}
+ ]
+ }
+
+
+async def test_if_fires_on_state_change(hass, calls):
+ """Test triggers firing."""
+ hass.states.async_set("media_player.entity", STATE_OFF)
+
+ data_template = (
+ "{label} - {{{{ trigger.platform}}}} - "
+ "{{{{ trigger.entity_id}}}} - {{{{ trigger.from_state.state}}}} - "
+ "{{{{ trigger.to_state.state}}}} - {{{{ trigger.for }}}}"
+ )
+ trigger_types = {"turned_on", "turned_off", "idle", "paused", "playing"}
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": "media_player.entity",
+ "type": trigger,
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {"some": data_template.format(label=trigger)},
+ },
+ }
+ for trigger in trigger_types
+ ]
+ },
+ )
+
+ # Fake that the entity is turning on.
+ hass.states.async_set("media_player.entity", STATE_ON)
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert (
+ calls[0].data["some"]
+ == "turned_on - device - media_player.entity - off - on - None"
+ )
+
+ # Fake that the entity is turning off.
+ hass.states.async_set("media_player.entity", STATE_OFF)
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert (
+ calls[1].data["some"]
+ == "turned_off - device - media_player.entity - on - off - None"
+ )
+
+ # Fake that the entity becomes idle.
+ hass.states.async_set("media_player.entity", STATE_IDLE)
+ await hass.async_block_till_done()
+ assert len(calls) == 3
+ assert (
+ calls[2].data["some"]
+ == "idle - device - media_player.entity - off - idle - None"
+ )
+
+ # Fake that the entity starts playing.
+ hass.states.async_set("media_player.entity", STATE_PLAYING)
+ await hass.async_block_till_done()
+ assert len(calls) == 4
+ assert (
+ calls[3].data["some"]
+ == "playing - device - media_player.entity - idle - playing - None"
+ )
+
+ # Fake that the entity is paused.
+ hass.states.async_set("media_player.entity", STATE_PAUSED)
+ await hass.async_block_till_done()
+ assert len(calls) == 5
+ assert (
+ calls[4].data["some"]
+ == "paused - device - media_player.entity - playing - paused - None"
+ )
+
+
+async def test_if_fires_on_state_change_with_for(hass, calls):
+ """Test for triggers firing with delay."""
+ entity_id = f"{DOMAIN}.entity"
+ hass.states.async_set(entity_id, STATE_OFF)
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": entity_id,
+ "type": "turned_on",
+ "for": {"seconds": 5},
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "turn_off {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(
+ (
+ "platform",
+ "entity_id",
+ "from_state.state",
+ "to_state.state",
+ "for",
+ )
+ )
+ },
+ },
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(entity_id).state == STATE_OFF
+ assert len(calls) == 0
+
+ hass.states.async_set(entity_id, STATE_ON)
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ await hass.async_block_till_done()
+ assert (
+ calls[0].data["some"] == f"turn_off device - {entity_id} - off - on - 0:00:05"
+ )
diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py
index ad10df7cfd3d7b..e3e2a3f1617d61 100644
--- a/tests/components/media_source/test_local_source.py
+++ b/tests/components/media_source/test_local_source.py
@@ -23,7 +23,7 @@ async def test_async_browse_media(hass):
await media_source.async_browse_media(
hass, f"{const.URI_SCHEME}{const.DOMAIN}/local/test/not/exist"
)
- assert str(excinfo.value) == "Invalid path."
+ assert str(excinfo.value) == "Path does not exist."
# Test browse file
with pytest.raises(media_source.BrowseError) as excinfo:
diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py
index ccb5f951fa215f..590c5149f9e87e 100644
--- a/tests/components/melissa/test_climate.py
+++ b/tests/components/melissa/test_climate.py
@@ -94,7 +94,7 @@ async def test_current_fan_mode(hass):
device = (await api.async_fetch_devices())[_SERIAL]
thermostat = MelissaClimate(api, _SERIAL, device)
await thermostat.async_update()
- assert SPEED_LOW == thermostat.fan_mode
+ assert thermostat.fan_mode == SPEED_LOW
thermostat._cur_settings = None
assert thermostat.fan_mode is None
@@ -185,7 +185,7 @@ async def test_state(hass):
device = (await api.async_fetch_devices())[_SERIAL]
thermostat = MelissaClimate(api, _SERIAL, device)
await thermostat.async_update()
- assert HVAC_MODE_HEAT == thermostat.state
+ assert thermostat.state == HVAC_MODE_HEAT
thermostat._cur_settings = None
assert thermostat.state is None
@@ -197,7 +197,7 @@ async def test_temperature_unit(hass):
api = melissa_mock()
device = (await api.async_fetch_devices())[_SERIAL]
thermostat = MelissaClimate(api, _SERIAL, device)
- assert TEMP_CELSIUS == thermostat.temperature_unit
+ assert thermostat.temperature_unit == TEMP_CELSIUS
async def test_min_temp(hass):
@@ -225,7 +225,7 @@ async def test_supported_features(hass):
device = (await api.async_fetch_devices())[_SERIAL]
thermostat = MelissaClimate(api, _SERIAL, device)
features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE
- assert features == thermostat.supported_features
+ assert thermostat.supported_features == features
async def test_set_temperature(hass):
@@ -249,7 +249,7 @@ async def test_fan_mode(hass):
await hass.async_block_till_done()
await thermostat.async_set_fan_mode(SPEED_HIGH)
await hass.async_block_till_done()
- assert SPEED_HIGH == thermostat.fan_mode
+ assert thermostat.fan_mode == SPEED_HIGH
async def test_set_operation_mode(hass):
@@ -262,7 +262,7 @@ async def test_set_operation_mode(hass):
await hass.async_block_till_done()
await thermostat.async_set_hvac_mode(HVAC_MODE_COOL)
await hass.async_block_till_done()
- assert HVAC_MODE_COOL == thermostat.hvac_mode
+ assert thermostat.hvac_mode == HVAC_MODE_COOL
async def test_send(hass):
@@ -275,7 +275,7 @@ async def test_send(hass):
await hass.async_block_till_done()
await thermostat.async_send({"fan": api.FAN_MEDIUM})
await hass.async_block_till_done()
- assert SPEED_MEDIUM == thermostat.fan_mode
+ assert thermostat.fan_mode == SPEED_MEDIUM
api.async_send.return_value = AsyncMock(return_value=False)
thermostat._cur_settings = None
await thermostat.async_send({"fan": api.FAN_LOW})
@@ -288,19 +288,18 @@ async def test_update(hass):
"""Test update."""
with patch(
"homeassistant.components.melissa.climate._LOGGER.warning"
- ) as mocked_warning:
- with patch("homeassistant.components.melissa"):
- api = melissa_mock()
- device = (await api.async_fetch_devices())[_SERIAL]
- thermostat = MelissaClimate(api, _SERIAL, device)
- await thermostat.async_update()
- assert SPEED_LOW == thermostat.fan_mode
- assert HVAC_MODE_HEAT == thermostat.state
- api.async_status = AsyncMock(side_effect=KeyError("boom"))
- await thermostat.async_update()
- mocked_warning.assert_called_once_with(
- "Unable to update entity %s", thermostat.entity_id
- )
+ ) as mocked_warning, patch("homeassistant.components.melissa"):
+ api = melissa_mock()
+ device = (await api.async_fetch_devices())[_SERIAL]
+ thermostat = MelissaClimate(api, _SERIAL, device)
+ await thermostat.async_update()
+ assert thermostat.fan_mode == SPEED_LOW
+ assert thermostat.state == HVAC_MODE_HEAT
+ api.async_status = AsyncMock(side_effect=KeyError("boom"))
+ await thermostat.async_update()
+ mocked_warning.assert_called_once_with(
+ "Unable to update entity %s", thermostat.entity_id
+ )
async def test_melissa_op_to_hass(hass):
@@ -309,10 +308,10 @@ async def test_melissa_op_to_hass(hass):
api = melissa_mock()
device = (await api.async_fetch_devices())[_SERIAL]
thermostat = MelissaClimate(api, _SERIAL, device)
- assert HVAC_MODE_FAN_ONLY == thermostat.melissa_op_to_hass(1)
- assert HVAC_MODE_HEAT == thermostat.melissa_op_to_hass(2)
- assert HVAC_MODE_COOL == thermostat.melissa_op_to_hass(3)
- assert HVAC_MODE_DRY == thermostat.melissa_op_to_hass(4)
+ assert thermostat.melissa_op_to_hass(1) == HVAC_MODE_FAN_ONLY
+ assert thermostat.melissa_op_to_hass(2) == HVAC_MODE_HEAT
+ assert thermostat.melissa_op_to_hass(3) == HVAC_MODE_COOL
+ assert thermostat.melissa_op_to_hass(4) == HVAC_MODE_DRY
assert thermostat.melissa_op_to_hass(5) is None
@@ -322,10 +321,10 @@ async def test_melissa_fan_to_hass(hass):
api = melissa_mock()
device = (await api.async_fetch_devices())[_SERIAL]
thermostat = MelissaClimate(api, _SERIAL, device)
- assert "auto" == thermostat.melissa_fan_to_hass(0)
- assert SPEED_LOW == thermostat.melissa_fan_to_hass(1)
- assert SPEED_MEDIUM == thermostat.melissa_fan_to_hass(2)
- assert SPEED_HIGH == thermostat.melissa_fan_to_hass(3)
+ assert thermostat.melissa_fan_to_hass(0) == "auto"
+ assert thermostat.melissa_fan_to_hass(1) == SPEED_LOW
+ assert thermostat.melissa_fan_to_hass(2) == SPEED_MEDIUM
+ assert thermostat.melissa_fan_to_hass(3) == SPEED_HIGH
assert thermostat.melissa_fan_to_hass(4) is None
@@ -333,35 +332,33 @@ async def test_hass_mode_to_melissa(hass):
"""Test for hass operations to melssa."""
with patch(
"homeassistant.components.melissa.climate._LOGGER.warning"
- ) as mocked_warning:
- with patch("homeassistant.components.melissa"):
- api = melissa_mock()
- device = (await api.async_fetch_devices())[_SERIAL]
- thermostat = MelissaClimate(api, _SERIAL, device)
- assert thermostat.hass_mode_to_melissa(HVAC_MODE_FAN_ONLY) == 1
- assert thermostat.hass_mode_to_melissa(HVAC_MODE_HEAT) == 2
- assert thermostat.hass_mode_to_melissa(HVAC_MODE_COOL) == 3
- assert thermostat.hass_mode_to_melissa(HVAC_MODE_DRY) == 4
- thermostat.hass_mode_to_melissa("test")
- mocked_warning.assert_called_once_with(
- "Melissa have no setting for %s mode", "test"
- )
+ ) as mocked_warning, patch("homeassistant.components.melissa"):
+ api = melissa_mock()
+ device = (await api.async_fetch_devices())[_SERIAL]
+ thermostat = MelissaClimate(api, _SERIAL, device)
+ assert thermostat.hass_mode_to_melissa(HVAC_MODE_FAN_ONLY) == 1
+ assert thermostat.hass_mode_to_melissa(HVAC_MODE_HEAT) == 2
+ assert thermostat.hass_mode_to_melissa(HVAC_MODE_COOL) == 3
+ assert thermostat.hass_mode_to_melissa(HVAC_MODE_DRY) == 4
+ thermostat.hass_mode_to_melissa("test")
+ mocked_warning.assert_called_once_with(
+ "Melissa have no setting for %s mode", "test"
+ )
async def test_hass_fan_to_melissa(hass):
"""Test for translate melissa states to hass."""
with patch(
"homeassistant.components.melissa.climate._LOGGER.warning"
- ) as mocked_warning:
- with patch("homeassistant.components.melissa"):
- api = melissa_mock()
- device = (await api.async_fetch_devices())[_SERIAL]
- thermostat = MelissaClimate(api, _SERIAL, device)
- assert thermostat.hass_fan_to_melissa("auto") == 0
- assert thermostat.hass_fan_to_melissa(SPEED_LOW) == 1
- assert thermostat.hass_fan_to_melissa(SPEED_MEDIUM) == 2
- assert thermostat.hass_fan_to_melissa(SPEED_HIGH) == 3
- thermostat.hass_fan_to_melissa("test")
- mocked_warning.assert_called_once_with(
- "Melissa have no setting for %s fan mode", "test"
- )
+ ) as mocked_warning, patch("homeassistant.components.melissa"):
+ api = melissa_mock()
+ device = (await api.async_fetch_devices())[_SERIAL]
+ thermostat = MelissaClimate(api, _SERIAL, device)
+ assert thermostat.hass_fan_to_melissa("auto") == 0
+ assert thermostat.hass_fan_to_melissa(SPEED_LOW) == 1
+ assert thermostat.hass_fan_to_melissa(SPEED_MEDIUM) == 2
+ assert thermostat.hass_fan_to_melissa(SPEED_HIGH) == 3
+ thermostat.hass_fan_to_melissa("test")
+ mocked_warning.assert_called_once_with(
+ "Melissa have no setting for %s fan mode", "test"
+ )
diff --git a/tests/components/meraki/test_device_tracker.py b/tests/components/meraki/test_device_tracker.py
index 0fa83eec9c8f14..1c22b24411a116 100644
--- a/tests/components/meraki/test_device_tracker.py
+++ b/tests/components/meraki/test_device_tracker.py
@@ -125,9 +125,9 @@ async def test_data_will_be_saved(mock_device_tracker_conf, hass, meraki_client)
state_name = hass.states.get(
"{}.{}".format("device_tracker", "00_26_ab_b8_a9_a4")
).state
- assert "home" == state_name
+ assert state_name == "home"
state_name = hass.states.get(
"{}.{}".format("device_tracker", "00_26_ab_b8_a9_a5")
).state
- assert "home" == state_name
+ assert state_name == "home"
diff --git a/tests/components/met/__init__.py b/tests/components/met/__init__.py
index 13b186f3b47252..0a17b415965dd7 100644
--- a/tests/components/met/__init__.py
+++ b/tests/components/met/__init__.py
@@ -1,20 +1,24 @@
"""Tests for Met.no."""
from unittest.mock import patch
-from homeassistant.components.met.const import DOMAIN
+from homeassistant.components.met.const import CONF_TRACK_HOME, DOMAIN
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from tests.common import MockConfigEntry
-async def init_integration(hass) -> MockConfigEntry:
+async def init_integration(hass, track_home=False) -> MockConfigEntry:
"""Set up the Met integration in Home Assistant."""
entry_data = {
CONF_NAME: "test",
CONF_LATITUDE: 0,
- CONF_LONGITUDE: 0,
- CONF_ELEVATION: 0,
+ CONF_LONGITUDE: 1.0,
+ CONF_ELEVATION: 1.0,
}
+
+ if track_home:
+ entry_data = {CONF_TRACK_HOME: True}
+
entry = MockConfigEntry(domain=DOMAIN, data=entry_data)
with patch(
"homeassistant.components.met.metno.MetWeatherData.fetching_data",
diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py
index 622475e8376db4..25e123f67e8284 100644
--- a/tests/components/met/test_config_flow.py
+++ b/tests/components/met/test_config_flow.py
@@ -4,6 +4,7 @@
import pytest
from homeassistant.components.met.const import DOMAIN, HOME_LOCATION_NAME
+from homeassistant.config import async_process_ha_core_config
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE
from tests.common import MockConfigEntry
@@ -106,6 +107,25 @@ async def test_onboarding_step(hass):
assert result["data"] == {"track_home": True}
+@pytest.mark.parametrize("latitude,longitude", [(52.3731339, 4.8903147), (0.0, 0.0)])
+async def test_onboarding_step_abort_no_home(hass, latitude, longitude):
+ """Test entry not created when default step fails."""
+ await async_process_ha_core_config(
+ hass,
+ {"latitude": latitude, "longitude": longitude},
+ )
+
+ assert hass.config.latitude == latitude
+ assert hass.config.longitude == longitude
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "onboarding"}, data={}
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "no_home"
+
+
async def test_import_step(hass):
"""Test initializing via import step."""
test_data = {
diff --git a/tests/components/met/test_init.py b/tests/components/met/test_init.py
index a3323f0156515f..074293249c8af4 100644
--- a/tests/components/met/test_init.py
+++ b/tests/components/met/test_init.py
@@ -1,6 +1,15 @@
"""Test the Met integration init."""
-from homeassistant.components.met.const import DOMAIN
-from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED
+from homeassistant.components.met.const import (
+ DEFAULT_HOME_LATITUDE,
+ DEFAULT_HOME_LONGITUDE,
+ DOMAIN,
+)
+from homeassistant.config import async_process_ha_core_config
+from homeassistant.config_entries import (
+ ENTRY_STATE_LOADED,
+ ENTRY_STATE_NOT_LOADED,
+ ENTRY_STATE_SETUP_ERROR,
+)
from . import init_integration
@@ -17,3 +26,24 @@ async def test_unload_entry(hass):
assert entry.state == ENTRY_STATE_NOT_LOADED
assert not hass.data.get(DOMAIN)
+
+
+async def test_fail_default_home_entry(hass, caplog):
+ """Test abort setup of default home location."""
+ await async_process_ha_core_config(
+ hass,
+ {"latitude": 52.3731339, "longitude": 4.8903147},
+ )
+
+ assert hass.config.latitude == DEFAULT_HOME_LATITUDE
+ assert hass.config.longitude == DEFAULT_HOME_LONGITUDE
+
+ entry = await init_integration(hass, track_home=True)
+
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+ assert entry.state == ENTRY_STATE_SETUP_ERROR
+
+ assert (
+ "Skip setting up met.no integration; No Home location has been set"
+ in caplog.text
+ )
diff --git a/tests/components/met/test_weather.py b/tests/components/met/test_weather.py
index 242352c2498de0..92e9b67466814b 100644
--- a/tests/components/met/test_weather.py
+++ b/tests/components/met/test_weather.py
@@ -2,6 +2,7 @@
from homeassistant.components.met import DOMAIN
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
+from homeassistant.helpers import entity_registry as er
async def test_tracking_home(hass, mock_weather):
@@ -12,7 +13,7 @@ async def test_tracking_home(hass, mock_weather):
assert len(mock_weather.mock_calls) == 4
# Test the hourly sensor is disabled by default
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
state = hass.states.get("weather.test_home_hourly")
assert state is None
@@ -28,8 +29,14 @@ async def test_tracking_home(hass, mock_weather):
assert len(mock_weather.mock_calls) == 8
+ # Same coordinates again should not trigger any new requests to met.no
+ await hass.config.async_update(latitude=10, longitude=20)
+ await hass.async_block_till_done()
+ assert len(mock_weather.mock_calls) == 8
+
entry = hass.config_entries.async_entries()[0]
await hass.config_entries.async_remove(entry.entry_id)
+ await hass.async_block_till_done()
assert len(hass.states.async_entity_ids("weather")) == 0
@@ -37,7 +44,7 @@ async def test_not_tracking_home(hass, mock_weather):
"""Test when we not track home."""
# Pre-create registry entry for disabled by default hourly weather
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
registry.async_get_or_create(
WEATHER_DOMAIN,
DOMAIN,
@@ -63,4 +70,5 @@ async def test_not_tracking_home(hass, mock_weather):
entry = hass.config_entries.async_entries()[0]
await hass.config_entries.async_remove(entry.entry_id)
+ await hass.async_block_till_done()
assert len(hass.states.async_entity_ids("weather")) == 0
diff --git a/tests/components/met_eireann/__init__.py b/tests/components/met_eireann/__init__.py
new file mode 100644
index 00000000000000..3dfadc06f6be7e
--- /dev/null
+++ b/tests/components/met_eireann/__init__.py
@@ -0,0 +1,27 @@
+"""Tests for Met Éireann."""
+from unittest.mock import patch
+
+from homeassistant.components.met_eireann.const import DOMAIN
+from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+
+from tests.common import MockConfigEntry
+
+
+async def init_integration(hass) -> MockConfigEntry:
+ """Set up the Met Éireann integration in Home Assistant."""
+ entry_data = {
+ CONF_NAME: "test",
+ CONF_LATITUDE: 0,
+ CONF_LONGITUDE: 0,
+ CONF_ELEVATION: 0,
+ }
+ entry = MockConfigEntry(domain=DOMAIN, data=entry_data)
+ with patch(
+ "homeassistant.components.met_eireann.meteireann.WeatherData.fetching_data",
+ return_value=True,
+ ):
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ return entry
diff --git a/tests/components/met_eireann/conftest.py b/tests/components/met_eireann/conftest.py
new file mode 100644
index 00000000000000..e73d1e41cca9bc
--- /dev/null
+++ b/tests/components/met_eireann/conftest.py
@@ -0,0 +1,22 @@
+"""Fixtures for Met Éireann weather testing."""
+from unittest.mock import AsyncMock, patch
+
+import pytest
+
+
+@pytest.fixture
+def mock_weather():
+ """Mock weather data."""
+ with patch("meteireann.WeatherData") as mock_data:
+ mock_data = mock_data.return_value
+ mock_data.fetching_data = AsyncMock(return_value=True)
+ mock_data.get_current_weather.return_value = {
+ "condition": "Cloud",
+ "temperature": 15,
+ "pressure": 100,
+ "humidity": 50,
+ "wind_speed": 10,
+ "wind_bearing": "NE",
+ }
+ mock_data.get_forecast.return_value = {}
+ yield mock_data
diff --git a/tests/components/met_eireann/test_config_flow.py b/tests/components/met_eireann/test_config_flow.py
new file mode 100644
index 00000000000000..50060541be540f
--- /dev/null
+++ b/tests/components/met_eireann/test_config_flow.py
@@ -0,0 +1,95 @@
+"""Tests for Met Éireann config flow."""
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.components.met_eireann.const import DOMAIN, HOME_LOCATION_NAME
+from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE
+
+
+@pytest.fixture(name="met_eireann_setup", autouse=True)
+def met_setup_fixture():
+ """Patch Met Éireann setup entry."""
+ with patch(
+ "homeassistant.components.met_eireann.async_setup_entry", return_value=True
+ ):
+ yield
+
+
+async def test_show_config_form(hass):
+ """Test show configuration form."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == config_entries.SOURCE_USER
+
+
+async def test_flow_with_home_location(hass):
+ """Test config flow.
+
+ Test the flow when a default location is configured.
+ Then it should return a form with default values.
+ """
+ hass.config.latitude = 1
+ hass.config.longitude = 2
+ hass.config.elevation = 3
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == config_entries.SOURCE_USER
+
+ default_data = result["data_schema"]({})
+ assert default_data["name"] == HOME_LOCATION_NAME
+ assert default_data["latitude"] == 1
+ assert default_data["longitude"] == 2
+ assert default_data["elevation"] == 3
+
+
+async def test_create_entry(hass):
+ """Test create entry from user input."""
+ test_data = {
+ "name": "test",
+ CONF_LONGITUDE: 0,
+ CONF_LATITUDE: 0,
+ CONF_ELEVATION: 0,
+ }
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == test_data.get("name")
+ assert result["data"] == test_data
+
+
+async def test_flow_entry_already_exists(hass):
+ """Test user input for config_entry that already exists.
+
+ Test to ensure the config form does not allow duplicate entries.
+ """
+ test_data = {
+ "name": "test",
+ CONF_LONGITUDE: 0,
+ CONF_LATITUDE: 0,
+ CONF_ELEVATION: 0,
+ }
+
+ # Create the first entry and assert that it is created successfully
+ result1 = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data
+ )
+ assert result1["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ # Create the second entry and assert that it is aborted
+ result2 = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result2["reason"] == "already_configured"
diff --git a/tests/components/met_eireann/test_init.py b/tests/components/met_eireann/test_init.py
new file mode 100644
index 00000000000000..8f95013cd7202f
--- /dev/null
+++ b/tests/components/met_eireann/test_init.py
@@ -0,0 +1,19 @@
+"""Test the Met Éireann integration init."""
+from homeassistant.components.met_eireann.const import DOMAIN
+from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED
+
+from . import init_integration
+
+
+async def test_unload_entry(hass):
+ """Test successful unload of entry."""
+ entry = await init_integration(hass)
+
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+ assert entry.state == ENTRY_STATE_LOADED
+
+ assert await hass.config_entries.async_unload(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert entry.state == ENTRY_STATE_NOT_LOADED
+ assert not hass.data.get(DOMAIN)
diff --git a/tests/components/met_eireann/test_weather.py b/tests/components/met_eireann/test_weather.py
new file mode 100644
index 00000000000000..e8d01b967a6ba1
--- /dev/null
+++ b/tests/components/met_eireann/test_weather.py
@@ -0,0 +1,31 @@
+"""Test Met Éireann weather entity."""
+
+from homeassistant.components.met_eireann.const import DOMAIN
+
+from tests.common import MockConfigEntry
+
+
+async def test_weather(hass, mock_weather):
+ """Test weather entity."""
+ # Create a mock configuration for testing
+ mock_data = MockConfigEntry(
+ domain=DOMAIN,
+ data={"name": "Somewhere", "latitude": 10, "longitude": 20, "elevation": 0},
+ )
+ mock_data.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_data.entry_id)
+ await hass.async_block_till_done()
+ assert len(hass.states.async_entity_ids("weather")) == 1
+ assert len(mock_weather.mock_calls) == 4
+
+ # Test we do not track config
+ await hass.config.async_update(latitude=10, longitude=20)
+ await hass.async_block_till_done()
+
+ assert len(mock_weather.mock_calls) == 4
+
+ entry = hass.config_entries.async_entries()[0]
+ await hass.config_entries.async_remove(entry.entry_id)
+ await hass.async_block_till_done()
+ assert len(hass.states.async_entity_ids("weather")) == 0
diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py
index 610aa91cd4cd0d..6103c43d3a4c8c 100644
--- a/tests/components/mfi/test_sensor.py
+++ b/tests/components/mfi/test_sensor.py
@@ -132,7 +132,7 @@ async def test_name(port, sensor):
async def test_uom_temp(port, sensor):
"""Test the UOM temperature."""
port.tag = "temperature"
- assert TEMP_CELSIUS == sensor.unit_of_measurement
+ assert sensor.unit_of_measurement == TEMP_CELSIUS
async def test_uom_power(port, sensor):
diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py
index 0409a4f387ad8f..1e9f56853c3dd9 100644
--- a/tests/components/mfi/test_switch.py
+++ b/tests/components/mfi/test_switch.py
@@ -118,7 +118,7 @@ async def test_current_power_w_no_data(port, switch):
assert switch.current_power_w == 0
-async def test_device_state_attributes(port, switch):
+async def test_extra_state_attributes(port, switch):
"""Test the state attributes."""
port.data = {"v_rms": 1.25, "i_rms": 2.75}
- assert switch.device_state_attributes == {"volts": 1.2, "amps": 2.8}
+ assert switch.extra_state_attributes == {"volts": 1.2, "amps": 2.8}
diff --git a/tests/components/mhz19/test_sensor.py b/tests/components/mhz19/test_sensor.py
index 5b6e1e9fa37e5c..e827b5dfbd2f96 100644
--- a/tests/components/mhz19/test_sensor.py
+++ b/tests/components/mhz19/test_sensor.py
@@ -93,7 +93,7 @@ async def test_co2_sensor(mock_function):
assert sensor.state == 1000
assert sensor.unit_of_measurement == CONCENTRATION_PARTS_PER_MILLION
assert sensor.should_poll
- assert sensor.device_state_attributes == {"temperature": 24}
+ assert sensor.extra_state_attributes == {"temperature": 24}
@patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(1000, 24))
@@ -107,7 +107,7 @@ async def test_temperature_sensor(mock_function):
assert sensor.state == 24
assert sensor.unit_of_measurement == TEMP_CELSIUS
assert sensor.should_poll
- assert sensor.device_state_attributes == {"co2_concentration": 1000}
+ assert sensor.extra_state_attributes == {"co2_concentration": 1000}
@patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(1000, 24))
diff --git a/tests/components/mikrotik/test_device_tracker.py b/tests/components/mikrotik/test_device_tracker.py
index d4151be0addb24..fcd29c18682c77 100644
--- a/tests/components/mikrotik/test_device_tracker.py
+++ b/tests/components/mikrotik/test_device_tracker.py
@@ -3,7 +3,7 @@
from homeassistant.components import mikrotik
import homeassistant.components.device_tracker as device_tracker
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -101,7 +101,7 @@ async def test_restoring_devices(hass):
)
config_entry.add_to_hass(hass)
- registry = await entity_registry.async_get_registry(hass)
+ registry = er.async_get(hass)
registry.async_get_or_create(
device_tracker.DOMAIN,
mikrotik.DOMAIN,
diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py
index 93360d5778660c..c6c352a8c3e6d0 100644
--- a/tests/components/min_max/test_sensor.py
+++ b/tests/components/min_max/test_sensor.py
@@ -50,10 +50,10 @@ async def test_min_sensor(hass):
assert str(float(MIN_VALUE)) == state.state
assert entity_ids[2] == state.attributes.get("min_entity_id")
- assert MAX_VALUE == state.attributes.get("max_value")
+ assert state.attributes.get("max_value") == MAX_VALUE
assert entity_ids[1] == state.attributes.get("max_entity_id")
- assert MEAN == state.attributes.get("mean")
- assert MEDIAN == state.attributes.get("median")
+ assert state.attributes.get("mean") == MEAN
+ assert state.attributes.get("median") == MEDIAN
async def test_max_sensor(hass):
@@ -80,10 +80,10 @@ async def test_max_sensor(hass):
assert str(float(MAX_VALUE)) == state.state
assert entity_ids[2] == state.attributes.get("min_entity_id")
- assert MIN_VALUE == state.attributes.get("min_value")
+ assert state.attributes.get("min_value") == MIN_VALUE
assert entity_ids[1] == state.attributes.get("max_entity_id")
- assert MEAN == state.attributes.get("mean")
- assert MEDIAN == state.attributes.get("median")
+ assert state.attributes.get("mean") == MEAN
+ assert state.attributes.get("median") == MEDIAN
async def test_mean_sensor(hass):
@@ -109,11 +109,11 @@ async def test_mean_sensor(hass):
state = hass.states.get("sensor.test_mean")
assert str(float(MEAN)) == state.state
- assert MIN_VALUE == state.attributes.get("min_value")
+ assert state.attributes.get("min_value") == MIN_VALUE
assert entity_ids[2] == state.attributes.get("min_entity_id")
- assert MAX_VALUE == state.attributes.get("max_value")
+ assert state.attributes.get("max_value") == MAX_VALUE
assert entity_ids[1] == state.attributes.get("max_entity_id")
- assert MEDIAN == state.attributes.get("median")
+ assert state.attributes.get("median") == MEDIAN
async def test_mean_1_digit_sensor(hass):
@@ -140,11 +140,11 @@ async def test_mean_1_digit_sensor(hass):
state = hass.states.get("sensor.test_mean")
assert str(float(MEAN_1_DIGIT)) == state.state
- assert MIN_VALUE == state.attributes.get("min_value")
+ assert state.attributes.get("min_value") == MIN_VALUE
assert entity_ids[2] == state.attributes.get("min_entity_id")
- assert MAX_VALUE == state.attributes.get("max_value")
+ assert state.attributes.get("max_value") == MAX_VALUE
assert entity_ids[1] == state.attributes.get("max_entity_id")
- assert MEDIAN == state.attributes.get("median")
+ assert state.attributes.get("median") == MEDIAN
async def test_mean_4_digit_sensor(hass):
@@ -171,11 +171,11 @@ async def test_mean_4_digit_sensor(hass):
state = hass.states.get("sensor.test_mean")
assert str(float(MEAN_4_DIGITS)) == state.state
- assert MIN_VALUE == state.attributes.get("min_value")
+ assert state.attributes.get("min_value") == MIN_VALUE
assert entity_ids[2] == state.attributes.get("min_entity_id")
- assert MAX_VALUE == state.attributes.get("max_value")
+ assert state.attributes.get("max_value") == MAX_VALUE
assert entity_ids[1] == state.attributes.get("max_entity_id")
- assert MEDIAN == state.attributes.get("median")
+ assert state.attributes.get("median") == MEDIAN
async def test_median_sensor(hass):
@@ -201,11 +201,11 @@ async def test_median_sensor(hass):
state = hass.states.get("sensor.test_median")
assert str(float(MEDIAN)) == state.state
- assert MIN_VALUE == state.attributes.get("min_value")
+ assert state.attributes.get("min_value") == MIN_VALUE
assert entity_ids[2] == state.attributes.get("min_entity_id")
- assert MAX_VALUE == state.attributes.get("max_value")
+ assert state.attributes.get("max_value") == MAX_VALUE
assert entity_ids[1] == state.attributes.get("max_entity_id")
- assert MEAN == state.attributes.get("mean")
+ assert state.attributes.get("mean") == MEAN
async def test_not_enough_sensor_value(hass):
@@ -228,7 +228,7 @@ async def test_not_enough_sensor_value(hass):
await hass.async_block_till_done()
state = hass.states.get("sensor.test_max")
- assert STATE_UNKNOWN == state.state
+ assert state.state == STATE_UNKNOWN
assert state.attributes.get("min_entity_id") is None
assert state.attributes.get("min_value") is None
assert state.attributes.get("max_entity_id") is None
@@ -259,7 +259,7 @@ async def test_not_enough_sensor_value(hass):
await hass.async_block_till_done()
state = hass.states.get("sensor.test_max")
- assert STATE_UNKNOWN == state.state
+ assert state.state == STATE_UNKNOWN
assert state.attributes.get("min_entity_id") is None
assert state.attributes.get("min_value") is None
assert state.attributes.get("max_entity_id") is None
@@ -299,7 +299,7 @@ async def test_different_unit_of_measurement(hass):
state = hass.states.get("sensor.test")
- assert STATE_UNKNOWN == state.state
+ assert state.state == STATE_UNKNOWN
assert state.attributes.get("unit_of_measurement") == "ERR"
hass.states.async_set(
@@ -309,7 +309,7 @@ async def test_different_unit_of_measurement(hass):
state = hass.states.get("sensor.test")
- assert STATE_UNKNOWN == state.state
+ assert state.state == STATE_UNKNOWN
assert state.attributes.get("unit_of_measurement") == "ERR"
@@ -336,10 +336,10 @@ async def test_last_sensor(hass):
assert str(float(value)) == state.state
assert entity_id == state.attributes.get("last_entity_id")
- assert MIN_VALUE == state.attributes.get("min_value")
- assert MAX_VALUE == state.attributes.get("max_value")
- assert MEAN == state.attributes.get("mean")
- assert MEDIAN == state.attributes.get("median")
+ assert state.attributes.get("min_value") == MIN_VALUE
+ assert state.attributes.get("max_value") == MAX_VALUE
+ assert state.attributes.get("mean") == MEAN
+ assert state.attributes.get("median") == MEDIAN
async def test_reload(hass):
diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py
index 2f3b7781ecf1f2..9fcea3261ee62a 100644
--- a/tests/components/minecraft_server/test_config_flow.py
+++ b/tests/components/minecraft_server/test_config_flow.py
@@ -103,31 +103,27 @@ async def test_invalid_ip(hass: HomeAssistantType) -> None:
async def test_same_host(hass: HomeAssistantType) -> None:
"""Test abort in case of same host name."""
- with patch(
- "aiodns.DNSResolver.query",
- side_effect=aiodns.error.DNSError,
+ with patch("aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError,), patch(
+ "mcstatus.server.MinecraftServer.status",
+ return_value=PingResponse(STATUS_RESPONSE_RAW),
):
- with patch(
- "mcstatus.server.MinecraftServer.status",
- return_value=PingResponse(STATUS_RESPONSE_RAW),
- ):
- unique_id = "mc.dummyserver.com-25565"
- config_data = {
- CONF_NAME: DEFAULT_NAME,
- CONF_HOST: "mc.dummyserver.com",
- CONF_PORT: DEFAULT_PORT,
- }
- mock_config_entry = MockConfigEntry(
- domain=DOMAIN, unique_id=unique_id, data=config_data
- )
- mock_config_entry.add_to_hass(hass)
-
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
- )
-
- assert result["type"] == RESULT_TYPE_ABORT
- assert result["reason"] == "already_configured"
+ unique_id = "mc.dummyserver.com-25565"
+ config_data = {
+ CONF_NAME: DEFAULT_NAME,
+ CONF_HOST: "mc.dummyserver.com",
+ CONF_PORT: DEFAULT_PORT,
+ }
+ mock_config_entry = MockConfigEntry(
+ domain=DOMAIN, unique_id=unique_id, data=config_data
+ )
+ mock_config_entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
+ )
+
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
async def test_port_too_small(hass: HomeAssistantType) -> None:
@@ -163,93 +159,80 @@ async def test_connection_failed(hass: HomeAssistantType) -> None:
with patch(
"aiodns.DNSResolver.query",
side_effect=aiodns.error.DNSError,
- ):
- with patch("mcstatus.server.MinecraftServer.status", side_effect=OSError):
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
- )
+ ), patch("mcstatus.server.MinecraftServer.status", side_effect=OSError):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
+ )
- assert result["type"] == RESULT_TYPE_FORM
- assert result["errors"] == {"base": "cannot_connect"}
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "cannot_connect"}
async def test_connection_succeeded_with_srv_record(hass: HomeAssistantType) -> None:
"""Test config entry in case of a successful connection with a SRV record."""
- with patch(
- "aiodns.DNSResolver.query",
- return_value=SRV_RECORDS,
+ with patch("aiodns.DNSResolver.query", return_value=SRV_RECORDS,), patch(
+ "mcstatus.server.MinecraftServer.status",
+ return_value=PingResponse(STATUS_RESPONSE_RAW),
):
- with patch(
- "mcstatus.server.MinecraftServer.status",
- return_value=PingResponse(STATUS_RESPONSE_RAW),
- ):
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_SRV
- )
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_SRV
+ )
- assert result["type"] == RESULT_TYPE_CREATE_ENTRY
- assert result["title"] == USER_INPUT_SRV[CONF_HOST]
- assert result["data"][CONF_NAME] == USER_INPUT_SRV[CONF_NAME]
- assert result["data"][CONF_HOST] == USER_INPUT_SRV[CONF_HOST]
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == USER_INPUT_SRV[CONF_HOST]
+ assert result["data"][CONF_NAME] == USER_INPUT_SRV[CONF_NAME]
+ assert result["data"][CONF_HOST] == USER_INPUT_SRV[CONF_HOST]
async def test_connection_succeeded_with_host(hass: HomeAssistantType) -> None:
"""Test config entry in case of a successful connection with a host name."""
- with patch(
- "aiodns.DNSResolver.query",
- side_effect=aiodns.error.DNSError,
+ with patch("aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError,), patch(
+ "mcstatus.server.MinecraftServer.status",
+ return_value=PingResponse(STATUS_RESPONSE_RAW),
):
- with patch(
- "mcstatus.server.MinecraftServer.status",
- return_value=PingResponse(STATUS_RESPONSE_RAW),
- ):
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
- )
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
+ )
- assert result["type"] == RESULT_TYPE_CREATE_ENTRY
- assert result["title"] == USER_INPUT[CONF_HOST]
- assert result["data"][CONF_NAME] == USER_INPUT[CONF_NAME]
- assert result["data"][CONF_HOST] == "mc.dummyserver.com"
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == USER_INPUT[CONF_HOST]
+ assert result["data"][CONF_NAME] == USER_INPUT[CONF_NAME]
+ assert result["data"][CONF_HOST] == "mc.dummyserver.com"
async def test_connection_succeeded_with_ip4(hass: HomeAssistantType) -> None:
"""Test config entry in case of a successful connection with an IPv4 address."""
- with patch("getmac.get_mac_address", return_value="01:23:45:67:89:ab"):
- with patch(
- "aiodns.DNSResolver.query",
- side_effect=aiodns.error.DNSError,
- ):
- with patch(
- "mcstatus.server.MinecraftServer.status",
- return_value=PingResponse(STATUS_RESPONSE_RAW),
- ):
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV4
- )
-
- assert result["type"] == RESULT_TYPE_CREATE_ENTRY
- assert result["title"] == USER_INPUT_IPV4[CONF_HOST]
- assert result["data"][CONF_NAME] == USER_INPUT_IPV4[CONF_NAME]
- assert result["data"][CONF_HOST] == "1.1.1.1"
+ with patch("getmac.get_mac_address", return_value="01:23:45:67:89:ab"), patch(
+ "aiodns.DNSResolver.query",
+ side_effect=aiodns.error.DNSError,
+ ), patch(
+ "mcstatus.server.MinecraftServer.status",
+ return_value=PingResponse(STATUS_RESPONSE_RAW),
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV4
+ )
+
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == USER_INPUT_IPV4[CONF_HOST]
+ assert result["data"][CONF_NAME] == USER_INPUT_IPV4[CONF_NAME]
+ assert result["data"][CONF_HOST] == "1.1.1.1"
async def test_connection_succeeded_with_ip6(hass: HomeAssistantType) -> None:
"""Test config entry in case of a successful connection with an IPv6 address."""
- with patch("getmac.get_mac_address", return_value="01:23:45:67:89:ab"):
- with patch(
- "aiodns.DNSResolver.query",
- side_effect=aiodns.error.DNSError,
- ):
- with patch(
- "mcstatus.server.MinecraftServer.status",
- return_value=PingResponse(STATUS_RESPONSE_RAW),
- ):
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV6
- )
-
- assert result["type"] == RESULT_TYPE_CREATE_ENTRY
- assert result["title"] == USER_INPUT_IPV6[CONF_HOST]
- assert result["data"][CONF_NAME] == USER_INPUT_IPV6[CONF_NAME]
- assert result["data"][CONF_HOST] == "::ffff:0101:0101"
+ with patch("getmac.get_mac_address", return_value="01:23:45:67:89:ab"), patch(
+ "aiodns.DNSResolver.query",
+ side_effect=aiodns.error.DNSError,
+ ), patch(
+ "mcstatus.server.MinecraftServer.status",
+ return_value=PingResponse(STATUS_RESPONSE_RAW),
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV6
+ )
+
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == USER_INPUT_IPV6[CONF_HOST]
+ assert result["data"][CONF_NAME] == USER_INPUT_IPV6[CONF_NAME]
+ assert result["data"][CONF_HOST] == "::ffff:0101:0101"
diff --git a/tests/components/minio/test_minio.py b/tests/components/minio/test_minio.py
index de66ee2fafaad4..d6668470b5534d 100644
--- a/tests/components/minio/test_minio.py
+++ b/tests/components/minio/test_minio.py
@@ -141,16 +141,16 @@ def event_callback(event):
while not events:
await asyncio.sleep(0)
- assert 1 == len(events)
+ assert len(events) == 1
event = events[0]
- assert DOMAIN == event.event_type
- assert "s3:ObjectCreated:Put" == event.data["event_name"]
- assert "5jJkTAo.jpg" == event.data["file_name"]
- assert "test" == event.data["bucket"]
- assert "5jJkTAo.jpg" == event.data["key"]
- assert "http://url" == event.data["presigned_url"]
- assert 0 == len(event.data["metadata"])
+ assert event.event_type == DOMAIN
+ assert event.data["event_name"] == "s3:ObjectCreated:Put"
+ assert event.data["file_name"] == "5jJkTAo.jpg"
+ assert event.data["bucket"] == "test"
+ assert event.data["key"] == "5jJkTAo.jpg"
+ assert event.data["presigned_url"] == "http://url"
+ assert len(event.data["metadata"]) == 0
async def test_queue_listener():
@@ -183,7 +183,7 @@ async def test_queue_listener():
"metadata": {},
}
- assert DOMAIN == call_domain
+ assert call_domain == DOMAIN
assert json.dumps(expected_event, sort_keys=True) == json.dumps(
call_event, sort_keys=True
)
diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py
index 7c611eb1010eec..db4843c126a99f 100644
--- a/tests/components/mobile_app/conftest.py
+++ b/tests/components/mobile_app/conftest.py
@@ -7,14 +7,6 @@
from .const import REGISTER, REGISTER_CLEARTEXT
-from tests.common import mock_device_registry
-
-
-@pytest.fixture
-def registry(hass):
- """Return a configured device registry."""
- return mock_device_registry(hass)
-
@pytest.fixture
async def create_registrations(hass, authed_api_client):
diff --git a/tests/components/mobile_app/test_binary_sensor.py b/tests/components/mobile_app/test_binary_sensor.py
new file mode 100644
index 00000000000000..7965bf472cbe08
--- /dev/null
+++ b/tests/components/mobile_app/test_binary_sensor.py
@@ -0,0 +1,271 @@
+"""Entity tests for mobile_app."""
+from homeassistant.const import STATE_OFF
+from homeassistant.helpers import device_registry as dr
+
+
+async def test_sensor(hass, create_registrations, webhook_client):
+ """Test that sensors can be registered and updated."""
+ webhook_id = create_registrations[1]["webhook_id"]
+ webhook_url = f"/api/webhook/{webhook_id}"
+
+ reg_resp = await webhook_client.post(
+ webhook_url,
+ json={
+ "type": "register_sensor",
+ "data": {
+ "attributes": {"foo": "bar"},
+ "device_class": "plug",
+ "icon": "mdi:power-plug",
+ "name": "Is Charging",
+ "state": True,
+ "type": "binary_sensor",
+ "unique_id": "is_charging",
+ },
+ },
+ )
+
+ assert reg_resp.status == 201
+
+ json = await reg_resp.json()
+ assert json == {"success": True}
+ await hass.async_block_till_done()
+
+ entity = hass.states.get("binary_sensor.test_1_is_charging")
+ assert entity is not None
+
+ assert entity.attributes["device_class"] == "plug"
+ assert entity.attributes["icon"] == "mdi:power-plug"
+ assert entity.attributes["foo"] == "bar"
+ assert entity.domain == "binary_sensor"
+ assert entity.name == "Test 1 Is Charging"
+ assert entity.state == "on"
+
+ update_resp = await webhook_client.post(
+ webhook_url,
+ json={
+ "type": "update_sensor_states",
+ "data": [
+ {
+ "icon": "mdi:battery-unknown",
+ "state": False,
+ "type": "binary_sensor",
+ "unique_id": "is_charging",
+ },
+ # This invalid data should not invalidate whole request
+ {
+ "type": "binary_sensor",
+ "unique_id": "invalid_state",
+ "invalid": "data",
+ },
+ ],
+ },
+ )
+
+ assert update_resp.status == 200
+
+ json = await update_resp.json()
+ assert json["invalid_state"]["success"] is False
+
+ updated_entity = hass.states.get("binary_sensor.test_1_is_charging")
+ assert updated_entity.state == "off"
+ assert "foo" not in updated_entity.attributes
+
+ dev_reg = dr.async_get(hass)
+ assert len(dev_reg.devices) == len(create_registrations)
+
+ # Reload to verify state is restored
+ config_entry = hass.config_entries.async_entries("mobile_app")[1]
+ await hass.config_entries.async_unload(config_entry.entry_id)
+ await hass.async_block_till_done()
+ unloaded_entity = hass.states.get("binary_sensor.test_1_is_charging")
+ assert unloaded_entity.state == "unavailable"
+
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ restored_entity = hass.states.get("binary_sensor.test_1_is_charging")
+ assert restored_entity.state == updated_entity.state
+ assert restored_entity.attributes == updated_entity.attributes
+
+
+async def test_sensor_must_register(hass, create_registrations, webhook_client):
+ """Test that sensors must be registered before updating."""
+ webhook_id = create_registrations[1]["webhook_id"]
+ webhook_url = f"/api/webhook/{webhook_id}"
+ resp = await webhook_client.post(
+ webhook_url,
+ json={
+ "type": "update_sensor_states",
+ "data": [
+ {"state": True, "type": "binary_sensor", "unique_id": "battery_state"}
+ ],
+ },
+ )
+
+ assert resp.status == 200
+
+ json = await resp.json()
+ assert json["battery_state"]["success"] is False
+ assert json["battery_state"]["error"]["code"] == "not_registered"
+
+
+async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client, caplog):
+ """Test that a duplicate unique ID in registration updates the sensor."""
+ webhook_id = create_registrations[1]["webhook_id"]
+ webhook_url = f"/api/webhook/{webhook_id}"
+
+ payload = {
+ "type": "register_sensor",
+ "data": {
+ "attributes": {"foo": "bar"},
+ "device_class": "plug",
+ "icon": "mdi:power-plug",
+ "name": "Is Charging",
+ "state": True,
+ "type": "binary_sensor",
+ "unique_id": "is_charging",
+ },
+ }
+
+ reg_resp = await webhook_client.post(webhook_url, json=payload)
+
+ assert reg_resp.status == 201
+
+ reg_json = await reg_resp.json()
+ assert reg_json == {"success": True}
+ await hass.async_block_till_done()
+
+ assert "Re-register" not in caplog.text
+
+ entity = hass.states.get("binary_sensor.test_1_is_charging")
+ assert entity is not None
+
+ assert entity.attributes["device_class"] == "plug"
+ assert entity.attributes["icon"] == "mdi:power-plug"
+ assert entity.attributes["foo"] == "bar"
+ assert entity.domain == "binary_sensor"
+ assert entity.name == "Test 1 Is Charging"
+ assert entity.state == "on"
+
+ payload["data"]["state"] = False
+ dupe_resp = await webhook_client.post(webhook_url, json=payload)
+
+ assert dupe_resp.status == 201
+ dupe_reg_json = await dupe_resp.json()
+ assert dupe_reg_json == {"success": True}
+ await hass.async_block_till_done()
+
+ assert "Re-register" in caplog.text
+
+ entity = hass.states.get("binary_sensor.test_1_is_charging")
+ assert entity is not None
+
+ assert entity.attributes["device_class"] == "plug"
+ assert entity.attributes["icon"] == "mdi:power-plug"
+ assert entity.attributes["foo"] == "bar"
+ assert entity.domain == "binary_sensor"
+ assert entity.name == "Test 1 Is Charging"
+ assert entity.state == "off"
+
+
+async def test_register_sensor_no_state(hass, create_registrations, webhook_client):
+ """Test that sensors can be registered, when there is no (unknown) state."""
+ webhook_id = create_registrations[1]["webhook_id"]
+ webhook_url = f"/api/webhook/{webhook_id}"
+
+ reg_resp = await webhook_client.post(
+ webhook_url,
+ json={
+ "type": "register_sensor",
+ "data": {
+ "name": "Is Charging",
+ "state": None,
+ "type": "binary_sensor",
+ "unique_id": "is_charging",
+ },
+ },
+ )
+
+ assert reg_resp.status == 201
+
+ json = await reg_resp.json()
+ assert json == {"success": True}
+ await hass.async_block_till_done()
+
+ entity = hass.states.get("binary_sensor.test_1_is_charging")
+ assert entity is not None
+
+ assert entity.domain == "binary_sensor"
+ assert entity.name == "Test 1 Is Charging"
+ assert entity.state == STATE_OFF # Binary sensor defaults to off
+
+ reg_resp = await webhook_client.post(
+ webhook_url,
+ json={
+ "type": "register_sensor",
+ "data": {
+ "name": "Backup Is Charging",
+ "type": "binary_sensor",
+ "unique_id": "backup_is_charging",
+ },
+ },
+ )
+
+ assert reg_resp.status == 201
+
+ json = await reg_resp.json()
+ assert json == {"success": True}
+ await hass.async_block_till_done()
+
+ entity = hass.states.get("binary_sensor.test_1_backup_is_charging")
+ assert entity
+
+ assert entity.domain == "binary_sensor"
+ assert entity.name == "Test 1 Backup Is Charging"
+ assert entity.state == STATE_OFF # Binary sensor defaults to off
+
+
+async def test_update_sensor_no_state(hass, create_registrations, webhook_client):
+ """Test that sensors can be updated, when there is no (unknown) state."""
+ webhook_id = create_registrations[1]["webhook_id"]
+ webhook_url = f"/api/webhook/{webhook_id}"
+
+ reg_resp = await webhook_client.post(
+ webhook_url,
+ json={
+ "type": "register_sensor",
+ "data": {
+ "name": "Is Charging",
+ "state": True,
+ "type": "binary_sensor",
+ "unique_id": "is_charging",
+ },
+ },
+ )
+
+ assert reg_resp.status == 201
+
+ json = await reg_resp.json()
+ assert json == {"success": True}
+ await hass.async_block_till_done()
+
+ entity = hass.states.get("binary_sensor.test_1_is_charging")
+ assert entity is not None
+ assert entity.state == "on"
+
+ update_resp = await webhook_client.post(
+ webhook_url,
+ json={
+ "type": "update_sensor_states",
+ "data": [
+ {"state": None, "type": "binary_sensor", "unique_id": "is_charging"}
+ ],
+ },
+ )
+
+ assert update_resp.status == 200
+
+ json = await update_resp.json()
+ assert json == {"is_charging": {"success": True}}
+
+ updated_entity = hass.states.get("binary_sensor.test_1_is_charging")
+ assert updated_entity.state == STATE_OFF # Binary sensor defaults to off
diff --git a/tests/components/mobile_app/test_init.py b/tests/components/mobile_app/test_init.py
index fe956796a965c1..abd4b0a72185b4 100644
--- a/tests/components/mobile_app/test_init.py
+++ b/tests/components/mobile_app/test_init.py
@@ -1,5 +1,6 @@
"""Tests for the mobile app integration."""
from homeassistant.components.mobile_app.const import DATA_DELETED_IDS, DOMAIN
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from .const import CALL_SERVICE
@@ -30,8 +31,8 @@ async def test_remove_entry(hass, create_registrations):
await hass.config_entries.async_remove(config_entry.entry_id)
assert config_entry.data["webhook_id"] in hass.data[DOMAIN][DATA_DELETED_IDS]
- dev_reg = await hass.helpers.device_registry.async_get_registry()
+ dev_reg = dr.async_get(hass)
assert len(dev_reg.devices) == 0
- ent_reg = await hass.helpers.entity_registry.async_get_registry()
+ ent_reg = er.async_get(hass)
assert len(ent_reg.entities) == 0
diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_sensor.py
similarity index 91%
rename from tests/components/mobile_app/test_entity.py
rename to tests/components/mobile_app/test_sensor.py
index ba121d766acfbc..ed638301bd6e3d 100644
--- a/tests/components/mobile_app/test_entity.py
+++ b/tests/components/mobile_app/test_sensor.py
@@ -1,6 +1,6 @@
"""Entity tests for mobile_app."""
from homeassistant.const import PERCENTAGE, STATE_UNKNOWN
-from homeassistant.helpers import device_registry
+from homeassistant.helpers import device_registry as dr
async def test_sensor(hass, create_registrations, webhook_client):
@@ -66,10 +66,24 @@ async def test_sensor(hass, create_registrations, webhook_client):
updated_entity = hass.states.get("sensor.test_1_battery_state")
assert updated_entity.state == "123"
+ assert "foo" not in updated_entity.attributes
- dev_reg = await device_registry.async_get_registry(hass)
+ dev_reg = dr.async_get(hass)
assert len(dev_reg.devices) == len(create_registrations)
+ # Reload to verify state is restored
+ config_entry = hass.config_entries.async_entries("mobile_app")[1]
+ await hass.config_entries.async_unload(config_entry.entry_id)
+ await hass.async_block_till_done()
+ unloaded_entity = hass.states.get("sensor.test_1_battery_state")
+ assert unloaded_entity.state == "unavailable"
+
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ restored_entity = hass.states.get("sensor.test_1_battery_state")
+ assert restored_entity.state == updated_entity.state
+ assert restored_entity.attributes == updated_entity.attributes
+
async def test_sensor_must_register(hass, create_registrations, webhook_client):
"""Test that sensors must be registered before updating."""
diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py
index 831c8250d7ae5d..d5cb72fa850de2 100644
--- a/tests/components/mobile_app/test_webhook.py
+++ b/tests/components/mobile_app/test_webhook.py
@@ -9,7 +9,6 @@
from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
-from homeassistant.setup import async_setup_component
from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE
@@ -109,7 +108,7 @@ async def test_webhook_handle_fire_event(hass, create_registrations, webhook_cli
@callback
def store_event(event):
- """Helepr to store events."""
+ """Help store events."""
events.append(event)
hass.bus.async_listen("test_event", store_event)
@@ -152,11 +151,29 @@ async def test_webhook_update_registration(webhook_client, authed_api_client):
async def test_webhook_handle_get_zones(hass, create_registrations, webhook_client):
"""Test that we can get zones properly."""
- await async_setup_component(
- hass,
- ZONE_DOMAIN,
- {ZONE_DOMAIN: {}},
- )
+ # Zone is already loaded as part of the fixture,
+ # so we just trigger a reload.
+ with patch(
+ "homeassistant.config.load_yaml_config_file",
+ autospec=True,
+ return_value={
+ ZONE_DOMAIN: [
+ {
+ "name": "School",
+ "latitude": 32.8773367,
+ "longitude": -117.2494053,
+ "radius": 250,
+ "icon": "mdi:school",
+ },
+ {
+ "name": "Work",
+ "latitude": 33.8773367,
+ "longitude": -118.2494053,
+ },
+ ]
+ },
+ ):
+ await hass.services.async_call(ZONE_DOMAIN, "reload", blocking=True)
resp = await webhook_client.post(
"/api/webhook/{}".format(create_registrations[1]["webhook_id"]),
@@ -166,10 +183,21 @@ async def test_webhook_handle_get_zones(hass, create_registrations, webhook_clie
assert resp.status == 200
json = await resp.json()
- assert len(json) == 1
+ assert len(json) == 3
zones = sorted(json, key=lambda entry: entry["entity_id"])
assert zones[0]["entity_id"] == "zone.home"
+ assert zones[1]["entity_id"] == "zone.school"
+ assert zones[1]["attributes"]["icon"] == "mdi:school"
+ assert zones[1]["attributes"]["latitude"] == 32.8773367
+ assert zones[1]["attributes"]["longitude"] == -117.2494053
+ assert zones[1]["attributes"]["radius"] == 250
+
+ assert zones[2]["entity_id"] == "zone.work"
+ assert "icon" not in zones[2]["attributes"]
+ assert zones[2]["attributes"]["latitude"] == 33.8773367
+ assert zones[2]["attributes"]["longitude"] == -118.2494053
+
async def test_webhook_handle_get_config(hass, create_registrations, webhook_client):
"""Test that we can get config properly."""
diff --git a/tests/components/mochad/conftest.py b/tests/components/mochad/conftest.py
index 9b095046d9c980..bd543eae94349e 100644
--- a/tests/components/mochad/conftest.py
+++ b/tests/components/mochad/conftest.py
@@ -1,2 +1,2 @@
"""mochad conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
diff --git a/tests/components/mochad/test_switch.py b/tests/components/mochad/test_switch.py
index 218248c3442237..0242417cb714da 100644
--- a/tests/components/mochad/test_switch.py
+++ b/tests/components/mochad/test_switch.py
@@ -39,7 +39,7 @@ async def test_setup_adds_proper_devices(hass):
async def test_name(switch_mock):
"""Test the name."""
- assert "fake_switch" == switch_mock.name
+ assert switch_mock.name == "fake_switch"
async def test_turn_on(switch_mock):
diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py
index e3a707b7fc9024..2c83f40546f3d8 100644
--- a/tests/components/modbus/conftest.py
+++ b/tests/components/modbus/conftest.py
@@ -1,31 +1,25 @@
"""The tests for the Modbus sensor component."""
+from datetime import timedelta
+import logging
from unittest import mock
-from unittest.mock import patch
import pytest
-from homeassistant.components.modbus.const import (
- CALL_TYPE_COIL,
- CALL_TYPE_DISCRETE,
- CALL_TYPE_REGISTER_INPUT,
- DEFAULT_HUB,
- MODBUS_DOMAIN as DOMAIN,
+from homeassistant.components.modbus.const import DEFAULT_HUB, MODBUS_DOMAIN as DOMAIN
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_NAME,
+ CONF_PLATFORM,
+ CONF_PORT,
+ CONF_SCAN_INTERVAL,
+ CONF_TYPE,
)
-from homeassistant.const import CONF_PLATFORM, CONF_SCAN_INTERVAL
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed
-
-@pytest.fixture()
-def mock_hub(hass):
- """Mock hub."""
- with patch("homeassistant.components.modbus.setup", return_value=True):
- hub = mock.MagicMock()
- hub.name = "hub"
- hass.data[DOMAIN] = {DEFAULT_HUB: hub}
- yield hub
+_LOGGER = logging.getLogger(__name__)
class ReadResult:
@@ -37,65 +31,120 @@ def __init__(self, register_words):
self.bits = register_words
-async def setup_base_test(
- sensor_name,
+async def base_test(
hass,
- use_mock_hub,
- data_array,
+ config_device,
+ device_name,
entity_domain,
- scan_interval,
+ array_name_discovery,
+ array_name_old_config,
+ register_words,
+ expected,
+ method_discovery=False,
+ check_config_only=False,
+ config_modbus=None,
+ scan_interval=None,
):
- """Run setup device for given config."""
-
- # Full sensor configuration
- config = {
- entity_domain: {
- CONF_PLATFORM: "modbus",
- CONF_SCAN_INTERVAL: scan_interval,
- **data_array,
+ """Run test on device for given config."""
+
+ if config_modbus is None:
+ config_modbus = {
+ DOMAIN: {
+ CONF_NAME: DEFAULT_HUB,
+ CONF_TYPE: "tcp",
+ CONF_HOST: "modbusTest",
+ CONF_PORT: 5001,
+ },
}
- }
-
- # Initialize sensor
- now = dt_util.utcnow()
- with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now):
- assert await async_setup_component(hass, entity_domain, config)
- await hass.async_block_till_done()
-
- entity_id = f"{entity_domain}.{sensor_name}"
- device = hass.states.get(entity_id)
- if device is None:
- pytest.fail("CONFIG failed, see output")
- return entity_id, now, device
-
-async def run_base_read_test(
- entity_id,
+ mock_sync = mock.MagicMock()
+ with mock.patch(
+ "homeassistant.components.modbus.modbus.ModbusTcpClient", return_value=mock_sync
+ ), mock.patch(
+ "homeassistant.components.modbus.modbus.ModbusSerialClient",
+ return_value=mock_sync,
+ ), mock.patch(
+ "homeassistant.components.modbus.modbus.ModbusUdpClient", return_value=mock_sync
+ ):
+
+ # Setup inputs for the sensor
+ read_result = ReadResult(register_words)
+ mock_sync.read_coils.return_value = read_result
+ mock_sync.read_discrete_inputs.return_value = read_result
+ mock_sync.read_input_registers.return_value = read_result
+ mock_sync.read_holding_registers.return_value = read_result
+
+ # mock timer and add old/new config
+ now = dt_util.utcnow()
+ with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now):
+ if method_discovery and config_device is not None:
+ # setup modbus which in turn does setup for the devices
+ config_modbus[DOMAIN].update(
+ {array_name_discovery: [{**config_device}]}
+ )
+ config_device = None
+ assert await async_setup_component(hass, DOMAIN, config_modbus)
+ await hass.async_block_till_done()
+
+ # setup platform old style
+ if config_device is not None:
+ config_device = {
+ entity_domain: {
+ CONF_PLATFORM: DOMAIN,
+ array_name_old_config: [
+ {
+ **config_device,
+ }
+ ],
+ }
+ }
+ if scan_interval is not None:
+ config_device[entity_domain][CONF_SCAN_INTERVAL] = scan_interval
+ assert await async_setup_component(hass, entity_domain, config_device)
+ await hass.async_block_till_done()
+
+ assert DOMAIN in hass.data
+ if config_device is not None:
+ entity_id = f"{entity_domain}.{device_name}"
+ device = hass.states.get(entity_id)
+ if device is None:
+ pytest.fail("CONFIG failed, see output")
+ if check_config_only:
+ return
+
+ # Trigger update call with time_changed event
+ now = now + timedelta(seconds=scan_interval + 60)
+ with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now):
+ async_fire_time_changed(hass, now)
+ await hass.async_block_till_done()
+
+ # Check state
+ entity_id = f"{entity_domain}.{device_name}"
+ return hass.states.get(entity_id).state
+
+
+async def base_config_test(
hass,
- use_mock_hub,
- register_type,
- register_words,
- expected,
- now,
+ config_device,
+ device_name,
+ entity_domain,
+ array_name_discovery,
+ array_name_old_config,
+ method_discovery=False,
+ config_modbus=None,
):
- """Run test for given config."""
-
- # Setup inputs for the sensor
- read_result = ReadResult(register_words)
- if register_type == CALL_TYPE_COIL:
- use_mock_hub.read_coils.return_value = read_result
- elif register_type == CALL_TYPE_DISCRETE:
- use_mock_hub.read_discrete_inputs.return_value = read_result
- elif register_type == CALL_TYPE_REGISTER_INPUT:
- use_mock_hub.read_input_registers.return_value = read_result
- else: # CALL_TYPE_REGISTER_HOLDING
- use_mock_hub.read_holding_registers.return_value = read_result
-
- # Trigger update call with time_changed event
- with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now):
- async_fire_time_changed(hass, now)
- await hass.async_block_till_done()
-
- # Check state
- state = hass.states.get(entity_id).state
- assert state == expected
+ """Check config of device for given config."""
+
+ await base_test(
+ hass,
+ config_device,
+ device_name,
+ entity_domain,
+ array_name_discovery,
+ array_name_old_config,
+ None,
+ None,
+ method_discovery=method_discovery,
+ check_config_only=True,
+ config_modbus=config_modbus,
+ )
diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py
new file mode 100644
index 00000000000000..cd8edb656a091d
--- /dev/null
+++ b/tests/components/modbus/test_init.py
@@ -0,0 +1,35 @@
+"""The tests for the Modbus init."""
+import pytest
+import voluptuous as vol
+
+from homeassistant.components.modbus import number
+
+
+@pytest.mark.parametrize(
+ "value,value_type",
+ [
+ (15, int),
+ (15.1, float),
+ ("15", int),
+ ("15.1", float),
+ (-15, int),
+ (-15.1, float),
+ ("-15", int),
+ ("-15.1", float),
+ ],
+)
+async def test_number_validator(value, value_type):
+ """Test number validator."""
+
+ assert isinstance(number(value), value_type)
+
+
+async def test_number_exception():
+ """Test number exception."""
+
+ try:
+ number("x15.1")
+ except (vol.Invalid):
+ return
+
+ pytest.fail("Number not throwing exception")
diff --git a/tests/components/modbus/test_modbus.py b/tests/components/modbus/test_modbus.py
new file mode 100644
index 00000000000000..708519bbb440d6
--- /dev/null
+++ b/tests/components/modbus/test_modbus.py
@@ -0,0 +1,72 @@
+"""The tests for the Modbus sensor component."""
+import pytest
+
+from homeassistant.components.modbus.const import (
+ CONF_BAUDRATE,
+ CONF_BYTESIZE,
+ CONF_PARITY,
+ CONF_STOPBITS,
+ MODBUS_DOMAIN as DOMAIN,
+)
+from homeassistant.const import (
+ CONF_DELAY,
+ CONF_HOST,
+ CONF_METHOD,
+ CONF_NAME,
+ CONF_PORT,
+ CONF_TIMEOUT,
+ CONF_TYPE,
+)
+
+from .conftest import base_config_test
+
+
+@pytest.mark.parametrize("do_discovery", [False, True])
+@pytest.mark.parametrize(
+ "do_options",
+ [
+ {},
+ {
+ CONF_NAME: "modbusTest",
+ CONF_TIMEOUT: 30,
+ CONF_DELAY: 10,
+ },
+ ],
+)
+@pytest.mark.parametrize(
+ "do_config",
+ [
+ {
+ CONF_TYPE: "tcp",
+ CONF_HOST: "modbusTestHost",
+ CONF_PORT: 5501,
+ },
+ {
+ CONF_TYPE: "serial",
+ CONF_BAUDRATE: 9600,
+ CONF_BYTESIZE: 8,
+ CONF_METHOD: "rtu",
+ CONF_PORT: "usb01",
+ CONF_PARITY: "E",
+ CONF_STOPBITS: 1,
+ },
+ ],
+)
+async def test_config_modbus(hass, do_discovery, do_options, do_config):
+ """Run test for modbus."""
+ config = {
+ DOMAIN: {
+ **do_config,
+ **do_options,
+ }
+ }
+ await base_config_test(
+ hass,
+ None,
+ "",
+ DOMAIN,
+ None,
+ None,
+ method_discovery=do_discovery,
+ config_modbus=config,
+ )
diff --git a/tests/components/modbus/test_modbus_binary_sensor.py b/tests/components/modbus/test_modbus_binary_sensor.py
index 91374cde22da4c..5c4e71cd669362 100644
--- a/tests/components/modbus/test_modbus_binary_sensor.py
+++ b/tests/components/modbus/test_modbus_binary_sensor.py
@@ -1,93 +1,96 @@
"""The tests for the Modbus sensor component."""
-from datetime import timedelta
-
import pytest
from homeassistant.components.binary_sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.modbus.const import (
CALL_TYPE_COIL,
CALL_TYPE_DISCRETE,
- CONF_ADDRESS,
CONF_INPUT_TYPE,
CONF_INPUTS,
)
-from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON
+from homeassistant.const import (
+ CONF_ADDRESS,
+ CONF_BINARY_SENSORS,
+ CONF_DEVICE_CLASS,
+ CONF_NAME,
+ CONF_SLAVE,
+ STATE_OFF,
+ STATE_ON,
+)
-from .conftest import run_base_read_test, setup_base_test
+from .conftest import base_config_test, base_test
+
+
+@pytest.mark.parametrize("do_discovery", [False, True])
+@pytest.mark.parametrize(
+ "do_options",
+ [
+ {},
+ {
+ CONF_SLAVE: 10,
+ CONF_INPUT_TYPE: CALL_TYPE_DISCRETE,
+ CONF_DEVICE_CLASS: "door",
+ },
+ ],
+)
+async def test_config_binary_sensor(hass, do_discovery, do_options):
+ """Run test for binary sensor."""
+ sensor_name = "test_sensor"
+ config_sensor = {
+ CONF_NAME: sensor_name,
+ CONF_ADDRESS: 51,
+ **do_options,
+ }
+ await base_config_test(
+ hass,
+ config_sensor,
+ sensor_name,
+ SENSOR_DOMAIN,
+ CONF_BINARY_SENSORS,
+ CONF_INPUTS,
+ method_discovery=do_discovery,
+ )
+@pytest.mark.parametrize("do_type", [CALL_TYPE_COIL, CALL_TYPE_DISCRETE])
@pytest.mark.parametrize(
- "cfg,regs,expected",
+ "regs,expected",
[
(
- {
- CONF_INPUT_TYPE: CALL_TYPE_COIL,
- },
[0xFF],
STATE_ON,
),
(
- {
- CONF_INPUT_TYPE: CALL_TYPE_COIL,
- },
[0x01],
STATE_ON,
),
(
- {
- CONF_INPUT_TYPE: CALL_TYPE_COIL,
- },
[0x00],
STATE_OFF,
),
(
- {
- CONF_INPUT_TYPE: CALL_TYPE_COIL,
- },
[0x80],
STATE_OFF,
),
(
- {
- CONF_INPUT_TYPE: CALL_TYPE_COIL,
- },
[0xFE],
STATE_OFF,
),
- (
- {
- CONF_INPUT_TYPE: CALL_TYPE_DISCRETE,
- },
- [0xFF],
- STATE_ON,
- ),
- (
- {
- CONF_INPUT_TYPE: CALL_TYPE_DISCRETE,
- },
- [0x00],
- STATE_OFF,
- ),
],
)
-async def test_coil_true(hass, mock_hub, cfg, regs, expected):
+async def test_all_binary_sensor(hass, do_type, regs, expected):
"""Run test for given config."""
sensor_name = "modbus_test_binary_sensor"
- scan_interval = 5
- entity_id, now, device = await setup_base_test(
- sensor_name,
+ state = await base_test(
hass,
- mock_hub,
- {CONF_INPUTS: [dict(**{CONF_NAME: sensor_name, CONF_ADDRESS: 1234}, **cfg)]},
+ {CONF_NAME: sensor_name, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: do_type},
+ sensor_name,
SENSOR_DOMAIN,
- scan_interval,
- )
- await run_base_read_test(
- entity_id,
- hass,
- mock_hub,
- cfg.get(CONF_INPUT_TYPE),
+ CONF_BINARY_SENSORS,
+ CONF_INPUTS,
regs,
expected,
- now + timedelta(seconds=scan_interval + 1),
+ method_discovery=True,
+ scan_interval=5,
)
+ assert state == expected
diff --git a/tests/components/modbus/test_modbus_climate.py b/tests/components/modbus/test_modbus_climate.py
new file mode 100644
index 00000000000000..f932817b12e980
--- /dev/null
+++ b/tests/components/modbus/test_modbus_climate.py
@@ -0,0 +1,78 @@
+"""The tests for the Modbus climate component."""
+import pytest
+
+from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
+from homeassistant.components.modbus.const import (
+ CONF_CLIMATES,
+ CONF_CURRENT_TEMP,
+ CONF_DATA_COUNT,
+ CONF_TARGET_TEMP,
+)
+from homeassistant.const import CONF_NAME, CONF_SCAN_INTERVAL, CONF_SLAVE
+
+from .conftest import base_config_test, base_test
+
+
+@pytest.mark.parametrize(
+ "do_options",
+ [
+ {},
+ {
+ CONF_SCAN_INTERVAL: 20,
+ CONF_DATA_COUNT: 2,
+ },
+ ],
+)
+async def test_config_climate(hass, do_options):
+ """Run test for climate."""
+ device_name = "test_climate"
+ device_config = {
+ CONF_NAME: device_name,
+ CONF_TARGET_TEMP: 117,
+ CONF_CURRENT_TEMP: 117,
+ CONF_SLAVE: 10,
+ **do_options,
+ }
+ await base_config_test(
+ hass,
+ device_config,
+ device_name,
+ CLIMATE_DOMAIN,
+ CONF_CLIMATES,
+ None,
+ method_discovery=True,
+ )
+
+
+@pytest.mark.parametrize(
+ "regs,expected",
+ [
+ (
+ [0x00],
+ "auto",
+ ),
+ ],
+)
+async def test_temperature_climate(hass, regs, expected):
+ """Run test for given config."""
+ climate_name = "modbus_test_climate"
+ return
+ state = await base_test(
+ hass,
+ {
+ CONF_NAME: climate_name,
+ CONF_SLAVE: 1,
+ CONF_TARGET_TEMP: 117,
+ CONF_CURRENT_TEMP: 117,
+ CONF_DATA_COUNT: 2,
+ },
+ climate_name,
+ CLIMATE_DOMAIN,
+ CONF_CLIMATES,
+ None,
+ regs,
+ expected,
+ method_discovery=True,
+ scan_interval=5,
+ )
+ assert state == expected
diff --git a/tests/components/modbus/test_modbus_cover.py b/tests/components/modbus/test_modbus_cover.py
new file mode 100644
index 00000000000000..b101c6784d551d
--- /dev/null
+++ b/tests/components/modbus/test_modbus_cover.py
@@ -0,0 +1,139 @@
+"""The tests for the Modbus cover component."""
+import pytest
+
+from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
+from homeassistant.components.modbus.const import CALL_TYPE_COIL, CONF_REGISTER
+from homeassistant.const import (
+ CONF_COVERS,
+ CONF_NAME,
+ CONF_SCAN_INTERVAL,
+ CONF_SLAVE,
+ STATE_OPEN,
+ STATE_OPENING,
+)
+
+from .conftest import base_config_test, base_test
+
+
+@pytest.mark.parametrize(
+ "do_options",
+ [
+ {},
+ {
+ CONF_SLAVE: 10,
+ CONF_SCAN_INTERVAL: 20,
+ },
+ ],
+)
+@pytest.mark.parametrize("read_type", [CALL_TYPE_COIL, CONF_REGISTER])
+async def test_config_cover(hass, do_options, read_type):
+ """Run test for cover."""
+ device_name = "test_cover"
+ device_config = {
+ CONF_NAME: device_name,
+ read_type: 1234,
+ **do_options,
+ }
+ await base_config_test(
+ hass,
+ device_config,
+ device_name,
+ COVER_DOMAIN,
+ CONF_COVERS,
+ None,
+ method_discovery=True,
+ )
+
+
+@pytest.mark.parametrize(
+ "regs,expected",
+ [
+ (
+ [0x00],
+ STATE_OPENING,
+ ),
+ (
+ [0x80],
+ STATE_OPENING,
+ ),
+ (
+ [0xFE],
+ STATE_OPENING,
+ ),
+ (
+ [0xFF],
+ STATE_OPENING,
+ ),
+ (
+ [0x01],
+ STATE_OPENING,
+ ),
+ ],
+)
+async def test_coil_cover(hass, regs, expected):
+ """Run test for given config."""
+ cover_name = "modbus_test_cover"
+ state = await base_test(
+ hass,
+ {
+ CONF_NAME: cover_name,
+ CALL_TYPE_COIL: 1234,
+ CONF_SLAVE: 1,
+ },
+ cover_name,
+ COVER_DOMAIN,
+ CONF_COVERS,
+ None,
+ regs,
+ expected,
+ method_discovery=True,
+ scan_interval=5,
+ )
+ assert state == expected
+
+
+@pytest.mark.parametrize(
+ "regs,expected",
+ [
+ (
+ [0x00],
+ STATE_OPEN,
+ ),
+ (
+ [0x80],
+ STATE_OPEN,
+ ),
+ (
+ [0xFE],
+ STATE_OPEN,
+ ),
+ (
+ [0xFF],
+ STATE_OPEN,
+ ),
+ (
+ [0x01],
+ STATE_OPEN,
+ ),
+ ],
+)
+async def test_register_COVER(hass, regs, expected):
+ """Run test for given config."""
+ cover_name = "modbus_test_cover"
+ state = await base_test(
+ hass,
+ {
+ CONF_NAME: cover_name,
+ CONF_REGISTER: 1234,
+ CONF_SLAVE: 1,
+ },
+ cover_name,
+ COVER_DOMAIN,
+ CONF_COVERS,
+ None,
+ regs,
+ expected,
+ method_discovery=True,
+ scan_interval=5,
+ )
+ assert state == expected
diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py
index 68cdbffa462bc4..ce9889d8aaa106 100644
--- a/tests/components/modbus/test_modbus_sensor.py
+++ b/tests/components/modbus/test_modbus_sensor.py
@@ -1,14 +1,11 @@
"""The tests for the Modbus sensor component."""
-from datetime import timedelta
-
import pytest
from homeassistant.components.modbus.const import (
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
- CONF_COUNT,
CONF_DATA_TYPE,
- CONF_OFFSET,
+ CONF_INPUT_TYPE,
CONF_PRECISION,
CONF_REGISTER,
CONF_REGISTER_TYPE,
@@ -21,9 +18,112 @@
DATA_TYPE_UINT,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
-from homeassistant.const import CONF_NAME
+from homeassistant.const import (
+ CONF_ADDRESS,
+ CONF_COUNT,
+ CONF_DEVICE_CLASS,
+ CONF_NAME,
+ CONF_OFFSET,
+ CONF_SENSORS,
+ CONF_SLAVE,
+)
-from .conftest import run_base_read_test, setup_base_test
+from .conftest import base_config_test, base_test
+
+
+@pytest.mark.parametrize(
+ "do_discovery, do_config",
+ [
+ (
+ False,
+ {
+ CONF_REGISTER: 51,
+ },
+ ),
+ (
+ False,
+ {
+ CONF_REGISTER: 51,
+ CONF_SLAVE: 10,
+ CONF_COUNT: 1,
+ CONF_DATA_TYPE: "int",
+ CONF_PRECISION: 0,
+ CONF_SCALE: 1,
+ CONF_REVERSE_ORDER: False,
+ CONF_OFFSET: 0,
+ CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
+ CONF_DEVICE_CLASS: "battery",
+ },
+ ),
+ (
+ False,
+ {
+ CONF_REGISTER: 51,
+ CONF_SLAVE: 10,
+ CONF_COUNT: 1,
+ CONF_DATA_TYPE: "int",
+ CONF_PRECISION: 0,
+ CONF_SCALE: 1,
+ CONF_REVERSE_ORDER: False,
+ CONF_OFFSET: 0,
+ CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_INPUT,
+ CONF_DEVICE_CLASS: "battery",
+ },
+ ),
+ (
+ True,
+ {
+ CONF_ADDRESS: 51,
+ },
+ ),
+ (
+ True,
+ {
+ CONF_ADDRESS: 51,
+ CONF_SLAVE: 10,
+ CONF_COUNT: 1,
+ CONF_DATA_TYPE: "int",
+ CONF_PRECISION: 0,
+ CONF_SCALE: 1,
+ CONF_REVERSE_ORDER: False,
+ CONF_OFFSET: 0,
+ CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
+ CONF_DEVICE_CLASS: "battery",
+ },
+ ),
+ (
+ True,
+ {
+ CONF_ADDRESS: 51,
+ CONF_SLAVE: 10,
+ CONF_COUNT: 1,
+ CONF_DATA_TYPE: "int",
+ CONF_PRECISION: 0,
+ CONF_SCALE: 1,
+ CONF_REVERSE_ORDER: False,
+ CONF_OFFSET: 0,
+ CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT,
+ CONF_DEVICE_CLASS: "battery",
+ },
+ ),
+ ],
+)
+async def test_config_sensor(hass, do_discovery, do_config):
+ """Run test for sensor."""
+ sensor_name = "test_sensor"
+ config_sensor = {
+ CONF_NAME: sensor_name,
+ **do_config,
+ }
+ await base_config_test(
+ hass,
+ config_sensor,
+ sensor_name,
+ SENSOR_DOMAIN,
+ CONF_SENSORS,
+ CONF_REGISTERS,
+ method_discovery=do_discovery,
+ )
@pytest.mark.parametrize(
@@ -189,7 +289,7 @@
(
{
CONF_COUNT: 2,
- CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_INPUT,
+ CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT,
CONF_DATA_TYPE: DATA_TYPE_UINT,
CONF_SCALE: 1,
CONF_OFFSET: 0,
@@ -201,7 +301,7 @@
(
{
CONF_COUNT: 2,
- CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
+ CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
CONF_DATA_TYPE: DATA_TYPE_UINT,
CONF_SCALE: 1,
CONF_OFFSET: 0,
@@ -213,7 +313,7 @@
(
{
CONF_COUNT: 2,
- CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
+ CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
CONF_DATA_TYPE: DATA_TYPE_FLOAT,
CONF_SCALE: 1,
CONF_OFFSET: 0,
@@ -225,7 +325,7 @@
(
{
CONF_COUNT: 8,
- CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
+ CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
CONF_DATA_TYPE: DATA_TYPE_STRING,
CONF_SCALE: 1,
CONF_OFFSET: 0,
@@ -236,28 +336,19 @@
),
],
)
-async def test_all_sensor(hass, mock_hub, cfg, regs, expected):
+async def test_all_sensor(hass, cfg, regs, expected):
"""Run test for sensor."""
sensor_name = "modbus_test_sensor"
- scan_interval = 5
- entity_id, now, device = await setup_base_test(
- sensor_name,
+ state = await base_test(
hass,
- mock_hub,
- {
- CONF_REGISTERS: [
- dict(**{CONF_NAME: sensor_name, CONF_REGISTER: 1234}, **cfg)
- ]
- },
+ {CONF_NAME: sensor_name, CONF_ADDRESS: 1234, **cfg},
+ sensor_name,
SENSOR_DOMAIN,
- scan_interval,
- )
- await run_base_read_test(
- entity_id,
- hass,
- mock_hub,
- cfg.get(CONF_REGISTER_TYPE),
+ CONF_SENSORS,
+ CONF_REGISTERS,
regs,
expected,
- now + timedelta(seconds=scan_interval + 1),
+ method_discovery=True,
+ scan_interval=5,
)
+ assert state == expected
diff --git a/tests/components/modbus/test_modbus_switch.py b/tests/components/modbus/test_modbus_switch.py
index da2ff953660123..91ab5bf97df8a3 100644
--- a/tests/components/modbus/test_modbus_switch.py
+++ b/tests/components/modbus/test_modbus_switch.py
@@ -1,26 +1,174 @@
"""The tests for the Modbus switch component."""
-from datetime import timedelta
-
import pytest
from homeassistant.components.modbus.const import (
CALL_TYPE_COIL,
CALL_TYPE_REGISTER_HOLDING,
+ CALL_TYPE_REGISTER_INPUT,
CONF_COILS,
+ CONF_INPUT_TYPE,
CONF_REGISTER,
+ CONF_REGISTER_TYPE,
CONF_REGISTERS,
+ CONF_STATE_OFF,
+ CONF_STATE_ON,
+ CONF_VERIFY_REGISTER,
+ CONF_VERIFY_STATE,
)
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import (
+ CONF_ADDRESS,
CONF_COMMAND_OFF,
CONF_COMMAND_ON,
+ CONF_DEVICE_CLASS,
CONF_NAME,
CONF_SLAVE,
+ CONF_SWITCHES,
STATE_OFF,
STATE_ON,
)
-from .conftest import run_base_read_test, setup_base_test
+from .conftest import base_config_test, base_test
+
+
+@pytest.mark.parametrize(
+ "array_type, do_config",
+ [
+ (
+ None,
+ {
+ CONF_ADDRESS: 1234,
+ },
+ ),
+ (
+ None,
+ {
+ CONF_ADDRESS: 1234,
+ CONF_INPUT_TYPE: CALL_TYPE_COIL,
+ },
+ ),
+ (
+ None,
+ {
+ CONF_ADDRESS: 1234,
+ CONF_SLAVE: 1,
+ CONF_STATE_OFF: 0,
+ CONF_STATE_ON: 1,
+ CONF_VERIFY_REGISTER: 1235,
+ CONF_VERIFY_STATE: False,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_DEVICE_CLASS: "switch",
+ CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
+ },
+ ),
+ (
+ None,
+ {
+ CONF_ADDRESS: 1234,
+ CONF_SLAVE: 1,
+ CONF_STATE_OFF: 0,
+ CONF_STATE_ON: 1,
+ CONF_VERIFY_REGISTER: 1235,
+ CONF_VERIFY_STATE: True,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_DEVICE_CLASS: "switch",
+ CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT,
+ },
+ ),
+ (
+ None,
+ {
+ CONF_ADDRESS: 1234,
+ CONF_INPUT_TYPE: CALL_TYPE_COIL,
+ CONF_SLAVE: 1,
+ CONF_DEVICE_CLASS: "switch",
+ CONF_INPUT_TYPE: CALL_TYPE_COIL,
+ },
+ ),
+ (
+ CONF_REGISTERS,
+ {
+ CONF_REGISTER: 1234,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ },
+ ),
+ (
+ CONF_REGISTERS,
+ {
+ CONF_REGISTER: 1234,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ },
+ ),
+ (
+ CONF_COILS,
+ {
+ CALL_TYPE_COIL: 1234,
+ CONF_SLAVE: 1,
+ },
+ ),
+ (
+ CONF_REGISTERS,
+ {
+ CONF_REGISTER: 1234,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_SLAVE: 1,
+ CONF_STATE_OFF: 0,
+ CONF_STATE_ON: 1,
+ CONF_VERIFY_REGISTER: 1235,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_VERIFY_STATE: True,
+ CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_INPUT,
+ },
+ ),
+ (
+ CONF_REGISTERS,
+ {
+ CONF_REGISTER: 1234,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_SLAVE: 1,
+ CONF_STATE_OFF: 0,
+ CONF_STATE_ON: 1,
+ CONF_VERIFY_REGISTER: 1235,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
+ CONF_VERIFY_STATE: True,
+ CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
+ },
+ ),
+ (
+ CONF_COILS,
+ {
+ CALL_TYPE_COIL: 1234,
+ CONF_SLAVE: 1,
+ },
+ ),
+ ],
+)
+async def test_config_switch(hass, array_type, do_config):
+ """Run test for switch."""
+ device_name = "test_switch"
+
+ device_config = {
+ CONF_NAME: device_name,
+ **do_config,
+ }
+
+ await base_config_test(
+ hass,
+ device_config,
+ device_name,
+ SWITCH_DOMAIN,
+ CONF_SWITCHES,
+ array_type,
+ method_discovery=(array_type is None),
+ )
@pytest.mark.parametrize(
@@ -48,32 +196,26 @@
),
],
)
-async def test_coil_switch(hass, mock_hub, regs, expected):
+async def test_coil_switch(hass, regs, expected):
"""Run test for given config."""
switch_name = "modbus_test_switch"
- scan_interval = 5
- entity_id, now, device = await setup_base_test(
- switch_name,
+ state = await base_test(
hass,
- mock_hub,
{
- CONF_COILS: [
- {CONF_NAME: switch_name, CALL_TYPE_COIL: 1234, CONF_SLAVE: 1},
- ]
+ CONF_NAME: switch_name,
+ CONF_ADDRESS: 1234,
+ CONF_INPUT_TYPE: CALL_TYPE_COIL,
},
+ switch_name,
SWITCH_DOMAIN,
- scan_interval,
- )
-
- await run_base_read_test(
- entity_id,
- hass,
- mock_hub,
- CALL_TYPE_COIL,
+ CONF_SWITCHES,
+ CONF_COILS,
regs,
expected,
- now + timedelta(seconds=scan_interval + 1),
+ method_discovery=True,
+ scan_interval=5,
)
+ assert state == expected
@pytest.mark.parametrize(
@@ -101,38 +243,28 @@ async def test_coil_switch(hass, mock_hub, regs, expected):
),
],
)
-async def test_register_switch(hass, mock_hub, regs, expected):
+async def test_register_switch(hass, regs, expected):
"""Run test for given config."""
switch_name = "modbus_test_switch"
- scan_interval = 5
- entity_id, now, device = await setup_base_test(
- switch_name,
+ state = await base_test(
hass,
- mock_hub,
{
- CONF_REGISTERS: [
- {
- CONF_NAME: switch_name,
- CONF_REGISTER: 1234,
- CONF_SLAVE: 1,
- CONF_COMMAND_OFF: 0x00,
- CONF_COMMAND_ON: 0x01,
- },
- ]
+ CONF_NAME: switch_name,
+ CONF_REGISTER: 1234,
+ CONF_SLAVE: 1,
+ CONF_COMMAND_OFF: 0x00,
+ CONF_COMMAND_ON: 0x01,
},
+ switch_name,
SWITCH_DOMAIN,
- scan_interval,
- )
-
- await run_base_read_test(
- entity_id,
- hass,
- mock_hub,
- CALL_TYPE_REGISTER_HOLDING,
+ CONF_SWITCHES,
+ CONF_REGISTERS,
regs,
expected,
- now + timedelta(seconds=scan_interval + 1),
+ method_discovery=False,
+ scan_interval=5,
)
+ assert state == expected
@pytest.mark.parametrize(
@@ -152,35 +284,25 @@ async def test_register_switch(hass, mock_hub, regs, expected):
),
],
)
-async def test_register_state_switch(hass, mock_hub, regs, expected):
+async def test_register_state_switch(hass, regs, expected):
"""Run test for given config."""
switch_name = "modbus_test_switch"
- scan_interval = 5
- entity_id, now, device = await setup_base_test(
- switch_name,
+ state = await base_test(
hass,
- mock_hub,
{
- CONF_REGISTERS: [
- {
- CONF_NAME: switch_name,
- CONF_REGISTER: 1234,
- CONF_SLAVE: 1,
- CONF_COMMAND_OFF: 0x04,
- CONF_COMMAND_ON: 0x40,
- },
- ]
+ CONF_NAME: switch_name,
+ CONF_REGISTER: 1234,
+ CONF_SLAVE: 1,
+ CONF_COMMAND_OFF: 0x04,
+ CONF_COMMAND_ON: 0x40,
},
+ switch_name,
SWITCH_DOMAIN,
- scan_interval,
- )
-
- await run_base_read_test(
- entity_id,
- hass,
- mock_hub,
- CALL_TYPE_REGISTER_HOLDING,
+ CONF_SWITCHES,
+ CONF_REGISTERS,
regs,
expected,
- now + timedelta(seconds=scan_interval + 1),
+ method_discovery=False,
+ scan_interval=5,
)
+ assert state == expected
diff --git a/tests/components/mold_indicator/test_sensor.py b/tests/components/mold_indicator/test_sensor.py
index 2496bddc57e034..04305b724d86ea 100644
--- a/tests/components/mold_indicator/test_sensor.py
+++ b/tests/components/mold_indicator/test_sensor.py
@@ -47,7 +47,7 @@ async def test_setup(hass):
await hass.async_block_till_done()
moldind = hass.states.get("sensor.mold_indicator")
assert moldind
- assert PERCENTAGE == moldind.attributes.get("unit_of_measurement")
+ assert moldind.attributes.get("unit_of_measurement") == PERCENTAGE
async def test_invalidcalib(hass):
diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py
index d7b505b5279b32..9d3bbd40a4640c 100644
--- a/tests/components/monoprice/test_media_player.py
+++ b/tests/components/monoprice/test_media_player.py
@@ -33,6 +33,7 @@
SERVICE_VOLUME_SET,
SERVICE_VOLUME_UP,
)
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_component import async_update_entity
from tests.common import MockConfigEntry
@@ -497,7 +498,7 @@ async def test_first_run_with_available_zones(hass):
monoprice = MockMonoprice()
await _setup_monoprice(hass, monoprice)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get(ZONE_7_ID)
assert not entry.disabled
@@ -510,7 +511,7 @@ async def test_first_run_with_failing_zones(hass):
with patch.object(MockMonoprice, "zone_status", side_effect=SerialException):
await _setup_monoprice(hass, monoprice)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get(ZONE_1_ID)
assert not entry.disabled
@@ -527,7 +528,7 @@ async def test_not_first_run_with_failing_zone(hass):
with patch.object(MockMonoprice, "zone_status", side_effect=SerialException):
await _setup_monoprice_not_first_run(hass, monoprice)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get(ZONE_1_ID)
assert not entry.disabled
diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py
index 5f0acc3e5c2539..6f70a4dbadb00c 100644
--- a/tests/components/mqtt/conftest.py
+++ b/tests/components/mqtt/conftest.py
@@ -1,2 +1,2 @@
"""Test fixtures for mqtt component."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py
index c8cd80372c6aaa..3d58cf834e92d7 100644
--- a/tests/components/mqtt/test_common.py
+++ b/tests/components/mqtt/test_common.py
@@ -8,6 +8,7 @@
from homeassistant.components.mqtt import debug_info
from homeassistant.components.mqtt.const import MQTT_DISCONNECTED
from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component
@@ -19,6 +20,7 @@
"name": "Beer",
"model": "Glass",
"sw_version": "0.1-beta",
+ "suggested_area": "default_area",
}
DEFAULT_CONFIG_DEVICE_INFO_MAC = {
@@ -27,6 +29,7 @@
"name": "Beer",
"model": "Glass",
"sw_version": "0.1-beta",
+ "suggested_area": "default_area",
}
@@ -725,7 +728,7 @@ async def help_test_entity_device_info_with_identifier(hass, mqtt_mock, domain,
config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID)
config["unique_id"] = "veryunique"
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
data = json.dumps(config)
async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data)
@@ -738,6 +741,7 @@ async def help_test_entity_device_info_with_identifier(hass, mqtt_mock, domain,
assert device.name == "Beer"
assert device.model == "Glass"
assert device.sw_version == "0.1-beta"
+ assert device.suggested_area == "default_area"
async def help_test_entity_device_info_with_connection(hass, mqtt_mock, domain, config):
@@ -750,7 +754,7 @@ async def help_test_entity_device_info_with_connection(hass, mqtt_mock, domain,
config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_MAC)
config["unique_id"] = "veryunique"
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
data = json.dumps(config)
async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data)
@@ -763,6 +767,7 @@ async def help_test_entity_device_info_with_connection(hass, mqtt_mock, domain,
assert device.name == "Beer"
assert device.model == "Glass"
assert device.sw_version == "0.1-beta"
+ assert device.suggested_area == "default_area"
async def help_test_entity_device_info_remove(hass, mqtt_mock, domain, config):
@@ -772,8 +777,8 @@ async def help_test_entity_device_info_remove(hass, mqtt_mock, domain, config):
config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID)
config["unique_id"] = "veryunique"
- dev_registry = await hass.helpers.device_registry.async_get_registry()
- ent_registry = await hass.helpers.entity_registry.async_get_registry()
+ dev_registry = dr.async_get(hass)
+ ent_registry = er.async_get(hass)
data = json.dumps(config)
async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data)
@@ -801,7 +806,7 @@ async def help_test_entity_device_info_update(hass, mqtt_mock, domain, config):
config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID)
config["unique_id"] = "veryunique"
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
data = json.dumps(config)
async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data)
@@ -913,7 +918,7 @@ async def help_test_entity_debug_info(hass, mqtt_mock, domain, config):
config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID)
config["unique_id"] = "veryunique"
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
data = json.dumps(config)
async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data)
@@ -946,7 +951,7 @@ async def help_test_entity_debug_info_max_messages(hass, mqtt_mock, domain, conf
config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID)
config["unique_id"] = "veryunique"
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
data = json.dumps(config)
async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data)
@@ -1008,7 +1013,7 @@ async def help_test_entity_debug_info_message(
if payload is None:
payload = "ON"
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
data = json.dumps(config)
async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data)
@@ -1054,7 +1059,7 @@ async def help_test_entity_debug_info_remove(hass, mqtt_mock, domain, config):
config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID)
config["unique_id"] = "veryunique"
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
data = json.dumps(config)
async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data)
@@ -1097,7 +1102,7 @@ async def help_test_entity_debug_info_update_entity_id(hass, mqtt_mock, domain,
config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID)
config["unique_id"] = "veryunique"
- dev_registry = await hass.helpers.device_registry.async_get_registry()
+ dev_registry = dr.async_get(hass)
ent_registry = mock_registry(hass, {})
data = json.dumps(config)
@@ -1141,3 +1146,39 @@ async def help_test_entity_debug_info_update_entity_id(hass, mqtt_mock, domain,
assert (
f"{domain}.test" not in hass.data[debug_info.DATA_MQTT_DEBUG_INFO]["entities"]
)
+
+
+async def help_test_entity_disabled_by_default(hass, mqtt_mock, domain, config):
+ """Test device registry remove."""
+ # Add device settings to config
+ config = copy.deepcopy(config[domain])
+ config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID)
+ config["enabled_by_default"] = False
+ config["unique_id"] = "veryunique1"
+
+ dev_registry = dr.async_get(hass)
+ ent_registry = er.async_get(hass)
+
+ # Discover a disabled entity
+ data = json.dumps(config)
+ async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla1/config", data)
+ await hass.async_block_till_done()
+ entity_id = ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique1")
+ assert not hass.states.get(entity_id)
+ assert dev_registry.async_get_device({("mqtt", "helloworld")})
+
+ # Discover an enabled entity, tied to the same device
+ config["enabled_by_default"] = True
+ config["unique_id"] = "veryunique2"
+ data = json.dumps(config)
+ async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla2/config", data)
+ await hass.async_block_till_done()
+ entity_id = ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique2")
+ assert hass.states.get(entity_id)
+
+ # Remove the enabled entity, both entities and the device should be removed
+ async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla2/config", "")
+ await hass.async_block_till_done()
+ assert not ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique1")
+ assert not ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique2")
+ assert not dev_registry.async_get_device({("mqtt", "helloworld")})
diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py
index 019f0e19911721..e28cd697457a25 100644
--- a/tests/components/mqtt/test_cover.py
+++ b/tests/components/mqtt/test_cover.py
@@ -333,7 +333,7 @@ async def test_optimistic_state_change(hass, mqtt_mock):
mqtt_mock.async_publish.assert_called_once_with("command-topic", "CLOSE", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("cover.test")
- assert STATE_CLOSED == state.state
+ assert state.state == STATE_CLOSED
await hass.services.async_call(
cover.DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: "cover.test"}, blocking=True
@@ -342,7 +342,7 @@ async def test_optimistic_state_change(hass, mqtt_mock):
mqtt_mock.async_publish.assert_called_once_with("command-topic", "OPEN", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("cover.test")
- assert STATE_OPEN == state.state
+ assert state.state == STATE_OPEN
await hass.services.async_call(
cover.DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: "cover.test"}, blocking=True
@@ -393,7 +393,7 @@ async def test_optimistic_state_change_with_position(hass, mqtt_mock):
mqtt_mock.async_publish.assert_called_once_with("command-topic", "CLOSE", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("cover.test")
- assert STATE_CLOSED == state.state
+ assert state.state == STATE_CLOSED
assert state.attributes.get(ATTR_CURRENT_POSITION) == 0
await hass.services.async_call(
@@ -403,7 +403,7 @@ async def test_optimistic_state_change_with_position(hass, mqtt_mock):
mqtt_mock.async_publish.assert_called_once_with("command-topic", "OPEN", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("cover.test")
- assert STATE_OPEN == state.state
+ assert state.state == STATE_OPEN
assert state.attributes.get(ATTR_CURRENT_POSITION) == 100
await hass.services.async_call(
@@ -525,9 +525,9 @@ async def test_current_cover_position(hass, mqtt_mock):
await hass.async_block_till_done()
state_attributes_dict = hass.states.get("cover.test").attributes
- assert not (ATTR_CURRENT_POSITION in state_attributes_dict)
- assert not (ATTR_CURRENT_TILT_POSITION in state_attributes_dict)
- assert not (4 & hass.states.get("cover.test").attributes["supported_features"] == 4)
+ assert ATTR_CURRENT_POSITION not in state_attributes_dict
+ assert ATTR_CURRENT_TILT_POSITION not in state_attributes_dict
+ assert 4 & hass.states.get("cover.test").attributes["supported_features"] != 4
async_fire_mqtt_message(hass, "get-position-topic", "0")
current_cover_position = hass.states.get("cover.test").attributes[
@@ -576,9 +576,9 @@ async def test_current_cover_position_inverted(hass, mqtt_mock):
await hass.async_block_till_done()
state_attributes_dict = hass.states.get("cover.test").attributes
- assert not (ATTR_CURRENT_POSITION in state_attributes_dict)
- assert not (ATTR_CURRENT_TILT_POSITION in state_attributes_dict)
- assert not (4 & hass.states.get("cover.test").attributes["supported_features"] == 4)
+ assert ATTR_CURRENT_POSITION not in state_attributes_dict
+ assert ATTR_CURRENT_TILT_POSITION not in state_attributes_dict
+ assert 4 & hass.states.get("cover.test").attributes["supported_features"] != 4
async_fire_mqtt_message(hass, "get-position-topic", "100")
current_percentage_cover_position = hass.states.get("cover.test").attributes[
@@ -659,14 +659,14 @@ async def test_position_update(hass, mqtt_mock):
await hass.async_block_till_done()
state_attributes_dict = hass.states.get("cover.test").attributes
- assert not (ATTR_CURRENT_POSITION in state_attributes_dict)
- assert not (ATTR_CURRENT_TILT_POSITION in state_attributes_dict)
+ assert ATTR_CURRENT_POSITION not in state_attributes_dict
+ assert ATTR_CURRENT_TILT_POSITION not in state_attributes_dict
assert 4 & hass.states.get("cover.test").attributes["supported_features"] == 4
async_fire_mqtt_message(hass, "get-position-topic", "22")
state_attributes_dict = hass.states.get("cover.test").attributes
assert ATTR_CURRENT_POSITION in state_attributes_dict
- assert not (ATTR_CURRENT_TILT_POSITION in state_attributes_dict)
+ assert ATTR_CURRENT_TILT_POSITION not in state_attributes_dict
current_cover_position = hass.states.get("cover.test").attributes[
ATTR_CURRENT_POSITION
]
@@ -1082,6 +1082,23 @@ async def test_tilt_given_value_optimistic(hass, mqtt_mock):
)
mqtt_mock.async_publish.reset_mock()
+ await hass.services.async_call(
+ cover.DOMAIN,
+ SERVICE_SET_COVER_TILT_POSITION,
+ {ATTR_ENTITY_ID: "cover.test", ATTR_TILT_POSITION: 50},
+ blocking=True,
+ )
+
+ current_cover_tilt_position = hass.states.get("cover.test").attributes[
+ ATTR_CURRENT_TILT_POSITION
+ ]
+ assert current_cover_tilt_position == 50
+
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tilt-command-topic", "50", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
await hass.services.async_call(
cover.DOMAIN,
SERVICE_CLOSE_COVER_TILT,
@@ -1298,6 +1315,112 @@ async def test_tilt_via_topic_altered_range(hass, mqtt_mock):
assert current_cover_tilt_position == 50
+async def test_tilt_status_out_of_range_warning(hass, caplog, mqtt_mock):
+ """Test tilt status via MQTT tilt out of range warning message."""
+ assert await async_setup_component(
+ hass,
+ cover.DOMAIN,
+ {
+ cover.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "state_topic": "state-topic",
+ "command_topic": "command-topic",
+ "qos": 0,
+ "payload_open": "OPEN",
+ "payload_close": "CLOSE",
+ "payload_stop": "STOP",
+ "tilt_command_topic": "tilt-command-topic",
+ "tilt_status_topic": "tilt-status-topic",
+ "tilt_min": 0,
+ "tilt_max": 50,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "tilt-status-topic", "60")
+
+ assert (
+ "Payload '60' is out of range, must be between '0' and '50' inclusive"
+ ) in caplog.text
+
+
+async def test_tilt_status_not_numeric_warning(hass, caplog, mqtt_mock):
+ """Test tilt status via MQTT tilt not numeric warning message."""
+ assert await async_setup_component(
+ hass,
+ cover.DOMAIN,
+ {
+ cover.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "state_topic": "state-topic",
+ "command_topic": "command-topic",
+ "qos": 0,
+ "payload_open": "OPEN",
+ "payload_close": "CLOSE",
+ "payload_stop": "STOP",
+ "tilt_command_topic": "tilt-command-topic",
+ "tilt_status_topic": "tilt-status-topic",
+ "tilt_min": 0,
+ "tilt_max": 50,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "tilt-status-topic", "abc")
+
+ assert ("Payload 'abc' is not numeric") in caplog.text
+
+
+async def test_tilt_via_topic_altered_range_inverted(hass, mqtt_mock):
+ """Test tilt status via MQTT with altered tilt range and inverted tilt position."""
+ assert await async_setup_component(
+ hass,
+ cover.DOMAIN,
+ {
+ cover.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "state_topic": "state-topic",
+ "command_topic": "command-topic",
+ "qos": 0,
+ "payload_open": "OPEN",
+ "payload_close": "CLOSE",
+ "payload_stop": "STOP",
+ "tilt_command_topic": "tilt-command-topic",
+ "tilt_status_topic": "tilt-status-topic",
+ "tilt_min": 50,
+ "tilt_max": 0,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "tilt-status-topic", "0")
+
+ current_cover_tilt_position = hass.states.get("cover.test").attributes[
+ ATTR_CURRENT_TILT_POSITION
+ ]
+ assert current_cover_tilt_position == 100
+
+ async_fire_mqtt_message(hass, "tilt-status-topic", "50")
+
+ current_cover_tilt_position = hass.states.get("cover.test").attributes[
+ ATTR_CURRENT_TILT_POSITION
+ ]
+ assert current_cover_tilt_position == 0
+
+ async_fire_mqtt_message(hass, "tilt-status-topic", "25")
+
+ current_cover_tilt_position = hass.states.get("cover.test").attributes[
+ ATTR_CURRENT_TILT_POSITION
+ ]
+ assert current_cover_tilt_position == 50
+
+
async def test_tilt_via_topic_template_altered_range(hass, mqtt_mock):
"""Test tilt status via MQTT and template with altered tilt range."""
assert await async_setup_component(
@@ -1381,6 +1504,41 @@ async def test_tilt_position(hass, mqtt_mock):
)
+async def test_tilt_position_templated(hass, mqtt_mock):
+ """Test tilt position via template."""
+ assert await async_setup_component(
+ hass,
+ cover.DOMAIN,
+ {
+ cover.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "state_topic": "state-topic",
+ "command_topic": "command-topic",
+ "qos": 0,
+ "payload_open": "OPEN",
+ "payload_close": "CLOSE",
+ "payload_stop": "STOP",
+ "tilt_command_topic": "tilt-command-topic",
+ "tilt_status_topic": "tilt-status-topic",
+ "tilt_command_template": "{{100-32}}",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ await hass.services.async_call(
+ cover.DOMAIN,
+ SERVICE_SET_COVER_TILT_POSITION,
+ {ATTR_ENTITY_ID: "cover.test", ATTR_TILT_POSITION: 100},
+ blocking=True,
+ )
+
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tilt-command-topic", "68", 0, False
+ )
+
+
async def test_tilt_position_altered_range(hass, mqtt_mock):
"""Test tilt via method invocation with altered range."""
assert await async_setup_component(
@@ -1978,3 +2136,297 @@ async def test_entity_debug_info_message(hass, mqtt_mock):
await help_test_entity_debug_info_message(
hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG
)
+
+
+async def test_deprecated_value_template_for_position_topic_warning(
+ hass, caplog, mqtt_mock
+):
+ """Test warning when value_template is used for position_topic."""
+ assert await async_setup_component(
+ hass,
+ cover.DOMAIN,
+ {
+ cover.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "command_topic": "command-topic",
+ "set_position_topic": "set-position-topic",
+ "position_topic": "position-topic",
+ "value_template": "{{100-62}}",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert (
+ "Using 'value_template' for 'position_topic' is deprecated "
+ "and will be removed from Home Assistant in version 2021.6, "
+ "please replace it with 'position_template'"
+ ) in caplog.text
+
+
+async def test_deprecated_tilt_invert_state_warning(hass, caplog, mqtt_mock):
+ """Test warning when tilt_invert_state is used."""
+ assert await async_setup_component(
+ hass,
+ cover.DOMAIN,
+ {
+ cover.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "command_topic": "command-topic",
+ "tilt_invert_state": True,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert (
+ "'tilt_invert_state' is deprecated "
+ "and will be removed from Home Assistant in version 2021.6, "
+ "please invert tilt using 'tilt_min' & 'tilt_max'"
+ ) in caplog.text
+
+
+async def test_no_deprecated_tilt_invert_state_warning(hass, caplog, mqtt_mock):
+ """Test warning when tilt_invert_state is used."""
+ assert await async_setup_component(
+ hass,
+ cover.DOMAIN,
+ {
+ cover.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "command_topic": "command-topic",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert (
+ "'tilt_invert_state' is deprecated "
+ "and will be removed from Home Assistant in version 2021.6, "
+ "please invert tilt using 'tilt_min' & 'tilt_max'"
+ ) not in caplog.text
+
+
+async def test_no_deprecated_warning_for_position_topic_using_position_template(
+ hass, caplog, mqtt_mock
+):
+ """Test no warning when position_template is used for position_topic."""
+ assert await async_setup_component(
+ hass,
+ cover.DOMAIN,
+ {
+ cover.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "command_topic": "command-topic",
+ "set_position_topic": "set-position-topic",
+ "position_topic": "position-topic",
+ "position_template": "{{100-62}}",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert (
+ "using 'value_template' for 'position_topic' is deprecated "
+ "and will be removed from Home Assistant in version 2021.6, "
+ "please replace it with 'position_template'"
+ ) not in caplog.text
+
+
+async def test_state_and_position_topics_state_not_set_via_position_topic(
+ hass, mqtt_mock
+):
+ """Test state is not set via position topic when both state and position topics are set."""
+ assert await async_setup_component(
+ hass,
+ cover.DOMAIN,
+ {
+ cover.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "state_topic": "state-topic",
+ "position_topic": "get-position-topic",
+ "position_open": 100,
+ "position_closed": 0,
+ "state_open": "OPEN",
+ "state_closed": "CLOSE",
+ "command_topic": "command-topic",
+ "qos": 0,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_UNKNOWN
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ async_fire_mqtt_message(hass, "state-topic", "OPEN")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_OPEN
+
+ async_fire_mqtt_message(hass, "get-position-topic", "0")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_OPEN
+
+ async_fire_mqtt_message(hass, "get-position-topic", "100")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_OPEN
+
+ async_fire_mqtt_message(hass, "state-topic", "CLOSE")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_CLOSED
+
+ async_fire_mqtt_message(hass, "get-position-topic", "0")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_CLOSED
+
+ async_fire_mqtt_message(hass, "get-position-topic", "100")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_CLOSED
+
+
+async def test_set_state_via_position_using_stopped_state(hass, mqtt_mock):
+ """Test the controlling state via position topic using stopped state."""
+ assert await async_setup_component(
+ hass,
+ cover.DOMAIN,
+ {
+ cover.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "state_topic": "state-topic",
+ "position_topic": "get-position-topic",
+ "position_open": 100,
+ "position_closed": 0,
+ "state_open": "OPEN",
+ "state_closed": "CLOSE",
+ "state_stopped": "STOPPED",
+ "command_topic": "command-topic",
+ "qos": 0,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_UNKNOWN
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ async_fire_mqtt_message(hass, "state-topic", "OPEN")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_OPEN
+
+ async_fire_mqtt_message(hass, "get-position-topic", "0")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_OPEN
+
+ async_fire_mqtt_message(hass, "state-topic", "STOPPED")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_CLOSED
+
+ async_fire_mqtt_message(hass, "get-position-topic", "100")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_CLOSED
+
+ async_fire_mqtt_message(hass, "state-topic", "STOPPED")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_OPEN
+
+
+async def test_position_via_position_topic_template(hass, mqtt_mock):
+ """Test position by updating status via position template."""
+ assert await async_setup_component(
+ hass,
+ cover.DOMAIN,
+ {
+ cover.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "state_topic": "state-topic",
+ "command_topic": "command-topic",
+ "set_position_topic": "set-position-topic",
+ "position_topic": "get-position-topic",
+ "position_template": "{{ (value | multiply(0.01)) | int }}",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "get-position-topic", "99")
+
+ current_cover_position_position = hass.states.get("cover.test").attributes[
+ ATTR_CURRENT_POSITION
+ ]
+ assert current_cover_position_position == 0
+
+ async_fire_mqtt_message(hass, "get-position-topic", "5000")
+
+ current_cover_position_position = hass.states.get("cover.test").attributes[
+ ATTR_CURRENT_POSITION
+ ]
+ assert current_cover_position_position == 50
+
+
+async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock):
+ """Test the controlling state via stopped state when no position topic."""
+ assert await async_setup_component(
+ hass,
+ cover.DOMAIN,
+ {
+ cover.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "state_topic": "state-topic",
+ "state_open": "OPEN",
+ "state_closed": "CLOSE",
+ "state_stopped": "STOPPED",
+ "state_opening": "OPENING",
+ "state_closing": "CLOSING",
+ "command_topic": "command-topic",
+ "qos": 0,
+ "optimistic": False,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "state-topic", "OPEN")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_OPEN
+
+ async_fire_mqtt_message(hass, "state-topic", "OPENING")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_OPENING
+
+ async_fire_mqtt_message(hass, "state-topic", "STOPPED")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_OPEN
+
+ async_fire_mqtt_message(hass, "state-topic", "CLOSING")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_CLOSING
+
+ async_fire_mqtt_message(hass, "state-topic", "STOPPED")
+
+ state = hass.states.get("cover.test")
+ assert state.state == STATE_CLOSED
diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py
index 2c445ee0fa5189..f158a878fcdf90 100644
--- a/tests/components/mqtt/test_device_tracker_discovery.py
+++ b/tests/components/mqtt/test_device_tracker_discovery.py
@@ -194,6 +194,7 @@ async def test_cleanup_device_tracker(hass, device_reg, entity_reg, mqtt_mock):
device_reg.async_remove_device(device_entry.id)
await hass.async_block_till_done()
+ await hass.async_block_till_done()
# Verify device and registry entries are cleared
device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")})
diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py
index f200de6a27418f..62ffade11f4d26 100644
--- a/tests/components/mqtt/test_device_trigger.py
+++ b/tests/components/mqtt/test_device_trigger.py
@@ -6,6 +6,7 @@
import homeassistant.components.automation as automation
from homeassistant.components.mqtt import DOMAIN, debug_info
from homeassistant.components.mqtt.device_trigger import async_attach_trigger
+from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
from tests.common import (
@@ -16,7 +17,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
@@ -290,6 +291,81 @@ async def test_if_fires_on_mqtt_message(hass, device_reg, calls, mqtt_mock):
assert calls[1].data["some"] == "long_press"
+async def test_if_fires_on_mqtt_message_template(hass, device_reg, calls, mqtt_mock):
+ """Test triggers firing."""
+ data1 = (
+ '{ "automation_type":"trigger",'
+ ' "device":{"identifiers":["0AFFD2"]},'
+ " \"payload\": \"{{ 'foo_press'|regex_replace('foo', 'short') }}\","
+ ' "topic": "foobar/triggers/button{{ sqrt(16)|round }}",'
+ ' "type": "button_short_press",'
+ ' "subtype": "button_1",'
+ ' "value_template": "{{ value_json.button }}"}'
+ )
+ data2 = (
+ '{ "automation_type":"trigger",'
+ ' "device":{"identifiers":["0AFFD2"]},'
+ " \"payload\": \"{{ 'foo_press'|regex_replace('foo', 'long') }}\","
+ ' "topic": "foobar/triggers/button{{ sqrt(16)|round }}",'
+ ' "type": "button_long_press",'
+ ' "subtype": "button_2",'
+ ' "value_template": "{{ value_json.button }}"}'
+ )
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1)
+ async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", data2)
+ await hass.async_block_till_done()
+ device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")})
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device_entry.id,
+ "discovery_id": "bla1",
+ "type": "button_short_press",
+ "subtype": "button_1",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {"some": ("short_press")},
+ },
+ },
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device_entry.id,
+ "discovery_id": "bla2",
+ "type": "button_1",
+ "subtype": "button_long_press",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {"some": ("long_press")},
+ },
+ },
+ ]
+ },
+ )
+
+ # Fake short press.
+ async_fire_mqtt_message(hass, "foobar/triggers/button4", '{"button":"short_press"}')
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "short_press"
+
+ # Fake long press.
+ async_fire_mqtt_message(hass, "foobar/triggers/button4", '{"button":"long_press"}')
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[1].data["some"] == "long_press"
+
+
async def test_if_fires_on_mqtt_message_late_discover(
hass, device_reg, calls, mqtt_mock
):
@@ -761,7 +837,7 @@ def callback(trigger):
async def test_entity_device_info_with_connection(hass, mqtt_mock):
"""Test MQTT device registry integration."""
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
data = json.dumps(
{
@@ -792,7 +868,7 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock):
async def test_entity_device_info_with_identifier(hass, mqtt_mock):
"""Test MQTT device registry integration."""
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
data = json.dumps(
{
@@ -823,7 +899,7 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock):
async def test_entity_device_info_update(hass, mqtt_mock):
"""Test device registry update."""
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
config = {
"automation_type": "trigger",
@@ -1084,7 +1160,7 @@ async def test_trigger_debug_info(hass, mqtt_mock):
This is a test helper for MQTT debug_info.
"""
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
config = {
"platform": "mqtt",
diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py
index c9b0879d490974..d55c8e0eccc73b 100644
--- a/tests/components/mqtt/test_discovery.py
+++ b/tests/components/mqtt/test_discovery.py
@@ -411,6 +411,7 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock):
device_reg.async_remove_device(device_entry.id)
await hass.async_block_till_done()
+ await hass.async_block_till_done()
# Verify device and registry entries are cleared
device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")})
@@ -441,7 +442,8 @@ async def test_discovery_expansion(hass, mqtt_mock, caplog):
' "name":"DiscoveryExpansionTest1 Device",'
' "mdl":"Generic",'
' "sw":"1.2.3.4",'
- ' "mf":"None"'
+ ' "mf":"None",'
+ ' "sa":"default_area"'
" }"
"}"
)
diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py
index 045b8fdaf0ee5d..be32540b04df66 100644
--- a/tests/components/mqtt/test_fan.py
+++ b/tests/components/mqtt/test_fan.py
@@ -2,8 +2,10 @@
from unittest.mock import patch
import pytest
+from voluptuous.error import MultipleInvalid
from homeassistant.components import fan
+from homeassistant.components.fan import NotValidPresetModeError
from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_SUPPORTED_FEATURES,
@@ -58,7 +60,7 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock):
assert hass.states.get("fan.test") is None
-async def test_controlling_state_via_topic(hass, mqtt_mock):
+async def test_controlling_state_via_topic(hass, mqtt_mock, caplog):
"""Test the controlling state via topic."""
assert await async_setup_component(
hass,
@@ -73,10 +75,27 @@ async def test_controlling_state_via_topic(hass, mqtt_mock):
"payload_on": "StAtE_On",
"oscillation_state_topic": "oscillation-state-topic",
"oscillation_command_topic": "oscillation-command-topic",
- "payload_oscillation_off": "OsC_OfF",
- "payload_oscillation_on": "OsC_On",
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
"speed_state_topic": "speed-state-topic",
"speed_command_topic": "speed-command-topic",
+ "payload_oscillation_off": "OsC_OfF",
+ "payload_oscillation_on": "OsC_On",
+ "percentage_state_topic": "percentage-state-topic",
+ "percentage_command_topic": "percentage-command-topic",
+ "preset_mode_state_topic": "preset-mode-state-topic",
+ "preset_mode_command_topic": "preset-mode-command-topic",
+ "preset_modes": [
+ "medium",
+ "medium-high",
+ "high",
+ "very-high",
+ "freaking-high",
+ "silent",
+ ],
+ "speed_range_min": 1,
+ "speed_range_max": 200,
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ "speeds": ["off", "low"],
"payload_off_speed": "speed_OfF",
"payload_low_speed": "speed_lOw",
"payload_medium_speed": "speed_mEdium",
@@ -87,16 +106,16 @@ async def test_controlling_state_via_topic(hass, mqtt_mock):
await hass.async_block_till_done()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert not state.attributes.get(ATTR_ASSUMED_STATE)
async_fire_mqtt_message(hass, "state-topic", "StAtE_On")
state = hass.states.get("fan.test")
- assert state.state is STATE_ON
+ assert state.state == STATE_ON
async_fire_mqtt_message(hass, "state-topic", "StAtE_OfF")
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get("oscillating") is False
async_fire_mqtt_message(hass, "oscillation-state-topic", "OsC_On")
@@ -107,6 +126,51 @@ async def test_controlling_state_via_topic(hass, mqtt_mock):
state = hass.states.get("fan.test")
assert state.attributes.get("oscillating") is False
+ async_fire_mqtt_message(hass, "percentage-state-topic", "0")
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0
+
+ async_fire_mqtt_message(hass, "percentage-state-topic", "50")
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 25
+
+ async_fire_mqtt_message(hass, "percentage-state-topic", "100")
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 50
+
+ async_fire_mqtt_message(hass, "percentage-state-topic", "200")
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100
+
+ async_fire_mqtt_message(hass, "percentage-state-topic", "202")
+ assert "not a valid speed within the speed range" in caplog.text
+ caplog.clear()
+
+ async_fire_mqtt_message(hass, "percentage-state-topic", "invalid")
+ assert "not a valid speed within the speed range" in caplog.text
+ caplog.clear()
+
+ async_fire_mqtt_message(hass, "preset-mode-state-topic", "low")
+ state = hass.states.get("fan.test")
+ assert state.attributes.get("preset_mode") == "low"
+
+ async_fire_mqtt_message(hass, "preset-mode-state-topic", "medium")
+ state = hass.states.get("fan.test")
+ assert state.attributes.get("preset_mode") == "medium"
+
+ async_fire_mqtt_message(hass, "preset-mode-state-topic", "very-high")
+ state = hass.states.get("fan.test")
+ assert state.attributes.get("preset_mode") == "very-high"
+
+ async_fire_mqtt_message(hass, "preset-mode-state-topic", "silent")
+ state = hass.states.get("fan.test")
+ assert state.attributes.get("preset_mode") == "silent"
+
+ async_fire_mqtt_message(hass, "preset-mode-state-topic", "ModeUnknown")
+ assert "not a valid preset mode" in caplog.text
+ caplog.clear()
+
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
assert state.attributes.get("speed") == fan.SPEED_OFF
async_fire_mqtt_message(hass, "speed-state-topic", "speed_lOw")
@@ -114,20 +178,173 @@ async def test_controlling_state_via_topic(hass, mqtt_mock):
assert state.attributes.get("speed") == fan.SPEED_LOW
async_fire_mqtt_message(hass, "speed-state-topic", "speed_mEdium")
+ assert "not a valid speed" in caplog.text
+ caplog.clear()
+
+ async_fire_mqtt_message(hass, "speed-state-topic", "speed_High")
+ assert "not a valid speed" in caplog.text
+ caplog.clear()
+
+ async_fire_mqtt_message(hass, "speed-state-topic", "speed_OfF")
+ state = hass.states.get("fan.test")
+ assert state.attributes.get("speed") == fan.SPEED_OFF
+
+ async_fire_mqtt_message(hass, "speed-state-topic", "speed_very_high")
+ assert "not a valid speed" in caplog.text
+ caplog.clear()
+
+
+async def test_controlling_state_via_topic_with_different_speed_range(
+ hass, mqtt_mock, caplog
+):
+ """Test the controlling state via topic using an alternate speed range."""
+ assert await async_setup_component(
+ hass,
+ fan.DOMAIN,
+ {
+ fan.DOMAIN: [
+ {
+ "platform": "mqtt",
+ "name": "test1",
+ "command_topic": "command-topic",
+ "percentage_state_topic": "percentage-state-topic1",
+ "percentage_command_topic": "percentage-command-topic1",
+ "speed_range_min": 1,
+ "speed_range_max": 100,
+ },
+ {
+ "platform": "mqtt",
+ "name": "test2",
+ "command_topic": "command-topic",
+ "percentage_state_topic": "percentage-state-topic2",
+ "percentage_command_topic": "percentage-command-topic2",
+ "speed_range_min": 1,
+ "speed_range_max": 200,
+ },
+ {
+ "platform": "mqtt",
+ "name": "test3",
+ "command_topic": "command-topic",
+ "percentage_state_topic": "percentage-state-topic3",
+ "percentage_command_topic": "percentage-command-topic3",
+ "speed_range_min": 81,
+ "speed_range_max": 1023,
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "percentage-state-topic1", "100")
+ state = hass.states.get("fan.test1")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100
+
+ async_fire_mqtt_message(hass, "percentage-state-topic2", "100")
+ state = hass.states.get("fan.test2")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 50
+
+ async_fire_mqtt_message(hass, "percentage-state-topic3", "1023")
+ state = hass.states.get("fan.test3")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100
+ async_fire_mqtt_message(hass, "percentage-state-topic3", "80")
+ state = hass.states.get("fan.test3")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0
+
+ state = hass.states.get("fan.test3")
+ async_fire_mqtt_message(hass, "percentage-state-topic3", "79")
+ assert "not a valid speed within the speed range" in caplog.text
+ caplog.clear()
+
+
+async def test_controlling_state_via_topic_no_percentage_topics(hass, mqtt_mock):
+ """Test the controlling state via topic without percentage topics."""
+ assert await async_setup_component(
+ hass,
+ fan.DOMAIN,
+ {
+ fan.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "state_topic": "state-topic",
+ "command_topic": "command-topic",
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ "speed_state_topic": "speed-state-topic",
+ "speed_command_topic": "speed-command-topic",
+ "preset_mode_state_topic": "preset-mode-state-topic",
+ "preset_mode_command_topic": "preset-mode-command-topic",
+ "preset_modes": [
+ "high",
+ "freaking-high",
+ "silent",
+ ],
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ "speeds": ["off", "low", "medium"],
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ async_fire_mqtt_message(hass, "preset-mode-state-topic", "freaking-high")
+ state = hass.states.get("fan.test")
+ assert state.attributes.get("preset_mode") == "freaking-high"
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ assert state.attributes.get("speed") == fan.SPEED_OFF
+
+ async_fire_mqtt_message(hass, "preset-mode-state-topic", "high")
+ state = hass.states.get("fan.test")
+ assert state.attributes.get("preset_mode") == "high"
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 75
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ assert state.attributes.get("speed") == fan.SPEED_OFF
+
+ async_fire_mqtt_message(hass, "preset-mode-state-topic", "silent")
+ state = hass.states.get("fan.test")
+ assert state.attributes.get("preset_mode") == "silent"
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 75
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ assert state.attributes.get("speed") == fan.SPEED_OFF
+
+ async_fire_mqtt_message(hass, "preset-mode-state-topic", "medium")
+ state = hass.states.get("fan.test")
+ assert state.attributes.get("preset_mode") == "medium"
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 50
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ assert state.attributes.get("speed") == fan.SPEED_OFF
+
+ async_fire_mqtt_message(hass, "preset-mode-state-topic", "low")
+ state = hass.states.get("fan.test")
+ assert state.attributes.get("preset_mode") == "low"
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 25
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ assert state.attributes.get("speed") == fan.SPEED_OFF
+
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ async_fire_mqtt_message(hass, "speed-state-topic", "medium")
state = hass.states.get("fan.test")
+ assert state.attributes.get("preset_mode") == "low"
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 50
assert state.attributes.get("speed") == fan.SPEED_MEDIUM
- async_fire_mqtt_message(hass, "speed-state-topic", "speed_High")
+ async_fire_mqtt_message(hass, "speed-state-topic", "low")
state = hass.states.get("fan.test")
- assert state.attributes.get("speed") == fan.SPEED_HIGH
+ assert state.attributes.get("preset_mode") == "low"
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 25
+ assert state.attributes.get("speed") == fan.SPEED_LOW
- async_fire_mqtt_message(hass, "speed-state-topic", "speed_OfF")
+ async_fire_mqtt_message(hass, "speed-state-topic", "off")
state = hass.states.get("fan.test")
+ assert state.attributes.get("preset_mode") == "low"
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0
assert state.attributes.get("speed") == fan.SPEED_OFF
-async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock):
- """Test the controlling state via topic and JSON message."""
+async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, caplog):
+ """Test the controlling state via topic and JSON message (percentage mode)."""
assert await async_setup_component(
hass,
fan.DOMAIN,
@@ -139,27 +356,40 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock):
"command_topic": "command-topic",
"oscillation_state_topic": "oscillation-state-topic",
"oscillation_command_topic": "oscillation-command-topic",
- "speed_state_topic": "speed-state-topic",
- "speed_command_topic": "speed-command-topic",
+ "percentage_state_topic": "percentage-state-topic",
+ "percentage_command_topic": "percentage-command-topic",
+ "preset_mode_state_topic": "preset-mode-state-topic",
+ "preset_mode_command_topic": "preset-mode-command-topic",
+ "preset_modes": [
+ "medium",
+ "medium-high",
+ "high",
+ "very-high",
+ "freaking-high",
+ "silent",
+ ],
"state_value_template": "{{ value_json.val }}",
"oscillation_value_template": "{{ value_json.val }}",
- "speed_value_template": "{{ value_json.val }}",
+ "percentage_value_template": "{{ value_json.val }}",
+ "preset_mode_value_template": "{{ value_json.val }}",
+ "speed_range_min": 1,
+ "speed_range_max": 100,
}
},
)
await hass.async_block_till_done()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert not state.attributes.get(ATTR_ASSUMED_STATE)
async_fire_mqtt_message(hass, "state-topic", '{"val":"ON"}')
state = hass.states.get("fan.test")
- assert state.state is STATE_ON
+ assert state.state == STATE_ON
async_fire_mqtt_message(hass, "state-topic", '{"val":"OFF"}')
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get("oscillating") is False
async_fire_mqtt_message(hass, "oscillation-state-topic", '{"val":"oscillate_on"}')
@@ -170,27 +400,910 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock):
state = hass.states.get("fan.test")
assert state.attributes.get("oscillating") is False
- assert state.attributes.get("speed") == fan.SPEED_OFF
+ async_fire_mqtt_message(hass, "percentage-state-topic", '{"val": 1}')
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 1
- async_fire_mqtt_message(hass, "speed-state-topic", '{"val":"low"}')
+ async_fire_mqtt_message(hass, "percentage-state-topic", '{"val": 100}')
state = hass.states.get("fan.test")
- assert state.attributes.get("speed") == fan.SPEED_LOW
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100
+
+ async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "low"}')
+ assert "not a valid preset mode" in caplog.text
+ caplog.clear()
- async_fire_mqtt_message(hass, "speed-state-topic", '{"val":"medium"}')
+ async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "medium"}')
state = hass.states.get("fan.test")
- assert state.attributes.get("speed") == fan.SPEED_MEDIUM
+ assert state.attributes.get("preset_mode") == "medium"
+
+ async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "freaking-high"}')
+ state = hass.states.get("fan.test")
+ assert state.attributes.get("preset_mode") == "freaking-high"
+
+ async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "silent"}')
+ state = hass.states.get("fan.test")
+ assert state.attributes.get("preset_mode") == "silent"
+
+
+async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock):
+ """Test optimistic mode without state topic."""
+ assert await async_setup_component(
+ hass,
+ fan.DOMAIN,
+ {
+ fan.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "command_topic": "command-topic",
+ "payload_off": "StAtE_OfF",
+ "payload_on": "StAtE_On",
+ "oscillation_command_topic": "oscillation-command-topic",
+ "payload_oscillation_off": "OsC_OfF",
+ "payload_oscillation_on": "OsC_On",
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ "speed_command_topic": "speed-command-topic",
+ "percentage_command_topic": "percentage-command-topic",
+ "preset_mode_command_topic": "preset-mode-command-topic",
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ "speeds": ["off", "low", "medium"],
+ "preset_modes": [
+ "high",
+ "freaking-high",
+ "silent",
+ ],
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ "payload_off_speed": "speed_OfF",
+ "payload_low_speed": "speed_lOw",
+ "payload_medium_speed": "speed_mEdium",
+ "payload_high_speed": "speed_High",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_on(hass, "fan.test")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "command-topic", "StAtE_On", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_off(hass, "fan.test")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "command-topic", "StAtE_OfF", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_oscillate(hass, "fan.test", True)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "oscillation-command-topic", "OsC_On", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_oscillate(hass, "fan.test", False)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "oscillation-command-topic", "OsC_OfF", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+ with pytest.raises(MultipleInvalid):
+ await common.async_set_percentage(hass, "fan.test", -1)
+
+ with pytest.raises(MultipleInvalid):
+ await common.async_set_percentage(hass, "fan.test", 101)
+
+ await common.async_set_percentage(hass, "fan.test", 100)
+ assert mqtt_mock.async_publish.call_count == 2
+ mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "100", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "freaking-high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_percentage(hass, "fan.test", 0)
+ assert mqtt_mock.async_publish.call_count == 3
+ mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "0", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "off", 0, False
+ )
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ mqtt_mock.async_publish.assert_any_call(
+ "speed-command-topic", "speed_OfF", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ assert state.attributes.get(fan.ATTR_SPEED) == fan.SPEED_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "low")
+ assert mqtt_mock.async_publish.call_count == 2
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ mqtt_mock.async_publish.assert_any_call(
+ "speed-command-topic", "speed_lOw", 0, False
+ )
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "low", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) == "low"
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ assert state.attributes.get(fan.ATTR_SPEED) == fan.SPEED_LOW
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "medium")
+ assert mqtt_mock.async_publish.call_count == 2
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ mqtt_mock.async_publish.assert_any_call(
+ "speed-command-topic", "speed_mEdium", 0, False
+ )
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "medium", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) == "medium"
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ assert state.attributes.get(fan.ATTR_SPEED) == fan.SPEED_MEDIUM
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "high")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "preset-mode-command-topic", "high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) == "high"
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "freaking-high")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "preset-mode-command-topic", "freaking-high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) == "freaking-high"
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "silent")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "preset-mode-command-topic", "silent", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) == "silent"
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
- async_fire_mqtt_message(hass, "speed-state-topic", '{"val":"high"}')
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ await common.async_set_speed(hass, "fan.test", fan.SPEED_LOW)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "speed-command-topic", "speed_lOw", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ await common.async_set_speed(hass, "fan.test", fan.SPEED_MEDIUM)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "speed-command-topic", "speed_mEdium", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ await common.async_set_speed(hass, "fan.test", fan.SPEED_HIGH)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "speed-command-topic", "speed_High", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ await common.async_set_speed(hass, "fan.test", fan.SPEED_OFF)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "speed-command-topic", "speed_OfF", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.attributes.get("speed") == fan.SPEED_HIGH
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
- async_fire_mqtt_message(hass, "speed-state-topic", '{"val":"off"}')
+
+async def test_sending_mqtt_commands_with_alternate_speed_range(hass, mqtt_mock):
+ """Test the controlling state via topic using an alternate speed range."""
+ assert await async_setup_component(
+ hass,
+ fan.DOMAIN,
+ {
+ fan.DOMAIN: [
+ {
+ "platform": "mqtt",
+ "name": "test1",
+ "command_topic": "command-topic",
+ "percentage_state_topic": "percentage-state-topic1",
+ "percentage_command_topic": "percentage-command-topic1",
+ "speed_range_min": 1,
+ "speed_range_max": 100,
+ },
+ {
+ "platform": "mqtt",
+ "name": "test2",
+ "command_topic": "command-topic",
+ "percentage_state_topic": "percentage-state-topic2",
+ "percentage_command_topic": "percentage-command-topic2",
+ "speed_range_min": 1,
+ "speed_range_max": 200,
+ },
+ {
+ "platform": "mqtt",
+ "name": "test3",
+ "command_topic": "command-topic",
+ "percentage_state_topic": "percentage-state-topic3",
+ "percentage_command_topic": "percentage-command-topic3",
+ "speed_range_min": 81,
+ "speed_range_max": 1023,
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+
+ await common.async_set_percentage(hass, "fan.test1", 0)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "percentage-command-topic1", "0", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test1")
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_percentage(hass, "fan.test1", 100)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "percentage-command-topic1", "100", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test1")
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_percentage(hass, "fan.test2", 0)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "percentage-command-topic2", "0", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test2")
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_percentage(hass, "fan.test2", 100)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "percentage-command-topic2", "200", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test2")
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_percentage(hass, "fan.test3", 0)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "percentage-command-topic3", "80", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test3")
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_percentage(hass, "fan.test3", 100)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "percentage-command-topic3", "1023", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test3")
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+
+async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, caplog):
+ """Test optimistic mode without state topic without legacy speed command topic."""
+ assert await async_setup_component(
+ hass,
+ fan.DOMAIN,
+ {
+ fan.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "command_topic": "command-topic",
+ "percentage_command_topic": "percentage-command-topic",
+ "preset_mode_command_topic": "preset-mode-command-topic",
+ "preset_modes": [
+ "high",
+ "freaking-high",
+ "silent",
+ ],
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_on(hass, "fan.test")
+ mqtt_mock.async_publish.assert_called_once_with("command-topic", "ON", 0, False)
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_off(hass, "fan.test")
+ mqtt_mock.async_publish.assert_called_once_with("command-topic", "OFF", 0, False)
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ with pytest.raises(MultipleInvalid):
+ await common.async_set_percentage(hass, "fan.test", -1)
+
+ with pytest.raises(MultipleInvalid):
+ await common.async_set_percentage(hass, "fan.test", 101)
+
+ await common.async_set_percentage(hass, "fan.test", 100)
+ mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "100", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "freaking-high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) == "freaking-high"
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_percentage(hass, "fan.test", 0)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "percentage-command-topic", "0", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0
+ assert state.attributes.get(fan.ATTR_SPEED) is None
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "low")
+ assert "not a valid preset mode" in caplog.text
+ caplog.clear()
+
+ await common.async_set_preset_mode(hass, "fan.test", "medium")
+ assert "not a valid preset mode" in caplog.text
+ caplog.clear()
+
+ await common.async_set_preset_mode(hass, "fan.test", "high")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "preset-mode-command-topic", "high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) == "high"
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "freaking-high")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "preset-mode-command-topic", "freaking-high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) == "freaking-high"
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "silent")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "preset-mode-command-topic", "silent", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) == "silent"
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_on(hass, "fan.test", percentage=25)
+ assert mqtt_mock.async_publish.call_count == 3
+ mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False)
+ mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "25", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_off(hass, "fan.test")
+ mqtt_mock.async_publish.assert_any_call("command-topic", "OFF", 0, False)
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_on(hass, "fan.test", preset_mode="high")
+ assert mqtt_mock.async_publish.call_count == 2
+ mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ with pytest.raises(NotValidPresetModeError):
+ await common.async_turn_on(hass, "fan.test", preset_mode="low")
+
+
+async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog):
+ """Test optimistic mode without state topic without legacy speed command topic."""
+ assert await async_setup_component(
+ hass,
+ fan.DOMAIN,
+ {
+ fan.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "command_topic": "command-topic",
+ "command_template": "state: {{ value }}",
+ "oscillation_command_topic": "oscillation-command-topic",
+ "oscillation_command_template": "oscillation: {{ value }}",
+ "percentage_command_topic": "percentage-command-topic",
+ "percentage_command_template": "percentage: {{ value }}",
+ "preset_mode_command_topic": "preset-mode-command-topic",
+ "preset_mode_command_template": "preset_mode: {{ value }}",
+ "preset_modes": [
+ "high",
+ "freaking-high",
+ "silent",
+ ],
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_on(hass, "fan.test")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "command-topic", "state: ON", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_off(hass, "fan.test")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "command-topic", "state: OFF", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ with pytest.raises(MultipleInvalid):
+ await common.async_set_percentage(hass, "fan.test", -1)
+
+ with pytest.raises(MultipleInvalid):
+ await common.async_set_percentage(hass, "fan.test", 101)
+
+ await common.async_set_percentage(hass, "fan.test", 100)
+ mqtt_mock.async_publish.assert_any_call(
+ "percentage-command-topic", "percentage: 100", 0, False
+ )
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "preset_mode: freaking-high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) == "freaking-high"
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_percentage(hass, "fan.test", 0)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "percentage-command-topic", "percentage: 0", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0
+ assert state.attributes.get(fan.ATTR_SPEED) is None
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "low")
+ assert "not a valid preset mode" in caplog.text
+ caplog.clear()
+
+ await common.async_set_preset_mode(hass, "fan.test", "medium")
+ assert "not a valid preset mode" in caplog.text
+ caplog.clear()
+
+ await common.async_set_preset_mode(hass, "fan.test", "high")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "preset-mode-command-topic", "preset_mode: high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) == "high"
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "freaking-high")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "preset-mode-command-topic", "preset_mode: freaking-high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) == "freaking-high"
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "silent")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "preset-mode-command-topic", "preset_mode: silent", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) == "silent"
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_on(hass, "fan.test", percentage=25)
+ assert mqtt_mock.async_publish.call_count == 3
+ mqtt_mock.async_publish.assert_any_call("command-topic", "state: ON", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "percentage-command-topic", "percentage: 25", 0, False
+ )
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "preset_mode: high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_off(hass, "fan.test")
+ mqtt_mock.async_publish.assert_any_call("command-topic", "state: OFF", 0, False)
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_on(hass, "fan.test", preset_mode="high")
+ assert mqtt_mock.async_publish.call_count == 2
+ mqtt_mock.async_publish.assert_any_call("command-topic", "state: ON", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "preset_mode: high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ with pytest.raises(NotValidPresetModeError):
+ await common.async_turn_on(hass, "fan.test", preset_mode="low")
+
+
+async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic(
+ hass, mqtt_mock
+):
+ """Test optimistic mode without state topic without percentage command topic."""
+ assert await async_setup_component(
+ hass,
+ fan.DOMAIN,
+ {
+ fan.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "command_topic": "command-topic",
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ "speed_state_topic": "speed-state-topic",
+ "speed_command_topic": "speed-command-topic",
+ "preset_mode_command_topic": "preset-mode-command-topic",
+ "preset_mode_state_topic": "preset-mode-state-topic",
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ "speeds": ["off", "low", "medium"],
+ "preset_modes": [
+ "high",
+ "freaking-high",
+ "silent",
+ ],
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ with pytest.raises(MultipleInvalid):
+ await common.async_set_percentage(hass, "fan.test", -1)
+
+ with pytest.raises(MultipleInvalid):
+ await common.async_set_percentage(hass, "fan.test", 101)
+
+ await common.async_set_percentage(hass, "fan.test", 100)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "freaking-high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_percentage(hass, "fan.test", 0)
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "off", 0, False)
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "low")
+ assert mqtt_mock.async_publish.call_count == 2
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "low", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "low", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) is None
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "medium")
+ assert mqtt_mock.async_publish.call_count == 2
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "medium", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) is None
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "high")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "preset-mode-command-topic", "high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) is None
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "freaking-high")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "preset-mode-command-topic", "freaking-high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) is None
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "silent")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "preset-mode-command-topic", "silent", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PRESET_MODE) is None
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ await common.async_set_speed(hass, "fan.test", fan.SPEED_LOW)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "speed-command-topic", "low", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_speed(hass, "fan.test", fan.SPEED_MEDIUM)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "speed-command-topic", "medium", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_speed(hass, "fan.test", fan.SPEED_HIGH)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "speed-command-topic", "high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_speed(hass, "fan.test", fan.SPEED_OFF)
+
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "off", 0, False)
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_on(hass, "fan.test", speed="medium")
+ assert mqtt_mock.async_publish.call_count == 3
+ mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False)
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "medium", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_off(hass, "fan.test")
+ mqtt_mock.async_publish.assert_called_once_with("command-topic", "OFF", 0, False)
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ await common.async_turn_on(hass, "fan.test", speed="high")
+ assert mqtt_mock.async_publish.call_count == 2
+ mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_off(hass, "fan.test")
+ mqtt_mock.async_publish.assert_called_once_with("command-topic", "OFF", 0, False)
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+
+# use of speeds is deprecated, support will be removed after a quarter (2021.7)
+async def test_sending_mqtt_commands_and_optimistic_legacy_speeds_only(
+ hass, mqtt_mock, caplog
+):
+ """Test optimistic mode without state topics with legacy speeds."""
+ assert await async_setup_component(
+ hass,
+ fan.DOMAIN,
+ {
+ fan.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "command_topic": "command-topic",
+ "speed_state_topic": "speed-state-topic",
+ "speed_command_topic": "speed-command-topic",
+ "speeds": ["off", "low", "medium", "high"],
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_percentage(hass, "fan.test", 100)
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "high", 0, False)
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100
+ assert state.attributes.get(fan.ATTR_SPEED) == "off"
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_percentage(hass, "fan.test", 0)
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "off", 0, False)
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "low")
+ assert "not a valid preset mode" in caplog.text
+ caplog.clear()
+
+ await common.async_set_speed(hass, "fan.test", fan.SPEED_LOW)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "speed-command-topic", "low", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_speed(hass, "fan.test", fan.SPEED_MEDIUM)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "speed-command-topic", "medium", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_speed(hass, "fan.test", fan.SPEED_HIGH)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "speed-command-topic", "high", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_speed(hass, "fan.test", fan.SPEED_OFF)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "speed-command-topic", "off", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_on(hass, "fan.test", speed="medium")
+ assert mqtt_mock.async_publish.call_count == 2
+ mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False)
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False)
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_off(hass, "fan.test")
+ mqtt_mock.async_publish.assert_called_once_with("command-topic", "OFF", 0, False)
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_on(hass, "fan.test", speed="off")
+ assert mqtt_mock.async_publish.call_count == 2
+ mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False)
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "off", 0, False)
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_off(hass, "fan.test")
+ mqtt_mock.async_publish.assert_called_once_with("command-topic", "OFF", 0, False)
+ mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.attributes.get("speed") == fan.SPEED_OFF
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
-async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock):
- """Test optimistic mode without state topic."""
+async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, caplog):
+ """Test optimistic mode with state topic and turn on attributes."""
assert await async_setup_component(
hass,
fan.DOMAIN,
@@ -198,223 +1311,307 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock):
fan.DOMAIN: {
"platform": "mqtt",
"name": "test",
+ "state_topic": "state-topic",
"command_topic": "command-topic",
- "payload_off": "StAtE_OfF",
- "payload_on": "StAtE_On",
- "oscillation_command_topic": "oscillation-command-topic",
- "oscillation_state_topic": "oscillation-state-topic",
- "payload_oscillation_off": "OsC_OfF",
- "payload_oscillation_on": "OsC_On",
- "speed_command_topic": "speed-command-topic",
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
"speed_state_topic": "speed-state-topic",
- "payload_off_speed": "speed_OfF",
- "payload_low_speed": "speed_lOw",
- "payload_medium_speed": "speed_mEdium",
- "payload_high_speed": "speed_High",
+ "speed_command_topic": "speed-command-topic",
+ "oscillation_state_topic": "oscillation-state-topic",
+ "oscillation_command_topic": "oscillation-command-topic",
+ "percentage_state_topic": "percentage-state-topic",
+ "percentage_command_topic": "percentage-command-topic",
+ "preset_mode_command_topic": "preset-mode-command-topic",
+ "preset_mode_state_topic": "preset-mode-state-topic",
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ "speeds": ["off", "low", "medium"],
+ "preset_modes": [
+ "high",
+ "freaking-high",
+ "silent",
+ ],
+ "optimistic": True,
}
},
)
await hass.async_block_till_done()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
await common.async_turn_on(hass, "fan.test")
- mqtt_mock.async_publish.assert_called_once_with(
- "command-topic", "StAtE_On", 0, False
- )
+ mqtt_mock.async_publish.assert_called_once_with("command-topic", "ON", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_ON
+ assert state.state == STATE_ON
assert state.attributes.get(ATTR_ASSUMED_STATE)
await common.async_turn_off(hass, "fan.test")
- mqtt_mock.async_publish.assert_called_once_with(
- "command-topic", "StAtE_OfF", 0, False
- )
+ mqtt_mock.async_publish.assert_called_once_with("command-topic", "OFF", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
- await common.async_oscillate(hass, "fan.test", True)
- mqtt_mock.async_publish.assert_called_once_with(
- "oscillation-command-topic", "OsC_On", 0, False
+ await common.async_turn_on(hass, "fan.test", speed=fan.SPEED_MEDIUM)
+ assert mqtt_mock.async_publish.call_count == 3
+ mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False)
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "medium", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_ON
assert state.attributes.get(ATTR_ASSUMED_STATE)
- await common.async_oscillate(hass, "fan.test", False)
- mqtt_mock.async_publish.assert_called_once_with(
- "oscillation-command-topic", "OsC_OfF", 0, False
- )
+ await common.async_turn_off(hass, "fan.test")
+ mqtt_mock.async_publish.assert_called_once_with("command-topic", "OFF", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
- await common.async_set_speed(hass, "fan.test", fan.SPEED_LOW)
- mqtt_mock.async_publish.assert_called_once_with(
- "speed-command-topic", "speed_lOw", 0, False
+ await common.async_turn_on(hass, "fan.test", percentage=25)
+ assert mqtt_mock.async_publish.call_count == 4
+ mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "low", 0, False
)
+ mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "25", 0, False)
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "low", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_ON
assert state.attributes.get(ATTR_ASSUMED_STATE)
- await common.async_set_speed(hass, "fan.test", fan.SPEED_MEDIUM)
- mqtt_mock.async_publish.assert_called_once_with(
- "speed-command-topic", "speed_mEdium", 0, False
- )
+ await common.async_turn_off(hass, "fan.test")
+ mqtt_mock.async_publish.assert_any_call("command-topic", "OFF", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
- await common.async_set_speed(hass, "fan.test", fan.SPEED_HIGH)
- mqtt_mock.async_publish.assert_called_once_with(
- "speed-command-topic", "speed_High", 0, False
+ await common.async_turn_on(hass, "fan.test", preset_mode="medium")
+ assert mqtt_mock.async_publish.call_count == 3
+ mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "medium", 0, False
)
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_ON
assert state.attributes.get(ATTR_ASSUMED_STATE)
- await common.async_set_speed(hass, "fan.test", fan.SPEED_OFF)
- mqtt_mock.async_publish.assert_called_once_with(
- "speed-command-topic", "speed_OfF", 0, False
+ await common.async_turn_on(hass, "fan.test", preset_mode="high")
+ assert mqtt_mock.async_publish.call_count == 2
+ mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "high", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_ON
assert state.attributes.get(ATTR_ASSUMED_STATE)
+ await common.async_turn_off(hass, "fan.test")
+ mqtt_mock.async_publish.assert_any_call("command-topic", "OFF", 0, False)
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
-async def test_on_sending_mqtt_commands_and_optimistic(hass, mqtt_mock):
- """Test on with speed."""
- assert await async_setup_component(
- hass,
- fan.DOMAIN,
- {
- fan.DOMAIN: {
- "platform": "mqtt",
- "name": "test",
- "command_topic": "command-topic",
- "oscillation_command_topic": "oscillation-command-topic",
- "speed_command_topic": "speed-command-topic",
- }
- },
+ await common.async_turn_on(hass, "fan.test", preset_mode="silent")
+ assert mqtt_mock.async_publish.call_count == 2
+ mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "silent", 0, False
)
- await hass.async_block_till_done()
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+ await common.async_turn_off(hass, "fan.test")
+ mqtt_mock.async_publish.assert_called_once_with("command-topic", "OFF", 0, False)
+ mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
- await common.async_turn_on(hass, "fan.test")
- mqtt_mock.async_publish.assert_called_once_with("command-topic", "ON", 0, False)
+ await common.async_turn_on(hass, "fan.test", preset_mode="silent")
+ assert mqtt_mock.async_publish.call_count == 2
+ mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "silent", 0, False
+ )
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_ON
+ assert state.state == STATE_ON
assert state.attributes.get(ATTR_ASSUMED_STATE)
- assert state.attributes.get(fan.ATTR_SPEED) is None
- assert state.attributes.get(fan.ATTR_OSCILLATING) is None
await common.async_turn_off(hass, "fan.test")
mqtt_mock.async_publish.assert_called_once_with("command-topic", "OFF", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
- await common.async_turn_on(hass, "fan.test", speed="low")
- assert mqtt_mock.async_publish.call_count == 2
+ await common.async_oscillate(hass, "fan.test", True)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "oscillation-command-topic", "oscillate_on", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_turn_on(hass, "fan.test", percentage=50)
+ assert mqtt_mock.async_publish.call_count == 4
mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False)
- mqtt_mock.async_publish.assert_any_call("speed-command-topic", "low", 0, False)
+ mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "50", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "medium", 0, False
+ )
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_ON
+ assert state.state == STATE_ON
assert state.attributes.get(ATTR_ASSUMED_STATE)
- assert state.attributes.get(fan.ATTR_SPEED) == "low"
- assert state.attributes.get(fan.ATTR_OSCILLATING) is None
+ await common.async_turn_off(hass, "fan.test")
+ mqtt_mock.async_publish.assert_any_call("command-topic", "OFF", 0, False)
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
-async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock):
- """Test optimistic mode with state topic."""
- assert await async_setup_component(
- hass,
- fan.DOMAIN,
- {
- fan.DOMAIN: {
- "platform": "mqtt",
- "name": "test",
- "state_topic": "state-topic",
- "command_topic": "command-topic",
- "oscillation_state_topic": "oscillation-state-topic",
- "oscillation_command_topic": "oscillation-command-topic",
- "speed_state_topic": "speed-state-topic",
- "speed_command_topic": "speed-command-topic",
- "optimistic": True,
- }
- },
+ await common.async_oscillate(hass, "fan.test", False)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "oscillation-command-topic", "oscillate_off", 0, False
)
- await hass.async_block_till_done()
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+ await common.async_set_percentage(hass, "fan.test", 33)
+ assert mqtt_mock.async_publish.call_count == 3
+ mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "33", 0, False)
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "medium", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
- await common.async_turn_on(hass, "fan.test")
- mqtt_mock.async_publish.assert_called_once_with("command-topic", "ON", 0, False)
+ await common.async_set_percentage(hass, "fan.test", 50)
+ assert mqtt_mock.async_publish.call_count == 3
+ mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "50", 0, False)
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "medium", 0, False
+ )
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_ON
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
- await common.async_turn_off(hass, "fan.test")
- mqtt_mock.async_publish.assert_called_once_with("command-topic", "OFF", 0, False)
+ await common.async_set_percentage(hass, "fan.test", 100)
+ assert mqtt_mock.async_publish.call_count == 2
+ mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "100", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "freaking-high", 0, False
+ )
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
- await common.async_oscillate(hass, "fan.test", True)
- mqtt_mock.async_publish.assert_called_once_with(
- "oscillation-command-topic", "oscillate_on", 0, False
+ await common.async_set_percentage(hass, "fan.test", 0)
+ assert mqtt_mock.async_publish.call_count == 3
+ mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "0", 0, False)
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "off", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "off", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
- await common.async_oscillate(hass, "fan.test", False)
+ with pytest.raises(MultipleInvalid):
+ await common.async_set_percentage(hass, "fan.test", 101)
+
+ await common.async_set_preset_mode(hass, "fan.test", "low")
+ assert mqtt_mock.async_publish.call_count == 2
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "low", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "low", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "medium")
+ assert mqtt_mock.async_publish.call_count == 2
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False)
+ mqtt_mock.async_publish.assert_any_call(
+ "preset-mode-command-topic", "medium", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "high")
mqtt_mock.async_publish.assert_called_once_with(
- "oscillation-command-topic", "oscillate_off", 0, False
+ "preset-mode-command-topic", "high", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
- await common.async_set_speed(hass, "fan.test", fan.SPEED_LOW)
+ await common.async_set_preset_mode(hass, "fan.test", "silent")
mqtt_mock.async_publish.assert_called_once_with(
- "speed-command-topic", "low", 0, False
+ "preset-mode-command-topic", "silent", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ await common.async_set_preset_mode(hass, "fan.test", "ModeX")
+ assert "not a valid preset mode" in caplog.text
+ caplog.clear()
+
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("fan.test")
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
await common.async_set_speed(hass, "fan.test", fan.SPEED_MEDIUM)
mqtt_mock.async_publish.assert_called_once_with(
"speed-command-topic", "medium", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
await common.async_set_speed(hass, "fan.test", fan.SPEED_HIGH)
@@ -423,7 +1620,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock):
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
await common.async_set_speed(hass, "fan.test", fan.SPEED_OFF)
@@ -432,14 +1629,15 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock):
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
- with pytest.raises(ValueError):
- await common.async_set_speed(hass, "fan.test", "cUsToM")
+ await common.async_set_speed(hass, "fan.test", "cUsToM")
+ assert "not a valid speed" in caplog.text
+ caplog.clear()
-async def test_attributes(hass, mqtt_mock):
+async def test_attributes(hass, mqtt_mock, caplog):
"""Test attributes."""
assert await async_setup_component(
hass,
@@ -450,76 +1648,96 @@ async def test_attributes(hass, mqtt_mock):
"name": "test",
"command_topic": "command-topic",
"oscillation_command_topic": "oscillation-command-topic",
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
"speed_command_topic": "speed-command-topic",
+ "preset_mode_command_topic": "preset-mode-command-topic",
+ "percentage_command_topic": "percentage-command-topic",
+ "preset_modes": [
+ "freaking-high",
+ "silent",
+ ],
}
},
)
await hass.async_block_till_done()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
- assert state.attributes.get(fan.ATTR_SPEED_LIST) == ["off", "low", "medium", "high"]
+ assert state.state == STATE_OFF
+ assert state.attributes.get(fan.ATTR_SPEED_LIST) == [
+ "low",
+ "medium",
+ "high",
+ "freaking-high",
+ ]
await common.async_turn_on(hass, "fan.test")
state = hass.states.get("fan.test")
- assert state.state is STATE_ON
+ assert state.state == STATE_ON
assert state.attributes.get(ATTR_ASSUMED_STATE)
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
assert state.attributes.get(fan.ATTR_SPEED) is None
assert state.attributes.get(fan.ATTR_OSCILLATING) is None
await common.async_turn_off(hass, "fan.test")
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
assert state.attributes.get(fan.ATTR_SPEED) is None
assert state.attributes.get(fan.ATTR_OSCILLATING) is None
await common.async_oscillate(hass, "fan.test", True)
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
assert state.attributes.get(fan.ATTR_SPEED) is None
assert state.attributes.get(fan.ATTR_OSCILLATING) is True
await common.async_oscillate(hass, "fan.test", False)
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
assert state.attributes.get(fan.ATTR_SPEED) is None
assert state.attributes.get(fan.ATTR_OSCILLATING) is False
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
await common.async_set_speed(hass, "fan.test", fan.SPEED_LOW)
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
assert state.attributes.get(fan.ATTR_SPEED) == "low"
assert state.attributes.get(fan.ATTR_OSCILLATING) is False
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
await common.async_set_speed(hass, "fan.test", fan.SPEED_MEDIUM)
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
assert state.attributes.get(fan.ATTR_SPEED) == "medium"
assert state.attributes.get(fan.ATTR_OSCILLATING) is False
await common.async_set_speed(hass, "fan.test", fan.SPEED_HIGH)
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
assert state.attributes.get(fan.ATTR_SPEED) == "high"
assert state.attributes.get(fan.ATTR_OSCILLATING) is False
await common.async_set_speed(hass, "fan.test", fan.SPEED_OFF)
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
+ assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
assert state.attributes.get(fan.ATTR_SPEED) == "off"
assert state.attributes.get(fan.ATTR_OSCILLATING) is False
- with pytest.raises(ValueError):
- await common.async_set_speed(hass, "fan.test", "cUsToM")
+ await common.async_set_speed(hass, "fan.test", "cUsToM")
+ assert "not a valid speed" in caplog.text
+ caplog.clear()
+# use of speeds is deprecated, support will be removed after a quarter (2021.7)
async def test_custom_speed_list(hass, mqtt_mock):
"""Test optimistic mode without state topic."""
assert await async_setup_component(
@@ -541,8 +1759,8 @@ async def test_custom_speed_list(hass, mqtt_mock):
await hass.async_block_till_done()
state = hass.states.get("fan.test")
- assert state.state is STATE_OFF
- assert state.attributes.get(fan.ATTR_SPEED_LIST) == ["off", "high"]
+ assert state.state == STATE_OFF
+ assert state.attributes.get(fan.ATTR_SPEED_LIST) == ["high"]
async def test_supported_features(hass, mqtt_mock):
@@ -565,17 +1783,120 @@ async def test_supported_features(hass, mqtt_mock):
},
{
"platform": "mqtt",
- "name": "test3",
+ "name": "test3a1",
+ "command_topic": "command-topic",
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ "speed_command_topic": "speed-command-topic",
+ },
+ {
+ "platform": "mqtt",
+ "name": "test3a2",
+ "command_topic": "command-topic",
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
+ "speed_command_topic": "speed-command-topic",
+ "speeds": ["low"],
+ },
+ {
+ "platform": "mqtt",
+ "name": "test3a3",
"command_topic": "command-topic",
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
"speed_command_topic": "speed-command-topic",
+ "speeds": ["off"],
+ },
+ {
+ "platform": "mqtt",
+ "name": "test3b",
+ "command_topic": "command-topic",
+ "percentage_command_topic": "percentage-command-topic",
+ },
+ {
+ "platform": "mqtt",
+ "name": "test3c1",
+ "command_topic": "command-topic",
+ "preset_mode_command_topic": "preset-mode-command-topic",
+ },
+ {
+ "platform": "mqtt",
+ "name": "test3c2",
+ "command_topic": "command-topic",
+ "preset_mode_command_topic": "preset-mode-command-topic",
+ "preset_modes": ["very-fast", "auto"],
+ },
+ {
+ "platform": "mqtt",
+ "name": "test3c3",
+ "command_topic": "command-topic",
+ "preset_mode_command_topic": "preset-mode-command-topic",
+ "preset_modes": ["off", "on", "auto"],
},
{
"platform": "mqtt",
"name": "test4",
"command_topic": "command-topic",
"oscillation_command_topic": "oscillation-command-topic",
+ # use of speeds is deprecated, support will be removed after a quarter (2021.7)
"speed_command_topic": "speed-command-topic",
},
+ {
+ "platform": "mqtt",
+ "name": "test4pcta",
+ "command_topic": "command-topic",
+ "percentage_command_topic": "percentage-command-topic",
+ },
+ {
+ "platform": "mqtt",
+ "name": "test4pctb",
+ "command_topic": "command-topic",
+ "oscillation_command_topic": "oscillation-command-topic",
+ "percentage_command_topic": "percentage-command-topic",
+ },
+ {
+ "platform": "mqtt",
+ "name": "test5pr_ma",
+ "command_topic": "command-topic",
+ "preset_mode_command_topic": "preset-mode-command-topic",
+ "preset_modes": ["Mode1", "Mode2", "Mode3"],
+ },
+ {
+ "platform": "mqtt",
+ "name": "test5pr_mb",
+ "command_topic": "command-topic",
+ "preset_mode_command_topic": "preset-mode-command-topic",
+ "preset_modes": ["off", "on", "auto"],
+ },
+ {
+ "platform": "mqtt",
+ "name": "test5pr_mc",
+ "command_topic": "command-topic",
+ "oscillation_command_topic": "oscillation-command-topic",
+ "preset_mode_command_topic": "preset-mode-command-topic",
+ "preset_modes": ["Mode1", "Mode2", "Mode3"],
+ },
+ {
+ "platform": "mqtt",
+ "name": "test6spd_range_a",
+ "command_topic": "command-topic",
+ "percentage_command_topic": "percentage-command-topic",
+ "speed_range_min": 1,
+ "speed_range_max": 40,
+ },
+ {
+ "platform": "mqtt",
+ "name": "test6spd_range_b",
+ "command_topic": "command-topic",
+ "percentage_command_topic": "percentage-command-topic",
+ "speed_range_min": 50,
+ "speed_range_max": 40,
+ },
+ {
+ "platform": "mqtt",
+ "name": "test6spd_range_c",
+ "command_topic": "command-topic",
+ "percentage_command_topic": "percentage-command-topic",
+ "speed_range_min": 0,
+ "speed_range_max": 40,
+ },
]
},
)
@@ -585,14 +1906,69 @@ async def test_supported_features(hass, mqtt_mock):
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0
state = hass.states.get("fan.test2")
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_OSCILLATE
- state = hass.states.get("fan.test3")
+
+ state = hass.states.get("fan.test3a1")
+ assert (
+ state.attributes.get(ATTR_SUPPORTED_FEATURES)
+ and fan.SUPPORT_SET_SPEED == fan.SUPPORT_SET_SPEED
+ )
+ state = hass.states.get("fan.test3a2")
+ assert (
+ state.attributes.get(ATTR_SUPPORTED_FEATURES)
+ and fan.SUPPORT_SET_SPEED == fan.SUPPORT_SET_SPEED
+ )
+ state = hass.states.get("fan.test3a3")
+ assert state is None
+
+ state = hass.states.get("fan.test3b")
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_SET_SPEED
+
+ state = hass.states.get("fan.test3c1")
+ assert state is None
+
+ state = hass.states.get("fan.test3c2")
+ assert (
+ state.attributes.get(ATTR_SUPPORTED_FEATURES)
+ == fan.SUPPORT_PRESET_MODE | fan.SUPPORT_SET_SPEED
+ )
+ state = hass.states.get("fan.test3c3")
+ assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE
+
state = hass.states.get("fan.test4")
assert (
state.attributes.get(ATTR_SUPPORTED_FEATURES)
== fan.SUPPORT_OSCILLATE | fan.SUPPORT_SET_SPEED
)
+ state = hass.states.get("fan.test4pcta")
+ assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_SET_SPEED
+ state = hass.states.get("fan.test4pctb")
+ assert (
+ state.attributes.get(ATTR_SUPPORTED_FEATURES)
+ == fan.SUPPORT_OSCILLATE | fan.SUPPORT_SET_SPEED
+ )
+
+ state = hass.states.get("fan.test5pr_ma")
+ assert (
+ state.attributes.get(ATTR_SUPPORTED_FEATURES)
+ == fan.SUPPORT_SET_SPEED | fan.SUPPORT_PRESET_MODE
+ )
+ state = hass.states.get("fan.test5pr_mb")
+ assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE
+
+ state = hass.states.get("fan.test5pr_mc")
+ assert (
+ state.attributes.get(ATTR_SUPPORTED_FEATURES)
+ == fan.SUPPORT_OSCILLATE | fan.SUPPORT_SET_SPEED | fan.SUPPORT_PRESET_MODE
+ )
+
+ state = hass.states.get("fan.test6spd_range_a")
+ assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_SET_SPEED
+ state = hass.states.get("fan.test6spd_range_b")
+ assert state is None
+ state = hass.states.get("fan.test6spd_range_c")
+ assert state is None
+
async def test_availability_when_connection_lost(hass, mqtt_mock):
"""Test availability after MQTT disconnection."""
@@ -643,7 +2019,7 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog):
)
-async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog):
+async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog):
"""Test attributes get extracted from a JSON result."""
await help_test_update_with_json_attrs_bad_JSON(
hass, mqtt_mock, caplog, fan.DOMAIN, DEFAULT_CONFIG
diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py
index 2907a0e4cfc42f..15401a6d02f7c7 100644
--- a/tests/components/mqtt/test_init.py
+++ b/tests/components/mqtt/test_init.py
@@ -19,7 +19,7 @@
TEMP_CELSIUS,
)
from homeassistant.core import callback
-from homeassistant.helpers import device_registry
+from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
@@ -1058,7 +1058,7 @@ async def test_mqtt_ws_remove_non_mqtt_device(
device_entry = device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
- connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
assert device_entry is not None
@@ -1157,7 +1157,7 @@ async def test_debug_info_multiple_devices(hass, mqtt_mock):
},
]
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
for d in devices:
data = json.dumps(d["config"])
@@ -1236,7 +1236,7 @@ async def test_debug_info_multiple_entities_triggers(hass, mqtt_mock):
},
]
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
for c in config:
data = json.dumps(c["config"])
@@ -1284,7 +1284,7 @@ async def test_debug_info_non_mqtt(hass, device_reg, entity_reg):
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
- connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
for device_class in DEVICE_CLASSES:
entity_reg.async_get_or_create(
@@ -1311,7 +1311,7 @@ async def test_debug_info_wildcard(hass, mqtt_mock):
"unique_id": "veryunique",
}
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
data = json.dumps(config)
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
@@ -1357,7 +1357,7 @@ async def test_debug_info_filter_same(hass, mqtt_mock):
"unique_id": "veryunique",
}
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
data = json.dumps(config)
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
@@ -1416,7 +1416,7 @@ async def test_debug_info_same_topic(hass, mqtt_mock):
"unique_id": "veryunique",
}
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
data = json.dumps(config)
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
@@ -1467,7 +1467,7 @@ async def test_debug_info_qos_retain(hass, mqtt_mock):
"unique_id": "veryunique",
}
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
data = json.dumps(config)
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py
index 933f49ff823449..e995b373d03706 100644
--- a/tests/components/mqtt/test_light.py
+++ b/tests/components/mqtt/test_light.py
@@ -161,7 +161,13 @@
from homeassistant import config as hass_config
from homeassistant.components import light
-from homeassistant.const import ATTR_ASSUMED_STATE, SERVICE_RELOAD, STATE_OFF, STATE_ON
+from homeassistant.const import (
+ ATTR_ASSUMED_STATE,
+ ATTR_SUPPORTED_FEATURES,
+ SERVICE_RELOAD,
+ STATE_OFF,
+ STATE_ON,
+)
import homeassistant.core as ha
from homeassistant.setup import async_setup_component
@@ -206,6 +212,27 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock):
assert hass.states.get("light.test") is None
+async def test_rgb_light(hass, mqtt_mock):
+ """Test RGB light flags brightness support."""
+ assert await async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ light.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "command_topic": "test_light_rgb/set",
+ "rgb_command_topic": "test_light_rgb/rgb/set",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test")
+ expected_features = light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS
+ assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features
+
+
async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqtt_mock):
"""Test if there is no color and brightness if no topic."""
assert await async_setup_component(
@@ -743,10 +770,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock):
with patch(
"homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state",
return_value=fake_state,
- ):
- with assert_setup_component(1, light.DOMAIN):
- assert await async_setup_component(hass, light.DOMAIN, config)
- await hass.async_block_till_done()
+ ), assert_setup_component(1, light.DOMAIN):
+ assert await async_setup_component(hass, light.DOMAIN, config)
+ await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.state == STATE_ON
diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py
index 1c9eed0e40423f..6c9c7ae903a807 100644
--- a/tests/components/mqtt/test_light_json.py
+++ b/tests/components/mqtt/test_light_json.py
@@ -162,6 +162,86 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock):
assert hass.states.get("light.test") is None
+@pytest.mark.parametrize("deprecated", ("color_temp", "hs", "rgb", "white_value", "xy"))
+async def test_fail_setup_if_color_mode_deprecated(hass, mqtt_mock, deprecated):
+ """Test if setup fails if color mode is combined with deprecated config keys."""
+ supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"]
+
+ config = {
+ light.DOMAIN: {
+ "brightness": True,
+ "color_mode": True,
+ "command_topic": "test_light_rgb/set",
+ "name": "test",
+ "platform": "mqtt",
+ "schema": "json",
+ "supported_color_modes": supported_color_modes,
+ }
+ }
+ config[light.DOMAIN][deprecated] = True
+ assert await async_setup_component(
+ hass,
+ light.DOMAIN,
+ config,
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("light.test") is None
+
+
+@pytest.mark.parametrize(
+ "supported_color_modes", [["onoff", "rgb"], ["brightness", "rgb"], ["unknown"]]
+)
+async def test_fail_setup_if_color_modes_invalid(
+ hass, mqtt_mock, supported_color_modes
+):
+ """Test if setup fails if supported color modes is invalid."""
+ config = {
+ light.DOMAIN: {
+ "brightness": True,
+ "color_mode": True,
+ "command_topic": "test_light_rgb/set",
+ "name": "test",
+ "platform": "mqtt",
+ "schema": "json",
+ "supported_color_modes": supported_color_modes,
+ }
+ }
+ assert await async_setup_component(
+ hass,
+ light.DOMAIN,
+ config,
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("light.test") is None
+
+
+async def test_rgb_light(hass, mqtt_mock):
+ """Test RGB light flags brightness support."""
+ assert await async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ light.DOMAIN: {
+ "platform": "mqtt",
+ "schema": "json",
+ "name": "test",
+ "command_topic": "test_light_rgb/set",
+ "rgb": True,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test")
+ expected_features = (
+ light.SUPPORT_TRANSITION
+ | light.SUPPORT_COLOR
+ | light.SUPPORT_FLASH
+ | light.SUPPORT_BRIGHTNESS
+ )
+ assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features
+
+
async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_mock):
"""Test for no RGB, brightness, color temp, effect, white val or XY."""
assert await async_setup_component(
@@ -295,11 +375,21 @@ async def test_controlling_state_via_topic(hass, mqtt_mock):
light_state = hass.states.get("light.test")
assert light_state.attributes.get("hs_color") == (180.0, 50.0)
+ async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "color":null}')
+
+ light_state = hass.states.get("light.test")
+ assert "hs_color" not in light_state.attributes
+
async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "color_temp":155}')
light_state = hass.states.get("light.test")
assert light_state.attributes.get("color_temp") == 155
+ async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "color_temp":null}')
+
+ light_state = hass.states.get("light.test")
+ assert "color_temp" not in light_state.attributes
+
async_fire_mqtt_message(
hass, "test_light_rgb", '{"state":"ON", "effect":"colorloop"}'
)
@@ -313,6 +403,166 @@ async def test_controlling_state_via_topic(hass, mqtt_mock):
assert light_state.attributes.get("white_value") == 155
+async def test_controlling_state_via_topic2(hass, mqtt_mock, caplog):
+ """Test the controlling of the state via topic for a light supporting color mode."""
+ supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"]
+
+ assert await async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ light.DOMAIN: {
+ "brightness": True,
+ "color_mode": True,
+ "command_topic": "test_light_rgb/set",
+ "effect": True,
+ "name": "test",
+ "platform": "mqtt",
+ "qos": "0",
+ "schema": "json",
+ "state_topic": "test_light_rgb",
+ "supported_color_modes": supported_color_modes,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test")
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 44
+ assert state.attributes.get("brightness") is None
+ assert state.attributes.get("color_mode") is None
+ assert state.attributes.get("color_temp") is None
+ assert state.attributes.get("effect") is None
+ assert state.attributes.get("hs_color") is None
+ assert state.attributes.get("rgb_color") is None
+ assert state.attributes.get("rgbw_color") is None
+ assert state.attributes.get("rgbww_color") is None
+ assert state.attributes.get("supported_color_modes") == supported_color_modes
+ assert state.attributes.get("white_value") is None
+ assert state.attributes.get("xy_color") is None
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ # Turn on the light, rgbww mode, additional values in the update
+ async_fire_mqtt_message(
+ hass,
+ "test_light_rgb",
+ '{"state":"ON",'
+ '"color_mode":"rgbww",'
+ '"color":{"r":255,"g":128,"b":64, "c": 32, "w": 16, "x": 1, "y": 1},'
+ '"brightness":255,'
+ '"color_temp":155,'
+ '"effect":"colorloop",'
+ '"white_value":150}',
+ )
+
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("brightness") == 255
+ assert state.attributes.get("color_mode") == "rgbww"
+ assert state.attributes.get("color_temp") is None
+ assert state.attributes.get("effect") == "colorloop"
+ assert state.attributes.get("hs_color") is None
+ assert state.attributes.get("rgb_color") is None
+ assert state.attributes.get("rgbw_color") is None
+ assert state.attributes.get("rgbww_color") == (255, 128, 64, 32, 16)
+ assert state.attributes.get("white_value") is None
+ assert state.attributes.get("xy_color") is None
+
+ # Light turned off
+ async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"OFF"}')
+ state = hass.states.get("light.test")
+ assert state.state == STATE_OFF
+
+ # Light turned on, brightness 100
+ async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "brightness":100}')
+ state = hass.states.get("light.test")
+ assert state.attributes["brightness"] == 100
+
+ # RGB color
+ async_fire_mqtt_message(
+ hass,
+ "test_light_rgb",
+ '{"state":"ON", "color_mode":"rgb", "color":{"r":64,"g":128,"b":255}}',
+ )
+ state = hass.states.get("light.test")
+ assert state.attributes.get("color_mode") == "rgb"
+ assert state.attributes.get("rgb_color") == (64, 128, 255)
+
+ # RGBW color
+ async_fire_mqtt_message(
+ hass,
+ "test_light_rgb",
+ '{"state":"ON", "color_mode":"rgbw", "color":{"r":64,"g":128,"b":255,"w":32}}',
+ )
+ state = hass.states.get("light.test")
+ assert state.attributes.get("color_mode") == "rgbw"
+ assert state.attributes.get("rgbw_color") == (64, 128, 255, 32)
+
+ # XY color
+ async_fire_mqtt_message(
+ hass,
+ "test_light_rgb",
+ '{"state":"ON", "color_mode":"xy", "color":{"x":0.135,"y":0.235}}',
+ )
+ state = hass.states.get("light.test")
+ assert state.attributes.get("color_mode") == "xy"
+ assert state.attributes.get("xy_color") == (0.135, 0.235)
+
+ # HS color
+ async_fire_mqtt_message(
+ hass,
+ "test_light_rgb",
+ '{"state":"ON", "color_mode":"hs", "color":{"h":180,"s":50}}',
+ )
+ state = hass.states.get("light.test")
+ assert state.attributes.get("color_mode") == "hs"
+ assert state.attributes.get("hs_color") == (180.0, 50.0)
+
+ # Color temp
+ async_fire_mqtt_message(
+ hass,
+ "test_light_rgb",
+ '{"state":"ON", "color_mode":"color_temp", "color_temp":155}',
+ )
+ state = hass.states.get("light.test")
+ assert state.attributes.get("color_mode") == "color_temp"
+ assert state.attributes.get("color_temp") == 155
+
+ async_fire_mqtt_message(
+ hass, "test_light_rgb", '{"state":"ON", "effect":"other_effect"}'
+ )
+ state = hass.states.get("light.test")
+ assert state.attributes.get("effect") == "other_effect"
+
+ # White value should be ignored
+ async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "white_value":155}')
+ state = hass.states.get("light.test")
+ assert state.attributes.get("white_value") is None
+
+ # Invalid color mode
+ async_fire_mqtt_message(
+ hass, "test_light_rgb", '{"state":"ON", "color_mode":"col_temp"}'
+ )
+ assert "Invalid color mode received" in caplog.text
+ caplog.clear()
+
+ # Incomplete color
+ async_fire_mqtt_message(
+ hass, "test_light_rgb", '{"state":"ON", "color_mode":"rgb"}'
+ )
+ assert "Invalid or incomplete color value received" in caplog.text
+ caplog.clear()
+
+ # Invalid color
+ async_fire_mqtt_message(
+ hass,
+ "test_light_rgb",
+ '{"state":"ON", "color_mode":"rgb", "color":{"r":64,"g":128,"b":"cow"}}',
+ )
+ assert "Invalid or incomplete color value received" in caplog.text
+
+
async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock):
"""Test the sending of command in optimistic mode."""
fake_state = ha.State(
@@ -447,6 +697,206 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock):
assert state.attributes["xy_color"] == (0.611, 0.375)
+async def test_sending_mqtt_commands_and_optimistic2(hass, mqtt_mock):
+ """Test the sending of command in optimistic mode for a light supporting color mode."""
+ supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"]
+ fake_state = ha.State(
+ "light.test",
+ "on",
+ {
+ "brightness": 95,
+ "color_temp": 100,
+ "color_mode": "rgb",
+ "effect": "random",
+ "hs_color": [100, 100],
+ "white_value": 50,
+ },
+ )
+
+ with patch(
+ "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state",
+ return_value=fake_state,
+ ):
+ assert await async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ light.DOMAIN: {
+ "brightness": True,
+ "color_mode": True,
+ "command_topic": "test_light_rgb/set",
+ "effect": True,
+ "name": "test",
+ "platform": "mqtt",
+ "qos": 2,
+ "schema": "json",
+ "supported_color_modes": supported_color_modes,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 44
+ assert state.attributes.get("brightness") == 95
+ assert state.attributes.get("color_mode") == "rgb"
+ assert state.attributes.get("color_temp") is None
+ assert state.attributes.get("effect") == "random"
+ assert state.attributes.get("hs_color") is None
+ assert state.attributes.get("rgb_color") is None
+ assert state.attributes.get("rgbw_color") is None
+ assert state.attributes.get("rgbww_color") is None
+ assert state.attributes.get("supported_color_modes") == supported_color_modes
+ assert state.attributes.get("white_value") is None
+ assert state.attributes.get("xy_color") is None
+ assert state.attributes.get(ATTR_ASSUMED_STATE)
+
+ # Turn the light on
+ await common.async_turn_on(hass, "light.test")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "test_light_rgb/set", '{"state": "ON"}', 2, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+
+ # Turn the light on with color temperature
+ await common.async_turn_on(hass, "light.test", color_temp=90)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "test_light_rgb/set",
+ JsonValidator('{"state": "ON", "color_temp": 90}'),
+ 2,
+ False,
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+
+ # Turn the light off
+ await common.async_turn_off(hass, "light.test")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "test_light_rgb/set", '{"state": "OFF"}', 2, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("light.test")
+ assert state.state == STATE_OFF
+
+ # Set hs color
+ await common.async_turn_on(hass, "light.test", brightness=75, hs_color=[359, 78])
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes["brightness"] == 75
+ assert state.attributes["color_mode"] == "hs"
+ assert state.attributes["hs_color"] == (359, 78)
+ assert state.attributes["rgb_color"] == (255, 56, 59)
+ assert state.attributes["xy_color"] == (0.654, 0.301)
+ assert "rgbw_color" not in state.attributes
+ assert "rgbww_color" not in state.attributes
+ assert "white_value" not in state.attributes
+ mqtt_mock.async_publish.assert_called_once_with(
+ "test_light_rgb/set",
+ JsonValidator(
+ '{"state": "ON", "color": {"h": 359.0, "s": 78.0},' ' "brightness": 75}'
+ ),
+ 2,
+ False,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Set rgb color, white value should be discarded
+ await common.async_turn_on(
+ hass, "light.test", rgb_color=[255, 128, 0], white_value=80
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes["brightness"] == 75
+ assert state.attributes["color_mode"] == "rgb"
+ assert state.attributes["hs_color"] == (30.118, 100.0)
+ assert state.attributes["rgb_color"] == (255, 128, 0)
+ assert state.attributes["xy_color"] == (0.611, 0.375)
+ assert "rgbw_color" not in state.attributes
+ assert "rgbww_color" not in state.attributes
+ assert "white_value" not in state.attributes
+ mqtt_mock.async_publish.assert_called_once_with(
+ "test_light_rgb/set",
+ JsonValidator('{"state": "ON", "color": {"r": 255, "g": 128, "b": 0} }'),
+ 2,
+ False,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Set rgbw color
+ await common.async_turn_on(
+ hass, "light.test", rgbw_color=[255, 128, 0, 123], white_value=80
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes["brightness"] == 75
+ assert state.attributes["color_mode"] == "rgbw"
+ assert state.attributes["rgbw_color"] == (255, 128, 0, 123)
+ assert "hs_color" not in state.attributes
+ assert "rgb_color" not in state.attributes
+ assert "rgbww_color" not in state.attributes
+ assert "white_value" not in state.attributes
+ assert "xy_color" not in state.attributes
+ mqtt_mock.async_publish.assert_called_once_with(
+ "test_light_rgb/set",
+ JsonValidator(
+ '{"state": "ON", "color": {"r": 255, "g": 128, "b": 0, "w": 123} }'
+ ),
+ 2,
+ False,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Set rgbww color
+ await common.async_turn_on(hass, "light.test", rgbww_color=[255, 128, 0, 45, 32])
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes["brightness"] == 75
+ assert state.attributes["color_mode"] == "rgbww"
+ assert state.attributes["rgbww_color"] == (255, 128, 0, 45, 32)
+ assert "hs_color" not in state.attributes
+ assert "rgb_color" not in state.attributes
+ assert "rgbw_color" not in state.attributes
+ assert "white_value" not in state.attributes
+ assert "xy_color" not in state.attributes
+ mqtt_mock.async_publish.assert_called_once_with(
+ "test_light_rgb/set",
+ JsonValidator(
+ '{"state": "ON", "color": {"r": 255, "g": 128, "b": 0, "c": 45, "w": 32} }'
+ ),
+ 2,
+ False,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Set xy color
+ await common.async_turn_on(
+ hass, "light.test", brightness=50, xy_color=[0.123, 0.223]
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes["brightness"] == 50
+ assert state.attributes["color_mode"] == "xy"
+ assert state.attributes["hs_color"] == (196.471, 100.0)
+ assert state.attributes["rgb_color"] == (0, 185, 255)
+ assert state.attributes["xy_color"] == (0.123, 0.223)
+ assert "rgbw_color" not in state.attributes
+ assert "rgbww_color" not in state.attributes
+ assert "white_value" not in state.attributes
+ mqtt_mock.async_publish.assert_called_once_with(
+ "test_light_rgb/set",
+ JsonValidator(
+ '{"state": "ON", "color": {"x": 0.123, "y": 0.223},' ' "brightness": 50}'
+ ),
+ 2,
+ False,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+
async def test_sending_hs_color(hass, mqtt_mock):
"""Test light.turn_on with hs color sends hs color parameters."""
assert await async_setup_component(
@@ -564,6 +1014,83 @@ async def test_sending_rgb_color_no_brightness(hass, mqtt_mock):
)
+async def test_sending_rgb_color_no_brightness2(hass, mqtt_mock):
+ """Test light.turn_on with hs color sends rgb color parameters."""
+ supported_color_modes = ["rgb", "rgbw", "rgbww"]
+ assert await async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ light.DOMAIN: {
+ "color_mode": True,
+ "command_topic": "test_light_rgb/set",
+ "name": "test",
+ "platform": "mqtt",
+ "schema": "json",
+ "supported_color_modes": supported_color_modes,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test")
+ assert state.state == STATE_OFF
+
+ await common.async_turn_on(
+ hass, "light.test", brightness=50, xy_color=[0.123, 0.123]
+ )
+ await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78])
+ await common.async_turn_on(
+ hass, "light.test", rgb_color=[255, 128, 0], brightness=255
+ )
+ await common.async_turn_on(
+ hass, "light.test", rgbw_color=[128, 64, 32, 16], brightness=128
+ )
+ await common.async_turn_on(
+ hass, "light.test", rgbww_color=[128, 64, 32, 16, 8], brightness=64
+ )
+
+ mqtt_mock.async_publish.assert_has_calls(
+ [
+ call(
+ "test_light_rgb/set",
+ JsonValidator('{"state": "ON", "color": {"r": 0, "g": 24, "b": 50}}'),
+ 0,
+ False,
+ ),
+ call(
+ "test_light_rgb/set",
+ JsonValidator('{"state": "ON", "color": {"r": 50, "g": 11, "b": 12}}'),
+ 0,
+ False,
+ ),
+ call(
+ "test_light_rgb/set",
+ JsonValidator('{"state": "ON", "color": {"r": 255, "g": 128, "b": 0}}'),
+ 0,
+ False,
+ ),
+ call(
+ "test_light_rgb/set",
+ JsonValidator(
+ '{"state": "ON", "color": {"r": 64, "g": 32, "b": 16, "w": 8}}'
+ ),
+ 0,
+ False,
+ ),
+ call(
+ "test_light_rgb/set",
+ JsonValidator(
+ '{"state": "ON", "color": {"r": 32, "g": 16, "b": 8, "c": 4, "w": 2}}'
+ ),
+ 0,
+ False,
+ ),
+ ],
+ any_order=True,
+ )
+
+
async def test_sending_rgb_color_with_brightness(hass, mqtt_mock):
"""Test light.turn_on with hs color sends rgb color parameters."""
assert await async_setup_component(
@@ -866,6 +1393,24 @@ async def test_flash_short_and_long(hass, mqtt_mock):
state = hass.states.get("light.test")
assert state.state == STATE_ON
+ await common.async_turn_off(hass, "light.test", flash="short")
+
+ mqtt_mock.async_publish.assert_called_once_with(
+ "test_light_rgb/set", JsonValidator('{"state": "OFF", "flash": 5}'), 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("light.test")
+ assert state.state == STATE_OFF
+
+ await common.async_turn_off(hass, "light.test", flash="long")
+
+ mqtt_mock.async_publish.assert_called_once_with(
+ "test_light_rgb/set", JsonValidator('{"state": "OFF", "flash": 15}'), 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ state = hass.states.get("light.test")
+ assert state.state == STATE_OFF
+
async def test_transition(hass, mqtt_mock):
"""Test for transition time being sent when included."""
@@ -1004,6 +1549,18 @@ async def test_invalid_values(hass, mqtt_mock):
assert state.attributes.get("white_value") == 255
assert state.attributes.get("color_temp") == 100
+ # Empty color value
+ async_fire_mqtt_message(
+ hass,
+ "test_light_rgb",
+ '{"state":"ON",' '"color":{}}',
+ )
+
+ # Color should not have changed
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("rgb_color") == (255, 255, 255)
+
# Bad HS color values
async_fire_mqtt_message(
hass,
diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py
index 733a39ce252e69..2e726d40ef112f 100644
--- a/tests/components/mqtt/test_light_template.py
+++ b/tests/components/mqtt/test_light_template.py
@@ -141,6 +141,37 @@ async def test_setup_fails(hass, mqtt_mock):
assert hass.states.get("light.test") is None
+async def test_rgb_light(hass, mqtt_mock):
+ """Test RGB light flags brightness support."""
+ assert await async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ light.DOMAIN: {
+ "platform": "mqtt",
+ "schema": "template",
+ "name": "test",
+ "command_topic": "test_light_rgb/set",
+ "command_on_template": "on",
+ "command_off_template": "off",
+ "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}',
+ "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}',
+ "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}',
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test")
+ expected_features = (
+ light.SUPPORT_TRANSITION
+ | light.SUPPORT_COLOR
+ | light.SUPPORT_FLASH
+ | light.SUPPORT_BRIGHTNESS
+ )
+ assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features
+
+
async def test_state_change_via_topic(hass, mqtt_mock):
"""Test state change via topic."""
with assert_setup_component(1, light.DOMAIN):
@@ -298,39 +329,38 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock):
with patch(
"homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state",
return_value=fake_state,
- ):
- with assert_setup_component(1, light.DOMAIN):
- assert await async_setup_component(
- hass,
- light.DOMAIN,
- {
- light.DOMAIN: {
- "platform": "mqtt",
- "schema": "template",
- "name": "test",
- "command_topic": "test_light_rgb/set",
- "command_on_template": "on,"
- "{{ brightness|d }},"
- "{{ color_temp|d }},"
- "{{ white_value|d }},"
- "{{ red|d }}-"
- "{{ green|d }}-"
- "{{ blue|d }}",
- "command_off_template": "off",
- "effect_list": ["colorloop", "random"],
- "optimistic": True,
- "state_template": '{{ value.split(",")[0] }}',
- "color_temp_template": '{{ value.split(",")[2] }}',
- "white_value_template": '{{ value.split(",")[3] }}',
- "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}',
- "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}',
- "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}',
- "effect_template": '{{ value.split(",")[5] }}',
- "qos": 2,
- }
- },
- )
- await hass.async_block_till_done()
+ ), assert_setup_component(1, light.DOMAIN):
+ assert await async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ light.DOMAIN: {
+ "platform": "mqtt",
+ "schema": "template",
+ "name": "test",
+ "command_topic": "test_light_rgb/set",
+ "command_on_template": "on,"
+ "{{ brightness|d }},"
+ "{{ color_temp|d }},"
+ "{{ white_value|d }},"
+ "{{ red|d }}-"
+ "{{ green|d }}-"
+ "{{ blue|d }}",
+ "command_off_template": "off",
+ "effect_list": ["colorloop", "random"],
+ "optimistic": True,
+ "state_template": '{{ value.split(",")[0] }}',
+ "color_temp_template": '{{ value.split(",")[2] }}',
+ "white_value_template": '{{ value.split(",")[3] }}',
+ "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}',
+ "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}',
+ "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}',
+ "effect_template": '{{ value.split(",")[5] }}',
+ "qos": 2,
+ }
+ },
+ )
+ await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.state == STATE_ON
@@ -677,7 +707,7 @@ async def test_transition(hass, mqtt_mock):
"name": "test",
"command_topic": "test_light_rgb/set",
"command_on_template": "on,{{ transition }}",
- "command_off_template": "off,{{ transition|d }}",
+ "command_off_template": "off,{{ transition|int|d }}",
"qos": 1,
}
},
@@ -689,15 +719,15 @@ async def test_transition(hass, mqtt_mock):
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40
- await common.async_turn_on(hass, "light.test", transition=10)
+ await common.async_turn_on(hass, "light.test", transition=10.0)
mqtt_mock.async_publish.assert_called_once_with(
- "test_light_rgb/set", "on,10", 1, False
+ "test_light_rgb/set", "on,10.0", 1, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("light.test")
assert state.state == STATE_ON
- await common.async_turn_off(hass, "light.test", transition=20)
+ await common.async_turn_off(hass, "light.test", transition=20.0)
mqtt_mock.async_publish.assert_called_once_with(
"test_light_rgb/set", "off,20", 1, False
)
diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py
index a2c2605d6ab4e1..373048f6f1a993 100644
--- a/tests/components/mqtt/test_sensor.py
+++ b/tests/components/mqtt/test_sensor.py
@@ -9,6 +9,7 @@
import homeassistant.components.sensor as sensor
from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE
import homeassistant.core as ha
+from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -36,6 +37,7 @@
help_test_entity_device_info_update,
help_test_entity_device_info_with_connection,
help_test_entity_device_info_with_identifier,
+ help_test_entity_disabled_by_default,
help_test_entity_id_update_discovery_update,
help_test_entity_id_update_subscriptions,
help_test_setting_attribute_via_mqtt_json_message,
@@ -574,7 +576,7 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock):
async def test_entity_device_info_with_hub(hass, mqtt_mock):
"""Test MQTT sensor device registry integration."""
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
hub = registry.async_get_or_create(
config_entry_id="123",
connections=set(),
@@ -631,3 +633,10 @@ async def test_entity_debug_info_update_entity_id(hass, mqtt_mock):
await help_test_entity_debug_info_update_entity_id(
hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG
)
+
+
+async def test_entity_disabled_by_default(hass, mqtt_mock):
+ """Test entity disabled by default."""
+ await help_test_entity_disabled_by_default(
+ hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG
+ )
diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py
index 67964a36e1a28f..98dcfa050e5424 100644
--- a/tests/components/mqtt/test_tag.py
+++ b/tests/components/mqtt/test_tag.py
@@ -5,6 +5,8 @@
import pytest
+from homeassistant.helpers import device_registry as dr
+
from tests.common import (
async_fire_mqtt_message,
async_get_device_automations,
@@ -378,7 +380,7 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry(
async def test_entity_device_info_with_connection(hass, mqtt_mock):
"""Test MQTT device registry integration."""
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
data = json.dumps(
{
@@ -406,7 +408,7 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock):
async def test_entity_device_info_with_identifier(hass, mqtt_mock):
"""Test MQTT device registry integration."""
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
data = json.dumps(
{
@@ -434,7 +436,7 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock):
async def test_entity_device_info_update(hass, mqtt_mock):
"""Test device registry update."""
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
config = {
"topic": "test-topic",
diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py
index b27af2b9bd014e..c2c77dcddd8d3a 100644
--- a/tests/components/mqtt/test_trigger.py
+++ b/tests/components/mqtt/test_trigger.py
@@ -8,7 +8,7 @@
from homeassistant.setup import async_setup_component
from tests.common import async_fire_mqtt_message, async_mock_service, mock_component
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
@@ -34,9 +34,9 @@ async def test_if_fires_on_topic_match(hass, calls):
"action": {
"service": "test.automation",
"data_template": {
- "some": "{{ trigger.platform }} - {{ trigger.topic }}"
- " - {{ trigger.payload }} - "
- "{{ trigger.payload_json.hello }}"
+ "some": "{{ trigger.platform }} - {{ trigger.topic }} - "
+ "{{ trigger.payload }} - {{ trigger.payload_json.hello }} - "
+ "{{ trigger.id }}"
},
},
}
@@ -46,7 +46,9 @@ async def test_if_fires_on_topic_match(hass, calls):
async_fire_mqtt_message(hass, "test-topic", '{ "hello": "world" }')
await hass.async_block_till_done()
assert len(calls) == 1
- assert 'mqtt - test-topic - { "hello": "world" } - world' == calls[0].data["some"]
+ assert (
+ calls[0].data["some"] == 'mqtt - test-topic - { "hello": "world" } - world - 0'
+ )
await hass.services.async_call(
automation.DOMAIN,
@@ -81,6 +83,114 @@ async def test_if_fires_on_topic_and_payload_match(hass, calls):
assert len(calls) == 1
+async def test_if_fires_on_topic_and_payload_match2(hass, calls):
+ """Test if message is fired on topic and payload match.
+
+ Make sure a payload which would render as a non string can still be matched.
+ """
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger": {
+ "platform": "mqtt",
+ "topic": "test-topic",
+ "payload": "0",
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+
+ async_fire_mqtt_message(hass, "test-topic", "0")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+
+async def test_if_fires_on_templated_topic_and_payload_match(hass, calls):
+ """Test if message is fired on templated topic and payload match."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger": {
+ "platform": "mqtt",
+ "topic": "test-topic-{{ sqrt(16)|round }}",
+ "payload": '{{ "foo"|regex_replace("foo", "bar") }}',
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+
+ async_fire_mqtt_message(hass, "test-topic-", "foo")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ async_fire_mqtt_message(hass, "test-topic-4", "foo")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ async_fire_mqtt_message(hass, "test-topic-4", "bar")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+
+async def test_if_fires_on_payload_template(hass, calls):
+ """Test if message is fired on templated topic and payload match."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger": {
+ "platform": "mqtt",
+ "topic": "test-topic",
+ "payload": "hello",
+ "value_template": "{{ value_json.wanted_key }}",
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+
+ async_fire_mqtt_message(hass, "test-topic", "hello")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ async_fire_mqtt_message(hass, "test-topic", '{"unwanted_key":"hello"}')
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ async_fire_mqtt_message(hass, "test-topic", '{"wanted_key":"hello"}')
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+
+async def test_non_allowed_templates(hass, calls, caplog):
+ """Test non allowed function in template."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger": {
+ "platform": "mqtt",
+ "topic": "test-topic-{{ states() }}",
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+
+ assert (
+ "Got error 'TemplateError: str: Use of 'states' is not supported in limited templates' when setting up triggers"
+ in caplog.text
+ )
+
+
async def test_if_not_fires_on_topic_but_no_payload_match(hass, calls):
"""Test if message is not fired on topic but no payload."""
assert await async_setup_component(
diff --git a/tests/components/mqtt_eventstream/test_init.py b/tests/components/mqtt_eventstream/test_init.py
index da37489a130db5..7f6b22bda90b5a 100644
--- a/tests/components/mqtt_eventstream/test_init.py
+++ b/tests/components/mqtt_eventstream/test_init.py
@@ -138,7 +138,7 @@ def listener(_):
async_fire_mqtt_message(hass, sub_topic, payload)
await hass.async_block_till_done()
- assert 1 == len(calls)
+ assert len(calls) == 1
async def test_ignored_event_doesnt_send_over_stream(hass, mqtt_mock):
diff --git a/tests/components/mqtt_room/test_sensor.py b/tests/components/mqtt_room/test_sensor.py
index ca5f9420dc567a..c3b8704c75467a 100644
--- a/tests/components/mqtt_room/test_sensor.py
+++ b/tests/components/mqtt_room/test_sensor.py
@@ -5,7 +5,7 @@
from homeassistant.components.mqtt import CONF_QOS, CONF_STATE_TOPIC, DEFAULT_QOS
import homeassistant.components.sensor as sensor
-from homeassistant.const import CONF_NAME, CONF_PLATFORM
+from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, CONF_PLATFORM, CONF_TIMEOUT
from homeassistant.setup import async_setup_component
from homeassistant.util import dt
@@ -21,9 +21,6 @@
SENSOR_STATE = f"sensor.{NAME}"
-CONF_DEVICE_ID = "device_id"
-CONF_TIMEOUT = "timeout"
-
NEAR_MESSAGE = {"id": DEVICE_ID, "name": NAME, "distance": 1}
FAR_MESSAGE = {"id": DEVICE_ID, "name": NAME, "distance": 10}
diff --git a/tests/components/mullvad/__init__.py b/tests/components/mullvad/__init__.py
new file mode 100644
index 00000000000000..dc940265eaccb7
--- /dev/null
+++ b/tests/components/mullvad/__init__.py
@@ -0,0 +1 @@
+"""Tests for the mullvad component."""
diff --git a/tests/components/mullvad/test_config_flow.py b/tests/components/mullvad/test_config_flow.py
new file mode 100644
index 00000000000000..c101e5a7246675
--- /dev/null
+++ b/tests/components/mullvad/test_config_flow.py
@@ -0,0 +1,94 @@
+"""Test the Mullvad config flow."""
+from unittest.mock import patch
+
+from mullvad_api import MullvadAPIError
+
+from homeassistant import config_entries, setup
+from homeassistant.components.mullvad.const import DOMAIN
+from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM
+
+from tests.common import MockConfigEntry
+
+
+async def test_form_user(hass):
+ """Test we can setup by the user."""
+ await setup.async_setup_component(hass, DOMAIN, {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == RESULT_TYPE_FORM
+ assert not result["errors"]
+
+ with patch(
+ "homeassistant.components.mullvad.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.mullvad.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry, patch(
+ "homeassistant.components.mullvad.config_flow.MullvadAPI"
+ ) as mock_mullvad_api:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Mullvad VPN"
+ assert result2["data"] == {}
+ assert len(mock_setup.mock_calls) == 0
+ assert len(mock_setup_entry.mock_calls) == 1
+ assert len(mock_mullvad_api.mock_calls) == 1
+
+
+async def test_form_user_only_once(hass):
+ """Test we can setup by the user only once."""
+ MockConfigEntry(domain=DOMAIN).add_to_hass(hass)
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+
+async def test_connection_error(hass):
+ """Test we show an error when we have trouble connecting."""
+ await setup.async_setup_component(hass, DOMAIN, {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.mullvad.config_flow.MullvadAPI",
+ side_effect=MullvadAPIError,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "cannot_connect"}
+
+
+async def test_unknown_error(hass):
+ """Test we show an error when an unknown error occurs."""
+ await setup.async_setup_component(hass, DOMAIN, {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.mullvad.config_flow.MullvadAPI",
+ side_effect=Exception,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "unknown"}
diff --git a/tests/components/my/__init__.py b/tests/components/my/__init__.py
new file mode 100644
index 00000000000000..82953c8dac2c98
--- /dev/null
+++ b/tests/components/my/__init__.py
@@ -0,0 +1 @@
+"""Tests for the my component."""
diff --git a/tests/components/my/test_init.py b/tests/components/my/test_init.py
new file mode 100644
index 00000000000000..86929271be9fa8
--- /dev/null
+++ b/tests/components/my/test_init.py
@@ -0,0 +1,17 @@
+"""Test the my init."""
+
+from unittest import mock
+
+from homeassistant.components.my import URL_PATH
+from homeassistant.setup import async_setup_component
+
+
+async def test_setup(hass):
+ """Test setup."""
+ with mock.patch(
+ "homeassistant.components.frontend.async_register_built_in_panel"
+ ) as mock_register_panel:
+ assert await async_setup_component(hass, "my", {"foo": "bar"})
+ assert mock_register_panel.call_args == mock.call(
+ hass, "my", frontend_url_path=URL_PATH
+ )
diff --git a/tests/components/myq/util.py b/tests/components/myq/util.py
index 84e85723918bf2..8cb0d17f592f9f 100644
--- a/tests/components/myq/util.py
+++ b/tests/components/myq/util.py
@@ -1,14 +1,18 @@
"""Tests for the myq integration."""
-
import json
+import logging
from unittest.mock import patch
+from pymyq.const import ACCOUNTS_ENDPOINT, DEVICES_ENDPOINT
+
from homeassistant.components.myq.const import DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture
+_LOGGER = logging.getLogger(__name__)
+
async def async_init_integration(
hass: HomeAssistant,
@@ -20,16 +24,24 @@ async def async_init_integration(
devices_json = load_fixture(devices_fixture)
devices_dict = json.loads(devices_json)
- def _handle_mock_api_request(method, endpoint, **kwargs):
- if endpoint == "Login":
- return {"SecurityToken": 1234}
- if endpoint == "My":
- return {"Account": {"Id": 1}}
- if endpoint == "Accounts/1/Devices":
- return devices_dict
- return {}
-
- with patch("pymyq.api.API.request", side_effect=_handle_mock_api_request):
+ def _handle_mock_api_oauth_authenticate():
+ return 1234, 1800
+
+ def _handle_mock_api_request(method, returns, url, **kwargs):
+ _LOGGER.debug("URL: %s", url)
+ if url == ACCOUNTS_ENDPOINT:
+ _LOGGER.debug("Accounts")
+ return None, {"accounts": [{"id": 1, "name": "mock"}]}
+ if url == DEVICES_ENDPOINT.format(account_id=1):
+ _LOGGER.debug("Devices")
+ return None, devices_dict
+ _LOGGER.debug("Something else")
+ return None, {}
+
+ with patch(
+ "pymyq.api.API._oauth_authenticate",
+ side_effect=_handle_mock_api_oauth_authenticate,
+ ), patch("pymyq.api.API.request", side_effect=_handle_mock_api_request):
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"}
)
diff --git a/tests/components/mysensors/__init__.py b/tests/components/mysensors/__init__.py
new file mode 100644
index 00000000000000..68fc6d7b4d7053
--- /dev/null
+++ b/tests/components/mysensors/__init__.py
@@ -0,0 +1 @@
+"""Tests for the MySensors integration."""
diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py
new file mode 100644
index 00000000000000..e4c4016d11ac12
--- /dev/null
+++ b/tests/components/mysensors/test_config_flow.py
@@ -0,0 +1,739 @@
+"""Test the MySensors config flow."""
+from __future__ import annotations
+
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant import config_entries, setup
+from homeassistant.components.mysensors.const import (
+ CONF_BAUD_RATE,
+ CONF_DEVICE,
+ CONF_GATEWAY_TYPE,
+ CONF_GATEWAY_TYPE_MQTT,
+ CONF_GATEWAY_TYPE_SERIAL,
+ CONF_GATEWAY_TYPE_TCP,
+ CONF_PERSISTENCE,
+ CONF_PERSISTENCE_FILE,
+ CONF_RETAIN,
+ CONF_TCP_PORT,
+ CONF_TOPIC_IN_PREFIX,
+ CONF_TOPIC_OUT_PREFIX,
+ CONF_VERSION,
+ DOMAIN,
+ ConfGatewayType,
+)
+from homeassistant.helpers.typing import HomeAssistantType
+
+from tests.common import MockConfigEntry
+
+
+async def get_form(
+ hass: HomeAssistantType, gatway_type: ConfGatewayType, expected_step_id: str
+):
+ """Get a form for the given gateway type."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ stepuser = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert stepuser["type"] == "form"
+ assert not stepuser["errors"]
+
+ result = await hass.config_entries.flow.async_configure(
+ stepuser["flow_id"],
+ {CONF_GATEWAY_TYPE: gatway_type},
+ )
+ await hass.async_block_till_done()
+ assert result["type"] == "form"
+ assert result["step_id"] == expected_step_id
+
+ return result
+
+
+async def test_config_mqtt(hass: HomeAssistantType):
+ """Test configuring a mqtt gateway."""
+ step = await get_form(hass, CONF_GATEWAY_TYPE_MQTT, "gw_mqtt")
+ flow_id = step["flow_id"]
+
+ with patch(
+ "homeassistant.components.mysensors.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.mysensors.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ flow_id,
+ {
+ CONF_RETAIN: True,
+ CONF_TOPIC_IN_PREFIX: "bla",
+ CONF_TOPIC_OUT_PREFIX: "blub",
+ CONF_VERSION: "2.4",
+ },
+ )
+ await hass.async_block_till_done()
+
+ if "errors" in result2:
+ assert not result2["errors"]
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "mqtt"
+ assert result2["data"] == {
+ CONF_DEVICE: "mqtt",
+ CONF_RETAIN: True,
+ CONF_TOPIC_IN_PREFIX: "bla",
+ CONF_TOPIC_OUT_PREFIX: "blub",
+ CONF_VERSION: "2.4",
+ CONF_GATEWAY_TYPE: "MQTT",
+ }
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_config_serial(hass: HomeAssistantType):
+ """Test configuring a gateway via serial."""
+ step = await get_form(hass, CONF_GATEWAY_TYPE_SERIAL, "gw_serial")
+ flow_id = step["flow_id"]
+
+ with patch( # mock is_serial_port because otherwise the test will be platform dependent (/dev/ttyACMx vs COMx)
+ "homeassistant.components.mysensors.config_flow.is_serial_port",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.mysensors.config_flow.try_connect", return_value=True
+ ), patch(
+ "homeassistant.components.mysensors.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.mysensors.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ flow_id,
+ {
+ CONF_BAUD_RATE: 115200,
+ CONF_DEVICE: "/dev/ttyACM0",
+ CONF_VERSION: "2.4",
+ },
+ )
+ await hass.async_block_till_done()
+
+ if "errors" in result2:
+ assert not result2["errors"]
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "/dev/ttyACM0"
+ assert result2["data"] == {
+ CONF_DEVICE: "/dev/ttyACM0",
+ CONF_BAUD_RATE: 115200,
+ CONF_VERSION: "2.4",
+ CONF_GATEWAY_TYPE: "Serial",
+ }
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_config_tcp(hass: HomeAssistantType):
+ """Test configuring a gateway via tcp."""
+ step = await get_form(hass, CONF_GATEWAY_TYPE_TCP, "gw_tcp")
+ flow_id = step["flow_id"]
+
+ with patch(
+ "homeassistant.components.mysensors.config_flow.try_connect", return_value=True
+ ), patch(
+ "homeassistant.components.mysensors.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.mysensors.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ flow_id,
+ {
+ CONF_TCP_PORT: 5003,
+ CONF_DEVICE: "127.0.0.1",
+ CONF_VERSION: "2.4",
+ },
+ )
+ await hass.async_block_till_done()
+
+ if "errors" in result2:
+ assert not result2["errors"]
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "127.0.0.1"
+ assert result2["data"] == {
+ CONF_DEVICE: "127.0.0.1",
+ CONF_TCP_PORT: 5003,
+ CONF_VERSION: "2.4",
+ CONF_GATEWAY_TYPE: "TCP",
+ }
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_fail_to_connect(hass: HomeAssistantType):
+ """Test configuring a gateway via tcp."""
+ step = await get_form(hass, CONF_GATEWAY_TYPE_TCP, "gw_tcp")
+ flow_id = step["flow_id"]
+
+ with patch(
+ "homeassistant.components.mysensors.config_flow.try_connect", return_value=False
+ ), patch(
+ "homeassistant.components.mysensors.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.mysensors.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ flow_id,
+ {
+ CONF_TCP_PORT: 5003,
+ CONF_DEVICE: "127.0.0.1",
+ CONF_VERSION: "2.4",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "form"
+ assert "errors" in result2
+ assert "base" in result2["errors"]
+ assert result2["errors"]["base"] == "cannot_connect"
+ assert len(mock_setup.mock_calls) == 0
+ assert len(mock_setup_entry.mock_calls) == 0
+
+
+@pytest.mark.parametrize(
+ "gateway_type, expected_step_id, user_input, err_field, err_string",
+ [
+ (
+ CONF_GATEWAY_TYPE_TCP,
+ "gw_tcp",
+ {
+ CONF_TCP_PORT: 600_000,
+ CONF_DEVICE: "127.0.0.1",
+ CONF_VERSION: "2.4",
+ },
+ CONF_TCP_PORT,
+ "port_out_of_range",
+ ),
+ (
+ CONF_GATEWAY_TYPE_TCP,
+ "gw_tcp",
+ {
+ CONF_TCP_PORT: 0,
+ CONF_DEVICE: "127.0.0.1",
+ CONF_VERSION: "2.4",
+ },
+ CONF_TCP_PORT,
+ "port_out_of_range",
+ ),
+ (
+ CONF_GATEWAY_TYPE_TCP,
+ "gw_tcp",
+ {
+ CONF_TCP_PORT: 5003,
+ CONF_DEVICE: "127.0.0.1",
+ CONF_VERSION: "a",
+ },
+ CONF_VERSION,
+ "invalid_version",
+ ),
+ (
+ CONF_GATEWAY_TYPE_TCP,
+ "gw_tcp",
+ {
+ CONF_TCP_PORT: 5003,
+ CONF_DEVICE: "127.0.0.1",
+ CONF_VERSION: "a.b",
+ },
+ CONF_VERSION,
+ "invalid_version",
+ ),
+ (
+ CONF_GATEWAY_TYPE_TCP,
+ "gw_tcp",
+ {
+ CONF_TCP_PORT: 5003,
+ CONF_DEVICE: "127.0.0.1",
+ },
+ CONF_VERSION,
+ "invalid_version",
+ ),
+ (
+ CONF_GATEWAY_TYPE_TCP,
+ "gw_tcp",
+ {
+ CONF_TCP_PORT: 5003,
+ CONF_DEVICE: "127.0.0.1",
+ CONF_VERSION: "4",
+ },
+ CONF_VERSION,
+ "invalid_version",
+ ),
+ (
+ CONF_GATEWAY_TYPE_TCP,
+ "gw_tcp",
+ {
+ CONF_TCP_PORT: 5003,
+ CONF_DEVICE: "127.0.0.1",
+ CONF_VERSION: "v3",
+ },
+ CONF_VERSION,
+ "invalid_version",
+ ),
+ (
+ CONF_GATEWAY_TYPE_TCP,
+ "gw_tcp",
+ {
+ CONF_TCP_PORT: 5003,
+ CONF_DEVICE: "127.0.0.",
+ },
+ CONF_DEVICE,
+ "invalid_ip",
+ ),
+ (
+ CONF_GATEWAY_TYPE_TCP,
+ "gw_tcp",
+ {
+ CONF_TCP_PORT: 5003,
+ CONF_DEVICE: "abcd",
+ },
+ CONF_DEVICE,
+ "invalid_ip",
+ ),
+ (
+ CONF_GATEWAY_TYPE_MQTT,
+ "gw_mqtt",
+ {
+ CONF_RETAIN: True,
+ CONF_TOPIC_IN_PREFIX: "bla",
+ CONF_TOPIC_OUT_PREFIX: "blub",
+ CONF_PERSISTENCE_FILE: "asdf.zip",
+ CONF_VERSION: "2.4",
+ },
+ CONF_PERSISTENCE_FILE,
+ "invalid_persistence_file",
+ ),
+ (
+ CONF_GATEWAY_TYPE_MQTT,
+ "gw_mqtt",
+ {
+ CONF_RETAIN: True,
+ CONF_TOPIC_IN_PREFIX: "/#/#",
+ CONF_TOPIC_OUT_PREFIX: "blub",
+ CONF_VERSION: "2.4",
+ },
+ CONF_TOPIC_IN_PREFIX,
+ "invalid_subscribe_topic",
+ ),
+ (
+ CONF_GATEWAY_TYPE_MQTT,
+ "gw_mqtt",
+ {
+ CONF_RETAIN: True,
+ CONF_TOPIC_IN_PREFIX: "asdf",
+ CONF_TOPIC_OUT_PREFIX: "/#/#",
+ CONF_VERSION: "2.4",
+ },
+ CONF_TOPIC_OUT_PREFIX,
+ "invalid_publish_topic",
+ ),
+ (
+ CONF_GATEWAY_TYPE_MQTT,
+ "gw_mqtt",
+ {
+ CONF_RETAIN: True,
+ CONF_TOPIC_IN_PREFIX: "asdf",
+ CONF_TOPIC_OUT_PREFIX: "asdf",
+ CONF_VERSION: "2.4",
+ },
+ CONF_TOPIC_OUT_PREFIX,
+ "same_topic",
+ ),
+ ],
+)
+async def test_config_invalid(
+ hass: HomeAssistantType,
+ gateway_type: ConfGatewayType,
+ expected_step_id: str,
+ user_input: dict[str, any],
+ err_field,
+ err_string,
+):
+ """Perform a test that is expected to generate an error."""
+ step = await get_form(hass, gateway_type, expected_step_id)
+ flow_id = step["flow_id"]
+
+ with patch(
+ "homeassistant.components.mysensors.config_flow.try_connect", return_value=True
+ ), patch(
+ "homeassistant.components.mysensors.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.mysensors.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ flow_id,
+ user_input,
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "form"
+ assert "errors" in result2
+ assert err_field in result2["errors"]
+ assert result2["errors"][err_field] == err_string
+ assert len(mock_setup.mock_calls) == 0
+ assert len(mock_setup_entry.mock_calls) == 0
+
+
+@pytest.mark.parametrize(
+ "user_input",
+ [
+ {
+ CONF_DEVICE: "COM5",
+ CONF_BAUD_RATE: 57600,
+ CONF_TCP_PORT: 5003,
+ CONF_RETAIN: True,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE_FILE: "bla.json",
+ },
+ {
+ CONF_DEVICE: "COM5",
+ CONF_PERSISTENCE_FILE: "bla.json",
+ CONF_BAUD_RATE: 57600,
+ CONF_TCP_PORT: 5003,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: True,
+ },
+ {
+ CONF_DEVICE: "mqtt",
+ CONF_BAUD_RATE: 115200,
+ CONF_TCP_PORT: 5003,
+ CONF_TOPIC_IN_PREFIX: "intopic",
+ CONF_TOPIC_OUT_PREFIX: "outtopic",
+ CONF_VERSION: "2.4",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ },
+ {
+ CONF_DEVICE: "127.0.0.1",
+ CONF_PERSISTENCE_FILE: "blub.pickle",
+ CONF_BAUD_RATE: 115200,
+ CONF_TCP_PORT: 343,
+ CONF_VERSION: "2.4",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ },
+ ],
+)
+async def test_import(hass: HomeAssistantType, user_input: dict):
+ """Test importing a gateway."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ with patch("sys.platform", "win32"), patch(
+ "homeassistant.components.mysensors.config_flow.try_connect", return_value=True
+ ), patch(
+ "homeassistant.components.mysensors.async_setup_entry",
+ return_value=True,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, data=user_input, context={"source": config_entries.SOURCE_IMPORT}
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "create_entry"
+
+
+@pytest.mark.parametrize(
+ "first_input, second_input, expected_result",
+ [
+ (
+ {
+ CONF_DEVICE: "mqtt",
+ CONF_VERSION: "2.3",
+ CONF_TOPIC_IN_PREFIX: "same1",
+ CONF_TOPIC_OUT_PREFIX: "same2",
+ },
+ {
+ CONF_DEVICE: "mqtt",
+ CONF_VERSION: "2.3",
+ CONF_TOPIC_IN_PREFIX: "same1",
+ CONF_TOPIC_OUT_PREFIX: "same2",
+ },
+ (CONF_TOPIC_IN_PREFIX, "duplicate_topic"),
+ ),
+ (
+ {
+ CONF_DEVICE: "mqtt",
+ CONF_VERSION: "2.3",
+ CONF_TOPIC_IN_PREFIX: "different1",
+ CONF_TOPIC_OUT_PREFIX: "different2",
+ },
+ {
+ CONF_DEVICE: "mqtt",
+ CONF_VERSION: "2.3",
+ CONF_TOPIC_IN_PREFIX: "different3",
+ CONF_TOPIC_OUT_PREFIX: "different4",
+ },
+ None,
+ ),
+ (
+ {
+ CONF_DEVICE: "mqtt",
+ CONF_VERSION: "2.3",
+ CONF_TOPIC_IN_PREFIX: "same1",
+ CONF_TOPIC_OUT_PREFIX: "different2",
+ },
+ {
+ CONF_DEVICE: "mqtt",
+ CONF_VERSION: "2.3",
+ CONF_TOPIC_IN_PREFIX: "same1",
+ CONF_TOPIC_OUT_PREFIX: "different4",
+ },
+ (CONF_TOPIC_IN_PREFIX, "duplicate_topic"),
+ ),
+ (
+ {
+ CONF_DEVICE: "mqtt",
+ CONF_VERSION: "2.3",
+ CONF_TOPIC_IN_PREFIX: "same1",
+ CONF_TOPIC_OUT_PREFIX: "different2",
+ },
+ {
+ CONF_DEVICE: "mqtt",
+ CONF_VERSION: "2.3",
+ CONF_TOPIC_IN_PREFIX: "different1",
+ CONF_TOPIC_OUT_PREFIX: "same1",
+ },
+ (CONF_TOPIC_OUT_PREFIX, "duplicate_topic"),
+ ),
+ (
+ {
+ CONF_DEVICE: "mqtt",
+ CONF_VERSION: "2.3",
+ CONF_TOPIC_IN_PREFIX: "same1",
+ CONF_TOPIC_OUT_PREFIX: "different2",
+ },
+ {
+ CONF_DEVICE: "mqtt",
+ CONF_VERSION: "2.3",
+ CONF_TOPIC_IN_PREFIX: "same1",
+ CONF_TOPIC_OUT_PREFIX: "different1",
+ },
+ (CONF_TOPIC_IN_PREFIX, "duplicate_topic"),
+ ),
+ (
+ {
+ CONF_DEVICE: "127.0.0.1",
+ CONF_PERSISTENCE_FILE: "same.json",
+ CONF_TCP_PORT: 343,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ },
+ {
+ CONF_DEVICE: "192.168.1.2",
+ CONF_PERSISTENCE_FILE: "same.json",
+ CONF_TCP_PORT: 343,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ },
+ ("persistence_file", "duplicate_persistence_file"),
+ ),
+ (
+ {
+ CONF_DEVICE: "127.0.0.1",
+ CONF_TCP_PORT: 343,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ },
+ {
+ CONF_DEVICE: "192.168.1.2",
+ CONF_PERSISTENCE_FILE: "same.json",
+ CONF_TCP_PORT: 343,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ },
+ None,
+ ),
+ (
+ {
+ CONF_DEVICE: "127.0.0.1",
+ CONF_TCP_PORT: 343,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ },
+ {
+ CONF_DEVICE: "192.168.1.2",
+ CONF_TCP_PORT: 343,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ },
+ None,
+ ),
+ (
+ {
+ CONF_DEVICE: "192.168.1.2",
+ CONF_PERSISTENCE_FILE: "different1.json",
+ CONF_TCP_PORT: 343,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ },
+ {
+ CONF_DEVICE: "192.168.1.2",
+ CONF_PERSISTENCE_FILE: "different2.json",
+ CONF_TCP_PORT: 343,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ },
+ ("base", "already_configured"),
+ ),
+ (
+ {
+ CONF_DEVICE: "192.168.1.2",
+ CONF_PERSISTENCE_FILE: "different1.json",
+ CONF_TCP_PORT: 343,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ },
+ {
+ CONF_DEVICE: "192.168.1.2",
+ CONF_PERSISTENCE_FILE: "different2.json",
+ CONF_TCP_PORT: 5003,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ },
+ None,
+ ),
+ (
+ {
+ CONF_DEVICE: "192.168.1.2",
+ CONF_TCP_PORT: 5003,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ },
+ {
+ CONF_DEVICE: "192.168.1.3",
+ CONF_TCP_PORT: 5003,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ },
+ None,
+ ),
+ (
+ {
+ CONF_DEVICE: "COM5",
+ CONF_TCP_PORT: 5003,
+ CONF_RETAIN: True,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE_FILE: "different1.json",
+ },
+ {
+ CONF_DEVICE: "COM5",
+ CONF_TCP_PORT: 5003,
+ CONF_RETAIN: True,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE_FILE: "different2.json",
+ },
+ ("base", "already_configured"),
+ ),
+ (
+ {
+ CONF_DEVICE: "COM6",
+ CONF_BAUD_RATE: 57600,
+ CONF_RETAIN: True,
+ CONF_VERSION: "2.3",
+ },
+ {
+ CONF_DEVICE: "COM5",
+ CONF_TCP_PORT: 5003,
+ CONF_RETAIN: True,
+ CONF_VERSION: "2.3",
+ },
+ None,
+ ),
+ (
+ {
+ CONF_DEVICE: "COM5",
+ CONF_BAUD_RATE: 115200,
+ CONF_RETAIN: True,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE_FILE: "different1.json",
+ },
+ {
+ CONF_DEVICE: "COM5",
+ CONF_BAUD_RATE: 57600,
+ CONF_RETAIN: True,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE_FILE: "different2.json",
+ },
+ ("base", "already_configured"),
+ ),
+ (
+ {
+ CONF_DEVICE: "COM5",
+ CONF_BAUD_RATE: 115200,
+ CONF_RETAIN: True,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE_FILE: "same.json",
+ },
+ {
+ CONF_DEVICE: "COM6",
+ CONF_BAUD_RATE: 57600,
+ CONF_RETAIN: True,
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE_FILE: "same.json",
+ },
+ ("persistence_file", "duplicate_persistence_file"),
+ ),
+ (
+ {
+ CONF_DEVICE: "mqtt",
+ CONF_PERSISTENCE_FILE: "bla.json",
+ CONF_BAUD_RATE: 115200,
+ CONF_TCP_PORT: 5003,
+ CONF_VERSION: "1.4",
+ },
+ {
+ CONF_DEVICE: "COM6",
+ CONF_PERSISTENCE_FILE: "bla2.json",
+ CONF_BAUD_RATE: 115200,
+ CONF_TCP_PORT: 5003,
+ CONF_VERSION: "1.4",
+ },
+ None,
+ ),
+ ],
+)
+async def test_duplicate(
+ hass: HomeAssistantType,
+ first_input: dict,
+ second_input: dict,
+ expected_result: tuple[str, str] | None,
+):
+ """Test duplicate detection."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ with patch("sys.platform", "win32"), patch(
+ "homeassistant.components.mysensors.config_flow.try_connect", return_value=True
+ ), patch(
+ "homeassistant.components.mysensors.async_setup_entry",
+ return_value=True,
+ ):
+ MockConfigEntry(domain=DOMAIN, data=first_input).add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, data=second_input, context={"source": config_entries.SOURCE_IMPORT}
+ )
+ await hass.async_block_till_done()
+ if expected_result is None:
+ assert result["type"] == "create_entry"
+ else:
+ assert result["type"] == "abort"
+ assert result["reason"] == expected_result[1]
diff --git a/tests/components/mysensors/test_gateway.py b/tests/components/mysensors/test_gateway.py
new file mode 100644
index 00000000000000..d3e360e0b9f8a5
--- /dev/null
+++ b/tests/components/mysensors/test_gateway.py
@@ -0,0 +1,30 @@
+"""Test function in gateway.py."""
+from unittest.mock import patch
+
+import pytest
+import voluptuous as vol
+
+from homeassistant.components.mysensors.gateway import is_serial_port
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+@pytest.mark.parametrize(
+ "port, expect_valid",
+ [
+ ("COM5", True),
+ ("asdf", False),
+ ("COM17", True),
+ ("COM", False),
+ ("/dev/ttyACM0", False),
+ ],
+)
+def test_is_serial_port_windows(hass: HomeAssistantType, port: str, expect_valid: bool):
+ """Test windows serial port."""
+
+ with patch("sys.platform", "win32"):
+ try:
+ is_serial_port(port)
+ except vol.Invalid:
+ assert not expect_valid
+ else:
+ assert expect_valid
diff --git a/tests/components/mysensors/test_init.py b/tests/components/mysensors/test_init.py
new file mode 100644
index 00000000000000..c85c627df9ff04
--- /dev/null
+++ b/tests/components/mysensors/test_init.py
@@ -0,0 +1,252 @@
+"""Test function in __init__.py."""
+from __future__ import annotations
+
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant.components.mysensors import (
+ CONF_BAUD_RATE,
+ CONF_DEVICE,
+ CONF_GATEWAYS,
+ CONF_PERSISTENCE,
+ CONF_PERSISTENCE_FILE,
+ CONF_RETAIN,
+ CONF_TCP_PORT,
+ CONF_VERSION,
+ DEFAULT_VERSION,
+ DOMAIN,
+)
+from homeassistant.components.mysensors.const import (
+ CONF_GATEWAY_TYPE,
+ CONF_GATEWAY_TYPE_MQTT,
+ CONF_GATEWAY_TYPE_SERIAL,
+ CONF_GATEWAY_TYPE_TCP,
+ CONF_TOPIC_IN_PREFIX,
+ CONF_TOPIC_OUT_PREFIX,
+)
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+from homeassistant.setup import async_setup_component
+
+
+@pytest.mark.parametrize(
+ "config, expected_calls, expected_to_succeed, expected_config_flow_user_input",
+ [
+ (
+ {
+ DOMAIN: {
+ CONF_GATEWAYS: [
+ {
+ CONF_DEVICE: "COM5",
+ CONF_PERSISTENCE_FILE: "bla.json",
+ CONF_BAUD_RATE: 57600,
+ CONF_TCP_PORT: 5003,
+ }
+ ],
+ CONF_VERSION: "2.3",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: True,
+ }
+ },
+ 1,
+ True,
+ {
+ CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL,
+ CONF_DEVICE: "COM5",
+ CONF_PERSISTENCE_FILE: "bla.json",
+ CONF_BAUD_RATE: 57600,
+ CONF_VERSION: "2.3",
+ },
+ ),
+ (
+ {
+ DOMAIN: {
+ CONF_GATEWAYS: [
+ {
+ CONF_DEVICE: "127.0.0.1",
+ CONF_PERSISTENCE_FILE: "blub.pickle",
+ CONF_BAUD_RATE: 115200,
+ CONF_TCP_PORT: 343,
+ }
+ ],
+ CONF_VERSION: "2.4",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ }
+ },
+ 1,
+ True,
+ {
+ CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP,
+ CONF_DEVICE: "127.0.0.1",
+ CONF_PERSISTENCE_FILE: "blub.pickle",
+ CONF_TCP_PORT: 343,
+ CONF_VERSION: "2.4",
+ },
+ ),
+ (
+ {
+ DOMAIN: {
+ CONF_GATEWAYS: [
+ {
+ CONF_DEVICE: "127.0.0.1",
+ }
+ ],
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ }
+ },
+ 1,
+ True,
+ {
+ CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP,
+ CONF_DEVICE: "127.0.0.1",
+ CONF_TCP_PORT: 5003,
+ CONF_VERSION: DEFAULT_VERSION,
+ },
+ ),
+ (
+ {
+ DOMAIN: {
+ CONF_GATEWAYS: [
+ {
+ CONF_DEVICE: "mqtt",
+ CONF_BAUD_RATE: 115200,
+ CONF_TCP_PORT: 5003,
+ CONF_TOPIC_IN_PREFIX: "intopic",
+ CONF_TOPIC_OUT_PREFIX: "outtopic",
+ }
+ ],
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ }
+ },
+ 1,
+ True,
+ {
+ CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT,
+ CONF_DEVICE: "mqtt",
+ CONF_VERSION: DEFAULT_VERSION,
+ CONF_TOPIC_OUT_PREFIX: "outtopic",
+ CONF_TOPIC_IN_PREFIX: "intopic",
+ },
+ ),
+ (
+ {
+ DOMAIN: {
+ CONF_GATEWAYS: [
+ {
+ CONF_DEVICE: "mqtt",
+ CONF_BAUD_RATE: 115200,
+ CONF_TCP_PORT: 5003,
+ }
+ ],
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ }
+ },
+ 0,
+ True,
+ {},
+ ),
+ (
+ {
+ DOMAIN: {
+ CONF_GATEWAYS: [
+ {
+ CONF_DEVICE: "mqtt",
+ CONF_PERSISTENCE_FILE: "bla.json",
+ CONF_TOPIC_OUT_PREFIX: "out",
+ CONF_TOPIC_IN_PREFIX: "in",
+ CONF_BAUD_RATE: 115200,
+ CONF_TCP_PORT: 5003,
+ },
+ {
+ CONF_DEVICE: "COM6",
+ CONF_PERSISTENCE_FILE: "bla2.json",
+ CONF_BAUD_RATE: 115200,
+ CONF_TCP_PORT: 5003,
+ },
+ ],
+ CONF_VERSION: "2.4",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ }
+ },
+ 2,
+ True,
+ {},
+ ),
+ (
+ {
+ DOMAIN: {
+ CONF_GATEWAYS: [
+ {
+ CONF_DEVICE: "mqtt",
+ CONF_PERSISTENCE_FILE: "bla.json",
+ CONF_BAUD_RATE: 115200,
+ CONF_TCP_PORT: 5003,
+ },
+ {
+ CONF_DEVICE: "COM6",
+ CONF_PERSISTENCE_FILE: "bla.json",
+ CONF_BAUD_RATE: 115200,
+ CONF_TCP_PORT: 5003,
+ },
+ ],
+ CONF_VERSION: "2.4",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ }
+ },
+ 0,
+ False,
+ {},
+ ),
+ (
+ {
+ DOMAIN: {
+ CONF_GATEWAYS: [
+ {
+ CONF_DEVICE: "COMx",
+ CONF_PERSISTENCE_FILE: "bla.json",
+ CONF_BAUD_RATE: 115200,
+ CONF_TCP_PORT: 5003,
+ },
+ ],
+ CONF_VERSION: "2.4",
+ CONF_PERSISTENCE: False,
+ CONF_RETAIN: False,
+ }
+ },
+ 0,
+ True,
+ {},
+ ),
+ ],
+)
+async def test_import(
+ hass: HomeAssistantType,
+ config: ConfigType,
+ expected_calls: int,
+ expected_to_succeed: bool,
+ expected_config_flow_user_input: dict[str, any],
+):
+ """Test importing a gateway."""
+ with patch("sys.platform", "win32"), patch(
+ "homeassistant.components.mysensors.config_flow.try_connect", return_value=True
+ ), patch(
+ "homeassistant.components.mysensors.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result = await async_setup_component(hass, DOMAIN, config)
+ assert result == expected_to_succeed
+ await hass.async_block_till_done()
+
+ assert len(mock_setup_entry.mock_calls) == expected_calls
+
+ if expected_calls > 0:
+ config_flow_user_input = mock_setup_entry.mock_calls[0][1][1].data
+ for key, value in expected_config_flow_user_input.items():
+ assert key in config_flow_user_input
+ assert config_flow_user_input[key] == value
diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py
index 84deef92d62f52..3c0c0fdb4db340 100644
--- a/tests/components/nest/camera_sdm_test.py
+++ b/tests/components/nest/camera_sdm_test.py
@@ -16,6 +16,8 @@
from homeassistant.components import camera
from homeassistant.components.camera import STATE_IDLE
from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers import device_registry as dr, entity_registry as er
+from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
from .common import async_setup_sdm_platform
@@ -167,13 +169,13 @@ async def test_camera_device(hass):
assert camera is not None
assert camera.state == STATE_IDLE
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("camera.my_camera")
assert entry.unique_id == "some-device-id-camera"
assert entry.original_name == "My Camera"
assert entry.domain == "camera"
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.name == "My Camera"
assert device.model == "Camera"
@@ -245,12 +247,20 @@ async def test_refresh_expired_stream_token(hass, auth):
DEVICE_TRAITS,
auth=auth,
)
+ assert await async_setup_component(hass, "stream", {})
assert len(hass.states.async_all()) == 1
cam = hass.states.get("camera.my_camera")
assert cam is not None
assert cam.state == STATE_IDLE
+ # Request a stream for the camera entity to exercise nest cam + camera interaction
+ # and shutdown on url expiration
+ with patch("homeassistant.components.camera.create_stream") as create_stream:
+ hls_url = await camera.async_request_stream(hass, "camera.my_camera", fmt="hls")
+ assert hls_url.startswith("/api/hls/") # Includes access token
+ assert create_stream.called
+
stream_source = await camera.async_get_stream_source(hass, "camera.my_camera")
assert stream_source == "rtsp://some/url?auth=g.1.streamingToken"
@@ -266,6 +276,13 @@ async def test_refresh_expired_stream_token(hass, auth):
stream_source = await camera.async_get_stream_source(hass, "camera.my_camera")
assert stream_source == "rtsp://some/url?auth=g.2.streamingToken"
+ # HLS stream is not re-created, just the source is updated
+ with patch("homeassistant.components.camera.create_stream") as create_stream:
+ hls_url1 = await camera.async_request_stream(
+ hass, "camera.my_camera", fmt="hls"
+ )
+ assert hls_url == hls_url1
+
# Next alarm is well before stream_2_expiration, no change
next_update = now + datetime.timedelta(seconds=100)
await fire_alarm(hass, next_update)
@@ -278,6 +295,13 @@ async def test_refresh_expired_stream_token(hass, auth):
stream_source = await camera.async_get_stream_source(hass, "camera.my_camera")
assert stream_source == "rtsp://some/url?auth=g.3.streamingToken"
+ # HLS stream is still not re-created
+ with patch("homeassistant.components.camera.create_stream") as create_stream:
+ hls_url2 = await camera.async_request_stream(
+ hass, "camera.my_camera", fmt="hls"
+ )
+ assert hls_url == hls_url2
+
async def test_stream_response_already_expired(hass, auth):
"""Test a API response returning an expired stream url."""
@@ -339,6 +363,7 @@ async def test_camera_removed(hass, auth):
for config_entry in hass.config_entries.async_entries(DOMAIN):
await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
@@ -355,12 +380,20 @@ async def test_refresh_expired_stream_failure(hass, auth):
make_stream_url_response(expiration=stream_2_expiration, token_num=2),
]
await async_setup_camera(hass, DEVICE_TRAITS, auth=auth)
+ assert await async_setup_component(hass, "stream", {})
assert len(hass.states.async_all()) == 1
cam = hass.states.get("camera.my_camera")
assert cam is not None
assert cam.state == STATE_IDLE
+ # Request an HLS stream
+ with patch("homeassistant.components.camera.create_stream") as create_stream:
+
+ hls_url = await camera.async_request_stream(hass, "camera.my_camera", fmt="hls")
+ assert hls_url.startswith("/api/hls/") # Includes access token
+ assert create_stream.called
+
stream_source = await camera.async_get_stream_source(hass, "camera.my_camera")
assert stream_source == "rtsp://some/url?auth=g.1.streamingToken"
@@ -373,6 +406,16 @@ async def test_refresh_expired_stream_failure(hass, auth):
stream_source = await camera.async_get_stream_source(hass, "camera.my_camera")
assert stream_source == "rtsp://some/url?auth=g.2.streamingToken"
+ # Requesting an HLS stream will create an entirely new stream
+ with patch("homeassistant.components.camera.create_stream") as create_stream:
+ # The HLS stream endpoint was invalidated, with a new auth token
+ hls_url2 = await camera.async_request_stream(
+ hass, "camera.my_camera", fmt="hls"
+ )
+ assert hls_url != hls_url2
+ assert hls_url2.startswith("/api/hls/") # Includes access token
+ assert create_stream.called
+
async def test_camera_image_from_last_event(hass, auth):
"""Test an image generated from an event."""
diff --git a/tests/components/nest/climate_sdm_test.py b/tests/components/nest/climate_sdm_test.py
index ef332d0e8480ba..888227b9cdeb64 100644
--- a/tests/components/nest/climate_sdm_test.py
+++ b/tests/components/nest/climate_sdm_test.py
@@ -819,6 +819,20 @@ async def test_thermostat_set_fan(hass, auth):
"params": {"timerMode": "OFF"},
}
+ # Turn on fan mode
+ await common.async_set_fan_mode(hass, FAN_ON)
+ await hass.async_block_till_done()
+
+ assert auth.method == "post"
+ assert auth.url == "some-device-id:executeCommand"
+ assert auth.json == {
+ "command": "sdm.devices.commands.Fan.SetTimer",
+ "params": {
+ "duration": "43200s",
+ "timerMode": "ON",
+ },
+ }
+
async def test_thermostat_fan_empty(hass):
"""Test a fan trait with an empty response."""
@@ -938,7 +952,7 @@ async def test_thermostat_set_hvac_fan_only(hass, auth):
assert url == "some-device-id:executeCommand"
assert json == {
"command": "sdm.devices.commands.Fan.SetTimer",
- "params": {"timerMode": "ON"},
+ "params": {"duration": "43200s", "timerMode": "ON"},
}
(method, url, json, headers) = auth.captured_requests.pop(0)
assert method == "post"
diff --git a/tests/components/nest/sensor_sdm_test.py b/tests/components/nest/sensor_sdm_test.py
index b8b2912b124c1b..dfdfd58d546980 100644
--- a/tests/components/nest/sensor_sdm_test.py
+++ b/tests/components/nest/sensor_sdm_test.py
@@ -8,6 +8,8 @@
from google_nest_sdm.device import Device
from google_nest_sdm.event import EventMessage
+from homeassistant.helpers import device_registry as dr, entity_registry as er
+
from .common import async_setup_sdm_platform
PLATFORM = "sensor"
@@ -52,13 +54,13 @@ async def test_thermostat_device(hass):
assert humidity is not None
assert humidity.state == "35.0"
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("sensor.my_sensor_temperature")
assert entry.unique_id == "some-device-id-temperature"
assert entry.original_name == "My Sensor Temperature"
assert entry.domain == "sensor"
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.name == "My Sensor"
assert device.model == "Thermostat"
@@ -197,13 +199,13 @@ async def test_device_with_unknown_type(hass):
assert temperature is not None
assert temperature.state == "25.1"
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("sensor.my_sensor_temperature")
assert entry.unique_id == "some-device-id-temperature"
assert entry.original_name == "My Sensor Temperature"
assert entry.domain == "sensor"
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.name == "My Sensor"
assert device.model is None
diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py
index 5e3b9bde442562..c9dc9992410d0c 100644
--- a/tests/components/nest/test_device_trigger.py
+++ b/tests/components/nest/test_device_trigger.py
@@ -9,6 +9,7 @@
)
from homeassistant.components.nest import DOMAIN
from homeassistant.components.nest.events import NEST_EVENT
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
@@ -101,7 +102,7 @@ async def test_get_triggers(hass):
)
await async_setup_camera(hass, {DEVICE_ID: camera})
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device_entry = device_registry.async_get_device({("nest", DEVICE_ID)})
expected_triggers = [
@@ -140,7 +141,7 @@ async def test_multiple_devices(hass):
)
await async_setup_camera(hass, {"device-id-1": camera1, "device-id-2": camera2})
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry1 = registry.async_get("camera.camera_1")
assert entry1.unique_id == "device-id-1-camera"
entry2 = registry.async_get("camera.camera_2")
@@ -176,7 +177,7 @@ async def test_triggers_for_invalid_device_id(hass):
)
await async_setup_camera(hass, {DEVICE_ID: camera})
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device_entry = device_registry.async_get_device({("nest", DEVICE_ID)})
assert device_entry is not None
@@ -198,7 +199,7 @@ async def test_no_triggers(hass):
camera = make_camera(device_id=DEVICE_ID, traits={})
await async_setup_camera(hass, {DEVICE_ID: camera})
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("camera.my_camera")
assert entry.unique_id == "some-device-id-camera"
@@ -288,7 +289,7 @@ async def test_subscriber_automation(hass, calls):
)
subscriber = await async_setup_camera(hass, {DEVICE_ID: camera})
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device_entry = device_registry.async_get_device({("nest", DEVICE_ID)})
assert await setup_automation(hass, device_entry.id, "camera_motion")
diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py
index 692507d6ff9050..df459a35f718bb 100644
--- a/tests/components/nest/test_events.py
+++ b/tests/components/nest/test_events.py
@@ -7,6 +7,7 @@
from google_nest_sdm.device import Device
from google_nest_sdm.event import EventMessage
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util.dt import utcnow
from .common import async_setup_sdm_platform
@@ -91,14 +92,14 @@ async def test_doorbell_chime_event(hass):
create_device_traits("sdm.devices.traits.DoorbellChime"),
)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("camera.front")
assert entry is not None
assert entry.unique_id == "some-device-id-camera"
assert entry.original_name == "Front"
assert entry.domain == "camera"
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
assert device.name == "Front"
assert device.model == "Doorbell"
@@ -127,7 +128,7 @@ async def test_camera_motion_event(hass):
"sdm.devices.types.CAMERA",
create_device_traits("sdm.devices.traits.CameraMotion"),
)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("camera.front")
assert entry is not None
@@ -154,7 +155,7 @@ async def test_camera_sound_event(hass):
"sdm.devices.types.CAMERA",
create_device_traits("sdm.devices.traits.CameraSound"),
)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("camera.front")
assert entry is not None
@@ -181,7 +182,7 @@ async def test_camera_person_event(hass):
"sdm.devices.types.DOORBELL",
create_device_traits("sdm.devices.traits.CameraEventImage"),
)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("camera.front")
assert entry is not None
@@ -208,7 +209,7 @@ async def test_camera_multiple_event(hass):
"sdm.devices.types.DOORBELL",
create_device_traits("sdm.devices.traits.CameraEventImage"),
)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("camera.front")
assert entry is not None
diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py
new file mode 100644
index 00000000000000..54e7610c4e5442
--- /dev/null
+++ b/tests/components/netatmo/common.py
@@ -0,0 +1,70 @@
+"""Common methods used across tests for Netatmo."""
+import json
+
+from homeassistant.components.webhook import async_handle_webhook
+from homeassistant.util.aiohttp import MockRequest
+
+from tests.common import load_fixture
+
+CLIENT_ID = "1234"
+CLIENT_SECRET = "5678"
+ALL_SCOPES = [
+ "read_station",
+ "read_camera",
+ "access_camera",
+ "write_camera",
+ "read_presence",
+ "access_presence",
+ "write_presence",
+ "read_homecoach",
+ "read_smokedetector",
+ "read_thermostat",
+ "write_thermostat",
+]
+
+COMMON_RESPONSE = {
+ "user_id": "91763b24c43d3e344f424e8d",
+ "home_id": "91763b24c43d3e344f424e8b",
+ "home_name": "MYHOME",
+ "user": {"id": "91763b24c43d3e344f424e8b", "email": "john@doe.com"},
+}
+
+TEST_TIME = 1559347200.0
+
+FAKE_WEBHOOK_ACTIVATION = {
+ "push_type": "webhook_activation",
+}
+
+
+def fake_post_request(**args):
+ """Return fake data."""
+ if "url" not in args:
+ return "{}"
+
+ endpoint = args["url"].split("/")[-1]
+ if endpoint in [
+ "setpersonsaway",
+ "setpersonshome",
+ "setstate",
+ "setroomthermpoint",
+ "setthermmode",
+ "switchhomeschedule",
+ ]:
+ return f'{{"{endpoint}": true}}'
+
+ return json.loads(load_fixture(f"netatmo/{endpoint}.json"))
+
+
+def fake_post_request_no_data(**args):
+ """Fake error during requesting backend data."""
+ return "{}"
+
+
+async def simulate_webhook(hass, webhook_id, response):
+ """Simulate a webhook event."""
+ request = MockRequest(
+ content=bytes(json.dumps({**COMMON_RESPONSE, **response}), "utf-8"),
+ mock_source="test",
+ )
+ await async_handle_webhook(hass, webhook_id, request)
+ await hass.async_block_till_done()
diff --git a/tests/components/netatmo/conftest.py b/tests/components/netatmo/conftest.py
new file mode 100644
index 00000000000000..9a16391d2a429c
--- /dev/null
+++ b/tests/components/netatmo/conftest.py
@@ -0,0 +1,134 @@
+"""Provide common Netatmo fixtures."""
+from contextlib import contextmanager
+from time import time
+from unittest.mock import patch
+
+import pytest
+
+from .common import ALL_SCOPES, TEST_TIME, fake_post_request, fake_post_request_no_data
+
+from tests.common import MockConfigEntry
+
+
+@pytest.fixture(name="config_entry")
+async def mock_config_entry_fixture(hass):
+ """Mock a config entry."""
+ mock_entry = MockConfigEntry(
+ domain="netatmo",
+ data={
+ "auth_implementation": "cloud",
+ "token": {
+ "refresh_token": "mock-refresh-token",
+ "access_token": "mock-access-token",
+ "type": "Bearer",
+ "expires_in": 60,
+ "expires_at": time() + 1000,
+ "scope": " ".join(ALL_SCOPES),
+ },
+ },
+ options={
+ "weather_areas": {
+ "Home avg": {
+ "lat_ne": 32.2345678,
+ "lon_ne": -117.1234567,
+ "lat_sw": 32.1234567,
+ "lon_sw": -117.2345678,
+ "show_on_map": False,
+ "area_name": "Home avg",
+ "mode": "avg",
+ },
+ "Home max": {
+ "lat_ne": 32.2345678,
+ "lon_ne": -117.1234567,
+ "lat_sw": 32.1234567,
+ "lon_sw": -117.2345678,
+ "show_on_map": True,
+ "area_name": "Home max",
+ "mode": "max",
+ },
+ }
+ },
+ )
+ mock_entry.add_to_hass(hass)
+
+ return mock_entry
+
+
+@contextmanager
+def selected_platforms(platforms=["camera", "climate", "light", "sensor"]):
+ """Restrict loaded platforms to list given."""
+ with patch("homeassistant.components.netatmo.PLATFORMS", platforms), patch(
+ "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth"
+ ) as mock_auth, patch(
+ "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
+ ), patch(
+ "homeassistant.components.webhook.async_generate_url"
+ ):
+ mock_auth.return_value.post_request.side_effect = fake_post_request
+ yield
+
+
+@pytest.fixture(name="entry")
+async def mock_entry_fixture(hass, config_entry):
+ """Mock setup of all platforms."""
+ with selected_platforms():
+ await hass.config_entries.async_setup(config_entry.entry_id)
+
+ await hass.async_block_till_done()
+ return config_entry
+
+
+@pytest.fixture(name="sensor_entry")
+async def mock_sensor_entry_fixture(hass, config_entry):
+ """Mock setup of sensor platform."""
+ with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ yield config_entry
+
+
+@pytest.fixture(name="camera_entry")
+async def mock_camera_entry_fixture(hass, config_entry):
+ """Mock setup of camera platform."""
+ with selected_platforms(["camera"]):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+
+ await hass.async_block_till_done()
+ return config_entry
+
+
+@pytest.fixture(name="light_entry")
+async def mock_light_entry_fixture(hass, config_entry):
+ """Mock setup of light platform."""
+ with selected_platforms(["light"]):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+
+ await hass.async_block_till_done()
+ return config_entry
+
+
+@pytest.fixture(name="climate_entry")
+async def mock_climate_entry_fixture(hass, config_entry):
+ """Mock setup of climate platform."""
+ with selected_platforms(["climate"]):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+
+ await hass.async_block_till_done()
+ return config_entry
+
+
+@pytest.fixture(name="entry_error")
+async def mock_entry_error_fixture(hass, config_entry):
+ """Mock erroneous setup of platforms."""
+ with patch(
+ "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth"
+ ) as mock_auth, patch(
+ "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
+ ), patch(
+ "homeassistant.components.webhook.async_generate_url"
+ ):
+ mock_auth.return_value.post_request.side_effect = fake_post_request_no_data
+ await hass.config_entries.async_setup(config_entry.entry_id)
+
+ await hass.async_block_till_done()
+ yield config_entry
diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py
new file mode 100644
index 00000000000000..372af748267fff
--- /dev/null
+++ b/tests/components/netatmo/test_camera.py
@@ -0,0 +1,288 @@
+"""The tests for Netatmo camera."""
+from datetime import timedelta
+from unittest.mock import patch
+
+from homeassistant.components import camera
+from homeassistant.components.camera import STATE_STREAMING
+from homeassistant.components.netatmo.const import (
+ NETATMO_EVENT,
+ SERVICE_SET_CAMERA_LIGHT,
+ SERVICE_SET_PERSON_AWAY,
+ SERVICE_SET_PERSONS_HOME,
+)
+from homeassistant.const import CONF_WEBHOOK_ID
+from homeassistant.util import dt
+
+from .common import fake_post_request, simulate_webhook
+
+from tests.common import async_capture_events, async_fire_time_changed
+
+
+async def test_setup_component_with_webhook(hass, camera_entry):
+ """Test setup with webhook."""
+ webhook_id = camera_entry.data[CONF_WEBHOOK_ID]
+ await hass.async_block_till_done()
+
+ camera_entity_indoor = "camera.netatmo_hall"
+ camera_entity_outdoor = "camera.netatmo_garden"
+ assert hass.states.get(camera_entity_indoor).state == "streaming"
+ response = {
+ "event_type": "off",
+ "device_id": "12:34:56:00:f1:62",
+ "camera_id": "12:34:56:00:f1:62",
+ "event_id": "601dce1560abca1ebad9b723",
+ "push_type": "NACamera-off",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(camera_entity_indoor).state == "idle"
+
+ response = {
+ "event_type": "on",
+ "device_id": "12:34:56:00:f1:62",
+ "camera_id": "12:34:56:00:f1:62",
+ "event_id": "646227f1dc0dfa000ec5f350",
+ "push_type": "NACamera-on",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(camera_entity_indoor).state == "streaming"
+
+ response = {
+ "event_type": "light_mode",
+ "device_id": "12:34:56:00:a5:a4",
+ "camera_id": "12:34:56:00:a5:a4",
+ "event_id": "601dce1560abca1ebad9b723",
+ "push_type": "NOC-light_mode",
+ "sub_type": "on",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(camera_entity_indoor).state == "streaming"
+ assert hass.states.get(camera_entity_outdoor).attributes["light_state"] == "on"
+
+ response = {
+ "event_type": "light_mode",
+ "device_id": "12:34:56:00:a5:a4",
+ "camera_id": "12:34:56:00:a5:a4",
+ "event_id": "601dce1560abca1ebad9b723",
+ "push_type": "NOC-light_mode",
+ "sub_type": "auto",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(camera_entity_outdoor).attributes["light_state"] == "auto"
+
+ response = {
+ "event_type": "light_mode",
+ "device_id": "12:34:56:00:a5:a4",
+ "event_id": "601dce1560abca1ebad9b723",
+ "push_type": "NOC-light_mode",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(camera_entity_indoor).state == "streaming"
+ assert hass.states.get(camera_entity_outdoor).attributes["light_state"] == "auto"
+
+
+IMAGE_BYTES_FROM_STREAM = b"test stream image bytes"
+
+
+async def test_camera_image_local(hass, camera_entry, requests_mock):
+ """Test retrieval or local camera image."""
+ await hass.async_block_till_done()
+
+ uri = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d"
+ stream_uri = uri + "/live/files/high/index.m3u8"
+ camera_entity_indoor = "camera.netatmo_hall"
+ cam = hass.states.get(camera_entity_indoor)
+
+ assert cam is not None
+ assert cam.state == STATE_STREAMING
+
+ stream_source = await camera.async_get_stream_source(hass, camera_entity_indoor)
+ assert stream_source == stream_uri
+
+ requests_mock.get(
+ uri + "/live/snapshot_720.jpg",
+ content=IMAGE_BYTES_FROM_STREAM,
+ )
+ image = await camera.async_get_image(hass, camera_entity_indoor)
+ assert image.content == IMAGE_BYTES_FROM_STREAM
+
+
+async def test_camera_image_vpn(hass, camera_entry, requests_mock):
+ """Test retrieval of remote camera image."""
+ await hass.async_block_till_done()
+
+ uri = (
+ "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/"
+ "6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTw,,"
+ )
+ stream_uri = uri + "/live/files/high/index.m3u8"
+ camera_entity_indoor = "camera.netatmo_garden"
+ cam = hass.states.get(camera_entity_indoor)
+
+ assert cam is not None
+ assert cam.state == STATE_STREAMING
+
+ stream_source = await camera.async_get_stream_source(hass, camera_entity_indoor)
+ assert stream_source == stream_uri
+
+ requests_mock.get(
+ uri + "/live/snapshot_720.jpg",
+ content=IMAGE_BYTES_FROM_STREAM,
+ )
+ image = await camera.async_get_image(hass, camera_entity_indoor)
+ assert image.content == IMAGE_BYTES_FROM_STREAM
+
+
+async def test_service_set_person_away(hass, camera_entry):
+ """Test service to set person as away."""
+ await hass.async_block_till_done()
+
+ data = {
+ "entity_id": "camera.netatmo_hall",
+ "person": "Richard Doe",
+ }
+
+ with patch("pyatmo.camera.CameraData.set_persons_away") as mock_set_persons_away:
+ await hass.services.async_call(
+ "netatmo", SERVICE_SET_PERSON_AWAY, service_data=data
+ )
+ await hass.async_block_till_done()
+ mock_set_persons_away.assert_called_once_with(
+ person_id="91827376-7e04-5298-83af-a0cb8372dff3",
+ home_id="91763b24c43d3e344f424e8b",
+ )
+
+ data = {
+ "entity_id": "camera.netatmo_hall",
+ }
+
+ with patch("pyatmo.camera.CameraData.set_persons_away") as mock_set_persons_away:
+ await hass.services.async_call(
+ "netatmo", SERVICE_SET_PERSON_AWAY, service_data=data
+ )
+ await hass.async_block_till_done()
+ mock_set_persons_away.assert_called_once_with(
+ person_id=None,
+ home_id="91763b24c43d3e344f424e8b",
+ )
+
+
+async def test_service_set_persons_home(hass, camera_entry):
+ """Test service to set persons as home."""
+ await hass.async_block_till_done()
+
+ data = {
+ "entity_id": "camera.netatmo_hall",
+ "persons": "John Doe",
+ }
+
+ with patch("pyatmo.camera.CameraData.set_persons_home") as mock_set_persons_home:
+ await hass.services.async_call(
+ "netatmo", SERVICE_SET_PERSONS_HOME, service_data=data
+ )
+ await hass.async_block_till_done()
+ mock_set_persons_home.assert_called_once_with(
+ person_ids=["91827374-7e04-5298-83ad-a0cb8372dff1"],
+ home_id="91763b24c43d3e344f424e8b",
+ )
+
+
+async def test_service_set_camera_light(hass, camera_entry):
+ """Test service to set the outdoor camera light mode."""
+ await hass.async_block_till_done()
+
+ data = {
+ "entity_id": "camera.netatmo_garden",
+ "camera_light_mode": "on",
+ }
+
+ with patch("pyatmo.camera.CameraData.set_state") as mock_set_state:
+ await hass.services.async_call(
+ "netatmo", SERVICE_SET_CAMERA_LIGHT, service_data=data
+ )
+ await hass.async_block_till_done()
+ mock_set_state.assert_called_once_with(
+ home_id="91763b24c43d3e344f424e8b",
+ camera_id="12:34:56:00:a5:a4",
+ floodlight="on",
+ )
+
+
+async def test_camera_reconnect_webhook(hass, config_entry):
+ """Test webhook event on camera reconnect."""
+ with patch(
+ "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth.post_request"
+ ) as mock_post, patch(
+ "homeassistant.components.netatmo.PLATFORMS", ["camera"]
+ ), patch(
+ "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
+ ), patch(
+ "homeassistant.components.webhook.async_generate_url"
+ ) as mock_webhook:
+ mock_post.side_effect = fake_post_request
+ mock_webhook.return_value = "https://example.com"
+ await hass.config_entries.async_setup(config_entry.entry_id)
+
+ await hass.async_block_till_done()
+
+ webhook_id = config_entry.data[CONF_WEBHOOK_ID]
+
+ # Fake webhook activation
+ response = {
+ "push_type": "webhook_activation",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+ await hass.async_block_till_done()
+
+ mock_post.assert_called()
+ mock_post.reset_mock()
+
+ # Fake camera reconnect
+ response = {
+ "push_type": "NACamera-connection",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+ await hass.async_block_till_done()
+
+ async_fire_time_changed(
+ hass,
+ dt.utcnow() + timedelta(seconds=60),
+ )
+ await hass.async_block_till_done()
+ mock_post.assert_called()
+
+
+async def test_webhook_person_event(hass, camera_entry):
+ """Test that person events are handled."""
+ test_netatmo_event = async_capture_events(hass, NETATMO_EVENT)
+ assert not test_netatmo_event
+
+ fake_webhook_event = {
+ "persons": [
+ {
+ "id": "91827374-7e04-5298-83ad-a0cb8372dff1",
+ "face_id": "a1b2c3d4e5",
+ "face_key": "9876543",
+ "is_known": True,
+ "face_url": "https://netatmocameraimage.blob.core.windows.net/production/12345",
+ }
+ ],
+ "snapshot_id": "123456789abc",
+ "snapshot_key": "foobar123",
+ "snapshot_url": "https://netatmocameraimage.blob.core.windows.net/production/12346",
+ "event_type": "person",
+ "camera_id": "12:34:56:00:f1:62",
+ "device_id": "12:34:56:00:f1:62",
+ "event_id": "1234567890",
+ "message": "MYHOME: John Doe has been seen by Indoor Camera ",
+ "push_type": "NACamera-person",
+ }
+
+ webhook_id = camera_entry.data[CONF_WEBHOOK_ID]
+ await simulate_webhook(hass, webhook_id, fake_webhook_event)
+
+ assert test_netatmo_event
diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py
new file mode 100644
index 00000000000000..ecec2871df895a
--- /dev/null
+++ b/tests/components/netatmo/test_climate.py
@@ -0,0 +1,756 @@
+"""The tests for the Netatmo climate platform."""
+from unittest.mock import Mock, patch
+
+import pytest
+
+from homeassistant.components.climate import (
+ DOMAIN as CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ SERVICE_SET_PRESET_MODE,
+ SERVICE_SET_TEMPERATURE,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+)
+from homeassistant.components.climate.const import (
+ ATTR_HVAC_MODE,
+ ATTR_PRESET_MODE,
+ HVAC_MODE_AUTO,
+ HVAC_MODE_HEAT,
+ HVAC_MODE_OFF,
+ PRESET_AWAY,
+ PRESET_BOOST,
+)
+from homeassistant.components.netatmo import climate
+from homeassistant.components.netatmo.climate import (
+ NA_THERM,
+ NA_VALVE,
+ PRESET_FROST_GUARD,
+ PRESET_SCHEDULE,
+)
+from homeassistant.components.netatmo.const import (
+ ATTR_SCHEDULE_NAME,
+ SERVICE_SET_SCHEDULE,
+)
+from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_WEBHOOK_ID
+
+from .common import simulate_webhook
+
+
+async def test_webhook_event_handling_thermostats(hass, climate_entry):
+ """Test service and webhook event handling with thermostats."""
+ webhook_id = climate_entry.data[CONF_WEBHOOK_ID]
+ climate_entity_livingroom = "climate.netatmo_livingroom"
+
+ assert hass.states.get(climate_entity_livingroom).state == "auto"
+ assert (
+ hass.states.get(climate_entity_livingroom).attributes["preset_mode"]
+ == "Schedule"
+ )
+ assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 12
+
+ # Test service setting the temperature
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {ATTR_ENTITY_ID: climate_entity_livingroom, ATTR_TEMPERATURE: 21},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ # Fake webhook thermostat manual set point
+ response = {
+ "room_id": "2746182631",
+ "home": {
+ "id": "91763b24c43d3e344f424e8b",
+ "name": "MYHOME",
+ "country": "DE",
+ "rooms": [
+ {
+ "id": "2746182631",
+ "name": "Livingroom",
+ "type": "livingroom",
+ "therm_setpoint_mode": "manual",
+ "therm_setpoint_temperature": 21,
+ "therm_setpoint_end_time": 1612734552,
+ }
+ ],
+ "modules": [
+ {"id": "12:34:56:00:01:ae", "name": "Livingroom", "type": "NATherm1"}
+ ],
+ },
+ "mode": "manual",
+ "event_type": "set_point",
+ "temperature": 21,
+ "push_type": "display_change",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(climate_entity_livingroom).state == "heat"
+ assert (
+ hass.states.get(climate_entity_livingroom).attributes["preset_mode"]
+ == "Schedule"
+ )
+ assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 21
+
+ # Test service setting the HVAC mode to "heat"
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: climate_entity_livingroom, ATTR_HVAC_MODE: HVAC_MODE_HEAT},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ # Fake webhook thermostat mode change to "Max"
+ response = {
+ "room_id": "2746182631",
+ "home": {
+ "id": "91763b24c43d3e344f424e8b",
+ "name": "MYHOME",
+ "country": "DE",
+ "rooms": [
+ {
+ "id": "2746182631",
+ "name": "Livingroom",
+ "type": "livingroom",
+ "therm_setpoint_mode": "max",
+ "therm_setpoint_end_time": 1612749189,
+ }
+ ],
+ "modules": [
+ {"id": "12:34:56:00:01:ae", "name": "Livingroom", "type": "NATherm1"}
+ ],
+ },
+ "mode": "max",
+ "event_type": "set_point",
+ "push_type": "display_change",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(climate_entity_livingroom).state == "heat"
+ assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 30
+
+ # Test service setting the HVAC mode to "off"
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: climate_entity_livingroom, ATTR_HVAC_MODE: HVAC_MODE_OFF},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ # Fake webhook turn thermostat off
+ response = {
+ "home": {
+ "id": "91763b24c43d3e344f424e8b",
+ "name": "MYHOME",
+ "country": "DE",
+ "rooms": [
+ {
+ "id": "2746182631",
+ "name": "Livingroom",
+ "type": "livingroom",
+ "therm_setpoint_mode": "off",
+ }
+ ],
+ "modules": [
+ {"id": "12:34:56:00:01:ae", "name": "Livingroom", "type": "NATherm1"}
+ ],
+ },
+ "mode": "off",
+ "event_type": "set_point",
+ "push_type": "display_change",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(climate_entity_livingroom).state == "off"
+
+ # Test service setting the HVAC mode to "auto"
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: climate_entity_livingroom, ATTR_HVAC_MODE: HVAC_MODE_AUTO},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ # Fake webhook thermostat mode cancel set point
+ response = {
+ "room_id": "2746182631",
+ "home": {
+ "id": "91763b24c43d3e344f424e8b",
+ "name": "MYHOME",
+ "country": "DE",
+ "rooms": [
+ {
+ "id": "2746182631",
+ "name": "Livingroom",
+ "type": "livingroom",
+ "therm_setpoint_mode": "home",
+ }
+ ],
+ "modules": [
+ {"id": "12:34:56:00:01:ae", "name": "Livingroom", "type": "NATherm1"}
+ ],
+ },
+ "mode": "home",
+ "event_type": "cancel_set_point",
+ "push_type": "display_change",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(climate_entity_livingroom).state == "auto"
+ assert (
+ hass.states.get(climate_entity_livingroom).attributes["preset_mode"]
+ == "Schedule"
+ )
+
+
+async def test_service_preset_mode_frost_guard_thermostat(hass, climate_entry):
+ """Test service with frost guard preset for thermostats."""
+ webhook_id = climate_entry.data[CONF_WEBHOOK_ID]
+ climate_entity_livingroom = "climate.netatmo_livingroom"
+
+ assert hass.states.get(climate_entity_livingroom).state == "auto"
+ assert (
+ hass.states.get(climate_entity_livingroom).attributes["preset_mode"]
+ == "Schedule"
+ )
+
+ # Test service setting the preset mode to "frost guard"
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {
+ ATTR_ENTITY_ID: climate_entity_livingroom,
+ ATTR_PRESET_MODE: PRESET_FROST_GUARD,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ # Fake webhook thermostat mode change to "Frost Guard"
+ response = {
+ "event_type": "therm_mode",
+ "home": {"id": "91763b24c43d3e344f424e8b", "therm_mode": "hg"},
+ "mode": "hg",
+ "previous_mode": "schedule",
+ "push_type": "home_event_changed",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(climate_entity_livingroom).state == "auto"
+ assert (
+ hass.states.get(climate_entity_livingroom).attributes["preset_mode"]
+ == "Frost Guard"
+ )
+
+ # Test service setting the preset mode to "frost guard"
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {
+ ATTR_ENTITY_ID: climate_entity_livingroom,
+ ATTR_PRESET_MODE: PRESET_SCHEDULE,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ # Test webhook thermostat mode change to "Schedule"
+ response = {
+ "event_type": "therm_mode",
+ "home": {"id": "91763b24c43d3e344f424e8b", "therm_mode": "schedule"},
+ "mode": "schedule",
+ "previous_mode": "hg",
+ "push_type": "home_event_changed",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(climate_entity_livingroom).state == "auto"
+ assert (
+ hass.states.get(climate_entity_livingroom).attributes["preset_mode"]
+ == "Schedule"
+ )
+
+
+async def test_service_preset_modes_thermostat(hass, climate_entry):
+ """Test service with preset modes for thermostats."""
+ webhook_id = climate_entry.data[CONF_WEBHOOK_ID]
+ climate_entity_livingroom = "climate.netatmo_livingroom"
+
+ assert hass.states.get(climate_entity_livingroom).state == "auto"
+ assert (
+ hass.states.get(climate_entity_livingroom).attributes["preset_mode"]
+ == "Schedule"
+ )
+
+ # Test service setting the preset mode to "away"
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: climate_entity_livingroom, ATTR_PRESET_MODE: PRESET_AWAY},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ # Fake webhook thermostat mode change to "Away"
+ response = {
+ "event_type": "therm_mode",
+ "home": {"id": "91763b24c43d3e344f424e8b", "therm_mode": "away"},
+ "mode": "away",
+ "previous_mode": "schedule",
+ "push_type": "home_event_changed",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(climate_entity_livingroom).state == "auto"
+ assert (
+ hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away"
+ )
+
+ # Test service setting the preset mode to "boost"
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: climate_entity_livingroom, ATTR_PRESET_MODE: PRESET_BOOST},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ # Test webhook thermostat mode change to "Max"
+ response = {
+ "room_id": "2746182631",
+ "home": {
+ "id": "91763b24c43d3e344f424e8b",
+ "name": "MYHOME",
+ "country": "DE",
+ "rooms": [
+ {
+ "id": "2746182631",
+ "name": "Livingroom",
+ "type": "livingroom",
+ "therm_setpoint_mode": "max",
+ "therm_setpoint_end_time": 1612749189,
+ }
+ ],
+ "modules": [
+ {"id": "12:34:56:00:01:ae", "name": "Livingroom", "type": "NATherm1"}
+ ],
+ },
+ "mode": "max",
+ "event_type": "set_point",
+ "push_type": "display_change",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(climate_entity_livingroom).state == "heat"
+ assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 30
+
+
+async def test_webhook_event_handling_no_data(hass, climate_entry):
+ """Test service and webhook event handling with erroneous data."""
+ # Test webhook without home entry
+ webhook_id = climate_entry.data[CONF_WEBHOOK_ID]
+
+ response = {
+ "push_type": "home_event_changed",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ # Test webhook with different home id
+ response = {
+ "home_id": "3d3e344f491763b24c424e8b",
+ "room_id": "2746182631",
+ "home": {
+ "id": "3d3e344f491763b24c424e8b",
+ "name": "MYHOME",
+ "country": "DE",
+ "rooms": [],
+ "modules": [],
+ },
+ "mode": "home",
+ "event_type": "cancel_set_point",
+ "push_type": "display_change",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ # Test webhook without room entries
+ response = {
+ "room_id": "2746182631",
+ "home": {
+ "id": "91763b24c43d3e344f424e8b",
+ "name": "MYHOME",
+ "country": "DE",
+ "rooms": [],
+ "modules": [],
+ },
+ "mode": "home",
+ "event_type": "cancel_set_point",
+ "push_type": "display_change",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+
+async def test_service_schedule_thermostats(hass, climate_entry, caplog):
+ """Test service for selecting Netatmo schedule with thermostats."""
+ webhook_id = climate_entry.data[CONF_WEBHOOK_ID]
+ climate_entity_livingroom = "climate.netatmo_livingroom"
+
+ # Test setting a valid schedule
+ with patch(
+ "pyatmo.thermostat.HomeData.switch_home_schedule"
+ ) as mock_switch_home_schedule:
+ await hass.services.async_call(
+ "netatmo",
+ SERVICE_SET_SCHEDULE,
+ {ATTR_ENTITY_ID: climate_entity_livingroom, ATTR_SCHEDULE_NAME: "Winter"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_switch_home_schedule.assert_called_once_with(
+ home_id="91763b24c43d3e344f424e8b", schedule_id="b1b54a2f45795764f59d50d8"
+ )
+
+ # Fake backend response for valve being turned on
+ response = {
+ "event_type": "schedule",
+ "schedule_id": "b1b54a2f45795764f59d50d8",
+ "previous_schedule_id": "59d32176d183948b05ab4dce",
+ "push_type": "home_event_changed",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert (
+ hass.states.get(climate_entity_livingroom).attributes["selected_schedule"]
+ == "Winter"
+ )
+
+ # Test setting an invalid schedule
+ with patch(
+ "pyatmo.thermostat.HomeData.switch_home_schedule"
+ ) as mock_switch_home_schedule:
+ await hass.services.async_call(
+ "netatmo",
+ SERVICE_SET_SCHEDULE,
+ {ATTR_ENTITY_ID: climate_entity_livingroom, ATTR_SCHEDULE_NAME: "summer"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_switch_home_schedule.assert_not_called()
+
+ assert "summer is not a invalid schedule" in caplog.text
+
+
+async def test_service_preset_mode_already_boost_valves(hass, climate_entry):
+ """Test service with boost preset for valves when already in boost mode."""
+ webhook_id = climate_entry.data[CONF_WEBHOOK_ID]
+ climate_entity_entrada = "climate.netatmo_entrada"
+
+ assert hass.states.get(climate_entity_entrada).state == "auto"
+ assert (
+ hass.states.get(climate_entity_entrada).attributes["preset_mode"]
+ == "Frost Guard"
+ )
+ assert hass.states.get(climate_entity_entrada).attributes["temperature"] == 7
+
+ # Test webhook valve mode change to "Max"
+ response = {
+ "room_id": "2833524037",
+ "home": {
+ "id": "91763b24c43d3e344f424e8b",
+ "name": "MYHOME",
+ "country": "DE",
+ "rooms": [
+ {
+ "id": "2833524037",
+ "name": "Entrada",
+ "type": "lobby",
+ "therm_setpoint_mode": "max",
+ "therm_setpoint_end_time": 1612749189,
+ }
+ ],
+ "modules": [{"id": "12:34:56:00:01:ae", "name": "Entrada", "type": "NRV"}],
+ },
+ "mode": "max",
+ "event_type": "set_point",
+ "push_type": "display_change",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ # Test service setting the preset mode to "boost"
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: climate_entity_entrada, ATTR_PRESET_MODE: PRESET_BOOST},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ # Test webhook valve mode change to "Max"
+ response = {
+ "room_id": "2833524037",
+ "home": {
+ "id": "91763b24c43d3e344f424e8b",
+ "name": "MYHOME",
+ "country": "DE",
+ "rooms": [
+ {
+ "id": "2833524037",
+ "name": "Entrada",
+ "type": "lobby",
+ "therm_setpoint_mode": "max",
+ "therm_setpoint_end_time": 1612749189,
+ }
+ ],
+ "modules": [{"id": "12:34:56:00:01:ae", "name": "Entrada", "type": "NRV"}],
+ },
+ "mode": "max",
+ "event_type": "set_point",
+ "push_type": "display_change",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(climate_entity_entrada).state == "heat"
+ assert hass.states.get(climate_entity_entrada).attributes["temperature"] == 30
+
+
+async def test_service_preset_mode_boost_valves(hass, climate_entry):
+ """Test service with boost preset for valves."""
+ webhook_id = climate_entry.data[CONF_WEBHOOK_ID]
+ climate_entity_entrada = "climate.netatmo_entrada"
+
+ # Test service setting the preset mode to "boost"
+ assert hass.states.get(climate_entity_entrada).state == "auto"
+ assert hass.states.get(climate_entity_entrada).attributes["temperature"] == 7
+
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: climate_entity_entrada, ATTR_PRESET_MODE: PRESET_BOOST},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ # Fake backend response
+ response = {
+ "room_id": "2833524037",
+ "home": {
+ "id": "91763b24c43d3e344f424e8b",
+ "name": "MYHOME",
+ "country": "DE",
+ "rooms": [
+ {
+ "id": "2833524037",
+ "name": "Entrada",
+ "type": "lobby",
+ "therm_setpoint_mode": "max",
+ "therm_setpoint_end_time": 1612749189,
+ }
+ ],
+ "modules": [{"id": "12:34:56:00:01:ae", "name": "Entrada", "type": "NRV"}],
+ },
+ "mode": "max",
+ "event_type": "set_point",
+ "push_type": "display_change",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(climate_entity_entrada).state == "heat"
+ assert hass.states.get(climate_entity_entrada).attributes["temperature"] == 30
+
+
+async def test_service_preset_mode_invalid(hass, climate_entry, caplog):
+ """Test service with invalid preset."""
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: "climate.netatmo_cocina", ATTR_PRESET_MODE: "invalid"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ assert "Preset mode 'invalid' not available" in caplog.text
+
+
+async def test_valves_service_turn_off(hass, climate_entry):
+ """Test service turn off for valves."""
+ webhook_id = climate_entry.data[CONF_WEBHOOK_ID]
+ climate_entity_entrada = "climate.netatmo_entrada"
+
+ # Test turning valve off
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: climate_entity_entrada},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ # Fake backend response for valve being turned off
+ response = {
+ "room_id": "2833524037",
+ "home": {
+ "id": "91763b24c43d3e344f424e8b",
+ "name": "MYHOME",
+ "country": "DE",
+ "rooms": [
+ {
+ "id": "2833524037",
+ "name": "Entrada",
+ "type": "lobby",
+ "therm_setpoint_mode": "off",
+ }
+ ],
+ "modules": [{"id": "12:34:56:00:01:ae", "name": "Entrada", "type": "NRV"}],
+ },
+ "mode": "off",
+ "event_type": "set_point",
+ "push_type": "display_change",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(climate_entity_entrada).state == "off"
+
+
+async def test_valves_service_turn_on(hass, climate_entry):
+ """Test service turn on for valves."""
+ webhook_id = climate_entry.data[CONF_WEBHOOK_ID]
+ climate_entity_entrada = "climate.netatmo_entrada"
+
+ # Test turning valve on
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: climate_entity_entrada},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ # Fake backend response for valve being turned on
+ response = {
+ "room_id": "2833524037",
+ "home": {
+ "id": "91763b24c43d3e344f424e8b",
+ "name": "MYHOME",
+ "country": "DE",
+ "rooms": [
+ {
+ "id": "2833524037",
+ "name": "Entrada",
+ "type": "lobby",
+ "therm_setpoint_mode": "home",
+ }
+ ],
+ "modules": [{"id": "12:34:56:00:01:ae", "name": "Entrada", "type": "NRV"}],
+ },
+ "mode": "home",
+ "event_type": "cancel_set_point",
+ "push_type": "display_change",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(climate_entity_entrada).state == "auto"
+
+
+@pytest.mark.parametrize(
+ "batterylevel, module_type, expected",
+ [
+ (4101, NA_THERM, 100),
+ (3601, NA_THERM, 80),
+ (3450, NA_THERM, 65),
+ (3301, NA_THERM, 50),
+ (3001, NA_THERM, 20),
+ (2799, NA_THERM, 0),
+ (3201, NA_VALVE, 100),
+ (2701, NA_VALVE, 80),
+ (2550, NA_VALVE, 65),
+ (2401, NA_VALVE, 50),
+ (2201, NA_VALVE, 20),
+ (2001, NA_VALVE, 0),
+ ],
+)
+async def test_interpolate(batterylevel, module_type, expected):
+ """Test interpolation of battery levels depending on device type."""
+ assert climate.interpolate(batterylevel, module_type) == expected
+
+
+async def test_get_all_home_ids():
+ """Test extracting all home ids returned by NetAtmo API."""
+ # Test with backend returning no data
+ assert climate.get_all_home_ids(None) == []
+
+ # Test with fake data
+ home_data = Mock()
+ home_data.homes = {
+ "123": {"id": "123", "name": "Home 1", "modules": [], "therm_schedules": []},
+ "987": {"id": "987", "name": "Home 2", "modules": [], "therm_schedules": []},
+ }
+ expected = ["123", "987"]
+ assert climate.get_all_home_ids(home_data) == expected
+
+
+async def test_webhook_home_id_mismatch(hass, climate_entry):
+ """Test service turn on for valves."""
+ webhook_id = climate_entry.data[CONF_WEBHOOK_ID]
+ climate_entity_entrada = "climate.netatmo_entrada"
+
+ assert hass.states.get(climate_entity_entrada).state == "auto"
+
+ # Fake backend response for valve being turned on
+ response = {
+ "room_id": "2833524037",
+ "home": {
+ "id": "123",
+ "name": "MYHOME",
+ "country": "DE",
+ "rooms": [
+ {
+ "id": "2833524037",
+ "name": "Entrada",
+ "type": "lobby",
+ "therm_setpoint_mode": "home",
+ }
+ ],
+ "modules": [{"id": "12:34:56:00:01:ae", "name": "Entrada", "type": "NRV"}],
+ },
+ "mode": "home",
+ "event_type": "cancel_set_point",
+ "push_type": "display_change",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(climate_entity_entrada).state == "auto"
+
+
+async def test_webhook_set_point(hass, climate_entry):
+ """Test service turn on for valves."""
+ webhook_id = climate_entry.data[CONF_WEBHOOK_ID]
+ climate_entity_entrada = "climate.netatmo_entrada"
+
+ # Fake backend response for valve being turned on
+ response = {
+ "room_id": "2746182631",
+ "home": {
+ "id": "91763b24c43d3e344f424e8b",
+ "name": "MYHOME",
+ "country": "DE",
+ "rooms": [
+ {
+ "id": "2833524037",
+ "name": "Entrada",
+ "type": "lobby",
+ "therm_setpoint_mode": "home",
+ "therm_setpoint_temperature": 30,
+ }
+ ],
+ "modules": [{"id": "12:34:56:00:01:ae", "name": "Entrada", "type": "NRV"}],
+ },
+ "mode": "home",
+ "event_type": "set_point",
+ "temperature": 21,
+ "push_type": "display_change",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(climate_entity_entrada).state == "heat"
diff --git a/tests/components/netatmo/test_device_trigger.py b/tests/components/netatmo/test_device_trigger.py
new file mode 100644
index 00000000000000..7e014d2648f9f9
--- /dev/null
+++ b/tests/components/netatmo/test_device_trigger.py
@@ -0,0 +1,311 @@
+"""The tests for Netatmo device triggers."""
+import pytest
+
+import homeassistant.components.automation as automation
+from homeassistant.components.netatmo import DOMAIN as NETATMO_DOMAIN
+from homeassistant.components.netatmo.const import (
+ CLIMATE_TRIGGERS,
+ INDOOR_CAMERA_TRIGGERS,
+ MODEL_NACAMERA,
+ MODEL_NAPLUG,
+ MODEL_NATHERM1,
+ MODEL_NOC,
+ MODEL_NRV,
+ NETATMO_EVENT,
+ OUTDOOR_CAMERA_TRIGGERS,
+)
+from homeassistant.components.netatmo.device_trigger import SUBTYPES
+from homeassistant.const import ATTR_DEVICE_ID
+from homeassistant.helpers import device_registry
+from homeassistant.setup import async_setup_component
+
+from tests.common import (
+ MockConfigEntry,
+ assert_lists_same,
+ async_capture_events,
+ async_get_device_automations,
+ async_mock_service,
+ mock_device_registry,
+ mock_registry,
+)
+
+
+@pytest.fixture
+def device_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_device_registry(hass)
+
+
+@pytest.fixture
+def entity_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_registry(hass)
+
+
+@pytest.fixture
+def calls(hass):
+ """Track calls to a mock service."""
+ return async_mock_service(hass, "test", "automation")
+
+
+@pytest.mark.parametrize(
+ "platform,device_type,event_types",
+ [
+ ("camera", MODEL_NOC, OUTDOOR_CAMERA_TRIGGERS),
+ ("camera", MODEL_NACAMERA, INDOOR_CAMERA_TRIGGERS),
+ ("climate", MODEL_NRV, CLIMATE_TRIGGERS),
+ ("climate", MODEL_NATHERM1, CLIMATE_TRIGGERS),
+ ],
+)
+async def test_get_triggers(
+ hass, device_reg, entity_reg, platform, device_type, event_types
+):
+ """Test we get the expected triggers from a netatmo devices."""
+ config_entry = MockConfigEntry(domain=NETATMO_DOMAIN, data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ model=device_type,
+ )
+ entity_reg.async_get_or_create(
+ platform, NETATMO_DOMAIN, "5678", device_id=device_entry.id
+ )
+ expected_triggers = []
+ for event_type in event_types:
+ if event_type in SUBTYPES:
+ for subtype in SUBTYPES[event_type]:
+ expected_triggers.append(
+ {
+ "platform": "device",
+ "domain": NETATMO_DOMAIN,
+ "type": event_type,
+ "subtype": subtype,
+ "device_id": device_entry.id,
+ "entity_id": f"{platform}.{NETATMO_DOMAIN}_5678",
+ }
+ )
+ else:
+ expected_triggers.append(
+ {
+ "platform": "device",
+ "domain": NETATMO_DOMAIN,
+ "type": event_type,
+ "device_id": device_entry.id,
+ "entity_id": f"{platform}.{NETATMO_DOMAIN}_5678",
+ }
+ )
+ triggers = [
+ trigger
+ for trigger in await async_get_device_automations(
+ hass, "trigger", device_entry.id
+ )
+ if trigger["domain"] == NETATMO_DOMAIN
+ ]
+ assert_lists_same(triggers, expected_triggers)
+
+
+@pytest.mark.parametrize(
+ "platform,camera_type,event_type",
+ [("camera", MODEL_NOC, trigger) for trigger in OUTDOOR_CAMERA_TRIGGERS]
+ + [("camera", MODEL_NACAMERA, trigger) for trigger in INDOOR_CAMERA_TRIGGERS]
+ + [
+ ("climate", MODEL_NRV, trigger)
+ for trigger in CLIMATE_TRIGGERS
+ if trigger not in SUBTYPES
+ ]
+ + [
+ ("climate", MODEL_NATHERM1, trigger)
+ for trigger in CLIMATE_TRIGGERS
+ if trigger not in SUBTYPES
+ ],
+)
+async def test_if_fires_on_event(
+ hass, calls, device_reg, entity_reg, platform, camera_type, event_type
+):
+ """Test for event triggers firing."""
+ mac_address = "12:34:56:AB:CD:EF"
+ connection = (device_registry.CONNECTION_NETWORK_MAC, mac_address)
+ config_entry = MockConfigEntry(domain=NETATMO_DOMAIN, data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={connection},
+ identifiers={(NETATMO_DOMAIN, mac_address)},
+ model=camera_type,
+ )
+ entity_reg.async_get_or_create(
+ platform, NETATMO_DOMAIN, "5678", device_id=device_entry.id
+ )
+ events = async_capture_events(hass, "netatmo_event")
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": NETATMO_DOMAIN,
+ "device_id": device_entry.id,
+ "entity_id": f"{platform}.{NETATMO_DOMAIN}_5678",
+ "type": event_type,
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": (
+ "{{trigger.event.data.type}} - {{trigger.platform}} - {{trigger.event.data.device_id}}"
+ )
+ },
+ },
+ },
+ ]
+ },
+ )
+
+ device = device_reg.async_get_device(set(), {connection})
+ assert device is not None
+
+ # Fake that the entity is turning on.
+ hass.bus.async_fire(
+ event_type=NETATMO_EVENT,
+ event_data={
+ "type": event_type,
+ ATTR_DEVICE_ID: device.id,
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(events) == 1
+ assert len(calls) == 1
+ assert calls[0].data["some"] == f"{event_type} - device - {device.id}"
+
+
+@pytest.mark.parametrize(
+ "platform,camera_type,event_type,sub_type",
+ [
+ ("climate", MODEL_NRV, trigger, subtype)
+ for trigger in SUBTYPES
+ for subtype in SUBTYPES[trigger]
+ ]
+ + [
+ ("climate", MODEL_NATHERM1, trigger, subtype)
+ for trigger in SUBTYPES
+ for subtype in SUBTYPES[trigger]
+ ],
+)
+async def test_if_fires_on_event_with_subtype(
+ hass, calls, device_reg, entity_reg, platform, camera_type, event_type, sub_type
+):
+ """Test for event triggers firing."""
+ mac_address = "12:34:56:AB:CD:EF"
+ connection = (device_registry.CONNECTION_NETWORK_MAC, mac_address)
+ config_entry = MockConfigEntry(domain=NETATMO_DOMAIN, data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={connection},
+ identifiers={(NETATMO_DOMAIN, mac_address)},
+ model=camera_type,
+ )
+ entity_reg.async_get_or_create(
+ platform, NETATMO_DOMAIN, "5678", device_id=device_entry.id
+ )
+ events = async_capture_events(hass, "netatmo_event")
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": NETATMO_DOMAIN,
+ "device_id": device_entry.id,
+ "entity_id": f"{platform}.{NETATMO_DOMAIN}_5678",
+ "type": event_type,
+ "subtype": sub_type,
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": (
+ "{{trigger.event.data.type}} - {{trigger.event.data.data.mode}} - "
+ "{{trigger.platform}} - {{trigger.event.data.device_id}}"
+ )
+ },
+ },
+ },
+ ]
+ },
+ )
+
+ device = device_reg.async_get_device(set(), {connection})
+ assert device is not None
+
+ # Fake that the entity is turning on.
+ hass.bus.async_fire(
+ event_type=NETATMO_EVENT,
+ event_data={
+ "type": event_type,
+ "data": {
+ "mode": sub_type,
+ },
+ ATTR_DEVICE_ID: device.id,
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(events) == 1
+ assert len(calls) == 1
+ assert calls[0].data["some"] == f"{event_type} - {sub_type} - device - {device.id}"
+
+
+@pytest.mark.parametrize(
+ "platform,device_type,event_type",
+ [("climate", MODEL_NAPLUG, trigger) for trigger in CLIMATE_TRIGGERS],
+)
+async def test_if_invalid_device(
+ hass, device_reg, entity_reg, platform, device_type, event_type
+):
+ """Test for event triggers firing."""
+ mac_address = "12:34:56:AB:CD:EF"
+ connection = (device_registry.CONNECTION_NETWORK_MAC, mac_address)
+ config_entry = MockConfigEntry(domain=NETATMO_DOMAIN, data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={connection},
+ identifiers={(NETATMO_DOMAIN, mac_address)},
+ model=device_type,
+ )
+ entity_reg.async_get_or_create(
+ platform, NETATMO_DOMAIN, "5678", device_id=device_entry.id
+ )
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": NETATMO_DOMAIN,
+ "device_id": device_entry.id,
+ "entity_id": f"{platform}.{NETATMO_DOMAIN}_5678",
+ "type": event_type,
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": (
+ "{{trigger.event.data.type}} - {{trigger.platform}} - {{trigger.event.data.device_id}}"
+ )
+ },
+ },
+ },
+ ]
+ },
+ )
diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py
new file mode 100644
index 00000000000000..2ec7d83689eac6
--- /dev/null
+++ b/tests/components/netatmo/test_init.py
@@ -0,0 +1,212 @@
+"""The tests for Netatmo component."""
+from time import time
+from unittest.mock import patch
+
+from homeassistant import config_entries
+from homeassistant.components.netatmo import DOMAIN
+from homeassistant.const import CONF_WEBHOOK_ID
+from homeassistant.setup import async_setup_component
+
+from .common import FAKE_WEBHOOK_ACTIVATION, fake_post_request, simulate_webhook
+
+from tests.common import MockConfigEntry
+from tests.components.cloud import mock_cloud
+
+# Fake webhook thermostat mode change to "Max"
+FAKE_WEBHOOK = {
+ "room_id": "2746182631",
+ "home": {
+ "id": "91763b24c43d3e344f424e8b",
+ "name": "MYHOME",
+ "country": "DE",
+ "rooms": [
+ {
+ "id": "2746182631",
+ "name": "Livingroom",
+ "type": "livingroom",
+ "therm_setpoint_mode": "max",
+ "therm_setpoint_end_time": 1612749189,
+ }
+ ],
+ "modules": [
+ {"id": "12:34:56:00:01:ae", "name": "Livingroom", "type": "NATherm1"}
+ ],
+ },
+ "mode": "max",
+ "event_type": "set_point",
+ "push_type": "display_change",
+}
+
+
+async def test_setup_component(hass):
+ """Test setup and teardown of the netatmo component."""
+ config_entry = MockConfigEntry(
+ domain="netatmo",
+ data={
+ "auth_implementation": "cloud",
+ "token": {
+ "refresh_token": "mock-refresh-token",
+ "access_token": "mock-access-token",
+ "type": "Bearer",
+ "expires_in": 60,
+ "expires_at": time() + 1000,
+ "scope": "read_station",
+ },
+ },
+ )
+ config_entry.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth"
+ ) as mock_auth, patch(
+ "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
+ ) as mock_impl, patch(
+ "homeassistant.components.webhook.async_generate_url"
+ ) as mock_webhook:
+ mock_auth.return_value.post_request.side_effect = fake_post_request
+ assert await async_setup_component(hass, "netatmo", {})
+
+ await hass.async_block_till_done()
+
+ mock_auth.assert_called_once()
+ mock_impl.assert_called_once()
+ mock_webhook.assert_called_once()
+
+ assert config_entry.state == config_entries.ENTRY_STATE_LOADED
+ assert hass.config_entries.async_entries(DOMAIN)
+ assert len(hass.states.async_all()) > 0
+
+ for config_entry in hass.config_entries.async_entries("netatmo"):
+ await hass.config_entries.async_remove(config_entry.entry_id)
+
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 0
+ assert not hass.config_entries.async_entries(DOMAIN)
+
+
+async def test_setup_component_with_config(hass, config_entry):
+ """Test setup of the netatmo component with dev account."""
+ with patch(
+ "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
+ ) as mock_impl, patch(
+ "homeassistant.components.webhook.async_generate_url"
+ ) as mock_webhook, patch(
+ "pyatmo.auth.NetatmoOAuth2.post_request"
+ ) as fake_post_requests, patch(
+ "homeassistant.components.netatmo.PLATFORMS", ["sensor"]
+ ):
+ assert await async_setup_component(
+ hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}}
+ )
+
+ await hass.async_block_till_done()
+
+ fake_post_requests.assert_called()
+ mock_impl.assert_called_once()
+ mock_webhook.assert_called_once()
+
+ assert config_entry.state == config_entries.ENTRY_STATE_LOADED
+ assert hass.config_entries.async_entries(DOMAIN)
+ assert len(hass.states.async_all()) > 0
+
+
+async def test_setup_component_with_webhook(hass, entry):
+ """Test setup and teardown of the netatmo component with webhook registration."""
+ webhook_id = entry.data[CONF_WEBHOOK_ID]
+ await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION)
+
+ assert len(hass.states.async_all()) > 0
+
+ webhook_id = entry.data[CONF_WEBHOOK_ID]
+ await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION)
+
+ # Assert webhook is established successfully
+ climate_entity_livingroom = "climate.netatmo_livingroom"
+ assert hass.states.get(climate_entity_livingroom).state == "auto"
+ await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK)
+ assert hass.states.get(climate_entity_livingroom).state == "heat"
+
+ for config_entry in hass.config_entries.async_entries("netatmo"):
+ await hass.config_entries.async_remove(config_entry.entry_id)
+
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 0
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 0
+
+
+async def test_setup_without_https(hass, config_entry):
+ """Test if set up with cloud link and without https."""
+ hass.config.components.add("cloud")
+ with patch(
+ "homeassistant.helpers.network.get_url",
+ return_value="https://example.nabu.casa",
+ ), patch(
+ "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth"
+ ) as mock_auth, patch(
+ "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
+ ), patch(
+ "homeassistant.components.webhook.async_generate_url"
+ ) as mock_webhook:
+ mock_auth.return_value.post_request.side_effect = fake_post_request
+ mock_webhook.return_value = "https://example.com"
+ assert await async_setup_component(
+ hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}}
+ )
+
+ await hass.async_block_till_done()
+
+ webhook_id = config_entry.data[CONF_WEBHOOK_ID]
+ await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION)
+
+ # Assert webhook is established successfully
+ climate_entity_livingroom = "climate.netatmo_livingroom"
+ assert hass.states.get(climate_entity_livingroom).state == "auto"
+ await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK)
+ await hass.async_block_till_done()
+ assert hass.states.get(climate_entity_livingroom).state == "heat"
+
+
+async def test_setup_with_cloud(hass, config_entry):
+ """Test if set up with active cloud subscription."""
+ await mock_cloud(hass)
+ await hass.async_block_till_done()
+
+ with patch(
+ "homeassistant.components.cloud.async_is_logged_in", return_value=True
+ ), patch(
+ "homeassistant.components.cloud.async_active_subscription", return_value=True
+ ), patch(
+ "homeassistant.components.cloud.async_create_cloudhook",
+ return_value="https://hooks.nabu.casa/ABCD",
+ ) as fake_create_cloudhook, patch(
+ "homeassistant.components.cloud.async_delete_cloudhook"
+ ) as fake_delete_cloudhook, patch(
+ "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth"
+ ) as mock_auth, patch(
+ "homeassistant.components.netatmo.PLATFORMS", []
+ ), patch(
+ "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
+ ), patch(
+ "homeassistant.components.webhook.async_generate_url"
+ ):
+ mock_auth.return_value.post_request.side_effect = fake_post_request
+ assert await async_setup_component(
+ hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}}
+ )
+ assert hass.components.cloud.async_active_subscription() is True
+ fake_create_cloudhook.assert_called_once()
+
+ assert (
+ hass.config_entries.async_entries("netatmo")[0].data["cloudhook_url"]
+ == "https://hooks.nabu.casa/ABCD"
+ )
+
+ await hass.async_block_till_done()
+ assert hass.config_entries.async_entries(DOMAIN)
+
+ for config_entry in hass.config_entries.async_entries("netatmo"):
+ await hass.config_entries.async_remove(config_entry.entry_id)
+ fake_delete_cloudhook.assert_called_once()
+
+ await hass.async_block_till_done()
+ assert not hass.config_entries.async_entries(DOMAIN)
diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py
new file mode 100644
index 00000000000000..4d84bc4e5a5e57
--- /dev/null
+++ b/tests/components/netatmo/test_light.py
@@ -0,0 +1,75 @@
+"""The tests for Netatmo light."""
+from unittest.mock import patch
+
+from homeassistant.components.light import (
+ DOMAIN as LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+)
+from homeassistant.const import ATTR_ENTITY_ID, CONF_WEBHOOK_ID
+
+from .common import FAKE_WEBHOOK_ACTIVATION, simulate_webhook
+
+
+async def test_light_setup_and_services(hass, light_entry):
+ """Test setup and services."""
+ webhook_id = light_entry.data[CONF_WEBHOOK_ID]
+
+ # Fake webhook activation
+ await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION)
+ await hass.async_block_till_done()
+
+ light_entity = "light.netatmo_garden"
+ assert hass.states.get(light_entity).state == "unavailable"
+
+ # Trigger light mode change
+ response = {
+ "event_type": "light_mode",
+ "device_id": "12:34:56:00:a5:a4",
+ "camera_id": "12:34:56:00:a5:a4",
+ "event_id": "601dce1560abca1ebad9b723",
+ "push_type": "NOC-light_mode",
+ "sub_type": "on",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(light_entity).state == "on"
+
+ # Trigger light mode change with erroneous webhook data
+ response = {
+ "event_type": "light_mode",
+ "device_id": "12:34:56:00:a5:a4",
+ }
+ await simulate_webhook(hass, webhook_id, response)
+
+ assert hass.states.get(light_entity).state == "on"
+
+ # Test turning light off
+ with patch("pyatmo.camera.CameraData.set_state") as mock_set_state:
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: light_entity},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_set_state.assert_called_once_with(
+ home_id="91763b24c43d3e344f424e8b",
+ camera_id="12:34:56:00:a5:a4",
+ floodlight="auto",
+ )
+
+ # Test turning light on
+ with patch("pyatmo.camera.CameraData.set_state") as mock_set_state:
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: light_entity},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_set_state.assert_called_once_with(
+ home_id="91763b24c43d3e344f424e8b",
+ camera_id="12:34:56:00:a5:a4",
+ floodlight="on",
+ )
diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py
new file mode 100644
index 00000000000000..fcb2ce454df09a
--- /dev/null
+++ b/tests/components/netatmo/test_sensor.py
@@ -0,0 +1,235 @@
+"""The tests for the Netatmo sensor platform."""
+from datetime import timedelta
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant.components.netatmo import sensor
+from homeassistant.components.netatmo.sensor import MODULE_TYPE_WIND
+from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
+from homeassistant.helpers import entity_registry as er
+from homeassistant.util import dt
+
+from .common import TEST_TIME
+from .conftest import selected_platforms
+
+from tests.common import async_fire_time_changed
+
+
+async def test_weather_sensor(hass, sensor_entry):
+ """Test weather sensor setup."""
+ prefix = "sensor.netatmo_mystation_"
+
+ assert hass.states.get(f"{prefix}temperature").state == "24.6"
+ assert hass.states.get(f"{prefix}humidity").state == "36"
+ assert hass.states.get(f"{prefix}co2").state == "749"
+ assert hass.states.get(f"{prefix}pressure").state == "1017.3"
+
+
+async def test_public_weather_sensor(hass, sensor_entry):
+ """Test public weather sensor setup."""
+ prefix = "sensor.netatmo_home_max_"
+
+ assert hass.states.get(f"{prefix}temperature").state == "27.4"
+ assert hass.states.get(f"{prefix}humidity").state == "76"
+ assert hass.states.get(f"{prefix}pressure").state == "1014.4"
+
+ prefix = "sensor.netatmo_home_avg_"
+
+ assert hass.states.get(f"{prefix}temperature").state == "22.7"
+ assert hass.states.get(f"{prefix}humidity").state == "63.2"
+ assert hass.states.get(f"{prefix}pressure").state == "1010.3"
+
+ assert len(hass.states.async_all()) > 0
+ entities_before_change = len(hass.states.async_all())
+
+ valid_option = {
+ "lat_ne": 32.91336,
+ "lon_ne": -117.187429,
+ "lat_sw": 32.83336,
+ "lon_sw": -117.26743,
+ "show_on_map": True,
+ "area_name": "Home avg",
+ "mode": "max",
+ }
+
+ result = await hass.config_entries.options.async_init(sensor_entry.entry_id)
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={"new_area": "Home avg"}
+ )
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input=valid_option
+ )
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={}
+ )
+ await hass.async_block_till_done()
+ async_fire_time_changed(
+ hass,
+ dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get(f"{prefix}temperature").state == "27.4"
+ assert hass.states.get(f"{prefix}humidity").state == "76"
+ assert hass.states.get(f"{prefix}pressure").state == "1014.4"
+
+ assert len(hass.states.async_all()) == entities_before_change
+
+
+@pytest.mark.parametrize(
+ "strength, expected",
+ [(50, "Full"), (60, "High"), (80, "Medium"), (90, "Low")],
+)
+async def test_process_wifi(strength, expected):
+ """Test wifi strength translation."""
+ assert sensor.process_wifi(strength) == expected
+
+
+@pytest.mark.parametrize(
+ "strength, expected",
+ [(50, "Full"), (70, "High"), (80, "Medium"), (90, "Low")],
+)
+async def test_process_rf(strength, expected):
+ """Test radio strength translation."""
+ assert sensor.process_rf(strength) == expected
+
+
+@pytest.mark.parametrize(
+ "health, expected",
+ [(4, "Unhealthy"), (3, "Poor"), (2, "Fair"), (1, "Fine"), (0, "Healthy")],
+)
+async def test_process_health(health, expected):
+ """Test health index translation."""
+ assert sensor.process_health(health) == expected
+
+
+@pytest.mark.parametrize(
+ "model, data, expected",
+ [
+ (MODULE_TYPE_WIND, 5591, "Full"),
+ (MODULE_TYPE_WIND, 5181, "High"),
+ (MODULE_TYPE_WIND, 4771, "Medium"),
+ (MODULE_TYPE_WIND, 4361, "Low"),
+ (MODULE_TYPE_WIND, 4300, "Very Low"),
+ ],
+)
+async def test_process_battery(model, data, expected):
+ """Test battery level translation."""
+ assert sensor.process_battery(data, model) == expected
+
+
+@pytest.mark.parametrize(
+ "angle, expected",
+ [
+ (0, "N"),
+ (40, "NE"),
+ (70, "E"),
+ (130, "SE"),
+ (160, "S"),
+ (220, "SW"),
+ (250, "W"),
+ (310, "NW"),
+ (340, "N"),
+ ],
+)
+async def test_process_angle(angle, expected):
+ """Test wind direction translation."""
+ assert sensor.process_angle(angle) == expected
+
+
+@pytest.mark.parametrize(
+ "angle, expected",
+ [(-1, 359), (-40, 320)],
+)
+async def test_fix_angle(angle, expected):
+ """Test wind angle fix."""
+ assert sensor.fix_angle(angle) == expected
+
+
+@pytest.mark.parametrize(
+ "uid, name, expected",
+ [
+ ("12:34:56:37:11:ca-reachable", "netatmo_mystation_reachable", "True"),
+ ("12:34:56:03:1b:e4-rf_status", "netatmo_mystation_yard_radio", "Full"),
+ (
+ "12:34:56:05:25:6e-rf_status",
+ "netatmo_valley_road_rain_gauge_radio",
+ "Medium",
+ ),
+ (
+ "12:34:56:36:fc:de-rf_status_lvl",
+ "netatmo_mystation_netatmooutdoor_radio_level",
+ "65",
+ ),
+ (
+ "12:34:56:37:11:ca-wifi_status_lvl",
+ "netatmo_mystation_wifi_level",
+ "45",
+ ),
+ (
+ "12:34:56:37:11:ca-wifi_status",
+ "netatmo_mystation_wifi_status",
+ "Full",
+ ),
+ (
+ "12:34:56:37:11:ca-temp_trend",
+ "netatmo_mystation_temperature_trend",
+ "stable",
+ ),
+ (
+ "12:34:56:37:11:ca-pressure_trend",
+ "netatmo_mystation_pressure_trend",
+ "down",
+ ),
+ ("12:34:56:05:51:20-sum_rain_1", "netatmo_mystation_yard_rain_last_hour", "0"),
+ ("12:34:56:05:51:20-sum_rain_24", "netatmo_mystation_yard_rain_today", "0"),
+ ("12:34:56:03:1b:e4-windangle", "netatmo_mystation_garden_direction", "SW"),
+ (
+ "12:34:56:03:1b:e4-windangle_value",
+ "netatmo_mystation_garden_angle",
+ "217",
+ ),
+ ("12:34:56:03:1b:e4-gustangle", "mystation_garden_gust_direction", "S"),
+ (
+ "12:34:56:03:1b:e4-gustangle",
+ "netatmo_mystation_garden_gust_direction",
+ "S",
+ ),
+ (
+ "12:34:56:03:1b:e4-gustangle_value",
+ "netatmo_mystation_garden_gust_angle_value",
+ "206",
+ ),
+ (
+ "12:34:56:03:1b:e4-guststrength",
+ "netatmo_mystation_garden_gust_strength",
+ "9",
+ ),
+ (
+ "12:34:56:26:68:92-health_idx",
+ "netatmo_baby_bedroom_health",
+ "Fine",
+ ),
+ ],
+)
+async def test_weather_sensor_enabling(hass, config_entry, uid, name, expected):
+ """Test enabling of by default disabled sensors."""
+ with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]):
+ states_before = len(hass.states.async_all())
+ assert hass.states.get(f"sensor.{name}") is None
+
+ registry = er.async_get(hass)
+ registry.async_get_or_create(
+ "sensor",
+ "netatmo",
+ uid,
+ suggested_object_id=name,
+ disabled_by=None,
+ )
+ await hass.config_entries.async_setup(config_entry.entry_id)
+
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) > states_before
+ assert hass.states.get(f"sensor.{name}").state == expected
diff --git a/tests/components/nightscout/test_sensor.py b/tests/components/nightscout/test_sensor.py
index 3df98a2595a742..5e73c75d93c619 100644
--- a/tests/components/nightscout/test_sensor.py
+++ b/tests/components/nightscout/test_sensor.py
@@ -1,12 +1,11 @@
"""The sensor tests for the Nightscout platform."""
from homeassistant.components.nightscout.const import (
- ATTR_DATE,
ATTR_DELTA,
ATTR_DEVICE,
ATTR_DIRECTION,
)
-from homeassistant.const import ATTR_ICON, STATE_UNAVAILABLE
+from homeassistant.const import ATTR_DATE, ATTR_ICON, STATE_UNAVAILABLE
from tests.components.nightscout import (
GLUCOSE_READINGS,
diff --git a/tests/components/nsw_fuel_station/test_sensor.py b/tests/components/nsw_fuel_station/test_sensor.py
index 28ff7a7ed9544e..40143a67bac173 100644
--- a/tests/components/nsw_fuel_station/test_sensor.py
+++ b/tests/components/nsw_fuel_station/test_sensor.py
@@ -99,5 +99,5 @@ async def test_sensor_values(hass):
assert await async_setup_component(hass, sensor.DOMAIN, {"sensor": VALID_CONFIG})
await hass.async_block_till_done()
- assert "140.0" == hass.states.get("sensor.my_fake_station_e10").state
- assert "150.0" == hass.states.get("sensor.my_fake_station_p95").state
+ assert hass.states.get("sensor.my_fake_station_e10").state == "140.0"
+ assert hass.states.get("sensor.my_fake_station_p95").state == "150.0"
diff --git a/tests/components/nuki/mock.py b/tests/components/nuki/mock.py
new file mode 100644
index 00000000000000..a7870ce0906d14
--- /dev/null
+++ b/tests/components/nuki/mock.py
@@ -0,0 +1,25 @@
+"""Mockup Nuki device."""
+from homeassistant import setup
+
+from tests.common import MockConfigEntry
+
+NAME = "Nuki_Bridge_75BCD15"
+HOST = "1.1.1.1"
+MAC = "01:23:45:67:89:ab"
+
+HW_ID = 123456789
+
+MOCK_INFO = {"ids": {"hardwareId": HW_ID}}
+
+
+async def setup_nuki_integration(hass):
+ """Create the Nuki device."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ entry = MockConfigEntry(
+ domain="nuki",
+ unique_id=HW_ID,
+ data={"host": HOST, "port": 8080, "token": "test-token"},
+ )
+ entry.add_to_hass(hass)
+
+ return entry
diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py
index 45168e42c9d69b..4039eef598402c 100644
--- a/tests/components/nuki/test_config_flow.py
+++ b/tests/components/nuki/test_config_flow.py
@@ -1,9 +1,15 @@
"""Test the nuki config flow."""
from unittest.mock import patch
-from homeassistant import config_entries, setup
-from homeassistant.components.nuki.config_flow import CannotConnect, InvalidAuth
+from pynuki.bridge import InvalidCredentialsException
+from requests.exceptions import RequestException
+
+from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS
from homeassistant.components.nuki.const import DOMAIN
+from homeassistant.const import CONF_TOKEN
+
+from .mock import HOST, MAC, MOCK_INFO, NAME, setup_nuki_integration
async def test_form(hass):
@@ -12,14 +18,12 @@ async def test_form(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- assert result["type"] == "form"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
- mock_info = {"ids": {"hardwareId": "0001"}}
-
with patch(
"homeassistant.components.nuki.config_flow.NukiBridge.info",
- return_value=mock_info,
+ return_value=MOCK_INFO,
), patch(
"homeassistant.components.nuki.async_setup", return_value=True
) as mock_setup, patch(
@@ -36,8 +40,8 @@ async def test_form(hass):
)
await hass.async_block_till_done()
- assert result2["type"] == "create_entry"
- assert result2["title"] == "0001"
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result2["title"] == 123456789
assert result2["data"] == {
"host": "1.1.1.1",
"port": 8080,
@@ -47,6 +51,37 @@ async def test_form(hass):
assert len(mock_setup_entry.mock_calls) == 1
+async def test_import(hass):
+ """Test that the import works."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ with patch(
+ "homeassistant.components.nuki.config_flow.NukiBridge.info",
+ return_value=MOCK_INFO,
+ ), patch(
+ "homeassistant.components.nuki.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.nuki.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={"host": "1.1.1.1", "port": 8080, "token": "test-token"},
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == 123456789
+ assert result["data"] == {
+ "host": "1.1.1.1",
+ "port": 8080,
+ "token": "test-token",
+ }
+
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
async def test_form_invalid_auth(hass):
"""Test we handle invalid auth."""
result = await hass.config_entries.flow.async_init(
@@ -55,7 +90,7 @@ async def test_form_invalid_auth(hass):
with patch(
"homeassistant.components.nuki.config_flow.NukiBridge.info",
- side_effect=InvalidAuth,
+ side_effect=InvalidCredentialsException,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -66,7 +101,7 @@ async def test_form_invalid_auth(hass):
},
)
- assert result2["type"] == "form"
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["errors"] == {"base": "invalid_auth"}
@@ -78,7 +113,7 @@ async def test_form_cannot_connect(hass):
with patch(
"homeassistant.components.nuki.config_flow.NukiBridge.info",
- side_effect=CannotConnect,
+ side_effect=RequestException,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -89,5 +124,207 @@ async def test_form_cannot_connect(hass):
},
)
- assert result2["type"] == "form"
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["errors"] == {"base": "cannot_connect"}
+
+
+async def test_form_unknown_exception(hass):
+ """Test we handle unknown exceptions."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.nuki.config_flow.NukiBridge.info",
+ side_effect=Exception,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "port": 8080,
+ "token": "test-token",
+ },
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "unknown"}
+
+
+async def test_form_already_configured(hass):
+ """Test we get the form."""
+ await setup_nuki_integration(hass)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.nuki.config_flow.NukiBridge.info",
+ return_value=MOCK_INFO,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "port": 8080,
+ "token": "test-token",
+ },
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result2["reason"] == "already_configured"
+
+
+async def test_dhcp_flow(hass):
+ """Test that DHCP discovery for new bridge works."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ data={HOSTNAME: NAME, IP_ADDRESS: HOST, MAC_ADDRESS: MAC},
+ context={"source": config_entries.SOURCE_DHCP},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == config_entries.SOURCE_USER
+
+ with patch(
+ "homeassistant.components.nuki.config_flow.NukiBridge.info",
+ return_value=MOCK_INFO,
+ ), patch(
+ "homeassistant.components.nuki.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.nuki.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "port": 8080,
+ "token": "test-token",
+ },
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result2["title"] == 123456789
+ assert result2["data"] == {
+ "host": "1.1.1.1",
+ "port": 8080,
+ "token": "test-token",
+ }
+
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_dhcp_flow_already_configured(hass):
+ """Test that DHCP doesn't setup already configured devices."""
+ await setup_nuki_integration(hass)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ data={HOSTNAME: NAME, IP_ADDRESS: HOST, MAC_ADDRESS: MAC},
+ context={"source": config_entries.SOURCE_DHCP},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+
+async def test_reauth_success(hass):
+ """Test starting a reauthentication flow."""
+ entry = await setup_nuki_integration(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth_confirm"
+
+ with patch(
+ "homeassistant.components.nuki.config_flow.NukiBridge.info",
+ return_value=MOCK_INFO,
+ ), patch("homeassistant.components.nuki.async_setup", return_value=True), patch(
+ "homeassistant.components.nuki.async_setup_entry",
+ return_value=True,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={CONF_TOKEN: "new-token"},
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result2["reason"] == "reauth_successful"
+ assert entry.data[CONF_TOKEN] == "new-token"
+
+
+async def test_reauth_invalid_auth(hass):
+ """Test starting a reauthentication flow with invalid auth."""
+ entry = await setup_nuki_integration(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth_confirm"
+
+ with patch(
+ "homeassistant.components.nuki.config_flow.NukiBridge.info",
+ side_effect=InvalidCredentialsException,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={CONF_TOKEN: "new-token"},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == "reauth_confirm"
+ assert result2["errors"] == {"base": "invalid_auth"}
+
+
+async def test_reauth_cannot_connect(hass):
+ """Test starting a reauthentication flow with cannot connect."""
+ entry = await setup_nuki_integration(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth_confirm"
+
+ with patch(
+ "homeassistant.components.nuki.config_flow.NukiBridge.info",
+ side_effect=RequestException,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={CONF_TOKEN: "new-token"},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == "reauth_confirm"
+ assert result2["errors"] == {"base": "cannot_connect"}
+
+
+async def test_reauth_unknown_exception(hass):
+ """Test starting a reauthentication flow with an unknown exception."""
+ entry = await setup_nuki_integration(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth_confirm"
+
+ with patch(
+ "homeassistant.components.nuki.config_flow.NukiBridge.info",
+ side_effect=Exception,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={CONF_TOKEN: "new-token"},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == "reauth_confirm"
+ assert result2["errors"] == {"base": "unknown"}
diff --git a/tests/components/number/test_device_action.py b/tests/components/number/test_device_action.py
index 21c79a9f7413f6..a981a331e7ead1 100644
--- a/tests/components/number/test_device_action.py
+++ b/tests/components/number/test_device_action.py
@@ -15,7 +15,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/nut/test_sensor.py b/tests/components/nut/test_sensor.py
index 4950d467d6afcb..0d8fec71d51d33 100644
--- a/tests/components/nut/test_sensor.py
+++ b/tests/components/nut/test_sensor.py
@@ -1,6 +1,7 @@
"""The sensor tests for the nut platform."""
from homeassistant.const import PERCENTAGE
+from homeassistant.helpers import entity_registry as er
from .util import async_init_integration
@@ -9,7 +10,7 @@ async def test_pr3000rt2u(hass):
"""Test creation of PR3000RT2U sensors."""
await async_init_integration(hass, "PR3000RT2U", ["battery.charge"])
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("sensor.ups1_battery_charge")
assert entry
assert entry.unique_id == "CPS_PR3000RT2U_PYVJO2000034_battery.charge"
@@ -35,7 +36,7 @@ async def test_cp1350c(hass):
config_entry = await async_init_integration(hass, "CP1350C", ["battery.charge"])
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("sensor.ups1_battery_charge")
assert entry
assert entry.unique_id == f"{config_entry.entry_id}_battery.charge"
@@ -60,7 +61,7 @@ async def test_5e850i(hass):
"""Test creation of 5E850I sensors."""
config_entry = await async_init_integration(hass, "5E850I", ["battery.charge"])
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("sensor.ups1_battery_charge")
assert entry
assert entry.unique_id == f"{config_entry.entry_id}_battery.charge"
@@ -85,7 +86,7 @@ async def test_5e650i(hass):
"""Test creation of 5E650I sensors."""
config_entry = await async_init_integration(hass, "5E650I", ["battery.charge"])
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("sensor.ups1_battery_charge")
assert entry
assert entry.unique_id == f"{config_entry.entry_id}_battery.charge"
@@ -110,7 +111,7 @@ async def test_backupsses600m1(hass):
"""Test creation of BACKUPSES600M1 sensors."""
await async_init_integration(hass, "BACKUPSES600M1", ["battery.charge"])
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("sensor.ups1_battery_charge")
assert entry
assert (
@@ -140,7 +141,7 @@ async def test_cp1500pfclcd(hass):
config_entry = await async_init_integration(
hass, "CP1500PFCLCD", ["battery.charge"]
)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("sensor.ups1_battery_charge")
assert entry
assert entry.unique_id == f"{config_entry.entry_id}_battery.charge"
@@ -165,7 +166,7 @@ async def test_dl650elcd(hass):
"""Test creation of DL650ELCD sensors."""
config_entry = await async_init_integration(hass, "DL650ELCD", ["battery.charge"])
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("sensor.ups1_battery_charge")
assert entry
assert entry.unique_id == f"{config_entry.entry_id}_battery.charge"
@@ -190,7 +191,7 @@ async def test_blazer_usb(hass):
"""Test creation of blazer_usb sensors."""
config_entry = await async_init_integration(hass, "blazer_usb", ["battery.charge"])
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get("sensor.ups1_battery_charge")
assert entry
assert entry.unique_id == f"{config_entry.entry_id}_battery.charge"
diff --git a/tests/components/nws/conftest.py b/tests/components/nws/conftest.py
index d01201bb4846a8..98ac9191e0d0c7 100644
--- a/tests/components/nws/conftest.py
+++ b/tests/components/nws/conftest.py
@@ -32,3 +32,21 @@ def mock_simple_nws_config():
instance.station = "ABC"
instance.stations = ["ABC"]
yield mock_nws
+
+
+@pytest.fixture()
+def no_sensor():
+ """Remove sensors."""
+ with patch(
+ "homeassistant.components.nws.sensor.async_setup_entry", return_value=True
+ ) as mock_setup_entry:
+ yield mock_setup_entry
+
+
+@pytest.fixture()
+def no_weather():
+ """Remove weather."""
+ with patch(
+ "homeassistant.components.nws.weather.async_setup_entry", return_value=True
+ ) as mock_setup_entry:
+ yield mock_setup_entry
diff --git a/tests/components/nws/const.py b/tests/components/nws/const.py
index ae2f826294fec8..4f4b140dbf91e1 100644
--- a/tests/components/nws/const.py
+++ b/tests/components/nws/const.py
@@ -44,6 +44,7 @@
DEFAULT_OBSERVATION = {
"temperature": 10,
"seaLevelPressure": 100000,
+ "barometricPressure": 100000,
"relativeHumidity": 10,
"windSpeed": 10,
"windDirection": 180,
@@ -53,9 +54,45 @@
"timestamp": "2019-08-12T23:53:00+00:00",
"iconTime": "day",
"iconWeather": (("Fair/clear", None),),
+ "dewpoint": 5,
+ "windChill": 5,
+ "heatIndex": 15,
+ "windGust": 20,
}
-EXPECTED_OBSERVATION_IMPERIAL = {
+SENSOR_EXPECTED_OBSERVATION_METRIC = {
+ "dewpoint": "5",
+ "temperature": "10",
+ "windChill": "5",
+ "heatIndex": "15",
+ "relativeHumidity": "10",
+ "windSpeed": "10",
+ "windGust": "20",
+ "windDirection": "180",
+ "barometricPressure": "100000",
+ "seaLevelPressure": "100000",
+ "visibility": "10000",
+}
+
+SENSOR_EXPECTED_OBSERVATION_IMPERIAL = {
+ "dewpoint": str(round(convert_temperature(5, TEMP_CELSIUS, TEMP_FAHRENHEIT))),
+ "temperature": str(round(convert_temperature(10, TEMP_CELSIUS, TEMP_FAHRENHEIT))),
+ "windChill": str(round(convert_temperature(5, TEMP_CELSIUS, TEMP_FAHRENHEIT))),
+ "heatIndex": str(round(convert_temperature(15, TEMP_CELSIUS, TEMP_FAHRENHEIT))),
+ "relativeHumidity": "10",
+ "windSpeed": str(round(convert_distance(10, LENGTH_KILOMETERS, LENGTH_MILES))),
+ "windGust": str(round(convert_distance(20, LENGTH_KILOMETERS, LENGTH_MILES))),
+ "windDirection": "180",
+ "barometricPressure": str(
+ round(convert_pressure(100000, PRESSURE_PA, PRESSURE_INHG), 2)
+ ),
+ "seaLevelPressure": str(
+ round(convert_pressure(100000, PRESSURE_PA, PRESSURE_INHG), 2)
+ ),
+ "visibility": str(round(convert_distance(10000, LENGTH_METERS, LENGTH_MILES))),
+}
+
+WEATHER_EXPECTED_OBSERVATION_IMPERIAL = {
ATTR_WEATHER_TEMPERATURE: round(
convert_temperature(10, TEMP_CELSIUS, TEMP_FAHRENHEIT)
),
@@ -72,7 +109,7 @@
ATTR_WEATHER_HUMIDITY: 10,
}
-EXPECTED_OBSERVATION_METRIC = {
+WEATHER_EXPECTED_OBSERVATION_METRIC = {
ATTR_WEATHER_TEMPERATURE: 10,
ATTR_WEATHER_WIND_BEARING: 180,
ATTR_WEATHER_WIND_SPEED: 10,
diff --git a/tests/components/nws/test_init.py b/tests/components/nws/test_init.py
index 44b5193d79c5c6..01a203aa07b482 100644
--- a/tests/components/nws/test_init.py
+++ b/tests/components/nws/test_init.py
@@ -1,6 +1,7 @@
"""Tests for init module."""
from homeassistant.components.nws.const import DOMAIN
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
+from homeassistant.const import STATE_UNAVAILABLE
from tests.common import MockConfigEntry
from tests.components.nws.const import NWS_CONFIG
@@ -25,5 +26,12 @@ async def test_unload_entry(hass, mock_simple_nws):
assert len(entries) == 1
assert await hass.config_entries.async_unload(entries[0].entry_id)
- assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0
+ entities = hass.states.async_entity_ids(WEATHER_DOMAIN)
+ assert len(entities) == 1
+ for entity in entities:
+ assert hass.states.get(entity).state == STATE_UNAVAILABLE
assert DOMAIN not in hass.data
+
+ assert await hass.config_entries.async_remove(entries[0].entry_id)
+ await hass.async_block_till_done()
+ assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0
diff --git a/tests/components/nws/test_sensor.py b/tests/components/nws/test_sensor.py
new file mode 100644
index 00000000000000..44b181b1ec44ef
--- /dev/null
+++ b/tests/components/nws/test_sensor.py
@@ -0,0 +1,95 @@
+"""Sensors for National Weather Service (NWS)."""
+import pytest
+
+from homeassistant.components.nws.const import (
+ ATTR_LABEL,
+ ATTRIBUTION,
+ DOMAIN,
+ SENSOR_TYPES,
+)
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN
+from homeassistant.util import slugify
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
+
+from tests.common import MockConfigEntry
+from tests.components.nws.const import (
+ EXPECTED_FORECAST_IMPERIAL,
+ EXPECTED_FORECAST_METRIC,
+ NONE_OBSERVATION,
+ NWS_CONFIG,
+ SENSOR_EXPECTED_OBSERVATION_IMPERIAL,
+ SENSOR_EXPECTED_OBSERVATION_METRIC,
+)
+
+
+@pytest.mark.parametrize(
+ "units,result_observation,result_forecast",
+ [
+ (
+ IMPERIAL_SYSTEM,
+ SENSOR_EXPECTED_OBSERVATION_IMPERIAL,
+ EXPECTED_FORECAST_IMPERIAL,
+ ),
+ (METRIC_SYSTEM, SENSOR_EXPECTED_OBSERVATION_METRIC, EXPECTED_FORECAST_METRIC),
+ ],
+)
+async def test_imperial_metric(
+ hass, units, result_observation, result_forecast, mock_simple_nws, no_weather
+):
+ """Test with imperial and metric units."""
+ registry = await hass.helpers.entity_registry.async_get_registry()
+
+ for sensor_name, sensor_data in SENSOR_TYPES.items():
+ registry.async_get_or_create(
+ SENSOR_DOMAIN,
+ DOMAIN,
+ f"35_-75_{sensor_name}",
+ suggested_object_id=f"abc_{sensor_data[ATTR_LABEL]}",
+ disabled_by=None,
+ )
+
+ hass.config.units = units
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=NWS_CONFIG,
+ )
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ for sensor_name, sensor_data in SENSOR_TYPES.items():
+ state = hass.states.get(f"sensor.abc_{slugify(sensor_data[ATTR_LABEL])}")
+ assert state
+ assert state.state == result_observation[sensor_name]
+ assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
+
+
+async def test_none_values(hass, mock_simple_nws, no_weather):
+ """Test with no values."""
+ instance = mock_simple_nws.return_value
+ instance.observation = NONE_OBSERVATION
+
+ registry = await hass.helpers.entity_registry.async_get_registry()
+
+ for sensor_name, sensor_data in SENSOR_TYPES.items():
+ registry.async_get_or_create(
+ SENSOR_DOMAIN,
+ DOMAIN,
+ f"35_-75_{sensor_name}",
+ suggested_object_id=f"abc_{sensor_data[ATTR_LABEL]}",
+ disabled_by=None,
+ )
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=NWS_CONFIG,
+ )
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ for sensor_name, sensor_data in SENSOR_TYPES.items():
+ state = hass.states.get(f"sensor.abc_{slugify(sensor_data[ATTR_LABEL])}")
+ assert state
+ assert state.state == STATE_UNKNOWN
diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py
index c7cb5b81c2a5bd..78ab7eb4ac58eb 100644
--- a/tests/components/nws/test_weather.py
+++ b/tests/components/nws/test_weather.py
@@ -12,6 +12,7 @@
DOMAIN as WEATHER_DOMAIN,
)
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
@@ -20,27 +21,31 @@
from tests.components.nws.const import (
EXPECTED_FORECAST_IMPERIAL,
EXPECTED_FORECAST_METRIC,
- EXPECTED_OBSERVATION_IMPERIAL,
- EXPECTED_OBSERVATION_METRIC,
NONE_FORECAST,
NONE_OBSERVATION,
NWS_CONFIG,
+ WEATHER_EXPECTED_OBSERVATION_IMPERIAL,
+ WEATHER_EXPECTED_OBSERVATION_METRIC,
)
@pytest.mark.parametrize(
"units,result_observation,result_forecast",
[
- (IMPERIAL_SYSTEM, EXPECTED_OBSERVATION_IMPERIAL, EXPECTED_FORECAST_IMPERIAL),
- (METRIC_SYSTEM, EXPECTED_OBSERVATION_METRIC, EXPECTED_FORECAST_METRIC),
+ (
+ IMPERIAL_SYSTEM,
+ WEATHER_EXPECTED_OBSERVATION_IMPERIAL,
+ EXPECTED_FORECAST_IMPERIAL,
+ ),
+ (METRIC_SYSTEM, WEATHER_EXPECTED_OBSERVATION_METRIC, EXPECTED_FORECAST_METRIC),
],
)
async def test_imperial_metric(
- hass, units, result_observation, result_forecast, mock_simple_nws
+ hass, units, result_observation, result_forecast, mock_simple_nws, no_sensor
):
"""Test with imperial and metric units."""
# enable the hourly entity
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
registry.async_get_or_create(
WEATHER_DOMAIN,
nws.DOMAIN,
@@ -85,7 +90,7 @@ async def test_imperial_metric(
assert forecast[0].get(key) == value
-async def test_none_values(hass, mock_simple_nws):
+async def test_none_values(hass, mock_simple_nws, no_sensor):
"""Test with none values in observation and forecast dicts."""
instance = mock_simple_nws.return_value
instance.observation = NONE_OBSERVATION
@@ -102,7 +107,7 @@ async def test_none_values(hass, mock_simple_nws):
state = hass.states.get("weather.abc_daynight")
assert state.state == STATE_UNKNOWN
data = state.attributes
- for key in EXPECTED_OBSERVATION_IMPERIAL:
+ for key in WEATHER_EXPECTED_OBSERVATION_IMPERIAL:
assert data.get(key) is None
forecast = data.get(ATTR_FORECAST)
@@ -110,7 +115,7 @@ async def test_none_values(hass, mock_simple_nws):
assert forecast[0].get(key) is None
-async def test_none(hass, mock_simple_nws):
+async def test_none(hass, mock_simple_nws, no_sensor):
"""Test with None as observation and forecast."""
instance = mock_simple_nws.return_value
instance.observation = None
@@ -129,14 +134,14 @@ async def test_none(hass, mock_simple_nws):
assert state.state == STATE_UNKNOWN
data = state.attributes
- for key in EXPECTED_OBSERVATION_IMPERIAL:
+ for key in WEATHER_EXPECTED_OBSERVATION_IMPERIAL:
assert data.get(key) is None
forecast = data.get(ATTR_FORECAST)
assert forecast is None
-async def test_error_station(hass, mock_simple_nws):
+async def test_error_station(hass, mock_simple_nws, no_sensor):
"""Test error in setting station."""
instance = mock_simple_nws.return_value
@@ -154,7 +159,7 @@ async def test_error_station(hass, mock_simple_nws):
assert hass.states.get("weather.abc_daynight") is None
-async def test_entity_refresh(hass, mock_simple_nws):
+async def test_entity_refresh(hass, mock_simple_nws, no_sensor):
"""Test manual refresh."""
instance = mock_simple_nws.return_value
@@ -183,7 +188,7 @@ async def test_entity_refresh(hass, mock_simple_nws):
instance.update_forecast_hourly.assert_called_once()
-async def test_error_observation(hass, mock_simple_nws):
+async def test_error_observation(hass, mock_simple_nws, no_sensor):
"""Test error during update observation."""
utc_time = dt_util.utcnow()
with patch("homeassistant.components.nws.utcnow") as mock_utc, patch(
@@ -247,7 +252,7 @@ def increment_time(time):
assert state.state == STATE_UNAVAILABLE
-async def test_error_forecast(hass, mock_simple_nws):
+async def test_error_forecast(hass, mock_simple_nws, no_sensor):
"""Test error during update forecast."""
instance = mock_simple_nws.return_value
instance.update_forecast.side_effect = aiohttp.ClientError
@@ -278,13 +283,13 @@ async def test_error_forecast(hass, mock_simple_nws):
assert state.state == ATTR_CONDITION_SUNNY
-async def test_error_forecast_hourly(hass, mock_simple_nws):
+async def test_error_forecast_hourly(hass, mock_simple_nws, no_sensor):
"""Test error during update forecast hourly."""
instance = mock_simple_nws.return_value
instance.update_forecast_hourly.side_effect = aiohttp.ClientError
# enable the hourly entity
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
registry.async_get_or_create(
WEATHER_DOMAIN,
nws.DOMAIN,
@@ -319,7 +324,7 @@ async def test_error_forecast_hourly(hass, mock_simple_nws):
assert state.state == ATTR_CONDITION_SUNNY
-async def test_forecast_hourly_disable_enable(hass, mock_simple_nws):
+async def test_forecast_hourly_disable_enable(hass, mock_simple_nws, no_sensor):
"""Test error during update forecast hourly."""
entry = MockConfigEntry(
domain=nws.DOMAIN,
@@ -330,7 +335,7 @@ async def test_forecast_hourly_disable_enable(hass, mock_simple_nws):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get_or_create(
WEATHER_DOMAIN,
nws.DOMAIN,
diff --git a/tests/components/nx584/test_binary_sensor.py b/tests/components/nx584/test_binary_sensor.py
index c12e8e24ebc5df..fd2e5b30bac304 100644
--- a/tests/components/nx584/test_binary_sensor.py
+++ b/tests/components/nx584/test_binary_sensor.py
@@ -143,10 +143,10 @@ def test_nx584_zone_sensor_normal():
"""Test for the NX584 zone sensor."""
zone = {"number": 1, "name": "foo", "state": True}
sensor = nx584.NX584ZoneSensor(zone, "motion")
- assert "foo" == sensor.name
+ assert sensor.name == "foo"
assert not sensor.should_poll
assert sensor.is_on
- assert sensor.device_state_attributes["zone_number"] == 1
+ assert sensor.extra_state_attributes["zone_number"] == 1
zone["state"] = False
assert not sensor.is_on
@@ -204,7 +204,7 @@ def run(fake_process):
assert fake_process.call_args == mock.call(fake_events[0])
run()
- assert 3 == client.get_events.call_count
+ assert client.get_events.call_count == 3
@mock.patch("time.sleep")
@@ -224,5 +224,5 @@ def fake_run():
mock_inner.side_effect = fake_run
with pytest.raises(StopMe):
watcher.run()
- assert 3 == mock_inner.call_count
+ assert mock_inner.call_count == 3
mock_sleep.assert_has_calls([mock.call(10), mock.call(10)])
diff --git a/tests/components/nzbget/test_sensor.py b/tests/components/nzbget/test_sensor.py
index f5954bc7ee0abc..de5e2382a63869 100644
--- a/tests/components/nzbget/test_sensor.py
+++ b/tests/components/nzbget/test_sensor.py
@@ -8,6 +8,7 @@
DATA_RATE_MEGABYTES_PER_SECOND,
DEVICE_CLASS_TIMESTAMP,
)
+from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from . import init_integration
@@ -19,7 +20,7 @@ async def test_sensors(hass, nzbget_api) -> None:
with patch("homeassistant.components.nzbget.sensor.utcnow", return_value=now):
entry = await init_integration(hass)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
uptime = now - timedelta(seconds=600)
diff --git a/tests/components/nzbget/test_switch.py b/tests/components/nzbget/test_switch.py
index c12fe8ca526e10..debfd9a1be8735 100644
--- a/tests/components/nzbget/test_switch.py
+++ b/tests/components/nzbget/test_switch.py
@@ -7,6 +7,7 @@
STATE_OFF,
STATE_ON,
)
+from homeassistant.helpers import entity_registry as er
from . import init_integration
@@ -18,7 +19,7 @@ async def test_download_switch(hass, nzbget_api) -> None:
entry = await init_integration(hass)
assert entry
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entity_id = "switch.nzbgettest_download"
entity_entry = registry.async_get(entity_id)
assert entity_entry
diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py
index fe956b2ac0a936..d8ae50b851fc81 100644
--- a/tests/components/onboarding/test_views.py
+++ b/tests/components/onboarding/test_views.py
@@ -8,6 +8,7 @@
from homeassistant.components import onboarding
from homeassistant.components.onboarding import const, views
from homeassistant.const import HTTP_FORBIDDEN
+from homeassistant.helpers import area_registry as ar
from homeassistant.setup import async_setup_component
from . import mock_storage
@@ -74,7 +75,7 @@ async def mock_supervisor_fixture(hass, aioclient_mock):
return_value={},
), patch(
"homeassistant.components.hassio.HassIO.get_supervisor_info",
- return_value={},
+ return_value={"diagnostics": True},
), patch(
"homeassistant.components.hassio.HassIO.get_os_info",
return_value={},
@@ -181,7 +182,7 @@ async def test_onboarding_user(hass, hass_storage, aiohttp_client):
)
# Validate created areas
- area_registry = await hass.helpers.area_registry.async_get_registry()
+ area_registry = ar.async_get(hass)
assert len(area_registry.areas) == 3
assert sorted([area.name for area in area_registry.async_list_areas()]) == [
"Bedroom",
@@ -417,3 +418,22 @@ async def test_onboarding_core_no_rpi_power(
rpi_power_state = hass.states.get("binary_sensor.rpi_power_status")
assert not rpi_power_state
+
+
+async def test_onboarding_analytics(hass, hass_storage, hass_client, hass_admin_user):
+ """Test finishing analytics step."""
+ mock_storage(hass_storage, {"done": [const.STEP_USER]})
+
+ assert await async_setup_component(hass, "onboarding", {})
+ await hass.async_block_till_done()
+
+ client = await hass_client()
+
+ resp = await client.post("/api/onboarding/analytics")
+
+ assert resp.status == 200
+
+ assert const.STEP_ANALYTICS in hass_storage[const.DOMAIN]["data"]["done"]
+
+ resp = await client.post("/api/onboarding/analytics")
+ assert resp.status == 403
diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py
index 716e73747f1322..f133f89d5d6268 100644
--- a/tests/components/onewire/__init__.py
+++ b/tests/components/onewire/__init__.py
@@ -1,7 +1,10 @@
"""Tests for 1-Wire integration."""
+from typing import Any, List, Tuple
from unittest.mock import patch
+from pyownet.protocol import ProtocolError
+
from homeassistant.components.onewire.const import (
CONF_MOUNT_DIR,
CONF_NAMES,
@@ -13,6 +16,8 @@
from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
+from .const import MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES
+
from tests.common import MockConfigEntry
@@ -89,3 +94,62 @@ async def setup_onewire_patched_owserver_integration(hass):
await hass.async_block_till_done()
return config_entry
+
+
+def setup_owproxy_mock_devices(owproxy, domain, device_ids) -> None:
+ """Set up mock for owproxy."""
+ dir_return_value = []
+ main_read_side_effect = []
+ sub_read_side_effect = []
+
+ for device_id in device_ids:
+ mock_device = MOCK_OWPROXY_DEVICES[device_id]
+
+ # Setup directory listing
+ dir_return_value += [f"/{device_id}/"]
+
+ # Setup device reads
+ main_read_side_effect += [device_id[0:2].encode()]
+ if "inject_reads" in mock_device:
+ main_read_side_effect += mock_device["inject_reads"]
+
+ # Setup sub-device reads
+ device_sensors = mock_device.get(domain, [])
+ for expected_sensor in device_sensors:
+ sub_read_side_effect.append(expected_sensor["injected_value"])
+
+ # Ensure enough read side effect
+ read_side_effect = (
+ main_read_side_effect
+ + sub_read_side_effect
+ + [ProtocolError("Missing injected value")] * 20
+ )
+ owproxy.return_value.dir.return_value = dir_return_value
+ owproxy.return_value.read.side_effect = read_side_effect
+
+
+def setup_sysbus_mock_devices(
+ domain: str, device_ids: List[str]
+) -> Tuple[List[str], List[Any]]:
+ """Set up mock for sysbus."""
+ glob_result = []
+ read_side_effect = []
+
+ for device_id in device_ids:
+ mock_device = MOCK_SYSBUS_DEVICES[device_id]
+
+ # Setup directory listing
+ glob_result += [f"/{DEFAULT_SYSBUS_MOUNT_DIR}/{device_id}"]
+
+ # Setup sub-device reads
+ device_sensors = mock_device.get(domain, [])
+ for expected_sensor in device_sensors:
+ if isinstance(expected_sensor["injected_value"], list):
+ read_side_effect += expected_sensor["injected_value"]
+ else:
+ read_side_effect.append(expected_sensor["injected_value"])
+
+ # Ensure enough read side effect
+ read_side_effect.extend([FileNotFoundError("Missing injected value")] * 20)
+
+ return (glob_result, read_side_effect)
diff --git a/tests/components/onewire/test_entity_owserver.py b/tests/components/onewire/const.py
similarity index 79%
rename from tests/components/onewire/test_entity_owserver.py
rename to tests/components/onewire/const.py
index 42cbf77711ce84..ccae8e695ce3d7 100644
--- a/tests/components/onewire/test_entity_owserver.py
+++ b/tests/components/onewire/const.py
@@ -1,15 +1,10 @@
-"""Tests for 1-Wire devices connected on OWServer."""
-from unittest.mock import patch
+"""Constants for 1-Wire integration."""
+from pi1wire import InvalidCRCException, UnsupportResponseException
from pyownet.protocol import Error as ProtocolError
-import pytest
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
-from homeassistant.components.onewire.const import (
- DOMAIN,
- PRESSURE_CBAR,
- SUPPORTED_PLATFORMS,
-)
+from homeassistant.components.onewire.const import DOMAIN, PRESSURE_CBAR
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import (
@@ -28,13 +23,8 @@
TEMP_CELSIUS,
VOLT,
)
-from homeassistant.setup import async_setup_component
-
-from . import setup_onewire_patched_owserver_integration
-from tests.common import mock_device_registry, mock_registry
-
-MOCK_DEVICE_SENSORS = {
+MOCK_OWPROXY_DEVICES = {
"00.111111111111": {
"inject_reads": [
b"", # read device type
@@ -190,7 +180,42 @@
"model": "DS2409",
"name": "1F.111111111111",
},
- SENSOR_DOMAIN: [],
+ "branches": {
+ "aux": {},
+ "main": {
+ "1D.111111111111": {
+ "inject_reads": [
+ b"DS2423", # read device type
+ ],
+ "device_info": {
+ "identifiers": {(DOMAIN, "1D.111111111111")},
+ "manufacturer": "Maxim Integrated",
+ "model": "DS2423",
+ "name": "1D.111111111111",
+ },
+ SENSOR_DOMAIN: [
+ {
+ "entity_id": "sensor.1d_111111111111_counter_a",
+ "device_file": "/1F.111111111111/main/1D.111111111111/counter.A",
+ "unique_id": "/1D.111111111111/counter.A",
+ "injected_value": b" 251123",
+ "result": "251123",
+ "unit": "count",
+ "class": None,
+ },
+ {
+ "entity_id": "sensor.1d_111111111111_counter_b",
+ "device_file": "/1F.111111111111/main/1D.111111111111/counter.B",
+ "unique_id": "/1D.111111111111/counter.B",
+ "injected_value": b" 248125",
+ "result": "248125",
+ "unit": "count",
+ "class": None,
+ },
+ ],
+ },
+ },
+ },
},
"22.111111111111": {
"inject_reads": [
@@ -752,65 +777,142 @@
},
}
-
-@pytest.mark.parametrize("device_id", MOCK_DEVICE_SENSORS.keys())
-@pytest.mark.parametrize("platform", SUPPORTED_PLATFORMS)
-@patch("homeassistant.components.onewire.onewirehub.protocol.proxy")
-async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform):
- """Test for 1-Wire device.
-
- As they would be on a clean setup: all binary-sensors and switches disabled.
- """
- await async_setup_component(hass, "persistent_notification", {})
- entity_registry = mock_registry(hass)
- device_registry = mock_device_registry(hass)
-
- mock_device_sensor = MOCK_DEVICE_SENSORS[device_id]
-
- device_family = device_id[0:2]
- dir_return_value = [f"/{device_id}/"]
- read_side_effect = [device_family.encode()]
- if "inject_reads" in mock_device_sensor:
- read_side_effect += mock_device_sensor["inject_reads"]
-
- expected_sensors = mock_device_sensor.get(platform, [])
- for expected_sensor in expected_sensors:
- read_side_effect.append(expected_sensor["injected_value"])
-
- # Ensure enough read side effect
- read_side_effect.extend([ProtocolError("Missing injected value")] * 20)
- owproxy.return_value.dir.return_value = dir_return_value
- owproxy.return_value.read.side_effect = read_side_effect
-
- with patch("homeassistant.components.onewire.SUPPORTED_PLATFORMS", [platform]):
- await setup_onewire_patched_owserver_integration(hass)
- await hass.async_block_till_done()
-
- assert len(entity_registry.entities) == len(expected_sensors)
-
- if len(expected_sensors) > 0:
- device_info = mock_device_sensor["device_info"]
- assert len(device_registry.devices) == 1
- registry_entry = device_registry.async_get_device({(DOMAIN, device_id)})
- assert registry_entry is not None
- assert registry_entry.identifiers == {(DOMAIN, device_id)}
- assert registry_entry.manufacturer == device_info["manufacturer"]
- assert registry_entry.name == device_info["name"]
- assert registry_entry.model == device_info["model"]
-
- for expected_sensor in expected_sensors:
- entity_id = expected_sensor["entity_id"]
- registry_entry = entity_registry.entities.get(entity_id)
- assert registry_entry is not None
- assert registry_entry.unique_id == expected_sensor["unique_id"]
- assert registry_entry.unit_of_measurement == expected_sensor["unit"]
- assert registry_entry.device_class == expected_sensor["class"]
- assert registry_entry.disabled == expected_sensor.get("disabled", False)
- state = hass.states.get(entity_id)
- if registry_entry.disabled:
- assert state is None
- else:
- assert state.state == expected_sensor["result"]
- assert state.attributes["device_file"] == expected_sensor.get(
- "device_file", registry_entry.unique_id
- )
+MOCK_SYSBUS_DEVICES = {
+ "00-111111111111": {SENSOR_DOMAIN: []},
+ "10-111111111111": {
+ "device_info": {
+ "identifiers": {(DOMAIN, "10-111111111111")},
+ "manufacturer": "Maxim Integrated",
+ "model": "10",
+ "name": "10-111111111111",
+ },
+ SENSOR_DOMAIN: [
+ {
+ "entity_id": "sensor.my_ds18b20_temperature",
+ "unique_id": "/sys/bus/w1/devices/10-111111111111/w1_slave",
+ "injected_value": 25.123,
+ "result": "25.1",
+ "unit": TEMP_CELSIUS,
+ "class": DEVICE_CLASS_TEMPERATURE,
+ },
+ ],
+ },
+ "12-111111111111": {SENSOR_DOMAIN: []},
+ "1D-111111111111": {SENSOR_DOMAIN: []},
+ "22-111111111111": {
+ "device_info": {
+ "identifiers": {(DOMAIN, "22-111111111111")},
+ "manufacturer": "Maxim Integrated",
+ "model": "22",
+ "name": "22-111111111111",
+ },
+ "sensor": [
+ {
+ "entity_id": "sensor.22_111111111111_temperature",
+ "unique_id": "/sys/bus/w1/devices/22-111111111111/w1_slave",
+ "injected_value": FileNotFoundError,
+ "result": "unknown",
+ "unit": TEMP_CELSIUS,
+ "class": DEVICE_CLASS_TEMPERATURE,
+ },
+ ],
+ },
+ "26-111111111111": {SENSOR_DOMAIN: []},
+ "28-111111111111": {
+ "device_info": {
+ "identifiers": {(DOMAIN, "28-111111111111")},
+ "manufacturer": "Maxim Integrated",
+ "model": "28",
+ "name": "28-111111111111",
+ },
+ SENSOR_DOMAIN: [
+ {
+ "entity_id": "sensor.28_111111111111_temperature",
+ "unique_id": "/sys/bus/w1/devices/28-111111111111/w1_slave",
+ "injected_value": InvalidCRCException,
+ "result": "unknown",
+ "unit": TEMP_CELSIUS,
+ "class": DEVICE_CLASS_TEMPERATURE,
+ },
+ ],
+ },
+ "29-111111111111": {SENSOR_DOMAIN: []},
+ "3B-111111111111": {
+ "device_info": {
+ "identifiers": {(DOMAIN, "3B-111111111111")},
+ "manufacturer": "Maxim Integrated",
+ "model": "3B",
+ "name": "3B-111111111111",
+ },
+ SENSOR_DOMAIN: [
+ {
+ "entity_id": "sensor.3b_111111111111_temperature",
+ "unique_id": "/sys/bus/w1/devices/3B-111111111111/w1_slave",
+ "injected_value": 29.993,
+ "result": "30.0",
+ "unit": TEMP_CELSIUS,
+ "class": DEVICE_CLASS_TEMPERATURE,
+ },
+ ],
+ },
+ "42-111111111111": {
+ "device_info": {
+ "identifiers": {(DOMAIN, "42-111111111111")},
+ "manufacturer": "Maxim Integrated",
+ "model": "42",
+ "name": "42-111111111111",
+ },
+ SENSOR_DOMAIN: [
+ {
+ "entity_id": "sensor.42_111111111111_temperature",
+ "unique_id": "/sys/bus/w1/devices/42-111111111111/w1_slave",
+ "injected_value": UnsupportResponseException,
+ "result": "unknown",
+ "unit": TEMP_CELSIUS,
+ "class": DEVICE_CLASS_TEMPERATURE,
+ },
+ ],
+ },
+ "42-111111111112": {
+ "device_info": {
+ "identifiers": {(DOMAIN, "42-111111111112")},
+ "manufacturer": "Maxim Integrated",
+ "model": "42",
+ "name": "42-111111111112",
+ },
+ SENSOR_DOMAIN: [
+ {
+ "entity_id": "sensor.42_111111111112_temperature",
+ "unique_id": "/sys/bus/w1/devices/42-111111111112/w1_slave",
+ "injected_value": [UnsupportResponseException] * 9 + ["27.993"],
+ "result": "28.0",
+ "unit": TEMP_CELSIUS,
+ "class": DEVICE_CLASS_TEMPERATURE,
+ },
+ ],
+ },
+ "42-111111111113": {
+ "device_info": {
+ "identifiers": {(DOMAIN, "42-111111111113")},
+ "manufacturer": "Maxim Integrated",
+ "model": "42",
+ "name": "42-111111111113",
+ },
+ SENSOR_DOMAIN: [
+ {
+ "entity_id": "sensor.42_111111111113_temperature",
+ "unique_id": "/sys/bus/w1/devices/42-111111111113/w1_slave",
+ "injected_value": [UnsupportResponseException] * 10 + ["27.993"],
+ "result": "unknown",
+ "unit": TEMP_CELSIUS,
+ "class": DEVICE_CLASS_TEMPERATURE,
+ },
+ ],
+ },
+ "EF-111111111111": {
+ SENSOR_DOMAIN: [],
+ },
+ "EF-111111111112": {
+ SENSOR_DOMAIN: [],
+ },
+}
diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py
index aee1641c24fe9a..91ae472278ab73 100644
--- a/tests/components/onewire/test_binary_sensor.py
+++ b/tests/components/onewire/test_binary_sensor.py
@@ -2,40 +2,25 @@
import copy
from unittest.mock import patch
-from pyownet.protocol import Error as ProtocolError
import pytest
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.onewire.binary_sensor import DEVICE_BINARY_SENSORS
-from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.setup import async_setup_component
-from . import setup_onewire_patched_owserver_integration
+from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices
+from .const import MOCK_OWPROXY_DEVICES
from tests.common import mock_registry
-MOCK_DEVICE_SENSORS = {
- "12.111111111111": {
- "inject_reads": [
- b"DS2406", # read device type
- ],
- BINARY_SENSOR_DOMAIN: [
- {
- "entity_id": "binary_sensor.12_111111111111_sensed_a",
- "injected_value": b" 1",
- "result": STATE_ON,
- },
- {
- "entity_id": "binary_sensor.12_111111111111_sensed_b",
- "injected_value": b" 0",
- "result": STATE_OFF,
- },
- ],
- },
+MOCK_BINARY_SENSORS = {
+ key: value
+ for (key, value) in MOCK_OWPROXY_DEVICES.items()
+ if BINARY_SENSOR_DOMAIN in value
}
-@pytest.mark.parametrize("device_id", MOCK_DEVICE_SENSORS.keys())
+@pytest.mark.parametrize("device_id", MOCK_BINARY_SENSORS.keys())
@patch("homeassistant.components.onewire.onewirehub.protocol.proxy")
async def test_owserver_binary_sensor(owproxy, hass, device_id):
"""Test for 1-Wire binary sensor.
@@ -45,30 +30,18 @@ async def test_owserver_binary_sensor(owproxy, hass, device_id):
await async_setup_component(hass, "persistent_notification", {})
entity_registry = mock_registry(hass)
- mock_device_sensor = MOCK_DEVICE_SENSORS[device_id]
+ setup_owproxy_mock_devices(owproxy, BINARY_SENSOR_DOMAIN, [device_id])
- device_family = device_id[0:2]
- dir_return_value = [f"/{device_id}/"]
- read_side_effect = [device_family.encode()]
- if "inject_reads" in mock_device_sensor:
- read_side_effect += mock_device_sensor["inject_reads"]
-
- expected_sensors = mock_device_sensor[BINARY_SENSOR_DOMAIN]
- for expected_sensor in expected_sensors:
- read_side_effect.append(expected_sensor["injected_value"])
-
- # Ensure enough read side effect
- read_side_effect.extend([ProtocolError("Missing injected value")] * 10)
- owproxy.return_value.dir.return_value = dir_return_value
- owproxy.return_value.read.side_effect = read_side_effect
+ mock_device = MOCK_BINARY_SENSORS[device_id]
+ expected_entities = mock_device[BINARY_SENSOR_DOMAIN]
# Force enable binary sensors
patch_device_binary_sensors = copy.deepcopy(DEVICE_BINARY_SENSORS)
- for item in patch_device_binary_sensors[device_family]:
+ for item in patch_device_binary_sensors[device_id[0:2]]:
item["default_disabled"] = False
with patch(
- "homeassistant.components.onewire.SUPPORTED_PLATFORMS", [BINARY_SENSOR_DOMAIN]
+ "homeassistant.components.onewire.PLATFORMS", [BINARY_SENSOR_DOMAIN]
), patch.dict(
"homeassistant.components.onewire.binary_sensor.DEVICE_BINARY_SENSORS",
patch_device_binary_sensors,
@@ -76,14 +49,14 @@ async def test_owserver_binary_sensor(owproxy, hass, device_id):
await setup_onewire_patched_owserver_integration(hass)
await hass.async_block_till_done()
- assert len(entity_registry.entities) == len(expected_sensors)
+ assert len(entity_registry.entities) == len(expected_entities)
- for expected_sensor in expected_sensors:
- entity_id = expected_sensor["entity_id"]
+ for expected_entity in expected_entities:
+ entity_id = expected_entity["entity_id"]
registry_entry = entity_registry.entities.get(entity_id)
assert registry_entry is not None
state = hass.states.get(entity_id)
- assert state.state == expected_sensor["result"]
- assert state.attributes["device_file"] == expected_sensor.get(
+ assert state.state == expected_entity["result"]
+ assert state.attributes["device_file"] == expected_entity.get(
"device_file", registry_entry.unique_id
)
diff --git a/tests/components/onewire/test_entity_sysbus.py b/tests/components/onewire/test_entity_sysbus.py
deleted file mode 100644
index 61a38c10f733ac..00000000000000
--- a/tests/components/onewire/test_entity_sysbus.py
+++ /dev/null
@@ -1,175 +0,0 @@
-"""Tests for 1-Wire devices connected on SysBus."""
-from unittest.mock import patch
-
-from pi1wire import InvalidCRCException, UnsupportResponseException
-import pytest
-
-from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN
-from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
-from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS
-from homeassistant.setup import async_setup_component
-
-from tests.common import mock_device_registry, mock_registry
-
-MOCK_CONFIG = {
- SENSOR_DOMAIN: {
- "platform": DOMAIN,
- "mount_dir": DEFAULT_SYSBUS_MOUNT_DIR,
- "names": {
- "10-111111111111": "My DS18B20",
- },
- }
-}
-
-MOCK_DEVICE_SENSORS = {
- "00-111111111111": {"sensors": []},
- "10-111111111111": {
- "device_info": {
- "identifiers": {(DOMAIN, "10-111111111111")},
- "manufacturer": "Maxim Integrated",
- "model": "10",
- "name": "10-111111111111",
- },
- "sensors": [
- {
- "entity_id": "sensor.my_ds18b20_temperature",
- "unique_id": "/sys/bus/w1/devices/10-111111111111/w1_slave",
- "injected_value": 25.123,
- "result": "25.1",
- "unit": TEMP_CELSIUS,
- "class": DEVICE_CLASS_TEMPERATURE,
- },
- ],
- },
- "12-111111111111": {"sensors": []},
- "1D-111111111111": {"sensors": []},
- "22-111111111111": {
- "device_info": {
- "identifiers": {(DOMAIN, "22-111111111111")},
- "manufacturer": "Maxim Integrated",
- "model": "22",
- "name": "22-111111111111",
- },
- "sensors": [
- {
- "entity_id": "sensor.22_111111111111_temperature",
- "unique_id": "/sys/bus/w1/devices/22-111111111111/w1_slave",
- "injected_value": FileNotFoundError,
- "result": "unknown",
- "unit": TEMP_CELSIUS,
- "class": DEVICE_CLASS_TEMPERATURE,
- },
- ],
- },
- "26-111111111111": {"sensors": []},
- "28-111111111111": {
- "device_info": {
- "identifiers": {(DOMAIN, "28-111111111111")},
- "manufacturer": "Maxim Integrated",
- "model": "28",
- "name": "28-111111111111",
- },
- "sensors": [
- {
- "entity_id": "sensor.28_111111111111_temperature",
- "unique_id": "/sys/bus/w1/devices/28-111111111111/w1_slave",
- "injected_value": InvalidCRCException,
- "result": "unknown",
- "unit": TEMP_CELSIUS,
- "class": DEVICE_CLASS_TEMPERATURE,
- },
- ],
- },
- "29-111111111111": {"sensors": []},
- "3B-111111111111": {
- "device_info": {
- "identifiers": {(DOMAIN, "3B-111111111111")},
- "manufacturer": "Maxim Integrated",
- "model": "3B",
- "name": "3B-111111111111",
- },
- "sensors": [
- {
- "entity_id": "sensor.3b_111111111111_temperature",
- "unique_id": "/sys/bus/w1/devices/3B-111111111111/w1_slave",
- "injected_value": 29.993,
- "result": "30.0",
- "unit": TEMP_CELSIUS,
- "class": DEVICE_CLASS_TEMPERATURE,
- },
- ],
- },
- "42-111111111111": {
- "device_info": {
- "identifiers": {(DOMAIN, "42-111111111111")},
- "manufacturer": "Maxim Integrated",
- "model": "42",
- "name": "42-111111111111",
- },
- "sensors": [
- {
- "entity_id": "sensor.42_111111111111_temperature",
- "unique_id": "/sys/bus/w1/devices/42-111111111111/w1_slave",
- "injected_value": UnsupportResponseException,
- "result": "unknown",
- "unit": TEMP_CELSIUS,
- "class": DEVICE_CLASS_TEMPERATURE,
- },
- ],
- },
- "EF-111111111111": {
- "sensors": [],
- },
- "EF-111111111112": {
- "sensors": [],
- },
-}
-
-
-@pytest.mark.parametrize("device_id", MOCK_DEVICE_SENSORS.keys())
-async def test_onewiredirect_setup_valid_device(hass, device_id):
- """Test that sysbus config entry works correctly."""
- entity_registry = mock_registry(hass)
- device_registry = mock_device_registry(hass)
-
- mock_device_sensor = MOCK_DEVICE_SENSORS[device_id]
-
- glob_result = [f"/{DEFAULT_SYSBUS_MOUNT_DIR}/{device_id}"]
- read_side_effect = []
- expected_sensors = mock_device_sensor["sensors"]
- for expected_sensor in expected_sensors:
- read_side_effect.append(expected_sensor["injected_value"])
-
- # Ensure enough read side effect
- read_side_effect.extend([FileNotFoundError("Missing injected value")] * 20)
-
- with patch(
- "homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True
- ), patch("pi1wire._finder.glob.glob", return_value=glob_result,), patch(
- "pi1wire.OneWire.get_temperature",
- side_effect=read_side_effect,
- ):
- assert await async_setup_component(hass, SENSOR_DOMAIN, MOCK_CONFIG)
- await hass.async_block_till_done()
-
- assert len(entity_registry.entities) == len(expected_sensors)
-
- if len(expected_sensors) > 0:
- device_info = mock_device_sensor["device_info"]
- assert len(device_registry.devices) == 1
- registry_entry = device_registry.async_get_device({(DOMAIN, device_id)})
- assert registry_entry is not None
- assert registry_entry.identifiers == {(DOMAIN, device_id)}
- assert registry_entry.manufacturer == device_info["manufacturer"]
- assert registry_entry.name == device_info["name"]
- assert registry_entry.model == device_info["model"]
-
- for expected_sensor in expected_sensors:
- entity_id = expected_sensor["entity_id"]
- registry_entry = entity_registry.entities.get(entity_id)
- assert registry_entry is not None
- assert registry_entry.unique_id == expected_sensor["unique_id"]
- assert registry_entry.unit_of_measurement == expected_sensor["unit"]
- assert registry_entry.device_class == expected_sensor["class"]
- state = hass.states.get(entity_id)
- assert state.state == expected_sensor["result"]
diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py
index 38e97206698f96..5783b241a2f635 100644
--- a/tests/components/onewire/test_init.py
+++ b/tests/components/onewire/test_init.py
@@ -4,6 +4,7 @@
from pyownet.protocol import ConnError, OwnetError
from homeassistant.components.onewire.const import CONF_TYPE_OWSERVER, DOMAIN
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import (
CONN_CLASS_LOCAL_POLL,
ENTRY_STATE_LOADED,
@@ -11,10 +12,17 @@
ENTRY_STATE_SETUP_RETRY,
)
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
+from homeassistant.helpers import device_registry as dr, entity_registry as er
+from homeassistant.setup import async_setup_component
+
+from . import (
+ setup_onewire_owserver_integration,
+ setup_onewire_patched_owserver_integration,
+ setup_onewire_sysbus_integration,
+ setup_owproxy_mock_devices,
+)
-from . import setup_onewire_owserver_integration, setup_onewire_sysbus_integration
-
-from tests.common import MockConfigEntry
+from tests.common import MockConfigEntry, mock_device_registry, mock_registry
async def test_owserver_connect_failure(hass):
@@ -87,3 +95,41 @@ async def test_unload_entry(hass):
assert config_entry_owserver.state == ENTRY_STATE_NOT_LOADED
assert config_entry_sysbus.state == ENTRY_STATE_NOT_LOADED
assert not hass.data.get(DOMAIN)
+
+
+@patch("homeassistant.components.onewire.onewirehub.protocol.proxy")
+async def test_registry_cleanup(owproxy, hass):
+ """Test for 1-Wire device.
+
+ As they would be on a clean setup: all binary-sensors and switches disabled.
+ """
+ await async_setup_component(hass, "persistent_notification", {})
+ entity_registry = mock_registry(hass)
+ device_registry = mock_device_registry(hass)
+
+ # Initialise with two components
+ setup_owproxy_mock_devices(
+ owproxy, SENSOR_DOMAIN, ["10.111111111111", "28.111111111111"]
+ )
+ with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]):
+ await setup_onewire_patched_owserver_integration(hass)
+ await hass.async_block_till_done()
+
+ assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2
+ assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 2
+
+ # Second item has disappeared from bus, and was removed manually from the front-end
+ setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, ["10.111111111111"])
+ entity_registry.async_remove("sensor.28_111111111111_temperature")
+ await hass.async_block_till_done()
+
+ assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 1
+ assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2
+
+ # Second item has disappeared from bus, and was removed manually from the front-end
+ with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]):
+ await hass.config_entries.async_reload("2")
+ await hass.async_block_till_done()
+
+ assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 1
+ assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 1
diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py
index 9e91da01b2111c..f3063dfc128d30 100644
--- a/tests/components/onewire/test_sensor.py
+++ b/tests/components/onewire/test_sensor.py
@@ -4,54 +4,33 @@
from pyownet.protocol import Error as ProtocolError
import pytest
-from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN
+from homeassistant.components.onewire.const import (
+ DEFAULT_SYSBUS_MOUNT_DIR,
+ DOMAIN,
+ PLATFORMS,
+)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.setup import async_setup_component
-from . import setup_onewire_patched_owserver_integration
+from . import (
+ setup_onewire_patched_owserver_integration,
+ setup_owproxy_mock_devices,
+ setup_sysbus_mock_devices,
+)
+from .const import MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES
-from tests.common import assert_setup_component, mock_registry
+from tests.common import assert_setup_component, mock_device_registry, mock_registry
MOCK_COUPLERS = {
- "1F.111111111111": {
- "inject_reads": [
- b"DS2409", # read device type
- ],
- "branches": {
- "aux": {},
- "main": {
- "1D.111111111111": {
- "inject_reads": [
- b"DS2423", # read device type
- ],
- "device_info": {
- "identifiers": {(DOMAIN, "1D.111111111111")},
- "manufacturer": "Maxim Integrated",
- "model": "DS2423",
- "name": "1D.111111111111",
- },
- SENSOR_DOMAIN: [
- {
- "entity_id": "sensor.1d_111111111111_counter_a",
- "device_file": "/1F.111111111111/main/1D.111111111111/counter.A",
- "unique_id": "/1D.111111111111/counter.A",
- "injected_value": b" 251123",
- "result": "251123",
- "unit": "count",
- "class": None,
- },
- {
- "entity_id": "sensor.1d_111111111111_counter_b",
- "device_file": "/1F.111111111111/main/1D.111111111111/counter.B",
- "unique_id": "/1D.111111111111/counter.B",
- "injected_value": b" 248125",
- "result": "248125",
- "unit": "count",
- "class": None,
- },
- ],
- },
- },
+ key: value for (key, value) in MOCK_OWPROXY_DEVICES.items() if "branches" in value
+}
+
+MOCK_SYSBUS_CONFIG = {
+ SENSOR_DOMAIN: {
+ "platform": DOMAIN,
+ "mount_dir": DEFAULT_SYSBUS_MOUNT_DIR,
+ "names": {
+ "10-111111111111": "My DS18B20",
},
}
}
@@ -134,7 +113,7 @@ async def test_sensors_on_owserver_coupler(owproxy, hass, device_id):
owproxy.return_value.dir.side_effect = dir_side_effect
owproxy.return_value.read.side_effect = read_side_effect
- with patch("homeassistant.components.onewire.SUPPORTED_PLATFORMS", [SENSOR_DOMAIN]):
+ with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]):
await setup_onewire_patched_owserver_integration(hass)
await hass.async_block_till_done()
@@ -154,3 +133,100 @@ async def test_sensors_on_owserver_coupler(owproxy, hass, device_id):
else:
assert state.state == expected_sensor["result"]
assert state.attributes["device_file"] == expected_sensor["device_file"]
+
+
+@pytest.mark.parametrize("device_id", MOCK_OWPROXY_DEVICES.keys())
+@pytest.mark.parametrize("platform", PLATFORMS)
+@patch("homeassistant.components.onewire.onewirehub.protocol.proxy")
+async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform):
+ """Test for 1-Wire device.
+
+ As they would be on a clean setup: all binary-sensors and switches disabled.
+ """
+ await async_setup_component(hass, "persistent_notification", {})
+ entity_registry = mock_registry(hass)
+ device_registry = mock_device_registry(hass)
+
+ setup_owproxy_mock_devices(owproxy, platform, [device_id])
+
+ mock_device = MOCK_OWPROXY_DEVICES[device_id]
+ expected_entities = mock_device.get(platform, [])
+
+ with patch("homeassistant.components.onewire.PLATFORMS", [platform]):
+ await setup_onewire_patched_owserver_integration(hass)
+ await hass.async_block_till_done()
+
+ assert len(entity_registry.entities) == len(expected_entities)
+
+ if len(expected_entities) > 0:
+ device_info = mock_device["device_info"]
+ assert len(device_registry.devices) == 1
+ registry_entry = device_registry.async_get_device({(DOMAIN, device_id)})
+ assert registry_entry is not None
+ assert registry_entry.identifiers == {(DOMAIN, device_id)}
+ assert registry_entry.manufacturer == device_info["manufacturer"]
+ assert registry_entry.name == device_info["name"]
+ assert registry_entry.model == device_info["model"]
+
+ for expected_entity in expected_entities:
+ entity_id = expected_entity["entity_id"]
+ registry_entry = entity_registry.entities.get(entity_id)
+ assert registry_entry is not None
+ assert registry_entry.unique_id == expected_entity["unique_id"]
+ assert registry_entry.unit_of_measurement == expected_entity["unit"]
+ assert registry_entry.device_class == expected_entity["class"]
+ assert registry_entry.disabled == expected_entity.get("disabled", False)
+ state = hass.states.get(entity_id)
+ if registry_entry.disabled:
+ assert state is None
+ else:
+ assert state.state == expected_entity["result"]
+ assert state.attributes["device_file"] == expected_entity.get(
+ "device_file", registry_entry.unique_id
+ )
+
+
+@pytest.mark.parametrize("device_id", MOCK_SYSBUS_DEVICES.keys())
+async def test_onewiredirect_setup_valid_device(hass, device_id):
+ """Test that sysbus config entry works correctly."""
+ await async_setup_component(hass, "persistent_notification", {})
+ entity_registry = mock_registry(hass)
+ device_registry = mock_device_registry(hass)
+
+ glob_result, read_side_effect = setup_sysbus_mock_devices(
+ SENSOR_DOMAIN, [device_id]
+ )
+
+ mock_device = MOCK_SYSBUS_DEVICES[device_id]
+ expected_entities = mock_device.get(SENSOR_DOMAIN, [])
+
+ with patch(
+ "homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True
+ ), patch("pi1wire._finder.glob.glob", return_value=glob_result,), patch(
+ "pi1wire.OneWire.get_temperature",
+ side_effect=read_side_effect,
+ ):
+ assert await async_setup_component(hass, SENSOR_DOMAIN, MOCK_SYSBUS_CONFIG)
+ await hass.async_block_till_done()
+
+ assert len(entity_registry.entities) == len(expected_entities)
+
+ if len(expected_entities) > 0:
+ device_info = mock_device["device_info"]
+ assert len(device_registry.devices) == 1
+ registry_entry = device_registry.async_get_device({(DOMAIN, device_id)})
+ assert registry_entry is not None
+ assert registry_entry.identifiers == {(DOMAIN, device_id)}
+ assert registry_entry.manufacturer == device_info["manufacturer"]
+ assert registry_entry.name == device_info["name"]
+ assert registry_entry.model == device_info["model"]
+
+ for expected_sensor in expected_entities:
+ entity_id = expected_sensor["entity_id"]
+ registry_entry = entity_registry.entities.get(entity_id)
+ assert registry_entry is not None
+ assert registry_entry.unique_id == expected_sensor["unique_id"]
+ assert registry_entry.unit_of_measurement == expected_sensor["unit"]
+ assert registry_entry.device_class == expected_sensor["class"]
+ state = hass.states.get(entity_id)
+ assert state.state == expected_sensor["result"]
diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py
index 1c778d4e264df3..91a9e32e902ca6 100644
--- a/tests/components/onewire/test_switch.py
+++ b/tests/components/onewire/test_switch.py
@@ -2,7 +2,6 @@
import copy
from unittest.mock import patch
-from pyownet.protocol import Error as ProtocolError
import pytest
from homeassistant.components.onewire.switch import DEVICE_SWITCHES
@@ -10,58 +9,19 @@
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TOGGLE, STATE_OFF, STATE_ON
from homeassistant.setup import async_setup_component
-from . import setup_onewire_patched_owserver_integration
+from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices
+from .const import MOCK_OWPROXY_DEVICES
from tests.common import mock_registry
-MOCK_DEVICE_SENSORS = {
- "12.111111111111": {
- "inject_reads": [
- b"DS2406", # read device type
- ],
- SWITCH_DOMAIN: [
- {
- "entity_id": "switch.12_111111111111_pio_a",
- "unique_id": "/12.111111111111/PIO.A",
- "injected_value": b" 1",
- "result": STATE_ON,
- "unit": None,
- "class": None,
- "disabled": True,
- },
- {
- "entity_id": "switch.12_111111111111_pio_b",
- "unique_id": "/12.111111111111/PIO.B",
- "injected_value": b" 0",
- "result": STATE_OFF,
- "unit": None,
- "class": None,
- "disabled": True,
- },
- {
- "entity_id": "switch.12_111111111111_latch_a",
- "unique_id": "/12.111111111111/latch.A",
- "injected_value": b" 1",
- "result": STATE_ON,
- "unit": None,
- "class": None,
- "disabled": True,
- },
- {
- "entity_id": "switch.12_111111111111_latch_b",
- "unique_id": "/12.111111111111/latch.B",
- "injected_value": b" 0",
- "result": STATE_OFF,
- "unit": None,
- "class": None,
- "disabled": True,
- },
- ],
- }
+MOCK_SWITCHES = {
+ key: value
+ for (key, value) in MOCK_OWPROXY_DEVICES.items()
+ if SWITCH_DOMAIN in value
}
-@pytest.mark.parametrize("device_id", ["12.111111111111"])
+@pytest.mark.parametrize("device_id", MOCK_SWITCHES.keys())
@patch("homeassistant.components.onewire.onewirehub.protocol.proxy")
async def test_owserver_switch(owproxy, hass, device_id):
"""Test for 1-Wire switch.
@@ -71,51 +31,39 @@ async def test_owserver_switch(owproxy, hass, device_id):
await async_setup_component(hass, "persistent_notification", {})
entity_registry = mock_registry(hass)
- mock_device_sensor = MOCK_DEVICE_SENSORS[device_id]
+ setup_owproxy_mock_devices(owproxy, SWITCH_DOMAIN, [device_id])
- device_family = device_id[0:2]
- dir_return_value = [f"/{device_id}/"]
- read_side_effect = [device_family.encode()]
- if "inject_reads" in mock_device_sensor:
- read_side_effect += mock_device_sensor["inject_reads"]
-
- expected_sensors = mock_device_sensor[SWITCH_DOMAIN]
- for expected_sensor in expected_sensors:
- read_side_effect.append(expected_sensor["injected_value"])
-
- # Ensure enough read side effect
- read_side_effect.extend([ProtocolError("Missing injected value")] * 10)
- owproxy.return_value.dir.return_value = dir_return_value
- owproxy.return_value.read.side_effect = read_side_effect
+ mock_device = MOCK_SWITCHES[device_id]
+ expected_entities = mock_device[SWITCH_DOMAIN]
# Force enable switches
patch_device_switches = copy.deepcopy(DEVICE_SWITCHES)
- for item in patch_device_switches[device_family]:
+ for item in patch_device_switches[device_id[0:2]]:
item["default_disabled"] = False
with patch(
- "homeassistant.components.onewire.SUPPORTED_PLATFORMS", [SWITCH_DOMAIN]
+ "homeassistant.components.onewire.PLATFORMS", [SWITCH_DOMAIN]
), patch.dict(
"homeassistant.components.onewire.switch.DEVICE_SWITCHES", patch_device_switches
):
await setup_onewire_patched_owserver_integration(hass)
await hass.async_block_till_done()
- assert len(entity_registry.entities) == len(expected_sensors)
+ assert len(entity_registry.entities) == len(expected_entities)
- for expected_sensor in expected_sensors:
- entity_id = expected_sensor["entity_id"]
+ for expected_entity in expected_entities:
+ entity_id = expected_entity["entity_id"]
registry_entry = entity_registry.entities.get(entity_id)
assert registry_entry is not None
state = hass.states.get(entity_id)
- assert state.state == expected_sensor["result"]
+ assert state.state == expected_entity["result"]
if state.state == STATE_ON:
owproxy.return_value.read.side_effect = [b" 0"]
- expected_sensor["result"] = STATE_OFF
+ expected_entity["result"] = STATE_OFF
elif state.state == STATE_OFF:
owproxy.return_value.read.side_effect = [b" 1"]
- expected_sensor["result"] = STATE_ON
+ expected_entity["result"] = STATE_ON
await hass.services.async_call(
SWITCH_DOMAIN,
@@ -126,7 +74,7 @@ async def test_owserver_switch(owproxy, hass, device_id):
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert state.state == expected_sensor["result"]
- assert state.attributes["device_file"] == expected_sensor.get(
+ assert state.state == expected_entity["result"]
+ assert state.attributes["device_file"] == expected_entity.get(
"device_file", registry_entry.unique_id
)
diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py
index 4d811b9f9859bc..6713bfecdaab2d 100644
--- a/tests/components/opentherm_gw/test_config_flow.py
+++ b/tests/components/opentherm_gw/test_config_flow.py
@@ -9,9 +9,18 @@
from homeassistant.components.opentherm_gw.const import (
CONF_FLOOR_TEMP,
CONF_PRECISION,
+ CONF_READ_PRECISION,
+ CONF_SET_PRECISION,
+ CONF_TEMPORARY_OVRD_MODE,
DOMAIN,
)
-from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME, PRECISION_HALVES
+from homeassistant.const import (
+ CONF_DEVICE,
+ CONF_ID,
+ CONF_NAME,
+ PRECISION_HALVES,
+ PRECISION_TENTHS,
+)
from tests.common import MockConfigEntry
@@ -167,6 +176,48 @@ async def test_form_connection_error(hass):
assert len(mock_connect.mock_calls) == 1
+async def test_options_migration(hass):
+ """Test migration of precision option after update."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="Mock Gateway",
+ data={
+ CONF_NAME: "Test Entry 1",
+ CONF_DEVICE: "/dev/ttyUSB0",
+ CONF_ID: "test_entry_1",
+ },
+ options={
+ CONF_FLOOR_TEMP: True,
+ CONF_PRECISION: PRECISION_TENTHS,
+ },
+ )
+ entry.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.connect_and_subscribe",
+ return_value=True,
+ ), patch("homeassistant.components.opentherm_gw.async_setup", return_value=True):
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.options.async_init(
+ entry.entry_id, context={"source": config_entries.SOURCE_USER}, data=None
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["data"][CONF_READ_PRECISION] == PRECISION_TENTHS
+ assert result["data"][CONF_SET_PRECISION] == PRECISION_TENTHS
+ assert result["data"][CONF_FLOOR_TEMP] is True
+
+
async def test_options_form(hass):
"""Test the options form."""
entry = MockConfigEntry(
@@ -181,6 +232,14 @@ async def test_options_form(hass):
)
entry.add_to_hass(hass)
+ with patch(
+ "homeassistant.components.opentherm_gw.async_setup", return_value=True
+ ), patch(
+ "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True
+ ):
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
result = await hass.config_entries.options.async_init(
entry.entry_id, context={"source": "test"}, data=None
)
@@ -189,11 +248,18 @@ async def test_options_form(hass):
result = await hass.config_entries.options.async_configure(
result["flow_id"],
- user_input={CONF_FLOOR_TEMP: True, CONF_PRECISION: PRECISION_HALVES},
+ user_input={
+ CONF_FLOOR_TEMP: True,
+ CONF_READ_PRECISION: PRECISION_HALVES,
+ CONF_SET_PRECISION: PRECISION_HALVES,
+ CONF_TEMPORARY_OVRD_MODE: True,
+ },
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result["data"][CONF_PRECISION] == PRECISION_HALVES
+ assert result["data"][CONF_READ_PRECISION] == PRECISION_HALVES
+ assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES
+ assert result["data"][CONF_TEMPORARY_OVRD_MODE] is True
assert result["data"][CONF_FLOOR_TEMP] is True
result = await hass.config_entries.options.async_init(
@@ -201,9 +267,31 @@ async def test_options_form(hass):
)
result = await hass.config_entries.options.async_configure(
- result["flow_id"], user_input={CONF_PRECISION: 0}
+ result["flow_id"], user_input={CONF_READ_PRECISION: 0}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result["data"][CONF_PRECISION] == 0.0
+ assert result["data"][CONF_READ_PRECISION] == 0.0
+ assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES
+ assert result["data"][CONF_TEMPORARY_OVRD_MODE] is True
assert result["data"][CONF_FLOOR_TEMP] is True
+
+ result = await hass.config_entries.options.async_init(
+ entry.entry_id, context={"source": "test"}, data=None
+ )
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_FLOOR_TEMP: False,
+ CONF_READ_PRECISION: PRECISION_TENTHS,
+ CONF_SET_PRECISION: PRECISION_HALVES,
+ CONF_TEMPORARY_OVRD_MODE: False,
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["data"][CONF_READ_PRECISION] == PRECISION_TENTHS
+ assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES
+ assert result["data"][CONF_TEMPORARY_OVRD_MODE] is False
+ assert result["data"][CONF_FLOOR_TEMP] is False
diff --git a/tests/components/opentherm_gw/test_init.py b/tests/components/opentherm_gw/test_init.py
index b28f869e1e6795..554f58fd81b05c 100644
--- a/tests/components/opentherm_gw/test_init.py
+++ b/tests/components/opentherm_gw/test_init.py
@@ -6,6 +6,7 @@
from homeassistant import setup
from homeassistant.components.opentherm_gw.const import DOMAIN
from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME
+from homeassistant.helpers import device_registry as dr
from tests.common import MockConfigEntry, mock_device_registry
@@ -38,7 +39,7 @@ async def test_device_registry_insert(hass):
await hass.async_block_till_done()
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
gw_dev = device_registry.async_get_device(identifiers={(DOMAIN, MOCK_GATEWAY_ID)})
assert gw_dev.sw_version == VERSION_OLD
diff --git a/tests/components/ozw/conftest.py b/tests/components/ozw/conftest.py
index a59388f118f675..1df365054d4a63 100644
--- a/tests/components/ozw/conftest.py
+++ b/tests/components/ozw/conftest.py
@@ -9,7 +9,7 @@
from .common import MQTTMessage
from tests.common import MockConfigEntry, load_fixture
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
@pytest.fixture(name="generic_data", scope="session")
diff --git a/tests/components/ozw/test_binary_sensor.py b/tests/components/ozw/test_binary_sensor.py
index 62b23be0ccae05..95b150b579141e 100644
--- a/tests/components/ozw/test_binary_sensor.py
+++ b/tests/components/ozw/test_binary_sensor.py
@@ -5,6 +5,7 @@
)
from homeassistant.components.ozw.const import DOMAIN
from homeassistant.const import ATTR_DEVICE_CLASS
+from homeassistant.helpers import entity_registry as er
from .common import setup_ozw
@@ -14,7 +15,7 @@ async def test_binary_sensor(hass, generic_data, binary_sensor_msg):
receive_msg = await setup_ozw(hass, fixture=generic_data)
# Test Legacy sensor (disabled by default)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entity_id = "binary_sensor.trisensor_sensor"
state = hass.states.get(entity_id)
assert state is None
@@ -46,7 +47,7 @@ async def test_binary_sensor(hass, generic_data, binary_sensor_msg):
async def test_sensor_enabled(hass, generic_data, binary_sensor_alt_msg):
"""Test enabling a legacy binary_sensor."""
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get_or_create(
BINARY_SENSOR_DOMAIN,
diff --git a/tests/components/ozw/test_init.py b/tests/components/ozw/test_init.py
index c76bfd4a3a05d1..339b690f4e4a67 100644
--- a/tests/components/ozw/test_init.py
+++ b/tests/components/ozw/test_init.py
@@ -4,6 +4,7 @@
from homeassistant import config_entries
from homeassistant.components.hassio.handler import HassioAPIError
from homeassistant.components.ozw import DOMAIN, PLATFORMS, const
+from homeassistant.const import ATTR_RESTORED, STATE_UNAVAILABLE
from .common import setup_ozw
@@ -53,6 +54,7 @@ async def test_publish_without_mqtt(hass, caplog):
# Sending a message should not error with the MQTT integration not set up.
send_message("test_topic", "test_payload")
+ await hass.async_block_till_done()
assert "MQTT integration is not set up" in caplog.text
@@ -75,14 +77,21 @@ async def test_unload_entry(hass, generic_data, switch_msg, caplog):
await hass.config_entries.async_unload(entry.entry_id)
assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED
- assert len(hass.states.async_entity_ids("switch")) == 0
+ entities = hass.states.async_entity_ids("switch")
+ assert len(entities) == 1
+ for entity in entities:
+ assert hass.states.get(entity).state == STATE_UNAVAILABLE
+ assert hass.states.get(entity).attributes.get(ATTR_RESTORED)
# Send a message for a switch from the broker to check that
# all entity topic subscribers are unsubscribed.
receive_message(switch_msg)
await hass.async_block_till_done()
- assert len(hass.states.async_entity_ids("switch")) == 0
+ assert len(hass.states.async_entity_ids("switch")) == 1
+ for entity in entities:
+ assert hass.states.get(entity).state == STATE_UNAVAILABLE
+ assert hass.states.get(entity).attributes.get(ATTR_RESTORED)
# Load the integration again and check that there are no errors when
# adding the entities.
@@ -127,8 +136,8 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog):
await hass.config_entries.async_remove(entry.entry_id)
- stop_addon.call_count == 1
- uninstall_addon.call_count == 1
+ assert stop_addon.call_count == 1
+ assert uninstall_addon.call_count == 1
assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
stop_addon.reset_mock()
@@ -141,8 +150,8 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog):
await hass.config_entries.async_remove(entry.entry_id)
- stop_addon.call_count == 1
- uninstall_addon.call_count == 0
+ assert stop_addon.call_count == 1
+ assert uninstall_addon.call_count == 0
assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
assert "Failed to stop the OpenZWave add-on" in caplog.text
@@ -157,8 +166,8 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog):
await hass.config_entries.async_remove(entry.entry_id)
- stop_addon.call_count == 1
- uninstall_addon.call_count == 1
+ assert stop_addon.call_count == 1
+ assert uninstall_addon.call_count == 1
assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
assert "Failed to uninstall the OpenZWave add-on" in caplog.text
diff --git a/tests/components/ozw/test_migration.py b/tests/components/ozw/test_migration.py
index d83a39f2b15331..076974bc48f30a 100644
--- a/tests/components/ozw/test_migration.py
+++ b/tests/components/ozw/test_migration.py
@@ -4,14 +4,7 @@
import pytest
from homeassistant.components.ozw.websocket_api import ID, TYPE
-from homeassistant.helpers.device_registry import (
- DeviceEntry,
- async_get_registry as async_get_device_registry,
-)
-from homeassistant.helpers.entity_registry import (
- RegistryEntry,
- async_get_registry as async_get_entity_registry,
-)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from .common import setup_ozw
@@ -41,35 +34,35 @@
@pytest.fixture(name="zwave_migration_data")
def zwave_migration_data_fixture(hass):
"""Return mock zwave migration data."""
- zwave_source_node_device = DeviceEntry(
+ zwave_source_node_device = dr.DeviceEntry(
id=ZWAVE_SOURCE_NODE_DEVICE_ID,
name_by_user=ZWAVE_SOURCE_NODE_DEVICE_NAME,
area_id=ZWAVE_SOURCE_NODE_DEVICE_AREA,
)
- zwave_source_node_entry = RegistryEntry(
+ zwave_source_node_entry = er.RegistryEntry(
entity_id=ZWAVE_SOURCE_ENTITY,
unique_id=ZWAVE_SOURCE_NODE_UNIQUE_ID,
platform="zwave",
name="Z-Wave Source Node",
)
- zwave_battery_device = DeviceEntry(
+ zwave_battery_device = dr.DeviceEntry(
id=ZWAVE_BATTERY_DEVICE_ID,
name_by_user=ZWAVE_BATTERY_DEVICE_NAME,
area_id=ZWAVE_BATTERY_DEVICE_AREA,
)
- zwave_battery_entry = RegistryEntry(
+ zwave_battery_entry = er.RegistryEntry(
entity_id=ZWAVE_BATTERY_ENTITY,
unique_id=ZWAVE_BATTERY_UNIQUE_ID,
platform="zwave",
name=ZWAVE_BATTERY_NAME,
icon=ZWAVE_BATTERY_ICON,
)
- zwave_power_device = DeviceEntry(
+ zwave_power_device = dr.DeviceEntry(
id=ZWAVE_POWER_DEVICE_ID,
name_by_user=ZWAVE_POWER_DEVICE_NAME,
area_id=ZWAVE_POWER_DEVICE_AREA,
)
- zwave_power_entry = RegistryEntry(
+ zwave_power_entry = er.RegistryEntry(
entity_id=ZWAVE_POWER_ENTITY,
unique_id=ZWAVE_POWER_UNIQUE_ID,
platform="zwave",
@@ -169,8 +162,8 @@ async def test_migrate_zwave(hass, migration_data, hass_ws_client, zwave_integra
assert result["migration_entity_map"] == migration_entity_map
assert result["migrated"] is True
- dev_reg = await async_get_device_registry(hass)
- ent_reg = await async_get_entity_registry(hass)
+ dev_reg = dr.async_get(hass)
+ ent_reg = er.async_get(hass)
# check the device registry migration
@@ -252,7 +245,7 @@ async def test_migrate_zwave_dry_run(
assert result["migration_entity_map"] == migration_entity_map
assert result["migrated"] is False
- ent_reg = await async_get_entity_registry(hass)
+ ent_reg = er.async_get(hass)
# no real migration should have been done
assert ent_reg.async_is_registered("sensor.water_sensor_6_battery_level")
diff --git a/tests/components/ozw/test_sensor.py b/tests/components/ozw/test_sensor.py
index 91de895648e134..500bd81aa0be20 100644
--- a/tests/components/ozw/test_sensor.py
+++ b/tests/components/ozw/test_sensor.py
@@ -7,6 +7,7 @@
DOMAIN as SENSOR_DOMAIN,
)
from homeassistant.const import ATTR_DEVICE_CLASS
+from homeassistant.helpers import entity_registry as er
from .common import setup_ozw
@@ -34,7 +35,7 @@ async def test_sensor(hass, generic_data):
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER
# Test ZWaveListSensor disabled by default
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entity_id = "sensor.water_sensor_6_instance_1_water"
state = hass.states.get(entity_id)
assert state is None
@@ -55,7 +56,7 @@ async def test_sensor(hass, generic_data):
async def test_sensor_enabled(hass, generic_data, sensor_msg):
"""Test enabling an advanced sensor."""
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get_or_create(
SENSOR_DOMAIN,
@@ -79,7 +80,7 @@ async def test_sensor_enabled(hass, generic_data, sensor_msg):
async def test_string_sensor(hass, string_sensor_data):
"""Test so the returned type is a string sensor."""
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry = registry.async_get_or_create(
SENSOR_DOMAIN,
diff --git a/tests/components/panasonic_viera/conftest.py b/tests/components/panasonic_viera/conftest.py
new file mode 100644
index 00000000000000..d1444f014777de
--- /dev/null
+++ b/tests/components/panasonic_viera/conftest.py
@@ -0,0 +1,104 @@
+"""Test helpers for Panasonic Viera."""
+
+from unittest.mock import Mock, patch
+
+from panasonic_viera import TV_TYPE_ENCRYPTED, TV_TYPE_NONENCRYPTED
+import pytest
+
+from homeassistant.components.panasonic_viera.const import (
+ ATTR_FRIENDLY_NAME,
+ ATTR_MANUFACTURER,
+ ATTR_MODEL_NUMBER,
+ ATTR_UDN,
+ CONF_APP_ID,
+ CONF_ENCRYPTION_KEY,
+ CONF_ON_ACTION,
+ DEFAULT_MANUFACTURER,
+ DEFAULT_MODEL_NUMBER,
+ DEFAULT_NAME,
+ DEFAULT_PORT,
+)
+from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
+
+MOCK_BASIC_DATA = {
+ CONF_HOST: "0.0.0.0",
+ CONF_NAME: DEFAULT_NAME,
+}
+
+MOCK_CONFIG_DATA = {
+ **MOCK_BASIC_DATA,
+ CONF_PORT: DEFAULT_PORT,
+ CONF_ON_ACTION: None,
+}
+
+MOCK_ENCRYPTION_DATA = {
+ CONF_APP_ID: "mock-app-id",
+ CONF_ENCRYPTION_KEY: "mock-encryption-key",
+}
+
+MOCK_DEVICE_INFO = {
+ ATTR_FRIENDLY_NAME: DEFAULT_NAME,
+ ATTR_MANUFACTURER: DEFAULT_MANUFACTURER,
+ ATTR_MODEL_NUMBER: DEFAULT_MODEL_NUMBER,
+ ATTR_UDN: "mock-unique-id",
+}
+
+
+def get_mock_remote(
+ request_error=None,
+ authorize_error=None,
+ encrypted=False,
+ app_id=None,
+ encryption_key=None,
+ device_info=MOCK_DEVICE_INFO,
+):
+ """Return a mock remote."""
+ mock_remote = Mock()
+
+ mock_remote.type = TV_TYPE_ENCRYPTED if encrypted else TV_TYPE_NONENCRYPTED
+ mock_remote.app_id = app_id
+ mock_remote.enc_key = encryption_key
+
+ def request_pin_code(name=None):
+ if request_error is not None:
+ raise request_error
+
+ mock_remote.request_pin_code = request_pin_code
+
+ def authorize_pin_code(pincode):
+ if pincode == "1234":
+ return
+
+ if authorize_error is not None:
+ raise authorize_error
+
+ mock_remote.authorize_pin_code = authorize_pin_code
+
+ def get_device_info():
+ return device_info
+
+ mock_remote.get_device_info = get_device_info
+
+ def send_key(key):
+ return
+
+ mock_remote.send_key = Mock(send_key)
+
+ def get_volume(key):
+ return 100
+
+ mock_remote.get_volume = Mock(get_volume)
+
+ return mock_remote
+
+
+@pytest.fixture(name="mock_remote")
+def mock_remote_fixture():
+ """Patch the library remote."""
+ mock_remote = get_mock_remote()
+
+ with patch(
+ "homeassistant.components.panasonic_viera.RemoteControl",
+ return_value=mock_remote,
+ ):
+ yield mock_remote
diff --git a/tests/components/panasonic_viera/test_config_flow.py b/tests/components/panasonic_viera/test_config_flow.py
index e099862604a2d3..dd7f629c29bf4e 100644
--- a/tests/components/panasonic_viera/test_config_flow.py
+++ b/tests/components/panasonic_viera/test_config_flow.py
@@ -1,90 +1,28 @@
"""Test the Panasonic Viera config flow."""
-from unittest.mock import Mock, patch
+from unittest.mock import patch
-from panasonic_viera import TV_TYPE_ENCRYPTED, TV_TYPE_NONENCRYPTED, SOAPError
-import pytest
+from panasonic_viera import SOAPError
from homeassistant import config_entries
from homeassistant.components.panasonic_viera.const import (
ATTR_DEVICE_INFO,
- ATTR_FRIENDLY_NAME,
- ATTR_MANUFACTURER,
- ATTR_MODEL_NUMBER,
- ATTR_UDN,
- CONF_APP_ID,
- CONF_ENCRYPTION_KEY,
- CONF_ON_ACTION,
- DEFAULT_MANUFACTURER,
- DEFAULT_MODEL_NUMBER,
DEFAULT_NAME,
- DEFAULT_PORT,
DOMAIN,
ERROR_INVALID_PIN_CODE,
)
-from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT
+from homeassistant.const import CONF_PIN
+
+from .conftest import (
+ MOCK_BASIC_DATA,
+ MOCK_CONFIG_DATA,
+ MOCK_DEVICE_INFO,
+ MOCK_ENCRYPTION_DATA,
+ get_mock_remote,
+)
from tests.common import MockConfigEntry
-@pytest.fixture(name="panasonic_viera_setup", autouse=True)
-def panasonic_viera_setup_fixture():
- """Mock panasonic_viera setup."""
- with patch(
- "homeassistant.components.panasonic_viera.async_setup", return_value=True
- ), patch(
- "homeassistant.components.panasonic_viera.async_setup_entry",
- return_value=True,
- ):
- yield
-
-
-def get_mock_remote(
- host="1.2.3.4",
- request_error=None,
- authorize_error=None,
- encrypted=False,
- app_id=None,
- encryption_key=None,
- name=DEFAULT_NAME,
- manufacturer=DEFAULT_MANUFACTURER,
- model_number=DEFAULT_MODEL_NUMBER,
- unique_id="mock-unique-id",
-):
- """Return a mock remote."""
- mock_remote = Mock()
-
- mock_remote.type = TV_TYPE_ENCRYPTED if encrypted else TV_TYPE_NONENCRYPTED
- mock_remote.app_id = app_id
- mock_remote.enc_key = encryption_key
-
- def request_pin_code(name=None):
- if request_error is not None:
- raise request_error
-
- mock_remote.request_pin_code = request_pin_code
-
- def authorize_pin_code(pincode):
- if pincode == "1234":
- return
-
- if authorize_error is not None:
- raise authorize_error
-
- mock_remote.authorize_pin_code = authorize_pin_code
-
- def get_device_info():
- return {
- ATTR_FRIENDLY_NAME: name,
- ATTR_MANUFACTURER: manufacturer,
- ATTR_MODEL_NUMBER: model_number,
- ATTR_UDN: unique_id,
- }
-
- mock_remote.get_device_info = get_device_info
-
- return mock_remote
-
-
async def test_flow_non_encrypted(hass):
"""Test flow without encryption."""
@@ -103,23 +41,12 @@ async def test_flow_non_encrypted(hass):
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME},
+ MOCK_BASIC_DATA,
)
assert result["type"] == "create_entry"
assert result["title"] == DEFAULT_NAME
- assert result["data"] == {
- CONF_HOST: "1.2.3.4",
- CONF_NAME: DEFAULT_NAME,
- CONF_PORT: DEFAULT_PORT,
- CONF_ON_ACTION: None,
- ATTR_DEVICE_INFO: {
- ATTR_FRIENDLY_NAME: DEFAULT_NAME,
- ATTR_MANUFACTURER: DEFAULT_MANUFACTURER,
- ATTR_MODEL_NUMBER: DEFAULT_MODEL_NUMBER,
- ATTR_UDN: "mock-unique-id",
- },
- }
+ assert result["data"] == {**MOCK_CONFIG_DATA, ATTR_DEVICE_INFO: MOCK_DEVICE_INFO}
async def test_flow_not_connected_error(hass):
@@ -138,7 +65,7 @@ async def test_flow_not_connected_error(hass):
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME},
+ MOCK_BASIC_DATA,
)
assert result["type"] == "form"
@@ -162,7 +89,7 @@ async def test_flow_unknown_abort(hass):
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME},
+ MOCK_BASIC_DATA,
)
assert result["type"] == "abort"
@@ -187,7 +114,7 @@ async def test_flow_encrypted_not_connected_pin_code_request(hass):
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME},
+ MOCK_BASIC_DATA,
)
assert result["type"] == "abort"
@@ -212,7 +139,7 @@ async def test_flow_encrypted_unknown_pin_code_request(hass):
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME},
+ MOCK_BASIC_DATA,
)
assert result["type"] == "abort"
@@ -231,8 +158,8 @@ async def test_flow_encrypted_valid_pin_code(hass):
mock_remote = get_mock_remote(
encrypted=True,
- app_id="test-app-id",
- encryption_key="test-encryption-key",
+ app_id="mock-app-id",
+ encryption_key="mock-encryption-key",
)
with patch(
@@ -241,7 +168,7 @@ async def test_flow_encrypted_valid_pin_code(hass):
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME},
+ MOCK_BASIC_DATA,
)
assert result["type"] == "form"
@@ -255,18 +182,9 @@ async def test_flow_encrypted_valid_pin_code(hass):
assert result["type"] == "create_entry"
assert result["title"] == DEFAULT_NAME
assert result["data"] == {
- CONF_HOST: "1.2.3.4",
- CONF_NAME: DEFAULT_NAME,
- CONF_PORT: DEFAULT_PORT,
- CONF_ON_ACTION: None,
- CONF_APP_ID: "test-app-id",
- CONF_ENCRYPTION_KEY: "test-encryption-key",
- ATTR_DEVICE_INFO: {
- ATTR_FRIENDLY_NAME: DEFAULT_NAME,
- ATTR_MANUFACTURER: DEFAULT_MANUFACTURER,
- ATTR_MODEL_NUMBER: DEFAULT_MODEL_NUMBER,
- ATTR_UDN: "mock-unique-id",
- },
+ **MOCK_CONFIG_DATA,
+ **MOCK_ENCRYPTION_DATA,
+ ATTR_DEVICE_INFO: MOCK_DEVICE_INFO,
}
@@ -288,7 +206,7 @@ async def test_flow_encrypted_invalid_pin_code_error(hass):
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME},
+ MOCK_BASIC_DATA,
)
assert result["type"] == "form"
@@ -326,7 +244,7 @@ async def test_flow_encrypted_not_connected_abort(hass):
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME},
+ MOCK_BASIC_DATA,
)
assert result["type"] == "form"
@@ -359,7 +277,7 @@ async def test_flow_encrypted_unknown_abort(hass):
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME},
+ MOCK_BASIC_DATA,
)
assert result["type"] == "form"
@@ -379,14 +297,14 @@ async def test_flow_non_encrypted_already_configured_abort(hass):
MockConfigEntry(
domain=DOMAIN,
- unique_id="1.2.3.4",
- data={CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME, CONF_PORT: DEFAULT_PORT},
+ unique_id="0.0.0.0",
+ data=MOCK_CONFIG_DATA,
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
- data={CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME},
+ data=MOCK_BASIC_DATA,
)
assert result["type"] == "abort"
@@ -398,20 +316,14 @@ async def test_flow_encrypted_already_configured_abort(hass):
MockConfigEntry(
domain=DOMAIN,
- unique_id="1.2.3.4",
- data={
- CONF_HOST: "1.2.3.4",
- CONF_NAME: DEFAULT_NAME,
- CONF_PORT: DEFAULT_PORT,
- CONF_APP_ID: "test-app-id",
- CONF_ENCRYPTION_KEY: "test-encryption-key",
- },
+ unique_id="0.0.0.0",
+ data={**MOCK_CONFIG_DATA, **MOCK_ENCRYPTION_DATA},
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
- data={CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME},
+ data=MOCK_BASIC_DATA,
)
assert result["type"] == "abort"
@@ -430,28 +342,12 @@ async def test_imported_flow_non_encrypted(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
- data={
- CONF_HOST: "1.2.3.4",
- CONF_NAME: DEFAULT_NAME,
- CONF_PORT: DEFAULT_PORT,
- CONF_ON_ACTION: "test-on-action",
- },
+ data=MOCK_CONFIG_DATA,
)
assert result["type"] == "create_entry"
assert result["title"] == DEFAULT_NAME
- assert result["data"] == {
- CONF_HOST: "1.2.3.4",
- CONF_NAME: DEFAULT_NAME,
- CONF_PORT: DEFAULT_PORT,
- CONF_ON_ACTION: "test-on-action",
- ATTR_DEVICE_INFO: {
- ATTR_FRIENDLY_NAME: DEFAULT_NAME,
- ATTR_MANUFACTURER: DEFAULT_MANUFACTURER,
- ATTR_MODEL_NUMBER: DEFAULT_MODEL_NUMBER,
- ATTR_UDN: "mock-unique-id",
- },
- }
+ assert result["data"] == {**MOCK_CONFIG_DATA, ATTR_DEVICE_INFO: MOCK_DEVICE_INFO}
async def test_imported_flow_encrypted_valid_pin_code(hass):
@@ -459,8 +355,8 @@ async def test_imported_flow_encrypted_valid_pin_code(hass):
mock_remote = get_mock_remote(
encrypted=True,
- app_id="test-app-id",
- encryption_key="test-encryption-key",
+ app_id="mock-app-id",
+ encryption_key="mock-encryption-key",
)
with patch(
@@ -470,12 +366,7 @@ async def test_imported_flow_encrypted_valid_pin_code(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
- data={
- CONF_HOST: "1.2.3.4",
- CONF_NAME: DEFAULT_NAME,
- CONF_PORT: DEFAULT_PORT,
- CONF_ON_ACTION: "test-on-action",
- },
+ data=MOCK_CONFIG_DATA,
)
assert result["type"] == "form"
@@ -489,18 +380,9 @@ async def test_imported_flow_encrypted_valid_pin_code(hass):
assert result["type"] == "create_entry"
assert result["title"] == DEFAULT_NAME
assert result["data"] == {
- CONF_HOST: "1.2.3.4",
- CONF_NAME: DEFAULT_NAME,
- CONF_PORT: DEFAULT_PORT,
- CONF_ON_ACTION: "test-on-action",
- CONF_APP_ID: "test-app-id",
- CONF_ENCRYPTION_KEY: "test-encryption-key",
- ATTR_DEVICE_INFO: {
- ATTR_FRIENDLY_NAME: DEFAULT_NAME,
- ATTR_MANUFACTURER: DEFAULT_MANUFACTURER,
- ATTR_MODEL_NUMBER: DEFAULT_MODEL_NUMBER,
- ATTR_UDN: "mock-unique-id",
- },
+ **MOCK_CONFIG_DATA,
+ **MOCK_ENCRYPTION_DATA,
+ ATTR_DEVICE_INFO: MOCK_DEVICE_INFO,
}
@@ -516,12 +398,7 @@ async def test_imported_flow_encrypted_invalid_pin_code_error(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
- data={
- CONF_HOST: "1.2.3.4",
- CONF_NAME: DEFAULT_NAME,
- CONF_PORT: DEFAULT_PORT,
- CONF_ON_ACTION: "test-on-action",
- },
+ data=MOCK_CONFIG_DATA,
)
assert result["type"] == "form"
@@ -553,12 +430,7 @@ async def test_imported_flow_encrypted_not_connected_abort(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
- data={
- CONF_HOST: "1.2.3.4",
- CONF_NAME: DEFAULT_NAME,
- CONF_PORT: DEFAULT_PORT,
- CONF_ON_ACTION: "test-on-action",
- },
+ data=MOCK_CONFIG_DATA,
)
assert result["type"] == "form"
@@ -585,12 +457,7 @@ async def test_imported_flow_encrypted_unknown_abort(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
- data={
- CONF_HOST: "1.2.3.4",
- CONF_NAME: DEFAULT_NAME,
- CONF_PORT: DEFAULT_PORT,
- CONF_ON_ACTION: "test-on-action",
- },
+ data=MOCK_CONFIG_DATA,
)
assert result["type"] == "form"
@@ -615,12 +482,7 @@ async def test_imported_flow_not_connected_error(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
- data={
- CONF_HOST: "1.2.3.4",
- CONF_NAME: DEFAULT_NAME,
- CONF_PORT: DEFAULT_PORT,
- CONF_ON_ACTION: "test-on-action",
- },
+ data=MOCK_CONFIG_DATA,
)
assert result["type"] == "form"
@@ -638,12 +500,7 @@ async def test_imported_flow_unknown_abort(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
- data={
- CONF_HOST: "1.2.3.4",
- CONF_NAME: DEFAULT_NAME,
- CONF_PORT: DEFAULT_PORT,
- CONF_ON_ACTION: "test-on-action",
- },
+ data=MOCK_CONFIG_DATA,
)
assert result["type"] == "abort"
@@ -655,19 +512,14 @@ async def test_imported_flow_non_encrypted_already_configured_abort(hass):
MockConfigEntry(
domain=DOMAIN,
- unique_id="1.2.3.4",
- data={
- CONF_HOST: "1.2.3.4",
- CONF_NAME: DEFAULT_NAME,
- CONF_PORT: DEFAULT_PORT,
- CONF_ON_ACTION: "test-on-action",
- },
+ unique_id="0.0.0.0",
+ data=MOCK_CONFIG_DATA,
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
- data={CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME},
+ data=MOCK_BASIC_DATA,
)
assert result["type"] == "abort"
@@ -679,21 +531,14 @@ async def test_imported_flow_encrypted_already_configured_abort(hass):
MockConfigEntry(
domain=DOMAIN,
- unique_id="1.2.3.4",
- data={
- CONF_HOST: "1.2.3.4",
- CONF_NAME: DEFAULT_NAME,
- CONF_PORT: DEFAULT_PORT,
- CONF_ON_ACTION: "test-on-action",
- CONF_APP_ID: "test-app-id",
- CONF_ENCRYPTION_KEY: "test-encryption-key",
- },
+ unique_id="0.0.0.0",
+ data={**MOCK_CONFIG_DATA, **MOCK_ENCRYPTION_DATA},
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
- data={CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME},
+ data=MOCK_BASIC_DATA,
)
assert result["type"] == "abort"
diff --git a/tests/components/panasonic_viera/test_init.py b/tests/components/panasonic_viera/test_init.py
index 8f95043f4fa8bb..7351b4e5544a1b 100644
--- a/tests/components/panasonic_viera/test_init.py
+++ b/tests/components/panasonic_viera/test_init.py
@@ -1,65 +1,27 @@
"""Test the Panasonic Viera setup process."""
-from unittest.mock import Mock, patch
+from unittest.mock import patch
from homeassistant.components.panasonic_viera.const import (
ATTR_DEVICE_INFO,
- ATTR_FRIENDLY_NAME,
- ATTR_MANUFACTURER,
- ATTR_MODEL_NUMBER,
ATTR_UDN,
- CONF_APP_ID,
- CONF_ENCRYPTION_KEY,
- CONF_ON_ACTION,
- DEFAULT_MANUFACTURER,
- DEFAULT_MODEL_NUMBER,
DEFAULT_NAME,
- DEFAULT_PORT,
DOMAIN,
)
from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED
-from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
+from homeassistant.const import CONF_HOST, STATE_UNAVAILABLE
from homeassistant.setup import async_setup_component
-from tests.common import MockConfigEntry
-
-MOCK_CONFIG_DATA = {
- CONF_HOST: "0.0.0.0",
- CONF_NAME: DEFAULT_NAME,
- CONF_PORT: DEFAULT_PORT,
- CONF_ON_ACTION: None,
-}
-
-MOCK_ENCRYPTION_DATA = {
- CONF_APP_ID: "mock-app-id",
- CONF_ENCRYPTION_KEY: "mock-encryption-key",
-}
-
-MOCK_DEVICE_INFO = {
- ATTR_FRIENDLY_NAME: DEFAULT_NAME,
- ATTR_MANUFACTURER: DEFAULT_MANUFACTURER,
- ATTR_MODEL_NUMBER: DEFAULT_MODEL_NUMBER,
- ATTR_UDN: "mock-unique-id",
-}
-
-
-def get_mock_remote(device_info=MOCK_DEVICE_INFO):
- """Return a mock remote."""
- mock_remote = Mock()
-
- async def async_create_remote_control(during_setup=False):
- return
-
- mock_remote.async_create_remote_control = async_create_remote_control
-
- async def async_get_device_info():
- return device_info
-
- mock_remote.async_get_device_info = async_get_device_info
+from .conftest import (
+ MOCK_CONFIG_DATA,
+ MOCK_DEVICE_INFO,
+ MOCK_ENCRYPTION_DATA,
+ get_mock_remote,
+)
- return mock_remote
+from tests.common import MockConfigEntry
-async def test_setup_entry_encrypted(hass):
+async def test_setup_entry_encrypted(hass, mock_remote):
"""Test setup with encrypted config entry."""
mock_entry = MockConfigEntry(
domain=DOMAIN,
@@ -69,22 +31,20 @@ async def test_setup_entry_encrypted(hass):
mock_entry.add_to_hass(hass)
- mock_remote = get_mock_remote()
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
- with patch(
- "homeassistant.components.panasonic_viera.Remote",
- return_value=mock_remote,
- ):
- await hass.config_entries.async_setup(mock_entry.entry_id)
- await hass.async_block_till_done()
+ state_tv = hass.states.get("media_player.panasonic_viera_tv")
+ state_remote = hass.states.get("remote.panasonic_viera_tv")
- state = hass.states.get("media_player.panasonic_viera_tv")
+ assert state_tv
+ assert state_tv.name == DEFAULT_NAME
- assert state
- assert state.name == DEFAULT_NAME
+ assert state_remote
+ assert state_remote.name == DEFAULT_NAME
-async def test_setup_entry_encrypted_missing_device_info(hass):
+async def test_setup_entry_encrypted_missing_device_info(hass, mock_remote):
"""Test setup with encrypted config entry and missing device info."""
mock_entry = MockConfigEntry(
domain=DOMAIN,
@@ -94,22 +54,20 @@ async def test_setup_entry_encrypted_missing_device_info(hass):
mock_entry.add_to_hass(hass)
- mock_remote = get_mock_remote()
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
- with patch(
- "homeassistant.components.panasonic_viera.Remote",
- return_value=mock_remote,
- ):
- await hass.config_entries.async_setup(mock_entry.entry_id)
- await hass.async_block_till_done()
+ assert mock_entry.data[ATTR_DEVICE_INFO] == MOCK_DEVICE_INFO
+ assert mock_entry.unique_id == MOCK_DEVICE_INFO[ATTR_UDN]
- assert mock_entry.data[ATTR_DEVICE_INFO] == MOCK_DEVICE_INFO
- assert mock_entry.unique_id == MOCK_DEVICE_INFO[ATTR_UDN]
+ state_tv = hass.states.get("media_player.panasonic_viera_tv")
+ state_remote = hass.states.get("remote.panasonic_viera_tv")
- state = hass.states.get("media_player.panasonic_viera_tv")
+ assert state_tv
+ assert state_tv.name == DEFAULT_NAME
- assert state
- assert state.name == DEFAULT_NAME
+ assert state_remote
+ assert state_remote.name == DEFAULT_NAME
async def test_setup_entry_encrypted_missing_device_info_none(hass):
@@ -125,7 +83,7 @@ async def test_setup_entry_encrypted_missing_device_info_none(hass):
mock_remote = get_mock_remote(device_info=None)
with patch(
- "homeassistant.components.panasonic_viera.Remote",
+ "homeassistant.components.panasonic_viera.RemoteControl",
return_value=mock_remote,
):
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -134,13 +92,17 @@ async def test_setup_entry_encrypted_missing_device_info_none(hass):
assert mock_entry.data[ATTR_DEVICE_INFO] is None
assert mock_entry.unique_id == MOCK_CONFIG_DATA[CONF_HOST]
- state = hass.states.get("media_player.panasonic_viera_tv")
+ state_tv = hass.states.get("media_player.panasonic_viera_tv")
+ state_remote = hass.states.get("remote.panasonic_viera_tv")
- assert state
- assert state.name == DEFAULT_NAME
+ assert state_tv
+ assert state_tv.name == DEFAULT_NAME
+ assert state_remote
+ assert state_remote.name == DEFAULT_NAME
-async def test_setup_entry_unencrypted(hass):
+
+async def test_setup_entry_unencrypted(hass, mock_remote):
"""Test setup with unencrypted config entry."""
mock_entry = MockConfigEntry(
domain=DOMAIN,
@@ -150,22 +112,20 @@ async def test_setup_entry_unencrypted(hass):
mock_entry.add_to_hass(hass)
- mock_remote = get_mock_remote()
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
- with patch(
- "homeassistant.components.panasonic_viera.Remote",
- return_value=mock_remote,
- ):
- await hass.config_entries.async_setup(mock_entry.entry_id)
- await hass.async_block_till_done()
+ state_tv = hass.states.get("media_player.panasonic_viera_tv")
+ state_remote = hass.states.get("remote.panasonic_viera_tv")
- state = hass.states.get("media_player.panasonic_viera_tv")
+ assert state_tv
+ assert state_tv.name == DEFAULT_NAME
- assert state
- assert state.name == DEFAULT_NAME
+ assert state_remote
+ assert state_remote.name == DEFAULT_NAME
-async def test_setup_entry_unencrypted_missing_device_info(hass):
+async def test_setup_entry_unencrypted_missing_device_info(hass, mock_remote):
"""Test setup with unencrypted config entry and missing device info."""
mock_entry = MockConfigEntry(
domain=DOMAIN,
@@ -175,22 +135,20 @@ async def test_setup_entry_unencrypted_missing_device_info(hass):
mock_entry.add_to_hass(hass)
- mock_remote = get_mock_remote()
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
- with patch(
- "homeassistant.components.panasonic_viera.Remote",
- return_value=mock_remote,
- ):
- await hass.config_entries.async_setup(mock_entry.entry_id)
- await hass.async_block_till_done()
+ assert mock_entry.data[ATTR_DEVICE_INFO] == MOCK_DEVICE_INFO
+ assert mock_entry.unique_id == MOCK_DEVICE_INFO[ATTR_UDN]
- assert mock_entry.data[ATTR_DEVICE_INFO] == MOCK_DEVICE_INFO
- assert mock_entry.unique_id == MOCK_DEVICE_INFO[ATTR_UDN]
+ state_tv = hass.states.get("media_player.panasonic_viera_tv")
+ state_remote = hass.states.get("remote.panasonic_viera_tv")
- state = hass.states.get("media_player.panasonic_viera_tv")
+ assert state_tv
+ assert state_tv.name == DEFAULT_NAME
- assert state
- assert state.name == DEFAULT_NAME
+ assert state_remote
+ assert state_remote.name == DEFAULT_NAME
async def test_setup_entry_unencrypted_missing_device_info_none(hass):
@@ -206,7 +164,7 @@ async def test_setup_entry_unencrypted_missing_device_info_none(hass):
mock_remote = get_mock_remote(device_info=None)
with patch(
- "homeassistant.components.panasonic_viera.Remote",
+ "homeassistant.components.panasonic_viera.RemoteControl",
return_value=mock_remote,
):
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -215,10 +173,14 @@ async def test_setup_entry_unencrypted_missing_device_info_none(hass):
assert mock_entry.data[ATTR_DEVICE_INFO] is None
assert mock_entry.unique_id == MOCK_CONFIG_DATA[CONF_HOST]
- state = hass.states.get("media_player.panasonic_viera_tv")
+ state_tv = hass.states.get("media_player.panasonic_viera_tv")
+ state_remote = hass.states.get("remote.panasonic_viera_tv")
- assert state
- assert state.name == DEFAULT_NAME
+ assert state_tv
+ assert state_tv.name == DEFAULT_NAME
+
+ assert state_remote
+ assert state_remote.name == DEFAULT_NAME
async def test_setup_config_flow_initiated(hass):
@@ -235,7 +197,7 @@ async def test_setup_config_flow_initiated(hass):
assert len(hass.config_entries.flow.async_progress()) == 1
-async def test_setup_unload_entry(hass):
+async def test_setup_unload_entry(hass, mock_remote):
"""Test if config entry is unloaded."""
mock_entry = MockConfigEntry(
domain=DOMAIN, unique_id=MOCK_DEVICE_INFO[ATTR_UDN], data=MOCK_CONFIG_DATA
@@ -243,19 +205,23 @@ async def test_setup_unload_entry(hass):
mock_entry.add_to_hass(hass)
- mock_remote = get_mock_remote()
-
- with patch(
- "homeassistant.components.panasonic_viera.Remote",
- return_value=mock_remote,
- ):
- await hass.config_entries.async_setup(mock_entry.entry_id)
- await hass.async_block_till_done()
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
await hass.config_entries.async_unload(mock_entry.entry_id)
-
assert mock_entry.state == ENTRY_STATE_NOT_LOADED
- state = hass.states.get("media_player.panasonic_viera_tv")
+ state_tv = hass.states.get("media_player.panasonic_viera_tv")
+ state_remote = hass.states.get("remote.panasonic_viera_tv")
+
+ assert state_tv.state == STATE_UNAVAILABLE
+ assert state_remote.state == STATE_UNAVAILABLE
+
+ await hass.config_entries.async_remove(mock_entry.entry_id)
+ await hass.async_block_till_done()
+
+ state_tv = hass.states.get("media_player.panasonic_viera_tv")
+ state_remote = hass.states.get("remote.panasonic_viera_tv")
- assert state is None
+ assert state_tv is None
+ assert state_remote is None
diff --git a/tests/components/panasonic_viera/test_remote.py b/tests/components/panasonic_viera/test_remote.py
new file mode 100644
index 00000000000000..6bfd7dee8eb3bd
--- /dev/null
+++ b/tests/components/panasonic_viera/test_remote.py
@@ -0,0 +1,58 @@
+"""Test the Panasonic Viera remote entity."""
+
+from unittest.mock import call
+
+from panasonic_viera import Keys
+
+from homeassistant.components.panasonic_viera.const import ATTR_UDN, DOMAIN
+from homeassistant.components.remote import (
+ ATTR_COMMAND,
+ DOMAIN as REMOTE_DOMAIN,
+ SERVICE_SEND_COMMAND,
+)
+from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
+
+from .conftest import MOCK_CONFIG_DATA, MOCK_DEVICE_INFO, MOCK_ENCRYPTION_DATA
+
+from tests.common import MockConfigEntry
+
+
+async def setup_panasonic_viera(hass):
+ """Initialize integration for tests."""
+ mock_entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id=MOCK_DEVICE_INFO[ATTR_UDN],
+ data={**MOCK_CONFIG_DATA, **MOCK_ENCRYPTION_DATA, **MOCK_DEVICE_INFO},
+ )
+
+ mock_entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
+
+
+async def test_onoff(hass, mock_remote):
+ """Test the on/off service calls."""
+
+ await setup_panasonic_viera(hass)
+
+ data = {ATTR_ENTITY_ID: "remote.panasonic_viera_tv"}
+
+ await hass.services.async_call(REMOTE_DOMAIN, SERVICE_TURN_OFF, data)
+ await hass.services.async_call(REMOTE_DOMAIN, SERVICE_TURN_ON, data)
+ await hass.async_block_till_done()
+
+ power = getattr(Keys.power, "value", Keys.power)
+ assert mock_remote.send_key.call_args_list == [call(power), call(power)]
+
+
+async def test_send_command(hass, mock_remote):
+ """Test the send_command service call."""
+
+ await setup_panasonic_viera(hass)
+
+ data = {ATTR_ENTITY_ID: "remote.panasonic_viera_tv", ATTR_COMMAND: "command"}
+ await hass.services.async_call(REMOTE_DOMAIN, SERVICE_SEND_COMMAND, data)
+ await hass.async_block_till_done()
+
+ assert mock_remote.send_key.call_args == call("command")
diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py
index 86ec71c1452f31..4e6793c07bba69 100644
--- a/tests/components/person/test_init.py
+++ b/tests/components/person/test_init.py
@@ -22,7 +22,7 @@
STATE_UNKNOWN,
)
from homeassistant.core import Context, CoreState, State
-from homeassistant.helpers import collection, entity_registry
+from homeassistant.helpers import collection, entity_registry as er
from homeassistant.setup import async_setup_component
from tests.common import mock_component, mock_restore_cache
@@ -589,7 +589,7 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup):
assert resp["success"]
assert len(hass.states.async_entity_ids("person")) == 0
- ent_reg = await hass.helpers.entity_registry.async_get_registry()
+ ent_reg = er.async_get(hass)
assert not ent_reg.async_is_registered("person.tracked_person")
@@ -681,7 +681,7 @@ async def test_update_person_when_user_removed(
async def test_removing_device_tracker(hass, storage_setup):
"""Test we automatically remove removed device trackers."""
storage_collection = hass.data[DOMAIN][1]
- reg = await entity_registry.async_get_registry(hass)
+ reg = er.async_get(hass)
entry = reg.async_get_or_create(
"device_tracker", "mobile_app", "bla", suggested_object_id="pixel"
)
diff --git a/tests/components/person/test_significant_change.py b/tests/components/person/test_significant_change.py
new file mode 100644
index 00000000000000..1b4f6940e90cb7
--- /dev/null
+++ b/tests/components/person/test_significant_change.py
@@ -0,0 +1,16 @@
+"""Test the Person significant change platform."""
+from homeassistant.components.person.significant_change import (
+ async_check_significant_change,
+)
+
+
+async def test_significant_change():
+ """Detect Person significant changes and ensure that attribute changes do not trigger a significant change."""
+ old_attrs = {"source": "device_tracker.wifi_device"}
+ new_attrs = {"source": "device_tracker.gps_device"}
+ assert not async_check_significant_change(
+ None, "home", old_attrs, "home", new_attrs
+ )
+ assert async_check_significant_change(
+ None, "home", new_attrs, "not_home", new_attrs
+ )
diff --git a/tests/components/philips_js/__init__.py b/tests/components/philips_js/__init__.py
new file mode 100644
index 00000000000000..9dea390a600615
--- /dev/null
+++ b/tests/components/philips_js/__init__.py
@@ -0,0 +1,77 @@
+"""Tests for the Philips TV integration."""
+
+MOCK_SERIAL_NO = "1234567890"
+MOCK_NAME = "Philips TV"
+
+MOCK_USERNAME = "mock_user"
+MOCK_PASSWORD = "mock_password"
+
+MOCK_SYSTEM = {
+ "menulanguage": "English",
+ "name": MOCK_NAME,
+ "country": "Sweden",
+ "serialnumber": MOCK_SERIAL_NO,
+ "softwareversion": "abcd",
+ "model": "modelname",
+}
+
+MOCK_SYSTEM_UNPAIRED = {
+ "menulanguage": "Dutch",
+ "name": "55PUS7181/12",
+ "country": "Netherlands",
+ "serialnumber": "ABCDEFGHIJKLF",
+ "softwareversion": "TPM191E_R.101.001.208.001",
+ "model": "65OLED855/12",
+ "deviceid": "1234567890",
+ "nettvversion": "6.0.2",
+ "epgsource": "one",
+ "api_version": {"Major": 6, "Minor": 2, "Patch": 0},
+ "featuring": {
+ "jsonfeatures": {
+ "editfavorites": ["TVChannels", "SatChannels"],
+ "recordings": ["List", "Schedule", "Manage"],
+ "ambilight": ["LoungeLight", "Hue", "Ambilight"],
+ "menuitems": ["Setup_Menu"],
+ "textentry": [
+ "context_based",
+ "initial_string_available",
+ "editor_info_available",
+ ],
+ "applications": ["TV_Apps", "TV_Games", "TV_Settings"],
+ "pointer": ["not_available"],
+ "inputkey": ["key"],
+ "activities": ["intent"],
+ "channels": ["preset_string"],
+ "mappings": ["server_mapping"],
+ },
+ "systemfeatures": {
+ "tvtype": "consumer",
+ "content": ["dmr", "dms_tad"],
+ "tvsearch": "intent",
+ "pairing_type": "digest_auth_pairing",
+ "secured_transport": "True",
+ },
+ },
+}
+
+MOCK_USERINPUT = {
+ "host": "1.1.1.1",
+}
+
+MOCK_IMPORT = {"host": "1.1.1.1", "api_version": 6}
+
+MOCK_CONFIG = {
+ "host": "1.1.1.1",
+ "api_version": 1,
+ "system": MOCK_SYSTEM,
+}
+
+MOCK_CONFIG_PAIRED = {
+ "host": "1.1.1.1",
+ "api_version": 6,
+ "username": MOCK_USERNAME,
+ "password": MOCK_PASSWORD,
+ "system": MOCK_SYSTEM_UNPAIRED,
+}
+
+MOCK_ENTITY_ID = "media_player.philips_tv"
diff --git a/tests/components/philips_js/conftest.py b/tests/components/philips_js/conftest.py
new file mode 100644
index 00000000000000..4b6150f9f8103c
--- /dev/null
+++ b/tests/components/philips_js/conftest.py
@@ -0,0 +1,71 @@
+"""Standard setup for tests."""
+from unittest.mock import create_autospec, patch
+
+from haphilipsjs import PhilipsTV
+from pytest import fixture
+
+from homeassistant import setup
+from homeassistant.components.philips_js.const import DOMAIN
+
+from . import MOCK_CONFIG, MOCK_ENTITY_ID, MOCK_NAME, MOCK_SERIAL_NO, MOCK_SYSTEM
+
+from tests.common import MockConfigEntry, mock_device_registry
+
+
+@fixture(autouse=True)
+async def setup_notification(hass):
+ """Configure notification system."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+
+@fixture(autouse=True)
+def mock_tv():
+ """Disable component actual use."""
+ tv = create_autospec(PhilipsTV)
+ tv.sources = {}
+ tv.channels = {}
+ tv.application = None
+ tv.applications = {}
+ tv.system = MOCK_SYSTEM
+ tv.api_version = 1
+ tv.api_version_detected = None
+ tv.on = True
+ tv.notify_change_supported = False
+ tv.pairing_type = None
+ tv.powerstate = None
+
+ with patch(
+ "homeassistant.components.philips_js.config_flow.PhilipsTV", return_value=tv
+ ), patch("homeassistant.components.philips_js.PhilipsTV", return_value=tv):
+ yield tv
+
+
+@fixture
+async def mock_config_entry(hass):
+ """Get standard player."""
+ config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, title=MOCK_NAME)
+ config_entry.add_to_hass(hass)
+ return config_entry
+
+
+@fixture
+def mock_device_reg(hass):
+ """Get standard device."""
+ return mock_device_registry(hass)
+
+
+@fixture
+async def mock_entity(hass, mock_device_reg, mock_config_entry):
+ """Get standard player."""
+ assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+ return MOCK_ENTITY_ID
+
+
+@fixture
+def mock_device(hass, mock_device_reg, mock_entity, mock_config_entry):
+ """Get standard device."""
+ return mock_device_reg.async_get_or_create(
+ config_entry_id=mock_config_entry.entry_id,
+ identifiers={(DOMAIN, MOCK_SERIAL_NO)},
+ )
diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py
new file mode 100644
index 00000000000000..45e896319f1552
--- /dev/null
+++ b/tests/components/philips_js/test_config_flow.py
@@ -0,0 +1,241 @@
+"""Test the Philips TV config flow."""
+from unittest.mock import ANY, patch
+
+from haphilipsjs import PairingFailure
+from pytest import fixture
+
+from homeassistant import config_entries
+from homeassistant.components.philips_js.const import DOMAIN
+
+from . import (
+ MOCK_CONFIG,
+ MOCK_CONFIG_PAIRED,
+ MOCK_IMPORT,
+ MOCK_PASSWORD,
+ MOCK_SYSTEM_UNPAIRED,
+ MOCK_USERINPUT,
+ MOCK_USERNAME,
+)
+
+
+@fixture(autouse=True)
+def mock_setup():
+ """Disable component setup."""
+ with patch(
+ "homeassistant.components.philips_js.async_setup", return_value=True
+ ) as mock_setup:
+ yield mock_setup
+
+
+@fixture(autouse=True)
+def mock_setup_entry():
+ """Disable component setup."""
+ with patch(
+ "homeassistant.components.philips_js.async_setup_entry", return_value=True
+ ) as mock_setup_entry:
+ yield mock_setup_entry
+
+
+@fixture
+async def mock_tv_pairable(mock_tv):
+ """Return a mock tv that is pariable."""
+ mock_tv.system = MOCK_SYSTEM_UNPAIRED
+ mock_tv.pairing_type = "digest_auth_pairing"
+ mock_tv.api_version = 6
+ mock_tv.api_version_detected = 6
+ mock_tv.secured_transport = True
+
+ mock_tv.pairRequest.return_value = {}
+ mock_tv.pairGrant.return_value = MOCK_USERNAME, MOCK_PASSWORD
+ return mock_tv
+
+
+async def test_import(hass, mock_setup, mock_setup_entry):
+ """Test we get an item on import."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data=MOCK_IMPORT,
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == "Philips TV (1234567890)"
+ assert result["data"] == MOCK_CONFIG
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_import_exist(hass, mock_config_entry):
+ """Test we get an item on import."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data=MOCK_IMPORT,
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+
+
+async def test_form(hass, mock_setup, mock_setup_entry):
+ """Test we get the form."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ MOCK_USERINPUT,
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Philips TV (1234567890)"
+ assert result2["data"] == MOCK_CONFIG
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_cannot_connect(hass, mock_tv):
+ """Test we handle cannot connect error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ mock_tv.system = None
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], MOCK_USERINPUT
+ )
+
+ assert result["type"] == "form"
+ assert result["errors"] == {"base": "cannot_connect"}
+
+
+async def test_form_unexpected_error(hass, mock_tv):
+ """Test we handle unexpected exceptions."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ mock_tv.getSystem.side_effect = Exception("Unexpected exception")
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], MOCK_USERINPUT
+ )
+
+ assert result["type"] == "form"
+ assert result["errors"] == {"base": "unknown"}
+
+
+async def test_pairing(hass, mock_tv_pairable, mock_setup, mock_setup_entry):
+ """Test we get the form."""
+ mock_tv = mock_tv_pairable
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ MOCK_USERINPUT,
+ )
+
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ mock_tv.setTransport.assert_called_with(True)
+ mock_tv.pairRequest.assert_called()
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"pin": "1234"}
+ )
+
+ assert result == {
+ "flow_id": ANY,
+ "type": "create_entry",
+ "description": None,
+ "description_placeholders": None,
+ "handler": "philips_js",
+ "result": ANY,
+ "title": "55PUS7181/12 (ABCDEFGHIJKLF)",
+ "data": MOCK_CONFIG_PAIRED,
+ "version": 1,
+ }
+
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_pair_request_failed(
+ hass, mock_tv_pairable, mock_setup, mock_setup_entry
+):
+ """Test we get the form."""
+ mock_tv = mock_tv_pairable
+ mock_tv.pairRequest.side_effect = PairingFailure({})
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ MOCK_USERINPUT,
+ )
+
+ assert result == {
+ "flow_id": ANY,
+ "description_placeholders": {"error_id": None},
+ "handler": "philips_js",
+ "reason": "pairing_failure",
+ "type": "abort",
+ }
+
+
+async def test_pair_grant_failed(hass, mock_tv_pairable, mock_setup, mock_setup_entry):
+ """Test we get the form."""
+ mock_tv = mock_tv_pairable
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ MOCK_USERINPUT,
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ mock_tv.setTransport.assert_called_with(True)
+ mock_tv.pairRequest.assert_called()
+
+ # Test with invalid pin
+ mock_tv.pairGrant.side_effect = PairingFailure({"error_id": "INVALID_PIN"})
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"pin": "1234"}
+ )
+
+ assert result["type"] == "form"
+ assert result["errors"] == {"pin": "invalid_pin"}
+
+ # Test with unexpected failure
+ mock_tv.pairGrant.side_effect = PairingFailure({})
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"pin": "1234"}
+ )
+
+ assert result == {
+ "flow_id": ANY,
+ "description_placeholders": {"error_id": None},
+ "handler": "philips_js",
+ "reason": "pairing_failure",
+ "type": "abort",
+ }
diff --git a/tests/components/philips_js/test_device_trigger.py b/tests/components/philips_js/test_device_trigger.py
new file mode 100644
index 00000000000000..936dd65f1152dd
--- /dev/null
+++ b/tests/components/philips_js/test_device_trigger.py
@@ -0,0 +1,77 @@
+"""The tests for Philips TV device triggers."""
+import pytest
+
+import homeassistant.components.automation as automation
+from homeassistant.components.philips_js.const import DOMAIN
+from homeassistant.setup import async_setup_component
+
+from tests.common import (
+ assert_lists_same,
+ async_get_device_automations,
+ async_mock_service,
+)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
+
+
+@pytest.fixture
+def calls(hass):
+ """Track calls to a mock service."""
+ return async_mock_service(hass, "test", "automation")
+
+
+async def test_get_triggers(hass, mock_device):
+ """Test we get the expected triggers."""
+ expected_triggers = [
+ {
+ "platform": "device",
+ "domain": DOMAIN,
+ "type": "turn_on",
+ "device_id": mock_device.id,
+ },
+ ]
+ triggers = await async_get_device_automations(hass, "trigger", mock_device.id)
+ assert_lists_same(triggers, expected_triggers)
+
+
+async def test_if_fires_on_turn_on_request(
+ hass, calls, mock_tv, mock_entity, mock_device
+):
+ """Test for turn_on and turn_off triggers firing."""
+
+ mock_tv.on = False
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": mock_device.id,
+ "type": "turn_on",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "{{ trigger.device_id }}",
+ "id": "{{ trigger.id}}",
+ },
+ },
+ }
+ ]
+ },
+ )
+
+ await hass.services.async_call(
+ "media_player",
+ "turn_on",
+ {"entity_id": mock_entity},
+ blocking=True,
+ )
+
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == mock_device.id
+ assert calls[0].data["id"] == 0
diff --git a/tests/components/pilight/test_init.py b/tests/components/pilight/test_init.py
index b69e03058bfb9d..f3512a72ee30fb 100644
--- a/tests/components/pilight/test_init.py
+++ b/tests/components/pilight/test_init.py
@@ -64,29 +64,31 @@ def set_callback(self, function):
@patch("homeassistant.components.pilight._LOGGER.error")
async def test_connection_failed_error(mock_error, hass):
"""Try to connect at 127.0.0.1:5001 with socket error."""
- with assert_setup_component(4):
- with patch("pilight.pilight.Client", side_effect=socket.error) as mock_client:
- assert not await async_setup_component(
- hass, pilight.DOMAIN, {pilight.DOMAIN: {}}
- )
- mock_client.assert_called_once_with(
- host=pilight.DEFAULT_HOST, port=pilight.DEFAULT_PORT
- )
- assert mock_error.call_count == 1
+ with assert_setup_component(4), patch(
+ "pilight.pilight.Client", side_effect=socket.error
+ ) as mock_client:
+ assert not await async_setup_component(
+ hass, pilight.DOMAIN, {pilight.DOMAIN: {}}
+ )
+ mock_client.assert_called_once_with(
+ host=pilight.DEFAULT_HOST, port=pilight.DEFAULT_PORT
+ )
+ assert mock_error.call_count == 1
@patch("homeassistant.components.pilight._LOGGER.error")
async def test_connection_timeout_error(mock_error, hass):
"""Try to connect at 127.0.0.1:5001 with socket timeout."""
- with assert_setup_component(4):
- with patch("pilight.pilight.Client", side_effect=socket.timeout) as mock_client:
- assert not await async_setup_component(
- hass, pilight.DOMAIN, {pilight.DOMAIN: {}}
- )
- mock_client.assert_called_once_with(
- host=pilight.DEFAULT_HOST, port=pilight.DEFAULT_PORT
- )
- assert mock_error.call_count == 1
+ with assert_setup_component(4), patch(
+ "pilight.pilight.Client", side_effect=socket.timeout
+ ) as mock_client:
+ assert not await async_setup_component(
+ hass, pilight.DOMAIN, {pilight.DOMAIN: {}}
+ )
+ mock_client.assert_called_once_with(
+ host=pilight.DEFAULT_HOST, port=pilight.DEFAULT_PORT
+ )
+ assert mock_error.call_count == 1
@patch("pilight.pilight.Client", PilightDaemonSim)
@@ -134,23 +136,22 @@ async def test_send_code(mock_pilight_error, hass):
@patch("homeassistant.components.pilight._LOGGER.error")
async def test_send_code_fail(mock_pilight_error, hass):
"""Check IOError exception error message."""
- with assert_setup_component(4):
- with patch("pilight.pilight.Client.send_code", side_effect=IOError):
- assert await async_setup_component(
- hass, pilight.DOMAIN, {pilight.DOMAIN: {}}
- )
+ with assert_setup_component(4), patch(
+ "pilight.pilight.Client.send_code", side_effect=IOError
+ ):
+ assert await async_setup_component(hass, pilight.DOMAIN, {pilight.DOMAIN: {}})
- # Call with protocol info, should not give error
- service_data = {"protocol": "test", "value": 42}
- await hass.services.async_call(
- pilight.DOMAIN,
- pilight.SERVICE_NAME,
- service_data=service_data,
- blocking=True,
- )
- await hass.async_block_till_done()
- error_log_call = mock_pilight_error.call_args_list[-1]
- assert "Pilight send failed" in str(error_log_call)
+ # Call with protocol info, should not give error
+ service_data = {"protocol": "test", "value": 42}
+ await hass.services.async_call(
+ pilight.DOMAIN,
+ pilight.SERVICE_NAME,
+ service_data=service_data,
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ error_log_call = mock_pilight_error.call_args_list[-1]
+ assert "Pilight send failed" in str(error_log_call)
@patch("homeassistant.components.pilight._LOGGER.error")
@@ -360,7 +361,7 @@ async def test_whitelist_no_match(mock_debug, hass):
await hass.async_block_till_done()
debug_log_call = mock_debug.call_args_list[-3]
- assert not ("Event pilight_received" in debug_log_call)
+ assert "Event pilight_received" not in debug_log_call
async def test_call_rate_delay_throttle_enabled(hass):
diff --git a/tests/components/plaato/__init__.py b/tests/components/plaato/__init__.py
new file mode 100644
index 00000000000000..dac4d341790c62
--- /dev/null
+++ b/tests/components/plaato/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Plaato integration."""
diff --git a/tests/components/plaato/test_config_flow.py b/tests/components/plaato/test_config_flow.py
new file mode 100644
index 00000000000000..7966882a97794b
--- /dev/null
+++ b/tests/components/plaato/test_config_flow.py
@@ -0,0 +1,309 @@
+"""Test the Plaato config flow."""
+from unittest.mock import patch
+
+from pyplaato.models.device import PlaatoDeviceType
+import pytest
+
+from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.components.plaato.const import (
+ CONF_DEVICE_NAME,
+ CONF_DEVICE_TYPE,
+ CONF_USE_WEBHOOK,
+ DOMAIN,
+)
+from homeassistant.const import CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_WEBHOOK_ID
+from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM
+
+from tests.common import MockConfigEntry
+
+BASE_URL = "http://example.com"
+WEBHOOK_ID = "webhook_id"
+UNIQUE_ID = "plaato_unique_id"
+
+
+@pytest.fixture(name="webhook_id")
+def mock_webhook_id():
+ """Mock webhook_id."""
+ with patch(
+ "homeassistant.components.webhook.async_generate_id", return_value=WEBHOOK_ID
+ ), patch(
+ "homeassistant.components.webhook.async_generate_url", return_value="hook_id"
+ ):
+ yield
+
+
+async def test_show_config_form(hass):
+ """Test show configuration form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+
+async def test_show_config_form_device_type_airlock(hass):
+ """Test show configuration form."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_USER},
+ data={
+ CONF_DEVICE_TYPE: PlaatoDeviceType.Airlock,
+ CONF_DEVICE_NAME: "device_name",
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "api_method"
+ assert result["data_schema"].schema.get(CONF_TOKEN) == str
+ assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) == bool
+
+
+async def test_show_config_form_device_type_keg(hass):
+ """Test show configuration form."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_USER},
+ data={CONF_DEVICE_TYPE: PlaatoDeviceType.Keg, CONF_DEVICE_NAME: "device_name"},
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "api_method"
+ assert result["data_schema"].schema.get(CONF_TOKEN) == str
+ assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) is None
+
+
+async def test_show_config_form_validate_webhook(hass, webhook_id):
+ """Test show configuration form."""
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_DEVICE_TYPE: PlaatoDeviceType.Airlock,
+ CONF_DEVICE_NAME: "device_name",
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "api_method"
+
+ hass.config.components.add("cloud")
+ with patch(
+ "homeassistant.components.cloud.async_active_subscription", return_value=True
+ ), patch(
+ "homeassistant.components.cloud.async_create_cloudhook",
+ return_value="https://hooks.nabu.casa/ABCD",
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_TOKEN: "",
+ CONF_USE_WEBHOOK: True,
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "webhook"
+
+
+async def test_show_config_form_validate_token(hass):
+ """Test show configuration form."""
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_DEVICE_TYPE: PlaatoDeviceType.Keg,
+ CONF_DEVICE_NAME: "device_name",
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "api_method"
+
+ with patch("homeassistant.components.plaato.async_setup_entry", return_value=True):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={CONF_TOKEN: "valid_token"}
+ )
+
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == PlaatoDeviceType.Keg.name
+ assert result["data"] == {
+ CONF_USE_WEBHOOK: False,
+ CONF_TOKEN: "valid_token",
+ CONF_DEVICE_TYPE: PlaatoDeviceType.Keg,
+ CONF_DEVICE_NAME: "device_name",
+ }
+
+
+async def test_show_config_form_no_cloud_webhook(hass, webhook_id):
+ """Test show configuration form."""
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_DEVICE_TYPE: PlaatoDeviceType.Airlock,
+ CONF_DEVICE_NAME: "device_name",
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "api_method"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_USE_WEBHOOK: True,
+ CONF_TOKEN: "",
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "webhook"
+ assert result["errors"] is None
+
+
+async def test_show_config_form_api_method_no_auth_token(hass, webhook_id):
+ """Test show configuration form."""
+
+ # Using Keg
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_DEVICE_TYPE: PlaatoDeviceType.Keg,
+ CONF_DEVICE_NAME: "device_name",
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "api_method"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={CONF_TOKEN: ""}
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "api_method"
+ assert len(result["errors"]) == 1
+ assert result["errors"]["base"] == "no_auth_token"
+
+ # Using Airlock
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_DEVICE_TYPE: PlaatoDeviceType.Airlock,
+ CONF_DEVICE_NAME: "device_name",
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "api_method"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={CONF_TOKEN: ""}
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "api_method"
+ assert len(result["errors"]) == 1
+ assert result["errors"]["base"] == "no_api_method"
+
+
+async def test_options(hass):
+ """Test updating options."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="NAME",
+ data={},
+ options={CONF_SCAN_INTERVAL: 5},
+ )
+ config_entry.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.plaato.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.plaato.async_setup_entry", return_value=True
+ ) as mock_setup_entry:
+
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={CONF_SCAN_INTERVAL: 10},
+ )
+
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["data"][CONF_SCAN_INTERVAL] == 10
+
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_options_webhook(hass, webhook_id):
+ """Test updating options."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="NAME",
+ data={CONF_USE_WEBHOOK: True, CONF_WEBHOOK_ID: None},
+ options={CONF_SCAN_INTERVAL: 5},
+ )
+ config_entry.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.plaato.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.plaato.async_setup_entry", return_value=True
+ ) as mock_setup_entry:
+
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "webhook"
+ assert result["description_placeholders"] == {"webhook_url": ""}
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={CONF_WEBHOOK_ID: WEBHOOK_ID},
+ )
+
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["data"][CONF_WEBHOOK_ID] == CONF_WEBHOOK_ID
+
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
diff --git a/tests/components/plant/test_init.py b/tests/components/plant/test_init.py
index f30350141c4e43..14e0f3668b0ca5 100644
--- a/tests/components/plant/test_init.py
+++ b/tests/components/plant/test_init.py
@@ -58,7 +58,7 @@ async def test_valid_data(hass):
State(GOOD_CONFIG["sensors"][reading], value),
)
assert sensor.state == "ok"
- attrib = sensor.state_attributes
+ attrib = sensor.extra_state_attributes
for reading, value in GOOD_DATA.items():
# battery level has a different name in
# the JSON format than in hass
@@ -70,13 +70,13 @@ async def test_low_battery(hass):
sensor = plant.Plant("other plant", GOOD_CONFIG)
sensor.entity_id = "sensor.mqtt_plant_battery"
sensor.hass = hass
- assert sensor.state_attributes["problem"] == "none"
+ assert sensor.extra_state_attributes["problem"] == "none"
sensor.state_changed(
"sensor.mqtt_plant_battery",
State("sensor.mqtt_plant_battery", 10),
)
assert sensor.state == "problem"
- assert sensor.state_attributes["problem"] == "battery low"
+ assert sensor.extra_state_attributes["problem"] == "battery low"
async def test_initial_states(hass):
@@ -88,7 +88,7 @@ async def test_initial_states(hass):
)
await hass.async_block_till_done()
state = hass.states.get(f"plant.{plant_name}")
- assert 5 == state.attributes[plant.READING_MOISTURE]
+ assert state.attributes[plant.READING_MOISTURE] == 5
async def test_update_states(hass):
@@ -103,8 +103,8 @@ async def test_update_states(hass):
hass.states.async_set(MOISTURE_ENTITY, 5, {ATTR_UNIT_OF_MEASUREMENT: CONDUCTIVITY})
await hass.async_block_till_done()
state = hass.states.get(f"plant.{plant_name}")
- assert STATE_PROBLEM == state.state
- assert 5 == state.attributes[plant.READING_MOISTURE]
+ assert state.state == STATE_PROBLEM
+ assert state.attributes[plant.READING_MOISTURE] == 5
async def test_unavailable_state(hass):
@@ -177,9 +177,9 @@ async def test_load_from_db(hass):
await hass.async_block_till_done()
state = hass.states.get(f"plant.{plant_name}")
- assert STATE_UNKNOWN == state.state
+ assert state.state == STATE_UNKNOWN
max_brightness = state.attributes.get(plant.ATTR_MAX_BRIGHTNESS_HISTORY)
- assert 30 == max_brightness
+ assert max_brightness == 30
async def test_brightness_history(hass):
@@ -191,17 +191,17 @@ async def test_brightness_history(hass):
hass.states.async_set(BRIGHTNESS_ENTITY, 100, {ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX})
await hass.async_block_till_done()
state = hass.states.get(f"plant.{plant_name}")
- assert STATE_PROBLEM == state.state
+ assert state.state == STATE_PROBLEM
hass.states.async_set(BRIGHTNESS_ENTITY, 600, {ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX})
await hass.async_block_till_done()
state = hass.states.get(f"plant.{plant_name}")
- assert STATE_OK == state.state
+ assert state.state == STATE_OK
hass.states.async_set(BRIGHTNESS_ENTITY, 100, {ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX})
await hass.async_block_till_done()
state = hass.states.get(f"plant.{plant_name}")
- assert STATE_OK == state.state
+ assert state.state == STATE_OK
def test_daily_history_no_data(hass):
@@ -217,7 +217,7 @@ def test_daily_history_one_day(hass):
for i in range(len(values)):
dh.add_measurement(values[i])
max_value = max(values[0 : i + 1])
- assert 1 == len(dh._days)
+ assert len(dh._days) == 1
assert dh.max == max_value
diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py
index d3e66cc4989d37..358ab293bf0967 100644
--- a/tests/components/plex/conftest.py
+++ b/tests/components/plex/conftest.py
@@ -108,12 +108,36 @@ def library_music_sort_fixture():
return load_fixture("plex/library_music_sort.xml")
+@pytest.fixture(name="library_movies_filtertypes", scope="session")
+def library_movies_filtertypes_fixture():
+ """Load filtertypes payload for movie library and return it."""
+ return load_fixture("plex/library_movies_filtertypes.xml")
+
+
@pytest.fixture(name="library", scope="session")
def library_fixture():
"""Load library payload and return it."""
return load_fixture("plex/library.xml")
+@pytest.fixture(name="library_tvshows_size", scope="session")
+def library_tvshows_size_fixture():
+ """Load tvshow library size payload and return it."""
+ return load_fixture("plex/library_tvshows_size.xml")
+
+
+@pytest.fixture(name="library_tvshows_size_episodes", scope="session")
+def library_tvshows_size_episodes_fixture():
+ """Load tvshow library size in episodes payload and return it."""
+ return load_fixture("plex/library_tvshows_size_episodes.xml")
+
+
+@pytest.fixture(name="library_tvshows_size_seasons", scope="session")
+def library_tvshows_size_seasons_fixture():
+ """Load tvshow library size in seasons payload and return it."""
+ return load_fixture("plex/library_tvshows_size_seasons.xml")
+
+
@pytest.fixture(name="library_sections", scope="session")
def library_sections_fixture():
"""Load library sections payload and return it."""
@@ -218,6 +242,12 @@ def plextv_resources_fixture(plextv_resources_base):
return plextv_resources_base.format(second_server_enabled=0)
+@pytest.fixture(name="plextv_shared_users", scope="session")
+def plextv_shared_users_fixture(plextv_resources_base):
+ """Load payload for plex.tv shared users and return it."""
+ return load_fixture("plex/plextv_shared_users.xml")
+
+
@pytest.fixture(name="session_base", scope="session")
def session_base_fixture():
"""Load the base session payload and return it."""
@@ -293,6 +323,7 @@ def mock_plex_calls(
children_200,
children_300,
empty_library,
+ empty_payload,
grandchildren_300,
library,
library_sections,
@@ -310,12 +341,15 @@ def mock_plex_calls(
playlist_500,
plextv_account,
plextv_resources,
+ plextv_shared_users,
plex_server_accounts,
plex_server_clients,
plex_server_default,
security_token,
):
"""Mock Plex API calls."""
+ requests_mock.get("https://plex.tv/api/users/", text=plextv_shared_users)
+ requests_mock.get("https://plex.tv/api/invites/requested", text=empty_payload)
requests_mock.get("https://plex.tv/users/account", text=plextv_account)
requests_mock.get("https://plex.tv/api/resources", text=plextv_resources)
diff --git a/tests/components/plex/helpers.py b/tests/components/plex/helpers.py
index 2fca88fae27e39..246922bccae075 100644
--- a/tests/components/plex/helpers.py
+++ b/tests/components/plex/helpers.py
@@ -1,7 +1,7 @@
"""Helper methods for Plex tests."""
from datetime import timedelta
-from plexwebsocket import SIGNAL_CONNECTION_STATE, SIGNAL_DATA, STATE_CONNECTED
+from plexwebsocket import SIGNAL_CONNECTION_STATE, STATE_CONNECTED
import homeassistant.util.dt as dt_util
@@ -26,10 +26,10 @@ def websocket_connected(mock_websocket):
callback(SIGNAL_CONNECTION_STATE, STATE_CONNECTED, None)
-def trigger_plex_update(mock_websocket, payload=UPDATE_PAYLOAD):
+def trigger_plex_update(mock_websocket, msgtype="playing", payload=UPDATE_PAYLOAD):
"""Call the websocket callback method with a Plex update."""
callback = mock_websocket.call_args[0][1]
- callback(SIGNAL_DATA, payload, None)
+ callback(msgtype, payload, None)
async def wait_for_debouncer(hass):
diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py
index f9966a18c27097..d9f02c493417a4 100644
--- a/tests/components/plex/test_browse_media.py
+++ b/tests/components/plex/test_browse_media.py
@@ -10,7 +10,9 @@
from .const import DEFAULT_DATA
-async def test_browse_media(hass, hass_ws_client, mock_plex_server, requests_mock):
+async def test_browse_media(
+ hass, hass_ws_client, mock_plex_server, requests_mock, library_movies_filtertypes
+):
"""Test getting Plex clients from plex.tv."""
websocket_client = await hass_ws_client(hass)
@@ -86,6 +88,11 @@ async def test_browse_media(hass, hass_ws_client, mock_plex_server, requests_moc
assert len(result["children"]) == len(mock_plex_server.library.onDeck())
# Browse into a special folder (library)
+ requests_mock.get(
+ f"{mock_plex_server.url_in_use}/library/sections/1/all?includeMeta=1",
+ text=library_movies_filtertypes,
+ )
+
msg_id += 1
library_section_id = next(iter(mock_plex_server.library.sections())).key
await websocket_client.send_json(
@@ -127,7 +134,7 @@ async def test_browse_media(hass, hass_ws_client, mock_plex_server, requests_moc
assert msg["success"]
result = msg["result"]
assert result[ATTR_MEDIA_CONTENT_TYPE] == "library"
- result_id = result[ATTR_MEDIA_CONTENT_ID]
+ result_id = int(result[ATTR_MEDIA_CONTENT_ID])
assert len(result["children"]) == len(
mock_plex_server.library.sectionByID(result_id).all()
) + len(SPECIAL_METHODS)
diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py
index bc0e59e658f4f5..e0555bab0e84d8 100644
--- a/tests/components/plex/test_config_flow.py
+++ b/tests/components/plex/test_config_flow.py
@@ -203,7 +203,7 @@ async def test_single_available_server(hass, mock_plex_calls):
server_id = result["data"][CONF_SERVER_IDENTIFIER]
mock_plex_server = hass.data[DOMAIN][SERVERS][server_id]
- assert result["title"] == mock_plex_server.friendly_name
+ assert result["title"] == mock_plex_server.url_in_use
assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name
assert (
result["data"][CONF_SERVER_IDENTIFIER]
@@ -259,7 +259,7 @@ async def test_multiple_servers_with_selection(
server_id = result["data"][CONF_SERVER_IDENTIFIER]
mock_plex_server = hass.data[DOMAIN][SERVERS][server_id]
- assert result["title"] == mock_plex_server.friendly_name
+ assert result["title"] == mock_plex_server.url_in_use
assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name
assert (
result["data"][CONF_SERVER_IDENTIFIER]
@@ -317,7 +317,7 @@ async def test_adding_last_unconfigured_server(
server_id = result["data"][CONF_SERVER_IDENTIFIER]
mock_plex_server = hass.data[DOMAIN][SERVERS][server_id]
- assert result["title"] == mock_plex_server.friendly_name
+ assert result["title"] == mock_plex_server.url_in_use
assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name
assert (
result["data"][CONF_SERVER_IDENTIFIER]
@@ -650,13 +650,14 @@ def __init__(self):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MANUAL_SERVER
)
+ await hass.async_block_till_done()
assert result["type"] == "create_entry"
server_id = result["data"][CONF_SERVER_IDENTIFIER]
mock_plex_server = hass.data[DOMAIN][SERVERS][server_id]
- assert result["title"] == mock_plex_server.friendly_name
+ assert result["title"] == mock_plex_server.url_in_use
assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name
assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier
assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use
@@ -692,7 +693,7 @@ async def test_manual_config_with_token(hass, mock_plex_calls):
server_id = result["data"][CONF_SERVER_IDENTIFIER]
mock_plex_server = hass.data[DOMAIN][SERVERS][server_id]
- assert result["title"] == mock_plex_server.friendly_name
+ assert result["title"] == mock_plex_server.url_in_use
assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name
assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier
assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use
diff --git a/tests/components/plex/test_device_handling.py b/tests/components/plex/test_device_handling.py
new file mode 100644
index 00000000000000..f36e5dc3641721
--- /dev/null
+++ b/tests/components/plex/test_device_handling.py
@@ -0,0 +1,137 @@
+"""Tests for handling the device registry."""
+
+from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN
+from homeassistant.components.plex.const import DOMAIN
+
+
+async def test_cleanup_orphaned_devices(hass, entry, setup_plex_server):
+ """Test cleaning up orphaned devices on startup."""
+ test_device_id = {(DOMAIN, "temporary_device_123")}
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+
+ test_device = device_registry.async_get_or_create(
+ config_entry_id=entry.entry_id,
+ identifiers=test_device_id,
+ )
+ assert test_device is not None
+
+ test_entity = entity_registry.async_get_or_create(
+ MP_DOMAIN, DOMAIN, "entity_unique_id_123", device_id=test_device.id
+ )
+ assert test_entity is not None
+
+ # Ensure device is not removed with an entity
+ await setup_plex_server()
+ device = device_registry.async_get_device(identifiers=test_device_id)
+ assert device is not None
+
+ await hass.config_entries.async_unload(entry.entry_id)
+
+ # Ensure device is removed without an entity
+ entity_registry.async_remove(test_entity.entity_id)
+ await setup_plex_server()
+ device = device_registry.async_get_device(identifiers=test_device_id)
+ assert device is None
+
+
+async def test_migrate_transient_devices(
+ hass, entry, setup_plex_server, requests_mock, player_plexweb_resources
+):
+ """Test cleaning up transient devices on startup."""
+ plexweb_device_id = {(DOMAIN, "plexweb_id")}
+ non_plexweb_device_id = {(DOMAIN, "1234567890123456-com-plexapp-android")}
+ plex_client_service_device_id = {(DOMAIN, "plex.tv-clients")}
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+
+ # Pre-create devices and entities to test device migration
+ plexweb_device = device_registry.async_get_or_create(
+ config_entry_id=entry.entry_id,
+ identifiers=plexweb_device_id,
+ model="Plex Web",
+ )
+ # plexweb_entity = entity_registry.async_get_or_create(MP_DOMAIN, DOMAIN, "unique_id_123:plexweb_id", suggested_object_id="plex_plex_web_chrome", device_id=plexweb_device.id)
+ entity_registry.async_get_or_create(
+ MP_DOMAIN,
+ DOMAIN,
+ "unique_id_123:plexweb_id",
+ suggested_object_id="plex_plex_web_chrome",
+ device_id=plexweb_device.id,
+ )
+
+ non_plexweb_device = device_registry.async_get_or_create(
+ config_entry_id=entry.entry_id,
+ identifiers=non_plexweb_device_id,
+ model="Plex for Android (TV)",
+ )
+ entity_registry.async_get_or_create(
+ MP_DOMAIN,
+ DOMAIN,
+ "unique_id_123:1234567890123456-com-plexapp-android",
+ suggested_object_id="plex_plex_for_android_tv_shield_android_tv",
+ device_id=non_plexweb_device.id,
+ )
+
+ # Ensure the Plex Web client is available
+ requests_mock.get("/resources", text=player_plexweb_resources)
+
+ plexweb_device = device_registry.async_get_device(identifiers=plexweb_device_id)
+ non_plexweb_device = device_registry.async_get_device(
+ identifiers=non_plexweb_device_id
+ )
+ plex_service_device = device_registry.async_get_device(
+ identifiers=plex_client_service_device_id
+ )
+
+ assert (
+ len(
+ hass.helpers.entity_registry.async_entries_for_device(
+ entity_registry, device_id=plexweb_device.id
+ )
+ )
+ == 1
+ )
+ assert (
+ len(
+ hass.helpers.entity_registry.async_entries_for_device(
+ entity_registry, device_id=non_plexweb_device.id
+ )
+ )
+ == 1
+ )
+ assert plex_service_device is None
+
+ # Ensure Plex Web entity is migrated to a service
+ await setup_plex_server()
+
+ plex_service_device = device_registry.async_get_device(
+ identifiers=plex_client_service_device_id
+ )
+
+ assert (
+ len(
+ hass.helpers.entity_registry.async_entries_for_device(
+ entity_registry, device_id=plexweb_device.id
+ )
+ )
+ == 0
+ )
+ assert (
+ len(
+ hass.helpers.entity_registry.async_entries_for_device(
+ entity_registry, device_id=non_plexweb_device.id
+ )
+ )
+ == 1
+ )
+ assert (
+ len(
+ hass.helpers.entity_registry.async_entries_for_device(
+ entity_registry, device_id=plex_service_device.id
+ )
+ )
+ == 1
+ )
diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py
index 95d2ef9bddb027..2e5a30ce11a4d1 100644
--- a/tests/components/plex/test_init.py
+++ b/tests/components/plex/test_init.py
@@ -116,6 +116,7 @@ async def test_setup_when_certificate_changed(
plex_server_default,
plextv_account,
plextv_resources,
+ plextv_shared_users,
):
"""Test setup component when the Plex certificate has changed."""
await async_setup_component(hass, "persistent_notification", {})
@@ -141,6 +142,9 @@ def __init__(self):
unique_id=DEFAULT_DATA["server_id"],
)
+ requests_mock.get("https://plex.tv/api/users/", text=plextv_shared_users)
+ requests_mock.get("https://plex.tv/api/invites/requested", text=empty_payload)
+
requests_mock.get("https://plex.tv/users/account", text=plextv_account)
requests_mock.get("https://plex.tv/api/resources", text=plextv_resources)
requests_mock.get(old_url, exc=WrongCertHostnameException)
diff --git a/tests/components/plex/test_media_players.py b/tests/components/plex/test_media_players.py
index 092d7e09008ed8..fbd1205b2ef76d 100644
--- a/tests/components/plex/test_media_players.py
+++ b/tests/components/plex/test_media_players.py
@@ -22,7 +22,7 @@ async def test_plex_tv_clients(
media_players_after = len(hass.states.async_entity_ids("media_player"))
assert media_players_after == media_players_before + 1
- await hass.config_entries.async_unload(entry.entry_id)
+ await hass.config_entries.async_remove(entry.entry_id)
# Ensure only plex.tv resource client is found
with patch("plexapi.server.PlexServer.sessions", return_value=[]):
diff --git a/tests/components/plex/test_sensor.py b/tests/components/plex/test_sensor.py
new file mode 100644
index 00000000000000..5fa50892f327a4
--- /dev/null
+++ b/tests/components/plex/test_sensor.py
@@ -0,0 +1,76 @@
+"""Tests for Plex sensors."""
+from datetime import timedelta
+
+from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
+from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.helpers import entity_registry as er
+from homeassistant.util import dt
+
+from .helpers import trigger_plex_update, wait_for_debouncer
+
+from tests.common import async_fire_time_changed
+
+LIBRARY_UPDATE_PAYLOAD = {"StatusNotification": [{"title": "Library scan complete"}]}
+
+
+async def test_library_sensor_values(
+ hass,
+ setup_plex_server,
+ mock_websocket,
+ requests_mock,
+ library_tvshows_size,
+ library_tvshows_size_episodes,
+ library_tvshows_size_seasons,
+):
+ """Test the library sensors."""
+ requests_mock.get(
+ "/library/sections/2/all?includeCollections=0&type=2",
+ text=library_tvshows_size,
+ )
+ requests_mock.get(
+ "/library/sections/2/all?includeCollections=0&type=3",
+ text=library_tvshows_size_seasons,
+ )
+ requests_mock.get(
+ "/library/sections/2/all?includeCollections=0&type=4",
+ text=library_tvshows_size_episodes,
+ )
+
+ await setup_plex_server()
+ await wait_for_debouncer(hass)
+
+ activity_sensor = hass.states.get("sensor.plex_plex_server_1")
+ assert activity_sensor.state == "1"
+
+ # Ensure sensor is created as disabled
+ assert hass.states.get("sensor.plex_server_1_library_tv_shows") is None
+
+ # Enable sensor and validate values
+ entity_registry = er.async_get(hass)
+ entity_registry.async_update_entity(
+ entity_id="sensor.plex_server_1_library_tv_shows", disabled_by=None
+ )
+ await hass.async_block_till_done()
+
+ async_fire_time_changed(
+ hass,
+ dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
+ )
+ await hass.async_block_till_done()
+
+ library_tv_sensor = hass.states.get("sensor.plex_server_1_library_tv_shows")
+ assert library_tv_sensor.state == "10"
+ assert library_tv_sensor.attributes["seasons"] == 1
+ assert library_tv_sensor.attributes["shows"] == 1
+
+ # Handle library deletion
+ requests_mock.get(
+ "/library/sections/2/all?includeCollections=0&type=2", status_code=404
+ )
+ trigger_plex_update(
+ mock_websocket, msgtype="status", payload=LIBRARY_UPDATE_PAYLOAD
+ )
+ await hass.async_block_till_done()
+
+ library_tv_sensor = hass.states.get("sensor.plex_server_1_library_tv_shows")
+ assert library_tv_sensor.state == STATE_UNAVAILABLE
diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py
index 0955c16c9ec29f..be071b4594779a 100644
--- a/tests/components/powerwall/test_config_flow.py
+++ b/tests/components/powerwall/test_config_flow.py
@@ -2,17 +2,23 @@
from unittest.mock import patch
-from tesla_powerwall import MissingAttributeError, PowerwallUnreachableError
+from tesla_powerwall import (
+ AccessDeniedError,
+ MissingAttributeError,
+ PowerwallUnreachableError,
+)
from homeassistant import config_entries, setup
from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS
from homeassistant.components.powerwall.const import DOMAIN
-from homeassistant.const import CONF_IP_ADDRESS
+from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
from .mocks import _mock_powerwall_side_effect, _mock_powerwall_site_name
from tests.common import MockConfigEntry
+VALID_CONFIG = {CONF_IP_ADDRESS: "1.2.3.4", CONF_PASSWORD: "00GGX"}
+
async def test_form_source_user(hass):
"""Test we get config flow setup form as a user."""
@@ -36,13 +42,13 @@ async def test_form_source_user(hass):
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {CONF_IP_ADDRESS: "1.2.3.4"},
+ VALID_CONFIG,
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == "My site"
- assert result2["data"] == {CONF_IP_ADDRESS: "1.2.3.4"}
+ assert result2["data"] == VALID_CONFIG
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
@@ -61,11 +67,32 @@ async def test_form_cannot_connect(hass):
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {CONF_IP_ADDRESS: "1.2.3.4"},
+ VALID_CONFIG,
)
assert result2["type"] == "form"
- assert result2["errors"] == {"base": "cannot_connect"}
+ assert result2["errors"] == {CONF_IP_ADDRESS: "cannot_connect"}
+
+
+async def test_invalid_auth(hass):
+ """Test we handle invalid auth error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ mock_powerwall = _mock_powerwall_side_effect(site_info=AccessDeniedError("any"))
+
+ with patch(
+ "homeassistant.components.powerwall.config_flow.Powerwall",
+ return_value=mock_powerwall,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ VALID_CONFIG,
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"}
async def test_form_unknown_exeption(hass):
@@ -81,8 +108,7 @@ async def test_form_unknown_exeption(hass):
return_value=mock_powerwall,
):
result2 = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {CONF_IP_ADDRESS: "1.2.3.4"},
+ result["flow_id"], VALID_CONFIG
)
assert result2["type"] == "form"
@@ -105,7 +131,7 @@ async def test_form_wrong_version(hass):
):
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {CONF_IP_ADDRESS: "1.2.3.4"},
+ VALID_CONFIG,
)
assert result3["type"] == "form"
@@ -178,16 +204,54 @@ async def test_dhcp_discovery(hass):
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {
- CONF_IP_ADDRESS: "1.1.1.1",
- },
+ VALID_CONFIG,
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == "Some site"
- assert result2["data"] == {
- CONF_IP_ADDRESS: "1.1.1.1",
- }
+ assert result2["data"] == VALID_CONFIG
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_reauth(hass):
+ """Test reauthenticate."""
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=VALID_CONFIG,
+ unique_id="1.2.3.4",
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=entry.data
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ mock_powerwall = await _mock_powerwall_site_name(hass, "My site")
+
+ with patch(
+ "homeassistant.components.powerwall.config_flow.Powerwall",
+ return_value=mock_powerwall,
+ ), patch(
+ "homeassistant.components.powerwall.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.powerwall.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_IP_ADDRESS: "1.2.3.4",
+ CONF_PASSWORD: "new-test-password",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "abort"
+ assert result2["reason"] == "reauth_successful"
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py
index eff4631c7e5a3e..32c7da9c78e6a6 100644
--- a/tests/components/powerwall/test_sensor.py
+++ b/tests/components/powerwall/test_sensor.py
@@ -1,9 +1,9 @@
"""The sensor tests for the powerwall platform."""
-
from unittest.mock import patch
from homeassistant.components.powerwall.const import DOMAIN
from homeassistant.const import CONF_IP_ADDRESS, PERCENTAGE
+from homeassistant.helpers import device_registry as dr
from .mocks import _mock_powerwall_with_fixtures
@@ -26,7 +26,7 @@ async def test_sensors(hass):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
reg_device = device_registry.async_get_device(
identifiers={("powerwall", "TG0123456789AB_TG9876543210BA")},
)
diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py
index bea42cc0888f29..fe91c7c0002e5e 100644
--- a/tests/components/prometheus/test_init.py
+++ b/tests/components/prometheus/test_init.py
@@ -224,7 +224,7 @@ async def test_minimal_config(hass, mock_client):
assert await async_setup_component(hass, prometheus.DOMAIN, config)
await hass.async_block_till_done()
assert hass.bus.listen.called
- assert EVENT_STATE_CHANGED == hass.bus.listen.call_args_list[0][0][0]
+ assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED
@pytest.mark.usefixtures("mock_bus")
@@ -251,7 +251,7 @@ async def test_full_config(hass, mock_client):
assert await async_setup_component(hass, prometheus.DOMAIN, config)
await hass.async_block_till_done()
assert hass.bus.listen.called
- assert EVENT_STATE_CHANGED == hass.bus.listen.call_args_list[0][0][0]
+ assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED
def make_event(entity_id):
diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py
index ad321181ec40a9..038b106f6c81ad 100644
--- a/tests/components/pvpc_hourly_pricing/test_config_flow.py
+++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py
@@ -7,7 +7,7 @@
from homeassistant import data_entry_flow
from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN
from homeassistant.const import CONF_NAME
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from .conftest import check_valid_state
@@ -60,7 +60,7 @@ def mock_now():
assert pvpc_aioclient_mock.call_count == 1
# Check removal
- registry = await entity_registry.async_get_registry(hass)
+ registry = er.async_get(hass)
registry_entity = registry.async_get("sensor.test")
assert await hass.config_entries.async_remove(registry_entity.config_entry_id)
diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py
index b58bd6eb46908c..142d833698d0f5 100644
--- a/tests/components/python_script/test_init.py
+++ b/tests/components/python_script/test_init.py
@@ -306,6 +306,7 @@ async def test_service_descriptions(hass):
service_descriptions1 = (
"hello:\n"
+ " name: ABC\n"
" description: Description of hello.py.\n"
" fields:\n"
" fake_param:\n"
@@ -333,6 +334,7 @@ async def test_service_descriptions(hass):
assert len(descriptions) == 1
+ assert descriptions[DOMAIN]["hello"]["name"] == "ABC"
assert descriptions[DOMAIN]["hello"]["description"] == "Description of hello.py."
assert (
descriptions[DOMAIN]["hello"]["fields"]["fake_param"]["description"]
@@ -343,6 +345,8 @@ async def test_service_descriptions(hass):
== "This is a test of python_script.hello"
)
+ # Verify default name = file name
+ assert descriptions[DOMAIN]["world_beer"]["name"] == "world_beer"
assert descriptions[DOMAIN]["world_beer"]["description"] == ""
assert bool(descriptions[DOMAIN]["world_beer"]["fields"]) is False
diff --git a/tests/components/radarr/test_sensor.py b/tests/components/radarr/test_sensor.py
index 514b8f1b81774d..aa8ccd74667c81 100644
--- a/tests/components/radarr/test_sensor.py
+++ b/tests/components/radarr/test_sensor.py
@@ -211,11 +211,11 @@ async def test_diskspace_no_paths(hass):
entity = hass.states.get("sensor.radarr_disk_space")
assert entity is not None
- assert "263.10" == entity.state
- assert "mdi:harddisk" == entity.attributes["icon"]
- assert DATA_GIGABYTES == entity.attributes["unit_of_measurement"]
- assert "Radarr Disk Space" == entity.attributes["friendly_name"]
- assert "263.10/465.42GB (56.53%)" == entity.attributes["/data"]
+ assert entity.state == "263.10"
+ assert entity.attributes["icon"] == "mdi:harddisk"
+ assert entity.attributes["unit_of_measurement"] == DATA_GIGABYTES
+ assert entity.attributes["friendly_name"] == "Radarr Disk Space"
+ assert entity.attributes["/data"] == "263.10/465.42GB (56.53%)"
async def test_diskspace_paths(hass):
@@ -240,11 +240,11 @@ async def test_diskspace_paths(hass):
entity = hass.states.get("sensor.radarr_disk_space")
assert entity is not None
- assert "263.10" == entity.state
- assert "mdi:harddisk" == entity.attributes["icon"]
- assert DATA_GIGABYTES == entity.attributes["unit_of_measurement"]
- assert "Radarr Disk Space" == entity.attributes["friendly_name"]
- assert "263.10/465.42GB (56.53%)" == entity.attributes["/data"]
+ assert entity.state == "263.10"
+ assert entity.attributes["icon"] == "mdi:harddisk"
+ assert entity.attributes["unit_of_measurement"] == DATA_GIGABYTES
+ assert entity.attributes["friendly_name"] == "Radarr Disk Space"
+ assert entity.attributes["/data"] == "263.10/465.42GB (56.53%)"
async def test_commands(hass):
@@ -269,11 +269,11 @@ async def test_commands(hass):
entity = hass.states.get("sensor.radarr_commands")
assert entity is not None
- assert 1 == int(entity.state)
- assert "mdi:code-braces" == entity.attributes["icon"]
- assert "Commands" == entity.attributes["unit_of_measurement"]
- assert "Radarr Commands" == entity.attributes["friendly_name"]
- assert "pending" == entity.attributes["RescanMovie"]
+ assert int(entity.state) == 1
+ assert entity.attributes["icon"] == "mdi:code-braces"
+ assert entity.attributes["unit_of_measurement"] == "Commands"
+ assert entity.attributes["friendly_name"] == "Radarr Commands"
+ assert entity.attributes["RescanMovie"] == "pending"
async def test_movies(hass):
@@ -298,11 +298,11 @@ async def test_movies(hass):
entity = hass.states.get("sensor.radarr_movies")
assert entity is not None
- assert 1 == int(entity.state)
- assert "mdi:television" == entity.attributes["icon"]
- assert "Movies" == entity.attributes["unit_of_measurement"]
- assert "Radarr Movies" == entity.attributes["friendly_name"]
- assert "false" == entity.attributes["Assassin's Creed (2016)"]
+ assert int(entity.state) == 1
+ assert entity.attributes["icon"] == "mdi:television"
+ assert entity.attributes["unit_of_measurement"] == "Movies"
+ assert entity.attributes["friendly_name"] == "Radarr Movies"
+ assert entity.attributes["Assassin's Creed (2016)"] == "false"
async def test_upcoming_multiple_days(hass):
@@ -327,11 +327,11 @@ async def test_upcoming_multiple_days(hass):
entity = hass.states.get("sensor.radarr_upcoming")
assert entity is not None
- assert 1 == int(entity.state)
- assert "mdi:television" == entity.attributes["icon"]
- assert "Movies" == entity.attributes["unit_of_measurement"]
- assert "Radarr Upcoming" == entity.attributes["friendly_name"]
- assert "2017-01-27T00:00:00Z" == entity.attributes["Resident Evil (2017)"]
+ assert int(entity.state) == 1
+ assert entity.attributes["icon"] == "mdi:television"
+ assert entity.attributes["unit_of_measurement"] == "Movies"
+ assert entity.attributes["friendly_name"] == "Radarr Upcoming"
+ assert entity.attributes["Resident Evil (2017)"] == "2017-01-27T00:00:00Z"
@pytest.mark.skip
@@ -357,11 +357,11 @@ async def test_upcoming_today(hass):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
entity = hass.states.get("sensor.radarr_upcoming")
- assert 1 == int(entity.state)
- assert "mdi:television" == entity.attributes["icon"]
- assert "Movies" == entity.attributes["unit_of_measurement"]
- assert "Radarr Upcoming" == entity.attributes["friendly_name"]
- assert "2017-01-27T00:00:00Z" == entity.attributes["Resident Evil (2017)"]
+ assert int(entity.state) == 1
+ assert entity.attributes["icon"] == "mdi:television"
+ assert entity.attributes["unit_of_measurement"] == "Movies"
+ assert entity.attributes["friendly_name"] == "Radarr Upcoming"
+ assert entity.attributes["Resident Evil (2017)"] == "2017-01-27T00:00:00Z"
async def test_system_status(hass):
@@ -384,10 +384,10 @@ async def test_system_status(hass):
await hass.async_block_till_done()
entity = hass.states.get("sensor.radarr_status")
assert entity is not None
- assert "0.2.0.210" == entity.state
- assert "mdi:information" == entity.attributes["icon"]
- assert "Radarr Status" == entity.attributes["friendly_name"]
- assert "4.8.13.1" == entity.attributes["osVersion"]
+ assert entity.state == "0.2.0.210"
+ assert entity.attributes["icon"] == "mdi:information"
+ assert entity.attributes["friendly_name"] == "Radarr Status"
+ assert entity.attributes["osVersion"] == "4.8.13.1"
async def test_ssl(hass):
@@ -411,11 +411,11 @@ async def test_ssl(hass):
await hass.async_block_till_done()
entity = hass.states.get("sensor.radarr_upcoming")
assert entity is not None
- assert 1 == int(entity.state)
- assert "mdi:television" == entity.attributes["icon"]
- assert "Movies" == entity.attributes["unit_of_measurement"]
- assert "Radarr Upcoming" == entity.attributes["friendly_name"]
- assert "2017-01-27T00:00:00Z" == entity.attributes["Resident Evil (2017)"]
+ assert int(entity.state) == 1
+ assert entity.attributes["icon"] == "mdi:television"
+ assert entity.attributes["unit_of_measurement"] == "Movies"
+ assert entity.attributes["friendly_name"] == "Radarr Upcoming"
+ assert entity.attributes["Resident Evil (2017)"] == "2017-01-27T00:00:00Z"
async def test_exception_handling(hass):
@@ -438,4 +438,4 @@ async def test_exception_handling(hass):
await hass.async_block_till_done()
entity = hass.states.get("sensor.radarr_upcoming")
assert entity is not None
- assert "unavailable" == entity.state
+ assert entity.state == "unavailable"
diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py
index 1d0e6dbbfa00e5..0bc4cfbfeb9430 100644
--- a/tests/components/recorder/common.py
+++ b/tests/components/recorder/common.py
@@ -1,23 +1,82 @@
"""Common test utils for working with recorder."""
-
from datetime import timedelta
+from homeassistant import core as ha
from homeassistant.components import recorder
+from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import dt as dt_util
-from tests.common import fire_time_changed
+from tests.common import async_fire_time_changed, fire_time_changed
+
+DEFAULT_PURGE_TASKS = 3
-def wait_recording_done(hass):
+def wait_recording_done(hass: HomeAssistantType) -> None:
"""Block till recording is done."""
+ hass.block_till_done()
trigger_db_commit(hass)
hass.block_till_done()
hass.data[recorder.DATA_INSTANCE].block_till_done()
hass.block_till_done()
-def trigger_db_commit(hass):
+async def async_wait_recording_done_without_instance(hass: HomeAssistantType) -> None:
+ """Block till recording is done."""
+ await hass.loop.run_in_executor(None, wait_recording_done, hass)
+
+
+def trigger_db_commit(hass: HomeAssistantType) -> None:
"""Force the recorder to commit."""
for _ in range(recorder.DEFAULT_COMMIT_INTERVAL):
# We only commit on time change
fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1))
+
+
+async def async_wait_recording_done(
+ hass: HomeAssistantType,
+ instance: recorder.Recorder,
+) -> None:
+ """Async wait until recording is done."""
+ await hass.async_block_till_done()
+ async_trigger_db_commit(hass)
+ await hass.async_block_till_done()
+ await async_recorder_block_till_done(hass, instance)
+ await hass.async_block_till_done()
+
+
+async def async_wait_purge_done(
+ hass: HomeAssistantType, instance: recorder.Recorder, max: int = None
+) -> None:
+ """Wait for max number of purge events.
+
+ Because a purge may insert another PurgeTask into
+ the queue after the WaitTask finishes, we need up to
+ a maximum number of WaitTasks that we will put into the
+ queue.
+ """
+ if not max:
+ max = DEFAULT_PURGE_TASKS
+ for _ in range(max + 1):
+ await async_wait_recording_done(hass, instance)
+
+
+@ha.callback
+def async_trigger_db_commit(hass: HomeAssistantType) -> None:
+ """Fore the recorder to commit. Async friendly."""
+ for _ in range(recorder.DEFAULT_COMMIT_INTERVAL):
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1))
+
+
+async def async_recorder_block_till_done(
+ hass: HomeAssistantType,
+ instance: recorder.Recorder,
+) -> None:
+ """Non blocking version of recorder.block_till_done()."""
+ await hass.async_add_executor_job(instance.block_till_done)
+
+
+def corrupt_db_file(test_db_file):
+ """Corrupt an sqlite3 database file."""
+ with open(test_db_file, "w+") as fhandle:
+ fhandle.seek(200)
+ fhandle.write("I am a corrupt db" * 100)
diff --git a/tests/components/recorder/conftest.py b/tests/components/recorder/conftest.py
index d91a86402ac52d..6eadb1c62ed000 100644
--- a/tests/components/recorder/conftest.py
+++ b/tests/components/recorder/conftest.py
@@ -1,10 +1,24 @@
"""Common test tools."""
+from __future__ import annotations
+
+from collections.abc import AsyncGenerator
+from typing import Awaitable, Callable, cast
import pytest
+from homeassistant.components.recorder import Recorder
from homeassistant.components.recorder.const import DATA_INSTANCE
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+
+from .common import async_recorder_block_till_done
-from tests.common import get_test_home_assistant, init_recorder_component
+from tests.common import (
+ async_init_recorder_component,
+ get_test_home_assistant,
+ init_recorder_component,
+)
+
+SetupRecorderInstanceT = Callable[..., Awaitable[Recorder]]
@pytest.fixture
@@ -22,3 +36,23 @@ def setup_recorder(config=None):
yield setup_recorder
hass.stop()
+
+
+@pytest.fixture
+async def async_setup_recorder_instance() -> AsyncGenerator[
+ SetupRecorderInstanceT, None
+]:
+ """Yield callable to setup recorder instance."""
+
+ async def async_setup_recorder(
+ hass: HomeAssistantType, config: ConfigType | None = None
+ ) -> Recorder:
+ """Setup and return recorder instance.""" # noqa: D401
+ await async_init_recorder_component(hass, config)
+ await hass.async_block_till_done()
+ instance = cast(Recorder, hass.data[DATA_INSTANCE])
+ await async_recorder_block_till_done(hass, instance)
+ assert isinstance(instance, Recorder)
+ return instance
+
+ yield async_setup_recorder
diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py
index d4092d709c0afa..b3c58995b37e26 100644
--- a/tests/components/recorder/test_init.py
+++ b/tests/components/recorder/test_init.py
@@ -6,37 +6,82 @@
from sqlalchemy.exc import OperationalError
from homeassistant.components.recorder import (
+ CONF_DB_URL,
CONFIG_SCHEMA,
+ DATA_INSTANCE,
DOMAIN,
+ SERVICE_DISABLE,
+ SERVICE_ENABLE,
+ SERVICE_PURGE,
+ SQLITE_URL_PREFIX,
Recorder,
run_information,
run_information_from_instance,
run_information_with_session,
)
-from homeassistant.components.recorder.const import DATA_INSTANCE
from homeassistant.components.recorder.models import Events, RecorderRuns, States
from homeassistant.components.recorder.util import session_scope
-from homeassistant.const import MATCH_ALL, STATE_LOCKED, STATE_UNLOCKED
-from homeassistant.core import Context, callback
-from homeassistant.setup import async_setup_component
+from homeassistant.const import (
+ EVENT_HOMEASSISTANT_STOP,
+ MATCH_ALL,
+ STATE_LOCKED,
+ STATE_UNLOCKED,
+)
+from homeassistant.core import Context, CoreState, callback
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.setup import async_setup_component, setup_component
from homeassistant.util import dt as dt_util
-from .common import wait_recording_done
+from .common import (
+ async_wait_recording_done,
+ async_wait_recording_done_without_instance,
+ corrupt_db_file,
+ wait_recording_done,
+)
+from .conftest import SetupRecorderInstanceT
+
+from tests.common import (
+ async_init_recorder_component,
+ fire_time_changed,
+ get_test_home_assistant,
+)
+
+
+async def test_shutdown_before_startup_finishes(hass):
+ """Test shutdown before recorder starts is clean."""
+
+ hass.state = CoreState.not_running
+
+ await async_init_recorder_component(hass)
+ await hass.async_block_till_done()
-from tests.common import fire_time_changed, get_test_home_assistant
+ session = await hass.async_add_executor_job(hass.data[DATA_INSTANCE].get_session)
+ with patch.object(hass.data[DATA_INSTANCE], "engine"):
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
+ await hass.async_block_till_done()
+ await hass.async_stop()
-def test_saving_state(hass, hass_recorder):
+ run_info = await hass.async_add_executor_job(run_information_with_session, session)
+
+ assert run_info.run_id == 1
+ assert run_info.start is not None
+ assert run_info.end is not None
+
+
+async def test_saving_state(
+ hass: HomeAssistantType, async_setup_recorder_instance: SetupRecorderInstanceT
+):
"""Test saving and restoring a state."""
- hass = hass_recorder()
+ instance = await async_setup_recorder_instance(hass)
entity_id = "test.recorder"
state = "restoring_from_db"
attributes = {"test_attr": 5, "test_attr_10": "nice"}
- hass.states.set(entity_id, state, attributes)
+ hass.states.async_set(entity_id, state, attributes)
- wait_recording_done(hass)
+ await async_wait_recording_done(hass, instance)
with session_scope(hass=hass) as session:
db_states = list(session.query(States))
@@ -486,5 +531,203 @@ def test_run_information(hass_recorder):
assert run_info.closed_incorrect is False
+def test_has_services(hass_recorder):
+ """Test the services exist."""
+ hass = hass_recorder()
+
+ assert hass.services.has_service(DOMAIN, SERVICE_DISABLE)
+ assert hass.services.has_service(DOMAIN, SERVICE_ENABLE)
+ assert hass.services.has_service(DOMAIN, SERVICE_PURGE)
+
+
+def test_service_disable_events_not_recording(hass, hass_recorder):
+ """Test that events are not recorded when recorder is disabled using service."""
+ hass = hass_recorder()
+
+ assert hass.services.call(
+ DOMAIN,
+ SERVICE_DISABLE,
+ {},
+ blocking=True,
+ )
+
+ event_type = "EVENT_TEST"
+
+ events = []
+
+ @callback
+ def event_listener(event):
+ """Record events from eventbus."""
+ if event.event_type == event_type:
+ events.append(event)
+
+ hass.bus.listen(MATCH_ALL, event_listener)
+
+ event_data1 = {"test_attr": 5, "test_attr_10": "nice"}
+ hass.bus.fire(event_type, event_data1)
+ wait_recording_done(hass)
+
+ assert len(events) == 1
+ event = events[0]
+
+ with session_scope(hass=hass) as session:
+ db_events = list(session.query(Events).filter_by(event_type=event_type))
+ assert len(db_events) == 0
+
+ assert hass.services.call(
+ DOMAIN,
+ SERVICE_ENABLE,
+ {},
+ blocking=True,
+ )
+
+ event_data2 = {"attr_one": 5, "attr_two": "nice"}
+ hass.bus.fire(event_type, event_data2)
+ wait_recording_done(hass)
+
+ assert len(events) == 2
+ assert events[0] != events[1]
+ assert events[0].data != events[1].data
+
+ with session_scope(hass=hass) as session:
+ db_events = list(session.query(Events).filter_by(event_type=event_type))
+ assert len(db_events) == 1
+ db_event = db_events[0].to_native()
+
+ event = events[1]
+
+ assert event.event_type == db_event.event_type
+ assert event.data == db_event.data
+ assert event.origin == db_event.origin
+ assert event.time_fired.replace(microsecond=0) == db_event.time_fired.replace(
+ microsecond=0
+ )
+
+
+def test_service_disable_states_not_recording(hass, hass_recorder):
+ """Test that state changes are not recorded when recorder is disabled using service."""
+ hass = hass_recorder()
+
+ assert hass.services.call(
+ DOMAIN,
+ SERVICE_DISABLE,
+ {},
+ blocking=True,
+ )
+
+ hass.states.set("test.one", "on", {})
+ wait_recording_done(hass)
+
+ with session_scope(hass=hass) as session:
+ assert len(list(session.query(States))) == 0
+
+ assert hass.services.call(
+ DOMAIN,
+ SERVICE_ENABLE,
+ {},
+ blocking=True,
+ )
+
+ hass.states.set("test.two", "off", {})
+ wait_recording_done(hass)
+
+ with session_scope(hass=hass) as session:
+ db_states = list(session.query(States))
+ assert len(db_states) == 1
+ assert db_states[0].event_id > 0
+ assert db_states[0].to_native() == _state_empty_context(hass, "test.two")
+
+
+def test_service_disable_run_information_recorded(tmpdir):
+ """Test that runs are still recorded when recorder is disabled."""
+ test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db")
+ dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}"
+
+ hass = get_test_home_assistant()
+ setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}})
+ hass.start()
+ wait_recording_done(hass)
+
+ with session_scope(hass=hass) as session:
+ db_run_info = list(session.query(RecorderRuns))
+ assert len(db_run_info) == 1
+ assert db_run_info[0].start is not None
+ assert db_run_info[0].end is None
+
+ assert hass.services.call(
+ DOMAIN,
+ SERVICE_DISABLE,
+ {},
+ blocking=True,
+ )
+
+ wait_recording_done(hass)
+ hass.stop()
+
+ hass = get_test_home_assistant()
+ setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}})
+ hass.start()
+ wait_recording_done(hass)
+
+ with session_scope(hass=hass) as session:
+ db_run_info = list(session.query(RecorderRuns))
+ assert len(db_run_info) == 2
+ assert db_run_info[0].start is not None
+ assert db_run_info[0].end is not None
+ assert db_run_info[1].start is not None
+ assert db_run_info[1].end is None
+
+ hass.stop()
+
+
class CannotSerializeMe:
"""A class that the JSONEncoder cannot serialize."""
+
+
+async def test_database_corruption_while_running(hass, tmpdir, caplog):
+ """Test we can recover from sqlite3 db corruption."""
+
+ def _create_tmpdir_for_test_db():
+ return tmpdir.mkdir("sqlite").join("test.db")
+
+ test_db_file = await hass.async_add_executor_job(_create_tmpdir_for_test_db)
+ dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}"
+
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}})
+ await hass.async_block_till_done()
+ caplog.clear()
+
+ hass.states.async_set("test.lost", "on", {})
+
+ await async_wait_recording_done_without_instance(hass)
+ await hass.async_add_executor_job(corrupt_db_file, test_db_file)
+ await async_wait_recording_done_without_instance(hass)
+
+ # This state will not be recorded because
+ # the database corruption will be discovered
+ # and we will have to rollback to recover
+ hass.states.async_set("test.one", "off", {})
+ await async_wait_recording_done_without_instance(hass)
+
+ assert "Unrecoverable sqlite3 database corruption detected" in caplog.text
+ assert "The system will rename the corrupt database file" in caplog.text
+ assert "Connected to recorder database" in caplog.text
+
+ # This state should go into the new database
+ hass.states.async_set("test.two", "on", {})
+ await async_wait_recording_done_without_instance(hass)
+
+ def _get_last_state():
+ with session_scope(hass=hass) as session:
+ db_states = list(session.query(States))
+ assert len(db_states) == 1
+ assert db_states[0].event_id > 0
+ return db_states[0].to_native()
+
+ state = await hass.async_add_executor_job(_get_last_state)
+ assert state.entity_id == "test.two"
+ assert state.state == "on"
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
+ await hass.async_block_till_done()
+ hass.stop()
diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py
index d10dad43d7518d..c4e0d32adcf066 100644
--- a/tests/components/recorder/test_migrate.py
+++ b/tests/components/recorder/test_migrate.py
@@ -1,13 +1,15 @@
"""The tests for the Recorder component."""
# pylint: disable=protected-access
-from unittest.mock import call, patch
+from unittest.mock import Mock, PropertyMock, call, patch
import pytest
from sqlalchemy import create_engine
+from sqlalchemy.exc import InternalError, OperationalError, ProgrammingError
from sqlalchemy.pool import StaticPool
from homeassistant.bootstrap import async_setup_component
-from homeassistant.components.recorder import const, migration, models
+from homeassistant.components.recorder import RecorderRuns, const, migration, models
+import homeassistant.util.dt as dt_util
from tests.components.recorder import models_original
@@ -50,8 +52,16 @@ async def test_schema_migrate(hass):
throwing exceptions. Maintaining a set of assertions based on schema
inspection could quickly become quite cumbersome.
"""
+
+ def _mock_setup_run(self):
+ self.run_info = RecorderRuns(
+ start=self.recording_start, created=dt_util.utcnow()
+ )
+
with patch("sqlalchemy.create_engine", new=create_engine_test), patch(
- "homeassistant.components.recorder.Recorder._setup_run"
+ "homeassistant.components.recorder.Recorder._setup_run",
+ side_effect=_mock_setup_run,
+ autospec=True,
) as setup_run:
await async_setup_component(
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
@@ -66,6 +76,26 @@ def test_invalid_update():
migration._apply_update(None, -1, 0)
+@pytest.mark.parametrize(
+ ["engine_type", "substr"],
+ [
+ ("postgresql", "ALTER event_type TYPE VARCHAR(64)"),
+ ("mssql", "ALTER COLUMN event_type VARCHAR(64)"),
+ ("mysql", "MODIFY event_type VARCHAR(64)"),
+ ("sqlite", None),
+ ],
+)
+def test_modify_column(engine_type, substr):
+ """Test that modify column generates the expected query."""
+ engine = Mock()
+ engine.dialect.name = engine_type
+ migration._modify_columns(engine, "events", ["event_type VARCHAR(64)"])
+ if substr:
+ assert substr in engine.execute.call_args[0][0].text
+ else:
+ assert not engine.execute.called
+
+
def test_forgiving_add_column():
"""Test that add column will continue if column exists."""
engine = create_engine("sqlite://", poolclass=StaticPool)
@@ -79,3 +109,30 @@ def test_forgiving_add_index():
engine = create_engine("sqlite://", poolclass=StaticPool)
models.Base.metadata.create_all(engine)
migration._create_index(engine, "states", "ix_states_context_id")
+
+
+@pytest.mark.parametrize(
+ "exception_type", [OperationalError, ProgrammingError, InternalError]
+)
+def test_forgiving_add_index_with_other_db_types(caplog, exception_type):
+ """Test that add index will continue if index exists on mysql and postgres."""
+ mocked_index = Mock()
+ type(mocked_index).name = "ix_states_context_id"
+ mocked_index.create = Mock(
+ side_effect=exception_type(
+ "CREATE INDEX ix_states_old_state_id ON states (old_state_id);",
+ [],
+ 'relation "ix_states_old_state_id" already exists',
+ )
+ )
+
+ mocked_table = Mock()
+ type(mocked_table).indexes = PropertyMock(return_value=[mocked_index])
+
+ with patch(
+ "homeassistant.components.recorder.migration.Table", return_value=mocked_table
+ ):
+ migration._create_index(Mock(), "states", "ix_states_context_id")
+
+ assert "already exists on states" in caplog.text
+ assert "continuing" in caplog.text
diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py
index 791bd84b11b603..f2fa9bf6400263 100644
--- a/tests/components/recorder/test_purge.py
+++ b/tests/components/recorder/test_purge.py
@@ -1,70 +1,87 @@
"""Test data purging."""
from datetime import datetime, timedelta
import json
-from unittest.mock import patch
+
+from sqlalchemy.orm.session import Session
from homeassistant.components import recorder
-from homeassistant.components.recorder.const import DATA_INSTANCE
from homeassistant.components.recorder.models import Events, RecorderRuns, States
from homeassistant.components.recorder.purge import purge_old_data
from homeassistant.components.recorder.util import session_scope
+from homeassistant.const import EVENT_STATE_CHANGED
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.util import dt as dt_util
-from .common import wait_recording_done
+from .common import (
+ async_recorder_block_till_done,
+ async_wait_purge_done,
+ async_wait_recording_done,
+)
+from .conftest import SetupRecorderInstanceT
-def test_purge_old_states(hass, hass_recorder):
+async def test_purge_old_states(
+ hass: HomeAssistantType, async_setup_recorder_instance: SetupRecorderInstanceT
+):
"""Test deleting old states."""
- hass = hass_recorder()
- _add_test_states(hass)
+ instance = await async_setup_recorder_instance(hass)
+
+ await _add_test_states(hass, instance)
# make sure we start with 6 states
with session_scope(hass=hass) as session:
states = session.query(States)
assert states.count() == 6
+ assert states[0].old_state_id is None
+ assert states[-1].old_state_id == states[-2].state_id
- # run purge_old_data()
- finished = purge_old_data(hass.data[DATA_INSTANCE], 4, repack=False)
- assert not finished
- assert states.count() == 4
+ events = session.query(Events).filter(Events.event_type == "state_changed")
+ assert events.count() == 6
- finished = purge_old_data(hass.data[DATA_INSTANCE], 4, repack=False)
+ # run purge_old_data()
+ finished = purge_old_data(instance, 4, repack=False)
assert not finished
assert states.count() == 2
- finished = purge_old_data(hass.data[DATA_INSTANCE], 4, repack=False)
+ states_after_purge = session.query(States)
+ assert states_after_purge[1].old_state_id == states_after_purge[0].state_id
+ assert states_after_purge[0].old_state_id is None
+
+ finished = purge_old_data(instance, 4, repack=False)
assert finished
assert states.count() == 2
-def test_purge_old_events(hass, hass_recorder):
+async def test_purge_old_events(
+ hass: HomeAssistantType, async_setup_recorder_instance: SetupRecorderInstanceT
+):
"""Test deleting old events."""
- hass = hass_recorder()
- _add_test_events(hass)
+ instance = await async_setup_recorder_instance(hass)
+
+ await _add_test_events(hass, instance)
with session_scope(hass=hass) as session:
events = session.query(Events).filter(Events.event_type.like("EVENT_TEST%"))
assert events.count() == 6
# run purge_old_data()
- finished = purge_old_data(hass.data[DATA_INSTANCE], 4, repack=False)
- assert not finished
- assert events.count() == 4
-
- finished = purge_old_data(hass.data[DATA_INSTANCE], 4, repack=False)
+ finished = purge_old_data(instance, 4, repack=False)
assert not finished
assert events.count() == 2
# we should only have 2 events left
- finished = purge_old_data(hass.data[DATA_INSTANCE], 4, repack=False)
+ finished = purge_old_data(instance, 4, repack=False)
assert finished
assert events.count() == 2
-def test_purge_old_recorder_runs(hass, hass_recorder):
+async def test_purge_old_recorder_runs(
+ hass: HomeAssistantType, async_setup_recorder_instance: SetupRecorderInstanceT
+):
"""Test deleting old recorder runs keeps current run."""
- hass = hass_recorder()
- _add_test_recorder_runs(hass)
+ instance = await async_setup_recorder_instance(hass)
+
+ await _add_test_recorder_runs(hass, instance)
# make sure we start with 7 recorder runs
with session_scope(hass=hass) as session:
@@ -72,18 +89,28 @@ def test_purge_old_recorder_runs(hass, hass_recorder):
assert recorder_runs.count() == 7
# run purge_old_data()
- finished = purge_old_data(hass.data[DATA_INSTANCE], 0, repack=False)
+ finished = purge_old_data(instance, 0, repack=False)
+ assert not finished
+
+ finished = purge_old_data(instance, 0, repack=False)
assert finished
assert recorder_runs.count() == 1
-def test_purge_method(hass, hass_recorder):
+async def test_purge_method(
+ hass: HomeAssistantType,
+ async_setup_recorder_instance: SetupRecorderInstanceT,
+ caplog,
+):
"""Test purge method."""
- hass = hass_recorder()
+ instance = await async_setup_recorder_instance(hass)
+
service_data = {"keep_days": 4}
- _add_test_events(hass)
- _add_test_states(hass)
- _add_test_recorder_runs(hass)
+ await _add_test_events(hass, instance)
+ await _add_test_states(hass, instance)
+ await _add_test_recorder_runs(hass, instance)
+ await hass.async_block_till_done()
+ await async_wait_recording_done(hass, instance)
# make sure we start with 6 states
with session_scope(hass=hass) as session:
@@ -95,29 +122,28 @@ def test_purge_method(hass, hass_recorder):
recorder_runs = session.query(RecorderRuns)
assert recorder_runs.count() == 7
+ runs_before_purge = recorder_runs.all()
- hass.data[DATA_INSTANCE].block_till_done()
- wait_recording_done(hass)
+ await hass.async_block_till_done()
+ await async_wait_purge_done(hass, instance)
# run purge method - no service data, use defaults
- hass.services.call("recorder", "purge")
- hass.block_till_done()
+ await hass.services.async_call("recorder", "purge")
+ await hass.async_block_till_done()
# Small wait for recorder thread
- hass.data[DATA_INSTANCE].block_till_done()
- wait_recording_done(hass)
+ await async_wait_purge_done(hass, instance)
# only purged old events
assert states.count() == 4
assert events.count() == 4
# run purge method - correct service data
- hass.services.call("recorder", "purge", service_data=service_data)
- hass.block_till_done()
+ await hass.services.async_call("recorder", "purge", service_data=service_data)
+ await hass.async_block_till_done()
# Small wait for recorder thread
- hass.data[DATA_INSTANCE].block_till_done()
- wait_recording_done(hass)
+ await async_wait_purge_done(hass, instance)
# we should only have 2 states left after purging
assert states.count() == 2
@@ -126,35 +152,421 @@ def test_purge_method(hass, hass_recorder):
assert events.count() == 2
# now we should only have 3 recorder runs left
- assert recorder_runs.count() == 3
+ runs = recorder_runs.all()
+ assert runs[0] == runs_before_purge[0]
+ assert runs[1] == runs_before_purge[5]
+ assert runs[2] == runs_before_purge[6]
- assert not ("EVENT_TEST_PURGE" in (event.event_type for event in events.all()))
+ assert "EVENT_TEST_PURGE" not in (event.event_type for event in events.all())
# run purge method - correct service data, with repack
- with patch("homeassistant.components.recorder.purge._LOGGER") as mock_logger:
- service_data["repack"] = True
- hass.services.call("recorder", "purge", service_data=service_data)
- hass.block_till_done()
- hass.data[DATA_INSTANCE].block_till_done()
- wait_recording_done(hass)
- assert (
- mock_logger.debug.mock_calls[5][1][0]
- == "Vacuuming SQL DB to free space"
+ service_data["repack"] = True
+ await hass.services.async_call("recorder", "purge", service_data=service_data)
+ await hass.async_block_till_done()
+ await async_wait_purge_done(hass, instance)
+ assert "Vacuuming SQL DB to free space" in caplog.text
+
+
+async def test_purge_edge_case(
+ hass: HomeAssistantType,
+ async_setup_recorder_instance: SetupRecorderInstanceT,
+):
+ """Test states and events are purged even if they occurred shortly before purge_before."""
+
+ async def _add_db_entries(hass: HomeAssistantType, timestamp: datetime) -> None:
+ with recorder.session_scope(hass=hass) as session:
+ session.add(
+ Events(
+ event_id=1001,
+ event_type="EVENT_TEST_PURGE",
+ event_data="{}",
+ origin="LOCAL",
+ created=timestamp,
+ time_fired=timestamp,
+ )
+ )
+ session.add(
+ States(
+ entity_id="test.recorder2",
+ domain="sensor",
+ state="purgeme",
+ attributes="{}",
+ last_changed=timestamp,
+ last_updated=timestamp,
+ created=timestamp,
+ event_id=1001,
+ )
)
+ instance = await async_setup_recorder_instance(hass, None)
+ await async_wait_purge_done(hass, instance)
+
+ service_data = {"keep_days": 2}
+ timestamp = dt_util.utcnow() - timedelta(days=2, minutes=1)
-def _add_test_states(hass):
+ await _add_db_entries(hass, timestamp)
+ with session_scope(hass=hass) as session:
+ states = session.query(States)
+ assert states.count() == 1
+
+ events = session.query(Events).filter(Events.event_type == "EVENT_TEST_PURGE")
+ assert events.count() == 1
+
+ await hass.services.async_call(
+ recorder.DOMAIN, recorder.SERVICE_PURGE, service_data
+ )
+ await hass.async_block_till_done()
+
+ await async_recorder_block_till_done(hass, instance)
+ await async_wait_purge_done(hass, instance)
+
+ assert states.count() == 0
+ assert events.count() == 0
+
+
+async def test_purge_filtered_states(
+ hass: HomeAssistantType,
+ async_setup_recorder_instance: SetupRecorderInstanceT,
+):
+ """Test filtered states are purged."""
+ config: ConfigType = {"exclude": {"entities": ["sensor.excluded"]}}
+ instance = await async_setup_recorder_instance(hass, config)
+ assert instance.entity_filter("sensor.excluded") is False
+
+ def _add_db_entries(hass: HomeAssistantType) -> None:
+ with recorder.session_scope(hass=hass) as session:
+ # Add states and state_changed events that should be purged
+ for days in range(1, 4):
+ timestamp = dt_util.utcnow() - timedelta(days=days)
+ for event_id in range(1000, 1020):
+ _add_state_and_state_changed_event(
+ session,
+ "sensor.excluded",
+ "purgeme",
+ timestamp,
+ event_id * days,
+ )
+ # Add state **without** state_changed event that should be purged
+ timestamp = dt_util.utcnow() - timedelta(days=1)
+ session.add(
+ States(
+ entity_id="sensor.excluded",
+ domain="sensor",
+ state="purgeme",
+ attributes="{}",
+ last_changed=timestamp,
+ last_updated=timestamp,
+ created=timestamp,
+ )
+ )
+ # Add states and state_changed events that should be keeped
+ timestamp = dt_util.utcnow() - timedelta(days=2)
+ for event_id in range(200, 210):
+ _add_state_and_state_changed_event(
+ session,
+ "sensor.keep",
+ "keep",
+ timestamp,
+ event_id,
+ )
+ # Add states with linked old_state_ids that need to be handled
+ timestamp = dt_util.utcnow() - timedelta(days=0)
+ state_1 = States(
+ entity_id="sensor.linked_old_state_id",
+ domain="sensor",
+ state="keep",
+ attributes="{}",
+ last_changed=timestamp,
+ last_updated=timestamp,
+ created=timestamp,
+ old_state_id=1,
+ )
+ timestamp = dt_util.utcnow() - timedelta(days=4)
+ state_2 = States(
+ entity_id="sensor.linked_old_state_id",
+ domain="sensor",
+ state="keep",
+ attributes="{}",
+ last_changed=timestamp,
+ last_updated=timestamp,
+ created=timestamp,
+ old_state_id=2,
+ )
+ state_3 = States(
+ entity_id="sensor.linked_old_state_id",
+ domain="sensor",
+ state="keep",
+ attributes="{}",
+ last_changed=timestamp,
+ last_updated=timestamp,
+ created=timestamp,
+ old_state_id=62, # keep
+ )
+ session.add_all((state_1, state_2, state_3))
+ # Add event that should be keeped
+ session.add(
+ Events(
+ event_id=100,
+ event_type="EVENT_KEEP",
+ event_data="{}",
+ origin="LOCAL",
+ created=timestamp,
+ time_fired=timestamp,
+ )
+ )
+
+ service_data = {"keep_days": 10}
+ _add_db_entries(hass)
+
+ with session_scope(hass=hass) as session:
+ states = session.query(States)
+ assert states.count() == 74
+
+ events_state_changed = session.query(Events).filter(
+ Events.event_type == EVENT_STATE_CHANGED
+ )
+ events_keep = session.query(Events).filter(Events.event_type == "EVENT_KEEP")
+ assert events_state_changed.count() == 70
+ assert events_keep.count() == 1
+
+ # Normal purge doesn't remove excluded entities
+ await hass.services.async_call(
+ recorder.DOMAIN, recorder.SERVICE_PURGE, service_data
+ )
+ await hass.async_block_till_done()
+
+ await async_recorder_block_till_done(hass, instance)
+ await async_wait_purge_done(hass, instance)
+
+ assert states.count() == 74
+ assert events_state_changed.count() == 70
+ assert events_keep.count() == 1
+
+ # Test with 'apply_filter' = True
+ service_data["apply_filter"] = True
+ await hass.services.async_call(
+ recorder.DOMAIN, recorder.SERVICE_PURGE, service_data
+ )
+ await hass.async_block_till_done()
+
+ await async_recorder_block_till_done(hass, instance)
+ await async_wait_purge_done(hass, instance)
+
+ await async_recorder_block_till_done(hass, instance)
+ await async_wait_purge_done(hass, instance)
+
+ assert states.count() == 13
+ assert events_state_changed.count() == 10
+ assert events_keep.count() == 1
+
+ states_sensor_excluded = session.query(States).filter(
+ States.entity_id == "sensor.excluded"
+ )
+ assert states_sensor_excluded.count() == 0
+
+ assert session.query(States).get(72).old_state_id is None
+ assert session.query(States).get(73).old_state_id is None
+ assert session.query(States).get(74).old_state_id == 62 # should have been kept
+
+
+async def test_purge_filtered_events(
+ hass: HomeAssistantType,
+ async_setup_recorder_instance: SetupRecorderInstanceT,
+):
+ """Test filtered events are purged."""
+ config: ConfigType = {"exclude": {"event_types": ["EVENT_PURGE"]}}
+ instance = await async_setup_recorder_instance(hass, config)
+
+ def _add_db_entries(hass: HomeAssistantType) -> None:
+ with recorder.session_scope(hass=hass) as session:
+ # Add events that should be purged
+ for days in range(1, 4):
+ timestamp = dt_util.utcnow() - timedelta(days=days)
+ for event_id in range(1000, 1020):
+ session.add(
+ Events(
+ event_id=event_id * days,
+ event_type="EVENT_PURGE",
+ event_data="{}",
+ origin="LOCAL",
+ created=timestamp,
+ time_fired=timestamp,
+ )
+ )
+
+ # Add states and state_changed events that should be keeped
+ timestamp = dt_util.utcnow() - timedelta(days=1)
+ for event_id in range(200, 210):
+ _add_state_and_state_changed_event(
+ session,
+ "sensor.keep",
+ "keep",
+ timestamp,
+ event_id,
+ )
+
+ service_data = {"keep_days": 10}
+ _add_db_entries(hass)
+
+ with session_scope(hass=hass) as session:
+ events_purge = session.query(Events).filter(Events.event_type == "EVENT_PURGE")
+ events_keep = session.query(Events).filter(
+ Events.event_type == EVENT_STATE_CHANGED
+ )
+ states = session.query(States)
+
+ assert events_purge.count() == 60
+ assert events_keep.count() == 10
+ assert states.count() == 10
+
+ # Normal purge doesn't remove excluded events
+ await hass.services.async_call(
+ recorder.DOMAIN, recorder.SERVICE_PURGE, service_data
+ )
+ await hass.async_block_till_done()
+
+ await async_recorder_block_till_done(hass, instance)
+ await async_wait_purge_done(hass, instance)
+
+ assert events_purge.count() == 60
+ assert events_keep.count() == 10
+ assert states.count() == 10
+
+ # Test with 'apply_filter' = True
+ service_data["apply_filter"] = True
+ await hass.services.async_call(
+ recorder.DOMAIN, recorder.SERVICE_PURGE, service_data
+ )
+ await hass.async_block_till_done()
+
+ await async_recorder_block_till_done(hass, instance)
+ await async_wait_purge_done(hass, instance)
+
+ await async_recorder_block_till_done(hass, instance)
+ await async_wait_purge_done(hass, instance)
+
+ assert events_purge.count() == 0
+ assert events_keep.count() == 10
+ assert states.count() == 10
+
+
+async def test_purge_filtered_events_state_changed(
+ hass: HomeAssistantType,
+ async_setup_recorder_instance: SetupRecorderInstanceT,
+):
+ """Test filtered state_changed events are purged. This should also remove all states."""
+ config: ConfigType = {"exclude": {"event_types": [EVENT_STATE_CHANGED]}}
+ instance = await async_setup_recorder_instance(hass, config)
+ # Assert entity_id is NOT excluded
+ assert instance.entity_filter("sensor.excluded") is True
+
+ def _add_db_entries(hass: HomeAssistantType) -> None:
+ with recorder.session_scope(hass=hass) as session:
+ # Add states and state_changed events that should be purged
+ for days in range(1, 4):
+ timestamp = dt_util.utcnow() - timedelta(days=days)
+ for event_id in range(1000, 1020):
+ _add_state_and_state_changed_event(
+ session,
+ "sensor.excluded",
+ "purgeme",
+ timestamp,
+ event_id * days,
+ )
+ # Add events that should be keeped
+ timestamp = dt_util.utcnow() - timedelta(days=1)
+ for event_id in range(200, 210):
+ session.add(
+ Events(
+ event_id=event_id,
+ event_type="EVENT_KEEP",
+ event_data="{}",
+ origin="LOCAL",
+ created=timestamp,
+ time_fired=timestamp,
+ )
+ )
+ # Add states with linked old_state_ids that need to be handled
+ timestamp = dt_util.utcnow() - timedelta(days=0)
+ state_1 = States(
+ entity_id="sensor.linked_old_state_id",
+ domain="sensor",
+ state="keep",
+ attributes="{}",
+ last_changed=timestamp,
+ last_updated=timestamp,
+ created=timestamp,
+ old_state_id=1,
+ )
+ timestamp = dt_util.utcnow() - timedelta(days=4)
+ state_2 = States(
+ entity_id="sensor.linked_old_state_id",
+ domain="sensor",
+ state="keep",
+ attributes="{}",
+ last_changed=timestamp,
+ last_updated=timestamp,
+ created=timestamp,
+ old_state_id=2,
+ )
+ state_3 = States(
+ entity_id="sensor.linked_old_state_id",
+ domain="sensor",
+ state="keep",
+ attributes="{}",
+ last_changed=timestamp,
+ last_updated=timestamp,
+ created=timestamp,
+ old_state_id=62, # keep
+ )
+ session.add_all((state_1, state_2, state_3))
+
+ service_data = {"keep_days": 10, "apply_filter": True}
+ _add_db_entries(hass)
+
+ with session_scope(hass=hass) as session:
+ events_keep = session.query(Events).filter(Events.event_type == "EVENT_KEEP")
+ events_purge = session.query(Events).filter(
+ Events.event_type == EVENT_STATE_CHANGED
+ )
+ states = session.query(States)
+
+ assert events_keep.count() == 10
+ assert events_purge.count() == 60
+ assert states.count() == 63
+
+ await hass.services.async_call(
+ recorder.DOMAIN, recorder.SERVICE_PURGE, service_data
+ )
+ await hass.async_block_till_done()
+
+ await async_recorder_block_till_done(hass, instance)
+ await async_wait_purge_done(hass, instance)
+
+ await async_recorder_block_till_done(hass, instance)
+ await async_wait_purge_done(hass, instance)
+
+ assert events_keep.count() == 10
+ assert events_purge.count() == 0
+ assert states.count() == 3
+
+ assert session.query(States).get(61).old_state_id is None
+ assert session.query(States).get(62).old_state_id is None
+ assert session.query(States).get(63).old_state_id == 62 # should have been kept
+
+
+async def _add_test_states(hass: HomeAssistantType, instance: recorder.Recorder):
"""Add multiple states to the db for testing."""
- now = datetime.now()
- five_days_ago = now - timedelta(days=5)
- eleven_days_ago = now - timedelta(days=11)
+ utcnow = dt_util.utcnow()
+ five_days_ago = utcnow - timedelta(days=5)
+ eleven_days_ago = utcnow - timedelta(days=11)
attributes = {"test_attr": 5, "test_attr_10": "nice"}
- hass.block_till_done()
- hass.data[DATA_INSTANCE].block_till_done()
- wait_recording_done(hass)
+ await hass.async_block_till_done()
+ await async_wait_recording_done(hass, instance)
with recorder.session_scope(hass=hass) as session:
+ old_state_id = None
for event_id in range(6):
if event_id < 2:
timestamp = eleven_days_ago
@@ -163,33 +575,43 @@ def _add_test_states(hass):
timestamp = five_days_ago
state = "purgeme"
else:
- timestamp = now
+ timestamp = utcnow
state = "dontpurgeme"
- session.add(
- States(
- entity_id="test.recorder2",
- domain="sensor",
- state=state,
- attributes=json.dumps(attributes),
- last_changed=timestamp,
- last_updated=timestamp,
- created=timestamp,
- event_id=event_id + 1000,
- )
+ event = Events(
+ event_type="state_changed",
+ event_data="{}",
+ origin="LOCAL",
+ created=timestamp,
+ time_fired=timestamp,
+ )
+ session.add(event)
+ session.flush()
+ state = States(
+ entity_id="test.recorder2",
+ domain="sensor",
+ state=state,
+ attributes=json.dumps(attributes),
+ last_changed=timestamp,
+ last_updated=timestamp,
+ created=timestamp,
+ event_id=event.event_id,
+ old_state_id=old_state_id,
)
+ session.add(state)
+ session.flush()
+ old_state_id = state.state_id
-def _add_test_events(hass):
+async def _add_test_events(hass: HomeAssistantType, instance: recorder.Recorder):
"""Add a few events for testing."""
- now = datetime.now()
- five_days_ago = now - timedelta(days=5)
- eleven_days_ago = now - timedelta(days=11)
+ utcnow = dt_util.utcnow()
+ five_days_ago = utcnow - timedelta(days=5)
+ eleven_days_ago = utcnow - timedelta(days=11)
event_data = {"test_attr": 5, "test_attr_10": "nice"}
- hass.block_till_done()
- hass.data[DATA_INSTANCE].block_till_done()
- wait_recording_done(hass)
+ await hass.async_block_till_done()
+ await async_wait_recording_done(hass, instance)
with recorder.session_scope(hass=hass) as session:
for event_id in range(6):
@@ -200,7 +622,7 @@ def _add_test_events(hass):
timestamp = five_days_ago
event_type = "EVENT_TEST_PURGE"
else:
- timestamp = now
+ timestamp = utcnow
event_type = "EVENT_TEST"
session.add(
@@ -214,15 +636,14 @@ def _add_test_events(hass):
)
-def _add_test_recorder_runs(hass):
+async def _add_test_recorder_runs(hass: HomeAssistantType, instance: recorder.Recorder):
"""Add a few recorder_runs for testing."""
- now = datetime.now()
- five_days_ago = now - timedelta(days=5)
- eleven_days_ago = now - timedelta(days=11)
+ utcnow = dt_util.utcnow()
+ five_days_ago = utcnow - timedelta(days=5)
+ eleven_days_ago = utcnow - timedelta(days=11)
- hass.block_till_done()
- hass.data[DATA_INSTANCE].block_till_done()
- wait_recording_done(hass)
+ await hass.async_block_till_done()
+ await async_wait_recording_done(hass, instance)
with recorder.session_scope(hass=hass) as session:
for rec_id in range(6):
@@ -231,7 +652,7 @@ def _add_test_recorder_runs(hass):
elif rec_id < 4:
timestamp = five_days_ago
else:
- timestamp = now
+ timestamp = utcnow
session.add(
RecorderRuns(
@@ -240,3 +661,35 @@ def _add_test_recorder_runs(hass):
end=timestamp + timedelta(days=1),
)
)
+
+
+def _add_state_and_state_changed_event(
+ session: Session,
+ entity_id: str,
+ state: str,
+ timestamp: datetime,
+ event_id: int,
+) -> None:
+ """Add state and state_changed event to database for testing."""
+ session.add(
+ States(
+ entity_id=entity_id,
+ domain="sensor",
+ state=state,
+ attributes="{}",
+ last_changed=timestamp,
+ last_updated=timestamp,
+ created=timestamp,
+ event_id=event_id,
+ )
+ )
+ session.add(
+ Events(
+ event_id=event_id,
+ event_type=EVENT_STATE_CHANGED,
+ event_data="{}",
+ origin="LOCAL",
+ created=timestamp,
+ time_fired=timestamp,
+ )
+ )
diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py
index a4109648d2f558..c814570416c32b 100644
--- a/tests/components/recorder/test_util.py
+++ b/tests/components/recorder/test_util.py
@@ -8,11 +8,16 @@
from homeassistant.components.recorder import util
from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX
+from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.util import dt as dt_util
-from .common import wait_recording_done
+from .common import corrupt_db_file
-from tests.common import get_test_home_assistant, init_recorder_component
+from tests.common import (
+ async_init_recorder_component,
+ get_test_home_assistant,
+ init_recorder_component,
+)
@pytest.fixture
@@ -83,14 +88,13 @@ def test_validate_or_move_away_sqlite_database_with_integrity_check(
test_db_file = f"{test_dir}/broken.db"
dburl = f"{SQLITE_URL_PREFIX}{test_db_file}"
- util.validate_sqlite_database(test_db_file, db_integrity_check) is True
-
+ assert util.validate_sqlite_database(test_db_file, db_integrity_check) is False
assert os.path.exists(test_db_file) is True
assert (
util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is False
)
- _corrupt_db_file(test_db_file)
+ corrupt_db_file(test_db_file)
assert util.validate_sqlite_database(dburl, db_integrity_check) is False
@@ -120,14 +124,13 @@ def test_validate_or_move_away_sqlite_database_without_integrity_check(
test_db_file = f"{test_dir}/broken.db"
dburl = f"{SQLITE_URL_PREFIX}{test_db_file}"
- util.validate_sqlite_database(test_db_file, db_integrity_check) is True
-
+ assert util.validate_sqlite_database(test_db_file, db_integrity_check) is False
assert os.path.exists(test_db_file) is True
assert (
util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is False
)
- _corrupt_db_file(test_db_file)
+ corrupt_db_file(test_db_file)
assert util.validate_sqlite_database(dburl, db_integrity_check) is False
@@ -142,18 +145,25 @@ def test_validate_or_move_away_sqlite_database_without_integrity_check(
assert util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is True
-def test_last_run_was_recently_clean(hass_recorder):
+async def test_last_run_was_recently_clean(hass):
"""Test we can check if the last recorder run was recently clean."""
- hass = hass_recorder()
+ await async_init_recorder_component(hass)
+ await hass.async_block_till_done()
cursor = hass.data[DATA_INSTANCE].engine.raw_connection().cursor()
- assert util.last_run_was_recently_clean(cursor) is False
+ assert (
+ await hass.async_add_executor_job(util.last_run_was_recently_clean, cursor)
+ is False
+ )
- hass.data[DATA_INSTANCE]._close_run()
- wait_recording_done(hass)
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
+ await hass.async_block_till_done()
- assert util.last_run_was_recently_clean(cursor) is True
+ assert (
+ await hass.async_add_executor_job(util.last_run_was_recently_clean, cursor)
+ is True
+ )
thirty_min_future_time = dt_util.utcnow() + timedelta(minutes=30)
@@ -161,7 +171,10 @@ def test_last_run_was_recently_clean(hass_recorder):
"homeassistant.components.recorder.dt_util.utcnow",
return_value=thirty_min_future_time,
):
- assert util.last_run_was_recently_clean(cursor) is False
+ assert (
+ await hass.async_add_executor_job(util.last_run_was_recently_clean, cursor)
+ is False
+ )
def test_basic_sanity_check(hass_recorder):
@@ -178,40 +191,69 @@ def test_basic_sanity_check(hass_recorder):
util.basic_sanity_check(cursor)
-def test_combined_checks(hass_recorder):
+def test_combined_checks(hass_recorder, caplog):
"""Run Checks on the open database."""
hass = hass_recorder()
- db_integrity_check = False
-
cursor = hass.data[DATA_INSTANCE].engine.raw_connection().cursor()
- assert (
- util.run_checks_on_open_db("fake_db_path", cursor, db_integrity_check) is None
- )
+ assert util.run_checks_on_open_db("fake_db_path", cursor, False) is None
+ assert "skipped because db_integrity_check was disabled" in caplog.text
+
+ caplog.clear()
+ assert util.run_checks_on_open_db("fake_db_path", cursor, True) is None
+ assert "could not validate that the sqlite3 database" in caplog.text
+
+ # We are patching recorder.util here in order
+ # to avoid creating the full database on disk
+ with patch(
+ "homeassistant.components.recorder.util.basic_sanity_check", return_value=False
+ ):
+ caplog.clear()
+ assert util.run_checks_on_open_db("fake_db_path", cursor, False) is None
+ assert "skipped because db_integrity_check was disabled" in caplog.text
+
+ caplog.clear()
+ assert util.run_checks_on_open_db("fake_db_path", cursor, True) is None
+ assert "could not validate that the sqlite3 database" in caplog.text
# We are patching recorder.util here in order
# to avoid creating the full database on disk
with patch("homeassistant.components.recorder.util.last_run_was_recently_clean"):
+ caplog.clear()
+ assert util.run_checks_on_open_db("fake_db_path", cursor, False) is None
+ assert (
+ "system was restarted cleanly and passed the basic sanity check"
+ in caplog.text
+ )
+
+ caplog.clear()
+ assert util.run_checks_on_open_db("fake_db_path", cursor, True) is None
assert (
- util.run_checks_on_open_db("fake_db_path", cursor, db_integrity_check)
- is None
+ "system was restarted cleanly and passed the basic sanity check"
+ in caplog.text
)
+ caplog.clear()
with patch(
"homeassistant.components.recorder.util.last_run_was_recently_clean",
side_effect=sqlite3.DatabaseError,
), pytest.raises(sqlite3.DatabaseError):
- util.run_checks_on_open_db("fake_db_path", cursor, db_integrity_check)
+ util.run_checks_on_open_db("fake_db_path", cursor, False)
+
+ caplog.clear()
+ with patch(
+ "homeassistant.components.recorder.util.last_run_was_recently_clean",
+ side_effect=sqlite3.DatabaseError,
+ ), pytest.raises(sqlite3.DatabaseError):
+ util.run_checks_on_open_db("fake_db_path", cursor, True)
cursor.execute("DROP TABLE events;")
+ caplog.clear()
with pytest.raises(sqlite3.DatabaseError):
- util.run_checks_on_open_db("fake_db_path", cursor, db_integrity_check)
-
+ util.run_checks_on_open_db("fake_db_path", cursor, False)
-def _corrupt_db_file(test_db_file):
- """Corrupt an sqlite3 database file."""
- f = open(test_db_file, "a")
- f.write("I am a corrupt db")
- f.close()
+ caplog.clear()
+ with pytest.raises(sqlite3.DatabaseError):
+ util.run_checks_on_open_db("fake_db_path", cursor, True)
diff --git a/tests/components/remote/test_device_action.py b/tests/components/remote/test_device_action.py
index 17165639e25270..7cd5a63298241f 100644
--- a/tests/components/remote/test_device_action.py
+++ b/tests/components/remote/test_device_action.py
@@ -16,7 +16,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py
index c6a2b3f0c52c03..12cf0e05493ccb 100644
--- a/tests/components/remote/test_device_condition.py
+++ b/tests/components/remote/test_device_condition.py
@@ -19,7 +19,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py
index eccf96c04f63b6..616c356936c010 100644
--- a/tests/components/remote/test_device_trigger.py
+++ b/tests/components/remote/test_device_trigger.py
@@ -19,7 +19,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py
index 1f57b97884c7c4..2c2005a7fee4c8 100644
--- a/tests/components/remote/test_init.py
+++ b/tests/components/remote/test_init.py
@@ -48,7 +48,7 @@ async def test_turn_on(hass):
assert len(turn_on_calls) == 1
call = turn_on_calls[-1]
- assert DOMAIN == call.domain
+ assert call.domain == DOMAIN
async def test_turn_off(hass):
diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py
index 9adb04ea40c2e6..f6445c250224cd 100644
--- a/tests/components/rest/test_binary_sensor.py
+++ b/tests/components/rest/test_binary_sensor.py
@@ -2,7 +2,7 @@
import asyncio
from os import path
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
import httpx
import respx
@@ -47,9 +47,12 @@ async def test_setup_missing_config(hass):
@respx.mock
-async def test_setup_failed_connect(hass):
+async def test_setup_failed_connect(hass, caplog):
"""Test setup when connection error occurs."""
- respx.get("http://localhost").mock(side_effect=httpx.RequestError)
+
+ respx.get("http://localhost").mock(
+ side_effect=httpx.RequestError("server offline", request=MagicMock())
+ )
assert await async_setup_component(
hass,
binary_sensor.DOMAIN,
@@ -63,6 +66,7 @@ async def test_setup_failed_connect(hass):
)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
+ assert "server offline" in caplog.text
@respx.mock
diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py
new file mode 100644
index 00000000000000..2902addca0c800
--- /dev/null
+++ b/tests/components/rest/test_init.py
@@ -0,0 +1,415 @@
+"""Tests for rest component."""
+
+import asyncio
+from datetime import timedelta
+from os import path
+from unittest.mock import patch
+
+import respx
+
+from homeassistant import config as hass_config
+from homeassistant.components.rest.const import DOMAIN
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ DATA_MEGABYTES,
+ SERVICE_RELOAD,
+ STATE_UNAVAILABLE,
+)
+from homeassistant.setup import async_setup_component
+from homeassistant.util.dt import utcnow
+
+from tests.common import async_fire_time_changed
+
+
+@respx.mock
+async def test_setup_with_endpoint_timeout_with_recovery(hass):
+ """Test setup with an endpoint that times out that recovers."""
+ await async_setup_component(hass, "homeassistant", {})
+
+ respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError())
+ assert await async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ DOMAIN: [
+ {
+ "resource": "http://localhost",
+ "method": "GET",
+ "verify_ssl": "false",
+ "timeout": 30,
+ "sensor": [
+ {
+ "unit_of_measurement": DATA_MEGABYTES,
+ "name": "sensor1",
+ "value_template": "{{ value_json.sensor1 }}",
+ },
+ {
+ "unit_of_measurement": DATA_MEGABYTES,
+ "name": "sensor2",
+ "value_template": "{{ value_json.sensor2 }}",
+ },
+ ],
+ "binary_sensor": [
+ {
+ "name": "binary_sensor1",
+ "value_template": "{{ value_json.binary_sensor1 }}",
+ },
+ {
+ "name": "binary_sensor2",
+ "value_template": "{{ value_json.binary_sensor2 }}",
+ },
+ ],
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 0
+
+ respx.get("http://localhost").respond(
+ status_code=200,
+ json={
+ "sensor1": "1",
+ "sensor2": "2",
+ "binary_sensor1": "on",
+ "binary_sensor2": "off",
+ },
+ )
+
+ # Refresh the coordinator
+ async_fire_time_changed(hass, utcnow() + timedelta(seconds=31))
+ await hass.async_block_till_done()
+
+ # Wait for platform setup retry
+ async_fire_time_changed(hass, utcnow() + timedelta(seconds=61))
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 4
+
+ assert hass.states.get("sensor.sensor1").state == "1"
+ assert hass.states.get("sensor.sensor2").state == "2"
+ assert hass.states.get("binary_sensor.binary_sensor1").state == "on"
+ assert hass.states.get("binary_sensor.binary_sensor2").state == "off"
+
+ # Now the end point flakes out again
+ respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError())
+
+ # Refresh the coordinator
+ async_fire_time_changed(hass, utcnow() + timedelta(seconds=31))
+ await hass.async_block_till_done()
+
+ assert hass.states.get("sensor.sensor1").state == STATE_UNAVAILABLE
+ assert hass.states.get("sensor.sensor2").state == STATE_UNAVAILABLE
+ assert hass.states.get("binary_sensor.binary_sensor1").state == STATE_UNAVAILABLE
+ assert hass.states.get("binary_sensor.binary_sensor2").state == STATE_UNAVAILABLE
+
+ # We request a manual refresh when the
+ # endpoint is working again
+
+ respx.get("http://localhost").respond(
+ status_code=200,
+ json={
+ "sensor1": "1",
+ "sensor2": "2",
+ "binary_sensor1": "on",
+ "binary_sensor2": "off",
+ },
+ )
+
+ await hass.services.async_call(
+ "homeassistant",
+ "update_entity",
+ {ATTR_ENTITY_ID: ["sensor.sensor1"]},
+ blocking=True,
+ )
+ assert hass.states.get("sensor.sensor1").state == "1"
+ assert hass.states.get("sensor.sensor2").state == "2"
+ assert hass.states.get("binary_sensor.binary_sensor1").state == "on"
+ assert hass.states.get("binary_sensor.binary_sensor2").state == "off"
+
+
+@respx.mock
+async def test_setup_minimum_resource_template(hass):
+ """Test setup with minimum configuration (resource_template)."""
+
+ respx.get("http://localhost").respond(
+ status_code=200,
+ json={
+ "sensor1": "1",
+ "sensor2": "2",
+ "binary_sensor1": "on",
+ "binary_sensor2": "off",
+ },
+ )
+ assert await async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ DOMAIN: [
+ {
+ "resource_template": "{% set url = 'http://localhost' %}{{ url }}",
+ "method": "GET",
+ "verify_ssl": "false",
+ "timeout": 30,
+ "sensor": [
+ {
+ "unit_of_measurement": DATA_MEGABYTES,
+ "name": "sensor1",
+ "value_template": "{{ value_json.sensor1 }}",
+ },
+ {
+ "unit_of_measurement": DATA_MEGABYTES,
+ "name": "sensor2",
+ "value_template": "{{ value_json.sensor2 }}",
+ },
+ ],
+ "binary_sensor": [
+ {
+ "name": "binary_sensor1",
+ "value_template": "{{ value_json.binary_sensor1 }}",
+ },
+ {
+ "name": "binary_sensor2",
+ "value_template": "{{ value_json.binary_sensor2 }}",
+ },
+ ],
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 4
+
+ assert hass.states.get("sensor.sensor1").state == "1"
+ assert hass.states.get("sensor.sensor2").state == "2"
+ assert hass.states.get("binary_sensor.binary_sensor1").state == "on"
+ assert hass.states.get("binary_sensor.binary_sensor2").state == "off"
+
+
+@respx.mock
+async def test_reload(hass):
+ """Verify we can reload."""
+
+ respx.get("http://localhost") % 200
+
+ assert await async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ DOMAIN: [
+ {
+ "resource": "http://localhost",
+ "method": "GET",
+ "verify_ssl": "false",
+ "timeout": 30,
+ "sensor": [
+ {
+ "name": "mockrest",
+ },
+ ],
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 1
+
+ assert hass.states.get("sensor.mockrest")
+
+ yaml_path = path.join(
+ _get_fixtures_base_path(),
+ "fixtures",
+ "rest/configuration_top_level.yaml",
+ )
+ with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path):
+ await hass.services.async_call(
+ "rest",
+ SERVICE_RELOAD,
+ {},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("sensor.mockreset") is None
+ assert hass.states.get("sensor.rollout")
+ assert hass.states.get("sensor.fallover")
+
+
+@respx.mock
+async def test_reload_and_remove_all(hass):
+ """Verify we can reload and remove all."""
+
+ respx.get("http://localhost") % 200
+
+ assert await async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ DOMAIN: [
+ {
+ "resource": "http://localhost",
+ "method": "GET",
+ "verify_ssl": "false",
+ "timeout": 30,
+ "sensor": [
+ {
+ "name": "mockrest",
+ },
+ ],
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 1
+
+ assert hass.states.get("sensor.mockrest")
+
+ yaml_path = path.join(
+ _get_fixtures_base_path(),
+ "fixtures",
+ "rest/configuration_empty.yaml",
+ )
+ with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path):
+ await hass.services.async_call(
+ "rest",
+ SERVICE_RELOAD,
+ {},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get("sensor.mockreset") is None
+
+
+@respx.mock
+async def test_reload_fails_to_read_configuration(hass):
+ """Verify reload when configuration is missing or broken."""
+
+ respx.get("http://localhost") % 200
+
+ assert await async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ DOMAIN: [
+ {
+ "resource": "http://localhost",
+ "method": "GET",
+ "verify_ssl": "false",
+ "timeout": 30,
+ "sensor": [
+ {
+ "name": "mockrest",
+ },
+ ],
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 1
+
+ yaml_path = path.join(
+ _get_fixtures_base_path(),
+ "fixtures",
+ "rest/configuration_invalid.notyaml",
+ )
+ with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path):
+ await hass.services.async_call(
+ "rest",
+ SERVICE_RELOAD,
+ {},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 1
+
+
+def _get_fixtures_base_path():
+ return path.dirname(path.dirname(path.dirname(__file__)))
+
+
+@respx.mock
+async def test_multiple_rest_endpoints(hass):
+ """Test multiple rest endpoints."""
+
+ respx.get("http://date.jsontest.com").respond(
+ status_code=200,
+ json={
+ "date": "03-17-2021",
+ "milliseconds_since_epoch": 1616008268573,
+ "time": "07:11:08 PM",
+ },
+ )
+
+ respx.get("http://time.jsontest.com").respond(
+ status_code=200,
+ json={
+ "date": "03-17-2021",
+ "milliseconds_since_epoch": 1616008299665,
+ "time": "07:11:39 PM",
+ },
+ )
+ respx.get("http://localhost").respond(
+ status_code=200,
+ json={
+ "value": "1",
+ },
+ )
+ assert await async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ DOMAIN: [
+ {
+ "resource": "http://date.jsontest.com",
+ "sensor": [
+ {
+ "name": "JSON Date",
+ "value_template": "{{ value_json.date }}",
+ },
+ {
+ "name": "JSON Date Time",
+ "value_template": "{{ value_json.time }}",
+ },
+ ],
+ },
+ {
+ "resource": "http://time.jsontest.com",
+ "sensor": [
+ {
+ "name": "JSON Time",
+ "value_template": "{{ value_json.time }}",
+ },
+ ],
+ },
+ {
+ "resource": "http://localhost",
+ "binary_sensor": [
+ {
+ "name": "Binary Sensor",
+ "value_template": "{{ value_json.value }}",
+ },
+ ],
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 4
+
+ assert hass.states.get("sensor.json_date").state == "03-17-2021"
+ assert hass.states.get("sensor.json_date_time").state == "07:11:08 PM"
+ assert hass.states.get("sensor.json_time").state == "07:11:39 PM"
+ assert hass.states.get("binary_sensor.binary_sensor").state == "on"
diff --git a/tests/components/rest/test_notify.py b/tests/components/rest/test_notify.py
index aa3e40c2dd4388..fb7b8a31238608 100644
--- a/tests/components/rest/test_notify.py
+++ b/tests/components/rest/test_notify.py
@@ -2,6 +2,8 @@
from os import path
from unittest.mock import patch
+import respx
+
from homeassistant import config as hass_config
import homeassistant.components.notify as notify
from homeassistant.components.rest import DOMAIN
@@ -9,8 +11,10 @@
from homeassistant.setup import async_setup_component
+@respx.mock
async def test_reload_notify(hass):
"""Verify we can reload the notify service."""
+ respx.get("http://localhost") % 200
assert await async_setup_component(
hass,
diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py
index 58309cd7532e7c..50b959be36b4b8 100644
--- a/tests/components/rest/test_sensor.py
+++ b/tests/components/rest/test_sensor.py
@@ -1,7 +1,7 @@
"""The tests for the REST sensor platform."""
import asyncio
from os import path
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
import httpx
import respx
@@ -41,9 +41,11 @@ async def test_setup_missing_schema(hass):
@respx.mock
-async def test_setup_failed_connect(hass):
+async def test_setup_failed_connect(hass, caplog):
"""Test setup when connection error occurs."""
- respx.get("http://localhost").mock(side_effect=httpx.RequestError)
+ respx.get("http://localhost").mock(
+ side_effect=httpx.RequestError("server offline", request=MagicMock())
+ )
assert await async_setup_component(
hass,
sensor.DOMAIN,
@@ -57,6 +59,7 @@ async def test_setup_failed_connect(hass):
)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
+ assert "server offline" in caplog.text
@respx.mock
@@ -91,6 +94,38 @@ async def test_setup_minimum(hass):
assert len(hass.states.async_all()) == 1
+@respx.mock
+async def test_manual_update(hass):
+ """Test setup with minimum configuration."""
+ await async_setup_component(hass, "homeassistant", {})
+ respx.get("http://localhost").respond(status_code=200, json={"data": "first"})
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "name": "mysensor",
+ "value_template": "{{ value_json.data }}",
+ "platform": "rest",
+ "resource_template": "{% set url = 'http://localhost' %}{{ url }}",
+ "method": "GET",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+ assert hass.states.get("sensor.mysensor").state == "first"
+
+ respx.get("http://localhost").respond(status_code=200, json={"data": "second"})
+ await hass.services.async_call(
+ "homeassistant",
+ "update_entity",
+ {ATTR_ENTITY_ID: ["sensor.mysensor"]},
+ blocking=True,
+ )
+ assert hass.states.get("sensor.mysensor").state == "second"
+
+
@respx.mock
async def test_setup_minimum_resource_template(hass):
"""Test setup with minimum configuration (resource_template)."""
diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py
index 5e0c9fbeab32dd..62fc30d9e4b06b 100644
--- a/tests/components/rest/test_switch.py
+++ b/tests/components/rest/test_switch.py
@@ -3,6 +3,7 @@
import aiohttp
+from homeassistant.components.rest import DOMAIN
import homeassistant.components.rest.switch as rest
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import (
@@ -34,14 +35,14 @@
async def test_setup_missing_config(hass):
"""Test setup with configuration missing required entries."""
- assert not await rest.async_setup_platform(hass, {CONF_PLATFORM: rest.DOMAIN}, None)
+ assert not await rest.async_setup_platform(hass, {CONF_PLATFORM: DOMAIN}, None)
async def test_setup_missing_schema(hass):
"""Test setup with resource missing schema."""
assert not await rest.async_setup_platform(
hass,
- {CONF_PLATFORM: rest.DOMAIN, CONF_RESOURCE: "localhost"},
+ {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "localhost"},
None,
)
@@ -51,7 +52,7 @@ async def test_setup_failed_connect(hass, aioclient_mock):
aioclient_mock.get("http://localhost", exc=aiohttp.ClientError)
assert not await rest.async_setup_platform(
hass,
- {CONF_PLATFORM: rest.DOMAIN, CONF_RESOURCE: "http://localhost"},
+ {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "http://localhost"},
None,
)
@@ -61,7 +62,7 @@ async def test_setup_timeout(hass, aioclient_mock):
aioclient_mock.get("http://localhost", exc=asyncio.TimeoutError())
assert not await rest.async_setup_platform(
hass,
- {CONF_PLATFORM: rest.DOMAIN, CONF_RESOURCE: "http://localhost"},
+ {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "http://localhost"},
None,
)
@@ -75,11 +76,12 @@ async def test_setup_minimum(hass, aioclient_mock):
SWITCH_DOMAIN,
{
SWITCH_DOMAIN: {
- CONF_PLATFORM: rest.DOMAIN,
+ CONF_PLATFORM: DOMAIN,
CONF_RESOURCE: "http://localhost",
}
},
)
+ await hass.async_block_till_done()
assert aioclient_mock.call_count == 1
@@ -92,12 +94,14 @@ async def test_setup_query_params(hass, aioclient_mock):
SWITCH_DOMAIN,
{
SWITCH_DOMAIN: {
- CONF_PLATFORM: rest.DOMAIN,
+ CONF_PLATFORM: DOMAIN,
CONF_RESOURCE: "http://localhost",
CONF_PARAMS: {"search": "something"},
}
},
)
+ await hass.async_block_till_done()
+
print(aioclient_mock)
assert aioclient_mock.call_count == 1
@@ -110,7 +114,7 @@ async def test_setup(hass, aioclient_mock):
SWITCH_DOMAIN,
{
SWITCH_DOMAIN: {
- CONF_PLATFORM: rest.DOMAIN,
+ CONF_PLATFORM: DOMAIN,
CONF_NAME: "foo",
CONF_RESOURCE: "http://localhost",
CONF_HEADERS: {"Content-type": CONTENT_TYPE_JSON},
@@ -119,6 +123,7 @@ async def test_setup(hass, aioclient_mock):
}
},
)
+ await hass.async_block_till_done()
assert aioclient_mock.call_count == 1
assert_setup_component(1, SWITCH_DOMAIN)
@@ -132,7 +137,7 @@ async def test_setup_with_state_resource(hass, aioclient_mock):
SWITCH_DOMAIN,
{
SWITCH_DOMAIN: {
- CONF_PLATFORM: rest.DOMAIN,
+ CONF_PLATFORM: DOMAIN,
CONF_NAME: "foo",
CONF_RESOURCE: "http://localhost",
rest.CONF_STATE_RESOURCE: "http://localhost/state",
@@ -142,6 +147,7 @@ async def test_setup_with_state_resource(hass, aioclient_mock):
}
},
)
+ await hass.async_block_till_done()
assert aioclient_mock.call_count == 1
assert_setup_component(1, SWITCH_DOMAIN)
@@ -173,7 +179,7 @@ def _setup_test_switch(hass):
def test_name(hass):
"""Test the name."""
switch, body_on, body_off = _setup_test_switch(hass)
- assert NAME == switch.name
+ assert switch.name == NAME
def test_is_on_before_update(hass):
diff --git a/tests/components/rflink/conftest.py b/tests/components/rflink/conftest.py
index f33c8ab89dbbc8..dcaeb0a5e01e51 100644
--- a/tests/components/rflink/conftest.py
+++ b/tests/components/rflink/conftest.py
@@ -1,2 +1,2 @@
"""rflink conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
diff --git a/tests/components/rflink/test_cover.py b/tests/components/rflink/test_cover.py
index fd3f5b84314577..1dac064d778c53 100644
--- a/tests/components/rflink/test_cover.py
+++ b/tests/components/rflink/test_cover.py
@@ -89,20 +89,16 @@ async def test_default_setup(hass, monkeypatch):
assert hass.states.get(f"{DOMAIN}.test").state == STATE_OPEN
# test changing state from HA propagates to RFLink
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
)
await hass.async_block_till_done()
assert hass.states.get(f"{DOMAIN}.test").state == STATE_CLOSED
assert protocol.send_command_ack.call_args_list[0][0][0] == "protocol_0_0"
assert protocol.send_command_ack.call_args_list[0][0][1] == "DOWN"
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
)
await hass.async_block_till_done()
assert hass.states.get(f"{DOMAIN}.test").state == STATE_OPEN
@@ -162,10 +158,8 @@ async def test_signal_repetitions(hass, monkeypatch):
_, _, protocol, _ = await mock_rflink(hass, config, DOMAIN, monkeypatch)
# test if signal repetition is performed according to configuration
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
)
# wait for commands and repetitions to finish
@@ -174,10 +168,8 @@ async def test_signal_repetitions(hass, monkeypatch):
assert protocol.send_command_ack.call_count == 2
# test if default apply to configured devices
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.test1"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.test1"}
)
# wait for commands and repetitions to finish
@@ -202,15 +194,11 @@ async def test_signal_repetitions_alternation(hass, monkeypatch):
# setup mocking rflink module
_, _, protocol, _ = await mock_rflink(hass, config, DOMAIN, monkeypatch)
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
)
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.test1"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.test1"}
)
await hass.async_block_till_done()
@@ -234,16 +222,12 @@ async def test_signal_repetitions_cancelling(hass, monkeypatch):
# setup mocking rflink module
_, _, protocol, _ = await mock_rflink(hass, config, DOMAIN, monkeypatch)
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
)
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
)
await hass.async_block_till_done()
@@ -606,12 +590,10 @@ async def test_inverted_cover(hass, monkeypatch):
# Sending the close command from HA should result
# in an 'DOWN' command sent to a non-newkaku device
# that has its type set to 'standard'.
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN,
- SERVICE_CLOSE_COVER,
- {ATTR_ENTITY_ID: f"{DOMAIN}.nonkaku_type_standard"},
- )
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_CLOSE_COVER,
+ {ATTR_ENTITY_ID: f"{DOMAIN}.nonkaku_type_standard"},
)
await hass.async_block_till_done()
@@ -623,12 +605,10 @@ async def test_inverted_cover(hass, monkeypatch):
# Sending the open command from HA should result
# in an 'UP' command sent to a non-newkaku device
# that has its type set to 'standard'.
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN,
- SERVICE_OPEN_COVER,
- {ATTR_ENTITY_ID: f"{DOMAIN}.nonkaku_type_standard"},
- )
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_OPEN_COVER,
+ {ATTR_ENTITY_ID: f"{DOMAIN}.nonkaku_type_standard"},
)
await hass.async_block_till_done()
@@ -640,10 +620,8 @@ async def test_inverted_cover(hass, monkeypatch):
# Sending the close command from HA should result
# in an 'DOWN' command sent to a non-newkaku device
# that has its type not specified.
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.nonkaku_type_none"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.nonkaku_type_none"}
)
await hass.async_block_till_done()
@@ -655,10 +633,8 @@ async def test_inverted_cover(hass, monkeypatch):
# Sending the open command from HA should result
# in an 'UP' command sent to a non-newkaku device
# that has its type not specified.
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.nonkaku_type_none"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.nonkaku_type_none"}
)
await hass.async_block_till_done()
@@ -670,12 +646,10 @@ async def test_inverted_cover(hass, monkeypatch):
# Sending the close command from HA should result
# in an 'UP' command sent to a non-newkaku device
# that has its type set to 'inverted'.
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN,
- SERVICE_CLOSE_COVER,
- {ATTR_ENTITY_ID: f"{DOMAIN}.nonkaku_type_inverted"},
- )
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_CLOSE_COVER,
+ {ATTR_ENTITY_ID: f"{DOMAIN}.nonkaku_type_inverted"},
)
await hass.async_block_till_done()
@@ -687,12 +661,10 @@ async def test_inverted_cover(hass, monkeypatch):
# Sending the open command from HA should result
# in an 'DOWN' command sent to a non-newkaku device
# that has its type set to 'inverted'.
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN,
- SERVICE_OPEN_COVER,
- {ATTR_ENTITY_ID: f"{DOMAIN}.nonkaku_type_inverted"},
- )
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_OPEN_COVER,
+ {ATTR_ENTITY_ID: f"{DOMAIN}.nonkaku_type_inverted"},
)
await hass.async_block_till_done()
@@ -704,12 +676,10 @@ async def test_inverted_cover(hass, monkeypatch):
# Sending the close command from HA should result
# in an 'DOWN' command sent to a newkaku device
# that has its type set to 'standard'.
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN,
- SERVICE_CLOSE_COVER,
- {ATTR_ENTITY_ID: f"{DOMAIN}.newkaku_type_standard"},
- )
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_CLOSE_COVER,
+ {ATTR_ENTITY_ID: f"{DOMAIN}.newkaku_type_standard"},
)
await hass.async_block_till_done()
@@ -721,12 +691,10 @@ async def test_inverted_cover(hass, monkeypatch):
# Sending the open command from HA should result
# in an 'UP' command sent to a newkaku device
# that has its type set to 'standard'.
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN,
- SERVICE_OPEN_COVER,
- {ATTR_ENTITY_ID: f"{DOMAIN}.newkaku_type_standard"},
- )
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_OPEN_COVER,
+ {ATTR_ENTITY_ID: f"{DOMAIN}.newkaku_type_standard"},
)
await hass.async_block_till_done()
@@ -738,10 +706,8 @@ async def test_inverted_cover(hass, monkeypatch):
# Sending the close command from HA should result
# in an 'UP' command sent to a newkaku device
# that has its type not specified.
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.newkaku_type_none"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.newkaku_type_none"}
)
await hass.async_block_till_done()
@@ -753,10 +719,8 @@ async def test_inverted_cover(hass, monkeypatch):
# Sending the open command from HA should result
# in an 'DOWN' command sent to a newkaku device
# that has its type not specified.
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.newkaku_type_none"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: f"{DOMAIN}.newkaku_type_none"}
)
await hass.async_block_till_done()
@@ -768,12 +732,10 @@ async def test_inverted_cover(hass, monkeypatch):
# Sending the close command from HA should result
# in an 'UP' command sent to a newkaku device
# that has its type set to 'inverted'.
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN,
- SERVICE_CLOSE_COVER,
- {ATTR_ENTITY_ID: f"{DOMAIN}.newkaku_type_inverted"},
- )
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_CLOSE_COVER,
+ {ATTR_ENTITY_ID: f"{DOMAIN}.newkaku_type_inverted"},
)
await hass.async_block_till_done()
@@ -785,12 +747,10 @@ async def test_inverted_cover(hass, monkeypatch):
# Sending the open command from HA should result
# in an 'DOWN' command sent to a newkaku device
# that has its type set to 'inverted'.
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN,
- SERVICE_OPEN_COVER,
- {ATTR_ENTITY_ID: f"{DOMAIN}.newkaku_type_inverted"},
- )
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_OPEN_COVER,
+ {ATTR_ENTITY_ID: f"{DOMAIN}.newkaku_type_inverted"},
)
await hass.async_block_till_done()
diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py
index 7ba90286e62d04..f93c9703d3023c 100644
--- a/tests/components/rflink/test_init.py
+++ b/tests/components/rflink/test_init.py
@@ -107,10 +107,8 @@ async def test_send_no_wait(hass, monkeypatch):
# setup mocking rflink module
_, _, protocol, _ = await mock_rflink(hass, config, domain, monkeypatch)
- hass.async_create_task(
- hass.services.async_call(
- domain, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "switch.test"}
- )
+ await hass.services.async_call(
+ domain, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "switch.test"}
)
await hass.async_block_till_done()
assert protocol.send_command.call_args_list[0][0][0] == "protocol_0_0"
@@ -133,10 +131,8 @@ async def test_cover_send_no_wait(hass, monkeypatch):
# setup mocking rflink module
_, _, protocol, _ = await mock_rflink(hass, config, domain, monkeypatch)
- hass.async_create_task(
- hass.services.async_call(
- domain, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: "cover.test"}
- )
+ await hass.services.async_call(
+ domain, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: "cover.test"}
)
await hass.async_block_till_done()
assert protocol.send_command.call_args_list[0][0][0] == "RTS_0100F2_0"
@@ -151,12 +147,10 @@ async def test_send_command(hass, monkeypatch):
# setup mocking rflink module
_, _, protocol, _ = await mock_rflink(hass, config, domain, monkeypatch)
- hass.async_create_task(
- hass.services.async_call(
- domain,
- SERVICE_SEND_COMMAND,
- {"device_id": "newkaku_0000c6c2_1", "command": "on"},
- )
+ await hass.services.async_call(
+ domain,
+ SERVICE_SEND_COMMAND,
+ {"device_id": "newkaku_0000c6c2_1", "command": "on"},
)
await hass.async_block_till_done()
assert protocol.send_command_ack.call_args_list[0][0][0] == "newkaku_0000c6c2_1"
@@ -196,6 +190,48 @@ async def test_send_command_invalid_arguments(hass, monkeypatch):
assert not success, "send command should not succeed for unknown command"
+async def test_send_command_event_propagation(hass, monkeypatch):
+ """Test event propagation for send_command service."""
+ domain = "light"
+ config = {
+ "rflink": {"port": "/dev/ttyABC0"},
+ domain: {
+ "platform": "rflink",
+ "devices": {
+ "protocol_0_1": {"name": "test1"},
+ },
+ },
+ }
+
+ # setup mocking rflink module
+ _, _, protocol, _ = await mock_rflink(hass, config, domain, monkeypatch)
+
+ # default value = 'off'
+ assert hass.states.get(f"{domain}.test1").state == "off"
+
+ await hass.services.async_call(
+ "rflink",
+ SERVICE_SEND_COMMAND,
+ {"device_id": "protocol_0_1", "command": "on"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert protocol.send_command_ack.call_args_list[0][0][0] == "protocol_0_1"
+ assert protocol.send_command_ack.call_args_list[0][0][1] == "on"
+ assert hass.states.get(f"{domain}.test1").state == "on"
+
+ await hass.services.async_call(
+ "rflink",
+ SERVICE_SEND_COMMAND,
+ {"device_id": "protocol_0_1", "command": "alloff"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert protocol.send_command_ack.call_args_list[1][0][0] == "protocol_0_1"
+ assert protocol.send_command_ack.call_args_list[1][0][1] == "alloff"
+ assert hass.states.get(f"{domain}.test1").state == "off"
+
+
async def test_reconnecting_after_disconnect(hass, monkeypatch):
"""An unexpected disconnect should cause a reconnect."""
domain = "sensor"
diff --git a/tests/components/rflink/test_light.py b/tests/components/rflink/test_light.py
index f3ea5e303754fa..5f9672ac9fc8f7 100644
--- a/tests/components/rflink/test_light.py
+++ b/tests/components/rflink/test_light.py
@@ -96,20 +96,16 @@ async def test_default_setup(hass, monkeypatch):
assert hass.states.get(f"{DOMAIN}.protocol2_0_1").state == "on"
# test changing state from HA propagates to RFLink
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
)
await hass.async_block_till_done()
assert hass.states.get(f"{DOMAIN}.test").state == "off"
assert protocol.send_command_ack.call_args_list[0][0][0] == "protocol_0_0"
assert protocol.send_command_ack.call_args_list[0][0][1] == "off"
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
)
await hass.async_block_till_done()
assert hass.states.get(f"{DOMAIN}.test").state == "on"
@@ -118,10 +114,8 @@ async def test_default_setup(hass, monkeypatch):
# protocols supporting dimming and on/off should create hybrid light entity
event_callback({"id": "newkaku_0_1", "command": "off"})
await hass.async_block_till_done()
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: f"{DOMAIN}.newkaku_0_1"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: f"{DOMAIN}.newkaku_0_1"}
)
await hass.async_block_till_done()
@@ -131,23 +125,19 @@ async def test_default_setup(hass, monkeypatch):
# and send on command for fallback
assert protocol.send_command_ack.call_args_list[3][0][1] == "on"
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: f"{DOMAIN}.newkaku_0_1", ATTR_BRIGHTNESS: 128},
- )
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: f"{DOMAIN}.newkaku_0_1", ATTR_BRIGHTNESS: 128},
)
await hass.async_block_till_done()
assert protocol.send_command_ack.call_args_list[4][0][1] == "7"
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN,
- SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: f"{DOMAIN}.dim_test", ATTR_BRIGHTNESS: 128},
- )
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: f"{DOMAIN}.dim_test", ATTR_BRIGHTNESS: 128},
)
await hass.async_block_till_done()
@@ -210,10 +200,8 @@ async def test_signal_repetitions(hass, monkeypatch):
)
# test if signal repetition is performed according to configuration
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
)
# wait for commands and repetitions to finish
@@ -222,10 +210,8 @@ async def test_signal_repetitions(hass, monkeypatch):
assert protocol.send_command_ack.call_count == 2
# test if default apply to configured devices
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: f"{DOMAIN}.test1"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: f"{DOMAIN}.test1"}
)
# wait for commands and repetitions to finish
@@ -239,10 +225,8 @@ async def test_signal_repetitions(hass, monkeypatch):
# make sure entity is created before setting state
await hass.async_block_till_done()
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: f"{DOMAIN}.protocol_0_2"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: f"{DOMAIN}.protocol_0_2"}
)
# wait for commands and repetitions to finish
@@ -340,26 +324,109 @@ async def test_type_toggle(hass, monkeypatch):
assert hass.states.get(f"{DOMAIN}.toggle_test").state == "off"
# test async_turn_off, must set state = 'on' ('off' + toggle)
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: f"{DOMAIN}.toggle_test"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: f"{DOMAIN}.toggle_test"}
)
await hass.async_block_till_done()
assert hass.states.get(f"{DOMAIN}.toggle_test").state == "on"
# test async_turn_on, must set state = 'off' (yes, sounds crazy)
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: f"{DOMAIN}.toggle_test"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: f"{DOMAIN}.toggle_test"}
)
await hass.async_block_till_done()
assert hass.states.get(f"{DOMAIN}.toggle_test").state == "off"
+async def test_set_level_command(hass, monkeypatch):
+ """Test 'set_level=XX' events."""
+ config = {
+ "rflink": {"port": "/dev/ttyABC0"},
+ DOMAIN: {
+ "platform": "rflink",
+ "devices": {
+ "newkaku_12345678_0": {"name": "l1"},
+ "test_no_dimmable": {"name": "l2"},
+ "test_dimmable": {"name": "l3", "type": "dimmable"},
+ "test_hybrid": {"name": "l4", "type": "hybrid"},
+ },
+ },
+ }
+
+ # setup mocking rflink module
+ event_callback, _, _, _ = await mock_rflink(hass, config, DOMAIN, monkeypatch)
+
+ # test sending command to a newkaku device
+ event_callback({"id": "newkaku_12345678_0", "command": "set_level=10"})
+ await hass.async_block_till_done()
+ # should affect state
+ state = hass.states.get(f"{DOMAIN}.l1")
+ assert state
+ assert state.state == STATE_ON
+ assert state.attributes[ATTR_BRIGHTNESS] == 170
+ # turn off
+ event_callback({"id": "newkaku_12345678_0", "command": "off"})
+ await hass.async_block_till_done()
+ state = hass.states.get(f"{DOMAIN}.l1")
+ assert state
+ assert state.state == STATE_OFF
+ # off light shouldn't have brightness
+ assert not state.attributes.get(ATTR_BRIGHTNESS)
+ # turn on
+ event_callback({"id": "newkaku_12345678_0", "command": "on"})
+ await hass.async_block_till_done()
+ state = hass.states.get(f"{DOMAIN}.l1")
+ assert state
+ assert state.state == STATE_ON
+ assert state.attributes[ATTR_BRIGHTNESS] == 170
+
+ # test sending command to a no dimmable device
+ event_callback({"id": "test_no_dimmable", "command": "set_level=10"})
+ await hass.async_block_till_done()
+ # should NOT affect state
+ state = hass.states.get(f"{DOMAIN}.l2")
+ assert state
+ assert state.state == STATE_OFF
+ assert not state.attributes.get(ATTR_BRIGHTNESS)
+
+ # test sending command to a dimmable device
+ event_callback({"id": "test_dimmable", "command": "set_level=5"})
+ await hass.async_block_till_done()
+ # should affect state
+ state = hass.states.get(f"{DOMAIN}.l3")
+ assert state
+ assert state.state == STATE_ON
+ assert state.attributes[ATTR_BRIGHTNESS] == 85
+
+ # test sending command to a hybrid device
+ event_callback({"id": "test_hybrid", "command": "set_level=15"})
+ await hass.async_block_till_done()
+ # should affect state
+ state = hass.states.get(f"{DOMAIN}.l4")
+ assert state
+ assert state.state == STATE_ON
+ assert state.attributes[ATTR_BRIGHTNESS] == 255
+
+ event_callback({"id": "test_hybrid", "command": "off"})
+ await hass.async_block_till_done()
+ # should affect state
+ state = hass.states.get(f"{DOMAIN}.l4")
+ assert state
+ assert state.state == STATE_OFF
+ # off light shouldn't have brightness
+ assert not state.attributes.get(ATTR_BRIGHTNESS)
+
+ event_callback({"id": "test_hybrid", "command": "set_level=0"})
+ await hass.async_block_till_done()
+ # should affect state
+ state = hass.states.get(f"{DOMAIN}.l4")
+ assert state
+ assert state.state == STATE_ON
+ assert state.attributes[ATTR_BRIGHTNESS] == 0
+
+
async def test_group_alias(hass, monkeypatch):
"""Group aliases should only respond to group commands (allon/alloff)."""
config = {
@@ -367,7 +434,12 @@ async def test_group_alias(hass, monkeypatch):
DOMAIN: {
"platform": "rflink",
"devices": {
- "protocol_0_0": {"name": "test", "group_aliases": ["test_group_0_0"]}
+ "protocol_0_0": {"name": "test", "group_aliases": ["test_group_0_0"]},
+ "protocol_0_1": {
+ "name": "test2",
+ "type": "dimmable",
+ "group_aliases": ["test_group_0_0"],
+ },
},
},
}
@@ -382,12 +454,14 @@ async def test_group_alias(hass, monkeypatch):
await hass.async_block_till_done()
assert hass.states.get(f"{DOMAIN}.test").state == "on"
+ assert hass.states.get(f"{DOMAIN}.test2").state == "on"
# test sending group command to group alias
event_callback({"id": "test_group_0_0", "command": "off"})
await hass.async_block_till_done()
assert hass.states.get(f"{DOMAIN}.test").state == "on"
+ assert hass.states.get(f"{DOMAIN}.test2").state == "on"
async def test_nogroup_alias(hass, monkeypatch):
@@ -416,7 +490,7 @@ async def test_nogroup_alias(hass, monkeypatch):
# should not affect state
assert hass.states.get(f"{DOMAIN}.test").state == "off"
- # test sending group command to nogroup alias
+ # test sending group commands to nogroup alias
event_callback({"id": "test_nogroup_0_0", "command": "on"})
await hass.async_block_till_done()
# should affect state
@@ -521,7 +595,8 @@ async def test_restore_state(hass, monkeypatch):
state = hass.states.get(f"{DOMAIN}.l4")
assert state
assert state.state == STATE_OFF
- assert state.attributes[ATTR_BRIGHTNESS] == 255
+ # off light shouldn't have brightness
+ assert not state.attributes.get(ATTR_BRIGHTNESS)
assert state.attributes["assumed_state"]
# test coverage for dimmable light
diff --git a/tests/components/rflink/test_switch.py b/tests/components/rflink/test_switch.py
index d696e8933be044..ef6bac55f21771 100644
--- a/tests/components/rflink/test_switch.py
+++ b/tests/components/rflink/test_switch.py
@@ -76,20 +76,16 @@ async def test_default_setup(hass, monkeypatch):
# events because every new unknown device is added as a light by default.
# test changing state from HA propagates to Rflink
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
)
await hass.async_block_till_done()
assert hass.states.get(f"{DOMAIN}.test").state == "off"
assert protocol.send_command_ack.call_args_list[0][0][0] == "protocol_0_0"
assert protocol.send_command_ack.call_args_list[0][0][1] == "off"
- hass.async_create_task(
- hass.services.async_call(
- DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
- )
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
)
await hass.async_block_till_done()
assert hass.states.get(f"{DOMAIN}.test").state == "on"
diff --git a/tests/components/rflink/test_utils.py b/tests/components/rflink/test_utils.py
new file mode 100644
index 00000000000000..50dd8100e8e41d
--- /dev/null
+++ b/tests/components/rflink/test_utils.py
@@ -0,0 +1,33 @@
+"""Test for RFLink utils methods."""
+from homeassistant.components.rflink.utils import (
+ brightness_to_rflink,
+ rflink_to_brightness,
+)
+
+
+async def test_utils(hass, monkeypatch):
+ """Test all utils methods."""
+ # test brightness_to_rflink
+ assert brightness_to_rflink(0) == 0
+ assert brightness_to_rflink(17) == 1
+ assert brightness_to_rflink(34) == 2
+ assert brightness_to_rflink(85) == 5
+ assert brightness_to_rflink(170) == 10
+ assert brightness_to_rflink(255) == 15
+
+ assert brightness_to_rflink(10) == 0
+ assert brightness_to_rflink(20) == 1
+ assert brightness_to_rflink(30) == 1
+ assert brightness_to_rflink(40) == 2
+ assert brightness_to_rflink(50) == 2
+ assert brightness_to_rflink(60) == 3
+ assert brightness_to_rflink(70) == 4
+ assert brightness_to_rflink(80) == 4
+
+ # test rflink_to_brightness
+ assert rflink_to_brightness(0) == 0
+ assert rflink_to_brightness(1) == 17
+ assert rflink_to_brightness(5) == 85
+ assert rflink_to_brightness(10) == 170
+ assert rflink_to_brightness(12) == 204
+ assert rflink_to_brightness(15) == 255
diff --git a/tests/components/rfxtrx/conftest.py b/tests/components/rfxtrx/conftest.py
index ee695bee9dd0fb..06e37545d252b9 100644
--- a/tests/components/rfxtrx/conftest.py
+++ b/tests/components/rfxtrx/conftest.py
@@ -9,7 +9,7 @@
from homeassistant.util.dt import utcnow
from tests.common import MockConfigEntry, async_fire_time_changed
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
def create_rfx_test_cfg(device="abcd", automatic_add=False, devices=None):
diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py
index e39c766bfd20eb..1c16507f960cfc 100644
--- a/tests/components/rfxtrx/test_config_flow.py
+++ b/tests/components/rfxtrx/test_config_flow.py
@@ -6,13 +6,7 @@
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.rfxtrx import DOMAIN, config_flow
-from homeassistant.helpers.device_registry import (
- async_entries_for_config_entry,
- async_get_registry as async_get_device_registry,
-)
-from homeassistant.helpers.entity_registry import (
- async_get_registry as async_get_entity_registry,
-)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.common import MockConfigEntry
@@ -610,8 +604,8 @@ async def test_options_add_remove_device(hass):
assert state.state == "off"
assert state.attributes.get("friendly_name") == "AC 213c7f2:48"
- device_registry = await async_get_device_registry(hass)
- device_entries = async_entries_for_config_entry(device_registry, entry.entry_id)
+ device_registry = dr.async_get(hass)
+ device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
assert device_entries[0].id
@@ -704,8 +698,8 @@ async def test_options_replace_sensor_device(hass):
)
assert state
- device_registry = await async_get_device_registry(hass)
- device_entries = async_entries_for_config_entry(device_registry, entry.entry_id)
+ device_registry = dr.async_get(hass)
+ device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
old_device = next(
(
@@ -751,7 +745,7 @@ async def test_options_replace_sensor_device(hass):
await hass.async_block_till_done()
- entity_registry = await async_get_entity_registry(hass)
+ entity_registry = er.async_get(hass)
entry = entity_registry.async_get(
"sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_rssi_numeric"
@@ -843,8 +837,8 @@ async def test_options_replace_control_device(hass):
state = hass.states.get("switch.ac_1118cdea_2")
assert state
- device_registry = await async_get_device_registry(hass)
- device_entries = async_entries_for_config_entry(device_registry, entry.entry_id)
+ device_registry = dr.async_get(hass)
+ device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
old_device = next(
(
@@ -890,7 +884,7 @@ async def test_options_replace_control_device(hass):
await hass.async_block_till_done()
- entity_registry = await async_get_entity_registry(hass)
+ entity_registry = er.async_get(hass)
entry = entity_registry.async_get("binary_sensor.ac_118cdea_2")
assert entry
@@ -941,8 +935,8 @@ async def test_options_remove_multiple_devices(hass):
state = hass.states.get("binary_sensor.ac_1118cdea_2")
assert state
- device_registry = await async_get_device_registry(hass)
- device_entries = async_entries_for_config_entry(device_registry, entry.entry_id)
+ device_registry = dr.async_get(hass)
+ device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
assert len(device_entries) == 3
@@ -1061,8 +1055,8 @@ async def test_options_add_and_configure_device(hass):
assert state.state == "off"
assert state.attributes.get("friendly_name") == "PT2262 22670e"
- device_registry = await async_get_device_registry(hass)
- device_entries = async_entries_for_config_entry(device_registry, entry.entry_id)
+ device_registry = dr.async_get(hass)
+ device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
assert device_entries[0].id
@@ -1151,8 +1145,8 @@ async def test_options_configure_rfy_cover_device(hass):
assert entry.data["devices"]["071a000001020301"]["venetian_blind_mode"] == "EU"
- device_registry = await async_get_device_registry(hass)
- device_entries = async_entries_for_config_entry(device_registry, entry.entry_id)
+ device_registry = dr.async_get(hass)
+ device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
assert device_entries[0].id
diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py
index 9112082a956a9a..b3829e2b5cc317 100644
--- a/tests/components/rfxtrx/test_init.py
+++ b/tests/components/rfxtrx/test_init.py
@@ -5,10 +5,7 @@
from homeassistant.components.rfxtrx import DOMAIN
from homeassistant.components.rfxtrx.const import EVENT_RFXTRX_EVENT
from homeassistant.core import callback
-from homeassistant.helpers.device_registry import (
- DeviceRegistry,
- async_get_registry as async_get_device_registry,
-)
+from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
@@ -79,7 +76,7 @@ async def test_fire_event(hass, rfxtrx):
await hass.async_block_till_done()
await hass.async_start()
- device_registry: DeviceRegistry = await async_get_device_registry(hass)
+ device_registry: dr.DeviceRegistry = dr.async_get(hass)
calls = []
diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py
index f2e05189dea285..cda662aab6491d 100644
--- a/tests/components/ring/conftest.py
+++ b/tests/components/ring/conftest.py
@@ -5,7 +5,7 @@
import requests_mock
from tests.common import load_fixture
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
@pytest.fixture(name="requests_mock")
diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py
index 5a2687e4cf91c0..603c0bf3e84f77 100644
--- a/tests/components/ring/test_light.py
+++ b/tests/components/ring/test_light.py
@@ -1,5 +1,6 @@
"""The tests for the Ring light platform."""
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
+from homeassistant.helpers import entity_registry as er
from .common import setup_platform
@@ -9,7 +10,7 @@
async def test_entity_registry(hass, requests_mock):
"""Tests that the devices are registered in the entity registry."""
await setup_platform(hass, LIGHT_DOMAIN)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry = entity_registry.async_get("light.front_light")
assert entry.unique_id == 765432
diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py
index 6979fafc01d76d..ab81ed5c69a3ed 100644
--- a/tests/components/ring/test_switch.py
+++ b/tests/components/ring/test_switch.py
@@ -1,5 +1,6 @@
"""The tests for the Ring switch platform."""
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
+from homeassistant.helpers import entity_registry as er
from .common import setup_platform
@@ -9,7 +10,7 @@
async def test_entity_registry(hass, requests_mock):
"""Tests that the devices are registered in the entity registry."""
await setup_platform(hass, SWITCH_DOMAIN)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry = entity_registry.async_get("switch.front_siren")
assert entry.unique_id == "765432-siren"
diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py
index a5a16379fe93ed..23c4de4c7aafbd 100644
--- a/tests/components/risco/test_alarm_control_panel.py
+++ b/tests/components/risco/test_alarm_control_panel.py
@@ -27,6 +27,7 @@
STATE_ALARM_TRIGGERED,
STATE_UNKNOWN,
)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.entity_component import async_update_entity
from .util import TEST_CONFIG, TEST_SITE_UUID, setup_risco
@@ -114,7 +115,7 @@ async def test_cannot_connect(hass):
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert not registry.async_is_registered(FIRST_ENTITY_ID)
assert not registry.async_is_registered(SECOND_ENTITY_ID)
@@ -130,14 +131,14 @@ async def test_unauthorized(hass):
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert not registry.async_is_registered(FIRST_ENTITY_ID)
assert not registry.async_is_registered(SECOND_ENTITY_ID)
async def test_setup(hass, two_part_alarm):
"""Test entity setup."""
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert not registry.async_is_registered(FIRST_ENTITY_ID)
assert not registry.async_is_registered(SECOND_ENTITY_ID)
@@ -147,7 +148,7 @@ async def test_setup(hass, two_part_alarm):
assert registry.async_is_registered(FIRST_ENTITY_ID)
assert registry.async_is_registered(SECOND_ENTITY_ID)
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_0")})
assert device is not None
assert device.manufacturer == "Risco"
@@ -251,7 +252,7 @@ async def test_sets_custom_mapping(hass, two_part_alarm):
"""Test settings the various modes when mapping some states."""
await setup_risco(hass, [], CUSTOM_MAPPING_OPTIONS)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entity = registry.async_get(FIRST_ENTITY_ID)
assert entity.supported_features == EXPECTED_FEATURES
@@ -277,7 +278,7 @@ async def test_sets_full_custom_mapping(hass, two_part_alarm):
"""Test settings the various modes when mapping all states."""
await setup_risco(hass, [], FULL_CUSTOM_MAPPING)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entity = registry.async_get(FIRST_ENTITY_ID)
assert (
entity.supported_features == EXPECTED_FEATURES | SUPPORT_ALARM_ARM_CUSTOM_BYPASS
diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py
index 7533512e3ef653..7f68db7939d484 100644
--- a/tests/components/risco/test_binary_sensor.py
+++ b/tests/components/risco/test_binary_sensor.py
@@ -4,6 +4,7 @@
from homeassistant.components.risco import CannotConnectError, UnauthorizedError
from homeassistant.components.risco.const import DOMAIN
from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.entity_component import async_update_entity
from .util import TEST_CONFIG, TEST_SITE_UUID, setup_risco
@@ -26,7 +27,7 @@ async def test_cannot_connect(hass):
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert not registry.async_is_registered(FIRST_ENTITY_ID)
assert not registry.async_is_registered(SECOND_ENTITY_ID)
@@ -42,14 +43,14 @@ async def test_unauthorized(hass):
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert not registry.async_is_registered(FIRST_ENTITY_ID)
assert not registry.async_is_registered(SECOND_ENTITY_ID)
async def test_setup(hass, two_zone_alarm): # noqa: F811
"""Test entity setup."""
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
assert not registry.async_is_registered(FIRST_ENTITY_ID)
assert not registry.async_is_registered(SECOND_ENTITY_ID)
@@ -59,7 +60,7 @@ async def test_setup(hass, two_zone_alarm): # noqa: F811
assert registry.async_is_registered(FIRST_ENTITY_ID)
assert registry.async_is_registered(SECOND_ENTITY_ID)
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_0")})
assert device is not None
assert device.manufacturer == "Risco"
diff --git a/tests/components/risco/test_sensor.py b/tests/components/risco/test_sensor.py
index 3d449f10e46f32..eb7ed990bd99b4 100644
--- a/tests/components/risco/test_sensor.py
+++ b/tests/components/risco/test_sensor.py
@@ -8,6 +8,7 @@
UnauthorizedError,
)
from homeassistant.components.risco.const import DOMAIN
+from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt
from .util import TEST_CONFIG, TEST_SITE_UUID, setup_risco
@@ -120,7 +121,7 @@ async def test_cannot_connect(hass):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
for id in ENTITY_IDS.values():
assert not registry.async_is_registered(id)
@@ -137,7 +138,7 @@ async def test_unauthorized(hass):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
for id in ENTITY_IDS.values():
assert not registry.async_is_registered(id)
@@ -167,7 +168,7 @@ def _check_state(hass, category, entity_id):
async def test_setup(hass, two_zone_alarm): # noqa: F811
"""Test entity setup."""
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
for id in ENTITY_IDS.values():
assert not registry.async_is_registered(id)
diff --git a/tests/components/rituals_perfume_genie/__init__.py b/tests/components/rituals_perfume_genie/__init__.py
new file mode 100644
index 00000000000000..bd90242f14c210
--- /dev/null
+++ b/tests/components/rituals_perfume_genie/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Rituals Perfume Genie integration."""
diff --git a/tests/components/rituals_perfume_genie/test_config_flow.py b/tests/components/rituals_perfume_genie/test_config_flow.py
new file mode 100644
index 00000000000000..92c3e15c2478cc
--- /dev/null
+++ b/tests/components/rituals_perfume_genie/test_config_flow.py
@@ -0,0 +1,119 @@
+"""Test the Rituals Perfume Genie config flow."""
+from unittest.mock import AsyncMock, MagicMock, patch
+
+from aiohttp import ClientResponseError
+from pyrituals import AuthenticationException
+
+from homeassistant import config_entries
+from homeassistant.components.rituals_perfume_genie.const import ACCOUNT_HASH, DOMAIN
+from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
+
+TEST_EMAIL = "rituals@example.com"
+VALID_PASSWORD = "passw0rd"
+WRONG_PASSWORD = "wrong-passw0rd"
+
+
+def _mock_account(*_):
+ account = MagicMock()
+ account.authenticate = AsyncMock()
+ account.data = {CONF_EMAIL: TEST_EMAIL, ACCOUNT_HASH: "any"}
+ return account
+
+
+async def test_form(hass):
+ """Test we get the form."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] is None
+
+ with patch(
+ "homeassistant.components.rituals_perfume_genie.config_flow.Account",
+ side_effect=_mock_account,
+ ), patch(
+ "homeassistant.components.rituals_perfume_genie.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.rituals_perfume_genie.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_EMAIL: TEST_EMAIL,
+ CONF_PASSWORD: VALID_PASSWORD,
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == TEST_EMAIL
+ assert isinstance(result2["data"][ACCOUNT_HASH], str)
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_invalid_auth(hass):
+ """Test we handle invalid auth."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.rituals_perfume_genie.config_flow.Account.authenticate",
+ side_effect=AuthenticationException,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_EMAIL: TEST_EMAIL,
+ CONF_PASSWORD: WRONG_PASSWORD,
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "invalid_auth"}
+
+
+async def test_form_auth_exception(hass):
+ """Test we handle auth exception."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.rituals_perfume_genie.config_flow.Account.authenticate",
+ side_effect=Exception,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_EMAIL: TEST_EMAIL,
+ CONF_PASSWORD: VALID_PASSWORD,
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "unknown"}
+
+
+async def test_form_cannot_connect(hass):
+ """Test we handle cannot connect error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.rituals_perfume_genie.config_flow.Account.authenticate",
+ side_effect=ClientResponseError(None, None, status=500),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_EMAIL: TEST_EMAIL,
+ CONF_PASSWORD: VALID_PASSWORD,
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "cannot_connect"}
diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py
index 1a1b46117bdc28..dca336be076d09 100644
--- a/tests/components/roku/test_media_player.py
+++ b/tests/components/roku/test_media_player.py
@@ -61,18 +61,23 @@
STATE_STANDBY,
STATE_UNAVAILABLE,
)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import dt as dt_util
from tests.common import async_fire_time_changed
-from tests.components.roku import UPNP_SERIAL, setup_integration
+from tests.components.roku import NAME_ROKUTV, UPNP_SERIAL, setup_integration
from tests.test_util.aiohttp import AiohttpClientMocker
MAIN_ENTITY_ID = f"{MP_DOMAIN}.my_roku_3"
TV_ENTITY_ID = f"{MP_DOMAIN}.58_onn_roku_tv"
TV_HOST = "192.168.1.161"
+TV_LOCATION = "Living room"
+TV_MANUFACTURER = "Onn"
+TV_MODEL = "100005844"
TV_SERIAL = "YN00H5555555"
+TV_SW_VERSION = "9.2.0"
async def test_setup(
@@ -81,7 +86,7 @@ async def test_setup(
"""Test setup with basic config."""
await setup_integration(hass, aioclient_mock)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
main = entity_registry.async_get(MAIN_ENTITY_ID)
assert hass.states.get(MAIN_ENTITY_ID)
@@ -113,7 +118,7 @@ async def test_tv_setup(
unique_id=TV_SERIAL,
)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
tv = entity_registry.async_get(TV_ENTITY_ID)
assert hass.states.get(TV_ENTITY_ID)
@@ -304,6 +309,29 @@ async def test_tv_attributes(
assert state.attributes.get(ATTR_MEDIA_TITLE) == "Airwolf"
+async def test_tv_device_registry(
+ hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test device registered for Roku TV in the device registry."""
+ await setup_integration(
+ hass,
+ aioclient_mock,
+ device="rokutv",
+ app="tvinput-dtv",
+ host=TV_HOST,
+ unique_id=TV_SERIAL,
+ )
+
+ device_registry = dr.async_get(hass)
+ reg_device = device_registry.async_get_device(identifiers={(DOMAIN, TV_SERIAL)})
+
+ assert reg_device.model == TV_MODEL
+ assert reg_device.sw_version == TV_SW_VERSION
+ assert reg_device.manufacturer == TV_MANUFACTURER
+ assert reg_device.suggested_area == TV_LOCATION
+ assert reg_device.name == NAME_ROKUTV
+
+
async def test_services(
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
) -> None:
diff --git a/tests/components/roku/test_remote.py b/tests/components/roku/test_remote.py
index 4122e0af1d1ecd..363c7134bad6a9 100644
--- a/tests/components/roku/test_remote.py
+++ b/tests/components/roku/test_remote.py
@@ -7,6 +7,7 @@
SERVICE_SEND_COMMAND,
)
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.typing import HomeAssistantType
from tests.components.roku import UPNP_SERIAL, setup_integration
@@ -31,7 +32,7 @@ async def test_unique_id(
"""Test unique id."""
await setup_integration(hass, aioclient_mock)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
main = entity_registry.async_get(MAIN_ENTITY_ID)
assert main.unique_id == UPNP_SERIAL
diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py
index bf8e674950f68f..ee3b7d4b49752b 100644
--- a/tests/components/roomba/test_config_flow.py
+++ b/tests/components/roomba/test_config_flow.py
@@ -1,23 +1,54 @@
"""Test the iRobot Roomba config flow."""
from unittest.mock import MagicMock, PropertyMock, patch
+import pytest
from roombapy import RoombaConnectionError
from roombapy.roomba import RoombaInfo
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS
-from homeassistant.components.roomba.const import (
- CONF_BLID,
- CONF_CONTINUOUS,
- CONF_DELAY,
- DOMAIN,
-)
-from homeassistant.const import CONF_HOST, CONF_PASSWORD
+from homeassistant.components.roomba import config_flow
+from homeassistant.components.roomba.const import CONF_BLID, CONF_CONTINUOUS, DOMAIN
+from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_PASSWORD
from tests.common import MockConfigEntry
MOCK_IP = "1.2.3.4"
-VALID_CONFIG = {CONF_HOST: "1.2.3.4", CONF_BLID: "blid", CONF_PASSWORD: "password"}
+VALID_CONFIG = {CONF_HOST: MOCK_IP, CONF_BLID: "BLID", CONF_PASSWORD: "password"}
+
+DHCP_DISCOVERY_DEVICES = [
+ {
+ IP_ADDRESS: MOCK_IP,
+ MAC_ADDRESS: "50:14:79:DD:EE:FF",
+ HOSTNAME: "irobot-blid",
+ },
+ {
+ IP_ADDRESS: MOCK_IP,
+ MAC_ADDRESS: "80:A5:89:DD:EE:FF",
+ HOSTNAME: "roomba-blid",
+ },
+]
+
+
+DHCP_DISCOVERY_DEVICES_WITHOUT_MATCHING_IP = [
+ {
+ IP_ADDRESS: "4.4.4.4",
+ MAC_ADDRESS: "50:14:79:DD:EE:FF",
+ HOSTNAME: "irobot-blid",
+ },
+ {
+ IP_ADDRESS: "5.5.5.5",
+ MAC_ADDRESS: "80:A5:89:DD:EE:FF",
+ HOSTNAME: "roomba-blid",
+ },
+]
+
+
+@pytest.fixture(autouse=True)
+def roomba_no_wake_time():
+ """Fixture that prevents sleep."""
+ with patch.object(config_flow, "ROOMBA_WAKE_TIME", 0):
+ yield
def _create_mocked_roomba(
@@ -35,7 +66,7 @@ def _mocked_discovery(*_):
roomba_discovery = MagicMock()
roomba = RoombaInfo(
- hostname="iRobot-blid",
+ hostname="irobot-BLID",
robot_name="robot_name",
ip=MOCK_IP,
mac="mac",
@@ -45,12 +76,22 @@ def _mocked_discovery(*_):
)
roomba_discovery.get_all = MagicMock(return_value=[roomba])
+ roomba_discovery.get = MagicMock(return_value=roomba)
+
return roomba_discovery
-def _mocked_failed_discovery(*_):
+def _mocked_no_devices_found_discovery(*_):
roomba_discovery = MagicMock()
roomba_discovery.get_all = MagicMock(return_value=[])
+ roomba_discovery.get = MagicMock(return_value=None)
+ return roomba_discovery
+
+
+def _mocked_failed_discovery(*_):
+ roomba_discovery = MagicMock()
+ roomba_discovery.get_all = MagicMock(side_effect=OSError)
+ roomba_discovery.get = MagicMock(side_effect=OSError)
return roomba_discovery
@@ -122,9 +163,9 @@ async def test_form_user_discovery_and_password_fetch(hass):
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result3["title"] == "robot_name"
- assert result3["result"].unique_id == "blid"
+ assert result3["result"].unique_id == "BLID"
assert result3["data"] == {
- CONF_BLID: "blid",
+ CONF_BLID: "BLID",
CONF_CONTINUOUS: True,
CONF_DELAY: 1,
CONF_HOST: MOCK_IP,
@@ -138,7 +179,7 @@ async def test_form_user_discovery_skips_known(hass):
"""Test discovery proceeds to manual if all discovered are already known."""
await setup.async_setup_component(hass, "persistent_notification", {})
- entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, unique_id="blid")
+ entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, unique_id="BLID")
entry.add_to_hass(hass)
with patch(
@@ -154,16 +195,16 @@ async def test_form_user_discovery_skips_known(hass):
assert result["step_id"] == "manual"
-async def test_form_user_failed_discovery_aborts_already_configured(hass):
+async def test_form_user_no_devices_found_discovery_aborts_already_configured(hass):
"""Test if we manually configure an existing host we abort."""
await setup.async_setup_component(hass, "persistent_notification", {})
- entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, unique_id="blid")
+ entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, unique_id="BLID")
entry.add_to_hass(hass)
with patch(
"homeassistant.components.roomba.config_flow.RoombaDiscovery",
- _mocked_failed_discovery,
+ _mocked_no_devices_found_discovery,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -241,9 +282,9 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass):
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result4["title"] == "myroomba"
- assert result4["result"].unique_id == "blid"
+ assert result4["result"].unique_id == "BLID"
assert result4["data"] == {
- CONF_BLID: "blid",
+ CONF_BLID: "BLID",
CONF_CONTINUOUS: True,
CONF_DELAY: 1,
CONF_HOST: MOCK_IP,
@@ -253,6 +294,35 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass):
assert len(mock_setup_entry.mock_calls) == 1
+async def test_form_user_discover_fails_aborts_already_configured(hass):
+ """Test if we manually configure an existing host we abort after failed discovery."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, unique_id="BLID")
+ entry.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.roomba.config_flow.RoombaDiscovery",
+ _mocked_failed_discovery,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] is None
+ assert result["step_id"] == "manual"
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_HOST: MOCK_IP, CONF_BLID: "blid"},
+ )
+ await hass.async_block_till_done()
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result2["reason"] == "already_configured"
+
+
async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_connect(
hass,
):
@@ -318,8 +388,8 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con
assert len(mock_setup_entry.mock_calls) == 0
-async def test_form_user_discovery_fails_and_auto_password_fetch(hass):
- """Test discovery fails and we can auto fetch the password."""
+async def test_form_user_discovery_no_devices_found_and_auto_password_fetch(hass):
+ """Test discovery finds no devices and we can auto fetch the password."""
await setup.async_setup_component(hass, "persistent_notification", {})
mocked_roomba = _create_mocked_roomba(
@@ -329,7 +399,7 @@ async def test_form_user_discovery_fails_and_auto_password_fetch(hass):
with patch(
"homeassistant.components.roomba.config_flow.RoombaDiscovery",
- _mocked_failed_discovery,
+ _mocked_no_devices_found_discovery,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -368,9 +438,9 @@ async def test_form_user_discovery_fails_and_auto_password_fetch(hass):
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result3["title"] == "myroomba"
- assert result3["result"].unique_id == "blid"
+ assert result3["result"].unique_id == "BLID"
assert result3["data"] == {
- CONF_BLID: "blid",
+ CONF_BLID: "BLID",
CONF_CONTINUOUS: True,
CONF_DELAY: 1,
CONF_HOST: MOCK_IP,
@@ -380,8 +450,8 @@ async def test_form_user_discovery_fails_and_auto_password_fetch(hass):
assert len(mock_setup_entry.mock_calls) == 1
-async def test_form_user_discovery_fails_and_password_fetch_fails(hass):
- """Test discovery fails and password fetch fails."""
+async def test_form_user_discovery_no_devices_found_and_password_fetch_fails(hass):
+ """Test discovery finds no devices and password fetch fails."""
await setup.async_setup_component(hass, "persistent_notification", {})
mocked_roomba = _create_mocked_roomba(
@@ -391,7 +461,7 @@ async def test_form_user_discovery_fails_and_password_fetch_fails(hass):
with patch(
"homeassistant.components.roomba.config_flow.RoombaDiscovery",
- _mocked_failed_discovery,
+ _mocked_no_devices_found_discovery,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -437,9 +507,9 @@ async def test_form_user_discovery_fails_and_password_fetch_fails(hass):
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result4["title"] == "myroomba"
- assert result4["result"].unique_id == "blid"
+ assert result4["result"].unique_id == "BLID"
assert result4["data"] == {
- CONF_BLID: "blid",
+ CONF_BLID: "BLID",
CONF_CONTINUOUS: True,
CONF_DELAY: 1,
CONF_HOST: MOCK_IP,
@@ -449,10 +519,10 @@ async def test_form_user_discovery_fails_and_password_fetch_fails(hass):
assert len(mock_setup_entry.mock_calls) == 1
-async def test_form_user_discovery_fails_and_password_fetch_fails_and_cannot_connect(
+async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_and_cannot_connect(
hass,
):
- """Test discovery fails and password fetch fails then we cannot connect."""
+ """Test discovery finds no devices and password fetch fails then we cannot connect."""
await setup.async_setup_component(hass, "persistent_notification", {})
mocked_roomba = _create_mocked_roomba(
@@ -463,7 +533,7 @@ async def test_form_user_discovery_fails_and_password_fetch_fails_and_cannot_con
with patch(
"homeassistant.components.roomba.config_flow.RoombaDiscovery",
- _mocked_failed_discovery,
+ _mocked_no_devices_found_discovery,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -570,9 +640,9 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused(ha
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result4["title"] == "myroomba"
- assert result4["result"].unique_id == "blid"
+ assert result4["result"].unique_id == "BLID"
assert result4["data"] == {
- CONF_BLID: "blid",
+ CONF_BLID: "BLID",
CONF_CONTINUOUS: True,
CONF_DELAY: 1,
CONF_HOST: MOCK_IP,
@@ -582,7 +652,8 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused(ha
assert len(mock_setup_entry.mock_calls) == 1
-async def test_dhcp_discovery_and_roomba_discovery_finds(hass):
+@pytest.mark.parametrize("discovery_data", DHCP_DISCOVERY_DEVICES)
+async def test_dhcp_discovery_and_roomba_discovery_finds(hass, discovery_data):
"""Test we can process the discovery from dhcp and roomba discovery matches the device."""
await setup.async_setup_component(hass, "persistent_notification", {})
@@ -597,11 +668,7 @@ async def test_dhcp_discovery_and_roomba_discovery_finds(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
- data={
- IP_ADDRESS: MOCK_IP,
- MAC_ADDRESS: "AA:BB:CC:DD:EE:FF",
- HOSTNAME: "iRobot-blid",
- },
+ data=discovery_data,
)
await hass.async_block_till_done()
@@ -630,9 +697,9 @@ async def test_dhcp_discovery_and_roomba_discovery_finds(hass):
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "robot_name"
- assert result2["result"].unique_id == "blid"
+ assert result2["result"].unique_id == "BLID"
assert result2["data"] == {
- CONF_BLID: "blid",
+ CONF_BLID: "BLID",
CONF_CONTINUOUS: True,
CONF_DELAY: 1,
CONF_HOST: MOCK_IP,
@@ -642,7 +709,8 @@ async def test_dhcp_discovery_and_roomba_discovery_finds(hass):
assert len(mock_setup_entry.mock_calls) == 1
-async def test_dhcp_discovery_falls_back_to_manual(hass):
+@pytest.mark.parametrize("discovery_data", DHCP_DISCOVERY_DEVICES_WITHOUT_MATCHING_IP)
+async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data):
"""Test we can process the discovery from dhcp but roomba discovery cannot find the device."""
await setup.async_setup_component(hass, "persistent_notification", {})
@@ -657,11 +725,7 @@ async def test_dhcp_discovery_falls_back_to_manual(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
- data={
- IP_ADDRESS: "1.1.1.1",
- MAC_ADDRESS: "AA:BB:CC:DD:EE:FF",
- HOSTNAME: "iRobot-blid",
- },
+ data=discovery_data,
)
await hass.async_block_till_done()
@@ -680,7 +744,7 @@ async def test_dhcp_discovery_falls_back_to_manual(hass):
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
- {CONF_HOST: "1.1.1.1", CONF_BLID: "blid"},
+ {CONF_HOST: MOCK_IP, CONF_BLID: "blid"},
)
await hass.async_block_till_done()
assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -706,12 +770,12 @@ async def test_dhcp_discovery_falls_back_to_manual(hass):
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result4["title"] == "myroomba"
- assert result4["result"].unique_id == "blid"
+ assert result4["result"].unique_id == "BLID"
assert result4["data"] == {
- CONF_BLID: "blid",
+ CONF_BLID: "BLID",
CONF_CONTINUOUS: True,
CONF_DELAY: 1,
- CONF_HOST: "1.1.1.1",
+ CONF_HOST: MOCK_IP,
CONF_PASSWORD: "password",
}
assert len(mock_setup.mock_calls) == 1
@@ -732,9 +796,9 @@ async def test_dhcp_discovery_with_ignored(hass):
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data={
- IP_ADDRESS: "1.1.1.1",
+ IP_ADDRESS: MOCK_IP,
MAC_ADDRESS: "AA:BB:CC:DD:EE:FF",
- HOSTNAME: "iRobot-blid",
+ HOSTNAME: "irobot-blid",
},
)
await hass.async_block_till_done()
@@ -746,7 +810,7 @@ async def test_dhcp_discovery_already_configured_host(hass):
"""Test we abort if the host is already configured."""
await setup.async_setup_component(hass, "persistent_notification", {})
- config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "1.1.1.1"})
+ config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: MOCK_IP})
config_entry.add_to_hass(hass)
with patch(
@@ -756,9 +820,9 @@ async def test_dhcp_discovery_already_configured_host(hass):
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data={
- IP_ADDRESS: "1.1.1.1",
+ IP_ADDRESS: MOCK_IP,
MAC_ADDRESS: "AA:BB:CC:DD:EE:FF",
- HOSTNAME: "iRobot-blid",
+ HOSTNAME: "irobot-blid",
},
)
await hass.async_block_till_done()
@@ -772,7 +836,7 @@ async def test_dhcp_discovery_already_configured_blid(hass):
await setup.async_setup_component(hass, "persistent_notification", {})
config_entry = MockConfigEntry(
- domain=DOMAIN, data={CONF_BLID: "blid"}, unique_id="blid"
+ domain=DOMAIN, data={CONF_BLID: "BLID"}, unique_id="BLID"
)
config_entry.add_to_hass(hass)
@@ -783,9 +847,9 @@ async def test_dhcp_discovery_already_configured_blid(hass):
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data={
- IP_ADDRESS: "1.1.1.1",
+ IP_ADDRESS: MOCK_IP,
MAC_ADDRESS: "AA:BB:CC:DD:EE:FF",
- HOSTNAME: "iRobot-blid",
+ HOSTNAME: "irobot-blid",
},
)
await hass.async_block_till_done()
@@ -799,7 +863,7 @@ async def test_dhcp_discovery_not_irobot(hass):
await setup.async_setup_component(hass, "persistent_notification", {})
config_entry = MockConfigEntry(
- domain=DOMAIN, data={CONF_BLID: "blid"}, unique_id="blid"
+ domain=DOMAIN, data={CONF_BLID: "BLID"}, unique_id="BLID"
)
config_entry.add_to_hass(hass)
@@ -810,12 +874,76 @@ async def test_dhcp_discovery_not_irobot(hass):
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data={
- IP_ADDRESS: "1.1.1.1",
+ IP_ADDRESS: MOCK_IP,
MAC_ADDRESS: "AA:BB:CC:DD:EE:FF",
- HOSTNAME: "NotiRobot-blid",
+ HOSTNAME: "Notirobot-blid",
},
)
await hass.async_block_till_done()
assert result["type"] == "abort"
assert result["reason"] == "not_irobot_device"
+
+
+async def test_dhcp_discovery_partial_hostname(hass):
+ """Test we abort flows when we have a partial hostname."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ with patch(
+ "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_DHCP},
+ data={
+ IP_ADDRESS: MOCK_IP,
+ MAC_ADDRESS: "AA:BB:CC:DD:EE:FF",
+ HOSTNAME: "irobot-blid",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "link"
+
+ with patch(
+ "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery
+ ):
+ result2 = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_DHCP},
+ data={
+ IP_ADDRESS: MOCK_IP,
+ MAC_ADDRESS: "AA:BB:CC:DD:EE:FF",
+ HOSTNAME: "irobot-blidthatislonger",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "form"
+ assert result2["step_id"] == "link"
+
+ current_flows = hass.config_entries.flow.async_progress()
+ assert len(current_flows) == 1
+ assert current_flows[0]["flow_id"] == result2["flow_id"]
+
+ with patch(
+ "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery
+ ):
+ result3 = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_DHCP},
+ data={
+ IP_ADDRESS: MOCK_IP,
+ MAC_ADDRESS: "AA:BB:CC:DD:EE:FF",
+ HOSTNAME: "irobot-bl",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result3["type"] == "abort"
+ assert result3["reason"] == "short_blid"
+
+ current_flows = hass.config_entries.flow.async_progress()
+ assert len(current_flows) == 1
+ assert current_flows[0]["flow_id"] == result2["flow_id"]
diff --git a/tests/components/ruckus_unleashed/test_device_tracker.py b/tests/components/ruckus_unleashed/test_device_tracker.py
index 37bae441abf7f9..9238200727396e 100644
--- a/tests/components/ruckus_unleashed/test_device_tracker.py
+++ b/tests/components/ruckus_unleashed/test_device_tracker.py
@@ -5,7 +5,7 @@
from homeassistant.components.ruckus_unleashed import API_MAC, DOMAIN
from homeassistant.components.ruckus_unleashed.const import API_AP, API_ID, API_NAME
from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.util import utcnow
@@ -80,7 +80,7 @@ async def test_restoring_clients(hass):
entry = mock_config_entry()
entry.add_to_hass(hass)
- registry = await entity_registry.async_get_registry(hass)
+ registry = er.async_get(hass)
registry.async_get_or_create(
"device_tracker",
DOMAIN,
@@ -120,7 +120,7 @@ async def test_client_device_setup(hass):
router_info = DEFAULT_AP_INFO[API_AP][API_ID]["1"]
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
client_device = device_registry.async_get_device(
identifiers={},
connections={(CONNECTION_NETWORK_MAC, TEST_CLIENT[API_MAC])},
diff --git a/tests/components/ruckus_unleashed/test_init.py b/tests/components/ruckus_unleashed/test_init.py
index 7b379a510113a8..0340f72891adea 100644
--- a/tests/components/ruckus_unleashed/test_init.py
+++ b/tests/components/ruckus_unleashed/test_init.py
@@ -19,6 +19,7 @@
ENTRY_STATE_NOT_LOADED,
ENTRY_STATE_SETUP_RETRY,
)
+from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from tests.components.ruckus_unleashed import (
@@ -64,7 +65,7 @@ async def test_router_device_setup(hass):
device_info = DEFAULT_AP_INFO[API_AP][API_ID]["1"]
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get_device(
identifiers={(CONNECTION_NETWORK_MAC, device_info[API_MAC])},
connections={(CONNECTION_NETWORK_MAC, device_info[API_MAC])},
diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py
index 44696e6f3706e2..a3d50d0214f39a 100644
--- a/tests/components/scene/test_init.py
+++ b/tests/components/scene/test_init.py
@@ -60,8 +60,8 @@ async def test_config_yaml_alias_anchor(hass, entities):
assert light.is_on(hass, light_1.entity_id)
assert light.is_on(hass, light_2.entity_id)
- assert 100 == light_1.last_call("turn_on")[1].get("brightness")
- assert 100 == light_2.last_call("turn_on")[1].get("brightness")
+ assert light_1.last_call("turn_on")[1].get("brightness") == 100
+ assert light_2.last_call("turn_on")[1].get("brightness") == 100
async def test_config_yaml_bool(hass, entities):
@@ -88,7 +88,7 @@ async def test_config_yaml_bool(hass, entities):
assert light.is_on(hass, light_1.entity_id)
assert light.is_on(hass, light_2.entity_id)
- assert 100 == light_2.last_call("turn_on")[1].get("brightness")
+ assert light_2.last_call("turn_on")[1].get("brightness") == 100
async def test_activate_scene(hass, entities):
diff --git a/tests/components/screenlogic/__init__.py b/tests/components/screenlogic/__init__.py
new file mode 100644
index 00000000000000..ad2b82960f0662
--- /dev/null
+++ b/tests/components/screenlogic/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Screenlogic integration."""
diff --git a/tests/components/screenlogic/test_config_flow.py b/tests/components/screenlogic/test_config_flow.py
new file mode 100644
index 00000000000000..71dc4935001508
--- /dev/null
+++ b/tests/components/screenlogic/test_config_flow.py
@@ -0,0 +1,344 @@
+"""Test the Pentair ScreenLogic config flow."""
+from unittest.mock import patch
+
+from screenlogicpy import ScreenLogicError
+from screenlogicpy.const import (
+ SL_GATEWAY_IP,
+ SL_GATEWAY_NAME,
+ SL_GATEWAY_PORT,
+ SL_GATEWAY_SUBTYPE,
+ SL_GATEWAY_TYPE,
+)
+
+from homeassistant import config_entries, setup
+from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS
+from homeassistant.components.screenlogic.config_flow import (
+ GATEWAY_MANUAL_ENTRY,
+ GATEWAY_SELECT_KEY,
+)
+from homeassistant.components.screenlogic.const import (
+ DEFAULT_SCAN_INTERVAL,
+ DOMAIN,
+ MIN_SCAN_INTERVAL,
+)
+from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL
+
+from tests.common import MockConfigEntry
+
+
+async def test_flow_discovery(hass):
+ """Test the flow works with basic discovery."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch(
+ "homeassistant.components.screenlogic.config_flow.discover",
+ return_value=[
+ {
+ SL_GATEWAY_IP: "1.1.1.1",
+ SL_GATEWAY_PORT: 80,
+ SL_GATEWAY_TYPE: 12,
+ SL_GATEWAY_SUBTYPE: 2,
+ SL_GATEWAY_NAME: "Pentair: 01-01-01",
+ },
+ ],
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+ assert result["step_id"] == "gateway_select"
+
+ with patch(
+ "homeassistant.components.screenlogic.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.screenlogic.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={GATEWAY_SELECT_KEY: "00:c0:33:01:01:01"}
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Pentair: 01-01-01"
+ assert result2["data"] == {
+ CONF_IP_ADDRESS: "1.1.1.1",
+ CONF_PORT: 80,
+ }
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_flow_discover_none(hass):
+ """Test when nothing is discovered."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch(
+ "homeassistant.components.screenlogic.config_flow.discover",
+ return_value=[],
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+ assert result["step_id"] == "gateway_entry"
+
+
+async def test_flow_discover_error(hass):
+ """Test when discovery errors."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch(
+ "homeassistant.components.screenlogic.config_flow.discover",
+ side_effect=ScreenLogicError("Fake error"),
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+ assert result["step_id"] == "gateway_entry"
+
+ with patch(
+ "homeassistant.components.screenlogic.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.screenlogic.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry, patch(
+ "homeassistant.components.screenlogic.config_flow.login.create_socket",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.screenlogic.config_flow.login.gateway_connect",
+ return_value="00-C0-33-01-01-01",
+ ):
+ result3 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_IP_ADDRESS: "1.1.1.1",
+ CONF_PORT: 80,
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result3["type"] == "create_entry"
+ assert result3["title"] == "Pentair: 01-01-01"
+ assert result3["data"] == {
+ CONF_IP_ADDRESS: "1.1.1.1",
+ CONF_PORT: 80,
+ }
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_dhcp(hass):
+ """Test DHCP discovery flow."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "dhcp"},
+ data={
+ HOSTNAME: "Pentair: 01-01-01",
+ IP_ADDRESS: "1.1.1.1",
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "gateway_entry"
+
+ with patch(
+ "homeassistant.components.screenlogic.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.screenlogic.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry, patch(
+ "homeassistant.components.screenlogic.config_flow.login.create_socket",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.screenlogic.config_flow.login.gateway_connect",
+ return_value="00-C0-33-01-01-01",
+ ):
+ result3 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_IP_ADDRESS: "1.1.1.1",
+ CONF_PORT: 80,
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result3["type"] == "create_entry"
+ assert result3["title"] == "Pentair: 01-01-01"
+ assert result3["data"] == {
+ CONF_IP_ADDRESS: "1.1.1.1",
+ CONF_PORT: 80,
+ }
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_manual_entry(hass):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ with patch(
+ "homeassistant.components.screenlogic.config_flow.discover",
+ return_value=[
+ {
+ SL_GATEWAY_IP: "1.1.1.1",
+ SL_GATEWAY_PORT: 80,
+ SL_GATEWAY_TYPE: 12,
+ SL_GATEWAY_SUBTYPE: 2,
+ SL_GATEWAY_NAME: "Pentair: 01-01-01",
+ },
+ ],
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+ assert result["step_id"] == "gateway_select"
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={GATEWAY_SELECT_KEY: GATEWAY_MANUAL_ENTRY}
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {}
+ assert result2["step_id"] == "gateway_entry"
+
+ with patch(
+ "homeassistant.components.screenlogic.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.screenlogic.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry, patch(
+ "homeassistant.components.screenlogic.config_flow.login.create_socket",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.screenlogic.config_flow.login.gateway_connect",
+ return_value="00-C0-33-01-01-01",
+ ):
+ result3 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_IP_ADDRESS: "1.1.1.1",
+ CONF_PORT: 80,
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result3["type"] == "create_entry"
+ assert result3["title"] == "Pentair: 01-01-01"
+ assert result3["data"] == {
+ CONF_IP_ADDRESS: "1.1.1.1",
+ CONF_PORT: 80,
+ }
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_cannot_connect(hass):
+ """Test we handle cannot connect error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.screenlogic.config_flow.login.create_socket",
+ return_value=None,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_IP_ADDRESS: "1.1.1.1",
+ CONF_PORT: 80,
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {CONF_IP_ADDRESS: "cannot_connect"}
+
+
+async def test_option_flow(hass):
+ """Test config flow options."""
+ entry = MockConfigEntry(domain=DOMAIN)
+ entry.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.screenlogic.async_setup", return_value=True
+ ), patch(
+ "homeassistant.components.screenlogic.async_setup_entry",
+ return_value=True,
+ ):
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={CONF_SCAN_INTERVAL: 15},
+ )
+ assert result["type"] == "create_entry"
+ assert result["data"] == {CONF_SCAN_INTERVAL: 15}
+
+
+async def test_option_flow_defaults(hass):
+ """Test config flow options."""
+ entry = MockConfigEntry(domain=DOMAIN)
+ entry.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.screenlogic.async_setup", return_value=True
+ ), patch(
+ "homeassistant.components.screenlogic.async_setup_entry",
+ return_value=True,
+ ):
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={}
+ )
+ assert result["type"] == "create_entry"
+ assert result["data"] == {
+ CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
+ }
+
+
+async def test_option_flow_input_floor(hass):
+ """Test config flow options."""
+ entry = MockConfigEntry(domain=DOMAIN)
+ entry.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.screenlogic.async_setup", return_value=True
+ ), patch(
+ "homeassistant.components.screenlogic.async_setup_entry",
+ return_value=True,
+ ):
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={CONF_SCAN_INTERVAL: 1}
+ )
+ assert result["type"] == "create_entry"
+ assert result["data"] == {
+ CONF_SCAN_INTERVAL: MIN_SCAN_INTERVAL,
+ }
diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py
index 95703949525a1d..cf2c632ee1662e 100644
--- a/tests/components/script/test_init.py
+++ b/tests/components/script/test_init.py
@@ -173,7 +173,7 @@ def state_listener(entity_id, old_state, new_state):
assert not script.is_on(hass, ENTITY_ID)
assert was_on
- assert 1 == event_mock.call_count
+ assert event_mock.call_count == 1
invalid_configs = [
@@ -190,7 +190,7 @@ async def test_setup_with_invalid_configs(hass, value):
hass, "script", {"script": value}
), f"Script loaded with wrong config {value}"
- assert 0 == len(hass.states.async_entity_ids("script"))
+ assert len(hass.states.async_entity_ids("script")) == 0
@pytest.mark.parametrize("running", ["no", "same", "different"])
@@ -269,6 +269,7 @@ async def test_service_descriptions(hass):
descriptions = await async_get_all_descriptions(hass)
+ assert descriptions[DOMAIN]["test"]["name"] == "test"
assert descriptions[DOMAIN]["test"]["description"] == "test description"
assert not descriptions[DOMAIN]["test"]["fields"]
@@ -303,6 +304,27 @@ async def test_service_descriptions(hass):
== "test_param example"
)
+ # Test 3: has "alias" that will be used as "name"
+ with patch(
+ "homeassistant.config.load_yaml_config_file",
+ return_value={
+ "script": {
+ "test_name": {
+ "alias": "ABC",
+ "sequence": [{"delay": {"seconds": 5}}],
+ }
+ }
+ },
+ ):
+ await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True)
+
+ descriptions = await async_get_all_descriptions(hass)
+
+ assert descriptions[DOMAIN]["test_name"]["name"] == "ABC"
+
+ # Test 4: verify that names from YAML are taken into account as well
+ assert descriptions[DOMAIN]["turn_on"]["name"] == "Turn on"
+
async def test_shared_context(hass):
"""Test that the shared context is passed down the chain."""
@@ -587,7 +609,7 @@ async def async_service_handler(service):
await asyncio.wait_for(service_called.wait(), 1)
service_called.clear()
- assert "script2a" == service_values[-1]
+ assert service_values[-1] == "script2a"
assert script.is_on(hass, "script.script1")
assert script.is_on(hass, "script.script2")
@@ -596,13 +618,13 @@ async def async_service_handler(service):
await asyncio.wait_for(service_called.wait(), 1)
service_called.clear()
- assert "script2b" == service_values[-1]
+ assert service_values[-1] == "script2b"
hass.states.async_set("input_boolean.test1", "on")
await asyncio.wait_for(service_called.wait(), 1)
service_called.clear()
- assert "script1" == service_values[-1]
+ assert service_values[-1] == "script1"
assert concurrently == script.is_on(hass, "script.script2")
if concurrently:
@@ -610,7 +632,7 @@ async def async_service_handler(service):
await asyncio.wait_for(service_called.wait(), 1)
service_called.clear()
- assert "script2b" == service_values[-1]
+ assert service_values[-1] == "script2b"
await hass.async_block_till_done()
diff --git a/tests/components/search/test_init.py b/tests/components/search/test_init.py
index 5710fa04698500..82935f2b41f9b3 100644
--- a/tests/components/search/test_init.py
+++ b/tests/components/search/test_init.py
@@ -1,16 +1,21 @@
"""Tests for Search integration."""
from homeassistant.components import search
+from homeassistant.helpers import (
+ area_registry as ar,
+ device_registry as dr,
+ entity_registry as er,
+)
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
async def test_search(hass):
"""Test that search works."""
- area_reg = await hass.helpers.area_registry.async_get_registry()
- device_reg = await hass.helpers.device_registry.async_get_registry()
- entity_reg = await hass.helpers.entity_registry.async_get_registry()
+ area_reg = ar.async_get(hass)
+ device_reg = dr.async_get(hass)
+ entity_reg = er.async_get(hass)
living_room_area = area_reg.async_create("Living Room")
@@ -188,6 +193,10 @@ async def test_search(hass):
},
)
+ # Ensure automations set up correctly.
+ assert hass.states.get("automation.wled_entity") is not None
+ assert hass.states.get("automation.wled_device") is not None
+
# Explore the graph from every node and make sure we find the same results
expected = {
"config_entry": {wled_config_entry.entry_id},
@@ -271,12 +280,70 @@ async def test_search(hass):
assert searcher.async_search(search_type, search_id) == {}
+async def test_area_lookup(hass):
+ """Test area based lookup."""
+ area_reg = ar.async_get(hass)
+ device_reg = dr.async_get(hass)
+ entity_reg = er.async_get(hass)
+
+ living_room_area = area_reg.async_create("Living Room")
+
+ await async_setup_component(
+ hass,
+ "script",
+ {
+ "script": {
+ "wled": {
+ "sequence": [
+ {
+ "service": "light.turn_on",
+ "target": {"area_id": living_room_area.id},
+ },
+ ]
+ },
+ }
+ },
+ )
+
+ assert await async_setup_component(
+ hass,
+ "automation",
+ {
+ "automation": [
+ {
+ "alias": "area_turn_on",
+ "trigger": {"platform": "template", "value_template": "true"},
+ "action": [
+ {
+ "service": "light.turn_on",
+ "data": {
+ "area_id": living_room_area.id,
+ },
+ },
+ ],
+ },
+ ]
+ },
+ )
+
+ searcher = search.Searcher(hass, device_reg, entity_reg)
+ assert searcher.async_search("area", living_room_area.id) == {
+ "script": {"script.wled"},
+ "automation": {"automation.area_turn_on"},
+ }
+
+ searcher = search.Searcher(hass, device_reg, entity_reg)
+ assert searcher.async_search("automation", "automation.area_turn_on") == {
+ "area": {living_room_area.id},
+ }
+
+
async def test_ws_api(hass, hass_ws_client):
"""Test WS API."""
assert await async_setup_component(hass, "search", {})
- area_reg = await hass.helpers.area_registry.async_get_registry()
- device_reg = await hass.helpers.device_registry.async_get_registry()
+ area_reg = ar.async_get(hass)
+ device_reg = dr.async_get(hass)
kitchen_area = area_reg.async_create("Kitchen")
diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py
index 9a023d6f5ade82..2de95d44eb1469 100644
--- a/tests/components/sensor/test_device_condition.py
+++ b/tests/components/sensor/test_device_condition.py
@@ -16,8 +16,11 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
-from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
+from tests.testing_config.custom_components.test.sensor import (
+ DEVICE_CLASSES,
+ UNITS_OF_MEASUREMENT,
+)
@pytest.fixture
@@ -69,6 +72,7 @@ async def test_get_conditions(hass, device_reg, entity_reg):
"entity_id": platform.ENTITIES[device_class].entity_id,
}
for device_class in DEVICE_CLASSES
+ if device_class in UNITS_OF_MEASUREMENT
for condition in ENTITY_CONDITIONS[device_class]
if device_class != "none"
]
diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py
index c39b4597632b71..4c65eff34abe18 100644
--- a/tests/components/sensor/test_device_trigger.py
+++ b/tests/components/sensor/test_device_trigger.py
@@ -20,8 +20,11 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
-from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
+from tests.testing_config.custom_components.test.sensor import (
+ DEVICE_CLASSES,
+ UNITS_OF_MEASUREMENT,
+)
@pytest.fixture
@@ -73,11 +76,12 @@ async def test_get_triggers(hass, device_reg, entity_reg):
"entity_id": platform.ENTITIES[device_class].entity_id,
}
for device_class in DEVICE_CLASSES
+ if device_class in UNITS_OF_MEASUREMENT
for trigger in ENTITY_TRIGGERS[device_class]
if device_class != "none"
]
triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
- assert len(triggers) == 12
+ assert len(triggers) == 13
assert triggers == expected_triggers
@@ -428,6 +432,7 @@ async def test_if_fires_on_state_change_with_for(hass, calls):
assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN
assert len(calls) == 0
+ hass.states.async_set(sensor1.entity_id, 10)
hass.states.async_set(sensor1.entity_id, 11)
await hass.async_block_till_done()
assert len(calls) == 0
@@ -437,5 +442,5 @@ async def test_if_fires_on_state_change_with_for(hass, calls):
await hass.async_block_till_done()
assert (
calls[0].data["some"]
- == f"turn_off device - {sensor1.entity_id} - unknown - 11 - 0:00:05"
+ == f"turn_off device - {sensor1.entity_id} - 10 - 11 - 0:00:05"
)
diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py
index 6519b435c0ad48..5ad904530b9d3b 100644
--- a/tests/components/seventeentrack/test_sensor.py
+++ b/tests/components/seventeentrack/test_sensor.py
@@ -1,6 +1,7 @@
"""Tests for the seventeentrack sensor."""
+from __future__ import annotations
+
import datetime
-from typing import Union
from unittest.mock import MagicMock, patch
from py17track.package import Package
@@ -70,7 +71,7 @@
class ClientMock:
"""Mock the py17track client to inject the ProfileMock."""
- def __init__(self, websession) -> None:
+ def __init__(self, session) -> None:
"""Mock the profile."""
self.profile = ProfileMock()
@@ -100,9 +101,12 @@ async def login(self, email: str, password: str) -> bool:
return self.__class__.login_result
async def packages(
- self, package_state: Union[int, str] = "", show_archived: bool = False
+ self,
+ package_state: int | str = "",
+ show_archived: bool = False,
+ tz: str = "UTC",
) -> list:
- """Packages mock."""
+ """Packages mock.""" # noqa: D401
return self.__class__.package_list[:]
async def summary(self, show_archived: bool = False) -> dict:
@@ -168,7 +172,14 @@ async def test_invalid_config(hass):
async def test_add_package(hass):
"""Ensure package is added correctly when user add a new package."""
package = Package(
- "456", 206, "friendly name 1", "info text 1", "location 1", 206, 2
+ "456",
+ 206,
+ "friendly name 1",
+ "info text 1",
+ "location 1",
+ "2020-08-10 10:32",
+ 206,
+ 2,
)
ProfileMock.package_list = [package]
@@ -177,7 +188,14 @@ async def test_add_package(hass):
assert len(hass.states.async_entity_ids()) == 1
package2 = Package(
- "789", 206, "friendly name 2", "info text 2", "location 2", 206, 2
+ "789",
+ 206,
+ "friendly name 2",
+ "info text 2",
+ "location 2",
+ "2020-08-10 14:25",
+ 206,
+ 2,
)
ProfileMock.package_list = [package, package2]
@@ -190,10 +208,24 @@ async def test_add_package(hass):
async def test_remove_package(hass):
"""Ensure entity is not there anymore if package is not there."""
package1 = Package(
- "456", 206, "friendly name 1", "info text 1", "location 1", 206, 2
+ "456",
+ 206,
+ "friendly name 1",
+ "info text 1",
+ "location 1",
+ "2020-08-10 10:32",
+ 206,
+ 2,
)
package2 = Package(
- "789", 206, "friendly name 2", "info text 2", "location 2", 206, 2
+ "789",
+ 206,
+ "friendly name 2",
+ "info text 2",
+ "location 2",
+ "2020-08-10 14:25",
+ 206,
+ 2,
)
ProfileMock.package_list = [package1, package2]
@@ -216,7 +248,14 @@ async def test_remove_package(hass):
async def test_friendly_name_changed(hass):
"""Test friendly name change."""
package = Package(
- "456", 206, "friendly name 1", "info text 1", "location 1", 206, 2
+ "456",
+ 206,
+ "friendly name 1",
+ "info text 1",
+ "location 1",
+ "2020-08-10 10:32",
+ 206,
+ 2,
)
ProfileMock.package_list = [package]
@@ -226,7 +265,14 @@ async def test_friendly_name_changed(hass):
assert len(hass.states.async_entity_ids()) == 1
package = Package(
- "456", 206, "friendly name 2", "info text 1", "location 1", 206, 2
+ "456",
+ 206,
+ "friendly name 2",
+ "info text 1",
+ "location 1",
+ "2020-08-10 10:32",
+ 206,
+ 2,
)
ProfileMock.package_list = [package]
@@ -243,7 +289,15 @@ async def test_friendly_name_changed(hass):
async def test_delivered_not_shown(hass):
"""Ensure delivered packages are not shown."""
package = Package(
- "456", 206, "friendly name 1", "info text 1", "location 1", 206, 2, 40
+ "456",
+ 206,
+ "friendly name 1",
+ "info text 1",
+ "location 1",
+ "2020-08-10 10:32",
+ 206,
+ 2,
+ 40,
)
ProfileMock.package_list = [package]
@@ -258,7 +312,15 @@ async def test_delivered_not_shown(hass):
async def test_delivered_shown(hass):
"""Ensure delivered packages are show when user choose to show them."""
package = Package(
- "456", 206, "friendly name 1", "info text 1", "location 1", 206, 2, 40
+ "456",
+ 206,
+ "friendly name 1",
+ "info text 1",
+ "location 1",
+ "2020-08-10 10:32",
+ 206,
+ 2,
+ 40,
)
ProfileMock.package_list = [package]
@@ -273,7 +335,14 @@ async def test_delivered_shown(hass):
async def test_becomes_delivered_not_shown_notification(hass):
"""Ensure notification is triggered when package becomes delivered."""
package = Package(
- "456", 206, "friendly name 1", "info text 1", "location 1", 206, 2
+ "456",
+ 206,
+ "friendly name 1",
+ "info text 1",
+ "location 1",
+ "2020-08-10 10:32",
+ 206,
+ 2,
)
ProfileMock.package_list = [package]
@@ -283,7 +352,15 @@ async def test_becomes_delivered_not_shown_notification(hass):
assert len(hass.states.async_entity_ids()) == 1
package_delivered = Package(
- "456", 206, "friendly name 1", "info text 1", "location 1", 206, 2, 40
+ "456",
+ 206,
+ "friendly name 1",
+ "info text 1",
+ "location 1",
+ "2020-08-10 10:32",
+ 206,
+ 2,
+ 40,
)
ProfileMock.package_list = [package_delivered]
@@ -309,3 +386,31 @@ async def test_summary_correctly_updated(hass):
assert len(hass.states.async_entity_ids()) == 7
for state in hass.states.async_all():
assert state.state == "1"
+
+
+async def test_utc_timestamp(hass):
+ """Ensure package timestamp is converted correctly from HA-defined time zone to UTC."""
+ package = Package(
+ "456",
+ 206,
+ "friendly name 1",
+ "info text 1",
+ "location 1",
+ "2020-08-10 10:32",
+ 206,
+ 2,
+ tz="Asia/Jakarta",
+ )
+ ProfileMock.package_list = [package]
+
+ await _setup_seventeentrack(hass)
+ assert hass.states.get("sensor.seventeentrack_package_456") is not None
+ assert len(hass.states.async_entity_ids()) == 1
+ assert (
+ str(
+ hass.states.get("sensor.seventeentrack_package_456").attributes.get(
+ "timestamp"
+ )
+ )
+ == "2020-08-10 03:32:00+00:00"
+ )
diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py
index edce5d75b2cd04..b36359ed31aceb 100644
--- a/tests/components/sharkiq/test_vacuum.py
+++ b/tests/components/sharkiq/test_vacuum.py
@@ -1,7 +1,9 @@
"""Test the Shark IQ vacuum entity."""
+from __future__ import annotations
+
from copy import deepcopy
import enum
-from typing import Any, Iterable, List, Optional
+from typing import Any, Iterable
from unittest.mock import patch
import pytest
@@ -46,6 +48,7 @@
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from .const import (
@@ -79,11 +82,11 @@ class MockAyla(AylaApi):
async def async_sign_in(self):
"""Instead of signing in, just return."""
- async def async_list_devices(self) -> List[dict]:
+ async def async_list_devices(self) -> list[dict]:
"""Return the device list."""
return [SHARK_DEVICE_DICT]
- async def async_get_devices(self, update: bool = True) -> List[SharkIqVacuum]:
+ async def async_get_devices(self, update: bool = True) -> list[SharkIqVacuum]:
"""Get the list of devices."""
shark = MockShark(self, SHARK_DEVICE_DICT)
shark.properties_full = deepcopy(SHARK_PROPERTIES_DICT)
@@ -97,7 +100,7 @@ async def async_request(self, http_method: str, url: str, **kwargs):
class MockShark(SharkIqVacuum):
"""Mocked SharkIqVacuum that won't hit the API."""
- async def async_update(self, property_list: Optional[Iterable[str]] = None):
+ async def async_update(self, property_list: Iterable[str] | None = None):
"""Don't do anything."""
def set_property_value(self, property_name, value):
@@ -128,7 +131,7 @@ async def setup_integration(hass):
async def test_simple_properties(hass: HomeAssistant):
"""Test that simple properties work as intended."""
state = hass.states.get(VAC_ENTITY_ID)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entity = registry.async_get(VAC_ENTITY_ID)
assert entity
@@ -199,7 +202,7 @@ async def test_device_properties(
hass: HomeAssistant, device_property: str, target_value: str
):
"""Test device properties."""
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
device = registry.async_get_device({(DOMAIN, "AC000Wxxxxxxxxx")})
assert getattr(device, device_property) == target_value
@@ -223,7 +226,7 @@ async def test_locate(hass):
)
@patch("sharkiqpy.ayla_api.AylaApi", MockAyla)
async def test_coordinator_updates(
- hass: HomeAssistant, side_effect: Optional[Exception], success: bool
+ hass: HomeAssistant, side_effect: Exception | None, success: bool
) -> None:
"""Test the update coordinator update functions."""
coordinator = hass.data[DOMAIN][ENTRY_ID]
diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py
index 928c186bc1147b..f3a5d46f64bd0c 100644
--- a/tests/components/shell_command/test_init.py
+++ b/tests/components/shell_command/test_init.py
@@ -1,8 +1,8 @@
"""The tests for the Shell command component."""
+from __future__ import annotations
import os
import tempfile
-from typing import Tuple
from unittest.mock import MagicMock, patch
from homeassistant.components import shell_command
@@ -12,7 +12,7 @@
def mock_process_creator(error: bool = False):
"""Mock a coroutine that creates a process when yielded."""
- async def communicate() -> Tuple[bytes, bytes]:
+ async def communicate() -> tuple[bytes, bytes]:
"""Mock a coroutine that runs a process when yielded.
Returns a tuple of (stdout, stderr).
@@ -79,7 +79,7 @@ async def test_template_render_no_template(mock_call, hass):
cmd = mock_call.mock_calls[0][1][0]
assert mock_call.call_count == 1
- assert "ls /bin" == cmd
+ assert cmd == "ls /bin"
@patch(
diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py
index 804d5a75952bfb..51659cf77361d8 100644
--- a/tests/components/shelly/conftest.py
+++ b/tests/components/shelly/conftest.py
@@ -10,10 +10,14 @@
DOMAIN,
EVENT_SHELLY_CLICK,
)
-from homeassistant.core import callback as ha_callback
from homeassistant.setup import async_setup_component
-from tests.common import MockConfigEntry, async_mock_service, mock_device_registry
+from tests.common import (
+ MockConfigEntry,
+ async_capture_events,
+ async_mock_service,
+ mock_device_registry,
+)
MOCK_SETTINGS = {
"name": "Test name",
@@ -81,9 +85,7 @@ def calls(hass):
@pytest.fixture
def events(hass):
"""Yield caught shelly_click events."""
- ha_events = []
- hass.bus.async_listen(EVENT_SHELLY_CLICK, ha_callback(ha_events.append))
- yield ha_events
+ return async_capture_events(hass, EVENT_SHELLY_CLICK)
@pytest.fixture
@@ -91,7 +93,11 @@ async def coap_wrapper(hass):
"""Setups a coap wrapper with mocked device."""
await async_setup_component(hass, "shelly", {})
- config_entry = MockConfigEntry(domain=DOMAIN, data={})
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={"sleep_period": 0, "model": "SHSW-25"},
+ unique_id="12345678",
+ )
config_entry.add_to_hass(hass)
device = Mock(
@@ -99,6 +105,7 @@ async def coap_wrapper(hass):
settings=MOCK_SETTINGS,
shelly=MOCK_SHELLY,
update=AsyncMock(),
+ initialized=True,
)
hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}}
diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py
index 60f899296f6022..463c9111a60efb 100644
--- a/tests/components/shelly/test_config_flow.py
+++ b/tests/components/shelly/test_config_flow.py
@@ -13,7 +13,7 @@
MOCK_SETTINGS = {
"name": "Test name",
- "device": {"mac": "test-mac", "hostname": "test-host"},
+ "device": {"mac": "test-mac", "hostname": "test-host", "type": "SHSW-1"},
}
DISCOVERY_INFO = {
"host": "1.1.1.1",
@@ -57,6 +57,8 @@ async def test_form(hass):
assert result2["title"] == "Test name"
assert result2["data"] == {
"host": "1.1.1.1",
+ "model": "SHSW-1",
+ "sleep_period": 0,
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
@@ -101,6 +103,8 @@ async def test_title_without_name(hass):
assert result2["title"] == "shelly1pm-12345"
assert result2["data"] == {
"host": "1.1.1.1",
+ "model": "SHSW-1",
+ "sleep_period": 0,
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
@@ -149,6 +153,8 @@ async def test_form_auth(hass):
assert result3["title"] == "Test name"
assert result3["data"] == {
"host": "1.1.1.1",
+ "model": "SHSW-1",
+ "sleep_period": 0,
"username": "test username",
"password": "test password",
}
@@ -332,6 +338,13 @@ async def test_zeroconf(hass):
with patch(
"aioshelly.get_info",
return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False},
+ ), patch(
+ "aioshelly.Device.create",
+ new=AsyncMock(
+ return_value=Mock(
+ settings=MOCK_SETTINGS,
+ )
+ ),
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
@@ -346,14 +359,8 @@ async def test_zeroconf(hass):
if flow["flow_id"] == result["flow_id"]
)
assert context["title_placeholders"]["name"] == "shelly1pm-12345"
+ assert context["confirm_only"] is True
with patch(
- "aioshelly.Device.create",
- new=AsyncMock(
- return_value=Mock(
- settings=MOCK_SETTINGS,
- )
- ),
- ), patch(
"homeassistant.components.shelly.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.shelly.async_setup_entry",
@@ -369,22 +376,40 @@ async def test_zeroconf(hass):
assert result2["title"] == "Test name"
assert result2["data"] == {
"host": "1.1.1.1",
+ "model": "SHSW-1",
+ "sleep_period": 0,
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
-@pytest.mark.parametrize(
- "error", [(asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown")]
-)
-async def test_zeroconf_confirm_error(hass, error):
- """Test we get the form."""
- exc, base_error = error
+async def test_zeroconf_sleeping_device(hass):
+ """Test sleeping device configuration via zeroconf."""
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"aioshelly.get_info",
- return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False},
+ return_value={
+ "mac": "test-mac",
+ "type": "SHSW-1",
+ "auth": False,
+ "sleep_mode": True,
+ },
+ ), patch(
+ "aioshelly.Device.create",
+ new=AsyncMock(
+ return_value=Mock(
+ settings={
+ "name": "Test name",
+ "device": {
+ "mac": "test-mac",
+ "hostname": "test-host",
+ "type": "SHSW-1",
+ },
+ "sleep_mode": {"period": 10, "unit": "m"},
+ },
+ )
+ ),
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
@@ -393,18 +418,66 @@ async def test_zeroconf_confirm_error(hass, error):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
-
+ context = next(
+ flow["context"]
+ for flow in hass.config_entries.flow.async_progress()
+ if flow["flow_id"] == result["flow_id"]
+ )
+ assert context["title_placeholders"]["name"] == "shelly1pm-12345"
with patch(
- "aioshelly.Device.create",
- new=AsyncMock(side_effect=exc),
- ):
+ "homeassistant.components.shelly.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.shelly.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{},
)
+ await hass.async_block_till_done()
- assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result2["errors"] == {"base": base_error}
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result2["title"] == "Test name"
+ assert result2["data"] == {
+ "host": "1.1.1.1",
+ "model": "SHSW-1",
+ "sleep_period": 600,
+ }
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+@pytest.mark.parametrize(
+ "error",
+ [
+ (aiohttp.ClientResponseError(Mock(), (), status=400), "cannot_connect"),
+ (asyncio.TimeoutError, "cannot_connect"),
+ ],
+)
+async def test_zeroconf_sleeping_device_error(hass, error):
+ """Test sleeping device configuration via zeroconf with error."""
+ exc = error
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ with patch(
+ "aioshelly.get_info",
+ return_value={
+ "mac": "test-mac",
+ "type": "SHSW-1",
+ "auth": False,
+ "sleep_mode": True,
+ },
+ ), patch(
+ "aioshelly.Device.create",
+ new=AsyncMock(side_effect=exc),
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ data=DISCOVERY_INFO,
+ context={"source": config_entries.SOURCE_ZEROCONF},
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "cannot_connect"
async def test_zeroconf_already_configured(hass):
@@ -472,13 +545,6 @@ async def test_zeroconf_require_auth(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
- result2 = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {},
- )
- assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result2["errors"] == {}
-
with patch(
"aioshelly.Device.create",
new=AsyncMock(
@@ -492,16 +558,18 @@ async def test_zeroconf_require_auth(hass):
"homeassistant.components.shelly.async_setup_entry",
return_value=True,
) as mock_setup_entry:
- result3 = await hass.config_entries.flow.async_configure(
- result2["flow_id"],
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
{"username": "test username", "password": "test password"},
)
await hass.async_block_till_done()
- assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result3["title"] == "Test name"
- assert result3["data"] == {
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result2["title"] == "Test name"
+ assert result2["data"] == {
"host": "1.1.1.1",
+ "model": "SHSW-1",
+ "sleep_period": 0,
"username": "test username",
"password": "test password",
}
diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py
index 0be4c70ef18f0e..48482787f4d58c 100644
--- a/tests/components/shopping_list/test_init.py
+++ b/tests/components/shopping_list/test_init.py
@@ -1,5 +1,6 @@
"""Test shopping list component."""
+from homeassistant.components.shopping_list.const import DOMAIN
from homeassistant.components.websocket_api.const import (
ERR_INVALID_FORMAT,
ERR_NOT_FOUND,
@@ -19,6 +20,39 @@ async def test_add_item(hass, sl_setup):
assert response.speech["plain"]["speech"] == "I've added beer to your shopping list"
+async def test_update_list(hass, sl_setup):
+ """Test updating all list items."""
+ await intent.async_handle(
+ hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}}
+ )
+
+ await intent.async_handle(
+ hass, "test", "HassShoppingListAddItem", {"item": {"value": "cheese"}}
+ )
+
+ # Update a single attribute, other attributes shouldn't change
+ await hass.data[DOMAIN].async_update_list({"complete": True})
+
+ beer = hass.data[DOMAIN].items[0]
+ assert beer["name"] == "beer"
+ assert beer["complete"] is True
+
+ cheese = hass.data[DOMAIN].items[1]
+ assert cheese["name"] == "cheese"
+ assert cheese["complete"] is True
+
+ # Update multiple attributes
+ await hass.data[DOMAIN].async_update_list({"name": "dupe", "complete": False})
+
+ beer = hass.data[DOMAIN].items[0]
+ assert beer["name"] == "dupe"
+ assert beer["complete"] is False
+
+ cheese = hass.data[DOMAIN].items[1]
+ assert cheese["name"] == "dupe"
+ assert cheese["complete"] is False
+
+
async def test_recent_items_intent(hass, sl_setup):
"""Test recent items."""
diff --git a/tests/components/signal_messenger/test_notify.py b/tests/components/signal_messenger/test_notify.py
index 4bb6cbdd19715b..d2dc93fea2e01e 100644
--- a/tests/components/signal_messenger/test_notify.py
+++ b/tests/components/signal_messenger/test_notify.py
@@ -87,12 +87,11 @@ def test_send_message_should_show_deprecation_warning(self, mock):
)
with self.assertLogs(
"homeassistant.components.signal_messenger.notify", level="WARNING"
- ) as context:
- with tempfile.NamedTemporaryFile(
- suffix=".png", prefix=os.path.basename(__file__)
- ) as tf:
- data = {"data": {"attachment": tf.name}}
- self._signalmessenger.send_message(message, **data)
+ ) as context, tempfile.NamedTemporaryFile(
+ suffix=".png", prefix=os.path.basename(__file__)
+ ) as tf:
+ data = {"data": {"attachment": tf.name}}
+ self._signalmessenger.send_message(message, **data)
self.assertIn(
"The 'attachment' option is deprecated, please replace it with 'attachments'. This option will become invalid in version 0.108",
context.output[0],
@@ -117,12 +116,11 @@ def test_send_message_with_attachment(self, mock):
)
with self.assertLogs(
"homeassistant.components.signal_messenger.notify", level="DEBUG"
- ) as context:
- with tempfile.NamedTemporaryFile(
- suffix=".png", prefix=os.path.basename(__file__)
- ) as tf:
- data = {"data": {"attachments": [tf.name]}}
- self._signalmessenger.send_message(message, **data)
+ ) as context, tempfile.NamedTemporaryFile(
+ suffix=".png", prefix=os.path.basename(__file__)
+ ) as tf:
+ data = {"data": {"attachments": [tf.name]}}
+ self._signalmessenger.send_message(message, **data)
self.assertIn("Sending signal message", context.output[0])
self.assertTrue(mock.called)
self.assertEqual(mock.call_count, 2)
diff --git a/tests/components/slack/test_notify.py b/tests/components/slack/test_notify.py
index 9fc6784a09e0eb..6c353cf8fc679a 100644
--- a/tests/components/slack/test_notify.py
+++ b/tests/components/slack/test_notify.py
@@ -1,7 +1,8 @@
"""Test slack notifications."""
+from __future__ import annotations
+
import copy
import logging
-from typing import List
from unittest.mock import AsyncMock, Mock, patch
from _pytest.logging import LogCaptureFixture
@@ -39,7 +40,7 @@
}
-def filter_log_records(caplog: LogCaptureFixture) -> List[logging.LogRecord]:
+def filter_log_records(caplog: LogCaptureFixture) -> list[logging.LogRecord]:
"""Filter all unrelated log records."""
return [
rec for rec in caplog.records if rec.name.endswith(f"{DOMAIN}.{notify.DOMAIN}")
diff --git a/tests/components/sleepiq/test_binary_sensor.py b/tests/components/sleepiq/test_binary_sensor.py
index c158554b278c97..9b4092d9d48370 100644
--- a/tests/components/sleepiq/test_binary_sensor.py
+++ b/tests/components/sleepiq/test_binary_sensor.py
@@ -18,15 +18,15 @@ async def test_sensor_setup(hass, requests_mock):
device_mock = MagicMock()
sleepiq.setup_platform(hass, CONFIG, device_mock, MagicMock())
devices = device_mock.call_args[0][0]
- assert 2 == len(devices)
+ assert len(devices) == 2
left_side = devices[1]
- assert "SleepNumber ILE Test1 Is In Bed" == left_side.name
- assert "on" == left_side.state
+ assert left_side.name == "SleepNumber ILE Test1 Is In Bed"
+ assert left_side.state == "on"
right_side = devices[0]
- assert "SleepNumber ILE Test2 Is In Bed" == right_side.name
- assert "off" == right_side.state
+ assert right_side.name == "SleepNumber ILE Test2 Is In Bed"
+ assert right_side.state == "off"
async def test_setup_single(hass, requests_mock):
@@ -38,8 +38,8 @@ async def test_setup_single(hass, requests_mock):
device_mock = MagicMock()
sleepiq.setup_platform(hass, CONFIG, device_mock, MagicMock())
devices = device_mock.call_args[0][0]
- assert 1 == len(devices)
+ assert len(devices) == 1
right_side = devices[0]
- assert "SleepNumber ILE Test1 Is In Bed" == right_side.name
- assert "on" == right_side.state
+ assert right_side.name == "SleepNumber ILE Test1 Is In Bed"
+ assert right_side.state == "on"
diff --git a/tests/components/sleepiq/test_sensor.py b/tests/components/sleepiq/test_sensor.py
index 559e808f5546fb..c6a584802b9f0d 100644
--- a/tests/components/sleepiq/test_sensor.py
+++ b/tests/components/sleepiq/test_sensor.py
@@ -18,15 +18,15 @@ async def test_setup(hass, requests_mock):
device_mock = MagicMock()
sleepiq.setup_platform(hass, CONFIG, device_mock, MagicMock())
devices = device_mock.call_args[0][0]
- assert 2 == len(devices)
+ assert len(devices) == 2
left_side = devices[1]
- assert "SleepNumber ILE Test1 SleepNumber" == left_side.name
- assert 40 == left_side.state
+ assert left_side.name == "SleepNumber ILE Test1 SleepNumber"
+ assert left_side.state == 40
right_side = devices[0]
- assert "SleepNumber ILE Test2 SleepNumber" == right_side.name
- assert 80 == right_side.state
+ assert right_side.name == "SleepNumber ILE Test2 SleepNumber"
+ assert right_side.state == 80
async def test_setup_sigle(hass, requests_mock):
@@ -38,8 +38,8 @@ async def test_setup_sigle(hass, requests_mock):
device_mock = MagicMock()
sleepiq.setup_platform(hass, CONFIG, device_mock, MagicMock())
devices = device_mock.call_args[0][0]
- assert 1 == len(devices)
+ assert len(devices) == 1
right_side = devices[0]
- assert "SleepNumber ILE Test1 SleepNumber" == right_side.name
- assert 40 == right_side.state
+ assert right_side.name == "SleepNumber ILE Test1 SleepNumber"
+ assert right_side.state == 40
diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py
index b99309bea52597..d145e7644a9ce6 100644
--- a/tests/components/smartthings/conftest.py
+++ b/tests/components/smartthings/conftest.py
@@ -47,7 +47,7 @@
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
COMPONENT_PREFIX = "homeassistant.components.smartthings."
diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py
index 6931b3dfbb548a..7e8ab7d2c9b762 100644
--- a/tests/components/smartthings/test_binary_sensor.py
+++ b/tests/components/smartthings/test_binary_sensor.py
@@ -12,7 +12,8 @@
)
from homeassistant.components.smartthings import binary_sensor
from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE
-from homeassistant.const import ATTR_FRIENDLY_NAME
+from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .conftest import setup_platform
@@ -49,8 +50,8 @@ async def test_entity_and_device_attributes(hass, device_factory):
device = device_factory(
"Motion Sensor 1", [Capability.motion_sensor], {Attribute.motion: "inactive"}
)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
+ device_registry = dr.async_get(hass)
# Act
await setup_platform(hass, BINARY_SENSOR_DOMAIN, devices=[device])
# Assert
@@ -93,4 +94,7 @@ async def test_unload_config_entry(hass, device_factory):
# Act
await hass.config_entries.async_forward_entry_unload(config_entry, "binary_sensor")
# Assert
- assert not hass.states.get("binary_sensor.motion_sensor_1_motion")
+ assert (
+ hass.states.get("binary_sensor.motion_sensor_1_motion").state
+ == STATE_UNAVAILABLE
+ )
diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py
index 11f7695a775ea1..dc8f2acc9fa54d 100644
--- a/tests/components/smartthings/test_climate.py
+++ b/tests/components/smartthings/test_climate.py
@@ -44,6 +44,7 @@
SERVICE_TURN_ON,
STATE_UNKNOWN,
)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from .conftest import setup_platform
@@ -569,8 +570,8 @@ async def test_set_turn_on(hass, air_conditioner):
async def test_entity_and_device_attributes(hass, thermostat):
"""Test the attributes of the entries are correct."""
await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat])
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
+ device_registry = dr.async_get(hass)
entry = entity_registry.async_get("climate.thermostat")
assert entry
diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py
index 0483480cb8a5df..44c2b2f9285c9d 100644
--- a/tests/components/smartthings/test_cover.py
+++ b/tests/components/smartthings/test_cover.py
@@ -19,7 +19,8 @@
STATE_OPENING,
)
from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE
-from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID
+from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, STATE_UNAVAILABLE
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .conftest import setup_platform
@@ -31,8 +32,8 @@ async def test_entity_and_device_attributes(hass, device_factory):
device = device_factory(
"Garage", [Capability.garage_door_control], {Attribute.door: "open"}
)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
+ device_registry = dr.async_get(hass)
# Act
await setup_platform(hass, COVER_DOMAIN, devices=[device])
# Assert
@@ -193,4 +194,4 @@ async def test_unload_config_entry(hass, device_factory):
# Act
await hass.config_entries.async_forward_entry_unload(config_entry, COVER_DOMAIN)
# Assert
- assert not hass.states.get("cover.garage")
+ assert hass.states.get("cover.garage").state == STATE_UNAVAILABLE
diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py
index 0ebef7e7323822..6cdfa5b8917704 100644
--- a/tests/components/smartthings/test_fan.py
+++ b/tests/components/smartthings/test_fan.py
@@ -17,7 +17,12 @@
SUPPORT_SET_SPEED,
)
from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE
-from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
+ STATE_UNAVAILABLE,
+)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .conftest import setup_platform
@@ -55,8 +60,8 @@ async def test_entity_and_device_attributes(hass, device_factory):
)
# Act
await setup_platform(hass, FAN_DOMAIN, devices=[device])
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
+ device_registry = dr.async_get(hass)
# Assert
entry = entity_registry.async_get("fan.fan_1")
assert entry
@@ -184,4 +189,4 @@ async def test_unload_config_entry(hass, device_factory):
# Act
await hass.config_entries.async_forward_entry_unload(config_entry, "fan")
# Assert
- assert not hass.states.get("fan.fan_1")
+ assert hass.states.get("fan.fan_1").state == STATE_UNAVAILABLE
diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py
index 9024b72bb85478..eed1d5d26b1f99 100644
--- a/tests/components/smartthings/test_init.py
+++ b/tests/components/smartthings/test_init.py
@@ -14,8 +14,8 @@
DATA_BROKERS,
DOMAIN,
EVENT_BUTTON,
+ PLATFORMS,
SIGNAL_SMARTTHINGS_UPDATE,
- SUPPORTED_PLATFORMS,
)
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import HTTP_FORBIDDEN, HTTP_INTERNAL_SERVER_ERROR
@@ -174,7 +174,7 @@ async def test_scenes_unauthorized_loads_platforms(
assert await smartthings.async_setup_entry(hass, config_entry)
# Assert platforms loaded
await hass.async_block_till_done()
- assert forward_mock.call_count == len(SUPPORTED_PLATFORMS)
+ assert forward_mock.call_count == len(PLATFORMS)
async def test_config_entry_loads_platforms(
@@ -206,7 +206,7 @@ async def test_config_entry_loads_platforms(
assert await smartthings.async_setup_entry(hass, config_entry)
# Assert platforms loaded
await hass.async_block_till_done()
- assert forward_mock.call_count == len(SUPPORTED_PLATFORMS)
+ assert forward_mock.call_count == len(PLATFORMS)
async def test_config_entry_loads_unconnected_cloud(
@@ -237,7 +237,7 @@ async def test_config_entry_loads_unconnected_cloud(
with patch.object(hass.config_entries, "async_forward_entry_setup") as forward_mock:
assert await smartthings.async_setup_entry(hass, config_entry)
await hass.async_block_till_done()
- assert forward_mock.call_count == len(SUPPORTED_PLATFORMS)
+ assert forward_mock.call_count == len(PLATFORMS)
async def test_unload_entry(hass, config_entry):
@@ -258,7 +258,7 @@ async def test_unload_entry(hass, config_entry):
assert config_entry.entry_id not in hass.data[DOMAIN][DATA_BROKERS]
# Assert platforms unloaded
await hass.async_block_till_done()
- assert forward_mock.call_count == len(SUPPORTED_PLATFORMS)
+ assert forward_mock.call_count == len(PLATFORMS)
async def test_remove_entry(hass, config_entry, smartthings_mock):
diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py
index bd9557c6b9716d..c9dbb0941619ca 100644
--- a/tests/components/smartthings/test_light.py
+++ b/tests/components/smartthings/test_light.py
@@ -19,7 +19,12 @@
SUPPORT_TRANSITION,
)
from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE
-from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
+ STATE_UNAVAILABLE,
+)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .conftest import setup_platform
@@ -106,8 +111,8 @@ async def test_entity_and_device_attributes(hass, device_factory):
"""Test the attributes of the entity are correct."""
# Arrange
device = device_factory("Light 1", [Capability.switch, Capability.switch_level])
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
+ device_registry = dr.async_get(hass)
# Act
await setup_platform(hass, LIGHT_DOMAIN, devices=[device])
# Assert
@@ -304,4 +309,4 @@ async def test_unload_config_entry(hass, device_factory):
# Act
await hass.config_entries.async_forward_entry_unload(config_entry, "light")
# Assert
- assert not hass.states.get("light.color_dimmer_2")
+ assert hass.states.get("light.color_dimmer_2").state == STATE_UNAVAILABLE
diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py
index 0492f2281ce538..1168108656e3d4 100644
--- a/tests/components/smartthings/test_lock.py
+++ b/tests/components/smartthings/test_lock.py
@@ -9,6 +9,8 @@
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE
+from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .conftest import setup_platform
@@ -18,8 +20,8 @@ async def test_entity_and_device_attributes(hass, device_factory):
"""Test the attributes of the entity are correct."""
# Arrange
device = device_factory("Lock_1", [Capability.lock], {Attribute.lock: "unlocked"})
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
+ device_registry = dr.async_get(hass)
# Act
await setup_platform(hass, LOCK_DOMAIN, devices=[device])
# Assert
@@ -104,4 +106,4 @@ async def test_unload_config_entry(hass, device_factory):
# Act
await hass.config_entries.async_forward_entry_unload(config_entry, "lock")
# Assert
- assert not hass.states.get("lock.lock_1")
+ assert hass.states.get("lock.lock_1").state == STATE_UNAVAILABLE
diff --git a/tests/components/smartthings/test_scene.py b/tests/components/smartthings/test_scene.py
index a9e6443d2bff11..647389eeb427bc 100644
--- a/tests/components/smartthings/test_scene.py
+++ b/tests/components/smartthings/test_scene.py
@@ -5,7 +5,8 @@
real HTTP calls are not initiated during testing.
"""
from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN
-from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON
+from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, STATE_UNAVAILABLE
+from homeassistant.helpers import entity_registry as er
from .conftest import setup_platform
@@ -13,7 +14,7 @@
async def test_entity_and_device_attributes(hass, scene):
"""Test the attributes of the entity are correct."""
# Arrange
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
# Act
await setup_platform(hass, SCENE_DOMAIN, scenes=[scene])
# Assert
@@ -46,4 +47,4 @@ async def test_unload_config_entry(hass, scene):
# Act
await hass.config_entries.async_forward_entry_unload(config_entry, SCENE_DOMAIN)
# Assert
- assert not hass.states.get("scene.test_scene")
+ assert hass.states.get("scene.test_scene").state == STATE_UNAVAILABLE
diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py
index 3faf0f621a3e35..0f148b8931fe7f 100644
--- a/tests/components/smartthings/test_sensor.py
+++ b/tests/components/smartthings/test_sensor.py
@@ -13,8 +13,10 @@
ATTR_FRIENDLY_NAME,
ATTR_UNIT_OF_MEASUREMENT,
PERCENTAGE,
+ STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .conftest import setup_platform
@@ -77,8 +79,8 @@ async def test_entity_and_device_attributes(hass, device_factory):
"""Test the attributes of the entity are correct."""
# Arrange
device = device_factory("Sensor 1", [Capability.battery], {Attribute.battery: 100})
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
+ device_registry = dr.async_get(hass)
# Act
await setup_platform(hass, SENSOR_DOMAIN, devices=[device])
# Assert
@@ -117,4 +119,4 @@ async def test_unload_config_entry(hass, device_factory):
# Act
await hass.config_entries.async_forward_entry_unload(config_entry, "sensor")
# Assert
- assert not hass.states.get("sensor.sensor_1_battery")
+ assert hass.states.get("sensor.sensor_1_battery").state == STATE_UNAVAILABLE
diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py
index 3ac86426eeb4c7..21d508bcbc2a5e 100644
--- a/tests/components/smartthings/test_switch.py
+++ b/tests/components/smartthings/test_switch.py
@@ -12,6 +12,8 @@
ATTR_TODAY_ENERGY_KWH,
DOMAIN as SWITCH_DOMAIN,
)
+from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .conftest import setup_platform
@@ -21,8 +23,8 @@ async def test_entity_and_device_attributes(hass, device_factory):
"""Test the attributes of the entity are correct."""
# Arrange
device = device_factory("Switch_1", [Capability.switch], {Attribute.switch: "on"})
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
+ device_registry = dr.async_get(hass)
# Act
await setup_platform(hass, SWITCH_DOMAIN, devices=[device])
# Assert
@@ -96,4 +98,4 @@ async def test_unload_config_entry(hass, device_factory):
# Act
await hass.config_entries.async_forward_entry_unload(config_entry, "switch")
# Assert
- assert not hass.states.get("switch.switch_1")
+ assert hass.states.get("switch.switch_1").state == STATE_UNAVAILABLE
diff --git a/tests/components/smarttub/__init__.py b/tests/components/smarttub/__init__.py
new file mode 100644
index 00000000000000..b19af1ee59a91b
--- /dev/null
+++ b/tests/components/smarttub/__init__.py
@@ -0,0 +1,15 @@
+"""Tests for the smarttub integration."""
+
+from datetime import timedelta
+
+from homeassistant.components.smarttub.const import SCAN_INTERVAL
+from homeassistant.util import dt
+
+from tests.common import async_fire_time_changed
+
+
+async def trigger_update(hass):
+ """Trigger a polling update by moving time forward."""
+ new_time = dt.utcnow() + timedelta(seconds=SCAN_INTERVAL + 1)
+ async_fire_time_changed(hass, new_time)
+ await hass.async_block_till_done()
diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py
new file mode 100644
index 00000000000000..7f23c2355bc7b2
--- /dev/null
+++ b/tests/components/smarttub/conftest.py
@@ -0,0 +1,147 @@
+"""Common fixtures for smarttub tests."""
+
+from unittest.mock import create_autospec, patch
+
+import pytest
+import smarttub
+
+from homeassistant.components.smarttub.const import DOMAIN
+from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
+from homeassistant.setup import async_setup_component
+
+from tests.common import MockConfigEntry
+
+
+@pytest.fixture
+def config_data():
+ """Provide configuration data for tests."""
+ return {CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"}
+
+
+@pytest.fixture
+def config_entry(config_data):
+ """Create a mock config entry."""
+ return MockConfigEntry(
+ domain=DOMAIN,
+ data=config_data,
+ options={},
+ )
+
+
+@pytest.fixture
+async def setup_component(hass):
+ """Set up the component."""
+ assert await async_setup_component(hass, DOMAIN, {}) is True
+
+
+@pytest.fixture(name="spa")
+def mock_spa():
+ """Mock a smarttub.Spa."""
+
+ mock_spa = create_autospec(smarttub.Spa, instance=True)
+ mock_spa.id = "mockspa1"
+ mock_spa.brand = "mockbrand1"
+ mock_spa.model = "mockmodel1"
+ mock_spa.get_status.return_value = smarttub.SpaState(
+ mock_spa,
+ **{
+ "setTemperature": 39,
+ "water": {"temperature": 38},
+ "heater": "ON",
+ "online": True,
+ "heatMode": "AUTO",
+ "state": "NORMAL",
+ "primaryFiltration": {
+ "cycle": 1,
+ "duration": 4,
+ "lastUpdated": "2021-01-20T11:38:57.014Z",
+ "mode": "NORMAL",
+ "startHour": 2,
+ "status": "INACTIVE",
+ },
+ "secondaryFiltration": {
+ "lastUpdated": "2020-07-09T19:39:52.961Z",
+ "mode": "AWAY",
+ "status": "INACTIVE",
+ },
+ "flowSwitch": "OPEN",
+ "ozone": "OFF",
+ "uv": "OFF",
+ "blowoutCycle": "INACTIVE",
+ "cleanupCycle": "INACTIVE",
+ },
+ )
+ mock_circulation_pump = create_autospec(smarttub.SpaPump, instance=True)
+ mock_circulation_pump.id = "CP"
+ mock_circulation_pump.spa = mock_spa
+ mock_circulation_pump.state = smarttub.SpaPump.PumpState.OFF
+ mock_circulation_pump.type = smarttub.SpaPump.PumpType.CIRCULATION
+
+ mock_jet_off = create_autospec(smarttub.SpaPump, instance=True)
+ mock_jet_off.id = "P1"
+ mock_jet_off.spa = mock_spa
+ mock_jet_off.state = smarttub.SpaPump.PumpState.OFF
+ mock_jet_off.type = smarttub.SpaPump.PumpType.JET
+
+ mock_jet_on = create_autospec(smarttub.SpaPump, instance=True)
+ mock_jet_on.id = "P2"
+ mock_jet_on.spa = mock_spa
+ mock_jet_on.state = smarttub.SpaPump.PumpState.HIGH
+ mock_jet_on.type = smarttub.SpaPump.PumpType.JET
+
+ mock_spa.get_pumps.return_value = [mock_circulation_pump, mock_jet_off, mock_jet_on]
+
+ mock_light_off = create_autospec(smarttub.SpaLight, instance=True)
+ mock_light_off.spa = mock_spa
+ mock_light_off.zone = 1
+ mock_light_off.intensity = 0
+ mock_light_off.mode = smarttub.SpaLight.LightMode.OFF
+
+ mock_light_on = create_autospec(smarttub.SpaLight, instance=True)
+ mock_light_on.spa = mock_spa
+ mock_light_on.zone = 2
+ mock_light_on.intensity = 50
+ mock_light_on.mode = smarttub.SpaLight.LightMode.PURPLE
+
+ mock_spa.get_lights.return_value = [mock_light_off, mock_light_on]
+
+ mock_filter_reminder = create_autospec(smarttub.SpaReminder, instance=True)
+ mock_filter_reminder.id = "FILTER01"
+ mock_filter_reminder.name = "MyFilter"
+ mock_filter_reminder.remaining_days = 2
+ mock_filter_reminder.snoozed = False
+
+ mock_spa.get_reminders.return_value = [mock_filter_reminder]
+
+ return mock_spa
+
+
+@pytest.fixture(name="account")
+def mock_account(spa):
+ """Mock a SmartTub.Account."""
+
+ mock_account = create_autospec(smarttub.Account, instance=True)
+ mock_account.id = "mockaccount1"
+ mock_account.get_spas.return_value = [spa]
+ return mock_account
+
+
+@pytest.fixture(name="smarttub_api", autouse=True)
+def mock_api(account, spa):
+ """Mock the SmartTub API."""
+
+ with patch(
+ "homeassistant.components.smarttub.controller.SmartTub",
+ autospec=True,
+ ) as api_class_mock:
+ api_mock = api_class_mock.return_value
+ api_mock.get_account.return_value = account
+ yield api_mock
+
+
+@pytest.fixture
+async def setup_entry(hass, config_entry):
+ """Initialize the config entry."""
+ config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
diff --git a/tests/components/smarttub/test_binary_sensor.py b/tests/components/smarttub/test_binary_sensor.py
new file mode 100644
index 00000000000000..5db97310c561c7
--- /dev/null
+++ b/tests/components/smarttub/test_binary_sensor.py
@@ -0,0 +1,26 @@
+"""Test the SmartTub binary sensor platform."""
+from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
+ STATE_OFF,
+ STATE_ON,
+)
+
+
+async def test_binary_sensors(spa, setup_entry, hass):
+ """Test simple binary sensors."""
+
+ entity_id = f"binary_sensor.{spa.brand}_{spa.model}_online"
+ state = hass.states.get(entity_id)
+ assert state is not None
+ assert state.state == STATE_ON
+ assert state.attributes.get("device_class") == DEVICE_CLASS_CONNECTIVITY
+
+
+async def test_reminders(spa, setup_entry, hass):
+ """Test the reminder sensor."""
+
+ entity_id = f"binary_sensor.{spa.brand}_{spa.model}_myfilter_reminder"
+ state = hass.states.get(entity_id)
+ assert state is not None
+ assert state.state == STATE_OFF
+ assert state.attributes["snoozed"] is False
diff --git a/tests/components/smarttub/test_climate.py b/tests/components/smarttub/test_climate.py
new file mode 100644
index 00000000000000..a034a4ce17edef
--- /dev/null
+++ b/tests/components/smarttub/test_climate.py
@@ -0,0 +1,95 @@
+"""Test the SmartTub climate platform."""
+
+import smarttub
+
+from homeassistant.components.climate.const import (
+ ATTR_CURRENT_TEMPERATURE,
+ ATTR_HVAC_ACTION,
+ ATTR_HVAC_MODE,
+ ATTR_HVAC_MODES,
+ ATTR_MAX_TEMP,
+ ATTR_MIN_TEMP,
+ ATTR_PRESET_MODE,
+ ATTR_PRESET_MODES,
+ CURRENT_HVAC_HEAT,
+ CURRENT_HVAC_IDLE,
+ DOMAIN as CLIMATE_DOMAIN,
+ HVAC_MODE_HEAT,
+ PRESET_ECO,
+ PRESET_NONE,
+ SERVICE_SET_HVAC_MODE,
+ SERVICE_SET_PRESET_MODE,
+ SERVICE_SET_TEMPERATURE,
+ SUPPORT_PRESET_MODE,
+ SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.components.smarttub.const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
+ ATTR_TEMPERATURE,
+)
+
+from . import trigger_update
+
+
+async def test_thermostat_update(spa, setup_entry, hass):
+ """Test the thermostat entity."""
+
+ entity_id = f"climate.{spa.brand}_{spa.model}_thermostat"
+ state = hass.states.get(entity_id)
+ assert state
+
+ assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT
+
+ spa.get_status.return_value.heater = "OFF"
+ await trigger_update(hass)
+ state = hass.states.get(entity_id)
+
+ assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
+
+ assert set(state.attributes[ATTR_HVAC_MODES]) == {HVAC_MODE_HEAT}
+ assert state.state == HVAC_MODE_HEAT
+ assert (
+ state.attributes[ATTR_SUPPORTED_FEATURES]
+ == SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE
+ )
+ assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 38
+ assert state.attributes[ATTR_TEMPERATURE] == 39
+ assert state.attributes[ATTR_MAX_TEMP] == DEFAULT_MAX_TEMP
+ assert state.attributes[ATTR_MIN_TEMP] == DEFAULT_MIN_TEMP
+ assert state.attributes[ATTR_PRESET_MODES] == ["none", "eco", "day"]
+
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 37},
+ blocking=True,
+ )
+ spa.set_temperature.assert_called_with(37)
+
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVAC_MODE_HEAT},
+ blocking=True,
+ )
+ # does nothing
+
+ assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_NONE
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_ECO},
+ blocking=True,
+ )
+ spa.set_heat_mode.assert_called_with(smarttub.Spa.HeatMode.ECONOMY)
+
+ spa.get_status.return_value.heat_mode = smarttub.Spa.HeatMode.ECONOMY
+ await trigger_update(hass)
+ state = hass.states.get(entity_id)
+ assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_ECO
+
+ spa.get_status.side_effect = smarttub.APIError
+ await trigger_update(hass)
+ # should not fail
diff --git a/tests/components/smarttub/test_config_flow.py b/tests/components/smarttub/test_config_flow.py
new file mode 100644
index 00000000000000..2608d867c0d6c7
--- /dev/null
+++ b/tests/components/smarttub/test_config_flow.py
@@ -0,0 +1,64 @@
+"""Test the smarttub config flow."""
+from unittest.mock import patch
+
+from smarttub import LoginFailed
+
+from homeassistant import config_entries
+from homeassistant.components.smarttub.const import DOMAIN
+
+
+async def test_form(hass):
+ """Test we get the form."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.smarttub.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.smarttub.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"email": "test-email", "password": "test-password"},
+ )
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "test-email"
+ assert result2["data"] == {
+ "email": "test-email",
+ "password": "test-password",
+ }
+ await hass.async_block_till_done()
+ mock_setup.assert_called_once()
+ mock_setup_entry.assert_called_once()
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"email": "test-email2", "password": "test-password2"}
+ )
+ assert result2["type"] == "abort"
+ assert result2["reason"] == "reauth_successful"
+
+
+async def test_form_invalid_auth(hass, smarttub_api):
+ """Test we handle invalid auth."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ smarttub_api.login.side_effect = LoginFailed
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"email": "test-email", "password": "test-password"},
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "invalid_auth"}
diff --git a/tests/components/smarttub/test_init.py b/tests/components/smarttub/test_init.py
new file mode 100644
index 00000000000000..01989818d3be67
--- /dev/null
+++ b/tests/components/smarttub/test_init.py
@@ -0,0 +1,54 @@
+"""Test smarttub setup process."""
+
+import asyncio
+
+from smarttub import LoginFailed
+
+from homeassistant.components import smarttub
+from homeassistant.config_entries import (
+ ENTRY_STATE_SETUP_ERROR,
+ ENTRY_STATE_SETUP_RETRY,
+)
+from homeassistant.setup import async_setup_component
+
+
+async def test_setup_with_no_config(setup_component, hass, smarttub_api):
+ """Test that we do not discover anything."""
+
+ # No flows started
+ assert len(hass.config_entries.flow.async_progress()) == 0
+
+ smarttub_api.login.assert_not_called()
+
+
+async def test_setup_entry_not_ready(setup_component, hass, config_entry, smarttub_api):
+ """Test setup when the entry is not ready."""
+ smarttub_api.login.side_effect = asyncio.TimeoutError
+
+ config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ assert config_entry.state == ENTRY_STATE_SETUP_RETRY
+
+
+async def test_setup_auth_failed(setup_component, hass, config_entry, smarttub_api):
+ """Test setup when the credentials are invalid."""
+ smarttub_api.login.side_effect = LoginFailed
+
+ config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ assert config_entry.state == ENTRY_STATE_SETUP_ERROR
+
+
+async def test_config_passed_to_config_entry(hass, config_entry, config_data):
+ """Test that configured options are loaded via config entry."""
+ config_entry.add_to_hass(hass)
+ assert await async_setup_component(hass, smarttub.DOMAIN, config_data)
+
+
+async def test_unload_entry(hass, config_entry):
+ """Test being able to unload an entry."""
+ config_entry.add_to_hass(hass)
+
+ assert await async_setup_component(hass, smarttub.DOMAIN, {}) is True
+
+ assert await hass.config_entries.async_unload(config_entry.entry_id)
diff --git a/tests/components/smarttub/test_light.py b/tests/components/smarttub/test_light.py
new file mode 100644
index 00000000000000..fe178278beef94
--- /dev/null
+++ b/tests/components/smarttub/test_light.py
@@ -0,0 +1,45 @@
+"""Test the SmartTub light platform."""
+
+import pytest
+from smarttub import SpaLight
+
+
+# the light in light_zone should have initial state light_state. we will call
+# service_name with service_params, and expect the resultant call to
+# SpaLight.set_mode to have set_mode_args parameters
+@pytest.mark.parametrize(
+ "light_zone,light_state,service_name,service_params,set_mode_args",
+ [
+ (1, "off", "turn_on", {}, (SpaLight.LightMode.PURPLE, 50)),
+ (1, "off", "turn_on", {"brightness": 255}, (SpaLight.LightMode.PURPLE, 100)),
+ (2, "on", "turn_off", {}, (SpaLight.LightMode.OFF, 0)),
+ ],
+)
+async def test_light(
+ spa,
+ setup_entry,
+ hass,
+ light_zone,
+ light_state,
+ service_name,
+ service_params,
+ set_mode_args,
+):
+ """Test light entity."""
+
+ entity_id = f"light.{spa.brand}_{spa.model}_light_{light_zone}"
+ state = hass.states.get(entity_id)
+ assert state is not None
+ assert state.state == light_state
+
+ light: SpaLight = next(
+ light for light in await spa.get_lights() if light.zone == light_zone
+ )
+
+ await hass.services.async_call(
+ "light",
+ service_name,
+ {"entity_id": entity_id, **service_params},
+ blocking=True,
+ )
+ light.set_mode.assert_called_with(*set_mode_args)
diff --git a/tests/components/smarttub/test_sensor.py b/tests/components/smarttub/test_sensor.py
new file mode 100644
index 00000000000000..4f179b2491004f
--- /dev/null
+++ b/tests/components/smarttub/test_sensor.py
@@ -0,0 +1,47 @@
+"""Test the SmartTub sensor platform."""
+
+import pytest
+
+
+@pytest.mark.parametrize(
+ "entity_suffix,expected_state",
+ [
+ ("state", "normal"),
+ ("flow_switch", "open"),
+ ("ozone", "off"),
+ ("uv", "off"),
+ ("blowout_cycle", "inactive"),
+ ("cleanup_cycle", "inactive"),
+ ],
+)
+async def test_sensor(spa, setup_entry, hass, entity_suffix, expected_state):
+ """Test simple sensors."""
+
+ entity_id = f"sensor.{spa.brand}_{spa.model}_{entity_suffix}"
+ state = hass.states.get(entity_id)
+ assert state is not None
+ assert state.state == expected_state
+
+
+async def test_primary_filtration(spa, setup_entry, hass):
+ """Test the primary filtration cycle sensor."""
+
+ entity_id = f"sensor.{spa.brand}_{spa.model}_primary_filtration_cycle"
+ state = hass.states.get(entity_id)
+ assert state is not None
+ assert state.state == "inactive"
+ assert state.attributes["duration"] == 4
+ assert state.attributes["cycle_last_updated"] is not None
+ assert state.attributes["mode"] == "normal"
+ assert state.attributes["start_hour"] == 2
+
+
+async def test_secondary_filtration(spa, setup_entry, hass):
+ """Test the secondary filtration cycle sensor."""
+
+ entity_id = f"sensor.{spa.brand}_{spa.model}_secondary_filtration_cycle"
+ state = hass.states.get(entity_id)
+ assert state is not None
+ assert state.state == "inactive"
+ assert state.attributes["cycle_last_updated"] is not None
+ assert state.attributes["mode"] == "away"
diff --git a/tests/components/smarttub/test_switch.py b/tests/components/smarttub/test_switch.py
new file mode 100644
index 00000000000000..81b53604065534
--- /dev/null
+++ b/tests/components/smarttub/test_switch.py
@@ -0,0 +1,51 @@
+"""Test the SmartTub switch platform."""
+
+import pytest
+
+from homeassistant.const import STATE_OFF, STATE_ON
+
+
+@pytest.mark.parametrize(
+ "pump_id,entity_suffix,pump_state",
+ [
+ ("CP", "circulation_pump", "off"),
+ ("P1", "jet_p1", "off"),
+ ("P2", "jet_p2", "on"),
+ ],
+)
+async def test_pumps(spa, setup_entry, hass, pump_id, pump_state, entity_suffix):
+ """Test pump entities."""
+
+ pump = next(pump for pump in await spa.get_pumps() if pump.id == pump_id)
+
+ entity_id = f"switch.{spa.brand}_{spa.model}_{entity_suffix}"
+ state = hass.states.get(entity_id)
+ assert state is not None
+ assert state.state == pump_state
+
+ await hass.services.async_call(
+ "switch",
+ "toggle",
+ {"entity_id": entity_id},
+ blocking=True,
+ )
+ pump.toggle.assert_called()
+
+ if state.state == STATE_OFF:
+ await hass.services.async_call(
+ "switch",
+ "turn_on",
+ {"entity_id": entity_id},
+ blocking=True,
+ )
+ pump.toggle.assert_called()
+ else:
+ assert state.state == STATE_ON
+
+ await hass.services.async_call(
+ "switch",
+ "turn_off",
+ {"entity_id": entity_id},
+ blocking=True,
+ )
+ pump.toggle.assert_called()
diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py
index 4caae0edcfeae5..bb21607feadd65 100644
--- a/tests/components/solaredge/test_config_flow.py
+++ b/tests/components/solaredge/test_config_flow.py
@@ -8,6 +8,7 @@
from homeassistant.components.solaredge import config_flow
from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME
from homeassistant.const import CONF_API_KEY, CONF_NAME
+from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@@ -25,14 +26,14 @@ def mock_controller():
yield api
-def init_config_flow(hass):
+def init_config_flow(hass: HomeAssistant) -> config_flow.SolarEdgeConfigFlow:
"""Init a configuration flow."""
flow = config_flow.SolarEdgeConfigFlow()
flow.hass = hass
return flow
-async def test_user(hass, test_api):
+async def test_user(hass: HomeAssistant, test_api: Mock) -> None:
"""Test user config."""
flow = init_config_flow(hass)
@@ -50,7 +51,7 @@ async def test_user(hass, test_api):
assert result["data"][CONF_API_KEY] == API_KEY
-async def test_import(hass, test_api):
+async def test_import(hass: HomeAssistant, test_api: Mock) -> None:
"""Test import step."""
flow = init_config_flow(hass)
@@ -73,7 +74,7 @@ async def test_import(hass, test_api):
assert result["data"][CONF_API_KEY] == API_KEY
-async def test_abort_if_already_setup(hass, test_api):
+async def test_abort_if_already_setup(hass: HomeAssistant, test_api: str) -> None:
"""Test we abort if the site_id is already setup."""
flow = init_config_flow(hass)
MockConfigEntry(
@@ -96,7 +97,7 @@ async def test_abort_if_already_setup(hass, test_api):
assert result["errors"] == {CONF_SITE_ID: "already_configured"}
-async def test_asserts(hass, test_api):
+async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None:
"""Test the _site_in_configuration_exists method."""
flow = init_config_flow(hass)
diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py
index 47adb5bdc91800..60200824c00f52 100644
--- a/tests/components/somfy/test_config_flow.py
+++ b/tests/components/somfy/test_config_flow.py
@@ -123,7 +123,9 @@ async def test_full_flow(
assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED
-async def test_abort_if_authorization_timeout(hass, mock_impl):
+async def test_abort_if_authorization_timeout(
+ hass, mock_impl, current_request_with_host
+):
"""Check Somfy authorization timeout."""
flow = config_flow.SomfyFlowHandler()
flow.hass = hass
diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py
index 0b306dc824088d..3a11688a56f9d7 100644
--- a/tests/components/sonarr/test_sensor.py
+++ b/tests/components/sonarr/test_sensor.py
@@ -12,6 +12,7 @@
DATA_GIGABYTES,
STATE_UNAVAILABLE,
)
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import dt as dt_util
@@ -27,7 +28,7 @@ async def test_sensors(
) -> None:
"""Test the creation and values of the sensors."""
entry = await setup_integration(hass, aioclient_mock, skip_entry_setup=True)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
# Pre-create registry entries for disabled by default sensors
sensors = {
@@ -107,7 +108,7 @@ async def test_disabled_by_default_sensors(
) -> None:
"""Test the disabled by default sensors."""
await setup_integration(hass, aioclient_mock)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
print(registry.entities)
state = hass.states.get(entity_id)
diff --git a/tests/components/songpal/test_media_player.py b/tests/components/songpal/test_media_player.py
index aff79ef62ffef1..0fd5644e794ac4 100644
--- a/tests/components/songpal/test_media_player.py
+++ b/tests/components/songpal/test_media_player.py
@@ -113,7 +113,7 @@ async def test_state(hass):
assert attributes["source"] == "title2"
assert attributes["supported_features"] == SUPPORT_SONGPAL
- device_registry = await dr.async_get_registry(hass)
+ device_registry = dr.async_get(hass)
device = device_registry.async_get_device(identifiers={(songpal.DOMAIN, MAC)})
assert device.connections == {(dr.CONNECTION_NETWORK_MAC, MAC)}
assert device.manufacturer == "Sony Corporation"
@@ -121,7 +121,7 @@ async def test_state(hass):
assert device.sw_version == SW_VERSION
assert device.model == MODEL
- entity_registry = await er.async_get_registry(hass)
+ entity_registry = er.async_get(hass)
entity = entity_registry.async_get(ENTITY_ID)
assert entity.unique_id == MAC
diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py
index 1ce2205813b1fe..3562d991e985bd 100644
--- a/tests/components/sonos/conftest.py
+++ b/tests/components/sonos/conftest.py
@@ -1,5 +1,5 @@
"""Configuration for Sonos tests."""
-from unittest.mock import Mock, patch as patch
+from unittest.mock import AsyncMock, MagicMock, Mock, patch as patch
import pytest
@@ -31,6 +31,10 @@ def soco_fixture(music_library, speaker_info, dummy_soco_service):
mock_soco.renderingControl = dummy_soco_service
mock_soco.zoneGroupTopology = dummy_soco_service
mock_soco.contentDirectory = dummy_soco_service
+ mock_soco.mute = False
+ mock_soco.night_mode = True
+ mock_soco.dialog_mode = True
+ mock_soco.volume = 19
yield mock_soco
@@ -41,6 +45,7 @@ def discover_fixture(soco):
def do_callback(callback, **kwargs):
callback(soco)
+ return MagicMock()
with patch("pysonos.discover_thread", side_effect=do_callback) as mock:
yield mock
@@ -56,7 +61,7 @@ def config_fixture():
def dummy_soco_service_fixture():
"""Create dummy_soco_service fixture."""
service = Mock()
- service.subscribe = Mock()
+ service.subscribe = AsyncMock()
return service
diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py
index 6a401ee0c16846..d5b0158d6c4463 100644
--- a/tests/components/sonos/test_media_player.py
+++ b/tests/components/sonos/test_media_player.py
@@ -2,8 +2,10 @@
import pytest
from homeassistant.components.sonos import DOMAIN, media_player
+from homeassistant.const import STATE_IDLE
from homeassistant.core import Context
from homeassistant.exceptions import Unauthorized
+from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
@@ -48,7 +50,7 @@ async def test_device_registry(hass, config_entry, config, soco):
"""Test sonos device registered in the device registry."""
await setup_platform(hass, config_entry, config)
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
reg_device = device_registry.async_get_device(
identifiers={("sonos", "RINCON_test")}
)
@@ -56,4 +58,19 @@ async def test_device_registry(hass, config_entry, config, soco):
assert reg_device.sw_version == "49.2-64250"
assert reg_device.connections == {("mac", "00:11:22:33:44:55")}
assert reg_device.manufacturer == "Sonos"
+ assert reg_device.suggested_area == "Zone A"
assert reg_device.name == "Zone A"
+
+
+async def test_entity_basic(hass, config_entry, discover):
+ """Test basic state and attributes."""
+ await setup_platform(hass, config_entry, {})
+
+ state = hass.states.get("media_player.zone_a")
+ assert state.state == STATE_IDLE
+ attributes = state.attributes
+ assert attributes["friendly_name"] == "Zone A"
+ assert attributes["is_volume_muted"] is False
+ assert attributes["night_sound"] is True
+ assert attributes["speech_enhance"] is True
+ assert attributes["volume_level"] == 0.19
diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py
index ddab7b1ba36152..11f59444c2c5d1 100644
--- a/tests/components/sql/test_sensor.py
+++ b/tests/components/sql/test_sensor.py
@@ -55,3 +55,43 @@ async def test_invalid_query(hass):
state = hass.states.get("sensor.count_tables")
assert state.state == STATE_UNKNOWN
+
+
+@pytest.mark.parametrize(
+ "url,expected_patterns,not_expected_patterns",
+ [
+ (
+ "sqlite://homeassistant:hunter2@homeassistant.local",
+ ["sqlite://****:****@homeassistant.local"],
+ ["sqlite://homeassistant:hunter2@homeassistant.local"],
+ ),
+ (
+ "sqlite://homeassistant.local",
+ ["sqlite://homeassistant.local"],
+ [],
+ ),
+ ],
+)
+async def test_invalid_url(hass, caplog, url, expected_patterns, not_expected_patterns):
+ """Test credentials in url is not logged."""
+ config = {
+ "sensor": {
+ "platform": "sql",
+ "db_url": url,
+ "queries": [
+ {
+ "name": "count_tables",
+ "query": "SELECT 5 as value",
+ "column": "value",
+ }
+ ],
+ }
+ }
+
+ assert await async_setup_component(hass, "sensor", config)
+ await hass.async_block_till_done()
+
+ for pattern in not_expected_patterns:
+ assert pattern not in caplog.text
+ for pattern in expected_patterns:
+ assert pattern in caplog.text
diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py
index 2dae2f78d017c8..cd734e1627cdbc 100644
--- a/tests/components/squeezebox/test_config_flow.py
+++ b/tests/components/squeezebox/test_config_flow.py
@@ -45,8 +45,6 @@ async def patch_async_query_unauthorized(self, *args):
async def test_user_form(hass):
"""Test user-initiated flow, including discovery and the edit step."""
with patch("pysqueezebox.Server.async_query", return_value={"uuid": UUID},), patch(
- "homeassistant.components.squeezebox.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.squeezebox.async_setup_entry",
return_value=True,
) as mock_setup_entry, patch(
@@ -77,7 +75,6 @@ async def test_user_form(hass):
}
await hass.async_block_till_done()
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
@@ -111,8 +108,6 @@ async def test_user_form_duplicate(hass):
"homeassistant.components.squeezebox.config_flow.async_discover",
mock_discover,
), patch("homeassistant.components.squeezebox.config_flow.TIMEOUT", 0.1), patch(
- "homeassistant.components.squeezebox.async_setup", return_value=True
- ), patch(
"homeassistant.components.squeezebox.async_setup_entry",
return_value=True,
):
@@ -204,8 +199,6 @@ async def test_discovery_no_uuid(hass):
async def test_import(hass):
"""Test handling of configuration imported."""
with patch("pysqueezebox.Server.async_query", return_value={"uuid": UUID},), patch(
- "homeassistant.components.squeezebox.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.squeezebox.async_setup_entry",
return_value=True,
) as mock_setup_entry:
@@ -217,7 +210,6 @@ async def test_import(hass):
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
await hass.async_block_till_done()
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
@@ -253,8 +245,6 @@ async def test_import_bad_auth(hass):
async def test_import_existing(hass):
"""Test handling of configuration import of existing server."""
with patch(
- "homeassistant.components.squeezebox.async_setup", return_value=True
- ), patch(
"homeassistant.components.squeezebox.async_setup_entry",
return_value=True,
), patch(
diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py
index a93e56b7b931af..069dc9eb64f034 100644
--- a/tests/components/srp_energy/test_sensor.py
+++ b/tests/components/srp_energy/test_sensor.py
@@ -91,7 +91,7 @@ async def test_srp_entity(hass):
assert srp_entity.icon == ICON
assert srp_entity.usage == "2.00"
assert srp_entity.should_poll is False
- assert srp_entity.device_state_attributes[ATTR_ATTRIBUTION] == ATTRIBUTION
+ assert srp_entity.extra_state_attributes[ATTR_ATTRIBUTION] == ATTRIBUTION
assert srp_entity.available is not None
await srp_entity.async_added_to_hass()
@@ -104,7 +104,7 @@ async def test_srp_entity_no_data(hass):
"""Test the SrpEntity."""
fake_coordinator = MagicMock(data=False)
srp_entity = SrpEntity(fake_coordinator)
- assert srp_entity.device_state_attributes is None
+ assert srp_entity.extra_state_attributes is None
async def test_srp_entity_no_coord_data(hass):
diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py
index 008995cd78dd23..8ca82e93bfc712 100644
--- a/tests/components/ssdp/test_init.py
+++ b/tests/components/ssdp/test_init.py
@@ -14,15 +14,18 @@ async def test_scan_match_st(hass, caplog):
"""Test matching based on ST."""
scanner = ssdp.Scanner(hass, {"mock-domain": [{"st": "mock-st"}]})
- with patch(
- "netdisco.ssdp.scan",
- return_value=[
+ async def _inject_entry(*args, **kwargs):
+ scanner.async_store_entry(
Mock(
st="mock-st",
location=None,
values={"usn": "mock-usn", "server": "mock-server", "ext": ""},
)
- ],
+ )
+
+ with patch(
+ "homeassistant.components.ssdp.async_search",
+ side_effect=_inject_entry,
), patch.object(
hass.config_entries.flow, "async_init", return_value=mock_coro()
) as mock_init:
@@ -58,9 +61,14 @@ async def test_scan_match_upnp_devicedesc(hass, aioclient_mock, key):
)
scanner = ssdp.Scanner(hass, {"mock-domain": [{key: "Paulus"}]})
+ async def _inject_entry(*args, **kwargs):
+ scanner.async_store_entry(
+ Mock(st="mock-st", location="http://1.1.1.1", values={})
+ )
+
with patch(
- "netdisco.ssdp.scan",
- return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
+ "homeassistant.components.ssdp.async_search",
+ side_effect=_inject_entry,
), patch.object(
hass.config_entries.flow, "async_init", return_value=mock_coro()
) as mock_init:
@@ -95,9 +103,14 @@ async def test_scan_not_all_present(hass, aioclient_mock):
},
)
+ async def _inject_entry(*args, **kwargs):
+ scanner.async_store_entry(
+ Mock(st="mock-st", location="http://1.1.1.1", values={})
+ )
+
with patch(
- "netdisco.ssdp.scan",
- return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
+ "homeassistant.components.ssdp.async_search",
+ side_effect=_inject_entry,
), patch.object(
hass.config_entries.flow, "async_init", return_value=mock_coro()
) as mock_init:
@@ -131,9 +144,14 @@ async def test_scan_not_all_match(hass, aioclient_mock):
},
)
+ async def _inject_entry(*args, **kwargs):
+ scanner.async_store_entry(
+ Mock(st="mock-st", location="http://1.1.1.1", values={})
+ )
+
with patch(
- "netdisco.ssdp.scan",
- return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
+ "homeassistant.components.ssdp.async_search",
+ side_effect=_inject_entry,
), patch.object(
hass.config_entries.flow, "async_init", return_value=mock_coro()
) as mock_init:
@@ -148,9 +166,14 @@ async def test_scan_description_fetch_fail(hass, aioclient_mock, exc):
aioclient_mock.get("http://1.1.1.1", exc=exc)
scanner = ssdp.Scanner(hass, {})
+ async def _inject_entry(*args, **kwargs):
+ scanner.async_store_entry(
+ Mock(st="mock-st", location="http://1.1.1.1", values={})
+ )
+
with patch(
- "netdisco.ssdp.scan",
- return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
+ "homeassistant.components.ssdp.async_search",
+ side_effect=_inject_entry,
):
await scanner.async_scan(None)
@@ -165,8 +188,61 @@ async def test_scan_description_parse_fail(hass, aioclient_mock):
)
scanner = ssdp.Scanner(hass, {})
+ async def _inject_entry(*args, **kwargs):
+ scanner.async_store_entry(
+ Mock(st="mock-st", location="http://1.1.1.1", values={})
+ )
+
with patch(
- "netdisco.ssdp.scan",
- return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
+ "homeassistant.components.ssdp.async_search",
+ side_effect=_inject_entry,
):
await scanner.async_scan(None)
+
+
+async def test_invalid_characters(hass, aioclient_mock):
+ """Test that we replace bad characters with placeholders."""
+ aioclient_mock.get(
+ "http://1.1.1.1",
+ text="""
+
+
+ ABC
+ \xff\xff\xff\xff
+
+
+ """,
+ )
+ scanner = ssdp.Scanner(
+ hass,
+ {
+ "mock-domain": [
+ {
+ ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC",
+ }
+ ]
+ },
+ )
+
+ async def _inject_entry(*args, **kwargs):
+ scanner.async_store_entry(
+ Mock(st="mock-st", location="http://1.1.1.1", values={})
+ )
+
+ with patch(
+ "homeassistant.components.ssdp.async_search",
+ side_effect=_inject_entry,
+ ), patch.object(
+ hass.config_entries.flow, "async_init", return_value=mock_coro()
+ ) as mock_init:
+ await scanner.async_scan(None)
+
+ assert len(mock_init.mock_calls) == 1
+ assert mock_init.mock_calls[0][1][0] == "mock-domain"
+ assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"}
+ assert mock_init.mock_calls[0][2]["data"] == {
+ "ssdp_location": "http://1.1.1.1",
+ "ssdp_st": "mock-st",
+ "deviceType": "ABC",
+ "serialNumber": "ÿÿÿÿ",
+ }
diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py
index b6bebcfeeda46b..60de732cf7938f 100644
--- a/tests/components/statistics/test_sensor.py
+++ b/tests/components/statistics/test_sensor.py
@@ -115,7 +115,7 @@ def test_sensor_source(self):
assert self.mean == state.attributes.get("mean")
assert self.count == state.attributes.get("count")
assert self.total == state.attributes.get("total")
- assert TEMP_CELSIUS == state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
assert self.change == state.attributes.get("change")
assert self.average_change == state.attributes.get("average_change")
@@ -146,8 +146,8 @@ def test_sampling_size(self):
state = self.hass.states.get("sensor.test")
- assert 3.8 == state.attributes.get("min_value")
- assert 14 == state.attributes.get("max_value")
+ assert state.attributes.get("min_value") == 3.8
+ assert state.attributes.get("max_value") == 14
def test_sampling_size_1(self):
"""Test validity of stats requiring only one sample."""
@@ -182,12 +182,12 @@ def test_sampling_size_1(self):
assert self.values[-1] == state.attributes.get("mean")
assert self.values[-1] == state.attributes.get("median")
assert self.values[-1] == state.attributes.get("total")
- assert 0 == state.attributes.get("change")
- assert 0 == state.attributes.get("average_change")
+ assert state.attributes.get("change") == 0
+ assert state.attributes.get("average_change") == 0
# require at least two data points
- assert STATE_UNKNOWN == state.attributes.get("variance")
- assert STATE_UNKNOWN == state.attributes.get("standard_deviation")
+ assert state.attributes.get("variance") == STATE_UNKNOWN
+ assert state.attributes.get("standard_deviation") == STATE_UNKNOWN
def test_max_age(self):
"""Test value deprecation."""
@@ -231,8 +231,8 @@ def mock_now():
state = self.hass.states.get("sensor.test")
- assert 6 == state.attributes.get("min_value")
- assert 14 == state.attributes.get("max_value")
+ assert state.attributes.get("min_value") == 6
+ assert state.attributes.get("max_value") == 14
def test_max_age_without_sensor_change(self):
"""Test value deprecation."""
@@ -276,8 +276,8 @@ def mock_now():
state = self.hass.states.get("sensor.test")
- assert 3.8 == state.attributes.get("min_value")
- assert 15.2 == state.attributes.get("max_value")
+ assert state.attributes.get("min_value") == 3.8
+ assert state.attributes.get("max_value") == 15.2
# wait for 3 minutes (max_age).
mock_data["return_time"] += timedelta(minutes=3)
diff --git a/tests/components/statsd/test_init.py b/tests/components/statsd/test_init.py
index 17e49b09951c48..a0e5fd51669d41 100644
--- a/tests/components/statsd/test_init.py
+++ b/tests/components/statsd/test_init.py
@@ -39,7 +39,7 @@ async def test_statsd_setup_full(hass):
assert mock_init.call_args == mock.call(host="host", port=123, prefix="foo")
assert hass.bus.listen.called
- assert EVENT_STATE_CHANGED == hass.bus.listen.call_args_list[0][0][0]
+ assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED
async def test_statsd_setup_defaults(hass):
diff --git a/tests/components/stream/common.py b/tests/components/stream/common.py
index c99cdef79841f5..4c6841d03dbc86 100644
--- a/tests/components/stream/common.py
+++ b/tests/components/stream/common.py
@@ -5,9 +5,6 @@
import av
import numpy as np
-from homeassistant.components.stream import Stream
-from homeassistant.components.stream.const import ATTR_STREAMS, DOMAIN
-
AUDIO_SAMPLE_RATE = 8000
@@ -43,6 +40,7 @@ def generate_audio_frame(pcm_mulaw=False):
stream.width = 480
stream.height = 320
stream.pix_fmt = "yuv420p"
+ stream.options.update({"g": str(fps), "keyint_min": str(fps)})
a_packet = None
last_a_dts = -1
@@ -93,10 +91,3 @@ def generate_audio_frame(pcm_mulaw=False):
output.seek(0)
return output
-
-
-def preload_stream(hass, stream_source):
- """Preload a stream for use in tests."""
- stream = Stream(hass, stream_source)
- hass.data[DOMAIN][ATTR_STREAMS][stream_source] = stream
- return stream
diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py
index 1b2f0645f9b77e..ead2018b528553 100644
--- a/tests/components/stream/conftest.py
+++ b/tests/components/stream/conftest.py
@@ -9,14 +9,13 @@
allows the tests to pause the worker thread before finalizing the stream
so that it can inspect the output.
"""
-
import logging
import threading
from unittest.mock import patch
import pytest
-from homeassistant.components.stream.core import Segment, StreamOutput
+from homeassistant.components.stream import Stream
class WorkerSync:
@@ -25,7 +24,7 @@ class WorkerSync:
def __init__(self):
"""Initialize WorkerSync."""
self._event = None
- self._put_original = StreamOutput.put
+ self._original = Stream._worker_finished
def pause(self):
"""Pause the worker before it finalizes the stream."""
@@ -33,19 +32,20 @@ def pause(self):
def resume(self):
"""Allow the worker thread to finalize the stream."""
+ logging.debug("waking blocked worker")
self._event.set()
- def blocking_put(self, stream_output: StreamOutput, segment: Segment):
- """Proxy StreamOutput.put, intercepted for test to pause worker."""
- if segment is None and self._event:
- # Worker is ending the stream, which clears all output buffers.
- # Block the worker thread until the test has a chance to verify
- # the segments under test.
- logging.error("blocking worker")
+ def blocking_finish(self, stream: Stream):
+ """Intercept call to pause stream worker."""
+ # Worker is ending the stream, which clears all output buffers.
+ # Block the worker thread until the test has a chance to verify
+ # the segments under test.
+ logging.debug("blocking worker")
+ if self._event:
self._event.wait()
- # Forward to actual StreamOutput.put
- self._put_original(stream_output, segment)
+ # Forward to actual implementation
+ self._original(stream)
@pytest.fixture()
@@ -53,8 +53,8 @@ def stream_worker_sync(hass):
"""Patch StreamOutput to allow test to synchronize worker stream end."""
sync = WorkerSync()
with patch(
- "homeassistant.components.stream.core.StreamOutput.put",
- side_effect=sync.blocking_put,
+ "homeassistant.components.stream.Stream._worker_finished",
+ side_effect=sync.blocking_finish,
autospec=True,
):
yield sync
diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py
index 790222b1630670..ab0c21efdfb3d6 100644
--- a/tests/components/stream/test_hls.py
+++ b/tests/components/stream/test_hls.py
@@ -1,20 +1,83 @@
"""The tests for hls streams."""
from datetime import timedelta
+import io
from unittest.mock import patch
from urllib.parse import urlparse
import av
+import pytest
-from homeassistant.components.stream import request_stream
+from homeassistant.components.stream import create_stream
+from homeassistant.components.stream.const import MAX_SEGMENTS, NUM_PLAYLIST_SEGMENTS
+from homeassistant.components.stream.core import Segment
from homeassistant.const import HTTP_NOT_FOUND
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed
-from tests.components.stream.common import generate_h264_video, preload_stream
+from tests.components.stream.common import generate_h264_video
+STREAM_SOURCE = "some-stream-source"
+SEQUENCE_BYTES = io.BytesIO(b"some-bytes")
+DURATION = 10
+TEST_TIMEOUT = 5.0 # Lower than 9s home assistant timeout
+MAX_ABORT_SEGMENTS = 20 # Abort test to avoid looping forever
-async def test_hls_stream(hass, hass_client, stream_worker_sync):
+
+class HlsClient:
+ """Test fixture for fetching the hls stream."""
+
+ def __init__(self, http_client, parsed_url):
+ """Initialize HlsClient."""
+ self.http_client = http_client
+ self.parsed_url = parsed_url
+
+ async def get(self, path=None):
+ """Fetch the hls stream for the specified path."""
+ url = self.parsed_url.path
+ if path:
+ # Strip off the master playlist suffix and replace with path
+ url = "/".join(self.parsed_url.path.split("/")[:-1]) + path
+ return await self.http_client.get(url)
+
+
+@pytest.fixture
+def hls_stream(hass, hass_client):
+ """Create test fixture for creating an HLS client for a stream."""
+
+ async def create_client_for_stream(stream):
+ http_client = await hass_client()
+ parsed_url = urlparse(stream.endpoint_url("hls"))
+ return HlsClient(http_client, parsed_url)
+
+ return create_client_for_stream
+
+
+def make_segment(segment, discontinuity=False):
+ """Create a playlist response for a segment."""
+ response = []
+ if discontinuity:
+ response.append("#EXT-X-DISCONTINUITY")
+ response.extend(["#EXTINF:10.0000,", f"./segment/{segment}.m4s"]),
+ return "\n".join(response)
+
+
+def make_playlist(sequence, discontinuity_sequence=0, segments=[]):
+ """Create a an hls playlist response for tests to assert on."""
+ response = [
+ "#EXTM3U",
+ "#EXT-X-VERSION:7",
+ "#EXT-X-TARGETDURATION:10",
+ '#EXT-X-MAP:URI="init.mp4"',
+ f"#EXT-X-MEDIA-SEQUENCE:{sequence}",
+ f"#EXT-X-DISCONTINUITY-SEQUENCE:{discontinuity_sequence}",
+ ]
+ response.extend(segments)
+ response.append("")
+ return "\n".join(response)
+
+
+async def test_hls_stream(hass, hls_stream, stream_worker_sync):
"""
Test hls stream.
@@ -27,31 +90,27 @@ async def test_hls_stream(hass, hass_client, stream_worker_sync):
# Setup demo HLS track
source = generate_h264_video()
- stream = preload_stream(hass, source)
- stream.add_provider("hls")
+ stream = create_stream(hass, source)
# Request stream
- url = request_stream(hass, source)
+ stream.add_provider("hls")
+ stream.start()
- http_client = await hass_client()
+ hls_client = await hls_stream(stream)
# Fetch playlist
- parsed_url = urlparse(url)
- playlist_response = await http_client.get(parsed_url.path)
+ playlist_response = await hls_client.get()
assert playlist_response.status == 200
# Fetch init
playlist = await playlist_response.text()
- playlist_url = "/".join(parsed_url.path.split("/")[:-1])
- init_url = playlist_url + "/init.mp4"
- init_response = await http_client.get(init_url)
+ init_response = await hls_client.get("/init.mp4")
assert init_response.status == 200
# Fetch segment
playlist = await playlist_response.text()
- playlist_url = "/".join(parsed_url.path.split("/")[:-1])
- segment_url = playlist_url + "/" + playlist.splitlines()[-1]
- segment_response = await http_client.get(segment_url)
+ segment_url = "/" + playlist.splitlines()[-1]
+ segment_response = await hls_client.get(segment_url)
assert segment_response.status == 200
stream_worker_sync.resume()
@@ -60,7 +119,7 @@ async def test_hls_stream(hass, hass_client, stream_worker_sync):
stream.stop()
# Ensure playlist not accessible after stream ends
- fail_response = await http_client.get(parsed_url.path)
+ fail_response = await hls_client.get()
assert fail_response.status == HTTP_NOT_FOUND
@@ -72,11 +131,12 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync):
# Setup demo HLS track
source = generate_h264_video()
- stream = preload_stream(hass, source)
- stream.add_provider("hls")
+ stream = create_stream(hass, source)
# Request stream
- url = request_stream(hass, source)
+ stream.add_provider("hls")
+ stream.start()
+ url = stream.endpoint_url("hls")
http_client = await hass_client()
@@ -98,42 +158,36 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync):
# Wait 5 minutes
future = dt_util.utcnow() + timedelta(minutes=5)
async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
# Ensure playlist not accessible
fail_response = await http_client.get(parsed_url.path)
assert fail_response.status == HTTP_NOT_FOUND
-async def test_stream_ended(hass, stream_worker_sync):
- """Test hls stream packets ended."""
+async def test_stream_timeout_after_stop(hass, hass_client, stream_worker_sync):
+ """Test hls stream timeout after the stream has been stopped already."""
await async_setup_component(hass, "stream", {"stream": {}})
stream_worker_sync.pause()
# Setup demo HLS track
source = generate_h264_video()
- stream = preload_stream(hass, source)
- track = stream.add_provider("hls")
+ stream = create_stream(hass, source)
# Request stream
- request_stream(hass, source)
-
- # Run it dead
- while True:
- segment = await track.recv()
- if segment is None:
- break
- segments = segment.sequence
- # Allow worker to finalize once enough of the stream is been consumed
- if segments > 1:
- stream_worker_sync.resume()
-
- assert segments > 1
- assert not track.get_segment()
+ stream.add_provider("hls")
+ stream.start()
- # Stop stream, if it hasn't quit already
+ stream_worker_sync.resume()
stream.stop()
+ # Wait 5 minutes and fire callback. Stream should already have been
+ # stopped so this is a no-op.
+ future = dt_util.utcnow() + timedelta(minutes=5)
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+
async def test_stream_keepalive(hass):
"""Test hls stream retries the stream when keepalive=True."""
@@ -141,7 +195,7 @@ async def test_stream_keepalive(hass):
# Setup demo HLS track
source = "test_stream_keepalive_source"
- stream = preload_stream(hass, source)
+ stream = create_stream(hass, source)
track = stream.add_provider("hls")
track.num_segments = 2
@@ -155,17 +209,173 @@ def time_side_effect():
return cur_time
with patch("av.open") as av_open, patch(
- "homeassistant.components.stream.worker.time"
+ "homeassistant.components.stream.time"
) as mock_time, patch(
- "homeassistant.components.stream.worker.STREAM_RESTART_INCREMENT", 0
+ "homeassistant.components.stream.STREAM_RESTART_INCREMENT", 0
):
av_open.side_effect = av.error.InvalidDataError(-2, "error")
mock_time.time.side_effect = time_side_effect
# Request stream
- request_stream(hass, source, keepalive=True)
+ stream.keepalive = True
+ stream.start()
stream._thread.join()
stream._thread = None
assert av_open.call_count == 2
# Stop stream, if it hasn't quit already
stream.stop()
+
+
+async def test_hls_playlist_view_no_output(hass, hass_client, hls_stream):
+ """Test rendering the hls playlist with no output segments."""
+ await async_setup_component(hass, "stream", {"stream": {}})
+
+ stream = create_stream(hass, STREAM_SOURCE)
+ stream.add_provider("hls")
+
+ hls_client = await hls_stream(stream)
+
+ # Fetch playlist
+ resp = await hls_client.get("/playlist.m3u8")
+ assert resp.status == 404
+
+
+async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync):
+ """Test rendering the hls playlist with 1 and 2 output segments."""
+ await async_setup_component(hass, "stream", {"stream": {}})
+
+ stream = create_stream(hass, STREAM_SOURCE)
+ stream_worker_sync.pause()
+ hls = stream.add_provider("hls")
+
+ hls.put(Segment(1, SEQUENCE_BYTES, DURATION))
+ await hass.async_block_till_done()
+
+ hls_client = await hls_stream(stream)
+
+ resp = await hls_client.get("/playlist.m3u8")
+ assert resp.status == 200
+ assert await resp.text() == make_playlist(sequence=1, segments=[make_segment(1)])
+
+ hls.put(Segment(2, SEQUENCE_BYTES, DURATION))
+ await hass.async_block_till_done()
+ resp = await hls_client.get("/playlist.m3u8")
+ assert resp.status == 200
+ assert await resp.text() == make_playlist(
+ sequence=1, segments=[make_segment(1), make_segment(2)]
+ )
+
+ stream_worker_sync.resume()
+ stream.stop()
+
+
+async def test_hls_max_segments(hass, hls_stream, stream_worker_sync):
+ """Test rendering the hls playlist with more segments than the segment deque can hold."""
+ await async_setup_component(hass, "stream", {"stream": {}})
+
+ stream = create_stream(hass, STREAM_SOURCE)
+ stream_worker_sync.pause()
+ hls = stream.add_provider("hls")
+
+ hls_client = await hls_stream(stream)
+
+ # Produce enough segments to overfill the output buffer by one
+ for sequence in range(1, MAX_SEGMENTS + 2):
+ hls.put(Segment(sequence, SEQUENCE_BYTES, DURATION))
+ await hass.async_block_till_done()
+
+ resp = await hls_client.get("/playlist.m3u8")
+ assert resp.status == 200
+
+ # Only NUM_PLAYLIST_SEGMENTS are returned in the playlist.
+ start = MAX_SEGMENTS + 2 - NUM_PLAYLIST_SEGMENTS
+ segments = []
+ for sequence in range(start, MAX_SEGMENTS + 2):
+ segments.append(make_segment(sequence))
+ assert await resp.text() == make_playlist(
+ sequence=start,
+ segments=segments,
+ )
+
+ # Fetch the actual segments with a fake byte payload
+ with patch(
+ "homeassistant.components.stream.hls.get_m4s", return_value=b"fake-payload"
+ ):
+ # The segment that fell off the buffer is not accessible
+ segment_response = await hls_client.get("/segment/1.m4s")
+ assert segment_response.status == 404
+
+ # However all segments in the buffer are accessible, even those that were not in the playlist.
+ for sequence in range(2, MAX_SEGMENTS + 2):
+ segment_response = await hls_client.get(f"/segment/{sequence}.m4s")
+ assert segment_response.status == 200
+
+ stream_worker_sync.resume()
+ stream.stop()
+
+
+async def test_hls_playlist_view_discontinuity(hass, hls_stream, stream_worker_sync):
+ """Test a discontinuity across segments in the stream with 3 segments."""
+ await async_setup_component(hass, "stream", {"stream": {}})
+
+ stream = create_stream(hass, STREAM_SOURCE)
+ stream_worker_sync.pause()
+ hls = stream.add_provider("hls")
+
+ hls.put(Segment(1, SEQUENCE_BYTES, DURATION, stream_id=0))
+ hls.put(Segment(2, SEQUENCE_BYTES, DURATION, stream_id=0))
+ hls.put(Segment(3, SEQUENCE_BYTES, DURATION, stream_id=1))
+ await hass.async_block_till_done()
+
+ hls_client = await hls_stream(stream)
+
+ resp = await hls_client.get("/playlist.m3u8")
+ assert resp.status == 200
+ assert await resp.text() == make_playlist(
+ sequence=1,
+ segments=[
+ make_segment(1),
+ make_segment(2),
+ make_segment(3, discontinuity=True),
+ ],
+ )
+
+ stream_worker_sync.resume()
+ stream.stop()
+
+
+async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sync):
+ """Test a discontinuity with more segments than the segment deque can hold."""
+ await async_setup_component(hass, "stream", {"stream": {}})
+
+ stream = create_stream(hass, STREAM_SOURCE)
+ stream_worker_sync.pause()
+ hls = stream.add_provider("hls")
+
+ hls_client = await hls_stream(stream)
+
+ hls.put(Segment(1, SEQUENCE_BYTES, DURATION, stream_id=0))
+
+ # Produce enough segments to overfill the output buffer by one
+ for sequence in range(1, MAX_SEGMENTS + 2):
+ hls.put(Segment(sequence, SEQUENCE_BYTES, DURATION, stream_id=1))
+ await hass.async_block_till_done()
+
+ resp = await hls_client.get("/playlist.m3u8")
+ assert resp.status == 200
+
+ # Only NUM_PLAYLIST_SEGMENTS are returned in the playlist causing the
+ # EXT-X-DISCONTINUITY tag to be omitted and EXT-X-DISCONTINUITY-SEQUENCE
+ # returned instead.
+ start = MAX_SEGMENTS + 2 - NUM_PLAYLIST_SEGMENTS
+ segments = []
+ for sequence in range(start, MAX_SEGMENTS + 2):
+ segments.append(make_segment(sequence))
+ assert await resp.text() == make_playlist(
+ sequence=start,
+ discontinuity_sequence=1,
+ segments=segments,
+ )
+
+ stream_worker_sync.resume()
+ stream.stop()
diff --git a/tests/components/stream/test_init.py b/tests/components/stream/test_init.py
deleted file mode 100644
index 1515ff1a490bad..00000000000000
--- a/tests/components/stream/test_init.py
+++ /dev/null
@@ -1,84 +0,0 @@
-"""The tests for stream."""
-from unittest.mock import AsyncMock, MagicMock, patch
-
-import pytest
-
-from homeassistant.components.stream.const import (
- ATTR_STREAMS,
- CONF_LOOKBACK,
- CONF_STREAM_SOURCE,
- DOMAIN,
- SERVICE_RECORD,
-)
-from homeassistant.const import CONF_FILENAME
-from homeassistant.exceptions import HomeAssistantError
-from homeassistant.setup import async_setup_component
-
-
-async def test_record_service_invalid_file(hass):
- """Test record service call with invalid file."""
- await async_setup_component(hass, "stream", {"stream": {}})
- data = {CONF_STREAM_SOURCE: "rtsp://my.video", CONF_FILENAME: "/my/invalid/path"}
- with pytest.raises(HomeAssistantError):
- await hass.services.async_call(DOMAIN, SERVICE_RECORD, data, blocking=True)
-
-
-async def test_record_service_init_stream(hass):
- """Test record service call with invalid file."""
- await async_setup_component(hass, "stream", {"stream": {}})
- data = {CONF_STREAM_SOURCE: "rtsp://my.video", CONF_FILENAME: "/my/invalid/path"}
- with patch("homeassistant.components.stream.Stream") as stream_mock, patch.object(
- hass.config, "is_allowed_path", return_value=True
- ):
- # Setup stubs
- stream_mock.return_value.outputs = {}
-
- # Call Service
- await hass.services.async_call(DOMAIN, SERVICE_RECORD, data, blocking=True)
-
- # Assert
- assert stream_mock.called
-
-
-async def test_record_service_existing_record_session(hass):
- """Test record service call with invalid file."""
- await async_setup_component(hass, "stream", {"stream": {}})
- source = "rtsp://my.video"
- data = {CONF_STREAM_SOURCE: source, CONF_FILENAME: "/my/invalid/path"}
-
- # Setup stubs
- stream_mock = MagicMock()
- stream_mock.return_value.outputs = {"recorder": MagicMock()}
- hass.data[DOMAIN][ATTR_STREAMS][source] = stream_mock
-
- with patch.object(hass.config, "is_allowed_path", return_value=True), pytest.raises(
- HomeAssistantError
- ):
- # Call Service
- await hass.services.async_call(DOMAIN, SERVICE_RECORD, data, blocking=True)
-
-
-async def test_record_service_lookback(hass):
- """Test record service call with invalid file."""
- await async_setup_component(hass, "stream", {"stream": {}})
- data = {
- CONF_STREAM_SOURCE: "rtsp://my.video",
- CONF_FILENAME: "/my/invalid/path",
- CONF_LOOKBACK: 4,
- }
-
- with patch("homeassistant.components.stream.Stream") as stream_mock, patch.object(
- hass.config, "is_allowed_path", return_value=True
- ):
- # Setup stubs
- hls_mock = MagicMock()
- hls_mock.target_duration = 2
- hls_mock.recv = AsyncMock(return_value=None)
- stream_mock.return_value.outputs = {"hls": hls_mock}
-
- # Call Service
- await hass.services.async_call(DOMAIN, SERVICE_RECORD, data, blocking=True)
-
- assert stream_mock.called
- stream_mock.return_value.add_provider.assert_called_once_with("recorder")
- assert hls_mock.recv.called
diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py
index 1b46738c8f2c16..5ee055754b9870 100644
--- a/tests/components/stream/test_recorder.py
+++ b/tests/components/stream/test_recorder.py
@@ -1,22 +1,28 @@
"""The tests for hls streams."""
+import asyncio
from datetime import timedelta
import logging
import os
import threading
+from typing import Deque
from unittest.mock import patch
+import async_timeout
import av
import pytest
+from homeassistant.components.stream import create_stream
from homeassistant.components.stream.core import Segment
from homeassistant.components.stream.recorder import recorder_save_worker
+from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed
-from tests.components.stream.common import generate_h264_video, preload_stream
+from tests.components.stream.common import generate_h264_video
-TEST_TIMEOUT = 10
+TEST_TIMEOUT = 7.0 # Lower than 9s home assistant timeout
+MAX_ABORT_SEGMENTS = 20 # Abort test to avoid looping forever
class SaveRecordWorkerSync:
@@ -30,23 +36,33 @@ class SaveRecordWorkerSync:
def __init__(self):
"""Initialize SaveRecordWorkerSync."""
self.reset()
+ self._segments = None
- def recorder_save_worker(self, *args, **kwargs):
+ def recorder_save_worker(self, file_out: str, segments: Deque[Segment]):
"""Mock method for patch."""
logging.debug("recorder_save_worker thread started")
assert self._save_thread is None
+ self._segments = segments
self._save_thread = threading.current_thread()
self._save_event.set()
- def join(self):
+ async def get_segments(self):
+ """Return the recorded video segments."""
+ with async_timeout.timeout(TEST_TIMEOUT):
+ await self._save_event.wait()
+ return self._segments
+
+ async def join(self):
"""Verify save worker was invoked and block on shutdown."""
- assert self._save_event.wait(timeout=TEST_TIMEOUT)
- self._save_thread.join()
+ with async_timeout.timeout(TEST_TIMEOUT):
+ await self._save_event.wait()
+ self._save_thread.join(timeout=TEST_TIMEOUT)
+ assert not self._save_thread.is_alive()
def reset(self):
"""Reset callback state for reuse in tests."""
self._save_thread = None
- self._save_event = threading.Event()
+ self._save_event = asyncio.Event()
@pytest.fixture()
@@ -61,7 +77,7 @@ def record_worker_sync(hass):
yield sync
-async def test_record_stream(hass, hass_client, stream_worker_sync, record_worker_sync):
+async def test_record_stream(hass, hass_client, record_worker_sync):
"""
Test record stream.
@@ -71,28 +87,42 @@ async def test_record_stream(hass, hass_client, stream_worker_sync, record_worke
"""
await async_setup_component(hass, "stream", {"stream": {}})
- stream_worker_sync.pause()
-
# Setup demo track
source = generate_h264_video()
- stream = preload_stream(hass, source)
- recorder = stream.add_provider("recorder")
- stream.start()
+ stream = create_stream(hass, source)
+ with patch.object(hass.config, "is_allowed_path", return_value=True):
+ await stream.async_record("/example/path")
- while True:
- segment = await recorder.recv()
- if not segment:
- break
- segments = segment.sequence
- if segments > 1:
- stream_worker_sync.resume()
-
- stream.stop()
- assert segments > 1
+ # After stream decoding finishes, the record worker thread starts
+ segments = await record_worker_sync.get_segments()
+ assert len(segments) >= 1
# Verify that the save worker was invoked, then block until its
# thread completes and is shutdown completely to avoid thread leaks.
- record_worker_sync.join()
+ await record_worker_sync.join()
+
+ stream.stop()
+
+
+async def test_record_lookback(
+ hass, hass_client, stream_worker_sync, record_worker_sync
+):
+ """Exercise record with loopback."""
+ await async_setup_component(hass, "stream", {"stream": {}})
+
+ source = generate_h264_video()
+ stream = create_stream(hass, source)
+
+ # Start an HLS feed to enable lookback
+ stream.add_provider("hls")
+ stream.start()
+
+ with patch.object(hass.config, "is_allowed_path", return_value=True):
+ await stream.async_record("/example/path", lookback=4)
+
+ # This test does not need recorder cleanup since it is not fully exercised
+
+ stream.stop()
async def test_recorder_timeout(hass, hass_client, stream_worker_sync):
@@ -106,14 +136,14 @@ async def test_recorder_timeout(hass, hass_client, stream_worker_sync):
stream_worker_sync.pause()
- with patch(
- "homeassistant.components.stream.recorder.RecorderOutput.cleanup"
- ) as mock_cleanup:
+ with patch("homeassistant.components.stream.IdleTimer.fire") as mock_timeout:
# Setup demo track
source = generate_h264_video()
- stream = preload_stream(hass, source)
+
+ stream = create_stream(hass, source)
+ with patch.object(hass.config, "is_allowed_path", return_value=True):
+ await stream.async_record("/example/path")
recorder = stream.add_provider("recorder")
- stream.start()
await recorder.recv()
@@ -122,7 +152,7 @@ async def test_recorder_timeout(hass, hass_client, stream_worker_sync):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
- assert mock_cleanup.called
+ assert mock_timeout.called
stream_worker_sync.resume()
stream.stop()
@@ -130,6 +160,19 @@ async def test_recorder_timeout(hass, hass_client, stream_worker_sync):
await hass.async_block_till_done()
+async def test_record_path_not_allowed(hass, hass_client):
+ """Test where the output path is not allowed by home assistant configuration."""
+ await async_setup_component(hass, "stream", {"stream": {}})
+
+ # Setup demo track
+ source = generate_h264_video()
+ stream = create_stream(hass, source)
+ with patch.object(
+ hass.config, "is_allowed_path", return_value=False
+ ), pytest.raises(HomeAssistantError):
+ await stream.async_record("/example/path")
+
+
async def test_recorder_save(tmpdir):
"""Test recorder save."""
# Setup
@@ -137,12 +180,37 @@ async def test_recorder_save(tmpdir):
filename = f"{tmpdir}/test.mp4"
# Run
- recorder_save_worker(filename, [Segment(1, source, 4)], "mp4")
+ recorder_save_worker(filename, [Segment(1, source, 4)])
# Assert
assert os.path.exists(filename)
+async def test_recorder_discontinuity(tmpdir):
+ """Test recorder save across a discontinuity."""
+ # Setup
+ source = generate_h264_video()
+ filename = f"{tmpdir}/test.mp4"
+
+ # Run
+ recorder_save_worker(filename, [Segment(1, source, 4, 0), Segment(2, source, 4, 1)])
+
+ # Assert
+ assert os.path.exists(filename)
+
+
+async def test_recorder_no_segements(tmpdir):
+ """Test recorder behavior with a stream failure which causes no segments."""
+ # Setup
+ filename = f"{tmpdir}/test.mp4"
+
+ # Run
+ recorder_save_worker("unused-file", [])
+
+ # Assert
+ assert not os.path.exists(filename)
+
+
async def test_record_stream_audio(
hass, hass_client, stream_worker_sync, record_worker_sync
):
@@ -167,9 +235,10 @@ async def test_record_stream_audio(
source = generate_h264_video(
container_format="mov", audio_codec=a_codec
) # mov can store PCM
- stream = preload_stream(hass, source)
+ stream = create_stream(hass, source)
+ with patch.object(hass.config, "is_allowed_path", return_value=True):
+ await stream.async_record("/example/path")
recorder = stream.add_provider("recorder")
- stream.start()
while True:
segment = await recorder.recv()
@@ -187,4 +256,14 @@ async def test_record_stream_audio(
# Verify that the save worker was invoked, then block until its
# thread completes and is shutdown completely to avoid thread leaks.
- record_worker_sync.join()
+ await record_worker_sync.join()
+
+
+async def test_recorder_log(hass, caplog):
+ """Test starting a stream to record logs the url without username and password."""
+ await async_setup_component(hass, "stream", {"stream": {}})
+ stream = create_stream(hass, "https://abcd:efgh@foo.bar")
+ with patch.object(hass.config, "is_allowed_path", return_value=True):
+ await stream.async_record("/example/path")
+ assert "https://abcd:efgh@foo.bar" not in caplog.text
+ assert "https://****:****@foo.bar" in caplog.text
diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py
new file mode 100644
index 00000000000000..d5527105a704c0
--- /dev/null
+++ b/tests/components/stream/test_worker.py
@@ -0,0 +1,591 @@
+"""Test the stream worker corner cases.
+
+Exercise the stream worker functionality by mocking av.open calls to return a
+fake media container as well a fake decoded stream in the form of a series of
+packets. This is needed as some of these cases can't be encoded using pyav. It
+is preferred to use test_hls.py for example, when possible.
+
+The worker opens the stream source (typically a URL) and gets back a
+container that has audio/video streams. The worker iterates over the sequence
+of packets and sends them to the appropriate output buffers. Each test
+creates a packet sequence, with a mocked output buffer to capture the segments
+pushed to the output streams. The packet sequence can be used to exercise
+failure modes or corner cases like how out of order packets are handled.
+"""
+
+import fractions
+import io
+import math
+import threading
+from unittest.mock import patch
+
+import av
+
+from homeassistant.components.stream import Stream
+from homeassistant.components.stream.const import (
+ MAX_MISSING_DTS,
+ MIN_SEGMENT_DURATION,
+ PACKETS_TO_WAIT_FOR_AUDIO,
+)
+from homeassistant.components.stream.worker import SegmentBuffer, stream_worker
+
+STREAM_SOURCE = "some-stream-source"
+# Formats here are arbitrary, not exercised by tests
+STREAM_OUTPUT_FORMAT = "hls"
+AUDIO_STREAM_FORMAT = "mp3"
+VIDEO_STREAM_FORMAT = "h264"
+VIDEO_FRAME_RATE = 12
+AUDIO_SAMPLE_RATE = 11025
+PACKET_DURATION = fractions.Fraction(1, VIDEO_FRAME_RATE) # in seconds
+SEGMENT_DURATION = (
+ math.ceil(MIN_SEGMENT_DURATION / PACKET_DURATION) * PACKET_DURATION
+) # in seconds
+TEST_SEQUENCE_LENGTH = 5 * VIDEO_FRAME_RATE
+LONGER_TEST_SEQUENCE_LENGTH = 20 * VIDEO_FRAME_RATE
+OUT_OF_ORDER_PACKET_INDEX = 3 * VIDEO_FRAME_RATE
+PACKETS_PER_SEGMENT = SEGMENT_DURATION / PACKET_DURATION
+SEGMENTS_PER_PACKET = PACKET_DURATION / SEGMENT_DURATION
+TIMEOUT = 15
+
+
+class FakePyAvStream:
+ """A fake pyav Stream."""
+
+ def __init__(self, name, rate):
+ """Initialize the stream."""
+ self.name = name
+ self.time_base = fractions.Fraction(1, rate)
+ self.profile = "ignored-profile"
+
+ class FakeCodec:
+ name = "aac"
+
+ self.codec = FakeCodec()
+
+
+VIDEO_STREAM = FakePyAvStream(VIDEO_STREAM_FORMAT, VIDEO_FRAME_RATE)
+AUDIO_STREAM = FakePyAvStream(AUDIO_STREAM_FORMAT, AUDIO_SAMPLE_RATE)
+
+
+class PacketSequence:
+ """Creates packets in a sequence for exercising stream worker behavior.
+
+ A test can create a PacketSequence(N) that will raise a StopIteration after
+ N packets. Each packet has an arbitrary monotomically increasing dts/pts value
+ that is parseable by the worker, but a test can manipulate the values to
+ exercise corner cases.
+ """
+
+ def __init__(self, num_packets):
+ """Initialize the sequence with the number of packets it provides."""
+ self.packet = 0
+ self.num_packets = num_packets
+
+ def __iter__(self):
+ """Reset the sequence."""
+ self.packet = 0
+ return self
+
+ def __next__(self):
+ """Return the next packet."""
+ if self.packet >= self.num_packets:
+ raise StopIteration
+ self.packet += 1
+
+ class FakePacket(bytearray):
+ # Be a bytearray so that memoryview works
+ def __init__(self):
+ super().__init__(3)
+
+ time_base = fractions.Fraction(1, VIDEO_FRAME_RATE)
+ dts = self.packet * PACKET_DURATION / time_base
+ pts = self.packet * PACKET_DURATION / time_base
+ duration = PACKET_DURATION / time_base
+ stream = VIDEO_STREAM
+ is_keyframe = True
+ size = 3
+
+ return FakePacket()
+
+
+class FakePyAvContainer:
+ """A fake container returned by mock av.open for a stream."""
+
+ def __init__(self, video_stream, audio_stream):
+ """Initialize the fake container."""
+ # Tests can override this to trigger different worker behavior
+ self.packets = PacketSequence(0)
+
+ class FakePyAvStreams:
+ video = [video_stream] if video_stream else []
+ audio = [audio_stream] if audio_stream else []
+
+ self.streams = FakePyAvStreams()
+
+ class FakePyAvFormat:
+ name = "ignored-format"
+
+ self.format = FakePyAvFormat()
+
+ def demux(self, streams):
+ """Decode the streams from container, and return a packet sequence."""
+ return self.packets
+
+ def close(self):
+ """Close the container."""
+ return
+
+
+class FakePyAvBuffer:
+ """Holds outputs of the decoded stream for tests to assert on results."""
+
+ def __init__(self):
+ """Initialize the FakePyAvBuffer."""
+ self.segments = []
+ self.audio_packets = []
+ self.video_packets = []
+
+ def add_stream(self, template=None):
+ """Create an output buffer that captures packets for test to examine."""
+
+ class FakeStream:
+ def __init__(self, capture_packets):
+ self.capture_packets = capture_packets
+
+ def close(self):
+ return
+
+ def mux(self, packet):
+ self.capture_packets.append(packet)
+
+ if template.name == AUDIO_STREAM_FORMAT:
+ return FakeStream(self.audio_packets)
+ return FakeStream(self.video_packets)
+
+ def mux(self, packet):
+ """Capture a packet for tests to examine."""
+ # Forward to appropriate FakeStream
+ packet.stream.mux(packet)
+
+ def close(self):
+ """Close the buffer."""
+ return
+
+ def capture_output_segment(self, segment):
+ """Capture the output segment for tests to inspect."""
+ self.segments.append(segment)
+
+
+class MockPyAv:
+ """Mocks out av.open."""
+
+ def __init__(self, video=True, audio=False):
+ """Initialize the MockPyAv."""
+ video_stream = VIDEO_STREAM if video else None
+ audio_stream = AUDIO_STREAM if audio else None
+ self.container = FakePyAvContainer(
+ video_stream=video_stream, audio_stream=audio_stream
+ )
+ self.capture_buffer = FakePyAvBuffer()
+
+ def open(self, stream_source, *args, **kwargs):
+ """Return a stream or buffer depending on args."""
+ if isinstance(stream_source, io.BytesIO):
+ return self.capture_buffer
+ return self.container
+
+
+async def async_decode_stream(hass, packets, py_av=None):
+ """Start a stream worker that decodes incoming stream packets into output segments."""
+ stream = Stream(hass, STREAM_SOURCE)
+ stream.add_provider(STREAM_OUTPUT_FORMAT)
+
+ if not py_av:
+ py_av = MockPyAv()
+ py_av.container.packets = packets
+
+ with patch("av.open", new=py_av.open), patch(
+ "homeassistant.components.stream.core.StreamOutput.put",
+ side_effect=py_av.capture_buffer.capture_output_segment,
+ ):
+ segment_buffer = SegmentBuffer(stream.outputs)
+ stream_worker(STREAM_SOURCE, {}, segment_buffer, threading.Event())
+ await hass.async_block_till_done()
+
+ return py_av.capture_buffer
+
+
+async def test_stream_open_fails(hass):
+ """Test failure on stream open."""
+ stream = Stream(hass, STREAM_SOURCE)
+ stream.add_provider(STREAM_OUTPUT_FORMAT)
+ with patch("av.open") as av_open:
+ av_open.side_effect = av.error.InvalidDataError(-2, "error")
+ segment_buffer = SegmentBuffer(stream.outputs)
+ stream_worker(STREAM_SOURCE, {}, segment_buffer, threading.Event())
+ await hass.async_block_till_done()
+ av_open.assert_called_once()
+
+
+async def test_stream_worker_success(hass):
+ """Test a short stream that ends and outputs everything correctly."""
+ decoded_stream = await async_decode_stream(
+ hass, PacketSequence(TEST_SEQUENCE_LENGTH)
+ )
+ segments = decoded_stream.segments
+ # Check number of segments. A segment is only formed when a packet from the next
+ # segment arrives, hence the subtraction of one from the sequence length.
+ assert len(segments) == int((TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET)
+ # Check sequence numbers
+ assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
+ # Check segment durations
+ assert all([s.duration == SEGMENT_DURATION for s in segments])
+ assert len(decoded_stream.video_packets) == TEST_SEQUENCE_LENGTH
+ assert len(decoded_stream.audio_packets) == 0
+
+
+async def test_skip_out_of_order_packet(hass):
+ """Skip a single out of order packet."""
+ packets = list(PacketSequence(TEST_SEQUENCE_LENGTH))
+ # This packet is out of order
+ packets[OUT_OF_ORDER_PACKET_INDEX].dts = -9090
+
+ decoded_stream = await async_decode_stream(hass, iter(packets))
+ segments = decoded_stream.segments
+ # Check sequence numbers
+ assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
+ # If skipped packet would have been the first packet of a segment, the previous
+ # segment will be longer by a packet duration
+ # We also may possibly lose a segment due to the shifting pts boundary
+ if OUT_OF_ORDER_PACKET_INDEX % PACKETS_PER_SEGMENT == 0:
+ # Check duration of affected segment and remove it
+ longer_segment_index = int(
+ (OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET
+ )
+ assert (
+ segments[longer_segment_index].duration
+ == SEGMENT_DURATION + PACKET_DURATION
+ )
+ del segments[longer_segment_index]
+ # Check number of segments
+ assert len(segments) == int((len(packets) - 1 - 1) * SEGMENTS_PER_PACKET - 1)
+ else: # Otherwise segment durations and number of segments are unaffected
+ # Check number of segments
+ assert len(segments) == int((len(packets) - 1) * SEGMENTS_PER_PACKET)
+ # Check remaining segment durations
+ assert all([s.duration == SEGMENT_DURATION for s in segments])
+ assert len(decoded_stream.video_packets) == len(packets) - 1
+ assert len(decoded_stream.audio_packets) == 0
+
+
+async def test_discard_old_packets(hass):
+ """Skip a series of out of order packets."""
+
+ packets = list(PacketSequence(TEST_SEQUENCE_LENGTH))
+ # Packets after this one are considered out of order
+ packets[OUT_OF_ORDER_PACKET_INDEX - 1].dts = 9090
+
+ decoded_stream = await async_decode_stream(hass, iter(packets))
+ segments = decoded_stream.segments
+ # Check number of segments
+ assert len(segments) == int((OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET)
+ # Check sequence numbers
+ assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
+ # Check segment durations
+ assert all([s.duration == SEGMENT_DURATION for s in segments])
+ assert len(decoded_stream.video_packets) == OUT_OF_ORDER_PACKET_INDEX
+ assert len(decoded_stream.audio_packets) == 0
+
+
+async def test_packet_overflow(hass):
+ """Packet is too far out of order, and looks like overflow, ending stream early."""
+
+ packets = list(PacketSequence(TEST_SEQUENCE_LENGTH))
+ # Packet is so far out of order, exceeds max gap and looks like overflow
+ packets[OUT_OF_ORDER_PACKET_INDEX].dts = -9000000
+
+ decoded_stream = await async_decode_stream(hass, iter(packets))
+ segments = decoded_stream.segments
+ # Check number of segments
+ assert len(segments) == int((OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET)
+ # Check sequence numbers
+ assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
+ # Check segment durations
+ assert all([s.duration == SEGMENT_DURATION for s in segments])
+ assert len(decoded_stream.video_packets) == OUT_OF_ORDER_PACKET_INDEX
+ assert len(decoded_stream.audio_packets) == 0
+
+
+async def test_skip_initial_bad_packets(hass):
+ """Tests a small number of initial "bad" packets with missing dts."""
+
+ num_packets = LONGER_TEST_SEQUENCE_LENGTH
+ packets = list(PacketSequence(num_packets))
+ num_bad_packets = MAX_MISSING_DTS - 1
+ for i in range(0, num_bad_packets):
+ packets[i].dts = None
+
+ decoded_stream = await async_decode_stream(hass, iter(packets))
+ segments = decoded_stream.segments
+ # Check number of segments
+ assert len(segments) == int(
+ (num_packets - num_bad_packets - 1) * SEGMENTS_PER_PACKET
+ )
+ # Check sequence numbers
+ assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
+ # Check segment durations
+ assert all([s.duration == SEGMENT_DURATION for s in segments])
+ assert len(decoded_stream.video_packets) == num_packets - num_bad_packets
+ assert len(decoded_stream.audio_packets) == 0
+
+
+async def test_too_many_initial_bad_packets_fails(hass):
+ """Test initial bad packets are too high, causing it to never start."""
+
+ num_packets = LONGER_TEST_SEQUENCE_LENGTH
+ packets = list(PacketSequence(num_packets))
+ num_bad_packets = MAX_MISSING_DTS + 1
+ for i in range(0, num_bad_packets):
+ packets[i].dts = None
+
+ decoded_stream = await async_decode_stream(hass, iter(packets))
+ segments = decoded_stream.segments
+ assert len(segments) == 0
+ assert len(decoded_stream.video_packets) == 0
+ assert len(decoded_stream.audio_packets) == 0
+
+
+async def test_skip_missing_dts(hass):
+ """Test packets in the middle of the stream missing DTS are skipped."""
+
+ num_packets = LONGER_TEST_SEQUENCE_LENGTH
+ packets = list(PacketSequence(num_packets))
+ bad_packet_start = int(LONGER_TEST_SEQUENCE_LENGTH / 2)
+ num_bad_packets = MAX_MISSING_DTS - 1
+ for i in range(bad_packet_start, bad_packet_start + num_bad_packets):
+ packets[i].dts = None
+
+ decoded_stream = await async_decode_stream(hass, iter(packets))
+ segments = decoded_stream.segments
+ # Check sequence numbers
+ assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
+ # Check segment durations (not counting the elongated segment)
+ assert (
+ sum([segments[i].duration == SEGMENT_DURATION for i in range(len(segments))])
+ >= len(segments) - 1
+ )
+ assert len(decoded_stream.video_packets) == num_packets - num_bad_packets
+ assert len(decoded_stream.audio_packets) == 0
+
+
+async def test_too_many_bad_packets(hass):
+ """Test bad packets are too many, causing it to end."""
+
+ num_packets = LONGER_TEST_SEQUENCE_LENGTH
+ packets = list(PacketSequence(num_packets))
+ bad_packet_start = int(LONGER_TEST_SEQUENCE_LENGTH / 2)
+ num_bad_packets = MAX_MISSING_DTS + 1
+ for i in range(bad_packet_start, bad_packet_start + num_bad_packets):
+ packets[i].dts = None
+
+ decoded_stream = await async_decode_stream(hass, iter(packets))
+ segments = decoded_stream.segments
+ assert len(segments) == int((bad_packet_start - 1) * SEGMENTS_PER_PACKET)
+ assert len(decoded_stream.video_packets) == bad_packet_start
+ assert len(decoded_stream.audio_packets) == 0
+
+
+async def test_no_video_stream(hass):
+ """Test no video stream in the container means no resulting output."""
+ py_av = MockPyAv(video=False)
+
+ decoded_stream = await async_decode_stream(
+ hass, PacketSequence(TEST_SEQUENCE_LENGTH), py_av=py_av
+ )
+ # Note: This failure scenario does not output an end of stream
+ segments = decoded_stream.segments
+ assert len(segments) == 0
+ assert len(decoded_stream.video_packets) == 0
+ assert len(decoded_stream.audio_packets) == 0
+
+
+async def test_audio_packets_not_found(hass):
+ """Set up an audio stream, but no audio packets are found."""
+ py_av = MockPyAv(audio=True)
+
+ num_packets = PACKETS_TO_WAIT_FOR_AUDIO + 1
+ packets = PacketSequence(num_packets) # Contains only video packets
+
+ decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av)
+ segments = decoded_stream.segments
+ assert len(segments) == int((num_packets - 1) * SEGMENTS_PER_PACKET)
+ assert len(decoded_stream.video_packets) == num_packets
+ assert len(decoded_stream.audio_packets) == 0
+
+
+async def test_adts_aac_audio(hass):
+ """Set up an ADTS AAC audio stream and disable audio."""
+ py_av = MockPyAv(audio=True)
+
+ num_packets = PACKETS_TO_WAIT_FOR_AUDIO + 1
+ packets = list(PacketSequence(num_packets))
+ packets[1].stream = AUDIO_STREAM
+ packets[1].dts = packets[0].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
+ packets[1].pts = packets[0].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
+ # The following is packet data is a sign of ADTS AAC
+ packets[1][0] = 255
+ packets[1][1] = 241
+
+ decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av)
+ assert len(decoded_stream.audio_packets) == 0
+
+
+async def test_audio_is_first_packet(hass):
+ """Set up an audio stream and audio packet is the first packet in the stream."""
+ py_av = MockPyAv(audio=True)
+
+ num_packets = PACKETS_TO_WAIT_FOR_AUDIO + 1
+ packets = list(PacketSequence(num_packets))
+ # Pair up an audio packet for each video packet
+ packets[0].stream = AUDIO_STREAM
+ packets[0].dts = packets[1].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
+ packets[0].pts = packets[1].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
+ packets[2].stream = AUDIO_STREAM
+ packets[2].dts = packets[3].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
+ packets[2].pts = packets[3].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
+
+ decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av)
+ segments = decoded_stream.segments
+ # The audio packets are segmented with the video packets
+ assert len(segments) == int((num_packets - 2 - 1) * SEGMENTS_PER_PACKET)
+ assert len(decoded_stream.video_packets) == num_packets - 2
+ assert len(decoded_stream.audio_packets) == 1
+
+
+async def test_audio_packets_found(hass):
+ """Set up an audio stream and audio packets are found at the start of the stream."""
+ py_av = MockPyAv(audio=True)
+
+ num_packets = PACKETS_TO_WAIT_FOR_AUDIO + 1
+ packets = list(PacketSequence(num_packets))
+ packets[1].stream = AUDIO_STREAM
+ packets[1].dts = packets[0].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
+ packets[1].pts = packets[0].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
+
+ decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av)
+ segments = decoded_stream.segments
+ # The audio packet above is buffered with the video packet
+ assert len(segments) == int((num_packets - 1 - 1) * SEGMENTS_PER_PACKET)
+ assert len(decoded_stream.video_packets) == num_packets - 1
+ assert len(decoded_stream.audio_packets) == 1
+
+
+async def test_pts_out_of_order(hass):
+ """Test pts can be out of order and still be valid."""
+
+ # Create a sequence of packets with some out of order pts
+ packets = list(PacketSequence(TEST_SEQUENCE_LENGTH))
+ for i, _ in enumerate(packets):
+ if i % PACKETS_PER_SEGMENT == 1:
+ packets[i].pts = packets[i - 1].pts - 1
+ packets[i].is_keyframe = False
+
+ decoded_stream = await async_decode_stream(hass, iter(packets))
+ segments = decoded_stream.segments
+ # Check number of segments
+ assert len(segments) == int((TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET)
+ # Check sequence numbers
+ assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
+ # Check segment durations
+ assert all([s.duration == SEGMENT_DURATION for s in segments])
+ assert len(decoded_stream.video_packets) == len(packets)
+ assert len(decoded_stream.audio_packets) == 0
+
+
+async def test_stream_stopped_while_decoding(hass):
+ """Tests that worker quits when stop() is called while decodign."""
+ # Add some synchronization so that the test can pause the background
+ # worker. When the worker is stopped, the test invokes stop() which
+ # will cause the worker thread to exit once it enters the decode
+ # loop
+ worker_open = threading.Event()
+ worker_wake = threading.Event()
+
+ stream = Stream(hass, STREAM_SOURCE)
+ stream.add_provider(STREAM_OUTPUT_FORMAT)
+
+ py_av = MockPyAv()
+ py_av.container.packets = PacketSequence(TEST_SEQUENCE_LENGTH)
+
+ def blocking_open(stream_source, *args, **kwargs):
+ # Let test know the thread is running
+ worker_open.set()
+ # Block worker thread until test wakes up
+ worker_wake.wait()
+ return py_av.open(stream_source, args, kwargs)
+
+ with patch("av.open", new=blocking_open):
+ stream.start()
+ assert worker_open.wait(TIMEOUT)
+ # Note: There is a race here where the worker could start as soon
+ # as the wake event is sent, completing all decode work.
+ worker_wake.set()
+ stream.stop()
+
+
+async def test_update_stream_source(hass):
+ """Tests that the worker is re-invoked when the stream source is updated."""
+ worker_open = threading.Event()
+ worker_wake = threading.Event()
+
+ stream = Stream(hass, STREAM_SOURCE)
+ stream.add_provider(STREAM_OUTPUT_FORMAT)
+ # Note that keepalive is not set here. The stream is "restarted" even though
+ # it is not stopping due to failure.
+
+ py_av = MockPyAv()
+ py_av.container.packets = PacketSequence(TEST_SEQUENCE_LENGTH)
+
+ last_stream_source = None
+
+ def blocking_open(stream_source, *args, **kwargs):
+ nonlocal last_stream_source
+ if not isinstance(stream_source, io.BytesIO):
+ last_stream_source = stream_source
+ # Let test know the thread is running
+ worker_open.set()
+ # Block worker thread until test wakes up
+ worker_wake.wait()
+ return py_av.open(stream_source, args, kwargs)
+
+ with patch("av.open", new=blocking_open):
+ stream.start()
+ assert worker_open.wait(TIMEOUT)
+ assert last_stream_source == STREAM_SOURCE
+
+ # Update the stream source, then the test wakes up the worker and assert
+ # that it re-opens the new stream (the test again waits on thread_started)
+ worker_open.clear()
+ stream.update_source(STREAM_SOURCE + "-updated-source")
+ worker_wake.set()
+ assert worker_open.wait(TIMEOUT)
+ assert last_stream_source == STREAM_SOURCE + "-updated-source"
+ worker_wake.set()
+
+ # Ccleanup
+ stream.stop()
+
+
+async def test_worker_log(hass, caplog):
+ """Test that the worker logs the url without username and password."""
+ stream = Stream(hass, "https://abcd:efgh@foo.bar")
+ stream.add_provider(STREAM_OUTPUT_FORMAT)
+ with patch("av.open") as av_open:
+ av_open.side_effect = av.error.InvalidDataError(-2, "error")
+ segment_buffer = SegmentBuffer(stream.outputs)
+ stream_worker(
+ "https://abcd:efgh@foo.bar", {}, segment_buffer, threading.Event()
+ )
+ await hass.async_block_till_done()
+ assert "https://abcd:efgh@foo.bar" not in caplog.text
+ assert "https://****:****@foo.bar" in caplog.text
diff --git a/tests/components/subaru/__init__.py b/tests/components/subaru/__init__.py
new file mode 100644
index 00000000000000..26b81c84a1ea62
--- /dev/null
+++ b/tests/components/subaru/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Subaru integration."""
diff --git a/tests/components/subaru/api_responses.py b/tests/components/subaru/api_responses.py
new file mode 100644
index 00000000000000..b6a79ab8829500
--- /dev/null
+++ b/tests/components/subaru/api_responses.py
@@ -0,0 +1,284 @@
+"""Sample API response data for tests."""
+
+from homeassistant.components.subaru.const import (
+ API_GEN_1,
+ API_GEN_2,
+ VEHICLE_API_GEN,
+ VEHICLE_HAS_EV,
+ VEHICLE_HAS_REMOTE_SERVICE,
+ VEHICLE_HAS_REMOTE_START,
+ VEHICLE_HAS_SAFETY_SERVICE,
+ VEHICLE_NAME,
+ VEHICLE_VIN,
+)
+
+TEST_VIN_1_G1 = "JF2ABCDE6L0000001"
+TEST_VIN_2_EV = "JF2ABCDE6L0000002"
+TEST_VIN_3_G2 = "JF2ABCDE6L0000003"
+
+VEHICLE_DATA = {
+ TEST_VIN_1_G1: {
+ VEHICLE_VIN: TEST_VIN_1_G1,
+ VEHICLE_NAME: "test_vehicle_1",
+ VEHICLE_HAS_EV: False,
+ VEHICLE_API_GEN: API_GEN_1,
+ VEHICLE_HAS_REMOTE_START: True,
+ VEHICLE_HAS_REMOTE_SERVICE: True,
+ VEHICLE_HAS_SAFETY_SERVICE: False,
+ },
+ TEST_VIN_2_EV: {
+ VEHICLE_VIN: TEST_VIN_2_EV,
+ VEHICLE_NAME: "test_vehicle_2",
+ VEHICLE_HAS_EV: True,
+ VEHICLE_API_GEN: API_GEN_2,
+ VEHICLE_HAS_REMOTE_START: True,
+ VEHICLE_HAS_REMOTE_SERVICE: True,
+ VEHICLE_HAS_SAFETY_SERVICE: True,
+ },
+ TEST_VIN_3_G2: {
+ VEHICLE_VIN: TEST_VIN_3_G2,
+ VEHICLE_NAME: "test_vehicle_3",
+ VEHICLE_HAS_EV: False,
+ VEHICLE_API_GEN: API_GEN_2,
+ VEHICLE_HAS_REMOTE_START: True,
+ VEHICLE_HAS_REMOTE_SERVICE: True,
+ VEHICLE_HAS_SAFETY_SERVICE: True,
+ },
+}
+
+VEHICLE_STATUS_EV = {
+ "status": {
+ "AVG_FUEL_CONSUMPTION": 2.3,
+ "BATTERY_VOLTAGE": "12.0",
+ "DISTANCE_TO_EMPTY_FUEL": 707,
+ "DOOR_BOOT_LOCK_STATUS": "UNKNOWN",
+ "DOOR_BOOT_POSITION": "CLOSED",
+ "DOOR_ENGINE_HOOD_LOCK_STATUS": "UNKNOWN",
+ "DOOR_ENGINE_HOOD_POSITION": "CLOSED",
+ "DOOR_FRONT_LEFT_LOCK_STATUS": "UNKNOWN",
+ "DOOR_FRONT_LEFT_POSITION": "CLOSED",
+ "DOOR_FRONT_RIGHT_LOCK_STATUS": "UNKNOWN",
+ "DOOR_FRONT_RIGHT_POSITION": "CLOSED",
+ "DOOR_REAR_LEFT_LOCK_STATUS": "UNKNOWN",
+ "DOOR_REAR_LEFT_POSITION": "CLOSED",
+ "DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN",
+ "DOOR_REAR_RIGHT_POSITION": "CLOSED",
+ "EV_CHARGER_STATE_TYPE": "CHARGING_STOPPED",
+ "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM",
+ "EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1",
+ "EV_DISTANCE_TO_EMPTY": 17,
+ "EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED",
+ "EV_STATE_OF_CHARGE_MODE": "EV_MODE",
+ "EV_STATE_OF_CHARGE_PERCENT": "100",
+ "EV_TIME_TO_FULLY_CHARGED": "65535",
+ "EV_VEHICLE_TIME_DAYOFWEEK": "6",
+ "EV_VEHICLE_TIME_HOUR": "14",
+ "EV_VEHICLE_TIME_MINUTE": "20",
+ "EV_VEHICLE_TIME_SECOND": "39",
+ "EXT_EXTERNAL_TEMP": "21.5",
+ "ODOMETER": 1234,
+ "POSITION_HEADING_DEGREE": "150",
+ "POSITION_SPEED_KMPH": "0",
+ "POSITION_TIMESTAMP": 1595560000.0,
+ "SEAT_BELT_STATUS_FRONT_LEFT": "BELTED",
+ "SEAT_BELT_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED",
+ "SEAT_BELT_STATUS_FRONT_RIGHT": "BELTED",
+ "SEAT_BELT_STATUS_SECOND_LEFT": "UNKNOWN",
+ "SEAT_BELT_STATUS_SECOND_MIDDLE": "UNKNOWN",
+ "SEAT_BELT_STATUS_SECOND_RIGHT": "UNKNOWN",
+ "SEAT_BELT_STATUS_THIRD_LEFT": "UNKNOWN",
+ "SEAT_BELT_STATUS_THIRD_MIDDLE": "UNKNOWN",
+ "SEAT_BELT_STATUS_THIRD_RIGHT": "UNKNOWN",
+ "SEAT_OCCUPATION_STATUS_FRONT_LEFT": "UNKNOWN",
+ "SEAT_OCCUPATION_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED",
+ "SEAT_OCCUPATION_STATUS_FRONT_RIGHT": "UNKNOWN",
+ "SEAT_OCCUPATION_STATUS_SECOND_LEFT": "UNKNOWN",
+ "SEAT_OCCUPATION_STATUS_SECOND_MIDDLE": "UNKNOWN",
+ "SEAT_OCCUPATION_STATUS_SECOND_RIGHT": "UNKNOWN",
+ "SEAT_OCCUPATION_STATUS_THIRD_LEFT": "UNKNOWN",
+ "SEAT_OCCUPATION_STATUS_THIRD_MIDDLE": "UNKNOWN",
+ "SEAT_OCCUPATION_STATUS_THIRD_RIGHT": "UNKNOWN",
+ "TIMESTAMP": 1595560000.0,
+ "TRANSMISSION_MODE": "UNKNOWN",
+ "TYRE_PRESSURE_FRONT_LEFT": 2550,
+ "TYRE_PRESSURE_FRONT_RIGHT": 2550,
+ "TYRE_PRESSURE_REAR_LEFT": 2450,
+ "TYRE_PRESSURE_REAR_RIGHT": 2350,
+ "TYRE_STATUS_FRONT_LEFT": "UNKNOWN",
+ "TYRE_STATUS_FRONT_RIGHT": "UNKNOWN",
+ "TYRE_STATUS_REAR_LEFT": "UNKNOWN",
+ "TYRE_STATUS_REAR_RIGHT": "UNKNOWN",
+ "VEHICLE_STATE_TYPE": "IGNITION_OFF",
+ "WINDOW_BACK_STATUS": "UNKNOWN",
+ "WINDOW_FRONT_LEFT_STATUS": "VENTED",
+ "WINDOW_FRONT_RIGHT_STATUS": "VENTED",
+ "WINDOW_REAR_LEFT_STATUS": "UNKNOWN",
+ "WINDOW_REAR_RIGHT_STATUS": "UNKNOWN",
+ "WINDOW_SUNROOF_STATUS": "UNKNOWN",
+ "heading": 170,
+ "latitude": 40.0,
+ "longitude": -100.0,
+ }
+}
+
+VEHICLE_STATUS_G2 = {
+ "status": {
+ "AVG_FUEL_CONSUMPTION": 2.3,
+ "BATTERY_VOLTAGE": "12.0",
+ "DISTANCE_TO_EMPTY_FUEL": 707,
+ "DOOR_BOOT_LOCK_STATUS": "UNKNOWN",
+ "DOOR_BOOT_POSITION": "CLOSED",
+ "DOOR_ENGINE_HOOD_LOCK_STATUS": "UNKNOWN",
+ "DOOR_ENGINE_HOOD_POSITION": "CLOSED",
+ "DOOR_FRONT_LEFT_LOCK_STATUS": "UNKNOWN",
+ "DOOR_FRONT_LEFT_POSITION": "CLOSED",
+ "DOOR_FRONT_RIGHT_LOCK_STATUS": "UNKNOWN",
+ "DOOR_FRONT_RIGHT_POSITION": "CLOSED",
+ "DOOR_REAR_LEFT_LOCK_STATUS": "UNKNOWN",
+ "DOOR_REAR_LEFT_POSITION": "CLOSED",
+ "DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN",
+ "DOOR_REAR_RIGHT_POSITION": "CLOSED",
+ "EXT_EXTERNAL_TEMP": "21.5",
+ "ODOMETER": 1234,
+ "POSITION_HEADING_DEGREE": "150",
+ "POSITION_SPEED_KMPH": "0",
+ "POSITION_TIMESTAMP": 1595560000.0,
+ "SEAT_BELT_STATUS_FRONT_LEFT": "BELTED",
+ "SEAT_BELT_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED",
+ "SEAT_BELT_STATUS_FRONT_RIGHT": "BELTED",
+ "SEAT_BELT_STATUS_SECOND_LEFT": "UNKNOWN",
+ "SEAT_BELT_STATUS_SECOND_MIDDLE": "UNKNOWN",
+ "SEAT_BELT_STATUS_SECOND_RIGHT": "UNKNOWN",
+ "SEAT_BELT_STATUS_THIRD_LEFT": "UNKNOWN",
+ "SEAT_BELT_STATUS_THIRD_MIDDLE": "UNKNOWN",
+ "SEAT_BELT_STATUS_THIRD_RIGHT": "UNKNOWN",
+ "SEAT_OCCUPATION_STATUS_FRONT_LEFT": "UNKNOWN",
+ "SEAT_OCCUPATION_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED",
+ "SEAT_OCCUPATION_STATUS_FRONT_RIGHT": "UNKNOWN",
+ "SEAT_OCCUPATION_STATUS_SECOND_LEFT": "UNKNOWN",
+ "SEAT_OCCUPATION_STATUS_SECOND_MIDDLE": "UNKNOWN",
+ "SEAT_OCCUPATION_STATUS_SECOND_RIGHT": "UNKNOWN",
+ "SEAT_OCCUPATION_STATUS_THIRD_LEFT": "UNKNOWN",
+ "SEAT_OCCUPATION_STATUS_THIRD_MIDDLE": "UNKNOWN",
+ "SEAT_OCCUPATION_STATUS_THIRD_RIGHT": "UNKNOWN",
+ "TIMESTAMP": 1595560000.0,
+ "TRANSMISSION_MODE": "UNKNOWN",
+ "TYRE_PRESSURE_FRONT_LEFT": 2550,
+ "TYRE_PRESSURE_FRONT_RIGHT": 2550,
+ "TYRE_PRESSURE_REAR_LEFT": 2450,
+ "TYRE_PRESSURE_REAR_RIGHT": 2350,
+ "TYRE_STATUS_FRONT_LEFT": "UNKNOWN",
+ "TYRE_STATUS_FRONT_RIGHT": "UNKNOWN",
+ "TYRE_STATUS_REAR_LEFT": "UNKNOWN",
+ "TYRE_STATUS_REAR_RIGHT": "UNKNOWN",
+ "VEHICLE_STATE_TYPE": "IGNITION_OFF",
+ "WINDOW_BACK_STATUS": "UNKNOWN",
+ "WINDOW_FRONT_LEFT_STATUS": "VENTED",
+ "WINDOW_FRONT_RIGHT_STATUS": "VENTED",
+ "WINDOW_REAR_LEFT_STATUS": "UNKNOWN",
+ "WINDOW_REAR_RIGHT_STATUS": "UNKNOWN",
+ "WINDOW_SUNROOF_STATUS": "UNKNOWN",
+ "heading": 170,
+ "latitude": 40.0,
+ "longitude": -100.0,
+ }
+}
+
+EXPECTED_STATE_EV_IMPERIAL = {
+ "AVG_FUEL_CONSUMPTION": "102.3",
+ "BATTERY_VOLTAGE": "12.0",
+ "DISTANCE_TO_EMPTY_FUEL": "439.3",
+ "EV_CHARGER_STATE_TYPE": "CHARGING_STOPPED",
+ "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM",
+ "EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1",
+ "EV_DISTANCE_TO_EMPTY": "17",
+ "EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED",
+ "EV_STATE_OF_CHARGE_MODE": "EV_MODE",
+ "EV_STATE_OF_CHARGE_PERCENT": "100",
+ "EV_TIME_TO_FULLY_CHARGED": "unknown",
+ "EV_VEHICLE_TIME_DAYOFWEEK": "6",
+ "EV_VEHICLE_TIME_HOUR": "14",
+ "EV_VEHICLE_TIME_MINUTE": "20",
+ "EV_VEHICLE_TIME_SECOND": "39",
+ "EXT_EXTERNAL_TEMP": "70.7",
+ "ODOMETER": "766.8",
+ "POSITION_HEADING_DEGREE": "150",
+ "POSITION_SPEED_KMPH": "0",
+ "POSITION_TIMESTAMP": 1595560000.0,
+ "TIMESTAMP": 1595560000.0,
+ "TRANSMISSION_MODE": "UNKNOWN",
+ "TYRE_PRESSURE_FRONT_LEFT": "37.0",
+ "TYRE_PRESSURE_FRONT_RIGHT": "37.0",
+ "TYRE_PRESSURE_REAR_LEFT": "35.5",
+ "TYRE_PRESSURE_REAR_RIGHT": "34.1",
+ "VEHICLE_STATE_TYPE": "IGNITION_OFF",
+ "heading": 170,
+ "latitude": 40.0,
+ "longitude": -100.0,
+}
+
+EXPECTED_STATE_EV_METRIC = {
+ "AVG_FUEL_CONSUMPTION": "2.3",
+ "BATTERY_VOLTAGE": "12.0",
+ "DISTANCE_TO_EMPTY_FUEL": "707",
+ "EV_CHARGER_STATE_TYPE": "CHARGING_STOPPED",
+ "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM",
+ "EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1",
+ "EV_DISTANCE_TO_EMPTY": "27.4",
+ "EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED",
+ "EV_STATE_OF_CHARGE_MODE": "EV_MODE",
+ "EV_STATE_OF_CHARGE_PERCENT": "100",
+ "EV_TIME_TO_FULLY_CHARGED": "unknown",
+ "EV_VEHICLE_TIME_DAYOFWEEK": "6",
+ "EV_VEHICLE_TIME_HOUR": "14",
+ "EV_VEHICLE_TIME_MINUTE": "20",
+ "EV_VEHICLE_TIME_SECOND": "39",
+ "EXT_EXTERNAL_TEMP": "21.5",
+ "ODOMETER": "1234",
+ "POSITION_HEADING_DEGREE": "150",
+ "POSITION_SPEED_KMPH": "0",
+ "POSITION_TIMESTAMP": 1595560000.0,
+ "TIMESTAMP": 1595560000.0,
+ "TRANSMISSION_MODE": "UNKNOWN",
+ "TYRE_PRESSURE_FRONT_LEFT": "2550",
+ "TYRE_PRESSURE_FRONT_RIGHT": "2550",
+ "TYRE_PRESSURE_REAR_LEFT": "2450",
+ "TYRE_PRESSURE_REAR_RIGHT": "2350",
+ "VEHICLE_STATE_TYPE": "IGNITION_OFF",
+ "heading": 170,
+ "latitude": 40.0,
+ "longitude": -100.0,
+}
+
+EXPECTED_STATE_EV_UNAVAILABLE = {
+ "AVG_FUEL_CONSUMPTION": "unavailable",
+ "BATTERY_VOLTAGE": "unavailable",
+ "DISTANCE_TO_EMPTY_FUEL": "unavailable",
+ "EV_CHARGER_STATE_TYPE": "unavailable",
+ "EV_CHARGE_SETTING_AMPERE_TYPE": "unavailable",
+ "EV_CHARGE_VOLT_TYPE": "unavailable",
+ "EV_DISTANCE_TO_EMPTY": "unavailable",
+ "EV_IS_PLUGGED_IN": "unavailable",
+ "EV_STATE_OF_CHARGE_MODE": "unavailable",
+ "EV_STATE_OF_CHARGE_PERCENT": "unavailable",
+ "EV_TIME_TO_FULLY_CHARGED": "unavailable",
+ "EV_VEHICLE_TIME_DAYOFWEEK": "unavailable",
+ "EV_VEHICLE_TIME_HOUR": "unavailable",
+ "EV_VEHICLE_TIME_MINUTE": "unavailable",
+ "EV_VEHICLE_TIME_SECOND": "unavailable",
+ "EXT_EXTERNAL_TEMP": "unavailable",
+ "ODOMETER": "unavailable",
+ "POSITION_HEADING_DEGREE": "unavailable",
+ "POSITION_SPEED_KMPH": "unavailable",
+ "POSITION_TIMESTAMP": "unavailable",
+ "TIMESTAMP": "unavailable",
+ "TRANSMISSION_MODE": "unavailable",
+ "TYRE_PRESSURE_FRONT_LEFT": "unavailable",
+ "TYRE_PRESSURE_FRONT_RIGHT": "unavailable",
+ "TYRE_PRESSURE_REAR_LEFT": "unavailable",
+ "TYRE_PRESSURE_REAR_RIGHT": "unavailable",
+ "VEHICLE_STATE_TYPE": "unavailable",
+ "heading": "unavailable",
+ "latitude": "unavailable",
+ "longitude": "unavailable",
+}
diff --git a/tests/components/subaru/conftest.py b/tests/components/subaru/conftest.py
new file mode 100644
index 00000000000000..1b8d1439e68828
--- /dev/null
+++ b/tests/components/subaru/conftest.py
@@ -0,0 +1,148 @@
+"""Common functions needed to setup tests for Subaru component."""
+from datetime import timedelta
+from unittest.mock import patch
+
+import pytest
+from subarulink.const import COUNTRY_USA
+
+from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN
+from homeassistant.components.subaru.const import (
+ CONF_COUNTRY,
+ CONF_UPDATE_ENABLED,
+ DOMAIN,
+ FETCH_INTERVAL,
+ VEHICLE_API_GEN,
+ VEHICLE_HAS_EV,
+ VEHICLE_HAS_REMOTE_SERVICE,
+ VEHICLE_HAS_REMOTE_START,
+ VEHICLE_HAS_SAFETY_SERVICE,
+ VEHICLE_NAME,
+)
+from homeassistant.config_entries import ENTRY_STATE_LOADED
+from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME
+from homeassistant.setup import async_setup_component
+import homeassistant.util.dt as dt_util
+
+from .api_responses import TEST_VIN_2_EV, VEHICLE_DATA, VEHICLE_STATUS_EV
+
+from tests.common import MockConfigEntry, async_fire_time_changed
+
+MOCK_API = "homeassistant.components.subaru.SubaruAPI."
+MOCK_API_CONNECT = f"{MOCK_API}connect"
+MOCK_API_IS_PIN_REQUIRED = f"{MOCK_API}is_pin_required"
+MOCK_API_TEST_PIN = f"{MOCK_API}test_pin"
+MOCK_API_UPDATE_SAVED_PIN = f"{MOCK_API}update_saved_pin"
+MOCK_API_GET_VEHICLES = f"{MOCK_API}get_vehicles"
+MOCK_API_VIN_TO_NAME = f"{MOCK_API}vin_to_name"
+MOCK_API_GET_API_GEN = f"{MOCK_API}get_api_gen"
+MOCK_API_GET_EV_STATUS = f"{MOCK_API}get_ev_status"
+MOCK_API_GET_RES_STATUS = f"{MOCK_API}get_res_status"
+MOCK_API_GET_REMOTE_STATUS = f"{MOCK_API}get_remote_status"
+MOCK_API_GET_SAFETY_STATUS = f"{MOCK_API}get_safety_status"
+MOCK_API_GET_DATA = f"{MOCK_API}get_data"
+MOCK_API_UPDATE = f"{MOCK_API}update"
+MOCK_API_FETCH = f"{MOCK_API}fetch"
+
+TEST_USERNAME = "user@email.com"
+TEST_PASSWORD = "password"
+TEST_PIN = "1234"
+TEST_DEVICE_ID = 1613183362
+TEST_COUNTRY = COUNTRY_USA
+
+TEST_CREDS = {
+ CONF_USERNAME: TEST_USERNAME,
+ CONF_PASSWORD: TEST_PASSWORD,
+ CONF_COUNTRY: TEST_COUNTRY,
+}
+
+TEST_CONFIG = {
+ CONF_USERNAME: TEST_USERNAME,
+ CONF_PASSWORD: TEST_PASSWORD,
+ CONF_COUNTRY: TEST_COUNTRY,
+ CONF_PIN: TEST_PIN,
+ CONF_DEVICE_ID: TEST_DEVICE_ID,
+}
+
+TEST_OPTIONS = {
+ CONF_UPDATE_ENABLED: True,
+}
+
+TEST_ENTITY_ID = "sensor.test_vehicle_2_odometer"
+
+
+def advance_time_to_next_fetch(hass):
+ """Fast forward time to next fetch."""
+ future = dt_util.utcnow() + timedelta(seconds=FETCH_INTERVAL + 30)
+ async_fire_time_changed(hass, future)
+
+
+async def setup_subaru_integration(
+ hass,
+ vehicle_list=None,
+ vehicle_data=None,
+ vehicle_status=None,
+ connect_effect=None,
+ fetch_effect=None,
+):
+ """Create Subaru entry."""
+ assert await async_setup_component(hass, HA_DOMAIN, {})
+ assert await async_setup_component(hass, DOMAIN, {})
+
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=TEST_CONFIG,
+ options=TEST_OPTIONS,
+ entry_id=1,
+ )
+ config_entry.add_to_hass(hass)
+
+ with patch(
+ MOCK_API_CONNECT,
+ return_value=connect_effect is None,
+ side_effect=connect_effect,
+ ), patch(MOCK_API_GET_VEHICLES, return_value=vehicle_list,), patch(
+ MOCK_API_VIN_TO_NAME,
+ return_value=vehicle_data[VEHICLE_NAME],
+ ), patch(
+ MOCK_API_GET_API_GEN,
+ return_value=vehicle_data[VEHICLE_API_GEN],
+ ), patch(
+ MOCK_API_GET_EV_STATUS,
+ return_value=vehicle_data[VEHICLE_HAS_EV],
+ ), patch(
+ MOCK_API_GET_RES_STATUS,
+ return_value=vehicle_data[VEHICLE_HAS_REMOTE_START],
+ ), patch(
+ MOCK_API_GET_REMOTE_STATUS,
+ return_value=vehicle_data[VEHICLE_HAS_REMOTE_SERVICE],
+ ), patch(
+ MOCK_API_GET_SAFETY_STATUS,
+ return_value=vehicle_data[VEHICLE_HAS_SAFETY_SERVICE],
+ ), patch(
+ MOCK_API_GET_DATA,
+ return_value=vehicle_status,
+ ), patch(
+ MOCK_API_UPDATE,
+ ), patch(
+ MOCK_API_FETCH, side_effect=fetch_effect
+ ):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ return config_entry
+
+
+@pytest.fixture
+async def ev_entry(hass):
+ """Create a Subaru entry representing an EV vehicle with full STARLINK subscription."""
+ entry = await setup_subaru_integration(
+ hass,
+ vehicle_list=[TEST_VIN_2_EV],
+ vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV],
+ vehicle_status=VEHICLE_STATUS_EV,
+ )
+ assert DOMAIN in hass.config_entries.async_domains()
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+ assert hass.config_entries.async_get_entry(entry.entry_id)
+ assert entry.state == ENTRY_STATE_LOADED
+ return entry
diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py
new file mode 100644
index 00000000000000..0218c11003c5b6
--- /dev/null
+++ b/tests/components/subaru/test_config_flow.py
@@ -0,0 +1,256 @@
+"""Tests for the Subaru component config flow."""
+# pylint: disable=redefined-outer-name
+from copy import deepcopy
+from unittest import mock
+from unittest.mock import patch
+
+import pytest
+from subarulink.exceptions import InvalidCredentials, InvalidPIN, SubaruException
+
+from homeassistant import config_entries
+from homeassistant.components.subaru import config_flow
+from homeassistant.components.subaru.const import CONF_UPDATE_ENABLED, DOMAIN
+from homeassistant.const import CONF_DEVICE_ID, CONF_PIN
+from homeassistant.setup import async_setup_component
+
+from .conftest import (
+ MOCK_API_CONNECT,
+ MOCK_API_IS_PIN_REQUIRED,
+ MOCK_API_TEST_PIN,
+ MOCK_API_UPDATE_SAVED_PIN,
+ TEST_CONFIG,
+ TEST_CREDS,
+ TEST_DEVICE_ID,
+ TEST_PIN,
+ TEST_USERNAME,
+)
+
+from tests.common import MockConfigEntry
+
+ASYNC_SETUP = "homeassistant.components.subaru.async_setup"
+ASYNC_SETUP_ENTRY = "homeassistant.components.subaru.async_setup_entry"
+
+
+async def test_user_form_init(user_form):
+ """Test the initial user form for first step of the config flow."""
+ assert user_form["description_placeholders"] is None
+ assert user_form["errors"] is None
+ assert user_form["handler"] == DOMAIN
+ assert user_form["step_id"] == "user"
+ assert user_form["type"] == "form"
+
+
+async def test_user_form_repeat_identifier(hass, user_form):
+ """Test we handle repeat identifiers."""
+ entry = MockConfigEntry(
+ domain=DOMAIN, title=TEST_USERNAME, data=TEST_CREDS, options=None
+ )
+ entry.add_to_hass(hass)
+
+ with patch(
+ MOCK_API_CONNECT,
+ return_value=True,
+ ) as mock_connect:
+ result = await hass.config_entries.flow.async_configure(
+ user_form["flow_id"],
+ TEST_CREDS,
+ )
+ assert len(mock_connect.mock_calls) == 0
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+
+
+async def test_user_form_cannot_connect(hass, user_form):
+ """Test we handle cannot connect error."""
+ with patch(
+ MOCK_API_CONNECT,
+ side_effect=SubaruException(None),
+ ) as mock_connect:
+ result = await hass.config_entries.flow.async_configure(
+ user_form["flow_id"],
+ TEST_CREDS,
+ )
+ assert len(mock_connect.mock_calls) == 1
+ assert result["type"] == "abort"
+ assert result["reason"] == "cannot_connect"
+
+
+async def test_user_form_invalid_auth(hass, user_form):
+ """Test we handle invalid auth."""
+ with patch(
+ MOCK_API_CONNECT,
+ side_effect=InvalidCredentials("invalidAccount"),
+ ) as mock_connect:
+ result = await hass.config_entries.flow.async_configure(
+ user_form["flow_id"],
+ TEST_CREDS,
+ )
+ assert len(mock_connect.mock_calls) == 1
+ assert result["type"] == "form"
+ assert result["errors"] == {"base": "invalid_auth"}
+
+
+async def test_user_form_pin_not_required(hass, user_form):
+ """Test successful login when no PIN is required."""
+ with patch(MOCK_API_CONNECT, return_value=True,) as mock_connect, patch(
+ MOCK_API_IS_PIN_REQUIRED,
+ return_value=False,
+ ) as mock_is_pin_required, patch(
+ ASYNC_SETUP, return_value=True
+ ) as mock_setup, patch(
+ ASYNC_SETUP_ENTRY, return_value=True
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_configure(
+ user_form["flow_id"],
+ TEST_CREDS,
+ )
+ assert len(mock_connect.mock_calls) == 1
+ assert len(mock_is_pin_required.mock_calls) == 1
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+ expected = {
+ "title": TEST_USERNAME,
+ "description": None,
+ "description_placeholders": None,
+ "flow_id": mock.ANY,
+ "result": mock.ANY,
+ "handler": DOMAIN,
+ "type": "create_entry",
+ "version": 1,
+ "data": deepcopy(TEST_CONFIG),
+ }
+ expected["data"][CONF_PIN] = None
+ result["data"][CONF_DEVICE_ID] = TEST_DEVICE_ID
+ assert result == expected
+
+
+async def test_pin_form_init(pin_form):
+ """Test the pin entry form for second step of the config flow."""
+ expected = {
+ "data_schema": config_flow.PIN_SCHEMA,
+ "description_placeholders": None,
+ "errors": None,
+ "flow_id": mock.ANY,
+ "handler": DOMAIN,
+ "step_id": "pin",
+ "type": "form",
+ }
+ assert pin_form == expected
+
+
+async def test_pin_form_bad_pin_format(hass, pin_form):
+ """Test we handle invalid pin."""
+ with patch(MOCK_API_TEST_PIN,) as mock_test_pin, patch(
+ MOCK_API_UPDATE_SAVED_PIN,
+ return_value=True,
+ ) as mock_update_saved_pin:
+ result = await hass.config_entries.flow.async_configure(
+ pin_form["flow_id"], user_input={CONF_PIN: "abcd"}
+ )
+ assert len(mock_test_pin.mock_calls) == 0
+ assert len(mock_update_saved_pin.mock_calls) == 1
+ assert result["type"] == "form"
+ assert result["errors"] == {"base": "bad_pin_format"}
+
+
+async def test_pin_form_success(hass, pin_form):
+ """Test successful PIN entry."""
+ with patch(MOCK_API_TEST_PIN, return_value=True,) as mock_test_pin, patch(
+ MOCK_API_UPDATE_SAVED_PIN,
+ return_value=True,
+ ) as mock_update_saved_pin, patch(
+ ASYNC_SETUP, return_value=True
+ ) as mock_setup, patch(
+ ASYNC_SETUP_ENTRY, return_value=True
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_configure(
+ pin_form["flow_id"], user_input={CONF_PIN: TEST_PIN}
+ )
+
+ assert len(mock_test_pin.mock_calls) == 1
+ assert len(mock_update_saved_pin.mock_calls) == 1
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+ expected = {
+ "title": TEST_USERNAME,
+ "description": None,
+ "description_placeholders": None,
+ "flow_id": mock.ANY,
+ "result": mock.ANY,
+ "handler": DOMAIN,
+ "type": "create_entry",
+ "version": 1,
+ "data": TEST_CONFIG,
+ }
+ result["data"][CONF_DEVICE_ID] = TEST_DEVICE_ID
+ assert result == expected
+
+
+async def test_pin_form_incorrect_pin(hass, pin_form):
+ """Test we handle invalid pin."""
+ with patch(
+ MOCK_API_TEST_PIN,
+ side_effect=InvalidPIN("invalidPin"),
+ ) as mock_test_pin, patch(
+ MOCK_API_UPDATE_SAVED_PIN,
+ return_value=True,
+ ) as mock_update_saved_pin:
+ result = await hass.config_entries.flow.async_configure(
+ pin_form["flow_id"], user_input={CONF_PIN: TEST_PIN}
+ )
+ assert len(mock_test_pin.mock_calls) == 1
+ assert len(mock_update_saved_pin.mock_calls) == 1
+ assert result["type"] == "form"
+ assert result["errors"] == {"base": "incorrect_pin"}
+
+
+async def test_option_flow_form(options_form):
+ """Test config flow options form."""
+ assert options_form["description_placeholders"] is None
+ assert options_form["errors"] is None
+ assert options_form["step_id"] == "init"
+ assert options_form["type"] == "form"
+
+
+async def test_option_flow(hass, options_form):
+ """Test config flow options."""
+ result = await hass.config_entries.options.async_configure(
+ options_form["flow_id"],
+ user_input={
+ CONF_UPDATE_ENABLED: False,
+ },
+ )
+ assert result["type"] == "create_entry"
+ assert result["data"] == {
+ CONF_UPDATE_ENABLED: False,
+ }
+
+
+@pytest.fixture
+async def user_form(hass):
+ """Return initial form for Subaru config flow."""
+ return await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+
+@pytest.fixture
+async def pin_form(hass, user_form):
+ """Return second form (PIN input) for Subaru config flow."""
+ with patch(MOCK_API_CONNECT, return_value=True,), patch(
+ MOCK_API_IS_PIN_REQUIRED,
+ return_value=True,
+ ):
+ return await hass.config_entries.flow.async_configure(
+ user_form["flow_id"], user_input=TEST_CREDS
+ )
+
+
+@pytest.fixture
+async def options_form(hass):
+ """Return options form for Subaru config flow."""
+ entry = MockConfigEntry(domain=DOMAIN, data={}, options=None)
+ entry.add_to_hass(hass)
+ await async_setup_component(hass, DOMAIN, {})
+ return await hass.config_entries.options.async_init(entry.entry_id)
diff --git a/tests/components/subaru/test_init.py b/tests/components/subaru/test_init.py
new file mode 100644
index 00000000000000..13b510e8c40c98
--- /dev/null
+++ b/tests/components/subaru/test_init.py
@@ -0,0 +1,153 @@
+"""Test Subaru component setup and updates."""
+from unittest.mock import patch
+
+from subarulink import InvalidCredentials, SubaruException
+
+from homeassistant.components.homeassistant import (
+ DOMAIN as HA_DOMAIN,
+ SERVICE_UPDATE_ENTITY,
+)
+from homeassistant.components.subaru.const import DOMAIN
+from homeassistant.config_entries import (
+ ENTRY_STATE_LOADED,
+ ENTRY_STATE_NOT_LOADED,
+ ENTRY_STATE_SETUP_ERROR,
+ ENTRY_STATE_SETUP_RETRY,
+)
+from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.setup import async_setup_component
+
+from .api_responses import (
+ TEST_VIN_1_G1,
+ TEST_VIN_2_EV,
+ TEST_VIN_3_G2,
+ VEHICLE_DATA,
+ VEHICLE_STATUS_EV,
+ VEHICLE_STATUS_G2,
+)
+from .conftest import (
+ MOCK_API_FETCH,
+ MOCK_API_UPDATE,
+ TEST_ENTITY_ID,
+ setup_subaru_integration,
+)
+
+
+async def test_setup_with_no_config(hass):
+ """Test DOMAIN is empty if there is no config."""
+ assert await async_setup_component(hass, DOMAIN, {})
+ await hass.async_block_till_done()
+ assert DOMAIN not in hass.config_entries.async_domains()
+
+
+async def test_setup_ev(hass, ev_entry):
+ """Test setup with an EV vehicle."""
+ check_entry = hass.config_entries.async_get_entry(ev_entry.entry_id)
+ assert check_entry
+ assert check_entry.state == ENTRY_STATE_LOADED
+
+
+async def test_setup_g2(hass):
+ """Test setup with a G2 vehcile ."""
+ entry = await setup_subaru_integration(
+ hass,
+ vehicle_list=[TEST_VIN_3_G2],
+ vehicle_data=VEHICLE_DATA[TEST_VIN_3_G2],
+ vehicle_status=VEHICLE_STATUS_G2,
+ )
+ check_entry = hass.config_entries.async_get_entry(entry.entry_id)
+ assert check_entry
+ assert check_entry.state == ENTRY_STATE_LOADED
+
+
+async def test_setup_g1(hass):
+ """Test setup with a G1 vehicle."""
+ entry = await setup_subaru_integration(
+ hass, vehicle_list=[TEST_VIN_1_G1], vehicle_data=VEHICLE_DATA[TEST_VIN_1_G1]
+ )
+ check_entry = hass.config_entries.async_get_entry(entry.entry_id)
+ assert check_entry
+ assert check_entry.state == ENTRY_STATE_LOADED
+
+
+async def test_unsuccessful_connect(hass):
+ """Test unsuccessful connect due to connectivity."""
+ entry = await setup_subaru_integration(
+ hass,
+ connect_effect=SubaruException("Service Unavailable"),
+ vehicle_list=[TEST_VIN_2_EV],
+ vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV],
+ vehicle_status=VEHICLE_STATUS_EV,
+ )
+ check_entry = hass.config_entries.async_get_entry(entry.entry_id)
+ assert check_entry
+ assert check_entry.state == ENTRY_STATE_SETUP_RETRY
+
+
+async def test_invalid_credentials(hass):
+ """Test invalid credentials."""
+ entry = await setup_subaru_integration(
+ hass,
+ connect_effect=InvalidCredentials("Invalid Credentials"),
+ vehicle_list=[TEST_VIN_2_EV],
+ vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV],
+ vehicle_status=VEHICLE_STATUS_EV,
+ )
+ check_entry = hass.config_entries.async_get_entry(entry.entry_id)
+ assert check_entry
+ assert check_entry.state == ENTRY_STATE_SETUP_ERROR
+
+
+async def test_update_skip_unsubscribed(hass):
+ """Test update function skips vehicles without subscription."""
+ await setup_subaru_integration(
+ hass, vehicle_list=[TEST_VIN_1_G1], vehicle_data=VEHICLE_DATA[TEST_VIN_1_G1]
+ )
+
+ with patch(MOCK_API_FETCH) as mock_fetch:
+ await hass.services.async_call(
+ HA_DOMAIN,
+ SERVICE_UPDATE_ENTITY,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID},
+ blocking=True,
+ )
+
+ await hass.async_block_till_done()
+ mock_fetch.assert_not_called()
+
+
+async def test_update_disabled(hass, ev_entry):
+ """Test update function disable option."""
+ with patch(MOCK_API_FETCH, side_effect=SubaruException("403 Error"),), patch(
+ MOCK_API_UPDATE,
+ ) as mock_update:
+ await hass.services.async_call(
+ HA_DOMAIN,
+ SERVICE_UPDATE_ENTITY,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_update.assert_not_called()
+
+
+async def test_fetch_failed(hass):
+ """Tests when fetch fails."""
+ await setup_subaru_integration(
+ hass,
+ vehicle_list=[TEST_VIN_2_EV],
+ vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV],
+ vehicle_status=VEHICLE_STATUS_EV,
+ fetch_effect=SubaruException("403 Error"),
+ )
+
+ test_entity = hass.states.get(TEST_ENTITY_ID)
+ assert test_entity.state == "unavailable"
+
+
+async def test_unload_entry(hass, ev_entry):
+ """Test that entry is unloaded."""
+ assert ev_entry.state == ENTRY_STATE_LOADED
+ assert await hass.config_entries.async_unload(ev_entry.entry_id)
+ await hass.async_block_till_done()
+ assert ev_entry.state == ENTRY_STATE_NOT_LOADED
diff --git a/tests/components/subaru/test_sensor.py b/tests/components/subaru/test_sensor.py
new file mode 100644
index 00000000000000..f2a66e7e5e915f
--- /dev/null
+++ b/tests/components/subaru/test_sensor.py
@@ -0,0 +1,72 @@
+"""Test Subaru sensors."""
+from unittest.mock import patch
+
+from homeassistant.components.subaru.const import VEHICLE_NAME
+from homeassistant.components.subaru.sensor import (
+ API_GEN_2_SENSORS,
+ EV_SENSORS,
+ SAFETY_SENSORS,
+ SENSOR_FIELD,
+ SENSOR_TYPE,
+)
+from homeassistant.util import slugify
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+
+from .api_responses import (
+ EXPECTED_STATE_EV_IMPERIAL,
+ EXPECTED_STATE_EV_METRIC,
+ EXPECTED_STATE_EV_UNAVAILABLE,
+ TEST_VIN_2_EV,
+ VEHICLE_DATA,
+ VEHICLE_STATUS_EV,
+)
+
+from tests.components.subaru.conftest import (
+ MOCK_API_FETCH,
+ MOCK_API_GET_DATA,
+ advance_time_to_next_fetch,
+)
+
+VEHICLE_NAME = VEHICLE_DATA[TEST_VIN_2_EV][VEHICLE_NAME]
+
+
+async def test_sensors_ev_imperial(hass, ev_entry):
+ """Test sensors supporting imperial units."""
+ hass.config.units = IMPERIAL_SYSTEM
+
+ with patch(MOCK_API_FETCH), patch(
+ MOCK_API_GET_DATA, return_value=VEHICLE_STATUS_EV
+ ):
+ advance_time_to_next_fetch(hass)
+ await hass.async_block_till_done()
+
+ _assert_data(hass, EXPECTED_STATE_EV_IMPERIAL)
+
+
+async def test_sensors_ev_metric(hass, ev_entry):
+ """Test sensors supporting metric units."""
+ _assert_data(hass, EXPECTED_STATE_EV_METRIC)
+
+
+async def test_sensors_missing_vin_data(hass, ev_entry):
+ """Test for missing VIN dataset."""
+ with patch(MOCK_API_FETCH), patch(MOCK_API_GET_DATA, return_value=None):
+ advance_time_to_next_fetch(hass)
+ await hass.async_block_till_done()
+
+ _assert_data(hass, EXPECTED_STATE_EV_UNAVAILABLE)
+
+
+def _assert_data(hass, expected_state):
+ sensor_list = EV_SENSORS
+ sensor_list.extend(API_GEN_2_SENSORS)
+ sensor_list.extend(SAFETY_SENSORS)
+ expected_states = {}
+ for item in sensor_list:
+ expected_states[
+ f"sensor.{slugify(f'{VEHICLE_NAME} {item[SENSOR_TYPE]}')}"
+ ] = expected_state[item[SENSOR_FIELD]]
+
+ for sensor in expected_states:
+ actual = hass.states.get(sensor)
+ assert actual.state == expected_states[sensor]
diff --git a/tests/components/sun/test_init.py b/tests/components/sun/test_init.py
index 1e95082b3581b5..800d3ab82fd4fd 100644
--- a/tests/components/sun/test_init.py
+++ b/tests/components/sun/test_init.py
@@ -22,18 +22,19 @@ async def test_setting_rising(hass, legacy_patchable_time):
await hass.async_block_till_done()
state = hass.states.get(sun.ENTITY_ID)
- from astral import Astral
+ from astral import LocationInfo
+ import astral.sun
- astral = Astral()
utc_today = utc_now.date()
- latitude = hass.config.latitude
- longitude = hass.config.longitude
+ location = LocationInfo(
+ latitude=hass.config.latitude, longitude=hass.config.longitude
+ )
mod = -1
while True:
- next_dawn = astral.dawn_utc(
- utc_today + timedelta(days=mod), latitude, longitude
+ next_dawn = astral.sun.dawn(
+ location.observer, date=utc_today + timedelta(days=mod)
)
if next_dawn > utc_now:
break
@@ -41,8 +42,8 @@ async def test_setting_rising(hass, legacy_patchable_time):
mod = -1
while True:
- next_dusk = astral.dusk_utc(
- utc_today + timedelta(days=mod), latitude, longitude
+ next_dusk = astral.sun.dusk(
+ location.observer, date=utc_today + timedelta(days=mod)
)
if next_dusk > utc_now:
break
@@ -50,8 +51,8 @@ async def test_setting_rising(hass, legacy_patchable_time):
mod = -1
while True:
- next_midnight = astral.solar_midnight_utc(
- utc_today + timedelta(days=mod), longitude
+ next_midnight = astral.sun.midnight(
+ location.observer, date=utc_today + timedelta(days=mod)
)
if next_midnight > utc_now:
break
@@ -59,15 +60,17 @@ async def test_setting_rising(hass, legacy_patchable_time):
mod = -1
while True:
- next_noon = astral.solar_noon_utc(utc_today + timedelta(days=mod), longitude)
+ next_noon = astral.sun.noon(
+ location.observer, date=utc_today + timedelta(days=mod)
+ )
if next_noon > utc_now:
break
mod += 1
mod = -1
while True:
- next_rising = astral.sunrise_utc(
- utc_today + timedelta(days=mod), latitude, longitude
+ next_rising = astral.sun.sunrise(
+ location.observer, date=utc_today + timedelta(days=mod)
)
if next_rising > utc_now:
break
@@ -75,8 +78,8 @@ async def test_setting_rising(hass, legacy_patchable_time):
mod = -1
while True:
- next_setting = astral.sunset_utc(
- utc_today + timedelta(days=mod), latitude, longitude
+ next_setting = astral.sun.sunset(
+ location.observer, date=utc_today + timedelta(days=mod)
)
if next_setting > utc_now:
break
@@ -152,10 +155,10 @@ async def test_norway_in_june(hass):
assert dt_util.parse_datetime(
state.attributes[sun.STATE_ATTR_NEXT_RISING]
- ) == datetime(2016, 7, 25, 23, 23, 39, tzinfo=dt_util.UTC)
+ ) == datetime(2016, 7, 24, 22, 59, 45, 689645, tzinfo=dt_util.UTC)
assert dt_util.parse_datetime(
state.attributes[sun.STATE_ATTR_NEXT_SETTING]
- ) == datetime(2016, 7, 26, 22, 19, 1, tzinfo=dt_util.UTC)
+ ) == datetime(2016, 7, 25, 22, 17, 13, 503932, tzinfo=dt_util.UTC)
assert state.state == sun.STATE_ABOVE_HORIZON
diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py
index a288150517dd2d..54dcef96e28e07 100644
--- a/tests/components/sun/test_trigger.py
+++ b/tests/components/sun/test_trigger.py
@@ -18,7 +18,7 @@
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed, async_mock_service, mock_component
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE
@@ -56,7 +56,10 @@ async def test_sunset_trigger(hass, calls, legacy_patchable_time):
{
automation.DOMAIN: {
"trigger": {"platform": "sun", "event": SUN_EVENT_SUNSET},
- "action": {"service": "test.automation"},
+ "action": {
+ "service": "test.automation",
+ "data_template": {"id": "{{ trigger.id}}"},
+ },
}
},
)
@@ -83,6 +86,7 @@ async def test_sunset_trigger(hass, calls, legacy_patchable_time):
async_fire_time_changed(hass, trigger_time)
await hass.async_block_till_done()
assert len(calls) == 1
+ assert calls[0].data["id"] == 0
async def test_sunrise_trigger(hass, calls, legacy_patchable_time):
@@ -184,17 +188,17 @@ async def test_if_action_before_sunrise_no_offset(hass, calls):
},
)
- # sunrise: 2015-09-16 06:32:43 local, sunset: 2015-09-16 18:55:24 local
- # sunrise: 2015-09-16 13:32:43 UTC, sunset: 2015-09-17 01:55:24 UTC
+ # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local
+ # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC
# now = sunrise + 1s -> 'before sunrise' not true
- now = datetime(2015, 9, 16, 13, 32, 44, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 0
# now = sunrise -> 'before sunrise' true
- now = datetime(2015, 9, 16, 13, 32, 43, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 16, 13, 33, 18, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
@@ -233,17 +237,17 @@ async def test_if_action_after_sunrise_no_offset(hass, calls):
},
)
- # sunrise: 2015-09-16 06:32:43 local, sunset: 2015-09-16 18:55:24 local
- # sunrise: 2015-09-16 13:32:43 UTC, sunset: 2015-09-17 01:55:24 UTC
+ # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local
+ # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC
# now = sunrise - 1s -> 'after sunrise' not true
- now = datetime(2015, 9, 16, 13, 32, 42, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 0
# now = sunrise + 1s -> 'after sunrise' true
- now = datetime(2015, 9, 16, 13, 32, 43, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
@@ -286,17 +290,17 @@ async def test_if_action_before_sunrise_with_offset(hass, calls):
},
)
- # sunrise: 2015-09-16 06:32:43 local, sunset: 2015-09-16 18:55:24 local
- # sunrise: 2015-09-16 13:32:43 UTC, sunset: 2015-09-17 01:55:24 UTC
+ # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local
+ # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC
# now = sunrise + 1s + 1h -> 'before sunrise' with offset +1h not true
- now = datetime(2015, 9, 16, 14, 32, 44, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 16, 14, 33, 19, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 0
# now = sunrise + 1h -> 'before sunrise' with offset +1h true
- now = datetime(2015, 9, 16, 14, 32, 43, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 16, 14, 33, 18, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
@@ -331,14 +335,14 @@ async def test_if_action_before_sunrise_with_offset(hass, calls):
assert len(calls) == 2
# now = sunset -> 'before sunrise' with offset +1h not true
- now = datetime(2015, 9, 17, 1, 56, 48, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 2
# now = sunset -1s -> 'before sunrise' with offset +1h not true
- now = datetime(2015, 9, 17, 1, 56, 45, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 17, 1, 53, 44, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
@@ -367,8 +371,8 @@ async def test_if_action_before_sunset_with_offset(hass, calls):
},
)
- # sunrise: 2015-09-16 06:32:43 local, sunset: 2015-09-16 18:55:24 local
- # sunrise: 2015-09-16 13:32:43 UTC, sunset: 2015-09-17 01:55:24 UTC
+ # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local
+ # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC
# now = local midnight -> 'before sunset' with offset +1h true
now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
@@ -377,14 +381,14 @@ async def test_if_action_before_sunset_with_offset(hass, calls):
assert len(calls) == 1
# now = sunset + 1s + 1h -> 'before sunset' with offset +1h not true
- now = datetime(2015, 9, 17, 2, 55, 25, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 17, 2, 53, 46, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 1
# now = sunset + 1h -> 'before sunset' with offset +1h true
- now = datetime(2015, 9, 17, 2, 55, 24, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 17, 2, 53, 44, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
@@ -405,14 +409,14 @@ async def test_if_action_before_sunset_with_offset(hass, calls):
assert len(calls) == 4
# now = sunrise -> 'before sunset' with offset +1h true
- now = datetime(2015, 9, 16, 13, 32, 43, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 16, 13, 33, 18, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 5
# now = sunrise -1s -> 'before sunset' with offset +1h true
- now = datetime(2015, 9, 16, 13, 32, 42, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
@@ -448,17 +452,17 @@ async def test_if_action_after_sunrise_with_offset(hass, calls):
},
)
- # sunrise: 2015-09-16 06:32:43 local, sunset: 2015-09-16 18:55:24 local
- # sunrise: 2015-09-16 13:32:43 UTC, sunset: 2015-09-17 01:55:24 UTC
+ # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local
+ # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC
# now = sunrise - 1s + 1h -> 'after sunrise' with offset +1h not true
- now = datetime(2015, 9, 16, 14, 32, 42, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 16, 14, 33, 17, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 0
# now = sunrise + 1h -> 'after sunrise' with offset +1h true
- now = datetime(2015, 9, 16, 14, 32, 43, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 16, 14, 33, 58, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
@@ -493,14 +497,14 @@ async def test_if_action_after_sunrise_with_offset(hass, calls):
assert len(calls) == 3
# now = sunset -> 'after sunrise' with offset +1h true
- now = datetime(2015, 9, 17, 1, 55, 24, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 4
# now = sunset + 1s -> 'after sunrise' with offset +1h true
- now = datetime(2015, 9, 17, 1, 55, 25, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
@@ -543,17 +547,17 @@ async def test_if_action_after_sunset_with_offset(hass, calls):
},
)
- # sunrise: 2015-09-15 06:32:05 local, sunset: 2015-09-15 18:56:46 local
- # sunrise: 2015-09-15 13:32:05 UTC, sunset: 2015-09-16 01:56:46 UTC
+ # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local
+ # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC
# now = sunset - 1s + 1h -> 'after sunset' with offset +1h not true
- now = datetime(2015, 9, 16, 2, 56, 45, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 17, 2, 53, 44, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 0
# now = sunset + 1h -> 'after sunset' with offset +1h true
- now = datetime(2015, 9, 16, 2, 56, 46, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 17, 2, 53, 45, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
@@ -596,31 +600,31 @@ async def test_if_action_before_and_after_during(hass, calls):
},
)
- # sunrise: 2015-09-16 06:32:43 local, sunset: 2015-09-16 18:55:24 local
- # sunrise: 2015-09-16 13:32:43 UTC, sunset: 2015-09-17 01:55:24 UTC
+ # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local
+ # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC
# now = sunrise - 1s -> 'after sunrise' + 'before sunset' not true
- now = datetime(2015, 9, 16, 13, 32, 42, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 0
# now = sunset + 1s -> 'after sunrise' + 'before sunset' not true
- now = datetime(2015, 9, 17, 1, 55, 25, tzinfo=dt_util.UTC)
+ now = datetime(2015, 9, 17, 1, 53, 46, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 0
- # now = sunrise -> 'after sunrise' + 'before sunset' true
- now = datetime(2015, 9, 16, 13, 32, 43, tzinfo=dt_util.UTC)
+ # now = sunrise + 1s -> 'after sunrise' + 'before sunset' true
+ now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 1
- # now = sunset -> 'after sunrise' + 'before sunset' true
- now = datetime(2015, 9, 17, 1, 55, 24, tzinfo=dt_util.UTC)
+ # now = sunset - 1s -> 'after sunrise' + 'before sunset' true
+ now = datetime(2015, 9, 17, 1, 53, 44, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
@@ -659,17 +663,17 @@ async def test_if_action_before_sunrise_no_offset_kotzebue(hass, calls):
},
)
- # sunrise: 2015-07-24 07:17:24 local, sunset: 2015-07-25 03:16:27 local
- # sunrise: 2015-07-24 15:17:24 UTC, sunset: 2015-07-25 11:16:27 UTC
+ # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local
+ # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC
# now = sunrise + 1s -> 'before sunrise' not true
- now = datetime(2015, 7, 24, 15, 17, 25, tzinfo=dt_util.UTC)
+ now = datetime(2015, 7, 24, 15, 21, 13, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 0
- # now = sunrise -> 'before sunrise' true
- now = datetime(2015, 7, 24, 15, 17, 24, tzinfo=dt_util.UTC)
+ # now = sunrise - 1h -> 'before sunrise' true
+ now = datetime(2015, 7, 24, 14, 21, 12, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
@@ -715,17 +719,17 @@ async def test_if_action_after_sunrise_no_offset_kotzebue(hass, calls):
},
)
- # sunrise: 2015-07-24 07:17:24 local, sunset: 2015-07-25 03:16:27 local
- # sunrise: 2015-07-24 15:17:24 UTC, sunset: 2015-07-25 11:16:27 UTC
+ # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local
+ # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC
# now = sunrise -> 'after sunrise' true
- now = datetime(2015, 7, 24, 15, 17, 24, tzinfo=dt_util.UTC)
+ now = datetime(2015, 7, 24, 15, 21, 12, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 1
- # now = sunrise - 1s -> 'after sunrise' not true
- now = datetime(2015, 7, 24, 15, 17, 23, tzinfo=dt_util.UTC)
+ # now = sunrise - 1h -> 'after sunrise' not true
+ now = datetime(2015, 7, 24, 14, 21, 12, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
@@ -771,17 +775,17 @@ async def test_if_action_before_sunset_no_offset_kotzebue(hass, calls):
},
)
- # sunrise: 2015-07-24 07:17:24 local, sunset: 2015-07-25 03:16:27 local
- # sunrise: 2015-07-24 15:17:24 UTC, sunset: 2015-07-25 11:16:27 UTC
- # now = sunrise + 1s -> 'before sunrise' not true
- now = datetime(2015, 7, 25, 11, 16, 28, tzinfo=dt_util.UTC)
+ # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local
+ # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC
+ # now = sunset + 1s -> 'before sunset' not true
+ now = datetime(2015, 7, 25, 11, 13, 34, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 0
- # now = sunrise -> 'before sunrise' true
- now = datetime(2015, 7, 25, 11, 16, 27, tzinfo=dt_util.UTC)
+ # now = sunset - 1h-> 'before sunset' true
+ now = datetime(2015, 7, 25, 10, 13, 33, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
@@ -827,17 +831,17 @@ async def test_if_action_after_sunset_no_offset_kotzebue(hass, calls):
},
)
- # sunrise: 2015-07-24 07:17:24 local, sunset: 2015-07-25 03:16:27 local
- # sunrise: 2015-07-24 15:17:24 UTC, sunset: 2015-07-25 11:16:27 UTC
+ # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local
+ # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC
# now = sunset -> 'after sunset' true
- now = datetime(2015, 7, 25, 11, 16, 27, tzinfo=dt_util.UTC)
+ now = datetime(2015, 7, 25, 11, 13, 33, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 1
# now = sunset - 1s -> 'after sunset' not true
- now = datetime(2015, 7, 25, 11, 16, 26, tzinfo=dt_util.UTC)
+ now = datetime(2015, 7, 25, 11, 13, 32, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
diff --git a/tests/components/surepetcare/test_binary_sensor.py b/tests/components/surepetcare/test_binary_sensor.py
index ad723f707f5bb3..67755dbd6450d0 100644
--- a/tests/components/surepetcare/test_binary_sensor.py
+++ b/tests/components/surepetcare/test_binary_sensor.py
@@ -2,6 +2,7 @@
from surepy import MESTART_RESOURCE
from homeassistant.components.surepetcare.const import DOMAIN
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from . import MOCK_API_DATA, MOCK_CONFIG, _patch_sensor_setup
@@ -24,7 +25,7 @@ async def test_binary_sensors(hass, surepetcare) -> None:
assert await async_setup_component(hass, DOMAIN, MOCK_CONFIG)
await hass.async_block_till_done()
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
state_entity_ids = hass.states.async_entity_ids()
for entity_id, unique_id in EXPECTED_ENTITY_IDS.items():
diff --git a/tests/components/switch/conftest.py b/tests/components/switch/conftest.py
index d69757a9b1b929..11f1563b72387a 100644
--- a/tests/components/switch/conftest.py
+++ b/tests/components/switch/conftest.py
@@ -1,2 +1,2 @@
"""switch conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py
index 4da401a215c7e6..2a98cd2fad49d4 100644
--- a/tests/components/switch/test_device_action.py
+++ b/tests/components/switch/test_device_action.py
@@ -16,7 +16,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py
index 67ba3e8e38e1a0..9273610dee9bbc 100644
--- a/tests/components/switch/test_device_condition.py
+++ b/tests/components/switch/test_device_condition.py
@@ -19,7 +19,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py
index 34817b687f8006..d958dd21911d57 100644
--- a/tests/components/switch/test_device_trigger.py
+++ b/tests/components/switch/test_device_trigger.py
@@ -19,7 +19,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py
index e7303a20ea5619..fda5f39922d4fd 100644
--- a/tests/components/switcher_kis/conftest.py
+++ b/tests/components/switcher_kis/conftest.py
@@ -1,8 +1,9 @@
"""Common fixtures and objects for the Switcher integration tests."""
+from __future__ import annotations
from asyncio import Queue
from datetime import datetime
-from typing import Any, Generator, Optional
+from typing import Any, Generator
from unittest.mock import AsyncMock, patch
from pytest import fixture
@@ -56,7 +57,7 @@ def state(self) -> str:
return DUMMY_DEVICE_STATE
@property
- def remaining_time(self) -> Optional[str]:
+ def remaining_time(self) -> str | None:
"""Return the time left to auto-off."""
return DUMMY_REMAINING_TIME
diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py
index 287e7139affdd2..ef54788d91078b 100644
--- a/tests/components/system_log/test_init.py
+++ b/tests/components/system_log/test_init.py
@@ -291,20 +291,19 @@ async def async_log_error_from_test_path(hass, path, sq):
call_path = "internal_path.py"
with patch.object(
_LOGGER, "findCaller", MagicMock(return_value=(call_path, 0, None, None))
+ ), patch(
+ "traceback.extract_stack",
+ MagicMock(
+ return_value=[
+ get_frame("main_path/main.py"),
+ get_frame(path),
+ get_frame(call_path),
+ get_frame("venv_path/logging/log.py"),
+ ]
+ ),
):
- with patch(
- "traceback.extract_stack",
- MagicMock(
- return_value=[
- get_frame("main_path/main.py"),
- get_frame(path),
- get_frame(call_path),
- get_frame("venv_path/logging/log.py"),
- ]
- ),
- ):
- _LOGGER.error("error message")
- await _async_block_until_queue_empty(hass, sq)
+ _LOGGER.error("error message")
+ await _async_block_until_queue_empty(hass, sq)
async def test_homeassistant_path(hass, simple_queue, hass_client):
diff --git a/tests/components/tado/test_sensor.py b/tests/components/tado/test_sensor.py
index 2fac88bc22e90e..bb926ff1ae2c78 100644
--- a/tests/components/tado/test_sensor.py
+++ b/tests/components/tado/test_sensor.py
@@ -21,6 +21,21 @@ async def test_air_con_create_sensors(hass):
assert state.state == "60.9"
+async def test_home_create_sensors(hass):
+ """Test creation of home sensors."""
+
+ await async_init_integration(hass)
+
+ state = hass.states.get("sensor.home_name_outdoor_temperature")
+ assert state.state == "7.46"
+
+ state = hass.states.get("sensor.home_name_solar_percentage")
+ assert state.state == "2.1"
+
+ state = hass.states.get("sensor.home_name_weather_condition")
+ assert state.state == "fog"
+
+
async def test_heater_create_sensors(hass):
"""Test creation of heater sensors."""
diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py
index d27ede47a63303..ce1dd92942de7e 100644
--- a/tests/components/tado/util.py
+++ b/tests/components/tado/util.py
@@ -18,6 +18,7 @@ async def async_init_integration(
token_fixture = "tado/token.json"
devices_fixture = "tado/devices.json"
me_fixture = "tado/me.json"
+ weather_fixture = "tado/weather.json"
zones_fixture = "tado/zones.json"
# WR1 Device
@@ -46,12 +47,19 @@ async def async_init_integration(
# Device Temp Offset
device_temp_offset = "tado/device_temp_offset.json"
+ # Zone Default Overlay
+ zone_def_overlay = "tado/zone_default_overlay.json"
+
with requests_mock.mock() as m:
m.post("https://auth.tado.com/oauth/token", text=load_fixture(token_fixture))
m.get(
"https://my.tado.com/api/v2/me",
text=load_fixture(me_fixture),
)
+ m.get(
+ "https://my.tado.com/api/v2/homes/1/weather",
+ text=load_fixture(weather_fixture),
+ )
m.get(
"https://my.tado.com/api/v2/homes/1/devices",
text=load_fixture(devices_fixture),
@@ -92,6 +100,26 @@ async def async_init_integration(
"https://my.tado.com/api/v2/homes/1/zones/1/capabilities",
text=load_fixture(zone_1_capabilities_fixture),
)
+ m.get(
+ "https://my.tado.com/api/v2/homes/1/zones/1/defaultOverlay",
+ text=load_fixture(zone_def_overlay),
+ )
+ m.get(
+ "https://my.tado.com/api/v2/homes/1/zones/2/defaultOverlay",
+ text=load_fixture(zone_def_overlay),
+ )
+ m.get(
+ "https://my.tado.com/api/v2/homes/1/zones/3/defaultOverlay",
+ text=load_fixture(zone_def_overlay),
+ )
+ m.get(
+ "https://my.tado.com/api/v2/homes/1/zones/4/defaultOverlay",
+ text=load_fixture(zone_def_overlay),
+ )
+ m.get(
+ "https://my.tado.com/api/v2/homes/1/zones/5/defaultOverlay",
+ text=load_fixture(zone_def_overlay),
+ )
m.get(
"https://my.tado.com/api/v2/homes/1/zones/5/state",
text=load_fixture(zone_5_state_fixture),
diff --git a/tests/components/tag/test_trigger.py b/tests/components/tag/test_trigger.py
index 9a97d95e7d54c1..3976c3193d1da1 100644
--- a/tests/components/tag/test_trigger.py
+++ b/tests/components/tag/test_trigger.py
@@ -9,7 +9,7 @@
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
@@ -50,7 +50,10 @@ async def test_triggers(hass, tag_setup, calls):
"trigger": {"platform": DOMAIN, TAG_ID: "abc123"},
"action": {
"service": "test.automation",
- "data": {"message": "service called"},
+ "data_template": {
+ "message": "service called",
+ "id": "{{ trigger.id}}",
+ },
},
}
]
@@ -64,6 +67,7 @@ async def test_triggers(hass, tag_setup, calls):
assert len(calls) == 1
assert calls[0].data["message"] == "service called"
+ assert calls[0].data["id"] == 0
await hass.services.async_call(
automation.DOMAIN,
diff --git a/tests/components/tasmota/conftest.py b/tests/components/tasmota/conftest.py
index 3c530a93d1ebbb..5c0b7d315f9e64 100644
--- a/tests/components/tasmota/conftest.py
+++ b/tests/components/tasmota/conftest.py
@@ -17,7 +17,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
@pytest.fixture
diff --git a/tests/components/tasmota/test_binary_sensor.py b/tests/components/tasmota/test_binary_sensor.py
index 6d4263853dcce2..6b13dcc89ec5ad 100644
--- a/tests/components/tasmota/test_binary_sensor.py
+++ b/tests/components/tasmota/test_binary_sensor.py
@@ -95,6 +95,12 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
state = hass.states.get("binary_sensor.tasmota_binary_sensor_1")
assert state.state == STATE_OFF
+ # Test force update flag
+ entity = hass.data["entity_components"]["binary_sensor"].get_entity(
+ "binary_sensor.tasmota_binary_sensor_1"
+ )
+ assert entity.force_update
+
async def test_controlling_state_via_mqtt_switchname(hass, mqtt_mock, setup_tasmota):
"""Test state update via MQTT."""
diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py
index 973ecd3c890a57..74e8d2a5e590ad 100644
--- a/tests/components/tasmota/test_common.py
+++ b/tests/components/tasmota/test_common.py
@@ -20,6 +20,7 @@
from homeassistant.components.tasmota.const import DEFAULT_PREFIX
from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.common import async_fire_mqtt_message
@@ -363,8 +364,8 @@ async def help_test_discovery_removal(
name="Test",
):
"""Test removal of discovered entity."""
- device_reg = await hass.helpers.device_registry.async_get_registry()
- entity_reg = await hass.helpers.entity_registry.async_get_registry()
+ device_reg = dr.async_get(hass)
+ entity_reg = er.async_get(hass)
data1 = json.dumps(config1)
data2 = json.dumps(config2)
@@ -470,8 +471,8 @@ async def help_test_discovery_device_remove(
hass, mqtt_mock, domain, unique_id, config, sensor_config=None
):
"""Test domain entity is removed when device is removed."""
- device_reg = await hass.helpers.device_registry.async_get_registry()
- entity_reg = await hass.helpers.entity_registry.async_get_registry()
+ device_reg = dr.async_get(hass)
+ entity_reg = er.async_get(hass)
config = copy.deepcopy(config)
@@ -502,7 +503,7 @@ async def help_test_entity_id_update_subscriptions(
hass, mqtt_mock, domain, config, topics=None, sensor_config=None, entity_id="test"
):
"""Test MQTT subscriptions are managed when entity_id is updated."""
- entity_reg = await hass.helpers.entity_registry.async_get_registry()
+ entity_reg = er.async_get(hass)
config = copy.deepcopy(config)
data = json.dumps(config)
@@ -548,7 +549,7 @@ async def help_test_entity_id_update_discovery_update(
hass, mqtt_mock, domain, config, sensor_config=None, entity_id="test"
):
"""Test MQTT discovery update after entity_id is updated."""
- entity_reg = await hass.helpers.entity_registry.async_get_registry()
+ entity_reg = er.async_get(hass)
config = copy.deepcopy(config)
data = json.dumps(config)
diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py
index ec8744881c5f08..1fa1c629a337be 100644
--- a/tests/components/tasmota/test_device_trigger.py
+++ b/tests/components/tasmota/test_device_trigger.py
@@ -18,7 +18,7 @@
async_fire_mqtt_message,
async_get_device_automations,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
async def test_get_triggers_btn(hass, device_reg, entity_reg, mqtt_mock, setup_tasmota):
diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py
index 4035c877bb87c5..a64c5e9c5e4136 100644
--- a/tests/components/tasmota/test_fan.py
+++ b/tests/components/tasmota/test_fan.py
@@ -52,6 +52,7 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
state = hass.states.get("fan.tasmota")
assert state.state == STATE_OFF
assert state.attributes["speed"] is None
+ assert state.attributes["percentage"] is None
assert state.attributes["speed_list"] == ["off", "low", "medium", "high"]
assert state.attributes["supported_features"] == fan.SUPPORT_SET_SPEED
assert not state.attributes.get(ATTR_ASSUMED_STATE)
@@ -60,31 +61,37 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
state = hass.states.get("fan.tasmota")
assert state.state == STATE_ON
assert state.attributes["speed"] == "low"
+ assert state.attributes["percentage"] == 33
async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":2}')
state = hass.states.get("fan.tasmota")
assert state.state == STATE_ON
assert state.attributes["speed"] == "medium"
+ assert state.attributes["percentage"] == 66
async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":3}')
state = hass.states.get("fan.tasmota")
assert state.state == STATE_ON
assert state.attributes["speed"] == "high"
+ assert state.attributes["percentage"] == 100
async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":0}')
state = hass.states.get("fan.tasmota")
assert state.state == STATE_OFF
assert state.attributes["speed"] == "off"
+ assert state.attributes["percentage"] == 0
async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"FanSpeed":1}')
state = hass.states.get("fan.tasmota")
assert state.state == STATE_ON
assert state.attributes["speed"] == "low"
+ assert state.attributes["percentage"] == 33
async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"FanSpeed":0}')
state = hass.states.get("fan.tasmota")
assert state.state == STATE_OFF
assert state.attributes["speed"] == "off"
+ assert state.attributes["percentage"] == 0
async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota):
@@ -151,6 +158,34 @@ async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota):
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/FanSpeed", "3", 0, False
)
+ mqtt_mock.async_publish.reset_mock()
+
+ # Set speed percentage and verify MQTT message is sent
+ await common.async_set_percentage(hass, "fan.tasmota", 0)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/FanSpeed", "0", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Set speed percentage and verify MQTT message is sent
+ await common.async_set_percentage(hass, "fan.tasmota", 15)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/FanSpeed", "1", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Set speed percentage and verify MQTT message is sent
+ await common.async_set_percentage(hass, "fan.tasmota", 50)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/FanSpeed", "2", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Set speed percentage and verify MQTT message is sent
+ await common.async_set_percentage(hass, "fan.tasmota", 90)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/FanSpeed", "3", 0, False
+ )
async def test_invalid_fan_speed(hass, mqtt_mock, setup_tasmota):
@@ -176,7 +211,7 @@ async def test_invalid_fan_speed(hass, mqtt_mock, setup_tasmota):
# Set an unsupported speed and verify MQTT message is not sent
with pytest.raises(ValueError) as excinfo:
await common.async_set_speed(hass, "fan.tasmota", "no_such_speed")
- assert "Unsupported speed no_such_speed" in str(excinfo.value)
+ assert "no_such_speed" in str(excinfo.value)
mqtt_mock.async_publish.assert_not_called()
diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py
index d64e39aacf0d00..a60f167c38f602 100644
--- a/tests/components/tasmota/test_light.py
+++ b/tests/components/tasmota/test_light.py
@@ -924,7 +924,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota):
await common.async_turn_on(hass, "light.test", brightness=255, transition=4)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade 1;NoDelay;Speed 8;NoDelay;Dimmer 100",
+ "NoDelay;Fade2 1;NoDelay;Speed2 8;NoDelay;Dimmer 100",
0,
False,
)
@@ -934,7 +934,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota):
await common.async_turn_on(hass, "light.test", brightness=255, transition=100)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade 1;NoDelay;Speed 40;NoDelay;Dimmer 100",
+ "NoDelay;Fade2 1;NoDelay;Speed2 40;NoDelay;Dimmer 100",
0,
False,
)
@@ -944,7 +944,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota):
await common.async_turn_on(hass, "light.test", brightness=0, transition=100)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade 1;NoDelay;Speed 1;NoDelay;Power1 OFF",
+ "NoDelay;Fade2 1;NoDelay;Speed2 1;NoDelay;Power1 OFF",
0,
False,
)
@@ -954,7 +954,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota):
await common.async_turn_on(hass, "light.test", brightness=128, transition=4)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade 1;NoDelay;Speed 16;NoDelay;Dimmer 50",
+ "NoDelay;Fade2 1;NoDelay;Speed2 16;NoDelay;Dimmer 50",
0,
False,
)
@@ -972,7 +972,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota):
await common.async_turn_off(hass, "light.test", transition=6)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade 1;NoDelay;Speed 24;NoDelay;Power1 OFF",
+ "NoDelay;Fade2 1;NoDelay;Speed2 24;NoDelay;Power1 OFF",
0,
False,
)
@@ -990,7 +990,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota):
await common.async_turn_off(hass, "light.test", transition=0)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade 0;NoDelay;Power1 OFF",
+ "NoDelay;Fade2 0;NoDelay;Power1 OFF",
0,
False,
)
@@ -1011,7 +1011,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota):
await common.async_turn_on(hass, "light.test", rgb_color=[255, 0, 0], transition=6)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade 1;NoDelay;Speed 24;NoDelay;Power1 ON;NoDelay;Color2 255,0,0",
+ "NoDelay;Fade2 1;NoDelay;Speed2 24;NoDelay;Power1 ON;NoDelay;Color2 255,0,0",
0,
False,
)
@@ -1032,7 +1032,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota):
await common.async_turn_on(hass, "light.test", rgb_color=[255, 0, 0], transition=6)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade 1;NoDelay;Speed 12;NoDelay;Power1 ON;NoDelay;Color2 255,0,0",
+ "NoDelay;Fade2 1;NoDelay;Speed2 12;NoDelay;Power1 ON;NoDelay;Color2 255,0,0",
0,
False,
)
@@ -1051,7 +1051,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota):
await common.async_turn_on(hass, "light.test", color_temp=500, transition=6)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade 1;NoDelay;Speed 24;NoDelay;Power1 ON;NoDelay;CT 500",
+ "NoDelay;Fade2 1;NoDelay;Speed2 24;NoDelay;Power1 ON;NoDelay;CT 500",
0,
False,
)
@@ -1070,7 +1070,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota):
await common.async_turn_on(hass, "light.test", color_temp=326, transition=6)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade 1;NoDelay;Speed 40;NoDelay;Power1 ON;NoDelay;CT 326",
+ "NoDelay;Fade2 1;NoDelay;Speed2 40;NoDelay;Power1 ON;NoDelay;CT 326",
0,
False,
)
@@ -1103,7 +1103,7 @@ async def test_transition_fixed(hass, mqtt_mock, setup_tasmota):
await common.async_turn_on(hass, "light.test", brightness=255, transition=4)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade 1;NoDelay;Speed 8;NoDelay;Dimmer 100",
+ "NoDelay;Fade2 1;NoDelay;Speed2 8;NoDelay;Dimmer 100",
0,
False,
)
@@ -1113,7 +1113,7 @@ async def test_transition_fixed(hass, mqtt_mock, setup_tasmota):
await common.async_turn_on(hass, "light.test", brightness=255, transition=100)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade 1;NoDelay;Speed 40;NoDelay;Dimmer 100",
+ "NoDelay;Fade2 1;NoDelay;Speed2 40;NoDelay;Dimmer 100",
0,
False,
)
@@ -1123,7 +1123,7 @@ async def test_transition_fixed(hass, mqtt_mock, setup_tasmota):
await common.async_turn_on(hass, "light.test", brightness=0, transition=4)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade 1;NoDelay;Speed 8;NoDelay;Power1 OFF",
+ "NoDelay;Fade2 1;NoDelay;Speed2 8;NoDelay;Power1 OFF",
0,
False,
)
@@ -1133,7 +1133,7 @@ async def test_transition_fixed(hass, mqtt_mock, setup_tasmota):
await common.async_turn_on(hass, "light.test", brightness=128, transition=4)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade 1;NoDelay;Speed 8;NoDelay;Dimmer 50",
+ "NoDelay;Fade2 1;NoDelay;Speed2 8;NoDelay;Dimmer 50",
0,
False,
)
@@ -1143,7 +1143,7 @@ async def test_transition_fixed(hass, mqtt_mock, setup_tasmota):
await common.async_turn_on(hass, "light.test", brightness=128, transition=0)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade 0;NoDelay;Dimmer 50",
+ "NoDelay;Fade2 0;NoDelay;Dimmer 50",
0,
False,
)
diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py
index fe415c264efb3c..2b7c388ca2f19f 100644
--- a/tests/components/tasmota/test_sensor.py
+++ b/tests/components/tasmota/test_sensor.py
@@ -17,6 +17,7 @@
from homeassistant.components import sensor
from homeassistant.components.tasmota.const import DEFAULT_PREFIX
from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNKNOWN
+from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt
from .test_common import (
@@ -226,7 +227,7 @@ async def test_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
@pytest.mark.parametrize("status_sensor_disabled", [False])
async def test_status_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
"""Test state update via MQTT."""
- entity_reg = await hass.helpers.entity_registry.async_get_registry()
+ entity_reg = er.async_get(hass)
# Pre-enable the status sensor
entity_reg.async_get_or_create(
@@ -275,11 +276,17 @@ async def test_status_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
state = hass.states.get("sensor.tasmota_status")
assert state.state == "20.0"
+ # Test force update flag
+ entity = hass.data["entity_components"]["sensor"].get_entity(
+ "sensor.tasmota_status"
+ )
+ assert entity.force_update
+
@pytest.mark.parametrize("status_sensor_disabled", [False])
async def test_single_shot_status_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
"""Test state update via MQTT."""
- entity_reg = await hass.helpers.entity_registry.async_get_registry()
+ entity_reg = er.async_get(hass)
# Pre-enable the status sensor
entity_reg.async_get_or_create(
@@ -363,7 +370,7 @@ async def test_restart_time_status_sensor_state_via_mqtt(
hass, mqtt_mock, setup_tasmota
):
"""Test state update via MQTT."""
- entity_reg = await hass.helpers.entity_registry.async_get_registry()
+ entity_reg = er.async_get(hass)
# Pre-enable the status sensor
entity_reg.async_get_or_create(
@@ -439,9 +446,9 @@ async def test_attributes(hass, mqtt_mock, setup_tasmota):
assert state.attributes.get("unit_of_measurement") == "°C"
state = hass.states.get("sensor.tasmota_beer_CarbonDioxide")
- assert state.attributes.get("device_class") is None
+ assert state.attributes.get("device_class") == "carbon_dioxide"
assert state.attributes.get("friendly_name") == "Tasmota Beer CarbonDioxide"
- assert state.attributes.get("icon") == "mdi:molecule-co2"
+ assert state.attributes.get("icon") is None
assert state.attributes.get("unit_of_measurement") == "ppm"
@@ -509,16 +516,16 @@ async def test_indexed_sensor_attributes(hass, mqtt_mock, setup_tasmota):
assert state.attributes.get("unit_of_measurement") == "°C"
state = hass.states.get("sensor.tasmota_dummy2_carbondioxide_1")
- assert state.attributes.get("device_class") is None
+ assert state.attributes.get("device_class") == "carbon_dioxide"
assert state.attributes.get("friendly_name") == "Tasmota Dummy2 CarbonDioxide 1"
- assert state.attributes.get("icon") == "mdi:molecule-co2"
+ assert state.attributes.get("icon") is None
assert state.attributes.get("unit_of_measurement") == "ppm"
@pytest.mark.parametrize("status_sensor_disabled", [False])
async def test_enable_status_sensor(hass, mqtt_mock, setup_tasmota):
"""Test enabling status sensor."""
- entity_reg = await hass.helpers.entity_registry.async_get_registry()
+ entity_reg = er.async_get(hass)
config = copy.deepcopy(DEFAULT_CONFIG)
mac = config["mac"]
diff --git a/tests/components/template/conftest.py b/tests/components/template/conftest.py
index 8e3491b160ab7c..0848200b35d8f4 100644
--- a/tests/components/template/conftest.py
+++ b/tests/components/template/conftest.py
@@ -1,2 +1,2 @@
"""template conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py
index 241ac88328e939..76602b394339e2 100644
--- a/tests/components/template/test_binary_sensor.py
+++ b/tests/components/template/test_binary_sensor.py
@@ -15,7 +15,7 @@
from homeassistant.core import CoreState
import homeassistant.util.dt as dt_util
-from tests.common import assert_setup_component, async_fire_time_changed
+from tests.common import async_fire_time_changed
async def test_setup(hass):
@@ -32,85 +32,79 @@ async def test_setup(hass):
},
}
}
- with assert_setup_component(1):
- assert await setup.async_setup_component(hass, binary_sensor.DOMAIN, config)
+ assert await setup.async_setup_component(hass, binary_sensor.DOMAIN, config)
async def test_setup_no_sensors(hass):
"""Test setup with no sensors."""
- with assert_setup_component(0):
- assert await setup.async_setup_component(
- hass, binary_sensor.DOMAIN, {"binary_sensor": {"platform": "template"}}
- )
+ assert await setup.async_setup_component(
+ hass, binary_sensor.DOMAIN, {"binary_sensor": {"platform": "template"}}
+ )
async def test_setup_invalid_device(hass):
"""Test the setup with invalid devices."""
- with assert_setup_component(0):
- assert await setup.async_setup_component(
- hass,
- binary_sensor.DOMAIN,
- {"binary_sensor": {"platform": "template", "sensors": {"foo bar": {}}}},
- )
+ assert await setup.async_setup_component(
+ hass,
+ binary_sensor.DOMAIN,
+ {"binary_sensor": {"platform": "template", "sensors": {"foo bar": {}}}},
+ )
async def test_setup_invalid_device_class(hass):
"""Test setup with invalid sensor class."""
- with assert_setup_component(0):
- assert await setup.async_setup_component(
- hass,
- binary_sensor.DOMAIN,
- {
- "binary_sensor": {
- "platform": "template",
- "sensors": {
- "test": {
- "value_template": "{{ foo }}",
- "device_class": "foobarnotreal",
- }
- },
- }
- },
- )
+ assert await setup.async_setup_component(
+ hass,
+ binary_sensor.DOMAIN,
+ {
+ "binary_sensor": {
+ "platform": "template",
+ "sensors": {
+ "test": {
+ "value_template": "{{ foo }}",
+ "device_class": "foobarnotreal",
+ }
+ },
+ }
+ },
+ )
async def test_setup_invalid_missing_template(hass):
"""Test setup with invalid and missing template."""
- with assert_setup_component(0):
- assert await setup.async_setup_component(
- hass,
- binary_sensor.DOMAIN,
- {
- "binary_sensor": {
- "platform": "template",
- "sensors": {"test": {"device_class": "motion"}},
- }
- },
- )
+ assert await setup.async_setup_component(
+ hass,
+ binary_sensor.DOMAIN,
+ {
+ "binary_sensor": {
+ "platform": "template",
+ "sensors": {"test": {"device_class": "motion"}},
+ }
+ },
+ )
async def test_icon_template(hass):
"""Test icon template."""
- with assert_setup_component(1):
- assert await setup.async_setup_component(
- hass,
- binary_sensor.DOMAIN,
- {
- "binary_sensor": {
- "platform": "template",
- "sensors": {
- "test_template_sensor": {
- "value_template": "{{ states.sensor.xyz.state }}",
- "icon_template": "{% if "
- "states.binary_sensor.test_state.state == "
- "'Works' %}"
- "mdi:check"
- "{% endif %}",
- }
- },
- }
- },
- )
+ assert await setup.async_setup_component(
+ hass,
+ binary_sensor.DOMAIN,
+ {
+ "binary_sensor": {
+ "platform": "template",
+ "sensors": {
+ "test_template_sensor": {
+ "value_template": "{{ states.sensor.xyz.state }}",
+ "icon_template": "{% if "
+ "states.binary_sensor.test_state.state == "
+ "'Works' %}"
+ "mdi:check"
+ "{% endif %}",
+ }
+ },
+ }
+ },
+ )
await hass.async_block_till_done()
await hass.async_start()
@@ -127,26 +121,25 @@ async def test_icon_template(hass):
async def test_entity_picture_template(hass):
"""Test entity_picture template."""
- with assert_setup_component(1):
- assert await setup.async_setup_component(
- hass,
- binary_sensor.DOMAIN,
- {
- "binary_sensor": {
- "platform": "template",
- "sensors": {
- "test_template_sensor": {
- "value_template": "{{ states.sensor.xyz.state }}",
- "entity_picture_template": "{% if "
- "states.binary_sensor.test_state.state == "
- "'Works' %}"
- "/local/sensor.png"
- "{% endif %}",
- }
- },
- }
- },
- )
+ assert await setup.async_setup_component(
+ hass,
+ binary_sensor.DOMAIN,
+ {
+ "binary_sensor": {
+ "platform": "template",
+ "sensors": {
+ "test_template_sensor": {
+ "value_template": "{{ states.sensor.xyz.state }}",
+ "entity_picture_template": "{% if "
+ "states.binary_sensor.test_state.state == "
+ "'Works' %}"
+ "/local/sensor.png"
+ "{% endif %}",
+ }
+ },
+ }
+ },
+ )
await hass.async_block_till_done()
await hass.async_start()
@@ -163,24 +156,23 @@ async def test_entity_picture_template(hass):
async def test_attribute_templates(hass):
"""Test attribute_templates template."""
- with assert_setup_component(1):
- assert await setup.async_setup_component(
- hass,
- binary_sensor.DOMAIN,
- {
- "binary_sensor": {
- "platform": "template",
- "sensors": {
- "test_template_sensor": {
- "value_template": "{{ states.sensor.xyz.state }}",
- "attribute_templates": {
- "test_attribute": "It {{ states.sensor.test_state.state }}."
- },
- }
- },
- }
- },
- )
+ assert await setup.async_setup_component(
+ hass,
+ binary_sensor.DOMAIN,
+ {
+ "binary_sensor": {
+ "platform": "template",
+ "sensors": {
+ "test_template_sensor": {
+ "value_template": "{{ states.sensor.xyz.state }}",
+ "attribute_templates": {
+ "test_attribute": "It {{ states.sensor.test_state.state }}."
+ },
+ }
+ },
+ }
+ },
+ )
await hass.async_block_till_done()
await hass.async_start()
@@ -202,35 +194,34 @@ async def test_match_all(hass):
"homeassistant.components.template.binary_sensor."
"BinarySensorTemplate._update_state"
) as _update_state:
- with assert_setup_component(1):
- assert await setup.async_setup_component(
- hass,
- binary_sensor.DOMAIN,
- {
- "binary_sensor": {
- "platform": "template",
- "sensors": {
- "match_all_template_sensor": {
- "value_template": (
- "{% for state in states %}"
- "{% if state.entity_id == 'sensor.humidity' %}"
- "{{ state.entity_id }}={{ state.state }}"
- "{% endif %}"
- "{% endfor %}"
- ),
- },
+ assert await setup.async_setup_component(
+ hass,
+ binary_sensor.DOMAIN,
+ {
+ "binary_sensor": {
+ "platform": "template",
+ "sensors": {
+ "match_all_template_sensor": {
+ "value_template": (
+ "{% for state in states %}"
+ "{% if state.entity_id == 'sensor.humidity' %}"
+ "{{ state.entity_id }}={{ state.state }}"
+ "{% endif %}"
+ "{% endfor %}"
+ ),
},
- }
- },
- )
+ },
+ }
+ },
+ )
- await hass.async_start()
- await hass.async_block_till_done()
- init_calls = len(_update_state.mock_calls)
+ await hass.async_start()
+ await hass.async_block_till_done()
+ init_calls = len(_update_state.mock_calls)
- hass.states.async_set("sensor.any_state", "update")
- await hass.async_block_till_done()
- assert len(_update_state.mock_calls) == init_calls
+ hass.states.async_set("sensor.any_state", "update")
+ await hass.async_block_till_done()
+ assert len(_update_state.mock_calls) == init_calls
async def test_event(hass):
@@ -247,8 +238,7 @@ async def test_event(hass):
},
}
}
- with assert_setup_component(1):
- assert await setup.async_setup_component(hass, binary_sensor.DOMAIN, config)
+ assert await setup.async_setup_component(hass, binary_sensor.DOMAIN, config)
await hass.async_block_till_done()
await hass.async_start()
diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py
index 08c789633fc393..c1309a16e67b60 100644
--- a/tests/components/template/test_cover.py
+++ b/tests/components/template/test_cover.py
@@ -1,4 +1,4 @@
-"""The tests the cover command line platform."""
+"""The tests for the Template cover platform."""
import pytest
from homeassistant import setup
@@ -15,9 +15,11 @@
SERVICE_TOGGLE,
SERVICE_TOGGLE_COVER_TILT,
STATE_CLOSED,
+ STATE_CLOSING,
STATE_OFF,
STATE_ON,
STATE_OPEN,
+ STATE_OPENING,
STATE_UNAVAILABLE,
)
@@ -74,6 +76,18 @@ async def test_template_state_text(hass, calls):
state = hass.states.get("cover.test_template_cover")
assert state.state == STATE_CLOSED
+ state = hass.states.async_set("cover.test_state", STATE_OPENING)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("cover.test_template_cover")
+ assert state.state == STATE_OPENING
+
+ state = hass.states.async_set("cover.test_state", STATE_CLOSING)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("cover.test_template_cover")
+ assert state.state == STATE_CLOSING
+
async def test_template_state_boolean(hass, calls):
"""Test the value_template attribute."""
diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py
index b3927ad3118d04..b5820cd5c760ea 100644
--- a/tests/components/template/test_fan.py
+++ b/tests/components/template/test_fan.py
@@ -203,6 +203,7 @@ async def test_templates_with_entities(hass, calls):
"preset_mode_template": "{{ states('input_select.preset_mode') }}",
"oscillating_template": "{{ states('input_select.osc') }}",
"direction_template": "{{ states('input_select.direction') }}",
+ "speed_count": "3",
"set_percentage": {
"service": "script.fans_set_speed",
"data_template": {"percentage": "{{ percentage }}"},
@@ -245,6 +246,10 @@ async def test_templates_with_entities(hass, calls):
await hass.async_block_till_done()
_verify(hass, STATE_ON, None, 0, True, DIRECTION_FORWARD, None)
+ hass.states.async_set(_STATE_INPUT_BOOLEAN, False)
+ await hass.async_block_till_done()
+ _verify(hass, STATE_OFF, None, 0, True, DIRECTION_FORWARD, None)
+
async def test_templates_with_entities_and_invalid_percentage(hass, calls):
"""Test templates with values from other entities."""
@@ -273,7 +278,7 @@ async def test_templates_with_entities_and_invalid_percentage(hass, calls):
await hass.async_start()
await hass.async_block_till_done()
- _verify(hass, STATE_OFF, SPEED_OFF, 0, None, None, None)
+ _verify(hass, STATE_ON, SPEED_OFF, 0, None, None, None)
hass.states.async_set("sensor.percentage", "33")
await hass.async_block_till_done()
@@ -298,7 +303,7 @@ async def test_templates_with_entities_and_invalid_percentage(hass, calls):
hass.states.async_set("sensor.percentage", "0")
await hass.async_block_till_done()
- _verify(hass, STATE_OFF, SPEED_OFF, 0, None, None, None)
+ _verify(hass, STATE_ON, SPEED_OFF, 0, None, None, None)
async def test_templates_with_entities_and_preset_modes(hass, calls):
@@ -648,6 +653,82 @@ async def test_set_percentage(hass, calls):
_verify(hass, STATE_ON, SPEED_MEDIUM, 50, None, None, None)
+async def test_increase_decrease_speed(hass, calls):
+ """Test set valid increase and decrease speed."""
+ await _register_components(hass, speed_count=3)
+
+ # Turn on fan
+ await common.async_turn_on(hass, _TEST_FAN)
+
+ # Set fan's percentage speed to 100
+ await common.async_set_percentage(hass, _TEST_FAN, 100)
+
+ # verify
+ assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 100
+
+ _verify(hass, STATE_ON, SPEED_HIGH, 100, None, None, None)
+
+ # Set fan's percentage speed to 66
+ await common.async_decrease_speed(hass, _TEST_FAN)
+ assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 66
+
+ _verify(hass, STATE_ON, SPEED_MEDIUM, 66, None, None, None)
+
+ # Set fan's percentage speed to 33
+ await common.async_decrease_speed(hass, _TEST_FAN)
+ assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 33
+
+ _verify(hass, STATE_ON, SPEED_LOW, 33, None, None, None)
+
+ # Set fan's percentage speed to 0
+ await common.async_decrease_speed(hass, _TEST_FAN)
+ assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 0
+
+ _verify(hass, STATE_OFF, SPEED_OFF, 0, None, None, None)
+
+ # Set fan's percentage speed to 33
+ await common.async_increase_speed(hass, _TEST_FAN)
+ assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 33
+
+ _verify(hass, STATE_ON, SPEED_LOW, 33, None, None, None)
+
+
+async def test_increase_decrease_speed_default_speed_count(hass, calls):
+ """Test set valid increase and decrease speed."""
+ await _register_components(
+ hass,
+ )
+
+ # Turn on fan
+ await common.async_turn_on(hass, _TEST_FAN)
+
+ # Set fan's percentage speed to 100
+ await common.async_set_percentage(hass, _TEST_FAN, 100)
+
+ # verify
+ assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 100
+
+ _verify(hass, STATE_ON, SPEED_HIGH, 100, None, None, None)
+
+ # Set fan's percentage speed to 99
+ await common.async_decrease_speed(hass, _TEST_FAN)
+ assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 99
+
+ _verify(hass, STATE_ON, SPEED_HIGH, 99, None, None, None)
+
+ # Set fan's percentage speed to 98
+ await common.async_decrease_speed(hass, _TEST_FAN)
+ assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 98
+
+ _verify(hass, STATE_ON, SPEED_HIGH, 98, None, None, None)
+
+ for _ in range(32):
+ await common.async_decrease_speed(hass, _TEST_FAN)
+ assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 66
+
+ _verify(hass, STATE_ON, SPEED_MEDIUM, 66, None, None, None)
+
+
async def test_set_invalid_speed_from_initial_stage(hass, calls):
"""Test set invalid speed when fan is in initial state."""
await _register_components(hass)
@@ -881,7 +962,9 @@ def _verify(
assert attributes.get(ATTR_PRESET_MODE) == expected_preset_mode
-async def _register_components(hass, speed_list=None, preset_modes=None):
+async def _register_components(
+ hass, speed_list=None, preset_modes=None, speed_count=None
+):
"""Register basic components for testing."""
with assert_setup_component(1, "input_boolean"):
assert await setup.async_setup_component(
@@ -1006,6 +1089,9 @@ async def _register_components(hass, speed_list=None, preset_modes=None):
if preset_modes:
test_fan_config["preset_modes"] = preset_modes
+ if speed_count:
+ test_fan_config["speed_count"] = speed_count
+
assert await setup.async_setup_component(
hass,
"fan",
@@ -1060,3 +1146,217 @@ async def test_unique_id(hass):
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
+
+
+@pytest.mark.parametrize(
+ "speed_count, percentage_step", [(0, 1), (100, 1), (3, 100 / 3)]
+)
+async def test_implemented_percentage(hass, speed_count, percentage_step):
+ """Test a fan that implements percentage."""
+ await setup.async_setup_component(
+ hass,
+ "fan",
+ {
+ "fan": {
+ "platform": "template",
+ "fans": {
+ "mechanical_ventilation": {
+ "friendly_name": "Mechanische ventilatie",
+ "unique_id": "a2fd2e38-674b-4b47-b5ef-cc2362211a72",
+ "value_template": "{{ states('light.mv_snelheid') }}",
+ "percentage_template": "{{ (state_attr('light.mv_snelheid','brightness') | int / 255 * 100) | int }}",
+ "turn_on": [
+ {
+ "service": "switch.turn_off",
+ "target": {
+ "entity_id": "switch.mv_automatisch",
+ },
+ },
+ {
+ "service": "light.turn_on",
+ "target": {
+ "entity_id": "light.mv_snelheid",
+ },
+ "data": {"brightness_pct": 40},
+ },
+ ],
+ "turn_off": [
+ {
+ "service": "light.turn_off",
+ "target": {
+ "entity_id": "light.mv_snelheid",
+ },
+ },
+ {
+ "service": "switch.turn_on",
+ "target": {
+ "entity_id": "switch.mv_automatisch",
+ },
+ },
+ ],
+ "set_percentage": [
+ {
+ "service": "light.turn_on",
+ "target": {
+ "entity_id": "light.mv_snelheid",
+ },
+ "data": {"brightness_pct": "{{ percentage }}"},
+ }
+ ],
+ "speed_count": speed_count,
+ },
+ },
+ },
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 1
+
+ state = hass.states.get("fan.mechanical_ventilation")
+ attributes = state.attributes
+ assert attributes["percentage_step"] == percentage_step
+
+
+async def test_implemented_preset_mode(hass):
+ """Test a fan that implements preset_mode."""
+ await setup.async_setup_component(
+ hass,
+ "fan",
+ {
+ "fan": {
+ "platform": "template",
+ "fans": {
+ "mechanical_ventilation": {
+ "friendly_name": "Mechanische ventilatie",
+ "unique_id": "a2fd2e38-674b-4b47-b5ef-cc2362211a72",
+ "value_template": "{{ states('light.mv_snelheid') }}",
+ "preset_mode_template": "{{ 'any' }}",
+ "preset_modes": ["any"],
+ "set_preset_mode": [
+ {
+ "service": "light.turn_on",
+ "target": {
+ "entity_id": "light.mv_snelheid",
+ },
+ "data": {"brightness_pct": "{{ percentage }}"},
+ }
+ ],
+ "turn_on": [
+ {
+ "service": "switch.turn_off",
+ "target": {
+ "entity_id": "switch.mv_automatisch",
+ },
+ },
+ {
+ "service": "light.turn_on",
+ "target": {
+ "entity_id": "light.mv_snelheid",
+ },
+ "data": {"brightness_pct": 40},
+ },
+ ],
+ "turn_off": [
+ {
+ "service": "light.turn_off",
+ "target": {
+ "entity_id": "light.mv_snelheid",
+ },
+ },
+ {
+ "service": "switch.turn_on",
+ "target": {
+ "entity_id": "switch.mv_automatisch",
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 1
+
+ state = hass.states.get("fan.mechanical_ventilation")
+ attributes = state.attributes
+ assert attributes["percentage"] is None
+
+
+async def test_implemented_speed(hass):
+ """Test a fan that implements speed."""
+ await setup.async_setup_component(
+ hass,
+ "fan",
+ {
+ "fan": {
+ "platform": "template",
+ "fans": {
+ "mechanical_ventilation": {
+ "friendly_name": "Mechanische ventilatie",
+ "unique_id": "a2fd2e38-674b-4b47-b5ef-cc2362211a72",
+ "value_template": "{{ states('light.mv_snelheid') }}",
+ "speed_template": "{{ 'fast' }}",
+ "speeds": ["slow", "fast"],
+ "set_preset_mode": [
+ {
+ "service": "light.turn_on",
+ "target": {
+ "entity_id": "light.mv_snelheid",
+ },
+ "data": {"brightness_pct": "{{ percentage }}"},
+ }
+ ],
+ "turn_on": [
+ {
+ "service": "switch.turn_off",
+ "target": {
+ "entity_id": "switch.mv_automatisch",
+ },
+ },
+ {
+ "service": "light.turn_on",
+ "target": {
+ "entity_id": "light.mv_snelheid",
+ },
+ "data": {"brightness_pct": 40},
+ },
+ ],
+ "turn_off": [
+ {
+ "service": "light.turn_off",
+ "target": {
+ "entity_id": "light.mv_snelheid",
+ },
+ },
+ {
+ "service": "switch.turn_on",
+ "target": {
+ "entity_id": "switch.mv_automatisch",
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 1
+
+ state = hass.states.get("fan.mechanical_ventilation")
+ attributes = state.attributes
+ assert attributes["percentage"] == 100
+ assert attributes["speed"] == "fast"
diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py
index 1c932c5af30fa6..0f8dff4026fe26 100644
--- a/tests/components/template/test_init.py
+++ b/tests/components/template/test_init.py
@@ -4,7 +4,8 @@
from unittest.mock import patch
from homeassistant import config
-from homeassistant.components.template import DOMAIN, SERVICE_RELOAD
+from homeassistant.components.template import DOMAIN
+from homeassistant.helpers.reload import SERVICE_RELOAD
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
@@ -26,7 +27,14 @@ async def test_reloadable(hass):
"value_template": "{{ states.sensor.test_sensor.state }}"
},
},
- }
+ },
+ "template": {
+ "trigger": {"platform": "event", "event_type": "event_1"},
+ "sensor": {
+ "name": "top level",
+ "state": "{{ trigger.event.data.source }}",
+ },
+ },
},
)
await hass.async_block_till_done()
@@ -34,8 +42,12 @@ async def test_reloadable(hass):
await hass.async_start()
await hass.async_block_till_done()
+ hass.bus.async_fire("event_1", {"source": "init"})
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 3
assert hass.states.get("sensor.state").state == "mytest"
- assert len(hass.states.async_all()) == 2
+ assert hass.states.get("sensor.top_level").state == "init"
yaml_path = path.join(
_get_fixtures_base_path(),
@@ -51,11 +63,16 @@ async def test_reloadable(hass):
)
await hass.async_block_till_done()
- assert len(hass.states.async_all()) == 3
+ assert len(hass.states.async_all()) == 4
+
+ hass.bus.async_fire("event_2", {"source": "reload"})
+ await hass.async_block_till_done()
assert hass.states.get("sensor.state") is None
+ assert hass.states.get("sensor.top_level") is None
assert hass.states.get("sensor.watching_tv_in_master_bedroom").state == "off"
assert float(hass.states.get("sensor.combined_sensor_energy_usage").state) == 0
+ assert hass.states.get("sensor.top_level_2").state == "reload"
async def test_reloadable_can_remove(hass):
@@ -73,7 +90,14 @@ async def test_reloadable_can_remove(hass):
"value_template": "{{ states.sensor.test_sensor.state }}"
},
},
- }
+ },
+ "template": {
+ "trigger": {"platform": "event", "event_type": "event_1"},
+ "sensor": {
+ "name": "top level",
+ "state": "{{ trigger.event.data.source }}",
+ },
+ },
},
)
await hass.async_block_till_done()
@@ -81,8 +105,12 @@ async def test_reloadable_can_remove(hass):
await hass.async_start()
await hass.async_block_till_done()
+ hass.bus.async_fire("event_1", {"source": "init"})
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 3
assert hass.states.get("sensor.state").state == "mytest"
- assert len(hass.states.async_all()) == 2
+ assert hass.states.get("sensor.top_level").state == "init"
yaml_path = path.join(
_get_fixtures_base_path(),
@@ -250,11 +278,12 @@ async def test_reloadable_multiple_platforms(hass):
)
await hass.async_block_till_done()
- assert len(hass.states.async_all()) == 3
+ assert len(hass.states.async_all()) == 4
assert hass.states.get("sensor.state") is None
assert hass.states.get("sensor.watching_tv_in_master_bedroom").state == "off"
assert float(hass.states.get("sensor.combined_sensor_energy_usage").state) == 0
+ assert hass.states.get("sensor.top_level_2") is not None
async def test_reload_sensors_that_reference_other_template_sensors(hass):
diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py
index 1b99e1cec0a198..b3a4b2a1aa4b0d 100644
--- a/tests/components/template/test_light.py
+++ b/tests/components/template/test_light.py
@@ -4,17 +4,23 @@
import pytest
from homeassistant import setup
+import homeassistant.components.light as light
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
ATTR_WHITE_VALUE,
)
-from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
-from homeassistant.core import callback
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+ STATE_OFF,
+ STATE_ON,
+ STATE_UNAVAILABLE,
+)
-from tests.common import assert_setup_component, get_test_home_assistant
-from tests.components.light import common
+from tests.common import assert_setup_component, async_mock_service
_LOGGER = logging.getLogger(__name__)
@@ -22,284 +28,619 @@
_STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state"
-class TestTemplateLight:
- """Test the Template light."""
-
- hass = None
- calls = None
- # pylint: disable=invalid-name
-
- def setup_method(self, method):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.calls = []
-
- @callback
- def record_call(service):
- """Track function calls."""
- self.calls.append(service)
-
- self.hass.services.register("test", "automation", record_call)
-
- def teardown_method(self, method):
- """Stop everything that was started."""
- self.hass.stop()
-
- def test_template_state_invalid(self):
- """Test template state with render error."""
- with assert_setup_component(1, "light"):
- assert setup.setup_component(
- self.hass,
- "light",
- {
- "light": {
- "platform": "template",
- "lights": {
- "test_template_light": {
- "value_template": "{{states.test['big.fat...']}}",
- "turn_on": {
- "service": "light.turn_on",
+@pytest.fixture(name="calls")
+def fixture_calls(hass):
+ """Track calls to a mock service."""
+ return async_mock_service(hass, "test", "automation")
+
+
+async def test_template_state_invalid(hass):
+ """Test template state with render error."""
+ with assert_setup_component(1, light.DOMAIN):
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ "light": {
+ "platform": "template",
+ "lights": {
+ "test_template_light": {
+ "value_template": "{{states.test['big.fat...']}}",
+ "turn_on": {
+ "service": "light.turn_on",
+ "entity_id": "light.test_state",
+ },
+ "turn_off": {
+ "service": "light.turn_off",
+ "entity_id": "light.test_state",
+ },
+ "set_level": {
+ "service": "light.turn_on",
+ "data_template": {
"entity_id": "light.test_state",
+ "brightness": "{{brightness}}",
},
- "turn_off": {
- "service": "light.turn_off",
+ },
+ }
+ },
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test_template_light")
+ assert state.state == STATE_OFF
+
+
+async def test_template_state_text(hass):
+ """Test the state text of a template."""
+ with assert_setup_component(1, light.DOMAIN):
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ "light": {
+ "platform": "template",
+ "lights": {
+ "test_template_light": {
+ "value_template": "{{ states.light.test_state.state }}",
+ "turn_on": {
+ "service": "light.turn_on",
+ "entity_id": "light.test_state",
+ },
+ "turn_off": {
+ "service": "light.turn_off",
+ "entity_id": "light.test_state",
+ },
+ "set_level": {
+ "service": "light.turn_on",
+ "data_template": {
"entity_id": "light.test_state",
+ "brightness": "{{brightness}}",
},
- "set_level": {
- "service": "light.turn_on",
- "data_template": {
- "entity_id": "light.test_state",
- "brightness": "{{brightness}}",
- },
- },
- }
- },
- }
- },
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- state = self.hass.states.get("light.test_template_light")
- assert state.state == STATE_OFF
-
- def test_template_state_text(self):
- """Test the state text of a template."""
- with assert_setup_component(1, "light"):
- assert setup.setup_component(
- self.hass,
- "light",
- {
- "light": {
- "platform": "template",
- "lights": {
- "test_template_light": {
- "value_template": "{{ states.light.test_state.state }}",
- "turn_on": {
- "service": "light.turn_on",
+ },
+ }
+ },
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ state = hass.states.async_set("light.test_state", STATE_ON)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test_template_light")
+ assert state.state == STATE_ON
+
+ state = hass.states.async_set("light.test_state", STATE_OFF)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test_template_light")
+ assert state.state == STATE_OFF
+
+
+@pytest.mark.parametrize(
+ "expected_state,template",
+ [(STATE_ON, "{{ 1 == 1 }}"), (STATE_OFF, "{{ 1 == 2 }}")],
+)
+async def test_template_state_boolean(hass, expected_state, template):
+ """Test the setting of the state with boolean on."""
+ with assert_setup_component(1, light.DOMAIN):
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ "light": {
+ "platform": "template",
+ "lights": {
+ "test_template_light": {
+ "value_template": template,
+ "turn_on": {
+ "service": "light.turn_on",
+ "entity_id": "light.test_state",
+ },
+ "turn_off": {
+ "service": "light.turn_off",
+ "entity_id": "light.test_state",
+ },
+ "set_level": {
+ "service": "light.turn_on",
+ "data_template": {
"entity_id": "light.test_state",
+ "brightness": "{{brightness}}",
},
- "turn_off": {
- "service": "light.turn_off",
+ },
+ }
+ },
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test_template_light")
+ assert state.state == expected_state
+
+
+async def test_template_syntax_error(hass):
+ """Test templating syntax error."""
+ with assert_setup_component(0, light.DOMAIN):
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ "light": {
+ "platform": "template",
+ "lights": {
+ "test_template_light": {
+ "value_template": "{%- if false -%}",
+ "turn_on": {
+ "service": "light.turn_on",
+ "entity_id": "light.test_state",
+ },
+ "turn_off": {
+ "service": "light.turn_off",
+ "entity_id": "light.test_state",
+ },
+ "set_level": {
+ "service": "light.turn_on",
+ "data_template": {
"entity_id": "light.test_state",
+ "brightness": "{{brightness}}",
},
- "set_level": {
- "service": "light.turn_on",
- "data_template": {
- "entity_id": "light.test_state",
- "brightness": "{{brightness}}",
- },
+ },
+ }
+ },
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert hass.states.async_all() == []
+
+
+async def test_invalid_name_does_not_create(hass):
+ """Test invalid name."""
+ with assert_setup_component(0, light.DOMAIN):
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ "light": {
+ "platform": "template",
+ "lights": {
+ "bad name here": {
+ "value_template": "{{ 1== 1}}",
+ "turn_on": {
+ "service": "light.turn_on",
+ "entity_id": "light.test_state",
+ },
+ "turn_off": {
+ "service": "light.turn_off",
+ "entity_id": "light.test_state",
+ },
+ "set_level": {
+ "service": "light.turn_on",
+ "data_template": {
+ "entity_id": "light.test_state",
+ "brightness": "{{brightness}}",
},
- }
+ },
+ }
+ },
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert hass.states.async_all() == []
+
+
+async def test_invalid_light_does_not_create(hass):
+ """Test invalid light."""
+ with assert_setup_component(0, light.DOMAIN):
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ "light": {
+ "platform": "template",
+ "switches": {"test_template_light": "Invalid"},
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert hass.states.async_all() == []
+
+
+async def test_no_lights_does_not_create(hass):
+ """Test if there are no lights no creation."""
+ with assert_setup_component(0, light.DOMAIN):
+ assert await setup.async_setup_component(
+ hass, "light", {"light": {"platform": "template"}}
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ assert hass.states.async_all() == []
+
+
+@pytest.mark.parametrize(
+ "missing_key, count", [("value_template", 1), ("turn_on", 0), ("turn_off", 0)]
+)
+async def test_missing_key(hass, missing_key, count):
+ """Test missing template."""
+ light_config = {
+ "light": {
+ "platform": "template",
+ "lights": {
+ "light_one": {
+ "value_template": "{{ 1== 1}}",
+ "turn_on": {
+ "service": "light.turn_on",
+ "entity_id": "light.test_state",
+ },
+ "turn_off": {
+ "service": "light.turn_off",
+ "entity_id": "light.test_state",
+ },
+ "set_level": {
+ "service": "light.turn_on",
+ "data_template": {
+ "entity_id": "light.test_state",
+ "brightness": "{{brightness}}",
+ },
+ },
+ }
+ },
+ }
+ }
+
+ del light_config["light"]["lights"]["light_one"][missing_key]
+ with assert_setup_component(count, light.DOMAIN):
+ assert await setup.async_setup_component(hass, "light", light_config)
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ if count:
+ assert hass.states.async_all() != []
+ else:
+ assert hass.states.async_all() == []
+
+
+async def test_on_action(hass, calls):
+ """Test on action."""
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ "light": {
+ "platform": "template",
+ "lights": {
+ "test_template_light": {
+ "value_template": "{{states.light.test_state.state}}",
+ "turn_on": {"service": "test.automation"},
+ "turn_off": {
+ "service": "light.turn_off",
+ "entity_id": "light.test_state",
+ },
+ "set_level": {
+ "service": "light.turn_on",
+ "data_template": {
+ "entity_id": "light.test_state",
+ "brightness": "{{brightness}}",
+ },
},
}
},
- )
+ }
+ },
+ )
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
- state = self.hass.states.set("light.test_state", STATE_ON)
- self.hass.block_till_done()
+ hass.states.async_set("light.test_state", STATE_OFF)
+ await hass.async_block_till_done()
- state = self.hass.states.get("light.test_template_light")
- assert state.state == STATE_ON
+ state = hass.states.get("light.test_template_light")
+ assert state.state == STATE_OFF
- state = self.hass.states.set("light.test_state", STATE_OFF)
- self.hass.block_till_done()
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "light.test_template_light"},
+ blocking=True,
+ )
- state = self.hass.states.get("light.test_template_light")
- assert state.state == STATE_OFF
+ assert len(calls) == 1
- @pytest.mark.parametrize(
- "expected_state,template",
- [(STATE_ON, "{{ 1 == 1 }}"), (STATE_OFF, "{{ 1 == 2 }}")],
- )
- def test_template_state_boolean(self, expected_state, template):
- """Test the setting of the state with boolean on."""
- with assert_setup_component(1, "light"):
- assert setup.setup_component(
- self.hass,
- "light",
- {
- "light": {
- "platform": "template",
- "lights": {
- "test_template_light": {
- "value_template": template,
- "turn_on": {
- "service": "light.turn_on",
- "entity_id": "light.test_state",
- },
- "turn_off": {
- "service": "light.turn_off",
- "entity_id": "light.test_state",
- },
- "set_level": {
- "service": "light.turn_on",
- "data_template": {
- "entity_id": "light.test_state",
- "brightness": "{{brightness}}",
- },
- },
- }
+
+async def test_on_action_optimistic(hass, calls):
+ """Test on action with optimistic state."""
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ "light": {
+ "platform": "template",
+ "lights": {
+ "test_template_light": {
+ "turn_on": {"service": "test.automation"},
+ "turn_off": {
+ "service": "light.turn_off",
+ "entity_id": "light.test_state",
+ },
+ "set_level": {
+ "service": "light.turn_on",
+ "data_template": {
+ "entity_id": "light.test_state",
+ "brightness": "{{brightness}}",
+ },
},
}
},
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- state = self.hass.states.get("light.test_template_light")
- assert state.state == expected_state
-
- def test_template_syntax_error(self):
- """Test templating syntax error."""
- with assert_setup_component(0, "light"):
- assert setup.setup_component(
- self.hass,
- "light",
- {
- "light": {
- "platform": "template",
- "lights": {
- "test_template_light": {
- "value_template": "{%- if false -%}",
- "turn_on": {
- "service": "light.turn_on",
- "entity_id": "light.test_state",
- },
- "turn_off": {
- "service": "light.turn_off",
- "entity_id": "light.test_state",
- },
- "set_level": {
- "service": "light.turn_on",
- "data_template": {
- "entity_id": "light.test_state",
- "brightness": "{{brightness}}",
- },
- },
- }
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ hass.states.async_set("light.test_state", STATE_OFF)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test_template_light")
+ assert state.state == STATE_OFF
+
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "light.test_template_light"},
+ blocking=True,
+ )
+
+ state = hass.states.get("light.test_template_light")
+ assert len(calls) == 1
+ assert state.state == STATE_ON
+
+
+async def test_off_action(hass, calls):
+ """Test off action."""
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ "light": {
+ "platform": "template",
+ "lights": {
+ "test_template_light": {
+ "value_template": "{{states.light.test_state.state}}",
+ "turn_on": {
+ "service": "light.turn_on",
+ "entity_id": "light.test_state",
+ },
+ "turn_off": {"service": "test.automation"},
+ "set_level": {
+ "service": "light.turn_on",
+ "data_template": {
+ "entity_id": "light.test_state",
+ "brightness": "{{brightness}}",
+ },
},
}
},
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- assert self.hass.states.all() == []
-
- def test_invalid_name_does_not_create(self):
- """Test invalid name."""
- with assert_setup_component(0, "light"):
- assert setup.setup_component(
- self.hass,
- "light",
- {
- "light": {
- "platform": "template",
- "lights": {
- "bad name here": {
- "value_template": "{{ 1== 1}}",
- "turn_on": {
- "service": "light.turn_on",
- "entity_id": "light.test_state",
- },
- "turn_off": {
- "service": "light.turn_off",
- "entity_id": "light.test_state",
- },
- "set_level": {
- "service": "light.turn_on",
- "data_template": {
- "entity_id": "light.test_state",
- "brightness": "{{brightness}}",
- },
- },
- }
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ hass.states.async_set("light.test_state", STATE_ON)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test_template_light")
+ assert state.state == STATE_ON
+
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "light.test_template_light"},
+ blocking=True,
+ )
+
+ assert len(calls) == 1
+
+
+async def test_off_action_optimistic(hass, calls):
+ """Test off action with optimistic state."""
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ "light": {
+ "platform": "template",
+ "lights": {
+ "test_template_light": {
+ "turn_on": {
+ "service": "light.turn_on",
+ "entity_id": "light.test_state",
+ },
+ "turn_off": {"service": "test.automation"},
+ "set_level": {
+ "service": "light.turn_on",
+ "data_template": {
+ "entity_id": "light.test_state",
+ "brightness": "{{brightness}}",
+ },
},
}
},
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- assert self.hass.states.all() == []
-
- def test_invalid_light_does_not_create(self):
- """Test invalid light."""
- with assert_setup_component(0, "light"):
- assert setup.setup_component(
- self.hass,
- "light",
- {
- "light": {
- "platform": "template",
- "switches": {"test_template_light": "Invalid"},
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test_template_light")
+ assert state.state == STATE_OFF
+
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "light.test_template_light"},
+ blocking=True,
+ )
+
+ assert len(calls) == 1
+ state = hass.states.get("light.test_template_light")
+ assert state.state == STATE_OFF
+
+
+async def test_white_value_action_no_template(hass, calls):
+ """Test setting white value with optimistic template."""
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ "light": {
+ "platform": "template",
+ "lights": {
+ "test_template_light": {
+ "value_template": "{{1 == 1}}",
+ "turn_on": {
+ "service": "light.turn_on",
+ "entity_id": "light.test_state",
+ },
+ "turn_off": {
+ "service": "light.turn_off",
+ "entity_id": "light.test_state",
+ },
+ "set_white_value": {
+ "service": "test.automation",
+ "data_template": {
+ "entity_id": "test.test_state",
+ "white_value": "{{white_value}}",
+ },
+ },
}
},
- )
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test_template_light")
+ assert state.attributes.get("white_value") is None
+
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "light.test_template_light", ATTR_WHITE_VALUE: 124},
+ blocking=True,
+ )
+
+ assert len(calls) == 1
+ assert calls[0].data["white_value"] == 124
+
+ state = hass.states.get("light.test_template_light")
+ assert state is not None
+ assert state.attributes.get("white_value") == 124
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
- assert self.hass.states.all() == []
+@pytest.mark.parametrize(
+ "expected_white_value,template",
+ [
+ (255, "{{255}}"),
+ (None, "{{256}}"),
+ (None, "{{x - 12}}"),
+ (None, "{{ none }}"),
+ (None, ""),
+ ],
+)
+async def test_white_value_template(hass, expected_white_value, template):
+ """Test the template for the white value."""
+ with assert_setup_component(1, light.DOMAIN):
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ "light": {
+ "platform": "template",
+ "lights": {
+ "test_template_light": {
+ "value_template": "{{ 1 == 1 }}",
+ "turn_on": {
+ "service": "light.turn_on",
+ "entity_id": "light.test_state",
+ },
+ "turn_off": {
+ "service": "light.turn_off",
+ "entity_id": "light.test_state",
+ },
+ "set_white_value": {
+ "service": "light.turn_on",
+ "data_template": {
+ "entity_id": "light.test_state",
+ "white_value": "{{white_value}}",
+ },
+ },
+ "white_value_template": template,
+ }
+ },
+ }
+ },
+ )
- def test_no_lights_does_not_create(self):
- """Test if there are no lights no creation."""
- with assert_setup_component(0, "light"):
- assert setup.setup_component(
- self.hass, "light", {"light": {"platform": "template"}}
- )
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
+ state = hass.states.get("light.test_template_light")
+ assert state is not None
+ assert state.attributes.get("white_value") == expected_white_value
- assert self.hass.states.all() == []
- @pytest.mark.parametrize(
- "missing_key, count", [("value_template", 1), ("turn_on", 0), ("turn_off", 0)]
- )
- def test_missing_key(self, missing_key, count):
- """Test missing template."""
- light = {
+async def test_level_action_no_template(hass, calls):
+ """Test setting brightness with optimistic template."""
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
"light": {
"platform": "template",
"lights": {
- "light_one": {
- "value_template": "{{ 1== 1}}",
+ "test_template_light": {
+ "value_template": "{{1 == 1}}",
"turn_on": {
"service": "light.turn_on",
"entity_id": "light.test_state",
@@ -309,84 +650,66 @@ def test_missing_key(self, missing_key, count):
"entity_id": "light.test_state",
},
"set_level": {
- "service": "light.turn_on",
+ "service": "test.automation",
"data_template": {
- "entity_id": "light.test_state",
+ "entity_id": "test.test_state",
"brightness": "{{brightness}}",
},
},
}
},
}
- }
-
- del light["light"]["lights"]["light_one"][missing_key]
- with assert_setup_component(count, "light"):
- assert setup.setup_component(self.hass, "light", light)
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- if count:
- assert self.hass.states.all() != []
- else:
- assert self.hass.states.all() == []
-
- def test_on_action(self):
- """Test on action."""
- assert setup.setup_component(
- self.hass,
- "light",
- {
- "light": {
- "platform": "template",
- "lights": {
- "test_template_light": {
- "value_template": "{{states.light.test_state.state}}",
- "turn_on": {"service": "test.automation"},
- "turn_off": {
- "service": "light.turn_off",
- "entity_id": "light.test_state",
- },
- "set_level": {
- "service": "light.turn_on",
- "data_template": {
- "entity_id": "light.test_state",
- "brightness": "{{brightness}}",
- },
- },
- }
- },
- }
- },
- )
+ },
+ )
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
+ state = hass.states.get("light.test_template_light")
+ assert state.attributes.get("brightness") is None
- self.hass.states.set("light.test_state", STATE_OFF)
- self.hass.block_till_done()
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "light.test_template_light", ATTR_BRIGHTNESS: 124},
+ blocking=True,
+ )
- state = self.hass.states.get("light.test_template_light")
- assert state.state == STATE_OFF
+ assert len(calls) == 1
+ assert calls[0].data["brightness"] == 124
- common.turn_on(self.hass, "light.test_template_light")
- self.hass.block_till_done()
+ state = hass.states.get("light.test_template_light")
+ _LOGGER.info(str(state.attributes))
+ assert state is not None
+ assert state.attributes.get("brightness") == 124
- assert len(self.calls) == 1
- def test_on_action_optimistic(self):
- """Test on action with optimistic state."""
- assert setup.setup_component(
- self.hass,
- "light",
+@pytest.mark.parametrize(
+ "expected_level,template",
+ [
+ (255, "{{255}}"),
+ (None, "{{256}}"),
+ (None, "{{x - 12}}"),
+ (None, "{{ none }}"),
+ (None, ""),
+ ],
+)
+async def test_level_template(hass, expected_level, template):
+ """Test the template for the level."""
+ with assert_setup_component(1, light.DOMAIN):
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
{
"light": {
"platform": "template",
"lights": {
"test_template_light": {
- "turn_on": {"service": "test.automation"},
+ "value_template": "{{ 1 == 1 }}",
+ "turn_on": {
+ "service": "light.turn_on",
+ "entity_id": "light.test_state",
+ },
"turn_off": {
"service": "light.turn_off",
"entity_id": "light.test_state",
@@ -398,88 +721,151 @@ def test_on_action_optimistic(self):
"brightness": "{{brightness}}",
},
},
+ "level_template": template,
}
},
}
},
)
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- self.hass.states.set("light.test_state", STATE_OFF)
- self.hass.block_till_done()
-
- state = self.hass.states.get("light.test_template_light")
- assert state.state == STATE_OFF
-
- common.turn_on(self.hass, "light.test_template_light")
- self.hass.block_till_done()
-
- state = self.hass.states.get("light.test_template_light")
- assert len(self.calls) == 1
- assert state.state == STATE_ON
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
- def test_off_action(self):
- """Test off action."""
- assert setup.setup_component(
- self.hass,
- "light",
+ state = hass.states.get("light.test_template_light")
+ assert state is not None
+ assert state.attributes.get("brightness") == expected_level
+
+
+@pytest.mark.parametrize(
+ "expected_temp,template",
+ [
+ (500, "{{500}}"),
+ (None, "{{501}}"),
+ (None, "{{x - 12}}"),
+ (None, "None"),
+ (None, "{{ none }}"),
+ (None, ""),
+ ],
+)
+async def test_temperature_template(hass, expected_temp, template):
+ """Test the template for the temperature."""
+ with assert_setup_component(1, light.DOMAIN):
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
{
"light": {
"platform": "template",
"lights": {
"test_template_light": {
- "value_template": "{{states.light.test_state.state}}",
+ "value_template": "{{ 1 == 1 }}",
"turn_on": {
"service": "light.turn_on",
"entity_id": "light.test_state",
},
- "turn_off": {"service": "test.automation"},
- "set_level": {
+ "turn_off": {
+ "service": "light.turn_off",
+ "entity_id": "light.test_state",
+ },
+ "set_temperature": {
"service": "light.turn_on",
"data_template": {
"entity_id": "light.test_state",
- "brightness": "{{brightness}}",
+ "color_temp": "{{color_temp}}",
},
},
+ "temperature_template": template,
}
},
}
},
)
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test_template_light")
+ assert state is not None
+ assert state.attributes.get("color_temp") == expected_temp
+
+
+async def test_temperature_action_no_template(hass, calls):
+ """Test setting temperature with optimistic template."""
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ "light": {
+ "platform": "template",
+ "lights": {
+ "test_template_light": {
+ "value_template": "{{1 == 1}}",
+ "turn_on": {
+ "service": "light.turn_on",
+ "entity_id": "light.test_state",
+ },
+ "turn_off": {
+ "service": "light.turn_off",
+ "entity_id": "light.test_state",
+ },
+ "set_temperature": {
+ "service": "test.automation",
+ "data_template": {
+ "entity_id": "test.test_state",
+ "color_temp": "{{color_temp}}",
+ },
+ },
+ }
+ },
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test_template_light")
+ assert state.attributes.get("color_template") is None
- self.hass.states.set("light.test_state", STATE_ON)
- self.hass.block_till_done()
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "light.test_template_light", ATTR_COLOR_TEMP: 345},
+ blocking=True,
+ )
- state = self.hass.states.get("light.test_template_light")
- assert state.state == STATE_ON
+ assert len(calls) == 1
+ assert calls[0].data["color_temp"] == 345
- common.turn_off(self.hass, "light.test_template_light")
- self.hass.block_till_done()
+ state = hass.states.get("light.test_template_light")
+ _LOGGER.info(str(state.attributes))
+ assert state is not None
+ assert state.attributes.get("color_temp") == 345
- assert len(self.calls) == 1
- def test_off_action_optimistic(self):
- """Test off action with optimistic state."""
- assert setup.setup_component(
- self.hass,
- "light",
+async def test_friendly_name(hass):
+ """Test the accessibility of the friendly_name attribute."""
+ with assert_setup_component(1, light.DOMAIN):
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
{
"light": {
"platform": "template",
"lights": {
"test_template_light": {
+ "friendly_name": "Template light",
+ "value_template": "{{ 1 == 1 }}",
"turn_on": {
"service": "light.turn_on",
"entity_id": "light.test_state",
},
- "turn_off": {"service": "test.automation"},
+ "turn_off": {
+ "service": "light.turn_off",
+ "entity_id": "light.test_state",
+ },
"set_level": {
"service": "light.turn_on",
"data_template": {
@@ -493,31 +879,29 @@ def test_off_action_optimistic(self):
},
)
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
- state = self.hass.states.get("light.test_template_light")
- assert state.state == STATE_OFF
+ state = hass.states.get("light.test_template_light")
+ assert state is not None
- common.turn_off(self.hass, "light.test_template_light")
- self.hass.block_till_done()
+ assert state.attributes.get("friendly_name") == "Template light"
- assert len(self.calls) == 1
- state = self.hass.states.get("light.test_template_light")
- assert state.state == STATE_OFF
- def test_white_value_action_no_template(self):
- """Test setting white value with optimistic template."""
- assert setup.setup_component(
- self.hass,
- "light",
+async def test_icon_template(hass):
+ """Test icon template."""
+ with assert_setup_component(1, light.DOMAIN):
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
{
"light": {
"platform": "template",
"lights": {
"test_template_light": {
- "value_template": "{{1 == 1}}",
+ "friendly_name": "Template light",
+ "value_template": "{{ 1 == 1 }}",
"turn_on": {
"service": "light.turn_on",
"entity_id": "light.test_state",
@@ -526,99 +910,50 @@ def test_white_value_action_no_template(self):
"service": "light.turn_off",
"entity_id": "light.test_state",
},
- "set_white_value": {
- "service": "test.automation",
+ "set_level": {
+ "service": "light.turn_on",
"data_template": {
- "entity_id": "test.test_state",
- "white_value": "{{white_value}}",
+ "entity_id": "light.test_state",
+ "brightness": "{{brightness}}",
},
},
+ "icon_template": "{% if states.light.test_state.state %}"
+ "mdi:check"
+ "{% endif %}",
}
},
}
},
)
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
- state = self.hass.states.get("light.test_template_light")
- assert state.attributes.get("white_value") is None
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test_template_light")
+ assert state.attributes.get("icon") == ""
- common.turn_on(
- self.hass, "light.test_template_light", **{ATTR_WHITE_VALUE: 124}
- )
- self.hass.block_till_done()
- assert len(self.calls) == 1
- assert self.calls[0].data["white_value"] == 124
-
- state = self.hass.states.get("light.test_template_light")
- assert state is not None
- assert state.attributes.get("white_value") == 124
-
- @pytest.mark.parametrize(
- "expected_white_value,template",
- [
- (255, "{{255}}"),
- (None, "{{256}}"),
- (None, "{{x - 12}}"),
- (None, "{{ none }}"),
- (None, ""),
- ],
- )
- def test_white_value_template(self, expected_white_value, template):
- """Test the template for the white value."""
- with assert_setup_component(1, "light"):
- assert setup.setup_component(
- self.hass,
- "light",
- {
- "light": {
- "platform": "template",
- "lights": {
- "test_template_light": {
- "value_template": "{{ 1 == 1 }}",
- "turn_on": {
- "service": "light.turn_on",
- "entity_id": "light.test_state",
- },
- "turn_off": {
- "service": "light.turn_off",
- "entity_id": "light.test_state",
- },
- "set_white_value": {
- "service": "light.turn_on",
- "data_template": {
- "entity_id": "light.test_state",
- "white_value": "{{white_value}}",
- },
- },
- "white_value_template": template,
- }
- },
- }
- },
- )
+ state = hass.states.async_set("light.test_state", STATE_ON)
+ await hass.async_block_till_done()
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
+ state = hass.states.get("light.test_template_light")
- state = self.hass.states.get("light.test_template_light")
- assert state is not None
- assert state.attributes.get("white_value") == expected_white_value
+ assert state.attributes["icon"] == "mdi:check"
- def test_level_action_no_template(self):
- """Test setting brightness with optimistic template."""
- assert setup.setup_component(
- self.hass,
- "light",
+
+async def test_entity_picture_template(hass):
+ """Test entity_picture template."""
+ with assert_setup_component(1, light.DOMAIN):
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
{
"light": {
"platform": "template",
"lights": {
"test_template_light": {
- "value_template": "{{1 == 1}}",
+ "friendly_name": "Template light",
+ "value_template": "{{ 1 == 1 }}",
"turn_on": {
"service": "light.turn_on",
"entity_id": "light.test_state",
@@ -628,343 +963,131 @@ def test_level_action_no_template(self):
"entity_id": "light.test_state",
},
"set_level": {
- "service": "test.automation",
+ "service": "light.turn_on",
"data_template": {
- "entity_id": "test.test_state",
+ "entity_id": "light.test_state",
"brightness": "{{brightness}}",
},
},
+ "entity_picture_template": "{% if states.light.test_state.state %}"
+ "/local/light.png"
+ "{% endif %}",
}
},
}
},
)
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- state = self.hass.states.get("light.test_template_light")
- assert state.attributes.get("brightness") is None
-
- common.turn_on(self.hass, "light.test_template_light", **{ATTR_BRIGHTNESS: 124})
- self.hass.block_till_done()
- assert len(self.calls) == 1
- assert self.calls[0].data["brightness"] == 124
-
- state = self.hass.states.get("light.test_template_light")
- _LOGGER.info(str(state.attributes))
- assert state is not None
- assert state.attributes.get("brightness") == 124
-
- @pytest.mark.parametrize(
- "expected_level,template",
- [
- (255, "{{255}}"),
- (None, "{{256}}"),
- (None, "{{x - 12}}"),
- (None, "{{ none }}"),
- (None, ""),
- ],
- )
- def test_level_template(self, expected_level, template):
- """Test the template for the level."""
- with assert_setup_component(1, "light"):
- assert setup.setup_component(
- self.hass,
- "light",
- {
- "light": {
- "platform": "template",
- "lights": {
- "test_template_light": {
- "value_template": "{{ 1 == 1 }}",
- "turn_on": {
- "service": "light.turn_on",
- "entity_id": "light.test_state",
- },
- "turn_off": {
- "service": "light.turn_off",
- "entity_id": "light.test_state",
- },
- "set_level": {
- "service": "light.turn_on",
- "data_template": {
- "entity_id": "light.test_state",
- "brightness": "{{brightness}}",
- },
- },
- "level_template": template,
- }
- },
- }
- },
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- state = self.hass.states.get("light.test_template_light")
- assert state is not None
- assert state.attributes.get("brightness") == expected_level
-
- @pytest.mark.parametrize(
- "expected_temp,template",
- [
- (500, "{{500}}"),
- (None, "{{501}}"),
- (None, "{{x - 12}}"),
- (None, "None"),
- (None, "{{ none }}"),
- (None, ""),
- ],
- )
- def test_temperature_template(self, expected_temp, template):
- """Test the template for the temperature."""
- with assert_setup_component(1, "light"):
- assert setup.setup_component(
- self.hass,
- "light",
- {
- "light": {
- "platform": "template",
- "lights": {
- "test_template_light": {
- "value_template": "{{ 1 == 1 }}",
- "turn_on": {
- "service": "light.turn_on",
- "entity_id": "light.test_state",
- },
- "turn_off": {
- "service": "light.turn_off",
- "entity_id": "light.test_state",
- },
- "set_temperature": {
- "service": "light.turn_on",
- "data_template": {
- "entity_id": "light.test_state",
- "color_temp": "{{color_temp}}",
- },
- },
- "temperature_template": template,
- }
- },
- }
- },
- )
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
- state = self.hass.states.get("light.test_template_light")
- assert state is not None
- assert state.attributes.get("color_temp") == expected_temp
+ state = hass.states.get("light.test_template_light")
+ assert state.attributes.get("entity_picture") == ""
- def test_temperature_action_no_template(self):
- """Test setting temperature with optimistic template."""
- assert setup.setup_component(
- self.hass,
- "light",
- {
- "light": {
- "platform": "template",
- "lights": {
- "test_template_light": {
- "value_template": "{{1 == 1}}",
- "turn_on": {
- "service": "light.turn_on",
- "entity_id": "light.test_state",
- },
- "turn_off": {
- "service": "light.turn_off",
- "entity_id": "light.test_state",
- },
- "set_temperature": {
+ state = hass.states.async_set("light.test_state", STATE_ON)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test_template_light")
+
+ assert state.attributes["entity_picture"] == "/local/light.png"
+
+
+async def test_color_action_no_template(hass, calls):
+ """Test setting color with optimistic template."""
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
+ {
+ "light": {
+ "platform": "template",
+ "lights": {
+ "test_template_light": {
+ "value_template": "{{1 == 1}}",
+ "turn_on": {
+ "service": "light.turn_on",
+ "entity_id": "light.test_state",
+ },
+ "turn_off": {
+ "service": "light.turn_off",
+ "entity_id": "light.test_state",
+ },
+ "set_color": [
+ {
"service": "test.automation",
"data_template": {
"entity_id": "test.test_state",
- "color_temp": "{{color_temp}}",
+ "h": "{{h}}",
+ "s": "{{s}}",
},
},
- }
- },
- }
- },
- )
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- state = self.hass.states.get("light.test_template_light")
- assert state.attributes.get("color_template") is None
-
- common.turn_on(self.hass, "light.test_template_light", **{ATTR_COLOR_TEMP: 345})
- self.hass.block_till_done()
- assert len(self.calls) == 1
- assert self.calls[0].data["color_temp"] == 345
-
- state = self.hass.states.get("light.test_template_light")
- _LOGGER.info(str(state.attributes))
- assert state is not None
- assert state.attributes.get("color_temp") == 345
-
- def test_friendly_name(self):
- """Test the accessibility of the friendly_name attribute."""
- with assert_setup_component(1, "light"):
- assert setup.setup_component(
- self.hass,
- "light",
- {
- "light": {
- "platform": "template",
- "lights": {
- "test_template_light": {
- "friendly_name": "Template light",
- "value_template": "{{ 1 == 1 }}",
- "turn_on": {
- "service": "light.turn_on",
- "entity_id": "light.test_state",
- },
- "turn_off": {
- "service": "light.turn_off",
- "entity_id": "light.test_state",
- },
- "set_level": {
- "service": "light.turn_on",
- "data_template": {
- "entity_id": "light.test_state",
- "brightness": "{{brightness}}",
- },
- },
- }
- },
- }
- },
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- state = self.hass.states.get("light.test_template_light")
- assert state is not None
-
- assert state.attributes.get("friendly_name") == "Template light"
-
- def test_icon_template(self):
- """Test icon template."""
- with assert_setup_component(1, "light"):
- assert setup.setup_component(
- self.hass,
- "light",
- {
- "light": {
- "platform": "template",
- "lights": {
- "test_template_light": {
- "friendly_name": "Template light",
- "value_template": "{{ 1 == 1 }}",
- "turn_on": {
- "service": "light.turn_on",
- "entity_id": "light.test_state",
- },
- "turn_off": {
- "service": "light.turn_off",
- "entity_id": "light.test_state",
- },
- "set_level": {
- "service": "light.turn_on",
- "data_template": {
- "entity_id": "light.test_state",
- "brightness": "{{brightness}}",
- },
- },
- "icon_template": "{% if states.light.test_state.state %}"
- "mdi:check"
- "{% endif %}",
- }
- },
- }
- },
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- state = self.hass.states.get("light.test_template_light")
- assert state.attributes.get("icon") == ""
-
- state = self.hass.states.set("light.test_state", STATE_ON)
- self.hass.block_till_done()
-
- state = self.hass.states.get("light.test_template_light")
-
- assert state.attributes["icon"] == "mdi:check"
-
- def test_entity_picture_template(self):
- """Test entity_picture template."""
- with assert_setup_component(1, "light"):
- assert setup.setup_component(
- self.hass,
- "light",
- {
- "light": {
- "platform": "template",
- "lights": {
- "test_template_light": {
- "friendly_name": "Template light",
- "value_template": "{{ 1 == 1 }}",
- "turn_on": {
- "service": "light.turn_on",
- "entity_id": "light.test_state",
- },
- "turn_off": {
- "service": "light.turn_off",
- "entity_id": "light.test_state",
- },
- "set_level": {
- "service": "light.turn_on",
- "data_template": {
- "entity_id": "light.test_state",
- "brightness": "{{brightness}}",
- },
+ {
+ "service": "test.automation",
+ "data_template": {
+ "entity_id": "test.test_state",
+ "s": "{{s}}",
+ "h": "{{h}}",
},
- "entity_picture_template": "{% if states.light.test_state.state %}"
- "/local/light.png"
- "{% endif %}",
- }
- },
+ },
+ ],
}
},
- )
-
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- state = self.hass.states.get("light.test_template_light")
- assert state.attributes.get("entity_picture") == ""
-
- state = self.hass.states.set("light.test_state", STATE_ON)
- self.hass.block_till_done()
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
- state = self.hass.states.get("light.test_template_light")
+ state = hass.states.get("light.test_template_light")
+ assert state.attributes.get("hs_color") is None
- assert state.attributes["entity_picture"] == "/local/light.png"
+ await hass.services.async_call(
+ light.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "light.test_template_light", ATTR_HS_COLOR: (40, 50)},
+ blocking=True,
+ )
- def test_color_action_no_template(self):
- """Test setting color with optimistic template."""
- assert setup.setup_component(
- self.hass,
- "light",
+ assert len(calls) == 2
+ assert calls[0].data["h"] == 40
+ assert calls[0].data["s"] == 50
+ assert calls[1].data["h"] == 40
+ assert calls[1].data["s"] == 50
+
+ state = hass.states.get("light.test_template_light")
+ _LOGGER.info(str(state.attributes))
+ assert state is not None
+ assert calls[0].data["h"] == 40
+ assert calls[0].data["s"] == 50
+ assert calls[1].data["h"] == 40
+ assert calls[1].data["s"] == 50
+
+
+@pytest.mark.parametrize(
+ "expected_hs,template",
+ [
+ ((360, 100), "{{(360, 100)}}"),
+ ((359.9, 99.9), "{{(359.9, 99.9)}}"),
+ (None, "{{(361, 100)}}"),
+ (None, "{{(360, 101)}}"),
+ (None, "{{x - 12}}"),
+ (None, ""),
+ (None, "{{ none }}"),
+ ],
+)
+async def test_color_template(hass, expected_hs, template):
+ """Test the template for the color."""
+ with assert_setup_component(1, light.DOMAIN):
+ assert await setup.async_setup_component(
+ hass,
+ light.DOMAIN,
{
"light": {
"platform": "template",
"lights": {
"test_template_light": {
- "value_template": "{{1 == 1}}",
+ "value_template": "{{ 1 == 1 }}",
"turn_on": {
"service": "light.turn_on",
"entity_id": "light.test_state",
@@ -975,112 +1098,32 @@ def test_color_action_no_template(self):
},
"set_color": [
{
- "service": "test.automation",
- "data_template": {
- "entity_id": "test.test_state",
- "h": "{{h}}",
- "s": "{{s}}",
- },
- },
- {
- "service": "test.automation",
+ "service": "input_number.set_value",
"data_template": {
- "entity_id": "test.test_state",
- "s": "{{s}}",
- "h": "{{h}}",
+ "entity_id": "input_number.h",
+ "color_temp": "{{h}}",
},
- },
+ }
],
+ "color_template": template,
}
},
}
},
)
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
-
- state = self.hass.states.get("light.test_template_light")
- assert state.attributes.get("hs_color") is None
-
- common.turn_on(
- self.hass, "light.test_template_light", **{ATTR_HS_COLOR: (40, 50)}
- )
- self.hass.block_till_done()
- assert len(self.calls) == 2
- assert self.calls[0].data["h"] == 40
- assert self.calls[0].data["s"] == 50
- assert self.calls[1].data["h"] == 40
- assert self.calls[1].data["s"] == 50
-
- state = self.hass.states.get("light.test_template_light")
- _LOGGER.info(str(state.attributes))
- assert state is not None
- assert self.calls[0].data["h"] == 40
- assert self.calls[0].data["s"] == 50
- assert self.calls[1].data["h"] == 40
- assert self.calls[1].data["s"] == 50
-
- @pytest.mark.parametrize(
- "expected_hs,template",
- [
- ((360, 100), "{{(360, 100)}}"),
- ((359.9, 99.9), "{{(359.9, 99.9)}}"),
- (None, "{{(361, 100)}}"),
- (None, "{{(360, 101)}}"),
- (None, "{{x - 12}}"),
- (None, ""),
- (None, "{{ none }}"),
- ],
- )
- def test_color_template(self, expected_hs, template):
- """Test the template for the color."""
- with assert_setup_component(1, "light"):
- assert setup.setup_component(
- self.hass,
- "light",
- {
- "light": {
- "platform": "template",
- "lights": {
- "test_template_light": {
- "value_template": "{{ 1 == 1 }}",
- "turn_on": {
- "service": "light.turn_on",
- "entity_id": "light.test_state",
- },
- "turn_off": {
- "service": "light.turn_off",
- "entity_id": "light.test_state",
- },
- "set_color": [
- {
- "service": "input_number.set_value",
- "data_template": {
- "entity_id": "input_number.h",
- "color_temp": "{{h}}",
- },
- }
- ],
- "color_template": template,
- }
- },
- }
- },
- )
- self.hass.block_till_done()
- self.hass.start()
- self.hass.block_till_done()
- state = self.hass.states.get("light.test_template_light")
- assert state is not None
- assert state.attributes.get("hs_color") == expected_hs
+ await hass.async_block_till_done()
+ await hass.async_start()
+ await hass.async_block_till_done()
+ state = hass.states.get("light.test_template_light")
+ assert state is not None
+ assert state.attributes.get("hs_color") == expected_hs
async def test_available_template_with_entities(hass):
"""Test availability templates with values from other entities."""
await setup.async_setup_component(
hass,
- "light",
+ light.DOMAIN,
{
"light": {
"platform": "template",
@@ -1130,7 +1173,7 @@ async def test_invalid_availability_template_keeps_component_available(hass, cap
"""Test that an invalid availability keeps the device available."""
await setup.async_setup_component(
hass,
- "light",
+ light.DOMAIN,
{
"light": {
"platform": "template",
@@ -1170,7 +1213,7 @@ async def test_unique_id(hass):
"""Test unique_id option only creates one light per id."""
await setup.async_setup_component(
hass,
- "light",
+ light.DOMAIN,
{
"light": {
"platform": "template",
diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py
index 7f560fa0abb190..d146f5d88defa9 100644
--- a/tests/components/template/test_sensor.py
+++ b/tests/components/template/test_sensor.py
@@ -3,6 +3,8 @@
from datetime import timedelta
from unittest.mock import patch
+import pytest
+
from homeassistant.bootstrap import async_from_config_dict
from homeassistant.components import sensor
from homeassistant.const import (
@@ -13,8 +15,10 @@
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
+ STATE_UNKNOWN,
)
-from homeassistant.core import CoreState, callback
+from homeassistant.core import Context, CoreState, callback
+from homeassistant.helpers import entity_registry
from homeassistant.helpers.template import Template
from homeassistant.setup import ATTR_COMPONENT, async_setup_component
import homeassistant.util.dt as dt_util
@@ -403,6 +407,7 @@ async def test_setup_valid_device_class(hass):
assert "device_class" not in state.attributes
+@pytest.mark.parametrize("load_registries", [False])
async def test_creating_sensor_loads_group(hass):
"""Test setting up template sensor loads group component first."""
order = []
@@ -983,3 +988,166 @@ async def test_duplicate_templates(hass):
state = hass.states.get("sensor.test_template_sensor")
assert state.attributes["friendly_name"] == "Def"
assert state.state == "Def"
+
+
+async def test_trigger_entity(hass):
+ """Test trigger entity works."""
+ assert await async_setup_component(
+ hass,
+ "template",
+ {
+ "template": [
+ {"invalid": "config"},
+ # Config after invalid should still be set up
+ {
+ "unique_id": "listening-test-event",
+ "trigger": {"platform": "event", "event_type": "test_event"},
+ "sensors": {
+ "hello": {
+ "friendly_name": "Hello Name",
+ "unique_id": "hello_name-id",
+ "device_class": "battery",
+ "unit_of_measurement": "%",
+ "value_template": "{{ trigger.event.data.beer }}",
+ "entity_picture_template": "{{ '/local/dogs.png' }}",
+ "icon_template": "{{ 'mdi:pirate' }}",
+ "attribute_templates": {
+ "plus_one": "{{ trigger.event.data.beer + 1 }}"
+ },
+ },
+ },
+ "sensor": [
+ {
+ "name": "via list",
+ "unique_id": "via_list-id",
+ "device_class": "battery",
+ "unit_of_measurement": "%",
+ "state": "{{ trigger.event.data.beer + 1 }}",
+ "picture": "{{ '/local/dogs.png' }}",
+ "icon": "{{ 'mdi:pirate' }}",
+ "attributes": {
+ "plus_one": "{{ trigger.event.data.beer + 1 }}"
+ },
+ }
+ ],
+ },
+ {
+ "trigger": [],
+ "sensors": {
+ "bare_minimum": {
+ "value_template": "{{ trigger.event.data.beer }}"
+ },
+ },
+ },
+ ],
+ },
+ )
+
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.hello_name")
+ assert state is not None
+ assert state.state == STATE_UNKNOWN
+
+ state = hass.states.get("sensor.bare_minimum")
+ assert state is not None
+ assert state.state == STATE_UNKNOWN
+
+ context = Context()
+ hass.bus.async_fire("test_event", {"beer": 2}, context=context)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.hello_name")
+ assert state.state == "2"
+ assert state.attributes.get("device_class") == "battery"
+ assert state.attributes.get("icon") == "mdi:pirate"
+ assert state.attributes.get("entity_picture") == "/local/dogs.png"
+ assert state.attributes.get("plus_one") == 3
+ assert state.attributes.get("unit_of_measurement") == "%"
+ assert state.context is context
+
+ ent_reg = entity_registry.async_get(hass)
+ assert len(ent_reg.entities) == 2
+ assert (
+ ent_reg.entities["sensor.hello_name"].unique_id
+ == "listening-test-event-hello_name-id"
+ )
+ assert (
+ ent_reg.entities["sensor.via_list"].unique_id
+ == "listening-test-event-via_list-id"
+ )
+
+ state = hass.states.get("sensor.via_list")
+ assert state.state == "3"
+ assert state.attributes.get("device_class") == "battery"
+ assert state.attributes.get("icon") == "mdi:pirate"
+ assert state.attributes.get("entity_picture") == "/local/dogs.png"
+ assert state.attributes.get("plus_one") == 3
+ assert state.attributes.get("unit_of_measurement") == "%"
+ assert state.context is context
+
+
+async def test_trigger_entity_render_error(hass):
+ """Test trigger entity handles render error."""
+ assert await async_setup_component(
+ hass,
+ "template",
+ {
+ "template": {
+ "trigger": {"platform": "event", "event_type": "test_event"},
+ "sensors": {
+ "hello": {
+ "unique_id": "no-base-id",
+ "friendly_name": "Hello",
+ "value_template": "{{ non_existing + 1 }}",
+ }
+ },
+ },
+ },
+ )
+
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.hello")
+ assert state is not None
+ assert state.state == STATE_UNKNOWN
+
+ context = Context()
+ hass.bus.async_fire("test_event", {"beer": 2}, context=context)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.hello")
+ assert state.state == STATE_UNAVAILABLE
+
+ ent_reg = entity_registry.async_get(hass)
+ assert len(ent_reg.entities) == 1
+ assert ent_reg.entities["sensor.hello"].unique_id == "no-base-id"
+
+
+async def test_trigger_not_allowed_platform_config(hass, caplog):
+ """Test we throw a helpful warning if a trigger is configured in platform config."""
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "template",
+ "trigger": {"platform": "event", "event_type": "test_event"},
+ "sensors": {
+ "test_template_sensor": {
+ "value_template": "{{ states.sensor.test_state.state }}",
+ "friendly_name_template": "{{ states.sensor.test_state.state }}",
+ }
+ },
+ }
+ },
+ )
+
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.test_template_sensor")
+ assert state is None
+ assert (
+ "You can only add triggers to template entities if they are defined under `template:`."
+ in caplog.text
+ )
diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py
index de4974cb1b643b..e9634248c72ac2 100644
--- a/tests/components/template/test_trigger.py
+++ b/tests/components/template/test_trigger.py
@@ -18,7 +18,7 @@
async_mock_service,
mock_component,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
@@ -43,13 +43,18 @@ async def test_if_fires_on_change_bool(hass, calls):
automation.DOMAIN: {
"trigger": {
"platform": "template",
- "value_template": "{{ states.test.entity.state and true }}",
+ "value_template": '{{ states.test.entity.state == "world" and true }}',
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {"id": "{{ trigger.id}}"},
},
- "action": {"service": "test.automation"},
}
},
)
+ assert len(calls) == 0
+
hass.states.async_set("test.entity", "world")
await hass.async_block_till_done()
assert len(calls) == 1
@@ -64,6 +69,7 @@ async def test_if_fires_on_change_bool(hass, calls):
hass.states.async_set("test.entity", "planet")
await hass.async_block_till_done()
assert len(calls) == 1
+ assert calls[0].data["id"] == 0
async def test_if_fires_on_change_str(hass, calls):
@@ -75,13 +81,15 @@ async def test_if_fires_on_change_str(hass, calls):
automation.DOMAIN: {
"trigger": {
"platform": "template",
- "value_template": '{{ states.test.entity.state and "true" }}',
+ "value_template": '{{ states.test.entity.state == "world" and "true" }}',
},
"action": {"service": "test.automation"},
}
},
)
+ assert len(calls) == 0
+
hass.states.async_set("test.entity", "world")
await hass.async_block_till_done()
assert len(calls) == 1
@@ -96,7 +104,7 @@ async def test_if_fires_on_change_str_crazy(hass, calls):
automation.DOMAIN: {
"trigger": {
"platform": "template",
- "value_template": '{{ states.test.entity.state and "TrUE" }}',
+ "value_template": '{{ states.test.entity.state == "world" and "TrUE" }}',
},
"action": {"service": "test.automation"},
}
@@ -108,6 +116,100 @@ async def test_if_fires_on_change_str_crazy(hass, calls):
assert len(calls) == 1
+async def test_if_not_fires_when_true_at_setup(hass, calls):
+ """Test for not firing during startup."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger": {
+ "platform": "template",
+ "value_template": '{{ states.test.entity.state == "hello" }}',
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+
+ assert len(calls) == 0
+
+ hass.states.async_set("test.entity", "hello", force_update=True)
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+
+async def test_if_not_fires_when_true_at_setup_variables(hass, calls):
+ """Test for not firing during startup + trigger_variables."""
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger_variables": {"entity": "test.entity"},
+ "trigger": {
+ "platform": "template",
+ "value_template": '{{ is_state(entity|default("test.entity2"), "hello") }}',
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+
+ assert len(calls) == 0
+
+ # Assert that the trigger doesn't fire immediately when it's setup
+ # If trigger_variable 'entity' is not passed to initial check at setup, the
+ # trigger will immediately fire
+ hass.states.async_set("test.entity", "hello", force_update=True)
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ hass.states.async_set("test.entity", "goodbye", force_update=True)
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+ # Assert that the trigger fires after state change
+ # If trigger_variable 'entity' is not passed to the template trigger, the
+ # trigger will never fire because it falls back to 'test.entity2'
+ hass.states.async_set("test.entity", "hello", force_update=True)
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+
+async def test_if_not_fires_because_fail(hass, calls):
+ """Test for not firing after TemplateError."""
+ hass.states.async_set("test.number", "1")
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger": {
+ "platform": "template",
+ "value_template": "{{ 84 / states.test.number.state|int == 42 }}",
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+
+ assert len(calls) == 0
+
+ hass.states.async_set("test.number", "2")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+ hass.states.async_set("test.number", "0")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+ hass.states.async_set("test.number", "2")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+
+
async def test_if_not_fires_on_change_bool(hass, calls):
"""Test for not firing on boolean change."""
assert await async_setup_component(
@@ -117,7 +219,7 @@ async def test_if_not_fires_on_change_bool(hass, calls):
automation.DOMAIN: {
"trigger": {
"platform": "template",
- "value_template": "{{ states.test.entity.state and false }}",
+ "value_template": '{{ states.test.entity.state == "world" and false }}',
},
"action": {"service": "test.automation"},
}
@@ -198,7 +300,7 @@ async def test_if_fires_on_two_change(hass, calls):
automation.DOMAIN: {
"trigger": {
"platform": "template",
- "value_template": "{{ states.test.entity.state and true }}",
+ "value_template": "{{ states.test.entity.state == 'world' }}",
},
"action": {"service": "test.automation"},
}
@@ -297,7 +399,7 @@ async def test_if_fires_on_change_with_template_advanced(hass, calls):
await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0].context.parent_id == context.id
- assert "template - test.entity - hello - world - None" == calls[0].data["some"]
+ assert calls[0].data["some"] == "template - test.entity - hello - world - None"
async def test_if_fires_on_no_change_with_template_advanced(hass, calls):
@@ -559,7 +661,7 @@ async def test_if_fires_on_change_with_for_advanced(hass, calls):
await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0].context.parent_id == context.id
- assert "template - test.entity - hello - world - 0:00:05" == calls[0].data["some"]
+ assert calls[0].data["some"] == "template - test.entity - hello - world - 0:00:05"
async def test_if_fires_on_change_with_for_0(hass, calls):
diff --git a/tests/components/template/test_weather.py b/tests/components/template/test_weather.py
new file mode 100644
index 00000000000000..649a54aa3aaccf
--- /dev/null
+++ b/tests/components/template/test_weather.py
@@ -0,0 +1,76 @@
+"""The tests for the Template Weather platform."""
+from homeassistant.components.weather import (
+ ATTR_WEATHER_ATTRIBUTION,
+ ATTR_WEATHER_HUMIDITY,
+ ATTR_WEATHER_OZONE,
+ ATTR_WEATHER_PRESSURE,
+ ATTR_WEATHER_TEMPERATURE,
+ ATTR_WEATHER_VISIBILITY,
+ ATTR_WEATHER_WIND_BEARING,
+ ATTR_WEATHER_WIND_SPEED,
+ DOMAIN,
+)
+from homeassistant.setup import async_setup_component
+
+
+async def test_template_state_text(hass):
+ """Test the state text of a template."""
+ await async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ "weather": [
+ {"weather": {"platform": "demo"}},
+ {
+ "platform": "template",
+ "name": "test",
+ "attribution_template": "{{ states('sensor.attribution') }}",
+ "condition_template": "sunny",
+ "forecast_template": "{{ states.weather.demo.attributes.forecast }}",
+ "temperature_template": "{{ states('sensor.temperature') | float }}",
+ "humidity_template": "{{ states('sensor.humidity') | int }}",
+ "pressure_template": "{{ states('sensor.pressure') }}",
+ "wind_speed_template": "{{ states('sensor.windspeed') }}",
+ "wind_bearing_template": "{{ states('sensor.windbearing') }}",
+ "ozone_template": "{{ states('sensor.ozone') }}",
+ "visibility_template": "{{ states('sensor.visibility') }}",
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+
+ await hass.async_start()
+ await hass.async_block_till_done()
+
+ hass.states.async_set("sensor.attribution", "The custom attribution")
+ await hass.async_block_till_done()
+ hass.states.async_set("sensor.temperature", 22.3)
+ await hass.async_block_till_done()
+ hass.states.async_set("sensor.humidity", 60)
+ await hass.async_block_till_done()
+ hass.states.async_set("sensor.pressure", 1000)
+ await hass.async_block_till_done()
+ hass.states.async_set("sensor.windspeed", 20)
+ await hass.async_block_till_done()
+ hass.states.async_set("sensor.windbearing", 180)
+ await hass.async_block_till_done()
+ hass.states.async_set("sensor.ozone", 25)
+ await hass.async_block_till_done()
+ hass.states.async_set("sensor.visibility", 4.6)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("weather.test")
+ assert state is not None
+
+ assert state.state == "sunny"
+
+ data = state.attributes
+ assert data.get(ATTR_WEATHER_ATTRIBUTION) == "The custom attribution"
+ assert data.get(ATTR_WEATHER_TEMPERATURE) == 22.3
+ assert data.get(ATTR_WEATHER_HUMIDITY) == 60
+ assert data.get(ATTR_WEATHER_PRESSURE) == 1000
+ assert data.get(ATTR_WEATHER_WIND_SPEED) == 20
+ assert data.get(ATTR_WEATHER_WIND_BEARING) == 180
+ assert data.get(ATTR_WEATHER_OZONE) == 25
+ assert data.get(ATTR_WEATHER_VISIBILITY) == 4.6
diff --git a/tests/components/tesla/test_config_flow.py b/tests/components/tesla/test_config_flow.py
index 7fb308ecc43ef4..b35ab0039d0f26 100644
--- a/tests/components/tesla/test_config_flow.py
+++ b/tests/components/tesla/test_config_flow.py
@@ -48,6 +48,8 @@ async def test_form(hass):
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "test@email.com"
assert result2["data"] == {
+ CONF_USERNAME: "test@email.com",
+ CONF_PASSWORD: "test",
CONF_TOKEN: "test-refresh-token",
CONF_ACCESS_TOKEN: "test-access-token",
}
@@ -95,7 +97,12 @@ async def test_form_cannot_connect(hass):
async def test_form_repeat_identifier(hass):
"""Test we handle repeat identifiers."""
- entry = MockConfigEntry(domain=DOMAIN, title="test-username", data={}, options=None)
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="test-username",
+ data={"username": "test-username", "password": "test-password"},
+ options=None,
+ )
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
@@ -110,8 +117,36 @@ async def test_form_repeat_identifier(hass):
{CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"},
)
- assert result2["type"] == "form"
- assert result2["errors"] == {CONF_USERNAME: "already_configured"}
+ assert result2["type"] == "abort"
+ assert result2["reason"] == "already_configured"
+
+
+async def test_form_reauth(hass):
+ """Test we handle reauth."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="test-username",
+ data={"username": "test-username", "password": "same"},
+ options=None,
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_REAUTH},
+ data={"username": "test-username"},
+ )
+ with patch(
+ "homeassistant.components.tesla.config_flow.TeslaAPI.connect",
+ return_value=("test-refresh-token", "test-access-token"),
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password"},
+ )
+
+ assert result2["type"] == "abort"
+ assert result2["reason"] == "reauth_successful"
async def test_import(hass):
diff --git a/tests/components/threshold/test_binary_sensor.py b/tests/components/threshold/test_binary_sensor.py
index 66ab9e6cac1ff0..af8c32a1549a4f 100644
--- a/tests/components/threshold/test_binary_sensor.py
+++ b/tests/components/threshold/test_binary_sensor.py
@@ -24,12 +24,12 @@ async def test_sensor_upper(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "sensor.test_monitored" == state.attributes.get("entity_id")
- assert 16 == state.attributes.get("sensor_value")
- assert "above" == state.attributes.get("position")
- assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
- assert 0.0 == state.attributes.get("hysteresis")
- assert "upper" == state.attributes.get("type")
+ assert state.attributes.get("entity_id") == "sensor.test_monitored"
+ assert state.attributes.get("sensor_value") == 16
+ assert state.attributes.get("position") == "above"
+ assert state.attributes.get("upper") == float(config["binary_sensor"]["upper"])
+ assert state.attributes.get("hysteresis") == 0.0
+ assert state.attributes.get("type") == "upper"
assert state.state == "on"
@@ -66,10 +66,10 @@ async def test_sensor_lower(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "above" == state.attributes.get("position")
- assert float(config["binary_sensor"]["lower"]) == state.attributes.get("lower")
- assert 0.0 == state.attributes.get("hysteresis")
- assert "lower" == state.attributes.get("type")
+ assert state.attributes.get("position") == "above"
+ assert state.attributes.get("lower") == float(config["binary_sensor"]["lower"])
+ assert state.attributes.get("hysteresis") == 0.0
+ assert state.attributes.get("type") == "lower"
assert state.state == "off"
@@ -100,10 +100,10 @@ async def test_sensor_hysteresis(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "above" == state.attributes.get("position")
- assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
- assert 2.5 == state.attributes.get("hysteresis")
- assert "upper" == state.attributes.get("type")
+ assert state.attributes.get("position") == "above"
+ assert state.attributes.get("upper") == float(config["binary_sensor"]["upper"])
+ assert state.attributes.get("hysteresis") == 2.5
+ assert state.attributes.get("type") == "upper"
assert state.state == "on"
@@ -158,12 +158,12 @@ async def test_sensor_in_range_no_hysteresis(hass):
state = hass.states.get("binary_sensor.threshold")
assert state.attributes.get("entity_id") == "sensor.test_monitored"
- assert 16 == state.attributes.get("sensor_value")
- assert "in_range" == state.attributes.get("position")
- assert float(config["binary_sensor"]["lower"]) == state.attributes.get("lower")
- assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
- assert 0.0 == state.attributes.get("hysteresis")
- assert "range" == state.attributes.get("type")
+ assert state.attributes.get("sensor_value") == 16
+ assert state.attributes.get("position") == "in_range"
+ assert state.attributes.get("lower") == float(config["binary_sensor"]["lower"])
+ assert state.attributes.get("upper") == float(config["binary_sensor"]["upper"])
+ assert state.attributes.get("hysteresis") == 0.0
+ assert state.attributes.get("type") == "range"
assert state.state == "on"
@@ -172,7 +172,7 @@ async def test_sensor_in_range_no_hysteresis(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "below" == state.attributes.get("position")
+ assert state.attributes.get("position") == "below"
assert state.state == "off"
hass.states.async_set("sensor.test_monitored", 21)
@@ -180,7 +180,7 @@ async def test_sensor_in_range_no_hysteresis(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "above" == state.attributes.get("position")
+ assert state.attributes.get("position") == "above"
assert state.state == "off"
@@ -206,15 +206,15 @@ async def test_sensor_in_range_with_hysteresis(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "sensor.test_monitored" == state.attributes.get("entity_id")
- assert 16 == state.attributes.get("sensor_value")
- assert "in_range" == state.attributes.get("position")
- assert float(config["binary_sensor"]["lower"]) == state.attributes.get("lower")
- assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
- assert float(config["binary_sensor"]["hysteresis"]) == state.attributes.get(
- "hysteresis"
+ assert state.attributes.get("entity_id") == "sensor.test_monitored"
+ assert state.attributes.get("sensor_value") == 16
+ assert state.attributes.get("position") == "in_range"
+ assert state.attributes.get("lower") == float(config["binary_sensor"]["lower"])
+ assert state.attributes.get("upper") == float(config["binary_sensor"]["upper"])
+ assert state.attributes.get("hysteresis") == float(
+ config["binary_sensor"]["hysteresis"]
)
- assert "range" == state.attributes.get("type")
+ assert state.attributes.get("type") == "range"
assert state.state == "on"
@@ -223,7 +223,7 @@ async def test_sensor_in_range_with_hysteresis(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "in_range" == state.attributes.get("position")
+ assert state.attributes.get("position") == "in_range"
assert state.state == "on"
hass.states.async_set("sensor.test_monitored", 7)
@@ -231,7 +231,7 @@ async def test_sensor_in_range_with_hysteresis(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "below" == state.attributes.get("position")
+ assert state.attributes.get("position") == "below"
assert state.state == "off"
hass.states.async_set("sensor.test_monitored", 12)
@@ -239,7 +239,7 @@ async def test_sensor_in_range_with_hysteresis(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "below" == state.attributes.get("position")
+ assert state.attributes.get("position") == "below"
assert state.state == "off"
hass.states.async_set("sensor.test_monitored", 13)
@@ -247,7 +247,7 @@ async def test_sensor_in_range_with_hysteresis(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "in_range" == state.attributes.get("position")
+ assert state.attributes.get("position") == "in_range"
assert state.state == "on"
hass.states.async_set("sensor.test_monitored", 22)
@@ -255,7 +255,7 @@ async def test_sensor_in_range_with_hysteresis(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "in_range" == state.attributes.get("position")
+ assert state.attributes.get("position") == "in_range"
assert state.state == "on"
hass.states.async_set("sensor.test_monitored", 23)
@@ -263,7 +263,7 @@ async def test_sensor_in_range_with_hysteresis(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "above" == state.attributes.get("position")
+ assert state.attributes.get("position") == "above"
assert state.state == "off"
hass.states.async_set("sensor.test_monitored", 18)
@@ -271,7 +271,7 @@ async def test_sensor_in_range_with_hysteresis(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "above" == state.attributes.get("position")
+ assert state.attributes.get("position") == "above"
assert state.state == "off"
hass.states.async_set("sensor.test_monitored", 17)
@@ -279,7 +279,7 @@ async def test_sensor_in_range_with_hysteresis(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "in_range" == state.attributes.get("position")
+ assert state.attributes.get("position") == "in_range"
assert state.state == "on"
@@ -304,13 +304,13 @@ async def test_sensor_in_range_unknown_state(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "sensor.test_monitored" == state.attributes.get("entity_id")
- assert 16 == state.attributes.get("sensor_value")
- assert "in_range" == state.attributes.get("position")
- assert float(config["binary_sensor"]["lower"]) == state.attributes.get("lower")
- assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
- assert 0.0 == state.attributes.get("hysteresis")
- assert "range" == state.attributes.get("type")
+ assert state.attributes.get("entity_id") == "sensor.test_monitored"
+ assert state.attributes.get("sensor_value") == 16
+ assert state.attributes.get("position") == "in_range"
+ assert state.attributes.get("lower") == float(config["binary_sensor"]["lower"])
+ assert state.attributes.get("upper") == float(config["binary_sensor"]["upper"])
+ assert state.attributes.get("hysteresis") == 0.0
+ assert state.attributes.get("type") == "range"
assert state.state == "on"
@@ -319,7 +319,7 @@ async def test_sensor_in_range_unknown_state(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "unknown" == state.attributes.get("position")
+ assert state.attributes.get("position") == "unknown"
assert state.state == "off"
@@ -341,8 +341,8 @@ async def test_sensor_lower_zero_threshold(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "lower" == state.attributes.get("type")
- assert float(config["binary_sensor"]["lower"]) == state.attributes.get("lower")
+ assert state.attributes.get("type") == "lower"
+ assert state.attributes.get("lower") == float(config["binary_sensor"]["lower"])
assert state.state == "off"
@@ -372,8 +372,8 @@ async def test_sensor_upper_zero_threshold(hass):
state = hass.states.get("binary_sensor.threshold")
- assert "upper" == state.attributes.get("type")
- assert float(config["binary_sensor"]["upper"]) == state.attributes.get("upper")
+ assert state.attributes.get("type") == "upper"
+ assert state.attributes.get("upper") == float(config["binary_sensor"]["upper"])
assert state.state == "off"
diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py
index 74c3eceeea271d..92958a7bce6bde 100644
--- a/tests/components/timer/test_init.py
+++ b/tests/components/timer/test_init.py
@@ -39,7 +39,7 @@
)
from homeassistant.core import Context, CoreState
from homeassistant.exceptions import Unauthorized
-from homeassistant.helpers import config_validation as cv, entity_registry
+from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
@@ -120,16 +120,16 @@ async def test_config_options(hass):
assert state_2 is not None
assert state_3 is not None
- assert STATUS_IDLE == state_1.state
+ assert state_1.state == STATUS_IDLE
assert ATTR_ICON not in state_1.attributes
assert ATTR_FRIENDLY_NAME not in state_1.attributes
- assert STATUS_IDLE == state_2.state
- assert "Hello World" == state_2.attributes.get(ATTR_FRIENDLY_NAME)
- assert "mdi:work" == state_2.attributes.get(ATTR_ICON)
- assert "0:00:10" == state_2.attributes.get(ATTR_DURATION)
+ assert state_2.state == STATUS_IDLE
+ assert state_2.attributes.get(ATTR_FRIENDLY_NAME) == "Hello World"
+ assert state_2.attributes.get(ATTR_ICON) == "mdi:work"
+ assert state_2.attributes.get(ATTR_DURATION) == "0:00:10"
- assert STATUS_IDLE == state_3.state
+ assert state_3.state == STATUS_IDLE
assert str(cv.time_period(DEFAULT_DURATION)) == state_3.attributes.get(
CONF_DURATION
)
@@ -248,7 +248,7 @@ async def test_no_initial_state_and_no_restore_state(hass):
async def test_config_reload(hass, hass_admin_user, hass_read_only_user):
"""Test reload service."""
count_start = len(hass.states.async_entity_ids())
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
_LOGGER.debug("ENTITIES @ start: %s", hass.states.async_entity_ids())
@@ -280,14 +280,14 @@ async def test_config_reload(hass, hass_admin_user, hass_read_only_user):
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is None
- assert STATUS_IDLE == state_1.state
+ assert state_1.state == STATUS_IDLE
assert ATTR_ICON not in state_1.attributes
assert ATTR_FRIENDLY_NAME not in state_1.attributes
- assert STATUS_IDLE == state_2.state
- assert "Hello World" == state_2.attributes.get(ATTR_FRIENDLY_NAME)
- assert "mdi:work" == state_2.attributes.get(ATTR_ICON)
- assert "0:00:10" == state_2.attributes.get(ATTR_DURATION)
+ assert state_2.state == STATUS_IDLE
+ assert state_2.attributes.get(ATTR_FRIENDLY_NAME) == "Hello World"
+ assert state_2.attributes.get(ATTR_ICON) == "mdi:work"
+ assert state_2.attributes.get(ATTR_DURATION) == "0:00:10"
with patch(
"homeassistant.config.load_yaml_config_file",
@@ -331,12 +331,12 @@ async def test_config_reload(hass, hass_admin_user, hass_read_only_user):
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is not None
- assert STATUS_IDLE == state_2.state
- assert "Hello World reloaded" == state_2.attributes.get(ATTR_FRIENDLY_NAME)
- assert "mdi:work-reloaded" == state_2.attributes.get(ATTR_ICON)
- assert "0:00:20" == state_2.attributes.get(ATTR_DURATION)
+ assert state_2.state == STATUS_IDLE
+ assert state_2.attributes.get(ATTR_FRIENDLY_NAME) == "Hello World reloaded"
+ assert state_2.attributes.get(ATTR_ICON) == "mdi:work-reloaded"
+ assert state_2.attributes.get(ATTR_DURATION) == "0:00:20"
- assert STATUS_IDLE == state_3.state
+ assert state_3.state == STATUS_IDLE
assert ATTR_ICON not in state_3.attributes
assert ATTR_FRIENDLY_NAME not in state_3.attributes
@@ -498,7 +498,7 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup):
timer_id = "from_storage"
timer_entity_id = f"{DOMAIN}.{DOMAIN}_{timer_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(timer_entity_id)
assert state is not None
@@ -525,7 +525,7 @@ async def test_update(hass, hass_ws_client, storage_setup):
timer_id = "from_storage"
timer_entity_id = f"{DOMAIN}.{DOMAIN}_{timer_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(timer_entity_id)
assert state.attributes[ATTR_FRIENDLY_NAME] == "timer from storage"
@@ -554,7 +554,7 @@ async def test_ws_create(hass, hass_ws_client, storage_setup):
timer_id = "new_timer"
timer_entity_id = f"{DOMAIN}.{timer_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(timer_entity_id)
assert state is None
diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py
index dff1e208ab6372..2eb506f80f362b 100644
--- a/tests/components/tod/test_binary_sensor.py
+++ b/tests/components/tod/test_binary_sensor.py
@@ -643,6 +643,7 @@ async def test_norwegian_case_summer(hass):
"""Test location in Norway where the sun doesn't set in summer."""
hass.config.latitude = 69.6
hass.config.longitude = 18.8
+ hass.config.elevation = 10.0
test_time = hass.config.time_zone.localize(datetime(2010, 6, 1)).astimezone(
pytz.UTC
@@ -652,7 +653,7 @@ async def test_norwegian_case_summer(hass):
get_astral_event_next(hass, "sunrise", dt_util.as_utc(test_time))
)
sunset = dt_util.as_local(
- get_astral_event_next(hass, "sunset", dt_util.as_utc(test_time))
+ get_astral_event_next(hass, "sunset", dt_util.as_utc(sunrise))
)
config = {
"binary_sensor": [
@@ -903,7 +904,7 @@ async def test_dst(hass):
await hass.async_block_till_done()
state = hass.states.get(entity_id)
- state.attributes["after"] == "2019-03-31T03:30:00+02:00"
- state.attributes["before"] == "2019-03-31T03:40:00+02:00"
- state.attributes["next_update"] == "2019-03-31T03:30:00+02:00"
+ assert state.attributes["after"] == "2019-03-31T03:30:00+02:00"
+ assert state.attributes["before"] == "2019-03-31T03:40:00+02:00"
+ assert state.attributes["next_update"] == "2019-03-31T03:30:00+02:00"
assert state.state == STATE_OFF
diff --git a/tests/components/totalconnect/common.py b/tests/components/totalconnect/common.py
index 17fa244f9b2839..d4285c07425002 100644
--- a/tests/components/totalconnect/common.py
+++ b/tests/components/totalconnect/common.py
@@ -3,7 +3,7 @@
from total_connect_client import TotalConnectClient
-from homeassistant.components.totalconnect import DOMAIN
+from homeassistant.components.totalconnect.const import CONF_USERCODES, DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.setup import async_setup_component
@@ -29,13 +29,19 @@
}
RESPONSE_AUTHENTICATE = {
- "ResultCode": 0,
+ "ResultCode": TotalConnectClient.TotalConnectClient.SUCCESS,
"SessionID": 1,
"Locations": LOCATIONS,
"ModuleFlags": MODULE_FLAGS,
"UserInfo": USER,
}
+RESPONSE_AUTHENTICATE_FAILED = {
+ "ResultCode": TotalConnectClient.TotalConnectClient.BAD_USER_OR_PASSWORD,
+ "ResultData": "test bad authentication",
+}
+
+
PARTITION_DISARMED = {
"PartitionID": "1",
"ArmingState": TotalConnectClient.TotalConnectLocation.DISARMED,
@@ -101,6 +107,32 @@
"ResultCode": TotalConnectClient.TotalConnectClient.COMMAND_FAILED,
"ResultData": "Command Failed",
}
+RESPONSE_USER_CODE_INVALID = {
+ "ResultCode": TotalConnectClient.TotalConnectClient.USER_CODE_INVALID,
+ "ResultData": "testing user code invalid",
+}
+RESPONSE_SUCCESS = {"ResultCode": TotalConnectClient.TotalConnectClient.SUCCESS}
+
+USERNAME = "username@me.com"
+PASSWORD = "password"
+USERCODES = {123456: "7890"}
+CONFIG_DATA = {
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ CONF_USERCODES: USERCODES,
+}
+CONFIG_DATA_NO_USERCODES = {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}
+
+
+USERNAME = "username@me.com"
+PASSWORD = "password"
+USERCODES = {123456: "7890"}
+CONFIG_DATA = {
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ CONF_USERCODES: USERCODES,
+}
+CONFIG_DATA_NO_USERCODES = {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}
async def setup_platform(hass, platform):
@@ -108,7 +140,7 @@ async def setup_platform(hass, platform):
# first set up a config entry and add it to hass
mock_entry = MockConfigEntry(
domain=DOMAIN,
- data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"},
+ data=CONFIG_DATA,
)
mock_entry.add_to_hass(hass)
diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py
index bc90c1aae2a4f1..12ad53733b5413 100644
--- a/tests/components/totalconnect/test_alarm_control_panel.py
+++ b/tests/components/totalconnect/test_alarm_control_panel.py
@@ -14,6 +14,7 @@
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
)
+from homeassistant.exceptions import HomeAssistantError
from .common import (
RESPONSE_ARM_FAILURE,
@@ -23,6 +24,7 @@
RESPONSE_DISARM_FAILURE,
RESPONSE_DISARM_SUCCESS,
RESPONSE_DISARMED,
+ RESPONSE_USER_CODE_INVALID,
setup_platform,
)
@@ -52,14 +54,14 @@ async def test_arm_home_success(hass):
side_effect=responses,
):
await setup_platform(hass, ALARM_DOMAIN)
- assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
await hass.services.async_call(
ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True
)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_HOME == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_HOME
async def test_arm_home_failure(hass):
@@ -70,15 +72,34 @@ async def test_arm_home_failure(hass):
side_effect=responses,
):
await setup_platform(hass, ALARM_DOMAIN)
- assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
- with pytest.raises(Exception) as e:
+ with pytest.raises(HomeAssistantError) as err:
await hass.services.async_call(
ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True
)
await hass.async_block_till_done()
- assert f"{e.value}" == "TotalConnect failed to arm home test."
- assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state
+ assert f"{err.value}" == "TotalConnect failed to arm home test."
+ assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
+
+
+async def test_arm_home_invalid_usercode(hass):
+ """Test arm home method with invalid usercode."""
+ responses = [RESPONSE_DISARMED, RESPONSE_USER_CODE_INVALID, RESPONSE_DISARMED]
+ with patch(
+ "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request",
+ side_effect=responses,
+ ):
+ await setup_platform(hass, ALARM_DOMAIN)
+ assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
+
+ with pytest.raises(HomeAssistantError) as err:
+ await hass.services.async_call(
+ ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True
+ )
+ await hass.async_block_till_done()
+ assert f"{err.value}" == "TotalConnect failed to arm home test."
+ assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
async def test_arm_away_success(hass):
@@ -89,13 +110,13 @@ async def test_arm_away_success(hass):
side_effect=responses,
):
await setup_platform(hass, ALARM_DOMAIN)
- assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
await hass.services.async_call(
ALARM_DOMAIN, SERVICE_ALARM_ARM_AWAY, DATA, blocking=True
)
await hass.async_block_till_done()
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY
async def test_arm_away_failure(hass):
@@ -106,15 +127,15 @@ async def test_arm_away_failure(hass):
side_effect=responses,
):
await setup_platform(hass, ALARM_DOMAIN)
- assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
- with pytest.raises(Exception) as e:
+ with pytest.raises(HomeAssistantError) as err:
await hass.services.async_call(
ALARM_DOMAIN, SERVICE_ALARM_ARM_AWAY, DATA, blocking=True
)
await hass.async_block_till_done()
- assert f"{e.value}" == "TotalConnect failed to arm away test."
- assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state
+ assert f"{err.value}" == "TotalConnect failed to arm away test."
+ assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
async def test_disarm_success(hass):
@@ -125,13 +146,13 @@ async def test_disarm_success(hass):
side_effect=responses,
):
await setup_platform(hass, ALARM_DOMAIN)
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY
await hass.services.async_call(
ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True
)
await hass.async_block_till_done()
- assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
async def test_disarm_failure(hass):
@@ -142,12 +163,31 @@ async def test_disarm_failure(hass):
side_effect=responses,
):
await setup_platform(hass, ALARM_DOMAIN)
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(ENTITY_ID).state
+ assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY
+
+ with pytest.raises(HomeAssistantError) as err:
+ await hass.services.async_call(
+ ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True
+ )
+ await hass.async_block_till_done()
+ assert f"{err.value}" == "TotalConnect failed to disarm test."
+ assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY
+
+
+async def test_disarm_invalid_usercode(hass):
+ """Test disarm method failure."""
+ responses = [RESPONSE_ARMED_AWAY, RESPONSE_USER_CODE_INVALID, RESPONSE_ARMED_AWAY]
+ with patch(
+ "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request",
+ side_effect=responses,
+ ):
+ await setup_platform(hass, ALARM_DOMAIN)
+ assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY
- with pytest.raises(Exception) as e:
+ with pytest.raises(HomeAssistantError) as err:
await hass.services.async_call(
ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True
)
await hass.async_block_till_done()
- assert f"{e.value}" == "TotalConnect failed to disarm test."
- assert STATE_ALARM_ARMED_AWAY == hass.states.get(ENTITY_ID).state
+ assert f"{err.value}" == "TotalConnect failed to disarm test."
+ assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY
diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py
index a1aa8780cfb64c..5d1723a835e3ab 100644
--- a/tests/components/totalconnect/test_config_flow.py
+++ b/tests/components/totalconnect/test_config_flow.py
@@ -1,78 +1,97 @@
-"""Tests for the iCloud config flow."""
+"""Tests for the TotalConnect config flow."""
from unittest.mock import patch
from homeassistant import data_entry_flow
-from homeassistant.components.totalconnect.const import DOMAIN
-from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant.components.totalconnect.const import CONF_LOCATION, DOMAIN
+from homeassistant.config_entries import SOURCE_USER
+from homeassistant.const import CONF_PASSWORD
+
+from .common import (
+ CONFIG_DATA,
+ CONFIG_DATA_NO_USERCODES,
+ RESPONSE_AUTHENTICATE,
+ RESPONSE_DISARMED,
+ RESPONSE_SUCCESS,
+ RESPONSE_USER_CODE_INVALID,
+ USERNAME,
+)
from tests.common import MockConfigEntry
-USERNAME = "username@me.com"
-PASSWORD = "password"
-
async def test_user(hass):
- """Test user config."""
- # no data provided so show the form
+ """Test user step."""
+ # user starts with no data entered, so show the user form
result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ data=None,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
- # now data is provided, so check if login is correct and create the entry
- with patch(
- "homeassistant.components.totalconnect.config_flow.TotalConnectClient.TotalConnectClient"
- ) as client_mock:
- client_mock.return_value.is_valid_credentials.return_value = True
+
+async def test_user_show_locations(hass):
+ """Test user locations form."""
+ # user/pass provided, so check if valid then ask for usercodes on locations form
+ responses = [
+ RESPONSE_AUTHENTICATE,
+ RESPONSE_DISARMED,
+ RESPONSE_USER_CODE_INVALID,
+ RESPONSE_SUCCESS,
+ ]
+
+ with patch("zeep.Client", autospec=True), patch(
+ "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request",
+ side_effect=responses,
+ ) as mock_request, patch(
+ "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.get_zone_details",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.totalconnect.async_setup_entry", return_value=True
+ ):
+
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
- data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ data=CONFIG_DATA_NO_USERCODES,
)
- assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ # first it should show the locations form
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "locations"
+ # client should have sent two requests, authenticate and get status
+ assert mock_request.call_count == 2
-
-async def test_import(hass):
- """Test import step with good username and password."""
- with patch(
- "homeassistant.components.totalconnect.config_flow.TotalConnectClient.TotalConnectClient"
- ) as client_mock:
- client_mock.return_value.is_valid_credentials.return_value = True
- result = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": SOURCE_IMPORT},
- data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ # user enters an invalid usercode
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={CONF_LOCATION: "bad"},
)
-
- assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == "locations"
+ # client should have sent 3rd request to validate usercode
+ assert mock_request.call_count == 3
+
+ # user enters a valid usercode
+ result3 = await hass.config_entries.flow.async_configure(
+ result2["flow_id"],
+ user_input={CONF_LOCATION: "7890"},
+ )
+ assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ # client should have sent another request to validate usercode
+ assert mock_request.call_count == 4
async def test_abort_if_already_setup(hass):
"""Test abort if the account is already setup."""
MockConfigEntry(
domain=DOMAIN,
- data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ data=CONFIG_DATA,
unique_id=USERNAME,
).add_to_hass(hass)
- # Should fail, same USERNAME (import)
- with patch(
- "homeassistant.components.totalconnect.config_flow.TotalConnectClient.TotalConnectClient"
- ) as client_mock:
- client_mock.return_value.is_valid_credentials.return_value = True
- result = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": SOURCE_IMPORT},
- data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
- )
-
- assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "already_configured"
-
# Should fail, same USERNAME (flow)
with patch(
"homeassistant.components.totalconnect.config_flow.TotalConnectClient.TotalConnectClient"
@@ -81,7 +100,7 @@ async def test_abort_if_already_setup(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
- data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ data=CONFIG_DATA,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
@@ -97,8 +116,51 @@ async def test_login_failed(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
- data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
+ data=CONFIG_DATA,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"base": "invalid_auth"}
+
+
+async def test_reauth(hass):
+ """Test reauth."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=CONFIG_DATA,
+ unique_id=USERNAME,
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=entry.data
+ )
+ assert result["step_id"] == "reauth_confirm"
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth_confirm"
+
+ with patch(
+ "homeassistant.components.totalconnect.config_flow.TotalConnectClient.TotalConnectClient"
+ ) as client_mock:
+ # first test with an invalid password
+ client_mock.return_value.is_valid_credentials.return_value = False
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={CONF_PASSWORD: "password"}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth_confirm"
+ assert result["errors"] == {"base": "invalid_auth"}
+
+ # now test with the password valid
+ client_mock.return_value.is_valid_credentials.return_value = True
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={CONF_PASSWORD: "password"}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "reauth_successful"
+
+ assert len(hass.config_entries.async_entries()) == 1
diff --git a/tests/components/totalconnect/test_init.py b/tests/components/totalconnect/test_init.py
new file mode 100644
index 00000000000000..b8024dbe70d606
--- /dev/null
+++ b/tests/components/totalconnect/test_init.py
@@ -0,0 +1,29 @@
+"""Tests for the TotalConnect init process."""
+from unittest.mock import patch
+
+from homeassistant.components.totalconnect.const import DOMAIN
+from homeassistant.config_entries import ENTRY_STATE_SETUP_ERROR
+from homeassistant.setup import async_setup_component
+
+from .common import CONFIG_DATA
+
+from tests.common import MockConfigEntry
+
+
+async def test_reauth_started(hass):
+ """Test that reauth is started when we have login errors."""
+ mock_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=CONFIG_DATA,
+ )
+ mock_entry.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient",
+ autospec=True,
+ ) as mock_client:
+ mock_client.return_value.is_valid_credentials.return_value = False
+ assert await async_setup_component(hass, DOMAIN, {})
+ await hass.async_block_till_done()
+
+ assert mock_entry.state == ENTRY_STATE_SETUP_ERROR
diff --git a/tests/components/tplink/conftest.py b/tests/components/tplink/conftest.py
index d1800513486f22..61b242c5d2e3e2 100644
--- a/tests/components/tplink/conftest.py
+++ b/tests/components/tplink/conftest.py
@@ -1,2 +1,2 @@
"""tplink conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py
index 8dbe7481339eb4..49309a6ecefbc5 100644
--- a/tests/components/tplink/test_init.py
+++ b/tests/components/tplink/test_init.py
@@ -1,5 +1,7 @@
"""Tests for the TP-Link component."""
-from typing import Any, Dict
+from __future__ import annotations
+
+from typing import Any
from unittest.mock import MagicMock, patch
from pyHS100 import SmartBulb, SmartDevice, SmartDeviceException, SmartPlug
@@ -88,25 +90,20 @@ class UnknownSmartDevice(SmartDevice):
@property
def has_emeter(self) -> bool:
"""Do nothing."""
- pass
def turn_off(self) -> None:
"""Do nothing."""
- pass
def turn_on(self) -> None:
"""Do nothing."""
- pass
@property
def is_on(self) -> bool:
"""Do nothing."""
- pass
@property
- def state_information(self) -> Dict[str, Any]:
+ def state_information(self) -> dict[str, Any]:
"""Do nothing."""
- pass
async def test_configuring_devices_from_multiple_sources(hass):
diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py
index 4c34e754ec3d11..5e81295468b6c7 100644
--- a/tests/components/tplink/test_light.py
+++ b/tests/components/tplink/test_light.py
@@ -60,6 +60,116 @@ class SmartSwitchMockData(NamedTuple):
get_sysinfo_mock: Mock
+@pytest.fixture(name="unknown_light_mock_data")
+def unknown_light_mock_data_fixture() -> None:
+ """Create light mock data."""
+ sys_info = {
+ "sw_ver": "1.2.3",
+ "hw_ver": "2.3.4",
+ "mac": "aa:bb:cc:dd:ee:ff",
+ "mic_mac": "00:11:22:33:44",
+ "type": "light",
+ "hwId": "1234",
+ "fwId": "4567",
+ "oemId": "891011",
+ "dev_name": "light1",
+ "rssi": 11,
+ "latitude": "0",
+ "longitude": "0",
+ "is_color": True,
+ "is_dimmable": True,
+ "is_variable_color_temp": True,
+ "model": "Foo",
+ "alias": "light1",
+ }
+ light_state = {
+ "on_off": True,
+ "dft_on_state": {
+ "brightness": 12,
+ "color_temp": 3200,
+ "hue": 110,
+ "saturation": 90,
+ },
+ "brightness": 13,
+ "color_temp": 3300,
+ "hue": 110,
+ "saturation": 90,
+ }
+
+ def set_light_state(state) -> None:
+ nonlocal light_state
+ drt_on_state = light_state["dft_on_state"]
+ drt_on_state.update(state.get("dft_on_state", {}))
+
+ light_state.update(state)
+ light_state["dft_on_state"] = drt_on_state
+ return light_state
+
+ set_light_state_patch = patch(
+ "homeassistant.components.tplink.common.SmartBulb.set_light_state",
+ side_effect=set_light_state,
+ )
+ get_light_state_patch = patch(
+ "homeassistant.components.tplink.common.SmartBulb.get_light_state",
+ return_value=light_state,
+ )
+ current_consumption_patch = patch(
+ "homeassistant.components.tplink.common.SmartDevice.current_consumption",
+ return_value=3.23,
+ )
+ get_sysinfo_patch = patch(
+ "homeassistant.components.tplink.common.SmartDevice.get_sysinfo",
+ return_value=sys_info,
+ )
+ get_emeter_daily_patch = patch(
+ "homeassistant.components.tplink.common.SmartDevice.get_emeter_daily",
+ return_value={
+ 1: 1.01,
+ 2: 1.02,
+ 3: 1.03,
+ 4: 1.04,
+ 5: 1.05,
+ 6: 1.06,
+ 7: 1.07,
+ 8: 1.08,
+ 9: 1.09,
+ 10: 1.10,
+ 11: 1.11,
+ 12: 1.12,
+ },
+ )
+ get_emeter_monthly_patch = patch(
+ "homeassistant.components.tplink.common.SmartDevice.get_emeter_monthly",
+ return_value={
+ 1: 2.01,
+ 2: 2.02,
+ 3: 2.03,
+ 4: 2.04,
+ 5: 2.05,
+ 6: 2.06,
+ 7: 2.07,
+ 8: 2.08,
+ 9: 2.09,
+ 10: 2.10,
+ 11: 2.11,
+ 12: 2.12,
+ },
+ )
+
+ with set_light_state_patch as set_light_state_mock, get_light_state_patch as get_light_state_mock, current_consumption_patch as current_consumption_mock, get_sysinfo_patch as get_sysinfo_mock, get_emeter_daily_patch as get_emeter_daily_mock, get_emeter_monthly_patch as get_emeter_monthly_mock:
+ yield LightMockData(
+ sys_info=sys_info,
+ light_state=light_state,
+ set_light_state=set_light_state,
+ set_light_state_mock=set_light_state_mock,
+ get_light_state_mock=get_light_state_mock,
+ current_consumption_mock=current_consumption_mock,
+ get_sysinfo_mock=get_sysinfo_mock,
+ get_emeter_daily_mock=get_emeter_daily_mock,
+ get_emeter_monthly_mock=get_emeter_monthly_mock,
+ )
+
+
@pytest.fixture(name="light_mock_data")
def light_mock_data_fixture() -> None:
"""Create light mock data."""
@@ -343,6 +453,31 @@ async def test_smartswitch(
assert sys_info["brightness"] == 66
+async def test_unknown_light(
+ hass: HomeAssistant, unknown_light_mock_data: LightMockData
+) -> None:
+ """Test function."""
+ await async_setup_component(hass, HA_DOMAIN, {})
+ await hass.async_block_till_done()
+
+ await async_setup_component(
+ hass,
+ tplink.DOMAIN,
+ {
+ tplink.DOMAIN: {
+ CONF_DISCOVERY: False,
+ CONF_LIGHT: [{CONF_HOST: "123.123.123.123"}],
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.light1")
+ assert state.state == "on"
+ assert state.attributes["min_mireds"] == 200
+ assert state.attributes["max_mireds"] == 370
+
+
async def test_light(hass: HomeAssistant, light_mock_data: LightMockData) -> None:
"""Test function."""
light_state = light_mock_data.light_state
diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py
index c7e031b2ca6f18..f372358ea1db9f 100644
--- a/tests/components/traccar/test_init.py
+++ b/tests/components/traccar/test_init.py
@@ -14,6 +14,7 @@
STATE_HOME,
STATE_NOT_HOME,
)
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import DATA_DISPATCHER
from homeassistant.setup import async_setup_component
@@ -113,7 +114,7 @@ async def test_enter_and_exit(hass, client, webhook_id):
state_name = hass.states.get(
"{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"])
).state
- assert STATE_HOME == state_name
+ assert state_name == STATE_HOME
# Enter Home again
req = await client.post(url, params=data)
@@ -122,7 +123,7 @@ async def test_enter_and_exit(hass, client, webhook_id):
state_name = hass.states.get(
"{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"])
).state
- assert STATE_HOME == state_name
+ assert state_name == STATE_HOME
data["lon"] = 0
data["lat"] = 0
@@ -134,12 +135,12 @@ async def test_enter_and_exit(hass, client, webhook_id):
state_name = hass.states.get(
"{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"])
).state
- assert STATE_NOT_HOME == state_name
+ assert state_name == STATE_NOT_HOME
- dev_reg = await hass.helpers.device_registry.async_get_registry()
+ dev_reg = dr.async_get(hass)
assert len(dev_reg.devices) == 1
- ent_reg = await hass.helpers.entity_registry.async_get_registry()
+ ent_reg = er.async_get(hass)
assert len(ent_reg.entities) == 1
@@ -236,7 +237,7 @@ async def test_load_unload_entry(hass, client, webhook_id):
state_name = hass.states.get(
"{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"])
).state
- assert STATE_HOME == state_name
+ assert state_name == STATE_HOME
assert len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) == 1
entry = hass.config_entries.async_entries(DOMAIN)[0]
diff --git a/tests/components/trace/__init__.py b/tests/components/trace/__init__.py
new file mode 100644
index 00000000000000..13937105ae3dfc
--- /dev/null
+++ b/tests/components/trace/__init__.py
@@ -0,0 +1 @@
+"""The tests for Trace."""
diff --git a/tests/components/trace/test_utils.py b/tests/components/trace/test_utils.py
new file mode 100644
index 00000000000000..ce0f09bfdd8b0b
--- /dev/null
+++ b/tests/components/trace/test_utils.py
@@ -0,0 +1,42 @@
+"""Test trace helpers."""
+from datetime import timedelta
+
+from homeassistant import core
+from homeassistant.components import trace
+from homeassistant.util import dt as dt_util
+
+
+def test_json_encoder(hass):
+ """Test the Trace JSON Encoder."""
+ ha_json_enc = trace.utils.TraceJSONEncoder()
+ state = core.State("test.test", "hello")
+
+ # Test serializing a datetime
+ now = dt_util.utcnow()
+ assert ha_json_enc.default(now) == now.isoformat()
+
+ # Test serializing a timedelta
+ data = timedelta(
+ days=50,
+ seconds=27,
+ microseconds=10,
+ milliseconds=29000,
+ minutes=5,
+ hours=8,
+ weeks=2,
+ )
+ assert ha_json_enc.default(data) == {
+ "__type": str(type(data)),
+ "total_seconds": data.total_seconds(),
+ }
+
+ # Test serializing a set()
+ data = {"milk", "beer"}
+ assert sorted(ha_json_enc.default(data)) == sorted(data)
+
+ # Test serializong object which implements as_dict
+ assert ha_json_enc.default(state) == state.as_dict()
+
+ # Default method falls back to repr(o)
+ o = object()
+ assert ha_json_enc.default(o) == {"__type": str(type(o)), "repr": repr(o)}
diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py
new file mode 100644
index 00000000000000..0b7b78b3f1a56b
--- /dev/null
+++ b/tests/components/trace/test_websocket_api.py
@@ -0,0 +1,1234 @@
+"""Test Trace websocket API."""
+import asyncio
+
+import pytest
+
+from homeassistant.bootstrap import async_setup_component
+from homeassistant.components.trace.const import STORED_TRACES
+from homeassistant.core import Context, callback
+from homeassistant.helpers.typing import UNDEFINED
+
+from tests.common import assert_lists_same
+
+
+def _find_run_id(traces, trace_type, item_id):
+ """Find newest run_id for an script or automation."""
+ for trace in reversed(traces):
+ if trace["domain"] == trace_type and trace["item_id"] == item_id:
+ return trace["run_id"]
+
+ return None
+
+
+def _find_traces(traces, trace_type, item_id):
+ """Find traces for an script or automation."""
+ return [
+ trace
+ for trace in traces
+ if trace["domain"] == trace_type and trace["item_id"] == item_id
+ ]
+
+
+async def _setup_automation_or_script(hass, domain, configs, script_config=None):
+ """Set up automations or scripts from automation config."""
+ if domain == "script":
+ configs = {config["id"]: {"sequence": config["action"]} for config in configs}
+
+ if script_config:
+ if domain == "automation":
+ assert await async_setup_component(
+ hass, "script", {"script": script_config}
+ )
+ else:
+ configs = {**configs, **script_config}
+
+ assert await async_setup_component(hass, domain, {domain: configs})
+
+
+async def _run_automation_or_script(hass, domain, config, event, context=None):
+ if domain == "automation":
+ hass.bus.async_fire(event, context=context)
+ else:
+ await hass.services.async_call("script", config["id"], context=context)
+
+
+def _assert_raw_config(domain, config, trace):
+ if domain == "script":
+ config = {"sequence": config["action"]}
+ assert trace["config"] == config
+
+
+async def _assert_contexts(client, next_id, contexts):
+ await client.send_json({"id": next_id(), "type": "trace/contexts"})
+ response = await client.receive_json()
+ assert response["success"]
+ assert response["result"] == contexts
+
+
+@pytest.mark.parametrize(
+ "domain, prefix, extra_trace_keys, trigger, context_key, condition_results",
+ [
+ (
+ "automation",
+ "action",
+ [
+ {"trigger/0"},
+ {"trigger/0", "condition/0"},
+ {"trigger/1", "condition/0"},
+ {"trigger/0", "condition/0"},
+ ],
+ [
+ "event 'test_event'",
+ "event 'test_event2'",
+ ],
+ "parent_id",
+ [True],
+ ),
+ ("script", "sequence", [set(), set()], [UNDEFINED, UNDEFINED], "id", []),
+ ],
+)
+async def test_get_trace(
+ hass,
+ hass_ws_client,
+ domain,
+ prefix,
+ extra_trace_keys,
+ trigger,
+ context_key,
+ condition_results,
+):
+ """Test tracing an script or automation."""
+ id = 1
+
+ def next_id():
+ nonlocal id
+ id += 1
+ return id
+
+ sun_config = {
+ "id": "sun",
+ "trigger": {"platform": "event", "event_type": "test_event"},
+ "action": {"service": "test.automation"},
+ }
+ moon_config = {
+ "id": "moon",
+ "trigger": [
+ {"platform": "event", "event_type": "test_event2"},
+ {"platform": "event", "event_type": "test_event3"},
+ ],
+ "condition": {
+ "condition": "template",
+ "value_template": "{{ trigger.event.event_type=='test_event2' }}",
+ },
+ "action": {"event": "another_event"},
+ }
+
+ sun_action = {
+ "limit": 10,
+ "params": {
+ "domain": "test",
+ "service": "automation",
+ "service_data": {},
+ "target": {},
+ },
+ "running_script": False,
+ }
+ moon_action = {"event": "another_event", "event_data": {}}
+
+ await _setup_automation_or_script(hass, domain, [sun_config, moon_config])
+
+ client = await hass_ws_client()
+ contexts = {}
+
+ # Trigger "sun" automation / run "sun" script
+ context = Context()
+ await _run_automation_or_script(hass, domain, sun_config, "test_event", context)
+ await hass.async_block_till_done()
+
+ # List traces
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
+ response = await client.receive_json()
+ assert response["success"]
+ run_id = _find_run_id(response["result"], domain, "sun")
+
+ # Get trace
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/get",
+ "domain": domain,
+ "item_id": "sun",
+ "run_id": run_id,
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ trace = response["result"]
+ assert set(trace["trace"]) == {f"{prefix}/0"} | extra_trace_keys[0]
+ assert len(trace["trace"][f"{prefix}/0"]) == 1
+ assert trace["trace"][f"{prefix}/0"][0]["error"]
+ assert trace["trace"][f"{prefix}/0"][0]["result"] == sun_action
+ _assert_raw_config(domain, sun_config, trace)
+ assert trace["blueprint_inputs"] is None
+ assert trace["context"]
+ assert trace["error"] == "Unable to find service test.automation"
+ assert trace["state"] == "stopped"
+ assert trace["script_execution"] == "error"
+ assert trace["item_id"] == "sun"
+ assert trace["context"][context_key] == context.id
+ assert trace.get("trigger", UNDEFINED) == trigger[0]
+ contexts[trace["context"]["id"]] = {
+ "run_id": trace["run_id"],
+ "domain": domain,
+ "item_id": trace["item_id"],
+ }
+
+ # Trigger "moon" automation, with passing condition / run "moon" script
+ await _run_automation_or_script(hass, domain, moon_config, "test_event2", context)
+ await hass.async_block_till_done()
+
+ # List traces
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
+ response = await client.receive_json()
+ assert response["success"]
+ run_id = _find_run_id(response["result"], domain, "moon")
+
+ # Get trace
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/get",
+ "domain": domain,
+ "item_id": "moon",
+ "run_id": run_id,
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ trace = response["result"]
+ assert set(trace["trace"]) == {f"{prefix}/0"} | extra_trace_keys[1]
+ assert len(trace["trace"][f"{prefix}/0"]) == 1
+ assert "error" not in trace["trace"][f"{prefix}/0"][0]
+ assert trace["trace"][f"{prefix}/0"][0]["result"] == moon_action
+ _assert_raw_config(domain, moon_config, trace)
+ assert trace["blueprint_inputs"] is None
+ assert trace["context"]
+ assert "error" not in trace
+ assert trace["state"] == "stopped"
+ assert trace["script_execution"] == "finished"
+ assert trace["item_id"] == "moon"
+
+ assert trace.get("trigger", UNDEFINED) == trigger[1]
+
+ assert len(trace["trace"].get("condition/0", [])) == len(condition_results)
+ for idx, condition_result in enumerate(condition_results):
+ assert trace["trace"]["condition/0"][idx]["result"] == {
+ "result": condition_result
+ }
+ contexts[trace["context"]["id"]] = {
+ "run_id": trace["run_id"],
+ "domain": domain,
+ "item_id": trace["item_id"],
+ }
+
+ if len(extra_trace_keys) <= 2:
+ # Check contexts
+ await _assert_contexts(client, next_id, contexts)
+ return
+
+ # Trigger "moon" automation with failing condition
+ hass.bus.async_fire("test_event3")
+ await hass.async_block_till_done()
+
+ # List traces
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
+ response = await client.receive_json()
+ assert response["success"]
+ run_id = _find_run_id(response["result"], "automation", "moon")
+
+ # Get trace
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/get",
+ "domain": domain,
+ "item_id": "moon",
+ "run_id": run_id,
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ trace = response["result"]
+ assert set(trace["trace"]) == extra_trace_keys[2]
+ assert len(trace["trace"]["condition/0"]) == 1
+ assert trace["trace"]["condition/0"][0]["result"] == {"result": False}
+ assert trace["config"] == moon_config
+ assert trace["context"]
+ assert "error" not in trace
+ assert trace["state"] == "stopped"
+ assert trace["script_execution"] == "failed_conditions"
+ assert trace["trigger"] == "event 'test_event3'"
+ assert trace["item_id"] == "moon"
+ contexts[trace["context"]["id"]] = {
+ "run_id": trace["run_id"],
+ "domain": domain,
+ "item_id": trace["item_id"],
+ }
+
+ # Trigger "moon" automation with passing condition
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+
+ # List traces
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
+ response = await client.receive_json()
+ assert response["success"]
+ run_id = _find_run_id(response["result"], "automation", "moon")
+
+ # Get trace
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/get",
+ "domain": domain,
+ "item_id": "moon",
+ "run_id": run_id,
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ trace = response["result"]
+ assert set(trace["trace"]) == {f"{prefix}/0"} | extra_trace_keys[3]
+ assert len(trace["trace"][f"{prefix}/0"]) == 1
+ assert "error" not in trace["trace"][f"{prefix}/0"][0]
+ assert trace["trace"][f"{prefix}/0"][0]["result"] == moon_action
+ assert len(trace["trace"]["condition/0"]) == 1
+ assert trace["trace"]["condition/0"][0]["result"] == {"result": True}
+ assert trace["config"] == moon_config
+ assert trace["context"]
+ assert "error" not in trace
+ assert trace["state"] == "stopped"
+ assert trace["script_execution"] == "finished"
+ assert trace["trigger"] == "event 'test_event2'"
+ assert trace["item_id"] == "moon"
+ contexts[trace["context"]["id"]] = {
+ "run_id": trace["run_id"],
+ "domain": domain,
+ "item_id": trace["item_id"],
+ }
+
+ # Check contexts
+ await _assert_contexts(client, next_id, contexts)
+
+
+@pytest.mark.parametrize("domain", ["automation", "script"])
+async def test_get_invalid_trace(hass, hass_ws_client, domain):
+ """Test getting a non-existing trace."""
+ assert await async_setup_component(hass, domain, {domain: {}})
+ client = await hass_ws_client()
+ await client.send_json(
+ {
+ "id": 1,
+ "type": "trace/get",
+ "domain": domain,
+ "item_id": "sun",
+ "run_id": "invalid",
+ }
+ )
+ response = await client.receive_json()
+ assert not response["success"]
+ assert response["error"]["code"] == "not_found"
+
+
+@pytest.mark.parametrize("domain", ["automation", "script"])
+async def test_trace_overflow(hass, hass_ws_client, domain):
+ """Test the number of stored traces per script or automation is limited."""
+ id = 1
+
+ def next_id():
+ nonlocal id
+ id += 1
+ return id
+
+ sun_config = {
+ "id": "sun",
+ "trigger": {"platform": "event", "event_type": "test_event"},
+ "action": {"event": "some_event"},
+ }
+ moon_config = {
+ "id": "moon",
+ "trigger": {"platform": "event", "event_type": "test_event2"},
+ "action": {"event": "another_event"},
+ }
+ await _setup_automation_or_script(hass, domain, [sun_config, moon_config])
+
+ client = await hass_ws_client()
+
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
+ response = await client.receive_json()
+ assert response["success"]
+ assert response["result"] == []
+
+ # Trigger "sun" and "moon" automation / script once
+ await _run_automation_or_script(hass, domain, sun_config, "test_event")
+ await _run_automation_or_script(hass, domain, moon_config, "test_event2")
+ await hass.async_block_till_done()
+
+ # List traces
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
+ response = await client.receive_json()
+ assert response["success"]
+ assert len(_find_traces(response["result"], domain, "moon")) == 1
+ moon_run_id = _find_run_id(response["result"], domain, "moon")
+ assert len(_find_traces(response["result"], domain, "sun")) == 1
+
+ # Trigger "moon" enough times to overflow the max number of stored traces
+ for _ in range(STORED_TRACES):
+ await _run_automation_or_script(hass, domain, moon_config, "test_event2")
+ await hass.async_block_till_done()
+
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
+ response = await client.receive_json()
+ assert response["success"]
+ moon_traces = _find_traces(response["result"], domain, "moon")
+ assert len(moon_traces) == STORED_TRACES
+ assert moon_traces[0]
+ assert int(moon_traces[0]["run_id"]) == int(moon_run_id) + 1
+ assert int(moon_traces[-1]["run_id"]) == int(moon_run_id) + STORED_TRACES
+ assert len(_find_traces(response["result"], domain, "sun")) == 1
+
+
+@pytest.mark.parametrize(
+ "domain, prefix, trigger, last_step, script_execution",
+ [
+ (
+ "automation",
+ "action",
+ [
+ "event 'test_event'",
+ "event 'test_event2'",
+ "event 'test_event3'",
+ "event 'test_event2'",
+ ],
+ ["{prefix}/0", "{prefix}/0", "condition/0", "{prefix}/0"],
+ ["error", "finished", "failed_conditions", "finished"],
+ ),
+ (
+ "script",
+ "sequence",
+ [UNDEFINED, UNDEFINED, UNDEFINED, UNDEFINED],
+ ["{prefix}/0", "{prefix}/0", "{prefix}/0", "{prefix}/0"],
+ ["error", "finished", "finished", "finished"],
+ ),
+ ],
+)
+async def test_list_traces(
+ hass, hass_ws_client, domain, prefix, trigger, last_step, script_execution
+):
+ """Test listing script and automation traces."""
+ id = 1
+
+ def next_id():
+ nonlocal id
+ id += 1
+ return id
+
+ sun_config = {
+ "id": "sun",
+ "trigger": {"platform": "event", "event_type": "test_event"},
+ "action": {"service": "test.automation"},
+ }
+ moon_config = {
+ "id": "moon",
+ "trigger": [
+ {"platform": "event", "event_type": "test_event2"},
+ {"platform": "event", "event_type": "test_event3"},
+ ],
+ "condition": {
+ "condition": "template",
+ "value_template": "{{ trigger.event.event_type=='test_event2' }}",
+ },
+ "action": {"event": "another_event"},
+ }
+ await _setup_automation_or_script(hass, domain, [sun_config, moon_config])
+
+ client = await hass_ws_client()
+
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
+ response = await client.receive_json()
+ assert response["success"]
+ assert response["result"] == []
+
+ await client.send_json(
+ {"id": next_id(), "type": "trace/list", "domain": domain, "item_id": "sun"}
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ assert response["result"] == []
+
+ # Trigger "sun" automation / run "sun" script
+ await _run_automation_or_script(hass, domain, sun_config, "test_event")
+ await hass.async_block_till_done()
+
+ # List traces
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
+ response = await client.receive_json()
+ assert response["success"]
+ assert len(response["result"]) == 1
+ assert len(_find_traces(response["result"], domain, "sun")) == 1
+
+ await client.send_json(
+ {"id": next_id(), "type": "trace/list", "domain": domain, "item_id": "sun"}
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ assert len(response["result"]) == 1
+ assert len(_find_traces(response["result"], domain, "sun")) == 1
+
+ await client.send_json(
+ {"id": next_id(), "type": "trace/list", "domain": domain, "item_id": "moon"}
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ assert response["result"] == []
+
+ # Trigger "moon" automation, with passing condition / run "moon" script
+ await _run_automation_or_script(hass, domain, moon_config, "test_event2")
+ await hass.async_block_till_done()
+
+ # Trigger "moon" automation, with failing condition / run "moon" script
+ await _run_automation_or_script(hass, domain, moon_config, "test_event3")
+ await hass.async_block_till_done()
+
+ # Trigger "moon" automation, with passing condition / run "moon" script
+ await _run_automation_or_script(hass, domain, moon_config, "test_event2")
+ await hass.async_block_till_done()
+
+ # List traces
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
+ response = await client.receive_json()
+ assert response["success"]
+ assert len(_find_traces(response["result"], domain, "moon")) == 3
+ assert len(_find_traces(response["result"], domain, "sun")) == 1
+ trace = _find_traces(response["result"], domain, "sun")[0]
+ assert trace["last_step"] == last_step[0].format(prefix=prefix)
+ assert trace["error"] == "Unable to find service test.automation"
+ assert trace["state"] == "stopped"
+ assert trace["script_execution"] == script_execution[0]
+ assert trace["timestamp"]
+ assert trace["item_id"] == "sun"
+ assert trace.get("trigger", UNDEFINED) == trigger[0]
+
+ trace = _find_traces(response["result"], domain, "moon")[0]
+ assert trace["last_step"] == last_step[1].format(prefix=prefix)
+ assert "error" not in trace
+ assert trace["state"] == "stopped"
+ assert trace["script_execution"] == script_execution[1]
+ assert trace["timestamp"]
+ assert trace["item_id"] == "moon"
+ assert trace.get("trigger", UNDEFINED) == trigger[1]
+
+ trace = _find_traces(response["result"], domain, "moon")[1]
+ assert trace["last_step"] == last_step[2].format(prefix=prefix)
+ assert "error" not in trace
+ assert trace["state"] == "stopped"
+ assert trace["script_execution"] == script_execution[2]
+ assert trace["timestamp"]
+ assert trace["item_id"] == "moon"
+ assert trace.get("trigger", UNDEFINED) == trigger[2]
+
+ trace = _find_traces(response["result"], domain, "moon")[2]
+ assert trace["last_step"] == last_step[3].format(prefix=prefix)
+ assert "error" not in trace
+ assert trace["state"] == "stopped"
+ assert trace["script_execution"] == script_execution[3]
+ assert trace["timestamp"]
+ assert trace["item_id"] == "moon"
+ assert trace.get("trigger", UNDEFINED) == trigger[3]
+
+
+@pytest.mark.parametrize(
+ "domain, prefix, extra_trace_keys",
+ [("automation", "action", {"trigger/0"}), ("script", "sequence", set())],
+)
+async def test_nested_traces(hass, hass_ws_client, domain, prefix, extra_trace_keys):
+ """Test nested automation and script traces."""
+ id = 1
+
+ def next_id():
+ nonlocal id
+ id += 1
+ return id
+
+ sun_config = {
+ "id": "sun",
+ "trigger": {"platform": "event", "event_type": "test_event"},
+ "action": {"service": "script.moon"},
+ }
+ moon_config = {"moon": {"sequence": {"event": "another_event"}}}
+ await _setup_automation_or_script(hass, domain, [sun_config], moon_config)
+
+ client = await hass_ws_client()
+
+ # Trigger "sun" automation / run "sun" script
+ await _run_automation_or_script(hass, domain, sun_config, "test_event")
+ await hass.async_block_till_done()
+
+ # List traces
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"})
+ response = await client.receive_json()
+ assert response["success"]
+ assert len(_find_traces(response["result"], "script", "moon")) == 1
+ moon_run_id = _find_run_id(response["result"], "script", "moon")
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
+ response = await client.receive_json()
+ assert response["success"]
+ assert len(_find_traces(response["result"], domain, "sun")) == 1
+ sun_run_id = _find_run_id(response["result"], domain, "sun")
+ assert sun_run_id != moon_run_id
+
+ # Get trace
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/get",
+ "domain": domain,
+ "item_id": "sun",
+ "run_id": sun_run_id,
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ trace = response["result"]
+ assert set(trace["trace"]) == {f"{prefix}/0"} | extra_trace_keys
+ assert len(trace["trace"][f"{prefix}/0"]) == 1
+ child_id = trace["trace"][f"{prefix}/0"][0]["child_id"]
+ assert child_id == {"domain": "script", "item_id": "moon", "run_id": moon_run_id}
+
+
+@pytest.mark.parametrize(
+ "domain, prefix", [("automation", "action"), ("script", "sequence")]
+)
+async def test_breakpoints(hass, hass_ws_client, domain, prefix):
+ """Test script and automation breakpoints."""
+ id = 1
+
+ def next_id():
+ nonlocal id
+ id += 1
+ return id
+
+ async def assert_last_step(item_id, expected_action, expected_state):
+ await client.send_json(
+ {"id": next_id(), "type": "trace/list", "domain": domain}
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ trace = _find_traces(response["result"], domain, item_id)[-1]
+ assert trace["last_step"] == expected_action
+ assert trace["state"] == expected_state
+ return trace["run_id"]
+
+ sun_config = {
+ "id": "sun",
+ "trigger": {"platform": "event", "event_type": "test_event"},
+ "action": [
+ {"event": "event0"},
+ {"event": "event1"},
+ {"event": "event2"},
+ {"event": "event3"},
+ {"event": "event4"},
+ {"event": "event5"},
+ {"event": "event6"},
+ {"event": "event7"},
+ {"event": "event8"},
+ ],
+ }
+ await _setup_automation_or_script(hass, domain, [sun_config])
+
+ client = await hass_ws_client()
+
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/debug/breakpoint/set",
+ "domain": domain,
+ "item_id": "sun",
+ "node": "1",
+ }
+ )
+ response = await client.receive_json()
+ assert not response["success"]
+
+ await client.send_json({"id": next_id(), "type": "trace/debug/breakpoint/list"})
+ response = await client.receive_json()
+ assert response["success"]
+ assert response["result"] == []
+
+ subscription_id = next_id()
+ await client.send_json(
+ {"id": subscription_id, "type": "trace/debug/breakpoint/subscribe"}
+ )
+ response = await client.receive_json()
+ assert response["success"]
+
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/debug/breakpoint/set",
+ "domain": domain,
+ "item_id": "sun",
+ "node": f"{prefix}/1",
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/debug/breakpoint/set",
+ "domain": domain,
+ "item_id": "sun",
+ "node": f"{prefix}/5",
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+
+ await client.send_json({"id": next_id(), "type": "trace/debug/breakpoint/list"})
+ response = await client.receive_json()
+ assert response["success"]
+ assert_lists_same(
+ response["result"],
+ [
+ {"node": f"{prefix}/1", "run_id": "*", "domain": domain, "item_id": "sun"},
+ {"node": f"{prefix}/5", "run_id": "*", "domain": domain, "item_id": "sun"},
+ ],
+ )
+
+ # Trigger "sun" automation / run "sun" script
+ await _run_automation_or_script(hass, domain, sun_config, "test_event")
+
+ response = await client.receive_json()
+ run_id = await assert_last_step("sun", f"{prefix}/1", "running")
+ assert response["event"] == {
+ "domain": domain,
+ "item_id": "sun",
+ "node": f"{prefix}/1",
+ "run_id": run_id,
+ }
+
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/debug/step",
+ "domain": domain,
+ "item_id": "sun",
+ "run_id": run_id,
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+
+ response = await client.receive_json()
+ run_id = await assert_last_step("sun", f"{prefix}/2", "running")
+ assert response["event"] == {
+ "domain": domain,
+ "item_id": "sun",
+ "node": f"{prefix}/2",
+ "run_id": run_id,
+ }
+
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/debug/continue",
+ "domain": domain,
+ "item_id": "sun",
+ "run_id": run_id,
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+
+ response = await client.receive_json()
+ run_id = await assert_last_step("sun", f"{prefix}/5", "running")
+ assert response["event"] == {
+ "domain": domain,
+ "item_id": "sun",
+ "node": f"{prefix}/5",
+ "run_id": run_id,
+ }
+
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/debug/stop",
+ "domain": domain,
+ "item_id": "sun",
+ "run_id": run_id,
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ await hass.async_block_till_done()
+ await assert_last_step("sun", f"{prefix}/5", "stopped")
+
+
+@pytest.mark.parametrize(
+ "domain, prefix", [("automation", "action"), ("script", "sequence")]
+)
+async def test_breakpoints_2(hass, hass_ws_client, domain, prefix):
+ """Test execution resumes and breakpoints are removed after subscription removed."""
+ id = 1
+
+ def next_id():
+ nonlocal id
+ id += 1
+ return id
+
+ async def assert_last_step(item_id, expected_action, expected_state):
+ await client.send_json(
+ {"id": next_id(), "type": "trace/list", "domain": domain}
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ trace = _find_traces(response["result"], domain, item_id)[-1]
+ assert trace["last_step"] == expected_action
+ assert trace["state"] == expected_state
+ return trace["run_id"]
+
+ sun_config = {
+ "id": "sun",
+ "trigger": {"platform": "event", "event_type": "test_event"},
+ "action": [
+ {"event": "event0"},
+ {"event": "event1"},
+ {"event": "event2"},
+ {"event": "event3"},
+ {"event": "event4"},
+ {"event": "event5"},
+ {"event": "event6"},
+ {"event": "event7"},
+ {"event": "event8"},
+ ],
+ }
+ await _setup_automation_or_script(hass, domain, [sun_config])
+
+ client = await hass_ws_client()
+
+ subscription_id = next_id()
+ await client.send_json(
+ {"id": subscription_id, "type": "trace/debug/breakpoint/subscribe"}
+ )
+ response = await client.receive_json()
+ assert response["success"]
+
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/debug/breakpoint/set",
+ "domain": domain,
+ "item_id": "sun",
+ "node": f"{prefix}/1",
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+
+ # Trigger "sun" automation / run "sun" script
+ await _run_automation_or_script(hass, domain, sun_config, "test_event")
+
+ response = await client.receive_json()
+ run_id = await assert_last_step("sun", f"{prefix}/1", "running")
+ assert response["event"] == {
+ "domain": domain,
+ "item_id": "sun",
+ "node": f"{prefix}/1",
+ "run_id": run_id,
+ }
+
+ # Unsubscribe - execution should resume
+ await client.send_json(
+ {"id": next_id(), "type": "unsubscribe_events", "subscription": subscription_id}
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ await hass.async_block_till_done()
+ await assert_last_step("sun", f"{prefix}/8", "stopped")
+
+ # Should not be possible to set breakpoints
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/debug/breakpoint/set",
+ "domain": domain,
+ "item_id": "sun",
+ "node": "1",
+ }
+ )
+ response = await client.receive_json()
+ assert not response["success"]
+
+ # Trigger "sun" automation / script, should finish without stopping on breakpoints
+ await _run_automation_or_script(hass, domain, sun_config, "test_event")
+ await hass.async_block_till_done()
+
+ new_run_id = await assert_last_step("sun", f"{prefix}/8", "stopped")
+ assert new_run_id != run_id
+
+
+@pytest.mark.parametrize(
+ "domain, prefix", [("automation", "action"), ("script", "sequence")]
+)
+async def test_breakpoints_3(hass, hass_ws_client, domain, prefix):
+ """Test breakpoints can be cleared."""
+ id = 1
+
+ def next_id():
+ nonlocal id
+ id += 1
+ return id
+
+ async def assert_last_step(item_id, expected_action, expected_state):
+ await client.send_json(
+ {"id": next_id(), "type": "trace/list", "domain": domain}
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ trace = _find_traces(response["result"], domain, item_id)[-1]
+ assert trace["last_step"] == expected_action
+ assert trace["state"] == expected_state
+ return trace["run_id"]
+
+ sun_config = {
+ "id": "sun",
+ "trigger": {"platform": "event", "event_type": "test_event"},
+ "action": [
+ {"event": "event0"},
+ {"event": "event1"},
+ {"event": "event2"},
+ {"event": "event3"},
+ {"event": "event4"},
+ {"event": "event5"},
+ {"event": "event6"},
+ {"event": "event7"},
+ {"event": "event8"},
+ ],
+ }
+ await _setup_automation_or_script(hass, domain, [sun_config])
+
+ client = await hass_ws_client()
+
+ subscription_id = next_id()
+ await client.send_json(
+ {"id": subscription_id, "type": "trace/debug/breakpoint/subscribe"}
+ )
+ response = await client.receive_json()
+ assert response["success"]
+
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/debug/breakpoint/set",
+ "domain": domain,
+ "item_id": "sun",
+ "node": f"{prefix}/1",
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/debug/breakpoint/set",
+ "domain": domain,
+ "item_id": "sun",
+ "node": f"{prefix}/5",
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+
+ # Trigger "sun" automation / run "sun" script
+ await _run_automation_or_script(hass, domain, sun_config, "test_event")
+
+ response = await client.receive_json()
+ run_id = await assert_last_step("sun", f"{prefix}/1", "running")
+ assert response["event"] == {
+ "domain": domain,
+ "item_id": "sun",
+ "node": f"{prefix}/1",
+ "run_id": run_id,
+ }
+
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/debug/continue",
+ "domain": domain,
+ "item_id": "sun",
+ "run_id": run_id,
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+
+ response = await client.receive_json()
+ run_id = await assert_last_step("sun", f"{prefix}/5", "running")
+ assert response["event"] == {
+ "domain": domain,
+ "item_id": "sun",
+ "node": f"{prefix}/5",
+ "run_id": run_id,
+ }
+
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/debug/stop",
+ "domain": domain,
+ "item_id": "sun",
+ "run_id": run_id,
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ await hass.async_block_till_done()
+ await assert_last_step("sun", f"{prefix}/5", "stopped")
+
+ # Clear 1st breakpoint
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/debug/breakpoint/clear",
+ "domain": domain,
+ "item_id": "sun",
+ "node": f"{prefix}/1",
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+
+ # Trigger "sun" automation / run "sun" script
+ await _run_automation_or_script(hass, domain, sun_config, "test_event")
+
+ response = await client.receive_json()
+ run_id = await assert_last_step("sun", f"{prefix}/5", "running")
+ assert response["event"] == {
+ "domain": domain,
+ "item_id": "sun",
+ "node": f"{prefix}/5",
+ "run_id": run_id,
+ }
+
+
+@pytest.mark.parametrize(
+ "script_mode,max_runs,script_execution",
+ [
+ ({"mode": "single"}, 1, "failed_single"),
+ ({"mode": "parallel", "max": 2}, 2, "failed_max_runs"),
+ ],
+)
+async def test_script_mode(
+ hass, hass_ws_client, script_mode, max_runs, script_execution
+):
+ """Test overlapping runs with max_runs > 1."""
+ id = 1
+
+ def next_id():
+ nonlocal id
+ id += 1
+ return id
+
+ flag = asyncio.Event()
+
+ @callback
+ def _handle_event(_):
+ flag.set()
+
+ event = "test_event"
+ script_config = {
+ "script1": {
+ "sequence": [
+ {"event": event, "event_data": {"value": 1}},
+ {"wait_template": "{{ states.switch.test.state == 'off' }}"},
+ {"event": event, "event_data": {"value": 2}},
+ ],
+ **script_mode,
+ },
+ }
+ client = await hass_ws_client()
+ hass.bus.async_listen(event, _handle_event)
+ assert await async_setup_component(hass, "script", {"script": script_config})
+
+ for _ in range(max_runs):
+ hass.states.async_set("switch.test", "on")
+ await hass.services.async_call("script", "script1")
+ await asyncio.wait_for(flag.wait(), 1)
+
+ # List traces
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"})
+ response = await client.receive_json()
+ assert response["success"]
+ traces = _find_traces(response["result"], "script", "script1")
+ assert len(traces) == max_runs
+ for trace in traces:
+ assert trace["state"] == "running"
+
+ # Start additional run of script while first runs are suspended in wait_template.
+
+ flag.clear()
+ await hass.services.async_call("script", "script1")
+
+ # List traces
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"})
+ response = await client.receive_json()
+ assert response["success"]
+ traces = _find_traces(response["result"], "script", "script1")
+ assert len(traces) == max_runs + 1
+ assert traces[-1]["state"] == "stopped"
+ assert traces[-1]["script_execution"] == script_execution
+
+
+@pytest.mark.parametrize(
+ "script_mode,script_execution",
+ [("restart", "cancelled"), ("parallel", "finished")],
+)
+async def test_script_mode_2(hass, hass_ws_client, script_mode, script_execution):
+ """Test overlapping runs with max_runs > 1."""
+ id = 1
+
+ def next_id():
+ nonlocal id
+ id += 1
+ return id
+
+ flag = asyncio.Event()
+
+ @callback
+ def _handle_event(_):
+ flag.set()
+
+ event = "test_event"
+ script_config = {
+ "script1": {
+ "sequence": [
+ {"event": event, "event_data": {"value": 1}},
+ {"wait_template": "{{ states.switch.test.state == 'off' }}"},
+ {"event": event, "event_data": {"value": 2}},
+ ],
+ "mode": script_mode,
+ }
+ }
+ client = await hass_ws_client()
+ hass.bus.async_listen(event, _handle_event)
+ assert await async_setup_component(hass, "script", {"script": script_config})
+
+ hass.states.async_set("switch.test", "on")
+ await hass.services.async_call("script", "script1")
+ await asyncio.wait_for(flag.wait(), 1)
+
+ # List traces
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"})
+ response = await client.receive_json()
+ assert response["success"]
+ trace = _find_traces(response["result"], "script", "script1")[0]
+ assert trace["state"] == "running"
+
+ # Start second run of script while first run is suspended in wait_template.
+
+ flag.clear()
+ await hass.services.async_call("script", "script1")
+ await asyncio.wait_for(flag.wait(), 1)
+
+ # List traces
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"})
+ response = await client.receive_json()
+ assert response["success"]
+ trace = _find_traces(response["result"], "script", "script1")[1]
+ assert trace["state"] == "running"
+
+ # Let both scripts finish
+ hass.states.async_set("switch.test", "off")
+ await hass.async_block_till_done()
+
+ # List traces
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"})
+ response = await client.receive_json()
+ assert response["success"]
+ trace = _find_traces(response["result"], "script", "script1")[0]
+ assert trace["state"] == "stopped"
+ assert trace["script_execution"] == script_execution
+ trace = _find_traces(response["result"], "script", "script1")[1]
+ assert trace["state"] == "stopped"
+ assert trace["script_execution"] == "finished"
+
+
+async def test_trace_blueprint_automation(hass, hass_ws_client):
+ """Test trace of blueprint automation."""
+ id = 1
+
+ def next_id():
+ nonlocal id
+ id += 1
+ return id
+
+ domain = "automation"
+ sun_config = {
+ "id": "sun",
+ "use_blueprint": {
+ "path": "test_event_service.yaml",
+ "input": {
+ "trigger_event": "blueprint_event",
+ "service_to_call": "test.automation",
+ },
+ },
+ }
+ sun_action = {
+ "limit": 10,
+ "params": {
+ "domain": "test",
+ "service": "automation",
+ "service_data": {},
+ "target": {"entity_id": ["light.kitchen"]},
+ },
+ "running_script": False,
+ }
+ assert await async_setup_component(hass, "automation", {"automation": sun_config})
+ client = await hass_ws_client()
+ hass.bus.async_fire("blueprint_event")
+ await hass.async_block_till_done()
+
+ # List traces
+ await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain})
+ response = await client.receive_json()
+ assert response["success"]
+ run_id = _find_run_id(response["result"], domain, "sun")
+
+ # Get trace
+ await client.send_json(
+ {
+ "id": next_id(),
+ "type": "trace/get",
+ "domain": domain,
+ "item_id": "sun",
+ "run_id": run_id,
+ }
+ )
+ response = await client.receive_json()
+ assert response["success"]
+ trace = response["result"]
+ assert set(trace["trace"]) == {"trigger/0", "action/0"}
+ assert len(trace["trace"]["action/0"]) == 1
+ assert trace["trace"]["action/0"][0]["error"]
+ assert trace["trace"]["action/0"][0]["result"] == sun_action
+ assert trace["config"]["id"] == "sun"
+ assert trace["blueprint_inputs"] == sun_config
+ assert trace["context"]
+ assert trace["error"] == "Unable to find service test.automation"
+ assert trace["state"] == "stopped"
+ assert trace["script_execution"] == "error"
+ assert trace["item_id"] == "sun"
+ assert trace.get("trigger", UNDEFINED) == "event 'blueprint_event'"
diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py
index 93675a9e4d1cf5..54a8625f23c139 100644
--- a/tests/components/tradfri/conftest.py
+++ b/tests/components/tradfri/conftest.py
@@ -5,7 +5,7 @@
from . import MOCK_GATEWAY_ID
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
# pylint: disable=protected-access
diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py
index 0983b5aa22fc15..da9ae9da146c20 100644
--- a/tests/components/tradfri/test_init.py
+++ b/tests/components/tradfri/test_init.py
@@ -2,10 +2,7 @@
from unittest.mock import patch
from homeassistant.components import tradfri
-from homeassistant.helpers.device_registry import (
- async_entries_for_config_entry,
- async_get_registry as async_get_device_registry,
-)
+from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
@@ -99,8 +96,8 @@ async def test_entry_setup_unload(hass, api_factory, gateway_id):
await hass.async_block_till_done()
assert setup.call_count == len(tradfri.PLATFORMS)
- dev_reg = await async_get_device_registry(hass)
- dev_entries = async_entries_for_config_entry(dev_reg, entry.entry_id)
+ dev_reg = dr.async_get(hass)
+ dev_entries = dr.async_entries_for_config_entry(dev_reg, entry.entry_id)
assert dev_entries
dev_entry = dev_entries[0]
diff --git a/tests/components/tuya/common.py b/tests/components/tuya/common.py
new file mode 100644
index 00000000000000..8dcef136b7f4c3
--- /dev/null
+++ b/tests/components/tuya/common.py
@@ -0,0 +1,75 @@
+"""Test code shared between test files."""
+
+from tuyaha.devices import climate, light, switch
+
+CLIMATE_ID = "1"
+CLIMATE_DATA = {
+ "data": {"state": "true", "temp_unit": climate.UNIT_CELSIUS},
+ "id": CLIMATE_ID,
+ "ha_type": "climate",
+ "name": "TestClimate",
+ "dev_type": "climate",
+}
+
+LIGHT_ID = "2"
+LIGHT_DATA = {
+ "data": {"state": "true"},
+ "id": LIGHT_ID,
+ "ha_type": "light",
+ "name": "TestLight",
+ "dev_type": "light",
+}
+
+SWITCH_ID = "3"
+SWITCH_DATA = {
+ "data": {"state": True},
+ "id": SWITCH_ID,
+ "ha_type": "switch",
+ "name": "TestSwitch",
+ "dev_type": "switch",
+}
+
+LIGHT_ID_FAKE1 = "9998"
+LIGHT_DATA_FAKE1 = {
+ "data": {"state": "true"},
+ "id": LIGHT_ID_FAKE1,
+ "ha_type": "light",
+ "name": "TestLightFake1",
+ "dev_type": "light",
+}
+
+LIGHT_ID_FAKE2 = "9999"
+LIGHT_DATA_FAKE2 = {
+ "data": {"state": "true"},
+ "id": LIGHT_ID_FAKE2,
+ "ha_type": "light",
+ "name": "TestLightFake2",
+ "dev_type": "light",
+}
+
+TUYA_DEVICES = [
+ climate.TuyaClimate(CLIMATE_DATA, None),
+ light.TuyaLight(LIGHT_DATA, None),
+ switch.TuyaSwitch(SWITCH_DATA, None),
+ light.TuyaLight(LIGHT_DATA_FAKE1, None),
+ light.TuyaLight(LIGHT_DATA_FAKE2, None),
+]
+
+
+class MockTuya:
+ """Mock for Tuya devices."""
+
+ def get_all_devices(self):
+ """Return all configured devices."""
+ return TUYA_DEVICES
+
+ def get_device_by_id(self, dev_id):
+ """Return configured device with dev id."""
+ if dev_id == LIGHT_ID_FAKE1:
+ return None
+ if dev_id == LIGHT_ID_FAKE2:
+ return switch.TuyaSwitch(SWITCH_DATA, None)
+ for device in TUYA_DEVICES:
+ if device.object_id() == dev_id:
+ return device
+ return None
diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py
index 0055b451e1a7a3..ede6e5ac1db36c 100644
--- a/tests/components/tuya/test_config_flow.py
+++ b/tests/components/tuya/test_config_flow.py
@@ -2,11 +2,47 @@
from unittest.mock import Mock, patch
import pytest
+from tuyaha.devices.climate import STEP_HALVES
from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException
-from homeassistant import config_entries, data_entry_flow, setup
-from homeassistant.components.tuya.const import CONF_COUNTRYCODE, DOMAIN
-from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.components.tuya.config_flow import (
+ CONF_LIST_DEVICES,
+ ERROR_DEV_MULTI_TYPE,
+ ERROR_DEV_NOT_CONFIG,
+ ERROR_DEV_NOT_FOUND,
+ RESULT_AUTH_FAILED,
+ RESULT_CONN_ERROR,
+ RESULT_SINGLE_INSTANCE,
+)
+from homeassistant.components.tuya.const import (
+ CONF_BRIGHTNESS_RANGE_MODE,
+ CONF_COUNTRYCODE,
+ CONF_CURR_TEMP_DIVIDER,
+ CONF_DISCOVERY_INTERVAL,
+ CONF_MAX_KELVIN,
+ CONF_MAX_TEMP,
+ CONF_MIN_KELVIN,
+ CONF_MIN_TEMP,
+ CONF_QUERY_DEVICE,
+ CONF_QUERY_INTERVAL,
+ CONF_SET_TEMP_DIVIDED,
+ CONF_SUPPORT_COLOR,
+ CONF_TEMP_DIVIDER,
+ CONF_TEMP_STEP_OVERRIDE,
+ CONF_TUYA_MAX_COLTEMP,
+ DOMAIN,
+ TUYA_DATA,
+)
+from homeassistant.const import (
+ CONF_PASSWORD,
+ CONF_PLATFORM,
+ CONF_UNIT_OF_MEASUREMENT,
+ CONF_USERNAME,
+ TEMP_CELSIUS,
+)
+
+from .common import CLIMATE_ID, LIGHT_ID, LIGHT_ID_FAKE1, LIGHT_ID_FAKE2, MockTuya
from tests.common import MockConfigEntry
@@ -30,9 +66,15 @@ def tuya_fixture() -> Mock:
yield tuya
+@pytest.fixture(name="tuya_setup", autouse=True)
+def tuya_setup_fixture():
+ """Mock tuya entry setup."""
+ with patch("homeassistant.components.tuya.async_setup_entry", return_value=True):
+ yield
+
+
async def test_user(hass, tuya):
"""Test user config."""
- await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
@@ -40,15 +82,10 @@ async def test_user(hass, tuya):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
- with patch(
- "homeassistant.components.tuya.async_setup", return_value=True
- ) as mock_setup, patch(
- "homeassistant.components.tuya.async_setup_entry", return_value=True
- ) as mock_setup_entry:
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"], user_input=TUYA_USER_DATA
- )
- await hass.async_block_till_done()
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input=TUYA_USER_DATA
+ )
+ await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == USERNAME
@@ -58,26 +95,15 @@ async def test_user(hass, tuya):
assert result["data"][CONF_PLATFORM] == TUYA_PLATFORM
assert not result["result"].unique_id
- assert len(mock_setup.mock_calls) == 1
- assert len(mock_setup_entry.mock_calls) == 1
-
async def test_import(hass, tuya):
"""Test import step."""
- await setup.async_setup_component(hass, "persistent_notification", {})
- with patch(
- "homeassistant.components.tuya.async_setup",
- return_value=True,
- ) as mock_setup, patch(
- "homeassistant.components.tuya.async_setup_entry",
- return_value=True,
- ) as mock_setup_entry:
- result = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": config_entries.SOURCE_IMPORT},
- data=TUYA_USER_DATA,
- )
- await hass.async_block_till_done()
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data=TUYA_USER_DATA,
+ )
+ await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == USERNAME
@@ -87,9 +113,6 @@ async def test_import(hass, tuya):
assert result["data"][CONF_PLATFORM] == TUYA_PLATFORM
assert not result["result"].unique_id
- assert len(mock_setup.mock_calls) == 1
- assert len(mock_setup_entry.mock_calls) == 1
-
async def test_abort_if_already_setup(hass, tuya):
"""Test we abort if Tuya is already setup."""
@@ -101,7 +124,7 @@ async def test_abort_if_already_setup(hass, tuya):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "single_instance_allowed"
+ assert result["reason"] == RESULT_SINGLE_INSTANCE
# Should fail, config exist (flow)
result = await hass.config_entries.flow.async_init(
@@ -109,7 +132,7 @@ async def test_abort_if_already_setup(hass, tuya):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "single_instance_allowed"
+ assert result["reason"] == RESULT_SINGLE_INSTANCE
async def test_abort_on_invalid_credentials(hass, tuya):
@@ -121,14 +144,14 @@ async def test_abort_on_invalid_credentials(hass, tuya):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {"base": "invalid_auth"}
+ assert result["errors"] == {"base": RESULT_AUTH_FAILED}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "invalid_auth"
+ assert result["reason"] == RESULT_AUTH_FAILED
async def test_abort_on_connection_error(hass, tuya):
@@ -140,11 +163,152 @@ async def test_abort_on_connection_error(hass, tuya):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "cannot_connect"
+ assert result["reason"] == RESULT_CONN_ERROR
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "cannot_connect"
+ assert result["reason"] == RESULT_CONN_ERROR
+
+
+async def test_options_flow(hass):
+ """Test config flow options."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data=TUYA_USER_DATA,
+ )
+ config_entry.add_to_hass(hass)
+
+ # Set up the integration to make sure the config flow module is loaded.
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ # Unload the integration to prepare for the test.
+ with patch("homeassistant.components.tuya.async_unload_entry", return_value=True):
+ assert await hass.config_entries.async_unload(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ # Test check for integration not loaded
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == RESULT_CONN_ERROR
+
+ # Load integration and enter options
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ hass.data[DOMAIN] = {TUYA_DATA: MockTuya()}
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ # Test dev not found error
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={CONF_LIST_DEVICES: [f"light-{LIGHT_ID_FAKE1}"]},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+ assert result["errors"] == {"base": ERROR_DEV_NOT_FOUND}
+
+ # Test dev type error
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={CONF_LIST_DEVICES: [f"light-{LIGHT_ID_FAKE2}"]},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+ assert result["errors"] == {"base": ERROR_DEV_NOT_CONFIG}
+
+ # Test multi dev error
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={CONF_LIST_DEVICES: [f"climate-{CLIMATE_ID}", f"light-{LIGHT_ID}"]},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+ assert result["errors"] == {"base": ERROR_DEV_MULTI_TYPE}
+
+ # Test climate options form
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={CONF_LIST_DEVICES: [f"climate-{CLIMATE_ID}"]}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "device"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
+ CONF_TEMP_DIVIDER: 10,
+ CONF_CURR_TEMP_DIVIDER: 5,
+ CONF_SET_TEMP_DIVIDED: False,
+ CONF_TEMP_STEP_OVERRIDE: STEP_HALVES,
+ CONF_MIN_TEMP: 12,
+ CONF_MAX_TEMP: 22,
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ # Test light options form
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={CONF_LIST_DEVICES: [f"light-{LIGHT_ID}"]}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "device"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_SUPPORT_COLOR: True,
+ CONF_BRIGHTNESS_RANGE_MODE: 1,
+ CONF_MIN_KELVIN: 4000,
+ CONF_MAX_KELVIN: 5000,
+ CONF_TUYA_MAX_COLTEMP: 12000,
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ # Test common options
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_DISCOVERY_INTERVAL: 100,
+ CONF_QUERY_INTERVAL: 50,
+ CONF_QUERY_DEVICE: LIGHT_ID,
+ },
+ )
+
+ # Verify results
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ climate_options = config_entry.options[CLIMATE_ID]
+ assert climate_options[CONF_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
+ assert climate_options[CONF_TEMP_DIVIDER] == 10
+ assert climate_options[CONF_CURR_TEMP_DIVIDER] == 5
+ assert climate_options[CONF_SET_TEMP_DIVIDED] is False
+ assert climate_options[CONF_TEMP_STEP_OVERRIDE] == STEP_HALVES
+ assert climate_options[CONF_MIN_TEMP] == 12
+ assert climate_options[CONF_MAX_TEMP] == 22
+
+ light_options = config_entry.options[LIGHT_ID]
+ assert light_options[CONF_SUPPORT_COLOR] is True
+ assert light_options[CONF_BRIGHTNESS_RANGE_MODE] == 1
+ assert light_options[CONF_MIN_KELVIN] == 4000
+ assert light_options[CONF_MAX_KELVIN] == 5000
+ assert light_options[CONF_TUYA_MAX_COLTEMP] == 12000
+
+ assert config_entry.options[CONF_DISCOVERY_INTERVAL] == 100
+ assert config_entry.options[CONF_QUERY_INTERVAL] == 50
+ assert config_entry.options[CONF_QUERY_DEVICE] == LIGHT_ID
diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py
index ec3edf73f29d0c..27fd86d0868f2e 100644
--- a/tests/components/twentemilieu/test_config_flow.py
+++ b/tests/components/twentemilieu/test_config_flow.py
@@ -10,8 +10,10 @@
DOMAIN,
)
from homeassistant.const import CONF_ID, CONTENT_TYPE_JSON
+from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
+from tests.test_util.aiohttp import AiohttpClientMocker
FIXTURE_USER_INPUT = {
CONF_ID: "12345",
@@ -21,7 +23,7 @@
}
-async def test_show_set_form(hass):
+async def test_show_set_form(hass: HomeAssistant) -> None:
"""Test that the setup form is served."""
flow = config_flow.TwenteMilieuFlowHandler()
flow.hass = hass
@@ -31,7 +33,9 @@ async def test_show_set_form(hass):
assert result["step_id"] == "user"
-async def test_connection_error(hass, aioclient_mock):
+async def test_connection_error(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
"""Test we show user form on Twente Milieu connection error."""
aioclient_mock.post(
"https://twentemilieuapi.ximmio.com/api/FetchAdress", exc=aiohttp.ClientError
@@ -46,7 +50,9 @@ async def test_connection_error(hass, aioclient_mock):
assert result["errors"] == {"base": "cannot_connect"}
-async def test_invalid_address(hass, aioclient_mock):
+async def test_invalid_address(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
"""Test we show user form on Twente Milieu invalid address error."""
aioclient_mock.post(
"https://twentemilieuapi.ximmio.com/api/FetchAdress",
@@ -63,7 +69,9 @@ async def test_invalid_address(hass, aioclient_mock):
assert result["errors"] == {"base": "invalid_address"}
-async def test_address_already_set_up(hass, aioclient_mock):
+async def test_address_already_set_up(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
"""Test we abort if address has already been set up."""
MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT, title="12345").add_to_hass(
hass
@@ -83,7 +91,9 @@ async def test_address_already_set_up(hass, aioclient_mock):
assert result["reason"] == "already_configured"
-async def test_full_flow_implementation(hass, aioclient_mock):
+async def test_full_flow_implementation(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
"""Test registering an integration and finishing flow works."""
aioclient_mock.post(
"https://twentemilieuapi.ximmio.com/api/FetchAdress",
diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py
index ee7f072a65c7c5..580e5f83ebf85c 100644
--- a/tests/components/twilio/test_init.py
+++ b/tests/components/twilio/test_init.py
@@ -1,17 +1,19 @@
"""Test the init file of Twilio."""
-from unittest.mock import patch
-
from homeassistant import data_entry_flow
from homeassistant.components import twilio
+from homeassistant.config import async_process_ha_core_config
from homeassistant.core import callback
async def test_config_flow_registers_webhook(hass, aiohttp_client):
"""Test setting up Twilio and sending webhook."""
- with patch("homeassistant.util.get_local_ip", return_value="example.com"):
- result = await hass.config_entries.flow.async_init(
- "twilio", context={"source": "user"}
- )
+ await async_process_ha_core_config(
+ hass,
+ {"internal_url": "http://example.local:8123"},
+ )
+ result = await hass.config_entries.flow.async_init(
+ "twilio", context={"source": "user"}
+ )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
diff --git a/tests/components/twinkly/test_twinkly.py b/tests/components/twinkly/test_twinkly.py
index c8158354195bb4..fcbbdb035c7a67 100644
--- a/tests/components/twinkly/test_twinkly.py
+++ b/tests/components/twinkly/test_twinkly.py
@@ -1,6 +1,6 @@
"""Tests for the integration of a twinly device."""
+from __future__ import annotations
-from typing import Tuple
from unittest.mock import patch
from homeassistant.components.twinkly.const import (
@@ -12,6 +12,7 @@
)
from homeassistant.components.twinkly.light import TwinklyLight
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.entity_registry import RegistryEntry
@@ -190,7 +191,7 @@ async def test_unload(hass: HomeAssistant):
async def _create_entries(
hass: HomeAssistant, client=None
-) -> Tuple[RegistryEntry, DeviceEntry, ClientMock]:
+) -> tuple[RegistryEntry, DeviceEntry, ClientMock]:
client = ClientMock() if client is None else client
def get_client_mock(client, _):
@@ -211,8 +212,8 @@ def get_client_mock(client, _):
assert await hass.config_entries.async_setup(client.id)
await hass.async_block_till_done()
- device_registry = await hass.helpers.device_registry.async_get_registry()
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
+ entity_registry = er.async_get(hass)
entity_id = entity_registry.async_get_entity_id("light", TWINKLY_DOMAIN, client.id)
entity = entity_registry.async_get(entity_id)
diff --git a/tests/components/uk_transport/test_sensor.py b/tests/components/uk_transport/test_sensor.py
index 54bc122aa56281..9d179b5b06b4b7 100644
--- a/tests/components/uk_transport/test_sensor.py
+++ b/tests/components/uk_transport/test_sensor.py
@@ -53,11 +53,11 @@ async def test_bus(hass):
bus_state = hass.states.get("sensor.next_bus_to_wantage")
assert None is not bus_state
- assert f"Next bus to {BUS_DIRECTION}" == bus_state.name
- assert BUS_ATCOCODE == bus_state.attributes[ATTR_ATCOCODE]
- assert "Harwell Campus" == bus_state.attributes[ATTR_LOCALITY]
- assert "Bus Station" == bus_state.attributes[ATTR_STOP_NAME]
- assert 2 == len(bus_state.attributes.get(ATTR_NEXT_BUSES))
+ assert bus_state.name == f"Next bus to {BUS_DIRECTION}"
+ assert bus_state.attributes[ATTR_ATCOCODE] == BUS_ATCOCODE
+ assert bus_state.attributes[ATTR_LOCALITY] == "Harwell Campus"
+ assert bus_state.attributes[ATTR_STOP_NAME] == "Bus Station"
+ assert len(bus_state.attributes.get(ATTR_NEXT_BUSES)) == 2
direction_re = re.compile(BUS_DIRECTION)
for bus in bus_state.attributes.get(ATTR_NEXT_BUSES):
@@ -77,13 +77,13 @@ async def test_train(hass):
train_state = hass.states.get("sensor.next_train_to_WAT")
assert None is not train_state
- assert f"Next train to {TRAIN_DESTINATION_NAME}" == train_state.name
- assert TRAIN_STATION_CODE == train_state.attributes[ATTR_STATION_CODE]
- assert TRAIN_DESTINATION_NAME == train_state.attributes[ATTR_CALLING_AT]
- assert 25 == len(train_state.attributes.get(ATTR_NEXT_TRAINS))
+ assert train_state.name == f"Next train to {TRAIN_DESTINATION_NAME}"
+ assert train_state.attributes[ATTR_STATION_CODE] == TRAIN_STATION_CODE
+ assert train_state.attributes[ATTR_CALLING_AT] == TRAIN_DESTINATION_NAME
+ assert len(train_state.attributes.get(ATTR_NEXT_TRAINS)) == 25
assert (
- "London Waterloo"
- == train_state.attributes[ATTR_NEXT_TRAINS][0]["destination_name"]
+ train_state.attributes[ATTR_NEXT_TRAINS][0]["destination_name"]
+ == "London Waterloo"
)
- assert "06:13" == train_state.attributes[ATTR_NEXT_TRAINS][0]["estimated"]
+ assert train_state.attributes[ATTR_NEXT_TRAINS][0]["estimated"] == "06:13"
diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py
index b0491a9fa2ad76..81af3f7243f6ed 100644
--- a/tests/components/unifi/conftest.py
+++ b/tests/components/unifi/conftest.py
@@ -1,9 +1,31 @@
"""Fixtures for UniFi methods."""
+from __future__ import annotations
+
from unittest.mock import patch
+from aiounifi.websocket import SIGNAL_CONNECTION_STATE, SIGNAL_DATA
import pytest
+@pytest.fixture(autouse=True)
+def mock_unifi_websocket():
+ """No real websocket allowed."""
+ with patch("aiounifi.controller.WSClient") as mock:
+
+ def make_websocket_call(data: dict | None = None, state: str = ""):
+ """Generate a websocket call."""
+ if data:
+ mock.return_value.data = data
+ mock.call_args[1]["callback"](SIGNAL_DATA)
+ elif state:
+ mock.return_value.state = state
+ mock.call_args[1]["callback"](SIGNAL_CONNECTION_STATE)
+ else:
+ raise NotImplementedError
+
+ yield make_websocket_call
+
+
@pytest.fixture(autouse=True)
def mock_discovery():
"""No real network traffic allowed."""
diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py
index 54c5fe291ea36b..106c18524142aa 100644
--- a/tests/components/unifi/test_config_flow.py
+++ b/tests/components/unifi/test_config_flow.py
@@ -1,9 +1,12 @@
"""Test UniFi config flow."""
+
+import socket
from unittest.mock import patch
import aiounifi
from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.components.unifi.config_flow import async_discover_unifi
from homeassistant.components.unifi.const import (
CONF_ALLOW_BANDWIDTH_SENSORS,
CONF_ALLOW_UPTIME_SENSORS,
@@ -112,7 +115,9 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery):
aioclient_mock.get(
"https://1.2.3.4:1234/api/self/sites",
json={
- "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}],
+ "data": [
+ {"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"}
+ ],
"meta": {"rc": "ok"},
},
headers={"content-type": CONTENT_TYPE_JSON},
@@ -132,6 +137,12 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery):
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Site name"
assert result["data"] == {
+ CONF_HOST: "1.2.3.4",
+ CONF_USERNAME: "username",
+ CONF_PASSWORD: "password",
+ CONF_PORT: 1234,
+ CONF_SITE_ID: "site_id",
+ CONF_VERIFY_SSL: True,
CONF_CONTROLLER: {
CONF_HOST: "1.2.3.4",
CONF_USERNAME: "username",
@@ -139,11 +150,28 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery):
CONF_PORT: 1234,
CONF_SITE_ID: "site_id",
CONF_VERIFY_SSL: True,
- }
+ },
}
-async def test_flow_works_multiple_sites(hass, aioclient_mock):
+async def test_flow_works_negative_discovery(hass, aioclient_mock, mock_discovery):
+ """Test config flow with a negative outcome of async_discovery_unifi."""
+ result = await hass.config_entries.flow.async_init(
+ UNIFI_DOMAIN, context={"source": "user"}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+ assert result["data_schema"]({CONF_USERNAME: "", CONF_PASSWORD: ""}) == {
+ CONF_HOST: "",
+ CONF_USERNAME: "",
+ CONF_PASSWORD: "",
+ CONF_PORT: 443,
+ CONF_VERIFY_SSL: False,
+ }
+
+
+async def test_flow_multiple_sites(hass, aioclient_mock):
"""Test config flow works when finding multiple sites."""
result = await hass.config_entries.flow.async_init(
UNIFI_DOMAIN, context={"source": "user"}
@@ -164,8 +192,8 @@ async def test_flow_works_multiple_sites(hass, aioclient_mock):
"https://1.2.3.4:1234/api/self/sites",
json={
"data": [
- {"name": "default", "role": "admin", "desc": "site name"},
- {"name": "site2", "role": "admin", "desc": "site2 name"},
+ {"name": "default", "role": "admin", "desc": "site name", "_id": "1"},
+ {"name": "site2", "role": "admin", "desc": "site2 name", "_id": "2"},
],
"meta": {"rc": "ok"},
},
@@ -185,13 +213,13 @@ async def test_flow_works_multiple_sites(hass, aioclient_mock):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "site"
- assert result["data_schema"]({"site": "default"})
- assert result["data_schema"]({"site": "site2"})
+ assert result["data_schema"]({"site": "1"})
+ assert result["data_schema"]({"site": "2"})
async def test_flow_raise_already_configured(hass, aioclient_mock):
"""Test config flow aborts since a connected config entry already exists."""
- await setup_unifi_integration(hass)
+ await setup_unifi_integration(hass, aioclient_mock)
result = await hass.config_entries.flow.async_init(
UNIFI_DOMAIN, context={"source": "user"}
@@ -200,6 +228,8 @@ async def test_flow_raise_already_configured(hass, aioclient_mock):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
+ aioclient_mock.clear_requests()
+
aioclient_mock.get("https://1.2.3.4:1234", status=302)
aioclient_mock.post(
@@ -211,7 +241,9 @@ async def test_flow_raise_already_configured(hass, aioclient_mock):
aioclient_mock.get(
"https://1.2.3.4:1234/api/self/sites",
json={
- "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}],
+ "data": [
+ {"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"}
+ ],
"meta": {"rc": "ok"},
},
headers={"content-type": CONTENT_TYPE_JSON},
@@ -235,12 +267,12 @@ async def test_flow_raise_already_configured(hass, aioclient_mock):
async def test_flow_aborts_configuration_updated(hass, aioclient_mock):
"""Test config flow aborts since a connected config entry already exists."""
entry = MockConfigEntry(
- domain=UNIFI_DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "office"}}
+ domain=UNIFI_DOMAIN, data={"host": "1.2.3.4", "site": "office"}, unique_id="2"
)
entry.add_to_hass(hass)
entry = MockConfigEntry(
- domain=UNIFI_DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "site_id"}}
+ domain=UNIFI_DOMAIN, data={"host": "1.2.3.4", "site": "site_id"}, unique_id="1"
)
entry.add_to_hass(hass)
@@ -262,7 +294,9 @@ async def test_flow_aborts_configuration_updated(hass, aioclient_mock):
aioclient_mock.get(
"https://1.2.3.4:1234/api/self/sites",
json={
- "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}],
+ "data": [
+ {"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"}
+ ],
"meta": {"rc": "ok"},
},
headers={"content-type": CONTENT_TYPE_JSON},
@@ -338,45 +372,23 @@ async def test_flow_fails_controller_unavailable(hass, aioclient_mock):
assert result["errors"] == {"base": "service_unavailable"}
-async def test_flow_fails_unknown_problem(hass, aioclient_mock):
- """Test config flow."""
- result = await hass.config_entries.flow.async_init(
- UNIFI_DOMAIN, context={"source": "user"}
- )
-
- assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["step_id"] == "user"
-
- aioclient_mock.get("https://1.2.3.4:1234", status=302)
-
- with patch("aiounifi.Controller.login", side_effect=Exception):
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={
- CONF_HOST: "1.2.3.4",
- CONF_USERNAME: "username",
- CONF_PASSWORD: "password",
- CONF_PORT: 1234,
- CONF_VERIFY_SSL: True,
- },
- )
-
- assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
-
-
async def test_reauth_flow_update_configuration(hass, aioclient_mock):
"""Verify reauth flow can update controller configuration."""
- controller = await setup_unifi_integration(hass)
+ config_entry = await setup_unifi_integration(hass, aioclient_mock)
+ controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
+ controller.available = False
result = await hass.config_entries.flow.async_init(
UNIFI_DOMAIN,
context={"source": SOURCE_REAUTH},
- data=controller.config_entry,
+ data=config_entry,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER
+ aioclient_mock.clear_requests()
+
aioclient_mock.get("https://1.2.3.4:1234", status=302)
aioclient_mock.post(
@@ -388,7 +400,9 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock):
aioclient_mock.get(
"https://1.2.3.4:1234/api/self/sites",
json={
- "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}],
+ "data": [
+ {"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"}
+ ],
"meta": {"rc": "ok"},
},
headers={"content-type": CONTENT_TYPE_JSON},
@@ -407,15 +421,16 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock):
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "reauth_successful"
- assert controller.host == "1.2.3.4"
- assert controller.config_entry.data[CONF_CONTROLLER][CONF_USERNAME] == "new_name"
- assert controller.config_entry.data[CONF_CONTROLLER][CONF_PASSWORD] == "new_pass"
+ assert config_entry.data[CONF_HOST] == "1.2.3.4"
+ assert config_entry.data[CONF_USERNAME] == "new_name"
+ assert config_entry.data[CONF_PASSWORD] == "new_pass"
-async def test_advanced_option_flow(hass):
+async def test_advanced_option_flow(hass, aioclient_mock):
"""Test advanced config flow options."""
- controller = await setup_unifi_integration(
+ config_entry = await setup_unifi_integration(
hass,
+ aioclient_mock,
clients_response=CLIENTS,
devices_response=DEVICES,
wlans_response=WLANS,
@@ -424,7 +439,7 @@ async def test_advanced_option_flow(hass):
)
result = await hass.config_entries.options.async_init(
- controller.config_entry.entry_id, context={"show_advanced_options": True}
+ config_entry.entry_id, context={"show_advanced_options": True}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -483,10 +498,11 @@ async def test_advanced_option_flow(hass):
}
-async def test_simple_option_flow(hass):
+async def test_simple_option_flow(hass, aioclient_mock):
"""Test simple config flow options."""
- controller = await setup_unifi_integration(
+ config_entry = await setup_unifi_integration(
hass,
+ aioclient_mock,
clients_response=CLIENTS,
wlans_response=WLANS,
dpigroup_response=DPI_GROUPS,
@@ -494,7 +510,7 @@ async def test_simple_option_flow(hass):
)
result = await hass.config_entries.options.async_init(
- controller.config_entry.entry_id, context={"show_advanced_options": False}
+ config_entry.entry_id, context={"show_advanced_options": False}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@@ -550,7 +566,7 @@ async def test_form_ssdp_aborts_if_host_already_exists(hass):
await setup.async_setup_component(hass, "persistent_notification", {})
entry = MockConfigEntry(
domain=UNIFI_DOMAIN,
- data={"controller": {"host": "192.168.208.1", "site": "site_id"}},
+ data={"host": "192.168.208.1", "site": "site_id"},
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
@@ -595,7 +611,7 @@ async def test_form_ssdp_gets_form_with_ignored_entry(hass):
await setup.async_setup_component(hass, "persistent_notification", {})
entry = MockConfigEntry(
domain=UNIFI_DOMAIN,
- data={},
+ data={"not_controller_key": None},
source=config_entries.SOURCE_IGNORE,
)
entry.add_to_hass(hass)
@@ -621,3 +637,15 @@ async def test_form_ssdp_gets_form_with_ignored_entry(hass):
"host": "1.2.3.4",
"site": "default",
}
+
+
+async def test_discover_unifi_positive(hass):
+ """Verify positive run of UniFi discovery."""
+ with patch("socket.gethostbyname", return_value=True):
+ assert await async_discover_unifi(hass)
+
+
+async def test_discover_unifi_negative(hass):
+ """Verify negative run of UniFi discovery."""
+ with patch("socket.gethostbyname", side_effect=socket.gaierror):
+ assert await async_discover_unifi(hass) is None
diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py
index 6acd507eaad7ce..50d464d23c0e14 100644
--- a/tests/components/unifi/test_controller.py
+++ b/tests/components/unifi/test_controller.py
@@ -1,10 +1,12 @@
"""Test UniFi Controller."""
-from collections import deque
+
+import asyncio
from copy import deepcopy
from datetime import timedelta
-from unittest.mock import patch
+from unittest.mock import Mock, patch
import aiounifi
+from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING
import pytest
from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN
@@ -13,6 +15,8 @@
from homeassistant.components.unifi.const import (
CONF_CONTROLLER,
CONF_SITE_ID,
+ CONF_TRACK_CLIENTS,
+ CONF_TRACK_DEVICES,
DEFAULT_ALLOW_BANDWIDTH_SENSORS,
DEFAULT_ALLOW_UPTIME_SENSORS,
DEFAULT_DETECTION_TIME,
@@ -23,7 +27,8 @@
UNIFI_WIRELESS_CLIENTS,
)
from homeassistant.components.unifi.controller import (
- SUPPORTED_PLATFORMS,
+ PLATFORMS,
+ RETRY_TIMER,
get_controller,
)
from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect
@@ -33,14 +38,21 @@
CONF_PORT,
CONF_USERNAME,
CONF_VERIFY_SSL,
+ CONTENT_TYPE_JSON,
)
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.setup import async_setup_component
+import homeassistant.util.dt as dt_util
+
+from tests.common import MockConfigEntry, async_fire_time_changed
-from tests.common import MockConfigEntry
+DEFAULT_CONFIG_ENTRY_ID = 1
+DEFAULT_HOST = "1.2.3.4"
+DEFAULT_SITE = "site_id"
CONTROLLER_HOST = {
"hostname": "controller_host",
- "ip": "1.2.3.4",
+ "ip": DEFAULT_HOST,
"is_wired": True,
"last_seen": 1562600145,
"mac": "10:00:00:00:00:01",
@@ -54,37 +66,106 @@
}
CONTROLLER_DATA = {
- CONF_HOST: "1.2.3.4",
+ CONF_HOST: DEFAULT_HOST,
CONF_USERNAME: "username",
CONF_PASSWORD: "password",
CONF_PORT: 1234,
- CONF_SITE_ID: "site_id",
+ CONF_SITE_ID: DEFAULT_SITE,
CONF_VERIFY_SSL: False,
}
-ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA}
+ENTRY_CONFIG = {**CONTROLLER_DATA, CONF_CONTROLLER: CONTROLLER_DATA}
ENTRY_OPTIONS = {}
CONFIGURATION = []
-SITES = {"Site name": {"desc": "Site name", "name": "site_id", "role": "admin"}}
+SITE = [{"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"}]
DESCRIPTION = [{"name": "username", "site_name": "site_id", "site_role": "admin"}]
+def mock_default_unifi_requests(
+ aioclient_mock,
+ host,
+ site_id,
+ sites=None,
+ description=None,
+ clients_response=None,
+ clients_all_response=None,
+ devices_response=None,
+ dpiapp_response=None,
+ dpigroup_response=None,
+ wlans_response=None,
+):
+ """Mock default UniFi requests responses."""
+ aioclient_mock.get(f"https://{host}:1234", status=302) # Check UniFi OS
+
+ aioclient_mock.post(
+ f"https://{host}:1234/api/login",
+ json={"data": "login successful", "meta": {"rc": "ok"}},
+ headers={"content-type": CONTENT_TYPE_JSON},
+ )
+
+ aioclient_mock.get(
+ f"https://{host}:1234/api/self/sites",
+ json={"data": sites or [], "meta": {"rc": "ok"}},
+ headers={"content-type": CONTENT_TYPE_JSON},
+ )
+
+ aioclient_mock.get(
+ f"https://{host}:1234/api/s/{site_id}/self",
+ json={"data": description or [], "meta": {"rc": "ok"}},
+ headers={"content-type": CONTENT_TYPE_JSON},
+ )
+
+ aioclient_mock.get(
+ f"https://{host}:1234/api/s/{site_id}/stat/sta",
+ json={"data": clients_response or [], "meta": {"rc": "ok"}},
+ headers={"content-type": CONTENT_TYPE_JSON},
+ )
+ aioclient_mock.get(
+ f"https://{host}:1234/api/s/{site_id}/rest/user",
+ json={"data": clients_all_response or [], "meta": {"rc": "ok"}},
+ headers={"content-type": CONTENT_TYPE_JSON},
+ )
+ aioclient_mock.get(
+ f"https://{host}:1234/api/s/{site_id}/stat/device",
+ json={"data": devices_response or [], "meta": {"rc": "ok"}},
+ headers={"content-type": CONTENT_TYPE_JSON},
+ )
+ aioclient_mock.get(
+ f"https://{host}:1234/api/s/{site_id}/rest/dpiapp",
+ json={"data": dpiapp_response or [], "meta": {"rc": "ok"}},
+ headers={"content-type": CONTENT_TYPE_JSON},
+ )
+ aioclient_mock.get(
+ f"https://{host}:1234/api/s/{site_id}/rest/dpigroup",
+ json={"data": dpigroup_response or [], "meta": {"rc": "ok"}},
+ headers={"content-type": CONTENT_TYPE_JSON},
+ )
+ aioclient_mock.get(
+ f"https://{host}:1234/api/s/{site_id}/rest/wlanconf",
+ json={"data": wlans_response or [], "meta": {"rc": "ok"}},
+ headers={"content-type": CONTENT_TYPE_JSON},
+ )
+
+
async def setup_unifi_integration(
hass,
+ aioclient_mock=None,
+ *,
config=ENTRY_CONFIG,
options=ENTRY_OPTIONS,
- sites=SITES,
+ sites=SITE,
site_description=DESCRIPTION,
clients_response=None,
- devices_response=None,
clients_all_response=None,
- wlans_response=None,
- dpigroup_response=None,
+ devices_response=None,
dpiapp_response=None,
+ dpigroup_response=None,
+ wlans_response=None,
known_wireless_clients=None,
controllers=None,
+ unique_id="1",
):
"""Create the UniFi controller."""
assert await async_setup_component(hass, UNIFI_DOMAIN, {})
@@ -93,7 +174,9 @@ async def setup_unifi_integration(
domain=UNIFI_DOMAIN,
data=deepcopy(config),
options=deepcopy(options),
- entry_id=1,
+ unique_id=unique_id,
+ entry_id=DEFAULT_CONFIG_ENTRY_ID,
+ version=1,
)
config_entry.add_to_hass(hass)
@@ -102,93 +185,49 @@ async def setup_unifi_integration(
known_wireless_clients, config_entry
)
- mock_client_responses = deque()
- if clients_response:
- mock_client_responses.append(clients_response)
-
- mock_device_responses = deque()
- if devices_response:
- mock_device_responses.append(devices_response)
-
- mock_client_all_responses = deque()
- if clients_all_response:
- mock_client_all_responses.append(clients_all_response)
-
- mock_wlans_responses = deque()
- if wlans_response:
- mock_wlans_responses.append(wlans_response)
-
- mock_dpigroup_responses = deque()
- if dpigroup_response:
- mock_dpigroup_responses.append(dpigroup_response)
-
- mock_dpiapp_responses = deque()
- if dpiapp_response:
- mock_dpiapp_responses.append(dpiapp_response)
-
- mock_requests = []
-
- async def mock_request(self, method, path, json=None):
- mock_requests.append({"method": method, "path": path, "json": json})
-
- if path == "/stat/sta" and mock_client_responses:
- return mock_client_responses.popleft()
- if path == "/stat/device" and mock_device_responses:
- return mock_device_responses.popleft()
- if path == "/rest/user" and mock_client_all_responses:
- return mock_client_all_responses.popleft()
- if path == "/rest/wlanconf" and mock_wlans_responses:
- return mock_wlans_responses.popleft()
- if path == "/rest/dpigroup" and mock_dpigroup_responses:
- return mock_dpigroup_responses.popleft()
- if path == "/rest/dpiapp" and mock_dpiapp_responses:
- return mock_dpiapp_responses.popleft()
- return {}
+ if aioclient_mock:
+ mock_default_unifi_requests(
+ aioclient_mock,
+ host=config_entry.data[CONF_HOST],
+ site_id=config_entry.data[CONF_SITE_ID],
+ sites=sites,
+ description=site_description,
+ clients_response=clients_response,
+ clients_all_response=clients_all_response,
+ devices_response=devices_response,
+ dpiapp_response=dpiapp_response,
+ dpigroup_response=dpigroup_response,
+ wlans_response=wlans_response,
+ )
- with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch(
- "aiounifi.Controller.login",
- return_value=True,
- ), patch("aiounifi.Controller.sites", return_value=sites), patch(
- "aiounifi.Controller.site_description", return_value=site_description
- ), patch(
- "aiounifi.Controller.request", new=mock_request
- ), patch.object(
- aiounifi.websocket.WSClient, "start", return_value=True
- ):
- await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
if config_entry.entry_id not in hass.data[UNIFI_DOMAIN]:
return None
- controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
- controller.mock_client_responses = mock_client_responses
- controller.mock_device_responses = mock_device_responses
- controller.mock_client_all_responses = mock_client_all_responses
- controller.mock_wlans_responses = mock_wlans_responses
- controller.mock_requests = mock_requests
+ return config_entry
- return controller
-
-async def test_controller_setup(hass):
+async def test_controller_setup(hass, aioclient_mock):
"""Successful setup."""
with patch(
"homeassistant.config_entries.ConfigEntries.async_forward_entry_setup",
return_value=True,
) as forward_entry_setup:
- controller = await setup_unifi_integration(hass)
+ config_entry = await setup_unifi_integration(hass, aioclient_mock)
+ controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
entry = controller.config_entry
- assert len(forward_entry_setup.mock_calls) == len(SUPPORTED_PLATFORMS)
+ assert len(forward_entry_setup.mock_calls) == len(PLATFORMS)
assert forward_entry_setup.mock_calls[0][1] == (entry, TRACKER_DOMAIN)
assert forward_entry_setup.mock_calls[1][1] == (entry, SENSOR_DOMAIN)
assert forward_entry_setup.mock_calls[2][1] == (entry, SWITCH_DOMAIN)
assert controller.host == CONTROLLER_DATA[CONF_HOST]
assert controller.site == CONTROLLER_DATA[CONF_SITE_ID]
- assert controller.site_name in SITES
- assert controller.site_role == SITES[controller.site_name]["role"]
+ assert controller.site_name == SITE[0]["desc"]
+ assert controller.site_role == SITE[0]["role"]
assert controller.option_allow_bandwidth_sensors == DEFAULT_ALLOW_BANDWIDTH_SENSORS
assert controller.option_allow_uptime_sensors == DEFAULT_ALLOW_UPTIME_SENSORS
@@ -201,14 +240,19 @@ async def test_controller_setup(hass):
assert controller.mac is None
- assert controller.signal_update == "unifi-update-1.2.3.4-site_id"
- assert controller.signal_remove == "unifi-remove-1.2.3.4-site_id"
- assert controller.signal_options_update == "unifi-options-1.2.3.4-site_id"
+ assert controller.signal_reachable == "unifi-reachable-1"
+ assert controller.signal_update == "unifi-update-1"
+ assert controller.signal_remove == "unifi-remove-1"
+ assert controller.signal_options_update == "unifi-options-1"
+ assert controller.signal_heartbeat_missed == "unifi-heartbeat-missed"
-async def test_controller_mac(hass):
+async def test_controller_mac(hass, aioclient_mock):
"""Test that it is possible to identify controller mac."""
- controller = await setup_unifi_integration(hass, clients_response=[CONTROLLER_HOST])
+ config_entry = await setup_unifi_integration(
+ hass, aioclient_mock, clients_response=[CONTROLLER_HOST]
+ )
+ controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
assert controller.mac == CONTROLLER_HOST["mac"]
@@ -243,9 +287,31 @@ async def test_controller_unknown_error(hass):
assert hass.data[UNIFI_DOMAIN] == {}
-async def test_reset_after_successful_setup(hass):
+async def test_config_entry_updated(hass, aioclient_mock):
+ """Calling reset when the entry has been setup."""
+ config_entry = await setup_unifi_integration(hass, aioclient_mock)
+ controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
+
+ event_call = Mock()
+ unsub = async_dispatcher_connect(hass, controller.signal_options_update, event_call)
+
+ hass.config_entries.async_update_entry(
+ config_entry, options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False}
+ )
+ await hass.async_block_till_done()
+
+ assert config_entry.options[CONF_TRACK_CLIENTS] is False
+ assert config_entry.options[CONF_TRACK_DEVICES] is False
+
+ event_call.assert_called_once()
+
+ unsub()
+
+
+async def test_reset_after_successful_setup(hass, aioclient_mock):
"""Calling reset when the entry has been setup."""
- controller = await setup_unifi_integration(hass)
+ config_entry = await setup_unifi_integration(hass, aioclient_mock)
+ controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
assert len(controller.listeners) == 6
@@ -256,30 +322,126 @@ async def test_reset_after_successful_setup(hass):
assert len(controller.listeners) == 0
-async def test_wireless_client_event_calls_update_wireless_devices(hass):
+async def test_reset_fails(hass, aioclient_mock):
+ """Calling reset when the entry has been setup can return false."""
+ config_entry = await setup_unifi_integration(hass, aioclient_mock)
+ controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
+
+ with patch(
+ "homeassistant.config_entries.ConfigEntries.async_forward_entry_unload",
+ return_value=False,
+ ):
+ result = await controller.async_reset()
+ await hass.async_block_till_done()
+
+ assert result is False
+
+
+async def test_connection_state_signalling(hass, aioclient_mock, mock_unifi_websocket):
+ """Verify connection statesignalling and connection state are working."""
+ client = {
+ "hostname": "client",
+ "ip": "10.0.0.1",
+ "is_wired": True,
+ "last_seen": dt_util.as_timestamp(dt_util.utcnow()),
+ "mac": "00:00:00:00:00:01",
+ }
+ await setup_unifi_integration(hass, aioclient_mock, clients_response=[client])
+
+ # Controller is connected
+ assert hass.states.get("device_tracker.client").state == "home"
+
+ mock_unifi_websocket(state=STATE_DISCONNECTED)
+ await hass.async_block_till_done()
+
+ # Controller is disconnected
+ assert hass.states.get("device_tracker.client").state == "unavailable"
+
+ mock_unifi_websocket(state=STATE_RUNNING)
+ await hass.async_block_till_done()
+
+ # Controller is once again connected
+ assert hass.states.get("device_tracker.client").state == "home"
+
+
+async def test_wireless_client_event_calls_update_wireless_devices(
+ hass, aioclient_mock, mock_unifi_websocket
+):
"""Call update_wireless_devices method when receiving wireless client event."""
- controller = await setup_unifi_integration(hass)
+ await setup_unifi_integration(hass, aioclient_mock)
with patch(
"homeassistant.components.unifi.controller.UniFiController.update_wireless_clients",
return_value=None,
) as wireless_clients_mock:
- controller.api.websocket._data = {
- "meta": {"rc": "ok", "message": "events"},
- "data": [
- {
- "datetime": "2020-01-20T19:37:04Z",
- "key": aiounifi.events.WIRELESS_CLIENT_CONNECTED,
- "msg": "User[11:22:33:44:55:66] has connected to WLAN",
- "time": 1579549024893,
- }
- ],
- }
- controller.api.session_handler("data")
+ mock_unifi_websocket(
+ data={
+ "meta": {"rc": "ok", "message": "events"},
+ "data": [
+ {
+ "datetime": "2020-01-20T19:37:04Z",
+ "key": aiounifi.events.WIRELESS_CLIENT_CONNECTED,
+ "msg": "User[11:22:33:44:55:66] has connected to WLAN",
+ "time": 1579549024893,
+ }
+ ],
+ },
+ )
assert wireless_clients_mock.assert_called_once
+async def test_reconnect_mechanism(hass, aioclient_mock, mock_unifi_websocket):
+ """Verify reconnect prints only on first reconnection try."""
+ await setup_unifi_integration(hass, aioclient_mock)
+
+ aioclient_mock.clear_requests()
+ aioclient_mock.post(f"https://{DEFAULT_HOST}:1234/api/login", status=502)
+
+ mock_unifi_websocket(state=STATE_DISCONNECTED)
+ await hass.async_block_till_done()
+
+ assert aioclient_mock.call_count == 0
+
+ new_time = dt_util.utcnow() + timedelta(seconds=RETRY_TIMER)
+ async_fire_time_changed(hass, new_time)
+ await hass.async_block_till_done()
+
+ assert aioclient_mock.call_count == 1
+
+ new_time = dt_util.utcnow() + timedelta(seconds=RETRY_TIMER)
+ async_fire_time_changed(hass, new_time)
+ await hass.async_block_till_done()
+
+ assert aioclient_mock.call_count == 2
+
+
+@pytest.mark.parametrize(
+ "exception",
+ [
+ asyncio.TimeoutError,
+ aiounifi.BadGateway,
+ aiounifi.ServiceUnavailable,
+ aiounifi.AiounifiException,
+ ],
+)
+async def test_reconnect_mechanism_exceptions(
+ hass, aioclient_mock, mock_unifi_websocket, exception
+):
+ """Verify async_reconnect calls expected methods."""
+ await setup_unifi_integration(hass, aioclient_mock)
+
+ with patch("aiounifi.Controller.login", side_effect=exception), patch(
+ "homeassistant.components.unifi.controller.UniFiController.reconnect"
+ ) as mock_reconnect:
+ mock_unifi_websocket(state=STATE_DISCONNECTED)
+ await hass.async_block_till_done()
+
+ new_time = dt_util.utcnow() + timedelta(seconds=RETRY_TIMER)
+ async_fire_time_changed(hass, new_time)
+ mock_reconnect.assert_called_once()
+
+
async def test_get_controller(hass):
"""Successful call."""
with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch(
diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py
index 6462fcba943740..2018ae39b64a89 100644
--- a/tests/components/unifi/test_device_tracker.py
+++ b/tests/components/unifi/test_device_tracker.py
@@ -1,5 +1,5 @@
"""The tests for the UniFi device tracker platform."""
-from copy import copy
+
from datetime import timedelta
from unittest.mock import patch
@@ -8,9 +8,8 @@
MESSAGE_CLIENT_REMOVED,
MESSAGE_DEVICE,
MESSAGE_EVENT,
- SIGNAL_CONNECTION_STATE,
)
-from aiounifi.websocket import SIGNAL_DATA, STATE_DISCONNECTED, STATE_RUNNING
+from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING
from homeassistant import config_entries
from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN
@@ -23,626 +22,722 @@
CONF_TRACK_WIRED_CLIENTS,
DOMAIN as UNIFI_DOMAIN,
)
-from homeassistant.const import STATE_UNAVAILABLE
-from homeassistant.helpers import entity_registry
-from homeassistant.setup import async_setup_component
+from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE
+from homeassistant.helpers import device_registry as dr, entity_registry as er
import homeassistant.util.dt as dt_util
from .test_controller import ENTRY_CONFIG, setup_unifi_integration
from tests.common import async_fire_time_changed
-CLIENT_1 = {
- "ap_mac": "00:00:00:00:02:01",
- "essid": "ssid",
- "hostname": "client_1",
- "ip": "10.0.0.1",
- "is_wired": False,
- "last_seen": 1562600145,
- "mac": "00:00:00:00:00:01",
-}
-CLIENT_2 = {
- "hostname": "client_2",
- "ip": "10.0.0.2",
- "is_wired": True,
- "last_seen": 1562600145,
- "mac": "00:00:00:00:00:02",
- "name": "Wired Client",
-}
-CLIENT_3 = {
- "essid": "ssid2",
- "hostname": "client_3",
- "ip": "10.0.0.3",
- "is_wired": False,
- "last_seen": 1562600145,
- "mac": "00:00:00:00:00:03",
-}
-CLIENT_4 = {
- "essid": "ssid",
- "hostname": "client_4",
- "ip": "10.0.0.4",
- "is_wired": True,
- "last_seen": 1562600145,
- "mac": "00:00:00:00:00:04",
-}
-CLIENT_5 = {
- "essid": "ssid",
- "hostname": "client_5",
- "ip": "10.0.0.5",
- "is_wired": True,
- "last_seen": None,
- "mac": "00:00:00:00:00:05",
-}
-
-DEVICE_1 = {
- "board_rev": 3,
- "device_id": "mock-id",
- "has_fan": True,
- "fan_level": 0,
- "ip": "10.0.1.1",
- "last_seen": 1562600145,
- "mac": "00:00:00:00:01:01",
- "model": "US16P150",
- "name": "device_1",
- "next_interval": 20,
- "overheating": True,
- "state": 1,
- "type": "usw",
- "upgradable": True,
- "version": "4.0.42.10433",
-}
-DEVICE_2 = {
- "board_rev": 3,
- "device_id": "mock-id",
- "has_fan": True,
- "ip": "10.0.1.2",
- "mac": "00:00:00:00:01:02",
- "model": "US16P150",
- "name": "device_2",
- "next_interval": 20,
- "state": 0,
- "type": "usw",
- "version": "4.0.42.10433",
-}
-
-EVENT_CLIENT_1_WIRELESS_CONNECTED = {
- "user": CLIENT_1["mac"],
- "ssid": CLIENT_1["essid"],
- "ap": CLIENT_1["ap_mac"],
- "radio": "na",
- "channel": "44",
- "hostname": CLIENT_1["hostname"],
- "key": "EVT_WU_Connected",
- "subsystem": "wlan",
- "site_id": "name",
- "time": 1587753456179,
- "datetime": "2020-04-24T18:37:36Z",
- "msg": f'User{[CLIENT_1["mac"]]} has connected to AP[{CLIENT_1["ap_mac"]}] with SSID "{CLIENT_1["essid"]}" on "channel 44(na)"',
- "_id": "5ea331fa30c49e00f90ddc1a",
-}
-
-EVENT_CLIENT_1_WIRELESS_DISCONNECTED = {
- "user": CLIENT_1["mac"],
- "ssid": CLIENT_1["essid"],
- "hostname": CLIENT_1["hostname"],
- "ap": CLIENT_1["ap_mac"],
- "duration": 467,
- "bytes": 459039,
- "key": "EVT_WU_Disconnected",
- "subsystem": "wlan",
- "site_id": "name",
- "time": 1587752927000,
- "datetime": "2020-04-24T18:28:47Z",
- "msg": f'User{[CLIENT_1["mac"]]} disconnected from "{CLIENT_1["essid"]}" (7m 47s connected, 448.28K bytes, last AP[{CLIENT_1["ap_mac"]}])',
- "_id": "5ea32ff730c49e00f90dca1a",
-}
-
-EVENT_DEVICE_2_UPGRADED = {
- "_id": "5eae7fe02ab79c00f9d38960",
- "datetime": "2020-05-09T20:06:37Z",
- "key": "EVT_SW_Upgraded",
- "msg": f'Switch[{DEVICE_2["mac"]}] was upgraded from "{DEVICE_2["version"]}" to "4.3.13.11253"',
- "subsystem": "lan",
- "sw": DEVICE_2["mac"],
- "sw_name": DEVICE_2["name"],
- "time": 1589054797635,
- "version_from": {DEVICE_2["version"]},
- "version_to": "4.3.13.11253",
-}
-
-
-async def test_platform_manually_configured(hass):
- """Test that nothing happens when configuring unifi through device tracker platform."""
- assert (
- await async_setup_component(
- hass, TRACKER_DOMAIN, {TRACKER_DOMAIN: {"platform": UNIFI_DOMAIN}}
- )
- is False
- )
- assert UNIFI_DOMAIN not in hass.data
-
-
-async def test_no_clients(hass):
+
+async def test_no_entities(hass, aioclient_mock):
"""Test the update_clients function when no clients are found."""
- await setup_unifi_integration(hass)
+ await setup_unifi_integration(hass, aioclient_mock)
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 0
-async def test_tracked_wireless_clients(hass):
- """Test the update_items function with some clients."""
- controller = await setup_unifi_integration(hass, clients_response=[CLIENT_1])
- assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
+async def test_tracked_wireless_clients(hass, aioclient_mock, mock_unifi_websocket):
+ """Verify tracking of wireless clients."""
+ client = {
+ "ap_mac": "00:00:00:00:02:01",
+ "essid": "ssid",
+ "hostname": "client",
+ "ip": "10.0.0.1",
+ "is_wired": False,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:01",
+ }
+ config_entry = await setup_unifi_integration(
+ hass, aioclient_mock, clients_response=[client]
+ )
+ controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
- assert client_1.state == "not_home"
+ assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
+ assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
# State change signalling works without events
- client_1_copy = copy(CLIENT_1)
- controller.api.websocket._data = {
- "meta": {"message": MESSAGE_CLIENT},
- "data": [client_1_copy],
- }
- controller.api.session_handler(SIGNAL_DATA)
+
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT},
+ "data": [client],
+ }
+ )
await hass.async_block_till_done()
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "home"
- assert client_1.attributes["ip"] == "10.0.0.1"
- assert client_1.attributes["mac"] == "00:00:00:00:00:01"
- assert client_1.attributes["hostname"] == "client_1"
- assert client_1.attributes["host_name"] == "client_1"
+ client_state = hass.states.get("device_tracker.client")
+ assert client_state.state == "home"
+ assert client_state.attributes["ip"] == "10.0.0.1"
+ assert client_state.attributes["mac"] == "00:00:00:00:00:01"
+ assert client_state.attributes["hostname"] == "client"
+ assert client_state.attributes["host_name"] == "client"
# State change signalling works with events
- controller.api.websocket._data = {
- "meta": {"message": MESSAGE_EVENT},
- "data": [EVENT_CLIENT_1_WIRELESS_DISCONNECTED],
+
+ # Disconnected event
+
+ event = {
+ "user": client["mac"],
+ "ssid": client["essid"],
+ "hostname": client["hostname"],
+ "ap": client["ap_mac"],
+ "duration": 467,
+ "bytes": 459039,
+ "key": "EVT_WU_Disconnected",
+ "subsystem": "wlan",
+ "site_id": "name",
+ "time": 1587752927000,
+ "datetime": "2020-04-24T18:28:47Z",
+ "msg": f'User{[client["mac"]]} disconnected from "{client["essid"]}" (7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])',
+ "_id": "5ea32ff730c49e00f90dca1a",
}
- controller.api.session_handler(SIGNAL_DATA)
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_EVENT},
+ "data": [event],
+ }
+ )
await hass.async_block_till_done()
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "home"
+ assert hass.states.get("device_tracker.client").state == STATE_HOME
+
+ # Change time to mark client as away
new_time = dt_util.utcnow() + controller.option_detection_time
with patch("homeassistant.util.dt.utcnow", return_value=new_time):
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "not_home"
-
- controller.api.websocket._data = {
- "meta": {"message": MESSAGE_EVENT},
- "data": [EVENT_CLIENT_1_WIRELESS_CONNECTED],
+ assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
+
+ # Connected event
+
+ event = {
+ "user": client["mac"],
+ "ssid": client["essid"],
+ "ap": client["ap_mac"],
+ "radio": "na",
+ "channel": "44",
+ "hostname": client["hostname"],
+ "key": "EVT_WU_Connected",
+ "subsystem": "wlan",
+ "site_id": "name",
+ "time": 1587753456179,
+ "datetime": "2020-04-24T18:37:36Z",
+ "msg": f'User{[client["mac"]]} has connected to AP[{client["ap_mac"]}] with SSID "{client["essid"]}" on "channel 44(na)"',
+ "_id": "5ea331fa30c49e00f90ddc1a",
}
- controller.api.session_handler(SIGNAL_DATA)
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_EVENT},
+ "data": [event],
+ }
+ )
await hass.async_block_till_done()
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "home"
+ assert hass.states.get("device_tracker.client").state == STATE_HOME
-async def test_tracked_clients(hass):
+async def test_tracked_clients(hass, aioclient_mock, mock_unifi_websocket):
"""Test the update_items function with some clients."""
- client_4_copy = copy(CLIENT_4)
- client_4_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
+ client_1 = {
+ "ap_mac": "00:00:00:00:02:01",
+ "essid": "ssid",
+ "hostname": "client_1",
+ "ip": "10.0.0.1",
+ "is_wired": False,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:01",
+ }
+ client_2 = {
+ "ip": "10.0.0.2",
+ "is_wired": True,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:02",
+ "name": "Client 2",
+ }
+ client_3 = {
+ "essid": "ssid2",
+ "hostname": "client_3",
+ "ip": "10.0.0.3",
+ "is_wired": False,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:03",
+ }
+ client_4 = {
+ "essid": "ssid",
+ "hostname": "client_4",
+ "ip": "10.0.0.4",
+ "is_wired": True,
+ "last_seen": dt_util.as_timestamp(dt_util.utcnow()),
+ "mac": "00:00:00:00:00:04",
+ }
+ client_5 = {
+ "essid": "ssid",
+ "hostname": "client_5",
+ "ip": "10.0.0.5",
+ "is_wired": True,
+ "last_seen": None,
+ "mac": "00:00:00:00:00:05",
+ }
- controller = await setup_unifi_integration(
+ await setup_unifi_integration(
hass,
+ aioclient_mock,
options={CONF_SSID_FILTER: ["ssid"]},
- clients_response=[CLIENT_1, CLIENT_2, CLIENT_3, CLIENT_5, client_4_copy],
- known_wireless_clients=(CLIENT_4["mac"],),
+ clients_response=[client_1, client_2, client_3, client_4, client_5],
+ known_wireless_clients=(client_4["mac"],),
)
- assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 4
-
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
- assert client_1.state == "not_home"
- client_2 = hass.states.get("device_tracker.wired_client")
- assert client_2 is not None
- assert client_2.state == "not_home"
+ assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 4
+ assert hass.states.get("device_tracker.client_1").state == STATE_NOT_HOME
+ assert hass.states.get("device_tracker.client_2").state == STATE_NOT_HOME
# Client on SSID not in SSID filter
- client_3 = hass.states.get("device_tracker.client_3")
- assert not client_3
+ assert not hass.states.get("device_tracker.client_3")
# Wireless client with wired bug, if bug active on restart mark device away
- client_4 = hass.states.get("device_tracker.client_4")
- assert client_4 is not None
- assert client_4.state == "not_home"
+ assert hass.states.get("device_tracker.client_4").state == STATE_NOT_HOME
# A client that has never been seen should be marked away.
- client_5 = hass.states.get("device_tracker.client_5")
- assert client_5 is not None
- assert client_5.state == "not_home"
+ assert hass.states.get("device_tracker.client_5").state == STATE_NOT_HOME
# State change signalling works
- client_1_copy = copy(CLIENT_1)
- event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_copy]}
- controller.api.message_handler(event)
+
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT},
+ "data": [client_1],
+ }
+ )
await hass.async_block_till_done()
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "home"
+ assert hass.states.get("device_tracker.client_1").state == STATE_HOME
-async def test_tracked_devices(hass):
+async def test_tracked_devices(hass, aioclient_mock, mock_unifi_websocket):
"""Test the update_items function with some devices."""
- controller = await setup_unifi_integration(
+ device_1 = {
+ "board_rev": 3,
+ "device_id": "mock-id",
+ "has_fan": True,
+ "fan_level": 0,
+ "ip": "10.0.1.1",
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:01:01",
+ "model": "US16P150",
+ "name": "Device 1",
+ "next_interval": 20,
+ "overheating": True,
+ "state": 1,
+ "type": "usw",
+ "upgradable": True,
+ "version": "4.0.42.10433",
+ }
+ device_2 = {
+ "board_rev": 3,
+ "device_id": "mock-id",
+ "has_fan": True,
+ "ip": "10.0.1.2",
+ "mac": "00:00:00:00:01:02",
+ "model": "US16P150",
+ "name": "Device 2",
+ "next_interval": 20,
+ "state": 0,
+ "type": "usw",
+ "version": "4.0.42.10433",
+ }
+ await setup_unifi_integration(
hass,
- devices_response=[DEVICE_1, DEVICE_2],
+ aioclient_mock,
+ devices_response=[device_1, device_2],
)
- assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1
- assert device_1.state == "home"
-
- device_2 = hass.states.get("device_tracker.device_2")
- assert device_2
- assert device_2.state == "not_home"
+ assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
+ assert hass.states.get("device_tracker.device_1").state == STATE_HOME
+ assert hass.states.get("device_tracker.device_2").state == STATE_NOT_HOME
# State change signalling work
- device_1_copy = copy(DEVICE_1)
- device_1_copy["next_interval"] = 20
- event = {"meta": {"message": MESSAGE_DEVICE}, "data": [device_1_copy]}
- controller.api.message_handler(event)
- device_2_copy = copy(DEVICE_2)
- device_2_copy["next_interval"] = 50
- event = {"meta": {"message": MESSAGE_DEVICE}, "data": [device_2_copy]}
- controller.api.message_handler(event)
+
+ device_1["next_interval"] = 20
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_DEVICE},
+ "data": [device_1],
+ }
+ )
+ device_2["next_interval"] = 50
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_DEVICE},
+ "data": [device_2],
+ }
+ )
await hass.async_block_till_done()
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1.state == "home"
- device_2 = hass.states.get("device_tracker.device_2")
- assert device_2.state == "home"
+ assert hass.states.get("device_tracker.device_1").state == STATE_HOME
+ assert hass.states.get("device_tracker.device_2").state == STATE_HOME
+
+ # Change of time can mark device not_home outside of expected reporting interval
new_time = dt_util.utcnow() + timedelta(seconds=90)
with patch("homeassistant.util.dt.utcnow", return_value=new_time):
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1.state == "not_home"
- device_2 = hass.states.get("device_tracker.device_2")
- assert device_2.state == "home"
+ assert hass.states.get("device_tracker.device_1").state == STATE_NOT_HOME
+ assert hass.states.get("device_tracker.device_2").state == STATE_HOME
# Disabled device is unavailable
- device_1_copy = copy(DEVICE_1)
- device_1_copy["disabled"] = True
- event = {"meta": {"message": MESSAGE_DEVICE}, "data": [device_1_copy]}
- controller.api.message_handler(event)
+
+ device_1["disabled"] = True
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_DEVICE},
+ "data": [device_1],
+ }
+ )
await hass.async_block_till_done()
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1.state == STATE_UNAVAILABLE
+ assert hass.states.get("device_tracker.device_1").state == STATE_UNAVAILABLE
+ assert hass.states.get("device_tracker.device_2").state == STATE_HOME
# Update device registry when device is upgraded
- device_2_copy = copy(DEVICE_2)
- device_2_copy["version"] = EVENT_DEVICE_2_UPGRADED["version_to"]
- message = {"meta": {"message": MESSAGE_DEVICE}, "data": [device_2_copy]}
- controller.api.message_handler(message)
- event = {"meta": {"message": MESSAGE_EVENT}, "data": [EVENT_DEVICE_2_UPGRADED]}
- controller.api.message_handler(event)
+
+ event = {
+ "_id": "5eae7fe02ab79c00f9d38960",
+ "datetime": "2020-05-09T20:06:37Z",
+ "key": "EVT_SW_Upgraded",
+ "msg": f'Switch[{device_2["mac"]}] was upgraded from "{device_2["version"]}" to "4.3.13.11253"',
+ "subsystem": "lan",
+ "sw": device_2["mac"],
+ "sw_name": device_2["name"],
+ "time": 1589054797635,
+ "version_from": {device_2["version"]},
+ "version_to": "4.3.13.11253",
+ }
+
+ device_2["version"] = event["version_to"]
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_DEVICE},
+ "data": [device_2],
+ }
+ )
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_EVENT},
+ "data": [event],
+ }
+ )
await hass.async_block_till_done()
# Verify device registry has been updated
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry = entity_registry.async_get("device_tracker.device_2")
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry = dr.async_get(hass)
device = device_registry.async_get(entry.device_id)
- assert device.sw_version == EVENT_DEVICE_2_UPGRADED["version_to"]
+ assert device.sw_version == event["version_to"]
-async def test_remove_clients(hass):
+async def test_remove_clients(hass, aioclient_mock, mock_unifi_websocket):
"""Test the remove_items function with some clients."""
- controller = await setup_unifi_integration(
- hass, clients_response=[CLIENT_1, CLIENT_2]
+ client_1 = {
+ "essid": "ssid",
+ "hostname": "client_1",
+ "is_wired": False,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:01",
+ }
+ client_2 = {
+ "hostname": "client_2",
+ "is_wired": True,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:02",
+ }
+ await setup_unifi_integration(
+ hass, aioclient_mock, clients_response=[client_1, client_2]
)
- assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
+ assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
+ assert hass.states.get("device_tracker.client_1")
+ assert hass.states.get("device_tracker.client_2")
- wired_client = hass.states.get("device_tracker.wired_client")
- assert wired_client is not None
+ # Remove client
- controller.api.websocket._data = {
- "meta": {"message": MESSAGE_CLIENT_REMOVED},
- "data": [CLIENT_1],
- }
- controller.api.session_handler(SIGNAL_DATA)
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT_REMOVED},
+ "data": [client_1],
+ }
+ )
+ await hass.async_block_till_done()
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
-
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is None
-
- wired_client = hass.states.get("device_tracker.wired_client")
- assert wired_client is not None
+ assert not hass.states.get("device_tracker.client_1")
+ assert hass.states.get("device_tracker.client_2")
-async def test_controller_state_change(hass):
+async def test_controller_state_change(hass, aioclient_mock, mock_unifi_websocket):
"""Verify entities state reflect on controller becoming unavailable."""
- controller = await setup_unifi_integration(
+ client = {
+ "essid": "ssid",
+ "hostname": "client",
+ "is_wired": False,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:01",
+ }
+ device = {
+ "board_rev": 3,
+ "device_id": "mock-id",
+ "has_fan": True,
+ "fan_level": 0,
+ "ip": "10.0.1.1",
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:01:01",
+ "model": "US16P150",
+ "name": "Device",
+ "next_interval": 20,
+ "overheating": True,
+ "state": 1,
+ "type": "usw",
+ "upgradable": True,
+ "version": "4.0.42.10433",
+ }
+
+ await setup_unifi_integration(
hass,
- clients_response=[CLIENT_1],
- devices_response=[DEVICE_1],
+ aioclient_mock,
+ clients_response=[client],
+ devices_response=[device],
)
- assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "not_home"
-
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1.state == "home"
+ assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
+ assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
+ assert hass.states.get("device_tracker.device").state == STATE_HOME
# Controller unavailable
- controller.async_unifi_signalling_callback(
- SIGNAL_CONNECTION_STATE, STATE_DISCONNECTED
- )
+ mock_unifi_websocket(state=STATE_DISCONNECTED)
await hass.async_block_till_done()
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == STATE_UNAVAILABLE
-
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1.state == STATE_UNAVAILABLE
+ assert hass.states.get("device_tracker.client").state == STATE_UNAVAILABLE
+ assert hass.states.get("device_tracker.device").state == STATE_UNAVAILABLE
# Controller available
- controller.async_unifi_signalling_callback(SIGNAL_CONNECTION_STATE, STATE_RUNNING)
+ mock_unifi_websocket(state=STATE_RUNNING)
await hass.async_block_till_done()
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "not_home"
-
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1.state == "home"
+ assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
+ assert hass.states.get("device_tracker.device").state == STATE_HOME
-async def test_option_track_clients(hass):
+async def test_option_track_clients(hass, aioclient_mock):
"""Test the tracking of clients can be turned off."""
- controller = await setup_unifi_integration(
+ wireless_client = {
+ "essid": "ssid",
+ "hostname": "wireless_client",
+ "is_wired": False,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:01",
+ }
+ wired_client = {
+ "is_wired": True,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:02",
+ "name": "Wired Client",
+ }
+ device = {
+ "board_rev": 3,
+ "device_id": "mock-id",
+ "has_fan": True,
+ "fan_level": 0,
+ "ip": "10.0.1.1",
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:01:01",
+ "model": "US16P150",
+ "name": "Device",
+ "next_interval": 20,
+ "overheating": True,
+ "state": 1,
+ "type": "usw",
+ "upgradable": True,
+ "version": "4.0.42.10433",
+ }
+
+ config_entry = await setup_unifi_integration(
hass,
- clients_response=[CLIENT_1, CLIENT_2],
- devices_response=[DEVICE_1],
+ aioclient_mock,
+ clients_response=[wireless_client, wired_client],
+ devices_response=[device],
)
- assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3
-
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
-
- client_2 = hass.states.get("device_tracker.wired_client")
- assert client_2 is not None
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1 is not None
+ assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3
+ assert hass.states.get("device_tracker.wireless_client")
+ assert hass.states.get("device_tracker.wired_client")
+ assert hass.states.get("device_tracker.device")
hass.config_entries.async_update_entry(
- controller.config_entry,
+ config_entry,
options={CONF_TRACK_CLIENTS: False},
)
await hass.async_block_till_done()
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is None
-
- client_2 = hass.states.get("device_tracker.wired_client")
- assert client_2 is None
-
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1 is not None
+ assert not hass.states.get("device_tracker.wireless_client")
+ assert not hass.states.get("device_tracker.wired_client")
+ assert hass.states.get("device_tracker.device")
hass.config_entries.async_update_entry(
- controller.config_entry,
+ config_entry,
options={CONF_TRACK_CLIENTS: True},
)
await hass.async_block_till_done()
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
-
- client_2 = hass.states.get("device_tracker.wired_client")
- assert client_2 is not None
-
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1 is not None
+ assert hass.states.get("device_tracker.wireless_client")
+ assert hass.states.get("device_tracker.wired_client")
+ assert hass.states.get("device_tracker.device")
-async def test_option_track_wired_clients(hass):
+async def test_option_track_wired_clients(hass, aioclient_mock):
"""Test the tracking of wired clients can be turned off."""
- controller = await setup_unifi_integration(
+ wireless_client = {
+ "essid": "ssid",
+ "hostname": "wireless_client",
+ "is_wired": False,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:01",
+ }
+ wired_client = {
+ "is_wired": True,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:02",
+ "name": "Wired Client",
+ }
+ device = {
+ "board_rev": 3,
+ "device_id": "mock-id",
+ "has_fan": True,
+ "fan_level": 0,
+ "ip": "10.0.1.1",
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:01:01",
+ "model": "US16P150",
+ "name": "Device",
+ "next_interval": 20,
+ "overheating": True,
+ "state": 1,
+ "type": "usw",
+ "upgradable": True,
+ "version": "4.0.42.10433",
+ }
+
+ config_entry = await setup_unifi_integration(
hass,
- clients_response=[CLIENT_1, CLIENT_2],
- devices_response=[DEVICE_1],
+ aioclient_mock,
+ clients_response=[wireless_client, wired_client],
+ devices_response=[device],
)
- assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
-
- client_2 = hass.states.get("device_tracker.wired_client")
- assert client_2 is not None
-
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1 is not None
+ assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3
+ assert hass.states.get("device_tracker.wireless_client")
+ assert hass.states.get("device_tracker.wired_client")
+ assert hass.states.get("device_tracker.device")
hass.config_entries.async_update_entry(
- controller.config_entry,
+ config_entry,
options={CONF_TRACK_WIRED_CLIENTS: False},
)
await hass.async_block_till_done()
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
-
- client_2 = hass.states.get("device_tracker.wired_client")
- assert client_2 is None
-
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1 is not None
+ assert hass.states.get("device_tracker.wireless_client")
+ assert not hass.states.get("device_tracker.wired_client")
+ assert hass.states.get("device_tracker.device")
hass.config_entries.async_update_entry(
- controller.config_entry,
+ config_entry,
options={CONF_TRACK_WIRED_CLIENTS: True},
)
await hass.async_block_till_done()
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
+ assert hass.states.get("device_tracker.wireless_client")
+ assert hass.states.get("device_tracker.wired_client")
+ assert hass.states.get("device_tracker.device")
- client_2 = hass.states.get("device_tracker.wired_client")
- assert client_2 is not None
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1 is not None
-
-
-async def test_option_track_devices(hass):
+async def test_option_track_devices(hass, aioclient_mock):
"""Test the tracking of devices can be turned off."""
- controller = await setup_unifi_integration(
+ client = {
+ "hostname": "client",
+ "is_wired": True,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:01",
+ }
+ device = {
+ "board_rev": 3,
+ "device_id": "mock-id",
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:01:01",
+ "model": "US16P150",
+ "name": "Device",
+ "next_interval": 20,
+ "overheating": True,
+ "state": 1,
+ "type": "usw",
+ "upgradable": True,
+ "version": "4.0.42.10433",
+ }
+
+ config_entry = await setup_unifi_integration(
hass,
- clients_response=[CLIENT_1, CLIENT_2],
- devices_response=[DEVICE_1],
+ aioclient_mock,
+ clients_response=[client],
+ devices_response=[device],
)
- assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3
-
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
-
- client_2 = hass.states.get("device_tracker.wired_client")
- assert client_2 is not None
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1 is not None
+ assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
+ assert hass.states.get("device_tracker.client")
+ assert hass.states.get("device_tracker.device")
hass.config_entries.async_update_entry(
- controller.config_entry,
+ config_entry,
options={CONF_TRACK_DEVICES: False},
)
await hass.async_block_till_done()
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
-
- client_2 = hass.states.get("device_tracker.wired_client")
- assert client_2 is not None
-
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1 is None
+ assert hass.states.get("device_tracker.client")
+ assert not hass.states.get("device_tracker.device")
hass.config_entries.async_update_entry(
- controller.config_entry,
+ config_entry,
options={CONF_TRACK_DEVICES: True},
)
await hass.async_block_till_done()
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
-
- client_2 = hass.states.get("device_tracker.wired_client")
- assert client_2 is not None
-
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1 is not None
+ assert hass.states.get("device_tracker.client")
+ assert hass.states.get("device_tracker.device")
-async def test_option_ssid_filter(hass):
+async def test_option_ssid_filter(hass, aioclient_mock, mock_unifi_websocket):
"""Test the SSID filter works.
- Client 1 will travel from a supported SSID to an unsupported ssid.
- Client 3 will be removed on change of options since it is in an unsupported SSID.
+ Client will travel from a supported SSID to an unsupported ssid.
+ Client on SSID2 will be removed on change of options.
"""
- client_1_copy = copy(CLIENT_1)
- client_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
+ client = {
+ "essid": "ssid",
+ "hostname": "client",
+ "is_wired": False,
+ "last_seen": dt_util.as_timestamp(dt_util.utcnow()),
+ "mac": "00:00:00:00:00:01",
+ }
+ client_on_ssid2 = {
+ "essid": "ssid2",
+ "hostname": "client_on_ssid2",
+ "is_wired": False,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:02",
+ }
- controller = await setup_unifi_integration(
- hass, clients_response=[client_1_copy, CLIENT_3]
+ config_entry = await setup_unifi_integration(
+ hass, aioclient_mock, clients_response=[client, client_on_ssid2]
)
- assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
+ controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "home"
+ assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
- client_3 = hass.states.get("device_tracker.client_3")
- assert client_3
+ assert hass.states.get("device_tracker.client").state == STATE_HOME
+ assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_NOT_HOME
# Setting SSID filter will remove clients outside of filter
hass.config_entries.async_update_entry(
- controller.config_entry,
+ config_entry,
options={CONF_SSID_FILTER: ["ssid"]},
)
await hass.async_block_till_done()
# Not affected by SSID filter
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "home"
+ assert hass.states.get("device_tracker.client").state == STATE_HOME
# Removed due to SSID filter
- client_3 = hass.states.get("device_tracker.client_3")
- assert not client_3
+ assert not hass.states.get("device_tracker.client_on_ssid2")
# Roams to SSID outside of filter
- client_1_copy = copy(CLIENT_1)
- client_1_copy["essid"] = "other_ssid"
- event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_copy]}
- controller.api.message_handler(event)
+ client["essid"] = "other_ssid"
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT},
+ "data": [client],
+ }
+ )
# Data update while SSID filter is in effect shouldn't create the client
- client_3_copy = copy(CLIENT_3)
- client_3_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
- event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_3_copy]}
- controller.api.message_handler(event)
+ client_on_ssid2["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT},
+ "data": [client_on_ssid2],
+ }
+ )
await hass.async_block_till_done()
# SSID filter marks client as away
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "not_home"
+ assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
# SSID still outside of filter
- client_3 = hass.states.get("device_tracker.client_3")
- assert not client_3
+ assert not hass.states.get("device_tracker.client_on_ssid2")
# Remove SSID filter
hass.config_entries.async_update_entry(
- controller.config_entry,
+ config_entry,
options={CONF_SSID_FILTER: []},
)
await hass.async_block_till_done()
- event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_copy]}
- controller.api.message_handler(event)
- event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_3_copy]}
- controller.api.message_handler(event)
+
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT},
+ "data": [client],
+ }
+ )
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT},
+ "data": [client_on_ssid2],
+ }
+ )
await hass.async_block_till_done()
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "home"
+ assert hass.states.get("device_tracker.client").state == STATE_HOME
+ assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_HOME
- client_3 = hass.states.get("device_tracker.client_3")
- assert client_3.state == "home"
+ # Time pass to mark client as away
new_time = dt_util.utcnow() + controller.option_detection_time
with patch("homeassistant.util.dt.utcnow", return_value=new_time):
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "not_home"
+ assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
- event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_3_copy]}
- controller.api.message_handler(event)
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT},
+ "data": [client_on_ssid2],
+ }
+ )
await hass.async_block_till_done()
+
# Client won't go away until after next update
- client_3 = hass.states.get("device_tracker.client_3")
- assert client_3.state == "home"
+ assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_HOME
# Trigger update to get client marked as away
- event = {"meta": {"message": MESSAGE_CLIENT}, "data": [CLIENT_3]}
- controller.api.message_handler(event)
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT},
+ "data": [client_on_ssid2],
+ }
+ )
await hass.async_block_till_done()
new_time = (
@@ -652,37 +747,51 @@ async def test_option_ssid_filter(hass):
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
- client_3 = hass.states.get("device_tracker.client_3")
- assert client_3.state == "not_home"
+ assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_NOT_HOME
-async def test_wireless_client_go_wired_issue(hass):
+async def test_wireless_client_go_wired_issue(
+ hass, aioclient_mock, mock_unifi_websocket
+):
"""Test the solution to catch wireless device go wired UniFi issue.
UniFi has a known issue that when a wireless device goes away it sometimes gets marked as wired.
"""
- client_1_client = copy(CLIENT_1)
- client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
+ client = {
+ "essid": "ssid",
+ "hostname": "client",
+ "ip": "10.0.0.1",
+ "is_wired": False,
+ "last_seen": dt_util.as_timestamp(dt_util.utcnow()),
+ "mac": "00:00:00:00:00:01",
+ }
+
+ config_entry = await setup_unifi_integration(
+ hass, aioclient_mock, clients_response=[client]
+ )
+ controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
- controller = await setup_unifi_integration(hass, clients_response=[client_1_client])
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
# Client is wireless
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
- assert client_1.state == "home"
- assert client_1.attributes["is_wired"] is False
+ client_state = hass.states.get("device_tracker.client")
+ assert client_state.state == STATE_HOME
+ assert client_state.attributes["is_wired"] is False
# Trigger wired bug
- client_1_client["is_wired"] = True
- event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_client]}
- controller.api.message_handler(event)
+ client["is_wired"] = True
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT},
+ "data": [client],
+ }
+ )
await hass.async_block_till_done()
# Wired bug fix keeps client marked as wireless
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "home"
- assert client_1.attributes["is_wired"] is False
+ client_state = hass.states.get("device_tracker.client")
+ assert client_state.state == STATE_HOME
+ assert client_state.attributes["is_wired"] is False
# Pass time
new_time = dt_util.utcnow() + controller.option_detection_time
@@ -691,58 +800,80 @@ async def test_wireless_client_go_wired_issue(hass):
await hass.async_block_till_done()
# Marked as home according to the timer
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "not_home"
- assert client_1.attributes["is_wired"] is False
+ client_state = hass.states.get("device_tracker.client")
+ assert client_state.state == STATE_NOT_HOME
+ assert client_state.attributes["is_wired"] is False
# Try to mark client as connected
- event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_client]}
- controller.api.message_handler(event)
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT},
+ "data": [client],
+ }
+ )
await hass.async_block_till_done()
# Make sure it don't go online again until wired bug disappears
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "not_home"
- assert client_1.attributes["is_wired"] is False
+ client_state = hass.states.get("device_tracker.client")
+ assert client_state.state == STATE_NOT_HOME
+ assert client_state.attributes["is_wired"] is False
# Make client wireless
- client_1_client["is_wired"] = False
- event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_client]}
- controller.api.message_handler(event)
+ client["is_wired"] = False
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT},
+ "data": [client],
+ }
+ )
await hass.async_block_till_done()
# Client is no longer affected by wired bug and can be marked online
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "home"
- assert client_1.attributes["is_wired"] is False
+ client_state = hass.states.get("device_tracker.client")
+ assert client_state.state == STATE_HOME
+ assert client_state.attributes["is_wired"] is False
-async def test_option_ignore_wired_bug(hass):
+async def test_option_ignore_wired_bug(hass, aioclient_mock, mock_unifi_websocket):
"""Test option to ignore wired bug."""
- client_1_client = copy(CLIENT_1)
- client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
+ client = {
+ "ap_mac": "00:00:00:00:02:01",
+ "essid": "ssid",
+ "hostname": "client",
+ "ip": "10.0.0.1",
+ "is_wired": False,
+ "last_seen": dt_util.as_timestamp(dt_util.utcnow()),
+ "mac": "00:00:00:00:00:01",
+ }
- controller = await setup_unifi_integration(
- hass, options={CONF_IGNORE_WIRED_BUG: True}, clients_response=[client_1_client]
+ config_entry = await setup_unifi_integration(
+ hass,
+ aioclient_mock,
+ options={CONF_IGNORE_WIRED_BUG: True},
+ clients_response=[client],
)
+ controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
# Client is wireless
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
- assert client_1.state == "home"
- assert client_1.attributes["is_wired"] is False
+ client_state = hass.states.get("device_tracker.client")
+ assert client_state.state == STATE_HOME
+ assert client_state.attributes["is_wired"] is False
# Trigger wired bug
- client_1_client["is_wired"] = True
- event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_client]}
- controller.api.message_handler(event)
+ client["is_wired"] = True
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT},
+ "data": [client],
+ }
+ )
await hass.async_block_till_done()
# Wired bug in effect
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "home"
- assert client_1.attributes["is_wired"] is True
+ client_state = hass.states.get("device_tracker.client")
+ assert client_state.state == STATE_HOME
+ assert client_state.attributes["is_wired"] is True
# pass time
new_time = dt_util.utcnow() + controller.option_detection_time
@@ -751,34 +882,61 @@ async def test_option_ignore_wired_bug(hass):
await hass.async_block_till_done()
# Timer marks client as away
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "not_home"
- assert client_1.attributes["is_wired"] is True
+ client_state = hass.states.get("device_tracker.client")
+ assert client_state.state == STATE_NOT_HOME
+ assert client_state.attributes["is_wired"] is True
# Mark client as connected again
- event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_client]}
- controller.api.message_handler(event)
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT},
+ "data": [client],
+ }
+ )
await hass.async_block_till_done()
# Ignoring wired bug allows client to go home again even while affected
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "home"
- assert client_1.attributes["is_wired"] is True
+ client_state = hass.states.get("device_tracker.client")
+ assert client_state.state == STATE_HOME
+ assert client_state.attributes["is_wired"] is True
# Make client wireless
- client_1_client["is_wired"] = False
- event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_client]}
- controller.api.message_handler(event)
+ client["is_wired"] = False
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT},
+ "data": [client],
+ }
+ )
await hass.async_block_till_done()
# Client is wireless and still connected
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1.state == "home"
- assert client_1.attributes["is_wired"] is False
-
+ client_state = hass.states.get("device_tracker.client")
+ assert client_state.state == STATE_HOME
+ assert client_state.attributes["is_wired"] is False
+
+
+async def test_restoring_client(hass, aioclient_mock):
+ """Verify clients are restored from clients_all if they ever was registered to entity registry."""
+ client = {
+ "hostname": "client",
+ "is_wired": True,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:01",
+ }
+ restored = {
+ "hostname": "restored",
+ "is_wired": True,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:02",
+ }
+ not_restored = {
+ "hostname": "not_restored",
+ "is_wired": True,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:03",
+ }
-async def test_restoring_client(hass):
- """Test the update_items function with some clients."""
config_entry = config_entries.ConfigEntry(
version=1,
domain=UNIFI_DOMAIN,
@@ -791,121 +949,171 @@ async def test_restoring_client(hass):
entry_id=1,
)
- registry = await entity_registry.async_get_registry(hass)
- registry.async_get_or_create(
- TRACKER_DOMAIN,
- UNIFI_DOMAIN,
- f'{CLIENT_1["mac"]}-site_id',
- suggested_object_id=CLIENT_1["hostname"],
- config_entry=config_entry,
- )
+ registry = er.async_get(hass)
registry.async_get_or_create(
TRACKER_DOMAIN,
UNIFI_DOMAIN,
- f'{CLIENT_2["mac"]}-site_id',
- suggested_object_id=CLIENT_2["hostname"],
+ f'{restored["mac"]}-site_id',
+ suggested_object_id=restored["hostname"],
config_entry=config_entry,
)
await setup_unifi_integration(
hass,
+ aioclient_mock,
options={CONF_BLOCK_CLIENT: True},
- clients_response=[CLIENT_2],
- clients_all_response=[CLIENT_1],
+ clients_response=[client],
+ clients_all_response=[restored, not_restored],
)
- assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
- device_1 = hass.states.get("device_tracker.client_1")
- assert device_1 is not None
+ assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
+ assert hass.states.get("device_tracker.client")
+ assert hass.states.get("device_tracker.restored")
+ assert not hass.states.get("device_tracker.not_restored")
-async def test_dont_track_clients(hass):
+async def test_dont_track_clients(hass, aioclient_mock):
"""Test don't track clients config works."""
- controller = await setup_unifi_integration(
+ wireless_client = {
+ "essid": "ssid",
+ "hostname": "Wireless client",
+ "ip": "10.0.0.1",
+ "is_wired": False,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:01",
+ }
+ wired_client = {
+ "hostname": "Wired client",
+ "ip": "10.0.0.2",
+ "is_wired": True,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:02",
+ }
+ device = {
+ "board_rev": 3,
+ "device_id": "mock-id",
+ "has_fan": True,
+ "fan_level": 0,
+ "ip": "10.0.1.1",
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:01:01",
+ "model": "US16P150",
+ "name": "Device",
+ "next_interval": 20,
+ "overheating": True,
+ "state": 1,
+ "type": "usw",
+ "upgradable": True,
+ "version": "4.0.42.10433",
+ }
+
+ config_entry = await setup_unifi_integration(
hass,
+ aioclient_mock,
options={CONF_TRACK_CLIENTS: False},
- clients_response=[CLIENT_1],
- devices_response=[DEVICE_1],
+ clients_response=[wireless_client, wired_client],
+ devices_response=[device],
)
- assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is None
-
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1 is not None
+ assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
+ assert not hass.states.get("device_tracker.wireless_client")
+ assert not hass.states.get("device_tracker.wired_client")
+ assert hass.states.get("device_tracker.device")
hass.config_entries.async_update_entry(
- controller.config_entry,
+ config_entry,
options={CONF_TRACK_CLIENTS: True},
)
await hass.async_block_till_done()
- assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
-
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
-
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1 is not None
+ assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3
+ assert hass.states.get("device_tracker.wireless_client")
+ assert hass.states.get("device_tracker.wired_client")
+ assert hass.states.get("device_tracker.device")
-async def test_dont_track_devices(hass):
+async def test_dont_track_devices(hass, aioclient_mock):
"""Test don't track devices config works."""
- controller = await setup_unifi_integration(
+ client = {
+ "hostname": "client",
+ "is_wired": True,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:01",
+ }
+ device = {
+ "board_rev": 3,
+ "device_id": "mock-id",
+ "has_fan": True,
+ "fan_level": 0,
+ "ip": "10.0.1.1",
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:01:01",
+ "model": "US16P150",
+ "name": "Device",
+ "next_interval": 20,
+ "overheating": True,
+ "state": 1,
+ "type": "usw",
+ "upgradable": True,
+ "version": "4.0.42.10433",
+ }
+
+ config_entry = await setup_unifi_integration(
hass,
+ aioclient_mock,
options={CONF_TRACK_DEVICES: False},
- clients_response=[CLIENT_1],
- devices_response=[DEVICE_1],
+ clients_response=[client],
+ devices_response=[device],
)
- assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
-
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1 is None
+ assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
+ assert hass.states.get("device_tracker.client")
+ assert not hass.states.get("device_tracker.device")
hass.config_entries.async_update_entry(
- controller.config_entry,
+ config_entry,
options={CONF_TRACK_DEVICES: True},
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
+ assert hass.states.get("device_tracker.client")
+ assert hass.states.get("device_tracker.device")
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
- device_1 = hass.states.get("device_tracker.device_1")
- assert device_1 is not None
-
-
-async def test_dont_track_wired_clients(hass):
+async def test_dont_track_wired_clients(hass, aioclient_mock):
"""Test don't track wired clients config works."""
- controller = await setup_unifi_integration(
+ wireless_client = {
+ "essid": "ssid",
+ "hostname": "Wireless Client",
+ "is_wired": False,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:01",
+ }
+ wired_client = {
+ "is_wired": True,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:02",
+ "name": "Wired Client",
+ }
+
+ config_entry = await setup_unifi_integration(
hass,
+ aioclient_mock,
options={CONF_TRACK_WIRED_CLIENTS: False},
- clients_response=[CLIENT_1, CLIENT_2],
+ clients_response=[wireless_client, wired_client],
)
- assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
-
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
- client_2 = hass.states.get("device_tracker.wired_client")
- assert client_2 is None
+ assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
+ assert hass.states.get("device_tracker.wireless_client")
+ assert not hass.states.get("device_tracker.wired_client")
hass.config_entries.async_update_entry(
- controller.config_entry,
+ config_entry,
options={CONF_TRACK_WIRED_CLIENTS: True},
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
-
- client_1 = hass.states.get("device_tracker.client_1")
- assert client_1 is not None
-
- client_2 = hass.states.get("device_tracker.wired_client")
- assert client_2 is not None
+ assert hass.states.get("device_tracker.wireless_client")
+ assert hass.states.get("device_tracker.wired_client")
diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py
index cc2a4b3e4a3c38..591165dabf2756 100644
--- a/tests/components/unifi/test_init.py
+++ b/tests/components/unifi/test_init.py
@@ -1,24 +1,31 @@
"""Test UniFi setup process."""
-from unittest.mock import AsyncMock, Mock, patch
+from unittest.mock import AsyncMock, patch
from homeassistant.components import unifi
-from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN
+from homeassistant.components.unifi import async_flatten_entry_data
+from homeassistant.components.unifi.const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN
+from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.setup import async_setup_component
-from .test_controller import setup_unifi_integration
+from .test_controller import (
+ CONTROLLER_DATA,
+ DEFAULT_CONFIG_ENTRY_ID,
+ ENTRY_CONFIG,
+ setup_unifi_integration,
+)
-from tests.common import MockConfigEntry, mock_coro
+from tests.common import MockConfigEntry
async def test_setup_with_no_config(hass):
- """Test that we do not discover anything or try to set up a bridge."""
+ """Test that we do not discover anything or try to set up a controller."""
assert await async_setup_component(hass, UNIFI_DOMAIN, {}) is True
assert UNIFI_DOMAIN not in hass.data
-async def test_successful_config_entry(hass):
+async def test_successful_config_entry(hass, aioclient_mock):
"""Test that configured options for a host are loaded via config entry."""
- await setup_unifi_integration(hass)
+ await setup_unifi_integration(hass, aioclient_mock, unique_id=None)
assert hass.data[UNIFI_DOMAIN]
@@ -31,43 +38,85 @@ async def test_controller_fail_setup(hass):
assert hass.data[UNIFI_DOMAIN] == {}
-async def test_controller_no_mac(hass):
+async def test_controller_mac(hass):
"""Test that configured options for a host are loaded via config entry."""
entry = MockConfigEntry(
- domain=UNIFI_DOMAIN,
- data={
- "controller": {
- "host": "0.0.0.0",
- "username": "user",
- "password": "pass",
- "port": 80,
- "site": "default",
- "verify_ssl": True,
- },
- "poe_control": True,
- },
+ domain=UNIFI_DOMAIN, data=ENTRY_CONFIG, unique_id="1", entry_id=1
)
entry.add_to_hass(hass)
- mock_registry = Mock()
- with patch(
- "homeassistant.components.unifi.UniFiController"
- ) as mock_controller, patch(
- "homeassistant.helpers.device_registry.async_get_registry",
- return_value=mock_coro(mock_registry),
- ):
+
+ with patch("homeassistant.components.unifi.UniFiController") as mock_controller:
mock_controller.return_value.async_setup = AsyncMock(return_value=True)
- mock_controller.return_value.mac = None
+ mock_controller.return_value.mac = "mac1"
assert await unifi.async_setup_entry(hass, entry) is True
assert len(mock_controller.mock_calls) == 2
- assert len(mock_registry.mock_calls) == 0
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get_or_create(
+ config_entry_id=entry.entry_id, connections={(CONNECTION_NETWORK_MAC, "mac1")}
+ )
+ assert device.manufacturer == "Ubiquiti Networks"
+ assert device.model == "UniFi Controller"
+ assert device.name == "UniFi Controller"
+ assert device.sw_version is None
+
+
+async def test_flatten_entry_data(hass):
+ """Verify entry data can be flattened."""
+ entry = MockConfigEntry(
+ domain=UNIFI_DOMAIN,
+ data={CONF_CONTROLLER: CONTROLLER_DATA},
+ )
+ await async_flatten_entry_data(hass, entry)
+
+ assert entry.data == ENTRY_CONFIG
-async def test_unload_entry(hass):
+async def test_unload_entry(hass, aioclient_mock):
"""Test being able to unload an entry."""
- controller = await setup_unifi_integration(hass)
+ config_entry = await setup_unifi_integration(hass, aioclient_mock)
assert hass.data[UNIFI_DOMAIN]
- assert await unifi.async_unload_entry(hass, controller.config_entry)
+ assert await hass.config_entries.async_unload(config_entry.entry_id)
assert not hass.data[UNIFI_DOMAIN]
+
+
+async def test_wireless_clients(hass, hass_storage, aioclient_mock):
+ """Verify wireless clients class."""
+ hass_storage[unifi.STORAGE_KEY] = {
+ "version": unifi.STORAGE_VERSION,
+ "data": {
+ DEFAULT_CONFIG_ENTRY_ID: {
+ "wireless_devices": ["00:00:00:00:00:00", "00:00:00:00:00:01"]
+ }
+ },
+ }
+
+ client_1 = {
+ "hostname": "client_1",
+ "ip": "10.0.0.1",
+ "is_wired": False,
+ "mac": "00:00:00:00:00:01",
+ }
+ client_2 = {
+ "hostname": "client_2",
+ "ip": "10.0.0.2",
+ "is_wired": False,
+ "mac": "00:00:00:00:00:02",
+ }
+ config_entry = await setup_unifi_integration(
+ hass, aioclient_mock, clients_response=[client_1, client_2]
+ )
+
+ for mac in [
+ "00:00:00:00:00:00",
+ "00:00:00:00:00:01",
+ "00:00:00:00:00:02",
+ ]:
+ assert (
+ mac
+ in hass_storage[unifi.STORAGE_KEY]["data"][config_entry.entry_id][
+ "wireless_devices"
+ ]
+ )
diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py
index dc2fea634c91e0..eec4fba7df903f 100644
--- a/tests/components/unifi/test_sensor.py
+++ b/tests/components/unifi/test_sensor.py
@@ -1,8 +1,9 @@
"""UniFi sensor platform tests."""
-from copy import deepcopy
+
+from datetime import datetime
+from unittest.mock import patch
from aiounifi.controller import MESSAGE_CLIENT, MESSAGE_CLIENT_REMOVED
-from aiounifi.websocket import SIGNAL_DATA
from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
@@ -14,168 +15,202 @@
DOMAIN as UNIFI_DOMAIN,
)
from homeassistant.helpers.dispatcher import async_dispatcher_send
-from homeassistant.setup import async_setup_component
+import homeassistant.util.dt as dt_util
from .test_controller import setup_unifi_integration
-CLIENTS = [
- {
- "hostname": "Wired client hostname",
- "ip": "10.0.0.1",
- "is_wired": True,
- "last_seen": 1562600145,
- "mac": "00:00:00:00:00:01",
- "name": "Wired client name",
- "oui": "Producer",
- "sw_mac": "00:00:00:00:01:01",
- "sw_port": 1,
- "wired-rx_bytes": 1234000000,
- "wired-tx_bytes": 5678000000,
- "uptime": 1600094505,
- },
- {
- "hostname": "Wireless client hostname",
- "ip": "10.0.0.2",
- "is_wired": False,
- "last_seen": 1562600145,
- "mac": "00:00:00:00:00:02",
- "name": "Wireless client name",
- "oui": "Producer",
- "sw_mac": "00:00:00:00:01:01",
- "sw_port": 2,
- "rx_bytes": 1234000000,
- "tx_bytes": 5678000000,
- "uptime": 1600094505,
- },
-]
-
-async def test_platform_manually_configured(hass):
- """Test that we do not discover anything or try to set up a controller."""
- assert (
- await async_setup_component(
- hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: {"platform": UNIFI_DOMAIN}}
- )
- is True
- )
- assert UNIFI_DOMAIN not in hass.data
-
-
-async def test_no_clients(hass):
+async def test_no_clients(hass, aioclient_mock):
"""Test the update_clients function when no clients are found."""
- controller = await setup_unifi_integration(
+ await setup_unifi_integration(
hass,
+ aioclient_mock,
options={
CONF_ALLOW_BANDWIDTH_SENSORS: True,
CONF_ALLOW_UPTIME_SENSORS: True,
},
)
- assert len(controller.mock_requests) == 6
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0
-async def test_sensors(hass):
- """Test the update_items function with some clients."""
- controller = await setup_unifi_integration(
+async def test_bandwidth_sensors(hass, aioclient_mock, mock_unifi_websocket):
+ """Verify that bandwidth sensors are working as expected."""
+ wired_client = {
+ "hostname": "Wired client",
+ "is_wired": True,
+ "mac": "00:00:00:00:00:01",
+ "oui": "Producer",
+ "wired-rx_bytes": 1234000000,
+ "wired-tx_bytes": 5678000000,
+ }
+ wireless_client = {
+ "is_wired": False,
+ "mac": "00:00:00:00:00:02",
+ "name": "Wireless client",
+ "oui": "Producer",
+ "rx_bytes": 2345000000,
+ "tx_bytes": 6789000000,
+ }
+ options = {
+ CONF_ALLOW_BANDWIDTH_SENSORS: True,
+ CONF_ALLOW_UPTIME_SENSORS: False,
+ CONF_TRACK_CLIENTS: False,
+ CONF_TRACK_DEVICES: False,
+ }
+
+ config_entry = await setup_unifi_integration(
hass,
- options={
- CONF_ALLOW_BANDWIDTH_SENSORS: True,
- CONF_ALLOW_UPTIME_SENSORS: True,
- CONF_TRACK_CLIENTS: False,
- CONF_TRACK_DEVICES: False,
- },
- clients_response=CLIENTS,
+ aioclient_mock,
+ options=options,
+ clients_response=[wired_client, wireless_client],
)
- assert len(controller.mock_requests) == 6
- assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6
+ assert len(hass.states.async_all()) == 5
+ assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4
+ assert hass.states.get("sensor.wired_client_rx").state == "1234.0"
+ assert hass.states.get("sensor.wired_client_tx").state == "5678.0"
+ assert hass.states.get("sensor.wireless_client_rx").state == "2345.0"
+ assert hass.states.get("sensor.wireless_client_tx").state == "6789.0"
- wired_client_rx = hass.states.get("sensor.wired_client_name_rx")
- assert wired_client_rx.state == "1234.0"
+ # Verify state update
- wired_client_tx = hass.states.get("sensor.wired_client_name_tx")
- assert wired_client_tx.state == "5678.0"
+ wireless_client["rx_bytes"] = 3456000000
+ wireless_client["tx_bytes"] = 7891000000
- wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime")
- assert wired_client_uptime.state == "2020-09-14T14:41:45+00:00"
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT},
+ "data": [wireless_client],
+ }
+ )
+ await hass.async_block_till_done()
- wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx")
- assert wireless_client_rx.state == "1234.0"
+ assert hass.states.get("sensor.wireless_client_rx").state == "3456.0"
+ assert hass.states.get("sensor.wireless_client_tx").state == "7891.0"
- wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx")
- assert wireless_client_tx.state == "5678.0"
+ # Disable option
- wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime")
- assert wireless_client_uptime.state == "2020-09-14T14:41:45+00:00"
+ options[CONF_ALLOW_BANDWIDTH_SENSORS] = False
+ hass.config_entries.async_update_entry(config_entry, options=options.copy())
+ await hass.async_block_till_done()
- clients = deepcopy(CLIENTS)
- clients[0]["is_wired"] = False
- clients[1]["rx_bytes"] = 2345000000
- clients[1]["tx_bytes"] = 6789000000
- clients[1]["uptime"] = 1600180860
+ assert len(hass.states.async_all()) == 1
+ assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0
+ assert hass.states.get("sensor.wireless_client_rx") is None
+ assert hass.states.get("sensor.wireless_client_tx") is None
+ assert hass.states.get("sensor.wired_client_rx") is None
+ assert hass.states.get("sensor.wired_client_tx") is None
+
+ # Enable option
- event = {"meta": {"message": MESSAGE_CLIENT}, "data": clients}
- controller.api.message_handler(event)
+ options[CONF_ALLOW_BANDWIDTH_SENSORS] = True
+ hass.config_entries.async_update_entry(config_entry, options=options.copy())
await hass.async_block_till_done()
- wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx")
- assert wireless_client_rx.state == "2345.0"
+ assert len(hass.states.async_all()) == 5
+ assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4
+ assert hass.states.get("sensor.wireless_client_rx")
+ assert hass.states.get("sensor.wireless_client_tx")
+ assert hass.states.get("sensor.wired_client_rx")
+ assert hass.states.get("sensor.wired_client_tx")
- wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx")
- assert wireless_client_tx.state == "6789.0"
+ # Try to add the sensors again, using a signal
- wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime")
- assert wireless_client_uptime.state == "2020-09-15T14:41:00+00:00"
+ clients_connected = {wired_client["mac"], wireless_client["mac"]}
+ devices_connected = set()
- hass.config_entries.async_update_entry(
- controller.config_entry,
- options={
- CONF_ALLOW_BANDWIDTH_SENSORS: False,
- CONF_ALLOW_UPTIME_SENSORS: False,
- },
+ controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
+
+ async_dispatcher_send(
+ hass,
+ controller.signal_update,
+ clients_connected,
+ devices_connected,
)
await hass.async_block_till_done()
- wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx")
- assert wireless_client_rx is None
+ assert len(hass.states.async_all()) == 5
+ assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4
- wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx")
- assert wireless_client_tx is None
- wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime")
- assert wired_client_uptime is None
+async def test_uptime_sensors(hass, aioclient_mock, mock_unifi_websocket):
+ """Verify that uptime sensors are working as expected."""
+ client1 = {
+ "mac": "00:00:00:00:00:01",
+ "name": "client1",
+ "oui": "Producer",
+ "uptime": 1609506061,
+ }
+ client2 = {
+ "hostname": "Client2",
+ "mac": "00:00:00:00:00:02",
+ "oui": "Producer",
+ "uptime": 60,
+ }
+ options = {
+ CONF_ALLOW_BANDWIDTH_SENSORS: False,
+ CONF_ALLOW_UPTIME_SENSORS: True,
+ CONF_TRACK_CLIENTS: False,
+ CONF_TRACK_DEVICES: False,
+ }
- wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime")
- assert wireless_client_uptime is None
+ now = datetime(2021, 1, 1, 1, tzinfo=dt_util.UTC)
+ with patch("homeassistant.util.dt.now", return_value=now):
+ config_entry = await setup_unifi_integration(
+ hass,
+ aioclient_mock,
+ options=options,
+ clients_response=[client1, client2],
+ )
- hass.config_entries.async_update_entry(
- controller.config_entry,
- options={
- CONF_ALLOW_BANDWIDTH_SENSORS: True,
- CONF_ALLOW_UPTIME_SENSORS: True,
- },
+ assert len(hass.states.async_all()) == 3
+ assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2
+ assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T13:01:01+00:00"
+ assert hass.states.get("sensor.client2_uptime").state == "2021-01-01T00:59:00+00:00"
+
+ # Verify state update
+
+ client1["uptime"] = 1609506062
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT},
+ "data": [client1],
+ }
)
await hass.async_block_till_done()
- wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx")
- assert wireless_client_rx.state == "2345.0"
+ assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T13:01:02+00:00"
+
+ # Disable option
+
+ options[CONF_ALLOW_UPTIME_SENSORS] = False
+ hass.config_entries.async_update_entry(config_entry, options=options.copy())
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 1
+ assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0
+ assert hass.states.get("sensor.client1_uptime") is None
+ assert hass.states.get("sensor.client2_uptime") is None
- wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx")
- assert wireless_client_tx.state == "6789.0"
+ # Enable option
- wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime")
- assert wireless_client_uptime.state == "2020-09-15T14:41:00+00:00"
+ options[CONF_ALLOW_UPTIME_SENSORS] = True
+ with patch("homeassistant.util.dt.now", return_value=now):
+ hass.config_entries.async_update_entry(config_entry, options=options.copy())
+ await hass.async_block_till_done()
- wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime")
- assert wired_client_uptime.state == "2020-09-14T14:41:45+00:00"
+ assert len(hass.states.async_all()) == 3
+ assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2
+ assert hass.states.get("sensor.client1_uptime")
+ assert hass.states.get("sensor.client2_uptime")
# Try to add the sensors again, using a signal
- clients_connected = set()
+
+ clients_connected = {client1["mac"], client2["mac"]}
devices_connected = set()
- clients_connected.add(clients[0]["mac"])
- clients_connected.add(clients[1]["mac"])
+ controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
async_dispatcher_send(
hass,
@@ -183,63 +218,69 @@ async def test_sensors(hass):
clients_connected,
devices_connected,
)
-
await hass.async_block_till_done()
- assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6
+ assert len(hass.states.async_all()) == 3
+ assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2
-async def test_remove_sensors(hass):
- """Test the remove_items function with some clients."""
- controller = await setup_unifi_integration(
+async def test_remove_sensors(hass, aioclient_mock, mock_unifi_websocket):
+ """Verify removing of clients work as expected."""
+ wired_client = {
+ "hostname": "Wired client",
+ "is_wired": True,
+ "mac": "00:00:00:00:00:01",
+ "oui": "Producer",
+ "wired-rx_bytes": 1234000000,
+ "wired-tx_bytes": 5678000000,
+ "uptime": 1600094505,
+ }
+ wireless_client = {
+ "is_wired": False,
+ "mac": "00:00:00:00:00:02",
+ "name": "Wireless client",
+ "oui": "Producer",
+ "rx_bytes": 2345000000,
+ "tx_bytes": 6789000000,
+ "uptime": 60,
+ }
+
+ await setup_unifi_integration(
hass,
+ aioclient_mock,
options={
CONF_ALLOW_BANDWIDTH_SENSORS: True,
CONF_ALLOW_UPTIME_SENSORS: True,
},
- clients_response=CLIENTS,
+ clients_response=[wired_client, wireless_client],
)
+
+ assert len(hass.states.async_all()) == 9
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
-
- wired_client_rx = hass.states.get("sensor.wired_client_name_rx")
- assert wired_client_rx is not None
- wired_client_tx = hass.states.get("sensor.wired_client_name_tx")
- assert wired_client_tx is not None
-
- wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime")
- assert wired_client_uptime is not None
-
- wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx")
- assert wireless_client_rx is not None
- wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx")
- assert wireless_client_tx is not None
-
- wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime")
- assert wireless_client_uptime is not None
-
- controller.api.websocket._data = {
- "meta": {"message": MESSAGE_CLIENT_REMOVED},
- "data": [CLIENTS[0]],
- }
- controller.api.session_handler(SIGNAL_DATA)
+ assert hass.states.get("sensor.wired_client_rx")
+ assert hass.states.get("sensor.wired_client_tx")
+ assert hass.states.get("sensor.wired_client_uptime")
+ assert hass.states.get("sensor.wireless_client_rx")
+ assert hass.states.get("sensor.wireless_client_tx")
+ assert hass.states.get("sensor.wireless_client_uptime")
+
+ # Remove wired client
+
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT_REMOVED},
+ "data": [wired_client],
+ }
+ )
await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 5
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
-
- wired_client_rx = hass.states.get("sensor.wired_client_name_rx")
- assert wired_client_rx is None
- wired_client_tx = hass.states.get("sensor.wired_client_name_tx")
- assert wired_client_tx is None
-
- wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime")
- assert wired_client_uptime is None
-
- wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx")
- assert wireless_client_rx is not None
- wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx")
- assert wireless_client_tx is not None
-
- wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime")
- assert wireless_client_uptime is not None
+ assert hass.states.get("sensor.wired_client_rx") is None
+ assert hass.states.get("sensor.wired_client_tx") is None
+ assert hass.states.get("sensor.wired_client_uptime") is None
+ assert hass.states.get("sensor.wireless_client_rx")
+ assert hass.states.get("sensor.wireless_client_tx")
+ assert hass.states.get("sensor.wireless_client_uptime")
diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py
index 903db479d34c7c..b8f6e2da5534c3 100644
--- a/tests/components/unifi/test_switch.py
+++ b/tests/components/unifi/test_switch.py
@@ -1,10 +1,10 @@
"""UniFi switch platform tests."""
from copy import deepcopy
+from unittest.mock import patch
from aiounifi.controller import MESSAGE_CLIENT_REMOVED, MESSAGE_EVENT
-from aiounifi.websocket import SIGNAL_DATA
-from homeassistant import config_entries
+from homeassistant import config_entries, core
from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.unifi.const import (
@@ -16,8 +16,8 @@
DOMAIN as UNIFI_DOMAIN,
)
from homeassistant.components.unifi.switch import POE_SWITCH
-from homeassistant.helpers import entity_registry
-from homeassistant.setup import async_setup_component
+from homeassistant.helpers import entity_registry as er
+from homeassistant.helpers.dispatcher import async_dispatcher_send
from .test_controller import (
CONTROLLER_HOST,
@@ -282,21 +282,11 @@
]
-async def test_platform_manually_configured(hass):
- """Test that we do not discover anything or try to set up a controller."""
- assert (
- await async_setup_component(
- hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: {"platform": UNIFI_DOMAIN}}
- )
- is True
- )
- assert UNIFI_DOMAIN not in hass.data
-
-
-async def test_no_clients(hass):
+async def test_no_clients(hass, aioclient_mock):
"""Test the update_clients function when no clients are found."""
- controller = await setup_unifi_integration(
+ await setup_unifi_integration(
hass,
+ aioclient_mock,
options={
CONF_TRACK_CLIENTS: False,
CONF_TRACK_DEVICES: False,
@@ -304,45 +294,46 @@ async def test_no_clients(hass):
},
)
- assert len(controller.mock_requests) == 6
+ assert aioclient_mock.call_count == 10
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
-async def test_controller_not_client(hass):
+async def test_controller_not_client(hass, aioclient_mock):
"""Test that the controller doesn't become a switch."""
- controller = await setup_unifi_integration(
+ await setup_unifi_integration(
hass,
+ aioclient_mock,
options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False},
clients_response=[CONTROLLER_HOST],
devices_response=[DEVICE_1],
)
- assert len(controller.mock_requests) == 6
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
cloudkey = hass.states.get("switch.cloud_key")
assert cloudkey is None
-async def test_not_admin(hass):
+async def test_not_admin(hass, aioclient_mock):
"""Test that switch platform only work on an admin account."""
description = deepcopy(DESCRIPTION)
description[0]["site_role"] = "not admin"
- controller = await setup_unifi_integration(
+ await setup_unifi_integration(
hass,
+ aioclient_mock,
options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False},
site_description=description,
clients_response=[CLIENT_1],
devices_response=[DEVICE_1],
)
- assert len(controller.mock_requests) == 6
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
-async def test_switches(hass):
+async def test_switches(hass, aioclient_mock):
"""Test the update_items function with some clients."""
- controller = await setup_unifi_integration(
+ config_entry = await setup_unifi_integration(
hass,
+ aioclient_mock,
options={
CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]],
CONF_TRACK_CLIENTS: False,
@@ -354,8 +345,8 @@ async def test_switches(hass):
dpigroup_response=DPI_GROUPS,
dpiapp_response=DPI_APPS,
)
+ controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
- assert len(controller.mock_requests) == 6
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 4
switch_1 = hass.states.get("switch.poe_client_1")
@@ -380,39 +371,46 @@ async def test_switches(hass):
dpi_switch = hass.states.get("switch.block_media_streaming")
assert dpi_switch is not None
assert dpi_switch.state == "on"
+ assert dpi_switch.attributes["icon"] == "mdi:network"
+
+ # Block and unblock client
+
+ aioclient_mock.post(
+ f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr",
+ )
await hass.services.async_call(
SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True
)
- assert len(controller.mock_requests) == 7
- assert controller.mock_requests[6] == {
- "json": {"mac": "00:00:00:00:01:01", "cmd": "block-sta"},
- "method": "post",
- "path": "/cmd/stamgr",
+ assert aioclient_mock.call_count == 11
+ assert aioclient_mock.mock_calls[10][2] == {
+ "mac": "00:00:00:00:01:01",
+ "cmd": "block-sta",
}
await hass.services.async_call(
SWITCH_DOMAIN, "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True
)
- assert len(controller.mock_requests) == 8
- assert controller.mock_requests[7] == {
- "json": {"mac": "00:00:00:00:01:01", "cmd": "unblock-sta"},
- "method": "post",
- "path": "/cmd/stamgr",
+ assert aioclient_mock.call_count == 12
+ assert aioclient_mock.mock_calls[11][2] == {
+ "mac": "00:00:00:00:01:01",
+ "cmd": "unblock-sta",
}
+ # Enable and disable DPI
+
+ aioclient_mock.put(
+ f"https://{controller.host}:1234/api/s/{controller.site}/rest/dpiapp/5f976f62e3c58f018ec7e17d",
+ )
+
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_off",
{"entity_id": "switch.block_media_streaming"},
blocking=True,
)
- assert len(controller.mock_requests) == 9
- assert controller.mock_requests[8] == {
- "json": {"enabled": False},
- "method": "put",
- "path": "/rest/dpiapp/5f976f62e3c58f018ec7e17d",
- }
+ assert aioclient_mock.call_count == 13
+ assert aioclient_mock.mock_calls[12][2] == {"enabled": False}
await hass.services.async_call(
SWITCH_DOMAIN,
@@ -420,22 +418,25 @@ async def test_switches(hass):
{"entity_id": "switch.block_media_streaming"},
blocking=True,
)
- assert len(controller.mock_requests) == 10
- assert controller.mock_requests[9] == {
- "json": {"enabled": True},
- "method": "put",
- "path": "/rest/dpiapp/5f976f62e3c58f018ec7e17d",
- }
+ assert aioclient_mock.call_count == 14
+ assert aioclient_mock.mock_calls[13][2] == {"enabled": True}
+
+ # Make sure no duplicates arise on generic signal update
+ async_dispatcher_send(hass, controller.signal_update)
+ await hass.async_block_till_done()
+ assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 4
-async def test_remove_switches(hass):
+async def test_remove_switches(hass, aioclient_mock, mock_unifi_websocket):
"""Test the update_items function with some clients."""
- controller = await setup_unifi_integration(
+ await setup_unifi_integration(
hass,
+ aioclient_mock,
options={CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]},
clients_response=[CLIENT_1, UNBLOCKED],
devices_response=[DEVICE_1],
)
+
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
poe_switch = hass.states.get("switch.poe_client_1")
@@ -444,11 +445,12 @@ async def test_remove_switches(hass):
block_switch = hass.states.get("switch.block_client_2")
assert block_switch is not None
- controller.api.websocket._data = {
- "meta": {"message": MESSAGE_CLIENT_REMOVED},
- "data": [CLIENT_1, UNBLOCKED],
- }
- controller.api.session_handler(SIGNAL_DATA)
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_CLIENT_REMOVED},
+ "data": [CLIENT_1, UNBLOCKED],
+ }
+ )
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
@@ -460,10 +462,11 @@ async def test_remove_switches(hass):
assert block_switch is None
-async def test_block_switches(hass):
+async def test_block_switches(hass, aioclient_mock, mock_unifi_websocket):
"""Test the update_items function with some clients."""
- controller = await setup_unifi_integration(
+ config_entry = await setup_unifi_integration(
hass,
+ aioclient_mock,
options={
CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]],
CONF_TRACK_CLIENTS: False,
@@ -472,6 +475,7 @@ async def test_block_switches(hass):
clients_response=[UNBLOCKED],
clients_all_response=[BLOCKED],
)
+ controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
@@ -483,11 +487,12 @@ async def test_block_switches(hass):
assert unblocked is not None
assert unblocked.state == "on"
- controller.api.websocket._data = {
- "meta": {"message": MESSAGE_EVENT},
- "data": [EVENT_BLOCKED_CLIENT_UNBLOCKED],
- }
- controller.api.session_handler(SIGNAL_DATA)
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_EVENT},
+ "data": [EVENT_BLOCKED_CLIENT_UNBLOCKED],
+ }
+ )
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
@@ -495,11 +500,12 @@ async def test_block_switches(hass):
assert blocked is not None
assert blocked.state == "on"
- controller.api.websocket._data = {
- "meta": {"message": MESSAGE_EVENT},
- "data": [EVENT_BLOCKED_CLIENT_BLOCKED],
- }
- controller.api.session_handler(SIGNAL_DATA)
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_EVENT},
+ "data": [EVENT_BLOCKED_CLIENT_BLOCKED],
+ }
+ )
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
@@ -507,31 +513,36 @@ async def test_block_switches(hass):
assert blocked is not None
assert blocked.state == "off"
+ aioclient_mock.post(
+ f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr",
+ )
+
await hass.services.async_call(
SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True
)
- assert len(controller.mock_requests) == 7
- assert controller.mock_requests[6] == {
- "json": {"mac": "00:00:00:00:01:01", "cmd": "block-sta"},
- "method": "post",
- "path": "/cmd/stamgr",
+ assert aioclient_mock.call_count == 11
+ assert aioclient_mock.mock_calls[10][2] == {
+ "mac": "00:00:00:00:01:01",
+ "cmd": "block-sta",
}
await hass.services.async_call(
SWITCH_DOMAIN, "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True
)
- assert len(controller.mock_requests) == 8
- assert controller.mock_requests[7] == {
- "json": {"mac": "00:00:00:00:01:01", "cmd": "unblock-sta"},
- "method": "post",
- "path": "/cmd/stamgr",
+ assert aioclient_mock.call_count == 12
+ assert aioclient_mock.mock_calls[11][2] == {
+ "mac": "00:00:00:00:01:01",
+ "cmd": "unblock-sta",
}
-async def test_new_client_discovered_on_block_control(hass):
+async def test_new_client_discovered_on_block_control(
+ hass, aioclient_mock, mock_unifi_websocket
+):
"""Test if 2nd update has a new client."""
- controller = await setup_unifi_integration(
+ await setup_unifi_integration(
hass,
+ aioclient_mock,
options={
CONF_BLOCK_CLIENT: [BLOCKED["mac"]],
CONF_TRACK_CLIENTS: False,
@@ -540,26 +551,27 @@ async def test_new_client_discovered_on_block_control(hass):
},
)
- assert len(controller.mock_requests) == 6
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
blocked = hass.states.get("switch.block_client_1")
assert blocked is None
- controller.api.websocket._data = {
- "meta": {"message": "sta:sync"},
- "data": [BLOCKED],
- }
- controller.api.session_handler(SIGNAL_DATA)
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": "sta:sync"},
+ "data": [BLOCKED],
+ }
+ )
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
- controller.api.websocket._data = {
- "meta": {"message": MESSAGE_EVENT},
- "data": [EVENT_BLOCKED_CLIENT_CONNECTED],
- }
- controller.api.session_handler(SIGNAL_DATA)
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_EVENT},
+ "data": [EVENT_BLOCKED_CLIENT_CONNECTED],
+ }
+ )
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
@@ -567,10 +579,11 @@ async def test_new_client_discovered_on_block_control(hass):
assert blocked is not None
-async def test_option_block_clients(hass):
+async def test_option_block_clients(hass, aioclient_mock):
"""Test the changes to option reflects accordingly."""
- controller = await setup_unifi_integration(
+ config_entry = await setup_unifi_integration(
hass,
+ aioclient_mock,
options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]},
clients_all_response=[BLOCKED, UNBLOCKED],
)
@@ -578,7 +591,7 @@ async def test_option_block_clients(hass):
# Add a second switch
hass.config_entries.async_update_entry(
- controller.config_entry,
+ config_entry,
options={CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]]},
)
await hass.async_block_till_done()
@@ -586,7 +599,7 @@ async def test_option_block_clients(hass):
# Remove the second switch again
hass.config_entries.async_update_entry(
- controller.config_entry,
+ config_entry,
options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]},
)
await hass.async_block_till_done()
@@ -594,7 +607,7 @@ async def test_option_block_clients(hass):
# Enable one and remove another one
hass.config_entries.async_update_entry(
- controller.config_entry,
+ config_entry,
options={CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]},
)
await hass.async_block_till_done()
@@ -602,17 +615,18 @@ async def test_option_block_clients(hass):
# Remove one
hass.config_entries.async_update_entry(
- controller.config_entry,
+ config_entry,
options={CONF_BLOCK_CLIENT: []},
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
-async def test_option_remove_switches(hass):
+async def test_option_remove_switches(hass, aioclient_mock):
"""Test removal of DPI switch when options updated."""
- controller = await setup_unifi_integration(
+ config_entry = await setup_unifi_integration(
hass,
+ aioclient_mock,
options={
CONF_TRACK_CLIENTS: False,
CONF_TRACK_DEVICES: False,
@@ -626,86 +640,85 @@ async def test_option_remove_switches(hass):
# Disable DPI Switches
hass.config_entries.async_update_entry(
- controller.config_entry,
+ config_entry,
options={CONF_DPI_RESTRICTIONS: False, CONF_POE_CLIENTS: False},
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
-async def test_new_client_discovered_on_poe_control(hass):
+async def test_new_client_discovered_on_poe_control(
+ hass, aioclient_mock, mock_unifi_websocket
+):
"""Test if 2nd update has a new client."""
- controller = await setup_unifi_integration(
+ config_entry = await setup_unifi_integration(
hass,
+ aioclient_mock,
options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False},
clients_response=[CLIENT_1],
devices_response=[DEVICE_1],
)
+ controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
- assert len(controller.mock_requests) == 6
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
- controller.api.websocket._data = {
- "meta": {"message": "sta:sync"},
- "data": [CLIENT_2],
- }
- controller.api.session_handler(SIGNAL_DATA)
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": "sta:sync"},
+ "data": [CLIENT_2],
+ }
+ )
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
- controller.api.websocket._data = {
- "meta": {"message": MESSAGE_EVENT},
- "data": [EVENT_CLIENT_2_CONNECTED],
- }
- controller.api.session_handler(SIGNAL_DATA)
+ mock_unifi_websocket(
+ data={
+ "meta": {"message": MESSAGE_EVENT},
+ "data": [EVENT_CLIENT_2_CONNECTED],
+ }
+ )
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
switch_2 = hass.states.get("switch.poe_client_2")
assert switch_2 is not None
+ aioclient_mock.put(
+ f"https://{controller.host}:1234/api/s/{controller.site}/rest/device/mock-id",
+ )
+
await hass.services.async_call(
SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True
)
- assert len(controller.mock_requests) == 7
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
- assert controller.mock_requests[6] == {
- "json": {
- "port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "off"}]
- },
- "method": "put",
- "path": "/rest/device/mock-id",
+ assert aioclient_mock.call_count == 11
+ assert aioclient_mock.mock_calls[10][2] == {
+ "port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "off"}]
}
await hass.services.async_call(
SWITCH_DOMAIN, "turn_on", {"entity_id": "switch.poe_client_1"}, blocking=True
)
- assert len(controller.mock_requests) == 8
- assert controller.mock_requests[7] == {
- "json": {
- "port_overrides": [
- {"port_idx": 1, "portconf_id": "1a1", "poe_mode": "auto"}
- ]
- },
- "method": "put",
- "path": "/rest/device/mock-id",
+ assert aioclient_mock.call_count == 12
+ assert aioclient_mock.mock_calls[11][2] == {
+ "port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "auto"}]
}
-async def test_ignore_multiple_poe_clients_on_same_port(hass):
+async def test_ignore_multiple_poe_clients_on_same_port(hass, aioclient_mock):
"""Ignore when there are multiple POE driven clients on same port.
If there is a non-UniFi switch powered by POE,
clients will be transparently marked as having POE as well.
"""
- controller = await setup_unifi_integration(
+ await setup_unifi_integration(
hass,
+ aioclient_mock,
clients_response=POE_SWITCH_CLIENTS,
devices_response=[DEVICE_1],
)
- assert len(controller.mock_requests) == 6
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3
switch_1 = hass.states.get("switch.poe_client_1")
@@ -714,8 +727,66 @@ async def test_ignore_multiple_poe_clients_on_same_port(hass):
assert switch_2 is None
-async def test_restoring_client(hass):
- """Test the update_items function with some clients."""
+async def test_restore_client_succeed(hass, aioclient_mock):
+ """Test that RestoreEntity works as expected."""
+ POE_DEVICE = {
+ "device_id": "12345",
+ "ip": "1.0.1.1",
+ "mac": "00:00:00:00:01:01",
+ "last_seen": 1562600145,
+ "model": "US16P150",
+ "name": "POE Switch",
+ "port_overrides": [
+ {
+ "poe_mode": "off",
+ "port_idx": 1,
+ "portconf_id": "5f3edd2aba4cc806a19f2db2",
+ }
+ ],
+ "port_table": [
+ {
+ "media": "GE",
+ "name": "Port 1",
+ "op_mode": "switch",
+ "poe_caps": 7,
+ "poe_class": "Unknown",
+ "poe_current": "0.00",
+ "poe_enable": False,
+ "poe_good": False,
+ "poe_mode": "off",
+ "poe_power": "0.00",
+ "poe_voltage": "0.00",
+ "port_idx": 1,
+ "port_poe": True,
+ "portconf_id": "5f3edd2aba4cc806a19f2db2",
+ "up": False,
+ },
+ ],
+ "state": 1,
+ "type": "usw",
+ "version": "4.0.42.10433",
+ }
+ POE_CLIENT = {
+ "hostname": "poe_client",
+ "ip": "1.0.0.1",
+ "is_wired": True,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:01",
+ "name": "POE Client",
+ "oui": "Producer",
+ }
+
+ fake_state = core.State(
+ "switch.poe_client",
+ "off",
+ {
+ "power": "0.00",
+ "switch": POE_DEVICE["mac"],
+ "port": 1,
+ "poe_mode": "auto",
+ },
+ )
+
config_entry = config_entries.ConfigEntry(
version=1,
domain=UNIFI_DOMAIN,
@@ -728,36 +799,120 @@ async def test_restoring_client(hass):
entry_id=1,
)
- registry = await entity_registry.async_get_registry(hass)
+ registry = er.async_get(hass)
registry.async_get_or_create(
SWITCH_DOMAIN,
UNIFI_DOMAIN,
- f'{POE_SWITCH}-{CLIENT_1["mac"]}',
- suggested_object_id=CLIENT_1["hostname"],
+ f'{POE_SWITCH}-{POE_CLIENT["mac"]}',
+ suggested_object_id=POE_CLIENT["hostname"],
config_entry=config_entry,
)
+
+ with patch(
+ "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state",
+ return_value=fake_state,
+ ):
+ await setup_unifi_integration(
+ hass,
+ aioclient_mock,
+ options={
+ CONF_TRACK_CLIENTS: False,
+ CONF_TRACK_DEVICES: False,
+ },
+ clients_response=[],
+ devices_response=[POE_DEVICE],
+ clients_all_response=[POE_CLIENT],
+ )
+
+ assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
+
+ poe_client = hass.states.get("switch.poe_client")
+ assert poe_client.state == "off"
+
+
+async def test_restore_client_no_old_state(hass, aioclient_mock):
+ """Test that RestoreEntity without old state makes entity unavailable."""
+ POE_DEVICE = {
+ "device_id": "12345",
+ "ip": "1.0.1.1",
+ "mac": "00:00:00:00:01:01",
+ "last_seen": 1562600145,
+ "model": "US16P150",
+ "name": "POE Switch",
+ "port_overrides": [
+ {
+ "poe_mode": "off",
+ "port_idx": 1,
+ "portconf_id": "5f3edd2aba4cc806a19f2db2",
+ }
+ ],
+ "port_table": [
+ {
+ "media": "GE",
+ "name": "Port 1",
+ "op_mode": "switch",
+ "poe_caps": 7,
+ "poe_class": "Unknown",
+ "poe_current": "0.00",
+ "poe_enable": False,
+ "poe_good": False,
+ "poe_mode": "off",
+ "poe_power": "0.00",
+ "poe_voltage": "0.00",
+ "port_idx": 1,
+ "port_poe": True,
+ "portconf_id": "5f3edd2aba4cc806a19f2db2",
+ "up": False,
+ },
+ ],
+ "state": 1,
+ "type": "usw",
+ "version": "4.0.42.10433",
+ }
+ POE_CLIENT = {
+ "hostname": "poe_client",
+ "ip": "1.0.0.1",
+ "is_wired": True,
+ "last_seen": 1562600145,
+ "mac": "00:00:00:00:00:01",
+ "name": "POE Client",
+ "oui": "Producer",
+ }
+
+ config_entry = config_entries.ConfigEntry(
+ version=1,
+ domain=UNIFI_DOMAIN,
+ title="Mock Title",
+ data=ENTRY_CONFIG,
+ source="test",
+ connection_class=config_entries.CONN_CLASS_LOCAL_POLL,
+ system_options={},
+ options={},
+ entry_id=1,
+ )
+
+ registry = er.async_get(hass)
registry.async_get_or_create(
SWITCH_DOMAIN,
UNIFI_DOMAIN,
- f'{POE_SWITCH}-{CLIENT_2["mac"]}',
- suggested_object_id=CLIENT_2["hostname"],
+ f'{POE_SWITCH}-{POE_CLIENT["mac"]}',
+ suggested_object_id=POE_CLIENT["hostname"],
config_entry=config_entry,
)
- controller = await setup_unifi_integration(
+ await setup_unifi_integration(
hass,
+ aioclient_mock,
options={
- CONF_BLOCK_CLIENT: ["random mac"],
CONF_TRACK_CLIENTS: False,
CONF_TRACK_DEVICES: False,
},
- clients_response=[CLIENT_2],
- devices_response=[DEVICE_1],
- clients_all_response=[CLIENT_1],
+ clients_response=[],
+ devices_response=[POE_DEVICE],
+ clients_all_response=[POE_CLIENT],
)
- assert len(controller.mock_requests) == 6
- assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
+ assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
- device_1 = hass.states.get("switch.client_1")
- assert device_1 is not None
+ poe_client = hass.states.get("switch.poe_client")
+ assert poe_client.state == "unavailable" # self.poe_mode is None
diff --git a/tests/components/unifi_direct/test_device_tracker.py b/tests/components/unifi_direct/test_device_tracker.py
index 1f96e3fd84b819..9f594901e9418e 100644
--- a/tests/components/unifi_direct/test_device_tracker.py
+++ b/tests/components/unifi_direct/test_device_tracker.py
@@ -77,9 +77,9 @@ async def test_get_device_name(mock_ssh, hass):
mock_ssh.return_value.before = load_fixture("unifi_direct.txt")
scanner = get_scanner(hass, conf_dict)
devices = scanner.scan_devices()
- assert 23 == len(devices)
- assert "iPhone" == scanner.get_device_name("98:00:c6:56:34:12")
- assert "iPhone" == scanner.get_device_name("98:00:C6:56:34:12")
+ assert len(devices) == 23
+ assert scanner.get_device_name("98:00:c6:56:34:12") == "iPhone"
+ assert scanner.get_device_name("98:00:C6:56:34:12") == "iPhone"
@patch("pexpect.pxssh.pxssh.logout")
diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py
index fd75620f3188b9..7bf19116d938c2 100644
--- a/tests/components/universal/test_media_player.py
+++ b/tests/components/universal/test_media_player.py
@@ -51,6 +51,7 @@ def __init__(self, hass, name):
self._tracks = 12
self._media_image_url = None
self._shuffle = False
+ self._sound_mode = None
self.service_calls = {
"turn_on": mock_service(
@@ -71,6 +72,9 @@ def __init__(self, hass, name):
"media_pause": mock_service(
hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_PAUSE
),
+ "media_stop": mock_service(
+ hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_STOP
+ ),
"media_previous_track": mock_service(
hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_PREVIOUS_TRACK
),
@@ -92,12 +96,21 @@ def __init__(self, hass, name):
"media_play_pause": mock_service(
hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_PLAY_PAUSE
),
+ "select_sound_mode": mock_service(
+ hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOUND_MODE
+ ),
"select_source": mock_service(
hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOURCE
),
+ "toggle": mock_service(
+ hass, media_player.DOMAIN, media_player.SERVICE_TOGGLE
+ ),
"clear_playlist": mock_service(
hass, media_player.DOMAIN, media_player.SERVICE_CLEAR_PLAYLIST
),
+ "repeat_set": mock_service(
+ hass, media_player.DOMAIN, media_player.SERVICE_REPEAT_SET
+ ),
"shuffle_set": mock_service(
hass, media_player.DOMAIN, media_player.SERVICE_SHUFFLE_SET
),
@@ -162,18 +175,30 @@ def media_pause(self):
"""Mock pause."""
self._state = STATE_PAUSED
+ def select_sound_mode(self, sound_mode):
+ """Set the sound mode."""
+ self._sound_mode = sound_mode
+
def select_source(self, source):
"""Set the input source."""
self._source = source
+ def async_toggle(self):
+ """Toggle the power on the media player."""
+ self._state = STATE_OFF if self._state == STATE_ON else STATE_ON
+
def clear_playlist(self):
"""Clear players playlist."""
self._tracks = 0
def set_shuffle(self, shuffle):
- """Clear players playlist."""
+ """Enable/disable shuffle mode."""
self._shuffle = shuffle
+ def set_repeat(self, repeat):
+ """Enable/disable repeat mode."""
+ self._repeat = repeat
+
class TestMediaPlayer(unittest.TestCase):
"""Test the media_player module."""
@@ -205,9 +230,18 @@ def setUp(self): # pylint: disable=invalid-name
self.mock_source_id = f"{input_select.DOMAIN}.source"
self.hass.states.set(self.mock_source_id, "dvd")
+ self.mock_sound_mode_list_id = f"{input_select.DOMAIN}.sound_mode_list"
+ self.hass.states.set(self.mock_sound_mode_list_id, ["music", "movie"])
+
+ self.mock_sound_mode_id = f"{input_select.DOMAIN}.sound_mode"
+ self.hass.states.set(self.mock_sound_mode_id, "music")
+
self.mock_shuffle_switch_id = switch.ENTITY_ID_FORMAT.format("shuffle")
self.hass.states.set(self.mock_shuffle_switch_id, STATE_OFF)
+ self.mock_repeat_switch_id = switch.ENTITY_ID_FORMAT.format("repeat")
+ self.hass.states.set(self.mock_repeat_switch_id, STATE_OFF)
+
self.config_children_only = {
"name": "test",
"platform": "universal",
@@ -230,6 +264,9 @@ def setUp(self): # pylint: disable=invalid-name
"source_list": self.mock_source_list_id,
"state": self.mock_state_switch_id,
"shuffle": self.mock_shuffle_switch_id,
+ "repeat": self.mock_repeat_switch_id,
+ "sound_mode_list": self.mock_sound_mode_list_id,
+ "sound_mode": self.mock_sound_mode_id,
},
}
self.addCleanup(self.tear_down_cleanup)
@@ -296,7 +333,7 @@ def test_config_bad_key(self):
config = {"name": "test", "asdf": 5, "platform": "universal"}
config = validate_config(config)
- assert not ("asdf" in config)
+ assert "asdf" not in config
def test_platform_setup(self):
"""Test platform setup."""
@@ -320,7 +357,7 @@ def add_entities(new_entities):
except MultipleInvalid:
setup_ok = False
assert not setup_ok
- assert 0 == len(entities)
+ assert len(entities) == 0
asyncio.run_coroutine_threadsafe(
universal.async_setup_platform(
@@ -328,8 +365,8 @@ def add_entities(new_entities):
),
self.hass.loop,
).result()
- assert 1 == len(entities)
- assert "test" == entities[0].name
+ assert len(entities) == 1
+ assert entities[0].name == "test"
def test_master_state(self):
"""Test master state property."""
@@ -345,9 +382,9 @@ def test_master_state_with_attrs(self):
ump = universal.UniversalMediaPlayer(self.hass, **config)
- assert STATE_OFF == ump.master_state
+ assert ump.master_state == STATE_OFF
self.hass.states.set(self.mock_state_switch_id, STATE_ON)
- assert STATE_ON == ump.master_state
+ assert ump.master_state == STATE_ON
def test_master_state_with_bad_attrs(self):
"""Test master state property."""
@@ -357,7 +394,7 @@ def test_master_state_with_bad_attrs(self):
ump = universal.UniversalMediaPlayer(self.hass, **config)
- assert STATE_OFF == ump.master_state
+ assert ump.master_state == STATE_OFF
def test_active_child_state(self):
"""Test active child state property."""
@@ -417,7 +454,7 @@ def test_state_children_only(self):
self.mock_mp_1.schedule_update_ha_state()
self.hass.block_till_done()
asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result()
- assert STATE_PLAYING == ump.state
+ assert ump.state == STATE_PLAYING
def test_state_with_children_and_attrs(self):
"""Test media player with children and master state."""
@@ -427,21 +464,21 @@ def test_state_with_children_and_attrs(self):
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"])
asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result()
- assert STATE_OFF == ump.state
+ assert ump.state == STATE_OFF
self.hass.states.set(self.mock_state_switch_id, STATE_ON)
asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result()
- assert STATE_ON == ump.state
+ assert ump.state == STATE_ON
self.mock_mp_1._state = STATE_PLAYING
self.mock_mp_1.schedule_update_ha_state()
self.hass.block_till_done()
asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result()
- assert STATE_PLAYING == ump.state
+ assert ump.state == STATE_PLAYING
self.hass.states.set(self.mock_state_switch_id, STATE_OFF)
asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result()
- assert STATE_OFF == ump.state
+ assert ump.state == STATE_OFF
def test_volume_level(self):
"""Test volume level property."""
@@ -457,13 +494,13 @@ def test_volume_level(self):
self.mock_mp_1.schedule_update_ha_state()
self.hass.block_till_done()
asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result()
- assert 0 == ump.volume_level
+ assert ump.volume_level == 0
self.mock_mp_1._volume_level = 1
self.mock_mp_1.schedule_update_ha_state()
self.hass.block_till_done()
asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result()
- assert 1 == ump.volume_level
+ assert ump.volume_level == 1
def test_media_image_url(self):
"""Test media_image_url property."""
@@ -507,16 +544,38 @@ def test_is_volume_muted_children_only(self):
asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result()
assert ump.is_volume_muted
+ def test_sound_mode_list_children_and_attr(self):
+ """Test sound mode list property w/ children and attrs."""
+ config = validate_config(self.config_children_and_attr)
+
+ ump = universal.UniversalMediaPlayer(self.hass, **config)
+
+ assert ump.sound_mode_list == "['music', 'movie']"
+
+ self.hass.states.set(self.mock_sound_mode_list_id, ["music", "movie", "game"])
+ assert ump.sound_mode_list == "['music', 'movie', 'game']"
+
def test_source_list_children_and_attr(self):
"""Test source list property w/ children and attrs."""
config = validate_config(self.config_children_and_attr)
ump = universal.UniversalMediaPlayer(self.hass, **config)
- assert "['dvd', 'htpc']" == ump.source_list
+ assert ump.source_list == "['dvd', 'htpc']"
self.hass.states.set(self.mock_source_list_id, ["dvd", "htpc", "game"])
- assert "['dvd', 'htpc', 'game']" == ump.source_list
+ assert ump.source_list == "['dvd', 'htpc', 'game']"
+
+ def test_sound_mode_children_and_attr(self):
+ """Test sound modeproperty w/ children and attrs."""
+ config = validate_config(self.config_children_and_attr)
+
+ ump = universal.UniversalMediaPlayer(self.hass, **config)
+
+ assert ump.sound_mode == "music"
+
+ self.hass.states.set(self.mock_sound_mode_id, "movie")
+ assert ump.sound_mode == "movie"
def test_source_children_and_attr(self):
"""Test source property w/ children and attrs."""
@@ -524,10 +583,10 @@ def test_source_children_and_attr(self):
ump = universal.UniversalMediaPlayer(self.hass, **config)
- assert "dvd" == ump.source
+ assert ump.source == "dvd"
self.hass.states.set(self.mock_source_id, "htpc")
- assert "htpc" == ump.source
+ assert ump.source == "htpc"
def test_volume_level_children_and_attr(self):
"""Test volume level property w/ children and attrs."""
@@ -535,10 +594,10 @@ def test_volume_level_children_and_attr(self):
ump = universal.UniversalMediaPlayer(self.hass, **config)
- assert 0 == ump.volume_level
+ assert ump.volume_level == 0
self.hass.states.set(self.mock_volume_id, 100)
- assert 100 == ump.volume_level
+ assert ump.volume_level == 100
def test_is_volume_muted_children_and_attr(self):
"""Test is volume muted property w/ children and attrs."""
@@ -559,14 +618,14 @@ def test_supported_features_children_only(self):
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"])
asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result()
- assert 0 == ump.supported_features
+ assert ump.supported_features == 0
self.mock_mp_1._supported_features = 512
self.mock_mp_1._state = STATE_PLAYING
self.mock_mp_1.schedule_update_ha_state()
self.hass.block_till_done()
asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result()
- assert 512 == ump.supported_features
+ assert ump.supported_features == 512
def test_supported_features_children_and_cmds(self):
"""Test supported media commands with children and attrs."""
@@ -579,8 +638,17 @@ def test_supported_features_children_and_cmds(self):
"volume_down": excmd,
"volume_mute": excmd,
"volume_set": excmd,
+ "select_sound_mode": excmd,
"select_source": excmd,
+ "repeat_set": excmd,
"shuffle_set": excmd,
+ "media_play": excmd,
+ "media_pause": excmd,
+ "media_stop": excmd,
+ "media_next_track": excmd,
+ "media_previous_track": excmd,
+ "toggle": excmd,
+ "clear_playlist": excmd,
}
config = validate_config(config)
@@ -598,13 +666,41 @@ def test_supported_features_children_and_cmds(self):
| universal.SUPPORT_TURN_OFF
| universal.SUPPORT_VOLUME_STEP
| universal.SUPPORT_VOLUME_MUTE
+ | universal.SUPPORT_SELECT_SOUND_MODE
| universal.SUPPORT_SELECT_SOURCE
+ | universal.SUPPORT_REPEAT_SET
| universal.SUPPORT_SHUFFLE_SET
| universal.SUPPORT_VOLUME_SET
+ | universal.SUPPORT_PLAY
+ | universal.SUPPORT_PAUSE
+ | universal.SUPPORT_STOP
+ | universal.SUPPORT_NEXT_TRACK
+ | universal.SUPPORT_PREVIOUS_TRACK
+ | universal.SUPPORT_CLEAR_PLAYLIST
)
assert check_flags == ump.supported_features
+ def test_supported_features_play_pause(self):
+ """Test supported media commands with play_pause function."""
+ config = copy(self.config_children_and_attr)
+ excmd = {"service": "media_player.test", "data": {"entity_id": "test"}}
+ config["commands"] = {"media_play_pause": excmd}
+ config = validate_config(config)
+
+ ump = universal.UniversalMediaPlayer(self.hass, **config)
+ ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"])
+ asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result()
+
+ self.mock_mp_1._state = STATE_PLAYING
+ self.mock_mp_1.schedule_update_ha_state()
+ self.hass.block_till_done()
+ asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result()
+
+ check_flags = universal.SUPPORT_PLAY | universal.SUPPORT_PAUSE
+
+ assert check_flags == ump.supported_features
+
def test_service_call_no_active_child(self):
"""Test a service call to children with no active child."""
config = validate_config(self.config_children_and_attr)
@@ -621,8 +717,8 @@ def test_service_call_no_active_child(self):
asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result()
asyncio.run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result()
- assert 0 == len(self.mock_mp_1.service_calls["turn_off"])
- assert 0 == len(self.mock_mp_2.service_calls["turn_off"])
+ assert len(self.mock_mp_1.service_calls["turn_off"]) == 0
+ assert len(self.mock_mp_2.service_calls["turn_off"]) == 0
def test_service_call_to_child(self):
"""Test service calls that should be routed to a child."""
@@ -638,78 +734,96 @@ def test_service_call_to_child(self):
asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result()
asyncio.run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result()
- assert 1 == len(self.mock_mp_2.service_calls["turn_off"])
+ assert len(self.mock_mp_2.service_calls["turn_off"]) == 1
asyncio.run_coroutine_threadsafe(ump.async_turn_on(), self.hass.loop).result()
- assert 1 == len(self.mock_mp_2.service_calls["turn_on"])
+ assert len(self.mock_mp_2.service_calls["turn_on"]) == 1
asyncio.run_coroutine_threadsafe(
ump.async_mute_volume(True), self.hass.loop
).result()
- assert 1 == len(self.mock_mp_2.service_calls["mute_volume"])
+ assert len(self.mock_mp_2.service_calls["mute_volume"]) == 1
asyncio.run_coroutine_threadsafe(
ump.async_set_volume_level(0.5), self.hass.loop
).result()
- assert 1 == len(self.mock_mp_2.service_calls["set_volume_level"])
+ assert len(self.mock_mp_2.service_calls["set_volume_level"]) == 1
asyncio.run_coroutine_threadsafe(
ump.async_media_play(), self.hass.loop
).result()
- assert 1 == len(self.mock_mp_2.service_calls["media_play"])
+ assert len(self.mock_mp_2.service_calls["media_play"]) == 1
asyncio.run_coroutine_threadsafe(
ump.async_media_pause(), self.hass.loop
).result()
- assert 1 == len(self.mock_mp_2.service_calls["media_pause"])
+ assert len(self.mock_mp_2.service_calls["media_pause"]) == 1
+
+ asyncio.run_coroutine_threadsafe(
+ ump.async_media_stop(), self.hass.loop
+ ).result()
+ assert len(self.mock_mp_2.service_calls["media_stop"]) == 1
asyncio.run_coroutine_threadsafe(
ump.async_media_previous_track(), self.hass.loop
).result()
- assert 1 == len(self.mock_mp_2.service_calls["media_previous_track"])
+ assert len(self.mock_mp_2.service_calls["media_previous_track"]) == 1
asyncio.run_coroutine_threadsafe(
ump.async_media_next_track(), self.hass.loop
).result()
- assert 1 == len(self.mock_mp_2.service_calls["media_next_track"])
+ assert len(self.mock_mp_2.service_calls["media_next_track"]) == 1
asyncio.run_coroutine_threadsafe(
ump.async_media_seek(100), self.hass.loop
).result()
- assert 1 == len(self.mock_mp_2.service_calls["media_seek"])
+ assert len(self.mock_mp_2.service_calls["media_seek"]) == 1
asyncio.run_coroutine_threadsafe(
ump.async_play_media("movie", "batman"), self.hass.loop
).result()
- assert 1 == len(self.mock_mp_2.service_calls["play_media"])
+ assert len(self.mock_mp_2.service_calls["play_media"]) == 1
asyncio.run_coroutine_threadsafe(ump.async_volume_up(), self.hass.loop).result()
- assert 1 == len(self.mock_mp_2.service_calls["volume_up"])
+ assert len(self.mock_mp_2.service_calls["volume_up"]) == 1
asyncio.run_coroutine_threadsafe(
ump.async_volume_down(), self.hass.loop
).result()
- assert 1 == len(self.mock_mp_2.service_calls["volume_down"])
+ assert len(self.mock_mp_2.service_calls["volume_down"]) == 1
asyncio.run_coroutine_threadsafe(
ump.async_media_play_pause(), self.hass.loop
).result()
- assert 1 == len(self.mock_mp_2.service_calls["media_play_pause"])
+ assert len(self.mock_mp_2.service_calls["media_play_pause"]) == 1
+
+ asyncio.run_coroutine_threadsafe(
+ ump.async_select_sound_mode("music"), self.hass.loop
+ ).result()
+ assert len(self.mock_mp_2.service_calls["select_sound_mode"]) == 1
asyncio.run_coroutine_threadsafe(
ump.async_select_source("dvd"), self.hass.loop
).result()
- assert 1 == len(self.mock_mp_2.service_calls["select_source"])
+ assert len(self.mock_mp_2.service_calls["select_source"]) == 1
asyncio.run_coroutine_threadsafe(
ump.async_clear_playlist(), self.hass.loop
).result()
- assert 1 == len(self.mock_mp_2.service_calls["clear_playlist"])
+ assert len(self.mock_mp_2.service_calls["clear_playlist"]) == 1
+
+ asyncio.run_coroutine_threadsafe(
+ ump.async_set_repeat(True), self.hass.loop
+ ).result()
+ assert len(self.mock_mp_2.service_calls["repeat_set"]) == 1
asyncio.run_coroutine_threadsafe(
ump.async_set_shuffle(True), self.hass.loop
).result()
- assert 1 == len(self.mock_mp_2.service_calls["shuffle_set"])
+ assert len(self.mock_mp_2.service_calls["shuffle_set"]) == 1
+
+ asyncio.run_coroutine_threadsafe(ump.async_toggle(), self.hass.loop).result()
+ assert len(self.mock_mp_2.service_calls["toggle"]) == 1
def test_service_call_to_command(self):
"""Test service call to command."""
@@ -729,7 +843,7 @@ def test_service_call_to_command(self):
asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result()
asyncio.run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result()
- assert 1 == len(service)
+ assert len(service) == 1
async def test_state_template(hass):
@@ -758,6 +872,25 @@ async def test_state_template(hass):
assert hass.states.get("media_player.tv").state == STATE_OFF
+async def test_device_class(hass):
+ """Test device_class property."""
+ hass.states.async_set("sensor.test_sensor", "on")
+
+ await async_setup_component(
+ hass,
+ "media_player",
+ {
+ "media_player": {
+ "platform": "universal",
+ "name": "tv",
+ "device_class": "tv",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("media_player.tv").attributes["device_class"] == "tv"
+
+
async def test_invalid_state_template(hass):
"""Test invalid state template sets state to None."""
hass.states.async_set("sensor.test_sensor", "on")
@@ -811,7 +944,7 @@ async def test_master_state_with_template(hass):
await hass.async_start()
await hass.async_block_till_done()
- hass.states.get("media_player.tv").state == STATE_ON
+ assert hass.states.get("media_player.tv").state == STATE_ON
events = []
@@ -823,7 +956,7 @@ async def test_master_state_with_template(hass):
hass.states.async_set("input_boolean.test", STATE_ON, context=context)
await hass.async_block_till_done()
- hass.states.get("media_player.tv").state == STATE_OFF
+ assert hass.states.get("media_player.tv").state == STATE_OFF
assert events[0].context == context
@@ -854,12 +987,12 @@ async def test_reload(hass):
await hass.async_start()
await hass.async_block_till_done()
- hass.states.get("media_player.tv").state == STATE_ON
+ assert hass.states.get("media_player.tv").state == STATE_ON
hass.states.async_set("input_boolean.test", STATE_ON)
await hass.async_block_till_done()
- hass.states.get("media_player.tv").state == STATE_OFF
+ assert hass.states.get("media_player.tv").state == STATE_OFF
hass.states.async_set("media_player.master_bedroom_2", STATE_OFF)
hass.states.async_set(
@@ -887,6 +1020,9 @@ async def test_reload(hass):
assert hass.states.get("media_player.tv") is None
assert hass.states.get("media_player.master_bed_tv").state == "on"
assert hass.states.get("media_player.master_bed_tv").attributes["source"] == "act2"
+ assert (
+ "device_class" not in hass.states.get("media_player.master_bed_tv").attributes
+ )
def _get_fixtures_base_path():
diff --git a/tests/components/upb/test_config_flow.py b/tests/components/upb/test_config_flow.py
index 779c66f08aa02f..1abcaa958b7263 100644
--- a/tests/components/upb/test_config_flow.py
+++ b/tests/components/upb/test_config_flow.py
@@ -43,8 +43,6 @@ async def test_full_upb_flow_with_serial_port(hass):
await setup.async_setup_component(hass, "persistent_notification", {})
with mocked_upb(), patch(
- "homeassistant.components.upb.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.upb.async_setup_entry", return_value=True
) as mock_setup_entry:
flow = await hass.config_entries.flow.async_init(
@@ -69,7 +67,6 @@ async def test_full_upb_flow_with_serial_port(hass):
"host": "serial:///dev/ttyS0:115200",
"file_path": "upb.upe",
}
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
@@ -116,8 +113,6 @@ async def test_form_import(hass):
await setup.async_setup_component(hass, "persistent_notification", {})
with mocked_upb(), patch(
- "homeassistant.components.upb.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.upb.async_setup_entry", return_value=True
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
@@ -131,7 +126,6 @@ async def test_form_import(hass):
assert result["title"] == "UPB"
assert result["data"] == {"host": "tcp://42.4.2.42", "file_path": "upb.upe"}
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
diff --git a/tests/components/updater/test_init.py b/tests/components/updater/test_init.py
index 75ba6c1abd58cf..2b0f494f5f541a 100644
--- a/tests/components/updater/test_init.py
+++ b/tests/components/updater/test_init.py
@@ -1,4 +1,4 @@
-"""The tests for the Updater component."""
+"""The tests for the Updater integration."""
from unittest.mock import patch
import pytest
@@ -12,8 +12,10 @@
NEW_VERSION = "10000.0"
MOCK_VERSION = "10.0"
MOCK_DEV_VERSION = "10.0.dev0"
-MOCK_HUUID = "abcdefg"
-MOCK_RESPONSE = {"version": "0.15", "release-notes": "https://home-assistant.io"}
+MOCK_RESPONSE = {
+ "current_version": "0.15",
+ "release_notes": "https://home-assistant.io",
+}
MOCK_CONFIG = {updater.DOMAIN: {"reporting": True}}
RELEASE_NOTES = "test release notes"
@@ -35,19 +37,8 @@ def mock_get_newest_version_fixture():
yield mock
-@pytest.fixture(name="mock_get_uuid", autouse=True)
-def mock_get_uuid_fixture():
- """Fixture to mock get_uuid."""
- with patch("homeassistant.helpers.instance_id.async_get") as mock:
- yield mock
-
-
-async def test_new_version_shows_entity_true(
- hass, mock_get_uuid, mock_get_newest_version
-):
+async def test_new_version_shows_entity_true(hass, mock_get_newest_version):
"""Test if sensor is true if new version is available."""
- mock_get_uuid.return_value = MOCK_HUUID
-
assert await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}})
await hass.async_block_till_done()
@@ -62,11 +53,8 @@ async def test_new_version_shows_entity_true(
)
-async def test_same_version_shows_entity_false(
- hass, mock_get_uuid, mock_get_newest_version
-):
+async def test_same_version_shows_entity_false(hass, mock_get_newest_version):
"""Test if sensor is false if no new version is available."""
- mock_get_uuid.return_value = MOCK_HUUID
mock_get_newest_version.return_value = (MOCK_VERSION, "")
assert await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}})
@@ -81,60 +69,32 @@ async def test_same_version_shows_entity_false(
assert "release_notes" not in hass.states.get("binary_sensor.updater").attributes
-async def test_disable_reporting(hass, mock_get_uuid, mock_get_newest_version):
+async def test_deprecated_reporting(hass, mock_get_newest_version, caplog):
"""Test we do not gather analytics when disable reporting is active."""
- mock_get_uuid.return_value = MOCK_HUUID
mock_get_newest_version.return_value = (MOCK_VERSION, "")
assert await async_setup_component(
- hass, updater.DOMAIN, {updater.DOMAIN: {"reporting": False}}
+ hass, updater.DOMAIN, {updater.DOMAIN: {"reporting": True}}
)
await hass.async_block_till_done()
- assert hass.states.is_state("binary_sensor.updater", "off")
- await updater.get_newest_version(hass, MOCK_HUUID, MOCK_CONFIG)
- call = mock_get_newest_version.mock_calls[0][1]
- assert call[0] is hass
- assert call[1] is None
-
-
-async def test_get_newest_version_no_analytics_when_no_huuid(hass, aioclient_mock):
- """Test we do not gather analytics when no huuid is passed in."""
- aioclient_mock.post(updater.UPDATER_URL, json=MOCK_RESPONSE)
-
- with patch(
- "homeassistant.helpers.system_info.async_get_system_info", side_effect=Exception
- ):
- res = await updater.get_newest_version(hass, None, False)
- assert res == (MOCK_RESPONSE["version"], MOCK_RESPONSE["release-notes"])
-
-
-async def test_get_newest_version_analytics_when_huuid(hass, aioclient_mock):
- """Test we gather analytics when huuid is passed in."""
- aioclient_mock.post(updater.UPDATER_URL, json=MOCK_RESPONSE)
-
- with patch(
- "homeassistant.helpers.system_info.async_get_system_info",
- return_value={"fake": "bla"},
- ):
- res = await updater.get_newest_version(hass, MOCK_HUUID, False)
- assert res == (MOCK_RESPONSE["version"], MOCK_RESPONSE["release-notes"])
+ assert "deprecated" in caplog.text
async def test_error_fetching_new_version_bad_json(hass, aioclient_mock):
"""Test we handle json error while fetching new version."""
- aioclient_mock.post(updater.UPDATER_URL, text="not json")
+ aioclient_mock.get(updater.UPDATER_URL, text="not json")
with patch(
"homeassistant.helpers.system_info.async_get_system_info",
return_value={"fake": "bla"},
), pytest.raises(UpdateFailed):
- await updater.get_newest_version(hass, MOCK_HUUID, False)
+ await updater.get_newest_version(hass)
async def test_error_fetching_new_version_invalid_response(hass, aioclient_mock):
"""Test we handle response error while fetching new version."""
- aioclient_mock.post(
+ aioclient_mock.get(
updater.UPDATER_URL,
json={
"version": "0.15"
@@ -146,14 +106,13 @@ async def test_error_fetching_new_version_invalid_response(hass, aioclient_mock)
"homeassistant.helpers.system_info.async_get_system_info",
return_value={"fake": "bla"},
), pytest.raises(UpdateFailed):
- await updater.get_newest_version(hass, MOCK_HUUID, False)
+ await updater.get_newest_version(hass)
async def test_new_version_shows_entity_after_hour_hassio(
- hass, mock_get_uuid, mock_get_newest_version
+ hass, mock_get_newest_version
):
"""Test if binary sensor gets updated if new version is available / Hass.io."""
- mock_get_uuid.return_value = MOCK_HUUID
mock_component(hass, "hassio")
hass.data["hassio_core_info"] = {"version_latest": "999.0"}
diff --git a/tests/components/upnp/mock_device.py b/tests/components/upnp/mock_device.py
index a70b3fa02370b2..d6027608137355 100644
--- a/tests/components/upnp/mock_device.py
+++ b/tests/components/upnp/mock_device.py
@@ -16,14 +16,14 @@
class MockDevice(Device):
"""Mock device for Device."""
- def __init__(self, udn):
+ def __init__(self, udn: str) -> None:
"""Initialize mock device."""
igd_device = object()
super().__init__(igd_device)
self._udn = udn
@classmethod
- async def async_create_device(cls, hass, ssdp_location):
+ async def async_create_device(cls, hass, ssdp_location) -> "MockDevice":
"""Return self."""
return cls("UDN")
@@ -52,6 +52,11 @@ def device_type(self) -> str:
"""Get the device type."""
return "urn:schemas-upnp-org:device:InternetGatewayDevice:1"
+ @property
+ def hostname(self) -> str:
+ """Get the hostname."""
+ return "mock-hostname"
+
async def async_get_traffic_data(self) -> Mapping[str, any]:
"""Get traffic data."""
return {
diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py
index f702d770ee6291..77d04381a12c9b 100644
--- a/tests/components/upnp/test_config_flow.py
+++ b/tests/components/upnp/test_config_flow.py
@@ -6,10 +6,12 @@
from homeassistant import config_entries, data_entry_flow
from homeassistant.components import ssdp
from homeassistant.components.upnp.const import (
+ CONFIG_ENTRY_HOSTNAME,
CONFIG_ENTRY_SCAN_INTERVAL,
CONFIG_ENTRY_ST,
CONFIG_ENTRY_UDN,
DEFAULT_SCAN_INTERVAL,
+ DISCOVERY_HOSTNAME,
DISCOVERY_LOCATION,
DISCOVERY_NAME,
DISCOVERY_ST,
@@ -41,6 +43,7 @@ async def test_flow_ssdp_discovery(hass: HomeAssistantType):
DISCOVERY_UDN: mock_device.udn,
DISCOVERY_UNIQUE_ID: mock_device.unique_id,
DISCOVERY_USN: mock_device.usn,
+ DISCOVERY_HOSTNAME: mock_device.hostname,
}
]
with patch.object(
@@ -75,10 +78,11 @@ async def test_flow_ssdp_discovery(hass: HomeAssistantType):
assert result["data"] == {
CONFIG_ENTRY_ST: mock_device.device_type,
CONFIG_ENTRY_UDN: mock_device.udn,
+ CONFIG_ENTRY_HOSTNAME: mock_device.hostname,
}
-async def test_flow_ssdp_discovery_incomplete(hass: HomeAssistantType):
+async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistantType):
"""Test config flow: incomplete discovery through ssdp."""
udn = "uuid:device_1"
location = "dummy"
@@ -89,15 +93,64 @@ async def test_flow_ssdp_discovery_incomplete(hass: HomeAssistantType):
DOMAIN,
context={"source": config_entries.SOURCE_SSDP},
data={
+ ssdp.ATTR_SSDP_LOCATION: location,
ssdp.ATTR_SSDP_ST: mock_device.device_type,
+ ssdp.ATTR_SSDP_USN: mock_device.usn,
# ssdp.ATTR_UPNP_UDN: mock_device.udn, # Not provided.
- ssdp.ATTR_SSDP_LOCATION: location,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "incomplete_discovery"
+async def test_flow_ssdp_discovery_ignored(hass: HomeAssistantType):
+ """Test config flow: discovery through ssdp, but ignored."""
+ udn = "uuid:device_random_1"
+ location = "dummy"
+ mock_device = MockDevice(udn)
+
+ # Existing entry.
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ CONFIG_ENTRY_UDN: "uuid:device_random_2",
+ CONFIG_ENTRY_ST: mock_device.device_type,
+ CONFIG_ENTRY_HOSTNAME: mock_device.hostname,
+ },
+ options={CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL},
+ )
+ config_entry.add_to_hass(hass)
+
+ discoveries = [
+ {
+ DISCOVERY_LOCATION: location,
+ DISCOVERY_NAME: mock_device.name,
+ DISCOVERY_ST: mock_device.device_type,
+ DISCOVERY_UDN: mock_device.udn,
+ DISCOVERY_UNIQUE_ID: mock_device.unique_id,
+ DISCOVERY_USN: mock_device.usn,
+ DISCOVERY_HOSTNAME: mock_device.hostname,
+ }
+ ]
+
+ with patch.object(
+ Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0])
+ ):
+ # Discovered via step ssdp, but ignored.
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_SSDP},
+ data={
+ ssdp.ATTR_SSDP_LOCATION: location,
+ ssdp.ATTR_SSDP_ST: mock_device.device_type,
+ ssdp.ATTR_SSDP_USN: mock_device.usn,
+ ssdp.ATTR_UPNP_UDN: mock_device.udn,
+ },
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "discovery_ignored"
+
+
async def test_flow_user(hass: HomeAssistantType):
"""Test config flow: discovered + configured through user."""
udn = "uuid:device_1"
@@ -111,6 +164,7 @@ async def test_flow_user(hass: HomeAssistantType):
DISCOVERY_UDN: mock_device.udn,
DISCOVERY_UNIQUE_ID: mock_device.unique_id,
DISCOVERY_USN: mock_device.usn,
+ DISCOVERY_HOSTNAME: mock_device.hostname,
}
]
@@ -139,6 +193,7 @@ async def test_flow_user(hass: HomeAssistantType):
assert result["data"] == {
CONFIG_ENTRY_ST: mock_device.device_type,
CONFIG_ENTRY_UDN: mock_device.udn,
+ CONFIG_ENTRY_HOSTNAME: mock_device.hostname,
}
@@ -155,6 +210,7 @@ async def test_flow_import(hass: HomeAssistantType):
DISCOVERY_UDN: mock_device.udn,
DISCOVERY_UNIQUE_ID: mock_device.unique_id,
DISCOVERY_USN: mock_device.usn,
+ DISCOVERY_HOSTNAME: mock_device.hostname,
}
]
@@ -175,6 +231,7 @@ async def test_flow_import(hass: HomeAssistantType):
assert result["data"] == {
CONFIG_ENTRY_ST: mock_device.device_type,
CONFIG_ENTRY_UDN: mock_device.udn,
+ CONFIG_ENTRY_HOSTNAME: mock_device.hostname,
}
@@ -189,6 +246,7 @@ async def test_flow_import_already_configured(hass: HomeAssistantType):
data={
CONFIG_ENTRY_UDN: mock_device.udn,
CONFIG_ENTRY_ST: mock_device.device_type,
+ CONFIG_ENTRY_HOSTNAME: mock_device.hostname,
},
options={CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL},
)
@@ -216,6 +274,7 @@ async def test_flow_import_incomplete(hass: HomeAssistantType):
DISCOVERY_UDN: mock_device.udn,
DISCOVERY_UNIQUE_ID: mock_device.unique_id,
DISCOVERY_USN: mock_device.usn,
+ DISCOVERY_HOSTNAME: mock_device.hostname,
}
]
@@ -243,6 +302,7 @@ async def test_options_flow(hass: HomeAssistantType):
DISCOVERY_UDN: mock_device.udn,
DISCOVERY_UNIQUE_ID: mock_device.unique_id,
DISCOVERY_USN: mock_device.usn,
+ DISCOVERY_HOSTNAME: mock_device.hostname,
}
]
config_entry = MockConfigEntry(
@@ -250,6 +310,7 @@ async def test_options_flow(hass: HomeAssistantType):
data={
CONFIG_ENTRY_UDN: mock_device.udn,
CONFIG_ENTRY_ST: mock_device.device_type,
+ CONFIG_ENTRY_HOSTNAME: mock_device.hostname,
},
options={CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL},
)
diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py
index 26b2f970feda82..086fbd677abed9 100644
--- a/tests/components/upnp/test_init.py
+++ b/tests/components/upnp/test_init.py
@@ -5,6 +5,7 @@
from homeassistant.components.upnp.const import (
CONFIG_ENTRY_ST,
CONFIG_ENTRY_UDN,
+ DISCOVERY_HOSTNAME,
DISCOVERY_LOCATION,
DISCOVERY_NAME,
DISCOVERY_ST,
@@ -35,6 +36,7 @@ async def test_async_setup_entry_default(hass: HomeAssistantType):
DISCOVERY_UDN: mock_device.udn,
DISCOVERY_UNIQUE_ID: mock_device.unique_id,
DISCOVERY_USN: mock_device.usn,
+ DISCOVERY_HOSTNAME: mock_device.hostname,
}
]
entry = MockConfigEntry(
@@ -83,6 +85,7 @@ async def test_sync_setup_entry_multiple_discoveries(hass: HomeAssistantType):
DISCOVERY_UDN: mock_device_0.udn,
DISCOVERY_UNIQUE_ID: mock_device_0.unique_id,
DISCOVERY_USN: mock_device_0.usn,
+ DISCOVERY_HOSTNAME: mock_device_0.hostname,
},
{
DISCOVERY_LOCATION: location_1,
@@ -91,6 +94,7 @@ async def test_sync_setup_entry_multiple_discoveries(hass: HomeAssistantType):
DISCOVERY_UDN: mock_device_1.udn,
DISCOVERY_UNIQUE_ID: mock_device_1.unique_id,
DISCOVERY_USN: mock_device_1.usn,
+ DISCOVERY_HOSTNAME: mock_device_1.hostname,
},
]
entry = MockConfigEntry(
diff --git a/tests/components/uvc/test_camera.py b/tests/components/uvc/test_camera.py
index 00c827b9973bf4..75d0d40c9f3e03 100644
--- a/tests/components/uvc/test_camera.py
+++ b/tests/components/uvc/test_camera.py
@@ -1,381 +1,602 @@
"""The tests for UVC camera module."""
-from datetime import datetime
-import socket
-import unittest
-from unittest import mock
+from datetime import datetime, timedelta, timezone
+from unittest.mock import call, patch
import pytest
import requests
-from uvcclient import camera, nvr
-
-from homeassistant.components.camera import SUPPORT_STREAM
-from homeassistant.components.uvc import camera as uvc
-from homeassistant.exceptions import PlatformNotReady
-from homeassistant.setup import setup_component
-
-from tests.common import get_test_home_assistant
-
-
-class TestUVCSetup(unittest.TestCase):
- """Test the UVC camera platform."""
-
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.hass.stop)
-
- @mock.patch("uvcclient.nvr.UVCRemote")
- @mock.patch.object(uvc, "UnifiVideoCamera")
- def test_setup_full_config(self, mock_uvc, mock_remote):
- """Test the setup with full configuration."""
- config = {
- "platform": "uvc",
- "nvr": "foo",
- "password": "bar",
- "port": 123,
- "key": "secret",
- }
+from uvcclient import camera as camera, nvr
+
+from homeassistant.components.camera import (
+ DEFAULT_CONTENT_TYPE,
+ SERVICE_DISABLE_MOTION,
+ SERVICE_ENABLE_MOTION,
+ STATE_RECORDING,
+ SUPPORT_STREAM,
+ async_get_image,
+ async_get_stream_source,
+)
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
+from homeassistant.setup import async_setup_component
+from homeassistant.util.dt import utcnow
+
+from tests.common import async_fire_time_changed
+
+
+@pytest.fixture(name="mock_remote")
+def mock_remote_fixture(camera_info):
+ """Mock the nvr.UVCRemote class."""
+ with patch("homeassistant.components.uvc.camera.nvr.UVCRemote") as mock_remote:
+
+ def setup(host, port, apikey, ssl=False):
+ """Set instance attributes."""
+ mock_remote.return_value._host = host
+ mock_remote.return_value._port = port
+ mock_remote.return_value._apikey = apikey
+ mock_remote.return_value._ssl = ssl
+ return mock_remote.return_value
+
+ mock_remote.side_effect = setup
+ mock_remote.return_value.get_camera.return_value = camera_info
mock_cameras = [
{"uuid": "one", "name": "Front", "id": "id1"},
{"uuid": "two", "name": "Back", "id": "id2"},
- {"uuid": "three", "name": "Old AirCam", "id": "id3"},
]
-
- def mock_get_camera(uuid):
- """Create a mock camera."""
- if uuid == "id3":
- return {"model": "airCam"}
- return {"model": "UVC"}
-
mock_remote.return_value.index.return_value = mock_cameras
- mock_remote.return_value.get_camera.side_effect = mock_get_camera
mock_remote.return_value.server_version = (3, 2, 0)
+ yield mock_remote
+
+
+@pytest.fixture(name="camera_info")
+def camera_info_fixture():
+ """Mock the camera info of a camera."""
+ return {
+ "model": "UVC",
+ "recordingSettings": {
+ "fullTimeRecordEnabled": True,
+ "motionRecordEnabled": False,
+ },
+ "host": "host-a",
+ "internalHost": "host-b",
+ "username": "admin",
+ "lastRecordingStartTime": 1610070992367,
+ "channels": [
+ {
+ "id": "0",
+ "width": 1920,
+ "height": 1080,
+ "fps": 25,
+ "bitrate": 6000000,
+ "isRtspEnabled": True,
+ "rtspUris": [
+ "rtsp://host-a:7447/uuid_rtspchannel_0",
+ "rtsp://foo:7447/uuid_rtspchannel_0",
+ ],
+ },
+ {
+ "id": "1",
+ "width": 1024,
+ "height": 576,
+ "fps": 15,
+ "bitrate": 1200000,
+ "isRtspEnabled": False,
+ "rtspUris": [
+ "rtsp://host-a:7447/uuid_rtspchannel_1",
+ "rtsp://foo:7447/uuid_rtspchannel_1",
+ ],
+ },
+ ],
+ }
- assert setup_component(self.hass, "camera", {"camera": config})
- self.hass.block_till_done()
-
- assert mock_remote.call_count == 1
- assert mock_remote.call_args == mock.call("foo", 123, "secret", ssl=False)
- mock_uvc.assert_has_calls(
- [
- mock.call(mock_remote.return_value, "id1", "Front", "bar"),
- mock.call(mock_remote.return_value, "id2", "Back", "bar"),
- ]
- )
-
- @mock.patch("uvcclient.nvr.UVCRemote")
- @mock.patch.object(uvc, "UnifiVideoCamera")
- def test_setup_partial_config(self, mock_uvc, mock_remote):
- """Test the setup with partial configuration."""
- config = {"platform": "uvc", "nvr": "foo", "key": "secret"}
- mock_cameras = [
- {"uuid": "one", "name": "Front", "id": "id1"},
- {"uuid": "two", "name": "Back", "id": "id2"},
- ]
- mock_remote.return_value.index.return_value = mock_cameras
- mock_remote.return_value.get_camera.return_value = {"model": "UVC"}
- mock_remote.return_value.server_version = (3, 2, 0)
- assert setup_component(self.hass, "camera", {"camera": config})
- self.hass.block_till_done()
-
- assert mock_remote.call_count == 1
- assert mock_remote.call_args == mock.call("foo", 7080, "secret", ssl=False)
- mock_uvc.assert_has_calls(
- [
- mock.call(mock_remote.return_value, "id1", "Front", "ubnt"),
- mock.call(mock_remote.return_value, "id2", "Back", "ubnt"),
- ]
- )
-
- @mock.patch("uvcclient.nvr.UVCRemote")
- @mock.patch.object(uvc, "UnifiVideoCamera")
- def test_setup_partial_config_v31x(self, mock_uvc, mock_remote):
- """Test the setup with a v3.1.x server."""
- config = {"platform": "uvc", "nvr": "foo", "key": "secret"}
- mock_cameras = [
- {"uuid": "one", "name": "Front", "id": "id1"},
- {"uuid": "two", "name": "Back", "id": "id2"},
- ]
- mock_remote.return_value.index.return_value = mock_cameras
- mock_remote.return_value.get_camera.return_value = {"model": "UVC"}
- mock_remote.return_value.server_version = (3, 1, 3)
-
- assert setup_component(self.hass, "camera", {"camera": config})
- self.hass.block_till_done()
-
- assert mock_remote.call_count == 1
- assert mock_remote.call_args == mock.call("foo", 7080, "secret", ssl=False)
- mock_uvc.assert_has_calls(
- [
- mock.call(mock_remote.return_value, "one", "Front", "ubnt"),
- mock.call(mock_remote.return_value, "two", "Back", "ubnt"),
- ]
- )
-
- @mock.patch.object(uvc, "UnifiVideoCamera")
- def test_setup_incomplete_config(self, mock_uvc):
- """Test the setup with incomplete configuration."""
- assert setup_component(self.hass, "camera", {"platform": "uvc", "nvr": "foo"})
- self.hass.block_till_done()
-
- assert not mock_uvc.called
- assert setup_component(
- self.hass, "camera", {"platform": "uvc", "key": "secret"}
- )
- self.hass.block_till_done()
-
- assert not mock_uvc.called
- assert setup_component(
- self.hass, "camera", {"platform": "uvc", "port": "invalid"}
- )
- self.hass.block_till_done()
-
- assert not mock_uvc.called
-
- @mock.patch.object(uvc, "UnifiVideoCamera")
- @mock.patch("uvcclient.nvr.UVCRemote")
- def setup_nvr_errors_during_indexing(self, error, mock_remote, mock_uvc):
- """Set up test for NVR errors during indexing."""
- config = {"platform": "uvc", "nvr": "foo", "key": "secret"}
- mock_remote.return_value.index.side_effect = error
- assert setup_component(self.hass, "camera", {"camera": config})
- self.hass.block_till_done()
-
- assert not mock_uvc.called
-
- def test_setup_nvr_error_during_indexing_notauthorized(self):
- """Test for error: nvr.NotAuthorized."""
- self.setup_nvr_errors_during_indexing(nvr.NotAuthorized)
-
- def test_setup_nvr_error_during_indexing_nvrerror(self):
- """Test for error: nvr.NvrError."""
- self.setup_nvr_errors_during_indexing(nvr.NvrError)
- pytest.raises(PlatformNotReady)
-
- def test_setup_nvr_error_during_indexing_connectionerror(self):
- """Test for error: requests.exceptions.ConnectionError."""
- self.setup_nvr_errors_during_indexing(requests.exceptions.ConnectionError)
- pytest.raises(PlatformNotReady)
-
- @mock.patch.object(uvc, "UnifiVideoCamera")
- @mock.patch("uvcclient.nvr.UVCRemote.__init__")
- def setup_nvr_errors_during_initialization(self, error, mock_remote, mock_uvc):
- """Set up test for NVR errors during initialization."""
- config = {"platform": "uvc", "nvr": "foo", "key": "secret"}
- mock_remote.return_value = None
- mock_remote.side_effect = error
- assert setup_component(self.hass, "camera", {"camera": config})
- self.hass.block_till_done()
-
- assert not mock_remote.index.called
- assert not mock_uvc.called
-
- def test_setup_nvr_error_during_initialization_notauthorized(self):
- """Test for error: nvr.NotAuthorized."""
- self.setup_nvr_errors_during_initialization(nvr.NotAuthorized)
-
- def test_setup_nvr_error_during_initialization_nvrerror(self):
- """Test for error: nvr.NvrError."""
- self.setup_nvr_errors_during_initialization(nvr.NvrError)
- pytest.raises(PlatformNotReady)
-
- def test_setup_nvr_error_during_initialization_connectionerror(self):
- """Test for error: requests.exceptions.ConnectionError."""
- self.setup_nvr_errors_during_initialization(requests.exceptions.ConnectionError)
- pytest.raises(PlatformNotReady)
-
-
-class TestUVC(unittest.TestCase):
- """Test class for UVC."""
-
- def setup_method(self, method):
- """Set up the mock camera."""
- self.nvr = mock.MagicMock()
- self.uuid = "uuid"
- self.name = "name"
- self.password = "seekret"
- self.uvc = uvc.UnifiVideoCamera(self.nvr, self.uuid, self.name, self.password)
- self.nvr.get_camera.return_value = {
- "model": "UVC Fake",
- "uuid": "06e3ff29-8048-31c2-8574-0852d1bd0e03",
- "recordingSettings": {
- "fullTimeRecordEnabled": True,
- "motionRecordEnabled": False,
- },
- "host": "host-a",
- "internalHost": "host-b",
- "username": "admin",
- "lastRecordingStartTime": 1610070992367,
- "channels": [
- {
- "id": "0",
- "width": 1920,
- "height": 1080,
- "fps": 25,
- "bitrate": 6000000,
- "isRtspEnabled": True,
- "rtspUris": [
- "rtsp://host-a:7447/uuid_rtspchannel_0",
- "rtsp://foo:7447/uuid_rtspchannel_0",
- ],
- },
- {
- "id": "1",
- "width": 1024,
- "height": 576,
- "fps": 15,
- "bitrate": 1200000,
- "isRtspEnabled": False,
- "rtspUris": [
- "rtsp://host-a:7447/uuid_rtspchannel_1",
- "rtsp://foo:7447/uuid_rtspchannel_1",
- ],
- },
- ],
- }
- self.nvr.server_version = (3, 2, 0)
- self.uvc.update()
-
- def test_properties(self):
- """Test the properties."""
- assert self.name == self.uvc.name
- assert self.uvc.is_recording
- assert "Ubiquiti" == self.uvc.brand
- assert "UVC Fake" == self.uvc.model
- assert SUPPORT_STREAM == self.uvc.supported_features
- assert "uuid" == self.uvc.unique_id
-
- def test_motion_recording_mode_properties(self):
- """Test the properties."""
- self.nvr.get_camera.return_value["recordingSettings"][
- "fullTimeRecordEnabled"
- ] = False
- self.nvr.get_camera.return_value["recordingSettings"][
- "motionRecordEnabled"
- ] = True
- assert not self.uvc.is_recording
- assert (
- datetime(2021, 1, 8, 1, 56, 32, 367000)
- == self.uvc.state_attributes["last_recording_start_time"]
- )
-
- self.nvr.get_camera.return_value["recordingIndicator"] = "DISABLED"
- assert not self.uvc.is_recording
-
- self.nvr.get_camera.return_value["recordingIndicator"] = "MOTION_INPROGRESS"
- assert self.uvc.is_recording
-
- self.nvr.get_camera.return_value["recordingIndicator"] = "MOTION_FINISHED"
- assert self.uvc.is_recording
-
- def test_stream(self):
- """Test the RTSP stream URI."""
- stream_source = yield from self.uvc.stream_source()
- assert stream_source == "rtsp://foo:7447/uuid_rtspchannel_0"
-
- @mock.patch("uvcclient.store.get_info_store")
- @mock.patch("uvcclient.camera.UVCCameraClientV320")
- def test_login(self, mock_camera, mock_store):
- """Test the login."""
- self.uvc._login()
- assert mock_camera.call_count == 1
- assert mock_camera.call_args == mock.call("host-a", "admin", "seekret")
- assert mock_camera.return_value.login.call_count == 1
- assert mock_camera.return_value.login.call_args == mock.call()
-
- @mock.patch("uvcclient.store.get_info_store")
- @mock.patch("uvcclient.camera.UVCCameraClient")
- def test_login_v31x(self, mock_camera, mock_store):
- """Test login with v3.1.x server."""
- self.nvr.server_version = (3, 1, 3)
- self.uvc._login()
- assert mock_camera.call_count == 1
- assert mock_camera.call_args == mock.call("host-a", "admin", "seekret")
- assert mock_camera.return_value.login.call_count == 1
- assert mock_camera.return_value.login.call_args == mock.call()
-
- @mock.patch("uvcclient.store.get_info_store")
- @mock.patch("uvcclient.camera.UVCCameraClientV320")
- def test_login_tries_both_addrs_and_caches(self, mock_camera, mock_store):
- """Test the login tries."""
- responses = [0]
-
- def mock_login(*a):
- """Mock login."""
- try:
- responses.pop(0)
- raise OSError
- except IndexError:
- pass
-
- mock_store.return_value.get_camera_password.return_value = None
- mock_camera.return_value.login.side_effect = mock_login
- self.uvc._login()
- assert 2 == mock_camera.call_count
- assert "host-b" == self.uvc._connect_addr
-
- mock_camera.reset_mock()
- self.uvc._login()
- assert mock_camera.call_count == 1
- assert mock_camera.call_args == mock.call("host-b", "admin", "seekret")
- assert mock_camera.return_value.login.call_count == 1
- assert mock_camera.return_value.login.call_args == mock.call()
-
- @mock.patch("uvcclient.store.get_info_store")
- @mock.patch("uvcclient.camera.UVCCameraClientV320")
- def test_login_fails_both_properly(self, mock_camera, mock_store):
- """Test if login fails properly."""
- mock_camera.return_value.login.side_effect = socket.error
- assert self.uvc._login() is None
- assert self.uvc._connect_addr is None
-
- def test_camera_image_tries_login_bails_on_failure(self):
- """Test retrieving failure."""
- with mock.patch.object(self.uvc, "_login") as mock_login:
- mock_login.return_value = False
- assert self.uvc.camera_image() is None
- assert mock_login.call_count == 1
- assert mock_login.call_args == mock.call()
-
- def test_camera_image_logged_in(self):
- """Test the login state."""
- self.uvc._camera = mock.MagicMock()
- assert self.uvc._camera.get_snapshot.return_value == self.uvc.camera_image()
-
- def test_camera_image_error(self):
- """Test the camera image error."""
- self.uvc._camera = mock.MagicMock()
- self.uvc._camera.get_snapshot.side_effect = camera.CameraConnectError
- assert self.uvc.camera_image() is None
-
- def test_camera_image_reauths(self):
- """Test the re-authentication."""
- responses = [0]
-
- def mock_snapshot():
- """Mock snapshot."""
- try:
- responses.pop()
- raise camera.CameraAuthError()
- except IndexError:
- pass
- return "image"
-
- self.uvc._camera = mock.MagicMock()
- self.uvc._camera.get_snapshot.side_effect = mock_snapshot
- with mock.patch.object(self.uvc, "_login") as mock_login:
- assert "image" == self.uvc.camera_image()
- assert mock_login.call_count == 1
- assert mock_login.call_args == mock.call()
- assert [] == responses
-
- def test_camera_image_reauths_only_once(self):
- """Test if the re-authentication only happens once."""
- self.uvc._camera = mock.MagicMock()
- self.uvc._camera.get_snapshot.side_effect = camera.CameraAuthError
- with mock.patch.object(self.uvc, "_login") as mock_login:
- with pytest.raises(camera.CameraAuthError):
- self.uvc.camera_image()
- assert mock_login.call_count == 1
- assert mock_login.call_args == mock.call()
+@pytest.fixture(name="camera_v320")
+def camera_v320_fixture():
+ """Mock the v320 camera."""
+ with patch(
+ "homeassistant.components.uvc.camera.uvc_camera.UVCCameraClientV320"
+ ) as camera:
+ camera.return_value.get_snapshot.return_value = "test_image"
+ yield camera
+
+
+@pytest.fixture(name="camera_v313")
+def camera_v313_fixture():
+ """Mock the v320 camera."""
+ with patch(
+ "homeassistant.components.uvc.camera.uvc_camera.UVCCameraClient"
+ ) as camera:
+ camera.return_value.get_snapshot.return_value = "test_image"
+ yield camera
+
+
+async def test_setup_full_config(hass, mock_remote, camera_info):
+ """Test the setup with full configuration."""
+ config = {
+ "platform": "uvc",
+ "nvr": "foo",
+ "password": "bar",
+ "port": 123,
+ "key": "secret",
+ }
+
+ def mock_get_camera(uuid):
+ """Create a mock camera."""
+ if uuid == "id3":
+ camera_info["model"] = "airCam"
+
+ return camera_info
+
+ mock_remote.return_value.index.return_value.append(
+ {"uuid": "three", "name": "Old AirCam", "id": "id3"}
+ )
+ mock_remote.return_value.get_camera.side_effect = mock_get_camera
+
+ assert await async_setup_component(hass, "camera", {"camera": config})
+ await hass.async_block_till_done()
+
+ assert mock_remote.call_count == 1
+ assert mock_remote.call_args == call("foo", 123, "secret", ssl=False)
+
+ camera_states = hass.states.async_all("camera")
+
+ assert len(camera_states) == 2
+
+ state = hass.states.get("camera.front")
+
+ assert state
+ assert state.name == "Front"
+
+ state = hass.states.get("camera.back")
+
+ assert state
+ assert state.name == "Back"
+
+ entity_registry = async_get_entity_registry(hass)
+ entity_entry = entity_registry.async_get("camera.front")
+
+ assert entity_entry.unique_id == "id1"
+
+ entity_entry = entity_registry.async_get("camera.back")
+
+ assert entity_entry.unique_id == "id2"
+
+
+async def test_setup_partial_config(hass, mock_remote):
+ """Test the setup with partial configuration."""
+ config = {"platform": "uvc", "nvr": "foo", "key": "secret"}
+
+ assert await async_setup_component(hass, "camera", {"camera": config})
+ await hass.async_block_till_done()
+
+ assert mock_remote.call_count == 1
+ assert mock_remote.call_args == call("foo", 7080, "secret", ssl=False)
+
+ camera_states = hass.states.async_all("camera")
+
+ assert len(camera_states) == 2
+
+ state = hass.states.get("camera.front")
+
+ assert state
+ assert state.name == "Front"
+
+ state = hass.states.get("camera.back")
+
+ assert state
+ assert state.name == "Back"
+
+ entity_registry = async_get_entity_registry(hass)
+ entity_entry = entity_registry.async_get("camera.front")
+
+ assert entity_entry.unique_id == "id1"
+
+ entity_entry = entity_registry.async_get("camera.back")
+
+ assert entity_entry.unique_id == "id2"
+
+
+async def test_setup_partial_config_v31x(hass, mock_remote):
+ """Test the setup with a v3.1.x server."""
+ config = {"platform": "uvc", "nvr": "foo", "key": "secret"}
+ mock_remote.return_value.server_version = (3, 1, 3)
+
+ assert await async_setup_component(hass, "camera", {"camera": config})
+ await hass.async_block_till_done()
+
+ assert mock_remote.call_count == 1
+ assert mock_remote.call_args == call("foo", 7080, "secret", ssl=False)
+
+ camera_states = hass.states.async_all("camera")
+
+ assert len(camera_states) == 2
+
+ state = hass.states.get("camera.front")
+
+ assert state
+ assert state.name == "Front"
+
+ state = hass.states.get("camera.back")
+
+ assert state
+ assert state.name == "Back"
+
+ entity_registry = async_get_entity_registry(hass)
+ entity_entry = entity_registry.async_get("camera.front")
+
+ assert entity_entry.unique_id == "one"
+
+ entity_entry = entity_registry.async_get("camera.back")
+
+ assert entity_entry.unique_id == "two"
+
+
+@pytest.mark.parametrize(
+ "config",
+ [
+ {"platform": "uvc", "nvr": "foo"},
+ {"platform": "uvc", "key": "secret"},
+ {"platform": "uvc", "nvr": "foo", "key": "secret", "port": "invalid"},
+ ],
+)
+async def test_setup_incomplete_config(hass, mock_remote, config):
+ """Test the setup with incomplete or invalid configuration."""
+ assert await async_setup_component(hass, "camera", config)
+ await hass.async_block_till_done()
+
+ camera_states = hass.states.async_all("camera")
+
+ assert not camera_states
+
+
+@pytest.mark.parametrize(
+ "error, ready_states",
+ [
+ (nvr.NotAuthorized, 0),
+ (nvr.NvrError, 2),
+ (requests.exceptions.ConnectionError, 2),
+ ],
+)
+async def test_setup_nvr_errors_during_indexing(hass, mock_remote, error, ready_states):
+ """Set up test for NVR errors during indexing."""
+ config = {"platform": "uvc", "nvr": "foo", "key": "secret"}
+ now = utcnow()
+ mock_remote.return_value.index.side_effect = error
+ assert await async_setup_component(hass, "camera", {"camera": config})
+ await hass.async_block_till_done()
+
+ camera_states = hass.states.async_all("camera")
+
+ assert not camera_states
+
+ # resolve the error
+ mock_remote.return_value.index.side_effect = None
+
+ async_fire_time_changed(hass, now + timedelta(seconds=31))
+ await hass.async_block_till_done()
+
+ camera_states = hass.states.async_all("camera")
+
+ assert len(camera_states) == ready_states
+
+
+@pytest.mark.parametrize(
+ "error, ready_states",
+ [
+ (nvr.NotAuthorized, 0),
+ (nvr.NvrError, 2),
+ (requests.exceptions.ConnectionError, 2),
+ ],
+)
+async def test_setup_nvr_errors_during_initialization(
+ hass, mock_remote, error, ready_states
+):
+ """Set up test for NVR errors during initialization."""
+ config = {"platform": "uvc", "nvr": "foo", "key": "secret"}
+ now = utcnow()
+ mock_remote.side_effect = error
+ assert await async_setup_component(hass, "camera", {"camera": config})
+ await hass.async_block_till_done()
+
+ assert not mock_remote.index.called
+
+ camera_states = hass.states.async_all("camera")
+
+ assert not camera_states
+
+ # resolve the error
+ mock_remote.side_effect = None
+
+ async_fire_time_changed(hass, now + timedelta(seconds=31))
+ await hass.async_block_till_done()
+
+ camera_states = hass.states.async_all("camera")
+
+ assert len(camera_states) == ready_states
+
+
+async def test_properties(hass, mock_remote):
+ """Test the properties."""
+ config = {"platform": "uvc", "nvr": "foo", "key": "secret"}
+ assert await async_setup_component(hass, "camera", {"camera": config})
+ await hass.async_block_till_done()
+
+ camera_states = hass.states.async_all("camera")
+
+ assert len(camera_states) == 2
+
+ state = hass.states.get("camera.front")
+
+ assert state
+ assert state.name == "Front"
+ assert state.state == STATE_RECORDING
+ assert state.attributes["brand"] == "Ubiquiti"
+ assert state.attributes["model_name"] == "UVC"
+ assert state.attributes["supported_features"] == SUPPORT_STREAM
+
+
+async def test_motion_recording_mode_properties(hass, mock_remote):
+ """Test the properties."""
+ config = {"platform": "uvc", "nvr": "foo", "key": "secret"}
+ now = utcnow()
+ assert await async_setup_component(hass, "camera", {"camera": config})
+ await hass.async_block_till_done()
+
+ state = hass.states.get("camera.front")
+
+ assert state
+ assert state.state == STATE_RECORDING
+
+ mock_remote.return_value.get_camera.return_value["recordingSettings"][
+ "fullTimeRecordEnabled"
+ ] = False
+ mock_remote.return_value.get_camera.return_value["recordingSettings"][
+ "motionRecordEnabled"
+ ] = True
+
+ async_fire_time_changed(hass, now + timedelta(seconds=31))
+ await hass.async_block_till_done()
+
+ state = hass.states.get("camera.front")
+
+ assert state
+ assert state.state != STATE_RECORDING
+ assert state.attributes["last_recording_start_time"] == datetime(
+ 2021, 1, 8, 1, 56, 32, 367000, tzinfo=timezone.utc
+ )
+
+ mock_remote.return_value.get_camera.return_value["recordingIndicator"] = "DISABLED"
+
+ async_fire_time_changed(hass, now + timedelta(seconds=61))
+ await hass.async_block_till_done()
+
+ state = hass.states.get("camera.front")
+
+ assert state
+ assert state.state != STATE_RECORDING
+
+ mock_remote.return_value.get_camera.return_value[
+ "recordingIndicator"
+ ] = "MOTION_INPROGRESS"
+
+ async_fire_time_changed(hass, now + timedelta(seconds=91))
+ await hass.async_block_till_done()
+
+ state = hass.states.get("camera.front")
+
+ assert state
+ assert state.state == STATE_RECORDING
+
+ mock_remote.return_value.get_camera.return_value[
+ "recordingIndicator"
+ ] = "MOTION_FINISHED"
+
+ async_fire_time_changed(hass, now + timedelta(seconds=121))
+ await hass.async_block_till_done()
+
+ state = hass.states.get("camera.front")
+
+ assert state
+ assert state.state == STATE_RECORDING
+
+
+async def test_stream(hass, mock_remote):
+ """Test the RTSP stream URI."""
+ config = {"platform": "uvc", "nvr": "foo", "key": "secret"}
+ assert await async_setup_component(hass, "camera", {"camera": config})
+ await hass.async_block_till_done()
+
+ stream_source = await async_get_stream_source(hass, "camera.front")
+
+ assert stream_source == "rtsp://foo:7447/uuid_rtspchannel_0"
+
+
+async def test_login(hass, mock_remote, camera_v320):
+ """Test the login."""
+ config = {"platform": "uvc", "nvr": "foo", "key": "secret"}
+ assert await async_setup_component(hass, "camera", {"camera": config})
+ await hass.async_block_till_done()
+
+ image = await async_get_image(hass, "camera.front")
+
+ assert camera_v320.call_count == 1
+ assert camera_v320.call_args == call("host-a", "admin", "ubnt")
+ assert camera_v320.return_value.login.call_count == 1
+ assert image.content_type == DEFAULT_CONTENT_TYPE
+ assert image.content == "test_image"
+
+
+async def test_login_v31x(hass, mock_remote, camera_v313):
+ """Test login with v3.1.x server."""
+ mock_remote.return_value.server_version = (3, 1, 3)
+ config = {"platform": "uvc", "nvr": "foo", "key": "secret"}
+ assert await async_setup_component(hass, "camera", {"camera": config})
+ await hass.async_block_till_done()
+
+ image = await async_get_image(hass, "camera.front")
+
+ assert camera_v313.call_count == 1
+ assert camera_v313.call_args == call("host-a", "admin", "ubnt")
+ assert camera_v313.return_value.login.call_count == 1
+ assert image.content_type == DEFAULT_CONTENT_TYPE
+ assert image.content == "test_image"
+
+
+@pytest.mark.parametrize(
+ "error", [OSError, camera.CameraConnectError, camera.CameraAuthError]
+)
+async def test_login_tries_both_addrs_and_caches(hass, mock_remote, camera_v320, error):
+ """Test the login tries."""
+ responses = [0]
+
+ def mock_login(*a):
+ """Mock login."""
+ try:
+ responses.pop(0)
+ raise error
+ except IndexError:
+ pass
+
+ snapshots = [0]
+
+ def mock_snapshots(*a):
+ """Mock get snapshots."""
+ try:
+ snapshots.pop(0)
+ raise camera.CameraAuthError()
+ except IndexError:
+ pass
+ return "test_image"
+
+ camera_v320.return_value.login.side_effect = mock_login
+
+ config = {"platform": "uvc", "nvr": "foo", "key": "secret"}
+ assert await async_setup_component(hass, "camera", {"camera": config})
+ await hass.async_block_till_done()
+
+ image = await async_get_image(hass, "camera.front")
+
+ assert camera_v320.call_count == 2
+ assert camera_v320.call_args == call("host-b", "admin", "ubnt")
+ assert image.content_type == DEFAULT_CONTENT_TYPE
+ assert image.content == "test_image"
+
+ camera_v320.reset_mock()
+ camera_v320.return_value.get_snapshot.side_effect = mock_snapshots
+
+ image = await async_get_image(hass, "camera.front")
+
+ assert camera_v320.call_count == 1
+ assert camera_v320.call_args == call("host-b", "admin", "ubnt")
+ assert camera_v320.return_value.login.call_count == 1
+ assert image.content_type == DEFAULT_CONTENT_TYPE
+ assert image.content == "test_image"
+
+
+async def test_login_fails_both_properly(hass, mock_remote, camera_v320):
+ """Test if login fails properly."""
+ camera_v320.return_value.login.side_effect = OSError
+ config = {"platform": "uvc", "nvr": "foo", "key": "secret"}
+ assert await async_setup_component(hass, "camera", {"camera": config})
+ await hass.async_block_till_done()
+
+ with pytest.raises(HomeAssistantError):
+ await async_get_image(hass, "camera.front")
+
+ assert camera_v320.return_value.get_snapshot.call_count == 0
+
+
+@pytest.mark.parametrize(
+ "source_error, raised_error, snapshot_calls",
+ [
+ (camera.CameraConnectError, HomeAssistantError, 1),
+ (camera.CameraAuthError, camera.CameraAuthError, 2),
+ ],
+)
+async def test_camera_image_error(
+ hass, mock_remote, camera_v320, source_error, raised_error, snapshot_calls
+):
+ """Test the camera image error."""
+ camera_v320.return_value.get_snapshot.side_effect = source_error
+ config = {"platform": "uvc", "nvr": "foo", "key": "secret"}
+ assert await async_setup_component(hass, "camera", {"camera": config})
+ await hass.async_block_till_done()
+
+ with pytest.raises(raised_error):
+ await async_get_image(hass, "camera.front")
+
+ assert camera_v320.return_value.get_snapshot.call_count == snapshot_calls
+
+
+async def test_enable_disable_motion_detection(hass, mock_remote, camera_info):
+ """Test enable and disable motion detection."""
+
+ def set_recordmode(uuid, mode):
+ """Set record mode."""
+ motion_record_enabled = mode == "motion"
+ camera_info["recordingSettings"]["motionRecordEnabled"] = motion_record_enabled
+
+ mock_remote.return_value.set_recordmode.side_effect = set_recordmode
+ config = {"platform": "uvc", "nvr": "foo", "key": "secret"}
+ assert await async_setup_component(hass, "camera", {"camera": config})
+ await hass.async_block_till_done()
+
+ state = hass.states.get("camera.front")
+
+ assert state
+ assert "motion_detection" not in state.attributes
+
+ await hass.services.async_call(
+ "camera", SERVICE_ENABLE_MOTION, {"entity_id": "camera.front"}, True
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("camera.front")
+
+ assert state
+ assert state.attributes["motion_detection"]
+
+ await hass.services.async_call(
+ "camera", SERVICE_DISABLE_MOTION, {"entity_id": "camera.front"}, True
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("camera.front")
+
+ assert state
+ assert "motion_detection" not in state.attributes
+
+ mock_remote.return_value.set_recordmode.side_effect = nvr.NvrError
+
+ await hass.services.async_call(
+ "camera", SERVICE_ENABLE_MOTION, {"entity_id": "camera.front"}, True
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("camera.front")
+
+ assert state
+ assert "motion_detection" not in state.attributes
+
+ mock_remote.return_value.set_recordmode.side_effect = set_recordmode
+
+ await hass.services.async_call(
+ "camera", SERVICE_ENABLE_MOTION, {"entity_id": "camera.front"}, True
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("camera.front")
+
+ assert state
+ assert state.attributes["motion_detection"]
+
+ mock_remote.return_value.set_recordmode.side_effect = nvr.NvrError
+
+ await hass.services.async_call(
+ "camera", SERVICE_DISABLE_MOTION, {"entity_id": "camera.front"}, True
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("camera.front")
+
+ assert state
+ assert state.attributes["motion_detection"]
diff --git a/tests/components/vacuum/test_device_action.py b/tests/components/vacuum/test_device_action.py
index 3edeaba2a4115f..aa5dc1786f7029 100644
--- a/tests/components/vacuum/test_device_action.py
+++ b/tests/components/vacuum/test_device_action.py
@@ -14,7 +14,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/vacuum/test_device_condition.py b/tests/components/vacuum/test_device_condition.py
index 3dc7a628741247..84a25d183b8512 100644
--- a/tests/components/vacuum/test_device_condition.py
+++ b/tests/components/vacuum/test_device_condition.py
@@ -19,7 +19,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py
index e3f615891e6cce..495ba02967b32f 100644
--- a/tests/components/vacuum/test_device_trigger.py
+++ b/tests/components/vacuum/test_device_trigger.py
@@ -1,20 +1,25 @@
"""The tests for Vacuum device triggers."""
+from datetime import timedelta
+
import pytest
import homeassistant.components.automation as automation
from homeassistant.components.vacuum import DOMAIN, STATE_CLEANING, STATE_DOCKED
from homeassistant.helpers import device_registry
from homeassistant.setup import async_setup_component
+import homeassistant.util.dt as dt_util
from tests.common import (
MockConfigEntry,
assert_lists_same,
+ async_fire_time_changed,
+ async_get_device_automation_capabilities,
async_get_device_automations,
async_mock_service,
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
@@ -64,6 +69,29 @@ async def test_get_triggers(hass, device_reg, entity_reg):
assert_lists_same(triggers, expected_triggers)
+async def test_get_trigger_capabilities(hass, device_reg, entity_reg):
+ """Test we get the expected capabilities from a vacuum device."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert len(triggers) == 2
+ for trigger in triggers:
+ capabilities = await async_get_device_automation_capabilities(
+ hass, "trigger", trigger
+ )
+ assert capabilities == {
+ "extra_fields": [
+ {"name": "for", "optional": True, "type": "positive_time_period_dict"}
+ ]
+ }
+
+
async def test_if_fires_on_state_change(hass, calls):
"""Test for turn_on and turn_off triggers firing."""
hass.states.async_set("vacuum.entity", STATE_DOCKED)
@@ -130,3 +158,58 @@ async def test_if_fires_on_state_change(hass, calls):
assert (
calls[1].data["some"] == "docked - device - vacuum.entity - cleaning - docked"
)
+
+
+async def test_if_fires_on_state_change_with_for(hass, calls):
+ """Test for triggers firing with delay."""
+ entity_id = f"{DOMAIN}.entity"
+ hass.states.async_set(entity_id, STATE_DOCKED)
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": entity_id,
+ "type": "cleaning",
+ "for": {"seconds": 5},
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "turn_off {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(
+ (
+ "platform",
+ "entity_id",
+ "from_state.state",
+ "to_state.state",
+ "for",
+ )
+ )
+ },
+ },
+ }
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(entity_id).state == STATE_DOCKED
+ assert len(calls) == 0
+
+ hass.states.async_set(entity_id, STATE_CLEANING)
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ await hass.async_block_till_done()
+ assert (
+ calls[0].data["some"]
+ == f"turn_off device - {entity_id} - docked - cleaning - 0:00:05"
+ )
diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py
index 43ef154b5884dd..ae3c0a1a1dedba 100644
--- a/tests/components/vera/common.py
+++ b/tests/components/vera/common.py
@@ -1,6 +1,8 @@
"""Common code for tests."""
+from __future__ import annotations
+
from enum import Enum
-from typing import Callable, Dict, NamedTuple, Tuple
+from typing import Callable, NamedTuple
from unittest.mock import MagicMock
import pyvera as pv
@@ -29,7 +31,7 @@ class ControllerData(NamedTuple):
class ComponentData(NamedTuple):
"""Test data about the vera component."""
- controller_data: Tuple[ControllerData]
+ controller_data: tuple[ControllerData]
class ConfigSource(Enum):
@@ -43,12 +45,12 @@ class ConfigSource(Enum):
class ControllerConfig(NamedTuple):
"""Test config for mocking a vera controller."""
- config: Dict
- options: Dict
+ config: dict
+ options: dict
config_source: ConfigSource
serial_number: str
- devices: Tuple[pv.VeraDevice, ...]
- scenes: Tuple[pv.VeraScene, ...]
+ devices: tuple[pv.VeraDevice, ...]
+ scenes: tuple[pv.VeraScene, ...]
setup_callback: SetupCallback
legacy_entity_unique_id: bool
@@ -58,8 +60,8 @@ def new_simple_controller_config(
options: dict = None,
config_source=ConfigSource.CONFIG_FLOW,
serial_number="1111",
- devices: Tuple[pv.VeraDevice, ...] = (),
- scenes: Tuple[pv.VeraScene, ...] = (),
+ devices: tuple[pv.VeraDevice, ...] = (),
+ scenes: tuple[pv.VeraScene, ...] = (),
setup_callback: SetupCallback = None,
legacy_entity_unique_id=False,
) -> ControllerConfig:
@@ -87,7 +89,7 @@ async def configure_component(
self,
hass: HomeAssistant,
controller_config: ControllerConfig = None,
- controller_configs: Tuple[ControllerConfig] = (),
+ controller_configs: tuple[ControllerConfig] = (),
) -> ComponentData:
"""Configure the component with multiple specific mock data."""
configs = list(controller_configs)
@@ -116,7 +118,7 @@ async def _configure_component(
if controller_config.legacy_entity_unique_id:
component_config[CONF_LEGACY_UNIQUE_ID] = True
- controller = MagicMock(spec=pv.VeraController) # type: pv.VeraController
+ controller: pv.VeraController = MagicMock(spec=pv.VeraController)
controller.base_url = component_config.get(CONF_CONTROLLER)
controller.register = MagicMock()
controller.start = MagicMock()
diff --git a/tests/components/vera/conftest.py b/tests/components/vera/conftest.py
index da0272077487f2..e5d2dac1dbf2f9 100644
--- a/tests/components/vera/conftest.py
+++ b/tests/components/vera/conftest.py
@@ -5,7 +5,7 @@
from .common import ComponentFactory
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
@pytest.fixture()
diff --git a/tests/components/vera/test_binary_sensor.py b/tests/components/vera/test_binary_sensor.py
index 1bcb8d1a183496..58b8ed7f4614dc 100644
--- a/tests/components/vera/test_binary_sensor.py
+++ b/tests/components/vera/test_binary_sensor.py
@@ -12,8 +12,9 @@ async def test_binary_sensor(
hass: HomeAssistant, vera_component_factory: ComponentFactory
) -> None:
"""Test function."""
- vera_device = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
+ vera_device: pv.VeraBinarySensor = MagicMock(spec=pv.VeraBinarySensor)
vera_device.device_id = 1
+ vera_device.comm_failure = False
vera_device.vera_device_id = vera_device.device_id
vera_device.name = "dev1"
vera_device.is_tripped = False
diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py
index 076b51997a0e63..4b301a1c96d96d 100644
--- a/tests/components/vera/test_climate.py
+++ b/tests/components/vera/test_climate.py
@@ -20,9 +20,10 @@ async def test_climate(
hass: HomeAssistant, vera_component_factory: ComponentFactory
) -> None:
"""Test function."""
- vera_device = MagicMock(spec=pv.VeraThermostat) # type: pv.VeraThermostat
+ vera_device: pv.VeraThermostat = MagicMock(spec=pv.VeraThermostat)
vera_device.device_id = 1
vera_device.vera_device_id = vera_device.device_id
+ vera_device.comm_failure = False
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_THERMOSTAT
vera_device.power = 10
@@ -130,9 +131,10 @@ async def test_climate_f(
hass: HomeAssistant, vera_component_factory: ComponentFactory
) -> None:
"""Test function."""
- vera_device = MagicMock(spec=pv.VeraThermostat) # type: pv.VeraThermostat
+ vera_device: pv.VeraThermostat = MagicMock(spec=pv.VeraThermostat)
vera_device.device_id = 1
vera_device.vera_device_id = vera_device.device_id
+ vera_device.comm_failure = False
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_THERMOSTAT
vera_device.power = 10
diff --git a/tests/components/vera/test_cover.py b/tests/components/vera/test_cover.py
index 0c05d84e2dbc1d..15b95b55e39847 100644
--- a/tests/components/vera/test_cover.py
+++ b/tests/components/vera/test_cover.py
@@ -12,9 +12,10 @@ async def test_cover(
hass: HomeAssistant, vera_component_factory: ComponentFactory
) -> None:
"""Test function."""
- vera_device = MagicMock(spec=pv.VeraCurtain) # type: pv.VeraCurtain
+ vera_device: pv.VeraCurtain = MagicMock(spec=pv.VeraCurtain)
vera_device.device_id = 1
vera_device.vera_device_id = vera_device.device_id
+ vera_device.comm_failure = False
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_CURTAIN
vera_device.is_closed = False
diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py
index b1d6010336a59b..c828ef55fcd574 100644
--- a/tests/components/vera/test_init.py
+++ b/tests/components/vera/test_init.py
@@ -13,6 +13,7 @@
)
from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
from .common import ComponentFactory, ConfigSource, new_simple_controller_config
@@ -23,7 +24,7 @@ async def test_init(
hass: HomeAssistant, vera_component_factory: ComponentFactory
) -> None:
"""Test function."""
- vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
+ vera_device1: pv.VeraBinarySensor = MagicMock(spec=pv.VeraBinarySensor)
vera_device1.device_id = 1
vera_device1.vera_device_id = vera_device1.device_id
vera_device1.name = "first_dev"
@@ -40,7 +41,7 @@ async def test_init(
),
)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry1 = entity_registry.async_get(entity1_id)
assert entry1
assert entry1.unique_id == "vera_first_serial_1"
@@ -50,7 +51,7 @@ async def test_init_from_file(
hass: HomeAssistant, vera_component_factory: ComponentFactory
) -> None:
"""Test function."""
- vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
+ vera_device1: pv.VeraBinarySensor = MagicMock(spec=pv.VeraBinarySensor)
vera_device1.device_id = 1
vera_device1.vera_device_id = vera_device1.device_id
vera_device1.name = "first_dev"
@@ -67,7 +68,7 @@ async def test_init_from_file(
),
)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry1 = entity_registry.async_get(entity1_id)
assert entry1
assert entry1.unique_id == "vera_first_serial_1"
@@ -77,14 +78,14 @@ async def test_multiple_controllers_with_legacy_one(
hass: HomeAssistant, vera_component_factory: ComponentFactory
) -> None:
"""Test multiple controllers with one legacy controller."""
- vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
+ vera_device1: pv.VeraBinarySensor = MagicMock(spec=pv.VeraBinarySensor)
vera_device1.device_id = 1
vera_device1.vera_device_id = vera_device1.device_id
vera_device1.name = "first_dev"
vera_device1.is_tripped = False
entity1_id = "binary_sensor.first_dev_1"
- vera_device2 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
+ vera_device2: pv.VeraBinarySensor = MagicMock(spec=pv.VeraBinarySensor)
vera_device2.device_id = 2
vera_device2.vera_device_id = vera_device2.device_id
vera_device2.name = "second_dev"
@@ -117,7 +118,7 @@ async def test_multiple_controllers_with_legacy_one(
),
)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entry1 = entity_registry.async_get(entity1_id)
assert entry1
@@ -132,7 +133,7 @@ async def test_unload(
hass: HomeAssistant, vera_component_factory: ComponentFactory
) -> None:
"""Test function."""
- vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
+ vera_device1: pv.VeraBinarySensor = MagicMock(spec=pv.VeraBinarySensor)
vera_device1.device_id = 1
vera_device1.vera_device_id = vera_device1.device_id
vera_device1.name = "first_dev"
@@ -186,34 +187,39 @@ async def test_exclude_and_light_ids(
hass: HomeAssistant, vera_component_factory: ComponentFactory, options
) -> None:
"""Test device exclusion, marking switches as lights and fixing the data type."""
- vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
+ vera_device1: pv.VeraBinarySensor = MagicMock(spec=pv.VeraBinarySensor)
vera_device1.device_id = 1
vera_device1.vera_device_id = 1
vera_device1.name = "dev1"
vera_device1.is_tripped = False
entity_id1 = "binary_sensor.dev1_1"
- vera_device2 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor
+ vera_device2: pv.VeraBinarySensor = MagicMock(spec=pv.VeraBinarySensor)
vera_device2.device_id = 2
vera_device2.vera_device_id = 2
vera_device2.name = "dev2"
vera_device2.is_tripped = False
entity_id2 = "binary_sensor.dev2_2"
- vera_device3 = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch
+ vera_device3: pv.VeraSwitch = MagicMock(spec=pv.VeraSwitch)
vera_device3.device_id = 3
vera_device3.vera_device_id = 3
vera_device3.name = "dev3"
vera_device3.category = pv.CATEGORY_SWITCH
vera_device3.is_switched_on = MagicMock(return_value=False)
+
entity_id3 = "switch.dev3_3"
- vera_device4 = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch
+ vera_device4: pv.VeraSwitch = MagicMock(spec=pv.VeraSwitch)
vera_device4.device_id = 4
vera_device4.vera_device_id = 4
vera_device4.name = "dev4"
vera_device4.category = pv.CATEGORY_SWITCH
vera_device4.is_switched_on = MagicMock(return_value=False)
+ vera_device4.get_brightness = MagicMock(return_value=0)
+ vera_device4.get_color = MagicMock(return_value=[0, 0, 0])
+ vera_device4.is_dimmable = True
+
entity_id4 = "light.dev4_4"
component_data = await vera_component_factory.configure_component(
diff --git a/tests/components/vera/test_light.py b/tests/components/vera/test_light.py
index 3b14aba742992c..c96199e59894ff 100644
--- a/tests/components/vera/test_light.py
+++ b/tests/components/vera/test_light.py
@@ -13,9 +13,10 @@ async def test_light(
hass: HomeAssistant, vera_component_factory: ComponentFactory
) -> None:
"""Test function."""
- vera_device = MagicMock(spec=pv.VeraDimmer) # type: pv.VeraDimmer
+ vera_device: pv.VeraDimmer = MagicMock(spec=pv.VeraDimmer)
vera_device.device_id = 1
vera_device.vera_device_id = vera_device.device_id
+ vera_device.comm_failure = False
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_DIMMER
vera_device.is_switched_on = MagicMock(return_value=False)
diff --git a/tests/components/vera/test_lock.py b/tests/components/vera/test_lock.py
index c288ac8709e1ab..644d58b9fc5a52 100644
--- a/tests/components/vera/test_lock.py
+++ b/tests/components/vera/test_lock.py
@@ -13,9 +13,10 @@ async def test_lock(
hass: HomeAssistant, vera_component_factory: ComponentFactory
) -> None:
"""Test function."""
- vera_device = MagicMock(spec=pv.VeraLock) # type: pv.VeraLock
+ vera_device: pv.VeraLock = MagicMock(spec=pv.VeraLock)
vera_device.device_id = 1
vera_device.vera_device_id = vera_device.device_id
+ vera_device.comm_failure = False
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_LOCK
vera_device.is_locked = MagicMock(return_value=False)
diff --git a/tests/components/vera/test_scene.py b/tests/components/vera/test_scene.py
index 2d4b7375498157..b23d220e74e2b8 100644
--- a/tests/components/vera/test_scene.py
+++ b/tests/components/vera/test_scene.py
@@ -12,7 +12,7 @@ async def test_scene(
hass: HomeAssistant, vera_component_factory: ComponentFactory
) -> None:
"""Test function."""
- vera_scene = MagicMock(spec=pv.VeraScene) # type: pv.VeraScene
+ vera_scene: pv.VeraScene = MagicMock(spec=pv.VeraScene)
vera_scene.scene_id = 1
vera_scene.vera_scene_id = vera_scene.scene_id
vera_scene.name = "dev1"
diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py
index 437776428164a5..a2086f9f5e0000 100644
--- a/tests/components/vera/test_sensor.py
+++ b/tests/components/vera/test_sensor.py
@@ -1,5 +1,7 @@
"""Vera tests."""
-from typing import Any, Callable, Tuple
+from __future__ import annotations
+
+from typing import Any, Callable
from unittest.mock import MagicMock
import pyvera as pv
@@ -15,14 +17,15 @@ async def run_sensor_test(
vera_component_factory: ComponentFactory,
category: int,
class_property: str,
- assert_states: Tuple[Tuple[Any, Any]],
+ assert_states: tuple[tuple[Any, Any]],
assert_unit_of_measurement: str = None,
setup_callback: Callable[[pv.VeraController], None] = None,
) -> None:
"""Test generic sensor."""
- vera_device = MagicMock(spec=pv.VeraSensor) # type: pv.VeraSensor
+ vera_device: pv.VeraSensor = MagicMock(spec=pv.VeraSensor)
vera_device.device_id = 1
vera_device.vera_device_id = vera_device.device_id
+ vera_device.comm_failure = False
vera_device.name = "dev1"
vera_device.category = category
setattr(vera_device, class_property, "33")
@@ -175,9 +178,10 @@ async def test_scene_controller_sensor(
hass: HomeAssistant, vera_component_factory: ComponentFactory
) -> None:
"""Test function."""
- vera_device = MagicMock(spec=pv.VeraSensor) # type: pv.VeraSensor
+ vera_device: pv.VeraSensor = MagicMock(spec=pv.VeraSensor)
vera_device.device_id = 1
vera_device.vera_device_id = vera_device.device_id
+ vera_device.comm_failure = False
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_SCENE_CONTROLLER
vera_device.get_last_scene_id = MagicMock(return_value="id0")
diff --git a/tests/components/vera/test_switch.py b/tests/components/vera/test_switch.py
index b61564c56bc577..2ffb472aeeb897 100644
--- a/tests/components/vera/test_switch.py
+++ b/tests/components/vera/test_switch.py
@@ -12,9 +12,10 @@ async def test_switch(
hass: HomeAssistant, vera_component_factory: ComponentFactory
) -> None:
"""Test function."""
- vera_device = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch
+ vera_device: pv.VeraSwitch = MagicMock(spec=pv.VeraSwitch)
vera_device.device_id = 1
vera_device.vera_device_id = vera_device.device_id
+ vera_device.comm_failure = False
vera_device.name = "dev1"
vera_device.category = pv.CATEGORY_SWITCH
vera_device.is_switched_on = MagicMock(return_value=False)
diff --git a/tests/components/verisure/__init__.py b/tests/components/verisure/__init__.py
index 0382661dbe3edf..08339e46c6fc40 100644
--- a/tests/components/verisure/__init__.py
+++ b/tests/components/verisure/__init__.py
@@ -1 +1 @@
-"""Tests for Verisure integration."""
+"""Tests for the Verisure integration."""
diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py
new file mode 100644
index 00000000000000..c53c418c72b48b
--- /dev/null
+++ b/tests/components/verisure/test_config_flow.py
@@ -0,0 +1,588 @@
+"""Test the Verisure config flow."""
+from __future__ import annotations
+
+from unittest.mock import PropertyMock, patch
+
+import pytest
+from verisure import Error as VerisureError, LoginError as VerisureLoginError
+
+from homeassistant import config_entries
+from homeassistant.components.dhcp import MAC_ADDRESS
+from homeassistant.components.verisure.const import (
+ CONF_GIID,
+ CONF_LOCK_CODE_DIGITS,
+ CONF_LOCK_DEFAULT_CODE,
+ DEFAULT_LOCK_CODE_DIGITS,
+ DOMAIN,
+)
+from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
+from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import (
+ RESULT_TYPE_ABORT,
+ RESULT_TYPE_CREATE_ENTRY,
+ RESULT_TYPE_FORM,
+)
+
+from tests.common import MockConfigEntry
+
+TEST_INSTALLATIONS = [
+ {"giid": "12345", "alias": "ascending", "street": "12345th street"},
+ {"giid": "54321", "alias": "descending", "street": "54321th street"},
+]
+TEST_INSTALLATION = [TEST_INSTALLATIONS[0]]
+
+
+async def test_full_user_flow_single_installation(hass: HomeAssistant) -> None:
+ """Test a full user initiated configuration flow with a single installation."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["step_id"] == "user"
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.verisure.config_flow.Verisure",
+ ) as mock_verisure, patch(
+ "homeassistant.components.verisure.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.verisure.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ type(mock_verisure.return_value).installations = PropertyMock(
+ return_value=TEST_INSTALLATION
+ )
+ mock_verisure.login.return_value = True
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "email": "verisure_my_pages@example.com",
+ "password": "SuperS3cr3t!",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result2["title"] == "ascending (12345th street)"
+ assert result2["data"] == {
+ CONF_GIID: "12345",
+ CONF_EMAIL: "verisure_my_pages@example.com",
+ CONF_PASSWORD: "SuperS3cr3t!",
+ }
+
+ assert len(mock_verisure.mock_calls) == 2
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_full_user_flow_multiple_installations(hass: HomeAssistant) -> None:
+ """Test a full user initiated configuration flow with multiple installations."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["step_id"] == "user"
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.verisure.config_flow.Verisure",
+ ) as mock_verisure:
+ type(mock_verisure.return_value).installations = PropertyMock(
+ return_value=TEST_INSTALLATIONS
+ )
+ mock_verisure.login.return_value = True
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "email": "verisure_my_pages@example.com",
+ "password": "SuperS3cr3t!",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["step_id"] == "installation"
+ assert result2["type"] == RESULT_TYPE_FORM
+ assert result2["errors"] is None
+
+ with patch(
+ "homeassistant.components.verisure.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.verisure.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result3 = await hass.config_entries.flow.async_configure(
+ result2["flow_id"], {"giid": "54321"}
+ )
+ await hass.async_block_till_done()
+
+ assert result3["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result3["title"] == "descending (54321th street)"
+ assert result3["data"] == {
+ CONF_GIID: "54321",
+ CONF_EMAIL: "verisure_my_pages@example.com",
+ CONF_PASSWORD: "SuperS3cr3t!",
+ }
+
+ assert len(mock_verisure.mock_calls) == 2
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_invalid_login(hass: HomeAssistant) -> None:
+ """Test a flow with an invalid Verisure My Pages login."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.verisure.config_flow.Verisure.login",
+ side_effect=VerisureLoginError,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "email": "verisure_my_pages@example.com",
+ "password": "SuperS3cr3t!",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == RESULT_TYPE_FORM
+ assert result2["step_id"] == "user"
+ assert result2["errors"] == {"base": "invalid_auth"}
+
+
+async def test_unknown_error(hass: HomeAssistant) -> None:
+ """Test a flow with an invalid Verisure My Pages login."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.verisure.config_flow.Verisure.login",
+ side_effect=VerisureError,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "email": "verisure_my_pages@example.com",
+ "password": "SuperS3cr3t!",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == RESULT_TYPE_FORM
+ assert result2["step_id"] == "user"
+ assert result2["errors"] == {"base": "unknown"}
+
+
+async def test_dhcp(hass: HomeAssistant) -> None:
+ """Test that DHCP discovery works."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ data={MAC_ADDRESS: "01:23:45:67:89:ab"},
+ context={"source": config_entries.SOURCE_DHCP},
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+
+async def test_reauth_flow(hass: HomeAssistant) -> None:
+ """Test a reauthentication flow."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="12345",
+ data={
+ CONF_EMAIL: "verisure_my_pages@example.com",
+ CONF_GIID: "12345",
+ CONF_PASSWORD: "SuperS3cr3t!",
+ },
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data={"entry": entry}
+ )
+ assert result["step_id"] == "reauth_confirm"
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.verisure.config_flow.Verisure.login",
+ return_value=True,
+ ) as mock_verisure, patch(
+ "homeassistant.components.verisure.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.verisure.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "email": "verisure_my_pages@example.com",
+ "password": "correct horse battery staple",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == RESULT_TYPE_ABORT
+ assert result2["reason"] == "reauth_successful"
+ assert entry.data == {
+ CONF_GIID: "12345",
+ CONF_EMAIL: "verisure_my_pages@example.com",
+ CONF_PASSWORD: "correct horse battery staple",
+ }
+
+ assert len(mock_verisure.mock_calls) == 1
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_reauth_flow_invalid_login(hass: HomeAssistant) -> None:
+ """Test a reauthentication flow."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="12345",
+ data={
+ CONF_EMAIL: "verisure_my_pages@example.com",
+ CONF_GIID: "12345",
+ CONF_PASSWORD: "SuperS3cr3t!",
+ },
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data={"entry": entry}
+ )
+
+ with patch(
+ "homeassistant.components.verisure.config_flow.Verisure.login",
+ side_effect=VerisureLoginError,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "email": "verisure_my_pages@example.com",
+ "password": "WrOngP4ssw0rd!",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["step_id"] == "reauth_confirm"
+ assert result2["type"] == RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "invalid_auth"}
+
+
+async def test_reauth_flow_unknown_error(hass: HomeAssistant) -> None:
+ """Test a reauthentication flow, with an unknown error happening."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="12345",
+ data={
+ CONF_EMAIL: "verisure_my_pages@example.com",
+ CONF_GIID: "12345",
+ CONF_PASSWORD: "SuperS3cr3t!",
+ },
+ )
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data={"entry": entry}
+ )
+
+ with patch(
+ "homeassistant.components.verisure.config_flow.Verisure.login",
+ side_effect=VerisureError,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "email": "verisure_my_pages@example.com",
+ "password": "WrOngP4ssw0rd!",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["step_id"] == "reauth_confirm"
+ assert result2["type"] == RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "unknown"}
+
+
+@pytest.mark.parametrize(
+ "input,output",
+ [
+ (
+ {
+ CONF_LOCK_CODE_DIGITS: 5,
+ CONF_LOCK_DEFAULT_CODE: "12345",
+ },
+ {
+ CONF_LOCK_CODE_DIGITS: 5,
+ CONF_LOCK_DEFAULT_CODE: "12345",
+ },
+ ),
+ (
+ {
+ CONF_LOCK_DEFAULT_CODE: "",
+ },
+ {
+ CONF_LOCK_DEFAULT_CODE: "",
+ CONF_LOCK_CODE_DIGITS: DEFAULT_LOCK_CODE_DIGITS,
+ },
+ ),
+ ],
+)
+async def test_options_flow(
+ hass: HomeAssistant, input: dict[str, int | str], output: dict[str, int | str]
+) -> None:
+ """Test options config flow."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="12345",
+ data={},
+ )
+ entry.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.verisure.async_setup", return_value=True
+ ), patch(
+ "homeassistant.components.verisure.async_setup_entry",
+ return_value=True,
+ ):
+ assert await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input=input,
+ )
+
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["data"] == output
+
+
+async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None:
+ """Test options config flow with a code format mismatch."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="12345",
+ data={},
+ )
+ entry.add_to_hass(hass)
+
+ with patch(
+ "homeassistant.components.verisure.async_setup", return_value=True
+ ), patch(
+ "homeassistant.components.verisure.async_setup_entry",
+ return_value=True,
+ ):
+ assert await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_LOCK_CODE_DIGITS: 5,
+ CONF_LOCK_DEFAULT_CODE: "123",
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+ assert result["errors"] == {"base": "code_format_mismatch"}
+
+
+#
+# Below this line are tests that can be removed once the YAML configuration
+# has been removed from this integration.
+#
+@pytest.mark.parametrize(
+ "giid,installations",
+ [
+ ("12345", TEST_INSTALLATION),
+ ("12345", TEST_INSTALLATIONS),
+ (None, TEST_INSTALLATION),
+ ],
+)
+async def test_imports(
+ hass: HomeAssistant, giid: str | None, installations: dict[str, str]
+) -> None:
+ """Test a YAML import with/without known giid on single/multiple installations."""
+ with patch(
+ "homeassistant.components.verisure.config_flow.Verisure",
+ ) as mock_verisure, patch(
+ "homeassistant.components.verisure.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.verisure.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ type(mock_verisure.return_value).installations = PropertyMock(
+ return_value=installations
+ )
+ mock_verisure.login.return_value = True
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={
+ CONF_EMAIL: "verisure_my_pages@example.com",
+ CONF_GIID: giid,
+ CONF_LOCK_CODE_DIGITS: 10,
+ CONF_LOCK_DEFAULT_CODE: "123456",
+ CONF_PASSWORD: "SuperS3cr3t!",
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "ascending (12345th street)"
+ assert result["data"] == {
+ CONF_EMAIL: "verisure_my_pages@example.com",
+ CONF_GIID: "12345",
+ CONF_LOCK_CODE_DIGITS: 10,
+ CONF_LOCK_DEFAULT_CODE: "123456",
+ CONF_PASSWORD: "SuperS3cr3t!",
+ }
+
+ assert len(mock_verisure.mock_calls) == 2
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_imports_invalid_login(hass: HomeAssistant) -> None:
+ """Test a YAML import that results in a invalid login."""
+ with patch(
+ "homeassistant.components.verisure.config_flow.Verisure.login",
+ side_effect=VerisureLoginError,
+ ):
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={
+ CONF_EMAIL: "verisure_my_pages@example.com",
+ CONF_GIID: None,
+ CONF_LOCK_CODE_DIGITS: None,
+ CONF_LOCK_DEFAULT_CODE: None,
+ CONF_PASSWORD: "SuperS3cr3t!",
+ },
+ )
+
+ assert result["step_id"] == "user"
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["errors"] == {"base": "invalid_auth"}
+
+ with patch(
+ "homeassistant.components.verisure.config_flow.Verisure",
+ ) as mock_verisure, patch(
+ "homeassistant.components.verisure.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.verisure.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ type(mock_verisure.return_value).installations = PropertyMock(
+ return_value=TEST_INSTALLATION
+ )
+ mock_verisure.login.return_value = True
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "email": "verisure_my_pages@example.com",
+ "password": "SuperS3cr3t!",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result2["title"] == "ascending (12345th street)"
+ assert result2["data"] == {
+ CONF_GIID: "12345",
+ CONF_EMAIL: "verisure_my_pages@example.com",
+ CONF_PASSWORD: "SuperS3cr3t!",
+ }
+
+ assert len(mock_verisure.mock_calls) == 2
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_imports_needs_user_installation_choice(hass: HomeAssistant) -> None:
+ """Test a YAML import that needs to use to decide on the installation."""
+ with patch(
+ "homeassistant.components.verisure.config_flow.Verisure",
+ ) as mock_verisure:
+ type(mock_verisure.return_value).installations = PropertyMock(
+ return_value=TEST_INSTALLATIONS
+ )
+ mock_verisure.login.return_value = True
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={
+ CONF_EMAIL: "verisure_my_pages@example.com",
+ CONF_GIID: None,
+ CONF_LOCK_CODE_DIGITS: None,
+ CONF_LOCK_DEFAULT_CODE: None,
+ CONF_PASSWORD: "SuperS3cr3t!",
+ },
+ )
+
+ assert result["step_id"] == "installation"
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["errors"] is None
+
+ with patch(
+ "homeassistant.components.verisure.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.verisure.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"giid": "12345"}
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result2["title"] == "ascending (12345th street)"
+ assert result2["data"] == {
+ CONF_GIID: "12345",
+ CONF_EMAIL: "verisure_my_pages@example.com",
+ CONF_PASSWORD: "SuperS3cr3t!",
+ }
+
+ assert len(mock_verisure.mock_calls) == 2
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+@pytest.mark.parametrize("giid", ["12345", None])
+async def test_import_already_exists(hass: HomeAssistant, giid: str | None) -> None:
+ """Test that import flow aborts if exists."""
+ MockConfigEntry(domain=DOMAIN, data={}, unique_id="12345").add_to_hass(hass)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={
+ CONF_EMAIL: "verisure_my_pages@example.com",
+ CONF_PASSWORD: "SuperS3cr3t!",
+ CONF_GIID: giid,
+ },
+ )
+
+ assert result["type"] == RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
diff --git a/tests/components/verisure/test_ethernet_status.py b/tests/components/verisure/test_ethernet_status.py
deleted file mode 100644
index 611adde19d9fd3..00000000000000
--- a/tests/components/verisure/test_ethernet_status.py
+++ /dev/null
@@ -1,67 +0,0 @@
-"""Test Verisure ethernet status."""
-from contextlib import contextmanager
-from unittest.mock import patch
-
-from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN
-from homeassistant.const import STATE_UNAVAILABLE
-from homeassistant.setup import async_setup_component
-
-CONFIG = {
- "verisure": {
- "username": "test",
- "password": "test",
- "alarm": False,
- "door_window": False,
- "hygrometers": False,
- "mouse": False,
- "smartplugs": False,
- "thermometers": False,
- "smartcam": False,
- }
-}
-
-
-@contextmanager
-def mock_hub(config, response):
- """Extensively mock out a verisure hub."""
- hub_prefix = "homeassistant.components.verisure.binary_sensor.hub"
- verisure_prefix = "verisure.Session"
- with patch(verisure_prefix) as session, patch(hub_prefix) as hub:
- session.login.return_value = True
-
- hub.config = config["verisure"]
- hub.get.return_value = response
- hub.get_first.return_value = response.get("ethernetConnectedNow", None)
-
- yield hub
-
-
-async def setup_verisure(hass, config, response):
- """Set up mock verisure."""
- with mock_hub(config, response):
- await async_setup_component(hass, VERISURE_DOMAIN, config)
- await hass.async_block_till_done()
-
-
-async def test_verisure_no_ethernet_status(hass):
- """Test no data from API."""
- await setup_verisure(hass, CONFIG, {})
- assert len(hass.states.async_all()) == 1
- entity_id = hass.states.async_entity_ids()[0]
- assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
-
-
-async def test_verisure_ethernet_status_disconnected(hass):
- """Test disconnected."""
- await setup_verisure(hass, CONFIG, {"ethernetConnectedNow": False})
- assert len(hass.states.async_all()) == 1
- entity_id = hass.states.async_entity_ids()[0]
- assert hass.states.get(entity_id).state == "off"
-
-
-async def test_verisure_ethernet_status_connected(hass):
- """Test connected."""
- await setup_verisure(hass, CONFIG, {"ethernetConnectedNow": True})
- assert len(hass.states.async_all()) == 1
- entity_id = hass.states.async_entity_ids()[0]
- assert hass.states.get(entity_id).state == "on"
diff --git a/tests/components/verisure/test_lock.py b/tests/components/verisure/test_lock.py
deleted file mode 100644
index d41bbab20379b2..00000000000000
--- a/tests/components/verisure/test_lock.py
+++ /dev/null
@@ -1,144 +0,0 @@
-"""Tests for the Verisure platform."""
-
-from contextlib import contextmanager
-from unittest.mock import call, patch
-
-from homeassistant.components.lock import (
- DOMAIN as LOCK_DOMAIN,
- SERVICE_LOCK,
- SERVICE_UNLOCK,
-)
-from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN
-from homeassistant.const import STATE_UNLOCKED
-from homeassistant.setup import async_setup_component
-
-NO_DEFAULT_LOCK_CODE_CONFIG = {
- "verisure": {
- "username": "test",
- "password": "test",
- "locks": True,
- "alarm": False,
- "door_window": False,
- "hygrometers": False,
- "mouse": False,
- "smartplugs": False,
- "thermometers": False,
- "smartcam": False,
- }
-}
-
-DEFAULT_LOCK_CODE_CONFIG = {
- "verisure": {
- "username": "test",
- "password": "test",
- "locks": True,
- "default_lock_code": "9999",
- "alarm": False,
- "door_window": False,
- "hygrometers": False,
- "mouse": False,
- "smartplugs": False,
- "thermometers": False,
- "smartcam": False,
- }
-}
-
-LOCKS = ["door_lock"]
-
-
-@contextmanager
-def mock_hub(config, get_response=LOCKS[0]):
- """Extensively mock out a verisure hub."""
- hub_prefix = "homeassistant.components.verisure.lock.hub"
- # Since there is no conf to disable ethernet status, mock hub for
- # binary sensor too
- hub_binary_sensor = "homeassistant.components.verisure.binary_sensor.hub"
- verisure_prefix = "verisure.Session"
- with patch(verisure_prefix) as session, patch(hub_prefix) as hub:
- session.login.return_value = True
-
- hub.config = config["verisure"]
- hub.get.return_value = LOCKS
- hub.get_first.return_value = get_response.upper()
- hub.session.set_lock_state.return_value = {
- "doorLockStateChangeTransactionId": "test"
- }
- hub.session.get_lock_state_transaction.return_value = {"result": "OK"}
-
- with patch(hub_binary_sensor, hub):
- yield hub
-
-
-async def setup_verisure_locks(hass, config):
- """Set up mock verisure locks."""
- with mock_hub(config):
- await async_setup_component(hass, VERISURE_DOMAIN, config)
- await hass.async_block_till_done()
- # lock.door_lock, ethernet_status
- assert len(hass.states.async_all()) == 2
-
-
-async def test_verisure_no_default_code(hass):
- """Test configs without a default lock code."""
- await setup_verisure_locks(hass, NO_DEFAULT_LOCK_CODE_CONFIG)
- with mock_hub(NO_DEFAULT_LOCK_CODE_CONFIG, STATE_UNLOCKED) as hub:
-
- mock = hub.session.set_lock_state
- await hass.services.async_call(
- LOCK_DOMAIN, SERVICE_LOCK, {"entity_id": "lock.door_lock"}
- )
- await hass.async_block_till_done()
- assert mock.call_count == 0
-
- await hass.services.async_call(
- LOCK_DOMAIN, SERVICE_LOCK, {"entity_id": "lock.door_lock", "code": "12345"}
- )
- await hass.async_block_till_done()
- assert mock.call_args == call("12345", LOCKS[0], "lock")
-
- mock.reset_mock()
- await hass.services.async_call(
- LOCK_DOMAIN, SERVICE_UNLOCK, {"entity_id": "lock.door_lock"}
- )
- await hass.async_block_till_done()
- assert mock.call_count == 0
-
- await hass.services.async_call(
- LOCK_DOMAIN,
- SERVICE_UNLOCK,
- {"entity_id": "lock.door_lock", "code": "12345"},
- )
- await hass.async_block_till_done()
- assert mock.call_args == call("12345", LOCKS[0], "unlock")
-
-
-async def test_verisure_default_code(hass):
- """Test configs with a default lock code."""
- await setup_verisure_locks(hass, DEFAULT_LOCK_CODE_CONFIG)
- with mock_hub(DEFAULT_LOCK_CODE_CONFIG, STATE_UNLOCKED) as hub:
- mock = hub.session.set_lock_state
- await hass.services.async_call(
- LOCK_DOMAIN, SERVICE_LOCK, {"entity_id": "lock.door_lock"}
- )
- await hass.async_block_till_done()
- assert mock.call_args == call("9999", LOCKS[0], "lock")
-
- await hass.services.async_call(
- LOCK_DOMAIN, SERVICE_UNLOCK, {"entity_id": "lock.door_lock"}
- )
- await hass.async_block_till_done()
- assert mock.call_args == call("9999", LOCKS[0], "unlock")
-
- await hass.services.async_call(
- LOCK_DOMAIN, SERVICE_LOCK, {"entity_id": "lock.door_lock", "code": "12345"}
- )
- await hass.async_block_till_done()
- assert mock.call_args == call("12345", LOCKS[0], "lock")
-
- await hass.services.async_call(
- LOCK_DOMAIN,
- SERVICE_UNLOCK,
- {"entity_id": "lock.door_lock", "code": "12345"},
- )
- await hass.async_block_till_done()
- assert mock.call_args == call("12345", LOCKS[0], "unlock")
diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py
index 917e6f7f291c13..8124827dbf0853 100644
--- a/tests/components/vizio/conftest.py
+++ b/tests/components/vizio/conftest.py
@@ -157,6 +157,9 @@ def vizio_cant_connect_fixture():
with patch(
"homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config",
AsyncMock(return_value=False),
+ ), patch(
+ "homeassistant.components.vizio.media_player.VizioAsync.get_power_state",
+ return_value=None,
):
yield
diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py
index cd6116625977e2..b223202d5b18fb 100644
--- a/tests/components/vizio/test_init.py
+++ b/tests/components/vizio/test_init.py
@@ -3,6 +3,7 @@
from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN
from homeassistant.components.vizio.const import DOMAIN
+from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.setup import async_setup_component
@@ -41,7 +42,10 @@ async def test_tv_load_and_unload(
assert await config_entry.async_unload(hass)
await hass.async_block_till_done()
- assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0
+ entities = hass.states.async_entity_ids(MP_DOMAIN)
+ assert len(entities) == 1
+ for entity in entities:
+ assert hass.states.get(entity).state == STATE_UNAVAILABLE
assert DOMAIN not in hass.data
@@ -62,5 +66,8 @@ async def test_speaker_load_and_unload(
assert await config_entry.async_unload(hass)
await hass.async_block_till_done()
- assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0
+ entities = hass.states.async_entity_ids(MP_DOMAIN)
+ assert len(entities) == 1
+ for entity in entities:
+ assert hass.states.get(entity).state == STATE_UNAVAILABLE
assert DOMAIN not in hass.data
diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py
index 78976032b00f09..48a1b5b464fd6c 100644
--- a/tests/components/vizio/test_media_player.py
+++ b/tests/components/vizio/test_media_player.py
@@ -1,7 +1,9 @@
"""Tests for Vizio config flow."""
+from __future__ import annotations
+
from contextlib import asynccontextmanager
from datetime import timedelta
-from typing import Any, Dict, List, Optional
+from typing import Any
from unittest.mock import call, patch
import pytest
@@ -87,7 +89,7 @@ async def _add_config_entry_to_hass(
await hass.async_block_till_done()
-def _get_ha_power_state(vizio_power_state: Optional[bool]) -> str:
+def _get_ha_power_state(vizio_power_state: bool | None) -> str:
"""Return HA power state given Vizio power state."""
if vizio_power_state:
return STATE_ON
@@ -98,7 +100,7 @@ def _get_ha_power_state(vizio_power_state: Optional[bool]) -> str:
return STATE_UNAVAILABLE
-def _assert_sources_and_volume(attr: Dict[str, Any], vizio_device_class: str) -> None:
+def _assert_sources_and_volume(attr: dict[str, Any], vizio_device_class: str) -> None:
"""Assert source list, source, and volume level based on attr dict and device class."""
assert attr["source_list"] == INPUT_LIST
assert attr["source"] == CURRENT_INPUT
@@ -111,7 +113,7 @@ def _assert_sources_and_volume(attr: Dict[str, Any], vizio_device_class: str) ->
def _get_attr_and_assert_base_attr(
hass: HomeAssistantType, device_class: str, power_state: str
-) -> Dict[str, Any]:
+) -> dict[str, Any]:
"""Return entity attributes after asserting name, device class, and power state."""
attr = hass.states.get(ENTITY_ID).attributes
assert attr["friendly_name"] == NAME
@@ -123,7 +125,7 @@ def _get_attr_and_assert_base_attr(
@asynccontextmanager
async def _cm_for_test_setup_without_apps(
- all_settings: Dict[str, Any], vizio_power_state: Optional[bool]
+ all_settings: dict[str, Any], vizio_power_state: bool | None
) -> None:
"""Context manager to setup test for Vizio devices without including app specific patches."""
with patch(
@@ -140,7 +142,7 @@ async def _cm_for_test_setup_without_apps(
async def _test_setup_tv(
- hass: HomeAssistantType, vizio_power_state: Optional[bool]
+ hass: HomeAssistantType, vizio_power_state: bool | None
) -> None:
"""Test Vizio TV entity setup."""
ha_power_state = _get_ha_power_state(vizio_power_state)
@@ -164,7 +166,7 @@ async def _test_setup_tv(
async def _test_setup_speaker(
- hass: HomeAssistantType, vizio_power_state: Optional[bool]
+ hass: HomeAssistantType, vizio_power_state: bool | None
) -> None:
"""Test Vizio Speaker entity setup."""
ha_power_state = _get_ha_power_state(vizio_power_state)
@@ -201,7 +203,7 @@ async def _test_setup_speaker(
@asynccontextmanager
async def _cm_for_test_setup_tv_with_apps(
- hass: HomeAssistantType, device_config: Dict[str, Any], app_config: Dict[str, Any]
+ hass: HomeAssistantType, device_config: dict[str, Any], app_config: dict[str, Any]
) -> None:
"""Context manager to setup test for Vizio TV with support for apps."""
config_entry = MockConfigEntry(
@@ -229,7 +231,7 @@ async def _cm_for_test_setup_tv_with_apps(
def _assert_source_list_with_apps(
- list_to_test: List[str], attr: Dict[str, Any]
+ list_to_test: list[str], attr: dict[str, Any]
) -> None:
"""Assert source list matches list_to_test after removing INPUT_APPS from list."""
for app_to_remove in INPUT_APPS:
@@ -239,23 +241,12 @@ def _assert_source_list_with_apps(
assert attr["source_list"] == list_to_test
-async def _test_setup_failure(hass: HomeAssistantType, config: str) -> None:
- """Test generic Vizio entity setup failure."""
- with patch(
- "homeassistant.components.vizio.media_player.VizioAsync.can_connect_with_auth_check",
- return_value=False,
- ):
- config_entry = MockConfigEntry(domain=DOMAIN, data=config, unique_id=UNIQUE_ID)
- await _add_config_entry_to_hass(hass, config_entry)
- assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0
-
-
async def _test_service(
hass: HomeAssistantType,
domain: str,
vizio_func_name: str,
ha_service_name: str,
- additional_service_data: Optional[Dict[str, Any]],
+ additional_service_data: dict[str, Any] | None,
*args,
**kwargs,
) -> None:
@@ -334,18 +325,28 @@ async def test_init_tv_unavailable(
await _test_setup_tv(hass, None)
-async def test_setup_failure_speaker(
- hass: HomeAssistantType, vizio_connect: pytest.fixture
+async def test_setup_unavailable_speaker(
+ hass: HomeAssistantType, vizio_cant_connect: pytest.fixture
) -> None:
- """Test speaker entity setup failure."""
- await _test_setup_failure(hass, MOCK_SPEAKER_CONFIG)
+ """Test speaker entity sets up as unavailable."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, unique_id=UNIQUE_ID
+ )
+ await _add_config_entry_to_hass(hass, config_entry)
+ assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1
+ assert hass.states.get("media_player.vizio").state == STATE_UNAVAILABLE
-async def test_setup_failure_tv(
- hass: HomeAssistantType, vizio_connect: pytest.fixture
+async def test_setup_unavailable_tv(
+ hass: HomeAssistantType, vizio_cant_connect: pytest.fixture
) -> None:
- """Test TV entity setup failure."""
- await _test_setup_failure(hass, MOCK_USER_VALID_TV_CONFIG)
+ """Test TV entity sets up as unavailable."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID
+ )
+ await _add_config_entry_to_hass(hass, config_entry)
+ assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1
+ assert hass.states.get("media_player.vizio").state == STATE_UNAVAILABLE
async def test_services(
@@ -461,8 +462,8 @@ async def test_options_update(
async def _test_update_availability_switch(
hass: HomeAssistantType,
- initial_power_state: Optional[bool],
- final_power_state: Optional[bool],
+ initial_power_state: bool | None,
+ final_power_state: bool | None,
caplog: pytest.fixture,
) -> None:
now = dt_util.utcnow()
diff --git a/tests/components/volumio/test_config_flow.py b/tests/components/volumio/test_config_flow.py
index 86b5027cd3d7f7..a0d3fc85ee34b0 100644
--- a/tests/components/volumio/test_config_flow.py
+++ b/tests/components/volumio/test_config_flow.py
@@ -42,8 +42,6 @@ async def test_form(hass):
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
return_value=TEST_SYSTEM_INFO,
), patch(
- "homeassistant.components.volumio.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.volumio.async_setup_entry",
return_value=True,
) as mock_setup_entry:
@@ -57,7 +55,6 @@ async def test_form(hass):
assert result2["title"] == "TestVolumio"
assert result2["data"] == {**TEST_SYSTEM_INFO, **TEST_CONNECTION}
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
@@ -82,7 +79,7 @@ async def test_form_updates_unique_id(hass):
with patch(
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
return_value=TEST_SYSTEM_INFO,
- ), patch("homeassistant.components.volumio.async_setup", return_value=True), patch(
+ ), patch(
"homeassistant.components.volumio.async_setup_entry",
return_value=True,
):
@@ -110,8 +107,6 @@ async def test_empty_system_info(hass):
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
return_value={},
), patch(
- "homeassistant.components.volumio.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.volumio.async_setup_entry",
return_value=True,
) as mock_setup_entry:
@@ -130,7 +125,6 @@ async def test_empty_system_info(hass):
"id": None,
}
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
@@ -183,8 +177,6 @@ async def test_discovery(hass):
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
return_value=TEST_SYSTEM_INFO,
), patch(
- "homeassistant.components.volumio.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.volumio.async_setup_entry",
return_value=True,
) as mock_setup_entry:
@@ -201,7 +193,6 @@ async def test_discovery(hass):
assert result2["result"]
assert result2["result"].unique_id == TEST_DISCOVERY_RESULT["id"]
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
@@ -257,8 +248,6 @@ async def test_discovery_updates_unique_id(hass):
entry.add_to_hass(hass)
with patch(
- "homeassistant.components.volumio.async_setup", return_value=True
- ) as mock_setup, patch(
"homeassistant.components.volumio.async_setup_entry",
return_value=True,
) as mock_setup_entry:
@@ -271,5 +260,4 @@ async def test_discovery_updates_unique_id(hass):
assert result["reason"] == "already_configured"
assert entry.data == TEST_DISCOVERY_RESULT
- assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
diff --git a/tests/components/vultr/test_binary_sensor.py b/tests/components/vultr/test_binary_sensor.py
index ab12bfda12c2b9..e0d71bf20a8311 100644
--- a/tests/components/vultr/test_binary_sensor.py
+++ b/tests/components/vultr/test_binary_sensor.py
@@ -73,35 +73,35 @@ def test_binary_sensor(self, mock):
# Test pre data retrieval
if device.subscription == "555555":
- assert "Vultr {}" == device.name
+ assert device.name == "Vultr {}"
device.update()
- device_attrs = device.device_state_attributes
+ device_attrs = device.extra_state_attributes
if device.subscription == "555555":
- assert "Vultr Another Server" == device.name
+ assert device.name == "Vultr Another Server"
if device.name == "A Server":
assert device.is_on is True
- assert "power" == device.device_class
- assert "on" == device.state
- assert "mdi:server" == device.icon
- assert "1000" == device_attrs[ATTR_ALLOWED_BANDWIDTH]
- assert "yes" == device_attrs[ATTR_AUTO_BACKUPS]
- assert "123.123.123.123" == device_attrs[ATTR_IPV4_ADDRESS]
- assert "10.05" == device_attrs[ATTR_COST_PER_MONTH]
- assert "2013-12-19 14:45:41" == device_attrs[ATTR_CREATED_AT]
- assert "576965" == device_attrs[ATTR_SUBSCRIPTION_ID]
+ assert device.device_class == "power"
+ assert device.state == "on"
+ assert device.icon == "mdi:server"
+ assert device_attrs[ATTR_ALLOWED_BANDWIDTH] == "1000"
+ assert device_attrs[ATTR_AUTO_BACKUPS] == "yes"
+ assert device_attrs[ATTR_IPV4_ADDRESS] == "123.123.123.123"
+ assert device_attrs[ATTR_COST_PER_MONTH] == "10.05"
+ assert device_attrs[ATTR_CREATED_AT] == "2013-12-19 14:45:41"
+ assert device_attrs[ATTR_SUBSCRIPTION_ID] == "576965"
elif device.name == "Failed Server":
assert device.is_on is False
- assert "off" == device.state
- assert "mdi:server-off" == device.icon
- assert "1000" == device_attrs[ATTR_ALLOWED_BANDWIDTH]
- assert "no" == device_attrs[ATTR_AUTO_BACKUPS]
- assert "192.168.100.50" == device_attrs[ATTR_IPV4_ADDRESS]
- assert "73.25" == device_attrs[ATTR_COST_PER_MONTH]
- assert "2014-10-13 14:45:41" == device_attrs[ATTR_CREATED_AT]
- assert "123456" == device_attrs[ATTR_SUBSCRIPTION_ID]
+ assert device.state == "off"
+ assert device.icon == "mdi:server-off"
+ assert device_attrs[ATTR_ALLOWED_BANDWIDTH] == "1000"
+ assert device_attrs[ATTR_AUTO_BACKUPS] == "no"
+ assert device_attrs[ATTR_IPV4_ADDRESS] == "192.168.100.50"
+ assert device_attrs[ATTR_COST_PER_MONTH] == "73.25"
+ assert device_attrs[ATTR_CREATED_AT] == "2014-10-13 14:45:41"
+ assert device_attrs[ATTR_SUBSCRIPTION_ID] == "123456"
def test_invalid_sensor_config(self):
"""Test config type failures."""
diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py
index 2b5e07c80b5dda..4449859ddb2e1a 100644
--- a/tests/components/vultr/test_sensor.py
+++ b/tests/components/vultr/test_sensor.py
@@ -73,7 +73,7 @@ def test_sensor(self, mock):
assert setup is None
- assert 5 == len(self.DEVICES)
+ assert len(self.DEVICES) == 5
tested = 0
@@ -87,34 +87,34 @@ def test_sensor(self, mock):
if device.unit_of_measurement == DATA_GIGABYTES: # Test Bandwidth Used
if device.subscription == "576965":
- assert "Vultr my new server Current Bandwidth Used" == device.name
- assert "mdi:chart-histogram" == device.icon
- assert 131.51 == device.state
- assert "mdi:chart-histogram" == device.icon
+ assert device.name == "Vultr my new server Current Bandwidth Used"
+ assert device.icon == "mdi:chart-histogram"
+ assert device.state == 131.51
+ assert device.icon == "mdi:chart-histogram"
tested += 1
elif device.subscription == "123456":
- assert "Server Current Bandwidth Used" == device.name
- assert 957.46 == device.state
+ assert device.name == "Server Current Bandwidth Used"
+ assert device.state == 957.46
tested += 1
elif device.unit_of_measurement == "US$": # Test Pending Charges
if device.subscription == "576965": # Default 'Vultr {} {}'
- assert "Vultr my new server Pending Charges" == device.name
- assert "mdi:currency-usd" == device.icon
- assert 46.67 == device.state
- assert "mdi:currency-usd" == device.icon
+ assert device.name == "Vultr my new server Pending Charges"
+ assert device.icon == "mdi:currency-usd"
+ assert device.state == 46.67
+ assert device.icon == "mdi:currency-usd"
tested += 1
elif device.subscription == "123456": # Custom name with 1 {}
- assert "Server Pending Charges" == device.name
- assert "not a number" == device.state
+ assert device.name == "Server Pending Charges"
+ assert device.state == "not a number"
tested += 1
elif device.subscription == "555555": # No {} in name
- assert "VPS Charges" == device.name
- assert 5.45 == device.state
+ assert device.name == "VPS Charges"
+ assert device.state == 5.45
tested += 1
assert tested == 5
@@ -161,4 +161,4 @@ def test_invalid_sensors(self, mock):
)
assert no_sub_setup is None
- assert 0 == len(self.DEVICES)
+ assert len(self.DEVICES) == 0
diff --git a/tests/components/vultr/test_switch.py b/tests/components/vultr/test_switch.py
index 12af400a44aee4..d6b7392ca9c0ae 100644
--- a/tests/components/vultr/test_switch.py
+++ b/tests/components/vultr/test_switch.py
@@ -77,7 +77,7 @@ def test_switch(self, mock):
tested += 1
device.update()
- device_attrs = device.device_state_attributes
+ device_attrs = device.extra_state_attributes
if device.subscription == "555555":
assert device.name == "Vultr Another Server"
diff --git a/tests/components/wake_on_lan/test_switch.py b/tests/components/wake_on_lan/test_switch.py
index c2e32f77ccffe7..1396ae80f1ef4d 100644
--- a/tests/components/wake_on_lan/test_switch.py
+++ b/tests/components/wake_on_lan/test_switch.py
@@ -41,7 +41,7 @@ async def test_valid_hostname(hass):
await hass.async_block_till_done()
state = hass.states.get("switch.wake_on_lan")
- assert STATE_OFF == state.state
+ assert state.state == STATE_OFF
with patch.object(subprocess, "call", return_value=0):
@@ -53,7 +53,7 @@ async def test_valid_hostname(hass):
)
state = hass.states.get("switch.wake_on_lan")
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
await hass.services.async_call(
switch.DOMAIN,
@@ -63,7 +63,7 @@ async def test_valid_hostname(hass):
)
state = hass.states.get("switch.wake_on_lan")
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
async def test_valid_hostname_windows(hass):
@@ -82,7 +82,7 @@ async def test_valid_hostname_windows(hass):
await hass.async_block_till_done()
state = hass.states.get("switch.wake_on_lan")
- assert STATE_OFF == state.state
+ assert state.state == STATE_OFF
with patch.object(subprocess, "call", return_value=0), patch.object(
platform, "system", return_value="Windows"
@@ -95,7 +95,7 @@ async def test_valid_hostname_windows(hass):
)
state = hass.states.get("switch.wake_on_lan")
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
async def test_broadcast_config_ip_and_port(hass, mock_send_magic_packet):
@@ -119,7 +119,7 @@ async def test_broadcast_config_ip_and_port(hass, mock_send_magic_packet):
await hass.async_block_till_done()
state = hass.states.get("switch.wake_on_lan")
- assert STATE_OFF == state.state
+ assert state.state == STATE_OFF
with patch.object(subprocess, "call", return_value=0):
@@ -155,7 +155,7 @@ async def test_broadcast_config_ip(hass, mock_send_magic_packet):
await hass.async_block_till_done()
state = hass.states.get("switch.wake_on_lan")
- assert STATE_OFF == state.state
+ assert state.state == STATE_OFF
with patch.object(subprocess, "call", return_value=0):
@@ -183,7 +183,7 @@ async def test_broadcast_config_port(hass, mock_send_magic_packet):
await hass.async_block_till_done()
state = hass.states.get("switch.wake_on_lan")
- assert STATE_OFF == state.state
+ assert state.state == STATE_OFF
with patch.object(subprocess, "call", return_value=0):
@@ -216,7 +216,7 @@ async def test_off_script(hass):
calls = async_mock_service(hass, "shell_command", "turn_off_target")
state = hass.states.get("switch.wake_on_lan")
- assert STATE_OFF == state.state
+ assert state.state == STATE_OFF
with patch.object(subprocess, "call", return_value=0):
@@ -228,7 +228,7 @@ async def test_off_script(hass):
)
state = hass.states.get("switch.wake_on_lan")
- assert STATE_ON == state.state
+ assert state.state == STATE_ON
assert len(calls) == 0
with patch.object(subprocess, "call", return_value=2):
@@ -241,7 +241,7 @@ async def test_off_script(hass):
)
state = hass.states.get("switch.wake_on_lan")
- assert STATE_OFF == state.state
+ assert state.state == STATE_OFF
assert len(calls) == 1
@@ -262,7 +262,7 @@ async def test_invalid_hostname_windows(hass):
await hass.async_block_till_done()
state = hass.states.get("switch.wake_on_lan")
- assert STATE_OFF == state.state
+ assert state.state == STATE_OFF
with patch.object(subprocess, "call", return_value=2):
@@ -274,4 +274,45 @@ async def test_invalid_hostname_windows(hass):
)
state = hass.states.get("switch.wake_on_lan")
- assert STATE_OFF == state.state
+ assert state.state == STATE_OFF
+
+
+async def test_no_hostname_state(hass):
+ """Test that the state updates if we do not pass in a hostname."""
+
+ assert await async_setup_component(
+ hass,
+ switch.DOMAIN,
+ {
+ "switch": {
+ "platform": "wake_on_lan",
+ "mac": "00-01-02-03-04-05",
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("switch.wake_on_lan")
+ assert state.state == STATE_OFF
+
+ with patch.object(subprocess, "call", return_value=0):
+
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "switch.wake_on_lan"},
+ blocking=True,
+ )
+
+ state = hass.states.get("switch.wake_on_lan")
+ assert state.state == STATE_ON
+
+ await hass.services.async_call(
+ switch.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "switch.wake_on_lan"},
+ blocking=True,
+ )
+
+ state = hass.states.get("switch.wake_on_lan")
+ assert state.state == STATE_OFF
diff --git a/tests/components/water_heater/test_device_action.py b/tests/components/water_heater/test_device_action.py
index 06bd43ec6544e1..060d9ead29fa4e 100644
--- a/tests/components/water_heater/test_device_action.py
+++ b/tests/components/water_heater/test_device_action.py
@@ -14,7 +14,7 @@
mock_device_registry,
mock_registry,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
diff --git a/tests/components/waze_travel_time/__init__.py b/tests/components/waze_travel_time/__init__.py
new file mode 100644
index 00000000000000..1df3d9314d07ee
--- /dev/null
+++ b/tests/components/waze_travel_time/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Waze Travel Time integration."""
diff --git a/tests/components/waze_travel_time/conftest.py b/tests/components/waze_travel_time/conftest.py
new file mode 100644
index 00000000000000..dd5b343cc166a1
--- /dev/null
+++ b/tests/components/waze_travel_time/conftest.py
@@ -0,0 +1,57 @@
+"""Fixtures for Waze Travel Time tests."""
+from unittest.mock import patch
+
+from WazeRouteCalculator import WRCError
+import pytest
+
+
+@pytest.fixture(name="skip_notifications", autouse=True)
+def skip_notifications_fixture():
+ """Skip notification calls."""
+ with patch("homeassistant.components.persistent_notification.async_create"), patch(
+ "homeassistant.components.persistent_notification.async_dismiss"
+ ):
+ yield
+
+
+@pytest.fixture(name="validate_config_entry")
+def validate_config_entry_fixture():
+ """Return valid config entry."""
+ with patch(
+ "homeassistant.components.waze_travel_time.helpers.WazeRouteCalculator"
+ ) as mock_wrc:
+ obj = mock_wrc.return_value
+ obj.calc_all_routes_info.return_value = None
+ yield
+
+
+@pytest.fixture(name="bypass_setup")
+def bypass_setup_fixture():
+ """Bypass entry setup."""
+ with patch(
+ "homeassistant.components.waze_travel_time.async_setup_entry",
+ return_value=True,
+ ):
+ yield
+
+
+@pytest.fixture(name="mock_update")
+def mock_update_fixture():
+ """Mock an update to the sensor."""
+ with patch(
+ "homeassistant.components.waze_travel_time.sensor.WazeRouteCalculator.calc_all_routes_info",
+ return_value={"My route": (150, 300)},
+ ):
+ yield
+
+
+@pytest.fixture(name="invalidate_config_entry")
+def invalidate_config_entry_fixture():
+ """Return invalid config entry."""
+ with patch(
+ "homeassistant.components.waze_travel_time.helpers.WazeRouteCalculator"
+ ) as mock_wrc:
+ obj = mock_wrc.return_value
+ obj.calc_all_routes_info.return_value = {}
+ obj.calc_all_routes_info.side_effect = WRCError("test")
+ yield
diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py
new file mode 100644
index 00000000000000..f6f1614ca25338
--- /dev/null
+++ b/tests/components/waze_travel_time/test_config_flow.py
@@ -0,0 +1,205 @@
+"""Test the Waze Travel Time config flow."""
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.components.waze_travel_time.const import (
+ CONF_AVOID_FERRIES,
+ CONF_AVOID_SUBSCRIPTION_ROADS,
+ CONF_AVOID_TOLL_ROADS,
+ CONF_DESTINATION,
+ CONF_EXCL_FILTER,
+ CONF_INCL_FILTER,
+ CONF_ORIGIN,
+ CONF_REALTIME,
+ CONF_UNITS,
+ CONF_VEHICLE_TYPE,
+ DEFAULT_NAME,
+ DOMAIN,
+)
+from homeassistant.const import CONF_REGION, CONF_UNIT_SYSTEM_IMPERIAL
+
+from tests.common import MockConfigEntry
+
+
+async def test_minimum_fields(hass, validate_config_entry, bypass_setup):
+ """Test we get the form."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ CONF_REGION: "US",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result2["title"] == f"{DEFAULT_NAME}: location1 -> location2"
+ assert result2["data"] == {
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ CONF_REGION: "US",
+ }
+
+
+async def test_options(hass, validate_config_entry, mock_update):
+ """Test options flow."""
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ CONF_REGION: "US",
+ },
+ )
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.options.async_init(entry.entry_id, data=None)
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_AVOID_FERRIES: True,
+ CONF_AVOID_SUBSCRIPTION_ROADS: True,
+ CONF_AVOID_TOLL_ROADS: True,
+ CONF_EXCL_FILTER: "exclude",
+ CONF_INCL_FILTER: "include",
+ CONF_REALTIME: False,
+ CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_VEHICLE_TYPE: "taxi",
+ },
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == ""
+ assert result["data"] == {
+ CONF_AVOID_FERRIES: True,
+ CONF_AVOID_SUBSCRIPTION_ROADS: True,
+ CONF_AVOID_TOLL_ROADS: True,
+ CONF_EXCL_FILTER: "exclude",
+ CONF_INCL_FILTER: "include",
+ CONF_REALTIME: False,
+ CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_VEHICLE_TYPE: "taxi",
+ }
+
+ assert entry.options == {
+ CONF_AVOID_FERRIES: True,
+ CONF_AVOID_SUBSCRIPTION_ROADS: True,
+ CONF_AVOID_TOLL_ROADS: True,
+ CONF_EXCL_FILTER: "exclude",
+ CONF_INCL_FILTER: "include",
+ CONF_REALTIME: False,
+ CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_VEHICLE_TYPE: "taxi",
+ }
+
+
+async def test_import(hass, validate_config_entry, mock_update):
+ """Test import for config flow."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ CONF_REGION: "US",
+ CONF_AVOID_FERRIES: True,
+ CONF_AVOID_SUBSCRIPTION_ROADS: True,
+ CONF_AVOID_TOLL_ROADS: True,
+ CONF_EXCL_FILTER: "exclude",
+ CONF_INCL_FILTER: "include",
+ CONF_REALTIME: False,
+ CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_VEHICLE_TYPE: "taxi",
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ await hass.async_block_till_done()
+ entry = hass.config_entries.async_entries(DOMAIN)[0]
+ assert entry.data == {
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ CONF_REGION: "US",
+ }
+ assert entry.options == {
+ CONF_AVOID_FERRIES: True,
+ CONF_AVOID_SUBSCRIPTION_ROADS: True,
+ CONF_AVOID_TOLL_ROADS: True,
+ CONF_EXCL_FILTER: "exclude",
+ CONF_INCL_FILTER: "include",
+ CONF_REALTIME: False,
+ CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_VEHICLE_TYPE: "taxi",
+ }
+
+
+async def test_dupe_id(hass, validate_config_entry, bypass_setup):
+ """Test setting up the same entry twice fails."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ CONF_REGION: "US",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ CONF_REGION: "US",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result2["reason"] == "already_configured"
+
+
+async def test_invalid_config_entry(hass, invalidate_config_entry):
+ """Test we get the form."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_ORIGIN: "location1",
+ CONF_DESTINATION: "location2",
+ CONF_REGION: "US",
+ },
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "cannot_connect"}
diff --git a/tests/components/webhook/test_trigger.py b/tests/components/webhook/test_trigger.py
index 2a22c330e14cad..ae70460de5d408 100644
--- a/tests/components/webhook/test_trigger.py
+++ b/tests/components/webhook/test_trigger.py
@@ -6,7 +6,7 @@
from homeassistant.core import callback
from homeassistant.setup import async_setup_component
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture(autouse=True)
@@ -36,7 +36,10 @@ def store_event(event):
"trigger": {"platform": "webhook", "webhook_id": "json_webhook"},
"action": {
"event": "test_success",
- "event_data_template": {"hello": "yo {{ trigger.json.hello }}"},
+ "event_data_template": {
+ "hello": "yo {{ trigger.json.hello }}",
+ "id": "{{ trigger.id}}",
+ },
},
}
},
@@ -50,6 +53,7 @@ def store_event(event):
assert len(events) == 1
assert events[0].data["hello"] == "yo world"
+ assert events[0].data["id"] == 0
async def test_webhook_post(hass, aiohttp_client):
diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py
index 70bc8274684860..716c496d88a87f 100644
--- a/tests/components/webostv/test_media_player.py
+++ b/tests/components/webostv/test_media_player.py
@@ -1,8 +1,10 @@
"""The tests for the LG webOS media player platform."""
-
+import json
+import os
from unittest.mock import patch
import pytest
+from sqlitedict import SqliteDict
from homeassistant.components import media_player
from homeassistant.components.media_player.const import (
@@ -12,13 +14,14 @@
)
from homeassistant.components.webostv.const import (
ATTR_BUTTON,
- ATTR_COMMAND,
ATTR_PAYLOAD,
DOMAIN,
SERVICE_BUTTON,
SERVICE_COMMAND,
+ WEBOSTV_CONFIG_FILE,
)
from homeassistant.const import (
+ ATTR_COMMAND,
ATTR_ENTITY_ID,
CONF_HOST,
CONF_NAME,
@@ -36,6 +39,7 @@ def client_fixture():
with patch(
"homeassistant.components.webostv.WebOsClient", autospec=True
) as mock_client_class:
+ mock_client_class.create.return_value = mock_client_class.return_value
client = mock_client_class.return_value
client.software_info = {"device_id": "a1:b1:c1:d1:e1:f1"}
client.client_key = "0123456789"
@@ -52,6 +56,13 @@ async def setup_webostv(hass):
await hass.async_block_till_done()
+@pytest.fixture
+def cleanup_config(hass):
+ """Test cleanup, remove the config file."""
+ yield
+ os.remove(hass.config.path(WEBOSTV_CONFIG_FILE))
+
+
async def test_mute(hass, client):
"""Test simple service call."""
@@ -128,3 +139,37 @@ async def test_command_with_optional_arg(hass, client):
client.request.assert_called_with(
"test", payload={"target": "https://www.google.com"}
)
+
+
+async def test_migrate_keyfile_to_sqlite(hass, client, cleanup_config):
+ """Test migration from JSON key-file to Sqlite based one."""
+ key = "3d5b1aeeb98e"
+ # Create config file with JSON content
+ config_file = hass.config.path(WEBOSTV_CONFIG_FILE)
+ with open(config_file, "w+") as file:
+ json.dump({"host": key}, file)
+
+ # Run the component setup
+ await setup_webostv(hass)
+
+ # Assert that the config file is a Sqlite database which contains the key
+ with SqliteDict(config_file) as conf:
+ assert conf.get("host") == key
+
+
+async def test_dont_migrate_sqlite_keyfile(hass, client, cleanup_config):
+ """Test that migration is not performed and setup still succeeds when config file is already an Sqlite DB."""
+ key = "3d5b1aeeb98e"
+
+ # Create config file with Sqlite DB
+ config_file = hass.config.path(WEBOSTV_CONFIG_FILE)
+ with SqliteDict(config_file) as conf:
+ conf["host"] = key
+ conf.commit()
+
+ # Run the component setup
+ await setup_webostv(hass)
+
+ # Assert that the config file is still an Sqlite database and setup didn't fail
+ with SqliteDict(config_file) as conf:
+ assert conf.get("host") == key
diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py
index a7aa17db6d3cae..3b01e6ecd8acb5 100644
--- a/tests/components/websocket_api/test_commands.py
+++ b/tests/components/websocket_api/test_commands.py
@@ -1,7 +1,12 @@
"""Tests for WebSocket API commands."""
+import datetime
+from unittest.mock import ANY, patch
+
from async_timeout import timeout
+import pytest
import voluptuous as vol
+from homeassistant.bootstrap import SIGNAL_BOOTSTRAP_INTEGRATONS
from homeassistant.components.websocket_api import const
from homeassistant.components.websocket_api.auth import (
TYPE_AUTH,
@@ -12,22 +17,122 @@
from homeassistant.core import Context, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity
+from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.loader import async_get_integration
-from homeassistant.setup import async_setup_component
+from homeassistant.setup import DATA_SETUP_TIME, async_setup_component
from tests.common import MockEntity, MockEntityPlatform, async_mock_service
async def test_call_service(hass, websocket_client):
"""Test call service command."""
- calls = []
+ calls = async_mock_service(hass, "domain_test", "test_service")
- @callback
- def service_call(call):
- calls.append(call)
+ await websocket_client.send_json(
+ {
+ "id": 5,
+ "type": "call_service",
+ "domain": "domain_test",
+ "service": "test_service",
+ "service_data": {"hello": "world"},
+ }
+ )
+
+ msg = await websocket_client.receive_json()
+ assert msg["id"] == 5
+ assert msg["type"] == const.TYPE_RESULT
+ assert msg["success"]
+
+ assert len(calls) == 1
+ call = calls[0]
+
+ assert call.domain == "domain_test"
+ assert call.service == "test_service"
+ assert call.data == {"hello": "world"}
+ assert call.context.as_dict() == msg["result"]["context"]
+
+
+@pytest.mark.parametrize("command", ("call_service", "call_service_action"))
+async def test_call_service_blocking(hass, websocket_client, command):
+ """Test call service commands block, except for homeassistant restart / stop."""
+ with patch(
+ "homeassistant.core.ServiceRegistry.async_call", autospec=True
+ ) as mock_call:
+ await websocket_client.send_json(
+ {
+ "id": 5,
+ "type": "call_service",
+ "domain": "domain_test",
+ "service": "test_service",
+ "service_data": {"hello": "world"},
+ },
+ )
+ msg = await websocket_client.receive_json()
- hass.services.async_register("domain_test", "test_service", service_call)
+ assert msg["id"] == 5
+ assert msg["type"] == const.TYPE_RESULT
+ assert msg["success"]
+ mock_call.assert_called_once_with(
+ ANY,
+ "domain_test",
+ "test_service",
+ {"hello": "world"},
+ blocking=True,
+ context=ANY,
+ target=ANY,
+ )
+
+ with patch(
+ "homeassistant.core.ServiceRegistry.async_call", autospec=True
+ ) as mock_call:
+ await websocket_client.send_json(
+ {
+ "id": 6,
+ "type": "call_service",
+ "domain": "homeassistant",
+ "service": "test_service",
+ },
+ )
+ msg = await websocket_client.receive_json()
+
+ assert msg["id"] == 6
+ assert msg["type"] == const.TYPE_RESULT
+ assert msg["success"]
+ mock_call.assert_called_once_with(
+ ANY,
+ "homeassistant",
+ "test_service",
+ ANY,
+ blocking=True,
+ context=ANY,
+ target=ANY,
+ )
+
+ with patch(
+ "homeassistant.core.ServiceRegistry.async_call", autospec=True
+ ) as mock_call:
+ await websocket_client.send_json(
+ {
+ "id": 7,
+ "type": "call_service",
+ "domain": "homeassistant",
+ "service": "restart",
+ },
+ )
+ msg = await websocket_client.receive_json()
+
+ assert msg["id"] == 7
+ assert msg["type"] == const.TYPE_RESULT
+ assert msg["success"]
+ mock_call.assert_called_once_with(
+ ANY, "homeassistant", "restart", ANY, blocking=False, context=ANY, target=ANY
+ )
+
+
+async def test_call_service_target(hass, websocket_client):
+ """Test call service command with target."""
+ calls = async_mock_service(hass, "domain_test", "test_service")
await websocket_client.send_json(
{
@@ -36,6 +141,10 @@ def service_call(call):
"domain": "domain_test",
"service": "test_service",
"service_data": {"hello": "world"},
+ "target": {
+ "entity_id": ["entity.one", "entity.two"],
+ "device_id": "deviceid",
+ },
}
)
@@ -49,7 +158,34 @@ def service_call(call):
assert call.domain == "domain_test"
assert call.service == "test_service"
- assert call.data == {"hello": "world"}
+ assert call.data == {
+ "hello": "world",
+ "entity_id": ["entity.one", "entity.two"],
+ "device_id": ["deviceid"],
+ }
+ assert call.context.as_dict() == msg["result"]["context"]
+
+
+async def test_call_service_target_template(hass, websocket_client):
+ """Test call service command with target does not allow template."""
+ await websocket_client.send_json(
+ {
+ "id": 5,
+ "type": "call_service",
+ "domain": "domain_test",
+ "service": "test_service",
+ "service_data": {"hello": "world"},
+ "target": {
+ "entity_id": "{{ 1 }}",
+ },
+ }
+ )
+
+ msg = await websocket_client.receive_json()
+ assert msg["id"] == 5
+ assert msg["type"] == const.TYPE_RESULT
+ assert not msg["success"]
+ assert msg["error"]["code"] == const.ERR_INVALID_FORMAT
async def test_call_service_not_found(hass, websocket_client):
@@ -191,7 +327,6 @@ async def unknown_error_call(_):
)
msg = await websocket_client.receive_json()
- print(msg)
assert msg["id"] == 5
assert msg["type"] == const.TYPE_RESULT
assert msg["success"] is False
@@ -208,7 +343,6 @@ async def unknown_error_call(_):
)
msg = await websocket_client.receive_json()
- print(msg)
assert msg["id"] == 6
assert msg["type"] == const.TYPE_RESULT
assert msg["success"] is False
@@ -936,3 +1070,100 @@ async def test_test_condition(hass, websocket_client):
assert msg["type"] == const.TYPE_RESULT
assert msg["success"]
assert msg["result"]["result"] is True
+
+
+async def test_execute_script(hass, websocket_client):
+ """Test testing a condition."""
+ calls = async_mock_service(hass, "domain_test", "test_service")
+
+ await websocket_client.send_json(
+ {
+ "id": 5,
+ "type": "execute_script",
+ "sequence": [
+ {
+ "service": "domain_test.test_service",
+ "data": {"hello": "world"},
+ }
+ ],
+ }
+ )
+
+ msg_no_var = await websocket_client.receive_json()
+ assert msg_no_var["id"] == 5
+ assert msg_no_var["type"] == const.TYPE_RESULT
+ assert msg_no_var["success"]
+
+ await websocket_client.send_json(
+ {
+ "id": 6,
+ "type": "execute_script",
+ "sequence": {
+ "service": "domain_test.test_service",
+ "data": {"hello": "{{ name }}"},
+ },
+ "variables": {"name": "From variable"},
+ }
+ )
+
+ msg_var = await websocket_client.receive_json()
+ assert msg_var["id"] == 6
+ assert msg_var["type"] == const.TYPE_RESULT
+ assert msg_var["success"]
+
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+ assert len(calls) == 2
+
+ call = calls[0]
+ assert call.domain == "domain_test"
+ assert call.service == "test_service"
+ assert call.data == {"hello": "world"}
+ assert call.context.as_dict() == msg_no_var["result"]["context"]
+
+ call = calls[1]
+ assert call.domain == "domain_test"
+ assert call.service == "test_service"
+ assert call.data == {"hello": "From variable"}
+ assert call.context.as_dict() == msg_var["result"]["context"]
+
+
+async def test_subscribe_unsubscribe_bootstrap_integrations(
+ hass, websocket_client, hass_admin_user
+):
+ """Test subscribe/unsubscribe bootstrap_integrations."""
+ await websocket_client.send_json(
+ {"id": 7, "type": "subscribe_bootstrap_integrations"}
+ )
+
+ msg = await websocket_client.receive_json()
+ assert msg["id"] == 7
+ assert msg["type"] == const.TYPE_RESULT
+ assert msg["success"]
+
+ message = {"august": 12.5, "isy994": 12.8}
+
+ async_dispatcher_send(hass, SIGNAL_BOOTSTRAP_INTEGRATONS, message)
+ msg = await websocket_client.receive_json()
+ assert msg["id"] == 7
+ assert msg["type"] == "event"
+ assert msg["event"] == message
+
+
+async def test_integration_setup_info(hass, websocket_client, hass_admin_user):
+ """Test subscribe/unsubscribe bootstrap_integrations."""
+ hass.data[DATA_SETUP_TIME] = {
+ "august": datetime.timedelta(seconds=12.5),
+ "isy994": datetime.timedelta(seconds=12.8),
+ }
+ await websocket_client.send_json({"id": 7, "type": "integration/setup_info"})
+
+ msg = await websocket_client.receive_json()
+ assert msg["id"] == 7
+ assert msg["type"] == const.TYPE_RESULT
+ assert msg["success"]
+ assert msg["result"] == [
+ {"domain": "august", "seconds": 12.5},
+ {"domain": "isy994", "seconds": 12.8},
+ ]
diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py
index d3cf4b854f8251..f3952f1dc4b74a 100644
--- a/tests/components/websocket_api/test_http.py
+++ b/tests/components/websocket_api/test_http.py
@@ -67,7 +67,7 @@ def instantiate_handler(*args):
async def test_non_json_message(hass, websocket_client, caplog):
- """Test trying to serialze non JSON objects."""
+ """Test trying to serialize non JSON objects."""
bad_data = object()
hass.states.async_set("test_domain.entity", "testing", {"bad": bad_data})
await websocket_client.send_json({"id": 5, "type": "get_states"})
@@ -77,6 +77,6 @@ async def test_non_json_message(hass, websocket_client, caplog):
assert msg["type"] == const.TYPE_RESULT
assert not msg["success"]
assert (
- f"Unable to serialize to JSON. Bad data found at $.result[0](state: test_domain.entity).attributes.bad={bad_data}("
+ f"Unable to serialize to JSON. Bad data found at $.result[0](State: test_domain.entity).attributes.bad={bad_data}("
in caplog.text
)
diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py
index 0e0a69216b2acc..69b4b84dcd3994 100644
--- a/tests/components/wemo/conftest.py
+++ b/tests/components/wemo/conftest.py
@@ -7,6 +7,7 @@
from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC
from homeassistant.components.wemo.const import DOMAIN
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
MOCK_HOST = "127.0.0.1"
@@ -72,7 +73,7 @@ async def async_wemo_entity_fixture(hass, pywemo_device):
)
await hass.async_block_till_done()
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
entity_entries = list(entity_registry.entities.values())
assert len(entity_entries) == 1
diff --git a/tests/components/wemo/entity_test_helpers.py b/tests/components/wemo/entity_test_helpers.py
index 0ecfc46d526487..e584cb5fb39d91 100644
--- a/tests/components/wemo/entity_test_helpers.py
+++ b/tests/components/wemo/entity_test_helpers.py
@@ -6,6 +6,9 @@
import threading
from unittest.mock import patch
+import async_timeout
+from pywemo.ouimeaux_device.api.service import ActionException
+
from homeassistant.components.homeassistant import (
DOMAIN as HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
@@ -127,7 +130,7 @@ async def test_async_locked_update_with_exception(
assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF
await async_setup_component(hass, HA_DOMAIN, {})
update_polling_method = update_polling_method or pywemo_device.get_state
- update_polling_method.side_effect = AttributeError
+ update_polling_method.side_effect = ActionException
await hass.services.async_call(
HA_DOMAIN,
@@ -137,7 +140,6 @@ async def test_async_locked_update_with_exception(
)
assert hass.states.get(wemo_entity.entity_id).state == STATE_UNAVAILABLE
- pywemo_device.reconnect_with_device.assert_called_with()
async def test_async_update_with_timeout_and_recovery(hass, wemo_entity, pywemo_device):
@@ -145,7 +147,19 @@ async def test_async_update_with_timeout_and_recovery(hass, wemo_entity, pywemo_
assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF
await async_setup_component(hass, HA_DOMAIN, {})
- with patch("async_timeout.timeout", side_effect=asyncio.TimeoutError):
+ event = threading.Event()
+
+ def get_state(*args):
+ event.wait()
+ return 0
+
+ if hasattr(pywemo_device, "bridge_update"):
+ pywemo_device.bridge_update.side_effect = get_state
+ else:
+ pywemo_device.get_state.side_effect = get_state
+ timeout = async_timeout.timeout(0)
+
+ with patch("async_timeout.timeout", return_value=timeout):
await hass.services.async_call(
HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
@@ -156,11 +170,6 @@ async def test_async_update_with_timeout_and_recovery(hass, wemo_entity, pywemo_
assert hass.states.get(wemo_entity.entity_id).state == STATE_UNAVAILABLE
# Check that the entity recovers and is available after the update succeeds.
- await hass.services.async_call(
- HA_DOMAIN,
- SERVICE_UPDATE_ENTITY,
- {ATTR_ENTITY_ID: [wemo_entity.entity_id]},
- blocking=True,
- )
-
+ event.set()
+ await hass.async_block_till_done()
assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF
diff --git a/tests/components/wemo/test_init.py b/tests/components/wemo/test_init.py
index 374222d86888da..1164af7cf958d6 100644
--- a/tests/components/wemo/test_init.py
+++ b/tests/components/wemo/test_init.py
@@ -6,6 +6,7 @@
from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC, WemoDiscovery
from homeassistant.components.wemo.const import DOMAIN
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util import dt
@@ -41,7 +42,7 @@ async def test_static_duplicate_static_entry(hass, pywemo_device):
},
)
await hass.async_block_till_done()
- entity_reg = await hass.helpers.entity_registry.async_get_registry()
+ entity_reg = er.async_get(hass)
entity_entries = list(entity_reg.entities.values())
assert len(entity_entries) == 1
@@ -59,7 +60,7 @@ async def test_static_config_with_port(hass, pywemo_device):
},
)
await hass.async_block_till_done()
- entity_reg = await hass.helpers.entity_registry.async_get_registry()
+ entity_reg = er.async_get(hass)
entity_entries = list(entity_reg.entities.values())
assert len(entity_entries) == 1
@@ -77,7 +78,7 @@ async def test_static_config_without_port(hass, pywemo_device):
},
)
await hass.async_block_till_done()
- entity_reg = await hass.helpers.entity_registry.async_get_registry()
+ entity_reg = er.async_get(hass)
entity_entries = list(entity_reg.entities.values())
assert len(entity_entries) == 1
@@ -132,7 +133,7 @@ def create_device(counter):
await hass.async_block_till_done()
# Verify that the expected number of devices were setup.
- entity_reg = await hass.helpers.entity_registry.async_get_registry()
+ entity_reg = er.async_get(hass)
entity_entries = list(entity_reg.entities.values())
assert len(entity_entries) == 3
diff --git a/tests/components/wemo/test_light_bridge.py b/tests/components/wemo/test_light_bridge.py
index 3e7f79200c6504..573f75a66d92de 100644
--- a/tests/components/wemo/test_light_bridge.py
+++ b/tests/components/wemo/test_light_bridge.py
@@ -69,9 +69,10 @@ async def test_async_update_with_timeout_and_recovery(
hass, pywemo_bridge_light, wemo_entity, pywemo_device
):
"""Test that the entity becomes unavailable after a timeout, and that it recovers."""
- await entity_test_helpers.test_async_update_with_timeout_and_recovery(
- hass, wemo_entity, pywemo_device
- )
+ with _bypass_throttling():
+ await entity_test_helpers.test_async_update_with_timeout_and_recovery(
+ hass, wemo_entity, pywemo_device
+ )
async def test_async_locked_update_with_exception(
diff --git a/tests/components/wilight/__init__.py b/tests/components/wilight/__init__.py
index e1c3134523598e..7ee7f0119a460c 100644
--- a/tests/components/wilight/__init__.py
+++ b/tests/components/wilight/__init__.py
@@ -1,4 +1,7 @@
"""Tests for the WiLight component."""
+
+from pywilight.const import DOMAIN
+
from homeassistant.components.ssdp import (
ATTR_SSDP_LOCATION,
ATTR_UPNP_MANUFACTURER,
@@ -10,7 +13,6 @@
CONF_MODEL_NAME,
CONF_SERIAL_NUMBER,
)
-from homeassistant.components.wilight.const import DOMAIN
from homeassistant.const import CONF_HOST
from homeassistant.helpers.typing import HomeAssistantType
@@ -24,6 +26,7 @@
UPNP_MODEL_NAME_DIMMER = "WiLight 0100001700020009-10010010"
UPNP_MODEL_NAME_COLOR = "WiLight 0107001800020009-11010"
UPNP_MODEL_NAME_LIGHT_FAN = "WiLight 0104001800010009-10"
+UPNP_MODEL_NAME_COVER = "WiLight 0103001800010009-10"
UPNP_MODEL_NUMBER = "123456789012345678901234567890123456"
UPNP_SERIAL = "000000000099"
UPNP_MAC_ADDRESS = "5C:CF:7F:8B:CA:56"
@@ -53,14 +56,6 @@
ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL,
}
-MOCK_SSDP_DISCOVERY_INFO_LIGHT_FAN = {
- ATTR_SSDP_LOCATION: SSDP_LOCATION,
- ATTR_UPNP_MANUFACTURER: UPNP_MANUFACTURER,
- ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_LIGHT_FAN,
- ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER,
- ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL,
-}
-
async def setup_integration(
hass: HomeAssistantType,
diff --git a/tests/components/wilight/conftest.py b/tests/components/wilight/conftest.py
index a8fd13553dddc9..b20a7757e22b9b 100644
--- a/tests/components/wilight/conftest.py
+++ b/tests/components/wilight/conftest.py
@@ -1,2 +1,2 @@
"""wilight conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py
index d44780092ec28d..9888dbe3ef945f 100644
--- a/tests/components/wilight/test_config_flow.py
+++ b/tests/components/wilight/test_config_flow.py
@@ -2,12 +2,12 @@
from unittest.mock import patch
import pytest
+from pywilight.const import DOMAIN
from homeassistant.components.wilight.config_flow import (
CONF_MODEL_NAME,
CONF_SERIAL_NUMBER,
)
-from homeassistant.components.wilight.const import DOMAIN
from homeassistant.config_entries import SOURCE_SSDP
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE
from homeassistant.data_entry_flow import (
diff --git a/tests/components/wilight/test_cover.py b/tests/components/wilight/test_cover.py
new file mode 100644
index 00000000000000..8b058d95836448
--- /dev/null
+++ b/tests/components/wilight/test_cover.py
@@ -0,0 +1,137 @@
+"""Tests for the WiLight integration."""
+from unittest.mock import patch
+
+import pytest
+import pywilight
+
+from homeassistant.components.cover import (
+ ATTR_CURRENT_POSITION,
+ ATTR_POSITION,
+ DOMAIN as COVER_DOMAIN,
+)
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ SERVICE_CLOSE_COVER,
+ SERVICE_OPEN_COVER,
+ SERVICE_SET_COVER_POSITION,
+ SERVICE_STOP_COVER,
+ STATE_CLOSED,
+ STATE_CLOSING,
+ STATE_OPEN,
+ STATE_OPENING,
+)
+from homeassistant.helpers import entity_registry as er
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import (
+ HOST,
+ UPNP_MAC_ADDRESS,
+ UPNP_MODEL_NAME_COVER,
+ UPNP_MODEL_NUMBER,
+ UPNP_SERIAL,
+ WILIGHT_ID,
+ setup_integration,
+)
+
+
+@pytest.fixture(name="dummy_device_from_host_cover")
+def mock_dummy_device_from_host_light_fan():
+ """Mock a valid api_devce."""
+
+ device = pywilight.wilight_from_discovery(
+ f"http://{HOST}:45995/wilight.xml",
+ UPNP_MAC_ADDRESS,
+ UPNP_MODEL_NAME_COVER,
+ UPNP_SERIAL,
+ UPNP_MODEL_NUMBER,
+ )
+
+ device.set_dummy(True)
+
+ with patch(
+ "pywilight.device_from_host",
+ return_value=device,
+ ):
+ yield device
+
+
+async def test_loading_cover(
+ hass: HomeAssistantType,
+ dummy_device_from_host_cover,
+) -> None:
+ """Test the WiLight configuration entry loading."""
+
+ entry = await setup_integration(hass)
+ assert entry
+ assert entry.unique_id == WILIGHT_ID
+
+ entity_registry = er.async_get(hass)
+
+ # First segment of the strip
+ state = hass.states.get("cover.wl000000000099_1")
+ assert state
+ assert state.state == STATE_CLOSED
+
+ entry = entity_registry.async_get("cover.wl000000000099_1")
+ assert entry
+ assert entry.unique_id == "WL000000000099_0"
+
+
+async def test_open_close_cover_state(
+ hass: HomeAssistantType, dummy_device_from_host_cover
+) -> None:
+ """Test the change of state of the cover."""
+ await setup_integration(hass)
+
+ # Open
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_OPEN_COVER,
+ {ATTR_ENTITY_ID: "cover.wl000000000099_1"},
+ blocking=True,
+ )
+
+ await hass.async_block_till_done()
+ state = hass.states.get("cover.wl000000000099_1")
+ assert state
+ assert state.state == STATE_OPENING
+
+ # Close
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_CLOSE_COVER,
+ {ATTR_ENTITY_ID: "cover.wl000000000099_1"},
+ blocking=True,
+ )
+
+ await hass.async_block_till_done()
+ state = hass.states.get("cover.wl000000000099_1")
+ assert state
+ assert state.state == STATE_CLOSING
+
+ # Set position
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_SET_COVER_POSITION,
+ {ATTR_POSITION: 50, ATTR_ENTITY_ID: "cover.wl000000000099_1"},
+ blocking=True,
+ )
+
+ await hass.async_block_till_done()
+ state = hass.states.get("cover.wl000000000099_1")
+ assert state
+ assert state.state == STATE_OPEN
+ assert state.attributes.get(ATTR_CURRENT_POSITION) == 50
+
+ # Stop
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_STOP_COVER,
+ {ATTR_ENTITY_ID: "cover.wl000000000099_1"},
+ blocking=True,
+ )
+
+ await hass.async_block_till_done()
+ state = hass.states.get("cover.wl000000000099_1")
+ assert state
+ assert state.state == STATE_OPEN
diff --git a/tests/components/wilight/test_fan.py b/tests/components/wilight/test_fan.py
index 9b656236b93419..0ad7789c52cfe7 100644
--- a/tests/components/wilight/test_fan.py
+++ b/tests/components/wilight/test_fan.py
@@ -6,15 +6,12 @@
from homeassistant.components.fan import (
ATTR_DIRECTION,
- ATTR_SPEED,
+ ATTR_PERCENTAGE,
DIRECTION_FORWARD,
DIRECTION_REVERSE,
DOMAIN as FAN_DOMAIN,
SERVICE_SET_DIRECTION,
- SERVICE_SET_SPEED,
- SPEED_HIGH,
- SPEED_LOW,
- SPEED_MEDIUM,
+ SERVICE_SET_PERCENTAGE,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -23,6 +20,7 @@
STATE_OFF,
STATE_ON,
)
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.typing import HomeAssistantType
from . import (
@@ -67,7 +65,7 @@ async def test_loading_light_fan(
assert entry
assert entry.unique_id == WILIGHT_ID
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
# First segment of the strip
state = hass.states.get("fan.wl000000000099_2")
@@ -102,7 +100,7 @@ async def test_on_off_fan_state(
await hass.services.async_call(
FAN_DOMAIN,
SERVICE_TURN_ON,
- {ATTR_SPEED: SPEED_LOW, ATTR_ENTITY_ID: "fan.wl000000000099_2"},
+ {ATTR_PERCENTAGE: 30, ATTR_ENTITY_ID: "fan.wl000000000099_2"},
blocking=True,
)
@@ -110,7 +108,7 @@ async def test_on_off_fan_state(
state = hass.states.get("fan.wl000000000099_2")
assert state
assert state.state == STATE_ON
- assert state.attributes.get(ATTR_SPEED) == SPEED_LOW
+ assert state.attributes.get(ATTR_PERCENTAGE) == 33
# Turn off
await hass.services.async_call(
@@ -135,41 +133,41 @@ async def test_speed_fan_state(
# Set speed Low
await hass.services.async_call(
FAN_DOMAIN,
- SERVICE_SET_SPEED,
- {ATTR_SPEED: SPEED_LOW, ATTR_ENTITY_ID: "fan.wl000000000099_2"},
+ SERVICE_SET_PERCENTAGE,
+ {ATTR_PERCENTAGE: 30, ATTR_ENTITY_ID: "fan.wl000000000099_2"},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("fan.wl000000000099_2")
assert state
- assert state.attributes.get(ATTR_SPEED) == SPEED_LOW
+ assert state.attributes.get(ATTR_PERCENTAGE) == 33
# Set speed Medium
await hass.services.async_call(
FAN_DOMAIN,
- SERVICE_SET_SPEED,
- {ATTR_SPEED: SPEED_MEDIUM, ATTR_ENTITY_ID: "fan.wl000000000099_2"},
+ SERVICE_SET_PERCENTAGE,
+ {ATTR_PERCENTAGE: 50, ATTR_ENTITY_ID: "fan.wl000000000099_2"},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("fan.wl000000000099_2")
assert state
- assert state.attributes.get(ATTR_SPEED) == SPEED_MEDIUM
+ assert state.attributes.get(ATTR_PERCENTAGE) == 66
# Set speed High
await hass.services.async_call(
FAN_DOMAIN,
- SERVICE_SET_SPEED,
- {ATTR_SPEED: SPEED_HIGH, ATTR_ENTITY_ID: "fan.wl000000000099_2"},
+ SERVICE_SET_PERCENTAGE,
+ {ATTR_PERCENTAGE: 90, ATTR_ENTITY_ID: "fan.wl000000000099_2"},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("fan.wl000000000099_2")
assert state
- assert state.attributes.get(ATTR_SPEED) == SPEED_HIGH
+ assert state.attributes.get(ATTR_PERCENTAGE) == 100
async def test_direction_fan_state(
diff --git a/tests/components/wilight/test_init.py b/tests/components/wilight/test_init.py
index c1557fb44d3438..1441564b640d49 100644
--- a/tests/components/wilight/test_init.py
+++ b/tests/components/wilight/test_init.py
@@ -3,8 +3,8 @@
import pytest
import pywilight
+from pywilight.const import DOMAIN
-from homeassistant.components.wilight.const import DOMAIN
from homeassistant.config_entries import (
ENTRY_STATE_LOADED,
ENTRY_STATE_NOT_LOADED,
diff --git a/tests/components/wilight/test_light.py b/tests/components/wilight/test_light.py
index b7250df546db51..9abe17ce9e56fc 100644
--- a/tests/components/wilight/test_light.py
+++ b/tests/components/wilight/test_light.py
@@ -16,6 +16,7 @@
STATE_OFF,
STATE_ON,
)
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.typing import HomeAssistantType
from tests.components.wilight import (
@@ -140,7 +141,7 @@ async def test_loading_light(
assert entry
assert entry.unique_id == WILIGHT_ID
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
# First segment of the strip
state = hass.states.get("light.wl000000000099_1")
diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py
index 80e6d07654c8da..4d3552bf6626c3 100644
--- a/tests/components/withings/common.py
+++ b/tests/components/withings/common.py
@@ -1,6 +1,7 @@
"""Common data for for the withings component tests."""
+from __future__ import annotations
+
from dataclasses import dataclass
-from typing import List, Optional, Tuple, Union
from unittest.mock import MagicMock
from urllib.parse import urlparse
@@ -49,27 +50,21 @@ class ProfileConfig:
profile: str
user_id: int
- api_response_user_get_device: Union[UserGetDeviceResponse, Exception]
- api_response_measure_get_meas: Union[MeasureGetMeasResponse, Exception]
- api_response_sleep_get_summary: Union[SleepGetSummaryResponse, Exception]
- api_response_notify_list: Union[NotifyListResponse, Exception]
- api_response_notify_revoke: Optional[Exception]
+ api_response_user_get_device: UserGetDeviceResponse | Exception
+ api_response_measure_get_meas: MeasureGetMeasResponse | Exception
+ api_response_sleep_get_summary: SleepGetSummaryResponse | Exception
+ api_response_notify_list: NotifyListResponse | Exception
+ api_response_notify_revoke: Exception | None
def new_profile_config(
profile: str,
user_id: int,
- api_response_user_get_device: Optional[
- Union[UserGetDeviceResponse, Exception]
- ] = None,
- api_response_measure_get_meas: Optional[
- Union[MeasureGetMeasResponse, Exception]
- ] = None,
- api_response_sleep_get_summary: Optional[
- Union[SleepGetSummaryResponse, Exception]
- ] = None,
- api_response_notify_list: Optional[Union[NotifyListResponse, Exception]] = None,
- api_response_notify_revoke: Optional[Exception] = None,
+ api_response_user_get_device: UserGetDeviceResponse | Exception | None = None,
+ api_response_measure_get_meas: MeasureGetMeasResponse | Exception | None = None,
+ api_response_sleep_get_summary: SleepGetSummaryResponse | Exception | None = None,
+ api_response_notify_list: NotifyListResponse | Exception | None = None,
+ api_response_notify_revoke: Exception | None = None,
) -> ProfileConfig:
"""Create a new profile config immutable object."""
return ProfileConfig(
@@ -118,13 +113,13 @@ def __init__(
self._aioclient_mock = aioclient_mock
self._client_id = None
self._client_secret = None
- self._profile_configs: Tuple[ProfileConfig, ...] = ()
+ self._profile_configs: tuple[ProfileConfig, ...] = ()
async def configure_component(
self,
client_id: str = "my_client_id",
client_secret: str = "my_client_secret",
- profile_configs: Tuple[ProfileConfig, ...] = (),
+ profile_configs: tuple[ProfileConfig, ...] = (),
) -> None:
"""Configure the wihings component."""
self._client_id = client_id
@@ -294,7 +289,7 @@ async def unload(self, profile: ProfileConfig) -> None:
def get_config_entries_for_user_id(
hass: HomeAssistant, user_id: int
-) -> Tuple[ConfigEntry]:
+) -> tuple[ConfigEntry]:
"""Get a list of config entries that apply to a specific withings user."""
return tuple(
[
@@ -305,7 +300,7 @@ def get_config_entries_for_user_id(
)
-def async_get_flow_for_user_id(hass: HomeAssistant, user_id: int) -> List[dict]:
+def async_get_flow_for_user_id(hass: HomeAssistant, user_id: int) -> list[dict]:
"""Get a flow for a user id."""
return [
flow
@@ -316,7 +311,7 @@ def async_get_flow_for_user_id(hass: HomeAssistant, user_id: int) -> List[dict]:
def get_data_manager_by_user_id(
hass: HomeAssistant, user_id: int
-) -> Optional[DataManager]:
+) -> DataManager | None:
"""Get a data manager by the user id."""
return next(
iter(
diff --git a/tests/components/withings/test_binary_sensor.py b/tests/components/withings/test_binary_sensor.py
index 3477671ea7960d..9f93e00f4ef320 100644
--- a/tests/components/withings/test_binary_sensor.py
+++ b/tests/components/withings/test_binary_sensor.py
@@ -8,22 +8,21 @@
from homeassistant.components.withings.const import Measurement
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_registry import EntityRegistry
from .common import ComponentFactory, new_profile_config
async def test_binary_sensor(
- hass: HomeAssistant, component_factory: ComponentFactory
+ hass: HomeAssistant, component_factory: ComponentFactory, current_request_with_host
) -> None:
"""Test binary sensor."""
in_bed_attribute = WITHINGS_MEASUREMENTS_MAP[Measurement.IN_BED]
person0 = new_profile_config("person0", 0)
person1 = new_profile_config("person1", 1)
- entity_registry: EntityRegistry = (
- await hass.helpers.entity_registry.async_get_registry()
- )
+ entity_registry: EntityRegistry = er.async_get(hass)
await component_factory.configure_component(profile_configs=(person0, person1))
assert not await async_get_entity_id(hass, in_bed_attribute, person0.user_id)
diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py
index a5946ff0533ce3..ef51f12398f5d9 100644
--- a/tests/components/withings/test_common.py
+++ b/tests/components/withings/test_common.py
@@ -74,6 +74,7 @@ async def test_webhook_post(
arg_user_id: Any,
arg_appli: Any,
expected_code: int,
+ current_request_with_host,
) -> None:
"""Test webhook callback."""
person0 = new_profile_config("person0", user_id)
@@ -107,6 +108,7 @@ async def test_webhook_head(
hass: HomeAssistant,
component_factory: ComponentFactory,
aiohttp_client,
+ current_request_with_host,
) -> None:
"""Test head method on webhook view."""
person0 = new_profile_config("person0", 0)
@@ -124,6 +126,7 @@ async def test_webhook_put(
hass: HomeAssistant,
component_factory: ComponentFactory,
aiohttp_client,
+ current_request_with_host,
) -> None:
"""Test webhook callback."""
person0 = new_profile_config("person0", 0)
diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py
index 8380c1340135da..4cbada948f4172 100644
--- a/tests/components/withings/test_config_flow.py
+++ b/tests/components/withings/test_config_flow.py
@@ -34,7 +34,7 @@ async def test_config_non_unique_profile(hass: HomeAssistant) -> None:
async def test_config_reauth_profile(
- hass: HomeAssistant, aiohttp_client, aioclient_mock
+ hass: HomeAssistant, aiohttp_client, aioclient_mock, current_request_with_host
) -> None:
"""Test reauth an existing profile re-creates the config entry."""
hass_config = {
diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py
index a9948860745227..465c26deb32611 100644
--- a/tests/components/withings/test_init.py
+++ b/tests/components/withings/test_init.py
@@ -125,7 +125,10 @@ async def test_async_setup_no_config(hass: HomeAssistant) -> None:
],
)
async def test_auth_failure(
- hass: HomeAssistant, component_factory: ComponentFactory, exception: Exception
+ hass: HomeAssistant,
+ component_factory: ComponentFactory,
+ exception: Exception,
+ current_request_with_host,
) -> None:
"""Test auth failure."""
person0 = new_profile_config(
diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py
index 16b83a585aa805..71e69967796e0a 100644
--- a/tests/components/withings/test_sensor.py
+++ b/tests/components/withings/test_sensor.py
@@ -27,6 +27,7 @@
)
from homeassistant.components.withings.const import Measurement
from homeassistant.core import HomeAssistant, State
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_registry import EntityRegistry
from .common import ComponentFactory, new_profile_config
@@ -301,12 +302,10 @@ def async_assert_state_equals(
async def test_sensor_default_enabled_entities(
- hass: HomeAssistant, component_factory: ComponentFactory
+ hass: HomeAssistant, component_factory: ComponentFactory, current_request_with_host
) -> None:
"""Test entities enabled by default."""
- entity_registry: EntityRegistry = (
- await hass.helpers.entity_registry.async_get_registry()
- )
+ entity_registry: EntityRegistry = er.async_get(hass)
await component_factory.configure_component(profile_configs=(PERSON0,))
@@ -344,12 +343,10 @@ async def test_sensor_default_enabled_entities(
async def test_all_entities(
- hass: HomeAssistant, component_factory: ComponentFactory
+ hass: HomeAssistant, component_factory: ComponentFactory, current_request_with_host
) -> None:
"""Test all entities."""
- entity_registry: EntityRegistry = (
- await hass.helpers.entity_registry.async_get_registry()
- )
+ entity_registry: EntityRegistry = er.async_get(hass)
with patch(
"homeassistant.components.withings.sensor.BaseWithingsSensor.entity_registry_enabled_default"
diff --git a/tests/components/wled/conftest.py b/tests/components/wled/conftest.py
index 31b71a92f198e7..7b8eb9cd50c26a 100644
--- a/tests/components/wled/conftest.py
+++ b/tests/components/wled/conftest.py
@@ -1,2 +1,2 @@
"""wled conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py
index eb9124ab906ed9..0077cea02025c0 100644
--- a/tests/components/wled/test_light.py
+++ b/tests/components/wled/test_light.py
@@ -36,6 +36,7 @@
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed, load_fixture
@@ -49,7 +50,7 @@ async def test_rgb_light_state(
"""Test the creation and values of the WLED lights."""
await init_integration(hass, aioclient_mock)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
# First segment of the strip
state = hass.states.get("light.wled_rgb_light_segment_0")
diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py
index 11e14bd79d9b6d..9cebf2cda32203 100644
--- a/tests/components/wled/test_sensor.py
+++ b/tests/components/wled/test_sensor.py
@@ -23,6 +23,7 @@
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
)
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from tests.components.wled import init_integration
@@ -35,7 +36,7 @@ async def test_sensors(
"""Test the creation and values of the WLED sensors."""
entry = await init_integration(hass, aioclient_mock, skip_setup=True)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
# Pre-create registry entries for disabled by default sensors
registry.async_get_or_create(
@@ -185,7 +186,7 @@ async def test_disabled_by_default_sensors(
) -> None:
"""Test the disabled by default WLED sensors."""
await init_integration(hass, aioclient_mock)
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
state = hass.states.get(entity_id)
assert state is None
diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py
index c6e30ef903ee85..ddeeee41ac8da9 100644
--- a/tests/components/wled/test_switch.py
+++ b/tests/components/wled/test_switch.py
@@ -20,6 +20,7 @@
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
from tests.components.wled import init_integration
from tests.test_util.aiohttp import AiohttpClientMocker
@@ -31,7 +32,7 @@ async def test_switch_state(
"""Test the creation and values of the WLED switches."""
await init_integration(hass, aioclient_mock)
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entity_registry = er.async_get(hass)
state = hass.states.get("switch.wled_rgb_light_nightlight")
assert state
diff --git a/tests/components/wsdot/test_sensor.py b/tests/components/wsdot/test_sensor.py
index 1b1c2e090f2b3b..bbb56efdedadc2 100644
--- a/tests/components/wsdot/test_sensor.py
+++ b/tests/components/wsdot/test_sensor.py
@@ -50,9 +50,9 @@ def add_entities(new_entities, update_before_add=False):
assert sensor.name == "I90 EB"
assert sensor.state == 11
assert (
- sensor.device_state_attributes[ATTR_DESCRIPTION]
+ sensor.extra_state_attributes[ATTR_DESCRIPTION]
== "Downtown Seattle to Downtown Bellevue via I-90"
)
- assert sensor.device_state_attributes[ATTR_TIME_UPDATED] == datetime(
+ assert sensor.extra_state_attributes[ATTR_TIME_UPDATED] == datetime(
2017, 1, 21, 15, 10, tzinfo=timezone(timedelta(hours=-8))
)
diff --git a/tests/components/xiaomi/test_device_tracker.py b/tests/components/xiaomi/test_device_tracker.py
index af5e192b9cfe98..b5764fac089ba3 100644
--- a/tests/components/xiaomi/test_device_tracker.py
+++ b/tests/components/xiaomi/test_device_tracker.py
@@ -226,9 +226,9 @@ async def test_valid_credential(mock_get, mock_post, hass):
}
scanner = get_scanner(hass, config)
assert scanner is not None
- assert 2 == len(scanner.scan_devices())
- assert "Device1" == scanner.get_device_name("23:83:BF:F6:38:A0")
- assert "Device2" == scanner.get_device_name("1D:98:EC:5E:D5:A6")
+ assert len(scanner.scan_devices()) == 2
+ assert scanner.get_device_name("23:83:BF:F6:38:A0") == "Device1"
+ assert scanner.get_device_name("1D:98:EC:5E:D5:A6") == "Device2"
@patch("requests.get", side_effect=mocked_requests)
@@ -250,6 +250,6 @@ async def test_token_timed_out(mock_get, mock_post, hass):
}
scanner = get_scanner(hass, config)
assert scanner is not None
- assert 2 == len(scanner.scan_devices())
- assert "Device1" == scanner.get_device_name("23:83:BF:F6:38:A0")
- assert "Device2" == scanner.get_device_name("1D:98:EC:5E:D5:A6")
+ assert len(scanner.scan_devices()) == 2
+ assert scanner.get_device_name("23:83:BF:F6:38:A0") == "Device1"
+ assert scanner.get_device_name("1D:98:EC:5E:D5:A6") == "Device2"
diff --git a/tests/components/xiaomi_aqara/test_config_flow.py b/tests/components/xiaomi_aqara/test_config_flow.py
index 280775a7130952..859338b82d3e59 100644
--- a/tests/components/xiaomi_aqara/test_config_flow.py
+++ b/tests/components/xiaomi_aqara/test_config_flow.py
@@ -5,9 +5,8 @@
import pytest
from homeassistant import config_entries
-from homeassistant.components import zeroconf
from homeassistant.components.xiaomi_aqara import config_flow, const
-from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT
+from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_PROTOCOL
ZEROCONF_NAME = "name"
ZEROCONF_PROP = "properties"
@@ -107,7 +106,7 @@ async def test_config_flow_user_success(hass):
CONF_PORT: TEST_PORT,
CONF_MAC: TEST_MAC,
const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE,
- const.CONF_PROTOCOL: TEST_PROTOCOL,
+ CONF_PROTOCOL: TEST_PROTOCOL,
const.CONF_KEY: TEST_KEY,
const.CONF_SID: TEST_SID,
}
@@ -159,7 +158,7 @@ async def test_config_flow_user_multiple_success(hass):
CONF_PORT: TEST_PORT,
CONF_MAC: TEST_MAC,
const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE,
- const.CONF_PROTOCOL: TEST_PROTOCOL,
+ CONF_PROTOCOL: TEST_PROTOCOL,
const.CONF_KEY: TEST_KEY,
const.CONF_SID: TEST_SID,
}
@@ -196,7 +195,7 @@ async def test_config_flow_user_no_key_success(hass):
CONF_PORT: TEST_PORT,
CONF_MAC: TEST_MAC,
const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE,
- const.CONF_PROTOCOL: TEST_PROTOCOL,
+ CONF_PROTOCOL: TEST_PROTOCOL,
const.CONF_KEY: None,
const.CONF_SID: TEST_SID,
}
@@ -243,7 +242,7 @@ async def test_config_flow_user_host_mac_success(hass):
CONF_PORT: TEST_PORT,
CONF_MAC: TEST_MAC,
const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE,
- const.CONF_PROTOCOL: TEST_PROTOCOL,
+ CONF_PROTOCOL: TEST_PROTOCOL,
const.CONF_KEY: None,
const.CONF_SID: TEST_SID,
}
@@ -402,7 +401,7 @@ async def test_zeroconf_success(hass):
const.DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
- zeroconf.ATTR_HOST: TEST_HOST,
+ CONF_HOST: TEST_HOST,
ZEROCONF_NAME: TEST_ZEROCONF_NAME,
ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC},
},
@@ -433,7 +432,7 @@ async def test_zeroconf_success(hass):
CONF_PORT: TEST_PORT,
CONF_MAC: TEST_MAC,
const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE,
- const.CONF_PROTOCOL: TEST_PROTOCOL,
+ CONF_PROTOCOL: TEST_PROTOCOL,
const.CONF_KEY: TEST_KEY,
const.CONF_SID: TEST_SID,
}
@@ -444,7 +443,7 @@ async def test_zeroconf_missing_data(hass):
result = await hass.config_entries.flow.async_init(
const.DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
- data={zeroconf.ATTR_HOST: TEST_HOST, ZEROCONF_NAME: TEST_ZEROCONF_NAME},
+ data={CONF_HOST: TEST_HOST, ZEROCONF_NAME: TEST_ZEROCONF_NAME},
)
assert result["type"] == "abort"
@@ -457,7 +456,7 @@ async def test_zeroconf_unknown_device(hass):
const.DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
- zeroconf.ATTR_HOST: TEST_HOST,
+ CONF_HOST: TEST_HOST,
ZEROCONF_NAME: "not-a-xiaomi-aqara-gateway",
ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC},
},
diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py
index dbe78957586ae5..de1ccbf1a8bceb 100644
--- a/tests/components/xiaomi_miio/test_config_flow.py
+++ b/tests/components/xiaomi_miio/test_config_flow.py
@@ -4,8 +4,8 @@
from miio import DeviceException
from homeassistant import config_entries
-from homeassistant.components import zeroconf
-from homeassistant.components.xiaomi_miio import config_flow, const
+from homeassistant.components.xiaomi_miio import const
+from homeassistant.components.xiaomi_miio.config_flow import DEFAULT_GATEWAY_NAME
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
ZEROCONF_NAME = "name"
@@ -15,8 +15,9 @@
TEST_HOST = "1.2.3.4"
TEST_TOKEN = "12345678901234567890123456789012"
TEST_NAME = "Test_Gateway"
-TEST_MODEL = "model5"
+TEST_MODEL = const.MODELS_GATEWAY[0]
TEST_MAC = "ab:cd:ef:gh:ij:kl"
+TEST_MAC_DEVICE = "abcdefghijkl"
TEST_GATEWAY_ID = TEST_MAC
TEST_HARDWARE_VERSION = "AB123"
TEST_FIRMWARE_VERSION = "1.2.3_456"
@@ -40,26 +41,6 @@ def get_mock_info(
return gateway_info
-async def test_config_flow_step_user_no_device(hass):
- """Test config flow, user step with no device selected."""
- result = await hass.config_entries.flow.async_init(
- const.DOMAIN, context={"source": config_entries.SOURCE_USER}
- )
-
- assert result["type"] == "form"
- assert result["step_id"] == "user"
- assert result["errors"] == {}
-
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {},
- )
-
- assert result["type"] == "form"
- assert result["step_id"] == "user"
- assert result["errors"] == {"base": "no_device_selected"}
-
-
async def test_config_flow_step_gateway_connect_error(hass):
"""Test config flow, gateway connection error."""
result = await hass.config_entries.flow.async_init(
@@ -67,29 +48,20 @@ async def test_config_flow_step_gateway_connect_error(hass):
)
assert result["type"] == "form"
- assert result["step_id"] == "user"
- assert result["errors"] == {}
-
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {config_flow.CONF_GATEWAY: True},
- )
-
- assert result["type"] == "form"
- assert result["step_id"] == "gateway"
+ assert result["step_id"] == "device"
assert result["errors"] == {}
with patch(
- "homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info",
+ "homeassistant.components.xiaomi_miio.device.Device.info",
side_effect=DeviceException({}),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {CONF_HOST: TEST_HOST, CONF_NAME: TEST_NAME, CONF_TOKEN: TEST_TOKEN},
+ {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN},
)
assert result["type"] == "form"
- assert result["step_id"] == "gateway"
+ assert result["step_id"] == "device"
assert result["errors"] == {"base": "cannot_connect"}
@@ -100,42 +72,30 @@ async def test_config_flow_gateway_success(hass):
)
assert result["type"] == "form"
- assert result["step_id"] == "user"
- assert result["errors"] == {}
-
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {config_flow.CONF_GATEWAY: True},
- )
-
- assert result["type"] == "form"
- assert result["step_id"] == "gateway"
+ assert result["step_id"] == "device"
assert result["errors"] == {}
mock_info = get_mock_info()
with patch(
- "homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info",
+ "homeassistant.components.xiaomi_miio.device.Device.info",
return_value=mock_info,
- ), patch(
- "homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.discover_devices",
- return_value=TEST_SUB_DEVICE_LIST,
), patch(
"homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {CONF_HOST: TEST_HOST, CONF_NAME: TEST_NAME, CONF_TOKEN: TEST_TOKEN},
+ {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN},
)
assert result["type"] == "create_entry"
- assert result["title"] == TEST_NAME
+ assert result["title"] == DEFAULT_GATEWAY_NAME
assert result["data"] == {
- config_flow.CONF_FLOW_TYPE: config_flow.CONF_GATEWAY,
+ const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
- "model": TEST_MODEL,
- "mac": TEST_MAC,
+ const.CONF_MODEL: TEST_MODEL,
+ const.CONF_MAC: TEST_MAC,
}
@@ -145,40 +105,37 @@ async def test_zeroconf_gateway_success(hass):
const.DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
- zeroconf.ATTR_HOST: TEST_HOST,
+ CONF_HOST: TEST_HOST,
ZEROCONF_NAME: TEST_ZEROCONF_NAME,
ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC},
},
)
assert result["type"] == "form"
- assert result["step_id"] == "gateway"
+ assert result["step_id"] == "device"
assert result["errors"] == {}
mock_info = get_mock_info()
with patch(
- "homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info",
+ "homeassistant.components.xiaomi_miio.device.Device.info",
return_value=mock_info,
- ), patch(
- "homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.discover_devices",
- return_value=TEST_SUB_DEVICE_LIST,
), patch(
"homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {CONF_NAME: TEST_NAME, CONF_TOKEN: TEST_TOKEN},
+ {CONF_TOKEN: TEST_TOKEN},
)
assert result["type"] == "create_entry"
- assert result["title"] == TEST_NAME
+ assert result["title"] == DEFAULT_GATEWAY_NAME
assert result["data"] == {
- config_flow.CONF_FLOW_TYPE: config_flow.CONF_GATEWAY,
+ const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
- "model": TEST_MODEL,
- "mac": TEST_MAC,
+ const.CONF_MODEL: TEST_MODEL,
+ const.CONF_MAC: TEST_MAC,
}
@@ -188,7 +145,7 @@ async def test_zeroconf_unknown_device(hass):
const.DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
- zeroconf.ATTR_HOST: TEST_HOST,
+ CONF_HOST: TEST_HOST,
ZEROCONF_NAME: "not-a-xiaomi-miio-device",
ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC},
},
@@ -213,8 +170,232 @@ async def test_zeroconf_missing_data(hass):
result = await hass.config_entries.flow.async_init(
const.DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
- data={zeroconf.ATTR_HOST: TEST_HOST, ZEROCONF_NAME: TEST_ZEROCONF_NAME},
+ data={CONF_HOST: TEST_HOST, ZEROCONF_NAME: TEST_ZEROCONF_NAME},
)
assert result["type"] == "abort"
assert result["reason"] == "not_xiaomi_miio"
+
+
+async def test_config_flow_step_device_connect_error(hass):
+ """Test config flow, device connection error."""
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "device"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.xiaomi_miio.device.Device.info",
+ side_effect=DeviceException({}),
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "device"
+ assert result["errors"] == {"base": "cannot_connect"}
+
+
+async def test_config_flow_step_unknown_device(hass):
+ """Test config flow, unknown device error."""
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "device"
+ assert result["errors"] == {}
+
+ mock_info = get_mock_info(model="UNKNOWN")
+
+ with patch(
+ "homeassistant.components.xiaomi_miio.device.Device.info",
+ return_value=mock_info,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "device"
+ assert result["errors"] == {"base": "unknown_device"}
+
+
+async def test_import_flow_success(hass):
+ """Test a successful import form yaml for a device."""
+ mock_info = get_mock_info(model=const.MODELS_SWITCH[0])
+
+ with patch(
+ "homeassistant.components.xiaomi_miio.device.Device.info",
+ return_value=mock_info,
+ ), patch(
+ "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
+ ):
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={CONF_NAME: TEST_NAME, CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN},
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == TEST_NAME
+ assert result["data"] == {
+ const.CONF_FLOW_TYPE: const.CONF_DEVICE,
+ CONF_HOST: TEST_HOST,
+ CONF_TOKEN: TEST_TOKEN,
+ const.CONF_MODEL: const.MODELS_SWITCH[0],
+ const.CONF_MAC: TEST_MAC,
+ }
+
+
+async def test_config_flow_step_device_manual_model_succes(hass):
+ """Test config flow, device connection error, manual model."""
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "device"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.xiaomi_miio.device.Device.info",
+ side_effect=DeviceException({}),
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "device"
+ assert result["errors"] == {"base": "cannot_connect"}
+
+ overwrite_model = const.MODELS_VACUUM[0]
+
+ with patch(
+ "homeassistant.components.xiaomi_miio.device.Device.info",
+ side_effect=DeviceException({}),
+ ), patch(
+ "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_TOKEN: TEST_TOKEN, const.CONF_MODEL: overwrite_model},
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == overwrite_model
+ assert result["data"] == {
+ const.CONF_FLOW_TYPE: const.CONF_DEVICE,
+ CONF_HOST: TEST_HOST,
+ CONF_TOKEN: TEST_TOKEN,
+ const.CONF_MODEL: overwrite_model,
+ const.CONF_MAC: None,
+ }
+
+
+async def config_flow_device_success(hass, model_to_test):
+ """Test a successful config flow for a device (base class)."""
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "device"
+ assert result["errors"] == {}
+
+ mock_info = get_mock_info(model=model_to_test)
+
+ with patch(
+ "homeassistant.components.xiaomi_miio.device.Device.info",
+ return_value=mock_info,
+ ), patch(
+ "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN},
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == model_to_test
+ assert result["data"] == {
+ const.CONF_FLOW_TYPE: const.CONF_DEVICE,
+ CONF_HOST: TEST_HOST,
+ CONF_TOKEN: TEST_TOKEN,
+ const.CONF_MODEL: model_to_test,
+ const.CONF_MAC: TEST_MAC,
+ }
+
+
+async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test):
+ """Test a successful zeroconf discovery of a device (base class)."""
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN,
+ context={"source": config_entries.SOURCE_ZEROCONF},
+ data={
+ CONF_HOST: TEST_HOST,
+ ZEROCONF_NAME: zeroconf_name_to_test,
+ ZEROCONF_PROP: {"poch": f"0:mac={TEST_MAC_DEVICE}\x00"},
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "device"
+ assert result["errors"] == {}
+
+ mock_info = get_mock_info(model=model_to_test)
+
+ with patch(
+ "homeassistant.components.xiaomi_miio.device.Device.info",
+ return_value=mock_info,
+ ), patch(
+ "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_TOKEN: TEST_TOKEN},
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == model_to_test
+ assert result["data"] == {
+ const.CONF_FLOW_TYPE: const.CONF_DEVICE,
+ CONF_HOST: TEST_HOST,
+ CONF_TOKEN: TEST_TOKEN,
+ const.CONF_MODEL: model_to_test,
+ const.CONF_MAC: TEST_MAC,
+ }
+
+
+async def test_config_flow_plug_success(hass):
+ """Test a successful config flow for a plug."""
+ test_plug_model = const.MODELS_SWITCH[0]
+ await config_flow_device_success(hass, test_plug_model)
+
+
+async def test_zeroconf_plug_success(hass):
+ """Test a successful zeroconf discovery of a plug."""
+ test_plug_model = const.MODELS_SWITCH[0]
+ test_zeroconf_name = const.MODELS_SWITCH[0].replace(".", "-")
+ await zeroconf_device_success(hass, test_zeroconf_name, test_plug_model)
+
+
+async def test_config_flow_vacuum_success(hass):
+ """Test a successful config flow for a vacuum."""
+ test_vacuum_model = const.MODELS_VACUUM[0]
+ await config_flow_device_success(hass, test_vacuum_model)
+
+
+async def test_zeroconf_vacuum_success(hass):
+ """Test a successful zeroconf discovery of a vacuum."""
+ test_vacuum_model = const.MODELS_VACUUM[0]
+ test_zeroconf_name = const.MODELS_VACUUM[0].replace(".", "-")
+ await zeroconf_device_success(hass, test_zeroconf_name, test_vacuum_model)
diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py
index b1a3c08b84b690..23e5d8884b34e0 100644
--- a/tests/components/xiaomi_miio/test_vacuum.py
+++ b/tests/components/xiaomi_miio/test_vacuum.py
@@ -22,6 +22,7 @@
STATE_CLEANING,
STATE_ERROR,
)
+from homeassistant.components.xiaomi_miio import const
from homeassistant.components.xiaomi_miio.const import DOMAIN as XIAOMI_DOMAIN
from homeassistant.components.xiaomi_miio.vacuum import (
ATTR_CLEANED_AREA,
@@ -38,7 +39,6 @@
ATTR_SIDE_BRUSH_LEFT,
ATTR_TIMERS,
CONF_HOST,
- CONF_NAME,
CONF_TOKEN,
SERVICE_CLEAN_SEGMENT,
SERVICE_CLEAN_ZONE,
@@ -51,12 +51,14 @@
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
- CONF_PLATFORM,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
)
-from homeassistant.setup import async_setup_component
+
+from .test_config_flow import TEST_MAC
+
+from tests.common import MockConfigEntry
PLATFORM = "xiaomi_miio"
@@ -521,17 +523,21 @@ async def setup_component(hass, entity_name):
"""Set up vacuum component."""
entity_id = f"{DOMAIN}.{entity_name}"
- await async_setup_component(
- hass,
- DOMAIN,
- {
- DOMAIN: {
- CONF_PLATFORM: PLATFORM,
- CONF_HOST: "192.168.1.100",
- CONF_NAME: entity_name,
- CONF_TOKEN: "12345678901234567890123456789012",
- }
+ config_entry = MockConfigEntry(
+ domain=XIAOMI_DOMAIN,
+ unique_id="123456",
+ title=entity_name,
+ data={
+ const.CONF_FLOW_TYPE: const.CONF_DEVICE,
+ CONF_HOST: "192.168.1.100",
+ CONF_TOKEN: "12345678901234567890123456789012",
+ const.CONF_MODEL: const.MODELS_VACUUM[0],
+ const.CONF_MAC: TEST_MAC,
},
)
+
+ config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
+
return entity_id
diff --git a/tests/components/yeelight/conftest.py b/tests/components/yeelight/conftest.py
index 3e65a60f374d41..f418e90e84840c 100644
--- a/tests/components/yeelight/conftest.py
+++ b/tests/components/yeelight/conftest.py
@@ -1,2 +1,2 @@
"""yeelight conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py
index 8fa1ba5c9880b4..6a1508d78965fb 100644
--- a/tests/components/yeelight/test_config_flow.py
+++ b/tests/components/yeelight/test_config_flow.py
@@ -3,7 +3,6 @@
from homeassistant import config_entries
from homeassistant.components.yeelight import (
- CONF_DEVICE,
CONF_MODE_MUSIC,
CONF_MODEL,
CONF_NIGHTLIGHT_SWITCH,
@@ -18,7 +17,7 @@
DOMAIN,
NIGHTLIGHT_SWITCH_TYPE_LIGHT,
)
-from homeassistant.const import CONF_HOST, CONF_ID, CONF_NAME
+from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_NAME
from homeassistant.core import HomeAssistant
from . import (
diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py
index c91ae33d9860d9..b6a59809d30c7a 100644
--- a/tests/components/yeelight/test_init.py
+++ b/tests/components/yeelight/test_init.py
@@ -11,9 +11,9 @@
DOMAIN,
NIGHTLIGHT_SWITCH_TYPE_LIGHT,
)
-from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME
+from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from . import (
@@ -50,6 +50,12 @@ async def test_setup_discovery(hass: HomeAssistant):
# Unload
assert await hass.config_entries.async_unload(config_entry.entry_id)
+ assert hass.states.get(ENTITY_BINARY_SENSOR).state == STATE_UNAVAILABLE
+ assert hass.states.get(ENTITY_LIGHT).state == STATE_UNAVAILABLE
+
+ # Remove
+ assert await hass.config_entries.async_remove(config_entry.entry_id)
+ await hass.async_block_till_done()
assert hass.states.get(ENTITY_BINARY_SENSOR) is None
assert hass.states.get(ENTITY_LIGHT) is None
@@ -100,11 +106,14 @@ async def test_unique_ids_device(hass: HomeAssistant):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
- er = await entity_registry.async_get_registry(hass)
- assert er.async_get(ENTITY_BINARY_SENSOR).unique_id == f"{ID}-nightlight_sensor"
- assert er.async_get(ENTITY_LIGHT).unique_id == ID
- assert er.async_get(ENTITY_NIGHTLIGHT).unique_id == f"{ID}-nightlight"
- assert er.async_get(ENTITY_AMBILIGHT).unique_id == f"{ID}-ambilight"
+ entity_registry = er.async_get(hass)
+ assert (
+ entity_registry.async_get(ENTITY_BINARY_SENSOR).unique_id
+ == f"{ID}-nightlight_sensor"
+ )
+ assert entity_registry.async_get(ENTITY_LIGHT).unique_id == ID
+ assert entity_registry.async_get(ENTITY_NIGHTLIGHT).unique_id == f"{ID}-nightlight"
+ assert entity_registry.async_get(ENTITY_AMBILIGHT).unique_id == f"{ID}-ambilight"
async def test_unique_ids_entry(hass: HomeAssistant):
@@ -125,18 +134,19 @@ async def test_unique_ids_entry(hass: HomeAssistant):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
- er = await entity_registry.async_get_registry(hass)
+ entity_registry = er.async_get(hass)
assert (
- er.async_get(ENTITY_BINARY_SENSOR).unique_id
+ entity_registry.async_get(ENTITY_BINARY_SENSOR).unique_id
== f"{config_entry.entry_id}-nightlight_sensor"
)
- assert er.async_get(ENTITY_LIGHT).unique_id == config_entry.entry_id
+ assert entity_registry.async_get(ENTITY_LIGHT).unique_id == config_entry.entry_id
assert (
- er.async_get(ENTITY_NIGHTLIGHT).unique_id
+ entity_registry.async_get(ENTITY_NIGHTLIGHT).unique_id
== f"{config_entry.entry_id}-nightlight"
)
assert (
- er.async_get(ENTITY_AMBILIGHT).unique_id == f"{config_entry.entry_id}-ambilight"
+ entity_registry.async_get(ENTITY_AMBILIGHT).unique_id
+ == f"{config_entry.entry_id}-ambilight"
)
@@ -164,8 +174,8 @@ async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant):
binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format(
IP_ADDRESS.replace(".", "_")
)
- er = await entity_registry.async_get_registry(hass)
- assert er.async_get(binary_sensor_entity_id) is None
+ entity_registry = er.async_get(hass)
+ assert entity_registry.async_get(binary_sensor_entity_id) is None
type(mocked_bulb).get_capabilities = MagicMock(CAPABILITIES)
type(mocked_bulb).get_properties = MagicMock(None)
@@ -173,5 +183,5 @@ async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant):
hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE].update()
await hass.async_block_till_done()
- er = await entity_registry.async_get_registry(hass)
- assert er.async_get(binary_sensor_entity_id) is not None
+ entity_registry = er.async_get(hass)
+ assert entity_registry.async_get(binary_sensor_entity_id) is not None
diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py
index 0b7415140a35bc..b6ce2fa4fafa11 100644
--- a/tests/components/yeelight/test_light.py
+++ b/tests/components/yeelight/test_light.py
@@ -85,7 +85,7 @@
)
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util.color import (
color_hs_to_RGB,
@@ -376,7 +376,7 @@ async def _async_test(
await hass.config_entries.async_unload(config_entry.entry_id)
await config_entry.async_remove(hass)
- registry = await entity_registry.async_get_registry(hass)
+ registry = er.async_get(hass)
registry.async_clear_config_entry(config_entry.entry_id)
# nightlight
@@ -430,6 +430,8 @@ async def _async_test(
"effect_list": YEELIGHT_MONO_EFFECT_LIST,
"supported_features": SUPPORT_YEELIGHT,
"brightness": bright,
+ "color_mode": "brightness",
+ "supported_color_modes": ["brightness"],
},
)
@@ -441,6 +443,8 @@ async def _async_test(
"effect_list": YEELIGHT_MONO_EFFECT_LIST,
"supported_features": SUPPORT_YEELIGHT,
"brightness": bright,
+ "color_mode": "brightness",
+ "supported_color_modes": ["brightness"],
},
)
@@ -463,8 +467,14 @@ async def _async_test(
"hs_color": hs_color,
"rgb_color": rgb_color,
"xy_color": xy_color,
+ "color_mode": "hs",
+ "supported_color_modes": ["color_temp", "hs"],
+ },
+ {
+ "supported_features": 0,
+ "color_mode": "onoff",
+ "supported_color_modes": ["onoff"],
},
- {"supported_features": 0},
)
# WhiteTemp
@@ -483,11 +493,15 @@ async def _async_test(
),
"brightness": current_brightness,
"color_temp": ct,
+ "color_mode": "color_temp",
+ "supported_color_modes": ["color_temp"],
},
{
"effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST,
"supported_features": SUPPORT_YEELIGHT,
"brightness": nl_br,
+ "color_mode": "brightness",
+ "supported_color_modes": ["brightness"],
},
)
@@ -512,11 +526,15 @@ async def _async_test(
),
"brightness": current_brightness,
"color_temp": ct,
+ "color_mode": "color_temp",
+ "supported_color_modes": ["color_temp"],
},
{
"effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST,
"supported_features": SUPPORT_YEELIGHT,
"brightness": nl_br,
+ "color_mode": "brightness",
+ "supported_color_modes": ["brightness"],
},
)
await _async_test(
@@ -532,6 +550,8 @@ async def _async_test(
"hs_color": bg_hs_color,
"rgb_color": bg_rgb_color,
"xy_color": bg_xy_color,
+ "color_mode": "hs",
+ "supported_color_modes": ["color_temp", "hs"],
},
name=f"{UNIQUE_NAME} ambilight",
entity_id=f"{ENTITY_LIGHT}_ambilight",
diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py
index cc34a511573e29..e7a30abc73f0b1 100644
--- a/tests/components/zeroconf/test_init.py
+++ b/tests/components/zeroconf/test_init.py
@@ -3,6 +3,7 @@
from zeroconf import (
BadTypeInNameException,
+ Error as ZeroconfError,
InterfaceChoice,
IPVersion,
ServiceInfo,
@@ -103,6 +104,24 @@ def mock_zc_info(service_type, name):
return mock_zc_info
+def get_zeroconf_info_mock_manufacturer(manufacturer):
+ """Return info for get_service_info for an zeroconf device."""
+
+ def mock_zc_info(service_type, name):
+ return ServiceInfo(
+ service_type,
+ name,
+ addresses=[b"\n\x00\x00\x14"],
+ port=80,
+ weight=0,
+ priority=0,
+ server="name.local.",
+ properties={b"manufacturer": manufacturer.encode()},
+ )
+
+ return mock_zc_info
+
+
async def test_setup(hass, mock_zeroconf):
"""Test configured options for a device are loaded via config entry."""
with patch.object(
@@ -236,7 +255,7 @@ async def test_service_with_invalid_name(hass, mock_zeroconf, caplog):
assert "Failed to get info for device name" in caplog.text
-async def test_zeroconf_match(hass, mock_zeroconf):
+async def test_zeroconf_match_macaddress(hass, mock_zeroconf):
"""Test configured options for a device are loaded via config entry."""
def http_only_service_update_mock(zeroconf, services, handlers):
@@ -273,6 +292,39 @@ def http_only_service_update_mock(zeroconf, services, handlers):
assert mock_config_flow.mock_calls[0][1][0] == "shelly"
+async def test_zeroconf_match_manufacturer(hass, mock_zeroconf):
+ """Test configured options for a device are loaded via config entry."""
+
+ def http_only_service_update_mock(zeroconf, services, handlers):
+ """Call service update handler."""
+ handlers[0](
+ zeroconf,
+ "_airplay._tcp.local.",
+ "s1000._airplay._tcp.local.",
+ ServiceStateChange.Added,
+ )
+
+ with patch.dict(
+ zc_gen.ZEROCONF,
+ {"_airplay._tcp.local.": [{"domain": "samsungtv", "manufacturer": "samsung*"}]},
+ clear=True,
+ ), patch.object(
+ hass.config_entries.flow, "async_init"
+ ) as mock_config_flow, patch.object(
+ zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
+ ) as mock_service_browser:
+ mock_zeroconf.get_service_info.side_effect = (
+ get_zeroconf_info_mock_manufacturer("Samsung Electronics")
+ )
+ assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ await hass.async_block_till_done()
+
+ assert len(mock_service_browser.mock_calls) == 1
+ assert len(mock_config_flow.mock_calls) == 1
+ assert mock_config_flow.mock_calls[0][1][0] == "samsungtv"
+
+
async def test_zeroconf_no_match(hass, mock_zeroconf):
"""Test configured options for a device are loaded via config entry."""
@@ -305,6 +357,38 @@ def http_only_service_update_mock(zeroconf, services, handlers):
assert len(mock_config_flow.mock_calls) == 0
+async def test_zeroconf_no_match_manufacturer(hass, mock_zeroconf):
+ """Test configured options for a device are loaded via config entry."""
+
+ def http_only_service_update_mock(zeroconf, services, handlers):
+ """Call service update handler."""
+ handlers[0](
+ zeroconf,
+ "_airplay._tcp.local.",
+ "s1000._airplay._tcp.local.",
+ ServiceStateChange.Added,
+ )
+
+ with patch.dict(
+ zc_gen.ZEROCONF,
+ {"_airplay._tcp.local.": [{"domain": "samsungtv", "manufacturer": "samsung*"}]},
+ clear=True,
+ ), patch.object(
+ hass.config_entries.flow, "async_init"
+ ) as mock_config_flow, patch.object(
+ zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock
+ ) as mock_service_browser:
+ mock_zeroconf.get_service_info.side_effect = (
+ get_zeroconf_info_mock_manufacturer("Not Samsung Electronics")
+ )
+ assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ await hass.async_block_till_done()
+
+ assert len(mock_service_browser.mock_calls) == 1
+ assert len(mock_config_flow.mock_calls) == 0
+
+
async def test_homekit_match_partial_space(hass, mock_zeroconf):
"""Test configured options for a device are loaded via config entry."""
with patch.dict(
@@ -495,3 +579,35 @@ async def test_get_instance(hass, mock_zeroconf):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
assert len(mock_zeroconf.ha_close.mock_calls) == 1
+
+
+async def test_removed_ignored(hass, mock_zeroconf):
+ """Test we remove it when a zeroconf entry is removed."""
+ mock_zeroconf.get_service_info.side_effect = ZeroconfError
+
+ def service_update_mock(zeroconf, services, handlers):
+ """Call service update handler."""
+ handlers[0](
+ zeroconf, "_service.added", "name._service.added", ServiceStateChange.Added
+ )
+ handlers[0](
+ zeroconf,
+ "_service.updated",
+ "name._service.updated",
+ ServiceStateChange.Updated,
+ )
+ handlers[0](
+ zeroconf,
+ "_service.removed",
+ "name._service.removed",
+ ServiceStateChange.Removed,
+ )
+
+ with patch.object(zeroconf, "HaServiceBrowser", side_effect=service_update_mock):
+ assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+ await hass.async_block_till_done()
+
+ assert len(mock_zeroconf.get_service_info.mock_calls) == 2
+ assert mock_zeroconf.get_service_info.mock_calls[0][1][0] == "_service.added"
+ assert mock_zeroconf.get_service_info.mock_calls[1][1][0] == "_service.updated"
diff --git a/tests/components/zerproc/conftest.py b/tests/components/zerproc/conftest.py
index b4c35bebc71260..9d6bd9dea2388f 100644
--- a/tests/components/zerproc/conftest.py
+++ b/tests/components/zerproc/conftest.py
@@ -1,2 +1,2 @@
"""zerproc conftest."""
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
diff --git a/tests/components/zerproc/test_light.py b/tests/components/zerproc/test_light.py
index 1f0c7652bfd69c..0fec6be645184b 100644
--- a/tests/components/zerproc/test_light.py
+++ b/tests/components/zerproc/test_light.py
@@ -7,14 +7,21 @@
from homeassistant import setup
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
+ ATTR_COLOR_MODE,
ATTR_HS_COLOR,
ATTR_RGB_COLOR,
+ ATTR_SUPPORTED_COLOR_MODES,
ATTR_XY_COLOR,
+ COLOR_MODE_HS,
SCAN_INTERVAL,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
)
-from homeassistant.components.zerproc.light import DOMAIN
+from homeassistant.components.zerproc.const import (
+ DATA_ADDRESSES,
+ DATA_DISCOVERY_SUBSCRIPTION,
+ DOMAIN,
+)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
@@ -96,6 +103,7 @@ async def test_init(hass, mock_entry):
assert state.state == STATE_OFF
assert state.attributes == {
ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF",
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS],
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR,
ATTR_ICON: "mdi:string-lights",
}
@@ -104,8 +112,10 @@ async def test_init(hass, mock_entry):
assert state.state == STATE_ON
assert state.attributes == {
ATTR_FRIENDLY_NAME: "LEDBlue-33445566",
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS],
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR,
ATTR_ICON: "mdi:string-lights",
+ ATTR_COLOR_MODE: COLOR_MODE_HS,
ATTR_BRIGHTNESS: 255,
ATTR_HS_COLOR: (221.176, 100.0),
ATTR_RGB_COLOR: (0, 80, 255),
@@ -118,6 +128,8 @@ async def test_init(hass, mock_entry):
assert mock_light_1.disconnect.called
assert mock_light_2.disconnect.called
+ assert hass.data[DOMAIN]["addresses"] == {"AA:BB:CC:DD:EE:FF", "11:22:33:44:55:66"}
+
async def test_discovery_exception(hass, mock_entry):
"""Test platform setup."""
@@ -136,42 +148,16 @@ async def test_discovery_exception(hass, mock_entry):
assert len(hass.data[DOMAIN]["addresses"]) == 0
-async def test_connect_exception(hass, mock_entry):
- """Test platform setup."""
- await setup.async_setup_component(hass, "persistent_notification", {})
-
- mock_entry.add_to_hass(hass)
-
- mock_light_1 = MagicMock(spec=pyzerproc.Light)
- mock_light_1.address = "AA:BB:CC:DD:EE:FF"
- mock_light_1.name = "LEDBlue-CCDDEEFF"
- mock_light_1.is_connected.return_value = False
-
- mock_light_2 = MagicMock(spec=pyzerproc.Light)
- mock_light_2.address = "11:22:33:44:55:66"
- mock_light_2.name = "LEDBlue-33445566"
- mock_light_2.is_connected.return_value = False
-
- with patch(
- "homeassistant.components.zerproc.light.pyzerproc.discover",
- return_value=[mock_light_1, mock_light_2],
- ), patch.object(
- mock_light_1, "connect", side_effect=pyzerproc.ZerprocException("TEST")
- ):
- await hass.config_entries.async_setup(mock_entry.entry_id)
- await hass.async_block_till_done()
-
- # The exception connecting to light 1 should be captured, but light 2
- # should still be added
- assert len(hass.data[DOMAIN]["addresses"]) == 1
-
-
async def test_remove_entry(hass, mock_light, mock_entry):
"""Test platform setup."""
+ assert hass.data[DOMAIN][DATA_ADDRESSES] == {"AA:BB:CC:DD:EE:FF"}
+ assert DATA_DISCOVERY_SUBSCRIPTION in hass.data[DOMAIN]
+
with patch.object(mock_light, "disconnect") as mock_disconnect:
await hass.config_entries.async_remove(mock_entry.entry_id)
assert mock_disconnect.called
+ assert DOMAIN not in hass.data
async def test_remove_entry_exceptions_caught(hass, mock_light, mock_entry):
@@ -297,6 +283,7 @@ async def test_light_update(hass, mock_light):
assert state.state == STATE_OFF
assert state.attributes == {
ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF",
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS],
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR,
ATTR_ICON: "mdi:string-lights",
}
@@ -315,6 +302,7 @@ async def test_light_update(hass, mock_light):
assert state.state == STATE_UNAVAILABLE
assert state.attributes == {
ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF",
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS],
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR,
ATTR_ICON: "mdi:string-lights",
}
@@ -332,6 +320,7 @@ async def test_light_update(hass, mock_light):
assert state.state == STATE_OFF
assert state.attributes == {
ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF",
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS],
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR,
ATTR_ICON: "mdi:string-lights",
}
@@ -349,8 +338,10 @@ async def test_light_update(hass, mock_light):
assert state.state == STATE_ON
assert state.attributes == {
ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF",
+ ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS],
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR,
ATTR_ICON: "mdi:string-lights",
+ ATTR_COLOR_MODE: COLOR_MODE_HS,
ATTR_BRIGHTNESS: 220,
ATTR_HS_COLOR: (261.429, 31.818),
ATTR_RGB_COLOR: (202, 173, 255),
diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py
index 234ca0c9ba547b..eeffa3fb911226 100644
--- a/tests/components/zha/common.py
+++ b/tests/components/zha/common.py
@@ -1,4 +1,5 @@
"""Common test objects."""
+import asyncio
import time
from unittest.mock import AsyncMock, Mock
@@ -237,3 +238,11 @@ async def async_test_rejoin(hass, zigpy_device, clusters, report_counts, ep_id=1
assert cluster.bind.await_count == 1
assert cluster.configure_reporting.call_count == reports
assert cluster.configure_reporting.await_count == reports
+
+
+async def async_wait_for_updates(hass):
+ """Wait until all scheduled updates are executed."""
+ await hass.async_block_till_done()
+ await asyncio.sleep(0)
+ await asyncio.sleep(0)
+ await hass.async_block_till_done()
diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py
index 57241b9bb74437..b3ac4aff16e560 100644
--- a/tests/components/zha/conftest.py
+++ b/tests/components/zha/conftest.py
@@ -16,7 +16,7 @@
from .common import FakeDevice, FakeEndpoint, get_zha_gateway
from tests.common import MockConfigEntry
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
FIXTURE_GRP_ID = 0x1001
FIXTURE_GRP_NAME = "fixture group"
diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py
index 363aa12db6ebce..8694b59ecfb252 100644
--- a/tests/components/zha/test_api.py
+++ b/tests/components/zha/test_api.py
@@ -28,7 +28,6 @@
ATTR_IEEE,
ATTR_MANUFACTURER,
ATTR_MODEL,
- ATTR_NAME,
ATTR_NEIGHBORS,
ATTR_QUIRK_APPLIED,
CLUSTER_TYPE_IN,
@@ -38,6 +37,7 @@
GROUP_IDS,
GROUP_NAME,
)
+from homeassistant.const import ATTR_NAME
from homeassistant.core import Context
from .conftest import FIXTURE_GRP_ID, FIXTURE_GRP_NAME
diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py
index 8a2ca1f05c3024..a391439a239738 100644
--- a/tests/components/zha/test_channels.py
+++ b/tests/components/zha/test_channels.py
@@ -415,10 +415,11 @@ async def test_ep_channels_configure(channel):
claimed = {ch_1.id: ch_1, ch_2.id: ch_2, ch_3.id: ch_3}
client_chans = {ch_4.id: ch_4, ch_5.id: ch_5}
- with mock.patch.dict(ep_channels.claimed_channels, claimed, clear=True):
- with mock.patch.dict(ep_channels.client_channels, client_chans, clear=True):
- await ep_channels.async_configure()
- await ep_channels.async_initialize(mock.sentinel.from_cache)
+ with mock.patch.dict(
+ ep_channels.claimed_channels, claimed, clear=True
+ ), mock.patch.dict(ep_channels.client_channels, client_chans, clear=True):
+ await ep_channels.async_configure()
+ await ep_channels.async_initialize(mock.sentinel.from_cache)
for ch in [*claimed.values(), *client_chans.values()]:
assert ch.async_initialize.call_count == 1
@@ -491,6 +492,38 @@ async def test_poll_control_cluster_command(hass, poll_control_device):
assert data["device_id"] == poll_control_device.device_id
+async def test_poll_control_ignore_list(hass, poll_control_device):
+ """Test poll control channel ignore list."""
+ set_long_poll_mock = AsyncMock()
+ poll_control_ch = poll_control_device.channels.pools[0].all_channels["1:0x0020"]
+ cluster = poll_control_ch.cluster
+
+ with mock.patch.object(cluster, "set_long_poll_interval", set_long_poll_mock):
+ await poll_control_ch.check_in_response(33)
+
+ assert set_long_poll_mock.call_count == 1
+
+ set_long_poll_mock.reset_mock()
+ poll_control_ch.skip_manufacturer_id(4151)
+ with mock.patch.object(cluster, "set_long_poll_interval", set_long_poll_mock):
+ await poll_control_ch.check_in_response(33)
+
+ assert set_long_poll_mock.call_count == 0
+
+
+async def test_poll_control_ikea(hass, poll_control_device):
+ """Test poll control channel ignore list for ikea."""
+ set_long_poll_mock = AsyncMock()
+ poll_control_ch = poll_control_device.channels.pools[0].all_channels["1:0x0020"]
+ cluster = poll_control_ch.cluster
+
+ poll_control_device.device.node_desc.manufacturer_code = 4476
+ with mock.patch.object(cluster, "set_long_poll_interval", set_long_poll_mock):
+ await poll_control_ch.check_in_response(33)
+
+ assert set_long_poll_mock.call_count == 0
+
+
@pytest.fixture
def zigpy_zll_device(zigpy_device_mock):
"""ZLL device fixture."""
diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py
index 05412ddb64dc6f..ea2d6dfb7e37af 100644
--- a/tests/components/zha/test_climate.py
+++ b/tests/components/zha/test_climate.py
@@ -33,6 +33,9 @@
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
PRESET_AWAY,
+ PRESET_BOOST,
+ PRESET_COMFORT,
+ PRESET_ECO,
PRESET_NONE,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HVAC_MODE,
@@ -44,6 +47,7 @@
HVAC_MODE_2_SYSTEM,
SEQ_OF_OPERATION,
)
+from homeassistant.components.zha.core.const import PRESET_COMPLEX, PRESET_SCHEDULE
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNKNOWN
from .common import async_enable_traffic, find_entity_id, send_attributes_report
@@ -103,8 +107,23 @@
"out_clusters": [zigpy.zcl.clusters.general.Ota.cluster_id],
}
}
+
+CLIMATE_MOES = {
+ 1: {
+ "device_type": zigpy.profiles.zha.DeviceType.THERMOSTAT,
+ "in_clusters": [
+ zigpy.zcl.clusters.general.Basic.cluster_id,
+ zigpy.zcl.clusters.general.Identify.cluster_id,
+ zigpy.zcl.clusters.hvac.Thermostat.cluster_id,
+ zigpy.zcl.clusters.hvac.UserInterface.cluster_id,
+ 61148,
+ ],
+ "out_clusters": [zigpy.zcl.clusters.general.Ota.cluster_id],
+ }
+}
MANUF_SINOPE = "Sinope Technologies"
MANUF_ZEN = "Zen Within"
+MANUF_MOES = "_TZE200_ckud7u2l"
ZCL_ATTR_PLUG = {
"abs_min_heat_setpoint_limit": 800,
@@ -183,6 +202,13 @@ async def device_climate_zen(device_climate_mock):
return await device_climate_mock(CLIMATE_ZEN, manuf=MANUF_ZEN)
+@pytest.fixture
+async def device_climate_moes(device_climate_mock):
+ """MOES thermostat."""
+
+ return await device_climate_mock(CLIMATE_MOES, manuf=MANUF_MOES)
+
+
def test_sequence_mappings():
"""Test correct mapping between control sequence -> HVAC Mode -> Sysmode."""
@@ -1106,3 +1132,160 @@ async def test_set_fan_mode(hass, device_climate_fan):
)
assert fan_cluster.write_attributes.await_count == 1
assert fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 5}
+
+
+async def test_set_moes_preset(hass, device_climate_moes):
+ """Test setting preset for moes trv."""
+
+ entity_id = await find_entity_id(DOMAIN, device_climate_moes, hass)
+ thrm_cluster = device_climate_moes.device.endpoints[1].thermostat
+
+ state = hass.states.get(entity_id)
+ assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY},
+ blocking=True,
+ )
+
+ assert thrm_cluster.write_attributes.await_count == 1
+ assert thrm_cluster.write_attributes.call_args_list[0][0][0] == {
+ "operation_preset": 0
+ }
+
+ thrm_cluster.write_attributes.reset_mock()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_SCHEDULE},
+ blocking=True,
+ )
+
+ assert thrm_cluster.write_attributes.await_count == 2
+ assert thrm_cluster.write_attributes.call_args_list[0][0][0] == {
+ "operation_preset": 2
+ }
+ assert thrm_cluster.write_attributes.call_args_list[1][0][0] == {
+ "operation_preset": 1
+ }
+
+ thrm_cluster.write_attributes.reset_mock()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_COMFORT},
+ blocking=True,
+ )
+
+ assert thrm_cluster.write_attributes.await_count == 2
+ assert thrm_cluster.write_attributes.call_args_list[0][0][0] == {
+ "operation_preset": 2
+ }
+ assert thrm_cluster.write_attributes.call_args_list[1][0][0] == {
+ "operation_preset": 3
+ }
+
+ thrm_cluster.write_attributes.reset_mock()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_ECO},
+ blocking=True,
+ )
+
+ assert thrm_cluster.write_attributes.await_count == 2
+ assert thrm_cluster.write_attributes.call_args_list[0][0][0] == {
+ "operation_preset": 2
+ }
+ assert thrm_cluster.write_attributes.call_args_list[1][0][0] == {
+ "operation_preset": 4
+ }
+
+ thrm_cluster.write_attributes.reset_mock()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_BOOST},
+ blocking=True,
+ )
+
+ assert thrm_cluster.write_attributes.await_count == 2
+ assert thrm_cluster.write_attributes.call_args_list[0][0][0] == {
+ "operation_preset": 2
+ }
+ assert thrm_cluster.write_attributes.call_args_list[1][0][0] == {
+ "operation_preset": 5
+ }
+
+ thrm_cluster.write_attributes.reset_mock()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_COMPLEX},
+ blocking=True,
+ )
+
+ assert thrm_cluster.write_attributes.await_count == 2
+ assert thrm_cluster.write_attributes.call_args_list[0][0][0] == {
+ "operation_preset": 2
+ }
+ assert thrm_cluster.write_attributes.call_args_list[1][0][0] == {
+ "operation_preset": 6
+ }
+
+ thrm_cluster.write_attributes.reset_mock()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE},
+ blocking=True,
+ )
+
+ assert thrm_cluster.write_attributes.await_count == 1
+ assert thrm_cluster.write_attributes.call_args_list[0][0][0] == {
+ "operation_preset": 2
+ }
+
+
+async def test_set_moes_operation_mode(hass, device_climate_moes):
+ """Test setting preset for moes trv."""
+
+ entity_id = await find_entity_id(DOMAIN, device_climate_moes, hass)
+ thrm_cluster = device_climate_moes.device.endpoints[1].thermostat
+
+ await send_attributes_report(hass, thrm_cluster, {"operation_preset": 0})
+
+ state = hass.states.get(entity_id)
+ assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY
+
+ await send_attributes_report(hass, thrm_cluster, {"operation_preset": 1})
+
+ state = hass.states.get(entity_id)
+ assert state.attributes[ATTR_PRESET_MODE] == PRESET_SCHEDULE
+
+ await send_attributes_report(hass, thrm_cluster, {"operation_preset": 2})
+
+ state = hass.states.get(entity_id)
+ assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
+
+ await send_attributes_report(hass, thrm_cluster, {"operation_preset": 3})
+
+ state = hass.states.get(entity_id)
+ assert state.attributes[ATTR_PRESET_MODE] == PRESET_COMFORT
+
+ await send_attributes_report(hass, thrm_cluster, {"operation_preset": 4})
+
+ state = hass.states.get(entity_id)
+ assert state.attributes[ATTR_PRESET_MODE] == PRESET_ECO
+
+ await send_attributes_report(hass, thrm_cluster, {"operation_preset": 5})
+
+ state = hass.states.get(entity_id)
+ assert state.attributes[ATTR_PRESET_MODE] == PRESET_BOOST
+
+ await send_attributes_report(hass, thrm_cluster, {"operation_preset": 6})
+
+ state = hass.states.get(entity_id)
+ assert state.attributes[ATTR_PRESET_MODE] == PRESET_COMPLEX
diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py
index fe65def839d2dc..127c5518a41bb9 100644
--- a/tests/components/zha/test_config_flow.py
+++ b/tests/components/zha/test_config_flow.py
@@ -28,6 +28,57 @@ def com_port():
return port
+@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True))
+@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True)
+async def test_discovery(detect_mock, hass):
+ """Test zeroconf flow -- radio detected."""
+ service_info = {
+ "host": "192.168.1.200",
+ "port": 6053,
+ "hostname": "_tube_zb_gw._tcp.local.",
+ "properties": {"name": "tube_123456"},
+ }
+ flow = await hass.config_entries.flow.async_init(
+ "zha", context={"source": "zeroconf"}, data=service_info
+ )
+ result = await hass.config_entries.flow.async_configure(
+ flow["flow_id"], user_input={}
+ )
+
+ assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "socket://192.168.1.200:6638"
+ assert result["data"] == {
+ "device": {
+ "baudrate": 115200,
+ "flow_control": None,
+ "path": "socket://192.168.1.200:6638",
+ },
+ CONF_RADIO_TYPE: "znp",
+ }
+
+
+@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True))
+@patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True)
+async def test_discovery_already_setup(detect_mock, hass):
+ """Test zeroconf flow -- radio detected."""
+ service_info = {
+ "host": "192.168.1.200",
+ "port": 6053,
+ "hostname": "_tube_zb_gw._tcp.local.",
+ "properties": {"name": "tube_123456"},
+ }
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ MockConfigEntry(domain=DOMAIN, data={"usb_path": "/dev/ttyUSB1"}).add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ "zha", context={"source": "zeroconf"}, data=service_info
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "single_instance_allowed"
+
+
@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()]))
@patch(
"homeassistant.components.zha.config_flow.detect_radios",
@@ -74,6 +125,7 @@ async def test_user_flow_not_detected(detect_mock, hass):
assert detect_mock.await_args[0][0] == port.device
+@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()]))
async def test_user_flow_show_form(hass):
"""Test user step form."""
result = await hass.config_entries.flow.async_init(
@@ -85,6 +137,18 @@ async def test_user_flow_show_form(hass):
assert result["step_id"] == "user"
+@patch("serial.tools.list_ports.comports", MagicMock(return_value=[]))
+async def test_user_flow_show_manual(hass):
+ """Test user flow manual entry when no comport detected."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={CONF_SOURCE: SOURCE_USER},
+ )
+
+ assert result["type"] == RESULT_TYPE_FORM
+ assert result["step_id"] == "pick_radio"
+
+
async def test_user_flow_manual(hass):
"""Test user flow manual entry."""
diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py
index 1ce75045d38e06..d3ab3c3ada2fbe 100644
--- a/tests/components/zha/test_device.py
+++ b/tests/components/zha/test_device.py
@@ -10,7 +10,7 @@
import homeassistant.components.zha.core.device as zha_core_device
from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE
-import homeassistant.helpers.device_registry as ha_dev_reg
+import homeassistant.helpers.device_registry as dr
import homeassistant.util.dt as dt_util
from .common import async_enable_traffic, make_zcl_header
@@ -227,7 +227,7 @@ async def test_ota_sw_version(hass, ota_zha_device):
"""Test device entry gets sw_version updated via OTA channel."""
ota_ch = ota_zha_device.channels.pools[0].client_channels["1:0x0019"]
- dev_registry = await ha_dev_reg.async_get_registry(hass)
+ dev_registry = dr.async_get(hass)
entry = dev_registry.async_get(ota_zha_device.device_id)
assert entry.sw_version is None
diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py
index 316d475f17f4a5..4a777fcebb616b 100644
--- a/tests/components/zha/test_device_action.py
+++ b/tests/components/zha/test_device_action.py
@@ -12,11 +12,11 @@
_async_get_device_automations as async_get_device_automations,
)
from homeassistant.components.zha import DOMAIN
-from homeassistant.helpers.device_registry import async_get_registry
+from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service, mock_coro
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
SHORT_PRESS = "remote_button_short_press"
COMMAND = "command"
@@ -49,7 +49,7 @@ async def test_get_actions(hass, device_ias):
ieee_address = str(device_ias[0].ieee)
- ha_device_registry = await async_get_registry(hass)
+ ha_device_registry = dr.async_get(hass)
reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)})
actions = await async_get_device_automations(hass, "action", reg_device.id)
@@ -72,7 +72,7 @@ async def test_action(hass, device_ias):
ieee_address = str(zha_device.ieee)
- ha_device_registry = await async_get_registry(hass)
+ ha_device_registry = dr.async_get(hass)
reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)})
with patch(
diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py
index 96ee5520e2aa40..b2f964e26953ae 100644
--- a/tests/components/zha/test_device_trigger.py
+++ b/tests/components/zha/test_device_trigger.py
@@ -8,7 +8,7 @@
import homeassistant.components.automation as automation
import homeassistant.components.zha.core.device as zha_core_device
-from homeassistant.helpers.device_registry import async_get_registry
+from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -19,7 +19,7 @@
async_get_device_automations,
async_mock_service,
)
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
ON = 1
OFF = 0
@@ -86,7 +86,7 @@ async def test_triggers(hass, mock_devices):
ieee_address = str(zha_device.ieee)
- ha_device_registry = await async_get_registry(hass)
+ ha_device_registry = dr.async_get(hass)
reg_device = ha_device_registry.async_get_device({("zha", ieee_address)})
triggers = await async_get_device_automations(hass, "trigger", reg_device.id)
@@ -144,7 +144,7 @@ async def test_no_triggers(hass, mock_devices):
_, zha_device = mock_devices
ieee_address = str(zha_device.ieee)
- ha_device_registry = await async_get_registry(hass)
+ ha_device_registry = dr.async_get(hass)
reg_device = ha_device_registry.async_get_device({("zha", ieee_address)})
triggers = await async_get_device_automations(hass, "trigger", reg_device.id)
@@ -173,7 +173,7 @@ async def test_if_fires_on_event(hass, mock_devices, calls):
}
ieee_address = str(zha_device.ieee)
- ha_device_registry = await async_get_registry(hass)
+ ha_device_registry = dr.async_get(hass)
reg_device = ha_device_registry.async_get_device({("zha", ieee_address)})
assert await async_setup_component(
@@ -282,7 +282,7 @@ async def test_exception_no_triggers(hass, mock_devices, calls, caplog):
_, zha_device = mock_devices
ieee_address = str(zha_device.ieee)
- ha_device_registry = await async_get_registry(hass)
+ ha_device_registry = dr.async_get(hass)
reg_device = ha_device_registry.async_get_device({("zha", ieee_address)})
await async_setup_component(
@@ -324,7 +324,7 @@ async def test_exception_bad_trigger(hass, mock_devices, calls, caplog):
}
ieee_address = str(zha_device.ieee)
- ha_device_registry = await async_get_registry(hass)
+ ha_device_registry = dr.async_get(hass)
reg_device = ha_device_registry.async_get_device({("zha", ieee_address)})
await async_setup_component(
diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py
index ac2ef085e14cf6..cd0e75a72375de 100644
--- a/tests/components/zha/test_discover.py
+++ b/tests/components/zha/test_discover.py
@@ -67,7 +67,7 @@ async def test_devices(
zha_device_joined_restored,
):
"""Test device discovery."""
- entity_registry = await homeassistant.helpers.entity_registry.async_get_registry(
+ entity_registry = homeassistant.helpers.entity_registry.async_get(
hass_disable_services
)
@@ -96,7 +96,7 @@ async def test_devices(
entity_ids = hass_disable_services.states.async_entity_ids()
await hass_disable_services.async_block_till_done()
zha_entity_ids = {
- ent for ent in entity_ids if ent.split(".")[0] in zha_const.COMPONENTS
+ ent for ent in entity_ids if ent.split(".")[0] in zha_const.PLATFORMS
}
if cluster_identify:
@@ -212,17 +212,14 @@ def test_discover_by_device_type_override():
with mock.patch(
"homeassistant.components.zha.core.registries.ZHA_ENTITIES.get_entity",
get_entity_mock,
- ):
- with mock.patch.dict(disc.PROBE._device_configs, overrides, clear=True):
- disc.PROBE.discover_by_device_type(ep_channels)
- assert get_entity_mock.call_count == 1
- assert ep_channels.claim_channels.call_count == 1
- assert ep_channels.claim_channels.call_args[0][0] is mock.sentinel.claimed
- assert ep_channels.async_new_entity.call_count == 1
- assert ep_channels.async_new_entity.call_args[0][0] == zha_const.SWITCH
- assert (
- ep_channels.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls
- )
+ ), mock.patch.dict(disc.PROBE._device_configs, overrides, clear=True):
+ disc.PROBE.discover_by_device_type(ep_channels)
+ assert get_entity_mock.call_count == 1
+ assert ep_channels.claim_channels.call_count == 1
+ assert ep_channels.claim_channels.call_args[0][0] is mock.sentinel.claimed
+ assert ep_channels.async_new_entity.call_count == 1
+ assert ep_channels.async_new_entity.call_args[0][0] == zha_const.SWITCH
+ assert ep_channels.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls
def test_discover_probe_single_cluster():
diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py
index 61828c135bc113..eed0e0b691e222 100644
--- a/tests/components/zha/test_fan.py
+++ b/tests/components/zha/test_fan.py
@@ -2,6 +2,7 @@
from unittest.mock import AsyncMock, call, patch
import pytest
+from zigpy.exceptions import ZigbeeException
import zigpy.profiles.zha as zha
import zigpy.zcl.clusters.general as general
import zigpy.zcl.clusters.hvac as hvac
@@ -9,17 +10,27 @@
from homeassistant.components import fan
from homeassistant.components.fan import (
+ ATTR_PERCENTAGE,
+ ATTR_PERCENTAGE_STEP,
+ ATTR_PRESET_MODE,
ATTR_SPEED,
DOMAIN,
+ SERVICE_SET_PRESET_MODE,
SERVICE_SET_SPEED,
SPEED_HIGH,
SPEED_LOW,
SPEED_MEDIUM,
SPEED_OFF,
+ NotValidPresetModeError,
)
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.zha.core.discovery import GROUP_PROBE
from homeassistant.components.zha.core.group import GroupMember
+from homeassistant.components.zha.fan import (
+ PRESET_MODE_AUTO,
+ PRESET_MODE_ON,
+ PRESET_MODE_SMART,
+)
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
@@ -39,6 +50,8 @@
send_attributes_report,
)
+from tests.components.zha.common import async_wait_for_updates
+
IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8"
IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8"
@@ -173,6 +186,20 @@ async def test_fan(hass, zha_device_joined_restored, zigpy_device):
assert len(cluster.write_attributes.mock_calls) == 1
assert cluster.write_attributes.call_args == call({"fan_mode": 3})
+ # change preset_mode from HA
+ cluster.write_attributes.reset_mock()
+ await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_ON)
+ assert len(cluster.write_attributes.mock_calls) == 1
+ assert cluster.write_attributes.call_args == call({"fan_mode": 4})
+
+ # set invalid preset_mode from HA
+ cluster.write_attributes.reset_mock()
+ with pytest.raises(NotValidPresetModeError):
+ await async_set_preset_mode(
+ hass, entity_id, preset_mode="invalid does not exist"
+ )
+ assert len(cluster.write_attributes.mock_calls) == 0
+
# test adding new fan to the network and HA
await async_test_rejoin(hass, zigpy_device, [cluster], (1,))
@@ -206,10 +233,25 @@ async def async_set_speed(hass, entity_id, speed=None):
await hass.services.async_call(DOMAIN, SERVICE_SET_SPEED, data, blocking=True)
+async def async_set_preset_mode(hass, entity_id, preset_mode=None):
+ """Set preset_mode for specified fan."""
+ data = {
+ key: value
+ for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_PRESET_MODE, preset_mode)]
+ if value is not None
+ }
+
+ await hass.services.async_call(DOMAIN, SERVICE_SET_PRESET_MODE, data, blocking=True)
+
+
@patch(
"zigpy.zcl.clusters.hvac.Fan.write_attributes",
new=AsyncMock(return_value=zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]),
)
+@patch(
+ "homeassistant.components.zha.entity.UPDATE_GROUP_FROM_CHILD_DELAY",
+ new=0,
+)
async def test_zha_group_fan_entity(hass, device_fan_1, device_fan_2, coordinator):
"""Test the fan entity for a ZHA group."""
zha_gateway = get_zha_gateway(hass)
@@ -247,13 +289,13 @@ async def test_zha_group_fan_entity(hass, device_fan_1, device_fan_2, coordinato
dev2_fan_cluster = device_fan_2.device.endpoints[1].fan
await async_enable_traffic(hass, [device_fan_1, device_fan_2], enabled=False)
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
# test that the fans were created and that they are unavailable
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
# allow traffic to flow through the gateway and device
await async_enable_traffic(hass, [device_fan_1, device_fan_2])
-
+ await async_wait_for_updates(hass)
# test that the fan group entity was created and is off
assert hass.states.get(entity_id).state == STATE_OFF
@@ -276,6 +318,24 @@ async def test_zha_group_fan_entity(hass, device_fan_1, device_fan_2, coordinato
assert len(group_fan_cluster.write_attributes.mock_calls) == 1
assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 3}
+ # change preset mode from HA
+ group_fan_cluster.write_attributes.reset_mock()
+ await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_ON)
+ assert len(group_fan_cluster.write_attributes.mock_calls) == 1
+ assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 4}
+
+ # change preset mode from HA
+ group_fan_cluster.write_attributes.reset_mock()
+ await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_AUTO)
+ assert len(group_fan_cluster.write_attributes.mock_calls) == 1
+ assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 5}
+
+ # change preset mode from HA
+ group_fan_cluster.write_attributes.reset_mock()
+ await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_SMART)
+ assert len(group_fan_cluster.write_attributes.mock_calls) == 1
+ assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 6}
+
# test some of the group logic to make sure we key off states correctly
await send_attributes_report(hass, dev1_fan_cluster, {0: 0})
await send_attributes_report(hass, dev2_fan_cluster, {0: 0})
@@ -284,26 +344,90 @@ async def test_zha_group_fan_entity(hass, device_fan_1, device_fan_2, coordinato
assert hass.states.get(entity_id).state == STATE_OFF
await send_attributes_report(hass, dev2_fan_cluster, {0: 2})
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
# test that group fan is speed medium
assert hass.states.get(entity_id).state == STATE_ON
await send_attributes_report(hass, dev2_fan_cluster, {0: 0})
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
# test that group fan is now off
assert hass.states.get(entity_id).state == STATE_OFF
+@patch(
+ "zigpy.zcl.clusters.hvac.Fan.write_attributes",
+ new=AsyncMock(side_effect=ZigbeeException),
+)
+@patch(
+ "homeassistant.components.zha.entity.UPDATE_GROUP_FROM_CHILD_DELAY",
+ new=0,
+)
+async def test_zha_group_fan_entity_failure_state(
+ hass, device_fan_1, device_fan_2, coordinator, caplog
+):
+ """Test the fan entity for a ZHA group when writing attributes generates an exception."""
+ zha_gateway = get_zha_gateway(hass)
+ assert zha_gateway is not None
+ zha_gateway.coordinator_zha_device = coordinator
+ coordinator._zha_gateway = zha_gateway
+ device_fan_1._zha_gateway = zha_gateway
+ device_fan_2._zha_gateway = zha_gateway
+ member_ieee_addresses = [device_fan_1.ieee, device_fan_2.ieee]
+ members = [GroupMember(device_fan_1.ieee, 1), GroupMember(device_fan_2.ieee, 1)]
+
+ # test creating a group with 2 members
+ zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members)
+ await hass.async_block_till_done()
+
+ assert zha_group is not None
+ assert len(zha_group.members) == 2
+ for member in zha_group.members:
+ assert member.device.ieee in member_ieee_addresses
+ assert member.group == zha_group
+ assert member.endpoint is not None
+
+ entity_domains = GROUP_PROBE.determine_entity_domains(hass, zha_group)
+ assert len(entity_domains) == 2
+
+ assert LIGHT_DOMAIN in entity_domains
+ assert DOMAIN in entity_domains
+
+ entity_id = async_find_group_entity_id(hass, DOMAIN, zha_group)
+ assert hass.states.get(entity_id) is not None
+
+ group_fan_cluster = zha_group.endpoint[hvac.Fan.cluster_id]
+
+ await async_enable_traffic(hass, [device_fan_1, device_fan_2], enabled=False)
+ await async_wait_for_updates(hass)
+ # test that the fans were created and that they are unavailable
+ assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
+
+ # allow traffic to flow through the gateway and device
+ await async_enable_traffic(hass, [device_fan_1, device_fan_2])
+ await async_wait_for_updates(hass)
+ # test that the fan group entity was created and is off
+ assert hass.states.get(entity_id).state == STATE_OFF
+
+ # turn on from HA
+ group_fan_cluster.write_attributes.reset_mock()
+ await async_turn_on(hass, entity_id)
+ await hass.async_block_till_done()
+ assert len(group_fan_cluster.write_attributes.mock_calls) == 1
+ assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 2}
+
+ assert "Could not set fan mode" in caplog.text
+
+
@pytest.mark.parametrize(
- "plug_read, expected_state, expected_speed",
+ "plug_read, expected_state, expected_speed, expected_percentage",
(
- (None, STATE_OFF, None),
- ({"fan_mode": 0}, STATE_OFF, SPEED_OFF),
- ({"fan_mode": 1}, STATE_ON, SPEED_LOW),
- ({"fan_mode": 2}, STATE_ON, SPEED_MEDIUM),
- ({"fan_mode": 3}, STATE_ON, SPEED_HIGH),
+ (None, STATE_OFF, None, None),
+ ({"fan_mode": 0}, STATE_OFF, SPEED_OFF, 0),
+ ({"fan_mode": 1}, STATE_ON, SPEED_LOW, 33),
+ ({"fan_mode": 2}, STATE_ON, SPEED_MEDIUM, 66),
+ ({"fan_mode": 3}, STATE_ON, SPEED_HIGH, 100),
),
)
async def test_fan_init(
@@ -313,6 +437,7 @@ async def test_fan_init(
plug_read,
expected_state,
expected_speed,
+ expected_percentage,
):
"""Test zha fan platform."""
@@ -324,6 +449,8 @@ async def test_fan_init(
assert entity_id is not None
assert hass.states.get(entity_id).state == expected_state
assert hass.states.get(entity_id).attributes[ATTR_SPEED] == expected_speed
+ assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == expected_percentage
+ assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None
async def test_fan_update_entity(
@@ -341,6 +468,9 @@ async def test_fan_update_entity(
assert entity_id is not None
assert hass.states.get(entity_id).state == STATE_OFF
assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_OFF
+ assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 0
+ assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None
+ assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 3
assert cluster.read_attributes.await_count == 1
await async_setup_component(hass, "homeassistant", {})
@@ -358,5 +488,8 @@ async def test_fan_update_entity(
"homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
)
assert hass.states.get(entity_id).state == STATE_ON
+ assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 33
assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_LOW
+ assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None
+ assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 3
assert cluster.read_attributes.await_count == 3
diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py
index 0a9a492a148957..fe367a3969bcbe 100644
--- a/tests/components/zha/test_light.py
+++ b/tests/components/zha/test_light.py
@@ -25,6 +25,7 @@
)
from tests.common import async_fire_time_changed
+from tests.components.zha.common import async_wait_for_updates
ON = 1
OFF = 0
@@ -309,7 +310,7 @@ async def async_test_on_from_light(hass, cluster, entity_id):
"""Test on off functionality from the light."""
# turn on at light
await send_attributes_report(hass, cluster, {1: -1, 0: 1, 2: 2})
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
assert hass.states.get(entity_id).state == STATE_ON
@@ -392,13 +393,11 @@ async def async_test_level_on_off_from_hass(
await hass.services.async_call(
DOMAIN, "turn_on", {"entity_id": entity_id, "brightness": 10}, blocking=True
)
- assert on_off_cluster.request.call_count == 1
- assert on_off_cluster.request.await_count == 1
+ # the onoff cluster is now not used when brightness is present by default
+ assert on_off_cluster.request.call_count == 0
+ assert on_off_cluster.request.await_count == 0
assert level_cluster.request.call_count == 1
assert level_cluster.request.await_count == 1
- assert on_off_cluster.request.call_args == call(
- False, ON, (), expect_reply=True, manufacturer=None, tries=1, tsn=None
- )
assert level_cluster.request.call_args == call(
False,
4,
@@ -468,6 +467,10 @@ async def async_test_flash_from_hass(hass, cluster, entity_id, flash):
"zigpy.zcl.clusters.general.OnOff.request",
new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
)
+@patch(
+ "homeassistant.components.zha.entity.UPDATE_GROUP_FROM_CHILD_DELAY",
+ new=0,
+)
async def test_zha_group_light_entity(
hass, device_light_1, device_light_2, device_light_3, coordinator
):
@@ -524,13 +527,13 @@ async def test_zha_group_light_entity(
await async_enable_traffic(
hass, [device_light_1, device_light_2, device_light_3], enabled=False
)
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
# test that the lights were created and that they are unavailable
assert hass.states.get(group_entity_id).state == STATE_UNAVAILABLE
# allow traffic to flow through the gateway and device
await async_enable_traffic(hass, [device_light_1, device_light_2, device_light_3])
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
# test that the lights were created and are off
assert hass.states.get(group_entity_id).state == STATE_OFF
@@ -582,7 +585,7 @@ async def test_zha_group_light_entity(
assert hass.states.get(group_entity_id).state == STATE_ON
await send_attributes_report(hass, dev2_cluster_on_off, {0: 0})
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
# test that group light is now off
assert hass.states.get(device_1_entity_id).state == STATE_OFF
@@ -590,7 +593,7 @@ async def test_zha_group_light_entity(
assert hass.states.get(group_entity_id).state == STATE_OFF
await send_attributes_report(hass, dev1_cluster_on_off, {0: 1})
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
# test that group light is now back on
assert hass.states.get(device_1_entity_id).state == STATE_ON
@@ -599,7 +602,7 @@ async def test_zha_group_light_entity(
# turn it off to test a new member add being tracked
await send_attributes_report(hass, dev1_cluster_on_off, {0: 0})
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
assert hass.states.get(device_1_entity_id).state == STATE_OFF
assert hass.states.get(device_2_entity_id).state == STATE_OFF
assert hass.states.get(group_entity_id).state == STATE_OFF
@@ -607,7 +610,7 @@ async def test_zha_group_light_entity(
# add a new member and test that his state is also tracked
await zha_group.async_add_members([GroupMember(device_light_3.ieee, 1)])
await send_attributes_report(hass, dev3_cluster_on_off, {0: 1})
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
assert device_3_entity_id in zha_group.member_entity_ids
assert len(zha_group.members) == 3
@@ -631,14 +634,14 @@ async def test_zha_group_light_entity(
# add a member back and ensure that the group entity was created again
await zha_group.async_add_members([GroupMember(device_light_3.ieee, 1)])
await send_attributes_report(hass, dev3_cluster_on_off, {0: 1})
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
assert len(zha_group.members) == 2
assert hass.states.get(group_entity_id).state == STATE_ON
# add a 3rd member and ensure we still have an entity and we track the new one
await send_attributes_report(hass, dev1_cluster_on_off, {0: 0})
await send_attributes_report(hass, dev3_cluster_on_off, {0: 0})
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
assert hass.states.get(group_entity_id).state == STATE_OFF
# this will test that _reprobe_group is used correctly
@@ -646,7 +649,7 @@ async def test_zha_group_light_entity(
[GroupMember(device_light_2.ieee, 1), GroupMember(coordinator.ieee, 1)]
)
await send_attributes_report(hass, dev2_cluster_on_off, {0: 1})
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
assert len(zha_group.members) == 4
assert hass.states.get(group_entity_id).state == STATE_ON
diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py
index 6c464efd7b2f02..72ba0aba9c5c3c 100644
--- a/tests/components/zha/test_lock.py
+++ b/tests/components/zha/test_lock.py
@@ -16,6 +16,9 @@
LOCK_DOOR = 0
UNLOCK_DOOR = 1
+SET_PIN_CODE = 5
+CLEAR_PIN_CODE = 7
+SET_USER_STATUS = 9
@pytest.fixture
@@ -68,6 +71,18 @@ async def test_lock(hass, lock):
# unlock from HA
await async_unlock(hass, cluster, entity_id)
+ # set user code
+ await async_set_user_code(hass, cluster, entity_id)
+
+ # clear user code
+ await async_clear_user_code(hass, cluster, entity_id)
+
+ # enable user code
+ await async_enable_user_code(hass, cluster, entity_id)
+
+ # disable user code
+ await async_disable_user_code(hass, cluster, entity_id)
+
async def async_lock(hass, cluster, entity_id):
"""Test lock functionality from hass."""
@@ -95,3 +110,91 @@ async def async_unlock(hass, cluster, entity_id):
assert cluster.request.call_count == 1
assert cluster.request.call_args[0][0] is False
assert cluster.request.call_args[0][1] == UNLOCK_DOOR
+
+
+async def async_set_user_code(hass, cluster, entity_id):
+ """Test set lock code functionality from hass."""
+ with patch(
+ "zigpy.zcl.Cluster.request", return_value=mock_coro([zcl_f.Status.SUCCESS])
+ ):
+ # set lock code via service call
+ await hass.services.async_call(
+ "zha",
+ "set_lock_user_code",
+ {"entity_id": entity_id, "code_slot": 3, "user_code": "13246579"},
+ blocking=True,
+ )
+ assert cluster.request.call_count == 1
+ assert cluster.request.call_args[0][0] is False
+ assert cluster.request.call_args[0][1] == SET_PIN_CODE
+ assert cluster.request.call_args[0][3] == 2 # user slot 3 => internal slot 2
+ assert cluster.request.call_args[0][4] == closures.DoorLock.UserStatus.Enabled
+ assert (
+ cluster.request.call_args[0][5] == closures.DoorLock.UserType.Unrestricted
+ )
+ assert cluster.request.call_args[0][6] == "13246579"
+
+
+async def async_clear_user_code(hass, cluster, entity_id):
+ """Test clear lock code functionality from hass."""
+ with patch(
+ "zigpy.zcl.Cluster.request", return_value=mock_coro([zcl_f.Status.SUCCESS])
+ ):
+ # set lock code via service call
+ await hass.services.async_call(
+ "zha",
+ "clear_lock_user_code",
+ {
+ "entity_id": entity_id,
+ "code_slot": 3,
+ },
+ blocking=True,
+ )
+ assert cluster.request.call_count == 1
+ assert cluster.request.call_args[0][0] is False
+ assert cluster.request.call_args[0][1] == CLEAR_PIN_CODE
+ assert cluster.request.call_args[0][3] == 2 # user slot 3 => internal slot 2
+
+
+async def async_enable_user_code(hass, cluster, entity_id):
+ """Test enable lock code functionality from hass."""
+ with patch(
+ "zigpy.zcl.Cluster.request", return_value=mock_coro([zcl_f.Status.SUCCESS])
+ ):
+ # set lock code via service call
+ await hass.services.async_call(
+ "zha",
+ "enable_lock_user_code",
+ {
+ "entity_id": entity_id,
+ "code_slot": 3,
+ },
+ blocking=True,
+ )
+ assert cluster.request.call_count == 1
+ assert cluster.request.call_args[0][0] is False
+ assert cluster.request.call_args[0][1] == SET_USER_STATUS
+ assert cluster.request.call_args[0][3] == 2 # user slot 3 => internal slot 2
+ assert cluster.request.call_args[0][4] == closures.DoorLock.UserStatus.Enabled
+
+
+async def async_disable_user_code(hass, cluster, entity_id):
+ """Test disable lock code functionality from hass."""
+ with patch(
+ "zigpy.zcl.Cluster.request", return_value=mock_coro([zcl_f.Status.SUCCESS])
+ ):
+ # set lock code via service call
+ await hass.services.async_call(
+ "zha",
+ "disable_lock_user_code",
+ {
+ "entity_id": entity_id,
+ "code_slot": 3,
+ },
+ blocking=True,
+ )
+ assert cluster.request.call_count == 1
+ assert cluster.request.call_args[0][0] is False
+ assert cluster.request.call_args[0][1] == SET_USER_STATUS
+ assert cluster.request.call_args[0][3] == 2 # user slot 3 => internal slot 2
+ assert cluster.request.call_args[0][4] == closures.DoorLock.UserStatus.Disabled
diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py
index da3037f720d832..4cec0753c684ab 100644
--- a/tests/components/zha/test_switch.py
+++ b/tests/components/zha/test_switch.py
@@ -20,6 +20,7 @@
)
from tests.common import mock_coro
+from tests.components.zha.common import async_wait_for_updates
ON = 1
OFF = 0
@@ -160,6 +161,10 @@ async def test_switch(hass, zha_device_joined_restored, zigpy_device):
await async_test_rejoin(hass, zigpy_device, [cluster], (1,))
+@patch(
+ "homeassistant.components.zha.entity.UPDATE_GROUP_FROM_CHILD_DELAY",
+ new=0,
+)
async def test_zha_group_switch_entity(
hass, device_switch_1, device_switch_2, coordinator
):
@@ -195,14 +200,14 @@ async def test_zha_group_switch_entity(
dev2_cluster_on_off = device_switch_2.device.endpoints[1].on_off
await async_enable_traffic(hass, [device_switch_1, device_switch_2], enabled=False)
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
# test that the lights were created and that they are off
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
# allow traffic to flow through the gateway and device
await async_enable_traffic(hass, [device_switch_1, device_switch_2])
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
# test that the lights were created and are off
assert hass.states.get(entity_id).state == STATE_OFF
@@ -240,25 +245,25 @@ async def test_zha_group_switch_entity(
# test some of the group logic to make sure we key off states correctly
await send_attributes_report(hass, dev1_cluster_on_off, {0: 1})
await send_attributes_report(hass, dev2_cluster_on_off, {0: 1})
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
# test that group light is on
assert hass.states.get(entity_id).state == STATE_ON
await send_attributes_report(hass, dev1_cluster_on_off, {0: 0})
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
# test that group light is still on
assert hass.states.get(entity_id).state == STATE_ON
await send_attributes_report(hass, dev2_cluster_on_off, {0: 0})
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
# test that group light is now off
assert hass.states.get(entity_id).state == STATE_OFF
await send_attributes_report(hass, dev1_cluster_on_off, {0: 1})
- await hass.async_block_till_done()
+ await async_wait_for_updates(hass)
# test that group light is now back on
assert hass.states.get(entity_id).state == STATE_ON
diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py
index 07fd83cbe779fd..8d0fddb921c87c 100644
--- a/tests/components/zone/test_init.py
+++ b/tests/components/zone/test_init.py
@@ -15,7 +15,7 @@
)
from homeassistant.core import Context
from homeassistant.exceptions import Unauthorized
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry
@@ -144,7 +144,7 @@ async def test_active_zone_skips_passive_zones_2(hass):
)
await hass.async_block_till_done()
active = zone.async_active_zone(hass, 32.880700, -117.237561)
- assert "zone.active_zone" == active.entity_id
+ assert active.entity_id == "zone.active_zone"
async def test_active_zone_prefers_smaller_zone_if_same_distance(hass):
@@ -173,7 +173,7 @@ async def test_active_zone_prefers_smaller_zone_if_same_distance(hass):
)
active = zone.async_active_zone(hass, latitude, longitude)
- assert "zone.small_zone" == active.entity_id
+ assert active.entity_id == "zone.small_zone"
async def test_active_zone_prefers_smaller_zone_if_same_distance_2(hass):
@@ -196,7 +196,7 @@ async def test_active_zone_prefers_smaller_zone_if_same_distance_2(hass):
)
active = zone.async_active_zone(hass, latitude, longitude)
- assert "zone.smallest_zone" == active.entity_id
+ assert active.entity_id == "zone.smallest_zone"
async def test_in_zone_works_for_passive_zones(hass):
@@ -244,7 +244,7 @@ async def test_core_config_update(hass):
async def test_reload(hass, hass_admin_user, hass_read_only_user):
"""Test reload service."""
count_start = len(hass.states.async_entity_ids())
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
assert await setup.async_setup_component(
hass,
@@ -365,7 +365,7 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup):
input_id = "from_storage"
input_entity_id = f"{DOMAIN}.{input_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state is not None
@@ -401,7 +401,7 @@ async def test_update(hass, hass_ws_client, storage_setup):
input_id = "from_storage"
input_entity_id = f"{DOMAIN}.{input_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state.attributes["latitude"] == 1
@@ -435,7 +435,7 @@ async def test_ws_create(hass, hass_ws_client, storage_setup):
input_id = "new_input"
input_entity_id = f"{DOMAIN}.{input_id}"
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
state = hass.states.get(input_entity_id)
assert state is None
diff --git a/tests/components/zone/test_trigger.py b/tests/components/zone/test_trigger.py
index d7f5857b466c2c..978603ae242c53 100644
--- a/tests/components/zone/test_trigger.py
+++ b/tests/components/zone/test_trigger.py
@@ -7,7 +7,7 @@
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service, mock_component
-from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
@pytest.fixture
@@ -66,6 +66,7 @@ async def test_if_fires_on_zone_enter(hass, calls):
"from_state.state",
"to_state.state",
"zone.name",
+ "id",
)
)
},
@@ -84,7 +85,7 @@ async def test_if_fires_on_zone_enter(hass, calls):
assert len(calls) == 1
assert calls[0].context.parent_id == context.id
- assert "zone - test.entity - hello - hello - test" == calls[0].data["some"]
+ assert calls[0].data["some"] == "zone - test.entity - hello - hello - test - 0"
# Set out of zone again so we can trigger call
hass.states.async_set(
diff --git a/tests/components/zwave/conftest.py b/tests/components/zwave/conftest.py
index 13da12c67fff70..027d3a82ea2a89 100644
--- a/tests/components/zwave/conftest.py
+++ b/tests/components/zwave/conftest.py
@@ -5,7 +5,7 @@
from homeassistant.components.zwave import const
-from tests.components.light.conftest import mock_light_profiles # noqa
+from tests.components.light.conftest import mock_light_profiles # noqa: F401
from tests.mock.zwave import MockNetwork, MockNode, MockOption, MockValue
diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py
index 5275ca79506721..1afe961709745d 100644
--- a/tests/components/zwave/test_climate.py
+++ b/tests/components/zwave/test_climate.py
@@ -845,10 +845,10 @@ def test_hvac_action_value_changed_unknown(device_unknown):
def test_fan_action_value_changed(device):
"""Test values changed for climate device."""
- assert device.device_state_attributes[climate.ATTR_FAN_ACTION] == 7
+ assert device.extra_state_attributes[climate.ATTR_FAN_ACTION] == 7
device.values.fan_action.data = 9
value_changed(device.values.fan_action)
- assert device.device_state_attributes[climate.ATTR_FAN_ACTION] == 9
+ assert device.extra_state_attributes[climate.ATTR_FAN_ACTION] == 9
def test_aux_heat_unsupported_set(device):
diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py
index d70c3d631d529c..6b1a6fe4f98cb5 100644
--- a/tests/components/zwave/test_init.py
+++ b/tests/components/zwave/test_init.py
@@ -17,9 +17,8 @@
const,
)
from homeassistant.components.zwave.binary_sensor import get_device
-from homeassistant.const import ATTR_ENTITY_ID
-from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg
-from homeassistant.helpers.entity_registry import async_get_registry
+from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME
+from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.common import async_fire_time_changed, mock_registry
from tests.mock.zwave import MockEntityValues, MockNetwork, MockNode, MockValue
@@ -203,20 +202,17 @@ async def sleep(duration, loop=None):
sleeps.append(duration)
await asyncio_sleep(0)
- with patch("homeassistant.components.zwave.dt_util.utcnow", new=utcnow):
- with patch("asyncio.sleep", new=sleep):
- with patch.object(zwave, "_LOGGER") as mock_logger:
- hass.data[DATA_NETWORK].state = MockNetwork.STATE_STARTED
+ with patch("homeassistant.components.zwave.dt_util.utcnow", new=utcnow), patch(
+ "asyncio.sleep", new=sleep
+ ), patch.object(zwave, "_LOGGER") as mock_logger:
+ hass.data[DATA_NETWORK].state = MockNetwork.STATE_STARTED
- await hass.async_start()
+ await hass.async_start()
- assert len(sleeps) == const.NETWORK_READY_WAIT_SECS
- assert mock_logger.warning.called
- assert len(mock_logger.warning.mock_calls) == 1
- assert (
- mock_logger.warning.mock_calls[0][1][1]
- == const.NETWORK_READY_WAIT_SECS
- )
+ assert len(sleeps) == const.NETWORK_READY_WAIT_SECS
+ assert mock_logger.warning.called
+ assert len(mock_logger.warning.mock_calls) == 1
+ assert mock_logger.warning.mock_calls[0][1][1] == const.NETWORK_READY_WAIT_SECS
async def test_device_entity(hass, mock_openzwave):
@@ -243,7 +239,7 @@ async def test_device_entity(hass, mock_openzwave):
assert not device.should_poll
assert device.unique_id == "10-11"
assert device.name == "Mock Node Sensor"
- assert device.device_state_attributes[zwave.ATTR_POWER] == 50.123
+ assert device.extra_state_attributes[zwave.ATTR_POWER] == 50.123
async def test_node_removed(hass, mock_openzwave):
@@ -342,19 +338,19 @@ async def sleep(duration, loop=None):
sleeps.append(duration)
await asyncio_sleep(0)
- with patch("homeassistant.components.zwave.dt_util.utcnow", new=utcnow):
- with patch("asyncio.sleep", new=sleep):
- with patch.object(zwave, "_LOGGER") as mock_logger:
- await hass.async_add_executor_job(mock_receivers[0], node)
- await hass.async_block_till_done()
-
- assert len(sleeps) == const.NODE_READY_WAIT_SECS
- assert mock_logger.warning.called
- assert len(mock_logger.warning.mock_calls) == 1
- assert mock_logger.warning.mock_calls[0][1][1:] == (
- 14,
- const.NODE_READY_WAIT_SECS,
- )
+ with patch("homeassistant.components.zwave.dt_util.utcnow", new=utcnow), patch(
+ "asyncio.sleep", new=sleep
+ ), patch.object(zwave, "_LOGGER") as mock_logger:
+ await hass.async_add_executor_job(mock_receivers[0], node)
+ await hass.async_block_till_done()
+
+ assert len(sleeps) == const.NODE_READY_WAIT_SECS
+ assert mock_logger.warning.called
+ assert len(mock_logger.warning.mock_calls) == 1
+ assert mock_logger.warning.mock_calls[0][1][1:] == (
+ 14,
+ const.NODE_READY_WAIT_SECS,
+ )
assert hass.states.get("zwave.unknown_node_14").state == "unknown"
@@ -472,8 +468,8 @@ def mock_connect(receiver, signal, *args, **kwargs):
assert hass.states.get("binary_sensor.mock_node_mock_value").state == "off"
assert hass.states.get("binary_sensor.mock_node_mock_value_b").state == "off"
- ent_reg = await async_get_registry(hass)
- dev_reg = await get_dev_reg(hass)
+ ent_reg = er.async_get(hass)
+ dev_reg = dr.async_get(hass)
entry = ent_reg.async_get("zwave.mock_node")
assert entry is not None
@@ -506,7 +502,7 @@ def mock_connect(receiver, signal, *args, **kwargs):
await hass.services.async_call(
"zwave",
"rename_node",
- {const.ATTR_NODE_ID: node.node_id, const.ATTR_NAME: "Demo Node"},
+ {const.ATTR_NODE_ID: node.node_id, ATTR_NAME: "Demo Node"},
)
await hass.async_block_till_done()
@@ -537,7 +533,7 @@ def mock_connect(receiver, signal, *args, **kwargs):
{
const.ATTR_NODE_ID: node.node_id,
const.ATTR_UPDATE_IDS: True,
- const.ATTR_NAME: "New Node",
+ ATTR_NAME: "New Node",
},
)
await hass.async_block_till_done()
@@ -568,7 +564,7 @@ def mock_connect(receiver, signal, *args, **kwargs):
const.ATTR_NODE_ID: node.node_id,
const.ATTR_VALUE_ID: value.object_id,
const.ATTR_UPDATE_IDS: True,
- const.ATTR_NAME: "New Label",
+ ATTR_NAME: "New Label",
},
)
await hass.async_block_till_done()
@@ -862,7 +858,7 @@ async def test_entity_discovery(
assert values.primary is value_class.primary
assert len(list(values)) == 3
- assert sorted(list(values), key=lambda a: id(a)) == sorted(
+ assert sorted(values, key=lambda a: id(a)) == sorted(
[value_class.primary, None, None], key=lambda a: id(a)
)
@@ -886,7 +882,7 @@ async def test_entity_discovery(
assert values.secondary is value_class.secondary
assert len(list(values)) == 3
- assert sorted(list(values), key=lambda a: id(a)) == sorted(
+ assert sorted(values, key=lambda a: id(a)) == sorted(
[value_class.primary, value_class.secondary, None], key=lambda a: id(a)
)
@@ -903,7 +899,7 @@ async def test_entity_discovery(
assert values.optional is value_class.optional
assert len(list(values)) == 3
- assert sorted(list(values), key=lambda a: id(a)) == sorted(
+ assert sorted(values, key=lambda a: id(a)) == sorted(
[value_class.primary, value_class.secondary, value_class.optional],
key=lambda a: id(a),
)
@@ -962,7 +958,7 @@ async def test_entity_existing_values(
assert values.secondary is value_class.secondary
assert values.optional is value_class.optional
assert len(list(values)) == 3
- assert sorted(list(values), key=lambda a: id(a)) == sorted(
+ assert sorted(values, key=lambda a: id(a)) == sorted(
[value_class.primary, value_class.secondary, value_class.optional],
key=lambda a: id(a),
)
@@ -1360,7 +1356,7 @@ async def test_rename_node(hass, mock_openzwave, zwave_setup_ready):
await hass.services.async_call(
"zwave",
"rename_node",
- {const.ATTR_NODE_ID: 11, const.ATTR_NAME: "test_name"},
+ {const.ATTR_NODE_ID: 11, ATTR_NAME: "test_name"},
)
await hass.async_block_till_done()
@@ -1383,7 +1379,7 @@ async def test_rename_value(hass, mock_openzwave, zwave_setup_ready):
{
const.ATTR_NODE_ID: 11,
const.ATTR_VALUE_ID: 123456,
- const.ATTR_NAME: "New Label",
+ ATTR_NAME: "New Label",
},
)
await hass.async_block_till_done()
diff --git a/tests/components/zwave/test_lock.py b/tests/components/zwave/test_lock.py
index d5b6d0a0d275dd..9050f06f87bb23 100644
--- a/tests/components/zwave/test_lock.py
+++ b/tests/components/zwave/test_lock.py
@@ -98,7 +98,7 @@ def test_track_message_workaround(mock_openzwave):
device = lock.get_device(node=node, values=values)
value_changed(values.primary)
assert device.is_locked
- assert device.device_state_attributes[lock.ATTR_NOTIFICATION] == "RF Lock"
+ assert device.extra_state_attributes[lock.ATTR_NOTIFICATION] == "RF Lock"
# Simulate a keypad unlock. We trigger a value_changed() which simulates
# the Alarm notification received from the lock. Then, we trigger
@@ -113,7 +113,7 @@ def test_track_message_workaround(mock_openzwave):
value_changed(values.primary)
assert not device.is_locked
assert (
- device.device_state_attributes[lock.ATTR_LOCK_STATUS]
+ device.extra_state_attributes[lock.ATTR_LOCK_STATUS]
== "Unlocked with Keypad by user 3"
)
@@ -122,7 +122,7 @@ def test_track_message_workaround(mock_openzwave):
node.stats["lastReceivedMessage"][5] = const.COMMAND_CLASS_DOOR_LOCK
value_changed(values.primary)
assert device.is_locked
- assert device.device_state_attributes[lock.ATTR_NOTIFICATION] == "RF Lock"
+ assert device.extra_state_attributes[lock.ATTR_NOTIFICATION] == "RF Lock"
def test_v2btze_value_changed(mock_openzwave):
@@ -198,7 +198,7 @@ def test_lock_access_control(mock_openzwave):
)
device = lock.get_device(node=node, values=values, node_config={})
- assert device.device_state_attributes[lock.ATTR_NOTIFICATION] == "Lock Jammed"
+ assert device.extra_state_attributes[lock.ATTR_NOTIFICATION] == "Lock Jammed"
def test_lock_alarm_type(mock_openzwave):
@@ -212,28 +212,28 @@ def test_lock_alarm_type(mock_openzwave):
)
device = lock.get_device(node=node, values=values, node_config={})
- assert lock.ATTR_LOCK_STATUS not in device.device_state_attributes
+ assert lock.ATTR_LOCK_STATUS not in device.extra_state_attributes
values.alarm_type.data = 21
value_changed(values.alarm_type)
assert (
- device.device_state_attributes[lock.ATTR_LOCK_STATUS] == "Manually Locked None"
+ device.extra_state_attributes[lock.ATTR_LOCK_STATUS] == "Manually Locked None"
)
values.alarm_type.data = 18
value_changed(values.alarm_type)
assert (
- device.device_state_attributes[lock.ATTR_LOCK_STATUS]
+ device.extra_state_attributes[lock.ATTR_LOCK_STATUS]
== "Locked with Keypad by user None"
)
values.alarm_type.data = 161
value_changed(values.alarm_type)
- assert device.device_state_attributes[lock.ATTR_LOCK_STATUS] == "Tamper Alarm: None"
+ assert device.extra_state_attributes[lock.ATTR_LOCK_STATUS] == "Tamper Alarm: None"
values.alarm_type.data = 9
value_changed(values.alarm_type)
- assert device.device_state_attributes[lock.ATTR_LOCK_STATUS] == "Deadbolt Jammed"
+ assert device.extra_state_attributes[lock.ATTR_LOCK_STATUS] == "Deadbolt Jammed"
def test_lock_alarm_level(mock_openzwave):
@@ -247,14 +247,14 @@ def test_lock_alarm_level(mock_openzwave):
)
device = lock.get_device(node=node, values=values, node_config={})
- assert lock.ATTR_LOCK_STATUS not in device.device_state_attributes
+ assert lock.ATTR_LOCK_STATUS not in device.extra_state_attributes
values.alarm_type.data = 21
values.alarm_level.data = 1
value_changed(values.alarm_type)
value_changed(values.alarm_level)
assert (
- device.device_state_attributes[lock.ATTR_LOCK_STATUS]
+ device.extra_state_attributes[lock.ATTR_LOCK_STATUS]
== "Manually Locked by Key Cylinder or Inside thumb turn"
)
@@ -263,7 +263,7 @@ def test_lock_alarm_level(mock_openzwave):
value_changed(values.alarm_type)
value_changed(values.alarm_level)
assert (
- device.device_state_attributes[lock.ATTR_LOCK_STATUS]
+ device.extra_state_attributes[lock.ATTR_LOCK_STATUS]
== "Locked with Keypad by user alice"
)
@@ -272,7 +272,7 @@ def test_lock_alarm_level(mock_openzwave):
value_changed(values.alarm_type)
value_changed(values.alarm_level)
assert (
- device.device_state_attributes[lock.ATTR_LOCK_STATUS]
+ device.extra_state_attributes[lock.ATTR_LOCK_STATUS]
== "Tamper Alarm: Too many keypresses"
)
diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py
index ba77aabc923866..c47201fb1682a4 100644
--- a/tests/components/zwave/test_node_entity.py
+++ b/tests/components/zwave/test_node_entity.py
@@ -193,8 +193,7 @@ def listener(event):
# Make sure application version isn't set before
assert (
- node_entity.ATTR_APPLICATION_VERSION
- not in entity.device_state_attributes.keys()
+ node_entity.ATTR_APPLICATION_VERSION not in entity.extra_state_attributes.keys()
)
# Add entity to hass
@@ -212,9 +211,7 @@ def listener(event):
)
await hass.async_block_till_done()
- assert (
- entity.device_state_attributes[node_entity.ATTR_APPLICATION_VERSION] == "5.10"
- )
+ assert entity.extra_state_attributes[node_entity.ATTR_APPLICATION_VERSION] == "5.10"
# Fire off a changed
value = mock_zwave.MockValue(
@@ -227,9 +224,7 @@ def listener(event):
)
await hass.async_block_till_done()
- assert (
- entity.device_state_attributes[node_entity.ATTR_APPLICATION_VERSION] == "4.14"
- )
+ assert entity.extra_state_attributes[node_entity.ATTR_APPLICATION_VERSION] == "4.14"
async def test_network_node_changed_from_value(hass, mock_openzwave):
@@ -306,7 +301,7 @@ async def test_node_changed(hass, mock_openzwave):
"node_name": "Mock Node",
"manufacturer_name": "Test Manufacturer",
"product_name": "Test Product",
- } == entity.device_state_attributes
+ } == entity.extra_state_attributes
node.get_values.return_value = {1: mock_zwave.MockValue(data=1800)}
zwave_network.manager.getNodeStatistics.return_value = {
@@ -616,12 +611,12 @@ async def test_node_changed(hass, mock_openzwave):
"sentCnt": 7,
"sentFailed": 1,
"sentTS": "2017-03-27 15:38:15:620 ",
- } == entity.device_state_attributes
+ } == entity.extra_state_attributes
node.can_wake_up_value = False
entity.node_changed()
- assert "wake_up_interval" not in entity.device_state_attributes
+ assert "wake_up_interval" not in entity.extra_state_attributes
async def test_name(hass, mock_openzwave):
diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py
index a7be137657c9d3..c94d11588751c9 100644
--- a/tests/components/zwave_js/common.py
+++ b/tests/components/zwave_js/common.py
@@ -1,12 +1,27 @@
"""Provide common test tools for Z-Wave JS."""
AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature"
+HUMIDITY_SENSOR = "sensor.multisensor_6_humidity"
ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2"
POWER_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
-SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports_current_value"
+SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports"
LOW_BATTERY_BINARY_SENSOR = "binary_sensor.multisensor_6_low_battery_level"
ENABLED_LEGACY_BINARY_SENSOR = "binary_sensor.z_wave_door_window_sensor_any"
DISABLED_LEGACY_BINARY_SENSOR = "binary_sensor.multisensor_6_any"
-NOTIFICATION_MOTION_BINARY_SENSOR = "binary_sensor.multisensor_6_motion_sensor_status"
+NOTIFICATION_MOTION_BINARY_SENSOR = (
+ "binary_sensor.multisensor_6_home_security_motion_detection"
+)
+NOTIFICATION_MOTION_SENSOR = "sensor.multisensor_6_home_security_motion_sensor_status"
PROPERTY_DOOR_STATUS_BINARY_SENSOR = (
"binary_sensor.august_smart_lock_pro_3rd_gen_the_current_status_of_the_door"
)
+CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat"
+CLIMATE_DANFOSS_LC13_ENTITY = "climate.living_connect_z_thermostat"
+CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY = "climate.thermostatic_valve"
+CLIMATE_FLOOR_THERMOSTAT_ENTITY = "climate.floor_thermostat"
+CLIMATE_MAIN_HEAT_ACTIONNER = "climate.main_heat_actionner"
+BULB_6_MULTI_COLOR_LIGHT_ENTITY = "light.bulb_6_multi_color"
+EATON_RF9640_ENTITY = "light.allloaddimmer"
+AEON_SMART_SWITCH_LIGHT_ENTITY = "light.smart_switch_6"
+ID_LOCK_CONFIG_PARAMETER_SENSOR = (
+ "sensor.z_wave_module_for_id_lock_150_and_101_config_parameter_door_lock_mode"
+)
diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py
index 470b4c0227b8c4..0135351fd0df09 100644
--- a/tests/components/zwave_js/conftest.py
+++ b/tests/components/zwave_js/conftest.py
@@ -1,6 +1,8 @@
"""Provide common Z-Wave JS fixtures."""
+import asyncio
+import copy
import json
-from unittest.mock import DEFAULT, Mock, patch
+from unittest.mock import AsyncMock, patch
import pytest
from zwave_js_server.event import Event
@@ -8,40 +10,133 @@
from zwave_js_server.model.node import Node
from zwave_js_server.version import VersionInfo
-from homeassistant.helpers.device_registry import (
- async_get_registry as async_get_device_registry,
-)
+from homeassistant.helpers.device_registry import async_get as async_get_device_registry
from tests.common import MockConfigEntry, load_fixture
+# Add-on fixtures
+
+
+@pytest.fixture(name="addon_info_side_effect")
+def addon_info_side_effect_fixture():
+ """Return the add-on info side effect."""
+ return None
+
+
+@pytest.fixture(name="addon_info")
+def mock_addon_info(addon_info_side_effect):
+ """Mock Supervisor add-on info."""
+ with patch(
+ "homeassistant.components.zwave_js.addon.async_get_addon_info",
+ side_effect=addon_info_side_effect,
+ ) as addon_info:
+ addon_info.return_value = {}
+ yield addon_info
+
+
+@pytest.fixture(name="addon_running")
+def mock_addon_running(addon_info):
+ """Mock add-on already running."""
+ addon_info.return_value["state"] = "started"
+ return addon_info
+
+
+@pytest.fixture(name="addon_installed")
+def mock_addon_installed(addon_info):
+ """Mock add-on already installed but not running."""
+ addon_info.return_value["state"] = "stopped"
+ addon_info.return_value["version"] = "1.0"
+ return addon_info
+
+
+@pytest.fixture(name="addon_options")
+def mock_addon_options(addon_info):
+ """Mock add-on options."""
+ addon_info.return_value["options"] = {}
+ return addon_info.return_value["options"]
+
+
+@pytest.fixture(name="set_addon_options_side_effect")
+def set_addon_options_side_effect_fixture():
+ """Return the set add-on options side effect."""
+ return None
+
+
+@pytest.fixture(name="set_addon_options")
+def mock_set_addon_options(set_addon_options_side_effect):
+ """Mock set add-on options."""
+ with patch(
+ "homeassistant.components.zwave_js.addon.async_set_addon_options",
+ side_effect=set_addon_options_side_effect,
+ ) as set_options:
+ yield set_options
-@pytest.fixture(name="device_registry")
-async def device_registry_fixture(hass):
- """Return the device registry."""
- return await async_get_device_registry(hass)
+@pytest.fixture(name="install_addon")
+def mock_install_addon():
+ """Mock install add-on."""
+ with patch(
+ "homeassistant.components.zwave_js.addon.async_install_addon"
+ ) as install_addon:
+ yield install_addon
-@pytest.fixture(name="discovery_info")
-def discovery_info_fixture():
- """Return the discovery info from the supervisor."""
- return DEFAULT
+
+@pytest.fixture(name="update_addon")
+def mock_update_addon():
+ """Mock update add-on."""
+ with patch(
+ "homeassistant.components.zwave_js.addon.async_update_addon"
+ ) as update_addon:
+ yield update_addon
-@pytest.fixture(name="discovery_info_side_effect")
-def discovery_info_side_effect_fixture():
- """Return the discovery info from the supervisor."""
+@pytest.fixture(name="start_addon_side_effect")
+def start_addon_side_effect_fixture():
+ """Return the set add-on options side effect."""
return None
-@pytest.fixture(name="get_addon_discovery_info")
-def mock_get_addon_discovery_info(discovery_info, discovery_info_side_effect):
- """Mock get add-on discovery info."""
+@pytest.fixture(name="start_addon")
+def mock_start_addon(start_addon_side_effect):
+ """Mock start add-on."""
+ with patch(
+ "homeassistant.components.zwave_js.addon.async_start_addon",
+ side_effect=start_addon_side_effect,
+ ) as start_addon:
+ yield start_addon
+
+
+@pytest.fixture(name="stop_addon")
+def stop_addon_fixture():
+ """Mock stop add-on."""
+ with patch(
+ "homeassistant.components.zwave_js.addon.async_stop_addon"
+ ) as stop_addon:
+ yield stop_addon
+
+
+@pytest.fixture(name="uninstall_addon")
+def uninstall_addon_fixture():
+ """Mock uninstall add-on."""
with patch(
- "homeassistant.components.hassio.async_get_addon_discovery_info",
- side_effect=discovery_info_side_effect,
- return_value=discovery_info,
- ) as get_addon_discovery_info:
- yield get_addon_discovery_info
+ "homeassistant.components.zwave_js.addon.async_uninstall_addon"
+ ) as uninstall_addon:
+ yield uninstall_addon
+
+
+@pytest.fixture(name="create_shapshot")
+def create_snapshot_fixture():
+ """Mock create snapshot."""
+ with patch(
+ "homeassistant.components.zwave_js.addon.async_create_snapshot"
+ ) as create_shapshot:
+ yield create_shapshot
+
+
+@pytest.fixture(name="device_registry")
+async def device_registry_fixture(hass):
+ """Return the device registry."""
+ return async_get_device_registry(hass)
@pytest.fixture(name="controller_state", scope="session")
@@ -87,7 +182,7 @@ def bulb_6_multi_color_state_fixture():
@pytest.fixture(name="eaton_rf9640_dimmer_state", scope="session")
def eaton_rf9640_dimmer_state_fixture():
- """Load the bulb 6 multi-color node state fixture data."""
+ """Load the eaton rf9640 dimmer node state fixture data."""
return json.loads(load_fixture("zwave_js/eaton_rf9640_dimmer_state.json"))
@@ -111,12 +206,52 @@ def climate_radio_thermostat_ct100_plus_state_fixture():
)
+@pytest.fixture(
+ name="climate_radio_thermostat_ct100_plus_different_endpoints_state",
+ scope="session",
+)
+def climate_radio_thermostat_ct100_plus_different_endpoints_state_fixture():
+ """Load the thermostat fixture state with values on different endpoints.
+
+ This device is a radio thermostat ct100.
+ """
+ return json.loads(
+ load_fixture(
+ "zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json"
+ )
+ )
+
+
+@pytest.fixture(name="climate_danfoss_lc_13_state", scope="session")
+def climate_danfoss_lc_13_state_fixture():
+ """Load the climate Danfoss (LC-13) electronic radiator thermostat node state fixture data."""
+ return json.loads(load_fixture("zwave_js/climate_danfoss_lc_13_state.json"))
+
+
+@pytest.fixture(name="climate_eurotronic_spirit_z_state", scope="session")
+def climate_eurotronic_spirit_z_state_fixture():
+ """Load the climate Eurotronic Spirit Z thermostat node state fixture data."""
+ return json.loads(load_fixture("zwave_js/climate_eurotronic_spirit_z_state.json"))
+
+
+@pytest.fixture(name="climate_heatit_z_trm3_state", scope="session")
+def climate_heatit_z_trm3_state_fixture():
+ """Load the climate HEATIT Z-TRM3 thermostat node state fixture data."""
+ return json.loads(load_fixture("zwave_js/climate_heatit_z_trm3_state.json"))
+
+
@pytest.fixture(name="nortek_thermostat_state", scope="session")
def nortek_thermostat_state_fixture():
"""Load the nortek thermostat node state fixture data."""
return json.loads(load_fixture("zwave_js/nortek_thermostat_state.json"))
+@pytest.fixture(name="srt321_hrt4_zw_state", scope="session")
+def srt321_hrt4_zw_state_fixture():
+ """Load the climate HRT4-ZW / SRT321 / SRT322 thermostat node state fixture data."""
+ return json.loads(load_fixture("zwave_js/srt321_hrt4_zw_state.json"))
+
+
@pytest.fixture(name="chain_actuator_zws12_state", scope="session")
def window_cover_state_fixture():
"""Load the window cover node state fixture data."""
@@ -129,46 +264,115 @@ def in_wall_smart_fan_control_state_fixture():
return json.loads(load_fixture("zwave_js/in_wall_smart_fan_control_state.json"))
-@pytest.fixture(name="client")
-def mock_client_fixture(controller_state, version_state):
- """Mock a client."""
+@pytest.fixture(name="gdc_zw062_state", scope="session")
+def motorized_barrier_cover_state_fixture():
+ """Load the motorized barrier cover node state fixture data."""
+ return json.loads(load_fixture("zwave_js/cover_zw062_state.json"))
+
+
+@pytest.fixture(name="iblinds_v2_state", scope="session")
+def iblinds_v2_state_fixture():
+ """Load the iBlinds v2 node state fixture data."""
+ return json.loads(load_fixture("zwave_js/cover_iblinds_v2_state.json"))
+
- def mock_callback():
- callbacks = []
+@pytest.fixture(name="aeon_smart_switch_6_state", scope="session")
+def aeon_smart_switch_6_state_fixture():
+ """Load the AEON Labs (ZW096) Smart Switch 6 node state fixture data."""
+ return json.loads(load_fixture("zwave_js/aeon_smart_switch_6_state.json"))
- def add_callback(cb):
- callbacks.append(cb)
- return DEFAULT
- return callbacks, Mock(side_effect=add_callback)
+@pytest.fixture(name="ge_12730_state", scope="session")
+def ge_12730_state_fixture():
+ """Load the GE 12730 node state fixture data."""
+ return json.loads(load_fixture("zwave_js/fan_ge_12730_state.json"))
+
+
+@pytest.fixture(name="aeotec_radiator_thermostat_state", scope="session")
+def aeotec_radiator_thermostat_state_fixture():
+ """Load the Aeotec Radiator Thermostat node state fixture data."""
+ return json.loads(load_fixture("zwave_js/aeotec_radiator_thermostat_state.json"))
+
+
+@pytest.fixture(name="inovelli_lzw36_state", scope="session")
+def inovelli_lzw36_state_fixture():
+ """Load the Inovelli LZW36 node state fixture data."""
+ return json.loads(load_fixture("zwave_js/inovelli_lzw36_state.json"))
+
+
+@pytest.fixture(name="null_name_check_state", scope="session")
+def null_name_check_state_fixture():
+ """Load the null name check node state fixture data."""
+ return json.loads(load_fixture("zwave_js/null_name_check_state.json"))
+
+
+@pytest.fixture(name="lock_id_lock_as_id150_state", scope="session")
+def lock_id_lock_as_id150_state_fixture():
+ """Load the id lock id-150 lock node state fixture data."""
+ return json.loads(load_fixture("zwave_js/lock_id_lock_as_id150_state.json"))
+
+
+@pytest.fixture(
+ name="climate_radio_thermostat_ct101_multiple_temp_units_state", scope="session"
+)
+def climate_radio_thermostat_ct101_multiple_temp_units_state_fixture():
+ """Load the climate multiple temp units node state fixture data."""
+ return json.loads(
+ load_fixture(
+ "zwave_js/climate_radio_thermostat_ct101_multiple_temp_units_state.json"
+ )
+ )
+
+
+@pytest.fixture(
+ name="climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state",
+ scope="session",
+)
+def climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state_fixture():
+ """Load the climate device with mode and setpoint on different endpoints node state fixture data."""
+ return json.loads(
+ load_fixture(
+ "zwave_js/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json"
+ )
+ )
+
+
+@pytest.fixture(name="client")
+def mock_client_fixture(controller_state, version_state):
+ """Mock a client."""
with patch(
"homeassistant.components.zwave_js.ZwaveClient", autospec=True
) as client_class:
client = client_class.return_value
- connect_callback, client.register_on_connect = mock_callback()
- initialized_callback, client.register_on_initialized = mock_callback()
-
async def connect():
- for cb in connect_callback:
- await cb()
+ await asyncio.sleep(0)
+ client.connected = True
+
+ async def listen(driver_ready: asyncio.Event) -> None:
+ driver_ready.set()
+ await asyncio.sleep(30)
+ assert False, "Listen wasn't canceled!"
- for cb in initialized_callback:
- await cb()
+ async def disconnect():
+ client.connected = False
- client.connect = Mock(side_effect=connect)
+ client.connect = AsyncMock(side_effect=connect)
+ client.listen = AsyncMock(side_effect=listen)
+ client.disconnect = AsyncMock(side_effect=disconnect)
client.driver = Driver(client, controller_state)
+
client.version = VersionInfo.from_message(version_state)
client.ws_server_url = "ws://test:3000/zjs"
- client.state = "connected"
+
yield client
@pytest.fixture(name="multisensor_6")
def multisensor_6_fixture(client, multisensor_6_state):
"""Mock a multisensor 6 node."""
- node = Node(client, multisensor_6_state)
+ node = Node(client, copy.deepcopy(multisensor_6_state))
client.driver.controller.nodes[node.node_id] = node
return node
@@ -176,7 +380,7 @@ def multisensor_6_fixture(client, multisensor_6_state):
@pytest.fixture(name="ecolink_door_sensor")
def legacy_binary_sensor_fixture(client, ecolink_door_sensor_state):
"""Mock a legacy_binary_sensor node."""
- node = Node(client, ecolink_door_sensor_state)
+ node = Node(client, copy.deepcopy(ecolink_door_sensor_state))
client.driver.controller.nodes[node.node_id] = node
return node
@@ -184,7 +388,7 @@ def legacy_binary_sensor_fixture(client, ecolink_door_sensor_state):
@pytest.fixture(name="hank_binary_switch")
def hank_binary_switch_fixture(client, hank_binary_switch_state):
"""Mock a binary switch node."""
- node = Node(client, hank_binary_switch_state)
+ node = Node(client, copy.deepcopy(hank_binary_switch_state))
client.driver.controller.nodes[node.node_id] = node
return node
@@ -192,7 +396,7 @@ def hank_binary_switch_fixture(client, hank_binary_switch_state):
@pytest.fixture(name="bulb_6_multi_color")
def bulb_6_multi_color_fixture(client, bulb_6_multi_color_state):
"""Mock a bulb 6 multi-color node."""
- node = Node(client, bulb_6_multi_color_state)
+ node = Node(client, copy.deepcopy(bulb_6_multi_color_state))
client.driver.controller.nodes[node.node_id] = node
return node
@@ -200,7 +404,7 @@ def bulb_6_multi_color_fixture(client, bulb_6_multi_color_state):
@pytest.fixture(name="eaton_rf9640_dimmer")
def eaton_rf9640_dimmer_fixture(client, eaton_rf9640_dimmer_state):
"""Mock a Eaton RF9640 (V4 compatible) dimmer node."""
- node = Node(client, eaton_rf9640_dimmer_state)
+ node = Node(client, copy.deepcopy(eaton_rf9640_dimmer_state))
client.driver.controller.nodes[node.node_id] = node
return node
@@ -208,7 +412,7 @@ def eaton_rf9640_dimmer_fixture(client, eaton_rf9640_dimmer_state):
@pytest.fixture(name="lock_schlage_be469")
def lock_schlage_be469_fixture(client, lock_schlage_be469_state):
"""Mock a schlage lock node."""
- node = Node(client, lock_schlage_be469_state)
+ node = Node(client, copy.deepcopy(lock_schlage_be469_state))
client.driver.controller.nodes[node.node_id] = node
return node
@@ -216,7 +420,7 @@ def lock_schlage_be469_fixture(client, lock_schlage_be469_state):
@pytest.fixture(name="lock_august_pro")
def lock_august_asl03_fixture(client, lock_august_asl03_state):
"""Mock a August Pro lock node."""
- node = Node(client, lock_august_asl03_state)
+ node = Node(client, copy.deepcopy(lock_august_asl03_state))
client.driver.controller.nodes[node.node_id] = node
return node
@@ -226,7 +430,44 @@ def climate_radio_thermostat_ct100_plus_fixture(
client, climate_radio_thermostat_ct100_plus_state
):
"""Mock a climate radio thermostat ct100 plus node."""
- node = Node(client, climate_radio_thermostat_ct100_plus_state)
+ node = Node(client, copy.deepcopy(climate_radio_thermostat_ct100_plus_state))
+ client.driver.controller.nodes[node.node_id] = node
+ return node
+
+
+@pytest.fixture(name="climate_radio_thermostat_ct100_plus_different_endpoints")
+def climate_radio_thermostat_ct100_plus_different_endpoints_fixture(
+ client, climate_radio_thermostat_ct100_plus_different_endpoints_state
+):
+ """Mock a climate radio thermostat ct100 plus node with values on different endpoints."""
+ node = Node(
+ client,
+ copy.deepcopy(climate_radio_thermostat_ct100_plus_different_endpoints_state),
+ )
+ client.driver.controller.nodes[node.node_id] = node
+ return node
+
+
+@pytest.fixture(name="climate_danfoss_lc_13")
+def climate_danfoss_lc_13_fixture(client, climate_danfoss_lc_13_state):
+ """Mock a climate radio danfoss LC-13 node."""
+ node = Node(client, copy.deepcopy(climate_danfoss_lc_13_state))
+ client.driver.controller.nodes[node.node_id] = node
+ return node
+
+
+@pytest.fixture(name="climate_eurotronic_spirit_z")
+def climate_eurotronic_spirit_z_fixture(client, climate_eurotronic_spirit_z_state):
+ """Mock a climate radio danfoss LC-13 node."""
+ node = Node(client, climate_eurotronic_spirit_z_state)
+ client.driver.controller.nodes[node.node_id] = node
+ return node
+
+
+@pytest.fixture(name="climate_heatit_z_trm3")
+def climate_heatit_z_trm3_fixture(client, climate_heatit_z_trm3_state):
+ """Mock a climate radio HEATIT Z-TRM3 node."""
+ node = Node(client, copy.deepcopy(climate_heatit_z_trm3_state))
client.driver.controller.nodes[node.node_id] = node
return node
@@ -234,7 +475,23 @@ def climate_radio_thermostat_ct100_plus_fixture(
@pytest.fixture(name="nortek_thermostat")
def nortek_thermostat_fixture(client, nortek_thermostat_state):
"""Mock a nortek thermostat node."""
- node = Node(client, nortek_thermostat_state)
+ node = Node(client, copy.deepcopy(nortek_thermostat_state))
+ client.driver.controller.nodes[node.node_id] = node
+ return node
+
+
+@pytest.fixture(name="srt321_hrt4_zw")
+def srt321_hrt4_zw_fixture(client, srt321_hrt4_zw_state):
+ """Mock a HRT4-ZW / SRT321 / SRT322 thermostat node."""
+ node = Node(client, copy.deepcopy(srt321_hrt4_zw_state))
+ client.driver.controller.nodes[node.node_id] = node
+ return node
+
+
+@pytest.fixture(name="aeotec_radiator_thermostat")
+def aeotec_radiator_thermostat_fixture(client, aeotec_radiator_thermostat_state):
+ """Mock a Aeotec thermostat node."""
+ node = Node(client, aeotec_radiator_thermostat_state)
client.driver.controller.nodes[node.node_id] = node
return node
@@ -271,7 +528,7 @@ async def integration_fixture(hass, client):
@pytest.fixture(name="chain_actuator_zws12")
def window_cover_fixture(client, chain_actuator_zws12_state):
"""Mock a window cover node."""
- node = Node(client, chain_actuator_zws12_state)
+ node = Node(client, copy.deepcopy(chain_actuator_zws12_state))
client.driver.controller.nodes[node.node_id] = node
return node
@@ -279,6 +536,104 @@ def window_cover_fixture(client, chain_actuator_zws12_state):
@pytest.fixture(name="in_wall_smart_fan_control")
def in_wall_smart_fan_control_fixture(client, in_wall_smart_fan_control_state):
"""Mock a fan node."""
- node = Node(client, in_wall_smart_fan_control_state)
+ node = Node(client, copy.deepcopy(in_wall_smart_fan_control_state))
+ client.driver.controller.nodes[node.node_id] = node
+ return node
+
+
+@pytest.fixture(name="null_name_check")
+def null_name_check_fixture(client, null_name_check_state):
+ """Mock a node with no name."""
+ node = Node(client, copy.deepcopy(null_name_check_state))
+ client.driver.controller.nodes[node.node_id] = node
+ return node
+
+
+@pytest.fixture(name="multiple_devices")
+def multiple_devices_fixture(
+ client, climate_radio_thermostat_ct100_plus_state, lock_schlage_be469_state
+):
+ """Mock a client with multiple devices."""
+ node = Node(client, copy.deepcopy(climate_radio_thermostat_ct100_plus_state))
+ client.driver.controller.nodes[node.node_id] = node
+ node = Node(client, copy.deepcopy(lock_schlage_be469_state))
+ client.driver.controller.nodes[node.node_id] = node
+ return client.driver.controller.nodes
+
+
+@pytest.fixture(name="gdc_zw062")
+def motorized_barrier_cover_fixture(client, gdc_zw062_state):
+ """Mock a motorized barrier node."""
+ node = Node(client, copy.deepcopy(gdc_zw062_state))
+ client.driver.controller.nodes[node.node_id] = node
+ return node
+
+
+@pytest.fixture(name="iblinds_v2")
+def iblinds_cover_fixture(client, iblinds_v2_state):
+ """Mock an iBlinds v2.0 window cover node."""
+ node = Node(client, copy.deepcopy(iblinds_v2_state))
+ client.driver.controller.nodes[node.node_id] = node
+ return node
+
+
+@pytest.fixture(name="aeon_smart_switch_6")
+def aeon_smart_switch_6_fixture(client, aeon_smart_switch_6_state):
+ """Mock an AEON Labs (ZW096) Smart Switch 6 node."""
+ node = Node(client, aeon_smart_switch_6_state)
+ client.driver.controller.nodes[node.node_id] = node
+ return node
+
+
+@pytest.fixture(name="ge_12730")
+def ge_12730_fixture(client, ge_12730_state):
+ """Mock a GE 12730 fan controller node."""
+ node = Node(client, copy.deepcopy(ge_12730_state))
+ client.driver.controller.nodes[node.node_id] = node
+ return node
+
+
+@pytest.fixture(name="inovelli_lzw36")
+def inovelli_lzw36_fixture(client, inovelli_lzw36_state):
+ """Mock a Inovelli LZW36 fan controller node."""
+ node = Node(client, copy.deepcopy(inovelli_lzw36_state))
+ client.driver.controller.nodes[node.node_id] = node
+ return node
+
+
+@pytest.fixture(name="lock_id_lock_as_id150")
+def lock_id_lock_as_id150(client, lock_id_lock_as_id150_state):
+ """Mock an id lock id-150 lock node."""
+ node = Node(client, copy.deepcopy(lock_id_lock_as_id150_state))
+ client.driver.controller.nodes[node.node_id] = node
+ return node
+
+
+@pytest.fixture(name="climate_radio_thermostat_ct101_multiple_temp_units")
+def climate_radio_thermostat_ct101_multiple_temp_units_fixture(
+ client, climate_radio_thermostat_ct101_multiple_temp_units_state
+):
+ """Mock a climate device with multiple temp units node."""
+ node = Node(
+ client, copy.deepcopy(climate_radio_thermostat_ct101_multiple_temp_units_state)
+ )
+ client.driver.controller.nodes[node.node_id] = node
+ return node
+
+
+@pytest.fixture(
+ name="climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints"
+)
+def climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_fixture(
+ client,
+ climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state,
+):
+ """Mock a climate device with mode and setpoint on differenet endpoints node."""
+ node = Node(
+ client,
+ copy.deepcopy(
+ climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state
+ ),
+ )
client.driver.controller.nodes[node.node_id] = node
return node
diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py
index 88e8acc577156f..eb198b01f82a63 100644
--- a/tests/components/zwave_js/test_api.py
+++ b/tests/components/zwave_js/test_api.py
@@ -1,11 +1,29 @@
"""Test the Z-Wave JS Websocket API."""
+import json
from unittest.mock import patch
+from zwave_js_server.const import LogLevel
from zwave_js_server.event import Event
-
-from homeassistant.components.zwave_js.api import ENTRY_ID, ID, NODE_ID, TYPE
+from zwave_js_server.exceptions import InvalidNewValue, NotFoundError, SetValueFailed
+
+from homeassistant.components.websocket_api.const import ERR_NOT_FOUND
+from homeassistant.components.zwave_js.api import (
+ CONFIG,
+ ENABLED,
+ ENTRY_ID,
+ FILENAME,
+ FORCE_CONSOLE,
+ ID,
+ LEVEL,
+ LOG_TO_FILE,
+ NODE_ID,
+ PROPERTY,
+ PROPERTY_KEY,
+ TYPE,
+ VALUE,
+)
from homeassistant.components.zwave_js.const import DOMAIN
-from homeassistant.helpers.device_registry import async_get_registry
+from homeassistant.helpers import device_registry as dr
async def test_websocket_api(hass, integration, multisensor_6, hass_ws_client):
@@ -40,6 +58,55 @@ async def test_websocket_api(hass, integration, multisensor_6, hass_ws_client):
assert not result["is_secure"]
assert result["status"] == 1
+ # Test getting configuration parameter values
+ await ws_client.send_json(
+ {
+ ID: 4,
+ TYPE: "zwave_js/get_config_parameters",
+ ENTRY_ID: entry.entry_id,
+ NODE_ID: node.node_id,
+ }
+ )
+ msg = await ws_client.receive_json()
+ result = msg["result"]
+
+ assert len(result) == 61
+ key = "52-112-0-2"
+ assert result[key]["property"] == 2
+ assert result[key]["property_key"] is None
+ assert result[key]["metadata"]["type"] == "number"
+ assert result[key]["configuration_value_type"] == "enumerated"
+ assert result[key]["metadata"]["states"]
+
+ key = "52-112-0-201-255"
+ assert result[key]["property_key"] == 255
+
+ # Test getting non-existent node fails
+ await ws_client.send_json(
+ {
+ ID: 5,
+ TYPE: "zwave_js/node_status",
+ ENTRY_ID: entry.entry_id,
+ NODE_ID: 99999,
+ }
+ )
+ msg = await ws_client.receive_json()
+ assert not msg["success"]
+ assert msg["error"]["code"] == ERR_NOT_FOUND
+
+ # Test getting non-existent node config params fails
+ await ws_client.send_json(
+ {
+ ID: 6,
+ TYPE: "zwave_js/get_config_parameters",
+ ENTRY_ID: entry.entry_id,
+ NODE_ID: 99999,
+ }
+ )
+ msg = await ws_client.receive_json()
+ assert not msg["success"]
+ assert msg["error"]["code"] == ERR_NOT_FOUND
+
async def test_add_node(
hass, integration, client, hass_ws_client, nortek_thermostat_added_event
@@ -134,7 +201,7 @@ async def test_remove_node(
# Add mock node to controller
client.driver.controller.nodes[67] = nortek_thermostat
- dev_reg = await async_get_registry(hass)
+ dev_reg = dr.async_get(hass)
# Create device registry entry for mock node
device = dev_reg.async_get_or_create(
@@ -155,6 +222,164 @@ async def test_remove_node(
assert device is None
+async def test_refresh_node_info(
+ hass, client, integration, hass_ws_client, multisensor_6
+):
+ """Test that the refresh_node_info WS API call works."""
+ entry = integration
+ ws_client = await hass_ws_client(hass)
+
+ client.async_send_command_no_wait.return_value = None
+ await ws_client.send_json(
+ {
+ ID: 1,
+ TYPE: "zwave_js/refresh_node_info",
+ ENTRY_ID: entry.entry_id,
+ NODE_ID: 52,
+ }
+ )
+ msg = await ws_client.receive_json()
+ assert msg["success"]
+
+ assert len(client.async_send_command_no_wait.call_args_list) == 1
+ args = client.async_send_command_no_wait.call_args[0][0]
+ assert args["command"] == "node.refresh_info"
+ assert args["nodeId"] == 52
+
+ client.async_send_command_no_wait.reset_mock()
+
+ await ws_client.send_json(
+ {
+ ID: 2,
+ TYPE: "zwave_js/refresh_node_info",
+ ENTRY_ID: entry.entry_id,
+ NODE_ID: 999,
+ }
+ )
+ msg = await ws_client.receive_json()
+ assert not msg["success"]
+ assert msg["error"]["code"] == "not_found"
+
+
+async def test_set_config_parameter(
+ hass, client, hass_ws_client, multisensor_6, integration
+):
+ """Test the set_config_parameter service."""
+ entry = integration
+ ws_client = await hass_ws_client(hass)
+
+ client.async_send_command_no_wait.return_value = None
+
+ await ws_client.send_json(
+ {
+ ID: 1,
+ TYPE: "zwave_js/set_config_parameter",
+ ENTRY_ID: entry.entry_id,
+ NODE_ID: 52,
+ PROPERTY: 102,
+ PROPERTY_KEY: 1,
+ VALUE: 1,
+ }
+ )
+
+ msg = await ws_client.receive_json()
+ assert msg["success"]
+
+ assert len(client.async_send_command_no_wait.call_args_list) == 1
+ args = client.async_send_command_no_wait.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 52
+ assert args["valueId"] == {
+ "commandClassName": "Configuration",
+ "commandClass": 112,
+ "endpoint": 0,
+ "property": 102,
+ "propertyName": "Group 2: Send battery reports",
+ "propertyKey": 1,
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": True,
+ "valueSize": 4,
+ "min": 0,
+ "max": 1,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": True,
+ "label": "Group 2: Send battery reports",
+ "description": "Include battery information in periodic reports to Group 2",
+ "isFromConfig": True,
+ },
+ "value": 0,
+ }
+ assert args["value"] == 1
+
+ client.async_send_command_no_wait.reset_mock()
+
+ with patch(
+ "homeassistant.components.zwave_js.api.async_set_config_parameter",
+ ) as set_param_mock:
+ set_param_mock.side_effect = InvalidNewValue("test")
+ await ws_client.send_json(
+ {
+ ID: 2,
+ TYPE: "zwave_js/set_config_parameter",
+ ENTRY_ID: entry.entry_id,
+ NODE_ID: 52,
+ PROPERTY: 102,
+ PROPERTY_KEY: 1,
+ VALUE: 1,
+ }
+ )
+
+ msg = await ws_client.receive_json()
+
+ assert len(client.async_send_command_no_wait.call_args_list) == 0
+ assert not msg["success"]
+ assert msg["error"]["code"] == "not_supported"
+ assert msg["error"]["message"] == "test"
+
+ set_param_mock.side_effect = NotFoundError("test")
+ await ws_client.send_json(
+ {
+ ID: 3,
+ TYPE: "zwave_js/set_config_parameter",
+ ENTRY_ID: entry.entry_id,
+ NODE_ID: 52,
+ PROPERTY: 102,
+ PROPERTY_KEY: 1,
+ VALUE: 1,
+ }
+ )
+
+ msg = await ws_client.receive_json()
+
+ assert len(client.async_send_command_no_wait.call_args_list) == 0
+ assert not msg["success"]
+ assert msg["error"]["code"] == "not_found"
+ assert msg["error"]["message"] == "test"
+
+ set_param_mock.side_effect = SetValueFailed("test")
+ await ws_client.send_json(
+ {
+ ID: 4,
+ TYPE: "zwave_js/set_config_parameter",
+ ENTRY_ID: entry.entry_id,
+ NODE_ID: 52,
+ PROPERTY: 102,
+ PROPERTY_KEY: 1,
+ VALUE: 1,
+ }
+ )
+
+ msg = await ws_client.receive_json()
+
+ assert len(client.async_send_command_no_wait.call_args_list) == 0
+ assert not msg["success"]
+ assert msg["error"]["code"] == "unknown_error"
+ assert msg["error"]["message"] == "test"
+
+
async def test_dump_view(integration, hass_client):
"""Test the HTTP dump view."""
client = await hass_client()
@@ -164,7 +389,7 @@ async def test_dump_view(integration, hass_client):
):
resp = await client.get(f"/api/zwave_js/dump/{integration.entry_id}")
assert resp.status == 200
- assert await resp.text() == '{"hello": "world"}\n{"second": "msg"}\n'
+ assert json.loads(await resp.text()) == [{"hello": "world"}, {"second": "msg"}]
async def test_dump_view_invalid_entry_id(integration, hass_client):
@@ -172,3 +397,158 @@ async def test_dump_view_invalid_entry_id(integration, hass_client):
client = await hass_client()
resp = await client.get("/api/zwave_js/dump/INVALID")
assert resp.status == 400
+
+
+async def test_update_log_config(hass, client, integration, hass_ws_client):
+ """Test that the update_log_config WS API call works and that schema validation works."""
+ entry = integration
+ ws_client = await hass_ws_client(hass)
+
+ # Test we can set log level
+ client.async_send_command.return_value = {"success": True}
+ await ws_client.send_json(
+ {
+ ID: 1,
+ TYPE: "zwave_js/update_log_config",
+ ENTRY_ID: entry.entry_id,
+ CONFIG: {LEVEL: "Error"},
+ }
+ )
+ msg = await ws_client.receive_json()
+ assert msg["success"]
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "update_log_config"
+ assert args["config"] == {"level": "error"}
+
+ client.async_send_command.reset_mock()
+
+ # Test we can set logToFile to True
+ client.async_send_command.return_value = {"success": True}
+ await ws_client.send_json(
+ {
+ ID: 2,
+ TYPE: "zwave_js/update_log_config",
+ ENTRY_ID: entry.entry_id,
+ CONFIG: {LOG_TO_FILE: True, FILENAME: "/test"},
+ }
+ )
+ msg = await ws_client.receive_json()
+ assert msg["success"]
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "update_log_config"
+ assert args["config"] == {"logToFile": True, "filename": "/test"}
+
+ client.async_send_command.reset_mock()
+
+ # Test all parameters
+ client.async_send_command.return_value = {"success": True}
+ await ws_client.send_json(
+ {
+ ID: 3,
+ TYPE: "zwave_js/update_log_config",
+ ENTRY_ID: entry.entry_id,
+ CONFIG: {
+ LEVEL: "Error",
+ LOG_TO_FILE: True,
+ FILENAME: "/test",
+ FORCE_CONSOLE: True,
+ ENABLED: True,
+ },
+ }
+ )
+ msg = await ws_client.receive_json()
+ assert msg["success"]
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "update_log_config"
+ assert args["config"] == {
+ "level": "error",
+ "logToFile": True,
+ "filename": "/test",
+ "forceConsole": True,
+ "enabled": True,
+ }
+
+ client.async_send_command.reset_mock()
+
+ # Test error when setting unrecognized log level
+ await ws_client.send_json(
+ {
+ ID: 4,
+ TYPE: "zwave_js/update_log_config",
+ ENTRY_ID: entry.entry_id,
+ CONFIG: {LEVEL: "bad_log_level"},
+ }
+ )
+ msg = await ws_client.receive_json()
+ assert not msg["success"]
+ assert "error" in msg and "value must be one of" in msg["error"]["message"]
+
+ # Test error without service data
+ await ws_client.send_json(
+ {
+ ID: 5,
+ TYPE: "zwave_js/update_log_config",
+ ENTRY_ID: entry.entry_id,
+ CONFIG: {},
+ }
+ )
+ msg = await ws_client.receive_json()
+ assert not msg["success"]
+ assert "error" in msg and "must contain at least one of" in msg["error"]["message"]
+
+ # Test error if we set logToFile to True without providing filename
+ await ws_client.send_json(
+ {
+ ID: 6,
+ TYPE: "zwave_js/update_log_config",
+ ENTRY_ID: entry.entry_id,
+ CONFIG: {LOG_TO_FILE: True},
+ }
+ )
+ msg = await ws_client.receive_json()
+ assert not msg["success"]
+ assert (
+ "error" in msg
+ and "must be provided if logging to file" in msg["error"]["message"]
+ )
+
+
+async def test_get_log_config(hass, client, integration, hass_ws_client):
+ """Test that the get_log_config WS API call works."""
+ entry = integration
+ ws_client = await hass_ws_client(hass)
+
+ # Test we can get log configuration
+ client.async_send_command.return_value = {
+ "success": True,
+ "config": {
+ "enabled": True,
+ "level": "error",
+ "logToFile": False,
+ "filename": "/test.txt",
+ "forceConsole": False,
+ },
+ }
+ await ws_client.send_json(
+ {
+ ID: 1,
+ TYPE: "zwave_js/get_log_config",
+ ENTRY_ID: entry.entry_id,
+ }
+ )
+ msg = await ws_client.receive_json()
+ assert msg["result"]
+ assert msg["success"]
+
+ log_config = msg["result"]
+ assert log_config["enabled"]
+ assert log_config["level"] == LogLevel.ERROR
+ assert log_config["log_to_file"] is False
+ assert log_config["filename"] == "/test.txt"
+ assert log_config["force_console"] is False
diff --git a/tests/components/zwave_js/test_binary_sensor.py b/tests/components/zwave_js/test_binary_sensor.py
index e8361d8b03ef6f..ddfed9727e65ad 100644
--- a/tests/components/zwave_js/test_binary_sensor.py
+++ b/tests/components/zwave_js/test_binary_sensor.py
@@ -3,6 +3,7 @@
from homeassistant.components.binary_sensor import DEVICE_CLASS_MOTION
from homeassistant.const import DEVICE_CLASS_BATTERY, STATE_OFF, STATE_ON
+from homeassistant.helpers import entity_registry as er
from .common import (
DISABLED_LEGACY_BINARY_SENSOR,
@@ -61,7 +62,7 @@ async def test_disabled_legacy_sensor(hass, multisensor_6, integration):
"""Test disabled legacy boolean binary sensor."""
# this node has Notification CC implemented so legacy binary sensor should be disabled
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entity_id = DISABLED_LEGACY_BINARY_SENSOR
state = hass.states.get(entity_id)
assert state is None
diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py
index aca1022f8b0bc7..83a607f3add39c 100644
--- a/tests/components/zwave_js/test_climate.py
+++ b/tests/components/zwave_js/test_climate.py
@@ -3,7 +3,9 @@
from zwave_js_server.event import Event
from homeassistant.components.climate.const import (
+ ATTR_CURRENT_HUMIDITY,
ATTR_CURRENT_TEMPERATURE,
+ ATTR_FAN_MODE,
ATTR_HVAC_ACTION,
ATTR_HVAC_MODE,
ATTR_HVAC_MODES,
@@ -18,13 +20,28 @@
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
PRESET_NONE,
+ SERVICE_SET_FAN_MODE,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_TEMPERATURE,
+ SUPPORT_FAN_MODE,
+ SUPPORT_TARGET_TEMPERATURE,
+ SUPPORT_TARGET_TEMPERATURE_RANGE,
+)
+from homeassistant.components.zwave_js.climate import ATTR_FAN_STATE
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
+ ATTR_TEMPERATURE,
)
-from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
-CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat_thermostat_mode"
+from .common import (
+ CLIMATE_DANFOSS_LC13_ENTITY,
+ CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY,
+ CLIMATE_FLOOR_THERMOSTAT_ENTITY,
+ CLIMATE_MAIN_HEAT_ACTIONNER,
+ CLIMATE_RADIO_THERMOSTAT_ENTITY,
+)
async def test_thermostat_v2(
@@ -42,45 +59,19 @@ async def test_thermostat_v2(
HVAC_MODE_COOL,
HVAC_MODE_HEAT_COOL,
]
+ assert state.attributes[ATTR_CURRENT_HUMIDITY] == 30
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.2
assert state.attributes[ATTR_TEMPERATURE] == 22.2
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
- assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
-
- # Test setting preset mode
- await hass.services.async_call(
- CLIMATE_DOMAIN,
- SERVICE_SET_PRESET_MODE,
- {
- ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
- ATTR_PRESET_MODE: PRESET_NONE,
- },
- blocking=True,
+ assert state.attributes[ATTR_FAN_MODE] == "Auto low"
+ assert state.attributes[ATTR_FAN_STATE] == "Idle / off"
+ assert (
+ state.attributes[ATTR_SUPPORTED_FEATURES]
+ == SUPPORT_TARGET_TEMPERATURE
+ | SUPPORT_TARGET_TEMPERATURE_RANGE
+ | SUPPORT_FAN_MODE
)
- assert len(client.async_send_command.call_args_list) == 1
- args = client.async_send_command.call_args[0][0]
- assert args["command"] == "node.set_value"
- assert args["nodeId"] == 13
- assert args["valueId"] == {
- "commandClassName": "Thermostat Mode",
- "commandClass": 64,
- "endpoint": 1,
- "property": "mode",
- "propertyName": "mode",
- "metadata": {
- "type": "number",
- "readable": True,
- "writeable": True,
- "min": 0,
- "max": 31,
- "label": "Thermostat mode",
- "states": {"0": "Off", "1": "Heat", "2": "Cool", "3": "Auto"},
- },
- "value": 1,
- }
- assert args["value"] == 1
-
client.async_send_command.reset_mock()
# Test setting hvac mode
@@ -287,40 +278,322 @@ async def test_thermostat_v2(
client.async_send_command.reset_mock()
+ # Test setting invalid hvac mode
with pytest.raises(ValueError):
- # Test setting unknown preset mode
await hass.services.async_call(
CLIMATE_DOMAIN,
- SERVICE_SET_PRESET_MODE,
+ SERVICE_SET_HVAC_MODE,
{
ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
- ATTR_PRESET_MODE: "unknown_preset",
+ ATTR_HVAC_MODE: HVAC_MODE_DRY,
},
blocking=True,
)
- assert len(client.async_send_command.call_args_list) == 0
+ client.async_send_command.reset_mock()
- # Test setting invalid hvac mode
+ # Test setting fan mode
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_FAN_MODE,
+ {
+ ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
+ ATTR_FAN_MODE: "Low",
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 13
+ assert args["valueId"] == {
+ "endpoint": 1,
+ "commandClass": 68,
+ "commandClassName": "Thermostat Fan Mode",
+ "property": "mode",
+ "propertyName": "mode",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": True,
+ "min": 0,
+ "max": 255,
+ "states": {"0": "Auto low", "1": "Low"},
+ "label": "Thermostat fan mode",
+ },
+ "value": 0,
+ }
+ assert args["value"] == 1
+
+ client.async_send_command.reset_mock()
+
+ # Test setting invalid fan mode
with pytest.raises(ValueError):
await hass.services.async_call(
CLIMATE_DOMAIN,
- SERVICE_SET_HVAC_MODE,
+ SERVICE_SET_FAN_MODE,
{
ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
- ATTR_HVAC_MODE: HVAC_MODE_DRY,
+ ATTR_FAN_MODE: "fake value",
},
blocking=True,
)
- # Test setting invalid preset mode
+
+async def test_thermostat_different_endpoints(
+ hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration
+):
+ """Test an entity with values on a different endpoint from the primary value."""
+ state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY)
+
+ assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.8
+ assert state.attributes[ATTR_FAN_MODE] == "Auto low"
+ assert state.attributes[ATTR_FAN_STATE] == "Idle / off"
+
+
+async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integration):
+ """Test a setpoint thermostat command class entity."""
+ node = climate_danfoss_lc_13
+ state = hass.states.get(CLIMATE_DANFOSS_LC13_ENTITY)
+
+ assert state
+ assert state.state == HVAC_MODE_HEAT
+ assert state.attributes[ATTR_TEMPERATURE] == 14
+ assert state.attributes[ATTR_HVAC_MODES] == [HVAC_MODE_HEAT]
+ assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_TARGET_TEMPERATURE
+
+ client.async_send_command_no_wait.reset_mock()
+
+ # Test setting temperature
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {
+ ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY,
+ ATTR_TEMPERATURE: 21.5,
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command_no_wait.call_args_list) == 1
+ args = client.async_send_command_no_wait.call_args_list[0][0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 5
+ assert args["valueId"] == {
+ "endpoint": 0,
+ "commandClass": 67,
+ "commandClassName": "Thermostat Setpoint",
+ "property": "setpoint",
+ "propertyName": "setpoint",
+ "propertyKey": 1,
+ "propertyKeyName": "Heating",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": True,
+ "unit": "\u00b0C",
+ "ccSpecific": {"setpointType": 1},
+ },
+ "value": 14,
+ }
+ assert args["value"] == 21.5
+
+ client.async_send_command_no_wait.reset_mock()
+
+ # Test setpoint mode update from value updated event
+ event = Event(
+ type="value updated",
+ data={
+ "source": "node",
+ "event": "value updated",
+ "nodeId": 5,
+ "args": {
+ "commandClassName": "Thermostat Setpoint",
+ "commandClass": 67,
+ "endpoint": 0,
+ "property": "setpoint",
+ "propertyKey": 1,
+ "propertyKeyName": "Heating",
+ "propertyName": "setpoint",
+ "newValue": 23,
+ "prevValue": 21.5,
+ },
+ },
+ )
+ node.receive_event(event)
+
+ state = hass.states.get(CLIMATE_DANFOSS_LC13_ENTITY)
+ assert state.state == HVAC_MODE_HEAT
+ assert state.attributes[ATTR_TEMPERATURE] == 23
+
+ client.async_send_command_no_wait.reset_mock()
+
+
+async def test_thermostat_heatit(hass, client, climate_heatit_z_trm3, integration):
+ """Test a thermostat v2 command class entity."""
+ state = hass.states.get(CLIMATE_FLOOR_THERMOSTAT_ENTITY)
+
+ assert state
+ assert state.state == HVAC_MODE_HEAT
+ assert state.attributes[ATTR_HVAC_MODES] == [
+ HVAC_MODE_OFF,
+ HVAC_MODE_HEAT,
+ ]
+ assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.9
+ assert state.attributes[ATTR_TEMPERATURE] == 22.5
+ assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
+ assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_TARGET_TEMPERATURE
+
+
+async def test_thermostat_srt321_hrt4_zw(hass, client, srt321_hrt4_zw, integration):
+ """Test a climate entity from a HRT4-ZW / SRT321 thermostat device.
+
+ This device currently has no setpoint values.
+ """
+ state = hass.states.get(CLIMATE_MAIN_HEAT_ACTIONNER)
+
+ assert state
+ assert state.state == HVAC_MODE_OFF
+ assert state.attributes[ATTR_HVAC_MODES] == [
+ HVAC_MODE_OFF,
+ HVAC_MODE_HEAT,
+ ]
+ assert state.attributes[ATTR_CURRENT_TEMPERATURE] is None
+ assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0
+
+
+async def test_preset_and_no_setpoint(
+ hass, client, climate_eurotronic_spirit_z, integration
+):
+ """Test preset without setpoint value."""
+ node = climate_eurotronic_spirit_z
+
+ state = hass.states.get(CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY)
+ assert state
+ assert state.state == HVAC_MODE_HEAT
+ assert state.attributes[ATTR_TEMPERATURE] == 22
+
+ # Test setting preset mode Full power
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {
+ ATTR_ENTITY_ID: CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY,
+ ATTR_PRESET_MODE: "Full power",
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 8
+ assert args["valueId"] == {
+ "commandClassName": "Thermostat Mode",
+ "commandClass": 64,
+ "endpoint": 0,
+ "property": "mode",
+ "propertyName": "mode",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": True,
+ "min": 0,
+ "max": 31,
+ "label": "Thermostat mode",
+ "states": {
+ "0": "Off",
+ "1": "Heat",
+ "11": "Energy heat",
+ "15": "Full power",
+ },
+ },
+ "value": 1,
+ }
+ assert args["value"] == 15
+
+ client.async_send_command.reset_mock()
+
+ # Test Full power preset update from value updated event
+ event = Event(
+ type="value updated",
+ data={
+ "source": "node",
+ "event": "value updated",
+ "nodeId": 8,
+ "args": {
+ "commandClassName": "Thermostat Mode",
+ "commandClass": 64,
+ "endpoint": 0,
+ "property": "mode",
+ "propertyName": "mode",
+ "newValue": 15,
+ "prevValue": 1,
+ },
+ },
+ )
+ node.receive_event(event)
+
+ state = hass.states.get(CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY)
+ assert state.state == HVAC_MODE_HEAT
+ assert state.attributes[ATTR_TEMPERATURE] is None
+ assert state.attributes[ATTR_PRESET_MODE] == "Full power"
+
with pytest.raises(ValueError):
+ # Test setting invalid preset mode
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE,
{
- ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
- ATTR_PRESET_MODE: "invalid_mode",
+ ATTR_ENTITY_ID: CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY,
+ ATTR_PRESET_MODE: "invalid_preset",
},
blocking=True,
)
+
+ assert len(client.async_send_command.call_args_list) == 0
+
+ client.async_send_command.reset_mock()
+
+ # Restore hvac mode by setting preset None
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {
+ ATTR_ENTITY_ID: CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY,
+ ATTR_PRESET_MODE: PRESET_NONE,
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 8
+ assert args["valueId"]["commandClass"] == 64
+ assert args["valueId"]["endpoint"] == 0
+ assert args["valueId"]["property"] == "mode"
+ assert args["value"] == 1
+
+ client.async_send_command.reset_mock()
+
+
+async def test_temp_unit_fix(
+ hass,
+ client,
+ climate_radio_thermostat_ct101_multiple_temp_units,
+ climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints,
+ integration,
+):
+ """Test temperaturee unit fix."""
+ state = hass.states.get("climate.thermostat")
+ assert state
+ assert state.attributes["current_temperature"] == 18.3
+
+ state = hass.states.get("climate.z_wave_thermostat")
+ assert state
+ assert state.attributes["current_temperature"] == 21.1
diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py
index 0270383174ee7c..7eea126e52ef06 100644
--- a/tests/components/zwave_js/test_config_flow.py
+++ b/tests/components/zwave_js/test_config_flow.py
@@ -1,13 +1,13 @@
"""Test the Z-Wave JS config flow."""
import asyncio
-from unittest.mock import patch
+from unittest.mock import DEFAULT, patch
import pytest
from zwave_js_server.version import VersionInfo
from homeassistant import config_entries, setup
from homeassistant.components.hassio.handler import HassioAPIError
-from homeassistant.components.zwave_js.config_flow import TITLE
+from homeassistant.components.zwave_js.config_flow import SERVER_VERSION_TIMEOUT, TITLE
from homeassistant.components.zwave_js.const import DOMAIN
from tests.common import MockConfigEntry
@@ -22,86 +22,33 @@
@pytest.fixture(name="supervisor")
def mock_supervisor_fixture():
"""Mock Supervisor."""
- with patch("homeassistant.components.hassio.is_hassio", return_value=True):
- yield
-
-
-@pytest.fixture(name="addon_info_side_effect")
-def addon_info_side_effect_fixture():
- """Return the add-on info side effect."""
- return None
-
-
-@pytest.fixture(name="addon_info")
-def mock_addon_info(addon_info_side_effect):
- """Mock Supervisor add-on info."""
with patch(
- "homeassistant.components.hassio.async_get_addon_info",
- side_effect=addon_info_side_effect,
- ) as addon_info:
- addon_info.return_value = {}
- yield addon_info
-
-
-@pytest.fixture(name="addon_running")
-def mock_addon_running(addon_info):
- """Mock add-on already running."""
- addon_info.return_value["state"] = "started"
- return addon_info
-
-
-@pytest.fixture(name="addon_installed")
-def mock_addon_installed(addon_info):
- """Mock add-on already installed but not running."""
- addon_info.return_value["state"] = "stopped"
- addon_info.return_value["version"] = "1.0"
- return addon_info
-
-
-@pytest.fixture(name="addon_options")
-def mock_addon_options(addon_info):
- """Mock add-on options."""
- addon_info.return_value["options"] = {}
- return addon_info.return_value["options"]
-
-
-@pytest.fixture(name="set_addon_options_side_effect")
-def set_addon_options_side_effect_fixture():
- """Return the set add-on options side effect."""
- return None
-
-
-@pytest.fixture(name="set_addon_options")
-def mock_set_addon_options(set_addon_options_side_effect):
- """Mock set add-on options."""
- with patch(
- "homeassistant.components.hassio.async_set_addon_options",
- side_effect=set_addon_options_side_effect,
- ) as set_options:
- yield set_options
+ "homeassistant.components.zwave_js.config_flow.is_hassio", return_value=True
+ ):
+ yield
-@pytest.fixture(name="install_addon")
-def mock_install_addon():
- """Mock install add-on."""
- with patch("homeassistant.components.hassio.async_install_addon") as install_addon:
- yield install_addon
+@pytest.fixture(name="discovery_info")
+def discovery_info_fixture():
+ """Return the discovery info from the supervisor."""
+ return DEFAULT
-@pytest.fixture(name="start_addon_side_effect")
-def start_addon_side_effect_fixture():
- """Return the set add-on options side effect."""
+@pytest.fixture(name="discovery_info_side_effect")
+def discovery_info_side_effect_fixture():
+ """Return the discovery info from the supervisor."""
return None
-@pytest.fixture(name="start_addon")
-def mock_start_addon(start_addon_side_effect):
- """Mock start add-on."""
+@pytest.fixture(name="get_addon_discovery_info")
+def mock_get_addon_discovery_info(discovery_info, discovery_info_side_effect):
+ """Mock get add-on discovery info."""
with patch(
- "homeassistant.components.hassio.async_start_addon",
- side_effect=start_addon_side_effect,
- ) as start_addon:
- yield start_addon
+ "homeassistant.components.zwave_js.addon.async_get_addon_discovery_info",
+ side_effect=discovery_info_side_effect,
+ return_value=discovery_info,
+ ) as get_addon_discovery_info:
+ yield get_addon_discovery_info
@pytest.fixture(name="server_version_side_effect")
@@ -111,26 +58,37 @@ def server_version_side_effect_fixture():
@pytest.fixture(name="get_server_version", autouse=True)
-def mock_get_server_version(server_version_side_effect):
+def mock_get_server_version(server_version_side_effect, server_version_timeout):
"""Mock server version."""
version_info = VersionInfo(
driver_version="mock-driver-version",
server_version="mock-server-version",
home_id=1234,
+ min_schema_version=0,
+ max_schema_version=1,
)
with patch(
"homeassistant.components.zwave_js.config_flow.get_server_version",
side_effect=server_version_side_effect,
return_value=version_info,
- ) as mock_version:
+ ) as mock_version, patch(
+ "homeassistant.components.zwave_js.config_flow.SERVER_VERSION_TIMEOUT",
+ new=server_version_timeout,
+ ):
yield mock_version
+@pytest.fixture(name="server_version_timeout")
+def mock_server_version_timeout():
+ """Patch the timeout for getting server version."""
+ return SERVER_VERSION_TIMEOUT
+
+
@pytest.fixture(name="addon_setup_time", autouse=True)
def mock_addon_setup_time():
"""Mock add-on setup sleep time."""
with patch(
- "homeassistant.components.zwave_js.config_flow.ADDON_SETUP_TIME", new=0
+ "homeassistant.components.zwave_js.config_flow.ADDON_SETUP_TIMEOUT", new=0
) as addon_setup_time:
yield addon_setup_time
@@ -171,22 +129,30 @@ async def test_manual(hass):
assert result2["result"].unique_id == 1234
+async def slow_server_version(*args):
+ """Simulate a slow server version."""
+ await asyncio.sleep(0.1)
+
+
@pytest.mark.parametrize(
- "url, server_version_side_effect, error",
+ "url, server_version_side_effect, server_version_timeout, error",
[
(
"not-ws-url",
None,
+ SERVER_VERSION_TIMEOUT,
"invalid_ws_url",
),
(
"ws://localhost:3000",
- asyncio.TimeoutError,
+ slow_server_version,
+ 0,
"cannot_connect",
),
(
"ws://localhost:3000",
Exception("Boom"),
+ SERVER_VERSION_TIMEOUT,
"unknown",
),
],
@@ -218,7 +184,16 @@ async def test_manual_errors(
async def test_manual_already_configured(hass):
"""Test that only one unique instance is allowed."""
- entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE, unique_id=1234)
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "url": "ws://localhost:3000",
+ "use_addon": True,
+ "integration_created_addon": True,
+ },
+ title=TITLE,
+ unique_id=1234,
+ )
entry.add_to_hass(hass)
await setup.async_setup_component(hass, "persistent_notification", {})
@@ -232,12 +207,15 @@ async def test_manual_already_configured(hass):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
- "url": "ws://localhost:3000",
+ "url": "ws://1.1.1.1:3001",
},
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
+ assert entry.data["url"] == "ws://1.1.1.1:3001"
+ assert entry.data["use_addon"] is False
+ assert entry.data["integration_created_addon"] is False
@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}])
@@ -399,12 +377,47 @@ async def test_discovery_addon_not_running(
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
- assert result["step_id"] == "start_addon"
assert result["type"] == "form"
+ assert result["step_id"] == "configure_addon"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"usb_path": "/test", "network_key": "abc123"}
+ )
+
+ assert result["type"] == "progress"
+ assert result["step_id"] == "start_addon"
+
+ with patch(
+ "homeassistant.components.zwave_js.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.zwave_js.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ await hass.async_block_till_done()
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ await hass.async_block_till_done()
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == TITLE
+ assert result["data"] == {
+ "url": "ws://host1:3001",
+ "usb_path": "/test",
+ "network_key": "abc123",
+ "use_addon": True,
+ "integration_created_addon": False,
+ }
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
async def test_discovery_addon_not_installed(
- hass, supervisor, addon_installed, install_addon, addon_options
+ hass,
+ supervisor,
+ addon_installed,
+ install_addon,
+ addon_options,
+ set_addon_options,
+ start_addon,
):
"""Test discovery with add-on not installed."""
addon_installed.return_value["version"] = None
@@ -429,8 +442,37 @@ async def test_discovery_addon_not_installed(
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == "form"
+ assert result["step_id"] == "configure_addon"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"usb_path": "/test", "network_key": "abc123"}
+ )
+
+ assert result["type"] == "progress"
assert result["step_id"] == "start_addon"
+ with patch(
+ "homeassistant.components.zwave_js.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.zwave_js.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ await hass.async_block_till_done()
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ await hass.async_block_till_done()
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == TITLE
+ assert result["data"] == {
+ "url": "ws://host1:3001",
+ "usb_path": "/test",
+ "network_key": "abc123",
+ "use_addon": True,
+ "integration_created_addon": True,
+ }
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
async def test_not_addon(hass, supervisor):
"""Test opting out of add-on on Supervisor."""
@@ -544,7 +586,7 @@ async def test_addon_running(
None,
None,
None,
- "addon_missing_discovery_info",
+ "addon_get_discovery_info_failed",
),
(
{"config": ADDON_DISCOVERY_INFO},
@@ -559,10 +601,13 @@ async def test_addon_running_failures(
hass,
supervisor,
addon_running,
+ addon_options,
get_addon_discovery_info,
abort_reason,
):
"""Test all failures when add-on is running."""
+ addon_options["device"] = "/test"
+ addon_options["network_key"] = "abc123"
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
@@ -582,10 +627,21 @@ async def test_addon_running_failures(
@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}])
async def test_addon_running_already_configured(
- hass, supervisor, addon_running, get_addon_discovery_info
+ hass, supervisor, addon_running, addon_options, get_addon_discovery_info
):
"""Test that only one unique instance is allowed when add-on is running."""
- entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE, unique_id=1234)
+ addon_options["device"] = "/test_new"
+ addon_options["network_key"] = "def456"
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "url": "ws://localhost:3000",
+ "usb_path": "/test",
+ "network_key": "abc123",
+ },
+ title=TITLE,
+ unique_id=1234,
+ )
entry.add_to_hass(hass)
await setup.async_setup_component(hass, "persistent_notification", {})
@@ -602,6 +658,9 @@ async def test_addon_running_already_configured(
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
+ assert entry.data["url"] == "ws://host1:3001"
+ assert entry.data["usb_path"] == "/test_new"
+ assert entry.data["network_key"] == "def456"
@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}])
@@ -629,6 +688,13 @@ async def test_addon_installed(
)
assert result["type"] == "form"
+ assert result["step_id"] == "configure_addon"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"usb_path": "/test", "network_key": "abc123"}
+ )
+
+ assert result["type"] == "progress"
assert result["step_id"] == "start_addon"
with patch(
@@ -637,9 +703,8 @@ async def test_addon_installed(
"homeassistant.components.zwave_js.async_setup_entry",
return_value=True,
) as mock_setup_entry:
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"], {"usb_path": "/test", "network_key": "abc123"}
- )
+ await hass.async_block_till_done()
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
await hass.async_block_till_done()
assert result["type"] == "create_entry"
@@ -683,40 +748,32 @@ async def test_addon_installed_start_failure(
)
assert result["type"] == "form"
- assert result["step_id"] == "start_addon"
+ assert result["step_id"] == "configure_addon"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"usb_path": "/test", "network_key": "abc123"}
)
- assert result["type"] == "form"
- assert result["errors"] == {"base": "addon_start_failed"}
+ assert result["type"] == "progress"
+ assert result["step_id"] == "start_addon"
+
+ await hass.async_block_till_done()
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "addon_start_failed"
@pytest.mark.parametrize(
- "set_addon_options_side_effect, start_addon_side_effect, discovery_info, "
- "server_version_side_effect, abort_reason",
+ "discovery_info, server_version_side_effect",
[
(
- HassioAPIError(),
- None,
- {"config": ADDON_DISCOVERY_INFO},
- None,
- "addon_set_config_failed",
- ),
- (
- None,
- None,
{"config": ADDON_DISCOVERY_INFO},
asyncio.TimeoutError,
- "cannot_connect",
),
(
None,
None,
- None,
- None,
- "addon_missing_discovery_info",
),
],
)
@@ -728,7 +785,6 @@ async def test_addon_installed_failures(
set_addon_options,
start_addon,
get_addon_discovery_info,
- abort_reason,
):
"""Test all failures when add-on is installed."""
await setup.async_setup_component(hass, "persistent_notification", {})
@@ -745,14 +801,58 @@ async def test_addon_installed_failures(
)
assert result["type"] == "form"
+ assert result["step_id"] == "configure_addon"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"usb_path": "/test", "network_key": "abc123"}
+ )
+
+ assert result["type"] == "progress"
assert result["step_id"] == "start_addon"
+ await hass.async_block_till_done()
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "addon_start_failed"
+
+
+@pytest.mark.parametrize(
+ "set_addon_options_side_effect, discovery_info",
+ [(HassioAPIError(), {"config": ADDON_DISCOVERY_INFO})],
+)
+async def test_addon_installed_set_options_failure(
+ hass,
+ supervisor,
+ addon_installed,
+ addon_options,
+ set_addon_options,
+ start_addon,
+ get_addon_discovery_info,
+):
+ """Test all failures when add-on is installed."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "on_supervisor"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"use_addon": True}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "configure_addon"
+
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"usb_path": "/test", "network_key": "abc123"}
)
assert result["type"] == "abort"
- assert result["reason"] == abort_reason
+ assert result["reason"] == "addon_set_config_failed"
@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}])
@@ -766,7 +866,16 @@ async def test_addon_installed_already_configured(
get_addon_discovery_info,
):
"""Test that only one unique instance is allowed when add-on is installed."""
- entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE, unique_id=1234)
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ "url": "ws://localhost:3000",
+ "usb_path": "/test",
+ "network_key": "abc123",
+ },
+ title=TITLE,
+ unique_id=1234,
+ )
entry.add_to_hass(hass)
await setup.async_setup_component(hass, "persistent_notification", {})
@@ -782,14 +891,23 @@ async def test_addon_installed_already_configured(
)
assert result["type"] == "form"
- assert result["step_id"] == "start_addon"
+ assert result["step_id"] == "configure_addon"
result = await hass.config_entries.flow.async_configure(
- result["flow_id"], {"usb_path": "/test", "network_key": "abc123"}
+ result["flow_id"], {"usb_path": "/test_new", "network_key": "def456"}
)
+ assert result["type"] == "progress"
+ assert result["step_id"] == "start_addon"
+
+ await hass.async_block_till_done()
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
+ assert entry.data["url"] == "ws://host1:3001"
+ assert entry.data["usb_path"] == "/test_new"
+ assert entry.data["network_key"] == "def456"
@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}])
@@ -819,6 +937,7 @@ async def test_addon_not_installed(
)
assert result["type"] == "progress"
+ assert result["step_id"] == "install_addon"
# Make sure the flow continues when the progress task is done.
await hass.async_block_till_done()
@@ -826,6 +945,13 @@ async def test_addon_not_installed(
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == "form"
+ assert result["step_id"] == "configure_addon"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"usb_path": "/test", "network_key": "abc123"}
+ )
+
+ assert result["type"] == "progress"
assert result["step_id"] == "start_addon"
with patch(
@@ -834,9 +960,8 @@ async def test_addon_not_installed(
"homeassistant.components.zwave_js.async_setup_entry",
return_value=True,
) as mock_setup_entry:
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"], {"usb_path": "/test", "network_key": "abc123"}
- )
+ await hass.async_block_till_done()
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
await hass.async_block_till_done()
assert result["type"] == "create_entry"
diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py
index c327034b61c98a..2378453e31a265 100644
--- a/tests/components/zwave_js/test_cover.py
+++ b/tests/components/zwave_js/test_cover.py
@@ -1,13 +1,28 @@
"""Test the Z-Wave JS cover platform."""
from zwave_js_server.event import Event
-from homeassistant.components.cover import ATTR_CURRENT_POSITION
+from homeassistant.components.cover import (
+ ATTR_CURRENT_POSITION,
+ DEVICE_CLASS_GARAGE,
+ DOMAIN,
+ SERVICE_CLOSE_COVER,
+ SERVICE_OPEN_COVER,
+)
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ STATE_CLOSED,
+ STATE_CLOSING,
+ STATE_OPEN,
+ STATE_OPENING,
+ STATE_UNKNOWN,
+)
-WINDOW_COVER_ENTITY = "cover.zws_12_current_value"
+WINDOW_COVER_ENTITY = "cover.zws_12"
+GDC_COVER_ENTITY = "cover.aeon_labs_garage_door_controller_gen5"
-async def test_cover(hass, client, chain_actuator_zws12, integration):
- """Test the light entity."""
+async def test_window_cover(hass, client, chain_actuator_zws12, integration):
+ """Test the cover entity."""
node = chain_actuator_zws12
state = hass.states.get(WINDOW_COVER_ENTITY)
@@ -107,9 +122,55 @@ async def test_cover(hass, client, chain_actuator_zws12, integration):
"label": "Target value",
},
}
- assert args["value"] == 99
+ assert args["value"]
client.async_send_command.reset_mock()
+ # Test stop after opening
+ await hass.services.async_call(
+ "cover",
+ "stop_cover",
+ {"entity_id": WINDOW_COVER_ENTITY},
+ blocking=True,
+ )
+
+ assert len(client.async_send_command.call_args_list) == 2
+ open_args = client.async_send_command.call_args_list[0][0][0]
+ assert open_args["command"] == "node.set_value"
+ assert open_args["nodeId"] == 6
+ assert open_args["valueId"] == {
+ "commandClassName": "Multilevel Switch",
+ "commandClass": 38,
+ "endpoint": 0,
+ "property": "Open",
+ "propertyName": "Open",
+ "metadata": {
+ "type": "boolean",
+ "readable": True,
+ "writeable": True,
+ "label": "Perform a level change (Open)",
+ "ccSpecific": {"switchType": 3},
+ },
+ }
+ assert not open_args["value"]
+
+ close_args = client.async_send_command.call_args_list[1][0][0]
+ assert close_args["command"] == "node.set_value"
+ assert close_args["nodeId"] == 6
+ assert close_args["valueId"] == {
+ "commandClassName": "Multilevel Switch",
+ "commandClass": 38,
+ "endpoint": 0,
+ "property": "Close",
+ "propertyName": "Close",
+ "metadata": {
+ "type": "boolean",
+ "readable": True,
+ "writeable": True,
+ "label": "Perform a level change (Close)",
+ "ccSpecific": {"switchType": 3},
+ },
+ }
+ assert not close_args["value"]
# Test position update from value updated event
event = Event(
@@ -130,6 +191,7 @@ async def test_cover(hass, client, chain_actuator_zws12, integration):
},
)
node.receive_event(event)
+ client.async_send_command.reset_mock()
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == "open"
@@ -141,7 +203,6 @@ async def test_cover(hass, client, chain_actuator_zws12, integration):
{"entity_id": WINDOW_COVER_ENTITY},
blocking=True,
)
-
assert len(client.async_send_command.call_args_list) == 1
args = client.async_send_command.call_args[0][0]
assert args["command"] == "node.set_value"
@@ -166,6 +227,55 @@ async def test_cover(hass, client, chain_actuator_zws12, integration):
client.async_send_command.reset_mock()
+ # Test stop after closing
+ await hass.services.async_call(
+ "cover",
+ "stop_cover",
+ {"entity_id": WINDOW_COVER_ENTITY},
+ blocking=True,
+ )
+
+ assert len(client.async_send_command.call_args_list) == 2
+ open_args = client.async_send_command.call_args_list[0][0][0]
+ assert open_args["command"] == "node.set_value"
+ assert open_args["nodeId"] == 6
+ assert open_args["valueId"] == {
+ "commandClassName": "Multilevel Switch",
+ "commandClass": 38,
+ "endpoint": 0,
+ "property": "Open",
+ "propertyName": "Open",
+ "metadata": {
+ "type": "boolean",
+ "readable": True,
+ "writeable": True,
+ "label": "Perform a level change (Open)",
+ "ccSpecific": {"switchType": 3},
+ },
+ }
+ assert not open_args["value"]
+
+ close_args = client.async_send_command.call_args_list[1][0][0]
+ assert close_args["command"] == "node.set_value"
+ assert close_args["nodeId"] == 6
+ assert close_args["valueId"] == {
+ "commandClassName": "Multilevel Switch",
+ "commandClass": 38,
+ "endpoint": 0,
+ "property": "Close",
+ "propertyName": "Close",
+ "metadata": {
+ "type": "boolean",
+ "readable": True,
+ "writeable": True,
+ "label": "Perform a level change (Close)",
+ "ccSpecific": {"switchType": 3},
+ },
+ }
+ assert not close_args["value"]
+
+ client.async_send_command.reset_mock()
+
event = Event(
type="value updated",
data={
@@ -187,3 +297,197 @@ async def test_cover(hass, client, chain_actuator_zws12, integration):
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == "closed"
+
+
+async def test_motor_barrier_cover(hass, client, gdc_zw062, integration):
+ """Test the cover entity."""
+ node = gdc_zw062
+
+ state = hass.states.get(GDC_COVER_ENTITY)
+ assert state
+ assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_GARAGE
+
+ assert state.state == STATE_CLOSED
+
+ # Test open
+ await hass.services.async_call(
+ DOMAIN, SERVICE_OPEN_COVER, {"entity_id": GDC_COVER_ENTITY}, blocking=True
+ )
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 12
+ assert args["value"] == 255
+ assert args["valueId"] == {
+ "ccVersion": 0,
+ "commandClass": 102,
+ "commandClassName": "Barrier Operator",
+ "endpoint": 0,
+ "metadata": {
+ "label": "Target Barrier State",
+ "max": 255,
+ "min": 0,
+ "readable": True,
+ "states": {"0": "Closed", "255": "Open"},
+ "type": "number",
+ "writeable": True,
+ },
+ "property": "targetState",
+ "propertyName": "targetState",
+ }
+
+ # state doesn't change until currentState value update is received
+ state = hass.states.get(GDC_COVER_ENTITY)
+ assert state.state == STATE_CLOSED
+
+ client.async_send_command.reset_mock()
+
+ # Test close
+ await hass.services.async_call(
+ DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": GDC_COVER_ENTITY}, blocking=True
+ )
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 12
+ assert args["value"] == 0
+ assert args["valueId"] == {
+ "ccVersion": 0,
+ "commandClass": 102,
+ "commandClassName": "Barrier Operator",
+ "endpoint": 0,
+ "metadata": {
+ "label": "Target Barrier State",
+ "max": 255,
+ "min": 0,
+ "readable": True,
+ "states": {"0": "Closed", "255": "Open"},
+ "type": "number",
+ "writeable": True,
+ },
+ "property": "targetState",
+ "propertyName": "targetState",
+ }
+
+ # state doesn't change until currentState value update is received
+ state = hass.states.get(GDC_COVER_ENTITY)
+ assert state.state == STATE_CLOSED
+
+ client.async_send_command.reset_mock()
+
+ # Barrier sends an opening state
+ event = Event(
+ type="value updated",
+ data={
+ "source": "node",
+ "event": "value updated",
+ "nodeId": 12,
+ "args": {
+ "commandClassName": "Barrier Operator",
+ "commandClass": 102,
+ "endpoint": 0,
+ "property": "currentState",
+ "newValue": 254,
+ "prevValue": 0,
+ "propertyName": "currentState",
+ },
+ },
+ )
+ node.receive_event(event)
+
+ state = hass.states.get(GDC_COVER_ENTITY)
+ assert state.state == STATE_OPENING
+
+ # Barrier sends an opened state
+ event = Event(
+ type="value updated",
+ data={
+ "source": "node",
+ "event": "value updated",
+ "nodeId": 12,
+ "args": {
+ "commandClassName": "Barrier Operator",
+ "commandClass": 102,
+ "endpoint": 0,
+ "property": "currentState",
+ "newValue": 255,
+ "prevValue": 254,
+ "propertyName": "currentState",
+ },
+ },
+ )
+ node.receive_event(event)
+
+ state = hass.states.get(GDC_COVER_ENTITY)
+ assert state.state == STATE_OPEN
+
+ # Barrier sends a closing state
+ event = Event(
+ type="value updated",
+ data={
+ "source": "node",
+ "event": "value updated",
+ "nodeId": 12,
+ "args": {
+ "commandClassName": "Barrier Operator",
+ "commandClass": 102,
+ "endpoint": 0,
+ "property": "currentState",
+ "newValue": 252,
+ "prevValue": 255,
+ "propertyName": "currentState",
+ },
+ },
+ )
+ node.receive_event(event)
+
+ state = hass.states.get(GDC_COVER_ENTITY)
+ assert state.state == STATE_CLOSING
+
+ # Barrier sends a closed state
+ event = Event(
+ type="value updated",
+ data={
+ "source": "node",
+ "event": "value updated",
+ "nodeId": 12,
+ "args": {
+ "commandClassName": "Barrier Operator",
+ "commandClass": 102,
+ "endpoint": 0,
+ "property": "currentState",
+ "newValue": 0,
+ "prevValue": 252,
+ "propertyName": "currentState",
+ },
+ },
+ )
+ node.receive_event(event)
+
+ state = hass.states.get(GDC_COVER_ENTITY)
+ assert state.state == STATE_CLOSED
+
+ # Barrier sends a stopped state
+ event = Event(
+ type="value updated",
+ data={
+ "source": "node",
+ "event": "value updated",
+ "nodeId": 12,
+ "args": {
+ "commandClassName": "Barrier Operator",
+ "commandClass": 102,
+ "endpoint": 0,
+ "property": "currentState",
+ "newValue": 253,
+ "prevValue": 252,
+ "propertyName": "currentState",
+ },
+ },
+ )
+ node.receive_event(event)
+
+ state = hass.states.get(GDC_COVER_ENTITY)
+ assert state.state == STATE_UNKNOWN
diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py
new file mode 100644
index 00000000000000..810ccb8df33ca9
--- /dev/null
+++ b/tests/components/zwave_js/test_discovery.py
@@ -0,0 +1,37 @@
+"""Test discovery of entities for device-specific schemas for the Z-Wave JS integration."""
+
+
+async def test_iblinds_v2(hass, client, iblinds_v2, integration):
+ """Test that an iBlinds v2.0 multilevel switch value is discovered as a cover."""
+ node = iblinds_v2
+ assert node.device_class.specific.label == "Unused"
+
+ state = hass.states.get("light.window_blind_controller")
+ assert not state
+
+ state = hass.states.get("cover.window_blind_controller")
+ assert state
+
+
+async def test_ge_12730(hass, client, ge_12730, integration):
+ """Test GE 12730 Fan Controller v2.0 multilevel switch is discovered as a fan."""
+ node = ge_12730
+ assert node.device_class.specific.label == "Multilevel Power Switch"
+
+ state = hass.states.get("light.in_wall_smart_fan_control")
+ assert not state
+
+ state = hass.states.get("fan.in_wall_smart_fan_control")
+ assert state
+
+
+async def test_inovelli_lzw36(hass, client, inovelli_lzw36, integration):
+ """Test LZW36 Fan Controller multilevel switch endpoint 2 is discovered as a fan."""
+ node = inovelli_lzw36
+ assert node.device_class.specific.label == "Unused"
+
+ state = hass.states.get("light.family_room_combo")
+ assert state.state == "off"
+
+ state = hass.states.get("fan.family_room_combo_2")
+ assert state
diff --git a/tests/components/zwave_js/test_events.py b/tests/components/zwave_js/test_events.py
new file mode 100644
index 00000000000000..15de6aa8887dee
--- /dev/null
+++ b/tests/components/zwave_js/test_events.py
@@ -0,0 +1,196 @@
+"""Test Z-Wave JS (value notification) events."""
+from zwave_js_server.const import CommandClass
+from zwave_js_server.event import Event
+
+from tests.common import async_capture_events
+
+
+async def test_scenes(hass, hank_binary_switch, integration, client):
+ """Test scene events."""
+ # just pick a random node to fake the value notification events
+ node = hank_binary_switch
+ events = async_capture_events(hass, "zwave_js_value_notification")
+
+ # Publish fake Basic Set value notification
+ event = Event(
+ type="value notification",
+ data={
+ "source": "node",
+ "event": "value notification",
+ "nodeId": 32,
+ "args": {
+ "commandClassName": "Basic",
+ "commandClass": 32,
+ "endpoint": 0,
+ "property": "event",
+ "propertyName": "event",
+ "value": 255,
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": False,
+ "min": 0,
+ "max": 255,
+ "label": "Event value",
+ },
+ "ccVersion": 1,
+ },
+ },
+ )
+ node.receive_event(event)
+ # wait for the event
+ await hass.async_block_till_done()
+ assert len(events) == 1
+ assert events[0].data["home_id"] == client.driver.controller.home_id
+ assert events[0].data["node_id"] == 32
+ assert events[0].data["endpoint"] == 0
+ assert events[0].data["command_class"] == 32
+ assert events[0].data["command_class_name"] == "Basic"
+ assert events[0].data["label"] == "Event value"
+ assert events[0].data["value"] == 255
+ assert events[0].data["value_raw"] == 255
+
+ # Publish fake Scene Activation value notification
+ event = Event(
+ type="value notification",
+ data={
+ "source": "node",
+ "event": "value notification",
+ "nodeId": 32,
+ "args": {
+ "commandClassName": "Scene Activation",
+ "commandClass": 43,
+ "endpoint": 0,
+ "property": "SceneID",
+ "propertyName": "SceneID",
+ "value": 16,
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": False,
+ "min": 0,
+ "max": 255,
+ "label": "Scene ID",
+ },
+ "ccVersion": 3,
+ },
+ },
+ )
+ node.receive_event(event)
+ # wait for the event
+ await hass.async_block_till_done()
+ assert len(events) == 2
+ assert events[1].data["command_class"] == 43
+ assert events[1].data["command_class_name"] == "Scene Activation"
+ assert events[1].data["label"] == "Scene ID"
+ assert events[1].data["value"] == 16
+ assert events[1].data["value_raw"] == 16
+
+ # Publish fake Central Scene value notification
+ event = Event(
+ type="value notification",
+ data={
+ "source": "node",
+ "event": "value notification",
+ "nodeId": 32,
+ "args": {
+ "commandClassName": "Central Scene",
+ "commandClass": 91,
+ "endpoint": 0,
+ "property": "scene",
+ "propertyKey": "001",
+ "propertyName": "scene",
+ "propertyKeyName": "001",
+ "value": 4,
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": False,
+ "min": 0,
+ "max": 255,
+ "label": "Scene 001",
+ "states": {
+ "0": "KeyPressed",
+ "1": "KeyReleased",
+ "2": "KeyHeldDown",
+ "3": "KeyPressed2x",
+ "4": "KeyPressed3x",
+ "5": "KeyPressed4x",
+ "6": "KeyPressed5x",
+ },
+ },
+ "ccVersion": 3,
+ },
+ },
+ )
+ node.receive_event(event)
+ # wait for the event
+ await hass.async_block_till_done()
+ assert len(events) == 3
+ assert events[2].data["command_class"] == 91
+ assert events[2].data["command_class_name"] == "Central Scene"
+ assert events[2].data["label"] == "Scene 001"
+ assert events[2].data["value"] == "KeyPressed3x"
+ assert events[2].data["value_raw"] == 4
+
+
+async def test_notifications(hass, hank_binary_switch, integration, client):
+ """Test notification events."""
+ # just pick a random node to fake the value notification events
+ node = hank_binary_switch
+ events = async_capture_events(hass, "zwave_js_notification")
+
+ # Publish fake Notification CC notification
+ event = Event(
+ type="notification",
+ data={
+ "source": "node",
+ "event": "notification",
+ "nodeId": 32,
+ "ccId": 113,
+ "args": {
+ "type": 6,
+ "event": 5,
+ "label": "Access Control",
+ "eventLabel": "Keypad lock operation",
+ "parameters": {"userId": 1},
+ },
+ },
+ )
+ node.receive_event(event)
+ # wait for the event
+ await hass.async_block_till_done()
+ assert len(events) == 1
+ assert events[0].data["home_id"] == client.driver.controller.home_id
+ assert events[0].data["node_id"] == 32
+ assert events[0].data["type"] == 6
+ assert events[0].data["event"] == 5
+ assert events[0].data["label"] == "Access Control"
+ assert events[0].data["event_label"] == "Keypad lock operation"
+ assert events[0].data["parameters"]["userId"] == 1
+ assert events[0].data["command_class"] == CommandClass.NOTIFICATION
+ assert events[0].data["command_class_name"] == "Notification"
+
+ # Publish fake Entry Control CC notification
+ event = Event(
+ type="notification",
+ data={
+ "source": "node",
+ "event": "notification",
+ "nodeId": 32,
+ "ccId": 111,
+ "args": {"eventType": 5, "dataType": 2, "eventData": "555"},
+ },
+ )
+
+ node.receive_event(event)
+ # wait for the event
+ await hass.async_block_till_done()
+ assert len(events) == 2
+ assert events[1].data["home_id"] == client.driver.controller.home_id
+ assert events[1].data["node_id"] == 32
+ assert events[1].data["event_type"] == 5
+ assert events[1].data["data_type"] == 2
+ assert events[1].data["event_data"] == "555"
+ assert events[1].data["command_class"] == CommandClass.ENTRY_CONTROL
+ assert events[1].data["command_class_name"] == "Entry Control"
diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py
index a817a551f9b536..5bd856c664a02c 100644
--- a/tests/components/zwave_js/test_fan.py
+++ b/tests/components/zwave_js/test_fan.py
@@ -4,7 +4,7 @@
from homeassistant.components.fan import ATTR_SPEED, SPEED_MEDIUM
-FAN_ENTITY = "fan.in_wall_smart_fan_control_current_value"
+FAN_ENTITY = "fan.in_wall_smart_fan_control"
async def test_fan(hass, client, in_wall_smart_fan_control, integration):
diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py
index b17945d05c6baa..3e7f79b9cec27e 100644
--- a/tests/components/zwave_js/test_init.py
+++ b/tests/components/zwave_js/test_init.py
@@ -1,21 +1,29 @@
"""Test the Z-Wave JS init module."""
from copy import deepcopy
-from unittest.mock import patch
+from unittest.mock import call, patch
import pytest
+from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion
from zwave_js_server.model.node import Node
from homeassistant.components.hassio.handler import HassioAPIError
from homeassistant.components.zwave_js.const import DOMAIN
+from homeassistant.components.zwave_js.helpers import get_device_id
from homeassistant.config_entries import (
CONN_CLASS_LOCAL_PUSH,
+ DISABLED_USER,
ENTRY_STATE_LOADED,
ENTRY_STATE_NOT_LOADED,
ENTRY_STATE_SETUP_RETRY,
)
from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.helpers import device_registry as dr, entity_registry as er
-from .common import AIR_TEMPERATURE_SENSOR
+from .common import (
+ AIR_TEMPERATURE_SENSOR,
+ EATON_RF9640_ENTITY,
+ NOTIFICATION_MOTION_BINARY_SENSOR,
+)
from tests.common import MockConfigEntry
@@ -27,38 +35,16 @@ def connect_timeout_fixture():
yield timeout
-@pytest.fixture(name="stop_addon")
-def stop_addon_fixture():
- """Mock stop add-on."""
- with patch("homeassistant.components.hassio.async_stop_addon") as stop_addon:
- yield stop_addon
-
-
-@pytest.fixture(name="uninstall_addon")
-def uninstall_addon_fixture():
- """Mock uninstall add-on."""
- with patch(
- "homeassistant.components.hassio.async_uninstall_addon"
- ) as uninstall_addon:
- yield uninstall_addon
-
-
async def test_entry_setup_unload(hass, client, integration):
"""Test the integration set up and unload."""
entry = integration
assert client.connect.call_count == 1
- assert client.register_on_initialized.call_count == 1
- assert client.register_on_disconnect.call_count == 1
- assert client.register_on_connect.call_count == 1
assert entry.state == ENTRY_STATE_LOADED
await hass.config_entries.async_unload(entry.entry_id)
assert client.disconnect.call_count == 1
- assert client.register_on_initialized.return_value.call_count == 1
- assert client.register_on_disconnect.return_value.call_count == 1
- assert client.register_on_connect.return_value.call_count == 1
assert entry.state == ENTRY_STATE_NOT_LOADED
@@ -69,40 +55,28 @@ async def test_home_assistant_stop(hass, client, integration):
assert client.disconnect.call_count == 1
-async def test_availability_reflect_connection_status(
- hass, client, multisensor_6, integration
-):
- """Test we handle disconnect and reconnect."""
- on_initialized = client.register_on_initialized.call_args[0][0]
- on_disconnect = client.register_on_disconnect.call_args[0][0]
- state = hass.states.get(AIR_TEMPERATURE_SENSOR)
-
- assert state
- assert state.state != STATE_UNAVAILABLE
-
- client.connected = False
+async def test_initialized_timeout(hass, client, connect_timeout):
+ """Test we handle a timeout during client initialization."""
+ entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
+ entry.add_to_hass(hass)
- await on_disconnect()
+ await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
- state = hass.states.get(AIR_TEMPERATURE_SENSOR)
-
- assert state
- assert state.state == STATE_UNAVAILABLE
-
- client.connected = True
-
- await on_initialized()
- await hass.async_block_till_done()
+ assert entry.state == ENTRY_STATE_SETUP_RETRY
- state = hass.states.get(AIR_TEMPERATURE_SENSOR)
- assert state
- assert state.state != STATE_UNAVAILABLE
+@pytest.mark.parametrize("error", [BaseZwaveJSServerError("Boom"), Exception("Boom")])
+async def test_listen_failure(hass, client, error):
+ """Test we handle errors during client listen."""
+ async def listen(driver_ready):
+ """Mock the client listen method."""
+ # Set the connect side effect to stop an endless loop on reload.
+ client.connect.side_effect = BaseZwaveJSServerError("Boom")
+ raise error
-async def test_initialized_timeout(hass, client, connect_timeout):
- """Test we handle a timeout during client initialization."""
+ client.listen.side_effect = listen
entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
entry.add_to_hass(hass)
@@ -139,6 +113,299 @@ async def test_on_node_added_ready(
)
+async def test_unique_id_migration_dupes(
+ hass, multisensor_6_state, client, integration
+):
+ """Test we remove an entity when ."""
+ ent_reg = er.async_get(hass)
+
+ entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1]
+
+ # Create entity RegistryEntry using old unique ID format
+ old_unique_id_1 = (
+ f"{client.driver.controller.home_id}.52.52-49-00-Air temperature-00"
+ )
+ entity_entry = ent_reg.async_get_or_create(
+ "sensor",
+ DOMAIN,
+ old_unique_id_1,
+ suggested_object_id=entity_name,
+ config_entry=integration,
+ original_name=entity_name,
+ )
+ assert entity_entry.entity_id == AIR_TEMPERATURE_SENSOR
+ assert entity_entry.unique_id == old_unique_id_1
+
+ # Create entity RegistryEntry using b0 unique ID format
+ old_unique_id_2 = (
+ f"{client.driver.controller.home_id}.52.52-49-0-Air temperature-00-00"
+ )
+ entity_entry = ent_reg.async_get_or_create(
+ "sensor",
+ DOMAIN,
+ old_unique_id_2,
+ suggested_object_id=f"{entity_name}_1",
+ config_entry=integration,
+ original_name=entity_name,
+ )
+ assert entity_entry.entity_id == f"{AIR_TEMPERATURE_SENSOR}_1"
+ assert entity_entry.unique_id == old_unique_id_2
+
+ # Add a ready node, unique ID should be migrated
+ node = Node(client, multisensor_6_state)
+ event = {"node": node}
+
+ client.driver.controller.emit("node added", event)
+ await hass.async_block_till_done()
+
+ # Check that new RegistryEntry is using new unique ID format
+ entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR)
+ new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature"
+ assert entity_entry.unique_id == new_unique_id
+
+ assert ent_reg.async_get(f"{AIR_TEMPERATURE_SENSOR}_1") is None
+
+
+async def test_unique_id_migration_v1(hass, multisensor_6_state, client, integration):
+ """Test unique ID is migrated from old format to new (version 1)."""
+ ent_reg = er.async_get(hass)
+
+ # Migrate version 1
+ entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1]
+
+ # Create entity RegistryEntry using old unique ID format
+ old_unique_id = f"{client.driver.controller.home_id}.52.52-49-00-Air temperature-00"
+ entity_entry = ent_reg.async_get_or_create(
+ "sensor",
+ DOMAIN,
+ old_unique_id,
+ suggested_object_id=entity_name,
+ config_entry=integration,
+ original_name=entity_name,
+ )
+ assert entity_entry.entity_id == AIR_TEMPERATURE_SENSOR
+ assert entity_entry.unique_id == old_unique_id
+
+ # Add a ready node, unique ID should be migrated
+ node = Node(client, multisensor_6_state)
+ event = {"node": node}
+
+ client.driver.controller.emit("node added", event)
+ await hass.async_block_till_done()
+
+ # Check that new RegistryEntry is using new unique ID format
+ entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR)
+ new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature"
+ assert entity_entry.unique_id == new_unique_id
+
+
+async def test_unique_id_migration_v2(hass, multisensor_6_state, client, integration):
+ """Test unique ID is migrated from old format to new (version 2)."""
+ ent_reg = er.async_get(hass)
+ # Migrate version 2
+ ILLUMINANCE_SENSOR = "sensor.multisensor_6_illuminance"
+ entity_name = ILLUMINANCE_SENSOR.split(".")[1]
+
+ # Create entity RegistryEntry using old unique ID format
+ old_unique_id = f"{client.driver.controller.home_id}.52.52-49-0-Illuminance-00-00"
+ entity_entry = ent_reg.async_get_or_create(
+ "sensor",
+ DOMAIN,
+ old_unique_id,
+ suggested_object_id=entity_name,
+ config_entry=integration,
+ original_name=entity_name,
+ )
+ assert entity_entry.entity_id == ILLUMINANCE_SENSOR
+ assert entity_entry.unique_id == old_unique_id
+
+ # Add a ready node, unique ID should be migrated
+ node = Node(client, multisensor_6_state)
+ event = {"node": node}
+
+ client.driver.controller.emit("node added", event)
+ await hass.async_block_till_done()
+
+ # Check that new RegistryEntry is using new unique ID format
+ entity_entry = ent_reg.async_get(ILLUMINANCE_SENSOR)
+ new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance"
+ assert entity_entry.unique_id == new_unique_id
+
+
+async def test_unique_id_migration_v3(hass, multisensor_6_state, client, integration):
+ """Test unique ID is migrated from old format to new (version 3)."""
+ ent_reg = er.async_get(hass)
+ # Migrate version 2
+ ILLUMINANCE_SENSOR = "sensor.multisensor_6_illuminance"
+ entity_name = ILLUMINANCE_SENSOR.split(".")[1]
+
+ # Create entity RegistryEntry using old unique ID format
+ old_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance-00-00"
+ entity_entry = ent_reg.async_get_or_create(
+ "sensor",
+ DOMAIN,
+ old_unique_id,
+ suggested_object_id=entity_name,
+ config_entry=integration,
+ original_name=entity_name,
+ )
+ assert entity_entry.entity_id == ILLUMINANCE_SENSOR
+ assert entity_entry.unique_id == old_unique_id
+
+ # Add a ready node, unique ID should be migrated
+ node = Node(client, multisensor_6_state)
+ event = {"node": node}
+
+ client.driver.controller.emit("node added", event)
+ await hass.async_block_till_done()
+
+ # Check that new RegistryEntry is using new unique ID format
+ entity_entry = ent_reg.async_get(ILLUMINANCE_SENSOR)
+ new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance"
+ assert entity_entry.unique_id == new_unique_id
+
+
+async def test_unique_id_migration_property_key_v1(
+ hass, hank_binary_switch_state, client, integration
+):
+ """Test unique ID with property key is migrated from old format to new (version 1)."""
+ ent_reg = er.async_get(hass)
+
+ SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
+ entity_name = SENSOR_NAME.split(".")[1]
+
+ # Create entity RegistryEntry using old unique ID format
+ old_unique_id = f"{client.driver.controller.home_id}.32.32-50-00-value-W_Consumed"
+ entity_entry = ent_reg.async_get_or_create(
+ "sensor",
+ DOMAIN,
+ old_unique_id,
+ suggested_object_id=entity_name,
+ config_entry=integration,
+ original_name=entity_name,
+ )
+ assert entity_entry.entity_id == SENSOR_NAME
+ assert entity_entry.unique_id == old_unique_id
+
+ # Add a ready node, unique ID should be migrated
+ node = Node(client, hank_binary_switch_state)
+ event = {"node": node}
+
+ client.driver.controller.emit("node added", event)
+ await hass.async_block_till_done()
+
+ # Check that new RegistryEntry is using new unique ID format
+ entity_entry = ent_reg.async_get(SENSOR_NAME)
+ new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049"
+ assert entity_entry.unique_id == new_unique_id
+
+
+async def test_unique_id_migration_property_key_v2(
+ hass, hank_binary_switch_state, client, integration
+):
+ """Test unique ID with property key is migrated from old format to new (version 2)."""
+ ent_reg = er.async_get(hass)
+
+ SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
+ entity_name = SENSOR_NAME.split(".")[1]
+
+ # Create entity RegistryEntry using old unique ID format
+ old_unique_id = (
+ f"{client.driver.controller.home_id}.32.32-50-0-value-66049-W_Consumed"
+ )
+ entity_entry = ent_reg.async_get_or_create(
+ "sensor",
+ DOMAIN,
+ old_unique_id,
+ suggested_object_id=entity_name,
+ config_entry=integration,
+ original_name=entity_name,
+ )
+ assert entity_entry.entity_id == SENSOR_NAME
+ assert entity_entry.unique_id == old_unique_id
+
+ # Add a ready node, unique ID should be migrated
+ node = Node(client, hank_binary_switch_state)
+ event = {"node": node}
+
+ client.driver.controller.emit("node added", event)
+ await hass.async_block_till_done()
+
+ # Check that new RegistryEntry is using new unique ID format
+ entity_entry = ent_reg.async_get(SENSOR_NAME)
+ new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049"
+ assert entity_entry.unique_id == new_unique_id
+
+
+async def test_unique_id_migration_property_key_v3(
+ hass, hank_binary_switch_state, client, integration
+):
+ """Test unique ID with property key is migrated from old format to new (version 3)."""
+ ent_reg = er.async_get(hass)
+
+ SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
+ entity_name = SENSOR_NAME.split(".")[1]
+
+ # Create entity RegistryEntry using old unique ID format
+ old_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049-W_Consumed"
+ entity_entry = ent_reg.async_get_or_create(
+ "sensor",
+ DOMAIN,
+ old_unique_id,
+ suggested_object_id=entity_name,
+ config_entry=integration,
+ original_name=entity_name,
+ )
+ assert entity_entry.entity_id == SENSOR_NAME
+ assert entity_entry.unique_id == old_unique_id
+
+ # Add a ready node, unique ID should be migrated
+ node = Node(client, hank_binary_switch_state)
+ event = {"node": node}
+
+ client.driver.controller.emit("node added", event)
+ await hass.async_block_till_done()
+
+ # Check that new RegistryEntry is using new unique ID format
+ entity_entry = ent_reg.async_get(SENSOR_NAME)
+ new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049"
+ assert entity_entry.unique_id == new_unique_id
+
+
+async def test_unique_id_migration_notification_binary_sensor(
+ hass, multisensor_6_state, client, integration
+):
+ """Test unique ID is migrated from old format to new for a notification binary sensor."""
+ ent_reg = er.async_get(hass)
+
+ entity_name = NOTIFICATION_MOTION_BINARY_SENSOR.split(".")[1]
+
+ # Create entity RegistryEntry using old unique ID format
+ old_unique_id = f"{client.driver.controller.home_id}.52.52-113-00-Home Security-Motion sensor status.8"
+ entity_entry = ent_reg.async_get_or_create(
+ "binary_sensor",
+ DOMAIN,
+ old_unique_id,
+ suggested_object_id=entity_name,
+ config_entry=integration,
+ original_name=entity_name,
+ )
+ assert entity_entry.entity_id == NOTIFICATION_MOTION_BINARY_SENSOR
+ assert entity_entry.unique_id == old_unique_id
+
+ # Add a ready node, unique ID should be migrated
+ node = Node(client, multisensor_6_state)
+ event = {"node": node}
+
+ client.driver.controller.emit("node added", event)
+ await hass.async_block_till_done()
+
+ # Check that new RegistryEntry is using new unique ID format
+ entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR)
+ new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8"
+ assert entity_entry.unique_id == new_unique_id
+
+
async def test_on_node_added_not_ready(
hass, multisensor_6_state, client, integration, device_registry
):
@@ -192,6 +459,12 @@ async def test_existing_node_ready(
)
+async def test_null_name(hass, client, null_name_check, integration):
+ """Test that node without a name gets a generic node name."""
+ node = null_name_check
+ assert hass.states.get(f"switch.node_{node.node_id}")
+
+
async def test_existing_node_not_ready(hass, client, multisensor_6, device_registry):
"""Test we handle a non ready node that exists during integration setup."""
node = multisensor_6
@@ -225,7 +498,203 @@ async def test_existing_node_not_ready(hass, client, multisensor_6, device_regis
)
-async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog):
+async def test_start_addon(
+ hass, addon_installed, install_addon, addon_options, set_addon_options, start_addon
+):
+ """Test start the Z-Wave JS add-on during entry setup."""
+ device = "/test"
+ network_key = "abc123"
+ addon_options = {
+ "device": device,
+ "network_key": network_key,
+ }
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="Z-Wave JS",
+ connection_class=CONN_CLASS_LOCAL_PUSH,
+ data={"use_addon": True, "usb_path": device, "network_key": network_key},
+ )
+ entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert entry.state == ENTRY_STATE_SETUP_RETRY
+ assert install_addon.call_count == 0
+ assert set_addon_options.call_count == 1
+ assert set_addon_options.call_args == call(
+ hass, "core_zwave_js", {"options": addon_options}
+ )
+ assert start_addon.call_count == 1
+ assert start_addon.call_args == call(hass, "core_zwave_js")
+
+
+async def test_install_addon(
+ hass, addon_installed, install_addon, addon_options, set_addon_options, start_addon
+):
+ """Test install and start the Z-Wave JS add-on during entry setup."""
+ addon_installed.return_value["version"] = None
+ device = "/test"
+ network_key = "abc123"
+ addon_options = {
+ "device": device,
+ "network_key": network_key,
+ }
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="Z-Wave JS",
+ connection_class=CONN_CLASS_LOCAL_PUSH,
+ data={"use_addon": True, "usb_path": device, "network_key": network_key},
+ )
+ entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert entry.state == ENTRY_STATE_SETUP_RETRY
+ assert install_addon.call_count == 1
+ assert install_addon.call_args == call(hass, "core_zwave_js")
+ assert set_addon_options.call_count == 1
+ assert set_addon_options.call_args == call(
+ hass, "core_zwave_js", {"options": addon_options}
+ )
+ assert start_addon.call_count == 1
+ assert start_addon.call_args == call(hass, "core_zwave_js")
+
+
+@pytest.mark.parametrize("addon_info_side_effect", [HassioAPIError("Boom")])
+async def test_addon_info_failure(
+ hass,
+ addon_installed,
+ install_addon,
+ addon_options,
+ set_addon_options,
+ start_addon,
+):
+ """Test failure to get add-on info for Z-Wave JS add-on during entry setup."""
+ device = "/test"
+ network_key = "abc123"
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="Z-Wave JS",
+ connection_class=CONN_CLASS_LOCAL_PUSH,
+ data={"use_addon": True, "usb_path": device, "network_key": network_key},
+ )
+ entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert entry.state == ENTRY_STATE_SETUP_RETRY
+ assert install_addon.call_count == 0
+ assert start_addon.call_count == 0
+
+
+@pytest.mark.parametrize(
+ "addon_version, update_available, update_calls, snapshot_calls, "
+ "update_addon_side_effect, create_shapshot_side_effect",
+ [
+ ("1.0", True, 1, 1, None, None),
+ ("1.0", False, 0, 0, None, None),
+ ("1.0", True, 1, 1, HassioAPIError("Boom"), None),
+ ("1.0", True, 0, 1, None, HassioAPIError("Boom")),
+ ],
+)
+async def test_update_addon(
+ hass,
+ client,
+ addon_info,
+ addon_installed,
+ addon_running,
+ create_shapshot,
+ update_addon,
+ addon_options,
+ addon_version,
+ update_available,
+ update_calls,
+ snapshot_calls,
+ update_addon_side_effect,
+ create_shapshot_side_effect,
+):
+ """Test update the Z-Wave JS add-on during entry setup."""
+ addon_info.return_value["version"] = addon_version
+ addon_info.return_value["update_available"] = update_available
+ create_shapshot.side_effect = create_shapshot_side_effect
+ update_addon.side_effect = update_addon_side_effect
+ client.connect.side_effect = InvalidServerVersion("Invalid version")
+ device = "/test"
+ network_key = "abc123"
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="Z-Wave JS",
+ connection_class=CONN_CLASS_LOCAL_PUSH,
+ data={
+ "url": "ws://host1:3001",
+ "use_addon": True,
+ "usb_path": device,
+ "network_key": network_key,
+ },
+ )
+ entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert entry.state == ENTRY_STATE_SETUP_RETRY
+ assert create_shapshot.call_count == snapshot_calls
+ assert update_addon.call_count == update_calls
+
+
+@pytest.mark.parametrize(
+ "stop_addon_side_effect, entry_state",
+ [
+ (None, ENTRY_STATE_NOT_LOADED),
+ (HassioAPIError("Boom"), ENTRY_STATE_LOADED),
+ ],
+)
+async def test_stop_addon(
+ hass,
+ client,
+ addon_installed,
+ addon_running,
+ addon_options,
+ stop_addon,
+ stop_addon_side_effect,
+ entry_state,
+):
+ """Test stop the Z-Wave JS add-on on entry unload if entry is disabled."""
+ stop_addon.side_effect = stop_addon_side_effect
+ device = "/test"
+ network_key = "abc123"
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="Z-Wave JS",
+ connection_class=CONN_CLASS_LOCAL_PUSH,
+ data={
+ "url": "ws://host1:3001",
+ "use_addon": True,
+ "usb_path": device,
+ "network_key": network_key,
+ },
+ )
+ entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert entry.state == ENTRY_STATE_LOADED
+
+ await hass.config_entries.async_set_disabled_by(entry.entry_id, DISABLED_USER)
+ await hass.async_block_till_done()
+
+ assert entry.state == entry_state
+ assert stop_addon.call_count == 1
+ assert stop_addon.call_args == call(hass, "core_zwave_js")
+
+
+async def test_remove_entry(
+ hass, addon_installed, stop_addon, create_shapshot, uninstall_addon, caplog
+):
"""Test remove the config entry."""
# test successful remove without created add-on
entry = MockConfigEntry(
@@ -256,10 +725,19 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog):
await hass.config_entries.async_remove(entry.entry_id)
assert stop_addon.call_count == 1
+ assert stop_addon.call_args == call(hass, "core_zwave_js")
+ assert create_shapshot.call_count == 1
+ assert create_shapshot.call_args == call(
+ hass,
+ {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]},
+ partial=True,
+ )
assert uninstall_addon.call_count == 1
+ assert uninstall_addon.call_args == call(hass, "core_zwave_js")
assert entry.state == ENTRY_STATE_NOT_LOADED
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
stop_addon.reset_mock()
+ create_shapshot.reset_mock()
uninstall_addon.reset_mock()
# test add-on stop failure
@@ -270,12 +748,39 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog):
await hass.config_entries.async_remove(entry.entry_id)
assert stop_addon.call_count == 1
+ assert stop_addon.call_args == call(hass, "core_zwave_js")
+ assert create_shapshot.call_count == 0
assert uninstall_addon.call_count == 0
assert entry.state == ENTRY_STATE_NOT_LOADED
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
assert "Failed to stop the Z-Wave JS add-on" in caplog.text
stop_addon.side_effect = None
stop_addon.reset_mock()
+ create_shapshot.reset_mock()
+ uninstall_addon.reset_mock()
+
+ # test create snapshot failure
+ entry.add_to_hass(hass)
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+ create_shapshot.side_effect = HassioAPIError()
+
+ await hass.config_entries.async_remove(entry.entry_id)
+
+ assert stop_addon.call_count == 1
+ assert stop_addon.call_args == call(hass, "core_zwave_js")
+ assert create_shapshot.call_count == 1
+ assert create_shapshot.call_args == call(
+ hass,
+ {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]},
+ partial=True,
+ )
+ assert uninstall_addon.call_count == 0
+ assert entry.state == ENTRY_STATE_NOT_LOADED
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 0
+ assert "Failed to create a snapshot of the Z-Wave JS add-on" in caplog.text
+ create_shapshot.side_effect = None
+ stop_addon.reset_mock()
+ create_shapshot.reset_mock()
uninstall_addon.reset_mock()
# test add-on uninstall failure
@@ -286,7 +791,60 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog):
await hass.config_entries.async_remove(entry.entry_id)
assert stop_addon.call_count == 1
+ assert stop_addon.call_args == call(hass, "core_zwave_js")
+ assert create_shapshot.call_count == 1
+ assert create_shapshot.call_args == call(
+ hass,
+ {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]},
+ partial=True,
+ )
assert uninstall_addon.call_count == 1
+ assert uninstall_addon.call_args == call(hass, "core_zwave_js")
assert entry.state == ENTRY_STATE_NOT_LOADED
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
assert "Failed to uninstall the Z-Wave JS add-on" in caplog.text
+
+
+async def test_removed_device(hass, client, multiple_devices, integration):
+ """Test that the device registry gets updated when a device gets removed."""
+ nodes = multiple_devices
+
+ # Verify how many nodes are available
+ assert len(client.driver.controller.nodes) == 2
+
+ # Make sure there are the same number of devices
+ dev_reg = dr.async_get(hass)
+ device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id)
+ assert len(device_entries) == 2
+
+ # Check how many entities there are
+ ent_reg = er.async_get(hass)
+ entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id)
+ assert len(entity_entries) == 24
+
+ # Remove a node and reload the entry
+ old_node = nodes.pop(13)
+ await hass.config_entries.async_reload(integration.entry_id)
+ await hass.async_block_till_done()
+
+ # Assert that the node and all of it's entities were removed from the device and
+ # entity registry
+ device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id)
+ assert len(device_entries) == 1
+ entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id)
+ assert len(entity_entries) == 15
+ assert dev_reg.async_get_device({get_device_id(client, old_node)}) is None
+
+
+async def test_suggested_area(hass, client, eaton_rf9640_dimmer):
+ """Test that suggested area works."""
+ dev_reg = dr.async_get(hass)
+ ent_reg = er.async_get(hass)
+
+ entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ entity = ent_reg.async_get(EATON_RF9640_ENTITY)
+ assert dev_reg.async_get(entity.device_id).area_id is not None
diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py
index a1b2318022bab1..9440f5967edd42 100644
--- a/tests/components/zwave_js/test_light.py
+++ b/tests/components/zwave_js/test_light.py
@@ -12,8 +12,11 @@
)
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON
-BULB_6_MULTI_COLOR_LIGHT_ENTITY = "light.bulb_6_multi_color_current_value"
-EATON_RF9640_ENTITY = "light.allloaddimmer_current_value"
+from .common import (
+ AEON_SMART_SWITCH_LIGHT_ENTITY,
+ BULB_6_MULTI_COLOR_LIGHT_ENTITY,
+ EATON_RF9640_ENTITY,
+)
async def test_light(hass, client, bulb_6_multi_color, integration):
@@ -136,58 +139,58 @@ async def test_light(hass, client, bulb_6_multi_color, integration):
blocking=True,
)
- assert len(client.async_send_command.call_args_list) == 5
- warm_args = client.async_send_command.call_args_list[0][0][0] # warm white 0
+ assert len(client.async_send_command.call_args_list) == 6
+ warm_args = client.async_send_command.call_args_list[0][0][0] # red 255
assert warm_args["command"] == "node.set_value"
assert warm_args["nodeId"] == 39
assert warm_args["valueId"]["commandClassName"] == "Color Switch"
assert warm_args["valueId"]["commandClass"] == 51
assert warm_args["valueId"]["endpoint"] == 0
- assert warm_args["valueId"]["metadata"]["label"] == "Target value (Warm White)"
+ assert warm_args["valueId"]["metadata"]["label"] == "Target value (Red)"
assert warm_args["valueId"]["property"] == "targetColor"
assert warm_args["valueId"]["propertyName"] == "targetColor"
- assert warm_args["value"] == 0
+ assert warm_args["value"] == 255
- cold_args = client.async_send_command.call_args_list[1][0][0] # cold white 0
+ cold_args = client.async_send_command.call_args_list[1][0][0] # green 76
assert cold_args["command"] == "node.set_value"
assert cold_args["nodeId"] == 39
assert cold_args["valueId"]["commandClassName"] == "Color Switch"
assert cold_args["valueId"]["commandClass"] == 51
assert cold_args["valueId"]["endpoint"] == 0
- assert cold_args["valueId"]["metadata"]["label"] == "Target value (Cold White)"
+ assert cold_args["valueId"]["metadata"]["label"] == "Target value (Green)"
assert cold_args["valueId"]["property"] == "targetColor"
assert cold_args["valueId"]["propertyName"] == "targetColor"
- assert cold_args["value"] == 0
- red_args = client.async_send_command.call_args_list[2][0][0] # red 255
+ assert cold_args["value"] == 76
+ red_args = client.async_send_command.call_args_list[2][0][0] # blue 255
assert red_args["command"] == "node.set_value"
assert red_args["nodeId"] == 39
assert red_args["valueId"]["commandClassName"] == "Color Switch"
assert red_args["valueId"]["commandClass"] == 51
assert red_args["valueId"]["endpoint"] == 0
- assert red_args["valueId"]["metadata"]["label"] == "Target value (Red)"
+ assert red_args["valueId"]["metadata"]["label"] == "Target value (Blue)"
assert red_args["valueId"]["property"] == "targetColor"
assert red_args["valueId"]["propertyName"] == "targetColor"
assert red_args["value"] == 255
- green_args = client.async_send_command.call_args_list[3][0][0] # green 76
+ green_args = client.async_send_command.call_args_list[3][0][0] # warm white 0
assert green_args["command"] == "node.set_value"
assert green_args["nodeId"] == 39
assert green_args["valueId"]["commandClassName"] == "Color Switch"
assert green_args["valueId"]["commandClass"] == 51
assert green_args["valueId"]["endpoint"] == 0
- assert green_args["valueId"]["metadata"]["label"] == "Target value (Green)"
+ assert green_args["valueId"]["metadata"]["label"] == "Target value (Warm White)"
assert green_args["valueId"]["property"] == "targetColor"
assert green_args["valueId"]["propertyName"] == "targetColor"
- assert green_args["value"] == 76
- blue_args = client.async_send_command.call_args_list[4][0][0] # blue 255
+ assert green_args["value"] == 0
+ blue_args = client.async_send_command.call_args_list[4][0][0] # cold white 0
assert blue_args["command"] == "node.set_value"
assert blue_args["nodeId"] == 39
assert blue_args["valueId"]["commandClassName"] == "Color Switch"
assert blue_args["valueId"]["commandClass"] == 51
assert blue_args["valueId"]["endpoint"] == 0
- assert blue_args["valueId"]["metadata"]["label"] == "Target value (Blue)"
+ assert blue_args["valueId"]["metadata"]["label"] == "Target value (Cold White)"
assert blue_args["valueId"]["property"] == "targetColor"
assert blue_args["valueId"]["propertyName"] == "targetColor"
- assert blue_args["value"] == 255
+ assert blue_args["value"] == 0
# Test rgb color update from value updated event
red_event = Event(
@@ -203,17 +206,21 @@ async def test_light(hass, client, bulb_6_multi_color, integration):
"property": "currentColor",
"newValue": 255,
"prevValue": 0,
+ "propertyKey": 2,
"propertyKeyName": "Red",
},
},
)
green_event = deepcopy(red_event)
- green_event.data["args"].update({"newValue": 76, "propertyKeyName": "Green"})
+ green_event.data["args"].update(
+ {"newValue": 76, "propertyKey": 3, "propertyKeyName": "Green"}
+ )
blue_event = deepcopy(red_event)
+ blue_event.data["args"]["propertyKey"] = 4
blue_event.data["args"]["propertyKeyName"] = "Blue"
warm_white_event = deepcopy(red_event)
warm_white_event.data["args"].update(
- {"newValue": 0, "propertyKeyName": "Warm White"}
+ {"newValue": 0, "propertyKey": 0, "propertyKeyName": "Warm White"}
)
node.receive_event(warm_white_event)
node.receive_event(red_event)
@@ -223,7 +230,6 @@ async def test_light(hass, client, bulb_6_multi_color, integration):
state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY)
assert state.state == STATE_ON
assert state.attributes[ATTR_BRIGHTNESS] == 255
- assert state.attributes[ATTR_COLOR_TEMP] == 370
assert state.attributes[ATTR_RGB_COLOR] == (255, 76, 255)
client.async_send_command.reset_mock()
@@ -236,7 +242,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration):
blocking=True,
)
- assert len(client.async_send_command.call_args_list) == 5
+ assert len(client.async_send_command.call_args_list) == 6
client.async_send_command.reset_mock()
@@ -248,7 +254,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration):
blocking=True,
)
- assert len(client.async_send_command.call_args_list) == 5
+ assert len(client.async_send_command.call_args_list) == 6
red_args = client.async_send_command.call_args_list[0][0][0] # red 0
assert red_args["command"] == "node.set_value"
assert red_args["nodeId"] == 39
@@ -316,23 +322,25 @@ async def test_light(hass, client, bulb_6_multi_color, integration):
"property": "currentColor",
"newValue": 0,
"prevValue": 255,
+ "propertyKey": 2,
"propertyKeyName": "Red",
},
},
)
green_event = deepcopy(red_event)
green_event.data["args"].update(
- {"newValue": 0, "prevValue": 76, "propertyKeyName": "Green"}
+ {"newValue": 0, "prevValue": 76, "propertyKey": 3, "propertyKeyName": "Green"}
)
blue_event = deepcopy(red_event)
+ blue_event.data["args"]["propertyKey"] = 4
blue_event.data["args"]["propertyKeyName"] = "Blue"
warm_white_event = deepcopy(red_event)
warm_white_event.data["args"].update(
- {"newValue": 20, "propertyKeyName": "Warm White"}
+ {"newValue": 20, "propertyKey": 0, "propertyKeyName": "Warm White"}
)
cold_white_event = deepcopy(red_event)
cold_white_event.data["args"].update(
- {"newValue": 235, "propertyKeyName": "Cold White"}
+ {"newValue": 235, "propertyKey": 1, "propertyKeyName": "Cold White"}
)
node.receive_event(red_event)
node.receive_event(green_event)
@@ -354,7 +362,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration):
blocking=True,
)
- assert len(client.async_send_command.call_args_list) == 5
+ assert len(client.async_send_command.call_args_list) == 6
client.async_send_command.reset_mock()
@@ -395,5 +403,11 @@ async def test_v4_dimmer_light(hass, client, eaton_rf9640_dimmer, integration):
assert state
assert state.state == STATE_ON
- # the light should pick targetvalue which has zwave value 20
- assert state.attributes[ATTR_BRIGHTNESS] == 52
+ # the light should pick currentvalue which has zwave value 22
+ assert state.attributes[ATTR_BRIGHTNESS] == 57
+
+
+async def test_optional_light(hass, client, aeon_smart_switch_6, integration):
+ """Test a device that has an additional light endpoint being identified as light."""
+ state = hass.states.get(AEON_SMART_SWITCH_LIGHT_ENTITY)
+ assert state.state == STATE_ON
diff --git a/tests/components/zwave_js/test_lock.py b/tests/components/zwave_js/test_lock.py
index 069b3497a55aaf..9ddc7abdd88eaa 100644
--- a/tests/components/zwave_js/test_lock.py
+++ b/tests/components/zwave_js/test_lock.py
@@ -14,7 +14,7 @@
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED
-SCHLAGE_BE469_LOCK_ENTITY = "lock.touchscreen_deadbolt_current_lock_mode"
+SCHLAGE_BE469_LOCK_ENTITY = "lock.touchscreen_deadbolt"
async def test_door_lock(hass, client, lock_schlage_be469, integration):
diff --git a/tests/components/zwave_js/test_number.py b/tests/components/zwave_js/test_number.py
new file mode 100644
index 00000000000000..b7d83068bea220
--- /dev/null
+++ b/tests/components/zwave_js/test_number.py
@@ -0,0 +1,69 @@
+"""Test the Z-Wave JS number platform."""
+from zwave_js_server.event import Event
+
+NUMBER_ENTITY = "number.thermostat_hvac_valve_control"
+
+
+async def test_number(hass, client, aeotec_radiator_thermostat, integration):
+ """Test the number entity."""
+ node = aeotec_radiator_thermostat
+ state = hass.states.get(NUMBER_ENTITY)
+
+ assert state
+ assert state.state == "75.0"
+
+ # Test turn on setting value
+ await hass.services.async_call(
+ "number",
+ "set_value",
+ {"entity_id": NUMBER_ENTITY, "value": 30},
+ blocking=True,
+ )
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 4
+ assert args["valueId"] == {
+ "commandClassName": "Multilevel Switch",
+ "commandClass": 38,
+ "ccVersion": 1,
+ "endpoint": 0,
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "metadata": {
+ "label": "Target value",
+ "max": 99,
+ "min": 0,
+ "type": "number",
+ "readable": True,
+ "writeable": True,
+ "label": "Target value",
+ },
+ }
+ assert args["value"] == 30.0
+
+ client.async_send_command.reset_mock()
+
+ # Test value update from value updated event
+ event = Event(
+ type="value updated",
+ data={
+ "source": "node",
+ "event": "value updated",
+ "nodeId": 4,
+ "args": {
+ "commandClassName": "Multilevel Switch",
+ "commandClass": 38,
+ "endpoint": 0,
+ "property": "currentValue",
+ "newValue": 99,
+ "prevValue": 0,
+ "propertyName": "currentValue",
+ },
+ },
+ )
+ node.receive_event(event)
+
+ state = hass.states.get(NUMBER_ENTITY)
+ assert state.state == "99.0"
diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py
index 284d2e1a84fa9e..afd3ae1a984ab6 100644
--- a/tests/components/zwave_js/test_sensor.py
+++ b/tests/components/zwave_js/test_sensor.py
@@ -1,14 +1,23 @@
"""Test the Z-Wave JS sensor platform."""
from homeassistant.const import (
DEVICE_CLASS_ENERGY,
+ DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
ENERGY_KILO_WATT_HOUR,
POWER_WATT,
TEMP_CELSIUS,
)
+from homeassistant.helpers import entity_registry as er
-from .common import AIR_TEMPERATURE_SENSOR, ENERGY_SENSOR, POWER_SENSOR
+from .common import (
+ AIR_TEMPERATURE_SENSOR,
+ ENERGY_SENSOR,
+ HUMIDITY_SENSOR,
+ ID_LOCK_CONFIG_PARAMETER_SENSOR,
+ NOTIFICATION_MOTION_SENSOR,
+ POWER_SENSOR,
+)
async def test_numeric_sensor(hass, multisensor_6, integration):
@@ -20,6 +29,13 @@ async def test_numeric_sensor(hass, multisensor_6, integration):
assert state.attributes["unit_of_measurement"] == TEMP_CELSIUS
assert state.attributes["device_class"] == DEVICE_CLASS_TEMPERATURE
+ state = hass.states.get(HUMIDITY_SENSOR)
+
+ assert state
+ assert state.state == "65.0"
+ assert state.attributes["unit_of_measurement"] == "%"
+ assert state.attributes["device_class"] == DEVICE_CLASS_HUMIDITY
+
async def test_energy_sensors(hass, hank_binary_switch, integration):
"""Test power and energy sensors."""
@@ -36,3 +52,36 @@ async def test_energy_sensors(hass, hank_binary_switch, integration):
assert state.state == "0.16"
assert state.attributes["unit_of_measurement"] == ENERGY_KILO_WATT_HOUR
assert state.attributes["device_class"] == DEVICE_CLASS_ENERGY
+
+
+async def test_disabled_notification_sensor(hass, multisensor_6, integration):
+ """Test sensor is created from Notification CC and is disabled."""
+ ent_reg = er.async_get(hass)
+ entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_SENSOR)
+
+ assert entity_entry
+ assert entity_entry.disabled
+ assert entity_entry.disabled_by == er.DISABLED_INTEGRATION
+
+ # Test enabling entity
+ updated_entry = ent_reg.async_update_entity(
+ entity_entry.entity_id, **{"disabled_by": None}
+ )
+ assert updated_entry != entity_entry
+ assert updated_entry.disabled is False
+
+ # reload integration and check if entity is correctly there
+ await hass.config_entries.async_reload(integration.entry_id)
+ await hass.async_block_till_done()
+
+ state = hass.states.get(NOTIFICATION_MOTION_SENSOR)
+ assert state.state == "Motion detection"
+ assert state.attributes["value"] == 8
+
+
+async def test_config_parameter_sensor(hass, lock_id_lock_as_id150, integration):
+ """Test config parameter sensor is created."""
+ ent_reg = er.async_get(hass)
+ entity_entry = ent_reg.async_get(ID_LOCK_CONFIG_PARAMETER_SENSOR)
+ assert entity_entry
+ assert entity_entry.disabled
diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py
new file mode 100644
index 00000000000000..7bdba7894d2f50
--- /dev/null
+++ b/tests/components/zwave_js/test_services.py
@@ -0,0 +1,621 @@
+"""Test the Z-Wave JS services."""
+import pytest
+import voluptuous as vol
+from zwave_js_server.exceptions import SetValueFailed
+
+from homeassistant.components.zwave_js.const import (
+ ATTR_COMMAND_CLASS,
+ ATTR_CONFIG_PARAMETER,
+ ATTR_CONFIG_PARAMETER_BITMASK,
+ ATTR_CONFIG_VALUE,
+ ATTR_PROPERTY,
+ ATTR_REFRESH_ALL_VALUES,
+ ATTR_VALUE,
+ ATTR_WAIT_FOR_RESULT,
+ DOMAIN,
+ SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
+ SERVICE_REFRESH_VALUE,
+ SERVICE_SET_CONFIG_PARAMETER,
+ SERVICE_SET_VALUE,
+)
+from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID
+from homeassistant.helpers.device_registry import (
+ async_entries_for_config_entry,
+ async_get as async_get_dev_reg,
+)
+from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg
+
+from .common import (
+ AIR_TEMPERATURE_SENSOR,
+ CLIMATE_DANFOSS_LC13_ENTITY,
+ CLIMATE_RADIO_THERMOSTAT_ENTITY,
+)
+
+from tests.common import MockConfigEntry
+
+
+async def test_set_config_parameter(hass, client, multisensor_6, integration):
+ """Test the set_config_parameter service."""
+ dev_reg = async_get_dev_reg(hass)
+ ent_reg = async_get_ent_reg(hass)
+ entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR)
+
+ # Test setting config parameter by property and property_key
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_CONFIG_PARAMETER,
+ {
+ ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
+ ATTR_CONFIG_PARAMETER: 102,
+ ATTR_CONFIG_PARAMETER_BITMASK: 1,
+ ATTR_CONFIG_VALUE: 1,
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command_no_wait.call_args_list) == 1
+ args = client.async_send_command_no_wait.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 52
+ assert args["valueId"] == {
+ "commandClassName": "Configuration",
+ "commandClass": 112,
+ "endpoint": 0,
+ "property": 102,
+ "propertyName": "Group 2: Send battery reports",
+ "propertyKey": 1,
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": True,
+ "valueSize": 4,
+ "min": 0,
+ "max": 1,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": True,
+ "label": "Group 2: Send battery reports",
+ "description": "Include battery information in periodic reports to Group 2",
+ "isFromConfig": True,
+ },
+ "value": 0,
+ }
+ assert args["value"] == 1
+
+ client.async_send_command_no_wait.reset_mock()
+
+ # Test setting parameter by property name
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_CONFIG_PARAMETER,
+ {
+ ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
+ ATTR_CONFIG_PARAMETER: "Group 2: Send battery reports",
+ ATTR_CONFIG_VALUE: 1,
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command_no_wait.call_args_list) == 1
+ args = client.async_send_command_no_wait.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 52
+ assert args["valueId"] == {
+ "commandClassName": "Configuration",
+ "commandClass": 112,
+ "endpoint": 0,
+ "property": 102,
+ "propertyName": "Group 2: Send battery reports",
+ "propertyKey": 1,
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": True,
+ "valueSize": 4,
+ "min": 0,
+ "max": 1,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": True,
+ "label": "Group 2: Send battery reports",
+ "description": "Include battery information in periodic reports to Group 2",
+ "isFromConfig": True,
+ },
+ "value": 0,
+ }
+ assert args["value"] == 1
+
+ client.async_send_command_no_wait.reset_mock()
+
+ # Test setting parameter by property name and state label
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_CONFIG_PARAMETER,
+ {
+ ATTR_DEVICE_ID: entity_entry.device_id,
+ ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)",
+ ATTR_CONFIG_VALUE: "Fahrenheit",
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command_no_wait.call_args_list) == 1
+ args = client.async_send_command_no_wait.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 52
+ assert args["valueId"] == {
+ "commandClassName": "Configuration",
+ "commandClass": 112,
+ "endpoint": 0,
+ "property": 41,
+ "propertyName": "Temperature Threshold (Unit)",
+ "propertyKey": 15,
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": True,
+ "valueSize": 3,
+ "min": 1,
+ "max": 2,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": False,
+ "states": {"1": "Celsius", "2": "Fahrenheit"},
+ "label": "Temperature Threshold (Unit)",
+ "isFromConfig": True,
+ },
+ "value": 0,
+ }
+ assert args["value"] == 2
+
+ client.async_send_command_no_wait.reset_mock()
+
+ # Test setting parameter by property and bitmask
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_CONFIG_PARAMETER,
+ {
+ ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
+ ATTR_CONFIG_PARAMETER: 102,
+ ATTR_CONFIG_PARAMETER_BITMASK: "0x01",
+ ATTR_CONFIG_VALUE: 1,
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command_no_wait.call_args_list) == 1
+ args = client.async_send_command_no_wait.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 52
+ assert args["valueId"] == {
+ "commandClassName": "Configuration",
+ "commandClass": 112,
+ "endpoint": 0,
+ "property": 102,
+ "propertyName": "Group 2: Send battery reports",
+ "propertyKey": 1,
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": True,
+ "valueSize": 4,
+ "min": 0,
+ "max": 1,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": True,
+ "label": "Group 2: Send battery reports",
+ "description": "Include battery information in periodic reports to Group 2",
+ "isFromConfig": True,
+ },
+ "value": 0,
+ }
+ assert args["value"] == 1
+
+ # Test that an invalid entity ID raises a ValueError
+ with pytest.raises(ValueError):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_CONFIG_PARAMETER,
+ {
+ ATTR_ENTITY_ID: "sensor.fake_entity",
+ ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)",
+ ATTR_CONFIG_VALUE: "Fahrenheit",
+ },
+ blocking=True,
+ )
+
+ # Test that an invalid device ID raises a ValueError
+ with pytest.raises(ValueError):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_CONFIG_PARAMETER,
+ {
+ ATTR_DEVICE_ID: "fake_device_id",
+ ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)",
+ ATTR_CONFIG_VALUE: "Fahrenheit",
+ },
+ blocking=True,
+ )
+
+ # Test that we can't include a bitmask value if parameter is a string
+ with pytest.raises(vol.Invalid):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_CONFIG_PARAMETER,
+ {
+ ATTR_DEVICE_ID: entity_entry.device_id,
+ ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)",
+ ATTR_CONFIG_PARAMETER_BITMASK: 1,
+ ATTR_CONFIG_VALUE: "Fahrenheit",
+ },
+ blocking=True,
+ )
+
+ non_zwave_js_config_entry = MockConfigEntry(entry_id="fake_entry_id")
+ non_zwave_js_config_entry.add_to_hass(hass)
+ non_zwave_js_device = dev_reg.async_get_or_create(
+ config_entry_id=non_zwave_js_config_entry.entry_id,
+ identifiers={("test", "test")},
+ )
+
+ # Test that a non Z-Wave JS device raises a ValueError
+ with pytest.raises(ValueError):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_CONFIG_PARAMETER,
+ {
+ ATTR_DEVICE_ID: non_zwave_js_device.id,
+ ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)",
+ ATTR_CONFIG_VALUE: "Fahrenheit",
+ },
+ blocking=True,
+ )
+
+ zwave_js_device_with_invalid_node_id = dev_reg.async_get_or_create(
+ config_entry_id=integration.entry_id, identifiers={(DOMAIN, "500-500")}
+ )
+
+ # Test that a Z-Wave JS device with an invalid node ID raises a ValueError
+ with pytest.raises(ValueError):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_CONFIG_PARAMETER,
+ {
+ ATTR_DEVICE_ID: zwave_js_device_with_invalid_node_id.id,
+ ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)",
+ ATTR_CONFIG_VALUE: "Fahrenheit",
+ },
+ blocking=True,
+ )
+
+ non_zwave_js_entity = ent_reg.async_get_or_create(
+ "test",
+ "sensor",
+ "test_sensor",
+ suggested_object_id="test_sensor",
+ config_entry=non_zwave_js_config_entry,
+ )
+
+ # Test that a non Z-Wave JS entity raises a ValueError
+ with pytest.raises(ValueError):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_CONFIG_PARAMETER,
+ {
+ ATTR_ENTITY_ID: non_zwave_js_entity.entity_id,
+ ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)",
+ ATTR_CONFIG_VALUE: "Fahrenheit",
+ },
+ blocking=True,
+ )
+
+ # Test that when a device is awake, we call async_send_command instead of
+ # async_send_command_no_wait
+ multisensor_6.handle_wake_up(None)
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_CONFIG_PARAMETER,
+ {
+ ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
+ ATTR_CONFIG_PARAMETER: 102,
+ ATTR_CONFIG_PARAMETER_BITMASK: 1,
+ ATTR_CONFIG_VALUE: 1,
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 52
+ assert args["valueId"] == {
+ "commandClassName": "Configuration",
+ "commandClass": 112,
+ "endpoint": 0,
+ "property": 102,
+ "propertyName": "Group 2: Send battery reports",
+ "propertyKey": 1,
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": True,
+ "valueSize": 4,
+ "min": 0,
+ "max": 1,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": True,
+ "label": "Group 2: Send battery reports",
+ "description": "Include battery information in periodic reports to Group 2",
+ "isFromConfig": True,
+ },
+ "value": 0,
+ }
+ assert args["value"] == 1
+
+ client.async_send_command.reset_mock()
+
+
+async def test_bulk_set_config_parameters(hass, client, multisensor_6, integration):
+ """Test the bulk_set_partial_config_parameters service."""
+ dev_reg = async_get_dev_reg(hass)
+ device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0]
+ # Test setting config parameter by property and property_key
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
+ {
+ ATTR_DEVICE_ID: device.id,
+ ATTR_CONFIG_PARAMETER: 102,
+ ATTR_CONFIG_VALUE: 241,
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command_no_wait.call_args_list) == 1
+ args = client.async_send_command_no_wait.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 52
+ assert args["valueId"] == {
+ "commandClass": 112,
+ "property": 102,
+ }
+ assert args["value"] == 241
+
+ client.async_send_command_no_wait.reset_mock()
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
+ {
+ ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
+ ATTR_CONFIG_PARAMETER: 102,
+ ATTR_CONFIG_VALUE: {
+ 1: 1,
+ 16: 1,
+ 32: 1,
+ 64: 1,
+ 128: 1,
+ },
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command_no_wait.call_args_list) == 1
+ args = client.async_send_command_no_wait.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 52
+ assert args["valueId"] == {
+ "commandClass": 112,
+ "property": 102,
+ }
+ assert args["value"] == 241
+
+ client.async_send_command_no_wait.reset_mock()
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
+ {
+ ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
+ ATTR_CONFIG_PARAMETER: 102,
+ ATTR_CONFIG_VALUE: {
+ "0x1": 1,
+ "0x10": 1,
+ "0x20": 1,
+ "0x40": 1,
+ "0x80": 1,
+ },
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command_no_wait.call_args_list) == 1
+ args = client.async_send_command_no_wait.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 52
+ assert args["valueId"] == {
+ "commandClass": 112,
+ "property": 102,
+ }
+ assert args["value"] == 241
+
+ client.async_send_command_no_wait.reset_mock()
+
+ # Test that when a device is awake, we call async_send_command instead of
+ # async_send_command_no_wait
+ multisensor_6.handle_wake_up(None)
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
+ {
+ ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR,
+ ATTR_CONFIG_PARAMETER: 102,
+ ATTR_CONFIG_VALUE: {
+ 1: 1,
+ 16: 1,
+ 32: 1,
+ 64: 1,
+ 128: 1,
+ },
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 52
+ assert args["valueId"] == {
+ "commandClass": 112,
+ "property": 102,
+ }
+ assert args["value"] == 241
+
+ client.async_send_command.reset_mock()
+
+
+async def test_poll_value(
+ hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration
+):
+ """Test the poll_value service."""
+ # Test polling the primary value
+ client.async_send_command.return_value = {"result": 2}
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_REFRESH_VALUE,
+ {ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY},
+ blocking=True,
+ )
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "node.poll_value"
+ assert args["nodeId"] == 26
+ assert args["valueId"] == {
+ "commandClassName": "Thermostat Mode",
+ "commandClass": 64,
+ "endpoint": 1,
+ "property": "mode",
+ "propertyName": "mode",
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": True,
+ "min": 0,
+ "max": 255,
+ "label": "Thermostat mode",
+ "states": {
+ "0": "Off",
+ "1": "Heat",
+ "2": "Cool",
+ },
+ },
+ "value": 2,
+ "ccVersion": 0,
+ }
+
+ client.async_send_command.reset_mock()
+
+ # Test polling all watched values
+ client.async_send_command.return_value = {"result": 2}
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_REFRESH_VALUE,
+ {
+ ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
+ ATTR_REFRESH_ALL_VALUES: True,
+ },
+ blocking=True,
+ )
+ assert len(client.async_send_command.call_args_list) == 7
+
+ # Test polling against an invalid entity raises ValueError
+ with pytest.raises(ValueError):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_REFRESH_VALUE,
+ {ATTR_ENTITY_ID: "sensor.fake_entity_id"},
+ blocking=True,
+ )
+
+
+async def test_set_value(hass, client, climate_danfoss_lc_13, integration):
+ """Test set_value service."""
+ dev_reg = async_get_dev_reg(hass)
+ device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0]
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_VALUE,
+ {
+ ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY,
+ ATTR_COMMAND_CLASS: 117,
+ ATTR_PROPERTY: "local",
+ ATTR_VALUE: 2,
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command_no_wait.call_args_list) == 1
+ args = client.async_send_command_no_wait.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 5
+ assert args["valueId"] == {
+ "commandClassName": "Protection",
+ "commandClass": 117,
+ "endpoint": 0,
+ "property": "local",
+ "propertyName": "local",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": True,
+ "label": "Local protection state",
+ "states": {"0": "Unprotected", "2": "NoOperationPossible"},
+ },
+ "value": 0,
+ }
+ assert args["value"] == 2
+
+ client.async_send_command_no_wait.reset_mock()
+
+ # Test that when a command fails we raise an exception
+ client.async_send_command.return_value = {"success": False}
+
+ with pytest.raises(SetValueFailed):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_VALUE,
+ {
+ ATTR_DEVICE_ID: device.id,
+ ATTR_COMMAND_CLASS: 117,
+ ATTR_PROPERTY: "local",
+ ATTR_VALUE: 2,
+ ATTR_WAIT_FOR_RESULT: True,
+ },
+ blocking=True,
+ )
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 5
+ assert args["valueId"] == {
+ "commandClassName": "Protection",
+ "commandClass": 117,
+ "endpoint": 0,
+ "property": "local",
+ "propertyName": "local",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": True,
+ "writeable": True,
+ "label": "Local protection state",
+ "states": {"0": "Unprotected", "2": "NoOperationPossible"},
+ },
+ "value": 0,
+ }
+ assert args["value"] == 2
diff --git a/tests/components/zwave_js/test_switch.py b/tests/components/zwave_js/test_switch.py
index a1d177cc5d8750..ea6e27d9b725f4 100644
--- a/tests/components/zwave_js/test_switch.py
+++ b/tests/components/zwave_js/test_switch.py
@@ -2,6 +2,9 @@
from zwave_js_server.event import Event
+from homeassistant.components.switch import DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON
+from homeassistant.const import STATE_OFF, STATE_ON
+
from .common import SWITCH_ENTITY
@@ -83,3 +86,141 @@ async def test_switch(hass, hank_binary_switch, integration, client):
"value": False,
}
assert args["value"] is False
+
+
+async def test_barrier_signaling_switch(hass, gdc_zw062, integration, client):
+ """Test barrier signaling state switch."""
+ node = gdc_zw062
+ entity = "switch.aeon_labs_garage_door_controller_gen5_signaling_state_visual"
+
+ state = hass.states.get(entity)
+ assert state
+ assert state.state == "on"
+
+ # Test turning off
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_OFF, {"entity_id": entity}, blocking=True
+ )
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 12
+ assert args["value"] == 0
+ assert args["valueId"] == {
+ "ccVersion": 0,
+ "commandClass": 102,
+ "commandClassName": "Barrier Operator",
+ "endpoint": 0,
+ "metadata": {
+ "label": "Signaling State (Visual)",
+ "max": 255,
+ "min": 0,
+ "readable": True,
+ "states": {"0": "Off", "255": "On"},
+ "type": "number",
+ "writeable": True,
+ },
+ "property": "signalingState",
+ "propertyKey": 2,
+ "propertyKeyName": "2",
+ "propertyName": "signalingState",
+ "value": 255,
+ }
+
+ # state change is optimistic and writes state
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity)
+ assert state.state == STATE_OFF
+
+ client.async_send_command.reset_mock()
+
+ # Test turning on
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_ON, {"entity_id": entity}, blocking=True
+ )
+
+ # Note: the valueId's value is still 255 because we never
+ # received an updated value
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 12
+ assert args["value"] == 255
+ assert args["valueId"] == {
+ "ccVersion": 0,
+ "commandClass": 102,
+ "commandClassName": "Barrier Operator",
+ "endpoint": 0,
+ "metadata": {
+ "label": "Signaling State (Visual)",
+ "max": 255,
+ "min": 0,
+ "readable": True,
+ "states": {"0": "Off", "255": "On"},
+ "type": "number",
+ "writeable": True,
+ },
+ "property": "signalingState",
+ "propertyKey": 2,
+ "propertyKeyName": "2",
+ "propertyName": "signalingState",
+ "value": 255,
+ }
+
+ # state change is optimistic and writes state
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity)
+ assert state.state == STATE_ON
+
+ # Received a refresh off
+ event = Event(
+ type="value updated",
+ data={
+ "source": "node",
+ "event": "value updated",
+ "nodeId": 12,
+ "args": {
+ "commandClassName": "Barrier Operator",
+ "commandClass": 102,
+ "endpoint": 0,
+ "property": "signalingState",
+ "propertyKey": 2,
+ "newValue": 0,
+ "prevValue": 0,
+ "propertyName": "signalingState",
+ "propertyKeyName": "2",
+ },
+ },
+ )
+ node.receive_event(event)
+
+ state = hass.states.get(entity)
+ assert state.state == STATE_OFF
+
+ # Received a refresh off
+ event = Event(
+ type="value updated",
+ data={
+ "source": "node",
+ "event": "value updated",
+ "nodeId": 12,
+ "args": {
+ "commandClassName": "Barrier Operator",
+ "commandClass": 102,
+ "endpoint": 0,
+ "property": "signalingState",
+ "propertyKey": 2,
+ "newValue": 255,
+ "prevValue": 255,
+ "propertyName": "signalingState",
+ "propertyKeyName": "2",
+ },
+ },
+ )
+ node.receive_event(event)
+
+ state = hass.states.get(entity)
+ assert state.state == STATE_ON
diff --git a/tests/conftest.py b/tests/conftest.py
index 6e3edbd73e86aa..3fc2dc748cbd17 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -121,7 +121,17 @@ def hass_storage():
@pytest.fixture
-def hass(loop, hass_storage, request):
+def load_registries():
+ """Fixture to control the loading of registries when setting up the hass fixture.
+
+ To avoid loading the registries, tests can be marked with:
+ @pytest.mark.parametrize("load_registries", [False])
+ """
+ return True
+
+
+@pytest.fixture
+def hass(loop, load_registries, hass_storage, request):
"""Fixture to provide a test instance of Home Assistant."""
def exc_handle(loop, context):
@@ -141,7 +151,7 @@ def exc_handle(loop, context):
orig_exception_handler(loop, context)
exceptions = []
- hass = loop.run_until_complete(async_test_home_assistant(loop))
+ hass = loop.run_until_complete(async_test_home_assistant(loop, load_registries))
orig_exception_handler = loop.get_exception_handler()
loop.set_exception_handler(exc_handle)
diff --git a/tests/fixtures/aemet/station-3195-data.json b/tests/fixtures/aemet/station-3195-data.json
new file mode 100644
index 00000000000000..1784a5fb3a42e8
--- /dev/null
+++ b/tests/fixtures/aemet/station-3195-data.json
@@ -0,0 +1,369 @@
+[ {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-08T14:00:00",
+ "prec" : 1.2,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 929.9,
+ "hr" : 97.0,
+ "pres_nmar" : 1009.9,
+ "tamin" : -0.1,
+ "ta" : 0.1,
+ "tamax" : 0.2,
+ "tpr" : -0.3,
+ "rviento" : 132.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-08T15:00:00",
+ "prec" : 1.5,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 929.0,
+ "hr" : 98.0,
+ "pres_nmar" : 1008.9,
+ "tamin" : 0.1,
+ "ta" : 0.2,
+ "tamax" : 0.3,
+ "tpr" : 0.0,
+ "rviento" : 154.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-08T16:00:00",
+ "prec" : 0.7,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 928.8,
+ "hr" : 98.0,
+ "pres_nmar" : 1008.6,
+ "tamin" : 0.2,
+ "ta" : 0.3,
+ "tamax" : 0.3,
+ "tpr" : 0.0,
+ "rviento" : 177.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-08T17:00:00",
+ "prec" : 1.7,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 928.6,
+ "hr" : 99.0,
+ "pres_nmar" : 1008.5,
+ "tamin" : 0.1,
+ "ta" : 0.1,
+ "tamax" : 0.3,
+ "tpr" : 0.0,
+ "rviento" : 174.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-08T18:00:00",
+ "prec" : 1.9,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 928.2,
+ "hr" : 99.0,
+ "pres_nmar" : 1008.1,
+ "tamin" : -0.1,
+ "ta" : -0.1,
+ "tamax" : 0.1,
+ "tpr" : -0.3,
+ "rviento" : 163.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-08T19:00:00",
+ "prec" : 3.0,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 928.4,
+ "hr" : 99.0,
+ "pres_nmar" : 1008.4,
+ "tamin" : -0.3,
+ "ta" : -0.3,
+ "tamax" : 0.0,
+ "tpr" : -0.5,
+ "rviento" : 79.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-08T20:00:00",
+ "prec" : 3.5,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 928.4,
+ "hr" : 99.0,
+ "pres_nmar" : 1008.5,
+ "tamin" : -0.6,
+ "ta" : -0.6,
+ "tamax" : -0.3,
+ "tpr" : -0.7,
+ "rviento" : 0.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-08T21:00:00",
+ "prec" : 2.6,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 928.1,
+ "hr" : 99.0,
+ "pres_nmar" : 1008.2,
+ "tamin" : -0.7,
+ "ta" : -0.7,
+ "tamax" : -0.5,
+ "tpr" : -0.7,
+ "rviento" : 0.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-08T22:00:00",
+ "prec" : 3.0,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 927.6,
+ "hr" : 99.0,
+ "pres_nmar" : 1007.7,
+ "tamin" : -0.8,
+ "ta" : -0.8,
+ "tamax" : -0.7,
+ "tpr" : -1.0,
+ "rviento" : 0.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-08T23:00:00",
+ "prec" : 2.9,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 926.9,
+ "hr" : 99.0,
+ "pres_nmar" : 1007.0,
+ "tamin" : -0.9,
+ "ta" : -0.9,
+ "tamax" : -0.7,
+ "tpr" : -1.0,
+ "rviento" : 0.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-09T00:00:00",
+ "prec" : 1.4,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 926.5,
+ "hr" : 99.0,
+ "pres_nmar" : 1006.6,
+ "tamin" : -1.0,
+ "ta" : -1.0,
+ "tamax" : -0.8,
+ "tpr" : -1.2,
+ "rviento" : 0.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-09T01:00:00",
+ "prec" : 2.0,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 925.9,
+ "hr" : 99.0,
+ "pres_nmar" : 1006.0,
+ "tamin" : -1.3,
+ "ta" : -1.3,
+ "tamax" : -1.0,
+ "tpr" : -1.4,
+ "rviento" : 0.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-09T02:00:00",
+ "prec" : 1.5,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 925.7,
+ "hr" : 99.0,
+ "pres_nmar" : 1005.8,
+ "tamin" : -1.5,
+ "ta" : -1.4,
+ "tamax" : -1.3,
+ "tpr" : -1.4,
+ "rviento" : 0.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-09T03:00:00",
+ "prec" : 1.2,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 925.6,
+ "hr" : 99.0,
+ "pres_nmar" : 1005.7,
+ "tamin" : -1.5,
+ "ta" : -1.4,
+ "tamax" : -1.4,
+ "tpr" : -1.4,
+ "rviento" : 0.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-09T04:00:00",
+ "prec" : 1.1,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 924.9,
+ "hr" : 99.0,
+ "pres_nmar" : 1005.0,
+ "tamin" : -1.5,
+ "ta" : -1.5,
+ "tamax" : -1.4,
+ "tpr" : -1.7,
+ "rviento" : 0.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-09T05:00:00",
+ "prec" : 0.7,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 924.6,
+ "hr" : 99.0,
+ "pres_nmar" : 1004.7,
+ "tamin" : -1.5,
+ "ta" : -1.5,
+ "tamax" : -1.4,
+ "tpr" : -1.7,
+ "rviento" : 0.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-09T06:00:00",
+ "prec" : 0.2,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 924.4,
+ "hr" : 99.0,
+ "pres_nmar" : 1004.5,
+ "tamin" : -1.6,
+ "ta" : -1.6,
+ "tamax" : -1.5,
+ "tpr" : -1.7,
+ "rviento" : 0.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-09T07:00:00",
+ "prec" : 0.0,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 924.4,
+ "hr" : 99.0,
+ "pres_nmar" : 1004.5,
+ "tamin" : -1.6,
+ "ta" : -1.6,
+ "tamax" : -1.6,
+ "tpr" : -1.7,
+ "rviento" : 0.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-09T08:00:00",
+ "prec" : 0.1,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 924.8,
+ "hr" : 99.0,
+ "pres_nmar" : 1004.9,
+ "tamin" : -1.6,
+ "ta" : -1.6,
+ "tamax" : -1.5,
+ "tpr" : -1.7,
+ "rviento" : 0.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-09T09:00:00",
+ "prec" : 0.0,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 925.0,
+ "hr" : 99.0,
+ "pres_nmar" : 1005.0,
+ "tamin" : -1.6,
+ "ta" : -1.3,
+ "tamax" : -1.3,
+ "tpr" : -1.4,
+ "rviento" : 0.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-09T10:00:00",
+ "prec" : 0.0,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 925.3,
+ "hr" : 99.0,
+ "pres_nmar" : 1005.3,
+ "tamin" : -1.3,
+ "ta" : -1.2,
+ "tamax" : -1.1,
+ "tpr" : -1.4,
+ "rviento" : 0.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-09T11:00:00",
+ "prec" : 4.4,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 925.4,
+ "hr" : 99.0,
+ "pres_nmar" : 1005.4,
+ "tamin" : -1.2,
+ "ta" : -1.0,
+ "tamax" : -1.0,
+ "tpr" : -1.2,
+ "rviento" : 0.0
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-09T12:00:00",
+ "prec" : 7.0,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 924.6,
+ "hr" : 99.0,
+ "pres_nmar" : 1004.4,
+ "tamin" : -1.0,
+ "ta" : -0.7,
+ "tamax" : -0.6,
+ "tpr" : -0.7,
+ "rviento" : 0.0
+} ]
diff --git a/tests/fixtures/aemet/station-3195.json b/tests/fixtures/aemet/station-3195.json
new file mode 100644
index 00000000000000..f97df3bea63447
--- /dev/null
+++ b/tests/fixtures/aemet/station-3195.json
@@ -0,0 +1,6 @@
+{
+ "descripcion" : "exito",
+ "estado" : 200,
+ "datos" : "https://opendata.aemet.es/opendata/sh/208c3ca3",
+ "metadatos" : "https://opendata.aemet.es/opendata/sh/55c2971b"
+}
diff --git a/tests/fixtures/aemet/station-list-data.json b/tests/fixtures/aemet/station-list-data.json
new file mode 100644
index 00000000000000..8b35bff6e4aefb
--- /dev/null
+++ b/tests/fixtures/aemet/station-list-data.json
@@ -0,0 +1,42 @@
+[ {
+ "idema" : "3194U",
+ "lon" : -3.724167,
+ "fint" : "2021-01-08T14:00:00",
+ "prec" : 1.3,
+ "alt" : 664.0,
+ "lat" : 40.45167,
+ "ubi" : "MADRID C. UNIVERSITARIA",
+ "hr" : 98.0,
+ "tamin" : 0.6,
+ "ta" : 0.9,
+ "tamax" : 1.0,
+ "tpr" : 0.6
+}, {
+ "idema" : "3194Y",
+ "lon" : -3.813369,
+ "fint" : "2021-01-08T14:00:00",
+ "prec" : 0.2,
+ "alt" : 665.0,
+ "lat" : 40.448437,
+ "ubi" : "POZUELO DE ALARCON (AUTOM�TICA)",
+ "hr" : 93.0,
+ "tamin" : 0.5,
+ "ta" : 0.6,
+ "tamax" : 0.6
+}, {
+ "idema" : "3195",
+ "lon" : -3.678095,
+ "fint" : "2021-01-08T14:00:00",
+ "prec" : 1.2,
+ "alt" : 667.0,
+ "lat" : 40.411804,
+ "ubi" : "MADRID RETIRO",
+ "pres" : 929.9,
+ "hr" : 97.0,
+ "pres_nmar" : 1009.9,
+ "tamin" : -0.1,
+ "ta" : 0.1,
+ "tamax" : 0.2,
+ "tpr" : -0.3,
+ "rviento" : 132.0
+} ]
diff --git a/tests/fixtures/aemet/station-list.json b/tests/fixtures/aemet/station-list.json
new file mode 100644
index 00000000000000..6e0dbc97d6de98
--- /dev/null
+++ b/tests/fixtures/aemet/station-list.json
@@ -0,0 +1,6 @@
+{
+ "descripcion" : "exito",
+ "estado" : 200,
+ "datos" : "https://opendata.aemet.es/opendata/sh/2c55192f",
+ "metadatos" : "https://opendata.aemet.es/opendata/sh/55c2971b"
+}
diff --git a/tests/fixtures/aemet/town-28065-forecast-daily-data.json b/tests/fixtures/aemet/town-28065-forecast-daily-data.json
new file mode 100644
index 00000000000000..77877c72f3a2e5
--- /dev/null
+++ b/tests/fixtures/aemet/town-28065-forecast-daily-data.json
@@ -0,0 +1,625 @@
+[ {
+ "origen" : {
+ "productor" : "Agencia Estatal de Meteorolog�a - AEMET. Gobierno de Espa�a",
+ "web" : "http://www.aemet.es",
+ "enlace" : "http://www.aemet.es/es/eltiempo/prediccion/municipios/getafe-id28065",
+ "language" : "es",
+ "copyright" : "� AEMET. Autorizado el uso de la informaci�n y su reproducci�n citando a AEMET como autora de la misma.",
+ "notaLegal" : "http://www.aemet.es/es/nota_legal"
+ },
+ "elaborado" : "2021-01-09T11:54:00",
+ "nombre" : "Getafe",
+ "provincia" : "Madrid",
+ "prediccion" : {
+ "dia" : [ {
+ "probPrecipitacion" : [ {
+ "value" : 0,
+ "periodo" : "00-24"
+ }, {
+ "value" : 0,
+ "periodo" : "00-12"
+ }, {
+ "value" : 100,
+ "periodo" : "12-24"
+ }, {
+ "value" : 0,
+ "periodo" : "00-06"
+ }, {
+ "value" : 100,
+ "periodo" : "06-12"
+ }, {
+ "value" : 100,
+ "periodo" : "12-18"
+ }, {
+ "value" : 100,
+ "periodo" : "18-24"
+ } ],
+ "cotaNieveProv" : [ {
+ "value" : "",
+ "periodo" : "00-24"
+ }, {
+ "value" : "",
+ "periodo" : "00-12"
+ }, {
+ "value" : "500",
+ "periodo" : "12-24"
+ }, {
+ "value" : "",
+ "periodo" : "00-06"
+ }, {
+ "value" : "400",
+ "periodo" : "06-12"
+ }, {
+ "value" : "500",
+ "periodo" : "12-18"
+ }, {
+ "value" : "600",
+ "periodo" : "18-24"
+ } ],
+ "estadoCielo" : [ {
+ "value" : "",
+ "periodo" : "00-24",
+ "descripcion" : ""
+ }, {
+ "value" : "",
+ "periodo" : "00-12",
+ "descripcion" : ""
+ }, {
+ "value" : "36",
+ "periodo" : "12-24",
+ "descripcion" : "Cubierto con nieve"
+ }, {
+ "value" : "",
+ "periodo" : "00-06",
+ "descripcion" : ""
+ }, {
+ "value" : "36",
+ "periodo" : "06-12",
+ "descripcion" : "Cubierto con nieve"
+ }, {
+ "value" : "36",
+ "periodo" : "12-18",
+ "descripcion" : "Cubierto con nieve"
+ }, {
+ "value" : "34n",
+ "periodo" : "18-24",
+ "descripcion" : "Nuboso con nieve"
+ } ],
+ "viento" : [ {
+ "direccion" : "",
+ "velocidad" : 0,
+ "periodo" : "00-24"
+ }, {
+ "direccion" : "",
+ "velocidad" : 0,
+ "periodo" : "00-12"
+ }, {
+ "direccion" : "E",
+ "velocidad" : 15,
+ "periodo" : "12-24"
+ }, {
+ "direccion" : "NE",
+ "velocidad" : 30,
+ "periodo" : "00-06"
+ }, {
+ "direccion" : "E",
+ "velocidad" : 15,
+ "periodo" : "06-12"
+ }, {
+ "direccion" : "E",
+ "velocidad" : 5,
+ "periodo" : "12-18"
+ }, {
+ "direccion" : "NE",
+ "velocidad" : 5,
+ "periodo" : "18-24"
+ } ],
+ "rachaMax" : [ {
+ "value" : "",
+ "periodo" : "00-24"
+ }, {
+ "value" : "",
+ "periodo" : "00-12"
+ }, {
+ "value" : "",
+ "periodo" : "12-24"
+ }, {
+ "value" : "40",
+ "periodo" : "00-06"
+ }, {
+ "value" : "",
+ "periodo" : "06-12"
+ }, {
+ "value" : "",
+ "periodo" : "12-18"
+ }, {
+ "value" : "",
+ "periodo" : "18-24"
+ } ],
+ "temperatura" : {
+ "maxima" : 2,
+ "minima" : -1,
+ "dato" : [ {
+ "value" : -1,
+ "hora" : 6
+ }, {
+ "value" : 0,
+ "hora" : 12
+ }, {
+ "value" : 1,
+ "hora" : 18
+ }, {
+ "value" : 1,
+ "hora" : 24
+ } ]
+ },
+ "sensTermica" : {
+ "maxima" : 1,
+ "minima" : -9,
+ "dato" : [ {
+ "value" : -1,
+ "hora" : 6
+ }, {
+ "value" : -4,
+ "hora" : 12
+ }, {
+ "value" : 1,
+ "hora" : 18
+ }, {
+ "value" : 1,
+ "hora" : 24
+ } ]
+ },
+ "humedadRelativa" : {
+ "maxima" : 100,
+ "minima" : 75,
+ "dato" : [ {
+ "value" : 100,
+ "hora" : 6
+ }, {
+ "value" : 100,
+ "hora" : 12
+ }, {
+ "value" : 95,
+ "hora" : 18
+ }, {
+ "value" : 75,
+ "hora" : 24
+ } ]
+ },
+ "uvMax" : 1,
+ "fecha" : "2021-01-09T00:00:00"
+ }, {
+ "probPrecipitacion" : [ {
+ "value" : 30,
+ "periodo" : "00-24"
+ }, {
+ "value" : 25,
+ "periodo" : "00-12"
+ }, {
+ "value" : 5,
+ "periodo" : "12-24"
+ }, {
+ "value" : 5,
+ "periodo" : "00-06"
+ }, {
+ "value" : 15,
+ "periodo" : "06-12"
+ }, {
+ "value" : 5,
+ "periodo" : "12-18"
+ }, {
+ "value" : 0,
+ "periodo" : "18-24"
+ } ],
+ "cotaNieveProv" : [ {
+ "value" : "600",
+ "periodo" : "00-24"
+ }, {
+ "value" : "600",
+ "periodo" : "00-12"
+ }, {
+ "value" : "",
+ "periodo" : "12-24"
+ }, {
+ "value" : "",
+ "periodo" : "00-06"
+ }, {
+ "value" : "600",
+ "periodo" : "06-12"
+ }, {
+ "value" : "",
+ "periodo" : "12-18"
+ }, {
+ "value" : "",
+ "periodo" : "18-24"
+ } ],
+ "estadoCielo" : [ {
+ "value" : "13",
+ "periodo" : "00-24",
+ "descripcion" : "Intervalos nubosos"
+ }, {
+ "value" : "15",
+ "periodo" : "00-12",
+ "descripcion" : "Muy nuboso"
+ }, {
+ "value" : "12",
+ "periodo" : "12-24",
+ "descripcion" : "Poco nuboso"
+ }, {
+ "value" : "14n",
+ "periodo" : "00-06",
+ "descripcion" : "Nuboso"
+ }, {
+ "value" : "15",
+ "periodo" : "06-12",
+ "descripcion" : "Muy nuboso"
+ }, {
+ "value" : "12",
+ "periodo" : "12-18",
+ "descripcion" : "Poco nuboso"
+ }, {
+ "value" : "12n",
+ "periodo" : "18-24",
+ "descripcion" : "Poco nuboso"
+ } ],
+ "viento" : [ {
+ "direccion" : "NE",
+ "velocidad" : 20,
+ "periodo" : "00-24"
+ }, {
+ "direccion" : "NE",
+ "velocidad" : 20,
+ "periodo" : "00-12"
+ }, {
+ "direccion" : "NE",
+ "velocidad" : 20,
+ "periodo" : "12-24"
+ }, {
+ "direccion" : "N",
+ "velocidad" : 10,
+ "periodo" : "00-06"
+ }, {
+ "direccion" : "NE",
+ "velocidad" : 20,
+ "periodo" : "06-12"
+ }, {
+ "direccion" : "NE",
+ "velocidad" : 15,
+ "periodo" : "12-18"
+ }, {
+ "direccion" : "NE",
+ "velocidad" : 20,
+ "periodo" : "18-24"
+ } ],
+ "rachaMax" : [ {
+ "value" : "30",
+ "periodo" : "00-24"
+ }, {
+ "value" : "30",
+ "periodo" : "00-12"
+ }, {
+ "value" : "30",
+ "periodo" : "12-24"
+ }, {
+ "value" : "",
+ "periodo" : "00-06"
+ }, {
+ "value" : "30",
+ "periodo" : "06-12"
+ }, {
+ "value" : "",
+ "periodo" : "12-18"
+ }, {
+ "value" : "",
+ "periodo" : "18-24"
+ } ],
+ "temperatura" : {
+ "maxima" : 4,
+ "minima" : -4,
+ "dato" : [ {
+ "value" : -1,
+ "hora" : 6
+ }, {
+ "value" : 3,
+ "hora" : 12
+ }, {
+ "value" : 1,
+ "hora" : 18
+ }, {
+ "value" : -1,
+ "hora" : 24
+ } ]
+ },
+ "sensTermica" : {
+ "maxima" : 1,
+ "minima" : -7,
+ "dato" : [ {
+ "value" : -4,
+ "hora" : 6
+ }, {
+ "value" : -2,
+ "hora" : 12
+ }, {
+ "value" : -4,
+ "hora" : 18
+ }, {
+ "value" : -6,
+ "hora" : 24
+ } ]
+ },
+ "humedadRelativa" : {
+ "maxima" : 100,
+ "minima" : 70,
+ "dato" : [ {
+ "value" : 90,
+ "hora" : 6
+ }, {
+ "value" : 75,
+ "hora" : 12
+ }, {
+ "value" : 80,
+ "hora" : 18
+ }, {
+ "value" : 80,
+ "hora" : 24
+ } ]
+ },
+ "uvMax" : 1,
+ "fecha" : "2021-01-10T00:00:00"
+ }, {
+ "probPrecipitacion" : [ {
+ "value" : 0,
+ "periodo" : "00-24"
+ }, {
+ "value" : 0,
+ "periodo" : "00-12"
+ }, {
+ "value" : 0,
+ "periodo" : "12-24"
+ } ],
+ "cotaNieveProv" : [ {
+ "value" : "",
+ "periodo" : "00-24"
+ }, {
+ "value" : "",
+ "periodo" : "00-12"
+ }, {
+ "value" : "",
+ "periodo" : "12-24"
+ } ],
+ "estadoCielo" : [ {
+ "value" : "12",
+ "periodo" : "00-24",
+ "descripcion" : "Poco nuboso"
+ }, {
+ "value" : "12",
+ "periodo" : "00-12",
+ "descripcion" : "Poco nuboso"
+ }, {
+ "value" : "12",
+ "periodo" : "12-24",
+ "descripcion" : "Poco nuboso"
+ } ],
+ "viento" : [ {
+ "direccion" : "N",
+ "velocidad" : 5,
+ "periodo" : "00-24"
+ }, {
+ "direccion" : "NE",
+ "velocidad" : 20,
+ "periodo" : "00-12"
+ }, {
+ "direccion" : "NO",
+ "velocidad" : 10,
+ "periodo" : "12-24"
+ } ],
+ "rachaMax" : [ {
+ "value" : "",
+ "periodo" : "00-24"
+ }, {
+ "value" : "",
+ "periodo" : "00-12"
+ }, {
+ "value" : "",
+ "periodo" : "12-24"
+ } ],
+ "temperatura" : {
+ "maxima" : 3,
+ "minima" : -7,
+ "dato" : [ ]
+ },
+ "sensTermica" : {
+ "maxima" : 3,
+ "minima" : -8,
+ "dato" : [ ]
+ },
+ "humedadRelativa" : {
+ "maxima" : 85,
+ "minima" : 60,
+ "dato" : [ ]
+ },
+ "uvMax" : 1,
+ "fecha" : "2021-01-11T00:00:00"
+ }, {
+ "probPrecipitacion" : [ {
+ "value" : 0,
+ "periodo" : "00-24"
+ }, {
+ "value" : 0,
+ "periodo" : "00-12"
+ }, {
+ "value" : 0,
+ "periodo" : "12-24"
+ } ],
+ "cotaNieveProv" : [ {
+ "value" : "",
+ "periodo" : "00-24"
+ }, {
+ "value" : "",
+ "periodo" : "00-12"
+ }, {
+ "value" : "",
+ "periodo" : "12-24"
+ } ],
+ "estadoCielo" : [ {
+ "value" : "12",
+ "periodo" : "00-24",
+ "descripcion" : "Poco nuboso"
+ }, {
+ "value" : "12",
+ "periodo" : "00-12",
+ "descripcion" : "Poco nuboso"
+ }, {
+ "value" : "12",
+ "periodo" : "12-24",
+ "descripcion" : "Poco nuboso"
+ } ],
+ "viento" : [ {
+ "direccion" : "C",
+ "velocidad" : 0,
+ "periodo" : "00-24"
+ }, {
+ "direccion" : "E",
+ "velocidad" : 5,
+ "periodo" : "00-12"
+ }, {
+ "direccion" : "C",
+ "velocidad" : 0,
+ "periodo" : "12-24"
+ } ],
+ "rachaMax" : [ {
+ "value" : "",
+ "periodo" : "00-24"
+ }, {
+ "value" : "",
+ "periodo" : "00-12"
+ }, {
+ "value" : "",
+ "periodo" : "12-24"
+ } ],
+ "temperatura" : {
+ "maxima" : -1,
+ "minima" : -13,
+ "dato" : [ ]
+ },
+ "sensTermica" : {
+ "maxima" : -1,
+ "minima" : -13,
+ "dato" : [ ]
+ },
+ "humedadRelativa" : {
+ "maxima" : 100,
+ "minima" : 65,
+ "dato" : [ ]
+ },
+ "uvMax" : 2,
+ "fecha" : "2021-01-12T00:00:00"
+ }, {
+ "probPrecipitacion" : [ {
+ "value" : 0
+ } ],
+ "cotaNieveProv" : [ {
+ "value" : ""
+ } ],
+ "estadoCielo" : [ {
+ "value" : "11",
+ "descripcion" : "Despejado"
+ } ],
+ "viento" : [ {
+ "direccion" : "C",
+ "velocidad" : 0
+ } ],
+ "rachaMax" : [ {
+ "value" : ""
+ } ],
+ "temperatura" : {
+ "maxima" : 6,
+ "minima" : -11,
+ "dato" : [ ]
+ },
+ "sensTermica" : {
+ "maxima" : 6,
+ "minima" : -11,
+ "dato" : [ ]
+ },
+ "humedadRelativa" : {
+ "maxima" : 100,
+ "minima" : 65,
+ "dato" : [ ]
+ },
+ "uvMax" : 2,
+ "fecha" : "2021-01-13T00:00:00"
+ }, {
+ "probPrecipitacion" : [ {
+ "value" : 0
+ } ],
+ "cotaNieveProv" : [ {
+ "value" : ""
+ } ],
+ "estadoCielo" : [ {
+ "value" : "12",
+ "descripcion" : "Poco nuboso"
+ } ],
+ "viento" : [ {
+ "direccion" : "C",
+ "velocidad" : 0
+ } ],
+ "rachaMax" : [ {
+ "value" : ""
+ } ],
+ "temperatura" : {
+ "maxima" : 6,
+ "minima" : -7,
+ "dato" : [ ]
+ },
+ "sensTermica" : {
+ "maxima" : 6,
+ "minima" : -7,
+ "dato" : [ ]
+ },
+ "humedadRelativa" : {
+ "maxima" : 100,
+ "minima" : 80,
+ "dato" : [ ]
+ },
+ "fecha" : "2021-01-14T00:00:00"
+ }, {
+ "probPrecipitacion" : [ {
+ "value" : 0
+ } ],
+ "cotaNieveProv" : [ {
+ "value" : ""
+ } ],
+ "estadoCielo" : [ {
+ "value" : "14",
+ "descripcion" : "Nuboso"
+ } ],
+ "viento" : [ {
+ "direccion" : "C",
+ "velocidad" : 0
+ } ],
+ "rachaMax" : [ {
+ "value" : ""
+ } ],
+ "temperatura" : {
+ "maxima" : 5,
+ "minima" : -4,
+ "dato" : [ ]
+ },
+ "sensTermica" : {
+ "maxima" : 5,
+ "minima" : -4,
+ "dato" : [ ]
+ },
+ "humedadRelativa" : {
+ "maxima" : 100,
+ "minima" : 55,
+ "dato" : [ ]
+ },
+ "fecha" : "2021-01-15T00:00:00"
+ } ]
+ },
+ "id" : 28065,
+ "version" : 1.0
+} ]
diff --git a/tests/fixtures/aemet/town-28065-forecast-daily.json b/tests/fixtures/aemet/town-28065-forecast-daily.json
new file mode 100644
index 00000000000000..35935658c506db
--- /dev/null
+++ b/tests/fixtures/aemet/town-28065-forecast-daily.json
@@ -0,0 +1,6 @@
+{
+ "descripcion" : "exito",
+ "estado" : 200,
+ "datos" : "https://opendata.aemet.es/opendata/sh/64e29abb",
+ "metadatos" : "https://opendata.aemet.es/opendata/sh/dfd88b22"
+}
diff --git a/tests/fixtures/aemet/town-28065-forecast-hourly-data.json b/tests/fixtures/aemet/town-28065-forecast-hourly-data.json
new file mode 100644
index 00000000000000..2bd3a22235a1f2
--- /dev/null
+++ b/tests/fixtures/aemet/town-28065-forecast-hourly-data.json
@@ -0,0 +1,1416 @@
+[ {
+ "origen" : {
+ "productor" : "Agencia Estatal de Meteorolog�a - AEMET. Gobierno de Espa�a",
+ "web" : "http://www.aemet.es",
+ "enlace" : "http://www.aemet.es/es/eltiempo/prediccion/municipios/horas/getafe-id28065",
+ "language" : "es",
+ "copyright" : "� AEMET. Autorizado el uso de la informaci�n y su reproducci�n citando a AEMET como autora de la misma.",
+ "notaLegal" : "http://www.aemet.es/es/nota_legal"
+ },
+ "elaborado" : "2021-01-09T11:47:45",
+ "nombre" : "Getafe",
+ "provincia" : "Madrid",
+ "prediccion" : {
+ "dia" : [ {
+ "estadoCielo" : [ {
+ "value" : "36n",
+ "periodo" : "07",
+ "descripcion" : "Cubierto con nieve"
+ }, {
+ "value" : "36n",
+ "periodo" : "08",
+ "descripcion" : "Cubierto con nieve"
+ }, {
+ "value" : "36",
+ "periodo" : "09",
+ "descripcion" : "Cubierto con nieve"
+ }, {
+ "value" : "36",
+ "periodo" : "10",
+ "descripcion" : "Cubierto con nieve"
+ }, {
+ "value" : "36",
+ "periodo" : "11",
+ "descripcion" : "Cubierto con nieve"
+ }, {
+ "value" : "36",
+ "periodo" : "12",
+ "descripcion" : "Cubierto con nieve"
+ }, {
+ "value" : "36",
+ "periodo" : "13",
+ "descripcion" : "Cubierto con nieve"
+ }, {
+ "value" : "46",
+ "periodo" : "14",
+ "descripcion" : "Cubierto con lluvia escasa"
+ }, {
+ "value" : "46",
+ "periodo" : "15",
+ "descripcion" : "Cubierto con lluvia escasa"
+ }, {
+ "value" : "36",
+ "periodo" : "16",
+ "descripcion" : "Cubierto con nieve"
+ }, {
+ "value" : "36",
+ "periodo" : "17",
+ "descripcion" : "Cubierto con nieve"
+ }, {
+ "value" : "74n",
+ "periodo" : "18",
+ "descripcion" : "Cubierto con nieve escasa"
+ }, {
+ "value" : "46n",
+ "periodo" : "19",
+ "descripcion" : "Cubierto con lluvia escasa"
+ }, {
+ "value" : "46n",
+ "periodo" : "20",
+ "descripcion" : "Cubierto con lluvia escasa"
+ }, {
+ "value" : "16n",
+ "periodo" : "21",
+ "descripcion" : "Cubierto"
+ }, {
+ "value" : "16n",
+ "periodo" : "22",
+ "descripcion" : "Cubierto"
+ }, {
+ "value" : "12n",
+ "periodo" : "23",
+ "descripcion" : "Poco nuboso"
+ } ],
+ "precipitacion" : [ {
+ "value" : "1.4",
+ "periodo" : "07"
+ }, {
+ "value" : "2.1",
+ "periodo" : "08"
+ }, {
+ "value" : "1.9",
+ "periodo" : "09"
+ }, {
+ "value" : "2",
+ "periodo" : "10"
+ }, {
+ "value" : "1.9",
+ "periodo" : "11"
+ }, {
+ "value" : "1.8",
+ "periodo" : "12"
+ }, {
+ "value" : "1.5",
+ "periodo" : "13"
+ }, {
+ "value" : "0.5",
+ "periodo" : "14"
+ }, {
+ "value" : "0.6",
+ "periodo" : "15"
+ }, {
+ "value" : "0.8",
+ "periodo" : "16"
+ }, {
+ "value" : "0.6",
+ "periodo" : "17"
+ }, {
+ "value" : "0.2",
+ "periodo" : "18"
+ }, {
+ "value" : "0.2",
+ "periodo" : "19"
+ }, {
+ "value" : "0.1",
+ "periodo" : "20"
+ }, {
+ "value" : "0",
+ "periodo" : "21"
+ }, {
+ "value" : "0",
+ "periodo" : "22"
+ }, {
+ "value" : "0",
+ "periodo" : "23"
+ } ],
+ "probPrecipitacion" : [ {
+ "value" : "",
+ "periodo" : "0107"
+ }, {
+ "value" : "100",
+ "periodo" : "0713"
+ }, {
+ "value" : "100",
+ "periodo" : "1319"
+ }, {
+ "value" : "100",
+ "periodo" : "1901"
+ } ],
+ "probTormenta" : [ {
+ "value" : "",
+ "periodo" : "0107"
+ }, {
+ "value" : "0",
+ "periodo" : "0713"
+ }, {
+ "value" : "0",
+ "periodo" : "1319"
+ }, {
+ "value" : "0",
+ "periodo" : "1901"
+ } ],
+ "nieve" : [ {
+ "value" : "1.4",
+ "periodo" : "07"
+ }, {
+ "value" : "2.1",
+ "periodo" : "08"
+ }, {
+ "value" : "1.9",
+ "periodo" : "09"
+ }, {
+ "value" : "2",
+ "periodo" : "10"
+ }, {
+ "value" : "1.9",
+ "periodo" : "11"
+ }, {
+ "value" : "1.8",
+ "periodo" : "12"
+ }, {
+ "value" : "1.2",
+ "periodo" : "13"
+ }, {
+ "value" : "0.1",
+ "periodo" : "14"
+ }, {
+ "value" : "0.2",
+ "periodo" : "15"
+ }, {
+ "value" : "0.6",
+ "periodo" : "16"
+ }, {
+ "value" : "0.6",
+ "periodo" : "17"
+ }, {
+ "value" : "0.2",
+ "periodo" : "18"
+ }, {
+ "value" : "0.1",
+ "periodo" : "19"
+ }, {
+ "value" : "0",
+ "periodo" : "20"
+ }, {
+ "value" : "0",
+ "periodo" : "21"
+ }, {
+ "value" : "0",
+ "periodo" : "22"
+ }, {
+ "value" : "0",
+ "periodo" : "23"
+ } ],
+ "probNieve" : [ {
+ "value" : "",
+ "periodo" : "0107"
+ }, {
+ "value" : "100",
+ "periodo" : "0713"
+ }, {
+ "value" : "100",
+ "periodo" : "1319"
+ }, {
+ "value" : "80",
+ "periodo" : "1901"
+ } ],
+ "temperatura" : [ {
+ "value" : "-1",
+ "periodo" : "07"
+ }, {
+ "value" : "-1",
+ "periodo" : "08"
+ }, {
+ "value" : "-1",
+ "periodo" : "09"
+ }, {
+ "value" : "-1",
+ "periodo" : "10"
+ }, {
+ "value" : "-1",
+ "periodo" : "11"
+ }, {
+ "value" : "-0",
+ "periodo" : "12"
+ }, {
+ "value" : "-0",
+ "periodo" : "13"
+ }, {
+ "value" : "0",
+ "periodo" : "14"
+ }, {
+ "value" : "1",
+ "periodo" : "15"
+ }, {
+ "value" : "1",
+ "periodo" : "16"
+ }, {
+ "value" : "1",
+ "periodo" : "17"
+ }, {
+ "value" : "1",
+ "periodo" : "18"
+ }, {
+ "value" : "1",
+ "periodo" : "19"
+ }, {
+ "value" : "1",
+ "periodo" : "20"
+ }, {
+ "value" : "1",
+ "periodo" : "21"
+ }, {
+ "value" : "1",
+ "periodo" : "22"
+ }, {
+ "value" : "1",
+ "periodo" : "23"
+ } ],
+ "sensTermica" : [ {
+ "value" : "-8",
+ "periodo" : "07"
+ }, {
+ "value" : "-7",
+ "periodo" : "08"
+ }, {
+ "value" : "-7",
+ "periodo" : "09"
+ }, {
+ "value" : "-6",
+ "periodo" : "10"
+ }, {
+ "value" : "-6",
+ "periodo" : "11"
+ }, {
+ "value" : "-4",
+ "periodo" : "12"
+ }, {
+ "value" : "-4",
+ "periodo" : "13"
+ }, {
+ "value" : "-4",
+ "periodo" : "14"
+ }, {
+ "value" : "-2",
+ "periodo" : "15"
+ }, {
+ "value" : "-2",
+ "periodo" : "16"
+ }, {
+ "value" : "-2",
+ "periodo" : "17"
+ }, {
+ "value" : "1",
+ "periodo" : "18"
+ }, {
+ "value" : "-2",
+ "periodo" : "19"
+ }, {
+ "value" : "1",
+ "periodo" : "20"
+ }, {
+ "value" : "1",
+ "periodo" : "21"
+ }, {
+ "value" : "1",
+ "periodo" : "22"
+ }, {
+ "value" : "-2",
+ "periodo" : "23"
+ } ],
+ "humedadRelativa" : [ {
+ "value" : "96",
+ "periodo" : "07"
+ }, {
+ "value" : "96",
+ "periodo" : "08"
+ }, {
+ "value" : "99",
+ "periodo" : "09"
+ }, {
+ "value" : "100",
+ "periodo" : "10"
+ }, {
+ "value" : "100",
+ "periodo" : "11"
+ }, {
+ "value" : "100",
+ "periodo" : "12"
+ }, {
+ "value" : "100",
+ "periodo" : "13"
+ }, {
+ "value" : "100",
+ "periodo" : "14"
+ }, {
+ "value" : "100",
+ "periodo" : "15"
+ }, {
+ "value" : "97",
+ "periodo" : "16"
+ }, {
+ "value" : "94",
+ "periodo" : "17"
+ }, {
+ "value" : "93",
+ "periodo" : "18"
+ }, {
+ "value" : "93",
+ "periodo" : "19"
+ }, {
+ "value" : "92",
+ "periodo" : "20"
+ }, {
+ "value" : "89",
+ "periodo" : "21"
+ }, {
+ "value" : "89",
+ "periodo" : "22"
+ }, {
+ "value" : "85",
+ "periodo" : "23"
+ } ],
+ "vientoAndRachaMax" : [ {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "28" ],
+ "periodo" : "07"
+ }, {
+ "value" : "41",
+ "periodo" : "07"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "27" ],
+ "periodo" : "08"
+ }, {
+ "value" : "41",
+ "periodo" : "08"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "25" ],
+ "periodo" : "09"
+ }, {
+ "value" : "39",
+ "periodo" : "09"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "20" ],
+ "periodo" : "10"
+ }, {
+ "value" : "36",
+ "periodo" : "10"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "17" ],
+ "periodo" : "11"
+ }, {
+ "value" : "29",
+ "periodo" : "11"
+ }, {
+ "direccion" : [ "E" ],
+ "velocidad" : [ "15" ],
+ "periodo" : "12"
+ }, {
+ "value" : "24",
+ "periodo" : "12"
+ }, {
+ "direccion" : [ "SE" ],
+ "velocidad" : [ "15" ],
+ "periodo" : "13"
+ }, {
+ "value" : "22",
+ "periodo" : "13"
+ }, {
+ "direccion" : [ "SE" ],
+ "velocidad" : [ "14" ],
+ "periodo" : "14"
+ }, {
+ "value" : "24",
+ "periodo" : "14"
+ }, {
+ "direccion" : [ "SE" ],
+ "velocidad" : [ "10" ],
+ "periodo" : "15"
+ }, {
+ "value" : "20",
+ "periodo" : "15"
+ }, {
+ "direccion" : [ "SE" ],
+ "velocidad" : [ "8" ],
+ "periodo" : "16"
+ }, {
+ "value" : "14",
+ "periodo" : "16"
+ }, {
+ "direccion" : [ "SE" ],
+ "velocidad" : [ "9" ],
+ "periodo" : "17"
+ }, {
+ "value" : "13",
+ "periodo" : "17"
+ }, {
+ "direccion" : [ "E" ],
+ "velocidad" : [ "7" ],
+ "periodo" : "18"
+ }, {
+ "value" : "13",
+ "periodo" : "18"
+ }, {
+ "direccion" : [ "SE" ],
+ "velocidad" : [ "8" ],
+ "periodo" : "19"
+ }, {
+ "value" : "12",
+ "periodo" : "19"
+ }, {
+ "direccion" : [ "SE" ],
+ "velocidad" : [ "6" ],
+ "periodo" : "20"
+ }, {
+ "value" : "12",
+ "periodo" : "20"
+ }, {
+ "direccion" : [ "E" ],
+ "velocidad" : [ "6" ],
+ "periodo" : "21"
+ }, {
+ "value" : "8",
+ "periodo" : "21"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "6" ],
+ "periodo" : "22"
+ }, {
+ "value" : "9",
+ "periodo" : "22"
+ }, {
+ "direccion" : [ "E" ],
+ "velocidad" : [ "8" ],
+ "periodo" : "23"
+ }, {
+ "value" : "11",
+ "periodo" : "23"
+ } ],
+ "fecha" : "2021-01-09T00:00:00",
+ "orto" : "08:37",
+ "ocaso" : "18:07"
+ }, {
+ "estadoCielo" : [ {
+ "value" : "12n",
+ "periodo" : "00",
+ "descripcion" : "Poco nuboso"
+ }, {
+ "value" : "81n",
+ "periodo" : "01",
+ "descripcion" : "Niebla"
+ }, {
+ "value" : "81n",
+ "periodo" : "02",
+ "descripcion" : "Niebla"
+ }, {
+ "value" : "81n",
+ "periodo" : "03",
+ "descripcion" : "Niebla"
+ }, {
+ "value" : "17n",
+ "periodo" : "04",
+ "descripcion" : "Nubes altas"
+ }, {
+ "value" : "16n",
+ "periodo" : "05",
+ "descripcion" : "Cubierto"
+ }, {
+ "value" : "16n",
+ "periodo" : "06",
+ "descripcion" : "Cubierto"
+ }, {
+ "value" : "16n",
+ "periodo" : "07",
+ "descripcion" : "Cubierto"
+ }, {
+ "value" : "16n",
+ "periodo" : "08",
+ "descripcion" : "Cubierto"
+ }, {
+ "value" : "14",
+ "periodo" : "09",
+ "descripcion" : "Nuboso"
+ }, {
+ "value" : "12",
+ "periodo" : "10",
+ "descripcion" : "Poco nuboso"
+ }, {
+ "value" : "12",
+ "periodo" : "11",
+ "descripcion" : "Poco nuboso"
+ }, {
+ "value" : "17",
+ "periodo" : "12",
+ "descripcion" : "Nubes altas"
+ }, {
+ "value" : "17",
+ "periodo" : "13",
+ "descripcion" : "Nubes altas"
+ }, {
+ "value" : "17",
+ "periodo" : "14",
+ "descripcion" : "Nubes altas"
+ }, {
+ "value" : "17",
+ "periodo" : "15",
+ "descripcion" : "Nubes altas"
+ }, {
+ "value" : "17",
+ "periodo" : "16",
+ "descripcion" : "Nubes altas"
+ }, {
+ "value" : "17",
+ "periodo" : "17",
+ "descripcion" : "Nubes altas"
+ }, {
+ "value" : "12n",
+ "periodo" : "18",
+ "descripcion" : "Poco nuboso"
+ }, {
+ "value" : "12n",
+ "periodo" : "19",
+ "descripcion" : "Poco nuboso"
+ }, {
+ "value" : "14n",
+ "periodo" : "20",
+ "descripcion" : "Nuboso"
+ }, {
+ "value" : "16n",
+ "periodo" : "21",
+ "descripcion" : "Cubierto"
+ }, {
+ "value" : "16n",
+ "periodo" : "22",
+ "descripcion" : "Cubierto"
+ }, {
+ "value" : "15n",
+ "periodo" : "23",
+ "descripcion" : "Muy nuboso"
+ } ],
+ "precipitacion" : [ {
+ "value" : "0",
+ "periodo" : "00"
+ }, {
+ "value" : "0",
+ "periodo" : "01"
+ }, {
+ "value" : "0",
+ "periodo" : "02"
+ }, {
+ "value" : "0",
+ "periodo" : "03"
+ }, {
+ "value" : "0",
+ "periodo" : "04"
+ }, {
+ "value" : "0",
+ "periodo" : "05"
+ }, {
+ "value" : "0",
+ "periodo" : "06"
+ }, {
+ "value" : "0",
+ "periodo" : "07"
+ }, {
+ "value" : "0",
+ "periodo" : "08"
+ }, {
+ "value" : "Ip",
+ "periodo" : "09"
+ }, {
+ "value" : "0",
+ "periodo" : "10"
+ }, {
+ "value" : "0",
+ "periodo" : "11"
+ }, {
+ "value" : "0",
+ "periodo" : "12"
+ }, {
+ "value" : "0",
+ "periodo" : "13"
+ }, {
+ "value" : "0",
+ "periodo" : "14"
+ }, {
+ "value" : "0",
+ "periodo" : "15"
+ }, {
+ "value" : "0",
+ "periodo" : "16"
+ }, {
+ "value" : "0",
+ "periodo" : "17"
+ }, {
+ "value" : "0",
+ "periodo" : "18"
+ }, {
+ "value" : "0",
+ "periodo" : "19"
+ }, {
+ "value" : "0",
+ "periodo" : "20"
+ }, {
+ "value" : "0",
+ "periodo" : "21"
+ }, {
+ "value" : "0",
+ "periodo" : "22"
+ }, {
+ "value" : "0",
+ "periodo" : "23"
+ } ],
+ "probPrecipitacion" : [ {
+ "value" : "10",
+ "periodo" : "0107"
+ }, {
+ "value" : "15",
+ "periodo" : "0713"
+ }, {
+ "value" : "5",
+ "periodo" : "1319"
+ }, {
+ "value" : "0",
+ "periodo" : "1901"
+ } ],
+ "probTormenta" : [ {
+ "value" : "0",
+ "periodo" : "0107"
+ }, {
+ "value" : "0",
+ "periodo" : "0713"
+ }, {
+ "value" : "0",
+ "periodo" : "1319"
+ }, {
+ "value" : "0",
+ "periodo" : "1901"
+ } ],
+ "nieve" : [ {
+ "value" : "0",
+ "periodo" : "00"
+ }, {
+ "value" : "0",
+ "periodo" : "01"
+ }, {
+ "value" : "0",
+ "periodo" : "02"
+ }, {
+ "value" : "0",
+ "periodo" : "03"
+ }, {
+ "value" : "0",
+ "periodo" : "04"
+ }, {
+ "value" : "0",
+ "periodo" : "05"
+ }, {
+ "value" : "0",
+ "periodo" : "06"
+ }, {
+ "value" : "0",
+ "periodo" : "07"
+ }, {
+ "value" : "0",
+ "periodo" : "08"
+ }, {
+ "value" : "Ip",
+ "periodo" : "09"
+ }, {
+ "value" : "0",
+ "periodo" : "10"
+ }, {
+ "value" : "0",
+ "periodo" : "11"
+ }, {
+ "value" : "0",
+ "periodo" : "12"
+ }, {
+ "value" : "0",
+ "periodo" : "13"
+ }, {
+ "value" : "0",
+ "periodo" : "14"
+ }, {
+ "value" : "0",
+ "periodo" : "15"
+ }, {
+ "value" : "0",
+ "periodo" : "16"
+ }, {
+ "value" : "0",
+ "periodo" : "17"
+ }, {
+ "value" : "0",
+ "periodo" : "18"
+ }, {
+ "value" : "0",
+ "periodo" : "19"
+ }, {
+ "value" : "0",
+ "periodo" : "20"
+ }, {
+ "value" : "0",
+ "periodo" : "21"
+ }, {
+ "value" : "0",
+ "periodo" : "22"
+ }, {
+ "value" : "0",
+ "periodo" : "23"
+ } ],
+ "probNieve" : [ {
+ "value" : "10",
+ "periodo" : "0107"
+ }, {
+ "value" : "10",
+ "periodo" : "0713"
+ }, {
+ "value" : "0",
+ "periodo" : "1319"
+ }, {
+ "value" : "0",
+ "periodo" : "1901"
+ } ],
+ "temperatura" : [ {
+ "value" : "1",
+ "periodo" : "00"
+ }, {
+ "value" : "0",
+ "periodo" : "01"
+ }, {
+ "value" : "-0",
+ "periodo" : "02"
+ }, {
+ "value" : "-0",
+ "periodo" : "03"
+ }, {
+ "value" : "-1",
+ "periodo" : "04"
+ }, {
+ "value" : "-1",
+ "periodo" : "05"
+ }, {
+ "value" : "-1",
+ "periodo" : "06"
+ }, {
+ "value" : "-2",
+ "periodo" : "07"
+ }, {
+ "value" : "-1",
+ "periodo" : "08"
+ }, {
+ "value" : "-1",
+ "periodo" : "09"
+ }, {
+ "value" : "0",
+ "periodo" : "10"
+ }, {
+ "value" : "2",
+ "periodo" : "11"
+ }, {
+ "value" : "3",
+ "periodo" : "12"
+ }, {
+ "value" : "3",
+ "periodo" : "13"
+ }, {
+ "value" : "3",
+ "periodo" : "14"
+ }, {
+ "value" : "4",
+ "periodo" : "15"
+ }, {
+ "value" : "3",
+ "periodo" : "16"
+ }, {
+ "value" : "2",
+ "periodo" : "17"
+ }, {
+ "value" : "1",
+ "periodo" : "18"
+ }, {
+ "value" : "1",
+ "periodo" : "19"
+ }, {
+ "value" : "1",
+ "periodo" : "20"
+ }, {
+ "value" : "1",
+ "periodo" : "21"
+ }, {
+ "value" : "0",
+ "periodo" : "22"
+ }, {
+ "value" : "-0",
+ "periodo" : "23"
+ } ],
+ "sensTermica" : [ {
+ "value" : "1",
+ "periodo" : "00"
+ }, {
+ "value" : "0",
+ "periodo" : "01"
+ }, {
+ "value" : "-0",
+ "periodo" : "02"
+ }, {
+ "value" : "-0",
+ "periodo" : "03"
+ }, {
+ "value" : "-4",
+ "periodo" : "04"
+ }, {
+ "value" : "-1",
+ "periodo" : "05"
+ }, {
+ "value" : "-4",
+ "periodo" : "06"
+ }, {
+ "value" : "-6",
+ "periodo" : "07"
+ }, {
+ "value" : "-6",
+ "periodo" : "08"
+ }, {
+ "value" : "-7",
+ "periodo" : "09"
+ }, {
+ "value" : "-5",
+ "periodo" : "10"
+ }, {
+ "value" : "-3",
+ "periodo" : "11"
+ }, {
+ "value" : "-2",
+ "periodo" : "12"
+ }, {
+ "value" : "-1",
+ "periodo" : "13"
+ }, {
+ "value" : "-1",
+ "periodo" : "14"
+ }, {
+ "value" : "0",
+ "periodo" : "15"
+ }, {
+ "value" : "-1",
+ "periodo" : "16"
+ }, {
+ "value" : "-2",
+ "periodo" : "17"
+ }, {
+ "value" : "-4",
+ "periodo" : "18"
+ }, {
+ "value" : "-4",
+ "periodo" : "19"
+ }, {
+ "value" : "-3",
+ "periodo" : "20"
+ }, {
+ "value" : "-4",
+ "periodo" : "21"
+ }, {
+ "value" : "-5",
+ "periodo" : "22"
+ }, {
+ "value" : "-5",
+ "periodo" : "23"
+ } ],
+ "humedadRelativa" : [ {
+ "value" : "74",
+ "periodo" : "00"
+ }, {
+ "value" : "71",
+ "periodo" : "01"
+ }, {
+ "value" : "80",
+ "periodo" : "02"
+ }, {
+ "value" : "84",
+ "periodo" : "03"
+ }, {
+ "value" : "81",
+ "periodo" : "04"
+ }, {
+ "value" : "78",
+ "periodo" : "05"
+ }, {
+ "value" : "90",
+ "periodo" : "06"
+ }, {
+ "value" : "100",
+ "periodo" : "07"
+ }, {
+ "value" : "100",
+ "periodo" : "08"
+ }, {
+ "value" : "93",
+ "periodo" : "09"
+ }, {
+ "value" : "84",
+ "periodo" : "10"
+ }, {
+ "value" : "78",
+ "periodo" : "11"
+ }, {
+ "value" : "73",
+ "periodo" : "12"
+ }, {
+ "value" : "74",
+ "periodo" : "13"
+ }, {
+ "value" : "74",
+ "periodo" : "14"
+ }, {
+ "value" : "73",
+ "periodo" : "15"
+ }, {
+ "value" : "78",
+ "periodo" : "16"
+ }, {
+ "value" : "79",
+ "periodo" : "17"
+ }, {
+ "value" : "79",
+ "periodo" : "18"
+ }, {
+ "value" : "77",
+ "periodo" : "19"
+ }, {
+ "value" : "75",
+ "periodo" : "20"
+ }, {
+ "value" : "77",
+ "periodo" : "21"
+ }, {
+ "value" : "80",
+ "periodo" : "22"
+ }, {
+ "value" : "80",
+ "periodo" : "23"
+ } ],
+ "vientoAndRachaMax" : [ {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "6" ],
+ "periodo" : "00"
+ }, {
+ "value" : "12",
+ "periodo" : "00"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "5" ],
+ "periodo" : "01"
+ }, {
+ "value" : "10",
+ "periodo" : "01"
+ }, {
+ "direccion" : [ "N" ],
+ "velocidad" : [ "6" ],
+ "periodo" : "02"
+ }, {
+ "value" : "11",
+ "periodo" : "02"
+ }, {
+ "direccion" : [ "N" ],
+ "velocidad" : [ "6" ],
+ "periodo" : "03"
+ }, {
+ "value" : "9",
+ "periodo" : "03"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "8" ],
+ "periodo" : "04"
+ }, {
+ "value" : "12",
+ "periodo" : "04"
+ }, {
+ "direccion" : [ "N" ],
+ "velocidad" : [ "5" ],
+ "periodo" : "05"
+ }, {
+ "value" : "11",
+ "periodo" : "05"
+ }, {
+ "direccion" : [ "N" ],
+ "velocidad" : [ "9" ],
+ "periodo" : "06"
+ }, {
+ "value" : "13",
+ "periodo" : "06"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "13" ],
+ "periodo" : "07"
+ }, {
+ "value" : "18",
+ "periodo" : "07"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "17" ],
+ "periodo" : "08"
+ }, {
+ "value" : "25",
+ "periodo" : "08"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "21" ],
+ "periodo" : "09"
+ }, {
+ "value" : "31",
+ "periodo" : "09"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "21" ],
+ "periodo" : "10"
+ }, {
+ "value" : "32",
+ "periodo" : "10"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "21" ],
+ "periodo" : "11"
+ }, {
+ "value" : "30",
+ "periodo" : "11"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "22" ],
+ "periodo" : "12"
+ }, {
+ "value" : "32",
+ "periodo" : "12"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "20" ],
+ "periodo" : "13"
+ }, {
+ "value" : "32",
+ "periodo" : "13"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "19" ],
+ "periodo" : "14"
+ }, {
+ "value" : "30",
+ "periodo" : "14"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "17" ],
+ "periodo" : "15"
+ }, {
+ "value" : "28",
+ "periodo" : "15"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "16" ],
+ "periodo" : "16"
+ }, {
+ "value" : "25",
+ "periodo" : "16"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "16" ],
+ "periodo" : "17"
+ }, {
+ "value" : "24",
+ "periodo" : "17"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "17" ],
+ "periodo" : "18"
+ }, {
+ "value" : "24",
+ "periodo" : "18"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "17" ],
+ "periodo" : "19"
+ }, {
+ "value" : "25",
+ "periodo" : "19"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "16" ],
+ "periodo" : "20"
+ }, {
+ "value" : "25",
+ "periodo" : "20"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "17" ],
+ "periodo" : "21"
+ }, {
+ "value" : "24",
+ "periodo" : "21"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "19" ],
+ "periodo" : "22"
+ }, {
+ "value" : "27",
+ "periodo" : "22"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "21" ],
+ "periodo" : "23"
+ }, {
+ "value" : "30",
+ "periodo" : "23"
+ } ],
+ "fecha" : "2021-01-10T00:00:00",
+ "orto" : "08:36",
+ "ocaso" : "18:08"
+ }, {
+ "estadoCielo" : [ {
+ "value" : "14n",
+ "periodo" : "00",
+ "descripcion" : "Nuboso"
+ }, {
+ "value" : "12n",
+ "periodo" : "01",
+ "descripcion" : "Poco nuboso"
+ }, {
+ "value" : "11n",
+ "periodo" : "02",
+ "descripcion" : "Despejado"
+ }, {
+ "value" : "11n",
+ "periodo" : "03",
+ "descripcion" : "Despejado"
+ }, {
+ "value" : "11n",
+ "periodo" : "04",
+ "descripcion" : "Despejado"
+ }, {
+ "value" : "11n",
+ "periodo" : "05",
+ "descripcion" : "Despejado"
+ }, {
+ "value" : "11n",
+ "periodo" : "06",
+ "descripcion" : "Despejado"
+ } ],
+ "precipitacion" : [ {
+ "value" : "0",
+ "periodo" : "00"
+ }, {
+ "value" : "0",
+ "periodo" : "01"
+ }, {
+ "value" : "0",
+ "periodo" : "02"
+ }, {
+ "value" : "0",
+ "periodo" : "03"
+ }, {
+ "value" : "0",
+ "periodo" : "04"
+ }, {
+ "value" : "0",
+ "periodo" : "05"
+ }, {
+ "value" : "0",
+ "periodo" : "06"
+ } ],
+ "probPrecipitacion" : [ {
+ "value" : "0",
+ "periodo" : "0107"
+ }, {
+ "value" : "",
+ "periodo" : "0713"
+ }, {
+ "value" : "",
+ "periodo" : "1319"
+ }, {
+ "value" : "",
+ "periodo" : "1901"
+ } ],
+ "probTormenta" : [ {
+ "value" : "0",
+ "periodo" : "0107"
+ }, {
+ "value" : "",
+ "periodo" : "0713"
+ }, {
+ "value" : "",
+ "periodo" : "1319"
+ }, {
+ "value" : "",
+ "periodo" : "1901"
+ } ],
+ "nieve" : [ {
+ "value" : "0",
+ "periodo" : "00"
+ }, {
+ "value" : "0",
+ "periodo" : "01"
+ }, {
+ "value" : "0",
+ "periodo" : "02"
+ }, {
+ "value" : "0",
+ "periodo" : "03"
+ }, {
+ "value" : "0",
+ "periodo" : "04"
+ }, {
+ "value" : "0",
+ "periodo" : "05"
+ }, {
+ "value" : "0",
+ "periodo" : "06"
+ } ],
+ "probNieve" : [ {
+ "value" : "0",
+ "periodo" : "0107"
+ }, {
+ "value" : "",
+ "periodo" : "0713"
+ }, {
+ "value" : "",
+ "periodo" : "1319"
+ }, {
+ "value" : "",
+ "periodo" : "1901"
+ } ],
+ "temperatura" : [ {
+ "value" : "-1",
+ "periodo" : "00"
+ }, {
+ "value" : "-1",
+ "periodo" : "01"
+ }, {
+ "value" : "-2",
+ "periodo" : "02"
+ }, {
+ "value" : "-2",
+ "periodo" : "03"
+ }, {
+ "value" : "-3",
+ "periodo" : "04"
+ }, {
+ "value" : "-4",
+ "periodo" : "05"
+ }, {
+ "value" : "-4",
+ "periodo" : "06"
+ } ],
+ "sensTermica" : [ {
+ "value" : "-6",
+ "periodo" : "00"
+ }, {
+ "value" : "-6",
+ "periodo" : "01"
+ }, {
+ "value" : "-6",
+ "periodo" : "02"
+ }, {
+ "value" : "-6",
+ "periodo" : "03"
+ }, {
+ "value" : "-7",
+ "periodo" : "04"
+ }, {
+ "value" : "-8",
+ "periodo" : "05"
+ }, {
+ "value" : "-8",
+ "periodo" : "06"
+ } ],
+ "humedadRelativa" : [ {
+ "value" : "81",
+ "periodo" : "00"
+ }, {
+ "value" : "79",
+ "periodo" : "01"
+ }, {
+ "value" : "77",
+ "periodo" : "02"
+ }, {
+ "value" : "76",
+ "periodo" : "03"
+ }, {
+ "value" : "76",
+ "periodo" : "04"
+ }, {
+ "value" : "76",
+ "periodo" : "05"
+ }, {
+ "value" : "78",
+ "periodo" : "06"
+ } ],
+ "vientoAndRachaMax" : [ {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "19" ],
+ "periodo" : "00"
+ }, {
+ "value" : "30",
+ "periodo" : "00"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "16" ],
+ "periodo" : "01"
+ }, {
+ "value" : "27",
+ "periodo" : "01"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "12" ],
+ "periodo" : "02"
+ }, {
+ "value" : "22",
+ "periodo" : "02"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "10" ],
+ "periodo" : "03"
+ }, {
+ "value" : "17",
+ "periodo" : "03"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "11" ],
+ "periodo" : "04"
+ }, {
+ "value" : "15",
+ "periodo" : "04"
+ }, {
+ "direccion" : [ "NE" ],
+ "velocidad" : [ "10" ],
+ "periodo" : "05"
+ }, {
+ "value" : "15",
+ "periodo" : "05"
+ }, {
+ "direccion" : [ "N" ],
+ "velocidad" : [ "10" ],
+ "periodo" : "06"
+ }, {
+ "value" : "15",
+ "periodo" : "06"
+ } ],
+ "fecha" : "2021-01-11T00:00:00",
+ "orto" : "08:36",
+ "ocaso" : "18:09"
+ } ]
+ },
+ "id" : "28065",
+ "version" : "1.0"
+} ]
diff --git a/tests/fixtures/aemet/town-28065-forecast-hourly.json b/tests/fixtures/aemet/town-28065-forecast-hourly.json
new file mode 100644
index 00000000000000..2fbcaaeb33e48a
--- /dev/null
+++ b/tests/fixtures/aemet/town-28065-forecast-hourly.json
@@ -0,0 +1,6 @@
+{
+ "descripcion" : "exito",
+ "estado" : 200,
+ "datos" : "https://opendata.aemet.es/opendata/sh/18ca1886",
+ "metadatos" : "https://opendata.aemet.es/opendata/sh/93a7c63d"
+}
diff --git a/tests/fixtures/aemet/town-id28065.json b/tests/fixtures/aemet/town-id28065.json
new file mode 100644
index 00000000000000..342b163062c539
--- /dev/null
+++ b/tests/fixtures/aemet/town-id28065.json
@@ -0,0 +1,15 @@
+[ {
+ "latitud" : "40�18'14.535144\"",
+ "id_old" : "28325",
+ "url" : "getafe-id28065",
+ "latitud_dec" : "40.30403754",
+ "altitud" : "622",
+ "capital" : "Getafe",
+ "num_hab" : "173057",
+ "zona_comarcal" : "722802",
+ "destacada" : "1",
+ "nombre" : "Getafe",
+ "longitud_dec" : "-3.72935236",
+ "id" : "id28065",
+ "longitud" : "-3�43'45.668496\""
+} ]
diff --git a/tests/fixtures/aemet/town-list.json b/tests/fixtures/aemet/town-list.json
new file mode 100644
index 00000000000000..d5ed23ef9350c5
--- /dev/null
+++ b/tests/fixtures/aemet/town-list.json
@@ -0,0 +1,43 @@
+[ {
+ "latitud" : "40�18'14.535144\"",
+ "id_old" : "28325",
+ "url" : "getafe-id28065",
+ "latitud_dec" : "40.30403754",
+ "altitud" : "622",
+ "capital" : "Getafe",
+ "num_hab" : "173057",
+ "zona_comarcal" : "722802",
+ "destacada" : "1",
+ "nombre" : "Getafe",
+ "longitud_dec" : "-3.72935236",
+ "id" : "id28065",
+ "longitud" : "-3�43'45.668496\""
+}, {
+ "latitud" : "40�19'54.277752\"",
+ "id_old" : "28370",
+ "url" : "leganes-id28074",
+ "latitud_dec" : "40.33174382",
+ "altitud" : "667",
+ "capital" : "Legan�s",
+ "num_hab" : "186696",
+ "zona_comarcal" : "722802",
+ "destacada" : "1",
+ "nombre" : "Legan�s",
+ "longitud_dec" : "-3.76655557",
+ "id" : "id28074",
+ "longitud" : "-3�45'59.600052\""
+}, {
+ "latitud" : "40�24'30.282876\"",
+ "id_old" : "28001",
+ "url" : "madrid-id28079",
+ "latitud_dec" : "40.40841191",
+ "altitud" : "657",
+ "capital" : "Madrid",
+ "num_hab" : "3165235",
+ "zona_comarcal" : "722802",
+ "destacada" : "1",
+ "nombre" : "Madrid",
+ "longitud_dec" : "-3.68760088",
+ "id" : "id28079",
+ "longitud" : "-3�41'15.363168\""
+} ]
diff --git a/tests/fixtures/august/get_activity.bridge_offline.json b/tests/fixtures/august/get_activity.bridge_offline.json
new file mode 100644
index 00000000000000..ed4aaadaf738ba
--- /dev/null
+++ b/tests/fixtures/august/get_activity.bridge_offline.json
@@ -0,0 +1,34 @@
+[{
+ "entities" : {
+ "activity" : "mockActivity2",
+ "house" : "123",
+ "device" : "online_with_doorsense",
+ "callingUser" : "mockUserId2",
+ "otherUser" : "deleted"
+ },
+ "callingUser" : {
+ "LastName" : "elven princess",
+ "UserID" : "mockUserId2",
+ "FirstName" : "Your favorite"
+ },
+ "otherUser" : {
+ "LastName" : "User",
+ "UserName" : "deleteduser",
+ "FirstName" : "Unknown",
+ "UserID" : "deleted",
+ "PhoneNo" : "deleted"
+ },
+ "deviceType" : "lock",
+ "deviceName" : "MockHouseTDoor",
+ "action" : "associated_bridge_offline",
+ "dateTime" : 1582007218000,
+ "info" : {
+ "remote" : true,
+ "DateLogActionID" : "ABC+Time"
+ },
+ "deviceID" : "online_with_doorsense",
+ "house" : {
+ "houseName" : "MockHouse",
+ "houseID" : "123"
+ }
+}]
diff --git a/tests/fixtures/august/get_activity.bridge_online.json b/tests/fixtures/august/get_activity.bridge_online.json
new file mode 100644
index 00000000000000..db14f06cfe939f
--- /dev/null
+++ b/tests/fixtures/august/get_activity.bridge_online.json
@@ -0,0 +1,34 @@
+[{
+ "entities" : {
+ "activity" : "mockActivity2",
+ "house" : "123",
+ "device" : "online_with_doorsense",
+ "callingUser" : "mockUserId2",
+ "otherUser" : "deleted"
+ },
+ "callingUser" : {
+ "LastName" : "elven princess",
+ "UserID" : "mockUserId2",
+ "FirstName" : "Your favorite"
+ },
+ "otherUser" : {
+ "LastName" : "User",
+ "UserName" : "deleteduser",
+ "FirstName" : "Unknown",
+ "UserID" : "deleted",
+ "PhoneNo" : "deleted"
+ },
+ "deviceType" : "lock",
+ "deviceName" : "MockHouseTDoor",
+ "action" : "associated_bridge_online",
+ "dateTime" : 1582007218000,
+ "info" : {
+ "remote" : true,
+ "DateLogActionID" : "ABC+Time"
+ },
+ "deviceID" : "online_with_doorsense",
+ "house" : {
+ "houseName" : "MockHouse",
+ "houseID" : "123"
+ }
+}]
diff --git a/tests/fixtures/august/get_doorbell.json b/tests/fixtures/august/get_doorbell.json
index abe6e37b1e3bc3..fb2cd5780c961d 100644
--- a/tests/fixtures/august/get_doorbell.json
+++ b/tests/fixtures/august/get_doorbell.json
@@ -55,7 +55,7 @@
"reconnect"
],
"doorbellID" : "K98GiDT45GUL",
- "HouseID" : "3dd2accaea08",
+ "HouseID" : "mockhouseid1",
"telemetry" : {
"signal_level" : -56,
"date" : "2017-12-10 08:05:12",
diff --git a/tests/fixtures/august/get_lock.online_with_doorsense.json b/tests/fixtures/august/get_lock.online_with_doorsense.json
index f737657048231f..e29614c9e489be 100644
--- a/tests/fixtures/august/get_lock.online_with_doorsense.json
+++ b/tests/fixtures/august/get_lock.online_with_doorsense.json
@@ -13,9 +13,10 @@
"updated" : "2000-00-00T00:00:00.447Z"
}
},
+ "pubsubChannel":"pubsub",
"Calibrated" : false,
"Created" : "2000-00-00T00:00:00.447Z",
- "HouseID" : "123",
+ "HouseID" : "mockhouseid1",
"HouseName" : "Test",
"LockID" : "online_with_doorsense",
"LockName" : "Online door with doorsense",
diff --git a/tests/fixtures/climacell/v3_forecast_daily.json b/tests/fixtures/climacell/v3_forecast_daily.json
new file mode 100644
index 00000000000000..18f2d77e0cf978
--- /dev/null
+++ b/tests/fixtures/climacell/v3_forecast_daily.json
@@ -0,0 +1,992 @@
+[
+ {
+ "temp": [
+ {
+ "observation_time": "2021-03-07T11:00:00Z",
+ "min": {
+ "value": 23.47,
+ "units": "F"
+ }
+ },
+ {
+ "observation_time": "2021-03-07T21:00:00Z",
+ "max": {
+ "value": 44.88,
+ "units": "F"
+ }
+ }
+ ],
+ "precipitation_accumulation": {
+ "value": 0,
+ "units": "in"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": [
+ {
+ "observation_time": "2021-03-08T00:00:00Z",
+ "min": {
+ "value": 2.58,
+ "units": "mph"
+ }
+ },
+ {
+ "observation_time": "2021-03-07T19:00:00Z",
+ "max": {
+ "value": 7.67,
+ "units": "mph"
+ }
+ }
+ ],
+ "wind_direction": [
+ {
+ "observation_time": "2021-03-08T00:00:00Z",
+ "min": {
+ "value": 72.1,
+ "units": "degrees"
+ }
+ },
+ {
+ "observation_time": "2021-03-07T19:00:00Z",
+ "max": {
+ "value": 313.49,
+ "units": "degrees"
+ }
+ }
+ ],
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-07"
+ },
+ "lat": 38.90694,
+ "lon": -77.03012
+ },
+ {
+ "temp": [
+ {
+ "observation_time": "2021-03-08T11:00:00Z",
+ "min": {
+ "value": 24.79,
+ "units": "F"
+ }
+ },
+ {
+ "observation_time": "2021-03-08T21:00:00Z",
+ "max": {
+ "value": 49.42,
+ "units": "F"
+ }
+ }
+ ],
+ "precipitation_accumulation": {
+ "value": 0,
+ "units": "in"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": [
+ {
+ "observation_time": "2021-03-08T22:00:00Z",
+ "min": {
+ "value": 1.97,
+ "units": "mph"
+ }
+ },
+ {
+ "observation_time": "2021-03-08T13:00:00Z",
+ "max": {
+ "value": 7.24,
+ "units": "mph"
+ }
+ }
+ ],
+ "wind_direction": [
+ {
+ "observation_time": "2021-03-08T22:00:00Z",
+ "min": {
+ "value": 268.74,
+ "units": "degrees"
+ }
+ },
+ {
+ "observation_time": "2021-03-08T13:00:00Z",
+ "max": {
+ "value": 324.8,
+ "units": "degrees"
+ }
+ }
+ ],
+ "weather_code": {
+ "value": "cloudy"
+ },
+ "observation_time": {
+ "value": "2021-03-08"
+ },
+ "lat": 38.90694,
+ "lon": -77.03012
+ },
+ {
+ "temp": [
+ {
+ "observation_time": "2021-03-09T11:00:00Z",
+ "min": {
+ "value": 31.48,
+ "units": "F"
+ }
+ },
+ {
+ "observation_time": "2021-03-09T21:00:00Z",
+ "max": {
+ "value": 66.98,
+ "units": "F"
+ }
+ }
+ ],
+ "precipitation_accumulation": {
+ "value": 0,
+ "units": "in"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": [
+ {
+ "observation_time": "2021-03-09T22:00:00Z",
+ "min": {
+ "value": 3.35,
+ "units": "mph"
+ }
+ },
+ {
+ "observation_time": "2021-03-09T19:00:00Z",
+ "max": {
+ "value": 7.05,
+ "units": "mph"
+ }
+ }
+ ],
+ "wind_direction": [
+ {
+ "observation_time": "2021-03-09T22:00:00Z",
+ "min": {
+ "value": 279.37,
+ "units": "degrees"
+ }
+ },
+ {
+ "observation_time": "2021-03-09T19:00:00Z",
+ "max": {
+ "value": 253.12,
+ "units": "degrees"
+ }
+ }
+ ],
+ "weather_code": {
+ "value": "mostly_cloudy"
+ },
+ "observation_time": {
+ "value": "2021-03-09"
+ },
+ "lat": 38.90694,
+ "lon": -77.03012
+ },
+ {
+ "temp": [
+ {
+ "observation_time": "2021-03-10T11:00:00Z",
+ "min": {
+ "value": 37.32,
+ "units": "F"
+ }
+ },
+ {
+ "observation_time": "2021-03-10T20:00:00Z",
+ "max": {
+ "value": 65.28,
+ "units": "F"
+ }
+ }
+ ],
+ "precipitation_accumulation": {
+ "value": 0,
+ "units": "in"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": [
+ {
+ "observation_time": "2021-03-10T05:00:00Z",
+ "min": {
+ "value": 2.13,
+ "units": "mph"
+ }
+ },
+ {
+ "observation_time": "2021-03-10T21:00:00Z",
+ "max": {
+ "value": 9.42,
+ "units": "mph"
+ }
+ }
+ ],
+ "wind_direction": [
+ {
+ "observation_time": "2021-03-10T05:00:00Z",
+ "min": {
+ "value": 342.01,
+ "units": "degrees"
+ }
+ },
+ {
+ "observation_time": "2021-03-10T21:00:00Z",
+ "max": {
+ "value": 193.22,
+ "units": "degrees"
+ }
+ }
+ ],
+ "weather_code": {
+ "value": "cloudy"
+ },
+ "observation_time": {
+ "value": "2021-03-10"
+ },
+ "lat": 38.90694,
+ "lon": -77.03012
+ },
+ {
+ "temp": [
+ {
+ "observation_time": "2021-03-11T12:00:00Z",
+ "min": {
+ "value": 48.69,
+ "units": "F"
+ }
+ },
+ {
+ "observation_time": "2021-03-11T21:00:00Z",
+ "max": {
+ "value": 67.37,
+ "units": "F"
+ }
+ }
+ ],
+ "precipitation_accumulation": {
+ "value": 0,
+ "units": "in"
+ },
+ "precipitation_probability": {
+ "value": 5,
+ "units": "%"
+ },
+ "wind_speed": [
+ {
+ "observation_time": "2021-03-11T02:00:00Z",
+ "min": {
+ "value": 8.82,
+ "units": "mph"
+ }
+ },
+ {
+ "observation_time": "2021-03-12T01:00:00Z",
+ "max": {
+ "value": 14.47,
+ "units": "mph"
+ }
+ }
+ ],
+ "wind_direction": [
+ {
+ "observation_time": "2021-03-11T02:00:00Z",
+ "min": {
+ "value": 176.84,
+ "units": "degrees"
+ }
+ },
+ {
+ "observation_time": "2021-03-12T01:00:00Z",
+ "max": {
+ "value": 210.63,
+ "units": "degrees"
+ }
+ }
+ ],
+ "weather_code": {
+ "value": "cloudy"
+ },
+ "observation_time": {
+ "value": "2021-03-11"
+ },
+ "lat": 38.90694,
+ "lon": -77.03012
+ },
+ {
+ "temp": [
+ {
+ "observation_time": "2021-03-12T12:00:00Z",
+ "min": {
+ "value": 53.83,
+ "units": "F"
+ }
+ },
+ {
+ "observation_time": "2021-03-12T18:00:00Z",
+ "max": {
+ "value": 67.91,
+ "units": "F"
+ }
+ }
+ ],
+ "precipitation_accumulation": {
+ "value": 0.0018,
+ "units": "in"
+ },
+ "precipitation_probability": {
+ "value": 25,
+ "units": "%"
+ },
+ "wind_speed": [
+ {
+ "observation_time": "2021-03-13T00:00:00Z",
+ "min": {
+ "value": 4.98,
+ "units": "mph"
+ }
+ },
+ {
+ "observation_time": "2021-03-12T02:00:00Z",
+ "max": {
+ "value": 15.69,
+ "units": "mph"
+ }
+ }
+ ],
+ "wind_direction": [
+ {
+ "observation_time": "2021-03-13T00:00:00Z",
+ "min": {
+ "value": 329.35,
+ "units": "degrees"
+ }
+ },
+ {
+ "observation_time": "2021-03-12T02:00:00Z",
+ "max": {
+ "value": 211.47,
+ "units": "degrees"
+ }
+ }
+ ],
+ "weather_code": {
+ "value": "cloudy"
+ },
+ "observation_time": {
+ "value": "2021-03-12"
+ },
+ "lat": 38.90694,
+ "lon": -77.03012
+ },
+ {
+ "temp": [
+ {
+ "observation_time": "2021-03-14T00:00:00Z",
+ "min": {
+ "value": 45.48,
+ "units": "F"
+ }
+ },
+ {
+ "observation_time": "2021-03-13T03:00:00Z",
+ "max": {
+ "value": 60.42,
+ "units": "F"
+ }
+ }
+ ],
+ "precipitation_accumulation": {
+ "value": 0,
+ "units": "in"
+ },
+ "precipitation_probability": {
+ "value": 25,
+ "units": "%"
+ },
+ "wind_speed": [
+ {
+ "observation_time": "2021-03-13T03:00:00Z",
+ "min": {
+ "value": 2.91,
+ "units": "mph"
+ }
+ },
+ {
+ "observation_time": "2021-03-13T21:00:00Z",
+ "max": {
+ "value": 9.72,
+ "units": "mph"
+ }
+ }
+ ],
+ "wind_direction": [
+ {
+ "observation_time": "2021-03-13T03:00:00Z",
+ "min": {
+ "value": 202.04,
+ "units": "degrees"
+ }
+ },
+ {
+ "observation_time": "2021-03-13T21:00:00Z",
+ "max": {
+ "value": 64.38,
+ "units": "degrees"
+ }
+ }
+ ],
+ "weather_code": {
+ "value": "cloudy"
+ },
+ "observation_time": {
+ "value": "2021-03-13"
+ },
+ "lat": 38.90694,
+ "lon": -77.03012
+ },
+ {
+ "temp": [
+ {
+ "observation_time": "2021-03-15T00:00:00Z",
+ "min": {
+ "value": 37.81,
+ "units": "F"
+ }
+ },
+ {
+ "observation_time": "2021-03-14T03:00:00Z",
+ "max": {
+ "value": 43.58,
+ "units": "F"
+ }
+ }
+ ],
+ "precipitation_accumulation": {
+ "value": 0.0423,
+ "units": "in"
+ },
+ "precipitation_probability": {
+ "value": 75,
+ "units": "%"
+ },
+ "wind_speed": [
+ {
+ "observation_time": "2021-03-14T06:00:00Z",
+ "min": {
+ "value": 5.34,
+ "units": "mph"
+ }
+ },
+ {
+ "observation_time": "2021-03-14T21:00:00Z",
+ "max": {
+ "value": 16.25,
+ "units": "mph"
+ }
+ }
+ ],
+ "wind_direction": [
+ {
+ "observation_time": "2021-03-14T06:00:00Z",
+ "min": {
+ "value": 57.52,
+ "units": "degrees"
+ }
+ },
+ {
+ "observation_time": "2021-03-14T21:00:00Z",
+ "max": {
+ "value": 83.23,
+ "units": "degrees"
+ }
+ }
+ ],
+ "weather_code": {
+ "value": "rain_light"
+ },
+ "observation_time": {
+ "value": "2021-03-14"
+ },
+ "lat": 38.90694,
+ "lon": -77.03012
+ },
+ {
+ "temp": [
+ {
+ "observation_time": "2021-03-16T00:00:00Z",
+ "min": {
+ "value": 32.31,
+ "units": "F"
+ }
+ },
+ {
+ "observation_time": "2021-03-15T09:00:00Z",
+ "max": {
+ "value": 34.21,
+ "units": "F"
+ }
+ }
+ ],
+ "precipitation_accumulation": {
+ "value": 0.2876,
+ "units": "in"
+ },
+ "precipitation_probability": {
+ "value": 95,
+ "units": "%"
+ },
+ "wind_speed": [
+ {
+ "observation_time": "2021-03-16T00:00:00Z",
+ "min": {
+ "value": 11.7,
+ "units": "mph"
+ }
+ },
+ {
+ "observation_time": "2021-03-15T18:00:00Z",
+ "max": {
+ "value": 15.89,
+ "units": "mph"
+ }
+ }
+ ],
+ "wind_direction": [
+ {
+ "observation_time": "2021-03-16T00:00:00Z",
+ "min": {
+ "value": 63.67,
+ "units": "degrees"
+ }
+ },
+ {
+ "observation_time": "2021-03-15T18:00:00Z",
+ "max": {
+ "value": 59.49,
+ "units": "degrees"
+ }
+ }
+ ],
+ "weather_code": {
+ "value": "snow_heavy"
+ },
+ "observation_time": {
+ "value": "2021-03-15"
+ },
+ "lat": 38.90694,
+ "lon": -77.03012
+ },
+ {
+ "temp": [
+ {
+ "observation_time": "2021-03-16T12:00:00Z",
+ "min": {
+ "value": 29.1,
+ "units": "F"
+ }
+ },
+ {
+ "observation_time": "2021-03-16T21:00:00Z",
+ "max": {
+ "value": 43,
+ "units": "F"
+ }
+ }
+ ],
+ "precipitation_accumulation": {
+ "value": 0.0002,
+ "units": "in"
+ },
+ "precipitation_probability": {
+ "value": 5,
+ "units": "%"
+ },
+ "wind_speed": [
+ {
+ "observation_time": "2021-03-16T18:00:00Z",
+ "min": {
+ "value": 4.98,
+ "units": "mph"
+ }
+ },
+ {
+ "observation_time": "2021-03-16T03:00:00Z",
+ "max": {
+ "value": 9.77,
+ "units": "mph"
+ }
+ }
+ ],
+ "wind_direction": [
+ {
+ "observation_time": "2021-03-16T18:00:00Z",
+ "min": {
+ "value": 80.47,
+ "units": "degrees"
+ }
+ },
+ {
+ "observation_time": "2021-03-16T03:00:00Z",
+ "max": {
+ "value": 58.98,
+ "units": "degrees"
+ }
+ }
+ ],
+ "weather_code": {
+ "value": "cloudy"
+ },
+ "observation_time": {
+ "value": "2021-03-16"
+ },
+ "lat": 38.90694,
+ "lon": -77.03012
+ },
+ {
+ "temp": [
+ {
+ "observation_time": "2021-03-17T12:00:00Z",
+ "min": {
+ "value": 34.32,
+ "units": "F"
+ }
+ },
+ {
+ "observation_time": "2021-03-17T21:00:00Z",
+ "max": {
+ "value": 52.4,
+ "units": "F"
+ }
+ }
+ ],
+ "precipitation_accumulation": {
+ "value": 0,
+ "units": "in"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": [
+ {
+ "observation_time": "2021-03-18T00:00:00Z",
+ "min": {
+ "value": 4.49,
+ "units": "mph"
+ }
+ },
+ {
+ "observation_time": "2021-03-17T03:00:00Z",
+ "max": {
+ "value": 6.71,
+ "units": "mph"
+ }
+ }
+ ],
+ "wind_direction": [
+ {
+ "observation_time": "2021-03-18T00:00:00Z",
+ "min": {
+ "value": 116.64,
+ "units": "degrees"
+ }
+ },
+ {
+ "observation_time": "2021-03-17T03:00:00Z",
+ "max": {
+ "value": 111.51,
+ "units": "degrees"
+ }
+ }
+ ],
+ "weather_code": {
+ "value": "cloudy"
+ },
+ "observation_time": {
+ "value": "2021-03-17"
+ },
+ "lat": 38.90694,
+ "lon": -77.03012
+ },
+ {
+ "temp": [
+ {
+ "observation_time": "2021-03-18T12:00:00Z",
+ "min": {
+ "value": 41.99,
+ "units": "F"
+ }
+ },
+ {
+ "observation_time": "2021-03-18T21:00:00Z",
+ "max": {
+ "value": 54.07,
+ "units": "F"
+ }
+ }
+ ],
+ "precipitation_accumulation": {
+ "value": 0,
+ "units": "in"
+ },
+ "precipitation_probability": {
+ "value": 5,
+ "units": "%"
+ },
+ "wind_speed": [
+ {
+ "observation_time": "2021-03-18T06:00:00Z",
+ "min": {
+ "value": 2.77,
+ "units": "mph"
+ }
+ },
+ {
+ "observation_time": "2021-03-18T03:00:00Z",
+ "max": {
+ "value": 5.22,
+ "units": "mph"
+ }
+ }
+ ],
+ "wind_direction": [
+ {
+ "observation_time": "2021-03-18T06:00:00Z",
+ "min": {
+ "value": 119.5,
+ "units": "degrees"
+ }
+ },
+ {
+ "observation_time": "2021-03-18T03:00:00Z",
+ "max": {
+ "value": 135.5,
+ "units": "degrees"
+ }
+ }
+ ],
+ "weather_code": {
+ "value": "cloudy"
+ },
+ "observation_time": {
+ "value": "2021-03-18"
+ },
+ "lat": 38.90694,
+ "lon": -77.03012
+ },
+ {
+ "temp": [
+ {
+ "observation_time": "2021-03-19T12:00:00Z",
+ "min": {
+ "value": 40.48,
+ "units": "F"
+ }
+ },
+ {
+ "observation_time": "2021-03-19T18:00:00Z",
+ "max": {
+ "value": 48.94,
+ "units": "F"
+ }
+ }
+ ],
+ "precipitation_accumulation": {
+ "value": 0.007,
+ "units": "in"
+ },
+ "precipitation_probability": {
+ "value": 45,
+ "units": "%"
+ },
+ "wind_speed": [
+ {
+ "observation_time": "2021-03-19T03:00:00Z",
+ "min": {
+ "value": 5.43,
+ "units": "mph"
+ }
+ },
+ {
+ "observation_time": "2021-03-20T00:00:00Z",
+ "max": {
+ "value": 11.1,
+ "units": "mph"
+ }
+ }
+ ],
+ "wind_direction": [
+ {
+ "observation_time": "2021-03-19T03:00:00Z",
+ "min": {
+ "value": 50.18,
+ "units": "degrees"
+ }
+ },
+ {
+ "observation_time": "2021-03-20T00:00:00Z",
+ "max": {
+ "value": 86.96,
+ "units": "degrees"
+ }
+ }
+ ],
+ "weather_code": {
+ "value": "cloudy"
+ },
+ "observation_time": {
+ "value": "2021-03-19"
+ },
+ "lat": 38.90694,
+ "lon": -77.03012
+ },
+ {
+ "temp": [
+ {
+ "observation_time": "2021-03-21T00:00:00Z",
+ "min": {
+ "value": 37.56,
+ "units": "F"
+ }
+ },
+ {
+ "observation_time": "2021-03-20T03:00:00Z",
+ "max": {
+ "value": 41.05,
+ "units": "F"
+ }
+ }
+ ],
+ "precipitation_accumulation": {
+ "value": 0.0485,
+ "units": "in"
+ },
+ "precipitation_probability": {
+ "value": 55,
+ "units": "%"
+ },
+ "wind_speed": [
+ {
+ "observation_time": "2021-03-20T03:00:00Z",
+ "min": {
+ "value": 10.9,
+ "units": "mph"
+ }
+ },
+ {
+ "observation_time": "2021-03-20T21:00:00Z",
+ "max": {
+ "value": 17.35,
+ "units": "mph"
+ }
+ }
+ ],
+ "wind_direction": [
+ {
+ "observation_time": "2021-03-20T03:00:00Z",
+ "min": {
+ "value": 70.56,
+ "units": "degrees"
+ }
+ },
+ {
+ "observation_time": "2021-03-20T21:00:00Z",
+ "max": {
+ "value": 58.55,
+ "units": "degrees"
+ }
+ }
+ ],
+ "weather_code": {
+ "value": "drizzle"
+ },
+ "observation_time": {
+ "value": "2021-03-20"
+ },
+ "lat": 38.90694,
+ "lon": -77.03012
+ },
+ {
+ "temp": [
+ {
+ "observation_time": "2021-03-21T12:00:00Z",
+ "min": {
+ "value": 33.66,
+ "units": "F"
+ }
+ },
+ {
+ "observation_time": "2021-03-21T21:00:00Z",
+ "max": {
+ "value": 44.3,
+ "units": "F"
+ }
+ }
+ ],
+ "precipitation_accumulation": {
+ "value": 0.0017,
+ "units": "in"
+ },
+ "precipitation_probability": {
+ "value": 20,
+ "units": "%"
+ },
+ "wind_speed": [
+ {
+ "observation_time": "2021-03-22T00:00:00Z",
+ "min": {
+ "value": 8.65,
+ "units": "mph"
+ }
+ },
+ {
+ "observation_time": "2021-03-21T03:00:00Z",
+ "max": {
+ "value": 16.53,
+ "units": "mph"
+ }
+ }
+ ],
+ "wind_direction": [
+ {
+ "observation_time": "2021-03-22T00:00:00Z",
+ "min": {
+ "value": 64.92,
+ "units": "degrees"
+ }
+ },
+ {
+ "observation_time": "2021-03-21T03:00:00Z",
+ "max": {
+ "value": 57.74,
+ "units": "degrees"
+ }
+ }
+ ],
+ "weather_code": {
+ "value": "cloudy"
+ },
+ "observation_time": {
+ "value": "2021-03-21"
+ },
+ "lat": 38.90694,
+ "lon": -77.03012
+ }
+]
\ No newline at end of file
diff --git a/tests/fixtures/climacell/v3_forecast_hourly.json b/tests/fixtures/climacell/v3_forecast_hourly.json
new file mode 100644
index 00000000000000..a550c7f4302fcf
--- /dev/null
+++ b/tests/fixtures/climacell/v3_forecast_hourly.json
@@ -0,0 +1,752 @@
+[
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 42.75,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 8.99,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 320.22,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-07T18:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 44.29,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 9.65,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 45.3,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 9.28,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 322.01,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-07T20:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 45.26,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 9.12,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 323.71,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-07T21:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 44.83,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 7.27,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 319.88,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-07T22:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 41.7,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 4.37,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 320.69,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-07T23:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 38.04,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 5.45,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 351.54,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-08T00:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 35.88,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 5.31,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 20.6,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-08T01:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 34.34,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 5.78,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 11.22,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-08T02:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 33.3,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 5.73,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 15.46,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-08T03:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 31.74,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 4.44,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 26.07,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-08T04:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 29.98,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 4.33,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 23.7,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-08T05:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 27.34,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 4.7,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 354.56,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-08T06:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 26.61,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 4.94,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 349.63,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-08T07:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 25.96,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 4.61,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 336.74,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-08T08:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 25.72,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 4.22,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 332.71,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-08T09:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 25.68,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 4.56,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 328.58,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-08T10:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 31.02,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 2.8,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 322.27,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-08T11:00:00.000Z"
+ }
+ },
+ {
+ "lon": -77.03012,
+ "lat": 38.90694,
+ "temp": {
+ "value": 31.04,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 2.82,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 325.27,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-08T12:00:00.000Z"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 29.95,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 7.24,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 324.8,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "mostly_clear"
+ },
+ "observation_time": {
+ "value": "2021-03-08T13:00:00.000Z"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 34.02,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 6.28,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 335.16,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "partly_cloudy"
+ },
+ "observation_time": {
+ "value": "2021-03-08T14:00:00.000Z"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 37.78,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 5.8,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 324.49,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "cloudy"
+ },
+ "observation_time": {
+ "value": "2021-03-08T15:00:00.000Z"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 40.57,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 5.5,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 310.68,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "mostly_cloudy"
+ },
+ "observation_time": {
+ "value": "2021-03-08T16:00:00.000Z"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 42.83,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 5.47,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 304.18,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "mostly_clear"
+ },
+ "observation_time": {
+ "value": "2021-03-08T17:00:00.000Z"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 45.07,
+ "units": "F"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "precipitation_probability": {
+ "value": 0,
+ "units": "%"
+ },
+ "wind_speed": {
+ "value": 4.88,
+ "units": "mph"
+ },
+ "wind_direction": {
+ "value": 301.19,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "observation_time": {
+ "value": "2021-03-08T18:00:00.000Z"
+ }
+ }
+]
\ No newline at end of file
diff --git a/tests/fixtures/climacell/v3_forecast_nowcast.json b/tests/fixtures/climacell/v3_forecast_nowcast.json
new file mode 100644
index 00000000000000..23372eae0f9429
--- /dev/null
+++ b/tests/fixtures/climacell/v3_forecast_nowcast.json
@@ -0,0 +1,782 @@
+[
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.14,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.58,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 320.22,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T18:54:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.17,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.59,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 320.22,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T18:55:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.19,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.6,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 320.22,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T18:56:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.22,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.61,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 320.22,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T18:57:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.24,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.62,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 320.22,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T18:58:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.27,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.64,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 320.22,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T18:59:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.29,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.65,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:00:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.31,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.64,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:01:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.33,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.63,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:02:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.34,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.63,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:03:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.36,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.62,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:04:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.38,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.61,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:05:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.4,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.61,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:06:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.41,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.6,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:07:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.43,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.6,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:08:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.45,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.59,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:09:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.46,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.58,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:10:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.48,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.58,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:11:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.5,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.57,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:12:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.51,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.57,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:13:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.53,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.56,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:14:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.55,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.55,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:15:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.56,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.55,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:16:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.58,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.54,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:17:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.6,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.54,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:18:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.61,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.53,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:19:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.63,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.52,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:20:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.65,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.52,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:21:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.66,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.51,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:22:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ },
+ {
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 44.68,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.51,
+ "units": "mph"
+ },
+ "precipitation": {
+ "value": 0,
+ "units": "in/hr"
+ },
+ "wind_direction": {
+ "value": 326.14,
+ "units": "degrees"
+ },
+ "observation_time": {
+ "value": "2021-03-07T19:23:06.493Z"
+ },
+ "weather_code": {
+ "value": "clear"
+ }
+ }
+]
\ No newline at end of file
diff --git a/tests/fixtures/climacell/v3_realtime.json b/tests/fixtures/climacell/v3_realtime.json
new file mode 100644
index 00000000000000..8ed05fe5383774
--- /dev/null
+++ b/tests/fixtures/climacell/v3_realtime.json
@@ -0,0 +1,38 @@
+{
+ "lat": 38.90694,
+ "lon": -77.03012,
+ "temp": {
+ "value": 43.93,
+ "units": "F"
+ },
+ "wind_speed": {
+ "value": 9.09,
+ "units": "mph"
+ },
+ "baro_pressure": {
+ "value": 30.3605,
+ "units": "inHg"
+ },
+ "visibility": {
+ "value": 6.21,
+ "units": "mi"
+ },
+ "humidity": {
+ "value": 24.5,
+ "units": "%"
+ },
+ "wind_direction": {
+ "value": 320.31,
+ "units": "degrees"
+ },
+ "weather_code": {
+ "value": "clear"
+ },
+ "o3": {
+ "value": 52.625,
+ "units": "ppb"
+ },
+ "observation_time": {
+ "value": "2021-03-07T18:54:06.055Z"
+ }
+}
\ No newline at end of file
diff --git a/tests/fixtures/climacell/v4.json b/tests/fixtures/climacell/v4.json
new file mode 100644
index 00000000000000..d667284a4ad894
--- /dev/null
+++ b/tests/fixtures/climacell/v4.json
@@ -0,0 +1,2360 @@
+{
+ "current": {
+ "temperature": 44.13,
+ "humidity": 22.71,
+ "pressureSeaLevel": 30.35,
+ "windSpeed": 9.33,
+ "windDirection": 315.14,
+ "weatherCode": 1000,
+ "visibility": 8.15,
+ "pollutantO3": 46.53
+ },
+ "forecasts": {
+ "nowcast": [
+ {
+ "startTime": "2021-03-07T17:48:00Z",
+ "values": {
+ "temperatureMin": 44.13,
+ "temperatureMax": 44.13,
+ "windSpeed": 9.33,
+ "windDirection": 315.14,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T17:53:00Z",
+ "values": {
+ "temperatureMin": 43.9,
+ "temperatureMax": 43.9,
+ "windSpeed": 9.31,
+ "windDirection": 315.14,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T17:58:00Z",
+ "values": {
+ "temperatureMin": 43.68,
+ "temperatureMax": 43.68,
+ "windSpeed": 9.28,
+ "windDirection": 315.14,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T18:03:00Z",
+ "values": {
+ "temperatureMin": 43.66,
+ "temperatureMax": 43.66,
+ "windSpeed": 9.26,
+ "windDirection": 315.14,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T18:08:00Z",
+ "values": {
+ "temperatureMin": 43.79,
+ "temperatureMax": 43.79,
+ "windSpeed": 9.22,
+ "windDirection": 315.14,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T18:13:00Z",
+ "values": {
+ "temperatureMin": 43.92,
+ "temperatureMax": 43.92,
+ "windSpeed": 9.17,
+ "windDirection": 315.14,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T18:18:00Z",
+ "values": {
+ "temperatureMin": 44.04,
+ "temperatureMax": 44.04,
+ "windSpeed": 9.13,
+ "windDirection": 315.14,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T18:23:00Z",
+ "values": {
+ "temperatureMin": 44.17,
+ "temperatureMax": 44.17,
+ "windSpeed": 9.06,
+ "windDirection": 315.14,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T18:28:00Z",
+ "values": {
+ "temperatureMin": 44.31,
+ "temperatureMax": 44.31,
+ "windSpeed": 9.02,
+ "windDirection": 315.14,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T18:33:00Z",
+ "values": {
+ "temperatureMin": 44.44,
+ "temperatureMax": 44.44,
+ "windSpeed": 8.97,
+ "windDirection": 321.71,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T18:38:00Z",
+ "values": {
+ "temperatureMin": 44.56,
+ "temperatureMax": 44.56,
+ "windSpeed": 8.93,
+ "windDirection": 321.71,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T18:43:00Z",
+ "values": {
+ "temperatureMin": 44.69,
+ "temperatureMax": 44.69,
+ "windSpeed": 8.88,
+ "windDirection": 321.71,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T18:48:00Z",
+ "values": {
+ "temperatureMin": 44.82,
+ "temperatureMax": 44.82,
+ "windSpeed": 8.84,
+ "windDirection": 321.71,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T18:53:00Z",
+ "values": {
+ "temperatureMin": 44.94,
+ "temperatureMax": 44.94,
+ "windSpeed": 8.79,
+ "windDirection": 321.71,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T18:58:00Z",
+ "values": {
+ "temperatureMin": 45.07,
+ "temperatureMax": 45.07,
+ "windSpeed": 8.75,
+ "windDirection": 321.71,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T19:03:00Z",
+ "values": {
+ "temperatureMin": 45.16,
+ "temperatureMax": 45.16,
+ "windSpeed": 8.75,
+ "windDirection": 321.71,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T19:08:00Z",
+ "values": {
+ "temperatureMin": 45.23,
+ "temperatureMax": 45.23,
+ "windSpeed": 8.75,
+ "windDirection": 321.71,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T19:13:00Z",
+ "values": {
+ "temperatureMin": 45.28,
+ "temperatureMax": 45.28,
+ "windSpeed": 8.77,
+ "windDirection": 321.71,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T19:18:00Z",
+ "values": {
+ "temperatureMin": 45.36,
+ "temperatureMax": 45.36,
+ "windSpeed": 8.79,
+ "windDirection": 321.71,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T19:23:00Z",
+ "values": {
+ "temperatureMin": 45.43,
+ "temperatureMax": 45.43,
+ "windSpeed": 8.81,
+ "windDirection": 321.71,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T19:28:00Z",
+ "values": {
+ "temperatureMin": 45.5,
+ "temperatureMax": 45.5,
+ "windSpeed": 8.81,
+ "windDirection": 321.71,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T19:33:00Z",
+ "values": {
+ "temperatureMin": 45.55,
+ "temperatureMax": 45.55,
+ "windSpeed": 8.84,
+ "windDirection": 323.38,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T19:38:00Z",
+ "values": {
+ "temperatureMin": 45.63,
+ "temperatureMax": 45.63,
+ "windSpeed": 8.86,
+ "windDirection": 323.38,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T19:43:00Z",
+ "values": {
+ "temperatureMin": 45.7,
+ "temperatureMax": 45.7,
+ "windSpeed": 8.88,
+ "windDirection": 323.38,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T19:48:00Z",
+ "values": {
+ "temperatureMin": 45.75,
+ "temperatureMax": 45.75,
+ "windSpeed": 8.9,
+ "windDirection": 323.38,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T19:53:00Z",
+ "values": {
+ "temperatureMin": 45.82,
+ "temperatureMax": 45.82,
+ "windSpeed": 8.9,
+ "windDirection": 323.38,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T19:58:00Z",
+ "values": {
+ "temperatureMin": 45.9,
+ "temperatureMax": 45.9,
+ "windSpeed": 8.93,
+ "windDirection": 323.38,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T20:03:00Z",
+ "values": {
+ "temperatureMin": 45.88,
+ "temperatureMax": 45.88,
+ "windSpeed": 8.97,
+ "windDirection": 323.38,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T20:08:00Z",
+ "values": {
+ "temperatureMin": 45.82,
+ "temperatureMax": 45.82,
+ "windSpeed": 9.02,
+ "windDirection": 323.38,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T20:13:00Z",
+ "values": {
+ "temperatureMin": 45.75,
+ "temperatureMax": 45.75,
+ "windSpeed": 9.06,
+ "windDirection": 323.38,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T20:18:00Z",
+ "values": {
+ "temperatureMin": 45.7,
+ "temperatureMax": 45.7,
+ "windSpeed": 9.1,
+ "windDirection": 323.38,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T20:23:00Z",
+ "values": {
+ "temperatureMin": 45.63,
+ "temperatureMax": 45.63,
+ "windSpeed": 9.15,
+ "windDirection": 323.38,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T20:28:00Z",
+ "values": {
+ "temperatureMin": 45.57,
+ "temperatureMax": 45.57,
+ "windSpeed": 9.19,
+ "windDirection": 323.38,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T20:33:00Z",
+ "values": {
+ "temperatureMin": 45.5,
+ "temperatureMax": 45.5,
+ "windSpeed": 9.24,
+ "windDirection": 318.43,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T20:38:00Z",
+ "values": {
+ "temperatureMin": 45.45,
+ "temperatureMax": 45.45,
+ "windSpeed": 9.28,
+ "windDirection": 318.43,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T20:43:00Z",
+ "values": {
+ "temperatureMin": 45.39,
+ "temperatureMax": 45.39,
+ "windSpeed": 9.33,
+ "windDirection": 318.43,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T20:48:00Z",
+ "values": {
+ "temperatureMin": 45.32,
+ "temperatureMax": 45.32,
+ "windSpeed": 9.37,
+ "windDirection": 318.43,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T20:53:00Z",
+ "values": {
+ "temperatureMin": 45.27,
+ "temperatureMax": 45.27,
+ "windSpeed": 9.42,
+ "windDirection": 318.43,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T20:58:00Z",
+ "values": {
+ "temperatureMin": 45.19,
+ "temperatureMax": 45.19,
+ "windSpeed": 9.46,
+ "windDirection": 318.43,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T21:03:00Z",
+ "values": {
+ "temperatureMin": 45.14,
+ "temperatureMax": 45.14,
+ "windSpeed": 9.4,
+ "windDirection": 318.43,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T21:08:00Z",
+ "values": {
+ "temperatureMin": 45.07,
+ "temperatureMax": 45.07,
+ "windSpeed": 9.24,
+ "windDirection": 318.43,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T21:13:00Z",
+ "values": {
+ "temperatureMin": 45.01,
+ "temperatureMax": 45.01,
+ "windSpeed": 9.08,
+ "windDirection": 318.43,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T21:18:00Z",
+ "values": {
+ "temperatureMin": 44.94,
+ "temperatureMax": 44.94,
+ "windSpeed": 8.95,
+ "windDirection": 318.43,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T21:23:00Z",
+ "values": {
+ "temperatureMin": 44.89,
+ "temperatureMax": 44.89,
+ "windSpeed": 8.79,
+ "windDirection": 318.43,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T21:28:00Z",
+ "values": {
+ "temperatureMin": 44.82,
+ "temperatureMax": 44.82,
+ "windSpeed": 8.63,
+ "windDirection": 318.43,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T21:33:00Z",
+ "values": {
+ "temperatureMin": 44.76,
+ "temperatureMax": 44.76,
+ "windSpeed": 8.5,
+ "windDirection": 320.9,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T21:38:00Z",
+ "values": {
+ "temperatureMin": 44.69,
+ "temperatureMax": 44.69,
+ "windSpeed": 8.34,
+ "windDirection": 320.9,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T21:43:00Z",
+ "values": {
+ "temperatureMin": 44.64,
+ "temperatureMax": 44.64,
+ "windSpeed": 8.19,
+ "windDirection": 320.9,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T21:48:00Z",
+ "values": {
+ "temperatureMin": 44.56,
+ "temperatureMax": 44.56,
+ "windSpeed": 8.05,
+ "windDirection": 320.9,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T21:53:00Z",
+ "values": {
+ "temperatureMin": 44.51,
+ "temperatureMax": 44.51,
+ "windSpeed": 7.9,
+ "windDirection": 320.9,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T21:58:00Z",
+ "values": {
+ "temperatureMin": 44.44,
+ "temperatureMax": 44.44,
+ "windSpeed": 7.74,
+ "windDirection": 320.9,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T22:03:00Z",
+ "values": {
+ "temperatureMin": 44.26,
+ "temperatureMax": 44.26,
+ "windSpeed": 7.47,
+ "windDirection": 320.9,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T22:08:00Z",
+ "values": {
+ "temperatureMin": 44.01,
+ "temperatureMax": 44.01,
+ "windSpeed": 7.14,
+ "windDirection": 320.9,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T22:13:00Z",
+ "values": {
+ "temperatureMin": 43.74,
+ "temperatureMax": 43.74,
+ "windSpeed": 6.78,
+ "windDirection": 320.9,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T22:18:00Z",
+ "values": {
+ "temperatureMin": 43.48,
+ "temperatureMax": 43.48,
+ "windSpeed": 6.44,
+ "windDirection": 320.9,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T22:23:00Z",
+ "values": {
+ "temperatureMin": 43.23,
+ "temperatureMax": 43.23,
+ "windSpeed": 6.08,
+ "windDirection": 320.9,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T22:28:00Z",
+ "values": {
+ "temperatureMin": 42.98,
+ "temperatureMax": 42.98,
+ "windSpeed": 5.75,
+ "windDirection": 320.9,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T22:33:00Z",
+ "values": {
+ "temperatureMin": 42.71,
+ "temperatureMax": 42.71,
+ "windSpeed": 5.39,
+ "windDirection": 322.11,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T22:38:00Z",
+ "values": {
+ "temperatureMin": 42.46,
+ "temperatureMax": 42.46,
+ "windSpeed": 5.06,
+ "windDirection": 322.11,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T22:43:00Z",
+ "values": {
+ "temperatureMin": 42.21,
+ "temperatureMax": 42.21,
+ "windSpeed": 4.7,
+ "windDirection": 322.11,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T22:48:00Z",
+ "values": {
+ "temperatureMin": 41.94,
+ "temperatureMax": 41.94,
+ "windSpeed": 4.36,
+ "windDirection": 322.11,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T22:53:00Z",
+ "values": {
+ "temperatureMin": 41.68,
+ "temperatureMax": 41.68,
+ "windSpeed": 4,
+ "windDirection": 322.11,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T22:58:00Z",
+ "values": {
+ "temperatureMin": 41.43,
+ "temperatureMax": 41.43,
+ "windSpeed": 3.67,
+ "windDirection": 322.11,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T23:03:00Z",
+ "values": {
+ "temperatureMin": 41.16,
+ "temperatureMax": 41.16,
+ "windSpeed": 3.6,
+ "windDirection": 322.11,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T23:08:00Z",
+ "values": {
+ "temperatureMin": 40.91,
+ "temperatureMax": 40.91,
+ "windSpeed": 3.76,
+ "windDirection": 322.11,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T23:13:00Z",
+ "values": {
+ "temperatureMin": 40.66,
+ "temperatureMax": 40.66,
+ "windSpeed": 3.91,
+ "windDirection": 322.11,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T23:18:00Z",
+ "values": {
+ "temperatureMin": 40.41,
+ "temperatureMax": 40.41,
+ "windSpeed": 4.05,
+ "windDirection": 322.11,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T23:23:00Z",
+ "values": {
+ "temperatureMin": 40.14,
+ "temperatureMax": 40.14,
+ "windSpeed": 4.21,
+ "windDirection": 322.11,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T23:28:00Z",
+ "values": {
+ "temperatureMin": 39.88,
+ "temperatureMax": 39.88,
+ "windSpeed": 4.36,
+ "windDirection": 322.11,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T23:33:00Z",
+ "values": {
+ "temperatureMin": 39.63,
+ "temperatureMax": 39.63,
+ "windSpeed": 4.5,
+ "windDirection": 295.94,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T23:38:00Z",
+ "values": {
+ "temperatureMin": 39.38,
+ "temperatureMax": 39.38,
+ "windSpeed": 4.65,
+ "windDirection": 295.94,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T23:43:00Z",
+ "values": {
+ "temperatureMin": 39.11,
+ "temperatureMax": 39.11,
+ "windSpeed": 4.79,
+ "windDirection": 295.94,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ }
+ ],
+ "hourly": [
+ {
+ "startTime": "2021-03-07T17:48:00Z",
+ "values": {
+ "temperatureMin": 44.13,
+ "temperatureMax": 44.13,
+ "windSpeed": 9.33,
+ "windDirection": 315.14,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T18:48:00Z",
+ "values": {
+ "temperatureMin": 44.82,
+ "temperatureMax": 44.82,
+ "windSpeed": 8.84,
+ "windDirection": 321.71,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T19:48:00Z",
+ "values": {
+ "temperatureMin": 45.75,
+ "temperatureMax": 45.75,
+ "windSpeed": 8.9,
+ "windDirection": 323.38,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T20:48:00Z",
+ "values": {
+ "temperatureMin": 45.32,
+ "temperatureMax": 45.32,
+ "windSpeed": 9.37,
+ "windDirection": 318.43,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T21:48:00Z",
+ "values": {
+ "temperatureMin": 44.56,
+ "temperatureMax": 44.56,
+ "windSpeed": 8.05,
+ "windDirection": 320.9,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T22:48:00Z",
+ "values": {
+ "temperatureMin": 41.94,
+ "temperatureMax": 41.94,
+ "windSpeed": 4.36,
+ "windDirection": 322.11,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-07T23:48:00Z",
+ "values": {
+ "temperatureMin": 38.86,
+ "temperatureMax": 38.86,
+ "windSpeed": 4.94,
+ "windDirection": 295.94,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T00:48:00Z",
+ "values": {
+ "temperatureMin": 36.18,
+ "temperatureMax": 36.18,
+ "windSpeed": 5.59,
+ "windDirection": 11.94,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T01:48:00Z",
+ "values": {
+ "temperatureMin": 34.3,
+ "temperatureMax": 34.3,
+ "windSpeed": 5.57,
+ "windDirection": 13.68,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T02:48:00Z",
+ "values": {
+ "temperatureMin": 32.88,
+ "temperatureMax": 32.88,
+ "windSpeed": 5.41,
+ "windDirection": 14.93,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T03:48:00Z",
+ "values": {
+ "temperatureMin": 31.91,
+ "temperatureMax": 31.91,
+ "windSpeed": 4.61,
+ "windDirection": 26.07,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T04:48:00Z",
+ "values": {
+ "temperatureMin": 29.17,
+ "temperatureMax": 29.17,
+ "windSpeed": 2.59,
+ "windDirection": 51.27,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T05:48:00Z",
+ "values": {
+ "temperatureMin": 27.37,
+ "temperatureMax": 27.37,
+ "windSpeed": 3.31,
+ "windDirection": 343.25,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T06:48:00Z",
+ "values": {
+ "temperatureMin": 26.73,
+ "temperatureMax": 26.73,
+ "windSpeed": 4.27,
+ "windDirection": 341.46,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T07:48:00Z",
+ "values": {
+ "temperatureMin": 26.38,
+ "temperatureMax": 26.38,
+ "windSpeed": 3.53,
+ "windDirection": 322.34,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T08:48:00Z",
+ "values": {
+ "temperatureMin": 26.15,
+ "temperatureMax": 26.15,
+ "windSpeed": 3.65,
+ "windDirection": 294.69,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T09:48:00Z",
+ "values": {
+ "temperatureMin": 30.07,
+ "temperatureMax": 30.07,
+ "windSpeed": 3.2,
+ "windDirection": 325.32,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T10:48:00Z",
+ "values": {
+ "temperatureMin": 31.03,
+ "temperatureMax": 31.03,
+ "windSpeed": 2.84,
+ "windDirection": 322.27,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T11:48:00Z",
+ "values": {
+ "temperatureMin": 27.23,
+ "temperatureMax": 27.23,
+ "windSpeed": 5.59,
+ "windDirection": 310.14,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T12:48:00Z",
+ "values": {
+ "temperatureMin": 29.21,
+ "temperatureMax": 29.21,
+ "windSpeed": 7.05,
+ "windDirection": 324.8,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T13:48:00Z",
+ "values": {
+ "temperatureMin": 33.19,
+ "temperatureMax": 33.19,
+ "windSpeed": 6.46,
+ "windDirection": 335.16,
+ "weatherCode": 1101,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T14:48:00Z",
+ "values": {
+ "temperatureMin": 37.02,
+ "temperatureMax": 37.02,
+ "windSpeed": 5.88,
+ "windDirection": 324.49,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T15:48:00Z",
+ "values": {
+ "temperatureMin": 40.01,
+ "temperatureMax": 40.01,
+ "windSpeed": 5.55,
+ "windDirection": 310.68,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T16:48:00Z",
+ "values": {
+ "temperatureMin": 42.37,
+ "temperatureMax": 42.37,
+ "windSpeed": 5.46,
+ "windDirection": 304.18,
+ "weatherCode": 1101,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T17:48:00Z",
+ "values": {
+ "temperatureMin": 44.62,
+ "temperatureMax": 44.62,
+ "windSpeed": 4.99,
+ "windDirection": 301.19,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T18:48:00Z",
+ "values": {
+ "temperatureMin": 46.78,
+ "temperatureMax": 46.78,
+ "windSpeed": 4.72,
+ "windDirection": 295.05,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T19:48:00Z",
+ "values": {
+ "temperatureMin": 48.42,
+ "temperatureMax": 48.42,
+ "windSpeed": 4.81,
+ "windDirection": 287.4,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T20:48:00Z",
+ "values": {
+ "temperatureMin": 49.28,
+ "temperatureMax": 49.28,
+ "windSpeed": 4.74,
+ "windDirection": 282.48,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T21:48:00Z",
+ "values": {
+ "temperatureMin": 48.72,
+ "temperatureMax": 48.72,
+ "windSpeed": 2.51,
+ "windDirection": 268.74,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T22:48:00Z",
+ "values": {
+ "temperatureMin": 44.37,
+ "temperatureMax": 44.37,
+ "windSpeed": 3.56,
+ "windDirection": 180.04,
+ "weatherCode": 1101,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T23:48:00Z",
+ "values": {
+ "temperatureMin": 39.9,
+ "temperatureMax": 39.9,
+ "windSpeed": 4.68,
+ "windDirection": 177.89,
+ "weatherCode": 1101,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T00:48:00Z",
+ "values": {
+ "temperatureMin": 37.87,
+ "temperatureMax": 37.87,
+ "windSpeed": 5.21,
+ "windDirection": 197.47,
+ "weatherCode": 1101,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T01:48:00Z",
+ "values": {
+ "temperatureMin": 36.91,
+ "temperatureMax": 36.91,
+ "windSpeed": 5.46,
+ "windDirection": 209.77,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T02:48:00Z",
+ "values": {
+ "temperatureMin": 36.64,
+ "temperatureMax": 36.64,
+ "windSpeed": 6.11,
+ "windDirection": 210.14,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T03:48:00Z",
+ "values": {
+ "temperatureMin": 36.63,
+ "temperatureMax": 36.63,
+ "windSpeed": 6.4,
+ "windDirection": 216,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T04:48:00Z",
+ "values": {
+ "temperatureMin": 36.23,
+ "temperatureMax": 36.23,
+ "windSpeed": 6.22,
+ "windDirection": 223.92,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T05:48:00Z",
+ "values": {
+ "temperatureMin": 35.58,
+ "temperatureMax": 35.58,
+ "windSpeed": 5.75,
+ "windDirection": 229.68,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T06:48:00Z",
+ "values": {
+ "temperatureMin": 34.68,
+ "temperatureMax": 34.68,
+ "windSpeed": 5.21,
+ "windDirection": 235.24,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T07:48:00Z",
+ "values": {
+ "temperatureMin": 33.69,
+ "temperatureMax": 33.69,
+ "windSpeed": 4.81,
+ "windDirection": 237.24,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T08:48:00Z",
+ "values": {
+ "temperatureMin": 32.74,
+ "temperatureMax": 32.74,
+ "windSpeed": 4.52,
+ "windDirection": 239.35,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T09:48:00Z",
+ "values": {
+ "temperatureMin": 32.05,
+ "temperatureMax": 32.05,
+ "windSpeed": 4.32,
+ "windDirection": 245.68,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T10:48:00Z",
+ "values": {
+ "temperatureMin": 31.57,
+ "temperatureMax": 31.57,
+ "windSpeed": 4.14,
+ "windDirection": 248.11,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T11:48:00Z",
+ "values": {
+ "temperatureMin": 32.92,
+ "temperatureMax": 32.92,
+ "windSpeed": 4.32,
+ "windDirection": 249.54,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T12:48:00Z",
+ "values": {
+ "temperatureMin": 38.5,
+ "temperatureMax": 38.5,
+ "windSpeed": 4.7,
+ "windDirection": 253.3,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T13:48:00Z",
+ "values": {
+ "temperatureMin": 46.08,
+ "temperatureMax": 46.08,
+ "windSpeed": 4.41,
+ "windDirection": 258.49,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T14:48:00Z",
+ "values": {
+ "temperatureMin": 53.26,
+ "temperatureMax": 53.26,
+ "windSpeed": 4.9,
+ "windDirection": 260.49,
+ "weatherCode": 1101,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T15:48:00Z",
+ "values": {
+ "temperatureMin": 58.15,
+ "temperatureMax": 58.15,
+ "windSpeed": 5.55,
+ "windDirection": 261.29,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T16:48:00Z",
+ "values": {
+ "temperatureMin": 61.56,
+ "temperatureMax": 61.56,
+ "windSpeed": 6.35,
+ "windDirection": 264.3,
+ "weatherCode": 1101,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T17:48:00Z",
+ "values": {
+ "temperatureMin": 64,
+ "temperatureMax": 64,
+ "windSpeed": 6.6,
+ "windDirection": 257.54,
+ "weatherCode": 1101,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T18:48:00Z",
+ "values": {
+ "temperatureMin": 65.79,
+ "temperatureMax": 65.79,
+ "windSpeed": 6.96,
+ "windDirection": 253.12,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T19:48:00Z",
+ "values": {
+ "temperatureMin": 66.74,
+ "temperatureMax": 66.74,
+ "windSpeed": 6.8,
+ "windDirection": 259.46,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T20:48:00Z",
+ "values": {
+ "temperatureMin": 66.96,
+ "temperatureMax": 66.96,
+ "windSpeed": 6.33,
+ "windDirection": 294.25,
+ "weatherCode": 1101,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T21:48:00Z",
+ "values": {
+ "temperatureMin": 64.35,
+ "temperatureMax": 64.35,
+ "windSpeed": 3.91,
+ "windDirection": 279.37,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T22:48:00Z",
+ "values": {
+ "temperatureMin": 61.07,
+ "temperatureMax": 61.07,
+ "windSpeed": 3.65,
+ "windDirection": 218.19,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T23:48:00Z",
+ "values": {
+ "temperatureMin": 56.3,
+ "temperatureMax": 56.3,
+ "windSpeed": 4.09,
+ "windDirection": 208.3,
+ "weatherCode": 1101,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T00:48:00Z",
+ "values": {
+ "temperatureMin": 53.19,
+ "temperatureMax": 53.19,
+ "windSpeed": 4.21,
+ "windDirection": 216.42,
+ "weatherCode": 1102,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T01:48:00Z",
+ "values": {
+ "temperatureMin": 51.94,
+ "temperatureMax": 51.94,
+ "windSpeed": 3.38,
+ "windDirection": 257.19,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T02:48:00Z",
+ "values": {
+ "temperatureMin": 49.82,
+ "temperatureMax": 49.82,
+ "windSpeed": 2.71,
+ "windDirection": 288.85,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T03:48:00Z",
+ "values": {
+ "temperatureMin": 48.24,
+ "temperatureMax": 48.24,
+ "windSpeed": 2.8,
+ "windDirection": 334.41,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T04:48:00Z",
+ "values": {
+ "temperatureMin": 47.44,
+ "temperatureMax": 47.44,
+ "windSpeed": 2.26,
+ "windDirection": 342.01,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T05:48:00Z",
+ "values": {
+ "temperatureMin": 45.59,
+ "temperatureMax": 45.59,
+ "windSpeed": 2.35,
+ "windDirection": 2.43,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T06:48:00Z",
+ "values": {
+ "temperatureMin": 43.43,
+ "temperatureMax": 43.43,
+ "windSpeed": 2.3,
+ "windDirection": 336.56,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T07:48:00Z",
+ "values": {
+ "temperatureMin": 41.11,
+ "temperatureMax": 41.11,
+ "windSpeed": 2.71,
+ "windDirection": 4.41,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T08:48:00Z",
+ "values": {
+ "temperatureMin": 39.58,
+ "temperatureMax": 39.58,
+ "windSpeed": 3.4,
+ "windDirection": 21.26,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T09:48:00Z",
+ "values": {
+ "temperatureMin": 39.85,
+ "temperatureMax": 39.85,
+ "windSpeed": 3.31,
+ "windDirection": 22.76,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T10:48:00Z",
+ "values": {
+ "temperatureMin": 37.85,
+ "temperatureMax": 37.85,
+ "windSpeed": 4.03,
+ "windDirection": 29.3,
+ "weatherCode": 1101,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T11:48:00Z",
+ "values": {
+ "temperatureMin": 38.97,
+ "temperatureMax": 38.97,
+ "windSpeed": 3.15,
+ "windDirection": 21.82,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T12:48:00Z",
+ "values": {
+ "temperatureMin": 44.31,
+ "temperatureMax": 44.31,
+ "windSpeed": 3.53,
+ "windDirection": 14.25,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T13:48:00Z",
+ "values": {
+ "temperatureMin": 50.25,
+ "temperatureMax": 50.25,
+ "windSpeed": 2.82,
+ "windDirection": 42.41,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T14:48:00Z",
+ "values": {
+ "temperatureMin": 54.97,
+ "temperatureMax": 54.97,
+ "windSpeed": 2.53,
+ "windDirection": 87.81,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T15:48:00Z",
+ "values": {
+ "temperatureMin": 58.46,
+ "temperatureMax": 58.46,
+ "windSpeed": 3.09,
+ "windDirection": 125.82,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T16:48:00Z",
+ "values": {
+ "temperatureMin": 61.21,
+ "temperatureMax": 61.21,
+ "windSpeed": 4.03,
+ "windDirection": 157.54,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T17:48:00Z",
+ "values": {
+ "temperatureMin": 63.36,
+ "temperatureMax": 63.36,
+ "windSpeed": 5.21,
+ "windDirection": 166.66,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T18:48:00Z",
+ "values": {
+ "temperatureMin": 64.83,
+ "temperatureMax": 64.83,
+ "windSpeed": 6.93,
+ "windDirection": 189.24,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T19:48:00Z",
+ "values": {
+ "temperatureMin": 65.23,
+ "temperatureMax": 65.23,
+ "windSpeed": 8.95,
+ "windDirection": 194.58,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T20:48:00Z",
+ "values": {
+ "temperatureMin": 64.98,
+ "temperatureMax": 64.98,
+ "windSpeed": 9.4,
+ "windDirection": 193.22,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T21:48:00Z",
+ "values": {
+ "temperatureMin": 64.06,
+ "temperatureMax": 64.06,
+ "windSpeed": 8.55,
+ "windDirection": 186.39,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T22:48:00Z",
+ "values": {
+ "temperatureMin": 61.9,
+ "temperatureMax": 61.9,
+ "windSpeed": 7.49,
+ "windDirection": 171.81,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T23:48:00Z",
+ "values": {
+ "temperatureMin": 59.4,
+ "temperatureMax": 59.4,
+ "windSpeed": 7.54,
+ "windDirection": 165.51,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T00:48:00Z",
+ "values": {
+ "temperatureMin": 57.63,
+ "temperatureMax": 57.63,
+ "windSpeed": 8.12,
+ "windDirection": 171.94,
+ "weatherCode": 1102,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T01:48:00Z",
+ "values": {
+ "temperatureMin": 56.17,
+ "temperatureMax": 56.17,
+ "windSpeed": 8.7,
+ "windDirection": 176.84,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T02:48:00Z",
+ "values": {
+ "temperatureMin": 55.36,
+ "temperatureMax": 55.36,
+ "windSpeed": 9.42,
+ "windDirection": 184.14,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T03:48:00Z",
+ "values": {
+ "temperatureMin": 54.88,
+ "temperatureMax": 54.88,
+ "windSpeed": 10,
+ "windDirection": 195.54,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T04:48:00Z",
+ "values": {
+ "temperatureMin": 54.14,
+ "temperatureMax": 54.14,
+ "windSpeed": 10.4,
+ "windDirection": 200.56,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T05:48:00Z",
+ "values": {
+ "temperatureMin": 53.46,
+ "temperatureMax": 53.46,
+ "windSpeed": 10.04,
+ "windDirection": 198.08,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T06:48:00Z",
+ "values": {
+ "temperatureMin": 52.11,
+ "temperatureMax": 52.11,
+ "windSpeed": 10.02,
+ "windDirection": 199.54,
+ "weatherCode": 1101,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T07:48:00Z",
+ "values": {
+ "temperatureMin": 51.64,
+ "temperatureMax": 51.64,
+ "windSpeed": 10.51,
+ "windDirection": 202.73,
+ "weatherCode": 1102,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T08:48:00Z",
+ "values": {
+ "temperatureMin": 50.79,
+ "temperatureMax": 50.79,
+ "windSpeed": 10.38,
+ "windDirection": 203.35,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T09:48:00Z",
+ "values": {
+ "temperatureMin": 49.93,
+ "temperatureMax": 49.93,
+ "windSpeed": 9.51,
+ "windDirection": 210.36,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T10:48:00Z",
+ "values": {
+ "temperatureMin": 49.1,
+ "temperatureMax": 49.1,
+ "windSpeed": 8.61,
+ "windDirection": 210.6,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T11:48:00Z",
+ "values": {
+ "temperatureMin": 48.42,
+ "temperatureMax": 48.42,
+ "windSpeed": 9.15,
+ "windDirection": 211.29,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T12:48:00Z",
+ "values": {
+ "temperatureMin": 48.9,
+ "temperatureMax": 48.9,
+ "windSpeed": 10.25,
+ "windDirection": 215.59,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T13:48:00Z",
+ "values": {
+ "temperatureMin": 50.54,
+ "temperatureMax": 50.54,
+ "windSpeed": 10.18,
+ "windDirection": 215.48,
+ "weatherCode": 1102,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T14:48:00Z",
+ "values": {
+ "temperatureMin": 53.19,
+ "temperatureMax": 53.19,
+ "windSpeed": 9.4,
+ "windDirection": 208.76,
+ "weatherCode": 1101,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T15:48:00Z",
+ "values": {
+ "temperatureMin": 56.19,
+ "temperatureMax": 56.19,
+ "windSpeed": 9.73,
+ "windDirection": 197.59,
+ "weatherCode": 1101,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T16:48:00Z",
+ "values": {
+ "temperatureMin": 59.34,
+ "temperatureMax": 59.34,
+ "windSpeed": 10.69,
+ "windDirection": 204.29,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T17:48:00Z",
+ "values": {
+ "temperatureMin": 62.35,
+ "temperatureMax": 62.35,
+ "windSpeed": 11.81,
+ "windDirection": 204.56,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T18:48:00Z",
+ "values": {
+ "temperatureMin": 64.6,
+ "temperatureMax": 64.6,
+ "windSpeed": 13.09,
+ "windDirection": 206.85,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T19:48:00Z",
+ "values": {
+ "temperatureMin": 65.91,
+ "temperatureMax": 65.91,
+ "windSpeed": 13.82,
+ "windDirection": 204.82,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T20:48:00Z",
+ "values": {
+ "temperatureMin": 66.22,
+ "temperatureMax": 66.22,
+ "windSpeed": 14.54,
+ "windDirection": 208.43,
+ "weatherCode": 1100,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T21:48:00Z",
+ "values": {
+ "temperatureMin": 65.46,
+ "temperatureMax": 65.46,
+ "windSpeed": 13.2,
+ "windDirection": 208.3,
+ "weatherCode": 1101,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T22:48:00Z",
+ "values": {
+ "temperatureMin": 64.35,
+ "temperatureMax": 64.35,
+ "windSpeed": 12.35,
+ "windDirection": 208.58,
+ "weatherCode": 1101,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T23:48:00Z",
+ "values": {
+ "temperatureMin": 62.85,
+ "temperatureMax": 62.85,
+ "windSpeed": 12.86,
+ "windDirection": 205.39,
+ "weatherCode": 1101,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-12T00:48:00Z",
+ "values": {
+ "temperatureMin": 61.75,
+ "temperatureMax": 61.75,
+ "windSpeed": 14.7,
+ "windDirection": 209.51,
+ "weatherCode": 1102,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-12T01:48:00Z",
+ "values": {
+ "temperatureMin": 61.2,
+ "temperatureMax": 61.2,
+ "windSpeed": 15.57,
+ "windDirection": 211.47,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-12T02:48:00Z",
+ "values": {
+ "temperatureMin": 60.46,
+ "temperatureMax": 60.46,
+ "windSpeed": 14.94,
+ "windDirection": 211.57,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-12T03:48:00Z",
+ "values": {
+ "temperatureMin": 59.94,
+ "temperatureMax": 59.94,
+ "windSpeed": 14.29,
+ "windDirection": 208.93,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-12T04:48:00Z",
+ "values": {
+ "temperatureMin": 59.52,
+ "temperatureMax": 59.52,
+ "windSpeed": 14.36,
+ "windDirection": 217.91,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ }
+ ],
+ "daily": [
+ {
+ "startTime": "2021-03-07T11:00:00Z",
+ "values": {
+ "temperatureMin": 26.11,
+ "temperatureMax": 45.93,
+ "windSpeed": 9.49,
+ "windDirection": 239.6,
+ "weatherCode": 1000,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-08T11:00:00Z",
+ "values": {
+ "temperatureMin": 26.28,
+ "temperatureMax": 49.42,
+ "windSpeed": 7.24,
+ "windDirection": 262.82,
+ "weatherCode": 1102,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-09T11:00:00Z",
+ "values": {
+ "temperatureMin": 31.48,
+ "temperatureMax": 66.98,
+ "windSpeed": 7.05,
+ "windDirection": 229.3,
+ "weatherCode": 1102,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-10T11:00:00Z",
+ "values": {
+ "temperatureMin": 37.32,
+ "temperatureMax": 65.28,
+ "windSpeed": 10.64,
+ "windDirection": 149.91,
+ "weatherCode": 1102,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-11T11:00:00Z",
+ "values": {
+ "temperatureMin": 48.29,
+ "temperatureMax": 66.25,
+ "windSpeed": 15.69,
+ "windDirection": 210.45,
+ "weatherCode": 1102,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-12T11:00:00Z",
+ "values": {
+ "temperatureMin": 53.83,
+ "temperatureMax": 67.91,
+ "windSpeed": 12.3,
+ "windDirection": 217.98,
+ "weatherCode": 4000,
+ "precipitationIntensityAvg": 0.0002,
+ "precipitationProbability": 25
+ }
+ },
+ {
+ "startTime": "2021-03-13T11:00:00Z",
+ "values": {
+ "temperatureMin": 42.91,
+ "temperatureMax": 54.48,
+ "windSpeed": 9.72,
+ "windDirection": 58.79,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 25
+ }
+ },
+ {
+ "startTime": "2021-03-14T10:00:00Z",
+ "values": {
+ "temperatureMin": 33.35,
+ "temperatureMax": 42.91,
+ "windSpeed": 16.25,
+ "windDirection": 70.25,
+ "weatherCode": 5101,
+ "precipitationIntensityAvg": 0.0393,
+ "precipitationProbability": 95
+ }
+ },
+ {
+ "startTime": "2021-03-15T10:00:00Z",
+ "values": {
+ "temperatureMin": 29.35,
+ "temperatureMax": 43.67,
+ "windSpeed": 15.89,
+ "windDirection": 84.47,
+ "weatherCode": 5001,
+ "precipitationIntensityAvg": 0.0024,
+ "precipitationProbability": 55
+ }
+ },
+ {
+ "startTime": "2021-03-16T10:00:00Z",
+ "values": {
+ "temperatureMin": 29.1,
+ "temperatureMax": 43,
+ "windSpeed": 6.71,
+ "windDirection": 103.85,
+ "weatherCode": 1102,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-17T10:00:00Z",
+ "values": {
+ "temperatureMin": 34.32,
+ "temperatureMax": 52.4,
+ "windSpeed": 7.27,
+ "windDirection": 145.41,
+ "weatherCode": 1102,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 0
+ }
+ },
+ {
+ "startTime": "2021-03-18T10:00:00Z",
+ "values": {
+ "temperatureMin": 41.32,
+ "temperatureMax": 54.07,
+ "windSpeed": 6.58,
+ "windDirection": 62.99,
+ "weatherCode": 1001,
+ "precipitationIntensityAvg": 0,
+ "precipitationProbability": 10
+ }
+ },
+ {
+ "startTime": "2021-03-19T10:00:00Z",
+ "values": {
+ "temperatureMin": 39.4,
+ "temperatureMax": 48.94,
+ "windSpeed": 13.91,
+ "windDirection": 68.54,
+ "weatherCode": 4000,
+ "precipitationIntensityAvg": 0.0048,
+ "precipitationProbability": 55
+ }
+ },
+ {
+ "startTime": "2021-03-20T10:00:00Z",
+ "values": {
+ "temperatureMin": 35.06,
+ "temperatureMax": 40.12,
+ "windSpeed": 17.35,
+ "windDirection": 56.98,
+ "weatherCode": 5001,
+ "precipitationIntensityAvg": 0.002,
+ "precipitationProbability": 33.3
+ }
+ },
+ {
+ "startTime": "2021-03-21T10:00:00Z",
+ "values": {
+ "temperatureMin": 33.66,
+ "temperatureMax": 66.54,
+ "windSpeed": 15.93,
+ "windDirection": 82.57,
+ "weatherCode": 5001,
+ "precipitationIntensityAvg": 0.0004,
+ "precipitationProbability": 45
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/tests/fixtures/flo/device_info_response_detector.json b/tests/fixtures/flo/device_info_response_detector.json
new file mode 100644
index 00000000000000..aac24ab5e68e50
--- /dev/null
+++ b/tests/fixtures/flo/device_info_response_detector.json
@@ -0,0 +1,161 @@
+{
+ "actionRules": [],
+ "battery": {
+ "level": 100,
+ "updated": "2021-03-01T12:05:00Z"
+ },
+ "connectivity": {
+ "ssid": "SOMESSID"
+ },
+ "deviceModel": "puck_v1",
+ "deviceType": "puck_oem",
+ "fwProperties": {
+ "alert_battery_active": false,
+ "alert_humidity_high_active": false,
+ "alert_humidity_high_count": 0,
+ "alert_humidity_low_active": false,
+ "alert_humidity_low_count": 1,
+ "alert_state": "inactive",
+ "alert_temperature_high_active": false,
+ "alert_temperature_high_count": 0,
+ "alert_temperature_low_active": false,
+ "alert_temperature_low_count": 0,
+ "alert_water_active": false,
+ "alert_water_count": 0,
+ "ap_mode_count": 1,
+ "beep_pattern": "off",
+ "button_click_count": 1,
+ "date": "2021-03-07T14:00:05.054Z",
+ "deep_sleep_count": 8229,
+ "device_boot_count": 25,
+ "device_boot_reason": "wakeup_timer",
+ "device_count": 8230,
+ "device_failed_count": 36,
+ "device_id": "1a2b3c4d5e6f",
+ "device_time_total": 405336,
+ "device_time_up": 1502,
+ "device_uuid": "32839",
+ "device_wakeup_count": 8254,
+ "flosense_shut_off_level": 2,
+ "fw_name": "1.1.15",
+ "fw_version": 10115,
+ "led_pattern": "led_blue_solid",
+ "limit_ota_battery_min": 30,
+ "pairing_state": "configured",
+ "reason": "heartbeat",
+ "serial_number": "111111111112",
+ "telemetry_battery_percent": 100,
+ "telemetry_battery_voltage": 2.9896278381347656,
+ "telemetry_count": 8224,
+ "telemetry_failed_count": 27,
+ "telemetry_humidity": 43.21965408325195,
+ "telemetry_rssi": 100,
+ "telemetry_temperature": 61.43144607543945,
+ "telemetry_water": false,
+ "timer_alarm_active": 10,
+ "timer_heartbeat_battery_low": 3600,
+ "timer_heartbeat_battery_ok": 1740,
+ "timer_heartbeat_last": 1740,
+ "timer_heartbeat_not_configured": 10,
+ "timer_heartbeat_retry_attempts": 3,
+ "timer_heartbeat_retry_delay": 600,
+ "timer_water_debounce": 2000,
+ "timer_wifi_ap_timeout": 600000,
+ "wifi_ap_ssid": "FloDetector-a123",
+ "wifi_sta_enc": "wpa2-psk",
+ "wifi_sta_failed_count": 21,
+ "wifi_sta_mac": "50:01:01:01:01:44",
+ "wifi_sta_ssid": "SOMESSID"
+ },
+ "fwVersion": "1.1.15",
+ "hardwareThresholds": {
+ "battery": {
+ "maxValue": 100,
+ "minValue": 0,
+ "okMax": 100,
+ "okMin": 20
+ },
+ "batteryEnabled": true,
+ "humidity": {
+ "maxValue": 100,
+ "minValue": 0,
+ "okMax": 85,
+ "okMin": 15
+ },
+ "humidityEnabled": true,
+ "tempC": {
+ "maxValue": 60,
+ "minValue": -17.77777777777778,
+ "okMax": 37.77777777777778,
+ "okMin": 0
+ },
+ "tempEnabled": true,
+ "tempF": {
+ "maxValue": 140,
+ "minValue": 0,
+ "okMax": 100,
+ "okMin": 32
+ }
+ },
+ "id": "32839",
+ "installStatus": {
+ "isInstalled": false
+ },
+ "isConnected": true,
+ "isPaired": true,
+ "lastHeardFromTime": "2021-03-07T14:05:00Z",
+ "location": {
+ "id": "mmnnoopp"
+ },
+ "macAddress": "1a2b3c4d5e6f",
+ "nickname": "Kitchen Sink",
+ "notifications": {
+ "pending": {
+ "alarmCount": [],
+ "critical": {
+ "count": 0,
+ "devices": {
+ "absolute": 0,
+ "count": 0
+ }
+ },
+ "criticalCount": 0,
+ "info": {
+ "count": 0,
+ "devices": {
+ "absolute": 0,
+ "count": 0
+ }
+ },
+ "infoCount": 0,
+ "warning": {
+ "count": 0,
+ "devices": {
+ "absolute": 0,
+ "count": 0
+ }
+ },
+ "warningCount": 0
+ }
+ },
+ "puckConfig": {
+ "configuredAt": "2020-09-01T18:15:12.216Z",
+ "isConfigured": true
+ },
+ "serialNumber": "111111111112",
+ "shutoff": {
+ "scheduledAt": "1970-01-01T00:00:00.000Z"
+ },
+ "systemMode": {
+ "isLocked": false,
+ "shouldInherit": true
+ },
+ "telemetry": {
+ "current": {
+ "humidity": 43,
+ "tempF": 61,
+ "updated": "2021-03-07T14:05:00Z"
+ }
+ },
+ "valve": {}
+}
diff --git a/tests/fixtures/flo/location_info_base_response.json b/tests/fixtures/flo/location_info_base_response.json
index f6840a0742bf90..a5a25da2d6c297 100644
--- a/tests/fixtures/flo/location_info_base_response.json
+++ b/tests/fixtures/flo/location_info_base_response.json
@@ -9,6 +9,10 @@
{
"id": "98765",
"macAddress": "123456abcdef"
+ },
+ {
+ "id": "32839",
+ "macAddress": "1a2b3c4d5e6f"
}
],
"userRoles": [
diff --git a/tests/fixtures/flo/user_info_expand_locations_response.json b/tests/fixtures/flo/user_info_expand_locations_response.json
index 829596b6849f27..18643e049baecc 100644
--- a/tests/fixtures/flo/user_info_expand_locations_response.json
+++ b/tests/fixtures/flo/user_info_expand_locations_response.json
@@ -19,6 +19,10 @@
{
"id": "98765",
"macAddress": "606405c11e10"
+ },
+ {
+ "id": "32839",
+ "macAddress": "1a2b3c4d5e6f"
}
],
"userRoles": [
diff --git a/tests/fixtures/mazda/get_vehicle_status.json b/tests/fixtures/mazda/get_vehicle_status.json
new file mode 100644
index 00000000000000..f170b222b318fe
--- /dev/null
+++ b/tests/fixtures/mazda/get_vehicle_status.json
@@ -0,0 +1,37 @@
+{
+ "lastUpdatedTimestamp": "20210123143809",
+ "latitude": 1.234567,
+ "longitude": -2.345678,
+ "positionTimestamp": "20210123143808",
+ "fuelRemainingPercent": 87.0,
+ "fuelDistanceRemainingKm": 380.8,
+ "odometerKm": 2795.8,
+ "doors": {
+ "driverDoorOpen": false,
+ "passengerDoorOpen": false,
+ "rearLeftDoorOpen": false,
+ "rearRightDoorOpen": false,
+ "trunkOpen": false,
+ "hoodOpen": false,
+ "fuelLidOpen": false
+ },
+ "doorLocks": {
+ "driverDoorUnlocked": false,
+ "passengerDoorUnlocked": false,
+ "rearLeftDoorUnlocked": false,
+ "rearRightDoorUnlocked": false
+ },
+ "windows": {
+ "driverWindowOpen": false,
+ "passengerWindowOpen": false,
+ "rearLeftWindowOpen": false,
+ "rearRightWindowOpen": false
+ },
+ "hazardLightsOn": false,
+ "tirePressure": {
+ "frontLeftTirePressurePsi": 35.0,
+ "frontRightTirePressurePsi": 35.0,
+ "rearLeftTirePressurePsi": 33.0,
+ "rearRightTirePressurePsi": 33.0
+ }
+}
\ No newline at end of file
diff --git a/tests/fixtures/mazda/get_vehicles.json b/tests/fixtures/mazda/get_vehicles.json
new file mode 100644
index 00000000000000..871eeb9d2ecd06
--- /dev/null
+++ b/tests/fixtures/mazda/get_vehicles.json
@@ -0,0 +1,17 @@
+[
+ {
+ "vin": "JM000000000000000",
+ "id": 12345,
+ "nickname": "My Mazda3",
+ "carlineCode": "M3S",
+ "carlineName": "MAZDA3 2.5 S SE AWD",
+ "modelYear": "2021",
+ "modelCode": "M3S SE XA",
+ "modelName": "W/ SELECT PKG AWD SDN",
+ "automaticTransmission": true,
+ "interiorColorCode": "BY3",
+ "interiorColorName": "BLACK",
+ "exteriorColorCode": "42M",
+ "exteriorColorName": "DEEP CRYSTAL BLUE MICA"
+ }
+]
\ No newline at end of file
diff --git a/tests/fixtures/netatmo/gethomecoachsdata.json b/tests/fixtures/netatmo/gethomecoachsdata.json
new file mode 100644
index 00000000000000..3f9de74bd1ac01
--- /dev/null
+++ b/tests/fixtures/netatmo/gethomecoachsdata.json
@@ -0,0 +1,202 @@
+{
+ "body": {
+ "devices": [
+ {
+ "_id": "12:34:56:26:69:0c",
+ "cipher_id": "enc:16:1UqwQlYV5AY2pfyEi5H47dmmFOOL3mCUo+KAkchL4A2CLI5u0e45Xr5jeAswO+XO",
+ "date_setup": 1544560184,
+ "last_setup": 1544560184,
+ "type": "NHC",
+ "last_status_store": 1558268332,
+ "firmware": 45,
+ "last_upgrade": 1544560186,
+ "wifi_status": 58,
+ "reachable": false,
+ "co2_calibrating": false,
+ "station_name": "Bedroom",
+ "data_type": [
+ "Temperature",
+ "CO2",
+ "Humidity",
+ "Noise",
+ "Pressure",
+ "health_idx"
+ ],
+ "place": {
+ "city": "Frankfurt",
+ "country": "DE",
+ "timezone": "Europe/Berlin",
+ "location": [
+ 52.516263,
+ 13.377726
+ ]
+ }
+ },
+ {
+ "_id": "12:34:56:25:cf:a8",
+ "cipher_id": "enc:16:A+Jm0yFWBwUyKinFDutPZK7I2PuHN1fqaE9oB/KF+McbFs3oN9CKpR/dYbqL4om2",
+ "date_setup": 1544562192,
+ "last_setup": 1544562192,
+ "type": "NHC",
+ "last_status_store": 1559198922,
+ "firmware": 45,
+ "last_upgrade": 1544562194,
+ "wifi_status": 41,
+ "reachable": true,
+ "co2_calibrating": false,
+ "station_name": "Kitchen",
+ "data_type": [
+ "Temperature",
+ "CO2",
+ "Humidity",
+ "Noise",
+ "Pressure",
+ "health_idx"
+ ],
+ "place": {
+ "city": "Frankfurt",
+ "country": "DE",
+ "timezone": "Europe/Berlin",
+ "location": [
+ 52.516263,
+ 13.377726
+ ]
+ }
+ },
+ {
+ "_id": "12:34:56:26:65:14",
+ "cipher_id": "enc:16:7kK6ZzG4L7NgfZZ6+dMvNxw4l6vXu+88SEJkCUklNdPa4KYIHmsfa1moOilEK61i",
+ "date_setup": 1544564061,
+ "last_setup": 1544564061,
+ "type": "NHC",
+ "last_status_store": 1559067159,
+ "firmware": 45,
+ "last_upgrade": 1544564302,
+ "wifi_status": 66,
+ "reachable": true,
+ "co2_calibrating": false,
+ "station_name": "Livingroom",
+ "data_type": [
+ "Temperature",
+ "CO2",
+ "Humidity",
+ "Noise",
+ "Pressure",
+ "health_idx"
+ ],
+ "place": {
+ "city": "Frankfurt",
+ "country": "DE",
+ "timezone": "Europe/Berlin",
+ "location": [
+ 52.516263,
+ 13.377726
+ ]
+ }
+ },
+ {
+ "_id": "12:34:56:3e:c5:46",
+ "station_name": "Parents Bedroom",
+ "date_setup": 1570732241,
+ "last_setup": 1570732241,
+ "type": "NHC",
+ "last_status_store": 1572073818,
+ "module_name": "Indoor",
+ "firmware": 45,
+ "wifi_status": 67,
+ "reachable": true,
+ "co2_calibrating": false,
+ "data_type": [
+ "Temperature",
+ "CO2",
+ "Humidity",
+ "Noise",
+ "Pressure",
+ "health_idx"
+ ],
+ "place": {
+ "city": "Frankfurt",
+ "country": "DE",
+ "timezone": "Europe/Berlin",
+ "location": [
+ 52.516263,
+ 13.377726
+ ]
+ },
+ "dashboard_data": {
+ "time_utc": 1572073816,
+ "Temperature": 20.3,
+ "CO2": 494,
+ "Humidity": 63,
+ "Noise": 42,
+ "Pressure": 1014.5,
+ "AbsolutePressure": 1004.1,
+ "health_idx": 1,
+ "min_temp": 20.3,
+ "max_temp": 21.6,
+ "date_max_temp": 1572059333,
+ "date_min_temp": 1572073816
+ }
+ },
+ {
+ "_id": "12:34:56:26:68:92",
+ "station_name": "Baby Bedroom",
+ "date_setup": 1571342643,
+ "last_setup": 1571342643,
+ "type": "NHC",
+ "last_status_store": 1572073995,
+ "module_name": "Indoor",
+ "firmware": 45,
+ "wifi_status": 68,
+ "reachable": true,
+ "co2_calibrating": false,
+ "data_type": [
+ "Temperature",
+ "CO2",
+ "Humidity",
+ "Noise",
+ "Pressure",
+ "health_idx"
+ ],
+ "place": {
+ "city": "Frankfurt",
+ "country": "DE",
+ "timezone": "Europe/Berlin",
+ "location": [
+ 52.516263,
+ 13.377726
+ ]
+ },
+ "dashboard_data": {
+ "time_utc": 1572073994,
+ "Temperature": 21.6,
+ "CO2": 1053,
+ "Humidity": 66,
+ "Noise": 45,
+ "Pressure": 1021.4,
+ "AbsolutePressure": 1011,
+ "health_idx": 1,
+ "min_temp": 20.9,
+ "max_temp": 21.6,
+ "date_max_temp": 1572073690,
+ "date_min_temp": 1572064254
+ }
+ }
+ ],
+ "user": {
+ "mail": "john@doe.com",
+ "administrative": {
+ "lang": "de-DE",
+ "reg_locale": "de-DE",
+ "country": "DE",
+ "unit": 0,
+ "windunit": 0,
+ "pressureunit": 0,
+ "feel_like_algo": 0
+ }
+ }
+ },
+ "status": "ok",
+ "time_exec": 0.095954179763794,
+ "time_server": 1559463229
+}
\ No newline at end of file
diff --git a/tests/fixtures/netatmo/gethomedata.json b/tests/fixtures/netatmo/gethomedata.json
new file mode 100644
index 00000000000000..db7d6aa438d823
--- /dev/null
+++ b/tests/fixtures/netatmo/gethomedata.json
@@ -0,0 +1,318 @@
+{
+ "body": {
+ "homes": [
+ {
+ "id": "91763b24c43d3e344f424e8b",
+ "name": "MYHOME",
+ "persons": [
+ {
+ "id": "91827374-7e04-5298-83ad-a0cb8372dff1",
+ "last_seen": 1557071156,
+ "out_of_sight": true,
+ "face": {
+ "id": "d74fad765b9100ef480720a9",
+ "version": 1,
+ "key": "a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7",
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7"
+ },
+ "pseudo": "John Doe"
+ },
+ {
+ "id": "91827375-7e04-5298-83ae-a0cb8372dff2",
+ "last_seen": 1560600726,
+ "out_of_sight": true,
+ "face": {
+ "id": "d74fad765b9100ef480720a9",
+ "version": 3,
+ "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72",
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72"
+ },
+ "pseudo": "Jane Doe"
+ },
+ {
+ "id": "91827376-7e04-5298-83af-a0cb8372dff3",
+ "last_seen": 1560626666,
+ "out_of_sight": false,
+ "face": {
+ "id": "d74fad765b9100ef480720a9",
+ "version": 1,
+ "key": "a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8",
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8"
+ },
+ "pseudo": "Richard Doe"
+ },
+ {
+ "id": "91827376-7e04-5298-83af-a0cb8372dff4",
+ "last_seen": 1560621666,
+ "out_of_sight": true,
+ "face": {
+ "id": "d0ef44fad765b980720710a9",
+ "version": 1,
+ "key": "ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928",
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/d0ef44fad765b980720710a9ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928"
+ }
+ }
+ ],
+ "place": {
+ "city": "Frankfurt",
+ "country": "DE",
+ "timezone": "Europe/Berlin"
+ },
+ "cameras": [
+ {
+ "id": "12:34:56:00:f1:62",
+ "type": "NACamera",
+ "status": "on",
+ "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTg,,",
+ "is_local": true,
+ "sd_status": "on",
+ "alim_status": "on",
+ "name": "Hall",
+ "modules": [
+ {
+ "id": "12:34:56:00:f2:f1",
+ "type": "NIS",
+ "battery_percent": 84,
+ "rf": 68,
+ "status": "no_news",
+ "monitoring": "on",
+ "alim_source": "battery",
+ "tamper_detection_enabled": true,
+ "name": "Welcome's Siren"
+ }
+ ],
+ "use_pin_code": false,
+ "last_setup": 1544828430
+ },
+ {
+ "id": "12:34:56:00:a5:a4",
+ "type": "NOC",
+ "status": "on",
+ "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTw,,",
+ "is_local": false,
+ "sd_status": "on",
+ "alim_status": "on",
+ "name": "Garden",
+ "last_setup": 1563737661,
+ "light_mode_status": "auto"
+ }
+ ],
+ "smokedetectors": [
+ {
+ "id": "12:34:56:00:8b:a2",
+ "type": "NSD",
+ "last_setup": 1567261859,
+ "name": "Hall"
+ },
+ {
+ "id": "12:34:56:00:8b:ac",
+ "type": "NSD",
+ "last_setup": 1567262759,
+ "name": "Kitchen"
+ }
+ ],
+ "events": [
+ {
+ "id": "a1b2c3d4e5f6abcdef123456",
+ "type": "person",
+ "time": 1560604700,
+ "camera_id": "12:34:56:00:f1:62",
+ "device_id": "12:34:56:00:f1:62",
+ "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1",
+ "video_status": "deleted",
+ "is_arrival": false,
+ "message": "John Doe gesehen"
+ },
+ {
+ "id": "a1b2c3d4e5f6abcdef123457",
+ "type": "person_away",
+ "time": 1560602400,
+ "camera_id": "12:34:56:00:f1:62",
+ "device_id": "12:34:56:00:f1:62",
+ "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1",
+ "message": "John Doe hat das Haus verlassen",
+ "sub_message": "John Doe gilt als abwesend, da das mit diesem Profil verbundene Telefon den Bereich des Hauses verlassen hat."
+ },
+ {
+ "id": "a1b2c3d4e5f6abcdef123458",
+ "type": "person",
+ "time": 1560601200,
+ "camera_id": "12:34:56:00:f1:62",
+ "device_id": "12:34:56:00:f1:62",
+ "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1",
+ "video_status": "deleted",
+ "is_arrival": false,
+ "message": "John Doe gesehen"
+ },
+ {
+ "id": "a1b2c3d4e5f6abcdef123459",
+ "type": "person",
+ "time": 1560600100,
+ "camera_id": "12:34:56:00:f1:62",
+ "device_id": "12:34:56:00:f1:62",
+ "person_id": "91827375-7e04-5298-83ae-a0cb8372dff2",
+ "snapshot": {
+ "id": "d74fad765b9100ef480720a9",
+ "version": 1,
+ "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72",
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72"
+ },
+ "video_id": "12345678-36bc-4b9a-9762-5194e707ed51",
+ "video_status": "available",
+ "is_arrival": false,
+ "message": "Jane Doe gesehen"
+ },
+ {
+ "id": "a1b2c3d4e5f6abcdef12345a",
+ "type": "person",
+ "time": 1560603600,
+ "camera_id": "12:34:56:00:f1:62",
+ "device_id": "12:34:56:00:f1:62",
+ "person_id": "91827375-7e04-5298-83ae-a0cb8372dff3",
+ "snapshot": {
+ "id": "532dde8d17554c022ab071b8",
+ "version": 1,
+ "key": "9fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28",
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b89fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28"
+ },
+ "video_id": "12345678-1234-46cb-ad8f-23d893874099",
+ "video_status": "available",
+ "is_arrival": false,
+ "message": "Bewegung erkannt"
+ },
+ {
+ "id": "a1b2c3d4e5f6abcdef12345b",
+ "type": "movement",
+ "time": 1560506200,
+ "camera_id": "12:34:56:00:f1:62",
+ "device_id": "12:34:56:00:f1:62",
+ "category": "human",
+ "snapshot": {
+ "id": "532dde8d17554c022ab071b9",
+ "version": 1,
+ "key": "8fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28",
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b98fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28"
+ },
+ "vignette": {
+ "id": "5dc021b5dea854bd2321707a",
+ "version": 1,
+ "key": "58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944",
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/5dc021b5dea854bd2321707a58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944"
+ },
+ "video_id": "12345678-1234-46cb-ad8f-23d89387409a",
+ "video_status": "available",
+ "message": "Bewegung erkannt"
+ },
+ {
+ "id": "a1b2c3d4e5f6abcdef12345c",
+ "type": "sound_test",
+ "time": 1560506210,
+ "camera_id": "12:34:56:00:8b:a2",
+ "device_id": "12:34:56:00:8b:a2",
+ "sub_type": 0,
+ "message": "Hall: Alarmton erfolgreich getestet"
+ },
+ {
+ "id": "a1b2c3d4e5f6abcdef12345d",
+ "type": "wifi_status",
+ "time": 1560506220,
+ "camera_id": "12:34:56:00:8b:a2",
+ "device_id": "12:34:56:00:8b:a2",
+ "sub_type": 1,
+ "message": "Hall:WLAN-Verbindung erfolgreich hergestellt"
+ },
+ {
+ "id": "a1b2c3d4e5f6abcdef12345e",
+ "type": "outdoor",
+ "time": 1560643100,
+ "camera_id": "12:34:56:00:a5:a4",
+ "device_id": "12:34:56:00:a5:a4",
+ "video_id": "string",
+ "video_status": "available",
+ "event_list": [
+ {
+ "type": "string",
+ "time": 1560643100,
+ "offset": 0,
+ "id": "c81bcf7b-2cfg-4ac9-8455-487ed00c0000",
+ "message": "Animal détecté",
+ "snapshot": {
+ "id": "5715e16849c75xxxx00000000xxxxx",
+ "version": 1,
+ "key": "7ac578d05030d0e170643a787ee0a29663dxxx00000xxxxx00000",
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/1aa"
+ },
+ "vignette": {
+ "id": "5715e16849c75xxxx00000000xxxxx",
+ "version": 1,
+ "key": "7ac578d05030d0e170643a787ee0a29663dxxx00000xxxxx00000",
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/1aa00000"
+ }
+ },
+ {
+ "type": "string",
+ "time": 1560506222,
+ "offset": 0,
+ "id": "c81bcf7b-2cfg-4ac9-8455-487ed00c0000",
+ "message": "Animal détecté",
+ "snapshot": {
+ "filename": "vod\/af74631d-8311-42dc-825b-82e3abeaab09\/events\/c53b-aze7a.jpg"
+ },
+ "vignette": {
+ "filename": "vod\/af74631d-8311-42dc-825b-82e3abeaab09\/events\/c5.jpg"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "91763b24c43d3e344f424e8c",
+ "persons": [],
+ "place": {
+ "city": "Frankfurt",
+ "country": "DE",
+ "timezone": "Europe/Berlin"
+ },
+ "cameras": [
+ {
+ "id": "12:34:56:00:a5:a5",
+ "type": "NOC",
+ "status": "on",
+ "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTz,,",
+ "is_local": true,
+ "sd_status": "on",
+ "alim_status": "on",
+ "name": "Street",
+ "last_setup": 1563737561,
+ "light_mode_status": "auto"
+ }
+ ],
+ "smokedetectors": []
+ },
+ {
+ "id": "91763b24c43d3e344f424e8d",
+ "persons": [],
+ "place": {
+ "city": "Frankfurt",
+ "country": "DE",
+ "timezone": "Europe/Berlin"
+ },
+ "cameras": [],
+ "smokedetectors": []
+ }
+ ],
+ "user": {
+ "reg_locale": "de-DE",
+ "lang": "de-DE",
+ "country": "DE",
+ "mail": "john@doe.com"
+ },
+ "global_info": {
+ "show_tags": true
+ }
+ },
+ "status": "ok",
+ "time_exec": 0.03621506690979,
+ "time_server": 1560626960
+}
\ No newline at end of file
diff --git a/tests/fixtures/netatmo/getpublicdata.json b/tests/fixtures/netatmo/getpublicdata.json
new file mode 100644
index 00000000000000..55202713890301
--- /dev/null
+++ b/tests/fixtures/netatmo/getpublicdata.json
@@ -0,0 +1,392 @@
+{
+ "status": "ok",
+ "time_server": 1560248397,
+ "time_exec": 0,
+ "body": [
+ {
+ "_id": "70:ee:50:36:94:7c",
+ "place": {
+ "location": [
+ 8.791382999999996,
+ 50.2136394
+ ],
+ "timezone": "Europe/Berlin",
+ "country": "DE",
+ "altitude": 132
+ },
+ "mark": 14,
+ "measures": {
+ "02:00:00:36:f2:94": {
+ "res": {
+ "1560248022": [
+ 21.4,
+ 62
+ ]
+ },
+ "type": [
+ "temperature",
+ "humidity"
+ ]
+ },
+ "70:ee:50:36:94:7c": {
+ "res": {
+ "1560248030": [
+ 1010.6
+ ]
+ },
+ "type": [
+ "pressure"
+ ]
+ },
+ "05:00:00:05:33:84": {
+ "rain_60min": 0.2,
+ "rain_24h": 12.322000000000001,
+ "rain_live": 0.5,
+ "rain_timeutc": 1560248022
+ }
+ },
+ "modules": [
+ "05:00:00:05:33:84",
+ "02:00:00:36:f2:94"
+ ],
+ "module_types": {
+ "05:00:00:05:33:84": "NAModule3",
+ "02:00:00:36:f2:94": "NAModule1"
+ }
+ },
+ {
+ "_id": "70:ee:50:1f:68:9e",
+ "place": {
+ "location": [
+ 8.795445200000017,
+ 50.2130169
+ ],
+ "timezone": "Europe/Berlin",
+ "country": "DE",
+ "altitude": 125
+ },
+ "mark": 14,
+ "measures": {
+ "02:00:00:1f:82:28": {
+ "res": {
+ "1560248312": [
+ 21.1,
+ 69
+ ]
+ },
+ "type": [
+ "temperature",
+ "humidity"
+ ]
+ },
+ "70:ee:50:1f:68:9e": {
+ "res": {
+ "1560248344": [
+ 1007.3
+ ]
+ },
+ "type": [
+ "pressure"
+ ]
+ },
+ "05:00:00:02:bb:6e": {
+ "rain_60min": 0,
+ "rain_24h": 9.999,
+ "rain_live": 0,
+ "rain_timeutc": 1560248344
+ }
+ },
+ "modules": [
+ "02:00:00:1f:82:28",
+ "05:00:00:02:bb:6e"
+ ],
+ "module_types": {
+ "02:00:00:1f:82:28": "NAModule1",
+ "05:00:00:02:bb:6e": "NAModule3"
+ }
+ },
+ {
+ "_id": "70:ee:50:27:25:b0",
+ "place": {
+ "location": [
+ 8.7807159,
+ 50.1946167
+ ],
+ "timezone": "Europe/Berlin",
+ "country": "DE",
+ "altitude": 112
+ },
+ "mark": 14,
+ "measures": {
+ "02:00:00:27:19:b2": {
+ "res": {
+ "1560247889": [
+ 23.2,
+ 60
+ ]
+ },
+ "type": [
+ "temperature",
+ "humidity"
+ ]
+ },
+ "70:ee:50:27:25:b0": {
+ "res": {
+ "1560247907": [
+ 1012.8
+ ]
+ },
+ "type": [
+ "pressure"
+ ]
+ },
+ "05:00:00:03:5d:2e": {
+ "rain_60min": 0,
+ "rain_24h": 11.716000000000001,
+ "rain_live": 0,
+ "rain_timeutc": 1560247896
+ }
+ },
+ "modules": [
+ "02:00:00:27:19:b2",
+ "05:00:00:03:5d:2e"
+ ],
+ "module_types": {
+ "02:00:00:27:19:b2": "NAModule1",
+ "05:00:00:03:5d:2e": "NAModule3"
+ }
+ },
+ {
+ "_id": "70:ee:50:04:ed:7a",
+ "place": {
+ "location": [
+ 8.785034,
+ 50.192169
+ ],
+ "timezone": "Europe/Berlin",
+ "country": "DE",
+ "altitude": 112
+ },
+ "mark": 14,
+ "measures": {
+ "02:00:00:04:c2:2e": {
+ "res": {
+ "1560248137": [
+ 19.8,
+ 76
+ ]
+ },
+ "type": [
+ "temperature",
+ "humidity"
+ ]
+ },
+ "70:ee:50:04:ed:7a": {
+ "res": {
+ "1560248152": [
+ 1005.4
+ ]
+ },
+ "type": [
+ "pressure"
+ ]
+ }
+ },
+ "modules": [
+ "02:00:00:04:c2:2e"
+ ],
+ "module_types": {
+ "02:00:00:04:c2:2e": "NAModule1"
+ }
+ },
+ {
+ "_id": "70:ee:50:27:9f:2c",
+ "place": {
+ "location": [
+ 8.785342,
+ 50.193573
+ ],
+ "timezone": "Europe/Berlin",
+ "country": "DE",
+ "altitude": 116
+ },
+ "mark": 1,
+ "measures": {
+ "02:00:00:27:aa:70": {
+ "res": {
+ "1560247821": [
+ 25.5,
+ 56
+ ]
+ },
+ "type": [
+ "temperature",
+ "humidity"
+ ]
+ },
+ "70:ee:50:27:9f:2c": {
+ "res": {
+ "1560247853": [
+ 1010.6
+ ]
+ },
+ "type": [
+ "pressure"
+ ]
+ }
+ },
+ "modules": [
+ "02:00:00:27:aa:70"
+ ],
+ "module_types": {
+ "02:00:00:27:aa:70": "NAModule1"
+ }
+ },
+ {
+ "_id": "70:ee:50:01:20:fa",
+ "place": {
+ "location": [
+ 8.7953,
+ 50.195241
+ ],
+ "timezone": "Europe/Berlin",
+ "country": "DE",
+ "altitude": 119
+ },
+ "mark": 1,
+ "measures": {
+ "02:00:00:00:f7:ba": {
+ "res": {
+ "1560247831": [
+ 27.4,
+ 58
+ ]
+ },
+ "type": [
+ "temperature",
+ "humidity"
+ ]
+ },
+ "70:ee:50:01:20:fa": {
+ "res": {
+ "1560247876": [
+ 1014.4
+ ]
+ },
+ "type": [
+ "pressure"
+ ]
+ }
+ },
+ "modules": [
+ "02:00:00:00:f7:ba"
+ ],
+ "module_types": {
+ "02:00:00:00:f7:ba": "NAModule1"
+ }
+ },
+ {
+ "_id": "70:ee:50:3c:02:78",
+ "place": {
+ "location": [
+ 8.795953681700666,
+ 50.19530139868166
+ ],
+ "timezone": "Europe/Berlin",
+ "country": "DE",
+ "altitude": 119
+ },
+ "mark": 7,
+ "measures": {
+ "02:00:00:3c:21:f2": {
+ "res": {
+ "1560248225": [
+ 23.3,
+ 58
+ ]
+ },
+ "type": [
+ "temperature",
+ "humidity"
+ ]
+ },
+ "70:ee:50:3c:02:78": {
+ "res": {
+ "1560248270": [
+ 1011.7
+ ]
+ },
+ "type": [
+ "pressure"
+ ]
+ }
+ },
+ "modules": [
+ "02:00:00:3c:21:f2"
+ ],
+ "module_types": {
+ "02:00:00:3c:21:f2": "NAModule1"
+ }
+ },
+ {
+ "_id": "70:ee:50:36:a9:fc",
+ "place": {
+ "location": [
+ 8.801164269110814,
+ 50.19596181704958
+ ],
+ "timezone": "Europe/Berlin",
+ "country": "DE",
+ "altitude": 113
+ },
+ "mark": 14,
+ "measures": {
+ "02:00:00:36:a9:50": {
+ "res": {
+ "1560248145": [
+ 20.1,
+ 67
+ ]
+ },
+ "type": [
+ "temperature",
+ "humidity"
+ ]
+ },
+ "70:ee:50:36:a9:fc": {
+ "res": {
+ "1560248191": [
+ 1010
+ ]
+ },
+ "type": [
+ "pressure"
+ ]
+ },
+ "05:00:00:02:92:82": {
+ "rain_60min": 0,
+ "rain_24h": 11.009,
+ "rain_live": 0,
+ "rain_timeutc": 1560248184
+ },
+ "06:00:00:03:19:76": {
+ "wind_strength": 15,
+ "wind_angle": 17,
+ "gust_strength": 31,
+ "gust_angle": 217,
+ "wind_timeutc": 1560248190
+ }
+ },
+ "modules": [
+ "05:00:00:02:92:82",
+ "02:00:00:36:a9:50",
+ "06:00:00:03:19:76"
+ ],
+ "module_types": {
+ "05:00:00:02:92:82": "NAModule3",
+ "02:00:00:36:a9:50": "NAModule1",
+ "06:00:00:03:19:76": "NAModule2"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/netatmo/getstationsdata.json b/tests/fixtures/netatmo/getstationsdata.json
new file mode 100644
index 00000000000000..2a18c7bd28071c
--- /dev/null
+++ b/tests/fixtures/netatmo/getstationsdata.json
@@ -0,0 +1,600 @@
+{
+ "body": {
+ "devices": [
+ {
+ "_id": "12:34:56:37:11:ca",
+ "cipher_id": "enc:16:zjiZF/q8jTScXVdDa/kvhUAIUPGeYszaD1ClEf8byAJkRjxc5oth7cAocrMUIApX",
+ "date_setup": 1544558432,
+ "last_setup": 1544558432,
+ "type": "NAMain",
+ "last_status_store": 1559413181,
+ "module_name": "NetatmoIndoor",
+ "firmware": 137,
+ "last_upgrade": 1544558433,
+ "wifi_status": 45,
+ "reachable": true,
+ "co2_calibrating": false,
+ "station_name": "MyStation",
+ "data_type": [
+ "Temperature",
+ "CO2",
+ "Humidity",
+ "Noise",
+ "Pressure"
+ ],
+ "place": {
+ "altitude": 664,
+ "city": "Frankfurt",
+ "country": "DE",
+ "timezone": "Europe/Berlin",
+ "location": [
+ 52.516263,
+ 13.377726
+ ]
+ },
+ "dashboard_data": {
+ "time_utc": 1559413171,
+ "Temperature": 24.6,
+ "CO2": 749,
+ "Humidity": 36,
+ "Noise": 37,
+ "Pressure": 1017.3,
+ "AbsolutePressure": 939.7,
+ "min_temp": 23.4,
+ "max_temp": 25.6,
+ "date_min_temp": 1559371924,
+ "date_max_temp": 1559411964,
+ "temp_trend": "stable",
+ "pressure_trend": "down"
+ },
+ "modules": [
+ {
+ "_id": "12:34:56:36:fc:de",
+ "type": "NAModule1",
+ "module_name": "NetatmoOutdoor",
+ "data_type": [
+ "Temperature",
+ "Humidity"
+ ],
+ "last_setup": 1544558433,
+ "reachable": true,
+ "dashboard_data": {
+ "time_utc": 1559413157,
+ "Temperature": 28.6,
+ "Humidity": 24,
+ "min_temp": 16.9,
+ "max_temp": 30.3,
+ "date_min_temp": 1559365579,
+ "date_max_temp": 1559404698,
+ "temp_trend": "down"
+ },
+ "firmware": 46,
+ "last_message": 1559413177,
+ "last_seen": 1559413157,
+ "rf_status": 65,
+ "battery_vp": 5738,
+ "battery_percent": 87
+ },
+ {
+ "_id": "12:34:56:07:bb:3e",
+ "type": "NAModule4",
+ "module_name": "Kitchen",
+ "data_type": [
+ "Temperature",
+ "CO2",
+ "Humidity"
+ ],
+ "last_setup": 1548956696,
+ "reachable": true,
+ "dashboard_data": {
+ "time_utc": 1559413125,
+ "Temperature": 28,
+ "CO2": 503,
+ "Humidity": 26,
+ "min_temp": 25,
+ "max_temp": 28,
+ "date_min_temp": 1559371577,
+ "date_max_temp": 1559412561,
+ "temp_trend": "up"
+ },
+ "firmware": 44,
+ "last_message": 1559413177,
+ "last_seen": 1559413177,
+ "rf_status": 73,
+ "battery_vp": 5687,
+ "battery_percent": 83
+ },
+ {
+ "_id": "12:34:56:07:bb:0e",
+ "type": "NAModule4",
+ "module_name": "Livingroom",
+ "data_type": [
+ "Temperature",
+ "CO2",
+ "Humidity"
+ ],
+ "last_setup": 1548957209,
+ "reachable": true,
+ "dashboard_data": {
+ "time_utc": 1559413093,
+ "Temperature": 26.4,
+ "CO2": 451,
+ "Humidity": 31,
+ "min_temp": 25.1,
+ "max_temp": 26.4,
+ "date_min_temp": 1559365290,
+ "date_max_temp": 1559413093,
+ "temp_trend": "stable"
+ },
+ "firmware": 44,
+ "last_message": 1559413177,
+ "last_seen": 1559413093,
+ "rf_status": 84,
+ "battery_vp": 5626,
+ "battery_percent": 79
+ },
+ {
+ "_id": "12:34:56:03:1b:e4",
+ "type": "NAModule2",
+ "module_name": "Garden",
+ "data_type": [
+ "Wind"
+ ],
+ "last_setup": 1549193862,
+ "reachable": true,
+ "dashboard_data": {
+ "time_utc": 1559413170,
+ "WindStrength": 4,
+ "WindAngle": 217,
+ "GustStrength": 9,
+ "GustAngle": 206,
+ "max_wind_str": 21,
+ "max_wind_angle": 217,
+ "date_max_wind_str": 1559386669
+ },
+ "firmware": 19,
+ "last_message": 1559413177,
+ "last_seen": 1559413177,
+ "rf_status": 59,
+ "battery_vp": 5689,
+ "battery_percent": 85
+ },
+ {
+ "_id": "12:34:56:05:51:20",
+ "type": "NAModule3",
+ "module_name": "Yard",
+ "data_type": [
+ "Rain"
+ ],
+ "last_setup": 1549194580,
+ "reachable": true,
+ "dashboard_data": {
+ "time_utc": 1559413170,
+ "Rain": 0,
+ "sum_rain_24": 0,
+ "sum_rain_1": 0
+ },
+ "firmware": 8,
+ "last_message": 1559413177,
+ "last_seen": 1559413170,
+ "rf_status": 67,
+ "battery_vp": 5860,
+ "battery_percent": 93
+ }
+ ]
+ },
+ {
+ "_id": "12 :34: 56:36:fd:3c",
+ "station_name": "Valley Road",
+ "date_setup": 1545897146,
+ "last_setup": 1545897146,
+ "type": "NAMain",
+ "last_status_store": 1581835369,
+ "firmware": 137,
+ "last_upgrade": 1545897125,
+ "wifi_status": 53,
+ "reachable": true,
+ "co2_calibrating": false,
+ "data_type": [
+ "Temperature",
+ "CO2",
+ "Humidity",
+ "Noise",
+ "Pressure"
+ ],
+ "place": {
+ "altitude": 69,
+ "city": "Valley",
+ "country": "AU",
+ "timezone": "Australia/Hobart",
+ "location": [
+ 148.444226,
+ -41.721282
+ ]
+ },
+ "read_only": true,
+ "dashboard_data": {
+ "time_utc": 1581835330,
+ "Temperature": 22.4,
+ "CO2": 471,
+ "Humidity": 46,
+ "Noise": 47,
+ "Pressure": 1011.5,
+ "AbsolutePressure": 1002.8,
+ "min_temp": 18.1,
+ "max_temp": 22.5,
+ "date_max_temp": 1581829891,
+ "date_min_temp": 1581794878,
+ "temp_trend": "stable",
+ "pressure_trend": "stable"
+ },
+ "modules": [
+ {
+ "_id": "12 :34: 56:36:e6:c0",
+ "type": "NAModule1",
+ "module_name": "Module",
+ "data_type": [
+ "Temperature",
+ "Humidity"
+ ],
+ "last_setup": 1545897146,
+ "battery_percent": 22,
+ "reachable": false,
+ "firmware": 46,
+ "last_message": 1572497781,
+ "last_seen": 1572497742,
+ "rf_status": 88,
+ "battery_vp": 4118
+ },
+ {
+ "_id": "12:34:56:05:25:6e",
+ "type": "NAModule3",
+ "module_name": "Rain Gauge",
+ "data_type": [
+ "Rain"
+ ],
+ "last_setup": 1553997427,
+ "battery_percent": 82,
+ "reachable": true,
+ "firmware": 8,
+ "last_message": 1581835362,
+ "last_seen": 1581835354,
+ "rf_status": 78,
+ "battery_vp": 5594,
+ "dashboard_data": {
+ "time_utc": 1581835329,
+ "Rain": 0,
+ "sum_rain_1": 0,
+ "sum_rain_24": 0
+ }
+ }
+ ]
+ },
+ {
+ "_id": "12:34:56:32:a7:60",
+ "home_name": "Ateljen",
+ "date_setup": 1566714693,
+ "last_setup": 1566714693,
+ "type": "NAMain",
+ "last_status_store": 1588481079,
+ "module_name": "Indoor",
+ "firmware": 177,
+ "last_upgrade": 1566714694,
+ "wifi_status": 50,
+ "reachable": true,
+ "co2_calibrating": false,
+ "data_type": [
+ "Temperature",
+ "CO2",
+ "Humidity",
+ "Noise",
+ "Pressure"
+ ],
+ "place": {
+ "altitude": 93,
+ "city": "Gothenburg",
+ "country": "SE",
+ "timezone": "Europe/Stockholm",
+ "location": [
+ 11.6136629,
+ 57.7006827
+ ]
+ },
+ "dashboard_data": {
+ "time_utc": 1588481073,
+ "Temperature": 18.2,
+ "CO2": 542,
+ "Humidity": 45,
+ "Noise": 45,
+ "Pressure": 1013,
+ "AbsolutePressure": 1001.9,
+ "min_temp": 18.2,
+ "max_temp": 19.5,
+ "date_max_temp": 1588456861,
+ "date_min_temp": 1588479561,
+ "temp_trend": "stable",
+ "pressure_trend": "up"
+ },
+ "modules": [
+ {
+ "_id": "12:34:56:32:db:06",
+ "type": "NAModule1",
+ "last_setup": 1587635819,
+ "data_type": [
+ "Temperature",
+ "Humidity"
+ ],
+ "battery_percent": 100,
+ "reachable": false,
+ "firmware": 255,
+ "last_message": 0,
+ "last_seen": 0,
+ "rf_status": 255,
+ "battery_vp": 65535
+ }
+ ]
+ },
+ {
+ "_id": "12:34:56:1c:68:2e",
+ "station_name": "Bol\u00e5s",
+ "date_setup": 1470935400,
+ "last_setup": 1470935400,
+ "type": "NAMain",
+ "last_status_store": 1588481399,
+ "module_name": "Inne - Nere",
+ "firmware": 177,
+ "last_upgrade": 1470935401,
+ "wifi_status": 13,
+ "reachable": true,
+ "co2_calibrating": false,
+ "data_type": [
+ "Temperature",
+ "CO2",
+ "Humidity",
+ "Noise",
+ "Pressure"
+ ],
+ "place": {
+ "altitude": 93,
+ "city": "Gothenburg",
+ "country": "SE",
+ "timezone": "Europe/Stockholm",
+ "location": [
+ 11.6136629,
+ 57.7006827
+ ]
+ },
+ "dashboard_data": {
+ "time_utc": 1588481387,
+ "Temperature": 20.8,
+ "CO2": 674,
+ "Humidity": 41,
+ "Noise": 34,
+ "Pressure": 1012.1,
+ "AbsolutePressure": 1001,
+ "min_temp": 20.8,
+ "max_temp": 22.2,
+ "date_max_temp": 1588456859,
+ "date_min_temp": 1588480176,
+ "temp_trend": "stable",
+ "pressure_trend": "up"
+ },
+ "modules": [
+ {
+ "_id": "12:34:56:02:b3:da",
+ "type": "NAModule3",
+ "module_name": "Regnm\u00e4tare",
+ "last_setup": 1470937706,
+ "data_type": [
+ "Rain"
+ ],
+ "battery_percent": 81,
+ "reachable": true,
+ "firmware": 12,
+ "last_message": 1588481393,
+ "last_seen": 1588481386,
+ "rf_status": 67,
+ "battery_vp": 5582,
+ "dashboard_data": {
+ "time_utc": 1588481386,
+ "Rain": 0,
+ "sum_rain_1": 0,
+ "sum_rain_24": 0.1
+ }
+ },
+ {
+ "_id": "12:34:56:03:76:60",
+ "type": "NAModule4",
+ "module_name": "Inne - Uppe",
+ "last_setup": 1470938089,
+ "data_type": [
+ "Temperature",
+ "CO2",
+ "Humidity"
+ ],
+ "battery_percent": 14,
+ "reachable": true,
+ "firmware": 50,
+ "last_message": 1588481393,
+ "last_seen": 1588481374,
+ "rf_status": 70,
+ "battery_vp": 4448,
+ "dashboard_data": {
+ "time_utc": 1588481374,
+ "Temperature": 19.6,
+ "CO2": 696,
+ "Humidity": 41,
+ "min_temp": 19.6,
+ "max_temp": 20.5,
+ "date_max_temp": 1588456817,
+ "date_min_temp": 1588481374,
+ "temp_trend": "stable"
+ }
+ },
+ {
+ "_id": "12:34:56:32:db:06",
+ "type": "NAModule1",
+ "module_name": "Ute",
+ "last_setup": 1566326027,
+ "data_type": [
+ "Temperature",
+ "Humidity"
+ ],
+ "battery_percent": 81,
+ "reachable": true,
+ "firmware": 50,
+ "last_message": 1588481393,
+ "last_seen": 1588481380,
+ "rf_status": 61,
+ "battery_vp": 5544,
+ "dashboard_data": {
+ "time_utc": 1588481380,
+ "Temperature": 6.4,
+ "Humidity": 91,
+ "min_temp": 3.6,
+ "max_temp": 6.4,
+ "date_max_temp": 1588481380,
+ "date_min_temp": 1588471383,
+ "temp_trend": "up"
+ }
+ }
+ ]
+ },
+ {
+ "_id": "12:34:56:1d:68:2e",
+ "date_setup": 1470935500,
+ "last_setup": 1470935500,
+ "type": "NAMain",
+ "last_status_store": 1588481399,
+ "module_name": "Basisstation",
+ "firmware": 177,
+ "last_upgrade": 1470935401,
+ "wifi_status": 13,
+ "reachable": true,
+ "co2_calibrating": false,
+ "data_type": [
+ "Temperature",
+ "CO2",
+ "Humidity",
+ "Noise",
+ "Pressure"
+ ],
+ "place": {
+ "altitude": 93,
+ "city": "Gothenburg",
+ "country": "SE",
+ "timezone": "Europe/Stockholm",
+ "location": [
+ 11.6136629,
+ 57.7006827
+ ]
+ },
+ "dashboard_data": {
+ "time_utc": 1588481387,
+ "Temperature": 20.8,
+ "CO2": 674,
+ "Humidity": 41,
+ "Noise": 34,
+ "Pressure": 1012.1,
+ "AbsolutePressure": 1001,
+ "min_temp": 20.8,
+ "max_temp": 22.2,
+ "date_max_temp": 1588456859,
+ "date_min_temp": 1588480176,
+ "temp_trend": "stable",
+ "pressure_trend": "up"
+ },
+ "modules": []
+ },
+ {
+ "_id": "12:34:56:58:c8:54",
+ "date_setup": 1605594014,
+ "last_setup": 1605594014,
+ "type": "NAMain",
+ "last_status_store": 1605878352,
+ "firmware": 178,
+ "wifi_status": 47,
+ "reachable": true,
+ "co2_calibrating": false,
+ "data_type": [
+ "Temperature",
+ "CO2",
+ "Humidity",
+ "Noise",
+ "Pressure"
+ ],
+ "place": {
+ "altitude": 65,
+ "city": "Njurunda District",
+ "country": "SE",
+ "timezone": "Europe/Stockholm",
+ "location": [
+ 17.123456,
+ 62.123456
+ ]
+ },
+ "station_name": "Njurunda (Indoor)",
+ "home_id": "5fb36b9ec68fd10c6467ca65",
+ "home_name": "Njurunda",
+ "dashboard_data": {
+ "time_utc": 1605878349,
+ "Temperature": 19.7,
+ "CO2": 993,
+ "Humidity": 40,
+ "Noise": 40,
+ "Pressure": 1015.6,
+ "AbsolutePressure": 1007.8,
+ "min_temp": 19.7,
+ "max_temp": 20.4,
+ "date_max_temp": 1605826917,
+ "date_min_temp": 1605873207,
+ "temp_trend": "stable",
+ "pressure_trend": "up"
+ },
+ "modules": [
+ {
+ "_id": "12:34:56:58:e6:38",
+ "type": "NAModule1",
+ "last_setup": 1605594034,
+ "data_type": [
+ "Temperature",
+ "Humidity"
+ ],
+ "battery_percent": 100,
+ "reachable": true,
+ "firmware": 50,
+ "last_message": 1605878347,
+ "last_seen": 1605878328,
+ "rf_status": 62,
+ "battery_vp": 6198,
+ "dashboard_data": {
+ "time_utc": 1605878328,
+ "Temperature": 0.6,
+ "Humidity": 77,
+ "min_temp": -2.1,
+ "max_temp": 1.5,
+ "date_max_temp": 1605865920,
+ "date_min_temp": 1605826904,
+ "temp_trend": "down"
+ }
+ }
+ ]
+ }
+ ],
+ "user": {
+ "mail": "john@doe.com",
+ "administrative": {
+ "lang": "de-DE",
+ "reg_locale": "de-DE",
+ "country": "DE",
+ "unit": 0,
+ "windunit": 0,
+ "pressureunit": 0,
+ "feel_like_algo": 0
+ }
+ }
+ },
+ "status": "ok",
+ "time_exec": 0.91107702255249,
+ "time_server": 1559413602
+}
\ No newline at end of file
diff --git a/tests/fixtures/netatmo/homesdata.json b/tests/fixtures/netatmo/homesdata.json
new file mode 100644
index 00000000000000..aecab91550cce6
--- /dev/null
+++ b/tests/fixtures/netatmo/homesdata.json
@@ -0,0 +1,595 @@
+{
+ "body": {
+ "homes": [
+ {
+ "id": "91763b24c43d3e344f424e8b",
+ "name": "MYHOME",
+ "altitude": 112,
+ "coordinates": [
+ 52.516263,
+ 13.377726
+ ],
+ "country": "DE",
+ "timezone": "Europe/Berlin",
+ "rooms": [
+ {
+ "id": "2746182631",
+ "name": "Livingroom",
+ "type": "livingroom",
+ "module_ids": [
+ "12:34:56:00:01:ae"
+ ]
+ },
+ {
+ "id": "3688132631",
+ "name": "Hall",
+ "type": "custom",
+ "module_ids": [
+ "12:34:56:00:f1:62"
+ ]
+ },
+ {
+ "id": "2833524037",
+ "name": "Entrada",
+ "type": "lobby",
+ "module_ids": [
+ "12:34:56:03:a5:54"
+ ]
+ },
+ {
+ "id": "2940411577",
+ "name": "Cocina",
+ "type": "kitchen",
+ "module_ids": [
+ "12:34:56:03:a0:ac"
+ ]
+ }
+ ],
+ "modules": [
+ {
+ "id": "12:34:56:00:fa:d0",
+ "type": "NAPlug",
+ "name": "Thermostat",
+ "setup_date": 1494963356,
+ "modules_bridged": [
+ "12:34:56:00:01:ae",
+ "12:34:56:03:a0:ac",
+ "12:34:56:03:a5:54"
+ ]
+ },
+ {
+ "id": "12:34:56:00:01:ae",
+ "type": "NATherm1",
+ "name": "Livingroom",
+ "setup_date": 1494963356,
+ "room_id": "2746182631",
+ "bridge": "12:34:56:00:fa:d0"
+ },
+ {
+ "id": "12:34:56:03:a5:54",
+ "type": "NRV",
+ "name": "Valve1",
+ "setup_date": 1554549767,
+ "room_id": "2833524037",
+ "bridge": "12:34:56:00:fa:d0"
+ },
+ {
+ "id": "12:34:56:03:a0:ac",
+ "type": "NRV",
+ "name": "Valve2",
+ "setup_date": 1554554444,
+ "room_id": "2940411577",
+ "bridge": "12:34:56:00:fa:d0"
+ },
+ {
+ "id": "12:34:56:00:f1:62",
+ "type": "NACamera",
+ "name": "Hall",
+ "setup_date": 1544828430,
+ "room_id": "3688132631"
+ }
+ ],
+ "therm_schedules": [
+ {
+ "zones": [
+ {
+ "type": 0,
+ "name": "Comfort",
+ "rooms_temp": [
+ {
+ "temp": 21,
+ "room_id": "2746182631"
+ }
+ ],
+ "id": 0
+ },
+ {
+ "type": 1,
+ "name": "Night",
+ "rooms_temp": [
+ {
+ "temp": 17,
+ "room_id": "2746182631"
+ }
+ ],
+ "id": 1
+ },
+ {
+ "type": 5,
+ "name": "Eco",
+ "rooms_temp": [
+ {
+ "temp": 17,
+ "room_id": "2746182631"
+ }
+ ],
+ "id": 4
+ }
+ ],
+ "timetable": [
+ {
+ "zone_id": 1,
+ "m_offset": 0
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 360
+ },
+ {
+ "zone_id": 4,
+ "m_offset": 420
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 960
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 1410
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 1800
+ },
+ {
+ "zone_id": 4,
+ "m_offset": 1860
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 2400
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 2850
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 3240
+ },
+ {
+ "zone_id": 4,
+ "m_offset": 3300
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 3840
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 4290
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 4680
+ },
+ {
+ "zone_id": 4,
+ "m_offset": 4740
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 5280
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 5730
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 6120
+ },
+ {
+ "zone_id": 4,
+ "m_offset": 6180
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 6720
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 7170
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 7620
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 8610
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 9060
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 10050
+ }
+ ],
+ "hg_temp": 7,
+ "away_temp": 14,
+ "name": "Default",
+ "selected": true,
+ "id": "591b54a2764ff4d50d8b5795",
+ "type": "therm"
+ },
+ {
+ "zones": [
+ {
+ "type": 0,
+ "name": "Comfort",
+ "rooms_temp": [
+ {
+ "temp": 21,
+ "room_id": "2746182631"
+ }
+ ],
+ "id": 0
+ },
+ {
+ "type": 1,
+ "name": "Night",
+ "rooms_temp": [
+ {
+ "temp": 17,
+ "room_id": "2746182631"
+ }
+ ],
+ "id": 1
+ },
+ {
+ "type": 5,
+ "name": "Eco",
+ "rooms_temp": [
+ {
+ "temp": 17,
+ "room_id": "2746182631"
+ }
+ ],
+ "id": 4
+ }
+ ],
+ "timetable": [
+ {
+ "zone_id": 1,
+ "m_offset": 0
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 360
+ },
+ {
+ "zone_id": 4,
+ "m_offset": 420
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 960
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 1410
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 1800
+ },
+ {
+ "zone_id": 4,
+ "m_offset": 1860
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 2400
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 2850
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 3240
+ },
+ {
+ "zone_id": 4,
+ "m_offset": 3300
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 3840
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 4290
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 4680
+ },
+ {
+ "zone_id": 4,
+ "m_offset": 4740
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 5280
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 5730
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 6120
+ },
+ {
+ "zone_id": 4,
+ "m_offset": 6180
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 6720
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 7170
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 7620
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 8610
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 9060
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 10050
+ }
+ ],
+ "hg_temp": 7,
+ "away_temp": 14,
+ "name": "Winter",
+ "id": "b1b54a2f45795764f59d50d8",
+ "type": "therm"
+ }
+ ],
+ "therm_setpoint_default_duration": 120,
+ "persons": [
+ {
+ "id": "91827374-7e04-5298-83ad-a0cb8372dff1",
+ "pseudo": "John Doe",
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7"
+ },
+ {
+ "id": "91827375-7e04-5298-83ae-a0cb8372dff2",
+ "pseudo": "Jane Doe",
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72"
+ },
+ {
+ "id": "91827376-7e04-5298-83af-a0cb8372dff3",
+ "pseudo": "Richard Doe",
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8"
+ }
+ ],
+ "schedules": [
+ {
+ "zones": [
+ {
+ "type": 0,
+ "name": "Komfort",
+ "rooms_temp": [
+ {
+ "temp": 21,
+ "room_id": "2746182631"
+ }
+ ],
+ "id": 0,
+ "rooms": [
+ {
+ "id": "2746182631",
+ "therm_setpoint_temperature": 21
+ }
+ ]
+ },
+ {
+ "type": 1,
+ "name": "Nacht",
+ "rooms_temp": [
+ {
+ "temp": 17,
+ "room_id": "2746182631"
+ }
+ ],
+ "id": 1,
+ "rooms": [
+ {
+ "id": "2746182631",
+ "therm_setpoint_temperature": 17
+ }
+ ]
+ },
+ {
+ "type": 5,
+ "name": "Eco",
+ "rooms_temp": [
+ {
+ "temp": 17,
+ "room_id": "2746182631"
+ }
+ ],
+ "id": 4,
+ "rooms": [
+ {
+ "id": "2746182631",
+ "therm_setpoint_temperature": 17
+ }
+ ]
+ }
+ ],
+ "timetable": [
+ {
+ "zone_id": 1,
+ "m_offset": 0
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 360
+ },
+ {
+ "zone_id": 4,
+ "m_offset": 420
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 960
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 1410
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 1800
+ },
+ {
+ "zone_id": 4,
+ "m_offset": 1860
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 2400
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 2850
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 3240
+ },
+ {
+ "zone_id": 4,
+ "m_offset": 3300
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 3840
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 4290
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 4680
+ },
+ {
+ "zone_id": 4,
+ "m_offset": 4740
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 5280
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 5730
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 6120
+ },
+ {
+ "zone_id": 4,
+ "m_offset": 6180
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 6720
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 7170
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 7620
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 8610
+ },
+ {
+ "zone_id": 0,
+ "m_offset": 9060
+ },
+ {
+ "zone_id": 1,
+ "m_offset": 10050
+ }
+ ],
+ "hg_temp": 7,
+ "away_temp": 14,
+ "name": "Default",
+ "id": "591b54a2764ff4d50d8b5795",
+ "selected": true,
+ "type": "therm"
+ }
+ ],
+ "therm_mode": "schedule"
+ },
+ {
+ "id": "91763b24c43d3e344f424e8c",
+ "altitude": 112,
+ "coordinates": [
+ 52.516263,
+ 13.377726
+ ],
+ "country": "DE",
+ "timezone": "Europe/Berlin",
+ "therm_setpoint_default_duration": 180,
+ "therm_mode": "schedule"
+ }
+ ],
+ "user": {
+ "email": "john@doe.com",
+ "language": "de-DE",
+ "locale": "de-DE",
+ "feel_like_algorithm": 0,
+ "unit_pressure": 0,
+ "unit_system": 0,
+ "unit_wind": 0,
+ "id": "91763b24c43d3e344f424e8b"
+ }
+ },
+ "status": "ok",
+ "time_exec": 0.056135892868042,
+ "time_server": 1559171003
+}
\ No newline at end of file
diff --git a/tests/fixtures/netatmo/homestatus.json b/tests/fixtures/netatmo/homestatus.json
new file mode 100644
index 00000000000000..5d508ea03b0f64
--- /dev/null
+++ b/tests/fixtures/netatmo/homestatus.json
@@ -0,0 +1,113 @@
+{
+ "status": "ok",
+ "time_server": 1559292039,
+ "body": {
+ "home": {
+ "modules": [
+ {
+ "id": "12:34:56:00:f1:62",
+ "type": "NACamera",
+ "monitoring": "on",
+ "sd_status": 4,
+ "alim_status": 2,
+ "locked": false,
+ "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.123.45/609e27de5699fb18147ab47d06846631/MTRPn_BeWCav5RBq4U1OMDruTW4dkQ0NuMwNDAw11g,,",
+ "is_local": true
+ },
+ {
+ "id": "12:34:56:00:fa:d0",
+ "type": "NAPlug",
+ "firmware_revision": 174,
+ "rf_strength": 107,
+ "wifi_strength": 42
+ },
+ {
+ "id": "12:34:56:00:01:ae",
+ "reachable": true,
+ "type": "NATherm1",
+ "firmware_revision": 65,
+ "rf_strength": 58,
+ "battery_level": 3793,
+ "boiler_valve_comfort_boost": false,
+ "boiler_status": false,
+ "anticipating": false,
+ "bridge": "12:34:56:00:fa:d0",
+ "battery_state": "high"
+ },
+ {
+ "id": "12:34:56:03:a5:54",
+ "reachable": true,
+ "type": "NRV",
+ "firmware_revision": 79,
+ "rf_strength": 51,
+ "battery_level": 3025,
+ "bridge": "12:34:56:00:fa:d0",
+ "battery_state": "full"
+ },
+ {
+ "id": "12:34:56:03:a0:ac",
+ "reachable": true,
+ "type": "NRV",
+ "firmware_revision": 79,
+ "rf_strength": 59,
+ "battery_level": 2329,
+ "bridge": "12:34:56:00:fa:d0",
+ "battery_state": "full"
+ }
+ ],
+ "rooms": [
+ {
+ "id": "2746182631",
+ "reachable": true,
+ "therm_measured_temperature": 19.8,
+ "therm_setpoint_temperature": 12,
+ "therm_setpoint_mode": "schedule",
+ "therm_setpoint_start_time": 1559229567,
+ "therm_setpoint_end_time": 0
+ },
+ {
+ "id": "2940411577",
+ "reachable": true,
+ "therm_measured_temperature": 5,
+ "heating_power_request": 1,
+ "therm_setpoint_temperature": 7,
+ "therm_setpoint_mode": "away",
+ "therm_setpoint_start_time": 0,
+ "therm_setpoint_end_time": 0,
+ "anticipating": false,
+ "open_window": false
+ },
+ {
+ "id": "2833524037",
+ "reachable": true,
+ "therm_measured_temperature": 24.5,
+ "heating_power_request": 0,
+ "therm_setpoint_temperature": 7,
+ "therm_setpoint_mode": "hg",
+ "therm_setpoint_start_time": 0,
+ "therm_setpoint_end_time": 0,
+ "anticipating": false,
+ "open_window": false
+ }
+ ],
+ "id": "91763b24c43d3e344f424e8b",
+ "persons": [
+ {
+ "id": "91827374-7e04-5298-83ad-a0cb8372dff1",
+ "last_seen": 1557071156,
+ "out_of_sight": true
+ },
+ {
+ "id": "91827375-7e04-5298-83ae-a0cb8372dff2",
+ "last_seen": 1559282761,
+ "out_of_sight": false
+ },
+ {
+ "id": "91827376-7e04-5298-83af-a0cb8372dff3",
+ "last_seen": 1559224132,
+ "out_of_sight": true
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/fixtures/netatmo/ping.json b/tests/fixtures/netatmo/ping.json
new file mode 100644
index 00000000000000..784975de5b0139
--- /dev/null
+++ b/tests/fixtures/netatmo/ping.json
@@ -0,0 +1,4 @@
+{
+ "local_url": "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d",
+ "product_name": "Welcome Netatmo"
+}
\ No newline at end of file
diff --git a/tests/fixtures/plex/library_movies_filtertypes.xml b/tests/fixtures/plex/library_movies_filtertypes.xml
new file mode 100644
index 00000000000000..0f305f385c22a5
--- /dev/null
+++ b/tests/fixtures/plex/library_movies_filtertypes.xml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/fixtures/plex/library_tvshows_size.xml b/tests/fixtures/plex/library_tvshows_size.xml
new file mode 100644
index 00000000000000..49f3fb5ea9b744
--- /dev/null
+++ b/tests/fixtures/plex/library_tvshows_size.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/tests/fixtures/plex/library_tvshows_size_episodes.xml b/tests/fixtures/plex/library_tvshows_size_episodes.xml
new file mode 100644
index 00000000000000..b326ac0e7219b4
--- /dev/null
+++ b/tests/fixtures/plex/library_tvshows_size_episodes.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/tests/fixtures/plex/library_tvshows_size_seasons.xml b/tests/fixtures/plex/library_tvshows_size_seasons.xml
new file mode 100644
index 00000000000000..49f3fb5ea9b744
--- /dev/null
+++ b/tests/fixtures/plex/library_tvshows_size_seasons.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/tests/fixtures/plex/plextv_shared_users.xml b/tests/fixtures/plex/plextv_shared_users.xml
new file mode 100644
index 00000000000000..9421bdfa17a983
--- /dev/null
+++ b/tests/fixtures/plex/plextv_shared_users.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/tests/fixtures/rest/configuration_empty.yaml b/tests/fixtures/rest/configuration_empty.yaml
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/tests/fixtures/rest/configuration_invalid.notyaml b/tests/fixtures/rest/configuration_invalid.notyaml
new file mode 100644
index 00000000000000..548d8bcf5a0f42
--- /dev/null
+++ b/tests/fixtures/rest/configuration_invalid.notyaml
@@ -0,0 +1,2 @@
+*!* NOT YAML
+
diff --git a/tests/fixtures/rest/configuration_top_level.yaml b/tests/fixtures/rest/configuration_top_level.yaml
new file mode 100644
index 00000000000000..df27e1601176f5
--- /dev/null
+++ b/tests/fixtures/rest/configuration_top_level.yaml
@@ -0,0 +1,12 @@
+rest:
+ - method: GET
+ resource: "http://localhost"
+ sensor:
+ name: fallover
+
+sensor:
+ - platform: rest
+ resource: "http://localhost"
+ method: GET
+ name: rollout
+
diff --git a/tests/fixtures/tado/weather.json b/tests/fixtures/tado/weather.json
new file mode 100644
index 00000000000000..72379f05512bf8
--- /dev/null
+++ b/tests/fixtures/tado/weather.json
@@ -0,0 +1,22 @@
+{
+ "outsideTemperature": {
+ "celsius": 7.46,
+ "fahrenheit": 45.43,
+ "precision": {
+ "celsius": 0.01,
+ "fahrenheit": 0.01
+ },
+ "timestamp": "2020-12-22T08:13:13.652Z",
+ "type": "TEMPERATURE"
+ },
+ "solarIntensity": {
+ "percentage": 2.1,
+ "timestamp": "2020-12-22T08:13:13.652Z",
+ "type": "PERCENTAGE"
+ },
+ "weatherState": {
+ "timestamp": "2020-12-22T08:13:13.652Z",
+ "type": "WEATHER_STATE",
+ "value": "FOGGY"
+ }
+}
diff --git a/tests/fixtures/tado/zone_default_overlay.json b/tests/fixtures/tado/zone_default_overlay.json
new file mode 100644
index 00000000000000..092b2b25d4dda1
--- /dev/null
+++ b/tests/fixtures/tado/zone_default_overlay.json
@@ -0,0 +1,5 @@
+{
+ "terminationCondition": {
+ "type": "MANUAL"
+ }
+}
diff --git a/tests/fixtures/template/sensor_configuration.yaml b/tests/fixtures/template/sensor_configuration.yaml
index 48ef4cf4304e50..8fb2ae9564fad0 100644
--- a/tests/fixtures/template/sensor_configuration.yaml
+++ b/tests/fixtures/template/sensor_configuration.yaml
@@ -21,3 +21,10 @@ sensor:
== "Watch TV" or state_attr("remote.alexander_master_bedroom","current_activity")
== "Watch Apple TV" %}on{% else %}off{% endif %}'
+template:
+ trigger:
+ platform: event
+ event_type: event_2
+ sensor:
+ name: top level 2
+ state: "{{ trigger.event.data.source }}"
diff --git a/tests/fixtures/zwave_js/aeon_smart_switch_6_state.json b/tests/fixtures/zwave_js/aeon_smart_switch_6_state.json
new file mode 100644
index 00000000000000..36db78faace357
--- /dev/null
+++ b/tests/fixtures/zwave_js/aeon_smart_switch_6_state.json
@@ -0,0 +1,1248 @@
+{
+ "nodeId": 102,
+ "index": 0,
+ "installerIcon": 1792,
+ "userIcon": 1792,
+ "status": 4,
+ "ready": true,
+ "deviceClass": {
+ "basic": {"key": 4, "label": "Routing Slave"},
+ "generic": {"key": 16, "label":"Binary Switch"},
+ "specific": {"key": 1, "label":"Binary Power Switch"},
+ "mandatorySupportedCCs": [],
+ "mandatoryControlCCs": []
+ },
+ "isListening": true,
+ "isFrequentListening": false,
+ "isRouting": true,
+ "maxBaudRate": 40000,
+ "isSecure": true,
+ "version": 4,
+ "isBeaming": true,
+ "manufacturerId": 134,
+ "productId": 96,
+ "productType": 3,
+ "firmwareVersion": "1.1",
+ "zwavePlusVersion": 1,
+ "nodeType": 0,
+ "roleType": 5,
+ "deviceConfig": {
+ "manufacturerId": 134,
+ "manufacturer": "AEON Labs",
+ "label": "ZW096",
+ "description": "Smart Switch 6",
+ "devices": [
+ { "productType": "0x0003", "productId": "0x0060" },
+ { "productType": "0x0103", "productId": "0x0060" },
+ { "productType": "0x0203", "productId": "0x0060" },
+ { "productType": "0x1d03", "productId": "0x0060" }
+ ],
+ "firmwareVersion": { "min": "0.0", "max": "255.255" },
+ "associations": {},
+ "paramInformation": { "_map": {} }
+ },
+ "label": "ZW096",
+ "neighbors": [1, 63, 90, 117],
+ "interviewAttempts": 1,
+ "interviewStage": 7,
+ "endpoints": [
+ {
+ "nodeId": 102,
+ "index": 0,
+ "installerIcon": 1792,
+ "userIcon": 1792
+ }
+ ],
+ "commandClasses": [],
+ "values": [
+ {
+ "endpoint": 0,
+ "commandClass": 37,
+ "commandClassName": "Binary Switch",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": false,
+ "label": "Current value"
+ },
+ "value": true
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 37,
+ "commandClassName": "Binary Switch",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Target value"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 99,
+ "label": "Target value"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "duration",
+ "propertyName": "duration",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "duration",
+ "readable": true,
+ "writeable": true,
+ "label": "Transition duration"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 99,
+ "label": "Current value"
+ },
+ "value": 99
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "Up",
+ "propertyName": "Up",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Perform a level change (Up)",
+ "ccSpecific": { "switchType": 2 }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "Down",
+ "propertyName": "Down",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Perform a level change (Down)",
+ "ccSpecific": { "switchType": 2 }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "value",
+ "propertyKey": 65537,
+ "propertyName": "value",
+ "propertyKeyName": "Electric_kWh_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [kWh]",
+ "unit": "kWh",
+ "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 }
+ },
+ "value": 659.813
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "previousValue",
+ "propertyKey": 65537,
+ "propertyName": "previousValue",
+ "propertyKeyName": "Electric_kWh_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [kWh] (prev. value)",
+ "unit": "kWh",
+ "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 }
+ },
+ "value": 659.813
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "deltaTime",
+ "propertyKey": 65537,
+ "propertyName": "deltaTime",
+ "propertyKeyName": "Electric_kWh_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [kWh] (prev. time delta)",
+ "unit": "s",
+ "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 }
+ },
+ "value": 1200
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "value",
+ "propertyKey": 66049,
+ "propertyName": "value",
+ "propertyKeyName": "Electric_W_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [W]",
+ "unit": "W",
+ "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "deltaTime",
+ "propertyKey": 66049,
+ "propertyName": "deltaTime",
+ "propertyKeyName": "Electric_W_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [W] (prev. time delta)",
+ "unit": "s",
+ "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "value",
+ "propertyKey": 66561,
+ "propertyName": "value",
+ "propertyKeyName": "Electric_V_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [V]",
+ "unit": "V",
+ "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 4 }
+ },
+ "value": 229.935
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "deltaTime",
+ "propertyKey": 66561,
+ "propertyName": "deltaTime",
+ "propertyKeyName": "Electric_V_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [V] (prev. time delta)",
+ "unit": "s",
+ "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 4 }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "value",
+ "propertyKey": 66817,
+ "propertyName": "value",
+ "propertyKeyName": "Electric_A_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [A]",
+ "unit": "A",
+ "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 5 }
+ },
+ "value": 9.699
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "deltaTime",
+ "propertyKey": 66817,
+ "propertyName": "deltaTime",
+ "propertyKeyName": "Electric_A_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [A] (prev. time delta)",
+ "unit": "s",
+ "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 5 }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "reset",
+ "propertyName": "reset",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "boolean",
+ "readable": false,
+ "writeable": true,
+ "label": "Reset accumulated values"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "previousValue",
+ "propertyKey": 66049,
+ "propertyName": "previousValue",
+ "propertyKeyName": "Electric_W_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [W] (prev. value)",
+ "unit": "W",
+ "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "previousValue",
+ "propertyKey": 66561,
+ "propertyName": "previousValue",
+ "propertyKeyName": "Electric_V_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [V] (prev. value)",
+ "unit": "V",
+ "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 4 }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "previousValue",
+ "propertyKey": 66817,
+ "propertyName": "previousValue",
+ "propertyKeyName": "Electric_A_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [A] (prev. value)",
+ "unit": "A",
+ "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 5 }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 51,
+ "commandClassName": "Color Switch",
+ "property": "duration",
+ "propertyName": "duration",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "duration",
+ "readable": true,
+ "writeable": true,
+ "label": "Remaining duration"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 51,
+ "commandClassName": "Color Switch",
+ "property": "currentColor",
+ "propertyKey": 2,
+ "propertyName": "currentColor",
+ "propertyKeyName": "Red",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Current value (Red)",
+ "description": "The current value of the Red color."
+ },
+ "value": 27
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 51,
+ "commandClassName": "Color Switch",
+ "property": "hexColor",
+ "propertyName": "hexColor",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "color",
+ "readable": true,
+ "writeable": true,
+ "minLength": 6,
+ "maxLength": 7,
+ "label": "RGB Color"
+ },
+ "value": "1b141b"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 51,
+ "commandClassName": "Color Switch",
+ "property": "currentColor",
+ "propertyKey": 3,
+ "propertyName": "currentColor",
+ "propertyKeyName": "Green",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Current value (Green)",
+ "description": "The current value of the Green color."
+ },
+ "value": 20
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 51,
+ "commandClassName": "Color Switch",
+ "property": "currentColor",
+ "propertyKey": 4,
+ "propertyName": "currentColor",
+ "propertyKeyName": "Blue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Current value (Blue)",
+ "description": "The current value of the Blue color."
+ },
+ "value": 27
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 51,
+ "commandClassName": "Color Switch",
+ "property": "targetColor",
+ "propertyKey": 2,
+ "propertyName": "targetColor",
+ "propertyKeyName": "Red",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 255,
+ "label": "Target value (Red)",
+ "description": "The target value of the Red color."
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 51,
+ "commandClassName": "Color Switch",
+ "property": "targetColor",
+ "propertyKey": 3,
+ "propertyName": "targetColor",
+ "propertyKeyName": "Green",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 255,
+ "label": "Target value (Green)",
+ "description": "The target value of the Green color."
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 51,
+ "commandClassName": "Color Switch",
+ "property": "targetColor",
+ "propertyKey": 4,
+ "propertyName": "targetColor",
+ "propertyKeyName": "Blue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 255,
+ "label": "Target value (Blue)",
+ "description": "The target value of the Blue color."
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 3,
+ "propertyName": "Current overload protection enable",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": { "0": "disabled", "1": "enabled" },
+ "label": "Current overload protection enable",
+ "isFromConfig": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 20,
+ "propertyName": "Output load after re-power",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 2,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "last status",
+ "1": "always on",
+ "2": "always off"
+ },
+ "label": "Output load after re-power",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 80,
+ "propertyName": "Enable send to associated devices",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 2,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "nothing",
+ "1": "hail CC",
+ "2": "basic CC report"
+ },
+ "label": "Enable send to associated devices",
+ "description": "Enable to send notifications to Group 1",
+ "isFromConfig": true
+ },
+ "value": 2
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 81,
+ "propertyName": "Configure LED state",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 2,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "LED follows load",
+ "1": "LED follows load for 5 seconds",
+ "2": "Night light mode"
+ },
+ "label": "Configure LED state",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 90,
+ "propertyName": "Enable items 91 and 92",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": { "0": "disabled", "1": "enabled" },
+ "label": "Enable items 91 and 92",
+ "isFromConfig": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 91,
+ "propertyName": "Wattage Threshold",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 2,
+ "min": 0,
+ "max": 60000,
+ "default": 25,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Wattage Threshold",
+ "description": "minimum change in wattage to trigger",
+ "isFromConfig": true
+ },
+ "value": 100
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 92,
+ "propertyName": "Wattage Percent Change",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 100,
+ "default": 5,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Wattage Percent Change",
+ "description": "minimum change in wattage percent",
+ "isFromConfig": true
+ },
+ "value": 100
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 101,
+ "propertyName": "Values to send to group 1",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 0,
+ "max": 15,
+ "default": 4,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Nothing",
+ "1": "Voltage",
+ "2": "Current",
+ "4": "Wattage",
+ "8": "kWh",
+ "15": "All Values"
+ },
+ "label": "Values to send to group 1",
+ "description": "Which reports need to send in Report group 1",
+ "isFromConfig": true
+ },
+ "value": 8
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 102,
+ "propertyName": "Values to send to group 2",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 0,
+ "max": 15,
+ "default": 8,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Nothing",
+ "1": "Voltage",
+ "2": "Current",
+ "4": "Wattage",
+ "8": "kWh",
+ "15": "All Values"
+ },
+ "label": "Values to send to group 2",
+ "description": "Which reports need to send in Report group 2",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 103,
+ "propertyName": "Values to send to group 3",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 0,
+ "max": 15,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Nothing",
+ "1": "Voltage",
+ "2": "Current",
+ "4": "Wattage",
+ "8": "kWh",
+ "15": "All Values"
+ },
+ "label": "Values to send to group 3",
+ "description": "Which reports need to send in Report group 3",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 111,
+ "propertyName": "Time interval for sending to group 1",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 1,
+ "max": 2147483647,
+ "default": 3,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Time interval for sending to group 1",
+ "description": "Group 1 automatic update interval",
+ "isFromConfig": true
+ },
+ "value": 1200
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 112,
+ "propertyName": "Time interval for sending to group 2",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 1,
+ "max": 2147483647,
+ "default": 600,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Time interval for sending to group 2",
+ "description": "Group 2 automatic update interval",
+ "isFromConfig": true
+ },
+ "value": 120
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 113,
+ "propertyName": "Time interval for sending to group 3",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 1,
+ "max": 2147483647,
+ "default": 600,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Time interval for sending to group 3",
+ "description": "Group 3 automatic update interval",
+ "isFromConfig": true
+ },
+ "value": 65460
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 252,
+ "propertyName": "Configuration Locked",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": { "0": "disabled", "1": "enabled" },
+ "label": "Configuration Locked",
+ "description": "Enable/disable Configuration Locked (0 =disable, 1 = enable).",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 83,
+ "propertyKey": 255,
+ "propertyName": "Blue night light color value",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 3,
+ "min": 0,
+ "max": 255,
+ "default": 221,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Blue night light color value",
+ "isFromConfig": true
+ },
+ "value": 27
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 83,
+ "propertyKey": 65280,
+ "propertyName": "Green night light color value",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 3,
+ "min": 0,
+ "max": 255,
+ "default": 160,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Green night light color value",
+ "isFromConfig": true
+ },
+ "value": 20
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 83,
+ "propertyKey": 16711680,
+ "propertyName": "Red night light color value",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 3,
+ "min": 0,
+ "max": 255,
+ "default": 221,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Red night light color value",
+ "description": "Configure the RGB value when it is in Night light mode",
+ "isFromConfig": true
+ },
+ "value": 27
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 84,
+ "propertyKey": 255,
+ "propertyName": "Green brightness in energy mode (%)",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 3,
+ "min": 0,
+ "max": 100,
+ "default": 50,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Green brightness in energy mode (%)",
+ "isFromConfig": true
+ },
+ "value": 50
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 84,
+ "propertyKey": 65280,
+ "propertyName": "Yellow brightness in energy mode (%)",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 3,
+ "min": 0,
+ "max": 100,
+ "default": 50,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Yellow brightness in energy mode (%)",
+ "isFromConfig": true
+ },
+ "value": 50
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 84,
+ "propertyKey": 16711680,
+ "propertyName": "Red brightness in energy mode (%)",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 3,
+ "min": 0,
+ "max": 100,
+ "default": 50,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Red brightness in energy mode (%)",
+ "isFromConfig": true
+ },
+ "value": 50
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 33,
+ "propertyName": "RGB LED color testing",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": false,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 0,
+ "max": 0,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "RGB LED color testing",
+ "isFromConfig": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 100,
+ "propertyName": "Set 101\u2010103 to default.",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": false,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": { "0": "False", "1": "True" },
+ "label": "Set 101\u2010103 to default.",
+ "isFromConfig": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 110,
+ "propertyName": "Set 111\u2010113 to default.",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": false,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": { "0": "False", "1": "True" },
+ "label": "Set 111\u2010113 to default.",
+ "isFromConfig": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 255,
+ "propertyName": "RESET",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": false,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "RESET",
+ "description": "Reset the device to defaults",
+ "isFromConfig": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Manufacturer ID"
+ },
+ "value": 134
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product type"
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product ID"
+ },
+ "value": 96
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ },
+ "value": "4.54"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ },
+ "value": ["1.1"]
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "hardwareVersion",
+ "propertyName": "hardwareVersion",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip hardware version"
+ }
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/tests/fixtures/zwave_js/aeotec_radiator_thermostat_state.json b/tests/fixtures/zwave_js/aeotec_radiator_thermostat_state.json
new file mode 100644
index 00000000000000..27c3f991d33334
--- /dev/null
+++ b/tests/fixtures/zwave_js/aeotec_radiator_thermostat_state.json
@@ -0,0 +1,620 @@
+{
+ "nodeId": 4,
+ "index": 0,
+ "installerIcon": 4608,
+ "userIcon": 4608,
+ "status": 4,
+ "ready": true,
+ "deviceClass": {
+ "basic": {"key": 4, "label":"Routing Slave"},
+ "generic": {"key": 8, "label":"Thermostat"},
+ "specific": {"key": 6, "label":"Thermostat General V2"},
+ "mandatorySupportedCCs": [],
+ "mandatoryControlCCs": []
+ },
+ "isListening": false,
+ "isFrequentListening": true,
+ "isRouting": true,
+ "maxBaudRate": 40000,
+ "isSecure": false,
+ "version": 4,
+ "isBeaming": true,
+ "manufacturerId": 881,
+ "productId": 21,
+ "productType": 2,
+ "firmwareVersion": "0.16",
+ "zwavePlusVersion": 1,
+ "nodeType": 0,
+ "roleType": 7,
+ "deviceConfig": {
+ "manufacturerId": 881,
+ "manufacturer": "Aeotec Ltd.",
+ "label": "Radiator Thermostat",
+ "description": "Thermostat - HVAC",
+ "devices": [{ "productType": "0x0002", "productId": "0x0015" }],
+ "firmwareVersion": { "min": "0.0", "max": "255.255" },
+ "paramInformation": { "_map": {} }
+ },
+ "label": "Radiator Thermostat",
+ "neighbors": [6, 7, 45, 67],
+ "interviewAttempts": 1,
+ "endpoints": [
+ { "nodeId": 4, "index": 0, "installerIcon": 4608, "userIcon": 4608 }
+ ],
+ "values": [
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 99,
+ "label": "Target value"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "duration",
+ "propertyName": "duration",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "duration",
+ "readable": true,
+ "writeable": true,
+ "label": "Transition duration"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 99,
+ "label": "Current value"
+ },
+ "value": 75
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "Up",
+ "propertyName": "Up",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Perform a level change (Up)",
+ "ccSpecific": { "switchType": 2 }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "Down",
+ "propertyName": "Down",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Perform a level change (Down)",
+ "ccSpecific": { "switchType": 2 }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 49,
+ "commandClassName": "Multilevel Sensor",
+ "property": "Air temperature",
+ "propertyName": "Air temperature",
+ "ccVersion": 5,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "unit": "\u00b0C",
+ "label": "Air temperature",
+ "ccSpecific": { "sensorType": 1, "scale": 0 }
+ },
+ "value": 19.37
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "mode",
+ "propertyName": "mode",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 31,
+ "label": "Thermostat mode",
+ "states": {
+ "0": "Off",
+ "1": "Heat",
+ "11": "Energy heat",
+ "15": "Full power"
+ }
+ },
+ "value": 31
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "manufacturerData",
+ "propertyName": "manufacturerData",
+ "ccVersion": 3,
+ "metadata": { "type": "any", "readable": true, "writeable": true }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 67,
+ "commandClassName": "Thermostat Setpoint",
+ "property": "setpoint",
+ "propertyName": "setpoint",
+ "propertyKeyName": "Heating",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 8,
+ "max": 28,
+ "unit": "\u00b0C",
+ "ccSpecific": { "setpointType": 1 }
+ },
+ "value": 24
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 67,
+ "commandClassName": "Thermostat Setpoint",
+ "property": "setpoint",
+ "propertyName": "setpoint",
+ "propertyKeyName": "Energy Save Heating",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 8,
+ "max": 28,
+ "unit": "\u00b0C",
+ "ccSpecific": { "setpointType": 11 }
+ },
+ "value": 18
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 1,
+ "propertyName": "Invert LCD orientation",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Normal orientation",
+ "1": "LCD content inverted"
+ },
+ "label": "Invert LCD orientation",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 2,
+ "propertyName": "LCD Timeout",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 30,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "LCD Timeout",
+ "description": "LCD Timeout in seconds",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 3,
+ "propertyName": "Backlight",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Backlight disabled",
+ "1": "Backlight enabled"
+ },
+ "label": "Backlight",
+ "isFromConfig": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 4,
+ "propertyName": "Battery report",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Battery reporting disabled",
+ "1": "Battery reporting enabled"
+ },
+ "label": "Battery report",
+ "description": "Battery reporting",
+ "isFromConfig": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 5,
+ "propertyName": "Measured Temperature",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 50,
+ "default": 5,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Measured Temperature",
+ "description": "Measured Temperature report. Reporting Delta in 1/10 Celsius. '0' to disable reporting.",
+ "isFromConfig": true
+ },
+ "value": 5
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 6,
+ "propertyName": "Valve position",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 100,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Valve position",
+ "description": "Valve position report. Reporting delta in percent. '0' to disable reporting.",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 7,
+ "propertyName": "Window open detection",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 3,
+ "default": 2,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Detection Disabled",
+ "1": "Sensitivity low",
+ "2": "Sensitivity medium",
+ "3": "Sensitivity high"
+ },
+ "label": "Window open detection",
+ "description": "Control 'Window open detection' sensitivity",
+ "isFromConfig": true
+ },
+ "value": 2
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 8,
+ "propertyName": "Temperature Offset",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": -128,
+ "max": 50,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Temperature Offset",
+ "description": "Measured Temperature offset",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 113,
+ "commandClassName": "Notification",
+ "property": "Power Management",
+ "propertyName": "Power Management",
+ "propertyKeyName": "Battery maintenance status",
+ "ccVersion": 8,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Battery maintenance status",
+ "states": {
+ "0": "idle",
+ "10": "Replace battery soon",
+ "11": "Replace battery now"
+ },
+ "ccSpecific": { "notificationType": 8 }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 113,
+ "commandClassName": "Notification",
+ "property": "System",
+ "propertyName": "System",
+ "propertyKeyName": "Hardware status",
+ "ccVersion": 8,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Hardware status",
+ "states": {
+ "0": "idle",
+ "3": "System hardware failure (with failure code)"
+ },
+ "ccSpecific": { "notificationType": 9 }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Manufacturer ID"
+ },
+ "value": 881
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product type"
+ },
+ "value": 2
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product ID"
+ },
+ "value": 21
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 117,
+ "commandClassName": "Protection",
+ "property": "local",
+ "propertyName": "local",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Local protection state",
+ "states": {
+ "0": "Unprotected",
+ "1": "ProtectedBySequence",
+ "2": "NoOperationPossible"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 128,
+ "commandClassName": "Battery",
+ "property": "level",
+ "propertyName": "level",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 100,
+ "unit": "%",
+ "label": "Battery level"
+ },
+ "value": 100
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 128,
+ "commandClassName": "Battery",
+ "property": "isLow",
+ "propertyName": "isLow",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": false,
+ "label": "Low battery level"
+ },
+ "value": false
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ },
+ "value": "4.61"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ },
+ "value": ["0.16"]
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "hardwareVersion",
+ "propertyName": "hardwareVersion",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip hardware version"
+ }
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/tests/fixtures/zwave_js/bulb_6_multi_color_state.json b/tests/fixtures/zwave_js/bulb_6_multi_color_state.json
index b7c422121c9c4b..64bfecfb20b4e2 100644
--- a/tests/fixtures/zwave_js/bulb_6_multi_color_state.json
+++ b/tests/fixtures/zwave_js/bulb_6_multi_color_state.json
@@ -6,14 +6,10 @@
"status": 4,
"ready": true,
"deviceClass": {
- "basic": "Static Controller",
- "generic": "Multilevel Switch",
- "specific": "Multilevel Power Switch",
- "mandatorySupportedCCs": [
- "Basic",
- "Multilevel Switch",
- "All Switch"
- ],
+ "basic": {"key": 2, "label": "Static Controller"},
+ "generic": {"key": 17, "label":"Multilevel Switch"},
+ "specific": {"key": 1, "label":"Multilevel Power Switch"},
+ "mandatorySupportedCCs": [],
"mandatoryControlCCs": []
},
"isListening": true,
@@ -67,6 +63,7 @@
"userIcon": 1536
}
],
+ "commandClasses": [],
"values": [
{
"commandClassName": "Multilevel Switch",
diff --git a/tests/fixtures/zwave_js/chain_actuator_zws12_state.json b/tests/fixtures/zwave_js/chain_actuator_zws12_state.json
index dbae35e04d0729..cf7adddc21e1cc 100644
--- a/tests/fixtures/zwave_js/chain_actuator_zws12_state.json
+++ b/tests/fixtures/zwave_js/chain_actuator_zws12_state.json
@@ -6,16 +6,10 @@
"status": 4,
"ready": true,
"deviceClass": {
- "basic": "Routing Slave",
- "generic": "Multilevel Switch",
- "specific": "Motor Control Class C",
- "mandatorySupportedCCs": [
- "Basic",
- "Multilevel Switch",
- "Binary Switch",
- "Manufacturer Specific",
- "Version"
- ],
+ "basic": {"key": 4, "label":"Routing Slave"},
+ "generic": {"key": 17, "label":"Multilevel Switch"},
+ "specific": {"key": 7, "label":"Motor Control Class C"},
+ "mandatorySupportedCCs": [],
"mandatoryControlCCs": []
},
"isListening": true,
@@ -52,6 +46,7 @@
"endpoints": [
{ "nodeId": 6, "index": 0, "installerIcon": 6656, "userIcon": 6656 }
],
+ "commandClasses": [],
"values": [
{
"commandClassName": "Multilevel Switch",
diff --git a/tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json b/tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json
new file mode 100644
index 00000000000000..8574674714f073
--- /dev/null
+++ b/tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json
@@ -0,0 +1,436 @@
+{
+ "nodeId": 5,
+ "index": 0,
+ "status": 1,
+ "ready": true,
+ "deviceClass": {
+ "basic": {
+ "key": 4,
+ "label": "Routing Slave"
+ },
+ "generic": {
+ "key": 8,
+ "label": "Thermostat"
+ },
+ "specific": {
+ "key": 4,
+ "label": "Setpoint Thermostat"
+ },
+ "mandatorySupportedCCs": [
+ 114,
+ 143,
+ 67,
+ 134
+ ],
+ "mandatoryControlledCCs": []
+ },
+ "isListening": false,
+ "isFrequentListening": false,
+ "isRouting": true,
+ "maxBaudRate": 40000,
+ "isSecure": false,
+ "version": 4,
+ "isBeaming": true,
+ "manufacturerId": 2,
+ "productId": 4,
+ "productType": 5,
+ "firmwareVersion": "1.1",
+ "deviceConfig": {
+ "filename": "/usr/src/app/node_modules/@zwave-js/config/config/devices/0x0002/lc-13.json",
+ "manufacturerId": 2,
+ "manufacturer": "Danfoss",
+ "label": "LC-13",
+ "description": "Living Connect Z Thermostat",
+ "devices": [
+ {
+ "productType": "0x0005",
+ "productId": "0x0004"
+ },
+ {
+ "productType": "0x8005",
+ "productId": "0x0001"
+ },
+ {
+ "productType": "0x8005",
+ "productId": "0x0002"
+ }
+ ],
+ "firmwareVersion": {
+ "min": "0.0",
+ "max": "255.255"
+ },
+ "associations": {},
+ "compat": {
+ "valueIdRegex": {},
+ "queryOnWakeup": [
+ [
+ "Battery",
+ "get"
+ ],
+ [
+ "Thermostat Setpoint",
+ "get",
+ 1
+ ]
+ ]
+ }
+ },
+ "label": "LC-13",
+ "neighbors": [
+ 1,
+ 14
+ ],
+ "interviewAttempts": 1,
+ "interviewStage": 7,
+ "commandClasses": [
+ {
+ "id": 67,
+ "name": "Thermostat Setpoint",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 70,
+ "name": "Climate Control Schedule",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 114,
+ "name": "Manufacturer Specific",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 117,
+ "name": "Protection",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 128,
+ "name": "Battery",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 129,
+ "name": "Clock",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 132,
+ "name": "Wake Up",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 134,
+ "name": "Version",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 143,
+ "name": "Multi Command",
+ "version": 1,
+ "isSecure": false
+ }
+ ],
+ "endpoints": [
+ {
+ "nodeId": 5,
+ "index": 0
+ }
+ ],
+ "values": [
+ {
+ "endpoint": 0,
+ "commandClass": 67,
+ "commandClassName": "Thermostat Setpoint",
+ "property": "setpoint",
+ "propertyKey": 1,
+ "propertyName": "setpoint",
+ "propertyKeyName": "Heating",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "unit": "\u00b0C",
+ "ccSpecific": {
+ "setpointType": 1
+ }
+ },
+ "value": 14
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 70,
+ "commandClassName": "Climate Control Schedule",
+ "property": "changeCounter",
+ "propertyName": "changeCounter",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 70,
+ "commandClassName": "Climate Control Schedule",
+ "property": "overrideType",
+ "propertyName": "overrideType",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 70,
+ "commandClassName": "Climate Control Schedule",
+ "property": "overrideState",
+ "propertyName": "overrideState",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": "Unused"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Manufacturer ID"
+ },
+ "value": 2
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product type"
+ },
+ "value": 5
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product ID"
+ },
+ "value": 4
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 117,
+ "commandClassName": "Protection",
+ "property": "local",
+ "propertyName": "local",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Local protection state",
+ "states": {
+ "0": "Unprotected",
+ "2": "NoOperationPossible"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 117,
+ "commandClassName": "Protection",
+ "property": "rf",
+ "propertyName": "rf",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "RF protection state",
+ "states": {}
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 117,
+ "commandClassName": "Protection",
+ "property": "exclusiveControlNodeId",
+ "propertyName": "exclusiveControlNodeId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 117,
+ "commandClassName": "Protection",
+ "property": "timeout",
+ "propertyName": "timeout",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 128,
+ "commandClassName": "Battery",
+ "property": "level",
+ "propertyName": "level",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 100,
+ "unit": "%",
+ "label": "Battery level"
+ },
+ "value": 49
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 128,
+ "commandClassName": "Battery",
+ "property": "isLow",
+ "propertyName": "isLow",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": false,
+ "label": "Low battery level"
+ },
+ "value": false
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 132,
+ "commandClassName": "Wake Up",
+ "property": "wakeUpInterval",
+ "propertyName": "wakeUpInterval",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": false,
+ "writeable": true,
+ "min": 60,
+ "max": 1800,
+ "label": "Wake Up interval",
+ "steps": 60,
+ "default": 300
+ },
+ "value": 300
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 132,
+ "commandClassName": "Wake Up",
+ "property": "controllerNodeId",
+ "propertyName": "controllerNodeId",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Node ID of the controller"
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ },
+ "value": 6
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ },
+ "value": "3.67"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ },
+ "value": [
+ "1.1"
+ ]
+ }
+ ]
+}
diff --git a/tests/fixtures/zwave_js/climate_eurotronic_spirit_z_state.json b/tests/fixtures/zwave_js/climate_eurotronic_spirit_z_state.json
new file mode 100644
index 00000000000000..8dff31a52256a8
--- /dev/null
+++ b/tests/fixtures/zwave_js/climate_eurotronic_spirit_z_state.json
@@ -0,0 +1,716 @@
+{
+ "nodeId": 8,
+ "index": 0,
+ "installerIcon": 4608,
+ "userIcon": 4608,
+ "status": 4,
+ "ready": true,
+ "deviceClass": {
+ "basic": {
+ "key": 4,
+ "label": "Routing Slave"
+ },
+ "generic": {
+ "key": 8,
+ "label": "Thermostat"
+ },
+ "specific": {
+ "key": 6,
+ "label": "Thermostat General V2"
+ },
+ "mandatorySupportedCCs": [
+ "Basic",
+ "Manufacturer Specific",
+ "Thermostat Mode",
+ "Thermostat Setpoint",
+ "Version"
+ ],
+ "mandatoryControlCCs": []
+ },
+ "isListening": false,
+ "isFrequentListening": true,
+ "isRouting": true,
+ "maxBaudRate": 40000,
+ "isSecure": false,
+ "version": 4,
+ "isBeaming": true,
+ "manufacturerId": 328,
+ "productId": 1,
+ "productType": 3,
+ "firmwareVersion": "0.16",
+ "zwavePlusVersion": 1,
+ "nodeType": 0,
+ "roleType": 7,
+ "deviceConfig": {
+ "manufacturerId": 328,
+ "manufacturer": "Eurotronics",
+ "label": "Spirit",
+ "description": "Thermostatic Valve",
+ "devices": [
+ {
+ "productType": "0x0003",
+ "productId": "0x0001"
+ },
+ {
+ "productType": "0x0003",
+ "productId": "0x0003"
+ }
+ ],
+ "firmwareVersion": {
+ "min": "0.0",
+ "max": "255.255"
+ },
+ "paramInformation": {
+ "_map": {}
+ }
+ },
+ "label": "Spirit",
+ "neighbors": [
+ 1,
+ 5,
+ 9,
+ 10,
+ 12,
+ 18,
+ 20,
+ 21,
+ 22
+ ],
+ "interviewAttempts": 1,
+ "endpoints": [
+ {
+ "nodeId": 8,
+ "index": 0,
+ "installerIcon": 4608,
+ "userIcon": 4608
+ }
+ ],
+ "values": [
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 99,
+ "label": "Target value"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "duration",
+ "propertyName": "duration",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "duration",
+ "readable": true,
+ "writeable": true,
+ "label": "Transition duration"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 99,
+ "label": "Current value"
+ },
+ "value": 8
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "Up",
+ "propertyName": "Up",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Perform a level change (Up)",
+ "ccSpecific": {
+ "switchType": 2
+ }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "Down",
+ "propertyName": "Down",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Perform a level change (Down)",
+ "ccSpecific": {
+ "switchType": 2
+ }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 49,
+ "commandClassName": "Multilevel Sensor",
+ "property": "Air temperature",
+ "propertyName": "Air temperature",
+ "ccVersion": 5,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "unit": "°C",
+ "label": "Air temperature",
+ "ccSpecific": {
+ "sensorType": 1,
+ "scale": 0
+ }
+ },
+ "value": 23.73
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "mode",
+ "propertyName": "mode",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 31,
+ "label": "Thermostat mode",
+ "states": {
+ "0": "Off",
+ "1": "Heat",
+ "11": "Energy heat",
+ "15": "Full power"
+ }
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "manufacturerData",
+ "propertyName": "manufacturerData",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 67,
+ "commandClassName": "Thermostat Setpoint",
+ "property": "setpoint",
+ "propertyKey": 1,
+ "propertyName": "setpoint",
+ "propertyKeyName": "Heating",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 8,
+ "max": 28,
+ "unit": "°C",
+ "ccSpecific": {
+ "setpointType": 1
+ }
+ },
+ "value": 22
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 67,
+ "commandClassName": "Thermostat Setpoint",
+ "property": "setpoint",
+ "propertyKey": 11,
+ "propertyName": "setpoint",
+ "propertyKeyName": "Energy Save Heating",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 8,
+ "max": 28,
+ "unit": "°C",
+ "ccSpecific": {
+ "setpointType": 11
+ }
+ },
+ "value": 18
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 1,
+ "propertyName": "LCD Invert",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "LCD-content normal",
+ "1": "LCD-content inverted (UK Edition)"
+ },
+ "label": "LCD Invert",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 2,
+ "propertyName": "LCD Timeout",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 30,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "LCD Timeout",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 3,
+ "propertyName": "Backlight",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Backlight disabled",
+ "1": "Backlight enabled"
+ },
+ "label": "Backlight",
+ "isFromConfig": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 4,
+ "propertyName": "Battery report",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "system notification",
+ "1": "Send battery status unsolicited once a day."
+ },
+ "label": "Battery report",
+ "isFromConfig": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 5,
+ "propertyName": "Measured Temperature report",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 50,
+ "default": 5,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Measured Temperature report",
+ "isFromConfig": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 6,
+ "propertyName": "Valve opening percentage report",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 100,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Valve opening percentage report",
+ "isFromConfig": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 7,
+ "propertyName": "Window open detection",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 3,
+ "default": 2,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Disabled",
+ "1": "Sensitivity low",
+ "2": "Sensitivity medium",
+ "3": "Sensitivity high"
+ },
+ "label": "Window open detection",
+ "isFromConfig": true
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 8,
+ "propertyName": "Temperature Offset",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": -128,
+ "max": 50,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Temperature Offset",
+ "description": "Measured Temperature offset",
+ "isFromConfig": true
+ },
+ "value": 10
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 113,
+ "commandClassName": "Notification",
+ "property": "alarmType",
+ "propertyName": "alarmType",
+ "ccVersion": 8,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Alarm Type"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 113,
+ "commandClassName": "Notification",
+ "property": "alarmLevel",
+ "propertyName": "alarmLevel",
+ "ccVersion": 8,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Alarm Level"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 113,
+ "commandClassName": "Notification",
+ "property": "Power Management",
+ "propertyKey": "Battery maintenance status",
+ "propertyName": "Power Management",
+ "propertyKeyName": "Battery maintenance status",
+ "ccVersion": 8,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Battery maintenance status",
+ "states": {
+ "0": "idle",
+ "10": "Replace battery soon",
+ "11": "Replace battery now"
+ },
+ "ccSpecific": {
+ "notificationType": 8
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 113,
+ "commandClassName": "Notification",
+ "property": "System",
+ "propertyKey": "Hardware status",
+ "propertyName": "System",
+ "propertyKeyName": "Hardware status",
+ "ccVersion": 8,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Hardware status",
+ "states": {
+ "0": "idle",
+ "3": "System hardware failure (with failure code)"
+ },
+ "ccSpecific": {
+ "notificationType": 9
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Manufacturer ID"
+ },
+ "value": 328
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product type"
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product ID"
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 117,
+ "commandClassName": "Protection",
+ "property": "local",
+ "propertyName": "local",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Local protection state",
+ "states": {
+ "0": "Unprotected",
+ "1": "ProtectedBySequence",
+ "2": "NoOperationPossible"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 128,
+ "commandClassName": "Battery",
+ "property": "level",
+ "propertyName": "level",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 100,
+ "unit": "%",
+ "label": "Battery level"
+ },
+ "value": 90
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 128,
+ "commandClassName": "Battery",
+ "property": "isLow",
+ "propertyName": "isLow",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": false,
+ "label": "Low battery level"
+ },
+ "value": false
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ },
+ "value": "4.61"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ },
+ "value": [
+ "0.16"
+ ]
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "hardwareVersion",
+ "propertyName": "hardwareVersion",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip hardware version"
+ }
+ }
+ ]
+}
diff --git a/tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json b/tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json
new file mode 100644
index 00000000000000..b26b69be9ad5e4
--- /dev/null
+++ b/tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json
@@ -0,0 +1,1177 @@
+{
+ "nodeId": 24,
+ "index": 0,
+ "installerIcon": 4608,
+ "userIcon": 4609,
+ "status": 4,
+ "ready": true,
+ "deviceClass": {
+ "basic": {"key": 4, "label":"Routing Slave"},
+ "generic": {"key": 8, "label":"Thermostat"},
+ "specific": {"key": 6, "label":"Thermostat General V2"},
+ "mandatorySupportedCCs": [],
+ "mandatoryControlCCs": []
+ },
+ "isListening": true,
+ "isFrequentListening": false,
+ "isRouting": true,
+ "maxBaudRate": 40000,
+ "isSecure": false,
+ "version": 4,
+ "isBeaming": true,
+ "manufacturerId": 411,
+ "productId": 515,
+ "productType": 3,
+ "firmwareVersion": "4.0",
+ "zwavePlusVersion": 1,
+ "nodeType": 0,
+ "roleType": 5,
+ "deviceConfig": {
+ "manufacturerId": 411,
+ "manufacturer": "ThermoFloor",
+ "label": "Heatit Z-TRM3",
+ "description": "Floor thermostat",
+ "devices": [
+ {
+ "productType": "0x0003",
+ "productId": "0x0203"
+ }
+ ],
+ "firmwareVersion": {
+ "min": "0.0",
+ "max": "255.255"
+ },
+ "paramInformation": {
+ "_map": {}
+ },
+ "compat": {
+ "valueIdRegex": {},
+ "overrideFloatEncoding": {
+ "size": 2
+ },
+ "addCCs": {}
+ }
+ },
+ "label": "Heatit Z-TRM3",
+ "neighbors": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 6,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 17,
+ 18,
+ 19,
+ 25,
+ 26,
+ 28
+ ],
+ "endpointCountIsDynamic": false,
+ "endpointsHaveIdenticalCapabilities": false,
+ "individualEndpointCount": 4,
+ "aggregatedEndpointCount": 0,
+ "interviewAttempts": 1,
+ "endpoints": [
+ {
+ "nodeId": 24,
+ "index": 0,
+ "installerIcon": 4608,
+ "userIcon": 4609
+ },
+ {
+ "nodeId": 24,
+ "index": 1,
+ "installerIcon": 4608,
+ "userIcon": 4609
+ },
+ {
+ "nodeId": 24,
+ "index": 2,
+ "installerIcon": 3328,
+ "userIcon": 3329
+ },
+ {
+ "nodeId": 24,
+ "index": 3,
+ "installerIcon": 3328,
+ "userIcon": 3329
+ },
+ {
+ "nodeId": 24,
+ "index": 4,
+ "installerIcon": 3328,
+ "userIcon": 3329
+ }
+ ],
+ "commandClasses": [],
+ "values": [
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 1,
+ "propertyName": "param001",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 2,
+ "propertyName": "Sensor mode",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 4,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "F-mode, floor sensor mode",
+ "1": "A-mode, internal room sensor mode",
+ "2": "AF-mode, internal sensor and floor sensor mode",
+ "3": "A2-mode, external room sensor mode",
+ "4": "A2F-mode, external sensor with floor limitation"
+ },
+ "label": "Sensor mode",
+ "description": "Sensor mode",
+ "isFromConfig": true
+ },
+ "value": 2
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 3,
+ "propertyName": "Floor sensor type",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 5,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "10K-NTC",
+ "1": "12K-NTC",
+ "2": "15K-NTC",
+ "3": "22K-NTC",
+ "4": "33K-NTC",
+ "5": "47K-NTC"
+ },
+ "label": "Floor sensor type",
+ "description": "Floor sensor type",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 4,
+ "propertyName": "Temperature control hysteresis (DIFF I)",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 3,
+ "max": 30,
+ "default": 5,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Temperature control hysteresis (DIFF I)",
+ "description": "Temperature control hysteresis (DIFF I), 1 equals 0.1 \u00b0C",
+ "isFromConfig": true
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 5,
+ "propertyName": "Floor minimum temperature limit (FLo)",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 2,
+ "min": 50,
+ "max": 400,
+ "default": 50,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Floor minimum temperature limit (FLo)",
+ "description": "Floor minimum temperature limit (FLo), 1 equals 0.1 \u00b0C",
+ "isFromConfig": true
+ },
+ "value": 50
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 6,
+ "propertyName": "Floor maximum temperature (FHi)",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 2,
+ "min": 50,
+ "max": 400,
+ "default": 400,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Floor maximum temperature (FHi)",
+ "description": "Floor maximum temperature (FHi), 1 equals 0.1 \u00b0C",
+ "isFromConfig": true
+ },
+ "value": 400
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 7,
+ "propertyName": "Air minimum temperature limit (ALo)",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 2,
+ "min": 50,
+ "max": 400,
+ "default": 50,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Air minimum temperature limit (ALo)",
+ "description": "Air minimum temperature limit (ALo), 1 equals 0.1 \u00b0C",
+ "isFromConfig": true
+ },
+ "value": 50
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 8,
+ "propertyName": "Air maximum temperature limit (AHi)",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 2,
+ "min": 50,
+ "max": 400,
+ "default": 400,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Air maximum temperature limit (AHi)",
+ "description": "Air maximum temperature limit (AHi), 1 equals 0.1 \u00b0C",
+ "isFromConfig": true
+ },
+ "value": 400
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 9,
+ "propertyName": "param009",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": 225
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 10,
+ "propertyName": "Room sensor calibration",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": -60,
+ "max": 60,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Room sensor calibration",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 11,
+ "propertyName": "Floor sensor calibration",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": -60,
+ "max": 60,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Floor sensor calibration",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 12,
+ "propertyName": "External sensor calibration",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": -60,
+ "max": 60,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "External sensor calibration",
+ "isFromConfig": true
+ },
+ "value": -42
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 13,
+ "propertyName": "Temperature display",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Display setpoint temperature",
+ "1": "Display calculated temperature"
+ },
+ "label": "Temperature display",
+ "description": "Selects which temperature is shown on the display.",
+ "isFromConfig": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 14,
+ "propertyName": "Button brightness - dimmed state",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 100,
+ "default": 50,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Button brightness - dimmed state",
+ "description": "Button brightness - dimmed state",
+ "isFromConfig": true
+ },
+ "value": 10
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 15,
+ "propertyName": "Button brightness - active state",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 100,
+ "default": 100,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Button brightness - active state",
+ "description": "Button brightness - active state",
+ "isFromConfig": true
+ },
+ "value": 50
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 16,
+ "propertyName": "Display brightness - dimmed state",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 100,
+ "default": 50,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Display brightness - dimmed state",
+ "description": "Display brightness - dimmed state",
+ "isFromConfig": true
+ },
+ "value": 5
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 17,
+ "propertyName": "Display brightness - active state",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 100,
+ "default": 100,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Display brightness - active state",
+ "description": "Display brightness - active state",
+ "isFromConfig": true
+ },
+ "value": 50
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 18,
+ "propertyName": "Temperature report interval",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 2,
+ "min": 0,
+ "max": 32767,
+ "default": 60,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Temperature report interval",
+ "description": "Temperature report interval",
+ "isFromConfig": true
+ },
+ "value": 360
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 19,
+ "propertyName": "Temperature report hysteresis",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 1,
+ "max": 100,
+ "default": 10,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Temperature report hysteresis",
+ "description": "Temperature report hysteresis",
+ "isFromConfig": true
+ },
+ "value": 10
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 20,
+ "propertyName": "Meter report interval",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 2,
+ "min": 0,
+ "max": 32767,
+ "default": 90,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Meter report interval",
+ "description": "Meter report interval",
+ "isFromConfig": true
+ },
+ "value": 3600
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 21,
+ "propertyName": "Meter report delta value",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 255,
+ "default": 10,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Meter report delta value",
+ "description": "Meter report delta value",
+ "isFromConfig": true
+ },
+ "value": 10
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Manufacturer ID"
+ },
+ "value": 411
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product type"
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product ID"
+ },
+ "value": 515
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ },
+ "value": "6.7"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ },
+ "value": [
+ "4.0"
+ ]
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "hardwareVersion",
+ "propertyName": "hardwareVersion",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip hardware version"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "sdkVersion",
+ "propertyName": "sdkVersion",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": "6.81.6"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "applicationFrameworkAPIVersion",
+ "propertyName": "applicationFrameworkAPIVersion",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": "4.3.0"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "applicationFrameworkBuildNumber",
+ "propertyName": "applicationFrameworkBuildNumber",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": 52445
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "hostInterfaceVersion",
+ "propertyName": "hostInterfaceVersion",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": "unused"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "hostInterfaceBuildNumber",
+ "propertyName": "hostInterfaceBuildNumber",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "zWaveProtocolVersion",
+ "propertyName": "zWaveProtocolVersion",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": "6.7.0"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "zWaveProtocolBuildNumber",
+ "propertyName": "zWaveProtocolBuildNumber",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": 97
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "applicationVersion",
+ "propertyName": "applicationVersion",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": "4.0.33"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "applicationBuildNumber",
+ "propertyName": "applicationBuildNumber",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": 52445
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 67,
+ "commandClassName": "Thermostat Setpoint",
+ "property": "setpoint",
+ "propertyName": "setpoint",
+ "propertyKey": 1,
+ "propertyKeyName": "Heating",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 5,
+ "max": 35,
+ "unit": "\u00b0C",
+ "ccSpecific": {
+ "setpointType": 1
+ }
+ },
+ "value": 22.5
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "mode",
+ "propertyName": "mode",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 31,
+ "label": "Thermostat mode",
+ "states": {
+ "0": "Off",
+ "1": "Heat"
+ }
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "manufacturerData",
+ "propertyName": "manufacturerData",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 66,
+ "commandClassName": "Thermostat Operating State",
+ "property": "state",
+ "propertyName": "state",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Operating state",
+ "states": {
+ "0": "Idle",
+ "1": "Heating",
+ "2": "Cooling",
+ "3": "Fan Only",
+ "4": "Pending Heat",
+ "5": "Pending Cool",
+ "6": "Vent/Economizer",
+ "7": "Aux Heating",
+ "8": "2nd Stage Heating",
+ "9": "2nd Stage Cooling",
+ "10": "2nd Stage Aux Heat",
+ "11": "3rd Stage Aux Heat"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "value",
+ "propertyName": "value",
+ "propertyKeyName": "Electric_kWh_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Value (Electric, Consumed)",
+ "unit": "kWh",
+ "ccSpecific": {
+ "meterType": 1,
+ "rateType": 1,
+ "scale": 0
+ }
+ },
+ "value": 369.2
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "deltaTime",
+ "propertyName": "deltaTime",
+ "propertyKeyName": "Electric_kWh_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Time since the previous reading",
+ "unit": "s",
+ "ccSpecific": {
+ "meterType": 1,
+ "rateType": 1,
+ "scale": 0
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "value",
+ "propertyName": "value",
+ "propertyKeyName": "Electric_W_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Value (Electric, Consumed)",
+ "unit": "W",
+ "ccSpecific": {
+ "meterType": 1,
+ "rateType": 1,
+ "scale": 2
+ }
+ },
+ "value": 0.09
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "deltaTime",
+ "propertyName": "deltaTime",
+ "propertyKeyName": "Electric_W_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Time since the previous reading",
+ "unit": "s",
+ "ccSpecific": {
+ "meterType": 1,
+ "rateType": 1,
+ "scale": 2
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "value",
+ "propertyName": "value",
+ "propertyKeyName": "Electric_V_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Value (Electric, Consumed)",
+ "unit": "V",
+ "ccSpecific": {
+ "meterType": 1,
+ "rateType": 1,
+ "scale": 4
+ }
+ },
+ "value": 238
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "deltaTime",
+ "propertyName": "deltaTime",
+ "propertyKeyName": "Electric_V_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Time since the previous reading",
+ "unit": "s",
+ "ccSpecific": {
+ "meterType": 1,
+ "rateType": 1,
+ "scale": 4
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "previousValue",
+ "propertyName": "previousValue",
+ "propertyKeyName": "Electric_kWh_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Previous value (Electric, Consumed)",
+ "unit": "kWh",
+ "ccSpecific": {
+ "meterType": 1,
+ "rateType": 1,
+ "scale": 0
+ }
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "previousValue",
+ "propertyName": "previousValue",
+ "propertyKeyName": "Electric_W_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Previous value (Electric, Consumed)",
+ "unit": "W",
+ "ccSpecific": {
+ "meterType": 1,
+ "rateType": 1,
+ "scale": 2
+ }
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "previousValue",
+ "propertyName": "previousValue",
+ "propertyKeyName": "Electric_V_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Previous value (Electric, Consumed)",
+ "unit": "V",
+ "ccSpecific": {
+ "meterType": 1,
+ "rateType": 1,
+ "scale": 4
+ }
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 49,
+ "commandClassName": "Multilevel Sensor",
+ "property": "Air temperature",
+ "propertyName": "Air temperature",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "unit": "\u00b0C",
+ "label": "Air temperature",
+ "ccSpecific": {
+ "sensorType": 1,
+ "scale": 0
+ }
+ },
+ "value": 22.9
+ },
+ {
+ "endpoint": 3,
+ "commandClass": 49,
+ "commandClassName": "Multilevel Sensor",
+ "property": "Air temperature",
+ "propertyName": "Air temperature",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "unit": "\u00b0C",
+ "label": "Air temperature",
+ "ccSpecific": {
+ "sensorType": 1,
+ "scale": 0
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 4,
+ "commandClass": 49,
+ "commandClassName": "Multilevel Sensor",
+ "property": "Air temperature",
+ "propertyName": "Air temperature",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "unit": "\u00b0C",
+ "label": "Air temperature",
+ "ccSpecific": {
+ "sensorType": 1,
+ "scale": 0
+ }
+ },
+ "value": 25.5
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json
new file mode 100644
index 00000000000000..52f2168fd83c47
--- /dev/null
+++ b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json
@@ -0,0 +1,830 @@
+{
+ "nodeId": 8,
+ "index": 0,
+ "status": 4,
+ "ready": true,
+ "isListening": true,
+ "isRouting": true,
+ "isSecure": false,
+ "manufacturerId": 152,
+ "productId": 263,
+ "productType": 25601,
+ "firmwareVersion": "9.1",
+ "deviceConfig": {
+ "filename": "/usr/src/node_modules/@zwave-js/config/config/devices/0x0098/ct100.json",
+ "manufacturer": "Radio Thermostat Company of America (RTC)",
+ "manufacturerId": 152,
+ "label": "CT100",
+ "description": "Z-Wave Thermostat",
+ "devices": [
+ {
+ "productType": 25601,
+ "productId": 21
+ },
+ {
+ "productType": 25601,
+ "productId": 259
+ },
+ {
+ "productType": 25601,
+ "productId": 261
+ },
+ {
+ "productType": 25601,
+ "productId": 262
+ },
+ {
+ "productType": 25601,
+ "productId": 263
+ },
+ {
+ "productType": 25601,
+ "productId": 509
+ },
+ {
+ "productType": 25602,
+ "productId": 1
+ }
+ ],
+ "firmwareVersion": {
+ "min": "0.0",
+ "max": "255.255"
+ },
+ "associations": {}
+ },
+ "label": "CT100",
+ "neighbors": [2, 3, 4, 5, 6, 7, 9],
+ "endpointCountIsDynamic": false,
+ "endpointsHaveIdenticalCapabilities": true,
+ "individualEndpointCount": 2,
+ "interviewAttempts": 0,
+ "interviewStage": 6,
+ "endpoints": [
+ {
+ "nodeId": 8,
+ "index": 0
+ },
+ {
+ "nodeId": 8,
+ "index": 1
+ },
+ {
+ "nodeId": 8,
+ "index": 2
+ }
+ ],
+ "values": [
+ {
+ "endpoint": 0,
+ "commandClass": 66,
+ "commandClassName": "Thermostat Operating State",
+ "property": "state",
+ "propertyName": "state",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Operating state",
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Idle",
+ "1": "Heating",
+ "2": "Cooling",
+ "3": "Fan Only",
+ "4": "Pending Heat",
+ "5": "Pending Cool",
+ "6": "Vent/Economizer",
+ "7": "Aux Heating",
+ "8": "2nd Stage Heating",
+ "9": "2nd Stage Cooling",
+ "10": "2nd Stage Aux Heat",
+ "11": "3rd Stage Aux Heat"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 67,
+ "commandClassName": "Thermostat Setpoint",
+ "property": "setpoint",
+ "propertyKey": 1,
+ "propertyName": "setpoint",
+ "propertyKeyName": "Heating",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "ccSpecific": {
+ "setpointType": 1
+ },
+ "unit": "\u00b0F"
+ },
+ "value": 69
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 67,
+ "commandClassName": "Thermostat Setpoint",
+ "property": "setpoint",
+ "propertyKey": 2,
+ "propertyName": "setpoint",
+ "propertyKeyName": "Cooling",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "ccSpecific": {
+ "setpointType": 2
+ },
+ "unit": "\u00b0F"
+ },
+ "value": 72
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 68,
+ "commandClassName": "Thermostat Fan Mode",
+ "property": "mode",
+ "propertyName": "mode",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Thermostat fan mode",
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Auto low",
+ "1": "Low"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 69,
+ "commandClassName": "Thermostat Fan State",
+ "property": "state",
+ "propertyName": "state",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Thermostat fan state",
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Idle / off",
+ "1": "Running / running low",
+ "2": "Running high",
+ "3": "Running medium",
+ "4": "Circulation mode",
+ "5": "Humidity circulation mode",
+ "6": "Right - left circulation mode",
+ "7": "Up - down circulation mode",
+ "8": "Quiet circulation mode"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Manufacturer ID",
+ "min": 0,
+ "max": 65535
+ },
+ "value": 152
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product type",
+ "min": 0,
+ "max": 65535
+ },
+ "value": 25601
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product ID",
+ "min": 0,
+ "max": 65535
+ },
+ "value": 263
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 128,
+ "commandClassName": "Battery",
+ "property": "level",
+ "propertyName": "level",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Battery level",
+ "min": 0,
+ "max": 100,
+ "unit": "%"
+ },
+ "value": 100
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 128,
+ "commandClassName": "Battery",
+ "property": "isLow",
+ "propertyName": "isLow",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": false,
+ "label": "Low battery level"
+ },
+ "value": false
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ },
+ "value": "3.28"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ },
+ "value": ["9.1"]
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 135,
+ "commandClassName": "Indicator",
+ "property": "value",
+ "propertyName": "value",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Indicator value",
+ "ccSpecific": {
+ "indicatorId": 0
+ },
+ "min": 0,
+ "max": 255
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 32,
+ "commandClassName": "Basic",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Current value",
+ "min": 0,
+ "max": 99
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 32,
+ "commandClassName": "Basic",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Target value",
+ "min": 0,
+ "max": 99
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Manufacturer ID",
+ "min": 0,
+ "max": 65535
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product type",
+ "min": 0,
+ "max": 65535
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product ID",
+ "min": 0,
+ "max": 65535
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "mode",
+ "propertyName": "mode",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Thermostat mode",
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Off",
+ "1": "Heat",
+ "2": "Cool",
+ "3": "Auto",
+ "4": "Auxiliary",
+ "5": "Resume (on)",
+ "6": "Fan",
+ "7": "Furnace",
+ "8": "Dry",
+ "9": "Moist",
+ "10": "Auto changeover",
+ "11": "Energy heat",
+ "12": "Energy cool",
+ "13": "Away",
+ "15": "Full power",
+ "31": "Manufacturer specific"
+ }
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "manufacturerData",
+ "propertyName": "manufacturerData",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 49,
+ "commandClassName": "Multilevel Sensor",
+ "property": "Air temperature",
+ "propertyName": "Air temperature",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Air temperature",
+ "ccSpecific": {
+ "sensorType": 1,
+ "scale": 1
+ },
+ "unit": "\u00b0F"
+ },
+ "value": 70
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 32,
+ "commandClassName": "Basic",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Current value",
+ "min": 0,
+ "max": 99
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 32,
+ "commandClassName": "Basic",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Target value",
+ "min": 0,
+ "max": 99
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Manufacturer ID",
+ "min": 0,
+ "max": 65535
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product type",
+ "min": 0,
+ "max": 65535
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product ID",
+ "min": 0,
+ "max": 65535
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "mode",
+ "propertyName": "mode",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Thermostat mode",
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Off",
+ "1": "Heat",
+ "2": "Cool",
+ "3": "Auto",
+ "4": "Auxiliary",
+ "5": "Resume (on)",
+ "6": "Fan",
+ "7": "Furnace",
+ "8": "Dry",
+ "9": "Moist",
+ "10": "Auto changeover",
+ "11": "Energy heat",
+ "12": "Energy cool",
+ "13": "Away",
+ "15": "Full power",
+ "31": "Manufacturer specific"
+ }
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "manufacturerData",
+ "propertyName": "manufacturerData",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 49,
+ "commandClassName": "Multilevel Sensor",
+ "property": "Humidity",
+ "propertyName": "Humidity",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Humidity",
+ "ccSpecific": {
+ "sensorType": 5,
+ "scale": 0
+ },
+ "unit": "%"
+ },
+ "value": 60
+ }
+ ],
+ "isFrequentListening": null,
+ "maxBaudRate": 40000,
+ "version": 3,
+ "deviceClass": {
+ "basic": {
+ "key": 4,
+ "label": "Routing Slave"
+ },
+ "generic": {
+ "key": 8,
+ "label": "Thermostat"
+ },
+ "specific": {
+ "key": 6,
+ "label": "General Thermostat V2"
+ },
+ "mandatorySupportedCCs": [32, 114, 64, 67, 134],
+ "mandatoryControlledCCs": []
+ },
+ "commandClasses": [
+ {
+ "id": 49,
+ "name": "Multilevel Sensor",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 64,
+ "name": "Thermostat Mode",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 66,
+ "name": "Thermostat Operating State",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 67,
+ "name": "Thermostat Setpoint",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 68,
+ "name": "Thermostat Fan Mode",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 69,
+ "name": "Thermostat Fan State",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 96,
+ "name": "Multi Channel",
+ "version": 3,
+ "isSecure": false
+ },
+ {
+ "id": 112,
+ "name": "Configuration",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 114,
+ "name": "Manufacturer Specific",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 128,
+ "name": "Battery",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 129,
+ "name": "Clock",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 133,
+ "name": "Association",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 134,
+ "name": "Version",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 135,
+ "name": "Indicator",
+ "version": 1,
+ "isSecure": false
+ }
+ ]
+}
diff --git a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json
new file mode 100644
index 00000000000000..f940dd210aa085
--- /dev/null
+++ b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json
@@ -0,0 +1,1087 @@
+{
+ "nodeId": 26,
+ "index": 0,
+ "installerIcon": 4608,
+ "userIcon": 4608,
+ "status": 4,
+ "ready": true,
+ "isListening": true,
+ "isRouting": true,
+ "isSecure": false,
+ "manufacturerId": 152,
+ "productId": 256,
+ "productType": 25602,
+ "firmwareVersion": "10.7",
+ "zwavePlusVersion": 1,
+ "deviceConfig": {
+ "filename": "/opt/node_modules/@zwave-js/config/config/devices/0x0098/ct100_plus.json",
+ "manufacturer": "Radio Thermostat Company of America (RTC)",
+ "manufacturerId": 152,
+ "label": "CT100 Plus",
+ "description": "Z-Wave Thermostat",
+ "devices": [
+ {
+ "productType": 25602,
+ "productId": 256
+ }
+ ],
+ "firmwareVersion": {
+ "min": "0.0",
+ "max": "255.255"
+ },
+ "paramInformation": {
+ "_map": {}
+ }
+ },
+ "label": "CT100 Plus",
+ "neighbors": [1, 2, 29, 3, 4, 5, 6],
+ "endpointCountIsDynamic": false,
+ "endpointsHaveIdenticalCapabilities": false,
+ "individualEndpointCount": 2,
+ "aggregatedEndpointCount": 0,
+ "interviewAttempts": 0,
+ "interviewStage": 6,
+ "endpoints": [
+ {
+ "nodeId": 26,
+ "index": 0,
+ "installerIcon": 4608,
+ "userIcon": 4608,
+ "deviceClass": {
+ "basic": {
+ "key": 4,
+ "label": "Routing Slave"
+ },
+ "generic": {
+ "key": 8,
+ "label": "Thermostat"
+ },
+ "specific": {
+ "key": 6,
+ "label": "General Thermostat V2"
+ },
+ "mandatorySupportedCCs": [32, 114, 64, 67, 134],
+ "mandatoryControlledCCs": []
+ }
+ },
+ {
+ "nodeId": 26,
+ "index": 1,
+ "deviceClass": {
+ "basic": {
+ "key": 4,
+ "label": "Routing Slave"
+ },
+ "generic": {
+ "key": 8,
+ "label": "Thermostat"
+ },
+ "specific": {
+ "key": 6,
+ "label": "General Thermostat V2"
+ },
+ "mandatorySupportedCCs": [32, 114, 64, 67, 134],
+ "mandatoryControlledCCs": []
+ }
+ },
+ {
+ "nodeId": 26,
+ "index": 2,
+ "deviceClass": {
+ "basic": {
+ "key": 4,
+ "label": "Routing Slave"
+ },
+ "generic": {
+ "key": 33,
+ "label": "Multilevel Sensor"
+ },
+ "specific": {
+ "key": 0,
+ "label": "Unused"
+ },
+ "mandatorySupportedCCs": [32, 49],
+ "mandatoryControlledCCs": []
+ }
+ }
+ ],
+ "values": [
+ {
+ "endpoint": 0,
+ "commandClass": 49,
+ "commandClassName": "Multilevel Sensor",
+ "property": "Air temperature",
+ "propertyName": "Air temperature",
+ "ccVersion": 5,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Air temperature",
+ "ccSpecific": {
+ "sensorType": 1,
+ "scale": 1
+ },
+ "unit": "\u00b0F"
+ },
+ "value": 73
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 49,
+ "commandClassName": "Multilevel Sensor",
+ "property": "Humidity",
+ "propertyName": "Humidity",
+ "ccVersion": 5,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Humidity",
+ "ccSpecific": {
+ "sensorType": 5,
+ "scale": 0
+ },
+ "unit": "%"
+ },
+ "value": 36
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 66,
+ "commandClassName": "Thermostat Operating State",
+ "property": "state",
+ "propertyName": "state",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Operating state",
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Idle",
+ "1": "Heating",
+ "2": "Cooling",
+ "3": "Fan Only",
+ "4": "Pending Heat",
+ "5": "Pending Cool",
+ "6": "Vent/Economizer",
+ "7": "Aux Heating",
+ "8": "2nd Stage Heating",
+ "9": "2nd Stage Cooling",
+ "10": "2nd Stage Aux Heat",
+ "11": "3rd Stage Aux Heat"
+ }
+ },
+ "value": 2
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 68,
+ "commandClassName": "Thermostat Fan Mode",
+ "property": "mode",
+ "propertyName": "mode",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Thermostat fan mode",
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Auto low",
+ "1": "Low"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 69,
+ "commandClassName": "Thermostat Fan State",
+ "property": "state",
+ "propertyName": "state",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Thermostat fan state",
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Idle / off",
+ "1": "Running / running low",
+ "2": "Running high",
+ "3": "Running medium",
+ "4": "Circulation mode",
+ "5": "Humidity circulation mode",
+ "6": "Right - left circulation mode",
+ "7": "Up - down circulation mode",
+ "8": "Quiet circulation mode"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 1,
+ "propertyName": "Temperature Reporting Threshold",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "description": "Reporting threshold for changes in the ambient temperature",
+ "label": "Temperature Reporting Threshold",
+ "default": 2,
+ "min": 0,
+ "max": 4,
+ "states": {
+ "0": "Disabled",
+ "1": "0.5\u00b0 F",
+ "2": "1.0\u00b0 F",
+ "3": "1.5\u00b0 F",
+ "4": "2.0\u00b0 F"
+ },
+ "valueSize": 1,
+ "format": 0,
+ "allowManualEntry": false,
+ "isFromConfig": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 2,
+ "propertyName": "HVAC Settings",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "description": "Configured HVAC settings",
+ "label": "HVAC Settings",
+ "default": 0,
+ "min": 0,
+ "max": 0,
+ "valueSize": 4,
+ "format": 0,
+ "allowManualEntry": true,
+ "isFromConfig": true
+ },
+ "value": 17891329
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 4,
+ "propertyName": "Power Status",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "description": "C-Wire / Battery Status",
+ "label": "Power Status",
+ "default": 0,
+ "min": 0,
+ "max": 0,
+ "valueSize": 1,
+ "format": 0,
+ "allowManualEntry": true,
+ "isFromConfig": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 7,
+ "propertyName": "Thermostat Swing Temperature",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "description": "Variance allowed from setpoint to engage HVAC",
+ "label": "Thermostat Swing Temperature",
+ "default": 2,
+ "min": 1,
+ "max": 8,
+ "states": {
+ "1": "0.5\u00b0 F",
+ "2": "1.0\u00b0 F",
+ "3": "1.5\u00b0 F",
+ "4": "2.0\u00b0 F",
+ "5": "2.5\u00b0 F",
+ "6": "3.0\u00b0 F",
+ "7": "3.5\u00b0 F",
+ "8": "4.0\u00b0 F"
+ },
+ "valueSize": 1,
+ "format": 0,
+ "allowManualEntry": false,
+ "isFromConfig": true
+ },
+ "value": 2
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 8,
+ "propertyName": "Thermostat Diff Temperature",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "description": "Configures additional stages",
+ "label": "Thermostat Diff Temperature",
+ "default": 4,
+ "min": 4,
+ "max": 12,
+ "states": {
+ "4": "2.0\u00b0 F",
+ "8": "4.0\u00b0 F",
+ "12": "6.0\u00b0 F"
+ },
+ "valueSize": 1,
+ "format": 0,
+ "allowManualEntry": false,
+ "isFromConfig": true
+ },
+ "value": 1028
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 9,
+ "propertyName": "Thermostat Recovery Mode",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "description": "Fast or Economy recovery mode",
+ "label": "Thermostat Recovery Mode",
+ "default": 2,
+ "min": 1,
+ "max": 2,
+ "states": {
+ "1": "Fast recovery mode",
+ "2": "Economy recovery mode"
+ },
+ "valueSize": 1,
+ "format": 0,
+ "allowManualEntry": false,
+ "isFromConfig": true
+ },
+ "value": 2
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 10,
+ "propertyName": "Temperature Reporting Filter",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "description": "Upper/Lower bounds for thermostat temperature reporting",
+ "label": "Temperature Reporting Filter",
+ "default": 124,
+ "min": 0,
+ "max": 124,
+ "valueSize": 4,
+ "format": 0,
+ "allowManualEntry": true,
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 11,
+ "propertyName": "Simple UI Mode",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "description": "Simple mode enable/disable",
+ "label": "Simple UI Mode",
+ "default": 1,
+ "min": 0,
+ "max": 1,
+ "states": {
+ "0": "Normal mode enabled",
+ "1": "Simple mode enabled"
+ },
+ "valueSize": 1,
+ "format": 0,
+ "allowManualEntry": false,
+ "isFromConfig": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 12,
+ "propertyName": "Multicast",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "description": "Enable or disables Multicast",
+ "label": "Multicast",
+ "default": 0,
+ "min": 0,
+ "max": 1,
+ "states": {
+ "0": "Multicast disabled",
+ "1": "Multicast enabled"
+ },
+ "valueSize": 1,
+ "format": 0,
+ "allowManualEntry": false,
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 3,
+ "propertyName": "Utility Lock Enable/Disable",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": false,
+ "writeable": true,
+ "description": "Prevents setpoint changes at thermostat",
+ "label": "Utility Lock Enable/Disable",
+ "default": 0,
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Utility lock disabled",
+ "1": "Utility lock enabled"
+ },
+ "valueSize": 1,
+ "format": 1,
+ "allowManualEntry": false,
+ "isFromConfig": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 5,
+ "propertyName": "Humidity Reporting Threshold",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "description": "Reporting threshold for changes in the relative humidity",
+ "label": "Humidity Reporting Threshold",
+ "default": 0,
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Disabled",
+ "1": "3% RH",
+ "2": "5% RH",
+ "3": "10% RH"
+ },
+ "valueSize": 1,
+ "format": 1,
+ "allowManualEntry": false,
+ "isFromConfig": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 6,
+ "propertyName": "Auxiliary/Emergency",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "description": "Enables or disables auxiliary / emergency heating",
+ "label": "Auxiliary/Emergency",
+ "default": 0,
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Auxiliary/Emergency heat disabled",
+ "1": "Auxiliary/Emergency heat enabled"
+ },
+ "valueSize": 1,
+ "format": 1,
+ "allowManualEntry": false,
+ "isFromConfig": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Manufacturer ID",
+ "min": 0,
+ "max": 65535
+ },
+ "value": 152
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product type",
+ "min": 0,
+ "max": 65535
+ },
+ "value": 25602
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product ID",
+ "min": 0,
+ "max": 65535
+ },
+ "value": 256
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 128,
+ "commandClassName": "Battery",
+ "property": "level",
+ "propertyName": "level",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Battery level",
+ "min": 0,
+ "max": 100,
+ "unit": "%"
+ },
+ "value": 100
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 128,
+ "commandClassName": "Battery",
+ "property": "isLow",
+ "propertyName": "isLow",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": false,
+ "label": "Low battery level"
+ },
+ "value": false
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ },
+ "value": "4.24"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ },
+ "value": ["10.7"]
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "hardwareVersion",
+ "propertyName": "hardwareVersion",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip hardware version"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 135,
+ "commandClassName": "Indicator",
+ "property": "value",
+ "propertyName": "value",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Indicator value",
+ "ccSpecific": {
+ "indicatorId": 0
+ },
+ "min": 0,
+ "max": 255
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 32,
+ "commandClassName": "Basic",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Current value",
+ "min": 0,
+ "max": 99
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 32,
+ "commandClassName": "Basic",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Target value",
+ "min": 0,
+ "max": 99
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Manufacturer ID",
+ "min": 0,
+ "max": 65535
+ },
+ "value": 152
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product type",
+ "min": 0,
+ "max": 65535
+ },
+ "value": 25602
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product ID",
+ "min": 0,
+ "max": 65535
+ },
+ "value": 256
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "mode",
+ "propertyName": "mode",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Thermostat mode",
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Off",
+ "1": "Heat",
+ "2": "Cool"
+ }
+ },
+ "value": 2
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "manufacturerData",
+ "propertyName": "manufacturerData",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 67,
+ "commandClassName": "Thermostat Setpoint",
+ "property": "setpoint",
+ "propertyKey": 1,
+ "propertyName": "setpoint",
+ "propertyKeyName": "Heating",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "ccSpecific": {
+ "setpointType": 1
+ },
+ "unit": "\u00b0F"
+ },
+ "value": 72
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 67,
+ "commandClassName": "Thermostat Setpoint",
+ "property": "setpoint",
+ "propertyKey": 2,
+ "propertyName": "setpoint",
+ "propertyKeyName": "Cooling",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "ccSpecific": {
+ "setpointType": 2
+ },
+ "unit": "\u00b0F"
+ },
+ "value": 73
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 32,
+ "commandClassName": "Basic",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Current value",
+ "min": 0,
+ "max": 99
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 32,
+ "commandClassName": "Basic",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Target value",
+ "min": 0,
+ "max": 99
+ }
+ }
+ ],
+ "isFrequentListening": false,
+ "maxDataRate": 100000,
+ "supportedDataRates": [40000, 100000],
+ "protocolVersion": 3,
+ "supportsBeaming": true,
+ "supportsSecurity": false,
+ "nodeType": 1,
+ "zwavePlusNodeType": 0,
+ "zwavePlusRoleType": 5,
+ "deviceClass": {
+ "basic": {
+ "key": 4,
+ "label": "Routing Slave"
+ },
+ "generic": {
+ "key": 8,
+ "label": "Thermostat"
+ },
+ "specific": {
+ "key": 6,
+ "label": "General Thermostat V2"
+ },
+ "mandatorySupportedCCs": [32, 114, 64, 67, 134],
+ "mandatoryControlledCCs": []
+ },
+ "commandClasses": [
+ {
+ "id": 49,
+ "name": "Multilevel Sensor",
+ "version": 5,
+ "isSecure": false
+ },
+ {
+ "id": 64,
+ "name": "Thermostat Mode",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 66,
+ "name": "Thermostat Operating State",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 67,
+ "name": "Thermostat Setpoint",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 68,
+ "name": "Thermostat Fan Mode",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 69,
+ "name": "Thermostat Fan State",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 89,
+ "name": "Association Group Information",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 90,
+ "name": "Device Reset Locally",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 94,
+ "name": "Z-Wave Plus Info",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 96,
+ "name": "Multi Channel",
+ "version": 4,
+ "isSecure": false
+ },
+ {
+ "id": 112,
+ "name": "Configuration",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 114,
+ "name": "Manufacturer Specific",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 122,
+ "name": "Firmware Update Meta Data",
+ "version": 3,
+ "isSecure": false
+ },
+ {
+ "id": 128,
+ "name": "Battery",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 129,
+ "name": "Clock",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 133,
+ "name": "Association",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 134,
+ "name": "Version",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 135,
+ "name": "Indicator",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 142,
+ "name": "Multi Channel Association",
+ "version": 3,
+ "isSecure": false
+ }
+ ]
+}
diff --git a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_state.json b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_state.json
index 77a68aafde1bcb..34df415301eb5a 100644
--- a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_state.json
+++ b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_state.json
@@ -6,16 +6,10 @@
"status": 4,
"ready": true,
"deviceClass": {
- "basic": "Static Controller",
- "generic": "Thermostat",
- "specific": "Thermostat General V2",
- "mandatorySupportedCCs": [
- "Basic",
- "Manufacturer Specific",
- "Thermostat Mode",
- "Thermostat Setpoint",
- "Version"
- ],
+ "basic": {"key": 2, "label":"Static Controller"},
+ "generic": {"key": 8, "label":"Thermostat"},
+ "specific": {"key": 6, "label":"Thermostat General V2"},
+ "mandatorySupportedCCs": [],
"mandatoryControlCCs": []
},
"isListening": true,
@@ -63,6 +57,7 @@
},
{ "nodeId": 13, "index": 2 }
],
+ "commandClasses": [],
"values": [
{
"commandClassName": "Manufacturer Specific",
@@ -594,6 +589,52 @@
"propertyName": "manufacturerData",
"metadata": { "type": "any", "readable": true, "writeable": true }
},
+ {
+ "endpoint": 1,
+ "commandClass": 68,
+ "commandClassName": "Thermostat Fan Mode",
+ "property": "mode",
+ "propertyName": "mode",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 255,
+ "states": { "0": "Auto low", "1": "Low" },
+ "label": "Thermostat fan mode"
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 69,
+ "commandClassName": "Thermostat Fan State",
+ "property": "state",
+ "propertyName": "state",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Idle / off",
+ "1": "Running / running low",
+ "2": "Running high",
+ "3": "Running medium",
+ "4": "Circulation mode",
+ "5": "Humidity circulation mode",
+ "6": "Right - left circulation mode",
+ "7": "Up - down circulation mode",
+ "8": "Quiet circulation mode"
+ },
+ "label": "Thermostat fan state"
+ },
+ "value": 0
+ },
{
"commandClassName": "Thermostat Operating State",
"commandClass": 66,
diff --git a/tests/fixtures/zwave_js/climate_radio_thermostat_ct101_multiple_temp_units_state.json b/tests/fixtures/zwave_js/climate_radio_thermostat_ct101_multiple_temp_units_state.json
new file mode 100644
index 00000000000000..5feaa247f2ef97
--- /dev/null
+++ b/tests/fixtures/zwave_js/climate_radio_thermostat_ct101_multiple_temp_units_state.json
@@ -0,0 +1,962 @@
+{
+ "nodeId": 4,
+ "index": 0,
+ "status": 4,
+ "ready": true,
+ "isListening": false,
+ "isRouting": true,
+ "isSecure": false,
+ "manufacturerId": 152,
+ "productId": 12,
+ "productType": 25857,
+ "firmwareVersion": "9.0",
+ "name": "Thermostat",
+ "deviceConfig": {
+ "filename": "/usr/src/app/node_modules/@zwave-js/config/config/devices/0x0098/ct101.json",
+ "manufacturer": "Radio Thermostat Company of America (RTC)",
+ "manufacturerId": 152,
+ "label": "CT101",
+ "description": "Z-Wave Thermostat",
+ "devices": [
+ {
+ "productType": 25857,
+ "productId": 0
+ },
+ {
+ "productType": 25857,
+ "productId": 11
+ },
+ {
+ "productType": 25857,
+ "productId": 12
+ },
+ {
+ "productType": 25857,
+ "productId": 13
+ }
+ ],
+ "firmwareVersion": {
+ "min": "0.0",
+ "max": "255.255"
+ },
+ "associations": {},
+ "paramInformation": {
+ "_map": {}
+ }
+ },
+ "label": "CT101",
+ "neighbors": [1, 11, 13, 14, 2, 3],
+ "endpointCountIsDynamic": false,
+ "endpointsHaveIdenticalCapabilities": true,
+ "individualEndpointCount": 2,
+ "interviewAttempts": 0,
+ "interviewStage": 6,
+ "endpoints": [
+ {
+ "nodeId": 4,
+ "index": 0
+ },
+ {
+ "nodeId": 4,
+ "index": 1
+ },
+ {
+ "nodeId": 4,
+ "index": 2
+ }
+ ],
+ "values": [
+ {
+ "endpoint": 0,
+ "commandClass": 66,
+ "commandClassName": "Thermostat Operating State",
+ "property": "state",
+ "propertyName": "state",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Operating state",
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Idle",
+ "1": "Heating",
+ "2": "Cooling",
+ "3": "Fan Only",
+ "4": "Pending Heat",
+ "5": "Pending Cool",
+ "6": "Vent/Economizer",
+ "7": "Aux Heating",
+ "8": "2nd Stage Heating",
+ "9": "2nd Stage Cooling",
+ "10": "2nd Stage Aux Heat",
+ "11": "3rd Stage Aux Heat"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 68,
+ "commandClassName": "Thermostat Fan Mode",
+ "property": "mode",
+ "propertyName": "mode",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Thermostat fan mode",
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Auto low",
+ "1": "Low"
+ }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 69,
+ "commandClassName": "Thermostat Fan State",
+ "property": "state",
+ "propertyName": "state",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Thermostat fan state",
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Idle / off",
+ "1": "Running / running low",
+ "2": "Running high",
+ "3": "Running medium",
+ "4": "Circulation mode",
+ "5": "Humidity circulation mode",
+ "6": "Right - left circulation mode",
+ "7": "Up - down circulation mode",
+ "8": "Quiet circulation mode"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 1,
+ "propertyName": "Temperature Reporting Threshold",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "default": 2,
+ "readable": true,
+ "writeable": true,
+ "description": "Reporting threshold for changes in the ambient temperature.",
+ "label": "Temperature Reporting Threshold",
+ "min": 0,
+ "max": 4,
+ "states": {
+ "0": "Disabled",
+ "1": "0.5\u00b0",
+ "2": "1.0\u00b0",
+ "3": "1.5\u00b0",
+ "4": "2.0\u00b0"
+ }
+ },
+ "value": 2
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 7,
+ "propertyName": "Thermostat Swing Temperature",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "default": 2,
+ "readable": true,
+ "writeable": true,
+ "description": "Variance allowed from setpoint",
+ "label": "Thermostat Swing Temperature",
+ "min": 1,
+ "max": 8,
+ "states": {
+ "1": "0.5\u00b0",
+ "2": "1.0\u00b0",
+ "3": "1.5\u00b0",
+ "4": "2.0\u00b0",
+ "5": "2.5\u00b0",
+ "6": "3.0\u00b0",
+ "7": "3.5\u00b0",
+ "8": "4.0\u00b0"
+ }
+ },
+ "value": 2
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 9,
+ "propertyName": "Thermostat Recovery Mode",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "default": 2,
+ "readable": true,
+ "writeable": true,
+ "description": "Fast or Economy recovery mode.",
+ "label": "Thermostat Recovery Mode",
+ "min": 1,
+ "max": 2,
+ "states": {
+ "1": "Fast recovery Mode",
+ "2": "Economy recovery Mode"
+ }
+ },
+ "value": 2
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 3,
+ "propertyName": "Utility Lock Enable/Disable",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "default": 0,
+ "readable": true,
+ "writeable": true,
+ "description": "Prevents setpoint changes at thermostat.",
+ "label": "Utility Lock Enable/Disable",
+ "min": 0,
+ "max": 1,
+ "states": {
+ "0": "Utility lock disabled",
+ "1": "Utility lock enabled"
+ }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Manufacturer ID",
+ "min": 0,
+ "max": 65535
+ },
+ "value": 152
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product type",
+ "min": 0,
+ "max": 65535
+ },
+ "value": 25857
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product ID",
+ "min": 0,
+ "max": 65535
+ },
+ "value": 12
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 128,
+ "commandClassName": "Battery",
+ "property": "level",
+ "propertyName": "level",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Battery level",
+ "min": 0,
+ "max": 100,
+ "unit": "%"
+ },
+ "value": 90
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 128,
+ "commandClassName": "Battery",
+ "property": "isLow",
+ "propertyName": "isLow",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": false,
+ "label": "Low battery level"
+ },
+ "value": false
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ },
+ "value": "3.28"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ },
+ "value": ["9.0"]
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 135,
+ "commandClassName": "Indicator",
+ "property": "value",
+ "propertyName": "value",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Indicator value",
+ "ccSpecific": {
+ "indicatorId": 0
+ },
+ "min": 0,
+ "max": 255
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 32,
+ "commandClassName": "Basic",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Current value",
+ "min": 0,
+ "max": 99
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 32,
+ "commandClassName": "Basic",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Target value",
+ "min": 0,
+ "max": 99
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Manufacturer ID",
+ "min": 0,
+ "max": 65535
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product type",
+ "min": 0,
+ "max": 65535
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product ID",
+ "min": 0,
+ "max": 65535
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "mode",
+ "propertyName": "mode",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Thermostat mode",
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Off",
+ "1": "Heat",
+ "2": "Cool",
+ "3": "Auto",
+ "4": "Auxiliary",
+ "5": "Resume (on)",
+ "6": "Fan",
+ "7": "Furnace",
+ "8": "Dry",
+ "9": "Moist",
+ "10": "Auto changeover",
+ "11": "Energy heat",
+ "12": "Energy cool",
+ "13": "Away",
+ "15": "Full power",
+ "31": "Manufacturer specific"
+ }
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "manufacturerData",
+ "propertyName": "manufacturerData",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 67,
+ "commandClassName": "Thermostat Setpoint",
+ "property": "setpoint",
+ "propertyKey": 1,
+ "propertyName": "setpoint",
+ "propertyKeyName": "Heating",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "ccSpecific": {
+ "setpointType": 1
+ },
+ "unit": "\u00b0F"
+ },
+ "value": 55
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 67,
+ "commandClassName": "Thermostat Setpoint",
+ "property": "setpoint",
+ "propertyKey": 2,
+ "propertyName": "setpoint",
+ "propertyKeyName": "Cooling",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "ccSpecific": {
+ "setpointType": 2
+ },
+ "unit": "\u00b0F"
+ },
+ "value": 78
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 67,
+ "commandClassName": "Thermostat Setpoint",
+ "property": "setpoint",
+ "propertyKey": 0,
+ "propertyName": "setpoint",
+ "propertyKeyName": "N/A",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "ccSpecific": {
+ "setpointType": 0
+ },
+ "unit": "\u00b0C"
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 49,
+ "commandClassName": "Multilevel Sensor",
+ "property": "Air temperature",
+ "propertyName": "Air temperature",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Air temperature",
+ "ccSpecific": {
+ "sensorType": 1,
+ "scale": 1
+ },
+ "unit": "\u00b0F"
+ },
+ "value": 65
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 32,
+ "commandClassName": "Basic",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Current value",
+ "min": 0,
+ "max": 99
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 32,
+ "commandClassName": "Basic",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Target value",
+ "min": 0,
+ "max": 99
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Manufacturer ID",
+ "min": 0,
+ "max": 65535
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product type",
+ "min": 0,
+ "max": 65535
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product ID",
+ "min": 0,
+ "max": 65535
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "mode",
+ "propertyName": "mode",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Thermostat mode",
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Off",
+ "1": "Heat",
+ "2": "Cool",
+ "3": "Auto",
+ "4": "Auxiliary",
+ "5": "Resume (on)",
+ "6": "Fan",
+ "7": "Furnace",
+ "8": "Dry",
+ "9": "Moist",
+ "10": "Auto changeover",
+ "11": "Energy heat",
+ "12": "Energy cool",
+ "13": "Away",
+ "15": "Full power",
+ "31": "Manufacturer specific"
+ }
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "manufacturerData",
+ "propertyName": "manufacturerData",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 49,
+ "commandClassName": "Multilevel Sensor",
+ "property": "Humidity",
+ "propertyName": "Humidity",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Humidity",
+ "ccSpecific": {
+ "sensorType": 5,
+ "scale": 0
+ },
+ "unit": "%"
+ },
+ "value": 56
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 49,
+ "commandClassName": "Multilevel Sensor",
+ "property": "Air temperature",
+ "propertyName": "Air temperature",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Air temperature",
+ "ccSpecific": {
+ "sensorType": 1,
+ "scale": 1
+ },
+ "unit": "\u00b0F"
+ },
+ "value": 62.5
+ }
+ ],
+ "isFrequentListening": true,
+ "maxBaudRate": 40000,
+ "version": 3,
+ "deviceClass": {
+ "basic": {
+ "key": 4,
+ "label": "Routing Slave"
+ },
+ "generic": {
+ "key": 8,
+ "label": "Thermostat"
+ },
+ "specific": {
+ "key": 6,
+ "label": "General Thermostat V2"
+ },
+ "mandatorySupportedCCs": [32, 114, 64, 67, 134],
+ "mandatoryControlledCCs": []
+ },
+ "commandClasses": [
+ {
+ "id": 49,
+ "name": "Multilevel Sensor",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 64,
+ "name": "Thermostat Mode",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 66,
+ "name": "Thermostat Operating State",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 67,
+ "name": "Thermostat Setpoint",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 68,
+ "name": "Thermostat Fan Mode",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 69,
+ "name": "Thermostat Fan State",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 96,
+ "name": "Multi Channel",
+ "version": 3,
+ "isSecure": false
+ },
+ {
+ "id": 112,
+ "name": "Configuration",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 114,
+ "name": "Manufacturer Specific",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 128,
+ "name": "Battery",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 129,
+ "name": "Clock",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 133,
+ "name": "Association",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 134,
+ "name": "Version",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 135,
+ "name": "Indicator",
+ "version": 1,
+ "isSecure": false
+ }
+ ]
+}
diff --git a/tests/fixtures/zwave_js/cover_iblinds_v2_state.json b/tests/fixtures/zwave_js/cover_iblinds_v2_state.json
new file mode 100644
index 00000000000000..35ce70f617aa76
--- /dev/null
+++ b/tests/fixtures/zwave_js/cover_iblinds_v2_state.json
@@ -0,0 +1,357 @@
+{
+ "nodeId": 54,
+ "index": 0,
+ "installerIcon": 6400,
+ "userIcon": 6400,
+ "status": 4,
+ "ready": true,
+ "deviceClass": {
+ "basic": {"key": 4, "label":"Routing Slave"},
+ "generic": {"key": 17, "label":"Routing Slave"},
+ "specific": {"key": 0, "label":"Unused"},
+ "mandatorySupportedCCs": [],
+ "mandatoryControlCCs": []
+ },
+ "isListening": false,
+ "isFrequentListening": true,
+ "isRouting": true,
+ "maxBaudRate": 40000,
+ "isSecure": false,
+ "version": 4,
+ "isBeaming": true,
+ "manufacturerId": 647,
+ "productId": 13,
+ "productType": 3,
+ "firmwareVersion": "1.65",
+ "zwavePlusVersion": 1,
+ "nodeType": 0,
+ "roleType": 7,
+ "deviceConfig": {
+ "manufacturerId": 647,
+ "manufacturer": "HAB Home Intelligence, LLC",
+ "label": "IB2.0",
+ "description": "Window Blind Controller",
+ "devices": [
+ {
+ "productType": "0x0003",
+ "productId": "0x000d"
+ }
+ ],
+ "firmwareVersion": {
+ "min": "0.0",
+ "max": "255.255"
+ },
+ "paramInformation": {
+ "_map": {}
+ }
+ },
+ "label": "IB2.0",
+ "neighbors": [
+ 1,
+ 2,
+ 3,
+ 7,
+ 8,
+ 11,
+ 15,
+ 18,
+ 19,
+ 22,
+ 26,
+ 27,
+ 44,
+ 52
+ ],
+ "interviewAttempts": 1,
+ "interviewStage": 7,
+ "endpoints": [
+ {
+ "nodeId": 54,
+ "index": 0,
+ "installerIcon": 6400,
+ "userIcon": 6400
+ }
+ ],
+ "commandClasses": [],
+ "values": [
+ {
+ "endpoint": 0,
+ "commandClass": 37,
+ "commandClassName": "Binary Switch",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": false,
+ "label": "Current value"
+ },
+ "value": true
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 37,
+ "commandClassName": "Binary Switch",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Target value"
+ },
+ "value": true
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 37,
+ "commandClassName": "Binary Switch",
+ "property": "duration",
+ "propertyName": "duration",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "duration",
+ "readable": true,
+ "writeable": true,
+ "label": "Transition duration"
+ },
+ "value": {
+ "value": 0,
+ "unit": "seconds"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 99,
+ "label": "Target value"
+ },
+ "value": 99
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "duration",
+ "propertyName": "duration",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "duration",
+ "readable": true,
+ "writeable": true,
+ "label": "Transition duration"
+ },
+ "value": {
+ "value": 0,
+ "unit": "seconds"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 99,
+ "label": "Current value"
+ },
+ "value": 30
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "Up",
+ "propertyName": "Up",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Perform a level change (Up)",
+ "ccSpecific": {
+ "switchType": 2
+ }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "Down",
+ "propertyName": "Down",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Perform a level change (Down)",
+ "ccSpecific": {
+ "switchType": 2
+ }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Manufacturer ID"
+ },
+ "value": 647
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product type"
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product ID"
+ },
+ "value": 13
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 128,
+ "commandClassName": "Battery",
+ "property": "level",
+ "propertyName": "level",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 100,
+ "unit": "%",
+ "label": "Battery level"
+ },
+ "value": 100
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 128,
+ "commandClassName": "Battery",
+ "property": "isLow",
+ "propertyName": "isLow",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": false,
+ "label": "Low battery level"
+ },
+ "value": false
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ },
+ "value": "4.33"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ },
+ "value": [
+ "1.65"
+ ]
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "hardwareVersion",
+ "propertyName": "hardwareVersion",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip hardware version"
+ }
+ }
+ ]
+}
diff --git a/tests/fixtures/zwave_js/cover_zw062_state.json b/tests/fixtures/zwave_js/cover_zw062_state.json
new file mode 100644
index 00000000000000..9e7b05adc34bf9
--- /dev/null
+++ b/tests/fixtures/zwave_js/cover_zw062_state.json
@@ -0,0 +1,921 @@
+{
+ "nodeId": 12,
+ "index": 0,
+ "installerIcon": 7680,
+ "userIcon": 7680,
+ "status": 4,
+ "ready": true,
+ "deviceClass": {
+ "basic": {"key": 4, "label":"Routing Slave"},
+ "generic": {"key": 64, "label":"Entry Control"},
+ "specific": {"key": 7, "label":"Secure Barrier Add-on"},
+ "mandatorySupportedCCs": [],
+ "mandatoryControlCCs": []
+ },
+ "isListening": true,
+ "isFrequentListening": false,
+ "isRouting": true,
+ "maxBaudRate": 40000,
+ "isSecure": true,
+ "version": 4,
+ "isBeaming": true,
+ "manufacturerId": 134,
+ "productId": 62,
+ "productType": 259,
+ "firmwareVersion": "1.12",
+ "zwavePlusVersion": 1,
+ "nodeType": 0,
+ "roleType": 5,
+ "deviceConfig": {
+ "manufacturerId": 134,
+ "manufacturer": "AEON Labs",
+ "label": "ZW062",
+ "description": "Aeon Labs Garage Door Controller Gen5",
+ "devices": [
+ {
+ "productType": "0x0003",
+ "productId": "0x003e"
+ },
+ {
+ "productType": "0x0103",
+ "productId": "0x003e"
+ },
+ {
+ "productType": "0x0203",
+ "productId": "0x003e"
+ }
+ ],
+ "firmwareVersion": {
+ "min": "0.0",
+ "max": "255.255"
+ },
+ "associations": {},
+ "paramInformation": {
+ "_map": {}
+ }
+ },
+ "label": "ZW062",
+ "neighbors": [
+ 1,
+ 8,
+ 11,
+ 15,
+ 19,
+ 21,
+ 22,
+ 24,
+ 25,
+ 26,
+ 27,
+ 29
+ ],
+ "interviewAttempts": 1,
+ "endpoints": [
+ {
+ "nodeId": 12,
+ "index": 0,
+ "installerIcon": 7680,
+ "userIcon": 7680
+ }
+ ],
+ "commandClasses": [],
+ "values": [
+ {
+ "endpoint": 0,
+ "commandClass": 37,
+ "commandClassName": "Binary Switch",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": false,
+ "label": "Current value"
+ },
+ "value": false
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 37,
+ "commandClassName": "Binary Switch",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Target value"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 102,
+ "commandClassName": "Barrier Operator",
+ "property": "currentState",
+ "propertyName": "currentState",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Current Barrier State",
+ "states": {
+ "0": "Closed",
+ "252": "Closing",
+ "253": "Stopped",
+ "254": "Opening",
+ "255": "Open"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 102,
+ "commandClassName": "Barrier Operator",
+ "property": "position",
+ "propertyName": "position",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 100,
+ "label": "Barrier Position",
+ "unit": "%"
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 102,
+ "commandClassName": "Barrier Operator",
+ "property": "signalingState",
+ "propertyKey": 1,
+ "propertyName": "signalingState",
+ "propertyKeyName": "1",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 255,
+ "label": "Signaling State (Audible)",
+ "states": {
+ "0": "Off",
+ "255": "On"
+ }
+ },
+ "value": 255
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 102,
+ "commandClassName": "Barrier Operator",
+ "property": "signalingState",
+ "propertyKey": 2,
+ "propertyName": "signalingState",
+ "propertyKeyName": "2",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 255,
+ "label": "Signaling State (Visual)",
+ "states": {
+ "0": "Off",
+ "255": "On"
+ }
+ },
+ "value": 255
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 102,
+ "commandClassName": "Barrier Operator",
+ "property": "targetState",
+ "propertyName": "targetState",
+ "ccVersion": 0,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 255,
+ "label": "Target Barrier State",
+ "states": {
+ "0": "Closed",
+ "255": "Open"
+ }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 32,
+ "propertyName": "Startup ringtone",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 1,
+ "max": 100,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Startup ringtone",
+ "description": "Configure the default startup ringtone",
+ "isFromConfig": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 35,
+ "propertyName": "Calibration timout",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 2,
+ "min": 1,
+ "max": 255,
+ "default": 60,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Calibration timout",
+ "description": "Set the timeout of all calibration steps for the Sensor.",
+ "isFromConfig": true
+ },
+ "value": 13
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 36,
+ "propertyName": "Number of alarm musics",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "valueSize": 1,
+ "min": 1,
+ "max": 100,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Number of alarm musics",
+ "description": "Get the number of alarm musics",
+ "isFromConfig": true
+ },
+ "value": 5
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 39,
+ "propertyName": "Unknown state alarm mode",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 0,
+ "max": 0,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Unknown state alarm mode",
+ "description": "Configuration alarm mode when the garage door is in \"unknown\" state",
+ "isFromConfig": true
+ },
+ "value": 100927488
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 40,
+ "propertyName": "Closed alarm mode",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 0,
+ "max": 0,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Closed alarm mode",
+ "description": "Configure the alarm mode when the garage door is in closed position.",
+ "isFromConfig": true
+ },
+ "value": 33883392
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 41,
+ "propertyName": "Tamper switch configuration",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 255,
+ "default": 0,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Tamper switch configuration",
+ "description": "Configuration report for the tamper switch State",
+ "isFromConfig": true
+ },
+ "value": 15
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 42,
+ "propertyName": "Battery state",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "valueSize": 1,
+ "min": 0,
+ "max": 16,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Battery state",
+ "description": "Configuration report for the battery state of Sensor",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 45,
+ "propertyName": "Temperature",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "valueSize": 2,
+ "min": 0,
+ "max": 500,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Temperature",
+ "description": "Get the environment temperature",
+ "isFromConfig": true
+ },
+ "value": 550
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 47,
+ "propertyName": "Button definition",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Mode 0",
+ "1": "Mode 1"
+ },
+ "label": "Button definition",
+ "description": "Define the function of Button- or Button+.",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 80,
+ "propertyName": "Door state change report type",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 1,
+ "max": 2,
+ "default": 2,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "1": "Send hail CC",
+ "2": "Send barrier operator report CC"
+ },
+ "label": "Door state change report type",
+ "description": "Configure the door state change report type",
+ "isFromConfig": true
+ },
+ "value": 2
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 241,
+ "propertyName": "Pair the Sensor",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 0,
+ "max": 1431655681,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Stop sensor pairing",
+ "1431655681": "Start sensor pairing"
+ },
+ "label": "Pair the Sensor",
+ "description": "Pair the Sensor with Garage Door Controller",
+ "isFromConfig": true
+ },
+ "value": 33554943
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 252,
+ "propertyName": "Lock Configuration",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Configuration enabled",
+ "1": "Configuration disabled (locked)"
+ },
+ "label": "Lock Configuration",
+ "description": "Enable/disable configuration",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 37,
+ "propertyKey": 255,
+ "propertyName": "Disable opening alarm",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 0,
+ "max": 1,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Disable alarm prompt",
+ "1": "Enable alarm prompt"
+ },
+ "label": "Disable opening alarm",
+ "isFromConfig": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 37,
+ "propertyKey": 65280,
+ "propertyName": "Opening alarm volume",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 1,
+ "max": 10,
+ "default": 8,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Opening alarm volume",
+ "isFromConfig": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 37,
+ "propertyKey": 16711680,
+ "propertyName": "Opening alarm choice",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 1,
+ "max": 4,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Opening alarm choice",
+ "description": "Alarm mode when the garage door is opening",
+ "isFromConfig": true
+ },
+ "value": 2
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 37,
+ "propertyKey": 251658240,
+ "propertyName": "Opening alarm LED mode",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 1,
+ "max": 10,
+ "default": 10,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Opening alarm LED mode",
+ "isFromConfig": true
+ },
+ "value": 5
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 38,
+ "propertyKey": 255,
+ "propertyName": "Disable closing alarm",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 0,
+ "max": 1,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Disable alarm prompt",
+ "1": "Enable alarm prompt"
+ },
+ "label": "Disable closing alarm",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 38,
+ "propertyKey": 65280,
+ "propertyName": "Closing alarm volume",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 1,
+ "max": 10,
+ "default": 8,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Closing alarm volume",
+ "isFromConfig": true
+ },
+ "value": 8
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 38,
+ "propertyKey": 16711680,
+ "propertyName": "Closing alarm choice",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 1,
+ "max": 4,
+ "default": 2,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Closing alarm choice",
+ "description": "Alarm mode when the garage door is closing",
+ "isFromConfig": true
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 38,
+ "propertyKey": 251658240,
+ "propertyName": "Closing alarm LED mode",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 1,
+ "max": 10,
+ "default": 6,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Closing alarm LED mode",
+ "isFromConfig": true
+ },
+ "value": 8
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 34,
+ "propertyName": "Sensor Calibration",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": false,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Calibration not active",
+ "1": "Begin calibration"
+ },
+ "label": "Sensor Calibration",
+ "description": "Perform Sensor Calibration",
+ "isFromConfig": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 43,
+ "propertyName": "Play or Pause ringtone",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": false,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 1,
+ "max": 255,
+ "default": 1,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Play or Pause ringtone",
+ "description": "Start playing or Stop playing the ringtone",
+ "isFromConfig": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 44,
+ "propertyName": "Ringtone test volume",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": false,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 1,
+ "max": 10,
+ "default": 1,
+ "format": 0,
+ "allowManualEntry": true,
+ "label": "Ringtone test volume",
+ "description": "Set volume for test of ringtone",
+ "isFromConfig": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 113,
+ "commandClassName": "Notification",
+ "property": "alarmType",
+ "propertyName": "alarmType",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Alarm Type"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 113,
+ "commandClassName": "Notification",
+ "property": "alarmLevel",
+ "propertyName": "alarmLevel",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Alarm Level"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Manufacturer ID"
+ },
+ "value": 134
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product type"
+ },
+ "value": 259
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product ID"
+ },
+ "value": 62
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ },
+ "value": "3.99"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ },
+ "value": [
+ "1.12"
+ ]
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "hardwareVersion",
+ "propertyName": "hardwareVersion",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip hardware version"
+ }
+ }
+ ]
+}
diff --git a/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json b/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json
index 38cbb63b1c6c53..b11d2bfd180898 100644
--- a/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json
+++ b/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json
@@ -6,14 +6,10 @@
"status": 4,
"ready": true,
"deviceClass": {
- "basic": "Routing Slave",
- "generic": "Multilevel Switch",
- "specific": "Multilevel Power Switch",
- "mandatorySupportedCCs": [
- "Basic",
- "Multilevel Switch",
- "All Switch"
- ],
+ "basic": {"key": 4, "label":"Routing Slave"},
+ "generic": {"key": 17, "label":"Routing Slave"},
+ "specific": {"key": 1, "label":"Multilevel Power Switch"},
+ "mandatorySupportedCCs": [],
"mandatoryControlCCs": []
},
"isListening": true,
@@ -31,7 +27,7 @@
"nodeType": 0,
"roleType": 5,
"name": "AllLoadDimmer",
- "location": "",
+ "location": "LivingRoom",
"deviceConfig": {
"manufacturerId": 26,
"manufacturer": "Eaton",
@@ -74,6 +70,7 @@
"userIcon": 1536
}
],
+ "commandClasses": [],
"values": [
{
"commandClassName": "Multilevel Switch",
@@ -124,7 +121,7 @@
"max": 99,
"label": "Current value"
},
- "value": 0,
+ "value": 22,
"ccVersion": 4
},
{
@@ -779,4 +776,4 @@
"ccVersion": 3
}
]
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/zwave_js/ecolink_door_sensor_state.json b/tests/fixtures/zwave_js/ecolink_door_sensor_state.json
index bd5f2c6b466371..9c2befdf5e8dfb 100644
--- a/tests/fixtures/zwave_js/ecolink_door_sensor_state.json
+++ b/tests/fixtures/zwave_js/ecolink_door_sensor_state.json
@@ -4,16 +4,11 @@
"status": 1,
"ready": true,
"deviceClass": {
- "basic": "Static Controller",
- "generic": "Binary Sensor",
- "specific": "Routing Binary Sensor",
- "mandatorySupportedCCs": [
- "Basic",
- "Binary Sensor"
- ],
- "mandatoryControlCCs": [
-
- ]
+ "basic": {"key": 2, "label":"Static Controller"},
+ "generic": {"key": 32, "label":"Binary Sensor"},
+ "specific": {"key": 1, "label":"Routing Binary Sensor"},
+ "mandatorySupportedCCs": [],
+ "mandatoryControlCCs": []
},
"isListening": false,
"isFrequentListening": false,
@@ -61,6 +56,7 @@
"index": 0
}
],
+ "commandClasses": [],
"values": [
{
"commandClassName": "Basic",
diff --git a/tests/fixtures/zwave_js/fan_ge_12730_state.json b/tests/fixtures/zwave_js/fan_ge_12730_state.json
new file mode 100644
index 00000000000000..b6cf59b4226050
--- /dev/null
+++ b/tests/fixtures/zwave_js/fan_ge_12730_state.json
@@ -0,0 +1,431 @@
+{
+ "nodeId": 24,
+ "index": 0,
+ "status": 4,
+ "ready": true,
+ "deviceClass": {
+ "basic": {"key": 4, "label":"Routing Slave"},
+ "generic": {"key": 17, "label":"Multilevel Switch"},
+ "specific": {"key": 1, "label":"Multilevel Power Switch"},
+ "mandatorySupportedCCs": [],
+ "mandatoryControlCCs": []
+ },
+ "isListening": true,
+ "isFrequentListening": false,
+ "isRouting": true,
+ "maxBaudRate": 40000,
+ "isSecure": false,
+ "version": 4,
+ "isBeaming": true,
+ "manufacturerId": 99,
+ "productId": 12340,
+ "productType": 18756,
+ "firmwareVersion": "3.10",
+ "deviceConfig": {
+ "manufacturerId": 99,
+ "manufacturer": "GE/Jasco",
+ "label": "12730 / ZW4002",
+ "description": "In-Wall Smart Fan Control",
+ "devices": [
+ {
+ "productType": "0x4944",
+ "productId": "0x3034"
+ }
+ ],
+ "firmwareVersion": {
+ "min": "0.0",
+ "max": "255.255"
+ },
+ "paramInformation": {
+ "_map": {}
+ }
+ },
+ "label": "12730 / ZW4002",
+ "neighbors": [
+ 1,
+ 12
+ ],
+ "interviewAttempts": 1,
+ "interviewStage": 7,
+ "endpoints": [
+ {
+ "nodeId": 24,
+ "index": 0
+ }
+ ],
+ "commandClasses": [],
+ "values": [
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 99,
+ "label": "Target value"
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "duration",
+ "propertyName": "duration",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "duration",
+ "readable": true,
+ "writeable": true,
+ "label": "Transition duration"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 99,
+ "label": "Current value"
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "Up",
+ "propertyName": "Up",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Perform a level change (Up)",
+ "ccSpecific": {
+ "switchType": 2
+ }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "Down",
+ "propertyName": "Down",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Perform a level change (Down)",
+ "ccSpecific": {
+ "switchType": 2
+ }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 3,
+ "propertyName": "LED Light",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 2,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "LED on when light off",
+ "1": "LED on when light on",
+ "2": "LED always off"
+ },
+ "label": "LED Light",
+ "description": "Sets when the LED on the switch is lit.",
+ "isFromConfig": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 4,
+ "propertyName": "Invert Switch",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 0,
+ "format": 0,
+ "allowManualEntry": false,
+ "states": {
+ "0": "No",
+ "1": "Yes"
+ },
+ "label": "Invert Switch",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 7,
+ "propertyName": "Dim Rate Steps (Z-Wave Command)",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 99,
+ "default": 1,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Dim Rate Steps (Z-Wave Command)",
+ "description": "Number of steps or levels",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 8,
+ "propertyName": "Dim Rate Timing (Z-Wave)",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 2,
+ "min": 1,
+ "max": 255,
+ "default": 3,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Dim Rate Timing (Z-Wave)",
+ "description": "Timing of steps or levels",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 9,
+ "propertyName": "Dim Rate Steps (Manual)",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 1,
+ "max": 99,
+ "default": 1,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Dim Rate Steps (Manual)",
+ "description": "Number of steps or levels",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 10,
+ "propertyName": "Dim Rate Timing (Manual)",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 2,
+ "min": 1,
+ "max": 255,
+ "default": 3,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Dim Rate Timing (Manual)",
+ "description": "Timing of steps",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 11,
+ "propertyName": "Dim Rate Steps (All-On/All-Off)",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 1,
+ "max": 99,
+ "default": 1,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Dim Rate Steps (All-On/All-Off)",
+ "description": "Number of steps or levels",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 12,
+ "propertyName": "Dim Rate Timing (All-On/All-Off)",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 2,
+ "min": 1,
+ "max": 255,
+ "default": 3,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Dim Rate Timing (All-On/All-Off)",
+ "description": "Timing of steps or levels",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Manufacturer ID"
+ },
+ "value": 99
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product type"
+ },
+ "value": 18756
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product ID"
+ },
+ "value": 12340
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ },
+ "value": 6
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ },
+ "value": "3.67"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ },
+ "value": [
+ "3.10"
+ ]
+ }
+ ]
+}
diff --git a/tests/fixtures/zwave_js/hank_binary_switch_state.json b/tests/fixtures/zwave_js/hank_binary_switch_state.json
index 0c629b3cf9973d..e5f739d63a5f16 100644
--- a/tests/fixtures/zwave_js/hank_binary_switch_state.json
+++ b/tests/fixtures/zwave_js/hank_binary_switch_state.json
@@ -6,14 +6,10 @@
"status": 4,
"ready": true,
"deviceClass": {
- "basic": "Static Controller",
- "generic": "Binary Switch",
- "specific": "Binary Power Switch",
- "mandatorySupportedCCs": [
- "Basic",
- "Binary Switch",
- "All Switch"
- ],
+ "basic": {"key": 2, "label":"Static Controller"},
+ "generic": {"key": 16, "label":"Binary Switch"},
+ "specific": {"key": 1, "label":"Binary Power Switch"},
+ "mandatorySupportedCCs": [],
"mandatoryControlCCs": []
},
"isListening": true,
@@ -67,6 +63,7 @@
"userIcon": 1792
}
],
+ "commandClasses": [],
"values": [
{
"commandClassName": "Binary Switch",
diff --git a/tests/fixtures/zwave_js/in_wall_smart_fan_control_state.json b/tests/fixtures/zwave_js/in_wall_smart_fan_control_state.json
index fe5550a54242cb..74467664955af5 100644
--- a/tests/fixtures/zwave_js/in_wall_smart_fan_control_state.json
+++ b/tests/fixtures/zwave_js/in_wall_smart_fan_control_state.json
@@ -6,13 +6,10 @@
"status": 4,
"ready": true,
"deviceClass": {
- "basic": "Routing Slave",
- "generic": "Multilevel Switch",
- "specific": "Fan Switch",
- "mandatorySupportedCCs": [
- "Basic",
- "Multilevel Switch"
- ],
+ "basic": {"key": 4, "label":"Routing Slave"},
+ "generic": {"key": 17, "label":"Multilevel Switch"},
+ "specific": {"key": 8, "label":"Fan Switch"},
+ "mandatorySupportedCCs": [],
"mandatoryControlCCs": []
},
"isListening": true,
@@ -87,6 +84,7 @@
"userIcon": 1024
}
],
+ "commandClasses": [],
"values": [
{
"commandClassName": "Multilevel Switch",
diff --git a/tests/fixtures/zwave_js/inovelli_lzw36_state.json b/tests/fixtures/zwave_js/inovelli_lzw36_state.json
new file mode 100644
index 00000000000000..bfa56891413373
--- /dev/null
+++ b/tests/fixtures/zwave_js/inovelli_lzw36_state.json
@@ -0,0 +1,1956 @@
+{
+ "nodeId": 19,
+ "index": 0,
+ "installerIcon": 7168,
+ "userIcon": 7168,
+ "status": 4,
+ "ready": true,
+ "deviceClass": {
+ "basic": {
+ "key": 4,
+ "label": "Routing Slave"
+ },
+ "generic": {
+ "key": 17,
+ "label": "Multilevel Switch"
+ },
+ "specific": {
+ "key": 0,
+ "label": "Unused"
+ },
+ "mandatorySupportedCCs": [
+ 32,
+ 38
+ ],
+ "mandatoryControlledCCs": []
+ },
+ "isListening": true,
+ "isFrequentListening": false,
+ "isRouting": true,
+ "maxBaudRate": 40000,
+ "isSecure": false,
+ "version": 4,
+ "isBeaming": true,
+ "manufacturerId": 798,
+ "productId": 1,
+ "productType": 14,
+ "firmwareVersion": "1.34",
+ "zwavePlusVersion": 2,
+ "nodeType": 0,
+ "roleType": 5,
+ "name": "family_room_combo",
+ "deviceConfig": {
+ "filename": "/opt/node_modules/@zwave-js/config/config/devices/0x031e/lzw36.json",
+ "manufacturerId": 798,
+ "manufacturer": "Inovelli",
+ "label": "LZW36",
+ "description": "Fan/Light Dimmer",
+ "devices": [
+ {
+ "productType": "0x000e",
+ "productId": "0x0001"
+ }
+ ],
+ "firmwareVersion": {
+ "min": "0.0",
+ "max": "255.255"
+ },
+ "associations": {},
+ "paramInformation": {
+ "_map": {}
+ }
+ },
+ "label": "LZW36",
+ "neighbors": [
+ 1,
+ 13,
+ 14,
+ 15,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30
+ ],
+ "endpointCountIsDynamic": false,
+ "endpointsHaveIdenticalCapabilities": true,
+ "individualEndpointCount": 2,
+ "aggregatedEndpointCount": 0,
+ "interviewAttempts": 1,
+ "interviewStage": 7,
+ "commandClasses": [
+ {
+ "id": 38,
+ "name": "Multilevel Switch",
+ "version": 4,
+ "isSecure": false
+ },
+ {
+ "id": 94,
+ "name": "Z-Wave Plus Info",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 152,
+ "name": "Security",
+ "version": 1,
+ "isSecure": true
+ },
+ {
+ "id": 108,
+ "name": "Supervision",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 112,
+ "name": "Configuration",
+ "version": 4,
+ "isSecure": false
+ },
+ {
+ "id": 133,
+ "name": "Association",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 89,
+ "name": "Association Group Information",
+ "version": 3,
+ "isSecure": false
+ },
+ {
+ "id": 142,
+ "name": "Multi Channel Association",
+ "version": 3,
+ "isSecure": false
+ },
+ {
+ "id": 134,
+ "name": "Version",
+ "version": 3,
+ "isSecure": false
+ },
+ {
+ "id": 114,
+ "name": "Manufacturer Specific",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 90,
+ "name": "Device Reset Locally",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 117,
+ "name": "Protection",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 122,
+ "name": "Firmware Update Meta Data",
+ "version": 5,
+ "isSecure": false
+ },
+ {
+ "id": 91,
+ "name": "Central Scene",
+ "version": 3,
+ "isSecure": false
+ },
+ {
+ "id": 135,
+ "name": "Indicator",
+ "version": 3,
+ "isSecure": false
+ },
+ {
+ "id": 96,
+ "name": "Multi Channel",
+ "version": 4,
+ "isSecure": false
+ },
+ {
+ "id": 50,
+ "name": "Meter",
+ "version": 3,
+ "isSecure": false
+ }
+ ],
+ "endpoints": [
+ {
+ "nodeId": 19,
+ "index": 0,
+ "installerIcon": 7168,
+ "userIcon": 7168
+ },
+ {
+ "nodeId": 19,
+ "index": 1,
+ "installerIcon": 1536,
+ "userIcon": 1536
+ },
+ {
+ "nodeId": 19,
+ "index": 2,
+ "installerIcon": 1536,
+ "userIcon": 1536
+ }
+ ],
+ "values": [
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 1,
+ "propertyName": "Light Dimming Speed",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 98,
+ "default": 4,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Light Dimming Speed",
+ "description": "This changes the speed in which the attached light dims up or down. A setting of 0 should turn the light immediately on or off (almost like an on/off switch). Increasing the value should slow down the transition speed. Range:0-98 Default: 4",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 2,
+ "propertyName": "Light Dimming Speed (From Switch)",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 99,
+ "default": 99,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Light Dimming Speed (From Switch)",
+ "description": "This changes the speed in which the attached light dims up or down when controlled from the physical switch. A setting of 0 should turn the light immediately on or off (almost like an on/off switch). Increasing the value should slow down the transition speed. A setting of 99 should keep this in sync with parameter 1. Range:0-99 Default: 99",
+ "isFromConfig": true
+ },
+ "value": 99
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 3,
+ "propertyName": "Light Ramp Rate",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 99,
+ "default": 99,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Light Ramp Rate",
+ "description": "This changes the speed in which the attached light turns on or off. For example, when a user sends the switch a basicSet(value: 0xFF) or basicSet(value: 0x00), this is the speed in which those actions take place. A setting of 0 should turn the light immediately on or off (almost like an on/off switch). Increasing the value should slow down the transition speed. A setting of 99 should keep this in sync with parameter 1. Range:0-99 Default: 99",
+ "isFromConfig": true
+ },
+ "value": 99
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 4,
+ "propertyName": "Light Ramp Rate (From Switch)",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 99,
+ "default": 99,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Light Ramp Rate (From Switch)",
+ "description": "This changes the speed in which the attached light turns on or off from the physical switch. For example, when a user presses the up or down button, this is the speed in which those actions take place. A setting of 0 should turn the light immediately on or off (almost like an on/off switch). Increasing the value should slow down the transition speed. A setting of 99 should keep this in sync with parameter 1. Range:0-99 Default: 99",
+ "isFromConfig": true
+ },
+ "value": 99
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 5,
+ "propertyName": "Minimum Light Level",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 1,
+ "max": 45,
+ "default": 1,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Minimum Light Level",
+ "description": "The minimum level that the dimmer allows the bulb to be dimmed to. Useful when the user has an LED bulb that does not turn on or flickers at a lower level. Range:1-45 Default: 1",
+ "isFromConfig": true
+ },
+ "value": 40
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 6,
+ "propertyName": "Maximum Light Level",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 55,
+ "max": 99,
+ "default": 99,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Maximum Light Level",
+ "description": "The maximum level that the dimmer allows the bulb to be dimmed to. Useful when the user has an LED bulb that reaches its maximum level before the dimmer value of 99. Range:55-99 Default: 99",
+ "isFromConfig": true
+ },
+ "value": 99
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 7,
+ "propertyName": "Minimum Fan Level",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 1,
+ "max": 45,
+ "default": 1,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Minimum Fan Level",
+ "description": "The minimum level that the dimmer allows the fan to be dimmed to. Useful when the user has a fan that does not turn at a lower level. Range:1-45 Default: 1",
+ "isFromConfig": true
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 8,
+ "propertyName": "Maximum Fan Level",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 55,
+ "max": 99,
+ "default": 99,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Maximum Fan Level",
+ "description": "The maximum level that the dimmer allows the fan to be dimmed to. Range:55-99 Default: 99",
+ "isFromConfig": true
+ },
+ "value": 99
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 10,
+ "propertyName": "Auto Off Light Timer",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 2,
+ "min": 0,
+ "max": 32767,
+ "default": 0,
+ "unit": "s",
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Auto Off Light Timer",
+ "description": "Automatically turns the light switch off after this many seconds. When the switch is turned on a timer is started that is the duration of this setting. When the timer expires, the switch is turned off. Range:0-32767 Default: 0",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 11,
+ "propertyName": "Auto Off Fan Timer",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 2,
+ "min": 0,
+ "max": 32767,
+ "default": 0,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Auto Off Fan Timer",
+ "description": "Automatically turns the fan switch off after this many seconds. When the switch is turned on a timer is started that is the duration of this setting. When the timer expires, the switch is turned off. Range:0-32767 Default: 0",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 12,
+ "propertyName": "Default Light Level (Local)",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 1,
+ "max": 99,
+ "default": 0,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Default Light Level (Local)",
+ "description": "Default level for the dimmer when it is powered on from the local switch. A setting of 0 means that the switch will return to the level that it was on before it was turned off. Range:1-99 Default: 0",
+ "isFromConfig": true
+ },
+ "value": 70
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 13,
+ "propertyName": "Default Light Level (Z-Wave)",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 1,
+ "max": 99,
+ "default": 0,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Default Light Level (Z-Wave)",
+ "description": "Default level for the dimmer when it is powered on from a Z-Wave command. A setting of 0 means that the switch will return to the level that it was on before it was turned off. Range:1-99 Default: 0",
+ "isFromConfig": true
+ },
+ "value": 85
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 14,
+ "propertyName": "Default Fan Level (Local)",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 99,
+ "default": 0,
+ "format": 1,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Last State",
+ "33": "Low",
+ "66": "Medium",
+ "99": "High"
+ },
+ "label": "Default Fan Level (Local)",
+ "description": "Default level for the fan dimmer when it is powered on from the local switch. Default: Last State",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 15,
+ "propertyName": "Default Fan Level (Z-Wave)",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 99,
+ "default": 0,
+ "format": 1,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Last State",
+ "33": "Low",
+ "66": "Medium",
+ "99": "High"
+ },
+ "label": "Default Fan Level (Z-Wave)",
+ "description": "Default level for the fan dimmer when it is powered on from the local switch. Default: Last State",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 16,
+ "propertyName": "Light State After Power Restored",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 100,
+ "default": 0,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Light State After Power Restored",
+ "description": "The state the switch should return to once power is restored after power failure. 0 = off, 1-99 = level, 100=previous. Range:0-100 Default: 100",
+ "isFromConfig": true
+ },
+ "value": 100
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 17,
+ "propertyName": "Fan State After Power Restored",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 100,
+ "default": 0,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Fan State After Power Restored",
+ "description": "The state the switch should return to once power is restored after power failure. Default: Off",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 18,
+ "propertyName": "Light LED Indicator Color",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 2,
+ "min": 0,
+ "max": 255,
+ "default": 170,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Light LED Indicator Color",
+ "description": "This is the color of the Light LED strip represented as part of the HUE color wheel. Since the wheel has 360 values and this parameter only has 255, the following equation can be used to determine the color: value/255 * 360 = Hue color wheel value Range: 0 to 255 Default: 170",
+ "isFromConfig": true
+ },
+ "value": 170
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 19,
+ "propertyName": "Light LED Strip Intensity",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 10,
+ "default": 5,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Light LED Strip Intensity",
+ "description": "This is the intensity of the Light LED strip. Range: 0-10 Default: 5",
+ "isFromConfig": true
+ },
+ "value": 10
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 20,
+ "propertyName": "Fan LED Indicator Color",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 2,
+ "min": 0,
+ "max": 255,
+ "default": 170,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Fan LED Indicator Color",
+ "description": "This is the color of the Fan LED strip represented as part of the HUE color wheel. Since the wheel has 360 values and this parameter only has 255, the following equation can be used to determine the color: value/255 * 360 = Hue color wheel value Range: 0 to 255 Default: 170",
+ "isFromConfig": true
+ },
+ "value": 170
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 21,
+ "propertyName": "Fan LED Strip Intensity",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 10,
+ "default": 5,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Fan LED Strip Intensity",
+ "description": "This is the intensity of the Fan LED strip. Range: 0-10 Default: 5",
+ "isFromConfig": true
+ },
+ "value": 10
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 22,
+ "propertyName": "Light LED Strip Intensity (When OFF)",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 10,
+ "default": 1,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Light LED Strip Intensity (When OFF)",
+ "description": "This is the intensity of the Light LED strip when the switch is off. This is useful for users to see the light switch location when the lights are off. Range: 0-10 Default: 1",
+ "isFromConfig": true
+ },
+ "value": 4
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 23,
+ "propertyName": "Fan LED Strip Intensity (When OFF)",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 10,
+ "default": 1,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Fan LED Strip Intensity (When OFF)",
+ "description": "This is the intensity of the Fan LED strip when the switch is off. This is useful for users to see the light switch location when the lights are off. Range: 0-10 Default: 1",
+ "isFromConfig": true
+ },
+ "value": 4
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 26,
+ "propertyName": "Light LED Strip Timeout",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 10,
+ "default": 3,
+ "unit": "s",
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Light LED Strip Timeout",
+ "description": "When the LED strip is disabled (Light LED Strip Intensity is set to 0), this setting allows the LED strip to turn on temporarily while being adjusted. Range: 0-10 Default: 3 Disabled: 0",
+ "isFromConfig": true
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 27,
+ "propertyName": "Fan LED Strip Timeout",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 10,
+ "default": 3,
+ "unit": "s",
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Fan LED Strip Timeout",
+ "description": "When the LED strip is disabled (Fan LED Strip Intensity is set to 0), this setting allows the LED strip to turn on temporarily while being adjusted. Range: 0-10 Default: 3 Disabled: 0",
+ "isFromConfig": true
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 28,
+ "propertyName": "Active Power Reports",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 100,
+ "default": 10,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Active Power Reports",
+ "description": "The power level change that will result in a new power report being sent. The value is a percentage of the previous report. 0 = disabled. Range:0-100 Default: 10",
+ "isFromConfig": true
+ },
+ "value": 10
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 29,
+ "propertyName": "Periodic Power & Energy Reports",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 2,
+ "min": 0,
+ "max": 32767,
+ "default": 3600,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Periodic Power & Energy Reports",
+ "description": "Time period between consecutive power & energy reports being sent (in seconds). The timer is reset after each report is sent. Range:0-32767 Default: 3600",
+ "isFromConfig": true
+ },
+ "value": 3600
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 30,
+ "propertyName": "Energy Reports",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 100,
+ "default": 10,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Energy Reports",
+ "description": "The energy level change that will result in a new energy report being sent. The value is a percentage of the previous report. Range:0-100 Default: 10",
+ "isFromConfig": true
+ },
+ "value": 10
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 31,
+ "propertyName": "Local Protection",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 3,
+ "default": 0,
+ "format": 1,
+ "allowManualEntry": false,
+ "states": {
+ "0": "None",
+ "1": "light",
+ "2": "fan",
+ "3": "Both"
+ },
+ "label": "Local Protection",
+ "description": "Enable local protection on these buttons. 0 = none, 1 = light, 2 = fan, 3 = both.",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 24,
+ "propertyKey": 2130706432,
+ "propertyName": "Light LED Strip Effect",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 0,
+ "max": 5,
+ "default": 0,
+ "format": 1,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Off",
+ "1": "Solid",
+ "2": "Slow Blink",
+ "3": "Fast Blink",
+ "4": "Chase",
+ "5": "Pulse"
+ },
+ "label": "Light LED Strip Effect",
+ "description": "Light LED Strip Effect. 0 = Off, 1 = Solid, 2 = Slow Blink, 3 = Fast Blink, 4 = Chase, 5 = Pulse Default: 0",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 24,
+ "propertyKey": 16711680,
+ "propertyName": "Light LED Strip Effect Duration",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 0,
+ "max": 255,
+ "default": 0,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Light LED Strip Effect Duration",
+ "description": "Light LED Strip Effect Duration. 1 to 60 = seconds, 61 to 120 minutes, 121 - 254 = hours, 255 = Indefinitely Default: 0",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 24,
+ "propertyKey": 65280,
+ "propertyName": "Light LED Strip Effect Intensity",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 0,
+ "max": 9,
+ "default": 3,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Light LED Strip Effect Intensity",
+ "description": "Light LED Strip Effect Intensity. 0 to 9. 0 = dim, 9 = bright Default: 3",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 24,
+ "propertyKey": 255,
+ "propertyName": "Light LED Strip Effect Color",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 0,
+ "max": 255,
+ "default": 0,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Light LED Strip Effect Color",
+ "description": "Light LED Strip Effect Color. Color - 0 - 255. Hue color wheel. value/255 * 360 = Hue color wheel value Range: 0-255 Default: 0",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 25,
+ "propertyKey": 2130706432,
+ "propertyName": "Fan LED Strip Effect",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 0,
+ "max": 5,
+ "default": 0,
+ "format": 1,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Off",
+ "1": "Solid",
+ "2": "Slow Blink",
+ "3": "Fast Blink",
+ "4": "Chase",
+ "5": "Pulse"
+ },
+ "label": "Fan LED Strip Effect",
+ "description": "Fan LED Strip Effect. 0 = Off, 1 = Solid, 2 = Slow Blink, 3 = Fast Blink, 4 = Chase, 5 = Pulse Default: 0",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 25,
+ "propertyKey": 16711680,
+ "propertyName": "Fan LED Strip Effect Duration",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 0,
+ "max": 255,
+ "default": 0,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Fan LED Strip Effect Duration",
+ "description": "Fan LED Strip Duration. 1 to 60 = seconds, 61 to 120 minutes, 121 - 254 = hours, 255 = Indefinitely Default: 0",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 25,
+ "propertyKey": 65280,
+ "propertyName": "Fan LED Strip Effect Intensity",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 0,
+ "max": 9,
+ "default": 3,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Fan LED Strip Effect Intensity",
+ "description": "Fan LED Strip Intensity 0 to 9. 0 = dim, 9 = bright Default: 3",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 25,
+ "propertyKey": 255,
+ "propertyName": "Fan LED Strip Effect Color",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 4,
+ "min": 0,
+ "max": 255,
+ "default": 0,
+ "format": 1,
+ "allowManualEntry": true,
+ "label": "Fan LED Strip Effect Color",
+ "description": "Fan LED Color 0 - 255. Hue color wheel. value/255 * 360 = Hue color wheel value Range: 0-255 Default: 0",
+ "isFromConfig": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 51,
+ "propertyName": "Instant On",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "valueSize": 1,
+ "min": 0,
+ "max": 1,
+ "default": 1,
+ "format": 1,
+ "allowManualEntry": false,
+ "states": {
+ "0": "Enabled (no delay)",
+ "1": "Disabled (700ms delay)"
+ },
+ "label": "Instant On",
+ "description": "Enables instant on (ie: disables the 700ms button delay). Note, if you disable the delay, it will also disable scene control except for the following: Light on/off pressed, held, released, Fan on/off pressed, held, released & light up/down fan up/down pressed (firmware 1.36+).",
+ "isFromConfig": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ },
+ "value": "7.13"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ },
+ "value": [
+ "1.34"
+ ]
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "hardwareVersion",
+ "propertyName": "hardwareVersion",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip hardware version"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "sdkVersion",
+ "propertyName": "sdkVersion",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": "7.13.4"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "applicationFrameworkAPIVersion",
+ "propertyName": "applicationFrameworkAPIVersion",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": "10.13.4"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "applicationFrameworkBuildNumber",
+ "propertyName": "applicationFrameworkBuildNumber",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": 310
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "hostInterfaceVersion",
+ "propertyName": "hostInterfaceVersion",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": "unused"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "hostInterfaceBuildNumber",
+ "propertyName": "hostInterfaceBuildNumber",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "zWaveProtocolVersion",
+ "propertyName": "zWaveProtocolVersion",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": "7.13.4"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "zWaveProtocolBuildNumber",
+ "propertyName": "zWaveProtocolBuildNumber",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": 310
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "applicationVersion",
+ "propertyName": "applicationVersion",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": "1.34.1"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "applicationBuildNumber",
+ "propertyName": "applicationBuildNumber",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ },
+ "value": 43707
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Manufacturer ID"
+ },
+ "value": 798
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product type"
+ },
+ "value": 14
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product ID"
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 117,
+ "commandClassName": "Protection",
+ "property": "local",
+ "propertyName": "local",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Local protection state",
+ "states": {
+ "0": "Unprotected",
+ "1": "ProtectedBySequence",
+ "2": "NoOperationPossible"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 117,
+ "commandClassName": "Protection",
+ "property": "rf",
+ "propertyName": "rf",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "RF protection state",
+ "states": {
+ "0": "Unprotected",
+ "1": "NoControl",
+ "2": "NoResponse"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 117,
+ "commandClassName": "Protection",
+ "property": "exclusiveControlNodeId",
+ "propertyName": "exclusiveControlNodeId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 117,
+ "commandClassName": "Protection",
+ "property": "timeout",
+ "propertyName": "timeout",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 91,
+ "commandClassName": "Central Scene",
+ "property": "slowRefresh",
+ "propertyName": "slowRefresh",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Send held down notifications at a slow rate",
+ "description": "When this is true, KeyHeldDown notifications are sent every 55s. When this is false, the notifications are sent every 200ms."
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 91,
+ "commandClassName": "Central Scene",
+ "property": "scene",
+ "propertyKey": "001",
+ "propertyName": "scene",
+ "propertyKeyName": "001",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Scene 001",
+ "states": {
+ "0": "KeyPressed",
+ "1": "KeyReleased",
+ "2": "KeyHeldDown",
+ "3": "KeyPressed2x",
+ "4": "KeyPressed3x",
+ "5": "KeyPressed4x",
+ "6": "KeyPressed5x"
+ }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 91,
+ "commandClassName": "Central Scene",
+ "property": "scene",
+ "propertyKey": "002",
+ "propertyName": "scene",
+ "propertyKeyName": "002",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Scene 002",
+ "states": {
+ "0": "KeyPressed",
+ "1": "KeyReleased",
+ "2": "KeyHeldDown",
+ "3": "KeyPressed2x",
+ "4": "KeyPressed3x",
+ "5": "KeyPressed4x",
+ "6": "KeyPressed5x"
+ }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 91,
+ "commandClassName": "Central Scene",
+ "property": "scene",
+ "propertyKey": "003",
+ "propertyName": "scene",
+ "propertyKeyName": "003",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Scene 003",
+ "states": {
+ "0": "KeyPressed"
+ }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 91,
+ "commandClassName": "Central Scene",
+ "property": "scene",
+ "propertyKey": "004",
+ "propertyName": "scene",
+ "propertyKeyName": "004",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Scene 004",
+ "states": {
+ "0": "KeyPressed"
+ }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 91,
+ "commandClassName": "Central Scene",
+ "property": "scene",
+ "propertyKey": "005",
+ "propertyName": "scene",
+ "propertyKeyName": "005",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Scene 005",
+ "states": {
+ "0": "KeyPressed"
+ }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 91,
+ "commandClassName": "Central Scene",
+ "property": "scene",
+ "propertyKey": "006",
+ "propertyName": "scene",
+ "propertyKeyName": "006",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 255,
+ "label": "Scene 006",
+ "states": {
+ "0": "KeyPressed"
+ }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 135,
+ "commandClassName": "Indicator",
+ "property": 80,
+ "propertyKey": 3,
+ "propertyName": "Node Identify",
+ "propertyKeyName": "On/Off Period: Duration",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Node Identify - On/Off Period: Duration",
+ "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"",
+ "ccSpecific": {
+ "indicatorId": 80,
+ "propertyId": 3
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 135,
+ "commandClassName": "Indicator",
+ "property": 80,
+ "propertyKey": 4,
+ "propertyName": "Node Identify",
+ "propertyKeyName": "On/Off Cycle Count",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Node Identify - On/Off Cycle Count",
+ "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"",
+ "ccSpecific": {
+ "indicatorId": 80,
+ "propertyId": 4
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 135,
+ "commandClassName": "Indicator",
+ "property": 80,
+ "propertyKey": 5,
+ "propertyName": "Node Identify",
+ "propertyKeyName": "On/Off Period: On time",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Node Identify - On/Off Period: On time",
+ "description": "This property is used to set the length of the On time during an On/Off period. It allows asymetic On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)",
+ "ccSpecific": {
+ "indicatorId": 80,
+ "propertyId": 5
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "value",
+ "propertyKey": 65537,
+ "propertyName": "value",
+ "propertyKeyName": "Electric_kWh_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [kWh]",
+ "unit": "kWh",
+ "ccSpecific": {
+ "meterType": 1,
+ "rateType": 1,
+ "scale": 0
+ }
+ },
+ "value": 78.057
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "deltaTime",
+ "propertyKey": 65537,
+ "propertyName": "deltaTime",
+ "propertyKeyName": "Electric_kWh_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [kWh] (prev. time delta)",
+ "unit": "s",
+ "ccSpecific": {
+ "meterType": 1,
+ "rateType": 1,
+ "scale": 0
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "value",
+ "propertyKey": 66049,
+ "propertyName": "value",
+ "propertyKeyName": "Electric_W_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [W]",
+ "unit": "W",
+ "ccSpecific": {
+ "meterType": 1,
+ "rateType": 1,
+ "scale": 2
+ }
+ },
+ "value": 0.4
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "deltaTime",
+ "propertyKey": 66049,
+ "propertyName": "deltaTime",
+ "propertyKeyName": "Electric_W_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [W] (prev. time delta)",
+ "unit": "s",
+ "ccSpecific": {
+ "meterType": 1,
+ "rateType": 1,
+ "scale": 2
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "reset",
+ "propertyName": "reset",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "boolean",
+ "readable": false,
+ "writeable": true,
+ "label": "Reset accumulated values"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "previousValue",
+ "propertyKey": 65537,
+ "propertyName": "previousValue",
+ "propertyKeyName": "Electric_kWh_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [kWh] (prev. value)",
+ "unit": "kWh",
+ "ccSpecific": {
+ "meterType": 1,
+ "rateType": 1,
+ "scale": 0
+ }
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 50,
+ "commandClassName": "Meter",
+ "property": "previousValue",
+ "propertyKey": 66049,
+ "propertyName": "previousValue",
+ "propertyKeyName": "Electric_W_Consumed",
+ "ccVersion": 3,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Electric Consumed [W] (prev. value)",
+ "unit": "W",
+ "ccSpecific": {
+ "meterType": 1,
+ "rateType": 1,
+ "scale": 2
+ }
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 99,
+ "label": "Target value"
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "duration",
+ "propertyName": "duration",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "duration",
+ "readable": true,
+ "writeable": true,
+ "label": "Transition duration"
+ },
+ "value": {
+ "value": 0,
+ "unit": "seconds"
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 99,
+ "label": "Current value"
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "Up",
+ "propertyName": "Up",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Perform a level change (Up)",
+ "ccSpecific": {
+ "switchType": 2
+ }
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "Down",
+ "propertyName": "Down",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Perform a level change (Down)",
+ "ccSpecific": {
+ "switchType": 2
+ }
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 99,
+ "label": "Target value"
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "duration",
+ "propertyName": "duration",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "duration",
+ "readable": true,
+ "writeable": true,
+ "label": "Transition duration"
+ },
+ "value": {
+ "value": 0,
+ "unit": "seconds"
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 99,
+ "label": "Current value"
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "Up",
+ "propertyName": "Up",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Perform a level change (Up)",
+ "ccSpecific": {
+ "switchType": 2
+ }
+ }
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 38,
+ "commandClassName": "Multilevel Switch",
+ "property": "Down",
+ "propertyName": "Down",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Perform a level change (Down)",
+ "ccSpecific": {
+ "switchType": 2
+ }
+ }
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/tests/fixtures/zwave_js/lock_august_asl03_state.json b/tests/fixtures/zwave_js/lock_august_asl03_state.json
index b6d4434185347f..2b218cd915b277 100644
--- a/tests/fixtures/zwave_js/lock_august_asl03_state.json
+++ b/tests/fixtures/zwave_js/lock_august_asl03_state.json
@@ -6,17 +6,10 @@
"status": 4,
"ready": true,
"deviceClass": {
- "basic": "Routing Slave",
- "generic": "Entry Control",
- "specific": "Secure Keypad Door Lock",
- "mandatorySupportedCCs": [
- "Basic",
- "Door Lock",
- "User Code",
- "Manufacturer Specific",
- "Security",
- "Version"
- ],
+ "basic": {"key": 4, "label":"Routing Slave"},
+ "generic": {"key": 64, "label":"Entry Control"},
+ "specific": {"key": 3, "label":"Secure Keypad Door Lock"},
+ "mandatorySupportedCCs": [],
"mandatoryControlCCs": []
},
"isListening": false,
@@ -73,6 +66,7 @@
"userIcon": 768
}
],
+ "commandClasses": [],
"values": [
{
"commandClassName": "Door Lock",
diff --git a/tests/fixtures/zwave_js/lock_id_lock_as_id150_state.json b/tests/fixtures/zwave_js/lock_id_lock_as_id150_state.json
new file mode 100644
index 00000000000000..f5e66b7e7a6675
--- /dev/null
+++ b/tests/fixtures/zwave_js/lock_id_lock_as_id150_state.json
@@ -0,0 +1,2919 @@
+{
+ "nodeId": 60,
+ "index": 0,
+ "installerIcon": 768,
+ "userIcon": 768,
+ "status": 4,
+ "ready": true,
+ "isListening": false,
+ "isFrequentListening": true,
+ "isRouting": true,
+ "maxBaudRate": 40000,
+ "isSecure": true,
+ "version": 4,
+ "isBeaming": true,
+ "manufacturerId": 883,
+ "productId": 1,
+ "productType": 3,
+ "firmwareVersion": "1.6",
+ "zwavePlusVersion": 1,
+ "nodeType": 0,
+ "roleType": 7,
+ "deviceConfig": {
+ "filename": "/usr/src/node_modules/@zwave-js/config/config/devices/0x0373/id-150_1.6.json",
+ "manufacturerId": 883,
+ "manufacturer": "ID Lock AS",
+ "label": "ID-150",
+ "description": "Z wave module for ID Lock 150 and 101",
+ "devices": [
+ {
+ "productType": "0x0003",
+ "productId": "0x0001"
+ }
+ ],
+ "firmwareVersion": {
+ "min": "1.6",
+ "max": "255.255"
+ },
+ "paramInformation": {
+ "_map": {}
+ }
+ },
+ "label": "ID-150",
+ "neighbors": [1, 10, 22, 30, 33, 34, 41, 43, 53, 55, 70, 71, 84, 93],
+ "interviewAttempts": 1,
+ "interviewStage": 7,
+ "endpoints": [
+ {
+ "nodeId": 60,
+ "index": 0,
+ "installerIcon": 768,
+ "userIcon": 768
+ }
+ ],
+ "values": [
+ {
+ "endpoint": 0,
+ "commandClass": 98,
+ "commandClassName": "Door Lock",
+ "property": "currentMode",
+ "propertyName": "currentMode",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Current lock mode",
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Unsecured",
+ "1": "UnsecuredWithTimeout",
+ "16": "InsideUnsecured",
+ "17": "InsideUnsecuredWithTimeout",
+ "32": "OutsideUnsecured",
+ "33": "OutsideUnsecuredWithTimeout",
+ "254": "Unknown",
+ "255": "Secured"
+ }
+ },
+ "value": 255
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 98,
+ "commandClassName": "Door Lock",
+ "property": "targetMode",
+ "propertyName": "targetMode",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Target lock mode",
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Unsecured",
+ "1": "UnsecuredWithTimeout",
+ "16": "InsideUnsecured",
+ "17": "InsideUnsecuredWithTimeout",
+ "32": "OutsideUnsecured",
+ "33": "OutsideUnsecuredWithTimeout",
+ "254": "Unknown",
+ "255": "Secured"
+ }
+ },
+ "value": 255
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 98,
+ "commandClassName": "Door Lock",
+ "property": "outsideHandlesCanOpenDoor",
+ "propertyName": "outsideHandlesCanOpenDoor",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Which outside handles can open the door (actual status)"
+ },
+ "value": [false, false, false, false]
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 98,
+ "commandClassName": "Door Lock",
+ "property": "insideHandlesCanOpenDoor",
+ "propertyName": "insideHandlesCanOpenDoor",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Which inside handles can open the door (actual status)"
+ },
+ "value": [false, false, false, false]
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 98,
+ "commandClassName": "Door Lock",
+ "property": "latchStatus",
+ "propertyName": "latchStatus",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "The current status of the latch"
+ },
+ "value": "open"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 98,
+ "commandClassName": "Door Lock",
+ "property": "boltStatus",
+ "propertyName": "boltStatus",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "The current status of the bolt"
+ },
+ "value": "locked"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 98,
+ "commandClassName": "Door Lock",
+ "property": "doorStatus",
+ "propertyName": "doorStatus",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "The current status of the door"
+ },
+ "value": "closed"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 98,
+ "commandClassName": "Door Lock",
+ "property": "lockTimeout",
+ "propertyName": "lockTimeout",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Seconds until lock mode times out"
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 98,
+ "commandClassName": "Door Lock",
+ "property": "operationType",
+ "propertyName": "operationType",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Lock operation type",
+ "min": 0,
+ "max": 255,
+ "states": {
+ "1": "Constant",
+ "2": "Timed"
+ }
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 98,
+ "commandClassName": "Door Lock",
+ "property": "outsideHandlesCanOpenDoorConfiguration",
+ "propertyName": "outsideHandlesCanOpenDoorConfiguration",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true,
+ "label": "Which outside handles can open the door (configuration)"
+ },
+ "value": [true, true, true, true]
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 98,
+ "commandClassName": "Door Lock",
+ "property": "insideHandlesCanOpenDoorConfiguration",
+ "propertyName": "insideHandlesCanOpenDoorConfiguration",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true,
+ "label": "Which inside handles can open the door (configuration)"
+ },
+ "value": [true, true, true, true]
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 98,
+ "commandClassName": "Door Lock",
+ "property": "lockTimeoutConfiguration",
+ "propertyName": "lockTimeoutConfiguration",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "Duration of timed mode in seconds",
+ "min": 0,
+ "max": 65535
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 1,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "1",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (1)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 1,
+ "propertyName": "userCode",
+ "propertyKeyName": "1",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (1)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": "5555"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 2,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "2",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (2)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 2,
+ "propertyName": "userCode",
+ "propertyKeyName": "2",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (2)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": "5555"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 3,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "3",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (3)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 3,
+ "propertyName": "userCode",
+ "propertyKeyName": "3",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (3)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": "5555"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 4,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "4",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (4)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 4,
+ "propertyName": "userCode",
+ "propertyKeyName": "4",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (4)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": "5555"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 5,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "5",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (5)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 5,
+ "propertyName": "userCode",
+ "propertyKeyName": "5",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (5)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 6,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "6",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (6)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 6,
+ "propertyName": "userCode",
+ "propertyKeyName": "6",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (6)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 7,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "7",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (7)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 7,
+ "propertyName": "userCode",
+ "propertyKeyName": "7",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (7)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 8,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "8",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (8)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 8,
+ "propertyName": "userCode",
+ "propertyKeyName": "8",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (8)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 9,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "9",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (9)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 9,
+ "propertyName": "userCode",
+ "propertyKeyName": "9",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (9)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": "5555"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 10,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "10",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (10)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 10,
+ "propertyName": "userCode",
+ "propertyKeyName": "10",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (10)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 11,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "11",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (11)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 11,
+ "propertyName": "userCode",
+ "propertyKeyName": "11",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (11)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 12,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "12",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (12)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 12,
+ "propertyName": "userCode",
+ "propertyKeyName": "12",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (12)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 13,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "13",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (13)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 13,
+ "propertyName": "userCode",
+ "propertyKeyName": "13",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (13)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 14,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "14",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (14)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 14,
+ "propertyName": "userCode",
+ "propertyKeyName": "14",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (14)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 15,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "15",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (15)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 15,
+ "propertyName": "userCode",
+ "propertyKeyName": "15",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (15)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 16,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "16",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (16)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 16,
+ "propertyName": "userCode",
+ "propertyKeyName": "16",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (16)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 17,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "17",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (17)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 17,
+ "propertyName": "userCode",
+ "propertyKeyName": "17",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (17)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 18,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "18",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (18)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 18,
+ "propertyName": "userCode",
+ "propertyKeyName": "18",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (18)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 19,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "19",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (19)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 19,
+ "propertyName": "userCode",
+ "propertyKeyName": "19",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (19)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 20,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "20",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (20)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 20,
+ "propertyName": "userCode",
+ "propertyKeyName": "20",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (20)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 21,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "21",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (21)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 21,
+ "propertyName": "userCode",
+ "propertyKeyName": "21",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (21)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 22,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "22",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (22)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 22,
+ "propertyName": "userCode",
+ "propertyKeyName": "22",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (22)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 23,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "23",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (23)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 23,
+ "propertyName": "userCode",
+ "propertyKeyName": "23",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (23)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 24,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "24",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (24)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 24,
+ "propertyName": "userCode",
+ "propertyKeyName": "24",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (24)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 25,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "25",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (25)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 25,
+ "propertyName": "userCode",
+ "propertyKeyName": "25",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (25)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 26,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "26",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (26)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 26,
+ "propertyName": "userCode",
+ "propertyKeyName": "26",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (26)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": {
+ "type": "Buffer",
+ "data": [0, 52, 50, 54, 53, 50, 50, 50, 51, 56]
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 27,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "27",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (27)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 27,
+ "propertyName": "userCode",
+ "propertyKeyName": "27",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (27)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 28,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "28",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (28)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 28,
+ "propertyName": "userCode",
+ "propertyKeyName": "28",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (28)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 29,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "29",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (29)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 29,
+ "propertyName": "userCode",
+ "propertyKeyName": "29",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (29)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 30,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "30",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (30)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 30,
+ "propertyName": "userCode",
+ "propertyKeyName": "30",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (30)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 31,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "31",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (31)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 31,
+ "propertyName": "userCode",
+ "propertyKeyName": "31",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (31)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 32,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "32",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (32)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 32,
+ "propertyName": "userCode",
+ "propertyKeyName": "32",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (32)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 33,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "33",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (33)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 33,
+ "propertyName": "userCode",
+ "propertyKeyName": "33",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (33)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 34,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "34",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (34)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 34,
+ "propertyName": "userCode",
+ "propertyKeyName": "34",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (34)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": {
+ "type": "Buffer",
+ "data": [0, 52, 53, 0, 49, 49, 50, 50, 51, 56]
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 35,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "35",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (35)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 35,
+ "propertyName": "userCode",
+ "propertyKeyName": "35",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (35)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": {
+ "type": "Buffer",
+ "data": [0, 52, 52, 69, 56, 56, 50, 50, 51, 56]
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 36,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "36",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (36)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 36,
+ "propertyName": "userCode",
+ "propertyKeyName": "36",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (36)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 37,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "37",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (37)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 37,
+ "propertyName": "userCode",
+ "propertyKeyName": "37",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (37)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 38,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "38",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (38)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 38,
+ "propertyName": "userCode",
+ "propertyKeyName": "38",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (38)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 39,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "39",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (39)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 39,
+ "propertyName": "userCode",
+ "propertyKeyName": "39",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (39)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 40,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "40",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (40)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 40,
+ "propertyName": "userCode",
+ "propertyKeyName": "40",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (40)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 42,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "42",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (42)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 42,
+ "propertyName": "userCode",
+ "propertyKeyName": "42",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (42)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 44,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "44",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (44)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 44,
+ "propertyName": "userCode",
+ "propertyKeyName": "44",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (44)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 46,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "46",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (46)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 46,
+ "propertyName": "userCode",
+ "propertyKeyName": "46",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (46)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 48,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "48",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (48)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 48,
+ "propertyName": "userCode",
+ "propertyKeyName": "48",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (48)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 50,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "50",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (50)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 50,
+ "propertyName": "userCode",
+ "propertyKeyName": "50",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (50)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 41,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "41",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (41)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 41,
+ "propertyName": "userCode",
+ "propertyKeyName": "41",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (41)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 43,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "43",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (43)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 43,
+ "propertyName": "userCode",
+ "propertyKeyName": "43",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (43)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 45,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "45",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (45)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 45,
+ "propertyName": "userCode",
+ "propertyKeyName": "45",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (45)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 47,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "47",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (47)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 47,
+ "propertyName": "userCode",
+ "propertyKeyName": "47",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (47)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 49,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "49",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (49)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 49,
+ "propertyName": "userCode",
+ "propertyKeyName": "49",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (49)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 108,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "108",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (108)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 108,
+ "propertyName": "userCode",
+ "propertyKeyName": "108",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (108)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": ""
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userIdStatus",
+ "propertyKey": 109,
+ "propertyName": "userIdStatus",
+ "propertyKeyName": "109",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "label": "User ID status (109)",
+ "states": {
+ "0": "Available",
+ "1": "Enabled",
+ "2": "Disabled"
+ }
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 99,
+ "commandClassName": "User Code",
+ "property": "userCode",
+ "propertyKey": 109,
+ "propertyName": "userCode",
+ "propertyKeyName": "109",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "string",
+ "readable": true,
+ "writeable": true,
+ "label": "User Code (109)",
+ "minLength": 4,
+ "maxLength": 10
+ },
+ "value": "51816472"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 1,
+ "propertyName": "Door lock mode",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "default": 1,
+ "readable": true,
+ "writeable": true,
+ "description": "Set if the lock is in away mode and if automatic locking should be enabled",
+ "label": "Door lock mode",
+ "min": 0,
+ "max": 3,
+ "states": {
+ "0": "Disable Away Manual Lock",
+ "1": "Disable Away Auto Lock",
+ "2": "Enable Away Manual Lock",
+ "3": "Enable Away Auto Lock"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 2,
+ "propertyName": "RFID Mode",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "default": 5,
+ "readable": true,
+ "writeable": true,
+ "label": "RFID Mode",
+ "min": 5,
+ "max": 9,
+ "states": {
+ "5": "RFID activated",
+ "9": "RFID deactivated"
+ }
+ },
+ "value": 5
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 3,
+ "propertyName": "Door Hinge Position Mode",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "default": 0,
+ "readable": true,
+ "writeable": true,
+ "description": "Tell the lock which side your hinges are on seen from the outside",
+ "label": "Door Hinge Position Mode",
+ "min": 0,
+ "max": 1,
+ "states": {
+ "0": "Right hinged operation",
+ "1": "Left hinged operation"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 4,
+ "propertyName": "Door Audio Volume Level",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "default": 5,
+ "readable": true,
+ "writeable": true,
+ "description": "Set the Audio Volume Level of the Lock",
+ "label": "Door Audio Volume Level",
+ "min": 0,
+ "max": 6,
+ "states": {
+ "0": "No sound",
+ "1": "Level 1",
+ "2": "Level 2",
+ "3": "Level 3",
+ "4": "Level 4",
+ "5": "Level 5",
+ "6": "Max. sound level"
+ }
+ },
+ "value": 5
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 5,
+ "propertyName": "Door ReLock Mode",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "default": 1,
+ "readable": true,
+ "writeable": true,
+ "description": "Sets if the door should relock or not",
+ "label": "Door ReLock Mode",
+ "min": 0,
+ "max": 1,
+ "states": {
+ "0": "Disabled",
+ "1": "Enabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 6,
+ "propertyName": "Service PIN Mode",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "default": 0,
+ "readable": true,
+ "writeable": true,
+ "description": "Sets the validity of the service PIN",
+ "label": "Service PIN Mode",
+ "min": 0,
+ "max": 254,
+ "states": {
+ "0": "Deactivated",
+ "1": "Valid 1 time",
+ "2": "Valid 2 times",
+ "3": "Valid 5 times",
+ "4": "Valid 10 times",
+ "5": "Generate Random PIN 1x use",
+ "6": "Generate Random PIN 24h use",
+ "7": "Always Valid",
+ "8": "Valid for 12h",
+ "9": "Valid for 24h",
+ "254": "Disabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 7,
+ "propertyName": "Door Lock Model Type",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "default": 0,
+ "readable": true,
+ "writeable": false,
+ "description": "Sends information if the model of the lock is 101 or 150",
+ "label": "Door Lock Model Type",
+ "min": 0,
+ "max": 0,
+ "states": {}
+ },
+ "value": -106
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 8,
+ "propertyName": "Updater Mode",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "default": 0,
+ "readable": true,
+ "writeable": true,
+ "description": "Enables use of the Updater app",
+ "label": "Updater Mode",
+ "min": 0,
+ "max": 3,
+ "states": {
+ "0": "Disabled (no sound)",
+ "1": "Enabled (no sound)",
+ "2": "Disabled",
+ "3": "Enabled"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 112,
+ "commandClassName": "Configuration",
+ "property": 9,
+ "propertyName": "Master PIN Unlock Mode",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "default": 1,
+ "readable": true,
+ "writeable": true,
+ "description": "Configures if the Master PIN can unlock",
+ "label": "Master PIN Unlock Mode",
+ "min": 0,
+ "max": 1,
+ "states": {
+ "0": "Disabled",
+ "1": "Enabled"
+ }
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 113,
+ "commandClassName": "Notification",
+ "property": "Access Control",
+ "propertyKey": "Lock state",
+ "propertyName": "Access Control",
+ "propertyKeyName": "Lock state",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Lock state",
+ "ccSpecific": {
+ "notificationType": 6
+ },
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "idle",
+ "11": "Lock jammed"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 113,
+ "commandClassName": "Notification",
+ "property": "Home Security",
+ "propertyKey": "Cover status",
+ "propertyName": "Home Security",
+ "propertyKeyName": "Cover status",
+ "ccVersion": 4,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Cover status",
+ "ccSpecific": {
+ "notificationType": 7
+ },
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "idle",
+ "3": "Tampering, product cover removed"
+ }
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Manufacturer ID",
+ "min": 0,
+ "max": 65535
+ },
+ "value": 883
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product type",
+ "min": 0,
+ "max": 65535
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Product ID",
+ "min": 0,
+ "max": 65535
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 128,
+ "commandClassName": "Battery",
+ "property": "level",
+ "propertyName": "level",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "label": "Battery level",
+ "min": 0,
+ "max": 100,
+ "unit": "%"
+ },
+ "value": 70
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 128,
+ "commandClassName": "Battery",
+ "property": "isLow",
+ "propertyName": "isLow",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": false,
+ "label": "Low battery level"
+ },
+ "value": false
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ },
+ "value": "4.5"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ },
+ "value": ["1.6"]
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "hardwareVersion",
+ "propertyName": "hardwareVersion",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip hardware version"
+ }
+ }
+ ],
+ "deviceClass": {
+ "basic": {
+ "key": 4,
+ "label": "Routing Slave"
+ },
+ "generic": {
+ "key": 64,
+ "label": "Entry Control"
+ },
+ "specific": {
+ "key": 3,
+ "label": "Secure Keypad Door Lock"
+ },
+ "mandatorySupportedCCs": [32, 98, 99, 114, 152, 134],
+ "mandatoryControlledCCs": []
+ },
+ "commandClasses": [
+ {
+ "id": 89,
+ "name": "Association Group Information",
+ "version": 1,
+ "isSecure": true
+ },
+ {
+ "id": 90,
+ "name": "Device Reset Locally",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 94,
+ "name": "Z-Wave Plus Info",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 98,
+ "name": "Door Lock",
+ "version": 2,
+ "isSecure": true
+ },
+ {
+ "id": 99,
+ "name": "User Code",
+ "version": 1,
+ "isSecure": true
+ },
+ {
+ "id": 112,
+ "name": "Configuration",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 113,
+ "name": "Notification",
+ "version": 4,
+ "isSecure": true
+ },
+ {
+ "id": 114,
+ "name": "Manufacturer Specific",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 122,
+ "name": "Firmware Update Meta Data",
+ "version": 2,
+ "isSecure": true
+ },
+ {
+ "id": 128,
+ "name": "Battery",
+ "version": 1,
+ "isSecure": true
+ },
+ {
+ "id": 133,
+ "name": "Association",
+ "version": 2,
+ "isSecure": true
+ },
+ {
+ "id": 134,
+ "name": "Version",
+ "version": 2,
+ "isSecure": true
+ },
+ {
+ "id": 152,
+ "name": "Security",
+ "version": 1,
+ "isSecure": true
+ }
+ ]
+}
diff --git a/tests/fixtures/zwave_js/lock_schlage_be469_state.json b/tests/fixtures/zwave_js/lock_schlage_be469_state.json
index af1fc92a206572..be1ddb9c3f0786 100644
--- a/tests/fixtures/zwave_js/lock_schlage_be469_state.json
+++ b/tests/fixtures/zwave_js/lock_schlage_be469_state.json
@@ -4,17 +4,10 @@
"status": 4,
"ready": true,
"deviceClass": {
- "basic": "Static Controller",
- "generic": "Entry Control",
- "specific": "Secure Keypad Door Lock",
- "mandatorySupportedCCs": [
- "Basic",
- "Door Lock",
- "User Code",
- "Manufacturer Specific",
- "Security",
- "Version"
- ],
+ "basic": {"key": 2, "label":"Static Controller"},
+ "generic": {"key": 64, "label":"Entry Control"},
+ "specific": {"key": 3, "label":"Secure Keypad Door Lock"},
+ "mandatorySupportedCCs": [],
"mandatoryControlCCs": []
},
"isListening": false,
@@ -57,6 +50,7 @@
"index": 0
}
],
+ "commandClasses": [],
"values": [
{
"commandClassName": "Door Lock",
diff --git a/tests/fixtures/zwave_js/multisensor_6_state.json b/tests/fixtures/zwave_js/multisensor_6_state.json
index 3c508ffd3ff018..131a5aa026f468 100644
--- a/tests/fixtures/zwave_js/multisensor_6_state.json
+++ b/tests/fixtures/zwave_js/multisensor_6_state.json
@@ -6,13 +6,10 @@
"status": 1,
"ready": true,
"deviceClass": {
- "basic": "Static Controller",
- "generic": "Multilevel Sensor",
- "specific": "Routing Multilevel Sensor",
- "mandatorySupportedCCs": [
- "Basic",
- "Multilevel Sensor"
- ],
+ "basic": {"key": 2, "label":"Static Controller"},
+ "generic": {"key": 21, "label":"Multilevel Sensor"},
+ "specific": {"key": 1, "label":"Routing Multilevel Sensor"},
+ "mandatorySupportedCCs": [],
"mandatoryControlCCs": []
},
"isListening": true,
@@ -70,6 +67,7 @@
"userIcon": 3079
}
],
+ "commandClasses": [],
"values": [
{
"commandClassName": "Basic",
diff --git a/tests/fixtures/zwave_js/nortek_thermostat_added_event.json b/tests/fixtures/zwave_js/nortek_thermostat_added_event.json
index d778f77ce2450f..60078100caf615 100644
--- a/tests/fixtures/zwave_js/nortek_thermostat_added_event.json
+++ b/tests/fixtures/zwave_js/nortek_thermostat_added_event.json
@@ -7,16 +7,10 @@
"status": 0,
"ready": false,
"deviceClass": {
- "basic": "Static Controller",
- "generic": "Thermostat",
- "specific": "Thermostat General V2",
- "mandatorySupportedCCs": [
- "Basic",
- "Manufacturer Specific",
- "Thermostat Mode",
- "Thermostat Setpoint",
- "Version"
- ],
+ "basic": {"key": 2, "label":"Static Controller"},
+ "generic": {"key": 8, "label":"Thermostat"},
+ "specific": {"key": 6, "label":"Thermostat General V2"},
+ "mandatorySupportedCCs": [],
"mandatoryControlCCs": []
},
"neighbors": [],
@@ -27,6 +21,7 @@
"index": 0
}
],
+ "commandClasses": [],
"values": [
{
"commandClassName": "Basic",
diff --git a/tests/fixtures/zwave_js/nortek_thermostat_removed_event.json b/tests/fixtures/zwave_js/nortek_thermostat_removed_event.json
index ed25a65054350b..01bad6c4a8fb18 100644
--- a/tests/fixtures/zwave_js/nortek_thermostat_removed_event.json
+++ b/tests/fixtures/zwave_js/nortek_thermostat_removed_event.json
@@ -7,16 +7,10 @@
"status": 4,
"ready": true,
"deviceClass": {
- "basic": "Static Controller",
- "generic": "Thermostat",
- "specific": "Thermostat General V2",
- "mandatorySupportedCCs": [
- "Basic",
- "Manufacturer Specific",
- "Thermostat Mode",
- "Thermostat Setpoint",
- "Version"
- ],
+ "basic": {"key": 2, "label":"Static Controller"},
+ "generic": {"key": 8, "label":"Thermostat"},
+ "specific": {"key": 6, "label":"Thermostat General V2"},
+ "mandatorySupportedCCs": [],
"mandatoryControlCCs": []
},
"isListening": false,
@@ -67,6 +61,7 @@
"index": 0
}
],
+ "commandClasses": [],
"values": [
{
"commandClassName": "Manufacturer Specific",
diff --git a/tests/fixtures/zwave_js/nortek_thermostat_state.json b/tests/fixtures/zwave_js/nortek_thermostat_state.json
index 62a08999cda727..4e6ca17e01386f 100644
--- a/tests/fixtures/zwave_js/nortek_thermostat_state.json
+++ b/tests/fixtures/zwave_js/nortek_thermostat_state.json
@@ -6,16 +6,10 @@
"status": 4,
"ready": true,
"deviceClass": {
- "basic": "Static Controller",
- "generic": "Thermostat",
- "specific": "Thermostat General V2",
- "mandatorySupportedCCs": [
- "Basic",
- "Manufacturer Specific",
- "Thermostat Mode",
- "Thermostat Setpoint",
- "Version"
- ],
+ "basic": {"key": 2, "label":"Static Controller"},
+ "generic": {"key": 8, "label":"Thermostat"},
+ "specific": {"key": 6, "label":"Thermostat General V2"},
+ "mandatorySupportedCCs": [],
"mandatoryControlCCs": []
},
"isListening": false,
@@ -75,6 +69,7 @@
"userIcon": 4608
}
],
+ "commandClasses": [],
"values": [
{
"commandClassName": "Manufacturer Specific",
diff --git a/tests/fixtures/zwave_js/null_name_check_state.json b/tests/fixtures/zwave_js/null_name_check_state.json
new file mode 100644
index 00000000000000..fe63eaee20787d
--- /dev/null
+++ b/tests/fixtures/zwave_js/null_name_check_state.json
@@ -0,0 +1,414 @@
+{
+ "nodeId": 10,
+ "index": 0,
+ "installerIcon": 3328,
+ "userIcon": 3328,
+ "status": 4,
+ "ready": true,
+ "isListening": true,
+ "isFrequentListening": false,
+ "isRouting": true,
+ "maxBaudRate": 40000,
+ "isSecure": false,
+ "version": 4,
+ "isBeaming": true,
+ "manufacturerId": 277,
+ "productId": 1,
+ "productType": 272,
+ "firmwareVersion": "2.17",
+ "zwavePlusVersion": 1,
+ "nodeType": 0,
+ "roleType": 1,
+ "neighbors": [],
+ "endpointCountIsDynamic": false,
+ "endpointsHaveIdenticalCapabilities": false,
+ "individualEndpointCount": 4,
+ "aggregatedEndpointCount": 0,
+ "interviewAttempts": 1,
+ "interviewStage": 7,
+ "endpoints": [
+ {
+ "nodeId": 10,
+ "index": 0,
+ "installerIcon": 3328,
+ "userIcon": 3328
+ },
+ {
+ "nodeId": 10,
+ "index": 1
+ },
+ {
+ "nodeId": 10,
+ "index": 2
+ },
+ {
+ "nodeId": 10,
+ "index": 3
+ },
+ {
+ "nodeId": 10,
+ "index": 4
+ }
+ ],
+ "values": [
+ {
+ "endpoint": 0,
+ "commandClass": 49,
+ "commandClassName": "Multilevel Sensor",
+ "property": "Air temperature",
+ "propertyName": "Air temperature",
+ "ccVersion": 7,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "unit": "\u00b0C",
+ "label": "Air temperature",
+ "ccSpecific": {
+ "sensorType": 1,
+ "scale": 0
+ }
+ },
+ "value": 2.9
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 49,
+ "commandClassName": "Multilevel Sensor",
+ "property": "Humidity",
+ "propertyName": "Humidity",
+ "ccVersion": 7,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "unit": "%",
+ "label": "Humidity",
+ "ccSpecific": {
+ "sensorType": 5,
+ "scale": 0
+ }
+ },
+ "value": 8
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Manufacturer ID"
+ },
+ "value": 277
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product type"
+ },
+ "value": 272
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product ID"
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ },
+ "value": "4.38"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ },
+ "value": ["2.17"]
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "hardwareVersion",
+ "propertyName": "hardwareVersion",
+ "ccVersion": 2,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip hardware version"
+ }
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 37,
+ "commandClassName": "Binary Switch",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": false,
+ "label": "Current value"
+ },
+ "value": false
+ },
+ {
+ "endpoint": 1,
+ "commandClass": 37,
+ "commandClassName": "Binary Switch",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Target value"
+ },
+ "value": false
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 37,
+ "commandClassName": "Binary Switch",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": false,
+ "label": "Current value"
+ },
+ "value": false
+ },
+ {
+ "endpoint": 2,
+ "commandClass": 37,
+ "commandClassName": "Binary Switch",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Target value"
+ },
+ "value": false
+ },
+ {
+ "endpoint": 3,
+ "commandClass": 37,
+ "commandClassName": "Binary Switch",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": false,
+ "label": "Current value"
+ },
+ "value": false
+ },
+ {
+ "endpoint": 3,
+ "commandClass": 37,
+ "commandClassName": "Binary Switch",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Target value"
+ }
+ },
+ {
+ "endpoint": 4,
+ "commandClass": 37,
+ "commandClassName": "Binary Switch",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": false,
+ "label": "Current value"
+ },
+ "value": true
+ },
+ {
+ "endpoint": 4,
+ "commandClass": 37,
+ "commandClassName": "Binary Switch",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Target value"
+ },
+ "value": true
+ }
+ ],
+ "deviceClass": {
+ "basic": {
+ "key": 4,
+ "label": "Routing Slave"
+ },
+ "generic": {
+ "key": 33,
+ "label": "Multilevel Sensor"
+ },
+ "specific": {
+ "key": 1,
+ "label": "Routing Multilevel Sensor"
+ },
+ "mandatorySupportedCCs": [32, 49],
+ "mandatoryControlledCCs": []
+ },
+ "commandClasses": [
+ {
+ "id": 37,
+ "name": "Binary Switch",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 49,
+ "name": "Multilevel Sensor",
+ "version": 7,
+ "isSecure": false
+ },
+ {
+ "id": 89,
+ "name": "Association Group Information",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 90,
+ "name": "Device Reset Locally",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 94,
+ "name": "Z-Wave Plus Info",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 96,
+ "name": "Multi Channel",
+ "version": 4,
+ "isSecure": false
+ },
+ {
+ "id": 112,
+ "name": "Configuration",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 114,
+ "name": "Manufacturer Specific",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 122,
+ "name": "Firmware Update Meta Data",
+ "version": 3,
+ "isSecure": false
+ },
+ {
+ "id": 133,
+ "name": "Association",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 134,
+ "name": "Version",
+ "version": 2,
+ "isSecure": false
+ },
+ {
+ "id": 142,
+ "name": "Multi Channel Association",
+ "version": 3,
+ "isSecure": false
+ }
+ ]
+}
diff --git a/tests/fixtures/zwave_js/srt321_hrt4_zw_state.json b/tests/fixtures/zwave_js/srt321_hrt4_zw_state.json
new file mode 100644
index 00000000000000..a2fdaa995614bd
--- /dev/null
+++ b/tests/fixtures/zwave_js/srt321_hrt4_zw_state.json
@@ -0,0 +1,262 @@
+{
+ "nodeId": 20,
+ "index": 0,
+ "status": 4,
+ "ready": true,
+ "deviceClass": {
+ "basic": {
+ "key": 4,
+ "label": "Routing Slave"
+ },
+ "generic": {
+ "key": 8,
+ "label": "Thermostat"
+ },
+ "specific": {
+ "key": 0,
+ "label": "Unused"
+ },
+ "mandatorySupportedCCs": [],
+ "mandatoryControlledCCs": []
+ },
+ "isListening": true,
+ "isFrequentListening": false,
+ "isRouting": true,
+ "maxBaudRate": 40000,
+ "isSecure": false,
+ "version": 3,
+ "isBeaming": true,
+ "manufacturerId": 89,
+ "productId": 1,
+ "productType": 3,
+ "firmwareVersion": "2.0",
+ "name": "main_heat_actionner",
+ "location": "kitchen",
+ "deviceConfig": {
+ "filename": "/opt/node_modules/@zwave-js/config/config/devices/0x0059/asr-zw.json",
+ "manufacturerId": 89,
+ "manufacturer": "Secure Meters (UK) Ltd.",
+ "label": "SRT322",
+ "description": "Thermostat Receiver",
+ "devices": [
+ {
+ "productType": "0x0003",
+ "productId": "0x0001"
+ }
+ ],
+ "firmwareVersion": {
+ "min": "0.0",
+ "max": "255.255"
+ }
+ },
+ "label": "SRT322",
+ "neighbors": [
+ 1,
+ 5,
+ 10,
+ 12,
+ 13,
+ 14,
+ 15,
+ 18,
+ 21
+ ],
+ "interviewAttempts": 1,
+ "interviewStage": 7,
+ "commandClasses": [
+ {
+ "id": 37,
+ "name": "Binary Switch",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 64,
+ "name": "Thermostat Mode",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 114,
+ "name": "Manufacturer Specific",
+ "version": 1,
+ "isSecure": false
+ },
+ {
+ "id": 134,
+ "name": "Version",
+ "version": 1,
+ "isSecure": false
+ }
+ ],
+ "endpoints": [
+ {
+ "nodeId": 20,
+ "index": 0
+ }
+ ],
+ "values": [
+ {
+ "endpoint": 0,
+ "commandClass": 37,
+ "commandClassName": "Binary Switch",
+ "property": "currentValue",
+ "propertyName": "currentValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": false,
+ "label": "Current value"
+ },
+ "value": false
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 37,
+ "commandClassName": "Binary Switch",
+ "property": "targetValue",
+ "propertyName": "targetValue",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "boolean",
+ "readable": true,
+ "writeable": true,
+ "label": "Target value"
+ },
+ "value": false
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "mode",
+ "propertyName": "mode",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": true,
+ "min": 0,
+ "max": 255,
+ "states": {
+ "0": "Off",
+ "1": "Heat"
+ },
+ "label": "Thermostat mode"
+ },
+ "value": 0
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 64,
+ "commandClassName": "Thermostat Mode",
+ "property": "manufacturerData",
+ "propertyName": "manufacturerData",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": true
+ }
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "manufacturerId",
+ "propertyName": "manufacturerId",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Manufacturer ID"
+ },
+ "value": 89
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productType",
+ "propertyName": "productType",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product type"
+ },
+ "value": 3
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 114,
+ "commandClassName": "Manufacturer Specific",
+ "property": "productId",
+ "propertyName": "productId",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "number",
+ "readable": true,
+ "writeable": false,
+ "min": 0,
+ "max": 65535,
+ "label": "Product ID"
+ },
+ "value": 1
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "libraryType",
+ "propertyName": "libraryType",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Library type"
+ },
+ "value": 6
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "protocolVersion",
+ "propertyName": "protocolVersion",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave protocol version"
+ },
+ "value": "2.78"
+ },
+ {
+ "endpoint": 0,
+ "commandClass": 134,
+ "commandClassName": "Version",
+ "property": "firmwareVersions",
+ "propertyName": "firmwareVersions",
+ "ccVersion": 1,
+ "metadata": {
+ "type": "any",
+ "readable": true,
+ "writeable": false,
+ "label": "Z-Wave chip firmware versions"
+ },
+ "value": [
+ "2.0"
+ ]
+ }
+ ]
+ }
diff --git a/tests/hassfest/test_version.py b/tests/hassfest/test_version.py
new file mode 100644
index 00000000000000..f99ee911a695f1
--- /dev/null
+++ b/tests/hassfest/test_version.py
@@ -0,0 +1,47 @@
+"""Tests for hassfest version."""
+import pytest
+import voluptuous as vol
+
+from script.hassfest.manifest import (
+ CUSTOM_INTEGRATION_MANIFEST_SCHEMA,
+ validate_version,
+)
+from script.hassfest.model import Integration
+
+
+@pytest.fixture
+def integration():
+ """Fixture for hassfest integration model."""
+ integration = Integration("")
+ integration.manifest = {
+ "domain": "test",
+ "documentation": "https://example.com",
+ "name": "test",
+ "codeowners": ["@awesome"],
+ }
+ return integration
+
+
+def test_validate_version_no_key(integration: Integration):
+ """Test validate version with no key."""
+ validate_version(integration)
+ assert (
+ "No 'version' key in the manifest file. This will cause a future version of Home Assistant to block this integration."
+ in [x.error for x in integration.errors]
+ )
+
+
+def test_validate_custom_integration_manifest(integration: Integration):
+ """Test validate custom integration manifest."""
+
+ with pytest.raises(vol.Invalid):
+ integration.manifest["version"] = "lorem_ipsum"
+ CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest)
+
+ with pytest.raises(vol.Invalid):
+ integration.manifest["version"] = None
+ CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest)
+
+ integration.manifest["version"] = "1"
+ schema = CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest)
+ assert schema["version"] == "1"
diff --git a/tests/helpers/conftest.py b/tests/helpers/conftest.py
new file mode 100644
index 00000000000000..4b3b9bf465d1f0
--- /dev/null
+++ b/tests/helpers/conftest.py
@@ -0,0 +1,31 @@
+"""Fixtures for helpers."""
+from unittest.mock import Mock, patch
+
+import pytest
+
+
+@pytest.fixture
+def mock_integration_frame():
+ """Mock as if we're calling code from inside an integration."""
+ correct_frame = Mock(
+ filename="/home/paulus/homeassistant/components/hue/light.py",
+ lineno="23",
+ line="self.light.is_on",
+ )
+ with patch(
+ "homeassistant.helpers.frame.extract_stack",
+ return_value=[
+ Mock(
+ filename="/home/paulus/homeassistant/core.py",
+ lineno="23",
+ line="do_something()",
+ ),
+ correct_frame,
+ Mock(
+ filename="/home/paulus/aiohue/lights.py",
+ lineno="2",
+ line="something()",
+ ),
+ ],
+ ):
+ yield correct_frame
diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py
index ec008dde7da1f8..7dca029987e85c 100644
--- a/tests/helpers/test_area_registry.py
+++ b/tests/helpers/test_area_registry.py
@@ -1,7 +1,4 @@
"""Tests for the Area Registry."""
-import asyncio
-import unittest.mock
-
import pytest
from homeassistant.core import callback
@@ -61,7 +58,7 @@ async def test_create_area_with_name_already_in_use(hass, registry, update_event
with pytest.raises(ValueError) as e_info:
area2 = registry.async_create("mock")
assert area1 != area2
- assert e_info == "Name is already in use"
+ assert e_info == "The name mock 2 (mock2) is already in use"
await hass.async_block_till_done()
@@ -84,7 +81,7 @@ async def test_delete_area(hass, registry, update_events):
"""Make sure that we can delete an area."""
area = registry.async_create("mock")
- await registry.async_delete(area.id)
+ registry.async_delete(area.id)
assert not registry.areas
@@ -136,6 +133,18 @@ async def test_update_area_with_same_name(registry):
assert len(registry.areas) == 1
+async def test_update_area_with_same_name_change_case(registry):
+ """Make sure that we can reapply the same name with a different case to the area."""
+ area = registry.async_create("mock")
+
+ updated_area = registry.async_update(area.id, name="Mock")
+
+ assert updated_area.name == "Mock"
+ assert updated_area.id == area.id
+ assert updated_area.normalized_name == area.normalized_name
+ assert len(registry.areas) == 1
+
+
async def test_update_area_with_name_already_in_use(registry):
"""Make sure that we can't update an area with a name already in use."""
area1 = registry.async_create("mock1")
@@ -143,17 +152,31 @@ async def test_update_area_with_name_already_in_use(registry):
with pytest.raises(ValueError) as e_info:
registry.async_update(area1.id, name="mock2")
- assert e_info == "Name is already in use"
+ assert e_info == "The name mock 2 (mock2) is already in use"
assert area1.name == "mock1"
assert area2.name == "mock2"
assert len(registry.areas) == 2
+async def test_update_area_with_normalized_name_already_in_use(registry):
+ """Make sure that we can't update an area with a normalized name already in use."""
+ area1 = registry.async_create("mock1")
+ area2 = registry.async_create("Moc k2")
+
+ with pytest.raises(ValueError) as e_info:
+ registry.async_update(area1.id, name="mock2")
+ assert e_info == "The name mock 2 (mock2) is already in use"
+
+ assert area1.name == "mock1"
+ assert area2.name == "Moc k2"
+ assert len(registry.areas) == 2
+
+
async def test_load_area(hass, registry):
"""Make sure that we can load/save data correctly."""
- registry.async_create("mock1")
- registry.async_create("mock2")
+ area1 = registry.async_create("mock1")
+ area2 = registry.async_create("mock2")
assert len(registry.areas) == 2
@@ -163,7 +186,13 @@ async def test_load_area(hass, registry):
assert list(registry.areas) == list(registry2.areas)
+ area1_registry2 = registry2.async_get_or_create("mock1")
+ assert area1_registry2.id == area1.id
+ area2_registry2 = registry2.async_get_or_create("mock2")
+ assert area2_registry2.id == area2.id
+
+@pytest.mark.parametrize("load_registries", [False])
async def test_loading_area_from_storage(hass, hass_storage):
"""Test loading stored areas on start."""
hass_storage[area_registry.STORAGE_KEY] = {
@@ -171,20 +200,45 @@ async def test_loading_area_from_storage(hass, hass_storage):
"data": {"areas": [{"id": "12345A", "name": "mock"}]},
}
- registry = await area_registry.async_get_registry(hass)
+ await area_registry.async_load(hass)
+ registry = area_registry.async_get(hass)
+
+ assert len(registry.areas) == 1
+
+
+async def test_async_get_or_create(hass, registry):
+ """Make sure we can get the area by name."""
+ area = registry.async_get_or_create("Mock1")
+ area2 = registry.async_get_or_create("mock1")
+ area3 = registry.async_get_or_create("mock 1")
+
+ assert area == area2
+ assert area == area3
+ assert area2 == area3
+
+
+async def test_async_get_area_by_name(hass, registry):
+ """Make sure we can get the area by name."""
+ registry.async_create("Mock1")
assert len(registry.areas) == 1
+ assert registry.async_get_area_by_name("M o c k 1").normalized_name == "mock1"
-async def test_loading_race_condition(hass):
- """Test only one storage load called when concurrent loading occurred ."""
- with unittest.mock.patch(
- "homeassistant.helpers.area_registry.AreaRegistry.async_load"
- ) as mock_load:
- results = await asyncio.gather(
- area_registry.async_get_registry(hass),
- area_registry.async_get_registry(hass),
- )
- mock_load.assert_called_once_with()
- assert results[0] == results[1]
+async def test_async_get_area_by_name_not_found(hass, registry):
+ """Make sure we return None for non-existent areas."""
+ registry.async_create("Mock1")
+
+ assert len(registry.areas) == 1
+
+ assert registry.async_get_area_by_name("non_exist") is None
+
+
+async def test_async_get_area(hass, registry):
+ """Make sure we can get the area by id."""
+ area = registry.async_create("Mock1")
+
+ assert len(registry.areas) == 1
+
+ assert registry.async_get_area(area.id).normalized_name == "mock1"
diff --git a/tests/helpers/test_collection.py b/tests/helpers/test_collection.py
index d5a8526b6da8d7..11ab0f46ce4527 100644
--- a/tests/helpers/test_collection.py
+++ b/tests/helpers/test_collection.py
@@ -226,7 +226,7 @@ async def test_attach_entity_component_collection(hass):
"""Test attaching collection to entity component."""
ent_comp = entity_component.EntityComponent(_LOGGER, "test", hass)
coll = collection.ObservableCollection(_LOGGER)
- collection.attach_entity_component_collection(ent_comp, coll, MockEntity)
+ collection.sync_entity_lifecycle(hass, "test", "test", ent_comp, coll, MockEntity)
await coll.notify_changes(
[
diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py
index fe2a9aa4406dcc..05f348ddfeb065 100644
--- a/tests/helpers/test_condition.py
+++ b/tests/helpers/test_condition.py
@@ -1,16 +1,55 @@
"""Test the condition helper."""
-from logging import ERROR
from unittest.mock import patch
import pytest
-from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers import condition
+from homeassistant.exceptions import ConditionError, HomeAssistantError
+from homeassistant.helpers import condition, trace
from homeassistant.helpers.template import Template
from homeassistant.setup import async_setup_component
from homeassistant.util import dt
+def assert_element(trace_element, expected_element, path):
+ """Assert a trace element is as expected.
+
+ Note: Unused variable 'path' is passed to get helpful errors from pytest.
+ """
+ expected_result = expected_element.get("result", {})
+ # Check that every item in expected_element is present and equal in trace_element
+ # The redundant set operation gives helpful errors from pytest
+ assert not set(expected_result) - set(trace_element._result or {})
+ for result_key, result in expected_result.items():
+ assert trace_element._result[result_key] == result
+
+ # Check for unexpected items in trace_element
+ assert not set(trace_element._result or {}) - set(expected_result)
+
+ if "error_type" in expected_element:
+ assert isinstance(trace_element._error, expected_element["error_type"])
+ else:
+ assert trace_element._error is None
+
+
+@pytest.fixture(autouse=True)
+def prepare_condition_trace():
+ """Clear previous trace."""
+ trace.trace_clear()
+
+
+def assert_condition_trace(expected):
+ """Assert a trace condition sequence is as expected."""
+ condition_trace = trace.trace_get(clear=False)
+ trace.trace_clear()
+ expected_trace_keys = list(expected.keys())
+ assert list(condition_trace.keys()) == expected_trace_keys
+ for trace_key_index, key in enumerate(expected_trace_keys):
+ assert len(condition_trace[key]) == len(expected[key])
+ for index, element in enumerate(expected[key]):
+ path = f"[{trace_key_index}][{index}]"
+ assert_element(condition_trace[key][index], element, path)
+
+
async def test_invalid_condition(hass):
"""Test if invalid condition raises."""
with pytest.raises(HomeAssistantError):
@@ -34,6 +73,7 @@ async def test_and_condition(hass):
test = await condition.async_from_config(
hass,
{
+ "alias": "And Condition",
"condition": "and",
"conditions": [
{
@@ -50,14 +90,128 @@ async def test_and_condition(hass):
},
)
+ with pytest.raises(ConditionError):
+ test(hass)
+ assert_condition_trace(
+ {
+ "": [{"error_type": ConditionError}],
+ "conditions/0": [{"error_type": ConditionError}],
+ "conditions/0/entity_id/0": [{"error_type": ConditionError}],
+ "conditions/1": [{"error_type": ConditionError}],
+ "conditions/1/entity_id/0": [{"error_type": ConditionError}],
+ }
+ )
+
hass.states.async_set("sensor.temperature", 120)
assert not test(hass)
+ assert_condition_trace(
+ {
+ "": [{"result": {"result": False}}],
+ "conditions/0": [{"result": {"result": False}}],
+ "conditions/0/entity_id/0": [
+ {"result": {"result": False, "state": "120", "wanted_state": "100"}}
+ ],
+ }
+ )
hass.states.async_set("sensor.temperature", 105)
assert not test(hass)
+ assert_condition_trace(
+ {
+ "": [{"result": {"result": False}}],
+ "conditions/0": [{"result": {"result": False}}],
+ "conditions/0/entity_id/0": [
+ {"result": {"result": False, "state": "105", "wanted_state": "100"}}
+ ],
+ }
+ )
hass.states.async_set("sensor.temperature", 100)
assert test(hass)
+ assert_condition_trace(
+ {
+ "": [{"result": {"result": True}}],
+ "conditions/0": [{"result": {"result": True}}],
+ "conditions/0/entity_id/0": [
+ {"result": {"result": True, "state": "100", "wanted_state": "100"}}
+ ],
+ "conditions/1": [{"result": {"result": True}}],
+ "conditions/1/entity_id/0": [{"result": {"result": True, "state": 100.0}}],
+ }
+ )
+
+
+async def test_and_condition_raises(hass):
+ """Test the 'and' condition."""
+ test = await condition.async_from_config(
+ hass,
+ {
+ "alias": "And Condition",
+ "condition": "and",
+ "conditions": [
+ {
+ "condition": "state",
+ "entity_id": "sensor.temperature",
+ "state": "100",
+ },
+ {
+ "condition": "numeric_state",
+ "entity_id": "sensor.temperature2",
+ "above": 110,
+ },
+ ],
+ },
+ )
+
+ # All subconditions raise, the AND-condition should raise
+ with pytest.raises(ConditionError):
+ test(hass)
+ assert_condition_trace(
+ {
+ "": [{"error_type": ConditionError}],
+ "conditions/0": [{"error_type": ConditionError}],
+ "conditions/0/entity_id/0": [{"error_type": ConditionError}],
+ "conditions/1": [{"error_type": ConditionError}],
+ "conditions/1/entity_id/0": [{"error_type": ConditionError}],
+ }
+ )
+
+ # The first subconditions raises, the second returns True, the AND-condition
+ # should raise
+ hass.states.async_set("sensor.temperature2", 120)
+ with pytest.raises(ConditionError):
+ test(hass)
+ assert_condition_trace(
+ {
+ "": [{"error_type": ConditionError}],
+ "conditions/0": [{"error_type": ConditionError}],
+ "conditions/0/entity_id/0": [{"error_type": ConditionError}],
+ "conditions/1": [{"result": {"result": True}}],
+ "conditions/1/entity_id/0": [{"result": {"result": True, "state": 120.0}}],
+ }
+ )
+
+ # The first subconditions raises, the second returns False, the AND-condition
+ # should return False
+ hass.states.async_set("sensor.temperature2", 90)
+ assert not test(hass)
+ assert_condition_trace(
+ {
+ "": [{"result": {"result": False}}],
+ "conditions/0": [{"error_type": ConditionError}],
+ "conditions/0/entity_id/0": [{"error_type": ConditionError}],
+ "conditions/1": [{"result": {"result": False}}],
+ "conditions/1/entity_id/0": [
+ {
+ "result": {
+ "result": False,
+ "state": 90.0,
+ "wanted_state_above": 110.0,
+ }
+ }
+ ],
+ }
+ )
async def test_and_condition_with_template(hass):
@@ -68,6 +222,7 @@ async def test_and_condition_with_template(hass):
"condition": "and",
"conditions": [
{
+ "alias": "Template Condition",
"condition": "template",
"value_template": '{{ states.sensor.temperature.state == "100" }}',
},
@@ -95,6 +250,7 @@ async def test_or_condition(hass):
test = await condition.async_from_config(
hass,
{
+ "alias": "Or Condition",
"condition": "or",
"conditions": [
{
@@ -111,14 +267,138 @@ async def test_or_condition(hass):
},
)
+ with pytest.raises(ConditionError):
+ test(hass)
+ assert_condition_trace(
+ {
+ "": [{"error_type": ConditionError}],
+ "conditions/0": [{"error_type": ConditionError}],
+ "conditions/0/entity_id/0": [{"error_type": ConditionError}],
+ "conditions/1": [{"error_type": ConditionError}],
+ "conditions/1/entity_id/0": [{"error_type": ConditionError}],
+ }
+ )
+
hass.states.async_set("sensor.temperature", 120)
assert not test(hass)
+ assert_condition_trace(
+ {
+ "": [{"result": {"result": False}}],
+ "conditions/0": [{"result": {"result": False}}],
+ "conditions/0/entity_id/0": [
+ {"result": {"result": False, "state": "120", "wanted_state": "100"}}
+ ],
+ "conditions/1": [{"result": {"result": False}}],
+ "conditions/1/entity_id/0": [
+ {
+ "result": {
+ "result": False,
+ "state": 120.0,
+ "wanted_state_below": 110.0,
+ }
+ }
+ ],
+ }
+ )
hass.states.async_set("sensor.temperature", 105)
assert test(hass)
+ assert_condition_trace(
+ {
+ "": [{"result": {"result": True}}],
+ "conditions/0": [{"result": {"result": False}}],
+ "conditions/0/entity_id/0": [
+ {"result": {"result": False, "state": "105", "wanted_state": "100"}}
+ ],
+ "conditions/1": [{"result": {"result": True}}],
+ "conditions/1/entity_id/0": [{"result": {"result": True, "state": 105.0}}],
+ }
+ )
hass.states.async_set("sensor.temperature", 100)
assert test(hass)
+ assert_condition_trace(
+ {
+ "": [{"result": {"result": True}}],
+ "conditions/0": [{"result": {"result": True}}],
+ "conditions/0/entity_id/0": [
+ {"result": {"result": True, "state": "100", "wanted_state": "100"}}
+ ],
+ }
+ )
+
+
+async def test_or_condition_raises(hass):
+ """Test the 'or' condition."""
+ test = await condition.async_from_config(
+ hass,
+ {
+ "alias": "Or Condition",
+ "condition": "or",
+ "conditions": [
+ {
+ "condition": "state",
+ "entity_id": "sensor.temperature",
+ "state": "100",
+ },
+ {
+ "condition": "numeric_state",
+ "entity_id": "sensor.temperature2",
+ "above": 110,
+ },
+ ],
+ },
+ )
+
+ # All subconditions raise, the OR-condition should raise
+ with pytest.raises(ConditionError):
+ test(hass)
+ assert_condition_trace(
+ {
+ "": [{"error_type": ConditionError}],
+ "conditions/0": [{"error_type": ConditionError}],
+ "conditions/0/entity_id/0": [{"error_type": ConditionError}],
+ "conditions/1": [{"error_type": ConditionError}],
+ "conditions/1/entity_id/0": [{"error_type": ConditionError}],
+ }
+ )
+
+ # The first subconditions raises, the second returns False, the OR-condition
+ # should raise
+ hass.states.async_set("sensor.temperature2", 100)
+ with pytest.raises(ConditionError):
+ test(hass)
+ assert_condition_trace(
+ {
+ "": [{"error_type": ConditionError}],
+ "conditions/0": [{"error_type": ConditionError}],
+ "conditions/0/entity_id/0": [{"error_type": ConditionError}],
+ "conditions/1": [{"result": {"result": False}}],
+ "conditions/1/entity_id/0": [
+ {
+ "result": {
+ "result": False,
+ "state": 100.0,
+ "wanted_state_above": 110.0,
+ }
+ }
+ ],
+ }
+ )
+
+ # The first subconditions raises, the second returns True, the OR-condition
+ # should return True
+ hass.states.async_set("sensor.temperature2", 120)
+ assert test(hass)
+ assert_condition_trace(
+ {
+ "": [{"result": {"result": True}}],
+ "conditions/0": [{"error_type": ConditionError}],
+ "conditions/0/entity_id/0": [{"error_type": ConditionError}],
+ "conditions/1": [{"result": {"result": True}}],
+ "conditions/1/entity_id/0": [{"result": {"result": True, "state": 120.0}}],
+ }
+ )
async def test_or_condition_with_template(hass):
@@ -153,6 +433,7 @@ async def test_not_condition(hass):
test = await condition.async_from_config(
hass,
{
+ "alias": "Not Condition",
"condition": "not",
"conditions": [
{
@@ -169,17 +450,148 @@ async def test_not_condition(hass):
},
)
+ with pytest.raises(ConditionError):
+ test(hass)
+ assert_condition_trace(
+ {
+ "": [{"error_type": ConditionError}],
+ "conditions/0": [{"error_type": ConditionError}],
+ "conditions/0/entity_id/0": [{"error_type": ConditionError}],
+ "conditions/1": [{"error_type": ConditionError}],
+ "conditions/1/entity_id/0": [{"error_type": ConditionError}],
+ }
+ )
+
hass.states.async_set("sensor.temperature", 101)
assert test(hass)
+ assert_condition_trace(
+ {
+ "": [{"result": {"result": True}}],
+ "conditions/0": [{"result": {"result": False}}],
+ "conditions/0/entity_id/0": [
+ {"result": {"result": False, "state": "101", "wanted_state": "100"}}
+ ],
+ "conditions/1": [{"result": {"result": False}}],
+ "conditions/1/entity_id/0": [
+ {
+ "result": {
+ "result": False,
+ "state": 101.0,
+ "wanted_state_below": 50.0,
+ }
+ }
+ ],
+ }
+ )
hass.states.async_set("sensor.temperature", 50)
assert test(hass)
+ assert_condition_trace(
+ {
+ "": [{"result": {"result": True}}],
+ "conditions/0": [{"result": {"result": False}}],
+ "conditions/0/entity_id/0": [
+ {"result": {"result": False, "state": "50", "wanted_state": "100"}}
+ ],
+ "conditions/1": [{"result": {"result": False}}],
+ "conditions/1/entity_id/0": [
+ {"result": {"result": False, "state": 50.0, "wanted_state_below": 50.0}}
+ ],
+ }
+ )
hass.states.async_set("sensor.temperature", 49)
assert not test(hass)
+ assert_condition_trace(
+ {
+ "": [{"result": {"result": False}}],
+ "conditions/0": [{"result": {"result": False}}],
+ "conditions/0/entity_id/0": [
+ {"result": {"result": False, "state": "49", "wanted_state": "100"}}
+ ],
+ "conditions/1": [{"result": {"result": True}}],
+ "conditions/1/entity_id/0": [{"result": {"result": True, "state": 49.0}}],
+ }
+ )
hass.states.async_set("sensor.temperature", 100)
assert not test(hass)
+ assert_condition_trace(
+ {
+ "": [{"result": {"result": False}}],
+ "conditions/0": [{"result": {"result": True}}],
+ "conditions/0/entity_id/0": [
+ {"result": {"result": True, "state": "100", "wanted_state": "100"}}
+ ],
+ }
+ )
+
+
+async def test_not_condition_raises(hass):
+ """Test the 'and' condition."""
+ test = await condition.async_from_config(
+ hass,
+ {
+ "alias": "Not Condition",
+ "condition": "not",
+ "conditions": [
+ {
+ "condition": "state",
+ "entity_id": "sensor.temperature",
+ "state": "100",
+ },
+ {
+ "condition": "numeric_state",
+ "entity_id": "sensor.temperature2",
+ "below": 50,
+ },
+ ],
+ },
+ )
+
+ # All subconditions raise, the NOT-condition should raise
+ with pytest.raises(ConditionError):
+ test(hass)
+ assert_condition_trace(
+ {
+ "": [{"error_type": ConditionError}],
+ "conditions/0": [{"error_type": ConditionError}],
+ "conditions/0/entity_id/0": [{"error_type": ConditionError}],
+ "conditions/1": [{"error_type": ConditionError}],
+ "conditions/1/entity_id/0": [{"error_type": ConditionError}],
+ }
+ )
+
+ # The first subconditions raises, the second returns False, the NOT-condition
+ # should raise
+ hass.states.async_set("sensor.temperature2", 90)
+ with pytest.raises(ConditionError):
+ test(hass)
+ assert_condition_trace(
+ {
+ "": [{"error_type": ConditionError}],
+ "conditions/0": [{"error_type": ConditionError}],
+ "conditions/0/entity_id/0": [{"error_type": ConditionError}],
+ "conditions/1": [{"result": {"result": False}}],
+ "conditions/1/entity_id/0": [
+ {"result": {"result": False, "state": 90.0, "wanted_state_below": 50.0}}
+ ],
+ }
+ )
+
+ # The first subconditions raises, the second returns True, the NOT-condition
+ # should return False
+ hass.states.async_set("sensor.temperature2", 40)
+ assert not test(hass)
+ assert_condition_trace(
+ {
+ "": [{"result": {"result": False}}],
+ "conditions/0": [{"error_type": ConditionError}],
+ "conditions/0/entity_id/0": [{"error_type": ConditionError}],
+ "conditions/1": [{"result": {"result": True}}],
+ "conditions/1/entity_id/0": [{"result": {"result": True, "state": 40.0}}],
+ }
+ )
async def test_not_condition_with_template(hass):
@@ -217,36 +629,45 @@ async def test_not_condition_with_template(hass):
async def test_time_window(hass):
"""Test time condition windows."""
- sixam = dt.parse_time("06:00:00")
- sixpm = dt.parse_time("18:00:00")
+ sixam = "06:00:00"
+ sixpm = "18:00:00"
+
+ test1 = await condition.async_from_config(
+ hass,
+ {"alias": "Time Cond", "condition": "time", "after": sixam, "before": sixpm},
+ )
+ test2 = await condition.async_from_config(
+ hass,
+ {"alias": "Time Cond", "condition": "time", "after": sixpm, "before": sixam},
+ )
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt.now().replace(hour=3),
):
- assert not condition.time(hass, after=sixam, before=sixpm)
- assert condition.time(hass, after=sixpm, before=sixam)
+ assert not test1(hass)
+ assert test2(hass)
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt.now().replace(hour=9),
):
- assert condition.time(hass, after=sixam, before=sixpm)
- assert not condition.time(hass, after=sixpm, before=sixam)
+ assert test1(hass)
+ assert not test2(hass)
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt.now().replace(hour=15),
):
- assert condition.time(hass, after=sixam, before=sixpm)
- assert not condition.time(hass, after=sixpm, before=sixam)
+ assert test1(hass)
+ assert not test2(hass)
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt.now().replace(hour=21),
):
- assert not condition.time(hass, after=sixam, before=sixpm)
- assert condition.time(hass, after=sixpm, before=sixam)
+ assert not test1(hass)
+ assert test2(hass)
async def test_time_using_input_datetime(hass):
@@ -334,25 +755,61 @@ async def test_time_using_input_datetime(hass):
hass, after="input_datetime.pm", before="input_datetime.am"
)
- assert not condition.time(hass, after="input_datetime.not_existing")
- assert not condition.time(hass, before="input_datetime.not_existing")
+ with pytest.raises(ConditionError):
+ condition.time(hass, after="input_datetime.not_existing")
+
+ with pytest.raises(ConditionError):
+ condition.time(hass, before="input_datetime.not_existing")
+
+async def test_state_raises(hass):
+ """Test that state raises ConditionError on errors."""
+ # No entity
+ with pytest.raises(ConditionError, match="no entity"):
+ condition.state(hass, entity=None, req_state="missing")
-async def test_if_numeric_state_not_raise_on_unavailable(hass):
- """Test numeric_state doesn't raise on unavailable/unknown state."""
+ # Unknown entities
test = await condition.async_from_config(
hass,
- {"condition": "numeric_state", "entity_id": "sensor.temperature", "below": 42},
+ {
+ "condition": "state",
+ "entity_id": ["sensor.door_unknown", "sensor.window_unknown"],
+ "state": "open",
+ },
)
+ with pytest.raises(ConditionError, match="unknown entity.*door"):
+ test(hass)
+ with pytest.raises(ConditionError, match="unknown entity.*window"):
+ test(hass)
+
+ # Unknown attribute
+ with pytest.raises(ConditionError, match=r"attribute .* does not exist"):
+ test = await condition.async_from_config(
+ hass,
+ {
+ "condition": "state",
+ "entity_id": "sensor.door",
+ "attribute": "model",
+ "state": "acme",
+ },
+ )
- with patch("homeassistant.helpers.condition._LOGGER.warning") as logwarn:
- hass.states.async_set("sensor.temperature", "unavailable")
- assert not test(hass)
- assert len(logwarn.mock_calls) == 0
+ hass.states.async_set("sensor.door", "open")
+ test(hass)
- hass.states.async_set("sensor.temperature", "unknown")
- assert not test(hass)
- assert len(logwarn.mock_calls) == 0
+ # Unknown state entity
+ with pytest.raises(ConditionError, match="input_text.missing"):
+ test = await condition.async_from_config(
+ hass,
+ {
+ "condition": "state",
+ "entity_id": "sensor.door",
+ "state": "input_text.missing",
+ },
+ )
+
+ hass.states.async_set("sensor.door", "open")
+ test(hass)
async def test_state_multiple_entities(hass):
@@ -392,6 +849,7 @@ async def test_multiple_states(hass):
"condition": "and",
"conditions": [
{
+ "alias": "State Condition",
"condition": "state",
"entity_id": "sensor.temperature",
"state": ["100", "200"],
@@ -428,7 +886,8 @@ async def test_state_attribute(hass):
)
hass.states.async_set("sensor.temperature", 100, {"unkown_attr": 200})
- assert not test(hass)
+ with pytest.raises(ConditionError):
+ test(hass)
hass.states.async_set("sensor.temperature", 100, {"attribute1": 200})
assert test(hass)
@@ -462,7 +921,8 @@ async def test_state_attribute_boolean(hass):
assert not test(hass)
hass.states.async_set("sensor.temperature", 100, {"no_happening": 201})
- assert not test(hass)
+ with pytest.raises(ConditionError):
+ test(hass)
hass.states.async_set("sensor.temperature", 100, {"happening": False})
assert test(hass)
@@ -501,7 +961,6 @@ async def test_state_using_input_entities(hass):
"state": [
"input_text.hello",
"input_select.hello",
- "input_number.not_exist",
"salut",
],
},
@@ -550,6 +1009,131 @@ async def test_state_using_input_entities(hass):
assert test(hass)
+async def test_numeric_state_known_non_matching(hass):
+ """Test that numeric_state doesn't match on known non-matching states."""
+ hass.states.async_set("sensor.temperature", "unavailable")
+ test = await condition.async_from_config(
+ hass,
+ {
+ "condition": "numeric_state",
+ "entity_id": "sensor.temperature",
+ "above": 0,
+ },
+ )
+
+ # Unavailable state
+ assert not test(hass)
+
+ # Unknown state
+ hass.states.async_set("sensor.temperature", "unknown")
+ assert not test(hass)
+
+
+async def test_numeric_state_raises(hass):
+ """Test that numeric_state raises ConditionError on errors."""
+ # Unknown entities
+ test = await condition.async_from_config(
+ hass,
+ {
+ "condition": "numeric_state",
+ "entity_id": ["sensor.temperature_unknown", "sensor.humidity_unknown"],
+ "above": 0,
+ },
+ )
+ with pytest.raises(ConditionError, match="unknown entity.*temperature"):
+ test(hass)
+ with pytest.raises(ConditionError, match="unknown entity.*humidity"):
+ test(hass)
+
+ # Unknown attribute
+ with pytest.raises(ConditionError, match=r"attribute .* does not exist"):
+ test = await condition.async_from_config(
+ hass,
+ {
+ "condition": "numeric_state",
+ "entity_id": "sensor.temperature",
+ "attribute": "temperature",
+ "above": 0,
+ },
+ )
+
+ hass.states.async_set("sensor.temperature", 50)
+ test(hass)
+
+ # Template error
+ with pytest.raises(ConditionError, match="ZeroDivisionError"):
+ test = await condition.async_from_config(
+ hass,
+ {
+ "condition": "numeric_state",
+ "entity_id": "sensor.temperature",
+ "value_template": "{{ 1 / 0 }}",
+ "above": 0,
+ },
+ )
+
+ hass.states.async_set("sensor.temperature", 50)
+ test(hass)
+
+ # Bad number
+ with pytest.raises(ConditionError, match="cannot be processed as a number"):
+ test = await condition.async_from_config(
+ hass,
+ {
+ "condition": "numeric_state",
+ "entity_id": "sensor.temperature",
+ "above": 0,
+ },
+ )
+
+ hass.states.async_set("sensor.temperature", "fifty")
+ test(hass)
+
+ # Below entity missing
+ with pytest.raises(ConditionError, match="'below' entity"):
+ test = await condition.async_from_config(
+ hass,
+ {
+ "condition": "numeric_state",
+ "entity_id": "sensor.temperature",
+ "below": "input_number.missing",
+ },
+ )
+
+ hass.states.async_set("sensor.temperature", 50)
+ test(hass)
+
+ # Below entity not a number
+ with pytest.raises(
+ ConditionError,
+ match="'below'.*input_number.missing.*cannot be processed as a number",
+ ):
+ hass.states.async_set("input_number.missing", "number")
+ test(hass)
+
+ # Above entity missing
+ with pytest.raises(ConditionError, match="'above' entity"):
+ test = await condition.async_from_config(
+ hass,
+ {
+ "condition": "numeric_state",
+ "entity_id": "sensor.temperature",
+ "above": "input_number.missing",
+ },
+ )
+
+ hass.states.async_set("sensor.temperature", 50)
+ test(hass)
+
+ # Above entity not a number
+ with pytest.raises(
+ ConditionError,
+ match="'above'.*input_number.missing.*cannot be processed as a number",
+ ):
+ hass.states.async_set("input_number.missing", "number")
+ test(hass)
+
+
async def test_numeric_state_multiple_entities(hass):
"""Test with multiple entities in condition."""
test = await condition.async_from_config(
@@ -558,6 +1142,7 @@ async def test_numeric_state_multiple_entities(hass):
"condition": "and",
"conditions": [
{
+ "alias": "Numeric State Condition",
"condition": "numeric_state",
"entity_id": ["sensor.temperature_1", "sensor.temperature_2"],
"below": 50,
@@ -579,7 +1164,7 @@ async def test_numeric_state_multiple_entities(hass):
assert not test(hass)
-async def test_numberic_state_attribute(hass):
+async def test_numeric_state_attribute(hass):
"""Test with numeric state attribute in condition."""
test = await condition.async_from_config(
hass,
@@ -597,7 +1182,8 @@ async def test_numberic_state_attribute(hass):
)
hass.states.async_set("sensor.temperature", 100, {"unkown_attr": 10})
- assert not test(hass)
+ with pytest.raises(ConditionError):
+ assert test(hass)
hass.states.async_set("sensor.temperature", 100, {"attribute1": 49})
assert test(hass)
@@ -609,7 +1195,8 @@ async def test_numberic_state_attribute(hass):
assert not test(hass)
hass.states.async_set("sensor.temperature", 100, {"attribute1": None})
- assert not test(hass)
+ with pytest.raises(ConditionError):
+ assert test(hass)
async def test_numeric_state_using_input_number(hass):
@@ -649,6 +1236,12 @@ async def test_numeric_state_using_input_number(hass):
hass.states.async_set("sensor.temperature", 100)
assert not test(hass)
+ hass.states.async_set("input_number.high", "unknown")
+ assert not test(hass)
+
+ hass.states.async_set("input_number.high", "unavailable")
+ assert not test(hass)
+
await hass.services.async_call(
"input_number",
"set_value",
@@ -660,13 +1253,107 @@ async def test_numeric_state_using_input_number(hass):
)
assert test(hass)
- assert not condition.async_numeric_state(
- hass, entity="sensor.temperature", below="input_number.not_exist"
+ hass.states.async_set("input_number.low", "unknown")
+ assert not test(hass)
+
+ hass.states.async_set("input_number.low", "unavailable")
+ assert not test(hass)
+
+ with pytest.raises(ConditionError):
+ condition.async_numeric_state(
+ hass, entity="sensor.temperature", below="input_number.not_exist"
+ )
+ with pytest.raises(ConditionError):
+ condition.async_numeric_state(
+ hass, entity="sensor.temperature", above="input_number.not_exist"
+ )
+
+
+async def test_zone_raises(hass):
+ """Test that zone raises ConditionError on errors."""
+ test = await condition.async_from_config(
+ hass,
+ {
+ "condition": "zone",
+ "entity_id": "device_tracker.cat",
+ "zone": "zone.home",
+ },
+ )
+
+ with pytest.raises(ConditionError, match="no zone"):
+ condition.zone(hass, zone_ent=None, entity="sensor.any")
+
+ with pytest.raises(ConditionError, match="unknown zone"):
+ test(hass)
+
+ hass.states.async_set(
+ "zone.home",
+ "zoning",
+ {"name": "home", "latitude": 2.1, "longitude": 1.1, "radius": 10},
+ )
+
+ with pytest.raises(ConditionError, match="no entity"):
+ condition.zone(hass, zone_ent="zone.home", entity=None)
+
+ with pytest.raises(ConditionError, match="unknown entity"):
+ test(hass)
+
+ hass.states.async_set(
+ "device_tracker.cat",
+ "home",
+ {"friendly_name": "cat"},
+ )
+
+ with pytest.raises(ConditionError, match="latitude"):
+ test(hass)
+
+ hass.states.async_set(
+ "device_tracker.cat",
+ "home",
+ {"friendly_name": "cat", "latitude": 2.1},
+ )
+
+ with pytest.raises(ConditionError, match="longitude"):
+ test(hass)
+
+ hass.states.async_set(
+ "device_tracker.cat",
+ "home",
+ {"friendly_name": "cat", "latitude": 2.1, "longitude": 1.1},
+ )
+
+ # All okay, now test multiple failed conditions
+ assert test(hass)
+
+ test = await condition.async_from_config(
+ hass,
+ {
+ "condition": "zone",
+ "entity_id": ["device_tracker.cat", "device_tracker.dog"],
+ "zone": ["zone.home", "zone.work"],
+ },
)
- assert not condition.async_numeric_state(
- hass, entity="sensor.temperature", above="input_number.not_exist"
+
+ with pytest.raises(ConditionError, match="dog"):
+ test(hass)
+
+ with pytest.raises(ConditionError, match="work"):
+ test(hass)
+
+ hass.states.async_set(
+ "zone.work",
+ "zoning",
+ {"name": "work", "latitude": 20, "longitude": 10, "radius": 25000},
)
+ hass.states.async_set(
+ "device_tracker.dog",
+ "work",
+ {"friendly_name": "dog", "latitude": 20.1, "longitude": 10.1},
+ )
+
+ assert test(hass)
+
async def test_zone_multiple_entities(hass):
"""Test with multiple entities in condition."""
@@ -676,6 +1363,7 @@ async def test_zone_multiple_entities(hass):
"condition": "and",
"conditions": [
{
+ "alias": "Zone Condition",
"condition": "zone",
"entity_id": ["device_tracker.person_1", "device_tracker.person_2"],
"zone": "zone.home",
@@ -901,19 +1589,14 @@ async def test_extract_devices():
)
-async def test_condition_template_error(hass, caplog):
+async def test_condition_template_error(hass):
"""Test invalid template."""
- caplog.set_level(ERROR)
-
test = await condition.async_from_config(
hass, {"condition": "template", "value_template": "{{ undefined.state }}"}
)
- assert not test(hass)
- assert len(caplog.records) == 1
- assert caplog.records[0].message.startswith(
- "Error during template condition: UndefinedError:"
- )
+ with pytest.raises(ConditionError, match="template"):
+ test(hass)
async def test_condition_template_invalid_results(hass):
diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py
index 874fd5df29addd..b3233556957bfd 100644
--- a/tests/helpers/test_config_entry_flow.py
+++ b/tests/helpers/test_config_entry_flow.py
@@ -78,6 +78,15 @@ async def test_user_has_confirmation(hass, discovery_flow_conf):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "confirm"
+ progress = hass.config_entries.flow.async_progress()
+ assert len(progress) == 1
+ assert progress[0]["flow_id"] == result["flow_id"]
+ assert progress[0]["context"] == {
+ "confirm_only": True,
+ "source": config_entries.SOURCE_USER,
+ "unique_id": "test",
+ }
+
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py
index 617c469069653b..b5257e635af1c4 100644
--- a/tests/helpers/test_config_entry_oauth2_flow.py
+++ b/tests/helpers/test_config_entry_oauth2_flow.py
@@ -99,9 +99,10 @@ def logger(self) -> logging.Logger:
"""Return logger."""
return logging.getLogger(__name__)
- with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestFlowHandler}):
- with pytest.raises(TypeError):
- TestFlowHandler()
+ with patch.dict(
+ config_entries.HANDLERS, {TEST_DOMAIN: TestFlowHandler}
+ ), pytest.raises(TypeError):
+ TestFlowHandler()
async def test_abort_if_no_implementation(hass, flow_handler):
@@ -113,7 +114,9 @@ async def test_abort_if_no_implementation(hass, flow_handler):
assert result["reason"] == "missing_configuration"
-async def test_abort_if_authorization_timeout(hass, flow_handler, local_impl):
+async def test_abort_if_authorization_timeout(
+ hass, flow_handler, local_impl, current_request_with_host
+):
"""Check timeout generating authorization url."""
flow_handler.async_register_implementation(hass, local_impl)
@@ -129,7 +132,9 @@ async def test_abort_if_authorization_timeout(hass, flow_handler, local_impl):
assert result["reason"] == "authorize_url_timeout"
-async def test_abort_if_no_url_available(hass, flow_handler, local_impl):
+async def test_abort_if_no_url_available(
+ hass, flow_handler, local_impl, current_request_with_host
+):
"""Check no_url_available generating authorization url."""
flow_handler.async_register_implementation(hass, local_impl)
diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py
index 1397e499c7ed5b..232d7bbb8b6032 100644
--- a/tests/helpers/test_config_validation.py
+++ b/tests/helpers/test_config_validation.py
@@ -1,4 +1,5 @@
"""Test config validators."""
+from collections import OrderedDict
from datetime import date, datetime, timedelta
import enum
import os
@@ -358,6 +359,11 @@ def test_service_schema():
"service": "homeassistant.turn_on",
"entity_id": ["light.kitchen", "light.ceiling"],
},
+ {
+ "service": "light.turn_on",
+ "entity_id": "all",
+ "alias": "turn on kitchen lights",
+ },
)
for value in options:
cv.SERVICE_SCHEMA(value)
@@ -794,6 +800,77 @@ def test_deprecated_cant_find_module():
)
+def test_deprecated_logger_with_config_attributes(caplog):
+ """Test if the logger outputs the correct message if the line and file attribute is available in config."""
+ file: str = "configuration.yaml"
+ line: int = 54
+ replacement = f"'mars' option near {file}:{line} is deprecated"
+ config = OrderedDict([("mars", "blah")])
+ setattr(config, "__config_file__", file)
+ setattr(config, "__line__", line)
+
+ cv.deprecated("mars", replacement_key="jupiter", default=False)(config)
+
+ assert len(caplog.records) == 1
+ assert replacement in caplog.text
+
+ caplog.clear()
+ assert len(caplog.records) == 0
+
+
+def test_deprecated_logger_with_one_config_attribute(caplog):
+ """Test if the logger outputs the correct message if only one of line and file attribute is available in config."""
+ file: str = "configuration.yaml"
+ line: int = 54
+ replacement = f"'mars' option near {file}:{line} is deprecated"
+ config = OrderedDict([("mars", "blah")])
+ setattr(config, "__config_file__", file)
+
+ cv.deprecated("mars", replacement_key="jupiter", default=False)(config)
+
+ assert len(caplog.records) == 1
+ assert replacement not in caplog.text
+ assert (
+ "The 'mars' option is deprecated, please replace it with 'jupiter'"
+ ) in caplog.text
+
+ caplog.clear()
+ assert len(caplog.records) == 0
+
+ config = OrderedDict([("mars", "blah")])
+ setattr(config, "__line__", line)
+
+ cv.deprecated("mars", replacement_key="jupiter", default=False)(config)
+
+ assert len(caplog.records) == 1
+ assert replacement not in caplog.text
+ assert (
+ "The 'mars' option is deprecated, please replace it with 'jupiter'"
+ ) in caplog.text
+
+ caplog.clear()
+ assert len(caplog.records) == 0
+
+
+def test_deprecated_logger_without_config_attributes(caplog):
+ """Test if the logger outputs the correct message if the line and file attribute is not available in config."""
+ file: str = "configuration.yaml"
+ line: int = 54
+ replacement = f"'mars' option near {file}:{line} is deprecated"
+ config = OrderedDict([("mars", "blah")])
+
+ cv.deprecated("mars", replacement_key="jupiter", default=False)(config)
+
+ assert len(caplog.records) == 1
+ assert replacement not in caplog.text
+ assert (
+ "The 'mars' option is deprecated, please replace it with 'jupiter'"
+ ) in caplog.text
+
+ caplog.clear()
+ assert len(caplog.records) == 0
+
+
def test_key_dependency():
"""Test key_dependency validator."""
schema = vol.Schema(cv.key_dependency("beer", "soda"))
@@ -857,7 +934,7 @@ def test_socket_timeout(): # pylint: disable=invalid-name
with pytest.raises(vol.Invalid):
schema(-1)
- assert _GLOBAL_DEFAULT_TIMEOUT == schema(None)
+ assert schema(None) == _GLOBAL_DEFAULT_TIMEOUT
assert schema(1) == 1.0
diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py
index 01959174335c0a..c5328000269b11 100644
--- a/tests/helpers/test_device_registry.py
+++ b/tests/helpers/test_device_registry.py
@@ -1,5 +1,4 @@
"""Tests for the Device Registry."""
-import asyncio
import time
from unittest.mock import patch
@@ -9,7 +8,12 @@
from homeassistant.core import CoreState, callback
from homeassistant.helpers import device_registry, entity_registry
-from tests.common import MockConfigEntry, flush_store, mock_device_registry
+from tests.common import (
+ MockConfigEntry,
+ flush_store,
+ mock_area_registry,
+ mock_device_registry,
+)
@pytest.fixture
@@ -18,6 +22,12 @@ def registry(hass):
return mock_device_registry(hass)
+@pytest.fixture
+def area_registry(hass):
+ """Return an empty, loaded, registry."""
+ return mock_area_registry(hass)
+
+
@pytest.fixture
def update_events(hass):
"""Capture update events."""
@@ -32,7 +42,9 @@ def async_capture(event):
return events
-async def test_get_or_create_returns_same_entry(hass, registry, update_events):
+async def test_get_or_create_returns_same_entry(
+ hass, registry, area_registry, update_events
+):
"""Make sure we do not duplicate entries."""
entry = registry.async_get_or_create(
config_entry_id="1234",
@@ -42,6 +54,7 @@ async def test_get_or_create_returns_same_entry(hass, registry, update_events):
name="name",
manufacturer="manufacturer",
model="model",
+ suggested_area="Game Room",
)
entry2 = registry.async_get_or_create(
config_entry_id="1234",
@@ -49,21 +62,31 @@ async def test_get_or_create_returns_same_entry(hass, registry, update_events):
identifiers={("bridgeid", "0123")},
manufacturer="manufacturer",
model="model",
+ suggested_area="Game Room",
)
entry3 = registry.async_get_or_create(
config_entry_id="1234",
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
+ game_room_area = area_registry.async_get_area_by_name("Game Room")
+ assert game_room_area is not None
+ assert len(area_registry.areas) == 1
+
assert len(registry.devices) == 1
+ assert entry.area_id == game_room_area.id
assert entry.id == entry2.id
assert entry.id == entry3.id
assert entry.identifiers == {("bridgeid", "0123")}
+ assert entry2.area_id == game_room_area.id
+
assert entry3.manufacturer == "manufacturer"
assert entry3.model == "model"
assert entry3.name == "name"
assert entry3.sw_version == "sw-version"
+ assert entry3.suggested_area == "Game Room"
+ assert entry3.area_id == game_room_area.id
await hass.async_block_till_done()
@@ -135,6 +158,7 @@ async def test_multiple_config_entries(registry):
assert entry2.config_entries == {"123", "456"}
+@pytest.mark.parametrize("load_registries", [False])
async def test_loading_from_storage(hass, hass_storage):
"""Test loading stored devices on start."""
hass_storage[device_registry.STORAGE_KEY] = {
@@ -154,6 +178,7 @@ async def test_loading_from_storage(hass, hass_storage):
"area_id": "12345A",
"name_by_user": "Test Friendly Name",
"disabled_by": "user",
+ "suggested_area": "Kitchen",
}
],
"deleted_devices": [
@@ -167,7 +192,8 @@ async def test_loading_from_storage(hass, hass_storage):
},
}
- registry = await device_registry.async_get_registry(hass)
+ await device_registry.async_load(hass)
+ registry = device_registry.async_get(hass)
assert len(registry.devices) == 1
assert len(registry.deleted_devices) == 1
@@ -443,7 +469,7 @@ async def test_specifying_via_device_update(registry):
assert light.via_device_id == via.id
-async def test_loading_saving_data(hass, registry):
+async def test_loading_saving_data(hass, registry, area_registry):
"""Test that we load/save data correctly."""
orig_via = registry.async_get_or_create(
config_entry_id="123",
@@ -505,7 +531,18 @@ async def test_loading_saving_data(hass, registry):
assert orig_light4.id == orig_light3.id
- assert len(registry.devices) == 3
+ orig_kitchen_light = registry.async_get_or_create(
+ config_entry_id="999",
+ connections=set(),
+ identifiers={("hue", "999")},
+ manufacturer="manufacturer",
+ model="light",
+ via_device=("hue", "0123"),
+ disabled_by="user",
+ suggested_area="Kitchen",
+ )
+
+ assert len(registry.devices) == 4
assert len(registry.deleted_devices) == 1
orig_via = registry.async_update_device(
@@ -529,6 +566,16 @@ async def test_loading_saving_data(hass, registry):
assert orig_light == new_light
assert orig_light4 == new_light4
+ # Ensure a save/load cycle does not keep suggested area
+ new_kitchen_light = registry2.async_get_device({("hue", "999")})
+ assert orig_kitchen_light.suggested_area == "Kitchen"
+
+ orig_kitchen_light_witout_suggested_area = registry.async_update_device(
+ orig_kitchen_light.id, suggested_area=None
+ )
+ assert orig_kitchen_light_witout_suggested_area.suggested_area is None
+ assert orig_kitchen_light_witout_suggested_area == new_kitchen_light
+
async def test_no_unnecessary_changes(registry):
"""Make sure we do not consider devices changes."""
@@ -687,20 +734,6 @@ async def test_update_remove_config_entries(hass, registry, update_events):
assert update_events[4]["device_id"] == entry3.id
-async def test_loading_race_condition(hass):
- """Test only one storage load called when concurrent loading occurred ."""
- with patch(
- "homeassistant.helpers.device_registry.DeviceRegistry.async_load"
- ) as mock_load:
- results = await asyncio.gather(
- device_registry.async_get_registry(hass),
- device_registry.async_get_registry(hass),
- )
-
- mock_load.assert_called_once_with()
- assert results[0] == results[1]
-
-
async def test_update_sw_version(registry):
"""Verify that we can update software version of a device."""
entry = registry.async_get_or_create(
@@ -719,6 +752,33 @@ async def test_update_sw_version(registry):
assert updated_entry.sw_version == sw_version
+async def test_update_suggested_area(registry, area_registry):
+ """Verify that we can update the suggested area version of a device."""
+ entry = registry.async_get_or_create(
+ config_entry_id="1234",
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ identifiers={("bla", "123")},
+ )
+ assert not entry.suggested_area
+ assert entry.area_id is None
+
+ suggested_area = "Pool"
+
+ with patch.object(registry, "async_schedule_save") as mock_save:
+ updated_entry = registry.async_update_device(
+ entry.id, suggested_area=suggested_area
+ )
+
+ assert mock_save.call_count == 1
+ assert updated_entry != entry
+ assert updated_entry.suggested_area == suggested_area
+
+ pool_area = area_registry.async_get_area_by_name("Pool")
+ assert pool_area is not None
+ assert updated_entry.area_id == pool_area.id
+ assert len(area_registry.areas) == 1
+
+
async def test_cleanup_device_registry(hass, registry):
"""Test cleanup works."""
config_entry = MockConfigEntry(domain="hue")
@@ -737,7 +797,7 @@ async def test_cleanup_device_registry(hass, registry):
identifiers={("something", "d4")}, config_entry_id="non_existing"
)
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = entity_registry.async_get(hass)
ent_reg.async_get_or_create("light", "hue", "e1", device_id=d1.id)
ent_reg.async_get_or_create("light", "hue", "e2", device_id=d1.id)
ent_reg.async_get_or_create("light", "hue", "e3", device_id=d3.id)
@@ -769,7 +829,7 @@ async def test_cleanup_device_registry_removes_expired_orphaned_devices(hass, re
assert len(registry.devices) == 0
assert len(registry.deleted_devices) == 3
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = entity_registry.async_get(hass)
device_registry.async_cleanup(hass, registry, ent_reg)
assert len(registry.devices) == 0
@@ -787,7 +847,6 @@ async def test_cleanup_device_registry_removes_expired_orphaned_devices(hass, re
async def test_cleanup_startup(hass):
"""Test we run a cleanup on startup."""
hass.state = CoreState.not_running
- await device_registry.async_get_registry(hass)
with patch(
"homeassistant.helpers.device_registry.Debouncer.async_call"
@@ -798,10 +857,16 @@ async def test_cleanup_startup(hass):
assert len(mock_call.mock_calls) == 1
+@pytest.mark.parametrize("load_registries", [False])
async def test_cleanup_entity_registry_change(hass):
- """Test we run a cleanup when entity registry changes."""
- await device_registry.async_get_registry(hass)
- ent_reg = await entity_registry.async_get_registry(hass)
+ """Test we run a cleanup when entity registry changes.
+
+ Don't pre-load the registries as the debouncer will then not be waiting for
+ EVENT_ENTITY_REGISTRY_UPDATED events.
+ """
+ await device_registry.async_load(hass)
+ await entity_registry.async_load(hass)
+ ent_reg = entity_registry.async_get(hass)
with patch(
"homeassistant.helpers.device_registry.Debouncer.async_call"
@@ -1111,3 +1176,73 @@ async def test_get_or_create_sets_default_values(hass, registry):
assert entry.name == "default name 1"
assert entry.model == "default model 1"
assert entry.manufacturer == "default manufacturer 1"
+
+
+async def test_verify_suggested_area_does_not_overwrite_area_id(
+ hass, registry, area_registry
+):
+ """Make sure suggested area does not override a set area id."""
+ game_room_area = area_registry.async_create("Game Room")
+
+ original_entry = registry.async_get_or_create(
+ config_entry_id="1234",
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ identifiers={("bridgeid", "0123")},
+ sw_version="sw-version",
+ name="name",
+ manufacturer="manufacturer",
+ model="model",
+ )
+ entry = registry.async_update_device(original_entry.id, area_id=game_room_area.id)
+
+ assert entry.area_id == game_room_area.id
+
+ entry2 = registry.async_get_or_create(
+ config_entry_id="1234",
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ identifiers={("bridgeid", "0123")},
+ sw_version="sw-version",
+ name="name",
+ manufacturer="manufacturer",
+ model="model",
+ suggested_area="New Game Room",
+ )
+ assert entry2.area_id == game_room_area.id
+
+
+async def test_disable_config_entry_disables_devices(hass, registry):
+ """Test that we disable entities tied to a config entry."""
+ config_entry = MockConfigEntry(domain="light")
+ config_entry.add_to_hass(hass)
+
+ entry1 = registry.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={("mac", "12:34:56:AB:CD:EF")},
+ )
+ entry2 = registry.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={("mac", "34:56:AB:CD:EF:12")},
+ disabled_by="user",
+ )
+
+ assert not entry1.disabled
+ assert entry2.disabled
+
+ await hass.config_entries.async_set_disabled_by(config_entry.entry_id, "user")
+ await hass.async_block_till_done()
+
+ entry1 = registry.async_get(entry1.id)
+ assert entry1.disabled
+ assert entry1.disabled_by == "config_entry"
+ entry2 = registry.async_get(entry2.id)
+ assert entry2.disabled
+ assert entry2.disabled_by == "user"
+
+ await hass.config_entries.async_set_disabled_by(config_entry.entry_id, None)
+ await hass.async_block_till_done()
+
+ entry1 = registry.async_get(entry1.id)
+ assert not entry1.disabled
+ entry2 = registry.async_get(entry2.id)
+ assert entry2.disabled
+ assert entry2.disabled_by == "user"
diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py
index 64f39fb13bd28e..d0d8580d69de2d 100644
--- a/tests/helpers/test_discovery.py
+++ b/tests/helpers/test_discovery.py
@@ -4,6 +4,8 @@
from homeassistant import setup
from homeassistant.core import callback
from homeassistant.helpers import discovery
+from homeassistant.helpers.dispatcher import dispatcher_send
+from homeassistant.util.async_ import run_callback_threadsafe
from tests.common import (
MockModule,
@@ -31,23 +33,22 @@ def test_listen(self, mock_setup_component):
"""Test discovery listen/discover combo."""
helpers = self.hass.helpers
calls_single = []
- calls_multi = []
@callback
def callback_single(service, info):
"""Service discovered callback."""
calls_single.append((service, info))
- @callback
- def callback_multi(service, info):
- """Service discovered callback."""
- calls_multi.append((service, info))
-
- helpers.discovery.listen("test service", callback_single)
- helpers.discovery.listen(["test service", "another service"], callback_multi)
+ self.hass.add_job(
+ helpers.discovery.async_listen, "test service", callback_single
+ )
- helpers.discovery.discover(
- "test service", "discovery info", "test_component", {}
+ self.hass.add_job(
+ helpers.discovery.async_discover,
+ "test service",
+ "discovery info",
+ "test_component",
+ {},
)
self.hass.block_till_done()
@@ -56,15 +57,6 @@ def callback_multi(service, info):
assert len(calls_single) == 1
assert calls_single[0] == ("test service", "discovery info")
- helpers.discovery.discover(
- "another service", "discovery info", "test_component", {}
- )
- self.hass.block_till_done()
-
- assert len(calls_single) == 1
- assert len(calls_multi) == 2
- assert ["test service", "another service"] == [info[0] for info in calls_multi]
-
@patch("homeassistant.setup.async_setup_component", return_value=mock_coro(True))
def test_platform(self, mock_setup_component):
"""Test discover platform method."""
@@ -75,7 +67,13 @@ def platform_callback(platform, info):
"""Platform callback method."""
calls.append((platform, info))
- discovery.listen_platform(self.hass, "test_component", platform_callback)
+ run_callback_threadsafe(
+ self.hass.loop,
+ discovery.async_listen_platform,
+ self.hass,
+ "test_component",
+ platform_callback,
+ ).result()
discovery.load_platform(
self.hass,
@@ -105,13 +103,10 @@ def platform_callback(platform, info):
assert len(calls) == 1
assert calls[0] == ("test_platform", "discovery info")
- self.hass.bus.fire(
- discovery.EVENT_PLATFORM_DISCOVERED,
- {
- discovery.ATTR_SERVICE: discovery.EVENT_LOAD_PLATFORM.format(
- "test_component"
- )
- },
+ dispatcher_send(
+ self.hass,
+ discovery.SIGNAL_PLATFORM_DISCOVERED,
+ {"service": discovery.EVENT_LOAD_PLATFORM.format("test_component")},
)
self.hass.block_till_done()
@@ -179,10 +174,12 @@ def test_1st_discovers_2nd_component(self, mock_signal):
"""
component_calls = []
- def component1_setup(hass, config):
+ async def component1_setup(hass, config):
"""Set up mock component."""
print("component1 setup")
- discovery.discover(hass, "test_component2", {}, "test_component2", {})
+ await discovery.async_discover(
+ hass, "test_component2", {}, "test_component2", {}
+ )
return True
def component2_setup(hass, config):
@@ -191,7 +188,7 @@ def component2_setup(hass, config):
return True
mock_integration(
- self.hass, MockModule("test_component1", setup=component1_setup)
+ self.hass, MockModule("test_component1", async_setup=component1_setup)
)
mock_integration(
diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py
index 52149b060e4758..b8d0fc7dc9ce7f 100644
--- a/tests/helpers/test_entity.py
+++ b/tests/helpers/test_entity.py
@@ -7,7 +7,7 @@
import pytest
-from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE
+from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import Context
from homeassistant.helpers import entity, entity_registry
@@ -718,3 +718,29 @@ async def test_setup_source(hass):
await platform.async_reset()
assert entity.entity_sources(hass) == {}
+
+
+async def test_removing_entity_unavailable(hass):
+ """Test removing an entity that is still registered creates an unavailable state."""
+ entry = entity_registry.RegistryEntry(
+ entity_id="hello.world",
+ unique_id="test-unique-id",
+ platform="test-platform",
+ disabled_by=None,
+ )
+
+ ent = entity.Entity()
+ ent.hass = hass
+ ent.entity_id = "hello.world"
+ ent.registry_entry = entry
+ ent.async_write_ha_state()
+
+ state = hass.states.get("hello.world")
+ assert state is not None
+ assert state.state == STATE_UNKNOWN
+
+ await ent.async_remove()
+
+ state = hass.states.get("hello.world")
+ assert state is not None
+ assert state.state == STATE_UNAVAILABLE
diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py
index 0a939ba2825089..e842d5aa1ae6b7 100644
--- a/tests/helpers/test_entity_platform.py
+++ b/tests/helpers/test_entity_platform.py
@@ -6,10 +6,14 @@
import pytest
-from homeassistant.const import PERCENTAGE
-from homeassistant.core import callback
+from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, PERCENTAGE
+from homeassistant.core import CoreState, callback
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
-from homeassistant.helpers import entity_platform, entity_registry
+from homeassistant.helpers import (
+ device_registry as dr,
+ entity_platform,
+ entity_registry as er,
+)
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.entity_component import (
DEFAULT_SCAN_INTERVAL,
@@ -467,7 +471,7 @@ async def test_overriding_name_from_registry(hass):
mock_registry(
hass,
{
- "test_domain.world": entity_registry.RegistryEntry(
+ "test_domain.world": er.RegistryEntry(
entity_id="test_domain.world",
unique_id="1234",
# Using component.async_add_entities is equal to platform "domain"
@@ -499,12 +503,12 @@ async def test_registry_respect_entity_disabled(hass):
mock_registry(
hass,
{
- "test_domain.world": entity_registry.RegistryEntry(
+ "test_domain.world": er.RegistryEntry(
entity_id="test_domain.world",
unique_id="1234",
# Using component.async_add_entities is equal to platform "domain"
platform="test_platform",
- disabled_by=entity_registry.DISABLED_USER,
+ disabled_by=er.DISABLED_USER,
)
},
)
@@ -520,7 +524,7 @@ async def test_entity_registry_updates_name(hass):
registry = mock_registry(
hass,
{
- "test_domain.world": entity_registry.RegistryEntry(
+ "test_domain.world": er.RegistryEntry(
entity_id="test_domain.world",
unique_id="1234",
# Using component.async_add_entities is equal to platform "domain"
@@ -588,6 +592,52 @@ async def test_setup_entry_platform_not_ready(hass, caplog):
assert len(mock_call_later.mock_calls) == 1
+async def test_setup_entry_platform_not_ready_with_message(hass, caplog):
+ """Test when an entry is not ready yet that includes a message."""
+ async_setup_entry = Mock(side_effect=PlatformNotReady("lp0 on fire"))
+ platform = MockPlatform(async_setup_entry=async_setup_entry)
+ config_entry = MockConfigEntry()
+ ent_platform = MockEntityPlatform(
+ hass, platform_name=config_entry.domain, platform=platform
+ )
+
+ with patch.object(entity_platform, "async_call_later") as mock_call_later:
+ assert not await ent_platform.async_setup_entry(config_entry)
+
+ full_name = f"{ent_platform.domain}.{config_entry.domain}"
+ assert full_name not in hass.config.components
+ assert len(async_setup_entry.mock_calls) == 1
+
+ assert "Platform test not ready yet" in caplog.text
+ assert "lp0 on fire" in caplog.text
+ assert len(mock_call_later.mock_calls) == 1
+
+
+async def test_setup_entry_platform_not_ready_from_exception(hass, caplog):
+ """Test when an entry is not ready yet that includes the causing exception string."""
+ original_exception = HomeAssistantError("The device dropped the connection")
+ platform_exception = PlatformNotReady()
+ platform_exception.__cause__ = original_exception
+
+ async_setup_entry = Mock(side_effect=platform_exception)
+ platform = MockPlatform(async_setup_entry=async_setup_entry)
+ config_entry = MockConfigEntry()
+ ent_platform = MockEntityPlatform(
+ hass, platform_name=config_entry.domain, platform=platform
+ )
+
+ with patch.object(entity_platform, "async_call_later") as mock_call_later:
+ assert not await ent_platform.async_setup_entry(config_entry)
+
+ full_name = f"{ent_platform.domain}.{config_entry.domain}"
+ assert full_name not in hass.config.components
+ assert len(async_setup_entry.mock_calls) == 1
+
+ assert "Platform test not ready yet" in caplog.text
+ assert "The device dropped the connection" in caplog.text
+ assert len(mock_call_later.mock_calls) == 1
+
+
async def test_reset_cancels_retry_setup(hass):
"""Test that resetting a platform will cancel scheduled a setup retry."""
async_setup_entry = Mock(side_effect=PlatformNotReady)
@@ -610,6 +660,31 @@ async def test_reset_cancels_retry_setup(hass):
assert ent_platform._async_cancel_retry_setup is None
+async def test_reset_cancels_retry_setup_when_not_started(hass):
+ """Test that resetting a platform will cancel scheduled a setup retry when not yet started."""
+ hass.state = CoreState.starting
+ async_setup_entry = Mock(side_effect=PlatformNotReady)
+ initial_listeners = hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED]
+
+ platform = MockPlatform(async_setup_entry=async_setup_entry)
+ config_entry = MockConfigEntry()
+ ent_platform = MockEntityPlatform(
+ hass, platform_name=config_entry.domain, platform=platform
+ )
+
+ assert not await ent_platform.async_setup_entry(config_entry)
+ await hass.async_block_till_done()
+ assert (
+ hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] == initial_listeners + 1
+ )
+ assert ent_platform._async_cancel_retry_setup is not None
+
+ await ent_platform.async_reset()
+ await hass.async_block_till_done()
+ assert hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] == initial_listeners
+ assert ent_platform._async_cancel_retry_setup is None
+
+
async def test_not_fails_with_adding_empty_entities_(hass):
"""Test for not fails on empty entities list."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
@@ -624,7 +699,7 @@ async def test_entity_registry_updates_entity_id(hass):
registry = mock_registry(
hass,
{
- "test_domain.world": entity_registry.RegistryEntry(
+ "test_domain.world": er.RegistryEntry(
entity_id="test_domain.world",
unique_id="1234",
# Using component.async_add_entities is equal to platform "domain"
@@ -656,14 +731,14 @@ async def test_entity_registry_updates_invalid_entity_id(hass):
registry = mock_registry(
hass,
{
- "test_domain.world": entity_registry.RegistryEntry(
+ "test_domain.world": er.RegistryEntry(
entity_id="test_domain.world",
unique_id="1234",
# Using component.async_add_entities is equal to platform "domain"
platform="test_platform",
name="Some name",
),
- "test_domain.existing": entity_registry.RegistryEntry(
+ "test_domain.existing": er.RegistryEntry(
entity_id="test_domain.existing",
unique_id="5678",
platform="test_platform",
@@ -703,7 +778,7 @@ async def test_entity_registry_updates_invalid_entity_id(hass):
async def test_device_info_called(hass):
"""Test device info is forwarded correctly."""
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
via = registry.async_get_or_create(
config_entry_id="123",
connections=set(),
@@ -728,6 +803,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"model": "test-model",
"name": "test-name",
"sw_version": "test-sw",
+ "suggested_area": "Heliport",
"entry_type": "service",
"via_device": ("hue", "via-id"),
},
@@ -755,13 +831,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
assert device.model == "test-model"
assert device.name == "test-name"
assert device.sw_version == "test-sw"
+ assert device.suggested_area == "Heliport"
assert device.entry_type == "service"
assert device.via_device_id == via.id
async def test_device_info_not_overrides(hass):
"""Test device info is forwarded correctly."""
- registry = await hass.helpers.device_registry.async_get_registry()
+ registry = dr.async_get(hass)
device = registry.async_get_or_create(
config_entry_id="bla",
connections={("mac", "abcd")},
@@ -821,7 +898,7 @@ async def test_entity_disabled_by_integration(hass):
assert entity_disabled.hass is None
assert entity_disabled.platform is None
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry_default = registry.async_get_or_create(DOMAIN, DOMAIN, "default")
assert entry_default.disabled_by is None
@@ -843,7 +920,7 @@ async def test_entity_info_added_to_entity_registry(hass):
await component.async_add_entities([entity_default])
- registry = await hass.helpers.entity_registry.async_get_registry()
+ registry = er.async_get(hass)
entry_default = registry.async_get_or_create(DOMAIN, DOMAIN, "default")
print(entry_default)
diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py
index 21f4392122e592..d671aacebb3d98 100644
--- a/tests/helpers/test_entity_registry.py
+++ b/tests/helpers/test_entity_registry.py
@@ -1,13 +1,11 @@
"""Tests for the Entity Registry."""
-import asyncio
-import unittest.mock
from unittest.mock import patch
import pytest
from homeassistant.const import EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE
from homeassistant.core import CoreState, callback, valid_entity_id
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry as er
from tests.common import (
MockConfigEntry,
@@ -34,7 +32,7 @@ def update_events(hass):
def async_capture(event):
events.append(event.data)
- hass.bus.async_listen(entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, async_capture)
+ hass.bus.async_listen(er.EVENT_ENTITY_REGISTRY_UPDATED, async_capture)
return events
@@ -76,7 +74,7 @@ def test_get_or_create_updates_data(registry):
capabilities={"max": 100},
supported_features=5,
device_class="mock-device-class",
- disabled_by=entity_registry.DISABLED_HASS,
+ disabled_by=er.DISABLED_HASS,
unit_of_measurement="initial-unit_of_measurement",
original_name="initial-original_name",
original_icon="initial-original_icon",
@@ -87,7 +85,7 @@ def test_get_or_create_updates_data(registry):
assert orig_entry.capabilities == {"max": 100}
assert orig_entry.supported_features == 5
assert orig_entry.device_class == "mock-device-class"
- assert orig_entry.disabled_by == entity_registry.DISABLED_HASS
+ assert orig_entry.disabled_by == er.DISABLED_HASS
assert orig_entry.unit_of_measurement == "initial-unit_of_measurement"
assert orig_entry.original_name == "initial-original_name"
assert orig_entry.original_icon == "initial-original_icon"
@@ -103,7 +101,7 @@ def test_get_or_create_updates_data(registry):
capabilities={"new-max": 100},
supported_features=10,
device_class="new-mock-device-class",
- disabled_by=entity_registry.DISABLED_USER,
+ disabled_by=er.DISABLED_USER,
unit_of_measurement="updated-unit_of_measurement",
original_name="updated-original_name",
original_icon="updated-original_icon",
@@ -118,7 +116,7 @@ def test_get_or_create_updates_data(registry):
assert new_entry.original_name == "updated-original_name"
assert new_entry.original_icon == "updated-original_icon"
# Should not be updated
- assert new_entry.disabled_by == entity_registry.DISABLED_HASS
+ assert new_entry.disabled_by == er.DISABLED_HASS
def test_get_or_create_suggested_object_id_conflict_register(registry):
@@ -164,7 +162,7 @@ async def test_loading_saving_data(hass, registry):
capabilities={"max": 100},
supported_features=5,
device_class="mock-device-class",
- disabled_by=entity_registry.DISABLED_HASS,
+ disabled_by=er.DISABLED_HASS,
original_name="Original Name",
original_icon="hass:original-icon",
)
@@ -175,7 +173,7 @@ async def test_loading_saving_data(hass, registry):
assert len(registry.entities) == 2
# Now load written data in new registry
- registry2 = entity_registry.EntityRegistry(hass)
+ registry2 = er.EntityRegistry(hass)
await flush_store(registry._store)
await registry2.async_load()
@@ -189,7 +187,7 @@ async def test_loading_saving_data(hass, registry):
assert new_entry2.device_id == "mock-dev-id"
assert new_entry2.area_id == "mock-area-id"
- assert new_entry2.disabled_by == entity_registry.DISABLED_HASS
+ assert new_entry2.disabled_by == er.DISABLED_HASS
assert new_entry2.capabilities == {"max": 100}
assert new_entry2.supported_features == 5
assert new_entry2.device_class == "mock-device-class"
@@ -219,10 +217,11 @@ def test_is_registered(registry):
assert not registry.async_is_registered("light.non_existing")
+@pytest.mark.parametrize("load_registries", [False])
async def test_loading_extra_values(hass, hass_storage):
"""Test we load extra data from the registry."""
- hass_storage[entity_registry.STORAGE_KEY] = {
- "version": entity_registry.STORAGE_VERSION,
+ hass_storage[er.STORAGE_KEY] = {
+ "version": er.STORAGE_VERSION,
"data": {
"entities": [
{
@@ -258,7 +257,8 @@ async def test_loading_extra_values(hass, hass_storage):
},
}
- registry = await entity_registry.async_get_registry(hass)
+ await er.async_load(hass)
+ registry = er.async_get(hass)
assert len(registry.entities) == 4
@@ -279,9 +279,9 @@ async def test_loading_extra_values(hass, hass_storage):
"test", "super_platform", "disabled-user"
)
assert entry_disabled_hass.disabled
- assert entry_disabled_hass.disabled_by == entity_registry.DISABLED_HASS
+ assert entry_disabled_hass.disabled_by == er.DISABLED_HASS
assert entry_disabled_user.disabled
- assert entry_disabled_user.disabled_by == entity_registry.DISABLED_USER
+ assert entry_disabled_user.disabled_by == er.DISABLED_USER
def test_async_get_entity_id(registry):
@@ -313,7 +313,7 @@ async def test_updating_config_entry_id(hass, registry, update_events):
assert update_events[0]["entity_id"] == entry.entity_id
assert update_events[1]["action"] == "update"
assert update_events[1]["entity_id"] == entry.entity_id
- assert update_events[1]["changes"] == ["config_entry_id"]
+ assert update_events[1]["changes"] == {"config_entry_id": "mock-id-1"}
async def test_removing_config_entry_id(hass, registry, update_events):
@@ -350,6 +350,7 @@ async def test_removing_area_id(registry):
assert entry_w_area != entry_wo_area
+@pytest.mark.parametrize("load_registries", [False])
async def test_migration(hass):
"""Test migration from old data to new."""
mock_config = MockConfigEntry(domain="test-platform", entry_id="test-config-id")
@@ -366,7 +367,8 @@ async def test_migration(hass):
with patch("os.path.isfile", return_value=True), patch("os.remove"), patch(
"homeassistant.helpers.entity_registry.load_yaml", return_value=old_conf
):
- registry = await entity_registry.async_get_registry(hass)
+ await er.async_load(hass)
+ registry = er.async_get(hass)
assert registry.async_is_registered("light.kitchen")
entry = registry.async_get_or_create(
@@ -382,8 +384,8 @@ async def test_migration(hass):
async def test_loading_invalid_entity_id(hass, hass_storage):
"""Test we autofix invalid entity IDs."""
- hass_storage[entity_registry.STORAGE_KEY] = {
- "version": entity_registry.STORAGE_VERSION,
+ hass_storage[er.STORAGE_KEY] = {
+ "version": er.STORAGE_VERSION,
"data": {
"entities": [
{
@@ -406,7 +408,7 @@ async def test_loading_invalid_entity_id(hass, hass_storage):
},
}
- registry = await entity_registry.async_get_registry(hass)
+ registry = er.async_get(hass)
entity_invalid_middle = registry.async_get_or_create(
"test", "super_platform", "id-invalid-middle"
@@ -427,20 +429,6 @@ async def test_loading_invalid_entity_id(hass, hass_storage):
assert valid_entity_id(entity_invalid_start.entity_id)
-async def test_loading_race_condition(hass):
- """Test only one storage load called when concurrent loading occurred ."""
- with unittest.mock.patch(
- "homeassistant.helpers.entity_registry.EntityRegistry.async_load"
- ) as mock_load:
- results = await asyncio.gather(
- entity_registry.async_get_registry(hass),
- entity_registry.async_get_registry(hass),
- )
-
- mock_load.assert_called_once_with()
- assert results[0] == results[1]
-
-
async def test_update_entity_unique_id(registry):
"""Test entity's unique_id is updated."""
mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1")
@@ -491,7 +479,7 @@ async def test_update_entity(registry):
for attr_name, new_value in (
("name", "new name"),
("icon", "new icon"),
- ("disabled_by", entity_registry.DISABLED_USER),
+ ("disabled_by", er.DISABLED_USER),
):
changes = {attr_name: new_value}
updated_entry = registry.async_update_entity(entry.entity_id, **changes)
@@ -543,7 +531,7 @@ async def test_restore_states(hass):
"""Test restoring states."""
hass.state = CoreState.not_running
- registry = await entity_registry.async_get_registry(hass)
+ registry = er.async_get(hass)
registry.async_get_or_create(
"light",
@@ -557,7 +545,7 @@ async def test_restore_states(hass):
"hue",
"5678",
suggested_object_id="disabled",
- disabled_by=entity_registry.DISABLED_HASS,
+ disabled_by=er.DISABLED_HASS,
)
registry.async_get_or_create(
"light",
@@ -609,7 +597,7 @@ async def test_async_get_device_class_lookup(hass):
"""Test registry device class lookup."""
hass.state = CoreState.not_running
- ent_reg = await entity_registry.async_get_registry(hass)
+ ent_reg = er.async_get(hass)
ent_reg.async_get_or_create(
"binary_sensor",
@@ -710,6 +698,39 @@ async def test_remove_device_removes_entities(hass, registry):
assert not registry.async_is_registered(entry.entity_id)
+async def test_update_device_race(hass, registry):
+ """Test race when a device is created, updated and removed."""
+ device_registry = mock_device_registry(hass)
+ config_entry = MockConfigEntry(domain="light")
+
+ # Create device
+ device_entry = device_registry.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={("mac", "12:34:56:AB:CD:EF")},
+ )
+ # Update it
+ device_registry.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ identifiers={("bridgeid", "0123")},
+ connections={("mac", "12:34:56:AB:CD:EF")},
+ )
+ # Add entity to the device
+ entry = registry.async_get_or_create(
+ "light",
+ "hue",
+ "5678",
+ config_entry=config_entry,
+ device_id=device_entry.id,
+ )
+
+ assert registry.async_is_registered(entry.entity_id)
+
+ device_registry.async_remove_device(device_entry.id)
+ await hass.async_block_till_done()
+
+ assert not registry.async_is_registered(entry.entity_id)
+
+
async def test_disable_device_disables_entities(hass, registry):
"""Test that we disable entities tied to a device."""
device_registry = mock_device_registry(hass)
@@ -736,9 +757,18 @@ async def test_disable_device_disables_entities(hass, registry):
device_id=device_entry.id,
disabled_by="user",
)
+ entry3 = registry.async_get_or_create(
+ "light",
+ "hue",
+ "EFGH",
+ config_entry=config_entry,
+ device_id=device_entry.id,
+ disabled_by="config_entry",
+ )
assert not entry1.disabled
assert entry2.disabled
+ assert entry3.disabled
device_registry.async_update_device(device_entry.id, disabled_by="user")
await hass.async_block_till_done()
@@ -749,6 +779,9 @@ async def test_disable_device_disables_entities(hass, registry):
entry2 = registry.async_get(entry2.entity_id)
assert entry2.disabled
assert entry2.disabled_by == "user"
+ entry3 = registry.async_get(entry3.entity_id)
+ assert entry3.disabled
+ assert entry3.disabled_by == "config_entry"
device_registry.async_update_device(device_entry.id, disabled_by=None)
await hass.async_block_till_done()
@@ -758,10 +791,78 @@ async def test_disable_device_disables_entities(hass, registry):
entry2 = registry.async_get(entry2.entity_id)
assert entry2.disabled
assert entry2.disabled_by == "user"
+ entry3 = registry.async_get(entry3.entity_id)
+ assert entry3.disabled
+ assert entry3.disabled_by == "config_entry"
+
+
+async def test_disable_config_entry_disables_entities(hass, registry):
+ """Test that we disable entities tied to a config entry."""
+ device_registry = mock_device_registry(hass)
+ config_entry = MockConfigEntry(domain="light")
+ config_entry.add_to_hass(hass)
+
+ device_entry = device_registry.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={("mac", "12:34:56:AB:CD:EF")},
+ )
+
+ entry1 = registry.async_get_or_create(
+ "light",
+ "hue",
+ "5678",
+ config_entry=config_entry,
+ device_id=device_entry.id,
+ )
+ entry2 = registry.async_get_or_create(
+ "light",
+ "hue",
+ "ABCD",
+ config_entry=config_entry,
+ device_id=device_entry.id,
+ disabled_by="user",
+ )
+ entry3 = registry.async_get_or_create(
+ "light",
+ "hue",
+ "EFGH",
+ config_entry=config_entry,
+ device_id=device_entry.id,
+ disabled_by="device",
+ )
+
+ assert not entry1.disabled
+ assert entry2.disabled
+ assert entry3.disabled
+
+ await hass.config_entries.async_set_disabled_by(config_entry.entry_id, "user")
+ await hass.async_block_till_done()
+
+ entry1 = registry.async_get(entry1.entity_id)
+ assert entry1.disabled
+ assert entry1.disabled_by == "config_entry"
+ entry2 = registry.async_get(entry2.entity_id)
+ assert entry2.disabled
+ assert entry2.disabled_by == "user"
+ entry3 = registry.async_get(entry3.entity_id)
+ assert entry3.disabled
+ assert entry3.disabled_by == "device"
+
+ await hass.config_entries.async_set_disabled_by(config_entry.entry_id, None)
+ await hass.async_block_till_done()
+
+ entry1 = registry.async_get(entry1.entity_id)
+ assert not entry1.disabled
+ entry2 = registry.async_get(entry2.entity_id)
+ assert entry2.disabled
+ assert entry2.disabled_by == "user"
+ # The device was re-enabled, so entity disabled by the device will be re-enabled too
+ entry3 = registry.async_get(entry3.entity_id)
+ assert not entry3.disabled_by
async def test_disabled_entities_excluded_from_entity_list(hass, registry):
- """Test that disabled entities are exclduded from async_entries_for_device."""
+ """Test that disabled entities are excluded from async_entries_for_device."""
device_registry = mock_device_registry(hass)
config_entry = MockConfigEntry(domain="light")
@@ -787,10 +888,10 @@ async def test_disabled_entities_excluded_from_entity_list(hass, registry):
disabled_by="user",
)
- entries = entity_registry.async_entries_for_device(registry, device_entry.id)
+ entries = er.async_entries_for_device(registry, device_entry.id)
assert entries == [entry1]
- entries = entity_registry.async_entries_for_device(
+ entries = er.async_entries_for_device(
registry, device_entry.id, include_disabled_entities=True
)
assert entries == [entry1, entry2]
diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py
index b0f58f76a661ae..b8291b97efa888 100644
--- a/tests/helpers/test_event.py
+++ b/tests/helpers/test_event.py
@@ -4,7 +4,8 @@
from datetime import datetime, timedelta
from unittest.mock import patch
-from astral import Astral
+from astral import LocationInfo
+import astral.sun
import jinja2
import pytest
@@ -2433,15 +2434,18 @@ async def test_track_sunrise(hass, legacy_patchable_time):
hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}
)
+ location = LocationInfo(
+ latitude=hass.config.latitude, longitude=hass.config.longitude
+ )
+
# Get next sunrise/sunset
- astral = Astral()
utc_now = datetime(2014, 5, 24, 12, 0, 0, tzinfo=dt_util.UTC)
utc_today = utc_now.date()
mod = -1
while True:
- next_rising = astral.sunrise_utc(
- utc_today + timedelta(days=mod), latitude, longitude
+ next_rising = astral.sun.sunrise(
+ location.observer, date=utc_today + timedelta(days=mod)
)
if next_rising > utc_now:
break
@@ -2493,15 +2497,18 @@ async def test_track_sunrise_update_location(hass, legacy_patchable_time):
hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}
)
+ location = LocationInfo(
+ latitude=hass.config.latitude, longitude=hass.config.longitude
+ )
+
# Get next sunrise
- astral = Astral()
utc_now = datetime(2014, 5, 24, 12, 0, 0, tzinfo=dt_util.UTC)
utc_today = utc_now.date()
mod = -1
while True:
- next_rising = astral.sunrise_utc(
- utc_today + timedelta(days=mod), hass.config.latitude, hass.config.longitude
+ next_rising = astral.sun.sunrise(
+ location.observer, date=utc_today + timedelta(days=mod)
)
if next_rising > utc_now:
break
@@ -2522,6 +2529,11 @@ async def test_track_sunrise_update_location(hass, legacy_patchable_time):
await hass.config.async_update(latitude=40.755931, longitude=-73.984606)
await hass.async_block_till_done()
+ # update location for astral
+ location = LocationInfo(
+ latitude=hass.config.latitude, longitude=hass.config.longitude
+ )
+
# Mimic sunrise
async_fire_time_changed(hass, next_rising)
await hass.async_block_till_done()
@@ -2531,8 +2543,8 @@ async def test_track_sunrise_update_location(hass, legacy_patchable_time):
# Get next sunrise
mod = -1
while True:
- next_rising = astral.sunrise_utc(
- utc_today + timedelta(days=mod), hass.config.latitude, hass.config.longitude
+ next_rising = astral.sun.sunrise(
+ location.observer, date=utc_today + timedelta(days=mod)
)
if next_rising > utc_now:
break
@@ -2549,6 +2561,8 @@ async def test_track_sunset(hass, legacy_patchable_time):
latitude = 32.87336
longitude = 117.22743
+ location = LocationInfo(latitude=latitude, longitude=longitude)
+
# Setup sun component
hass.config.latitude = latitude
hass.config.longitude = longitude
@@ -2557,14 +2571,13 @@ async def test_track_sunset(hass, legacy_patchable_time):
)
# Get next sunrise/sunset
- astral = Astral()
utc_now = datetime(2014, 5, 24, 12, 0, 0, tzinfo=dt_util.UTC)
utc_today = utc_now.date()
mod = -1
while True:
- next_setting = astral.sunset_utc(
- utc_today + timedelta(days=mod), latitude, longitude
+ next_setting = astral.sun.sunset(
+ location.observer, date=utc_today + timedelta(days=mod)
)
if next_setting > utc_now:
break
diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py
index 7fc46b3699d8ab..b198a16adb1958 100644
--- a/tests/helpers/test_frame.py
+++ b/tests/helpers/test_frame.py
@@ -6,34 +6,13 @@
from homeassistant.helpers import frame
-async def test_extract_frame_integration(caplog):
+async def test_extract_frame_integration(caplog, mock_integration_frame):
"""Test extracting the current frame from integration context."""
- correct_frame = Mock(
- filename="/home/paulus/homeassistant/components/hue/light.py",
- lineno="23",
- line="self.light.is_on",
- )
- with patch(
- "homeassistant.helpers.frame.extract_stack",
- return_value=[
- Mock(
- filename="/home/paulus/homeassistant/core.py",
- lineno="23",
- line="do_something()",
- ),
- correct_frame,
- Mock(
- filename="/home/paulus/aiohue/lights.py",
- lineno="2",
- line="something()",
- ),
- ],
- ):
- found_frame, integration, path = frame.get_integration_frame()
+ found_frame, integration, path = frame.get_integration_frame()
assert integration == "hue"
assert path == "homeassistant/components/"
- assert found_frame == correct_frame
+ assert found_frame == mock_integration_frame
async def test_extract_frame_integration_with_excluded_intergration(caplog):
diff --git a/tests/helpers/test_httpx_client.py b/tests/helpers/test_httpx_client.py
index 53a6985f5cc779..a47463b6b98945 100644
--- a/tests/helpers/test_httpx_client.py
+++ b/tests/helpers/test_httpx_client.py
@@ -80,6 +80,19 @@ async def test_get_async_client_patched_close(hass):
assert mock_aclose.call_count == 0
+async def test_get_async_client_context_manager(hass):
+ """Test using the async client with a context manager does not close the session."""
+
+ with patch("httpx.AsyncClient.aclose") as mock_aclose:
+ httpx_session = client.get_async_client(hass)
+ assert isinstance(hass.data[client.DATA_ASYNC_CLIENT], httpx.AsyncClient)
+
+ async with httpx_session:
+ pass
+
+ assert mock_aclose.call_count == 0
+
+
async def test_warning_close_session_integration(hass, caplog):
"""Test log warning message when closing the session from integration context."""
with patch(
diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py
index 8ae516b2bbe6b3..1a68f2b8da59d7 100644
--- a/tests/helpers/test_json.py
+++ b/tests/helpers/test_json.py
@@ -11,11 +11,17 @@ def test_json_encoder(hass):
ha_json_enc = JSONEncoder()
state = core.State("test.test", "hello")
+ # Test serializing a datetime
+ now = dt_util.utcnow()
+ assert ha_json_enc.default(now) == now.isoformat()
+
+ # Test serializing a set()
+ data = {"milk", "beer"}
+ assert sorted(ha_json_enc.default(data)) == sorted(data)
+
+ # Test serializing an object which implements as_dict
assert ha_json_enc.default(state) == state.as_dict()
# Default method raises TypeError if non HA object
with pytest.raises(TypeError):
ha_json_enc.default(1)
-
- now = dt_util.utcnow()
- assert ha_json_enc.default(now) == now.isoformat()
diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py
index 06158558d5e0c3..05c72f10db568f 100644
--- a/tests/helpers/test_network.py
+++ b/tests/helpers/test_network.py
@@ -9,7 +9,6 @@
from homeassistant.helpers.network import (
NoURLAvailableError,
_get_cloud_url,
- _get_deprecated_base_url,
_get_external_url,
_get_internal_url,
_get_request_host,
@@ -166,9 +165,7 @@ async def test_get_url_internal_fallback(hass: HomeAssistant):
"""Test getting an instance URL when the user has not set an internal URL."""
assert hass.config.internal_url is None
- hass.config.api = Mock(
- use_ssl=False, port=8123, deprecated_base_url=None, local_ip="192.168.123.123"
- )
+ hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123")
assert _get_internal_url(hass) == "http://192.168.123.123:8123"
with pytest.raises(NoURLAvailableError):
@@ -180,9 +177,7 @@ async def test_get_url_internal_fallback(hass: HomeAssistant):
with pytest.raises(NoURLAvailableError):
_get_internal_url(hass, require_ssl=True)
- hass.config.api = Mock(
- use_ssl=False, port=80, deprecated_base_url=None, local_ip="192.168.123.123"
- )
+ hass.config.api = Mock(use_ssl=False, port=80, local_ip="192.168.123.123")
assert _get_internal_url(hass) == "http://192.168.123.123"
assert (
_get_internal_url(hass, require_standard_port=True) == "http://192.168.123.123"
@@ -194,7 +189,7 @@ async def test_get_url_internal_fallback(hass: HomeAssistant):
with pytest.raises(NoURLAvailableError):
_get_internal_url(hass, require_ssl=True)
- hass.config.api = Mock(use_ssl=True, port=443, deprecated_base_url=None)
+ hass.config.api = Mock(use_ssl=True, port=443)
with pytest.raises(NoURLAvailableError):
_get_internal_url(hass)
@@ -208,9 +203,7 @@ async def test_get_url_internal_fallback(hass: HomeAssistant):
_get_internal_url(hass, require_ssl=True)
# Do no accept any local loopback address as fallback
- hass.config.api = Mock(
- use_ssl=False, port=80, deprecated_base_url=None, local_ip="127.0.0.1"
- )
+ hass.config.api = Mock(use_ssl=False, port=80, local_ip="127.0.0.1")
with pytest.raises(NoURLAvailableError):
_get_internal_url(hass)
@@ -384,9 +377,8 @@ async def test_get_cloud_url(hass: HomeAssistant):
hass.components.cloud,
"async_remote_ui_url",
side_effect=cloud.CloudNotAvailable,
- ):
- with pytest.raises(NoURLAvailableError):
- _get_cloud_url(hass)
+ ), pytest.raises(NoURLAvailableError):
+ _get_cloud_url(hass)
async def test_get_external_url_cloud_fallback(hass: HomeAssistant):
@@ -457,9 +449,7 @@ async def test_get_url(hass: HomeAssistant):
with pytest.raises(NoURLAvailableError):
get_url(hass)
- hass.config.api = Mock(
- use_ssl=False, port=8123, deprecated_base_url=None, local_ip="192.168.123.123"
- )
+ hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123")
assert get_url(hass) == "http://192.168.123.123:8123"
assert get_url(hass, prefer_external=True) == "http://192.168.123.123:8123"
@@ -543,274 +533,11 @@ async def test_get_request_host(hass: HomeAssistant):
assert _get_request_host() == "example.com"
-async def test_get_deprecated_base_url_internal(hass: HomeAssistant):
- """Test getting an internal instance URL from the deprecated base_url."""
- # Test with SSL local URL
- hass.config.api = Mock(deprecated_base_url="https://example.local")
- assert _get_deprecated_base_url(hass, internal=True) == "https://example.local"
- assert (
- _get_deprecated_base_url(hass, internal=True, allow_ip=False)
- == "https://example.local"
- )
- assert (
- _get_deprecated_base_url(hass, internal=True, require_ssl=True)
- == "https://example.local"
- )
- assert (
- _get_deprecated_base_url(hass, internal=True, require_standard_port=True)
- == "https://example.local"
- )
-
- # Test with no SSL, local IP URL
- hass.config.api = Mock(deprecated_base_url="http://10.10.10.10:8123")
- assert _get_deprecated_base_url(hass, internal=True) == "http://10.10.10.10:8123"
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, internal=True, allow_ip=False)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, internal=True, require_ssl=True)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, internal=True, require_standard_port=True)
-
- # Test with SSL, local IP URL
- hass.config.api = Mock(deprecated_base_url="https://10.10.10.10")
- assert _get_deprecated_base_url(hass, internal=True) == "https://10.10.10.10"
- assert (
- _get_deprecated_base_url(hass, internal=True, require_ssl=True)
- == "https://10.10.10.10"
- )
- assert (
- _get_deprecated_base_url(hass, internal=True, require_standard_port=True)
- == "https://10.10.10.10"
- )
-
- # Test external URL
- hass.config.api = Mock(deprecated_base_url="https://example.com")
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, internal=True)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, internal=True, require_ssl=True)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, internal=True, require_standard_port=True)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, internal=True, allow_ip=False)
-
- # Test with loopback
- hass.config.api = Mock(deprecated_base_url="https://127.0.0.42")
- with pytest.raises(NoURLAvailableError):
- assert _get_deprecated_base_url(hass, internal=True)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, internal=True, allow_ip=False)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, internal=True, require_ssl=True)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, internal=True, require_standard_port=True)
-
-
-async def test_get_deprecated_base_url_external(hass: HomeAssistant):
- """Test getting an external instance URL from the deprecated base_url."""
- # Test with SSL and external domain on standard port
- hass.config.api = Mock(deprecated_base_url="https://example.com:443/")
- assert _get_deprecated_base_url(hass) == "https://example.com"
- assert _get_deprecated_base_url(hass, require_ssl=True) == "https://example.com"
- assert (
- _get_deprecated_base_url(hass, require_standard_port=True)
- == "https://example.com"
- )
-
- # Test without SSL and external domain on non-standard port
- hass.config.api = Mock(deprecated_base_url="http://example.com:8123/")
- assert _get_deprecated_base_url(hass) == "http://example.com:8123"
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, require_ssl=True)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, require_standard_port=True)
-
- # Test SSL on external IP
- hass.config.api = Mock(deprecated_base_url="https://1.1.1.1")
- assert _get_deprecated_base_url(hass) == "https://1.1.1.1"
- assert _get_deprecated_base_url(hass, require_ssl=True) == "https://1.1.1.1"
- assert (
- _get_deprecated_base_url(hass, require_standard_port=True) == "https://1.1.1.1"
- )
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, allow_ip=False)
-
- # Test with private IP
- hass.config.api = Mock(deprecated_base_url="https://10.10.10.10")
- with pytest.raises(NoURLAvailableError):
- assert _get_deprecated_base_url(hass)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, allow_ip=False)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, require_ssl=True)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, require_standard_port=True)
-
- # Test with local domain
- hass.config.api = Mock(deprecated_base_url="https://example.local")
- with pytest.raises(NoURLAvailableError):
- assert _get_deprecated_base_url(hass)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, allow_ip=False)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, require_ssl=True)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, require_standard_port=True)
-
- # Test with loopback
- hass.config.api = Mock(deprecated_base_url="https://127.0.0.42")
- with pytest.raises(NoURLAvailableError):
- assert _get_deprecated_base_url(hass)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, allow_ip=False)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, require_ssl=True)
-
- with pytest.raises(NoURLAvailableError):
- _get_deprecated_base_url(hass, require_standard_port=True)
-
-
-async def test_get_internal_url_with_base_url_fallback(hass: HomeAssistant):
- """Test getting an internal instance URL with the deprecated base_url fallback."""
- hass.config.api = Mock(
- use_ssl=False, port=8123, deprecated_base_url=None, local_ip="192.168.123.123"
- )
- assert hass.config.internal_url is None
- assert _get_internal_url(hass) == "http://192.168.123.123:8123"
-
- with pytest.raises(NoURLAvailableError):
- _get_internal_url(hass, allow_ip=False)
-
- with pytest.raises(NoURLAvailableError):
- _get_internal_url(hass, require_standard_port=True)
-
- with pytest.raises(NoURLAvailableError):
- _get_internal_url(hass, require_ssl=True)
-
- # Add base_url
- hass.config.api = Mock(
- use_ssl=False, port=8123, deprecated_base_url="https://example.local"
- )
- assert _get_internal_url(hass) == "https://example.local"
- assert _get_internal_url(hass, allow_ip=False) == "https://example.local"
- assert (
- _get_internal_url(hass, require_standard_port=True) == "https://example.local"
- )
- assert _get_internal_url(hass, require_ssl=True) == "https://example.local"
-
- # Add internal URL
- await async_process_ha_core_config(
- hass,
- {"internal_url": "https://internal.local"},
- )
- assert _get_internal_url(hass) == "https://internal.local"
- assert _get_internal_url(hass, allow_ip=False) == "https://internal.local"
- assert (
- _get_internal_url(hass, require_standard_port=True) == "https://internal.local"
- )
- assert _get_internal_url(hass, require_ssl=True) == "https://internal.local"
-
- # Add internal URL, mixed results
- await async_process_ha_core_config(
- hass,
- {"internal_url": "http://internal.local:8123"},
- )
- assert _get_internal_url(hass) == "http://internal.local:8123"
- assert _get_internal_url(hass, allow_ip=False) == "http://internal.local:8123"
- assert (
- _get_internal_url(hass, require_standard_port=True) == "https://example.local"
- )
- assert _get_internal_url(hass, require_ssl=True) == "https://example.local"
-
- # Add internal URL set to an IP
- await async_process_ha_core_config(
- hass,
- {"internal_url": "http://10.10.10.10:8123"},
- )
- assert _get_internal_url(hass) == "http://10.10.10.10:8123"
- assert _get_internal_url(hass, allow_ip=False) == "https://example.local"
- assert (
- _get_internal_url(hass, require_standard_port=True) == "https://example.local"
- )
- assert _get_internal_url(hass, require_ssl=True) == "https://example.local"
-
-
-async def test_get_external_url_with_base_url_fallback(hass: HomeAssistant):
- """Test getting an external instance URL with the deprecated base_url fallback."""
- hass.config.api = Mock(use_ssl=False, port=8123, deprecated_base_url=None)
- assert hass.config.internal_url is None
-
- with pytest.raises(NoURLAvailableError):
- _get_external_url(hass)
-
- # Test with SSL and external domain on standard port
- hass.config.api = Mock(deprecated_base_url="https://example.com:443/")
- assert _get_external_url(hass) == "https://example.com"
- assert _get_external_url(hass, allow_ip=False) == "https://example.com"
- assert _get_external_url(hass, require_ssl=True) == "https://example.com"
- assert _get_external_url(hass, require_standard_port=True) == "https://example.com"
-
- # Add external URL
- await async_process_ha_core_config(
- hass,
- {"external_url": "https://external.example.com"},
- )
- assert _get_external_url(hass) == "https://external.example.com"
- assert _get_external_url(hass, allow_ip=False) == "https://external.example.com"
- assert (
- _get_external_url(hass, require_standard_port=True)
- == "https://external.example.com"
- )
- assert _get_external_url(hass, require_ssl=True) == "https://external.example.com"
-
- # Add external URL, mixed results
- await async_process_ha_core_config(
- hass,
- {"external_url": "http://external.example.com:8123"},
- )
- assert _get_external_url(hass) == "http://external.example.com:8123"
- assert _get_external_url(hass, allow_ip=False) == "http://external.example.com:8123"
- assert _get_external_url(hass, require_standard_port=True) == "https://example.com"
- assert _get_external_url(hass, require_ssl=True) == "https://example.com"
-
- # Add external URL set to an IP
- await async_process_ha_core_config(
- hass,
- {"external_url": "http://1.1.1.1:8123"},
- )
- assert _get_external_url(hass) == "http://1.1.1.1:8123"
- assert _get_external_url(hass, allow_ip=False) == "https://example.com"
- assert _get_external_url(hass, require_standard_port=True) == "https://example.com"
- assert _get_external_url(hass, require_ssl=True) == "https://example.com"
-
-
async def test_get_current_request_url_with_known_host(
hass: HomeAssistant, current_request
):
"""Test getting current request URL with known hosts addresses."""
- hass.config.api = Mock(
- use_ssl=False, port=8123, local_ip="127.0.0.1", deprecated_base_url=None
- )
+ hass.config.api = Mock(use_ssl=False, port=8123, local_ip="127.0.0.1")
assert hass.config.internal_url is None
with pytest.raises(NoURLAvailableError):
diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py
index 65d8b442bf08d7..7224dd706778ef 100644
--- a/tests/helpers/test_script.py
+++ b/tests/helpers/test_script.py
@@ -16,8 +16,10 @@
from homeassistant import exceptions
import homeassistant.components.scene as scene
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON
-from homeassistant.core import Context, CoreState, callback
-from homeassistant.helpers import config_validation as cv, script
+from homeassistant.core import SERVICE_CALL_LIMIT, Context, CoreState, callback
+from homeassistant.exceptions import ConditionError, ServiceNotFound
+from homeassistant.helpers import config_validation as cv, script, trace
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -30,6 +32,76 @@
ENTITY_ID = "script.test"
+@pytest.fixture(autouse=True)
+def prepare_tracing():
+ """Prepare tracing."""
+ trace.trace_get()
+
+
+def compare_trigger_item(actual_trigger, expected_trigger):
+ """Compare trigger data description."""
+ assert actual_trigger["description"] == expected_trigger["description"]
+
+
+def compare_result_item(key, actual, expected):
+ """Compare an item in the result dict."""
+ if key == "wait" and (expected.get("trigger") is not None):
+ assert "trigger" in actual
+ expected_trigger = expected.pop("trigger")
+ actual_trigger = actual.pop("trigger")
+ compare_trigger_item(actual_trigger, expected_trigger)
+
+ assert actual == expected
+
+
+def assert_element(trace_element, expected_element, path):
+ """Assert a trace element is as expected.
+
+ Note: Unused variable 'path' is passed to get helpful errors from pytest.
+ """
+ expected_result = expected_element.get("result", {})
+
+ # Check that every item in expected_element is present and equal in trace_element
+ # The redundant set operation gives helpful errors from pytest
+ assert not set(expected_result) - set(trace_element._result or {})
+ for result_key, result in expected_result.items():
+ compare_result_item(result_key, trace_element._result[result_key], result)
+ assert trace_element._result[result_key] == result
+
+ # Check for unexpected items in trace_element
+ assert not set(trace_element._result or {}) - set(expected_result)
+
+ if "error_type" in expected_element:
+ assert isinstance(trace_element._error, expected_element["error_type"])
+ else:
+ assert trace_element._error is None
+
+ # Don't check variables when script starts
+ if trace_element.path == "0":
+ return
+
+ if "variables" in expected_element:
+ assert expected_element["variables"] == trace_element._variables
+ else:
+ assert not trace_element._variables
+
+
+def assert_action_trace(expected, expected_script_execution="finished"):
+ """Assert a trace condition sequence is as expected."""
+ action_trace = trace.trace_get(clear=False)
+ script_execution = trace.script_execution_get()
+ trace.trace_clear()
+ expected_trace_keys = list(expected.keys())
+ assert list(action_trace.keys()) == expected_trace_keys
+ for trace_key_index, key in enumerate(expected_trace_keys):
+ assert len(action_trace[key]) == len(expected[key])
+ for index, element in enumerate(expected[key]):
+ path = f"[{trace_key_index}][{index}]"
+ assert_element(action_trace[key][index], element, path)
+
+ assert script_execution == expected_script_execution
+
+
def async_watch_for_action(script_obj, message):
"""Watch for message in last_action."""
flag = asyncio.Event()
@@ -50,9 +122,17 @@ async def test_firing_event_basic(hass, caplog):
context = Context()
events = async_capture_events(hass, event)
- sequence = cv.SCRIPT_SCHEMA({"event": event, "event_data": {"hello": "world"}})
+ alias = "event step"
+ sequence = cv.SCRIPT_SCHEMA(
+ {"alias": alias, "event": event, "event_data": {"hello": "world"}}
+ )
+
script_obj = script.Script(
- hass, sequence, "Test Name", "test_domain", running_description="test script"
+ hass,
+ sequence,
+ "Test Name",
+ "test_domain",
+ running_description="test script",
)
await script_obj.async_run(context=context)
@@ -63,6 +143,15 @@ async def test_firing_event_basic(hass, caplog):
assert events[0].data.get("hello") == "world"
assert ".test_name:" in caplog.text
assert "Test Name: Running test script" in caplog.text
+ assert f"Executing step {alias}" in caplog.text
+
+ assert_action_trace(
+ {
+ "0": [
+ {"result": {"event": "test_event", "event_data": {"hello": "world"}}},
+ ],
+ }
+ )
async def test_firing_event_template(hass):
@@ -106,13 +195,34 @@ async def test_firing_event_template(hass):
"list2": ["yes", "yesyes"],
}
+ assert_action_trace(
+ {
+ "0": [
+ {
+ "result": {
+ "event": "test_event",
+ "event_data": {
+ "dict": {1: "yes", 2: "yesyes", 3: "yesyesyes"},
+ "dict2": {1: "yes", 2: "yesyes", 3: "yesyesyes"},
+ "list": ["yes", "yesyes"],
+ "list2": ["yes", "yesyes"],
+ },
+ }
+ }
+ ],
+ }
+ )
+
-async def test_calling_service_basic(hass):
+async def test_calling_service_basic(hass, caplog):
"""Test the calling of a service."""
context = Context()
calls = async_mock_service(hass, "test", "script")
- sequence = cv.SCRIPT_SCHEMA({"service": "test.script", "data": {"hello": "world"}})
+ alias = "service step"
+ sequence = cv.SCRIPT_SCHEMA(
+ {"alias": alias, "service": "test.script", "data": {"hello": "world"}}
+ )
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
await script_obj.async_run(context=context)
@@ -121,6 +231,26 @@ async def test_calling_service_basic(hass):
assert len(calls) == 1
assert calls[0].context is context
assert calls[0].data.get("hello") == "world"
+ assert f"Executing step {alias}" in caplog.text
+
+ assert_action_trace(
+ {
+ "0": [
+ {
+ "result": {
+ "limit": SERVICE_CALL_LIMIT,
+ "params": {
+ "domain": "test",
+ "service": "script",
+ "service_data": {"hello": "world"},
+ "target": {},
+ },
+ "running_script": False,
+ }
+ }
+ ],
+ }
+ )
async def test_calling_service_template(hass):
@@ -156,6 +286,25 @@ async def test_calling_service_template(hass):
assert calls[0].context is context
assert calls[0].data.get("hello") == "world"
+ assert_action_trace(
+ {
+ "0": [
+ {
+ "result": {
+ "limit": SERVICE_CALL_LIMIT,
+ "params": {
+ "domain": "test",
+ "service": "script",
+ "service_data": {"hello": "world"},
+ "target": {},
+ },
+ "running_script": False,
+ }
+ }
+ ],
+ }
+ )
+
async def test_data_template_with_templated_key(hass):
"""Test the calling of a service with a data_template with a templated key."""
@@ -174,7 +323,26 @@ async def test_data_template_with_templated_key(hass):
assert len(calls) == 1
assert calls[0].context is context
- assert "hello" in calls[0].data
+ assert calls[0].data.get("hello") == "world"
+
+ assert_action_trace(
+ {
+ "0": [
+ {
+ "result": {
+ "limit": SERVICE_CALL_LIMIT,
+ "params": {
+ "domain": "test",
+ "service": "script",
+ "service_data": {"hello": "world"},
+ "target": {},
+ },
+ "running_script": False,
+ }
+ }
+ ],
+ }
+ )
async def test_multiple_runs_no_wait(hass):
@@ -250,12 +418,13 @@ def heard_event_cb(event):
assert len(calls) == 4
-async def test_activating_scene(hass):
+async def test_activating_scene(hass, caplog):
"""Test the activation of a scene."""
context = Context()
calls = async_mock_service(hass, scene.DOMAIN, SERVICE_TURN_ON)
- sequence = cv.SCRIPT_SCHEMA({"scene": "scene.hello"})
+ alias = "scene step"
+ sequence = cv.SCRIPT_SCHEMA({"alias": alias, "scene": "scene.hello"})
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
await script_obj.async_run(context=context)
@@ -264,6 +433,13 @@ async def test_activating_scene(hass):
assert len(calls) == 1
assert calls[0].context is context
assert calls[0].data.get(ATTR_ENTITY_ID) == "scene.hello"
+ assert f"Executing step {alias}" in caplog.text
+
+ assert_action_trace(
+ {
+ "0": [{"result": {"scene": "scene.hello"}}],
+ }
+ )
@pytest.mark.parametrize("count", [1, 3])
@@ -340,6 +516,12 @@ async def test_delay_basic(hass):
assert not script_obj.is_running
assert script_obj.last_action is None
+ assert_action_trace(
+ {
+ "0": [{"result": {"delay": 5.0, "done": True}}],
+ }
+ )
+
async def test_multiple_runs_delay(hass):
"""Test multiple runs with delay in script."""
@@ -404,6 +586,12 @@ async def test_delay_template_ok(hass):
assert not script_obj.is_running
+ assert_action_trace(
+ {
+ "0": [{"result": {"delay": 5.0, "done": True}}],
+ }
+ )
+
async def test_delay_template_invalid(hass, caplog):
"""Test the delay as a template that fails."""
@@ -431,6 +619,14 @@ async def test_delay_template_invalid(hass, caplog):
assert not script_obj.is_running
assert len(events) == 1
+ assert_action_trace(
+ {
+ "0": [{"result": {"event": "test_event", "event_data": {}}}],
+ "1": [{"error_type": script._StopScript}],
+ },
+ expected_script_execution="aborted",
+ )
+
async def test_delay_template_complex_ok(hass):
"""Test the delay with a working complex template."""
@@ -451,6 +647,12 @@ async def test_delay_template_complex_ok(hass):
assert not script_obj.is_running
+ assert_action_trace(
+ {
+ "0": [{"result": {"delay": 5.0, "done": True}}],
+ }
+ )
+
async def test_delay_template_complex_invalid(hass, caplog):
"""Test the delay with a complex template that fails."""
@@ -478,6 +680,14 @@ async def test_delay_template_complex_invalid(hass, caplog):
assert not script_obj.is_running
assert len(events) == 1
+ assert_action_trace(
+ {
+ "0": [{"result": {"event": "test_event", "event_data": {}}}],
+ "1": [{"error_type": script._StopScript}],
+ },
+ expected_script_execution="aborted",
+ )
+
async def test_cancel_delay(hass):
"""Test the cancelling while the delay is present."""
@@ -509,6 +719,13 @@ async def test_cancel_delay(hass):
assert not script_obj.is_running
assert len(events) == 0
+ assert_action_trace(
+ {
+ "0": [{"result": {"delay": 5.0, "done": False}}],
+ },
+ expected_script_execution="cancelled",
+ )
+
@pytest.mark.parametrize("action_type", ["template", "trigger"])
async def test_wait_basic(hass, action_type):
@@ -544,6 +761,71 @@ async def test_wait_basic(hass, action_type):
assert not script_obj.is_running
assert script_obj.last_action is None
+ if action_type == "template":
+ assert_action_trace(
+ {
+ "0": [{"result": {"wait": {"completed": True, "remaining": None}}}],
+ }
+ )
+ else:
+ assert_action_trace(
+ {
+ "0": [
+ {
+ "result": {
+ "wait": {
+ "trigger": {"description": "state of switch.test"},
+ "remaining": None,
+ }
+ }
+ }
+ ],
+ }
+ )
+
+
+async def test_wait_for_trigger_variables(hass):
+ """Test variables are passed to wait_for_trigger action."""
+ context = Context()
+ wait_alias = "wait step"
+ actions = [
+ {
+ "alias": "variables",
+ "variables": {"seconds": 5},
+ },
+ {
+ "alias": wait_alias,
+ "wait_for_trigger": {
+ "platform": "state",
+ "entity_id": "switch.test",
+ "to": "off",
+ "for": {"seconds": "{{ seconds }}"},
+ },
+ },
+ ]
+ sequence = cv.SCRIPT_SCHEMA(actions)
+ sequence = await script.async_validate_actions_config(hass, sequence)
+ script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
+ wait_started_flag = async_watch_for_action(script_obj, wait_alias)
+
+ try:
+ hass.states.async_set("switch.test", "on")
+ hass.async_create_task(script_obj.async_run(context=context))
+ await asyncio.wait_for(wait_started_flag.wait(), 1)
+ assert script_obj.is_running
+ assert script_obj.last_action == wait_alias
+ hass.states.async_set("switch.test", "off")
+ # the script task + 2 tasks created by wait_for_trigger script step
+ await hass.async_wait_for_task_count(3)
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
+ await hass.async_block_till_done()
+ except (AssertionError, asyncio.TimeoutError):
+ await script_obj.async_stop()
+ raise
+ else:
+ assert not script_obj.is_running
+ assert script_obj.last_action is None
+
@pytest.mark.parametrize("action_type", ["template", "trigger"])
async def test_wait_basic_times_out(hass, action_type):
@@ -579,6 +861,19 @@ async def test_wait_basic_times_out(hass, action_type):
assert timed_out
+ if action_type == "template":
+ assert_action_trace(
+ {
+ "0": [{"result": {"wait": {"completed": False, "remaining": None}}}],
+ }
+ )
+ else:
+ assert_action_trace(
+ {
+ "0": [{"result": {"wait": {"trigger": None, "remaining": None}}}],
+ }
+ )
+
@pytest.mark.parametrize("action_type", ["template", "trigger"])
async def test_multiple_runs_wait(hass, action_type):
@@ -676,6 +971,21 @@ async def test_cancel_wait(hass, action_type):
assert not script_obj.is_running
assert len(events) == 0
+ if action_type == "template":
+ assert_action_trace(
+ {
+ "0": [{"result": {"wait": {"completed": False, "remaining": None}}}],
+ },
+ expected_script_execution="cancelled",
+ )
+ else:
+ assert_action_trace(
+ {
+ "0": [{"result": {"wait": {"trigger": None, "remaining": None}}}],
+ },
+ expected_script_execution="cancelled",
+ )
+
async def test_wait_template_not_schedule(hass):
"""Test the wait template with correct condition."""
@@ -697,6 +1007,19 @@ async def test_wait_template_not_schedule(hass):
assert not script_obj.is_running
assert len(events) == 2
+ assert_action_trace(
+ {
+ "0": [{"result": {"event": "test_event", "event_data": {}}}],
+ "1": [{"result": {"wait": {"completed": True, "remaining": None}}}],
+ "2": [
+ {
+ "result": {"event": "test_event", "event_data": {}},
+ "variables": {"wait": {"completed": True, "remaining": None}},
+ }
+ ],
+ }
+ )
+
@pytest.mark.parametrize(
"timeout_param", [5, "{{ 5 }}", {"seconds": 5}, {"seconds": "{{ 5 }}"}]
@@ -746,6 +1069,21 @@ async def test_wait_timeout(hass, caplog, timeout_param, action_type):
assert len(events) == 1
assert "(timeout: 0:00:05)" in caplog.text
+ if action_type == "template":
+ variable_wait = {"wait": {"completed": False, "remaining": 0.0}}
+ else:
+ variable_wait = {"wait": {"trigger": None, "remaining": 0.0}}
+ expected_trace = {
+ "0": [{"result": variable_wait}],
+ "1": [
+ {
+ "result": {"event": "test_event", "event_data": {}},
+ "variables": variable_wait,
+ }
+ ],
+ }
+ assert_action_trace(expected_trace)
+
@pytest.mark.parametrize(
"continue_on_timeout,n_events", [(False, 0), (True, 1), (None, 1)]
@@ -791,6 +1129,27 @@ async def test_wait_continue_on_timeout(
assert not script_obj.is_running
assert len(events) == n_events
+ if action_type == "template":
+ variable_wait = {"wait": {"completed": False, "remaining": 0.0}}
+ else:
+ variable_wait = {"wait": {"trigger": None, "remaining": 0.0}}
+ expected_trace = {
+ "0": [{"result": variable_wait}],
+ }
+ if continue_on_timeout is False:
+ expected_trace["0"][0]["result"]["timeout"] = True
+ expected_trace["0"][0]["error_type"] = script._StopScript
+ expected_script_execution = "aborted"
+ else:
+ expected_trace["1"] = [
+ {
+ "result": {"event": "test_event", "event_data": {}},
+ "variables": variable_wait,
+ }
+ ]
+ expected_script_execution = "finished"
+ assert_action_trace(expected_trace, expected_script_execution)
+
async def test_wait_template_variables_in(hass):
"""Test the wait template with input variables."""
@@ -815,6 +1174,12 @@ async def test_wait_template_variables_in(hass):
assert not script_obj.is_running
+ assert_action_trace(
+ {
+ "0": [{"result": {"wait": {"completed": True, "remaining": None}}}],
+ }
+ )
+
async def test_wait_template_with_utcnow(hass):
"""Test the wait template with utcnow."""
@@ -840,6 +1205,12 @@ async def test_wait_template_with_utcnow(hass):
await hass.async_block_till_done()
assert not script_obj.is_running
+ assert_action_trace(
+ {
+ "0": [{"result": {"wait": {"completed": True, "remaining": None}}}],
+ }
+ )
+
async def test_wait_template_with_utcnow_no_match(hass):
"""Test the wait template with utcnow that does not match."""
@@ -870,6 +1241,12 @@ async def test_wait_template_with_utcnow_no_match(hass):
assert timed_out
+ assert_action_trace(
+ {
+ "0": [{"result": {"wait": {"completed": False, "remaining": None}}}],
+ }
+ )
+
@pytest.mark.parametrize("mode", ["no_timeout", "timeout_finish", "timeout_not_finish"])
@pytest.mark.parametrize("action_type", ["template", "trigger"])
@@ -963,6 +1340,12 @@ async def async_attach_trigger_mock(*args, **kwargs):
assert "Unknown error while setting up trigger" in caplog.text
+ assert_action_trace(
+ {
+ "0": [{"result": {"wait": {"trigger": None, "remaining": None}}}],
+ }
+ )
+
async def test_wait_for_trigger_generated_exception(hass, caplog):
"""Test bad wait_for_trigger."""
@@ -989,15 +1372,63 @@ async def async_attach_trigger_mock(*args, **kwargs):
assert "ValueError" in caplog.text
assert "something bad" in caplog.text
+ assert_action_trace(
+ {
+ "0": [{"result": {"wait": {"trigger": None, "remaining": None}}}],
+ }
+ )
+
+
+async def test_condition_warning(hass, caplog):
+ """Test warning on condition."""
+ event = "test_event"
+ events = async_capture_events(hass, event)
+ sequence = cv.SCRIPT_SCHEMA(
+ [
+ {"event": event},
+ {
+ "condition": "numeric_state",
+ "entity_id": "test.entity",
+ "above": 0,
+ },
+ {"event": event},
+ ]
+ )
+ script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
+
+ caplog.clear()
+ caplog.set_level(logging.WARNING)
+
+ hass.states.async_set("test.entity", "string")
+ await script_obj.async_run(context=Context())
+ await hass.async_block_till_done()
+
+ assert len(caplog.record_tuples) == 1
+ assert caplog.record_tuples[0][1] == logging.WARNING
+
+ assert len(events) == 1
-async def test_condition_basic(hass):
+ assert_action_trace(
+ {
+ "0": [{"result": {"event": "test_event", "event_data": {}}}],
+ "1": [{"error_type": script._StopScript, "result": {"result": False}}],
+ "1/condition": [{"error_type": ConditionError}],
+ "1/condition/entity_id/0": [{"error_type": ConditionError}],
+ },
+ expected_script_execution="aborted",
+ )
+
+
+async def test_condition_basic(hass, caplog):
"""Test if we can use conditions in a script."""
event = "test_event"
events = async_capture_events(hass, event)
+ alias = "condition step"
sequence = cv.SCRIPT_SCHEMA(
[
{"event": event},
{
+ "alias": alias,
"condition": "template",
"value_template": "{{ states.test.entity.state == 'hello' }}",
},
@@ -1010,15 +1441,36 @@ async def test_condition_basic(hass):
await script_obj.async_run(context=Context())
await hass.async_block_till_done()
+ assert f"Test condition {alias}: True" in caplog.text
+ caplog.clear()
assert len(events) == 2
+ assert_action_trace(
+ {
+ "0": [{"result": {"event": "test_event", "event_data": {}}}],
+ "1": [{"result": {"result": True}}],
+ "1/condition": [{"result": {"result": True}}],
+ "2": [{"result": {"event": "test_event", "event_data": {}}}],
+ }
+ )
+
hass.states.async_set("test.entity", "goodbye")
await script_obj.async_run(context=Context())
await hass.async_block_till_done()
+ assert f"Test condition {alias}: False" in caplog.text
assert len(events) == 3
+ assert_action_trace(
+ {
+ "0": [{"result": {"event": "test_event", "event_data": {}}}],
+ "1": [{"error_type": script._StopScript, "result": {"result": False}}],
+ "1/condition": [{"result": {"result": False}}],
+ },
+ expected_script_execution="aborted",
+ )
+
@patch("homeassistant.helpers.script.condition.async_from_config")
async def test_condition_created_once(async_from_config, hass):
@@ -1067,14 +1519,16 @@ async def test_condition_all_cached(hass):
assert len(script_obj._config_cache) == 2
-async def test_repeat_count(hass):
+@pytest.mark.parametrize("count", [3, script.ACTION_TRACE_NODE_MAX_LEN * 2])
+async def test_repeat_count(hass, caplog, count):
"""Test repeat action w/ count option."""
event = "test_event"
events = async_capture_events(hass, event)
- count = 3
+ alias = "condition step"
sequence = cv.SCRIPT_SCHEMA(
{
+ "alias": alias,
"repeat": {
"count": count,
"sequence": {
@@ -1085,9 +1539,10 @@ async def test_repeat_count(hass):
"last": "{{ repeat.last }}",
},
},
- }
+ },
}
)
+
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
await script_obj.async_run(context=Context())
@@ -1098,6 +1553,95 @@ async def test_repeat_count(hass):
assert event.data.get("first") == (index == 0)
assert event.data.get("index") == index + 1
assert event.data.get("last") == (index == count - 1)
+ assert caplog.text.count(f"Repeating {alias}") == count
+ first_index = max(1, count - script.ACTION_TRACE_NODE_MAX_LEN + 1)
+ last_index = count + 1
+ assert_action_trace(
+ {
+ "0": [{}],
+ "0/repeat/sequence/0": [
+ {
+ "result": {
+ "event": "test_event",
+ "event_data": {
+ "first": index == 1,
+ "index": index,
+ "last": index == count,
+ },
+ },
+ "variables": {
+ "repeat": {
+ "first": index == 1,
+ "index": index,
+ "last": index == count,
+ }
+ },
+ }
+ for index in range(first_index, last_index)
+ ],
+ }
+ )
+
+
+@pytest.mark.parametrize("condition", ["while", "until"])
+async def test_repeat_condition_warning(hass, caplog, condition):
+ """Test warning on repeat conditions."""
+ event = "test_event"
+ events = async_capture_events(hass, event)
+ count = 0 if condition == "while" else 1
+
+ sequence = {
+ "repeat": {
+ "sequence": [
+ {
+ "event": event,
+ },
+ ],
+ }
+ }
+ sequence["repeat"][condition] = {
+ "condition": "numeric_state",
+ "entity_id": "sensor.test",
+ "value_template": "{{ unassigned_variable }}",
+ "above": "0",
+ }
+
+ script_obj = script.Script(
+ hass, cv.SCRIPT_SCHEMA(sequence), f"Test {condition}", "test_domain"
+ )
+
+ # wait_started = async_watch_for_action(script_obj, "wait")
+ hass.states.async_set("sensor.test", "1")
+
+ caplog.clear()
+ caplog.set_level(logging.WARNING)
+
+ hass.async_create_task(script_obj.async_run(context=Context()))
+ await asyncio.wait_for(hass.async_block_till_done(), 1)
+
+ assert f"Error in '{condition}[0]' evaluation" in caplog.text
+
+ assert len(events) == count
+
+ expected_trace = {"0": [{}]}
+ if condition == "until":
+ expected_trace["0/repeat/sequence/0"] = [
+ {
+ "result": {"event": "test_event", "event_data": {}},
+ "variables": {"repeat": {"first": True, "index": 1}},
+ }
+ ]
+ expected_trace["0/repeat"] = [
+ {
+ "result": {"result": None},
+ "variables": {"repeat": {"first": True, "index": 1}},
+ }
+ ]
+ expected_trace[f"0/repeat/{condition}/0"] = [{"error_type": ConditionError}]
+ expected_trace[f"0/repeat/{condition}/0/entity_id/0"] = [
+ {"error_type": ConditionError}
+ ]
+ assert_action_trace(expected_trace)
@pytest.mark.parametrize("condition", ["while", "until"])
@@ -1182,15 +1726,14 @@ async def test_repeat_var_in_condition(hass, condition):
sequence = {"repeat": {"sequence": {"event": event}}}
if condition == "while":
- sequence["repeat"]["while"] = {
- "condition": "template",
- "value_template": "{{ repeat.index <= 2 }}",
- }
+ value_template = "{{ repeat.index <= 2 }}"
else:
- sequence["repeat"]["until"] = {
- "condition": "template",
- "value_template": "{{ repeat.index == 2 }}",
- }
+ value_template = "{{ repeat.index == 2 }}"
+ sequence["repeat"][condition] = {
+ "condition": "template",
+ "value_template": value_template,
+ }
+
script_obj = script.Script(
hass, cv.SCRIPT_SCHEMA(sequence), "Test Name", "test_domain"
)
@@ -1203,6 +1746,63 @@ async def test_repeat_var_in_condition(hass, condition):
assert len(events) == 2
+ if condition == "while":
+ expected_trace = {
+ "0": [{}],
+ "0/repeat": [
+ {
+ "result": {"result": True},
+ "variables": {"repeat": {"first": True, "index": 1}},
+ },
+ {
+ "result": {"result": True},
+ "variables": {"repeat": {"first": False, "index": 2}},
+ },
+ {
+ "result": {"result": False},
+ "variables": {"repeat": {"first": False, "index": 3}},
+ },
+ ],
+ "0/repeat/while/0": [
+ {"result": {"result": True}},
+ {"result": {"result": True}},
+ {"result": {"result": False}},
+ ],
+ "0/repeat/sequence/0": [
+ {"result": {"event": "test_event", "event_data": {}}}
+ ]
+ * 2,
+ }
+ else:
+ expected_trace = {
+ "0": [{}],
+ "0/repeat/sequence/0": [
+ {
+ "result": {"event": "test_event", "event_data": {}},
+ "variables": {"repeat": {"first": True, "index": 1}},
+ },
+ {
+ "result": {"event": "test_event", "event_data": {}},
+ "variables": {"repeat": {"first": False, "index": 2}},
+ },
+ ],
+ "0/repeat": [
+ {
+ "result": {"result": False},
+ "variables": {"repeat": {"first": True, "index": 1}},
+ },
+ {
+ "result": {"result": True},
+ "variables": {"repeat": {"first": False, "index": 2}},
+ },
+ ],
+ "0/repeat/until/0": [
+ {"result": {"result": False}},
+ {"result": {"result": True}},
+ ],
+ }
+ assert_action_trace(expected_trace)
+
@pytest.mark.parametrize(
"variables,first_last,inside_x",
@@ -1304,30 +1904,141 @@ async def test_repeat_nested(hass, variables, first_last, inside_x):
"x": result[3],
}
+ event_data1 = {"repeat": None, "x": inside_x}
+ event_data2 = [
+ {"first": True, "index": 1, "last": False, "x": inside_x},
+ {"first": False, "index": 2, "last": True, "x": inside_x},
+ ]
+ variable_repeat = [
+ {"repeat": {"first": True, "index": 1, "last": False}},
+ {"repeat": {"first": False, "index": 2, "last": True}},
+ ]
+ expected_trace = {
+ "0": [{"result": {"event": "test_event", "event_data": event_data1}}],
+ "1": [{}],
+ "1/repeat/sequence/0": [
+ {
+ "result": {"event": "test_event", "event_data": event_data2[0]},
+ "variables": variable_repeat[0],
+ },
+ {
+ "result": {"event": "test_event", "event_data": event_data2[1]},
+ "variables": variable_repeat[1],
+ },
+ ],
+ "1/repeat/sequence/1": [{}, {}],
+ "1/repeat/sequence/1/repeat/sequence/0": [
+ {"result": {"event": "test_event", "event_data": event_data2[0]}},
+ {
+ "result": {"event": "test_event", "event_data": event_data2[1]},
+ "variables": variable_repeat[1],
+ },
+ {
+ "result": {"event": "test_event", "event_data": event_data2[0]},
+ "variables": variable_repeat[0],
+ },
+ {"result": {"event": "test_event", "event_data": event_data2[1]}},
+ ],
+ "1/repeat/sequence/2": [
+ {"result": {"event": "test_event", "event_data": event_data2[0]}},
+ {"result": {"event": "test_event", "event_data": event_data2[1]}},
+ ],
+ "2": [{"result": {"event": "test_event", "event_data": event_data1}}],
+ }
+ assert_action_trace(expected_trace)
+
+
+async def test_choose_warning(hass, caplog):
+ """Test warning on choose."""
+ event = "test_event"
+ events = async_capture_events(hass, event)
+
+ sequence = cv.SCRIPT_SCHEMA(
+ {
+ "choose": [
+ {
+ "conditions": {
+ "condition": "numeric_state",
+ "entity_id": "test.entity",
+ "value_template": "{{ undefined_a + undefined_b }}",
+ "above": 1,
+ },
+ "sequence": {"event": event, "event_data": {"choice": "first"}},
+ },
+ {
+ "conditions": {
+ "condition": "numeric_state",
+ "entity_id": "test.entity",
+ "value_template": "{{ 'string' }}",
+ "above": 2,
+ },
+ "sequence": {"event": event, "event_data": {"choice": "second"}},
+ },
+ ],
+ "default": {"event": event, "event_data": {"choice": "default"}},
+ }
+ )
+ script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
+
+ hass.states.async_set("test.entity", "9")
+ await hass.async_block_till_done()
+
+ caplog.clear()
+ caplog.set_level(logging.WARNING)
+
+ await script_obj.async_run(context=Context())
+ await hass.async_block_till_done()
+
+ assert len(caplog.record_tuples) == 2
+ assert caplog.record_tuples[0][1] == logging.WARNING
+ assert caplog.record_tuples[1][1] == logging.WARNING
+
+ assert len(events) == 1
+ assert events[0].data["choice"] == "default"
+
@pytest.mark.parametrize("var,result", [(1, "first"), (2, "second"), (3, "default")])
-async def test_choose(hass, var, result):
+async def test_choose(hass, caplog, var, result):
"""Test choose action."""
event = "test_event"
events = async_capture_events(hass, event)
+ alias = "choose step"
+ choice = {1: "choice one", 2: "choice two", 3: None}
+ aliases = {1: "sequence one", 2: "sequence two", 3: "default sequence"}
sequence = cv.SCRIPT_SCHEMA(
{
+ "alias": alias,
"choose": [
{
+ "alias": choice[1],
"conditions": {
"condition": "template",
"value_template": "{{ var == 1 }}",
},
- "sequence": {"event": event, "event_data": {"choice": "first"}},
+ "sequence": {
+ "alias": aliases[1],
+ "event": event,
+ "event_data": {"choice": "first"},
+ },
},
{
+ "alias": choice[2],
"conditions": "{{ var == 2 }}",
- "sequence": {"event": event, "event_data": {"choice": "second"}},
+ "sequence": {
+ "alias": aliases[2],
+ "event": event,
+ "event_data": {"choice": "second"},
+ },
},
],
- "default": {"event": event, "event_data": {"choice": "default"}},
+ "default": {
+ "alias": aliases[3],
+ "event": event,
+ "event_data": {"choice": "default"},
+ },
}
)
+
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
await script_obj.async_run(MappingProxyType({"var": var}), Context())
@@ -1335,6 +2046,35 @@ async def test_choose(hass, var, result):
assert len(events) == 1
assert events[0].data["choice"] == result
+ expected_choice = choice[var]
+ if var == 3:
+ expected_choice = "default"
+ assert f"{alias}: {expected_choice}: Executing step {aliases[var]}" in caplog.text
+
+ expected_choice = var - 1
+ if var == 3:
+ expected_choice = "default"
+
+ expected_trace = {"0": [{"result": {"choice": expected_choice}}]}
+ if var >= 1:
+ expected_trace["0/choose/0"] = [{"result": {"result": var == 1}}]
+ expected_trace["0/choose/0/conditions/0"] = [{"result": {"result": var == 1}}]
+ if var >= 2:
+ expected_trace["0/choose/1"] = [{"result": {"result": var == 2}}]
+ expected_trace["0/choose/1/conditions/0"] = [{"result": {"result": var == 2}}]
+ if var == 1:
+ expected_trace["0/choose/0/sequence/0"] = [
+ {"result": {"event": "test_event", "event_data": {"choice": "first"}}}
+ ]
+ if var == 2:
+ expected_trace["0/choose/1/sequence/0"] = [
+ {"result": {"event": "test_event", "event_data": {"choice": "second"}}}
+ ]
+ if var == 3:
+ expected_trace["0/default/0"] = [
+ {"result": {"event": "test_event", "event_data": {"choice": "default"}}}
+ ]
+ assert_action_trace(expected_trace)
@pytest.mark.parametrize(
@@ -1396,6 +2136,25 @@ async def test_propagate_error_service_not_found(hass):
assert len(events) == 0
assert not script_obj.is_running
+ expected_trace = {
+ "0": [
+ {
+ "error_type": ServiceNotFound,
+ "result": {
+ "limit": 10,
+ "params": {
+ "domain": "test",
+ "service": "script",
+ "service_data": {},
+ "target": {},
+ },
+ "running_script": False,
+ },
+ }
+ ],
+ }
+ assert_action_trace(expected_trace, expected_script_execution="error")
+
async def test_propagate_error_invalid_service_data(hass):
"""Test that a script aborts when we send invalid service data."""
@@ -1414,6 +2173,25 @@ async def test_propagate_error_invalid_service_data(hass):
assert len(calls) == 0
assert not script_obj.is_running
+ expected_trace = {
+ "0": [
+ {
+ "error_type": vol.MultipleInvalid,
+ "result": {
+ "limit": 10,
+ "params": {
+ "domain": "test",
+ "service": "script",
+ "service_data": {"text": 1},
+ "target": {},
+ },
+ "running_script": False,
+ },
+ }
+ ],
+ }
+ assert_action_trace(expected_trace, expected_script_execution="error")
+
async def test_propagate_error_service_exception(hass):
"""Test that a script aborts when a service throws an exception."""
@@ -1436,6 +2214,25 @@ def record_call(service):
assert len(events) == 0
assert not script_obj.is_running
+ expected_trace = {
+ "0": [
+ {
+ "error_type": ValueError,
+ "result": {
+ "limit": 10,
+ "params": {
+ "domain": "test",
+ "service": "script",
+ "service_data": {},
+ "target": {},
+ },
+ "running_script": False,
+ },
+ }
+ ],
+ }
+ assert_action_trace(expected_trace, expected_script_execution="error")
+
async def test_referenced_entities(hass):
"""Test referenced entities."""
@@ -1879,6 +2676,11 @@ async def test_shutdown_at(hass, caplog):
assert not script_obj.is_running
assert "Stopping scripts running at shutdown: test script" in caplog.text
+ expected_trace = {
+ "0": [{"result": {"delay": 120.0, "done": False}}],
+ }
+ assert_action_trace(expected_trace)
+
async def test_shutdown_after(hass, caplog):
"""Test stopping scripts at shutdown."""
@@ -1910,6 +2712,11 @@ async def test_shutdown_after(hass, caplog):
in caplog.text
)
+ expected_trace = {
+ "0": [{"result": {"delay": 120.0, "done": False}}],
+ }
+ assert_action_trace(expected_trace)
+
async def test_update_logger(hass, caplog):
"""Test updating logger."""
@@ -1951,9 +2758,10 @@ def started_action():
async def test_set_variable(hass, caplog):
"""Test setting variables in scripts."""
+ alias = "variables step"
sequence = cv.SCRIPT_SCHEMA(
[
- {"variables": {"variable": "value"}},
+ {"alias": alias, "variables": {"variable": "value"}},
{"service": "test.script", "data": {"value": "{{ variable }}"}},
]
)
@@ -1965,6 +2773,27 @@ async def test_set_variable(hass, caplog):
await hass.async_block_till_done()
assert mock_calls[0].data["value"] == "value"
+ assert f"Executing step {alias}" in caplog.text
+
+ expected_trace = {
+ "0": [{}],
+ "1": [
+ {
+ "result": {
+ "limit": SERVICE_CALL_LIMIT,
+ "params": {
+ "domain": "test",
+ "service": "script",
+ "service_data": {"value": "value"},
+ "target": {},
+ },
+ "running_script": False,
+ },
+ "variables": {"variable": "value"},
+ }
+ ],
+ }
+ assert_action_trace(expected_trace)
async def test_set_redefines_variable(hass, caplog):
@@ -1987,6 +2816,42 @@ async def test_set_redefines_variable(hass, caplog):
assert mock_calls[0].data["value"] == 1
assert mock_calls[1].data["value"] == 2
+ expected_trace = {
+ "0": [{}],
+ "1": [
+ {
+ "result": {
+ "limit": SERVICE_CALL_LIMIT,
+ "params": {
+ "domain": "test",
+ "service": "script",
+ "service_data": {"value": 1},
+ "target": {},
+ },
+ "running_script": False,
+ },
+ "variables": {"variable": "1"},
+ }
+ ],
+ "2": [{}],
+ "3": [
+ {
+ "result": {
+ "limit": SERVICE_CALL_LIMIT,
+ "params": {
+ "domain": "test",
+ "service": "script",
+ "service_data": {"value": 2},
+ "target": {},
+ },
+ "running_script": False,
+ },
+ "variables": {"variable": 2},
+ }
+ ],
+ }
+ assert_action_trace(expected_trace)
+
async def test_validate_action_config(hass):
"""Validate action config."""
@@ -2097,3 +2962,165 @@ async def trigger_wait_event(_):
await hass.async_block_till_done()
assert len(mock_calls) == 1
+
+
+async def test_breakpoints_1(hass):
+ """Test setting a breakpoint halts execution, and execution can be resumed."""
+ event = "test_event"
+ events = async_capture_events(hass, event)
+ sequence = cv.SCRIPT_SCHEMA(
+ [
+ {"event": event, "event_data": {"value": 0}}, # Node "0"
+ {"event": event, "event_data": {"value": 1}}, # Node "1"
+ {"event": event, "event_data": {"value": 2}}, # Node "2"
+ {"event": event, "event_data": {"value": 3}}, # Node "3"
+ {"event": event, "event_data": {"value": 4}}, # Node "4"
+ {"event": event, "event_data": {"value": 5}}, # Node "5"
+ {"event": event, "event_data": {"value": 6}}, # Node "6"
+ {"event": event, "event_data": {"value": 7}}, # Node "7"
+ ]
+ )
+ logger = logging.getLogger("TEST")
+ script_obj = script.Script(
+ hass,
+ sequence,
+ "Test Name",
+ "test_domain",
+ script_mode="queued",
+ max_runs=2,
+ logger=logger,
+ )
+ trace.trace_id_set(("script_1", "1"))
+ script.breakpoint_set(hass, "script_1", script.RUN_ID_ANY, "1")
+ script.breakpoint_set(hass, "script_1", script.RUN_ID_ANY, "5")
+
+ breakpoint_hit_event = asyncio.Event()
+
+ @callback
+ def breakpoint_hit(*_):
+ breakpoint_hit_event.set()
+
+ async_dispatcher_connect(hass, script.SCRIPT_BREAKPOINT_HIT, breakpoint_hit)
+
+ watch_messages = []
+
+ @callback
+ def check_action():
+ for message, flag in watch_messages:
+ if script_obj.last_action and message in script_obj.last_action:
+ flag.set()
+
+ script_obj.change_listener = check_action
+
+ assert not script_obj.is_running
+ assert script_obj.runs == 0
+
+ # Start script, should stop on breakpoint at node "1"
+ hass.async_create_task(script_obj.async_run(context=Context()))
+ await breakpoint_hit_event.wait()
+ assert script_obj.is_running
+ assert script_obj.runs == 1
+ assert len(events) == 1
+ assert events[-1].data["value"] == 0
+
+ # Single step script, should stop at node "2"
+ breakpoint_hit_event.clear()
+ script.debug_step(hass, "script_1", "1")
+ await breakpoint_hit_event.wait()
+ assert script_obj.is_running
+ assert script_obj.runs == 1
+ assert len(events) == 2
+ assert events[-1].data["value"] == 1
+
+ # Single step script, should stop at node "3"
+ breakpoint_hit_event.clear()
+ script.debug_step(hass, "script_1", "1")
+ await breakpoint_hit_event.wait()
+ assert script_obj.is_running
+ assert script_obj.runs == 1
+ assert len(events) == 3
+ assert events[-1].data["value"] == 2
+
+ # Resume script, should stop on breakpoint at node "5"
+ breakpoint_hit_event.clear()
+ script.debug_continue(hass, "script_1", "1")
+ await breakpoint_hit_event.wait()
+ assert script_obj.is_running
+ assert script_obj.runs == 1
+ assert len(events) == 5
+ assert events[-1].data["value"] == 4
+
+ # Resume script, should run until completion
+ script.debug_continue(hass, "script_1", "1")
+ await hass.async_block_till_done()
+ assert not script_obj.is_running
+ assert script_obj.runs == 0
+ assert len(events) == 8
+ assert events[-1].data["value"] == 7
+
+
+async def test_breakpoints_2(hass):
+ """Test setting a breakpoint halts execution, and execution can be aborted."""
+ event = "test_event"
+ events = async_capture_events(hass, event)
+ sequence = cv.SCRIPT_SCHEMA(
+ [
+ {"event": event, "event_data": {"value": 0}}, # Node "0"
+ {"event": event, "event_data": {"value": 1}}, # Node "1"
+ {"event": event, "event_data": {"value": 2}}, # Node "2"
+ {"event": event, "event_data": {"value": 3}}, # Node "3"
+ {"event": event, "event_data": {"value": 4}}, # Node "4"
+ {"event": event, "event_data": {"value": 5}}, # Node "5"
+ {"event": event, "event_data": {"value": 6}}, # Node "6"
+ {"event": event, "event_data": {"value": 7}}, # Node "7"
+ ]
+ )
+ logger = logging.getLogger("TEST")
+ script_obj = script.Script(
+ hass,
+ sequence,
+ "Test Name",
+ "test_domain",
+ script_mode="queued",
+ max_runs=2,
+ logger=logger,
+ )
+ trace.trace_id_set(("script_1", "1"))
+ script.breakpoint_set(hass, "script_1", script.RUN_ID_ANY, "1")
+ script.breakpoint_set(hass, "script_1", script.RUN_ID_ANY, "5")
+
+ breakpoint_hit_event = asyncio.Event()
+
+ @callback
+ def breakpoint_hit(*_):
+ breakpoint_hit_event.set()
+
+ async_dispatcher_connect(hass, script.SCRIPT_BREAKPOINT_HIT, breakpoint_hit)
+
+ watch_messages = []
+
+ @callback
+ def check_action():
+ for message, flag in watch_messages:
+ if script_obj.last_action and message in script_obj.last_action:
+ flag.set()
+
+ script_obj.change_listener = check_action
+
+ assert not script_obj.is_running
+ assert script_obj.runs == 0
+
+ # Start script, should stop on breakpoint at node "1"
+ hass.async_create_task(script_obj.async_run(context=Context()))
+ await breakpoint_hit_event.wait()
+ assert script_obj.is_running
+ assert script_obj.runs == 1
+ assert len(events) == 1
+ assert events[-1].data["value"] == 0
+
+ # Abort script
+ script.debug_stop(hass, "script_1", "1")
+ await hass.async_block_till_done()
+ assert not script_obj.is_running
+ assert script_obj.runs == 0
+ assert len(events) == 1
diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py
index 2916d6167039d6..23d8200be23c73 100644
--- a/tests/helpers/test_selector.py
+++ b/tests/helpers/test_selector.py
@@ -118,6 +118,15 @@ def test_number_selector_schema(schema):
selector.validate_selector({"number": schema})
+@pytest.mark.parametrize(
+ "schema",
+ ({},),
+)
+def test_addon_selector_schema(schema):
+ """Test add-on selector."""
+ selector.validate_selector({"addon": schema})
+
+
@pytest.mark.parametrize(
"schema",
({},),
@@ -187,3 +196,26 @@ def test_object_selector_schema(schema):
def test_text_selector_schema(schema):
"""Test text selector."""
selector.validate_selector({"text": schema})
+
+
+@pytest.mark.parametrize(
+ "schema",
+ ({"options": ["red", "green", "blue"]},),
+)
+def test_select_selector_schema(schema):
+ """Test select selector."""
+ selector.validate_selector({"select": schema})
+
+
+@pytest.mark.parametrize(
+ "schema",
+ (
+ {},
+ {"options": {"hello": "World"}},
+ {"options": []},
+ ),
+)
+def test_select_selector_schema_error(schema):
+ """Test select selector."""
+ with pytest.raises(vol.Invalid):
+ selector.validate_selector({"select": schema})
diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py
index 95ccdc843951ba..7538c0f6f2c9a2 100644
--- a/tests/helpers/test_service.py
+++ b/tests/helpers/test_service.py
@@ -195,6 +195,48 @@ def test_service_call(self):
"area_id": ["test-area-id"],
}
+ config = {
+ "service": "{{ 'test_domain.test_service' }}",
+ "target": {
+ "area_id": ["area-42", "{{ 'area-51' }}"],
+ "device_id": ["abcdef", "{{ 'fedcba' }}"],
+ "entity_id": ["light.static", "{{ 'light.dynamic' }}"],
+ },
+ }
+
+ service.call_from_config(self.hass, config)
+ self.hass.block_till_done()
+
+ assert dict(self.calls[1].data) == {
+ "area_id": ["area-42", "area-51"],
+ "device_id": ["abcdef", "fedcba"],
+ "entity_id": ["light.static", "light.dynamic"],
+ }
+
+ config = {
+ "service": "{{ 'test_domain.test_service' }}",
+ "target": "{{ var_target }}",
+ }
+
+ service.call_from_config(
+ self.hass,
+ config,
+ variables={
+ "var_target": {
+ "entity_id": "light.static",
+ "area_id": ["area-42", "area-51"],
+ },
+ },
+ )
+
+ service.call_from_config(self.hass, config)
+ self.hass.block_till_done()
+
+ assert dict(self.calls[2].data) == {
+ "area_id": ["area-42", "area-51"],
+ "entity_id": ["light.static"],
+ }
+
def test_service_template_service_call(self):
"""Test legacy service_template call with templating."""
config = {
@@ -543,22 +585,21 @@ async def test_call_context_target_specific_no_auth(
hass, mock_handle_entity_call, mock_entities
):
"""Check targeting specific entities without auth."""
- with pytest.raises(exceptions.Unauthorized) as err:
- with patch(
- "homeassistant.auth.AuthManager.async_get_user",
- return_value=Mock(permissions=PolicyPermissions({}, None)),
- ):
- await service.entity_service_call(
- hass,
- [Mock(entities=mock_entities)],
- Mock(),
- ha.ServiceCall(
- "test_domain",
- "test_service",
- {"entity_id": "light.kitchen"},
- context=ha.Context(user_id="mock-id"),
- ),
- )
+ with pytest.raises(exceptions.Unauthorized) as err, patch(
+ "homeassistant.auth.AuthManager.async_get_user",
+ return_value=Mock(permissions=PolicyPermissions({}, None)),
+ ):
+ await service.entity_service_call(
+ hass,
+ [Mock(entities=mock_entities)],
+ Mock(),
+ ha.ServiceCall(
+ "test_domain",
+ "test_service",
+ {"entity_id": "light.kitchen"},
+ context=ha.Context(user_id="mock-id"),
+ ),
+ )
assert err.value.context.user_id == "mock-id"
assert err.value.entity_id == "light.kitchen"
@@ -997,3 +1038,28 @@ async def test_async_extract_entities_warn_referenced(hass, caplog):
"Unable to find referenced areas non-existent-area, devices non-existent-device, entities non.existent"
in caplog.text
)
+
+
+async def test_async_extract_config_entry_ids(hass):
+ """Test we can find devices that have no entities."""
+
+ device_no_entities = dev_reg.DeviceEntry(
+ id="device-no-entities", config_entries={"abc"}
+ )
+
+ call = ha.ServiceCall(
+ "homeassistant",
+ "reload_config_entry",
+ {
+ "device_id": "device-no-entities",
+ },
+ )
+
+ mock_device_registry(
+ hass,
+ {
+ device_no_entities.id: device_no_entities,
+ },
+ )
+
+ assert await service.async_extract_config_entry_ids(hass, call) == {"abc"}
diff --git a/tests/helpers/test_significant_change.py b/tests/helpers/test_significant_change.py
index e72951d36dd2f9..79f3dd3fe3e4bd 100644
--- a/tests/helpers/test_significant_change.py
+++ b/tests/helpers/test_significant_change.py
@@ -5,7 +5,6 @@
from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import State
from homeassistant.helpers import significant_change
-from homeassistant.setup import async_setup_component
@pytest.fixture(name="checker")
@@ -26,8 +25,6 @@ def async_check_significant_change(
async def test_signicant_change(hass, checker):
"""Test initialize helper works."""
- assert await async_setup_component(hass, "sensor", {})
-
ent_id = "test_domain.test_entity"
attrs = {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY}
@@ -48,3 +45,30 @@ async def test_signicant_change(hass, checker):
# State turned unavailable
assert checker.async_is_significant_change(State(ent_id, "100", attrs))
assert checker.async_is_significant_change(State(ent_id, STATE_UNAVAILABLE, attrs))
+
+
+async def test_significant_change_extra(hass, checker):
+ """Test extra significant checker works."""
+ ent_id = "test_domain.test_entity"
+ attrs = {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY}
+
+ assert checker.async_is_significant_change(State(ent_id, "100", attrs), extra_arg=1)
+ assert checker.async_is_significant_change(State(ent_id, "200", attrs), extra_arg=1)
+
+ # Reset the last significiant change to 100 to repeat test but with
+ # extra checker installed.
+ assert checker.async_is_significant_change(State(ent_id, "100", attrs), extra_arg=1)
+
+ def extra_significant_check(
+ hass, old_state, old_attrs, old_extra_arg, new_state, new_attrs, new_extra_arg
+ ):
+ return old_extra_arg != new_extra_arg
+
+ checker.extra_significant_check = extra_significant_check
+
+ # This is normally a significant change (100 -> 200), but the extra arg check marks it
+ # as insignificant.
+ assert not checker.async_is_significant_change(
+ State(ent_id, "200", attrs), extra_arg=1
+ )
+ assert checker.async_is_significant_change(State(ent_id, "200", attrs), extra_arg=2)
diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py
index 89b0f3c685088f..aa1d148fbd7ec8 100644
--- a/tests/helpers/test_state.py
+++ b/tests/helpers/test_state.py
@@ -25,7 +25,7 @@
from tests.common import async_mock_service
-async def test_async_track_states(hass):
+async def test_async_track_states(hass, mock_integration_frame):
"""Test AsyncTrackStates context manager."""
point1 = dt_util.utcnow()
point2 = point1 + timedelta(seconds=5)
@@ -82,7 +82,7 @@ async def test_call_to_component(hass):
)
-async def test_get_changed_since(hass):
+async def test_get_changed_since(hass, mock_integration_frame):
"""Test get_changed_since."""
point1 = dt_util.utcnow()
point2 = point1 + timedelta(seconds=5)
diff --git a/tests/helpers/test_sun.py b/tests/helpers/test_sun.py
index b8ecd1ed86aa96..84545bf43b60a0 100644
--- a/tests/helpers/test_sun.py
+++ b/tests/helpers/test_sun.py
@@ -11,18 +11,19 @@
def test_next_events(hass):
"""Test retrieving next sun events."""
utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC)
- from astral import Astral
+ from astral import LocationInfo
+ import astral.sun
- astral = Astral()
utc_today = utc_now.date()
- latitude = hass.config.latitude
- longitude = hass.config.longitude
+ location = LocationInfo(
+ latitude=hass.config.latitude, longitude=hass.config.longitude
+ )
mod = -1
while True:
- next_dawn = astral.dawn_utc(
- utc_today + timedelta(days=mod), latitude, longitude
+ next_dawn = astral.sun.dawn(
+ location.observer, date=utc_today + timedelta(days=mod)
)
if next_dawn > utc_now:
break
@@ -30,8 +31,8 @@ def test_next_events(hass):
mod = -1
while True:
- next_dusk = astral.dusk_utc(
- utc_today + timedelta(days=mod), latitude, longitude
+ next_dusk = astral.sun.dusk(
+ location.observer, date=utc_today + timedelta(days=mod)
)
if next_dusk > utc_now:
break
@@ -39,8 +40,8 @@ def test_next_events(hass):
mod = -1
while True:
- next_midnight = astral.solar_midnight_utc(
- utc_today + timedelta(days=mod), longitude
+ next_midnight = astral.sun.midnight(
+ location.observer, date=utc_today + timedelta(days=mod)
)
if next_midnight > utc_now:
break
@@ -48,15 +49,17 @@ def test_next_events(hass):
mod = -1
while True:
- next_noon = astral.solar_noon_utc(utc_today + timedelta(days=mod), longitude)
+ next_noon = astral.sun.noon(
+ location.observer, date=utc_today + timedelta(days=mod)
+ )
if next_noon > utc_now:
break
mod += 1
mod = -1
while True:
- next_rising = astral.sunrise_utc(
- utc_today + timedelta(days=mod), latitude, longitude
+ next_rising = astral.sun.sunrise(
+ location.observer, date=utc_today + timedelta(days=mod)
)
if next_rising > utc_now:
break
@@ -64,8 +67,8 @@ def test_next_events(hass):
mod = -1
while True:
- next_setting = astral.sunset_utc(
- utc_today + timedelta(days=mod), latitude, longitude
+ next_setting = astral.sun.sunset(
+ location.observer, utc_today + timedelta(days=mod)
)
if next_setting > utc_now:
break
@@ -74,8 +77,8 @@ def test_next_events(hass):
with patch("homeassistant.helpers.condition.dt_util.utcnow", return_value=utc_now):
assert next_dawn == sun.get_astral_event_next(hass, "dawn")
assert next_dusk == sun.get_astral_event_next(hass, "dusk")
- assert next_midnight == sun.get_astral_event_next(hass, "solar_midnight")
- assert next_noon == sun.get_astral_event_next(hass, "solar_noon")
+ assert next_midnight == sun.get_astral_event_next(hass, "midnight")
+ assert next_noon == sun.get_astral_event_next(hass, "noon")
assert next_rising == sun.get_astral_event_next(hass, SUN_EVENT_SUNRISE)
assert next_setting == sun.get_astral_event_next(hass, SUN_EVENT_SUNSET)
@@ -83,25 +86,26 @@ def test_next_events(hass):
def test_date_events(hass):
"""Test retrieving next sun events."""
utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC)
- from astral import Astral
+ from astral import LocationInfo
+ import astral.sun
- astral = Astral()
utc_today = utc_now.date()
- latitude = hass.config.latitude
- longitude = hass.config.longitude
+ location = LocationInfo(
+ latitude=hass.config.latitude, longitude=hass.config.longitude
+ )
- dawn = astral.dawn_utc(utc_today, latitude, longitude)
- dusk = astral.dusk_utc(utc_today, latitude, longitude)
- midnight = astral.solar_midnight_utc(utc_today, longitude)
- noon = astral.solar_noon_utc(utc_today, longitude)
- sunrise = astral.sunrise_utc(utc_today, latitude, longitude)
- sunset = astral.sunset_utc(utc_today, latitude, longitude)
+ dawn = astral.sun.dawn(location.observer, utc_today)
+ dusk = astral.sun.dusk(location.observer, utc_today)
+ midnight = astral.sun.midnight(location.observer, utc_today)
+ noon = astral.sun.noon(location.observer, utc_today)
+ sunrise = astral.sun.sunrise(location.observer, utc_today)
+ sunset = astral.sun.sunset(location.observer, utc_today)
assert dawn == sun.get_astral_event_date(hass, "dawn", utc_today)
assert dusk == sun.get_astral_event_date(hass, "dusk", utc_today)
- assert midnight == sun.get_astral_event_date(hass, "solar_midnight", utc_today)
- assert noon == sun.get_astral_event_date(hass, "solar_noon", utc_today)
+ assert midnight == sun.get_astral_event_date(hass, "midnight", utc_today)
+ assert noon == sun.get_astral_event_date(hass, "noon", utc_today)
assert sunrise == sun.get_astral_event_date(hass, SUN_EVENT_SUNRISE, utc_today)
assert sunset == sun.get_astral_event_date(hass, SUN_EVENT_SUNSET, utc_today)
@@ -109,26 +113,27 @@ def test_date_events(hass):
def test_date_events_default_date(hass):
"""Test retrieving next sun events."""
utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC)
- from astral import Astral
+ from astral import LocationInfo
+ import astral.sun
- astral = Astral()
utc_today = utc_now.date()
- latitude = hass.config.latitude
- longitude = hass.config.longitude
+ location = LocationInfo(
+ latitude=hass.config.latitude, longitude=hass.config.longitude
+ )
- dawn = astral.dawn_utc(utc_today, latitude, longitude)
- dusk = astral.dusk_utc(utc_today, latitude, longitude)
- midnight = astral.solar_midnight_utc(utc_today, longitude)
- noon = astral.solar_noon_utc(utc_today, longitude)
- sunrise = astral.sunrise_utc(utc_today, latitude, longitude)
- sunset = astral.sunset_utc(utc_today, latitude, longitude)
+ dawn = astral.sun.dawn(location.observer, date=utc_today)
+ dusk = astral.sun.dusk(location.observer, date=utc_today)
+ midnight = astral.sun.midnight(location.observer, date=utc_today)
+ noon = astral.sun.noon(location.observer, date=utc_today)
+ sunrise = astral.sun.sunrise(location.observer, date=utc_today)
+ sunset = astral.sun.sunset(location.observer, date=utc_today)
with patch("homeassistant.util.dt.now", return_value=utc_now):
assert dawn == sun.get_astral_event_date(hass, "dawn", utc_today)
assert dusk == sun.get_astral_event_date(hass, "dusk", utc_today)
- assert midnight == sun.get_astral_event_date(hass, "solar_midnight", utc_today)
- assert noon == sun.get_astral_event_date(hass, "solar_noon", utc_today)
+ assert midnight == sun.get_astral_event_date(hass, "midnight", utc_today)
+ assert noon == sun.get_astral_event_date(hass, "noon", utc_today)
assert sunrise == sun.get_astral_event_date(hass, SUN_EVENT_SUNRISE, utc_today)
assert sunset == sun.get_astral_event_date(hass, SUN_EVENT_SUNSET, utc_today)
@@ -136,25 +141,26 @@ def test_date_events_default_date(hass):
def test_date_events_accepts_datetime(hass):
"""Test retrieving next sun events."""
utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC)
- from astral import Astral
+ from astral import LocationInfo
+ import astral.sun
- astral = Astral()
utc_today = utc_now.date()
- latitude = hass.config.latitude
- longitude = hass.config.longitude
+ location = LocationInfo(
+ latitude=hass.config.latitude, longitude=hass.config.longitude
+ )
- dawn = astral.dawn_utc(utc_today, latitude, longitude)
- dusk = astral.dusk_utc(utc_today, latitude, longitude)
- midnight = astral.solar_midnight_utc(utc_today, longitude)
- noon = astral.solar_noon_utc(utc_today, longitude)
- sunrise = astral.sunrise_utc(utc_today, latitude, longitude)
- sunset = astral.sunset_utc(utc_today, latitude, longitude)
+ dawn = astral.sun.dawn(location.observer, date=utc_today)
+ dusk = astral.sun.dusk(location.observer, date=utc_today)
+ midnight = astral.sun.midnight(location.observer, date=utc_today)
+ noon = astral.sun.noon(location.observer, date=utc_today)
+ sunrise = astral.sun.sunrise(location.observer, date=utc_today)
+ sunset = astral.sun.sunset(location.observer, date=utc_today)
assert dawn == sun.get_astral_event_date(hass, "dawn", utc_now)
assert dusk == sun.get_astral_event_date(hass, "dusk", utc_now)
- assert midnight == sun.get_astral_event_date(hass, "solar_midnight", utc_now)
- assert noon == sun.get_astral_event_date(hass, "solar_noon", utc_now)
+ assert midnight == sun.get_astral_event_date(hass, "midnight", utc_now)
+ assert noon == sun.get_astral_event_date(hass, "noon", utc_now)
assert sunrise == sun.get_astral_event_date(hass, SUN_EVENT_SUNRISE, utc_now)
assert sunset == sun.get_astral_event_date(hass, SUN_EVENT_SUNSET, utc_now)
@@ -184,10 +190,10 @@ def test_norway_in_june(hass):
print(sun.get_astral_event_date(hass, SUN_EVENT_SUNSET, datetime(2017, 7, 26)))
assert sun.get_astral_event_next(hass, SUN_EVENT_SUNRISE, june) == datetime(
- 2016, 7, 25, 23, 23, 39, tzinfo=dt_util.UTC
+ 2016, 7, 24, 22, 59, 45, 689645, tzinfo=dt_util.UTC
)
assert sun.get_astral_event_next(hass, SUN_EVENT_SUNSET, june) == datetime(
- 2016, 7, 26, 22, 19, 1, tzinfo=dt_util.UTC
+ 2016, 7, 25, 22, 17, 13, 503932, tzinfo=dt_util.UTC
)
assert sun.get_astral_event_date(hass, SUN_EVENT_SUNRISE, june) is None
assert sun.get_astral_event_date(hass, SUN_EVENT_SUNSET, june) is None
diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py
index 174d61ea47049c..a8924f513c6e23 100644
--- a/tests/helpers/test_template.py
+++ b/tests/helpers/test_template.py
@@ -24,6 +24,8 @@
import homeassistant.util.dt as dt_util
from homeassistant.util.unit_system import UnitSystem
+from tests.common import MockConfigEntry, mock_device_registry, mock_registry
+
def _set_up_units(hass):
"""Set up the tests."""
@@ -878,41 +880,38 @@ def test_relative_time(mock_is_safe, hass):
"""Test relative_time method."""
now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
with patch("homeassistant.util.dt.now", return_value=now):
- assert (
- "1 hour"
- == template.Template(
- '{{relative_time(strptime("2000-01-01 09:00:00", "%Y-%m-%d %H:%M:%S"))}}',
- hass,
- ).async_render()
- )
- assert (
- "2 hours"
- == template.Template(
- '{{relative_time(strptime("2000-01-01 09:00:00 +01:00", "%Y-%m-%d %H:%M:%S %z"))}}',
- hass,
- ).async_render()
- )
- assert (
- "1 hour"
- == template.Template(
- '{{relative_time(strptime("2000-01-01 03:00:00 -06:00", "%Y-%m-%d %H:%M:%S %z"))}}',
- hass,
- ).async_render()
- )
- assert (
- str(template.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z"))
- == template.Template(
- '{{relative_time(strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z"))}}',
- hass,
- ).async_render()
- )
- assert (
- "string"
- == template.Template(
- '{{relative_time("string")}}',
- hass,
- ).async_render()
+ result = template.Template(
+ '{{relative_time(strptime("2000-01-01 09:00:00", "%Y-%m-%d %H:%M:%S"))}}',
+ hass,
+ ).async_render()
+ assert result == "1 hour"
+
+ result = template.Template(
+ '{{relative_time(strptime("2000-01-01 09:00:00 +01:00", "%Y-%m-%d %H:%M:%S %z"))}}',
+ hass,
+ ).async_render()
+ assert result == "2 hours"
+
+ result = template.Template(
+ '{{relative_time(strptime("2000-01-01 03:00:00 -06:00", "%Y-%m-%d %H:%M:%S %z"))}}',
+ hass,
+ ).async_render()
+ assert result == "1 hour"
+
+ result1 = str(
+ template.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
)
+ result2 = template.Template(
+ '{{relative_time(strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z"))}}',
+ hass,
+ ).async_render()
+ assert result1 == result2
+
+ result = template.Template(
+ '{{relative_time("string")}}',
+ hass,
+ ).async_render()
+ assert result == "string"
@patch(
@@ -923,55 +922,46 @@ def test_timedelta(mock_is_safe, hass):
"""Test relative_time method."""
now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
with patch("homeassistant.util.dt.now", return_value=now):
- assert (
- "0:02:00"
- == template.Template(
- "{{timedelta(seconds=120)}}",
- hass,
- ).async_render()
- )
- assert (
- "1 day, 0:00:00"
- == template.Template(
- "{{timedelta(seconds=86400)}}",
- hass,
- ).async_render()
- )
- assert (
- "1 day, 4:00:00"
- == template.Template(
- "{{timedelta(days=1, hours=4)}}",
- hass,
- ).async_render()
- )
- assert (
- "1 hour"
- == template.Template(
- "{{relative_time(now() - timedelta(seconds=3600))}}",
- hass,
- ).async_render()
- )
- assert (
- "1 day"
- == template.Template(
- "{{relative_time(now() - timedelta(seconds=86400))}}",
- hass,
- ).async_render()
- )
- assert (
- "1 day"
- == template.Template(
- "{{relative_time(now() - timedelta(seconds=86401))}}",
- hass,
- ).async_render()
- )
- assert (
- "15 days"
- == template.Template(
- "{{relative_time(now() - timedelta(weeks=2, days=1))}}",
- hass,
- ).async_render()
- )
+ result = template.Template(
+ "{{timedelta(seconds=120)}}",
+ hass,
+ ).async_render()
+ assert result == "0:02:00"
+
+ result = template.Template(
+ "{{timedelta(seconds=86400)}}",
+ hass,
+ ).async_render()
+ assert result == "1 day, 0:00:00"
+
+ result = template.Template(
+ "{{timedelta(days=1, hours=4)}}", hass
+ ).async_render()
+ assert result == "1 day, 4:00:00"
+
+ result = template.Template(
+ "{{relative_time(now() - timedelta(seconds=3600))}}",
+ hass,
+ ).async_render()
+ assert result == "1 hour"
+
+ result = template.Template(
+ "{{relative_time(now() - timedelta(seconds=86400))}}",
+ hass,
+ ).async_render()
+ assert result == "1 day"
+
+ result = template.Template(
+ "{{relative_time(now() - timedelta(seconds=86401))}}",
+ hass,
+ ).async_render()
+ assert result == "1 day"
+
+ result = template.Template(
+ "{{relative_time(now() - timedelta(weeks=2, days=1))}}",
+ hass,
+ ).async_render()
+ assert result == "15 days"
def test_regex_match(hass):
@@ -1470,6 +1460,79 @@ async def test_expand(hass):
assert info.rate_limit is None
+async def test_device_entities(hass):
+ """Test expand function."""
+ config_entry = MockConfigEntry(domain="light")
+ device_registry = mock_device_registry(hass)
+ entity_registry = mock_registry(hass)
+
+ # Test non existing device ids
+ info = render_to_info(hass, "{{ device_entities('abc123') }}")
+ assert_result_info(info, [])
+ assert info.rate_limit is None
+
+ info = render_to_info(hass, "{{ device_entities(56) }}")
+ assert_result_info(info, [])
+ assert info.rate_limit is None
+
+ # Test device without entities
+ device_entry = device_registry.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={("mac", "12:34:56:AB:CD:EF")},
+ )
+ info = render_to_info(hass, f"{{{{ device_entities('{device_entry.id}') }}}}")
+ assert_result_info(info, [])
+ assert info.rate_limit is None
+
+ # Test device with single entity, which has no state
+ entity_registry.async_get_or_create(
+ "light",
+ "hue",
+ "5678",
+ config_entry=config_entry,
+ device_id=device_entry.id,
+ )
+ info = render_to_info(hass, f"{{{{ device_entities('{device_entry.id}') }}}}")
+ assert_result_info(info, ["light.hue_5678"], [])
+ assert info.rate_limit is None
+ info = render_to_info(
+ hass,
+ f"{{{{ device_entities('{device_entry.id}') | expand | map(attribute='entity_id') | join(', ') }}}}",
+ )
+ assert_result_info(info, "", ["light.hue_5678"])
+ assert info.rate_limit is None
+
+ # Test device with single entity, with state
+ hass.states.async_set("light.hue_5678", "happy")
+ info = render_to_info(
+ hass,
+ f"{{{{ device_entities('{device_entry.id}') | expand | map(attribute='entity_id') | join(', ') }}}}",
+ )
+ assert_result_info(info, "light.hue_5678", ["light.hue_5678"])
+ assert info.rate_limit is None
+
+ # Test device with multiple entities, which have a state
+ entity_registry.async_get_or_create(
+ "light",
+ "hue",
+ "ABCD",
+ config_entry=config_entry,
+ device_id=device_entry.id,
+ )
+ hass.states.async_set("light.hue_abcd", "camper")
+ info = render_to_info(hass, f"{{{{ device_entities('{device_entry.id}') }}}}")
+ assert_result_info(info, ["light.hue_5678", "light.hue_abcd"], [])
+ assert info.rate_limit is None
+ info = render_to_info(
+ hass,
+ f"{{{{ device_entities('{device_entry.id}') | expand | map(attribute='entity_id') | join(', ') }}}}",
+ )
+ assert_result_info(
+ info, "light.hue_5678, light.hue_abcd", ["light.hue_5678", "light.hue_abcd"]
+ )
+ assert info.rate_limit is None
+
+
def test_closest_function_to_coord(hass):
"""Test closest function to coord."""
hass.states.async_set(
@@ -2434,3 +2497,13 @@ async def test_parse_result(hass):
("0011101.00100001010001", "0011101.00100001010001"),
):
assert template.Template(tpl, hass).async_render() == result
+
+
+async def test_undefined_variable(hass, caplog):
+ """Test a warning is logged on undefined variables."""
+ tpl = template.Template("{{ no_such_variable }}", hass)
+ assert tpl.async_render() == ""
+ assert (
+ "Template variable warning: 'no_such_variable' is undefined when rendering '{{ no_such_variable }}'"
+ in caplog.text
+ )
diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py
index e9754f83a264b4..391f2be38ec353 100644
--- a/tests/helpers/test_update_coordinator.py
+++ b/tests/helpers/test_update_coordinator.py
@@ -11,6 +11,7 @@
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import CoreState
+from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import update_coordinator
from homeassistant.util.dt import utcnow
@@ -18,6 +19,36 @@
_LOGGER = logging.getLogger(__name__)
+KNOWN_ERRORS = [
+ (asyncio.TimeoutError, asyncio.TimeoutError, "Timeout fetching test data"),
+ (
+ requests.exceptions.Timeout,
+ requests.exceptions.Timeout,
+ "Timeout fetching test data",
+ ),
+ (
+ urllib.error.URLError("timed out"),
+ urllib.error.URLError,
+ "Timeout fetching test data",
+ ),
+ (aiohttp.ClientError, aiohttp.ClientError, "Error requesting test data"),
+ (
+ requests.exceptions.RequestException,
+ requests.exceptions.RequestException,
+ "Error requesting test data",
+ ),
+ (
+ urllib.error.URLError("something"),
+ urllib.error.URLError,
+ "Error requesting test data",
+ ),
+ (
+ update_coordinator.UpdateFailed,
+ update_coordinator.UpdateFailed,
+ "Error fetching test data",
+ ),
+]
+
def get_crd(hass, update_interval):
"""Make coordinator mocks."""
@@ -113,15 +144,7 @@ async def test_request_refresh_no_auto_update(crd_without_update_interval):
@pytest.mark.parametrize(
"err_msg",
- [
- (asyncio.TimeoutError, "Timeout fetching test data"),
- (requests.exceptions.Timeout, "Timeout fetching test data"),
- (urllib.error.URLError("timed out"), "Timeout fetching test data"),
- (aiohttp.ClientError, "Error requesting test data"),
- (requests.exceptions.RequestException, "Error requesting test data"),
- (urllib.error.URLError("something"), "Error requesting test data"),
- (update_coordinator.UpdateFailed, "Error fetching test data"),
- ],
+ KNOWN_ERRORS,
)
async def test_refresh_known_errors(err_msg, crd, caplog):
"""Test raising known errors."""
@@ -131,7 +154,8 @@ async def test_refresh_known_errors(err_msg, crd, caplog):
assert crd.data is None
assert crd.last_update_success is False
- assert err_msg[1] in caplog.text
+ assert isinstance(crd.last_exception, err_msg[1])
+ assert err_msg[2] in caplog.text
async def test_refresh_fail_unknown(crd, caplog):
@@ -310,3 +334,31 @@ async def test_stop_refresh_on_ha_stop(hass, crd):
async_fire_time_changed(hass, utcnow() + update_interval)
await hass.async_block_till_done()
assert crd.data == 1
+
+
+@pytest.mark.parametrize(
+ "err_msg",
+ KNOWN_ERRORS,
+)
+async def test_async_config_entry_first_refresh_failure(err_msg, crd, caplog):
+ """Test async_config_entry_first_refresh raises ConfigEntryNotReady on failure.
+
+ Verify we do not log the exception since raising ConfigEntryNotReady
+ will be caught by config_entries.async_setup which will log it with
+ a decreasing level of logging once the first message is logged.
+ """
+ crd.update_method = AsyncMock(side_effect=err_msg[0])
+
+ with pytest.raises(ConfigEntryNotReady):
+ await crd.async_config_entry_first_refresh()
+
+ assert crd.last_update_success is False
+ assert isinstance(crd.last_exception, err_msg[1])
+ assert err_msg[2] not in caplog.text
+
+
+async def test_async_config_entry_first_refresh_success(crd, caplog):
+ """Test first refresh successfully."""
+ await crd.async_config_entry_first_refresh()
+
+ assert crd.last_update_success is True
diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py
index 6eaaee87af0ecb..ea6048dfc9e702 100644
--- a/tests/scripts/test_check_config.py
+++ b/tests/scripts/test_check_config.py
@@ -1,5 +1,4 @@
"""Test check_config script."""
-import logging
from unittest.mock import patch
import pytest
@@ -9,8 +8,6 @@
from tests.common import get_test_config_dir, patch_yaml_files
-_LOGGER = logging.getLogger(__name__)
-
BASE_CONFIG = (
"homeassistant:\n"
" name: Home\n"
diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py
index fc653c25d0bae8..24646386278974 100644
--- a/tests/test_bootstrap.py
+++ b/tests/test_bootstrap.py
@@ -71,6 +71,7 @@ async def test_load_hassio(hass):
assert bootstrap._get_domains(hass, {}) == {"hassio"}
+@pytest.mark.parametrize("load_registries", [False])
async def test_empty_setup(hass):
"""Test an empty set up loads the core."""
await bootstrap.async_from_config_dict({}, hass)
@@ -91,6 +92,7 @@ async def test_core_failure_loads_safe_mode(hass, caplog):
assert "group" not in hass.config.components
+@pytest.mark.parametrize("load_registries", [False])
async def test_setting_up_config(hass):
"""Test we set up domains in config."""
await bootstrap._async_set_up_integrations(
@@ -100,6 +102,7 @@ async def test_setting_up_config(hass):
assert "group" in hass.config.components
+@pytest.mark.parametrize("load_registries", [False])
async def test_setup_after_deps_all_present(hass):
"""Test after_dependencies when all present."""
order = []
@@ -144,6 +147,7 @@ async def async_setup(hass, config):
assert order == ["logger", "root", "first_dep", "second_dep"]
+@pytest.mark.parametrize("load_registries", [False])
async def test_setup_after_deps_in_stage_1_ignored(hass):
"""Test after_dependencies are ignored in stage 1."""
# This test relies on this
@@ -190,6 +194,7 @@ async def async_setup(hass, config):
assert order == ["cloud", "an_after_dep", "normal_integration"]
+@pytest.mark.parametrize("load_registries", [False])
async def test_setup_after_deps_via_platform(hass):
"""Test after_dependencies set up via platform."""
order = []
@@ -239,6 +244,7 @@ def continue_loading(_):
assert order == ["after_dep_of_platform_int", "platform_int"]
+@pytest.mark.parametrize("load_registries", [False])
async def test_setup_after_deps_not_trigger_load(hass):
"""Test after_dependencies does not trigger loading it."""
order = []
@@ -277,6 +283,7 @@ async def async_setup(hass, config):
assert "second_dep" in hass.config.components
+@pytest.mark.parametrize("load_registries", [False])
async def test_setup_after_deps_not_present(hass):
"""Test after_dependencies when referenced integration doesn't exist."""
order = []
@@ -424,7 +431,9 @@ async def _async_setup_that_blocks_startup(*args, **kwargs):
with patch(
"homeassistant.config.async_hass_config_yaml",
return_value={"browser": {}, "frontend": {}},
- ), patch.object(bootstrap, "LOG_SLOW_STARTUP_INTERVAL", 0.3), patch(
+ ), patch.object(bootstrap, "LOG_SLOW_STARTUP_INTERVAL", 0.3), patch.object(
+ bootstrap, "SLOW_STARTUP_CHECK_INTERVAL", 0.05
+ ), patch(
"homeassistant.components.frontend.async_setup",
side_effect=_async_setup_that_blocks_startup,
):
diff --git a/tests/test_config.py b/tests/test_config.py
index 7dd7d61e8efab6..299cf9caa7386e 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -1088,6 +1088,26 @@ async def test_component_config_exceptions(hass, caplog):
in caplog.text
)
+ # get_component raising
+ caplog.clear()
+ assert (
+ await config_util.async_process_component_config(
+ hass,
+ {"test_domain": {}},
+ integration=Mock(
+ pkg_path="homeassistant.components.test_domain",
+ domain="test_domain",
+ get_component=Mock(
+ side_effect=FileNotFoundError(
+ "No such file or directory: b'liblibc.a'"
+ )
+ ),
+ ),
+ )
+ is None
+ )
+ assert "Unable to import test_domain: No such file or directory" in caplog.text
+
@pytest.mark.parametrize(
"domain, schema, expected",
diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py
index 24444f6a6c0a73..c35ba61a7670a5 100644
--- a/tests/test_config_entries.py
+++ b/tests/test_config_entries.py
@@ -6,8 +6,10 @@
import pytest
from homeassistant import config_entries, data_entry_flow, loader
-from homeassistant.core import callback
-from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
+from homeassistant.core import CoreState, callback
+from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util import dt
@@ -43,7 +45,7 @@ class MockFlowHandler(config_entries.ConfigFlow):
def manager(hass):
"""Fixture of a loaded config manager."""
manager = config_entries.ConfigEntries(hass, {})
- manager._entries = []
+ manager._entries = {}
manager._store._async_ensure_stop_listener = lambda: None
hass.config_entries = manager
return manager
@@ -299,7 +301,7 @@ async def mock_setup_entry_platform(hass, entry, async_add_entities):
assert len(hass.states.async_all()) == 1
# Check entity got added to entity registry
- ent_reg = await hass.helpers.entity_registry.async_get_registry()
+ ent_reg = er.async_get(hass)
assert len(ent_reg.entities) == 1
entity_entry = list(ent_reg.entities.values())[0]
assert entity_entry.config_entry_id == entry.entry_id
@@ -472,7 +474,7 @@ async def test_entries_gets_entries(manager):
assert manager.async_entries("test2") == [entry1, entry2]
-async def test_domains_gets_uniques(manager):
+async def test_domains_gets_domains_uniques(manager):
"""Test we only return each domain once."""
MockConfigEntry(domain="test").add_to_manager(manager)
MockConfigEntry(domain="test2").add_to_manager(manager)
@@ -483,6 +485,46 @@ async def test_domains_gets_uniques(manager):
assert manager.async_domains() == ["test", "test2", "test3"]
+async def test_domains_gets_domains_excludes_ignore_and_disabled(manager):
+ """Test we only return each domain once."""
+ MockConfigEntry(domain="test").add_to_manager(manager)
+ MockConfigEntry(domain="test2").add_to_manager(manager)
+ MockConfigEntry(domain="test2").add_to_manager(manager)
+ MockConfigEntry(
+ domain="ignored", source=config_entries.SOURCE_IGNORE
+ ).add_to_manager(manager)
+ MockConfigEntry(domain="test3").add_to_manager(manager)
+ MockConfigEntry(domain="disabled", disabled_by="user").add_to_manager(manager)
+ assert manager.async_domains() == ["test", "test2", "test3"]
+ assert manager.async_domains(include_ignore=False) == ["test", "test2", "test3"]
+ assert manager.async_domains(include_disabled=False) == ["test", "test2", "test3"]
+ assert manager.async_domains(include_ignore=False, include_disabled=False) == [
+ "test",
+ "test2",
+ "test3",
+ ]
+
+ assert manager.async_domains(include_ignore=True) == [
+ "test",
+ "test2",
+ "ignored",
+ "test3",
+ ]
+ assert manager.async_domains(include_disabled=True) == [
+ "test",
+ "test2",
+ "test3",
+ "disabled",
+ ]
+ assert manager.async_domains(include_ignore=True, include_disabled=True) == [
+ "test",
+ "test2",
+ "ignored",
+ "test3",
+ "disabled",
+ ]
+
+
async def test_saving_and_loading(hass):
"""Test that we're saving and loading correctly."""
mock_integration(
@@ -794,7 +836,9 @@ async def test_setup_raise_not_ready(hass, caplog):
"""Test a setup raising not ready."""
entry = MockConfigEntry(title="test_title", domain="test")
- mock_setup_entry = AsyncMock(side_effect=ConfigEntryNotReady)
+ mock_setup_entry = AsyncMock(
+ side_effect=ConfigEntryNotReady("The internet connection is offline")
+ )
mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry))
mock_entity_platform(hass, "config_flow.test", None)
@@ -802,7 +846,10 @@ async def test_setup_raise_not_ready(hass, caplog):
await entry.async_setup(hass)
assert len(mock_call.mock_calls) == 1
- assert "Config entry 'test_title' for test integration not ready yet" in caplog.text
+ assert (
+ "Config entry 'test_title' for test integration not ready yet: The internet connection is offline"
+ in caplog.text
+ )
p_hass, p_wait_time, p_setup = mock_call.mock_calls[0][1]
assert p_hass is hass
@@ -816,6 +863,28 @@ async def test_setup_raise_not_ready(hass, caplog):
assert entry.state == config_entries.ENTRY_STATE_LOADED
+async def test_setup_raise_not_ready_from_exception(hass, caplog):
+ """Test a setup raising not ready from another exception."""
+ entry = MockConfigEntry(title="test_title", domain="test")
+
+ original_exception = HomeAssistantError("The device dropped the connection")
+ config_entry_exception = ConfigEntryNotReady()
+ config_entry_exception.__cause__ = original_exception
+
+ mock_setup_entry = AsyncMock(side_effect=config_entry_exception)
+ mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry))
+ mock_entity_platform(hass, "config_flow.test", None)
+
+ with patch("homeassistant.helpers.event.async_call_later") as mock_call:
+ await entry.async_setup(hass)
+
+ assert len(mock_call.mock_calls) == 1
+ assert (
+ "Config entry 'test_title' for test integration not ready yet: The device dropped the connection"
+ in caplog.text
+ )
+
+
async def test_setup_retrying_during_unload(hass):
"""Test if we unload an entry that is in retry mode."""
entry = MockConfigEntry(domain="test")
@@ -836,6 +905,33 @@ async def test_setup_retrying_during_unload(hass):
assert len(mock_call.return_value.mock_calls) == 1
+async def test_setup_retrying_during_unload_before_started(hass):
+ """Test if we unload an entry that is in retry mode before started."""
+ entry = MockConfigEntry(domain="test")
+ hass.state = CoreState.starting
+ initial_listeners = hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED]
+
+ mock_setup_entry = AsyncMock(side_effect=ConfigEntryNotReady)
+ mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry))
+ mock_entity_platform(hass, "config_flow.test", None)
+
+ await entry.async_setup(hass)
+ await hass.async_block_till_done()
+
+ assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY
+ assert (
+ hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] == initial_listeners + 1
+ )
+
+ await entry.async_unload(hass)
+ await hass.async_block_till_done()
+
+ assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED
+ assert (
+ hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] == initial_listeners + 0
+ )
+
+
async def test_entry_options(hass, manager):
"""Test that we can set options on an entry."""
entry = MockConfigEntry(domain="test", data={"first": True}, options=None)
@@ -1108,6 +1204,110 @@ async def test_entry_reload_error(hass, manager, state):
assert entry.state == state
+async def test_entry_disable_succeed(hass, manager):
+ """Test that we can disable an entry."""
+ entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED)
+ entry.add_to_hass(hass)
+
+ async_setup = AsyncMock(return_value=True)
+ async_setup_entry = AsyncMock(return_value=True)
+ async_unload_entry = AsyncMock(return_value=True)
+
+ mock_integration(
+ hass,
+ MockModule(
+ "comp",
+ async_setup=async_setup,
+ async_setup_entry=async_setup_entry,
+ async_unload_entry=async_unload_entry,
+ ),
+ )
+ mock_entity_platform(hass, "config_flow.comp", None)
+
+ # Disable
+ assert await manager.async_set_disabled_by(
+ entry.entry_id, config_entries.DISABLED_USER
+ )
+ assert len(async_unload_entry.mock_calls) == 1
+ assert len(async_setup.mock_calls) == 0
+ assert len(async_setup_entry.mock_calls) == 0
+ assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED
+
+ # Enable
+ assert await manager.async_set_disabled_by(entry.entry_id, None)
+ assert len(async_unload_entry.mock_calls) == 1
+ assert len(async_setup.mock_calls) == 1
+ assert len(async_setup_entry.mock_calls) == 1
+ assert entry.state == config_entries.ENTRY_STATE_LOADED
+
+
+async def test_entry_disable_without_reload_support(hass, manager):
+ """Test that we can disable an entry without reload support."""
+ entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED)
+ entry.add_to_hass(hass)
+
+ async_setup = AsyncMock(return_value=True)
+ async_setup_entry = AsyncMock(return_value=True)
+
+ mock_integration(
+ hass,
+ MockModule(
+ "comp",
+ async_setup=async_setup,
+ async_setup_entry=async_setup_entry,
+ ),
+ )
+ mock_entity_platform(hass, "config_flow.comp", None)
+
+ # Disable
+ assert not await manager.async_set_disabled_by(
+ entry.entry_id, config_entries.DISABLED_USER
+ )
+ assert len(async_setup.mock_calls) == 0
+ assert len(async_setup_entry.mock_calls) == 0
+ assert entry.state == config_entries.ENTRY_STATE_FAILED_UNLOAD
+
+ # Enable
+ with pytest.raises(config_entries.OperationNotAllowed):
+ await manager.async_set_disabled_by(entry.entry_id, None)
+ assert len(async_setup.mock_calls) == 0
+ assert len(async_setup_entry.mock_calls) == 0
+ assert entry.state == config_entries.ENTRY_STATE_FAILED_UNLOAD
+
+
+async def test_entry_enable_without_reload_support(hass, manager):
+ """Test that we can disable an entry without reload support."""
+ entry = MockConfigEntry(domain="comp", disabled_by=config_entries.DISABLED_USER)
+ entry.add_to_hass(hass)
+
+ async_setup = AsyncMock(return_value=True)
+ async_setup_entry = AsyncMock(return_value=True)
+
+ mock_integration(
+ hass,
+ MockModule(
+ "comp",
+ async_setup=async_setup,
+ async_setup_entry=async_setup_entry,
+ ),
+ )
+ mock_entity_platform(hass, "config_flow.comp", None)
+
+ # Enable
+ assert await manager.async_set_disabled_by(entry.entry_id, None)
+ assert len(async_setup.mock_calls) == 1
+ assert len(async_setup_entry.mock_calls) == 1
+ assert entry.state == config_entries.ENTRY_STATE_LOADED
+
+ # Disable
+ assert not await manager.async_set_disabled_by(
+ entry.entry_id, config_entries.DISABLED_USER
+ )
+ assert len(async_setup.mock_calls) == 1
+ assert len(async_setup_entry.mock_calls) == 1
+ assert entry.state == config_entries.ENTRY_STATE_FAILED_UNLOAD
+
+
async def test_init_custom_integration(hass):
"""Test initializing flow for custom integration."""
integration = loader.Integration(
@@ -1116,12 +1316,11 @@ async def test_init_custom_integration(hass):
None,
{"name": "Hue", "dependencies": [], "requirements": [], "domain": "hue"},
)
- with pytest.raises(data_entry_flow.UnknownHandler):
- with patch(
- "homeassistant.loader.async_get_integration",
- return_value=integration,
- ):
- await hass.config_entries.flow.async_init("bla")
+ with pytest.raises(data_entry_flow.UnknownHandler), patch(
+ "homeassistant.loader.async_get_integration",
+ return_value=integration,
+ ):
+ await hass.config_entries.flow.async_init("bla")
async def test_support_entry_unload(hass):
@@ -1278,6 +1477,43 @@ async def async_step_user(self, user_input=None):
assert len(async_remove_entry.mock_calls) == 1
+async def test_entry_id_existing_entry(hass, manager):
+ """Test that we throw when the entry id collides."""
+ collide_entry_id = "collide"
+ hass.config.components.add("comp")
+ MockConfigEntry(
+ entry_id=collide_entry_id,
+ domain="comp",
+ state=config_entries.ENTRY_STATE_LOADED,
+ unique_id="mock-unique-id",
+ ).add_to_hass(hass)
+
+ mock_integration(
+ hass,
+ MockModule("comp"),
+ )
+ mock_entity_platform(hass, "config_flow.comp", None)
+
+ class TestFlow(config_entries.ConfigFlow):
+ """Test flow."""
+
+ VERSION = 1
+
+ async def async_step_user(self, user_input=None):
+ """Test user step."""
+ return self.async_create_entry(title="mock-title", data={"via": "flow"})
+
+ with pytest.raises(HomeAssistantError), patch.dict(
+ config_entries.HANDLERS, {"comp": TestFlow}
+ ), patch(
+ "homeassistant.config_entries.uuid_util.random_uuid_hex",
+ return_value=collide_entry_id,
+ ):
+ await manager.flow.async_init(
+ "comp", context={"source": config_entries.SOURCE_USER}
+ )
+
+
async def test_unique_id_update_existing_entry_without_reload(hass, manager):
"""Test that we update an entry if there already is an entry with unique ID."""
hass.config.components.add("comp")
@@ -1585,6 +1821,154 @@ async def async_step_user(self, user_input=None):
assert len(async_reload.mock_calls) == 0
+async def test_manual_add_overrides_ignored_entry_singleton(hass, manager):
+ """Test that we can ignore manually add entry, overriding ignored entry."""
+ hass.config.components.add("comp")
+ entry = MockConfigEntry(
+ domain="comp",
+ state=config_entries.ENTRY_STATE_LOADED,
+ source=config_entries.SOURCE_IGNORE,
+ )
+ entry.add_to_hass(hass)
+
+ mock_setup_entry = AsyncMock(return_value=True)
+
+ mock_integration(hass, MockModule("comp", async_setup_entry=mock_setup_entry))
+ mock_entity_platform(hass, "config_flow.comp", None)
+
+ class TestFlow(config_entries.ConfigFlow):
+ """Test flow."""
+
+ VERSION = 1
+
+ async def async_step_user(self, user_input=None):
+ """Test user step."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+ return self.async_create_entry(title="title", data={"token": "supersecret"})
+
+ with patch.dict(config_entries.HANDLERS, {"comp": TestFlow, "beer": 5}):
+ await manager.flow.async_init(
+ "comp", context={"source": config_entries.SOURCE_USER}
+ )
+ await hass.async_block_till_done()
+
+ assert len(mock_setup_entry.mock_calls) == 1
+ p_hass, p_entry = mock_setup_entry.mock_calls[0][1]
+
+ assert p_hass is hass
+ assert p_entry.data == {"token": "supersecret"}
+
+
+async def test__async_current_entries_does_not_skip_ignore_non_user(hass, manager):
+ """Test that _async_current_entries does not skip ignore by default for non user step."""
+ hass.config.components.add("comp")
+ entry = MockConfigEntry(
+ domain="comp",
+ state=config_entries.ENTRY_STATE_LOADED,
+ source=config_entries.SOURCE_IGNORE,
+ )
+ entry.add_to_hass(hass)
+
+ mock_setup_entry = AsyncMock(return_value=True)
+
+ mock_integration(hass, MockModule("comp", async_setup_entry=mock_setup_entry))
+ mock_entity_platform(hass, "config_flow.comp", None)
+
+ class TestFlow(config_entries.ConfigFlow):
+ """Test flow."""
+
+ VERSION = 1
+
+ async def async_step_import(self, user_input=None):
+ """Test not the user step."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+ return self.async_create_entry(title="title", data={"token": "supersecret"})
+
+ with patch.dict(config_entries.HANDLERS, {"comp": TestFlow, "beer": 5}):
+ await manager.flow.async_init(
+ "comp", context={"source": config_entries.SOURCE_IMPORT}
+ )
+ await hass.async_block_till_done()
+
+ assert len(mock_setup_entry.mock_calls) == 0
+
+
+async def test__async_current_entries_explict_skip_ignore(hass, manager):
+ """Test that _async_current_entries can explicitly include ignore."""
+ hass.config.components.add("comp")
+ entry = MockConfigEntry(
+ domain="comp",
+ state=config_entries.ENTRY_STATE_LOADED,
+ source=config_entries.SOURCE_IGNORE,
+ )
+ entry.add_to_hass(hass)
+
+ mock_setup_entry = AsyncMock(return_value=True)
+
+ mock_integration(hass, MockModule("comp", async_setup_entry=mock_setup_entry))
+ mock_entity_platform(hass, "config_flow.comp", None)
+
+ class TestFlow(config_entries.ConfigFlow):
+ """Test flow."""
+
+ VERSION = 1
+
+ async def async_step_import(self, user_input=None):
+ """Test not the user step."""
+ if self._async_current_entries(include_ignore=False):
+ return self.async_abort(reason="single_instance_allowed")
+ return self.async_create_entry(title="title", data={"token": "supersecret"})
+
+ with patch.dict(config_entries.HANDLERS, {"comp": TestFlow, "beer": 5}):
+ await manager.flow.async_init(
+ "comp", context={"source": config_entries.SOURCE_IMPORT}
+ )
+ await hass.async_block_till_done()
+
+ assert len(mock_setup_entry.mock_calls) == 1
+ p_hass, p_entry = mock_setup_entry.mock_calls[0][1]
+
+ assert p_hass is hass
+ assert p_entry.data == {"token": "supersecret"}
+
+
+async def test__async_current_entries_explict_include_ignore(hass, manager):
+ """Test that _async_current_entries can explicitly include ignore."""
+ hass.config.components.add("comp")
+ entry = MockConfigEntry(
+ domain="comp",
+ state=config_entries.ENTRY_STATE_LOADED,
+ source=config_entries.SOURCE_IGNORE,
+ )
+ entry.add_to_hass(hass)
+
+ mock_setup_entry = AsyncMock(return_value=True)
+
+ mock_integration(hass, MockModule("comp", async_setup_entry=mock_setup_entry))
+ mock_entity_platform(hass, "config_flow.comp", None)
+
+ class TestFlow(config_entries.ConfigFlow):
+ """Test flow."""
+
+ VERSION = 1
+
+ async def async_step_import(self, user_input=None):
+ """Test not the user step."""
+ if self._async_current_entries(include_ignore=True):
+ return self.async_abort(reason="single_instance_allowed")
+ return self.async_create_entry(title="title", data={"token": "supersecret"})
+
+ with patch.dict(config_entries.HANDLERS, {"comp": TestFlow, "beer": 5}):
+ await manager.flow.async_init(
+ "comp", context={"source": config_entries.SOURCE_IMPORT}
+ )
+ await hass.async_block_till_done()
+
+ assert len(mock_setup_entry.mock_calls) == 0
+
+
async def test_unignore_step_form(hass, manager):
"""Test that we can ignore flows that are in progress and have a unique ID, then rediscover them."""
async_setup_entry = AsyncMock(return_value=True)
diff --git a/tests/test_core.py b/tests/test_core.py
index dbed2b8c0bf5b5..d3283c14b84a08 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -36,6 +36,7 @@
from homeassistant.exceptions import (
InvalidEntityFormatError,
InvalidStateError,
+ MaxLengthExceeded,
ServiceNotFound,
)
import homeassistant.util.dt as dt_util
@@ -169,6 +170,30 @@ async def test_stage_shutdown(hass):
assert len(test_all) == 2
+async def test_shutdown_calls_block_till_done_after_shutdown_run_callback_threadsafe(
+ hass,
+):
+ """Ensure shutdown_run_callback_threadsafe is called before the final async_block_till_done."""
+ stop_calls = []
+
+ async def _record_block_till_done():
+ nonlocal stop_calls
+ stop_calls.append("async_block_till_done")
+
+ def _record_shutdown_run_callback_threadsafe(loop):
+ nonlocal stop_calls
+ stop_calls.append(("shutdown_run_callback_threadsafe", loop))
+
+ with patch.object(hass, "async_block_till_done", _record_block_till_done), patch(
+ "homeassistant.core.shutdown_run_callback_threadsafe",
+ _record_shutdown_run_callback_threadsafe,
+ ):
+ await hass.async_stop()
+
+ assert stop_calls[-2] == ("shutdown_run_callback_threadsafe", hass.loop)
+ assert stop_calls[-1] == "async_block_till_done"
+
+
async def test_pending_sheduler(hass):
"""Add a coro to pending tasks."""
call_count = []
@@ -355,6 +380,35 @@ def listener(_):
unsub()
+async def test_eventbus_filtered_listener(hass):
+ """Test we can prefilter events."""
+ calls = []
+
+ @ha.callback
+ def listener(event):
+ """Mock listener."""
+ calls.append(event)
+
+ @ha.callback
+ def filter(event):
+ """Mock filter."""
+ return not event.data["filtered"]
+
+ unsub = hass.bus.async_listen("test", listener, event_filter=filter)
+
+ hass.bus.async_fire("test", {"filtered": True})
+ await hass.async_block_till_done()
+
+ assert len(calls) == 0
+
+ hass.bus.async_fire("test", {"filtered": False})
+ await hass.async_block_till_done()
+
+ assert len(calls) == 1
+
+ unsub()
+
+
async def test_eventbus_unsubscribe_listener(hass):
"""Test unsubscribe listener from returned function."""
calls = []
@@ -471,6 +525,21 @@ async def coroutine_listener(event):
assert len(coroutine_calls) == 1
+async def test_eventbus_max_length_exceeded(hass):
+ """Test that an exception is raised when the max character length is exceeded."""
+
+ long_evt_name = (
+ "this_event_exceeds_the_max_character_length_even_with_the_new_limit"
+ )
+
+ with pytest.raises(MaxLengthExceeded) as exc_info:
+ hass.bus.async_fire(long_evt_name)
+
+ assert exc_info.value.property_name == "event_type"
+ assert exc_info.value.max_length == 64
+ assert exc_info.value.value == long_evt_name
+
+
def test_state_init():
"""Test state.init."""
with pytest.raises(InvalidEntityFormatError):
@@ -1270,41 +1339,6 @@ def test_valid_entity_id():
assert ha.valid_entity_id(valid), valid
-async def test_migration_base_url(hass, hass_storage):
- """Test that we migrate base url to internal/external url."""
- config = ha.Config(hass)
- stored = {"version": 1, "data": {}}
- hass_storage[ha.CORE_STORAGE_KEY] = stored
- with patch.object(hass.bus, "async_listen_once") as mock_listen:
- # Empty config
- await config.async_load()
- assert len(mock_listen.mock_calls) == 0
-
- # With just a name
- stored["data"] = {"location_name": "Test Name"}
- await config.async_load()
- assert len(mock_listen.mock_calls) == 1
-
- # With external url
- stored["data"]["external_url"] = "https://example.com"
- await config.async_load()
- assert len(mock_listen.mock_calls) == 1
-
- # Test that the event listener works
- assert mock_listen.mock_calls[0][1][0] == EVENT_HOMEASSISTANT_START
-
- # External
- hass.config.api = Mock(deprecated_base_url="https://loaded-example.com")
- await mock_listen.mock_calls[0][1][1](None)
- assert config.external_url == "https://loaded-example.com"
-
- # Internal
- for internal in ("http://hass.local", "http://192.168.1.100:8123"):
- hass.config.api = Mock(deprecated_base_url=internal)
- await mock_listen.mock_calls[0][1][1](None)
- assert config.internal_url == internal
-
-
async def test_additional_data_in_core_config(hass, hass_storage):
"""Test that we can handle additional data in core configuration."""
config = ha.Config(hass)
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
new file mode 100644
index 00000000000000..959f0846cae17e
--- /dev/null
+++ b/tests/test_exceptions.py
@@ -0,0 +1,46 @@
+"""Test to verify that Home Assistant exceptions work."""
+from homeassistant.exceptions import (
+ ConditionErrorContainer,
+ ConditionErrorIndex,
+ ConditionErrorMessage,
+)
+
+
+def test_conditionerror_format():
+ """Test ConditionError stringifiers."""
+ error1 = ConditionErrorMessage("test", "A test error")
+ assert str(error1) == "In 'test' condition: A test error"
+
+ error2 = ConditionErrorMessage("test", "Another error")
+ assert str(error2) == "In 'test' condition: Another error"
+
+ error_pos1 = ConditionErrorIndex("box", index=0, total=2, error=error1)
+ assert (
+ str(error_pos1)
+ == """In 'box' (item 1 of 2):
+ In 'test' condition: A test error"""
+ )
+
+ error_pos2 = ConditionErrorIndex("box", index=1, total=2, error=error2)
+ assert (
+ str(error_pos2)
+ == """In 'box' (item 2 of 2):
+ In 'test' condition: Another error"""
+ )
+
+ error_container1 = ConditionErrorContainer("box", errors=[error_pos1, error_pos2])
+ print(error_container1)
+ assert (
+ str(error_container1)
+ == """In 'box' (item 1 of 2):
+ In 'test' condition: A test error
+In 'box' (item 2 of 2):
+ In 'test' condition: Another error"""
+ )
+
+ error_pos3 = ConditionErrorIndex("box", index=0, total=1, error=error1)
+ assert (
+ str(error_pos3)
+ == """In 'box':
+ In 'test' condition: A test error"""
+ )
diff --git a/tests/test_loader.py b/tests/test_loader.py
index c1c27f56cb7ea9..8acc8a7de4f5c0 100644
--- a/tests/test_loader.py
+++ b/tests/test_loader.py
@@ -130,11 +130,67 @@ async def test_custom_component_name(hass):
async def test_log_warning_custom_component(hass, caplog):
"""Test that we log a warning when loading a custom component."""
- hass.components.test_standalone
- assert "You are using a custom integration for test_standalone" in caplog.text
+ await loader.async_get_integration(hass, "test_standalone")
+ assert "You are using a custom integration test_standalone" in caplog.text
await loader.async_get_integration(hass, "test")
- assert "You are using a custom integration for test " in caplog.text
+ assert "You are using a custom integration test " in caplog.text
+
+
+async def test_custom_integration_missing_version(hass, caplog):
+ """Test that we log a warning when custom integrations are missing a version."""
+ test_integration_1 = loader.Integration(
+ hass, "custom_components.test1", None, {"domain": "test1"}
+ )
+ test_integration_2 = loader.Integration(
+ hass,
+ "custom_components.test2",
+ None,
+ loader.manifest_from_legacy_module("test2", "custom_components.test2"),
+ )
+
+ with patch("homeassistant.loader.async_get_custom_components") as mock_get:
+ mock_get.return_value = {
+ "test1": test_integration_1,
+ "test2": test_integration_2,
+ }
+
+ await loader.async_get_integration(hass, "test1")
+ assert (
+ "No 'version' key in the manifest file for custom integration 'test1'."
+ in caplog.text
+ )
+
+ await loader.async_get_integration(hass, "test2")
+ assert (
+ "No 'version' key in the manifest file for custom integration 'test2'."
+ in caplog.text
+ )
+
+
+async def test_no_version_warning_for_none_custom_integrations(hass, caplog):
+ """Test that we do not log a warning when core integrations are missing a version."""
+ await loader.async_get_integration(hass, "hue")
+ assert (
+ "No 'version' key in the manifest file for custom integration 'hue'."
+ not in caplog.text
+ )
+
+
+async def test_custom_integration_version_not_valid(hass, caplog):
+ """Test that we log a warning when custom integrations have a invalid version."""
+ test_integration = loader.Integration(
+ hass, "custom_components.test", None, {"domain": "test", "version": "test"}
+ )
+
+ with patch("homeassistant.loader.async_get_custom_components") as mock_get:
+ mock_get.return_value = {"test": test_integration}
+
+ await loader.async_get_integration(hass, "test")
+ assert (
+ "'test' is not a valid version for custom integration 'test'."
+ in caplog.text
+ )
async def test_get_integration(hass):
@@ -154,7 +210,6 @@ async def test_get_integration_legacy(hass):
async def test_get_integration_custom_component(hass, enable_custom_integrations):
"""Test resolving integration."""
integration = await loader.async_get_integration(hass, "test_package")
- print(integration)
assert integration.get_component().DOMAIN == "test_package"
assert integration.name == "Test Package"
@@ -189,6 +244,7 @@ def test_integration_properties(hass):
{"manufacturer": "Signify", "modelName": "Philips hue bridge 2015"},
],
"mqtt": ["hue/discovery"],
+ "version": "1.0.0",
},
)
assert integration.name == "Philips Hue"
@@ -215,6 +271,7 @@ def test_integration_properties(hass):
assert integration.dependencies == ["test-dep"]
assert integration.requirements == ["test-req==1.0.0"]
assert integration.is_built_in is True
+ assert integration.version == "1.0.0"
integration = loader.Integration(
hass,
@@ -233,6 +290,7 @@ def test_integration_properties(hass):
assert integration.dhcp is None
assert integration.ssdp is None
assert integration.mqtt is None
+ assert integration.version is None
integration = loader.Integration(
hass,
diff --git a/tests/test_requirements.py b/tests/test_requirements.py
index 5f74e504de826d..acc83afeec29a0 100644
--- a/tests/test_requirements.py
+++ b/tests/test_requirements.py
@@ -84,9 +84,8 @@ async def test_install_missing_package(hass):
"""Test an install attempt on an existing package."""
with patch(
"homeassistant.util.package.install_package", return_value=False
- ) as mock_inst:
- with pytest.raises(RequirementsNotFound):
- await async_process_requirements(hass, "test_component", ["hello==1.0.0"])
+ ) as mock_inst, pytest.raises(RequirementsNotFound):
+ await async_process_requirements(hass, "test_component", ["hello==1.0.0"])
assert len(mock_inst.mock_calls) == 1
@@ -140,6 +139,69 @@ async def test_get_integration_with_requirements(hass):
]
+async def test_get_integration_with_missing_dependencies(hass):
+ """Check getting an integration with missing dependencies."""
+ hass.config.skip_pip = False
+ mock_integration(
+ hass,
+ MockModule("test_component_after_dep"),
+ )
+ mock_integration(
+ hass,
+ MockModule(
+ "test_component",
+ dependencies=["test_component_dep"],
+ partial_manifest={"after_dependencies": ["test_component_after_dep"]},
+ ),
+ )
+ mock_integration(
+ hass,
+ MockModule(
+ "test_custom_component",
+ dependencies=["test_component_dep"],
+ partial_manifest={"after_dependencies": ["test_component_after_dep"]},
+ ),
+ built_in=False,
+ )
+ with pytest.raises(loader.IntegrationNotFound):
+ await async_get_integration_with_requirements(hass, "test_component")
+ with pytest.raises(loader.IntegrationNotFound):
+ await async_get_integration_with_requirements(hass, "test_custom_component")
+
+
+async def test_get_built_in_integration_with_missing_after_dependencies(hass):
+ """Check getting a built_in integration with missing after_dependencies results in exception."""
+ hass.config.skip_pip = False
+ mock_integration(
+ hass,
+ MockModule(
+ "test_component",
+ partial_manifest={"after_dependencies": ["test_component_after_dep"]},
+ ),
+ built_in=True,
+ )
+ with pytest.raises(loader.IntegrationNotFound):
+ await async_get_integration_with_requirements(hass, "test_component")
+
+
+async def test_get_custom_integration_with_missing_after_dependencies(hass):
+ """Check getting a custom integration with missing after_dependencies."""
+ hass.config.skip_pip = False
+ mock_integration(
+ hass,
+ MockModule(
+ "test_custom_component",
+ partial_manifest={"after_dependencies": ["test_component_after_dep"]},
+ ),
+ built_in=False,
+ )
+ integration = await async_get_integration_with_requirements(
+ hass, "test_custom_component"
+ )
+ assert integration
+ assert integration.domain == "test_custom_component"
+
+
async def test_install_with_wheels_index(hass):
"""Test an install attempt with wheels index URL."""
hass.config.skip_pip = False
diff --git a/tests/test_setup.py b/tests/test_setup.py
index 539ed3f14428c3..72613722ca1f1f 100644
--- a/tests/test_setup.py
+++ b/tests/test_setup.py
@@ -1,9 +1,10 @@
"""Test component/platform setup."""
# pylint: disable=protected-access
import asyncio
+import datetime
import os
import threading
-from unittest.mock import Mock, patch
+from unittest.mock import AsyncMock, Mock, patch
import pytest
import voluptuous as vol
@@ -441,10 +442,14 @@ def test_all_work_done_before_start(self):
"""Test all init work done till start."""
call_order = []
- def component1_setup(hass, config):
+ async def component1_setup(hass, config):
"""Set up mock component."""
- discovery.discover(hass, "test_component2", {}, "test_component2", {})
- discovery.discover(hass, "test_component3", {}, "test_component3", {})
+ await discovery.async_discover(
+ hass, "test_component2", {}, "test_component2", {}
+ )
+ await discovery.async_discover(
+ hass, "test_component3", {}, "test_component3", {}
+ )
return True
def component_track_setup(hass, config):
@@ -453,7 +458,7 @@ def component_track_setup(hass, config):
return True
mock_integration(
- self.hass, MockModule("test_component1", setup=component1_setup)
+ self.hass, MockModule("test_component1", async_setup=component1_setup)
)
mock_integration(
@@ -596,3 +601,75 @@ async def test_integration_disabled(hass, caplog):
result = await setup.async_setup_component(hass, "test_component1", {})
assert not result
assert disabled_reason in caplog.text
+
+
+async def test_async_get_loaded_integrations(hass):
+ """Test we can enumerate loaded integations."""
+ hass.config.components.add("notbase")
+ hass.config.components.add("switch")
+ hass.config.components.add("notbase.switch")
+ hass.config.components.add("myintegration")
+ hass.config.components.add("device_tracker")
+ hass.config.components.add("device_tracker.other")
+ hass.config.components.add("myintegration.light")
+ assert setup.async_get_loaded_integrations(hass) == {
+ "other",
+ "switch",
+ "notbase",
+ "myintegration",
+ "device_tracker",
+ }
+
+
+async def test_integration_no_setup(hass, caplog):
+ """Test we fail integration setup without setup functions."""
+ mock_integration(
+ hass,
+ MockModule("test_integration_without_setup", setup=False),
+ )
+ result = await setup.async_setup_component(
+ hass, "test_integration_without_setup", {}
+ )
+ assert not result
+ assert "No setup or config entry setup function defined" in caplog.text
+
+
+async def test_integration_only_setup_entry(hass):
+ """Test we have an integration with only a setup entry method."""
+ mock_integration(
+ hass,
+ MockModule(
+ "test_integration_only_entry",
+ setup=False,
+ async_setup_entry=AsyncMock(return_value=True),
+ ),
+ )
+ assert await setup.async_setup_component(hass, "test_integration_only_entry", {})
+
+
+async def test_async_start_setup(hass):
+ """Test setup started context manager keeps track of setup times."""
+ with setup.async_start_setup(hass, ["august"]):
+ assert isinstance(
+ hass.data[setup.DATA_SETUP_STARTED]["august"], datetime.datetime
+ )
+ with setup.async_start_setup(hass, ["august"]):
+ assert isinstance(
+ hass.data[setup.DATA_SETUP_STARTED]["august_2"], datetime.datetime
+ )
+
+ assert "august" not in hass.data[setup.DATA_SETUP_STARTED]
+ assert isinstance(hass.data[setup.DATA_SETUP_TIME]["august"], datetime.timedelta)
+ assert "august_2" not in hass.data[setup.DATA_SETUP_TIME]
+
+
+async def test_async_start_setup_platforms(hass):
+ """Test setup started context manager keeps track of setup times for platforms."""
+ with setup.async_start_setup(hass, ["sensor.august"]):
+ assert isinstance(
+ hass.data[setup.DATA_SETUP_STARTED]["sensor.august"], datetime.datetime
+ )
+
+ assert "august" not in hass.data[setup.DATA_SETUP_STARTED]
+ assert isinstance(hass.data[setup.DATA_SETUP_TIME]["august"], datetime.timedelta)
+ assert "sensor" not in hass.data[setup.DATA_SETUP_TIME]
diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py
index 53949c20b068a9..5219212f1cf19e 100644
--- a/tests/test_util/aiohttp.py
+++ b/tests/test_util/aiohttp.py
@@ -245,9 +245,9 @@ async def read(self):
"""Return mock response."""
return self.response
- async def text(self, encoding="utf-8"):
+ async def text(self, encoding="utf-8", errors="strict"):
"""Return mock response as a string."""
- return self.response.decode(encoding)
+ return self.response.decode(encoding, errors=errors)
async def json(self, encoding="utf-8", content_type=None):
"""Return mock response as a json."""
diff --git a/tests/testing_config/custom_components/test/alarm_control_panel.py b/tests/testing_config/custom_components/test/alarm_control_panel.py
index 6535f5aa1f595e..864c99ec5df219 100644
--- a/tests/testing_config/custom_components/test/alarm_control_panel.py
+++ b/tests/testing_config/custom_components/test/alarm_control_panel.py
@@ -84,25 +84,25 @@ def supported_features(self) -> int:
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self._state = STATE_ALARM_ARMED_AWAY
- self.async_write_ha_state()
+ self.schedule_update_ha_state()
def alarm_arm_home(self, code=None):
"""Send arm home command."""
self._state = STATE_ALARM_ARMED_HOME
- self.async_write_ha_state()
+ self.schedule_update_ha_state()
def alarm_arm_night(self, code=None):
"""Send arm night command."""
self._state = STATE_ALARM_ARMED_NIGHT
- self.async_write_ha_state()
+ self.schedule_update_ha_state()
def alarm_disarm(self, code=None):
"""Send disarm command."""
if code == "1234":
self._state = STATE_ALARM_DISARMED
- self.async_write_ha_state()
+ self.schedule_update_ha_state()
def alarm_trigger(self, code=None):
"""Send alarm trigger command."""
self._state = STATE_ALARM_TRIGGERED
- self.async_write_ha_state()
+ self.schedule_update_ha_state()
diff --git a/tests/testing_config/custom_components/test/image_processing.py b/tests/testing_config/custom_components/test/image_processing.py
index a2004fe32bdab8..343c60a78fef03 100644
--- a/tests/testing_config/custom_components/test/image_processing.py
+++ b/tests/testing_config/custom_components/test/image_processing.py
@@ -41,7 +41,7 @@ def state(self):
return self._count
@property
- def device_state_attributes(self):
+ def extra_state_attributes(self):
"""Return device specific state attributes."""
return {"image": self._image}
diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py
index 863412fe747daa..84008d90c27f4e 100644
--- a/tests/testing_config/custom_components/test/light.py
+++ b/tests/testing_config/custom_components/test/light.py
@@ -37,4 +37,17 @@ class MockLight(MockToggleEntity, LightEntity):
"""Mock light class."""
brightness = None
+ supported_color_modes = None
supported_features = 0
+
+ color_mode = None
+
+ hs_color = None
+ xy_color = None
+ rgb_color = None
+ rgbw_color = None
+ rgbww_color = None
+
+ color_temp = None
+
+ white_value = None
diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py
index d467a93fd6214d..384db20d2d469c 100644
--- a/tests/testing_config/custom_components/test/sensor.py
+++ b/tests/testing_config/custom_components/test/sensor.py
@@ -4,7 +4,12 @@
Call init before using it in your tests to ensure clean test data.
"""
import homeassistant.components.sensor as sensor
-from homeassistant.const import PERCENTAGE, PRESSURE_HPA, SIGNAL_STRENGTH_DECIBELS
+from homeassistant.const import (
+ CONCENTRATION_PARTS_PER_MILLION,
+ PERCENTAGE,
+ PRESSURE_HPA,
+ SIGNAL_STRENGTH_DECIBELS,
+)
from tests.common import MockEntity
@@ -13,11 +18,12 @@
UNITS_OF_MEASUREMENT = {
sensor.DEVICE_CLASS_BATTERY: PERCENTAGE, # % of battery that is left
+ sensor.DEVICE_CLASS_CO: CONCENTRATION_PARTS_PER_MILLION, # ppm of CO concentration
+ sensor.DEVICE_CLASS_CO2: CONCENTRATION_PARTS_PER_MILLION, # ppm of CO2 concentration
sensor.DEVICE_CLASS_HUMIDITY: PERCENTAGE, # % of humidity in the air
sensor.DEVICE_CLASS_ILLUMINANCE: "lm", # current light level (lx/lm)
sensor.DEVICE_CLASS_SIGNAL_STRENGTH: SIGNAL_STRENGTH_DECIBELS, # signal strength (dB/dBm)
sensor.DEVICE_CLASS_TEMPERATURE: "C", # temperature (C/F)
- sensor.DEVICE_CLASS_TIMESTAMP: "hh:mm:ss", # timestamp (ISO8601)
sensor.DEVICE_CLASS_PRESSURE: PRESSURE_HPA, # pressure (hPa/mbar)
sensor.DEVICE_CLASS_POWER: "kW", # power (W/kW)
sensor.DEVICE_CLASS_CURRENT: "A", # current (A)
diff --git a/tests/util/test_async.py b/tests/util/test_async.py
index db088ada93e703..19413c57aaa882 100644
--- a/tests/util/test_async.py
+++ b/tests/util/test_async.py
@@ -50,7 +50,8 @@ def test_fire_coroutine_threadsafe_from_inside_event_loop(
def test_run_callback_threadsafe_from_inside_event_loop(mock_ident, _):
"""Testing calling run_callback_threadsafe from inside an event loop."""
callback = MagicMock()
- loop = MagicMock()
+
+ loop = Mock(spec=["call_soon_threadsafe"])
loop._thread_ident = None
mock_ident.return_value = 5
@@ -77,7 +78,7 @@ async def test_check_loop_async():
async def test_check_loop_async_integration(caplog):
"""Test check_loop detects when called from event loop from integration context."""
- with patch(
+ with pytest.raises(RuntimeError), patch(
"homeassistant.util.async_.extract_stack",
return_value=[
Mock(
@@ -106,7 +107,7 @@ async def test_check_loop_async_integration(caplog):
async def test_check_loop_async_custom(caplog):
"""Test check_loop detects when called from event loop with custom component context."""
- with patch(
+ with pytest.raises(RuntimeError), patch(
"homeassistant.util.async_.extract_stack",
return_value=[
Mock(
@@ -168,3 +169,46 @@ async def _increment_runs_if_in_time():
)
assert results == [2, 2, -1, -1]
+
+
+async def test_shutdown_run_callback_threadsafe(hass):
+ """Test we can shutdown run_callback_threadsafe."""
+ hasync.shutdown_run_callback_threadsafe(hass.loop)
+ callback = MagicMock()
+
+ with pytest.raises(RuntimeError):
+ hasync.run_callback_threadsafe(hass.loop, callback)
+
+
+async def test_run_callback_threadsafe(hass):
+ """Test run_callback_threadsafe runs code in the event loop."""
+ it_ran = False
+
+ def callback():
+ nonlocal it_ran
+ it_ran = True
+
+ assert hasync.run_callback_threadsafe(hass.loop, callback)
+ assert it_ran is False
+
+ # Verify that async_block_till_done will flush
+ # out the callback
+ await hass.async_block_till_done()
+ assert it_ran is True
+
+
+async def test_callback_is_always_scheduled(hass):
+ """Test run_callback_threadsafe always calls call_soon_threadsafe before checking for shutdown."""
+ # We have to check the shutdown state AFTER the callback is scheduled otherwise
+ # the function could continue on and the caller call `future.result()` after
+ # the point in the main thread where callbacks are no longer run.
+
+ callback = MagicMock()
+ hasync.shutdown_run_callback_threadsafe(hass.loop)
+
+ with patch.object(
+ hass.loop, "call_soon_threadsafe"
+ ) as mock_call_soon_threadsafe, pytest.raises(RuntimeError):
+ hasync.run_callback_threadsafe(hass.loop, callback)
+
+ mock_call_soon_threadsafe.assert_called_once()
diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py
index 7c4ca77fd79d77..5001301220136d 100644
--- a/tests/util/test_dt.py
+++ b/tests/util/test_dt.py
@@ -19,7 +19,7 @@ def test_get_time_zone_retrieves_valid_time_zone():
time_zone = dt_util.get_time_zone(TEST_TIME_ZONE)
assert time_zone is not None
- assert TEST_TIME_ZONE == time_zone.zone
+ assert time_zone.zone == TEST_TIME_ZONE
def test_get_time_zone_returns_none_for_garbage_time_zone():
@@ -177,14 +177,14 @@ def test_get_age():
def test_parse_time_expression():
"""Test parse_time_expression."""
- assert [x for x in range(60)] == dt_util.parse_time_expression("*", 0, 59)
- assert [x for x in range(60)] == dt_util.parse_time_expression(None, 0, 59)
+ assert list(range(60)) == dt_util.parse_time_expression("*", 0, 59)
+ assert list(range(60)) == dt_util.parse_time_expression(None, 0, 59)
- assert [x for x in range(0, 60, 5)] == dt_util.parse_time_expression("/5", 0, 59)
+ assert list(range(0, 60, 5)) == dt_util.parse_time_expression("/5", 0, 59)
assert [1, 2, 3] == dt_util.parse_time_expression([2, 1, 3], 0, 59)
- assert [x for x in range(24)] == dt_util.parse_time_expression("*", 0, 23)
+ assert list(range(24)) == dt_util.parse_time_expression("*", 0, 23)
assert [42] == dt_util.parse_time_expression(42, 0, 59)
assert [42] == dt_util.parse_time_expression("42", 0, 59)
diff --git a/tests/util/test_json.py b/tests/util/test_json.py
index 1cbaaae7d235c3..1d82f5972a3ba0 100644
--- a/tests/util/test_json.py
+++ b/tests/util/test_json.py
@@ -153,7 +153,7 @@ def default(self, o):
[State("mock_domain.mock_entity", "on", {"bad": bad_data})],
dump=partial(dumps, cls=MockJSONEncoder),
)
- == {"$[0](state: mock_domain.mock_entity).attributes.bad": bad_data}
+ == {"$[0](State: mock_domain.mock_entity).attributes.bad": bad_data}
)
assert (
@@ -161,5 +161,20 @@ def default(self, o):
[Event("bad_event", {"bad_attribute": bad_data})],
dump=partial(dumps, cls=MockJSONEncoder),
)
- == {"$[0](event: bad_event).data.bad_attribute": bad_data}
+ == {"$[0](Event: bad_event).data.bad_attribute": bad_data}
+ )
+
+ class BadData:
+ def __init__(self):
+ self.bla = bad_data
+
+ def as_dict(self):
+ return {"bla": self.bla}
+
+ assert (
+ find_paths_unserializable_data(
+ BadData(),
+ dump=partial(dumps, cls=MockJSONEncoder),
+ )
+ == {"$(BadData).bla": bad_data}
)
diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py
index 1a82c35e82d206..9277d92f3686fb 100644
--- a/tests/util/test_logging.py
+++ b/tests/util/test_logging.py
@@ -1,11 +1,13 @@
"""Test Home Assistant logging util methods."""
import asyncio
+from functools import partial
import logging
import queue
from unittest.mock import patch
import pytest
+from homeassistant.core import callback, is_callback
import homeassistant.util.logging as logging_util
@@ -80,3 +82,30 @@ async def job():
await hass.async_block_till_done()
assert "This is a bad coroutine" in caplog.text
assert "in test_async_create_catching_coro" in caplog.text
+
+
+def test_catch_log_exception():
+ """Test it is still a callback after wrapping including partial."""
+
+ async def async_meth():
+ pass
+
+ assert asyncio.iscoroutinefunction(
+ logging_util.catch_log_exception(partial(async_meth), lambda: None)
+ )
+
+ @callback
+ def callback_meth():
+ pass
+
+ assert is_callback(
+ logging_util.catch_log_exception(partial(callback_meth), lambda: None)
+ )
+
+ def sync_meth():
+ pass
+
+ wrapped = logging_util.catch_log_exception(partial(sync_meth), lambda: None)
+
+ assert not is_callback(wrapped)
+ assert not asyncio.iscoroutinefunction(wrapped)
diff --git a/tests/util/test_network.py b/tests/util/test_network.py
index 2cd710e1d6c467..089ef5e0ab8e87 100644
--- a/tests/util/test_network.py
+++ b/tests/util/test_network.py
@@ -33,6 +33,12 @@ def test_is_link_local():
assert not network_util.is_link_local(ip_address("127.0.0.1"))
+def test_is_invalid():
+ """Test invalid address."""
+ assert network_util.is_invalid(ip_address("0.0.0.0"))
+ assert not network_util.is_invalid(ip_address("127.0.0.1"))
+
+
def test_is_local():
"""Test local addresses."""
assert network_util.is_local(ip_address("192.168.0.1"))
diff --git a/tests/util/test_package.py b/tests/util/test_package.py
index 0c251662444406..494fe5fa11fbe8 100644
--- a/tests/util/test_package.py
+++ b/tests/util/test_package.py
@@ -239,10 +239,55 @@ async def test_async_get_user_site(mock_env_copy):
def test_check_package_global():
"""Test for an installed package."""
- installed_package = list(pkg_resources.working_set)[0].project_name
+ first_package = list(pkg_resources.working_set)[0]
+ installed_package = first_package.project_name
+ installed_version = first_package.version
+
assert package.is_installed(installed_package)
+ assert package.is_installed(f"{installed_package}=={installed_version}")
+ assert package.is_installed(f"{installed_package}>={installed_version}")
+ assert package.is_installed(f"{installed_package}<={installed_version}")
+ assert not package.is_installed(f"{installed_package}<{installed_version}")
+
+
+def test_check_package_version_does_not_match():
+ """Test for version mismatch."""
+ installed_package = list(pkg_resources.working_set)[0].project_name
+ assert not package.is_installed(f"{installed_package}==999.999.999")
+ assert not package.is_installed(f"{installed_package}>=999.999.999")
def test_check_package_zip():
"""Test for an installed zip package."""
assert not package.is_installed(TEST_ZIP_REQ)
+
+
+def test_get_distribution_falls_back_to_version():
+ """Test for get_distribution failing and fallback to version."""
+ first_package = list(pkg_resources.working_set)[0]
+ installed_package = first_package.project_name
+ installed_version = first_package.version
+
+ with patch(
+ "homeassistant.util.package.pkg_resources.get_distribution",
+ side_effect=pkg_resources.ExtractionError,
+ ):
+ assert package.is_installed(installed_package)
+ assert package.is_installed(f"{installed_package}=={installed_version}")
+ assert package.is_installed(f"{installed_package}>={installed_version}")
+ assert package.is_installed(f"{installed_package}<={installed_version}")
+ assert not package.is_installed(f"{installed_package}<{installed_version}")
+
+
+def test_check_package_previous_failed_install():
+ """Test for when a previously install package failed and left cruft behind."""
+ first_package = list(pkg_resources.working_set)[0]
+ installed_package = first_package.project_name
+ installed_version = first_package.version
+
+ with patch(
+ "homeassistant.util.package.pkg_resources.get_distribution",
+ side_effect=pkg_resources.ExtractionError,
+ ), patch("homeassistant.util.package.version", return_value=None):
+ assert not package.is_installed(installed_package)
+ assert not package.is_installed(f"{installed_package}=={installed_version}")
diff --git a/tests/util/test_percentage.py b/tests/util/test_percentage.py
index 4ad28f8567c7cb..37e4c6d9615c3c 100644
--- a/tests/util/test_percentage.py
+++ b/tests/util/test_percentage.py
@@ -28,15 +28,6 @@
LARGE_ORDERED_LIST = [SPEED_1, SPEED_2, SPEED_3, SPEED_4, SPEED_5, SPEED_6, SPEED_7]
-async def test_ordered_list_percentage_round_trip():
- """Test we can round trip."""
- for ordered_list in (SMALL_ORDERED_LIST, LARGE_ORDERED_LIST):
- for i in range(1, 100):
- ordered_list_item_to_percentage(
- ordered_list, percentage_to_ordered_list_item(ordered_list, i)
- ) == i
-
-
async def test_ordered_list_item_to_percentage():
"""Test percentage of an item in an ordered list."""
@@ -156,3 +147,34 @@ async def test_percentage_to_ranged_value_small():
assert math.ceil(percentage_to_ranged_value(range, 66)) == 4
assert math.ceil(percentage_to_ranged_value(range, 83)) == 5
assert math.ceil(percentage_to_ranged_value(range, 100)) == 6
+
+
+async def test_ranged_value_to_percentage_starting_at_one():
+ """Test a range that starts with 1."""
+ range = (1, 4)
+
+ assert ranged_value_to_percentage(range, 1) == 25
+ assert ranged_value_to_percentage(range, 2) == 50
+ assert ranged_value_to_percentage(range, 3) == 75
+ assert ranged_value_to_percentage(range, 4) == 100
+
+
+async def test_ranged_value_to_percentage_starting_high():
+ """Test a range that does not start with 1."""
+ range = (101, 255)
+
+ assert ranged_value_to_percentage(range, 101) == 0
+ assert ranged_value_to_percentage(range, 139) == 25
+ assert ranged_value_to_percentage(range, 178) == 50
+ assert ranged_value_to_percentage(range, 217) == 75
+ assert ranged_value_to_percentage(range, 255) == 100
+
+
+async def test_ranged_value_to_percentage_starting_zero():
+ """Test a range that starts with 0."""
+ range = (0, 3)
+
+ assert ranged_value_to_percentage(range, 0) == 25
+ assert ranged_value_to_percentage(range, 1) == 50
+ assert ranged_value_to_percentage(range, 2) == 75
+ assert ranged_value_to_percentage(range, 3) == 100
diff --git a/tests/util/test_timeout.py b/tests/util/test_timeout.py
index 39ec8d916bdd67..227dde1923453b 100644
--- a/tests/util/test_timeout.py
+++ b/tests/util/test_timeout.py
@@ -1,5 +1,6 @@
"""Test Home Assistant timeout handler."""
import asyncio
+from contextlib import suppress
import time
import pytest
@@ -232,11 +233,9 @@ async def test_mix_zone_timeout():
timeout = TimeoutManager()
async with timeout.async_timeout(0.1):
- try:
+ with suppress(asyncio.TimeoutError):
async with timeout.async_timeout(0.2, "test"):
await asyncio.sleep(0.4)
- except asyncio.TimeoutError:
- pass
async def test_mix_zone_timeout_trigger_global():
@@ -245,11 +244,9 @@ async def test_mix_zone_timeout_trigger_global():
with pytest.raises(asyncio.TimeoutError):
async with timeout.async_timeout(0.1):
- try:
+ with suppress(asyncio.TimeoutError):
async with timeout.async_timeout(0.1, "test"):
await asyncio.sleep(0.3)
- except asyncio.TimeoutError:
- pass
await asyncio.sleep(0.3)
@@ -259,11 +256,9 @@ async def test_mix_zone_timeout_trigger_global_cool_down():
timeout = TimeoutManager()
async with timeout.async_timeout(0.1, cool_down=0.3):
- try:
+ with suppress(asyncio.TimeoutError):
async with timeout.async_timeout(0.1, "test"):
await asyncio.sleep(0.3)
- except asyncio.TimeoutError:
- pass
await asyncio.sleep(0.2)
@@ -301,11 +296,9 @@ async def test_simple_zone_timeout_freeze_without_timeout_exeption():
with pytest.raises(asyncio.TimeoutError):
async with timeout.async_timeout(0.1):
- try:
+ with suppress(RuntimeError):
async with timeout.async_freeze("test"):
raise RuntimeError()
- except RuntimeError:
- pass
await asyncio.sleep(0.4)
@@ -316,10 +309,8 @@ async def test_simple_zone_timeout_zone_with_timeout_exeption():
with pytest.raises(asyncio.TimeoutError):
async with timeout.async_timeout(0.1):
- try:
+ with suppress(RuntimeError):
async with timeout.async_timeout(0.3, "test"):
raise RuntimeError()
- except RuntimeError:
- pass
await asyncio.sleep(0.3)
diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py
index e0e4524a2f2e53..74abfef452faed 100644
--- a/tests/util/test_unit_system.py
+++ b/tests/util/test_unit_system.py
@@ -171,11 +171,11 @@ def test_pressure_to_imperial():
def test_properties():
"""Test the unit properties are returned as expected."""
- assert LENGTH_KILOMETERS == METRIC_SYSTEM.length_unit
- assert TEMP_CELSIUS == METRIC_SYSTEM.temperature_unit
- assert MASS_GRAMS == METRIC_SYSTEM.mass_unit
- assert VOLUME_LITERS == METRIC_SYSTEM.volume_unit
- assert PRESSURE_PA == METRIC_SYSTEM.pressure_unit
+ assert METRIC_SYSTEM.length_unit == LENGTH_KILOMETERS
+ assert METRIC_SYSTEM.temperature_unit == TEMP_CELSIUS
+ assert METRIC_SYSTEM.mass_unit == MASS_GRAMS
+ assert METRIC_SYSTEM.volume_unit == VOLUME_LITERS
+ assert METRIC_SYSTEM.pressure_unit == PRESSURE_PA
def test_is_metric():
diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py
index 34097287bc3734..2b86b3c50e9c5e 100644
--- a/tests/util/yaml/test_init.py
+++ b/tests/util/yaml/test_init.py
@@ -1,6 +1,5 @@
"""Test Home Assistant yaml loader."""
import io
-import logging
import os
import unittest
from unittest.mock import patch
@@ -15,19 +14,11 @@
from tests.common import get_test_config_dir, patch_yaml_files
-@pytest.fixture(autouse=True)
-def mock_credstash():
- """Mock credstash so it doesn't connect to the internet."""
- with patch.object(yaml_loader, "credstash") as mock_credstash:
- mock_credstash.getSecret.return_value = None
- yield mock_credstash
-
-
def test_simple_list():
"""Test simple list."""
conf = "config:\n - simple\n - list"
with io.StringIO(conf) as file:
- doc = yaml_loader.yaml.safe_load(file)
+ doc = yaml_loader.yaml.load(file, Loader=yaml_loader.SafeLineLoader)
assert doc["config"] == ["simple", "list"]
@@ -35,7 +26,7 @@ def test_simple_dict():
"""Test simple dict."""
conf = "key: value"
with io.StringIO(conf) as file:
- doc = yaml_loader.yaml.safe_load(file)
+ doc = yaml_loader.yaml.load(file, Loader=yaml_loader.SafeLineLoader)
assert doc["key"] == "value"
@@ -58,7 +49,7 @@ def test_environment_variable():
os.environ["PASSWORD"] = "secret_password"
conf = "password: !env_var PASSWORD"
with io.StringIO(conf) as file:
- doc = yaml_loader.yaml.safe_load(file)
+ doc = yaml_loader.yaml.load(file, Loader=yaml_loader.SafeLineLoader)
assert doc["password"] == "secret_password"
del os.environ["PASSWORD"]
@@ -67,16 +58,15 @@ def test_environment_variable_default():
"""Test config file with default value for environment variable."""
conf = "password: !env_var PASSWORD secret_password"
with io.StringIO(conf) as file:
- doc = yaml_loader.yaml.safe_load(file)
+ doc = yaml_loader.yaml.load(file, Loader=yaml_loader.SafeLineLoader)
assert doc["password"] == "secret_password"
def test_invalid_environment_variable():
"""Test config file with no environment variable sat."""
conf = "password: !env_var PASSWORD"
- with pytest.raises(HomeAssistantError):
- with io.StringIO(conf) as file:
- yaml_loader.yaml.safe_load(file)
+ with pytest.raises(HomeAssistantError), io.StringIO(conf) as file:
+ yaml_loader.yaml.load(file, Loader=yaml_loader.SafeLineLoader)
def test_include_yaml():
@@ -84,13 +74,13 @@ def test_include_yaml():
with patch_yaml_files({"test.yaml": "value"}):
conf = "key: !include test.yaml"
with io.StringIO(conf) as file:
- doc = yaml_loader.yaml.safe_load(file)
+ doc = yaml_loader.yaml.load(file, Loader=yaml_loader.SafeLineLoader)
assert doc["key"] == "value"
with patch_yaml_files({"test.yaml": None}):
conf = "key: !include test.yaml"
with io.StringIO(conf) as file:
- doc = yaml_loader.yaml.safe_load(file)
+ doc = yaml_loader.yaml.load(file, Loader=yaml_loader.SafeLineLoader)
assert doc["key"] == {}
@@ -102,7 +92,7 @@ def test_include_dir_list(mock_walk):
with patch_yaml_files({"/test/one.yaml": "one", "/test/two.yaml": "two"}):
conf = "key: !include_dir_list /test"
with io.StringIO(conf) as file:
- doc = yaml_loader.yaml.safe_load(file)
+ doc = yaml_loader.yaml.load(file, Loader=yaml_loader.SafeLineLoader)
assert doc["key"] == sorted(["one", "two"])
@@ -127,7 +117,7 @@ def test_include_dir_list_recursive(mock_walk):
assert (
".ignore" in mock_walk.return_value[0][1]
), "Expecting .ignore in here"
- doc = yaml_loader.yaml.safe_load(file)
+ doc = yaml_loader.yaml.load(file, Loader=yaml_loader.SafeLineLoader)
assert "tmp2" in mock_walk.return_value[0][1]
assert ".ignore" not in mock_walk.return_value[0][1]
assert sorted(doc["key"]) == sorted(["zero", "one", "two"])
@@ -144,7 +134,7 @@ def test_include_dir_named(mock_walk):
conf = "key: !include_dir_named /test"
correct = {"first": "one", "second": "two"}
with io.StringIO(conf) as file:
- doc = yaml_loader.yaml.safe_load(file)
+ doc = yaml_loader.yaml.load(file, Loader=yaml_loader.SafeLineLoader)
assert doc["key"] == correct
@@ -170,7 +160,7 @@ def test_include_dir_named_recursive(mock_walk):
assert (
".ignore" in mock_walk.return_value[0][1]
), "Expecting .ignore in here"
- doc = yaml_loader.yaml.safe_load(file)
+ doc = yaml_loader.yaml.load(file, Loader=yaml_loader.SafeLineLoader)
assert "tmp2" in mock_walk.return_value[0][1]
assert ".ignore" not in mock_walk.return_value[0][1]
assert doc["key"] == correct
@@ -186,7 +176,7 @@ def test_include_dir_merge_list(mock_walk):
):
conf = "key: !include_dir_merge_list /test"
with io.StringIO(conf) as file:
- doc = yaml_loader.yaml.safe_load(file)
+ doc = yaml_loader.yaml.load(file, Loader=yaml_loader.SafeLineLoader)
assert sorted(doc["key"]) == sorted(["one", "two", "three"])
@@ -211,7 +201,7 @@ def test_include_dir_merge_list_recursive(mock_walk):
assert (
".ignore" in mock_walk.return_value[0][1]
), "Expecting .ignore in here"
- doc = yaml_loader.yaml.safe_load(file)
+ doc = yaml_loader.yaml.load(file, Loader=yaml_loader.SafeLineLoader)
assert "tmp2" in mock_walk.return_value[0][1]
assert ".ignore" not in mock_walk.return_value[0][1]
assert sorted(doc["key"]) == sorted(["one", "two", "three", "four"])
@@ -230,7 +220,7 @@ def test_include_dir_merge_named(mock_walk):
with patch_yaml_files(files):
conf = "key: !include_dir_merge_named /test"
with io.StringIO(conf) as file:
- doc = yaml_loader.yaml.safe_load(file)
+ doc = yaml_loader.yaml.load(file, Loader=yaml_loader.SafeLineLoader)
assert doc["key"] == {"key1": "one", "key2": "two", "key3": "three"}
@@ -255,7 +245,7 @@ def test_include_dir_merge_named_recursive(mock_walk):
assert (
".ignore" in mock_walk.return_value[0][1]
), "Expecting .ignore in here"
- doc = yaml_loader.yaml.safe_load(file)
+ doc = yaml_loader.yaml.load(file, Loader=yaml_loader.SafeLineLoader)
assert "tmp2" in mock_walk.return_value[0][1]
assert ".ignore" not in mock_walk.return_value[0][1]
assert doc["key"] == {
@@ -287,25 +277,11 @@ def test_dump_unicode():
FILES = {}
-def load_yaml(fname, string):
+def load_yaml(fname, string, secrets=None):
"""Write a string to file and return the parsed yaml."""
FILES[fname] = string
with patch_yaml_files(FILES):
- return load_yaml_config_file(fname)
-
-
-class FakeKeyring:
- """Fake a keyring class."""
-
- def __init__(self, secrets_dict):
- """Store keyring dictionary."""
- self._secrets = secrets_dict
-
- # pylint: disable=protected-access
- def get_password(self, domain, name):
- """Retrieve password."""
- assert domain == yaml._SECRET_NAMESPACE
- return self._secrets.get(name)
+ return load_yaml_config_file(fname, secrets)
class TestSecrets(unittest.TestCase):
@@ -316,7 +292,6 @@ class TestSecrets(unittest.TestCase):
def setUp(self):
"""Create & load secrets file."""
config_dir = get_test_config_dir()
- yaml.clear_secret_cache()
self._yaml_path = os.path.join(config_dir, YAML_CONFIG_FILE)
self._secret_path = os.path.join(config_dir, yaml.SECRET_YAML)
self._sub_folder_path = os.path.join(config_dir, "subFolder")
@@ -338,11 +313,11 @@ def setUp(self):
" username: !secret comp1_un\n"
" password: !secret comp1_pw\n"
"",
+ yaml_loader.Secrets(config_dir),
)
def tearDown(self):
"""Clean up secrets."""
- yaml.clear_secret_cache()
FILES.clear()
def test_secrets_from_yaml(self):
@@ -364,6 +339,7 @@ def test_secrets_from_parent_folder(self):
" username: !secret comp1_un\n"
" password: !secret comp1_pw\n"
"",
+ yaml_loader.Secrets(get_test_config_dir()),
)
assert expected == self._yaml["http"]
@@ -382,6 +358,7 @@ def test_secret_overrides_parent(self):
" username: !secret comp1_un\n"
" password: !secret comp1_pw\n"
"",
+ yaml_loader.Secrets(get_test_config_dir()),
)
assert expected == self._yaml["http"]
@@ -395,27 +372,6 @@ def test_secrets_from_unrelated_fails(self):
"http:\n api_password: !secret test",
)
- def test_secrets_keyring(self):
- """Test keyring fallback & get_password."""
- yaml_loader.keyring = None # Ensure its not there
- yaml_str = "http:\n api_password: !secret http_pw_keyring"
- with pytest.raises(HomeAssistantError):
- load_yaml(self._yaml_path, yaml_str)
-
- yaml_loader.keyring = FakeKeyring({"http_pw_keyring": "yeah"})
- _yaml = load_yaml(self._yaml_path, yaml_str)
- assert {"http": {"api_password": "yeah"}} == _yaml
-
- @patch.object(yaml_loader, "credstash")
- def test_secrets_credstash(self, mock_credstash):
- """Test credstash fallback & get_password."""
- mock_credstash.getSecret.return_value = "yeah"
- yaml_str = "http:\n api_password: !secret http_pw_credstash"
- _yaml = load_yaml(self._yaml_path, yaml_str)
- log = logging.getLogger()
- log.error(_yaml["http"])
- assert {"api_password": "yeah"} == _yaml["http"]
-
def test_secrets_logger_removed(self):
"""Ensure logger: debug was removed."""
with pytest.raises(HomeAssistantError):
@@ -424,9 +380,12 @@ def test_secrets_logger_removed(self):
@patch("homeassistant.util.yaml.loader._LOGGER.error")
def test_bad_logger_value(self, mock_error):
"""Ensure logger: debug was removed."""
- yaml.clear_secret_cache()
load_yaml(self._secret_path, "logger: info\npw: abc")
- load_yaml(self._yaml_path, "api_password: !secret pw")
+ load_yaml(
+ self._yaml_path,
+ "api_password: !secret pw",
+ yaml_loader.Secrets(get_test_config_dir()),
+ )
assert mock_error.call_count == 1, "Expected an error about logger: value"
def test_secrets_are_not_dict(self):
@@ -434,7 +393,6 @@ def test_secrets_are_not_dict(self):
FILES[
self._secret_path
] = "- http_pw: pwhttp\n comp1_un: un1\n comp1_pw: pw1\n"
- yaml.clear_secret_cache()
with pytest.raises(HomeAssistantError):
load_yaml(
self._yaml_path,
@@ -463,6 +421,15 @@ def test_duplicate_key(caplog):
assert "contains duplicate key" in caplog.text
+def test_no_recursive_secrets(caplog):
+ """Test that loading of secrets from the secrets file fails correctly."""
+ files = {YAML_CONFIG_FILE: "key: !secret a", yaml.SECRET_YAML: "a: 1\nb: !secret a"}
+ with patch_yaml_files(files), pytest.raises(HomeAssistantError) as e:
+ load_yaml_config_file(YAML_CONFIG_FILE)
+
+ assert e.value.args == ("Secrets not supported in this YAML file",)
+
+
def test_input_class():
"""Test input class."""
input = yaml_loader.Input("hello")
diff --git a/tox.ini b/tox.ini
index 9c9963c28eed03..0c58d2356b9161 100644
--- a/tox.ini
+++ b/tox.ini
@@ -5,6 +5,8 @@ ignore_basepython_conflict = True
[testenv]
basepython = {env:PYTHON3_PATH:python3}
+# pip version duplicated in homeassistant/package_constraints.txt
+pip_version = pip>=8.0.3,<20.3
commands =
pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar {posargs}
{toxinidir}/script/check_dirty
@@ -39,6 +41,6 @@ commands =
[testenv:typing]
deps =
- -r{toxinidir}/requirements_test.txt
+ -r{toxinidir}/requirements_test_all.txt
commands =
- pre-commit run mypy {posargs: --all-files}
+ mypy homeassistant